Add MainMenu, PopupMenu, MenuItem, and RadioGroup control types

Menus use 0 0 0 0 geometry (non-visual) with Parent property for
hierarchy. TFormCtrlRec.Control widened from TControl to TComponent
to support non-visual menu components. RadioGroup auto-wires Click
with ItemIndex data.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Duensing 2026-03-05 14:40:31 -06:00
parent 2d5ed2a3b1
commit 03d44440fd
4 changed files with 362 additions and 108 deletions

View file

@ -322,6 +322,10 @@ messages are available, dispatching each command as it arrives.
| `Panel` | TPanel | Cosmetic container panel |
| `ScrollBar` | TScrollBar | Horizontal or vertical scrollbar|
| `MediaPlayer` | TMediaPlayer | MCI media player control |
| `MainMenu` | TMainMenu | Form main menu bar |
| `PopupMenu` | TPopupMenu | Context (right-click) menu |
| `MenuItem` | TMenuItem | Menu item (child of menu) |
| `RadioGroup` | TRadioGroup | Grouped radio buttons |
### Creating Controls
@ -359,12 +363,12 @@ CTRL.SET 1 3 Text="world" Enabled=0
### Caption
- **Applies to:** Label, Button, CheckBox, GroupBox, RadioButton, Panel
- **Applies to:** Label, Button, CheckBox, GroupBox, RadioButton, Panel, MenuItem, RadioGroup
- **Format:** Quoted string
- **Example:** `Caption="Submit"`
The display text for labels, buttons, check boxes, group boxes, radio
buttons, and panels.
buttons, panels, menu items, and radio groups.
### Text
@ -381,7 +385,7 @@ CTRL.SET 1 5 Text="Line one\nLine two\nLine three"
### Items
- **Applies to:** ListBox, ComboBox
- **Applies to:** ListBox, ComboBox, RadioGroup
- **Format:** Quoted string, items separated by `\n`
- **Example:** `Items="Red\nGreen\nBlue"`
@ -390,7 +394,7 @@ new items are added.
### Checked
- **Applies to:** CheckBox, RadioButton
- **Applies to:** CheckBox, RadioButton, MenuItem
- **Format:** `0` (unchecked) or `1` (checked)
- **Example:** `Checked=1`
@ -433,7 +437,7 @@ Maximum number of characters the user can type.
### ItemIndex
- **Applies to:** ListBox, ComboBox
- **Applies to:** ListBox, ComboBox, RadioGroup
- **Format:** Integer (-1 = no selection)
- **Example:** `ItemIndex=2`
@ -588,6 +592,41 @@ Pseudo-property that triggers a method call instead of setting a
value. Valid commands: `Open`, `Play`, `Stop`, `Close`, `Pause`,
`Resume`, `Rewind`, `Next`, `Previous`.
### Parent
- **Applies to:** MenuItem
- **Format:** Integer (ctrlId of parent menu or menu item)
- **Example:** `Parent=1`
Specifies the parent for a menu item. The parent can be a MainMenu,
PopupMenu, or another MenuItem (for submenus).
### Columns
- **Applies to:** RadioGroup
- **Format:** Integer
- **Example:** `Columns=2`
Number of columns for the radio button layout.
### ShortCut
- **Applies to:** MenuItem
- **Format:** Integer (Delphi ShortCut value)
- **Example:** `ShortCut=16467`
Keyboard accelerator for the menu item. Uses Delphi's ShortCut
encoding (virtual key + modifier flags).
### PopupMenu
- **Applies to:** Any visual control
- **Format:** Integer (ctrlId of a PopupMenu)
- **Example:** `PopupMenu=2`
Associates a PopupMenu with a control. When the user right-clicks
the control, the popup menu is displayed.
### BasePath
`BasePath` is a property on `TFormClient` (not a protocol property).
@ -619,6 +658,8 @@ No `EVENT.BIND` command is needed.
| ListBox | Select | `<index> "selected text"` |
| ComboBox | Select | `<index> "selected text"` |
| ComboBox | Change | `"new text"` |
| MenuItem | Click | (none) |
| RadioGroup | Click | `<itemIndex>` |
### Opt-in Events
@ -666,6 +707,25 @@ EVENT 1 3 KeyDown 13 (client sends back when Enter pressed)
EVENT.UNBIND 1 3 KeyDown (server disconnects)
```
### Menu Hierarchy Example
Menus are built using flat CTRL.CREATE commands with `Parent` properties
to establish the hierarchy:
```
CTRL.CREATE 1 1 MainMenu 0 0 0 0
CTRL.CREATE 1 2 MenuItem 0 0 0 0 Caption="&File" Parent=1
CTRL.CREATE 1 3 MenuItem 0 0 0 0 Caption="&Open" Parent=2 ShortCut=16463
CTRL.CREATE 1 4 MenuItem 0 0 0 0 Caption="&Save" Parent=2 ShortCut=16467
CTRL.CREATE 1 5 MenuItem 0 0 0 0 Caption="E&xit" Parent=2
CTRL.CREATE 1 6 MenuItem 0 0 0 0 Caption="&Help" Parent=1
CTRL.CREATE 1 7 MenuItem 0 0 0 0 Caption="&About" Parent=6
```
This creates a menu bar with File (Open, Save, Exit) and Help (About).
MenuItem ctrl 2 is a child of MainMenu ctrl 1; ctrl 3-5 are children of
the File item (ctrl 2); ctrl 7 is a child of the Help item (ctrl 6).
## String Encoding
All strings in the protocol are double-quoted. The following escape
@ -697,9 +757,8 @@ any protocol or application code.
## Limitations
- **RadioButton grouping:** All RadioButtons on a form belong to one
group. There is no support for multiple independent radio groups
per form (the protocol has a flat parent model with no child
containment).
group. Use RadioGroup for multiple independent radio groups per
form.
- **Image format:** Only BMP files are supported for the Picture
property.
- **dfm2form and Picture:** The converter cannot extract image

View file

@ -32,7 +32,11 @@ typedef enum {
ctRadioButton,
ctPanel,
ctScrollBar,
ctMediaPlayer
ctMediaPlayer,
ctMainMenu,
ctPopupMenu,
ctMenuItem,
ctRadioGroup
} CtrlTypeE;
typedef struct {
@ -103,6 +107,11 @@ typedef struct {
bool hasAutoOpen;
bool hasFileName;
bool hasDeviceType;
int32_t parentCtrlIdx;
int32_t columns;
int32_t shortCut;
bool hasColumns;
bool hasShortCut;
} DfmCtrlT;
typedef struct {
@ -143,7 +152,7 @@ static void escapeStr(const char *src, char *dst, int32_t dstSize);
static void initCtrl(DfmCtrlT *ctrl);
static void initForm(DfmFormT *form);
static CtrlTypeE mapClassName(const char *className);
static bool parseComponent(const uint8_t *data, int32_t size, int32_t *pos, DfmFormT *form, bool isRoot);
static bool parseComponent(const uint8_t *data, int32_t size, int32_t *pos, DfmFormT *form, bool isRoot, int32_t parentIdx);
static void parseProperties(const uint8_t *data, int32_t size, int32_t *pos, DfmFormT *form, DfmCtrlT *ctrl, bool isForm);
static int32_t readByte(const uint8_t *data, int32_t size, int32_t *pos);
static int32_t readInt16LE(const uint8_t *data, int32_t size, int32_t *pos);
@ -360,6 +369,18 @@ static CtrlTypeE mapClassName(const char *className) {
if (stricmp_local(className, "TMediaPlayer") == 0) {
return ctMediaPlayer;
}
if (stricmp_local(className, "TMainMenu") == 0) {
return ctMainMenu;
}
if (stricmp_local(className, "TPopupMenu") == 0) {
return ctPopupMenu;
}
if (stricmp_local(className, "TMenuItem") == 0) {
return ctMenuItem;
}
if (stricmp_local(className, "TRadioGroup") == 0) {
return ctRadioGroup;
}
return ctUnknown;
}
@ -370,13 +391,14 @@ static CtrlTypeE mapClassName(const char *className) {
static void initCtrl(DfmCtrlT *ctrl) {
memset(ctrl, 0, sizeof(DfmCtrlT));
ctrl->enabled = 1;
ctrl->visible = 1;
ctrl->itemIndex = -1;
ctrl->tabOrder = -1;
ctrl->bevelOuter = 1; // bvRaised
ctrl->largeChange = 1;
ctrl->smallChange = 1;
ctrl->enabled = 1;
ctrl->visible = 1;
ctrl->itemIndex = -1;
ctrl->tabOrder = -1;
ctrl->bevelOuter = 1; // bvRaised
ctrl->largeChange = 1;
ctrl->smallChange = 1;
ctrl->parentCtrlIdx = -1;
}
@ -790,6 +812,12 @@ static void parseProperties(const uint8_t *data, int32_t size, int32_t *pos, Dfm
ctrl->autoOpen = readIntValue(data, size, pos, tag);
}
ctrl->hasAutoOpen = true;
} else if (!isForm && stricmp_local(propName, "Columns") == 0) {
ctrl->columns = readIntValue(data, size, pos, tag);
ctrl->hasColumns = true;
} else if (!isForm && stricmp_local(propName, "ShortCut") == 0) {
ctrl->shortCut = readIntValue(data, size, pos, tag);
ctrl->hasShortCut = true;
} else {
skipValue(data, size, pos, tag);
}
@ -801,7 +829,7 @@ static void parseProperties(const uint8_t *data, int32_t size, int32_t *pos, Dfm
// Parse a component (form or child control) recursively
// ---------------------------------------------------------------------------
static bool parseComponent(const uint8_t *data, int32_t size, int32_t *pos, DfmFormT *form, bool isRoot) {
static bool parseComponent(const uint8_t *data, int32_t size, int32_t *pos, DfmFormT *form, bool isRoot, int32_t parentIdx) {
// Check for flags byte (Delphi 1.0 rarely uses this but handle it)
uint8_t peek = data[*pos];
if ((peek & 0xF0) == 0xF0) {
@ -833,7 +861,7 @@ static bool parseComponent(const uint8_t *data, int32_t size, int32_t *pos, DfmF
(*pos)++; // consume terminator
break;
}
parseComponent(data, size, pos, form, false);
parseComponent(data, size, pos, form, false, -1);
}
} else {
// Child control
@ -851,7 +879,7 @@ static bool parseComponent(const uint8_t *data, int32_t size, int32_t *pos, DfmF
(*pos)++;
break;
}
parseComponent(data, size, pos, form, false);
parseComponent(data, size, pos, form, false, -1);
}
return true;
}
@ -861,22 +889,24 @@ static bool parseComponent(const uint8_t *data, int32_t size, int32_t *pos, DfmF
exit(1);
}
DfmCtrlT *ctrl = &form->ctrls[form->ctrlCount];
int32_t myIdx = form->ctrlCount;
DfmCtrlT *ctrl = &form->ctrls[myIdx];
initCtrl(ctrl);
ctrl->type = type;
ctrl->type = type;
ctrl->parentCtrlIdx = parentIdx;
snprintf(ctrl->name, sizeof(ctrl->name), "%s", instName);
parseProperties(data, size, pos, form, ctrl, false);
form->ctrlCount++;
// Skip nested children (controls within controls, e.g., panels)
// Recurse into children (menu items, panels, etc.)
while (*pos < size) {
peek = data[*pos];
if (peek == 0x00) {
(*pos)++;
break;
}
parseComponent(data, size, pos, form, false);
parseComponent(data, size, pos, form, false, myIdx);
}
}
@ -931,13 +961,16 @@ static void emitCtrl(FILE *out, int32_t formId, int32_t ctrlId, DfmCtrlT *ctrl)
"Unknown", "Label", "Edit", "Button",
"CheckBox", "ListBox", "ComboBox", "Memo",
"Image", "GroupBox", "RadioButton", "Panel",
"ScrollBar", "MediaPlayer"
"ScrollBar", "MediaPlayer",
"MainMenu", "PopupMenu", "MenuItem", "RadioGroup"
};
char escaped[8192];
fprintf(out, "CTRL.CREATE %d %d %s %d %d %d %d",
formId, ctrlId, typeNames[ctrl->type],
ctrl->left, ctrl->top, ctrl->width, ctrl->height);
if (ctrl->type == ctMainMenu || ctrl->type == ctPopupMenu || ctrl->type == ctMenuItem) {
fprintf(out, "CTRL.CREATE %d %d %s 0 0 0 0", formId, ctrlId, typeNames[ctrl->type]);
} else {
fprintf(out, "CTRL.CREATE %d %d %s %d %d %d %d", formId, ctrlId, typeNames[ctrl->type], ctrl->left, ctrl->top, ctrl->width, ctrl->height);
}
// Inline properties
if (ctrl->hasCaption) {
@ -1023,6 +1056,15 @@ static void emitCtrl(FILE *out, int32_t formId, int32_t ctrlId, DfmCtrlT *ctrl)
if (ctrl->hasAutoOpen && ctrl->autoOpen) {
fprintf(out, " AutoOpen=1");
}
if (ctrl->type == ctMenuItem && ctrl->parentCtrlIdx >= 0) {
fprintf(out, " Parent=%d", ctrl->parentCtrlIdx + 1);
}
if (ctrl->hasColumns && ctrl->columns != 0) {
fprintf(out, " Columns=%d", ctrl->columns);
}
if (ctrl->hasShortCut && ctrl->shortCut != 0) {
fprintf(out, " ShortCut=%d", ctrl->shortCut);
}
fprintf(out, "\n");
@ -1030,7 +1072,7 @@ static void emitCtrl(FILE *out, int32_t formId, int32_t ctrlId, DfmCtrlT *ctrl)
// Auto-wired: Button/CheckBox→Click, Edit→Change, ListBox→Select,
// ComboBox→Select+Change, Memo→Change
bool autoClick = (ctrl->type == ctButton || ctrl->type == ctCheckBox || ctrl->type == ctRadioButton);
bool autoClick = (ctrl->type == ctButton || ctrl->type == ctCheckBox || ctrl->type == ctRadioButton || ctrl->type == ctMenuItem || ctrl->type == ctRadioGroup);
bool autoChange = (ctrl->type == ctEdit || ctrl->type == ctComboBox || ctrl->type == ctMemo || ctrl->type == ctScrollBar);
bool autoSelect = (ctrl->type == ctListBox || ctrl->type == ctComboBox);
@ -1160,7 +1202,7 @@ int main(int argc, char *argv[]) {
DfmFormT form;
initForm(&form);
int32_t pos = 4; // skip TPF0
parseComponent(data, (int32_t)fileSize, &pos, &form, true);
parseComponent(data, (int32_t)fileSize, &pos, &form, true, -1);
// Default caption if empty
if (form.caption[0] == '\0') {

View file

@ -13,7 +13,7 @@ unit FormCli;
interface
uses
SysUtils, Classes, Controls, Forms, StdCtrls, ExtCtrls, MPlayer, WinTypes, WinProcs;
SysUtils, Classes, Controls, Forms, StdCtrls, ExtCtrls, Menus, MPlayer, WinTypes, WinProcs;
const
MaxMsgLen = 4096;
@ -29,7 +29,8 @@ type
TCtrlTypeE = (ctUnknown, ctLabel, ctEdit, ctButton,
ctCheckBox, ctListBox, ctComboBox, ctMemo,
ctImage, ctGroupBox, ctRadioButton, ctPanel,
ctScrollBar, ctMediaPlayer);
ctScrollBar, ctMediaPlayer,
ctMainMenu, ctPopupMenu, ctMenuItem, ctRadioGroup);
{ Bound event flags }
TBoundEvent = (beDblClick, beKeyDown, beKeyUp,
@ -42,7 +43,7 @@ type
TFormCtrlRec = record
CtrlId: Integer;
CtrlType: TCtrlTypeE;
Control: TControl;
Control: TComponent;
Bound: TBoundEvents;
end;
@ -81,8 +82,8 @@ type
procedure FreeCtrlRec(CR: PFormCtrlRec);
{ Property application }
procedure ApplyProp(CR: PFormCtrlRec; Key, Value: PChar);
procedure ApplyInlineProps(CR: PFormCtrlRec; P: PChar);
procedure ApplyProp(FR: PFormRec; CR: PFormCtrlRec; Key, Value: PChar);
procedure ApplyInlineProps(FR: PFormRec; CR: PFormCtrlRec; P: PChar);
{ Event wiring }
procedure WireAutoEvents(CR: PFormCtrlRec);
@ -109,7 +110,9 @@ type
procedure HandleClick(Sender: TObject);
procedure HandleRadioButtonClick(Sender: TObject);
procedure HandleScrollBarChange(Sender: TObject);
procedure HandleMenuItemClick(Sender: TObject);
procedure HandleMediaPlayerNotify(Sender: TObject);
procedure HandleRadioGroupClick(Sender: TObject);
{ Outgoing event helpers }
procedure SendEvent(FormId, CtrlId: Integer; const EventName: string; const Data: string);
@ -333,8 +336,12 @@ end;
procedure TFormClient.FreeCtrlRec(CR: PFormCtrlRec);
begin
{ Menu items are owned by their parent menu and freed automatically }
if CR^.Control <> nil then
CR^.Control.Free;
begin
if not (CR^.Control is TMenuItem) then
CR^.Control.Free;
end;
Dispose(CR);
end;
@ -403,7 +410,7 @@ var
FormId: Integer;
CtrlId: Integer;
begin
Tag := (Sender as TControl).Tag;
Tag := (Sender as TComponent).Tag;
FormId := Tag shr 16;
CtrlId := Tag and $FFFF;
SendEvent(FormId, CtrlId, 'Click', '');
@ -416,7 +423,7 @@ var
FormId: Integer;
CtrlId: Integer;
begin
Tag := (Sender as TControl).Tag;
Tag := (Sender as TComponent).Tag;
FormId := Tag shr 16;
CtrlId := Tag and $FFFF;
SendEvent(FormId, CtrlId, 'Click', '');
@ -431,7 +438,7 @@ var
Escaped: array[0..4095] of Char;
Txt: string;
begin
Tag := (Sender as TControl).Tag;
Tag := (Sender as TComponent).Tag;
FormId := Tag shr 16;
CtrlId := Tag and $FFFF;
Txt := (Sender as TEdit).Text;
@ -449,7 +456,7 @@ var
Escaped: array[0..4095] of Char;
Txt: string;
begin
Tag := (Sender as TControl).Tag;
Tag := (Sender as TComponent).Tag;
FormId := Tag shr 16;
CtrlId := Tag and $FFFF;
Txt := (Sender as TMemo).Text;
@ -467,7 +474,7 @@ var
Escaped: array[0..4095] of Char;
Txt: string;
begin
Tag := (Sender as TControl).Tag;
Tag := (Sender as TComponent).Tag;
FormId := Tag shr 16;
CtrlId := Tag and $FFFF;
Txt := (Sender as TComboBox).Text;
@ -486,7 +493,7 @@ var
Escaped: array[0..4095] of Char;
Txt: string;
begin
Tag := (Sender as TControl).Tag;
Tag := (Sender as TComponent).Tag;
FormId := Tag shr 16;
CtrlId := Tag and $FFFF;
Idx := (Sender as TListBox).ItemIndex;
@ -510,7 +517,7 @@ var
Escaped: array[0..4095] of Char;
Txt: string;
begin
Tag := (Sender as TControl).Tag;
Tag := (Sender as TComponent).Tag;
FormId := Tag shr 16;
CtrlId := Tag and $FFFF;
Idx := (Sender as TComboBox).ItemIndex;
@ -531,7 +538,7 @@ var
FormId: Integer;
CtrlId: Integer;
begin
Tag := (Sender as TControl).Tag;
Tag := (Sender as TComponent).Tag;
FormId := Tag shr 16;
CtrlId := Tag and $FFFF;
SendEvent(FormId, CtrlId, 'DblClick', '');
@ -544,7 +551,7 @@ var
FormId: Integer;
CtrlId: Integer;
begin
Tag := (Sender as TControl).Tag;
Tag := (Sender as TComponent).Tag;
FormId := Tag shr 16;
CtrlId := Tag and $FFFF;
SendEvent(FormId, CtrlId, 'Enter', '');
@ -557,7 +564,7 @@ var
FormId: Integer;
CtrlId: Integer;
begin
Tag := (Sender as TControl).Tag;
Tag := (Sender as TComponent).Tag;
FormId := Tag shr 16;
CtrlId := Tag and $FFFF;
SendEvent(FormId, CtrlId, 'Exit', '');
@ -571,7 +578,7 @@ var
FormId: Integer;
CtrlId: Integer;
begin
Tag := (Sender as TControl).Tag;
Tag := (Sender as TComponent).Tag;
FormId := Tag shr 16;
CtrlId := Tag and $FFFF;
SendEvent(FormId, CtrlId, 'KeyDown', IntToStr(Key));
@ -585,7 +592,7 @@ var
FormId: Integer;
CtrlId: Integer;
begin
Tag := (Sender as TControl).Tag;
Tag := (Sender as TComponent).Tag;
FormId := Tag shr 16;
CtrlId := Tag and $FFFF;
SendEvent(FormId, CtrlId, 'KeyUp', IntToStr(Key));
@ -600,7 +607,7 @@ var
CtrlId: Integer;
Btn: Integer;
begin
Tag := (Sender as TControl).Tag;
Tag := (Sender as TComponent).Tag;
FormId := Tag shr 16;
CtrlId := Tag and $FFFF;
Btn := Ord(Button);
@ -617,7 +624,7 @@ var
CtrlId: Integer;
Btn: Integer;
begin
Tag := (Sender as TControl).Tag;
Tag := (Sender as TComponent).Tag;
FormId := Tag shr 16;
CtrlId := Tag and $FFFF;
Btn := Ord(Button);
@ -633,7 +640,7 @@ var
FormId: Integer;
CtrlId: Integer;
begin
Tag := (Sender as TControl).Tag;
Tag := (Sender as TComponent).Tag;
FormId := Tag shr 16;
CtrlId := Tag and $FFFF;
SendEvent(FormId, CtrlId, 'MouseMove',
@ -647,7 +654,7 @@ var
FormId: Integer;
CtrlId: Integer;
begin
Tag := (Sender as TControl).Tag;
Tag := (Sender as TComponent).Tag;
FormId := Tag shr 16;
CtrlId := Tag and $FFFF;
SendEvent(FormId, CtrlId, 'Click', '');
@ -660,7 +667,7 @@ var
FormId: Integer;
CtrlId: Integer;
begin
Tag := (Sender as TControl).Tag;
Tag := (Sender as TComponent).Tag;
FormId := Tag shr 16;
CtrlId := Tag and $FFFF;
SendEvent(FormId, CtrlId, 'Click', '');
@ -673,7 +680,7 @@ var
FormId: Integer;
CtrlId: Integer;
begin
Tag := (Sender as TControl).Tag;
Tag := (Sender as TComponent).Tag;
FormId := Tag shr 16;
CtrlId := Tag and $FFFF;
SendEvent(FormId, CtrlId, 'Change', IntToStr((Sender as TScrollBar).Position));
@ -686,13 +693,39 @@ var
FormId: Integer;
CtrlId: Integer;
begin
Tag := (Sender as TControl).Tag;
Tag := (Sender as TComponent).Tag;
FormId := Tag shr 16;
CtrlId := Tag and $FFFF;
SendEvent(FormId, CtrlId, 'Notify', '');
end;
procedure TFormClient.HandleMenuItemClick(Sender: TObject);
var
Tag: Longint;
FormId: Integer;
CtrlId: Integer;
begin
Tag := (Sender as TComponent).Tag;
FormId := Tag shr 16;
CtrlId := Tag and $FFFF;
SendEvent(FormId, CtrlId, 'Click', '');
end;
procedure TFormClient.HandleRadioGroupClick(Sender: TObject);
var
Tag: Longint;
FormId: Integer;
CtrlId: Integer;
begin
Tag := (Sender as TComponent).Tag;
FormId := Tag shr 16;
CtrlId := Tag and $FFFF;
SendEvent(FormId, CtrlId, 'Click', IntToStr((Sender as TRadioGroup).ItemIndex));
end;
procedure TFormClient.HandleFormClose(Sender: TObject;
var Action: TCloseAction);
var
@ -730,6 +763,10 @@ begin
(CR^.Control as TRadioButton).OnClick := HandleRadioButtonClick;
ctScrollBar:
(CR^.Control as TScrollBar).OnChange := HandleScrollBarChange;
ctMenuItem:
(CR^.Control as TMenuItem).OnClick := HandleMenuItemClick;
ctRadioGroup:
(CR^.Control as TRadioGroup).OnClick := HandleRadioGroupClick;
end;
end;
@ -738,7 +775,8 @@ procedure TFormClient.WireOptEvent(CR: PFormCtrlRec; const EventName: string);
begin
if EventName = 'DblClick' then
begin
(CR^.Control as TControl).OnDblClick := HandleDblClick;
if CR^.Control is TControl then
(CR^.Control as TControl).OnDblClick := HandleDblClick;
CR^.Bound := CR^.Bound + [beDblClick];
end
else if EventName = 'Enter' then
@ -767,17 +805,20 @@ begin
end
else if EventName = 'MouseDown' then
begin
(CR^.Control as TControl).OnMouseDown := HandleMouseDown;
if CR^.Control is TControl then
(CR^.Control as TControl).OnMouseDown := HandleMouseDown;
CR^.Bound := CR^.Bound + [beMouseDown];
end
else if EventName = 'MouseUp' then
begin
(CR^.Control as TControl).OnMouseUp := HandleMouseUp;
if CR^.Control is TControl then
(CR^.Control as TControl).OnMouseUp := HandleMouseUp;
CR^.Bound := CR^.Bound + [beMouseUp];
end
else if EventName = 'MouseMove' then
begin
(CR^.Control as TControl).OnMouseMove := HandleMouseMove;
if CR^.Control is TControl then
(CR^.Control as TControl).OnMouseMove := HandleMouseMove;
CR^.Bound := CR^.Bound + [beMouseMove];
end
else if EventName = 'Click' then
@ -803,7 +844,8 @@ procedure TFormClient.UnwireOptEvent(CR: PFormCtrlRec; const EventName: string);
begin
if EventName = 'DblClick' then
begin
(CR^.Control as TControl).OnDblClick := nil;
if CR^.Control is TControl then
(CR^.Control as TControl).OnDblClick := nil;
CR^.Bound := CR^.Bound - [beDblClick];
end
else if EventName = 'Enter' then
@ -832,17 +874,20 @@ begin
end
else if EventName = 'MouseDown' then
begin
(CR^.Control as TControl).OnMouseDown := nil;
if CR^.Control is TControl then
(CR^.Control as TControl).OnMouseDown := nil;
CR^.Bound := CR^.Bound - [beMouseDown];
end
else if EventName = 'MouseUp' then
begin
(CR^.Control as TControl).OnMouseUp := nil;
if CR^.Control is TControl then
(CR^.Control as TControl).OnMouseUp := nil;
CR^.Bound := CR^.Bound - [beMouseUp];
end
else if EventName = 'MouseMove' then
begin
(CR^.Control as TControl).OnMouseMove := nil;
if CR^.Control is TControl then
(CR^.Control as TControl).OnMouseMove := nil;
CR^.Bound := CR^.Bound - [beMouseMove];
end
else if EventName = 'Click' then
@ -866,7 +911,7 @@ end;
{ ----- Property application ----------------------------------------------- }
procedure TFormClient.ApplyProp(CR: PFormCtrlRec; Key, Value: PChar);
procedure TFormClient.ApplyProp(FR: PFormRec; CR: PFormCtrlRec; Key, Value: PChar);
var
S: string;
Unesc: array[0..4095] of Char;
@ -874,6 +919,7 @@ var
Lines: TStringList;
P: PChar;
Start: PChar;
PCR: PFormCtrlRec;
begin
S := StrPas(Key);
@ -887,6 +933,8 @@ begin
ctGroupBox: (CR^.Control as TGroupBox).Caption := StrPas(Unesc);
ctRadioButton: (CR^.Control as TRadioButton).Caption := StrPas(Unesc);
ctPanel: (CR^.Control as TPanel).Caption := StrPas(Unesc);
ctMenuItem: (CR^.Control as TMenuItem).Caption := StrPas(Unesc);
ctRadioGroup: (CR^.Control as TRadioGroup).Caption := StrPas(Unesc);
end;
end
else if S = 'Text' then
@ -975,6 +1023,26 @@ begin
if Start^ <> #0 then
(CR^.Control as TComboBox).Items.Add(StrPas(Start));
end;
ctRadioGroup:
begin
(CR^.Control as TRadioGroup).Items.Clear;
P := Unesc;
Start := P;
while P^ <> #0 do
begin
if P^ = #10 then
begin
P^ := #0;
(CR^.Control as TRadioGroup).Items.Add(StrPas(Start));
Inc(P);
Start := P;
end
else
Inc(P);
end;
if Start^ <> #0 then
(CR^.Control as TRadioGroup).Items.Add(StrPas(Start));
end;
end;
end
else if S = 'Checked' then
@ -983,17 +1051,25 @@ begin
if CR^.CtrlType = ctCheckBox then
(CR^.Control as TCheckBox).Checked := (N <> 0)
else if CR^.CtrlType = ctRadioButton then
(CR^.Control as TRadioButton).Checked := (N <> 0);
(CR^.Control as TRadioButton).Checked := (N <> 0)
else if CR^.CtrlType = ctMenuItem then
(CR^.Control as TMenuItem).Checked := (N <> 0);
end
else if S = 'Enabled' then
begin
N := StrToIntDef(StrPas(Value), 1);
CR^.Control.Enabled := (N <> 0);
if CR^.Control is TControl then
(CR^.Control as TControl).Enabled := (N <> 0)
else if CR^.Control is TMenuItem then
(CR^.Control as TMenuItem).Enabled := (N <> 0);
end
else if S = 'Visible' then
begin
N := StrToIntDef(StrPas(Value), 1);
CR^.Control.Visible := (N <> 0);
if CR^.Control is TControl then
(CR^.Control as TControl).Visible := (N <> 0)
else if CR^.Control is TMenuItem then
(CR^.Control as TMenuItem).Visible := (N <> 0);
end
else if S = 'MaxLength' then
begin
@ -1025,8 +1101,9 @@ begin
begin
N := StrToIntDef(StrPas(Value), -1);
case CR^.CtrlType of
ctListBox: (CR^.Control as TListBox).ItemIndex := N;
ctComboBox: (CR^.Control as TComboBox).ItemIndex := N;
ctListBox: (CR^.Control as TListBox).ItemIndex := N;
ctComboBox: (CR^.Control as TComboBox).ItemIndex := N;
ctRadioGroup: (CR^.Control as TRadioGroup).ItemIndex := N;
end;
end
else if S = 'Stretch' then
@ -1196,11 +1273,45 @@ begin
except
end;
end;
end
else if S = 'Parent' then
begin
if CR^.CtrlType = ctMenuItem then
begin
N := StrToIntDef(StrPas(Value), 0);
PCR := FindCtrl(FR, N);
if PCR <> nil then
begin
if PCR^.Control is TMenu then
(PCR^.Control as TMenu).Items.Add(CR^.Control as TMenuItem)
else if PCR^.Control is TMenuItem then
(PCR^.Control as TMenuItem).Add(CR^.Control as TMenuItem);
end;
end;
end
else if S = 'Columns' then
begin
N := StrToIntDef(StrPas(Value), 1);
if CR^.CtrlType = ctRadioGroup then
(CR^.Control as TRadioGroup).Columns := N;
end
else if S = 'ShortCut' then
begin
N := StrToIntDef(StrPas(Value), 0);
if CR^.CtrlType = ctMenuItem then
(CR^.Control as TMenuItem).ShortCut := N;
end
else if S = 'PopupMenu' then
begin
N := StrToIntDef(StrPas(Value), 0);
PCR := FindCtrl(FR, N);
if (PCR <> nil) and (PCR^.Control is TPopupMenu) and (CR^.Control is TControl) then
(CR^.Control as TControl).PopupMenu := PCR^.Control as TPopupMenu;
end;
end;
procedure TFormClient.ApplyInlineProps(CR: PFormCtrlRec; P: PChar);
procedure TFormClient.ApplyInlineProps(FR: PFormRec; CR: PFormCtrlRec; P: PChar);
var
Token: array[0..4095] of Char;
Key: array[0..63] of Char;
@ -1248,7 +1359,7 @@ begin
StrCopy(Value, Eq);
end;
ApplyProp(CR, Key, Value);
ApplyProp(FR, CR, Key, Value);
end;
end;
@ -1286,6 +1397,14 @@ begin
Result := ctScrollBar
else if S = 'MediaPlayer' then
Result := ctMediaPlayer
else if S = 'MainMenu' then
Result := ctMainMenu
else if S = 'PopupMenu' then
Result := ctPopupMenu
else if S = 'MenuItem' then
Result := ctMenuItem
else if S = 'RadioGroup' then
Result := ctRadioGroup
else
Result := ctUnknown;
end;
@ -1378,7 +1497,7 @@ var
FR: PFormRec;
CR: PFormCtrlRec;
CType: TCtrlTypeE;
Ctrl: TControl;
Comp: TComponent;
begin
FormId := ParseInt(P);
CtrlId := ParseInt(P);
@ -1398,62 +1517,82 @@ begin
Exit;
{ Create the control }
Ctrl := nil;
Comp := nil;
case CType of
ctLabel:
begin
Ctrl := TLabel.Create(FR^.Form);
(Ctrl as TLabel).AutoSize := False;
Comp := TLabel.Create(FR^.Form);
(Comp as TLabel).AutoSize := False;
end;
ctEdit:
Ctrl := TEdit.Create(FR^.Form);
Comp := TEdit.Create(FR^.Form);
ctButton:
Ctrl := TButton.Create(FR^.Form);
Comp := TButton.Create(FR^.Form);
ctCheckBox:
Ctrl := TCheckBox.Create(FR^.Form);
Comp := TCheckBox.Create(FR^.Form);
ctListBox:
Ctrl := TListBox.Create(FR^.Form);
Comp := TListBox.Create(FR^.Form);
ctComboBox:
Ctrl := TComboBox.Create(FR^.Form);
Comp := TComboBox.Create(FR^.Form);
ctMemo:
Ctrl := TMemo.Create(FR^.Form);
Comp := TMemo.Create(FR^.Form);
ctImage:
begin
Ctrl := TImage.Create(FR^.Form);
(Ctrl as TImage).AutoSize := False;
Comp := TImage.Create(FR^.Form);
(Comp as TImage).AutoSize := False;
end;
ctGroupBox:
Ctrl := TGroupBox.Create(FR^.Form);
Comp := TGroupBox.Create(FR^.Form);
ctRadioButton:
Ctrl := TRadioButton.Create(FR^.Form);
Comp := TRadioButton.Create(FR^.Form);
ctPanel:
Ctrl := TPanel.Create(FR^.Form);
Comp := TPanel.Create(FR^.Form);
ctScrollBar:
Ctrl := TScrollBar.Create(FR^.Form);
Comp := TScrollBar.Create(FR^.Form);
ctMediaPlayer:
Ctrl := TMediaPlayer.Create(FR^.Form);
Comp := TMediaPlayer.Create(FR^.Form);
ctMainMenu:
begin
Comp := TMainMenu.Create(FR^.Form);
FR^.Form.Menu := Comp as TMainMenu;
end;
ctPopupMenu:
Comp := TPopupMenu.Create(FR^.Form);
ctMenuItem:
Comp := TMenuItem.Create(FR^.Form);
ctRadioGroup:
Comp := TRadioGroup.Create(FR^.Form);
end;
if Ctrl = nil then
if Comp = nil then
Exit;
{ Set parent and geometry }
if Ctrl is TWinControl then
(Ctrl as TWinControl).Parent := FR^.Form
else
Ctrl.Parent := FR^.Form;
{ Set parent and geometry for visual controls }
if Comp is TWinControl then
begin
(Comp as TWinControl).Parent := FR^.Form;
(Comp as TControl).Left := Left;
(Comp as TControl).Top := Top;
(Comp as TControl).Width := Width;
(Comp as TControl).Height := Height;
end
else if Comp is TControl then
begin
(Comp as TControl).Parent := FR^.Form;
(Comp as TControl).Left := Left;
(Comp as TControl).Top := Top;
(Comp as TControl).Width := Width;
(Comp as TControl).Height := Height;
end;
{ else: non-visual (menus, menu items) — no parent or geometry }
Ctrl.Left := Left;
Ctrl.Top := Top;
Ctrl.Width := Width;
Ctrl.Height := Height;
Ctrl.Tag := (Longint(FormId) shl 16) or CtrlId;
Comp.Tag := (Longint(FormId) shl 16) or CtrlId;
{ Create control record }
New(CR);
CR^.CtrlId := CtrlId;
CR^.CtrlType := CType;
CR^.Control := Ctrl;
CR^.Control := Comp;
CR^.Bound := [];
FR^.Ctrls.Add(CR);
@ -1462,7 +1601,7 @@ begin
WireAutoEvents(CR);
{ Apply inline properties }
ApplyInlineProps(CR, P);
ApplyInlineProps(FR, CR, P);
end;
@ -1484,7 +1623,7 @@ begin
if CR = nil then
Exit;
ApplyInlineProps(CR, P);
ApplyInlineProps(FR, CR, P);
end;

View file

@ -70,7 +70,7 @@ Event data varies by event type:
| Event | Data |
|-----------|-------------------------------|
| Click | (none) |
| Click | (none), or `<itemIndex>` (RadioGroup) |
| DblClick | (none) |
| Change | `"new text"` or `<position>` (ScrollBar) |
| Select | `<index> "selected text"` |
@ -101,28 +101,38 @@ Event data varies by event type:
| Panel | TPanel | (none) |
| ScrollBar | TScrollBar | Change |
| MediaPlayer | TMediaPlayer | (none) |
| MainMenu | TMainMenu | (none) |
| PopupMenu | TPopupMenu | (none) |
| MenuItem | TMenuItem | Click |
| RadioGroup | TRadioGroup | Click |
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
parent menu or menu item. One MainMenu per form (auto-attached).
PopupMenu is associated with any control via the `PopupMenu` property.
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.
MouseDown, MouseUp, MouseMove. Menu and RadioGroup components do
not support opt-in events.
## Properties
| Property | Applies To | Value Format |
|-------------|---------------------------------------------|-------------------------------------------|
| Caption | Label, Button, CheckBox, GroupBox, RadioButton, Panel | Quoted string |
| Caption | Label, Button, CheckBox, GroupBox, RadioButton, Panel, MenuItem, RadioGroup | Quoted string |
| Text | Edit, ComboBox, Memo | Quoted string (`\n` for line breaks) |
| Items | ListBox, ComboBox | Quoted string (`\n`-delimited) |
| Checked | CheckBox, RadioButton | 0 or 1 |
| Items | ListBox, ComboBox, RadioGroup | Quoted string (`\n`-delimited) |
| Checked | CheckBox, RadioButton, MenuItem | 0 or 1 |
| Enabled | All | 0 or 1 |
| Visible | All | 0 or 1 |
| MaxLength | Edit | Integer |
| ReadOnly | Edit, Memo | 0 or 1 |
| ScrollBars | Memo | 0-3 (ssNone..ssBoth) |
| ItemIndex | ListBox, ComboBox | Integer (-1 = none) |
| ItemIndex | ListBox, ComboBox, RadioGroup | Integer (-1 = none) |
| TabOrder | All windowed controls | Integer |
| Stretch | Image | 0 or 1 |
| Center | Image | 0 or 1 |
@ -141,6 +151,10 @@ MouseDown, MouseUp, MouseMove.
| DeviceType | MediaPlayer | Quoted string (e.g., `dtWaveAudio`) |
| AutoOpen | MediaPlayer | 0 or 1 |
| Command | MediaPlayer | Quoted string (pseudo-property, see below)|
| Parent | MenuItem | Integer (ctrlId of parent menu/item) |
| Columns | RadioGroup | Integer (number of columns) |
| ShortCut | MenuItem | Integer (Delphi ShortCut value) |
| PopupMenu | Any TControl | Integer (ctrlId of PopupMenu) |
File path properties (Picture, FileName) are resolved relative to the
client's `BasePath` setting. Subdirectories are allowed (e.g.,