dustland

dustball in dustland

win32程序设计-chapter6 键盘

windows SDK chapter 6 keyboard

键盘基础

who is using the keyboard?

正在使用键盘的窗口,称为有输入焦点的窗口

windows操作系统上处于最顶层的窗口往往是焦点窗口,然而存在两分屏看似并列的情况,实际上这时候可以从多方面判断哪个窗口是焦点窗口

比如从任务栏

image-20220812083837696

这里Typora上有高亮,就是焦点窗口,

windows终端,DEVC++,VisualStudio等等都是黑色的,不是焦点窗口

又如从标题栏

image-20220812083907654
左侧typora为焦点窗口,其标题栏为深色.

在word,wps等等文字编辑器中,是否获得焦点更加明显,只要光标在闪烁就意味着持有焦点

实际上窗口过程通过处理WM_SETFOCUS,WM_KILLFOCUS两个消息来判断自己有没有焦点

1
2
3
4
5
6
case WM_SETFOCUS:
cout<<"focused"<<endl;
return 0;
case WM_KILLFOCUS:
cout<<"left"<<endl;
return 0;
1
PS C:\Users\86135\desktop\myWin32> g++ main.cpp -O0 -o main -lkernel32 -lgdi32 -luser32 -m32
焦点在windows终端
焦点在the Hello Program

队列与同步

键盘动作由键盘驱动程序转化为格式化的消息之后,首先发往windows消息队列,不会直接发往焦点窗口

这是因为,有的键盘消息具有修改焦点窗口的功能,比如Alt+F4可关闭当前窗口.又比如win+r可以打开运行框,此时窗口焦点自动放在运行框上

windows需要先看一看键盘消息是针对当前焦点窗口的还是针对整个系统的.

如果发生了焦点转移,那么windows需要保证后续的相应键盘消息指向新的焦点窗口

击键消息

image-20220812085353287

其中系统键一般是Alt+其他键的组合键,比如Alt+Tab,切换任务窗口.

键按下之后如果不松开,会间隔一定时间(这个间隔在系统启动时BIOS中设置)之后进入连续输入状态(连续输入的速度也取决于BIOS中的设置),此时应用程序会收到一连串的WM_KEYDOWN或者WM_SYSKEYDOWN消息

系统键对于windows操作系统来说更加重要,应用程序一般忽略系统键信息,交给DefWindowProc处理.windows会处理所有Alt组合键信息

如果非得在应用程序中处理Alt消息,并且处理完成之后立刻返回,不调用DefWindowProc,那么系统键消息将会被应用程序截胡,不能发给操作系统.此时按下Alt+F4就无法关闭该窗口了

比如:

1
2
3
4
5
6
case WM_SYSKEYDOWN:
cout << "sysKey down" << endl;
return 0;
case WM_SYSKEYUP:
cout << "sysKey up" << endl;
return 0;

此时只会在终端上打印一下,但是只要时Alt键的消息都不会发往windows

当组合键中没有Alt时,不会产生系统键消息,顶多产生WM_KEYDOWN和WM_KEYUP,应用程序可以根据自己的兴趣选择处理其中的部分消息,如果应用程序不做处理,windows也不做处理

不管是系统键消息还是非系统键消息,不管是按下还是起来,所有的键盘消息都会伴随着wParam表示虚拟键代码,lParam包含本次击键的其他数据

虚拟键代码wParam

捕获WM_KEYDOWN或者WM_KEYUP消息只能说明非系统键被按下或者松开了,单凭着一个信息无法判断谁被按下或者松开了.wParam就提供了更多的细节

该案件消息是一个枚举类型,大多数都以VK_开头

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#define VK_LBUTTON 0x01
#define VK_RBUTTON 0x02
#define VK_CANCEL 0x03
#define VK_MBUTTON 0x04
#define VK_XBUTTON1 0x05
#define VK_XBUTTON2 0x06
#define VK_BACK 0x08
#define VK_TAB 0x09
#define VK_CLEAR 0x0C
#define VK_RETURN 0x0D
#define VK_SHIFT 0x10
#define VK_CONTROL 0x11
#define VK_MENU 0x12
#define VK_PAUSE 0x13
...

前两个鼠标动作貌似通过处理不到

