From 8d996ca138d40069bce92ef8e155f7a268979045 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 21 Jul 2023 00:03:37 +0800 Subject: [PATCH] This closes #1582, fixes the formula calculation bug, and improves form controls - Fix incorrect formula calculate results on a nested argument function which returns a numeric result - Add a new exported error variable `ErrorFormControlValue` - Rename exported enumeration `FormControlCheckbox` to `FormControlCheckBox` - Rename exported enumeration `FormControlRadio` to `FormControlOptionButton` - The `AddFormControl` function supports new 5 form controls: spin button, check box, group box, label, and scroll bar - Update documentation for the `GraphicOptions` data type, `AddFormControl` and `NewStreamWriter` functions - Update the unit tests --- calc.go | 16 +-- calc_test.go | 17 +++ errors.go | 3 + picture.go | 64 +++++----- picture_test.go | 2 + stream.go | 18 +-- vml.go | 323 ++++++++++++++++++++++++++++++++++-------------- vmlDrawing.go | 93 +++++++++----- vml_test.go | 84 +++++++------ xmlDrawing.go | 4 + 10 files changed, 421 insertions(+), 203 deletions(-) diff --git a/calc.go b/calc.go index 3c44949..66493bf 100644 --- a/calc.go +++ b/calc.go @@ -1053,16 +1053,16 @@ func (f *File) evalInfixExpFunc(ctx *calcContext, sheet, cell string, token, nex if nextToken.TType == efp.TokenTypeOperatorInfix || (opftStack.Len() > 1 && opfdStack.Len() > 0) { // mathematics calculate in formula function opfdStack.Push(arg) - } else { - argsStack.Peek().(*list.List).PushBack(arg) + return newEmptyFormulaArg() } - } else { - val := arg.Value() - if arg.Type == ArgMatrix && len(arg.Matrix) > 0 && len(arg.Matrix[0]) > 0 { - val = arg.Matrix[0][0].Value() - } - opdStack.Push(newStringFormulaArg(val)) + argsStack.Peek().(*list.List).PushBack(arg) + return newEmptyFormulaArg() } + if arg.Type == ArgMatrix && len(arg.Matrix) > 0 && len(arg.Matrix[0]) > 0 { + opdStack.Push(arg.Matrix[0][0]) + return newEmptyFormulaArg() + } + opdStack.Push(arg) return newEmptyFormulaArg() } diff --git a/calc_test.go b/calc_test.go index 24c6efa..702aeb4 100644 --- a/calc_test.go +++ b/calc_test.go @@ -5918,6 +5918,23 @@ func TestCalcCellResolver(t *testing.T) { assert.NoError(t, err, formula) assert.Equal(t, expected, result, formula) } + // Test calculates formula that contains a nested argument function which returns a numeric result + f = NewFile() + for _, cell := range []string{"A1", "B2", "B3", "B4"} { + assert.NoError(t, f.SetCellValue("Sheet1", cell, "ABC")) + } + for cell, formula := range map[string]string{ + "A2": "IF(B2<>\"\",MAX(A1:A1)+1,\"\")", + "A3": "IF(B3<>\"\",MAX(A1:A2)+1,\"\")", + "A4": "IF(B4<>\"\",MAX(A1:A3)+1,\"\")", + } { + assert.NoError(t, f.SetCellFormula("Sheet1", cell, formula)) + } + for cell, expected := range map[string]string{"A2": "1", "A3": "2", "A4": "3"} { + result, err := f.CalcCellValue("Sheet1", cell) + assert.NoError(t, err) + assert.Equal(t, expected, result) + } } func TestEvalInfixExp(t *testing.T) { diff --git a/errors.go b/errors.go index b8d2022..254890e 100644 --- a/errors.go +++ b/errors.go @@ -255,4 +255,7 @@ var ( // ErrUnprotectWorkbookPassword defined the error message on remove workbook // protection with password verification failed. ErrUnprotectWorkbookPassword = errors.New("workbook protect password not match") + // ErrorFormControlValue defined the error message for receiving a scroll + // value exceeds limit. + ErrorFormControlValue = fmt.Errorf("scroll value must be between 0 and %d", MaxFormControlValue) ) diff --git a/picture.go b/picture.go index fb14c95..9b026f5 100644 --- a/picture.go +++ b/picture.go @@ -110,41 +110,46 @@ func parseGraphicOptions(opts *GraphicOptions) *GraphicOptions { // } // } // -// The optional parameter "AutoFit" specifies if you make image size auto-fits the -// cell, the default value of that is 'false'. +// The optional parameter "AltText" is used to add alternative text to a graph +// object. // -// The optional parameter "Hyperlink" specifies the hyperlink of the image. +// The optional parameter "PrintObject" indicates whether the graph object is +// printed when the worksheet is printed, the default value of that is 'true'. +// +// The optional parameter "Locked" indicates whether lock the graph object. +// Locking an object has no effect unless the sheet is protected. +// +// The optional parameter "LockAspectRatio" indicates whether lock aspect ratio +// for the graph object, the default value of that is 'false'. +// +// The optional parameter "AutoFit" specifies if you make graph object size +// auto-fits the cell, the default value of that is 'false'. +// +// The optional parameter "OffsetX" specifies the horizontal offset of the graph +// object with the cell, the default value of that is 0. +// +// The optional parameter "OffsetY" specifies the vertical offset of the graph +// object with the cell, the default value of that is 0. +// +// The optional parameter "ScaleX" specifies the horizontal scale of graph +// object, the default value of that is 1.0 which presents 100%. +// +// The optional parameter "ScaleY" specifies the vertical scale of graph object, +// the default value of that is 1.0 which presents 100%. +// +// The optional parameter "Hyperlink" specifies the hyperlink of the graph +// object. // // The optional parameter "HyperlinkType" defines two types of // hyperlink "External" for website or "Location" for moving to one of the // cells in this workbook. When the "HyperlinkType" is "Location", // coordinates need to start with "#". // -// The optional parameter "Positioning" defines two types of the position of an -// image in an Excel spreadsheet, "oneCell" (Move but don't size with -// cells) or "absolute" (Don't move or size with cells). If you don't set this -// parameter, the default positioning is move and size with cells. -// -// The optional parameter "PrintObject" indicates whether the image is printed -// when the worksheet is printed, the default value of that is 'true'. -// -// The optional parameter "LockAspectRatio" indicates whether lock aspect -// ratio for the image, the default value of that is 'false'. -// -// The optional parameter "Locked" indicates whether lock the image. Locking -// an object has no effect unless the sheet is protected. -// -// The optional parameter "OffsetX" specifies the horizontal offset of the -// image with the cell, the default value of that is 0. -// -// The optional parameter "ScaleX" specifies the horizontal scale of images, -// the default value of that is 1.0 which presents 100%. -// -// The optional parameter "OffsetY" specifies the vertical offset of the -// image with the cell, the default value of that is 0. -// -// The optional parameter "ScaleY" specifies the vertical scale of images, -// the default value of that is 1.0 which presents 100%. +// The optional parameter "Positioning" defines 3 types of the position of a +// graph object in a spreadsheet: "oneCell" (Move but don't size with +// cells), "twoCell" (Move and size with cells), and "absolute" (Don't move or +// size with cells). If you don't set this parameter, the default positioning +// is to move and size with cells. func (f *File) AddPicture(sheet, cell, name string, opts *GraphicOptions) error { var err error // Check picture exists first. @@ -330,6 +335,9 @@ func (f *File) addDrawingPicture(sheet, drawingXML, cell, ext string, rID, hyper if err != nil { return err } + if opts.Positioning != "" && inStrSlice(supportedPositioning, opts.Positioning, true) == -1 { + return ErrParameterInvalid + } width, height := img.Width, img.Height if opts.AutoFit { if width, height, col, row, err = f.drawingResize(sheet, cell, float64(width), float64(height), opts); err != nil { diff --git a/picture_test.go b/picture_test.go index a2e0fb7..c3c0d6e 100644 --- a/picture_test.go +++ b/picture_test.go @@ -192,6 +192,8 @@ func TestAddDrawingPicture(t *testing.T) { f := NewFile() opts := &GraphicOptions{PrintObject: boolPtr(true), Locked: boolPtr(false)} assert.EqualError(t, f.addDrawingPicture("sheet1", "", "A", "", 0, 0, image.Config{}, opts), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) + // Test addDrawingPicture with invalid positioning types + assert.Equal(t, f.addDrawingPicture("sheet1", "", "A1", "", 0, 0, image.Config{}, &GraphicOptions{Positioning: "x"}), ErrParameterInvalid) path := "xl/drawings/drawing1.xml" f.Pkg.Store(path, MacintoshCyrillicCharset) diff --git a/stream.go b/stream.go index 82d7129..13a14d8 100644 --- a/stream.go +++ b/stream.go @@ -38,14 +38,16 @@ type StreamWriter struct { tableParts string } -// NewStreamWriter return stream writer struct by given worksheet name for -// generate new worksheet with large amounts of data. Note that after set -// rows, you must call the 'Flush' method to end the streaming writing process -// and ensure that the order of row numbers is ascending, the normal mode -// functions and stream mode functions can't be work mixed to writing data on -// the worksheets, you can't get cell value when in-memory chunks data over -// 16MB. For example, set data for worksheet of size 102400 rows x 50 columns -// with numbers and style: +// NewStreamWriter returns stream writer struct by given worksheet name used for +// writing data on a new existing empty worksheet with large amounts of data. +// Note that after writing data with the stream writer for the worksheet, you +// must call the 'Flush' method to end the streaming writing process, ensure +// that the order of row numbers is ascending when set rows, and the normal +// mode functions and stream mode functions can not be work mixed to writing +// data on the worksheets. The stream writer will try to use temporary files on +// disk to reduce the memory usage when in-memory chunks data over 16MB, and +// you can't get cell value at this time. For example, set data for worksheet +// of size 102400 rows x 50 columns with numbers and style: // // f := excelize.NewFile() // defer func() { diff --git a/vml.go b/vml.go index 02d9e19..cbe07e2 100644 --- a/vml.go +++ b/vml.go @@ -28,8 +28,12 @@ type FormControlType byte const ( FormControlNote FormControlType = iota FormControlButton - FormControlCheckbox - FormControlRadio + FormControlOptionButton + FormControlSpinButton + FormControlCheckBox + FormControlGroupBox + FormControlLabel + FormControlScrollBar ) // GetComments retrieves all comments in a worksheet by given worksheet name. @@ -105,13 +109,8 @@ func (f *File) getSheetComments(sheetFile string) string { // }) func (f *File) AddComment(sheet string, opts Comment) error { return f.addVMLObject(vmlOptions{ - Sheet: sheet, - Author: opts.Author, - AuthorID: opts.AuthorID, - Cell: opts.Cell, - Text: opts.Text, - Type: FormControlNote, - Paragraph: opts.Paragraph, + sheet: sheet, Comment: opts, + FormControl: FormControl{Cell: opts.Cell, Type: FormControlNote}, }) } @@ -184,18 +183,18 @@ func (f *File) addComment(commentsXML string, opts vmlOptions) error { return err } chars, cmt := 0, xlsxComment{ - Ref: opts.Cell, + Ref: opts.Comment.Cell, AuthorID: authorID, Text: xlsxText{R: []xlsxR{}}, } - if opts.Text != "" { - if len(opts.Text) > TotalCellChars { - opts.Text = opts.Text[:TotalCellChars] + if opts.Comment.Text != "" { + if len(opts.Comment.Text) > TotalCellChars { + opts.Comment.Text = opts.Comment.Text[:TotalCellChars] } - cmt.Text.T = stringPtr(opts.Text) - chars += len(opts.Text) + cmt.Text.T = stringPtr(opts.Comment.Text) + chars += len(opts.Comment.Text) } - for _, run := range opts.Paragraph { + for _, run := range opts.Comment.Paragraph { if chars == TotalCellChars { break } @@ -277,9 +276,15 @@ func (f *File) commentsWriter() { // AddFormControl provides the method to add form control button in a worksheet // by given worksheet name and form control options. Supported form control -// type: button and radio. The file extension should be XLSM or XLTM. For -// example: +// type: button, check box, group box, label, option button, scroll bar and +// spinner. If set macro for the form control, the workbook extension should be +// XLSM or XLTM. Scroll value must be between 0 and 30000. // +// Example 1, add button form control with macro, rich-text, custom button size, +// print property on Sheet1!A1, and let the button do not move or size with +// cells: +// +// enable := true // err := f.AddFormControl("Sheet1", excelize.FormControl{ // Cell: "A1", // Type: excelize.FormControlButton, @@ -300,20 +305,51 @@ func (f *File) commentsWriter() { // Text: "C1=A1+B1", // }, // }, +// Format: excelize.GraphicOptions{ +// PrintObject: &enable, +// Positioning: "absolute", +// }, +// }) +// +// Example 2, add option button form control with checked status and text on +// Sheet1!A1: +// +// err := f.AddFormControl("Sheet1", excelize.FormControl{ +// Cell: "A1", +// Type: excelize.FormControlOptionButton, +// Text: "Option Button 1", +// Checked: true, +// }) +// +// Example 3, add spin button form control on Sheet1!A2 to increase or decrease +// the value of Sheet1!A1: +// +// err := f.AddFormControl("Sheet1", excelize.FormControl{ +// Cell: "A2", +// Type: excelize.FormControlSpinButton, +// CurrentVal: 7, +// MinVal: 5, +// MaxVal: 10, +// IncChange: 1, +// CellLink: "A1", +// }) +// +// Example 4, add horizontally scroll bar form control on Sheet1!A2 to change +// the value of Sheet1!A1 by click the scroll arrows or drag the scroll box: +// +// err := f.AddFormControl("Sheet1", excelize.FormControl{ +// Cell: "A2", +// Type: excelize.FormControlScrollBar, +// CurrentVal: 50, +// MinVal: 10, +// MaxVal: 100, +// IncChange: 1, +// PageChange: 1, +// CellLink: "A1", // }) func (f *File) AddFormControl(sheet string, opts FormControl) error { return f.addVMLObject(vmlOptions{ - FormCtrl: true, - Sheet: sheet, - Type: opts.Type, - Checked: opts.Checked, - Cell: opts.Cell, - Macro: opts.Macro, - Width: opts.Width, - Height: opts.Height, - Format: opts.Format, - Text: opts.Text, - Paragraph: opts.Paragraph, + formCtrl: true, sheet: sheet, FormControl: opts, }) } @@ -443,41 +479,41 @@ func (f *File) vmlDrawingWriter() { // relationships for comments and form controls. func (f *File) addVMLObject(opts vmlOptions) error { // Read sheet data. - ws, err := f.workSheetReader(opts.Sheet) + ws, err := f.workSheetReader(opts.sheet) if err != nil { return err } vmlID := f.countComments() + 1 - if opts.FormCtrl { - if opts.Type > FormControlRadio { + if opts.formCtrl { + if opts.Type > FormControlScrollBar { return ErrParameterInvalid } vmlID = f.countVMLDrawing() + 1 } drawingVML := "xl/drawings/vmlDrawing" + strconv.Itoa(vmlID) + ".vml" sheetRelationshipsDrawingVML := "../drawings/vmlDrawing" + strconv.Itoa(vmlID) + ".vml" - sheetXMLPath, _ := f.getSheetXMLPath(opts.Sheet) + sheetXMLPath, _ := f.getSheetXMLPath(opts.sheet) sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetXMLPath, "xl/worksheets/") + ".rels" if ws.LegacyDrawing != nil { // The worksheet already has a VML relationships, use the relationships drawing ../drawings/vmlDrawing%d.vml. - sheetRelationshipsDrawingVML = f.getSheetRelationshipsTargetByID(opts.Sheet, ws.LegacyDrawing.RID) + sheetRelationshipsDrawingVML = f.getSheetRelationshipsTargetByID(opts.sheet, ws.LegacyDrawing.RID) vmlID, _ = strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(sheetRelationshipsDrawingVML, "../drawings/vmlDrawing"), ".vml")) drawingVML = strings.ReplaceAll(sheetRelationshipsDrawingVML, "..", "xl") } else { // Add first VML drawing for given sheet. rID := f.addRels(sheetRels, SourceRelationshipDrawingVML, sheetRelationshipsDrawingVML, "") - f.addSheetNameSpace(opts.Sheet, SourceRelationship) - f.addSheetLegacyDrawing(opts.Sheet, rID) + f.addSheetNameSpace(opts.sheet, SourceRelationship) + f.addSheetLegacyDrawing(opts.sheet, rID) } if err = f.addDrawingVML(vmlID, drawingVML, prepareFormCtrlOptions(&opts)); err != nil { return err } - if !opts.FormCtrl { + if !opts.formCtrl { commentsXML := "xl/comments" + strconv.Itoa(vmlID) + ".xml" if err = f.addComment(commentsXML, opts); err != nil { return err } - if sheetXMLPath, ok := f.getSheetXMLPath(opts.Sheet); ok && f.getSheetComments(filepath.Base(sheetXMLPath)) == "" { + if sheetXMLPath, ok := f.getSheetXMLPath(opts.sheet); ok && f.getSheetComments(filepath.Base(sheetXMLPath)) == "" { sheetRelationshipsComments := "../comments" + strconv.Itoa(vmlID) + ".xml" f.addRels(sheetRels, SourceRelationshipComments, sheetRelationshipsComments, "") } @@ -488,7 +524,7 @@ func (f *File) addVMLObject(opts vmlOptions) error { // prepareFormCtrlOptions provides a function to parse the format settings of // the form control with default value. func prepareFormCtrlOptions(opts *vmlOptions) *vmlOptions { - for _, runs := range opts.Paragraph { + for _, runs := range opts.FormControl.Paragraph { for _, subStr := range strings.Split(runs.Text, "\n") { opts.rows++ if chars := len(subStr); chars > opts.cols { @@ -496,8 +532,8 @@ func prepareFormCtrlOptions(opts *vmlOptions) *vmlOptions { } } } - if len(opts.Paragraph) == 0 { - opts.rows, opts.cols = 1, len(opts.Text) + if len(opts.FormControl.Paragraph) == 0 { + opts.rows, opts.cols = 1, len(opts.FormControl.Text) } if opts.Format.ScaleX == 0 { opts.Format.ScaleX = 1 @@ -520,10 +556,10 @@ func prepareFormCtrlOptions(opts *vmlOptions) *vmlOptions { // formCtrlText returns font element in the VML for control form text. func formCtrlText(opts *vmlOptions) []vmlFont { var font []vmlFont - if opts.Text != "" { - font = append(font, vmlFont{Content: opts.Text}) + if opts.FormControl.Text != "" { + font = append(font, vmlFont{Content: opts.FormControl.Text}) } - for _, run := range opts.Paragraph { + for _, run := range opts.FormControl.Paragraph { fnt := vmlFont{ Content: run.Text + "

\r\n", } @@ -551,22 +587,10 @@ func formCtrlText(opts *vmlOptions) []vmlFont { return font } -var formCtrlPresets = map[FormControlType]struct { - objectType string - filled string - fillColor string - stroked string - strokeColor string - strokeButton string - fill *vFill - textHAlign string - textVAlign string - noThreeD *string - firstButton *string - shadow *vShadow -}{ +var formCtrlPresets = map[FormControlType]formCtrlPreset{ FormControlNote: { objectType: "Note", + autoFill: "True", filled: "", fillColor: "#FBF6D6", stroked: "", @@ -586,6 +610,7 @@ var formCtrlPresets = map[FormControlType]struct { }, FormControlButton: { objectType: "Button", + autoFill: "True", filled: "", fillColor: "buttonFace [67]", stroked: "", @@ -603,8 +628,9 @@ var formCtrlPresets = map[FormControlType]struct { firstButton: nil, shadow: nil, }, - FormControlCheckbox: { + FormControlCheckBox: { objectType: "Checkbox", + autoFill: "True", filled: "f", fillColor: "window [65]", stroked: "f", @@ -617,8 +643,39 @@ var formCtrlPresets = map[FormControlType]struct { firstButton: nil, shadow: nil, }, - FormControlRadio: { + FormControlGroupBox: { + objectType: "GBox", + autoFill: "False", + filled: "f", + fillColor: "", + stroked: "f", + strokeColor: "windowText [64]", + strokeButton: "", + fill: nil, + textHAlign: "", + textVAlign: "", + noThreeD: stringPtr(""), + firstButton: nil, + shadow: nil, + }, + FormControlLabel: { + objectType: "Label", + autoFill: "False", + filled: "f", + fillColor: "window [65]", + stroked: "f", + strokeColor: "windowText [64]", + strokeButton: "", + fill: nil, + textHAlign: "", + textVAlign: "", + noThreeD: nil, + firstButton: nil, + shadow: nil, + }, + FormControlOptionButton: { objectType: "Radio", + autoFill: "False", filled: "f", fillColor: "window [65]", stroked: "f", @@ -631,6 +688,116 @@ var formCtrlPresets = map[FormControlType]struct { firstButton: stringPtr(""), shadow: nil, }, + FormControlScrollBar: { + objectType: "Scroll", + autoFill: "", + filled: "", + fillColor: "", + stroked: "f", + strokeColor: "windowText [64]", + strokeButton: "", + fill: nil, + textHAlign: "", + textVAlign: "", + noThreeD: nil, + firstButton: nil, + shadow: nil, + }, + FormControlSpinButton: { + objectType: "Spin", + autoFill: "False", + filled: "", + fillColor: "", + stroked: "f", + strokeColor: "windowText [64]", + strokeButton: "", + fill: nil, + textHAlign: "", + textVAlign: "", + noThreeD: nil, + firstButton: nil, + shadow: nil, + }, +} + +// addFormCtrl check and add scroll bar or spinner form control by given options. +func (sp *encodeShape) addFormCtrl(opts *vmlOptions) error { + if opts.Type != FormControlScrollBar && opts.Type != FormControlSpinButton { + return nil + } + if opts.CurrentVal > MaxFormControlValue || + opts.MinVal > MaxFormControlValue || + opts.MaxVal > MaxFormControlValue || + opts.IncChange > MaxFormControlValue || + opts.PageChange > MaxFormControlValue { + return ErrorFormControlValue + } + if opts.CellLink != "" { + if _, _, err := CellNameToCoordinates(opts.CellLink); err != nil { + return err + } + } + sp.ClientData.FmlaLink = opts.CellLink + sp.ClientData.Val = opts.CurrentVal + sp.ClientData.Min = opts.MinVal + sp.ClientData.Max = opts.MaxVal + sp.ClientData.Inc = opts.IncChange + sp.ClientData.Page = opts.PageChange + if opts.Type == FormControlScrollBar { + if opts.Horizontally { + sp.ClientData.Horiz = stringPtr("") + } + sp.ClientData.Dx = 15 + } + return nil +} + +// addFormCtrlShape returns a VML shape by given preset and options. +func (f *File) addFormCtrlShape(preset formCtrlPreset, col, row int, anchor string, opts *vmlOptions) (*encodeShape, error) { + sp := encodeShape{ + Fill: preset.fill, + Shadow: preset.shadow, + Path: &vPath{ConnectType: "none"}, + TextBox: &vTextBox{ + Style: "mso-direction-alt:auto", + Div: &xlsxDiv{Style: "text-align:left"}, + }, + ClientData: &xClientData{ + ObjectType: preset.objectType, + Anchor: anchor, + AutoFill: preset.autoFill, + Row: row - 1, + Column: col - 1, + TextHAlign: preset.textHAlign, + TextVAlign: preset.textVAlign, + NoThreeD: preset.noThreeD, + FirstButton: preset.firstButton, + }, + } + if opts.Format.PrintObject != nil && !*opts.Format.PrintObject { + sp.ClientData.PrintObject = "False" + } + if opts.Format.Positioning != "" { + idx := inStrSlice(supportedPositioning, opts.Format.Positioning, true) + if idx == -1 { + return &sp, ErrParameterInvalid + } + sp.ClientData.MoveWithCells = []*string{stringPtr(""), nil, nil}[idx] + sp.ClientData.SizeWithCells = []*string{stringPtr(""), stringPtr(""), nil}[idx] + } + if opts.FormControl.Type == FormControlNote { + sp.ClientData.MoveWithCells = stringPtr("") + sp.ClientData.SizeWithCells = stringPtr("") + } + if !opts.formCtrl { + return &sp, nil + } + sp.TextBox.Div.Font = formCtrlText(opts) + sp.ClientData.FmlaMacro = opts.Macro + if (opts.Type == FormControlCheckBox || opts.Type == FormControlOptionButton) && opts.Checked { + sp.ClientData.Checked = 1 + } + return &sp, sp.addFormCtrl(opts) } // addDrawingVML provides a function to create VML drawing XML as @@ -639,20 +806,18 @@ var formCtrlPresets = map[FormControlType]struct { // LeftOffset, TopRow, TopOffset, RightColumn, RightOffset, BottomRow, // BottomOffset. func (f *File) addDrawingVML(dataID int, drawingVML string, opts *vmlOptions) error { - col, row, err := CellNameToCoordinates(opts.Cell) + col, row, err := CellNameToCoordinates(opts.FormControl.Cell) if err != nil { return err } anchor := fmt.Sprintf("%d, 23, %d, 0, %d, %d, %d, 5", col, row, col+opts.rows+2, col+opts.cols-1, row+opts.rows+2) vmlID, vml, preset := 202, f.VMLDrawing[drawingVML], formCtrlPresets[opts.Type] style := "position:absolute;73.5pt;width:108pt;height:59.25pt;z-index:1;visibility:hidden" - var font []vmlFont - if opts.FormCtrl { + if opts.formCtrl { vmlID = 201 style = "position:absolute;73.5pt;width:108pt;height:59.25pt;z-index:1;mso-wrap-style:tight" - colStart, rowStart, colEnd, rowEnd, x2, y2 := f.positionObjectPixels(opts.Sheet, col, row, opts.Format.OffsetX, opts.Format.OffsetY, int(opts.Width), int(opts.Height)) + colStart, rowStart, colEnd, rowEnd, x2, y2 := f.positionObjectPixels(opts.sheet, col, row, opts.Format.OffsetX, opts.Format.OffsetY, int(opts.Width), int(opts.Height)) anchor = fmt.Sprintf("%d, 0, %d, 0, %d, %d, %d, %d", colStart, rowStart, colEnd, x2, rowEnd, y2) - font = formCtrlText(opts) } if vml == nil { vml = &vmlDrawing{ @@ -699,31 +864,9 @@ func (f *File) addDrawingVML(dataID int, drawingVML string, opts *vmlOptions) er } } } - sp := encodeShape{ - Fill: preset.fill, - Shadow: preset.shadow, - Path: &vPath{ConnectType: "none"}, - TextBox: &vTextBox{ - Style: "mso-direction-alt:auto", - Div: &xlsxDiv{Style: "text-align:left", Font: font}, - }, - ClientData: &xClientData{ - ObjectType: preset.objectType, - Anchor: anchor, - AutoFill: "True", - Row: row - 1, - Column: col - 1, - TextHAlign: preset.textHAlign, - TextVAlign: preset.textVAlign, - NoThreeD: preset.noThreeD, - FirstButton: preset.firstButton, - }, - } - if opts.FormCtrl { - sp.ClientData.FmlaMacro = opts.Macro - } - if (opts.Type == FormControlCheckbox || opts.Type == FormControlRadio) && opts.Checked { - sp.ClientData.Checked = stringPtr("1") + sp, err := f.addFormCtrlShape(preset, col, row, anchor, opts) + if err != nil { + return err } s, _ := xml.Marshal(sp) shape := xlsxShape{ diff --git a/vmlDrawing.go b/vmlDrawing.go index eae224a..d09054f 100644 --- a/vmlDrawing.go +++ b/vmlDrawing.go @@ -48,7 +48,7 @@ type xlsxShape struct { Style string `xml:"style,attr"` Button string `xml:"o:button,attr,omitempty"` Filled string `xml:"filled,attr,omitempty"` - FillColor string `xml:"fillcolor,attr"` + FillColor string `xml:"fillcolor,attr,omitempty"` InsetMode string `xml:"urn:schemas-microsoft-com:office:office insetmode,attr,omitempty"` Stroked string `xml:"stroked,attr,omitempty"` StrokeColor string `xml:"strokecolor,attr,omitempty"` @@ -128,18 +128,28 @@ type vmlFont struct { // element. type xClientData struct { ObjectType string `xml:"ObjectType,attr"` - MoveWithCells string `xml:"x:MoveWithCells"` - SizeWithCells string `xml:"x:SizeWithCells"` + MoveWithCells *string `xml:"x:MoveWithCells"` + SizeWithCells *string `xml:"x:SizeWithCells"` Anchor string `xml:"x:Anchor"` - AutoFill string `xml:"x:AutoFill"` - Row int `xml:"x:Row"` - Column int `xml:"x:Column"` + Locked string `xml:"x:Locked,omitempty"` + PrintObject string `xml:"x:PrintObject,omitempty"` + AutoFill string `xml:"x:AutoFill,omitempty"` FmlaMacro string `xml:"x:FmlaMacro,omitempty"` TextHAlign string `xml:"x:TextHAlign,omitempty"` TextVAlign string `xml:"x:TextVAlign,omitempty"` - Checked *string `xml:"x:Checked,omitempty"` - NoThreeD *string `xml:"x:NoThreeD,omitempty"` - FirstButton *string `xml:"x:FirstButton,omitempty"` + Row int `xml:"x:Row"` + Column int `xml:"x:Column"` + Checked int `xml:"x:Checked,omitempty"` + FmlaLink string `xml:"x:FmlaLink,omitempty"` + NoThreeD *string `xml:"x:NoThreeD"` + FirstButton *string `xml:"x:FirstButton"` + Val uint `xml:"x:Val,omitempty"` + Min uint `xml:"x:Min,omitempty"` + Max uint `xml:"x:Max,omitempty"` + Inc uint `xml:"x:Inc,omitempty"` + Page uint `xml:"x:Page,omitempty"` + Horiz *string `xml:"x:Horiz"` + Dx uint `xml:"x:Dx,omitempty"` } // decodeVmlDrawing defines the structure used to parse the file @@ -165,7 +175,7 @@ type decodeShape struct { Style string `xml:"style,attr"` Button string `xml:"button,attr,omitempty"` Filled string `xml:"filled,attr,omitempty"` - FillColor string `xml:"fillcolor,attr"` + FillColor string `xml:"fillcolor,attr,omitempty"` InsetMode string `xml:"urn:schemas-microsoft-com:office:office insetmode,attr,omitempty"` Stroked string `xml:"stroked,attr,omitempty"` StrokeColor string `xml:"strokecolor,attr,omitempty"` @@ -195,34 +205,49 @@ type encodeShape struct { ClientData *xClientData `xml:"x:ClientData"` } +// formCtrlPreset defines the structure used to form control presets. +type formCtrlPreset struct { + autoFill string + fill *vFill + fillColor string + filled string + firstButton *string + noThreeD *string + objectType string + shadow *vShadow + strokeButton string + strokeColor string + stroked string + textHAlign string + textVAlign string +} + // vmlOptions defines the structure used to internal comments and form controls. type vmlOptions struct { - rows int - cols int - FormCtrl bool - Sheet string - Author string - AuthorID int - Cell string - Checked bool - Text string - Macro string - Width uint - Height uint - Paragraph []RichTextRun - Type FormControlType - Format GraphicOptions + rows int + cols int + formCtrl bool + sheet string + Comment + FormControl } // FormControl directly maps the form controls information. type FormControl struct { - Cell string - Macro string - Width uint - Height uint - Checked bool - Text string - Paragraph []RichTextRun - Type FormControlType - Format GraphicOptions + Cell string + Macro string + Width uint + Height uint + Checked bool + CurrentVal uint + MinVal uint + MaxVal uint + IncChange uint + PageChange uint + Horizontally bool + CellLink string + Text string + Paragraph []RichTextRun + Type FormControlType + Format GraphicOptions } diff --git a/vml_test.go b/vml_test.go index 09262ec..aba262d 100644 --- a/vml_test.go +++ b/vml_test.go @@ -149,26 +149,20 @@ func TestCountComments(t *testing.T) { func TestAddDrawingVML(t *testing.T) { // Test addDrawingVML with illegal cell reference f := NewFile() - assert.EqualError(t, f.addDrawingVML(0, "", &vmlOptions{Cell: "*"}), newCellNameToCoordinatesError("*", newInvalidCellNameError("*")).Error()) + assert.Equal(t, f.addDrawingVML(0, "", &vmlOptions{FormControl: FormControl{Cell: "*"}}), newCellNameToCoordinatesError("*", newInvalidCellNameError("*"))) f.Pkg.Store("xl/drawings/vmlDrawing1.vml", MacintoshCyrillicCharset) - assert.EqualError(t, f.addDrawingVML(0, "xl/drawings/vmlDrawing1.vml", &vmlOptions{Cell: "A1"}), "XML syntax error on line 1: invalid UTF-8") + assert.EqualError(t, f.addDrawingVML(0, "xl/drawings/vmlDrawing1.vml", &vmlOptions{FormControl: FormControl{Cell: "A1"}}), "XML syntax error on line 1: invalid UTF-8") } func TestFormControl(t *testing.T) { f := NewFile() assert.NoError(t, f.AddFormControl("Sheet1", FormControl{ - Cell: "D1", - Type: FormControlButton, - Macro: "Button1_Click", + Cell: "D1", Type: FormControlButton, Macro: "Button1_Click", })) assert.NoError(t, f.AddFormControl("Sheet1", FormControl{ - Cell: "A1", - Type: FormControlButton, - Macro: "Button1_Click", - Width: 140, - Height: 60, - Text: "Button 1\r\n", + Cell: "A1", Type: FormControlButton, Macro: "Button1_Click", + Width: 140, Height: 60, Text: "Button 1\r\n", Paragraph: []RichTextRun{ { Font: &Font{ @@ -182,28 +176,42 @@ func TestFormControl(t *testing.T) { Text: "C1=A1+B1", }, }, + Format: GraphicOptions{PrintObject: boolPtr(true), Positioning: "absolute"}, })) assert.NoError(t, f.AddFormControl("Sheet1", FormControl{ - Cell: "A5", - Type: FormControlCheckbox, - Text: "Check Box 1", - Checked: true, + Cell: "A5", Type: FormControlCheckBox, Text: "Check Box 1", + Checked: true, Format: GraphicOptions{ + PrintObject: boolPtr(false), Positioning: "oneCell", + }, })) assert.NoError(t, f.AddFormControl("Sheet1", FormControl{ - Cell: "A6", - Type: FormControlCheckbox, - Text: "Check Box 2", + Cell: "A6", Type: FormControlCheckBox, Text: "Check Box 2", + Format: GraphicOptions{Positioning: "twoCell"}, })) assert.NoError(t, f.AddFormControl("Sheet1", FormControl{ - Cell: "A7", - Type: FormControlRadio, - Text: "Option Button 1", - Checked: true, + Cell: "A7", Type: FormControlOptionButton, Text: "Option Button 1", Checked: true, })) assert.NoError(t, f.AddFormControl("Sheet1", FormControl{ - Cell: "A8", - Type: FormControlRadio, - Text: "Option Button 2", + Cell: "A8", Type: FormControlOptionButton, Text: "Option Button 2", + })) + assert.NoError(t, f.AddFormControl("Sheet1", FormControl{ + Cell: "D3", Type: FormControlGroupBox, Text: "Group Box 1", + Width: 140, Height: 60, + })) + assert.NoError(t, f.AddFormControl("Sheet1", FormControl{ + Cell: "A9", Type: FormControlLabel, Text: "Label 1", Width: 140, + })) + assert.NoError(t, f.AddFormControl("Sheet1", FormControl{ + Cell: "C5", Type: FormControlSpinButton, Width: 40, Height: 60, + CurrentVal: 7, MinVal: 5, MaxVal: 10, IncChange: 1, CellLink: "C2", + })) + assert.NoError(t, f.AddFormControl("Sheet1", FormControl{ + Cell: "D7", Type: FormControlScrollBar, Width: 140, Height: 20, + CurrentVal: 50, MinVal: 10, MaxVal: 100, IncChange: 1, PageChange: 1, Horizontally: true, CellLink: "C3", + })) + assert.NoError(t, f.AddFormControl("Sheet1", FormControl{ + Cell: "G1", Type: FormControlScrollBar, Width: 20, Height: 140, + CurrentVal: 50, MinVal: 1000, MaxVal: 100, IncChange: 1, PageChange: 1, CellLink: "C4", })) assert.NoError(t, f.SetSheetProps("Sheet1", &SheetPropsOptions{CodeName: stringPtr("Sheet1")})) file, err := os.ReadFile(filepath.Join("test", "vbaProject.bin")) @@ -214,23 +222,29 @@ func TestFormControl(t *testing.T) { f, err = OpenFile(filepath.Join("test", "TestAddFormControl.xlsm")) assert.NoError(t, err) assert.NoError(t, f.AddFormControl("Sheet1", FormControl{ - Cell: "D4", - Type: FormControlButton, - Macro: "Button1_Click", - Text: "Button 2", + Cell: "D4", Type: FormControlButton, Macro: "Button1_Click", Text: "Button 2", })) // Test add unsupported form control assert.Equal(t, f.AddFormControl("Sheet1", FormControl{ - Cell: "A1", - Type: 0x37, - Macro: "Button1_Click", + Cell: "A1", Type: 0x37, Macro: "Button1_Click", }), ErrParameterInvalid) // Test add form control on not exists worksheet assert.Equal(t, f.AddFormControl("SheetN", FormControl{ - Cell: "A1", - Type: FormControlButton, - Macro: "Button1_Click", + Cell: "A1", Type: FormControlButton, Macro: "Button1_Click", }), newNoExistSheetError("SheetN")) + // Test add form control with invalid positioning types + assert.Equal(t, f.AddFormControl("Sheet1", FormControl{ + Cell: "A1", Type: FormControlButton, + Format: GraphicOptions{Positioning: "x"}, + }), ErrParameterInvalid) + // Test add spin form control with illegal cell link reference + assert.Equal(t, f.AddFormControl("Sheet1", FormControl{ + Cell: "C5", Type: FormControlSpinButton, CellLink: "*", + }), newCellNameToCoordinatesError("*", newInvalidCellNameError("*"))) + // Test add spin form control with invalid scroll value + assert.Equal(t, f.AddFormControl("Sheet1", FormControl{ + Cell: "C5", Type: FormControlSpinButton, CurrentVal: MaxFormControlValue + 1, + }), ErrorFormControlValue) assert.NoError(t, f.Close()) // Test delete form control f, err = OpenFile(filepath.Join("test", "TestAddFormControl.xlsm")) diff --git a/xmlDrawing.go b/xmlDrawing.go index affa039..ba38655 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -126,6 +126,7 @@ const ( MaxColumnWidth = 255 MaxFieldLength = 255 MaxFilePathLength = 207 + MaxFormControlValue = 30000 MaxFontFamilyLength = 31 MaxFontSize = 409 MaxRowHeight = 409 @@ -218,6 +219,9 @@ var supportedDrawingUnderlineTypes = []string{ "wavyDbl", } +// supportedPositioning defined supported positioning types. +var supportedPositioning = []string{"absolute", "oneCell", "twoCell"} + // xlsxCNvPr directly maps the cNvPr (Non-Visual Drawing Properties). This // element specifies non-visual canvas properties. This allows for additional // information that does not affect the appearance of the picture to be stored.