вторник, 5 апреля 2016 г.

Мини FAQ по WebBrowser. Часть 2-я.



  1. Как сохранить скриншот страницы.
  2. Как в своем коде обработать событие веб-страницы.
  3. Как предотвратить запуск Internet Explorer.
  4. В mshtml нет функций querySelector, querySelectorAll и других, можно ли их использовать в WebBrowser?
Как сохранить скриншот страницы.
Действительно, для большинства элементов управления это делается с помощью метода DrawToBitmap. У WebBrowser этот метод не работает. Точнее работает, но сохраняет не вебстранцу, отображенную в нем, а сам элемент управления, то есть белый прямоугольник.
Раньше можно было использовать для прорисовки любого узла страницы интерфейс IHTMLElementRender interface (Windows). Утилита tlbimp немного неправильно создавала сигнатуры для него и приходилось создавать в коде исправленную версию интерфейса, выглядело это так.
vb.net
1
2
3
4
5
<ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("3050F669-98B5-11CF-BB82-00AA00BDCE0B")>
Interface IHTMLElementRenderFixed
    Sub DrawToDC(hdc As IntPtr)
    Sub SetDocumentPrinter(bstrPrinterName As String, hdc As IntPtr)
End Interface
Ну и код, который мог это использовать выглядел примерно так
vb.net
1
2
3
4
5
6
7
8
9
10
11
        If SaveFileDialog1.ShowDialog = Windows.Forms.DialogResult.OK Then
            Dim render As IHTMLElementRenderFixed = CType(WebBrowser1.Document.Body.DomElement, IHTMLElementRenderFixed)
            Using bmp As New Bitmap(WebBrowser1.ClientSize.Width, WebBrowser1.ClientSize.Height)
                Using gr = Graphics.FromImage(bmp)
                    Dim dc = gr.GetHdc
                    render.DrawToDC(dc)
                    bmp.Save(SaveFileDialog1.FileName)
                    gr.ReleaseHdc(dc)
                End Using
            End Using
        End If
К сожалению, сейчас (насколько я знаю, начиная с IE9) этот код, хоть и не выдает ошибок, но и не работает, а просто оставляет изображение нетронутым. Компенсировать это обстоятельство, насколько я знаю, нечем (по крайней мере в самом WebBrowser и библиотеке mshtm), поскольку этот способ мог не только скриншот контрола делать, но и для прорисовки отдельного элемента страницы тоже подходил.
Для того, чтобы сделать скриншот экрана нашел вот такой способ. c# - Converting WebBrowser.Document To A Bitmap? - Stack Overflow
На VB.Net это будет выглядеть так
vb.net
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
Public Class NativeMethods
 
    <ComImport>
    <Guid("0000010D-0000-0000-C000-000000000046")>
    <InterfaceType(ComInterfaceType.InterfaceIsIUnknown)>
    Interface IViewObject
        Sub Draw(<MarshalAs(UnmanagedType.U4)> dwAspect As UInteger, lindex As Integer, pvAspect As IntPtr, <[In]> ptd As IntPtr, hdcTargetDev As IntPtr, hdcDraw As IntPtr, <MarshalAs(UnmanagedType.Struct)> ByRef lprcBounds As RECT, <[In]> lprcWBounds As IntPtr, pfnContinue As IntPtr, <MarshalAs(UnmanagedType.U4)> dwContinue As UInteger)
    End Interface
 
    <StructLayout(LayoutKind.Sequential, Pack:=4)>
    Structure RECT
 
        Public Left As Integer
        Public Top As Integer
        Public Right As Integer
        Public Bottom As Integer
    End Structure
 
    Public Shared Sub GetImage(obj As Object, destination As Image, backgroundColor As Color)
        Using graphics As Graphics = graphics.FromImage(destination)
            Dim deviceContextHandle As IntPtr = IntPtr.Zero
            Dim rectangle As RECT = New RECT()
            rectangle.Right = destination.Width
            rectangle.Bottom = destination.Height
            graphics.Clear(backgroundColor)
            Try
                deviceContextHandle = graphics.GetHdc()
                Dim viewObject As IViewObject = CType(obj, IViewObject)
                viewObject.Draw(1, -1, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, deviceContextHandle, rectangle, IntPtr.Zero, IntPtr.Zero, 0)
            Finally
                If deviceContextHandle <> IntPtr.Zero Then
                    graphics.ReleaseHdc(deviceContextHandle)
                End If
            End Try
        End Using
    End Sub
