[WinForm] 使用 WebBrowser 操作 HTML 頁面的 Element

介紹使用 WebBrowser 操作網頁內容及 InvalidCastException 錯誤跟停頓問題的處理方法。

前言


  在 Window Form 應用程式如果需要瀏覽網頁時可以崁入 WebBrowser 控制項,但如果需要操作崁入的 HTML 的網頁元素,就需要額外的操作,以下紀錄幾種操作 HTML 元素的方法以及會碰到的問題。

 

建立 WinForm 應用程式


  首先先建立一個 Window Form 應用程式,在 Form1 表單上拉入一個 Button 與 WebBrowser 控制項,如下

 

  在 Button1 事件中使用 WebBrowser.Navigate 方法載入指定的網頁,如下


private void button1_Click(object sender, EventArgs e)
{
    webBrowser1.Navigate(Application.StartupPath + "\\Page.html");
}

 

  直接執行程式後點擊「前往網頁」按鈕就能夠看到網頁已經載入 WebBrowser 控制項中,如下

 

操作 HTML 網頁中的元素


  接下來如需要針對 HTML 網頁上的元素進行控制時,可以透過 WebBrowser 控制項提供的方法來處理,首先要在網頁載入完成後才進行操作,點選 WebBrowser 控制項加入 DocumentCompleted 事件,DocumentCompleted 事件是當網頁文件完全載入後即會觸發,就可以透過 Document、DocumentText、或 DocumentStream 屬性取得網頁內容,如下。


private void webBrowser1_DocumentCompleted
    (object sender, WebBrowserDocumentCompletedEventArgs e)
{
    if (webBrowser1.ReadyState == WebBrowserReadyState.Complete)
    {
        ThreadPool.QueueUserWorkItem(o =>
        {
            FormWork();
        }); 
    }
}

private void FormWork()
{
    // 進行操作
}

 

  透過 WebBrowser.ReadyState 屬性可以知道網頁使否已經載入完成,當載入完成後就使用 Thread 委派 FormWork 方法來處理網頁元素的操作,相關操作方法以下列出幾項。

 

操作文字框


webBrowser1.Document.GetElementById("name").SetAttribute("value", "Arvin"); 

 

操作下拉選單


HtmlElementCollection opts = webBrowser1.Document.
    GetElementById("sex").GetElementsByTagName("option");
foreach (HtmlElement opt in opts)
{
    if (opt.GetAttribute("value") == "男")
        opt.SetAttribute("selected", "selected");
}

 

操作單選框


HtmlElementCollection opts = webBrowser1.Document.
    GetElementsByTagName("input").GetElementsByName("skill");
foreach (HtmlElement opt in opts)
{
    if (opt.GetAttribute("value") == "WebForm")
        opt.InvokeMember("click");
}

 

執行 JavaScript 方法 (不需寫入左右括弧符號)


webBrowser1.Document.InvokeScript("ShowInfo");

 

取得網頁內崁 iFrame 網頁的內容方法


webBrowser1.Document.Window.Frames[0].Document

 

  以上列出幾種操作網頁元素的做法,不外乎就是取得元素物件再透過 SetAttribute 方法來取得或設定值,最後將幾種操作方式組合後來測試自動輸入表單的功能並且希望輸入表單時能夠一步一步輸入,所以加入 Sleep 方法停頓一秒鐘,如下


private void FormWork()
{
    webBrowser1.Document.GetElementById("name").SetAttribute("value", "Arvin");
    Thread.Sleep(1000);

    webBrowser1.Document.GetElementById("phone").SetAttribute("value", "0912345678");
    Thread.Sleep(1000);

    HtmlElementCollection opts = webBrowser1.Document.
        GetElementById("sex").GetElementsByTagName("option");
    foreach (HtmlElement opt in opts)
    {
        if (opt.GetAttribute("value") == "男")
            opt.SetAttribute("selected", "selected");
    }
    Thread.Sleep(1000);

    HtmlElementCollection opts2 = webBrowser1.Document.
        GetElementsByTagName("input").GetElementsByName("skill");
    foreach (HtmlElement opt in opts2)
    {
        if (opt.GetAttribute("value") == "WebForm")
            opt.InvokeMember("click");
    }
    Thread.Sleep(1000);

    webBrowser1.Document.InvokeScript("ShowInfo");
}

 

  完成後執行程式碼上就會跳出 InvalidCastException 錯誤,如下

 

  其原因是因為執行緒安全的關係,無法在非主執行緒的線程下操作 UI 控制項,在以上的程式碼中直接在 Thread 方法中取得 webBrowser1 控制項進行操作因而導致了錯誤發生。

 

  調整程式碼先使用 Control.InvokeRequired 屬性來判斷是否在主執行緒下執行,若不是的話則呼叫 Invoke 方法指定委派,如下。


