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要解決,寫這篇文章純屬技術分享給需要的人;由於工程內有許多項目相關性的東西,不方便全部開源,如果有什麼疑問歡迎交流。