WinComm/forms/README.md
Scott Duensing e45d72588a Apply K&R brace style to drv/ and README examples
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 19:24:36 -06:00

537 lines
16 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 |
### 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
- **Format:** Quoted string
- **Example:** `Caption="Submit"`
The display text for labels, buttons, and check boxes.
### Text
- **Applies to:** Edit, ComboBox, Memo
- **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
- **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
- **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
- **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
- **Format:** Integer (-1 = no selection)
- **Example:** `ItemIndex=2`
### TabOrder
- **Applies to:** All windowed controls (all types except Label)
- **Format:** Integer
- **Example:** `TabOrder=3`
Controls the keyboard tab navigation order within the form.
## 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) |
| Edit | Change | `"new text"` |
| Memo | Change | `"new text"` |
| ListBox | Select | `<index> "selected text"` |
| ComboBox | Select | `<index> "selected text"` |
| ComboBox | 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 |
|-----------|-----------------------|---------------------------------|
| DblClick | (none) | Double-click on any control |
| 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)
```
## 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.
## 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) |