在 C++ 应用中使用设计器 UI 文件

Qt 设计器 UI 文件表示表单的控件树的 XML 格式。表单可以被处理

  • 在编译时,这意味着表单会被转换为可编译的 C++ 代码。
  • 在运行时,这意味着表单会被 QUiLoader 类处理并动态构建控件树,在解析 XML 文件时。

编译时表单处理

您使用 Qt Designer 创建用户界面组件,并使用 Qt 的集成构建工具,qmakeuic,在构建应用程序时为其生成代码。生成的代码包含表单的用户界面对象。它是一个包含

  • 指向表单的控件、布局、布局项、按钮组和操作的指针。
  • 一个名为 setupUi() 的成员函数,用于在父控件上构建控件树。
  • 一个名为 retranslateUi() 的成员函数,用于处理表单的字符串属性翻译。更多信息,请参阅 响应语言变化

生成的代码可以包含在您的应用程序中并直接使用它。或者,您可以用它来扩展标准小部件的子类。

可以使用以下方法之一在您的应用程序中使用编译时处理的表单

  • 直接方法:您构建一个用作组件占位符的小部件,并在其中设置用户界面。
  • 单继承方法:您扩展表单的基类(例如 QWidgetQDialog),并包含表单用户界面对象的私有实例。
  • 多重继承方法:您同时扩展表单的基类和表单的用户界面对象。这允许在子类的范围内直接使用表单中定义的小部件。

为了演示,我们创建了一个简单的计算器表单应用程序。它基于原始的 计算器表单 示例。

该应用程序由一个源文件 main.cpp 和一个 UI 文件组成。

下面是使用 Qt Designer 设计的 calculatorform.ui 文件

当使用 CMake 构建可执行文件时,需要一个 CMakeLists.txt 文件

cmake_minimum_required(VERSION 3.16)
project(calculatorform LANGUAGES CXX)

set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON)

find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets)

qt_add_executable(calculatorform
                  calculatorform.ui main.cpp)

set_target_properties(calculatorform PROPERTIES
    WIN32_EXECUTABLE TRUE
    MACOSX_BUNDLE TRUE
)

target_link_libraries(calculatorform  PUBLIC
    Qt::Core
    Qt::Gui
    Qt::Widgets
)

该表单在 qt_add_executable() 中列出,其作为 C++ 源文件之一。选项 CMAKE_AUTOUIC 会通知 CMake 运行 uic 工具以创建一个可以被源文件使用的 ui_calculatorform.h 文件。

在用 qmake 构建可执行文件时,需要一个 .pro 文件。

TEMPLATE    = app
FORMS       = calculatorform.ui
SOURCES     = main.cpp

此文件的特色是 FORMS 声明,指出 qmake 需要处理哪些文件以使用 uic。在这种情况下,使用 calculatorform.ui 文件创建一个 ui_calculatorform.h 文件,该文件可用于列在 SOURCES 声明中的任何文件。

注意:您可以使用 Qt Creator 来创建计算器表单项目。它将自动生成 main.cpp、UI 和所需构建工具的项目文件,您可以进行修改。

直接方法

要使用直接方法,我们直接在 main.cpp 中包含 ui_calculatorform.h 文件。

#include "ui_calculatorform.h"

main 函数通过构造一个标准的 QWidget 来创建计算器小部件,该小部件供我们托管由 calculatorform.ui 文件描述的用户界面。

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);
    QWidget widget;
    Ui::CalculatorForm ui;
    ui.setupUi(&widget);

    widget.show();
    return app.exec();
}

在这种情况下,Ui::CalculatorForm 是一个来自 ui_calculatorform.h 文件的接口描述对象,设置对话框中所有小部件以及其信号与槽的连接。

直接方法为您提供一个快速简便的方法来使用简单、独立的组件。但是,使用 Qt Designer 创建的组件往往需要与应用程序代码紧密集成。例如,上面提供的 CalculatorForm 代码可以编译并运行,但 QSpinBox 对象不会与 QLabel 进行交互,因为我们需要一个自定义槽来执行加法操作并将结果显示在 QLabel 上。为了实现这一点,我们需要使用单继承方法。

