文章目录
- 概述
- ui文件的本质
- 设计师绘制与代码编写
- 前情回顾
- 一次小事故
- ui中的布局设置到窗口
- 真的不行吗
- "最外层"布局概念
- 从中间代码看(中层布局)
- 虚拟出来的窗口
- 实现绘制布局的混编
- 组合使用多个UI的部分窗口
- **UI绘制**
- 大坑(续上)
- 以继承方式使用UI文件
- 尝试跳出坑
- 继承UI类
概述
该文尝试QtIDE下ui文件的本质,包括ui文件内容接结构解析,ui文件编译后的中间文件结构解析。如何混合使用QtUI设计器和手写布局编写GUI界面,以达到较好的开发效率和人机效果。掌握这种技巧后,对于UI的开发效率提升是极大的,将通过最后的例子进行说明…
ui文件的本质
当我们进行,添加Qt设计师界面类,或者是新建基于QWidget的应用程序时,均会自动生成后缀ui的文件。(个人理解)ui文件是的本质是一种xml文件,是QtDesigner环境使它能进行可视化编辑。下面贴一段,我们画一个最简单的ui界面,主要是为了后续章节的描述来定义名词。我们在mywidget.ui中仅仅绘制一个QToolButton按钮控件。
在QtCreator IDE中点击Forms下双击打开ui文件时,默认用UI设计师可视化,此时若切换到编辑或调试卡,或者直接用Notepad++打开,均会呈现xml格式,片段如下:
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>CMyWidget</class>
<widget class="QWidget" name="CMyWidget">
...
</ui>
自动生成的界面类主体代码如下,这个类有一个叫做Ui::CMyWidget *ui的成员,并在构造函数中new它。:
namespace Ui {
class CMyWidget;
}
class CMyWidget : public QWidget
{
Q_OBJECT
public:
explicit CMyWidget(QWidget *parent = nullptr);
~CMyWidget();
private:
Ui::CMyWidget *ui;
};
CMyWidget::CMyWidget(QWidget *parent) :
QWidget(parent),
ui(new Ui::CMyWidget)
{
ui->setupUi(this);
}
倘若你没有认真思考过,某些童鞋可能会认为上述ui成员对象是"显示的界面",但实际上并不是。CMyWidget类所实例化出来的对象(假设叫pmyWidgetInstance),才是真正的我们看到的那个界面对象,ui成员所代表的是这个界面内部的部分,即pmyWidgetInstance界面所包含的子窗口、控件、布局等。这里要注意Ui::CMyWidget与CMyWidget类名称一样,带来的迷惑性,前者其实是Ui_CMyWidget(非窗口类)的派生类,后者是QWidget的派生。若还是不清楚,我们继续看由ui文件编译出来的中间类。
执行编译后,生成与ui文件名称mywidget.ui对应的ui_mywidget.h/ui_mywidget.cpp文件,其中的类定义如下:
class Ui_CMyWidget
{
public:
QToolButton *toolButton;
//pMyWidget是主类中传入的'this'(CMyWidget对象指针)
void setupUi(QWidget *CMyWidget)
{
toolButton = new QToolButton(CMyWidget);
} // setupUi
};
//使用命名空间 重新定义中间类名称
namespace Ui {
class CMyWidget: public Ui_CMyWidget {};
} // namespace Ui
可以发现,Ui_CMyWidget类并没有继承QWidget等窗口类,说明它自己不是什么窗口对象,它的主要内容仅仅是我们在Designer中绘制的那个按钮控件及其相关的属性配置代码。
基于上述分析,对自主代码(CMyWidget类)来说,ui文件是一种透明的存在。它在作用上,接近宏定义,因为你完全可以像展开宏一样,在界面主类(CMyWidget)的构造函数中展开setupUi函数的内容,然后,删除ui文件。
设计师绘制与代码编写
@ QtCreator中使用代码和Designer绘制,混合的来编写UI界面 @ 无疑,QtDesigne的存在,方便了ui开发,但是,有时候,代码编写和布局ui会更加的灵活。所以,如果"即手动绘制(包含创建和布局),也代码编写(创建和布局)",感觉这种模式在一定程度上能提高编程效率。但是能不能呢? 基于前边章节的分析,我们已经有了答案,是能的,但是为了心安理得的这么来做,我们进一步来证明下。
(注意-如下的插图中,带点矩阵的是UI设计器中的截图,不带点阵的是Demo运行截图。)
前情回顾
另外的, 之前我们已经验证过,控件在窗口中的布局显示与是否指定父窗口是没关系的。后来从GitHub上读到些ui工程的源码,发现举例:某组控件的父窗口全部指定为FormA,但是我们却可以将这组控件布局并显示在FormB窗口中(FormB可以是FormA的子窗口、并列窗口…)。顾补充如下:控件的布局和最终的显示,与其指定的父窗口对象(Designer绘制的控件是默认带父窗对象的)没有关系。很高兴,感觉离自由的混合绘制和Code界面不远了,接着遇见了下边的难题…
一次小事故
下边以简化的Demo来描述事故:在一个空白mywidget.ui文件中绘制两个按钮控件并将它们局部的左垂直布局,然后去到CMyWidget构造函数的setupUi代码行后执行:
//测试-1
CMyWidget::CMyWidget(QWidget *parent) :
QWidget(parent),
ui(new Ui::CMyWidget)
{
ui->setupUi(this);
QHBoxLayout *pHLayoutFram1 = new QHBoxLayout();
QHBoxLayout *pHLayoutFram2 = new QHBoxLayout();
ui->frame_1->setLayout(pHLayoutFram1);
ui->frame_2->setLayout(pHLayoutFram2);
//手绘的垂直布局(verticalLayout自动带父窗)
pHLayoutFram1->addLayout(ui->verticalLayout);
pHLayoutFram1->addStretch(5);
//自定义垂直布局
QVBoxLayout *pVBoxLayout = new QVBoxLayout();
pVBoxLayout->addWidget(ui->toolButton_2_1);
pVBoxLayout->addWidget(ui->toolButton_2_2);
pHLayoutFram2->addLayout(pVBoxLayout);
pHLayoutFram2->addStretch(5);
pHLayoutFram2->addSpacing(5);
}
//测试-2 //替换对应行如下
QVBoxLayout *pVBoxLayout = new QVBoxLayout(this);
通过运行效果,将已经存在的ui中绘制的布局,用addLayout加入到另一个主代码中创建的布局时,不生效或效果异常。于是猜测,若某个布局在创建时指定了父窗口,则无法将它addLayout到另一个布局中。打开上述测例中中ui_mywidget.cpp文件,可以发现ui->verticalLayout的创建代码:
verticalLayout = new QVBoxLayout(layoutWidget);
在Designer中创建的这个verticalLayout布局确实有一个父类窗口!!!然后,我有产生了一堆问号???
- 哪里来了一个 QWidget *layoutWidget; 我根本没有绘制这个东西!
- 很明显Designer是支持多布局嵌套的,解析器在解析这种布局时,肯定存在一种规则,控制者new布局时是否指定父窗口!
ui中的布局设置到窗口
结合上述测试例子,我们基本得出的结论是,如果将一个已经指定父窗口创建的布局(如测试1中的verticalLayout、测试2中的pVBoxLayout),加入到另一个代码编写的布局对象(pHLayoutFram1、pHLayoutFram2)时,是看上去无效的。但是我们也发现,如果不嵌套这层pHLayoutFram布局,而是直接的将(即.测试1中的verticalLayout、测试2中的pVBoxLayout)的布局设置到窗口frame1和frame2,是生效的,效果图和测试代码如下:
//直接使用手绘布局
ui->verticalLayout->setContentsMargins(9,9,9,9);
ui->verticalLayout->addSpacing(6);
ui->frame_1->setLayout(ui->verticalLayout);
//使用自定义布局
QVBoxLayout *pVBoxLayout = new QVBoxLayout(this);
pVBoxLayout->addWidget(ui->toolButton_2_1);
pVBoxLayout->addWidget(ui->toolButton_2_2);
pVBoxLayout->setContentsMargins(9,9,9,9);
pVBoxLayout->addSpacing(6);
ui->frame_2->setLayout(pVBoxLayout);
透过现象(1-1和1-2按钮乖乖的约束在了fram-1中),我们接下来将分析,为啥那些个指定了父对象的布局,直接被setLayout时是生效的,但是当使用addLayout时却是无效的,Here采用的办法是跟踪源码…
//外部调用 //param 'this' is a widget
QHBoxLayout *pHLayout = new QHBoxLayout(this);
//第一步 父窗口parent传给到父类
QBoxLayout::QBoxLayout(Direction dir, QWidget *parent)
: QLayout(*new QBoxLayoutPrivate, 0, parent)
//第二部 将自己设置成父窗的主布局
QLayout::QLayout(QWidget *parent)
: QObject(*new QLayoutPrivate, parent)
{
if (!parent)
return;
parent->setLayout(this);
}
void QWidget::setLayout(QLayout *l) //Qt 5.12 函数定义-全
{
//不能设置为空
if (Q_UNLIKELY(!l)) {
qWarning("QWidget::setLayout: Cannot set layout to 0");
return;
}
//不能设置为相同的布局
if (layout()) {
if (Q_UNLIKELY(layout() != l))
qWarning("QWidget::setLayout: Attempting to set QLayout \"%s\" on %s \"%s\", which already has a"
" layout", l->objectName().toLocal8Bit().data(), metaObject()->className(),
objectName().toLocal8Bit().data());
return;
}
QObject *oldParent = l->parent();
//已经存在布局 且不与待设置的布局对象相同
if (oldParent && oldParent != this) {
if (oldParent->isWidgetType()) {
// Steal the layout off a widget parent. Takes effect when
// morphing laid-out container widgets in Designer.
QWidget *oldParentWidget = static_cast<QWidget *>(oldParent);
//这是关键 //删除原布局
oldParentWidget->takeLayout();
} else {
qWarning("QWidget::setLayout: Attempting to set QLayout \"%s\" on %s \"%s\", when the QLayout already has a parent",
l->objectName().toLocal8Bit().data(), metaObject()->className(),
objectName().toLocal8Bit().data());
return;
}
}
Q_D(QWidget);
l->d_func()->topLevel = true;
//替换为新布局
d->layout = l;
if (oldParent != this) {
l->setParent(this);
l->d_func()->reparentChildWidgets(this);
l->invalidate();
}
if (isWindow() && d->maybeTopData())
d->topData()->sizeAdjusted = false;
}
void QBoxLayout::insertLayout(int index, QLayout *layout, int stretch)
{
...
QBoxLayoutItem *it = new QBoxLayoutItem(layout, stretch);
d->list.insert(index, it);
...
}
void QBoxLayout::insertWidget(int index, QWidget *widget, int stretch,
Qt::Alignment alignment)
//其实现与insertLayout类似 都是Item的管理 其中都不包含父对象的处理
关键语句为 parent->setLayout(this); 把自己设置成了parent的主布局,这就参了。一开始想到的方案是,在外部执行this->setLayout(nullptr);这样将设置的主布局再取消掉,然后再将pHLayout加入到真正的主布局中,但是却验证失败。因为 QWidget::setLayout: Cannot set layout to 0
真的不行吗
如果为某个布局,如QHBoxLayout对象在创建时指定了父窗口,则这个布局再插入到其它布局中时,是"显示无效"的。而在UI中进行布局绘制时,生成的布局对象,那些"最外层的"总被编译成带父对象。这就尴尬了,我如果手绘一个最外层布局,想在代码中调用,实话变的不好办了,影响到了自由使用代码和绘图混合编写Uide大计…
"最外层"布局概念
前边提到过好几次"最外层布局"这个概念,这是个人造,它的语义依托于,ui是对主体界面类透明的。而下边的测试也进一步论证了这个关于"透明"的说法。在ui文件生成的中间类(Ui::CMyWidge)中,每个布局对象必须要找到一个窗口做依托,若找不到,则会自动生成一个(下边的例子会具体说明)。下边将详细的测试:在UI中当嵌套绘制多层布局、在ui中没有依托窗口的最外布局,QtDesigner生成的UI文件编译后是怎样的?
从中间代码看(中层布局)
下边的例子,主要用来说明,处于中间级别的布局(如verticalLayout_2),在自动生成的代码中,并不指定父窗。在Designer中绘制如上图的简单窗口,生成Ui::CMyWidget类的主要代码片段截取如下:(其中,红色框是布局对象的显示,左边是verticalLayout,右边小是verticalLayout_2,右边大是verticalLayout_3…)
QWidget *layoutWidget;
QVBoxLayout *verticalLayout;
QWidget *layoutWidget1;
QVBoxLayout *verticalLayout_3;
QVBoxLayout *verticalLayout_2;
void setupUi(QWidget *pMyWidget)
{
layoutWidget = new QWidget(pMyWidget);
verticalLayout = new QVBoxLayout(layoutWidget);
layoutWidget1 = new QWidget(pMyWidget);
verticalLayout_3 = new QVBoxLayout(layoutWidget1);
verticalLayout_2 = new QVBoxLayout();
verticalLayout_3->addLayout(verticalLayout_2);
} // setupUi
这里我们重点关注verticalLayout_2对象,发现其在new时是没有指定父对象的,而最外层的布局对象在ui_中间文件中的创建,统统被指定了父窗口(如layoutWidget、layoutWidget_3)。还有一件奇怪的事情,解释器竟然为每个最外层布局虚拟出来了一个父窗口对象(这些个虚出来的窗口对象,会在下文有所描述)。
虚拟出来的窗口
这个测试主要是强迫症的驱使,想搞明白"虚拟窗口"是怎么出来的。接下来我们新建一个frame,然后将上述两个外层布局拖到其中并进行一次水平布局:
重新查看新绘制布局编译生成的中间代码,上述"虚拟出来的"layoutWidget、layoutWidget1统统都消失了,因为现在没有任何的一个外层布局? 是的!现在应该能很明白什么是"最外层布局"啦!
//void setupUi(QWidget *pMyWidget)
verticalLayout_3 = new QVBoxLayout();
verticalLayout_2 = new QVBoxLayout();
verticalLayout = new QVBoxLayout();
horizontalLayout = new QHBoxLayout(frame);
打断下让我们重新梳理"最外层布局"的概念,即为什么第一个例子中出现了两个虚拟的窗口layoutWidget和layoutWidget1,在第二个绘制中它们又消失了?
因为ui文件是一个透明的存在(前边讲过),而Layout必须是依托于某个widget存在的,而Ui_CMyWidget类中原本并没有任何QWidget对象来容纳这个布局。所以,当有一个最外层布局,解析器必须跟随创建一个窗口对象,当有两个最外层布局就会创建两个这样的QWidget窗口容器,有三个最外层布局…,当增加了一个frame后,最外层布布局个数变成了0,所以没有任何的布局容器了…
只有最外层布局对象创建时自动指定了父对象窗口frame。此处注意,若frame中没有进行水平布局而只是将控件放进去,则原先的最外层布局verticalLayout_3、verticalLayout在创建时依然保持着自动带虚拟父对象创建。
实现绘制布局的混编
ui解析器在布局创建方面的作用规律基本总结为,若果一个布局对象在绘制时,包含在更高级别的布局中,则它在生成的代码中,不会携带父窗口对象。否则,它就是一个当前ui绘图中的最外层布局对象,编译器还必须为它生成一个布局对象的容器(QWidget对象)窗口…
“布局的容器窗口”,这为解决不能混编最外层布局提供了突破口,如:上述案例中,我们不能在自主代码的布局汇总直接addLayout添加ui->verticalLayout_3,但是却可以直接添加它的容器ui->layoutWidget1窗口来使用。
不过,这种方法有个小毛病,编译器自动增加的这个,最外层布局依托的窗口类的名称不是那么固定,有时候叫widget有时候叫layoutWidget1,布局稍微有变动后,可能需要在代码中改动名称…
再粗暴的一个方法是,不要在ui中出现最外层布局,而是为它绘制一个如QFrame的容器窗口来对外使用,这样混合布局就方便多了…
组合使用多个UI的部分窗口
这里将的并不是将多个小UI整体罗列到一个大的窗口中进行显示,Here 表述的是:UI文件-A 有两部分窗口,分别为A-PartL/A-PartR; UI文件-B有也两部分窗口,分别为B-PartL/B-PartR; 另外存在一个负责显示的窗口WidgetC,C也有左右两个部分(假设左边是一个stackedWidget,右边是一个toolBox),现在要将A-PartL和B-PartL插入到stackedWidget,将A-PartR和B-PartR插入到toolBox进行显示。
在没有关于UI文件本质的调研之前,我们形成一个类似WidgetC的窗口显示,是很头疼的。
第一种方案,你可能没有A/B两个UI文件,将所有的绘制都在WidgetC对应的UI中绘制,这样不仅绘制起来很不方便,在类的实现上也会过于的臃肿,我不不喜欢这样,我喜欢自己干自己的,干干净净的…
第二种方案,分别将A-PartL和B-PartL、A-PartR和B-PartR实现为4个Qt设计师界面,这样也很烦人,因为通常,A-PartR中存在A-PartL的控制按钮,B也是。如果去C里建立RL两部分的关联,感觉要砸电脑…
第三种方案,这是曾经奢望的方案,现在要实现的方案。它既满足将一个A/B功能分装在不同的设计师界面类中,又能满足A/B的左右两部分操作在同一个类中关联。该方案基于前边章节的研究,如UI文件编译结果类不是Widget、在UI外部实现布局的规律分析、对setupUi(QWidget *pDependWidget)的分析…
UI绘制
UI-A L/R | UI-B L/R |
这里以UI-A文件为例讲解下其对应的设计师界面类的构建过程。
UI_A::UI_A(QWidget *parent) :
QWidget(parent),
ui(new Ui::UI_A)
{
ui->setupUi(this);
} //上述是Qt界面设计师类生成的,应该都很熟悉
改造如下:
A::A(QWidget *parent) :
QObject(), //修改为从QObject继承
ui(new Ui::A)
{
ui->setupUi(parent); //为ui_a.h中的Ui_A类指定依赖窗口
}
//ui_a.h定义
class Ui_A {...}
namespace Ui { class A: public Ui_A {}; }
//a.cpp
//A中左右两个部分的信号槽关系维护 与构建A的界面设计师类时完全一致
关于A/B的使用:
//导出A/B类中的窗口Part
Qwidget *A::ExpUiPart(int iRLType)
在WidgetC中对A的使用
m_pformA = new A(this);
QHBoxLayout *pHlayout1= new QHBoxLayout();
pHlayout1->addWidget(m_pformA->ExpUiPart(L));
pHlayout1->setContentsMargins(0, 0, 0, 0);
ui->stack_page_1->setLayout(pHlayout1);
//分别对应插入其他的三个UI-part 完成布局即可 不再赘述
运行效果片段截图如下:
上边的界面效果出来后,当时非常高兴,却全然不知自己已经掉进了坑里…
大坑(续上)
我发现之前已经写好的,使用QtDesigner中的“控件-右键-跳转到槽”功能建立起来的信号槽关系,统统的都不生效啦,纠结了半天,还单独写了测试用例进行了比对,最终将问题范围缩小到是 改动Qt设计师界面类的默认外部结构 导致的…
在正式揭晓答案前先来像一个问题,就是我们使用Qt设计师的转到槽功能时,Qt框架是如何帮助我们进行自动的信号槽连接的呢,即那个connect代码行在哪里?
Automatic connection of signals and slots provides both a standard naming convention and an explicit(明确的) interface for widget designers(设计器) to work to. By providing source code that c a given interface, user interface designers can check that their designs actually work without having to write code themselves.
我们很清楚Qt设计器为我们生成如下格式的槽函数,当我们用转到槽功能时:
void on_<widget name>_<signal name>(<signal parameters>);
更狠的,我们再新建带按钮的Qt设计器界面类(Dialog With Buttons Bottom)时,我们甚至都没有在头文件中发现上述提到的格式化的槽函数,但是我们点击时照样有响应。毋庸置疑的时,不管是自己手动connect控件槽函数,还是设计器为你生成、还是设计器为你做的更多,最终的底层实现都是一样的,那就是qt的元对象系统的connect信号槽机制…
A Dialog With Auto-Connect 在 Qt 帮助文档中的描述:
Although it is easy to implement a custom slot in the dialog and connect it in the constructor, we could instead use QMetaObject’s auto-connection facilities to connect the OK button’s clicked() signal to a slot in our subclass. uic automatically generates code in the dialog’s setupUi() function to do this, so we only need to declare and implement a slot with a name that follows a standard convention: void on__();
看到上边这句话,我恍然大悟,赶紧的有打开了moc文件和ui的预处理代码文件:
void setupUi(QWidget *Widget)
{ ...
lineEdit = new QLineEdit(Widget);
...
QMetaObject::connectSlotsByName(Widget);
} //setupUi
没错,就是QMetaObject::connectSlotsByName函数在默默的为你作了祝一切。最近很忙,不打算继续看这个函数的实现,有兴趣的自己去探索吧。这里再回过头来,说说上一节的坑:为了将两个UI文件中的4部分两两组合进入第三个窗口中,我们投机的将原本这两个设计器界面类的继承从QWidget调成了QObject类,但是我们却没有调整
setupUi(QWidget *Form)函数的接口,在实例实现中我们投机的将WidgetC的指针传递给了UIA和UIB类:
class Ui_A //ui_a.h
void setupUi(QWidget *A)
QMetaObject::connectSlotsByName(A);
在IDE下,UI文件的预处理类Ui_A中的connectSlotsByName函数需要出入QWidget *A参数,即传入设计器对外展示类A(继承自QWidget)的指针,IDE需要从中查找控件名称已匹配参数,然后进行内部自动的connect连接!而由于我们的投机不到位,将WidgetC的指针传了进去,当然找不到了,对外表现就是所有的Qt设计器生成的槽函数都不起作用了!
下边的章节将试图用另一种方案来解决这个问题,即解决找不到自动链接需要的控件名称的问题,其思路来源属于“subclass”(上文的English Help内容),用子类。
以继承方式使用UI文件
早些年,用Qt3.0和VS2007的时候,对于UI文件的使用,都是用直接继承的方式,在Qt的一些文档中,似乎都快忘记它啦!
尝试跳出坑
class A : public QWidget, public Ui_A //未使用命名空间
但是这里有个问题,我在C中创建A的对象时,我不希望A是多个窗口对象,因为这可能会影响A中的部件窗口在C中的显示。所以我只能是这样:
class A : public Ui_A { ...Q_OBJECT... }
A::A(QWidget *parent)
{ setupUi(parent); }
然后问题来了,编译不过去。异常出现在moc_ 中间文件中,如:
&Ui_A::staticMetaObject 错误行定义提示 ‘staticMetaObject’ is not a member of ‘Ui_A’ ,其实也是,Ui_A又不是从QObject继承的,果断去掉Q_OBJECT宏开关。重新编译是成功了,一运行,才意识到自己又跳回到坑里啦。只要setupUi函数的输入不是this,IDE似乎是无法进行自动connect链接的…
难道,难道,我为了穿插着使用UI文件中的部件,我只能手动去connect控件信号槽了吗,我陷入了良久的思考…
继承UI类
class A : public QWidget, public Ui_A //未使用命名空间
{ ...
private slots:
void on_pushButton_clicked();
}
A::A(QWidget *parent)
{
setupUi(this); //这里从传递WidgetC的指针变换为传递自己咯
}
在上述构造形式下,想要在Qt设计器中使用转到槽功能添加一个槽时,提示查找添加槽错误;
出现上述错误的原因是,你在某些名字的使用上并未遵循Qt IDE的要求。修改如下,就可以啦.
namespace Ui {
class A; //即增加了设计师工具需要的Ui::A -- 解析过程的需要
}
class A : public QWidget, public Ui::A
{
Q_OBJECT
void on_pushButton_2_clicked();
}
一种临时解决方案:
其实这里完全没有必要将UIA和UIB的继承关系从QWidget修改为QObject,事实上,只要在创建UIA和UIB对象时,不要指定Widget是他们的父窗口就可以万事大吉。UIA和UIB所代表的主体界面不会显现出来,也不再会出现信号槽失效问题…(无论是一继承还是创建ui对象的方式使用UI文件)