2013年6月29日星期六

如何为现有项目无痛加入快捷键系统

为什么要加入快捷键系统

    对用户来说:
        1、鼠标只有有限的几个按键,操作多靠点选菜单或其他标识为可点击的区域,移动慢、点准难,但使用简单——初级用户需要它。
        2、快捷键则因为键盘按键多,可以对应的功能也多,同时容易实现盲按,可以有效提高操作效率——许多高级用户所需要的功能。
    对开发者来说:
        使用快捷键不需要在界面上明显展示可点击的控件,可以有效节约窗口空间——界面设计;
        迎合高级用户需求,提高产品口碑——产品意识。

快捷键系统应达到的目标

    1、执行效率高
        如果按下键盘按键,应用要经过很长时间才能反应,那显然是不合适的。
    2、不打破现有代码结构 (耦合度低)
        对于一些大的项目,如果加入快捷键支持需要修改大部分已有的类,显然也没有吸引力。
    3、全局统一的机制,不同的功能之间互不冲突
        比如控件A使用了按键F1来执行放大功能,那么类B就不能使用按键F1来执行删除文件功能。按键与功能之间是一一对应的。
    4、增删改查"按键-功能关联"容易,并且可以动态修改
        比如我们实现了某个新的功能,现在要将这个功能和某个快捷键对应起来,能不能很简单就做到?

早期EzViewer对按键的支持

   在EzViewer在设计中,原先并没有考虑到快捷键实现的设计问题,只是简单在MainWindow处理按键事件,调用相应对象的方法,因此存在以下问题:
    1、虽然有对键盘按键的支持,但都是hard code进去的,不支持自定义;
    2、添加某个功能的按键支持就要增加一些代码,这样导致代码越来越多;
    
void MainWindow::keyPressEvent(QKeyEvent *e)
{
switch(e->key()){
case Qt::Key_Right:
case Qt::Key_Down:
case Qt::Key_PageDown:
case Qt::Key_J:
nextPic();
e->accept();
break;
... ...
default:
QWidget::keyPressEvent(e);
break;
}
        }
    3、MainWindow严重依赖各个类的具体实现或接口。
    
case Qt::Key_Equal:
case Qt::Key_Plus:
switch(e->modifiers()){
case Qt::ShiftModifier:
viewer->zoomIn(0.05);
break;
case Qt::ControlModifier:
viewer->zoomIn(0.2);
break;
default:
viewer->zoomIn(0.1);
break;
}
e->accept();
break;

 利用泛型+多态的实现方案

    对一个简单的小项目,可能上面那样hard code就足够了。但是如果项目已经很大呢?这时候如何在不对原有系统进行大的改动的情况下引入快捷键支持?
    这里介绍我的一点经验。在设计快捷键系统的时候,我是这样考虑的:
        1、一个全局单例的快捷键管理类,ActionManager,用于管理按键与功能之间的对应关系,以及传入一个按键参数后自动运行对应功能;
        2、ActionManager必须尽可能少依赖,即它不应该知道具体的类或方法。
    从上面两点考虑,我初步的构想是这样的:
        ActionManager存储两个链表,链表里面都是键值对,其中一个是“标签 - 指向函数的指针”键值对,另一个是“按键值 - 标签”键值对。这样当用户按下按键时,我们将按键值传给ActionManager,ActionManager遍历“按键值 - 标签”,如果找到对应的标签,说明这个快捷键是绑定了某个功能的,这时候再遍历“标签 - 指向函数的指针”键值对,找到对应的函数指针并调用这个函数。
    使用上面的方法, 需要解决的一个问题是,如何存储“指向函数的指针”。C++里面可以定义“指向函数的指针”类型,但是这个类型只能匹配签名完全一致的情况,即需要函数的返回值类型、参数个数和类型都一一对应。类的成员函数的指针类型也和指向函数的指针类型不同。为了能够存储不同类型的函数或成员函数,需要将“标签 - 指向函数的指针”键值对封装起来。为此,我采用了一个抽象类,简化实现如下:
    
