[Win32]键盘消息

这篇博客详细介绍了在Win32环境下如何处理键盘消息,包括添加Page Up/Down和方向键翻页,使用SendMessage模拟滚条消息,以及如何理解和处理WM_KEYDOWN和WM_CHAR消息,特别提到了TranslateMessage函数的作用。还探讨了击键和字符消息的排序问题,以及处理控制字符的方法。文中给出了KeyView1和Typer两个示例程序,分别用于观察键盘消息和实现简单的文本编辑功能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 为Sysmets3程序添加击键消息:

    1) 主要是添加了Page Up、Page Down、光标方向键等击键消息,用于页面翻滚;

    2) 我们没有必要在每个击键消息中添加滚条操作的代码,而只要在WM_KEYDOWN消息中使用SendMessage函数传递假的WM_SCROLL消息给窗口来模拟滚条消息即可,这样就可以让滚条消息响应只处理滚条消息,这样分工明确,不会让代码冗余杂乱;

    3) 关于SendMessage函数的用法:

SendMessage( hWnd, message, wParam, lParam );

直接将一条消息按照参数指定的那样包装成MSG结构体发送给响应的窗口hWnd处,hWnd可以不是产生该消息的窗口,可以是该程序的其它窗口甚至是其它程序的窗口(程序间的通信);

在这里这需要这样使用即可:

case WM_KEYDOWN:
	switch ( wParam )
	{
	case VK_HOME:
		SendMessage( hWnd, WM_VSCROLL, SB_TOP, 0 );
		break;
	....
	}
...
     4) 具体代码:

// sysmets4.c

#include <windows.h>

#include "sysmets.h"

LRESULT CALLBACK WndProc( HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam );

int WINAPI WinMain(
	HINSTANCE	hInstance,
	HINSTANCE	hPreInstanc,
	LPSTR		lpszCmdLine,
	int			nCmdShow
	)
{
	static TCHAR	szAppName[] = TEXT("sysmets4");

	WNDCLASS	wndclass;
	HWND		hWnd;
	MSG			msg;

	wndclass.style			= CS_HREDRAW | CS_VREDRAW;
	wndclass.lpfnWndProc	= WndProc;
	wndclass.cbClsExtra		= 0;
	wndclass.cbWndExtra		= 0;
	wndclass.hInstance		= hInstance;
	wndclass.hIcon			= LoadIcon( NULL, IDI_APPLICATION );
	wndclass.hCursor		= LoadCursor( NULL, IDC_ARROW );
	wndclass.hbrBackground	= (HBRUSH)GetStockObject( WHITE_BRUSH );
	wndclass.lpszMenuName	= NULL;
	wndclass.lpszClassName	= szAppName;

	RegisterClass( &wndclass );

	hWnd = CreateWindow(
		szAppName, TEXT("Get System Metrics Version 4"),
		WS_OVERLAPPEDWINDOW | WS_VSCROLL | WS_HSCROLL,
		CW_USEDEFAULT, CW_USEDEFAULT,
		CW_USEDEFAULT, CW_USEDEFAULT,
		NULL, NULL, hInstance, NULL
	);

	ShowWindow( hWnd, nCmdShow );
	UpdateWindow( hWnd );

	while ( GetMessage( &msg, NULL, 0, 0 ) )
	{
		TranslateMessage( &msg );
		DispatchMessage( &msg );
	}

	return msg.wParam;
}

