Добавленный в RAD Studio 10.4 Sydney новый контрол TEdgeBrowser позволяет легко встроить браузер Microsoft Edge на основе Chromium в приложение написанное на Delphi или C++Builder. Но его функциональные возможности ограничены возможностями Microsoft.Web.WebView2. Например, у всех интернет-браузеров есть функция для просмотра исходного кода загруженной в него веб-страницы, а у WebView2 такого метода нет. Но этот недостаток можно легко исправить.
Идея очень простая. Контрол WebView2 позволяет в отображаемом в нем документе выполнять код написанный на JavaScript. А JavaScript, используя DOM, может получить доступ к загруженному в интернет-браузере документу и его элементам.
Начнем с DOM. Веб-страница, которая загружается в интернет-браузере, анализируется и преобразуется браузером в объектную модель документа (Document Object Model). DOM предоставляет собой объектно-ориентированный программный интерфейс (API) для доступа к элементам документов в форматах HTML, XHTML и XML. В нем каждая веб-страница, которая загружается в браузер, имеет свой собственный объект "document". Он служит интерфейсом для доступа ко всем свойствам документа. Модель документа представляет собой дерево, в узлах которого хранятся объекты типа "Element", предоставляющие интерфейс для доступа к свойствам отдельных элементов документа. Свойство элемента "outerHTML" хранит содержимое элемента. Значит нам достаточно взять корневой элемент загруженного документа (например, для HTML документов это <html>) и получить его содержимое. То есть путь к исходному коду веб-страницы будет выглядеть так: document.documentElement.outerHTML.
В RAD Studio есть модуль WebView2.pas, в который импортировано описание интерфейсов и констант из WebView2.tlb. В модуле WebView2 нас интересуют две вещи:
Идея очень простая. Контрол WebView2 позволяет в отображаемом в нем документе выполнять код написанный на JavaScript. А JavaScript, используя DOM, может получить доступ к загруженному в интернет-браузере документу и его элементам.
Начнем с DOM. Веб-страница, которая загружается в интернет-браузере, анализируется и преобразуется браузером в объектную модель документа (Document Object Model). DOM предоставляет собой объектно-ориентированный программный интерфейс (API) для доступа к элементам документов в форматах HTML, XHTML и XML. В нем каждая веб-страница, которая загружается в браузер, имеет свой собственный объект "document". Он служит интерфейсом для доступа ко всем свойствам документа. Модель документа представляет собой дерево, в узлах которого хранятся объекты типа "Element", предоставляющие интерфейс для доступа к свойствам отдельных элементов документа. Свойство элемента "outerHTML" хранит содержимое элемента. Значит нам достаточно взять корневой элемент загруженного документа (например, для HTML документов это <html>) и получить его содержимое. То есть путь к исходному коду веб-страницы будет выглядеть так: document.documentElement.outerHTML.
В RAD Studio есть модуль WebView2.pas, в который импортировано описание интерфейсов и констант из WebView2.tlb. В модуле WebView2 нас интересуют две вещи:
- ICoreWebView2.ExecuteScript – функция, которая выполняет строку с кодом JavaScript в загруженном в WebView2 документе
ICoreWebView2 = interface(IUnknown) ['{189B8AAF-0426-4748-B9AD-243F537EB46B}'] ... function ExecuteScript(javaScript: PWideChar; const handler: ICoreWebView2ExecuteScriptCompletedHandler): HResult; stdcall; end;
- WebView.ICoreWebView2ExecuteScriptCompletedHandler – ссылка на функцию, которая получает результат вызова метода ICoreWebView2.ExecuteScript
ICoreWebView2ExecuteScriptCompletedHandler = interface(IUnknown) ['{3B717C93-3ED5-4450-9B13-7F56AA367AC7}'] function Invoke(errorCode: HResult; resultObjectAsJson: PWideChar): HResult; stdcall; end;
procedure TForm1.btnViewPageSourceClick(Sender: TObject); begin if EdgeBrowser.WebViewCreated then ViewPageSource(EdgeBrowser.DefaultInterface) else reSource.Clear end; procedure TForm1.ViewPageSource(WebView: ICoreWebView2); begin WebView.ExecuteScript('encodeURI(document.documentElement.outerHTML)', ICoreWebView2ExecuteScriptCompletedHandler( function (errorCode: HResult; resultObjectAsJson: PWideChar): HResult stdcall begin if resultObjectAsJson = 'null' then reSource.Clear else reSource.Text := TNetEncoding.URL.Decode(resultObjectAsJson).DeQuotedString('"'); Result := S_OK; end)); end;Небольшие комментарии к коду:
- EdgeBrowser.DefaultInterface – ссылка на инициализированный в EdgeBrowser контрол WebView2.
- encodeURI – функция JavaScript, которая заменяет некоторые символы на управляющие последовательности, представляющие UTF-8 кодировку символа.
- resultObjectAsJson = 'null' – вызов скрипта может вернуть строку 'null' (например, если страница находится в состоянии загрузки).
- TNetEncoding.URL.Decode – декодируем результат в читаемый вид.
type TCustomEdgeBrowser = class(TWinControl) procedure ExecuteScript(const JavaScript: string); ... TExecuteScriptEvent = procedure (Sender: TCustomEdgeBrowser; AResult: HResult; const AResultObjectAsJson: string) of object;При их использовании код программы будет выглядеть немного иначе:
procedure TForm1.btnViewPageSourceClick(Sender: TObject); begin if EdgeBrowser.WebViewCreated then EdgeBrowser.ExecuteScript('encodeURI(document.documentElement.outerHTML)') else reSource.Clear end; procedure TForm1.EdgeBrowserExecuteScript(Sender: TCustomEdgeBrowser; AResult: HRESULT; const AResultObjectAsJson: string); begin if AResultObjectAsJson = 'null' then reSource.Clear else reSource.Text := TNetEncoding.URL.Decode(AResultObjectAsJson).DeQuotedString('"'); end;Мне больше нравится первый вариант. В нем вызов ExecuteScript и функция, которая обрабатывает его результат однозначно связаны. Вариант, предложенный разработчиками RAD Studio, требует общего обработчика OnExecuteScript в котором, если программа будет выполнять различные команды JavaScript, необходимо будет делать дополнительные проверки.
Я не понимаю почему, но этот код способен только заполнить TMemo, а попытка подставить вместо этого тобычный StringList заканчивается провалом - ничего не записывается.
ОтветитьУдалитьЯ попробовал добавить TStringList как посредника. Все работает:
Удалитьif AResultObjectAsJson = 'null'
then reSource.Clear
else begin
var sl: TStringList := TStringList.Create;
try
sl.Text := TNetEncoding.URL.Decode(AResultObjectAsJson).DeQuotedString('"');
reSource.Text := sl.Text;
finally
sl.Free
end;
end;
Я просто не понимаю, что происходит. Если я пишу
ОтветитьУдалитьHTMLSource.Text := TNetEncoding.URL.Decode(resultObjectAsJson).DeQuotedString('"');
то HTMLSource.Text в дебаггере равен '', HTMLSource.Count равен нулю, в этом стринглисте ничего нет. Причем, если HTMLSource является локальной переменной, то компилятор "оптимизирует" ее до состояния E2003 Undeclared identifier.
Если я пишу вот так:
Memo1.Text := TNetEncoding.URL.Decode(resultObjectAsJson).DeQuotedString('"');
HTMLSource.Text:=Memo1.Text;
то в Memo присутствует исходный текст страницы, его видно на форме, но опять же Memo1.Text = '', и HTMLSource остается пустым. Даже через assign ничего не копируется. Это какая-то дичь...
Это потому что под отладкой вы смотрите Memo1.Text до того, как туда записались значения.
УдалитьЭтот код будет работать случайным образом, обычно работать не будет. Дело в том что ExecuteScript выполняется какое-то время и значение в reSource.Text появится не сразу. В случае если попытаться сразу после ExecuteScript прочитать reSource.Text - там обычно пусто будет. Поэтому нужен таймаут на определение.
ОтветитьУдалитьВот так работает:
var
reSource: string;
function EdgeBrowserGetPageSource(aEdgeBrowser: TEdgeBrowser; ATimeOut{sec}: Integer; ABreak: PBoolean): String;
var
WebView: ICoreWebView2;
tFinish: TDateTime;
begin
Result := '';
if AEdgeBrowser.WebViewCreated then
begin
reSource := '';
WebView := AEdgeBrowser.DefaultInterface;
WebView.ExecuteScript('encodeURI(document.documentElement.outerHTML)',
ICoreWebView2ExecuteScriptCompletedHandler(
function (errorCode: HResult; resultObjectAsJson: PWideChar): HResult stdcall
begin
if resultObjectAsJson <> 'null' then
reSource := TNetEncoding.URL.Decode(resultObjectAsJson).DeQuotedString('"');
Result := S_OK;
end));
tFinish := Now() + ATimeOut/24/60/60;
repeat
if length(reSource) = 0 then
sleep(100);
Application.ProcessMessages;
until (length(reSource) > 0) or (Now() >= tFinish) or (assigned(ABreak) and ABreak^);
Result := reSource;
end;
end;