为什么要加入快捷键系统
对用户来说:
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 - actionstatic 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、增删改查"按键-功能关联"容易,并且可以动态修改