WinComm/forms/dfm2form.c
Scott Duensing ae2aef0119 Add remote forms system: DFM converter, server library, and client engine
Text-based protocol for serving Delphi-designed forms over serial.
dfm2form converts binary DFM (TPF0) to protocol commands on Linux.
formsrv loads .form files and sends/receives via pluggable transport.
formcli creates native Win 3.1 controls and routes events back to server.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 18:35:54 -06:00

976 lines
32 KiB
C

// dfm2form.c - Convert Delphi 1.0 binary DFM (TPF0) to .form protocol text
//
// Usage: dfm2form [-i <formId>] <input.dfm> [output.form]
//
// Reads a binary DFM file, extracts the form and control definitions,
// and outputs protocol commands (FORM.CREATE, CTRL.CREATE, EVENT.BIND,
// FORM.SHOW) suitable for the remote forms system.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <stdbool.h>
#include <ctype.h>
#include <errno.h>
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
typedef enum {
ctUnknown = 0,
ctLabel,
ctEdit,
ctButton,
ctCheckBox,
ctListBox,
ctComboBox,
ctMemo
} CtrlTypeE;
typedef struct {
char name[64];
CtrlTypeE type;
int32_t left;
int32_t top;
int32_t width;
int32_t height;
char caption[256];
char text[4096];
char items[4096];
int32_t checked;
int32_t enabled;
int32_t visible;
int32_t maxLength;
int32_t readOnly;
int32_t scrollBars;
int32_t tabOrder;
int32_t itemIndex;
bool hasCaption;
bool hasText;
bool hasItems;
bool hasChecked;
bool hasEnabled;
bool hasVisible;
bool hasMaxLength;
bool hasReadOnly;
bool hasScrollBars;
bool hasTabOrder;
bool hasItemIndex;
bool hasOnClick;
bool hasOnChange;
bool hasOnDblClick;
bool hasOnEnter;
bool hasOnExit;
bool hasOnKeyDown;
bool hasOnKeyUp;
bool hasOnMouseDown;
bool hasOnMouseUp;
} DfmCtrlT;
typedef struct {
char name[64];
int32_t width;
int32_t height;
char caption[256];
DfmCtrlT ctrls[256];
int32_t ctrlCount;
} DfmFormT;
// DFM value type tags
enum {
vaNull = 0x00,
vaList = 0x01,
vaInt8 = 0x02,
vaInt16 = 0x03,
vaInt32 = 0x04,
vaExtended = 0x05,
vaString = 0x06,
vaIdent = 0x07,
vaFalse = 0x08,
vaTrue = 0x09,
vaBinary = 0x0A,
vaSet = 0x0B,
vaLString = 0x0C,
vaNil = 0x0D,
vaCollection = 0x0E
};
// ---------------------------------------------------------------------------
// Prototypes
// ---------------------------------------------------------------------------
static void emitCtrl(FILE *out, int32_t formId, int32_t ctrlId, DfmCtrlT *ctrl);
static void emitForm(FILE *out, int32_t formId, DfmFormT *form);
static void escapeStr(const char *src, char *dst, int32_t dstSize);
static void initCtrl(DfmCtrlT *ctrl);
static void initForm(DfmFormT *form);
static CtrlTypeE mapClassName(const char *className);
static bool parseComponent(const uint8_t *data, int32_t size, int32_t *pos, DfmFormT *form, bool isRoot);
static void parseProperties(const uint8_t *data, int32_t size, int32_t *pos, DfmFormT *form, DfmCtrlT *ctrl, bool isForm);
static int32_t readByte(const uint8_t *data, int32_t size, int32_t *pos);
static int32_t readInt16LE(const uint8_t *data, int32_t size, int32_t *pos);
static int32_t readInt32LE(const uint8_t *data, int32_t size, int32_t *pos);
static int32_t readIntValue(const uint8_t *data, int32_t size, int32_t *pos, uint8_t tag);
static bool readStr(const uint8_t *data, int32_t size, int32_t *pos, char *buf, int32_t bufSize);
static void skipValue(const uint8_t *data, int32_t size, int32_t *pos, uint8_t tag);
static int stricmp_local(const char *a, const char *b);
static void usage(const char *progName);
// ---------------------------------------------------------------------------
// Low-level DFM readers
// ---------------------------------------------------------------------------
static int32_t readByte(const uint8_t *data, int32_t size, int32_t *pos)
{
if (*pos >= size) {
fprintf(stderr, "Error: unexpected end of file at offset %d\n", *pos);
exit(1);
}
return data[(*pos)++];
}
static int32_t readInt16LE(const uint8_t *data, int32_t size, int32_t *pos)
{
if (*pos + 2 > size) {
fprintf(stderr, "Error: unexpected end of file at offset %d\n", *pos);
exit(1);
}
int16_t val = (int16_t)(data[*pos] | (data[*pos + 1] << 8));
*pos += 2;
return val;
}
static int32_t readInt32LE(const uint8_t *data, int32_t size, int32_t *pos)
{
if (*pos + 4 > size) {
fprintf(stderr, "Error: unexpected end of file at offset %d\n", *pos);
exit(1);
}
int32_t val = (int32_t)(data[*pos] | (data[*pos + 1] << 8) |
(data[*pos + 2] << 16) | (data[*pos + 3] << 24));
*pos += 4;
return val;
}
static bool readStr(const uint8_t *data, int32_t size, int32_t *pos, char *buf, int32_t bufSize)
{
int32_t len = readByte(data, size, pos);
if (*pos + len > size) {
fprintf(stderr, "Error: string overflows file at offset %d\n", *pos);
exit(1);
}
int32_t copyLen = (len < bufSize - 1) ? len : bufSize - 1;
memcpy(buf, data + *pos, copyLen);
buf[copyLen] = '\0';
*pos += len;
return true;
}
static int32_t readIntValue(const uint8_t *data, int32_t size, int32_t *pos, uint8_t tag)
{
switch (tag) {
case vaInt8:
return (int8_t)readByte(data, size, pos);
case vaInt16:
return readInt16LE(data, size, pos);
case vaInt32:
return readInt32LE(data, size, pos);
default:
return 0;
}
}
// ---------------------------------------------------------------------------
// Skip unknown property values
// ---------------------------------------------------------------------------
static void skipValue(const uint8_t *data, int32_t size, int32_t *pos, uint8_t tag)
{
int32_t len;
char buf[256];
switch (tag) {
case vaNull:
break;
case vaList:
// skip items until vaNull
while (*pos < size) {
uint8_t itemTag = readByte(data, size, pos);
if (itemTag == vaNull) {
break;
}
skipValue(data, size, pos, itemTag);
}
break;
case vaInt8:
*pos += 1;
break;
case vaInt16:
*pos += 2;
break;
case vaInt32:
*pos += 4;
break;
case vaExtended:
*pos += 10;
break;
case vaString:
len = readByte(data, size, pos);
*pos += len;
break;
case vaIdent:
len = readByte(data, size, pos);
*pos += len;
break;
case vaFalse:
case vaTrue:
case vaNil:
break;
case vaBinary:
len = readInt32LE(data, size, pos);
*pos += len;
break;
case vaSet:
// repeated strings, empty string terminates
while (*pos < size) {
readStr(data, size, pos, buf, sizeof(buf));
if (buf[0] == '\0') {
break;
}
}
break;
case vaLString:
len = readInt32LE(data, size, pos);
*pos += len;
break;
case vaCollection:
// items bracketed by vaList/vaNull
while (*pos < size) {
uint8_t itemTag = readByte(data, size, pos);
if (itemTag == vaNull) {
break;
}
skipValue(data, size, pos, itemTag);
}
break;
default:
fprintf(stderr, "Warning: unknown value tag 0x%02X at offset %d\n", tag, *pos);
break;
}
}
// ---------------------------------------------------------------------------
// Case-insensitive string compare
// ---------------------------------------------------------------------------
static int stricmp_local(const char *a, const char *b)
{
while (*a && *b) {
int ca = tolower((unsigned char)*a);
int cb = tolower((unsigned char)*b);
if (ca != cb) {
return ca - cb;
}
a++;
b++;
}
return (unsigned char)*a - (unsigned char)*b;
}
// ---------------------------------------------------------------------------
// Map Delphi class name to control type
// ---------------------------------------------------------------------------
static CtrlTypeE mapClassName(const char *className)
{
if (stricmp_local(className, "TLabel") == 0) {
return ctLabel;
}
if (stricmp_local(className, "TEdit") == 0) {
return ctEdit;
}
if (stricmp_local(className, "TButton") == 0) {
return ctButton;
}
if (stricmp_local(className, "TCheckBox") == 0) {
return ctCheckBox;
}
if (stricmp_local(className, "TListBox") == 0) {
return ctListBox;
}
if (stricmp_local(className, "TComboBox") == 0) {
return ctComboBox;
}
if (stricmp_local(className, "TMemo") == 0) {
return ctMemo;
}
return ctUnknown;
}
// ---------------------------------------------------------------------------
// Init helpers
// ---------------------------------------------------------------------------
static void initCtrl(DfmCtrlT *ctrl)
{
memset(ctrl, 0, sizeof(DfmCtrlT));
ctrl->enabled = 1;
ctrl->visible = 1;
ctrl->itemIndex = -1;
ctrl->tabOrder = -1;
}
static void initForm(DfmFormT *form)
{
memset(form, 0, sizeof(DfmFormT));
}
// ---------------------------------------------------------------------------
// Parse properties from DFM binary
// ---------------------------------------------------------------------------
static void parseProperties(const uint8_t *data, int32_t size, int32_t *pos,
DfmFormT *form, DfmCtrlT *ctrl, bool isForm)
{
char propName[64];
char strBuf[4096];
while (*pos < size) {
// Property name (empty = end of properties)
int32_t nameLen = readByte(data, size, pos);
if (nameLen == 0) {
break;
}
// Read the property name
(*pos)--; // back up, readStr reads the length byte
readStr(data, size, pos, propName, sizeof(propName));
// Read value tag
uint8_t tag = readByte(data, size, pos);
// Match known properties
if (isForm && stricmp_local(propName, "Caption") == 0) {
if (tag == vaString) {
readStr(data, size, pos, form->caption, sizeof(form->caption));
} else if (tag == vaLString) {
int32_t len = readInt32LE(data, size, pos);
int32_t copyLen = (len < (int32_t)sizeof(form->caption) - 1) ? len : (int32_t)sizeof(form->caption) - 1;
memcpy(form->caption, data + *pos, copyLen);
form->caption[copyLen] = '\0';
*pos += len;
} else {
skipValue(data, size, pos, tag);
}
} else if (isForm && stricmp_local(propName, "ClientWidth") == 0) {
form->width = readIntValue(data, size, pos, tag);
} else if (isForm && stricmp_local(propName, "ClientHeight") == 0) {
form->height = readIntValue(data, size, pos, tag);
} else if (isForm && stricmp_local(propName, "Width") == 0 && form->width == 0) {
form->width = readIntValue(data, size, pos, tag);
} else if (isForm && stricmp_local(propName, "Height") == 0 && form->height == 0) {
form->height = readIntValue(data, size, pos, tag);
} else if (!isForm && stricmp_local(propName, "Left") == 0) {
ctrl->left = readIntValue(data, size, pos, tag);
} else if (!isForm && stricmp_local(propName, "Top") == 0) {
ctrl->top = readIntValue(data, size, pos, tag);
} else if (!isForm && stricmp_local(propName, "Width") == 0) {
ctrl->width = readIntValue(data, size, pos, tag);
} else if (!isForm && stricmp_local(propName, "Height") == 0) {
ctrl->height = readIntValue(data, size, pos, tag);
} else if (!isForm && stricmp_local(propName, "Caption") == 0) {
if (tag == vaString) {
readStr(data, size, pos, ctrl->caption, sizeof(ctrl->caption));
} else if (tag == vaLString) {
int32_t len = readInt32LE(data, size, pos);
int32_t copyLen = (len < (int32_t)sizeof(ctrl->caption) - 1) ? len : (int32_t)sizeof(ctrl->caption) - 1;
memcpy(ctrl->caption, data + *pos, copyLen);
ctrl->caption[copyLen] = '\0';
*pos += len;
} else {
skipValue(data, size, pos, tag);
}
ctrl->hasCaption = true;
} else if (!isForm && stricmp_local(propName, "Text") == 0) {
if (tag == vaString) {
readStr(data, size, pos, ctrl->text, sizeof(ctrl->text));
} else if (tag == vaLString) {
int32_t len = readInt32LE(data, size, pos);
int32_t copyLen = (len < (int32_t)sizeof(ctrl->text) - 1) ? len : (int32_t)sizeof(ctrl->text) - 1;
memcpy(ctrl->text, data + *pos, copyLen);
ctrl->text[copyLen] = '\0';
*pos += len;
} else {
skipValue(data, size, pos, tag);
}
ctrl->hasText = true;
} else if (!isForm && stricmp_local(propName, "Items.Strings") == 0 && tag == vaList) {
// List of strings for ListBox/ComboBox
ctrl->items[0] = '\0';
int32_t itemsLen = 0;
while (*pos < size) {
uint8_t itemTag = readByte(data, size, pos);
if (itemTag == vaNull) {
break;
}
if (itemTag == vaString) {
readStr(data, size, pos, strBuf, sizeof(strBuf));
} else if (itemTag == vaLString) {
int32_t len = readInt32LE(data, size, pos);
int32_t copyLen = (len < (int32_t)sizeof(strBuf) - 1) ? len : (int32_t)sizeof(strBuf) - 1;
memcpy(strBuf, data + *pos, copyLen);
strBuf[copyLen] = '\0';
*pos += len;
} else {
skipValue(data, size, pos, itemTag);
continue;
}
int32_t slen = (int32_t)strlen(strBuf);
if (itemsLen + slen + 2 < (int32_t)sizeof(ctrl->items)) {
if (itemsLen > 0) {
ctrl->items[itemsLen++] = '\n';
}
memcpy(ctrl->items + itemsLen, strBuf, slen);
itemsLen += slen;
ctrl->items[itemsLen] = '\0';
}
}
ctrl->hasItems = true;
} else if (!isForm && stricmp_local(propName, "Checked") == 0) {
if (tag == vaTrue) {
ctrl->checked = 1;
} else if (tag == vaFalse) {
ctrl->checked = 0;
} else {
ctrl->checked = readIntValue(data, size, pos, tag);
}
ctrl->hasChecked = true;
} else if (!isForm && stricmp_local(propName, "State") == 0) {
// TCheckBox.State: cbUnchecked=0, cbChecked=1, cbGrayed=2
if (tag == vaIdent) {
readStr(data, size, pos, strBuf, sizeof(strBuf));
if (stricmp_local(strBuf, "cbChecked") == 0) {
ctrl->checked = 1;
} else {
ctrl->checked = 0;
}
} else {
ctrl->checked = readIntValue(data, size, pos, tag);
}
ctrl->hasChecked = true;
} else if (!isForm && stricmp_local(propName, "Enabled") == 0) {
if (tag == vaTrue) {
ctrl->enabled = 1;
} else if (tag == vaFalse) {
ctrl->enabled = 0;
} else {
ctrl->enabled = readIntValue(data, size, pos, tag);
}
ctrl->hasEnabled = true;
} else if (!isForm && stricmp_local(propName, "Visible") == 0) {
if (tag == vaTrue) {
ctrl->visible = 1;
} else if (tag == vaFalse) {
ctrl->visible = 0;
} else {
ctrl->visible = readIntValue(data, size, pos, tag);
}
ctrl->hasVisible = true;
} else if (!isForm && stricmp_local(propName, "MaxLength") == 0) {
ctrl->maxLength = readIntValue(data, size, pos, tag);
ctrl->hasMaxLength = true;
} else if (!isForm && stricmp_local(propName, "ReadOnly") == 0) {
if (tag == vaTrue) {
ctrl->readOnly = 1;
} else if (tag == vaFalse) {
ctrl->readOnly = 0;
} else {
ctrl->readOnly = readIntValue(data, size, pos, tag);
}
ctrl->hasReadOnly = true;
} else if (!isForm && stricmp_local(propName, "ScrollBars") == 0) {
if (tag == vaIdent) {
readStr(data, size, pos, strBuf, sizeof(strBuf));
if (stricmp_local(strBuf, "ssNone") == 0) {
ctrl->scrollBars = 0;
} else if (stricmp_local(strBuf, "ssHorizontal") == 0) {
ctrl->scrollBars = 1;
} else if (stricmp_local(strBuf, "ssVertical") == 0) {
ctrl->scrollBars = 2;
} else if (stricmp_local(strBuf, "ssBoth") == 0) {
ctrl->scrollBars = 3;
} else {
ctrl->scrollBars = 0;
}
} else {
ctrl->scrollBars = readIntValue(data, size, pos, tag);
}
ctrl->hasScrollBars = true;
} else if (!isForm && stricmp_local(propName, "TabOrder") == 0) {
ctrl->tabOrder = readIntValue(data, size, pos, tag);
ctrl->hasTabOrder = true;
} else if (!isForm && stricmp_local(propName, "ItemIndex") == 0) {
ctrl->itemIndex = readIntValue(data, size, pos, tag);
ctrl->hasItemIndex = true;
} else if (stricmp_local(propName, "OnClick") == 0) {
if (tag == vaIdent) {
readStr(data, size, pos, strBuf, sizeof(strBuf));
} else {
skipValue(data, size, pos, tag);
}
if (!isForm) {
ctrl->hasOnClick = true;
}
} else if (stricmp_local(propName, "OnChange") == 0) {
if (tag == vaIdent) {
readStr(data, size, pos, strBuf, sizeof(strBuf));
} else {
skipValue(data, size, pos, tag);
}
if (!isForm) {
ctrl->hasOnChange = true;
}
} else if (stricmp_local(propName, "OnDblClick") == 0) {
if (tag == vaIdent) {
readStr(data, size, pos, strBuf, sizeof(strBuf));
} else {
skipValue(data, size, pos, tag);
}
if (!isForm) {
ctrl->hasOnDblClick = true;
}
} else if (stricmp_local(propName, "OnEnter") == 0) {
if (tag == vaIdent) {
readStr(data, size, pos, strBuf, sizeof(strBuf));
} else {
skipValue(data, size, pos, tag);
}
if (!isForm) {
ctrl->hasOnEnter = true;
}
} else if (stricmp_local(propName, "OnExit") == 0) {
if (tag == vaIdent) {
readStr(data, size, pos, strBuf, sizeof(strBuf));
} else {
skipValue(data, size, pos, tag);
}
if (!isForm) {
ctrl->hasOnExit = true;
}
} else if (stricmp_local(propName, "OnKeyDown") == 0) {
if (tag == vaIdent) {
readStr(data, size, pos, strBuf, sizeof(strBuf));
} else {
skipValue(data, size, pos, tag);
}
if (!isForm) {
ctrl->hasOnKeyDown = true;
}
} else if (stricmp_local(propName, "OnKeyUp") == 0) {
if (tag == vaIdent) {
readStr(data, size, pos, strBuf, sizeof(strBuf));
} else {
skipValue(data, size, pos, tag);
}
if (!isForm) {
ctrl->hasOnKeyUp = true;
}
} else if (stricmp_local(propName, "OnMouseDown") == 0) {
if (tag == vaIdent) {
readStr(data, size, pos, strBuf, sizeof(strBuf));
} else {
skipValue(data, size, pos, tag);
}
if (!isForm) {
ctrl->hasOnMouseDown = true;
}
} else if (stricmp_local(propName, "OnMouseUp") == 0) {
if (tag == vaIdent) {
readStr(data, size, pos, strBuf, sizeof(strBuf));
} else {
skipValue(data, size, pos, tag);
}
if (!isForm) {
ctrl->hasOnMouseUp = true;
}
} else {
skipValue(data, size, pos, tag);
}
}
}
// ---------------------------------------------------------------------------
// Parse a component (form or child control) recursively
// ---------------------------------------------------------------------------
static bool parseComponent(const uint8_t *data, int32_t size, int32_t *pos,
DfmFormT *form, bool isRoot)
{
// Check for flags byte (Delphi 1.0 rarely uses this but handle it)
uint8_t peek = data[*pos];
if ((peek & 0xF0) == 0xF0) {
uint8_t flags = readByte(data, size, pos);
if (flags & 0x02) {
// ffChildPos: skip tagged integer
uint8_t childTag = readByte(data, size, pos);
readIntValue(data, size, pos, childTag);
}
}
// Class name
char className[64];
readStr(data, size, pos, className, sizeof(className));
// Instance name
char instName[64];
readStr(data, size, pos, instName, sizeof(instName));
if (isRoot) {
// This is the form itself
snprintf(form->name, sizeof(form->name), "%s", instName);
parseProperties(data, size, pos, form, NULL, true);
// Parse child components
while (*pos < size) {
peek = data[*pos];
if (peek == 0x00) {
(*pos)++; // consume terminator
break;
}
parseComponent(data, size, pos, form, false);
}
} else {
// Child control
CtrlTypeE type = mapClassName(className);
if (type == ctUnknown) {
fprintf(stderr, "Warning: unknown control class '%s' (%s), skipping\n", className, instName);
// Still need to parse properties and children to advance pos
DfmCtrlT dummy;
initCtrl(&dummy);
parseProperties(data, size, pos, form, &dummy, false);
// Skip nested children
while (*pos < size) {
peek = data[*pos];
if (peek == 0x00) {
(*pos)++;
break;
}
parseComponent(data, size, pos, form, false);
}
return true;
}
if (form->ctrlCount >= 256) {
fprintf(stderr, "Error: too many controls (max 256)\n");
exit(1);
}
DfmCtrlT *ctrl = &form->ctrls[form->ctrlCount];
initCtrl(ctrl);
ctrl->type = type;
snprintf(ctrl->name, sizeof(ctrl->name), "%s", instName);
parseProperties(data, size, pos, form, ctrl, false);
form->ctrlCount++;
// Skip nested children (controls within controls, e.g., panels)
while (*pos < size) {
peek = data[*pos];
if (peek == 0x00) {
(*pos)++;
break;
}
parseComponent(data, size, pos, form, false);
}
}
return true;
}
// ---------------------------------------------------------------------------
// Escape a string for protocol output
// ---------------------------------------------------------------------------
static void escapeStr(const char *src, char *dst, int32_t dstSize)
{
int32_t di = 0;
for (int32_t si = 0; src[si] != '\0' && di < dstSize - 2; si++) {
char c = src[si];
if (c == '"') {
if (di + 2 >= dstSize - 1) { break; }
dst[di++] = '\\';
dst[di++] = '"';
} else if (c == '\\') {
if (di + 2 >= dstSize - 1) { break; }
dst[di++] = '\\';
dst[di++] = '\\';
} else if (c == '\n') {
if (di + 2 >= dstSize - 1) { break; }
dst[di++] = '\\';
dst[di++] = 'n';
} else if (c == '\r') {
if (di + 2 >= dstSize - 1) { break; }
dst[di++] = '\\';
dst[di++] = 'r';
} else if (c == '\t') {
if (di + 2 >= dstSize - 1) { break; }
dst[di++] = '\\';
dst[di++] = 't';
} else {
dst[di++] = c;
}
}
dst[di] = '\0';
}
// ---------------------------------------------------------------------------
// Emit protocol commands for a single control
// ---------------------------------------------------------------------------
static void emitCtrl(FILE *out, int32_t formId, int32_t ctrlId, DfmCtrlT *ctrl)
{
static const char *typeNames[] = {
"Unknown", "Label", "Edit", "Button",
"CheckBox", "ListBox", "ComboBox", "Memo"
};
char escaped[8192];
fprintf(out, "CTRL.CREATE %d %d %s %d %d %d %d",
formId, ctrlId, typeNames[ctrl->type],
ctrl->left, ctrl->top, ctrl->width, ctrl->height);
// Inline properties
if (ctrl->hasCaption) {
escapeStr(ctrl->caption, escaped, sizeof(escaped));
fprintf(out, " Caption=\"%s\"", escaped);
}
if (ctrl->hasText) {
escapeStr(ctrl->text, escaped, sizeof(escaped));
fprintf(out, " Text=\"%s\"", escaped);
}
if (ctrl->hasItems) {
escapeStr(ctrl->items, escaped, sizeof(escaped));
fprintf(out, " Items=\"%s\"", escaped);
}
if (ctrl->hasChecked) {
fprintf(out, " Checked=%d", ctrl->checked);
}
if (ctrl->hasEnabled && ctrl->enabled == 0) {
fprintf(out, " Enabled=0");
}
if (ctrl->hasVisible && ctrl->visible == 0) {
fprintf(out, " Visible=0");
}
if (ctrl->hasMaxLength && ctrl->maxLength > 0) {
fprintf(out, " MaxLength=%d", ctrl->maxLength);
}
if (ctrl->hasReadOnly && ctrl->readOnly) {
fprintf(out, " ReadOnly=1");
}
if (ctrl->hasScrollBars && ctrl->scrollBars != 0) {
fprintf(out, " ScrollBars=%d", ctrl->scrollBars);
}
if (ctrl->hasTabOrder && ctrl->tabOrder >= 0) {
fprintf(out, " TabOrder=%d", ctrl->tabOrder);
}
if (ctrl->hasItemIndex && ctrl->itemIndex >= 0) {
fprintf(out, " ItemIndex=%d", ctrl->itemIndex);
}
fprintf(out, "\n");
// Emit EVENT.BIND for non-auto-wired events
// Auto-wired: Button/CheckBox→Click, Edit→Change, ListBox→Select,
// ComboBox→Select+Change, Memo→Change
bool autoClick = (ctrl->type == ctButton || ctrl->type == ctCheckBox);
bool autoChange = (ctrl->type == ctEdit || ctrl->type == ctComboBox || ctrl->type == ctMemo);
bool autoSelect = (ctrl->type == ctListBox || ctrl->type == ctComboBox);
if (ctrl->hasOnClick && !autoClick) {
fprintf(out, "EVENT.BIND %d %d Click\n", formId, ctrlId);
}
if (ctrl->hasOnChange && !autoChange) {
fprintf(out, "EVENT.BIND %d %d Change\n", formId, ctrlId);
}
if (ctrl->hasOnDblClick) {
fprintf(out, "EVENT.BIND %d %d DblClick\n", formId, ctrlId);
}
if (ctrl->hasOnEnter) {
fprintf(out, "EVENT.BIND %d %d Enter\n", formId, ctrlId);
}
if (ctrl->hasOnExit) {
fprintf(out, "EVENT.BIND %d %d Exit\n", formId, ctrlId);
}
if (ctrl->hasOnKeyDown) {
fprintf(out, "EVENT.BIND %d %d KeyDown\n", formId, ctrlId);
}
if (ctrl->hasOnKeyUp) {
fprintf(out, "EVENT.BIND %d %d KeyUp\n", formId, ctrlId);
}
if (ctrl->hasOnMouseDown) {
fprintf(out, "EVENT.BIND %d %d MouseDown\n", formId, ctrlId);
}
if (ctrl->hasOnMouseUp) {
fprintf(out, "EVENT.BIND %d %d MouseUp\n", formId, ctrlId);
}
// Suppress unused variable warning for autoSelect
(void)autoSelect;
}
// ---------------------------------------------------------------------------
// Emit the complete form
// ---------------------------------------------------------------------------
static void emitForm(FILE *out, int32_t formId, DfmFormT *form)
{
char escaped[512];
escapeStr(form->caption, escaped, sizeof(escaped));
fprintf(out, "FORM.CREATE %d %d %d \"%s\"\n",
formId, form->width, form->height, escaped);
for (int32_t i = 0; i < form->ctrlCount; i++) {
emitCtrl(out, formId, i + 1, &form->ctrls[i]);
}
fprintf(out, "FORM.SHOW %d\n", formId);
}
// ---------------------------------------------------------------------------
// Usage
// ---------------------------------------------------------------------------
static void usage(const char *progName)
{
fprintf(stderr, "Usage: %s [-i <formId>] <input.dfm> [output.form]\n", progName);
fprintf(stderr, " -i <formId> Set form ID (default: 1)\n");
exit(1);
}
// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
int main(int argc, char *argv[])
{
int32_t formId = 1;
const char *inputPath = NULL;
const char *outputPath = NULL;
// Parse arguments
int32_t i = 1;
while (i < argc) {
if (strcmp(argv[i], "-i") == 0) {
if (i + 1 >= argc) {
usage(argv[0]);
}
formId = atoi(argv[++i]);
if (formId <= 0) {
fprintf(stderr, "Error: form ID must be positive\n");
exit(1);
}
} else if (argv[i][0] == '-') {
usage(argv[0]);
} else if (inputPath == NULL) {
inputPath = argv[i];
} else if (outputPath == NULL) {
outputPath = argv[i];
} else {
usage(argv[0]);
}
i++;
}
if (inputPath == NULL) {
usage(argv[0]);
}
// Read input file
FILE *fin = fopen(inputPath, "rb");
if (fin == NULL) {
fprintf(stderr, "Error: cannot open '%s': %s\n", inputPath, strerror(errno));
exit(1);
}
fseek(fin, 0, SEEK_END);
long fileSize = ftell(fin);
fseek(fin, 0, SEEK_SET);
uint8_t *data = (uint8_t *)malloc(fileSize);
if (data == NULL) {
fprintf(stderr, "Error: out of memory\n");
fclose(fin);
exit(1);
}
if ((long)fread(data, 1, fileSize, fin) != fileSize) {
fprintf(stderr, "Error: failed to read '%s'\n", inputPath);
free(data);
fclose(fin);
exit(1);
}
fclose(fin);
// Verify TPF0 signature
if (fileSize < 4 || memcmp(data, "TPF0", 4) != 0) {
fprintf(stderr, "Error: '%s' is not a Delphi binary DFM (missing TPF0 signature)\n", inputPath);
free(data);
exit(1);
}
// Parse
DfmFormT form;
initForm(&form);
int32_t pos = 4; // skip TPF0
parseComponent(data, (int32_t)fileSize, &pos, &form, true);
// Default caption if empty
if (form.caption[0] == '\0') {
snprintf(form.caption, sizeof(form.caption), "%s", form.name);
}
// Output
FILE *fout;
if (outputPath != NULL) {
fout = fopen(outputPath, "w");
if (fout == NULL) {
fprintf(stderr, "Error: cannot create '%s': %s\n", outputPath, strerror(errno));
free(data);
exit(1);
}
} else {
fout = stdout;
}
emitForm(fout, formId, &form);
if (fout != stdout) {
fclose(fout);
}
free(data);
return 0;
}