LRESULT CALLBACK WndProc( HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam )
{
	static int		cxChar, cxCaps, cyChar;
	static int		cxClient, cyClient;
	static int		nMaxWidth, nMaxHeight;

	TCHAR	szBuffer[10];

	HDC				hdc;
	PAINTSTRUCT		ps;
	TEXTMETRIC		tm;
	SCROLLINFO		si;

	int		iPreVertPos, iPreHorzPos;
	int		iCurVertPos, iCurHorzPos;
	int		iBeginPaint, iEndPaint;

	int		i;
	int		x, y;

	switch ( message )
	{
	case WM_CREATE:
		hdc = GetDC( hWnd );

		GetTextMetrics( hdc, &tm );
		cxChar = tm.tmAveCharWidth;
		cxCaps = ( tm.tmPitchAndFamily & 1 ? 3 : 2 ) * cxChar / 2;
		cyChar = tm.tmHeight + tm.tmExternalLeading;

		ReleaseDC( hWnd, hdc );

		nMaxWidth  = 2 + ( 22 * cxCaps + 40 * cxChar ) / cxChar;
		nMaxHeight = NUMLINES - 1;

		return 0;

	case WM_SIZE:
		cxClient = LOWORD( lParam );
		cyClient = HIWORD( lParam );

		si.cbSize	= sizeof( SCROLLINFO );
		si.fMask	= SIF_RANGE | SIF_PAGE;
		si.nMin		= 0;
		si.nMax		= nMaxHeight;
		si.nPage	= cyClient / cyChar;
		SetScrollInfo( hWnd, SB_VERT, &si, TRUE );

		si.cbSize	= sizeof( SCROLLINFO );
		si.fMask	= SIF_RANGE | SIF_PAGE;
		si.nMin		= 0;
		si.nMax		= nMaxWidth;
		si.nPage	= cxClient / cxChar;
		SetScrollInfo( hWnd, SB_HORZ, &si, TRUE );

		return 0;

	case WM_VSCROLL:
		si.fMask = SIF_ALL;
		GetScrollInfo( hWnd, SB_VERT, &si );

		iPreVertPos = si.nPos;

		switch ( LOWORD( wParam ) )
		{
		case SB_TOP: // 在滚条上直接用鼠标操作并不会产生这样的消息
					 // 这样的消息只有通过WM_KEYDOWN中通过SendMessage产生
			si.nPos = si.nMin;
			break;

		case SB_BOTTOM:
			si.nPos = si.nMax;
			break;

		case SB_LINEUP:
			si.nPos--;
			break;

		case SB_LINEDOWN:
			si.nPos++;
			break;

		case SB_PAGEUP:
			si.nPos -= si.nPage;
			break;

		case SB_PAGEDOWN:
			si.nPos += si.nPage;
			break;

		case SB_THUMBTRACK:
			si.nPos = si.nTrackPos;
			break;
		}

		si.cbSize	= sizeof( SCROLLINFO );
		si.fMask	= SIF_POS;
		SetScrollInfo( hWnd, SB_VERT, &si, TRUE );
		GetScrollInfo( hWnd, SB_VERT, &si );

		if ( si.nPos != iPreVertPos )
		{
			ScrollWindow( hWnd, 0, cyChar * ( iPreVertPos - si.nPos ), NULL, NULL );
		}

		return 0;

	case WM_HSCROLL:

		si.fMask = SIF_ALL;
		GetScrollInfo( hWnd, SB_HORZ, &si );

		iPreHorzPos = si.nPos;

		switch ( LOWORD( wParam ) )
		{
		case SB_LEFT:
			si.nPos = si.nMin;
			break;

		case SB_RIGHT:
			si.nPos = si.nMax;
			break;

		case SB_LINELEFT:
			si.nPos--;
			break;

		case SB_LINERIGHT:
			si.nPos++;
			break;

		case SB_PAGELEFT:
			si.nPos -= si.nPage;
			break;

		case SB_PAGERIGHT:
			si.nPos += si.nPage;
			break;
			
		case SB_THUMBTRACK:
			si.nPos = si.nTrackPos;
			break;
		}

		si.cbSize	= sizeof( SCROLLINFO );
		si.fMask	= SIF_POS;
		SetScrollInfo( hWnd, SB_HORZ, &si, TRUE );
		GetScrollInfo( hWnd, SB_HORZ, &si );

		if ( si.nPos != iPreHorzPos )
		{
			ScrollWindow( hWnd, cxChar * ( iPreHorzPos - si.nPos ), 0, NULL, NULL );
		}

		return 0;

	case WM_KEYDOWN:
		switch ( wParam )
		{
		case VK_HOME: // 顶部
			SendMessage( hWnd, WM_VSCROLL, SB_TOP, 0 );
			break;

		case VK_END: // 底部
			SendMessage( hWnd, WM_VSCROLL, SB_BOTTOM, 0 );
			break;

		case VK_PRIOR: // 上一页,Page Up
			SendMessage( hWnd, WM_VSCROLL, SB_PAGEUP, 0 );
			break;

		case VK_NEXT: // 下一页,Page Down
			SendMessage( hWnd, WM_VSCROLL, SB_PAGEDOWN, 0 );
			break;

		case VK_UP: // 上一行,上光标
			SendMessage( hWnd, WM_VSCROLL, SB_LINEUP, 0 );
			break;

		case VK_DOWN: // 下一行,下光标
			SendMessage( hWnd, WM_VSCROLL, SB_LINEDOWN, 0 );
			break;

		case VK_LEFT: // 左一个单位,左光标
			SendMessage( hWnd, WM_HSCROLL, SB_LINELEFT, 0 );
			break;

		case VK_RIGHT: // 右一个单位,右光标
			SendMessage( hWnd, WM_HSCROLL, SB_LINERIGHT, 0 );
			break;
		}

		return 0;

		case WM_PAINT:
			hdc = BeginPaint( hWnd, &ps );

			si.cbSize	= sizeof( SCROLLINFO );
			si.fMask	= SIF_POS;
			GetScrollInfo( hWnd, SB_VERT, &si );
			iCurVertPos = si.nPos;

			si.cbSize	= sizeof( SCROLLINFO );
			si.fMask	= SIF_POS;
			GetScrollInfo( hWnd, SB_HORZ, &si );
			iCurHorzPos = si.nPos;

			iBeginPaint = max( 0, iCurVertPos + ps.rcPaint.top / cyChar );
			iEndPaint	= min( nMaxHeight, iCurVertPos + ps.rcPaint.bottom / cyChar );

			for ( i = iBeginPaint; i <= iEndPaint; i++ )
			{
				x = cxChar * ( 1 - iCurHorzPos );
				y = cyChar * ( i - iCurVertPos );

				TextOut( hdc, x, y, sysmetrics[i].szLabel, lstrlen( sysmetrics[i].szLabel ) );

				TextOut( hdc, x + 22 * cxCaps, y, sysmetrics[i].szDesc, lstrlen( sysmetrics[i].szDesc ) );

				SetTextAlign( hdc, TA_RIGHT | TA_TOP );
				TextOut( hdc, x + 22 * cxCaps + 40 * cxChar, y, szBuffer,
					wsprintf( szBuffer, TEXT("%5d"), sysmetrics[i].iIndex ) );
				SetTextAlign( hdc, TA_LEFT | TA_TOP );
			}

			EndPaint( hWnd, &ps );
			return 0;

		case WM_DESTROY:
			PostQuitMessage( 0 );
			return 0;
	}

	return DefWindowProc( hWnd, message, wParam, lParam );
}

2. 字符消息的基本概念:

    1) 对于可以产生显示字符的按键,如果想利用按键所表示的字符的话可以通过GetKeyState函数判断转义(大小写)等获取按键所代表的字符,但是别忘了,释放按键同样也会产生击键消息,因此通过这种方法来获取击键所代表的字符是很不可取的;

    2) 这个工作Windows已经帮你做了,那就是TranslateMessage函数,可以为每一个KEYDOWN(包括系统的)消息生成一个WM_CHAR消息,并将按键所代表的ASCII码或UNICODE码保存在WM_CHAR消息的wParam中,并且不会为KEYUP消息声称WM_CHAR消息,这样就不会有多余的字符了;

    3) WM_CHAR的lParam则复制了产生它的击键消息的lParam;

    4) 判断字符是ASCII码还是UNICODE:如果使用的是RegisterClassA注册的窗口,Translate的时候就转成ASCII码,如果使用RegisterClassW注册的窗口则翻译成UNICODE码,而使用通用版本的则转换成(TCHAR)wParam;

    5) 产看窗口是哪种编码方式:fUnicode = IsWindowUnicode( hWnd ),TRUE表示UNICODE编码,FALSE则为ASCII编码;


3. 击键消息和字符消息之间的排序问题:

主要分一下三种情况:

按一次A键并立即释放:


Shift->A,再立即释放:



连续按住A,最后释放:


如果程序反应不过来,WM_KEYDOWN的计数值大于1,则WM_CHAR的计数值和其相同(本身就是复制过来的)

最后还有一种就是Shift A按着不放的情形,这种就是上面的情形上第一个为Shift的WM_KEYDOWN,最后一个是Shift的WM_KEYUP,中间不会重复出现Shift的WM_KEYDOWN,这里Windows将其处理成和Caps Lock一样的锁定状态,而不会和A键一样产生连续的Shift的WM_KEYDOWN消息了!!!

当Alt、Shift、Ctrl和其它键同时一直按下的时候消息的排列方式和上述相同,Alt、Shift、Ctrl的KEYDOWN消息只在开头出现一次,不会重复出现于中间,并且它们的组合可以产生一些控制字符,控制字符如\n、\b等会包含在WM_CHAR的wParam中,并且还有一个有趣的现象就是,当Alt、Shift、Ctrl同时按下时,总是按照Alt->Shift->Ctrl的先后顺序排列,而释放的时候则是按照谁先被释放所释放(产生WM_KEYUP消息),如下显示:


这是Alt+Shift+Ctrl+A按下不放而产生NULL控制字符的一个消息排列过程示例


4. 处理控制字符:

    1) 通常Esc、空格、回车键会产生控制字符,而这些键也有虚拟键代码;

    2) 通常是在WM_CHAR消息中处理这些键的消息,比如

case WM_CHAR:
	switch ( wParam )
	{
	case '\b': // 退格键
		...
		break;

	case '\n': // 回车键
		...;\
		break;
	...
	}
...

5. 关于死字符消息:

    1) 其实总共有4中字符消息:


    2) 死字符出现在一些欧美语系的键盘上,某些键自己不产生字符,只有和一些字符按键配合使用时可以为那些字符加上音调符号,这些键会产生死字符消息(有死字符键击键消息产生,同样由TranslateMessage翻译产生),单独按下改建会产生WM_DEADCHAR消息,接着不放再按下响应的字母键就会产生一个wParam为带音调符号的字符的ASCII码的WM_CHAR消息;


6. KeyView1程序:

改程序记录每次击键,并跟踪每次物理击键后产生的键盘消息(击键消息和字符消息),如果是击键消息则显示其虚拟键码及其所代表的按键(按键都用大写表示),如果是字符消息,则显示其ASCII码以及所代表的字符,ASCI码用32为16进制表示,同时跟踪这两种消息的lParam的6个字段的值,按照重复次数、OEM扫描码、扩展标记、内容标记(Alt键是否被按下)、之前状态、转换状态的顺序打印出来:

#include <windows.h>

LRESULT CALLBACK WndProc( HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam );

int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd )
{
	static TCHAR	szAppName[] = TEXT("keyview1");

	WNDCLASS	wndclass;
	HWND		hWnd;
	MSG			msg;

	wndclass.style			= CS_HREDRAW | CS_VREDRAW;
	wndclass.lpfnWndProc	= WndProc;
	wndclass.cbClsExtra		= 0;
	wndclass.cbWndExtra		= 0;
	wndclass.hInstance		= hInstance;
	wndclass.hIcon			= LoadIcon( NULL, IDI_APPLICATION );
	wndclass.hCursor		= LoadCursor( NULL, IDC_ARROW );
	wndclass.hbrBackground	= (HBRUSH)GetStockObject( WHITE_BRUSH );
	wndclass.lpszMenuName	= NULL;
	wndclass.lpszClassName	= szAppName;

	RegisterClass( &wndclass );

	hWnd = CreateWindow(
		szAppName, TEXT("Keyboard Message Viewer Version 1"),
		WS_OVERLAPPEDWINDOW,
		CW_USEDEFAULT, CW_USEDEFAULT,
		CW_USEDEFAULT, CW_USEDEFAULT,
		NULL, NULL, hInstance, NULL
	);

	ShowWindow( hWnd, nShowCmd );
	UpdateWindow( hWnd );

	while ( GetMessage( &msg, NULL, 0, 0 ) )
	{
		TranslateMessage( &msg );
		DispatchMessage( &msg );
	}

	return msg.wParam;
}

