C# WebBrowser 一些小用法

紀錄一些  C# .Net 裡面的 WebBrowser 小用法

HtmlElement  .GetAttribute() .SetAttribute() .InvokeMember()

IHTMLDocument2 .parentWindow.execScript()

WebBrowser.Document.Window.Frames

2018/01/15 補充

使用 NotifyIcon 達到最小化到系統列

利用 System.Threading.Mutex 做

唯一開啟,不能重複開啟程式


因為公司換新的人資系統

中午午休需要線上打卡,而我動不動就忘記去用

所以想做個自動打卡程式

本來想使用 WatiN 偷懶的去使用

結果發現官網已經消失,且NuGet找到的版本是2011年的

不知為何 在Windows 7 + Visual Studio 2015 的環境下居然無法運行

所以就放棄使用。觀察了新的人資系統發現是使用ASP.net 開發

所以第一時間就放棄使用 HttpWebRequestHttpWebResponse去截取資料

(或許是因為經驗不夠或是相關知識不足,要模擬出ASP.net的 VIEWSTATE 太花時間)

所以決定直接使用 WebBrowser 去做個簡單打卡程式

簡單紀錄下一些事項,此程式只是自己再用的簡單程式,所以不是做得很嚴謹

一開始在運行程式的時候會跳出 "物件沒有支援這個屬性或方法'attachEvent'" 的錯誤(在網頁資訊)

Google之後發現是WebBrowser預設的IE運作版本太低,所以裡面的一些語法轉換會出問題

參考了Google到的前輩文章做了修改

[C#]設定WebBrowser Control運行的User Agent版本

做了機碼的修改,因為要修改機馬,所以需要使用管理者權限才能運行程式

using Microsoft.Win32; // 要修改機碼使用

using mshtml; // 後面需要用IHTMLDocument2

var appName = Process.GetCurrentProcess().MainModule.ModuleName;
Registry.SetValue(@"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Internet Explorer\MAIN\FeatureControl\FEATURE_BROWSER_EMULATION", appName, 11001, RegistryValueKind.DWord);

這樣之後就可以開始抓網頁內的資料了

HtmlElement userName = webBrowser.Document.GetElementById("txtAccount");
userName?.SetAttribute("value", IDText);
 
HtmlElement userPass = webBrowser.Document.GetElementById("txtPwd");
userPass?.SetAttribute("value", PWText);

利用網頁的id資料抓出 HtmlElement 之後因為密碼關係 網頁不使用innerText所以會造成登入錯誤

所以改用 .SetAttribute("value", ) 將密碼設進去 value

HtmlElement submit = webBrowser.Document.All["btnSubmit"];
submit?.InvokeMember("Click");

找出登入的按鈕後使用.InvokeMember("click") 送出登入功能


結果這時候進去要抓登入後的資料一直抓不到,後來才發現

原來登入後的頁面使用了IFrame,所以內框的資料一直抓不到

為了方便我是直接在新增一個 webBrowser 去連接 Frame的網址

<我沒特別驗證,我猜想在同一個程式裡面的WebBrowser,因為網域的關係

所以Cookie是共用的,另外如果網頁自己用到Session也是共用,所以很多資訊不用額外處理

這就是用 WebBrowser 元件的好處 "開發快速" 一堆細節可以省略處理 >

之後在 WebBrowser 的  DocumentCompleted 事件裡,判斷是否有 IFrame

if (webBrowser.Document.Window.Frames.Count > 0)
{
     string url = webBrowser.Document.Window.Frames[0].Url.AbsoluteUri;
     wbFrames.Navigate(url);
     wbFrames.DocumentCompleted += new WebBrowserDocumentCompletedEventHandler(wb_DocumentCompleted);
}

之後因為 打卡的時候會跳出 確認框,所以需要在網頁讀取完成後

注入一個javascript去做自動確認功能

IHTMLDocument2 相關說明資料請自行Google

var temp = (IHTMLDocument2)wbFrames.Document.DomDocument;
// JavaScript 自動確認
temp.parentWindow.execScript("function confirm(s){return 1;} ""javaScript");
temp.parentWindow.execScript("function alert(s){return true;} ""javaScript");

因為 網頁用 confirm去做確認框 但是 alert  情況也很像,所以都寫上當作範例

最後只要抓出簽到跟簽退的功能做按出就好了

但是因為,該網頁兩個功能都沒做id,把id作在上層的元件裡

所以造成無法直接抓出元件來點擊,小改了一下做法

HtmlElement divID = wbFrames.Document.GetElementById("有id的上層元件");
HtmlElementCollection items = divID .GetElementsByTagName("A"); // 因為是使用超聯結,所以抓出 tag "A"
foreach (HtmlElement el in items)
{
    string text = el.GetAttribute("InnerText");
    if (text == "簽到")
    {
        el.InvokeMember("click");
        break;
    }
}

2018/01/15 補充兩個小片段

Google都能搜尋到的片段,只是為了方便,所以我也記錄一下

第一部分,寫在 static class Program裡面的 static void Main()

用來判斷是否有重複開啟軟體

// 是否可以開新的
bool canCreateNew;
// 獲取程式的Guid 作為唯一標識
Attribute guid_attr = Attribute.GetCustomAttribute(Assembly.GetExecutingAssembly(), typeof(GuidAttribute));
string guid = ((GuidAttribute)guid_attr).Value;
// 利用 Mutex 判斷是否已經有開啟
var _mutex = new System.Threading.Mutex(true, guid, out canCreateNew);
if (false == canCreateNew)
{
    // 發現重複
    MessageBox.Show("重複開啟""重複開啟"MessageBoxButtons.OK, MessageBoxIcon.Error);
    return;
}
_mutex.ReleaseMutex();

第二部分是 為了好看 讓程式 可以縮小到工作列圖示

首先是在共用地方宣告

//宣告NotifyIcon
private System.Windows.Forms.NotifyIcon notifyIcon1;

然後再 Form 初始化的地方

設定 NotifyIcon 相關資訊,使用MouseDoubleClick 是為了讓他最小化可以還原

//指定使用的容器
this.notifyIcon1 = new System.Windows.Forms.NotifyIcon(this.components);
//建立NotifyIcon
this.notifyIcon1.Icon = new Icon("myIcon.ico");
this.notifyIcon1.Text = "AppName";
this.notifyIcon1.MouseDoubleClick += new MouseEventHandler(this.notifyIcon1_MouseDoubleClick);

另外加入 MouseDoubleClick 的 Function

private void notifyIcon1_MouseDoubleClick(object sender, MouseEventArgs e)
{
    this.Show();
    this.WindowState = FormWindowState.Normal;
}

另外要一進入就最小化的功能的話

請自行添加 Form1_Shown 事件,在裡面補充

private void Form1_Shown(object sender, EventArgs e)
{
    if (bFirst) // 記錄是否為軟體第一次開啟
    {
        bFirst = false;
        this.Hide();
        this.notifyIcon1.Visible = true;
        this.notifyIcon1.ShowBalloonTip(5000, "開啟成功""開啟成功"ToolTipIcon.Info);
    }
}

寫在 Form1_Load 這沒用,因為這時候視窗還沒開啟