# 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. 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 [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: ```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); // 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 ```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 = 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: ```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 | | `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 [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 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: ```pascal 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 | `` | | ListBox | Select | ` "selected text"` | | ComboBox | Select | ` "selected text"` | | ComboBox | Change | `"new text"` | | MenuItem | Click | (none) | | RadioGroup | Click | `` | | BitBtn | Click | (none) | | SpeedButton | Click | (none) | | TabSet | Change | `` | | TabbedNotebook | Change | `` | | MaskEdit | Change | `"new text"` | | StringGrid | SelectCell | ` ` | ### 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 | `` | Windows virtual key code | | KeyUp | `` | Windows virtual key code | | Enter | (none) | Control received focus | | Exit | (none) | Control lost focus | | MouseDown | `