• 基于MFC的MVC框架的编程实践


    1. UI框架

    Windows桌面应用的开发,C++语言依然占据着主流。用C++开发桌面应用,面临UI框架的选择,主流的C++ UI框架有MFC,DUI,Qt,cef。它们各有各的优势。MFC,制作简单的UI非常容易,因为MFC并不完全开源,所以想深度美化MFC UI,难度非常大,多用于一些对UI效果要求不高的工业控制软件。DUI(Direct UI),即直接绘制的UI,所有的控制部件都是通过GDI/GDI+绘制的,因为其开源,如果想自定义一些控件,也会更容易一点。像360,钉钉,微信,有道等桌面应用程序都是基于DUI开发的UI。Qt功能非常强大,其支持不同的UI开发模式,有传统的Widget,还有基于前端技术的UML,另外,还能跨平台。但是Qt因为其功能强大,所以其附带的库非常多,开发框架比较重,所以一些对UI要求不是太高的软件,并且不追求跨平台的桌面软件,可能不会选择Qt。WPS是Qt开发的,并且做到跨平台。cef,就是Chrome浏览器的显示内核库。如果UI相使用Web开发技术,适量和底层通信,就可以使用cef框架。

    2. MVC

    MVC 设计模式一般指 MVC 框架,M(Model)指数据模型层,V(View)指视图层,C(Controller)指控制层。使用MVC的目的是将UI,数据,业务逻辑分开,以解耦这三个模块的关系。这样就可以做到在保证业务逻辑不变的前提下,可以自由更换UI和数据模式,提升代码的可维护性。
    在这里插入图片描述
    上图是MVC各模块的关系图,但是这种引用的方式并不够灵活,在某些框架下,适用性是够好。但是在MFC框架下,其实并不完善。如Model中的数据有更新,那么此时怎么办呢?难度View定时去查询数据,这么做的效率又太低了。
    为了解决上述问题,可以将MVC稍微变化一下,如下图的MVP。数据的什么时候响应由Presenter来主要完成,而不是由View去不停地询问再显示,这样可以提高效率。在MFC中,可以通过消息机制,将要显示的内容通知到UI。
    在这里插入图片描述
    上图的Presenter模块功能太多了,使用起来可能不够方便。如:view上有编辑框,编译框每输入一个符号,是不是就必须立即通过Presenter响应并存放在Model中呢?那么编辑框连续输入多个字符,那是不是必须响应多次呢?使用起来就不方便了。使用MVC的首要目的是将代码分开,至于各模块之间的交互,选择一个更适合自己项目的数据流方式。

    3. MFC

    微软基础类库(英语:Microsoft Foundation Classes,简称MFC)是微软公司提供的一个类库(class libraries),以C++类的形式封装了Windows API,并且包含一个应用程序框架,以减少应用程序开发人员的工作量。MFC的UI开发框架,主要包括:基于对话框、基于单文档和基于多文档。
    单文档和多文档模式,其实已经是利用了MVC框架。Document、View、Frame,正好对应MVC。一些对UI要求较低的工具,常使用基于对话框的开发模板。经常看到一些开源代码,其所有的功能基于都写在了CXXXDlg.cpp文件中,这样导致业务逻辑、数据和UI完全耦合在一起,再加上多线程,动不动还会出现主线程死锁的情况。这样的项目,越维护到后面,累积的问题越大,越让人痛苦。

    4. MFC实践MVC

    下面我们实现一个最简单的加法计算的基于Dialog的工程,用以实践MVC框架。

    4.1. View

    View如下图,代码在CCalcDlg.h/CCalcDlg.cpp中。
    在这里插入图片描述
    其头文件中引入两个接口类,用来设置"+"左右的数据和获取结果。Run Action直接转发到Controller模块,然后建立一个消息响应来更新数据。其代码基本设计如下:

    CCalcDlg::CCalcDlg(ICalcModel* pModel, ICtrl* pCtrl) : m_pModel(pModel), m_pCtrl(pCtrl)
    {
        ......
    }
    
    void CCalcDlg::OnCompute()
    {	
    	UpdateData(TRUE);
    	m_pModel->SetLeft(m_nLeft);
    	m_pModel->SetRight(m_nRight);
        // 直接转发Action
    	m_pCtrl->OnCompute();
    }
    
    // 建立消息响应更新结果
    ON_MESSAGE(WM_UPDATE_RUSULT, &CCalcDlg::OnUpdateResult)
    
    LRESULT CCalcDlg::OnUpdateResult(WPARAM wParam, LPARAM lParam)
    {
    	int nRes = *(int*)(wParam);
    	m_nRus = nRes;
    	UpdateData(FALSE);
    
    	return 1;
    }
    
    
    • 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

    4.2. Model

    通过View,我们可以看到其中有3个数据,数据都以接口的形式提供,"+“左右的数据需要提供设置的接口,”="右边的数据则需要读的接口。设计一个给View模块使用的接口类如下:

    class ICalcModel
    {
    public:
        virtual void SetLeftValue(int val) = 0;
        virtual void SetRightValue(int val) = 0;
        virtual int GetResult() = 0;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    另外还需要设计一个接口类给Controller模块使用,"+“左右的数据需要提供读取的接口,”="右边的数据则需要设置的接口。

    class ICtlModel
    {
    public:
        virtual int GetLeftValue() = 0;
        virtual int GetRightValue() = 0;
        virtual void SetResult(int val) = 0;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    真实的数据模块实现上述接口,并且完成数据的存储。

    class ArithmeticModel : public CCalcModel, public ICtlModel
    {
    public:
        virtual void SetLeftValue(int val) override
        {
            m_left = val;
        }
        virtual void SetRightValue(int val) override
        {
            m_left = val;
        }
        virtual int GetResult() override
        {
            return m_result;
        }
        virtual int GetLeftValue() override
        {
            return m_left;
        }
        virtual int GetRightValue() override
        {
            return m_right;
        }
        virtual void SetResult(int val) override
        {
            m_result = val;
        }
    
    private:
        int m_left = 0;
        int m_right = 0;
        int m_result = 0;
    };
    
    • 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

    4.3. Controller

    控制模块主要接口View的消息响应,并通知View更新结果,然后从Model模块获取数据,完成计算并设置结果。为了更好的解耦,所有的编程都是基于接口实现的。Cotroller接口:

    class ICtrl
    {
    public:
        virtual void Init(HWND hWnd) = 0;
        virtual void OnCompute() = 0;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    具体的业务逻辑代码:

    class CArithmetic : public ICtrl
    {
    public:
        CArithmetic(ICtlModel* pModel) : m_pModel(pModel){}
        virtual void Init(HWND hWnd) override
        {
            m_hWnd = hWnd;
        }
    
        virtual void OnCompute() override
        {
            int result = m_pModel->GetLeft() + m_pModel->GetRight();
            m_pModel->SetResult(result);
            // 通知View更新结果
            ::SendMessage(m_hWnd, WM_UPDATE_RESULT, 0, 0);
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    4.4. CXXApp模块

    CXXApp模块负责总的调用,其主要代码如下:

    ArithmeticModel model;
    CArithmetic arith(&model);
    CCalcDlg dlg(&model, &arith);
    dlg.DoModel();
    
    • 1
    • 2
    • 3
    • 4

    4.5. 下载

    示例代码下载

  • 相关阅读:
    STM32快速入门(定时器之输入捕获)
    浅析森林烟火AI检测算法的应用及场景使用说明
    2023数维杯国际赛数学建模D题思路模型分析
    perl的一些注意事项
    C语言之scanf
    URI 和 URL
    Spring Boot中RedisTemplate的使用
    Vue项目流程8,导航守卫的使用,图片懒加载,利用vee-validate实现表单验证,路由懒加载,打包并处理map文件
    微信小程序开发的OA会议之会议,个人中心的页面搭建及模板以及自定义组件
    ioremap()
  • 原文地址:https://blog.csdn.net/feihe027/article/details/126303750