如果你有一个现成的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; }执行效果如下
最近我一个同学在项目中使用到了duilib框架，但是之前并没有接触过，他与我讨论这方面的内容，看着官方给出的精美的例子，我对这个库有了很大的兴趣，我自己也是初学这个东东，我在网上花了不少时间来找相关的资料，但是找到的不多，官方给的文档又不全面，但是我还是找到了一些博主贡献的优秀的博文，现在我是通过博文上的讲解加上自己查看源代码的一些心得，正在艰难的前行。现在正在看的是博主Alberl在博客园中的duilib基础教程中的内容，下面的代码都是在他博客中给出代码的基础上做了一点小小的修改。点击这里跳转到对应的博客，以及博主夜雨無聲的博客，博客地址duilib的简介国内首个开源 的directui 界面库,它提供了一个所见即所得的开发工具——UIDesigner，它只有主框架窗口，其余的空间全部采用绘制的方式实现，所以对于控件来说没有句柄和窗口类等内容，它通过UIDesigner工具将用户定义的窗口保存在xml文件中，在创建窗口时读取xml文件中的内容，来绘制相应的控件。目前有许多界面采用duilib编写，大家可以去网上搜集相关资料。环境的配置首先我们去github上获取相关的源代码，这个是对应的项目地址：https://github.com/duilib/duilib下载完后，在目录中找到一个.sln结尾的文件，使用visual studio编译器打开，打开后发现有一个duilib的项目，以及其他，其实真正有用的就是这个duilib，其余的都是官方给出的例子代码。一般只需要编译这个duilib项目就可以了，当初没注意直接点了编译全部的，结果报了一堆错误，其实都是没有对应的lib和dll文件造成的。在VS环境下有一个编译选项，如下图所示上面有4个编译选项，最好将所有的都编译一遍，这样在对应项目的bin目录下会生成四个dll文件，这几个文件分别是debug下的UNICODE编码文件、ANSI文件以及Release版本下的UNICODE编码文件、ANSI文件。u代表unicode d代表debug。另外在lib目录下会生成对应的lib文件。在新建的工程中，点击属性在属性对话框中选择VC++目录，在源文件，库文件，包含文件中将对应的路径添加进去，分别是项目目录和lib文件目录。如下图（刚开始有点问题所以添加的内容有点多，但是不影响正常使用）：最后可以在环境变量的Path变量中添加对应的dll路径，这样就不需要将dll文件拷贝到自己项目的exe文件所在位置处。其实上述的环境可以不用设置，如果不设置，在编写程序包含相关路径时就需要给定完整的路径。到此处为止，整个开发环境就已经搭建好了，剩下的就是代码的编写了。基本的框架窗口首先新建一个Win32类型的项目，添加主函数。然后创建一个新类，我们叫做CDuiFrameWnd，下面是类的源代码//头文件 #include <DuiLib\UIlib.h> using namespace DuiLib; class CDuiFrameWnd : public CWindowWnd { public: CDuiFrameWnd(); ~CDuiFrameWnd(); virtual LPCTSTR GetWindowClassName() const; virtual LRESULT HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam); protected: CPaintManagerUI m_PaintManager; };//cpp文件 LPCTSTR CDuiFrameWnd::GetWindowClassName() const { return _T("DuiFrameWnd"); } LRESULT CDuiFrameWnd::HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam) { LRESULT lRes = 0; if (WM_CLOSE == uMsg) { ::CloseWindow(m_hWnd); ::DestroyWindow(m_hWnd); } if (WM_DESTROY == uMsg) { ::PostQuitMessage(0); } return __super::HandleMessage(uMsg, wParam, lParam); }为了能够使用对应的dll文件，还需要引入对应的lib文件，我们在公共的头文件中加入如下代码#ifdef _DEBUG # ifdef _UNICODE # pragma comment(lib, "Duilib_ud.lib") # else # pragma comment(lib, "Duilib_d.lib") # endif #else # ifdef _UNICODE # pragma comment(lib, "Duilib_u.lib") # else # pragma comment(lib, "Duilib.lib") # endif #endif在主函数中的代码如下：int APIENTRY wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPWSTR lpCmdLine, _In_ int nCmdShow) { CPaintManagerUI::SetInstance(hInstance); CDuiFrameWnd duiFrame; //#define UI_WNDSTYLE_FRAME (WS_VISIBLE | WS_OVERLAPPEDWINDOW) duiFrame.Create(NULL, _T("测试"), UI_WNDSTYLE_FRAME, WS_EX_WINDOWEDGE); duiFrame.ShowWindow(); CPaintManagerUI::MessageLoop(); return 0; }这些代码就可以帮助我们生成基本的框架窗口，另外我们需要时刻记住的是duilib是对win32 API的封装，所以可以直接使用win32的编程方式，如果以后有不会用的地方完全可以使用win32 的API来完成相关的功能的编写。框架的剖析既然它能够生成单文档的框架窗口，那么代码中所做的几步基本上与用纯粹的win32 API相同，所以我们沿着这个思路来进行框架的简单剖析。主函数中首先是代码CPaintManagerUI::SetInstance(hInstance);至于类CPaintManagerUI到底有什么作用，这个我也不太清楚，现在我还没有仔细看关于这个类的相关代码，这句话主要还是获取了进程的实例句柄。现在先不关心这个。下面的几步主要是在类CDuiFrameWnd中完成或者说在它的基类CWindowWnd中完成。创建窗口类主函数中的第二段代码主要完成的是类CDuiFrameWnd对象的创建，我们跟到对应的构造函数中发现它并没有做多余的操作，现在先不管它是如何构造的，它下面就是调用了类的Create函数创建了一个窗口，这个函数的代码如下：HWND CWindowWnd::Create(HWND hwndParent, LPCTSTR pstrName, DWORD dwStyle, DWORD dwExStyle, int x, int y, int cx, int cy, HMENU hMenu) { if( GetSuperClassName() != NULL && !RegisterSuperclass() ) return NULL; if( GetSuperClassName() == NULL && !RegisterWindowClass() ) return NULL; m_hWnd = ::CreateWindowEx(dwExStyle, GetWindowClassName(), pstrName, dwStyle, x, y, cx, cy, hwndParent, hMenu, CPaintManagerUI::GetInstance(), this); ASSERT(m_hWnd!=NULL); return m_hWnd; }我们主要来看第二个if中的代码，首先获得了父窗口的字符串为NULL，然后执行RegisterWindowClass，我们进一步跟到RegisterWindowClass中，它的代码如下：bool CWindowWnd::RegisterWindowClass() { WNDCLASS wc = { 0 }; wc.style = GetClassStyle(); wc.cbClsExtra = 0; wc.cbWndExtra = 0; wc.hIcon = NULL; wc.lpfnWndProc = CWindowWnd::__WndProc; wc.hInstance = CPaintManagerUI::GetInstance(); //之前设置的实例句柄在这个地方使用 wc.hCursor = ::LoadCursor(NULL, IDC_ARROW); wc.hbrBackground = NULL; wc.lpszMenuName = NULL; wc.lpszClassName = GetWindowClassName(); ATOM ret = ::RegisterClass(&wc); ASSERT(ret!=NULL || ::GetLastError()==ERROR_CLASS_ALREADY_EXISTS); return ret != NULL || ::GetLastError() == ERROR_CLASS_ALREADY_EXISTS; }我们发现首先进行的是窗口类的创建，在创建窗口类时主要关心的是窗口类的lpfnWndProc成员和lpszClassName 。lpszClassName 调用了函数GetWindowClassName，这个函数我们在派生类中进行了重写，所以根据多态它会调用派生类的GetWindowClassName函数，将我们给定的字符串作为窗口类的类名注册窗口类从上面的代码可以看出注册的代码也是放在RegisterWindowClass中。在最后调用了RegisterClass函数完成了注册。创建窗口当RegisterWindowClass执行完成后，会接着执行下面的代码，也就是 m_hWnd = ::CreateWindowEx(dwExStyle, GetWindowClassName(), pstrName, dwStyle, x, y, cx, cy, hwndParent, hMenu, CPaintManagerUI::GetInstance(), this);完成创建窗口的任务。显示窗口Create函数执行完成后，会接着执行下面的duiFrame.ShowWindow();我们跟到这个函数中，函数代码如下：void CWindowWnd::ShowWindow(bool bShow /*= true*/, bool bTakeFocus /*= false*/) { ASSERT(::IsWindow(m_hWnd)); if( !::IsWindow(m_hWnd) ) return; ::ShowWindow(m_hWnd, bShow ? (bTakeFocus ? SW_SHOWNORMAL : SW_SHOWNOACTIVATE) : SW_HIDE); }函数ShowWindow默认传入参数为bShow = true bTakeFocus = false；在最后进行ShowWindow函数的调用时，根据bShow和bTakeFocus来进行值得传入，根据代码我们发现，当不传入参数时调用的其实是这样的代码ShowWindow(m_hWnd, SW_SHOWNOACTIVATE);消息循环消息循环其实是通过代码CPaintManagerUI::MessageLoop();完成，我们跟到MessageLoop函数中看 MSG msg = { 0 }; while( ::GetMessage(&msg, NULL, 0, 0) ) { if( !CPaintManagerUI::TranslateMessage(&msg) ) { ::TranslateMessage(&msg); ::DispatchMessage(&msg); } }在这个函数中完成了消息循环。回调函数上面我们留了一个lpfnWndProc函数指针没有说，现在来说明这个部分，跟进到对应的构造函数中，发现类本身不做任何操作，但是父类的构造函数进行了相关的初始化操作，下面是对应的代码CWindowWnd::CWindowWnd() : m_hWnd(NULL), m_OldWndProc(::DefWindowProc), m_bSubclassed(false) { }这样就将lpfnWndProc指向了__WndProc，用于处理默认的消息。这是一个静态的处理函数，下面是它的代码：LRESULT CALLBACK CWindowWnd::__WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { CWindowWnd* pThis = NULL; if( uMsg == WM_NCCREATE ) { LPCREATESTRUCT lpcs = reinterpret_cast<LPCREATESTRUCT>(lParam); pThis = static_cast<CWindowWnd*>(lpcs->lpCreateParams); pThis->m_hWnd = hWnd; //当开始创建窗口将窗口类对象的指针放入到对应的GWLP_USERDATA字段中 ::SetWindowLongPtr(hWnd, GWLP_USERDATA, reinterpret_cast<LPARAM>(pThis)); } else { //取出窗口类对象的指针 pThis = reinterpret_cast<CWindowWnd*>(::GetWindowLongPtr(hWnd, GWLP_USERDATA)); if( uMsg == WM_NCDESTROY && pThis != NULL ) { LRESULT lRes = ::CallWindowProc(pThis->m_OldWndProc, hWnd, uMsg, wParam, lParam); ::SetWindowLongPtr(pThis->m_hWnd, GWLP_USERDATA, 0L); if( pThis->m_bSubclassed ) pThis->Unsubclass(); pThis->m_hWnd = NULL; pThis->OnFinalMessage(hWnd); return lRes; } } if( pThis != NULL ) { return pThis->HandleMessage(uMsg, wParam, lParam); } else { return ::DefWindowProc(hWnd, uMsg, wParam, lParam); } }上述的代码，在创建窗口时将窗口类对象指针存入到对应的位置便于在其他位置取出并使用。通过return pThis->HandleMessage(uMsg, wParam, lParam);这句话调用的具体对象的HandleMessage，我们在对应的派生类中定义了相应的虚函数，所以根据多态它会调用我们重写的虚函数来处理具体消息，至于我们不关心的消息，它会调用LRESULT lRes = ::CallWindowProc(pThis->m_OldWndProc, hWnd, uMsg, wParam, lParam);或者DefWindowProc，通过对基类的构造函数的查看，我们发现其实m_OldWndProc就是DefWindowProc。总结上面我们说明了duilib的基本框架，下面来总结一下：CPaintManagerUI::SetInstance(hInstance);设置进程的实例句柄，这个值会在注册窗口类时使用在CWindowWnd类中由Create函数完成窗口类的创建于注册，以及窗口的创建工作CWindowWnd类中的ShowWindow函数用于显示窗口消息循环由CPaintManagerUI::MessageLoop();代码完成最后需要重写MessageHandle函数用于处理我们感兴趣的消息。并且在最后需要调用基类的MessageHandle函数，主要是为了调用DefWindowProc处理我们不感兴趣的消息。