16 KiB
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:
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 intobuf. 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
// 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
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
#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:
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 intoBuf. Return the number of bytes read, or 0 if no message is available.WriteMessage— SendLenbytes fromBufas a complete message.
PChar-based (not short strings) to handle messages up to 4096 bytes.
Serial Transport Example
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
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) or1(checked) - Example:
Checked=1
Enabled
- Applies to: All control types
- Format:
0(disabled) or1(enabled) - Example:
Enabled=0
Visible
- Applies to: All control types
- Format:
0(hidden) or1(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) or1(read-only) - Example:
ReadOnly=1
ScrollBars
- Applies to: Memo
- Format: Integer 0-3
- Values:
0— ssNone (no scroll bars)1— ssHorizontal2— ssVertical3— 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
ReadMessagemust be non-blocking (return 0 if no data).WriteMessagesends 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) |