本文是《用 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<QGraphicsTransform *> &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 的矩阵,这里的任何二维变换都可以用这三个矩阵的乘积来表示。(参考 《场景视图中二维变换矩阵的计算》)
S(Scale)表示缩放矩阵
R(Rotate)表示旋转矩阵
T(Translate)表示平移矩阵
3个有序变换(S、R、T)可以任意组合,但是要保持相对顺序,理论上共有8种组合。常见的组合有:
- R * T变换:物体只有位置变化,没有形状变化
在有些情况下,需要能够抵消父节点(QGraphicsItem)的一部分变换。这就需要引入逆变换,即以上三个矩阵的逆矩阵 、、。因为变换离父节点更近,所以都应该安排在普通变换的后面,而且三种次序也是反过来的。
综合起来,一个节点的变换有下列变换矩阵组成:
与普通变换一样,逆变换也共有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。