原创~~作者码字不易,如需转载请注明出处,谢谢~

欢迎来我的博客小站(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绘制一颗好看的二叉树相关推荐

  1. 如何用 OpenGL 绘制雪花?

    作者 | 许向武 责编 | 张红月 出品 | CSDN博客 看冬奥才知道,阿勒泰不但是中国的"雪都",还是"人类滑雪起源地".这个说法是否成立,姑且不论,阿勒泰 ...

  2. 9.如何使用QT绘制导航箭头的图标

    利用QT绘制一个地图导航软件中的导航图标,代码如下 #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include <QLa ...

  3. python画树叶-手把手|如何用Python绘制JS地图?

    原标题:手把手|如何用Python绘制JS地图? 关于转载授授权 大数据文摘作品,欢迎个人转发朋友圈,自媒体.媒体.机构转载务必申请授权,后台留言"机构名称+文章标题+转载",申请 ...

  4. 用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 ...

  5. QT绘制带有数据源的图表

    QT绘制带有数据源的图表 项目简介 项目技术 项目展示 主要源码片段解析 获取完整项目源码传送门 项目简介 QML将XmlListModel用作图表的数据源. 项目技术 qt5.12,qt chart ...

  6. QT绘制散点图(1)

    QT绘制散点图1 项目简介 项目技术 项目展示 主要源码片段解析 获取完整项目源码传送门 项目简介 创建一个简单的散点图以及如何与该图进行交互. 项目技术 qt5.12,qt charts模块,C++ ...

  7. QT绘制散点图(2)

    QT绘制散点图2 项目简介 项目技术 项目展示 主要源码片段解析 获取完整项目源码传送门 项目简介 创建简单的散点图. 项目技术 qt5.12,qt charts模块,C++ 项目展示 主要源码片段解 ...

  8. QT绘制嵌套的圆饼状图

    QT绘制嵌套的圆饼状图 项目简介 项目技术 项目展示 主要源码片段解析 获取完整项目源码传送门 项目简介 使用QPieSeries API创建嵌套的甜甜圈图. 项目技术 qt5.12,qt chart ...

  9. QT绘制百分比条形图。

    QT绘制百分比条形图 项目简介 项目技术 项目展示 主要源码片段解析 获取完整项目源码传送门 项目简介 创建简单的百分比条形图. 百分比条形图将数据集中显示为每个类别中所有数据集的百分比. 创建百分比 ...

最新文章

  1. 新手熊猫烧香学习笔记
  2. Hadoop前期准备--centos7
  3. 邮件MIME格式分析
  4. CloudFoundry命令行安装和卸载插件
  5. 动态规划--重拾我的“背包”
  6. jsonp react 获取返回值_必须要会的 50 个React 面试题(下)
  7. html文件设置断点调试,断点调试
  8. 大数——大数阶乘(hdu1042)
  9. 图的单源最短路径(Dijkstra算法)
  10. 【报告分享】2020中国电商直播粉丝价值研究报告.pdf(附下载链接)
  11. Vaadin介绍与开发练习之二(创建第一个Vaadin类)
  12. Linux(centos7.4)上FTP服务器搭建(使用yum)
  13. 强悍的 ubuntu —— ubuntu 与 windows 双系统的交互
  14. office完全卸载工具
  15. 如何用计算机管理员权限,怎么打开管理员权限,电脑怎么用管理员权限
  16. Unity-汽车仿真-1.车库UI菜单滑动功能(利用iTween)
  17. vue前端项目富文本应用
  18. 【一文读懂】python 中的 numpy.reshape(a, newshape, order=‘C‘) 详细说明及实例讲解
  19. Js、 replace 全部内容替换、替换全部匹配内容、替换第一个
  20. 5、提取snp indel 位点

热门文章

  1. CentOS7本地源yum配置
  2. STM32通用定时器输出带死区互补PWM/任意移相PWM
  3. 从NASA图片发现的“太阳UFO” 近似形状物体
  4. PR字幕怎么去黑色背景
  5. SmartPhone-系统文件篇
  6. js实现小游戏 贪吃蛇
  7. 【CubeMX配置STM32驱动超声波模块(HC-SR04)】
  8. 12306买的票如何报销?可以网上打印吗?
  9. 湖南湘中计算机学校历任校长,2005学年度湖南省中等职业学校.doc
  10. 三个常见博弈游戏以及 SG 函数和 SG 定理