本文不会对C++语法进行讲解,本文假设读者已经很熟悉C++语法了,若读者对C++语法不熟悉,请参阅本人所著《C++语法详解》一书。
本文对窗口的重绘原理、有效区域、无效区域、鼠标和键盘消息作了细致全面的讲解,本文图文并茂讲解详细细致,由浅入深,内容较为全面。
本文使用VS2005、VS2010、VS2015编译器进行讲解,
本文内容完全属于个人见解与参考文献的作者无关,限于水平有限,其中难免有误解之处,望指出更正。
声明:禁止抄袭、复印、转载本文,本文作者拥有完全版权。
主要参考文献:
1、深入浅出MFC(第2版) 侯俊杰 著 华中科技大学出版社 出版日期不祥 2、VC++深入详解 孙鑫 余安萍 编著 电子工业出版社 2006年6月
3、windows程序设计(第5版 珍藏版) [美]Charles Petzold著 方敏 张胜 梁路平 赵勇等 译 清华大学出版社 2010年9月
4、Visual C++2013入门经典(第7版) [美] Ivor Horton著 李周芳 江凌 译 清华大学出版社 2015年1月 5、windows图形编程 [美] Feng Yuan著 英宇工作室 译 机械工业出版社 2002年4月
6、MFC windows程序设计(第2版) [美] Jeff Prosise著 北京博彥科技发展有限责任公司 译 清华大学出版社 2007年5月
7、Visual C++.NET宝典 [美]Tom Archer, Andrew Whitechapel著 马云 叶喜涛 张毅峰 等译 电子工业出版社2003年2月
8、Visual C++ 2010开发权威指南 尹成 颜成钢 编著 人民邮电出版社 2010年8月
第4章 窗口重绘原理
本章需要了解第1部分的内容。
为方便测试,可使用以下方法让MFC程序在控制台输出信息 1、包含头文件 2、在需要使用控制台输出的地方使用以下语句 AllocConsole(); //开启控制台,注意检查返回值 _cprintf(\"ddd\"); //输出信息到控制台,该函数与C语言的printf类似。 FreeConsole(); //关闭控制台。 3、使用以上方法输出中文会出现乱码。 4.1 消息整体流程 一、MFC程序消息整体流程 1、系统会为每一个应用程序创建一个消息队列,产生的消息会被依次放入消息队列中。 2、进队与非进队消息:进队消息是由系统放入到应用程序的消息队列中的消息,不进队的消息是指由系统直接发送给窗口而未进入消息队列的消息,无论是进队还是不进队消息,最终都是由系统的窗口过程函数对消息进行处理的。 3、消息处理原理:当应用程序产生一个事件(比如按下鼠标)时,操作系统会感知到这一事件,然后把这个事件包装成一个消息,并投递到应用程序的消息队列中,然后应用程序在无限循环(即消息循环)中对消息队列进行检索,若检索到消息,则从消息队列中取出消息,并由操作系统把消息传递给窗口过程函数进行处理。消息在被取出之前会一直处于消息队列中等待,应用程序的绝大部分操作就是在等待下一个消息的到达,若无消息到达,则应用程序什么也不会做。 4、在MFC中,对消息的处理被分为三大部分,即:消息循环、消息映射、消息路由。每一部分都被进行了单独的封装,消息循环主要就是一个无限循环,即SDK中的 while(GetMessage(&m,NULL,0,0))就是消息循环,消息循环主要的任务就是循环读取消息队列中的消息,并把读取到的消息传递给过程函数处理,因此消息循环相对比较简单,MFC由虚函数CWndThread::Run实现消息循环,消息映射和消息路由则更复杂。 5、MFC实现消息处理的步骤: ①、MFC消息的执行过程与SDK程序是相同的,因此读者需了解第1章的内容。 ②、MFC程序的窗口由入口函数创建 ③、然后在入口函数中调用虚函数CWndThread::Run(实际调用的是子类重定义的Run函数)而进入消息循环(其实就是一个无限循环语句),并对消息队列进行检索。 ④、然后使用GetMessage函数从消息队列中取出消息 ⑤、再使用DispatchMessage函数把消息传递给窗口过程函数进行处理 ⑥、注:Run函数的实现与SDK函数的步骤是一致的,因Run函数相对较简单,读者可自行查看其源码,CWinThread::Run函数位于thrdcore.cpp文件中,CWinApp::Run位于appcore.cpp文件中,源代码中会用到PumpMessage函数,该函数也位于thrdcore.cpp文件中。 ⑦、MFC具体执行流程见下图 GetMessage DispatchMessage 操作系统 消息队列 消息循环 输入的消息 过程函数 WM_PAINT 调用Run函数处理消息 WM_KEYDOWN 进入消息循环 …… 应用程序 图XXX:MFC消息流程 二、与消息处理有关的函数及结构体、类 1、MSG结构体、GetMessage、TranslateMessage、DispatchMessage函数详见第1章 2、::PeekMessage函数原型如下: BOOL PeekMessage(LPMSG lpMsg, HWND hWnd, UNIT wMsgFilterMin, UNIT wMsgFilterMax, UNIT wRemoveMsg ①、作用:PeekMessage函数的功能与GetMessage相同,即都是从消息队列中获取消息。 ②、各参数的函数与GetMessage函数相同, ③、最后一个参数wRemoveMsg可取以下值 PM_NOREMOVE(0x0000):表示使用PeekMessage获取消息之后,不把消息从消息队 列中移除 PM_REMOVE(0x0001):表示使用PeekMessage获取消息之后,把消息从消息队列中移除 ④、返回值:若有消息可用则返回非零值,若无消息可用则返回零值。注意,该函数检索到WM_QUIT消息不会返回零值,这是与GetMessage函数不同的地方。 ⑤、该函数通常不会从消息队列中删除WM_PAINT消息,但是,若WM_PAINT消息的更新区域为NULL,则该函数会将该消息从消息队列中删除。 三、窗口创建与销毁时的消息流程 因MFC程序的消息流程与SDK程序是相同的,它们的区别仅在与MFC进行了封装,但原理是相同的。为讲解方便,以SDK程序为例进行讲解 1、创建窗口时发送的消息流程 ①、当应用程序使用CreateWindowEx创建窗口时,会发送一条WM_CREATE消息,过程函数在创建窗口之后但在窗口变为可见之前接收到此消息。 ②、当调用ShowWindow函数时,会发送WM_SIZE和WM_SHOWWINDOW消息。 ③、当调用UpdateWindow函数时,会发送WM_PAINT消息(此消息详见后文)。 ④、在过程函数处理某一消息的过程中,程序不会被其他消息突然中断。 2、关闭窗口时发送的消息流程: ①、当用户点击窗口上的关闭按钮时,系统首先发送一条WM_CLOSE消息, ②、若程序未对WM_CLOSE消息进行处理, 则系统把此消息传递给DefWindowPoc函数,该函数会调用DestroyWindow函数 来处理WM_CLOSE消息, DestroyWindow函数首先销毁窗口,然后向过程函数发送WM_DESTROY消息, 注意:此时窗口虽销毁,但程序并未结束。 WM_DESTROY消息首先发送到被销毁的窗口,然后发送到子窗口(若存在的话)。 在子窗口被销毁之后,DefWindowPoc函数接着发送WM_NCDESTROY消息。 相比之下WM_DESTROY消息发送在子窗口被销毁之前,而WM_NCDESTROY 发送在子窗口被销毁之后。 若不存在子窗口,则WM_NCDESTROY消息发送在WM_DESTROY消息之后, 也就是说WM_NCDESTROY是窗口销毁之前发送的最后一条消息。 调用DestroyWindow函数后应注意::GetMessage函数的第二个形参hWnd将变为 无效,因此::GetMessage会发生错误,从而返回−1,不会再返回0,消息循环由此限入死循环,因此消息循环中需对::GetMessage返回的值−1进行处理。 若程序员想在用户点击窗口关闭按钮时弹出一个对话框让用户选择是否关闭窗 口,则应在WM_CLOSE消息内进行处理。 ③、在应用程序的过程函数中必须调用PostQuitMessage函数处理WM_DESTROY消息,该函数会向消息队列中投递一条WM_QUIT消息,然后返回,由此可见,若顶层窗口未调用PostQuitMessage函数,则不会发送WM_QUIT消息,因此消息循环永不会停止,程序也不会结束。 ④、当GetMessage函数接收到WM_QUIT消息后返回值0,结束消息循环,接着执行其后的语句,直至程序结束。PostQuitMessage函数的实参值将作为WM_QUIT消息的wParam参数,此值最终一般会用做入口函数的返回值。 3、窗口类与窗口的关系 窗口类对象(即CWnd及其子类所创建的对象)和窗口是两个不同的概念,它们的关系类似于函数与在函数使用new分配的内存空间,即窗口类对象和窗口的生命期是不一致的,窗口是一种资源,窗口仅仅是经过窗口类对象创建的而已,但是窗口被销毁了,并不表示窗口类对象的生命期结束,相反窗口类对象的生命期结束,也并不表示窗口被销毁(通常这种情形应销毁窗口)。窗口和窗口类对象之间的关系,通过窗口类之中的成员变量m_hWnd相关联,也就是说m_hWnd保存有窗口的句柄。 4.2 WM_PAINT消息与窗口重绘 同理,以SDK程序讲解其原理,MFC程序只是进行了封装,原理是相同的。 一、更新区域、无效区域 1、区域的概念:区域就是一块空间,该空间可以是矩形、圆形等。区域具有以下特点: 裁剪性:使用区域,可以把图形绘制限制在客户区的某一个特定部分,即超过区域 范围的图形会被裁剪掉,也就是说区域具有裁剪特性,常见的区域就是矩形区域。 可叠加性:多个区域可以使用并、交、差等集合运算叠加为一个区域。 区域是GDI对象:与画笔、画刷等相同,区域也是GDI对象,本文暂时只需了解区 域的这些特性,具体内容详见后文。 2、更新区域和无效区域:无效的或过时的且需要被重绘的那部分区域被称为“更新区域”。 3、更新区域和无效区域的区别:窗口无效不一定需要重绘,比如窗口被另一窗口挡住的区域,此时不一定需要重绘。因此只有在窗口无效且该区域需要被重绘时才是更新区域,重绘的区域可以小于或大于无效区域,此时更新区域的大小不等于无效区域,大多数情况下,更新区域被设置为无效区域,因此大多数情况下二者是等同的。 4、有效区域:常常把清除窗口的更新区域称为使窗口的区域有效(即有效区域)。 5、窗口需要更新的情形有: 窗口刚创建时,此时整个客户区都是需要更新的, 窗口从无到有、改变尺寸、最小化后再恢复、此时更新区域为整个客户区。 被其他窗口遮档然后需要显示的部分 二、窗口重绘算法 1、窗口重绘需要解决以什么方式重绘、什么时候需要重绘、在程序的什么地方重绘、重绘的范围有多大等问题。下面一一进行讲解。 2、窗口重绘的方法:通常使用的是擦除再重新绘制策略,而不使用保存数据再恢复的策略,因为图形编程需要保存的数据非常多。具体步骤为:当窗口需要重绘时,窗口中的所有图形都会被擦除,然后在窗口中再重新进行绘制,这个过程对用户而言是透明的,给用户的感觉就是窗口中的图形始终保持在窗口中。但在以下情形windows总是会保存被覆盖部分的显示内容,然后再恢复:鼠标指针在客户区内移动,在客户区内拖动图标。 3、在何处何时重绘:当客户区的内容无效且需要更新时,向过程函数发送WM_PAINT(窗口更新)消息,通知过程函数对窗口进行重绘,以恢复客户区的内容,而非客户区的重绘工作则是由windows负责的。因此若要使客户区的内容始终被显示,就应把该内容放到过程函数中响应WM_PAINT的代码中,也就是说未在WM_PAINT代码中的内容,不会被重绘。此处只讲解了windows是怎样进行窗口重绘的,发送WM_PAINT消息除了此处讲的情形之外,还有其他的情形,详见后文。 4、重绘的范围:一般情况下系统不需要将整个窗口的客户区都进行重绘,重绘时只需绘制窗口的无效部分即可(最常见的就是矩形区域),因此当系统确定窗口需要更新时,它将更新区域的尺寸设置为窗口的无效部分,系统内部为每一个窗口都保存有一个无效矩形的数据 结构(即PAINTSTRUCT结构体)。注意:设置更新区域不会立即导致应用程序绘制(因为重绘的步骤是在WM_PAINT消息内部处理的),还应注意,窗口无效不等于窗口就立即需要更新。 5、注:窗口大小变化时是否重绘,还取决于WNDCLASSEX结构体的style成员,是否设置为值CS_HREDRAW和CS_VREDRAW,若设置这两个值,则每当窗口更改大小时,都可以让应用程序重新绘制客户区的全部内容,这样可确保在绘图时,是在一个完全空的客户区内重新开始。把图形的大小设置为与客户区的大小相关联,并同时设置这两个参数,可以让客户区的图形随着客户区大小的改变而改变。 6、以上各点可简单总结为: ①、当客户区的内容无效且需要更新时(重绘时机),需要重绘窗口, ②、此时发送WM_PAINT消息在过程函数中重绘图形(重绘地点), ③、重绘的范围一般为窗口的无效区域(重绘范围), ④、重绘时先擦除旧图形再在窗口中进行重新绘制(重绘方法)。 三、windows内部的窗口重绘步骤 1、理解windows的窗口重绘:要理解windows的窗口重绘,需要把问题简单化,其实windows并没有多少内部代码去实现以上算法,大部分的代码都是需要由程序员自已去编写的。 2、下面讲解一下windows实现窗口重绘的具体步骤: ①、基本原理:MFC是用C++语言编写的,因此是一定遵守C++语法规则的。 ②、使用InvalidateRect函数设置无效区域,注意:区域是可以被叠加的。 ③、由系统等到合适时机发送WM_PAINT消息(该消息也可由程序员发送),窗口重绘的主要工作就位于处理WM_PAINT消息的代码中了,因此若不处理该消息,则程序什么也不会做。 ④、处理WM_PAINT消息的方式一般都是调用BeginPaint和EndPaint函数对,当然程序员也可以用自已的方式处理WM_PAINT消息,不过这种方法无法使用windows内部代码实现的功能,所有功能都需要手动编写。 3、windows内部的BeginPaint函数实现的功能有: ①、、这个函数会发送WM_ERASEBKGND消息以擦除背景,系统默认是调用 DefWindowProc函数擦除背景。 ②、把使用InvalidateRect和ValidateRect函数设置的无效区域设置为更新区域,并把设置 好的更新区域保存在PAINTSTRUCT结构体的rcPaint字段中。 ③、设置BeginPaint返回的DC的区域属性。 ④、清除更新区域,从而删除消息队列中的WM_PAINT消息。 ⑤、返回DC,程序员可使用该DC进行绘图。 ⑥、由此可见,BeginPaint函数并没有绘制图形,图形的重绘完全是程序员自已完成的, 系统内部没有对图形进行重绘的代码(仅仅只有擦除背景的操作),而且BeginPaint所实现的功能,程序员也可以自已编写代码来完成。 ⑦、由此可见,程序员能用到的windows内部代码一般有:区域的设置、WM_PAINT消 息的发送、背景的擦除,其余的步骤就需要由程序员自已编写代码了,其实这3个步骤有时也是需要程序员进行干涉的。 4、更新区域、rcPaint字段、BeginPaint函数返回的DC的区域属性 ①、更新区域、rcPaint字段、BeginPaint函数返回的DC的区域属性所指定的是三个不同 的相互独立的区域范围。 ②、rcPaint字段:该字段是RECT类型的,并不是区域句柄(HRGN)或区域类型(CRgn),因此rcPaint中保存的范围只是一个大致范围,若使用多个InvalidateRect函数设置了无效区域,则rcPaint字段保存的一般是多个InvalidateRect函数设置的范围的叠加,比如若使用InvalidateRect分别设置了两个无效区域(50,50,100,100)和(150,150,200,200)则rcPaint保存的字段的范围将是(50,50,200,200)。 ③、更新区域:该区域是决定是否发送 (50,50) WM_PAINT消息的区域,只要程序中存在 更新区域 (100,100) 无效区域且需要更新时就会发送WM_PAINT 消息,即只要更新区域不为空就会发送 (110,110) WM_PAINT消息,因此,更新区域就是所有 (150,150) 无效区域的叠加,比如设置了两个无效区域 (50,50,110,110)、(150,150,200,200),和一个 有效区域(50,50,100,100),则更新区域如右图。 ④、DC的区域属性:该区域是DC的绘图区域, 超过该区域的图形会被裁剪,因此该区域是 (200,200) 一个GDI对象,也就是说该区域的类型是 区域句柄(HRGN)或区域类型(CRgn),该区域的范围是由InvalidateRect和ValidateRect函数设置的区域进行并、异或等运算后的结果, 5、由以上讨论可得到如下结论: ①、BeginPaint返回的DC区域属性的范围与保存在rcPaint字段中区域的范围并不一定会一致。 ②、调用BeginPaint函数后,更新区域会被清除,但BeginPaint返回的DC区域属性不会被清除,他的范围仍然有效,也就是说此时使用该DC绘制的图形只会被绘制在该区域属性的范围内(超过该区域的图形会被裁剪)。由此可见更新区域和DC区域属性是两个不同且独立的区域。 ③、仅仅使用BeginPaint返回的DC绘制的图形才会被限制在该DC的区域属性范围内,若使用其他形式获得的DC(比如使用GetDC()函数获得的DC),将不受此区域属性的限制,因为这些DC不拥有BeginPaint返回的DC的区域属性。 ④、InvalidateRect、validateRect函数设置的区域,可以改变BeginPaint返回的DC区域属性的范围。 ⑤、InvalidateRect和validateRect函数设置的区域范围不能被其他DC使用(比如使用 GetDC()函数获得的DC),除非程序员有办法把这两个函数设置的区域添加到GetDC获得的DC的区域属性内。因此InvalidateRect和validateRect函数设置的区域范围需要由BeginPaint返回的DC来使用,否则这两个函数设置的范围不会起作用。 四、WM_PAINT消息与BeginPaint函数 1、发送WM_PAINT消息的基本策略 ①、基本规则:只有当消息队列中没有消息,且更新区域不为空(NULL)时,系统才会向过程函数发送WM_PAINT消息。也就是说,在客户区内只要有更新区域,都会导致发送WM_PAINT消息(当消息队列中无消息时便开始发送),若应用程序未处理WM_PAINT消息(即此时一直有更新区域),则会导致系统不断重复的发送WM_PAINT消息,也就是说在WM_PAINT代码块的内部需要把窗口更新区域设置为有效区域(即必须清除更新区域),否则系统会无限重复发送WM_PAINT消息。 ②、如果某个操作影响了窗口的内容,则系统会将该窗口的受影响部分标记为准备好进行更新,并在下一个机会中发送WM_PAINT消息 2、产生WM_PAIN消息的时机: ①、基本规则:WM_PAINT消息是由系统生成的,并且不应由应用程序进行发送。 ②、调用UpdateWindow函数会发送WM_PAINT消息,若更新区域不是空的, UpdateWindow将直接向窗口发送WM_PAINT消息(未进入消息队列),而不管应用程序消息队列中的其他消息的数量,也就是说UpdateWindow可以在消息队列不是空的情况下发送WM_PAINT消息。 ③、当窗口客户区的一部分或全部变为无效且必须更新时,系统会发送WM_PAINT消息。 ④、调用ScrollWindow或ScrollDc函数滚动客户区 ⑤、InvalidateRect和InvalidateRgn函数不会立即产生WM_PAINT消息,需等到合适时机 由系统发送WM_PAINT消息。 ⑥、windows不会在消息队列中放置多条WM_PAINT消息,若队列中已有一条WM_PAINT消息,系统会重新计算一个新的无效矩形。 3、处理WM_PAINT消息与BeginPaint和EndPaint函数 ①、在处理WM_PAINT消息时,若需要使用BeginPaint函数(注意:并不一定必须要调用BeginPaint函数),则必须成对地调用BeginPaint和EndPaint函数。若::EndPaint未被调用,则应用程序将不能正确地重绘客户区。 ②、BeginPaint函数也只能在WM_PAINT消息的响应代码中使用,在其他地方不能使用,且BeginPaint函数得到的DC,必须用EndPaint函数去释放 ③、若过程函数未对WM_PAINT消息进行处理,则必须由DefWindowProc来处理,该函数只是简单的依次调用BeginPaint和EndPaint函数,以使客户区变为有效。 ④、清除更新区域的方法(重要): 在处理WM_PAINT消息时,在调用BeginPaint函数后,整个客户区就会变为有 效的(即清除更新区域), 也可使用ValidateRect函数使客户区中任意的矩形变得有效。 注意:若WM_PAINT内部没有清除更新区域的代码,则只要更新区域不为空, 就会导致系统不断重复的发送WM_PAINT消息。此规则与消息队列中只有一条WM_PAINT消息并不矛盾,因为此时WM_PAINT消息是已经被处理了的,只是在WM_PAINT的代码内部无清除更新区域的代码而已。 4、WM_ERASEBKGND消息与BeginPaint函数 ①、在BeginPaint返回之前,系统将WM_NCPAINT(绘制非客户区)和WM_ERASEBKGND (擦除背景)消息发送到窗口过程。大多数应用程序依靠默认窗口函数DefWindowProc来绘制非客户区域(菜单栏、标题栏等)。 ②、如果将窗口类背景画笔设置为NULL,那么无论何时必须绘制窗口背景,系统都会向 窗口过程发送WM_ERASEBKGND消息,让您绘制自定义背景 ③、如果处理WM_ERASEBKGND,则应用程序应该使用消息的wParam参数来绘制背景。该参数包含窗口显示设备上下文的句柄。绘制背景后,应用程序应该返回一个非零值。 5、MFC中的窗口重绘:在MFC中使用CPaintDC类对上述过程进行封装,CPaintDC类从构造函数调用::BeginPaint,从析构函数调用::EndPaint,因此使用CPaintDC其实就是在间接使用::BeginPaint和::EndPaint函数绘制图形。 CPaintDC类主要用于响应WM_PAINT消息,其工作区域为客户区,因此只能在OnPaint处理程序中使用该类,而不能在其他地方使用 6、下图为窗口创建和销毁的整个消息流程。 WM_CREATE CreateWindowEx() WM_SHOWWIND ShowWindow() WM_SIZE DefWindowProc() 不处理 UpdateWindow() WM_PAINT 发送WM_ERASEBKGND 处 消息擦除背景 理 BeginPaint() 填充PAINTSTRUCT结构 WM_CLOSE 关闭窗口 体中各个字段的值 是否处理 设置返回的DC的区域属 程序员自是 否 程序员的代码 性范围 行处理 DefWindowPoc() 调用后整个客户区就会变 EndPaint() 为有效的(即清除更新区 WM_DESTROY 域) 返回值就是设备环境句柄 PostQuitMessage() WM_QUIT 窗口创建与销毁的消息流程 四、相关的函数及结构体、类的原型 1、CDC* ::BeginPaint(HWND hwnd, LPPAINTSTRUCT lpPaint); 其中LPPAINTSTRUCT是指向结构体类型PAINTSTRUCT(该结构体见下文)的指针。 2、CDC* ::EndPaint(HWND hwnd, PAINTSTRUCT *lpPaint); 3、PAINTSTRUCT结构体原型: typedef struct tagPAINTSTRUCT{ HDC hdc; BOOL fErase; RECT rcPaint; BOOL fRestore; BOOL fIncUpdate; BYTE rgbReserved[16]; }PAINTSTRUCT; ①、应用程序一般只使用该结构体的前3个字段,其他字段由系统内部使用。 ②、hdc:是设备环境的句柄,BeginPaint函数返回的就是这个值。 ③、fErase指示背景是否必须擦除。若此值不为零,则应用程序应该擦除背景(即程序员需在过程函数中使用画刷填充背景)。若此值为0(大多数情况下为止值),则表示系统在先前的BeginPaint函数中已经擦除了无效区域的背景。若想自定义背景擦除的方式,可以自已处理WM_ERASEBKGND消息。 ④、rcPaint字段是以像素为单位的无效矩形的大小(此值就是设置的更新区域的大小),若rcPaint为 NULL,则将清除更新区域,这会阻止后续的 WM_PAINT消息的生成。在使用BeginPaint返回的设备环境时,windows无论如何都不会在rcPaint定义的矩形之外绘制图形的。 4、BOOL ::InvalidateRect( HWND hWnd, const RECT *lpRect, BOOL bErase); ①、作用:添加一个无效区域,无效区域会在更新区域中累积,该函数不会立即产生WM_PAINT消息,需等到合适时机由系统发送WM_PAINT消息。 ②、hWnd:需更改更新区域的窗口的句柄。若为NULL(不建议),则将使所有窗口无效(注意:不仅仅此应用程序的窗口)并重新绘制,并在函数返回之前发送WM_ERASEBKGND和WM_NCPAINT消息。 ③、lpRect:将lpRect指定的矩形区域设置为无效区域。若此参数为NULL,则把整个客户区为无效区域。 ④、bErase:指定无效区域内的背景处理方式。若此参数为TRUE,则调用BeginPaint函数时会清除背景,若为FALSE则背景保持不变,同时在调用BeginPaint函数后 PAINTSTRUCT结构体中的fErase字段的值将是非零。注意:BeginPaint函数清除背 然后在处理该消息的内部代码中重绘背景,景的方法是发送一个WM_ERASEBKGND, 因此设置此值为TRUE就意味着会使BeginPaint函数发送WM_ERASEBKGND消息,若为FLASH则不会发送WM_ERASEBKGND消息,此时背景不会被擦除。 ⑤、返回值:若调用成功则返回非零值,否则返回零值。 5、BOOL ::InvalidateRgn( HWND hWnd, HRGN hRgn, BOOL bErase); 参数及意义同InvalidateRect只是指定区域的方式使用HRGN句柄。 6、BOOL ::ValidateRect ( HWND hWnd, const RECT *lpRect); ①、作用:使指定窗口的矩形区域有效并更新。 ②、hWnd:需更改的窗口名柄,若此参数为NULL,则系统将重绘所有的窗口,并在函数返回前将WM_ERASEBKGND和WM_NCPAINT消息发送到过程函数。 ③、lpRect:使由lpRect指定的矩形有效(即从更新区域中清除由lpRect指定的矩形部分),若为NULL,则所有的客户区都将有效(即清除整个客户区的更新区域)。 ④、返回值:若函数成功则返回非零,否则返回零。 7、BOOL ::ValidateRgn( HWND hWnd, HRGN hRgn); 参数及意义同ValidateRect只是指定区域的方式使用HRGN句柄。 8、BOOL ::GetUpdateRect(HWND hWnd, LPRECT lpRect, BOOL bErase); ①、作用:获取更新区域的矩形坐标。 ②、lpRect:用于保存获取到的更新区域的矩形坐标。若该值为NULL,则存在更新区域时返回非零值,若不存在更新区域则返回零。 ③、bErase:指定是否擦除更新区域中的背景 ,若为TRUE且更新区域不为空,则清除背景。 9、对于MFC,4至8的函数都有相应的MFC版本,他们都位于CWnd类之中。 10、BOOL ::PostMessage(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam); ①、作用:向指定窗口的消息队列中发布(post)一个消息,然后立即返回,此函数不需等待线程处理消息。从windows Vista开始,消息发送受UIPI的限制。 ②、hWnd:需接收消息的窗口的句柄。还可取以下特殊值: HWND_BROADCAST:对应值为(HWND)0xFFFF,表示把消息发布到所有的 顶级窗口,包括禁用或不可见的无主窗口、重叠窗口和弹出窗口,但不会发布到子窗口。 NULL:此时该函数类似于使用dwThreadld参数调用的PostThreadMessage函 数。 ③、Msg:需要发布的消息,不要使用该函数发布WM_QUIT消息。要发布自定义的消息,则该消息的取值应 >= WM_USER(0x0400),也就是说比WM_USER更小的消息值为系统消息,比如WM_PAINT、WM_LBUTTONUP等消息的值,都小于WM_USER。 ④、wParam、lParam:这两个参数是消息Msg所包含的信息。 ⑤、返回值:若函数成功则返回非零值,若函数失败,则返回零。 11、BOOL ::SendMessage(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam); ①、作用:向指定窗口的过程函数直接发送(send)一条消息(未发送至消息队列),并直到过程函数处理完此消息后才返回(函数未立即返回)。从windows Vista开始,消息发送受UIPI的限制。 ②、hWnd:需接收消息的窗口的句柄。还可取以下特殊值: HWND_BROADCAST:对应值为(HWND)0xFFFF,表示把消息发布到所有的 顶级窗口,包括禁用或不可见的无主窗口、重叠窗口和弹出窗口,但不会发布到子窗口。 ③、Msg、wParam、lParam参数与PostMessage函数相同。 ④、返回值:返回消息处理的结果,因此返回值取决于发送的消息。 示例1:验证窗口重绘 #include class B :public CFrameWnd {public: B() { Create(NULL, _T(\"HYONG\"), WS_OVERLAPPEDWINDOW);} DECLARE_MESSAGE_MAP() afx_msg void OnPaint(); afx_msg void OnLButtonDown(UINT nFlags, CPoint point); }; BOOL A::InitInstance() { AllocConsole(); //开启控制台 m_pMainWnd = new B(); m_pMainWnd->ShowWindow(m_nCmdShow); m_pMainWnd->UpdateWindow(); // 此函数会发送WM_PAINT消息 return TRUE; } A ma; BEGIN_MESSAGE_MAP(B, CFrameWnd) ON_WM_PAINT() ON_WM_LBUTTONDOWN() END_MESSAGE_MAP() void B::OnPaint(){ //用于处理WM_PAINT消息的函数 CPaintDC dc(this); /*MFC中的CPaintDC类,在构造函数中调用了::BeginPaint(),在析构函数中调 用了::EndPaint()*/ _cprintf(\"D\"); //使用控制台输出一些内容。 dc.Rectangle(50, 170, 150, 250); }//在WM_PAINT消息内部绘制一个矩形。 void B::OnLButtonDown(UINT nFlags, CPoint point){ CDC* pc=GetDC(); pc->Rectangle(50, 50, 150, 150);} //在WM_PAINT消息外绘制一个矩形,该图形不会被重绘。 运行结果及说明 控制台 控制台,可见系统发送 了WM_PAINT消息 按下鼠 标左键按下鼠标后输出的图 后输出形,未被重绘 的图形 WM_PAINT中的 图形,被重绘 把窗口移出屏幕左侧外面,然后再移回来后的情形 示例2:验证WM_PAINT消息的不断重复发送 #include class B :public CFrameWnd {public: B() { Create(NULL, _T(\"HYONG\"), WS_OVERLAPPEDWINDOW);} DECLARE_MESSAGE_MAP() afx_msg void OnPaint(); }; BOOL A::InitInstance() { AllocConsole(); //开启控制台 m_pMainWnd = new B(); m_pMainWnd->ShowWindow(m_nCmdShow); m_pMainWnd->UpdateWindow(); //此函数会发送WM_PAINT消息 return TRUE;} A ma; BEGIN_MESSAGE_MAP(B, CFrameWnd) ON_WM_PAINT() END_MESSAGE_MAP() /*情形1、处理WM_PAINT消息的函数内部没有清除更新区域的代码,于是系统会不断重复的发送WM_PAINT消 息,从而使控制台会无限输出字符D,并且CPU资源会被大量占用,且电脑可能会因 此变慢。*/ void B::OnPaint(){ _cprintf(\"D\");} /*情形2、若使用以下OnPaint函数处理WM_PAINT消息,则不会出现上述情形,CPaintDC类会在构造函数内部调用::BeginPaint函数,此函数会清除窗口的更新区域,从而不会使系统不断重复的发送WM_PAINT消息。*/ void B::OnPaint(){ _cprintf(\"D\\n\"); CPaintDC dc(this); //或使用以下等效的SDK代码 //PAINTSTRUCT ps; ::BeginPaint(m_hWnd,&ps); ::EndPaint(m_hWnd,&ps); } 示例:理解区域属性、BeginPaint函数及DC(设备上下文) #include BEGIN_MESSAGE_MAP(B, CFrameWnd) ON_WM_PAINT() ON_WM_LBUTTONDOWN() END_MESSAGE_MAP() void B::OnPaint(){ CRect r4; GetUpdateRect(&r4); _cprintf(\"getDC=%d,%d,%d,%d\\n\",r4.top,r4.left,r4.bottom,r4.right); //验证更新区域的大小。 CDC* pc = GetDC(); /*因为pc并未设置区域属性,因此pc不会受到InvalidateRect函数设置的无效 区域的影响,因此pc的更新区域为整个客户区,若欲使GetDC返回的DC受到InvalidateRect函数设置的无效区域的影响,必须为该DC设置区域属性。*/ pc->Rectangle(50, 50, 130, 130); system(\"pause\"); //可使用VC++的该函数让程序暂停,以观察程序的执行步骤。 CPaintDC dc(this); //该类在构造函数中调用了::BeginPaint(),在析构函数中调用了::EndPaint() /*BeginPaint函数会擦除背景。BeginPaint函数会使用InvalidateRect函数设置 的无效区域来设置dc的区域属性,因此使用CPaintDC创建的dc绘制的图形,将被限制在该区域内。BeginPaint函数还会填充PAINTSTRUCT结构体中各字段的值,注意rcPaint字段的区域范围并不一定与dc的区域属性范围相同。最后BeginPaint还会清除更新区域*/ system(\"pause\"); //在此处暂停可以看到调用BeginPaint函数后系统对背景的擦除过程。 GetUpdateRect(&r4); //返回更新区域。 _cprintf(\"beginPaint=%d,%d,%d,%d\\n\", r4.top,r4.left,r4.bottom,r4.right); //更新区域为0,即更新区域已被清除。 _cprintf(\"rcPaint=%d,%d,%d,%d\\n\", dc.m_ps.rcPaint.top, dc.m_ps.rcPaint.left, dc.m_ps.rcPaint.bottom, dc.m_ps.rcPaint.right); /*可见dc的区域属性并不为零, 注意:dc的区域和更新区域现在是两个不同的区域。*/ CBrush cb; cb.CreateSolidBrush(RGB(255, 0, 0)); dc.SelectObject(&cb); dc.Rectangle(50, 50, 150, 150); } /*使用dc绘制的图形,将被限制在由InvalidateRect函数间接 设置的区域范围内。*/ void B::OnLButtonDown(UINT nFlags, CPoint point){ CDC* pc=GetDC(); CBrush cb; cb.CreateSolidBrush(RGB(111,111,111)); CRect r; r.left = 50; r.top=50; r.right=110; r.bottom=110; InvalidateRect(&r,1); /*设置无效区域,并擦除背景,调用此函数后并不会立即发送WM_PAINT消息, 第二个实参为非零会使用BeginPaint函数发送WM_ERASEBKGND消息以擦除背景*/ pc->SelectObject(&cb); pc->Rectangle(50, 50, 150, 150); _cprintf(\"BD , \"); } 运行结果及说明: 1、因为有控制台及暂停代码,因此运行此程序需注意以下问题 2、不要把窗口移至屏幕外面,这样有可能导致电脑卡死,若出现这种情况,请使用ALT+TAB切换到控制台窗口,关闭控制台以结束程序。 3、若程序暂停,请切换到控制台窗口,按提示点击键盘上任意键以使程序继续执行。 4、在程序暂停其间不要在客户区多次点击鼠标左键,因为在暂停期间会导致鼠标消息被放入消息队列中等待处理,当程序运行时会连续处理多个在消息队列中的鼠标消息。 5、未按鼠标时程序运行的过程如下: ①、处理WM_PAINT消息,调用OnPaint函数。 首先在控制台输出“getDC=0,0,357,380”后 面的数字是客户区的大小,然后在客户区绘 制一个白色填充的矩形(50,50,13,130),然后 程序暂停。 ②、切换到控制台,按任意键让程序继续执行。 ③、程序通过CPaintDC类的构造函数调用 BeginPaint函数。然后程序暂停,再次切换至控制台按任意键让程序继续执行,最后结果如右图。 6、在客户区按下鼠标左键后程序运行的过程如下: ①、调用OnLButtonDown函数,首先设置一个无效区域(50,50,110,110),然后绘制一个灰 色填充的矩形(50,50,150,150),此矩形会覆盖之前绘制的红色矩形,再然后输出BD,最后系统发送WM_PAINT消息,可见,在此处并不是调用InvalidateRect函数之后就立即发送的WM_PAINT消息。 (50,50,110,110) ②、处理WM_PAINT消息,调用OnPaint函数。 ③、输出getDC=50,50,110,110,数字为此时的无效区域范 围,然后绘制一个白色填充的矩形(50,50,130,130),程序 暂停。因为使用InvalidateRect函数设置的无效区域未被 设置为GetDC返回的DC的区域属性,因此GetDC返回 的DC会在整个客户区上绘图,因此绘制的图形不受 (50,50,150,150) InvalidateRect函数设置的无效区域的影响,见右图 (50,50,130,130) ④、切换到控制台按任意键继续执行,程序通过CPaintDC类 的构造函数调用BeginPaint函数,该函数会使用MFC内 部代码执行以下操作: 使用窗口类WNDCLASSEX结构体中字段 hbrBackground设置的画刷擦除无效区域 (50,50,110,110)的背景,见右图。 (50,50,150,150) 将该无效区域(50,50,110,110)设置为更新区域, (50,50,130,130) 并设置PAINTSTRUCT结构体的rcPaint字段 擦除背景(50,50,110,110) 的值为更新区域的范围(50,50,110,110),此时 由CPaintDC创建的DC的区域属性范围也是 更新区域的范围(50,50,110,110),使用该DC 绘制的图形,将位于该区域内。 清除更新区域(50,50,110,110), 然后程序暂停。 ⑤、切换到控制台按任意键继续执行,首先输出 (50,50,150,150) beginPaint=0,0,0,0,可见更新区域已被清除,然后输 (50,50,130,130) 出rcPaint=50,50,110,110。 红色矩形(50,50,110,110) ⑥、然后程序在dc的区域属性范围(50,50,110,110)内绘制 虚线框是为说明问题 一个红色的矩形(50,50,150,150),超出区域的部分被 而绘制的,实际输出的 图形没有虚线框 裁剪,因此最后红色矩形的大小为(50,50,110,110), 见右图。 示例:理解DC的区域属性、rcPaint字段、更新区域 #include class A :public CWinApp { public: BOOL InitInstance(); }; class B :public CFrameWnd {public: B() { Create(NULL, _T(\"HYONG\"), WS_OVERLAPPEDWINDOW, CRect(100, 100, 500, 500)); } DECLARE_MESSAGE_MAP() afx_msg void OnPaint(); afx_msg void OnLButtonDown(UINT nFlags, CPoint point); afx_msg BOOL OnEraseBkgnd(CDC* pDC); }; BOOL A::InitInstance() { AllocConsole(); m_pMainWnd = new B(); m_pMainWnd->ShowWindow(m_nCmdShow); m_pMainWnd->UpdateWindow(); return TRUE; } A ma; BEGIN_MESSAGE_MAP(B, CFrameWnd) ON_WM_PAINT() ON_WM_LBUTTONDOWN() ON_WM_ERASEBKGND() END_MESSAGE_MAP() void B::OnPaint() { CBrush cb; cb.CreateSolidBrush(RGB(255, 0, 0)); //创建画刷 CRect r; CPaintDC dc(this); //使用InvalidateRect和ValidateRect函数可设置CPaintDC创建的DC的区域属性 GetUpdateRect(&r); //获取更新区域 _cprintf(\"updateRect = %d,%d,%d,%d\\n\", r.left,r.top,r.right,r.bottom); /*可见,调用BeginPaint函数后,更新区域为零(这意 味着不会发送WM_PAINT消息)。*/ _cprintf(\"rcPaint = %d,%d,%d,%d\\n\", dc.m_ps.rcPaint.left, dc.m_ps.rcPaint.top, dc.m_ps.rcPaint.right, dc.m_ps.rcPaint.bottom); /*输出rcPaint字段的区域范围,由此可见,rcPaint的区域 范围和更新区域是两个不同的相互独立的区域。*/ dc.m_ps.rcPaint.left=0; dc.m_ps.rcPaint.top=0; dc.m_ps.rcPaint.right=10; dc.m_ps.rcPaint.bottom=10; //重新设置rcPaint范围的大小 static int i = 0; //注意:静态变量只会初始化一次。 if (i != 0) { //此语句表示,在第一次创建窗口时,不绘制以下矩形(50,50,240,240); dc.SelectObject(&cb); dc.Rectangle(50, 50, 240, 240);} /*可见,虽然更新区域为零,但是dc区域属性的范围并不 为零,也就是说dc的区域属性,并没有受到BeginPaint函数的影响,还可看到dc的区域属性和rcPaint字段的区域也是两个互相独立的区域。*/ i = 1; system(\"pause\"); } void B::OnLButtonDown(UINT nFlags, CPoint point) { CDC* pc = GetDC(); CBrush cb; cb.CreateSolidBrush(RGB(111, 111, 111)); CRect r; r.left = 50; r.top = 50; r.right = 110; r.bottom = 110; CRect r1; r1.left = 50; r1.top = 50; r1.right = 100; r1.bottom = 100; CRect r2; r2.left = 150; r2.top = 150; r2.right = 200; r2.bottom = 200; InvalidateRect(&r, 1); //将矩形r设为无效区域 InvalidateRect(&r2, 1); //将矩形r2设为无效区域 ValidateRect(&r1); //将矩形r1设为有效区域。 //UpdateWindow(); /*调用以上三函数后并不会立即发送WM_PAINT消息,要立即发送WM_PAINT消 息,可使用此函数。*/ pc->SelectObject(&cb); pc->Rectangle(50, 50, 150, 150); /*此处绘制的图形不会受到InvalidateRect和ValidateRect函数 设置的区域的影响,因为此处的DC未设置区域属性。*/ _cprintf(\"BD, \"); system(\"pause\"); } //系统至少要等到该函数执行完之后才会发送WM_PAINT消息。 BOOL B::OnEraseBkgnd(CDC* pDC) { //处理WM_ERASEBKGND消息以擦除背景 CFrameWnd::OnEraseBkgnd(pDC); /*使用父类的相应函数擦除背景,否则需程序员自行设计程序擦除 背景,若没有擦除背景的代码,则在窗口更新时,会在客户区内留下残影。*/ system(\"pause\"); //此处暂停能看到背景被清除时的步骤。 CDC* pc = GetDC(); //注意:此DC未设置任何的区域属性。 pc->Rectangle(50, 50, 240, 240); _cprintf(\"E, \"); system(\"pause\"); return 1; } //应返回非零值 运行结果及说明: 未在客户区按下鼠标时的绘制过程如下: ①、处理WM_PAINT消息,调用OnPaint函数。程序通过CPaintDC类的构造函数调用 BeginPaint函数,该函数发送WM_ERASEBKGND消息 ②、调用OnEraseBkgnd函数,该函数使用CFrameWnd类中的OnEraseBkgnd函数擦除背景,然后程序暂停,切换至控制台按任意键让程序继续执行,然后绘制一个白色填充的矩形(50,50,240,240),然后输出E,程序再次暂停。 ③、切换至控制台按任意键让程序继续执行,返回到OnPaint函数,输出updateRect=0,0,0,0 和rcPaint=0,0,380,357,然后程序暂停,至此程序运行完毕。最后结果如下图。 在客户区按下鼠标时的绘制顺序如下 OnLButtonDowOnEraseBkgndOnEraseBkgndOnPaint OnEraseBkgnd的背景色区域和OnPaint的红绝背景区域是 CPaintDC创建的DC的区域属性的范围,该范围是 InvalidateRect(&r,1)和InvalidateRect(&r2, 1)进行并运 算后,再与ValidateRect(&r1)进行异或运算后的结果 BD是按下鼠标时最先输出的地方 ①、调用OnLButtonDown函数,首先设置无效区域(50,50,110,110)和(150,150,200,200),然后 再设置一个有效区域(50,50,100,100),然后在之前绘制的白色填充矩形之上绘制一个灰色填充的矩形(50,50,150,150),之后输出BD,然后程序暂停,此时的结果见上图左侧第一幅图。 ②、切换至控制台按任意键让程序继续执行,系统发送WM_PAINT消息,调用OnPaint函数。 程序通过CPaintDC类的构造函数调用BeginPaint函数,该函数发送WM_ERASEBKGND消息,调用OnEraseBkgnd函数,该函数使用CFrameWnd类中的OnEraseBkgnd函数擦除背景,然后程序暂停,此时的结果见上图左侧第二幅图。 ③、切换至控制台按任意键让程序继续执行,然后绘制一个白色填充的矩形(50,50,240,240), 此时绘制的矩形,会覆盖掉之前绘制的图形,之后输出E,程序暂停。此时的结果见上图左侧第三幅图。 ④、切换到控制台按任意键继续执行,返回到OnPaint函数,输出updateRect=0,0,0,0和 rcPaint=50,50,200,200,此处可看到,更新区域和rcPaint的区域是两个不同且相互独立的区域。 ⑤、执行之后的if语句中的语句dc.Rectangle(50, 50, 240, 240);绘制一个红色填充的矩形 (50,50,240,240),但是此时dc的区域属性的范围是InvalidateRect(&r,1)和 InvalidateRect(&r2, 1)进行并运算后,再与ValidateRect(&r1)进行异或运算后的结果(最终范围见上图左侧第4幅图中的红色区域部分),因此绘制的矩形在超过这个范围之外的部 分会被裁剪掉,之后程序暂停,至此程序运行完毕。在此处可看到dc的区域属性的范围、更新区域、rcPaint的区域,这是三个互相独立的不同的区域。 示例:模仿窗口重绘过程 #include int MS = WM_USER + 1; //创建自已的消息。 BEGIN_MESSAGE_MAP(B, CFrameWnd) ON_WM_PAINT() ON_WM_LBUTTONDOWN() ON_MESSAGE(MS,f) //关联自已的消息和消息处理函数。 END_MESSAGE_MAP() LRESULT B::f(WPARAM w,LPARAM l){ //该函数的功能类似于OnPaint函数。 CDC* pc = GetDC(); CBrush cb; cb.CreateSolidBrush(RGB(255, 0, 0)); pc->SelectObject(&cb); pc->SelectObject(&m_cr); //将区域属性m_cr选入DC,区域m_cr的范围可由in()和n()函数设置。 pc->Rectangle(50, 50, 240, 240); _cprintf(\"F, \"); system(\"pause\"); return 1; } void B::OnLButtonDown(UINT nFlags, CPoint point){ CDC* pc=GetDC(); CBrush cb; cb.CreateSolidBrush(RGB(111,111,111)); CRect r; r.left = 50; r.top=50; r.right=110; r.bottom=110; CRect r1; r1.left = 50; r1.top = 50; r1.right = 100; r1.bottom = 100; CRect r2; r2.left = 150; r2.top = 150; r2.right = 200; r2.bottom = 200; in(r); //增加m_cr的区域范围,类似于InvalidateRect函数。 pc->SelectObject(&cb); pc->Rectangle(50, 50, 250, 250); in(r2); //增加m_cr的区域范围。 n(r1); //减少m_cr的区域范围,类似于ValidateRect函数。 _cprintf(\"BD, \"); system(\"pause\"); SendMessage(MS, 0, 0);//发送自已的消息。 } //WM_PAINT消息仍会发送 void B::OnPaint() {/*该函数仅仅是清除所有的更新区域以避免无限的发送WM_PAINT消息,其余什么事都 不做,也就是说该函数不具有擦除背景等其他功能。*/ ValidateRect(NULL); //使整个客户区有效。 _cprintf(\"P, \"); system(\"pause\"); } void B::in(CRect r) { //类似于InvalidateRect函数的功能。 CRgn cr; cr.CreateRectRgn(r.left,r.top,r.right,r.bottom); //创建一个矩形区域 m_cr.CombineRgn(&m_cr, &cr, RGN_OR);} //求区域cr与m_cr的并集 void B::n(CRect r) { //类似于ValidateRect函数的功能。 CRgn cr; cr.CreateRectRgn(r.left, r.top, r.right, r.bottom); //创建一个矩形区域 m_cr.CombineRgn(&m_cr, &cr, RGN_XOR);}/*求区域cr与m_cr的异或集,其结果就是区域cr与m_cr不 重叠的那部分区域。*/ 点击鼠标左键后的最终结果如下图 红色部分的区域为使用in和n函数设置的区域。 第5章 鼠标 鼠标消息中的L表示left(左),R表示right(右),M表示middle(中),为方便讲解,文中会使用x表示L、R、或者M中的任意一个。 一、基本概念 1、热点(hot spot):热点指示了鼠标指针在显示设备上的精确位置,它具有一个单像素的精度。比如常见的斜向箭头鼠标指针(IDC_ARROW)其热点位于箭头的顶点处,再如IDC_CROSS指针的热点位于十字形图案的中心。 2、鼠标指针:又称为光标,默认鼠标指针的外观由窗口类的结构体WNDCLASSEX中的hCursor字段指定。 3、鼠标的双击速度、鼠标按钮是否切换、鼠标指针的形状等可通过控制面板进行设置,也可使用相关函数进行设置(相关函数见后文)。 4、鼠标消息:可分为客户区鼠标消息和非客户区鼠标消息。 5、需使用到的函数及宏: ①、客户区坐标和屏幕坐标间的转换:CWnd::ClientToScreen、CWnd::ScreenToClient ②、设备坐标和逻辑坐标间的转换:CDC::DPtoLP、CDC::LPtoDP ③、位提取宏:LOWORD(低位)、HIWORD(高位),这两个宏只能提取正数的值,当提取 的值有负数时不能使用这两个宏。比如x=LOWORD(lParam)表示把lParam的低位进行提取,并把提取的结果存储在x中。 ④、位提取函数:GET_X_LPARAM和GET_Y_LPARAM这两个函数可提取值为负数的 参数,原型如下: int GET_X_LPARAM(LPARAM lParam); 表示提取lParam的低位值,并把该值以int形式返回。 5.1 客户区鼠标消息 1、以下的讨论仅限于位于客户区内的鼠标消息 左键 右键 中键 按下 WM_LBUTTONDOWN WM_RBUTTONDOWN WM_MBUTTONDOWN 客户区鼠标消息 释放 WM_LBUTTONUP WM_RBUTTONUP WM_MBUTTONUP 双击 WM_LBUTTONDBLCLK WM_RBUTTONDBLCLK WM_MBUTTONDBLCLK 说明:客户区鼠标消息还包括:WM_MOUSEMOVE鼠标在客户区移动。 2、鼠标单击消息: ①、若鼠标在客户区被按下,然后将鼠标光标移至窗口外再释放,此时窗口将会只接收到 WM_xBUTTONDOWN消息,而不会接收到WM_xBUTTONUP消息。同理,若在其他客口内按下鼠标,然后移至用户窗口并释放,此时用户窗口将只接收到WM_xBUTTONUP消息而不会接收到WM_xBUTTONDOWN消息 ②、鼠标捕获:若在鼠标被按下后将鼠标捕获,则无论鼠标的光标位于何处,鼠标的消息 都会被发送到执行鼠标捕获的窗口中,这就保证了鼠标被按下,然后将其移至窗口外释放时,窗口同样能接收到鼠标被释放的消息。 3、鼠标双击消息 ①、窗口是否接收鼠标双击消息取决于窗口类的结构体WNDCLASSEX中的style字段的 取值,若该值为CS_DBLCLKS则接收双击消息(默认情形),否则窗口不接收双击消息。 ②、鼠标双击消息还受到两次单击时的速度(称为双击速度)和两次单击时的物理位置的影 响。默认情形下,两次单击时的物理位置范围在一个平均系统字体字符宽度和半个字符高度的范围内。双击速度可以在控制面板中进行更改。两次单击之间的时间间隔长度可使用GetMessageTime函数来得到(该函数暂不讲解)。 ③、在处理鼠标双击消息时,不要对两次单击消息执行不相关的操作,比如对于文件夹的操作,通常是第一次单击后选定文件夹,第二次单击时打开该文件夹。 ④、两次快速单击时鼠标消息的产生顺序如下表 两次快速单击时鼠标消息的产生顺序 接收鼠标双击消息 WM_xBUTTONDOWN WM_xBUTTONUP WM_xBUTTONDBLCLK WM_xBUTTONUP 不接收鼠标双击消息 WM_xBUTTONDOWN WM_xBUTTONUP WM_xBUTTONDOWN WM_xBUTTONUP 4、鼠标移动消息(WM_MOUSEMOVE) ①、鼠标移动时,在光标下面的窗口会接收到鼠标移动的消息WM_MOUSEMOVE ②、WM_MOUSEMOVE报告的是光标的最近位置。 ③、光标移动会产生大最的报告位置的消息,因此系统并不会把每次移动所产生的 WM_MOUSEMOVE消息都发送到消息队列中,若光标飞快地移过窗口,则程序通常只会接收到少数的WM_MOUSEMOVE消息,若光标缓慢的移动,则程序会报告光标移动时的所有点的位置。程序具体接收到的WM_MOUSEMOVE消息的个数决定于鼠标硬件和过程函数处理鼠标移动消息的速度。 5、处理客户区鼠标消息 添加消息处理函数时,最好使用“类视图”的方式添加,这样可避免去查询各消息处理函数的原型 ①、使用MFC默认的消息处理函数处理客户区鼠标消息 系统默认的客户区鼠标消息处理函数都具有如下形式的原型: afx_msg void OnxButtonXXX(UINT nFlags, CPoint point); 参数point指示光标相对于客户区左上角的x和y的设备坐标,单位为像素,注意: 不是逻辑坐标,可使用CDC::DPtoLP把point转换为逻辑坐标。对于 WM_xBUTTONDOWN和WM_xBUTTONDBLCLK消息,指的是鼠标按下时光标所在的位置,对于WM_xBUTTONUP指的是鼠标释放时光标的位置。对于WM_MOUSEMOVE指的是最近的光标位置。 参数nFlags用于指示各虚拟键的状态,可使用按位与\"&\"运算符进行测试,比如 nFlags&MK_SHIFT的结果若为非零值,则表示键盘的shift键被按下。使用该参数可实现一些特殊的处理,比如当按住shift键并移动鼠标绘图时,可限制用户只能绘制水平或垂直线。使用GetKeyState函数也能得到各虚拟键的状态,对于键盘中未按下的键和没有按下鼠标时不能使用GetKeyState函数,也就是说,只有当键被按下后,GetKeyState函数才会报告键被按下的状态。 nFlags参数如下表 nFlags参数 虚拟键 MK_LBUTTON MK_MBUTTON MK_RBUTTON 意义 鼠标左键被按下 鼠标中键被按下 鼠标右键被按下 虚拟键 MK_CONTROL MK_SHIFT 意义 ctrl键被按下 shift键被按下 ②、通过ON_MESSAGE宏添加自定义的函数处理客户区鼠标消息,原型如下 afx_msg LRESULT f(WPARAM wParam, LPARAM lParam) 函数名是由程序员自已命名的。 lParam:表示光标相对于客户区左上角的x和y的设备坐标,其中低位表示x坐标, 高位表示y坐标,因此要获取x和y坐标的值,需要对参数lParam的值进行提取,该值可使用按位与进行提取,也可使用LOWORD(低位)和HIWORD(高位)宏进行提取,比如x=LOWORD(lParam)。lParam参数获取的具体位置与MFC默认的鼠标消息处理函数的point参数相同。 wParam:表示各虚拟键的状态,其取值与MFC默认的鼠标消息处理函数的nFlags 参数相同。 5.2 非客户区鼠标消息与击中测试 一、非客户区鼠标消息 1、非客户区鼠标消息的范围:该消息发生在窗口内除客户区外的区域,包括标题栏、菜单栏、滚动条等,注意非客户区不包括位于窗口外的屏幕区域。 2、以下的讨论仅限于位于非客户区内的鼠标消息 非客户区鼠标消息 NC表示nonclient(非客户区) 左键 右键 中键 按下 WM_NCLBUTTONDOWN WM_NCRBUTTONDOWN WM_NCMBUTTONDOWN 释放 WM_NCLBUTTONUP WM_NCRBUTTONUP WM_NCMBUTTONUP 双击 WM_NCLBUTTONDBLCLK WM_NCRBUTTONDBLCLK WM_NCMBUTTONDBLCLK 说明:非客户区鼠标消息还包括:WM_NCMOUSEMOVE鼠标在非客户区移动。 3、鼠标单击消息、鼠标移动消息与客户区鼠标消息的原理相同 4、鼠标双击消息:与客户区鼠标消息不同的是无论窗口类的结构体WNDCLASSEX中的style字段的取值是否为CS_DBLCLKS,窗口都会接收双击消息。其余原理与客户区鼠标消息相同。 二、击中测试 1、击中测试(或命中测试)(hit-test):击中测试主要用于判断光标是否位于某个范围内,比如标题栏上有最大化、最小化按钮,那么当点击鼠标时,怎样根据光标当前的位置判断出鼠标点击的是最大化还是最小化按钮呢?击中测试就是用于完成此任务的。 2、击中测试原理:击中测试的代码可由MFC完成,但程序员也可能会编写自已的击中测试代码,以检测用户在客户区究竟选择了什么对象,有一种比较简单的思想,就是使用一个函数比如int f(…)用于计算鼠标光标所在位置是否在某个范围内,比如在第1个范围内返回代码1,在第2个范围内返回代码2等等,然后程序调用f函数,根据返回的代码值判 断该代码是标题栏、最大化按钮还是最小化按钮等等,然后再根据鼠标是单击、双击等对程序作出反应,这就实现了击中测试的代码。 3、WM_NCHITTEST消息 ①、WM_NCHITTEST消息可由程序员自行处理,也可能MFC内部代码处理,通常程序 员不需要处理WM_NCHITTEST消息,而是由MFC来处理。 ②、Windows在发送客户区或非客户区鼠标消息之前,先发送光标的屏幕坐标和WM_NCHITTEST消息。 ③、Windows处理WM_NCHITTEST消息的思想是消息引发消息:首先使用光标的坐标来确定光标位于窗口上的范围,然后发送一个客户区或非客户区鼠标消息。也就是说Windows会利用WM_NCHITTEST消息来产生其他鼠标消息。 ④、Windows消息引发消息的示例:比如当击中测试检测到用户双击鼠标的区域是标题 栏时,Windows的处理过程大致如下: 处理WM_NCHITTEST消息的处理函数返回HTCAPTION(此标识符表示击中的 是标题栏)。 系统根据返回的HTCAPTION值,在消息队列中添加一个非客户区鼠标双击消息 WM_NCLBUTTONDBLCLK。 在处理WM_NCLBUTTONDBLCLK消息时,系统会给窗口发送一个 WM_SYSCOMMAND消息,其中wParam参数等于SC_MAXIMIZE或SC_RESTORE,然后根据wParam的值使窗口最大化或恢复原来的状态。 4、此点内容为自已测试的内容 理论上来讲程序员通过对WM_NCHITTEST消息的处理,可以控制系统对鼠标消息的发送,但经测试程序员自行处理的WM_NCHITTEST消息只会对客户区的鼠标消息产生影响,对非客户区的鼠标消息不会产生影响。 三、处理击中测试和非客户区鼠标消息 1、处理击中测试消息WM_NCHITTEST ①、使用MFC默认的消息处理函数处理WM_NCHITTEST消息,其原型如下: afx_msg LRESULT OnNcHitTest(CPoint point); point表示相对于屏幕坐标的鼠标指针的位置。可使用CWnd::ScreenToClient函数 把屏幕坐标转换为客户区坐标。 返回值为一个反应指针击中测试的代码,它是一个枚举值,取值见下表。 值 HTCAPTION(2) HTCLIENT(1) HTMENU(5) HTHELP(21) HTCLOSE(20) 表xxxx 常见的命中测试码 位置 值 标题栏 客户区 菜单栏 帮助按钮 关闭按钮 HTTOP(12) HTBOTTOM(15) HTLEFT(10) HTRIGHT(11) HTTOPLEFT(13) 位置 在可调整大小窗口的上部 在可调整大小窗口的底部 在可调整大小窗口的左边界 在可调整大小窗口的右边框 窗口边界左上角 HTVSCROLL(7) HTHSCROLL(6) HTREDUCE(8)或HTMINBUTTON HTZOOM(9)或HTMAXBUTTON HTGROWBOX或HTSIZE(4) 垂直滚动栏 水平滚动栏 最小化按钮 最大化按钮 还原按钮 HTTOPRIGHT(14) HTBOTTOMLEFT(16) HTBOTTOMRIGHT(17) HTSYSMENU(3) HTNOWHERE(0)或THERROR(-2) 窗口边界右上角 在可调整大小窗口的左下角 在可调整大小窗口的右下角 系统菜单栏或子窗口的关闭按钮 在屏幕背景或窗口之间的分隔线(此值表示光标未位于窗口之中) ②、通过ON_MESSAGE宏添加自定义的函数处理WM_NCHITTEST消息,原型如下 afx_msg LRESULT f(WPARAM wParam, LPARAM lParam) 函数名是由程序员自已命名的,返回值见上表。 lParam:表示光标相对于屏幕左上角的x和y的屏幕坐标,其中低位表示x坐标, 高位表示y坐标 wParam:该参数未用到。 2、处理非客户区鼠标消息 ①、使用MFC默认的消息处理函数处理非客户区鼠标消息,其原型如下 afx_msg void OnNcxButtonXXX(UINT nHitTest, CPoint point); 参数point指示光标相对于屏幕左上角的坐标,单位为像素,因为是非客户区鼠标 消息,所以坐标相对于屏幕的左上角。 参数nHitTest用于指示窗口的命中测试码,即用于指示鼠标击中了窗口的什么位置, 其取值与WM_NCHITTEST消息处理函数的值相同,其取值见表xxxx。理论上来讲,该值就是处理WM_NCHITTEST消息的函数的返回值。 ②、通过ON_MESSAGE宏添加自定义的函数处理非客户区鼠标消息,原型如下 afx_msg LRESULT f(WPARAM wParam, LPARAM lParam) 函数名是由程序员自已命名的 lParam:指示光标相对于屏幕左上角的坐标,其中低位表示x坐标,高位表示y坐 标 wParam:用于指示窗口的命中测试码,理论上来讲,该值就是处理WM_NCHITTEST 消息的函数的返回值。 四、示例代码 以下代码仅列出核心程序处理部分,消息的添加和窗口的创建部分代码省略。 示例1:创建可在客户区拖动的窗口 LRESULT B::OnNcHitTest(CPoint point){ UINT i=CFrameWnd::OnNcHitTest(point); if(i==HTCLIENT) i=HTCAPTION; return i; } 程序运行结果及说明: 在窗口客户区上的操作就像是在标题栏上的操作一样。即单击鼠标左键不放可在客 户区拖动窗口,双击客户区可放大/还原窗品。 因为OnNcHitTest返回的是HTCAPTION击中测试代码,该代码表示鼠标击中的是 非客户区,因此客户区将不能接收到客户区鼠标消息,只能接收到非客户区鼠标消息。 示例2:使单击标题栏失效 void B::OnNcLButtonDown(UINT nHitTest, CPoint point){ if(nHitTest!=HTCAPTION)CWnd::OnNcLButtonDown(nHitTest,point); } 程序运行结果及说明: 结果:在窗口的标题栏上按住鼠标左键不放无法移动窗口(即单击标题栏不起作用了) 此程序只对处理非客户区鼠标消息WM_NCLBUTTONDOWN, 不需要处理击中测 试消息WM_NCHITTEST 使用类似代码还可实现使双击标题栏失效等其他操作。 示例3:使客户区无法接收客户区和非客户区鼠标消息 LRESULT B::OnNcHitTest(CPoint point){ return HTNOWHERE; } 程序运行结果及说明: 结果:此时在客户区不能接收到任何鼠标消息(包括客户区和非客户区鼠标消息),经 测试,此代码对非客户区不受影响(比如,标题栏仍能接收到非客户区鼠标消息)。 HTNOWHERE表示鼠标击中的是屏幕背景,也就是说鼠标未击中窗口的任何部分, 理论上来讲,返回此值窗口不会接受到任何鼠标消息,但经测试对非客户区的鼠标消息不受影响(比如标题栏、菜单栏、最大/最小化按钮等仍能接收到非客户区鼠标消息) 5.3 捕获鼠标 1、为什么需要捕获鼠标:一般情况下,若用户在窗口内按住鼠标不放,然后把鼠标移至窗口外释放,这时窗口将只会接收到鼠标按下消息,不能接收到鼠标释放消息,若要使窗口能接收到在窗口外释放的鼠标消息,就需要使用到捕获鼠标的功能,也就是说捕获鼠标能使窗口接收到位于窗口之外的鼠标消息。 2、捕获鼠标的作用:比如需要使用鼠标左键按下时设置直线的起点,当鼠标释放时设置直线的终点,并在这两点之间绘制一直线,但用户若把鼠标移到窗口外进行释放,则这条直线将会被悬置,从而绘制不出该直线,要绘制此类型的直线就需要捕获鼠标。 3、捕获鼠标的方法:使用CWnd::SetCapture函数捕获鼠标,使用::ReleaseCapture释放捕获的鼠标。 4、捕获鼠标的算法及应用 ①、在win32环境下,为防止程序独占鼠标资源,当鼠标键已释放而窗口仍拥有捕获能力 时,系统会停止向该窗口发送鼠标消息。为防止产生类似的混乱情形,鼠标捕获通常应位于鼠标键按下的处理函数中,撤销捕获的鼠标通常应位于鼠标键释放的处理函数中。 ②、当鼠标被捕获后,窗口可以接收到鼠标消息而不管光标位于什么位置,直到鼠标键被 释放或捕获的鼠标被撤销, ③、若鼠标在鼠标键按下的处理函数中被捕获后,就算鼠标离开了窗口,窗口仍然能接收 到报道光标位置的鼠标消息(即WM_MOUSEMOVE消息),此时光标的位置是相对于客户区坐标的,但坐标的值可以为负值,也可超出客户区的范围大小。 5、相关函数及原型 ①、CWnd* CWnd::SetCapture() //该函数用于捕获鼠标。 ②、BOOL ::ReleaseCapture(); //该函数用于撤销捕获的鼠标。 ③、static CWnd* CWnd::GetCapture(); 返回捕获鼠标的窗口,若窗口未捕获鼠标则返回NLL,注意:返回值是临时值, 不应存储以供以后使用。 典型应用是判断自身窗口是否捕获了鼠标,语句为if (GetCapture()==this); 示例:橡皮筋直线及捕获鼠标 #include class A :public CWinApp { public: BOOL InitInstance(); }; class B :public CFrameWnd {public: B():x(0) {Create(NULL, _T(\"HYONG\"), WS_OVERLAPPEDWINDOW, CRect(100, 100, 500, 500)); } int x; //用于判断光标移动时是否绘制直线 CPoint cp; //存储直线的坐标 afx_msg void OnLButtonDown(UINT nFlags, CPoint point); afx_msg void OnLButtonUp(UINT nFlags, CPoint point); afx_msg void OnMouseMove(UINT nFlags, CPoint point); DECLARE_MESSAGE_MAP() }; BOOL A::InitInstance() { m_pMainWnd = new B(); m_pMainWnd->ShowWindow(m_nCmdShow); m_pMainWnd->UpdateWindow(); return TRUE; } A ma; BEGIN_MESSAGE_MAP(B, CFrameWnd) ON_WM_LBUTTONDOWN() ON_WM_LBUTTONUP() ON_WM_MOUSEMOVE() END_MESSAGE_MAP() void line(CDC *pc, CPoint p2); //用于画直线的函数 void B::OnLButtonDown(UINT nFlags, CPoint point) { x = 1; //开启鼠标移动时绘制直线 cp = point; //把左键按下时的光标位置保存于cp中 SetCapture(); } //按下鼠标左键时捕获鼠标 void B::OnLButtonUp(UINT nFlags, CPoint point){ CRect cr; CClientDC dc(this); GetClientRect(&cr); dc.MoveTo(cp); if (point.x<0 || point.y<0 || point.x>cr.right || point.y>cr.bottom) { //光标位于客户区外时绘制一条起点为(100,100),终点为(200,100)的直线 InvalidateRect(0); //使整个客户区无效 UpdateWindow(); //立即更新无效区域 dc.MoveTo(100,100); //设置直线起点坐标 cp.x=200;cp.y=100; //设置直线终点坐标 line(&dc, cp); } //绘制直线 else {//光标位于客户区内时绘制一条以鼠标按下为起点,鼠标释放为终点的直线 InvalidateRect(0); UpdateWindow(); line(&dc, point); } //if语句结束 x = 0; //关闭鼠标移动时绘制直线 ReleaseCapture(); }//释放鼠标左键时撤销捕获的鼠标 void B::OnMouseMove(UINT nFlags, CPoint point){ if (x == 1) { //按下左键时,根据鼠标移动的轨迹绘制橡皮筋直线 CClientDC dc(this); InvalidateRect(0); UpdateWindow(); dc.MoveTo(cp); line(&dc, point); }} void line(CDC *pc, CPoint p2){ pc->LineTo(p2); } 程序运行结果及说明:当在客户区按住鼠标不放并移动时会出现一第根着光标移动的橡皮筋直线,当释放鼠标时绘制一条从按下鼠标的位置到释放鼠标位置处的直线,光光标移动客户区外时绘制一条起点为(100,100),终点为(200,100)的直线 5.4 鼠标光标 鼠标是全局共享硬件资源,通常鼠标消息发送给光标下的窗口, 一、光标的形状 1、本小节主要针对光标的形状和位置进行讲解。 2、设置光标形状的方法 ①、方法1:设置类光标:在注册窗口时由WNDCLASSEX结构体中的字段hCursor设置 光标的形状,使用此方法设置的光标形状被称为类光标,这是常用的方法。 ②、方法2:使用::SetCursor函数设置光标的形状,本小节重点讲解此方法,此函数通常 在处理WM_SETCURSOR消息的处理函数中进行使用,使用此方法可以实现把光标移至客户区的不同区域而出现不同的形状,比如在客户区上半部分使光标是十字形状,下半部分使光标为I形状等。 ③、方法3:使用::SetClassLongPtr函数设置光标的形状,该函数详见第2部分第4章。 3、WM_SETCURSOR消息 ①、发送时机:当鼠标未被捕获,在窗口内移动时系统会发送WM_SETCURSOR消息。 ②、系统默认处理方式:系统对WM_SETCURSOR消息的默认响应是调用::SetCursor函 数设置光标,若命中测试码是HTCLIENT(客户区),则光标形状为类光标,若命中测 试码位于客户区之外则显示为箭头,由此可见当光标在屏幕上移动时会自动更新其形状。 ③、在处理WM_SETCURSOR消息时设置了光标的形状,则注册窗口时设置的类光标形 状会被忽略。注意:类光标优先于其他情形下设置的光标,也就是说在其他情形下设置的光标形状,当移动鼠标时会恢复类光标的形状。要使其他情形下设置的光标形状不被恢复为类光标的形状,此时可把类光标设置为NULL。比如若在鼠标按下时设置了光标的新形状,则仅当鼠标按下时光标才显示出新形状,若释放鼠标或移动鼠标都会被重新设置为类光标的形状;若是在鼠标移动时设置了光标的新形状,则在移动鼠标时光标会在新形状与类光标的形状之间来回变换,因为移动光标时会更新两次光标形状。 4、处理WM_SETCURSOR消息 ①、使用MFC默认的消息处理函数处理非客户区鼠标消息,其原型如下 afx_msg BOOL OnSetCrusor(CWnd *pWnd,UINT nHitTest,UINT message); pWnd:指向包含光标的窗口。此指针可能是临时指针,不应存储供以后使用。 nHitTest:表示命中测试码,使用此参数可以确定光标在窗口内的位置。若命中测 试码为HTERROR,且message是鼠标键按下消息时,则MessageBeep成员函数会被调用。 message:表示鼠标消息。 返回值:若光标需要进一步处理则返回FALSE,否则返回TRUE。比如父窗口若返 回TRUE,则对光标的进一步处理就停止。 ②、通过ON_MESSAGE宏添加自定义的函数处理WM_SETCURSOR消息,原型如下 afx_msg LRESULT f(WPARAM wParam, LPARAM lParam) 函数名是由程序员自已命名的 lParam:低位字段是命中测试码,高位字段表示鼠标消息。 wParam:包含此光标的窗口的句柄。 返回值:若光标需要进一步处理则返回0,否则返回非零值。比如父窗口若返回TRUE, 则对光标的进一步处理就停止。 5、相关函数及原型 ①、SetCursor函数:HCURSOR ::SetCursor(HCURSOR hCursor); 作用:设置光标的形状,只有当前设置的新光标与以前的光标不同时才会设置光标, 否则函数立即返回。 HCROSR是一个光标句柄。 hCursor:需设置的光标句柄,光标必须由CreateCursor创建、或由LoadCursor、 LoadImage函数加载,若该参数为NULL,则表示从屏幕上移除光标。 返回值:返回前一个光标的句柄,若以前没有光标则返回NULL ②、LoadCursor函数: HCURSOR ::LoadCursor(HINSTANCE hInstance,LPCTSTR lpCursorName); 作用:加载光标资源 hInstance:要加载光标的实例的句柄。 lpCursorName:要加载的光标的名称,此参数的值也可是使用MAKEINTRESOURCE 宏将一个整数值转换为包含资源名称的字符串,此字符串是由整数的低位字中指定的值和高位字中的零组成。 返回值:或函数成功则返回加载的光标的句柄,若函数失败则返回NULL。 若要获得系统预定义的鼠标指针形式则第一个形参必须设置为NULL,第二个形参可 取值及效果如下表 表xxx:LoadCursor函数pCursorName形参可取的值 值 MAKEINTRESOURCE宏 形状 说明 IDC_ARROW MAKEINTRESOURCE(32512) 标准箭头 IDC_HELP MAKEINTRESOURCE(32651) 帮助光标 IDC_IBEAM MAKEINTRESOURCE(32513) 工字形光标 IDC_SIZEWE MAKEINTRESOURCE(32644) 指向东西的双向箭头 IDC_UPARROW MAKEINTRESOURCE(32516) 垂直箭头 IDC_WAIT MAKEINTRESOURCE(32514) 沙漏形 IDC_NO MAKEINTRESOURCE(32648) IDC_SIZENWSE MAKEINTRESOURCE(32642) 指向西北和东南的双向箭头 IDC_SIZENS MAKEINTRESOURCE(32645) 指向南北的双向箭头 IDC_HAND MAKEINTRESOURCE(32649) 手形光标 IDC_APPSTARTING MAKEINTRESOURCE(32650) 标准箭头和小沙漏 IDC_CROSS MAKEINTRESOURCE(32515) 十字光标 IDC_SIZENESW MAKEINTRESOURCE(32643) 指向东北和西南的双向箭头 IDC_SIZEALL MAKEINTRESOURCE(32646) 四角箭头 示例:LoadCursor(NULL,IDC_HELP);或LoadCursor(NULL,MAKEINTRESOURCE(32651)) ③、MAKEINTRESOURCE宏:LPTSTR MAKEINTRESOURCE(WORD wteteger); 作用:将整数值wteteger转换为资源名称的字符串,返回值是低位字中的指定值和高位字中的零。 ④、GetCursor函数:HCURSOR ::GetCursor(); 作用:返回当前光标的句柄,若没有光标则返回NULL。 ⑤、ShowCursor函数:int ::ShowCursor(BOOL bShow); 作用:显示或隐藏光标。该函数根据内部的显示计数器来决定是否显示光标,只有 当计数器的值大于或等于0时才会显示光标。若安装了鼠标,则初始计数器的值为0,若未安装鼠标,则初始计数器的值为-1。 bShow:若该参数为TRUE则显示计数增加1,若为FALSE则显示计数减1。因此 每调用一次::ShowCursor(TRUE)则计数器加1,每调用一次::ShowCursor(FALSE)则计数器减1。 ⑥、LoadImage、CreateCursor、GetCursorInfo函数暂不讲解。 二、光标的位置 需用到的函数 1、SetCursorPos函数:BOOL ::SetCursorPos(int x , int y); 作用:将光标移动到指定的位置(屏幕坐标)。使用该函数可实现由键盘来移动光标。 返回值:若函数成功则返回非零值,否则返回零。 2、GetCursorPos函数:BOOL ::GetCursorPos(LPPOINT lpPoint); 作用:获取鼠标光标的当前位置(屏幕坐标),并将其坐标值存储于lpPoint中。 返回值:若函数成功则返回非零值,否则返回零。 3、ClipCursor函数:BOOL ::ClipCursor(const RECT * lpRect); 作用:把光标限制在屏幕上的矩形区域内,若光标位置位于矩形之外(比如由 SetCursorPos设置于矩形之外),则系统会自动调整以使其保持在矩形之内。 lpRect:指向限制光标的矩形区域的指针,若此参数为NULL,则光标可自由移动到 屏幕上的任意位置。光标是一种共享资源,因此若某个程序限制了光标,而其他程序又要使用光标,则必须使用该函数释放光标。 返回值:若函数成功则返回非零值,否则返回零。 4、GetClipCursor函数:BOOL ::GetClipCursor(LPRECT lpRect); 作用:获取光标的约束矩形(屏幕坐标),并将其值存储于lpRect中。 返回值:若函数成功则返回非零值,否则返回零。 示例:设置光标形状(类光标不为NULL的情形) #include class A :public CWinApp { public: BOOL InitInstance(); }; class B :public CFrameWnd {public: B(){ Create(NULL, _T(\"HYONG\"), WS_OVERLAPPEDWINDOW, CRect(100, 100, 500, 500)); } afx_msg void OnLButtonDown(UINT nFlags, CPoint point); afx_msg void OnMouseMove(UINT nFlags, CPoint point); DECLARE_MESSAGE_MAP() }; BOOL A::InitInstance() { m_pMainWnd = new B(); m_pMainWnd->ShowWindow(m_nCmdShow); m_pMainWnd->UpdateWindow(); return TRUE; } A ma; BEGIN_MESSAGE_MAP(B, CFrameWnd) ON_WM_LBUTTONDOWN() ON_WM_MOUSEMOVE() END_MESSAGE_MAP() void B::OnLButtonDown(UINT nFlags, CPoint point) { SetCursor(LoadCursor(NULL, IDC_WAIT)); } //按下鼠标左键时设置的光标形状 void B::OnMouseMove(UINT nFlags, CPoint point){ SetCursor(LoadCursor(NULL, IDC_CROSS)); }//鼠标移动时设置的光标形状 程序运行结果及说明: 1、若按下鼠标左键不放并且不移动时,光标会变为IDC_WAIT的形状;若按下鼠标左键且不移动,然后释放鼠标左键,则光标会变为类光标的形状;若按住左键不放且快速移动,则会出现以下情形2的情形。 2、当用户在客户区快速移动光标时,光标会在十字形和类光标的形状(此时为标准箭头)之间快速的交替变换。 示例:设置光标形状(类光标为NULL的情形) #include class A :public CWinApp { public: BOOL InitInstance(); }; class B :public CFrameWnd {public: B() { CString s = AfxRegisterWndClass (CS_HREDRAW | CS_VREDRAW, 0, //类光标为NULL。 (HBRUSH)GetStockObject(WHITE_BRUSH)); CreateEx(0, s, \"B\", WS_OVERLAPPEDWINDOW, CRect(100, 100, 300, 300), 0, 0, 0); } afx_msg void OnLButtonDown(UINT nFlags, CPoint point); DECLARE_MESSAGE_MAP() }; BOOL A::InitInstance() { m_pMainWnd = new B(); m_pMainWnd->ShowWindow(m_nCmdShow); m_pMainWnd->UpdateWindow(); return TRUE; } A ma; BEGIN_MESSAGE_MAP(B, CFrameWnd) ON_WM_LBUTTONDOWN() END_MESSAGE_MAP() void B::OnLButtonDown(UINT nFlags, CPoint point) { SetCursor(LoadCursor(NULL, IDC_WAIT));} 程序运行结果及说明:因注册窗口时类光标被设置为NULL,因此,当在客户区按下鼠标左键后,光标的形状会变为IDC_WAIT,此时再移动鼠标光标的形状也不再会改变(因为此时类光标为NULL) 示例:通过WM_SETCURSOR处理函数设置光标形状 #include class A :public CWinApp { public: BOOL InitInstance(); }; class B :public CFrameWnd {public: B(){ Create(NULL, _T(\"HYONG\"), WS_OVERLAPPEDWINDOW, CRect(100, 100, 500, 500)); } afx_msg BOOL OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message); DECLARE_MESSAGE_MAP() }; BOOL A::InitInstance() { m_pMainWnd = new B(); m_pMainWnd->ShowWindow(m_nCmdShow); m_pMainWnd->UpdateWindow(); return TRUE;} A ma; BEGIN_MESSAGE_MAP(B, CFrameWnd) ON_WM_SETCURSOR() END_MESSAGE_MAP() BOOL B::OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message){ CRect cr;CPoint cp; GetClientRect(&cr); GetCursorPos(&cp); //返回光标的位置(相对于屏幕坐标) ScreenToClient(&cp); if(nHitTest==HTCAPTION) //若光标位于标题栏 SetCursor(LoadCursor(NULL, IDC_CROSS)); if(nHitTest==HTCLIENT) //若光标位于标题栏 if(cp.y 在处理WM_SETCURSOR消息时设置了光标的形状,则注册窗口时设置的类光标形状 会被忽略。因此在客户区快速移动光标时不会出现光标交替变换的情形。 把光标移至标题栏时,光标的形状为IDC_CROSS;把光标移到客户区上半部分时,光 标的形状为IDC_HAND;把光标移到客户区下半部分时,光标的形状为IDC_NO;当在客户区按下鼠标左键不放且不移动时,光标的形状为IDC_SIZEALL。 5.5 鼠标停留与离开消息 (WM_MOUSELEAVE和WM_MOUSEHOVER消息) 一、WM_MOUSEHOVER和WM_MOUSELEAVE消息 1、鼠标停留消息WM_MOUSEHOVER和鼠标离开消息WM_MOUSELEAVE系统不会自动发布,需由程序员调用::TrackMouseEvent函数进行发布。 2、因为WM_MOUSEHOVER和WM_MOUSELEAVE消息不是由系统自动发布的,因此系统没有默认的处理这两个消息的消息处理函数,程序员处理这两个消息时,需要使用ON_MESSAGE宏。 3、一旦生成了WM_MOUSEHOVER和WM_MOUSELEAVE消息,则对此消息的跟踪就停止,也就是说这两个消息不会持续的发布,若要使这两个消息再次发布需要再次调用::TrackMouseEvent函数。 4、WM_MOUSEHOVER消息: ①、该消息并不是指鼠标在原地原封不动时才会产生此消息,只要鼠标在指定的矩形内停留指定的时间就会产生此消息。 ②、矩形的范围和停留时间可通过分别使用以下参数调用::SystemParametersInfo函数来查看或设置:SPI_GETMOUSEHOVERWIDTH、SPI_GETMOUSEHOVERHEIGHT、SPI_GETMOUSEHOVERTIME、SPI_SETMOUSEHOVERWIDTH、SPI_SETMOUSEHOVERHEIGHT、SPI_SETMOUSEHOVERTIME ③、wParam参数:指定了各虚拟键的状态,如下表 MK_CONTROL(0x0008) MK_SHIFT(0x0004) 按下CTRL键 按下SHIFT键 MK_LBUTTON(0x0001) MK_RBUTTON(0x0002) 按下鼠标左键 按下鼠标右键 MK_MBUTTON(0x0010) 按下鼠标中键 MK_XBUTTON1(0x0020) 第一个X按钮关闭 MK_XBUTTON2(0x0040) 第二个X按钮关闭 ④、lParam参数:低位字表示光标的x坐标,高位字表示y坐标(相对于客户区) ⑤、返回值:若处理了此消息,则应返回零。 5、WM_MOUSELEAVE消息:该消息的wParam和lParam参数都未使用,必须为零。若处理了此消息,则应返回零。 二、相关函数及原型 1、TrackMouseEvent函数:BOOL ::TrackMouseEvent(LPTRACKMOUSEEVENT lpEventTrack); ①、作用:当鼠标离开窗口或停留在窗口上指定时间时发布消息。 ②、lpEventTrack:是一个指向TRACKMOUSEEVENT结构体的指针,详见下文。 ③、返回值:若函数成功则返回非零值,否则返回零。 ④、该函数发布的消息如下表 消息 WM_MOUSEHOVER WM_MOUSELEAVE WM_NCMOUSEHOVER WM_NCMOUSELEAVE 消息。 当鼠标离开由TrackMouseEvent函数指定的窗口的客户区域时生成此消息。 与以上两消息类似,只是针对于非客户区。 说明 当鼠标停留在客户区上由TrackMouseEvent函数指定的一段时间时生成此 2、TRACKMOUSEEVENT结构体: typedef struct tagTRACKMOUSEEVENT{ DWORD cbSize; DWORD dwFlags; HWND hwndTrack; DWORD dwHoverTime; }TRACKMOUSEEVENT, * LPTRACKMOUSEEVENT ①、cbSize:TRACKMOUSEEVENT结构体的大小,以字节为单位。 ②、dwFlags:表示调用者想要执行的操作。可取以下值 dwFlags的取值 值 TME_CANCEL(0x80000000) 说明 想要取消的跟踪请求,使用时还必须同时指定要取消的跟踪类型,比如想要取消停留跟踪,调用者必须传递TME_CANCEL和TME_HOVER标志。 注册接收WM_MOUSEHOVER消息 注册接收WM_MOUSELEAVE消息 形式向非客户区域提供通知。 TME_QUERY(0x40000000) 允许系统使用当前的::TrackMouseEvent设置填写TRACKMOUSEEVENT结构。 TME_HOVER(0x00000001) TME_LEAVE(0x00000002) TME_NONCLIENT(0x00000010) 以WM_NCMOUSEHOVER和WM_NCMOUSELEAVE消息的 ③、hwndTrack:需要跟踪的窗口的句柄。 ④、dwHoverTime:以毫秒为单位的光标停留时间,光标必须停留这么久才会发送 WM_MOUSEHOVER消息,该值可以使用默认值HOVER_DEFAULT(400毫秒) 3、SystemParametersInfo函数: BOOL ::SystemParametersInfo (UINT uiAction, UINT uiParam, PVOID pvParam, UINT fWinIni) ①、作用:用于检索和设置系统参数。此函数可以检索和设置大量的系统参数,因此是一个功能强大的函数。 ②、uiAction:要检索或设置的系统参数,系统参数分为以下几类:辅助功能参数、桌面 参数、图标参数、输入参数、菜单参数、电源参数、屏幕保护程序参数、超时参数、UI效果参数、窗口参数。其中每一类系统参数都拥有众多的值,因此此参数的取值有非常的多,本小节仅列出与鼠标有关的参数值,其取值见下表。 ③、uiParam:此参数的使用取决于uiAction使用的系统参数,若未使用该参数则必须设 置为0。 ④、pvParam:此参数的使用取决于uiAction使用的系统参数,若未使用该参数则必须设 置为NULL。 ⑤、fWinIni:指定是否更新系统配置文件,若更改则把WM_SETTINGCHANGE消息广 播到所有顶层窗口,以通知其更改;若不想更改配置文件,则此参数可以为零,也可是以下的一个或多个值: SPIF_UPDATEINIFILE:把新的系统参数写入配置文件。 SPIF_SENDCHANGE:更新配置文件后,广播WM_SETTINGCHANGE消息 SPIF_SENDWININICHANGE:与SPIF_SENDCHANGE相同。 ⑥、返回值:若函数成功则返回非零值,否则返回零。 SystemParametersInfo函数uiAction参数取值 值 说明 SPI_GETMOUSEHOVERHEIGHT 检索鼠标指针生成WM_MOUSEHOVER消息的矩形的高度 (以像素为单位),此时pvParam参数必须是一个指向UINT的变量以接收检索到的高度值。 SPI_GETMOUSEHOVERWIDTH 检索鼠标指针生成WM_MOUSEHOVER消息的矩形的宽度 (以像素为单位),此时pvParam参数必须是一个指向UINT的变量以接收检索到的宽度值。 SPI_GETMOUSEHOVERTIME 检索鼠标指针生成WM_MOUSEHOVER消息的停留时间(以 毫秒为单位),此时pvParam参数秘须是一个指向UINT的变量以接收检索到的时间值。 SPI_SETMOUSEHOVERHEIGHT SPI_SETMOUSEHOVERWIDTH SPI_SETMOUSEHOVERTIME 设置鼠标指针生成WM_MOUSEHOVER消息的矩形的高度(以像素为单位),此时uiParam参数为设置的新的高度值。 设置鼠标指针生成WM_MOUSEHOVER消息的矩形的宽度(以像素为单位),此时uiParam参数为设置的新的宽度值。 设置鼠标指针生成WM_MOUSEHOVER消息的停留时间(以毫秒为单位),此时uiParam参数为设置的新的时间值,此设 置仅在TrackMouseEvent函数的形参中的dwHoverTime成员使用HOVER_DEFAULT时起作用。 SPI_SETDOUBLECLICKTIME 设置鼠标双击时间为uiParam参数的值,若值大于5000毫秒,则系统把双击时间设置为5000毫秒。还可使用SetDoubleClickTime函数来设置双击时间。 SPI_SETDOUBLECLKHEIGHT SPI_SETDOUBLECLKWIDTH SPI_SETMOUSEBUTTONSWAP 设置鼠标双击矩形的高度为uiParam参数的值。要检索该值需使用SM_CYDOUBLECLK调用GetSystemMetrics函数。 设置鼠标双击矩形的宽度为uiParam参数的值。要检索该值需使用SM_CXDOUBLECLK调用GetSystemMetrics函数。 交换鼠标左右键的意义,若uiParam参数为TRUE表示交换左右键,要检索此消息需使用SM_SWAPBUTTON调用GetSystemMetrics函数。 示例:发布WM_MOUSELEAVE和WM_MOUSEHOVER消息 #include class A :public CWinApp { public: BOOL InitInstance(); }; class B :public CFrameWnd {public: B(){ Create(NULL, _T(\"HYONG\"), WS_OVERLAPPEDWINDOW, CRect(100, 100, 500, 500)); } afx_msg void OnLButtonDown(UINT nFlags, CPoint point); afx_msg void OnMouseMove(UINT nFlags, CPoint point); afx_msg LRESULT f(WPARAM wParam, LPARAM lParam); afx_msg LRESULT g(WPARAM wParam, LPARAM lParam); DECLARE_MESSAGE_MAP() }; BOOL A::InitInstance() { m_pMainWnd = new B(); m_pMainWnd->ShowWindow(m_nCmdShow); m_pMainWnd->UpdateWindow(); return TRUE; } A ma; BEGIN_MESSAGE_MAP(B, CFrameWnd) ON_WM_LBUTTONDOWN() ON_WM_MOUSEMOVE() ON_MESSAGE(WM_MOUSELEAVE, &B::f) //使用f函数处理WM_MOUSELEAVE消处 ON_MESSAGE(WM_MOUSEHOVER, &B::g) END_MESSAGE_MAP() void B::OnLButtonDown(UINT nFlags, CPoint point) { UINT i=0; SystemParametersInfo(SPI_SETMOUSEHOVERTIME, 2000, 0, 0); //设置光标的停留时间为毫秒 SystemParametersInfo(SPI_GETMOUSEHOVERTIME, 0, &i, 0); //检索光标的停留时间。 TRACE(\"\\n,%d,\\n\",i); } afx_msg void B::OnMouseMove(UINT nFlags, CPoint point) { TRACKMOUSEEVENT tme; tme.cbSize = sizeof(tme); tme.dwFlags = TME_LEAVE | TME_HOVER; //需要发布WM_MOUSELEAVE和WM_MOUSEHOVER消息 tme.hwndTrack = m_hWnd; tme.dwHoverTime = HOVER_DEFAULT; //使用默认的光标停留时间(400毫秒) TrackMouseEvent(&tme); } //使用该函数发布WM_MOUSELEAVE和WM_MOUSEHOVER消息 afx_msg LRESULT B::f(WPARAM wParam, LPARAM lParam){ TRACE(\"\\nLEAVE\\n\"); //用于验证 return 0; }//若处理了消息,则应返回零。 afx_msg LRESULT B::g(WPARAM wParam, LPARAM lParam){ TRACE(\"\\nHOVER\\n\");//用于验证 return 0; }//若处理了消息,则应返回零。 程序运行结果及说明: 当鼠标离开客户区时输出LEAVE(输出的内容位于编译器的输出窗口内), 当鼠标在客户区移动后并停留400毫秒输出“HOVER”。 当按下鼠标左键后,再移动鼠标,此时鼠标需要在客户区停留2000毫秒才会输出 “HOVER”,在这之后,移动鼠标后都需要在客户区停留2000毫秒才会输出“HOVER”, 每输出一次“HOVER”都需要调用一次TrackMouseEvent函数发布WM_MOUSEHOVER 消息,也就是说在本示例,想要输出一次“HOVER”都要移动一次鼠标,然后在客户区停留指定的时间。 5.6 与鼠标有关的其他函数 1、GetSystemMetrics函数:int ::GetSystemMetrics(int nIndex); 作用:检索系统的配置设置。该函数可检索非常多的信息,比如窗口标题栏按钮的 宽度、图标的宽度、显示屏的宽度、以及一些鼠标信息等。 返回值:若函数成功则返回非零值,否则返回零。 nIndex:需要检索的系统配置设置,因该参数取值非常多,下表仅列出与鼠标有关的 取值。 值 SM_CMOUSEBUTTONS SM_CXDOUBLECLK SM_CYDOUBLECLK SM_MOUSEPRESENT SM_SWAPBUTTON 说明 鼠标的按钮数,若未安装鼠标则为0。 双击时矩形的宽度,要设置该宽度需使用 SPI_SETDOUBLECLKWIDTH调用SystemParametersInfo函数。 双击时矩形的高度,要设置该高度需使用 SPI_SETDOUBLECLKHEIGHT调用SystemParametersInfo函数。 若安装了鼠标则返回非零值,否则为0。 若交换了鼠标左右键的含义则为非零值,否则为0。 2、SetDoubleClickTime函数:BOOL ::SetDoubleClickTime(UINT uInterval); 作用:设置鼠标的双击时间为uInterval(以毫秒为单位),若设置为0,则使用默认时间500毫秒,若设置的值大于5000毫秒,则系统会把该值设置为5000毫秒。 返回值:若函数成功则返回非零值,否则返回零。 3、GetDoubleClickTime函数:UINT ::GetDoubleClickTime(); 作用:返回鼠标的双击时间(以毫秒为单位)。 第6章 键盘 6.1击键消息 一、基础 1、键盘也是共享的全局硬件资源。 2、输入焦点:把能接收到键盘消息的窗口称为具有输入焦点的窗口,任何时候只有一个窗口拥有输入焦点,系统使用WM_SETFOCUS和WM_KILLFOCUS消息表示窗口焦点的接收和失去(有关焦点的更多内容参阅相关章节)。以下情形的窗口拥有输入焦点: 活动窗口,活动窗口总是最上层的窗口,当活动窗口有标题栏时,windows会加这显 示该标题栏。 活动窗口的子窗口(比如按钮、单选按钮等)。 3、发送键盘消息:系统总是将键盘消息发送给具有输入焦点的窗口,若拥有焦点的窗口是子窗口,则键盘消息发送给子窗口且此后会停止给主窗口发送消息。 4、键盘消息分为击键消息和字符消息。 对于可显示字符的击键操作,系统会产生击键消息同时还会产生字符消息,比如对 标示为“A”的一次击键操作,会产生键按下和键释放两次击键操作,同时还会产生一个可显示的字符“A”或“a”(具体显示什么字符还要视CTRL、SHIFT等键是否被按下)。 键盘上的键不一定都会产生一个字符,比如SHIFT键、光标移动键等都不会产生字 符,对这些按键系统将只产生击键消息而无字符消息。 5、可忽略的键盘消息(以下的消息系统已经实现了相应的功能) 属于系统功能的击键操作(即ALT键和F10键) 一些快捷键,快捷键一般是CTRL键与字母键的组合(比如常用的CTRL+C复制功能 等) 对话框中的键盘接口(对话框详见相关章节) 编辑控件中的一些键盘操作(编辑控件详见相关章节) 二、击键消息 1、击键消息:表示键是被按下还是释放的消息,有以下几类击键消息 WM_KEYDOWN:键按下 WM_KEYUP:键释放 WM_SYSKEYDOWN:系统键按下 WM_SYSKEYUP:系统键释放。 2、系统键消息: ①、指的是Alt和F10两个键,这两个键对于windows来讲有其特定的意义,ALT键用于处理一些快捷操作(比如ATL+TABLE、ALT+ES、ALT+F4等),F10用作菜单选择的快捷键,比如按下F10之后再按F,通常会拉下“文件”菜单。 ②、Alt和F10中的任意一个键被按下和释放时,它们分别产生WM_SYSKEYDOWN和WM_SYSKEYUP消息。 ③、若ALT键按着的同时别的键也被按下了,则仍然产生WM_SYSKEYDOWN和WM_SYSKEYUP消息。 ④、对于WM_SYSKEYDOWN和WM_SYSKEYUP消息,应该交由windows进行处理,程序员通常不需处理这些消息,若程序员处理了这些消息,则处理完之后,最好调用::DefWindowProc函数以对系统键进行默认处理,以避免影响windows对它的处理,若程序最终未调用::DefWindowProc函数处理WM_SYSKEYDOWN和 WM_SYSKEYUP消息,则ALT+TAB、ALT+F4等系统命令消息会停止工作。 3、连续按键与同时按下多个键的行为 ①、若按住一个键不放,会被认为发生了一次连续的按键行为(自动重复行为),此时系统会发送一连串的WM_KEYDOWN或WM_SYSKEYDOWN消息,当键最终被释放时,产生一个WM_KEYUP或WM_SYSKEYUP消息,也就是说此时键按下消息有多个,键释放消息只有一个。 ②、当一个键被按着的同时,另一个键被按下并释放,则新产生的WM_KEYDOWN和WM_KEYUP消息会把先前的WM_KEYDOWN和WM_KEYUP消息分开。 ③、示例:比如按着A不放,然后按下F并释放,会按如下顺序产生键盘消息 消息 按键 WM_KEYDOWN 键A WM_KEYDOWN 键A WM_KEYDOWN 键A …. … WM_KEYDOWN 键F WM_KEYUP 键F 停止发送WM_KEYDOWN消息,直至键A被释放 WM_KEYUP 键A 4、虚拟键代码 ①、键盘都拥有很多个按键,早期的键盘使用的是硬编码形式表示用户按下的是哪一个键,比如数值16表示Q键,17表示W键等,其中的数值被称为扫描码,硬编码基于键盘的自然布局,因此与设备的相关性比较大。 ②、Windows使用虚拟键代码来表示用户按下了键盘上的哪一个键,虚拟键代码使使用与设备无关,比如虚拟键代码VK_TAB表示用户按下了键盘上的TAB键。 ③、虚拟键代码被定义为winuser.h文件中的常量值,这些值以VK_开头,下表列出了常用了虚拟键代表与其等效的16进制值。 虚拟键代码 16进制值 虚拟键代码 对应键 0x01 0x02 0x03 0x04 0x08 0x09 0x0C 0x0D 0x10 0x11 0x12 0x13 0x14 0x1B 0x20 0x21 0x22 0x23 0x24 0x25 0x26 0x27 0x28 0x29 0x2A 0x2B 0x2C 0x2D 0x2E 0x2F 0x30~39 0x41~5A 0x5B 0x5C 0x5D 0x60~69 0x6A 0x6B 0x6C 0x6D 0x6E 0x6F 0x70~87 0x90 0x91 0xBA 0xBB 0xBC VK_LBUTTON VK_RBUTTON VK_CANCEL VK_MBUTTON VK_BACK VK_TAB VK_CLEAR VK_RETURN VK_SHIFT VK_CONTROL VK_MENU VK_PAUSE VK_CAPITAL VK_ESCAPE VK_SPACE VK_PRIOR VK_NEXT VK_END VK_HOME VK_LEFT VK_UP VK_RIGHT VK_DOWN VK_SELECT VK_PRINT VK_EXECUTE VK_SNAPSHOT VK_INSERT VK_DELETE VK_HELP 无 无 VK_LWIN VK_RWIN VK_APPS VK_NUMPAD0~ VK_NUMPAD9 VK_MULTIPLY VK_ADD VK_SEPARATOR VK_SUBTRACT VK_DECIMAL VK_DIVIDE VK_F1~VK_F24 VK_NUMLOCK VK_SCROLL VK_OEM_1 VK_OEM_PLUS VK_OEM_COMMA 鼠标左键 鼠标右键 Ctrl+Break(中断),Break键一般位于F12的右侧的三个按键中,这是唯一标识同时按下两个键的虚拟键代码。 鼠标中键 退格键 Tab键 Clear键(数字锁定键关闭时的数字键5) 回车键 Shift键 Ctrl键 Alt键 Pause键(位于F12右侧) 大写锁定键(Caps Lock) Esc键 空格键 Page Up键 Page Down键 End键 Home键 左箭头 上箭头 右箭头 下箭头 拥有这三个键的键盘很少见 Print Screen键 Inset键 Del键 主键盘上的0~9(与ASCII码的数字值相同) A~Z键(与ASCII码的大写A到Z的值相同) 左windows键 这三个键由微软Natural Keyboard键盘及其兼容键盘产生 右windows键 Application键 数字锁定键打开时,小键盘上的数字键0~9 数字键区的* 数字键区的+ 数字键区的− 数字键区的点\".\" 数字键区的/ F1~F24键(大多数键盘只有F1~F12键) 数字锁定键 Scroll Lock键(位于F12右侧) 对于美国标准键盘“;:”键。 对于任何国家/地区,“+”键 对于任何国家/地区,“,”键 以下的虚拟键代码表示的键会依键盘不同而不同。 0xBD 0xBE 0xBF 0xC0 0xDB 0xDC 0xDD 0xDE VK_OEM_MINUS VK_OEM_PERIOD VK_OEM_2 VK_OEM_3 VK_OEM_4 VK_OEM_5 VK_OEM_6 VK_OEM_7 对于任何国家/地区,“-”键 对于任何国家/地区,“.”键 对于美国标准键盘“/?”键。 对于美国标准键盘“~”键 对于美国标准键盘“[{”键 对于美国标准键盘“\\ |”键 对于美国标准键盘“]}”键 对于美国标准键盘“单引号/双引号”键 以下虚拟键代码仅可在以下函数中使用 GetKeyState、GetKeyboardState、GetAsyncKeyState、SetKeyboardState、MapVirtualKey 0xA0 0xA1 0xA2 0xA3 0xA4 0xA5 VK_LSHIFT VK_RSHIFT VK_LCONTROL VK_RCONTROL VK_LMENU VK_RMENU Shift键(左侧) Shift键(右侧) Ctrl键(左侧) Ctrl键(右侧) Alt键(左侧) Alt键(右侧) 三、处理击键消息 1、WM_KEYDOWN、WM_KEYUP、WM_SYSKEYDOWN、WM_SYSKEYUP消息 1)、wParam:表示虚拟键代码 2)、lParam:32位的lParam参数被分成了如下图所示6个字段 上下文代码 扩展键标记 键的先前状态 31 30 29 28 27 26 25 24 23 … 16 15 … 00 转换状态 保留,未使用 重复计数 OEM扫描码 键盘消息lParam参数取值 ①、重复计数: 对于WM_KEYDOWN或WM_SYSKEYDOWN消息,重复计数通常为1,对于 WM_KEYUP或WM_SYSKEYUP消息,则始终为1。 若击键的的速度过快,以致于程序跟不上输入的速率来处理击键消息,则系统会 把两个或多个WM_KEYDOWN或WM_SYSKEYDOWN消息合并为一个,且相应的增加重复计数。也就是说此时重复计数大于1,或者说当重复计数大于1时,此时连续击键的速度快于程序的处理能力。 通常,程序都会忽略重复计数,这样就将合并的键按下消息作 为一个消息来处 理(此时重复计数大于1),这样做可以防止键盘溢出的发生。键盘溢出是指用户释放键以后程序仍还在响应按键消息的情形。 ②、OEM扫描码:是由键盘硬件产生的代码,现在几乎不再需要该值了。 ③、扩展键标记:若是扩展键则为1,否则为0。对于IBM增强型101或102键盘,扩展 键标记指的是:键盘右侧的Ctrl和Alt键;键盘主体与数字小键盘之间的Home、End、Insert、Delete、Page Up、Page Down及箭头键;数字小键盘中的斜线和回车键。也就是说若键盘消息来自以上按键则扩展键标记为1,若是其他按键则扩展键标记为0。 ④、上下文代码:若击键的同时按下了Alt键则为1,否则为0。WM_SYSKEYUP和 WM_SYSKEYDOWN消息,此位始终为1,对于WM_KDYUP和WM_KEYDOWN消息,此位始终为0(注意:带有Alt的按键产生的是WM_SYSKEYUP和WM_SYSKEYDOWN消息)。上下文代码有以下两种例外情形: 若活动窗口最小化了,则所有的击键会产生WM_SYSKEYUP和 WM_SYSKEYDOWN消息,若此时Alt键未被按下,则上下文代码为0。 对于某些非英语键盘上的一些需要通过Shift、Ctrl或Alt键与另一键的组合产生 的字符,此时会把上下 文代码设置为1,而此时产生的消息不是系统键消息。 ⑤、键的先前状态:若键之前是释放状态,则键的先前状态为0,若键之前是按下状态, 则为1。WM_KEYUP和WM_SYSKEYUP消息此位始终为1,WM_KEYDOWN和WM_SYSKEYDOWN消息,此位可能为0或1,当为1时表明产生了自动重复输入,此时的消息为重复击键产生的第二个或后续发出的消息。比如若按下Shit键一段时间,则产生的消息顺序如下表 产生的消息 WM_KEYDWON WM_KEYDWON WM_KEYDWON …… WM_KEYUP 虚拟键代码 VK_SHIFT VK_SHIFT VK_SHIFT …… VK_SHIFT 键的先前状态 0 1 1 1 1 ⑥、转换状态:若键正被按下,则为0,若键正被释放,则为1。WM_KEYDWON和 WM_SYSKEYDOWN消息,此位始终为0,WM_KEYUP和WM_SYSKEYUP此位始终为1。 2、对于WM_KEYDWON消息,MFC提供了上应的消息处理函数,其名称为OnKeyDown,对于其他键盘消息都有类似的函数,他们的原型都是相同的,以下为方便说明,使用函数名OnXXX代替各键盘消息处理函数。 afx_msg void OnXXX(UINT nchar, UINT nRepCnt, UINT nFlags) nChar:表示虚拟键代码(见上文) nRepCnt:表示重复计数(见上文) nFlags:是一个标志位,其每位的含义见图(具体意义见上文) 上下文代码 扩展键标记 键的先前状态 15 14 13 12 11 10 09 08 07 … 00 转换状态 保留,未使用 OEM扫描码 键盘消息lFlags参数取值 四、转义状态 1、转义键:是指Shift、Ctrl、Alt键。 2、切换键:是指Caps Lock、Num Lock、Scroll Lock键。 3、在编写程序时有时需要知道转义键或切换键是否被按下,而这些键的状态信息并未被编入键盘消息之中,因此系统提供了::GetKeyState函数用于获取这些键的状态信息。 4、GetKeyState函数:SHORT ::GetKeyState(int nVirtKey); ①、作用:检索指定的单个虚拟键的状态。 ②、nVirtKey:需要获取状态的虚拟键代码。若所需的虚拟键代码是字母(A~Z)或数字(0~9),则此值应是该字符的ASCII码值。 ③、返回值:返回值指示虚拟键的状态,具体如下 若高位为1(即值为负),表示键处于按下状态,否则键处于释放状态 若低位为1,则表示键处理切换状态,比如若Caps Lock键处于切换状态,则键盘上 的切换指示灯(若有的话)会亮起。 ④、说明: 虚拟键代码VK_LSHIFT、VK_RSHIFT、VK_LCONTROL、VK_RCONTROL、 VK_LMENU、VK_RMENU可用于此函数的参数,以用于区分左右。 可以使用VK_LBUTTON、VK_RBUTTON、VK_MBUTTON来判断鼠标按钮的 状态。 该函数返回的信息只在键盘消息能从消息队列中被检索到之后才有效,因 此::GetKeyState函数不应用在键盘消息处理程序之外的地方,在这种情况下可使用::GetAsyncKeyState函数,该函数与GetKeyState函数有一些不同,本文暂不讲解 函数BOOL ::GetKeyboardState(PBYTE lpKeyState)可获取256个虚拟键的状态信 息,并把结果存储在lpKeyState中。类似,BOOL ::SetKeyboardState(PBYTE lpKeyState)函数,用于设置256个虚拟键的状态信息。 ⑤、使用::GetKeyState函数 使用该函数判断键是否处于切断状态时应屏蔽掉除最低位之外的其他所有位,因为此 时的最高位仍然会表示键是否被按下。比如判断Num Lock键是否处于切换状态应使用以下代码 ::GetKeyState(VK_NUMLOCK)&0x01 //若结果为真则表示Num Lock被打开。 使用以下代码可以测试是否按下了组合键 nChar == VK_RIGHT)&&(::GetKeyState(VK_SHIFT)<0) 若Shift键和右箭头键被同时按下,则结果为真。 6.2 字符消息 一、字符消息基础 1、虚拟键代码和字符的关系 按下键盘上的一个键通常应能产生一个字符,虚拟键代码是标示键盘按下的是哪一个键,而字符,可能按下一个键就产生一个字符,也可能多个按键的组合才会产生一个字符,也可能按下一个键不产生字符。比如按下“A”键,若Shift未按下,此时产生小写字符'a',若同时按下了Shift,则产生一个大写字符'A',也就是说大写字符'A'是由Shift+A两个键组合产生的。由此可见,使用虚拟键代码并不能表示按键产生的是什么字符。 2、按下一个按键产生的字符还与键盘的布局有关系,比如使用美国布局键盘的用户,按下Shift+0会产生一个右括号,而荷兰键盘布局的用户则会产生一个撇号。 3、系统提供的::TranslateMessage函数的作用就是用于把击键消息转换为字符消息的。在MFC的消息循环中会自动调用::TranslateMessage函数。 二、处理字符消息 1、字符消息分为如下表所示四类 字符消息的分类 非系统字符 系统字符 字符 WM_CHAR WM_SYSCHAR 死字符 WM_DEADCHAR WM_SYSDEADCHAR 2、四类字符消息中的lParam参数与击键消息中的lParam参数相同,但wParam参数不再是虚拟键代码,而是表示的是ANSI或Unicode字符代码。 3、每一个WM_CHAR消息都包含ANSI字符集或Unicode字符集中的符号,若窗口是使用RegisterClassA注册的,则得到ANSI字符,若窗口是使用RegisterClassW注册的,则得到Unicode字符。使用如下函数,可判断当前窗口是否使用Unicode字符集 BOOL ::IsWindowUnicode(HWND hWnd) 若返回值不为零则表示使用的是Unicode字符集,否则使用的是ANSI字符集。 4、MFC处理字符消息的处理函数其函数原型如下: afx_msg void Onchar(UINT nChar, UINT nRepCnt, UINT nFlags); nChar:该参数不再是虚拟键代码,而是表示的是ANSI或Unicode字符代码 nRepCnt和nFlags参数与击键消息的参数相同。 三、消息产生的顺序 1、因为字符消息是由::TranslateMessage函数把击键消息转换而来的,因此字符消息从WM_KEYDOWN和WM_SYSKEYDOWN消息产生。 2、若Caps Lock键未打开,用户按下键A再释放,则这时产生的消息顺序如下: 消息 WM_KEYDOWN WM_CHAR WM_KEYUP 虚拟键代码 'A'键的虚拟键代码(0x41) 'A'键的虚拟键代码(0x41) 字符代码 'a'的字符编码(0x61) 3、若Caps Lock键未打开,用户按下Shift键,再按下A键,然后释放A键,再释放Shift键,则这时产生的消息顺序如下: 消息 WM_KEYDOWN WM_KEYDOWN WM_CHAR WM_KEYUP WM_KEYUP 虚拟键代码 VK_SHIFT(0x10) 'A'键的虚拟键代码(0x41) 'A'键的虚拟键代码(0x41) VK_SHIFT(0x10) 字符代码 'A'的字符编码(0x41) 4、若Caps Lock键未打开,用户按下Shift+A键,同时还按下了Alt键,则这时产生的消息顺序如下: 消息 虚拟键代码 字符代码 WM_SYSKEYDOWN VK_SHIFT(0x10) WM_SYSKEYDOWN 'A'键的虚拟键代码(0x41) WM_SYSCHAR 'A'的字符编码(0x41) WM_SYSKEYUP 'A'键的虚拟键代码(0x41) WM_SYSKEYUP VK_SHIFT(0x10) 说明:由于产生的是系统键消息,通常程序员不需要处理这些消息。 5、若按住A键不放,则产生的则这时产生的消息顺序如下: 消息 WM_KEYDOWN WM_CHAR WM_KEYDOWN WM_CHAR …… WM_KEYDOWN WM_CHAR WM_KEYUP 虚拟键代码 'A'键的虚拟键代码(0x41) 'A'键的虚拟键代码(0x41) …… 'A'键的虚拟键代码(0x41) 'A'键的虚拟键代码(0x41) 字符代码 A'的字符编码(0x61) A'的字符编码(0x61) …… A'的字符编码(0x61) 6、Ctrl与字母键的组合会产生ASCII控制字符,其范围由0x01(Ctrl+A)到0x1A(Ctrl+Z),系统也会产用Ctrl和字符键的组合作为快捷键(比如常用的复制快捷键Ctrl+C)。下表列出一些控制字符 按键 空格键 等效组合键 Ctrl+H 字符码 0x08 \\b ANSI C转义字符 Tab键 Ctrl+回车 回车键 Esc键 Ctrl+I Ctrl+J Ctrl+M Ctrl+[ 0x09 0x0A 0x0D 0x1B \ \\n \\r 四、死字符消息 1、死键的由来:类似德语之类的键盘,可能需要给某些字符加上音调,这时就需要使用一个键来给字母加音调,这个键就是死键。比如若使用+\\=键给字符加音调,则该键就是死键。 2、死字符消息WM_DEADCHAR和WM_SYSDEADCHAR通常不需要程序员去处理,因为系统已经有内置的处理了。 3、若用户安装了相应的键盘(比如德语键盘),当按下死键时,系统会产生相应的WM_DEADCHAR消息,该消息的wParam参数为音调本身的ASCII码或Unicode码,若再接着按下可带有音调用字符键时(比如A键),则系统接着产生一个WM_CHR消息,该消息的wParam参数等于带有音调的字母'a'的ANSI码。 4、安装其他语言键盘的方式是在Windows系统中的控制面板【键盘】中进行安装的。 6.3 插入符号(不是光标) 1、插入符:是用来标记下一个字符插入的地方的,常见的形状为闪烁的竖直线条,当然插入符还有其他形状,比如方块形状等。 2、插入符是一种共享资源,但是不是全局共享资源,他是单线程共享资源,也就是说插入符被运行在同一线程上的所有窗口所共享。 3、插入符实际上是一个位图,因此其外观可使用一个位图来进行设置。 4、插入符需要使用以下来自CWnd类的成员函数来处理 处理插入符的函数 函数名 作用 说明 CreateCaret 使用位图创建一个插入符 创建插入符的函数,创建插入符之后直到调用 ShowCaret之前,插入符是不可见的。插入符的尺CreateSolidCaret 创建实线或块状插入符 寸使用的是逻辑单位。 CreateGrayCaret 创建灰线或块状插入符 GetCaretPos 检索当前插入符的位置 SetCaretPos 设置插入符的位置 移动插入符的位置,通常是程序员的工作 ShowCaret 显示插入符 若使用HideCaret进行了两次以上的连续调用,则HideCaret 对ShowCaret的调用也必须进行相同的次数才能隐藏插入符。 使插入符可见。 ::DestroyCaret 销毁插入符, 这是个SDK函数,MFC中没有与之等价的函数。 ::GetCaretBlinkTime 获取插入符的闪烁时间 ::SetCaretBlinkTime 设置插入符的闪烁时间 5、使用插入符时通常应遵守以下规则: ①、插入符应在窗口接收到输入焦点(WM_SETFOCUS消息)时创建,在失去输入焦点 (WM_KILLFOCUS消息)时销毁。因为任何时候只有一个窗口拥有输入焦点,使多个窗口同时使插入符闪烁是没有意义的。 ②、插入符不应在WM_Create消息中创建,在WM_DESTROY消息中销毁,因为若程序 有多个窗口时,则多个窗口必须共享同一个插入符,而一个消息队列只能够支持一个插入符号。 ③、当绘制图形时,为避免显示冲突,应该隐藏插入符,当画完图之后再重新显示插入符, 对于MFC在OnPaint处理程序中,隐藏和重新显示插入符,已经由::BeginPaint和::EndPaint函数完成了,不需程序员处理。 ④、当输入一个字符,或使用鼠标移动插入符时,需要使用SetCaretPos函数来完成,这 通常是程序员的工作,系统并不会处理此项工作。 ⑤、由此可见,创建插入符需要执行以下步骤 使用CreateSolidCaret或另两个函数创建插入符 使用SetCaretPos设置插入符的位置 使用ShowCaret显示插入符 销毁插入符时需要执行以下步骤 使用HideCaret隐藏插入符 使用GetCaretPos的返回值保存当前插入符的位置,以便下次显示时使用。 使用::DestroyCaret销毁插入符。 6、插入符移动位置的计算: 若使用鼠标点击来设置插入符在字符串中的位置,这时需要计算出光标的位置和字符串起点位置的水平差值,然后根据此值计算出插入符的位置,因此不可避免的要使用一些计算。 使用CDC::GetTextExtent或CDC::GetTabbedTextExtent函数,可以获取前n个字符的累积宽度。 二、需要用到的函数 1、CreateCaret函数:void CWnd::CreateCaret(CBitmap* pBitmap); 作用:创建插入符,其形状为pBitmap指向的位图(位图详见相关章节)。 2、CreateSolidCaret函数:void CWnd::CreateSolidCaret(int nWidth, int nHeight); 作用:创建指定宽度和高度的插入符 nWidth:插入符的宽度(逻辑单位),若为0,则宽度设置为系统定义的窗口边界宽度, nHeight:插入符的高度(逻辑单位),若为0,则高度设置为系统定义的窗口边界高度。 3、CreateGrayCaret函数:void CWnd::CreateGrayCaret(int nWidth, int nHeight); 作用:创建指定宽度和高度的灰色插入符 nWidth:插入符的宽度(逻辑单位),若为0,则宽度设置为系统定义的窗口边界宽度, nHeight:插入符的高度(逻辑单位),若为0,则高度设置为系统定义的窗口边界高度。 4、ShowCaret函数:void CWnd::ShowCaret(); 作用:显示插入符,一旦显示,插入符会自动闪烁,若调用HideCaret函数多次隐藏插入符,则应调用相同次数的ShowCaret函数显示插入符,否则插入符不会被显示。 5、HideCaret函数:void CWnd::HideCaret(); //隐藏插入符。 6、SetCaretPos函数:static void CWnd::SetCaretPos(POINT point); 作用:设置插入符的位置,位置以客户区坐标指定。 7、GetCaretPos函数:static CPoint CWnd::GetCaretPos(); //检索插入符的位置。 示例:使用键盘输入字符并简单的处理插入符 #include class A :public CWinApp { public: BOOL InitInstance(); }; class B :public CFrameWnd {public: TEXTMETRIC tm; CRect cr; //客户区大小 int cx, cy; //客户区大小 int ccx, ccy; //每行和每列字符个数 CPoint cpoint; //光标坐标 B() {cpoint.x=0;cpoint.y=0; Create(NULL, _T(\"HYONG\"), WS_OVERLAPPEDWINDOW, CRect(100, 100, 500, 500)); } DECLARE_MESSAGE_MAP() afx_msg void OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags); afx_msg void OnChar(UINT nChar, UINT nRepCnt, UINT nFlags); afx_msg void OnSetFocus(CWnd* pOldWnd); afx_msg void OnKillFocus(CWnd* pNewWnd); afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct); }; BOOL A::InitInstance() { m_pMainWnd = new B(); m_pMainWnd->ShowWindow(m_nCmdShow); m_pMainWnd->UpdateWindow(); return TRUE;} A ma; BEGIN_MESSAGE_MAP(B, CFrameWnd) ON_WM_KEYDOWN() ON_WM_CHAR() ON_WM_SETFOCUS() ON_WM_KILLFOCUS() ON_WM_CREATE() END_MESSAGE_MAP() int B::OnCreate(LPCREATESTRUCT lpCreateStruct){ //此消息处理函数主要用于初始化数据。 CClientDC dc(this); /*因为未创建全局共享的DC,因此此处需要创建一个与OnChar函数中相同的 DC,以初始化字体信息。*/ dc.SelectObject(GetStockObject(SYSTEM_FIXED_FONT)); GetClientRect(&cr); GetTextMetrics(dc, &tm); //tm变量需要在整个类B中使用,初始化tm需要在此函数中处理。 cx = cr.right - cr.left; //设置客户区的宽度。 cy = cr.bottom - cr.top; //设置客户区的高度。 ccx = cx / tm.tmAveCharWidth; //计算在客户区水平方向能显示的字符个数 ccy = cy / tm.tmHeight;//计算在客户区垂直方向能显示的字符个数 return 0; } void B::OnSetFocus(CWnd* pOldWnd){ CreateSolidCaret(2, tm.tmHeight); //创建插入符 SetCaretPos(cpoint); //设置插入符位置 ShowCaret(); } void B::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags){ CClientDC dc(this); dc.SelectObject(GetStockObject(SYSTEM_FIXED_FONT)); HideCaret(); /*首先隐藏插入符,然后再输出文本,最后再显示插入符,这样可以避免移动插入符 时在客户区留下插入符的痕迹。*/ /*以下仅对输入回车符进行了处理,若输入退格符(backspace键)等其他不可显示的字符,则客户区会显示一个类似方块的字符。*/ if (nChar == 0x0D) { //处理回车符,以避免显示一个类似方块的字符。 ShowCaret(); return; } else dc.TextOut(cpoint.x,cpoint.y,(char)nChar); //显示从键盘输入的字符 if (cpoint.x < ccx*tm.tmAveCharWidth) //输出一个字符后,使插入符向后移一个字符。 { cpoint.x=cpoint.x+tm.tmAveCharWidth; } else { //若到达行末,则使插入符移至下一行的开头。 cpoint.x=0; cpoint.y = cpoint.y + tm.tmHeight; } SetCaretPos(cpoint); //设置插入符的位置。 ShowCaret(); } //显示插入符,否则插入符会不可见。 void B::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags){ /*以下仅对按下方向箭和回车键进行了处理,对于按下delete、home、end等按键的处理需要另外的算法,本示例不作介绍。*/ if (nChar == VK_LEFT) //按下左箭,使插入符左移一个字符 { if (cpoint.x>0) cpoint.x = cpoint.x - tm.tmAveCharWidth; } if (nChar == VK_RIGHT)//按下右箭,使插入符右移一个字符 {if (cpoint.x if (nChar == 0x0D) //按下回车,使插入符移至下一行的开头。 {if (cpoint.y < ccy*tm.tmHeight){cpoint.x = 0;cpoint.y = cpoint.y + tm.tmHeight;}} SetCaretPos(cpoint); }//设置插入符的位置 void B::OnKillFocus(CWnd* pNewWnd){ HideCaret(); ::DestroyCaret(); }//当失去焦点时销毁插入符 运行结果及说明:本示例能显示从键盘输入的字符,并可实现使用方向箭对插入符进行移动,按下回车可使插入符移至下一行的开头。本示例未对鼠标事件进行处理,因此不能使用鼠标来移动插入符,若输入类似退格符等不可显示字符,会在客户区输出一个类似方块的字符。 插入符 输入的不可显示的字符 作者:黄邦勇帅(原名:黄勇) 2018-1-10 因篇幅问题不能全部显示,请点此查看更多更全内容