WinComm/forms/README.md
Scott Duensing 0e8bb9f989 Place build outputs in obj/ and bin/ directories
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 15:38:59 -05:00

30 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 bin/dfm2form and obj/formsrv.o
make clean        # removes obj/ and bin/

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:

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

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

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

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
Image TImage Bitmap image display (BMP only)
GroupBox TGroupBox Cosmetic grouping frame
RadioButton TRadioButton Radio button (one group/form)
Panel TPanel Cosmetic container panel
ScrollBar TScrollBar Horizontal or vertical scrollbar
MediaPlayer TMediaPlayer MCI media player control
MainMenu TMainMenu Form main menu bar
PopupMenu TPopupMenu Context (right-click) menu
MenuItem TMenuItem Menu item (child of menu)
RadioGroup TRadioGroup Grouped radio buttons
BitBtn TBitBtn Button with bitmap glyph
SpeedButton TSpeedButton Flat toolbar-style button
TabSet TTabSet Tab strip (no pages)
Notebook TNotebook Multi-page container
TabbedNotebook TTabbedNotebook Tabbed multi-page container
MaskEdit TMaskEdit Masked text input
Outline TOutline Hierarchical tree list
Bevel TBevel Cosmetic beveled line/box
Header THeader Column header bar
ScrollBox TScrollBox Scrollable container
StringGrid TStringGrid Editable string grid

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

Common Properties

These properties apply to all or most controls:

Property Format Description
Enabled 0 or 1 Disable or enable the control
Visible 0 or 1 Hide or show the control
TabOrder Integer Keyboard tab order within the form (all windowed controls except Label and Image)
PopupMenu Integer (ctrlId of PopupMenu) Associates a right-click context menu with the control

Label, Button, GroupBox

Property Format
Caption Quoted string

The display text for the control.

Example: Caption="Submit"

Edit

Property Format Description
Text Quoted string Editable text content
MaxLength Integer Max characters (0 = no limit)
ReadOnly 0 or 1 Prevent user editing when enabled

Example: Text="Hello world" MaxLength=50

CheckBox, RadioButton

Property Format Description
Caption Quoted string Display text
Checked 0 or 1 Checked or unchecked

Example: Caption="Accept terms" Checked=1

ListBox

Property Format Description
Items Quoted string (\n-delimited) Item list
ItemIndex Integer (-1 = none) Selected item

Replaces the entire item list. The control is cleared before the new items are added.

Example: Items="Red\nGreen\nBlue" ItemIndex=0

ComboBox

Property Format Description
Text Quoted string Editable text
Items Quoted string (\n-delimited) Drop-down items
ItemIndex Integer (-1 = none) Selected item

Example: Items="Red\nGreen\nBlue" ItemIndex=1

Memo

Property Format Description
Text Quoted string (\n for line breaks) Text content
ReadOnly 0 or 1 Prevent editing
ScrollBars Integer 0-3 Scroll bar style

ScrollBars values:

  • 0 — ssNone (no scroll bars)
  • 1 — ssHorizontal
  • 2 — ssVertical
  • 3 — ssBoth

Example:

CTRL.SET 1 5 Text="Line one\nLine two\nLine three" ScrollBars=2

Image

Property Format Description
Picture Quoted string (filename) BMP file to display
Stretch 0 or 1 Stretch to fill bounds
Center 0 or 1 Center within bounds
Transparent 0 or 1 Transparent background

Picture path is resolved relative to the client's BasePath setting. Subdirectories are allowed. Only BMP files are supported.

Example: Picture="images\logo.bmp" Stretch=1

Note: dfm2form does not emit Picture from DFM files because image data is stored as a binary blob in the DFM. Set Picture at runtime via CTRL.SET or by manually editing the .form file.

Panel

Property Format Description
Caption Quoted string Display text
BevelOuter Integer 0-2 Outer bevel
BevelInner Integer 0-2 Inner bevel
BorderStyle 0 or 1 Border

Bevel values: 0 (bvNone), 1 (bvLowered), 2 (bvRaised). BorderStyle values: 0 (bsNone), 1 (bsSingle).

Example: BevelOuter=2 BevelInner=1 BorderStyle=0

ScrollBar

Property Format Description
Kind Integer 0 (sbHorizontal), 1 (sbVertical)
Min Integer Minimum value
Max Integer Maximum value
Position Integer Current position
LargeChange Integer Change on track click
SmallChange Integer Change on arrow click

