Vom Magazinthema zur Projektpraxis
Passende Leistungs- und Technikseiten zum Beitrag
„Wir müssen nur kurz ein Web-UI einbetten – Login, ein Report, ein kleines internes Portal.“ Genau an dieser Stelle kippt Delphi WebView2 FMX in echten Windows-Clients regelmäßig: nicht beim Rendern der ersten URL, sondern beim Lifecycle (asynchron, COM-basiert), beim Schließen der Form, bei Downloads ohne Kontrolle und bei einer JS↔Delphi-Kommunikation, die im Supportfall nachvollziehbar bleiben muss.
Konstruierte, aber typische Situation aus Modernisierungen: Ein gewachsener FMX-Client (Windows) soll einen OAuth/OIDC-Login-Flow und danach einen React-basierten „Admin“-Bereich anzeigen. Parallel existieren Legacy-Workflows (Druck, Dateiexport, Gerätezugriff), die aus dem Web-Frontend angestoßen werden sollen. Die erste Iteration funktioniert am Entwicklerrechner. Zwei Wochen später: sporadische AVs beim Schließen, „Downloads verschwinden“, und der Support kann den Fehler nicht reproduzieren, weil Cache/Storage je Maschine anders sind.
Dieser Source-Schnipsel ist als Entscheidungspfad aufgebaut: Sie wählen bewusst, wie tief Sie WebView2 in Ihre FMX-App integrieren. Im Kern steht ein gekapselter Host mit State-Machine, definierter UserDataFolder-Strategie, einer JSON-Bridge über WebMessage und einem Download-Hook, der auch im Betrieb nicht überrascht.
Entscheidungspfad: Wie „viel“ Delphi WebView2 FMX brauchen Sie wirklich?
Bevor Code entsteht, lohnt eine klare Entscheidung. WebView2 kann von „eingebettete Ansicht“ bis „vollwertige Integrationsschicht“ reichen.
- Nur Anzeige (Read-only): Sie navigieren zu einer URL und brauchen kaum Interaktion. Dann reicht oft eine minimale Initialisierung, aber Sie sollten trotzdem ein stabiles UserDataFolder setzen und den Dispose sauber halten.
- Interaktion (Commands, Datenübergabe): Web-UI soll Delphi-Workflows anstoßen. Dann brauchen Sie eine Bridge mit Vertrag (Nachrichtenschema, Whitelist, Korrelation) und eine Entkopplung zur Business-Logik (Layer: UI → Application → Domain → Infrastruktur).
- Betriebsrelevante Features (Downloads, Debugging, Support): Sobald Dateien, DevTools, Remote-Debug oder Support-Reproduktion wichtig werden, müssen Sie Events, Pfade, Policies und Logging so designen, dass sie im Feld tragfähig sind.
| Entscheidung | Wenn Sie das ignorieren, passiert meist… | Minimum-Maßnahme |
|---|---|---|
| UserDataFolder nicht gesetzt | Rechteprobleme, nicht reproduzierbare Zustände, Support kann Bugs nicht nachstellen | Pfad pro Windows-User außerhalb des Installationsverzeichnisses |
| Keine State-Machine/Dispose-Logik | AVs bei Form-Close, Handler feuern nach Destroy | Disposed-Flag + Tokens unhooken + nil-Reihenfolge |
| ExecuteScript als „Bridge“ | Unversionierbare String-Skripte, schweres Error-Handling, „irgendwo“ Nebenwirkungen | WebMessage mit JSON-Vertrag und Whitelist |
| Downloads nicht abgefangen | Unkontrollierte Ablage, „hängende“ Dialoge, Compliance-Fragen | DownloadStarting hooken, optional handled + eigener Pfad |
WebView2 in FMX: warum es kein „Component drop“ ist
WebView2 ist eine COM-nahe API (Component Object Model: Referenzzählung, Interfaces, Events) mit asynchroner Initialisierung. FMX (FireMonkey) abstrahiert zwar Window-Handles, aber WebView2 will am Ende ein Parent-HWND und klare Bounds in Pixeln. Dazu kommt: Ereignisse treffen nicht automatisch in einem Kontext ein, in dem Sie FMX-Controls sicher anfassen dürfen.
Zwei zentrale, belastbare Anker sind hier wichtig: Erstens die offizielle WebView2-Architektur als Laufzeitkomponente (Edge Runtime + SDK), inklusive des User Data Folder-Konzepts, das Microsoft als Persistenz- und Profilablage beschreibt (Microsoft Learn: WebView2). Zweitens das Event- und Messaging-Modell (WebMessageReceived, PostWebMessageAsString), das als definierter Kanal für App↔Web-Kommunikation vorgesehen ist (Microsoft Learn: Communication between host and web content).
Source-Schnipsel: Host mit State-Machine, Token-Unhook und JSON-Bridge
Der folgende Code ist bewusst nicht „kürzestmöglich“, sondern auf Betriebssicherheit getrimmt. Ungewöhnlicher, aber in der Praxis entscheidender Punkt: EventRegistrationToken speichern und beim Dispose explizit remove_* aufrufen. Das reduziert die Klassiker „Callback nach Destroy“ deutlich.
Hinweis: Je nach Delphi-Setup nutzen Sie ein Import der WebView2-TLB oder eine Wrapper-Unit. Die Signaturen sind sehr ähnlich, aber einzelne Typnamen können variieren.
unit NbWebView2Host;
interface
uses
System.SysUtils, System.Classes, System.IOUtils, System.JSON,
Winapi.Windows, Winapi.ActiveX,
FMX.Types,
WebView2, WebView2_TLB;
type
TWebView2State = (wvsCreated, wvsInitializing, wvsReady, wvsDisposed);
TWebView2JsonMessage = record
Name: string;
Cid: string;
Payload: TJSONObject;
class function TryParse(const S: string; out M: TWebView2JsonMessage): Boolean; static;
end;
TOnWebMessage = reference to procedure(const M: TWebView2JsonMessage);
TOnDownloadMeta = reference to procedure(const Uri, MimeType, SuggestedFileName: string; TotalBytes: Int64);
TNbWebView2Host = class
private
FParentHwnd: HWND;
FState: TWebView2State;
FUserDataFolder: string;
FEnvironment: ICoreWebView2Environment;
FController: ICoreWebView2Controller;
FWebView: ICoreWebView2;
FTokenWebMsg: EventRegistrationToken;
FTokenDownload: EventRegistrationToken;
FOnWebMessage: TOnWebMessage;
FOnDownloadMeta: TOnDownloadMeta;
procedure RequireState(const Allowed: array of TWebView2State);
function EffectiveUserDataFolder: string;
procedure HookEvents;
procedure UnhookEvents;
procedure HandleWebMessage(const args: ICoreWebView2WebMessageReceivedEventArgs);
procedure HandleDownloadStarting(const args: ICoreWebView2DownloadStartingEventArgs);
public
constructor Create(AParentHwnd: HWND; const AUserDataFolder: string = '');
destructor Destroy; override;
procedure InitializeAsync;
procedure Dispose;
procedure Navigate(const Url: string);
procedure ResizePx(const BoundsPx: TRect);
procedure PostJson(const Obj: TJSONObject);
property State: TWebView2State read FState;
property WebView: ICoreWebView2 read FWebView;
property OnWebMessage: TOnWebMessage read FOnWebMessage write FOnWebMessage;
property OnDownloadMeta: TOnDownloadMeta read FOnDownloadMeta write FOnDownloadMeta;
end;
implementation
class function TWebView2JsonMessage.TryParse(const S: string; out M: TWebView2JsonMessage): Boolean;
var
V: TJSONValue;
O: TJSONObject;
P: TJSONValue;
begin
Result := False;
M.Name := '';
M.Cid := '';
M.Payload := nil;
V := TJSONObject.ParseJSONValue(S);
try
if not (V is TJSONObject) then Exit;
O := TJSONObject(V);
M.Name := O.GetValue('name', '');
M.Cid := O.GetValue('cid', '');
if O.TryGetValue('payload', P) and (P is TJSONObject) then
M.Payload := TJSONObject(P.Clone)
else
M.Payload := TJSONObject.Create;
Result := M.Name <> '';
finally
V.Free;
end;
end;
constructor TNbWebView2Host.Create(AParentHwnd: HWND; const AUserDataFolder: string);
begin
inherited Create;
FParentHwnd := AParentHwnd;
FUserDataFolder := AUserDataFolder;
FState := wvsCreated;
FTokenWebMsg.value := 0;
FTokenDownload.value := 0;
end;
destructor TNbWebView2Host.Destroy;
begin
Dispose;
inherited;
end;
procedure TNbWebView2Host.RequireState(const Allowed: array of TWebView2State);
var
S: TWebView2State;
Ok: Boolean;
I: Integer;
begin
Ok := False;
for I := Low(Allowed) to High(Allowed) do
if FState = Allowed[I] then
Ok := True;
if not Ok then
begin
for S := Low(TWebView2State) to High(TWebView2State) do begin end; // nur, um Enum im Debug sichtbar zu halten
raise EInvalidOperation.CreateFmt('WebView2Host: ungültiger Zustand (%d).', [Ord(FState)]);
end;
end;
function TNbWebView2Host.EffectiveUserDataFolder: string;
begin
if FUserDataFolder <> '' then
Exit(FUserDataFolder);
// Stabiler, pro Windows-User; bewusst nicht im Installationsordner.
Result := TPath.Combine(TPath.GetHomePath, 'MyCompany\MyApp\WebView2UserData');
ForceDirectories(Result);
end;
procedure TNbWebView2Host.InitializeAsync;
var
Opt: ICoreWebView2EnvironmentOptions;
UserData: string;
begin
RequireState([wvsCreated]);
FState := wvsInitializing;
UserData := EffectiveUserDataFolder;
Opt := TCoreWebView2EnvironmentOptions.Create;
OleCheck(CreateCoreWebView2EnvironmentWithOptions(
nil,
PWideChar(UserData),
Opt,
TCoreWebView2CreateCoreWebView2EnvironmentCompletedHandler.Create(
procedure(errorCode: HRESULT; const createdEnvironment: ICoreWebView2Environment)
begin
if FState = wvsDisposed then Exit;
OleCheck(errorCode);
FEnvironment := createdEnvironment;
OleCheck(FEnvironment.CreateCoreWebView2Controller(
FParentHwnd,
TCoreWebView2CreateCoreWebView2ControllerCompletedHandler.Create(
procedure(errorCode2: HRESULT; const createdController: ICoreWebView2Controller)
begin
if FState = wvsDisposed then Exit;
OleCheck(errorCode2);
FController := createdController;
OleCheck(FController.get_CoreWebView2(FWebView));
HookEvents;
OleCheck(FController.put_IsVisible(1));
FState := wvsReady;
end)));
end)));
end;
procedure TNbWebView2Host.HookEvents;
begin
if FWebView = nil then Exit;
OleCheck(FWebView.add_WebMessageReceived(
TCoreWebView2WebMessageReceivedEventHandler.Create(
procedure(const sender: ICoreWebView2; const args: ICoreWebView2WebMessageReceivedEventArgs)
begin
if FState = wvsDisposed then Exit;
HandleWebMessage(args);
end),
FTokenWebMsg));
OleCheck(FWebView.add_DownloadStarting(
TCoreWebView2DownloadStartingEventHandler.Create(
procedure(const sender: ICoreWebView2; const args: ICoreWebView2DownloadStartingEventArgs)
begin
if FState = wvsDisposed then Exit;
HandleDownloadStarting(args);
end),
FTokenDownload));
end;
procedure TNbWebView2Host.UnhookEvents;
begin
// Wichtig: remove_* reduziert Nachläufer nach Dispose.
if FWebView <> nil then
begin
if FTokenWebMsg.value <> 0 then
FWebView.remove_WebMessageReceived(FTokenWebMsg);
if FTokenDownload.value <> 0 then
FWebView.remove_DownloadStarting(FTokenDownload);
end;
FTokenWebMsg.value := 0;
FTokenDownload.value := 0;
end;
procedure TNbWebView2Host.Dispose;
begin
if FState = wvsDisposed then Exit;
FState := wvsDisposed;
UnhookEvents;
// Reihenfolge: erst WebView/Controller lösen, dann Environment.
FWebView := nil;
FController := nil;
FEnvironment := nil;
end;
procedure TNbWebView2Host.Navigate(const Url: string);
begin
RequireState([wvsReady]);
OleCheck(FWebView.Navigate(PWideChar(Url)));
end;
procedure TNbWebView2Host.ResizePx(const BoundsPx: TRect);
var
R: tagRECT;
begin
if (FState <> wvsReady) or (FController = nil) then Exit;
R.Left := BoundsPx.Left;
R.Top := BoundsPx.Top;
R.Right := BoundsPx.Right;
R.Bottom := BoundsPx.Bottom;
OleCheck(FController.put_Bounds(R));
end;
procedure TNbWebView2Host.PostJson(const Obj: TJSONObject);
var
S: string;
begin
if (FState <> wvsReady) or (FWebView = nil) then Exit;
S := Obj.ToJSON;
OleCheck(FWebView.PostWebMessageAsString(PWideChar(S)));
end;
procedure TNbWebView2Host.HandleWebMessage(const args: ICoreWebView2WebMessageReceivedEventArgs);
var
P: PWideChar;
S: string;
M: TWebView2JsonMessage;
begin
P := nil;
OleCheck(args.TryGetWebMessageAsString(P));
try
S := P;
finally
CoTaskMemFree(P);
end;
if not Assigned(FOnWebMessage) then Exit;
if not TWebView2JsonMessage.TryParse(S, M) then Exit;
try
// UI-Update hier nicht direkt erzwingen: in der App-Schicht ggf. TThread.Queue nutzen.
FOnWebMessage(M);
finally
M.Payload.Free;
end;
end;
procedure TNbWebView2Host.HandleDownloadStarting(const args: ICoreWebView2DownloadStartingEventArgs);
var
Dl: ICoreWebView2DownloadOperation;
Uri, Mime, ResultPath: PWideChar;
Total: Int64;
Suggested: string;
begin
Uri := nil;
Mime := nil;
ResultPath := nil;
OleCheck(args.get_DownloadOperation(Dl));
OleCheck(Dl.get_TotalBytesToReceive(Total));
OleCheck(Dl.get_Uri(Uri));
OleCheck(Dl.get_MimeType(Mime));
OleCheck(Dl.get_ResultFilePath(ResultPath));
try
Suggested := ExtractFileName(string(ResultPath));
if Suggested = '' then Suggested := 'download.bin';
if Assigned(FOnDownloadMeta) then
FOnDownloadMeta(string(Uri), string(Mime), Suggested, Total);
// Variante: eigenes Handling aktivieren
// args.put_Handled(1);
finally
CoTaskMemFree(Uri);
CoTaskMemFree(Mime);
CoTaskMemFree(ResultPath);
end;
end;
end.
Was an diesem Snippet „ungewöhnlich sauber“ ist
- State-Machine statt Bool: wvsCreated → wvsInitializing → wvsReady → wvsDisposed. Das macht Fehlbedienung (Navigate zu früh, Dispose doppelt) sichtbar.
- Explizites Unhooking: EventRegistrationToken wird gespeichert und in Dispose entfernt. Das ist ein häufig ausgelassener Punkt, der in gewachsenen Clients echte Stabilität bringt.
- UserDataFolder bewusst gewählt: pro User, außerhalb des Installationspfads. Das ist nicht „nice to have“, sondern ein Support- und Betriebshebel.
Randfälle: Threading, DPI und Form-Lifecycle in FMX
Callbacks und FMX-Thread
Auch wenn viele WebView2-Callbacks „UI-nah“ wirken: behandeln Sie jede Bridge-Nachricht so, als käme sie aus einem fremden Kontext. Die sichere Regel: FMX-UI nur über TThread.Queue(nil, ...) anfassen. Architekturbegriff dazu: Thread-Affinität bedeutet, dass UI-Objekte an einen bestimmten Thread gebunden sind.
Resize und DPI-Skalierung
FMX arbeitet in logischen Koordinaten, WebView2 will Pixel-Rects. Wenn Ihre App per Monitor-DPI skaliert (Wechsel zwischen 100%/150% Monitoren), testen Sie gezielt: Resize beim Monitorwechsel und beim Maximieren/Restore. Hier entstehen gern „1-Pixel-Flackern“, falsche Hit-Tests oder abgeschnittene Ränder, wenn die Umrechnung nicht zentral gelöst ist.
Downloads: Compliance, Ablage und „warum ist ResultFilePath leer?“
WebView2-Downloads laufen über ICoreWebView2DownloadOperation. Im Betrieb sind zwei Dinge relevant: Erstens ist ResultFilePath nicht garantiert früh aussagekräftig. Zweitens wollen viele Unternehmen Ablageorte kontrollieren (Projektordner, Quarantäne, kein „Downloads“-Wildwuchs).
In reifen Clients sind das typische Regeln:
- Größenlimit und MIME-Filter (z. B. blocke „application/x-msdownload“ je nach Policy).
- Metadaten-Logging (URI, MIME, Bytes) als Audit-Trail; Inhalte nicht ins Log.
- Optional
args.Handled = Trueund eigener Pfad/Name, wenn Prozesse das erfordern.
Genau eine redaktionelle Einschätzung
Aus Redaktion und Betriebssicht ist der häufigste Fehler nicht „zu wenig WebView2-Wissen“, sondern fehlende Entscheidung: Man startet mit einem Mini-Prototyp und übernimmt ihn unverändert in produktive Business-Software. WebView2 ist dann nicht das Problem – die fehlende Kapselung und fehlende Betriebsstrategie sind es.
Grenzen und Risiken: wann WebView2 in FMX nicht die richtige Kante ist
- Multiplattform: WebView2 ist Windows. Wenn Ihr FMX-Client wirklich auf macOS/iOS/Android muss, planen Sie eine WebView-Abstraktion oder trennen Sie die Web-UI anders.
- Sicherheitsmodell: Sobald externe Inhalte geladen werden, brauchen Sie Domain-Restriktionen, Navigation-Policies und ein klares Update-/Runtime-Konzept. Sonst bauen Sie eine ungewollte Angriffsfläche in den Client.
- Supportfähigkeit: Ein definierter UserDataFolder hilft, kann aber auch personenbezogene Daten enthalten (Cookies, Storage). Für Supportfälle müssen Datenschutz und Prozesse geklärt sein (z. B. wie man Ordner exportiert, bereinigt oder anonymisiert).
Fazit: Delphi WebView2 FMX als Integrationskomponente, nicht als UI-Gimmick
Wenn Sie Delphi WebView2 FMX wie eine Integrationskomponente behandeln – mit State-Machine, sauberem Dispose, Token-Unhooking, kontrolliertem WebView2 User Data Folder und einer versionierbaren JSON-Bridge –, wird daraus eine belastbare Modernisierungskante für gewachsene Clients. Lohnt sich besonders, wenn Sie Web-Technik gezielt für Login-Flows, eingebettete Portale oder Admin-Views nutzen, aber die Kernlogik bewusst in Delphi halten.
Einsatzgrenze: Sobald Sie anfangen, über die Bridge „beliebige“ Business-Logik aus dem Web nach Delphi zu drücken, verlieren Sie Testbarkeit, Versionierbarkeit und im Zweifel die Kontrolle über Sicherheit und Betrieb. Dann ist nicht mehr WebView2 das Thema, sondern Architekturdisziplin.
Für dieses Thema sind auch Delphi Fmx Edge Browser und Webview2 Firemonkey wichtig. Der Beitrag ordnet diese Aspekte verständlich ein und zeigt, worauf es im Alltag ankommt.
Projekt oder Modernisierungsvorhaben mit Net-Base besprechen.
nächster Schritt
Wenn aus dem Thema ein reales Projekt wird, sollten Architektur, Bestand und Betrieb früh zusammen betrachtet werden.
Wir unterstuetzen nicht nur bei Einzelfragen, sondern auch dann, wenn aus Source-Schnipseln, Legacy-Themen oder Portalideen ein belastbares Unternehmensprojekt werden soll.
- Bestand, Zielbild und technische Risiken werden zusammen bewertet.
- REST, Datenzugriff, Portale und Rollout werden nicht als Spätfolgen verschoben.
- Sie sehen früh, welcher Weg wirtschaftlich und betrieblich tragfähig ist.