C++框架_之Qt的窗口部件系统的详解-上

时间:2023-12-09 22:01:49

C++框架_之Qt的窗口部件系统的详解-上

第一部分概述

第一次建立helloworld程序时,曾看到Qt Creator提供的默认基类只有QMainWindow、QWidget和QDialog三种。是的,这三种窗体也是以后用的最多的,QMainWindow是带有菜单栏和工具栏的主窗口类,QDialog是各种对话框的基类,而它们二者全部继承自QWidget。不仅如此,其实所有的窗口部件都继承自QWidget。

C++框架_之Qt的窗口部件系统的详解-上

第二部分--核心部分

2.1Qt窗口坐标体系

坐标体系

以左上角为原点,X向右增加,Y向下增加。

C++框架_之Qt的窗口部件系统的详解-上

对于嵌套窗口,其坐标是相对于父窗口来说的。

对于窗口的大小和位置,根据是否包含边框和标题栏两种情况,要用不同的函数来获取它们的数值。

C++框架_之Qt的窗口部件系统的详解-上

这里的函数分为两类,一类是包含框架的,一类是不包含框架的:

   包含框架:x()、y()、frameGeometry()、pos()和move()等函数;

  不包含框架:geometry()、width()、height()、rect()和size()等函数。

2.2QWidget

所有窗口及窗口控件都是从QWidget直接或间接派生出来的。

QWidget类是所有用户界面对象的基类,被称为基础窗口部件。QWidget继承自QObject类和QPaintDevice类,其中QObject类是所有支持Qt对象模型(Qt Object Model)的Qt对象的的基类,QPaintDevice类是所有可以绘制的对象的基类。

2.21对象模型

C++框架_之Qt的窗口部件系统的详解-上

在Qt中创建对象的时候会提供一个Parent对象指针,下面来解释这个parent到底是干什么的。

● QObject是以对象树的形式组织起来的。

   ★当你创建一个QObject对象时,会看到QObject的构造函数接收一个QObject指针作为参数,这个参数就是 parent,也就是父对象指针。

  这相当于,在创建QObject对象时,可以提供一个其父对象,我们创建的这个QObject对象会自动添加到其父对象的children()列表。

  ★当父对象析构的时候,这个列表中的所有对象也会被析构。(注意,这里的父对象并不是继承意义上的父类!)--这就是为什么Qt动态分配内存会自动清除空间

  这种机制在 GUI 程序设计中相当有用。例如,一个按钮有一个QShortcut(快捷键)对象作为其子对象。当我们删除按钮的时候,这个快捷键理应被删除。这是合理的。

●QWidget是能够在屏幕上显示的一切组件的父类。

  ★QWidget继承自QObject,因此也继承了这种对象树关系。一个孩子自动地成为父组件的一个子组件。因此,它会显示在父组件的坐标系统中,被父组件的边界剪裁。例如,当用户关闭一个对话框的时候,应用程序将其删除,那么,我们希望属于这个对话框的按钮、图标等应该一起被删除。事实就是如此,因为这些都是对话框的子组件。

    ★ 当然,我们也可以自己删除子对象,它们会自动从其父对象列表中删除。比如,当我们删除了一个工具栏时,其所在的主窗口会自动将该工具栏从其子对象列表中删除,并且自动调整屏幕显示。

  Qt 引入对象树的概念,在一定程度上解决了内存问题。

●当一个QObject对象在堆上创建的时候,Qt 会同时为其创建一个对象树。不过,对象树中对象的顺序是没有定义的。这意味着,销毁这些对象的顺序也是未定义的。

●任何对象树中的 QObject对象 delete 的时候,如果这个对象有 parent,则自动将其从 parent 的children()列表中删除;如果有孩子,则自动 delete 每一个孩子。Qt 保证没有QObject会被 delete 两次,这是由析构顺序决定的。

如果QObject在栈上创建,Qt 保持同样的行为。正常情况下,这也不会发生什么问题。来看下下面的代码片段:

{ QWidget window; QPushButton quit("Quit", &window); }

作为父组件的 window 和作为子组件的 quit 都是QObject的子类(事实上,它们都是QWidget的子类,而QWidget是QObject的子类)。这段代码是正确的,quit 的析构函数不会被调用两次,因为标准 C++要求,局部对象的析构顺序应该按照其创建顺序的相反过程。因此,这段代码在超出作用域时,会先调用 quit 的析构函数,将其从父对象 window 的子对象列表中删除,然后才会再调用 window 的析构函数。

但是,如果我们使用下面的代码:

{

    QPushButton quit("Quit");

    QWidget window;

    quit.setParent(&window);

}

情况又有所不同,析构顺序就有了问题。我们看到,在上面的代码中,作为父对象的 window 会首先被析构,因为它是最后一个创建的对象。在析构过程中,它会调用子对象列表中每一个对象的析构函数,也就是说, quit 此时就被析构了。然后,代码继续执行,在 window 析构之后,quit 也会被析构,因为 quit 也是一个局部变量,在超出作用域的时候当然也需要析构。但是,这时候已经是第二次调用 quit 的析构函数了,C++ 不允许调用两次析构函数,因此,程序崩溃了。

由此我们看到,Qt 的对象树机制虽然帮助我们在一定程度上解决了内存问题,但是也引入了一些值得注意的事情。这些细节在今后的开发过程中很可能时不时跳出来烦扰一下,所以,我们最好从开始就养成良好习惯,在 Qt 中,尽量在构造的时候就指定 parent 对象,并且大胆在堆上创建。

2.22窗口、子部件以及窗口类型

来看一个代码片段:

    // 新建QWidget类对象,默认parent参数是0,所以它是个窗口
    QWidget *widget = new QWidget();
    // 设置窗口标题
    widget->setWindowTitle(QObject::tr("我是widget"));
    // 新建QLabel对象,默认parent参数是0,所以它是个窗口
    QLabel *label = new QLabel();
    label->setWindowTitle(QObject::tr("我是label"));
    // 设置要显示的信息
    label->setText(QObject::tr("label:我是个窗口"));
    // 改变部件大小,以便能显示出完整的内容
    label->resize(, );
    // label2指定了父窗口为widget,所以不是窗口
    QLabel *label2 = new QLabel(widget);
    label2->setText(QObject::tr("label2:我不是独立窗口,只是widget的子部件"));
    label2->resize(, );
    // 在屏幕上显示出来
    label->show();
    widget->show();

★在程序中定义了一个QWidget类对象的指针widget和两个QLabel对象指针label与label2,其中label没有父窗口,而label2在widget中,widget是其父窗口。

★窗口部件(Widget)这里简称部件,是Qt中建立用户界面的主要元素。像主窗口、对话框、标签、还有以后要介绍到的按钮、文本输入框等都是窗口部件。

★在Qt中,把没有嵌入到其他部件中的部件称为窗口,一般的,窗口都有边框和标题栏,就像程序中的widget和label一样。

★QMainWindow和大量的QDialog子类是最一般的窗口类型。窗口就是没有父部件的部件,所以又称为*部件(top-level widget)。与其相对的是非窗口部件,又称为子部件(child widget)。在Qt中大部分部件被用作子部件,它们嵌入在别的窗口中,例如程序中的label2。

2.23窗口类型

QWidget的构造函数有两个参数:

QWidget * parent = 0和Qt::WindowFlags f = ;

前面的parent就是指父窗口部件,默认值为0,表明没有父窗口; 而后面的f参数是Qt::WindowFlags类型的,它是一个枚举类型,分为窗口类型(WindowType)和窗口标志(WindowFlags。前者可以定义窗口的类型,比如我们这里f=0,表明使用了Qt::Widget一项,这是QWidget的默认类型,这种类型的部件如果有父窗口,那么它就是子部件,否则就是独立的窗口。

例如:使用其中的Qt::Dialog和Qt::SplashScreen,更改程序中的新建对象的那两行代码:

QWidget *widget = , Qt::Dialog); QLabel *label = , Qt::SplashScreen);

当更改窗口类型后,窗口的样式发生了改变,一个是对话框类型,一个是欢迎窗口类型。 而对于窗口标志,它主要的作用是更改窗口的标题栏和边框,而且它们可以和窗口类型进行位或操作。

下面再次更改那两行代码:

QWidget *widget = , Qt::Dialog | Qt::FramelessWindowHint); QLabel *label = , Qt::SplashScreen | Qt::WindowStaysOnTopHint);

Qt::FramelessWindowHint用来产生一个没有边框的窗口,而Qt::WindowStaysOnTopHint用来使该窗口停留在所有其它窗口上面。

2.3程序调试

//下面在讲解窗口几何布局的几个函数的同时,讲解一下程序调试方面的内容。
//将主函数内容更改如下:
#include <QApplication>
#include <QWidget>
int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    QWidget widget;
    int x = widget.x();
    int y = widget.y();
    QRect geometry = widget.geometry();
    QRect frame = widget.frameGeometry();
    return a.exec();
}

说明:

x()、y()分别返回部件的位置坐标的x、y值,它们的默认值为0。 而geometry()和frameGeometry()函数分别返回没有边框和包含边框的窗口框架矩形的值,其返回值是QRect类型的,就是一个矩形,它的形式是(位置坐标,大小信息),也就是(x,y,宽,高)。

下面在int x = widget.x(); 一行代码的标号前面点击鼠标左键来设置断点。 所谓断点,就是程序运行到该行代码时会暂停下来,从而可以查看一些信息,如变量值等。要取消断点,只要在那个断点上再点击一下就可以了。设置好断点后便可以按下F5或者左下角的调试按钮开始调试。这时程序会先进行构建再进入调试模式,这个过程可能需要一些时间。在程序构建时可能会出现警告,那是因为我们定义了变量却没有使用造成的,不用管它。

调试模式

C++框架_之Qt的窗口部件系统的详解-上

下面对调试模式的几个按钮和窗口进行简单介绍:

①继续按钮。程序在断点处停了下来,按下继续按钮后,程序便会像正常运行一样,执行后面的代码,直到遇到下一个断点,或者程序结束。

②停止调试按钮。按下该按钮后结束调试。

③单步跳过按钮。直接执行本行代码,然后指向下一行代码。

④单步进入按钮。进入调用的函数内部。

⑤单步跳出按钮。当进入函数内部时,跳出该函数,一般与单步进入配合使用。

⑥重新启动调试会话。

⑦显示源码对应的汇编指令,并可以单步调试。

⑧堆栈视图。这里显示了从程序开始到断点处,所有嵌套调用的函数所在的源文件名和行号。

⑨其它视图。这里可以选择多种视图。

单步调试

  点击一下“单步进入”按钮,或者按下F11,这时,程序会跳转到QWidget类的x()函数的源码处,这里对这个函数不做过多讲解,下面直接按下“单步跳出”按钮回到原来的断点处。然后便开始一直按“单步跳过”按钮,单步执行程序,并查看局部变量和监视器视图中相应变量值的变化情况。等执行到最后一行代码return a.exec();时,按下“停止调试”按钮,结束调试。

  这里要补充说明一下,我们在程序调试过程中可以进入到Qt类的源码中,其实还有一个很简单的方法也可以实现这个功能,就是在编辑器中将鼠标光标定位到一个类名或者函数上,然后按下F2键,或者点击鼠标右键,选择“跟踪光标位置的符号”,这时编辑器就会跳转到其源码处。

  从变量监视器中可以看到x、y、geometry和frame四个变量初始值都是一个随机未知数。等到调试完成后,x、y的值均为0,这是它们的默认值。而geometry的值为640x480+0+0,frame的值为639x479+0+0。

现在对这些值还不是很清楚,不过,为什么x、y的值会是0呢?我们可能会想到,应该是窗口没有显示的原因,那么就更改代码,让窗口先显示出来,再看这些值。在QWidget widget;一行代码后添加一行代码:

widget.show();

  现在再次调试程序,这时会发现窗口只显示了一个标题栏,先不管它,继续在Qt Creator中点击“单步跳过”按钮。当我们将程序运行到最后一行代码return a.exec();时,再次按下“单步跳过”按钮后,程序窗口终于显示出来了。这是因为只有程序进入主事件循环后才能接收事件,而show()函数会触发显示事件,所以只有在完成a.exe()函数调用进入消息循环后才能正常显示。这次看到几个变量的值都有了变化,但是这时还是不清楚这些值的含义。

注意:因为使用调试器进行调试要等待一段时间,而且步骤很麻烦,对于初学者来说,如果按错了按钮,还很容易出错。 所以,并不推荐初学者使用。

2.4QMainWindow

QMainWindow是一个为用户提供主窗口程序的类,包含一个菜单栏(menu bar)、多个工具栏(tool bars)、多个锚接部件(dock widgets)、一个状态栏(status bar)及一个中心部件(central widget),是许多应用程序的基础,如文本编辑器,图片编辑器等。

C++框架_之Qt的窗口部件系统的详解-上

2.41菜单栏

一个主窗口最多只有一个菜单栏。位于主窗口顶部、主窗口标题栏下面。

●创建菜单栏,通过QMainWindow类的menubar()函数获取主窗口菜单栏指针

QMenuBar * menuBar() const

●创建菜单,调用QMenu的成员函数addMenu来添加菜单

QAction* addMenu(QMenu * menu)

QMenu* addMenu(const QString & title)

QMenu* addMenu(const QIcon & icon, const QString & title)

●创建菜单项,调用QMenu的成员函数addAction来添加菜单项

QAction* activeAction() const

QAction* addAction(const QString & text)

QAction* addAction(const QIcon & icon, const QString & text)

QAction* addAction(const QString & text, const QObject * receiver,

 )

