Windows MFC 工程应用开发与框架原理完全剖析教程(上)

  • 第1章 MFC基础篇
    • 1.1 导论:MFC windows程序开发究竟是什么
    • 1.2 CWinApp、CFrameWnd与windows对象的对应关系
    • 1.3 通过代码逐项比对MFC对象和Windows对象的概念
    • 1.4 从Windows对象到MFC对象,成员变量与函数的辨析
    • 1.5 消息映射与windows事件驱动编程
    • 1.6 MFC本质的概要小结及VS编译器生成MFC代码剖析
    • 1.7 MFC对象与资源绑定的代码演示
    • 1.8 添加成员变量剖析
    • 1.9 值类型、控件类型与Windows API的关系
    • 1.10 值类型、控件类型与UpdateData的原理解释-以CEdit为例
    • 1.11 从对话框应用程序出发学习定制MFC的开发能力以Checkbox为例
    • 1.12 Combox控件与Trace使用技巧
    • 1.13 CListbox、Button控件、Edit综合-消息映射
    • 1.14 网页管理专家标准控件综合案例(上)-App Wizard的类向导生成了什么:窗体资源与C++对象绑定再剖析
    • 1.15 网页管理专家标准控件综合案例(下)-剪贴板与跨进程通信原理详解
    • 1.16 MFC多线程开发:概论
    • 1.17 多线程文件搜索器的实践案例:MFC线程对象是一个复杂构成
    • 1.18 多线程文件搜索前继知识FindFile API演示与实现思路
    • 1.19 搜索辅助线程设计
    • 1.20 主线程业务逻辑介绍
    • 1.21 工作者线程设计与消息循环的Crash Bug解读
    • 1.22 工作者线程与UI线程通信:UI线程核心观念
    • 1.23 自定义消息通信机制综合案例:日志切割器案例(一):资源文件
    • 1.24 自定义消息通信机制综合案例:文件切割核心代码完成(二)
    • 1.25 自定义消息通信机制综合案例:UI主线程与文件切割线程通信
  • 第2章 原理篇一 浅出MFC框架
    • 2.1 导论:MFC原理思想浅析(message based, event driven)
    • 2.2 使用C++语法封装Windows SDK C 风格程序
    • 2.3 第一个MFC的简易封装:HINSTANCE、句柄,为什么用CWinApp封装WinMain,用CFrameWnd封装窗体
    • 2.4 简易封装案例上——从MFC中Cwnd与CWinApp的原型实现
      • 封装窗口类CFrameWnd
      • 封装应用程序类CWinApp
    • 2.5 简易封装案例下——从C到C++的封装困境

第1章 MFC基础篇

1.1 导论:MFC windows程序开发究竟是什么

基本概念解释:
所谓的Application Framework实际上是一套完整的程序模型:

MFC和Windows API:

所谓的MFC就是一套为Windows应用程序开发所设计的一组类库:

我们只要遵循MFC的规约,我们就能完成标准的应用程序开发。

我们学MFC,就是学MFC的这些类结构,学习这些类结构相应的windows操作系统里面的概念,如何把MFC的对象对应到windows的对象,并且用MFC的方法写出windows的应用程序,就是我们学习的核心内容之一。

从GUI设计快速上手MFC:
我们如何辨析MFC对象和windows对象呢?我们首先来看一个非常重要的概念。

我们接下来要讲的所谓的Windows对象,或者说是Windows对象当中非常重要的一个组成部分,就是这个窗口,我们把这个窗口对象可以当做是Windows对象的一个代表;
那么接下来我们要辨析的MFC对象是什么?
我们说过了MFC是一个C++的类库,也就是说MFC的对象是一个C++对象,大家都知道C++可以在Windows上跑,可以在Linux上跑,可以在Unix上跑,那么C++对象肯定不是一个Windows对象,这是很自然的一个概念;
那么我们MFC之所以能够开发Windows应用程序,它一定是用C++的语法在某种程度上建立起了一个C++对象和Windows对象的关联关系,而这个关联关系是MFC框架帮你做好了的,我们作为一个使用MFC框架的人来讲,我们并不需要知道它是怎么建立关联的,我们只需要知道由哪一个MFC对象对应哪一个Windows对象,因此当我要做一个Windows视窗应用的时候,我只要去找到这样一个MFC给我们写的C++对象,我们就能够完成我们的开发。

那么有了这样一个概念,我们就知道了我们对于MFC的第一个掌握,就是要能够非常清晰的知道有哪些C++对象是MFC已经做好了给我们,我们如何把这个对象变成一个Windows操作系统能感知的内容,进行程序设计。

1.2 CWinApp、CFrameWnd与windows对象的对应关系

从这里我们就可以看出,我们的MFC它实际上也是一个Windows程序,那我们怎么用MFC框架呢?

只有选择了MFC它才能够依赖MFC这个框架来完成我们的开发。

正如我们用Windows应用程序开发的时候,我们要加windows.h头文件,那么MFC的时候我们就要加MFC的头文件:

1.3 通过代码逐项比对MFC对象和Windows对象的概念

1.4 从Windows对象到MFC对象,成员变量与函数的辨析

1.5 消息映射与windows事件驱动编程

我们来添加第一个非常重要的宏,就是把我们的成员函数和我们的Windows消息关联起来:

afx_msg是MFC给我们提供的一个占位符;
我们把这个新添加的成员函数HandleButton加入消息循环,让MFC框架完成把Windows消息映射到我们的成员函数的过程,这个过程是按照MFC的规约:

那么我们如何去完成它的这样一个消息循环呢?
我们得在外面写一组宏:

我得告诉你我要开始消息循环了,那么这个消息循环告诉你我们怎么做,你得把你的这个窗体注册到MFC的框架当中,首先告诉你你是一个MainWindow,同时你一定要回溯到MFC的框架里头CFrameWnd;

那么在这样的过程当中,接下来它会告诉你你要映射哪一个事件呢:我要映射ON_BN_CLICKED这个事件,你的标号是什么呢,注意啊,这个标号一定要和你创建的CButton的标号要一致10002,否则的话它是没有办法把你画出来的这个按钮图形和你的这个事件ON_BN_CLICKED对应上去的;
接下来我得告诉你你的成员函数的入口地址得告诉它,叫HandleButton;

再接下来关闭,END_MESSAGE_MAP(),至此我们的消息映射就完成了。

接下来我们就需要对我们的这个消息映射做一个业务逻辑的处理,我需要把HandleButton给实现了:

这个证明我们完成了一个MFC的对象和Windows对象,以及Windows消息和MFC的成员函数的对应关系。

1.6 MFC本质的概要小结及VS编译器生成MFC代码剖析

我们讲了MFC是如何使用消息映射这样的工具,把这些C++的对象和它所关心的Windows消息给它合并起来,形成了自己特有的成员函数。

那么对于我们MFC这样一个程序当中,我们如果要想透彻的把握MFC的生产方式,那么最好的方法就是我们在App Wizard的过程当中,我们自己手动的实现一个对话框,让我们知道这样的一个MFC究竟帮我们做了什么事情,简而言之,VS帮我们做了什么事情,MFC生成了什么代码,是我们要解决的一个内容。

1.7 MFC对象与资源绑定的代码演示

我们加入MFC的支持:

这个时候你编译并运行程序,发现和之前的01FstMFC程序没什么区别:

这究竟意味着一件什么样的事情呢?
实际上这个资源就是画出来的一个图形,在我们Windows视窗操作系统当中,它实际上就是一个位图,就是一张图贴在这里,但是这个图并没有被我们操作系统感知到,因此它也不会把这样一个图形显示出来给我们终端用户看;
那么我们编写程序的一个重要目标,就是我们希望我们通过画的这样一个界面,或者说这样一个窗口,供用户来交互,那么我们写的应用程序就一定得把我们画的这样一个东西给它加载到我们当前这个02MFCDlg这个应用程序当中来;
那么怎么加载呢?
很简单,我们需要有某种的C++文件或者C文件把我们的这样一个资源给加进去,那么接下来我们首先先用win32的方法,把我们这样的一个Dialog给加进来;
这样一个Dialog资源怎么被我们这样的一个应用程序感知到呢?