单继承方法

要使用单继承方法,我们需要将标准 Qt 小部件子类化,并包含表单用户界面对象的私有实例。这可能采取以下形式

  • 成员变量
  • 指针成员变量

使用成员变量

在此方法中,我们子类化一个 Qt 小部件,并在构造函数内设置用户界面。以这种方式使用的组件将表单中使用的窗口和小部件暴露给 Qt 小部件子类,并为用户界面和应用程序中的其他对象之间提供建立信号和槽连接的标准系统。生成的 Ui::CalculatorForm 结构是类的一个成员。

此方法在 Calculator Form 示例中使用。

为了确保我们可以使用用户界面,我们需要在引用 Ui::CalculatorForm 之前包含 uic 生成的头文件。

#include "ui_calculatorform.h"

对于 CMake

qt_add_executable(calculatorform
    calculatorform.cpp calculatorform.h calculatorform.ui
    main.cpp
)

对于 qmake

HEADERS     = calculatorform.h

子类是以下定义的

class CalculatorForm : public QWidget
{
    Q_OBJECT

public:
    explicit CalculatorForm(QWidget *parent = nullptr);

private slots:
    void updateResult();

private:
    Ui::CalculatorForm ui;
};

该类的重要特性是私有的 ui 对象,它提供了设置和管理用户界面的代码。

子类的构造函数通过调用 ui 对象的 setupUi() 函数,仅为对话框创建和配置所有窗口和小部件。一旦这样做,就可以根据需要修改用户界面。

CalculatorForm::CalculatorForm(QWidget *parent)
    : QWidget(parent)
{
    ui.setupUi(this);
    connect(ui.inputSpinBox1, &QSpinBox::valueChanged, this, &CalculatorForm::updateResult);
    connect(ui.inputSpinBox2, &QSpinBox::valueChanged, this, &CalculatorForm::updateResult);
}

我们可以通过添加具体对象名的 `on_` 前缀以通常的方式连接用户界面小部件中的信号和槽。有关更多信息,请参阅包含自动连接的小部件和对话框

此方法的优点是使用继承的简单性来提供一个基于 QWidget 的接口,并且封装了用户界面小部件变量在 `ui` 数据成员内。我们可以使用这种方法在同一小部件中定义多个用户界面,每个都在自己的命名空间中,并覆盖(或组合)它们。例如,可以使用这种方法从现有的表单创建单独的标签页。

使用指针成员变量

另一种方法是使 `Ui::CalculatorForm` 结构成为类的指针成员。如下所示则呈现其头文件

namespace Ui {
    class CalculatorForm;
}

class CalculatorForm : public QWidget
...
virtual ~CalculatorForm();
...
private:
    Ui::CalculatorForm *ui;
...

相应的源文件如下所示

#include "ui_calculatorform.h"

CalculatorForm::CalculatorForm(QWidget *parent) :
    QWidget(parent), ui(new Ui::CalculatorForm)
{
    ui->setupUi(this);
}

CalculatorForm::~CalculatorForm()
{
    delete ui;
}

此方法的优势是用户界面对象可以被前置声明,这意味着我们不需要在头文件中包含生成的 ui_calculatorform.h 文件。然后可以更改表单,而不必重新编译依赖的源文件。这在对二进制兼容性有限制的类中尤其重要。

我们通常推荐这种方法用于库和大型应用程序。有关更多信息,请参阅创建共享库

多重继承方法

使用 Qt Designer 创建的表单可以与基于标准 QWidget 的类一起子类化。这种方法使得表单中定义的所有用户界面组件都直接在子类的范围内访问,并允许使用 connect() 函数以通常的方式进行信号和槽的连接。

我们需要包含由 uiccalculatorform.ui 文件生成的头文件,如下所示

