教你做可拖动的Widget

iPhone拥有众多让各路厂商艳羡的feature, 大家都卯着劲向它靠拢, 甚至觉得不follow iPhone的风格就OUT了。 迎合潮流当然没错, 不过想崭露头角光靠迎合是不够的… iPhone提供了支持多点触摸的屏幕, 这成为其酷炫效果的一个基础, 用户可以用手指在屏幕上滑动的方式操纵程序, 比如可以把图标拖来拖去、可以用手指头拖动浏览器的内容等。 很多人已经开始对此上瘾了,呵呵。

闲话两句家常下面进入本文的正题 -- 硬件再有趣也得有相应的软件才能有用武之地 -- 今天就介绍个用Qt实现类似的拖动效果的例子。 我们没有支持多点触摸的设备这没有关系, 就在桌面上用鼠标拖动一下看看效果就好了。

本例子的原文发表于Qt Labs Blog, 作者是大牛ariya, 原文在

http://labs.trolltech.com/blogs/2008/11/15/flick-list-or-kinetic-scrolling/

在ariya最新的一篇blog里提到了这篇古老的博文引起了笔者的注意, 遂决定今天把它简单分析一下。

该例子在dojo项目的flickcharm目录下,代码在git服务器上, 下载命令是:
git clone git://gitorious.org/qt-labs/graphics-dojo.git

该例子的功能是定义了一个FlickCharm类, 该类可以给任何从QScrollArea类和QWebView类派生的窗体类添加用鼠标拖动内容的效果。 其核心是利用Qt的eventFilter, 首先把FlickCharm注册为窗体类的eventFilter对象, 这样FlickCharm可以优先获得窗体类的鼠标事件, 对鼠标事件进行处理并渲染拖动的动画效果。 整个例子不过区区三百行的代码(包括注释空行等), 堪称非常精炼, 功能又丝毫不逊色, 很值得一读。

Section 1

按照一般阅读源码的顺序, 先看main函数:

main.cpp里有三段主要的代码
1、构造QGraphicsView显示一列文字
代码比较占地方,这里不贴了, 唯一提一点, 这里给graphicsview加了足够多的item, 使之有足够的内容供拖动。

2、构造一个QWebView显示网页内容
打开了一个内容丰富的页面, 保证有足够的内容供拖动。

3、将QGraphicsView和QWebView注册到FlickCharm

    FlickCharm FlickCharm;
FlickCharm.activateOn(&canvas);
FlickCharm.activateOn(&web);

Section 2

下一步当然是追踪到FlickCharm的实现文件看看activateOn函数做了些什么。
flickcharm.cpp
FlickCharm::activateOn函数里针对QAbstractScrollArea的派生类和QWebView的派生类做了不同的操作, 但两者几乎一模一样, 就以QWebView为例:

void FlickCharm::activateOn(QWidget *widget)
{
//...QAbstractScrollArea code...
// QWebView code:
QWebView *webView = dynamic_cast(widget);
if (webView) {
QWebFrame *frame = webView->page()->mainFrame();
frame->setScrollBarPolicy(Qt::Vertical, Qt::ScrollBarAlwaysOff);
frame->setScrollBarPolicy(Qt::Horizontal, Qt::ScrollBarAlwaysOff);

webView->installEventFilter(this);

d->flickData.remove(webView);
d->flickData[webView] = new FlickData;
d->flickData[webView]->widget = webView;
d->flickData[webView]->state = FlickData::Steady;
return;
}
}

这个函数比较简单, 第一步先检查widget是不是QWebView的派生类, 如果是将QWebView的水平方向和垂直方向的滚动条都关掉; 第二步把FlickCharm注册为QWebView的eventFilter对象; 第三步将webview加到数据表里。 由于每个FlickCharm可以监控多个窗体, 所以定义了一个数据表flickData, 这个数据表是个哈西表, 该表存储widget指针和与拖动相关的一些属性。

Section 3

注册了eventFilter, 下面就是在FlickCharm::eventFilter函数里等待鼠标事件的到来。(如果对此有疑问, 去看前面的blog, Qt的Event Filter 扫扫盲)。 我们来看看eventFilter函数的实现, 该函数是针对拖动的不同状态处理鼠标事件。 对于拖动的状态阶段, 作者定义了一个类似状态机的东西, 用几个枚举值表示不同的状态。 这部分的代码比较多, 不能都贴上, 下面是代码的架构和笔者的理解, 和源码对照来看比较好。 说实话这部分代码有点玄妙, 分支很多, 但有些分支完全没有用到。 囧
笔者这里只按照自己的理解注释有用的代码。。如果注释有错误请谅解。

bool FlickCharm::eventFilter(QObject *object, QEvent *event)
{
获得鼠标事件后先检查一下是不是注册的widget, 如果是就查找它对应的flick数据data。
switch (data->state) {
case FlickData::Steady:
初始状态Steady表示空闲的等待状态, 在这种状态下用户如果按下鼠标就进入Pressed状态,
此时需要记录滚动条的初始位置。
break;
case FlickData::Pressed:
如果此时用户抬起鼠标, 表示用户没有拖动的动作;
如果用户拖动鼠标, 转换为ManualScroll状态并开始动画效果的定时器。
break;
case FlickData::ManualScroll:
//鼠标移动时计算移动的量并用setScrollOffset设置:
if (mouseEvent->type() == QEvent::MouseMove) {
consumed = true;
QPoint delta = mouseEvent->pos() - data->pressPos;
setScrollOffset(data->widget, data->offset - delta);
}
鼠标抬起时进入AutoScroll状态。
break;
}
return consumed;//如果鼠标事件被FlickCharm处理, 就返回true;
//否则返回false,该事件还会继续被分发。
}

Section 4

eventFilter的代码提到了另一个核心代码就是具体的内容变化是如何实现的, 如果笔者说出来大家可能要大跌眼镜了, 因为这里没有任何的magic, 只是简单的调用了widget类的滚动条本身提供方法, 具体代码在setScrollOffset函数中。

static void setScrollOffset(QWidget *widget, const QPoint &p)
{
QAbstractScrollArea *scrollArea = dynamic_cast(widget);
if (scrollArea) {
scrollArea->horizontalScrollBar()->setValue(p.x());
scrollArea->verticalScrollBar()->setValue(p.y());
}

QWebView *webView = dynamic_cast(widget);
QWebFrame *frame = webView ? webView->page()->mainFrame() : 0;
if (frame)
frame->evaluateJavaScript(QString("window.scrollTo(%1,%2);").arg(p.x()).arg(p.y()));
}

Section 5

代码当中还有个让人困惑的定时器, 实际上去掉这个定时器对拖动的效果并没有什么影响。 不知道在真正的多点触摸硬件上会不会有不同。

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页