此时我们点击这个对话框右上角关闭按钮是不起作用的。

接下来,我们要借助我们的MFC提供的类向导的方法来完成,我们有两种方法。

1)我们在项目当中可以使用添加类:

2)我们在资源窗体当中鼠标右键单击,使用添加类:

我们为什么要添加一个类呢?
我们如果不用C++的这样的一个源代码,就是cpp源文件,和我们这样一个.rc文件(我们这个对话框资源IDD_DLGDEMO是存在于.rc文件当中的),我们如果不通过vs帮助我们生成的这样一个绑定,我们是没有办法让我们的C++运行时来感知到我们画出来的这样一个图形。

1.8 添加成员变量剖析

我们先回顾下App wizard帮我们做了什么事情:
App wizard实际上是帮我们用代码将Windows的资源、Windows对象封装成了符合C++语法的MFC对象,我们可以看到在rc文件当中建立的对话框,我们把这个对话框通过App wizard帮我们生成了CMyDlg.h文件和CMyDlg.cpp文件,这样子的话我们就可以用CMyDlg这样的一个C++语法,来完成这个C++对象的创建,而这个C++对象的创建我们就可以直接把我们画好的那个对话框给用户看,而且我们可以直接关闭它,这样就使得我们编程的简便程度大大提高了。

这是添加成员变量,根据我们C++的语法知识,这个成员变量一定是写在类里面的,那它写在哪个类里面呢?
我们想想看啊,这个Button控件是写在Dialog这个资源里面的,同时这个Dialog控件又已经生成在CMyDlg这个文件当中,换句话就是我们CMyDlg封装了一个Dialog资源,那么同样的我们这个Button按照MFC的设计思想,这个Button应该是我们CMyDlg这样一个类的成员变量。

我们这个成员变量生成完了以后,我们肯定想对这个成员变脸进行一些编辑,我们还是想为这样一个变量添加一些事件响应,我们如何来做这样一个事情呢?

从上图可以看到,MFC已经感知到Button1这样一个控件变成了一个CMyDlg中的CButton对象,我们可以看到有一个OnBnClickedButton1,它会告诉你我直接就生成了一个和我们当前C++语法相适应的一个成员函数,供你来响应这样一个Button1的鼠标单击事件。

我们可以看到在这个地方加了一个afx_msg开头的函数,告诉你这个是要根据MFC的这样一个方法来加入消息循环。

1.9 值类型、控件类型与Windows API的关系

我们可以看到有两个类别,根据我们前面讲的这两个类别都会跟IDC_EDIT1编号的控件进行绑定,这个绑定是MFC的一个非常重要的机制,接下来我们就用代码演示这样的机制意味着什么。

对于我们的这个Edit控件来讲,我们添加的这个成员变量的类型是CEdit,说明这是MFC封装起来的C++语言这样一个对象,我们看到它有两个类别(Control、Value),这两个类别都绑定了IDC_EDIT1,这个就是告诉你我们用CEdit这样一个MFC已经定义好了的内置的MFC对象,来对我们这个IDC_EDIT1进行一个绑定,换句话说,如果说我们不用这个MFC对象,能不能够去操作IDC_EDIT1这个特征呢,答案也是可以的,我们首先来看看怎么做这个事情。

我们希望通过Button1这样一个按钮的事件,当鼠标单击的时候去改变编辑框里面的内容:

如果我们想要修改编辑框里面的内容,那就得让我们当前这样一个应用程序去感知到编辑框这样一个对象,那首先就得拿到这样一个对象,我们可以用句柄来获得这样一个Edit,我们说过Edit也是一个子窗体:

但是如果我们每次都要这样做的话会非常繁琐,MFC就是把我们这一系列东西进行了一个封装,我告诉你我用一个CEdit对象就能够去完成这一系列内容,那怎么做呢,我们就可以来进行关联了,关联的方法就是接下来我们用App Wizard当中的添加成员变量,完成MFC的对象和我们的这个控件的绑定。

1.10 值类型、控件类型与UpdateData的原理解释-以CEdit为例

首先我们添加一个Control控件变量:

这样子我们就可以很方便的来进行操作,我们看用C++的方法是不是可以简化我们的编程:

这样的好处就是特别便于我们面向对象编程,我有一个对象m_edit_ctl,用对象调用一个方法SetWindowTextW就完成了,这个比我们直接要知道这个窗体,和知道这个窗体这样一个句柄,要变得面向用户编程更加的友好。

那么我们的这样的一个值类型的变量又是什么意思呢?

我们继续为这个Edit控件添加一个变量,此时这个变量已经有了,不能添加Control类别的变量了,我们可以添加Value类别的变量m_edit_cs,这个变量的类型是CString,告诉你这是一个字符串类型的。

实际上我们的这个vs编译器帮我们干的事情,就是想帮我们把我们看到的Edit这样一个资源进行一个内部绑定,它是通过MFC当中的内部框架机制,把我们的这一个m_edit_cs成员变量,和我们窗体上面实实在在的资源,这么一个图片或者说位图的显示,进行了绑定,以方便我们编程。

还有比用控件m_edit_ctl的SetWindowTextW方法更好的方式,MFC帮我们做了进一步的封装:

那么这个时候就把我们这个值类型的变量进行了设定。
但是此时我们编译运行程序,点击Button1,Edit控件里面仍然没有内容,这个没有改是对的,为什么没有改是对的呢?
因为C++的作用是在于我们精确地控制我们的每一行代码,因此我们的工作量也是很繁重的,我们控制能力强,那这句话的意思是什么呢?
我们的m_edit_cs它是一个CString对象,这个CString对象是一个C++对象,C++对象是没有平台能力的,什么叫没有平台能力?
我们想让我们对话框里面的编辑框的内容进行修改,它一定要借助win32的API,也就是说最后它一定要依靠类似于这种SetWindowText,借助Windows操作系统把这个内容给它画上去,这个时候我们仅仅去改变一个字符串东西是做不了的,那怎么办呢?

我们就要把这个字符串的信息给它放进去,我们通过MFC框架已经绑定了这个Edit资源对象是IDC_EDIT1,因为我们使用类向导的时候就告诉你了是IDC_EDIT1,这个时候我们就根据MFC框架里面的约定调用UpdateData(FALSE)这样一个函数,这时Windows规约告诉你,你只要做这样一件事情,你就不需要再去操心我去拿到它的句柄、找到它的对象,进行把内容放进去,你只需要告诉我你建立了这样一个值对象,同时调用UpdateData(FALSE),我自动的帮你找到句柄,我自动的帮你写上去。

这个时候内容就放上去了,这就证明了我们MFC是一个C++的语法,它能够按照C++的语法规范获得对象,对对象的成员函数进行操作,同时如果我们想让它得到Windows平台当中所显示的结果,我们就需要调用Windows平台给我们提供的函数,来完成我们对Windows应用程序设计的这样一个交互能力。

我们要知道,我们仅仅依靠C++的规范是没有办法在Windows当中呈现出我们想要的效果的,那么我们要把一个C++的对象变成Windows应用程序当中可以交互的这么一个控件(按钮、编辑框),我们一定要借助win32的API,只有这样的情况下它才能让我们的操作系统感知到我们所要做的这样一个事情。
那么这个时候就要告诉你了,所谓的APP Wizard要干的事情就是我们这边讲的,就是帮我们生成一种机制,是什么机制呢,就是让我们可以用C++的语法操作这些对象,只要我们查阅MSDN或者查询MFC的手册,我们就可以完成业务逻辑,但是我们必须要时刻知道,我们这些C++语法只能是帮我们进行调用,而我们如果要想让操作系统感知到我们所做的这一系列Windows应用,还得借助Windows的API,这个就是我们讲的我们的App Wizard实际上就是帮我们生成规定套路的代码,我们按照这个套路进行程序设计。

1.11 从对话框应用程序出发学习定制MFC的开发能力以Checkbox为例

