浅墨浓香

想要天亮进城,就得天黑赶路。

导航

第6章 键盘_6.3-6.4 字符消息、键盘消息和字符集

Posted on 2015-07-18 08:34  浅墨浓香  阅读(1086)  评论(0编辑  收藏  举报
 6.3 字符消息

  while(GetMessage(&msg,NULL,0,0))

{

   TranslateMessage(&msg);//Windows将根据击键消息、转义状态和键盘的国家/地区配置等信息,将扫描码转换成相应的字符码,如果可以组合成一个字符,则把字符消息放入应用程序的消息队列,产生WM_CHAR、WM_DEADCHAR等消息。

   DispatchMessage(&msg);

}

6.3.1 四类字符消息(WM_CHAR\WM_DEADCHAR、WM_SYSCHAR\WM_SYSDEADCHAR)

(1)lParam——与击键消息的lParam相同

(2)wParam——ANSI或Unicode字符码

    ①如何区别是8位的ANSI还是16位的Unicode?——看RegisterClass是A或W版的。

②获取字符的方法:(TCHAR)wParam;

③判断窗口过程用的是哪种编码:fUnicode= IsWindowsUnicode(hwnd);

6.3.2 消息排序

(1)发送顺序

非系统消息发送顺序

系统消息发送顺序

WM_KEYDOWN

WM_CHAR

WM_KEYUP

WM_SYSKEYDOWN

WM_SYSCHAR

WM_SYSKEYUP

(2)应用举例

   ①输入大写字母A:Shift+A

消息顺序

击键或键码

WM_KEYDOWN

VK_SHIFT(0x10);//shift只产生击键,而不产生字符消息

WM_KEYDOWN

‘A’的虚拟键(0x41)

WM_CHAR

‘A’的字符编码(0x41)

WM_KEYUP

‘A’的虚拟键(0x41)

WM_KEYUP

VK_SHIFT(0x10)

②持续按住A不放

 消息顺序

击键或键码

WM_KEYDOWN

‘A’的虚拟键(0x41)

WM_CHAR

‘a’的字符编码(0x61)

WM_KEYDOWN

‘A’的虚拟键(0x41)

WM_CHAR

‘a’的字符编码(0x61)

WM_KEYDOWN

‘A’的虚拟键(0x41)

WM_CHAR

‘a’的字符编码(0x61)

WM_KEYUP

‘A’的虚拟键(0x41)

6.3.3 控制字符的处理

(1)处理击键和字符消息的原则:

    ①读取字符到窗口时,就处理WM_CHAR消息

②读取光标键、功能键、Delete、Insert、Shift、Ctrl、Alt时处理WM_KEYDOWN。

③Tab、回车、空格、Esc键。根据情况,一般在WM_CHAR中处理,如下:

  case WM_CHAR:

        switch(wParam);

        {

        Case ‘\b’: //退格键

              break;

        Case ‘\t’: //Tab键

              break;

        Case ‘\n’: //换行符

              break;

        Case ‘\r’: //回车键

              break;

        default:

              break;

}

6.3.4 死字符消息

(1)死键:在非美国英语键盘上,某些键可以给字母加音调,但本身不产生字符,因此被称为“死键”。

(2)死字符的工作方式:

①当按下死键(如德语键盘的+=键),相应的窗口过程会收到wParam为音调本身的ASCII码的WM_DEADCHAR消息。若接着按可带音调的字母(如A键),窗口过程会收到参数wParam为‘á’的ANSI码的WM_CHAR消息。因而不必自己去处理WM_DEADCHAR消息,因为WM_CHAR消息己经可以获得所有的信息。

②容错机制:如果按下死键,再按不可再音调的字母键(如‘S’键),那么窗口过程会连接收到两个WM_CHAR消息。第1个消息的wParam等于音调本身的ASCII码,第2个消息等于‘s’字母的ASCII码。

6.4 键盘消息和字符集

6.4.1 KEYVIEW1程序

(1)各列说明——为了便于分栏,采用等宽字体。

 
(2)标题栏:SetBkMode(TRANSPARENT)——szTop和szUnd坐标相同,为防止第二行将第一行抹去,使用 “文本背景透明”模式

(3)GetKeyNameText——根据lParam里的扫描码获取按键名称。

【KeyView1程序】
/*------------------------------------------------------------
KEYVIEW1.C -- Displays Keyboard And Character Messages
(c) Charles Petzold, 1998
------------------------------------------------------------*/
#include <windows.h>
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
    PSTR szCmdLine, int iCmdShow)
{
    static TCHAR szAppName[] = TEXT("KeyView1");
    HWND         hwnd;
    MSG          msg;
    WNDCLASS     wndclass;
    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;
    if (!RegisterClass(&wndclass))
    {
        MessageBox(NULL, TEXT("This program requires Windows NT!"),
            szAppName, MB_ICONERROR);
        return 0;
    }

    hwnd = CreateWindow(szAppName,                  // window class name
        TEXT("KeyView #1"), // window caption
        WS_OVERLAPPEDWINDOW,        // window style
        CW_USEDEFAULT,              // initial x position
        CW_USEDEFAULT,              // initial y position
        CW_USEDEFAULT,              // initial x size
        CW_USEDEFAULT,              // initial y size
        NULL,                       // parent window handle
        NULL,                       // window menu handle
        hInstance,                  // program instance handle
        NULL);                     // creation parameters

    ShowWindow(hwnd, iCmdShow);
    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 cxClientMax, cyClientMax, cxClient, cyClient, cxChar, cyChar;
    static int cLinesMax, cLines;
    TEXTMETRIC tm;
    static RECT  rectScroll;
    static PMSG pmsg;
    HDC         hdc;
    PAINTSTRUCT ps;
    static TCHAR  szTop[] = TEXT("Message        Key       Char     Repeat Scan Ext ALT Prev Tran");
    static TCHAR  szUnd[] = TEXT("_______        ___       ____     ______ ____ ___ ___ ____ ____");//是下划线_,而不是—
    static TCHAR* szFormat[2] = {
        TEXT("%-13s %3d %-15s%c%6u %4d %3s %3s %4s %4s"),
        TEXT("%-13s            0X%04X%1s%c %6u %4d %3s %3s %4s %4s") };
    TCHAR szBuf[128], szKeyName[32];
    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") };
    int iType;
    static TCHAR* szYes = TEXT("Yes");
    static TCHAR* szNo = TEXT("No");
    static TCHAR* szDown = TEXT("Down");
    static TCHAR* szUp = TEXT("Up");
    switch (message)
    {
    case WM_CREATE:
    case WM_DISPLAYCHANGE://显示器分辨率改变时
        //获得最大的客户区
        cxClientMax = GetSystemMetrics(SM_CXMAXIMIZED);
        cyClientMax = GetSystemMetrics(SM_CYMAXIMIZED);

        //获得等宽字体的大小
        hdc = GetDC(hwnd);
        SelectObject(hdc, GetStockObject(SYSTEM_FIXED_FONT));
        GetTextMetrics(hdc, &tm);
        cxChar = tm.tmAveCharWidth;
        cyChar = tm.tmHeight;
        ReleaseDC(hwnd, hdc);
        //为消息数组分配内存
        if (pmsg) free(pmsg);
        cLinesMax = cyClientMax / cyChar;
        pmsg = malloc(cLinesMax* sizeof(MSG));

        cLines = 0;
        // return 0 ;  //继续执行下去
    case WM_SIZE:
        if (message == WM_SIZE)
        {
            cxClient = LOWORD(lParam);
            cyClient = HIWORD(lParam);
        }
        //计算滚动窗口的范围
        rectScroll.top = cyChar; //第二行开始(因第1行为标题)
        rectScroll.left = 0;
        rectScroll.bottom = cyChar*(cyClient / cyChar);//不等于cyClient,应等于每行高度*行数。
        rectScroll.right = cxClient;
        //重绘
        InvalidateRect(hwnd, NULL, TRUE);//该行不可删除,可能是WM_INPUTLANGCHANGE或WM_DISPLAYCHANGE引起
        return 0;
    case WM_KEYDOWN:
    case WM_KEYUP:
    case WM_CHAR:
    case WM_DEADCHAR:
    case WM_SYSKEYDOWN:
    case WM_SYSKEYUP:
    case WM_SYSCHAR:
    case WM_SYSDEADCHAR:
        //重新安排消息数组
        for (int i = cLinesMax - 1; i > 0; i--)
        {
            pmsg[i] = pmsg[i - 1];
        }
        //把当前消息存入消息数组的首元素
        pmsg[0].message = message;
        pmsg[0].hwnd = hwnd;
        pmsg[0].wParam = wParam;
        pmsg[0].lParam = lParam;
        cLines = min(cLines + 1, cLinesMax); //每按一个按键,增加一行。
        //滚屏
        ScrollWindow(hwnd, 0, -cyChar, &rectScroll, &rectScroll);
        break;  //不直接return,因为系统击键消息还要调用DefWindowProc处理
    case WM_PAINT:
        hdc = BeginPaint(hwnd, &ps);

        SelectObject(hdc, GetStockObject(SYSTEM_FIXED_FONT));
        SetBkMode(hdc, TRANSPARENT);
        TextOut(hdc, 0, 0, szTop, lstrlen(szTop));
        TextOut(hdc, 0, 0, szUnd, lstrlen(szUnd));
        //显示消息数组的内容
        for (int i = 0; i < min(cLines, cyClient / cyChar - 1); i++)
        {
            iType = pmsg[i].message == WM_CHAR ||
                pmsg[i].message == WM_SYSCHAR ||
                pmsg[i].message == WM_DEADCHAR ||
                pmsg[i].message == WM_SYSDEADCHAR;
            GetKeyNameText(pmsg[i].lParam, szKeyName, sizeof(szKeyName) / sizeof(TCHAR));
            int iLen = wsprintf(szBuf, szFormat[iType],
                szMessage[pmsg[i].message - WM_KEYFIRST], //第1个参数,消息名称
                pmsg[i].wParam,  //第2个参数,击键时虚拟键代码,字符消息时显示字符的十六进制
                (PTSTR)(iType ? TEXT(" ") : szKeyName),     //第3个参数字符消息为“”,击键为键名称
                (TCHAR)(iType ? pmsg[i].wParam : ' '),      //第4个参数:字符消息时,显示字符本身
                LOWORD(pmsg[i].lParam),                     //第5个参数,重复次数
                HIWORD(pmsg[i].lParam) & 0xFF,                //第6个参数,扫描码在第16-23位,共8位。
                0x01000000 & pmsg[i].lParam ? szYes : szNo,     //第7个参数:扩展标记,在第24位
                0x20000000 & pmsg[i].lParam ? szYes : szNo, //第8个参数:ALT标记,在第29位
                0x40000000 & pmsg[i].lParam ? szDown : szUp, //第9个参数:先前状态,在第30位
                0x80000000 & pmsg[i].lParam ? szUp : szDown);//第10个参数:转换状态,在第31位

            TextOut(hdc, 0, (cyClient / cyChar - 1 - i)*cyChar, szBuf, iLen);
        }
        EndPaint(hwnd, &ps);
        return 0;

    case WM_DESTROY:
        if (pmsg) free(pmsg);
        pmsg = NULL;
        PostQuitMessage(0);
        return 0;
    }
    return DefWindowProc(hwnd, message, wParam, lParam);
}

 

