QT 中的 Graphics View 系统. 是一个相对成熟的渲染引擎的上层框架,通常也可以会叫做 Scene - View。
通常会有 QGraphicsView, QGraphicsScene, QGraphicsItem 这几个类构成。
view是视口(viewport);scene是一个场景,负责容纳各种item;而item就是可见的这些元件。
一般来说,绘图可以使用 QPainter直接在重绘事件中进行绘制,但是,当我们想要选择绘制的图形的时候,就犯难了。我们的painter是直接在屏幕上写写画画,没有人来管理,在当前的mouse事件中也不知道如何处理这些项。
这个时候,Graphics View 就解决了这个问题,通过scene来管理各种图元item项。item在scene上绘制,scene在view上显示。
本文,就是利用Graphics View 系统来实现了一个简单的 有向图/无向图 编辑器。
编辑的图输出效果如下:
绘制点和绘制线是一个图元,那么就是一个 QGraphicsItem,继承自 QGraphicsItem,然后去重写绘制方法
在绘制点和线的时候,需要重写QGraphicsItem的绘制函数,也就是
paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
如何绘制点
graphNode
类的设计:
classgraphNode:publicQObject,publicQGraphicsItem{
Q_OBJECT
public:graphNode(QPointF point,int r =10, QString str ="0");// QGraphicsItem interfacepublic:
QRectF boundingRect()constoverride;voidpaint(QPainter *painter,const QStyleOptionGraphicsItem *option, QWidget *widget)override;
QPointF getPoint()const;intgetR()const;voidsetR(int newR);const QString &getText()const;voidsetText(const QString &newText);const QColor &getFrontColor()const;voidsetFrontColor(const QColor &newFrontColor);const QColor &getBackColor()const;voidsetBackColor(const QColor &newBackColor);intgetRoundWidth()const;voidsetRoundWidth(int newRoundWidth);private:
QPointF point;// 绘制的初始点int r;// 半径
QString text;// 点的文字
QColor frontColor;// 前景色Ⅰ
QColor backColor;// 背景色Ⅰint roundWidth;// 圆的宽Ⅰ};
在这个类中,我自定义了一些属性,方便配置点的颜色,大小等等。
核心还是在于paint函数,其余都是辅助功能
下面是paint函数的实现:
void graphNode::paint(QPainter *painter,const QStyleOptionGraphicsItem *option, QWidget *widget){
QPen pen;
pen.setWidth(roundWidth);
painter->setRenderHint(QPainter::HighQualityAntialiasing);if(option->state & QStyle::State_Selected)
pen.setColor(QColor((frontColor.red()+125)%255,(frontColor.green())%255,(frontColor.blue()+125)%255));// 选中时颜色变化else pen.setColor(frontColor);
painter->setPen(pen);
painter->drawEllipse(QRectF(point.x()- r, point.y()- r, r *2, r *2));
QPainterPath path;
path.addEllipse(QRectF(point.x()- r, point.y()- r, r *2, r *2));
painter->fillPath(path,QBrush(backColor));
painter->drawText(boundingRect(),
Qt::AlignHCenter |
Qt::AlignVCenter, text);}
paint一共做了两件事情,第一件事情绘制一个圆,第二件事情就是绘制一个标识文字。
其中的A就是标识文字
如何绘制线
graphLine
类设计如下
classgraphLine:publicQObject,publicQGraphicsLineItem{
Q_OBJECT
public:enumLineType{
LeftToRight,// ==>
RightToLeft,// <==
TwoWayArrow,// <=>
NoArrow,// <=>};explicitgraphLine(graphNode *begin,
graphNode *end,
LineType type = NoArrow,
QObject *parent =nullptr);private:
graphNode *begin;
graphNode *end;int length;
QColor color;
LineType lineType;private:voidpaintArrow(graphNode* begin, graphNode* end, QPainter* painter);public:
QPainterPath shape()constoverride;voidpaint(QPainter *painter,const QStyleOptionGraphicsItem *option, QWidget *widget)override;const QColor &getColor()const;voidsetColor(const QColor &newColor);
LineType getLineType()const;voidsetLineType(LineType newType);
graphNode *getBegin()const;
graphNode *getEnd()const;};
其中
paintArrow
用来绘制箭头
绘制线的线有两种,一种是不带箭头的,一种是带方向箭头的。
不带箭头
不带箭头的比较好绘制,计算一下起点和终点的坐标,画一条线就是。
auto r_begin = begin->getPoint()+ begin->pos();auto r_end = end->getPoint()+ end->pos();
QLineF lines(r_begin, r_end);setLine(lines);
QPen pen;
pen.setWidth(2);
painter->setPen(pen);
painter->setRenderHint(QPainter::HighQualityAntialiasing);
painter->drawLine(line());
带箭头
绘制箭头可能需要一些计算,不过由于我们这里这个图形选择的圆,其实还是比较容易计算的。
如果是多边形,要麻烦一点。
在这里,我们想要的效果是箭头始终紧贴着其指向的圆。
比如这种效果:
我们知道 起点 a 和 终点 b的坐标,知道圆的半径,其实就很容易的推导出 圆和这条直线的交点是多少了。
大概是这样:
点
a
坐标为
(
x
1
,
y
1
)
,
点
b
坐标为
(
x
2
,
y
2
)
现在,
a
b
的距离
=
(
x
2
−
x
1
)
2
+
(
y
2
−
y
1
)
2
直线
a
b
的斜率为
k
=
(
y
2
−
y
1
)
/
(
x
2
−
x
1
)
现在点
c
(
x
,
y
)
在
a
b
上,若与
a
的距离为
c
的话。则有:
{
(
x
−
x
1
)
2
+
(
y
−
y
1
)
2
=
c
2
(
y
−
y
1
)
/
(
x
−
x
1
)
=
k
点
a
已知,距离
c
已知,斜率
k
已知
联立方程可以解得:
{
x
=
±
c
1
+
k
2
+
x
1
y
=
±
c
k
1
+
k
2
+
y
1
点 a 坐标为 (x_1, y_1), 点b坐标为(x_2, y_2) \\ 现在,a b的距离 = (x_2 - x_1)^2 +(y_2 - y_1)^2 \\ 直线 ab的斜率为 k = (y_2-y_1) / (x_2-x_1) \\ 现在点c(x, y)在ab上,若与a的距离为c的话。则有:\\ \begin{cases} (x - x_1)^2 +(y - y_1)^2 = c^2 \\ (y-y_1) / (x-x_1) = k \end{cases} 点a已知,距离c已知,斜率k已知 \\ 联立方程可以解得:\\ \begin{cases} x = \pm \frac{c}{\sqrt{1 + k^2}} +x_1\\ y = \pm \frac{c k}{\sqrt{1 + k^2}} +y_1 \end{cases}
点a坐标为(x1,y1),点b坐标为(x2,y2)现在,ab的距离=(x2−x1)2+(y2−y1)2直线ab的斜率为k=(y2−y1)/(x2−x1)现在点c(x,y)在ab上,若与a的距离为c的话。则有:{(x−x1)2+(y−y1)2=c2(y−y1)/(x−x1)=k点a已知,距离c已知,斜率k已知联立方程可以解得:{x=±1+k2c+x1y=±1+k2ck+y1
直线和圆相交的点圆两个,只有一个是合法的,这里只需要判断一下即可
bool__graphLine__containsLine(QPointF begin, QPointF end, QPointF now){
QLineF a(begin, end);
QLineF b(begin, now);
QLineF c(now, end);if(fabs(a.length()- b.length()- c.length())<1e-6)returntrue;returnfalse;}
计算出圆与直线的交点之后,绘制两根直线,分别向上和向下偏移30°来充当箭头即可。
void graphLine::paintArrow(graphNode* begin, graphNode* end, QPainter* painter){auto r_begin = begin->getPoint()+ begin->pos();auto r_end = end->getPoint()+ end->pos();
QLineF lines(r_begin, r_end);auto length = end->getR()+ end->getRoundWidth()/2;// 宽度是内圈外圈各渲染一部分
qreal dx, dy;if(fabs(lines.dx())<1e-6){
dx =0;
dy = length;}else{auto k = lines.dy()/ lines.dx();
qreal base =sqrt(k * k +1);
dx = length / base;
dy = length * k / base;}
QPointF dis(dx, dy);
QPointF now;if(__graphLine__containsLine(r_begin, r_end,QPointF(r_end + dis))){
now =QPointF(r_end + dis);}else{
now =QPointF(r_end - dis);}
QLineF arrowHead(now, r_begin);
arrowHead.setLength(10+ end->getRoundWidth());
arrowHead.setAngle(arrowHead.angle()-30);// 上方
painter->drawLine(arrowHead);
arrowHead.setAngle(arrowHead.angle()+60);// 下方
painter->drawLine(arrowHead);}
知道如何绘制箭头之后,和绘制直线组合起来,就可以了;
paint
完整代码
void graphLine::paint(QPainter *painter,const QStyleOptionGraphicsItem *option, QWidget *widget){auto r_begin = begin->getPoint()+ begin->pos();auto r_end = end->getPoint()+ end->pos();
QLineF lines(r_begin, r_end);setLine(lines);
QPen pen;
pen.setWidth(2);if(isSelected()){
pen.setColor(QColor((color.red()+125)%255,(color.green())%255,(color.blue()+125)%255));}else{
pen.setColor(color);}
painter->setPen(pen);
painter->setRenderHint(QPainter::HighQualityAntialiasing);
painter->drawLine(line());switch(lineType){case LeftToRight:paintArrow(begin, end, painter);break;case RightToLeft:paintArrow(end, begin, painter);break;case TwoWayArrow:paintArrow(begin, end, painter);paintArrow(end, begin, painter);break;case NoArrow:;default:;}}
在这里,添加点我选择使用右键单击添加,连接点是选择两个点就自动添加一根线
这些处理将直接在 view类里面进行处理,因此,我自定义了一个graph类
classgraph:publicQGraphicsView{
Q_OBJECT
public:enumSelectItemMode{
Line,
Node,
None =10086,};explicitgraph(QWidget *parent =nullptr);
QList<graphLine*>Lines();
QList<graphNode*>Nodes();voidsetMode(SelectItemMode);private:
SelectItemMode selectItemMode;
QSet<graphLine*> graphLines;
QSet<graphNode*> graphNodes;
QHash<graphNode*, QSet<graphNode*>> graphMap;private:voidmouseLButtonClick(QMouseEvent *event);voidmouseRButtonClick(QMouseEvent *event);protected:voidmousePressEvent(QMouseEvent *event)override;voidmouseMoveEvent(QMouseEvent *event)override;voidmouseReleaseEvent(QMouseEvent *event)override;
signals:voidmouseClickEvent(QPoint point);voidmouseMoveEvent(QPoint point);voidselectItem(QGraphicsItem *);// QWidget interfaceprotected:voidresizeEvent(QResizeEvent *event)override;voidpaintEvent(QPaintEvent *event)override;private slots:voidon_scene_select_change();voidon_selection_change(QGraphicsItem *, QGraphicsItem *, Qt::FocusReason);};
添加点
graph类重写
mousePressEvent
方法。
void graph::mousePressEvent(QMouseEvent *event){switch(event->button()){case Qt::MouseButton::RightButton:mouseRButtonClick(event);break;default:QGraphicsView::mousePressEvent(event);}}
然后在
mouseRButtonClick
中处理右键事件
void graph::mouseRButtonClick(QMouseEvent *event){auto pointScene =mapToScene(event->pos());auto item =newgraphNode(pointScene,20,QString("A")));
item->setFlag(QGraphicsItem::ItemIsMovable,true);if(selectItemMode == Node){
item->setFlags( item->flags()|
QGraphicsItem::ItemIsFocusable |
QGraphicsItem::ItemIsSelectable);}scene()->addItem(item);
graphNodes.insert(item);}
添加线
添加线需要通过处理
selectionChanged
connect(scene(),SIGNAL(selectionChanged()),this,SLOT(on_scene_select_change()));
当选择的item为2时,则连接一条直线
void graph::on_scene_select_change(){// mode select graphNodeauto list =scene()->selectedItems();if(selectItemMode == Node){staticdecltype(list) old_list;if(list.size()>2){scene()->clearSelection();return;}if(list.size()==2){auto a{dynamic_cast<graphNode*>(list[0])},
b{dynamic_cast<graphNode*>(list[1])};if(old_list[0]!= list[0]) std::swap(a, b);if(graphMap[a].contains(b))return;// 两点之间有线不需要连接Ⅰ
graphMap[a].insert(b);
graphMap[b].insert(a);auto now =newgraphLine(a, b);if(selectItemMode == Line){
now->setFlags( now->flags()|
QGraphicsItem::ItemIsFocusable |
QGraphicsItem::ItemIsSelectable);}scene()->addItem(now);
graphLines.insert(now);}
old_list = list;}elseif(selectItemMode == Line){if(list.size()>1){scene()->clearSelection();return;}}auto item =scene()->mouseGrabberItem();
emit selectItem(item);}
到这里,基本上,核心的东西就完成了,剩下的是ui界面了。
我的ui界面比较丑,大概长这样:
这就是一个最基本的 图 编辑器了
{来自 amjieker }
版权归原作者 AMjieker 所有, 如有侵权,请联系我们删除。