4.3 Qt Graphics 场景中的交互逻辑

本文介绍了在Qt中实现电子白板时,如何处理控件选择和空间变换编辑。控件选择涉及命中测试逻辑,包括阻止、命中和透过三种情况。空间变换通过全局QGraphicsItem代理实现,以统一编辑操作并简化简单控件的处理。此外,文章还详细讨论了鼠标、触摸和滚轮事件的处理,以及在不同设备上的交互考虑。
摘要由CSDN通过智能技术生成

        本文是《用 Qt 实现电子白板》的其中一节,建议全章阅读。


        在电子白板中,针对控件的选择、二维空间变换编辑操作是最基本的交互逻辑。所谓空间变换编辑操作,是对控件进行平移、缩放、旋转,所依赖的设备主要有鼠标、触摸,以及部分键盘按键。

        QGraphicsItem 提供了一些的选择、平移的功能,比如 ItemIsSelectable、ItemIsMovable,但是不能完全满足我们的需求,所以需要我们自己实现所有功能。

控件选择

        给定一个点,这个点命中哪个控件?问题的解答并不是那么简单。

        现实情况是,这个点可能会命中多个控件(因为控件层叠),当然可以规定命中最上面一个。然而,有可能这个点命中了控件的透明部分(比如不规则几何图形的外围),那就需要跳过这一层,也有可能需要根据控件的状态来判断(比如画笔控件,在书写打开、关闭模式下的不同的行为)。

        作为一个完整的方案,还需要考虑更多因素、细节。我们的目标是让控件选择逻辑与控件自己的编辑交互(如点击绘图框是为了绘制笔迹),能够有机的融合在一起。

        所以,我们让控件自己执行命中测试,测试返回有三种可能:

  • 【阻止】,则跳出处理,未选中如何控件
  • 【命中】,则选中该控件
  • 【透过】,则继续测试下一个控件

         大概的测试流程是这样的:

        一开始,上层的控件都返回【透过】,继续测试,直到有一个控件返回不是【透过】。如果返回的是【命中】,那么就选择该控件,如果返回【阻止】,意味着控件自己想处理该操作,此时也是停止命中测试,但是不选择任何控件。

        这里需要用到 QGraphicsScene::items(QPointF) 方法,它返回一个点下面的所有 QGraphicsItem,这些 QGraphicsItem 的形状(shape)包含该点。一个 QGraphicsItem 的形状可以是不规则图形(可以看到它是用 QPainterPath 表示的)。

        另外,在调用控件的命中测试时,需要将点坐标转换到该控件的相对坐标系中,使用 QGraphicsItem::mapToItem 完成。

this->mapToItem(control->item(), pos)

         mapToItem 还有个简写方法 mapToParent,以及反向操作 mapFromItem。

空间变换

        一般针对控件的空间变换编辑是由控件自己处理的(就是 QGraphicsItem 自己实现的那样),但是我们提出了另一个思路,就是由一个全局的 QGraphicsItem 代理所有控件的空间变换编辑。这种方案有下列优势:

  • 统一了编辑框(如下图)的操作管理,编辑框有 8 个可能的拖拽句柄
  • 对一些简单的控件来说,完全不需要操心处理鼠标、触摸事件
  • 输入的坐标点是相对于一个固定的坐标系,不会因为控件移动而导致坐标系跟着移动

         其中第三点比较关键。可能有些人在处理视图平移时,会发现视图会不听话的来回抖动,那可能就是使用了相对控件自身的坐标值。

        有时候,还需要在更高层的坐标系中处理位置编辑,这时我们会使用场景的坐标(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();
    }
}

按键事件

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Fighting Horse

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值