QtGUI-灵活使用UI文件

    技术2024-11-03  21

    文章目录

    概述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/RUI-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文件)

    Processed: 0.013, SQL: 9