从零开始学 VC 系列教程


从零开始学 VC 系列教程 从零开始学VC系列教程 一.信息显示实验 ..........................................................................1 从零开始学VC系列教程 二. 对话框及常用控件实验 ........................................................8 从零开始学VC系列教程之 三.串口通信与自定义消息 ....................................................21 从零开始学VC系列教程之 四.并口控制与类的使用 ........................................................34 从零开始学VC系列教程 五.消息机制与自定义消息 .........................................................45 从零开始学 VC 系列教程 一.信息显示实验 题外话:第一次写教程,不知道该怎么开始.以前见过的教程可能都会介绍比较多的理论 然后才开始讲实际操作,我想对于 VC,理论讲得太多大家可能更糊涂,所以选择了这个方案,我 们先一步步照着做,每一次只介绍一两个知识点,并且把理论放在后面说明,希望大家能喜欢 这种方式,并对这种方式提出意见和建议,以便以后章节更适合大家的学习. 或许我写的还不能称之为教程,只是一些操作步骤及说明,通过这些练习,希望能使大家 对 VC 有一个新的认识.VC 的功能是十分强大的,但我们一般并不会用到所有的功能,就像大 家熟悉的 KeilC 一样,其实 Keil 功能也是很多的,平时也没见谁把 Keil 的功能都用到了.基于 此,我写的这个教程可能在 VC 专业人士看来比较简单.不是要班门弄斧,而是希望推行上位机 技术,形成一个氛围让大家来学习交流,PC 机与下位机结合毕竟也是发展的一个方向.望高手 谅解小辈的不自量力.同时,我们结合单片机编程,让大家明白上位机控制方法及原理,当然这 是今后章节的内容了. 本节内容:学会在文本框中显示文本,学会弹出式对话框使用. 学习目的:信息显示是人机交互的基础,同时,信息显示也为以后的程序调试打下基础. 1. 新建工程.打开 VC6.0 点击[文件]->[新建],弹出如下对话框. 在工程选项中选择 MFC AppWizard[exe],选择好工程存入的位置,然后在工程名称中填入工 程的名称,例如 Eg01,填入 Eg01 后.VC 会在我们选择的路径后自动加入一个以工程名命名的 文件夹作为工程目录.完成按[确定]就会进入工程向导,出现以下的提示. 在这个我们选择[基于对话框]就行了.单文档及多文档方式会在以后详细介绍.这里也简要说 明一下.①单文档.程序一次只能处理一个文档对象(文件).像写字板,记事本等.②多文档. 程序一次可以处理多个对象(文件),像 word 之类的,可以同时打开多个文件.③基本对话框. 程序一般用于处理一些比较小的,工具型的软件.像双龙的 ISP 下载软件, Easy 51Pro 等都是 基于对话框.一些不是很复杂的工业软件也通常可以用基于对话框的形式,这种方式的优点 是所见即所得,一些控件可以像 VB 一样拖到工作区就行了.选好以后直接按[完成]就可以了. 至于[下一步]的向导,这里我们先不管.然后会进入以下的画面.其中红色及绿色是我的标注, 看不清图片可以下载图片放大. 选中静态文本框[TODO:在这里设置对话框控制],单击指标右键,在弹出的快捷菜单中选择 [属性 Properties]菜单项,弹出如下对话框 修改 IDC_STATIC 为 IDC_FIRSTLABEL,标题可以写为”第一个应用程序”然后关闭这个对话 框,就可以看到刚才的静态文本框内容变为”第一个应用程序”了. 在刚才的操作中,我们将静态文本框的 ID 改为了 IDC_FIRSTLABEL,在 VC 中,ID 是控件的标 志,不能有相同的 ID 出现,但允许多个名为 IDC_STATIC 的 ID 号.所以,一般并不把文件名命 为 IDC_STATIC,除非以后我们不想在程序中改变这个控件的属性.有一些控制,例如 GroupBox 一般并不在程序中改变什么,所以不改变其 ID 号.ID 是程序访问控件的标识,所以 一个应用程序中不能有重复 ID. 按下来我们要添加一个按钮. 用鼠标将控件条上的按钮拖到对话框上,可以看到对话框上多了一个按钮. 用上叙方法,改变 按钮 ID 为 IDC_BTN1,标题改为”显示”,这样就做好一个按钮了.下面我们为按钮添加代码. 双击按钮,会出现以下的提示框. 按[OK]为按钮添加响应函数,出面以下画面 void CEg01Dlg::OnBtn1() { // TODO: Add your control notification handler code here } 这一段就是刚才按钮的函数了.当然,现在还没有响应.现在我们添加一个事件,让这个按钮按 下后上面的静态文本框的内容变为”串口号:COM1 波特率:57600”把上面的代码改为 void CEg01Dlg::OnBtn1() { // TODO: Add your control notification handler code here SetDlgItemText(IDC_FIRSTLABEL,” 串口号:COM1 波特率:57600”); } 添加完成以后,按 F7 编译.如果编译通过,按 F5 运行,可以看到程序运行后的对话框 按下按钮,可以看到上面的静态文本框的内容改变了.那么恭喜你,完成我们这个例子的第一 步操作了. 当然,我这里的对话框看起来没那么高,大家可以调一下高度.怎么退回到控件编辑状态呢? 首先,我们单击选项卡中的[ResorceView],现在大家只能看到[Reso…]看到 IDD_EG01_DIALG 了吧,这个其实也是一个ID,是我们对话框的ID,如果大家改一下对话框的ID,就可以看到这里 的 ID 也变了.在 VC 中,所有的控件都认为是窗体,只是形式不同.双击 IDD_EG01_DIALG 就 可以看到我们刚才的控制编辑状态了.双击[显示]按钮又可以回到代码状态. 刚才的例子很简单,却是我们以后会用得最多的.下面我们来分析一下代码 void CEg01Dlg::OnBtn1() { // TODO: Add your control notification handler code here SetDlgItemText (IDC_FIRSTLABEL,” 串口号:COM1 波特率:57600”); } 看看 SetDlgItemText 这个函数,大家就会明白了, IDC_FIRSTLABEL 是我们给静态文本框分 配的 ID,后面的参数是要显示的内容.上面我们提到过,VC 中的控件都认为是窗体,所以,对于 所有的文本显示,我们都可以用这个函数来做,大家可以试着加入一个编辑框(Edit),同样可以 使用用这个函数,只要 ID 号对了就可以了.VC 对大小写敏感,大家要注意大小写. 下面我们用另一个方法来实现. 把代码改为 void CEg01Dlg::OnBtn1() { // TODO: Add your control notification handler code here CString a; a=” 串口号:COM1 波特率:57600”; SetDlgItemText (IDC_FIRSTLABEL,a); } 然后按 F7 编译,无误则按 F5 运行.可以看到运行结果是一样的. 大家可以看到,我们把变量 a 定义为 CString 类型,这个类型我们在 C 中可能没有见过.CString 其实是 VC 中的一个类.这里我们要引入类的概念了.类跟 C 语言结构体很像,结构体可以有成 员变量,但不能有成员函数,类不但可以有成员变量,还可以有成员函数,并且支持多种方法.在 VC 中,类用 class 标识.下面是一个类的定义,我们来分析一下. class CEg01Dlg : public CDialog { public: CString GetPath(void); protected: int GetMax(int a,int b); public: int a; int b; }; 这个类是我改过的,为了大家看得明白一点.首先,我们从这句 class CEg01Dlg : public CDialog 知道这个类名是 CEg01Dlg,那么 public 后面的 CDialog 是什么意思呢?CDialog 其实也是一个 类,是 VC 中标准对话框类. public CDialog 意思是我们定义的这个类 CEg01Dlg 从 CDialog 派 生,也叫从 CDialog 继承,从继承这个词语大家就会明白 CEg01Dlg 不但可以有自身函数,而且 还可以用到 CDialog 里的变量及函数,所以说是继承,就像我们说的继承财产一样,儿子不但可 以有自己的钱,还有从老爸那里继承的产财,这些钱都能花.所以,我们也叫 CDialog 为父类(不 是爸类啊),把 CEg01Dlg 称为子类.当然,这是相对的.如果再定义一个类从 CEg01Dlg 派生,那 么 CEg01Dlg 也就是父类,派生出来的类就是子类.这段可能有点难明白,大家可以慢慢体会. 我们再回到 CString 上面来,这个类不是派生的,所以也没有父类了.用 CString 定义一个变量 a,我们称为类变量,CString 有很强大的功能,我们这里只用到一个功能,就是字符串,a 可以是一 个不定长度的字符串,所以我们可以给 a 赋一个任意的字符串而不用关心长度.当然,我们也可 以用 int StringLen=a.GetLength();来获得这个字符串的长度.从这句可以看到,GetLength()是类 CString 里的一个函数.大家可能又会关心一个问题,怎么把长度显示出来呢?我们把代码改为 void CEg01Dlg::OnBtn1() { // TODO: Add your control notification handler code here CString a; a=" 串口号:COM1 波特率:57600"; SetDlgItemText (IDC_FIRSTLABEL,a); a.Format("字符串的长度:%d",a.GetLength()); MessageBox(a); } 运行一下,效果如下 这里我们用到了类 Cstring 的另一个成员函数 Format();这个函数用起来跟 C 语言里的 Printf() 很像,我就不多说明了.格式化后的字符串还是存放在 a 中,在这里,我们还用到了一个新的函 数 MessageBox();我们只是用了这个函数最简单的用法,显示一个字符串.MessageBox 用于弹 出一个提示对话框.查查 MSDN 就知道,函数原型是 int MessageBox( LPCTSTR lpszText, LPCTSTR lpszCaption = NULL, UINT nType = MB_OK ); 在VC里面,函数可以有默认值,例如LPCTSTR lpszCaption = NULL, UINT nType = MB_OK变 量 lpszCaption 的默认值是 NULL, nType 的默认值是 MB_OK,这些有默认值的变量我们可以 不传值,所以这个函数使用时,最简单的用法就是传 lpszText 就可以了.如果我们要显示一个固 定的字串,可以这样调用 MessageBox(“大家好”);是不是很简单啊? 这一章就写到这里了,一些问题大家可以提.未尽知识点,会在以后再详细介绍. 从零开始学 VC 系列教程 二. 对话框及常用控件实验 恭喜你,进入 VC 学习的第二节了.这一节是人机交互的基础.所谓人机交互,说通俗点就 是与机器对话.然而我们现在的技术还不能像科幻片里一样与机器人直接说话就行了.所以, 我们的操作意图还得通过文本输入,命令按钮等等来实现. 本节内容:学会对话框调用及一些常用控件的使用方法. 学习目的:学习人机交互,为软件开发提供界面基础. 1. 新建工程.参考第一节的方法新建一个工程,名字为 Eg02 完成后如下图 细心的朋友一定会发现.新建的工程里还有一个对话框,ID 名是 IDD_ABOUTBOX 这个是做 什么用的呢?我们用到的软件都会有一个版权声明.通过第一章的学习,大家应该知道怎么观 看这个 IDD_ABOUTBOX 对话框了吧.没错!双击 IDD_ABOUTBOX 就可以了.我们会看到如 下的一个对话框 这就是我们这个程序的关于对话框,一般用于版权声明及版本号标识.大家看到的这个对话框 里有两个静态文本框和一个图像框(Picture),静态文本框我们在前一节已经介绍过了.大家可 以修改一下版权所有这一行,填什么都可以,签个大名也行.完成以后你一定想看看效果,这个 对话框怎么打开呢?其实 VC 已经为我们做好了.先按 F7 编译,然后按 F5 运行.大家可以看到 程序运行了. 单击应用程序图标,就会出现一个菜单,选最后一个[关于 Eg02],关于对话框就弹出来了. 当然,这是系统为我们做好的.自己怎么在程序中调用这个对话框呢?为了演示,首先参考第一 节的内容添加一个按钮,然后把按钮的 ID 改为 IDC_BTN_ABOUTME,把标题,也就是 Caption 改为[关于].最终效果如下 下面我们为按钮添加代码.相信大家一定还记得怎么进入代码吧..对了,双击[关于]按钮,在弹 出的对话框中点[确定]就可以了.为了让大家更好的理解下面的操作,我们先要解释一下关于 对话框的类.VC 向导会为关于对话框建立一个类,大家看看下面的图 单击标签 ClassView(这里显示的是[Class…])就可以看到 Eg02 这个应用程序的类.第一个 CAboutDlg 就是关于对话框的类.CEg02Dlg 对应 IDD_EG02_DIALOG.中间的 Ceg02App 是应 用程序的基础类.所以,如果要对关于对话框进行操作,就要用到类 CAboutDlg,因为与此有关 的函数及变量都封装在 CAboutDlg 中.看到这里大家可能又糊涂了,没关系,在以后的教程中, 通过一些练习大家就会慢慢领会到的.这里还是先为[关于]按钮添加代码. void CEg02Dlg::OnBtnAboutme() { // TODO: Add your control notification handler code here } 上面是 VC 为[关于]按钮添加的响应函数.我们添加代码成以下所示 void CEg02Dlg::OnBtnAboutme() { // TODO: Add your control notification handler code here CAboutDlg ADlg; ADlg.DoModal(); } 一共有两句,第一句是CAboutDlg ADlg;作用是定义一个变量Adlg.第二句是ADlg.DoModal(); 功能是调用类 CAboutDlg 里的一个函数 DoModal();这个函数在 MSDN 里的解释是 Call this member function to invoke the modal dialog box and return the dialog-box result when done. This member function handles all interaction with the user while the dialog box is active. This is what makes the dialog box modal; that is, the user cannot interact with other windows until the dialog box is closed.一般我们用于显示一个对话框.其实大家看看 CAboutDlg 这个类下面,只有两个 函数 DoModal()这个函数并不在这个里面.第一章我们提到过类的派生和继承.其实 CAboutDlg 这 个类是派生于 CDialog 类,DoModal()这个函数是 CDialog 的成员函数,由于 CAboutDlg 是继承 父类 CDialog 的,所以 CDialog 里的函数在 CAboutDlg 中也可以使用. 下面我们来说说几个常用控件的使用. 首先在 IDD_EG02_DIALOG 对话框中加入一个 Edit(编辑框)控件.Edit 一般用于输入输出数 据文本.相当于 VB 里的 TextBox.加入 Edit 控件后,编辑其属性为 然后,我们再加一个按钮(PushButton),并编辑其属性为 接下来,我们先说一下要实现的效果.很简单,在编辑框里输入一个文本,然后按显示,就把文本 显示在静态文本框中.所以,这里要把静态文本编辑框的 ID 改为 IDC_DISPLABEL 下面我们为[显示]按钮添加代码 void CEg02Dlg::OnBtnShow() { // TODO: Add your control notification handler code here CString a; GetDlgItemText(IDC_EDIT_INPUT,a); SetDlgItemText(IDC_DISPLABEL,a); } 其实不复杂,也只有三句,第一句定义一个 CString 类变量 a 我们来说说 GetDlgItemText 这个 函数吧.查查 MSDN 就知道函数原型了. int GetDlgItemText( int nID, LPTSTR lpStr, int nMaxCount ) const; int GetDlgItemText( int nID, CString& rString ) const; 大家看看就觉得奇怪了,怎么有两个原型啊?并且一个是传两个参数,另一个是传三个参数.在 VC里面,同一个类下是可以存在多个同名函数的,具体调用哪个函数要看参数的不同.在这里 我们传入了两个参数,所以VC会调用int GetDlgItemText( int nID, CString& rString ) const; 这个函数.第一个函数是控件的ID号,第二个是字串.第二个传了地址,所以我们在下一句中用 的a已经是获得IDC_EDIT_INPUT的文本了.运行效果如下 下面介绍一下进度条的使用以及定时器的使用. 我们要实现的效果是进度从 0 到满格,然后再从 0 到满格,依次循环.每跳一格间隔 500ms,这个 时间我们用定时器来实现. 首先从控件条里拖出一个进程条到对话框,修改属性如下 然后我们要介绍一下 VC 的定时器.VC 里面使用定时器有多种方式,我们先介绍一种作为抛 砖引玉 首先添加一个 Windows 消息处理器.消息这个词语可能很陌生,我们会在后面很多次说明.这 里先照图做 在类管理器里选中 Ceg02Dlg 这个类,然后点右键,就会弹出一个菜单,选择[Add Windows Message Handler…],接下来会弹出另一个菜单 双击 WM_TIMER 然后按[确定]就可以了.大家会看到,CEg02Dlg 类中多了一个函数 这个就是 VC 中的定时器响应函数.然后双击这个函数就进入代码了 然后我们为 Timer 事件添加代码. void CEg02Dlg::OnTimer(UINT nIDEvent) { // TODO: Add your message handler code here and/or call default IDC_PROGRESS static int nPos=0; ((CProgressCtrl*)(GetDlgItem(IDC_PROGRESS)))->SetPos(nPos); if(nPos<100) nPos+=10; else nPos=0; CDialog::OnTimer(nIDEvent); } 首先定义一个整型的变量 nPos 用于记录进度条的进度值.默认时,进度条 0 为空,100 为满格. 从后的程序大家可以看到,这个变量自加到 100 就变为 0.最难理解的就是 ((CProgressCtrl*)(GetDlgItem(IDC_PROGRESS)))->SetPos(nPos); 首先, GetDlgItem(IDC_PROGRESS)这个函数用来获取 IDC_PROGRESS 的句柄,在 VC 里面 引入了句柄这个词语,我们将在下一章中对消息和句柄进行详细的说明, 句柄是 WINDOWS 用来标识被应用程序所建立或使用的对象的唯一整数,WINDOWS 使用各种各样的句柄标 识诸如应用程序实例,窗口,控制,位图,GDI 对象等等。WINDOWS 句柄有点象 C 语言 中的文件句柄。在这里,GetDlgItem 获取了窗体句柄,大家应该还记得,在 VC 里面,控制 就看成窗体。(CProgressCtrl*)这个地方是将返回的句柄强制转换为 CProgressCtrl 类型,与 C 语言的中的强制转换是一样的.进度条的控制类是 CprogressCtrl.而前面返回的是一个窗体类 型,所以先要强制转换.在第一章中,我们提到过,VC 中的控件都认为是窗体,在这里就体现出 来了. SetPos(nPos);这个函数是类 CprogressCtrl 的成员函数,用来指定当前进度条的进度.最后 还有一步,就是激活这个定时器.像我们的 C51 或 AVR 一样,要初始化定时器. 而 void CEg02Dlg::OnTimer(UINT nIDEvent)这个函数就像我们单片机的定时器中断服务函数一样. 时间到了就会自动执行. 参考上面的图,双击 OnInitDialog(),就可以进入对话框初始化函数,只要添加一句就可以了.完 成后如下 BOOL CEg02Dlg::OnInitDialog() { CDialog::OnInitDialog(); // Add "About..." menu item to system menu. // IDM_ABOUTBOX must be in the system command range. ASSERT((IDM_ABOUTBOX & 0xFFF0) == IDM_ABOUTBOX); ASSERT(IDM_ABOUTBOX < 0xF000); CMenu* pSysMenu = GetSystemMenu(FALSE); if (pSysMenu != NULL) { CString strAboutMenu; strAboutMenu.LoadString(IDS_ABOUTBOX); if (!strAboutMenu.IsEmpty()) { pSysMenu->AppendMenu(MF_SEPARATOR); pSysMenu->AppendMenu(MF_STRING, IDM_ABOUTBOX, strAboutMenu); } } // Set the icon for this dialog. The framework does this automatically // when the application's main window is not a dialog SetIcon(m_hIcon, TRUE); // Set big icon SetIcon(m_hIcon, FALSE); // Set small icon // TODO: Add extra initialization here SetTimer(0,500,NULL); //这里是添加的,别的都是自动生成的 return TRUE; // return TRUE unless you set the focus to a control } 在这里我们只添加 SetTimer(0,500,NULL);其中,参数 0 代表定时器的 ID 号为 0,.第二个 参数 500 是定时器的时间,单位为 ms,后面的 NULL 是指不要回调函数. 按 F7 编译后运行就可以看到运行效果了. 或许大家有个问题,为什么开始时是添加了一个 WM_TIMER 的消息处理器呢?我们使用 SetTimer 开始定时器后, 如果没有回调函数, 系统会在每次定时时间到后发送一个 WM_TIMER 消息到窗体.窗体收到这个信息后,调用 OnTimer()函数进行处理.系统定义这个 函数为 afx_msg void OnTimer(UINT nIDEvent);可以看到是以 afx_msg 修饰的,这种函数会与 其中一个信息关联,或者说是消息影射到这个函数.每当有消息发过来,都会执行这个函数.大 家只要好好想想单片机的定时器中断就会明白的,原理一样,只是传输机制不同. 如果上面的内容你制作成功了,那么恭喜你,第二节就基本学完了. 下面我们也来说说多任务和消息机制吧. Windows 是基于消息机制的,它是一个多任务的操作系统,也就是说,同一时间内,系统会挂起 多个任务.为了说明多任务,我们先来看一段单片机程序. Void main(void) { While(1) { TaskA(); } } 这个程序很简单,单片机工作后就进入 while()循环了,单片机这个时候就干一件事,那就 是执行任务 TaskA.这样的工作总是在一个主循环内实施,一次只执行一个任务的我们称为单 任务系统.单片机程序只要不引入操作系统并且由一个主循环一直执行完毕的基本都是单任 务的.有些程序也是这样写的. Void main(void) { While(1) { TaskA(); TaskB(); TaskC(); } } 这样的程序看起来似乎是有三个任务了.这三个任务是顺序执行的,也就是说,必须让 A 完成后,才能到 B,B 完成后才能到 C.如果 A 有一个长时间的延时,系统就会在 A 中空等,然而 B 与 A 本来是无关的,这样空等的时间就算是浪费了.如果我们的 windows 也是顺序执行的就 麻烦了,那时我们不仅仅会说 Windows 有点慢,而是说 Windows 像蜗牛一样在爬.如果我们让 这些任务更合理的安排一下,在执行 A 的时候,有空就去执行 B,而在 B 的空闲时间去执行 C 或者 A,那么时间就节省下来了.如果时间切换够快,那么我们可以认为 A,B,C 三个任务在同时 进行.所以,如果我们把单片机的执行时间分成若干等份,每份 1ms 或者更小,这种时间等份我 们称为时间片,每次时间到了就换一个任务.也就是说,第一毫秒执行 A,这时我们并不等 A 全 部执行完,并记住这个断点,到了第二毫秒执行 B,第三毫秒执行 C,第四毫秒又执行 A 并从原 来的断点开始执行,依次直至三个任务都完成.大家可以看到,任务 A 每三毫秒执行了一次.这 就是多任务的模型了. Windows 其实就是这么干的.只是每次任务时间不一定是我们上面说的 3ms,因为系统同 一时间内可能会有很多待执行的任务,这些任务就被系统按优先级排成队,一个个取出来执行. 比如我们现在打开 VC,系统在加载 VC,同时我们还可以移动鼠标,这些好像都是在同时进行, 其实它们也是按时间片分时执行的. 现在分析一下这个工作过程吧.现在我们要打开 VC,系统开始加载 VC 了,可以看到 VC 的 LOGO 界面,但这个时候我们还可以动一下鼠标,硬件首先会响应,并给软件发个通知,而软 件这个时候可能还没有轮到鼠标程序的执行,怎么办呢?系统会先把这个鼠标操作保存起来, 轮到鼠标时间片时再来处理鼠标操作.大家可能觉得,这样鼠标操作不是滞后了吗?确实是这 样的,但这个滞后时间是很短的,我们基本不会察觉出来. 我们来看看消息的定义. 消息系统对于一个 win32 程序来说十分重要,它是一个程序运行的动力源泉。一个消息, 是系统定义的一个 32 位的值,他唯一的定义了一个事件,向 Windows 发出一个通知,告诉 应用程序某个事情发生了。例如,单击鼠标、改变窗口尺寸、按下键盘上的一个键都会使 Windows 发送一个消息给应用程序。 消息本身是作为一个记录传递给应用程序的,这个记录中包含了消息的类型以及其他信 息。例如,对于单击鼠标所产生的消息来说,这个记录中包含了单击鼠标时的坐标。 综合前面的分析,消息就好理解了,鼠标动一下会产生一个事件,这个事件必须要告诉系 统啊,怎么告诉系统呢?Windows 把这个定义为 Message 也就是消息,所以,消息可以认为是一 个操作记号,记录上一步所发生的或者说是下一步所要处理的事件.系统为消息安排了一个队 列,里面可以存放很多个消息,我们称为消息队列.有键盘操作时,把键盘操作放到消息队列里 面,相当于做一个记号,我们动了键盘,有鼠标操作时,把鼠标操作放到消息队列,也便记住我们 在键盘后还动了鼠标,系统就把这些东西一个个按时间取出来执行.在 Windows 里面,鼠标消 息可以有几种,例如单击落了左键,系统会产生一个消息为 WM_LBUTTONDOWN 如果我们 移动了鼠标,就像产生一个消息为 WM_MOUSEMOVE 消息入队后,系统会依次处理.当然,我 们这样的解释虽然容易懂,但不是非常专业,并且 Windows 内部处理消息也比这复杂多了,但 基本原理就是这样的了,并且,我们在进行操作时,并不用关心 Windows 的底层是怎么处理这 些事件的,除非我们的操作真的让 Windows 生气了,就必须去查查出什么问题了. 说了这么多,消息可能还是一个十分飘渺的概念,倒底要怎么用呢?我们来举个例子说明 一下.给系统发消息一般会用到两个函数,一个是 SendMessage,另一个是 PostMessage,这两个 是有点区别的,具体区别作为这次的作品,大家去网上查查.例如我们要退出当前的应用程序, 最简单的方法当然是按一下关闭的那个红 X,如果我们要自己让程序退出,可以给系统发一个 消息,其实那个关闭程序的红 X 也是这么干的.为了演示,首先在上次的对话框中加一个按钮, 修改标题也就是 Caption 为”关闭程序”,ID 号改为 IDC_QUIT_ME 双击为按钮添加以下代码. PostMessage(WM_CLOSE); 编译运行试试,如果没有写错代码就可以用那个按钮退出程序了. 这一节也就写到这里了,可以大家还是有点糊涂,慢慢体会一下就好了.还是不明白可以 来这里提问. 从零开始学 VC 系列教程之 三.串口通信与自定义消息 课程之前:首先请大家确认一下前面两章都已经熟悉,因为一些前面已经介绍过的基础操作在 这里将不再详细说明,如果有什么问题,可以翻看一下前面的两章或者留言提问.本章是基于 PC 机与单片机的串口通信,用到了一个动态链接库和一个自定义消息. 学习目标:掌握 VC 下串口编程式的方法,掌握动态库的静态调用及自定义消息. 课程详解: 1. 参照第一章新建一个基于对话框的 Vc 工程,名称定义为 Eg03. 2. 工程建立后,在对话框上加入一个组合框(ComboBox),ID 号改为 IDC_COMPORT 用于选 择使用 PC 机上的哪一个串口. 在组合框后加入一个按钮,标题(Caption)改为”打开”,ID号改为IDC_BTN_PORTOPEN 用 于打开串口,开始通信. 下面加入一个编程框(EDIT),ID 号改为 IDC_EDIT_RECMSG 用于显示接收到的数据. 在编程框下面再添加一个编程框(EDIT),.ID 号改为 IDC_EDIT_SEDMSG 用于添加要发 送的数据. 然后在这个编程框后加入一个按钮.标题(Caption)为”发送”,ID 号为 IDC_BTN_SEND 最后调整位置及大小如下 3. 添加 Lib 文件.这里介绍的串口通信用的不是 VC 自带的 MSCOMM 控件.原因有两个,一 是顺便介绍一下动态库和自定义消息的用法.二是 MSCOMM 控件使用时数据类型转换 比较复杂,并且使用也不是很方便.当然,以后也会介绍多线程串口通信给大家,我们会在 后面开设一章多线程编程方法,并在那里详细介绍基于多线程的串口通信.这里使用一个 动态库,其实也是别人封装好了的多线程通信,名字是 Pcomm.在工程下载中,给出了三个 文件,分别是 Pcomm.h, Pcomm.lib, Pcomm.dll,现在请大家把这三个文件拷到工程目录,也 就是 Eg03 这个文件夹中.至于什么是动态库,这三个文件倒底是什么作用,我们做完这个 例程后再解释,现在还是先按步就搬,营造一个感性认识.下面添加 Lib 文件到工程.首先点 击[工程](Project),选择下拉式菜单中的[设置](ProjectSettings) 然后会弹出一个对话框,在标签卡中选择[连接] 然后在[对象/模块]中添加 Pcomm.lib,完成后如上面所示,单击[确定]退出.这样,我们就为 Pcomm.dll 这个动态库添加了静态链接,同时,这也就是动态库的静态链接方法,当然,还有一步 就是包含 Pcomm.h 这个头文件.在刚才的步骤中,我们将 Pcomm.Lib 添加到工程,这个文件主 要用于指定 Pcomm.dll 中各个功能函数的入口及地址,Pcomm.Lib 就像一个地图指出目的地 的路标,而真正的函数是在 Pcomm.Dll 中的.当然,为了方便调用,我们还要得到 Pcomm.Dll 中 的函数声明,这些函数声明就在 Pcomm.h 这个头文件中,所以,大家打开 Pcomm.h 这个文件,只 有函数及变量定义,并没有函数过程.下面我们来添加这个文件. 4. 打开左边的[工作空间](WorkSpace)中选择标签[ClassView](这里大家只能看到[Class…] 这一步前两章已经详细介绍过了,大家可以参考.),然后双击[OnInitDialog]就可以打开代 码窗口了,在原有头文件包含后面加入串口头文件引用.输入#include ”Pcomm.h”就可以 了,完成后如下图 这一步我们加入了动态库的函数声明,后面就可以直接使用 Pcomm.Dll 中的函数了.下面我们 来添加事件响应. 单击工作空间中间的标签[ResourceView]( 大家看到的是[Reso…]), 再双击 [IDD_EG03_DIALOG]就可以回到控件编辑状态. 首先为[打开]按钮添加代码.双击[打开]按钮,然后在按钮事件中添加.完成后如下 void CEg03Dlg::OnBtnPortopen() { // TODO: Add your control notification handler code here Port=GetDlgItemInt(IDC_COMPORT); if(SIO_OK!=sio_open(Port)) { MessageBox("串口打开错误"); } else { sio_ioctl(Port,BaudRate,DataBits | StopBits | Parity); sio_cnt_irq(Port,CntIrq,1); } } 其中, sio 开头的变量及函数都是 Pcomm 中的,我们来解释一下. sio_open 是打开某个串口,传 入的参数是串口号,如果我们要打开 COM1,可以用 sio_open(1),返回的参数在 Pcomm 里面定 义了,如果返回 SIO_OK 就表示串口打开没有问题,否则,就是打开串口失败. sio_ioctl 用于设 置通信的相关信息,Port 中串口号, BaudRate 是波特率, DataBits 是数据位数, StopBits 是停止 位数, Parity 是校验. sio_cnt_irq 用于设定中断回调函数.中断回调函数其实前面的 Timer 定时 器里也提到过,在这里,我们设定一个中断回调函数,每当串口接收到指定字节数据时,系统就 会自动调用这个中断回调函数,就像单片机中的串口中断函数一样.这里的回调函数名是 CntIrq,我们将在后面定义.细心的朋友一定会发现, BaudRate,DataBits | StopBits | Parity 这些 都没定义过啊?所以要定义一下这些变量.参照前面的加入文件的方法,在头文件引用下一行 加入以下宏定义 #define BaudRate B57600 //波特率 #define DataBits BIT_8 //数据位 #define Parity P_NONE //效验位 #define StopBits STOP_1 //停止位 完成后如图 下面我们要定义 sio_cnt_irq 一般来说,中断回调函数并不写在类里面,我们添加后如下 ///////////////////////////串口中断回调函数////////////////////////////////// VOID CALLBACK CntIrq(int port) { if(::AfxGetMainWnd()) { if(::AfxGetMainWnd()->m_hWnd) { ::PostMessage(::AfxGetMainWnd()->m_hWnd,WM_PCOMM,0,0); } } } 学习过前面两章我们知道,这个中断回调函数只做了一件事情,就是发送一个 WM_PCOMM 消息到窗口. AfxGetMainWnd()这个函数用于获得主窗口,返回类型是CWnd的指针,主窗体句 柄我们是不知道的,用 AfxGetMainWnd()->m_hWnd 来获得.这样,消息就可到发到主窗体了. 有了这个函数,每当串口接收了数据,就会发一个消息到窗体.WM_PCOMM 这个消息不是系 统的,也不是Pcomm本身的,它是我们自定义的一个消息,怎么定义呢?我们在前面说的宏定义 后面再加入一个定义 #define WM_PCOMM WM_USER+500 //自定义消息 WM_USER 是一个消息地址,这个是系统定义好的,从这个地址开始可以自定义消息, 我们把 WM_PCOMM 定义为 WM_USER+500 也就是说,我们定义的这个消息位于 WM_USER 后面 的偏移 500,当然,这只是个地址,与执行先后无关.这个偏移大家可以自己随便设,不与别的自 定义消息冲突就行了.消息定义好了还要为消息添加关联.首先要定义一个消息响应函数,名 字随便,我们这里取名为 OnPcomm(),双击[工作空间]中的 Ceg03Dlg 就可以打开窗体的文头 件,这里定义了 Ceg03Dlg 这个类,我们在类定义里面添加一个成员函数. afx_msg void OnPcomm(); //这里是我们自定义的消息响应函数 完成后如图 此外,这里还顺便定义了一个变量,就是前面我们用到的 Port 用于记录打开的串口号. public: int Port; 位置就放在 DECLARE_MESSAGE_MAP() 的前面.函数声明就可以了.现在来添加函数体.双 击[OnInitDialog( )],然后在该文件的最后添加一个函数.写成如下形式. void CEg03Dlg::OnPcomm() { char buf[200]; int end=sio_read(Port,buf,100); if(end) { CString a,b=""; GetDlgItemText(IDC_EDIT_RECMSG,b); buf[end]=0; for(int i=0;i='A' && a.GetAt(i)<='Z') b=(a.GetAt(i)-55)*16; //判断填入的是字母还 是数字,并把字符转换成十六进制数 else if(a.GetAt(i)>='0' && a.GetAt(i)<='9') b=(a.GetAt(i)-0x30)*16; if(a.GetAt(i+1)>='A' && a.GetAt(i+1)<='Z') b+=(a.GetAt(i+1)-55); else if(a.GetAt(i+1)>='0' && a.GetAt(i+1)<='9') b+=(a.GetAt(i+1)-0x30); sio_putch(Port,b); //发送 } } 这一段主要是把获得的编辑框内的字串转换成十六进制的数字,转换一个发送一个.Cstring 类型以前已经提起来,应该际上是一个类, MakeUpper 是一个成员函数,用于将字符串全部转 成大写.GetAt 也是一个成员函数,可以取出字符串中任意下标的字符. sio_putch 用于发送一 个字符. 填写待发送数据的时候要注意,每两位中间空格一下.填入的是十六进制数据. 下面再来总结一下静态方式调用动态库的方法. 1. 拷贝 Lib,H 头文件到工程路径 2. 在工程->设置中加入 Lib 模块. 3. 加入.h 头文件,用于函数声明 4. 将 Dll 文件拷入到工程目标路径中 总结一下自定义消息方法: 1. 用#define WM_NAME WM_USER+1 定义一个自定义消息,名称随便.一般用 WM 开 头.WM_USER+1中的1那个数字是自己定的,一个消息就无所谓了,喜欢多少都行,如果要 定义很多个消息,不要冲突就行了 2. 在类定义里面声明一个消息响应函数,写成 afx_msg void FunctionName();格式. 3. 添加一个消息影射 ON_MESSAGE(WM_NAME, FunctionName)注意这句后面是没有分 号的. 4. 写好 FunctionName 的函数. 很简单的四步就行了. 从零开始学 VC 系列教程之 四.并口控制与类的使用 课程之前:这一章让大家等很久了,一直在考虑并口操作应该怎么写.其实这是一个很简单的 课题,因为是基于成熟操作类的基础上的,我们主要是学习如果使用网上查到的资料,这一点 很关键,只有学会了利用各种资源,才能自主学习取得进步,同时,也可以有效的提高开发效率. 学习目标:掌握 VC 下并口程序的方法及类的使用. 课程详解: 参照第一章新建一个基于对话框的 Vc 工程,名称定义为 Eg04. 类型选基于对话框. 在本例中,我们要用到一些资源,现列出如下 WinIo.sys WinIo.dll 这是一个动态库,与 WinIo.sys 完成同并口的连接. WinIo.lib 这是为静态调用提供的引入库文件.虽然引入库文件和静态库文件都是以 Lib 为扩 展名的,但实际上有本质的区别. WinIo.h 这是 IO 操作的头文件. ParallelPort.cpp 这是一个并口操作类,用于同 WinIo 接口,并定义了相关的操作函数.虽然这个 类用于同WinIo 的操作接口,但这个类并不是从WinIO 派生的,只是一个独立的自定义类.至于 类的自定义,以前我们也提到过. ParallelPort.h 这是并口类的头文件. 这几个文件是可以从网上下载到的,并不是我做的,所以这个动态库大家就不用问我要源码了, 因为我也没有. 首先,把 WinIo.lib WinIo.h ParallelPort.cpp ParallelPort.h 拷贝到工程目录中,后面马上就要用到. 再把 WinIo.Dll WinIo.sys 拷贝到工程目录的 Debug 目录中,以后工程发布后,这两个文件要跟 随工程一起. 下面添加引入库到系统中,点击[工程]->[设置]在弹出的对话框中选择[连接]标签,然后在[对 象/库模块]中加入 WinIo.lib 完成后如下图. 然后加入并口操作类的头文件.一般来说,网上可以下载到的源码都是以类的形式给出 的,VC 的好处也在于可以把一个操作封装成类,以便在不同的工程中调用.如图,双击类管理器 中的 Ceg04Dlg 在弹出的文件中加入#include "ParallelPort.h" //添加并口类头文件 同时,还要定义一个类变量,用于并口类的操作. 在刚打开的类定义文件中找到 CEg04Dlg 类, 并加入一个类变量 m_Port,完成后如下图所示 为了操行方便,把并口操作类也加入到工程中,点击[FileView]标签,标签位置在类管理类下面, 见上图中的[FileVi…]然后在[Source Files]中加入 ParallelPort.cpp 在[Header Files]中加入 ParallelPort.h 这样,就可以在类管理中看到我们所用到的并口操作类了,同时也可以方便的查 阅类函及成员变量. 下面在界面中加入六个按钮和两个文本编辑框,方法前面已经介绍多次.完成后如下图 其中,各控件 ID 如下 上面的编辑框 IDC_EDIT_READ 用于显示收到的数据 下面的编辑框 IDC_EDIT_WRITE 用于填写要发送的数据 读数据口 IDC_BTN_DREAD 用于读取并口数据总线 写数据口 IDC_BTN_DWRITE 用于写并口数据口数据 读控制口 IDC_BTN_CREAD 用于读取并口控制总线 写控制口 IDC_BTN_CWRITE 用于写并口控制口数据 读状态口 IDC_BTN_SREAD 用于读并口状态总线 并口流水灯 IDC_BTN_LED 用于在数据线上输出流水灯效果 先要介绍一下并口,以便理解接下来的操作是什么意思.以下一些关于并口的介绍,摘自互联 网. 并口 SPP 模式寄存器定义 数据寄存器(基地址) 位 引脚:D-sub 信号名 信号源 是否在连接器 处倒相 引脚:Centronics 0 2 数据位 0 PC 否 2 1 3 数据位 1 PC 否 3 2 4 数据位 2 PC 否 4 3 5 数据位 3 PC 否 5 4 6 数据位 4 PC 否 6 5 7 数据位 5 PC 否 7 6 8 数据位 6 PC 否 8 7 9 数据位 7 PC 否 9 注:控制寄存器的第 5 位控制数据位是否能够输出。 状态寄存器(基地址+1) 位 引脚:D-sub 信号名 信号源 是否在连接器 处倒相 引脚: Centronics 3 15 nError 外设 否 32 4 13 Select 外设 否 13 5 12 Paper Out 外设 否 12 6 10 nAck 外设 否 10 7 11 Busy 外设 是 11 注:1,2 位未定义。 控制寄存器(基地址+2) 位 引脚:D-sub 信号名 信号源 是否在连接 器处倒相 引脚: Centronics 0 1 nStrobe PC 是 1 1 14 nAutoLF PC 是 14 2 16 nInit PC 否 31 3 17 nSelectIn PC 是 36 注: 连接器中没有提供的附加位: 4:4:中断启用,此位为 1 时,IRQ 从 nAck 送往系统的中断控制器;为 0 时,IRQ 不送往中断控制器。 5:5:双向控制端口的方向控制位,此位为 0 时,输出启动;为 1 时,不能输出; 控制端口可以读取外部逻辑电平。 6、7:未定义。 学过单片机或相关知识的应该可以看懂表格中的意思了,上面罗列的是除去电源及地线后可 用到的 IO 口,数据口 8 位,由状态寄存器第五位决定能否输出,状态口共 5 位,控制口共 4 位. 一般我们只用到数据口来传输数据.一般来说,并口基地址是 0x378,也就是数据寄存器的地址, 在并口类中默认.明白了这些以后,就可以对并口进行相应的操作了. 接下来要在程序初始化时初始化并口,在前面的操作中,定义了一个类变量 m_Port,这个类变 量在后面我们将多次用到.首先展开类管理器中的 CEg04Dlg 双击 OnInitDialog()在窗口初始 化中加入以下代码. //在这里初始化并口 if(m_Port.InitPort()==TRUE) { SetDlgItemText(IDC_EDIT_READ,"并口初始化成功,并口地址:0x378"); } else { SetDlgItemText(IDC_EDIT_READ,"并口初始化失败!"); } 完成后如下图所示 m_Port.InitPort()是并口操作类中的并口初始化函数,传入参数是并口地址,由于类定义时默认 了0x378为并口地址,这里可以不用传参数了.如果初始化成功,传回布尔变量TRUE,运行一下, 应该可以看到并口初始化成功的信息. 接下来为读数据口添加代码. 双击[读数据口]按钮,在弹出的代码中加入程序如下 void CEg04Dlg::OnBtnDread() { // TODO: Add your control notification handler code here BYTE nPortData=m_Port.ReadData(); //从并口读到数据 CString a,b; GetDlgItemText(IDC_EDIT_READ,b); //获取编辑框中原有的文本 a.Format("\r\n 读到数据口数据:%2.2X",nPortData); b+=a; SetDlgItemText(IDC_EDIT_READ,b); //写入文本到编辑框 //以下用于将滑块自动移到最后一行 unsigned char nLine=((CEdit*)GetDlgItem(IDC_EDIT_READ))->GetLineCount(); ((CEdit*)GetDlgItem(IDC_EDIT_READ))->GetLineCount(); ((CEdit*)GetDlgItem(IDC_EDIT_READ))->LineScroll(nLine); } 完成后如图所示(图 8) 这样就可以从并口读到数据了.同样的,可以添加控制口,状态口的按钮响应代码,用于读到控 制口及状态口, 方法相同, 这里就不再详叙了. 不同的是读控制口时, 用到的函数是 m_Port.ReadCtrl();读状态口用到的函数是 m_Port.ReadState(); 再有一个问题要说一下,编辑框默认状态下是单行的,也就是说,我们的换行操作是没有用的. 可以鼠标右击编辑框,在弹出的菜单中选[属性],并修改其属性如下图所示(图 7) 下面来添加写数据操作.在写数据操作中,我们要换一种方法来读写文本编辑框了. 首先鼠标右击下面的写操作编辑框,在弹出的菜单中选择[建立类向导],英文版的 VC 请根据 翻译. 然后会弹出一个类向导对话框, 选择[MemberVariables]标签卡后显示如下. ,在 ID 列表中,我们可以看到窗口中为控件添加的 ID 号,双击[IDC_EDIT_WRITE]会弹出另一 个对话框,将各框中的内容修改如下图 第一个框中的 m_WriteEdit 是我们为 IDC_EDIT_WRITE 这个编辑器定义的一个类变量名,这 个类变量会自动与编辑框 IDC_EDIT_WRITE 关联,在以后的操作中用到. 确定后双击[写数据口]按钮,为写数据口按钮添加代码,完成后如下 void CEg04Dlg::OnBtnDwrite() { // TODO: Add your control notification handler code here CString a; unsigned char b=0; m_WriteEdit.GetWindowText(a); //获取编辑框中文本 a.MakeUpper();//全部转换为大写 if(a=="") { MessageBox("发送内容不能为空!"); return; } for(unsigned char i=0;ia.GetLength()) { MessageBox("数据格式填写不正确!"); return; } if(a.GetAt(i)>='A' && a.GetAt(i)<='Z') b=(a.GetAt(i)-55)*16; //判断填入的是字母还 是数字,并把字符转换成十六进制数 else if(a.GetAt(i)>='0' && a.GetAt(i)<='9') b=(a.GetAt(i)-0x30)*16; else { MessageBox("数据格式填写不正确!"); return; } if(a.GetAt(i+1)>='A' && a.GetAt(i+1)<='Z') b+=(a.GetAt(i+1)-55); else if(a.GetAt(i+1)>='0' && a.GetAt(i+1)<='9') b+=(a.GetAt(i+1)-0x30); else { MessageBox("数据格式填写不正确!"); return; } m_Port.WriteData(b); } } 这里可能有点长,其实与第三章的发送是一样的,增加了一点容错处理而已.在这里,我们用 m_WriteEdit.GetWindowText(a);来获取编辑框中文本, m_WriteEdit 是开始时候在类向导中定 义的变量,GetWindowText 是一个窗口函数,因为文本编辑框也认为是窗口,所以窗口操作函数 是通用的,由于 m_WriteEdit 与文本框 IDC_EDIT_WRITE 关联,获取的是文本框中的内容.同 样,由于 m_WriteEdit 是 CEdit 变量,于是就可以有所有 CEdit 的所有属性,这与结构变量有点 相似,大家可以慢慢体会.其余口的操作类似,不再重复说了. 这个并口类还有很多功能函数,这里也清一下. #define PIN_STROBE 1 #define PIN_AUTO 14 #define PIN_D0 2 #define PIN_D1 3 #define PIN_D2 4 #define PIN_D3 5 #define PIN_D4 6 #define PIN_D5 7 #define PIN_D6 8 #define PIN_D7 9 #define PIN_ERROR 15 #define PIN_INIT 16 #define PIN_SELIN 17 #define PIN_ACK 10 #define PIN_BUSY 11 #define PIN_PE 12 #define PIN_SLCT 13 这些是对 IO 口的定义,用于单个 IO 口的操作,操作函数有以下几个 BOOL SetPinLogic(int nPin,BOOL bLogic); //设置指定引脚,bLogic=1 高电平,bLogic=0 低电平 BOOL GetPinLogic(int nPin);//得到指定引脚的电平 BOOL SetPinL(int nPin);//设置指定引脚为低电平 BOOL SetPinH(int nPin);//设置指定引脚为高电平 以上些都是这个并口类的函数了,有了这些,我想大家也就可以随便做点东西了,记得曾有过 用并口直接驱动 LCD 的例子,建议也试一下. 最后一个按钮是流水灯,其实这个对于大家来说并不难,我们也提供一个例子.双击[并口流水 灯]添加流水灯代码.IO 口低电平有效. void CEg04Dlg::OnBtnLed() { // TODO: Add your control notification handler code here unsigned char nDataIO=0x01; CString a,b; unsigned char nCData=m_Port.ReadCtrl(); //读控制寄存器数据 nCData&=0xDF; //保证第五位为低,使能 IO 输出 m_Port.WriteCtrl(nCData); //写控制口数据 for(char i=0;i<8;i++) { m_Port.WriteData(0xff-nDataIO); GetDlgItemText(IDC_EDIT_READ,b); //获取编辑框中原有的文本 a.Format("\r\n 流水灯数据[%d]:%2.2X",i+1,0xff-nDataIO); b+=a; SetDlgItemText(IDC_EDIT_READ,b); //写入文本到编辑框 //以下用于将滑块自动移到最后一行 unsigned char nLine=((CEdit*)GetDlgItem(IDC_EDIT_READ))->GetLineCount(); ((CEdit*)GetDlgItem(IDC_EDIT_READ))->GetLineCount(); ((CEdit*)GetDlgItem(IDC_EDIT_READ))->LineScroll(nLine); nDataIO<<=1; Sleep(200); //延时 200ms } } 接上硬件可以看到,按一下出现一次流水灯效果,由于不方便按硬件,就没有试过了. 这一章就先到这里了.建议大家自己想个作品来试一下,同时也希望大家分享一下自己的学习 及实验感想,把作品也发上来庆祝一下. 从零开始学VC系列教程 五.消息机制与自定义消息 题外话:我们一再重复的讲Windows的消息,因为这的确是一个关键,会用,并且用好是非常必要的。 这一章我们主要介绍一下消息机制及消息传输,做几个小小的实验,让大家在概念上明白怎么用。 本节内容:学会消息发送接收及自定义消息 学习目的: 撑握消息机制的概念,学会基于消息的操作. 这一章我们要做的例子看起来很简单,只是实现的方式不一样。按[开始]时,在文本框内显示当前的 鼠标坐标,按[结束]后,显示一个结束信息,按[关闭]则退出程序. 1.新建工程.首先建一个基于 MFC 对话框的工程,名字为 Eg05。前面四章我们已经介绍过很多次建立工程 的方法了,这里不再重复了,如果还有不明白的,可以看一下以前的记录,或者给我留言。 2.在对话框上放一个 EditBox,改 ID 称为 IDC_EDIT_SHOW 然后再添加三个按钮,分别设置标题为[开始],[结 束],[关闭]。设 置 ID 分别为 IDC_BTN_STAR,IDC_BTN_OVER,IDC_BTN_CLOSE。通过前面几章的学习, 我们知道 ID 号大家可以自己随便设定,当然最好是接近控件的作用简称或接近控件名的简称。完成后如下 图所示。 首先我们来为[关闭]按钮添加响应,双击[关闭]按钮,添加响应程序。只有一行,呵呵。完成后如下图所示: void CEg05Dlg::OnBtnClose() { // TODO: Add your control notification handler code here PostMessage(WM_CLOSE); } 编译运行,按[关闭]就可以看到程序退出了,晕死,是不是太简单了?下面我们来说一下为什么是这样。 在这里,我们用 PostMessage 函数发送了一个名为 WM_CLOSE 的消息,这是一个系统消息,用于关闭程 序。查一查 MSDN,可以看到这个函数的一些说明。原文放在下面,方便没有下载 MSDN 的朋友看看. CWnd::PostMessage BOOL PostMessage( UINT message, WPARAM wParam = 0, LPARAM lParam = 0 ); Return Value Nonzero if the message is posted; otherwise 0. Parameters message Specifies the message to be posted. wParam Specifies additional message information. The content of this parameter depends on the message being posted. lParam Specifies additional message information. The content of this parameter depends on the message being posted. Remarks Places a message in the window’s message queue and then returns without waiting for the corresponding window to process the message. Messages in a message 从原型我们可以看到,其实这个函数还有两个参数,分别为 wParam 和 lParam,分别默认为 0 了。这两个 参数是什么意思呢? wParam 和 lParam 这两个是 Win16 系统遗留下来的产物,在 Win16API 中 WndProc 有两个参数: 一个是 WORD 类型的 16 位整型变量;另一个是 LONG 类型的 32 位整型变量。因此根据匈牙利命名法, 16 位的变量就被命名为 wParam, 32 位的变量就被命名为 lParam。 到了 Win32API 中,原来的 16 位变量也被扩展为 32 位,因此此时 wParam 和 lParam 的大小完全相同。 在 Win32API 的早期,为了保证和 Win16API 的代码可移植性 MS 定义了 WPARAM 和 LPARAM 两个宏。 当时保留了 w 前缀的原因一方面是由于 WPARAM 宏也已 W 开头,还有也因为要提醒程序员注意到可移植 性,当然到了现在 Win16 早已退出历史舞台,这个前缀也就约定俗成的沿用下来了。WPARAM 和 LPARAM 本质上没有什么区别:都是 32 位数, 但是区别也还是有的:除了上面说的关于 16 位的的历史问题外, MICROSOFT 在使用时两种参数分别代表不同的含义和内容,WPARAM 常常代表一些控件的 ID 或者高位 低位组合起来分别表示鼠标的位置,如果消息的发送者需要将某种结构的指针或者是某种类型的句柄时, 习惯上用 LPARAM 来传递,可以参考各种控件的通知消息。 在 WM_CLSOE 中,我们使用了不带参数的调用,其实这个消息本来也没有参数,所以后面的两个参数就 默认了。 为了更好的说明这两个参数,下面我们来添加本例程的另一个功能,显示鼠标的坐标。其实显示坐标不用 这么麻烦,在这里是为了更好的展示消息的参数。首先添加一个类变量,从类管理器中双击[CEg05Dlg]这 个类,会打开这个类的头文件,在类定义中添加一个布尔变量,名称为 m_bMouseMoveDisp 完成后如下 图所示 如上图,我们添加了两行代码。 public: BOOL m_bMouseMoveDisp; 然后要对这个变量初始化,双击类管理器中的[OnInitDialog]函数,可以打开对话框的初始化函数,我们在 后面添加一行代码:m_bMouseMoveDisp=false; //这里就是变量初始化 下面要添加一个函数,选择[CEg05Dlg],点右键会弹出一个菜单,如图所示 选择[Add Virtual Function…]弹出一个对话框 然后在左边的列表中双击[PreTranslateMessage],可以看到这个函数添加到右边了,然后选择这个函数, 双击或点最后一个按钮[编辑存在]就可以添加到类中了。完成后如图: 当然上面的图中,我们还添加了一些响应代码, BOOL CEg05Dlg::PreTranslateMessage(MSG* pMsg) { // TODO: Add your specialized code here and/or call the base class if(pMsg->message==WM_MOUSEMOVE && m_bMouseMoveDisp==TRUE) { CString a; a.Format("X 坐标:%d,Y 坐标:%d",pMsg->lParam&0x0000ffff,(pMsg->lParam&0xffff0000)>>16); SetDlgItemText(IDC_EDIT_SHOW,a); } return CDialog::PreTranslateMessage(pMsg); } 重载这个函数可以捕获本窗体的所有经过消息队列的消息,在 win32 程序中,关于消息有两 种传递方式: a.MFC 消息,MFC 会把所有的消息一条条放到一个 AFX_MSGMAP_ENTRY 结构中,形成一个 数组,该数组存放了所有的消息和与它们相关的参数。也可以说是放到消息队列里去。 b. 采用 SendMessage()或其他类似的方式向窗口直接发送的而不经过消息队列的消息。 这两种方式中只有第一种(穿过消息队列的消息)才受 PreTranslateMessage()影响, 第二种消息并不会理睬 PreTranslateMessage()的存在。 通过函数参数可以看到,传进来的是一个消息结构,我们查 MSDN 可以得到消息结构的原型: typedef struct tagMSG { // msg HWND hwnd; UINT message; WPARAM wParam; LPARAM lParam; DWORD time; POINT pt; } MSG; 很明显,可以从 message 得到消息名称,所以我们写成 if(pMsg->message==WM_MOUSEMOVE && m_bMouseMoveDisp==TRUE)在这里,m_bMouseMoveDisp 是前面一步我们定义的类变量。当我 们移动鼠标时,系统会发出一个名为 WM_ MOUSEMOVE 的消息到所有窗口,这个消息会经 过消息队列,所以可以在这里捕获到,这个消息带两个参数,分别为 fwKeys = wParam; // key flags xPos = LOWORD(lParam); // horizontal position of cursor yPos = HIWORD(lParam); // vertical position of cursor 可以看到,lParam 这个参数包括了光标所在处的坐标,而 wParam 则包含了鼠标键的事件, 包括 MK_CONTROL Set if the ctrl key is down. MK_LBUTTON Set if the left mouse button is down. MK_MBUTTON Set if the middle mouse button is down. MK_RBUTTON Set if the right mouse button is down. MK_SHIFT Set if the shift key is down. 所在我们用 pMsg->lParam&0x0000ffff 取得参数 lParam 的低字节 16 位为 X 坐标, 用,pMsg->lParam&0xffff0000)>>16 的高 16 位数据,并移至低位作为 Y 坐标,然后嘛,大 家都看得懂了,显示在 IDC_EDIT_SHOW 中。当然,这里还有一个条件,就是 m_bMouseMoveDisp 要为 TRUE,这个就是按钮的事了,我们双击[开始]按钮,添加一行代 码。bMouseMoveDisp=TRUE;完成后编译运行,点[开始]就可以看到框内显示了鼠标在窗 体中的坐标了。实时效果如下: 要关闭坐标显示就很简单了,把 m_bMouseMoveDisp 置为 FALSE 就行了,所以大家可以 为[结束]按钮添加一行代码就实现了。这里我们并不这样做,为了演示我们来绕一个大弯解 决这个问题。按下这个按钮要发出一个消息 WM_DISMOUSEDISP。很明显,这个消息是 不存在的,我们自定义一个。双击[CEg05Dialolg],在前面添加一个定义。完成后如下图所 示: WM_USER 是 windows 自定义消息.一般一股是+几后再用。 如:#define WM_SHIT WM_USER+1 也就是说 WM_USER 是个分水岭,一般系统消息到不了这个数.在这里,我们自定义了一个 消息,名为 WM_DISMOUSEDISP 下面双击[结束]按钮,添加响应代码 PostMessage(WM_DISMOUSEDISP);这里我们也不传 参数了。从上面的分析大家可以看到,SendMessage 也可以发送消息,但消息并不经过消 息队列。这两个函数有点区别的。PostMessage 只负责将消息放到消息队列中,不确定何 时及是否处理 SendMessage 要等到受到消息处理的返回码(DWord 类型)后才继续 PostMessage 执行后马上返回 SendMessage 必须等到消息被处理后才会返回。 也就是说,我们发送的 WM_DISMOUSEDISP 可以通过 PreTranslateMessage 捕获到了。下 面怎么写呢?想必大家也就明白了。以下给出响应代码。 BOOL CEg05Dlg::PreTranslateMessage(MSG* pMsg) { // TODO: Add your specialized code here and/or call the base class if(pMsg->message==WM_MOUSEMOVE && m_bMouseMoveDisp==TRUE) { CString a; a.Format("X 坐标:%d,Y 坐标:%d",pMsg->lParam&0x0000ffff,(pMsg->lParam&0xffff0000)>>16); SetDlgItemText(IDC_EDIT_SHOW,a); } if(pMsg->message==WM_DISMOUSEDISP && m_bMouseMoveDisp==TRUE) { m_bMouseMoveDisp=false; SetDlgItemText(IDC_EDIT_SHOW,"关闭鼠标坐标显示!"); } return CDialog::PreTranslateMessage(pMsg); } 编译运行看看,是不是就可以了。 这一章先到这里了,在上一章中,我们提到过自定义消息及消息影射到函数,大家可以再回 头看一下,加深理解。 有不明白的可以到 Ouravr 或我的论坛留言 http://bbs.bqmcu.com.cn
还剩53页未读

继续阅读

下载pdf到电脑,查找使用更方便

pdf的实际排版效果,会与网站的显示效果略有不同!!

需要 15 金币 [ 分享pdf获得金币 ] 4 人已下载

下载pdf

pdf贡献者

wzj7531

贡献于2012-01-01

下载需要 15 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!
下载pdf