目录
一、项目文件解析
widget.h
main.cpp
widget.cpp
widget.ui
.pro文件
二、QT 实现Hello World程序
(一)按钮控件
1. 纯代码
2. 图形化
(二)标签控件
1. 纯代码
2. 图形化
三、内存泄漏问题
四、qdebug()的使用
五、Qt 中的对象树
六、使用其他方式创建一个 Hello World 程序(编辑框和按钮方式)
(一)编辑框
1. 图形化
2. 纯代码
(二)按钮
1. 图形化
2. 纯代码
七、关于 Qt 中的命名规范 / 快捷键 / 查询文档
八、Qt 窗口坐标系
一、项目文件解析
创建一个新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 WorldpushButton->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 Worldlabel->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像素,效果如下:
还可以设置窗口弹出时的位置,设置为弹出为显示器的原点处:
查看效果,在窗口弹出后位置在显示器左上角: