Never save something for a special occasion. Every day in your life is a special occasion.
分类: 其他平台
2013-06-12 16:37:55
dos模拟按键识别 单击、双击、长按
本文研究一种 基于DOS平台的按键功能模拟方案,实现以下功能:
1、识别单击、双击
2、识别长按(3s)
先来认识 dos 提供的2个按键相关的api
键盘驱动程序检测到按键后将按键放入按键队列,供系统读取。
系统通过函数kbhit, bioskey 可从按键队列读取按键,大致区别如下:
int kbhit() 非阻塞调用,检测是否有按键按下,但不将其从按键队列中清除。通常配合 getch 使用读取按键值。
int bioskey(cmd) 是tc编译环境下的函数,有2种调用方式:
cmd=0时,阻塞调用,行为类似 getch。
cmd=1时,非阻塞调用,行为类似 kbhit。
另外,当cmd=3时 bioskey 还能检测功能(ctrl,Shift,Alt)键是否按下。
适应于单片机的 按键识别状态机如下
程序每100ms检测一次按键电平,若从0(无效)到1(有效),则识别为KeyDown;
若连接30次(3s)都为1,则进入KeepPress状态,识别为长按(3s);
若3s内电平变为0,则进入Click状态,但还不确定是单击;
进入Click状态后若300ms内又按下,则进入DblClick状态,识别为双击;
进入Click状态后若300ms内未再按下,则识别为单击,进入Idle状态。
根据以上状态图在dos下实现的模拟按键检测
#include
#include
#include
#include
int main() { char ch=0; char chbak=0;
printf("hello C\n");
for(;;) { const char* up_evt = "up"; const char* down_evt = "down"; const char * e = NULL;
delay(100); // 注2
ch = 0; if(kbhit()) { ch = 0; chbak = 0; while( kbhit() && (ch=getch())) { printf("getchar:%c\n", ch); if(chbak != ch) /* 检测到按键 */ { if(chbak) /* 不同的按键 */ { ungetch(ch); ch = chbak; break; } chbak = ch; /* 保存此次按键 */ break; } }
if(ch) { printf("Key down:%c\n", ch); e = &down_evt; }
if(ch == '\33') { printf("Bye\n"); fflush(stdout); break; } } else { if(chbak!=0) { e = up_evt; printf("======Key UP:%c\n", chbak); chbak = 0; } }
if(e) printf("EVENT:%s\n", e); }
return 0; } |
程序每100ms读取按键队列,若kbhit返回真,则识别有键按键,发送KD事件;若kbhit返回假,且前一100ms内有键按下,则识别按键释放,发送KU事件。
这个程序不能如期工作,测试发现:“保持按键按下时 kbhit返回真” 不总是成立。
为什么呢?这与按键驱动程序有关:
l 为了快速输入同一字符,字符间隔定义为80ms(注3);
l 为正确识别用户“快速输入同一字符”的意图,将识别出按键按下后的450ms作为长按识别区,长按识别区内不向按键队列中每80ms放入一个键值。(长按识别区后每 80ms 向按键队列中放入一个键值。)
注3:
80ms,450ms只是估计值。
键盘驱动程序工作原理示意图
从示意图中可以看到,长按时若在长按识别区2次调用kbhit+getch,则第2次调用kbhit()将返回false。
这个图也说明:
1、OS不直接检测按键,而是通过api获取按键值;
2、OS至少经过“长按识别区”的时间才能区分“单击”和“长按”。
为了检测长按,将检测间隔改为delay(450),同时修改状态机如下:
程序每450ms检测读取按键队列,若有按键则发送事件KD,进入KeyDown状态;
与次读取按键队列时若有按键,则发送事件KD,cntPress对KD进行计数。
若连接多次(3000/450)都有KD,则进入KeepPress状态,识别为长按。
若在识别为长按前读取按键队列返回false,则发送事件 KU,识别为n击(n=1、2),进入MyIdle开始下一轮按键识别。
读取按键队列的代码如下:
#include
#include
#include
#include
#define LONG_PRESS_IND 450/*ms*/ #define CNT_PRESS_3S (3000/LONG_PRESS_IND - 1)
static SimKeyDrv l_skd;
int main() { char ch=0; char chbak=0; // 保存从按键队列中读取到的键值 int flagKU = 0;
SimKeyDrv_ctor(&l_skd); QFsm_init((QFsm*)&l_skd, (QEvt*)0);
printf("hi qfsm\n");
for(;;) { static KeyUp up_evt = {KU_SIG, 0, 0, 0}; static KeyDown down_evt = {KD_SIG, 0, 0, 0}; static TickEvt tick_evt = {TICK_SIG, 0, 0, 0}; QEvt * e = NULL;
if(++tick_evt.fine_time == 10) tick_evt.fine_time = 0;
QFsm_dispatch((QFsm*)&l_skd, (QEvt*)&tick_evt);
delay(LONG_PRESS_IND); // 读取按键按键队列的时间间隔
if(kbhit()) { while(kbhit() && (ch=getch())) /* 处理重复键值,最多2次 */ { //printf("dbg getchar:%c(%c)\n", ch, chbak); if(chbak != ch) { if(chbak) /* 若干重复键值后遇到不同的键值 */ { ungetch(ch); /* 放回队列在下次处理 */ ch = chbak; /* 本次要处理的键值 */ flagKU = 1; /* 处理下一键值前当前键已释放*/ break; } chbak = ch; /* 保存以备ungetch */ break; } }
if(ch == '\33') { printf("Bye\n"); fflush(stdout); break; }
// 应该在发送 KD 事件后再发送 KU 事件,但这个bug不影响需求“模拟单击、双击、长按”的实现。 // bug: press "aab", expect "click2a,click1b" if(flagKU) { flagKU = 0; //printf("======dbg Key UP2:%c(%c)\n", ch,chbak); e = (QEvt*)&up_evt; ((KeyUp*)e)->key = ch; chbak = 0; } else { //printf("dbg Key down:%c(%c)\n", ch,chbak); e = (QEvt*)&down_evt; ((KeyDown*)e)->key = ch; }
} else { if(ch!=0) { //printf("======dbg Key UP:%c(%c)\n", ch,chbak); e = (QEvt*)&up_evt; ((KeyUp*)e)->key = ch; ch = 0; chbak = 0; } }
if(e) { //printf("EVENT:%p\n", e); QFsm_dispatch((QFsm*)&l_skd, e); } }
return 0; } |
读取按键后交由QFsm处理状态图对应的逻辑(也可以使用原始的双层switch-case实现):
/***************************************************************************** * Model: SimKeyDrv2.qm * File: E:\Program\QP\qpc\examples\80x86\dos\watcom\l\SimKeyDriver/SKD2.c * * This code has been generated by QM tool (see state-machine.com/qm). * DO NOT EDIT THIS FILE MANUALLY. All your changes will be lost. * * This program is open source software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as published * by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * for more details. *****************************************************************************/ /* @(/3/0) .................................................................*/ /* Simulate Key Driver base on DOS.
*/
// Include #include "qp_port.h"
#include
#include
#include
#include
// Signals typedef enum SimKeyDrvSignalTag{ KD_SIG = Q_USER_SIG, KU_SIG, TICK_SIG }SimKeyDrvSignal;
// events /* @(/1/0) .................................................................*/ typedef struct KeyDownTag { /* protected: */ QEvt super;
/* public: */ uint8_t key; } KeyDown;
/* @(/1/1) .................................................................*/ typedef struct KeyUpTag { /* protected: */ QEvt super;
/* public: */ uint8_t key; } KeyUp;
/* @(/1/2) .................................................................*/ typedef struct TickEvtTag { /* protected: */ QEvt super;
/* public: */ uint8_t fine_time; } TickEvt;
// oa /* @(/2/0) .................................................................*/ typedef struct SimKeyDrvTag { /* protected: */ QFsm super;
/* public: */ uint8_t key; uint8_t preKey; uint8_t cntPress; } SimKeyDrv;
/* public: */ static void SimKeyDrv_ctor(SimKeyDrv * const me);
/* protected: */ static QState SimKeyDrv_initial(SimKeyDrv * const me, QEvt const * const e); static QState SimKeyDrv_MyIdle(SimKeyDrv * const me, QEvt const * const e); static QState SimKeyDrv_KeyDown(SimKeyDrv * const me, QEvt const * const e); static QState SimKeyDrv_KeepPress(SimKeyDrv * const me, QEvt const * const e);
// constractor //$define(AOs::SimKeyDrv::ctor)
/* @(/2/0) .................................................................*/ /* @(/2/0/3) ...............................................................*/ static void SimKeyDrv_ctor(SimKeyDrv * const me) { QFsm_ctor(&me->super, (QStateHandler)&SimKeyDrv_initial);/* superclass ctor */
} /* @(/2/0/4) ...............................................................*/ /* @(/2/0/4/0) */ static QState SimKeyDrv_initial(SimKeyDrv * const me, QEvt const * const e) { me->cntPress = 0; //me->key = 0; //me->preKey = 0;
return Q_TRAN(&SimKeyDrv_MyIdle); } /* @(/2/0/4/1) .............................................................*/ static QState SimKeyDrv_MyIdle(SimKeyDrv * const me, QEvt const * const e) { QState status_; switch (e->sig) { /* @(/2/0/4/1/0) */ case KD_SIG: { status_ = Q_TRAN(&SimKeyDrv_KeyDown); break; } default: { status_ = Q_IGNORED(); break; } } return status_; } /* @(/2/0/4/2) .............................................................*/ static QState SimKeyDrv_KeyDown(SimKeyDrv * const me, QEvt const * const e) { QState status_; switch (e->sig) { /* @(/2/0/4/2) */ case Q_ENTRY_SIG: { me->cntPress = 1; printf("Entry:KeyDown\n"); status_ = Q_HANDLED(); break; } /* @(/2/0/4/2/0) */ case KU_SIG: { printf("=======Click %d, %c!\n", me->cntPress, ((KeyUp*)e)->key); status_ = Q_TRAN(&SimKeyDrv_MyIdle); break; } /* @(/2/0/4/2/1) */ case KD_SIG: { me->cntPress++; /* @(/2/0/4/2/1/0) */ if (me->cntPress > (3000/450)) { me->key = ((KeyDown*)e)->key; status_ = Q_TRAN(&SimKeyDrv_KeepPress); } /* @(/2/0/4/2/1/1) */ else { status_ = Q_HANDLED(); } break; } default: { status_ = Q_IGNORED(); break; } } return status_; } /* @(/2/0/4/3) .............................................................*/ static QState SimKeyDrv_KeepPress(SimKeyDrv * const me, QEvt const * const e) { QState status_; switch (e->sig) { /* @(/2/0/4/3) */ case Q_ENTRY_SIG: { printf("=======Keep Press 3s, %c\n", me->key);
status_ = Q_HANDLED(); break; } /* @(/2/0/4/3/0) */ case KU_SIG: { printf("=======Press tick %d, %c!\n", me->cntPress, me->key); status_ = Q_TRAN(&SimKeyDrv_MyIdle); break; } default: { status_ = Q_IGNORED(); break; } } return status_; }
|
测试:
总结:
状态机思想很明晰。别看这里状态处理函数显得复杂,相对成堆 if-else 写出的意大利面条式代码,qm生成的状态处理函数规整而易维护。最后总结重点如下:
1、 此实验目标是在dos下模拟按键识别出单击、长按,还有双击。
2、 明白这点:os通过api读取按键队列,而直接识别按键的是键盘驱动程序。由于驱动程序的长按识别区的存在,长按时kbhit()可能返回false,这取决于os对按键队列的读取间隔。
3、 每次读取时忽略2次以上的重复键值。
4、 看状态图,而不要把注意力焦点在代码上。
5、 使用QM生成和维护状态机处理函数,掌握QP的QFsm使用。