diff --git a/components/fpspreadsheet/examples/fpschart/fpschart.lpi b/components/fpspreadsheet/examples/fpschart/fpschart.lpi index 7d8bced85..4610fff23 100644 --- a/components/fpspreadsheet/examples/fpschart/fpschart.lpi +++ b/components/fpspreadsheet/examples/fpschart/fpschart.lpi @@ -4,6 +4,7 @@ + @@ -72,6 +73,9 @@ + + + @@ -79,6 +83,9 @@ + + + diff --git a/components/fpspreadsheet/examples/fpschart/fpschart.lpr b/components/fpspreadsheet/examples/fpschart/fpschart.lpr index a0cd3af67..fbe063756 100644 --- a/components/fpspreadsheet/examples/fpschart/fpschart.lpr +++ b/components/fpspreadsheet/examples/fpschart/fpschart.lpr @@ -13,7 +13,6 @@ uses {$R *.res} begin - Application.Title:='project1'; Application.Initialize; Application.CreateForm(TFPSChartForm, FPSChartForm); Application.Run; diff --git a/components/fpspreadsheet/examples/fpschart/mainform.lfm b/components/fpspreadsheet/examples/fpschart/mainform.lfm index 89c70905a..6260e0ad5 100644 --- a/components/fpspreadsheet/examples/fpschart/mainform.lfm +++ b/components/fpspreadsheet/examples/fpschart/mainform.lfm @@ -1,16 +1,17 @@ object FPSChartForm: TFPSChartForm - Left = 179 - Height = 331 - Top = 157 - Width = 742 + Left = 239 + Height = 382 + Top = 154 + Width = 700 Caption = 'FPSpreadsheet Chart Example' - ClientHeight = 331 - ClientWidth = 742 + ClientHeight = 382 + ClientWidth = 700 + OnCreate = FormCreate LCLVersion = '0.9.29' object MyChart: TChart - Left = 400 + Left = 352 Height = 240 - Top = 24 + Top = 136 Width = 336 AxisList = < item @@ -37,26 +38,106 @@ object FPSChartForm: TFPSChartForm object WorksheetGrid: TsWorksheetGrid Left = 16 Height = 240 - Top = 24 - Width = 360 + Top = 136 + Width = 328 Options = [goFixedVertLine, goFixedHorzLine, goVertLine, goHorzLine, goRangeSelect, goEditing, goSmoothScroll] TabOrder = 1 end object btnCreateGraphic: TButton - Left = 104 + Left = 464 Height = 25 - Top = 280 + Top = 56 Width = 128 Caption = 'Create Graphic' OnClick = btnCreateGraphicClick TabOrder = 2 end + object Label1: TLabel + Left = 16 + Height = 34 + Top = 8 + Width = 676 + AutoSize = False + Caption = 'Please add data to the grid or load it from a file, then choose the location of the data for the X and Y axises and click on the button "Create Graphic" to generate a chart.' + ParentColor = False + WordWrap = True + end + object editSourceFile: TFileNameEdit + Left = 152 + Height = 22 + Top = 48 + Width = 136 + DialogOptions = [] + FilterIndex = 0 + HideDirectories = False + ButtonWidth = 23 + NumGlyphs = 0 + MaxLength = 0 + TabOrder = 3 + end + object Label2: TLabel + Left = 14 + Height = 18 + Top = 51 + Width = 130 + Caption = 'Source Spreadsheet:' + ParentColor = False + end + object btnLoadSpreadsheet: TButton + Left = 320 + Height = 25 + Top = 48 + Width = 75 + Caption = 'Load' + OnClick = btnLoadSpreadsheetClick + TabOrder = 4 + end + object editXAxis: TLabeledEdit + Left = 64 + Height = 22 + Top = 80 + Width = 80 + EditLabel.AnchorSideLeft.Control = editXAxis + EditLabel.AnchorSideTop.Control = editXAxis + EditLabel.AnchorSideTop.Side = asrCenter + EditLabel.AnchorSideRight.Control = editXAxis + EditLabel.AnchorSideBottom.Control = editXAxis + EditLabel.Left = 17 + EditLabel.Height = 18 + EditLabel.Top = 82 + EditLabel.Width = 44 + EditLabel.Caption = 'X-Axis:' + EditLabel.ParentColor = False + LabelPosition = lpLeft + TabOrder = 5 + Text = 'A1:A5' + end + object EditYAxis: TLabeledEdit + Left = 208 + Height = 22 + Top = 80 + Width = 80 + EditLabel.AnchorSideLeft.Control = EditYAxis + EditLabel.AnchorSideTop.Control = EditYAxis + EditLabel.AnchorSideTop.Side = asrCenter + EditLabel.AnchorSideRight.Control = EditYAxis + EditLabel.AnchorSideBottom.Control = EditYAxis + EditLabel.Left = 161 + EditLabel.Height = 18 + EditLabel.Top = 82 + EditLabel.Width = 44 + EditLabel.Caption = 'Y-Axis:' + EditLabel.ParentColor = False + LabelPosition = lpLeft + TabOrder = 6 + Text = 'B1:B5' + end object FPSChartSource: TsWorksheetChartSource PointsNumber = 5 YFirstCellCol = 1 XSelectionDirection = fpsVerticalSelection YSelectionDirection = fpsVerticalSelection - left = 376 - top = 264 + left = 632 + top = 56 end end diff --git a/components/fpspreadsheet/examples/fpschart/mainform.pas b/components/fpspreadsheet/examples/fpschart/mainform.pas index 6b92a64f4..b9ba09255 100644 --- a/components/fpspreadsheet/examples/fpschart/mainform.pas +++ b/components/fpspreadsheet/examples/fpschart/mainform.pas @@ -6,7 +6,8 @@ interface uses Classes, SysUtils, FileUtil, LResources, Forms, Controls, Graphics, Dialogs, - StdCtrls, Grids, fpspreadsheetchart, fpspreadsheetgrid, TAGraph, TASeries; + StdCtrls, Grids, EditBtn, ExtCtrls, fpspreadsheetchart, fpspreadsheetgrid, + TAGraph, TASeries; type @@ -14,11 +15,19 @@ type TFPSChartForm = class(TForm) btnCreateGraphic: TButton; + btnLoadSpreadsheet: TButton; + editSourceFile: TFileNameEdit; + Label1: TLabel; + Label2: TLabel; + editXAxis: TLabeledEdit; + EditYAxis: TLabeledEdit; MyChart: TChart; FPSChartSource: TsWorksheetChartSource; MyChartLineSeries: TLineSeries; WorksheetGrid: TsWorksheetGrid; procedure btnCreateGraphicClick(Sender: TObject); + procedure btnLoadSpreadsheetClick(Sender: TObject); + procedure FormCreate(Sender: TObject); private { private declarations } public @@ -30,14 +39,40 @@ var implementation +uses + // FPSpreadsheet and supported formats + fpspreadsheet, xlsbiff8, xlsbiff5, xlsbiff2, xlsxooxml, fpsopendocument; + {$R *.lfm} { TFPSChartForm } procedure TFPSChartForm.btnCreateGraphicClick(Sender: TObject); begin + FPSChartSource.LoadPropertiesFromStrings(editXAxis.Text, editYAxis.Text, '', '', ''); FPSChartSource.LoadFromWorksheetGrid(WorksheetGrid); end; +procedure TFPSChartForm.btnLoadSpreadsheetClick(Sender: TObject); +var + Format: TsSpreadsheetFormat; + lExt: string; +begin + // First some logic to detect the format from the extension + lExt := ExtractFileExt(editSourceFile.Text); + if lExt = STR_EXCEL_EXTENSION then Format := sfExcel2 + else if lExt = STR_OOXML_EXCEL_EXTENSION then Format := sfOOXML + else if lExt = STR_OPENDOCUMENT_CALC_EXTENSION then Format := sfOpenDocument + else raise Exception.Create('Invalid File Extension'); + + // Now the actual loading + WorksheetGrid.LoadFromSpreadsheetFile(editSourceFile.Text, Format); +end; + +procedure TFPSChartForm.FormCreate(Sender: TObject); +begin + editSourceFile.InitialDir := ExtractFilePath(ParamStr(0)); +end; + end. diff --git a/components/fpspreadsheet/examples/fpschart/t1.xls b/components/fpspreadsheet/examples/fpschart/t1.xls new file mode 100644 index 000000000..4947be62c Binary files /dev/null and b/components/fpspreadsheet/examples/fpschart/t1.xls differ diff --git a/components/fpspreadsheet/examples/fpschart/t2.ods b/components/fpspreadsheet/examples/fpschart/t2.ods new file mode 100644 index 000000000..819f6d8bf Binary files /dev/null and b/components/fpspreadsheet/examples/fpschart/t2.ods differ diff --git a/components/fpspreadsheet/fpsopendocument.pas b/components/fpspreadsheet/fpsopendocument.pas index ec8618f9c..a542544fd 100755 --- a/components/fpspreadsheet/fpsopendocument.pas +++ b/components/fpspreadsheet/fpsopendocument.pas @@ -189,7 +189,8 @@ begin //process each cell of the row CellNode:=RowNode.FindNode('table:table-cell'); - while Assigned(CellNode) do begin + while Assigned(CellNode) do + begin ParamColsRepeated:=GetAttrValue(CellNode,'table:number-columns-repeated'); if ParamColsRepeated='' then ParamColsRepeated:='1'; @@ -200,11 +201,9 @@ begin for ColsCount:=0 to StrToInt(ParamColsRepeated)-1 do begin if ParamValueType='string' then ReadLabel(Row+RowsCount,Col+ColsCount,CellNode) - else - if ParamFormula<>'' then + else if ParamFormula<>'' then ReadFormula(Row+RowsCount,Col+ColsCount,CellNode) - else - if ParamValueType='float' then + else if ParamValueType='float' then ReadNumber(Row+RowsCount,Col+ColsCount,CellNode); end; //for ColsCount end; //for RowsCount @@ -226,7 +225,8 @@ end; procedure TsSpreadOpenDocReader.ReadFormula(ARow: Word; ACol : Word; ACellNode : TDOMNode); begin - + // For now just read the number + ReadNumber(ARow, ACol, ACellNode); end; procedure TsSpreadOpenDocReader.ReadLabel(ARow: Word; ACol : Word; ACellNode : TDOMNode); @@ -237,14 +237,21 @@ end; procedure TsSpreadOpenDocReader.ReadNumber(ARow: Word; ACol : Word; ACellNode : TDOMNode); var FSettings: TFormatSettings; - Value: String; + Value, Str: String; + lNumber: Double; begin FSettings.DecimalSeparator:='.'; Value:=GetAttrValue(ACellNode,'office:value'); - if UpperCase(Value)='1.#INF' then begin + if UpperCase(Value)='1.#INF' then + begin FWorkSheet.WriteNumber(Arow,ACol,1.0/0.0); - end else begin - FWorkSheet.WriteNumber(Arow,ACol,StrToFloat(GetAttrValue(ACellNode,'office:value'),FSettings)); + end + else + begin + // Don't merge, or else we can't debug + Str := GetAttrValue(ACellNode,'office:value'); + lNumber := StrToFloat(Str,FSettings); + FWorkSheet.WriteNumber(Arow,ACol,lNumber); end; end; diff --git a/components/fpspreadsheet/fpspreadsheet.pas b/components/fpspreadsheet/fpspreadsheet.pas index 089faaacd..cfcaaad29 100755 --- a/components/fpspreadsheet/fpspreadsheet.pas +++ b/components/fpspreadsheet/fpspreadsheet.pas @@ -74,7 +74,14 @@ type TCellContentType = (cctEmpty, cctFormula, cctRPNFormula, cctNumber, cctUTF8String); - {@@ Cell structure for TsWorksheet } + {@@ Cell structure for TsWorksheet + + Never suppose that all *Value fields are valid, + only one of the ContentTypes is valid. For other fields + use TWorksheet.ReadAsUTF8Text and similar methods + + @see TWorksheet.ReadAsUTF8Text + } TCell = record Col: Byte; // zero-based @@ -211,6 +218,8 @@ procedure RegisterSpreadFormat( AWriterClass: TsSpreadWriterClass; AFormat: TsSpreadsheetFormat); + + implementation uses diff --git a/components/fpspreadsheet/fpspreadsheetchart.pas b/components/fpspreadsheet/fpspreadsheetchart.pas index d2e020184..87725a5fa 100644 --- a/components/fpspreadsheet/fpspreadsheetchart.pas +++ b/components/fpspreadsheet/fpspreadsheetchart.pas @@ -19,12 +19,10 @@ uses // FPSpreadsheet Visual fpspreadsheetgrid, // FPSpreadsheet - fpspreadsheet; + fpspreadsheet, fpsutils; type - TsSelectionDirection = (fpsVerticalSelection, fpsHorizontalSelection); - {@@ Chart data source designed to work together with TChart from Lazarus to display the data. @@ -60,6 +58,7 @@ type constructor Create(AOwner: TComponent); override; destructor Destroy; override; procedure LoadFromWorksheetGrid(const AValue: TsWorksheetGrid); + procedure LoadPropertiesFromStrings(AXInterval, AYInterval, AXTitle, AYTitle, ATitle: string); public published // property WorksheetGrid: TsWorksheetGrid read FWorksheetGrid write SetWorksheetGrid; @@ -205,4 +204,16 @@ begin Notify; end; +procedure TsWorksheetChartSource.LoadPropertiesFromStrings(AXInterval, + AYInterval, AXTitle, AYTitle, ATitle: string); +var + lXCount, lYCount: Integer; +begin + ParseIntervalString(AXInterval, FXFirstCellRow, FXFirstCellCol, lXCount, FXSelectionDirection); + ParseIntervalString(AYInterval, FYFirstCellRow, FYFirstCellCol, lYCount, FYSelectionDirection); + if lXCount <> lYCount then raise Exception.Create( + 'TsWorksheetChartSource.LoadPropertiesFromStrings: Interval sizes don''t match'); + FPointsNumber := lXCount; +end; + end. diff --git a/components/fpspreadsheet/fpspreadsheetgrid.pas b/components/fpspreadsheet/fpspreadsheetgrid.pas index ba72a9ea8..36ed1cd4d 100644 --- a/components/fpspreadsheet/fpspreadsheetgrid.pas +++ b/components/fpspreadsheet/fpspreadsheetgrid.pas @@ -31,6 +31,7 @@ type { methods } constructor Create(AOwner: TComponent); override; procedure LoadFromWorksheet(AWorksheet: TsWorksheet); + procedure LoadFromSpreadsheetFile(AFileName: string; AFormat: TsSpreadsheetFormat; AWorksheetIndex: Integer = 0); procedure SaveToWorksheet(AWorksheet: TsWorksheet); property DisplayFixedColRow: Boolean read FDisplayFixedColRow write SetDisplayFixedColRow; end; @@ -210,7 +211,7 @@ begin begin lCol := lCell^.Col; lRow := lCell^.Row; - lStr := lCell^.UTF8StringValue; + lStr := FWorksheet.ReadAsUTF8Text(lRow, lCol); if DisplayFixedColRow then SetCells(lCol + 1, lRow + 1, lStr) @@ -221,6 +222,20 @@ begin end; end; +procedure TsCustomWorksheetGrid.LoadFromSpreadsheetFile(AFileName: string; + AFormat: TsSpreadsheetFormat; AWorksheetIndex: Integer); +var + lWorkbook: TsWorkbook; +begin + lWorkbook := TsWorkbook.Create; + try + lWorkbook.ReadFromFile(AFileName, AFormat); + LoadFromWorksheet(lWorkbook.GetWorksheetByIndex(AWorksheetIndex)); + finally + lWorkbook.Free; + end; +end; + procedure TsCustomWorksheetGrid.SaveToWorksheet(AWorksheet: TsWorksheet); var x, y: Integer; diff --git a/components/fpspreadsheet/fpsutils.pas b/components/fpspreadsheet/fpsutils.pas index 5c477a3c8..070abefe0 100644 --- a/components/fpspreadsheet/fpsutils.pas +++ b/components/fpspreadsheet/fpsutils.pas @@ -1,3 +1,6 @@ +{ + Utility functions from FPSpreadsheet +} unit fpsutils; {$mode objfpc}{$H+} @@ -5,8 +8,13 @@ unit fpsutils; interface uses - Classes, SysUtils; + Classes, SysUtils, StrUtils; +// Exported types +type + TsSelectionDirection = (fpsVerticalSelection, fpsHorizontalSelection); + +// Endianess helper functions function WordToLE(AValue: Word): Word; function DWordToLE(AValue: Cardinal): Cardinal; function IntegerToLE(AValue: Integer): Integer; @@ -16,6 +24,17 @@ function WordLEtoN(AValue: Word): Word; function DWordLEtoN(AValue: Cardinal): Cardinal; function WideStringLEToN(const AValue: WideString): WideString; +// Other routines +function ParseIntervalString(const AStr: string; + var AFirstCellRow, AFirstCellCol, ACount: Integer; + var ADirection: TsSelectionDirection): Boolean; +function ParseCellString(const AStr: string; + var ACellRow, ACellCol: Integer): Boolean; +function ParseCellRowString(const AStr: string; + var AResult: Integer): Boolean; +function ParseCellColString(const AStr: string; + var AResult: Integer): Boolean; + implementation { @@ -114,5 +133,136 @@ begin {$ENDIF} end; +{@@ + Parses strings like A5:A10 into an selection interval information +} +function ParseIntervalString(const AStr: string; + var AFirstCellRow, AFirstCellCol, ACount: Integer; + var ADirection: TsSelectionDirection): Boolean; +var + Cells: TStringList; + LastCellRow, LastCellCol: Integer; +begin + Result := True; + + // First get the cells + Cells := TStringList.Create; + ExtractStrings([':'],[], PChar(AStr), Cells); + + // Then parse each of them + Result := ParseCellString(Cells[0], AFirstCellRow, AFirstCellCol); + if not Result then Exit; + Result := ParseCellString(Cells[1], LastCellRow, LastCellCol); + if not Result then Exit; + + if AFirstCellRow = LastCellRow then + begin + ADirection := fpsHorizontalSelection; + ACount := LastCellCol - AFirstCellCol + 1; + end + else if AFirstCellCol = LastCellCol then + begin + ADirection := fpsVerticalSelection; + ACount := LastCellRow - AFirstCellRow + 1; + end + else Exit(False); +end; + +{@@ + Parses a cell string, like 'A1' into zero-based column and row numbers + + The parser is a simple state machine, with the following states: + + 0 - Reading Column part 1 (necesserely needs a letter) + 1 - Reading Column part 2, but could be the first number as well + 2 - Reading Row +} +function ParseCellString(const AStr: string; var ACellRow, ACellCol: Integer): Boolean; +var + i: Integer; + state: Integer; + Col, Row: string; + lChar: Char; +const + cLetters = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', + 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'W', 'X', 'Y', 'Z']; + cDigits = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; +begin + // Starting state + Result := True; + state := 0; + Col := ''; + Row := ''; + + // Separates the string into a row and a col + for i := 0 to Length(AStr) - 1 do + begin + lChar := AStr[i + 1]; + + case state of + + 0: + begin + if lChar in cLetters then + begin + Col := lChar; + state := 1; + end + else Exit(False); + end; + + 1: + begin + if lChar in cLetters then Col := Col + lChar + else if lChar in cDigits then + begin + Row := lChar; + state := 2; + end + else Exit(False); + end; + + 2: + begin + if lChar in cDigits then Row := Row + lChar + else Exit(False); + end; + + end; + end; + + // Now parses each separetely + ParseCellRowString(Row, ACellRow); + ParseCellColString(Col, ACellCol); +end; + +function ParseCellRowString(const AStr: string; var AResult: Integer): Boolean; +begin + try + AResult := StrToInt(AStr) - 1; + except + Result := False; + end; + Result := True; +end; + +function ParseCellColString(const AStr: string; var AResult: Integer): Boolean; +const + INT_NUM_LETTERS = 26; +begin + Result := False; + AResult := 0; + + if Length(AStr) = 1 then AResult := Ord(AStr[1]) - Ord('A') + else if Length(AStr) = 2 then + begin + AResult := (Ord(AStr[1]) - Ord('A') + 1) * INT_NUM_LETTERS + + Ord(AStr[2]) - Ord('A'); + end + else Exit(False); + + Result := True; +end; + end. diff --git a/components/fpspreadsheet/xlsbiff5.pas b/components/fpspreadsheet/xlsbiff5.pas index 51edc752a..940b10673 100755 --- a/components/fpspreadsheet/xlsbiff5.pas +++ b/components/fpspreadsheet/xlsbiff5.pas @@ -1250,9 +1250,11 @@ begin try // Only one stream is necessary for any number of worksheets OLEDocument.Stream := MemStream; - OLEStorage.ReadOLEFile(AFileName, OLEDocument); + // Check if the operation succeded + if MemStream.Size = 0 then raise Exception.Create('FPSpreadsheet: Reading the OLE document failed'); + // Rewind the stream and read from it MemStream.Position := 0; ReadFromStream(MemStream, AData); diff --git a/components/fpspreadsheet/xlsbiff8.pas b/components/fpspreadsheet/xlsbiff8.pas index 3de7d01d1..4566ec521 100755 --- a/components/fpspreadsheet/xlsbiff8.pas +++ b/components/fpspreadsheet/xlsbiff8.pas @@ -1234,9 +1234,11 @@ begin try // Only one stream is necessary for any number of worksheets OLEDocument.Stream := MemStream; - OLEStorage.ReadOLEFile(AFileName, OLEDocument,'Workbook'); + // Check if the operation succeded + if MemStream.Size = 0 then raise Exception.Create('FPSpreadsheet: Reading the OLE document failed'); + // Rewind the stream and read from it MemStream.Position := 0; ReadFromStream(MemStream, AData);