WinComm/forms
Scott Duensing 38125a51a1 Add documentation for remote forms system
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 18:55:32 -06:00
..
dfm2form.c Add remote forms system: DFM converter, server library, and client engine 2026-03-04 18:35:54 -06:00
formcli.pas Add remote forms system: DFM converter, server library, and client engine 2026-03-04 18:35:54 -06:00
formsrv.c Add remote forms system: DFM converter, server library, and client engine 2026-03-04 18:35:54 -06:00
formsrv.h Add remote forms system: DFM converter, server library, and client engine 2026-03-04 18:35:54 -06:00
makefile Add remote forms system: DFM converter, server library, and client engine 2026-03-04 18:35:54 -06:00
protocol.md Add remote forms system: DFM converter, server library, and client engine 2026-03-04 18:35:54 -06:00
README.md Add documentation for remote forms system 2026-03-04 18:55:32 -06:00

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 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

// 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 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

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) 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)