Add StringGrid control type

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Duensing 2026-03-05 15:35:09 -06:00
parent dd115d3727
commit da21cc71f6
4 changed files with 392 additions and 8 deletions

View file

@ -336,6 +336,7 @@ messages are available, dispatching each command as it arrives.
| `Bevel` | TBevel | Cosmetic beveled line/box |
| `Header` | THeader | Column header bar |
| `ScrollBox` | TScrollBox | Scrollable container |
| `StringGrid` | TStringGrid | Editable string grid |
### Creating Controls
@ -728,6 +729,91 @@ Input mask string (mask;save literals;blank char).
- **Format:** `0` (bsLowered) or `1` (bsRaised)
- **Example:** `Style=1`
### ColCount
- **Applies to:** StringGrid
- **Format:** Integer (default 5)
- **Example:** `ColCount=10`
Number of columns in the grid.
### RowCount
- **Applies to:** StringGrid
- **Format:** Integer (default 5)
- **Example:** `RowCount=20`
Number of rows in the grid.
### FixedCols
- **Applies to:** StringGrid
- **Format:** Integer (default 1)
- **Example:** `FixedCols=1`
Number of non-scrollable columns on the left.
### FixedRows
- **Applies to:** StringGrid
- **Format:** Integer (default 1)
- **Example:** `FixedRows=1`
Number of non-scrollable rows at the top.
### DefaultColWidth
- **Applies to:** StringGrid
- **Format:** Integer (pixels)
- **Example:** `DefaultColWidth=80`
### DefaultRowHeight
- **Applies to:** StringGrid
- **Format:** Integer (pixels)
- **Example:** `DefaultRowHeight=20`
### Options (StringGrid)
- **Applies to:** StringGrid
- **Format:** Integer (bitmask)
- **Example:** `Options=1549`
Bitmask of TGridOption values:
| Bit | Value | Option | Description |
|-----|--------|---------------------|---------------------------|
| 0 | 0x0001 | goFixedVertLine | Vertical lines on fixed |
| 1 | 0x0002 | goFixedHorzLine | Horizontal lines on fixed |
| 2 | 0x0004 | goVertLine | Vertical lines on cells |
| 3 | 0x0008 | goHorzLine | Horizontal lines on cells |
| 4 | 0x0010 | goRangeSelect | Allow range selection |
| 5 | 0x0020 | goDrawFocusSelected | Draw focused cell selected|
| 6 | 0x0040 | goRowSizing | Allow row resizing |
| 7 | 0x0080 | goColSizing | Allow column resizing |
| 8 | 0x0100 | goRowMoving | Allow row moving |
| 9 | 0x0200 | goColMoving | Allow column moving |
| 10 | 0x0400 | goEditing | Allow in-place editing |
| 11 | 0x0800 | goTabs | Tab between cells |
| 12 | 0x1000 | goThumbTracking | Track scrollbar thumb |
### Cells
- **Applies to:** StringGrid
- **Format:** Quoted string (tab-delimited columns, `\n`-delimited rows)
- **Example:** `Cells="Name\tAge\nAlice\t30\nBob\t25"`
Bulk-loads all cell data. Columns are separated by tab characters,
rows by newlines. Row 0 is the first row (typically fixed header).
### Cell
- **Applies to:** StringGrid
- **Format:** Quoted string (`col,row,value`)
- **Example:** `Cell="1,2,Hello"`
Sets a single cell value. Column and row are zero-based indices.
### BasePath
`BasePath` is a property on `TFormClient` (not a protocol property).
@ -766,6 +852,7 @@ No `EVENT.BIND` command is needed.
| TabSet | Change | `<tabIndex>` |
| TabbedNotebook | Change | `<pageIndex>` |
| MaskEdit | Change | `"new text"` |
| StringGrid | SelectCell | `<col> <row>` |
### Opt-in Events
@ -784,6 +871,7 @@ client will send them. Use `EVENT.UNBIND` to disconnect.
| MouseDown | `<x> <y> <button>` | 0=left, 1=right, 2=middle |
| MouseUp | `<x> <y> <button>` | 0=left, 1=right, 2=middle |
| MouseMove | `<x> <y> 0` | Coordinates relative to control |
| SetEditText | `<col> <row> "text"` | StringGrid in-place edit |
### Form Close Event

View file

@ -46,7 +46,8 @@ typedef enum {
ctOutline,
ctBevel,
ctHeader,
ctScrollBox
ctScrollBox,
ctStringGrid
} CtrlTypeE;
typedef struct {
@ -140,6 +141,21 @@ typedef struct {
bool hasShape;
bool hasStyle;
bool hasEditMask;
int32_t colCount;
int32_t rowCount;
int32_t fixedCols;
int32_t fixedRows;
int32_t defaultColWidth;
int32_t defaultRowHeight;
int32_t options;
bool hasColCount;
bool hasRowCount;
bool hasFixedCols;
bool hasFixedRows;
bool hasDefaultColWidth;
bool hasDefaultRowHeight;
bool hasOptions;
bool hasOnSetEditText;
} DfmCtrlT;
typedef struct {
@ -439,6 +455,9 @@ static CtrlTypeE mapClassName(const char *className) {
if (stricmp_local(className, "TScrollBox") == 0) {
return ctScrollBox;
}
if (stricmp_local(className, "TStringGrid") == 0) {
return ctStringGrid;
}
return ctUnknown;
}
@ -1013,6 +1032,59 @@ static void parseProperties(const uint8_t *data, int32_t size, int32_t *pos, Dfm
ctrl->style = readIntValue(data, size, pos, tag);
}
ctrl->hasStyle = true;
} else if (!isForm && stricmp_local(propName, "ColCount") == 0) {
ctrl->colCount = readIntValue(data, size, pos, tag);
ctrl->hasColCount = true;
} else if (!isForm && stricmp_local(propName, "RowCount") == 0) {
ctrl->rowCount = readIntValue(data, size, pos, tag);
ctrl->hasRowCount = true;
} else if (!isForm && stricmp_local(propName, "FixedCols") == 0) {
ctrl->fixedCols = readIntValue(data, size, pos, tag);
ctrl->hasFixedCols = true;
} else if (!isForm && stricmp_local(propName, "FixedRows") == 0) {
ctrl->fixedRows = readIntValue(data, size, pos, tag);
ctrl->hasFixedRows = true;
} else if (!isForm && stricmp_local(propName, "DefaultColWidth") == 0) {
ctrl->defaultColWidth = readIntValue(data, size, pos, tag);
ctrl->hasDefaultColWidth = true;
} else if (!isForm && stricmp_local(propName, "DefaultRowHeight") == 0) {
ctrl->defaultRowHeight = readIntValue(data, size, pos, tag);
ctrl->hasDefaultRowHeight = true;
} else if (!isForm && stricmp_local(propName, "Options") == 0) {
if (tag == vaSet) {
ctrl->options = 0;
while (*pos < size) {
readStr(data, size, pos, strBuf, sizeof(strBuf));
if (strBuf[0] == '\0') {
break;
}
if (stricmp_local(strBuf, "goFixedVertLine") == 0) { ctrl->options |= 0x0001; }
else if (stricmp_local(strBuf, "goFixedHorzLine") == 0) { ctrl->options |= 0x0002; }
else if (stricmp_local(strBuf, "goVertLine") == 0) { ctrl->options |= 0x0004; }
else if (stricmp_local(strBuf, "goHorzLine") == 0) { ctrl->options |= 0x0008; }
else if (stricmp_local(strBuf, "goRangeSelect") == 0) { ctrl->options |= 0x0010; }
else if (stricmp_local(strBuf, "goDrawFocusSelected") == 0) { ctrl->options |= 0x0020; }
else if (stricmp_local(strBuf, "goRowSizing") == 0) { ctrl->options |= 0x0040; }
else if (stricmp_local(strBuf, "goColSizing") == 0) { ctrl->options |= 0x0080; }
else if (stricmp_local(strBuf, "goRowMoving") == 0) { ctrl->options |= 0x0100; }
else if (stricmp_local(strBuf, "goColMoving") == 0) { ctrl->options |= 0x0200; }
else if (stricmp_local(strBuf, "goEditing") == 0) { ctrl->options |= 0x0400; }
else if (stricmp_local(strBuf, "goTabs") == 0) { ctrl->options |= 0x0800; }
else if (stricmp_local(strBuf, "goThumbTracking") == 0) { ctrl->options |= 0x1000; }
}
} else {
ctrl->options = readIntValue(data, size, pos, tag);
}
ctrl->hasOptions = true;
} else if (stricmp_local(propName, "OnSetEditText") == 0) {
if (tag == vaIdent) {
readStr(data, size, pos, strBuf, sizeof(strBuf));
} else {
skipValue(data, size, pos, tag);
}
if (!isForm) {
ctrl->hasOnSetEditText = true;
}
} else {
skipValue(data, size, pos, tag);
}
@ -1160,7 +1232,7 @@ static void emitCtrl(FILE *out, int32_t formId, int32_t ctrlId, DfmCtrlT *ctrl)
"MainMenu", "PopupMenu", "MenuItem", "RadioGroup",
"BitBtn", "SpeedButton", "TabSet", "Notebook",
"TabbedNotebook", "MaskEdit", "Outline", "Bevel",
"Header", "ScrollBox"
"Header", "ScrollBox", "StringGrid"
};
char escaped[8192];
@ -1291,6 +1363,27 @@ static void emitCtrl(FILE *out, int32_t formId, int32_t ctrlId, DfmCtrlT *ctrl)
if (ctrl->hasStyle && ctrl->style != 0) {
fprintf(out, " Style=%d", ctrl->style);
}
if (ctrl->hasColCount) {
fprintf(out, " ColCount=%d", ctrl->colCount);
}
if (ctrl->hasRowCount) {
fprintf(out, " RowCount=%d", ctrl->rowCount);
}
if (ctrl->hasFixedCols) {
fprintf(out, " FixedCols=%d", ctrl->fixedCols);
}
if (ctrl->hasFixedRows) {
fprintf(out, " FixedRows=%d", ctrl->fixedRows);
}
if (ctrl->hasDefaultColWidth) {
fprintf(out, " DefaultColWidth=%d", ctrl->defaultColWidth);
}
if (ctrl->hasDefaultRowHeight) {
fprintf(out, " DefaultRowHeight=%d", ctrl->defaultRowHeight);
}
if (ctrl->hasOptions) {
fprintf(out, " Options=%d", ctrl->options);
}
fprintf(out, "\n");
@ -1329,6 +1422,9 @@ static void emitCtrl(FILE *out, int32_t formId, int32_t ctrlId, DfmCtrlT *ctrl)
if (ctrl->hasOnMouseUp) {
fprintf(out, "EVENT.BIND %d %d MouseUp\n", formId, ctrlId);
}
if (ctrl->hasOnSetEditText) {
fprintf(out, "EVENT.BIND %d %d SetEditText\n", formId, ctrlId);
}
// Suppress unused variable warning for autoSelect
(void)autoSelect;

View file

@ -14,7 +14,7 @@ interface
uses
SysUtils, Classes, Controls, Forms, StdCtrls, ExtCtrls, Menus, Buttons, Tabs,
TabNotBk, Mask, Outline, MPlayer, WinTypes, WinProcs;
TabNotBk, Mask, Outline, Grids, MPlayer, WinTypes, WinProcs;
const
MaxMsgLen = 4096;
@ -34,12 +34,12 @@ type
ctMainMenu, ctPopupMenu, ctMenuItem, ctRadioGroup,
ctBitBtn, ctSpeedButton, ctTabSet, ctNotebook,
ctTabbedNotebook, ctMaskEdit, ctOutline, ctBevel,
ctHeader, ctScrollBox);
ctHeader, ctScrollBox, ctStringGrid);
{ Bound event flags }
TBoundEvent = (beDblClick, beKeyDown, beKeyUp,
beEnter, beExit, beMouseDown, beMouseUp, beMouseMove,
beClick, beNotify);
beClick, beNotify, beSetEditText);
TBoundEvents = set of TBoundEvent;
{ Per-control record }
@ -120,6 +120,8 @@ type
procedure HandleRadioGroupClick(Sender: TObject);
procedure HandleTabSetChange(Sender: TObject);
procedure HandleTabbedNotebookChange(Sender: TObject);
procedure HandleStringGridSelectCell(Sender: TObject; ACol, ARow: Longint; var CanSelect: Boolean);
procedure HandleStringGridSetEditText(Sender: TObject; ACol, ARow: Longint; const Value: string);
{ Outgoing event helpers }
procedure SendEvent(FormId, CtrlId: Integer; const EventName: string; const Data: string);
@ -777,6 +779,36 @@ begin
end;
procedure TFormClient.HandleStringGridSelectCell(Sender: TObject; ACol, ARow: Longint; var CanSelect: Boolean);
var
Tag: Longint;
FormId: Integer;
CtrlId: Integer;
begin
Tag := (Sender as TComponent).Tag;
FormId := Tag shr 16;
CtrlId := Tag and $FFFF;
SendEvent(FormId, CtrlId, 'SelectCell', IntToStr(ACol) + ' ' + IntToStr(ARow));
CanSelect := True;
end;
procedure TFormClient.HandleStringGridSetEditText(Sender: TObject; ACol, ARow: Longint; const Value: string);
var
Tag: Longint;
FormId: Integer;
CtrlId: Integer;
Escaped: array[0..4095] of Char;
begin
Tag := (Sender as TComponent).Tag;
FormId := Tag shr 16;
CtrlId := Tag and $FFFF;
StrPCopy(FTmpBuf, Value);
EscapeString(FTmpBuf, Escaped, SizeOf(Escaped));
SendEvent(FormId, CtrlId, 'SetEditText', IntToStr(ACol) + ' ' + IntToStr(ARow) + ' "' + StrPas(Escaped) + '"');
end;
procedure TFormClient.HandleFormClose(Sender: TObject;
var Action: TCloseAction);
var
@ -828,6 +860,8 @@ begin
(CR^.Control as TTabbedNotebook).OnChange := HandleTabbedNotebookChange;
ctMaskEdit:
(CR^.Control as TMaskEdit).OnChange := HandleMaskEditChange;
ctStringGrid:
(CR^.Control as TStringGrid).OnSelectCell := HandleStringGridSelectCell;
end;
end;
@ -897,6 +931,12 @@ begin
if CR^.Control is TMediaPlayer then
(CR^.Control as TMediaPlayer).OnNotify := HandleMediaPlayerNotify;
CR^.Bound := CR^.Bound + [beNotify];
end
else if EventName = 'SetEditText' then
begin
if CR^.Control is TStringGrid then
(CR^.Control as TStringGrid).OnSetEditText := HandleStringGridSetEditText;
CR^.Bound := CR^.Bound + [beSetEditText];
end;
end;
@ -966,6 +1006,12 @@ begin
if CR^.Control is TMediaPlayer then
(CR^.Control as TMediaPlayer).OnNotify := nil;
CR^.Bound := CR^.Bound - [beNotify];
end
else if EventName = 'SetEditText' then
begin
if CR^.Control is TStringGrid then
(CR^.Control as TStringGrid).OnSetEditText := nil;
CR^.Bound := CR^.Bound - [beSetEditText];
end;
end;
@ -981,6 +1027,10 @@ var
P: PChar;
Start: PChar;
PCR: PFormCtrlRec;
Col: Integer;
Row: Integer;
W: Word;
Opts: TGridOptions;
begin
S := StrPas(Key);
@ -1538,6 +1588,119 @@ begin
N := StrToIntDef(StrPas(Value), 0);
if CR^.CtrlType = ctBevel then
(CR^.Control as TBevel).Style := TBevelStyle(N);
end
else if S = 'ColCount' then
begin
N := StrToIntDef(StrPas(Value), 5);
if CR^.CtrlType = ctStringGrid then
(CR^.Control as TStringGrid).ColCount := N;
end
else if S = 'RowCount' then
begin
N := StrToIntDef(StrPas(Value), 5);
if CR^.CtrlType = ctStringGrid then
(CR^.Control as TStringGrid).RowCount := N;
end
else if S = 'FixedCols' then
begin
N := StrToIntDef(StrPas(Value), 1);
if CR^.CtrlType = ctStringGrid then
(CR^.Control as TStringGrid).FixedCols := N;
end
else if S = 'FixedRows' then
begin
N := StrToIntDef(StrPas(Value), 1);
if CR^.CtrlType = ctStringGrid then
(CR^.Control as TStringGrid).FixedRows := N;
end
else if S = 'DefaultColWidth' then
begin
N := StrToIntDef(StrPas(Value), 64);
if CR^.CtrlType = ctStringGrid then
(CR^.Control as TStringGrid).DefaultColWidth := N;
end
else if S = 'DefaultRowHeight' then
begin
N := StrToIntDef(StrPas(Value), 24);
if CR^.CtrlType = ctStringGrid then
(CR^.Control as TStringGrid).DefaultRowHeight := N;
end
else if S = 'Options' then
begin
if CR^.CtrlType = ctStringGrid then
begin
N := StrToIntDef(StrPas(Value), 0);
W := Word(N);
Move(W, Opts, SizeOf(Opts));
(CR^.Control as TStringGrid).Options := Opts;
end;
end
else if S = 'Cells' then
begin
if CR^.CtrlType = ctStringGrid then
begin
UnescapeString(Value, Unesc, SizeOf(Unesc));
Row := 0;
P := Unesc;
while P^ <> #0 do
begin
{ Parse one row: tab-delimited columns }
Col := 0;
Start := P;
while (P^ <> #0) and (P^ <> #10) do
begin
if P^ = #9 then
begin
P^ := #0;
if (Col < (CR^.Control as TStringGrid).ColCount) and (Row < (CR^.Control as TStringGrid).RowCount) then
(CR^.Control as TStringGrid).Cells[Col, Row] := StrPas(Start);
Inc(Col);
Inc(P);
Start := P;
end
else
Inc(P);
end;
{ Last column in row }
if (Col < (CR^.Control as TStringGrid).ColCount) and (Row < (CR^.Control as TStringGrid).RowCount) then
(CR^.Control as TStringGrid).Cells[Col, Row] := StrPas(Start);
if P^ = #10 then
Inc(P);
Inc(Row);
end;
end;
end
else if S = 'Cell' then
begin
if CR^.CtrlType = ctStringGrid then
begin
{ Format: "col,row,value" }
UnescapeString(Value, Unesc, SizeOf(Unesc));
P := Unesc;
{ Parse col }
Start := P;
while (P^ <> #0) and (P^ <> ',') do
Inc(P);
if P^ = ',' then
begin
P^ := #0;
Col := StrToIntDef(StrPas(Start), 0);
Inc(P);
{ Parse row }
Start := P;
while (P^ <> #0) and (P^ <> ',') do
Inc(P);
if P^ = ',' then
begin
P^ := #0;
Row := StrToIntDef(StrPas(Start), 0);
Inc(P);
{ Rest is value }
if (Col < (CR^.Control as TStringGrid).ColCount) and (Row < (CR^.Control as TStringGrid).RowCount) then
(CR^.Control as TStringGrid).Cells[Col, Row] := StrPas(P);
end;
end;
end;
end;
end;
@ -1656,6 +1819,8 @@ begin
Result := ctHeader
else if S = 'ScrollBox' then
Result := ctScrollBox
else if S = 'StringGrid' then
Result := ctStringGrid
else
Result := ctUnknown;
end;
@ -1833,6 +1998,8 @@ begin
Comp := THeader.Create(FR^.Form);
ctScrollBox:
Comp := TScrollBox.Create(FR^.Form);
ctStringGrid:
Comp := TStringGrid.Create(FR^.Form);
end;
if Comp = nil then

View file

@ -83,6 +83,8 @@ Event data varies by event type:
| Exit | (none) |
| Close | (none) |
| Notify | (none) |
| SelectCell| `<col> <row>` |
| SetEditText| `<col> <row> "text"` |
## Control Types
@ -115,6 +117,7 @@ Event data varies by event type:
| Bevel | TBevel | (none) |
| Header | THeader | (none) |
| ScrollBox | TScrollBox | (none) |
| StringGrid | TStringGrid | SelectCell |
MainMenu and PopupMenu use `0 0 0 0` geometry (non-visual). MenuItem
uses `0 0 0 0` geometry and requires a `Parent` property to specify its
@ -125,9 +128,9 @@ GroupBox and Panel are cosmetic containers (flat parent model — no child
containment in the protocol). RadioButtons are all in one group per form.
Opt-in events (require EVENT.BIND): Click (Image, GroupBox, Panel),
Notify (MediaPlayer), DblClick, KeyDown, KeyUp, Enter, Exit,
MouseDown, MouseUp, MouseMove. Menu and RadioGroup components do
not support opt-in events.
Notify (MediaPlayer), SetEditText (StringGrid), DblClick, KeyDown,
KeyUp, Enter, Exit, MouseDown, MouseUp, MouseMove. Menu and
RadioGroup components do not support opt-in events.
## Properties
@ -174,6 +177,15 @@ not support opt-in events.
| OutlineStyle| Outline | 0-6 (osText..osTreePictureText) |
| Shape | Bevel | 0-5 (bsBox..bsRightLine) |
| Style | Bevel | 0-1 (bsLowered, bsRaised) |
| ColCount | StringGrid | Integer (default 5) |
| RowCount | StringGrid | Integer (default 5) |
| FixedCols | StringGrid | Integer (default 1) |
| FixedRows | StringGrid | Integer (default 1) |
| DefaultColWidth | StringGrid | Integer (pixels) |
| DefaultRowHeight| StringGrid | Integer (pixels) |
| Options | StringGrid | Integer (bitmask, see below) |
| Cells | StringGrid | Quoted string (tab-delimited cols, `\n`-delimited rows) |
| Cell | StringGrid | Quoted string (`col,row,value`) |
File path properties (Picture, FileName) are resolved relative to the
client's `BasePath` setting. Subdirectories are allowed (e.g.,
@ -185,6 +197,27 @@ MediaPlayer rather than setting a value. Valid commands: `Open`,
`Play`, `Stop`, `Close`, `Pause`, `Resume`, `Rewind`, `Next`,
`Previous`.
StringGrid Options is an integer bitmask of TGridOption values:
| Bit | Value | Option |
|--------|--------|---------------------|
| 0 | 0x0001 | goFixedVertLine |
| 1 | 0x0002 | goFixedHorzLine |
| 2 | 0x0004 | goVertLine |
| 3 | 0x0008 | goHorzLine |
| 4 | 0x0010 | goRangeSelect |
| 5 | 0x0020 | goDrawFocusSelected |
| 6 | 0x0040 | goRowSizing |
| 7 | 0x0080 | goColSizing |
| 8 | 0x0100 | goRowMoving |
| 9 | 0x0200 | goColMoving |
| 10 | 0x0400 | goEditing |
| 11 | 0x0800 | goTabs |
| 12 | 0x1000 | goThumbTracking |
Cells is a bulk-load property: columns are tab-delimited, rows are
`\n`-delimited. Cell is an individual update: `"col,row,value"`.
## String Encoding
- Strings in the protocol are always double-quoted.