Od tematu magazynowego do praktyki projektowej
Pasujące strony usługowe i techniczne do artykułu
Kto w istniejącym oprogramowaniu biznesowym nagle chce „mal eben” osadzić nowoczesne treści webowe, często trafia na Windows z WebView2. W Delphi WebView2 FMX WebView2 FMX podstawowy problem rzadko polega na wyświetleniu adresu URL, a na poprawnym osadzeniu w interfejsie FireMonkey (FMX), niezawodnym inicjalizowaniu (asynchronicznym i opartym na COM) oraz pułapkach związanych z Edge dotyczącymi katalogów User-Data, pobieraniami, debugowaniem i solidną komunikacją JS↔Delphi.
Ten fragment kodu źródłowego pokazuje wzorzec, który preferuję dla aplikacji łatwych w utrzymaniu: opakowany obiekt „Host”, który kontroluje cykl życia WebView2, oraz zdefiniowany most przez WebMessage (JSON), zamiast dowolnego „ExecuteScript wszędzie”. Celem nie jest kod demonstracyjny, lecz komponent, który przetrwa w rozrośniętych klientach.
Dlaczego WebView2 w FMX różni się od „wstawienia komponentu przeglądarki”
WebView2 to API bliskie COM/WinRT z asynchroniczną inicjalizacją. FireMonkey abstraktuje uchwyty Windows, jednak ostatecznie dla WebView2 potrzebne jest prawdziwe okno nadrzędne (HWND) oraz kontrolowane przekazywanie zmiany rozmiaru i fokusu. Równocześnie zdarzenia nie zawsze występują tam, gdzie się ich w FMX spodziewamy. Jeśli podejdziesz do tego „quick and dirty”, zwykle otrzymasz:
- sporadyczne AVs przy zamykaniu formularza (callbacki trafiają po Destroy)
- zdarzenia nawigacji wykonywane w niewłaściwym kontekście wątku
- niedeterministyczna trwałość/cache z powodu niejasnej strategii UserDataFolder
- brak pobrań lub „zawieszone” dialogi pobierania
- debugowanie dostępne tylko przypadkiem zamiast przez ukierunkowaną konfigurację zdalnego debugowania
Środkiem zaradczym jest jasny lifecycle: Create → InitializeAsync → Attach → Navigate → Detach/Dispose – oraz zdefiniowana granica między UI a silnikiem przeglądarki.
Fragment kodu: WebView2Host für Delphi WebView2 FMX
Następujący kod schematycznie przedstawia opakowaną klasę hosta, która (1) tworzy konfigurację środowiska WebView2, (2) wiąże obiekt kontrolera z HWND, (3) łączy zdarzenia nawigacji i pobierania oraz (4) udostępnia oparte na JSON most JS za pomocą WebMessageReceived. Kod jest świadomie „architekturfähig”: kapsułkuje referencje COM, zapobiega wywołaniom zwrotnym pojawiającym się po Destroy oraz pozwala na separację katalogów UserDataFolder dla scenariuszy operacyjnych takich jak „pro User” lub „pro Maschine”.
unit WebView2Host;
interface
uses
System.SysUtils, System.Classes, System.IOUtils, System.JSON,
Winapi.Windows, Winapi.ActiveX,
FMX.Types,
WebView2, WebView2_TLB; // w zależności od konfiguracji: WebView2.pas lub 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 być nieobecny lub mieć wartość 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;
// Odłączyć zdarzenia przed zwolnieniem obiektów COM
UnhookEvents;
FWebView := nil;
FController := nil;
FEnvironment := nil;
inherited;
end;
procedure TWebView2Host.EnsureNotDestroyed;
begin
if FDestroyed then
raise EInvalidOperation.Create(‚WebView2Host został już zniszczony.‘);
end;
function TWebView2Host.MakeUserDataFolder: string;
begin
if FUserDataFolder <> “ then
Exit(FUserDataFolder);
// W praktyce: na aplikację i na użytkownika Windows, nie w katalogu programu
Result := TPath.Combine(TPath.GetHomePath, ‚AppDataLocalMyCompanyMyAppWebView2‘);
ForceDirectories(Result);
end;
procedure TWebView2Host.InitializeAsync;
var
UserData: string;
Opt: ICoreWebView2EnvironmentOptions;
begin
EnsureNotDestroyed;
UserData := MakeUserDataFolder;
// Opcje: tutaj można dodać dodatkowe argumenty przeglądarki, np. Remote-Debug
Opt := TCoreWebView2EnvironmentOptions.Create;
// Asynchroniczne tworzenie środowiska
OleCheck(CreateCoreWebView2EnvironmentWithOptions(
nil, PWideChar(UserData), Opt,
TCoreWebView2CreateCoreWebView2EnvironmentCompletedHandler.Create(
procedure (errorCode: HRESULT; const createdEnvironment: ICoreWebView2Environment)
begin
if FDestroyed then Exit;
OleCheck(errorCode);
FEnvironment := createdEnvironment;
// Powiązanie kontrolera z HWND rodzica
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;
// Ustawienie widoczności początkowej
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));
// Uwaga: dla solidnego odpinania zdarzeń należy przechowywać tokeny.
// W wielu projektach jest to wystarczające, gdy host współistnieje z formularzem.
end;
procedure TWebView2Host.UnhookEvents;
begin
// Odporna wersja: zapamiętać tokeny i wywołać remove_*.
// Tu jako komentarz, ponieważ konfiguracja jednostki importu i zarządzanie tokenami zależą od wrappera.
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));
// W praktyce: ResultFileName początkowo pusty, zależnie od źródła.
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);
// Opcjonalnie: własny interfejs pobierania, wtedy ustawić 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 nie został jeszcze zainicjalizowany.‘);
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.
Cel podejścia
- Kapsułkowanie cyklu życia: formularz FMX zna tylko „Initialize/Navigate/Resize”, nie szczegóły COM.
- Most z kontraktem: komunikaty JSON z
name, opcjonalniecid(Correlation-ID) ipayloadsą łatwe w utrzymaniu i testowalne. - Persistencja niezawodna w eksploatacji: kontrolowany
UserDataFolderzapobiega kolizjom pamięci podręcznej, problemom z uprawnieniami i sytuacjom „działa na komputerze dewelopera, nie w środowisku produkcyjnym”.
JS↔Delphi-Bridge: warum WebMessage stabiler ist als ExecuteScript
WebView2 oferuje kilka kanałów komunikacji. W praktyce ExecuteScript kusi prostotą, ale jest trudny do wersjonowania: wrzucacie ciągi znaków do interpretera bez wyraźnych kanałów odpowiedzi i bez solidnego mapowania błędów. PostWebMessageAsString / WebMessageReceived to zdefiniowany kanał.
Krawędź przypadków, która często pojawia się w środowiskach korporacyjnych: musicie z frontendu webowego (np. portalu wewnętrznego) uruchomić workflow Delphi (druk, dostęp do urządzeń, integracja z legacy). Wtedy potrzebujecie:
- whitelistę nazw komunikatów
- Correlation-ID dla odpowiedzi asynchronicznych
- centralne miejsce walidujące payloady (np. pola obowiązkowe, limity rozmiaru)
W hoście to punkt OnWebMessageReceived. Rzeczywista walidacja powinna być w warstwie wyżej (np. Application-Service), aby utrzymać separację technologii UI/WebView2 i logiki biznesowej (klasyczna architektura warstwowa: UI → Application → Domain → Infrastruktur).
Downloads und Dateiablage: was im Betrieb oft überrascht
Pobierania w WebView2 obsługiwane są przez ICoreWebView2DownloadOperation. W zależności od źródła ResultFilePath może być na początku pusty lub ustawiany dopiero później. Dodatkowo wiele firm nie chce, aby użytkownicy końcowi zapisywali pliki w niekontrolowane miejsca.
Sprawdzone wzorce:
- Przechwytywanie DownloadStarting i przejęcie obsługi w UI przez
args.put_Handled(1)(własna ścieżka, konwencja nazewnictwa, katalog kwarantanny). - Limity rozmiaru plików i kontrole typu MIME, aby uniknąć „przez przypadek 4 GB pliku logu”.
- Audyt: zapisywać metadane pobierania (URI, MIME, bajty) do logów, nie zawartość.
Jeśli macie procesy regulowane (np. zatwierdzenia, pełna ścieżka audytu), obsługa przez eventy to jedyne miejsce, w którym możecie włączyć świat przeglądarki w zasady operacyjne.
Debugging: DevTools, Remote Debug Port und reproduzierbare Zustände
Debugowanie WebView2 często zawodzi, bo stany nie są odtwarzalne. Pomagają dwie dźwignie:
- Włączanie/wyłączanie DevTools przez
ICoreWebView2Settings(w kodzie:SetDevToolsEnabled) – w release zwykle wyłączone, w przypadku wsparcia włączane celowo. - Stabilny UserDataFolder: jeśli wsparcie ma odtworzyć błąd, zdefiniowana ścieżka jest na wagę złota. Możecie zabezpieczyć/spakować folder (Uwaga: ochrona danych/osobowe dane identyfikacyjne/PII) i porównywać stany precyzyjnie.
Opcjonalnie (w zależności od wrappera) możecie przekazać EnvironmentOptions z dodatkowymi argumentami przeglądarki, np. zdalny port debugowania. Ma to sens, gdy analizujecie aplikację na systemie testowym bez lokalnych narzędzi deweloperskich. Ograniczenia: w środowiskach produkcyjnych musi to być wyraźnie autoryzowane i udokumentowane, inaczej tworzycie niepotrzebny wektor ataku.
Stolperfallen in Delphi WebView2 FMX: COM, Threads und Form-Lifecycle
1) Wywołania zwrotne po zamknięciu
Asynchroniczne CompletedHandler mogą nadejść po tym, jak forma już się zamyka. W przykładzie FDestroyed zapobiega dostępowi do zwolnionych obiektów. Dodatkowo bardziej odporne jest:
- Przechowywać tokeny zdarzeń i w
Destroypoprawnie wywołaćremove_* - Zezwolić na InitializeAsync tylko raz (maszyna stanów: Created/Initializing/Ready/Disposed)
2) Thread-Kontext
Wiele handlerów pojawia się co prawda „UI-nah“, ale nie polegaj na tym, że możesz bezpośrednio pisać do kontrolek FMX. Jeśli w OnWebMessage aktualizujesz UI, bezpieczną opcją jest TThread.Queue(nil, ...). Ja chętnie rozdzielam: Host zbiera zdarzenie, Application-Service decyduje, UI jest aktualizowane wyłącznie przez Queue.
3) DPI/Resize und FMX-Layouts
FMX operuje w jednostkach logicznych, WebView2 oczekuje Pixel-Rects. W praktyce potrzebujesz wyraźnego miejsca, w którym tłumaczysz Bounds kontrolek FMX na rzeczywiste piksele. Fragment przyjmuje TRect; w Twojej formie powinieneś z tego wyprowadzić współrzędne WinAPI (np. przez FMX.Platform.Win i Handle-APIs). Jeśli aplikacja skaluje się według DPI monitora, testuj przełączanie między monitorami: WebView2 jest tu bardziej wrażliwy niż czyste kontrolki FMX.
Kiedy warto użyć WebView2 w FMX — a kiedy nie
WebView2 ma sens, gdy w rozbudowanej Delphi-aplikacji klienckiej chcesz celowo wykorzystywać technologię webową: osadzone widoki administracyjne, przepływy logowania OAuth/OIDC, raporty HTML, portale wewnętrzne lub kontrolowane „Micro-Frontends“. Jako most modernizacyjny też jest praktyczne, o ile wyraźnie rozdzielisz odpowiedzialności i nie pozwolisz, by bridge stał się niekontrolowanym tylnym wejściem dla logiki biznesowej.
Ograniczenia podejścia:
- 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: Gdy ładujesz treści z zewnątrz, musisz mocniej ograniczyć nawigację, dozwolone domeny i cele pobrań. To należy ująć w wymaganiach, nie „później“.
- Support: UserDataFolder i zależności runtime (WebView2 Runtime) muszą być częścią Twojej koncepcji operacyjnej/wdrożeniowej.
Wnioski
Delphi WebView2 FMX to nie tyle gadżet UI, co komponent integracyjny z własnym lifecycle. Jeśli zainicjalizowanie, eventing, UserDataFolder i JS-Bridge opakujesz strukturalnie, WebView2 stanie się stabilnym elementem rozwiązań korporacyjnych: Web-UI tam, gdzie ma sens, i Delphi-logika tam, gdzie jej miejsce. Jeśli zaś bez kontroli wykonujesz skrypty, pozostawiasz ścieżki przypadkowi i nie odłączasz zdarzeń, otrzymasz dokładnie ten rodzaj „sporadycznych w terenie“ błędów, które pochłaniają czas i podważają zaufanie.
Jeśli chcesz w istniejącej Delphi-aplikacji czysto zintegrować WebView2 lub technicznie ocenić krawędź modernizacji, porozmawiaj z nami:
W kontekście merytorycznym ważną rolę odgrywają także Webview2 Firemonkey i Delphi Fmx Edge Browser, gdy integracje, przepływy danych i dalszy rozwój muszą współgrać w sposób uporządkowany.
Następny krok
Gdy temat stanie się rzeczywistym projektem, architekturę, stan istniejący i eksploatację należy wcześnie rozpatrywać wspólnie.
Wspieramy nie tylko w pojedynczych zagadnieniach, lecz także wtedy, gdy z fragmentów kodu źródłowego, kwestii związanych z systemami legacy lub koncepcji portalu ma powstać solidny projekt dla przedsiębiorstwa.
- Stan istniejący, obraz docelowy i ryzyka techniczne są oceniane łącznie.
- REST, dostęp do danych, portale i Rollout nie są odkładane na później.
- Wcześnie widzą Państwo, która droga jest ekonomicznie opłacalna i operacyjnie wykonalna.