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

最近公司产品中自定义浏览器比较老,打开一些支持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的向导生成一个对话框并编辑资源文件。最后我的对话框大概长成这样


对话框.png

我会将按钮下面部分全部作为浏览器页面。

我们在程序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);

至此我们已经能够生成一个简单的浏览器程序


浏览器.png

似乎到这已经差不多该结束了,但是现在我遇到了在整个程序完成期间最大的问题,那就是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上。最后再次感谢那位发邮件告诉的朋友。。。。。

参考资料

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266

推荐阅读更多精彩内容

  • Windows 常用消息大全 表A-1 Windows消息分布 消息范围说 明 0 ~ WM_USER – 1系统...
    北风知我意阅读 1,847评论 0 0
  • windows消息机制(MFC) 消息分类与消息队列 Windows中,消息使用统一的结构体(MSG)来存放信息,...
    北风知我意阅读 5,316评论 0 1
  •   ECMAScript 是 JavaScript 的核心,但如果要在 Web 中使用 JavaScript,那么...
    霜天晓阅读 831评论 0 0
  • 日常开发中都是新建一个应用来单独接收rabbitmq的消息。 1.maven依赖 2.application.ym...
    孔垂云阅读 1,222评论 0 1
  • 铁甲钢拳是一部告诉我们坚持不懈精神的电影。里面的主角:查理,是一个拳击手,可是他又想要一个机器拳击手来打比赛。他一...
    王浩然666阅读 668评论 1 3