根据我们前面所学的知识,我们就知道了这个App就相当于我们这个应用程序进程实例,这个Dlg就相当于我们画的窗口。

我们来找一下App Wizard帮我们生成的这个对话框的C++对象:

我们以后会经常看到的这个AFX开头的实际上是MFC定义的宏,这个实际上是MFC封装的全局函数的一个所写;
上图所选的这句话,实际上就把CAboutDlg和你的这个ID为IDD_ABOUTBOX资源给关联起来了,换句话说,以后我们只要写一个CAboutDlg myDlg;,这个时候它的这样一个显示,就是把IDD_ABOUTDLG这样一个资源显示给你看。

那么我们MY03MFCCHECKBOXDEMO这个Dialog是在什么地方呢?

换句话说,我们以后想使用IDD_MY03MFCCHECKBOXDEMO_DIALOG这样一个资源显示出来的窗口,来和用户交互的画,我们只需要调用CMy03MFCCheckboxDemoDlg这样一个类进行交互就OK了。

我们往窗体上面放一个Checkbox控件,我们希望选中这个控件的时候,就让这个窗口永远置顶:

接着我们要编写勾选置顶这个Checkbox的业务逻辑。

根据前面我们所学的知识,我们依然需要把Checkbox这个子窗体里面的内容,以一个MFC对象给它封装起来:

这样就在我们CMy03MFCCheckboxDemoDlg类里面自动帮我们增加了CButton m_chk_ctl这样一个成员变量供我们使用。

我们希望当这一个Checkbox被选中的时候,我们看看它有没有被选中,它如果被选中的情况下,我们就让当前的窗口置顶;如果没有选中呢,我们就让它不置顶;
这个时候我们想想看,该怎么完成它的事件驱动编程:

这个SetWindowPos是设置它的Z轴方向。

1.12 Combox控件与Trace使用技巧

上节课我们了解了一下,我们如何使用App Wizard实现一个对话框的应用程序,并且进一步的推进了我们对于资源和我们的.cpp文件包装的这样一个概念。

Combox有一个非常重要的属性叫Data,我们知道组合框里面有一系列的内容,比如说背景、上海、天津等城市名,供我们来选择一个地方,在这里我们添加如上图所示的数字111、222、333(每一项都是通过分号来分割的),在默认的情况下这样一个Combox就能够显示出我们预先配置好的内容:

接下来我们想对这个Combox做进一步的使用,我们想动态的去修改这样一个Combox里面的内容,这个时候我们引入一个Button控件来帮助我们完成,我们给这个Button控件添加一个变量来帮助我们来使用:

那我们现在想要在ClickedButton1这样一个事件当中来访问我们这样一个组合框,大家自然而然的想到是不是要给这个组合框设置一个变量呢?
当然也可以,那么我们想在这个地方帮助大家进一步巩固我们这个控件和MFC的关系,我们帮助大家来看一下,我们如何在这个Button的Clicked事件当中来对Combox的添加内容进行获取,我们可以选择类视图来直接定位它:

我们要拿到Combox控件,MFC有一个非常重要的方法叫GetDlgItem:

拿到Combox里面的内容以后,我们除了可以用MessageBox来输出内容以外(MessageBox可能会阻碍我们当前线程的运行,不是太方便),我们还可以用MFC的调试帮助宏TRACE来做:

我们在调试程序的时候,这个TRACE是很方便的,它不会阻塞我们当前这个UI线程,这个就是我们TRACE当中的一个非常重要的手段,也是MFC内置帮我们提供的。

那么我们如何单击Button按钮就往Combox里面添加一个条目呢?
这个时候里面需要用到的一个方法就是AddString方法:

我们演示了TRACE和Combox的使用,并且用纯粹的代码方式获取里面的控件并加以编辑,这是一个非常有实用价值的工程技巧。

1.13 CListbox、Button控件、Edit综合-消息映射

我们如何把一个Windows的消息直接映射起来呢?
我们前面都是通过按钮进行绑定,那我现在就想让窗体当中有一个鼠标按下,就能往我们这个Listbox里面进行一个内容的添加;
在窗体上点鼠标右键,点击类向导:

我们选择WM_LBUTTONDOWN,这样我们就可以把一个Windows消息和我们的这个资源(Listbox)进行绑定,我们点击添加处理程序:

这个时候它就自动帮我们生成好了,而且把这个函数名OnLButtonDown也帮我们弄好了,然后我们直接点击编辑代码到该函数定义处:

在CDialogEx::OnLButtonDown这个对话框里面的鼠标左键按下的事件之前,我们先让它弹出一个对话框,做一个演示,以证明我们这个消息添加是成功的:

接下来,我们就可以在这个已有的工程当中,当我们点击一下就往Listbox里面添加一个相应的内容:

我们发现VS提示这边看不到IDC_LIST1,这是因为我们没有把这个资源标识符放进来,我们把资源文件加入进来:

这个时候它就能标识出我们这个IDC_LIST1了。

我们希望每在窗体当中做一次鼠标左键按下,我们就把一个固定的内容添到Listbox里面去:

接下来我们做一个简单的小综合,来让大家熟悉下控件和控件之间的交互。
我们添加一个Button和Static Text,我们希望每单击一次Button的时候,我们就把Listbox里面的内容给显示到Static Text里面去,这个就是一个简单地控件和控件之间的交互:

我们可以用添加变量的方法,但是我们为了让大家能够学一种方式就能够处理绝大多数的问题;
所以我们首先对Button做一个属性的设置,并添加一个BN_CLICKED事件处理函数:

我们通过这个方法向大家证明,这个资源文件当中的子控件、子窗体,其实和你添加的成员变量,是没有必然关系的,我们可以通过MFC的框架直接把这样一个事件驱动内容和这个子控件关联起来,这是我想和大家介绍的一个非常重要的知识。

这个GetText函数的第一个参数nIndex的意思是,我们的Listbox里面每一个条目都有一个索引,我们可以拿到当前选中条目的索引。

我们发现单击Button按钮,Listbox里面的内容并没有添加进Static Text里面,这是怎么回事呢?
我们打断点调试分析下:

我们可以选中上图pList->GetCurSel()这句代码,添加监视,此时我们看到编译器提示CListBox::GetCurSel这个函数没有地址,这个是不是说我们没有选中Listbox里面的条目导致的问题呢;
我们点击Listbox里面的条目后,这个时候才是选中的状态,这个时候我们再去点击Button按钮:

这个1111显示的位置是我们添加对话框资源的时候,中间那个默认的Static Text,所以我们之后可以修改下代码就行了;
所以说,GetCurSel函数只有条目是选中状态的时候才会拿到里面的内容。

当我单击Button这个子窗体的时候,为什么我这个鼠标左键单击Button按钮的事件,和我在窗体上面的单击,有不同的效果呢?
这个过程当中牵扯到一个非常重要的概念,这个概念叫做事件冒泡。

我们Windows是由一个一个视窗给它组合起来的,在我这个视窗当中有一个一个的按钮,比如说我们以下图的椭圆按钮为例:

实际上对于我们Windows来讲它肯定是能捕获在这个区域当中的鼠标按下,对于我们操作系统来讲,我在按钮这个区域里面按下,无可厚非我当前这个子窗体肯定是能截获到的;同样的,在我这个按钮子窗体之外的Dialog,它应该也是能够接收到这样的一个鼠标按下的事件,那为什么这样的事件就被转化成BN_CLICKED(针对按钮的),而没有变成我的那个WM_LBUTTONDOWN往Dialog里面添东西呢?
这个实际上是MFC当中对我们Windows程序设计一个比较好的封装,这个也就使得我们初学MFC的人对这个情况有一个困惑。
实际上在Windows编程当中,当这个子窗体在父窗体之上的时候,父窗体它实际上相当于一个容器,这个子窗体按下的时候,它会向父窗体发一个叫做WM_COMMAND消息,而这个WM_COMMAND消息的低字节用来作为消息传递的时候,告诉父窗体我的这样一个消息啊是我这个子窗体接收到的,那么MFC对它做了一个非常精巧的包装,就是让你标准化编程的时候,告诉你这样一个事件我只响应子窗体的,而对父窗体的东西进行忽略了;
那如果说我们需要对这个事件冒泡进行一个遍历的话,那我们需要把这个消息往父窗体再做一次动作,那这个时候呢就要对我们的MFC进行一个深度的扩展,但是在标准化编程过程当中,一般情况下我们不会考虑这样的事情,因为在正常的Windows程序当中,我们认为按下这个按钮就是希望点击一下这个按钮,而和我的父窗体没有直接的关系,所以通过这个地方和大家解释了一下这个事件冒泡的思想。

