EGE专栏:EGE专栏
一、按键消息
下图为笔记本键盘的一个常见键位布局,其中包含了键盘的大部分按键。
**按键消息(Key message)**是指
1. 按键消息类型
键盘上的按键在按下和抬起时,系统会发送相应的按键消息,通知程序用户使用键盘进行了哪些操作。程序可以读取这些消息,识别后做出响应。同时,键盘作为输入工具主要是用于输入文字,系统除了发送按下和抬起消息外,还会发送字符输入消息,程序可以读取来处理用户输入的字符。
EGE中的按键消息分为三种,分别是 按键按下、按键抬起 和 字符输入 消息,定义在 key_msg_e
枚举中:
// 按键消息类型枚举
typedef enum key_msg_e {
key_msg_down = 1, // 按键按下
key_msg_up = 2, // 按键抬起
key_msg_char = 4, // 字符输入
} key_msg_e;
2. 长按与短按
按键长按与短按时,消息的发送是两种不同的情况,短按是指按键按下后很快又松开,而 长按 是指按键被按下后,保持足够长的时间后再松开。
2.1 短按
当按键按下时,系统会发送到一条 按键按下(down) 的消息,如果当前使用的是英文输入法,并且按键对应一个字符(如A键),紧接着还会发送到一条 字符输入消息(char)。
中文输入法的字符输入消息会等到确认再发送,并不是在按键按下时。例如拼音输入法,需要多个按键才能确定一个词,这时会将中文字词的编码拆分成多个字符输入消息再将它们一起发送。
松开按键时,系统会发送到一条 按键抬起(up) 消息。
2.2 长按
一般同一个按键的按下和抬起消息是成对出现的,但如果按住某个键的时间足够长,会触发键盘的自动重复功能,以一定的时间间隔重复发送 按下消息 和 字符输入消息,直到松开按键才停止重复发送。松开按键时,长按和短按都会发送一条按键抬起消息。这就是按键长按时按键消息的发送情况。
3. 按键消息结构体
在EGE中,按键消息的结构体 key_msg 定义如下:
typedef struct key_msg {
unsigned int msg; //消息类型
unsigned int key; //键码
unsigned int flags; //辅助键标志
}key_msg;
结构体key_msg 有三个成员:
成员 | 含义 | 值 |
---|---|---|
msg | 表示消息类型:按下、抬起或字符 | key_msg_e 中的枚举值 |
key | 当是按下、抬起消息时,表示相应按键的键码。如果是字符消息,则存储 字符编码(有时一个字符编码分多个消息存储 ) | windows 虚拟键码 或字符编码 |
flags | 辅助键标志,用二进制位表示辅助键Shift, Ctrl是否被按下,这样可以识别类似 Ctrl + A 的快捷键 | 使用 key_flag_e 中的枚举值表示辅助键是否被按下 |
二、按键消息处理
1. 按键消息队列
用户使用键盘进行输入时,系统会将产生的消息发送至活动窗口。EGE窗口接收到后按键消息后,会将消息存储到消息队列中。在程序中可以使用 kbmsg() 判断存储按键消息的消息队列是否为空,如果不为空,则可以使用 getkey() 从消息队列中取出一个按键消息。
如果消息队列为空,getkey() 会一直等待,直到有按键消息进入队列。
1.1 flushkey() 清空按键消息队列
当你觉得按键消息队列中的消息已经没有用时,可以将这些消息清空。这样下一次就能 直接处理到最新的键盘消息了。
清空键盘消息缓存区的函数为
flushkey();
2. 按键消息处理循环
在从按键消息队列中取出按键消息前,先用 kbmsg() 判断队列中是否保存有消息,如果有再调用 getkey() 取出队列中的消息,否则 getkey() 会一直等待,直到有新的按键消息产生才会继续往下执行。
// 从队列中取出所有按键消息
while (kbmsg()) {
key_msg keyMsg = getkey();
}
使用 while() 循环目的是要将队列中的消息全部取出,因为队列中消息都是用户在之前操作键盘产生的,距离用户操作已经过去了一小段时间,如果现在不处理完毕,那么消息处理会被拖延得更久,更加滞后,用户可能会感觉到程序对按键的响应比较慢。
三、虚拟键码
键盘上的按键被映射成一个值,称为 虚拟键码(Virtual-key code),是系统定义的独立于设备的值,值在 1到255之间,0不是按键的键码。(例如,在系统中,不管是什么键盘,回车键都是用同一个值表示)
按键消息结构体中的key成员便是用来表示存储按键的虚拟键码,通过它我们可以知道按键消息是由哪个按键产生的。
虚拟键码的宏命名一般以 VK_
开头,如回车键 VK_RETURN
,部分可以用ASCII字符表示的按键,就用ASCII值表示,如数字键、标点符号键、字母键等(字母键是大写字母的ASCII值
),没有单独命名。如A键,虚拟键码就等于大写字母 A 的 ASCII字符的值 'A'
。
EGE为了更方便表示这些键,为这些虚拟键码另起了更为友好的名称,如回车键 key_enter
, 字母A键 key_A
,数字0键 key_0
等。按键对应的值依然和虚拟键码一致,并没有更改。定义如下
typedef enum key_code_e {
//鼠标左右中三键
key_mouse_l = 0x01,
key_mouse_r = 0x02,
key_mouse_m = 0x04,
//退格,Tab,回车键
key_back = 0x08,
key_tab = 0x09,
key_enter = 0x0d,
//辅助键
key_shift = 0x10,
key_control = 0x11,
key_menu = 0x12,
key_pause = 0x13,
//大写锁定,esc键,空格键
key_capslock = 0x14,
key_esc = 0x1b,
key_space = 0x20,
//上一页,下一页,行首,行尾
key_pageup = 0x21,
key_pagedown = 0x22,
key_home = 0x23,
key_end = 0x24,
//方向键
key_left = 0x25,
key_up = 0x26,
key_right = 0x27,
key_down = 0x28,
key_print = 0x2a,
key_snapshot = 0x2c,
//插入,删除键
key_insert = 0x2d,
key_delete = 0x2e,
//大键盘数字键
key_0 = 0x30,
key_1 = 0x31,
key_2 = 0x32,
key_3 = 0x33,
key_4 = 0x34,
key_5 = 0x35,
key_6 = 0x36,
key_7 = 0x37,
key_8 = 0x38,
key_9 = 0x39,
//字母键中的A ~ Z键
key_A = 0x41,
key_B = 0x42,
key_C = 0x43,
key_D = 0x44,
key_E = 0x45,
key_F = 0x46,
key_G = 0x47,
key_H = 0x48,
key_I = 0x49,
key_J = 0x4a,
key_K = 0x4b,
key_L = 0x4c,
key_M = 0x4d,
key_N = 0x4e,
key_O = 0x4f,
key_P = 0x50,
key_Q = 0x51,
key_R = 0x52,
key_S = 0x53,
key_T = 0x54,
key_U = 0x55,
key_V = 0x56,
key_W = 0x57,
key_X = 0x58,
key_Y = 0x59,
key_Z = 0x5a,
//windows键
key_win_l = 0x5b,
key_win_r = 0x5c,
key_sleep = 0x5f,
//小键盘的数字键,就是九个数字围成九宫格那个
key_num0 = 0x60,
key_num1 = 0x61,
key_num2 = 0x62,
key_num3 = 0x63,
key_num4 = 0x64,
key_num5 = 0x65,
key_num6 = 0x66,
key_num7 = 0x67,
key_num8 = 0x68,
key_num9 = 0x69,
//小键盘的符号键
key_multiply = 0x6a, *
key_add = 0x6b, +
key_separator = 0x6c,
key_subtract = 0x6d, -
key_decimal = 0x6e, .
key_divide = 0x6f, /
//这个是键盘上方的12个功能键
key_f1 = 0x70,
key_f2 = 0x71,
key_f3 = 0x72,
key_f4 = 0x73,
key_f5 = 0x74,
key_f6 = 0x75,
key_f7 = 0x76,
key_f8 = 0x77,
key_f9 = 0x78,
key_f10 = 0x79,
key_f11 = 0x7a,
key_f12 = 0x7b,
//小键盘数字锁
key_numlock = 0x90,
key_scrolllock = 0x91,
//可能左右两边都有一个
key_shift_l = 0xa0,
key_shift_r = 0xa1,
key_control_l = 0xa2,
key_control_r = 0xa3,
key_menu_l = 0xa4,
key_menu_r = 0xa5,
//大键盘上的符号键
key_semicolon = 0xba, ; 分号
key_plus = 0xbb, + 加号
key_comma = 0xbc, , 逗号
key_minus = 0xbd, - 减号
key_period = 0xbe, . 句号
key_slash = 0xbf, / 右斜杠
key_tilde = 0xc0, ` 波浪符(下面的点)
key_lbrace = 0xdb, [ 左方
key_backslash = 0xdc, \ 反斜杠
key_rbrace = 0xdd, ] 右方
key_quote = 0xde, ' 引号
key_ime_process = 0xe5,
}key_code_e;
四、按键状态检测
按键有两个状态:松开状态和按下状态。
1. keystate() 函数
判断某个按键当前是否是按下状态,可以使用EGE中的 keystate()
函数,参数key是按键的虚拟键码,如果按键当前处于按下状态,那么 keystate() 函数返回 1,否则返回 0。
int keystate(int key);
需要注意的是,keystate() 函数检测按键状态是依靠窗口接收到的按键消息,如果EGE窗口处于非活动窗口,那么按下按键是无法检测到的。实际上也不需要检测,在其它窗口按下按键本身就是和本窗口无关。如果真的想要检测,可以使用 win API 中的 GetAsyncKeyState() 函数。
由于一些键盘检测电路的设计原因,在某些按键组合按下时,会有无法识别出其它一小部分按键按下松开的现象。至于是哪些按键的组合会出现这种状况,要看键盘的电路如何设计。
例如,检测回车键是否是按下状态:
if (keystate(key_enter)) {
}
通过keystate()检测的缺点
缺点是只能检测按键 当前的状态,如果在一段时间内很多按键快速地按下又松开,结束后你再去检测当前状态,是无法得到他们在这一段时间内按下和抬起的顺序的。
当某一帧的计算非常耗时,在这段时间内可能会出现按键操作无效的现象,因为keystate() 丢失了两次检测中间这段时间内按键的状态变化信息。两次检测之间间隔时间越久,灵敏度越低。
2. 通过读取按键消息记录按键状态
每当按键触发按下消息时,就表明按键接下来是按下状态,触发抬起消息就说明按键接下来是松开状态。因此,我们可以通过读取按键消息来得到按键的状态。得到的按键状态需要记录,否则如果后面没有接收到对应按键的按键消息的话,将无法得知按键的状态。
按键消息发送的顺序和按键按下抬起的顺序一致,因此可以通过按键消息准确地知道按键状态变化顺序,而不像 keystate()只能获取按键当前的状态。
示例代码如下所示,虚拟键码范围为1~255,因此可以直接创建一个256大小的bool数组存储按键状态。当检测到消息类型为 key_msg_down
时,设置对应按键状态为按下;当检测到消息类型为 key_msg_up
时,设置对应按键状态为松开。
当然,如果觉得占用内存比较多,可以使用二进制位来表示,需占用32个字节。
// 存储按键状态:是否被按下
bool keyIsPressed[256] = {false};
while (kbmsg()) {
key_msg msg = getkey();
if (msg.msg == key_msg_down) {
//按键按下,变为按下状态
keyIsPressed[msg.key] = true;
} else if (msg.msg == key_msg_up) {
//按键抬起,变为松开状态
keyIsPressed[msg.key] = false;
}
}
五、按键控制
用户按下松开按键时,系统会发送按键的按下、抬起消息,我们可以在程序中读取并识别出这些消息,然后对用户按键动作做出响应。
按键消息结构体中的 msg 和 key 成员分别表示消息类型和按键。对于按键控制,我们只处理 按下(key_msg_down) 和 抬起(key_msg_up) 这两个类型的消息,字符输入类型的消息我们就暂时先忽略掉。
// 按键消息结构体
typedef struct key_msg {
unsigned int msg; //消息类型
unsigned int key; //键码
unsigned int flags; //辅助键标志
}key_msg;
// 按键消息类型枚举
typedef enum key_msg_e {
key_msg_down = 1, // 按键按下
key_msg_up = 2, // 按键抬起
key_msg_char = 4, // 字符输入
} key_msg_e;
1. 按键识别
虚拟键码统一了按键的标识,我们可以通过虚拟键码来确定消息是由哪个按键触发的。
while (kbmsm()) {
key_msg msg = getkey();
if ((msg.key = key_enter)) {
// 由回车键触发的消息,可能按下、抬起或字符消息
}
}
2. 按键按下与抬起
上面通过虚拟键码可以得到触发的按键,但对于控制来说还是不够的,因为在一个按键被按下和松开的过程中,会触发多次按键消息:按下、字符输入、抬起。
如果仅仅是根据键码判断用户是否按下了按键的话,那么按键每被按下一次,会发送多条与该按键相关的消息,程序会识别到按键多次按下,所以还需要通过消息类型将这些消息区分开来。
按键的按下和抬起消息可以通过 key_msg 结构体中的 msg 成员的值识别出来。
按键无论是长按还是短按,都只会发送一次 抬起消息(up),而按键长按时却会多次发送 按下消息(down),这是需要注意的地方。
while (kbmsg())
{
key_msg msg = getkey();
// 通过key_msg的成员msg来判断消息类型
if (msg.msg == key_msg_down) {
//按键按下消息
//长按会多次触发,这个需要注意
} else if (msg.msg == key_msg_up) {
//按键抬起消息
} else {
// 字符输入消息:key_msg_char
}
}
所以某个按键抬起的消息可以通过对消息类型和虚拟键码的组合判断进行确定。
while (kbmsg())
{
key_msg msg = getkey();
// 回车键抬起
if ((msg.msg == key_msg_up) && (msg.key == key_enter)) {
}
}
那对于按键按下的消息该如何进行判断呢?长按会发送多次按键按下的消息,怎么识别出按键按下时发送的第一条消息?那就要通过消息发送前按键的状态来解决。
本来Windows发送的按键消息中已经包含按键之前的状态,但由于EGE库中处理出现失误,没有加入相关位,因此无法直接由按键消息得到按键之前的状态。
由按键消息记录按键的状态,每次读取到按下消息时,先对按键之前的状态进行判断。如果是按键按下时发送的第一条消息,那它之前肯定是松开状态。
//记录按键状态:是否被按下,初始是松开状态; false:松开,true:按下
bool keyIsPressed[256] = {false};
while (kbmsg()) {
key_msg msg = getkey();
if (msg.msg == key_msg_down) {
// 按键被按下,先判断之前的状态是否是松开
if (!keyIsPressed[msg.key]) {
// 按键之前是松开状态,因此是第一次发送的按下消息
这里执行按键按下时进行的操作
}else {
// 按键长按时重复发送的按下消息
}
//按键按下,记录按键已变为按下状态
keyIsPressed[msg.key] = true;
} else if (msg.msg == key_msg_up) {
这里执行按键按下时进行的操作
// 按键抬起,记录按键已变为松开状态
keyIsPressed[msg.key] = false;
}
}
3. 辅助键
按键消息的成员变量flags中有两个位是用来指示辅助键Shift和Ctrl是否被按下的。如果对应位上为1,那就辅助键被按下。
typedef enum key_flag_e {
key_flag_shift = 0x100,
key_flag_ctrl = 0x200,
}key_flag_e;
while (kbmsg()) {
key_msg msg = getkey();
// 判断按键被按下时,辅助键是否也被按下
if (msg.msg == key_msg_down) {
if (msg.flags & key_flag_shift) {
//判断是否按下了 Shift 辅助键
}
if (msg.flags & key_flag_ctrl) {
//判断是否按下了 Ctrl 辅助键
}
}
}
六、字符输入
1. 获取字符输入 getch()
除暂停作用外,不建议使用。
如果你只是想判断是哪个按键按下的或者获取输入的字符,这里有种简单的方式,那就是 **kbhit() 和 getch() 的组合。
最常用的 getch(),这时候程序会暂停,等待用户按下按键,返回值是按键输入的字符的ASCII值或者是功能键的码值。
这个码值不是虚拟键码,如果能用ASCII表示,那就是等于ASCII码,如果不能表示,那么码值大于255。
int ch = getch();
getch() 返回的值需要用两个字节表示,部分键按下时返回值大于0xFF,所以不能使用 char
类型变量进行存储,否则会被被截断,应该用 int。
如果不想暂停,可以使用 kbhit() 检测是否有字符输入,如果没有就跳过,有就读取字符,这样就不影响程序运行了。
//判断是否有按键字符输入,有就读取字符。
while (kbhit()) {
int ch = getch();
...
}
2. 字符输入:由按键消息读取(EGE20.08新增)
字符类型按键消息是EGE20.08实现的功能,20.08之前的版本虽然有key_msg_char定义,但实际上并没有实现。
key_msg中的 msg 成员表示的是消息类型。
当msg等于 key_msg_char 时,就表明是字符消息,可以由 key 成员得到输入字符的编码。
2.1 字符的编码
英文输入时,那么得到的就是字符的 ASCII 码,这和普通的字符输入一致,这时候使用就很简单(相比getch()直接获取要多写几行)
。
key_msgkeyMsg = key_msg{ 0 }; //初始化
while (kbmsg()) { //判断是否有按键消息,避免堵塞
keyMsg = getkey(); //获取按键消息
if (keyMsg.msg == key_msg_char) { //判断是否是字符输入
int ch = keyMsg.key; //得到输入的字符
这个ch就是得到的字符了
}
}
如果是中文输入,那么得到的是汉字的编码值。通常编码默认采用的是本地编码GBK或 GB2312,这个编码可以设置。
2.2 编码设置
ege可以设置窗口的字符编码方式,中文系统默认是GB2312编码,也可以设置成Unicode编码(UTF-16)
,这通过初始化模式INIT_UNICODE来设置。
下面就是将窗口字符编码设置成Unicode编码
initgraph(640, 480, INIT_UNICODE);
这里简单说一下几个概念:
- 码点:即一个字符在Unicode编码集中对应的数字值。这里注意一下,只是数学上的数值概念,并不涉及到具体在计算机中如何存储。
- 代码单元:具体的字符编码用来表示码点的基本单元,一个码点可由多个代码单元来表示。
2.2.1 GB2312编码(国家简体中文字符集)
GB2312编码的代码单元为一个字节大小。
ASCII字符占一个代码单元,中文字符占两个代码单元。
GB2312的代码单元是一个字节大小,所以ASCII字符用一个代码单元来表示,而中文字符用两个代码单元来表示。所以中文字符的输入会分成两个字节来发送。
ASCII编码字节的最高位是0,只用低7位。而GBK编码除了包含 ASCII 外,剩余的汉字部分,汉字编码固定占两个字节,并且每个字节的最高位为1。所以可以用最高位判断是字符是1个字节还是两个字节。
2.2.2 UTF-16编码
UTF-16编码的代码单元为两个个字节大小,即16位。一个码点用一个或两个代码单元来表示。
UTF-16编码的代码单元占两个字节,这时候直接用宽字符类型wchar_t 存储即可,然后就可以使用宽字符相关的函数来输出。
3. 字符的输入
我们可以通过读取按键消息,如果消息类型为字符类型,即key_msg_char,那么就读取字符,做字符消息处理。
按键字符消息处理如下所示:
while (kbmsg()) {
key_msg keyMsg = getkey();
//判断是否是字符类型消息
if (keyMsg.msg == key_msg_char) {
int ch = keyMsg.key;
//这里就得到了字符的值,即ch
}
}
这里注意一下,key_msg.key的类型是int类型。
因为有不同的编码,对于中文,keyMsg.key对应的不一定就是完整的一个字符。
3.1 UTF-16编码
此时keyMsg.key内的值用两个字节即可以表示,对应宽字符类型wchar_t。
key_msgkeyMsg = key_msg{ 0 }; //初始化
while (kbmsg()) { //判断是否有按键消息,避免堵塞
keyMsg = getkey(); //获取按键消息
if (keyMsg.msg == key_msg_char) { //判断是否是字符输入
wchar_t ch = (wchar_t)keyMsg.key; //得到的一个代码单元
}
}
用宽字符相关的函数即可处理
3.2 GB2312编码
GB2312编码的代码单元是一个字节大小,中文字符用两个字节表示,所以中文字符会连发两个字符型消息。
字符你需要判断哪些字节是属于同一个文字的编码。
这里以GB2312编码为例,ASCII码部分,字节的最高位为0,而一个汉字部分,一个汉字固定占两个字节,每个字节最高位都是1。所以可以检测最高位,发现是0就是ASCII字符,如果是1就凑够两个字节。
3.3 字符的存储与处理
读取到字符后,根据编码用char 型或wchar_t数组来存储,然后使用相应的普通字符串函数或者宽字符类型字符串函数来操作即可,像字符串拼接,字符串输出之类,系统都能自行处理。
4. GB2312编码字符的处理示例
汉字输入的时候,输入法会有候选窗口弹出,直到按下空格或数字才会选中对应的汉字。此时字符消息一起发出,一个汉字发出两个字符消息。并且是按编码中的顺序发出。所以只要按顺序存储即可。可以创建个字符缓存区,并且增加缓存区长度记录。
下面是消息处理循环,字符的处理就在while循环中。
const int buffSize = 128;
char buff[128] = {""}; //缓存区
int len = 0; //记录长度
key_msg keyMsg;
while (kbmsg()) {
keyMsg = getkey();
/*---------------------------------*/
这里处理按键消息
/*---------------------------------*/
}
先判断是否为字符消息
if (keyMsg.msg == key_msg_char)
获取键值key,即 key_msg 的 key 成员,因为GB2312的代码单元为一个字节大小,所以key值用一个字节即可存储。
char ch = (char)keyMsg.key;
- 对字节的最高位判断,最高位为0则是ASCII码,否则GB2312的扩展部分。如果是想单字输出,如输出到控制台,就可以凑够两个字节输出了,而如果想一起输入,直接保存即可,后面一起输出。
单字输出:
这里凑够一个字就直接输出。(英文一个字节,中文两个字节)
if (keyMsg.msg == key_msg_char) {
char ch = keyMsg.key;
if ((ch & 0x80) == 0) { //ASCII字符
if ((ch == '\r') || (ch == '\n'))
putchar('\n');
else
putchar(ch);
}
else{
buff[len++] = ch;
if (len >= 2) { //没有存储完汉字的全部字节
buff[len] = '\0'; //末尾增加结束符
printf("%s", buff); //这里仅做直接输出处理
len = 0; //长度清零
}
}
}
完整程序
#define SHOW_CONSOLE
#include <graphics.h>
#include <stdio.h>
int main()
{
initgraph(640, 480, 0);
setcaption("按键消息类型测试");
setbkcolor(WHITE);
setcolor(BLACK);
setfont(18, 0, "宋体");
char buff[3] = { "" }; //缓存区
int len = 0; //记录长度
xyprintf(40, 200, "请输入中文或英文,然后查看控制台输出");
for (; is_run(); delay_fps(60)) {
while (kbmsg()) {
key_msg keyMsg = getkey();
if (keyMsg.msg == key_msg_char) {
char ch = keyMsg.key;
if ((ch & 0x80) == 0) { //ASCII字符
if ((ch == '\r') || (ch == '\n'))
putchar('\n');
else
putchar(ch);
}
else{
buff[len++] = ch;
if (len >= 2) { //没有存储完汉字的全部字节
buff[len] = '\0'; //末尾增加结束符
printf("%s", buff); //这里仅做直接输出处理
len = 0; //长度清零
}
}
}
}
}
closegraph();
return 0;
}
单行输出
这里等到输入一个回车符再输出,因为输入缓存区总有限制,如果一行文字过长,做直接输出处理。
这里还有个小问题,如果满了的时候,输出时最后那个汉字仅仅读取了其中一个字节,存不下了呀,没读完整呀。怎么办?那么要把这个字节保留下来,并且原来的位置设成 ‘\0’ 再输出,然后把这个字节放到存到缓存区的第一的字节,长度设为1。
那怎么读取的时候怎么知道最后那个是汉字的哪个字节?那当然是读取的过程中设立一个变量,用来对自己进行计数,如果是GB2312编码,因为固定的两个字节,在while循环外设立一个bool值,检测到汉字的一个字节取反就可以了,初始为false,最后也为 false ,则说明不是汉字的前一个字节。
字符的处理就在 charHandle() 函数中,为了防止嵌套太多,所以放到了函数里,同时也增加了清晰度。只是这样会增加一个传参的步骤。需要将缓存区长度返回,所以用了指针。
示例程序
#define SHOW_CONSOLE
#include <graphics.h>
#include <stdio.h>
//字符处理函数
void charHandle(char ch, char* buff, int *len, int buffSize)
{
static bool half = false;
bool output = false; //输出标记
bool lineCompleted = false; //行结束标记
//存储
buff[(*len)++] = ch;
//如果是英文字符的编码
if ((ch & 0x80) == 0) {
//回车或满则输出标志置位
if ((ch == '\r') || (ch == '\n')) {
(*len)--; //回车要清掉
output = true;
lineCompleted = true; //行已结束
} else if (*len >= buffSize - 1) {
output = true;
}
}
//如果是中文字符的编码
else {
half = !half; //对标志位取反
//判断是否满(最后要留一个存结束符),,满则输出标志置位
if ((*len >= buffSize - 1)) {
output = true;
}
}
if (output) {
//当前之前汉字只读了一半,那么把这个用'\0'替换
if (half)
(*len)--;
//输出清空
buff[(*len)++] = '\0';
printf("%s", buff);
if (lineCompleted)
putchar('\n');
*len = 0;
//填充之前的字节
if (half)
buff[(*len)++] = ch;
}
}
int main()
{
initgraph(640, 480, 0);
setcaption("按键消息类型测试");
setbkcolor(WHITE);
setcolor(BLACK);
setfont(18, 0, "宋体");
#define BUFF_SIZE 16
char buff[BUFF_SIZE] = { "" }; //缓存区
int len = 0; //记录长度
xyprintf(40, 200, "请输入中文或英文,然后查看控制台输出");
xyprintf(40, 220, "(回车主动输出或缓存区满后输出)");
xyprintf(40, 240, "当前缓存区内所占字节:%3d / %d", len, BUFF_SIZE);
for (; is_run(); delay_fps(60)) {
while (kbmsg()) {
key_msg keyMsg = getkey();
if (keyMsg.msg == key_msg_char) {
char keyVal = keyMsg.key;
charHandle(keyVal, buff, &len, BUFF_SIZE);
xyprintf(40, 240, "当前缓存区内所占字节:%3d / %d", len, BUFF_SIZE);
}
}
}
closegraph();
return 0;
}
七、按键控制移动示例
1. 单次移动
单次移动: 按下一次按键只会移动一次,长按也不会再次移动。
单次移动一般是在按键按下时移动,按键按下动作可以通过按键状态切换来检测(消息类型是 key_msg_down 且之前是松开状态)。如果想响应按键抬起动作,那么只需要检测按键消息类型是否是 key_msg_up
即可,按键抬起消息并不会重复发送。
#include <graphics.h>
#include <math.h>
void drawGrid(int width, int height, int totalRow, int totalCol);
void positionOffset(int key, int* xOffset, int* yOffset);
bool PositionIsValid(int totalRow, int totalCol, int row, int col);
void drawCircle(float x, float y, float radius);
bool keyIsPressed[256] = { false };
int main()
{
const int winWidth = 600;
const int COL_NUM = 7, ROW_NUM = COL_NUM;
initgraph(winWidth, winWidth, INIT_RENDERMANUAL);
ege_enable_aa(true);
setbkcolor(EGERGB(0xFF, 0xFF, 0xFF));
setfont(20, 0, "SimSun");
settextjustify(RIGHT_TEXT, TOP_TEXT);
setbkmode(TRANSPARENT);
int curRow = ROW_NUM / 2, curCol = COL_NUM / 2;
for (; is_run(); delay_fps(60)) {
while (kbmsg()) {
key_msg msg = getkey();
if (msg.msg == key_msg_down) {
// 按键按下
if (!keyIsPressed[msg.key]) {
//按键按下时处理
int colOffset = 0, rowOffset = 0;
positionOffset(msg.key, &colOffset, &rowOffset);
int rowNext = curRow + rowOffset, colNext = curCol + colOffset;
// 位置有效则移动至下一个位置
if (PositionIsValid(ROW_NUM, COL_NUM, rowNext, colNext)) {
curRow = rowNext;
curCol = colNext;
}
}
//按键按下,记录按键已变为按下状态
keyIsPressed[msg.key] = true;
} else if (msg.msg == key_msg_up) {
// 按键抬起,记录按键已变为松开状态
keyIsPressed[msg.key] = false;
}
}
cleardevice();
drawGrid(winWidth, winWidth, ROW_NUM, COL_NUM);
float radius = (float)winWidth / (2.0f * COL_NUM);
float x = winWidth * ((float)curCol / COL_NUM) + radius;
float y = winWidth * ((float)curRow / ROW_NUM) + radius;
drawCircle(x, y, radius);
setcolor(BLACK);
xyprintf(winWidth, 0, "当前位置:(%d, %d)", curRow, curCol);
}
return 0;
}
// 绘制网格
void drawGrid(int width, int height, int totalRow, int totalCol)
{
setlinestyle(DASHED_LINE, 0, 1);
setcolor(EGERGB(0xC0, 0xC0, 0xC0));
for (int i = 1; i < totalCol; i++) {
int x = (int)roundf((float)width * i / totalCol);
line(x, 0, x, height);
}
for (int i = 1; i < totalRow; i++) {
int y = (int)roundf((float)height * i / totalRow);
line(0, y, width, y);
}
}
//绘制圆
void drawCircle(float x, float y, float radius)
{
setfillcolor(EGEACOLOR(0xFF, 0xA0D8EF));
ege_fillellipse(x - radius, y - radius, 2 * radius, 2 * radius);
setlinestyle(SOLID_LINE, 0, 2);
setcolor(BLACK);
ege_ellipse(x - radius, y - radius, 2 * radius, 2 * radius);
}
void positionOffset(int key, int* xOffset, int* yOffset)
{
int dx = 0, dy = 0;
switch (key) {
case 'A': case key_left: dx = -1; break; //左移
case 'W': case key_up: dy = -1; break; //上移
case 'D': case key_right: dx = 1; break; //右移
case 'S': case key_down: dy = 1; break; //下移
default: break; //其他键不移动
}
*xOffset = dx;
*yOffset = dy;
}
bool PositionIsValid(int totalRow, int totalCol, int row, int col)
{
return (0 <= row) && (row < totalRow) && (0 <= col) && (col < totalCol);
}
2. 匀速移动
匀速移动 是指物体在每一帧移动相同的距离或者单位时间内移动相同的距离。
这是两种不同的移动策略,如果每一帧间隔的时间相同或者保持平均间隔时间在一定值,那么按帧匀速和按时间匀速效果差不多,
如果渲染时一些帧突然耗时严重,这时按帧匀速和按时间匀速这两种就会出现较大的位置偏差。相对来说,按帧匀速能够避免卡顿时物体移动出现跳跃,使用户反应不及,但匀速性没有按时间匀速好。
按照按键消息发送的数量来移动是无法实现的匀速移动的,因为按键消息的发送并不是匀速的,特别是长按时,从第一次发送按下消息到第二次发送会间隔较长时间,而后续自动重复发送时发送速度又比较快。按键消息的发送和帧率并不同步,如果按照是否接收到按键消息来移动,那么就会出现部分帧静止部分帧移动的情况,这样看起来物体移动动画就比较卡顿。
为了让物体每一帧都匀速移动,应该通过检测 按键当前状态 的方来确定当前帧是否需要移动物体。
每帧检测一下按键的当前状态,如果控制物体移动的按键处于按下状态,那就移动物体。按帧匀速是每一帧移动相同的距离,如果是按时间匀速,则需要计算与上一帧间隔的时间,根据间隔时间来确定移动的距离,而不是固定。
按键当前状态使用 keystate() 进行检测即可,因为不需要考虑两帧之间按键状态切换的时序问题。
#include <graphics.h>
#include <Windows.h>
int main()
{
const int winWidth = 600, winHeight = 600;
const int radius = 64;
initgraph(winWidth, winHeight, INIT_RENDERMANUAL);
//为了美观,使用抗锯齿
ege_enable_aa(true);
setbkcolor(EGEACOLOR(0XFF, WHITE));
setcolor(BLACK);
setfillcolor(EGEARGB(0XFF, 0XFF, 0, 0XFF));
setfont(20, 0, "Simsun");
settextjustify(RIGHT_TEXT, TOP_TEXT);
setbkmode(TRANSPARENT);
//浮点型,精确位置,
float xCircle = winWidth / 2, yCircle = winHeight / 2;
//0, 1, 2, 3分别对应方向 左 上 右 下
int keys[4] = {'A', 'W', 'D', 'S'};
int directionKeys[4] = { key_left, key_up, key_right, key_down };
//移动速度, 浮点型能任意控制速度快慢,
const float speed = 2.0f;
float xSpeeds[4] = { -speed, 0, speed, 0 };
float ySpeeds[4] = { 0,-speed, 0, speed };
setlinewidth(2.0f);
setcolor(BLACK);
setfillcolor(EGEACOLOR(0xFF, 0xA0D8EF));
timeBeginPeriod(1);
for (; is_run(); delay_fps(60)) {
//根据按键按下状态对位置增量进行累加,可八方向移动
float xNext = xCircle;
float yNext = yCircle;
// 判断四个方向键是否被按下
for (int i = 0; i < 4; i++) {
if (keystate(keys[i]) || keystate(directionKeys[i])) {
// 往对应方向进行位移
xNext += xSpeeds[i];
yNext += ySpeeds[i];
}
}
// 如果移动则检测移动是否有效
if (xNext != xCircle || yNext != yCircle) {
//检测是否超出边界
if (radius <= xNext && xNext <= (winWidth - radius)
&& radius <= yNext && yNext <= (winHeight - radius)) {
xCircle = xNext;
yCircle = yNext;
}
}
/*上面是做位置计算的,下面是根据位置绘图的部分*/
//清屏
cleardevice();
ege_fillellipse(xCircle - radius, yCircle - radius, 2 * radius, 2 * radius);
ege_ellipse(xCircle - radius, yCircle - radius, 2 * radius, 2 * radius);
xyprintf(winWidth, 0, "当前位置:(%.2f, %.2f)", xCircle, yCircle);
}
timeBeginPeriod(2);
return 0;
}
3. 平滑移动
平滑移动 也叫 惯性移动,模仿物体具有的惯性,移动速度不会突变,而是从0逐渐增加,或者逐渐衰减至0。当然,本身显示的帧和像素本身就是离散的,不可能有连续,所以这里速度不会突变是指视觉上有一定过渡时间,能让人眼感觉到移动速度在缓慢变化。
平滑移动较为简单的实现就是模拟物理上的力,给物体一个匀加速度。另外有些实现是将物体的移动速度和距离联系。不管如何实现,让速度的变化幅度降低到人眼感觉到平滑渐变即可。
#include <cassert>
#include <cmath>
#include <graphics.h>
#include <Windows.h>
float clamp(float value, float max, float min);
float distance(float* speed, float forceAcceleration, float frictionAcceleration);
int main()
{
SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
const int winWidth = 800, winHeight = 800;
const int radius = 64;
timeBeginPeriod(1);
initgraph(winWidth, winHeight, INIT_RENDERMANUAL);
//为了美观,使用抗锯齿
ege_enable_aa(true);
setbkcolor(EGEACOLOR(0XFF, WHITE));
setcolor(BLACK);
setfillcolor(EGEARGB(0XFF, 0XFF, 0, 0XFF));
setfont(20, 0, "Simsun");
settextjustify(RIGHT_TEXT, TOP_TEXT);
setbkmode(TRANSPARENT);
setlinewidth(2.0f);
setcolor(BLACK);
setfillcolor(EGEACOLOR(0xFF, 0xA0D8EF));
//0, 1, 2, 3分别对应方向 左 上 右 下
int keys[4] = {'A', 'W', 'D', 'S'};
int directionKeys[4] = { key_left, key_up, key_right, key_down };
// 物体速度及加速度参数
const float MAX_SPEED = 8.0f;
const float ACCELERATION = 15.0f / 60.0f; //按键按下时在对应方向上施加的加速度
const float FRICTION_ACCELERATION = ACCELERATION / 2.0f; //摩擦力的加速度
const float xOffset[4] = {-1.0f, 0.0f, 1.0f, 0.0f}; //指示左上右下的偏移方向
const float yOffset[4] = {0, -1.0f, 0.0f, 1.0f}; //指示左上右下的偏移方向
//物体位置和速度
float xCircle = winWidth / 2, yCircle = winHeight / 2;
float xSpeed = 0.0f, ySpeed = 0.0f;
for (; is_run(); delay_fps(60)) {
//根据按键计算加速度
float xForceAcceleration = 0.0f, yForceAcceleration = 0.0f;
// 判断四个方向键是否被按下
for (int i = 0; i < 4; i++) {
if (keystate(keys[i]) || keystate(directionKeys[i])) {
// 根据方向键计算各方向的加速度
xForceAcceleration += xOffset[i] * ACCELERATION;
yForceAcceleration += yOffset[i] * ACCELERATION;
}
}
// 根据移动方向计算摩擦力对物体的加速度,用于减速
float resultantVelocity = sqrtf(xSpeed * xSpeed + ySpeed * ySpeed); //速度方向
float xFrictionAccelertion, yFrictionAccelertion;
if (resultantVelocity == 0.0f) {
xFrictionAccelertion = 0.0f;
yFrictionAccelertion = 0.0f;
}
else {
// 摩擦力产生的加速度和速度方向相反
xFrictionAccelertion = -(xSpeed / resultantVelocity) * FRICTION_ACCELERATION;
yFrictionAccelertion = -(ySpeed / resultantVelocity) * FRICTION_ACCELERATION;
}
// 计算移动距离
float xDistance = distance(&xSpeed, xForceAcceleration, xFrictionAccelertion);
float yDistance = distance(&ySpeed, yForceAcceleration, yFrictionAccelertion);
// 限制最大速率
xSpeed = clamp(xSpeed, MAX_SPEED, -MAX_SPEED);
ySpeed = clamp(ySpeed, MAX_SPEED, -MAX_SPEED);
// 根据物体位移量
float xNext = xCircle + xDistance;
float yNext = yCircle + yDistance;
if (xCircle != xNext) {
xCircle = clamp(xNext, winWidth - radius, radius);
if (xCircle != xNext)
xSpeed = 0.0f;
}
if (yCircle != yNext) {
yCircle = clamp(yNext, winHeight - radius, radius);
if (yCircle != yNext)
ySpeed = 0.0f;
}
/*上面是做位置计算的,下面是根据位置绘图的部分*/
//清屏
cleardevice();
ege_fillellipse(xCircle - radius, yCircle - radius, 2 * radius, 2 * radius);
ege_ellipse(xCircle - radius, yCircle - radius, 2 * radius, 2 * radius);
xyprintf(winWidth, 0, "当前位置:(%.2f, %.2f)", xCircle, yCircle);
}
timeBeginPeriod(2);
return 0;
}
float distance(float* speed, float forceAcceleration, float frictionAcceleration)
{
float accelertation = forceAcceleration + frictionAcceleration;
float sp = *speed;
// 如果出现力使物体速度反向,则需要考虑自然停止或摩擦力反向
if ((sp < 0) != (sp + accelertation < 0)) {
// 计算速度为0时的移动, S = -(v^2) / (2a)
float dist = -sp * sp / (2.0f * accelertation);
float time = fabs(sp / accelertation);
// 如果施加有主动力,则根据剩余时间继续移动, S = (1/2) * 加速度 * 时间的平方
// 反向移动后摩擦力也反向
if (forceAcceleration != 0.0f) {
dist += 0.5f * (forceAcceleration - frictionAcceleration) * (1 - time) * (1 - time);
*speed = (forceAcceleration - frictionAcceleration) * (1 - time);
} else {
*speed = 0.0f;
}
return dist;
}else {
*speed = sp + accelertation;
return (sp + sp + accelertation) * 0.5f;
}
}
float clamp(float value, float max, float min)
{
return (value > max) ? max : ((value < min) ? min : value);
}
EGE专栏:EGE专栏