מהנושא במגזין ליישום בפרויקט
דפי שירות וטכניים רלוונטיים למאמר
מי שרוצה להטמיע פתאום „mal eben“ תוכן ווב מודרני בתוך תוכנת עסקית קיימת, יגיע על Windows ל-WebView2. ב- Delphi WebView2 FMX הבעיה העיקרית נדירה שהיא רק הצגת URL; האתגר הוא הטמעה נקייה בממשק FireMonkey (FMX), אתחול מהימן (אסינכרוני ובסיס COM), וכן מלכודות Edge סביב תיקיות User-Data, הורדות, ניפוי שגיאות ותקשורת JS↔Delphi אמינה.
קטע הקוד הזה מציג תבנית שאותה אני מעדיף ליישומים הניתנים לתחזוקה: אובייקט „Host“ מקופסל השולט ב-Lifecycle של WebView2, וכן גשר מוגדר דרך WebMessage (JSON), במקום קריאות „ExecuteScript“ מפוזרות בכל מקום. המטרה אינה קוד דמו, אלא רכיב ששרד בקליינטים שצמחו עם הזמן.
Warum WebView2 in FMX anders ist als „Browser-Component drop“
WebView2 היא API קרובה ל-COM/WinRT עם אתחול אסינכרוני. FireMonkey מהווה אבסטרקציה ל-Windows-Handles, אך בסופו של דבר תצטרכו עבור WebView2 חלון הורה אמיתי (Parent-Window, HWND) והעברת Resize/Focus מבוקרת. במקביל, אירועים לא תמיד רצים במקום שבו מצפים להם ב-FMX. אם תתחילו פה „quick and dirty“, תקבלו בדרך כלל:
- AVs נקודתיים בעת סגירת טופס (Callbacks מגיעים אחרי Destroy)
- Navigation-Events מתוך הקשר Thread שגוי
- בעיות Persistenz/Cache לא אמינות בגלל אסטרטגיית UserDataFolder לא ברורה
- אין הורדות או דיאלוגי הורדה „תלויים“
- Debugging תלוי במזל במקום בקונפיגורציית Remote-Debug ממוקדת
התרופה היא Lifecycle ברור: Create → InitializeAsync → Attach → Navigate → Detach/Dispose – ומחסום מוגדר בין ה-UI למנוע הדפדפן.
Source-Schnipsel: WebView2Host für Delphi WebView2 FMX
הקוד הבא מסמן מחלקת Host מקופסת, המיישמת (1) קונפיגורציית WebView2-Environment, (2) קשירת Controller-אובייקט ל-HWND, (3) חיווט אירועי Navigation ו-Download, ו-(4) הצעת גשר JS מבוסס JSON דרך WebMessageReceived. הקוד מתוכנן בכוונה להיות „architekturfähig“: הוא מקפל התייחסויות COM, מונע Callback-משכוים אחרי Destroy, ומאפשר גבולות תפעול כגון ‚pro User‘ או ‚pro Maschine‘ ו-UserDataFolder נפרדים.
unit WebView2Host;
interface
uses
System.SysUtils, System.Classes, System.IOUtils, System.JSON,
Winapi.Windows, Winapi.ActiveX,
FMX.Types,
WebView2, WebView2_TLB; // בהתאם להגדרות: 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;
// אפשרויות: כאן ניתן להוסיף ארגומנטים לדפדפן, לדוגמה Remote-Debug
Opt := TCoreWebView2EnvironmentOptions.Create;
// יצירת סביבה אסינכרונית
OleCheck(CreateCoreWebView2EnvironmentWithOptions(
nil, PWideChar(UserData), Opt,
TCoreWebView2CreateCoreWebView2EnvironmentCompletedHandler.Create(
procedure (errorCode: HRESULT; const createdEnvironment: ICoreWebView2Environment)
begin
if FDestroyed then Exit;
OleCheck(errorCode);
FEnvironment := createdEnvironment;
// קשירת Controller ל-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 קיים רק במסגרת ה-form.
end;
procedure TWebView2Host.UnhookEvents;
begin
// גרסה קשיחה: לזכור את ה-Tokens ולהפעיל remove_*.
// כאן כהערה בלבד, כי הגדרת יחידת ה-import וניהול ה-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);
// אופציונלי: ממשק משתמש הורדה מותאם, ואז להגדיר 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.
מטרת הגישה
- הקפסולציה של מחזור החיים: טופס FMX מכיר רק „Initialize/Navigate/Resize“, ולא פרטי COM.
- גשר מבוסס חוזה: הודעות JSON עם
name, באופן אופציונליcid(Correlation-ID) ו-payloadהן ניתנות לתחזוקה ולבדיקה. - התמדה תפעולית בטוחה: תיקיית
UserDataFolderמבוקרת מונעת התנגשות בקאש, בעיות הרשאות ו־“זה עובד על מחשב המפתח, אבל לא בסביבת הייצור“.
JS↔Delphi-Bridge: למה WebMessage יציב יותר מ-ExecuteScript
WebView2 מציע מספר דרכי תקשורת. במעשה, ExecuteScript מפתה, אבל קשה לגרסאות: אתם דוחפים מחרוזות לפרשן, בלי ערוצי תשובה ברורים ובלי מיפוי שגיאות יציב. PostWebMessageAsString / WebMessageReceived הם לעומת זאת ערוץ מוגדר.
מקרה שולי, שמופיע לעיתים קרובות בסביבות ארגוניות: אתם צריכים להפעיל workflow של Delphi מתוך ממשק ווב (למשל פורטל פנימי) — הדפסה, גישה להתקנים, אינטגרציה עם מערכות ישנות. עבור זה אתם זקוקים ל:
- רשימת לבנה של שמות הודעות
- Correlation-IDs לתשובות אסינכרוניות
- נקודה מרכזית שמבצעת ולידציה של payloads (למשל שדות חובה, מגבלות גודל)
בצד ה־Host זו הפונקציה OnWebMessageReceived. הוולידציה עצמה שייכת לשכבה מעליה (למשל Application-Service), כדי להפריד בין טכניקת ה־UI/WebView2 לבין לוגיקת העסק — ארכיטקטורת שכבות קלאסית: UI → Application → Domain → Infrastruktur.
Downloads und Dateiablage: was im Betrieb oft überrascht
הורדות ב־WebView2 מנוהלות דרך ICoreWebView2DownloadOperation. בהתאם למקור יכול להיות ש־ResultFilePath יהיה ריק מוקדם או יוגדר רק מאוחר יותר. בנוסף, חברות רבות אינן מעוניינות שמשתמשי קצה ישמרו לתיקיות לא מבוקרות.
דפוסים מבוססים:
- ליירט את DownloadStarting ודרך
args.put_Handled(1)שה־UI תטפל בעצמה (נתיב ספציפי, קונבנציה לשמות, תיקיית בידוד/Quarantine). - גבולות גודל קבצים ובדיקות MIME-Type, כדי להימנע ממקרה של „קובץ לוג של 4 GB בטעות“.
- Auditing: לרשום במערכת הלוגים את מטא־הנתונים של ההורדה (URI, MIME, Bytes), לא את התוכן.
אם יש לכם תהליכים מפוקחים (למשל אושרות, יכולת מעקב), הטיפול דרך האירועים הוא הנקודה היחידה שבה אפשר לשלב את עולם הדפדפן בכללי התפעול שלכם.
Debugging: DevTools, Remote Debug Port und reproduzierbare Zustände
ניפוי שגיאות ב־WebView2 נכשל לעיתים קרובות כי המצבים לא ניתנים לשחזור. שתי התאמות עוזרות:
- הפעל/השבת DevTools דרך
ICoreWebView2Settings(בקוד:SetDevToolsEnabled) — בגרסאות Release בדרך כלל כבוי, ובמקרי תמיכה מדליקים באופן ממוקד. - נתיב יציב של UserDataFolder: אם צוות התמיכה צריך לשחזר תקלה, נתיב מוגדר הוא זהב. ניתן לגבות/לדחוס את התיקייה (זהירות: Datenschutz/PII) ולבצע השוואת מצבים מפורטת.
אופציונלי (תלוי ב־wrapper) ניתן להוסיף ל־EnvironmentOptions ארגומנטים נוספים לדפדפן, למשל פורט Remote-Debug. זה שימושי כאשר אתם מנתחים אפליקציה על מערכת בדיקה ללא כלים מקומיים של מפתחים. גבולות: בסביבות פרודקשן יש לפתוח ולתעד זאת בצורה מסודרת, אחרת אתם יוצרים משטח התקפה מיותר.
Stolperfallen in Delphi WebView2 FMX: COM, Threads und Form-Lifecycle
1) קריאות חזרה (Callbacks) לאחר הסגירה
מטפלי ה-CompletedHandler הא-סינכרוניים עשויים להגיע אחרי שהטופס כבר נסגר. בקטע הקוד FDestroyed מונע גישה לאובייקטים ששוחררו. גישה עמידה יותר היא בנוסף:
- לאחסן טוקנים עבור אירועים ולקרוא ב־
Destroyבאופן מסודר ל־remove_* - להתיר את
InitializeAsyncרק פעם אחת (State-Machine: Created/Initializing/Ready/Disposed)
2) הקשר התהליכים (Thread-Kontext)
רבים מה-Handler אמנם מגיעים „קרובים ל־UI“, אך אל תסמכו על כך שתוכלו לכתוב ישירות ל־FMX-Controls. אם אתם מעדכנים את ה־UI בתוך OnWebMessage, האפשרות הבטוחה היא TThread.Queue(nil, ...). אני נוהג להפריד: ה-Host אוסף את האירוע, ה־Application-Service מקבל את ההחלטה, וה־UI מתעדכן אך ורק דרך Queue.
3) DPI/Resize und FMX-Layouts
FMX מחשב ביחידות לוגיות, בעוד ש־WebView2 מצפה ל־Pixel-Rects. בפועל אתם צריכים נקודה ברורה שבה אתם מתרגמים את ה־Bounds של FMX-Controls לפיקסלים אמיתיים. הדוגמה מקבלת TRect; בטופס שלכם עליכם לגזור ממנו את קואורדינטות ה־WinAPI (למשל דרך FMX.Platform.Win ו‑Handle-APIs). אם האפליקציה סקלתית לפי DPI של המוניטור, בדקו מעבר בין מוניטורים: WebView2 כאן רגיש יותר מאשר רכיבי FMX נקיים.
מתי WebView2 בתוך FMX משתלם — ומתי לא
WebView2 משתלם אם אתם רוצים לשלב טכנולוגיית Web באופן מובנה ביישום לקוח מתקדם Delphi: תצוגות ניהול מוטמעות, זרמי כניסה OAuth/OIDC, דוחות HTML, פורטלים פנימיים או „Micro-Frontends“ מבוקרים. גם כגשר מודרניזציה זה מעשי, בתנאי שחוצים תחומי אחריות נקיים ולא מאפשרים ל־Bridge להפוך לדלת אחורית בלתי מבוקרת לעקומת לוגיקת עסקית.
מגבלות הגישה:
- Plattform: התבנית מרוכזת סביב Windows. FMX הוא רב‑פלטפורמי, WebView2 אינו. ל־macOS/iOS/Android תצטרכו WebViews אחרים או שכבת אבסטרקציה.
- Security/Hardening: ברגע שמטענים תוכן חיצוני, יש להגביל בחומרה ניווט, דומיינים מותרים ויעדי הורדה. זה צריך להיכלל בדרישות, לא „מאוחר יותר“.
- Support: ה־UserDataFolder ותלויות-runtime (WebView2 Runtime) חייבים להיות חלק מתוכנית התפעול/הפריסה שלכם.
מסקנה
Delphi WebView2 FMX הוא פחות גאדג’ט UI ויותר רכיב אינטגרציה עם מחזור חיים עצמאי. אם תארגנו באופן מבני את האתחול, ה־Eventing, ה־UserDataFolder וה־JS‑Bridge, WebView2 יהפוך לחלק יציב בפתרונות ארגוניים דיגיטליים: Web‑UI שם שבו זה הגיוני, וDelphi‑לוגיקה שם שבה היא שייכת. אם במקום זאת תפעילו סקריפטים באופן בלתי מבוקר, תתנו לנתיבים להתגלגל לפי מזל ולא תנתקו אירועים, תקבלו בדיוק את סוג השגיאות „ספוראדיות בשטח“ שגורמות לבזבוז זמן ושוחקות אמון.
אם אתם רוצים לשלב WebView2 בצורה מסודרת ביישום Delphi קיים או להעריך טכנית קו מודרניזציה, פנו אלינו:
בהקשר המקצועי משחקים גם Webview2 Firemonkey ו־Delphi Fmx Edge Browser תפקיד חשוב כאשר אינטגרציות, זרימות נתונים והמשך פיתוח צריכים להשתלב באופן מסודר.
השלב הבא
כאשר הנושא הופך לפרויקט ממשי, יש לבחון כבר בשלב מוקדם את הארכיטקטורה, המצב הקיים והתפעול יחד.
אנו תומכים לא רק בשאלות נקודתיות, אלא גם כשמקטעי קוד מקור, נושאי Legacy או רעיונות פורטל אמורים להפוך לפרויקט ארגוני מהימן ועמיד.
- המצב הקיים, תמונת היעד והסיכונים הטכניים מוערכים יחד.
- REST, גישה לנתונים, פורטלים ו-Rollout לא יידחו כתוצאות מאוחרות.
- אתם מזהים מוקדם איזה נתיב בר-קיימא מבחינה כלכלית ותפעולית.