如何用Qt绘制一颗好看的二叉树
原创~~作者码字不易,如需转载请注明出处,谢谢~
欢迎来我的博客小站(Aayu Yain = 学无止境 = 阿宇的可爱博客)逛一逛,有惊喜呦~
最近在学习数据结构二叉树,通过在C++控制台界面已实现了二叉树的前序创建,层次创建和前序加中序确定一颗二叉树三种创建方式。那么问题就来了,一颗已经创建好的二叉树,如何能以图形化的界面显示出来呢?
最终,在学习了Qt的绘图事件和坐标系统的相关函数后,发现可以使用Qt绘制出一颗漂亮的二叉树。
效果如下图:
初始化窗口
高度为2的树
高度为3的树
高度为4的树,可以发现高度控制在四层是最好看的
高度为5的树,发现节点就已经变得很小了
对于6层、7层等以上高度的树,也是可以绘制出来的,但可以想象的到,树节点会变得很小。
所以这里有一个可以改进的地方是,可以给这个QWidget控件分别添加一个水平和竖直的滚动条,设置一个控制条件是当树的高度大于5时,不在缩小其大小,这样的话,画布无法显示所有的节点,但是可以滚动条查看其它的节点。
但是我现在并没有实现这个功能,有时间和精力的朋友可以去尝试一下。
接下来的部分我会重点讲解paintEvent()事件,对于创建一个窗口和ui界面的设计等其它部分,我会简单的带过。
OK,让我们开始吧
首先创建一个基于QWidget类的窗口应用程序,作为我们的主窗口(即初始界面),我们可以在这个界面输入前序序列、中序序列和层次序列。并添加一个“创建”按钮和“清空”按钮。如下图
接下来,我们需要自定义一个新的类myPaint,它继承与QWidget类,这样我们就可以重写paintEvent()事件,达到绘图的效果。
在整个项目上右击—>添加新文件—>选择C++Class,选择base class为QWidget类,点击确定,我们的自定义myPaint类就创建好了,myPaint就相当于我们的画布。
当我们点击创建的时候,读取lineEdit控件里面的内容,然后动态创建一个myPaint类的对象,调用自定义函数setInput(QString, QString),将两个QString类型的变量传递给myPaint类,这部分的代码如下
void Widget::on_btnCreat_clicked()
{QString input1 = ui->lEdInput1->text();QString input2 = ui->lEdInput2->text();myPaint *p = new myPaint;if (p->setInput(input1, input2))p->show();elseQMessageBox::warning(this, "警告", "无效的输入");
}
接下来让我们看看myPaint类的内容
myPaint类拥有公有成员函数setInput(),保护成员函数paintEvent(),以及私有成员myTree,代码如下
public:explicit myPaint(QWidget *parent = 0);bool setInput(QString input1, QString input2);protected:void paintEvent(QPaintEvent *);private:linkedBinaryTree<char>* myTree;
myTree的数据类型就是你自定义的二叉树。
首先我们来看一下myPaint构造函数的内容
myPaint::myPaint(QWidget *parent) : QWidget(parent)
{resize(600, 400);setWindowTitle("怎么样,好看吗");setWindowIcon(QIcon("://image/tree.png"));myTree = new linkedBinaryTree<char>;
}
主要是为窗口设置一些属性,然后为myTree动态分配空间。
然后当myPaint类的对象调用setInput()函数时,myTree就可以根据传过来的参数来创建一颗二叉树,代码如下
bool myPaint::setInput(QString input1, QString input2)
{// 将QString转化为C++标准模板库里的Stringstd::string s1 = input1.toStdString();std::string s2 = input2.toStdString();try {if (s1 != "" && s2 != ""){myTree->preAndInCreatBinaryTree(s1, s2);}else if (s1 != ""){myTree->preCreatBinaryTree(s1);}else if (s2 != ""){myTree->levelCreatBinaryTree(s2);}}catch (invalidSequence) {return false;}return true;
}
当s1和s2都不为空时,会被认为是通过前序和中序序列确定一颗二叉树
当只有s1不为空时,认为是通过前序序列创建二叉树
当只有s2不为空时,认为是通过层次序列创建一颗二叉树
创建成功时返回true,就可以在刚刚“创建”按钮的槽函数里通过show()函数来显示这个窗口,并自动调用paintEvent()函数绘制我们想要的图形。
接下来就是讲解的重点部分了,如何绘制一颗好看的二叉树。
在此之前,先给大家补充一个知识,关于Qt的坐标系统,如下图
在一个空白窗口中,Qt默认的坐标系统的原点就是窗口的左上角,水平向右是X轴的正方向,竖直向下是Y轴正方向,如位置1
但是Qt同样也提供了一组函数使我们可以操作这个坐标系统,如平移,旋转,缩放等操作。
translate()函数提供了坐标系统平移的功能。假使我们用W表示当前窗口的宽度,用H表示当前窗口的高度,那么
translate(W/2, H/2)就可以将坐标系统的原点平移至上图的位置2。这时(0, 0)坐标点就表示窗口的中点,(-W/2, -H/2)就表示窗口的左上点,是不是很好理解?
rotate()函数可以旋转坐标系统。如rotate(45)可以将坐标系统顺时针旋转45度;rotate(-45)可以将坐标系统逆时针旋转45度。
接下来还有两个函数,这两个函数一般是成对使用的,它们就是save()函数和restore()函数,功能如同其字面意思,保存和恢复。含义就是当调用save()函数时,可以将当前坐标系统的状态压入堆栈;restore()函数可以从堆栈中弹出栈顶的坐标系统的状态。那么有什么用呢?
我们可以设想这样一个场景,最初,坐标系统的原点(下文简称原点)位于窗口的左上角,我们调用save()函数。然后通过translate(W/2, H/2)函数将原点平移至窗口的中间位置,我们再调用一次save()函数。接下来你可能会对坐标系统进行各种各样的操作,平移,旋转,缩放啊等等。这时,你可以突然调用restore()函数,你会发现原点突然出现在窗口的中间位置了,即恢复上一次调用save()函数时坐标的状态了。再调用一个restore(),原点出现在窗口的左上角。对save()和restore()函数理解了吗?
OK,接下来我们分析一下一颗二叉树在窗口中的位置,如下图
这是一颗四层二叉树。首先来看第四层的叶子节点,有8个叶子节点,9个空位。我们以节点的半径R为单位长度,窗口的宽用W表示,高用H来表示。易计算得
W = (8 + 9) * 2 * R,很好理解吧。而8个节点又和二叉树的高度有关,如果我们用treeHeight表示二叉树的高度,则
W = (2 ^ (treeHeight - 1) + (2 ^ (treeHeight - 1) + 1)) * 2 * R。蓝色部分是节点个数8,红色部分是空位9
化简一下可得 W = (2 * 2 ^ treeHeight + 2) * R
其中窗口的宽W可以通过widgt()函数获得,而treeHeight也有对应的二叉树方法可以获得,所以我们就可以求出二叉树节点的半径了。这部分代码如下
qreal W = this->width(); // 画布的宽
qreal H = this->height(); // 画布的高
int treeHeight = myTree->getHeight(); // 树的高度
qreal R = W / (2 * std::pow(2, treeHeight) + 2); // 节点的半径
那么上图中二叉树的层高h是如何确定的呢?
其实也很简单,用窗口的高度H减去4 * R,在除以(treeHeight - 1)就可以了。4个R就是最上层和最下层节点中心距窗口的上边缘和下边缘的距离之和。代码如下
const int layerHeight = (H-4*R) / (treeHeight-1); // 层高,即垂直偏移量
我们再来看上面那幅图,可以发现,从最底层的叶子节点开始,每向上一层,所形成的直角三角形的底边长就会增加二倍,不信你数数它多少个单位长度R就知道了。
好了,现在思路就很明显了,根据所画节点的层数,可以确定它距父节点的水平偏移量,垂直偏移量就是我们刚刚确定的层高。然后我们就可以根据边长计算出角度,从而计算出两个节点连线的长度。
在遍历节点的过程中,我们采用前序非递归遍历的方法,这时就需要一个栈来存放当前节点的右孩子节点了。除此之外还不够,我们还必须存放右孩子节点的所在的层数,因为必须通过层数来确定它距父节点的水平偏移量。
我们可以通过一个自定义栈节点来实现我们所说的,代码如下
struct stackNode
{binaryTreeNode<char>* treeNode;int layer; // 标记该节点属于第几层
};
在遍历之前,进行一些初始化工作,代码如下
// 初始化
// 节点的定义
QRectF node(QPointF(-R, -R), QPointF(R, R));
arrayStack<stackNode> stack; // 存放右孩子节点
stackNode qNode;arrayStack<QPointF> points; // 存放右孩子节点相对于当前坐标系统原点的位置
QPointF point;qreal Hoffset; // 水平偏移量
binaryTreeNode<char>* t = myTree->getRoot();
const qreal Pi = 3.14159;
int curLayer = 1;
int curAngle; // 当前角度
qreal deg; // 当前弧度// 将坐标系统的原点(下文简称原点)移动到初始位置
painter.translate(W/2, 2*R);
注意到,我们在绘制节点之前,将坐标系统的原点移动到窗口水平居中,高度距上边缘2R的位置,这就是根节点的位置
接下来开始我们的while循环,代码如下
while (1){painter.drawEllipse(node);painter.drawText(node, Qt::AlignCenter, QString(t->element));// 设置孩子节点相对于父节点的水平偏移量Hoffset = std::pow(2, (treeHeight - curLayer)) * R;deg = std::atan(Hoffset / layerHeight); // 返回的是弧度curAngle = 180 / Pi * deg; // 将弧度转化为角度if (t->rightChild != NULL){// 坐标轴逆时针旋转painter.rotate(-curAngle);//绘制图形路径painter.drawLine(0, R, 0, layerHeight / std::cos(deg) - R);// 旋转复原painter.rotate(curAngle);// 右孩子节点压栈qNode.treeNode = t->rightChild;qNode.layer = curLayer + 1;stack.push(qNode);// 右孩子相对于当前坐标系统原点的位置压栈points.push(QPointF(QPointF(0, 0) + QPointF(Hoffset, layerHeight)));painter.save();}if (t->leftChild != NULL){// 坐标轴顺时针旋转painter.rotate(curAngle);// 绘制边painter.drawLine(0, R, 0, layerHeight / std::cos(deg) - R);// 旋转复原painter.rotate(-curAngle);// 原点移动到左孩子节点的位置painter.translate(QPointF(QPointF(0, 0) + QPointF(-Hoffset, layerHeight)));t = t->leftChild;// 层次加1curLayer++;}else {try {// 获取到右节点的层次状态stack.pop(qNode);t = qNode.treeNode;curLayer = qNode.layer;// 原点移动到右孩子节点的位置painter.restore();points.pop(point);painter.translate(point);}catch (stackEmpty) { painter.resetTransform(); return; }}}
将当前节点绘制出来后,我们需要计算出它孩子节点相对于它的水平偏移量。根据上文的分析,对于一个高度为4的树,第一层和第二层的水平偏移量为8R,第二层和第三层的水平偏移量为4R,第三层和第四层的水平偏移量为2R,以curLayer表示当前绘制节点的层数,Hoffset表示当前的水平偏移量时,有如下公式
Hoffset = 2 ^ (treeHeight - curLayer) * R
在结合层高,可以计算出连线的角度。
如果当前节点的右孩子节点存在的话,将坐标系统逆时针旋转,旋转的角度即为刚刚计算出来的角度,然后画线
painter.drawLine(0, R, 0, layerHeight / std::cos(deg) - R);
前两个参数表示起点坐标(0,R),后两个参数表示线终点坐标,注意,因为这时候坐标系统已经旋转了,所以起点和终点的X轴的值都为0。那么为什么终点的纵坐标还要减R呢?因为连线不能之间连到节点的中心呀,所以终点纵坐标需要减R。
连线绘制完之后,将右孩子节点压栈,右孩子节点的位置压栈,注意,这里必须要调用save()函数保存当前的坐标系统。
这是为什么呢?因为右孩子节点的位置是相对于当前父节点的位置来说的,所以当弹栈时,也同样调用restore()函数来恢复右孩子节点所在的坐标系统状态(即恢复后的原点就是该右孩子的父节点的位置)。
最后,catch语句调用的resetTransform()函数是将坐标系统重置为最初的状态。
好了,这就是我们绘制一颗二叉树的全部过程了~~~
下面我会附上myPaint.cpp的全部代码,供大家参考。
作者码字不易,如需转载请注明出处,谢谢~
#include "mypaint.h"
#include <QPainter>
#include <QIcon>struct stackNode
{binaryTreeNode<char>* treeNode;int layer; // 标记该节点属于第几层
};myPaint::myPaint(QWidget *parent) : QWidget(parent)
{resize(600, 400);setWindowTitle("怎么样,好看吗");setWindowIcon(QIcon("://image/tree.png"));myTree = new linkedBinaryTree<char>;
}bool myPaint::setInput(QString input1, QString input2)
{// 将QString转化为C++标准模板库里的Stringstd::string s1 = input1.toStdString();std::string s2 = input2.toStdString();try {if (s1 != "" && s2 != ""){myTree->preAndInCreatBinaryTree(s1, s2);}else if (s1 != ""){myTree->preCreatBinaryTree(s1);}else if (s2 != ""){myTree->levelCreatBinaryTree(s2);}}catch (invalidSequence) {return false;}return true;
}void myPaint::paintEvent(QPaintEvent *)
{//创建QPainter对象QPainter painter(this);// 反锯齿painter.setRenderHint(QPainter::Antialiasing);painter.setRenderHint(QPainter::TextAntialiasing);//背景图painter.drawPixmap(rect(), QPixmap("://image/600_400.jpg"));//设置字体QFont font;font.setPointSize(12);font.setBold(true);painter.setFont(font);//设置画笔QPen penLine;penLine.setWidth(2); //线宽penLine.setColor(Qt::blue); //划线颜色penLine.setStyle(Qt::SolidLine);//线的类型,实线、虚线等penLine.setCapStyle(Qt::FlatCap);//线端点样式penLine.setJoinStyle(Qt::BevelJoin);//线的连接点样式painter.setPen(penLine);qreal W = this->width(); // 画布的宽qreal H = this->height(); // 画布的高int treeHeight = myTree->getHeight(); // 树的高度qreal R = W / (2 * std::pow(2, treeHeight) + 2); // 节点的半径const int layerHeight = (H-4*R) / (treeHeight-1); // 层高,即垂直偏移量// 初始化// 节点的定义QRectF node(QPointF(-R, -R), QPointF(R, R));arrayStack<stackNode> stack; // 存放右孩子节点stackNode qNode;arrayStack<QPointF> points; // 存放右孩子节点相对于当前坐标系统原点的位置QPointF point;qreal Hoffset; // 水平偏移量binaryTreeNode<char>* t = myTree->getRoot();const qreal Pi = 3.14159;int curLayer = 1;int curAngle; // 当前角度qreal deg; // 当前弧度// 将坐标系统的原点(下文简称原点)移动到初始位置painter.translate(W/2, 2*R);while (1){painter.drawEllipse(node);painter.drawText(node, Qt::AlignCenter, QString(t->element));// 设置孩子节点相对于父节点的水平偏移量Hoffset = std::pow(2, (treeHeight - curLayer)) * R;deg = std::atan(Hoffset / layerHeight); // 返回的是弧度curAngle = 180 / Pi * deg; // 将弧度转化为角度if (t->rightChild != NULL){// 坐标轴逆时针旋转painter.rotate(-curAngle);//绘制图形路径painter.drawLine(0, R, 0, layerHeight / std::cos(deg) - R);// 旋转复原painter.rotate(curAngle);// 右孩子节点压栈qNode.treeNode = t->rightChild;qNode.layer = curLayer + 1;stack.push(qNode);// 右孩子相对于当前坐标系统原点的位置压栈points.push(QPointF(QPointF(0, 0) + QPointF(Hoffset, layerHeight)));painter.save();}if (t->leftChild != NULL){// 坐标轴顺时针旋转painter.rotate(curAngle);// 绘制边painter.drawLine(0, R, 0, layerHeight / std::cos(deg) - R);// 旋转复原painter.rotate(-curAngle);// 原点移动到左孩子节点的位置painter.translate(QPointF(QPointF(0, 0) + QPointF(-Hoffset, layerHeight)));t = t->leftChild;// 层次加1curLayer++;}else {try {// 获取到右节点的层次状态stack.pop(qNode);t = qNode.treeNode;curLayer = qNode.layer;// 原点移动到右孩子节点的位置painter.restore();points.pop(point);painter.translate(point);}catch (stackEmpty) { painter.resetTransform(); return; }}}
}
如何用Qt绘制一颗好看的二叉树相关推荐
- 如何用 OpenGL 绘制雪花?
作者 | 许向武 责编 | 张红月 出品 | CSDN博客 看冬奥才知道,阿勒泰不但是中国的"雪都",还是"人类滑雪起源地".这个说法是否成立,姑且不论,阿勒泰 ...
- 9.如何使用QT绘制导航箭头的图标
利用QT绘制一个地图导航软件中的导航图标,代码如下 #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include <QLa ...
- python画树叶-手把手|如何用Python绘制JS地图?
原标题:手把手|如何用Python绘制JS地图? 关于转载授授权 大数据文摘作品,欢迎个人转发朋友圈,自媒体.媒体.机构转载务必申请授权,后台留言"机构名称+文章标题+转载",申请 ...
- 用python绘制柱状图标题-如何用Python绘制3D柱形图
本文主要讲解如何使用python绘制三维的柱形图,如下图 源代码如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 2 ...
- QT绘制带有数据源的图表
QT绘制带有数据源的图表 项目简介 项目技术 项目展示 主要源码片段解析 获取完整项目源码传送门 项目简介 QML将XmlListModel用作图表的数据源. 项目技术 qt5.12,qt chart ...
- QT绘制散点图(1)
QT绘制散点图1 项目简介 项目技术 项目展示 主要源码片段解析 获取完整项目源码传送门 项目简介 创建一个简单的散点图以及如何与该图进行交互. 项目技术 qt5.12,qt charts模块,C++ ...
- QT绘制散点图(2)
QT绘制散点图2 项目简介 项目技术 项目展示 主要源码片段解析 获取完整项目源码传送门 项目简介 创建简单的散点图. 项目技术 qt5.12,qt charts模块,C++ 项目展示 主要源码片段解 ...
- QT绘制嵌套的圆饼状图
QT绘制嵌套的圆饼状图 项目简介 项目技术 项目展示 主要源码片段解析 获取完整项目源码传送门 项目简介 使用QPieSeries API创建嵌套的甜甜圈图. 项目技术 qt5.12,qt chart ...
- QT绘制百分比条形图。
QT绘制百分比条形图 项目简介 项目技术 项目展示 主要源码片段解析 获取完整项目源码传送门 项目简介 创建简单的百分比条形图. 百分比条形图将数据集中显示为每个类别中所有数据集的百分比. 创建百分比 ...
最新文章
- 新手熊猫烧香学习笔记
- Hadoop前期准备--centos7
- 邮件MIME格式分析
- CloudFoundry命令行安装和卸载插件
- 动态规划--重拾我的“背包”
- jsonp react 获取返回值_必须要会的 50 个React 面试题(下)
- html文件设置断点调试,断点调试
- 大数——大数阶乘(hdu1042)
- 图的单源最短路径(Dijkstra算法)
- 【报告分享】2020中国电商直播粉丝价值研究报告.pdf(附下载链接)
- Vaadin介绍与开发练习之二(创建第一个Vaadin类)
- Linux(centos7.4)上FTP服务器搭建(使用yum)
- 强悍的 ubuntu —— ubuntu 与 windows 双系统的交互
- office完全卸载工具
- 如何用计算机管理员权限,怎么打开管理员权限,电脑怎么用管理员权限
- Unity-汽车仿真-1.车库UI菜单滑动功能(利用iTween)
- vue前端项目富文本应用
- 【一文读懂】python 中的 numpy.reshape(a, newshape, order=‘C‘) 详细说明及实例讲解
- Js、 replace 全部内容替换、替换全部匹配内容、替换第一个
- 5、提取snp indel 位点