Qt——实现”Hello World“、认识对象树与Qt坐标系
在创建项目时,使用的基类Base Class
为QWidget
1. 使用图形化界面的方式实现“Hello World”
- 双击文件:
widget.ui
,进入designer
模式:
-
在“控件盒子”的“Display Widgets”中找到“Label”,并拖放到白板中
-
双击刚刚拖放到
Label
,输入"Hello World"
-
最后直接运行即可:
此时我们回过头来看widget.ui
和编译生成的ui_widget.h
文件,都发生了变化:
// widget.ui
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0"><class>Widget</class><widget class="QWidget" name="Widget"><property name="geometry"><rect><x>0</x><y>0</y><width>800</width><height>600</height></rect></property><property name="windowTitle"><string>Widget</string></property><widget class="QLabel" name="label"><property name="geometry"><rect><x>230</x><y>110</y><width>81</width><height>51</height></rect></property><property name="text"><string>"Hello World"</string></property></widget></widget><resources/><connections/>
</ui>// ui_widget.h
/********************************************************************************
** Form generated from reading UI file 'widget.ui'
**
** Created by: Qt User Interface Compiler version 6.7.3
**
** WARNING! All changes made in this file will be lost when recompiling UI file!
********************************************************************************/#ifndef UI_WIDGET_H
#define UI_WIDGET_H#include <QtCore/QVariant>
#include <QtWidgets/QApplication>
#include <QtWidgets/QLabel>
#include <QtWidgets/QWidget>QT_BEGIN_NAMESPACEclass Ui_Widget
{
public:QLabel *label;void setupUi(QWidget *Widget){if (Widget->objectName().isEmpty())Widget->setObjectName("Widget");Widget->resize(800, 600);label = new QLabel(Widget);label->setObjectName("label");label->setGeometry(QRect(230, 110, 81, 51));retranslateUi(Widget);QMetaObject::connectSlotsByName(Widget);} // setupUivoid retranslateUi(QWidget *Widget){Widget->setWindowTitle(QCoreApplication::translate("Widget", "Widget", nullptr));label->setText(QCoreApplication::translate("Widget", "\"Hello World\"", nullptr));} // retranslateUi};namespace Ui {class Widget: public Ui_Widget {};
} // namespace UiQT_END_NAMESPACE#endif // UI_WIDGET_H
2. 用代码的方式实现“Hello World”
一般来说,在实现图形化界面时,一般在继承类的构造函数中进行编写,如这里的widget.h
实现如下:
#include "widget.h"
#include "./ui_widget.h"
#include <QLabel>Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget)
{ui->setupUi(this);QLabel* label = new QLabel(this);label->setText("\"Hello World\"");
}Widget::~Widget()
{delete ui;
}
-
Qt中每个类都有其同名的头文件,要使用类
QLabel
,就需要包含头文件<QLabel>
-
在定义
QLabel
对象时,一般建议在堆上创建,同时建议将这个对象挂到对象树
上,其父节点就是this
-
方法
label->setText
的形参实际上是类型QString
- Qt的诞生时间比C++标准形成的时间要早,因此,对于一些常见的数据结构,Qt自己造了一些轮子
- 例如:
QString
、QVector
、QList
、QMap
等 - C++标准库中的容器也可以很方便的和Qt中的容器类相互转换
最后运行结果如图:
内存泄露问题
可以注意到,为了使用QLable
对象,我们只用了new
语句:QLabel* label = new QLabel(this);
我们在后面没有delete
掉,不就内存泄露了吗?
这里可以告诉大家结论:
- 上述代码在Qt中,不会产生内存泄露, 及在合适的时候 **,**
label
会被自动释放 - 而原因就是,
label
对象被挂到对象树上了
对象树
引入对象树的主要目的,就是将诸如QLable
这样的空间进行统一管理,这样就可以统一在合适的时候进行释放
- 所谓合适的时候,就是图形界面窗口关闭/销毁的时候
- 而如果不适用对象树,就可能导致一些资源的提前释放,具体到图形化界面中就可能导致一些控件元素不会正常显示
- 在上面的代码中,将
QLabel
对象开辟在堆空间上,就是为了将其生命周期交给对象树进行管理
而如果开辟在栈上,就可能引发问题:
#include "widget.h"
#include "./ui_widget.h"
#include <QLabel>Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget)
{ui->setupUi(this);// QLabel* label = new QLabel(this);// label->setText("\"Hello World\"");QLabel label(this);label.setText("\"Hello World\"");
}Widget::~Widget()
{delete ui;
}
运行:
发现预期的"Hello World"
不见了
为了验证如果一个对象被挂到对象树上,会被自动清理,我么可以新建一个类myQLabel
,其继承于QLabel
- 新建文件
-
选择C++中的
C++ class
-
填写自定义类的信息
代码如下:
// myQLabel.h
#ifndef MYQLABEL_H
#define MYQLABEL_H#include <QLabel>class myQLabel : public QLabel
{
public:myQLabel(QWidget* parent);~myQLabel();
};#endif // MYQLABEL_H// myQlabel.cpp
#include "myqlabel.h"#include <iostream>myQLabel::myQLabel(QWidget* parent): QLabel(parent) {}myQLabel::~myQLabel()
{std::cout << "MyQLabel 被释放" << std::endl;
}// widget.cpp
#include "widget.h"
#include "./ui_widget.h"
// #include <QLabel>
#include "myqlabel.h"Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget)
{ui->setupUi(this);myQLabel* label = new myQLabel(this);label->setText("\"Hello World\"");
}Widget::~Widget()
{delete ui;
}
可以看到,在关闭图形化窗口时,会输出:
乱码问题
从上面的输出我们可以看到,本来应该被打印的汉字“被释放”,输出竟然是乱码,这是为什么?
首先要说明结论:乱码之所以出现,其最主要的原因就是编码方式不一致
- 源代码文件的编码格式
- 终端(控制台)的显示编码格式
- 字符串本身的编码处理方式
接下来,我们同样用该清楚,一个汉字到底占几个字节?
- 对于这个问题,我们首先要区分汉字是采用哪种编码方式进行编码的——GBK,UTF-8
- 如果采用GBK方式编码,那一个汉字就占2个字节
- 如果采用UTF-8方式编码,那一个汉字就占2~4个字节,一般为3字节
我们先来查看输出汉字的文件myqlable.cpp
是采用哪种方式进行编码:
- 可以看到,编码方式为UTF-8
- 而如果显示为
ANSI
,那么编码方式就是GBK
既然myqlabel.cpp
的编码方式为utf8,但输出到控制台却出现乱码,说明终端控制台的编码方式就不是UTF-8
qDebug
为了解决这一问题,我们不是用C++标准的标准输出std::cout
,而采用Qt提供的QDebug
类:
#include "myqlabel.h"#include <QDebug>myQLabel::myQLabel(QWidget* parent): QLabel(parent) {}myQLabel::~myQLabel()
{qDebug() << "MyQLabel 被释放";
}
QDebug
这个类重载了移位运算符<<
,不直接使用类QDebug
;而qdebug
是一个宏,封装了QDebug
对象,我们可以像使用std::cout
一样来使用它- 使用
qDebug()
还有另一个好处,我们可以使用开关来对日志输出进行关闭, 防止日志信息对用户的干扰
3. 使用输入框实现 “Hello World”
3.1 使用图形化界面的方式
-
双击文件
widget.ui
,进入designer
模式后找到控件:Line Edit
-
将其拖放到白板中,双击控件,输入
"Hello World"
-
保存并运行即可
3.2 使用代码的方式
和QLabel
的操作方式一样,将widget.cpp
文件修改如下:
#include "widget.h"
#include "./ui_widget.h"#include <QLineEdit>Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget)
{ui->setupUi(this);QLineEdit* line_edit = new QLineEdit(this);line_edit->setText("\"Hello World\"");
}Widget::~Widget()
{delete ui;
}
运行:
4. 使用按钮的方式实现“Hello World”
4.1 使用图形化界面的方式
- 双击文件
widget.ui
,进入designer
模式后找到控件:Push Button
-
将其拖放到白板中,双击控件,输入
"Hello World"
同时可以看到,在
QObject
中出现了objectName
和对应的值:-
在
Qt Designer
创建一个控件的时候,此时会给这个控件分配一个objectName
属性,这个属性的值要求在界面中是唯一的。我么也可以自己指定这个值 -
当
CMake
在预处理.ui
文件的时候,就会根据objectName
来生成同名的类。例如上面的控件pushButton
被生成的类名就是myPushButton
-
我们可以在生成的
ui_widget.h
中看到这样的代码:
-
-
进入编辑界面,修改
widget.cpp
如下:
// widget.h
#ifndef WIDGET_H
#define WIDGET_H#include <QWidget>QT_BEGIN_NAMESPACE
namespace Ui {
class Widget;
}
QT_END_NAMESPACEclass Widget : public QWidget
{Q_OBJECTpublic:Widget(QWidget *parent = nullptr);~Widget();void clickHandler();private:Ui::Widget *ui;
};
#endif // WIDGET_H//widget.cpp
#include "widget.h"
#include "./ui_widget.h"Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget)
{ui->setupUi(this);connect(ui->myPushButton, &QPushButton::clicked, this, &Widget::clickHandler);
}Widget::~Widget()
{delete ui;
}// 当控件内容为“Hello World”,点击按钮后,就会变为“Hello Qt”
// 当空间内容为“Hello Qt”,点击按钮后,就会变为“Hello World“
void Widget::clickHandler()
{if (ui->myPushButton->text() == "\"Hello World\"") {ui->myPushButton->setText("\"Hello Qt\"");} else {ui->myPushButton->setText("\"Hello World\"");}
}
上述出现的connect
是类QObject
提供的静态函数,其功能是连接“信号”和“槽”,其4个参数的作用分别为:
4.2 使用代码的方式
代码如下:
// widget.h
#ifndef WIDGET_H
#define WIDGET_H#include <QWidget>
#include <QPushButton>QT_BEGIN_NAMESPACE
namespace Ui {
class Widget;
}
QT_END_NAMESPACEclass Widget : public QWidget
{Q_OBJECTpublic:Widget(QWidget *parent = nullptr);~Widget();void clickHandler();private:Ui::Widget *ui;QPushButton* myPushButton_ = nullptr;
};
#endif // WIDGET_H// widget.cpp
#include "widget.h"
#include "./ui_widget.h"Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget), myPushButton_(new QPushButton(this))
{ui->setupUi(this);myPushButton_->setText("\"Hello World\"");connect(myPushButton_, &QPushButton::clicked, this, &Widget::clickHandler);
}Widget::~Widget()
{delete ui;
}void Widget::clickHandler()
{if (myPushButton_->text() == "\"Hello World\"") {myPushButton_->setText("\"Hello Qt\"");} else {myPushButton_->setText("\"Hello World\"");}
}
5. 图形化方式开发和代码方式开发
实际开发中,图形化方式开发界面和以纯代码方式代码界面都是很常用的方法
- 如果界面内容是比较固定的,此时就会使用图形化界面来构造
- 如果界面内容经常要动态变化,就需要以代码的方式进行开发
- 此外,这两种方式也可以配合使用
6. Qt 坐标系
Qt的坐标系是 平面直角坐标系,其X轴从左往右增长,Y轴从上往下增长
坐标系的原点就是屏幕的左上角/窗口的左上角
给Qt的控件指定位置,就需要设置坐标,对于这个控件来说,其原点是相对于父窗口/父控件的
而如果要指定控件或窗口的位置,就需要用到对应的move
成员函数:
Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget), myPushButton_(new QPushButton(this))
{ui->setupUi(this);myPushButton_->setText("\"Hello World\"");myPushButton_->move(200, 300);this->move(100, 200);
}
- move的第一个参数为水平移动距离,单位为像素px
- move的第二个参数为竖直移动距离,单位为像素px
运行如图: