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
This commit is contained in:
xuri 2023-07-21 00:03:37 +08:00
parent b667987084
commit 8d996ca138
No known key found for this signature in database
GPG Key ID: BA5E5BB1C948EDF7
10 changed files with 421 additions and 203 deletions

16
calc.go
View File

@ -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()
}

View File

@ -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) {

View File

@ -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)
)

View File

@ -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 {

View File

@ -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)

View File

@ -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() {

323
vml.go
View File

@ -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 + "<br></br>\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{

View File

@ -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
}

View File

@ -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"))

View File

@ -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.