Delphi-Konzept: DFM/FMX Support hinzugefügt

- Neues Dokument 07-DFM-FMX-Support.md: Format-Referenz, Extraktions-Strategie, DfmExtractor-Konzept
- Neues Fixture MainForm.dfm: Verschachtelte Komponenten, Event-Handler (FormCreate, btnLoginClick, etc.)
- Integration Guide erweitert: DfmExtractor-Implementierung (analog LiquidExtractor), Coding-Style-Hinweise
- NodeKind-Mapping erweitert: DFM-Komponenten als component, Event-Handler als references, Verschachtelung als contains
- Checklist: DFM/FMX von 'Zukunft' auf konkreten Plan (Phase 1b) hochgestuft
- README aktualisiert: DFM/FMX-Support in Ziel, Inhalt und Grammar-Abschnitt aufgenommen
This commit is contained in:
Olaf Monien
2026-02-11 01:06:16 +01:00
parent dcca22c78f
commit 8dfc99572f
6 changed files with 499 additions and 13 deletions
+14 -4
View File
@@ -3,9 +3,9 @@
## A) Plumbing (Infrastruktur)
- [ ] `npm install tree-sitter-pascal` als Dependency
- [ ] `src/types.ts`: `'pascal'` zum `Language` Union-Type hinzufügen
- [ ] `src/types.ts`: `DEFAULT_CONFIG.include` um `'**/*.pas'`, `'**/*.dpr'`, `'**/*.dpk'`, `'**/*.lpr'` erweitern
- [ ] `src/types.ts`: `DEFAULT_CONFIG.include` um `'**/*.pas'`, `'**/*.dpr'`, `'**/*.dpk'`, `'**/*.lpr'`, `'**/*.dfm'`, `'**/*.fmx'` erweitern
- [ ] `src/extraction/grammars.ts`: Grammar-Loader `pascal: () => require('tree-sitter-pascal')`
- [ ] `src/extraction/grammars.ts`: Extension-Mapping `.pas`, `.dpr`, `.dpk`, `.lpr``'pascal'`
- [ ] `src/extraction/grammars.ts`: Extension-Mapping `.pas`, `.dpr`, `.dpk`, `.lpr`, `.dfm`, `.fmx``'pascal'`
- [ ] `src/extraction/grammars.ts`: Display-Name `pascal: 'Pascal / Delphi'`
- [ ] `src/extraction/tree-sitter.ts`: `LanguageExtractor` für `pascal` in `EXTRACTORS` Map
@@ -53,9 +53,19 @@
- [ ] Language in Supported-Languages-Dokumentation aufnehmen
- [ ] CLI-Hilfe / Config-Validierung aktualisieren
## H) Bekannte Einschränkungen (für spätere Phasen)
## H) DFM/FMX Form-Dateien (Phase 1b)
- [ ] `DfmExtractor`-Klasse in `src/extraction/tree-sitter.ts` implementieren (analog `LiquidExtractor`)
- [ ] Routing in `extractFromSource()`: `.dfm`/`.fmx``DfmExtractor`
- [ ] Komponenten als NodeKind `component` extrahieren (`object <Name>: <Typ>`)
- [ ] Verschachtelung als EdgeKind `contains` abbilden
- [ ] Event-Handler (`OnClick = MethodName`) als `UnresolvedReference` speichern → EdgeKind `references`
- [ ] Mehrzeilige Properties korrekt überspringen
- [ ] `inherited`-Blöcke als `component` behandeln
- [ ] DFM-Fixture (`MainForm.dfm`) in Test-Suite aufnehmen
- [ ] Resolution: Event-Handler-Namen zu Methoden in zugehöriger `.pas`-Datei auflösen
## I) Bekannte Einschränkungen (für spätere Phasen)
- [ ] **`with`-Statements** (Phase 2): Verschleiern den Qualifier bei Aufrufen. Im MVP werden Calls innerhalb von `with`-Blöcken ohne Qualifier extrahiert.
- [ ] **`.dfm`/`.fmx` Form-Dateien** (Zukunft): Enthalten Komponenten-Deklarationen und Event-Verknüpfungen. Aktuell nicht unterstützt.
- [ ] **`.inc` Include-Dateien** (Optional): Enthalten Code-Fragmente, die per `{$I filename}` eingebunden werden. Parser liefert möglicherweise keinen vollständigen AST.
- [ ] **Class Helpers / Record Helpers** (Phase 2): `declHelper`-Knoten erweitern existierende Typen. Erfordert spezielle Resolution-Logik.
- [ ] **Generics** (Phase 2): `typerefTpl` und `genericTpl` für generische Typen.
+20 -1
View File
@@ -43,13 +43,32 @@ Dieses Dokument zeigt die explizite Zuordnung von Delphi/Pascal-Konzepten zu den
| Unit enthält Klasse | AST Parent-Child | `contains` |
| Klasse enthält Methode | AST Parent-Child | `contains` |
## DFM/FMX Node-Mapping
DFM/FMX-Dateien werden durch einen eigenen `DfmExtractor` (Regex-basiert, analog `LiquidExtractor`) verarbeitet.
| DFM-Konzept | Erkennung | CodeGraph `NodeKind` | Anmerkungen |
|---|---|---|---|
| Formular | `object Form1: TForm1` (Top-Level) | `component` | Root-Komponente der DFM-Datei |
| UI-Komponente | `object Button1: TButton` (verschachtelt) | `component` | Jede Komponente wird ein Node |
| Geerbte Komponente | `inherited Form1: TForm1` | `component` | Formular-Vererbung |
## DFM/FMX Edge-Mapping
| DFM-Beziehung | Erkennung | CodeGraph `EdgeKind` | Beschreibung |
|---|---|---|---|
| Verschachtelung | `object` innerhalb `object` | `contains` | Panel1 enthält Button1 |
| Event-Handler | `OnClick = Button1Click` | `references` | Komponente → Methode in `.pas` |
| Datei enthält Formular | Top-Level `object` | `contains` | DFM-Datei → Root-Komponente |
> **Hinweis:** Event-Handler-Verknüpfungen werden als `UnresolvedReference` gespeichert und in der Resolution-Phase zur entsprechenden Methode in der zugehörigen `.pas`-Datei aufgelöst.
## Nicht abgebildete Konzepte (Phase 2+)
| Konzept | Grund |
|---|---|
| `with`-Statement | Kein eigener EdgeKind nötig; erschwert nur die Call-Qualifizierung |
| Class Helper | Kein eigener NodeKind; als `class` mit speziellem Bezug zum erweiterten Typ |
| `.dfm`/`.fmx` Komponenten | Kein Code im Pascal-Sinne; erfordert eigenen Parser |
| `exports`-Klauseln | `declExports` → könnte als `export` NodeKind abgebildet werden |
| Generics | `typerefTpl` → beeinflusst Typ-Referenzen, nicht den NodeKind |
+216 -1
View File
@@ -134,9 +134,224 @@ pascal: {
},
```
## Hinweise
## Hinweise zur Pascal-Integration
- `declProc` wird sowohl für `functionTypes` als auch `methodTypes` verwendet. CodeGraph unterscheidet anhand des AST-Kontexts (Parent ist `declClass` → method, sonst → function).
- Die `getVisibility`-Funktion traversiert aufwärts zum `declSection`-Knoten.
- Records werden als `class` behandelt, da `declClass` mit `kRecord` Kind-Knoten beides abdeckt.
---
## 5) DFM/FMX-Support: Custom Extractor
DFM/FMX-Dateien verwenden ein einfaches zeilenbasiertes Textformat (kein tree-sitter Parser vorhanden). Die Integration erfolgt analog zu `LiquidExtractor` und `SvelteExtractor` als **Custom Extractor**.
### Extension-Mapping ergänzen (`src/extraction/grammars.ts`)
```typescript
export const EXTENSION_MAP: Record<string, Language> = {
// ...bestehende Mappings...
'.pas': 'pascal',
'.dpr': 'pascal',
'.dpk': 'pascal',
'.lpr': 'pascal',
'.dfm': 'pascal', // ← NEU: DFM-Formulare
'.fmx': 'pascal', // ← NEU: FMX-Formulare
};
```
> **Hinweis:** DFM/FMX werden als Sprache `pascal` registriert. Die Unterscheidung zwischen tree-sitter-Parsing (`.pas`) und Custom Extractor (`.dfm`/`.fmx`) erfolgt in `extractFromSource()` anhand der Dateiendung.
### DEFAULT_CONFIG.include ergänzen (`src/types.ts`)
```typescript
include: [
// ...bestehende Patterns...
'**/*.pas',
'**/*.dpr',
'**/*.dpk',
'**/*.lpr',
'**/*.dfm', // ← NEU
'**/*.fmx', // ← NEU
],
```
### Routing in `extractFromSource()` (`src/extraction/tree-sitter.ts`)
```typescript
export function extractFromSource(
filePath: string,
source: string,
language?: Language
): ExtractionResult {
const detectedLanguage = language || detectLanguage(filePath);
// Use custom extractor for Svelte
if (detectedLanguage === 'svelte') {
const extractor = new SvelteExtractor(filePath, source);
return extractor.extract();
}
// Use custom extractor for Liquid
if (detectedLanguage === 'liquid') {
const extractor = new LiquidExtractor(filePath, source);
return extractor.extract();
}
// Use custom extractor for DFM/FMX form files ← NEU
if (detectedLanguage === 'pascal' &&
(filePath.endsWith('.dfm') || filePath.endsWith('.fmx'))) {
const extractor = new DfmExtractor(filePath, source);
return extractor.extract();
}
const extractor = new TreeSitterExtractor(filePath, source, detectedLanguage);
return extractor.extract();
}
```
### DfmExtractor Klasse (Coding Style analog LiquidExtractor)
```typescript
/**
* Custom extractor for Delphi DFM/FMX form files.
*
* DFM/FMX files describe the visual component hierarchy and event handler
* bindings. They use a simple text format (object/end blocks) that we parse
* with regex — no tree-sitter grammar exists for this format.
*
* Extracted information:
* - Components as NodeKind `component`
* - Nesting as EdgeKind `contains`
* - Event handlers (OnClick = MethodName) as UnresolvedReference → EdgeKind `references`
*/
export class DfmExtractor {
private filePath: string;
private source: string;
private nodes: Node[] = [];
private edges: Edge[] = [];
private unresolvedReferences: UnresolvedReference[] = [];
private errors: ExtractionError[] = [];
constructor(filePath: string, source: string) {
this.filePath = filePath;
this.source = source;
}
/**
* Extract components and event handler references from DFM/FMX source
*/
extract(): ExtractionResult {
const startTime = Date.now();
try {
const fileNode = this.createFileNode();
this.parseComponents(fileNode.id);
} catch (error) {
captureException(error, { operation: 'dfm-extraction', filePath: this.filePath });
this.errors.push({
message: `DFM extraction error: ${error instanceof Error ? error.message : String(error)}`,
severity: 'error',
});
}
return {
nodes: this.nodes,
edges: this.edges,
unresolvedReferences: this.unresolvedReferences,
errors: this.errors,
durationMs: Date.now() - startTime,
};
}
/** Create a file node for the DFM form file */
private createFileNode(): Node { /* ... analog LiquidExtractor ... */ }
/** Parse object/end blocks and extract components + event handlers */
private parseComponents(fileNodeId: string): void {
const lines = this.source.split('\n');
const stack: string[] = [fileNodeId]; // Stack der Parent-Node-IDs
const objectPattern = /^\s*(object|inherited|inline)\s+(\w+)\s*:\s*(\w+)/;
const eventPattern = /^\s*(On\w+)\s*=\s*(\w+)/;
const endPattern = /^\s*end\s*$/;
const multiLineStart = /=\s*\(\s*$/;
let inMultiLine = false;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const lineNum = i + 1;
// Mehrzeilige Properties überspringen
if (inMultiLine) {
if (line.trimEnd().endsWith(')')) inMultiLine = false;
continue;
}
if (multiLineStart.test(line)) {
inMultiLine = true;
continue;
}
// Component-Deklaration
const objMatch = line.match(objectPattern);
if (objMatch) {
const [, , name, typeName] = objMatch;
const nodeId = generateNodeId(this.filePath, 'component', name, lineNum);
this.nodes.push({
id: nodeId,
kind: 'component',
name,
qualifiedName: `${this.filePath}#${name}`,
filePath: this.filePath,
language: 'pascal',
startLine: lineNum,
endLine: lineNum, // wird beim zugehörigen 'end' aktualisiert
startColumn: 0,
endColumn: line.length,
metadata: { componentType: typeName },
updatedAt: Date.now(),
});
this.edges.push({
source: stack[stack.length - 1],
target: nodeId,
kind: 'contains',
});
stack.push(nodeId);
continue;
}
// Event-Handler
const eventMatch = line.match(eventPattern);
if (eventMatch) {
const [, , methodName] = eventMatch;
this.unresolvedReferences.push({
sourceNodeId: stack[stack.length - 1],
targetName: methodName,
kind: 'references',
filePath: this.filePath,
line: lineNum,
});
continue;
}
// Block-Ende
if (endPattern.test(line)) {
if (stack.length > 1) stack.pop();
}
}
}
}
```
### Coding-Style-Hinweise
Der `DfmExtractor` folgt dem bestehenden Coding Style der Custom Extractors:
- **JSDoc-Kommentare** (`/** */`) für jede öffentliche Methode und die Klasse selbst
- **Private Felder** für `nodes`, `edges`, `unresolvedReferences`, `errors`
- **`generateNodeId()`** für deterministische Node-IDs
- **`captureException()`** für Error-Tracking in catch-Blöcken
- **`ExtractionResult`** als Return-Type von `extract()`
- **`UnresolvedReference`** für Event-Handler (werden in der Resolution-Phase aufgelöst)
- **Kein tree-sitter** — rein Regex/zeilenbasiertes Parsing
+149
View File
@@ -0,0 +1,149 @@
# DFM/FMX Form-Dateien: Format-Referenz & Extraktions-Konzept
Delphi-Projekte bestehen nicht nur aus `.pas`-Dateien, sondern auch aus **Form-Dateien** (`.dfm` für VCL, `.fmx` für FireMonkey). Diese enthalten die visuelle Komponentenhierarchie und — besonders wertvoll für CodeGraph — die **Event-Handler-Verknüpfungen** zwischen UI-Komponenten und Pascal-Methoden.
## Warum DFM/FMX für CodeGraph relevant sind
| Information | Wert für CodeGraph |
|---|---|
| Komponenten-Deklarationen (`object Button1: TButton`) | NodeKind `component` — zeigt welche UI-Elemente existieren |
| Event-Handler (`OnClick = Button1Click`) | EdgeKind `references` — verknüpft UI mit Code in `.pas` |
| Komponenten-Hierarchie (verschachtelte `object`-Blöcke) | EdgeKind `contains` — zeigt Parent-Child-Beziehungen |
| Komponenten-Typ (`TButton`, `TPanel`, `TEdit`) | Metadata — Typ-Information für Impact-Analyse |
**Besonders wertvoll:** Wenn ein Entwickler eine Methode in der `.pas`-Datei umbenennt, zeigt CodeGraph sofort, dass ein DFM-Event-Handler darauf verweist → Impact-Analyse funktioniert über Dateigrenzen hinweg.
## DFM-Textformat
DFM-Dateien können in **Text** oder **Binär** gespeichert werden. Moderne Delphi-Projekte verwenden fast ausschließlich das Textformat (besser für Versionskontrolle). FMX-Dateien verwenden das gleiche Textformat.
### Grundstruktur
```
object Form1: TForm1
Left = 0
Top = 0
Caption = 'Hauptformular'
ClientHeight = 400
ClientWidth = 600
OnCreate = FormCreate
OnDestroy = FormDestroy
object Panel1: TPanel
Left = 0
Top = 0
Width = 600
Height = 50
Align = alTop
object Label1: TLabel
Left = 16
Top = 16
Caption = 'Willkommen'
end
object Button1: TButton
Left = 500
Top = 12
Caption = 'Login'
OnClick = Button1Click
end
end
object Memo1: TMemo
Left = 0
Top = 50
Width = 600
Height = 350
Align = alClient
end
end
```
### Syntax-Regeln
1. **Top-Level:** `object <Name>: <Typ>` — das Formular selbst
2. **Verschachtelte Komponenten:** `object <Name>: <Typ>` innerhalb eines anderen `object`-Blocks
3. **Properties:** `<Key> = <Value>` — einfache Zuweisung
4. **Event-Handler:** `On<Event> = <MethodenName>` — Verknüpfung zu Pascal-Methode
5. **Ende:** `end` schließt jeden `object`-Block
6. **Vererbung:** `inherited <Name>: <Typ>` statt `object` bei geerbten Formularen
7. **Inline-Objekte:** `inline <Name>: <Typ>` für inline erstellte Objekte
### Event-Handler erkennen
Event-Handler sind Properties, deren **Key mit `On` beginnt** und deren **Value ein Bezeichner** (kein String, keine Zahl) ist:
```
OnClick = Button1Click ← Event-Handler → references Button1Click
OnChange = EditChanged ← Event-Handler → references EditChanged
Caption = 'Nicht ein Event' ← Normales Property (String-Wert)
Left = 100 ← Normales Property (Zahl-Wert)
Align = alTop ← Normales Property (Enum-Wert)
```
### Mehrzeilige Properties
Einige Properties erstrecken sich über mehrere Zeilen:
```
SQL.Strings = (
'SELECT * FROM users'
'WHERE active = 1'
'ORDER BY name')
Items.Strings = (
'Option A'
'Option B')
```
Diese sind für CodeGraph weniger relevant, müssen aber beim Parsen korrekt übersprungen werden.
## Extraktions-Strategie: DfmExtractor
Analog zum `LiquidExtractor` und `SvelteExtractor` wird ein **`DfmExtractor`** als Custom Extractor implementiert — **ohne tree-sitter**, rein Regex/Zeilen-basiert.
### Zu extrahierende Nodes
| DFM-Element | CodeGraph NodeKind | Beispiel |
|---|---|---|
| Top-Level `object` | `component` | `Form1: TForm1` |
| Verschachtelte `object` | `component` | `Button1: TButton` |
| `inherited` | `component` | Geerbte Komponente |
### Zu extrahierende Edges
| DFM-Beziehung | CodeGraph EdgeKind | Beschreibung |
|---|---|---|
| Verschachtelung | `contains` | Panel1 enthält Button1 |
| Event-Handler | `references` | Button1 → `Button1Click` (Methode in .pas) |
| Datei enthält Komponente | `contains` | DFM-Datei → Form1 |
### Parsing-Algorithmus (Pseudocode)
```
für jede Zeile:
wenn "object <Name>: <Typ>" oder "inherited <Name>: <Typ>":
→ neuen component-Node erstellen
→ contains-Edge vom Parent
→ auf Stack pushen
wenn "end":
→ Stack poppen
wenn "<Key> = <Value>" und Key beginnt mit "On" und Value ist Bezeichner:
→ references-Edge: aktuelle Komponente → Value (Methodenname)
→ als UnresolvedReference speichern (wird später zur .pas-Methode aufgelöst)
wenn mehrzeiliges Property (endet mit "("):
→ Zeilen überspringen bis ")"
```
### Verknüpfung DFM ↔ PAS
Die Zuordnung DFM → PAS erfolgt über den **Dateinamen**:
- `MainForm.dfm` gehört zu `MainForm.pas` (gleicher Basename)
- Event-Handler `Button1Click` → Methode `TForm1.Button1Click` in der zugehörigen `.pas`-Datei
Diese Verknüpfung erfolgt in der **Resolution-Phase**, nicht beim Parsen.
## Kein tree-sitter nötig
Es gibt keinen tree-sitter Parser für DFM/FMX. Das Format ist aber so einfach strukturiert (zeilenbasiert, keine komplexe Grammatik), dass ein Regex-basierter Parser vollkommen ausreicht — genau wie bei Liquid-Templates.
## Phase
DFM/FMX-Support wird als **Phase 1b** eingeplant — parallel zum Pascal-Support, da die Verknüpfung DFM ↔ PAS den größten Mehrwert liefert, wenn beides gleichzeitig verfügbar ist.
+8 -7
View File
@@ -2,7 +2,7 @@
Dieses Verzeichnis enthält einen **umsetzbaren Bauplan** für Pascal/Delphi-Support in CodeGraph. Es basiert auf der tatsächlichen CodeGraph-Architektur und den verifizierten AST-Knotentypen von [`tree-sitter-pascal`](https://github.com/Isopod/tree-sitter-pascal).
**Ziel:** CodeGraph soll **Pascal/Delphi**-Dateien indexieren und daraus **Nodes** (Units, Klassen, Methoden, Properties, Enums, …) sowie **Edges** (uses/imports, calls, extends, implements, …) extrahieren.
**Ziel:** CodeGraph soll **Pascal/Delphi**-Dateien indexieren und daraus **Nodes** (Units, Klassen, Methoden, Properties, Enums, …) sowie **Edges** (uses/imports, calls, extends, implements, …) extrahieren. Zusätzlich werden **DFM/FMX-Formulardateien** unterstützt, um UI-Komponenten und Event-Handler-Verknüpfungen zu erfassen.
## Inhalt
@@ -13,8 +13,9 @@ Dieses Verzeichnis enthält einen **umsetzbaren Bauplan** für Pascal/Delphi-Sup
| `Docs/03-AST-Referenz.md` | Verifizierte AST-Knotentypen aus tree-sitter-pascal |
| `Docs/04-Checklist.md` | Umsetzungs-Checkliste mit bekannten Einschränkungen |
| `Docs/05-NodeKind-Mapping.md` | Explizite Zuordnung Delphi → CodeGraph NodeKind/EdgeKind |
| `Docs/06-Integration-Guide.md` | Konkrete Code-Diffs für `grammars.ts`, `types.ts`, `tree-sitter.ts` |
| `fixtures/` | Delphi-Beispieldateien zum Testen der Extraktion |
| `Docs/06-Integration-Guide.md` | Konkrete Code-Diffs für `grammars.ts`, `types.ts`, `tree-sitter.ts` + DfmExtractor |
| `Docs/07-DFM-FMX-Support.md` | DFM/FMX Form-Dateien: Format-Referenz & Extraktions-Konzept |
| `fixtures/` | Delphi-Beispieldateien zum Testen (`.pas`, `.dpr`, `.dfm`) |
| `resolution/` | Resolver-Heuristiken (Unit-Mapping, Call-Resolution) |
## Architektur-Übersicht
@@ -33,8 +34,8 @@ CodeGraph verwendet **keine** per-Sprache Plugin-Verzeichnisse. Die Integration
4. **Resolver erweitern** (mindestens `uses` → Unit-File, einfache Call-Auflösung)
5. **Fixtures in die Tests aufnehmen** und Assertions hinzufügen
## Grammar
## Grammar & Parsing
Verwendet wird: [`tree-sitter-pascal`](https://github.com/Isopod/tree-sitter-pascal) (npm: `tree-sitter-pascal`)
- Unterstützt: Delphi, FreePascal, Standard-Pascal
- Dateierweiterungen: `.pas`, `.dpr`, `.dpk`, `.lpr`
**Pascal-Dateien** (`.pas`, `.dpr`, `.dpk`, `.lpr`) werden mit [`tree-sitter-pascal`](https://github.com/Isopod/tree-sitter-pascal) geparst (AST-basierte Extraktion via `LanguageExtractor`).
**DFM/FMX-Dateien** (`.dfm`, `.fmx`) werden mit einem **Custom Extractor** (`DfmExtractor`) verarbeitet — analog zu `LiquidExtractor` und `SvelteExtractor`. Das DFM-Textformat ist zeilenbasiert und wird per Regex geparst. Besonders wertvoll: Event-Handler wie `OnClick = Button1Click` erzeugen `references`-Edges zu den zugehörigen Methoden in der `.pas`-Datei.
+92
View File
@@ -0,0 +1,92 @@
object frmMain: TfrmMain
Left = 0
Top = 0
Caption = 'CodeGraph DFM Fixture'
ClientHeight = 480
ClientWidth = 640
Color = clBtnFace
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -12
Font.Name = 'Segoe UI'
OnCreate = FormCreate
OnDestroy = FormDestroy
PixelsPerInch = 96
TextHeight = 15
object pnlTop: TPanel
Left = 0
Top = 0
Width = 640
Height = 50
Align = alTop
BevelOuter = bvNone
TabOrder = 0
object lblTitle: TLabel
Left = 16
Top = 16
Width = 200
Height = 15
Caption = 'Authentication Service'
end
object btnLogin: TButton
Left = 540
Top = 12
Width = 80
Height = 25
Caption = 'Login'
TabOrder = 0
OnClick = btnLoginClick
end
end
object pnlContent: TPanel
Left = 0
Top = 50
Width = 640
Height = 390
Align = alClient
BevelOuter = bvNone
TabOrder = 1
object edtUsername: TEdit
Left = 16
Top = 16
Width = 300
Height = 23
TabOrder = 0
TextHint = 'Username'
OnChange = edtUsernameChange
end
object edtPassword: TEdit
Left = 16
Top = 48
Width = 300
Height = 23
PasswordChar = '*'
TabOrder = 1
TextHint = 'Password'
OnKeyPress = edtPasswordKeyPress
end
object mmoLog: TMemo
Left = 16
Top = 88
Width = 608
Height = 280
ReadOnly = True
ScrollBars = ssVertical
TabOrder = 2
end
end
object pnlStatus: TStatusBar
Left = 0
Top = 440
Width = 640
Height = 40
Panels = <
item
Width = 200
end
item
Width = 200
end>
end
end