如果你有一个现成的MFC项目在做维护，但是你厌倦了使用MFC繁琐的操作来做界面美化，或者你需要在这个项目中用到QT里面好用的某些功能；亦或者是你需要使用某些只能在MFC中使用的组件，但是界面这部分已经用QT做好了。那么这篇文章可能可以帮助到你演示环境使用Visual Studio 2019 + QT5.12.8 版本添加QT依赖首先创建一个基于对话框的MFC工程，当然其他的像是多文档、单文档工程也是可以的，只是为了简单起见我这里用的是对话框然后通过鼠标右键点击项目，然后依次点击属性 --> C/C++ -->常规在工程的附加头文件中添加上QCore、QGui、QWidget和QT的头文件路径这里记得按照对应编译选项来选择包含64位或者32。接着在连接器-->常规 中的附加库目录中添加qt的lib库最后再在连接器-->输入中添加依赖的lib文件，需要注意的是，debug版本需要链接上带d的lib文件，release则链接上不带d的。先编译一下，如果没有问题，qt相关的配置已经完成了添加信号槽机制MFC是基于Windows 消息队列来处理和响应ui事件的，而qt是采用信号槽机制来响应的，我们虽然添加了qt的依赖，但是现在只能使用其他的qt库，无法使用qt中的信号槽，需要额外添加一些组件来使mfc支持信号槽。好在这部分需求qt相关的研发人员已经考虑到了，可以在github中找到 QMfcApp我们可以将这两个文件给拷贝下来，添加到项目中。并且在cpp文件相应位置添加上 #include "pch.h"包含预处理头中间会有报错，这是因为在Unicode 字符集下 CString 中的字符串类型是 wchar_t* QString::fromLocal8bit 无法 从 wchar_t* 转化为 char* 所以这里可以修改一下，使用 QString::fromStdWString(),然后进行编译在QMfcApp.cpp的注释里面可以看到，如何使用它/*! Creates an instance of QApplication, passing the command line of \a mfcApp to the QApplication constructor, and returns the new object. The returned object must be destroyed by the caller. Use this static function if you want to perform additional initializations after creating the application object, or if you want to create Qt GUI elements in the InitInstance() reimplementation of CWinApp: \code BOOL MyMfcApp::InitInstance() { // standard MFC initialization // ... // This sets the global qApp pointer QMfcApp::instance(this); // Qt GUI initialization } BOOL MyMfcApp::Run() { int result = QMfcApp::run(this); delete qApp; return result; } \endcode \sa run() */首先在app类的InitInstance 函数中初始化QApplication类BOOL CMFCWithQtApp::InitInstance() { CWinApp::InitInstance(); QMfcApp::instance(this); return true; }然后需要重写 app类的run 方法，在该方法中调用QMFC 的run方法int CMFCWithQtApp::Run() { int result = QMfcApp::run(this); delete qApp; return result; }再次编译一下，完成了往mfc中添加信号槽机制的功能添加qt界面在项目中新建一个界面类，让他继承自QWidget，如下// MainUI.h #pragma once #include <QWidget> class MainUI: public QWidget { Q_OBJECT public: MainUI(QWidget* parent = nullptr); ~MainUI(); }; //MainUI.cpp #include "pch.h" #include "MainUI.h" #include <QPushButton> MainUI::MainUI(QWidget* parent) : QWidget(parent) { setWindowTitle("Qt Windows"); setFixedSize(800, 720); QPushButton* pBtn = new QPushButton(QString::fromLocal8Bit("这是一个Qt按钮"), this); } MainUI::~MainUI() { }然后在App 类的 InitInstance 中启动该界面BOOL CMFCWithQtApp::InitInstance() { CWinApp::InitInstance(); QMfcApp::instance(this); MainUI ui; ui.show(); QMfcApp::exec(); return FALSE; }然后编译，这个时候发现会在链接的时候包一些错误，找不到一些 meta 的函数的定义配置元编译过程传统的c/c++ 从源代码到生成可执行文件的过程需要经过预编译、编译、链接。而qt在预编译前会进行元编译，生成一个moc_开头的源码文件，后续编译的真正的文件其实是这个元编译生成的文件。MFC项目不会经历这一步，所以会报错。在MainUI.h 上点击右键，选择属性, 将项目类型选择为自定义生成工具然后应用，这个时候会出现新的选项在命令行和输入这两栏中分别填入 "moc.exe" "%(FullPath)" -o ".\GeneratedFiles\moc_%(Filename).cpp" "-fpch.h" "-f../MainUI.h" 和 .\GeneratedFiles\moc_%(Filename).cpp命令行的含义是会使用moc元编译器编译当前文件，并将生成的文件放入到当前目录下的GeneratedFiles子目录中，并且以moc_开头作为文件名，后面 -f 表示生成的新文件中会包含 #include "pch.h" 和 #include "../MainUI.h" 然后在文件中选择右键，编译。这样将会生成moc文件(两边的双引号也好包含进去)如果编译的时候报找不到moc.exe 这样的错误，请配置QT中的bin路径到环境变量中我们将新生成的文件添加到项目中再次编译，成功过后点击运行就可以看到qt界面已经展示出来了一些问题的处理窗口出来了，但是在我的环境下出现两个问题，关闭窗口后进程无法退出；程序退出后出现内存泄露的问题针对这两个问题可以在QMfcApp.cpp 文件中修改// 表示在最后一个qt窗口退出时，关闭QApplication setQuitOnLastWindowClosed(true); //将之前的false改为true// ~QMfcApp() 中添加这两句，当析构完成后关闭进程 HANDLE hself = GetCurrentProcess(); TerminateProcess(hself, 0);测试信号槽我们可以在MainUI中添加信号槽MainUI::MainUI(QWidget* parent) : QWidget(parent) { setWindowTitle("Qt Windows"); setFixedSize(800, 720); QPushButton* pBtn = new QPushButton(QString::fromLocal8Bit("这是一个Qt按钮"), this); connect(pBtn, &QPushButton::clicked, [=]() { QMessageBox::information(this, QString::fromLocal8Bit("信号槽"), QString::fromLocal8Bit("这是由信号槽弹出来的")); }); }点击按钮之后，消息框也正常弹出来了使用qt designer 设计界面使用 qtdesigner 设计这样一个界面qtdesigner 会生成一个.ui文件，在qt的开发环境中，会自动使用uic.exe 将这个文件生成一个对应的.h文件。我们先将ui文件导入到项目，并且按照之前的步骤设置自定义生成工具，填入如下命令行"uic.exe" "%(FullPath)" -o ".\ui_%(Filename).h"并且填写上输出路径.\ui_%(Filename).h编译之后会生成一个对应的ui_MainUi.h 文件，修改对应的MainUI.h 头文件，加上关于它的引用，并且添加一个ui的对象指针//MainUI.h #pragma once #include <QWidget> #include "ui_Main.h" class MainUI: public QWidget { Q_OBJECT public: MainUI(QWidget* parent = nullptr); ~MainUI(); private: Ui::mainUI* m_pUI; };在类的构造中，使用ui对象来产生界面元素//MainUI.cpp #include "pch.h" #include "MainUI.h" #include <QPushButton> #include <QMessageBox> MainUI::MainUI(QWidget* parent) : QWidget(parent), m_pUI(new Ui::mainUI()) { m_pUI->setupUi(this); connect(m_pUI->pushButton, &QPushButton::clicked, [=]() { QMessageBox::information(this, QString::fromLocal8Bit("qt 信号槽测试"), m_pUI->lineEdit->text()); }); } MainUI::~MainUI() { delete m_pUI; }执行效果如下
最近公司产品中自定义浏览器比较老，打开一些支持h5 的站莫名报错，而且经常弹框。已经到了令人无法忍受的地步了，于是我想到了将内核由之前的IE 升级到Chromium。之前想到的是使用cef来做，而且网上的资源和教程也很多，后来在自己尝试的过程中发现使用cef时程序会莫名其妙的崩溃，特别是在关闭对话框的时候。我在网上找了一堆资料，尝试了各种版本未果，这个方案也就放弃了。后来又搜到了wke和miniblink，对比二者官方的文档和demo，我决定使用miniblink，毕竟我直接搜索wke browser 出来的都是miniblink，只有搜索wke github 才会有真正的wke，而且wke似乎没有api文档，最后miniblink是国人写的，文档都是中文而且又有专门的qq交流群，有问题可以咨询一下。什么是miniblinkminiblink 是由国内大神 龙泉寺扫地僧 针对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/WebBrowserPS:最近有一位朋友发邮件告诉我说，wkeWebView 不能响应键盘消息与对话框是模态还是非模态无关，主要是要处理wkeWebView的WM_GETDLGCODE 消息，那位朋友给出的代码如下:switch(uMsg) { case WM_GETDLGCODE: return DLGC_WANTARROWS | DLGC_WANTALLKEYS | DLGC_WANTCHARS; }我试了一下，发现确实是这样，相比较我上面提出的改为非模态的方式来说，还是用模态对话框方便、毕竟MFC对话框程序本来就是非模态的。所以这里我将代码做了一下修改。并同步到了GitHub上。最后再次感谢那位发邮件告诉的朋友。。。。。参考资料miniblink API文档官方DEMO