LRESULT CALLBACK WndProc( HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam )
{
	// 分栏打印,并且各个标题加下划线(要使用等宽字体,否则下划线和字会不一致!)
	static TCHAR szTop[] = TEXT ("Message        Key       Char     ")
						   TEXT ("Repeat Scan Ext ALT Prev Tran");
	static TCHAR szUnd[] = TEXT ("_______        ___       ____     ")
						   TEXT ("______ ____ ___ ___ ____ ____");
	
	static TCHAR * szFormat[2] = { // 打印数据的格式字符串 
		TEXT ("%-13s %3d %-15s%c%6u %4d %3s %3s %4s %4s"), // WM_KEYDOWN消息的信息
        TEXT ("%-13s            0x%04X%1s%c %6u %4d %3s %3s %4s %4s") // WM_KEYCHAR消息的信息
	};

	static LPTSTR	szMessage[] = { // 按照windef.h中定义的顺序存储消息名称字符串
		TEXT("WM_KEYDOWN"),		TEXT("WM_KEYUP"),
		TEXT("WM_CHAR"),		TEXT("WM_DEADCHAR"),
		TEXT("WM_SYSKEYDOWN"),	TEXT("WM_SYSKEYUP"),
		TEXT("WM_SYSCHAR"),		TEXT("WM_SYSDEADCHAR")
	};

	static LPTSTR	szYes	= TEXT("Yes"); // 是与否的标志字符串
	static LPSTR	szNo	= TEXT("No");
	static LPSTR	szDown	= TEXT("Down"); // 表示一个键是否被按下
	static LPSTR	szUp	= TEXT("Up"); // 表示一个键是否被释放

	// 这里要关心屏幕分辨率改变后屏高有多少个像素和新的屏高能容纳多少行文本
	static int		cyClientMax, cLinesMax; 
	// 只需要知道字符高度即可,水平方向上文本打印用格式化字符串完成,不需要cxChar来控制
	static int		cxClient, cyClient, cyChar; 

	static PMSG		pmsg; // 将产生的键盘消息按照时间顺序保存在此缓冲区中,其最大容量由屏幕高度决定
						  // 将会根据屏幕高度动态调整缓冲区的大小
	static int		cLines; // 目前总共产生了多少键盘消息(并且是被记录在一个缓冲区中的)
	                        // 如果多于缓冲区容量的最早的消息将会被丢弃
	static int		cLinesScroll; // 滚动去能容纳的文本行数
	static RECT		rtScroll; // 客户区需要滚动的区域,就是第一行标题栏(消息名等的)以下的区域
	                          // 按照产生的消息由下到上翻滚,最下一行显示最新消息,最上一行显示最旧消息
	                          // 超出标题栏的部分不予以显示

	TCHAR	szBuffer[128]; // 格式化后的字符串的缓冲区
	TCHAR	szKeyName[32]; // 通过GetKeyNameText函数获取按键的名称(不是ASCII码)字符串

	HDC				hdc;
	PAINTSTRUCT		ps;
	TEXTMETRIC		tm;

	int		i;
	int		fnType; // 标记当前的消息是哪类消息,击键消息为0,字符消息为1

	switch ( message )
	{
	case WM_CREATE: // 窗口创建和屏幕分辨率改变做同样的初始化
	case WM_DISPLAYCHANGE: // 屏幕分辨率改变
		cyClientMax = GetSystemMetrics( SM_CYMAXIMIZED ); // 当前屏幕最大高度(以像素为单位)

		hdc = GetDC( hWnd ); // 获取字符高度

		GetTextMetrics( hdc, &tm );
		cyChar = tm.tmHeight + tm.tmExternalLeading;

		ReleaseDC( hWnd, hdc ); // 释放后等宽字体无效,下次获取DC还是默认的变宽字体

		if ( pmsg ) // 分辨率改变后要重新设置缓冲区
		{
			free( pmsg );
			pmsg = NULL;
		}
		cLinesMax = cyClientMax / cyChar;
		pmsg = (PMSG)malloc( cLinesMax * sizeof( MSG ) ); // 重设
		cLines = 0; // 同时缓冲区刷新,效果相当于重启程序,从头再来一遍

		// 此时不要return 0,以为分辨率的改变往往也导致窗口尺寸的改变,所以继续处理WM_SIZE
	case WM_SIZE:
		cxClient = LOWORD( lParam );
		cyClient = HIWORD( lParam );

		cLinesScroll = cyClient / cyChar - 1; // 滚动区能容纳的文本行数

		// 客户区尺寸改变必然导致翻滚区域的改变
		// 翻滚区域不包含第一行的标题栏
		rtScroll.left	= 0;
		rtScroll.right	= cxClient;
		rtScroll.top	= cyChar; // 除去第一行
		// 刚好显示行的整数倍,不留一点儿空,更加美观!
		rtScroll.bottom	= cyChar * ( cyClient / cyChar );

		InvalidateRect( hWnd, NULL, TRUE ); // 是为WM_DISPLAYCHANGE准备的!分别率改变后必须重刷!
		return 0;

	// 记录所有的键盘消息
	case WM_SYSKEYDOWN:
	case WM_SYSKEYUP:
	case WM_KEYDOWN:
	case WM_KEYUP:
	case WM_CHAR:
	case WM_DEADCHAR:
	case WM_SYSCHAR:
	case WM_SYSDEADCHAR:
		// 进来的新消息放在队头,后面的消息往后挪一格,超出缓冲区的消息被舍弃
		for ( i = cLinesMax - 1; i > 0; i-- ) // 行数不多,无需优化
		{
			pmsg[i] = pmsg[i - 1];			
		}

		// 将新消息插入队头
		pmsg[0].hwnd	= hWnd;
		pmsg[0].message	= message;
		pmsg[0].wParam	= wParam;
		pmsg[0].lParam	= lParam;

		cLines = min( cLines + 1, cLinesMax ); // 当前读入的消息数量不能超出缓冲区,超出就舍弃

		// 新来的消息信息显示在底部,之前的消息信息向上翻滚一行
		ScrollWindow( hWnd, 0, -cyChar, &rtScroll, &rtScroll );
		// 最然这里没有人为的将最新的小心信息用TextOut显示在最下面一行
		// 但是注意翻滚矩形区域也作为剪裁区域传入ScrollWindow,这说明
		// ScrollWindow底层利用了WM_PAINT的代码绘制,并且剪裁矩形作为ps.rcPaint来使用

		break; // 不要直接return,把系统消息交由DefWindowProc来处理

	case WM_PAINT:
		hdc = BeginPaint( hWnd, &ps );

		SelectObject( hdc, GetStockObject( SYSTEM_FIXED_FONT ) ); // 设置为等宽字体,字体也是GDI对象之一
		SetBkMode( hdc, TRANSPARENT ); // 让字的背景透明,这样在打印下划线的时候就不会用白刷把字刷白
		TextOut( hdc, 0, 0, szTop, lstrlen( szTop ) ); // 输出具有下划线的标题栏(客户区第一行)
		TextOut( hdc, 0, 0, szUnd, lstrlen( szUnd ) );
		// 如果是翻滚的话,第一行就不在剪裁区域之内了

		for ( i = 0; i < min( cLines, cLinesScroll ); i++ )
		{
			fnType = WM_CHAR == pmsg[i].message		||
					 WM_SYSCHAR == pmsg[i].message	||
					 WM_DEADCHAR == pmsg[i].message	||
					 WM_SYSDEADCHAR == pmsg[i].message;
			// 查看是那种键盘消息,击键消息和字符消息输出信息格式不一致

			// 该函数可以获得击键消息对应的键的名称字符串
			// 判断是哪个键用的是击键消息的lParam,应该是基于OEM码识别的,可能不同厂商的键盘的键
			// 的名字不一样吧!
			GetKeyNameText( pmsg[i].lParam, szKeyName, sizeof( szKeyName ) / sizeof( TCHAR ) );

			TextOut( hdc, 0, ( cyClient / cyChar - i - 1 ) * cyChar, szBuffer, wsprintf( szBuffer, szFormat[fnType],
				szMessage[ pmsg[i].message - WM_KEYFIRST ], // 显示消息的名称,顺序和windef.h中定义的顺序一致
				pmsg[i].wParam, // 虚拟键代码,用十进制表示,如果是字符消息按照选择的格式将输出ASCII码的32位16进制表示
				(LPTSTR)( fnType ? TEXT(" ") : szKeyName ), // 如果是击键消息则显示键名,字符消息直接在下面显示字符就行了
				(TCHAR)( fnType ? pmsg[i].wParam : ' ' ), // 如果是字符消息直接显示字符,击键消息空出来
				LOWORD( pmsg[i].lParam ), // 重复计数
				HIWORD( pmsg[i].lParam ) & 0xFF, // OEM扫描码
				0x01000000 & pmsg[i].lParam ? szYes : szNo, // 扩展标记
				0x20000000 & pmsg[i].lParam ? szYes : szNo, // 内容标记(是否按下Alt)
				0x40000000 & pmsg[i].lParam ? szUp : szDown, // 之前的状态
				0x80000000 & pmsg[i].lParam ? szUp : szDown // 转换状态
				)
			);
		}

		EndPaint( hWnd, &ps );
		return 0;

	case WM_DESTROY:
		PostQuitMessage( 0 );
		return 0;
	}

	return DefWindowProc( hWnd, message, wParam, lParam );
}

