最近在做数据可视化的相关工作,包括曲线图,航迹图,图结构,树结构等。其中树结构相关的工作笔者以前曾经做过,可以参考笔者以前的博客。qt自定义树形控件之一和qt自定义树形控件之二,当时还用数据库对树结构进行持久化。所以这几天的重心在图结构和曲线图上。本文主要介绍图结构的可视化,后续做曲线图进行介绍。
关于图结构的可视化,qt自带一个例子,叫做Elastic Nodes Example。里面有边和节点的绘图实现以及用边连接两个节点。本文内容基于此实现。
首先是node.h的实现,为了使任意两个节点的连线都不穿过其他节点,需要使所有节点均布在一个圆上。
double interval = 2*3.1416/graph->nodeNumber();
qreal xt = qSin(interval*m_node_id)*widgetw;
qreal yt = qCos(interval*m_node_id)*widgeth;
setPos(xt,yt);
同时在calculateForces函数中实现动态,在鼠标拖动一个节点后,该节点可以回到原来的位置。
void Node::calculateForces()
{
//将所有节点固定在一个圆上
qreal xvel = 0;
qreal yvel = 0;
double interval = 2*3.1416/graph->nodeNumber();
qreal xt = qSin(interval*m_node_id)*widgetw;
qreal yt = qCos(interval*m_node_id)*widgeth;
xvel =( xt - pos().rx())/10;
yvel =( yt - pos().ry())/10;
if (qAbs(xvel) < 0.1 && qAbs(yvel) < 0.1)
return;
QRectF sceneRect = scene()->sceneRect();
newPos = pos() + QPointF(xvel, yvel);
newPos.setX(qMin(qMax(newPos.x(), sceneRect.left() + 10), sceneRect.right() - 10));
newPos.setY(qMin(qMax(newPos.y(), sceneRect.top() + 10), sceneRect.bottom() - 10));
}
同时为了使在节点上显示编号,在painter函数最后添加
painter->drawText(QPointF(-4,4),QString::number(m_node_id));
m_node_id作为node类构造函数的一个参数。
画出的6个点如下图所示
if(!is_choosed)
{
gradient.setColorAt(1, QColor(Qt::yellow).lighter(120));
gradient.setColorAt(0, QColor(Qt::yellow).lighter(120));
}
else
{
gradient.setColorAt(1, QColor(Qt::green).lighter(120));
gradient.setColorAt(0, QColor(Qt::green).lighter(120));
}
在mousePressEvent函数中,添加
graph->choosenode(m_node_id);
m_node_id是node类构造函数的一个参数,代表这个node的编号。这样GraphWidget类就能知道是哪个节点被选中了.
choosenode的实现如下:
void GraphWidget::choosenode(int node_id)
{
if(is_waitlink == false) //记录第一个点
{
firstnode = node_id;
node[firstnode]->setchoosed(true);
node[firstnode]->update();
is_waitlink = true;
}
else //判断和第一个点的关系
{
node[firstnode]->setchoosed(false); //不起作用
node[firstnode]->update(); //需要加上这一句
if(node_id != firstnode)
{
int maxid = qMax(node_id,firstnode);
int minid = qMin(node_id,firstnode);
int edge_id = (18-minid)*(minid+1)/2+maxid-10;
if(!edge[edge_id]) //指针需要初始化,野指针和空指针的区别
{
edge[edge_id] = new Edge(node[minid], node[maxid]);
myscene->addItem(edge[edge_id]);
}
else
{
myscene->removeItem(edge[edge_id]);
edge[edge_id] = NULL;
node[firstnode]->setchoosed(false);
}
}
is_waitlink = false;
}
}
该函数的功能是实现依次选择的两个节点的连接和断开。当选择第一个节点时,把这个节点修改了绿色,
在选择第二个节点时,把前一个选择的节点改回黄色,同时把这两个节点连接起来,如果这两个节点本来是连接的,则把这两个节点断开。
这里面使用了一个数学技巧,如果要快速的存储和读取边,通常的想法是放到一个二维数组中,但是edge[3][5]和edge[5][3]会重复。这里使用一维数组存储边。在本例中,假定最多只能有10个节点,那么最多只能有45(C(10,2))条边。假定两个节点编号为x,y,且x