欢迎光临
我们一直在努力

解读 Qt “对象树”和“界面结构树”的底层逻辑

在这里插入图片描述 刚开始尝试编写 Qt 代码时,我们会遇到这样一类问题:创建好了一个 menu,也把 parent 设为了 menuBar 但却显示不出来;创建好了一个 action,也把 parent 设为了 menu 也显示不出来,后来发现还必须调用 QMenuBar::addMenu() 把新建的 menu 添加给 menuBar 以及调用 QMenu::addAction() 把新建的 action 添加给 menu 才能在界面上看到它们。于是,这就引出了我们的问题:为什么设置了 parent 却没有实现我们想要的“添加”效果,还要再另外调用 add 方法?现然两个方法肯定做了不一样的事,在 Qt 里是怎么定义:“子设置父”和“父添加子”这两种行为的?是否分别赋予了不同的含义?这个问题涉及到的 Qt 中的两个重要概念,一个是“对象树”,另一个没有一致的称谓,本文使用“界面结构树”指代,后文会详细解释。

在我们展开讨论前,我觉得有必要给有经验的程序员提一个醒:我们知道,在所有具有层级关系的数据结构中,关联“父子关系”都有两个独立动作:1) 子对象设置 parent;2) 父对象添加 child,一套数据结构的设计者可以选择在“子对象设置 parent 时同步调用 parent 的 addChild() 方法完成双向关联”,也可以选择在“父对象添加 child 时同步调用 child 的 setParent() 方法完成双向关联”,或者什么都不做,让用户自己维护是否需要建立双向关联关系。

但是,在 Qt 里情况完全不同,Qt 的所有控件确实都有 setParent() 方法,部分的容器型控件和布局也有 addXxx() 方法,但是这两套方法和树形数据结构中所谓的“建立父子双向关联”不能说没有一点关系没有,但关系确实不大。笼统地说:Qt 依靠设置父控件(setParent() 方法)建立“对象树”,依靠添加子控件(addXxx() 方法)建立“界面结构树”,这是两条相对独立的逻辑线但在一些地方又会有交叉,比如:在某些容器型控件添加子控件时会自动调用 setParent() 方法把子控件的 parent 设为自己,而 Layout 和另外一些容器型控件(如 QButtonGroup)则不会,再比如有的控件仅需 setParent 即可,而有的需要在 setParent 的同时还要其父控件主动 add 它,否则控件将不可见。所有这些情形,我们都会在本文逐一讨论。

1. 明确概念

❏ 界面结构树

UI 界面上各种控件因布局(自上到下的排布关系)会天然地形成一种“树形结构”,这种“树型结构”是一种视觉上的,逻辑上的,虽然 Qt 官方没有给这种树形结构起一个专属名称,但它是“确确实实”存在的,主要体现在“我们需要显式地调用某种 add 方法将视觉上(逻辑上)具有上下级关系的布局、控件装配在一起”,具体到代码层面就是指容器类控件和布局的 addXxx() 子控件方法,例如:QLayout::addWidget()、QMenuBar::addMenu()、QMenu::addAction() 等。由于没有找到一种广泛使用的称谓,本文我们暂且把这种树形结构称为“界面结构树”。在《手把手带你写 Qt 应用主窗体》一文中,我们绘制了一个典型窗体应用的界面结构树,这里引用一下作为一个示例:

QMainWindow ( mainWindow )
├── QMenuBar ( menuBar )
│ └── QMenu ( menuFile )
│ └── QAction ( actionOpen )
├── QToolBar ( toolBar )
│ └── QAction ( actionOpen )
├── QWidget ( centralWidget )
│ └── QVBoxLayout ( verticalLayout )
│ └── QTextEdit ( textEdit )
└── QStatusBar ( statusBar )

❏ 对象树

不同于界面结构树,Qt 中的“对象树”是一个非常明确的概念,也是官方给出的命名。在 Qt 中所有的 QObject(当然包括所有控件)都有一个 parent(父 QObject),在创建时或通过 setParent() 方法指定,通过这种关联方式可以把 UI 上的所有控件组织成一种树状的层级结构,被称为“对象树”。Qt 设计这种“树状结构”的主要动机是:依靠自上而下的层级结构来管理控件的“内存”和“生命周期”,简单地说就是:父控件会维持一个 children 控件列表,当父控件被销毁时会自动销毁其所有子控件,并释放内存。在《手把手带你写 Qt 应用主窗体》一文中,我们绘制了一个典型窗体应用的对象树,和前面的界面结构树是相对应的:

QMainWindow ( mainWindow )
├── QMenuBar ( menuBar )
│ └── QMenu ( menuFile )
├── QToolBar ( toolBar )
├── QAction ( actionOpen )
├── QWidget ( centralWidget )
│ └── QTextEdit ( textEdit )
└── QStatusBar ( statusBar )

通过和前面的界面结构树作对比可以看出至少有点不同:1) 布局是不会在对象树中存在的(布局实质上是一种算法,不像控件那样占据内存还有生命周期);2) action 在对象树中的 parent 是主窗体,而不是 menu 或 toolBar )

