====== 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]]