1.14 网页管理专家标准控件综合案例(上)-App Wizard的类向导生成了什么:窗体资源与C++对象绑定再剖析

同时添加一个List Box:

我们希望在点击添加网址的时候,能够弹出一个新的窗体,这就需要插入一个新的窗体资源:

修改新的窗体(Dialog)的ID为IDD_DLG_ADDR;
添加两个Edit,并修改它们的ID为IDC_EDITADDR和IDC_EDITUSER;再添加两个提示信息(Static Text),它们的Caption分别为网址和用户名,并添加一个Button,修改它的Caption为确认添加:

现在我想点一下这个按钮,马上就可以形成事件,但是我们发现这个Button的事件里面没有内容,这是怎么回事呢,其实很简单,我们说过这个控件里面的事件都是通过代码控制的,你现在有没有地方来写这个代码呢,你根本没有地方来写这个代码,因为你现在仅仅是个资源文件,那我现在需要把这个资源文件变成一个可以写的C++文件,我才能够把我的这样一个事件给加进去,那怎么办呢?
大家很清楚了,我用类向导App Wizard,右键点击窗体,点击添加类:

我要把这个资源变成一个MFC可以识别的类,这样的话我在有了这个类之后,就有了它的CPP文件和.h文件,我就可以往里头添加代码,这样App Wizard才能帮我们添加代码,才能把我们想要的功能给它实现。

这个时候它就可以添加事件了,而且我们现在也知道了,我加的这个事件仅仅是和资源文件相关联的,它是否添加成员变量并没有关系,我可以把它的事件加进去:

现在我们需要做的事情就是,当我点击确认添加按钮的时候,就把网址和用户名这两个值保存起来,我需要把这两个保存起来的值供我的CMy05AddrExpert这个调用者拿到,因为当我点击添加网址以后,我就要能够把保存起来的网址给写到Listbox里面去,那现在的问题就是,我们怎么能够传递窗体间的值呢?我们说,这个时候我们有MFC的对象来帮我们标记这个窗体,所以我们可以用C++的语法来完成这个变量和变量之间的传递。

比如说,在我的这一个CDlgAddr的窗体上,我可以人为的定义两个CString成员变量,让这两个成员变量用来存储信息供我使用:

有了这两个成员变量以后,我们可以到类视图里面看:

一旦单击按钮,我想得到这两个编辑框里面的值:

现在我们就在当前的这个CDlgAddr里面拿到了这两个值,我拿到这两个值以后我是怎么和CMy05AddrExpert关联起来的呢?
我们回到CMy05AddrExpert窗体这里,给添加网址按钮添加一个事件:

然后我们要添加一个头文件,让它知道要开哪个对话框,这样的话它就能够找到我的CDlgAddr:

这样子我就可以把这个对话框显示出来了,我的dlg里面有两个变量strAddr和strUser,所以我们现在就可以把这两个变量加到Listbox里面去了:

接下来,当我们双击Listbox里面的条目,就能够跳转到该条目前面的网址,那我们就必须得对这个Listbox做一个事件绑定:

这样子的话,它的事件驱动就被我们添加进来了。

这个时候我们需要对拿到的字符串做一个切分:

MFC为我们提供了一个非常好用的函数,该函数的第3个参数意思是说,你要找到第0个起始的分号,这样就可以把分号前面的内容给截出来了。

右下角输出了我们的网址,证明我们现在的这个提取是正确的。

1.15 网页管理专家标准控件综合案例(下)-剪贴板与跨进程通信原理详解

能够打开网址以后,接下来我们希望条目内容后面跟的一个用户名我们能放在剪切板里面。

我们在程序里面建立了一个字符缓冲区(szName),把我们的output这样的一个要么是unicode、要么是多字节的内容,变成一个char类型的字符数组,大家都知道char类型的字符数组不管在什么环境当中,它都是一个字节的,这样子的话就有利于我们用字节进行传输。

GlobalAlloc函数的第1个参数GHND意思是不移动的。

这就是我们这个业务需求的意义。

我们来解释下,它为什么要用char来做这样一件事情:

而我们用的CString它有可能是多字节,也有可能是unicode,我们为了解决这个字节的问题:

这就是我们讲的要用char来做转换的原因,这样的话方便我们剪切板程序按一个字节一个字节来填充内容。

上图网页管理专家和IE浏览器是两个用户进程,它们具有各自的逻辑上0到2G的内存地址空间,而且它们都有一个共同的2G到4G的空间叫操作系统内核,如果我们想把网页管理专家里面的数据传递给IE浏览器,我们在逻辑上讲,我的0到2G和你的0到2G是没有办法互相访问的,那我们怎么办呢?
我们只要使用内核当中的剪切板(相当于一块内存),换句话说我有了这个剪切板以后,你可以访问它,我也可以访问它,在逻辑上都是相通的,但是问题就是我现在如果想去访问剪切板的话,我首先要打开这个剪切板,同时我还得确保别的进程没有打开这个剪切板,否则的话我也往里头写,你也往里头写,这个剪切板不就乱套了么,所以第一步我们先做if(OpenClipboard()),如果我能打开我就能够往剪切板里写东西,但是我写东西也不是简简单单写;
首先我得在os kernel当中申请一块内存,这块内存就是我们讲的hMem,我得把我的数据放到hMem里面去,然后再由这个hMem塞到剪切板里面去,以帮助我的IE浏览器得到它,所以我得向os kernel申请一块内存(GlobalAlloc),接下来我把我里面的数据塞到hMem里面以后,再把hMem塞到剪切板,供其他进程跨进程来访问;
同时,当我完成了这种数据塞进去的操作以后,即完成了hMem向剪切板写内容以后,这个os kernel就要把这个hMem给关掉,不关掉的话就会造成资源泄漏。

1.16 MFC多线程开发:概论

MFC提供了多种多样的程序来简化程序的开发,特别是MFC的多线程技术它的便捷程度是非常容易的。

工作者线程它没有消息机制,因为它没有窗体,也就不存在在Windows当中去捕获键盘、鼠标等这些东西,供我们用户界面呈现来使用,通常它是用来执行后台计算任务。

为了透彻的讲解MFC当中线程的工作,我们建立一个控制台项目,这样就能够向大家展示出MFC里面的内容:

在上图MFC的使用那个地方,我们不要用标准Windows库,我们要用MFC的类库。

你要用MFC的类库的话,你要加它的头文件afxwin.h:

线程函数执行完了之后返回0,这是规约。

1.17 多线程文件搜索器的实践案例:MFC线程对象是一个复杂构成

1.18 多线程文件搜索前继知识FindFile API演示与实现思路

我们依然选择一个win32控制台项目帮大家深刻的理解MFC编程:

我们修改一下项目的字符集为多字节字符集(使用printf、scanf方便些),并且使用MFC类库:

命令行当中也可以用MFC类库,前提是你不要用和图形化用户界面相关的。

如果文件名是目录的话就把它打印出来,我的目的就是把当前的目录名全部打出来:

1.19 搜索辅助线程设计

我要有一个构造函数,我得知道我是哪个线程启动的,所以我得有线程的数量。

定义两个成员变量:你找到的结果的数量,以及你当前有多少个活动线程;
只有当活动线程都为0,同时m_listDir都为0的时候我才能够终止我的这样一个循环。

很可能当前目录链表为空,有9个线程都在等待,但是只要有1个线程还在工作,这个线程是有可能发现这个目录底下还有子目录的。

