Vom Magazinthema zur Projektpraxis
Passende Leistungs- und Technikseiten zum Beitrag
Wer in einer bestehenden Business-Software plötzlich „mal eben“ moderne Web-Inhalte einbetten will, landet auf Windows bei WebView2. In Delphi WebView2 FMX ist das Grundproblem selten das Anzeigen einer URL, sondern die saubere Einbettung in eine FireMonkey-Oberfläche (FMX), das zuverlässige Initialisieren (asynchron und COM-basiert), sowie die Edge-Fallstricke rund um User-Data-Verzeichnisse, Downloads, Debugging und eine robuste JS↔Delphi-Kommunikation.
Dieser Source-Schnipsel zeigt ein Muster, das ich für wartbare Anwendungen bevorzuge: ein gekapseltes „Host“-Objekt, das den WebView2-Lifecycle kontrolliert, sowie eine definierte Bridge über WebMessage (JSON), statt beliebigem „ExecuteScript überall“. Ziel ist kein Demo-Code, sondern ein Baustein, der in gewachsenen Clients überlebt.
Warum WebView2 in FMX anders ist als „Browser-Component drop“
WebView2 ist eine COM/WinRT-nahe API mit asynchroner Initialisierung. FireMonkey abstrahiert Windows-Handles, trotzdem benötigen Sie für WebView2 am Ende ein echtes Parent-Window (HWND) und kontrollierte Resize-/Focus-Weiterleitung. Gleichzeitig laufen Ereignisse nicht immer dort auf, wo man sie in FMX erwartet. Wenn Sie hier „quick and dirty“ starten, bekommen Sie typischerweise:
- sporadische AVs beim Form-Schließen (Callbacks treffen nach Destroy ein)
- Navigation-Events aus einem falschen Thread-Kontext
- unzuverlässige Persistenz/Cache-Probleme wegen unklarer UserDataFolder-Strategie
- keine Downloads oder „hängende“ Download-Dialoge
- Debugging nur über Glück statt gezielter Remote-Debug-Konfiguration
Das Gegenmittel ist ein klarer Lifecycle: Create → InitializeAsync → Attach → Navigate → Detach/Dispose – und eine definierte Grenze zwischen UI und Browser-Engine.
Source-Schnipsel: WebView2Host für Delphi WebView2 FMX
Der folgende Code skizziert eine gekapselte Host-Klasse, die (1) eine WebView2-Environment-Konfiguration erstellt, (2) das Controller-Objekt an ein HWND bindet, (3) Navigation und Download-Events verdrahtet und (4) eine JSON-basierte JS-Bridge über WebMessageReceived anbietet. Der Code ist bewusst „architekturfähig“: er kapselt COM-Referenzen, verhindert Callback-Nachläufer nach Destroy, und erlaubt Betriebskanten wie „pro User“ oder „pro Maschine“ getrennte UserDataFolder.
unit WebView2Host;
interface
uses
System.SysUtils, System.Classes, System.IOUtils, System.JSON,
Winapi.Windows, Winapi.ActiveX,
FMX.Types,
WebView2, WebView2_TLB; // je nach Setup: WebView2.pas oder Import-TLB
type
TWebView2JsonMessage = record
Name: string;
CorrelationId: string;
Payload: TJSONObject;
class function TryParse(const Json: string; out Msg: TWebView2JsonMessage): Boolean; static;
end;
TOnWebMessage = reference to procedure(const Msg: TWebView2JsonMessage);
TOnDownload = reference to procedure(const FileName, MimeType: string; TotalBytes: Int64);
TWebView2Host = class
private
FParentHwnd: HWND;
FUserDataFolder: string;
FEnvironment: ICoreWebView2Environment;
FController: ICoreWebView2Controller;
FWebView: ICoreWebView2;
FDestroyed: Boolean;
FOnWebMessage: TOnWebMessage;
FOnDownload: TOnDownload;
procedure EnsureNotDestroyed;
function MakeUserDataFolder: string;
// Event handler
procedure HookEvents;
procedure UnhookEvents;
procedure OnWebMessageReceived(
const sender: ICoreWebView2;
const args: ICoreWebView2WebMessageReceivedEventArgs);
procedure OnDownloadStarting(
const sender: ICoreWebView2;
const args: ICoreWebView2DownloadStartingEventArgs);
public
constructor Create(AParentHwnd: HWND; const AUserDataFolder: string = '');
destructor Destroy; override;
procedure InitializeAsync;
procedure Navigate(const Url: string);
procedure Resize(const Bounds: TRect);
procedure PostJsonToWeb(const Obj: TJSONObject);
procedure SetDevToolsEnabled(const Enabled: Boolean);
property WebView: ICoreWebView2 read FWebView;
property OnWebMessage: TOnWebMessage read FOnWebMessage write FOnWebMessage;
property OnDownload: TOnDownload read FOnDownload write FOnDownload;
end;
implementation
{ TWebView2JsonMessage }
class function TWebView2JsonMessage.TryParse(const Json: string; out Msg: TWebView2JsonMessage): Boolean;
var
V: TJSONValue;
O: TJSONObject;
begin
Result := False;
Msg.Name := '';
Msg.CorrelationId := '';
Msg.Payload := nil;
V := TJSONObject.ParseJSONValue(Json);
try
if not (V is TJSONObject) then Exit;
O := TJSONObject(V);
Msg.Name := O.GetValue('name', '');
Msg.CorrelationId := O.GetValue('cid', '');
// Payload kann fehlen oder null sein
if O.TryGetValue('payload', Msg.Payload) then
Msg.Payload := TJSONObject(Msg.Payload.Clone)
else
Msg.Payload := TJSONObject.Create;
Result := Msg.Name <> '';
finally
V.Free;
end;
end;
{ TWebView2Host }
constructor TWebView2Host.Create(AParentHwnd: HWND; const AUserDataFolder: string);
begin
inherited Create;
FParentHwnd := AParentHwnd;
FUserDataFolder := AUserDataFolder;
FDestroyed := False;
end;
destructor TWebView2Host.Destroy;
begin
FDestroyed := True;
// Events lösen, bevor COM-Objekte freigegeben werden
UnhookEvents;
FWebView := nil;
FController := nil;
FEnvironment := nil;
inherited;
end;
procedure TWebView2Host.EnsureNotDestroyed;
begin
if FDestroyed then
raise EInvalidOperation.Create('WebView2Host ist bereits zerstört.');
end;
function TWebView2Host.MakeUserDataFolder: string;
begin
if FUserDataFolder <> '' then
Exit(FUserDataFolder);
// Praxis: pro App + pro Windows-User, nicht in Programmverzeichnis
Result := TPath.Combine(TPath.GetHomePath, 'AppDataLocalMyCompanyMyAppWebView2');
ForceDirectories(Result);
end;
procedure TWebView2Host.InitializeAsync;
var
UserData: string;
Opt: ICoreWebView2EnvironmentOptions;
begin
EnsureNotDestroyed;
UserData := MakeUserDataFolder;
// Options: hier können zusätzliche Browser-Argumente rein, z.B. Remote-Debug
Opt := TCoreWebView2EnvironmentOptions.Create;
// Async CreateEnvironment
OleCheck(CreateCoreWebView2EnvironmentWithOptions(
nil, PWideChar(UserData), Opt,
TCoreWebView2CreateCoreWebView2EnvironmentCompletedHandler.Create(
procedure (errorCode: HRESULT; const createdEnvironment: ICoreWebView2Environment)
begin
if FDestroyed then Exit;
OleCheck(errorCode);
FEnvironment := createdEnvironment;
// Controller an Parent HWND binden
OleCheck(FEnvironment.CreateCoreWebView2Controller(
FParentHwnd,
TCoreWebView2CreateCoreWebView2ControllerCompletedHandler.Create(
procedure (errorCode2: HRESULT; const createdController: ICoreWebView2Controller)
begin
if FDestroyed then Exit;
OleCheck(errorCode2);
FController := createdController;
OleCheck(FController.get_CoreWebView2(FWebView));
HookEvents;
// Initial sichtbar machen
FController.put_IsVisible(1);
end)));
end)));
end;
procedure TWebView2Host.HookEvents;
var
TokenMsg, TokenDl: EventRegistrationToken;
begin
if (FWebView = nil) then Exit;
// WebMessageReceived (JS->Delphi)
TokenMsg.value := 0;
OleCheck(FWebView.add_WebMessageReceived(
TCoreWebView2WebMessageReceivedEventHandler.Create(
procedure(const sender: ICoreWebView2; const args: ICoreWebView2WebMessageReceivedEventArgs)
begin
if FDestroyed then Exit;
OnWebMessageReceived(sender, args);
end), TokenMsg));
// DownloadStarting
TokenDl.value := 0;
OleCheck(FWebView.add_DownloadStarting(
TCoreWebView2DownloadStartingEventHandler.Create(
procedure(const sender: ICoreWebView2; const args: ICoreWebView2DownloadStartingEventArgs)
begin
if FDestroyed then Exit;
OnDownloadStarting(sender, args);
end), TokenDl));
// Hinweis: Für robustes Unhooking sollten Sie die Tokens speichern.
// In vielen Projekten ist das ausreichend, wenn der Host nur mit dem Form lebt.
end;
procedure TWebView2Host.UnhookEvents;
begin
// Robust-Variante: Tokens merken und remove_* aufrufen.
// Hier als Kommentar, weil das Import-Unit-Setup und Token-Verwaltung je nach Wrapper variiert.
end;
procedure TWebView2Host.OnWebMessageReceived(
const sender: ICoreWebView2;
const args: ICoreWebView2WebMessageReceivedEventArgs);
var
Json: PWideChar;
S: string;
Msg: TWebView2JsonMessage;
begin
Json := nil;
OleCheck(args.TryGetWebMessageAsString(Json));
try
S := Json;
finally
CoTaskMemFree(Json);
end;
if Assigned(FOnWebMessage) and TWebView2JsonMessage.TryParse(S, Msg) then
begin
try
FOnWebMessage(Msg);
finally
Msg.Payload.Free;
end;
end;
end;
procedure TWebView2Host.OnDownloadStarting(
const sender: ICoreWebView2;
const args: ICoreWebView2DownloadStartingEventArgs);
var
Dl: ICoreWebView2DownloadOperation;
Uri, Mime, ResultFile: PWideChar;
Total: Int64;
FileName: string;
begin
Uri := nil;
Mime := nil;
ResultFile := nil;
OleCheck(args.get_DownloadOperation(Dl));
OleCheck(Dl.get_TotalBytesToReceive(Total));
// In der Praxis: ResultFileName initial leer, je nach Quelle.
OleCheck(Dl.get_ResultFilePath(ResultFile));
OleCheck(Dl.get_MimeType(Mime));
OleCheck(Dl.get_Uri(Uri));
try
FileName := ExtractFileName(string(ResultFile));
if FileName = '' then
FileName := 'download.bin';
if Assigned(FOnDownload) then
FOnDownload(FileName, string(Mime), Total);
// Optional: eigene Download-UI, dann Handled setzen
// args.put_Handled(1);
finally
CoTaskMemFree(Uri);
CoTaskMemFree(Mime);
CoTaskMemFree(ResultFile);
end;
end;
procedure TWebView2Host.Navigate(const Url: string);
begin
EnsureNotDestroyed;
if FWebView = nil then
raise EInvalidOperation.Create('WebView2 ist noch nicht initialisiert.');
OleCheck(FWebView.Navigate(PWideChar(Url)));
end;
procedure TWebView2Host.Resize(const Bounds: TRect);
var
R: tagRECT;
begin
if FController = nil then Exit;
R.Left := Bounds.Left;
R.Top := Bounds.Top;
R.Right := Bounds.Right;
R.Bottom := Bounds.Bottom;
OleCheck(FController.put_Bounds(R));
end;
procedure TWebView2Host.PostJsonToWeb(const Obj: TJSONObject);
var
S: string;
begin
EnsureNotDestroyed;
if FWebView = nil then Exit;
S := Obj.ToJSON;
OleCheck(FWebView.PostWebMessageAsString(PWideChar(S)));
end;
procedure TWebView2Host.SetDevToolsEnabled(const Enabled: Boolean);
var
Settings: ICoreWebView2Settings;
begin
if (FWebView = nil) then Exit;
OleCheck(FWebView.get_Settings(Settings));
OleCheck(Settings.put_AreDevToolsEnabled(Ord(Enabled)));
end;
end.
Zweck des Ansatzes
- Lifecycle-Kapselung: Die FMX-Form kennt nur „Initialize/Navigate/Resize“, nicht COM-Details.
- Bridge mit Vertrag: JSON-Nachrichten mit
name, optionalcid(Correlation-ID) undpayloadsind wartbar und testbar. - Betriebssichere Persistenz: ein kontrolliertes
UserDataFolderverhindert Cache-Kollisionen, Rechteprobleme und „läuft auf Entwicklerrechner, nicht im Betrieb“.
JS↔Delphi-Bridge: warum WebMessage stabiler ist als ExecuteScript
WebView2 bietet mehrere Wege der Kommunikation. In der Praxis ist ExecuteScript verführerisch, aber schlecht zu versionieren: Sie schieben Strings in einen Interpreter, ohne klare Antwort-Kanäle und ohne robustes Error-Mapping. PostWebMessageAsString / WebMessageReceived ist dagegen ein definierter Kanal.
Randfall, der in Unternehmensumgebungen häufig auftaucht: Sie müssen aus einem Web-Frontend (z. B. internes Portal) einen Delphi-Workflow starten (Druck, Gerätezugriff, Legacy-Integration). Dann brauchen Sie:
- eine Whitelist von Message-Namen
- Correlation-IDs für asynchrone Antworten
- eine zentrale Stelle, die Payloads validiert (z. B. Pflichtfelder, Größenlimits)
Im Host ist das die Stelle OnWebMessageReceived. Die eigentliche Validierung gehört in eine darüber liegende Schicht (z. B. Application-Service), damit Sie UI-/WebView2-Technik und Business-Logik getrennt halten (klassische Layer-Architektur: UI → Application → Domain → Infrastruktur).
Downloads und Dateiablage: was im Betrieb oft überrascht
Downloads laufen in WebView2 über ICoreWebView2DownloadOperation. Je nach Quelle kann ResultFilePath früh leer sein oder erst später gesetzt werden. Zudem wollen viele Unternehmen nicht, dass Endanwender in unkontrollierte Ordner speichern.
Bewährte Muster:
- DownloadStarting abfangen und per
args.put_Handled(1)die UI selbst übernehmen (eigener Pfad, Namenskonvention, Quarantäne-Ordner). - Dateigrößen-Grenzen und MIME-Type-Checks, um „versehentlich 4 GB Logfile“ zu vermeiden.
- Auditing: Download-Metadaten (URI, MIME, Bytes) in Ihr Logging schreiben, nicht den Inhalt.
Wenn Sie regulierte Prozesse haben (z. B. Freigaben, Nachvollziehbarkeit), ist das Handling über die Events die einzige Stelle, an der Sie die Browser-Welt in Ihre Betriebsregeln integrieren können.
Debugging: DevTools, Remote Debug Port und reproduzierbare Zustände
WebView2-Debugging kippt häufig daran, dass Zustände nicht reproduzierbar sind. Zwei Stellschrauben helfen:
- DevTools aktivieren/deaktivieren über
ICoreWebView2Settings(im Code:SetDevToolsEnabled) – im Release oft aus, im Support-Fall gezielt an. - Stabiles UserDataFolder: Wenn Ihr Support einen Fehler nachstellen soll, ist ein definierter Pfad Gold wert. Sie können den Ordner sichern/zippen (Achtung: Datenschutz/PII) und Zustände gezielt vergleichen.
Optional (je nach Wrapper) können Sie EnvironmentOptions mit zusätzlichen Browser-Argumenten versehen, z. B. einen Remote-Debug-Port. Das ist sinnvoll, wenn Sie eine Anwendung auf einem Testsystem ohne lokale Entwickler-Tools analysieren müssen. Grenzen: In produktiven Umgebungen muss das sauber freigeschaltet und dokumentiert sein, sonst schaffen Sie eine unnötige Angriffsfläche.
Stolperfallen in Delphi WebView2 FMX: COM, Threads und Form-Lifecycle
1) Callbacks nach dem Schließen
Die asynchronen CompletedHandler können eintreffen, nachdem die Form schon schließt. Im Snippet verhindert FDestroyed den Zugriff auf freigegebene Objekte. Robuster ist zusätzlich:
- Tokens für Events speichern und in
Destroysauberremove_*aufrufen - InitializeAsync nur einmal zulassen (State-Machine: Created/Initializing/Ready/Disposed)
2) Thread-Kontext
Viele Handler kommen zwar „UI-nah“, aber verlassen Sie sich nicht darauf, dass Sie direkt in FMX-Controls schreiben können. Wenn Sie in OnWebMessage UI aktualisieren, ist TThread.Queue(nil, ...) die sichere Variante. Ich trenne gern: Host sammelt Ereignis, Application-Service entscheidet, UI wird ausschließlich per Queue aktualisiert.
3) DPI/Resize und FMX-Layouts
FMX rechnet in logischen Einheiten, WebView2 erwartet Pixel-Rects. In der Praxis brauchen Sie eine klare Stelle, an der Sie aus FMX-Controls Bounds in echte Pixel übersetzen. Das Snippet nimmt ein TRect an; in Ihrer Form sollten Sie daraus die WinAPI-Koordinaten ableiten (z. B. über FMX.Platform.Win und Handle-APIs). Wenn die App per Monitor-DPI skaliert, testen Sie den Wechsel zwischen Monitoren: WebView2 ist hier empfindlicher als reine FMX-Controls.
Wann sich WebView2 in FMX lohnt – und wann nicht
WebView2 lohnt sich, wenn Sie in einer gewachsenen Delphi-Client-Anwendung Web-Technik gezielt einsetzen wollen: eingebettete Admin-Views, OAuth/OIDC-Login-Flows, HTML-Reports, interne Portale oder kontrollierte „Micro-Frontends“. Auch als Modernisierungsbrücke ist es praktikabel, solange Sie die Zuständigkeiten sauber schneiden und die Bridge nicht zur unkontrollierten Hintertür für Business-Logik machen.
Grenzen des Ansatzes:
- Plattform: Das Muster ist Windows-zentriert. FMX ist multiplattformfähig, WebView2 ist es nicht. Für macOS/iOS/Android brauchen Sie andere WebViews oder eine Abstraktionsschicht.
- Security/Hardening: Sobald externe Inhalte geladen werden, müssen Sie Navigation, erlaubte Domains und Download-Ziele härter einschränken. Das gehört in Requirements, nicht „später“.
- Support: UserDataFolder und Runtime-Abhängigkeiten (WebView2 Runtime) müssen Teil Ihres Betriebs-/Rollout-Konzepts sein.
Fazit
Delphi WebView2 FMX ist weniger ein UI-Gadget als eine Integrationskomponente mit eigenem Lifecycle. Wenn Sie Initialisierung, Eventing, UserDataFolder und JS-Bridge strukturiert kapseln, wird WebView2 ein stabiler Baustein für digitale Unternehmenslösungen: Web-UI dort, wo es Sinn ergibt, und Delphi-Logik dort, wo sie hingehört. Wenn Sie dagegen unkontrolliert Skripte feuern, Pfade dem Zufall überlassen und Events nicht entkoppeln, bekommen Sie genau die Sorte „sporadisch im Feld“-Fehler, die Zeit frisst und Vertrauen kostet.
Wenn Sie bei einer bestehenden Delphi-Anwendung WebView2 sauber integrieren oder eine Modernisierungskante technisch bewerten wollen, sprechen Sie mit uns:
Im fachlichen Umfeld spielen auch Webview2 Firemonkey und Delphi Fmx Edge Browser eine wichtige Rolle, wenn Integrationen, Datenflüsse und Weiterentwicklung sauber zusammenspielen müssen.
Projekt oder Modernisierungsvorhaben mit Net-Base besprechen.
Nächster Schritt
Wenn aus dem Thema ein reales Projekt wird, sollten Architektur, Bestand und Betrieb frueh 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 Spaetfolgen verschoben.
- Sie sehen frueh, welcher Weg wirtschaftlich und betrieblich tragfähig ist.