2. 底层逻辑

尽管从概念上看界面结构树和对象树很相似,似乎分辨不出太大的差异,但是如果从 API 上看,它们是有本质区别的。对象树的组织逻辑是:如果 B 控件依赖于 A 控件而存在,当 A 被销毁时,B 绝不应该单独存在,则 A、B 之间是对象树中的父子关系”;而界面结构树的组织逻辑是:如果在视觉上(或逻辑上) B 控件从属于 A 控件且必须依赖 A 控件才能正常展示(例如必须根据 A 的大小和位置才能计算出 B 应该的大小和位置,或者是要继承 A 的可见性配置(父控件隐藏 → 子控件也会隐藏)),则 A、B 之间是界面结构树中的父子关系。

在上述的描述中,你应该可以敏锐地觉察到:界面结构树和对象树的“底层组织逻辑”是不一样的,前者关注的是控件在可视层面上的“联动”关系(父控件会影响子控件的显示,必须添加它进行统筹管理),后者关注的是控件的生命周期,与控件展示逻辑关联很小,这就决定了它们并不总是一致的!

否则在 Qt 中就不会出现 addXxx() 和 setParent() 两套 API 了,因为如果它们总是一致的,使用其中一套 API 就可以建立起树状结构。更不妙的是:虽然是两套 API,但它们又不是完全切割的,在某些情况下又会有“交叉”,这是我学习 Qt 过程中体会到一个比较明显的问题, Qt 的在一些方面的设计真得不好,大面上的原则说要“隔离”,但某些细节又会“犯规”,打自己的脸。

3. 不一致的情形

承接前文,既然界面结构树和对象树的“底层组织逻辑”不一样,则必然会出现各种“不一致”的情形,但是这些情形并不是某一种单一维度下的,而是多种维度交叉起来的,所以你可能会觉得它们之间的条理性不太好,主要原因是这两种树在理论上是独立的,但是构建它们的 API:addXxx() 和 setParent() 在某些情况下会有耦合,再加上有一些“不一致”本就是应该的,所以会有一些复杂。

3.1 加入界面结构树和对象树,但父对象不一致 ➩ 本就应该的正确配置

这是最能体现界面结构树和对象树差异的一种情形,是正常情形。示例一:在设置 QAction 的 parent 时,一般会设置为父窗体,而不是一个 QMenu 或 QToolBar,因为一个 QAction 可以同时存在于多个菜单或工具栏中,属于“全局共有”的一类组件(个人认为 QAtion 的设计也不是一个能经得起严格推敲的例证,严格来说 QMenu 下面应该再设计一个 QMenuItem 负责 UI 层面的逻辑封装,同理,QToolBar 下面也应该有一个 QToolButton,然后让它们两个关联同一个 QAction,这样才算是把 QAction 解放出来,变成游离于控件之外的受父窗体管理的控件);示例二: QRadioButton 的 parent 通常也是设置为父窗体而不是 QButtonGroup,因为 QButtonGroup 只管理单选按钮间的“互斥逻辑”,不负责管理它们的生命周期。

3.2 加入界面结构树时被自动加入对象树 ➩ 部分容器控件的典型操作

对于不少容器类型的控件来说,它们在使用 addXxx() 添加界面结构树上的子控件时,往往会同时调用子控件的 setParent() 方法,把自己设为其子控件的 parent,也就是同时完成了对象树部分的构建工作。典型的例子是:QMenuBar::addMenu() 和 QMenu::addAction(),所以通常添加一个 menu 的方法是直接在 menuBar 上调用 addMenu():

QMenu* fileMenu = menuBar()->addMenu(tr("&File"));

而不是 new 一个出来再设置 parent。上述 add 方法非常高效,做了好几个动作:

addXxx()
├── setParent()
├── 加入内部数据结构
├── 建立信号
├── 触发布局
└── 更新UI

下表汇总典型的容器型控件或布局是否会在addXxx() 时自动设置子控件的 parent:

类型addXxx() 是否会自动设置子控件的 parent (自动调用 setParent())
纯视觉容器 (QWidget) 通常会 setParent
布局管理器 (QLayout) ❌ 不会
逻辑分组 (QButtonGroup) ❌ 不会
可复用对象 (QAction) ❌ 不应
复杂组合容器 (QTabWidget) 可能重定向 parent

3.3 加入对象树但未加入界面结构树 ➩ 控件不可见

文章开始列举的 menu 和 menuBar 以及 action 和 menu 两个例子就是这种情况,属于未正确配置的案例。以下这些“容器型”控件,都需要明确使用 addXxx() 将子控件加入界面结构树(单独让子控件 setParent() 不会另其被显示 ):

容器仅 setParent 是否有效
QMenuBar
QMenu
QLayout
QStackedWidget
QTabWidget
QToolBar

此外,肯定还有其他一些情况,但不够典型,我们不再单独研究。

赞(0)
未经允许不得转载:171主机测评 » 解读 Qt “对象树”和“界面结构树”的底层逻辑
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址