End Class
Ну и собственно запускается это так
vb.net
1
2
3
4
5
6
        If SaveFileDialog1.ShowDialog = Windows.Forms.DialogResult.OK Then
            Using bm As New Bitmap(WebBrowser1.ClientSize.Width, WebBrowser1.ClientSize.Height)
                NativeMethods.GetImage(WebBrowser1.ActiveXInstance, bm, Color.White)
                bm.Save(SaveFileDialog1.FileName)
            End Using
        End If
Метод прекрасно работает, но сохраняет сам элемент управления и видимую часть страницы. Если же надо работать с отдельными фрагментами, то видимо WebBrowser тут не подойдет и лучше использовать что-то другое. Вот кое-какая информация к размышлению на эту тему
Awesomium.NET - Home
HTML Renderer - Home
C#: WebBrowser vs Gecko vs Awesomium vs OpenWebKitSharp: What To Choose And How to Use - CodeProject

Кроме того для получения изображения всего документа (разбитого на страницы) можно использовать методы Print и ShowPrintDialog. В системе Windows есть виртуальный принтер, печатающий в xps-документ. Пакет Microsoft Office устанавливает виртуальный принтер, печатающий заметку OneNote. Есть и другие виртуальные принтеры, которые также можно установить. Обычно они не добавляют команду Print к HTML-файлам и просто вызвать печать их с помощью передачи этой команды Process.Start вместе с объектом ProcessStartInfo не получится. А WebBrowser вполне решает проблему печати. Приведу примерный код печати документа без визуального представления браузера
vb.net
1
2
3
4
5
6
7
8
9
    Sub PrintHtml(htmlCode As String)
        Dim browser As New WebBrowser
        AddHandler browser.DocumentCompleted,
            Sub(s, e)
                browser.Print()
                browser.Dispose()
            End Sub
        browser.DocumentText = htmlCode
    End Sub
Если браузер отображается на форме, то надо просто вызвать метод Print для печати на принтере по умолчанию, или ShowPrintDialog, если принтер надо выбрать.

Как в своем коде обработать событие веб-страницы.
Для начала самый простой вариант. Добавляем в проект веб-страницу следующего содержания.
HTML5
1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
 
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="utf-8" />
    <title></title>
</head>
<body>
<button id="button1">Кнопка 1</button>
</body>
</html>
Настраиваем свойства элемента проекта, чтобы страница копировалась в каталог исполняемого файла. Создаем переменную с путем к странице.
vb.net
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    Dim pagePath = IO.Path.Combine(Application.StartupPath, "PageEventHandler.html")
 
    Private Sub PageEventHandlerForm_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        WebBrowser1.ScriptErrorsSuppressed = True
        WebBrowser1.Navigate(pagePath)
    End Sub
 
    Private Sub WebBrowser1_DocumentCompleted(sender As Object, e As WebBrowserDocumentCompletedEventArgs) Handles WebBrowser1.DocumentCompleted
        Dim button1 = WebBrowser1.Document.GetElementById("button1")
        ' Имя события надо указывать с префиксом on
        button1.AttachEventHandler("onclick", AddressOf WebPage_Button1_Click)
    End Sub
 
    Sub WebPage_Button1_Click(sender As Object, e As EventArgs)
        MsgBox("Нажата кнопка 1")
    End Sub
Запускаем, нажимаем, получаем сообщение, радуемся... не долго. Если заменить код обработчика на такой
vb.net
1
MsgBox(CType(sender, HtmlElement).InnerText)
то вопреки ожиданиям ничего не произойдет. А если обработать ошибку, то можно получить NullReferenceException. То есть узнать, через sender, какой именно объект на странице создал событие не получится.
Это плохая новость, поскольку на веб странице удобно обрабатывать события группы элементов, добавив обработчик ближайшему общему контейнеру и там уже в обработчике можно определить из аргументов события, какой именно элемент его инициировал. Но проблема решается благодаря тому, что в IE информация о событии передается в обработчик не через аргументы, а через свойство event объекта window. Изменим код страницы следующим образом.
HTML5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!DOCTYPE html>
 
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="utf-8" />
    <title></title>
</head>
<body>
<button id="button1">Кнопка 1</button>
    <div id="buttonrange">
        <button>Кнопка 2</button>
        <button>Кнопка 3</button>
        <button>Кнопка 4</button>
        <button>Кнопка 5</button>
        <button>Кнопка 6</button>
    </div>
 
