Qt/GUI-.ui文件随心编辑

文章目录

  • 概述
  • ui文件的本质
  • 设计师绘制与代码编写
    • 前情回顾
    • 一次小事故
    • ui中的布局设置到窗口
  • 真的不行吗
    • "最外层"布局概念
    • 从中间代码看(中层布局)
    • 虚拟出来的窗口
  • 实现绘制布局的混编

概述

该文尝试QtIDE下ui文件的本质,包括ui文件内容接结构解析,ui文件编译后的中间文件结构解析。如何混合使用QtUI设计器和手写布局编写GUI界面,以达到较好的开发效率和人机效果。

ui文件的本质

当我们进行,添加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代码行后执行:
Qt/GUI-.ui文件随心编辑_第1张图片

//测试-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);

Qt/GUI-.ui文件随心编辑_第2张图片
通过运行效果,将已经存在的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,是生效的,效果图和测试代码如下:
Qt/GUI-.ui文件随心编辑_第3张图片

    //直接使用手绘布局
    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文件编译后是怎样的?

从中间代码看(中层布局)

Qt/GUI-.ui文件随心编辑_第4张图片
下边的例子,主要用来说明,处于中间级别的布局(如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,然后将上述两个外层布局拖到其中并进行一次水平布局:
Qt/GUI-.ui文件随心编辑_第5张图片
重新查看新绘制布局编译生成的中间代码,上述"虚拟出来的"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的容器窗口来对外使用,这样混合布局就方便多了…

你可能感兴趣的:(#,Qt/GUI,设计/实现)