QT 自定义QGraphicsItem 缩放后旋转 图形出现漂移问题
实现自定义QGraphicsItem缩放和旋转时,遇到了这样一个问题:将item旋转一个角度,然后拖拽放大,再次进行旋转时图像会发生漂移。原本以为是放大后中心点位置没有改变,导致旋转时以原中心的旋转出现了偏移,但是重新设置旋转中心 setTransformOriginPoint(rect.center()); 并没有起作用,图像仍然出现漂移。通过查阅相关问题发现是item旋转后item坐标系保持不变、原点保持不变,item重绘时先恢复未应用transform的坐标系然后再应用transform,为保证item坐标系相对item没有变化,坐标系会按照新的旋转中心进行旋转,从而导致图形发生漂移。
帮助文件中提到:
- 对QGraphicsItem进行变换不影响原点坐标 pos();
- 对QGraphicsItem进行变换不影响本地坐标系;
- 对QGraphicsItem进行变换顺序不同会导致最终结果不同;
可以看出QGraphicsItem旋转后,item的坐标系也跟着旋转。QGraphicsItem旋转后再缩放,坐标变换如下图所示:
当item放大后,pos() 在scene中的位置没有变,item的坐标系位置相对item也没有变化。当再次旋转时,item进行重绘,重绘坐标原点仍然是原来的pos()坐标。重绘时先绘制图形,然后应用该图形的transform。而rect.center()的坐标已经不再是缩放时的center,所以会发生图形漂移。如下示意图,灰色方块为旋转前的图形,黑色方框为旋转后的图形,绿色方框为缩放后的图形,绿色方块为重绘时,放大后图形应绘制的位置,重绘后旋转时会以该方块的中心进行旋转,而旋转后与原来的位置出现偏差。
参考文章QGraphicsItem旋转后,坐标变化机制解析,计算在原坐标系中TransformOriginPoint的实际位置与期望位置的位移,缩放后移动图形到新位置可以解决旋转漂移的问题。具体计算方法比较复杂,但是我感觉有更简单的处理方法可以解决这个问题。
解决思路:缩放后对pos()坐标进行更改,将新rect的中心点设置为新的pos(),这样当再次旋转触发重绘时,新rect的位置不会发生变化。此方法更简单,缩放后不用平移图形(缩放后再平移感觉就是手动漂移图形),而且不用自己去计算新的旋转中心。
建议:自定义QGraphicsItem时一定要将pos()设置为rect的中心点,即rect 的中心坐标为(0,0), 左topleft坐标为(-width/2, -height/2)
部分实现代码如下:
/**
* 本示例采用的是给rect添加了调整控件,此代码是调整控件中的代码
* from 为鼠标在scene上移动的起始位置
* to 为鼠标移动的结束位置
*/
void RectSelector::sizeAdjusterMove(const QPointF &from, const QPointF &to)
{
// 将坐标映射到item坐标系
QPointF itemFrom = parentItem()->mapFromScene(from);
QPointF itemTo = parentItem()->mapFromScene(to);
QPointF moveOffset = itemTo - itemFrom;
// 累计偏移量,图形缩放偏移小于1时不重绘
sizeOffsetTotal += moveOffset;
if(abs(sizeOffsetTotal.x()) < 1 && abs(sizeOffsetTotal.y()) < 1){
return;
}
moveOffset = sizeOffsetTotal;
AdjustPoint *point = (AdjustPoint *)sender();
QRectF offset(0,0,0,0);
// 判断是哪个控制点控制图形缩放,计算该控制点多图形的改变
QPointF centerOffset(0,0);
if (point->getId() == "topLeft") {
offset.setTopLeft(moveOffset);
centerOffset = moveOffset/2;
} else if(point->getId() == "topMid"){
offset.setTop(moveOffset.y());
centerOffset.setY(moveOffset.y()/2);
} else if(point->getId() == "topRight"){
offset.setTopRight(moveOffset);
centerOffset = moveOffset/2;
} else if(point->getId() == "left"){
offset.setLeft(moveOffset.x());
centerOffset.setX(moveOffset.x()/2);
} else if(point->getId() == "right"){
offset.setRight(moveOffset.x());
centerOffset.setX(moveOffset.x()/2);
} else if(point->getId() == "bottomLeft"){
offset.setBottomLeft(moveOffset);
centerOffset = moveOffset/2;
} else if(point->getId() == "bottomMid"){
offset.setBottom(moveOffset.y());
centerOffset.setY(moveOffset.y()/2);
} else if(point->getId() == "bottomRight"){
offset.setBottomRight(moveOffset);
centerOffset = moveOffset/2;
}
// 更新选中框大小
QRectF newRect = rect.adjusted(offset.left(), offset.top(), offset.right(), offset.bottom());
if (newRect.width() <= 0 || newRect.height() <= 0){
return;
}
refreshSelectRect(newRect);
// 计算原点在scene上移动的距离
QPointF src = parentItem()->mapToScene(0,0);
QPointF dst = parentItem()->mapToScene(centerOffset);
QPointF posOffset = dst - src;
QPointF oldPos = parentItem()->pos();
QPointF newPos = QPointF(oldPos.x() + posOffset.x(), oldPos.y() + posOffset.y());
// 调整被控图形的pos坐标,可以保证在有旋转角度时图形位置不会跳动
parentItem()->setPos(newPos);
// 发出大小改变信号
emit rectSizeChanged(offset);
// 清空累计信息
sizeOffsetTotal.setX(0);
sizeOffsetTotal.setY(0);
}
void RectSelector::refreshSelectRect(const QRectF &newRect)
{
prepareGeometryChange();
rect = newRect;
update();
// 重新定位调整点
setSizeAdjusterPos(rect);
setCornerAdjusterPos();
setRotateAdjusterPos(rect);
}
/**
* 角度旋转,from,to与sizeAdjusterMove相同
*/
void RectSelector::rotateAdjusterMove(const QPointF &from, const QPointF &to)
{
// 找到原点
QPointF origin = parentItem()->pos();
emit rectRotateChanged(QLineF(origin, to).angleTo(QLineF(origin, from)));
}
该代码可以解决图形漂移问题,但是由于缩放时在不断调整pos()位置,导致缩放时图形有一些闪烁,看起来不是很舒服。要解决闪烁的问题就需要在缩放时只记录pos()的偏移量,不更改pos(),在图形旋转时修改pos()到实际位置,并将偏移量置空。修改后的代码如下:
/**
* 本示例采用的是给rect添加了调整控件,此代码是调整控件中的代码
* from 为鼠标在scene上移动的起始位置
* to 为鼠标移动的结束位置
*/
void RectSelector::sizeAdjusterMove(const QPointF &from, const QPointF &to)
{
QPointF itemFrom = parentItem()->mapFromScene(from);
QPointF itemTo = parentItem()->mapFromScene(to);
QPointF moveOffset = itemTo - itemFrom;
// 累计偏移量
sizeOffsetTotal += moveOffset;
if(abs(sizeOffsetTotal.x()) < 1 && abs(sizeOffsetTotal.y()) < 1){
return;
}
moveOffset = sizeOffsetTotal;
AdjustPoint *point = (AdjustPoint *)sender();
QRectF offset(0,0,0,0);
if (point->getId() == "topLeft") {
offset.setTopLeft(moveOffset);
} else if(point->getId() == "topMid"){
offset.setTop(moveOffset.y());
} else if(point->getId() == "topRight"){
offset.setTopRight(moveOffset);
} else if(point->getId() == "left"){
offset.setLeft(moveOffset.x());
} else if(point->getId() == "right"){
offset.setRight(moveOffset.x());
} else if(point->getId() == "bottomLeft"){
offset.setBottomLeft(moveOffset);
} else if(point->getId() == "bottomMid"){
offset.setBottom(moveOffset.y());
} else if(point->getId() == "bottomRight"){
offset.setBottomRight(moveOffset);
}
// 更新选中框大小
QRectF newRect = rect.adjusted(offset.left(), offset.top(), offset.right(), offset.bottom());
if (newRect.width() <= 0 || newRect.height() <= 0){
return;
}
refreshSelectRect(newRect);
// 发出大小改变信号
emit rectSizeChanged(offset);
// 清空累计信息
sizeOffsetTotal.setX(0);
sizeOffsetTotal.setY(0);
}
/**
* 创建矩形选择控制框
*/
void RectItem::createSelectRect()
{
selector = new RectSelector(getPaintRect(), borderWidth, this);
connect(selector, SIGNAL(rectSizeChanged(QRectF)), this, SLOT(sizeChanged(QRectF)));
if (rounded){
selector->showCornerAdjuster(true);
selector->setRadius(arcSize);
connect(selector, SIGNAL(rectCornerChanged(qreal)), this, SLOT(cornerChanged(qreal)));
}
connect(selector, SIGNAL(rectRotateChanged(qreal)), this, SLOT(rotateChanged(qreal)));
}
/**
* 在自定义矩形大小改变时记录新中心点的偏移量
* @param offsetValue 矩形4个边的偏移量
*/
void RectItem::sizeChanged(QRectF offsetValue)
{
// 通知 scene item 大小即将改变
prepareGeometryChange();
rect.adjust(offsetValue.left(), offsetValue.top(),
offsetValue.right(), offsetValue.bottom());
centerOffset = mapToParent(rect.center()) - pos();
}
/**
* 重新设定矩形位置
*/
void RectItem::redefineRect()
{
// 定义矩形位置,保持(0,0) 为矩形中心位置
QPointF pt(rect.width()/2, rect.height()/2);
rect.setTopLeft(-pt);
rect.setBottomRight(pt);
// 更新控制点位置
if(!selector.isNull()){
selector->refreshSelectRect(rect);
}
}
/**
* 响应矩形旋转事件
* @param offsetValue 角度偏移量
*/
void RectItem::rotateChanged(qreal offsetValue)
{
qreal oldAngle = rotation();
qreal angle = oldAngle + offsetValue;
if (angle > 360){
angle -= 360;
}
if (angle < -360){
angle += 360;
}
prepareGeometryChange();
// 发现中心偏移时,调整中心位置
if(centerOffset.x() != 0 || centerOffset.y() != 0){
// 调整中心点位置
setPos(centerOffset + pos());
// 调整矩形位置
redefineRect();
// 偏移量置空
centerOffset.setX(0);
centerOffset.setY(0);
}
setRotation(angle);
}
参考文章:
Qt中QTransform的translate和rotate实现过程
QGraphicsRectItem美观实现缩放,旋转,平移
QGraphicsItem鼠标拖动旋转(五)
QGraphicsItem旋转后,坐标变化机制解析
Qt:QGraphicsItem对象setPos(),setScale(),setRotation()操作后Item坐标和Scene坐标的变化