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

虚拟键盘(软键盘)设计要点
    前些天花了很多时间写这样一个软键盘,效果是显示一个与键盘外观相似的视图,通过鼠标单击像活动窗口发送虚拟的键盘消息。目标是实现像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】

你可能感兴趣的:(虚拟键盘(软键盘)设计要点)