Net-Base Časopis

14.06.2026

Delphi WebView2 u FMX-u: ispravno inicijalizovati, implementirati JS-Bridge, preuzimanja i debugging pod kontrolom

WebView2 u FireMonkey zvuči kao „samo ugraditi preglednik“, ali u praksi zataji pri inicijalizaciji, navigacionim događajima, JS↔Delphi-vezi, upravljanju preuzimanjima i otklanjanju grešaka. Ovaj isječak izvornog koda prikazuje robustan obrazac sa jasno definiranim odgovornostima...

14.06.2026

Od teme magazina do projektne prakse

Povezane stranice usluga i tehnologije za članak

Ko želi u postojećoj poslovnoj softverskoj aplikaciji odjednom „maltene brzo“ ugraditi moderne web-sadržaje, na Windows će naići kod WebView2. U Delphi WebView2 FMX osnovni problem rijetko jeste samo prikaz URL-a, već čista integracija u FireMonkey-površinu (FMX), pouzdano inicijaliziranje (asinkrono i COM-bazirano), te Edge-poteškoće oko User-Data direktorija, preuzimanja, debagiranja i robusne JS↔Delphi komunikacije.

Ovaj source-ispis pokazuje obrazac koji preferiram za održive aplikacije: enkapsulirani „Host“-objekt koji kontrolira WebView2-Lifecycle, te definirani bridge preko WebMessage (JSON), umjesto proizvoljnog „ExecuteScript svugdje“. Cilj nije demo-kod, već građevni blok koji preživi u zrelim klijentima.

Warum WebView2 in FMX anders ist als „Browser-Component drop“

WebView2 je COM/WinRT- bliska API sa asinkronom inicijalizacijom. FireMonkey apstrahuje Windows-handlee, ipak za WebView2 na kraju trebate pravo parent-window (HWND) i kontrolirano prosljeđivanje promjena veličine/fokusa. Istovremeno se događaji ne izvršavaju uvijek tamo gdje ih u FMX očekujete. Ako ovdje krenete „quick and dirty“, tipično ćete dobiti:

  • sporadične AV-e pri zatvaranju forme (Callbacks stižu nakon Destroy)
  • Navigation-Events u pogrešnom kontekstu niti
  • nepouzdanu persistenciju/cache-probleme zbog nejasne UserDataFolder-strategije
  • nema preuzimanja ili „zaglavljeni“ dijalozi za download
  • debagiranje samo slučajno umjesto ciljane konfiguracije za remote-Debug

Protivotrov je jasan Lifecycle: Create → InitializeAsync → Attach → Navigate → Detach/Dispose – i definirana granica između UI i browser-engine.

Source-Schnipsel: WebView2Host für Delphi WebView2 FMX

Sljedeći kod skicira enkapsuliranu host-klasu koja (1) stvara WebView2-Environment-konfiguraciju, (2) veže controller-objekt za HWND, (3) žiča Navigation i Download događaje i (4) nudi JSON-bazirani JS-bridge preko WebMessageReceived. Kod je namjerno „architekturfähig“: enkapsulira COM-referencije, sprječava callback-naslijeđivanje nakon Destroy, i omogućava operativne rubove poput odvojenih UserDataFoldera „po korisniku“ ili „po mašini“.

unit WebView2Host;

interface

uses
System.SysUtils, System.Classes, System.IOUtils, System.JSON,
Winapi.Windows, Winapi.ActiveX,
FMX.Types,
WebView2, WebView2_TLB; // ovisno o postavkama: WebView2.pas ili 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 može nedostajati ili biti null
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;

// Uklonite događaje prije nego što se COM objekti oslobode
UnhookEvents;

FWebView := nil;
FController := nil;
FEnvironment := nil;

inherited;
end;

procedure TWebView2Host.EnsureNotDestroyed;
begin
if FDestroyed then
raise EInvalidOperation.Create(‚WebView2Host je već uništen.‘);
end;

function TWebView2Host.MakeUserDataFolder: string;
begin
if FUserDataFolder <> “ then
Exit(FUserDataFolder);

// U praksi: po aplikaciji + po Windows-korisniku, ne u direktoriju programa
Result := TPath.Combine(TPath.GetHomePath, ‚AppDataLocalMyCompanyMyAppWebView2‘);
ForceDirectories(Result);
end;

procedure TWebView2Host.InitializeAsync;
var
UserData: string;
Opt: ICoreWebView2EnvironmentOptions;
begin
EnsureNotDestroyed;

UserData := MakeUserDataFolder;

// Opcije: ovdje mogu ići dodatni argumenti preglednika, npr. 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;

// Povezivanje kontrolera na roditeljski HWND
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;

// Inicijalno prikazati
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));

// Napomena: Za robusno uklanjanje veza trebali biste pohraniti tokene.
// U mnogim projektima je to dovoljno ako host živi istim životnim ciklusom kao forma.
end;

procedure TWebView2Host.UnhookEvents;
begin
// Robusna varijanta: zapamtite tokene i pozovite remove_*.
// Ovdje kao komentar, jer se setup import-unit i upravljanje tokenima razlikuju ovisno o wrapperu.
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));

// U praksi: ResultFileName je inicijalno prazan, ovisno o izvoru.
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);

// Opcionalno: vlastiti UI preuzimanja, tada postavite Handled
// 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 još nije inicijalizovan.‘);

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.