#include "ui_calculatorform.h"

类以与在单继承方法中使用的方法相似的方式定义,只是这次我们从 两者 QWidget 和 `Ui::CalculatorForm` 继承,如下所示

class CalculatorForm : public QWidget, private Ui::CalculatorForm
{
    Q_OBJECT

public:
    explicit CalculatorForm(QWidget *parent = nullptr);

private slots:
    void on_inputSpinBox1_valueChanged(int value);
    void on_inputSpinBox2_valueChanged(int value);
};

我们私有继承 `Ui::CalculatorForm` 以确保用户界面对象在我们的子类中是私有的。我们也可以以相同的方式使用 `public` 或 `protected` 关键字继承它,就像我们可以在前一个例子中使 `ui` 或 `protected`。

子类的构造函数执行了与在单继承例子中使用的构造函数类似的许多任务

CalculatorForm::CalculatorForm(QWidget *parent)
    : QWidget(parent)
{
    setupUi(this);
}

在这种情况下,可以像以手工创建的代码小部件一样访问用户界面中使用的小部件。我们不再需要 `ui` 前缀来访问它们。

对语言更改的反应

如果用户界面语言发生变化,Qt 通过发送类型为 QEvent::LanguageChange 的事件来通知应用程序。为了调用用户界面对象的成员函数 retranslateUi(),我们按照如下方式在实际表单类中重新实现 QWidget::changeEvent()

void CalculatorForm::changeEvent(QEvent *e)
{
    QWidget::changeEvent(e);
    switch (e->type()) {
    case QEvent::LanguageChange:
        ui->retranslateUi(this);
        break;
    default:
        break;
   }
}

运行时表单处理

或者,表单可以在运行时处理,生成动态生成用户界面。这可以通过使用QtUiTools模块来实现,该模块提供了QUiLoader类来处理使用Qt Designer创建的表单。

UiTools方法

要处理运行时的表单,需要包含包含UI文件的资源文件。此外,还需要配置应用程序以使用QtUiTools模块。这通过在CMake项目文件中包含以下声明来实现,以确保应用程序被适当编译并链接。

find_package(Qt6 REQUIRED COMPONENTS Core Gui UiTools Widgets)
target_link_libraries(textfinder PUBLIC
    Qt::Core
    Qt::Gui
    Qt::UiTools
    Qt::Widgets
)

对于 qmake

QT += uitools

QUiLoader类提供了一个表单加载对象来构建用户界面。该用户界面可以从任何QIODevice获取,例如QFile对象,以获取存储在项目资源文件中的表单。函数QUiLoader::load()使用文件中包含的用户界面描述来构建表单小部件。

可以使用以下指令包含QtUiTools模块类

#include <QtUiTools>

函数QUiLoader::load()的调用方式如以下Text Finder示例代码所示

static QWidget *loadUiFile(QWidget *parent)
{
    QFile file(u":/forms/textfinder.ui"_s);
    file.open(QIODevice::ReadOnly);

    QUiLoader loader;
    return loader.load(&file, parent);
}

在一个使用QtUiTools在运行时构建其用户界面的类中,我们可以使用QObject::findChild()在表单中定位对象。例如,在以下代码中,我们根据对象的名称和小部件类型定位一些组件

    ui_findButton = findChild<QPushButton*>("findButton");
    ui_textEdit = findChild<QTextEdit*>("textEdit");
    ui_lineEdit = findChild<QLineEdit*>("lineEdit");

在运行时处理表单使开发者可以自由地更改程序的用户界面,只需更改UI文件。这在根据不同的用户需求自定义程序时很有用,例如提供更大图标或不同的配色方案以支持辅助功能。

自动连接

为编译时或运行时表单定义的信号和槽连接可以手动或自动设置,这利用了QMetaObject在信号和适合命名的槽之间建立连接的能力。

