使用miniblink 在程序中嵌入浏览器

最近公司产品中自定义浏览器比较老,打开一些支持h5 的站莫名报错,而且经常弹框。已经到了令人无法忍受的地步了,于是我想到了将内核由之前的IE 升级到Chromium。之前想到的是使用cef来做,而且网上的资源和教程也很多,后来在自己尝试的过程中发现使用cef时程序会莫名其妙的崩溃,特别是在关闭对话框的时候。我在网上找了一堆资料,尝试了各种版本未果,这个方案也就放弃了。后来又搜到了wke和miniblink,对比二者官方的文档和demo,我决定使用miniblink,毕竟我直接搜索wke browser 出来的都是miniblink,只有搜索wke github 才会有真正的wke,而且wke似乎没有api文档,最后miniblink是国人写的,文档都是中文而且又有专门的qq交流群,有问题可以咨询一下。

miniblink 是由国内大神 龙泉寺扫地僧 针对chromium内核进行裁剪去掉了所有多余的部件,只保留最基本的排版引擎blink,而产生的一款号称全球小巧的浏览器内核项目,目前miniblink 保持了10M左右的极简大小,相比CEF 动辄几百M的大小确实小巧许多。而且能很好的支持H5等一些列新标准,同时它内嵌了Nodejs 支持electron。而且也支持各种语言调用。

官方的地址如下为 https://weolar.github.io/miniblink/index.html

说了这么多那么该怎么用呢?从官方的介绍来看,我们可以使用VS的向导程序生成一个普通的win32 窗口程序,然后生成的这些代码中将函数InitInstance 中的代码全部删除加上这么5句话

1
2
3
4
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的相关资源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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视图的句柄

1
wkeWebView m_web;

我们在对话框的OnInitDialog函数中创建这么一个视图,用来加载百度的首页面

1
2
3
4
5
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发送键盘消息就可以了.于是我在对应的处理函数中添加了相关代码

1
2
3
4
5
6
7
8
//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);

1
2
3
4
5
6
7
8
//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);
1
2
3
4
5
6
7
8
//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来修改窗口的窗口过程,然后在窗口过程中处理这些消息就行了。根据这个思路整理一下代码

1
2
3
4
5
//在创建wkeWebView 之后来hook窗口过程
HWND hWnd = wkeGetHostHWND(m_web);
g_OldProc = (WNDPROC)SetWindowLong(hWnd, GWL_WNDPROC, (LONG)MyWndProc);
//为了能够在全局函数中使用对话框类的东西,我们为窗口绑定一个对话框类的指针
SetWindowLong(hWnd, GWL_USERDATA, this);

接着在MyWndProc中处理对应的消息事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
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类中相关代码,吧模态改成非模态的就行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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消息中改。

1
2
3
4
5
6
void CWebBrowserDlg::OnClose()
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
DestroyWindow();
//CDialog::OnClose();
}

好了,这个时候基本已经完成了。就剩下一些按钮事件处理了。

按钮事件的处理

这里直接贴代码吧,基本只有几行,很容易看懂的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
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 之后直接绑定这些事件

1
2
3
4
wkeOnTitleChanged(m_web, wkeOnTitleChangedCallBack, this); //最后一个参数是传递用户数据,这里我们传递this指针进去
wkeOnURLChanged(m_web, wkeOnURLChangedCallBack, this);
wkeOnNavigation(m_web, wkeOnNavigationCallBack, this);
wkeOnCreateView(m_web, onBrowserCreateView, this);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// 页面标题更改时调用此回调
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 消息,那位朋友给出的代码如下:

1
2
3
4
5
switch(uMsg)
{
case WM_GETDLGCODE:
return DLGC_WANTARROWS | DLGC_WANTALLKEYS | DLGC_WANTCHARS;
}

我试了一下,发现确实是这样,相比较我上面提出的改为非模态的方式来说,还是用模态对话框方便、毕竟MFC对话框程序本来就是非模态的。所以这里我将代码做了一下修改。并同步到了GitHub上。最后再次感谢那位发邮件告诉的朋友。。。。。

参考资料