Qt备忘录之四:可视化窗体设计原理

可视化的窗体设计其实并不陌生,在MFC或者.net框架中都用的到。Qt也提供了类似的功能。不论是在Qt Creator中或是用VS-AddIn新建或打开Qt工程,默认都会使用Qt Designer对打开.ui文件进行编辑。用Qt的可视化功能搭建窗体操作流程与其他工具类似,本文将不同之处列举一下,并探究一下原理。本文中所用的简单示例可点此下载

Buddy及其设置

Buddy是Qt里面比较特殊的一种控件间关系。从效果上,这种关系可以简单表述为:当按下一个label对象的快捷键之后,焦点会被它的buddy接收。例如在Qt Designer里新建一个如下所示的窗体:

Buddy示例-1

其中,label对象的Text表示为Go to My &Buddy,即定义了Alt+B作为该label的快捷键。现在编译之后生成的窗体如下图所示,其中label的Text依旧是Go to My &Buddy,“&”标识还可见,按下Atl+B也没有任何效果。

没有编辑buddy是的窗体效果

Edit(编辑)——Edit Buddies(编辑伙伴)之后,可以编辑label对象的buddies。

编辑buddy

buddy箭头

编辑之后,可以看见buddy视图中,有一个从label指向PushButton的箭头,即表示这个PushButton作为label的buddy。返回Widget视图后,可以看见label的Text变为“Go to My Buddy”。

添加buddy之后的widget界面

检查label对象的属性,可以看见在buddy一栏出现了pushButton。

label属性中的buddy设置

编译之后,按下Alt+B,确实可以触发pushButton动作。

增加buddy后的窗体效果

建立同窗体signal与slot的连接

现在在这个工程中添加一个LCD Number对象,然后通过Edit(编辑)——Edit Signals/Slots(编辑信号/槽)建立pushButton与LCD Number的signal/slot连接。

Edit Signals and Slots

建立连接后,在signals/slots视图中,可以看见一个由pushButton指向LCD Number的箭头,箭头的两端分别是clicked()和hide(),即表示将pushButton的clicked()与LCD Number的hide()相连接。

Edit Signals and Slots 2

也可以通过查看signals & slots关系图,证明上述关系已建立。

Edit Signals and Slots 3

编译后运行,单击pushButton或者按Alt+B,LCD Number消失(隐藏)。程序执行如预期。

Edit Signals and Slots 4

把.ui炼成.h

关闭Qt Designer,用文本查看工具打开.ui文件,可以发现其实.ui文件是一个XML格式的文件。看看这里面有些什么:

qtdesignertest.ui

[code lang="XML" collapse="false"]
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>QtDesignerTest</class><!--主类-->
<widget class="QWidget" name="QtDesignerTest"><!--声明名为QtDesignerTest的QWidget对象-->
<property name="geometry"><!--QWidget对象的geometry(几何)属性-->
<rect>
<x>0</x>
<y>0</y>
<width>315</width>
<height>42</height>
</rect>
</property>
……
[/code]

所有在Qt Designer中设计的对象,都会以对象/属性声明的方式在XML中体现,与一般XML的用法差别不大。体现Qt特点的buddy和signals/slots用如下的方法体现:

[code lang="XML" collapse="false" firstline="15"]
……
<widget class="QLabel" name="label"><!--声明名为label的Qlabel对象-->
<property name="geometry">
<rect>
<x>11</x>
<y>11</y>
<width>84</width>
<height>16</height>
</rect>
</property>
<property name="text"><!--label的text属性-->
<string>Go to My &amp;Buddy</string><!--&符号被转换为&amp;-->
</property>
<property name="buddy"><!--buddy也是label的属性-->
<cstring>pushButton</cstring><!--这里是指定pushButton作为label的buddy-->
</property>
</widget>
……
[/code]

从这里再次印证buddy是QLabel的属性。只是这里将buddy的前后用<cstring>标签包起来,没有理解是什么意思。以后如果想明白了,就在这里补充。

[code lang="XML" collapse="false" firstline="58"]
……
<layoutdefault spacing="6" margin="11"/><!--这个似乎是默认的布局-->
<resources/><!--这个标签很奇怪,只有尾没有头-->
<connections><!--这一节就是定义signal和slot的连接了-->
<connection>
<sender>pushButton</sender>
<signal>clicked()</signal>
<receiver>lcdNumber</receiver>
<slot>hide()</slot>
<hints><!--这两个是Qt Designer中connect箭头的前后位置,其实对于程序没有作用的-->
<hint type="sourcelabel">
<x>199</x>
<y>30</y>
</hint>
<hint type="destinationlabel">
<x>263</x>
<y>30</y>
</hint>
</hints>
</connection>
</connections>
</ui>
[/code]