接下来,我得对我们的m_listDir进行一个构造,我得告诉你我怎么把我这个单链表串起来,根据MFC的要求,你得告诉我你得偏移量是什么,我告诉你我要组合的是CDirectoryNode,同时我串的是pNext,这样的话呢,MFC提供的CWinThread类就能帮我们把这样一个CTypedSimpleList串联起来,这是MFC的一个特征,我们来用一下让大家熟悉这样的结构,为我们后面进一步的去构建MFC的内容打下坚实的基础,这样也方便以后创建我们的MFC的多线程。

接下来我得告诉你我们怎么来通知线程和线程之间的交互呢,我得创建两个未受信的事件对象(我查找的时候其他人请等待、我的线程有没有停止工作),一开始我肯定都没有线程启动,也都没有往dir里面去贴啊,但是这里我得把它们初始化好,否则我是没有办法用的。

接下来我得做临界区了,因为我这个Rapid一旦起来了以后,我们得由互斥来访问了,因为当我取走第一个书架,第二个人是不能取这个书架的。

1.20 主线程业务逻辑介绍

1.21 工作者线程设计与消息循环的Crash Bug解读

很可能当前目录链表为空,有9个线程都在等待,但是只要有1个线程还在工作,这个线程是有可能发现这个目录底下还有子目录的。

说明我现在等待,在我等的过程当中我还看到了已经查找完毕了,已经没有任何一个线程还在工作了,都是在等待了,这时候就离开临界区。

我计数完毕了,同时我又是非活动的,那我要去进入等待状态;
这个等待状态是要告诉主线程,如果你有新的目录可以通知我,让我去继续去推进,否则的话我现在没有活干了,我就等在这里;
首先我进入等待状态,那我什么时候开始激活我当前线程呢,我就等我主线程告诉你,我又有新的dir了,请你执行:

以上是目录的情况;
如果不是目录呢?那就该判断这个文件了(CheckFile):

这个架子已经被人领走了,那么接下来别人就不需要再去搜这个架子了。

这个告诉你我们一个搜索线程需要结束了,你就退出循环吧;
同时判断一下,当前线程是否是最后一个结束循环的线程,是的话就通知下主线程:

如果它不超时,那我就通知主线程,最后一个搜索线程也结束了。

让编译器检查下看看它有没有语法问题,运行后我们发现它没有显示输出,而且程序发生异常:

没有显示输出是因为找到文件的时候我们没有打印出来,我们添加一个打印输出,看看它是不是在找:

这是MFC工作线程当中必须注意的一个问题,这是一个坑,很微妙。

它告诉你已经找到这些文件了,但是为什么会引发异常呢?
这是一个非常重要的问题,这个问题很微妙,在下图36行这里当你没有找到的时候,阻塞在这里,设置的超时时间是INFINITY,我们把它改成INFINITE,就是这一个问题:

这个INFINITY和INFINITE有什么区别呢?
在win32编程中这两个区别不大;
但是在MFC当中,它有一个自己的消息循环机制,如果我们用了INFINITY这个东西,它会一直等待下去,我们的工作者线程就不会进入消息循环,那就会导致主线程crash掉。

1.22 工作者线程与UI线程通信:UI线程核心观念

我们新建一个MFC项目:

如果我们想在我们MFC当中用C++的方式访问它(线程窗口),就得把这个资源(线程窗口)进行类化:

我们就可以用CThreadWnd这样一个对象来表示这个资源(线程窗口)。

由于主窗口里面没有窗体,所以我们看到右下角事件那里没有相应的消息;
那我们想让鼠标左键按下的时候来响应它的话,可以右键、类向导,给它添加一个WM_LBUTTONDOWN消息处理函数:

我们可以用AfxMessageBox来验证下这个鼠标左键单击的工作是否OK:

如果我们想让当前的主窗口能够感知到我们想让弹出的线程窗口,那我们就要把线程窗口类化的头文件CThreadWnd加进来:

有了CThreadWnd这个类了,我们就可以让它弹出来:

这个是主界面线程创建的窗口。
这是个模态对话框,就是我们如果不把这个线程窗口对话框关闭的话,主界面是没有办法进行响应的;我们把这个线程窗口关闭以后,主界面才能够响应。
这个就是我们讲的主对话框的概念,这样的做法是我们直接把一个窗体给它弹出来了,那如果我们想用一个UI线程来做上述弹出线程窗口这个事情的话,该怎么做呢?
我们想要添加一个MFC内部的一个类的派生关系,我们熟悉的话可以手写,如果不熟悉VS也提供了方法,让它来帮助生成这样的代码:

这个时候它就直接对我们的CMyThread进行了扩展,在我们这个线程的过程当中,我们希望我们新开一个线程,也能把我们的对话框给它弹出来。
那我要弹出这个线程窗口的对话框,该怎么办呢?
我得让我当前这个CMyThread感知到ThreadWnd的存在,我就得把ThreadWin.h加进来:

CMyThread这个线程就专门负责我的这个新的线程窗口(CThreadWnd)给它弹出来,和我刚刚那个方法有什么区别呢?

修改原来主线程那里的代码,开一个UI线程把这个窗口弹出来:

这个是线程创建的窗口,然后呢,我如果点主窗口,它一样会被阻塞,这个说明是UI线程当中的一个特征,这是MFC帮我们封装起来的一个观点:
就是说MFC之所以会分成所谓的UI线程和我们的这种工作者线程,核心区别在于二者是否加入窗体消息循环!

我们知道它的DoModal之所以会阻塞,它阻塞的是窗体消息,那么这个窗体消息只要是UI线程,它都会把这个窗体消息一直放在那个地方,使得我们整个应用程序(不论是你的主界面,还是我们新开的这个线程窗口,都在一个应用程序里面)被加载起来以后它就是一个进程,在同一个进程里面它们都会接收同一个Windows的消息,所以你即使开了一个新的UI线程, 并不能够确保它走另外一套消息循环机制;
这个地方就是UI线程和工作者线程当中一个非常重要的区别。

1.23 自定义消息通信机制综合案例:日志切割器案例(一):资源文件

为了能够进一步的向大家演示这个程序的开发,进一步的深刻去体会VS的开发,我们选择一个Win32项目,并设置为空项目:

为了节约大家的时间,同时也想向大家表明,我们的VS是一个编译环境,这个编译环境能够把你的.rc文件、.h文件、.cpp文件变成一个可执行程序,那我们怎么做呢?
这里为了简化我们的开发,把以前做好的resource.h和FileCutter.rc复制到当前项目文件夹下:

接下来我们把这两个文件贴进项目里面来:

这样就使得我们的软件开发变得特别容易复用,这样效率比较高。

上图就证明我们的资源已经加载完毕了,接下来就可以对这个资源进行开发。

我们手动添加我们的代码,我们这样做的目的是为了抛弃App Wizard帮我们生成的冗余的内容,我们突出重点,为我们接下来的MFC原理精讲打好一个重要的基础。

这个afxcmn.h是MFC通用的控件。

同样的,我得有资源,有这个窗体让它感知到:

这个CMainDialog构造函数是要对CDialog进行覆盖的;
并且定义一个MFC的进度条CProgreassCtrl对象。

UIControl这个是用来重绘界面的。

CFileCutter这个类还没做,在这里占位表明以后要完成。

接下来我们还得做一系列的对Windows消息的响应工作(窗口初始化、窗口退出):

接下来我要加消息了,在Windows当中所有的Windows消息对MFC成员函数的映射都用afx_msg占位符来做,这个afx_msg就是用来表明要进入Windows的消息映射和消息循环了,帮它把Windows的消息变成一个事件驱动的函数。

你既然定义了3个消息,你就得写3个消息对应的消息响应函数。

根据MFC的规约,你写了这么多函数,这些函数我怎么加入消息循环,帮你映射呢?

我们添加Cutter.cpp实现文件:

return FALSE是说我们全部构建完毕了,请你加入消息循环。

再接下来我们要把这个窗体给你画出来,我们已经有了这样一个Dialog对话框(FileCutter.rc资源文件里面的IDD_FILECUTTER_DIALOG),我们怎么把这个资源加载进来呢?

