WebBrowser彈出視窗之(二)––showModalDialog( ) & showModelessDialog( )
showModalDialog並不建立新的瀏覽器視窗,也不建立新的瀏覽器物件,而是在WebBrowser的同一個執行緒中建立的視窗,而showModelessDialog( )則是在新的執行緒中建立的視窗,所以處理方式不相同。
當showModalDialog( )被呼叫後,瀏覽器執行緒會建立一個對話方塊,該對話方塊包含兩個視窗,父視窗的類為“Internet Explorer_TridentDlgFrame”,子視窗的類為“Internet Explorer_Server”,其子視窗即為IE核心的視窗,可以通過給該視窗傳送訊息,進行一些自動化操作(如按鍵、滑鼠點選等)。當子視窗建立時,父視窗會收到WM_PARENTNOTIFY訊息,hwnd值即為父視窗的值,wParam的值即為Internet Explorer_Server的視窗。我們捕獲視窗後就可以捕捉到IE核心視窗的控制代碼了。
STEP 1: 建立視窗事件的鉤子函式
// 使用Windows API函式
[DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr SetWindowsHookEx(HookType hooktype, HookProcedureDelegate callback, IntPtr hMod, UInt32 dwThreadId);
// Hook Types
public enum HookType
{
WH_JOURNALRECORD = 0,
WH_JOURNALPLAYBACK = 1,
WH_KEYBOARD = 2,
WH_GETMESSAGE = 3,
WH_CALLWNDPROC = 4,
WH_CBT = 5,
WH_SYSMSGFILTER = 6,
WH_MOUSE = 7,
WH_HARDWARE = 8,
WH_DEBUG = 9,
WH_SHELL = 10,
WH_FOREGROUNDIDLE = 11,
WH_CALLWNDPROCRET = 12,
WH_KEYBOARD_LL = 13,
WH_MOUSE_LL = 14
}
[StructLayout(LayoutKind.Sequential)]
public struct CWPRETSTRUCT
{
public IntPtr lResult;
public IntPtr lParam;
public IntPtr wParam;
public UInt32 message;
public IntPtr hwnd;
};
// Delegate for the EnumChildWindows method
private delegate Boolean EnumerateWindowDelegate(IntPtr pHwnd, IntPtr pParam);
private static Win32API.HookProcedureDelegate _WH_CALLWNDPROCRET_PROC =
new Win32API.HookProcedureDelegate(WH_CALLWNDPROCRET_PROC);
// 在程式開始處呼叫該方法
public static void Hook( )
{
if (_pWH_CALLWNDPROCRET == IntPtr.Zero) {
_pWH_CALLWNDPROCRET = Win32API.SetWindowsHookEx(
Win32API.HookType.WH_CALLWNDPROCRET
,_WH_CALLWNDPROCRET_PROC
,IntPtr.Zero
,(uint)AppDomain.GetCurrentThreadId( )); // current thread
}
if (_pWH_CALLWNDPROCRET == IntPtr.Zero)
throw new ApplicationException("Failed to install window hook via: SetWindowsHookEx( )");
}
// 自定義的訊息鉤子函式,在函式中捕捉PARENTNOTIFY訊息,然後再檢查該訊息是否視窗建立或銷燬視窗
// 然後建立一個個事件WindowLife,在WebBrowser中新增該事件的處理函式即可
public delegate void WindowLifeHandler(Win32Message msg, IntPtr parent, IntPtr child);
public static event WindowLifeHandler WindowLife;
private static Int32 WH_CALLWNDPROCRET_PROC(Int32 iCode, IntPtr pWParam, IntPtr pLParam)
{
if (iCode < 0)
return Win32API.CallNextHookEx(_pWH_CALLWNDPROCRET, iCode, pWParam, pLParam);
Win32API.CWPRETSTRUCT cwp = (Win32API.CWPRETSTRUCT)Marshal.PtrToStructure(pLParam, typeof(Win32API.CWPRETSTRUCT));
Win32Message msg = Win32Message.WM_NULL;
try {
msg = (Win32Message)cwp.message;
} catch {
return Win32API.CallNextHookEx(_pWH_CALLWNDPROCRET, iCode, pWParam, pLParam); ;
}
if (msg == Win32Message.WM_PARENTNOTIFY) {
if ((int)cwp.wParam == (int)Win32Message.WM_CREATE || (int)cwp.wParam == (int)Win32Message.WM_DESTROY) {
System.Diagnostics.Debug.WriteLine("WM_PARENTNOTIFY hwnd=0x{0:x8} wParam={1} lParam=0x{2:x8}"
,(int)cwp.hwnd, (Win32Message)cwp.wParam,(int)cwp.lParam);
if (WindowLife != null)
WindowLife((Win32Message)cwp.wParam, cwp.hwnd, cwp.lParam);
}
}
return Win32API.CallNextHookEx(_pWH_CALLWNDPROCRET, iCode, pWParam, pLParam);
}
STEP 2:繼承WebBrowser並在派生類中新增WindowLife事件的處理函式,在派生類的建構函式中增加如下程式碼:
public class ExWebBrowser : System.Windows.Forms.WebBrowser
{
public ExWebBrowser( )
{
_windowLifeDelegate = new WindowsMessageHooker.WindowLifeHandler(OnWindowLife);
WindowsMessageHooker.WindowLife += _windowLifeDelegate;
this.Disposed += (sender, e) => {
WindowsMessageHooker.WindowLife -= _windowLifeDelegate;
};
private IntPtr _webPageDialogHandle = IntPtr.Zero;
private static readonly string IE_WebDialogClassName = "Internet Explorer_TridentDlgFrame";
private static readonly string IE_ServerClassName = "Internet Explorer_Server";
public delegate void WebPageDialogHandler(IntPtr hwnd, string title);
// 建立網頁對話方塊時觸發的事件
public event WebPageDialogHandler ShowWebDialog;
// 關閉網頁對話方塊時觸發的事件
public event WebPageDialogHandler CloseWebDialog;
private WindowsMessageHooker.WindowLifeHandler _windowLifeDelegate = null;
private void OnWindowLife(noock.windows.Win32Message msg, IntPtr parent, IntPtr child)
{
StringBuilder buffer = null;
string childClass = null;
string parentClass = null;
buffer = new StringBuilder(256);
if (child != _webPageDialogHandle) {
noock.windows.Win32API.GetClassName(child, buffer, buffer.Capacity);
childClass = buffer.ToString( );
System.Diagnostics.Debug.WriteLine("child class:" + childClass);
if (childClass != IE_ServerClassName)
return;
noock.windows.Win32API.GetClassName(parent, buffer, buffer.Capacity);
parentClass = buffer.ToString( );
System.Diagnostics.Debug.WriteLine("parent class:" + parentClass);
if (parentClass != IE_WebDialogClassName)
return;
}
noock.windows.Win32API.GetWindowText(parent, buffer, buffer.Capacity);
string title = buffer.ToString();
if (msg == noock.windows.Win32Message.WM_CREATE) {
_webPageDialogHandle = child;
System.Diagnostics.Debug.WriteLine(title, "showModalDialog( ) Opening:");
if (ShowWebDialog != null) {
ShowWebDialog(_webPageDialogHandle, title);
}
} else if (msg == noock.windows.Win32Message.WM_DESTROY) {
_webPageDialogHandle = IntPtr.Zero;
System.Diagnostics.Debug.WriteLine(title, "showModalDialog( ) Closing:");
if (CloseWebDialog != null) {
CloseWebDialog(child, title);
}
}
}
}
這樣就擴充套件了WebBrowser的事件,可以觸發showModalDialog( )彈出對話方塊的彈出事件。
STEP3: 因為函式的鉤子是使用API實現,是系統級的,所有必須在程式退出時釋放鉤子函式佔用的資源
public static bool Hooked
{
get { return _pWH_CALLWNDPROCRET != IntPtr.Zero; }
}
public static void Unhook( )
{
if ( ! Hooked) {
Win32API.UnhookWindowsHookEx(_pWH_CALLWNDPROCRET);
}
}
但是,還有一個問題,這樣只能捕捉到showModalDialog( )彈出的對話方塊,而不能捕捉到showModelessDialog( )彈出的非模態對話方塊,因為鉤子函式在上面的程式碼中只捕捉主執行緒的訊息,而非模態對話方塊則是單獨的執行緒。遺憾的是SetWindowsHookEx( )不支援面向程序的鉤子函式,除了面向執行緒就是面向全域性的,捕捉整個桌面(一般相當於整個使用者)的所有訊息,雖然這樣做也可以捕捉到相應的事件,但顯然效率是比較低的。而且,非模態對話方塊在實際應用中並不多見。
我們還可以通過一個折衷的方法,使用API來搜尋WebBrowser視窗關係樹中附近的視窗,看有沒有其所有者是WebBrowser父視窗的網頁對話方塊,例如程式碼:
public IntPtr WebPageDialogHandle
{
get
{
InvokeMethod invoker = new InvokeMethod(( ) =>
{
if (_webPageDialogHandle != IntPtr.Zero && Win32API.IsWindow(_webPageDialogHandle)) {
} else {
_webPageDialogHandle = SearchWebDialog(ParentForm.Handle, Win32API.GW_HWNDPREV);
if (_webPageDialogHandle == IntPtr.Zero)
_webPageDialogHandle = SearchWebDialog(ParentForm.Handle, Win32API.GW_HWNDNEXT);
}
}
);
this.Invoke(invoker);
return _webPageDialogHandle;
}
}
private IntPtr SearchWebDialog(IntPtr start, uint direction)
{
int processId, nextProcId;
int threadId = Win32API.GetWindowThreadProcessId(ParentForm.Handle, out processId);
StringBuilder sb = new StringBuilder(256);
IntPtr nextWin = Win32API.GetNextWindow(ParentForm.Handle, direction);
int nextTh = Win32API.GetWindowThreadProcessId(nextWin, out nextProcId);
while (nextProcId == processId) {
if (ParentForm.Handle == Win32API.GetParent(nextWin)) {
Win32API.GetClassName(nextWin, sb, sb.Capacity);
if (sb.ToString() == IE_WebDialogClassName) {
_webPageDialogHandle = Win32API.FindWindowExxx(nextWin, IntPtr.Zero, IE_ServerClassName, null);
return _webPageDialogHandle;
}
}
nextWin = Win32API.GetNextWindow(nextWin, direction);
nextTh = Win32API.GetWindowThreadProcessId(nextWin, out processId);
}
return IntPtr.Zero;
}
public static IntPtr FindWindowExxx(IntPtr parentHandle, IntPtr childAfter, string lclassName, string windowTitle)
{
IntPtr res = FindWindowEx(parentHandle, childAfter, lclassName, windowTitle);
if (res != IntPtr.Zero)
return res;
while ( (res = FindWindowEx(parentHandle, res, null, null)) != IntPtr.Zero) {
IntPtr aim = FindWindowExxx(res, IntPtr.Zero, lclassName, windowTitle);
if (aim != IntPtr.Zero)
return aim;
}
return IntPtr.Zero;
}
其實視窗的控制代碼只是核心物件中的一個地址,同一個程序的視窗控制代碼一般也是連續,正如程式碼中所示,我們不需要搜尋所有的視窗,而只需要進程序ID為邊界在WebBrowser父視窗的附近搜尋。非模態對話方塊的父視窗夠本是桌面視窗,而不是WebBrowser所在的視窗,所以在SearchWebDialog函式中呼叫了GetParent函式,而沒有使用GetAncestor,因為前者返回的不一定是父視窗,也可能是所有者,這正是非模態對話方塊與WebBrowser視窗的關係,瀏覽器視窗是模態對話方塊的父視窗,而只是非模態框的所有者,而不是其父視窗。
獲取非模態對話方塊的控制代碼以後,就隨便你對它做什麼了,傳送訊息模擬按鍵、模擬滑鼠點選、關閉等,都是可以的了。
補上Win32API類的部分程式碼
// Delegate for a WH_ hook procedure
public delegate Int32 HookProcedureDelegate(Int32 iCode, IntPtr pWParam, IntPtr pLParam);
[DllImport("user32.dll")]
public static extern IntPtr GetWindow(IntPtr hWnd, uint uCmd);
/// <summary>
/// Retrieves the identifier of the thread that created the specified window and, optionally,
/// the identifier of the process that created the window.
/// </summary>
/// <param name="handle"></param>
/// <param name="processId">if not null, fill the process id</param>
/// <returns>thread id</returns>
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern int GetWindowThreadProcessId(IntPtr handle, out int processId);
[DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
public static extern IntPtr GetParent(IntPtr hWnd);
[DllImport("user32.dll", SetLastError = true, EntryPoint="GetWindow")]
public static extern IntPtr GetNextWindow(IntPtr hWnd, uint Command);