WinComm/forms/README.md
Scott Duensing 0e8bb9f989 Place build outputs in obj/ and bin/ directories
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 15:38:59 -05:00

856 lines
30 KiB
Markdown

# Remote Forms System
A remote GUI system for Windows 3.1. Forms are designed visually in
Delphi 1.0's IDE, converted to a text protocol by a Linux tool, and
served to a Delphi 1.0 client over serial. The client creates native
Windows 3.1 controls and sends user events back to the server.
## Architecture
```
Linux Serial Windows 3.1
+-----------------+ +----------------+ +------------------+
| Delphi IDE | | | | |
| design → .DFM | | Transport | | TFormClient |
| | | | (newline- | | creates native |
| dfm2form | | delimited) | | controls from |
| | | | | | commands, sends |
| .form file | | Server -----> |---->| events back |
| | | | commands | | |
| FormServerT | | | | |
| loads .form, | | <----- Client |<----| |
| sends commands | | events | | |
+-----------------+ +----------------+ +------------------+
```
## Components
| File | Language | Platform | Purpose |
|---------------|-----------|----------|---------------------------------------|
| `dfm2form.c` | C | Linux | Converts binary DFM to `.form` text |
| `formsrv.h` | C | Linux | Server library header |
| `formsrv.c` | C | Linux | Server library implementation |
| `formcli.pas` | Pascal | Win 3.1 | Client form engine (Delphi 1.0) |
| `protocol.md` | — | — | Protocol specification |
## Building
```
make # builds bin/dfm2form and obj/formsrv.o
make clean # removes obj/ and bin/
```
Requires GCC on Linux. The Delphi unit (`formcli.pas`) is compiled as
part of a Delphi 1.0 project on Windows.
## DFM Converter
`dfm2form` reads Delphi 1.0 binary DFM files (TPF0 format) and outputs
`.form` files containing protocol commands. The form ID in the output
is a placeholder (`0`); the server assigns a dynamic ID when it streams
the file to the client.
```
dfm2form <input.dfm> [output.form]
```
**Examples:**
```
dfm2form login.dfm # output to stdout
dfm2form login.dfm login.form # output to file
```
**Output** is a sequence of protocol commands:
```
FORM.CREATE 0 400 300 "Login"
CTRL.CREATE 0 1 Label 20 20 100 17 Caption="Username:"
CTRL.CREATE 0 2 Edit 120 18 200 21 Text="" MaxLength=32 TabOrder=0
CTRL.CREATE 0 3 Label 20 52 100 17 Caption="Password:"
CTRL.CREATE 0 4 Edit 120 50 200 21 Text="" MaxLength=32 TabOrder=1
CTRL.CREATE 0 5 Button 245 90 75 25 Caption="OK" TabOrder=2
CTRL.CREATE 0 6 Button 160 90 75 25 Caption="Cancel" TabOrder=3
EVENT.BIND 0 5 Enter
FORM.SHOW 0
```
The placeholder `0` IDs are replaced at runtime by `formServerSendForm`.
The converter maps Delphi class names to protocol control types,
extracts geometry and properties, and emits `EVENT.BIND` for any
event handler assignments in the DFM that are not auto-wired (see
Events below). Unknown control classes are skipped with a warning.
## Server Library (C)
The server streams `.form` files from disk to a remote client through
a pluggable transport interface, assigning dynamic form IDs. It also
receives events from the client and dispatches them to a callback.
### Transport Interface
Implement `FormTransportT` to connect the server to your communication
channel:
```c
typedef struct {
int (*readMessage)(char *buf, int32_t maxLen, void *ctx);
void (*writeMessage)(const char *buf, void *ctx);
void *ctx;
} FormTransportT;
```
- `readMessage` — Read one complete message into `buf`. Return the
number of bytes read, or 0 if no message is available. Must not
block.
- `writeMessage` — Send a null-terminated message string. The
transport adds framing (e.g., CR+LF for serial).
- `ctx` — Opaque pointer passed through to both callbacks.
### API
```c
// Create/destroy
FormServerT *formServerCreate(FormTransportT *transport);
void formServerDestroy(FormServerT *server);
// Stream a .form file to the client, assigning a dynamic form ID.
// Returns the assigned form ID, or -1 on error.
int32_t formServerSendForm(FormServerT *server, const char *path);
// Form visibility
void formServerShowForm(FormServerT *server, int32_t formId);
void formServerHideForm(FormServerT *server, int32_t formId);
// Destroy a form on the client.
void formServerDestroyForm(FormServerT *server, int32_t formId);
// Update a control property
void formServerSetProp(FormServerT *server, int32_t formId,
int32_t ctrlId, const char *prop,
const char *value);
// Bind/unbind opt-in events
void formServerBindEvent(FormServerT *server, int32_t formId,
int32_t ctrlId, const char *eventName);
void formServerUnbindEvent(FormServerT *server, int32_t formId,
int32_t ctrlId, const char *eventName);
// Set event callback
void formServerSetEventCallback(FormServerT *server,
EventCallbackT cb, void *userData);
// Poll for one incoming event. Returns true if an event was processed.
bool formServerPollEvent(FormServerT *server);
```
### Event Callback
```c
typedef void (*EventCallbackT)(int32_t formId, int32_t ctrlId,
const char *eventName, const char *data,
void *userData);
```
- `formId` / `ctrlId` — Identify which control fired the event.
- `eventName``"Click"`, `"Change"`, `"Select"`, etc.
- `data` — Event-specific data (may be empty). See the Events
section below for formats.
### Server Example
```c
#include "formsrv.h"
void onEvent(int32_t formId, int32_t ctrlId, const char *eventName, const char *data, void *userData) {
printf("Event: form=%d ctrl=%d event=%s data=%s\n",
formId, ctrlId, eventName, data);
FormServerT *server = (FormServerT *)userData;
// React to a button click by updating a label
if (ctrlId == 5 && strcmp(eventName, "Click") == 0) {
formServerSetProp(server, formId, 1, "Caption", "\"Clicked!\"");
}
}
int main(void) {
FormTransportT transport = { myReadFn, myWriteFn, myCtx };
FormServerT *server = formServerCreate(&transport);
formServerSetEventCallback(server, onEvent, server);
int32_t formId = formServerSendForm(server, "login.form");
// Main loop
while (running) {
formServerPollEvent(server);
}
formServerDestroy(server);
return 0;
}
```
## Client Engine (Delphi 1.0)
The client receives protocol commands, creates native Windows 3.1
controls, and sends user events back through the transport.
### Transport Interface
Subclass `TFormTransport` to provide your communication channel:
```pascal
TFormTransport = class(TObject)
public
function ReadMessage(Buf: PChar; BufSize: Integer): Integer; virtual; abstract;
procedure WriteMessage(Buf: PChar; Len: Integer); virtual; abstract;
end;
```
- `ReadMessage` — Read one complete message into `Buf`. Return the
number of bytes read, or 0 if no message is available.
- `WriteMessage` — Send `Len` bytes from `Buf` as a complete message.
PChar-based (not short strings) to handle messages up to 4096 bytes.
### Serial Transport Example
```pascal
TSerialTransport = class(TFormTransport)
private
FComm: TKPComm;
FRecvBuf: array[0..4095] of Char;
FRecvLen: Integer;
public
function ReadMessage(Buf: PChar; BufSize: Integer): Integer; override;
procedure WriteMessage(Buf: PChar; Len: Integer); override;
end;
function TSerialTransport.ReadMessage(Buf: PChar; BufSize: Integer): Integer;
var
S: string;
I: Integer;
Len: Integer;
begin
Result := 0;
{ Accumulate serial data }
S := FComm.Input;
if Length(S) > 0 then
begin
Move(S[1], FRecvBuf[FRecvLen], Length(S));
Inc(FRecvLen, Length(S));
end;
{ Scan for newline }
for I := 0 to FRecvLen - 1 do
begin
if FRecvBuf[I] = #10 then
begin
Len := I;
if (Len > 0) and (FRecvBuf[Len - 1] = #13) then
Dec(Len);
if Len > BufSize - 1 then
Len := BufSize - 1;
Move(FRecvBuf, Buf^, Len);
Buf[Len] := #0;
{ Shift remainder }
Move(FRecvBuf[I + 1], FRecvBuf, FRecvLen - I - 1);
Dec(FRecvLen, I + 1);
Result := Len;
Exit;
end;
end;
end;
procedure TSerialTransport.WriteMessage(Buf: PChar; Len: Integer);
var
Msg: string;
begin
Msg := StrPas(Buf) + #13#10;
FComm.Output := Msg;
end;
```
### Using TFormClient
```pascal
var
Transport: TSerialTransport;
Client: TFormClient;
{ Setup }
Transport := TSerialTransport.Create;
Transport.FComm := KPComm1; { your TKPComm instance }
Client := TFormClient.Create(Transport);
{ Main loop (PeekMessage style) }
while not Terminated do
begin
while PeekMessage(Msg, 0, 0, 0, PM_REMOVE) do
begin
TranslateMessage(Msg);
DispatchMessage(Msg);
end;
Client.ProcessMessages; { pumps incoming commands }
Yield;
end;
{ Cleanup }
Client.Free;
Transport.Free;
```
`ProcessMessages` calls `ReadMessage` in a loop until no more
messages are available, dispatching each command as it arrives.
## Supported Controls
| Type | Delphi Class | Description |
|---------------|--------------|--------------------------------|
| `Label` | TLabel | Static text label |
| `Edit` | TEdit | Single-line text input |
| `Button` | TButton | Push button |
| `CheckBox` | TCheckBox | Check box with label |
| `ListBox` | TListBox | Scrollable list of items |
| `ComboBox` | TComboBox | Drop-down list with 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 |
| `MainMenu` | TMainMenu | Form main menu bar |
| `PopupMenu` | TPopupMenu | Context (right-click) menu |
| `MenuItem` | TMenuItem | Menu item (child of menu) |
| `RadioGroup` | TRadioGroup | Grouped radio buttons |
| `BitBtn` | TBitBtn | Button with bitmap glyph |
| `SpeedButton` | TSpeedButton | Flat toolbar-style button |
| `TabSet` | TTabSet | Tab strip (no pages) |
| `Notebook` | TNotebook | Multi-page container |
| `TabbedNotebook` | TTabbedNotebook | Tabbed multi-page container |
| `MaskEdit` | TMaskEdit | Masked text input |
| `Outline` | TOutline | Hierarchical tree list |
| `Bevel` | TBevel | Cosmetic beveled line/box |
| `Header` | THeader | Column header bar |
| `ScrollBox` | TScrollBox | Scrollable container |
| `StringGrid` | TStringGrid | Editable string grid |
### Creating Controls
Controls are created with `CTRL.CREATE`, specifying position, size,
and optional inline properties:
```
CTRL.CREATE <formId> <ctrlId> <type> <left> <top> <width> <height> [Key="val" ...]
```
Example:
```
CTRL.CREATE 1 3 Edit 120 50 200 21 Text="hello" MaxLength=32 TabOrder=1
```
Control IDs are positive integers assigned by the server. They must
be unique within a form.
### Updating Controls
Properties can be changed at any time after creation with `CTRL.SET`:
```
CTRL.SET <formId> <ctrlId> Key="val" [Key="val" ...]
```
Example:
```
CTRL.SET 1 3 Text="world" Enabled=0
```
## Properties
### Common Properties
These properties apply to all or most controls:
| Property | Format | Description |
|-----------|-------------------------------|-------------------------------|
| Enabled | `0` or `1` | Disable or enable the control |
| Visible | `0` or `1` | Hide or show the control |
| TabOrder | Integer | Keyboard tab order within the form (all windowed controls except Label and Image) |
| PopupMenu | Integer (ctrlId of PopupMenu) | Associates a right-click context menu with the control |
### Label, Button, GroupBox
| Property | Format |
|----------|---------------|
| Caption | Quoted string |
The display text for the control.
Example: `Caption="Submit"`
### Edit
| Property | Format | Description |
|-----------|---------------|------------------------------------|
| Text | Quoted string | Editable text content |
| MaxLength | Integer | Max characters (0 = no limit) |
| ReadOnly | `0` or `1` | Prevent user editing when enabled |
Example: `Text="Hello world" MaxLength=50`
### CheckBox, RadioButton
| Property | Format | Description |
|----------|---------------|----------------------|
| Caption | Quoted string | Display text |
| Checked | `0` or `1` | Checked or unchecked |
Example: `Caption="Accept terms" Checked=1`
### ListBox
| Property | Format | Description |
|-----------|-------------------------------------|---------------|
| Items | Quoted string (`\n`-delimited) | Item list |
| ItemIndex | Integer (-1 = none) | Selected item |
Replaces the entire item list. The control is cleared before the
new items are added.
Example: `Items="Red\nGreen\nBlue" ItemIndex=0`
### ComboBox
| Property | Format | Description |
|-----------|-------------------------------------|-----------------|
| Text | Quoted string | Editable text |
| Items | Quoted string (`\n`-delimited) | Drop-down items |
| ItemIndex | Integer (-1 = none) | Selected item |
Example: `Items="Red\nGreen\nBlue" ItemIndex=1`
### Memo
| Property | Format | Description |
|------------|--------------------------------------|------------------|
| Text | Quoted string (`\n` for line breaks) | Text content |
| ReadOnly | `0` or `1` | Prevent editing |
| ScrollBars | Integer 0-3 | Scroll bar style |
ScrollBars values:
- `0` — ssNone (no scroll bars)
- `1` — ssHorizontal
- `2` — ssVertical
- `3` — ssBoth
Example:
```
CTRL.SET 1 5 Text="Line one\nLine two\nLine three" ScrollBars=2
```
### Image
| Property | Format | Description |
|-------------|--------------------------|--------------------------|
| Picture | Quoted string (filename) | BMP file to display |
| Stretch | `0` or `1` | Stretch to fill bounds |
| Center | `0` or `1` | Center within bounds |
| Transparent | `0` or `1` | Transparent background |
Picture path is resolved relative to the client's `BasePath` setting.
Subdirectories are allowed. Only BMP files are supported.
Example: `Picture="images\logo.bmp" Stretch=1`
**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.
### Panel
| Property | Format | Description |
|-------------|---------------|--------------|
| Caption | Quoted string | Display text |
| BevelOuter | Integer 0-2 | Outer bevel |
| BevelInner | Integer 0-2 | Inner bevel |
| BorderStyle | `0` or `1` | Border |
Bevel values: `0` (bvNone), `1` (bvLowered), `2` (bvRaised).
BorderStyle values: `0` (bsNone), `1` (bsSingle).
Example: `BevelOuter=2 BevelInner=1 BorderStyle=0`
### ScrollBar
| Property | Format | Description |
|-------------|---------|---------------------------------------|
| Kind | Integer | `0` (sbHorizontal), `1` (sbVertical) |
| Min | Integer | Minimum value |
| Max | Integer | Maximum value |
| Position | Integer | Current position |
| LargeChange | Integer | Change on track click |
| SmallChange | Integer | Change on arrow click |
Example: `Kind=1 Min=0 Max=100 Position=50 LargeChange=10 SmallChange=1`
### MediaPlayer
| Property | Format | Description |
|------------|---------------|-----------------------------------|
| FileName | Quoted string | Media file path |
| DeviceType | Quoted string | MCI device type |
| AutoOpen | `0` or `1` | Open file automatically |
| Command | Quoted string | Pseudo-property (triggers method) |
FileName path is resolved relative to the client's `BasePath` setting.
DeviceType values: `dtAutoSelect`, `dtAVIVideo`, `dtCDAudio`, `dtDAT`,
`dtDigitalVideo`, `dtMMMovie`, `dtOther`, `dtOverlay`, `dtScanner`,
`dtSequencer`, `dtVCR`, `dtVideodisc`, `dtWaveAudio`.
Command triggers a method call instead of setting a value. Valid
commands: `Open`, `Play`, `Stop`, `Close`, `Pause`, `Resume`,
`Rewind`, `Next`, `Previous`.
Example: `FileName="sounds\intro.wav" DeviceType="dtWaveAudio" AutoOpen=1`
### MainMenu, PopupMenu
No type-specific properties. One MainMenu per form (auto-attached).
PopupMenu is associated with any control via the common `PopupMenu`
property.
### MenuItem
| Property | Format | Description |
|----------|---------------|------------------------------|
| Caption | Quoted string | Menu item text |
| Parent | Integer | ctrlId of parent menu/item |
| Checked | `0` or `1` | Check mark |
| ShortCut | Integer | Delphi ShortCut value |
ShortCut uses Delphi's encoding (virtual key + modifier flags).
Example: `Caption="&Open" Parent=2 ShortCut=16463`
### RadioGroup
| Property | Format | Description |
|-----------|--------------------------------|---------------------|
| Caption | Quoted string | Group label |
| Items | Quoted string (`\n`-delimited) | Radio button labels |
| ItemIndex | Integer (-1 = none) | Selected item |
| Columns | Integer | Column count |
Example: `Caption="Color" Items="Red\nGreen\nBlue" Columns=2 ItemIndex=0`
### BitBtn
| Property | Format | Description |
|-----------|---------------|------------------------|
| Caption | Quoted string | Button text |
| Kind | Integer 0-10 | Predefined button kind |
| Layout | Integer 0-3 | Glyph position |
| NumGlyphs | Integer 1-4 | Glyph images in bitmap |
Kind values: `0` (bkCustom), `1` (bkOK), `2` (bkCancel), `3` (bkHelp),
`4` (bkYes), `5` (bkNo), `6` (bkClose), `7` (bkAbort), `8` (bkRetry),
`9` (bkIgnore), `10` (bkAll).
Layout values: `0` (blGlyphLeft), `1` (blGlyphRight), `2` (blGlyphTop),
`3` (blGlyphBottom).
NumGlyphs: number of glyph images (up, disabled, clicked, down).
Example: `Caption="OK" Kind=1 Layout=0 NumGlyphs=2`
### SpeedButton
| Property | Format | Description |
|------------|---------------|-----------------------------|
| Caption | Quoted string | Button text |
| Layout | Integer 0-3 | Glyph position |
| NumGlyphs | Integer 1-4 | Glyph images in bitmap |
| GroupIndex | Integer | Radio group (0 = no group) |
| Down | `0` or `1` | Pressed state |
| AllowAllUp | `0` or `1` | Allow all buttons unpressed |
Layout values: `0` (blGlyphLeft), `1` (blGlyphRight), `2` (blGlyphTop),
`3` (blGlyphBottom).
Speed buttons with the same non-zero GroupIndex act as a radio group.
Down is only meaningful when GroupIndex is non-zero.
Example: `GroupIndex=1 Down=1 AllowAllUp=0`
### TabSet, Notebook, TabbedNotebook
| Property | Format | Description |
|-----------|--------------------------------|-------------|
| Items | Quoted string (`\n`-delimited) | Tab/page list |
| ItemIndex | Integer (-1 = none) | Active tab |
Example: `Items="General\nAdvanced\nAbout" ItemIndex=0`
### MaskEdit
| Property | Format | Description |
|-----------|---------------|-----------------------------------|
| Text | Quoted string | Editable text content |
| MaxLength | Integer | Max characters (0 = no limit) |
| EditMask | Quoted string | Input mask (mask;save;blank char) |
Example: `EditMask="(999) 000-0000;1;_"`
### Outline
| Property | Format | Description |
|--------------|--------------------------------|---------------|
| Items | Quoted string (`\n`-delimited) | Tree items |
| OutlineStyle | Integer 0-6 | Display style |
OutlineStyle values:
- `0` — osText
- `1` — osPlusMinusText
- `2` — osPlusMinus
- `3` — osPictureText
- `4` — osPicturePlusMinusText
- `5` — osTreeText
- `6` — osTreePictureText
Example: `OutlineStyle=5`
### Bevel
| Property | Format | Description |
|----------|-------------|----------------|
| Shape | Integer 0-5 | Bevel shape |
| Style | `0` or `1` | Lowered/raised |
Shape values:
- `0` — bsBox
- `1` — bsFrame
- `2` — bsTopLine
- `3` — bsBottomLine
- `4` — bsLeftLine
- `5` — bsRightLine
Style values: `0` (bsLowered), `1` (bsRaised).
Example: `Shape=2 Style=1`
### Header
| Property | Format | Description |
|----------|--------------------------------|-----------------|
| Items | Quoted string (`\n`-delimited) | Section headers |
Example: `Items="Name\nAge\nCity"`
### ScrollBox
No type-specific properties.
### StringGrid
| Property | Format | Description |
|------------------|---------------------------------------------------------|--------------------------|
| ColCount | Integer (default 5) | Number of columns |
| RowCount | Integer (default 5) | Number of rows |
| FixedCols | Integer (default 1) | Non-scrollable left cols |
| FixedRows | Integer (default 1) | Non-scrollable top rows |
| DefaultColWidth | Integer (pixels) | Default column width |
| DefaultRowHeight | Integer (pixels) | Default row height |
| Options | Integer (bitmask) | Grid options |
| Cells | Quoted string (tab-delimited cols, `\n`-delimited rows) | Bulk-load all cells |
| Cell | Quoted string (`col,row,value`) | Set a single cell |
Options 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 is a bulk-load property: columns are separated by tab characters,
rows by newlines. Row 0 is the first row (typically fixed header).
Cell sets a single cell value: column and row are zero-based indices.
Example: `Cells="Name\tAge\nAlice\t30\nBob\t25"`
Example: `Cell="1,2,Hello"`
### 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
### Auto-wired Events
These events are automatically connected when a control is created.
No `EVENT.BIND` command is needed.
| Control Type | Event | Data Sent |
|-------------|---------|-------------------------------|
| Button | Click | (none) |
| CheckBox | Click | (none) |
| RadioButton | Click | (none) |
| Edit | Change | `"new text"` |
| Memo | Change | `"new text"` |
| ScrollBar | Change | `<position>` |
| ListBox | Select | `<index> "selected text"` |
| ComboBox | Select | `<index> "selected text"` |
| ComboBox | Change | `"new text"` |
| MenuItem | Click | (none) |
| RadioGroup | Click | `<itemIndex>` |
| BitBtn | Click | (none) |
| SpeedButton | Click | (none) |
| TabSet | Change | `<tabIndex>` |
| TabbedNotebook | Change | `<pageIndex>` |
| MaskEdit | Change | `"new text"` |
| StringGrid | SelectCell | `<col> <row>` |
### Opt-in Events
These events require an explicit `EVENT.BIND` command before the
client will send them. Use `EVENT.UNBIND` to disconnect.
| Event | Data Sent | Notes |
|-----------|-----------------------|-----------------------------------|
| Click | (none) | Image, GroupBox, Panel only |
| DblClick | (none) | Double-click on any control |
| Notify | (none) | MediaPlayer playback notification |
| KeyDown | `<vkCode>` | Windows virtual key code |
| KeyUp | `<vkCode>` | Windows virtual key code |
| Enter | (none) | Control received focus |
| Exit | (none) | Control lost focus |
| 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
When the user clicks the form's close button, the client sends:
```
EVENT <formId> 0 Close
```
The form is **not** automatically destroyed. The server must send
`FORM.DESTROY` to close it, or `FORM.HIDE` to hide it, or ignore
the event to keep it open.
### Event Message Format
All events from client to server follow this format:
```
EVENT <formId> <ctrlId> <eventName> [<data>]
```
### Binding Example
```
EVENT.BIND 1 3 KeyDown (server sends)
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
sequences are recognized:
| Escape | Character |
|--------|-----------------|
| `\"` | Literal `"` |
| `\\` | Literal `\` |
| `\n` | Newline (LF) |
| `\r` | Carriage return |
| `\t` | Tab |
## Transport Layer
The protocol is transport-agnostic. Both the C server and Delphi
client communicate through abstract interfaces that deliver whole
messages. The current implementation uses newline-delimited serial
(CR+LF framing), but the transport can be replaced without changing
any protocol or application code.
### Requirements
- `ReadMessage` must be non-blocking (return 0 if no data).
- `WriteMessage` sends one complete message per call.
- Transport handles framing — the protocol layer never sees
delimiters.
## Limitations
- **RadioButton grouping:** All RadioButtons on a form belong to one
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
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
| Limit | Value |
|---------------------------|-------|
| Max message length | 4096 bytes |
| Max controls per form | 256 |
| Form ID range | 1-65535 (stored in high word of Tag) |
| Control ID range | 1-65535 (stored in low word of Tag) |