WinComm/forms/README.md
Scott Duensing dd115d3727 Add BitBtn, SpeedButton, TabSet, Notebook, TabbedNotebook, MaskEdit, Outline, Bevel, Header, and ScrollBox control types
Completes the Delphi 1.0 Standard, Additional, and Win31 component
palettes. DFM parser maps Tabs/Lines/Pages/Sections.Strings to Items
and TabIndex/PageIndex to ItemIndex. Kind extended with bk* idents
for BitBtn.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 15:13:59 -06:00

885 lines
25 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 dfm2form and formsrv.o
make clean # removes build artifacts
```
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 |
### 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
### Caption
- **Applies to:** Label, Button, CheckBox, GroupBox, RadioButton, Panel, MenuItem, RadioGroup, BitBtn, SpeedButton
- **Format:** Quoted string
- **Example:** `Caption="Submit"`
The display text for labels, buttons, check boxes, group boxes, radio
buttons, panels, menu items, radio groups, and bitmap/speed buttons.
### Text
- **Applies to:** Edit, ComboBox, Memo, MaskEdit
- **Format:** Quoted string
- **Example:** `Text="Hello world"`
The editable text content. For Memo controls, use `\n` for line
breaks within the quoted string:
```
CTRL.SET 1 5 Text="Line one\nLine two\nLine three"
```
### Items
- **Applies to:** ListBox, ComboBox, RadioGroup, TabSet, Notebook, TabbedNotebook, Outline, Header
- **Format:** Quoted string, items separated by `\n`
- **Example:** `Items="Red\nGreen\nBlue"`
Replaces the entire item list. The control is cleared before the
new items are added.
### Checked
- **Applies to:** CheckBox, RadioButton, MenuItem
- **Format:** `0` (unchecked) or `1` (checked)
- **Example:** `Checked=1`
### Enabled
- **Applies to:** All control types
- **Format:** `0` (disabled) or `1` (enabled)
- **Example:** `Enabled=0`
### Visible
- **Applies to:** All control types
- **Format:** `0` (hidden) or `1` (visible)
- **Example:** `Visible=0`
### MaxLength
- **Applies to:** Edit, MaskEdit
- **Format:** Integer (0 = no limit)
- **Example:** `MaxLength=50`
Maximum number of characters the user can type.
### ReadOnly
- **Applies to:** Edit, Memo
- **Format:** `0` (editable) or `1` (read-only)
- **Example:** `ReadOnly=1`
### ScrollBars
- **Applies to:** Memo
- **Format:** Integer 0-3
- **Values:**
- `0` — ssNone (no scroll bars)
- `1` — ssHorizontal
- `2` — ssVertical
- `3` — ssBoth
- **Example:** `ScrollBars=2`
### ItemIndex
- **Applies to:** ListBox, ComboBox, RadioGroup, TabSet, Notebook, TabbedNotebook
- **Format:** Integer (-1 = no selection)
- **Example:** `ItemIndex=2`
### TabOrder
- **Applies to:** All windowed controls (all types except Label and Image)
- **Format:** Integer
- **Example:** `TabOrder=3`
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, BitBtn
- **Format:** Integer
- **Values (ScrollBar):** `0` (sbHorizontal), `1` (sbVertical)
- **Values (BitBtn):** `0` (bkCustom), `1` (bkOK), `2` (bkCancel),
`3` (bkHelp), `4` (bkYes), `5` (bkNo), `6` (bkClose), `7` (bkAbort),
`8` (bkRetry), `9` (bkIgnore), `10` (bkAll)
- **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`.
### 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.
### Layout
- **Applies to:** BitBtn, SpeedButton
- **Format:** Integer 0-3
- **Values:**
- `0` — blGlyphLeft
- `1` — blGlyphRight
- `2` — blGlyphTop
- `3` — blGlyphBottom
- **Example:** `Layout=2`
Position of the glyph relative to the caption text.
### NumGlyphs
- **Applies to:** BitBtn, SpeedButton
- **Format:** Integer (1-4)
- **Example:** `NumGlyphs=2`
Number of glyph images in the bitmap (up, disabled, clicked, down).
### GroupIndex
- **Applies to:** SpeedButton
- **Format:** Integer (0 = no group)
- **Example:** `GroupIndex=1`
Speed buttons with the same non-zero GroupIndex act as a radio group.
### Down
- **Applies to:** SpeedButton
- **Format:** `0` (up) or `1` (down)
- **Example:** `Down=1`
Whether the speed button is pressed. Only meaningful when
GroupIndex is non-zero.
### AllowAllUp
- **Applies to:** SpeedButton
- **Format:** `0` (off) or `1` (on)
- **Example:** `AllowAllUp=1`
When enabled, all speed buttons in a group can be unpressed.
### EditMask
- **Applies to:** MaskEdit
- **Format:** Quoted string
- **Example:** `EditMask="(999) 000-0000;1;_"`
Input mask string (mask;save literals;blank char).
### OutlineStyle
- **Applies to:** Outline
- **Format:** Integer 0-6
- **Values:**
- `0` — osText
- `1` — osPlusMinusText
- `2` — osPlusMinus
- `3` — osPictureText
- `4` — osPicturePlusMinusText
- `5` — osTreeText
- `6` — osTreePictureText
- **Example:** `OutlineStyle=5`
### Shape
- **Applies to:** Bevel
- **Format:** Integer 0-5
- **Values:**
- `0` — bsBox
- `1` — bsFrame
- `2` — bsTopLine
- `3` — bsBottomLine
- `4` — bsLeftLine
- `5` — bsRightLine
- **Example:** `Shape=2`
### Style (Bevel)
- **Applies to:** Bevel
- **Format:** `0` (bsLowered) or `1` (bsRaised)
- **Example:** `Style=1`
### 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"` |
### 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 |
### 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) |