diff --git a/components/fpspreadsheet/examples/visual/fpschart/fpschartlink/chart_demo.lpi b/components/fpspreadsheet/examples/visual/fpschart/fpschartlink/chart_demo.lpi index dd2428053..5c2589e1b 100644 --- a/components/fpspreadsheet/examples/visual/fpschart/fpschartlink/chart_demo.lpi +++ b/components/fpspreadsheet/examples/visual/fpschart/fpschartlink/chart_demo.lpi @@ -65,6 +65,7 @@ + diff --git a/components/fpspreadsheet/examples/visual/fpschart/fpschartlink/main.lfm b/components/fpspreadsheet/examples/visual/fpschart/fpschartlink/main.lfm index f86d3619f..46e6786c4 100644 --- a/components/fpspreadsheet/examples/visual/fpschart/fpschartlink/main.lfm +++ b/components/fpspreadsheet/examples/visual/fpschart/fpschartlink/main.lfm @@ -10,8 +10,8 @@ object Form1: TForm1 OnCreate = FormCreate object sWorksheetGrid1: TsWorksheetGrid Left = 0 - Height = 527 - Top = 0 + Height = 489 + Top = 38 Width = 402 FrozenCols = 0 FrozenRows = 0 @@ -28,14 +28,14 @@ object Form1: TForm1 end object Splitter1: TSplitter Left = 402 - Height = 527 - Top = 0 + Height = 489 + Top = 38 Width = 5 end object Chart1: TChart Left = 407 - Height = 527 - Top = 0 + Height = 489 + Top = 38 Width = 694 AxisList = < item @@ -55,10 +55,96 @@ object Form1: TForm1 ) Align = alClient end + object Panel1: TPanel + Left = 0 + Height = 38 + Top = 0 + Width = 1101 + Align = alTop + AutoSize = True + BevelOuter = bvNone + ClientHeight = 38 + ClientWidth = 1101 + TabOrder = 3 + object Label1: TLabel + AnchorSideLeft.Control = Panel1 + AnchorSideTop.Control = Panel1 + AnchorSideTop.Side = asrCenter + Left = 6 + Height = 15 + Top = 12 + Width = 51 + BorderSpacing.Around = 6 + Caption = 'File name' + end + object ComboBox1: TComboBox + AnchorSideLeft.Control = Label1 + AnchorSideLeft.Side = asrBottom + AnchorSideTop.Control = Panel1 + AnchorSideTop.Side = asrCenter + AnchorSideRight.Control = Button1 + Left = 63 + Height = 23 + Top = 8 + Width = 911 + Anchors = [akTop, akLeft, akRight] + BorderSpacing.Around = 6 + ItemHeight = 15 + Items.Strings = ( + '../../../other/chart/area.ods' + '../../../other/chart/bars.ods' + '../../../other/chart/bubble.ods' + '../../../other/chart/pie.ods' + '../../../other/chart/radar.ods' + '../../../other/chart/regression.ods' + ) + TabOrder = 0 + TextHint = 'Enter or select file name' + OnCloseUp = ComboBox1CloseUp + end + object Button1: TButton + AnchorSideTop.Control = Panel1 + AnchorSideTop.Side = asrCenter + AnchorSideRight.Control = Button2 + Left = 980 + Height = 25 + Top = 7 + Width = 35 + Anchors = [akTop, akRight] + AutoSize = True + BorderSpacing.Around = 6 + Caption = '...' + TabOrder = 1 + OnClick = Button1Click + end + object Button2: TButton + AnchorSideTop.Control = Panel1 + AnchorSideTop.Side = asrCenter + AnchorSideRight.Control = Panel1 + AnchorSideRight.Side = asrBottom + Left = 1021 + Height = 25 + Top = 7 + Width = 74 + Anchors = [akTop, akRight] + AutoSize = True + BorderSpacing.Around = 6 + Caption = 'Open file' + TabOrder = 2 + OnClick = Button2Click + end + end object sWorkbookSource1: TsWorkbookSource FileFormat = sfUser Options = [] Left = 244 Top = 138 end + object OpenDialog1: TOpenDialog + DefaultExt = '.ods' + Filter = 'OpenDocument Spreadsheet Files|*.ods' + Options = [ofFileMustExist, ofEnableSizing, ofViewDetail] + Left = 976 + Top = 56 + end end diff --git a/components/fpspreadsheet/examples/visual/fpschart/fpschartlink/main.pas b/components/fpspreadsheet/examples/visual/fpschart/fpschartlink/main.pas index 251cecc0b..6c7fc272f 100644 --- a/components/fpspreadsheet/examples/visual/fpschart/fpschartlink/main.pas +++ b/components/fpspreadsheet/examples/visual/fpschart/fpschartlink/main.pas @@ -5,7 +5,7 @@ unit main; interface uses - Classes, SysUtils, Forms, Controls, Graphics, Dialogs, ExtCtrls, + Classes, SysUtils, Forms, Controls, Graphics, Dialogs, ExtCtrls, StdCtrls, TAGraph, fpSpreadsheet, fpsTypes, fpsOpenDocument, fpSpreadsheetCtrls, fpSpreadsheetGrid, fpSpreadsheetChart; @@ -15,13 +15,24 @@ type { TForm1 } TForm1 = class(TForm) + Button1: TButton; + Button2: TButton; Chart1: TChart; + ComboBox1: TComboBox; + Label1: TLabel; + OpenDialog1: TOpenDialog; + Panel1: TPanel; Splitter1: TSplitter; sWorkbookSource1: TsWorkbookSource; sWorksheetGrid1: TsWorksheetGrid; + procedure Button1Click(Sender: TObject); + procedure Button2Click(Sender: TObject); + procedure ComboBox1CloseUp(Sender: TObject); procedure FormCreate(Sender: TObject); private sChartLink: TsWorkbookChartLink; + FFileName: String; + procedure LoadFile(AFileName: String); public @@ -46,11 +57,47 @@ const { TForm1 } +procedure TForm1.Button1Click(Sender: TObject); +begin + OpenDialog1.InitialDir := ExtractFilePath(Combobox1.Text); + OpenDialog1.FileName := ''; + if OpenDialog1.Execute then + begin + Combobox1.Text := OpenDialog1.FileName; + LoadFile(OpenDialog1.FileName); + end; +end; + +procedure TForm1.Button2Click(Sender: TObject); +begin + LoadFile(Combobox1.Text); +end; + +procedure TForm1.ComboBox1CloseUp(Sender: TObject); +begin + Combobox1.Text := Combobox1.Items[Combobox1.ItemIndex]; + LoadFile(Combobox1.Text); +end; + procedure TForm1.FormCreate(Sender: TObject); +begin + if ParamCount > 0 then + begin + Combobox1.Text := ParamStr(1); + LoadFile(Combobox1.Text); + end; +end; + +procedure TForm1.LoadFile(AFileName: String); var fn: String; begin - fn := ExpandFileName(FILE_NAME); + fn := ExpandFileName(AFileName); + if not FileExists(fn) then + begin + MessageDlg('File "' + fn + '" not found.', mtError, [mbOK], 0); + exit; + end; sWorkbookSource1.FileFormat := sfOpenDocument; if FileExists(fn) then @@ -61,6 +108,5 @@ begin sChartLink.WorkbookSource := sWorkbookSource1; sChartLink.WorkbookChartIndex := 0; end; - end. diff --git a/components/fpspreadsheet/source/common/fpschart.pas b/components/fpspreadsheet/source/common/fpschart.pas index a55c13d9a..57807f704 100644 --- a/components/fpspreadsheet/source/common/fpschart.pas +++ b/components/fpspreadsheet/source/common/fpschart.pas @@ -390,24 +390,12 @@ type constructor Create(AChart: TsChart); override; end; - TsBubbleSeries = class(TsChartSeries) - // to do: inherited from ScatterSeries, but without symbols - private - FBubbleRange: TsChartRange; - public - constructor Create(AChart: TsChart); override; - destructor Destroy; override; - procedure SetBubbleRange(ARow1, ACol1, ARow2, ACol2: Cardinal); - procedure SetBubbleRange(ASheet1: String; ARow1, ACol1: Cardinal; ASheet2: String; ARow2, ACol2: Cardinal); - property BubbleRange: TsChartRange read FBubbleRange; - end; - TsChartSeriesSymbol = ( cssRect, cssDiamond, cssTriangle, cssTriangleDown, cssTriangleLeft, cssTriangleRight, cssCircle, cssStar, cssX, cssPlus, cssAsterisk ); - TsLineSeries = class(TsChartSeries) + TsCustomLineSeries = class(TsChartSeries) private FSymbol: TsChartSeriesSymbol; FSymbolHeight: Double; // in mm @@ -417,9 +405,7 @@ type FBorder: TsChartLine; function GetSymbolFill: TsChartFill; procedure SetSymbolFill(Value: TsChartFill); - public - constructor Create(AChart: TsChart); override; - destructor Destroy; override; + protected property Symbol: TsChartSeriesSymbol read FSymbol write FSymbol; property SymbolBorder: TsChartLine read FBorder write FBorder; property SymbolFill: TsChartFill read GetSymbolFill write SetSymbolFill; @@ -427,6 +413,20 @@ type property SymbolWidth: double read FSymbolWidth write FSymbolWidth; property ShowLines: Boolean read FShowLines write FShowLines; property ShowSymbols: Boolean read FShowSymbols write FShowSymbols; + public + constructor Create(AChart: TsChart); override; + destructor Destroy; override; + end; + + TsLineSeries = class(TsCustomLineSeries) + public + property Symbol; + property SymbolBorder; + property SymbolFill; + property SymbolHeight; + property SymbolWidth; + property ShowLines; + property ShowSymbols; end; TsPieSeries = class(TsChartSeries) @@ -487,7 +487,7 @@ type destructor Destroy; override; end; - TsScatterSeries = class(TsLineSeries) + TsCustomScatterSeries = class(TsCustomLineSeries) private FRegression: TsChartRegression; public @@ -496,6 +496,28 @@ type property Regression: TsChartRegression read FRegression write FRegression; end; + TsScatterSeries = class(TsCustomScatterSeries) + public + property Symbol; + property SymbolBorder; + property SymbolFill; + property SymbolHeight; + property SymbolWidth; + property ShowLines; + property ShowSymbols; + end; + + TsBubbleSeries = class(TsCustomScatterSeries) + private + FBubbleRange: TsChartRange; + public + constructor Create(AChart: TsChart); override; + destructor Destroy; override; + procedure SetBubbleRange(ARow1, ACol1, ARow2, ACol2: Cardinal); + procedure SetBubbleRange(ASheet1: String; ARow1, ACol1: Cardinal; ASheet2: String; ARow2, ACol2: Cardinal); + property BubbleRange: TsChartRange read FBubbleRange; + end; + TsChartSeriesList = class(TFPObjectList) private function GetItem(AIndex: Integer): TsChartSeries; @@ -1428,9 +1450,9 @@ begin end; -{ TsLineSeries } +{ TsCustomLineSeries } -constructor TsLineSeries.Create(AChart: TsChart); +constructor TsCustomLineSeries.Create(AChart: TsChart); begin inherited Create(AChart); FChartType := ctLine; @@ -1445,18 +1467,18 @@ begin FBorder.Color := scBlack; end; -destructor TsLineSeries.Destroy; +destructor TsCustomLineSeries.Destroy; begin FBorder.Free; inherited; end; -function TsLineSeries.GetSymbolFill: TsChartFill; +function TsCustomLineSeries.GetSymbolFill: TsChartFill; begin Result := FFill; end; -procedure TsLineSeries.SetSymbolFill(Value: TsChartFill); +procedure TsCustomLineSeries.SetSymbolFill(Value: TsChartFill); begin FFill := Value; end; @@ -1574,16 +1596,16 @@ begin end; -{ TsScatterSeries } +{ TsCustomScatterSeries } -constructor TsScatterSeries.Create(AChart: TsChart); +constructor TsCustomScatterSeries.Create(AChart: TsChart); begin inherited Create(AChart); FChartType := ctScatter; FRegression := TsChartRegression.Create; end; -destructor TsScatterSeries.Destroy; +destructor TsCustomScatterSeries.Destroy; begin FRegression.Free; inherited; diff --git a/components/fpspreadsheet/source/common/fpsopendocumentchart.pas b/components/fpspreadsheet/source/common/fpsopendocumentchart.pas index 098316224..555d397db 100644 --- a/components/fpspreadsheet/source/common/fpsopendocumentchart.pas +++ b/components/fpspreadsheet/source/common/fpsopendocumentchart.pas @@ -3202,8 +3202,8 @@ begin series := AChart.Series[ASeriesIndex]; - // These are the x values of a scatter or bubble plot. - if (series is TsScatterSeries) or (series is TsBubbleSeries) then + // These are the x values of a scatter plot. + if (series is TsScatterSeries) then begin domainRangeX := GetSheetCellRangeString_ODS( series.XRange.GetSheet1Name, series.XRange.GetSheet2Name, diff --git a/components/fpspreadsheet/source/visual/fpspreadsheetchart.pas b/components/fpspreadsheet/source/visual/fpspreadsheetchart.pas index d3665d230..207258d36 100644 --- a/components/fpspreadsheet/source/visual/fpspreadsheetchart.pas +++ b/components/fpspreadsheet/source/visual/fpspreadsheetchart.pas @@ -63,6 +63,7 @@ type protected FCurItem: TChartDataItem; function BuildRangeStr(AIndex: TsXYLRange; AListSeparator: char = #0): String; + procedure ClearRanges; function CountValues(AIndex: TsXYLRange): Integer; function GetCount: Integer; override; function GetItem(AIndex: Integer): PChartDataItem; override; @@ -141,7 +142,7 @@ type procedure UpdateAreaSeries(AWorkbookSeries: TsAreaSeries; AChartSeries: TAreaSeries); procedure UpdateBarSeries(AWorkbookSeries: TsBarSeries; AChartSeries: TBarSeries); procedure UpdateBubbleSeries(AWorkbookSeries: TsBubbleSeries; AChartSeries: TBubbleSeries); - procedure UpdateLineSeries(AWorkbookSeries: TsLineSeries; AChartSeries: TLineSeries); + procedure UpdateCustomLineSeries(AWorkbookSeries: TsCustomLineSeries; AChartSeries: TLineSeries); procedure UpdatePieSeries(AWorkbookSeries: TsPieSeries; AChartSeries: TPieSeries); procedure UpdatePolarSeries(AWorkbookSeries: TsRadarSeries; AChartSeries: TPolarSeries); procedure UpdateScatterSeries(AWorkbookSeries: TsScatterSeries; AChartSeries: TLineSeries); @@ -173,6 +174,7 @@ uses type TBasicPointSeriesOpener = class(TBasicPointSeries); + TsCustomLineSeriesOpener = class(TsCustomLineSeries); function mmToPx(mm: Double; ppi: Integer): Integer; begin @@ -304,15 +306,7 @@ end; constructor TsWorkbookChartSource.Create(AOwner: TComponent); begin inherited; - SetLength(FRanges[rngX], 1); - SetLength(FRanges[rngY], 1); - SetLength(FRanges[rngLabel], 1); - SetLength(FRanges[rngColor], 1); - - SetLength(FWorksheets[rngX], 1); - SetLength(FWorksheets[rngY], 1); - SetLength(FWorksheets[rngLabel], 1); - Setlength(FWorksheets[rngColor], 1); + ClearRanges; end; {@@ ---------------------------------------------------------------------------- @@ -380,6 +374,25 @@ begin end; end; +procedure TsWorkbookChartSource.ClearRanges; +begin + SetLength(FRanges[rngX], 1); FRanges[rngX, 0 ] := nil; + SetLength(FRanges[rngY], 1); FRanges[rngY, 0] := nil; + SetLength(FRanges[rngLabel], 1); FRanges[rngLabel, 0] := nil; + SetLength(FRanges[rngColor], 1); FRanges[rngColor, 0] := nil; + + SetLength(FWorksheets[rngX], 1); FWorksheets[rngX, 0] := nil; + SetLength(FWorksheets[rngY], 1); FWorksheets[rngY, 0] := nil; + SetLength(FWorksheets[rngLabel], 1); FWorksheets[rngLabel, 0] := nil; + SetLength(FWorksheets[rngColor], 1); FWorksheets[rngColor, 0] := nil; + + FRangeStr[rngX] := ''; + FRangeStr[rngY] := ''; + FRangeStr[rngLabel] := ''; + FRangeStr[rngColor] := ''; +end; + + {@@ ---------------------------------------------------------------------------- Counts the number of x or y values contained in the x/y ranges @@ -620,7 +633,10 @@ begin // Workbook has been successfully loaded, all sheets are ready if (lniWorkbook in AChangedItems) then + begin + ClearRanges; Prepare; + end; // Used worksheet has been renamed? if (lniWorksheetRename in AChangedItems) then @@ -896,7 +912,6 @@ begin FWorkbookSource := AValue; if FWorkbookSource <> nil then FWorkbookSource.AddListener(self); -// FWorkbook := GetWorkbook; ListenerNotification([lniWorkbook, lniWorksheet]); Prepare; end; @@ -1017,6 +1032,8 @@ begin Result := TBubbleSeries.Create(FChart); src.SetYRange(1, TsBubbleSeries(ASeries).BubbleRange); end; + ctPie: + Result := TPieSeries.Create(FChart); else exit(nil); end; @@ -1079,7 +1096,7 @@ begin ctBubble: UpdateBubbleSeries(TsBubbleSeries(ASeries), TBubbleSeries(ser)); ctLine: - UpdateLineSeries(TsLineSeries(ASeries), TLineSeries(ser)); + UpdateCustomLineSeries(TsLineSeries(ASeries), TLineSeries(ser)); ctScatter: UpdateScatterSeries(TsScatterSeries(ASeries), TLineSeries(ser)); ctPie, ctRing: @@ -1347,7 +1364,11 @@ end; procedure TsWorkbookChartLink.ListenerNotification(AChangedItems: TsNotificationItems; AData: Pointer = nil); begin - // to be completed + Unused(AData); + + // Workbook has been successfully loaded, all sheets are ready + if (lniWorkbook in AChangedItems) then + ClearChart; end; procedure TsWorkbookChartLink.Notification(AComponent: TComponent; Operation: TOperation); @@ -1561,49 +1582,6 @@ begin FChart.Frame.Visible := AWorkbookChart.PlotArea.Border.Style <> clsNoLine; end; -{ -procedure TsWorkbookChartLink.UpdateBarSeries(AWorkbookChart: TsChart); -var - i, n: Integer; - ser: TBarSeries; - barWidth, totalBarWidth: Integer; -begin - if AWorkbookChart.GetChartType <> ctBar then - exit; - - // Count the bar series - n := 0; - for i := 0 to AWorkbookChart.Series.Count-1 do - begin - if AWorkbookChart.Series[i].ChartType = ctBar then - inc(n); - end; - - // Iterate over bar series to put them side-by-side or to stack them - totalBarWidth := 90; - barWidth := round(totalBarWidth / n); - for i := 0 to FChart.SeriesCount-1 do - if FChart.Series[i] is TBarSeries then - begin - ser := TBarSeries(FChart.Series[i]); - case AWorkbookChart.Stackmode of - csmSideBySide: - begin - ser.BarWidthPercent := barWidth; - ser.BarWidthStyle := bwPercentMin; - ser.BarOffsetPercent := round((i - (n - 1)/2)*barWidth); - end; - csmStacked: - ser.Stacked := true; - csmStackedPercentage: - begin - ser.Stacked := true; - end; - end; - end; -end; -} - procedure TsWorkbookChartLink.UpdateChartBrush(AWorkbookChart: TsChart; AWorkbookFill: TsChartFill; ABrush: TBrush); var @@ -1763,7 +1741,7 @@ begin end; end; -procedure TsWorkbookChartLink.UpdateLineSeries(AWorkbookSeries: TsLineSeries; +procedure TsWorkbookChartLink.UpdateCustomLineSeries(AWorkbookSeries: TsCustomLineSeries; AChartSeries: TLineSeries); const POINTER_STYLES: array[TsChartSeriesSymbol] of TSeriesPointerstyle = ( @@ -1781,19 +1759,20 @@ const ); var ppi: Integer; + openedWorkbookSeries: TsCustomLineSeriesOpener absolute AWorkbookSeries; begin ppi := GetParentForm(FChart).PixelsPerInch; UpdateChartPen(AWorkbookSeries.Chart, AWorkbookSeries.Line, AChartSeries.LinePen); AChartSeries.ShowLines := AWorkbookSeries.Line.Style <> clsNoLine; - AChartSeries.ShowPoints := AWorkbookSeries.ShowSymbols; + AChartSeries.ShowPoints := openedWorkbookSeries.ShowSymbols; if AChartSeries.ShowPoints then begin UpdateChartBrush(AWorkbookSeries.Chart, AWorkbookSeries.Fill, AChartSeries.Pointer.Brush); AChartSeries.Pointer.Pen.Color := AChartSeries.LinePen.Color; - AChartSeries.Pointer.Style := POINTER_STYLES[AWorkbookSeries.Symbol]; - AChartSeries.Pointer.HorizSize := mmToPx(AWorkbookSeries.SymbolWidth, ppi); - AChartSeries.Pointer.VertSize := mmToPx(AWorkbookSeries.SymbolHeight, ppi); + AChartSeries.Pointer.Style := POINTER_STYLES[openedWorkbookSeries.Symbol]; + AChartSeries.Pointer.HorizSize := mmToPx(openedWorkbookSeries.SymbolWidth, ppi); + AChartSeries.Pointer.VertSize := mmToPx(openedWorkbookSeries.SymbolHeight, ppi); end; AChartSeries.Stacked := AWorkbookSeries.Chart.StackMode <> csmSideBySide; if AChartSeries.Source is TCalculatedChartSource then @@ -1838,7 +1817,7 @@ var ser: TFitSeries; s: String; begin - UpdateLineSeries(AWorkbookSeries, AChartSeries); + UpdateCustomLineSeries(AWorkbookSeries, AChartSeries); if AWorkbookSeries.Regression.RegressionType = rtNone then exit; diff --git a/components/fpspreadsheet/source/visual/fpspreadsheetctrls.pas b/components/fpspreadsheet/source/visual/fpspreadsheetctrls.pas index f285640fb..39f6398e0 100644 --- a/components/fpspreadsheet/source/visual/fpspreadsheetctrls.pas +++ b/components/fpspreadsheet/source/visual/fpspreadsheetctrls.pas @@ -1331,12 +1331,21 @@ var begin for j:=0 to FListeners.Count-1 do begin C := TComponent(FListeners[j]); - if C.GetInterface(GUID_SpreadsheetControl, I) then - I.ListenerNotification(AChangedItems, AData) - else - raise Exception.CreateFmt('[TsWorkbookSource.NotifyListeners] Class %s is not prepared to be a spreadsheet listener.', - [C.ClassName]); + if (C <> nil) then + begin + if C.GetInterface(GUID_SpreadsheetControl, I) then + I.ListenerNotification(AChangedItems, AData) + else + raise Exception.CreateFmt('[TsWorkbookSource.NotifyListeners] Class %s is not prepared to be a spreadsheet listener.', + [C.ClassName]); + end; end; + + // Cleanup listener list from removed listeners (= set to nil) while + // NotifyListeners was running + for j := FListeners.Count-1 downto 0 do + if FListeners[j] = nil then + FListeners.Delete(j); end; {@@ ---------------------------------------------------------------------------- @@ -1355,11 +1364,20 @@ begin C := TComponent(FListeners[j]); if C = AListener then begin - FListeners.Delete(j); - if C.GetInterface(GUID_SpreadsheetControl, I) then - I.RemoveWorkbookSource - else - raise Exception.CreateFmt('Class %s not prepared for listening.',[AListener.ClassName]); + FListeners[j] := nil; + // Do not delete the listener here (FListeners.Delete(j)) because + // RemoveListeners may be called while NotifyListeners is still running. + // The problem can be that a chart may destroy a listening chart source + // which would trigger RemoveListener. If the chart source then would be + // deleted from the list the NotifiyListeners loop would access + // unallocated memory --> crash + if C <> nil then + begin + if C.GetInterface(GUID_SpreadsheetControl, I) then + I.RemoveWorkbookSource + else + raise Exception.CreateFmt('Class %s not prepared for listening.',[AListener.ClassName]); + end; end; end; end;