EGE基础:键盘输入篇 |
您所在的位置:网站首页 › ege图形库怎么输出图片 › EGE基础:键盘输入篇 |
EGE专栏:EGE专栏 目录 一、按键消息1. 按键消息类型2. 长按与短按2.1 短按2.2 长按 3. 按键消息结构体 二、按键消息处理1. 按键消息队列1.1 flushkey() 清空按键消息队列 2. 按键消息处理循环 三、虚拟键码四、按键状态检测1. keystate() 函数通过keystate()检测的缺点 2. 通过读取按键消息记录按键状态 五、按键控制1. 按键识别2. 按键按下与抬起3. 辅助键 六、字符输入1. 获取字符输入 getch()2. 字符输入:由按键消息读取(EGE20.08新增)2.1 字符的编码2.2 编码设置2.2.1 GB2312编码(国家简体中文字符集)2.2.2 UTF-16编码 3. 字符的输入3.1 UTF-16编码3.2 GB2312编码3.3 字符的存储与处理 4. GB2312编码字符的处理示例示例程序 七、按键控制移动示例1. 单次移动2. 匀速移动3. 平滑移动 一、按键消息下图为笔记本键盘的一个常见键位布局,其中包含了键盘的大部分按键。
键盘上的按键在按下和抬起时,系统会发送相应的按键消息,通知程序用户使用键盘进行了哪些操作。程序可以读取这些消息,识别后做出响应。同时,键盘作为输入工具主要是用于输入文字,系统除了发送按下和抬起消息外,还会发送字符输入消息,程序可以读取来处理用户输入的字符。 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) 消息。 一般同一个按键的按下和抬起消息是成对出现的,但如果按住某个键的时间足够长,会触发键盘的自动重复功能,以一定的时间间隔重复发送 按下消息 和 字符输入消息,直到松开按键才停止重复发送。松开按键时,长按和短按都会发送一条按键抬起消息。这就是按键长按时按键消息的发送情况。 在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() 会一直等待,直到有按键消息进入队列。 当你觉得按键消息队列中的消息已经没有用时,可以将这些消息清空。这样下一次就能 直接处理到最新的键盘消息了。 清空键盘消息缓存区的函数为 flushkey(); 2. 按键消息处理循环在从按键消息队列中取出按键消息前,先用 kbmsg() 判断队列中是否保存有消息,如果有再调用 getkey() 取出队列中的消息,否则 getkey() 会一直等待,直到有新的按键消息产生才会继续往下执行。 // 从队列中取出所有按键消息 while (kbmsg()) { key_msg keyMsg = getkey(); }使用 while() 循环目的是要将队列中的消息全部取出,因为队列中消息都是用户在之前操作键盘产生的,距离用户操作已经过去了一小段时间,如果现在不处理完毕,那么消息处理会被拖延得更久,更加滞后,用户可能会感觉到程序对按键的响应比较慢。 三、虚拟键码Windows官方文档:虚拟键码(Virtual-key-codes) 键盘上的按键被映射成一个值,称为 虚拟键码(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; 四、按键状态检测按键有两个状态:松开状态和按下状态。 判断某个按键当前是否是按下状态,可以使用EGE中的 keystate() 函数,参数key是按键的虚拟键码,如果按键当前处于按下状态,那么 keystate() 函数返回 1,否则返回 0。 int keystate(int key);需要注意的是,keystate() 函数检测按键状态是依靠窗口接收到的按键消息,如果EGE窗口处于非活动窗口,那么按下按键是无法检测到的。实际上也不需要检测,在其它窗口按下按键本身就是和本窗口无关。如果真的想要检测,可以使用 win API 中的 GetAsyncKeyState() 函数。 由于一些键盘检测电路的设计原因,在某些按键组合按下时,会有无法识别出其它一小部分按键按下松开的现象。至于是哪些按键的组合会出现这种状况,要看键盘的电路如何设计。 例如,检测回车键是否是按下状态: if (keystate(key_enter)) { } 通过keystate()检测的缺点缺点是只能检测按键 当前的状态,如果在一段时间内很多按键快速地按下又松开,结束后你再去检测当前状态,是无法得到他们在这一段时间内按下和抬起的顺序的。 当某一帧的计算非常耗时,在这段时间内可能会出现按键操作无效的现象,因为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编码字符的处理示例汉字输入的时候,输入法会有候选窗口弹出,直到按下空格或数字才会选中对应的汉字。此时字符消息一起发出,一个汉字发出两个字符消息。并且是按编码中的顺序发出。所以只要按顺序存储即可。可以创建个字符缓存区,并且增加缓存区长度记录。
先判断是否为字符消息 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 #include 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。 单次移动: 按下一次按键只会移动一次,长按也不会再次移动。 单次移动一般是在按键按下时移动,按键按下动作可以通过按键状态切换来检测(消息类型是 key_msg_down 且之前是松开状态)。如果想响应按键抬起动作,那么只需要检测按键消息类型是否是 key_msg_up 即可,按键抬起消息并不会重复发送。 |
今日新闻 |
推荐新闻 |
CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3 |