Example: Kind=1 Min=0 Max=100 Position=50 LargeChange=10 SmallChange=1

MediaPlayer

Property Format Description
FileName Quoted string Media file path
DeviceType Quoted string MCI device type
AutoOpen 0 or 1 Open file automatically
Command Quoted string Pseudo-property (triggers method)

FileName path is resolved relative to the client's BasePath setting.

DeviceType values: dtAutoSelect, dtAVIVideo, dtCDAudio, dtDAT, dtDigitalVideo, dtMMMovie, dtOther, dtOverlay, dtScanner, dtSequencer, dtVCR, dtVideodisc, dtWaveAudio.

Command triggers a method call instead of setting a value. Valid commands: Open, Play, Stop, Close, Pause, Resume, Rewind, Next, Previous.

Example: FileName="sounds\intro.wav" DeviceType="dtWaveAudio" AutoOpen=1

MainMenu, PopupMenu

No type-specific properties. One MainMenu per form (auto-attached). PopupMenu is associated with any control via the common PopupMenu property.

MenuItem

Property Format Description
Caption Quoted string Menu item text
Parent Integer ctrlId of parent menu/item
Checked 0 or 1 Check mark
ShortCut Integer Delphi ShortCut value

ShortCut uses Delphi's encoding (virtual key + modifier flags).

Example: Caption="&Open" Parent=2 ShortCut=16463

RadioGroup

Property Format Description
Caption Quoted string Group label
Items Quoted string (\n-delimited) Radio button labels
ItemIndex Integer (-1 = none) Selected item
Columns Integer Column count

Example: Caption="Color" Items="Red\nGreen\nBlue" Columns=2 ItemIndex=0

BitBtn

Property Format Description
Caption Quoted string Button text
Kind Integer 0-10 Predefined button kind
Layout Integer 0-3 Glyph position
NumGlyphs Integer 1-4 Glyph images in bitmap

Kind values: 0 (bkCustom), 1 (bkOK), 2 (bkCancel), 3 (bkHelp), 4 (bkYes), 5 (bkNo), 6 (bkClose), 7 (bkAbort), 8 (bkRetry), 9 (bkIgnore), 10 (bkAll).

Layout values: 0 (blGlyphLeft), 1 (blGlyphRight), 2 (blGlyphTop), 3 (blGlyphBottom).

NumGlyphs: number of glyph images (up, disabled, clicked, down).

Example: Caption="OK" Kind=1 Layout=0 NumGlyphs=2

SpeedButton

Property Format Description
Caption Quoted string Button text
Layout Integer 0-3 Glyph position
NumGlyphs Integer 1-4 Glyph images in bitmap
GroupIndex Integer Radio group (0 = no group)
Down 0 or 1 Pressed state
AllowAllUp 0 or 1 Allow all buttons unpressed

Layout values: 0 (blGlyphLeft), 1 (blGlyphRight), 2 (blGlyphTop), 3 (blGlyphBottom).

Speed buttons with the same non-zero GroupIndex act as a radio group. Down is only meaningful when GroupIndex is non-zero.

Example: GroupIndex=1 Down=1 AllowAllUp=0

TabSet, Notebook, TabbedNotebook

Property Format Description
Items Quoted string (\n-delimited) Tab/page list
ItemIndex Integer (-1 = none) Active tab

Example: Items="General\nAdvanced\nAbout" ItemIndex=0

MaskEdit

Property Format Description
Text Quoted string Editable text content
MaxLength Integer Max characters (0 = no limit)
EditMask Quoted string Input mask (mask;save;blank char)

Example: EditMask="(999) 000-0000;1;_"

Outline

Property Format Description
Items Quoted string (\n-delimited) Tree items
OutlineStyle Integer 0-6 Display style

OutlineStyle values:

  • 0 — osText
  • 1 — osPlusMinusText
  • 2 — osPlusMinus
  • 3 — osPictureText
  • 4 — osPicturePlusMinusText
  • 5 — osTreeText
  • 6 — osTreePictureText

Example: OutlineStyle=5

Bevel

Property Format Description
Shape Integer 0-5 Bevel shape
Style 0 or 1 Lowered/raised

Shape values:

  • 0 — bsBox
  • 1 — bsFrame
  • 2 — bsTopLine
  • 3 — bsBottomLine
  • 4 — bsLeftLine
  • 5 — bsRightLine

Style values: 0 (bsLowered), 1 (bsRaised).

Example: Shape=2 Style=1

Header

Property Format Description
Items Quoted string (\n-delimited) Section headers

Example: Items="Name\nAge\nCity"

ScrollBox

No type-specific properties.

StringGrid

Property Format Description
ColCount Integer (default 5) Number of columns
RowCount Integer (default 5) Number of rows
FixedCols Integer (default 1) Non-scrollable left cols
FixedRows Integer (default 1) Non-scrollable top rows
DefaultColWidth Integer (pixels) Default column width
DefaultRowHeight Integer (pixels) Default row height
Options Integer (bitmask) Grid options
Cells Quoted string (tab-delimited cols, \n-delimited rows) Bulk-load all cells
Cell Quoted string (col,row,value) Set a single cell

Options bitmask of TGridOption values:

Bit Value Option Description
0 0x0001 goFixedVertLine Vertical lines on fixed
1 0x0002 goFixedHorzLine Horizontal lines on fixed
2 0x0004 goVertLine Vertical lines on cells
3 0x0008 goHorzLine Horizontal lines on cells
4 0x0010 goRangeSelect Allow range selection
5 0x0020 goDrawFocusSelected Draw focused cell selected
6 0x0040 goRowSizing Allow row resizing
7 0x0080 goColSizing Allow column resizing
8 0x0100 goRowMoving Allow row moving
9 0x0200 goColMoving Allow column moving
10 0x0400 goEditing Allow in-place editing
11 0x0800 goTabs Tab between cells
12 0x1000 goThumbTracking Track scrollbar thumb

Cells is a bulk-load property: columns are separated by tab characters, rows by newlines. Row 0 is the first row (typically fixed header). Cell sets a single cell value: column and row are zero-based indices.

Example: Cells="Name\tAge\nAlice\t30\nBob\t25"

Example: Cell="1,2,Hello"

BasePath

BasePath is a property on TFormClient (not a protocol property). Set it before loading forms to specify where file-based properties (Picture, FileName) resolve relative paths. Example:

Client.BasePath := 'C:\MYAPP';

A Picture value of "images\logo.bmp" then resolves to C:\MYAPP\images\logo.bmp.

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)
RadioButton Click (none)
Edit Change "new text"
Memo Change "new text"
ScrollBar Change <position>
ListBox Select <index> "selected text"
ComboBox Select <index> "selected text"
ComboBox Change "new text"
MenuItem Click (none)
RadioGroup Click <itemIndex>
BitBtn Click (none)
SpeedButton Click (none)
TabSet Change <tabIndex>
TabbedNotebook Change <pageIndex>
MaskEdit Change "new text"
StringGrid SelectCell <col> <row>

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
Click (none) Image, GroupBox, Panel only
DblClick (none) Double-click on any control
Notify (none) MediaPlayer playback notification
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
SetEditText <col> <row> "text" StringGrid in-place edit

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)

Menu Hierarchy Example

Menus are built using flat CTRL.CREATE commands with Parent properties to establish the hierarchy:

CTRL.CREATE 1 1 MainMenu 0 0 0 0
CTRL.CREATE 1 2 MenuItem 0 0 0 0 Caption="&File" Parent=1
CTRL.CREATE 1 3 MenuItem 0 0 0 0 Caption="&Open" Parent=2 ShortCut=16463
CTRL.CREATE 1 4 MenuItem 0 0 0 0 Caption="&Save" Parent=2 ShortCut=16467
CTRL.CREATE 1 5 MenuItem 0 0 0 0 Caption="E&xit" Parent=2
CTRL.CREATE 1 6 MenuItem 0 0 0 0 Caption="&Help" Parent=1
CTRL.CREATE 1 7 MenuItem 0 0 0 0 Caption="&About" Parent=6

This creates a menu bar with File (Open, Save, Exit) and Help (About). MenuItem ctrl 2 is a child of MainMenu ctrl 1; ctrl 3-5 are children of the File item (ctrl 2); ctrl 7 is a child of the Help item (ctrl 6).

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.

Limitations

  • RadioButton grouping: All RadioButtons on a form belong to one group. Use RadioGroup for multiple independent radio groups per form.
  • Image format: Only BMP files are supported for the Picture property.
  • dfm2form and Picture: The converter cannot extract image filenames from DFM files (image data is stored as a binary blob). Set Picture at runtime via CTRL.SET or by manually editing the .form file.
  • GroupBox and Panel: These are cosmetic-only containers. The protocol has no child containment — all controls are direct children of the form.

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)