这个地方有一个问题啊,我告诉你我用的这个CMainDialog,我把这个IDD_FILECUTTER_DIALOG和pParentWnd给它放进去了,但是我到底是怎么做的呢?
我实际上是调用了父类的CDialog方法:

接下来我们就要把消息循环给它逐个加入,以供我们的MFC框架把我们实际的信息给它放进来;
我们在手敲这段代码的过程当中,也是帮大家熟悉MFC究竟帮我们做了什么事情。

这里面做的事情是,我要把哪些消息进行映射和路由。
ON_BN_CLICKED这个消息,路由哪一个呢,告诉你叫IDC_SOURCEBROWER,和我们的OnSourceBrower给它连起来(让IDC_SOURCEBROWER上的消息路由到OnSourceBrower方法里面去)。
这个时候BEGIN_MESSAGE_MAP告诉你,你要把我的消息和哪个地方进行加联,让我们MFC可以感知你呢,我把当前的这个窗体CMainDialog给你,我的CMainDialog实际上对的就是我的IDD_FILECUTTER_DIALOG,那么我告诉你,我的CMainDialog这样一个类要来接收你的消息循环,那从哪个地方进行路由呢,CDialog里面已经告诉你怎么去路由了。
其他控件上的消息映射以此类推:

由于自定义消息WM_CUTTERSTART是在我们线程的辅助类CFileCutter里面的,所以我先不做这个自定义消息映射,先放在这。

首先把我们的窗体给画出来,并把重绘界面的UIControl写出来,这两个函数都没有写全,这里先打个桩占个位:

每做一次Select的时候我们要更新一下界面(OnSelect,更新状态和进度条)。

因为没有辅助类,我们先把这些自定义消息处理函数注释掉:

至此我们这个程序的框架已经OK了。

编译报错,提示没有MFC类库的支持;解决:

1.24 自定义消息通信机制综合案例:文件切割核心代码完成(二)

接下来我们就可以着手完成我们的文件切割辅助线程。

WM_CUTTERSTART它的wParam封装的是整个文件切分的数量nTotal。
WM_CUTTERSTOP它的wParam封装的是退出码,而它的lParam里面是完成的数量。
WM_CUTTERSTATUS它的lParam封装的是我们现在完成了多少个,目前完成的数量。

exitUserForce是用户可以强行终止;
exitSourceErr和exitDestErr这两个就是考虑到如果我在工作过程中,突然这个文件有错误,被删除了;

CFileCutter构造函数要传一个句柄过来,因为我们要让工作者线程和窗体进行通信的,所以你得要告诉我要把我的消息推给哪一个窗体,因为WM_CUTTERSTATUS是Windows的窗口消息,窗口消息是压到系统队列里面去的,它压到系统队列里面去了以后,由操作系统再路由给你的窗体,所以我们得告诉你窗体是哪一个,否则工作者线程和UI线程工作的通信会有问题。

我们并不知道你是否一直在存在,所以返回m_bRunning这么一个特征,后面再完善这个,这里先占位。

这个uFileSize的意识是我们按多少为一个单位进行切分的。

SuspendCutter是暂停切分,ResumeCutter是否继续,比如做了一半可以暂停。

下图31行写错了,它其实是一个析构函数,用来防止泄漏、回收资源。
再接下来我们生成的这样一个工作线程从哪个地方来呢?我们用一个友元函数:

这个线程要和窗体能够进行交互,线程又会侵入到另外一个模块的空间,那么这个时候我们就用了这样一个友元的概念放在这里。

m_uFileSize,每个单元的单个文件的大小;
m_bSplit,切分的情况。

m_bContinue,你是否继续工作;
m_bRunning,你是否处于工作状态,你这个线程是不是好的,我们要时不时看一下,这有点类似于咱们在网络当中看一个设备是否存在,跟心跳包一样,你这个线程是不是在工作,通过这个m_bRunning来指示它。

接下来我们得有一些若干个比较重要的和我们窗体通信的内容:

FileCutter.cpp实现文件:

pCutter->m_hWorkEvent,首先看它有没有通知我推进;

完成的任务是告诉协作方,我们正在工作,这个时候就要用临界区:

在临界区里面的任务就是,我要把当前线程正在活动,这样的话我就能够被推进;

在这个线程里面我们真正开始工作的时候,就是我们要开始调用传过来的pCutter当中的DoSplit方法:

这就是我们这个工作线程里面的逻辑;
接下来我们要开始逐个实现它的接口成员:

在CFileCutter构造函数中,构建起我们UI线程和工作者线程里面的关联关系,并创建一个事件对象供我们临界区使用(在未受信的状态下)。

接下来我要创建工作者线程了:

我让你进_CutterEntry线程函数入口地址,然后把当前对象给你,因为你要知道这是MFC的方法,它需要把当前对象传给它;

同时把它的自动重置给它关掉(m_bAutoDelete):

它的通信是由我们自己来维护的;并调用ResumeThread函数让它推进线程。

Reset是对我们的工作区进行一个预设重置开始。

我们看一下Reset的意义,放在这里是为了结构化编程。
实际上它想干的事情,是对我们这里面的源文件也好、目标文件夹也好,进行一个设置,把它进行一个清理;
首先为了防止线程不一致的问题,必须保证它是互斥访问的(通过临界区):

就是我每次启动以后,接下来我都把源文件路径名清空,让你有机会去选择新的源文件和新的目标文件夹,这个一定要互斥的,否则的话在我不知道推进的序列的时候,很有可能里面的值是脏数据,引起脏读这些问题。

解决资源泄漏的问题(下图函数名有误,应为析构函数):

我们首先告诉你我这样一个m_bContinue要让它置为FALSE,因为在我的这个线程对象工作起来了以后,我并不知道哪句话会被打断,很有可能在我这个挂起的过程当中可能也会被打断,那这个时候我要让它互斥的访问,以确保它确实为FALSE,然后当我能够在需要逻辑检查的时候,它和我所要规划的值是一样的。

接下来我们要防止线程一直等待,那我得告诉你我这个线程已经确实能够退出工作了:

我们可以确保我们当前线程是工作的,直到你给我发消息过来:

下图if语句中的条件应改为:if(!m_bRunning)

让它告诉你我现在要开始工作了,即m_bSplit = TRUE;
当我的这些准备工作做好了以后,我要通知线程开始工作:

用CWinThread里面的SuspendThread方法让线程挂起。

互斥的去改变一个非常重要的值:m_bContinue
在做完了以后,强制退出前,我们还得告诉你不能让我的线程处在可以继续的状态:

CFile::shareDenyWrite,这个文件我不让你写;
CFile::typeBinary,我是按照二进制的格式。

接下来我得看看你是不是读的OK了,如果不行那我得发一个消息告诉你打开文件出错了,因为你是工作者线程向UI线程发消息,只能塞,往消息队列里面推东西了,会由操作系统路由给你的UI线程。

逐层创建文件夹:

我们的代码是逐层创建的。(例如:E:\qycache\ad_cache\TempFile)

下图代码136行错误,应改为:strDestDir += ‘\’;

首先我要告诉你你要分割多少个(因为它是整数除,所以要加1):

用字符数组来做缓冲区:

sDestName,比如我们要切割的文件叫Log.txt,要切割3份的话,分别叫Log_1.txt、Log_2.txt、Log_3.txt;
nPreCount,这里我们先假定切割1个文件;

好,我现在的Log_1.txt写完了,就该做Log_2.txt了,我得做这个事情,我要通知一下用户我现在已经切好一个文件了:

这是因为我们前面定义自定义消息的时候,笔误导致这个消息定义错了,修改如下:

编译后发现错误:

_CutterEntry里面要有一个返回值,我们这个返回值在我们线程里面有一个常见的东西,就是当我们的线程完成了以后,要返回0。

1.25 自定义消息通信机制综合案例:UI主线程与文件切割线程通信

首先第一个我们要能够弹文件写内容。

为ComboBox添加按1M分割、10M分割和20M分割;
默认的对它选中第0个,即按1M分割。

为XMFCLogCutter添加CFileCutter指针成员:

我们的UIControl里面要做的内容也很简单,需要把里面相应的UI进行设定:

首先看看你当前的工作者线程是否正在运行,然后根据是否运行来禁用或启用该控件。

我这个UI线程能否接收响应,都是要和你的工作者线程进行一个协同的;
当我这个程序启起来的时候,我能不能够去点暂停,能不能够点启动,都是要和你的工作者线程进行通信的,那么这个通信都是来观察你工作者线程里面的状态,我通过这个bIsWorking来获取的。

我们现在缺一个CDirDialog,那我们就建一个CDirDialog类:

这是我们把一个目录对话框显示给大家用,大家可以在开发的时候直接拿来用。

不需要窗体句柄,也不需要指向目录,保持默认即可;
我唯一需要设置的就是m_bi里面的pszDisplayName,显示的那个内容就是你规定的过滤内容:

我要过滤txt呢,还是其他文件呢;我只需要你的目录信息就可以了,因为我接下来就是用目录信息来做的。

m_bi真正的父窗体是谁呢?你传进来的hWndParent。

回到我们的Cutter.cpp里面来:

我们检查下输入,我们看看它里面的内容是否存在,存在的话我们才开始能够进行选择,我们都是通过感知你的控件里面的内容的:

我们把选择的分割单位设到临时变量str里面去,因为接下来我会告诉你我要开始启动工作线程了(第129行代码)。

接下来我们开始完成自定义消息的通信:

我们首先得看一下进度条,开始的时候可能要设置下进度条,开始工作:

更改了界面之后,用UIControl重绘一下。

最后更新一下界面,让用户能够实时交互。

编译运行看看程序是否正常:

这边有问题,我们调试下:

FileCutter.cpp中的137行,将!=改为+=
这样的话才能保证我每一次把我们的字符串拼接是OK的。

还有一个地方,在分割完之后,打开的文件没有关闭,并在最后通知用户工作完成:

还是出错引发断点,它停在了上图UIControl调用那里,我们分析下这个问题在什么地方,这个时候很有可能是通信的问题,通信的时候我们看看是不是组件哪个地方有问题,和组件相关的问题我们往上看。

我们分析发现m_Progress没有初始化,它是一个空指针,我们修改代码:

我们这个日志文件6M多,所以切7个文件是很正常的。

第2章 原理篇一 浅出MFC框架

2.1 导论:MFC原理思想浅析(message based, event driven)

上图有两个队列,左边队列叫系统消息队列,右边是我们用户的应用程序消息队列,USER Module就是我们用户模块,这个用户模块就是上图最右边MYAPP.EXE,我们分成两部分(两个函数)。

2.2 使用C++语法封装Windows SDK C 风格程序

我们打开之前的捕获窗口中鼠标位置的Win32Cap应用程序,来回顾一下win32程序的结构,我们首先看一下这个程序的功能:

当我们鼠标在这个窗口客户区按下鼠标左键单击的时候,它就会把鼠标单击的位置坐标显示在客户区左上角。

这个WinMain函数相当于咱们这个窗体被操作系统拉起来了以后它有的一个主线程,那么感知这样一个主的窗口应用程序的特征是hInstance,它实际上是一种窗口实例,我们通过这个东西由操作系统感知,操作系统用hInstance感知到这样一个窗口进程,同时你这个窗口长什么样子,它用wndclass的每一个分量告诉你,我是不是需要水平重绘啊,它的图标、光标,它的背景,它是否有菜单等等这一系列东西;
还有一个非常重要的东西,我这个窗口当中可以接收一系列的这种动作,比如说鼠标左键按下,比如说窗口接收到键盘的按下,比如当前窗口接收到菜单的命令,它会把你当前注册的这个窗口wndclass中每一个分量的数据结构,对应到MainWndProc窗口回调过程这样一个函数当中;
这个回调函数是Windows会把它捕捉到的所有消息,路由给我们当前的窗口过程,从而完成窗口当中的业务这个工作。

由于随着业务需求程序越来越复杂,窗口回调函数中的switch分支也会越来越庞大,不利于我们的开发和维护;
所以就提出了一个观点:

我们新建一个cpp文件MyMsgCap.cpp,把Win32.cpp里面的内容复制过来,并把Win32.cpp里面的内容全部注释掉。

然后把switch里面的内容删除:

我们如何来做MFC那样的封装呢?

它实际上就是把WM_CREATE这个Windows消息,和我们的一个OnCreate函数建立起对应关系。

我们以后就不再需要维护switch结构了,我们只要有一类Windows定义的消息,我们写一个消息,让这个消息和我们这样的自定义函数来进行对应,我们只需要当有消息的时候找到对应的函数就能完成入口点;
这么一来,我们的WndProc就永远不必改变了:

为了更好地理解我们这样的概念,我们一起来看一下,如何实现我们的MainWndProc窗口回调函数:

隔离变化,封装变化,让我们程序的可维护性变得更加强壮。

接着我们来写OnPaint函数,把Win32.cpp里面WM_PAINT消息处理分支代码复用一下:

我们看到str变量没有,我们以前的想法是,当鼠标按下执行LButtonDown函数的时候把鼠标按下的坐标信息放到str字符串里面,str对于这两个case分支都是可见的(如下图所示):

现在,我们需要有一个能跨越这两个函数(OnPaint和OnLButtonDown)的共同的变量,这就是一个全局变量:

当前这个程序的可读性和可维护性,比我们以前那个Win32Cap程序好太多,因为我们以后只要专心致志的写OnPaint和OnLButtonDown函数就行了。

但是在我们这个程序当中有str这种全局变量的存在,这个全局变量解决的是OnPaint和OnLButtonDown通信的问题,那么我们又会有一个问题出现了,这个str全局变量对于我们的WinMain主函数和我们的MainWndProc这样一个窗口回调函数,它是没有意义的,对于我们的OnCreate函数来讲也没有太大的意义,那这样必然会使得,如果我们的程序业务逻辑越来越庞杂了以后,我们可能会充斥着大量的这种为业务而业务的全局变量,这样一个的全局变量对于我们工程来讲,又是一个很糟糕的事情,因为这个str它只针对OnPaint和OnLButtonDown的业务逻辑相关;
这个时候我们就想了,我们能不能进一步的把我们这种数据和我们数据封装方法整合的更加紧密一点呢?

我们通过message映射的这种雏形,让大家感受到这种框架封装的思路和解决问题的工程学意义。
我们如何利用C++的语法封装出我们相应的一些动态识别、类型跟踪、动态创建等这一系列的技术,从而把我们一个窗口映射成一个C++对象,把我们一个应用程序变成一个C++对象,通过C++的运行时支撑出我们这样一个面向对象的概念,把我们C语法风格的函数,变成一个可以通过对象和对象之间驱动完成的内容,给它实现起来。

2.3 第一个MFC的简易封装:HINSTANCE、句柄,为什么用CWinApp封装WinMain,用CFrameWnd封装窗体

简单的来讲,在我们写Windows应用程序的时候,我们一定要有一个概念,它是一个有内核存在的结构,换句话说,我们写的这个程序它一方面向上是给用户进行交互,向下是给操作系统进行响应,帮助我们的应用程序完成业务逻辑功能。

Windows的应用程序,它的主函数和事件消息处理函数是由系统进行隔离的,也就是说一个Windows当中主函数是由操作系统调用的,与之相对应的窗口回调过程呢也是由操作系统调用的;
这也就意味着实际上Windows应用程序它的主函数做了两样工作,第一件事情是创建图形接口,第二件事情是把自己压入一个叫做消息循环的过程当中,并且让自己一直处在这个循环当中,它通过不断地接收消息事件,并且对消息事件进行捡取分发,从而完成自己整个生命周期。

2.4 简易封装案例上——从MFC中Cwnd与CWinApp的原型实现

我们还是选择一个空的Win32的GUI项目,来帮助大家来体会一下封装的过程:

首先定义全局变量和函数,这些是用来通信的:

hInst是我们这个应用程序被我们Windows拉起来以后,作为一个进程来感知的进程句柄;
这是Windows操作系统感知应用程序存在的唯一标志。

