S.l.e!ep.¢%

像打了激速一样,以四倍的速度运转,开心的工作
简单、开放、平等的公司文化;尊重个性、自由与个人价值;
posts - 1098, comments - 335, trackbacks - 0, articles - 1
  C++博客 :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理

虚拟键盘(软键盘)设计要点

Posted on 2009-10-19 14:27 S.l.e!ep.¢% 阅读(3650) 评论(5)  编辑 收藏 引用 所属分类: VC
    前些天花了很多时间写这样一个软键盘,效果是显示一个与键盘外观相似的视图,通过鼠标单击像活动窗口发送虚拟的键盘消息。目标是实现像windows自带的软键盘osk相似。
    看似很简单的工作,设计中却遇到了很多困难。
    困难一:键盘按键分类
        键盘按键有很多种分类方法。
        第一种:按显示分类。按住shift键,字母键、符号键显示上面的字符;按下caps lock键,字母键切换为大写字母。
        第二种:按功能分类。大体有可显示字符类、控制类。控制类包括shift,ctrl等。
        为了解决可变的显示问题,采用了一个自我感觉非常好的解决方案:字符集、键集相互独立。如此一来,只要总体按照功能分类,通过特定功能的按键控制有效字符集即可,也就是说,对普通按键来说,它只负责到指定的字符集中去取对应序号的字符即可。
//LabelSet.h
#pragma once

//字母标签集合
class LabelSet
{
public:
    LabelSet(LPCSTR
* _pTable,int _n);
    LPCSTR getLabel(
int _id) const;

    
~LabelSet();

protected:
    LabelSet(){}

private:
    LPCSTR
* pTable;
    
int n;
};

//相当于单刀双掷开关组
class LabelSetEx
{
protected:
    
struct Switch
    {
        LabelSet
* s[2];
        
int at;
    };

public:
    LabelSetEx(
int _n);
    
bool addSets(int id,LPCSTR* s1,LPCSTR* s2,int n,int at = 0);
    LPCSTR getLable(
int id,int off) const;
    
void turn(int id);

    
~LabelSetEx();

private:
    
int n;    //开关组总个数
    Switch* pGroup;    //开关组
};

//
//LabelSet.cpp
#include "StdAfx.h"
#include 
"LabelSet.h"
#include 
<algorithm>
#include 
<cassert>

using namespace std;

LabelSet::LabelSet( LPCSTR
* _pTable,int _n )
{
    n 
= _n;
    pTable 
= new LPCSTR[n];
    copy(_pTable,_pTable 
+ _n,pTable);
}

LPCSTR LabelSet::getLabel( 
int _id ) const
{
    
return pTable[_id];
}

LabelSet::
~LabelSet()
{
    delete [] pTable;
}

LabelSetEx::LabelSetEx( 
int _n )
{
    n 
= _n;
    pGroup 
= new Switch[n];
    memset(pGroup,
0,n * sizeof(pGroup[0]));
}

