Du thème du magazine à la pratique des projets
Pages de services et techniques pertinentes pour l'article
« Nous devons juste intégrer rapidement une interface Web — login, un rapport, un petit portail interne. » C’est précisément à ce niveau que Delphi WebView2 FMX bascule régulièrement dans de véritables clients Windows : pas lors du rendu de la première URL, mais dans le lifecycle (asynchrone, basé sur COM), à la fermeture du formulaire, lors de téléchargements hors contrôle et dans une communication JS↔Delphi qui doit rester traçable en cas de support.
Situation construite mais typique des modernisations : un client FMX hérité (Windows) doit afficher un flux de connexion OAuth/OIDC puis une zone « Admin » basée sur React. Parallèlement, des workflows legacy (impression, export de fichiers, accès aux appareils) doivent pouvoir être déclenchés depuis le front Web. La première itération fonctionne sur la machine du développeur. Deux semaines plus tard : AVs sporadiques à la fermeture, « les téléchargements disparaissent », et le support ne peut pas reproduire l’erreur parce que le cache/le storage varient selon les machines.
Ce fragment de source est construit comme un chemin de décision : vous choisissez délibérément à quel point vous intégrez WebView2 dans votre application FMX. Au cœur se trouve un hôte encapsulé avec une state-machine, une stratégie définie de UserDataFolder, un pont JSON via WebMessage et un hook de téléchargement qui ne surprend pas non plus en production.
Chemin de décision : de combien de Delphi WebView2 FMX avez-vous réellement besoin ?
Avant d’écrire du code, une décision claire vaut mieux. WebView2 peut aller d’une « vue embarquée » à une « couche d’intégration complète ».
- Affichage uniquement (Read-only) : vous naviguez vers une URL et n’avez presque pas d’interaction. Une initialisation minimale suffit souvent, mais vous devez quand même définir un UserDataFolder stable et assurer un Dispose propre.
- Interaction (commands, passage de données) : l’UI Web doit déclencher des workflows Delphi. Vous avez alors besoin d’un pont avec contrat (schéma de messages, liste blanche, corrélation) et d’un découplage avec la logique métier (couches : UI → Application → Domain → Infrastructure).
- Fonctionnalités opérationnelles (téléchargements, debugging, support) : dès que des fichiers, DevTools, debug à distance ou la reproduction par le support deviennent importants, il faut concevoir événements, chemins, politiques et logging pour qu’ils tiennent sur le terrain.
| Décision | Si vous l’ignorez, il se passe généralement… | Mesure minimale |
|---|---|---|
| UserDataFolder non défini | Problèmes de droits, états non reproductibles, le support ne peut pas reproduire les bugs | Chemin par utilisateur Windows en dehors du répertoire d’installation |
| Pas de state-machine / logique de Dispose | AVs à la fermeture du formulaire, des handlers se déclenchent après Destroy | Flag Disposed + détacher les tokens + ordre de mise à nil |
| ExecuteScript utilisé comme « bridge » | Scripts sous forme de chaînes non versionnables, gestion d’erreurs difficile, effets secondaires « quelque part » | WebMessage avec contrat JSON et liste blanche |
| Téléchargements non interceptés | Emplacements incontrôlés, dialogues « bloqués », questions de conformité | Intercepter DownloadStarting, éventuellement le traiter + chemin dédié |
WebView2 dans FMX : pourquoi ce n’est pas un « Component drop »
WebView2 est une API proche du COM (Component Object Model : comptage de références, interfaces, événements) avec initialisation asynchrone. FMX (FireMonkey) abstrait certes les handles de fenêtres, mais WebView2 exige en fin de compte un Parent-HWND et des limites claires en pixels. De plus : les événements n’arrivent pas automatiquement dans un contexte où vous pouvez manipuler en toute sécurité des contrôles FMX.
Deux ancrages centraux et robustes sont importants ici : premièrement l’architecture officielle de WebView2 en tant que composant d’exécution (Edge Runtime + SDK), incluant le concept de User Data Folder que Microsoft décrit comme emplacement de persistance et de profils (Microsoft Learn : WebView2). Deuxièmement le modèle d’événements et de messagerie (WebMessageReceived, PostWebMessageAsString), prévu comme canal défini pour la communication App↔Web (Microsoft Learn : Communication entre l’hôte et le contenu web).
Source-Schnipsel: Host mit State-Machine, Token-Unhook und JSON-Bridge
Le code suivant n’est volontairement pas « le plus court possible », mais optimisé pour la sécurité d’exploitation. Point inhabituel, mais décisif en pratique : stocker EventRegistrationToken et, lors du Dispose, appeler explicitement remove_*. Cela réduit nettement le classique « callback après Destroy ».
Remarque : selon le setup Delphi vous utiliserez un import de la WebView2-TLB ou une Wrapper-Unit. Les signatures sont très similaires, mais certains noms de types peuvent varier.
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; // uniquement pour garder l’énumération visible dans le débogueur
raise EInvalidOperation.CreateFmt(‚WebView2Host : état invalide (%d).‘, [Ord(FState)]);
end;
end;
function TNbWebView2Host.EffectiveUserDataFolder: string;
begin
if FUserDataFolder <> “ then
Exit(FUserDataFolder);
// Plus stable, par Windows-User ; délibérément pas dans le dossier d’installation.
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
// Important : remove_* réduit les callbacks résiduels après 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;
// Ordre : d’abord détacher WebView/Controller, puis l’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
// Ne pas forcer la mise à jour de l’UI directement ici : dans la couche applicative, utiliser éventuellement TThread.Queue.
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 : activer une gestion personnalisée
// args.put_Handled(1);
finally
CoTaskMemFree(Uri);
CoTaskMemFree(Mime);
CoTaskMemFree(ResultPath);
end;
end;
end.
Ce qui rend cet extrait « remarquablement propre »
- Machine d’état plutôt qu’un booléen : wvsCreated → wvsInitializing → wvsReady → wvsDisposed. Cela rend les mauvaises utilisations visibles (Navigate trop tôt, Dispose en double).
- Désinscription explicite : EventRegistrationToken est stocké et supprimé dans Dispose. C’est un point souvent omis, qui apporte une vraie stabilité dans des clients existants.
- Choix délibéré du UserDataFolder : par utilisateur, en dehors du chemin d’installation. Ce n’est pas un « nice to have », mais un levier pour le support et l’exploitation.
Cas limites : Threading, DPI et cycle de vie des formulaires dans FMX
Callbacks et thread FMX
Même si de nombreux callbacks WebView2 semblent « proches de l’UI » : traitez chaque message de la bridge comme s’il provenait d’un contexte externe. La règle sûre : n’accédez à l’UI FMX que via TThread.Queue(nil, ...). Le concept d’architecture associé : affinité de thread signifie que les objets UI sont liés à un thread spécifique.
Redimensionnement et mise à l’échelle DPI
FMX travaille en coordonnées logiques, WebView2 attend des rectangles en pixels. Si votre application est mise à l’échelle par le DPI du moniteur (passage entre moniteurs 100%/150%), testez spécifiquement : redimensionnement lors du changement de moniteur et lors du passage en maximisé/RESTauré. On observe souvent des « scintillements d’un pixel », des tests de hit incorrects ou des bordures tronquées si la conversion n’est pas résolue de manière centralisée.
Téléchargements : conformité, emplacement et « pourquoi ResultFilePath est-il vide ? »
Les téléchargements WebView2 sont gérés via ICoreWebView2DownloadOperation. En production, deux points sont importants : premièrement, ResultFilePath n’est pas nécessairement renseigné de manière fiable dès le départ. Deuxièmement, de nombreuses entreprises veulent contrôler les emplacements de stockage (dossier projet, quarantaine, pas de prolifération « Downloads »).
Dans des clients matures, les règles typiques sont :
- Limite de taille et filtrage MIME (p. ex. bloquer « application/x-msdownload » selon la politique).
- Journalisation des métadonnées (URI, MIME, octets) comme trace d’audit ; ne pas enregistrer le contenu dans les logs.
- Éventuellement
args.Handled = Trueet chemin/nom personnalisé si les processus l’exigent.
Une appréciation rédactionnelle précise
Du point de vue de la rédaction et de l’exploitation, l’erreur la plus courante n’est pas « trop peu de connaissances WebView2 », mais l’absence de décision : on démarre avec un mini-prototype et on l’intègre tel quel dans un logiciel métier en production. WebView2 n’est alors pas le problème — c’est l’absence d’encapsulation et de stratégie d’exploitation.
Limites et risques : quand WebView2 dans FMX n’est pas la bonne option
- Multiplateforme : WebView2 est Windows. Si votre client FMX doit réellement fonctionner sur macOS/iOS/Android, prévoyez une abstraction de WebView ou séparez autrement l’UI web.
- Modèle de sécurité : dès lors que du contenu externe est chargé, vous avez besoin de RESTrictions de domaine, de politiques de navigation et d’un concept clair de mise à jour/runtime. Sinon vous introduisez une surface d’attaque indésirable dans le client.
- Facilité de support : Un UserDataFolder défini aide, mais peut aussi contenir des données à caractère personnel (cookies, Storage). Pour les cas de support, la protection des données et les processus doivent être clarifiés (p. ex. comment exporter, nettoyer ou anonymiser des dossiers).
Conclusion : Delphi WebView2 FMX comme composant d’intégration, pas comme gadget d’interface utilisateur
Si vous traitez Delphi WebView2 FMX comme un composant d’intégration — avec une state-machine, un Dispose propre, du Token-Unhooking, un WebView2 User Data Folder contrôlé et un JSON-Bridge versionnable —, cela devient un point de modernisation robuste pour des clients existants. Cela vaut particulièrement la peine si vous utilisez la technologie Web de manière ciblée pour des login-flows, des portails intégrés ou des vues d’administration, tout en maintenant délibérément la logique cœur dans Delphi.
Limite d’utilisation : dès que vous commencez à pousser via le bridge une logique métier « arbitraire » du Web vers Delphi, vous perdez en testabilité, en capacité de versionnement et, le cas échéant, le contrôle sur la sécurité et l’exploitation. À ce stade, il ne s’agit plus de WebView2 mais de discipline architecturale.
Pour ce sujet, les Delphi Fmx Edge Browser et Webview2 Firemonkey sont également importants. L’article replace ces aspects de manière claire et montre ce qui compte au quotidien.
Discuter d’un projet ou d’un chantier de modernisation avec Net-Base.
Étape suivante
Lorsque le sujet devient un projet réel, l'architecture, l'existant et l'exploitation doivent être examinés ensemble dès le départ.
Nous n'intervenons pas seulement sur des questions ponctuelles, mais aussi lorsque des fragments de code source, des problématiques liées aux systèmes legacy ou des concepts de portail doivent se transformer en un projet d'entreprise robuste.
- L'état des lieux, l'état cible et les risques techniques sont évalués conjointement.
- REST, l’accès aux données, les portails et le déploiement ne sont pas reportés à des phases ultérieures.
- Vous identifiez tôt quelle voie est viable économiquement et opérationnellement.