最近公司產品中自定義瀏覽器比較老,打開一些支持h5 的站莫名報錯,而且經常彈框。已經到了令人無法忍受的地步了,于是我想到了將內核由之前的IE 升級到Chromium。之前想到的是使用cef來做,而且網上的資源和教程也很多,后來在自己嘗試的過程中發現使用cef時程序會莫名其妙的崩潰,特別是在關閉對話框的時候。我在網上找了一堆資料,嘗試了各種版本未果,這個方案也就放棄了。后來又搜到了wke和miniblink,對比二者官方的文檔和demo,我決定使用miniblink,畢竟我直接搜索wke browser 出來的都是miniblink,只有搜索wke github 才會有真正的wke,而且wke似乎沒有api文檔,最后miniblink是國人寫的,文檔都是中文而且又有專門的qq交流群,有問題可以咨詢一下。
什么是miniblink
miniblink 是由國內大神 龍泉寺掃地僧 針對chromium內核進行裁剪去掉了所有多余的部件,只保留最基本的排版引擎blink,而產生的一款號稱全球小巧的瀏覽器內核項目,目前miniblink 保持了10M左右的極簡大小,相比CEF 動輒幾百M的大小確實小巧許多。而且能很好的支持H5等一些列新標準,同時它內嵌了Nodejs 支持electron。而且也支持各種語言調用。
官方的地址如下為 https://weolar.github.io/miniblink/index.html
使用miniblink
說了這么多那么該怎么用呢?從官方的介紹來看,我們可以使用VS的向導程序生成一個普通的win32 窗口程序,然后生成的這些代碼中將函數InitInstance 中的代碼全部刪除加上這么5句話
wkeSetWkeDllPath(L"E:\\mycode\\miniblink49\\trunk\\out\\Release_vc6\\node.dll");
wkeInitialize();
wkeWebView window = wkeCreateWebWindow(WKE_WINDOW_TYPE_POPUP, NULL, 0, 0, 1080, 680);
wkeLoadURL(window, "qq.com");
wkeShowWindow(window, TRUE);
當然,使用這些函數需要下載它的SDK開發包,然后在對應位置包含wke.h。
這些代碼會生成一個窗口程序,具體的請敢興趣的朋友自己去實踐看看效果。或者編譯運行一下它的demo程序。
在對話框中使用
現在我想在對話框中使用,那么該怎么辦呢。
首先也是先用MFC的向導生成一個對話框并編輯資源文件。最后我的對話框大概長成這樣
我會將按鈕下面部分全部作為瀏覽器頁面。
我們在程序APP類的InitInstance函數 中初始化miniblink庫,并在對話框被關閉后直接卸載miniblink的相關資源
wkeSetWkeDllPath(L"node.dll");
wkeInitialize();
CWebBrowserDlg dlg = CWebBrowserDlg();
m_pMainWnd = dlg;
INT_PTR nResponse = dlg.DoModal();
if (nResponse == IDOK)
{
// TODO: 在此放置處理何時用
// “確定”來關閉對話框的代碼
}
else if (nResponse == IDCANCEL)
{
// TODO: 在此放置處理何時用
// “取消”來關閉對話框的代碼
}
// 由于對話框已關閉,所以將返回 FALSE 以便退出應用程序,
// 而不是啟動應用程序的消息泵。
wkeFinalize();
然后在主對話框類中新增一個成員變量用來保存miniblink的web視圖的句柄
wkeWebView m_web;
我們在對話框的OnInitDialog函數中創建這么一個視圖,用來加載百度的首頁面
GetClientRect(&rtClient);
rtClient.top += 24;
m_web = wkeCreateWebWindow(WKE_WINDOW_TYPE_CONTROL, *this, rtClient.left, rtClient.top, rtClient.right - rtClient.left, rtClient.bottom - rtClient.top);
wkeLoadURL(m_web, "https://www.baidu.com");
wkeShowWindow(m_web, TRUE);
至此我們已經能夠生成一個簡單的瀏覽器程序
似乎到這已經差不多該結束了,但是現在我遇到了在整個程序完成期間最大的問題,那就是web頁面無法響應鍵盤消息,我嘗試過改成窗口程序,發現改了之后能正常運行,但是我要的是對話框啊。這么改只能證明這個庫是沒問題的。
后來我在群里面發出了這樣的疑問,有朋友告訴我說應該是wkeWebView沒有接受到鍵盤消息,于是我打算處理主對話框的WM_KEYDOWN 和WM_KEYUP 以及WM_CHAR消息,根據官方的文檔,應該是只需要攔截對話框的這三個消息,然后使用函數wkeFireKeyUpEvent、wkeFireKeyDownEvent、wkeFireKeyPressEvent函數分別向wkeWebView發送鍵盤消息就可以了.于是我在對應的處理函數中添加了相關代碼
//OnChar
unsigned int flags = 0;
if (nFlags & KF_REPEAT)
flags |= WKE_REPEAT;
if (nFlags & KF_EXTENDED)
flags |= WKE_EXTENDED;
wkeFireKeyPressEvent(m_web, nChar, flags, false);
//OnKeyUp
unsigned int flags = 0;
if (nFlags & KF_REPEAT)
flags |= WKE_REPEAT;
if (nFlags & KF_EXTENDED)
flags |= WKE_EXTENDED;
wkeFireKeyUpEvent(m_web, virtualKeyCode, flags, false);
//OnKeyDown
unsigned int flags = 0;
if (nFlags & KF_REPEAT)
flags |= WKE_REPEAT;
if (nFlags & KF_EXTENDED)
flags |= WKE_EXTENDED;
wkeFireKeyDownEvent(m_web, virtualKeyCode, flags, false);
但是這么干,我通過調試發現它好像并沒有進入到這些函數里面來,也就是說鍵盤消息不是由主對話框來處理的。那么現在只能在wkeWebView 對應的窗口中來處理了。那么怎么捕獲這個窗口的消息呢,miniblink提供了函數wkeGetHostHWND 來根據視圖的句柄獲取對應窗口的句柄,那么現在的思路就是這樣的:首先獲取對應的窗口句柄然后通過SetWindowLong來修改窗口的窗口過程,然后在窗口過程中處理這些消息就行了。根據這個思路整理一下代碼
//在創建wkeWebView 之后來hook窗口過程
HWND hWnd = wkeGetHostHWND(m_web);
g_OldProc = (WNDPROC)SetWindowLong(hWnd, GWL_WNDPROC, (LONG)MyWndProc);
//為了能夠在全局函數中使用對話框類的東西,我們為窗口綁定一個對話框類的指針
SetWindowLong(hWnd, GWL_USERDATA, this);
接著在MyWndProc中處理對應的消息事件
LRESULT CALLBACK MyWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
CWebBrowserDlg* pDlg = (CWebBrowserDlg*)GetWindowLong(hWnd, GWL_USERDATA);
if (NULL == pDlg)
{
return CallWindowProc(g_OldProc, hWnd, uMsg, wParam, lParam);
}
switch (uMsg)
{
case WM_KEYUP:
{
unsigned int virtualKeyCode = wParam;
unsigned int flags = 0;
if (HIWORD(lParam) & KF_REPEAT)
flags |= WKE_REPEAT;
if (HIWORD(lParam) & KF_EXTENDED)
flags |= WKE_EXTENDED;
wkeFireKeyDownEvent(pDlg->m_web, virtualKeyCode, flags, false);
}
break;
case WM_KEYDOWN:
{
unsigned int virtualKeyCode = wParam;
unsigned int flags = 0;
if (HIWORD(lParam) & KF_REPEAT)
flags |= WKE_REPEAT;
if (HIWORD(lParam) & KF_EXTENDED)
flags |= WKE_EXTENDED;
wkeFireKeyUpEvent(pDlg->m_web, virtualKeyCode, flags, false);
}
break;
case WM_CHAR:
{
unsigned int charCode = wParam;
unsigned int flags = 0;
if (HIWORD(lParam) & KF_REPEAT)
flags |= WKE_REPEAT;
if (HIWORD(lParam) & KF_EXTENDED)
flags |= WKE_EXTENDED;
wkeFireKeyPressEvent(pDlg->m_web, charCode, flags, false);
}
break;
default:
return CallWindowProc(g_OldProc, hWnd, uMsg, wParam, lParam);
}
return 0;
}
這樣做之后我發現它雖然能夠截取到這些消息并執行它,但是在調用wkeFireKeyPressEvent等函數之后仍然無法響應鍵盤消息。難道是wkeCreateWebWindow 創建出來的窗口不能做子窗口?帶著這個疑問我根據官方文檔嘗試了一下使用wkeCreateWebView ,然后將它綁定到對應的窗口上,然后這個整體作為子窗口的方式。
代碼太長了,我就不放出來了,有興趣的可以翻到本文尾部,我將這個demo項目放到的GitHub上。
結果還是不行。這些函數仍然進不來。
真的郁悶,難道要換方案?我這個時候已經開始準備換方案了,在編譯wke 的時候心情極度煩躁,我在之前的程序上不停的敲擊鍵盤,就聽見“等等等~”。我靠!這不是想從模態對話框上切換回主頁面時的那個聲音嗎?會不會是因為模態對話框的關系?
這個時候我瞬間來了靈感。那就換吧,主要改一下APP類中相關代碼,吧模態改成非模態的就行
CWebBrowserDlg *dlg = new CWebBrowserDlg();
dlg->Create(IDD_WEBBROWSER_DIALOG);
m_pMainWnd = dlg;
INT_PTR nResponse = dlg->ShowWindow(SW_SHOW);
if (nResponse == IDOK)
{
// TODO: 在此放置處理何時用
// “確定”來關閉對話框的代碼
}
else if (nResponse == IDCANCEL)
{
// TODO: 在此放置處理何時用
// “取消”來關閉對話框的代碼
}
// 由于對話框已關閉,所以將返回 FALSE 以便退出應用程序,
// 而不是啟動應用程序的消息泵。
//由于是非模態對話框,所以這里需要自己寫消息環
MSG msg = { 0 };
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
delete dlg;
臥槽,居然成功了,能正常相應了!為什么模態就不行呢,后來我在復盤的時候想到,應該是wkeWebView的窗口并沒有做成那種嚴格意義上的子窗口,它是一個獨立的,所以模態對話框把消息給攔截了不讓傳到其他的窗口導致的這個問題。
這個也算是成功了。
這個時候問題又來了,程序關不掉了,雖然說窗口是關了,但是程序并沒有退出,后來調試發現,消息環沒有退出。這個時候我想到應該是關閉時調用的是EndDialog。但是此時已經改成非模態了,需要最后調用DestroyWindow,那么這個地方就得去對話框的OnClose消息中改。
void CWebBrowserDlg::OnClose()
{
// TODO: 在此添加消息處理程序代碼和/或調用默認值
DestroyWindow();
//CDialog::OnClose();
}
好了,這個時候基本已經完成了。就剩下一些按鈕事件處理了。
按鈕事件的處理
這里直接貼代碼吧,基本只有幾行,很容易看懂的
void CWebBrowserDlg::OnBnClickedBtnBack()
{
// TODO: 在此添加控件通知處理程序代碼
if (wkeCanGoBack(m_web))
{
wkeGoBack(m_web);
}
}
void CWebBrowserDlg::OnBnClickedBtnForward()
{
// TODO: 在此添加控件通知處理程序代碼
if (wkeCanGoForward(m_web))
{
wkeGoForward(m_web);
}
}
void CWebBrowserDlg::OnBnClickedBtnStop()
{
// TODO: 在此添加控件通知處理程序代碼
wkeStopLoading(m_web);
}
void CWebBrowserDlg::OnBnClickedBtnRefresh()
{
// TODO: 在此添加控件通知處理程序代碼
wkeReload(m_web);
}
void CWebBrowserDlg::OnBnClickedBtnGo()
{
// TODO: 在此添加控件通知處理程序代碼
CString csurl;
GetDlgItem(IDC_EDIT_URL)->GetWindowText(csurl);
wkeLoadURLW(m_web, csurl);
}
//設置代理
void CWebBrowserDlg::OnBnClickedBtnProxy()
{
CDlgProxySet dlgProxySet;
dlgProxySet.DoModal();
wkeProxy proxy;
proxy.type = WKE_PROXY_HTTP;
USES_CONVERSION;
strcpy_s(proxy.hostname, sizeof(proxy.hostname), T2A(dlgProxySet.csIP));
proxy.port = dlgProxySet.m_port;
wkeSetProxy(&proxy);
// TODO: 在此添加控件通知處理程序代碼
}
wkeView 的回調函數
現在主體功能已經完成了,要跟瀏覽器類似,需要處理這樣幾個東西。第一個是url欄中的內容會根據當前主頁面的url做調整,特別是針對302、301 跳轉的情況。第二個是窗口的標題應該改為頁面的標題;第三個是在某些頁面中超鏈接用的是_blank,時應該能正常打開新窗口。
為了實現這些目標,我們需要處理一些wkeView的事件,我們創建了wkeWebView 之后直接綁定這些事件
wkeOnTitleChanged(m_web, wkeOnTitleChangedCallBack, this); //最后一個參數是傳遞用戶數據,這里我們傳遞this指針進去
wkeOnURLChanged(m_web, wkeOnURLChangedCallBack, this);
wkeOnNavigation(m_web, wkeOnNavigationCallBack, this);
wkeOnCreateView(m_web, onBrowserCreateView, this);
// 頁面標題更改時調用此回調
void _cdecl wkeOnTitleChangedCallBack(wkeWebView webView, void* param, const wkeString title)
{
CWebBrowserDlg *pDlg = (CWebBrowserDlg*)param;
if (NULL != pDlg)
{
pDlg->SetWindowText(wkeGetStringW(title));
}
}
//url變更時調用此回調
void _cdecl wkeOnURLChangedCallBack(wkeWebView webView, void* param, const wkeString url)
{
CWebBrowserDlg *pDlg = (CWebBrowserDlg*)param;
if (NULL != pDlg)
{
pDlg->GetDlgItem(IDC_EDIT_URL)->SetWindowTextW(wkeGetStringW(url));
}
}
//網頁開始瀏覽將觸發回調, 這里主要是為了它能打開一些本地的程序
bool _cdecl wkeOnNavigationCallBack(wkeWebView webView, void* param, wkeNavigationType navigationType, const wkeString url)
{
const wchar_t* urlStr = wkeGetStringW(url);
if (wcsstr(urlStr, L"exec://") == urlStr) {
PROCESS_INFORMATION processInfo = { 0 };
STARTUPINFOW startupInfo = { 0 };
startupInfo.cb = sizeof(startupInfo);
BOOL succeeded = CreateProcessW(NULL, (LPWSTR)urlStr + 7, NULL, NULL, FALSE, 0, NULL, NULL, &startupInfo, &processInfo);
if (succeeded) {
CloseHandle(processInfo.hProcess);
CloseHandle(processInfo.hThread);
}
return false;
}
return true;
}
//網頁點擊a標簽創建新窗口時將觸發回調
wkeWebView _cdecl onBrowserCreateView(wkeWebView webView, void* param, wkeNavigationType navType, const wkeString urlStr, const wkeWindowFeatures* features)
{
const wchar_t* url = wkeGetStringW(urlStr);
wkeWebView newWindow = wkeCreateWebWindow(WKE_WINDOW_TYPE_POPUP, NULL, features->x, features->y, features->width, features->height);
wkeShowWindow(newWindow, true);
return newWindow;
}
至此這個瀏覽器的demo就完成了。最后貼上對應的demo項目地址: https://github.com/aMonst/WebBrowser
PS
:最近有一位朋友發郵件告訴我說,wkeWebView 不能響應鍵盤消息與對話框是模態還是非模態無關,主要是要處理wkeWebView的WM_GETDLGCODE 消息,那位朋友給出的代碼如下:
switch(uMsg)
{
case WM_GETDLGCODE:
return DLGC_WANTARROWS | DLGC_WANTALLKEYS | DLGC_WANTCHARS;
}
我試了一下,發現確實是這樣,相比較我上面提出的改為非模態的方式來說,還是用模態對話框方便、畢竟MFC對話框程序本來就是非模態的。所以這里我將代碼做了一下修改。并同步到了GitHub上。最后再次感謝那位發郵件告訴的朋友。。。。。
參考資料
閱讀原文:https://www.cnblogs.com/lanuage/p/18541298
該文章在 2025/1/8 15:20:35 編輯過