Van magazinethema naar projectpraktijk
Relevante dienst- en technische pagina's bij het artikel
Wie in bestaande bedrijfssoftware plotseling ‚even snel‘ moderne webinhoud wil insluiten, stuit bij Windows op WebView2. In Delphi WebView2 FMX is het fundamentele probleem zelden het tonen van een URL, maar de nette integratie in een FireMonkey‑interface (FMX), het betrouwbare initialiseren (asynchroon en COM‑gebaseerd), en de Edge-valkuilen rond User‑Data‑mappen, downloads, debugging en een robuuste JS↔Delphi‑communicatie.
Dit broncodefragment toont een patroon dat ik voor onderhoudbare toepassingen prefereer: een ingekapseld ‚Host‘-object dat de WebView2‑lifecycle regelt, en een gedefinieerde bridge via WebMessage (JSON), in plaats van willekeurig ‚ExecuteScript‘ overal. Het doel is geen demo‑code, maar een bouwsteen die in gegroeide clients overleeft.
Waarom WebView2 in FMX anders is dan een „Browser-Component drop”
WebView2 is een COM/WinRT‑nauwkeurige API met asynchrone initialisatie. FireMonkey abstraheert Windows‑handles, maar uiteindelijk heeft u voor WebView2 een echt parent‑window (HWND) en gecontroleerde resize-/focus‑doorgave nodig. Tegelijkertijd lopen events niet altijd op de plekken waarop men ze in FMX verwacht. Als u hier ‚quick and dirty‘ begint, krijgt u typisch:
- sporadische AVs bij het sluiten van het formulier (callbacks komen na Destroy binnen)
- navigatie‑events vanuit een verkeerde thread‑context
- onbetrouwbare persistentie/cache‑problemen door een onduidelijke UserDataFolder‑strategie
- geen downloads of ‚hangende‘ downloaddialogen
- debugging alleen toevallig in plaats van via een gerichte remote‑debugconfiguratie
Het tegengif is een duidelijke lifecycle: Create → InitializeAsync → Attach → Navigate → Detach/Dispose – en een gedefinieerde grens tussen UI en browser‑engine.
Broncodefragment: WebView2Host voor Delphi WebView2 FMX
De volgende code schetst een ingekapselde host‑klasse die (1) een WebView2‑Environment‑configuratie aanmaakt, (2) het Controller‑object aan een HWND bindt, (3) navigatie‑ en downloadevents bedrading geeft en (4) een op JSON gebaseerde JS‑bridge via WebMessageReceived aanbiedt. De code is bewust „architectuurgeschikt“: hij kapselt COM‑referenties, voorkomt callback‑nazwermen na Destroy, en maakt bedrijfsgrenzen mogelijk zoals gescheiden UserDataFolder voor ‚pro User‘ of ‚pro Maschine‘.
unit WebView2Host;
interface
uses
System.SysUtils, System.Classes, System.IOUtils, System.JSON,
Winapi.Windows, Winapi.ActiveX,
FMX.Types,
WebView2, WebView2_TLB; // afhankelijk van de setup: WebView2.pas of 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 kan ontbreken of null zijn
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 ontkoppelen voordat COM-objecten worden vrijgegeven
UnhookEvents;
FWebView := nil;
FController := nil;
FEnvironment := nil;
inherited;
end;
procedure TWebView2Host.EnsureNotDestroyed;
begin
if FDestroyed then
raise EInvalidOperation.Create('WebView2Host is al vernietigd.');
end;
function TWebView2Host.MakeUserDataFolder: string;
begin
if FUserDataFolder <> '' then
Exit(FUserDataFolder);
// Praktijk: per app + per Windows-gebruiker, niet in de programmamap
Result := TPath.Combine(TPath.GetHomePath, 'AppDataLocalMyCompanyMyAppWebView2');
ForceDirectories(Result);
end;
procedure TWebView2Host.InitializeAsync;
var
UserData: string;
Opt: ICoreWebView2EnvironmentOptions;
begin
EnsureNotDestroyed;
UserData := MakeUserDataFolder;
// Opties: hier kunnen extra browserargumenten worden toegevoegd, bijv. Remote-Debug
Opt := TCoreWebView2EnvironmentOptions.Create;
// Asynchroon 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 aan parent-HWND koppelen
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 zichtbaar maken
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));
// Opmerking: voor robuust unhooken moet u de tokens opslaan.
// In veel projecten is dat voldoende als de host slechts zo lang leeft als het formulier.
end;
procedure TWebView2Host.UnhookEvents;
begin
// Robuuste variant: tokens onthouden en remove_* aanroepen.
// Hier als commentaar, omdat de import-unit-setup en tokenbeheer per wrapper kunnen verschillen.
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 de praktijk: ResultFileName aanvankelijk leeg, afhankelijk van de bron.
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);
// Optioneel: eigen download-UI, dan Handled instellen
// 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 is nog niet geïnitialiseerd.');
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.
Doel van de aanpak
- Levenscyclus-inkapseling: Het FMX-formulier kent alleen „Initialize/Navigate/Resize“, geen COM-details.
- Bridge met contract: JSON-berichten met
name, optioneelcid(Correlation-ID) enpayloadzijn onderhoudbaar en testbaar. - Bedrijfsklare persistentie: een gecontroleerd
UserDataFoldervoorkomt cache-collisies, rechtenproblemen en het probleem dat iets „op de ontwikkelaarsmachine werkt, maar niet in productie”.
JS↔Delphi-Bridge: waarom WebMessage stabieler is dan ExecuteScript
WebView2 biedt meerdere communicatieroutes. In de praktijk is ExecuteScript verleidelijk, maar lastig te versioneren: u duwt strings in een interpreter zonder duidelijke antwoordkanalen en zonder robuuste foutmapping. PostWebMessageAsString / WebMessageReceived is daarentegen een gedefinieerd kanaal.
Randgeval dat in bedrijfsomgevingen vaak voorkomt: u moet vanuit een web-frontend (bijv. intern portaal) een Delphi-workflow starten (print, apparaattoegang, legacy-integratie). Dan heeft u nodig:
- een whitelist van message-namen
- Correlation-IDs voor asynchrone antwoorden
- een centrale plek die payloads valideert (bijv. verplichte velden, groottebeperkingen)
In de host is dat de handler OnWebMessageReceived. De daadwerkelijke validatie hoort in een daarbovenliggende laag (bijv. Application-Service), zodat u UI-/WebView2-techniek en businesslogica gescheiden houdt (klassieke lagenarchitectuur: UI → Application → Domain → Infrastructuur).
Downloads en bestandsopslag: wat in productie vaak verrast
Downloads lopen in WebView2 via ICoreWebView2DownloadOperation. Afhankelijk van de bron kan ResultFilePath in het begin leeg zijn of pas later worden gezet. Bovendien willen veel bedrijven niet dat eindgebruikers in ongecontroleerde mappen opslaan.
Beproefde patronen:
- DownloadStarting onderscheppen en met
args.put_Handled(1)de UI zelf afhandelen (eigen pad, naamsconventie, quarantaine-map). - Bestandsgrootte-limieten en MIME-type-controles, om per ongeluk een 4 GB logbestand te voorkomen.
- Auditing: schrijf download-metadata (URI, MIME, bytes) naar uw logging, niet de inhoud.
Als u gereguleerde processen heeft (bijv. vrijgaven, traceerbaarheid), is de event-gebaseerde afhandeling de enige plek waar u de browserwereld in uw operationele regels kunt integreren.
Debugging: DevTools, Remote Debug Port en reproduceerbare toestanden
WebView2-debugging faalt vaak doordat toestanden niet reproduceerbaar zijn. Twee instelpunten helpen:
- DevTools in-/uitschakelen via
ICoreWebView2Settings(in de code:SetDevToolsEnabled) – in de release vaak uit, in supportgevallen gericht aan. - Stabiel
UserDataFolder: als uw support een fout wil reproduceren, is een gedefinieerd pad goud waard. U kunt de map veiligstellen/zippen (let op: privacy/PII) en toestanden doelgericht vergelijken.
Optioneel (afhankelijk van de wrapper) kunt u EnvironmentOptions van extra browserargumenten voorzien, bijvoorbeeld een remote-debug-port. Dat is zinvol als u een applicatie op een testsysteem zonder lokale ontwikkelaarstools moet analyseren. Grenzen: in productieomgevingen moet dit zorgvuldig worden vrijgegeven en gedocumenteerd, anders creëert u een onnodig aanvalsoppervlak.
Stolperfallen in Delphi WebView2 FMX: COM, Threads und Form-Lifecycle
1) Callbacks na het sluiten
De asynchrone CompletedHandler kunnen binnenkomen nadat de Form al sluit. In het snippet voorkomt FDestroyed de toegang tot vrijgegeven objecten. Robuuster is daarnaast:
- Tokens voor events opslaan en in
Destroynetjesremove_*aanroepen - InitializeAsync slechts één keer toelaten (State-Machine: Created/Initializing/Ready/Disposed)
2) Thread-context
Veel Handler komen wel „UI-nabij“, maar vertrouw er niet op dat u direct in FMX-Controls kunt schrijven. Als u in OnWebMessage de UI bijwerkt, is TThread.Queue(nil, ...) de veilige variant. Ik scheid graag: de Host verzamelt het event, de Application-Service beslist, de UI wordt uitsluitend via Queue bijgewerkt.
3) DPI/Resize und FMX-Layouts
FMX rekent in logische eenheden, WebView2 verwacht Pixel-Rects. In de praktijk heeft u een duidelijke plek nodig waar u uit FMX-Controls Bounds in echte pixels vertaalt. Het Snippet gaat uit van een TRect; in uw Form moet u daaruit de WinAPI-coördinaten afleiden (bijv. via FMX.Platform.Win en Handle-APIs). Als de app per monitor-DPI schaalt, test dan het wisselen tussen monitoren: WebView2 is hier gevoeliger dan pure FMX-Controls.
Wanneer WebView2 in FMX zinvol is — en wanneer niet
WebView2 loont als u in een gegroeide Delphi-clientapplicatie webtechniek doelgericht wilt inzetten: ingebedde Admin-Views, OAuth/OIDC-login-flows, HTML-Reports, interne portalen of gecontroleerde „Micro-Frontends“. Ook als moderniseringsbrug is het praktisch, zolang u de verantwoordelijkheden scherp snijdt en de bridge niet tot een ongecontroleerde achterdeur voor business-logica maakt.
Beperkingen van de aanpak:
- Platform: Het patroon is Windows-gecentreerd. FMX is multiplatformfähig, WebView2 is dat niet. Voor macOS/iOS/Android heeft u andere WebViews of een abstractielaag nodig.
- Security/Hardening: Zodra externe inhoud geladen wordt, moet u navigatie, toegestane domeinen en downloaddoelen strikter beperken. Dat hoort in de Requirements, niet „later“.
- Support: UserDataFolder en runtime-afhankelijkheden (WebView2 Runtime) moeten deel uitmaken van uw beheer-/Rollout-Konzept.
Conclusie
Delphi WebView2 FMX is minder een UI-gadget dan een integratiecomponent met een eigen lifecycle. Als u initialisatie, eventing, UserDataFolder en JS-Bridge gestructureerd kapselt, wordt WebView2 een stabiele bouwsteen voor digitale bedrijfsoplossingen: Web-UI waar het zin heeft, en Delphi-logica waar die thuishoort. Als u daarentegen onverantwoord scripts afvuurt, paden aan het toeval overlaat en events niet ontkoppelt, krijgt u precies het soort „sporadisch im Feld“-fouten dat tijd vreet en vertrouwen schaadt.
Als u bij een bestaande Delphi-applicatie WebView2 netjes wilt integreren of een moderniseringsknoop technisch wilt beoordelen, neem contact met ons op:
In het vakgebied spelen ook Webview2 Firemonkey en Delphi Fmx Edge Browser een belangrijke rol wanneer integraties, datastromen en doorontwikkeling goed op elkaar moeten aansluiten.
Volgende stap
Wanneer het onderwerp een echt project wordt, zouden architectuur, bestaande omgeving en beheer in een vroeg stadium gezamenlijk moeten worden bekeken.
We ondersteunen niet alleen bij individuele vragen, maar ook wanneer uit broncodefragmenten, legacy-onderwerpen of portalideeën een robuust bedrijfsproject moet ontstaan.
- Huidige situatie, doelbeeld en technische risico's worden gezamenlijk beoordeeld.
- REST, gegevens‑toegang, portalen en uitrol worden niet als latere gevolgen uitgesteld.
- U ziet vroeg welke weg economisch en operationeel houdbaar is.