c++应用实例一:使用Qt框架编写排序算法GUI程序

写在开始

由于前段时间一直在复习c++,并了解到了一个基于c++的开源框架:Qt。它非常适用于上位机软件的开发,对于本人将来的毕业设计十分重要,因此有必要使用Qt做一个练手的小项目,巩固c++学习效果的同时为将来使用Qt做软件开发打下基础,这对毕业设计和丰富工作技能大有裨益。

在很早之前逛知乎时,遇到过一个帖子,原帖地址已经忘记了,那哥们用python做了一个各种排序算法的速度比较程序,具体的实现效果是将一个数列中的每个数映射成长短不一的bar,当然越长放入代表数越大,使用各种排序算法对其排序,最终的效果是bar按照从小到大的顺序排列,形成了斜边呈锯齿状的直角三角形,从成型的速度上就可以看出各种排序算法的效率。

于是我在想是不是我也可以用Qt/c++的方式实现同样的效果呢,便有了之后的设计。


开发工具

Qt 对于linux系统最为友好,但是作为一名微软全家桶的使用者来说,放弃windows的温床实在不容易,因此还是选择了基于windows平台的开发工具。Qt在windows下是一个独立的安装包,可以自行上网下载安装,没什么特别注意的,遇到问题注意扒帖就行。软件安装完成后可使用自带的IDE:Qt creater,但是需要一些额外的配置。

由于对visual studio比较熟悉,我依旧是选择了vs2013+Qt5的组合,需要额外的插件这一部分也蛮简单的,在此就不赘述了,值得注意的是虽然使用了vs作为IDE但是并不是Qt的软件一点都不用,在UI的设计部分使用了Qt软件包中的Qt designer,当然你可以不用这个布局,用更加硬核的方式来设计UI————在vs中使用代码布局,这对于初学者了解其底层的布局原理很有好处,在我知道可以混用Qt designer 与vs之前一直使用这种方式,在转用布局软件设计UI时布局会更加科学。


整体思路

整个Ui的设计比较简单,以4种排序算法为例,需要四副pixMap显示bars的排布情况,一个排序开始按钮,具体布局如下图所示:

图中四个lael均为其设置pixMap用于显示画出的bars,这四幅图分别代表四种排序算法:冒泡排序、选择排序、希尔排序和插入排序,其中冒泡排序、选择排序和插入排序的时间复杂度一致,希尔排序的时间复杂度最小,因此理论上四种方法中希尔排序应当最快,其次三种应该一致,但是真正的花费时间与实际的数据相关,有些排序算法耗费时间不是稳定的。

由于程序运行的顺序是串行的若将四种排序算法写在同一个线程中,代码的位置与执行的先后密切相关,因此该问题是一个典型的多线程编程问题,四种排序算法各占用一个线程,各自处理相同规模的数据,排序的结果同步更新到各自的pixMap中。

开始排序的按键需要测试者手动点击,这个按键的click()信号与槽函数连接,开启四个线程,开启单个线程的函数执行所需时间较短,因此放在一个线程中依次执行对最终的结果影响不大。

为了实现排序过程的动画效果演示,在主线程中启用了一个定时器timer,每100ms刷新一次排序结果在label中显示,在定时事件的函数中执行的是重绘命令,因此触发重绘事件,重绘事件的触发函数中进行bars的绘制。

经过以上步骤可以实现四幅排序动画的实现,为了延长排序的时间,选择数据类型为double,vector长度为4*5000。

下面将介绍核心的几个部分:vs+QtDesigner的UI设计方式、使用QLabel和QPainter进行简单的绘图、多线程编程。


使用vs+QtDesigner设计UI

在vs2013中安装 QT VS TOOLS插件之后可以像新建MFC项目一样建立Qt项目,新建项目完成之后在右侧的“解决方案资源管理器”自带一个.ui文件,双击这个.ui文件应该可以打开Qt designer并在里面编辑布局,若不能打开的话可以参考这篇帖子