数字键(不是右侧数字键盘,是左侧主键盘上一长条的数字键)和字母键:

image-20220812092101119

虽然键盘可以接收到键盘上@#$%*这种字符的消息,但是这是对于英语键盘而言的,对于欧洲人来说欧元符号比美元符号更加重要,如果要写出地区无关的代码,那么我们需要和windows操作系统协作处理键盘消息,对于有意义的字符的处理,一般使用字符消息,不使用击键消息

微软键盘特有:

image-20220812092200252

需要给哪个键安排任务的时候就去查它的宏定义编码即可

其他消息lParam

image-20220812094137721

重复计数Repeat Count

低16位为重复计数,当一个键按下一直不松开时,应用程序会接收到一大串WM_KEYDOWN消息.如果窗口过程的处理速度跟不上,那么windows会把多个WM_KEYDOWN消息合并成一条消息,然后修改其lParam的低16位,决定这条消息重复了多少次

在比较卡顿的电脑上使用word有过这种体验,一直按着某个键不放,计算机可能不会实时跟着显示字符,但是一段时间后呼哧出来一摊这个字符

OEM扫描码

历史古董了,现在几乎不用了

拓展键标记Extended Key Flag

对于IBM拓展键盘才有用,一般的键盘上已经有足够多的键用了,不需要拓展了

内容代码Context Code

如果Alt键被按下,则相应的键盘消息中该值为1

先前状态Previous Key State

如果先前改键是松开的则该值为0

如果先前该键是按下的则该值为1

这个值可以用来去除重复输入,比如老年人动作缓慢,本来就只想输入一个A结果按下A忘了松开了 ,可以判断先前该键的状态,如果也是按下的则抛弃

1
2
3
4
case WM_KEYDOWN:
if((lParam>>30)&1){//lParam第31位是键先前状态,右移30位之后成为最低位,和1按位与只保留该位
return 0;
}

转换状态Transition State

键正在被按下则转换状态为0

键正在被释放则转换状态为1

转义状态

转义键:Shift,Alt,Ctrl

切换键:Caps Lock,Num Lock,Scroll Lock

区分主键盘上大小写输入就要看这些键的状态

考虑如何判断一套组合键都有谁按下了呢?比如Ctrl+F,首先按下的是Ctrl,然后按下F,但是按下F的时候引用程序已经处理过了Ctrl,现在只知道按下了F.这样看来是不是还得开一些变量记录刚才按下了谁呢?当Ctrl松开的时候这个变量置零,当Ctrl按下的时候这个变量置1.

windows操作系统确实是按照这个思想做的,它给我们代劳了.我们要判断Ctrl的状态,只需要GetKeyState(VK_CONTROL);就可以获取windows帮我们记录好的Ctrl状态了

1
2
3
SHORT GetKeyState(
[in] int nVirtKey
);

当需要查询状态的键处于按下时,该函数返回负值.否则最低位置1

可以通过是否小于0或者和1按位与判断状态

1
2
3
4
5
6
7
8
9
if(wParam<=0x5A&&wParam>=0x41){//首先判断是不是英文字母
if(GetKeyState(VK_CAPITAL)){
szBuffer[cnt_keys]=wParam;//大写字母添加进入缓冲区
}
else{
szBuffer[cnt_keys]=wParam-'A'+'a';//小写字母添加进入缓冲区
}
++cnt_keys;
}

应用

给滚动条添加键盘动作:

按下PgUp往前翻页,按下PgDn往后翻页

按上下键滚动一行

按左右键左右滚动一个字符的宽度

实际上直接发送信息处理滚动条消息即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
case WM_KEYDOWN:
switch (wParam) {
case VK_HOME:
SendMessage(hwnd, WM_VSCROLL, SB_BOTTOM, 0);
break;
case VK_END:
SendMessage(hwnd, WM_VSCROLL, SB_TOP, 0);
break;
case VK_PRIOR:
SendMessage(hwnd, WM_VSCROLL, SB_PAGEUP, 0);
break;
case VK_NEXT:
SendMessage(hwnd, WM_VSCROLL, SB_PAGEDOWN, 0);
case VK_UP:
SendMessage(hwnd, WM_VSCROLL, SB_LINEUP, 0);
break;
case VK_DOWN:
SendMessage(hwnd, WM_VSCROLL, SB_LINEDOWN, 0);
break;
case VK_RIGHT:
SendMessage(hwnd, WM_HSCROLL, SB_LINERIGHT, 0);
break;
case VK_LEFT:
SendMessage(hwnd, WM_HSCROLL, SB_LINELEFT, 0);
break;
}

字符消息

再看消息循环

1
2
3
4
5
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}

在主函数的最后,返回之前,像定式一样必须要有一个消息循环.

其中GetMessage从应用程序的消息队列中,获取一条新消息,用msg承载该消息

DispatchMessage将该消息分拣交给相应窗口

TranslageMessage将可打印字符的击键消息转换为字符消息,然后将该字符消息放入应用程序的消息队列

可打印字符包括英文字母,阿拉伯数字,标点符号,运算符号等等

不包括Ctrl,Shift,Insert,Delete,Alt

字符消息的种类:

image-20220812105605047

死字符只对需要重音符号的语言键盘有用,比如德语.可以忽略

应用程序主要就处理WM_CHAR消息,该消息从WM_KEYDOWN翻译而来.

WM_SYSCHAR消息从WM_SYSKEYDOWN翻译而来

参数意义

对于字符消息,lParam参数的含义和击键消息相同

image-20220812201421640

wParam是ANSI或者Unicode编码的字符码,这一点和击键消息中不同

具体是用的ANSI还是Unicode编码,要看注册窗口类的时候调用的是RegisterClassA还是RegisterClassW

如果使用RegisterClass,最近的windows操作系统上会被宏定义为RegisterClassW.因为Windows 2000之后Unicode标识符就被定义了

1
2
3
4
5
#ifdef UNICODE
#define RegisterClass RegisterClassW
#else
#define RegisterClass RegisterClassA
#endif // !UNICODE

先来后到

当字符键盘比如'A'按下后,对于应用程序的消息队列

首先收到的是'A'的击键消息WM_KEYDOWN,虚拟键代码wParam=0x41

然后收到的是'A'的字符消息WM_CHAR,字符编码wParam=0x61

然后'A'松开时收到击键消息WM_KEYUP,虚拟键代码wParam=0x41

各司其职

对于功能键比如Ctrl,Shift等等,作为虚拟键,需要处理WM_KEYDOWN消息

对于可打印字符就需要处理字符消息WM_CHAR

Tab,回车,空格,Esc作为控制字符也处理WM_CHAR

image-20220812203020207
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
case WM_CHAR:
switch (wParam)
{
case '\b':
cout << "space down" << endl;
break;
case '\t':
cout << "Tab down" << endl;
break;
case '\n':
cout << "Ctrl+Enter down" << endl;
break;
case '\r':
cout << "Enter down" << endl;
break;
}
return 0;

应用

windows程序设计在此给出了一个例子,记录键盘动作并且输出到屏幕

分析其过程函数WndProc

变量定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
	static int cxClientMax, cyClientMax, cxClient, cyClient, cxChar, cyChar;
//cxClientMax为客户区最大宽度,根据该值决定保存显示哪一部分消息
//cxClient是客户区宽度
static int cLinesMax, cLines;//当前客户区最大能打印多少行,已经使用了多少行
static PMSG pmsg;//消息结构体
static RECT rectScroll;//客户区矩形
static TCHAR szTop[] = TEXT("Message Key Char ") TEXT("Repeat Scan Ext ALT Prev Tran");//顶部栏目
static TCHAR szUnd[] = TEXT("_______ ___ ____ ") TEXT("______ ____ ___ ___ ____ ____");//下划线
static TCHAR szFormat[2][100] = {//格式化字符串,用于szTop和szUnd的打印格式
TEXT("%-13s %3d %-15s%c%6u %4d %3s %3s %4s %4s"),
TEXT("%-13s 0x%04X%1s%c %6u %4d %3s %3s %4s %4s")
};