LabelSetEx::
~LabelSetEx()
{
    
while(n--)
    {
        
if(pGroup[n].s[0== pGroup[n].s[1])
            delete pGroup[n].s[
0];
        
else
        {
            delete pGroup[n].s[
0];
            delete pGroup[n].s[
1];
        }
    }
    delete [] pGroup;
}

bool LabelSetEx::addSets( int id,LPCSTR* s1,LPCSTR* s2,int n,int at /*= 0*/ )
{
    assert((at 
& ~1== 0);
    
if(pGroup[id].s[0!= NULL)
        
return false;
    LabelSet
* p = new LabelSet(s1,n);
    pGroup[id].s[
0= p;
    
if(s1 == s2)
        pGroup[id].s[
1= p;
    
else
        pGroup[id].s[
1= new LabelSet(s2,n);
    pGroup[id].at 
= at;
    
return true;
}

LPCSTR LabelSetEx::getLable( 
int id,int off ) const
{
    Switch
* p = pGroup + id;
    
return p->s[p->at]->getLabel(off);
}

void LabelSetEx::turn( int id )
{
    assert((pGroup
->at & ~1== 0);
    pGroup[id].at 
^= 1;
}
        以上取开关的索引id是指字符集的分类id,在config.h文件下定义了这样的id
#pragma once

//分类id的定义
#define LABEL_SET_ALPHA  0
#define LABEL_SET_SYMBOL 1
#define LABEL_SET_NUMPAD 2
#define LABEL_SET_MAIN   3
#define LABEL_SET_HELP   4

//字母串表
extern LPCSTR AlphaTable1[];    //小写
extern LPCSTR AlphaTable2[];    //大写
extern const int AlphaTableSize;

//符号串表
extern LPCSTR SymbolTable1[];    //
extern LPCSTR SymbolTable2[];    //
extern const int SymbolTableSize;

//小键盘数字表
extern LPCSTR NumPadTable1[];    //数字
extern LPCSTR NumPadTable2[];    //光标控制
extern const int NumPadTableSize;

//主键盘单显
extern LPCSTR MainTable[];
extern const int MainTableSize;

//辅助键盘单显
extern LPCSTR HelpTable[];
extern const int HelpTableSize;

struct KeyConfig
{
    
short id;        //分类id
    short offset;    //类内偏移
    RECT rt;    //位置
    BYTE vk;    //虚拟码
};

extern KeyConfig kcs[];
extern const int kcSize;
extern const SIZE kbSize;
        第一次这样写代码,写完发现这样极大地提高了灵活性,只要在配置文件config.cpp中修改,就可以产生很多种不同的界面(虽然仍然是代码级别的,毕竟迈出了第一步,今后还会尝试改成xml配置)。
        言归正传,这样的设计分离了按键与显示,可配置能力大大加强。但仍然存在第二个大问题。
    问题二:输入焦点的确定
        方案一:现在只要在网上搜索“虚拟键盘”,能够搜到一大溜的源代码,但只可惜全是同一份拷贝,而且存在一点小错误。他的解决方案是:利用 PreTranslateMessage,在底层调用它之前,前台窗口仍然没有改变,此时是获得前一个前台窗口的好时机,获得后保存,并将使用 AttachThreadInput将当前线程绑定活动窗口的消息队列,然后在单击虚拟键盘时使用SetFocus将保存的窗口设为焦点(源代码中同时使用了SetForgroundWindow和SetFocus,这是失效的原因),然后发送虚拟按键。
        方案二:其实有更简便的方法。设置主窗口属性为WM_ES_NOACTIVATE,这样窗口就不会成为前台窗口,不管如何发送键盘消息,拥有焦点的窗口总会收到。但此时仍然存在问题。当移动窗口时,效果不大顺畅,而且没办法响应菜单命令,那是因为该窗口始终不是前台窗口造成的。解决方法就是在单击标题栏时,成为前台窗口,释放是归还前台。
void CMainFrame::OnNcLButtonDown(UINT nHitTest, CPoint point)
{
    
if(m_hForground == NULL)
    {
        m_hForground 
= ::GetForegroundWindow();
        ModifyStyleEx(WS_EX_NOACTIVATE,
0);
        SetForegroundWindow();
    }
    CFrameWnd::OnNcLButtonDown(nHitTest, point);
}
                但是,如果想当然归还前台使用WM_NCLBUTTONUP消息的话,就要让你失望了,windows似乎有意跟我们开玩笑,必须单击两次才能响应这个消息。没办法,于是尝试WM_NCMOUSELEAVE,但效果也不好,最终尝试WM_NCMOUSEMOVE,很好,这次终于成功了。
void CMainFrame::OnNcMouseMove(UINT nHitTest, CPoint point)
{
    
if(m_hForground != NULL)
    {
        ::SetForegroundWindow(m_hForground);
        ModifyStyleEx(
0,WS_EX_NOACTIVATE);
        m_hForground 
= NULL;
    }
    CFrameWnd::OnNcMouseMove(nHitTest, point);
}
        问题到此为止,现在说说一点小小的发现。
        原本以为一般的按键就两种状态,通过down、up改变,如果用方波描述,down就是下降沿触发,up是上升沿触发。也曾了解,像shift这样的按键会很复杂,存在多个状态。后来测试发现,shift并非一个特例,所有的按键都有4个状态,通过down、up改变状态。只是不同按键对状态的关注点不同。
        可以做这样一个测试,用GetKeyboardState得到各个虚拟码对应的按键状态。最高位为1时表示键被按下,最高位为1时,如果是lock键则表示被锁住,对于其他键,各有各的作用。
        比如一个键,用2位的二进制数表示这些状态,设初始状态为10,经过down后,变为01,经过up后,变为11,再经过down后,变为00,再经过up后,变为10,如此四个状态经过down、up实现了周期性的状态装换。大体符合这样的规律:
            10-(down xor 11)->01->(up xor 10)->11-(down xor 11)->00(up xor 10)->10。
        这样,如果虚拟得比较彻底,在虚拟键盘内部可以轻易地实现状态的记忆,并且可以获得足够的信息。对于显示、控制都非常方便。

    这只是第一个版本,还有很多问题需要解决。
    待解决问题一:xml配置动态配置键盘,及动态更换显示效果。
    待解决问题二:同步物理键盘。
    待解决问题三:更深层次,防止键盘消息被hook,初步认识,似乎可以使用剪贴板。
   【源代码1.2版本:http://www.cppblog.com/Files/yefeng/VirtualKeyboard1.2.rar

Feedback

# re: 虚拟键盘(软键盘)设计要点   回复  更多评论   

2010-02-03 09:40 by 长天
谢谢博主的文章和程序,感谢博主的技术分享

# re: 虚拟键盘(软键盘)设计要点   回复  更多评论   

2011-04-29 17:58 by ss
你这键盘我想给他改小点,每个虚拟按钮的间隔在哪个函数改啊!?谢谢了哈

# re: 虚拟键盘(软键盘)设计要点   回复  更多评论   

2011-07-14 16:36 by 李进安
很不错,下载学习一下

# re: 虚拟键盘(软键盘)设计要点   回复  更多评论   

2013-10-22 16:10 by red
非常感谢博主!正好要开发软键盘

# re: 虚拟键盘(软键盘)设计要点   回复  更多评论   

2013-10-23 09:49 by red
博主 有个小bug不知道该怎么改
当点击完某个键的时候 时不时会出现 该键还遗留按下去的蓝色 回不到原本颜色

是和页面的刷新快慢有关吗?

非常感谢

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理