</body>
</html>
Изменим код добавления события и создадим обработчик кликов всех новых кнопок
vb.net
1
2
3
4
5
6
7
8
9
10
11
12
13
    Private Sub WebBrowser1_DocumentCompleted(sender As Object, e As WebBrowserDocumentCompletedEventArgs) Handles WebBrowser1.DocumentCompleted
        Dim button1 = WebBrowser1.Document.GetElementById("button1")
        button1.AttachEventHandler("onclick", AddressOf WebPage_Button1_Click)
        Dim buttonrange = WebBrowser1.Document.GetElementById("buttonrange")
        buttonrange.AttachEventHandler("onclick", AddressOf WebPage_Buttonrange_Click)
 
    End Sub
 
    Sub WebPage_Buttonrange_Click(sender As Object, e As EventArgs)
        Dim window = WebBrowser1.Document.Window.DomWindow
        Dim btn = window.[event].srcElement
        MsgBox("Нажата " & btn.innerText)
    End Sub
Теперь можно нажимать любую кнопку и получать сообщение с текстом этой кнопки.

Ну, уж коль скоро дело все равно не обошлось без DomWindow и позднего связывания, то видимо совсем не лишним будет объяснить, как эту задачу решить с использованием библиотеки mshtml.
Подключаем библиотеку к проекту(либо на вкладке .Net можно найти одну из готовых сборок взаимодействия, либо на вкладке COM, тогда сборка взаимодействия будет сгенерирована специально для проекта, на вкладке COM она назыается Microsoft HTML Object Library).
Теперь изменим код следующим образом
vb.net
1
2
3
4
5
6
7
8
9
10
11
12
    Private Sub WebBrowser1_DocumentCompleted(sender As Object, e As WebBrowserDocumentCompletedEventArgs) Handles WebBrowser1.DocumentCompleted
        Dim button1 = WebBrowser1.Document.GetElementById("button1")
        button1.AttachEventHandler("onclick", AddressOf WebPage_Button1_Click)
        Dim buttonrange = CType(WebBrowser1.Document.GetElementById("buttonrange").DomElement, mshtml.HTMLElementEvents2_Event)
        AddHandler buttonrange.onclick, AddressOf ButtonRange_onclick
    End Sub
 
    Private Function ButtonRange_onclick(pEvtObj As mshtml.IHTMLEventObj) As Boolean
        Dim e As mshtml.IHTMLEventObj2 = CType(pEvtObj, mshtml.IHTMLEventObj2)
        MsgBox(e.srcElement.innerText)
        Return True
    End Function
Таким образом мы получаем подсказки по типам и их членам, контроль типов на этапе разработки, но при этом придется много думать о том, где какой тип использовать. Например в данном коде мы привели div к типу mshtml.HTMLElementEvents2_Event, если бы это была кнопка , то понадобился бы тип mshtml.HTMLButtonElementEvents2_Event (хотя возможно и этот бы тоже подошел). В обработчике нам пришлось привести объект аргументов события к другому интерфейсу, чтобы получить доступ к интересующему нас свойству. И вся работа с типами этой библиотеки сводится к подобным манипуляциям, поэтому иногда проще написать код, не указывая типов.

Как предотвратить запуск Internet Explorer.
Проблема возникает когда на странице пользователь либо кликает ссылку, которая должна открыться в новом окне, либо выполняется код window.open(), либо когда пользователь из контекстного меню ссылки выбирает "Открыть в новом окне", либо использует "горячие клавиши" для той же цели. Когда это происходит в IE, то вроде все нормально, а вот когда окно IE открывается при действиях пользователя в другом приложении, использующем WebBrowser, то это уже выглядит совсем не комильфо.

Решается проблема довольно просто - в обработчике события NewWindow. Там можно просто запретить открытие нового окна и выполнить вместо этого какие-то другие действия.
vb.net
1
2
3
4
5
6
7
8
    Private Sub WebBrowser1_DocumentCompleted(sender As Object, e As WebBrowserDocumentCompletedEventArgs) Handles WebBrowser1.DocumentCompleted
        Dim links = (From l As HtmlElement In WebBrowser1.Document.GetElementsByTagName("a")).ToArray
        Array.ForEach(links, Sub(a) a.SetAttribute("target", "_blank"))
    End Sub
 
    Private Sub WebBrowser1_NewWindow(sender As Object, e As System.ComponentModel.CancelEventArgs) Handles WebBrowser1.NewWindow
        e.Cancel = True
    End Sub