static TCHAR szYes[] = TEXT("Yes");
static TCHAR szNo[] = TEXT("No");
static TCHAR szDown[] = TEXT("Down");
static TCHAR szUp[] = TEXT("Up");
static TCHAR szMessage[8][100] = {//八种键盘消息
TEXT("WM_KEYDOWN"), TEXT("WM_KEYUP"),
TEXT("WM_CHAR"), TEXT("WM_DEADCHAR"),
TEXT("WM_SYSKEYDOWN"),TEXT("WM_SYSKEYUP"),
TEXT("WM_SYSCHAR"), TEXT("WM_SYSDEADCHAR")
};
HDC hdc;//设备环境句柄
int i, iType;//循环变量,
PAINTSTRUCT ps;
TCHAR szBuffer[128], szKeyName[32];
TEXTMETRIC tm;//字符信息结构体

pmsg

存放消息的结构体

1
2
3
4
5
6
7
8
9
10
11
typedef struct tagMSG {
HWND hwnd;
UINT message;
WPARAM wParam;
LPARAM lParam;
DWORD time;
POINT pt;
#ifdef _MAC
DWORD lPrivate;
#endif
} MSG, *PMSG, NEAR *NPMSG, FAR *LPMSG;

其前四个参数和WndProc窗口过程函数相同

1
LRESULT CALLBACK    WndProc(HWND, UINT, WPARAM, LPARAM);

该结构体的作用就是完整记录一条消息,甚至连消息产生时间time都有记录

POINT pt记录的是发生该消息时鼠标的位置(相对于整块屏幕的坐标)

ps

记录绘画信息的结构体

1
2
3
4
5
6
7
8
typedef struct tagPAINTSTRUCT {
HDC hdc;
BOOL fErase;
RECT rcPaint;
BOOL fRestore;
BOOL fIncUpdate;
BYTE rgbReserved[32];
} PAINTSTRUCT, *PPAINTSTRUCT, *NPPAINTSTRUCT, *LPPAINTSTRUCT;
hdc

设备环境句柄

fErase

表明背景是否擦除,非0则擦除

rcPaint

需要重绘的矩形范围

剩下三个成员尚未使用

窗口创建和变形消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
case WM_CREATE:
case WM_DISPLAYCHANGE://窗口最大化还有普通状态变化时获取该消息
// Get maximum size of client area
cxClientMax = GetSystemMetrics(SM_CXMAXIMIZED);//顶层窗口最大化时的宽度(像素)
cyClientMax = GetSystemMetrics(SM_CYMAXIMIZED);
// Get character size for fixed-pitch font
hdc = GetDC(hwnd);
SelectObject(hdc, GetStockObject(SYSTEM_FIXED_FONT));//使用系统等宽字体
GetTextMetrics(hdc, &tm);//获取系统等宽字体信息
cxChar = tm.tmAveCharWidth;//字符宽度,列宽度
cyChar = tm.tmHeight+tm.tmExternalLeading;//行高度
ReleaseDC(hwnd, hdc);
// Allocate memory for display lines
if (pmsg)//如果pmsg非NULL则表明先前已经给他分配过堆空间,那么本次需要重新分配,先把以前的扬了
free(pmsg);
cLinesMax = cyClientMax / cyChar;//计算行数
pmsg = (PMSG)malloc(cLinesMax * sizeof(MSG));//根据当前客户区大小决定pmsg数组大小
cLines = 0;//已使用行数清零
// fall through
//此处没有返回直接继续执行case WM_SIZE标签中的内容
case WM_SIZE:
if (message == WM_SIZE)//由于能够到达此块的不止WM_SIZE消息,还有可能时WM_CREATE和WM_DISPLAYCHANGE
{
cxClient = LOWORD(lParam);//只有窗口尺寸发生变化时才重新获取客户区尺寸
cyClient = HIWORD(lParam);
}
// Calculate scrolling rectangle
//滚动矩形的
rectScroll.left = 0;
rectScroll.right = cxClient;
rectScroll.top = cyChar;//滚动矩形起点是第一行,第0行用于打印栏目常量
rectScroll.bottom = cyChar * (cyClient / cyChar);//滚动矩形的底是客户区最多能容纳的那一行
InvalidateRect(hwnd, NULL, TRUE);//一旦客户区尺寸变化则重绘整个客户区
return 0;

键盘消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
	//所有的键盘动作,包括击键消息和字符消息还有系统键消息都记录下来
