====== Extensions für WShell: Vom leeren Ordner bis zur verschlüsselten WebAPI-Abfrage ====== WShell führt Extensions aus — dieselben, die auch in VSCode laufen. Dieser Artikel zeigt Schritt für Schritt, wie Sie Ihre erste Extension erstellen, in die WShell-Oberfläche integrieren, ein Master-Detail-Formular entwerfen und über verschlüsselte WebAPI-Aufrufe mit einem Backend kommunizieren. ~~READMORE~~ ===== Bevor Sie starten ===== Stellen Sie sicher, dass Sie eine laufende WShell haben ([[de:pub:blog:2026-03-wvdsshell-installation|Installationsanleitung]]). Für die Entwicklung brauchen Sie außerdem: * **Node.js 20 LTS** oder neuer — als Extension-Host für JavaScript-Extensions * **pas2js 3.3.1** — wenn Sie Pascal statt JavaScript schreiben möchten * **VSCode** mit zwei Extensions aus dem WvdS-Ökosystem: * **FPC Solution Manager** (''wvds.fpc-solution-manager'') — Projekt anlegen, bauen, ausführen * **pas2js Studio** (''wvds.fpc-pas2js-studio'') — visueller Formular-Designer, Code-Generator, Live-Vorschau Ohne diese beiden Extensions legen Sie Manifest, Compiler-Flags und Formularcode von Hand an — fehlerträchtig und bei jedem neuen Projekt dieselbe Arbeit. Der Solution Manager übernimmt die Projektstruktur: Er kennt die richtigen pas2js-Flags, erzeugt ein konsistentes Build-Skript und bindet das Projekt direkt in die VSCode-Toolchain ein. pas2js Studio ergänzt den visuellen Teil: Formulare entstehen per Drag & Drop statt durch manuelles JSON-Editieren, und der Code-Generator hält Felder, FindComponent-Aufrufe und Event-Bindungen automatisch synchron. Das Ergebnis ist ein reproduzierbarer Workflow, der Einarbeitungszeit verkürzt, Flüchtigkeitsfehler eliminiert und den Weg vom Prototyp zur fertigen Extension auf Minuten statt Stunden reduziert. ===== Schritt 1: Ein Extension-Projekt mit dem Solution Manager anlegen ===== Der **FPC Solution Manager** erzeugt die komplette Projektstruktur für Sie — Manifest, Einstiegspunkt, Build-Skript. Sie müssen nichts von Hand anlegen. ==== Den Projekt-Wizard starten ==== Öffnen Sie die Befehlspalette (''Ctrl+Shift+P'') und wählen Sie: FPCSE: Start Project Wizard Der Wizard führt Sie durch fünf Schritte: - **Willkommen** — Neues FPC-Projekt, neues pas2js-Projekt oder bestehendes öffnen? - **Toolchain prüfen** — Sind FPC, Lazarus und pas2js korrekt konfiguriert? - **Projekttyp wählen** — Web App, Node.js CLI, VSCode/WvdS Extension, PWA, ... - **Konfigurieren** — Name, Speicherort, Build-Optionen - **Zusammenfassung** — Übersicht der Dateien, die erzeugt werden Für eine WShell Extension wählen Sie den Typ **"VSCode Extension"** unter der Kategorie **pas2js Web**. ==== Was der Wizard erzeugt ==== Nach Abschluss haben Sie ein vollständiges Projekt: phonebook/ package.json <-- Manifest (vom Wizard ausgefüllt) src-pas/ extension.pas <-- Einstiegspunkt mit activate/deactivate dist/ extension.js <-- Wird beim ersten Build erzeugt build.ps1 <-- Build-Skript Der Solution Manager registriert das Projekt automatisch in der Projektbaumansicht. Von dort können Sie bauen (''Ctrl+Shift+B''), ausführen (''F5'') und Einstellungen ändern (''F4''). Jede Extension braucht mindestens eine ''package.json'' (Manifest) und einen Einstiegspunkt. Der Wizard erzeugt beides automatisch — hier ist, was dahinter steckt. **package.json — der Ausweis Ihrer Extension:** { "name": "phonebook", "displayName": "Telefonbuch", "version": "1.0.0", "publisher": "mycompany", "main": "./dist/extension.js", "activationEvents": [ "onStartupFinished" ], "contributes": { "commands": [ { "command": "phonebook.open", "title": "Telefonbuch öffnen", "category": "Phonebook" } ] } } ^ Feld ^ Pflicht ^ Bedeutung ^ | ''name'' | Ja | Kleingeschriebener Bezeichner (keine Leerzeichen) | | ''publisher'' | Ja | Ihre Herausgeber-ID | | ''version'' | Ja | Semantische Version (z.B. ''1.0.0'') | | ''main'' | Ja | Pfad zum kompilierten Einstiegspunkt | | ''activationEvents'' | Ja | Wann soll die WShell Ihre Extension laden? | ''onStartupFinished'' lässt die WShell zuerst fertig starten, bevor Ihre Extension geladen wird. **extension.pas — der Einstiegspunkt:** Die ''activate''-Funktion wird von der WShell aufgerufen. Im Hintergrund: - Die WShell startet einen **Node.js-Prozess** für Ihre Extension - Sie verbindet sich über eine **Named Pipe** (''\\.\pipe\wvdx-'') - Der ''wvdx-host'' empfängt einen ''activate''-Aufruf per JSON-RPC 2.0 - Ihre ''activate(context)''-Funktion wird ausgeführt - Was Sie zurückgeben, speichert die WShell in ihrer API-Registry **pas2js-Compiler-Flags** (vom Build-Skript automatisch gesetzt): ^ Flag ^ Zweck ^ | ''-Tnodejs'' | Zielplattform ist Node.js (nicht der Browser) | | ''-Jc'' | Alle Units in eine einzige Datei zusammenführen | | ''-Jirtl.js'' | Die pas2js-Laufzeitbibliothek einbinden | **Paketformate:** ^ Format ^ Aufbau ^ Verwendung ^ | ''.wvdx'' | Dateien direkt im ZIP-Wurzelverzeichnis | WShell nativ | | ''.vsix'' | Dateien unter ''extension/'' im ZIP | VSCode-kompatibel | Installationspfad: ''%LOCALAPPDATA%\.wvdx\extensions\'' ===== Schritt 2: Ein Master-Detail-Formular entwerfen (Telefonbuch) ===== Jetzt wird es praktisch. Wir entwerfen ein einfaches Telefonbuch mit zwei Bereichen: * **Master** (links): Kontaktliste * **Detail** (rechts): Name, Telefonnummer, E-Mail des ausgewählten Kontakts ==== 2a. Neues Formular anlegen ==== Öffnen Sie die Befehlspalette und wählen Sie: pas2js: New Web Form Geben Sie als Namen **PhonebookForm** ein. Das erzeugt zwei Dateien: * ''PhonebookForm.wfm'' — die Formularbeschreibung (JSON) * ''PhonebookForm.pas'' — die Pascal-Unit mit der Formularklasse ==== 2b. Das Layout im Designer aufbauen ==== Öffnen Sie ''PhonebookForm.wfm'' — der visuelle Designer startet automatisch. Er zeigt drei Bereiche: * **Links:** Komponentenpalette (Standard, Editors, Data, ...) * **Mitte:** Formular-Canvas (Drag & Drop) * **Rechts:** Eigenschafts-Inspektor Bauen Sie das Layout Schritt für Schritt auf: **Schritt 1 — Hauptcontainer:** Das Formular hat bereits ein leeres ''flexColumn''-Layout. Ändern Sie es auf ''flexRow'' (im Inspektor unter //Layout > Typ//). Das teilt das Formular in eine linke und rechte Hälfte. **Schritt 2 — Master-Panel:** Ziehen Sie ein ''TWPanel'' aus der Palette auf das Formular. Benennen Sie es ''MasterPanel''. Setzen Sie: ^ Eigenschaft ^ Wert ^ | Layout-Typ | ''flexColumn'' | | Breite | ''300'' (feste Breite in Pixel) | | Höhe | ''100%'' | **Schritt 3 — Suchfeld:** Ziehen Sie ein ''TWEdit'' in das ''MasterPanel''. Benennen Sie es ''SearchEdit''. Setzen Sie: ^ Eigenschaft ^ Wert ^ | Placeholder | ''Kontakt suchen...'' | | Breite | ''100%'' | | Margin | ''sm'' | **Schritt 4 — Kontaktliste:** Ziehen Sie eine ''TWListBox'' unter das Suchfeld. Benennen Sie sie ''ContactList''. Setzen Sie: ^ Eigenschaft ^ Wert ^ | Breite | ''100%'' | | FlexGrow | ''1'' (füllt den restlichen Platz) | | Margin | ''sm'' | **Schritt 5 — Detail-Panel:** Ziehen Sie ein zweites ''TWPanel'' rechts neben das ''MasterPanel''. Benennen Sie es ''DetailPanel''. Setzen Sie: ^ Eigenschaft ^ Wert ^ | Layout-Typ | ''flexColumn'' | | FlexGrow | ''1'' (füllt den gesamten Restplatz) | | Margin | ''md'' | **Schritt 6 — Detail-Felder:** Ziehen Sie in das ''DetailPanel'' drei ''TWEdit''-Felder untereinander: ^ Name ^ Placeholder ^ | ''NameEdit'' | ''Vorname Nachname'' | | ''PhoneEdit'' | ''+43 1 234 5678'' | | ''EmailEdit'' | ''name@example.com'' | **Schritt 7 — Speichern-Button:** Ziehen Sie einen ''TWButton'' unter die drei Felder. Benennen Sie ihn ''SaveButton'', Text: **"Speichern"**. ==== 2c. Die WFM-Datei im Überblick ==== Der Designer speichert alles als JSON. Sie können die WFM-Datei jederzeit auch direkt als JSON bearbeiten — der Designer und der Text-Editor bleiben synchron. { "version": "1.0.0", "form": { "name": "PhonebookForm", "className": "TPhonebookForm", "width": 800, "height": 500, "layoutKind": "flexRow", "children": [ { "name": "MasterPanel", "className": "TWPanel", "layout": { "type": "flexColumn", "width": "300", "height": "100%" }, "children": [ { "name": "SearchEdit", "className": "TWEdit", "layout": { "width": "100%", "margin": "sm" }, "properties": { "placeholder": "Kontakt suchen..." } }, { "name": "ContactList", "className": "TWListBox", "layout": { "width": "100%", "flexGrow": "1", "margin": "sm" } } ] }, { "name": "DetailPanel", "className": "TWPanel", "layout": { "type": "flexColumn", "flexGrow": "1", "margin": "md" }, "children": [ { "name": "NameEdit", "className": "TWEdit", "properties": { "placeholder": "Vorname Nachname" } }, { "name": "PhoneEdit", "className": "TWEdit", "properties": { "placeholder": "+43 1 234 5678" } }, { "name": "EmailEdit", "className": "TWEdit", "properties": { "placeholder": "name@example.com" } }, { "name": "SaveButton", "className": "TWButton", "properties": { "text": "Speichern" } } ] } ] }, "metadata": { "unitName": "PhonebookForm" } } ===== Schritt 3: Events binden ===== Das Formular sieht jetzt gut aus, aber es tut noch nichts. Jetzt verbinden wir Benutzeraktionen mit Pascal-Code. ==== 3a. Ein Event im Designer verdrahten ==== Klicken Sie im Designer auf ''ContactList''. Im rechten Inspektor wechseln Sie zum Tab **Events**. Dort sehen Sie: ^ Event ^ Handler ^ | ''OnClick'' | (leer) | | ''OnDblClick'' | (leer) | Doppelklicken Sie auf die leere Zeile neben **OnClick**. Der Designer: - Erzeugt einen Handler-Namen (''ContactListClick'') - Fügt die Deklaration in die ''.pas''-Datei ein - Fügt den Rumpf in die Implementation ein - Springt direkt zur neuen Prozedur Wiederholen Sie das für: ^ Komponente ^ Event ^ Erzeugter Handler ^ | ''SearchEdit'' | ''OnChange'' | ''SearchEditChange'' | | ''SaveButton'' | ''OnClick'' | ''SaveButtonClick'' | ==== 3b. Was der Code-Generator in der .pas-Datei macht ==== Der Designer pflegt automatisch zwei markierte Bereiche in Ihrer Unit: **Felder** (zwischen ''{ WFM-AUTO-BEGIN }'' und ''{ WFM-AUTO-END }'') sowie **FindComponent-Aufrufe und Event-Bindungen** in ''AfterCreate''. Alles **außerhalb** dieser Marker bleibt bei jeder Regenerierung erhalten — Sie können beliebig eigenen Code ergänzen, ohne dass der Designer ihn überschreibt. **Felder:** private { WFM-AUTO-BEGIN } FMasterPanel: TWPanel; FSearchEdit: TWEdit; FContactList: TWListBox; FDetailPanel: TWPanel; FNameEdit: TWEdit; FPhoneEdit: TWEdit; FEmailEdit: TWEdit; FSaveButton: TWButton; { WFM-AUTO-END } **FindComponent-Aufrufe und Event-Bindungen:** procedure TPhonebookForm.AfterCreate; begin inherited; { WFM-AUTO-BEGIN } FMasterPanel := TWPanel(FindComponent('MasterPanel')); FSearchEdit := TWEdit(FindComponent('SearchEdit')); FContactList := TWListBox(FindComponent('ContactList')); FDetailPanel := TWPanel(FindComponent('DetailPanel')); FNameEdit := TWEdit(FindComponent('NameEdit')); FPhoneEdit := TWEdit(FindComponent('PhoneEdit')); FEmailEdit := TWEdit(FindComponent('EmailEdit')); FSaveButton := TWButton(FindComponent('SaveButton')); { WFM-AUTO-END } { WFM-EVENT-BEGIN } FContactList.OnClick := @ContactListClick; FSearchEdit.OnChange := @SearchEditChange; FSaveButton.OnClick := @SaveButtonClick; { WFM-EVENT-END } end; ==== 3c. Die Event-Handler implementieren ==== Jetzt füllen Sie die leeren Handler mit Logik. ''ContactListClick'' zeigt die Detail-Felder des ausgewählten Kontakts, ''SearchEditChange'' filtert die Liste live, und ''SaveButtonClick'' schreibt Änderungen zurück ins Array. procedure TPhonebookForm.ContactListClick(Sender: TWControl); var LIndex: Integer; begin LIndex := FContactList.ItemIndex; if LIndex < 0 then Exit; // Detail-Felder aus der Datenquelle füllen FNameEdit.Text := FContacts[LIndex].Name; FPhoneEdit.Text := FContacts[LIndex].Phone; FEmailEdit.Text := FContacts[LIndex].Email; end; procedure TPhonebookForm.SearchEditChange(Sender: TWControl); var LFilter: string; begin LFilter := LowerCase(FSearchEdit.Text); FContactList.Items.Clear; for var I := 0 to High(FContacts) do if (LFilter = '') or (Pos(LFilter, LowerCase(FContacts[I].Name)) > 0) then FContactList.Items.Add(FContacts[I].Name); end; procedure TPhonebookForm.SaveButtonClick(Sender: TWControl); begin if FContactList.ItemIndex < 0 then Exit; FContacts[FContactList.ItemIndex].Name := FNameEdit.Text; FContacts[FContactList.ItemIndex].Phone := FPhoneEdit.Text; FContacts[FContactList.ItemIndex].Email := FEmailEdit.Text; end; ===== Schritt 4: Daten an das Formular binden ===== Das Telefonbuch braucht Kontaktdaten. Diese kommen aus dem Shell DataBridge (Schritt 8) oder — zum Testen — aus einem lokalen Array. ==== 4a. Ein Datenmodell definieren ==== Ergänzen Sie in der Unit ein Record und ein Array: type TContact = record Name: string; Phone: string; Email: string; end; var FContacts: array of TContact; ==== 4b. Kontakte beim Formular-Start laden ==== Fügen Sie eine Lademethode hinzu und rufen Sie sie in ''AfterCreate'' auf (nach den WFM-Markern): procedure TPhonebookForm.LoadContacts; begin SetLength(FContacts, 3); FContacts[0].Name := 'Anna Huber'; FContacts[0].Phone := '+43 1 234 5678'; FContacts[0].Email := 'anna@example.com'; FContacts[1].Name := 'Max Bauer'; FContacts[1].Phone := '+43 1 876 5432'; FContacts[1].Email := 'max@example.com'; FContacts[2].Name := 'Lisa Gruber'; FContacts[2].Phone := '+43 1 555 0000'; FContacts[2].Email := 'lisa@example.com'; FContactList.Items.Clear; for var I := 0 to High(FContacts) do FContactList.Items.Add(FContacts[I].Name); end; ==== 4c. Daten stattdessen aus der Datenbank laden ==== Wenn die Extension in WShell läuft und ein Daten-Provider konfiguriert ist, ersetzen Sie ''LoadContacts'' durch eine DataBridge-Abfrage. Der DataBridge kümmert sich um die Datenbankverbindung — Ihre Extension sendet nur die SQL-Abfrage und empfängt ein JSON-Ergebnis. procedure TPhonebookForm.LoadContactsFromDB; begin DataBridge.Query('main', 'SELECT name, phone, email FROM contacts ORDER BY name', nil, @HandleContactsLoaded); end; procedure TPhonebookForm.HandleContactsLoaded(AResult: TJSObject); var LRows: TJSArray; LRow: TJSObject; begin LRows := TJSArray(AResult.Properties['rows']); SetLength(FContacts, LRows.Length); for var I := 0 to LRows.Length - 1 do begin LRow := TJSObject(LRows[I]); FContacts[I].Name := string(LRow.Properties['name']); FContacts[I].Phone := string(LRow.Properties['phone']); FContacts[I].Email := string(LRow.Properties['email']); end; FContactList.Items.Clear; for var I := 0 to High(FContacts) do FContactList.Items.Add(FContacts[I].Name); end; ===== Schritt 5: Bauen, testen, verpacken ===== ==== 5a. Im Solution Manager bauen ==== Drücken Sie ''Ctrl+Shift+B'' oder klicken Sie im Projektbaum auf //Build//. Der Solution Manager: - Ruft pas2js mit den richtigen Flags auf (''-Tnodejs -Jc -Jirtl.js'') - Sammelt alle Unit-Pfade aus Ihrer Projektkonfiguration - Zeigt Fehler, Warnungen und Hinweise im Diagnose-Panel Ziel: **0 Fehler, 0 Warnungen, 0 Hinweise.** ==== 5b. In der WShell testen ==== Klicken Sie im Projektbaum mit der rechten Maustaste auf Ihr Projekt und wählen Sie **Extension Build → Install Extension**. Der Solution Manager baut die Extension, erzeugt das VSIX-Paket und installiert es direkt in die lokale WShell-Instanz. Nach einem Neustart der WShell erscheint Ihr Telefonbuch als neuer Tab. ==== 5c. Als VSIX verpacken ==== Rechtsklick auf das Projekt → **Extension Build → Package VSIX**. Der Solution Manager: - Erstellt einen Release-Build (optimiert, ohne Debug-Symbole) - Erzeugt das VSIX-Paket unter ''binaries/vsix/{name}-{version}.vsix'' - Zeigt das Ergebnis im Output-Panel Die fertige ''.vsix''-Datei können Sie an andere Entwickler weitergeben oder über den Extension-Manager installieren. Zum Veröffentlichen im Marketplace verwenden Sie **Extension Build → Publish Extension**. Falls Sie den Solution Manager nicht verwenden oder den Build in eine CI/CD-Pipeline einbinden möchten, können Sie eine eigene ''build.ps1'' schreiben. Das folgende Unter-Howto zeigt Ihnen den Aufbau Schritt für Schritt. **Warum ein eigenes Skript?** Der Solution Manager steuert den gesamten Build über seine UI. Wenn Sie den Build jedoch **automatisieren** möchten — etwa in einer Jenkins-Pipeline, einem GitHub-Actions-Workflow oder einem Nightly-Build — brauchen Sie ein Skript, das ohne grafische Oberfläche funktioniert. Ein PowerShell-Skript gibt Ihnen außerdem volle Kontrolle über Flags, Pfade und Nachverarbeitung. **Schritt 1 — Grundgerüst anlegen** Erstellen Sie im Projektverzeichnis eine Datei ''build.ps1''. Das Skript nimmt **Switch-Parameter** entgegen, damit Sie den Build-Modus über die Kommandozeile steuern können: [CmdletBinding()] param( [switch]$Debug, # Debug-Build mit Source-Maps (Standard) [switch]$Release, # Optimierter Build ohne Debug-Symbole [switch]$Clean, # dist/ vor dem Build löschen [switch]$Vsix, # Nach dem Build ein VSIX-Paket erzeugen [switch]$Watch, # Dateiänderungen überwachen, inkrementell bauen [switch]$Test, # Testsuite ausführen [switch]$Log # Build-Ausgabe in Logdatei schreiben ) Die Parameter sind **kombinierbar**: ''-Release -Vsix'' baut optimiert und verpackt in einem Schritt. Ohne Flag greift ''-Debug'' als Vorgabe — so ist der häufigste Aufruf zugleich der kürzeste. **Schritt 2 — pas2js-Kompilierung einbinden** Im Kern ruft das Skript den pas2js-Compiler auf. Die Flags unterscheiden sich je nach Build-Modus: $Compiler = "C:\Lazarus\fpc\3.3.1\bin\pas2js.exe" $Source = "src-pas/extension.pas" $Output = "dist/extension.js" if ($Clean -and (Test-Path "dist")) { Remove-Item "dist" -Recurse -Force } $Args = @("-Tnodejs", "-Jc", "-Jirtl.js") if ($Release) { $Args += "-O2" # Optimierungsstufe 2 } else { $Args += "-Jm" # Source-Maps für Fehlersuche } $Args += "-o$Output" $Args += $Source & $Compiler @Args if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } **Begründung:** ''-Jm'' erzeugt Source-Maps, die im Browser-Debugger oder in der WShell-DevConsole Pascal-Zeilennummern statt JavaScript-Offsets zeigen. Im Release-Build werden sie weggelassen, weil sie das Paket unnötig vergrößern. **Schritt 3 — VSIX-Paket erzeugen** Für das Verpacken benötigen Sie ''@vscode/vsce''. Das Skript sucht das Tool zuerst global, dann lokal: if ($Vsix) { $vsce = Get-Command vsce -ErrorAction SilentlyContinue if (-not $vsce) { $vsce = "node_modules/.bin/vsce" if (-not (Test-Path $vsce)) { Write-Error "vsce nicht gefunden. Installieren: npm install -g @vscode/vsce" exit 1 } } & $vsce package --out "binaries/vsix/" if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } } ''vsce package'' liest die ''package.json'', sammelt alle unter ''files'' aufgeführten Ressourcen ein und erzeugt eine ''.vsix''-Datei unter ''binaries/vsix/{name}-{version}.vsix''. Falls ''vsce'' fehlt, installieren Sie es einmalig: npm install -g @vscode/vsce **Schritt 4 — Lokal installieren und testen** Die fertige ''.vsix''-Datei können Sie direkt über die Kommandozeile installieren: # In VSCode: code --install-extension binaries/vsix/phonebook-1.0.0.vsix # In WShell: # Kopieren Sie den Ausgabeordner nach %LOCALAPPDATA%\.wvdx\extensions\ # und starten Sie die WShell neu. **Typische Aufrufe im Überblick** ^ Szenario ^ Aufruf ^ | Schneller Debug-Build | ''powershell -File build.ps1'' | | Release ohne Paket | ''powershell -File build.ps1 -Release'' | | Release + VSIX | ''powershell -File build.ps1 -Release -Vsix'' | | Sauberer Neuaufbau | ''powershell -File build.ps1 -Clean -Release -Vsix'' | | Entwicklung mit Watch | ''powershell -File build.ps1 -Watch'' | **Hinweis:** Setzen Sie ''-ExecutionPolicy Bypass'' voran, falls die PowerShell-Ausführungsrichtlinie signierte Skripte verlangt. ===== Schritt 6: Menüs in die WShell einbinden ===== Ab hier geht es um die Integration Ihrer fertigen Extension in die WShell-Oberfläche. Die WShell hat eine Menüleiste (Datei, Bearbeiten, Ansicht, ...). Ihre Extension kann Einträge zu bestehenden Menüs hinzufügen oder komplett neue Menüs anlegen. ==== Einträge zu einem bestehenden Menü hinzufügen ==== In der ''package.json'' tragen Sie unter ''contributes.menus'' ein, wo Ihr Befehl erscheinen soll. Der **Schlüssel** ist der Menü-Ort, der **Wert** ist eine Liste von Einträgen. { "contributes": { "commands": [ { "command": "myext.export", "title": "Daten exportieren", "category": "Meine Extension" } ], "menus": { "menuBar/file": [ { "command": "myext.export", "group": "export", "order": 1 } ] } } } Dieser Eintrag fügt "Daten exportieren" ins **Datei-Menü** ein. So funktioniert das Zusammenführen: - Die WShell sammelt alle ''menus''-Beiträge aus allen Extensions - ''menuBar/file'' bedeutet: suche das bestehende Datei-Menü - Einträge werden nach **Gruppe** (alphabetisch) und **Reihenfolge** (numerisch) sortiert - Trennlinien zwischen verschiedenen Gruppen entstehen automatisch ==== Ein neues Top-Level-Menü anlegen ==== Um ein eigenes Menü (z.B. "Werkzeuge") hinzuzufügen, verwenden Sie **Untermenüs**: { "contributes": { "submenus": [ { "id": "myext.tools", "label": "Werkzeuge" } ], "menus": { "menuBar": [ { "submenu": "myext.tools", "order": 50 } ], "myext.tools": [ { "command": "myext.export", "group": "io", "order": 1 }, { "command": "myext.import", "group": "io", "order": 2 } ] } } } Die WShell verarbeitet das in zwei Durchläufen: - **Durchlauf 1:** ''menuBar'' → legt das neue Top-Level-Menü "Werkzeuge" an - **Durchlauf 2:** ''myext.tools'' → füllt es mit Ihren Befehlen ==== Wo können Menü-Einträge erscheinen? ==== ^ Ort ^ Wo er erscheint ^ | ''menuBar'' | Neues Top-Level-Menü | | ''menuBar/file'' | Im Datei-Menü | | ''commandPalette'' | In der Befehlspalette (Ctrl+Shift+P) | | ''view/title'' | Als Aktionsbutton in der Kopfzeile einer Ansicht | | ''view/item/context'' | Im Rechtsklick-Menü auf Baumeinträge | ===== Schritt 7: QuickBar-Buttons in die Titelleiste einbinden ===== Die **QuickBar** ist die Reihe von Symbolbuttons in der Titelleiste der WShell — Schnellzugriffe, die immer sichtbar sind. ==== Wie QuickBar-Buttons funktionieren ==== Im Gegensatz zu Menüs werden QuickBar-Buttons nicht über die ''package.json'' definiert, sondern **zur Laufzeit** bereitgestellt. Der Ablauf: - Ein Tab wird aktiv → die WShell fragt: "Welche Schnellbefehle hast du?" - Der Tab liefert eine Liste von Buttons zurück - Die WShell baut die QuickBar mit diesen Buttons neu auf - Wird ein anderer Tab aktiv, werden die Buttons entfernt und durch neue ersetzt ==== Buttons in Pascal definieren ==== function TMyTab.GetQuickCommands: TWvdSQuickCommands; begin SetLength(Result, 2); Result[0].CommandID := 'myext.save'; Result[0].ImageIndex := 0; Result[0].Hint := 'Speichern'; Result[0].Enabled := True; Result[0].Visible := True; Result[0].OnExecute := @HandleSave; Result[1].CommandID := 'myext.refresh'; Result[1].ImageIndex := 1; Result[1].Hint := 'Aktualisieren'; Result[1].Enabled := True; Result[1].Visible := True; Result[1].OnExecute := @HandleRefresh; end; ==== Zwei Bereiche in der QuickBar ==== Die QuickBar zeigt zwei Gruppen, getrennt durch eine sichtbare Linie: ^ Seite ^ Quelle ^ Beispiel ^ | Links | Editor-Aktionen | Speichern, Rückgängig | | Rechts | Globale Schnellbefehle | Aktualisieren, Einstellungen | Wenn mehr Buttons vorhanden sind als Platz in der Titelleiste, erscheint automatisch ein **"..." Überlauf-Menü**. ===== Schritt 8: WShell-Datendienste für WebAPI-Abfragen nutzen ===== Die WShell stellt einen **DataBridge** bereit. Damit kann Ihre Extension Datenbanken abfragen oder HTTP-Endpunkte ansprechen — ohne eigenen Low-Level-Code. ==== So funktioniert der Datenfluss ==== Ihre Extension sendet eine Nachricht an die WShell. Die WShell leitet alles, was mit ''data.'' beginnt, an den DataBridge weiter: Ihre Extension --> postMessage({command: 'data.query', ...}) --> SystemHandler --> DataBridge --> Datenbank-Provider --> JSON-Ergebnis zurück an Ihre Extension ==== Verfügbare Datenbefehle ==== ^ Befehl ^ Zweck ^ Gibt zurück ^ | ''data.query'' | SELECT-Abfrage | Spalten und Zeilen | | ''data.execute'' | INSERT/UPDATE/DELETE | Anzahl betroffener Zeilen | | ''data.applyDelta'' | Massenänderungen | Anzahl betroffener Zeilen | | ''data.getSchema'' | Tabellenstruktur | Spaltenname, Typ, Nullable | | ''data.getTables'' | Tabellen auflisten | Liste der Tabellennamen | | ''data.ping'' | Verbindung testen | ok: true/false | ==== Beispiel: Daten aus der Extension abfragen ==== // In Ihrer Extension: // 1. Abfrage an die WShell senden window.wvdxHost.postMessage({ type: 'request', command: 'data.query', id: 1, params: { provider: 'main', query: 'SELECT id, name, email FROM customers WHERE active = :active', parameters: { active: true } } }); // 2. Ergebnis empfangen window.addEventListener('message', function(event) { var msg = event.data; if (msg.id === 1 && msg.result) { console.log('Spalten:', msg.result.columns); console.log('Zeilen:', msg.result.rows); } }); ==== Den Daten-Provider konfigurieren ==== In der ''settings.json'' der WShell definieren Sie Ihren Provider: { "data.providers": { "main": { "type": "sqldb", "connection": "Host=localhost;Database=mydb;User=app;Password=..." } } } Wichtig zur Sicherheit: * Alle Abfragen nutzen **parametrisierte Statements** — kein SQL-Injection möglich * Verbindungszeichenfolgen werden **nie** an die Extension weitergegeben * SQL-Fehlermeldungen werden **bereinigt**, bevor sie zurückgesendet werden ===== Schritt 9: Payloads mit Post-Quantum-Kryptographie verschlüsseln ===== Die WShell enthält **Post-Quantum-Kryptographie** (PQC) auf Basis von OpenSSL 3.6. Damit kann Ihre Extension Daten verschlüsseln, bevor sie an einen WebAPI-Endpunkt gesendet werden — zum Beispiel den **WvdS Gateway Service**. ==== Warum Post-Quantum? ==== Herkömmliche Verschlüsselung (RSA, ECDH) wird durch Quantencomputer gebrochen werden. Die neuen NIST-Standards sind gegen Quantenangriffe resistent ([[de:pub:blog:2026-03-pq-crypto|mehr dazu]]): ^ Algorithmus ^ Standard ^ Zweck ^ | **ML-KEM** (Kyber) | FIPS 203 | Schlüsselaustausch | | **ML-DSA** (Dilithium) | FIPS 204 | Digitale Signaturen | | **AES-256-GCM** | — | Symmetrische Verschlüsselung (schnell, für Nutzdaten) | | **HKDF-SHA256** | — | Schlüsselableitung | ==== So verschlüsselt die WShell Ihren Payload ==== Wenn Ihre Extension ''crypto.encrypt'' aufruft, läuft intern ein vierstufiger Prozess: - **ML-KEM Encapsulate** mit dem öffentlichen Schlüssel des Empfängers → gemeinsames Geheimnis + KEM-Chiffretext - **HKDF-SHA256** aus dem Geheimnis → AES-256-Schlüssel (32 Bytes) - **AES-GCM-Encrypt** mit dem AES-Schlüssel → Nonce + Chiffretext + Authentifizierungs-Tag - **Zusammenpacken** → ''{ kemCiphertext, aesPayload }'' Der Empfänger (z.B. Gateway) kehrt den Prozess mit seinem privaten ML-KEM-Schlüssel um. ==== Verfügbare Krypto-Befehle ==== ^ Befehl ^ Zweck ^ | ''crypto.isAvailable'' | Prüfen, ob OpenSSL 3.6 verfügbar ist | | ''crypto.encrypt'' | Payload PQ-verschlüsseln (ML-KEM + AES-GCM) | | ''crypto.decrypt'' | PQ-verschlüsselten Payload entschlüsseln | | ''crypto.sign'' | ML-DSA digitale Signatur erstellen | | ''crypto.verify'' | ML-DSA Signatur prüfen | | ''crypto.generateKeyPair'' | ML-KEM oder ML-DSA Schlüsselpaar erzeugen | | ''crypto.keyStore.generate'' | Schlüsselpaar erzeugen und speichern | | ''crypto.keyStore.list'' | Gespeicherte Schlüssel auflisten | | ''crypto.keyStore.rotate'' | Schlüssel rotieren (neuer Schlüssel, alter bleibt) | ==== Beispiel: Einen Payload für den Gateway verschlüsseln ==== Der Ablauf in vier Schritten: PQC-Verfügbarkeit prüfen, Schlüsselpaar erzeugen, Payload verschlüsseln, verschlüsseltes Ergebnis per HTTPS an den Gateway senden. // 1. Prüfen, ob PQC verfügbar ist window.wvdxHost.postMessage({ type: 'request', command: 'crypto.isAvailable', id: 100 }); // 2. Schlüsselpaar im KeyStore erzeugen window.wvdxHost.postMessage({ type: 'request', command: 'crypto.keyStore.generate', id: 101, params: { keyId: 'gateway-session', algorithm: 'ML-KEM-768' } }); // 3. Ihren API-Payload verschlüsseln var payload = JSON.stringify({ endpoint: '/api/customers', method: 'POST', body: { name: 'Acme GmbH', country: 'AT' } }); window.wvdxHost.postMessage({ type: 'request', command: 'crypto.encrypt', id: 102, params: { recipientPublicKey: gatewayPublicKeyBase64, plaintext: btoa(payload), aad: btoa('my-extension-v1') } }); // 4. Verschlüsseltes Ergebnis an den Gateway senden window.addEventListener('message', function(event) { var msg = event.data; if (msg.id === 102 && msg.result) { fetch('https://gateway.example.com/api/customers', { method: 'POST', headers: { 'Content-Type': 'application/octet-stream', 'X-Payload-Encrypted': 'true', 'X-Session-Id': sessionId }, body: msg.result.encrypted }); } }); ==== Der Gateway Service als Endpunkt ==== Der **WvdS Gateway Service** ist ein Backend, das PQ-verschlüsselte Payloads nativ versteht. Er sitzt zwischen Ihrer Extension und dem eigentlichen API: Extension --> PQ-Encrypt(Payload) --> HTTPS POST an Gateway --> Gateway-Middleware entschlüsselt --> Weiterleitung an Backend-API --> Antwort wird zurück verschlüsselt --> Extension entschlüsselt Ergebnis Der Gateway unterstützt drei Verschlüsselungsmodi: ^ Modus ^ Wann verwendet ^ Schlüsselquelle ^ | **Session-basiert** | ''X-Session-Id'' Header vorhanden | ML-KEM-abgeleiteter Sitzungsschlüssel | | **Statisch** | Kein Session-Header | HKDF aus MasterSecret | | **Durchleitung** | Verschlüsselung deaktiviert | Keine Verschlüsselung | ==== Sicherheitsgarantien ==== * **Private Schlüssel verlassen nie die WShell** — Ihre Extension erhält nur öffentliche Schlüssel * **Schlüsselmaterial wird aus dem Speicher gelöscht** nach Verwendung * **OpenSSL-Fehlermeldungen werden bereinigt** — keine internen Details gelangen zur Extension * **Maximale Payload-Größe:** 10 MB (Schutz gegen Denial-of-Service) ---- ===== Zusammenfassung ===== ^ Schritt ^ Thema ^ Werkzeug / Mechanismus ^ | 1 | Projekt anlegen | **FPC Solution Manager** → ''FPCSE: Start Project Wizard'' | | 2 | Formular-Layout entwerfen | **pas2js Studio** → WFM-Designer (Drag & Drop) | | 3 | Events binden | Designer → Doppelklick auf Event → Handler wird erzeugt | | 4 | Daten binden | ''TContact''-Record lokal oder per DataBridge aus DB | | 5 | Bauen und testen | Solution Manager → Rechtsklick → **Extension Build** (Build / Install / Package VSIX) | | 6 | Menü-Integration | ''contributes.menus'' mit Ort-Schlüsseln (''menuBar/file'' etc.) | | 7 | QuickBar-Buttons | ''IWvdSShellContribution.GetQuickCommands()'' zur Laufzeit | | 8 | Datenabfragen | ''data.query'' per postMessage an den DataBridge | | 9 | PQ-Verschlüsselung | ''crypto.encrypt'' (ML-KEM + AES-256-GCM) über CryptoBridge + Gateway | [[de:pub:blog:2026-03-wvdsshell-installation|WShell installieren]] | [[de:pub:kontakt|Fragen?]] | [[de:pub:blog:start|Zurück zum Blog]]