双击成功打开后,可以使用类似于c#、VB等拖拽的方式布局控件,还可以在右侧的工具栏修改控件的值、显示名称、大小位置等属性,在本例中拖拽4个label调整到合适大小,这里为305*250,删除显示文字,用于显示图片。每个label另搭配一个显示文本的label用于标记排序算法,选中每一对控件使用垂直布局。四个组合体再使用栅格布局,这样四个窗口呈“田”字配置。

在“田”字下方再拖拽一个启动按钮,这样整个UI布局就呈现出上一节中的布局样式。另外在Qt designer中还可以配置信号和槽函数,鼠标点击上方工具栏中的 “编辑信号和槽”按钮,如下图所示:

此时之前编辑好的UI文件会改变样子点击下方的button拖出一条类似于接地的信号线,并弹出一个配置链接对话框:

左侧为信号右侧为槽函数,在右侧点击 “编辑”按钮,新建一个槽函数,名字可以自取,之后新建的槽函数会出现在右侧。在左侧信号栏中选择clicked()信号,右侧选择刚定义的槽函数,就OK了。至此在Qt designer中的操作全部完成,下面转入vs2013编写代码。

在vs左侧的资源管理器中生成了与项目名对应的c++类,在头文件中添加槽函数声明,函数名与QT Designer中的一致。

1
2
private slots:
void slotSortPushButon();

在源文件中添加函数实现,即开启排序子线程,具体内容在多线程编程小节中介绍。

1
2
3
4
void SortGUI::slotSortPushButon()
{
...
}


使用QPainter进行简单的绘图

Qt中的绘图方法有很多,还可以使用功能丰富的外挂库比如qcustomplot库等,由于本例中绘制的内容比较单一,元素比较少因此选用了基本的Qpainter+QPixMap来绘制图片。
首先在添加QPixMap和QPainter类成员:

1
2
3
private:
QPixmap *pixMap[4]; //4副图像显示4种排序算法
QPainter *pixPainter[4]; //4副图像对应4支画笔

在构造函数中为成员分配内存,并与之前的4个label绑定。

1
2
3
4
5
6
7
8
9
pixMap[0] = new QPixmap(ui.win1->size());  //定义图像1
pixMap[1] = new QPixmap(ui.win2->size()); //定义图像2
pixMap[2] = new QPixmap(ui.win3->size()); //定义图像3
pixMap[3] = new QPixmap(ui.win4->size()); //定义图像4

pixPainter[0] = new QPainter(pixMap[0]); //定义画笔1
pixPainter[1] = new QPainter(pixMap[1]); //定义画笔2
pixPainter[2] = new QPainter(pixMap[2]); //定义画笔3
pixPainter[3] = new QPainter(pixMap[3]); //定义画笔4

注意,自动生成的ui_**.h为与Qt Designer中的布局文件对应的UI类头文件,在主窗口类中已经自动生成了一个ui类,默认命名为ui,可以通过ui.控件成员->成员函数()的方式访问ui布局文件中的空间成员,如上述代码中,win1-4就是布局文件中用于显示图片的4个label,使用label控件的setPixmap函数可以将QPixmap与QLabel绑定显示:

1
2
3
4
ui.win1->setPixmap(*pixMap[0]);
ui.win2->setPixmap(*pixMap[1]);
ui.win3->setPixmap(*pixMap[2]);
ui.win4->setPixmap(*pixMap[3]);

为了实现动态刷新,启用Qt中的定时事件中断和重绘事件中断,在主窗口类的头文件中添加成员函数:

1
2
void paintEvent(QPaintEvent *);  //重绘事件
void timerEvent(QTimerEvent *event); //定时器触发事件

在构造函数中开启一个定时器:

1
int timerID = this->startTimer(10);  //10ms触发一次

其中startTimer输入参数为触发时间单位为ms,返回值为定时器的序号,可以使用这个ID调用killTimer(timer ID);关闭定时器。在定时器触发的函数中可以继续触发重绘事件,当空间被遮挡或覆盖时就会触发重绘,也可以调用update()强行触发重绘,在这里只要在定时中断函数里调用setPixmap函数就可以了。

1
2
3
4
5
6
7
void SortGUI::timerEvent(QTimerEvent *event)
{
ui.win1->setPixmap(*pixMap[0]);
ui.win2->setPixmap(*pixMap[1]);
ui.win3->setPixmap(*pixMap[2]);
ui.win4->setPixmap(*pixMap[3]); //这里貌似有点问题,重绘被触发了4次
}

其实对于本例来说不用重绘事件也可以,之间在定时器触发的函数中绘制图像即可,但是在Qt中更为常用的方式还是使用重绘事件,这里要注意一点,千万不要再paintEvent函数中调用update函数或setPixmap函数此时会发生可怕的递归,体现为cpu占用异常高。

在paintEvent函数中绘制具体的图像,以绘制单个bar为例:

1
2
3
4
5
6
void SortGUI::drawBar(double x, int y, int width,int height,QPainter *painter)
{
QPen pen(Qt::blue); //设置画笔蓝色
painter->setPen(pen);
painter->drawRect(x, y, width, height); //绘制矩形
}

使用painter绘制矩形之前要先为其设置画笔或画刷,使用drawRext函数绘制矩形形参为矩形的左上角坐标、高度、宽度,绘制其他形状如椭圆等方法类似。

接下来要做的就是把数据映射成等宽、高度不同的bars,然后依次绘制就行了,这一步比较简单,在此就不再赘述了。

Qt中的多线程编程

Qt中自带一个多线程的类QThread,我们在定义自己的多线程时,需要先添加一个类继承QThread,再重新实现父类的纯虚函数run()。

1
2
3
4
class sortThread : public QThread
{
...
}

这里要注意,子线程与主线程(即显示窗口)共享全局变量数据,理应使用互斥锁或信号量进行变量同步,但是在这里主线程仅仅是读取数据的值并根据这个值绘制图形,并没有改变实际数据的值,因此在这里没有进行互斥体或信号量的操作(这一部分还没有彻底搞明白)。这里为线程类添加了一个数据标号,用于辨别对哪一组数据进行排序,也代表了不同的排序算法,在run()函数中则根据这个标号实行具体的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void sortThread::run()
{
if (threadStop == false)
{
switch (dataNumber)
{
case (0) :
BubbleSort(); //冒泡排序
stop();
break;
case(1) :
SelectionSort(); //选择排序
stop();
break;
case(2) :
ShellSort(); //希尔排序
stop();
break;
case(3) :
InsertSort(); //插入排序
stop();
break;
default:
break;
}
}
}

自定义的线程类实现完成之后要在主窗口类中添加4个该类成员,并在开始按钮点击的槽函数中开启多线程:

1
2
3
4
5
6
7
void SortGUI::slotSortPushButon()
{
subThread1->start();
subThread2->start();
subThread3->start();
subThread4->start();
}

执行start函数后子线程就开启了,线程中会自动的调用run()函数,记得在析构函数中调用stop函数安全的结束线程,并调用wait函数等待程序结束。其中stop函数为开发者自行添加,内容为将threadStop标志位置位。

1
2
3
4
void sortThread::stop()
{
threadStop = true;
}

结果与总结

下面贴出几张程序运行结果的截图,由于动图和视频比较麻烦这里只张贴静态图像:
乱序数据:

排序中间结果:

最终放入排序结果:

最终的结果显示理论的时间复杂度最低的希尔排序速度最快,冒泡排序和选择排序次之,且速度接近,插入排序速度最慢,这可能与实际的数据分布情况相关。具体的项目源码可以参看我的仓库

关于C++/Qt的应用实例还会持续更新,争取每次都有新的内容吧,中间还会穿插一些别的内容,最近沉迷于刀塔自走棋将来做一期关于游戏的内容也说不好呦!