class Action
{
    public:
        virtual bool run() = 0;
};
   这个抽象类派生出一系列的模板类,例如:
        
template <typename T, typename ReturnType = void>
class ActionImpl : public Action
{
public:
typedef ReturnType (T::*FuncType)();

ActionImpl(T *obj, FuncType f) : object(obj), function(f)  {}

virtual bool run() {
if (object && function) {
(object->*function)();
return true;
}
return false;
}
private:
T *object;
FuncType function;
};
    上面的类是用于封装无参数的类成员函数指针的,如果需要存储带有参数的类成员函数指针,需要派生出其他类,下面是封装带有一个参数的类成员函数的方法:
template <typename T, typename ReturnType = void, typename ArgumentType = int>
class ActionImplWithArgument : public Action
{
public:
typedef ReturnType (T::*FuncType)(ArgumentType);

ActionImplWithArgument(T *obj, FuncType f, ArgumentType arg)
: object(obj), function(f), param(arg)
{}

virtual bool run() {
if (object && function) {
(object->*function)(param);
return true;
}
return false;
}
private:
T *object;
FuncType function;
ArgumentType param;
};
    用这种方式还可以封装全局的函数指针,由于我的项目采用C++面向对象开发,不需要用到,这里就不讲了。
    于是通过封装指向函数的指针,原先的设计变成这样
     ActionManager存储两个链表,链表里面都是键值对,其中一个是“标签 - 指向Action对象的指针”键值对,另一个是“按键值 - 标签”键值对。“指向Action对象的指针”中实际存储的是指向其派生类对象的指针。这样当用户按下按键时,我们将按键值传给ActionManager,ActionManager遍历“按键值 - 标签”,如果找到对应的标签,说明这个快捷键是绑定了某个功能的,这时候再遍历“标签 - 指向Action对象的指针”键值对,找到对应的Action指针并调用其run()方法。  由于多态,将会正确运行我们需要的功能。
    实际使用中为了提高效率,并没有采用链表的设计,而是使用Map,后续还可以根据需要改成HashMap,以提高查找效率。ActionManager类的简化实现如下:
    
class ActionManager
{
public:
template <typename PT, typename T, typename ReturnType>
static void registerFunction(const QString &description, PT *obj,
ReturnType (T::*function)(), const QString &tag);

// ... 这里省略了一些注册含参数的成员函数指针的方法
// run the function that bind with the key sequence.
static bool run(const QString &keySequence);

private:
static QMap<QString, Action*> actionMap;     // actionTag - action
static QMap<QString, QString> shortcutMap; // keySequence  - actionTag
};
    虽然模板看起来很复杂,但是实际使用很简单,这得益于C++编译器强大的模板类型推断能力。每个声明自己某个功能允许快捷键调用的类,都只要在自己的构造函数中按如下方法调用,就能将这个功能注册到ActionManager中:
    
ActionManager::registerFunction(tr("Open"), this, &MainWindow::openFile, "MainWindow::openFile");
    而MainWindow现在要做的事仅仅是在用户有按键输入的时候将按键值传递给ActionManager,不需要关心其他类的具体实现了:
    
void MainWindow::keyPressEvent(QKeyEvent *e)
{
QKeySequence keys(e->modifiers() + e->key());

if(ActionManager::run(keys.toString())) {
e->accept();
} else {
QWidget::keyPressEvent(e);
}
}
当然,具体按键与功能的绑定需要在某个地方进行设置,并进行保存,这里就不多说了。

是否达到原先的目的


    1、执行效率高
        这个系统基于键-值对,两次查找后就能直接调用对应的成员函数,效率上没什么大的损失。
    2、不打破现有代码结构 (耦合度低)
        现有的类只需在构造函数或其他地方注册自己的功能到ActionManager中。ActionManager并不关心自己保存的是什么。
    3、全局统一的机制,不同的功能之间互不冲突
        采用Map存储,按键与功能之间是一一对应的。
    4、增删改查"按键-功能关联"容易,并且可以动态修改
        这个设计支持动态增删改查,容易修改按键与功能的关联。

文档信息

没有评论:

发表评论