4.4 Qt Graphics 场景中的二维空间变换

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


        场景中的二维空间变换是针对控件的平移、缩放、旋转变换。

        在上一级我们介绍过场景中的交互逻辑,其中一大块就是操作空间变换,那些是空间变换的输入。另外,我们在《场景视图中二维空间变换矩阵的计算》中介绍了变换矩阵的计算方法。本文的重点在于对二维变换的管理。

二维空间变换API

        在 Qt Graphics 中,二维变换相关的 api 有(QGraphicsItem):

void setPos(const QPointF &pos);
void setRotation(qreal angle);
void setScale(qreal scale);
void setTransformOriginPoint(const QPointF &origin);
void setTransform(const QTransform &matrix, bool combine = false);
void setTransformations(const QList &transformations);

        按作用的先后顺序是:

  • Transform
  • Transformations
  • Rotation(TransformOriginPoint)
  • Scale(TransformOriginPoint)
  • Pos

        本身这些功能是有冗余的,比如 Transform 完全可以用 Transformations 增加一项代替。而 Rotation、Scale、Pos 则是为了方便使用,虽然实现与 Transformations 并不统一,但也是可以替代的。

        在电子白板中,没有使用 Rotation、Scale、Pos。这是为了降低二维变换的处理复杂度。而使用 Transform 仅仅是为了将控件的中心点变换到 {0,0} 位置。所有平移、缩放、旋转都是通过 Transformations 实现的。

        Transformations 表示的其实是一组有序的 QGraphicsTransform。在 QGraphicsTransform 内部使用 QMatrix4x4 这个 4x4 的矩阵表示变换,但是我们只使用了二维变换的能力,可以看成 3x3 的矩阵处理。

空间变换分解

        二维变换分解下来有平移、旋转、缩放三个部分,每部分都是一个 3x3 的矩阵,这里的任何二维变换都可以用这三个矩阵的乘积来表示。(参考 《场景视图中二维变换矩阵的计算》)

        A = S * R * T

        S(Scale)表示缩放矩阵

        R(Rotate)表示旋转矩阵

        T(Translate)表示平移矩阵

        3个有序变换(SRT)可以任意组合,但是要保持相对顺序,理论上共有8种组合。常见的组合有:

  • R * T变换物体只有位置变化,没有形状变化

        在有些情况下,需要能够抵消父节点(QGraphicsItem)的一部分变换。这就需要引入逆变换,即以上三个矩阵的逆矩阵 S^{-1}R^{-1}T^{-1}。因为变换离父节点更近,所以都应该安排在普通变换的后面,而且三种次序也是反过来的。

        综合起来,一个节点的变换有下列变换矩阵组成:A = S * R * T * T^{-1} * R^{-1} * S^{-1}

        与普通变换一样,逆变换也共有8种组合。常见的逆变换组合有:

  • S’:        主物体在变换时,子物体没有形变
  • T’R’ :    主物体在变换时,子物体没有位置变化
  • T’R’S’:  主物体在变换时,子物体相对主物体固定

         下面的代码在 ControlTransform 类(继承 QGraphicsTransform )中实现了 QGraphicsTransform 的 applyTo  方法,switch 分支对应不同的变换组合(共 16 个,普通变换、逆变换不会同时存在于一个 QGraphicsTransform  对象中)。

void ControlTransform::applyTo(QMatrix4x4 *matrix) const
{
    switch (type_) {
    case Identity:
        break;
    case Translate:
        *matrix = matrix->toTransform() * transform_->translate();
        break;
    case Rotate:
        *matrix = matrix->toTransform() * transform_->rotate();
        break;
    case RotateTranslate: // Frame
        *matrix = matrix->toTransform() * transform_->rotateTranslate();
        break;
    case Scale: // FrameItem
        *matrix = matrix->toTransform() * transform_->scale();
        break;
    case ScaleTranslate:
        *matrix = matrix->toTransform() * (transform_->scale() * transform_->translate());
        break;
    case ScaleRotate:
        *matrix = matrix->toTransform() * transform_->scaleRotate();
        break;
    case ScaleRotateTranslate: // PureItem
        *matrix = matrix->toTransform() * transform_->transform();
        break;
    case NoInvert:
         break;
    case InvertTranslate:
        *matrix = matrix->toTransform() * transform_->translate().inverted();
        break;
    case InvertRotate:
        *matrix = matrix->toTransform() * transform_->rotate().inverted();
        break;
    case InvertRotateTranslate:
        *matrix = matrix->toTransform() * transform_->rotateTranslate().inverted();
        break;
    case InvertScale:
        *matrix = matrix->toTransform() * transform_->scale().inverted();
        break;
    case InvertScaleTranslate:
        *matrix = matrix->toTransform() * (transform_->scale() * transform_->translate()).inverted();
        break;
    case InvertScaleRotate:
        *matrix = matrix->toTransform() * transform_->scaleRotate().inverted();
        break;
    case InvertScaleRotateTranslate:
        *matrix = matrix->toTransform() * transform_->transform().inverted();
        break;
    }
}

        这里的 ControlTransform 实现依赖空间变化计算类 ResourceTransform,多个 ControlTransform 可能共享同一个 ResourceTransform,一旦 ResourceTransform 有变化,ControlTransform 会通知节点触发更新。

ControlTransform::ControlTransform(ResourceTransform const & transform, ControlTransform::Type type)
    : transform_(&transform)
    , type_(type)
{
    QObject::connect(transform_, &ResourceTransform::changed,
                     this, &ControlTransform::update);
}

         一个节点 (QGraphicsItem)中可能有多个 ControlTransform 。

空间变换的使用

节点

相对节点

(父节点)

变换组合 备注
画布 场景 缩放、平移
控件 画布 缩放、旋转、平移

        

控件 外边框 缩放
外边框 画布 旋转、平移

保持相对布局一致

替代控件的旋转、平移

选择框 画布 旋转、平移

边框不会缩放,保持各元素大小相对固定

同步控件的旋转、平移 

上下文菜单 画布 平移;逆缩放 保持全局大小一致
上下文菜单 场景 平移
状态展示 控件

平移;

(逆旋转),逆缩放

相对画布不旋转

相对画布大小固定

         该表描述了电子白板中的各个元素的空间变换方式。

         在相对于父节点的逆变换中,会共享父节点的 ResourceTransform,所以能够做到与父节点同步变化,起到对抗父节点空间变换的效果。

        另一个共享 ResourceTransform 的例子是在“选择框”中,为了与控件的旋转、平移保持一致,选择框动态绑定当前选中控件的 ResourceTransform。

        

你可能感兴趣的:(Qt,框架性开发实践,qt)