Net-Base Dergi

14.06.2026

Delphi WebView2 in FMX: düzgün başlatma, JS-Bridge oluşturma, indirmeler ve hata ayıklama kontrol altında

WebView2 in FireMonkey “basitçe tarayıcı gömme” gibi görünse de, pratikte başlatma, navigasyon olayları, JS↔Delphi köprüsü, indirme işlemleri ve hata ayıklamada aksıyor. Bu kaynak kod parçası, net sorumluluk ayrımıyla sağlam bir desen gösteriyor...

14.06.2026

Dergi konusundan proje pratiğine

İçeriğe Uygun Hizmet ve Teknik Sayfalar

Mevcut bir kurumsal yazılıma aniden „mal eben“ modern web içeriği gömmek isteyenler Windows için WebView2’ye yönelir. In Delphi WebView2 FMX içinde temel sorun nadiren bir URL’nin gösterilmesi; asıl mesele FireMonkey arayüzüne (FMX) düzgün gömme, güvenilir başlatma (asenkron ve COM tabanlı) ile Edge’e ilişkin User-Data dizinleri, indirmeler, hata ayıklama ve sağlam bir JS↔Delphi iletişimi etrafındaki tuzaklardır.

Bu Source-Schnipsel, bakım yapılabilir uygulamalar için tercih ettiğim bir modeli gösterir: WebView2 yaşam döngüsünü kontrol eden kapsüllenmiş bir „Host“ nesnesi ve rastgele her yerde ExecuteScript çalıştırmak yerine WebMessage (JSON) üzerinden tanımlı bir köprü. Amaç demo kodu değil; gelişmiş istemcilerde ayakta kalacak bir bileşendir.

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

WebView2 asenkron başlatmaya sahip COM/WinRT’e yakın bir API’dir. FireMonkey Windows handle’larını soyutlar, yine de WebView2 için sonunda gerçek bir Parent-Window (HWND) ve kontrollü yeniden boyutlandırma/odak iletimi gerekir. Aynı zamanda olaylar her zaman FMX’te beklendiği yerde çalışmayabilir. Burada „quick and dirty“ başlarsanız, tipik olarak şunlarla karşılaşırsınız:

  • Form kapatılırken aralıklı AVs (Callbacks Destroy’dan sonra gelir)
  • Navigasyon olayları yanlış bir thread bağlamından gelme
  • belirsiz UserDataFolder stratejisi nedeniyle güvenilmez persistenz/önbellek sorunları
  • indirmeler yok veya „takılan“ indirme diyalogları
  • hata ayıklama hedeflenmiş uzaktan hata ayıklama yapılandırması yerine şansa bağlı

Karşı önlem açık bir lifecycle’tır: Create → InitializeAsync → Attach → Navigate → Detach/Dispose – ve UI ile tarayıcı motoru arasında tanımlı bir sınır.

Source-Schnipsel: WebView2Host für Delphi WebView2 FMX

Aşağıdaki kod, (1) bir WebView2-Environment konfigürasyonu oluşturur, (2) controller nesnesini bir HWND’ye bağlar, (3) navigasyon ve indirme olaylarını bağlar ve (4) WebMessageReceived üzerinden JSON tabanlı bir JS köprüsü sunan kapsüllenmiş bir Host sınıfını taslak halinde gösterir. Kod kasıtlı olarak „architekturfähig“: COM referanslarını kapsüller, Destroy’dan sonra callback artçılarını engeller ve işletme sınırları olarak „kullanıcı başına“ veya „makine başına“ ayrı UserDataFolder’lara izin verir.

unit WebView2Host;

interface

uses
System.SysUtils, System.Classes, System.IOUtils, System.JSON,
Winapi.Windows, Winapi.ActiveX,
FMX.Types,
WebView2, WebView2_TLB; // kurulumunuza bağlı: WebView2.pas veya 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;

// Olay işleyicileri
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;

// Eventleri COM nesneleri serbest bırakılmadan önce çöz
UnhookEvents;

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

inherited;
end;

procedure TWebView2Host.EnsureNotDestroyed;
begin
if FDestroyed then
raise EInvalidOperation.Create(‚WebView2Host zaten yok edildi.‘);
end;

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

// Pratikte: uygulama başına ve Windows-kullanıcı başına, program dizininde değil
Result := TPath.Combine(TPath.GetHomePath, ‚AppDataLocalMyCompanyMyAppWebView2‘);
ForceDirectories(Result);
end;

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

UserData := MakeUserDataFolder;

// Options: buraya ekstra tarayıcı argümanları eklenebilir, örn. Remote-Debug
Opt := TCoreWebView2EnvironmentOptions.Create;

