本系列目录
Qt (高仿Visio)流程图组件开发(一) 效果展示及基本开发框架构思
Qt (高仿Visio)流程图组件开发(二) 基本图元绘制 图元间连线绘制
Qt (高仿Visio)流程图组件开发(三) 图元基类如何定义,流程图多种图元类型实现
Qt (高仿Visio)流程图组件开发(四) 流程图 图元对齐 磁吸线功能
Qt (高仿Visio)流程图组件开发(五) 流程图 双击编辑图元内容实现
Qt (高仿Visio)流程图组件开发(六) 流程图 线图元 如何绘制曲线 连接线移除视口后无法显示
Qt (高仿Visio)流程图组件开发(七) 流程图 简单操作界面搭建
Qt (高仿Visio)流程图组件开发(八) 流程图 鼠标拖动图元到场景(QGraphicsScene)创建
Qt (高仿Visio)流程图组件开发(九) 流程图 代码展示
前言
本文主要讲解简单的实现,如何绘制图元,图元之间如何连线,拖动图元线跟随图元动,线的位置判断等等。只是经验分享,描述内容并不绝对,如有误差欢迎指正。
一、如何绘制图元
所有的图元都可以看作“放置在”场景(QGraphicsScene)上,调用接口addItem(QGraphicsItem *item),即可在场景添加一个图元。
QGraphicsView* m_view = new QGraphicsView(this);
QGraphicsScene* m_scene = m_view->scene();
QGraphicsRectItem* rect_item = new QGraphicsRectItem();
m_scene->addItem(rect_item);
但是这只适用于简单的图元绘制,并不能满足我们绘制特殊图元的需求,例如样式,选中态,文本显示等等,所以我们需要通过重写图元类来绘制特殊图元。这里以流程图中的“流程”图元为例就行讲解。下面两个虚函数是我认为重写一个图元类比较主要的部分。
public:
virtual QRectF boundingRect() const;
virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = Q_NULLPTR);
boundingRect() 函数主要用来确定绘制图元的形状生效范围,这个会影响到之后的选中功能。
paint()函数主要来绘制图元的形状、内容、交互状态,但是只涉及其本身的绘制,与其他图元无关。
图元类只关注自身相关的操作,对与全局的一些操作,例如:菜单、移动、选中、删除等等并不在该类下处理。“流程”图元实现如下:
QRectF FlowchartGraphicsRectItem::boundingRect() const
{
QRectF rect = QGraphicsRectItem::boundingRect().adjusted(-3, -3, 3, 3);
return rect;
}
void FlowchartGraphicsRectItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget /*= Q_NULLPTR*/)
{
painter->setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing);
painter->save();
painter->setPen(item_infor_->item_style_.pen_);
painter->setBrush(item_infor_->item_style_.brush_);
painter->drawRect(this->boundingRect());
painter->restore();
/*[] 绘制选中外框*/
if (this->isSelected()) {
/*[1]: 绘制外围的矩形框 */
QPen selected_pen(QColor(147, 147, 147));
selected_pen.setWidth(0.5);
selected_pen.setStyle(Qt::DashLine);
painter->setPen(selected_pen);
painter->setBrush(Qt::NoBrush);
painter->drawRect(this->boundingRect());
const auto left_center_rect = QRectF((left_point_ - QPointF(2, 2)), QSizeF(5, 5));
const auto right_center_rect = QRectF((right_point_ - QPointF(3, 3)), QSizeF(5, 5));
const auto top_center_rect = QRectF((top_point_ - QPointF(2, 2)), QSizeF(5, 5));
const auto bottom_center_rect = QRectF((bottom_point_ - QPointF(3, 3)), QSizeF(5, 5));
/*[2]: 绘制四个方向的连接点 */
painter->setPen(QColor(147, 147, 147));
painter->setBrush(QColor(255, 255, 255));
painter->drawRect(left_center_rect);
painter->drawRect(right_center_rect);
painter->drawRect(top_center_rect);
painter->drawRect(bottom_center_rect);
}
DrawItemText(painter);
}
二、两图元之间如何连线
visio在绘制连线时,会有一根线跟随鼠标移动,最后在两个图元之间建立连线。接下来我们分析如何实现连线这一动态效果,在鼠标按下时我们添加一个线图元,位置为鼠标按下位置,鼠标移动时添加一个临时线图元,终点为当前鼠标位置。鼠标释放时,判断起点与终点的位置有哪些图元,并在起点与终点处各选取最上层的一个图元,获取两图元对象,绘制连线,删除临时线图元对象。
绘制图元主要在鼠标事件中实现
virtual void mousePressEvent(QGraphicsSceneMouseEvent *event);
virtual void mouseMoveEvent(QGraphicsSceneMouseEvent *event);
virtual void mouseReleaseEvent(QGraphicsSceneMouseEvent *event);
鼠标按下时创建线图元对象,并添加到场景(QGraphicsScene)中。
temp_line_ = new QGraphicsLineItem(QLineF(event->scenePos(), event->scenePos()));
temp_line_->setPen(QPen(QColor(89, 152, 209), 3));
addItem(temp_line_);
鼠标移动时创建一个临时线图元对象,并添加到场景(QGraphicsScene)中,起点为鼠标按下位置,终点为鼠标移动实时位置,以此来实现一个动态绘制连线的过程。
if (scene_mode_ == SceneMode::DrawLineItem
&& nullptr != temp_line_) {
// 如果临时线段不为空的时候,将press事件中的点和和移动的位置点进行连接,更新临时线
QLineF temp_line(temp_line_->line().p1(), event->scenePos());
temp_line_->setLine(temp_line);
}
鼠标释放时从场景移除线图元对象,删除临时线图元,获取起点与终点坐标,获取两点坐标最上层图元对象,两图元为基准绘制线图元。因为此时有临时线图元对象,所以需要判断获取的图元集合,并删除第一个临时线图元对象。
if (temp_line_ != nullptr && scene_mode_ == SceneMode::DrawLineItem) {
// ! [1] 移除线段的开始点p1和结束点p2处的线段item,QGraphicsItem
QList<QGraphicsItem*> startItems = items(temp_line_->line().p1());
if (!startItems.isEmpty() && startItems.first() == temp_line_)
startItems.removeFirst();
QList<QGraphicsItem*> endItems = items(temp_line_->line().p2());
if (!endItems.isEmpty() && endItems.first() == temp_line_)
endItems.removeFirst();
// ! [2] 将临时线段删除,以获取开始和结束Item,临时线段已经无用
removeItem(temp_line_);
delete temp_line_;
temp_line_ = nullptr;
// ! [3] 根据开始和结束item重新绘制带箭头的连接线
if (!startItems.isEmpty() && !endItems.isEmpty() && startItems.first() != endItems.first()) {
FlowcharGraphicsLink* link_item = AddStartToEndItem(QGraphToFlow(startItems.first()), QGraphToFlow(endItems.first()));
}
}
三、如何实现线跟随图元移动
对于流程图元的移动只需通过接口设置运行图元移动即可,但是线图元的移动则需要我们手动去实现。首先分析,线图元是跟随其他图元进行移动的,属于被动更新,这样就有了两种实现方式:其一,其他图元提供接口或属性,线图元内部维护起点与终点图元,通过实时主动获取图元位置来更新线图元位置;其二,线图元提供接口或属性,其他图元内部维护与其连接的线图元对象,图元移动时也相应的更改线图元位置。这里采用第一种实现方式,第二种方式存在一定权限,玩家可自行实现去发现。
第一种实现方式,在绘制连线时得到起点与终点的两个图元对象,将这两个图元作为成员对象保存到线图元中,线图元在绘制时通过接口实时获取两图元位置进行绘制。这里实现较为简单不展示代码,之后会在本系列最后展示全部代码。
四、线的位置判断
连线在绘制时的起点与终点虽说是两图元的位置,但是具体是图元的什么位置还需要我们去实现,当然也可以直接设置为图元中心点,但这样略微有点不美观。这是一个优化的功能,与具体流程图实现没有关系,玩家可选择性学习。
流程图元需要实现一些位置获取的接口,获取上中,下中,左中,右中,中位置五个接口进行判断,该方法是最简单粗暴的办法,玩家有其他更好的实现也可以~,代码如下:
// ! [1] 获取起始点
switch (CompareDrawPoint(start_item_->GetCenterPoint(), end_item_->GetCenterPoint()))
{
case DrawLineAlignment::DrawLeft:
start_point_ = start_item_->GetLeftPoint();
end_point_ = end_item_->GetRightPoint();
break;
case DrawLineAlignment::DrawRight:
start_point_ = start_item_->GetRightPoint();
end_point_ = end_item_->GetLeftPoint();
break;
case DrawLineAlignment::DrawTop:
start_point_ = start_item_->GetTopPoint();
end_point_ = end_item_->GetBottomPoint();
break;
case DrawLineAlignment::DrawBottom:
start_point_ = start_item_->GetBottomPoint();
end_point_ = end_item_->GetTopPoint();
break;
default:
break;
}
// 判断箭头连线起始点类型
DrawLineAlignment FlowcharGraphicsLink::CompareDrawPoint(QPointF _start_point, QPointF _end_point)
{
// 直线斜率
double k_num = (double)(_end_point.y() - _start_point.y()) / (_end_point.x() - _start_point.x());
if (k_num >= -1 && k_num <= 1)
{
if (_start_point.x() >= _end_point.x()){
return DrawLineAlignment::DrawLeft;
}
else{
return DrawLineAlignment::DrawRight;
}
}
else
{
if (_start_point.y() >= _end_point.y()){
return DrawLineAlignment::DrawTop;
}
else{
return DrawLineAlignment::DrawBottom;
}
}
}
总结
本文主要介绍流程图的基本实现,通过此文可以简单实现一个流程图。
本文只是经验分享,描述内容并不绝对,如有误差欢迎指正。
如果此文帮助到你( •̀ ω •́ )✧,动动小手点个赞可好O(∩_∩)O。
原创文章,转载请标明本文出处。