Add documentation for remote forms system
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
fd76319cb3
commit
38125a51a1
1 changed files with 547 additions and 0 deletions
547
forms/README.md
Normal file
547
forms/README.md
Normal file
|
|
@ -0,0 +1,547 @@
|
|||
# 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.
|
||||
|
||||
```
|
||||
dfm2form [-i <formId>] <input.dfm> [output.form]
|
||||
```
|
||||
|
||||
**Options:**
|
||||
|
||||
- `-i <formId>` — Set the form ID (default: 1). Each form needs a
|
||||
unique ID when serving multiple forms.
|
||||
|
||||
**Examples:**
|
||||
|
||||
```
|
||||
dfm2form login.dfm # output to stdout
|
||||
dfm2form login.dfm login.form # output to file
|
||||
dfm2form -i 2 settings.dfm settings.form
|
||||
```
|
||||
|
||||
**Output** is a sequence of protocol commands:
|
||||
|
||||
```
|
||||
FORM.CREATE 1 400 300 "Login"
|
||||
CTRL.CREATE 1 1 Label 20 20 100 17 Caption="Username:"
|
||||
CTRL.CREATE 1 2 Edit 120 18 200 21 Text="" MaxLength=32 TabOrder=0
|
||||
CTRL.CREATE 1 3 Label 20 52 100 17 Caption="Password:"
|
||||
CTRL.CREATE 1 4 Edit 120 50 200 21 Text="" MaxLength=32 TabOrder=1
|
||||
CTRL.CREATE 1 5 Button 245 90 75 25 Caption="OK" TabOrder=2
|
||||
CTRL.CREATE 1 6 Button 160 90 75 25 Caption="Cancel" TabOrder=3
|
||||
EVENT.BIND 1 5 Enter
|
||||
FORM.SHOW 1
|
||||
```
|
||||
|
||||
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 loads `.form` files and sends their commands to a remote
|
||||
client through a pluggable transport interface. 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);
|
||||
|
||||
// Load a .form file. Returns the form ID, or -1 on error.
|
||||
int32_t formServerLoadFile(FormServerT *server, const char *path);
|
||||
|
||||
// Send all commands for a loaded form to the client.
|
||||
void formServerSendForm(FormServerT *server, int32_t formId);
|
||||
|
||||
// Form visibility
|
||||
void formServerShowForm(FormServerT *server, int32_t formId);
|
||||
void formServerHideForm(FormServerT *server, int32_t formId);
|
||||
|
||||
// Destroy a form (sends FORM.DESTROY and removes from store)
|
||||
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 = formServerLoadFile(server, "login.form");
|
||||
formServerSendForm(server, formId);
|
||||
|
||||
// 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 forms (server) | 64 |
|
||||
| Max lines per .form file | 1024 |
|
||||
| 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) |
|
||||
Loading…
Add table
Reference in a new issue