- Visual C++2013从入门到精通(视频教学版)
- 朱文伟
- 3876字
- 2025-02-23 19:54:07
2.8 键盘
2.8.1 键盘概述
用程序来表达键盘和鼠标的操作是Visual C++开发中经常会碰到的,本节先阐述键盘的操作。键盘的硬件原理是用户每次按下或释放某个键,键盘都会产生一个扫描码,以确定相应的键,并且按下的时候扫描码最高位是0,释放的时候最高位是1,因此每个键能产生两个不同的扫描码。
物理键盘产生扫描码,不同的键盘产生的扫描码不同。为了屏蔽不同的物理键盘的差异,Windows系统通过一个虚拟键盘来提供与设备无关的键盘操作。虚拟键盘不是一个物理硬件,但Windows应用程序要访问键盘都必须先通过虚拟键盘,然后再通过物理键盘的驱动程序映射到物理键盘上,如果要更换物理键盘,只需要替换其驱动程序,虚拟键盘就知道了,而上层应用程序是不需要变动的,它只和虚拟键盘打交道。
虚拟键盘上的键码通过宏定义来表示,它们在WinUser.h中定义。常见的虚拟键码如下表所示。

(续表)

要注意的是,数字键0到9并没有定义VK_开头的宏,而是用ASCII的值’0’到’9',同样键盘上的A到Z也没有定义VK_开头的宏,直接用’A’到‘Z',另外键盘上都是大写的英文字母。
2.8.2 键盘消息
用户对键盘的操作都会产生一个事件,然后向相应的程序发送消息,由键盘事件产生的消息称为键盘消息,通常键盘消息分为两种:击键消息和字符消息。两者的区别是,只要按下或放开键盘上的键就会产生击键消息,但只有按了可显示字符的键,此时不但产生击键消息,还会产生字符消息。
1.击键消息
击键消息是指用户按下键盘上的某个键或释放某个键而产生的消息。而字符消息是指按下或释放的键是可显示的字符或回车键、Esc键和Tab键时发出的消息,比如按下字符K,就会产生字符消息,当然也会产生击键消息。只要操作键盘击键消息是肯定会产生的,但字符消息不一定。
击键消息分按下和释放两类消息,按下消息分为系统键按下消息和非系统键按下消息,前者用宏WM_SYSKEYDOWN表示,后者用WM_KEYDOWN表示。而释放键消息也分为系统键释放消息和非系统键释放消息,前者用宏WM_SYSKEYUP表示,后者用WM_KEYUP表示。通常,按下和释放是成对出现的,释放消息跟在按下消息之后。
非系统键按下的消息是WM_KEYDOWN,在Visual C++中,可以可视化地为某个窗口添加消息处理函数,WM_KEYDOWN消息的消息处理函数是这样的:
afx_msg void OnKeyDown( UINT nChar, UINT nRepCnt, UINT nFlags );
afx_msg表示这个函数是一个消息处理函数。其中参数nChar用来确定虚拟键值(注意是键值,不是字符,字符有可能是区分大小写的,但键值就是印在每个按键上的内容,比如键F的键值就是F,但该键对应的字符可能会有’f’或’F'); nRepCnt表示重复次数;nFlags用来表示扫描码、翻译键、以前键的状态等。
有时候,在处理按键消息时,还需要知道其他键的状态,比如Ctrl键或Shift键是否按着,大写键(Caps Lock)、数字键(Num Lock)、滚动键(Scroll Lock)是否打开。系统提供了两个函数GetkeyState和GetAsyncKeyState来获取这些键的信息。函数GetKeyState可以获取击键消息发生时指定键的状态,其声明如下:
SHORT GetKeyState( int nVirtKey);
其中参数nVirtKey表示虚拟键码,如VK_SHIFT,要注意的是,如果要获取字母(‘A'—‘Z’或‘a'—‘z')和数字键(‘0'—‘9'),必须用其字符对应的ASCII码值传入参数。函数返回值是虚拟键的状态,如果键处于按下状态,则最高位为1,如果为弹起状态,最低位为0;如果最低位是1,则表示该键处于打开状态,比如大写键(Caps Lock)、数字键(Num Lock)和滚动键(Scroll Lock)。
函数GetAsyncKeyState用来获取执行该函数时指定键的状态,其声明如下:
SHORT GetAsyncKeyState( int vKey);
参数和返回值的含义同GetKeyState。GetAsyncKeyState用得不多,大多数场合用到的是GetKeyState。
下面我们看个例子,显示用户按下的字母键值和F1键,并判断是否同时按下Shift和空格键,如不是,则退出程序。
【例2.40】 通过按键消息显示用户按键的键值
(1)打开Visual C++ 2013,新建一个单文档工程。
(2)切换到类视图,然后找到类CTestView,单击它,然后在属性视图中选择“消息”页,找到消息WM_KEYDOWN,在其右边下拉框中选择OnKeyDown,如图2-89所示。

图2-89
这就是可视化的方式添加按键消息处理函数。此时会显示代码编辑窗口,并自动定位到函数OnKeyDown处,在该函数中添加代码如下:
void CTestView::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags) { // TODO: 在此添加消息处理程序代码和/或调用默认值 CString str; short sh = GetKeyState(VK_SHIFT); //获取Shift键的状态 if (nChar >= 'A' &&nChar <= 'Z') //如果用户按下的是字母键 { str.Format(_T("你按了%c"), nChar); //格式化字符串 AfxMessageBox(str); //显示信息 } else if (nChar == VK_F1) //如果按下的是F1键 AfxMessageBox(_T("帮助")); else if (nChar == VK_SPACE&&(sh & 0x8000)) //如果按下的是空格键并且Shift键也同时按下了 PostQuitMessage(0); //退出程序 CView::OnKeyDown(nChar, nRepCnt, nFlags); }
因为键盘上的字母键的键值都是大写的,所以nChar只需在‘A’和‘Z’直接判断即可。PostQuitMessage是系统API,它会向窗口消息队列发送一个WM_QUIT消息,这样窗口收到后就会退出程序了。PostQuitMessage函数的声明如下:
void PostQuitMessage( int nExitCode);
其中参数nExitCode指定应用程序退出码,该参数其实就是消息WM_QUIT的消息参数wParm,一般设0即可。如果用户按着Shift键,则sh的最高位为1,所以要和0x8000进行与操作,就判断最高位是否为0。
(3)保存工程并运行,运行结果如图2-90所示。

图2-90
2.拦截键盘消息
在MFC编程中,有关键盘编程还有个经常碰到的场合是预处理按键或释放消息,使得正常的按键或释放消息处理函数获取不到消息,以此达到定制某种控件的目的,比如我们不想让某个编辑控件能接收用户输入,就要提前拦截按键消息了。提前拦截消息的方法是重载虚拟函数CWinApp::PreTranslateMessage,该函数在消息调度前被调用,声明如下:
virtual BOOL PreTranslateMessage( MSG* pMsg );
其中参数pMsg为指向要处理的消息结构体MSG的指针。如果消息在PreTranslateMessage中被完全处理并且不需要进一步处理,则返回非零值。如果消息还需要按照通常方式处理,则返回零。
消息结构体MSG前面已经阐述过了,里面有成员变量wParam和lParam,按键消息所附带的信息就放在这两个消息参数中,其中wParam用来存放虚拟键码;lParam用来确定重复计数、扫描码、扩展键标记、按键前的状态等,它是32位值,不同部分的内容不同,具体如下:
● 0~15:表示当前消息的重复计数
● 16~23:确定扫描码,依赖于OEM
● 24:是否为扩展键
● 25~28:保留不用
● 29:上下文键,如果是WM_KEYDOWN消息,则总为0
● 30:确定先前状态,如果消息发生前键是按着的则为1,否则为0
● 31:过渡状态标记,如果是WM_KEYDOWN消息,则总为0
下面来看个例子,在按键消息处理之前先拦截。
【例2.41】 拦截按键消息
(1)打开Visual C++ 2013,新建一个单文档工程。
(2)切换到类视图,然后找到类CTestView,单击它,然后在属性视图中选择“消息”页,找到消息WM_KEYDOWN,在其右边下拉框中选择OnKeyDown,这样为视图添加按键消息处理函数,并添加代码如下:
void CTestView::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags) { // TODO: 在此添加消息处理程序代码和/或调用默认值 CString str; str.Format(_T("OnKeyDown:你按下了%c"), nChar); //格式化字符串 AfxMessageBox(str); //显示信息 CView::OnKeyDown(nChar, nRepCnt, nFlags); }
(3)下面为视图重载PreTranslateMessage函数。切换到类视图,选中CTestView,单击它,然后在属性视图中选择“重写”页,然后找到虚拟函数PreTranslateMessage,在其右边下拉框中选择PreTranslateMessage,如图2-91所示。

图2-91
这样就为视图重载了虚拟函数PreTranslateMessage,此时会自动打开代码编辑窗口,然后在该函数中添加代码如下:
BOOL CTestView::PreTranslateMessage(MSG* pMsg) { // TODO: 在此添加专用代码和/或调用基类 if (pMsg->message == WM_KEYDOWN) //消息是否为按键消息 { CString str; str.Format(_T("拦截到按键消息:你按下了%c"), pMsg->wParam); //格式化字符串 AfxMessageBox(str); //显示信息 return TRUE; //返回非0表示不再交给正常的消息处理函数处理 } return CView::PreTranslateMessage(pMsg); }
首先判断消息是否为按键消息,如果是就获取虚拟键码并组成字符串,显示结果,然后返回TRUE,这样正常的消息处理函数OnKeyDown就接收不到了。如果返回FALSE,则会执行OnKeyDown。
(4)保存工程并运行,运行结果如图2-92所示。

图2-92
3.字符消息
上面主要讲了按键消息(WM_KEYDOWN)的使用,除此之外字符消息也会经常用到。系统会对WM_KEYDOWN或WM_SYSKEYDOWN两个按键消息中的虚拟键码进行判断,如果是可显示字符,则会把字符消息放入消息队列中,这样在按键消息的下一次消息循环中,字符消息就会发送给窗口。有4种字符消息:WM_CHAR、WM_DEADCHAR、WM_SYSCHAR和WM_SYSDEADCHAR,前两种跟随在WM_KEYDOWN后,后两种跟随在WM_SYSKEYDOWN后。WM_CHAR是非系统字符中的一般字符消息,WM_DEADCHAR是非系统字符中的死字符消息;WM_SYSCHAR是系统字符中的一般字符消息,WM_SYSDEADCHAR是系统字符中的死字符消息。所谓死字符消息,是指某些非英语键盘上,一些给字母加音标的键由于自身并不产生字符,因此把它们所产生的消息称为死字符消息。Windows已经对死字符消息进行了较好的处理,应用程序通常不需要去处理。
字符消息WM_CHAR的消息处理函数是窗口类的成员函数,即CWnd::OnChar,它的函数声明如下:
afx_msg void OnChar(UINT nChar, UINT nRepCnt, UINT nFlags);
其中参数nChar表示键的字符代码值;nRepCnt表示重复计数,即当用户按下键时重复的击键数目;nFlags表示扫描码、键暂态码、以前的键状态以及上下文代码等,它是一个32位值,不同位部分的内容如下:
● 0~15:指定了重复计数。其值是用户按下键时重复的击键数目。
● 16~23:指定了扫描码。其值依赖于原始设备制造商(OEM)。
● 24:指明该键是否是扩展键,如增强的101或102键盘上右边的ALT或CTRL键,如果它是个扩展键,则该值为1;否则为0。
● 25~28:Windows内部使用。
● 29:指定了上下文代码。如果按键时ALT键是按下的,则该值为1;否则为0。
● 30:指定了以前的键状态。如果在发送消息前键是按下的,则值为1;如果键是弹起的,则值为0。
● 31:指定了键的暂态。如果该键正被放开,则值为1;如果键正被按下,则该值为0。
下面我们来看2个例子,第一个例子比较简单,它通过字符消息来显示用户按下了字母键和0~9的字符键,并判断是否同时按下Shift和空格键,若是则退出程序。第二个例子有一定难度,综合了几个消息的应用,对于按下可显示的字符会产生字符消息WM_CHAR,并且是紧跟在按键消息WM_KEYDOWN之后产生的,如果按键后马上弹起,WM_CHAR之后的消息应该是WM_KEYUP。该程序会以列表的形式把接收到的按键消息和字符消息及其键符、键值等内容打印出来。
【例2.42】 通过字符消息显示用户按键的字符
(1)打开Visual C++ 2013,新建一个单文档工程。
(2)切换到类视图,然后找到类CTestView,单击它,然后在属性视图中选择“消息”页,找到消息WM_CHAR,在其右边下拉列表框中选择OnChar,如图2-93所示。

图2-93
这就是可视化的方式添加字符消息处理函数。此时会显示代码编辑窗口,并自动定位到函数OnChar处,在该函数中添加代码如下:
void CTestView::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags) { // TODO: 在此添加消息处理程序代码和/或调用默认值 CString str; short sh = GetKeyState(VK_SHIFT); //获取Shift键的状态 if (nChar >= '0' &&nChar <= '9') //如果用户按下的是数字字符 { str.Format(_T("你按了%c"), nChar); //格式化字符串 AfxMessageBox(str); //显示信息 } if (nChar >= 'a' &&nChar <= 'z') //如果用户按下的是小写字母字符 { str.Format(_T("你按了%c"), nChar); //格式化字符串 AfxMessageBox(str); //显示信息 } else if (nChar == VK_SPACE && (sh & 0x8000)) //如果按下的是空格键并且Shift键也同时按下了 PostQuitMessage(0); //退出程序 CView::OnChar(nChar, nRepCnt, nFlags); }
程序判断了用户是否按了‘0’到‘9’的字符和小写字母的字符键,如果是,则显示按下键所产生的字符,并且判断是否同时按下Shift键和空格键,如果是则退出程序。
要注意的是,OnChar中的nChar是按键所产生的字符,而不是按键所对应的键值。前面有例子通过按键消息显示用户按键的键值,大家可以对比下两个例子的区别。
(3)保存工程并运行,运行结果如图2-94所示。

图2-94
【例2.43】 打印按键消息和字符消息的键符和键值
(1)打开Visual C++ 2013,新建一个单文档工程。
(2)切换到解决方案资源管理器,然后打开Test.cpp,在该文件开头定义几个全局变量:
static int cyChar; //保存字体高度 static int cyClient; //客户区的当前高度 static int cyClientMax; //客户区的最大高度 static int cMaxLineNum, cLineNum; // cMaxLineNum 表示显示的最大行;cLineNum为当前行 static PMSG pmsg; //指向接收到的消息的缓冲区 static RECT rectScroll; //客户区中要滚动的矩形坐标 //定义各个列头的名称 static TCHAR szTop[] = TEXT("消息 扫描码 键符 键值 字符 字符值 重复计数"); //有可能要打印的消息名称 static TCHAR * szMessage[] = { TEXT("WM_KEYDOWN"), TEXT("WM_KEYUP"), TEXT("WM_CHAR"), TEXT("WM_DEADCHAR"), TEXT("WM_SYSKEYDOWN"), TEXT("WM_SYSKEYUP"), TEXT("WM_SYSCHAR"), TEXT("WM_SYSDEADCHAR") };
(3)切换到类视图,然后找到类CTestView,单击它,然后在属性视图中选择“消息”页,找到消息WM_CREATE,在其右边下拉框中选择OnCreatre,这样为视图添加按键消息处理函数,并添加代码如下:
int CTestView::OnCreate(LPCREATESTRUCT lpCreateStruct) { HDC hdc; TEXTMETRIC tm; if (CView::OnCreate(lpCreateStruct) == -1) return -1; // TODO: 在此添加您专用的创建代码 cyClientMax = GetSystemMetrics(SM_CYMAXIMIZED); //获取屏幕高度 hdc = ::GetDC(m_hWnd); //获取设备描述表句柄 ::SelectObject(hdc, GetStockObject(SYSTEM_FIXED_FONT)); //把系统字体选进设备描述表 ::GetTextMetrics(hdc, &tm); //得到该字体尺寸 cyChar = tm.tmHeight; //保存高度 ::ReleaseDC(m_hWnd, hdc); //释放设备描述表 cLineNum = 0; cMaxLineNum = cyClientMax / cyChar; //得到该字体的文本能在屏幕上显示的最大行数 /*开辟存放消息缓冲区的空间,屏幕上最多能显示cMaxLineNum条消息,我们就开辟cMaxLineNum个MSG的空间*/ pmsg = (PMSG)malloc(cMaxLineNum * sizeof(MSG)); return 0; }
(4)同样的方法添加视图窗口尺寸改变消息WM_SIZE,在其消息函数OnSize中添加代码如下:
void CTestView::OnSize(UINT nType, int cx, int cy) { CView::OnSize(nType, cx, cy); // TODO: 在此处添加消息处理程序代码 cxClient = cx; //保存客户区宽度 cyClient = cy; //保存客户区高度 //窗口大小变了,客户区所容纳的内容也会变,我们要更新以后滚动时所需的客户区矩形坐标 rectScroll.left = 0; rectScroll.right = cxClient; rectScroll.top = cyChar; rectScroll.bottom = cyChar * (cyClient / cyChar); InvalidateRect(NULL, TRUE); //重画整个客户区 }
(5)为视图窗口重载窗口消息处理函数WindowProc,单击类TestView,然后在其属性窗口中切换到“重写”页,在该页下找到“WindowProc”,然后单击右边空白处,选择WindowProc,接着添加代码如下:
LRESULT CTestView::WindowProc(UINT message, WPARAM wParam, LPARAM lParam) { // TODO: 在此添加专用代码和/或调用基类 int i; /*判断是否是按键消息、键弹起消息、字符消息、系统按键消息、系统键弹起消息和系统字符消息*/ if (WM_KEYDOWN == message || WM_KEYUP == message || WM_CHAR == message || WM_SYSKEYDOWN == message || WM_SYSKEYUP == message || WM_SYSCHAR == message ) { for (i = cMaxLineNum - 1; i > 0; i--) //把消息往后移动一个位置,pmsg[cMaxLineNum -1]被舍弃 pmsg[i] = pmsg[i -1]; //刚刚接收到的消息存放在第一个位置 pmsg[0].hwnd = m_hWnd; pmsg[0].message = message; pmsg[0].wParam = wParam; pmsg[0].lParam = lParam; //确定要消息显示在的那一行 cLineNum = min(cLineNum + 1, cMaxLineNum); //数据更新了,滚动客户区的内容,往上滚动,最新的消息显示在最下行 ScrollWindow(0, -cyChar, &rectScroll, &rectScroll); } return CView::WindowProc(message, wParam, lParam); }
重载窗口消息处理函数,可以拦截我们所需要处理的消息。拦截后,把数组位置向后移动,并把最新的消息存放在数组的第一个位置,这样最后一个位置的消息被覆盖舍弃了。然后我们看当前在客户区显示的行是否到达最大行了,如果不是,则递增。最后让客户区向上滚动,滚动客户区的内容的函数为CWnd:: ScrollWindow,该函数声明如下:
void ScrollWindow( int xAmount, int yAmount, LPCRECT lpRect = NULL, LPCRECT lpClipRect = NULL );
其中,参数xAmount指定了水平滚动的量,使用设备单位,在左滚时,该参数必须为负;yAmount指定了垂直滚动的量,使用设备单位,在上滚时,该参数必须为负;lpRect指向一个CRect对象或RECT结构,指定了要滚动的客户区的部分,如果lpRect为NULL,则将滚动整个客户区,如果光标区域与滚动矩形重叠,则插字符将被重定位;lpClipRect指向一个CRect对象或RECT结构,指定了要滚动的裁剪区域。只有这个矩形中的位才会被滚动,在矩形之外的位不会被影响,即使它们是在lpRect矩形之内,如果lpClipRect为NULL,则不会在滚动矩形上进行裁剪。
(6)添加绘图消息WM_PAINT。切换到类视图,然后找到类CTestView,单击它,然后在属性视图中选择“消息”页,找到消息WM_PAINT,在其右边下拉框中选择OnPaint,并添加代码如下:
void CTestView::OnPaint() { int i, nType; TCHAR szBuff[150], szKeyName[30]; // szKeyName保存键符的名称 CPaintDC dc(this); // device context for painting // TODO: 在此处添加消息处理程序代码 // 不为绘图消息调用CView::OnPaint() dc.SelectObject( GetStockObject(SYSTEM_FIXED_FONT)); //把文本字体选进设备上下文 dc.TextOut( 0, 0, szTop, lstrlen(szTop)); //画出各个列头的名称 //画出已拦截到的消息及其各个参数,比如键符、键值等 for ( i = 0; i < min(cLineNum, cyClient / cyChar -1); i++) { if (pmsg[i].message == WM_CHAR || pmsg[i].message == WM_SYSCHAR) nType = 1; //如果是字符消息则要标记,以便下面键所对应字符 else nType = 0; //获取键符,键符就是印在键盘上的字符 if (! GetKeyNameText(pmsg[i].lParam, szKeyName, 30)) { wsprintf(szBuff, _T("GetKeyNameText failed")); dc.TextOut(0, (cyClient / cyChar - 1 - i) * cyChar, szBuff,_tcslen(szBuff)); return; } wsprintf(szBuff, nType ? TEXT("%-13s %4d %1s %c 0x%04X %6u") : TEXT("%-13s %4d %-15s%c %3d %6u"), szMessage[pmsg[i].message - WM_KEYFIRST], //消息 HIWORD(pmsg[i].lParam) & 0xFF, //扫描码 (PTSTR)(nType ? TEXT(" ") : szKeyName), //键符 (TCHAR)(nType ? pmsg[i].wParam : ' '), //字符 pmsg[i].wParam, //键值或字符值 LOWORD(pmsg[i].lParam) //重复计数 ); //输出当前行 dc.TextOut(0, (cyClient / cyChar - 1 - i) * cyChar, szBuff,_tcslen(szBuff)); } }
首先把系统字体选进设备描述表中,然后开始打印列头。关于设备描述表的概念在图形图像那一章会讲到,这里只需知道即可。
打印完列头,就开始一行一行打印所拦截到消息,并且通过nType来标记是否为字符消息,如果是的话,会打印出该键的字符。值得注意的是,键符和字符是不一样的,键符就是印在键盘上的字符,而字符有大写,有小写,不一定和键符相同,比如字符‘f',而键符是‘F',只有按了键符‘F',才会出现字符‘f'。
(7)保存工程并运行,运行结果如图2-95所示。

图2-95