在使用QGraphicsView架构制作流程图类的界面时,需要用带箭头的连线将两个item连接起来,来表明连接关系,本文介绍了箭头连接类的方法和相关问题
一.实现方法
架构自身为我们提供了QGraphicsItem的衍生子类QGraphicsLineItem,构造时传入两个点即可生成:
QGraphicsLineItem *line = new QGraphicsLineItem(x1, y1, x2, y2);
将line添加到scene中即可显示连线,但这样做有几个缺点:
- 它只是一根线,而我们想要的是带有箭头的线
- 传入的位置坐标已经固定,如果我们的item改变位置,连线不会改变,不具有灵活性
我的解决方法是在构造时绑定两个item,在paint()函数中实时获得它们的坐标作为线的起点和终点
class Arrow : public QGraphicsLineItem {
public:
Arrow(QGraphicsItem *startItem, QGraphicsItem *endItem);
private:
QGraphicsItem *m_startItem;
QGraphicsItem *m_endItem;
核心部分为重写的paint函数,需要获取最新的起点和终点坐标,并且根据这两个坐标补出两侧箭头的坐标
void Arrow::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) {
Q_UNUSED(option);
Q_UNUSED(widget);
// 设置画笔
QPen pen(Qt::black); // 可以根据需要修改颜色
painter->setPen(pen);
// 获取起始和结束点
QPointF startPoint = m_startItem->sceneBoundingRect().center();
QPointF endPoint = m_endItem->sceneBoundingRect().center();
// 绘制线条
painter->drawLine(startPoint, endPoint);
//不加入可能会影响刷新
setLine(startPoint,endPoint);
// 计算箭头头部的位置和方向
double angle = std::atan2(endPoint.y() - startPoint.y(), endPoint.x() - startPoint.x());
QPointF arrowHead1 = endPoint - QPointF(sin(angle + M_PI / 6) * 10, cos(angle + M_PI / 6) * 10);
QPointF arrowHead2 = endPoint - QPointF(sin(angle - M_PI / 6) * 10, cos(angle - M_PI / 6) * 10);
// 绘制箭头头部
painter->drawLine(endPoint, arrowHead1);
painter->drawLine(endPoint, arrowHead2);
}
在获得起点和终点时,可以在item里设置一个函数专门返回连接点的坐标,这点根据需求灵活改变。
这个类一共有三条线,主体直线一条,两侧的箭头各一条,共需要四个点。主要难点部分在计算主线段两侧的箭头点,我在项目中使用的是下面的计算方法:
//起点坐标(x1,y1) ,终点坐标(x2,y2)
double length=10; //箭头长度
double angle=0.5; //箭头与主线段的角度
float x3=x2-length*cos(atan2((y2-y1),(x2-x1))-angle);
float y3=y2-length*sin(atan2((y2-y1),(x2-x1))-angle);
float x4=x2-length*sin(atan2((x2-x1),(y2-y1))-angle);
float y4=y2-length*cos(atan2((x2-x1),(y2-y1))-angle);
painter->drawLine(x2,y2,x3,y3);
painter->drawLine(x2,y2,x4,y4);
完成以上步骤后,我们得到一个箭头类,它可以被添加到scene中并显示出来,但这其实这是第一步,后面有很多的细节需要优化。
二.相关问题及解决方法
1.连线刷新时有残留的像素
在拖动item让连线刷新的时候,会发现拖动时会有干扰的像素存在scene中,而当拖动放开的时候,那些残余的像素的又会消失,十分影响体验。这其实是因为boundingRect没有重新实现,需要在其中向外扩充一点。其实其他的自定义item也可能会有这个问题,解决方法也是向外扩充一点边界,要比显示的范围大。
2.选中范围太小,只有恰好点击在线上才会被选中
除非线的宽度设置的很粗,否则在实际使用时不可能完全点到线,我们希望的是点击到线的周围就可以触发选中。解决方法是重写函数shape(),其实和boundingRect函数一样,在自定义item中都需要我们去重写的,只不过不写也不会报错,于是便留下了很多问题。
问题1和2有大佬写文章解释了其中的原因,我也是参考后才得以解决的,大家可以参考下,里面有具体的重写代码:
3.切换scene后,连线不显示
在一个scene中可以选择想要的流程图进行展示,可是当我切换到另一个是,其他的item都会显示出来,只有连线类没有显示。在paint函数中调用debug后,发现根本没有调用paint函数,类是创建出来了,但是核心的代码没有被调用,可不是没有显示。一番折腾后,在下面这篇博客找到了答案:
QGraphicsView放大时,paint有时不被调用,导致图像绘制不出来(2)
在创建完arrow后调用prepareGeometryChange,后然后在调用upDate。即可刷新出来
4.改变scene位置、缩放后,连线不显示
我在第一个流程图拖拽scene的位置,或者进行缩放后,再点击第二个流程图就会不显示连线,下面这篇文章给了我启发:
QGraphicsView放大时,paint有时不被调用,导致图像绘制不出来(1)
于是我再每次更换流程图时都重新设置scene的rect和缩放比例,虽然不清楚原因,但是问题解决了
附上view怎样重置缩放比例:
获取初始的缩放因子:
在进行任何缩放操作之前,你可以记录下 QGraphicsView 的初始缩放因子。这可以通过获取视图的初始变换矩阵来完成。
QTransform initialTransform = graphicsView->transform();
重置缩放因子:
如果希望在之后的某个时刻将视图的缩放恢复到初始状态,可以将视图的变换矩阵设置为初始变换矩阵。
graphicsView->setTransform(initialTransform);