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、增删改查"按键-功能关联"容易,并且可以动态修改
        这个设计支持动态增删改查,容易修改按键与功能的关联。

文档信息

2013年6月16日星期日

Qt4读取exif的一个轻量级C++库

    exif是jpeg的一个扩展。标准jpeg文件通常包含了很多个section,而exif信息就保存在其中一个section里。正如软件行业通常会遇到的情况,在未成为正式标准前总有许多厂商发表了各自的实现,exif也不例外,这就导致exif信息解析起来比较复杂。目前对exif支持最好也最有名的当属C语言写成的libexif了。
    我在EzViewer项目中,最开始是使用libexif库来解析exif信息的。但是libexif有许多缺点,例如库比较大(有1M左右),与C++对接比较麻烦,与Qt的国际化方式不兼容,api也很难用。事实上,使用libexif需要了解许多exif结构上的知识,学习成本比较高。因此后来决定换掉libexif。
    开始找到的替换方案是exiv2,这是一个有名的C++库。阻止我使用它的原因是这个库仍然比较大。后来又找到jhead,一个C语言的命令行程序,十分轻量(编译后只有几十K)。我一开始尝试将其简化并改造成库,也确实完成了,但用起来情况不理想:因为原先程序是为命令行设计的,因此没有很好的容错处理,一旦解析错误就直接退出,不利于程序中调用。
    最终我找到easyexif。easyexif是一个轻量级的纯C++库(源码只有几百行),专门用于解析exif信息。它的用法很简单:读取整个文件到内存中,调用EXIFInfo::parseFrom()方法对该内存进行解析,就可以获取一系列exif信息了。当然,easyexif也有缺点:1、因为是轻量级的,所以只支持标准exif的一些常见属性;2、需要将整个文件读入内存,即使那个文件可能不包含exif信息;3、EXIFInfo类对一些值没有进行初始化或初始值不正确,导致无法判断某些属性是否存在,无法复用EXIFInfo类。
    针对easyexif的这些缺点,我用Qt4写了一个类,包含下面功能:预先解析文件,借鉴jhead的代码判断是否jpeg格式、是否含有exif section、exif section的起始位置和长度,这样只在含有exif section时才将这个sectionn读入内存,传给EXIFInfo::parseFrom()进行解析。同时也结合exif规范文档对easyexif本身进行了修改,包括增加一些属性支持、更好的初始化以便复用EXIFInfo类。所有的代码可以在EzViewer源码中找到,主要是ImageHeader类和exif.h、exif.cpp文件。 ImageHeader类的用法可以参考其中ImageWrapper.cpp的attribute()方法。

参考:

文档信息