最近公司產(chǎn)品中自定義瀏覽器比較老,打開一些支持h5 的站莫名報(bào)錯(cuò),而且經(jīng)常彈框。已經(jīng)到了令人無法忍受的地步了,于是我想到了將內(nèi)核由之前的IE 升級(jí)到Chromium。之前想到的是使用cef來做,而且網(wǎng)上的資源和教程也很多,后來在自己嘗試的過程中發(fā)現(xiàn)使用cef時(shí)程序會(huì)莫名其妙的崩潰,特別是在關(guān)閉對(duì)話框的時(shí)候。我在網(wǎng)上找了一堆資料,嘗試了各種版本未果,這個(gè)方案也就放棄了。后來又搜到了wke和miniblink,對(duì)比二者官方的文檔和demo,我決定使用miniblink,畢竟我直接搜索wke browser 出來的都是miniblink,只有搜索wke github 才會(huì)有真正的wke,而且wke似乎沒有api文檔,最后miniblink是國(guó)人寫的,文檔都是中文而且又有專門的qq交流群,有問題可以咨詢一下。
什么是miniblink
miniblink 是由國(guó)內(nèi)大神 龍泉寺掃地僧 針對(duì)chromium內(nèi)核進(jìn)行裁剪去掉了所有多余的部件,只保留最基本的排版引擎blink,而產(chǎn)生的一款號(hào)稱全球小巧的瀏覽器內(nèi)核項(xiàng)目,目前miniblink 保持了10M左右的極簡(jiǎn)大小,相比CEF 動(dòng)輒幾百M(fèi)的大小確實(shí)小巧許多。而且能很好的支持H5等一些列新標(biāo)準(zhǔn),同時(shí)它內(nèi)嵌了Nodejs 支持electron。而且也支持各種語(yǔ)言調(diào)用。
官方的地址如下為 https://weolar.github.io/miniblink/index.html
使用miniblink
說了這么多那么該怎么用呢?從官方的介紹來看,我們可以使用VS的向?qū)С绦蛏梢粋€(gè)普通的win32 窗口程序,然后生成的這些代碼中將函數(shù)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);
當(dāng)然,使用這些函數(shù)需要下載它的SDK開發(fā)包,然后在對(duì)應(yīng)位置包含wke.h。
這些代碼會(huì)生成一個(gè)窗口程序,具體的請(qǐng)敢興趣的朋友自己去實(shí)踐看看效果。或者編譯運(yùn)行一下它的demo程序。
在對(duì)話框中使用
現(xiàn)在我想在對(duì)話框中使用,那么該怎么辦呢。
首先也是先用MFC的向?qū)梢粋€(gè)對(duì)話框并編輯資源文件。最后我的對(duì)話框大概長(zhǎng)成這樣
我會(huì)將按鈕下面部分全部作為瀏覽器頁(yè)面。
我們?cè)诔绦駻PP類的InitInstance函數(shù) 中初始化miniblink庫(kù),并在對(duì)話框被關(guān)閉后直接卸載miniblink的相關(guān)資源
wkeSetWkeDllPath(L"node.dll");
wkeInitialize();
CWebBrowserDlg dlg = CWebBrowserDlg();
m_pMainWnd = dlg;
INT_PTR nResponse = dlg.DoModal();
if (nResponse == IDOK)
{
// TODO: 在此放置處理何時(shí)用
// “確定”來關(guān)閉對(duì)話框的代碼
}
else if (nResponse == IDCANCEL)
{
// TODO: 在此放置處理何時(shí)用
// “取消”來關(guān)閉對(duì)話框的代碼
}
// 由于對(duì)話框已關(guān)閉,所以將返回 FALSE 以便退出應(yīng)用程序,
// 而不是啟動(dòng)應(yīng)用程序的消息泵。
wkeFinalize();
然后在主對(duì)話框類中新增一個(gè)成員變量用來保存miniblink的web視圖的句柄
wkeWebView m_web;
我們?cè)趯?duì)話框的OnInitDialog函數(shù)中創(chuàng)建這么一個(gè)視圖,用來加載百度的首頁(yè)面
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);
至此我們已經(jīng)能夠生成一個(gè)簡(jiǎn)單的瀏覽器程序
似乎到這已經(jīng)差不多該結(jié)束了,但是現(xiàn)在我遇到了在整個(gè)程序完成期間最大的問題,那就是web頁(yè)面無法響應(yīng)鍵盤消息,我嘗試過改成窗口程序,發(fā)現(xiàn)改了之后能正常運(yùn)行,但是我要的是對(duì)話框啊。這么改只能證明這個(gè)庫(kù)是沒問題的。
后來我在群里面發(fā)出了這樣的疑問,有朋友告訴我說應(yīng)該是wkeWebView沒有接受到鍵盤消息,于是我打算處理主對(duì)話框的WM_KEYDOWN 和WM_KEYUP 以及WM_CHAR消息,根據(jù)官方的文檔,應(yīng)該是只需要攔截對(duì)話框的這三個(gè)消息,然后使用函數(shù)wkeFireKeyUpEvent、wkeFireKeyDownEvent、wkeFireKeyPressEvent函數(shù)分別向wkeWebView發(fā)送鍵盤消息就可以了.于是我在對(duì)應(yīng)的處理函數(shù)中添加了相關(guān)代碼
//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);
但是這么干,我通過調(diào)試發(fā)現(xiàn)它好像并沒有進(jìn)入到這些函數(shù)里面來,也就是說鍵盤消息不是由主對(duì)話框來處理的。那么現(xiàn)在只能在wkeWebView 對(duì)應(yīng)的窗口中來處理了。那么怎么捕獲這個(gè)窗口的消息呢,miniblink提供了函數(shù)wkeGetHostHWND 來根據(jù)視圖的句柄獲取對(duì)應(yīng)窗口的句柄,那么現(xiàn)在的思路就是這樣的:首先獲取對(duì)應(yīng)的窗口句柄然后通過SetWindowLong來修改窗口的窗口過程,然后在窗口過程中處理這些消息就行了。根據(jù)這個(gè)思路整理一下代碼
//在創(chuàng)建wkeWebView 之后來hook窗口過程
HWND hWnd = wkeGetHostHWND(m_web);
g_OldProc = (WNDPROC)SetWindowLong(hWnd, GWL_WNDPROC, (LONG)MyWndProc);
//為了能夠在全局函數(shù)中使用對(duì)話框類的東西,我們?yōu)榇翱诮壎ㄒ粋€(gè)對(duì)話框類的指針
SetWindowLong(hWnd, GWL_USERDATA, this);
接著在MyWndProc中處理對(duì)應(yīng)的消息事件
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;
}
這樣做之后我發(fā)現(xiàn)它雖然能夠截取到這些消息并執(zhí)行它,但是在調(diào)用wkeFireKeyPressEvent等函數(shù)之后仍然無法響應(yīng)鍵盤消息。難道是wkeCreateWebWindow 創(chuàng)建出來的窗口不能做子窗口?帶著這個(gè)疑問我根據(jù)官方文檔嘗試了一下使用wkeCreateWebView ,然后將它綁定到對(duì)應(yīng)的窗口上,然后這個(gè)整體作為子窗口的方式。
代碼太長(zhǎng)了,我就不放出來了,有興趣的可以翻到本文尾部,我將這個(gè)demo項(xiàng)目放到的GitHub上。
結(jié)果還是不行。這些函數(shù)仍然進(jìn)不來。
真的郁悶,難道要換方案?我這個(gè)時(shí)候已經(jīng)開始準(zhǔn)備換方案了,在編譯wke 的時(shí)候心情極度煩躁,我在之前的程序上不停的敲擊鍵盤,就聽見“等等等~”。我靠!這不是想從模態(tài)對(duì)話框上切換回主頁(yè)面時(shí)的那個(gè)聲音嗎?會(huì)不會(huì)是因?yàn)槟B(tài)對(duì)話框的關(guān)系?
這個(gè)時(shí)候我瞬間來了靈感。那就換吧,主要改一下APP類中相關(guān)代碼,吧模態(tài)改成非模態(tài)的就行
CWebBrowserDlg *dlg = new CWebBrowserDlg();
dlg->Create(IDD_WEBBROWSER_DIALOG);
m_pMainWnd = dlg;
INT_PTR nResponse = dlg->ShowWindow(SW_SHOW);
if (nResponse == IDOK)
{
// TODO: 在此放置處理何時(shí)用
// “確定”來關(guān)閉對(duì)話框的代碼
}
else if (nResponse == IDCANCEL)
{
// TODO: 在此放置處理何時(shí)用
// “取消”來關(guān)閉對(duì)話框的代碼
}
// 由于對(duì)話框已關(guān)閉,所以將返回 FALSE 以便退出應(yīng)用程序,
// 而不是啟動(dòng)應(yīng)用程序的消息泵。
//由于是非模態(tài)對(duì)話框,所以這里需要自己寫消息環(huán)
MSG msg = { 0 };
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
delete dlg;
臥槽,居然成功了,能正常相應(yīng)了!為什么模態(tài)就不行呢,后來我在復(fù)盤的時(shí)候想到,應(yīng)該是wkeWebView的窗口并沒有做成那種嚴(yán)格意義上的子窗口,它是一個(gè)獨(dú)立的,所以模態(tài)對(duì)話框把消息給攔截了不讓傳到其他的窗口導(dǎo)致的這個(gè)問題。
這個(gè)也算是成功了。
這個(gè)時(shí)候問題又來了,程序關(guān)不掉了,雖然說窗口是關(guān)了,但是程序并沒有退出,后來調(diào)試發(fā)現(xiàn),消息環(huán)沒有退出。這個(gè)時(shí)候我想到應(yīng)該是關(guān)閉時(shí)調(diào)用的是EndDialog。但是此時(shí)已經(jīng)改成非模態(tài)了,需要最后調(diào)用DestroyWindow,那么這個(gè)地方就得去對(duì)話框的OnClose消息中改。
void CWebBrowserDlg::OnClose()
{
// TODO: 在此添加消息處理程序代碼和/或調(diào)用默認(rèn)值
DestroyWindow();
//CDialog::OnClose();
}
好了,這個(gè)時(shí)候基本已經(jīng)完成了。就剩下一些按鈕事件處理了。
按鈕事件的處理
這里直接貼代碼吧,基本只有幾行,很容易看懂的
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);
}
//設(shè)置代理
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 的回調(diào)函數(shù)
現(xiàn)在主體功能已經(jīng)完成了,要跟瀏覽器類似,需要處理這樣幾個(gè)東西。第一個(gè)是url欄中的內(nèi)容會(huì)根據(jù)當(dāng)前主頁(yè)面的url做調(diào)整,特別是針對(duì)302、301 跳轉(zhuǎn)的情況。第二個(gè)是窗口的標(biāo)題應(yīng)該改為頁(yè)面的標(biāo)題;第三個(gè)是在某些頁(yè)面中超鏈接用的是_blank,時(shí)應(yīng)該能正常打開新窗口。
為了實(shí)現(xiàn)這些目標(biāo),我們需要處理一些wkeView的事件,我們創(chuàng)建了wkeWebView 之后直接綁定這些事件
wkeOnTitleChanged(m_web, wkeOnTitleChangedCallBack, this); //最后一個(gè)參數(shù)是傳遞用戶數(shù)據(jù),這里我們傳遞this指針進(jìn)去
wkeOnURLChanged(m_web, wkeOnURLChangedCallBack, this);
wkeOnNavigation(m_web, wkeOnNavigationCallBack, this);
wkeOnCreateView(m_web, onBrowserCreateView, this);
// 頁(yè)面標(biāo)題更改時(shí)調(diào)用此回調(diào)
void _cdecl wkeOnTitleChangedCallBack(wkeWebView webView, void* param, const wkeString title)
{
CWebBrowserDlg *pDlg = (CWebBrowserDlg*)param;
if (NULL != pDlg)
{
pDlg->SetWindowText(wkeGetStringW(title));
}
}
//url變更時(shí)調(diào)用此回調(diào)
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));
}
}
//網(wǎng)頁(yè)開始瀏覽將觸發(fā)回調(diào), 這里主要是為了它能打開一些本地的程序
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;
}
//網(wǎng)頁(yè)點(diǎn)擊a標(biāo)簽創(chuàng)建新窗口時(shí)將觸發(fā)回調(diào)
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;
}
至此這個(gè)瀏覽器的demo就完成了。最后貼上對(duì)應(yīng)的demo項(xiàng)目地址: https://github.com/aMonst/WebBrowser
PS
:最近有一位朋友發(fā)郵件告訴我說,wkeWebView 不能響應(yīng)鍵盤消息與對(duì)話框是模態(tài)還是非模態(tài)無關(guān),主要是要處理wkeWebView的WM_GETDLGCODE 消息,那位朋友給出的代碼如下:
switch(uMsg)
{
case WM_GETDLGCODE:
return DLGC_WANTARROWS | DLGC_WANTALLKEYS | DLGC_WANTCHARS;
}
我試了一下,發(fā)現(xiàn)確實(shí)是這樣,相比較我上面提出的改為非模態(tài)的方式來說,還是用模態(tài)對(duì)話框方便、畢竟MFC對(duì)話框程序本來就是非模態(tài)的。所以這里我將代碼做了一下修改。并同步到了GitHub上。最后再次感謝那位發(fā)郵件告訴的朋友。。。。。
參考資料
閱讀原文:https://www.cnblogs.com/lanuage/p/18541298
該文章在 2025/1/8 15:20:35 編輯過