7. Typer程序:

改程序是一个简单的文本编辑器,支持光标移动见(文本光标,通常光标是指鼠标在屏幕上的位图表示),按下Esc键擦除窗口内容,调整窗口大小或改变键盘输入语言时窗口内容也将被擦除,支持上下左右光标移动键,以及退格、Delete等操作:

// typer.c

#include <windows.h>

#define BUFFER(x,y)		( *( pBuffer + (y) * cxBuffer + (x) ) )

LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam );

int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd )
{
	static TCHAR	szAppName[] = TEXT("typer");

	WNDCLASS	wndclass;
	HWND		hwnd;
	MSG			msg;

	wndclass.style			= CS_HREDRAW | CS_VREDRAW;
	wndclass.lpfnWndProc	= WndProc;
	wndclass.cbClsExtra		= 0;
	wndclass.cbWndExtra		= 0;
	wndclass.hInstance		= hInstance;
	wndclass.hIcon			= LoadIcon( NULL, IDI_APPLICATION );
	wndclass.hCursor		= LoadCursor( NULL, IDC_ARROW );
	wndclass.hbrBackground	= (HBRUSH)GetStockObject( WHITE_BRUSH );
	wndclass.lpszMenuName	= NULL;
	wndclass.lpszClassName	= szAppName;

	RegisterClass( &wndclass );

	hwnd = CreateWindow(
		szAppName, TEXT("Typing Program"),
		WS_OVERLAPPEDWINDOW,
		CW_USEDEFAULT, CW_USEDEFAULT,
		CW_USEDEFAULT, CW_USEDEFAULT,
		NULL, NULL, hInstance, NULL
	);

	ShowWindow( hwnd, nShowCmd );
	UpdateWindow( hwnd );

	while ( GetMessage( &msg , NULL, 0, 0 ) )
	{
		TranslateMessage( &msg );
		DispatchMessage( &msg );
	}

	return msg.wParam;
}

LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam )
{
	static DWORD	dwCharSet = DEFAULT_CHARSET; // 初始化为系统默认的字符集
	static int		cxChar, cyChar;
	static int		cxClient, cyClient;

	// 文本编辑框内的文本暂时保存在该缓冲区Buffer中
	static PTCHAR	pBuffer = NULL;
	static int		cxBuffer, cyBuffer; // 用一维缓冲区模拟二维缓冲区(因为malloc的时候比较方便,不需要for循环)

	static int		xCaret, yCaret; // 文本光标的位置

	HDC				hdc;
	PAINTSTRUCT		ps;
	TEXTMETRIC		tm;

	int		x, y;
	int		i;

	switch ( message )
	{
	case WM_INPUTLANGCHANGE: // 输入语言改变,需要相应地改变程序所使用的字符集
		dwCharSet = wParam;
		// 接着连通WM_CREATE的代码,为了获取新字符集的字体宽度和高度信息

	case WM_CREATE:
		hdc = GetDC( hwnd );
		// 选入新字符集,并设为等宽(就是不变宽),变宽就是VARIABLE_PITCH了
		SelectObject( hdc, CreateFont( 0, 0, 0, 0, 0, 0, 0, 0, dwCharSet, 0, 0, 0, FIXED_PITCH, NULL ) );

		GetTextMetrics( hdc, &tm );
		cxChar = tm.tmAveCharWidth;
		cyChar = tm.tmHeight + tm.tmExternalLeading;

		DeleteObject( SelectObject( hdc, GetStockObject( SYSTEM_FONT ) ) ); // 刚刚Create出来的字体删掉
		ReleaseDC( hwnd, hdc ); // 释放后一切设置都失效,下次获得DC后一切都需要重新设置,否则都将是系统默认设置
		// 接着WM_SIZE的代码,获得一片缓冲区,缓冲区的大小有客户区大小和字体大小共同决定,因为也有字体大小的因素,
		// 所以WM_INPUTLANGCHANGE的字体的改变也需要设置相应的缓冲区的大小,所以是一环接一环的

	case WM_SIZE:
		if ( WM_SIZE == message ) // 可能信息来自以上面两个消息,则此时的lParam不再是当前客户区的尺寸了
		{ // 如果不加以判断可能会导致客户区尺寸计算错误!
			cxClient = LOWORD( lParam );
			cyClient = HIWORD( lParam );
		}

		// 缓冲区存放的就是字符,因此单位就是个(可能是UNICODE的,因此盲目用字节算)
		cxBuffer = max( 1, cxClient / cxChar );
		cyBuffer = max( 1, cyClient / cyChar );

		if ( !pBuffer )
		{
			free( pBuffer );
			pBuffer = NULL;
		}

		// 内容初始化为空白,文本光标也初始化在左上角了
		pBuffer = (PTCHAR)malloc( cxBuffer * cyBuffer * sizeof( TCHAR ) );
		memset( pBuffer, ' ', cxBuffer * cyBuffer * sizeof( TCHAR ) ); // 都初始化为空白

		xCaret = 0;
		yCaret = 0;

		// 记住!只有在绘制前后用Hide和ShowCaret,以及获得失去焦点时分别Show和Hide,其余情况
		// 一律不准用这两个函数!
		// 因为Show和Hide的效果是叠加的,当其中一个连用多次,另一个必须也用同样多次才能起效果!!!

		//if (  hwnd == GetFocus() ) // 收到WM_SIZE消息不一定代表具有输入焦点
		SetCaretPos( 0, 0 );

		// 产生WM_PAINT队列消息,并且BeginPaint的时候无需擦除背景
		// 因为缓冲区文本为空,输出到客户区的本来就是空白,因此无需先将背景擦白
		// 节省时间
		InvalidateRect( hwnd, NULL, FALSE );
		return 0;

	case WM_SETFOCUS: // 当期窗口获得焦点
		// 首先在指定窗口创建光标
		// 第二个参数是光标的位图句柄,只在自定义的时候使用,NULL表示使用系统默认的光标
		// 后面两个是光标的宽和高(以像素为单位)
		CreateCaret( hwnd, NULL, cxChar, cyChar );
		// 改变当前光标的位置(像素为单位)
		// 由于一个消息队列仅支持一个文本光标(即一个应用程序仅支持一个光标)
		// 所以SetCaretPos的时候不需要传什么文本光标句柄之类
		// 这从逻辑上将也是说得通的,因为同时朝多个窗口输入文字是没有意义的
		// 输入焦点窗口有且仅有一个而已
		SetCaretPos( xCaret * cxChar, yCaret * cyChar ); 
		// 在指定窗口显示文本光标
		// 在开启该功能之后使用SetCaretPos则光标会随设定的位置随时改变(显式的)
		// 但是如果用HideCaret( hwnd ),就会将光标隐藏起来,之后再SetPos就只能隐式改变位置了
		ShowCaret( hwnd ); 
		return 0;

	case WM_KILLFOCUS: // 当前窗口失去焦点
		HideCaret( hwnd );
		DestroyCaret(); // 隐藏后直接从内存中删除资源,留着浪费内存,重新获取焦点后再创建
		return 0;

	case WM_KEYDOWN:
		switch ( wParam )
		{
		case VK_HOME: // 水平起始
			xCaret = 0;
			break;

		case VK_END: // 水平末尾
			xCaret = cxBuffer - 1;
			break;

		case VK_PRIOR: // 上翻平移置顶(水平位置不变)
			yCaret = 0;
			break;

		case VK_NEXT: // 下翻沉底
			yCaret = 0;
			break;

		case VK_LEFT: // 左光标左移一位
			xCaret = max( xCaret - 1, 0 ); // 移到头不能移了
			break;

		case VK_RIGHT: // 右光标右移一位
			xCaret = min( xCaret + 1, cxBuffer - 1 );
			break;

		case VK_UP: // 上光标上移一位
			yCaret = max( yCaret - 1, 0 );
			break;

		case VK_DOWN: // 下光标下移一位
			yCaret = min( yCaret + 1, cyBuffer - 1 );
			break;

		// 以上都仅仅是移动光标的问题,break跳出后直接用SetCaretPos移动即可
		// 接下来的Delete虽然无需移动光标但是需要删除字符,即后面的字符要向前挪一位覆盖
		// 因此就涉及到绘制了,绘制的原则就是在绘制过程中隐藏光标,绘制完成后再显示光标
		// 这样更加人性化更加合理
		case VK_DELETE:
			HideCaret( hwnd );

			for ( x = xCaret; x + 1 < cxBuffer; x++ )
			{
				BUFFER( x, yCaret ) = BUFFER( x + 1, yCaret );
			}
			BUFFER( cxBuffer - 1, yCaret ) = ' '; // 行末补空格(TCHAR可以这样赋值,赋值在低位)

			hdc = GetDC( hwnd );
			SelectObject( hdc, CreateFont( 0, 0, 0, 0, 0, 0, 0, 0, dwCharSet, 0, 0, 0, FIXED_PITCH, NULL ) );
			TextOut( hdc, xCaret * cxChar, yCaret * cyChar, &BUFFER( xCaret, yCaret ), cxBuffer - xCaret ); // 绘制
			DeleteObject( SelectObject( hdc, GetStockObject( SYSTEM_FONT  ) ) ); // 删除创建的当前选择的字体(Create的能删,Stock的不能删)
			ReleaseDC( hwnd, hdc );

			ShowCaret( hwnd ); // 显示出来
			return 0; // 由于Delete是删除后面的,光标位置不变,可以直接return
		}

		SetCaretPos( xCaret * cxChar, yCaret * cyChar );
		return 0;
	
	case WM_CHAR: // 有输入的字体,也包含一些控制字(回车、退格、Esc等)
		for ( i = 0; i < LOWORD( lParam ); i++ ) // 考虑到一些连续输入的字符处理不过来,因此跟踪这些计数值
		{ // 因为我们这里假设用户输入的每个字符都是重要的,但是击键消息就不跟踪计数了,为了避免无意识的滚动
			switch ( wParam )
			{
			case '\b': // 退格
				if ( xCaret )
				{
					xCaret--; // 用Delete模拟退格
					SendMessage( hwnd, WM_KEYDOWN, VK_DELETE, 1 );
				}
				break;
				
			case '\t': // Tab
				do { // Tab的真正含义是往右移动最少的若干个空格使得当前位置刚好是len(Tab)的整数倍!
					SendMessage( hwnd, WM_CHAR, ' ', 1 );
				} while ( xCaret % 8 );
				break;

			case '\n': // Ctrl + Enter,功能和下光标键一样,只不过最后一行再\n就回到第一行去了
				if ( ++yCaret == cyBuffer ) yCaret = 0;
				break;

			case '\r': // Enter,在\n的基础上跳到行始
				xCaret = 0;
				if ( ++yCaret == cyBuffer ) yCaret = 0;
				break;

			case '\x1B': // Esc,刷掉重来
				memset( pBuffer, ' ', cxBuffer * cyBuffer * sizeof( TCHAR ) );
				xCaret = 0;
				yCaret = 0;
				InvalidateRect( hwnd, NULL, TRUE );
				break;

			default: // 正常地显示字符,需要绘制!
				HideCaret( hwnd );

				BUFFER( xCaret, yCaret ) = (TCHAR)wParam;
				hdc = GetDC( hwnd );
				SelectObject( hdc, CreateFont( 0, 0, 0, 0, 0, 0, 0, 0, dwCharSet, 0, 0, 0, FIXED_PITCH, NULL ) );
				TextOut( hdc, xCaret * cxChar, yCaret * cyChar, &BUFFER( xCaret, yCaret ), 1 ); // 输出输入的那个字符
				DeleteObject( SelectObject( hdc, GetStockObject( SYSTEM_FONT ) ) );
				ReleaseDC( hwnd, hdc );

				ShowCaret( hwnd );

				// 光标往后挪一位
				if ( ++xCaret == cxBuffer )
				{
					xCaret = 0;
					if ( ++yCaret == cyBuffer ) yCaret = 0;
				}
				break;
			} // switch
		} // for

		SetCaretPos( xCaret * cxChar, yCaret * cyChar );
		return 0;

	case WM_PAINT:
		HideCaret( hwnd );

		hdc = BeginPaint( hwnd, &ps );
		SelectObject( hdc, CreateFont( 0, 0, 0, 0, 0, 0, 0, 0, dwCharSet, 0, 0, 0, FIXED_PITCH, NULL ) );
		for ( y = 0; y < cyBuffer; y++ )
		{
			TextOut( hdc, 0, y * cyChar, &BUFFER( 0, y ), cxBuffer );
		}
		DeleteObject( SelectObject( hdc, GetStockObject( SYSTEM_FONT ) ) );
		EndPaint( hwnd, &ps );

		ShowCaret( hwnd );
		return 0;

	case WM_DESTROY:
		PostQuitMessage( 0 );
		return 0;
	} // switch

	return DefWindowProc( hwnd, message, wParam, lParam );
} // WndProc


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值