2013年8月24日星期六

为你的Qt4代码添加Qt5支持

    如果Qt4代码足够规范,要使其支持使用Qt5编译还是比较容易的。下面是我在用Qt5编译EzViewer时遇到的一些问题,总结一下。总的来说,如果只使用Widget,Qt5与Qt4的区别还是比较小的。
区别一:

    Qt 5 与 Qt 4 的一个主要区别是,将 widget 从 QtGui 模块放到了全新的 QtWidgets 模块。这对我们项目的影响主要有两点:

1、.pro项目文件,需要加入widgets模块:
    QT += widgets
    为了保持与Qt4的兼容,可以这样写:
    greaterThan(QT_MAJOR_VERSION, 4): QT += widgets

2、头文件。
    1) 为了方便,很多人都是这样引用Qt的头文件的:
    #include <QtGui>
    在Qt5里面,则还需要加上:
    #include <QtWidgets>
    2) 有些人则比较注意只引用要用到的头文件,但他们可能是这样写的:
    #include <QtGui/QWidget>
    这种方式现在也要改成下面的样子:
    #include <QtWidgets/QWidget>
    3) 不论是上面哪种方式,为了和Qt4兼容,要写成这个样子:
    #if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)
    // #include <QtWidgets> or others
    #endif // QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)
    
    4) 因此,为了保持兼容和简洁,建议最好采用下面的方式引用头文件:
    #include <QWidget>
    这种方式在Qt4和Qt5中都是正确的。

区别二:
为了让代码跨平台,经常需要针对不同平台的特性做一些处理,通常是依赖平台相关的宏来判断。在 Qt 5 中,所有的 Q_WS_* 都变成了 Q_OS_*。因此,在 Qt 4 中的代码
#ifdef Q_WS_WIN
// call windows API
#endif
在 Qt 5 中应该写成
#ifdef Q_OS_WIN
// call windows API
#endif
而我的做法是这样的,在用到这些宏的cpp文件前面加上这样的定义:
    #if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)
    #define Q_WS_WIN Q_OS_WIN
    #endif // QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)
这样下面的代码就可以照常使用Q_WS_WIN了。


参考文档:


文档信息

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()方法。

参考:

文档信息

2013年1月6日星期日

让Qt4支持psd与webp格式的图片

    Qt4的图片格式支持是通过插件形式的,想要支持一种新的图片格式,只要继承QImageIOPlugin和QImageIOHandler两个类,在里面实现对这种图片格式的解析,保存到QImage对象即可。编译时则在.pro文件中指定TEMPLATE为lib,生成的库文件可以放到Qt安装目录下的plugins\imageformats目录,在发布程序时则随其他图片格式插件一起发布。这样QImageReader会自动加载插件,就可以正确显示图片了。
    Qt4想支持psd图片格式,可以使用libqpsd这个库。根据作者的说法,目前只支持读取压缩格式为RLE,色彩模式为RGB(8Bpp per channel only)的psd图片。实际使用中发现加载psd图片时速度比较慢,这是因为作者的代码中使用了太多的循环,其中有些处理比较复杂。经过我的优化,将原先的循环次数减少为原来的一半,这样速度可以提高一倍。实际使用效果也证明了这一点。我修改过的库源码放在这里:github.com/yuezhao/libqpsd
    至于webp格式支持,原先我是参考了以下内容:qwebp.7z 以及qiviewer的源码,自己写了个webp插件。但是qiviewer的libwebp代码比较旧,不支持透明通道。最后在github上找到了同样开源的qt_webp,直接编译它的代码就能用了,省了不少力气。
    EzViewer还在更新中,接下来会让它支持更多图片格式,更加好用!


文档信息

2012年10月25日星期四

Ez看图重构,变化的开始

      Ez看图最初编写时只有一个大概的框架,是一步步在试验中成长起来的,因此导致内部模块划分不够细,每个类都很复杂。比如负责图片显示的类ImageViewer,连文件切换这些功能都要由它实现,不同功能的代码混在一起,难以理清头绪,维护起来也比较困难,容易忽略某些条件,引入bug。
      于是一直想着要进行重构。刚好国庆几天回家有空,就先进行了大略的改写,开始的思路是把文件管理的部分归到一个独立的类PicManager中,作为ImageViewer类的一个成员,这样可以维持ImageViewer的接口不变,通过简单的Adapter模式调用PicManager的功能。修改起来也很简单,很快就完成了。
       但后来一想这样还是要多维护一些代码,而且也不是很干净,于是再次进行重构。这次把ImageViewer作为基类,就只是简单的图片显示功能,每次调用它的loadImage(Qimage&)函数传进一个图像,它就会显示出来,缩放、翻转等功能也都还是封装在里面,但没有了任何文件相关的功能,彻底干净下来了。PicManager类则公开继承ImageViewer,负责管理文件,实现文件切换等,同时如果有动态图片则定时刷新。这样接口还是原来那样,除了这两个类别的几乎不用改,但却使得功能清晰下来了,代码也容易阅读了许多。
       另外为了更好地封装不同图片格式的区别,我引入了一个新的类ImageCache,里面记录了图片的一些信息。每次PicManager读取图片实际就是获得一个ImageCache对象,然后提取其QImage成员,调用ImageViewer类的接口显示。这个类的引入也使得下一个将要实现的功能——图片预读取——的实现变得容易了。
        总而言之,这次重构把原来的ImageViewer类功能进行了合理的区分,代码也整洁了许多。下一步还要继续重构MainWindow类,让整个程序更简单,更合理。

       体验新版Ez看图可以:点击这里


