diff --git a/components/fpspreadsheet/source/common/fpsutils.pas b/components/fpspreadsheet/source/common/fpsutils.pas index 069ea0f1f..e63470a87 100644 --- a/components/fpspreadsheet/source/common/fpsutils.pas +++ b/components/fpspreadsheet/source/common/fpsutils.pas @@ -93,11 +93,14 @@ function GetCellRangeString(ARange: TsCellRange; function GetCellString(ARow,ACol: Cardinal; AFlags: TsRelFlags = [rfRelRow, rfRelCol]): String; function GetColString(AColIndex: Integer): String; +function GetRowString(ARowIndex: Integer): String; // -- "R1C1" syntax function ParseCellRangeString_R1C1(const AStr: string; ABaseRow, ABaseCol: Cardinal; out AFirstCellRow, AFirstCellCol, ALastCellRow, ALastCellCol: Cardinal; out AFlags: TsRelFlags): Boolean; +function ParseCellString_R1C1(const AStr: String; ABaseRow, ABaseCol: Cardinal; + out ASheet: String; out ACellRow, ACellCol: Cardinal; out AFlags: TsRelFlags): Boolean; overload; function ParseCellString_R1C1(const AStr: String; ABaseRow, ABaseCol: Cardinal; out ACellRow, ACellCol: Cardinal; out AFlags: TsRelFlags): Boolean; overload; function ParseCellString_R1C1(const AStr: string; ABaseRow, ABaseCol: Cardinal; @@ -701,6 +704,22 @@ begin if rfRelCol in f then Include(AFlags, rfRelCol2); end; +function ParseCellString_R1C1(const AStr: String; ABaseRow, ABaseCol: Cardinal; + out ASheet: String; out ACellRow, ACellCol: Cardinal; + out AFlags: TsRelFlags): Boolean; +var + p: Integer; +begin + p := pos('!', AStr); + if p > 0 then begin + ASheet := Copy(AStr, 1, p-1); + Result := ParseCellString_R1C1(Copy(AStr, p+1, MaxInt), ABaserow, ABaseCol, ACellRow, ACellCol, AFlags); + end else begin + ASheet := ''; + Result := ParseCellString_R1C1(AStr, ABaseRow, ABaseCol, ACellRow, ACellCol, AFlags); + end; +end; + {@@ ---------------------------------------------------------------------------- Parses a cell string in "R1C1" notation into zero-based column and row numbers 'AFlags' indicates relative addresses. @@ -1078,6 +1097,17 @@ begin end; end; +{@@ ---------------------------------------------------------------------------- + Calculates an Excel row name ('1', '2' etc) from the zero-based row index + + @param ARowIndex Zero-based row index + @return Numerical, one-based row name string. +-------------------------------------------------------------------------------} +function GetRowString(ARowIndex: Integer): String; +begin + Result := IntToStr(ARowIndex+1); +end; + const RELCHAR: Array[boolean] of String = ('$', ''); diff --git a/components/fpspreadsheet/source/common/xlsxml.pas b/components/fpspreadsheet/source/common/xlsxml.pas index 20bcfbce6..35b54aa3d 100644 --- a/components/fpspreadsheet/source/common/xlsxml.pas +++ b/components/fpspreadsheet/source/common/xlsxml.pas @@ -43,8 +43,10 @@ type procedure ReadCell(ANode: TDOMNode; AWorksheet: TsBasicWorksheet; ARow, ACol: Integer); procedure ReadCellProtection(ANode: TDOMNode; var AFormat: TsCellFormat); procedure ReadComment(ANode: TDOMNode; AWorksheet: TsBasicWorksheet; ACell: PCell); + procedure ReadExcelWorkbook(ANode: TDOMNode); procedure ReadFont(ANode: TDOMNode; var AFormat: TsCellFormat); procedure ReadInterior(ANode: TDOMNode; var AFormat: TsCellFormat); + procedure ReadNames(ANode: TDOMNode; AWorksheet: TsBasicWorksheet); procedure ReadNumberFormat(ANode: TDOMNode; var AFormat: TsCellFormat); procedure ReadRow(ANode: TDOMNode; AWorksheet: TsBasicWorksheet; ARow: Integer); procedure ReadStyle(ANode: TDOMNode); @@ -193,6 +195,11 @@ begin end; end; + +{=============================================================================== + TsSpreadExcelXMLReader +===============================================================================} + {@@ ---------------------------------------------------------------------------- Constructor of the ExcelXML reader -------------------------------------------------------------------------------} @@ -539,6 +546,41 @@ begin TsWorksheet(AWorksheet).WriteComment(ACell, txt); end; +{@@ ---------------------------------------------------------------------------- + Reads the "ExcelWorkbook" node +-------------------------------------------------------------------------------} +procedure TsSpreadExcelXMLReader.ReadExcelWorkbook(ANode: TDOMNode); +var + s: String; + nodeName: String; + n: Integer; +begin + if ANode = nil then + exit; + + ANode := ANode.FirstChild; + while ANode <> nil do begin + nodeName := ANode.NodeName; + if nodeName = 'ActiveSheet' then begin + s := ANode.TextContent; + if (s <> '') and TryStrToInt(s, n) then + with TsWorkbook(FWorkbook) do + SelectWorksheet(GetWorksheetByIndex(n)); + end else + if nodeName = 'ProtectStructure' then begin + s := ANode.TextContent; + if s = 'True' then + FWorkbook.Protection := FWorkbook.Protection + [bpLockStructure]; + end else + if nodeName = 'ProtectWindows' then begin + s := ANode.TextContent; + if s = 'True' then + FWorkbook.Protection := FWorkbook.Protection + [bpLockWindows]; + end; + ANode := ANode.NextSibling; + end; +end; + {@@ ---------------------------------------------------------------------------- Reads the "Styles/Style/Font" node -------------------------------------------------------------------------------} @@ -629,6 +671,91 @@ begin Include(AFormat.UsedFormattingFields, uffBackground); end; +{@@ ---------------------------------------------------------------------------- + Reads a "Worksheet/Names" node +-------------------------------------------------------------------------------} +procedure TsSpreadExcelXMLReader.ReadNames(ANode: TDOMNode; + AWorksheet: TsBasicWorksheet); + + procedure DoProcess(AStr: String; var ARowIndex, AColIndex: Cardinal; + out IsRow: Boolean); + var + p: Integer; + begin + p := pos('!', AStr); + if p > 0 then AStr := Copy(AStr, p+1, MaxInt); + IsRow := AStr[1] in ['R', 'r']; + Delete(AStr, 1, 1); + if IsRow then + ARowIndex := StrToInt(AStr) - 1 + else + AColIndex := StrToInt(AStr) - 1; + end; + + procedure DoRepeatedRowsCols(AStr: String); + var + p: Integer; + isRow: Boolean; + r1: Cardinal = UNASSIGNED_ROW_COL_INDEX; + c1: Cardinal = UNASSIGNED_ROW_COL_INDEX; + r2: Cardinal = UNASSIGNED_ROW_COL_INDEX; + c2: Cardinal = UNASSIGNED_ROW_COL_INDEX; + begin + p := pos(':', AStr); + // No colon --> Single range, e.g. "=Sheet1!C1" + if p = 0 then + begin + DoProcess(AStr, r1, c1, isRow); + r2 := r1; + c2 := c1; + end else + // Colon --> Range block, e.g. "Sheet1!R1:R2" + begin + DoProcess(copy(AStr, 1, p-1), r1, c1, isRow); + DoProcess(copy(AStr, p+1, MaxInt), r2, c2, isRow); + end; + if isRow then + TsWorksheet(AWorksheet).PageLayout.SetRepeatedRows(r1, r2) + else + TsWorksheet(AWorksheet).PageLayout.SetRepeatedCols(c1, c2); + end; + +var + s, s1: String; + nodeName: String; + sheet1, sheet2: String; + r1, c1, r2, c2: Cardinal; + flags: TsRelFlags; + p, q: Integer; +begin + while ANode <> nil do begin + nodeName := ANode.NodeName; + if nodeName = 'NamedRange' then begin + s := GetAttrValue(ANode, 'ss:Name'); + if s = 'Print_Area' then begin + // + s := GetAttrValue(ANode, 'ss:RefersTo'); + if (s <> '') and ParseCellRangeString_R1C1(s, 0, 0, sheet1, sheet2, r1, c1, r2, c2, flags) then + TsWorksheet(AWorksheet).PageLayout.AddPrintRange(r1, c1, r2, c2); + // to do: include sheet names here! + end else + if s = 'Print_Titles' then begin + // + s := GetAttrValue(ANode, 'ss:RefersTo'); + if s <> '' then begin + p := pos(',', s); + if p > 0 then begin + DoRepeatedRowsCols(copy(s, 1, p-1)); + DoRepeatedRowsCols(copy(s, p+1, MaxInt)); + end else + DoRepeatedRowsCols(s); + end; + end; + end; + ANode := ANode.NextSibling; + end; +end; + {@@ ---------------------------------------------------------------------------- Reads a "Styles/Style/NumberFormat" node -------------------------------------------------------------------------------} @@ -801,6 +928,11 @@ begin end; end; + // Hidden + s := GetAttrValue(ANode, 'ss:Hidden'); + if s = '1' then + sheet.HideCol(c); + inc(c); end else @@ -824,6 +956,11 @@ begin if (s <> '') and TryStrToFloat(s, x, FPointSeparatorSettings) then sheet.WriteRowHeight(r, x, suPoints); + // Hidden + s := GetAttrValue(ANode, 'ss:Hidden'); + if (s = '1') then + sheet.HideRow(r); + // Row format s := GetAttrValue(ANode, 'ss:StyleID'); if s <> '' then begin @@ -853,12 +990,22 @@ var nodeName: String; s: String; begin + if ANode = nil then + exit; + + s := GetAttrValue(ANode, 'ss:Protected'); + if s ='1' then + AWorksheet.Options := AWorksheet.Options + [soProtected]; + ; + ANode := ANode.FirstChild; while ANode <> nil do begin nodeName := ANode.NodeName; if nodeName = 'Table' then ReadTable(ANode.FirstChild, AWorksheet) else if nodeName = 'WorksheetOptions' then - ReadWorksheetOptions(ANode.FirstChild, AWorksheet); + ReadWorksheetOptions(ANode.FirstChild, AWorksheet) + else if nodeName = 'Names' then + ReadNames(ANode.FirstChild, AWorksheet); ANode := ANode.NextSibling; end; end; @@ -870,12 +1017,15 @@ procedure TsSpreadExcelXMLReader.ReadWorksheetOptions(ANode: TDOMNode; AWorksheet: TsBasicWorksheet); var sheet: TsWorksheet absolute AWorksheet; - node: TDOMNode; + node, childnode: TDOMNode; nodeName: String; s: String; x: Double; n: Integer; hasFitToPage: Boolean = false; + c, r: Cardinal; + r1, c1, r2, c2: Cardinal; + flags: TsRelFlags; begin if ANode = nil then exit; @@ -984,7 +1134,110 @@ begin end; node := node.NextSibling; end; + end else + if nodeName = 'Selected' then + TsWorkbook(FWorkbook).ActiveWorksheet := sheet + else + if nodeName = 'Panes' then begin + c := sheet.ActiveCellCol; + r := sheet.ActiveCellRow; + node := ANode.FirstChild; + while node <> nil do begin + nodeName := node.NodeName; + if nodeName = 'Pane' then begin + childnode := node.FirstChild; + while childnode <> nil do begin + nodeName := childNode.NodeName; + if nodeName = 'ActiveRow' then begin + s := childNode.TextContent; + if (s <> '') and TryStrToInt(s, n) then + r := n; + end else + if nodeName = 'ActiveCol' then begin + s := childNode.TextContent; + if (s <> '') and TryStrToInt(s, n) then + c := n; + end; + childnode := childNode.NextSibling; + end; + end; + node := node.NextSibling; + end; + sheet.SelectCell(r, c); + end else + if nodeName = 'FreezePanes' then + sheet.Options := sheet.Options + [soHasFrozenPanes] + else + if (nodeName = 'TopRowBottomPane') then begin + s := ANode.TextContent; + if (s <> '') and TryStrToInt(s, n) then + sheet.TopPaneHeight := n; + end else + if (nodeName = 'LeftColumnRightPane') then begin + s := ANode.TextContent; + if (s <> '') and TryStrToInt(s, n) then + sheet.LeftPaneWidth := n; + end else + if nodeName = 'DoNotDisplayGridlines' then + sheet.Options := sheet.Options - [soShowGridLines] + else + if nodeName = 'DoNotDisplayHeadings' then + sheet.Options := sheet.Options - [soShowHeaders] + else + if nodeName = 'Zoom' then begin + s := ANode.TextContent; + if (s <> '') and TryStrToFloat(s, x) then + sheet.Zoomfactor := x * 0.01; + end else + if nodeName = 'Visible' then begin + s := ANode.TextContent; + if s = 'SheetHidden' then + sheet.Options := sheet.Options + [soHidden]; + end else + if nodeName = 'AllowFormatCells' then + sheet.Protection := sheet.Protection - [spFormatCells] + else + if nodeName = 'AllowSizeCols' then + sheet.Protection := sheet.Protection - [spFormatColumns] + else + if nodeName = 'AllowSizeRows' then + sheet.Protection := sheet.Protection - [spFormatRows] + else + if nodeName = 'AllowInsertCols' then + sheet.Protection := sheet.Protection - [spInsertColumns] + else + if nodeName = 'AllowInsertRows' then + sheet.Protection := sheet.Protection - [spInsertRows] + else + if nodeName = 'AllowInsertHyperlinks' then + sheet.Protection := sheet.Protection - [spInsertHyperLinks] + else + if nodeName = 'AllowDeleteCols' then + sheet.Protection := sheet.Protection - [spDeleteColumns] + else + if nodeName = 'AllowDeleteRows' then + sheet.Protection := sheet.Protection - [spDeleteRows] + else + if nodeName = 'AllowSort' then + sheet.Protection := sheet.Protection - [spSort] + else + if nodeName = 'ProtectObjects' then + sheet.Protection := sheet.Protection + [spObjects] + else + { + if nodeName = 'ProtectScenarios' then + sheet.Protection := sheet.Protection + [spScenarios]; + else + } + if nodeName = 'EnableSelection' then begin + s := ANode.TextContent; + if s = 'NoSelection' then + sheet.Protection := sheet.Protection + [spSelectLockedCells, spSelectUnlockedCells] + else + if s = 'Unlocked' then + sheet.Protection := sheet.Protection + [spSelectLockedCells]; end; + ANode := ANode.NextSibling; end; @@ -1029,7 +1282,7 @@ begin s := GetAttrValue(ANode, 'ss:Name'); if s <> '' then begin // the case of '' should not happen FWorksheet := TsWorkbook(FWorkbook).AddWorksheet(s); - ReadWorksheet(ANode.FirstChild, FWorksheet); + ReadWorksheet(ANode, FWorksheet); end; end; ANode := ANode.NextSibling; @@ -1047,14 +1300,26 @@ var begin try ReadXMLStream(doc, AStream); + + // Read style list ReadStyles(doc.DocumentElement.FindNode('Styles')); + + // Read worksheets and their contents ReadWorksheets(doc.DocumentElement.FindNode('Worksheet')); + + // Read ExcelWorkbook node after worksheet nodes although before it is + // found before the worksheet nodes in the file, because is requires + // worksheets to be existing. + ReadExcelWorkbook(doc.DocumentElement.FindNode('ExcelWorkbook')); finally doc.Free; end; end; +{=============================================================================== + TsSpreadExcelXMLWriter +===============================================================================} {@@ ---------------------------------------------------------------------------- Constructor of the ExcelXML writer @@ -1815,6 +2080,8 @@ begin StrUtils.IfThen(AWorksheet.IsProtected {and [spScenarios in AWorksheet.Protection])}, 'True', 'False') ]); + // todo - Several protection options + // Put it all together... AppendToStream(AStream, INDENT2 + '' + LF + INDENT3 + diff --git a/components/fpspreadsheet/source/visual/fpspreadsheetctrls.pas b/components/fpspreadsheet/source/visual/fpspreadsheetctrls.pas index 25c8e3f26..84dad2c65 100644 --- a/components/fpspreadsheet/source/visual/fpspreadsheetctrls.pas +++ b/components/fpspreadsheet/source/visual/fpspreadsheetctrls.pas @@ -4106,6 +4106,41 @@ begin else AStrings.Add(' FooterImage, right='); + if ASheet.PageLayout.NumPrintRanges = 0 then + AStrings.Add(' Print ranges=') + else + for i := 0 to ASheet.PageLayout.NumPrintRanges-1 do + with ASheet.PageLayout.PrintRange[i] do + AStrings.Add(Format(' Print range #%d=$%s$%s:$%s$%s', [ i, + GetColString(Col1), GetRowString(Row1), GetColString(Col2), GetRowString(Row2) + ])); + + if ASheet.PageLayout.RepeatedRows.FirstIndex = UNASSIGNED_ROW_COL_INDEX then + AStrings.Add(' Repeated rows=') + else + if ASheet.PageLayout.RepeatedRows.FirstIndex = ASheet.PageLayout.RepeatedRows.LastIndex then + AStrings.Add(Format(' Repeated rows=$%s', [ + GetRowString(ASheet.PageLayout.RepeatedRows.FirstIndex) + ])) + else + AStrings.Add(Format(' Repeated rows=$%s:$%s', [ + GetRowString(ASheet.PageLayout.RepeatedRows.FirstIndex), + GetRowString(ASheet.PageLayout.RepeatedRows.lastIndex) + ])); + + if ASheet.PageLayout.RepeatedCols.FirstIndex = UNASSIGNED_ROW_COL_INDEX then + AStrings.Add(' Repeated columns=') + else + if ASheet.PageLayout.RepeatedCols.FirstIndex = ASheet.PageLayout.RepeatedCols.LastIndex then + AStrings.Add(Format(' Repeated columns=$%s', [ + GetColString(ASheet.PageLayout.RepeatedCols.FirstIndex) + ])) + else + AStrings.Add(Format(' Repeated columns=$%s:$%s', [ + GetColString(ASheet.PageLayout.RepeatedCols.FirstIndex), + GetColString(ASheet.PageLayout.RepeatedCols.lastIndex) + ])); + s := ''; for po in TsPrintOption do if po in ASheet.PageLayout.Options then s := s + '; ' + GetEnumName(typeInfo(TsPrintOption), ord(po));