547 lines
16 KiB
Markdown
547 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.
|
|
|
|
```
|
|
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) |
|