1 前言
最近因项目需求,需要制作一个可以绘制树结构的“事件链”插件,于是呼找到了QT自带的一个画流程图的例子“diagramscene”,还在网上找到了另外一个例子,然后我结合了两个demo实现了我的“事件编辑器”的前期实现工作,虽然有点小BUG但是基本上算是完成了。
1)qt 自带的绘制流程图示例,可以在QT Creator搜索“diagramscene”
D:\Qt\Qt5.9.3\Examples\Qt-5.9.3\widgets\graphicsview\diagramscene
https://download.csdn.net/download/octdream/11953929
2) qt绘制流程图示例程序——Diagram
https://download.csdn.net/download/octdream/11953930
2 效果预览
3 功能分析
由上图可以看到,本人实现了节点的添加,编辑,删除,移动以及节点与节点之间建立父子关系。
其实除了这些功能以外,还实现了链路图的保存,加载功能,可实现反复编辑操作,内部采用读写数据的方式完成。
4 实现
1)节点图元实现
相比原先的demo而言,我作了如下几个方面的修改:
i)添加了节点绑定自定义信息
这个信息由项目需要来定,为自定义结构体,可以实现一个节点能携带更多我们所需的信息。
public:
//设置节点所表示事件的具体信息;
void setEventInfo(const KDEventInfoCn &info);
//获取节点所表示事件的具体信息;
KDEventInfoCn eventInfo();
private:
KDEventInfoCn mEventInfo;
void KDEventNode::setEventInfo(const KDEventInfoCn &info)
{
prepareGeometryChange();
this->mEventInfo = info;
update();
}
KDEventInfoCn KDEventNode::eventInfo()
{
return this->mEventInfo;
}
ii) 添加了节点支持多行文本显示
QT中QFontMetricsF 是不支持换行计算的,所以只能自己分割字符串来计算多行的文本外边框。
QRectF KDEventNode::getContentRect(QString content) const
{
QFontMetricsF fm = qApp->fontMetrics();
QRectF rect = fm.boundingRect(content);
//单行字体大小
float fontHeight = fm.height();
//行距
float lineHeight = 4;
float maxWidth = 0;
QStringList textList = content.split("\n");
//最大的宽度
for(int i = 0;i < textList.size(); i++)
{
float temp = fm.width(textList.at(i));
if(temp > maxWidth)
{
maxWidth = temp;
}
}
// 计算总高度
float textHeightSum = fontHeight * textList.size();
float lintHeightSum = lineHeight * (textList.size()-1);
float heightSum = textHeightSum + lintHeightSum;
float widthSum = maxWidth ;
rect.setSize(QSizeF(widthSum,heightSum));
return rect;
}
2)连线图元实现
i) 优化了连线与节点的交点计算,并且优化了箭头的样式
void KDEventLink::paint(QPainter *painter, const QStyleOptionGraphicsItem *,QWidget *)
{
if (_mFromNode->collidesWithItem(_mToNode))
return;
QPen myPen = pen();
myPen.setColor(_mColor);
qreal arrowSize = 15;
painter->setPen(myPen);
painter->setBrush(_mColor);
QLineF centerLine(_mFromNode->pos(), _mToNode->pos());
QPolygonF endPolygon = _mToNode->shape().toFillPolygon();//_mToNode->polygon();
QPointF intersectPointTo = getIntersectPoint(centerLine,endPolygon,_mToNode);
QPolygonF startPolygon = _mFromNode->shape().toFillPolygon();
QPointF intersectPointFrom = getIntersectPoint(centerLine,startPolygon,_mFromNode);
setLine(QLineF(intersectPointTo,intersectPointFrom));
double angle = ::acos(line().dx() / line().length());
if (line().dy() >= 0)
angle = (Pi * 2) - angle;
QPointF arrowP1 = line().p1() + QPointF(sin(angle + Pi / 3) * arrowSize,
cos(angle + Pi / 3) * arrowSize);
QPointF arrowP2 = line().p1() + QPointF(sin(angle + Pi - Pi / 3) * arrowSize,
cos(angle + Pi - Pi / 3) * arrowSize);
qreal d = -(arrowSize-6);
QPointF arrowP3 = line().p1() + QPointF((d *(line().p1().x() -line().p2().x()))/line().length() ,
(d *(line().p1().y() -line().p2().y()))/line().length());
_mLinkHead.clear();
_mLinkHead << line().p1() << arrowP1 << arrowP3 << arrowP2;
painter->drawLine(line());
painter->drawPolygon(_mLinkHead);
if (isSelected()) {
painter->setPen(QPen(_mColor, 1, Qt::DashLine));
QLineF myLine = line();
myLine.translate(0, 4.0);
painter->drawLine(myLine);
myLine.translate(0,-8.0);
painter->drawLine(myLine);
}
}
QPointF KDEventLink::getIntersectPoint(QLineF centerLine,QPolygonF polygon,KDEventNode *node)
{
QPointF p1 = polygon.first() + node->pos();
QPointF p2;
QPointF intersectPoint;
QLineF polyLine;
for (int i = 1; i < polygon.count(); ++i) {
p2 = polygon.at(i) + node->pos(); //偏移座标 + 原点 = 实际座标
polyLine = QLineF(p1, p2);
QLineF::IntersectType intersectType =
polyLine.intersect(centerLine, &intersectPoint);
if (intersectType == QLineF::BoundedIntersection)
break;
p1 = p2;
}
return intersectPoint;
}
3)绘制场景
i) 添加了节点移动信号
节点移动信号,是为了支持节点发生移动后可以正常保存两数据库中,当重新绘制链路图时,可以正确显示节点的位置。
虽然系统提供了节点的x,y座标移动信号,但是感觉触发太频繁;我这里是当 : 选中——>移动——>释放,完成这个过程才会触发一个移动信号。
ii)添加节点插入信号
当用户在场景中点击鼠标左键,会在点击位置产生一个节点,此时则与会产生一个节点插入信号
ii)添加连接线插入信号
当用户在场景中在一节点上点击鼠标左键不松然后拖动至另一节点上释放左键,则会在开始节点与目标节点中间产生一个条带箭头的连接线,此时也会产生一个连接线插入信号。
signals:
//节点插入
void nodeInserted(KDEventNode *item);
//连接线插入
void linkInserted(KDEventLink *item);
//节点移动
void nodeMoved();
protected:
void mousePressEvent(QGraphicsSceneMouseEvent *mouseEvent) override;
void mouseMoveEvent(QGraphicsSceneMouseEvent *mouseEvent) override;
void mouseReleaseEvent(QGraphicsSceneMouseEvent *mouseEvent) override;
private:
bool isChoosed;
bool isMoved;
void KDEventScene::mousePressEvent(QGraphicsSceneMouseEvent *mouseEvent)
{
if (mouseEvent->button() != Qt::LeftButton)
return;
if(this->mCurrWarInfo == NULL)
{
HRMessageBox::information(mParent, "没有选择一个交战项目", "请选择一个或创建一个交战项目");
return ;
}
if(this->mCurrEventChainInfo == NULL)
{
HRMessageBox::information(mParent, "没有选择一个事件链", "请选择一个或创建一个事件链");
return ;
}
KDEventNode *item;
switch (mMode) {
case InsertNode:{
item = new KDEventNode(mItemMenu,mParent);
addItem(item);
seqNumber++;
item->setPos(mouseEvent->scenePos());
KDEventInfoCn info;
info.eventChainId = this->mCurrEventChainInfo->id;
info.warId = this->mCurrWarInfo->id;
info.content = tr("事件 %1").arg(seqNumber);
info.nodePos = item->pos();
item->setEventInfo(info);
emit nodeInserted(item);
break;
}
case InsertLine:
line = new QGraphicsLineItem(QLineF(mouseEvent->scenePos(),mouseEvent->scenePos()));
line->setPen(QPen(myLineColor, 2));
addItem(line);
break;
default:
isChoosed = true;
break;
}
QGraphicsScene::mousePressEvent(mouseEvent);
}
void KDEventScene::mouseMoveEvent(QGraphicsSceneMouseEvent *mouseEvent)
{
if (mMode == InsertLine && line != 0) {
QLineF newLine(line->line().p1(), mouseEvent->scenePos());
line->setLine(newLine);
} else if (mMode == MoveNode) {
if(isChoosed)
isMoved = true;
QGraphicsScene::mouseMoveEvent(mouseEvent);
}
}
void KDEventScene::mouseReleaseEvent(QGraphicsSceneMouseEvent *mouseEvent)
{
if (line != 0 && mMode == InsertLine) {
QList<QGraphicsItem *> startItems = items(line->line().p1());
if (startItems.count() && startItems.first() == line)
startItems.removeFirst();
QList<QGraphicsItem *> endItems = items(line->line().p2());
if (endItems.count() && endItems.first() == line)
endItems.removeFirst();
removeItem(line);
delete line;
if (startItems.count() > 0 && endItems.count() > 0 && startItems.first() != endItems.first()) {
KDEventNode *startItem = qgraphicsitem_cast<KDEventNode *>(startItems.first());
KDEventNode *endItem = qgraphicsitem_cast<KDEventNode *>(endItems.first());
KDEventLink *arrow = new KDEventLink(startItem, endItem);
arrow->setColor(myLineColor);
startItem->addLink(arrow);
endItem->addLink(arrow);
//arrow->setZValue(-1000.0);
addItem(arrow);
arrow->updatePosition();
emit linkInserted(arrow);
}
}
else if(mMode == MoveNode && isMoved)
{
emit nodeMoved();
}
isMoved = false;
isChoosed = false;
line = 0;
QGraphicsScene::mouseReleaseEvent(mouseEvent);
}
4)链路图操作
i) 插入、更新
在主界面中需要关联三个信号:节点插入、连接线插入、节点移动
void KDEventEditorWidget::initGraphicsView()
{
_p->_scene = new KDEventScene(_p->_nodeMenu,this);
_p->_scene->setSceneRect(QRectF(0, 0, 5000, 5000));
_p->_scene->setBackgroundBrush(Qt::white);
connect(_p->_scene, SIGNAL(nodeInserted(KDEventNode*)),this, SLOT(nodeInserted(KDEventNode*)));
connect(_p->_scene, SIGNAL(linkInserted(KDEventLink*)),this, SLOT(linkInserted(KDEventLink*)));
connect(_p->_scene, SIGNAL(nodeMoved()),this, SLOT(nodeMoved()));
ui->graphicsView->setScene(_p->_scene);
ui->graphicsView->setDragMode(QGraphicsView::RubberBandDrag);
ui->graphicsView->setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing);
}
在各个槽函数中需要将所需要的数据保存下来,我在项目我采用的是Sqlite来保存数据的,当然你也可以采用文件或其它数据库来保存数据;主要是要将节点数据,节点在场景中的位置、节点的父节点信息保存下来,这样才能复现链路图。
节点插入是向数据库插入一条节点信息的过程。(INSERT)
连接线插入是节点之间创建父子关系的过程,需要在数据库中更新节点信息。(UPDATE)
节点移动是节点的位置改变的过程,也需要在数据库中更新节点位置信息。(UPDATE)
//得到节点信息,插入数据库
void KDEventEditorWidget::nodeInserted(KDEventNode *node)
{
KDEventInfoCn infoCn = node->eventInfo();
_p->_sqliteDB->insert(&infoCn);
}
void KDEventEditorWidget::linkInserted(KDEventLink *link)
{
KDEventNode *node = link->toNode();
KDEventInfoCn infoCn = node->eventInfo();
infoCn.parentId = link->fromNode()->eventInfo().id;
node->setEventInfo(infoCn);
_p->_sqliteDB->update(&infoCn);
}
void KDEventEditorWidget::nodeMoved()
{
QList<QGraphicsItem *> items = _p->_scene->selectedItems();
foreach(QGraphicsItem * item,items)
{
if (item->type() == KDEventNode::Type)
{
KDEventNode *node = qgraphicsitem_cast<KDEventNode *>(item);
KDEventInfoCn infoCn = node->eventInfo();
infoCn.nodePos = node->pos();
_p->_sqliteDB->update(&infoCn);
}
}
}
在这里我在移动节点是,实现了兼容了可以框选多个节点进行移动,然后保存所有节点位置信息。
ii)节点删除
由于项目需要,我们的链路图中的节点之间是有因关系,当删除个节点时,需要删除这个节点即它的所有子节点,所以我使用了一个递归算法来删除。
void KDEventEditorWidget::deleteItem()
{
QGraphicsItem *item = selectedItem();
if (item)
{
int ret = HRMessageBox::information(this, "删除事件节点", "您确定要删除这个事件节点即所有子节点吗?",HRMessageBox::Yes | HRMessageBox::No,HRMessageBox::No);
if(ret == HRMessageBox::Yes){
delNodeBranch(item);
}
}
}
//递归删除
void KDEventEditorWidget::delNodeBranch(QGraphicsItem *item)
{
if (item->type() == KDEventLink::Type)
{
KDEventLink *link = qgraphicsitem_cast<KDEventLink *>(item);
delNodeBranch(link->toNode());
}
else if (item->type() == KDEventNode::Type)
{
KDEventNode *node = qgraphicsitem_cast<KDEventNode *>(item);
//断开与父节点的连接
foreach(KDEventLink *link,node->links)
{
if(link->toNode() == node)
{
node->removeLink(link);
link->fromNode()->removeLink(link);
_p->_scene->removeItem(link);
delete link;
}
}
foreach(KDEventLink *link,node->links)
{
if(link->toNode() != node)
{
delNodeBranch(link->toNode());
}
}
_p->_scene->removeItem(node);
_p->_sqliteDB->remove(&node->eventInfo());
delete node;
}
}
iii) 重新加载链路图
项目中需要对保存的链路图,重新加载后,再次编辑,这是一个数据读取及场景复现的问题,所以之间保存的节点位置,及父节点等信息起到了作用。
//点击事件链,加载该事件链的所有事件
void KDEventEditorWidget::on_eventChainTreeWidget_itemClicked(QTreeWidgetItem * dirItem, int column)
{
if (dirItem)
{
currEventChainInfo = dirItem->data(0,Qt::UserRole+1).value<EventChainInfo>();
_p->_scene->setCurrEventChain(&currEventChainInfo);
ui->dockWidget_3->setWindowTitle(QString("%0 拓扑图").arg(dirItem->text(column)));
//TD: 加载该事件链的所有节点
clearGrapscsView();
reDrawNodes(getEventChainNodeList(currEventChainInfo));
}
}
QList<KDEventInfoCn> KDEventEditorWidget::getEventChainNodeList(EventChainInfo info)
{
QList<KDEventInfoCn> result;
QString sql = QString("select * from EventInfo where event_chain_id='%0'").arg(info.id);
if(_p->_sqliteDB->select(sql))
{
QSqlQuery mQuery = _p->_sqliteDB->getSqlQuery();
while(mQuery.next())
{
KDEventInfoCn nodeInfo(
mQuery.value(0).toString(),
mQuery.value(1).toString(),
mQuery.value(2).toString(),
mQuery.value(3).toString(),
mQuery.value(4).toInt(),
mQuery.value(5).toInt(),
mQuery.value(6).toDouble(),
mQuery.value(7).toInt(),
mQuery.value(8).toInt(),
mQuery.value(9).toDateTime(),
mQuery.value(10).toDouble(),
mQuery.value(11).toInt(),
mQuery.value(12).toUInt(),
mQuery.value(13).toUInt(),
mQuery.value(14).toUInt(),
mQuery.value(15).toUInt(),
mQuery.value(16).toUInt(),
mQuery.value(17).toDouble(),
mQuery.value(18).toDouble(),
mQuery.value(19).toString(),
mQuery.value(20).toString());
result.append(nodeInfo);
}
}
return result;
}
void KDEventEditorWidget::reDrawNodes(QList<KDEventInfoCn> list)
{
QMap<QString,KDEventNode *> nodeMap;
QMap<QString,KDEventTypeInfo> eventTypesMap = getEventTypeList();
KDEventNode * node = NULL;
foreach(KDEventInfoCn info,list)
{
if(!info.eventType.id.isEmpty())
{
info.eventType = eventTypesMap[info.eventType.id];
}
node = _p->_scene->addNode(info);
nodeMap[info.id] = node;
}
QMap<QString,KDEventNode *>::const_iterator it = nodeMap.constBegin();
KDEventLink *link = NULL;
while (it != nodeMap.constEnd()) {
KDEventNode * toNode = it.value();
KDEventNode * fromNode = NULL;
QMap<QString,KDEventNode *>::const_iterator iter = nodeMap.find(toNode->eventInfo().parentId);
if(iter != nodeMap.constEnd())
{
fromNode = iter.value();
link = new KDEventLink(fromNode,toNode);
_p->_scene->addLink(link);
}
++it;
}
}
4 结束语
项目还属于初期阶段,后期还会继续优化功能,还有一些BUG要解决,写这篇文章纯属技术分享给需要的人;由于工程内有许多项目相关性的东西,不方便全部开源,如果有什么疑问欢迎交流。