Add Image, GroupBox, RadioButton, Panel, ScrollBar, and MediaPlayer control types

Extends the remote forms system from 7 to 13 control types across
dfm2form converter, formcli client engine, and documentation.
Image and MediaPlayer support file paths resolved via BasePath.
MediaPlayer adds a Command pseudo-property for method calls.
RadioButton auto-wires Click; ScrollBar auto-wires Change.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Duensing 2026-03-04 20:02:03 -06:00
parent e45d72588a
commit 2d5ed2a3b1
4 changed files with 825 additions and 62 deletions

View file

@ -308,7 +308,7 @@ messages are available, dispatching each command as it arrives.
## Supported Controls ## Supported Controls
| Type | Delphi Class | Description | | Type | Delphi Class | Description |
|------------|-------------|--------------------------------| |---------------|--------------|--------------------------------|
| `Label` | TLabel | Static text label | | `Label` | TLabel | Static text label |
| `Edit` | TEdit | Single-line text input | | `Edit` | TEdit | Single-line text input |
| `Button` | TButton | Push button | | `Button` | TButton | Push button |
@ -316,6 +316,12 @@ messages are available, dispatching each command as it arrives.
| `ListBox` | TListBox | Scrollable list of items | | `ListBox` | TListBox | Scrollable list of items |
| `ComboBox` | TComboBox | Drop-down list with text input | | `ComboBox` | TComboBox | Drop-down list with text input |
| `Memo` | TMemo | Multi-line text input | | `Memo` | TMemo | Multi-line text input |
| `Image` | TImage | Bitmap image display (BMP only)|
| `GroupBox` | TGroupBox | Cosmetic grouping frame |
| `RadioButton` | TRadioButton | Radio button (one group/form) |
| `Panel` | TPanel | Cosmetic container panel |
| `ScrollBar` | TScrollBar | Horizontal or vertical scrollbar|
| `MediaPlayer` | TMediaPlayer | MCI media player control |
### Creating Controls ### Creating Controls
@ -353,11 +359,12 @@ CTRL.SET 1 3 Text="world" Enabled=0
### Caption ### Caption
- **Applies to:** Label, Button, CheckBox - **Applies to:** Label, Button, CheckBox, GroupBox, RadioButton, Panel
- **Format:** Quoted string - **Format:** Quoted string
- **Example:** `Caption="Submit"` - **Example:** `Caption="Submit"`
The display text for labels, buttons, and check boxes. The display text for labels, buttons, check boxes, group boxes, radio
buttons, and panels.
### Text ### Text
@ -383,7 +390,7 @@ new items are added.
### Checked ### Checked
- **Applies to:** CheckBox - **Applies to:** CheckBox, RadioButton
- **Format:** `0` (unchecked) or `1` (checked) - **Format:** `0` (unchecked) or `1` (checked)
- **Example:** `Checked=1` - **Example:** `Checked=1`
@ -432,12 +439,168 @@ Maximum number of characters the user can type.
### TabOrder ### TabOrder
- **Applies to:** All windowed controls (all types except Label) - **Applies to:** All windowed controls (all types except Label and Image)
- **Format:** Integer - **Format:** Integer
- **Example:** `TabOrder=3` - **Example:** `TabOrder=3`
Controls the keyboard tab navigation order within the form. Controls the keyboard tab navigation order within the form.
### Stretch
- **Applies to:** Image
- **Format:** `0` (off) or `1` (on)
- **Example:** `Stretch=1`
When enabled, the image is stretched to fill the control bounds.
### Center
- **Applies to:** Image
- **Format:** `0` (off) or `1` (on)
- **Example:** `Center=1`
When enabled, the image is centered within the control bounds.
### Transparent
- **Applies to:** Image
- **Format:** `0` (off) or `1` (on)
- **Example:** `Transparent=1`
When enabled, the image background is transparent.
### Picture
- **Applies to:** Image
- **Format:** Quoted string (filename)
- **Example:** `Picture="images\logo.bmp"`
BMP file to display. The path is resolved relative to the client's
`BasePath` setting. Subdirectories are allowed.
**Note:** `dfm2form` does not emit Picture from DFM files because
image data is stored as a binary blob in the DFM. Set Picture at
runtime via `CTRL.SET` or by manually editing the `.form` file.
### BevelOuter
- **Applies to:** Panel
- **Format:** Integer 0-2
- **Values:**
- `0` — bvNone
- `1` — bvLowered
- `2` — bvRaised
- **Example:** `BevelOuter=2`
### BevelInner
- **Applies to:** Panel
- **Format:** Integer 0-2
- **Values:**
- `0` — bvNone
- `1` — bvLowered
- `2` — bvRaised
- **Example:** `BevelInner=1`
### BorderStyle (Panel)
- **Applies to:** Panel
- **Format:** `0` (bsNone) or `1` (bsSingle)
- **Example:** `BorderStyle=1`
### Kind
- **Applies to:** ScrollBar
- **Format:** `0` (sbHorizontal) or `1` (sbVertical)
- **Example:** `Kind=1`
### Min
- **Applies to:** ScrollBar
- **Format:** Integer
- **Example:** `Min=0`
### Max
- **Applies to:** ScrollBar
- **Format:** Integer
- **Example:** `Max=100`
### Position
- **Applies to:** ScrollBar
- **Format:** Integer
- **Example:** `Position=50`
### LargeChange
- **Applies to:** ScrollBar
- **Format:** Integer
- **Example:** `LargeChange=10`
The amount the position changes when the user clicks the scroll bar
track.
### SmallChange
- **Applies to:** ScrollBar
- **Format:** Integer
- **Example:** `SmallChange=1`
The amount the position changes when the user clicks an arrow button.
### FileName
- **Applies to:** MediaPlayer
- **Format:** Quoted string (file path)
- **Example:** `FileName="sounds\intro.wav"`
Media file to open. The path is resolved relative to the client's
`BasePath` setting.
### DeviceType
- **Applies to:** MediaPlayer
- **Format:** Quoted string
- **Example:** `DeviceType="dtWaveAudio"`
MCI device type. Valid values: `dtAutoSelect`, `dtAVIVideo`,
`dtCDAudio`, `dtDAT`, `dtDigitalVideo`, `dtMMMovie`, `dtOther`,
`dtOverlay`, `dtScanner`, `dtSequencer`, `dtVCR`, `dtVideodisc`,
`dtWaveAudio`.
### AutoOpen
- **Applies to:** MediaPlayer
- **Format:** `0` (off) or `1` (on)
- **Example:** `AutoOpen=1`
When enabled, the media file is opened automatically when FileName
is set.
### Command
- **Applies to:** MediaPlayer
- **Format:** Quoted string
- **Example:** `Command="Play"`
Pseudo-property that triggers a method call instead of setting a
value. Valid commands: `Open`, `Play`, `Stop`, `Close`, `Pause`,
`Resume`, `Rewind`, `Next`, `Previous`.
### BasePath
`BasePath` is a property on `TFormClient` (not a protocol property).
Set it before loading forms to specify where file-based properties
(Picture, FileName) resolve relative paths. Example:
```pascal
Client.BasePath := 'C:\MYAPP';
```
A Picture value of `"images\logo.bmp"` then resolves to
`C:\MYAPP\images\logo.bmp`.
## Events ## Events
### Auto-wired Events ### Auto-wired Events
@ -449,8 +612,10 @@ No `EVENT.BIND` command is needed.
|-------------|---------|-------------------------------| |-------------|---------|-------------------------------|
| Button | Click | (none) | | Button | Click | (none) |
| CheckBox | Click | (none) | | CheckBox | Click | (none) |
| RadioButton | Click | (none) |
| Edit | Change | `"new text"` | | Edit | Change | `"new text"` |
| Memo | Change | `"new text"` | | Memo | Change | `"new text"` |
| ScrollBar | Change | `<position>` |
| ListBox | Select | `<index> "selected text"` | | ListBox | Select | `<index> "selected text"` |
| ComboBox | Select | `<index> "selected text"` | | ComboBox | Select | `<index> "selected text"` |
| ComboBox | Change | `"new text"` | | ComboBox | Change | `"new text"` |
@ -461,8 +626,10 @@ These events require an explicit `EVENT.BIND` command before the
client will send them. Use `EVENT.UNBIND` to disconnect. client will send them. Use `EVENT.UNBIND` to disconnect.
| Event | Data Sent | Notes | | Event | Data Sent | Notes |
|-----------|-----------------------|---------------------------------| |-----------|-----------------------|-----------------------------------|
| Click | (none) | Image, GroupBox, Panel only |
| DblClick | (none) | Double-click on any control | | DblClick | (none) | Double-click on any control |
| Notify | (none) | MediaPlayer playback notification |
| KeyDown | `<vkCode>` | Windows virtual key code | | KeyDown | `<vkCode>` | Windows virtual key code |
| KeyUp | `<vkCode>` | Windows virtual key code | | KeyUp | `<vkCode>` | Windows virtual key code |
| Enter | (none) | Control received focus | | Enter | (none) | Control received focus |
@ -527,6 +694,22 @@ any protocol or application code.
- Transport handles framing — the protocol layer never sees - Transport handles framing — the protocol layer never sees
delimiters. delimiters.
## 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).
- **Image format:** Only BMP files are supported for the Picture
property.
- **dfm2form and Picture:** The converter cannot extract image
filenames from DFM files (image data is stored as a binary blob).
Set Picture at runtime via `CTRL.SET` or by manually editing the
`.form` file.
- **GroupBox and Panel:** These are cosmetic-only containers. The
protocol has no child containment — all controls are direct
children of the form.
## Limits ## Limits
| Limit | Value | | Limit | Value |

