# 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 ] [output.form] ``` **Options:** - `-i ` — 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 [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 ### 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 | ` "selected text"` | | ComboBox | Select | ` "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 | `` | Windows virtual key code | | KeyUp | `` | Windows virtual key code | | Enter | (none) | Control received focus | | Exit | (none) | Control lost focus | | MouseDown | `