6.4.2 非英语键盘问题

美国英语版的系统字体
 

(1)切换不同键盘布局——KeyView1编译为非Unicode的多字节版本

   ①实验

  A、在美国英语版的Windows下,切换不同键盘布局时字符显示的问题

键盘

WM_KEYDOWN

虚拟键代码

(十进制)

扫描码

(十进制)

美国英语键盘

(WM_CHAR)

希腊键盘

(WM_CHAR)

俄语键盘

(WM_CHAR)

A

65

30

0x61(a)

0xE1(á)

0xF4(ô)

B

66

48

0x62(b)

0xE2(â)

0xE8(è)

C

67

46

0x63(c)

0xF8(ø)

0xF1(ñ)

D

68

32

0x64(d)

0xE4(ä)

0xE2(â)

E

69

18

0x65(e)

0xE5(å)

0xF3(ó)

B、在希腊语版的Windows下,切换不同键盘布局时字符显示的问题

键盘

WM_KEYDOWN

虚拟键代码

(十进制)

扫描码

(十进制)

美国英语键盘(WM_CHAR)

希腊键盘(WM_CHAR)

俄语键盘(WM_CHAR)

A

65

30

0x61(a)

0xE1(α)

0xF4(τ)

B

66

48

0x62(b)

0xE2(β)

0xE8(θ)

C

67

46

0x63(c)

0xF8(ψ)

0xF1(ρ)

D

68

32

0x64(d)

0xE4(δ)

0xE2(β)

E

69

18

0x65(e)

0xE5(ε)

0xF3(σ)

      ②结论及原因——以A实验为例说明。

A、绿色为正确的显示结果,红色为不正确的显示结果。

B、当切换键盘布局时,同一个的按键会发出同样的WM_KEYDOWN消息和扫描码(因为扫描码是根据键盘的自然布局发出的,与硬件有关)。但WM_CHAR中收到的字符码,会因切换不同的键盘而不同。此时,因在同一个美国英语Windows版本下试验,所以三种键盘下通过字符码读取的字符都是来自同一个美国英语版本的字符集。从而导致希腊语键盘和俄语键盘下读出来的字符是错的,因为这些字符不是希腊字母和西里尔文字母。(注意:SYSTEM_FONT得到的字符集因Windows语言的版本不同而不同,有美国英语版、希腊版、俄语版的之分)。如果要正确显示出正确的希腊字母和西里尔字母,就要工作在不同语言的Windows里,这就是非Unicode字符集的不便之处。

 (2)死键在不同版本的Windows下的字符显示问题

   ①死键的产生——以德语键盘为例。

    先按“=”发送WM_DEADCHAR消息;B、再按字符,发送带有音调的WM_CHAR字符)

键盘

WM_KEYDOWN

虚拟键代码

(十进制)

扫描码

(十进制)

美国英语Windows

(WM_CHAR)

希腊语版Windows

(WM_CHAR)

A

65

30

0xE1(á)

0xE1(α)

E

69

18

0xE9(é)

0xE9(ι)

I

73

23

0xED(í)

0xED(ν)

O

79

24

0xF3(ó)

0xF3(σ)

U

85

22

0xFA(ú)

0xFA(ϊ)

②结论与原因

A、绿色为正确的显示结果,红色为不正确的显示结果。

B、同一种键盘布局下,同一按键发送WM_CHAR的字符码是一样的。

C、在不同的Windows下,因字符集不同,所以显示的字符也是不同的。

 

6.4.3 字符集和字体

(1)Windows支持的三种字体:

①位图字体——Windows标题栏、菜单、按钮、对话框中使用

②矢量字体:过时

③TrueType字体:可缩放。

(2)系统字体(位图字体)

GetStockObject

标识符

字样名称

(CreateFont的参数)

备注

SYSTEM_FONT

System

DC默认。得到的字符集因语言不同

SYSTEM_FIXED_FONT

FixedSys

等宽,得到的字符集因语言不同

OEM_FIXED_FONT

Terminal

用于MS_DOS下,得到的字符集因语言不同

DEFAULT_GUI_FONT

MS Sans Serif或宋体等

标准控件、用户界面

ANSI_FIXED_FONT

Courier

tmCharSet=0

ANSI_VAR_FONT

MS Sans Serif

 

DEVICE_DEFAULT_FONT

System

得到的字符集因语言不同

注意:①SYSTEM_FONT、OEM_FIXED_FONT等会因Windows语言版本不同,而得到不同字符集。

②ANSI_FIXED_FONT、ANSI_VAR_FONT得到的字符集不会因Windows不同而不同。

(3)不同语言版本下得到的ANSI字符集对比

   ①通过SYSTEM_FONT得到的系统默认字符集的不同

 美国英语版Windows

希腊语版Windows

 

俄语版Windows

日语版Windows

②从上面可以看出,调用GetStockObject(SYSTEM_FONT)会因Windows版本的不同,而得到不同的字符集。但同一版本的Windows下得到的系统字符集,不会因键盘布局而不同。这也是导致KeyView1程序(非Unicode版)在美国英语版的Windows里切换到非英语键盘时,显示字符仍是美国版ANSI字符集(CharSet=0),而不是相应国家字符集的原因。

6.4.4 Unicode解决方案

(1)切换不同键盘布局——KeyView1编译为Unicode的版本

     在美国英语版的Windows下,切换不同键盘布局时字符显示的问题

键盘

WM_KEYDOWN

虚拟键代码

(十进制)

扫描码

(十进制)

美国英语键盘

(WM_CHAR)

希腊键盘

(WM_CHAR)

俄语键盘

(WM_CHAR)

A

65

30

0x0061(a)

0x03B1(α)

0x0444(ф)

B

66

48

0x0062(b)

0x03B2(β)

0x0438(и)

C

67

46

0x0063(c)

0x03C8(ψ)

0x0441(с)

D

68

32

0x0064(d)

0x03B4(δ)

0x0432(в)

E

69

18

0x0065(e)

0x03B5(ε)

0x0443(у)

说明:①Unicode下WM_CHAR为16位的字符码,而不是8位的,WM_CHAR的值可能高于0xFF。

②为什么定义了Unicode版本的KeyView1,切换希腊键盘和俄语键盘显示还会出错呢?

因为位图字体(除中日韩版)最多可包含256个字符,所以尽管希腊键盘和俄语键盘得到的字符码是正确的,但显示为实心块的错误。当使用SYSTEM_FONT或SYSTEM_FIXED_FONT总有某些语言的字符不能正确显示。(中日韩版的系统字体为TrueType,可多于256个字符)。
 

6.4.5 TrueType字体和大字体

(1)创建逻辑字体——CreateFont和CreateFrontIndirect

①CreateFont的参数:第9个为“字符集ID”,第13个设置等宽FIXED_PITCH。

②CreateFontIndirect参数为LOGFONT结构指针,与CreateFont的14个参数一一对应。

(2)WM_INPUTLANGECHANGE消息:表示输入法或键盘布局发生了改变

参数

含义

备注

wParam

字符集ID

可以使用TranslateCharsetInfo这个API得到字符集的信息

lParam

该输入法的键盘布局(KeyboardLayout)

①ImmGetDescription取得输入法名

②ImmGetIMEFileName得到该输入法的文件位置

③ImmGetProperty得到输入法属性

【KeyView2程序】

 
/*------------------------------------------------------------

KEYVIEW2.C -- Displays Keyboard And Character Messages
(c) Charles Petzold, 1998
------------------------------------------------------------*/
#include <windows.h>
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
    PSTR szCmdLine, int iCmdShow)
{
    static TCHAR szAppName[] = TEXT("KeyView1");
    HWND         hwnd;
    MSG          msg;
    WNDCLASS     wndclass;
    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;
    if (!RegisterClass(&wndclass))
    {
        MessageBox(NULL, TEXT("This program requires Windows NT!"),
            szAppName, MB_ICONERROR);
        return 0;
    }

    hwnd = CreateWindow(szAppName,                  // window class name
        TEXT("KeyView #2"), // window caption
        WS_OVERLAPPEDWINDOW,        // window style
        CW_USEDEFAULT,              // initial x position
        CW_USEDEFAULT,              // initial y position
        CW_USEDEFAULT,              // initial x size
        CW_USEDEFAULT,              // initial y size
        NULL,                       // parent window handle
        NULL,                       // window menu handle
        hInstance,                  // program instance handle
        NULL);                     // creation parameters

    ShowWindow(hwnd, iCmdShow);
    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 cxClientMax, cyClientMax, cxClient, cyClient, cxChar, cyChar;
    static int cLinesMax, cLines;
    TEXTMETRIC tm;
    static RECT  rectScroll;
    static PMSG pmsg;
    HDC         hdc;
    PAINTSTRUCT ps;
    static TCHAR  szTop[] = TEXT("Message        Key       Char     Repeat Scan Ext ALT Prev Tran");
    static TCHAR  szUnd[] = TEXT("_______        ___       ____     ______ ____ ___ ___ ____ ____");//是下划线_,而不是—
    static TCHAR* szFormat[2] = {
        TEXT("%-13s %3d %-15s%c%6u %4d %3s %3s %4s %4s"),
        TEXT("%-13s            0X%04X%1s%c %6u %4d %3s %3s %4s %4s") };
    TCHAR szBuf[128], szKeyName[32];
    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") };
    int iType;
    static TCHAR* szYes = TEXT("Yes");
    static TCHAR* szNo = TEXT("No");
    static TCHAR* szDown = TEXT("Down");
    static TCHAR* szUp = TEXT("Up");
    switch (message)
    {
    case WM_INPUTLANGCHANGE:
        dwCharSet = wParam;
        //继续执行下去
    case WM_CREATE:
    case WM_DISPLAYCHANGE://显示器分辨率改变时
        //获得最大的客户区
        cxClientMax = GetSystemMetrics(SM_CXMAXIMIZED);
        cyClientMax = GetSystemMetrics(SM_CYMAXIMIZED);

        //获得等宽字体的大小
        hdc = GetDC(hwnd);
        SelectObject(hdc, GetStockObject(SYSTEM_FIXED_FONT));
        GetTextMetrics(hdc, &tm);
        cxChar = tm.tmAveCharWidth;
        cyChar = tm.tmHeight;
        ReleaseDC(hwnd, hdc);
        //为消息数组分配内存
        if (pmsg) free(pmsg);
        cLinesMax = cyClientMax / cyChar;
        pmsg = malloc(cLinesMax* sizeof(MSG));

        cLines = 0;
        //return 0 ;          //继续执行下去
    case WM_SIZE:
        if (message == WM_SIZE)
        {
            cxClient = LOWORD(lParam);
            cyClient = HIWORD(lParam);
        }
        //计算滚动窗口的范围
        rectScroll.top = cyChar; //第二行开始(因第1行为标题)
        rectScroll.left = 0;
        rectScroll.bottom = cyChar*(cyClient / cyChar);//不等于cyClient,应等于每行高度*行数。
        rectScroll.right = cxClient;
        //重绘
        InvalidateRect(hwnd, NULL, TRUE);//该行不可删除,因为执行到这里,可能是WM_INPUTLANGCHANGE或WM_DISPLAYCHANGE引起
        if (message == WM_INPUTLANGCHANGE) return TRUE;
        return 0;
    case WM_KEYDOWN:
    case WM_KEYUP:
    case WM_CHAR:
    case WM_DEADCHAR:
    case WM_SYSKEYDOWN:
    case WM_SYSKEYUP:
    case WM_SYSCHAR:
    case WM_SYSDEADCHAR:
        //重新安排消息数组
        for (int i = cLinesMax - 1; i > 0; i--)
        {
            pmsg[i] = pmsg[i - 1];
        }
        //把当前消息存入消息数组的首元素
        pmsg[0].message = message;
        pmsg[0].hwnd = hwnd;
        pmsg[0].wParam = wParam;
        pmsg[0].lParam = lParam;
        cLines = min(cLines + 1, cLinesMax); //每按一个按键,增加一行。
        //滚屏
        ScrollWindow(hwnd, 0, -cyChar, &rectScroll, &rectScroll);
        break;  //不直接return,因为系统击键消息还要调用DefWindowProc处理
    case WM_PAINT:
        hdc = BeginPaint(hwnd, &ps);

        //SelectObject(hdc, GetStockObject(SYSTEM_FIXED_FONT));
        SelectObject(hdc, CreateFont(0, 0, 0, 0, 0, 0, 0, 0,
            dwCharSet, 0, 0, 0, FIXED_PITCH, 0));
        SetBkMode(hdc, TRANSPARENT);
        TextOut(hdc, 0, 0, szTop, lstrlen(szTop));
        TextOut(hdc, 0, 0, szUnd, lstrlen(szUnd));
        //显示消息数组的内容
        for (int i = 0; i < min(cLines, cyClient / cyChar - 1); i++)
        {
            iType = pmsg[i].message == WM_CHAR ||
                pmsg[i].message == WM_SYSCHAR ||
                pmsg[i].message == WM_DEADCHAR ||
                pmsg[i].message == WM_SYSDEADCHAR;
            GetKeyNameText(pmsg[i].lParam, szKeyName, sizeof(szKeyName) / sizeof(TCHAR));
            int iLen = wsprintf(szBuf, szFormat[iType],
                szMessage[pmsg[i].message - WM_KEYFIRST], //第1个参数,消息名称
                pmsg[i].wParam,  //第2个参数,击键时虚拟键代码,字符消息时显示字符的十六进制
                (PTSTR)(iType ? TEXT(" ") : szKeyName),     //第3个参数字符消息为“”,击键为键名称
                (TCHAR)(iType ? pmsg[i].wParam : ' '),      //第4个参数:字符消息时,显示字符本身
                LOWORD(pmsg[i].lParam),                     //第5个参数,重复次数
                HIWORD(pmsg[i].lParam) & 0xFF,                //第6个参数,扫描码在第16-23位,共8位。
                0x01000000 & pmsg[i].lParam ? szYes : szNo,     //第7个参数:扩展标记,在第24位
                0x20000000 & pmsg[i].lParam ? szYes : szNo, //第8个参数:ALT标记,在第29位
                0x40000000 & pmsg[i].lParam ? szDown : szUp, //第9个参数:先前状态,在第30位
                0x80000000 & pmsg[i].lParam ? szUp : szDown);//第10个参数:转换状态,在第31位

            TextOut(hdc, 0, (cyClient / cyChar - 1 - i)*cyChar, szBuf, iLen);
        }
        DeleteObject(SelectObject(hdc, GetStockObject(SYSTEM_FONT)));
        EndPaint(hwnd, &ps);
        return 0;

    case WM_DESTROY:
        if (pmsg) free(pmsg);
        pmsg = NULL;
        PostQuitMessage(0);
        return 0;
    }
    return DefWindowProc(hwnd, message, wParam, lParam);
}