该文尝试QtIDE下ui文件的本质,包括ui文件内容接结构解析,ui文件编译后的中间文件结构解析。如何混合使用QtUI设计器和手写布局编写GUI界面,以达到较好的开发效率和人机效果。
当我们进行,添加Qt设计师界面类,或者是新建基于QWidget的应用程序时,均会自动生成后缀ui的文件。(个人理解)ui文件是的本质是一种xml文件,是QtDesigner环境使它能进行可视化编辑。下面贴一段,我们画一个最简单的ui界面,主要是为了后续章节的描述来定义名词。我们在mywidget.ui中仅仅绘制一个QToolButton按钮控件。
在QtCreator IDE中点击Forms下双击打开ui文件时,默认用UI设计师可视化,此时若切换到编辑或调试卡,或者直接用Notepad++打开,均会呈现xml格式,片段如下:
<ui version="4.0">
<class>CMyWidgetclass>
<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布局确实有一个父类窗口!!!然后,我有产生了一堆问号???
结合上述测试例子,我们基本得出的结论是,如果将一个已经指定父窗口创建的布局(如测试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的容器窗口来对外使用,这样混合布局就方便多了…