case WM_KEYDOWN://键盘按下
case WM_KEYUP://键盘起来
case WM_CHAR://字符消息
case WM_DEADCHAR://死字符消息
case WM_SYSKEYDOWN://系统键Alt按下
case WM_SYSKEYUP://系统键起来
case WM_SYSCHAR://系统字符消息
case WM_SYSDEADCHAR://系统死字符消息
// Rearrange storage array
for (i = cLinesMax - 1; i > 0; i--)
{
pmsg[i] = pmsg[i - 1];//新的键盘记录放到pmsg[0],历史消息都顺次后移一个
//如果消息总数超过了cLinesMax则丢弃早些时候到达的消息,实际上维护了一个队列
}
// Store new message
pmsg[0].hwnd = hwnd;//记录最新键盘消息
pmsg[0].message = message;
pmsg[0].wParam = wParam;
pmsg[0].lParam = lParam;
cLines = min(cLines + 1, cLinesMax);//更新 "已使用行数" ,但是最大值不能超过最大行数
// Scroll up the display
ScrollWindow(hwnd, 0, -cyChar, &rectScroll, &rectScroll);//滚动rectScroll对应区域,实际上整个网上平移cyChar即一行的高度
break; // i.e., call DefWindowProc so Sys messages work

"i.e., call DefWindowProc so Sys messages work"翻译成人话:

"也就是说,调用DefWindowProc"处理系统键消息

最后这里用的是break不是return,意味着只是看看键盘消息是谁而不进行拦截,记录一下接着丢给系统做取舍,保证Alt系统消息能够被正确处理

绘图消息

前面的一切消息处理都是在给绘图消息的处理做准备

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
case WM_PAINT:
hdc = BeginPaint(hwnd, &ps);
SelectObject(hdc, GetStockObject(SYSTEM_FIXED_FONT));
SetBkMode(hdc, TRANSPARENT);
TextOut(hdc, 0, 0, szTop, lstrlen(szTop));//打印最顶上第0行
TextOut(hdc, 0, 0, szUnd, lstrlen(szUnd));
for (i = 0; i < min(cLines, cyClient / cyChar - 1); i++)//一共打印cLines行,但是不能超过客户区总行数
{
iType = pmsg[i].message == WM_CHAR ||//如果是字符消息则iType=1否则为0
pmsg[i].message == WM_SYSCHAR ||
pmsg[i].message == WM_DEADCHAR ||
pmsg[i].message == WM_SYSDEADCHAR;
GetKeyNameText(pmsg[i].lParam, szKeyName,
sizeof(szKeyName) / sizeof(TCHAR));//获取表示键名的字符串
TextOut(hdc, 0, (cyClient / cyChar - 1 - i) * cyChar, szBuffer,
wsprintf(szBuffer, szFormat[iType],//根据szFormat[iType]格式化字符串进行打印,即区分字符消息和虚拟键消息
szMessage[pmsg[i].message - WM_KEYFIRST],///如此计算出来正好得到消息类型
pmsg[i].wParam,
(PTSTR)(iType ? TEXT(" ") : szKeyName),//如果是字符类型则打印空字符串否则打印键名
(TCHAR)(iType ? pmsg[i].wParam : ' '),//如果是字符类型则打印该字符,否则打印空串
LOWORD(pmsg[i].lParam),
HIWORD(pmsg[i].lParam) & 0xFF,
0x01000000 & pmsg[i].lParam ? szYes : szNo,
0x20000000 & pmsg[i].lParam ? szYes : szNo,
0x40000000 & pmsg[i].lParam ? szDown : szUp,
0x80000000 & pmsg[i].lParam ? szUp : szDown));
}
EndPaint(hwnd, &ps);
return 0;

其中szMessage[pmsg[i].message - WM_KEYFIRST]很巧妙

szMessage在WndProc一开始有定义

1
2
3
4
5
6
static TCHAR  szMessage[8][100] = {
TEXT("WM_KEYDOWN"), TEXT("WM_KEYUP"),
TEXT("WM_CHAR"), TEXT("WM_DEADCHAR"),
TEXT("WM_SYSKEYDOWN"),TEXT("WM_SYSKEYUP"),
TEXT("WM_SYSCHAR"), TEXT("WM_SYSDEADCHAR")
};
1
2
3
4
5
6
7
8
9
#define WM_KEYFIRST                     0x0100
#define WM_KEYDOWN 0x0100
#define WM_KEYUP 0x0101
#define WM_CHAR 0x0102
#define WM_DEADCHAR 0x0103
#define WM_SYSKEYDOWN 0x0104
#define WM_SYSKEYUP 0x0105
#define WM_SYSCHAR 0x0106
#define WM_SYSDEADCHAR 0x0107

WM_KEYFIRST表示WM消息的第一个,WM_KEYDOWN-WM_KEYFIRSST=0x100-0x100=0

正好对应szMessage下标为0的的成员TEXT("WM_KEYDOWN")

窗口销毁消息

1
2
3
4
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}

插入符号

向文本编辑器中输入文本时,当前输入位置会有一个方格或者竖线或者下划线闪烁,

这个闪烁的玩意儿叫做"插入符号(caret)",而不是"光标(cursor)",光标是指鼠标位置

windows终端中的插入符号

windowsAPI提供了插入符号的现成的函数,

在此之前我还以为这个闪烁效果得是行尾一个竖线|和空格交替重绘.客户区其他部位一直重绘.显然客户区重新打印一遍太耗时了

1
2
3
4
5
6
7
8
CreateCaret 建立与视窗有关的插入符号
SetCaretPos 在视窗中设定插入符号的位置
ShowCaret 显示插入符号
HideCaret 隐藏插入符号
DestroyCaret 撤消插入符号
GetCaretPos 获取当前插入符号位置
GetCaretBlinkTime 获取符号闪烁时间
SetCaretBlinkTime 设置符号闪烁时间

时机

插入符号的作用就是提示用户当前输入位置,显然这个正在被输入的窗口是焦点窗口

而一个窗口过程可能负责多个窗口的消息处理,那么插入符号的改变应该是窗口特定的,不应是窗口过程特定的

因此最好的处理时机是WM_SETFOCUS和WM_KILLFOCUS消息

在处理WM_SETFOCUS时调用CreateCaret,在处理WM_KILLFOCUS消息时调用DestoryCaret

创建插入符号之后并没有立刻输出到屏幕,需要再挑一个适当的时候调用ShowCaret显示它

在处理WM_PAINT时调用CreateCaret,在处理其他需要绘图的消息时调用HideCaret暂时隐藏插入符号.其他消息处理完成之后再调用CreateCaret重新显示插入符号

HideCaret的效果叠加,假设连续调用了10次HideCaret函数,那么就需要调用ShowCaret函数10次才可以把插入符号拽出来.

baby notepad

"弟弟军训完了,非要给我露一手"

书上在这里给出了一个例子,一个婴儿版的文本编辑器

分析一下它的窗口过程WndProc函数

宏定义

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

本定义用于取缓冲区的第x行第y列这个字符.

实际上用了一个一维数组模拟二维数组,BUFFER(x,y)相当于buffer[x][y]

变量定义

1
2
3
4
5
6
7
8
9
static DWORD dwCharSet = DEFAULT_CHARSET;//字符集标志
static int cxChar, cyChar, cxClient, cyClient, cxBuffer, cyBuffer,
xCaret, yCaret;
static TCHAR* pBuffer = NULL;
HDC hdc;
int x, y, i;
PAINTSTRUCT ps;
TEXTMETRIC tm;

字符集变换,窗口创建,尺寸变化消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
	case WM_INPUTLANGCHANGE:
dwCharSet = wParam;//字符集,和输入法有关,默认是DEFAULT_CHARSET
// fall through
case WM_CREATE:
hdc = GetDC(hwnd);
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;//字符高度

DeleteObject(SelectObject(hdc, GetStockObject(SYSTEM_FONT)));//使用系统字体
ReleaseDC(hwnd, hdc);
// fall through
case WM_SIZE:
// obtain window size in pixels
if (message == WM_SIZE)
{
cxClient = LOWORD(lParam);//更新客户区尺寸记录
cyClient = HIWORD(lParam);
}
// calculate window size in characters

cxBuffer = max(1, cxClient / cxChar);//横向最多打印cxClient/cxChar列
cyBuffer = max(1, cyClient / cyChar);//纵向最多打印cyClient/cyChar行

// allocate memory for buffer and clear it

if (pBuffer != NULL)
free(pBuffer);
pBuffer = (TCHAR*)malloc(cxBuffer * cyBuffer * sizeof(TCHAR));//pBuffer为需要打印到屏幕的缓冲区,其大小按照cxBuffer*cyBuffer分配

for (y = 0; y < cyBuffer; y++)
for (x = 0; x < cxBuffer; x++)
BUFFER(x, y) = ' ';//初始化buffer全为空格
// set caret to upper left corner
xCaret = 0;//初始插入字符位置(0,0)
yCaret = 0;

if (hwnd == GetFocus())//判断当前窗口是否为焦点窗口
SetCaretPos(xCaret * cxChar, yCaret * cyChar);//如果是已经是焦点窗口了则设置当前插入字符位置
InvalidateRect(hwnd, NULL, TRUE);//重绘整个客户区
return 0;

获取焦点消息

1
2
3
4
5
6
case WM_SETFOCUS:
// create and show the caret
CreateCaret(hwnd, NULL, cxChar, cyChar);//当前窗口获取焦点,此时创建插入字符
SetCaretPos(xCaret * cxChar, yCaret * cyChar);//设置好插入字符的位置,这个位置可以通过方向键等改变
ShowCaret(hwnd);//显示插入字符
return 0;

失去焦点消息

1
2
3
4
5
case WM_KILLFOCUS:
// hide and destroy the caret
HideCaret(hwnd);//失去焦点,隐藏插入符号
DestroyCaret();//删除插入符号
return 0;

键盘消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
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 = cyBuffer - 1;
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;

case VK_DELETE://退格键
for (x = xCaret; x < cxBuffer - 1; x++)//退格键的作用是当前行当前位置之后的所有字符前移一个字符的宽度,删除当前字符
BUFFER(x, yCaret) = BUFFER(x + 1, yCaret);

BUFFER(cxBuffer - 1, yCaret) = ' ';//退格前本行最后一个字符置空格

HideCaret(hwnd);//临时隐藏插入字符,必须的操作,否则插入字符后面会拖着个黑框
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)));//这句话写不写无所谓
ReleaseDC(hwnd, hdc);
ShowCaret(hwnd);
break;
}
SetCaretPos(xCaret * cxChar, yCaret * cyChar);//设置插入字符的位置
return 0;

字符消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
case WM_CHAR:
for (i = 0; i < (int)LOWORD(lParam); i++)
{
switch (wParam)
{
case '\b': // backspace
if (xCaret > 0)//退格最多退到本行开始
{
xCaret--;//插入符号的位置左移
SendMessage(hwnd, WM_KEYDOWN, VK_DELETE,
1);//发生退格的时候要删除字符
}
break;
case '\t': // tab
do
{
SendMessage(hwnd, WM_CHAR, ' ', 1);
} while (xCaret % 8 != 0);//右移直到xCaret位置8字符对齐
break;
case '\n': // line feed
if (++yCaret == cyBuffer)//回车后需要跳到下一行相同x位置
yCaret = 0;
break;

case '\r': // carriage return
xCaret = 0;

if (++yCaret == cyBuffer)
yCaret = 0;
break;

case '\x1B': // escape ,清空缓冲区
for (y = 0; y < cyBuffer; y++)
for (x = 0; x < cxBuffer; x++)
BUFFER(x, y) = ' ';//遍历清空缓冲区

xCaret = 0;
yCaret = 0;

InvalidateRect(hwnd, NULL, FALSE);//清空缓冲区后立刻重绘
break;

default: // character codes ,其他字符看作可打印字符,存入缓冲区
BUFFER(xCaret, yCaret) = (TCHAR)wParam;

HideCaret(hwnd);
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;
}
}

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

绘图消息

1
2
3
4
5
6
7
8
9
10
case WM_PAINT:
hdc = BeginPaint(hwnd, &ps);

SelectObject(hdc, CreateFont(0, 0, 0, 0, 0, 0, 0, 0,
dwCharSet, 0, 0, 0, FIXED_PITCH, NULL));//等宽字体,FIXED_PITCH等宽间距
for (y = 0; y < cyBuffer; y++)//按行遍历,每次打印一整行
TextOut(hdc, 0, y * cyChar, &BUFFER(0, y), cxBuffer);
DeleteObject(SelectObject(hdc,GetStockObject(SYSTEM_FONT)));
EndPaint(hwnd, &ps);
return 0;

窗口销毁消息

1
2
3
4
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}