目录
六、使用其他方式创建一个 Hello World 程序(编辑框和按钮方式)
一、项目文件解析
创建一个新qt项目后,会生成几个默认对应的文件
widget.h
注意:这里的文件名在创建时默认为widget.h,我这里只是重命名了这个文件名
- 在QT中如果某个类想要使用信号与槽(signal 和 slot)的机制,那么必须引入Q_OBJECT这个宏,并且要将这个宏写在这个类的前面,这时候QT编译器就允许这个类自定义信号和槽函数;
- Ui::Widget *ui:这个指针是在 namespace Ui 里的 Widget 类里面定义的,并且这个ui 指针实际上是指向可视化设计的界面的,后面要访问这个可视化界面上的控件都是通过这个指针去访问的。
main.cpp
使用QT创建一个任意一个工程过后,main.cpp文件中都会自动生成以下代码:
- Qt系统提供的标准类名声明头⽂件没有 .h 后缀。
- Qt⼀个类对应⼀个头⽂件,类名就是头⽂件名。
- QApplication 为应⽤程序类;QApplication a;(a为应⽤程序对象,有且仅有⼀个。)
3.1 QApplication管理图形⽤⼾界⾯应⽤程序的控制流和主要设置。
3.2 QApplication是Qt的整个后台管理的命脉。它包含主事件循环,在其中来⾃窗⼝系统和其它资源的所有事件处理和调度。它也处理应⽤程序的初始化和结束,并且提供对话管理。
3.3 对于任何⼀个使⽤Qt的图形⽤⼾界⾯应⽤程序,都正好存在⼀个QApplication对象,⽽不论这个应⽤程序在同⼀时间内是不是有0、1、2或更多个窗⼝。 - Widget w:实例化窗口对象。
- w.show():显示窗口对象,与之对应的 w.close() 隐藏窗口对象。
- a.exec():程序进⼊消息循环,等待对⽤⼾输⼊进⾏响应。这⾥main()把控制权转交给Qt,Qt完成事件处理⼯作,当应⽤程序退出的时候exec()的值就会返回。在 exec()中,Qt 接受并处理⽤⼾和系统的事件并且把它们传递给适当的窗⼝部件。
widget.cpp
注意:这里的文件名在创建时默认为widget.cpp,我这里只是重命名了这个文件名
widget.ui
widget.ui 是窗体界⾯定义⽂件,是⼀个XML⽂件,定义了窗⼝上的所有组件的属性设置、布局,及其信号与槽函数的关联等。⽤UI设计器可视化设计的界⾯都由Qt⾃动解析,并以XML⽂件的形式保存下来。在设计界⾯时,只需在UI设计器⾥进⾏可视化设计即可,⽽不⽤管widget.ui⽂件是怎么⽣成的。
.pro文件
⼯程新建好之后,在⼯程⽬录列表中有⼀个后缀为".pro"的⽂件,".pro"⽂件就是⼯程⽂件(project)
,它是 qmake ⾃动⽣成的⽤于⽣产 makefile 的配置⽂件。如图所示:
双击进⼊该⽂件,该⽂件的核⼼内容如下:
- QT +=coregui:Qt 包含的模块
- greaterThan(QT_MAJOR_VERSION, 4): QT+=widgets:⼤于Qt4版本才包含widget模块
- TARGET=QtFirst:应⽤程序名⽣成的 .exe 程序名称
- TEMPLATE=app:模板类型,应⽤程序模板
- SOURCES+=main.cpp\ :源⽂件
- widget.cpp:源⽂件
- HEADERS+=widget.h:头⽂件
“.pro” ⽂件的写法如下:
- 注释:从"#"开始,到这⼀⾏结束。
- QT += coregui:Qt包含的模块 Qt5包含的模块如下图所⽰:
- greaterThan(QT_MAJOR_VERSION,4):QT+=widgets 这条语句的含义是,如果QT_MAJOR_VERSION ⼤于4也就是当前使⽤的Qt5及更⾼版本)需要增加widgets 模块。如果项⽬仅需⽀持Qt5,也可以直接添加"QT+=widgets"⼀句。不过为了保持代码兼容,最好还是按照QtCreator⽣成的语句编写。
- 指定⽣成的应⽤程序名:TARGET=QtDemo
- TEMPLATE=app:模板。告诉qmake为这个应⽤程序⽣成哪种makefile。下⾯是可供选择的模板:
5.1 app:建⽴⼀个应⽤程序的makefile。这是默认值,所以如果模板没有被指定,这个将被使⽤。
5.2 lib:建⽴⼀个库的makefile。
5.3 vcapp:建⽴⼀个应⽤程序的VisualStudio项⽬⽂件。
5.4 vclib: 建⽴⼀个库的VisualStudio项⽬⽂件。
5.5 subdirs:这是⼀个特殊的模板,它可以创建⼀个能够进⼊特定⽬录的makefile并且为它调⽤make的makefile。 - ⼯程中包含的源⽂件:SOURCES+=main.cpp/widget.cpp
- ⼯程中包含的头⽂件:HEADERS+=widget.h
- ⼯程中包含的资源⽂件:RESOURCES+=painter.qrc
- ⼯程中包含的"ui"设计⽂件:FORMS+=widget.ui
- 配置信息:CONFIG+=c++11(使⽤c++11的特性) CONFIG⽤来告诉qmake关于应⽤程序的配置信息。
二、QT 实现Hello World程序
利用 Qt 创建一个Hello World 程序有两种方式。分别是图形化的方式和纯代码的方式。
(一)按钮控件
1. 纯代码
#include "mywidget.h"
#include "ui_mywidget.h"
#include <QPushButton> // 添加按钮对应的头文件
MyWidget::MyWidget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::MyWidget)
{
ui->setupUi(this);
// 在这里编写我们的代码
// 创建一个按钮控件
QPushButton *pushButton = new QPushButton;
// 在这个按钮写一个Hello World
pushButton->setText("Hello World!");
// 将这个按钮控件添加在窗口上
pushButton->setParent(this);
}
MyWidget::~MyWidget()
{
delete ui;
}
效果图:
2. 图形化
将按钮控件拖至屏幕上,双击控件,输入 Hello World。效果图如下:
(二)标签控件
1. 纯代码
#include "mywidget.h"
#include "ui_mywidget.h"
#include <QLabel> // 引入标签控件的头文件
#include <QFont> // 引入字体头文件
MyWidget::MyWidget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::MyWidget)
{
ui->setupUi(this);
// 创建一个标签控件
QLabel *label = new QLabel;
// 在这个标签写上Hello World
label->setText("Hello World");
// 创建一个字体对象
// 将标签上的文字的字体设置成指定格式
QFont font("黑体", 64);
label->setFont(font);
// 将标签这个控件放在窗口上
// 并将这个控件移动到像素坐标为(150, 150)的地方
label->move(150, 150);
label->setParent(this);
}
MyWidget::~MyWidget()
{
delete ui;
}
效果图如下:
2. 图形化
效果图如下:
三、内存泄漏问题
在这段代码中,在 new 了一个对象之后,却没有 delete ,会不会造成内存泄漏?
答案是在 Qt 中不会造成内存泄漏。此处通过 new 的方式创建对象,就是为了把该对象的生命周期交给 Qt 的对象树统一管理。代码中的 label 对象会在合适的时机被析构释放,这个合适的时机是指在窗口关闭的时候。
四、qdebug()的使用
qDebug() 的使用效果和 std::cout << std::endl 的效果一样,但是比后者好。
原因是 qDebug() 会自动处理编码方式,在 Qt 中如果用 cout 打印文字日志,可能会出现乱码,因为编码不同。
如在析构函数中加入打印语句:
Widget::~Widget()
{
std::cout << "析构函数\n";
delete ui;
}
运行后,然后关闭窗口,自动调用析构函数,查看应用程序输出,会看到乱码:
所以通常采用 qDebug() 的方式去打印日志:
#include <QDebug> // 包含这个头文件
Widget::~Widget()
{
qDebug() << "析构函数";
delete ui;
}
qDebug() 还有一个优点就是可以统一打开和关闭,就能方便程序员调试。
在 .pro 文件加入这条代码即可:
DEFINES += QT_NO_DEBUG_OUTPUT
需要 清除 然后重新构建才有效
五、Qt 中的对象树
在Qt中创建很多对象的时候会提供一个parent对象指针,这个parent到底是干什么的呢?
QObject是以对象树的形式组织起来的。
- 当创建一个QObject对象时,会看到QObject的构造函数接收⼀个QObject指针作为参数,这个参数就是parent,即父对象指针。
- 在创建QObject对象时,可以提供⼀个其父对象,创建的这个QObject对象会自动添加到其父对象的children()列表。
- 当父对象析构的时候,这个列表中的所有对象也会被析构。(注意:这里的父对象并不是继承意义上的父类!)。
这种机制在GUI程序设计中相当有用。如,一个按钮有一个QShortcut(快捷键)对象作为其子对象。当删除按钮的时候,这个快捷键理应被删除。这是合理的。
QWidget是能够在屏幕上显示的一切组件的父类。
- QWidget继承自QObject,因此也继承了这种对象树关系。一个孩子自动地成为父组件的⼀个子组件。因此,它会显示在父组件的坐标系统中,被父组件的边界剪裁。如:当用户关闭⼀个对话框的时候,应用程序将其删除,我们希望属于这个对话框的按钮、图标等应该⼀起被删除。因为这些都是对话框的子组件。
- 我们也可以自己删除子对象,它们会自动从其父对象列表中删除。如:当删除了一个工具栏时,其所在的主窗口会自动将该工具栏从其子对象列表中删除,并且自动调整屏幕显示。
对象树机制一定程度上解决了内存问题。
- 当一个QObject对象在堆上创建的时候,Qt会同时为其创建一个对象树。不过对象树中对象的顺序是没有定义的,即销毁这些对象的顺序也是未定义的。
- 任何对象树中的QObject对象delete时,若这个对象有parent,则自动将其从parent的children()列表中删除;若有孩子,则自动delete每一个孩子。Qt保证没有QObject会被delete两次,这是由析构顺序决定的。
若QObject在栈上创建,Qt保持同样的行为。正常情况下也不会发生什么问题:
{
QWidget window;
QPushButton quit("Quit", &window);
}
作为父组件的window和作为子组件的quit都是QObject的子类。这段代码是正确的,quit的析构函数不会被调用两次,因为标准C++要求,局部对象的析构顺序应该按照其创建顺序的相反过程。因此,这段代码在超出作用域时,会先调用quit的析构函数,将其从父对象window的子对象列表中删除,然后才会再调用window的析构函数。
{
QPushButton quit("Quit");
QWidget window;
quit.setParent(&window);
}
上述代码的析构顺序就有了问题。在上面的代码中,作为父对象的window会先被析构,因为它是最后一个创建的对象。在析构过程中,它会调用子对象列表中每一个对象的析构函数,即quit此时就被析构了。然后代码继续执行,在window析构之后,quit也会被析构,因为quit也是⼀个局部变量,在超出作用域的时候当然也需要析构。但是,这时候已经是第⼆次调用quit的析构函数了,C++不允许调用两次析构函数,因此,程序崩溃了:
自定义MyPushButton类观察释放过程
mypushbutton.h:
#ifndef MYPUSHBUTTON_H
#define MYPUSHBUTTON_H
#include <QPushButton>
#include <QDebug>
class MyPushButton : public QPushButton
{
Q_OBJECT
public:
explicit MyPushButton(QWidget *parent = nullptr);
~MyPushButton();//添加MyPushButton类的析构函数
signals:
};
#endif // MYPUSHBUTTON_H
mypushbutton.cpp:
#include "mypushbutton.h"
MyPushButton::MyPushButton(QWidget *parent) : QPushButton(parent)
{
qDebug() << "我的按钮的构造函数被调用";
}
MyPushButton::~MyPushButton()
{
qDebug() << "我的按钮的析构函数被调用";
}
widget.h:
#include "widget.h"
#include "ui_widget.h"
#include "mypushbutton.h"
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
MyPushButton* button = new MyPushButton(this);
//传入this,将其设置到对象树中,当窗口关闭时就会自动调用其析构函数
button->setText("我的按钮");
}
Widget::~Widget()
{
qDebug() << "Widget的析构函数被调用";
delete ui;
}
运行结果如下:
对象树确保的是先释放子节点的内存,后释放父节点的内存。而析构函数的调用顺序则不⼀定遵守上述要求,因此看到子节点的析构执行顺序反而在父节点析构顺序之后。
注意:调用析构函数和释放内存并非是同一件事情。
总结:
Qt的对象树机制虽然在⼀定程度上解决了内存问题,但是也引入了⼀些值得注意的事情。这些细节在今后的开发过程中很可能时不时跳出来烦扰一下,所以最好从开始就养成良好习惯。
注意:在Qt中,尽量在构造的时候就指定parent对象,并且大胆在堆上创建
六、使用其他方式创建一个 Hello World 程序(编辑框和按钮方式)
除了上面使用 label 标签的形式,还可以采用编辑框和按钮的方式。同样分为图形化界面方式和纯代码方式完成。
(一)编辑框
1. 图形化
如下图所示,编辑框分为单行编辑框和多行编辑框,这里选择单行编辑框即可。拖动到编辑界面,然后输入文字:
运行查看效果。编辑框的内容运行后可以自己修改:
2. 纯代码
纯代码方式和之前介绍过的大同小异。首先需要创建一个 QLineEdit 对象,然后设置文字:
效果图如下:
(二)按钮
1. 图形化
在 ui 界面找到 Push Button 然后拖动到图形化编辑界面输入文字:
运行查看效果,可以点击这个按钮:
这个按钮是可以点击的,但是点击了没有反应。如果想要点击了有反应,需要用到信号和槽机制,后面会讲。这里先简单说一下,本质就是给按钮的点击操作,关联一个处理函数,当用户点击的时候,就会执行这个处理函数。
这里需要用到 connect 函数:
- 第一个参数需要访问到 ui 文件中创建的控件,表示哪个控件发出的信号,在 Qt Designer 中创建一个控件的时候,此时就会给这个控件分配一个 objectName 属性,这个属性的值,要求在界面中是唯一的。qmake 在预处理.ui文件的时候,就会根据这里的 objectname 生成对应的C++代码。这个值可以在ui界面手动修改:
- 第二个参数表示发出了个什么信号,这里 clicked 表示点击,当用户点击按钮时就会发出该信号。
- 第三个参数表示谁来处理该信号,将第二个参数的信号能够关联到对应的槽函数上,在此之前需要确认关联到哪个对象的槽函数,填写 this ,关联到 widget 对象的槽函数。
- 第四个参数传一个具体的处理函数,这个函数需要我们自己去写处理方式。我们的处理方式是:点击一次按钮,文本在 Hello World!和Hello Qt! 之间来回切换。代码如下:
void Widget::handClick()
{
if(ui->pushButton->text() == QString("Hello World!"))
{
ui->pushButton->setText("Hello Qt!");
}
else
{
ui->pushButton->setText("Hello World!");
}
}
记得先在头文件声明该函数:
声明以后,快速生成函数定义快捷键:alt + 回车 会自动在 .cpp 文件帮我们处理好一些代码
运行效果如下:
点击一次按钮:
文本内容改变了,再点击一次
点击一次按钮就会在两个文本内容之间来回切换,成功实现我们想要的功能。
2. 纯代码
纯代码方式实现,首先需要先 new 一个 button 对象,为了能让其他成员函数能够访问该变量,需要将该对象定义为成员变量:
具体代码如下,功能如上面所述:
运行查看效果:
点击后:
再次点击:
实际开发以纯代码为主还是图形化界面为主?
这两者都很重要,难分主次。
- 如果当前程序界面,界面内容是比较固定的,此时就会以图形化界面的方式来构造界面。
- 如果当前界面,经常要动态变化,此时就会以代码的方式来构造界面。
- 这两种方式,哪种方便就用哪种,而且这两种方式可以配合使用。
七、关于 Qt 中的命名规范 / 快捷键 / 查询文档
命名规范
- 类名:首字母大写,单词和单词之间首字母大写,也就是大驼峰命名法。如:QPushButton
- 函数名:首字母小写,单词和单词之间首字母大写,也就是小驼峰命名法。如:setText
快捷键
- 注释:ctrl+ /
- 运行:ctrl+ R
- 编译:cttrl+ B
- 字体缩放:ctrl +鼠标滑轮
- 查找:ctrl + F
- 整行移动:ctrl+shift+ ↑/↓
- 帮助文档:F1
- 自动对齐:ctrl+ i;
- 同名之间的.h和.cpp的切换:F4
- 生成函数声明的对应定义:alt + enter
使用帮助文档
打开文档一共有三种方法
- 光标放到要查询的类名/方法名上,直接按F1
- Qt Creator左侧边栏中直接用鼠标单击"帮助"按钮。
- 找到Qt Creator的安装路径,在"bin"文件夹下找到assistant.exe,双击打开。
八、Qt 窗口坐标系
坐标体系:以左上角为原点(0,0),X向右增加,Y向下增加
对于嵌套窗口,其坐标是相对于父窗口来说的:
比如图中 QPushButton 的父窗口就是 QWidget,原点就是QWidget 左上角。QWidget没有父元素,相当于父元素就是整个显示器,原点就是显示器左上角。
上面我们提到过,以纯代码的方式创建一个这样一个程序,按钮会默认在左上角,也就是原点(0,0)处,这里的位置是可以通过设置改变的:
#include <QPushButton>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
QPushButton* button = new QPushButton(this);
button->setText("按钮"); // 如果不设置,默认位置就是父窗口的(0,0)坐标
button->move(200, 300);
}
如果想要给控件设置位置就要使用 move() 方法:
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
QPushButton* button = new QPushButton(this);
button->setText("按钮"); // 如果不设置,默认位置就是父窗口的(0,0)坐标
button->move(200, 300);
}
这里move的第一个参数就是X坐标,第二个参数就是Y坐标,而参数的单位就是像素,显示器本身就是一大堆可以发光的亮点:
电脑屏幕设置中的分辨率:在水平方向上有1920个像素,垂直方向上有1080个像素,显示器像素越大,画面越好。
把这个按钮相对于原点水平移动200像素,垂直移动300像素,效果如下:
还可以设置窗口弹出时的位置,设置为弹出为显示器的原点处:
查看效果,在窗口弹出后位置在显示器左上角: