Inhaltsverzeichnis

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.

Bevor Sie starten

Stellen Sie sicher, dass Sie eine laufende WShell haben (Installationsanleitung). Für die Entwicklung brauchen Sie außerdem:

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:

  1. Willkommen — Neues FPC-Projekt, neues pas2js-Projekt oder bestehendes öffnen?
  2. Toolchain prüfen — Sind FPC, Lazarus und pas2js korrekt konfiguriert?
  3. Projekttyp wählen — Web App, Node.js CLI, VSCode/WvdS Extension, PWA, …
  4. Konfigurieren — Name, Speicherort, Build-Optionen
  5. 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).

Was der Wizard im Hintergrund erzeugt (für Fortgeschrittene)

Was der Wizard im Hintergrund erzeugt (für Fortgeschrittene)

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:

  1. Die WShell startet einen Node.js-Prozess für Ihre Extension
  2. Sie verbindet sich über eine Named Pipe (\\.\pipe\wvdx-<extensionId>)
  3. Der wvdx-host empfängt einen activate-Aufruf per JSON-RPC 2.0
  4. Ihre activate(context)-Funktion wird ausgeführt
  5. 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:

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:

2b. Das Layout im Designer aufbauen

Öffnen Sie PhonebookForm.wfm — der visuelle Designer startet automatisch. Er zeigt drei Bereiche:

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.

Die erzeugte PhonebookForm.wfm im Detail

Die erzeugte PhonebookForm.wfm im Detail

{
  "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:

  1. Erzeugt einen Handler-Namen (ContactListClick)
  2. Fügt die Deklaration in die .pas-Datei ein
  3. Fügt den Rumpf in die Implementation ein
  4. 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.

Generierter Code: Felder und AfterCreate

Generierter Code: Felder und AfterCreate

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.

Event-Handler: ContactListClick, SearchEditChange, SaveButtonClick

Event-Handler: ContactListClick, SearchEditChange, SaveButtonClick

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.

DataBridge-Abfrage: LoadContactsFromDB + HandleContactsLoaded

DataBridge-Abfrage: LoadContactsFromDB + HandleContactsLoaded

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:

  1. Ruft pas2js mit den richtigen Flags auf (-Tnodejs -Jc -Jirtl.js)
  2. Sammelt alle Unit-Pfade aus Ihrer Projektkonfiguration
  3. 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:

  1. Erstellt einen Release-Build (optimiert, ohne Debug-Symbole)
  2. Erzeugt das VSIX-Paket unter binaries/vsix/{name}-{version}.vsix
  3. 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.

Alternative: Eigenes Build-Skript für CI/CD oder Kommandozeile

Alternative: Eigenes Build-Skript für CI/CD oder Kommandozeile

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:

  1. Die WShell sammelt alle menus-Beiträge aus allen Extensions
  2. menuBar/file bedeutet: suche das bestehende Datei-Menü
  3. Einträge werden nach Gruppe (alphabetisch) und Reihenfolge (numerisch) sortiert
  4. 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:

  1. Durchlauf 1: menuBar → legt das neue Top-Level-Menü „Werkzeuge“ an
  2. 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:

  1. Ein Tab wird aktiv → die WShell fragt: „Welche Schnellbefehle hast du?“
  2. Der Tab liefert eine Liste von Buttons zurück
  3. Die WShell baut die QuickBar mit diesen Buttons neu auf
  4. 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:

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 (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:

  1. ML-KEM Encapsulate mit dem öffentlichen Schlüssel des Empfängers → gemeinsames Geheimnis + KEM-Chiffretext
  2. HKDF-SHA256 aus dem Geheimnis → AES-256-Schlüssel (32 Bytes)
  3. AES-GCM-Encrypt mit dem AES-Schlüssel → Nonce + Chiffretext + Authentifizierungs-Tag
  4. 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.

Vollständiges JavaScript-Beispiel: PQ-Verschlüsselung + Gateway-Aufruf

Vollständiges JavaScript-Beispiel: PQ-Verschlüsselung + Gateway-Aufruf

// 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


Zusammenfassung

Schritt Thema Werkzeug / Mechanismus
1 Projekt anlegen FPC Solution ManagerFPCSE: 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

WShell installieren | Fragen? | Zurück zum Blog