这一段中,</resource>标签比较奇怪,只有结尾没有开头,不符合XML的规范。按我的理解,应该是指代这个标签之上的所有代码都是“资源”吧。Qt会将这个.ui文件转换为一个以ui_为前缀的.h文件,大概长这个样子:

ui_qtdesignertest.h

[code lang="cpp" collapse="false"]
/********************************************************************************
** Form generated from reading UI file 'qtdesignertest.ui'
**
** Created by: Qt User Interface Compiler version 5.4.0
**
** WARNING! All changes made in this file will be lost when recompiling UI file!
********************************************************************************/

#ifndef UI_QTDESIGNERTEST_H
#define UI_QTDESIGNERTEST_H

#include <QtCore/QVariant>
#include <QtWidgets/QAction>
#include <QtWidgets/QApplication>
#include <QtWidgets/QButtonGroup>
#include <QtWidgets/QHeaderView>
#include <QtWidgets/QLCDNumber>
#include <QtWidgets/QLabel>
#include <QtWidgets/QPushButton>
#include <QtWidgets/QWidget>

QT_BEGIN_NAMESPACE

class Ui_QtDesignerTest
{
public:
QLabel *label;
QPushButton *pushButton;
QLCDNumber *lcdNumber;

void setupUi(QWidget *QtDesignerTest)
{
if (QtDesignerTest->objectName().isEmpty())
QtDesignerTest->setObjectName(QStringLiteral("QtDesignerTest"));
QtDesignerTest->resize(315, 42);
label = new QLabel(QtDesignerTest);
label->setObjectName(QStringLiteral("label"));
label->setGeometry(QRect(11, 11, 84, 16));
pushButton = new QPushButton(QtDesignerTest);
pushButton->setObjectName(QStringLiteral("pushButton"));
pushButton->setGeometry(QRect(110, 10, 86, 23));
lcdNumber = new QLCDNumber(QtDesignerTest);
lcdNumber->setObjectName(QStringLiteral("lcdNumber"));
lcdNumber->setGeometry(QRect(208, 11, 64, 23));
lcdNumber->setProperty("value", QVariant(3.14));
#ifndef QT_NO_SHORTCUT
label->setBuddy(pushButton);
#endif // QT_NO_SHORTCUT

retranslateUi(QtDesignerTest);
QObject::connect(pushButton, SIGNAL(clicked()), lcdNumber, SLOT(hide()));

QMetaObject::connectSlotsByName(QtDesignerTest);
} // setupUi

void retranslateUi(QWidget *QtDesignerTest)
{
QtDesignerTest->setWindowTitle(QApplication::translate("QtDesignerTest", "QtDesignerTest", 0));
label->setText(QApplication::translate("QtDesignerTest", "Go to My &Buddy", 0));
pushButton->setText(QApplication::translate("QtDesignerTest", "I'm The BUDDY", 0));
} // retranslateUi

};

namespace Ui {
class QtDesignerTest: public Ui_QtDesignerTest {};
} // namespace Ui

QT_END_NAMESPACE

#endif // UI_QTDESIGNERTEST_H
[/code]

这已经是c++的标准语法了,都看得明白,就是把刚刚在.ui里面实现的东西写成c++的一个类。其中setupUi(QWidget*)用于将各种对象根据给定的规则搭建窗体,retranslateUi(QWidget*)用于将与用户交互的各个对象的文字部分进行国际化设置(使用translate()函数)。

回到cpp文件中,我们看看界面是怎么形成的。

qtdesignertest.cpp

[code lang="cpp" collapse="false"]
#include "qtdesignertest.h"
#include "ui_qtdesignertest.h"

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

QtDesignerTest::~QtDesignerTest()
{
delete ui;
}
[/code]

就一句,调用setupUi(QWidget*),也就是上面ui_qtdesignertest.h中的setupUi(QWidget*)。其中ui是UI::QtDesignerTest类的实例对象。这里的QtDesignerTest类和UI::QtDesignerTest类是不同的,ui_qtdesignertest.h中可以看到,UI::QtDesignerTest是由Ui_QtDesignerTest类继承而来。

由此产生一个想法:后面的工程中,直接继承Ui_QtDesignerTest类来实现界面的修改和功能添加应该也是可以的。

二零一五年三月十六日

顾毅 写于厦门