В данном примере при загрузке документа всем ссылкам добавляется атрибут target="_blank", благодаря чему они должны открываться в новом окне и в этом случае открылось бы окно Internet Explorer, если бы не обработчик события NewWindow, в котором мы запретили открытие новых окон.
Но тут возникает вопрос: а как же быть со ссылкой? Ведь хотелось бы, чтобы она открывалась или в новой вкладке или в другом окне, но этого же приложения, или, на худой конец в этом же окне. Это можно сделать, если знать адрес, который должен был открыться в новом окне. Но, увы - это событие нам не дает такой информации. Есть "топорное" решение этой проблемы - использовать в качестве адреса текст статусбара
vb.net
1
OpenNewTab(WebBrowser1.Document.Window.StatusBarText)
Обычно это работает, поскольку нечасто встречаются страницы, на которых при наведении курсора на ссылку в статусбаре появляется что-то другое кроме ее адреса. Но такое вполне возможно. Кроме того возможно программное открытие окна и клик по ссылке без наведения мыши (можно найти ссылку табулятором или клавишами со стрелками). Так что этот способ, хоть и можно иногда использовать, но все-таки лучше найти что-нибудь понадежнее.
Для решения этой проблемы нам понадобится добавить в на панель инструментов WebBrowser из вкладки COM. В принципе его можно даже и не использовать, но добавить на форму надо для того, чтобы вместе с ним в проекте появились сборки взаимодействия, в которых определено все, что нам понадобится для решения задачи.
Итак в контекстном меню Панели Элементов выбираем пункт "Выбрать элементы". В открывшемся диалоговом окне переходим на вкладку COM-компоненты и устанавливаем флажок на элементе Microsoft Web Browser. Нажимаем OK и элемент появится на панели. Далее его надо перетащить на форму. Сам Control лучше не использовать, поскольку в нем есть много неудобств из-за отсутствия многих привычных свойств (таких как Dock например). Но при перетаскивании его на форму в проект добавляются две сборки: AxInterop.SHDocVw и Interop.SHDocVw. Они-то нам и нужны, без них ничего не получится.
Теперь COM-браузер можно убрать с формы и обработать событие "обычного" браузера. Код примерно такой.
vb.net
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
    Private Sub WebBrowser1_DocumentCompleted(sender As Object, e As WebBrowserDocumentCompletedEventArgs) Handles WebBrowser1.DocumentCompleted
        Dim links = (From l As HtmlElement In WebBrowser1.Document.GetElementsByTagName("a")).ToArray
        Array.ForEach(links, Sub(a) a.SetAttribute("target", "_blank"))
        Dim axbr As SHDocVw.WebBrowser = CType(WebBrowser1.ActiveXInstance, SHDocVw.WebBrowser)
        AddHandler axbr.NewWindow3, AddressOf WebBrower1_NewWindow3
    End Sub
 
 
    Private Sub WebBrower1_NewWindow3(ByRef ppDisp As Object, ByRef Cancel As Boolean, dwFlags As UInteger, bstrUrlContext As String, bstrUrl As String)
        Cancel = True
        Dim tpage As New TabPage
        Dim br As New WebBrowser
        tpage.Controls.Add(br)
        br.Dock = DockStyle.Fill
        br.Navigate(bstrUrl)
        br.ScriptErrorsSuppressed = True
        TabControl1.TabPages.Add(tpage)
        Dim axbr As SHDocVw.WebBrowser = br.ActiveXInstance
        AddHandler axbr.NewWindow3, AddressOf WebBrower1_NewWindow3
        AddHandler br.DocumentCompleted,
            Sub(sender As Object, ea As WebBrowserDocumentCompletedEventArgs)
                tpage.Text = CType(sender, WebBrowser).Document.Title
            End Sub
    End Sub
На форме TabControl с WebBrowser и вместо открытия IE будет появляться новая вкладка.

В mshtml нет функций querySelector, querySelectorAll и других, можно ли их использовать в WebBrowser?
Использовать можно, но только через скрипты. Как это работает - не знаю, скорей всего они реализованы на уровне движка JavaScript. Для часто используемых функций можно создать расширения управляемых классов.
vb.net
1
2
3
4
    <Extension>
    Public Function QuerySelector(document As HtmlDocument, selector As String)
        Return document.InvokeScript("eval", New Object() {String.Format("document.querySelector(""{0}"")", selector)})
    End Function
Единственное, что надо понимать, что это может не работать, если код страницы отображается в режиме совместимости со старыми версиями. Если свои страницы(например запущенные локально) не поддерживают новых возможностей, то иногда приходится явно указывать, с какими версиями должна быть совместима страница.
HTML5
1
    <meta http-equiv="x-ua-compatible" content="IE=9;IE=10;IE=11">

Комментариев нет :

Отправить комментарий