private delegate void FormWorkDelegate();

private void FormWork()
{
    if (webBrowser1.InvokeRequired)
        webBrowser1.Invoke(new FormWorkDelegate(FormWork));
    else
    {
        webBrowser1.Document.GetElementById("name").SetAttribute("value", "Arvin");
        Thread.Sleep(1000);

        webBrowser1.Document.GetElementById("phone").SetAttribute("value", "0912345678");
        Thread.Sleep(1000);

        HtmlElementCollection opts = webBrowser1.Document.
            GetElementById("sex").GetElementsByTagName("option");
        foreach (HtmlElement opt in opts)
        {
            if (opt.GetAttribute("value") == "男")
                opt.SetAttribute("selected", "selected");
        }
        Thread.Sleep(1000);

        HtmlElementCollection opts2 = webBrowser1.Document.
            GetElementsByTagName("input").GetElementsByName("skill");
        foreach (HtmlElement opt in opts2)
        {
            if (opt.GetAttribute("value") == "WebForm")
                opt.InvokeMember("click");
        }
        Thread.Sleep(1000);

        webBrowser1.Document.InvokeScript("ShowInfo");
    }
}

 

  在次執行後發現不會跳出錯誤訊息了,但是卻發生另一個問題,就是當頁面載入後在填入表單值時程式會停頓一段時候才一次顯示所以欄位的值,這樣的結果並不符合當初所要一步一步的填入表單的需求,如下

 

  這種問題在我此篇MSDN發問中 ThankfulHeart 大有提到,Invoke 方法是使用 UI 的線程,而如果在 UI 線程中使用了 Sleep 方法將導致 UI 畫面被阻塞,因此才讓畫面陷入了停頓的狀態。

 

  而在 FormWork 方法中我將操作的方法都包含在 Invoke 的程式區塊中,所以在此如要避免長時間的畫面阻塞,應該要盡可能的切割使用到 Invoke 的區段,因此修改程式如下。


private void FormWork()
{
    this.Invoke(new MethodInvoker(() =>
    {
        webBrowser1.Document.GetElementById("name").SetAttribute("value", "Arvin");
    }));
    Thread.Sleep(1000);

    this.Invoke(new MethodInvoker(() =>
    {
        webBrowser1.Document.GetElementById("phone").SetAttribute("value", "0912345678");
    }));
    Thread.Sleep(1000);

    this.Invoke(new MethodInvoker(() =>
    {
        HtmlElementCollection opts = webBrowser1.Document.
            GetElementById("sex").GetElementsByTagName("option");
        foreach (HtmlElement opt in opts)
        {
            if (opt.GetAttribute("value") == "男")
                opt.SetAttribute("selected", "selected");
        }
    }));
    Thread.Sleep(1000);

    this.Invoke(new MethodInvoker(() =>
    {
        HtmlElementCollection opts2 = webBrowser1.Document.
            GetElementsByTagName("input").GetElementsByName("skill");
        foreach (HtmlElement opt in opts2)
        {
            if (opt.GetAttribute("value") == "WebForm")
                opt.InvokeMember("click");
        }
    }));
    Thread.Sleep(1000);

    this.Invoke(new MethodInvoker(() =>
    {
        webBrowser1.Document.InvokeScript("ShowInfo");
    }));
}

 

  透過切割每個動作呼叫對應的 Invoke 去執行後執行結果如下

 

  以上就是一個簡單使用 WinForm 的 WebBrowser 控制項控制網頁元素的方式,在此紀錄一下做法。

 

範例程式碼


WinWebBrowser.rar

 

參考資料


WebBrowser.DocumentCompleted 事件

Control.InvokeRequired 屬性

Control.Invoke 方法 (Delegate)

 

 


以上文章敘述如有錯誤及觀念不正確,請不吝嗇指教
如有侵權內容也請您與我反應~謝謝您 :)