通常,在QDialog中,如果我们想在接受用户输入之前处理这些信息,我们需要将OK按钮的clicked()信号连接到对话框中的自定义槽。我们首先展示一个手动的信号连接对话框示例,然后将其与一个使用自动连接的对话框进行比较。

没有自动连接的对话框

我们按以前的方式定义对话框,但现在除了构造函数外还包含一个槽

class ImageDialog : public QDialog, private Ui::ImageDialog
{
    Q_OBJECT

public:
    explicit ImageDialog(QWidget *parent = nullptr);

private slots:
    void checkValues();
};

checkValues()槽将被用于验证用户提供的值。

在对话框的构造函数中,我们将控件设置与之前相同,并将对话框的Cancel按钮的clicked()信号连接到对话框的reject()槽。我们还禁用了两个按钮上的autoDefault属性,以确保对话框不会影响编辑行处理回车键事件的方式

ImageDialog::ImageDialog(QWidget *parent)
    : QDialog(parent)
{
    setupUi(this);
    okButton->setAutoDefault(false);
    cancelButton->setAutoDefault(false);
    ...
    connect(okButton, &QAbstractButton::clicked, this, &ImageDialog::checkValues);
}

我们将OK按钮的clicked()信号连接到我们实现的checkValues()槽

void ImageDialog::checkValues()
{
    if (nameLineEdit->text().isEmpty()) {
        QMessageBox::information(this, tr("No Image Name"),
            tr("Please supply a name for the image."), QMessageBox::Cancel);
    } else {
        accept();
    }
}

此自定义槽执行必要的最小操作以确保用户输入的数据有效 - 它只在提供图像名称的情况下接受输入。

具有自动连接的控件和对话框

虽然在对话框中实现自定义槽并连接到构造函数中很容易,但我们也可以利用QMetaObject的自动连接功能,将对话框中OK按钮的clicked()信号连接到我们子类的槽中。uic会自动在对话框的setupUi()函数中生成代码来实现这一点,所以我们只需声明并实现一个遵循标准约定的槽即可

void on_<object name>_<signal name>(<signal parameters>);

注意:当在表单中重命名小部件时,需要相应地修改槽名称,这可能会导致维护问题。因此,我们建议不要在新代码中使用这种方法。

使用这种约定,我们可以定义并实现一个响应OK按钮鼠标点击的槽

class ImageDialog : public QDialog, private Ui::ImageDialog
{
    Q_OBJECT

public:
    explicit ImageDialog(QWidget *parent = nullptr);

private slots:
    void on_okButton_clicked();
};

另一个自动信号和槽连接的例子是Text Finder及其on_findButton_clicked()槽。

我们使用QMetaObject的系统来启用信号和槽连接

    QMetaObject::connectSlotsByName(this);

这使我们能够实现槽,如下所示

void TextFinder::on_findButton_clicked()
{
    QString searchString = ui_lineEdit->text();
    QTextDocument *document = ui_textEdit->document();

    bool found = false;

    // undo previous change (if any)
    document->undo();

    if (searchString.isEmpty()) {
        QMessageBox::information(this, tr("Empty Search Field"),
                                 tr("The search field is empty. "
                                    "Please enter a word and click Find."));
    } else {
        QTextCursor highlightCursor(document);
        QTextCursor cursor(document);

        cursor.beginEditBlock();
    ...
        cursor.endEditBlock();

        if (found == false) {
            QMessageBox::information(this, tr("Word Not Found"),
                                     tr("Sorry, the word cannot be found."));
        }
    }
}

自动连接信号和槽提供了标准的命名约定和显式的接口供小部件设计者使用。通过提供实现特定接口的源代码,用户界面设计者可以验证其设计是否实际工作,而无需亲自编写代码。

© 2024 Qt公司有限。此处提供的文档贡献是各自所有者的版权。此处提供的文档是根据自由软件基金会的发布的GNU自由文档许可协议版本1.3的条款提供的。Qt以及相应的标志是芬兰和/或在全世界其他国家的Qt公司的商标。所有其他商标均为各自所有者的财产。