QAction* addAction(const QIcon & icon, const QString & text,

const QObject * receiver, const char * member,

)

Qt 并没有专门的菜单项类,只是使用一个QAction类,抽象出公共的动作。当我们把QAction对象添加到菜单,就显示成一个菜单项,添加到工具栏,就显示成一个工具按钮。用户可以通过点击菜单项、点击工具栏按钮、点击快捷键来激活这个动作。

2.42 工具栏

主窗口的工具栏上可以有多个工具条,通常采用一个菜单对应一个工具条的的方式,也可根据需要进行工具条的划分。

●直接调用QMainWindow类的addToolBar()函数获取主窗口的工具条对象,每增加一个工具条都需要调用一次该函数。

● 插入属于工具条的动作,即在工具条上添加操作。

通过QToolBar类的addAction函数添加。

  ● 工具条是一个可移动的窗口,它的停靠区域由QToolBar的allowAreas决定,包括:

  ● Qt::LeftToolBarArea 停靠在左侧

  ●Qt::RightToolBarArea 停靠在右侧

  ● Qt::TopToolBarArea 停靠在顶部

  ●Qt::BottomToolBarArea    停靠在底部

  ● Qt::AllToolBarAreas 以上四个位置都可停靠

使用setAllowedAreas()函数指定停靠区域: 

setAllowedAreas(Qt::LeftToolBarArea | Qt::RightToolBarArea)

使用setMoveable()函数设定工具栏的可移动性

setMoveable(false)//工具条不可移动, 只能停靠在初始化的位置上

2.43状态栏

●派生自QWidget类,使用方法与QWidget类似,QStatusBar类常用成员函数:

//添加小部件

)

//插入小部件

)

//删除小部件

void removeWidget(QWidget * widget)

2.5资源文件

  Qt 资源系统是一个跨平台的资源机制,用于将程序运行时所需要的资源以二进制的形式存储于可执行文件内部。如果你的程序需要加载特定的资源(图标、文本翻译等),那么,将其放置在资源文件中,就再也不需要担心这些文件的丢失。也就是说,如果你将资源以资源文件形式存储,它是会编译到可执行文件内部。

  使用 Qt Creator 可以很方便地创建资源文件。我们可以在工程上点右键,选择“添加新文件…”,可以在 Qt 分类下找到“Qt 资源文件”:

C++框架_之Qt的窗口部件系统的详解-上

点击“选择…”按钮,打开“新建 Qt 资源文件”对话框。在这里我们输入资源文件的名字和路径:

C++框架_之Qt的窗口部件系统的详解-上

点击下一步,选择所需要的版本控制系统,然后直接选择完成。我们可以在 Qt Creator 的左侧文件列表中看到“资源文件”一项,也就是我们新创建的资源文件:

C++框架_之Qt的窗口部件系统的详解-上

右侧的编辑区有个“添加”,我们首先需要添加前缀,比如我们将前缀取名为 images。然后选中这个前缀,继续点击添加文件,可以找到我们所需添加的文件。这里,我们选择 document-open.png 文件。当我们完成操作之后,Qt Creator 应该是这样子的:

C++框架_之Qt的窗口部件系统的详解-上

接下来,我们还可以添加另外的前缀或者另外的文件。这取决于你的需要。当我们添加完成之后,我们可以像前面一章讲解的那样,通过使用 : 开头的路径来找到这个文件。比如,我们的前缀是 /images,文件是 document-open.png,那么就可以使用:/images/document-open.png找到这个文件。

这么做带来的一个问题是,如果以后我们要更改文件名,比如将 docuemnt-open.png 改成 docopen.png,那么,所有使用了这个名字的路径都需要修改。所以,更好的办法是,我们给这个文件去一个“别名”,以后就以这个别名来引用这个文件。具体做法是,选中这个文件,添加别名信息:

C++框架_之Qt的窗口部件系统的详解-上

这样,我们可以直接使用:/images/doc-open引用到这个资源,无需关心图片的真实文件名。

如果我们使用文本编辑器打开 res.qrc 文件,就会看到一下内容:

<RCC>
        <qresource prefix="/images">
            <file alias="doc-open">document-open.png</file>
        </qresource>
        <qresource prefix="/images/fr" lang="fr">
            <file alias="doc-open">document-open-fr.png</file>
        </qresource>
</RCC>

我们可以对比一下,看看 Qt Creator 帮我们生成的是怎样的 qrc 文件。当我们编译工程之后,我们可以在构建目录中找到 qrc_res.cpp 文件,这就是 Qt 将我们的资源编译成了 C++ 代码。

可能有中部分  下部分   。。。。