Geometry(几何)
位置和尺寸实际上是四个属性的统称:
x:横坐标
y:纵坐标
width:宽度
height:高度
但在实际开发中,我们通常不会直接使用这些属性,而是通过一系列封装好的方法来获取或修改。对于 Qt 的坐标系,需要注意它是一个“左手坐标系”,其中坐标系的原点是当前元素的父元素的左上角。
API 说明
geometry()
:获取控件的位置和尺寸,返回结果是一个QRect(矩形)
,包含 x、y、width 和 height 属性,其中 x 和 y 是左上角的坐标。setGeometry(QRect)
或setGeometry(int x, int y, int width, int height)
:设置控件的位置和尺寸,可以直接设置一个QRect
,也可以分四个属性单独设置。move只是修改位置,即x,y
Qt 中针对一些几何上的概念也进行了封装:
QPoint:表示一个点;QRect:表示一个矩形;这些属于是小对象。里面的属性非常少,占用控件比较小。C++使用上述对象,通常就会按照值得方式来进行参数得传递!未必要使用指针或引用,又不大!拷贝开销也不大!
代码示例:控制按钮的位置
在界面中拖入五个按钮,其
objectName
分别为pushButton_target
、pushButton_up
、pushButton_down
、pushButton_left
和pushButton_right
,初始位置和大小可以随意。在
widget.cpp
中编写四个按钮的 slot 函数void Widget::on_pushButton_up_clicked() { QRect rect = ui->pushButton_target->geometry(); rect.setY(rect.y() - 5); ui->pushButton_target->setGeometry(rect); } void Widget::on_pushButton_down_clicked() { QRect rect = ui->pushButton_target->geometry(); rect.setY(rect.y() + 5); ui->pushButton_target->setGeometry(rect); } void Widget::on_pushButton_left_clicked() { QRect rect = ui->pushButton_target->geometry(); rect.setX(rect.x() - 5); ui->pushButton_target->setGeometry(rect); } void Widget::on_pushButton_right_clicked() { QRect rect = ui->pushButton_target->geometry(); rect.setX(rect.x() + 5); ui->pushButton_target->setGeometry(rect); }
运行程序后,按下下方的四个按钮,会控制 target
的左上角的位置,对应的按钮整个尺寸也会发生改变。
上述代码中直接设置了 QRect
中的 x 和 y,实际上 QRect
内部存储了左上和右下两个点的坐标,再通过这两个点的坐标差值计算长宽。单纯修改左上坐标会引起整个矩形的长宽发生改变。如果想让整个按钮都移动,可以改为以下代码:
void Widget::on_pushButton_up_clicked()
{
QRect rect = ui->pushButton_target->geometry();
ui->pushButton_target->setGeometry(rect.x(), rect.y() - 5, rect.width(), rect.height());
}
void Widget::on_pushButton_down_clicked()
{
QRect rect = ui->pushButton_target->geometry();
ui->pushButton_target->setGeometry(rect.x(), rect.y() + 5, rect.width(), rect.height());
}
void Widget::on_pushButton_left_clicked()
{
QRect rect = ui->pushButton_target->geometry();
ui->pushButton_target->setGeometry(rect.x() - 5, rect.y(), rect.width(), rect.height());
}
void Widget::on_pushButton_right_clicked()
{
QRect rect = ui->pushButton_target->geometry();
ui->pushButton_target->setGeometry(rect.x() + 5, rect.y(), rect.width(), rect.height());
}
上述代码使用 move
方法也可以实现。
代码示例:一个表白程序
往界面上拖拽两个按钮和一个 Label,其
objectName
分别为pushButton_accept
、pushButton_reject
和label
,控件中文本如下图所示。在
widget.cpp
中添加 slot 函数:void Widget::on_pushButton_accept_clicked() { ui->label->setText("女神快来嘴一个! mua~~"); } void Widget::on_pushButton_reject_pressed() { // 获取窗口的宽度和高度 int width = this->geometry().width(); int height = this->geometry().height(); // 重新生成按钮的位置 int x = rand() % width; int y = rand() % height; // 设置新的位置 ui->pushButton_reject->move(x, y); }
运行程序后,当点击“残忍拒绝”时,按钮会跑开。上述代码使用的是 pressed
(鼠标按下事件)。如果使用 mouseMoveEvent
,效果会更强烈,只要鼠标移动过来,按钮就跑开。不过对应的代码会更复杂(需要自定义类继承自 QPushButton
,重写 mouseMoveEvent
方法),此处暂不展开。
Window Frame (窗口框架)的影响
如果 widget
作为一个窗口(带有标题栏、最小化、最大化、关闭按钮),那么在计算尺寸和坐标时有两种算法:包含 window frame 和不包含 window frame。
其中 x()
、y()
、frameGeometry()
、pos()
、move()
都是按照包含 window frame 的方式来计算的;而 geometry()
、width()
、height()
、rect()
、size()
则是按照不包含 window frame 的方式来计算的。当然,如果一个 widget 不是作为窗口,上述两类方式得到的结果是一致的。
相关 API
API | 说明 |
---|---|
x() |
获取横坐标,计算时包含 window frame |
y() |
获取纵坐标,计算时包含 window frame |
pos() |
返回 QPoint 对象,其中包含 x() 、y() 、setX() 、setY() 等方法,计算时包含 window frame |
frameSize() |
返回 QSize 对象,其中包含 width() 、height() 、setWidth() 、setHeight() 等方法,计算时包含 window frame |
frameGeometry() |
返回 QRect 对象,QRect 相当于 QPoint 和 QSize 的结合体,可以获取 x、y、width、size,计算时包含 window frame 对象 |
width() |
获取宽度,计算时不包含 window frame |
height() |
获取高度,计算时不包含 window frame |
size() |
返回 QSize 对象,其中包含 width() 、height() 、setWidth() 、setHeight() 等方法,计算时不包含 window frame |
rect() |
返回 QRect 对象,QRect 相当于 QPoint 和 QSize 的结合体,可以获取并设置 x、y、width、size,计算时不包含 window frame 对象 |
geometry() |
返回 QRect 对象,QRect 相当于 QPoint 和 QSize 的结合体,可以获取 x、y、width、size,计算时不包含 window frame 对象 |
setGeometry() |
直接设置窗口的位置和尺寸,可以设置 x、y、width、height 或者 QRect 对象,计算时不包含 window frame 对象 |
在 Qt 中,关于位置尺寸 ,提供了很多的 API。
- 有的 API 的位置信息是以 Widget 本体左上角为原点的(不考虑 Window frame)
- 有的 API 的位置信息是以 Window frame 左上角为原点的
认真观察上面的表格,其实这里的 API 中有 frameGeometry
和 geometry
两个就足够完成所有需求了。为什么要提供这么多功能重复的 API 呢?这涉及到 Qt API 的设计理念:尽量符合人的直觉。例如,Qt 的 QVector
中,尾插元素操作有以下方法:push_back
、append
、+=
、<<
,这些方法的效果都是等价的,即使不翻阅文档,单纯凭借直觉也能把代码写对。
代码示例:感受 geometry 和 frameGeometry 的区别
在界面上放置一个按钮。
在按钮的 slot 函数中编写代码:
void Widget::on_pushButton_clicked()
{
QRect rect1 = this->geometry();
QRect rect2 = this->frameGeometry();
qDebug() << rect1;
qDebug() << rect2;
}
执行程序后,可以看到在构造函数中,打印出的 geometry
和 frameGeometry
是相同的;
QRect(0,0 800x600)
QRect(0,0 800x600)
我们是在构造函数中打印日志的,Window Frame 就是构造后添加到对象树的过程中所涉及到的一个关键环节!! 此时还看不出双方的差异,就需要加到更迟一点的环节!--- 第二步
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
// //此处直接针对Widget对象,使用geometry和framegeometry
// QRect rect1 = this->geometry();
// QRect rect2 = this->frameGeometry();
// qDebug()<<rect1;
// qDebug()<<rect2;
QPushButton* button = new QPushButton(this);
button->setText("按钮");
button->move(100,100);
connect(button,&QPushButton::clicked,this,&Widget::handle);
}
Widget::~Widget()
{
delete ui;
}
void Widget::handle()
{
//此处直接针对Widget对象,使用geometry和framegeometry
QRect rect1 = this->geometry();
QRect rect2 = this->frameGeometry();
qDebug()<<rect1;
qDebug()<<rect2;
}
我们创建按钮,点击按钮时,打印的 geometry
和 frameGeometry
则存在差异。--- 第三步
注意
在构造方法中,Widget
刚刚创建出来,还没有加入到对象树中,此时也不具备 Window frame。在按钮的 slot 函数中,由于用户点击的时候,对象树已经构造好了,此时 Widget
已经具备了 Window frame,因此在位置和尺寸上均出现了差异。如果把上述代码修改成打印 pushButton
的 geometry
和 frameGeometry
,结果就是完全相同的,因为 pushButton
并非是一个窗口。
windowTitle(窗口标题)
API 说明
windowTitle()
:获取控件的窗口标题。setWindowTitle(const QString& title)
:设置控件的窗口标题。
注意:上述设置操作针对不同的 widget 可能会有不同的行为。当前 windowTitle 属性,是从属于 QWidget的,QWidget 是一个广泛的概念,一个按钮,一个输入框就是一个 QWidget 了,如果是顶层 widget(独立窗口),这个操作才会有效;如果是子 widget,这个操作没有任何效果。
代码示例:设置窗口标题 修改 widget.cpp
:
1 Widget::Widget(QWidget *parent)
2 : QWidget(parent)
3 , ui(new Ui::Widget)
4 {
5 ui->setupUi(this);
6
7 // 设置窗口标题
8 this->setWindowTitle("这是窗口标题");
9 }
执行效果:窗口标题被设置为“这是窗口标题”。
我们可以继续创建一个按钮控件:
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
this->setWindowTitle("这是窗口标题");
QPushButton* button = new QPushButton(this);
button->setText("按钮");
button->setWindowTitle("通过按钮设置窗口标题");
}
很明显,按钮对窗口标题的设置是没有影响的!
不过没有报错?其实这个设定是不太科学的!这一点,我们更希望当写不不太科学的代码的时候,能够给一些报错提示!
windowIcon(窗口图标)
我们运行一个程序,在窗口和任务栏中就会有相关的图标:不同的程序图标不同:
API 说明
windowIcon()
:获取控件的窗口图标,返回 QIcon 对象。setWindowIcon(const QIcon& icon)
:设置控件的窗口图标。
注意:这两个 API 同 windowTitle
,上述操作仅针对顶层 widget 有效。
代码示例:设置窗口图标
先在 D 盘中放一个图片,名字为
rose.png
。
修改
widget.cpp
:
#include <QIcon>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
//设置图标
// "D:\\for_test\\rose.png"
QIcon icon("D:/for_test/rose.png");//路径不要带中文
//C++11引入了 raw string 解决该问题 --- 字符串里不包含任何转义字符(所有的字符都不会转义)
//r("D:\for_test\rose.png")
this->setWindowIcon(icon);
}
注意:Windows 下路径的分隔符可以使用
/
也可以使用\
,但是如果在字符串中使用\
,需要写作转义字符的形式\\
。因此更推荐使用/
。另外,C++11引入了 raw string 解决该问题 --- 字符串里不包含任何转义字符(所有的字符都不会转义)--- 用法:r("D:\for_test\rose.png")
运行程序,可以看到窗口图标已经成为上述图片,同时程序在任务栏中的图标也发生了改变。
Qt 将各种涉及到的相关概念,都封装成了类,QIcon 就表示一个图标。
我们之前推荐使用堆去创建对象,主要因为要通过 Qt 对象树来释放对象,确保当前控件的生命周期是足够的!QIcon 自身是比较小的对象,创建出来之后,就是要设置到某一个 QWidget 里面,QIcon 对象本身释放不释放,不影响图标最终的显示 -- 可以看成就是给某一个函数调用传参数的参数作用。而且 QIcon 也不支持对象树机制!
实际开发中的注意事项
一般不会在代码中通过绝对路径引入图片,因为无法保证程序发布后,用户的电脑上也有同样的路径。--- 说不定有的没有D盘 --- 不要写死了
如果使用相对路径,则需要确保代码中的相对路径写法和图片实际所在的路径匹配(比如代码中写作
"./image/rose.jpg"
,就需要在当前工作目录中创建image
目录,并把rose.png
放进去)。绝对路径:以盘符(Windows)或者以
/
(Linux)开头的路径。相对路径:以
.
(表示当前路径)或者以..
(表示当前路径上级路径)开头的路径。其中.
经常也会省略。相对路径的前提是需要明确“当前工作目录”。对于 Qt 程序来说,当前工作目录可能是变化的。比如通过 Qt Creator 运行的程序,当前工作目录是项目的构建目录;直接双击 exe 运行,工作目录则是 exe 所在目录。
所谓构建目录,是和 Qt 项目并列的,专门用来放生成的临时文件和最终 exe 的目录。
代码示例:获取当前的工作目录
- 在界面上创建一个比较大的 label,确保能把路径显示完整。
objectName
使用默认的label
即可。 - 修改
widget.cpp
:#include <QDir> Widget::Widget(QWidget *parent) : QWidget(parent) , ui(new Ui::Widget) { ui->setupUi(this); // 获取到当前工作目录 QString currentDir = QDir::currentPath(); // 设置工作目录到 label 中 ui->label->setText(currentDir); }
- 直接在 Qt Creator 中执行程序,可以看到当前工作目录是项目的构建目录。
- 进入上述构建目录,把里面的 exe 拷贝到其他目录中(比如 D: 中)。再次执行程序,可以看到当前工作目录已经发生改变。
- 要想直接能双击 exe 运行,需要先把 Qt 的路径添加到 path 环境变量中,否则会提示找不到动态库。这一点在最开始搭建开发环境的时候已经操作过,此处不再赘述。
- 注意,上述构建目录是随时可删除的。比如点击菜单栏中的“构建”->“清理项目”,就会把这个目录中的内容清空掉。因此如果把图片文件放到构建目录中,可能在不小心删除后就丢失了。我们还是希望能够把图片和源代码放在一起,并且使我们的程序无论拷贝到任何位置中都能正确使用图片。
Qt 的 qrc 资源管理机制
通过 qrc 机制,可以重根本上解决两个问题:
1. 确保图片所在的路径在目标用户机器上存在。
2. 确保图片不会被用户搞没了。
方式:给 Qt 项目中引入一个额外的 xml 文件(后缀使用 .qrc 表示),在这个 xml 中,把要使用的图片资源给导入进来,并且在 xml 中进行记录!
Qt 在编译项目的时候,就会根据 qrc 中的描述的图片信息,找到图片内容,并且提取出图片的二进制数据,把这些二进制数据转化成 C++ 代码,最终编译到 exe 中!
缺点就是 qrc 无法导入太大的资源文件,比如搞几个 GB 的视频文件,qrc 就无能力了!
Qt 使用 qrc 机制帮我们自动完成了上述工作,更方便地来管理项目依赖的静态资源。
qrc 文件:是一种 XML 格式的资源配置文件,它用 XML 记录硬盘上的文件和对应的随意指定的资源名称。应用程序通过资源名称来访问这些资源。
在 Qt 开发中,可以通过将资源文件添加到项目中来方便地访问和管理这些资源。这些资源文件可以位于 qrc 文件所在目录的同级或其子目录下。
在构建程序的过程中,Qt 会把资源文件的二进制数据转成 cpp 代码,编译到 exe 中。从而使依赖的资源变得“路径无关”。
这种资源管理机制并非 Qt 独有,很多开发框架都有类似的机制。例如 Android 的 Resources 和 AssetManager 也是类似的效果。
代码示例:通过 qrc 管理图片作为图标
右键项目,创建一个 Qt Resource File(qrc 文件),文件名随意起(不要带中文),此处叫做
resource.qrc
。在 qrc 编辑器中,添加前缀。此处我们前缀设置成
/
即可。所谓的前缀,可以理解成“虚拟目录”(没有在我们的电脑中真实存在,是 Qt 抽象出来的)。这个前缀决定了后续我们如何在代码中访问资源。在资源编辑器中,点击 add Files 添加资源文件。此处我们需要添加的是
rose.png
。注意:添加的文件必须是在 qrc 文件的同级目录,或者同级目录的子目录中。因此需要把之前 D 盘中的rose.png
复制到上述目录中。添加完毕后,可以在资源编辑器中看到添加好的文件。在代码中使用
rose.png
:
#include <QIcon>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
//设置图标
QIcon icon(":/rose.png");
this->setWindowIcon(icon);
}
注意上述路径的访问规则:
使用
:
作为开头,表示从 qrc 中读取资源。/
是上面配置的前缀。rose.jpg
是资源的名称。需要确保代码中编写的路径和添加到 qrc 中资源的路径匹配。否则资源无法被访问(同时也不会有报错提示)。
运行程序,可以看到图标已经能正确设置。
接下来,可以进入到项目的构建目录,可以看到目录中多了一个 qrc_resource.cpp
文件。直接打开这个文件,可以看到类似如下代码:
/****************************************************************************
** Resource object code
**
** Created by: The Resource Compiler for Qt version 6.9.0
**
** WARNING! All changes made in this file will be lost!
*****************************************************************************/
#ifdef _MSC_VER
// disable informational message "function ... selected for automatic inline expansion"
#pragma warning (disable: 4711)
#endif
static const unsigned char qt_resource_data[] = {
// rose.png
0x0,0x13,0x78,0xfb,
0x89,
0x50,0x4e,0x47,0xd,0xa,0x1a,0xa,0x0,0x0,0x0,0xd,0x49,0x48,0x44,0x52,0x0,
0x0,0x5,0x30,0x0,0x0,0x5,0x30,0x8,0x2,0x0,0x0,0x0,0xfb,0xf,0xed,0x35,
0x0,0x0,0x0,0x5b,0x65,0x58,0x49,0x66,0x4d,0x4d,0x0,0x2a,0x0,0x0,0x0,0x8,
0x0,0x1,0x1,0x3b,0x0,0x2,0x0,0x0,0x0,0x41,0x0,0x0,0x0,0x1a,0x0,0x0,
0x0,0x0,0x61,0x32,0x34,0x63,0x37,0x63,0x35,0x63,0x62,0x36,0x30,0x62,0x62,0x37,
0x66,0x36,0x30,0x39,0x32,0x61,0x62,0x35,0x62,0x61,0x36,0x37,0x37,0x66,0x34,0x61,
0x30,0x31,0x66,0x33,0x66,0x36,0x63,0x33,0x34,0x38,0x62,0x36,0x39,0x62,0x66,0x31,
0x62,0x35,0x31,0x65,0x35,0x38,0x65,0x63,0x63,0x31,0x37,0x66,0x39,0x61,0x61,0x32,
0x33,0x33,0x0,0x1b,0x21,0xc5,0xce,0x0,0x0,0x20,0x0,0x49,0x44,0x41,0x54,0x78,
0x9c,0x94,0xbd,0x4b,0x96,0x24,0x3b,0xe,0x2c,0x66,0x6,0x8f,0xea,0xf7,0x56,0xa7,
0xad,0x68,0xac,0xa5,0x68,0xa0,0xad,0x49,0xab,0xd0,0xe0,0xdd,0x4a,0x87,0x69,0x80,
0xf,0x41,0xba,0x47,0xde,0x96,0x9f,0xee,0xba,0x91,0x11,0x74,0x12,0xc4,0x9f,0x4,
0x8,0xf2,0xff,0xfa,0x7f,0xfe,0x5f,0x0,0x4e,0x48,0xfa,0xf1,0x5b,0x12,0x79,0x49,
// .............
};
#ifdef QT_NAMESPACE
# define QT_RCC_PREPEND_NAMESPACE(name) ::QT_NAMESPACE::name
# define QT_RCC_MANGLE_NAMESPACE0(x) x
# define QT_RCC_MANGLE_NAMESPACE1(a, b) a##_##b
# define QT_RCC_MANGLE_NAMESPACE2(a, b) QT_RCC_MANGLE_NAMESPACE1(a,b)
# define QT_RCC_MANGLE_NAMESPACE(name) QT_RCC_MANGLE_NAMESPACE2( \
QT_RCC_MANGLE_NAMESPACE0(name), QT_RCC_MANGLE_NAMESPACE0(QT_NAMESPACE))
#else
# define QT_RCC_PREPEND_NAMESPACE(name) name
# define QT_RCC_MANGLE_NAMESPACE(name) name
#endif
#if defined(QT_INLINE_NAMESPACE)
inline namespace QT_NAMESPACE {
#elif defined(QT_NAMESPACE)
namespace QT_NAMESPACE {
#endif
bool qRegisterResourceData(int, const unsigned char *, const unsigned char *, const unsigned char *);
bool qUnregisterResourceData(int, const unsigned char *, const unsigned char *, const unsigned char *);
#ifdef QT_NAMESPACE
}
#endif
int QT_RCC_MANGLE_NAMESPACE(qInitResources_resource)();
int QT_RCC_MANGLE_NAMESPACE(qInitResources_resource)()
{
int version = 3;
QT_RCC_PREPEND_NAMESPACE(qRegisterResourceData)
(version, qt_resource_struct, qt_resource_name, qt_resource_data);
return 1;
}
int QT_RCC_MANGLE_NAMESPACE(qCleanupResources_resource)();
int QT_RCC_MANGLE_NAMESPACE(qCleanupResources_resource)()
{
int version = 3;
QT_RCC_PREPEND_NAMESPACE(qUnregisterResourceData)
(version, qt_resource_struct, qt_resource_name, qt_resource_data);
return 1;
}
#ifdef __clang__
# pragma clang diagnostic push
# pragma clang diagnostic ignored "-Wexit-time-destructors"
#endif
namespace {
struct initializer {
initializer() { QT_RCC_MANGLE_NAMESPACE(qInitResources_resource)(); }
~initializer() { QT_RCC_MANGLE_NAMESPACE(qCleanupResources_resource)(); }
} dummy;
}
#ifdef __clang__
# pragma clang diagnostic pop
#endif
上述代码其实就是通过 unsigned char
数组,把 rose.png
中的每个字节都记录下来。这些代码会被编译到 exe 中。后续无论 exe 被复制到哪个目录下,都确保能够访问到该图片资源。
qrc 资源管理方案的优缺点
优点:确保了图片、字体、声音等资源能够真正做到“目录无关”,无论如何都不会出现资源丢失的情况。
缺点:不适合管理体积大的资源。如果资源比较大(比如是几个 MB 的文件),或者资源特别多,生成的最终的 exe 体积就会比较大,程序运行消耗的内存也会增大,程序编译的时间也会显著增加。
下一篇更精彩😝😝😝😝😝😝
下一篇更精彩😝😝😝😝😝😝
下一篇更精彩😝😝😝😝😝😝