Από το θέμα του περιοδικού στην πρακτική εφαρμογή του έργου
Σχετικές σελίδες υπηρεσιών και τεχνολογίας για το άρθρο
Όποιος σε μια υπάρχουσα επιχειρησιακή λογισμική ξαφνικά θέλει «απλά» να ενσωματώσει σύγχρονο web περιεχόμενο, καταλήγει στο Windows με το WebView2. Σε Delphi WebView2 FMX το βασικό πρόβλημα σπάνια είναι η εμφάνιση ενός URL, αλλά η καθαρή ενσωμάτωση σε μια επιφάνεια FireMonkey (FMX), η αξιόπιστη αρχικοποίηση (ασύγχρονη και βασισμένη σε COM), καθώς και οι παγίδες του Edge γύρω από καταλόγους User-Data, λήψεις, debugging και μια ανθεκτική JS↔Delphi-επικοινωνία.
Αυτό το απόσπασμα κώδικα δείχνει ένα μοτίβο που προτιμώ για εφαρμογές εύκολες στη συντήρηση: μια ενθυλακωμένη κλάση «Host» που ελέγχει τον κύκλο ζωής του WebView2, καθώς και μια ορισμένη γέφυρα μέσω WebMessage (JSON), αντί για ExecuteScript παντού. Σκοπός δεν είναι κώδικας επίδειξης, αλλά ένα δομικό στοιχείο που επιβιώνει σε ώριμους clients.
Γιατί το WebView2 στο FMX είναι διαφορετικό από ένα «Browser-Component drop»
Το WebView2 είναι μια API κοντά σε COM/WinRT με ασύγχρονη αρχικοποίηση. Το FireMonkey αφαιρεί τα Windows-handles, ωστόσο στο τέλος για το WebView2 χρειάζεστε ένα πραγματικό Parent-Window (HWND) και ελεγχόμενη προώθηση αλλαγών μεγέθους/εστίασης. Ταυτόχρονα, τα γεγονότα δεν εκτελούνται πάντα εκεί όπου τα περιμένετε στο FMX. Αν ξεκινήσετε εδώ «quick and dirty», τυπικά θα αντιμετωπίσετε:
- σποραδικά AVs κατά το κλείσιμο της φόρμας (Callbacks φτάνουν μετά το Destroy)
- Navigation-Events από λάθος context νήματος
- μη αξιόπιστη επιμονή/προβλήματα cache λόγω ασαφούς στρατηγικής UserDataFolder
- καμία λήψη ή «κολλημένοι» διάλογοι λήψης
- Debugging μόνο με τύχη αντί για στοχευμένη ρύθμιση Remote-Debug
Το αντίδοτο είναι ένας σαφής κύκλος ζωής: Create → InitializeAsync → Attach → Navigate → Detach/Dispose – και ένα ορισμένο όριο μεταξύ UI και Browser-Engine.
Απόσπασμα κώδικα: WebView2Host για Delphi WebView2 FMX
Ο παρακάτω κώδικας σκιαγραφεί μια ενθυλακωμένη κλάση Host που (1) δημιουργεί μια WebView2-Environment-διαμόρφωση, (2) δένει το Controller-αντικείμενο σε ένα HWND, (3) καλωδιώνει τα Navigation και Download-Events και (4) προσφέρει μια JSON-βασισμένη JS-γέφυρα μέσω του WebMessageReceived. Ο κώδικας είναι σκόπιμα «architekturfähig»: ενθυλακώνει COM-αναφορές, αποτρέπει callbacks μετά το Destroy, και επιτρέπει όρια λειτουργίας όπως ξεχωριστούς UserDataFolder «ανά χρήστη» ή «ανά μηχανή».
unit WebView2Host;
interface
uses
System.SysUtils, System.Classes, System.IOUtils, System.JSON,
Winapi.Windows, Winapi.ActiveX,
FMX.Types,
WebView2, WebView2_TLB; // ανάλογα με το setup: WebView2.pas ή 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 μπορεί να λείπει ή να είναι 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;
// Αποσύνδεση συμβάντων πριν απελευθερωθούν τα αντικείμενα COM
UnhookEvents;
FWebView := nil;
FController := nil;
FEnvironment := nil;
inherited;
end;
procedure TWebView2Host.EnsureNotDestroyed;
begin
if FDestroyed then
raise EInvalidOperation.Create(‚Το WebView2Host έχει ήδη καταστραφεί.‘);
end;
function TWebView2Host.MakeUserDataFolder: string;
begin
if FUserDataFolder <> “ then
Exit(FUserDataFolder);
// Στην πράξη: ανά εφαρμογή + ανά χρήστη Windows, όχι στο φάκελο του προγράμματος
Result := TPath.Combine(TPath.GetHomePath, ‚AppDataLocalMyCompanyMyAppWebView2‘);
ForceDirectories(Result);
end;
procedure TWebView2Host.InitializeAsync;
var
UserData: string;
Opt: ICoreWebView2EnvironmentOptions;
begin
EnsureNotDestroyed;
UserData := MakeUserDataFolder;
// Επιλογές: εδώ μπορούν να προστεθούν επιπλέον παραμέτροι του browser, π.χ. 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;
// Σύνδεση του controller με το parent 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;
// Κάνουμε το αρχικά ορατό
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));
// Σημείωση: Για αξιόπιστο αποσύνδεσμο θα πρέπει να αποθηκεύετε τα tokens.
// Σε πολλά έργα αυτό είναι επαρκές εάν ο host ζει μαζί με τη φόρμα.
end;
procedure TWebView2Host.UnhookEvents;
begin
// Ανθεκτική μέθοδος: αποθηκεύστε τα tokens και καλέστε remove_*.
// Εδώ ως σχόλιο, επειδή το setup της import-unit και η διαχείριση των tokens ποικίλλουν ανά wrapper.
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));
// Στην πράξη: το ResultFileName μπορεί αρχικά να είναι κενό, ανάλογα με την πηγή.
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);
// Προαιρετικά: δική σας UI για λήψη, τότε ορίστε το 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 δεν έχει ακόμη αρχικοποιηθεί.‘);
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.
Σκοπός της προσέγγισης
- Lifecycle-Kapselung: Η φόρμα FMX γνωρίζει μόνο „Initialize/Navigate/Resize“, όχι λεπτομέρειες COM.
- Bridge mit Vertrag: Τα μηνύματα JSON με
name, προαιρετικάcid(Correlation-ID) καιpayloadείναι συντηρήσιμα και ελεγξιμα. - Betriebssichere Persistenz: ένας ελεγχόμενος
UserDataFolderαποτρέπει συγκρούσεις cache, προβλήματα δικαιωμάτων και το «läuft auf Entwicklerrechner, nicht im Betrieb».
JS↔Delphi-Bridge: warum WebMessage stabiler ist als ExecuteScript
WebView2 προσφέρει πολλαπλούς δρόμους επικοινωνίας. Στην πράξη το ExecuteScript είναι δελεαστικό, αλλά δύσκολο στη διαχείριση εκδόσεων: ωθείτε strings σε έναν διερμηνέα χωρίς σαφείς κανάλια απάντησης και χωρίς ανθεκτικό mapping σφαλμάτων. Το PostWebMessageAsString / WebMessageReceived είναι αντ‘ αυτού ένας ορισμένος κανάλι.
Μια οριακή περίπτωση που εμφανίζεται συχνά σε εταιρικά περιβάλλοντα: πρέπει από ένα Web-Frontend (π.χ. εσωτερικό Portal) να ξεκινήσετε έναν Delphi-workflow (εκτύπωση, πρόσβαση σε συσκευές, ενσωμάτωση legacy). Τότε χρειάζεστε:
- μια λευκή λίστα με ονόματα μηνυμάτων
- Correlation-IDs για ασύγχρονες απαντήσεις
- μια κεντρική θέση που επικυρώνει τα Payloads (π.χ. υποχρεωτικά πεδία, όρια μεγέθους)
Στον Host αυτό είναι το σημείο OnWebMessageReceived. Η πραγματική επικύρωση ανήκει σε ένα ανώτερο στρώμα (π.χ. Application-Service), ώστε να διατηρείτε τη τεχνική UI/WebView2 και τη business-λογική διαχωρισμένες (κλασική αρχιτεκτονική στρωμάτων: UI → Application → Domain → Υποδομή).
Downloads und Dateiablage: was im Betrieb oft überrascht
Οι λήψεις στο WebView2 γίνονται μέσω ICoreWebView2DownloadOperation. Ανάλογα με την πηγή, το ResultFilePath μπορεί αρχικά να είναι κενό ή να οριστεί αργότερα. Επιπλέον, πολλές εταιρείες δεν θέλουν οι τελικοί χρήστες να αποθηκεύουν σε μη ελεγχόμενους φακέλους.
Βέλτιστες πρακτικές:
- DownloadStarting abfangen και με
args.put_Handled(1)να αναλάβει η UI την επεξεργασία (δικός σας φάκελος, σύμβαση ονοματολογίας, φάκελος καραντίνας). - Όρια μεγέθους αρχείων και έλεγχοι MIME-Type, για να αποφευχθεί ένα «versehentlich 4 GB Logfile».
- Auditing: Καταγράψτε τα μεταδεδομένα της λήψης (URI, MIME, Bytes) στο logging σας, όχι το περιεχόμενο.
Εάν έχετε ρυθμισμένες διαδικασίες (π.χ. εγκρίσεις, ιχνηλασιμότητα), ο χειρισμός μέσω των events είναι το μοναδικό σημείο όπου μπορείτε να ενσωματώσετε τον κόσμο του browser στους επιχειρησιακούς κανόνες σας.
Debugging: DevTools, Remote Debug Port und reproduzierbare Zustände
Η αποσφαλμάτωση WebView2 συχνά αποτυγχάνει επειδή οι καταστάσεις δεν είναι αναπαραγώγιμες. Δύο ρυθμίσεις βοηθούν:
- Ενεργοποίηση/απενεργοποίηση DevTools μέσω
ICoreWebView2Settings(στον κώδικα:SetDevToolsEnabled) – στο Release συχνά απενεργοποιημένο, σε περίπτωση υποστήριξης ενεργοποιείται στοχευμένα. - Σταθερό UserDataFolder: Όταν η υποστήριξή σας πρέπει να αναπαράγει ένα σφάλμα, ένας καθορισμένος κατάλογος είναι ανεκτίμητος. Μπορείτε να ασφαλίσετε/συμπιέσετε τον φάκελο (Προσοχή: προστασία δεδομένων/PII) και να συγκρίνετε στοχευμένα καταστάσεις.
Προαιρετικά (ανάλογα με τον Wrapper) μπορείτε να ρυθμίσετε τα EnvironmentOptions με πρόσθετα επιχειρήματα του browser, π.χ. έναν Remote-Debug-Port. Αυτό είναι χρήσιμο όταν πρέπει να αναλύσετε μια εφαρμογή σε ένα test-system χωρίς τοπικά εργαλεία ανάπτυξης. Όρια: σε παραγωγικά περιβάλλοντα πρέπει να ενεργοποιείται και να τεκμηριώνεται προσεκτικά, αλλιώς δημιουργείτε μια περιττή επιφάνεια επίθεσης.
Stolperfallen in Delphi WebView2 FMX: COM, Threads und Form-Lifecycle
1) Callbacks μετά το κλείσιμο
Οι ασύγχρονοι CompletedHandler μπορούν να φτάσουν αφότου η φόρμα έχει ήδη κλείσει. Στο Snippet το FDestroyed αποτρέπει την πρόσβαση σε απελευθερωμένα αντικείμενα. Πιο ανθεκτικό είναι επιπλέον:
- Αποθηκεύστε tokens για τα γεγονότα και στο
Destroyκαλέστε καθαρά ταremove_* - Να επιτρέπεται το InitializeAsync μόνο μία φορά (State-Machine: Created/Initializing/Ready/Disposed)
2) Πλαίσιο νήματος
Πολλοί Handler εμφανίζονται «UI‑nah», αλλά μην βασίζεστε ότι μπορείτε να γράψετε απευθείας σε FMX‑Controls. Εάν ενημερώνετε το UI στο OnWebMessage, το TThread.Queue(nil, ...) είναι η ασφαλής επιλογή. Προτιμώ να διαχωρίζω: ο Host συλλέγει το γεγονός, ο Application‑Service αποφασίζει, και το UI ενημερώνεται αποκλειστικά μέσω Queue.
3) DPI/Resize και FMX‑Layouts
Το FMX υπολογίζει σε λογικές μονάδες, ενώ το WebView2 αναμένει Pixel‑Rects. Στην πράξη χρειάζεστε ένα σαφές σημείο όπου μετατρέπετε τα bounds των FMX‑Controls σε πραγματικά pixel. Το Snippet δέχεται ένα TRect; στη φόρμα σας θα πρέπει από αυτό να παράγετε τις WinAPI‑συντεταγμένες (π.χ. μέσω FMX.Platform.Win και των Handle‑APIs). Εάν η εφαρμογή κλιμακώνεται με βάση το monitor‑DPI, δοκιμάστε τη μετακίνηση μεταξύ monitors: το WebView2 είναι εδώ πιο ευαίσθητο σε σχέση με τα καθαρά FMX‑Controls.
Πότε αξίζει το WebView2 σε FMX — και πότε όχι
Το WebView2 αξίζει όταν θέλετε να χρησιμοποιήσετε τεχνολογία Web στοχευμένα σε μια ώριμη Delphi‑client εφαρμογή: ενσωματωμένα Admin‑Views, OAuth/OIDC‑Login‑Flows, HTML‑Reports, εσωτερικά portals ή ελεγχόμενα «Micro‑Frontends». Ως γέφυρα εκσυγχρονισμού είναι επίσης πρακτικό, εφόσον κόβετε καθαρά τις ευθύνες και δεν αφήνετε τη γέφυρα να γίνει ανεξέλεγκτη πίσω πόρτα για Business‑Logik.
Όρια της προσέγγισης:
- Πλατφόρμα: Το πρότυπο είναι Windows‑κεντρικό. Το FMX είναι πολυπλατφορμικό, το WebView2 δεν είναι. Για macOS/iOS/Android χρειάζεστε άλλα WebViews ή ένα επίπεδο αφαίρεσης.
- Security/Hardening: Μόλις φορτώνονται εξωτερικά περιεχόμενα, πρέπει να περιορίσετε αυστηρά την πλοήγηση, τα επιτρεπόμενα domains και τους προορισμούς λήψης. Αυτό πρέπει να ενσωματωθεί στα Requirements, όχι «αργότερα».
- Support: Το UserDataFolder και οι runtime‑εξαρτήσεις (WebView2 Runtime) πρέπει να αποτελούν μέρος του σχεδίου λειτουργίας/rollout.
Συμπέρασμα
Delphi WebView2 FMX δεν είναι ένα UI‑gadget αλλά ένα στοιχείο ολοκλήρωσης με δικό του lifecycle. Αν τυλίξετε δομημένα την αρχικοποίηση, το eventing, το UserDataFolder και τη JS‑Bridge, το WebView2 μπορεί να γίνει ένας σταθερός δομικός λίθος για ψηφιακές επιχειρησιακές λύσεις: Web‑UI εκεί όπου έχει νόημα, και Delphi‑λογική εκεί όπου ανήκει. Αν αντίθετα εκτελείτε ανεξέλεγκτα scripts, αφήνετε μονοπάτια στην τύχη και δεν αποσυνδέετε τα events, θα πάρετε ακριβώς το είδος των «σποραδικά στο πεδίο» σφαλμάτων που καταναλώνουν χρόνο και απομειώνουν την εμπιστοσύνη.
Εάν θέλετε να ενσωματώσετε καθαρά το WebView2 σε μια υπάρχουσα Delphi‑εφαρμογή ή να αξιολογήσετε τεχνικά μια άκρη εκσυγχρονισμού, μιλήστε μαζί μας:
Στο τεχνικό περιβάλλον παίζουν ρόλο επίσης το Webview2 Firemonkey και το Delphi Fmx Edge Browser, όταν ενσωματώσεις, ροές δεδομένων και περαιτέρω ανάπτυξη πρέπει να συνεργάζονται με σαφή τρόπο.
Επόμενο βήμα
Όταν από το θέμα προκύψει ένα πραγματικό έργο, η αρχιτεκτονική, η υφιστάμενη κατάσταση και η λειτουργία πρέπει να εξεταστούν έγκαιρα από κοινού.
Υποστηρίζουμε όχι μόνο σε μεμονωμένα ζητήματα, αλλά και όταν από αποσπάσματα πηγαίου κώδικα, θέματα legacy ή ιδέες για πύλες πρέπει να προκύψει ένα αξιόπιστο εταιρικό έργο.
- Η υφιστάμενη κατάσταση, το επιθυμητό μελλοντικό μοντέλο και οι τεχνικοί κίνδυνοι αξιολογούνται από κοινού.
- REST, η πρόσβαση στα δεδομένα, οι πύλες και το rollout δεν αναβάλλονται ως μετέπειτα συνέπειες.
- Αναγνωρίζετε έγκαιρα ποια προσέγγιση είναι οικονομικά και λειτουργικά βιώσιμη.