Svrha pristupa

  • Inkapsulacija životnog ciklusa: FMX-forma poznaje samo „Initialize/Navigate/Resize“, ne COM-detalje.
  • Bridge s ugovorom: JSON-poruke s name, opcionalno cid (Correlation-ID) i payload su održive i testabilne.
  • Operativno sigurna persistencija: kontrolirani UserDataFolder sprječava kolizije cache-a, probleme s pravima i „radi na računarima developera, a ne u produkciji“.

JS↔Delphi-Bridge: warum WebMessage stabiler ist als ExecuteScript

WebView2 nudi više puteva komunikacije. U praksi je ExecuteScript primamljiv, ali se teško verzionira: gurate stringove u interpreter bez jasnih kanala za odgovore i bez robusnog mapiranja grešaka. PostWebMessageAsString / WebMessageReceived su, nasuprot tome, definirani kanal.

Rubni slučaj koji se često javlja u poslovnim okruženjima: morate iz web-frontenda (npr. internog portala) pokrenuti Delphi-workflow (ispis, pristup uređajima, integracija sa naslijeđenim sustavima). Tada vam trebaju:

  • bijela lista naziva poruka
  • Correlation-ID-e za asinkrone odgovore
  • centralno mjesto koje validira payloadove (npr. obavezna polja, ograničenja veličine)

U hostu je to mjesto OnWebMessageReceived. Stvarna validacija pripada višem sloju (npr. Application-Service), kako biste držali UI-/WebView2-tehniku i poslovnu logiku odvojeno (klasična slojevita arhitektura: UI → Application → Domain → Infrastruktur).

Downloads und Dateiablage: was im Betrieb oft überrascht

Preuzimanja u WebView2 prolaze preko ICoreWebView2DownloadOperation. Ovisno o izvoru, ResultFilePath može biti rano prazan ili postavljen tek kasnije. Osim toga, mnoge kompanije ne žele da krajnji korisnici spremaju u nekontrolisane foldere.

Provjereni obrasci:

  • Presretanje DownloadStarting i preuzimanje kontrole nad UI putem args.put_Handled(1) (sopstveni put, konvencija imenovanja, mapa za karantenu).
  • Ograničenja veličine datoteka i provjere MIME-tipova, kako biste izbjegli slučajno preuzimanje 4 GB log fajla.
  • Auditing: upišite metapodatke preuzimanja (URI, MIME, bajtovi) u logging, ne sadržaj.

Ako imate regulirane procese (npr. odobrenja, mogućnost praćenja/auditabilnost), rukovanje putem događaja je jedino mjesto na kojem možete integrirati svijet preglednika u vaše operativne pravila.

Debugging: DevTools, Remote Debug Port und reproduzierbare Zustände

Debugiranje WebView2 često poklekne zato što stanja nisu ponovljiva. Dva podešavanja pomažu:

  • Aktiviranje/deaktiviranje DevTools preko ICoreWebView2Settings (u kodu: SetDevToolsEnabled) – u releaseu često isključeno, u slučaju podrške ciljano uključeno.
  • Stabilan UserDataFolder: ako vaša podrška treba rekonstruirati grešku, definisani put vrijedi zlata. Možete folder rezervno kopirati/zipovati (Pažnja: zaštita podataka/PII) i ciljano uspoređivati stanja.

Opcionalno (ovisno o wrapperu) možete EnvironmentOptions opremiti dodatnim argumentima za preglednik, npr. Remote-Debug-Port. To ima smisla ako trebate analizirati aplikaciju na testnom sistemu bez lokalnih razvojnih alata. Granice: u produkcijskim okruženjima to mora biti uredno odobreno i dokumentirano, inače otvarate nepotreban vektor napada.

Stolperfallen in Delphi WebView2 FMX: COM, Threads und Form-Lifecycle

1) Callbacks nach dem Schließen

Asynchrone CompletedHandler können eintreffen, nachdem die Form bereits schließt. Im Snippet verhindert FDestroyed den Zugriff auf freigegebene Objekte. Robuster ist zusätzlich:

  • Tokeni für Events speichern und in Destroy sauber remove_* 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 die UI aktualisieren, ist TThread.Queue(nil, ...) die sichere Variante. Ich trenne gern: Host sammelt das Ereignis, Application-Service entscheidet, die 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.

Sljedeći korak

Ako se tema pretvori u stvarni projekat, arhitekturu, postojeći sistem i operacije trebalo bi rano zajednički razmotriti.

Pružamo podršku ne samo pri pojedinačnim pitanjima, već i kada iz fragmenata izvornog koda, naslijeđenih sistema ili ideja za portal treba nastati robustan poslovni projekat.

  • Postojeće stanje, ciljno stanje i tehnički rizici procjenjuju se zajedno.
  • REST, pristup podacima, portali i Rollout neće se odgađati za kasnije faze.
  • Pravovremeno prepoznajete koji pristup je ekonomski i operativno održiv.

Podijeli objavu

Ovu objavu direktno proslijediti

LinkedIn, X, XING, Facebook, WhatsApp i e-pošta su odmah dostupni. Za Instagram pripremamo link i kratak tekst.

E-pošta

Instagram se otvara u novom tabu. Link i kratak tekst se prethodno kopiraju u međuspremnik.