View file

@ -26,7 +26,13 @@ typedef enum {
ctCheckBox, ctCheckBox,
ctListBox, ctListBox,
ctComboBox, ctComboBox,
ctMemo ctMemo,
ctImage,
ctGroupBox,
ctRadioButton,
ctPanel,
ctScrollBar,
ctMediaPlayer
} CtrlTypeE; } CtrlTypeE;
typedef struct { typedef struct {
@ -67,6 +73,36 @@ typedef struct {
bool hasOnKeyUp; bool hasOnKeyUp;
bool hasOnMouseDown; bool hasOnMouseDown;
bool hasOnMouseUp; bool hasOnMouseUp;
int32_t stretch;
int32_t center;
int32_t transparent;
int32_t bevelOuter;
int32_t bevelInner;
int32_t borderStyle;
int32_t kind;
int32_t min;
int32_t max;
int32_t position;
int32_t largeChange;
int32_t smallChange;
int32_t autoOpen;
char fileName[256];
char deviceType[64];
bool hasStretch;
bool hasCenter;
bool hasTransparent;
bool hasBevelOuter;
bool hasBevelInner;
bool hasBorderStyle;
bool hasKind;
bool hasMin;
bool hasMax;
bool hasPosition;
bool hasLargeChange;
bool hasSmallChange;
bool hasAutoOpen;
bool hasFileName;
bool hasDeviceType;
} DfmCtrlT; } DfmCtrlT;
typedef struct { typedef struct {
@ -306,6 +342,24 @@ static CtrlTypeE mapClassName(const char *className) {
if (stricmp_local(className, "TMemo") == 0) { if (stricmp_local(className, "TMemo") == 0) {
return ctMemo; return ctMemo;
} }
if (stricmp_local(className, "TImage") == 0) {
return ctImage;
}
if (stricmp_local(className, "TGroupBox") == 0) {
return ctGroupBox;
}
if (stricmp_local(className, "TRadioButton") == 0) {
return ctRadioButton;
}
if (stricmp_local(className, "TPanel") == 0) {
return ctPanel;
}
if (stricmp_local(className, "TScrollBar") == 0) {
return ctScrollBar;
}
if (stricmp_local(className, "TMediaPlayer") == 0) {
return ctMediaPlayer;
}
return ctUnknown; return ctUnknown;
} }
@ -320,6 +374,9 @@ static void initCtrl(DfmCtrlT *ctrl) {
ctrl->visible = 1; ctrl->visible = 1;
ctrl->itemIndex = -1; ctrl->itemIndex = -1;
ctrl->tabOrder = -1; ctrl->tabOrder = -1;
ctrl->bevelOuter = 1; // bvRaised
ctrl->largeChange = 1;
ctrl->smallChange = 1;
} }
@ -594,6 +651,145 @@ static void parseProperties(const uint8_t *data, int32_t size, int32_t *pos, Dfm
if (!isForm) { if (!isForm) {
ctrl->hasOnMouseUp = true; ctrl->hasOnMouseUp = true;
} }
} else if (!isForm && stricmp_local(propName, "Stretch") == 0) {
if (tag == vaTrue) {
ctrl->stretch = 1;
} else if (tag == vaFalse) {
ctrl->stretch = 0;
} else {
ctrl->stretch = readIntValue(data, size, pos, tag);
}
ctrl->hasStretch = true;
} else if (!isForm && stricmp_local(propName, "Center") == 0) {
if (tag == vaTrue) {
ctrl->center = 1;
} else if (tag == vaFalse) {
ctrl->center = 0;
} else {
ctrl->center = readIntValue(data, size, pos, tag);
}
ctrl->hasCenter = true;
} else if (!isForm && stricmp_local(propName, "Transparent") == 0) {
if (tag == vaTrue) {
ctrl->transparent = 1;
} else if (tag == vaFalse) {
ctrl->transparent = 0;
} else {
ctrl->transparent = readIntValue(data, size, pos, tag);
}
ctrl->hasTransparent = true;
} else if (!isForm && stricmp_local(propName, "BevelOuter") == 0) {
if (tag == vaIdent) {
readStr(data, size, pos, strBuf, sizeof(strBuf));
if (stricmp_local(strBuf, "bvNone") == 0) {
ctrl->bevelOuter = 0;
} else if (stricmp_local(strBuf, "bvLowered") == 0) {
ctrl->bevelOuter = 1;
} else if (stricmp_local(strBuf, "bvRaised") == 0) {
ctrl->bevelOuter = 2;
} else {
ctrl->bevelOuter = 0;
}
} else {
ctrl->bevelOuter = readIntValue(data, size, pos, tag);
}
ctrl->hasBevelOuter = true;
} else if (!isForm && stricmp_local(propName, "BevelInner") == 0) {
if (tag == vaIdent) {
readStr(data, size, pos, strBuf, sizeof(strBuf));
if (stricmp_local(strBuf, "bvNone") == 0) {
ctrl->bevelInner = 0;
} else if (stricmp_local(strBuf, "bvLowered") == 0) {
ctrl->bevelInner = 1;
} else if (stricmp_local(strBuf, "bvRaised") == 0) {
ctrl->bevelInner = 2;
} else {
ctrl->bevelInner = 0;
}
} else {
ctrl->bevelInner = readIntValue(data, size, pos, tag);
}
ctrl->hasBevelInner = true;
} else if (!isForm && stricmp_local(propName, "BorderStyle") == 0) {
if (tag == vaIdent) {
readStr(data, size, pos, strBuf, sizeof(strBuf));
if (stricmp_local(strBuf, "bsNone") == 0) {
ctrl->borderStyle = 0;
} else if (stricmp_local(strBuf, "bsSingle") == 0) {
ctrl->borderStyle = 1;
} else {
ctrl->borderStyle = 0;
}
} else {
ctrl->borderStyle = readIntValue(data, size, pos, tag);
}
ctrl->hasBorderStyle = true;
} else if (!isForm && stricmp_local(propName, "Kind") == 0) {
if (tag == vaIdent) {
readStr(data, size, pos, strBuf, sizeof(strBuf));
if (stricmp_local(strBuf, "sbHorizontal") == 0) {
ctrl->kind = 0;
} else if (stricmp_local(strBuf, "sbVertical") == 0) {
ctrl->kind = 1;
} else {
ctrl->kind = 0;
}
} else {
ctrl->kind = readIntValue(data, size, pos, tag);
}
ctrl->hasKind = true;
} else if (!isForm && stricmp_local(propName, "Min") == 0) {
ctrl->min = readIntValue(data, size, pos, tag);
ctrl->hasMin = true;
} else if (!isForm && stricmp_local(propName, "Max") == 0) {
ctrl->max = readIntValue(data, size, pos, tag);
ctrl->hasMax = true;
} else if (!isForm && stricmp_local(propName, "Position") == 0) {
ctrl->position = readIntValue(data, size, pos, tag);
ctrl->hasPosition = true;
} else if (!isForm && stricmp_local(propName, "LargeChange") == 0) {
ctrl->largeChange = readIntValue(data, size, pos, tag);
ctrl->hasLargeChange = true;
} else if (!isForm && stricmp_local(propName, "SmallChange") == 0) {
ctrl->smallChange = readIntValue(data, size, pos, tag);
ctrl->hasSmallChange = true;
} else if (!isForm && stricmp_local(propName, "FileName") == 0) {
if (tag == vaString) {
readStr(data, size, pos, ctrl->fileName, sizeof(ctrl->fileName));
} else if (tag == vaLString) {
int32_t len = readInt32LE(data, size, pos);
int32_t copyLen = (len < (int32_t)sizeof(ctrl->fileName) - 1) ? len : (int32_t)sizeof(ctrl->fileName) - 1;
memcpy(ctrl->fileName, data + *pos, copyLen);
ctrl->fileName[copyLen] = '\0';
*pos += len;
} else {
skipValue(data, size, pos, tag);
}
ctrl->hasFileName = true;
} else if (!isForm && stricmp_local(propName, "DeviceType") == 0) {
if (tag == vaIdent) {
readStr(data, size, pos, ctrl->deviceType, sizeof(ctrl->deviceType));
} else if (tag == vaString) {
readStr(data, size, pos, ctrl->deviceType, sizeof(ctrl->deviceType));
} else if (tag == vaLString) {
int32_t len = readInt32LE(data, size, pos);
int32_t copyLen = (len < (int32_t)sizeof(ctrl->deviceType) - 1) ? len : (int32_t)sizeof(ctrl->deviceType) - 1;
memcpy(ctrl->deviceType, data + *pos, copyLen);
ctrl->deviceType[copyLen] = '\0';
*pos += len;
} else {
skipValue(data, size, pos, tag);
}
ctrl->hasDeviceType = true;
} else if (!isForm && stricmp_local(propName, "AutoOpen") == 0) {
if (tag == vaTrue) {
ctrl->autoOpen = 1;
} else if (tag == vaFalse) {
ctrl->autoOpen = 0;
} else {
ctrl->autoOpen = readIntValue(data, size, pos, tag);
}
ctrl->hasAutoOpen = true;
} else { } else {
skipValue(data, size, pos, tag); skipValue(data, size, pos, tag);
} }
@ -733,7 +929,9 @@ static void escapeStr(const char *src, char *dst, int32_t dstSize) {
static void emitCtrl(FILE *out, int32_t formId, int32_t ctrlId, DfmCtrlT *ctrl) { static void emitCtrl(FILE *out, int32_t formId, int32_t ctrlId, DfmCtrlT *ctrl) {
static const char *typeNames[] = { static const char *typeNames[] = {
"Unknown", "Label", "Edit", "Button", "Unknown", "Label", "Edit", "Button",
"CheckBox", "ListBox", "ComboBox", "Memo" "CheckBox", "ListBox", "ComboBox", "Memo",
"Image", "GroupBox", "RadioButton", "Panel",
"ScrollBar", "MediaPlayer"
}; };
char escaped[8192]; char escaped[8192];
@ -778,6 +976,53 @@ static void emitCtrl(FILE *out, int32_t formId, int32_t ctrlId, DfmCtrlT *ctrl)
if (ctrl->hasItemIndex && ctrl->itemIndex >= 0) { if (ctrl->hasItemIndex && ctrl->itemIndex >= 0) {
fprintf(out, " ItemIndex=%d", ctrl->itemIndex); fprintf(out, " ItemIndex=%d", ctrl->itemIndex);
} }
if (ctrl->hasStretch && ctrl->stretch) {
fprintf(out, " Stretch=1");
}
if (ctrl->hasCenter && ctrl->center) {
fprintf(out, " Center=1");
}
if (ctrl->hasTransparent && ctrl->transparent) {
fprintf(out, " Transparent=1");
}
if (ctrl->hasBevelOuter) {
fprintf(out, " BevelOuter=%d", ctrl->bevelOuter);
}
if (ctrl->hasBevelInner && ctrl->bevelInner != 0) {
fprintf(out, " BevelInner=%d", ctrl->bevelInner);
}
if (ctrl->hasBorderStyle && ctrl->borderStyle != 0) {
fprintf(out, " BorderStyle=%d", ctrl->borderStyle);
}
if (ctrl->hasKind && ctrl->kind != 0) {
fprintf(out, " Kind=%d", ctrl->kind);
}
if (ctrl->hasMin) {
fprintf(out, " Min=%d", ctrl->min);
}
if (ctrl->hasMax) {
fprintf(out, " Max=%d", ctrl->max);
}
if (ctrl->hasPosition && ctrl->position != 0) {
fprintf(out, " Position=%d", ctrl->position);
}
if (ctrl->hasLargeChange && ctrl->largeChange != 1) {
fprintf(out, " LargeChange=%d", ctrl->largeChange);
}
if (ctrl->hasSmallChange && ctrl->smallChange != 1) {
fprintf(out, " SmallChange=%d", ctrl->smallChange);
}
if (ctrl->hasFileName) {
escapeStr(ctrl->fileName, escaped, sizeof(escaped));
fprintf(out, " FileName=\"%s\"", escaped);
}
if (ctrl->hasDeviceType) {
escapeStr(ctrl->deviceType, escaped, sizeof(escaped));
fprintf(out, " DeviceType=\"%s\"", escaped);
}
if (ctrl->hasAutoOpen && ctrl->autoOpen) {
fprintf(out, " AutoOpen=1");
}
fprintf(out, "\n"); fprintf(out, "\n");
@ -785,8 +1030,8 @@ static void emitCtrl(FILE *out, int32_t formId, int32_t ctrlId, DfmCtrlT *ctrl)
// Auto-wired: Button/CheckBox→Click, Edit→Change, ListBox→Select, // Auto-wired: Button/CheckBox→Click, Edit→Change, ListBox→Select,
// ComboBox→Select+Change, Memo→Change // ComboBox→Select+Change, Memo→Change
bool autoClick = (ctrl->type == ctButton || ctrl->type == ctCheckBox); bool autoClick = (ctrl->type == ctButton || ctrl->type == ctCheckBox || ctrl->type == ctRadioButton);
bool autoChange = (ctrl->type == ctEdit || ctrl->type == ctComboBox || ctrl->type == ctMemo); bool autoChange = (ctrl->type == ctEdit || ctrl->type == ctComboBox || ctrl->type == ctMemo || ctrl->type == ctScrollBar);
bool autoSelect = (ctrl->type == ctListBox || ctrl->type == ctComboBox); bool autoSelect = (ctrl->type == ctListBox || ctrl->type == ctComboBox);
if (ctrl->hasOnClick && !autoClick) { if (ctrl->hasOnClick && !autoClick) {

View file

@ -13,7 +13,7 @@ unit FormCli;
interface interface
uses uses
SysUtils, Classes, Controls, Forms, StdCtrls, WinTypes, WinProcs; SysUtils, Classes, Controls, Forms, StdCtrls, ExtCtrls, MPlayer, WinTypes, WinProcs;
const const
MaxMsgLen = 4096; MaxMsgLen = 4096;
@ -27,11 +27,14 @@ type
end; end;
TCtrlTypeE = (ctUnknown, ctLabel, ctEdit, ctButton, TCtrlTypeE = (ctUnknown, ctLabel, ctEdit, ctButton,
ctCheckBox, ctListBox, ctComboBox, ctMemo); ctCheckBox, ctListBox, ctComboBox, ctMemo,
ctImage, ctGroupBox, ctRadioButton, ctPanel,
ctScrollBar, ctMediaPlayer);
{ Bound event flags } { Bound event flags }
TBoundEvent = (beDblClick, beKeyDown, beKeyUp, TBoundEvent = (beDblClick, beKeyDown, beKeyUp,
beEnter, beExit, beMouseDown, beMouseUp, beMouseMove); beEnter, beExit, beMouseDown, beMouseUp, beMouseMove,
beClick, beNotify);
TBoundEvents = set of TBoundEvent; TBoundEvents = set of TBoundEvent;
{ Per-control record } { Per-control record }
@ -58,6 +61,7 @@ type
FForms: TList; { of PFormRec } FForms: TList; { of PFormRec }
FMsgBuf: PChar; { read buffer } FMsgBuf: PChar; { read buffer }
FTmpBuf: PChar; { scratch buffer for outgoing messages } FTmpBuf: PChar; { scratch buffer for outgoing messages }
FBasePath: string;
{ Command dispatch } { Command dispatch }
procedure DoCtrlCreate(P: PChar); procedure DoCtrlCreate(P: PChar);
@ -102,6 +106,10 @@ type
procedure HandleMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); procedure HandleMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
procedure HandleMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer); procedure HandleMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer);
procedure HandleMouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); procedure HandleMouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
procedure HandleClick(Sender: TObject);
procedure HandleRadioButtonClick(Sender: TObject);
procedure HandleScrollBarChange(Sender: TObject);
procedure HandleMediaPlayerNotify(Sender: TObject);
{ Outgoing event helpers } { Outgoing event helpers }
procedure SendEvent(FormId, CtrlId: Integer; const EventName: string; const Data: string); procedure SendEvent(FormId, CtrlId: Integer; const EventName: string; const Data: string);
@ -116,6 +124,7 @@ type
destructor Destroy; override; destructor Destroy; override;
procedure ProcessMessages; procedure ProcessMessages;
property Transport: TFormTransport read FTransport; property Transport: TFormTransport read FTransport;
property BasePath: string read FBasePath write FBasePath;
end; end;
implementation implementation
@ -632,6 +641,58 @@ begin
end; end;
procedure TFormClient.HandleClick(Sender: TObject);
var
Tag: Longint;
FormId: Integer;
CtrlId: Integer;
begin
Tag := (Sender as TControl).Tag;
FormId := Tag shr 16;
CtrlId := Tag and $FFFF;
SendEvent(FormId, CtrlId, 'Click', '');
end;
procedure TFormClient.HandleRadioButtonClick(Sender: TObject);
var
Tag: Longint;
FormId: Integer;
CtrlId: Integer;
begin
Tag := (Sender as TControl).Tag;
FormId := Tag shr 16;
CtrlId := Tag and $FFFF;
SendEvent(FormId, CtrlId, 'Click', '');
end;
procedure TFormClient.HandleScrollBarChange(Sender: TObject);
var
Tag: Longint;
FormId: Integer;
CtrlId: Integer;
begin
Tag := (Sender as TControl).Tag;
FormId := Tag shr 16;
CtrlId := Tag and $FFFF;
SendEvent(FormId, CtrlId, 'Change', IntToStr((Sender as TScrollBar).Position));
end;
procedure TFormClient.HandleMediaPlayerNotify(Sender: TObject);
var
Tag: Longint;
FormId: Integer;
CtrlId: Integer;
begin
Tag := (Sender as TControl).Tag;
FormId := Tag shr 16;
CtrlId := Tag and $FFFF;
SendEvent(FormId, CtrlId, 'Notify', '');
end;
procedure TFormClient.HandleFormClose(Sender: TObject; procedure TFormClient.HandleFormClose(Sender: TObject;
var Action: TCloseAction); var Action: TCloseAction);
var var
@ -665,6 +726,10 @@ begin
(CR^.Control as TComboBox).OnClick := HandleComboBoxSelect; (CR^.Control as TComboBox).OnClick := HandleComboBoxSelect;
(CR^.Control as TComboBox).OnChange := HandleComboBoxChange; (CR^.Control as TComboBox).OnChange := HandleComboBoxChange;
end; end;
ctRadioButton:
(CR^.Control as TRadioButton).OnClick := HandleRadioButtonClick;
ctScrollBar:
(CR^.Control as TScrollBar).OnChange := HandleScrollBarChange;
end; end;
end; end;
@ -714,6 +779,22 @@ begin
begin begin
(CR^.Control as TControl).OnMouseMove := HandleMouseMove; (CR^.Control as TControl).OnMouseMove := HandleMouseMove;
CR^.Bound := CR^.Bound + [beMouseMove]; CR^.Bound := CR^.Bound + [beMouseMove];
end
else if EventName = 'Click' then
begin
if CR^.Control is TImage then
(CR^.Control as TImage).OnClick := HandleClick
else if CR^.Control is TPanel then
(CR^.Control as TPanel).OnClick := HandleClick
else if CR^.Control is TGroupBox then
(CR^.Control as TGroupBox).OnClick := HandleClick;
CR^.Bound := CR^.Bound + [beClick];
end
else if EventName = 'Notify' then
begin
if CR^.Control is TMediaPlayer then
(CR^.Control as TMediaPlayer).OnNotify := HandleMediaPlayerNotify;
CR^.Bound := CR^.Bound + [beNotify];
end; end;
end; end;
@ -763,6 +844,22 @@ begin
begin begin
(CR^.Control as TControl).OnMouseMove := nil; (CR^.Control as TControl).OnMouseMove := nil;
CR^.Bound := CR^.Bound - [beMouseMove]; CR^.Bound := CR^.Bound - [beMouseMove];
end
else if EventName = 'Click' then
begin
if CR^.Control is TImage then
(CR^.Control as TImage).OnClick := nil
else if CR^.Control is TPanel then
(CR^.Control as TPanel).OnClick := nil
else if CR^.Control is TGroupBox then
(CR^.Control as TGroupBox).OnClick := nil;
CR^.Bound := CR^.Bound - [beClick];
end
else if EventName = 'Notify' then
begin
if CR^.Control is TMediaPlayer then
(CR^.Control as TMediaPlayer).OnNotify := nil;
CR^.Bound := CR^.Bound - [beNotify];
end; end;
end; end;
@ -787,6 +884,9 @@ begin
ctLabel: (CR^.Control as TLabel).Caption := StrPas(Unesc); ctLabel: (CR^.Control as TLabel).Caption := StrPas(Unesc);
ctButton: (CR^.Control as TButton).Caption := StrPas(Unesc); ctButton: (CR^.Control as TButton).Caption := StrPas(Unesc);
ctCheckBox: (CR^.Control as TCheckBox).Caption := StrPas(Unesc); ctCheckBox: (CR^.Control as TCheckBox).Caption := StrPas(Unesc);
ctGroupBox: (CR^.Control as TGroupBox).Caption := StrPas(Unesc);
ctRadioButton: (CR^.Control as TRadioButton).Caption := StrPas(Unesc);
ctPanel: (CR^.Control as TPanel).Caption := StrPas(Unesc);
end; end;
end end
else if S = 'Text' then else if S = 'Text' then
@ -881,7 +981,9 @@ begin
begin begin
N := StrToIntDef(StrPas(Value), 0); N := StrToIntDef(StrPas(Value), 0);
if CR^.CtrlType = ctCheckBox then if CR^.CtrlType = ctCheckBox then
(CR^.Control as TCheckBox).Checked := (N <> 0); (CR^.Control as TCheckBox).Checked := (N <> 0)
else if CR^.CtrlType = ctRadioButton then
(CR^.Control as TRadioButton).Checked := (N <> 0);
end end
else if S = 'Enabled' then else if S = 'Enabled' then
begin begin
@ -926,6 +1028,174 @@ begin
ctListBox: (CR^.Control as TListBox).ItemIndex := N; ctListBox: (CR^.Control as TListBox).ItemIndex := N;
ctComboBox: (CR^.Control as TComboBox).ItemIndex := N; ctComboBox: (CR^.Control as TComboBox).ItemIndex := N;
end; end;
end
else if S = 'Stretch' then
begin
N := StrToIntDef(StrPas(Value), 0);
if CR^.CtrlType = ctImage then
(CR^.Control as TImage).Stretch := (N <> 0);
end
else if S = 'Center' then
begin
N := StrToIntDef(StrPas(Value), 0);
if CR^.CtrlType = ctImage then
(CR^.Control as TImage).Center := (N <> 0);
end
else if S = 'Transparent' then
begin
N := StrToIntDef(StrPas(Value), 0);
if CR^.CtrlType = ctImage then
(CR^.Control as TImage).Transparent := (N <> 0);
end
else if S = 'Picture' then
begin
if CR^.CtrlType = ctImage then
begin
UnescapeString(Value, Unesc, SizeOf(Unesc));
try
if FBasePath <> '' then
(CR^.Control as TImage).Picture.LoadFromFile(FBasePath + '\' + StrPas(Unesc))
else
(CR^.Control as TImage).Picture.LoadFromFile(StrPas(Unesc));
except
end;
end;
end
else if S = 'BevelOuter' then
begin
N := StrToIntDef(StrPas(Value), 1);
if CR^.CtrlType = ctPanel then
(CR^.Control as TPanel).BevelOuter := TBevelCut(N);
end
else if S = 'BevelInner' then
begin
N := StrToIntDef(StrPas(Value), 0);
if CR^.CtrlType = ctPanel then
(CR^.Control as TPanel).BevelInner := TBevelCut(N);
end
else if S = 'BorderStyle' then
begin
N := StrToIntDef(StrPas(Value), 0);
if CR^.CtrlType = ctPanel then
(CR^.Control as TPanel).BorderStyle := TBorderStyle(N);
end
else if S = 'Kind' then
begin
N := StrToIntDef(StrPas(Value), 0);
if CR^.CtrlType = ctScrollBar then
(CR^.Control as TScrollBar).Kind := TScrollBarKind(N);
end
else if S = 'Min' then
begin
N := StrToIntDef(StrPas(Value), 0);
if CR^.CtrlType = ctScrollBar then
(CR^.Control as TScrollBar).Min := N;
end
else if S = 'Max' then
begin
N := StrToIntDef(StrPas(Value), 0);
if CR^.CtrlType = ctScrollBar then
(CR^.Control as TScrollBar).Max := N;
end
else if S = 'Position' then
begin
N := StrToIntDef(StrPas(Value), 0);
if CR^.CtrlType = ctScrollBar then
(CR^.Control as TScrollBar).Position := N;
end
else if S = 'LargeChange' then
begin
N := StrToIntDef(StrPas(Value), 1);
if CR^.CtrlType = ctScrollBar then
(CR^.Control as TScrollBar).LargeChange := N;
end
else if S = 'SmallChange' then
begin
N := StrToIntDef(StrPas(Value), 1);
if CR^.CtrlType = ctScrollBar then
(CR^.Control as TScrollBar).SmallChange := N;
end
else if S = 'FileName' then
begin
if CR^.CtrlType = ctMediaPlayer then
begin
UnescapeString(Value, Unesc, SizeOf(Unesc));
if FBasePath <> '' then
(CR^.Control as TMediaPlayer).FileName := FBasePath + '\' + StrPas(Unesc)
else
(CR^.Control as TMediaPlayer).FileName := StrPas(Unesc);
end;
end
else if S = 'DeviceType' then
begin
if CR^.CtrlType = ctMediaPlayer then
begin
UnescapeString(Value, Unesc, SizeOf(Unesc));
S := StrPas(Unesc);
if S = 'dtAutoSelect' then
(CR^.Control as TMediaPlayer).DeviceType := dtAutoSelect
else if S = 'dtAVIVideo' then
(CR^.Control as TMediaPlayer).DeviceType := dtAVIVideo
else if S = 'dtCDAudio' then
(CR^.Control as TMediaPlayer).DeviceType := dtCDAudio
else if S = 'dtDAT' then
(CR^.Control as TMediaPlayer).DeviceType := dtDAT
else if S = 'dtDigitalVideo' then
(CR^.Control as TMediaPlayer).DeviceType := dtDigitalVideo
else if S = 'dtMMMovie' then
(CR^.Control as TMediaPlayer).DeviceType := dtMMMovie
else if S = 'dtOther' then
(CR^.Control as TMediaPlayer).DeviceType := dtOther
else if S = 'dtOverlay' then
(CR^.Control as TMediaPlayer).DeviceType := dtOverlay
else if S = 'dtScanner' then
(CR^.Control as TMediaPlayer).DeviceType := dtScanner
else if S = 'dtSequencer' then
(CR^.Control as TMediaPlayer).DeviceType := dtSequencer
else if S = 'dtVCR' then
(CR^.Control as TMediaPlayer).DeviceType := dtVCR
else if S = 'dtVideodisc' then
(CR^.Control as TMediaPlayer).DeviceType := dtVideodisc
else if S = 'dtWaveAudio' then
(CR^.Control as TMediaPlayer).DeviceType := dtWaveAudio
else
(CR^.Control as TMediaPlayer).DeviceType := dtAutoSelect;
end;
end
else if S = 'AutoOpen' then
begin
N := StrToIntDef(StrPas(Value), 0);
if CR^.CtrlType = ctMediaPlayer then
(CR^.Control as TMediaPlayer).AutoOpen := (N <> 0);
end
else if S = 'Command' then
begin
if CR^.CtrlType = ctMediaPlayer then
begin
UnescapeString(Value, Unesc, SizeOf(Unesc));
S := StrPas(Unesc);
try
if S = 'Open' then
(CR^.Control as TMediaPlayer).Open
else if S = 'Play' then
(CR^.Control as TMediaPlayer).Play
else if S = 'Stop' then
(CR^.Control as TMediaPlayer).Stop
else if S = 'Close' then
(CR^.Control as TMediaPlayer).Close
else if S = 'Pause' then
(CR^.Control as TMediaPlayer).Pause
else if S = 'Resume' then
(CR^.Control as TMediaPlayer).Resume
else if S = 'Rewind' then
(CR^.Control as TMediaPlayer).Rewind
else if S = 'Next' then
(CR^.Control as TMediaPlayer).Next
else if S = 'Previous' then
(CR^.Control as TMediaPlayer).Previous;
except
end;
end;
end; end;
end; end;
@ -1004,6 +1274,18 @@ begin
Result := ctComboBox Result := ctComboBox
else if S = 'Memo' then else if S = 'Memo' then
Result := ctMemo Result := ctMemo
else if S = 'Image' then
Result := ctImage
else if S = 'GroupBox' then
Result := ctGroupBox
else if S = 'RadioButton' then
Result := ctRadioButton
else if S = 'Panel' then
Result := ctPanel
else if S = 'ScrollBar' then
Result := ctScrollBar
else if S = 'MediaPlayer' then
Result := ctMediaPlayer
else else
Result := ctUnknown; Result := ctUnknown;
end; end;
@ -1135,6 +1417,21 @@ begin
Ctrl := TComboBox.Create(FR^.Form); Ctrl := TComboBox.Create(FR^.Form);
ctMemo: ctMemo:
Ctrl := TMemo.Create(FR^.Form); Ctrl := TMemo.Create(FR^.Form);
ctImage:
begin
Ctrl := TImage.Create(FR^.Form);
(Ctrl as TImage).AutoSize := False;
end;
ctGroupBox:
Ctrl := TGroupBox.Create(FR^.Form);
ctRadioButton:
Ctrl := TRadioButton.Create(FR^.Form);
ctPanel:
Ctrl := TPanel.Create(FR^.Form);
ctScrollBar:
Ctrl := TScrollBar.Create(FR^.Form);
ctMediaPlayer:
Ctrl := TMediaPlayer.Create(FR^.Form);
end; end;
if Ctrl = nil then if Ctrl = nil then

View file

@ -72,7 +72,7 @@ Event data varies by event type:
|-----------|-------------------------------| |-----------|-------------------------------|
| Click | (none) | | Click | (none) |
| DblClick | (none) | | DblClick | (none) |
| Change | `"new text"` | | Change | `"new text"` or `<position>` (ScrollBar) |
| Select | `<index> "selected text"` | | Select | `<index> "selected text"` |
| KeyDown | `<vkCode>` | | KeyDown | `<vkCode>` |
| KeyUp | `<vkCode>` | | KeyUp | `<vkCode>` |
@ -82,11 +82,12 @@ Event data varies by event type:
| Enter | (none) | | Enter | (none) |
| Exit | (none) | | Exit | (none) |
| Close | (none) | | Close | (none) |
| Notify | (none) |
## Control Types ## Control Types
| Type | Delphi Class | Auto-wired Events | | Type | Delphi Class | Auto-wired Events |
|----------|-------------|-------------------| |-------------|--------------|-------------------|
| Label | TLabel | (none) | | Label | TLabel | (none) |
| Edit | TEdit | Change | | Edit | TEdit | Change |
| Button | TButton | Click | | Button | TButton | Click |
@ -94,18 +95,28 @@ Event data varies by event type:
| ListBox | TListBox | Select | | ListBox | TListBox | Select |
| ComboBox | TComboBox | Select, Change | | ComboBox | TComboBox | Select, Change |
| Memo | TMemo | Change | | Memo | TMemo | Change |
| Image | TImage | (none) |
| GroupBox | TGroupBox | (none) |
| RadioButton | TRadioButton | Click |
| Panel | TPanel | (none) |
| ScrollBar | TScrollBar | Change |
| MediaPlayer | TMediaPlayer | (none) |
Opt-in events (require EVENT.BIND): DblClick, KeyDown, KeyUp, Enter, Exit, 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.
## Properties ## Properties
| Property | Applies To | Value Format | | Property | Applies To | Value Format |
|------------|-------------------------------|-------------------------------------------| |-------------|---------------------------------------------|-------------------------------------------|
| Caption | Label, Button, CheckBox | Quoted string | | Caption | Label, Button, CheckBox, GroupBox, RadioButton, Panel | Quoted string |
| Text | Edit, ComboBox, Memo | Quoted string (`\n` for line breaks) | | Text | Edit, ComboBox, Memo | Quoted string (`\n` for line breaks) |
| Items | ListBox, ComboBox | Quoted string (`\n`-delimited) | | Items | ListBox, ComboBox | Quoted string (`\n`-delimited) |
| Checked | CheckBox | 0 or 1 | | Checked | CheckBox, RadioButton | 0 or 1 |
| Enabled | All | 0 or 1 | | Enabled | All | 0 or 1 |
| Visible | All | 0 or 1 | | Visible | All | 0 or 1 |
| MaxLength | Edit | Integer | | MaxLength | Edit | Integer |
@ -113,6 +124,33 @@ MouseDown, MouseUp, MouseMove.
| ScrollBars | Memo | 0-3 (ssNone..ssBoth) | | ScrollBars | Memo | 0-3 (ssNone..ssBoth) |
| ItemIndex | ListBox, ComboBox | Integer (-1 = none) | | ItemIndex | ListBox, ComboBox | Integer (-1 = none) |
| TabOrder | All windowed controls | Integer | | TabOrder | All windowed controls | Integer |
| Stretch | Image | 0 or 1 |
| Center | Image | 0 or 1 |
| Transparent | Image | 0 or 1 |
| Picture | Image | Quoted string (filename, BMP only) |
| BevelOuter | Panel | 0-2 (bvNone, bvLowered, bvRaised) |
| BevelInner | Panel | 0-2 (bvNone, bvLowered, bvRaised) |
| BorderStyle | Panel | 0-1 (bsNone, bsSingle) |
| Kind | ScrollBar | 0-1 (sbHorizontal, sbVertical) |
| Min | ScrollBar | Integer |
| Max | ScrollBar | Integer |
| Position | ScrollBar | Integer |
| LargeChange | ScrollBar | Integer |
| SmallChange | ScrollBar | Integer |
| FileName | MediaPlayer | Quoted string (media file path) |
| DeviceType | MediaPlayer | Quoted string (e.g., `dtWaveAudio`) |
| AutoOpen | MediaPlayer | 0 or 1 |
| Command | MediaPlayer | Quoted string (pseudo-property, see below)|
File path properties (Picture, FileName) are resolved relative to the
client's `BasePath` setting. Subdirectories are allowed (e.g.,
`Picture="images\logo.bmp"` with `BasePath=C:\MYAPP` resolves to
`C:\MYAPP\images\logo.bmp`).
Command is a pseudo-property that triggers a method call on the
MediaPlayer rather than setting a value. Valid commands: `Open`,
`Play`, `Stop`, `Close`, `Pause`, `Resume`, `Rewind`, `Next`,
`Previous`.
## String Encoding ## String Encoding