按惯例,这一篇文章主要还是作者读《深入浅出MFC》整理的一些笔记。不过本次还加上了一些作者自己的理解。
实验的前期准备
做实验前,你最好了解一下MFC的执行流程,从winapp到各类控件的实际变化过程,可以参考博主之前的笔记。
实验描述:我们需要在各窗口控件之间自由的把各种类型的变量传来传去,不是搞那些眼花缭乱的Windows宏定义(什么这个句柄那个句柄,这个指针那个指针的),而就搞三个简单的:char、int和char*。实际上这三个实体类型变量怎么在MFC各个类之间传来传去搞明白了,那些奇奇怪怪的各种函数用得上的Windows宏定义就都明白了。
实验的一些思想准备:
1.MFC的本质是一个封装好的类库,你在实际使用时是一个类一个类的用的。需要什么功能的时候就用什么类。比如你需要一个锤子🔨,那么你需要先声明一个锤子🔨(或者说召唤一个🔨),你懂的,声明你就一定要给这个🔨取个名字,你取好名字之后,这个锤子才能真正变成实物,你才能正确的调用锤子锤东西(调用锤子的锤功能:锤())。(如果你看过fate动漫的话,类就是卫宫大侠固有结界里面的剑,你在用的时候就是剑在现实世界的投影,当然,这样的剑是想要多少把就有多少把的)
2.RC文件是你画画的地方,是外表,C++是你在画好的东西实际运作的逻辑,是内在。好比RC文件是你画的一个人的脸,这张脸有眼睛有鼻子有嘴巴,而C++是眼睛嘴巴鼻子的实际功能,是支撑眼睛嘴巴鼻子正常工作的血管、肌肉、心跳这些你看不见的逻辑东西。一个窗口完全由RC文件、图片资源、RC编译器就能构成了。RC文件会告诉你每个地方的名字(一个ID)C语言的类会加载RC文件中得ID,并在message_map和resource.h中加载这个ID给类。RC文件的格式和写法你不用了解的太多,只需要重点关注一个变量就行了:ID。你也可以在你的项目文件中找到.rc文件来具体读一读,看看文件里的id是怎么用在各个地方的。
IDD_MFCLAB_DIALOG DIALOGEX 0, 0, 322, 189
STYLE DS_SETFONT | DS_FIXEDSYS | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME
EXSTYLE WS_EX_APPWINDOW
FONT 9, "MS Shell Dlg", 0, 0, 0x1
BEGIN
DEFPUSHBUTTON "确定",IDOK,211,168,50,14
PUSHBUTTON "取消",IDCANCEL,265,168,50,14
LTEXT "启动",IDC_TEXT,28,71,38,17
PUSHBUTTON "启动",made,22,106,50,14
PUSHBUTTON "复原",IDC_BUTTON2,22,126,50,14
END
从这里可以看到,不同的组件类型,所在位置,ID名称,隶属于哪个主面板,都在.rc文件中清清楚楚的写明白了。笔者建议你,最好把画画和实现功能看成两件事,分别交给RC文件和C去做,让画画的专心画画,让回调函数的专心回调。
3.在MFC的众多类中,比较重要的是CWinApp和CWnd这两个类,这两个类一个初始化程序,一个给定传统意义上的回调函数,是最常用到的两个类(不论是哪个类,你都可以在“…\VS\IDE\VC\Tools\MSVC\14.34.31933\atlmfc\src\mfc”路径下去找,去搜索)。CWinApp这个类在MFC自身执行WinMain函数的时候会调用,你只需把CWinApp的InitInstance()写好就行了。其中注意m_pMainWnd这个变量是属于CWinApp类的一个全局变量,比较特殊。
这个m_pMainWnd变量是CWnd*类型的,这个全局变量指向哪个具体的CWnd,MFC就把哪个CWnd当成主界面,优先打开这个界面。主CWnd窗口确定后,你还需要把RC文件中的ID与CWnd里的各种类、变量进行关联(message map和resource里面会有)。
关联的过程中,你只用管两件事:RC文件的某个地方具体需要一个什么样的CWnd类(根据你需要的CWnd函数来的)、在属于这个地方的RC消息函数中,哪个消息函数应该有什么响应。具体的不同类的不同功能可以去这里找。
4.消息体简述,如果你从消息函数映射、消息体的执行角度去理清所有的过程的话,一级一级的宏展开太不好了,真的很难读(单如果你能读懂,也会有很多启发,知道原来封装类是这么干的)。有一个简单的办法可以跳过这些繁琐的步骤,这个办法需要你有一点点灵动的直觉。消息者,事物之变化也。你看着电脑屏幕,敏锐的觉察到哪里有变化,哪里就一定有消息。
哲学的说,操作系统中给了你很多描述操作系统状态的变量,这些变量跟你的实际系统状态一一对应,实际上构成了对应操作系统的状态变量空间(类似于热力学中的相空间),你把两个不同的操作系统状态做对比,哪个系统状态量不一样(也就是描述两个操作系统的变量在相空间中分别属于两个点),我们就可以把这个不一样的变化看作一个消息。你就可以用此消息做一个消息体,也可以相应的写这个消息体的回调函数。
消息者,事物之变化也,Windows的系统变量空间是固定的,就那么多。如果你敏锐的发现系统发生了一点变化,那么对应的,系统的变量状态空间也一定会有所变化,所以说,只要你能找到的相空间中不同的点,点到点之间的过程,都可以作为消息体。
举例来说就是:鼠标点击物件可以作为消息体,鼠标点击物件两次,也可以作为消息体(时间与鼠标点击次数自然是Windows系统变量空间的一部分,且这两个事件对应了状态空间中不同的两个点)。
5.消息体与回调函数的绑定是在message_map中完成的,具体来说是在message_map一系列眼花缭乱宏展开之中最核心的AFX_MSGMAP_ENTRY _messageEntries[]数组内定义的,如果你不清楚这个数组是什么东西,你可以去看我之前讲message_map的博客。
这里简单的描述一下,MFC有一个自动机,会顺序地读取_messageEntries[]数组内的所有绑定起来的元素,并将每个你绑定好消息体和回调函数的_messageEntries结构体放在传统的WndProc函数中执行,从而达到消息体与回调效应绑定的作用。
如果你还搞不清楚,你可以这样理解:回调函数是一定要你自己写的函数,在此决定了触发后的响应。从4可知,你只要确定一个消息,你就可以让系统在监听到这个消息的时候做出一点事情,做的事情就放在你写的回调函数中,也是由操作系统帮你去做,你只需要在message_map里把消息和回调函数绑定就可以了,系统会明白的。
6.RC中的各类组件只是给了你一个框架,你还需要往这个框架里注入灵魂,也就是你的RC中声明的各类控件ID都需要给他们配备对应的CWnd类。你注入的CWnd类也需要取一个变量名称,如果你注入的类需要执行具体的功能(也就是执行类自带的跟操作系统交互的函数),那么你在定义CWnd类时要声明这是一个control变量(也就是可以调用系统交接函数的投影出来的🔨),否则,如果你只是想给某个类添加一个方便你使用的数据,那么你可以定义一个值变量(就是定义一个int、char、或者系统宏CString之类的不会跟系统交互的变量)。
给RC控件ID注入实际的CWnd类,有助于你彻底确定这一类可能跟哪些种类的消息体发生关系,在这一CWnd类中,你可以从各种可能的消息体(比如单击、双击、或者长按)挑出一个你想用的,作为回调函数的触发条件,前往message_map绑定即可。
回调函数内部,多半也是在写你给出的实例类变量中的各种功能函数。
实验的内容
本次实验使用两个具体组件:static_text、button。要求是在按下button时static_text显示我们准备好的字符串,并且在按下不同的button时,让一个变量可以在不同的组件之间传递。
第一步:在RC编辑器中画画
在你给定的编辑器中,添加两个static_text部件和三个button部件,更改他们的描述性字符串,更改他们的控件ID(ID和描述字符串不一样的)。
第二步:为RC文件中的各类控件ID添加实例化的CWnd子类
打开ClassWizard(右键菜单中的类向导),在成员变量中添加各变量。
如上图所示,最左边的是RC画画中画出来的控件ID,你可以在此处针对不同的控件ID赋予不同的CWnd子类(投影🔨),你也可以针对不同的控件做本地化的变量。变量在头文件中声明,并在DoDataExchange()函数中绑定,详见如下代码:
//在头文件中新加类变量的定义:
// 构造
public:
CMFCLabDlg(CWnd* pParent = nullptr); // 标准构造函数
virtual ~CMFCLabDlg();
// 对话框数据
#ifdef AFX_DESIGN_TIME
enum { IDD = IDD_MFCLAB_DIALOG };
#endif
protected:
virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV 支持
// 实现
protected:
CMFCLabDlgAutoProxy* m_pAutoProxy;
HICON m_hIcon;
BOOL CanExit();
// 生成的消息映射函数
virtual BOOL OnInitDialog();
afx_msg void OnSysCommand(UINT nID, LPARAM lParam);
afx_msg void OnPaint();
afx_msg HCURSOR OnQueryDragIcon();
afx_msg void OnClose();
DECLARE_MESSAGE_MAP()
public:
CButton class_R;
CStatic text_L;
CStatic text_R;
CButton class_A;
CButton class_B;
afx_msg void OnBnClickedA();
afx_msg void OnBnClickedB();
afx_msg void OnBnClickedgotor();
};
//将新添加的类变量添加到主对话框中去
上述头文件中定义了新添加的类变量,并在下面函数中进行绑定:
//这个方法一般写在主对话框的cpp文件中
void CMFCLabDlg::DoDataExchange(CDataExchange* pDX)
{
CDialogEx::DoDataExchange(pDX);
DDX_Control(pDX, gotoR, class_R);
DDX_Control(pDX, Left, text_L);
DDX_Control(pDX, Right, text_R);
DDX_Control(pDX, set_A, class_A);
DDX_Control(pDX, set_B, class_B);
}
第三步:挑选你认为合适的消息体作为触发器,写对应的回调函数并在message_map中绑定
你可以在Classwizard中找到不同CWnd子类常用的消息体(也并不是就只能用这些了,CWnd子类你可以自己写的):
根据你实际的控件ID种类的不同,Classwizard已经把常见的消息体列出来了,比如针对gotoR这个按钮而言,BN_CLIKED、BCN_DROPDOWN、BCN_HOTITEMCHANGE、BN_DOUBLECLICKED、BN_KILLFOCUS这些东西都是某个消息体的宏名。
下面是各类宏的溯源记录:
//MESSAGE_MAP
BEGIN_MESSAGE_MAP(CMFCLabDlg, CDialogEx)
ON_WM_SYSCOMMAND()
ON_WM_CLOSE()
ON_WM_PAINT()
ON_WM_QUERYDRAGICON()
ON_BN_CLICKED(set_A, &CMFCLabDlg::OnBnClickedA)
ON_BN_CLICKED(set_B, &CMFCLabDlg::OnBnClickedB)
ON_BN_CLICKED(gotoR, &CMFCLabDlg::OnBnClickedgotor)
END_MESSAGE_MAP()
// User Button Notification Codes
#define ON_BN_CLICKED(id, memberFxn) \
ON_CONTROL(BN_CLICKED, id, memberFxn)
#define ON_BN_DOUBLECLICKED(id, memberFxn) \
ON_CONTROL(BN_DOUBLECLICKED, id, memberFxn)
#define ON_BN_SETFOCUS(id, memberFxn) \
ON_CONTROL(BN_SETFOCUS, id, memberFxn)
#define ON_BN_KILLFOCUS(id, memberFxn) \
ON_CONTROL(BN_KILLFOCUS, id, memberFxn)
// for general controls
#define ON_CONTROL(wNotifyCode, id, memberFxn) \
{ WM_COMMAND, (WORD)wNotifyCode, (WORD)id, (WORD)id, AfxSigCmd_v, \
(static_cast< AFX_PMSG > (memberFxn)) },
#define WM_COMMAND 0x0111
可以看到,从message_map中绑定的消息体宏名可以一路展开到具体的消息体地址、消息体id、消息体实际AfxSigCmd_v函数。
第四步:在回调函数中写你认为需要执行的操作
由于窗口的各类物件都是实在的类对象,所以我们可以用类对象本身的函数实现特定的功能,比如让类对象本身的数值变化。
我们在三个按钮的回调函数中做如下动作:
void CMFCLabDlg::OnBnClickedA()
{
// TODO: 在此添加控件通知处理程序代码
text_L.SetWindowText(_T("A"));
}
void CMFCLabDlg::OnBnClickedB()
{
// TODO: 在此添加控件通知处理程序代码
text_L.SetWindowText(_T("B"));
}
void CMFCLabDlg::OnBnClickedgotor()
{
// TODO: 在此添加控件通知处理程序代码
CString buff;
text_L.GetWindowText(buff);
text_R.SetWindowText(buff);
}
实际效果为:
点A按钮,左边变A,点B按钮,左边变B,点gotoR按钮,右边与左边保持一致。
至此实验结束。