From 2c81897ffc8eb36dc50e6c9cc8a33261bf7aa632 Mon Sep 17 00:00:00 2001 From: wp_xxyyzz Date: Mon, 5 Jan 2015 23:32:49 +0000 Subject: [PATCH] fpspreadsheet: Avoid duplicate code in TsWorkbookChartSource. Fix crashes of the chartsource when used worksheets are renamed or deleted. More refined notification of visual controls. git-svn-id: https://svn.code.sf.net/p/lazarus-ccr/svn@3867 8e941d3f-bd1b-0410-a28a-d453659cc2b4 --- .../fpsctrls_no_install/demo_ctrls.lpi | 7 +- .../fpsctrls_no_install/demo_ctrls.lpr | 2 +- .../examples/fpsgrid_no_install/fpsgrid.lpi | 7 +- .../examples/fpsgrid_no_install/fpsgrid.lpr | 2 +- components/fpspreadsheet/fpspreadsheet.pas | 164 ++++++++- .../fpspreadsheet/fpspreadsheetchart.pas | 310 +++++++++++------- .../fpspreadsheet/fpspreadsheetctrls.pas | 53 ++- .../fpspreadsheet/fpspreadsheetgrid.pas | 3 +- 8 files changed, 409 insertions(+), 139 deletions(-) diff --git a/components/fpspreadsheet/examples/fpsctrls_no_install/demo_ctrls.lpi b/components/fpspreadsheet/examples/fpsctrls_no_install/demo_ctrls.lpi index dda4a8d9a..d1b1dc218 100644 --- a/components/fpspreadsheet/examples/fpsctrls_no_install/demo_ctrls.lpi +++ b/components/fpspreadsheet/examples/fpsctrls_no_install/demo_ctrls.lpi @@ -28,10 +28,13 @@ - + - + + + + diff --git a/components/fpspreadsheet/examples/fpsctrls_no_install/demo_ctrls.lpr b/components/fpspreadsheet/examples/fpsctrls_no_install/demo_ctrls.lpr index 1d3ae70d5..91629d122 100644 --- a/components/fpspreadsheet/examples/fpsctrls_no_install/demo_ctrls.lpr +++ b/components/fpspreadsheet/examples/fpsctrls_no_install/demo_ctrls.lpr @@ -7,7 +7,7 @@ uses cthreads, {$ENDIF}{$ENDIF} Interfaces, // this includes the LCL widgetset - Forms, main; + Forms, tachartlazaruspkg, main; {$R *.res} diff --git a/components/fpspreadsheet/examples/fpsgrid_no_install/fpsgrid.lpi b/components/fpspreadsheet/examples/fpsgrid_no_install/fpsgrid.lpi index 3b346347c..b81e9bb7e 100644 --- a/components/fpspreadsheet/examples/fpsgrid_no_install/fpsgrid.lpi +++ b/components/fpspreadsheet/examples/fpsgrid_no_install/fpsgrid.lpi @@ -51,10 +51,13 @@ - + - + + + + diff --git a/components/fpspreadsheet/examples/fpsgrid_no_install/fpsgrid.lpr b/components/fpspreadsheet/examples/fpsgrid_no_install/fpsgrid.lpr index 75994f791..481d4c295 100644 --- a/components/fpspreadsheet/examples/fpsgrid_no_install/fpsgrid.lpr +++ b/components/fpspreadsheet/examples/fpsgrid_no_install/fpsgrid.lpr @@ -7,7 +7,7 @@ uses cthreads, {$ENDIF}{$ENDIF} Interfaces, // this includes the LCL widgetset - Forms, mainfrm + Forms, tachartlazaruspkg, mainfrm { you can add units after this }; {$R *.res} diff --git a/components/fpspreadsheet/fpspreadsheet.pas b/components/fpspreadsheet/fpspreadsheet.pas index 0361ab78c..3ef824e21 100755 --- a/components/fpspreadsheet/fpspreadsheet.pas +++ b/components/fpspreadsheet/fpspreadsheet.pas @@ -969,8 +969,10 @@ type FOnWriteCellData: TsWorkbookWriteCellDataEvent; FOnReadCellData: TsWorkbookReadCellDataEvent; FOnChangeWorksheet: TsWorksheetEvent; + FOnRenameWorksheet: TsWorksheetEvent; FOnAddWorksheet: TsWorksheetEvent; FOnRemoveWorksheet: TsRemoveWorksheetEvent; + FOnRemovingWorksheet: TsWorksheetEvent; FOnSelectWorksheet: TsWorksheetEvent; FOnChangePalette: TNotifyEvent; FFileName: String; @@ -1033,6 +1035,14 @@ type function ValidWorksheetName(var AName: String; ReplaceDuplicateName: Boolean = false): Boolean; + { String-to-cell/range conversion } + function TryStrToCell(AText: String; out AWorksheet: TsWorksheet; + out ARow,ACol: Cardinal; AListSeparator: Char = #0): Boolean; + function TryStrToCellRange(AText: String; out AWorksheet: TsWorksheet; + out ARange: TsCellRange; AListSeparator: Char = #0): Boolean; + function TryStrToCellRanges(AText: String; out AWorksheet: TsWorksheet; + out ARanges: TsCellRangeArray; AListSeparator: Char = #0): Boolean; + { Font handling } function AddFont(const AFontName: String; ASize: Single; AStyle: TsFontStyles; AColor: TsColor): Integer; overload; @@ -1093,8 +1103,12 @@ type property OnChangeWorksheet: TsWorksheetEvent read FOnChangeWorksheet write FOnChangeWorksheet; {@@ This event fires whenever a workbook is loaded } property OnOpenWorkbook: TNotifyEvent read FOnOpenWorkbook write FOnOpenWorkbook; - {@@ This event fires when a worksheet is deleted } + {@@ This event fires whenever a worksheet is renamed } + property OnRenameWorksheet: TsWorksheetEvent read FOnRenameWorksheet write FOnRenameWorksheet; + {@@ This event fires AFTER a worksheet has been deleted } property OnRemoveWorksheet: TsRemoveWorksheetEvent read FOnRemoveWorksheet write FOnRemoveWorksheet; + {@@ This event fires BEFORE a worksheet is deleted } + property OnRemovingWorksheet: TsWorksheetEvent read FOnRemovingWorksheet write FOnRemovingWorksheet; {@@ This event fires when a worksheet is made "active"} property OnSelectWorksheet: TsWorksheetEvent read FOnSelectWorksheet write FOnSelectWorksheet; {@@ This event allows to provide external cell data for writing to file, @@ -3597,7 +3611,7 @@ begin begin FName := AName; if (FWorkbook.FLockCount = 0) and Assigned(FWorkbook.FOnChangeWorksheet) then - FWorkbook.FOnChangeWorksheet(FWorkbook, self); + FWorkbook.FOnRenameWorksheet(FWorkbook, self); end; end; @@ -7098,6 +7112,8 @@ begin i := GetWorksheetIndex(AWorksheet); if (i <> -1) and (AWorksheet <> nil) then begin + if Assigned(FOnRemovingWorksheet) then + FOnRemovingWorksheet(self, AWorksheet); FWorksheets.Delete(i); AWorksheet.Free; if Assigned(FOnRemoveWorksheet) then @@ -7199,6 +7215,150 @@ begin end; *) +{ String-to-cell/range conversion } + +{@@ ---------------------------------------------------------------------------- + Analyses a string which can contain an array of cell ranges along with a + worksheet name. Extracts the worksheet (if missing the "active" worksheet of + the workbook is returned) and the cell's row and column indexes. + + @param AText General cell range string in Excel notation, + i.e. worksheet name + ! + cell in A1 notation. + Example: Sheet1!A1:A10; A1:A10 or A1 are valid as well. + @param AWorksheet Pointer to the worksheet referred to by AText. If AText + does not contain the worksheet name, the active worksheet + of the workbook is returned + @param ARow, ACol Zero-based row and column index of the cell identified + by ATest. If AText contains one ore more cell ranges + then the upper left corner of the first range is returned. + @param AListSeparator Character to separate the cell blocks in the text + If #0 then the ListSeparator of the workbook's FormatSettings + is used. + @returns TRUE if AText is a valid list of cell ranges, FALSE if not. If the + result is FALSE then AWorksheet, ARow and ACol may have unpredictable + values. +-------------------------------------------------------------------------------} +function TsWorkbook.TryStrToCell(AText: String; out AWorksheet: TsWorksheet; + out ARow,ACol: Cardinal; AListSeparator: Char = #0): Boolean; +var + ranges: TsCellRangeArray; +begin + Result := TryStrToCellRanges(AText, AWorksheet, ranges, AListSeparator); + if Result then + begin + ARow := ranges[0].Row1; + ACol := ranges[0].Col1; + end; +end; + +{@@ ---------------------------------------------------------------------------- + Analyses a string which can contain an array of cell ranges along with a + worksheet name. Extracts the worksheet (if missing the "active" worksheet of + the workbook is returned) and the cell range (or the first cell range, if there + are several ranges). + + @param AText General cell range string in Excel notation, + i.e. worksheet name + ! + cell in A1 notation. + Example: Sheet1!A1:A10; A1:A10 or A1 are valid as well. + @param AWorksheet Pointer to the worksheet referred to by AText. If AText + does not contain the worksheet name, the active worksheet + of the workbook is returned + @param ARange TsCellRange records identifying the cell block. If AText + contains several cell ranges the first one is returned. + @param AListSeparator Character to separate the cell blocks in the text + If #0 then the ListSeparator of the workbook's FormatSettings + is used. + @returns TRUE if AText is a valid cell range, FALSE if not. If the + result is FALSE then AWorksheet and ARange may have unpredictable + values. +-------------------------------------------------------------------------------} +function TsWorkbook.TryStrToCellRange(AText: String; out AWorksheet: TsWorksheet; + out ARange: TsCellRange; AListSeparator: Char = #0): Boolean; +var + ranges: TsCellRangeArray; +begin + Result := TryStrToCellRanges(AText, AWorksheet, ranges, AListSeparator); + if Result then ARange := ranges[0]; +end; + +{@@ ---------------------------------------------------------------------------- + Analyses a string which can contain an array of cell ranges along with a + worksheet name. Extracts the worksheet (if missing the "active" worksheet of + the workbook is returned) and the range array. + + @param AText General cell range string in Excel notation, + i.e. worksheet name + ! + cell in A1 notation. + Example: Sheet1!A1:A10; A1:A10 or A1 are valid as well. + @param AWorksheet Pointer to the worksheet referred to by AText. If AText + does not contain the worksheet name, the active worksheet + of the workbook is returned + @param ARanges Array of TsCellRange records identifying the cell blocks + @param AListSeparator Character to separate the cell blocks in the text + If #0 then the ListSeparator of the workbook's FormatSettings + is used. + @returns TRUE if AText is a valid list of cell ranges, FALSE if not. If the + result is FALSE then AWorksheet and ARanges may have unpredictable + values. +-------------------------------------------------------------------------------} +function TsWorkbook.TryStrToCellRanges(AText: String; out AWorksheet: TsWorksheet; + out ARanges: TsCellRangeArray; AListSeparator: Char = #0): Boolean; +var + i: Integer; + s: String; + L: TStrings; +begin + Result := false; + AWorksheet := nil; + SetLength(ARanges, 0); + + if AText = '' then + exit; + + i := pos(SHEETSEPARATOR, AText); + if i = 0 then + AWorksheet := FActiveWorksheet + else begin + AWorksheet := GetWorksheetByName(Copy(AText, 1, i-1)); + if AWorksheet = nil then + exit; + AText := Copy(AText, i+1, Length(AText)); + end; + + L := TStringList.Create; + try + if AListSeparator = #0 then + L.Delimiter := FormatSettings.ListSeparator + else + L.Delimiter := AListSeparator; + L.StrictDelimiter := true; + L.DelimitedText := AText; + if L.Count = 0 then + begin + AWorksheet := nil; + exit; + end; + SetLength(ARanges, L.Count); + for i:=0 to L.Count-1 do begin + if pos(':', L[i]) = 0 then begin + Result := ParseCellString(L[i], ARanges[i].Row1, ARanges[i].Col1); + if Result then begin + ARanges[i].Row2 := ARanges[i].Row1; + ARanges[i].Col2 := ARanges[i].Col1; + end; + end else + Result := ParseCellRangeString(L[i], ARanges[i]); + if not Result then begin + SetLength(ARanges, 0); + AWorksheet := nil; + exit; + end; + end; + finally + L.Free; + end; +end; + + { Font handling } {@@ ---------------------------------------------------------------------------- diff --git a/components/fpspreadsheet/fpspreadsheetchart.pas b/components/fpspreadsheet/fpspreadsheetchart.pas index 0054eb50e..bd811d050 100644 --- a/components/fpspreadsheet/fpspreadsheetchart.pas +++ b/components/fpspreadsheet/fpspreadsheetchart.pas @@ -1,11 +1,15 @@ -{ -fpspreadsheetgrid.pas +{ fpspreadsheetchart.pas } -Chart data source designed to work together with TChart from Lazarus to display the data -and with TsWorksheetGrid from FPSpreadsheet to load data from a grid. +{@@ ---------------------------------------------------------------------------- +Chart data source designed to work together with TChart from Lazarus +to display the data and with FPSpreadsheet to load data. + +AUTHORS: Felipe Monteiro de Carvalho, Werner Pamler + +LICENSE: See the file COPYING.modifiedLGPL.txt, included in the Lazarus + distribution, for details about the license. +-------------------------------------------------------------------------------} -AUTHORS: Felipe Monteiro de Carvalho -} unit fpspreadsheetchart; {$mode objfpc}{$H+} @@ -15,11 +19,11 @@ interface uses Classes, SysUtils, LResources, Forms, Controls, Graphics, Dialogs, // TChart - {tasources,} TACustomSource, - // FPSpreadsheet Visual - fpspreadsheetctrls, fpspreadsheetgrid, + TACustomSource, // FPSpreadsheet - fpspreadsheet, fpsutils; + fpspreadsheet, fpsutils, + // FPSpreadsheet Visual + fpspreadsheetctrls, fpspreadsheetgrid; type @@ -83,8 +87,8 @@ type FWorkbookSource: TsWorkbookSource; FWorkbook: TsWorkbook; FWorksheets: array[TsXYRange] of TsWorksheet; + FRangeStr: array[TsXYRange] of String; FRanges: array[TsXYRange] of TsCellRangeArray; - FDirections: array[TsXYRange] of TsSelectionDirection; FPointsNumber: Cardinal; function GetRange(AIndex: TsXYRange): String; function GetWorkbook: TsWorkbook; @@ -94,10 +98,13 @@ type procedure SetWorkbookSource(AValue: TsWorkbookSource); protected FCurItem: TChartDataItem; + function BuildRangeStr(AIndex: TsXYRange; AListSeparator: char = #0): String; function CountValues(AIndex: TsXYRange): Integer; function GetCount: Integer; override; function GetItem(AIndex: Integer): PChartDataItem; override; procedure Notification(AComponent: TComponent; Operation: TOperation); override; + procedure Prepare; overload; + procedure Prepare(AIndex: TsXYRange); overload; procedure SetYCount(AValue: Cardinal); override; public destructor Destroy; override; @@ -270,27 +277,65 @@ end; { TsWorkbookChartSource } {------------------------------------------------------------------------------} +{@@ ---------------------------------------------------------------------------- + Destructor of the WorkbookChartSource. + Removes itself from the WorkbookSource's listener list. +-------------------------------------------------------------------------------} destructor TsWorkbookChartSource.Destroy; begin if FWorkbookSource <> nil then FWorkbookSource.RemoveListener(self); inherited Destroy; end; +{@@ ---------------------------------------------------------------------------- + Constructs the range string from the stored internal information. Is needed + to have the worksheet name in the range string in order to make the range + string unique. +-------------------------------------------------------------------------------} +function TsWorkbookChartSource.BuildRangeStr(AIndex: TsXYRange; + AListSeparator: Char = #0): String; +var + L: TStrings; + range: TsCellRange; +begin + if (FWorkbook = nil) or (FWorksheets[AIndex] = nil) or (Length(FRanges) = 0) then + exit(''); + + L := TStringList.Create; + try + if AListSeparator = #0 then + L.Delimiter := FWorkbook.FormatSettings.ListSeparator + else + L.Delimiter := AListSeparator; + L.StrictDelimiter := true; + for range in FRanges[AIndex] do + L.Add(GetCellRangeString(range, rfAllRel, true)); + Result := FWorksheets[AIndex].Name + SHEETSEPARATOR + L.DelimitedText; + finally + L.Free; + end; +end; + {@@ ---------------------------------------------------------------------------- Counts the number of x or y values contained in the x/y ranges + + @param AIndex Identifies whether values in the x or y ranges are counted. -------------------------------------------------------------------------------} function TsWorkbookChartSource.CountValues(AIndex: TsXYRange): Integer; var ir: Integer; + range: TsCellRange; begin Result := 0; - case FDirections[AIndex] of - fpsVerticalSelection: - for ir:=0 to High(FRanges[AIndex]) do - inc(Result, FRanges[AIndex, ir].Row2 - FRanges[AIndex, ir].Row1 + 1); - fpsHorizontalSelection: - for ir:=0 to High(FRanges[AIndex]) do - inc(Result, FRanges[AIndex, ir].Col2 - FRanges[AIndex, ir].Col1 + 1); + for range in FRanges[AIndex] do + begin + if range.Col1 = range.Col2 then + inc(Result, range.Row2 - range.Row1 + 1) + else + if range.Row1 = range.Row2 then + inc(Result, range.Col2 - range.Col1 + 1) + else + raise Exception.Create('x/y ranges can only be 1 column wide or 1 row high.'); end; end; @@ -306,6 +351,10 @@ end; {@@ ---------------------------------------------------------------------------- Main ChartSource method called from the series requiring data for plotting. Retrieves the data from the workbook. + + @param AIndex Index of the data point in the series. + @return Pointer to a TChartDataItem record containing the x and y coordinates, + the data point mark text, and the individual data point color. -------------------------------------------------------------------------------} function TsWorkbookChartSource.GetItem(AIndex: Integer): PChartDataItem; var @@ -325,25 +374,8 @@ end; by the workbook's FormatSettings. -------------------------------------------------------------------------------} function TsWorkbookChartsource.GetRange(AIndex: TsXYRange): String; -var - L: TStrings; - ir: Integer; begin - if FWorksheets[AIndex] = nil then - begin - Result := ''; - exit; - end; - - L := TStringList.Create; - try - L.Delimiter := Workbook.FormatSettings.ListSeparator; - for ir:=0 to High(FRanges[AIndex]) do - L.Add(GetCellRangeString(FRanges[AIndex, ir], rfAllRel, true)); - Result := FWorksheets[AIndex].Name + SHEETSEPARATOR + L.DelimitedText; - finally - L.Free; - end; + Result := FRangeStr[AIndex]; end; {@@ ---------------------------------------------------------------------------- @@ -358,45 +390,59 @@ begin FWorkbook := Result; end; +{@@ ---------------------------------------------------------------------------- + Helper method the prepare the information required for the series data point. + + @param XOrY Identifies whether the method retrieves the x or y + coordinate. + @param APointIndex Index of the data point for which the data are required + @param ANumber (output) x or y coordinate of the data point + @param AText Data point marks label text +-------------------------------------------------------------------------------} procedure TsWorkbookChartSource.GetXYItem(XOrY:TsXYRange; APointIndex: Integer; out ANumber: Double; out AText: String); var range: TsCellRange; - i, j: Integer; + idx, ir: Integer; len: Integer; row, col: Cardinal; cell: PCell; begin cell := nil; - i := 0; - case FDirections[XOrY] of - fpsVerticalSelection: - for j:=0 to High(FRanges[XOrY]) do begin - range := FRanges[XOrY, j]; - len := range.Row2 - range.Row1 + 1; - if (APointIndex >= i) and (APointIndex < i + len) then begin - row := range.Row1 + APointIndex - i; - col := range.Col1; - cell := FWorksheets[XOrY].FindCell(row, col); - break; - end; - inc(i, len); - end; + idx := 0; + if FRanges[XOrY] = nil then + exit; - fpsHorizontalSelection: - for j:=0 to High(FRanges[XOrY]) do begin - range := FRanges[XOrY, j]; - len := range.Col2 - range.Col1 + 1; - if (APointIndex >= i) and (APointIndex < i + len) then begin - row := range.Row1; - col := range.Col1 + APointIndex - i; - cell := FWorksheets[XOrY].FindCell(row, col); - break; - end; - inc(i, len); + for range in FRanges[XOrY] do + begin + if (range.Col1 = range.Col2) then // vertical range + begin + len := range.Row2 - range.Row1 + 1; + if (APointIndex >= idx) and (APointIndex < idx + len) then + begin + row := range.Row1 + APointIndex - idx; + col := range.Col1; + break; end; + inc(idx, len); + end else // horizontal range + if (range.Row1 = range.Row2) then + begin + len := range.Col2 - range.Col1 + 1; + if (APointIndex >= idx) and (APointIndex < idx + len) then + begin + row := range.Row1; + col := range.Col1 + APointIndex - idx; + break; + end; + end else + raise Exception.Create('x/y ranges can only be 1 column wide or 1 row high'); end; - if cell = nil then begin + + cell := FWorksheets[XOrY].FindCell(row, col); + + if cell = nil then + begin ANumber := NaN; AText := ''; end else @@ -417,9 +463,11 @@ end; and to worksheet changes by selecting the tab corresponding to the selected worksheet. - @param AChangedItems Set with elements identifying whether workbook, worksheet - cell content or cell formatting has changed + @param AChangedItems Set with elements identifying whether workbook, + worksheet, cell content or cell formatting has changed @param AData Additional data, not used here + + @see TsNotificationItem -------------------------------------------------------------------------------} procedure TsWorkbookChartSource.ListenerNotification( AChangedItems: TsNotificationItems; AData: Pointer = nil); @@ -431,9 +479,26 @@ var begin Unused(AData); - // Worksheet changes - if (lniWorksheet in AChangedItems) and (Workbook <> nil) then - Reset; + // Workbook has been successfully loaded, all sheets are ready + if (lniWorkbook in AChangedItems) then + Prepare; + + // Used worksheet has been renamed? + if (lniWorksheetRename in AChangedItems) then + for xy in TsXYRange do + if TsWorksheet(AData) = FWorksheets[xy] then begin + FRangeStr[xy] := BuildRangeStr(xy); + Prepare(xy); + end; + + // Used worksheet will be deleted? + if (lniWorksheetRemoving in AChangedItems) then + for xy in TsXYRange do + if TsWorksheet(AData) = FWorksheets[xy] then begin + FWorksheets[xy] := nil; + FRangeStr[xy] := BuildRangeStr(xy); + Prepare(xy); + end; // Cell changes: Enforce recalculation of axes if modified cell is within the // x or y range(s). @@ -457,6 +522,7 @@ begin end; end; + {@@ ---------------------------------------------------------------------------- Standard component notification: The ChartSource is notified that the WorkbookSource is being removed. @@ -469,6 +535,57 @@ begin SetWorkbookSource(nil); end; +{@@ ---------------------------------------------------------------------------- + Parses the x and y cell range strings and extracts internal information + (worksheet used, cell range coordinates) +-------------------------------------------------------------------------------} +procedure TsWorkbookChartSource.Prepare; +begin + Prepare(rngX); + Prepare(rngY); +end; + +{@@ ---------------------------------------------------------------------------- + Parses the range string of the data specified by AIndex and extracts internal + information (worksheet used, cell range coordinates) + + @param AIndex Identifies whether x or y cell ranges are analyzed +-------------------------------------------------------------------------------} +procedure TsWorkbookChartSource.Prepare(AIndex: TsXYRange); +const + XY: array[TsXYRange] of string = ('x', 'y'); +var + range: TsCellRange; +begin + if (FWorkbook = nil) or (FRangeStr[AIndex] = '') then begin + FWorksheets[AIndex] := nil; + SetLength(FRanges[AIndex], 0); + FPointsNumber := 0; + Reset; + exit; + end; + + if FWorkbook.TryStrToCellRanges(FRangeStr[AIndex], FWorksheets[AIndex], FRanges[AIndex]) + then begin + for range in FRanges[AIndex] do + if (range.Col1 <> range.Col2) and (range.Row1 <> range.Row2) then + raise Exception.Create('x/y ranges can only be 1 column wide or 1 row high'); + FPointsNumber := Max(CountValues(rngX), CountValues(rngY)); + // If x and y ranges are of different size empty data points will be plotted. + Reset; + // Make sure to include worksheet name in RangeString. + FRangeStr[AIndex] := BuildRangeStr(AIndex); + end else + if (FWorkbook.GetWorksheetCount > 0) then begin + if FWorksheets[AIndex] = nil then + raise Exception.CreateFmt('Worksheet of %s cell range "%s" does not exist.', + [XY[AIndex], FRangeStr[AIndex]]) + else + raise Exception.CreateFmt('No valid %s cell range in "%s".', + [XY[AIndex], FRangeStr[AIndex]]); + end; +end; + {@@ ---------------------------------------------------------------------------- Resets internal buffers and notfies chart elements of the changes, in particular, enforces recalculation of axis limits @@ -491,59 +608,9 @@ end; character defined in the Workbook's FormatSettings. -------------------------------------------------------------------------------} procedure TsWorkbookChartSource.SetRange(AIndex: TsXYRange; const AValue: String); -var - s: String; - p, i: Integer; - L: TStrings; - sd: TsSelectionDirection; - sd0: TsSelectionDirection; begin - if (FWorkbook = nil) then - exit; - - p := pos(SHEETSEPARATOR, AValue); - if p = 0 then - begin - FWorksheets[AIndex] := FWorkbook.ActiveWorksheet; - s := AValue; - end else - begin - s := Copy(AValue, 1, p-1); - FWorksheets[AIndex] := FWorkbook.GetWorksheetByName(s); - if FWorksheets[AIndex] = nil then - raise Exception.CreateFmt('%s cell range "%s" is in a non-existing '+ - 'worksheet.', [''+char(ord('x')+ord(AIndex)), AValue]); - s := Copy(AValue, p+1, Length(AValue)); - end; - L := TStringList.Create; - try - L.Delimiter := FWorkbook.FormatSettings.ListSeparator; - L.DelimitedText := s; - if L.Count = 0 then - raise Exception.CreateFmt('No %s cell range contained in "%s".', - [''+char(ord('x')+ord(AIndex)), AValue] - ); - sd := fpsVerticalSelection; - SetLength(FRanges[AIndex], L.Count); - for i:=0 to L.Count-1 do - if ParseCellRangeString(L[i], FRanges[AIndex, i]) then begin - if FRanges[AIndex, i].Col1 = FRanges[AIndex, i].Col2 then - sd := fpsVerticalSelection - else - if FRanges[AIndex, i].Row1 = FRanges[AIndex, i].Row2 then - sd := fpsHorizontalSelection - else - raise Exception.Create('Selection can only be 1 column wide or 1 row high'); - end else - raise Exception.CreateFmt('No valid %s cell range in "%s".', - [''+char(ord('x')+ord(AIndex)), L[i]] - ); - FPointsNumber := Max(CountValues(rngX), CountValues(rngY)); - // If x and y ranges are of different size empty data points will be plotted. - Reset; - finally - L.Free; - end; + FRangeStr[AIndex] := AValue; + Prepare; end; {@@ ---------------------------------------------------------------------------- @@ -564,12 +631,11 @@ end; {@@ ---------------------------------------------------------------------------- Inherited ChartSource method telling the series how many y values are used. - Currently we support only single valued data + Currently we support only single valued data (YCount = 1). -------------------------------------------------------------------------------} procedure TsWorkbookChartSource.SetYCount(AValue: Cardinal); begin FYCount := AValue; - // currently not used end; diff --git a/components/fpspreadsheet/fpspreadsheetctrls.pas b/components/fpspreadsheet/fpspreadsheetctrls.pas index 80182b5e0..29bf1e4cf 100644 --- a/components/fpspreadsheet/fpspreadsheetctrls.pas +++ b/components/fpspreadsheet/fpspreadsheetctrls.pas @@ -39,8 +39,10 @@ type {@@ Describes during communication between WorkbookSource and visual controls which kind of item has changed: the workbook, the worksheet, a cell value, or a cell formatting, etc. } - TsNotificationItem = (lniWorkbook, lniWorksheet, lniCell, lniSelection, - lniAbortSelection, lniRow, lniPalette); + TsNotificationItem = (lniWorkbook, + lniWorksheet, lniWorksheetAdd, lniWorksheetRemoving, lniWorksheetRemove, + lniWorksheetRename, + lniCell, lniSelection, lniAbortSelection, lniRow, lniPalette); {@@ This set accompanies the notification between WorkbookSource and visual controls and describes which items have changed in the spreadsheet. } TsNotificationItems = set of TsNotificationItem; @@ -81,6 +83,8 @@ type procedure WorksheetAddedHandler(Sender: TObject; ASheet: TsWorksheet); procedure WorksheetChangedHandler(Sender: TObject; ASheet: TsWorksheet); procedure WorksheetRemovedHandler(Sender: TObject; ASheetIndex: Integer); + procedure WorksheetRemovingHandler(Sender: TObject; AWorksheet: TsWorksheet); + procedure WorksheetRenamedHandler(Sender: TObject; AWorksheet: TsWorksheet); procedure WorksheetSelectedHandler(Sender: TObject; AWorksheet: TsWorksheet); protected @@ -772,6 +776,8 @@ begin FWorkbook.OnAddWorksheet := @WorksheetAddedHandler; FWorkbook.OnChangeWorksheet := @WorksheetChangedHandler; FWorkbook.OnRemoveWorksheet := @WorksheetRemovedHandler; + FWorkbook.OnRemovingWorksheet := @WorksheetRemovingHandler; + FWorkbook.OnRenameWorksheet := @WorksheetRenamedHandler; FWorkbook.OnSelectWorksheet := @WorksheetSelectedHandler; FWorkbook.OnChangePalette := @WorkbookChangedPaletteHandler; // Pass options to workbook @@ -1227,7 +1233,7 @@ procedure TsWorkbookSource.WorksheetAddedHandler(Sender: TObject; ASheet: TsWorksheet); begin Unused(Sender); - NotifyListeners([lniWorkbook]); + NotifyListeners([lniWorksheetAdd]); SelectWorksheet(ASheet); end; @@ -1275,10 +1281,34 @@ begin end else sheet := FWorksheet; FWorksheet := sheet; // is needed by listeners! - NotifyListeners([lniWorkbook]); + NotifyListeners([lniWorksheetRemove]); SelectWorksheet(sheet); end; +{@@ ---------------------------------------------------------------------------- + Event handler called BEFORE a worksheet is deleted. + + @param Sender Workbook containing the worksheet + @param AWorksheet Worksheet which is to be deleted +-------------------------------------------------------------------------------} +procedure TsWorkbookSource.WorksheetRemovingHandler(Sender: TObject; + AWorksheet: TsWorksheet); +begin + NotifyListeners([lniWorksheetRemoving], AWorksheet); +end; + +{@@ ---------------------------------------------------------------------------- + Event handler called after a worksheet has been renamed + + @param Sender Workbook containing the worksheet + @param AWorksheet Worksheet which has been renamed +-------------------------------------------------------------------------------} +procedure TsWorkbookSource.WorksheetRenamedHandler(Sender: TObject; + AWorksheet: TsWorksheet); +begin + NotifyListeners([lniWorksheetRename], AWorksheet); +end; + {@@ ---------------------------------------------------------------------------- Event handler called whenever a the workbook makes a worksheet "active". @@ -1388,16 +1418,23 @@ var begin Unused(AData); - // Workbook changed - if (lniWorkbook in AChangedItems) then + // Workbook changed: new workbook, worksheet added/renamed/deleted + if (AChangedItems * [lniWorkbook, lniWorksheetAdd, lniWorksheetRemove, lniWorksheetRename] <> []) then begin inc(FLockCount); // avoid WorkbookSelect message when adding each tab GetSheetList(Tabs); - TabIndex := Tabs.Count-1; + if (lniWorkbook in AChangedItems) then + TabIndex := 0 + else + if (lniWorksheetAdd in AChangedItems) then + TabIndex := Tabs.Count-1 + else + if (lniWorksheetRename in AChangedItems) then + TabIndex := Workbook.GetWorksheetIndex(TsWorksheet(AData)); dec(FLockCount); end; - // Worksheet changed + // Worksheet selected if (lniWorksheet in AChangedItems) and (Worksheet <> nil) then begin i := Tabs.IndexOf(Worksheet.Name); diff --git a/components/fpspreadsheet/fpspreadsheetgrid.pas b/components/fpspreadsheet/fpspreadsheetgrid.pas index 49301514b..216726eca 100644 --- a/components/fpspreadsheet/fpspreadsheetgrid.pas +++ b/components/fpspreadsheet/fpspreadsheetgrid.pas @@ -2902,7 +2902,8 @@ end; procedure TsCustomWorksheetGrid.Loaded; begin inherited; - NewWorkbook(FInitColCount, FInitRowCount); + if FWorkbookSource = nil then + NewWorkbook(FInitColCount, FInitRowCount); end; {@@ ----------------------------------------------------------------------------