// Asenkron CreateEnvironment çağrısı
OleCheck(CreateCoreWebView2EnvironmentWithOptions(
nil, PWideChar(UserData), Opt,
TCoreWebView2CreateCoreWebView2EnvironmentCompletedHandler.Create(
procedure (errorCode: HRESULT; const createdEnvironment: ICoreWebView2Environment)
begin
if FDestroyed then Exit;
OleCheck(errorCode);

FEnvironment := createdEnvironment;

// Controller’ı Parent HWND’ye bağla
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;

// Başlangıçta görünür yap
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));

// Not: Güvenli bir şekilde Unhook etmek için tokenları saklayın.
// Birçok projede, host yalnızca form ile yaşıyorsa bu yeterli olur.
end;

procedure TWebView2Host.UnhookEvents;
begin
// Daha sağlam yöntem: tokenları saklayıp remove_* çağırın.
// Burada yorum olarak bırakıldı, çünkü import birimi kurulumu ve token yönetimi wrapper’a göre değişir.
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));

// Uygulamada: ResultFileName başlangıçta boş olabilir, kaynağa bağlı olarak.
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);

// İsteğe bağlı: kendi indirme UI’nız varsa, args.put_Handled(1) ile işaretleyin
// 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 henüz başlatılmamış.‘);

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.

Yaklaşımın Amacı

  • Lifecycle-Kapselung: FMX formu yalnızca „Initialize/Navigate/Resize“ bilir, COM ayrıntılarını bilmez.
  • Bridge mit Vertrag: JSON mesajları içinde name, isteğe bağlı cid (Correlation-ID) ve payload olması halinde bunlar bakımı yapılabilir ve test edilebilir.
  • İşletmeye uygun kalıcılık: kontrollü bir UserDataFolder, önbellek çakışmalarını, izin sorunlarını ve „geliştirici makinesinde çalışıyor, işletmede değil“ durumunu engeller.

JS↔Delphi-Köprüsü: warum WebMessage stabiler ist als ExecuteScript

WebView2 iletişim için birden fazla yol sunar. Pratikte ExecuteScript caziptir, ancak sürümlendirmesi zordur: stringleri bir yorumlayıcıya gönderirsiniz, net yanıt kanalları ve sağlam hata eşlemesi olmadan. Buna karşılık PostWebMessageAsString / WebMessageReceived tanımlı bir kanaldır.

Kurumsal ortamlarda sıkça görülen bir sınır durumu: bir Web-Frontend’ten (örn. dahili portal) bir Delphi iş akışını başlatmanız gerekir (yazdırma, cihaz erişimi, legacy entegrasyon). Bu durumda ihtiyacınız olanlar:

  • Mesaj adları için bir beyaz liste
  • asenkron yanıtlar için Correlation-ID’ler
  • Payloadları doğrulayan merkezi bir katman (örn. zorunlu alanlar, boyut limitleri)

Host tarafında bu OnWebMessageReceived yeridir. Asıl doğrulama bunun üstündeki bir katmanda (örn. Application-Service) olmalıdır, böylece UI/WebView2 teknolojisi ile iş mantığını ayırırsınız (klasik katmanlı mimari: UI → Application → Domain → Infrastruktur).

İndirmeler ve dosya depolama: işletmede sık sık sürpriz olan konular

WebView2’de indirmeler ICoreWebView2DownloadOperation üzerinden yürür. Kaynağa göre ResultFilePath erken dönemde boş olabilir ya da daha sonra atanabilir. Ayrıca pek çok şirket son kullanıcıların kontrolsüz klasörlere kaydetmesini istemez.

Kabul edilmiş desenler:

  • DownloadStarting olayını yakalayın ve args.put_Handled(1) ile UI kontrolünü kendiniz üstlenin (özel yol, adlandırma kuralı, karantina klasörü).
  • Dosya boyutu sınırları ve MIME tipi kontrolleri, „kazara 4 GB’lık log dosyası“ gibi durumları önlemek için.
  • Auditing: İndirme meta verilerini (URI, MIME, byte sayısı) logunuza yazın, içeriği değil.

Eğer düzenlenmiş süreçleriniz varsa (örn. onaylar, izlenebilirlik), olaylar üzerinden yapılan işlem tarayıcı dünyasını işletme kurallarınıza entegre edebileceğiniz tek noktadır.

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

WebView2 debug süreci sıklıkla durumların yeniden üretilememesi nedeniyle başarısız olur. İki ayar yardımcı olur:

  • DevTools’u etkinleştirme/devre dışı bırakma ICoreWebView2Settings üzerinden (kodda: SetDevToolsEnabled) – Release sürümlerinde genellikle kapalı, destek durumlarında hedefli olarak açılır.
  • Stabiles UserDataFolder: Destek ekibiniz bir hatayı yeniden üretmek istiyorsa tanımlı bir yol paha biçilmezdir. Klasörü yedekleyebilir/zip’leyebilir (Dikkat: Datenschutz/PII) ve durumları hedefli şekilde karşılaştırabilirsiniz.

İsteğe bağlı olarak (kullandığınız wrapper’a bağlı) EnvironmentOptions’a ek tarayıcı argümanları ekleyebilirsiniz, örn. bir Remote-Debug-Port. Bu, yerel geliştirici araçlarının olmadığı bir test sistemindeki uygulamayı analiz etmeniz gerektiğinde anlamlıdır. Sınırlar: üretim ortamlarında bunun düzgünce yetkilendirilip belgelenmesi gerekir; aksi halde gereksiz bir saldırı yüzeyi oluşturursunuz.

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

1) Kapatıldıktan Sonra Callbacks