封装窗口类CFrameWnd

这个时候我们的CFrameWnd的这样一个注册窗口的功能就做成了,我们这个注册窗口仅仅是让我们操作系统感知到了这样一个应用程序的窗口对象的存在,我们还没有办法把它画出来。

同样的我要让它加入消息循环:

这样子的话我们的窗口就封装完成了。

封装应用程序类CWinApp

这里对ShowText作为一个传递的全局变量进行响应。

把这个全程序优化给它关掉,因为这个导致Debug和Release版本冲突了。

2.5 简易封装案例下——从C到C++的封装困境

我们将上节课中的CWinApp给抽出去,新建一个WinApp.h头文件:

我们把CWinApp类中的实现也给它抽出来,新建一个WinApp.cpp文件:

我们还得将CFrameWnd类给抽出来:

我们可以看到,WndProc和hInstance这边都感知不到,我们要进一步把这些内容进行抽象。

我们还需要这一系列的全局变量和函数原型:

把上面两张图的内容都抽出来,新建头文件global.h:

注意啊,这个时候我们的global.h、FrameWnd.h和WinApp.h对于我们的客户端程序也就是SimpleCWinAppAndCFrameWnd.cpp实际上是不可见的,我们可以把需要的内容(头文件),按照语法规范加入相应的文件当中。

我们把WinApp.h,以及WinApp.cpp中的CMyApp类和FrameWnd.h给加到我们客户端程序当中:

也就是说我们把WinApp.h、FrameWnd.h加进来,同时我们按照规范进行派生(CMyApp),按照这个框架开始运行,我们的应用程序就能够被构建起来。

虽然我们按照可以完成C++对C风格的Windows程序的封装,但是这种封装是不完全的,特别是上图这样子的消息函数做的也不好,所以当它加入消息循环以后啊,我们如果要是把每个消息映射到每个事件当中,就会变得比较困难;
而且还有一个问题,即使我们保证上面没有形式上的语法错误,编译的时候仍然有错误。

这个时候能够在编译环境当中找到ShowText的内容,但是重新编译生成我们发现会有一堆错误:

有C++开发经验的就会知道这种是由于重复包含导致的,而且在这些重复包含当中,我们通过弱符号链接的时候它在找对应位置的时候都会有问题,因此我们的MFC必须对我们的类结构进行一个统一的设计。

在一开始我们给的这种SimpleMFC程序,是想让大家知道这样的现象,就是我们可以通过对Win32程序的一种纠正,准确的来讲就是说把它变成可以用面向对象的思想,以及函数和数据结构的绑定,使得我们这个框架结构变得清晰;
这种简易的封装让大家知道了我们的CFrameWnd和CWinApp从代码原理当中进行了一种简装和拼图。

接下来,我们会仿照MFC开始对它逐个的关键技术进行仿真,分别是动态器识别,CWnd的生成,工作者线程和UI线程的设计,自己动手完成一个MFC程序。

Windows MFC 工程应用开发与框架原理完全剖析教程(上)相关推荐

  1. Windows MFC 工程应用开发与框架原理完全剖析教程(下)

    Windows MFC 工程应用开发与框架原理完全剖析教程(下 消息循环基础类CCmdTarget的设计 消息循环基础类CWnd的定义 消息循环基础类CWnd的句柄映射 消息循环基础类CWnd的窗口注 ...

  2. MFC主流开发库BCGControlBar下载安装教程!

    1.BCGControlBar简介 1.1 概述 BCG是MFC的一个扩展库,可以用来构建类似于Microsoft Office 2003/2007/2010/2013/2016 和 Microsof ...

  3. Windows phone 应用开发[12]-Pex 构建自动化白盒测试[下]

    本篇承接于上篇Windows phone 应用开发[11]-Pex 构建自动化白盒测试[上] .大概了解Pex作为自动化白盒测试工具工作方式.以及提出参数化单元测试的概念.为开发人员减少手动编写大量独 ...

  4. VS2015之博大精深的MFC项目开发(二)

    VS2015之博大精深的MFC项目开发(二) 第二章 MFC原理篇 1.MFC06-1:CString类的测试 1.1 operator+函数 1.2 Delete函数 1.3 Find函数 1.4 ...

  5. Visual C++ MFC/ATL开发-高级篇(一)

    在VC++6.0中用MFC进行COM编程首先应当明确,MFC中是通过嵌套类而不是多重继承来实现COM接口的,通过接口映射机制将接口和实现该接口的嵌套类关联起来:MFC中提供一套简明的宏来实现嵌套类的定 ...

  6. 《精通Windows Sockets网络开发--基于Visual C++实现》.(孙海民).[PDF]ckook

    图书作者: 孙海民 图书编号: 9787115179111 图书格式: PDF 出 版 社: 人民邮电出版社 出版年份: 2008 图书页数: 400-500 [内容简介] windows socke ...

  7. QT + OpenCV + MinGW 在windows下配置开发环境

           由于研究项目需要,最近开始接触C++界面设计,关于"QT + OpenCV + MinGW在windows下配置开发环境"着实让人头疼,单次配置时间相当长,也十分不容 ...

  8. Windows 网络通讯开发

    Windows 网络通讯开发 一.Windows网络开发API 由于C++标准库中没有网络库,所以进行网络开发的时候要调用系统API.Windows通讯开发API包括以下几个基本函数及成员类型: 1. ...

  9. 《MFC游戏开发》笔记五 定时器和简单动画

    本系列文章由七十一雾央编写,转载请注明出处. http://blog.csdn.net/u011371356/article/details/9332377 作者:七十一雾央 新浪微博:http:// ...

  10. 《MFC游戏开发》笔记三 游戏贴图与透明特效的实现

    本系列文章由七十一雾央编写,转载请注明出处. http://blog.csdn.net/u011371356/article/details/9313239 作者:七十一雾央 新浪微博:http:// ...

最新文章

  1. 单目相机的内外参标定
  2. The RAII Programming Idiom
  3. 主成分分析的数学原理
  4. Windows2008+sqlserver2008集群安装(图文并貌)
  5. 【.Net Micro Framework PortingKit – 12】SysTick驱动开发
  6. 【转】eclipse中egit插件使用
  7. 实例讲解Nginx下的rewrite规则 来源:Linux社区
  8. 前牙正常覆盖是多少_深覆合和深覆盖两者怎么区分?花两分钟进来了解一下
  9. 学生图书管理系统(附源代码及数据库)
  10. docker 批量关闭处于exited状态的container容器--shell工具
  11. STM32基于Proteus虚拟仿真电源设置
  12. python 基因测序_使用机器学习和Python揭开DNA测序神秘面纱
  13. stm32下OLED屏的应用
  14. 华为鸿蒙OS5摄概念机,华为P50Pro概念图:首发鸿蒙OS,后置5摄能让iPhone12甘拜下风吗...
  15. Dreaming to Distill Data-free Knowledge Transfer via DeepInversion
  16. matlab 双y轴对数坐标 误差线,matlab双y轴添加误差棒(转载)
  17. 帕斯卡三角形html,数学之美:杨辉三角(帕斯卡三角)的奇特性质
  18. Scratch软件编程等级考试一级——20191221
  19. 第033层:Java核心知识点面试题之-MySql篇
  20. React多页面应用5(webpack生产环境配置,包括压缩js代码,图片转码等)

热门文章

  1. refresh是什么?Spring refresh的12个步骤
  2. xcode9 IphoneX 调试
  3. java stringbuilder 构造函数_java---StringBuilder类的用法(转载)
  4. 饭后小甜点leetcode——哈希表
  5. 饭后的不良习惯,看看你中了几招?
  6. 计算机wifi无法打开,苹果Mac电脑WiFi无法打开解决办法
  7. 如何在Windows 10上修复缩略图问题
  8. 合作式智能运输系统车路协同云控系统V2X设备接入技术规范
  9. 三分钟细数 el-form 表单校验的坑点,前车之鉴,可助你避坑
  10. win11怎么开启休眠睡眠?