赞
踩
本文是《用 Qt 实现电子白板》的其中一节,建议全章阅读。
在电子白板中,针对控件的选择、二维空间变换编辑操作是最基本的交互逻辑。所谓空间变换编辑操作,是对控件进行平移、缩放、旋转,所依赖的设备主要有鼠标、触摸,以及部分键盘按键。
QGraphicsItem 提供了一些的选择、平移的功能,比如 ItemIsSelectable、ItemIsMovable,但是不能完全满足我们的需求,所以需要我们自己实现所有功能。
给定一个点,这个点命中哪个控件?问题的解答并不是那么简单。
现实情况是,这个点可能会命中多个控件(因为控件层叠),当然可以规定命中最上面一个。然而,有可能这个点命中了控件的透明部分(比如不规则几何图形的外围),那就需要跳过这一层,也有可能需要根据控件的状态来判断(比如画笔控件,在书写打开、关闭模式下的不同的行为)。
作为一个完整的方案,还需要考虑更多因素、细节。我们的目标是让控件选择逻辑与控件自己的编辑交互(如点击绘图框是为了绘制笔迹),能够有机的融合在一起。
所以,我们让控件自己执行命中测试,测试返回有三种可能:
大概的测试流程是这样的:
一开始,上层的控件都返回【透过】,继续测试,直到有一个控件返回不是【透过】。如果返回的是【命中】,那么就选择该控件,如果返回【阻止】,意味着控件自己想处理该操作,此时也是停止命中测试,但是不选择任何控件。
这里需要用到 QGraphicsScene::items(QPointF) 方法,它返回一个点下面的所有 QGraphicsItem,这些 QGraphicsItem 的形状(shape)包含该点。一个 QGraphicsItem 的形状可以是不规则图形(可以看到它是用 QPainterPath 表示的)。
另外,在调用控件的命中测试时,需要将点坐标转换到该控件的相对坐标系中,使用 QGraphicsItem::mapToItem 完成。
this->mapToItem(control->item(), pos)
mapToItem 还有个简写方法 mapToParent,以及反向操作 mapFromItem。
一般针对控件的空间变换编辑是由控件自己处理的(就是 QGraphicsItem 自己实现的那样),但是我们提出了另一个思路,就是由一个全局的 QGraphicsItem 代理所有控件的空间变换编辑。这种方案有下列优势:
其中第三点比较关键。可能有些人在处理视图平移时,会发现视图会不听话的来回抖动,那可能就是使用了相对控件自身的坐标值。
有时候,还需要在更高层的坐标系中处理位置编辑,这时我们会使用场景的坐标(scenePos)作为输入。
位置编辑本质上是在操作 的二维变换矩阵,我们在下一节会做详细介绍。
在 QGraphicsItem 中,需要明确声明接收鼠标事件,QGraphicsScene 才能给它分发鼠标事件(如果收不到鼠标事件,一般就是这个原因):
setAcceptedMouseButtons(Qt::LeftButton);
处理鼠标事件通常需要实现下面三个方法:
- void ItemSelector::mousePressEvent(QGraphicsSceneMouseEvent *event)
- void ItemSelector::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
- void ItemSelector::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
鼠标事件中,有三个坐标值:pos,scenePos,screenPos,分别是相对于当前 QGraphicsItem,场景 QGraphicsScene 和 屏幕的位置。可以根据需要选择使用。
这里需要注意的是,event 默认已经 accept 了,不做任何处理,事件不会再分发给其他(层级靠下面的)控件了,所以当没有选中任何控件时,需要重置 accepted 标记,可以用下面两种方法:直接调用事件的 ignore() 方法,或者转发给父类处理。最终的父类 QGraphicsItem 默认处理还是调用 ignore() 方法。
- event->ignore()
- 或者
- SuperClass::mousePressEvent(event);
在触摸输入的情况下,也会收到鼠标事件,这是因为 Qt 会用没有处理的触摸事件,合成(转化为)鼠标事件。另外,在 Windows 系统中,应用会先后从系统收到触摸、鼠标事件,Qt 也原样转发,但是不再自己合成鼠标事件。
总的来说,我们需要针对合成的鼠标事件做处理,大部分情况下,我们判断是否还在处理触摸事件的过程中,如果是,就忽略鼠标事件。
- void ItemSelector::mousePressEvent(QGraphicsSceneMouseEvent *event)
- {
- #if ENBALE_TOUCH
- if (!touchPositions_.empty()) {
- return;
- }
- #endif
- ......
- }
当然,也可以显式判断鼠标事件是否是合成的,比如:
- if (event->source() != Qt::MouseEventNotSynthesized) {
- QGraphicsItem::mousePressEvent(event);
- return;
- }
在 QGraphicsItem 中,需要明确声明接收触摸事件,QGraphicsScene 才能给它分发触摸事件:
setAcceptTouchEvents(true);
处理触摸事件没有各种子方法,统一在 sceneEvent 方法中处理:
- bool ItemSelector::sceneEvent(QEvent *event)
- {
- switch (event->type()) {
- case QEvent::TouchBegin:
- touchBegin(static_cast<QTouchEvent*>(event));
- break;
- case QEvent::TouchUpdate:
- touchUpdate(static_cast<QTouchEvent*>(event));
- break;
- case QEvent::TouchEnd:
- touchEnd(static_cast<QTouchEvent*>(event));
- break;
- }
- }
触摸事件支持多指触摸,所有事件中包含 TouchPoint 数组。每个 TouchPoint 被分配了一个唯一的 id,还有四个坐标值:pos,scenePos,screenPos,normalizedPos() ,每种坐标还有 startXXXPos,lastXXXPos 表示触摸开始的位置和是一个事件的位置。
一般情况下,我们需要跟踪每个手指的滑动轨迹,已经手指个数变化的情况,所以使用一个 map(以 TouchPoint 的 id 作为 key)来保存手指的位置(并没有使用 lastXXXPos)。
在做控件位置编辑时,两个手指作为手势操作处理,其他情况只作为平移操作处理(只考虑第一个手指的位置)。两种情况都需要确保在上一个事件也有同样的 TouchPoint id,只有对边前后坐标值才能计算位置变化。
除了触摸屏,笔记本的触摸板(TouchPad)也是触摸事件的来源。在 Windows 中,触摸板一般发出的是鼠标事件,苹果笔记本(MacBook)则是触摸事件。
但是在 MacBook 中处理触摸事件,是比较麻烦的。它的 TouchPad 可以改变鼠标位置,手指按下去的触摸事件中的位置就是鼠标位置,但是接下来的触摸事件中的位置就与鼠标位置不同步了。这部分需要继续研究,有结果后再来更新。
滚轮也可以用来平移、缩放控件。实现 wheelEvent 处理滚轮事件。
void ItemSelector::wheelEvent(QGraphicsSceneWheelEvent *event)
与鼠标事件一样,滚轮事件也有坐标位置(也是鼠标位置),但还有另外一个数值 delta,表示滚动的距离(一般是固定值,可正负,系统设置里面可以修改)。
使用坐标位置,可以测试命中的控件。
可以结合按键(Ctrl、Shift)来切换滚轮的操作效果。正常作为平移操作处理,按住 Ctrl 时,作为缩放操作处理,按住 Shift 可以切换平移的方向(从上下切换为左右)。为了方便处理,Qt 已经在滚轮事件的 modifiers() 方法中返回这些按键状态了。
贴一段处理滚轮的相对完整的代码:
- void ItemSelector::wheelEvent(QGraphicsSceneWheelEvent *event)
- {
- selectAt(mapFromItem(currentEventSource_, event->pos()), event->scenePos(), Wheel);
- if (tempControl_) {
- if (event->modifiers().testFlag(Qt::KeyboardModifier::ControlModifier)) {
- qreal delta = event->delta() > 0 ? 1.2 : 1.0 / 1.2;
- tempControl_->scale(tempControl_->item()->mapFromScene(event->scenePos()), delta);
- } else {
- QPointF d;
- if (event->modifiers().testFlag(Qt::KeyboardModifier::ShiftModifier)) {
- d.setX(event->delta());
- } else {
- d.setY(event->delta());
- }
- tempControl_->move(d);
- }
- selectRelease(Wheel);
- } else {
- event->ignore();
- }
- }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。