Asenkron CompletedHandler’lar form zaten kapanmış olmasına rağmen gelebilir. Örnekte FDestroyed paylaşılan nesnelere erişimi engelliyor. Daha sağlam olması için ayrıca:

  • Olaylar için token’ları saklayın ve Destroy içinde temiz biçimde remove_* çağırın
  • InitializeAsync’e yalnızca bir kez izin verin (State-Machine: Created/Initializing/Ready/Disposed)

2) Thread-Kontext

Birçok handler “UI’ye yakın” gelir, ancak doğrudan FMX kontrollerine yazabileceğinize güvenmeyin. OnWebMessage içinde UI güncelliyorsanız, TThread.Queue(nil, ...) güvenli seçenek olacaktır. Ben ayrı tutmayı tercih ederim: Host olayı toplar, Application-Service karar verir, UI yalnızca Queue ile güncellenir.

3) DPI/Resize und FMX-Layouts

FMX mantıksal birimlerle hesap yapar, WebView2 ise piksel-rects bekler. Pratikte FMX kontrollerinin Bounds değerlerini gerçek piksele çevireceğiniz belirli bir nokta gerekir. Örnek bir TRect kabul ediyor; formunuzda buradan WinAPI koordinatlarını türetmelisiniz (ör. FMX.Platform.Win ve Handle-API’leri üzerinden). Uygulama monitör DPI’sına göre ölçekleniyorsa, monitörler arası geçişi test edin: WebView2 burada saf FMX kontrollerine göre daha hassastır.

WebView2’nin FMX’te ne zaman işe yaradığı — ne zaman yaramadığı

WebView2, gelişmiş bir Delphi istemci uygulamasında web teknolojisini hedefli olarak kullanmak istiyorsanız tercih edilir: gömülü yönetici görünümleri, OAuth/OIDC giriş akışları, HTML raporlar, dahili portallar veya kontrollü „Micro-Frontends“. Ayrıca modernizasyon köprüsü olarak pratiktir, ancak sorumlulukları net ayırdığınız ve köprüyü iş mantığı için kontrolsüz bir arka kapı haline getirmediğiniz sürece.

Yöntemin sınırları:

  • Plattform: Bu desen Windows-merkezlidir. FMX çoklu platform desteğine sahiptir, WebView2 ise değildir. macOS/iOS/Android için başka WebView’lara veya bir soyutlama katmanına ihtiyacınız olacaktır.
  • Security/Hardening: Dış içerikler yüklendiğinde navigasyon, izin verilen domain’ler ve indirme hedeflerini daha katı sınırlamanız gerekir. Bu, gereksinimler (Requirements) kısmında ele alınmalı, „sonra“ bırakılmamalıdır.
  • Support: UserDataFolder ve runtime bağımlılıkları (WebView2 Runtime) operasyon/rollout konseptinizin bir parçası olmalıdır.

Fazit

Delphi WebView2 FMX bir UI oyuncaktan ziyade kendi yaşam döngüsüne sahip bir entegrasyon bileşenidir. Başlatmayı, eventing’i, UserDataFolder ve JS-Bridge’i yapılandırılmış biçimde kapsüllediğinizde, WebView2 dijital kurumsal çözümler için stabil bir yapı taşı olur: Web-UI gerektiği yerde, Delphi mantığı ait olduğu yerde kalır. Buna karşın kontrolsüz script’ler çalıştırıyor, yolları rastgele bırakıyor ve eventleri ayrıştıramıyorsanız, zaman yiyen ve güveni zedeleyen „saha içinde aralıklı“ hatalarla karşılaşırsınız.

Mevcut bir Delphi uygulamasında WebView2’yi temiz entegre etmek veya bir modernizasyon eşiğini teknik olarak değerlendirmek istiyorsanız, bizimle konuşun:

Uzmanlık alanında entegrasyonlar, veri akışları ve devam eden geliştirme uyum içinde olması gerektiğinde Webview2 Firemonkey ve Delphi Fmx Edge Browser da önemli bir rol oynar.

Projeyi veya modernizasyon girişimini Net-Base ile görüşün.

Sonraki adım

Konu gerçek bir projeye dönüştüğünde, mimari, mevcut yapı ve işletme erken aşamada birlikte ele alınmalıdır.

Bireysel sorularda destek vermekle kalmıyoruz; kaynak kodu parçacıklarından, legacy konularından veya portal fikirlerinden sağlam bir kurumsal projeye dönüşene kadar da destek veriyoruz.

  • Mevcut durum, hedef durum ve teknik riskler birlikte değerlendirilir.
  • REST, veri erişimi, portallar ve Rollout sonraki işler olarak ertelenmez.
  • Hangi yolun ekonomik ve işletme açısından uygulanabilir olduğunu erken görürsünüz.

Gönderiyi paylaş

Bu gönderiyi doğrudan paylaş

LinkedIn, X, XING, Facebook, WhatsApp ve e-posta hemen kullanılabilir. Instagram için bağlantı ve kısa metni doğrudan hazırlıyoruz.

E-posta

Instagram yeni bir sekmede açılır. Bağlantı ve kısa metin önceden panoya kopyalanır.