如果安装好了Qt相关的内容,Clion似乎会自动的在建立新项目的时候提供Qt的选项(时间久远,有些忘记了,如果有对此比较明白的可以评论一下)我们可以开一个部件类Qt程序来开始我们的GUI编程之旅。
(→第一行的Qt Console Executable是Qt风格的控制台程序,不会有窗口出现,也不是这份教程的重点)
按照Clion的一贯传统,新建项目的文件都是可以直接运行,来验证自己的环境是否正确的。实际上这个程序确实可以直接点击运行:
(居然将Hello world! 写进按钮的文本里,真是自由)
而这份最简单的程序只由两个文件组成:CMakeList和main.cpp(cmake-build-debug文件夹一般情况下不需要我们去进行修改,程序的关键还是外面两个文件,虽说如此,但是辛苦生成的这个文件夹删了程序也跑不了)
我们先放下CMakeLists.txt不管,去观察主函数文件,可以看到这个程序对应的代码量是相当简洁的,但是为了实现我们想要的功能,这样是不够的。
Clion生成的初始CMakeLists文件的解析
#include
#include
int main(int argc, char *argv[]) {
QApplication a(argc, argv);
QPushButton button("Hello world!", nullptr);
button.resize(200, 100);
button.show();
return QApplication::exec();
}
QApplication是一个单例(通过return中调用的静态函数就能大致猜测到),是Qt的标准应用程序类。在第一行定义的实例a就是应用程序对象。在调用其exec方法后,就会开始应用程序的消息循环和事件处理。show函数虽然是表示显示该窗口的意思,但是在开启事件处理之前,都不会真的将窗口显示出来,这也是在Debug到show之后,没有任何窗口出现的原因。
接下来我们以main函数为基础,稍加修改,在不增添文件的基础上实现一个可以实现切换字体是否加粗,斜体,下划线功能的窗口。
(可以先不具体追究每个功能如何实现,先总体感受整体是如何实现的)
//Filename:main.cpp
#include
#include
//#include
#include
#include
#include
class MyQDialogTutorial : public QDialog
{
Q_OBJECT
private:
QCheckBox *chkBoxUnder;
QCheckBox *chkBoxItalic;
QCheckBox *chkBoxBold;
QPlainTextEdit *txtEdit;
void iniUI();//UI 创建与初始化
void iniSignalSlots();//初始化信号与槽的链接
private slots:
void on_chkBoxUnder(bool checked); //Underline 的clicked(bool)信号的槽函数
void on_chkBoxItalic(bool checked);//Italic 的clicked(bool)信号的槽函数
void on_chkBoxBold(bool checked); //Bold 的clicked(bool)信号的槽函数
public:
MyQDialogTutorial(QWidget *parent = 0);
~MyQDialogTutorial();
};
#include "main.moc"
void MyQDialogTutorial::iniUI()
{
//创建 Underline, Italic, Bold三个CheckBox,并水平布局
chkBoxUnder=new QCheckBox(tr("Underline"));
chkBoxItalic=new QCheckBox(tr("Italic"));
chkBoxBold=new QCheckBox(tr("Bold"));
QHBoxLayout *HLay1=new QHBoxLayout;
HLay1->addWidget(chkBoxUnder);
HLay1->addWidget(chkBoxItalic);
HLay1->addWidget(chkBoxBold);
//创建 文本框,并设置初始字体
txtEdit=new QPlainTextEdit;
txtEdit->setPlainText("Hello world\n\nIt is my demo");
QFont font=txtEdit->font(); //获取字体 其实就是默认字体咯
font.setPointSize(20);//修改字体大小为20
txtEdit->setFont(font);//设置字体
//创建 垂直布局,并设置为主布局
QVBoxLayout *VLay=new QVBoxLayout;
VLay->addLayout(HLay1); //添加字体类型组
VLay->addWidget(txtEdit);//添加PlainTextEdit
setLayout(VLay); //设置为窗体的主布局
}
void MyQDialogTutorial::iniSignalSlots()
{
//三个字体设置的 QCheckBox 的clicked(bool)事件与 相应的槽函数关联
connect(chkBoxUnder,SIGNAL(clicked(bool)),this,SLOT(on_chkBoxUnder(bool)));//
connect(chkBoxItalic,SIGNAL(clicked(bool)),this,SLOT(on_chkBoxItalic(bool)));//
connect(chkBoxBold,SIGNAL(clicked(bool)),this,SLOT(on_chkBoxBold(bool)));//
}
void MyQDialogTutorial::on_chkBoxUnder(bool checked)
{
QFont font=txtEdit->font();
font.setUnderline(checked);
txtEdit->setFont(font);
}
void MyQDialogTutorial::on_chkBoxItalic(bool checked)
{
QFont font=txtEdit->font();
font.setItalic(checked);
txtEdit->setFont(font);
}
void MyQDialogTutorial::on_chkBoxBold(bool checked)
{
QFont font=txtEdit->font();
font.setBold(checked);
txtEdit->setFont(font);
}
MyQDialogTutorial::MyQDialogTutorial(QWidget *parent)
: QDialog(parent)
{
iniUI(); //界面创建与布局
iniSignalSlots(); //信号与槽的关联
setWindowTitle("Form created manually");//设置窗体标题
}
MyQDialogTutorial::~MyQDialogTutorial()
{
}
int main(int argc, char *argv[]) {
QApplication a(argc, argv);
MyQDialogTutorial w;
w.show();
return QApplication::exec();
}
现在实现这个功能相关的所有函数已经聚合在这样一个文件里(尽管之后会火速改正这个坏习惯,将代码分散在更多的文件中会让编程变得更加舒适,以及一些有助于令CMake实现AUTOMOC功能的其他的好处)
如果不出什么意外,运行会得到这样一个窗口,随着上面可选框的不同选择会有不同的炫酷效果(也不是很炫酷)
下面来分析一下这些代码:
首先定义了一个名为MyQDialogTutorial的类,继承自Dialog父类(意为对话框),以及配套的构造函数和析构函数(尽管析构函数什么都没有写)
第一行的宏定义Q_OBJECT是使用Qt中关键的技术—信号与槽的重要标志,只有加入了Q_OBJECT,才能使用QT中的signal和slot机制。
Q_OBJECT都做了什么工作
接下来在private部分,首先声明了界面上各个组件的指针变量,之后会在适当的地方进行创建和布局(也就是函数iniUI中)。然后自定义了两个函数,iniUI()函数用来创建所有组件,并且完成布局和属性设置,而iniSignialSlots()用来完成信号与槽函数的关联。
在private slots部分则声明了三个槽函数,分别对应下划线,斜体,加粗三个功能。
#include “main.moc” 必须放在类MyQDialogTutorial定义完成之后(即使是最后一行也无所谓),否则编译时会出现一些关于moc的错误。
后面就是函数的具体实现,可以说函数的名字都相当易懂,结合英文翻译可以看个七七八八,这里挑出里面比较重要的部分讲一下:
1.作为一个GUI界面,布局的规划是无法绕开的话题,这里使用了盒子布局,将元件横向竖向组合,最后选择最外层的布局作为整个对话框的布局(setLayout),这样就实现了一个很有弹性的布局(在运行后试着手动拖动对话框的大小,其中的控件也会自动变化位置。
2.在访问控件时,因为控件是在类内定义出来的,直接使用定义的指针访问即可。(这一点会与UI实现方法有所区分)
3.信号与槽的绑定是非常有趣的部分,也有各种各样的重载,现在先按照这个形式对照着写,之后会有更详细的解释。
小结一下,使用代码实现Qt的各种功能是最为自由的方式,但在布局的时候会有些让人不太舒适。
但实际上写在一个文件里只是为了让各位看到Qt本身并不是非常复杂的内容,实际上无论是普通的C++编程还是QtCreater(最主流的Qt编程软件),都不会建议各位把所有代码写在一个文件里,我们将代码拆分到三个文件里,会变得更加清晰整洁。
在项目主文件夹右键→New→C/C++ Source File 用这种方法创建一份C++源文件:
勾选上Create an associated header生成一个头文件。(这里的Name可以随便起一个)
(注意这里勾选的Add to targets 将CMakeList.txt文件进行了一定的修改,就是将新添加的两个文件的文件名追加在标绿行括号内部的后边)
然后将代码分到三个文件中:
//Filename:MyQtDialogFirst.h
#ifndef EXAMPLE2_1_MYQTDIALOGFIRST_H
#define EXAMPLE2_1_MYQTDIALOGFIRST_H
#include
#include
#include
class MyQDialogTutorial : public QDialog
{
Q_OBJECT
private:
QCheckBox *chkBoxUnder;
QCheckBox *chkBoxItalic;
QCheckBox *chkBoxBold;
QPlainTextEdit *txtEdit;
void iniUI();//UI 创建与初始化
void iniSignalSlots();//初始化信号与槽的链接
private slots:
void on_chkBoxUnder(bool checked); //Underline 的clicked(bool)信号的槽函数
void on_chkBoxItalic(bool checked);//Italic 的clicked(bool)信号的槽函数
void on_chkBoxBold(bool checked); //Bold 的clicked(bool)信号的槽函数
public:
MyQDialogTutorial(QWidget *parent = 0);
~MyQDialogTutorial();
};
#endif //EXAMPLE2_1_MYQTDIALOGFIRST_H
//Filename:MyQtDialogFirst.cpp
#include "MyQtDialogFirst.h"
#include
void MyQDialogTutorial::iniUI()
{
//创建 Underline, Italic, Bold三个CheckBox,并水平布局
chkBoxUnder=new QCheckBox(tr("Underline"));
chkBoxItalic=new QCheckBox(tr("Italic"));
chkBoxBold=new QCheckBox(tr("Bold"));
QHBoxLayout *HLay1=new QHBoxLayout;
HLay1->addWidget(chkBoxUnder);
HLay1->addWidget(chkBoxItalic);
HLay1->addWidget(chkBoxBold);
//创建 文本框,并设置初始字体
txtEdit=new QPlainTextEdit;
txtEdit->setPlainText("Hello world\n\nIt is my demo");
QFont font=txtEdit->font(); //获取字体 其实就是默认字体咯
font.setPointSize(20);//修改字体大小为20
txtEdit->setFont(font);//设置字体
//创建 垂直布局,并设置为主布局
QVBoxLayout *VLay=new QVBoxLayout;
VLay->addLayout(HLay1); //添加字体类型组
VLay->addWidget(txtEdit);//添加PlainTextEdit
setLayout(VLay); //设置为窗体的主布局
}
void MyQDialogTutorial::iniSignalSlots()
{
//三个字体设置的 QCheckBox 的clicked(bool)事件与 相应的槽函数关联
connect(chkBoxUnder,SIGNAL(clicked(bool)),this,SLOT(on_chkBoxUnder(bool)));//
connect(chkBoxItalic,SIGNAL(clicked(bool)),this,SLOT(on_chkBoxItalic(bool)));//
connect(chkBoxBold,SIGNAL(clicked(bool)),this,SLOT(on_chkBoxBold(bool)));//
}
void MyQDialogTutorial::on_chkBoxUnder(bool checked)
{
QFont font=txtEdit->font();
font.setUnderline(checked);
txtEdit->setFont(font);
}
void MyQDialogTutorial::on_chkBoxItalic(bool checked)
{
QFont font=txtEdit->font();
font.setItalic(checked);
txtEdit->setFont(font);
}
void MyQDialogTutorial::on_chkBoxBold(bool checked)
{
QFont font=txtEdit->font();
font.setBold(checked);
txtEdit->setFont(font);
}
MyQDialogTutorial::MyQDialogTutorial(QWidget *parent)
: QDialog(parent)
{
iniUI(); //界面创建与布局
iniSignalSlots(); //信号与槽的关联
setWindowTitle("Form created manually");//设置窗体标题
}
MyQDialogTutorial::~MyQDialogTutorial()
{
}
//Filename:main.cpp
#include
#include "MyQtDialogFirst.h"
int main(int argc, char *argv[]) {
QApplication a(argc, argv);
MyQDialogTutorial w;
w.show();
return QApplication::exec();
}
如此一来,三个文件分工明确
main.cpp主管创建应用程序,创建显示窗口,运行应用程序,开始应用程序的消息循环和事件处理。
MyQtDialogFirst.h则是声明了MyQDialogTutorial类(尽管大部分情况下,头文件的名字和他定义的类的名字应该是相对应的且文件名使用全小写下划线,类名每个单词首字母大写,但这里为了表示这并不是一个强制要求进行了区分,之后的代码会尽量按照C++的命名规范进行编写)MyQtDialogFirst.cpp则对MyQDialogTutorial类各个函数的具体实现进行了具体的描述。
如果有细心的读者对比两份代码,会发现第一份代码中提到的#include "main.moc"已经不见了,这应该是进行文件分离带来的好处,但具体是怎么触发的我也不是很清楚…,但这一页大概是有描述到的。
AUTOMOC - CMake 3.22.1 Documentation
虽然Qt允许程序员完全使用C++代码来进行程序设计,但是作为一个GUI的框架,可以预见的在控件相当多的时候,进行控件的定义和布局是非常不人性化的过程,而Qt也提供了一个可视化的界面来帮助设计UI,这就是在安装的时候就提到的——Qt Designer。
我们新创建一个项目Example2_2,Clion对于Qt UI Class的创建依旧有相关的手段进行。(尽管不采取这个手段一个个进行创建也是没有问题的)
在项目主文件夹右键→New→Qt UI Class 用这种方法创建一个Qt UI类:
像这样定义其类名和文件名(并不需要严格按照图片所示来写),命名空间可以先放一放。
然后将main.cpp修改成以下的样子,就可以试着运行了(当然,窗口什么也没有)
#include
#include "my_qt_dialog_second.h"
int main(int argc, char *argv[]) {
QApplication a(argc, argv);
MyQtUI::MyQtDialogSecond A;
A.show();
return QApplication::exec();
}
接下来我们对自动生成的三个文件,以及进行编译时会生成的第四个文件进行依次分析
#ifndef EXAMPLE2_2_MY_QT_DIALOG_SECOND_H
#define EXAMPLE2_2_MY_QT_DIALOG_SECOND_H
#include
namespace MyQtSecond {
QT_BEGIN_NAMESPACE
namespace Ui { class MyQtDialogSecond; }
QT_END_NAMESPACE
class MyQtDialogSecond : public QDialog {
Q_OBJECT
public:
explicit MyQtDialogSecond(QWidget *parent = nullptr);
~MyQtDialogSecond() override;
private:
Ui::MyQtDialogSecond *ui;
};
} // MyQtSecond
#endif //EXAMPLE2_2_MY_QT_DIALOG_SECOND_H
my_qt_dialog_second.h文件是窗体类的头文件,由于我们选择的基类是对话框,所以产生了一个继承自QDialog的MyQtDialogSecond的类。
除了最外层的namespace可以起到隔离的作用之外,其中还有一个短小的namespace Ui包含了一个MyQtDialogSecond类,但在这个命名空间中的MyQtDialogSecond和后文即将定义的MyQtDialogSecond类并不是同一个,这里的类是ui_widget.h中的类,这是Qt为了实现一定的解耦进行的操作
接下来定义的MyQtDialogSecond继承自QDialog,并且使用了宏Q_OBJECT以使用信号与槽,之后就是平常的构造函数和析构函数
后文有一个私有指针的定义,非常细节的使用了前文namespace中的类,指向可视化界面,若要访问界面上的组件,都需要利用这个指针,具体为什么要用这种方法分离可以在ui_my_qt_dialog_second.h的分析中找到
// You may need to build the project (run Qt uic code generator) to get "ui_my_qt_dialog_second.h" resolved
#include "my_qt_dialog_second.h"
#include "ui_my_qt_dialog_second.h"
namespace MyQtSecond {
MyQtDialogSecond::MyQtDialogSecond(QWidget *parent) :
QDialog(parent), ui(new Ui::MyQtDialogSecond) {
ui->setupUi(this);
}
MyQtDialogSecond::~MyQtDialogSecond() {
delete ui;
}
} // MyQtSecond
my_qt_dialog_second.cpp是类MyQtSecond的实现代码,在这个简单的程序里,只有一个构造函数和析构函数,首先在构造函数中运行了其父类的构造函数,然后初始化了指针ui。之后便调用了ui的setupUI(this)方法,这个方法实现了窗口的生成和各种属性的设置,信号与槽的关联,这个ui的具体内容是在ui_my_qt_dialog_second.h中的,现在只需要理解这个ui指针指向了另一个文件中定义的内容就好。(实际上个人感觉不去想太多,直接拿来用编程是不会有障碍的。)
析构函数只是简单的删除了用new方法创建的指针ui
<ui version="4.0">
<class>MyQtSecond::MyQtDialogSecondclass>
<widget class="QDialog" name="MyQtSecond::MyQtDialogSecond">
<property name="geometry">
<rect>
<x>0x>
<y>0y>
<width>320width>
<height>195height>
rect>
property>
<property name="windowTitle">
<string>MyQtDialogSecondstring>
property>
widget>
<resources/>
<connections/>
ui>
my_qt_dialog_second.ui文件本质上就是一个XML文件,定义了窗口上所有组件的属性信息和布局,不建议任何人去尝试深入解读(其实也没什么可解读的)这份xml文件以及用手去修改文件的本体内容,在Qt Designer中进行修改要快捷准确的多。
自动生成ui_my_qt_dialog_second.h 文件的分析
虽然是自动生成的文件,但是也对我们理解程序的运行过程有重要的作用
小结一下,结合Designer完成Qt的布局是较为方便的手段,虽然实际上在Designer中也可以进行信号与槽的设计,但我这里并没有去做实验,大家可以自己自己探索一下。除此之外Designer其实功能也有限,并不能像代码那样实现所有的功能。所以我个人比较喜欢的绑定手法是在代码中去写,而将静态资源通过ui实现,尽量向代码化方向靠拢。