文档信息



2012年9月13日星期四

Ez看图更新了!

看了一暑假的《Effective C++》、《More Effective C++》,不动动手可不行。今天把今年年初写的EzViewer做了一下重构,去除一些无用的功能,新增了Windows下的文件格式关联,算个2.2版吧。还有很多想法没完成,今后还会慢慢改进的。项目地址:http://code.google.com/p/ezviewer/  ,可以用git check 出源码。  下载体验

下面是介绍:

EZ看图是一个简单的图片浏览器,使用Qt4编写,开源跨平台,国际化支持。 支持jpg, bmp, gif, png, jpeg, ico, svg, pbm, pgm, ppm, tif, tiff, xbm, xpm等多种图片格式。 特点有:
  • 采用统一绘图机制,可以旋转、镜像、缩放、拖动图片,包括动态gif。
  • 图片缩放显示时使用抗锯齿功能。
  • 图片缩放大于窗体时,鼠标左键快速拖动图片,释放鼠标后会依惯性滑动。
  • 可直接从资源管理器拖动一个或多个图片到看图器。
  • 动态gif可以暂停或步进。
  • 可自动隐藏的控制栏。
  • 自动播放/暂停功能。
  • 支持复制图片内容到剪贴板。
  • Windows下支持文件格式关联。 (New!)
  • 双击可以全屏。支持快捷键:
    1. 导航键上下左右、PageUp? / PageDown? 以及J/K:切换图片;
    2. O或N:打开图片对话框;
    3. Q或Esc:关闭图片;
    4. +/-:缩放图片;注意按住Ctrl时可以快速缩放,按住Shift时可以慢速缩放。
    5. 回车键:全屏显示;
    6. I:显示图片信息;
    7. D或Del:删除图片对话框;注意按住Ctrl时不会出现对话框,而是直接删除图片;
    8. Ctrl+C:复制图片内容到剪贴板;
    9. L/R:向左/右旋转90度;
    10. H/V:水平/竖直镜像图片;
    11. S:设置;
    12. 空格键/Pause:gif动画暂停/继续。
    13. F:gif动画暂停/步进。
    14. P:自动播放/暂停功能。
下图是同一张图片放大10倍时,不使用抗锯齿与使用抗锯齿的区别: 




文档信息


2012年9月10日星期一

Linux下时间计算与转换相关的类型和函数

最近忙于学院安排的实习,好久没有写博客了。。贴一下之前整理的Linux时间相关的东西,其中有些是来自C/C++标准库的:


Linux下日历时间相关的类型有:
    time_t               //算术类型
    struct tm           //分散时间结构
    struct timeval    //gettimeofday的结构


跟时间转换和计算相关的函数 (只写函数名,不是完整的原型。注释说明的是来自哪个头文件,以及相应的作用。下同):
    time                  //<time.h> ,  get time_t
    gettimeofday    //<sys/time.h> , get timeval
    localtime           //<time.h> , time_t --> tm
    gmtime             //<time.h> , time_t --> tm
    mktime             //<time.h> , tm --> time_t
    ctime                 //<time.h> , time_t --> string
    astime               //<time.h> , tm --> string
    strftime              //<time.h> , tm --> format string
    wcsftime            //<wchar.h> , tm --> format string
    getdate              //<time.h> , string --> tm
    strptime             //<time.h> , string --> tm

    difftime              //<time.h> , 计算time_t之差
    tzset                   //<time.h> , 设置时区



与程序执行时间相关的类型:
    clock_t               //以CLOCKS_PER_SEC或时钟滴答为单位
    struct timeval
    struct timespec   //<time.h>


测量时间间隔的函数:
    clock                   //<time.h> , 得到执行时间
    times                   //<sys/times.h> , 得到进程和子进程的执行时间


文档信息