第一章 基础,工具和第一行代码
*******************************************************************
******************* 1.2 布局,对象层级和内存管理 *************************
*******************************************************************
1.2.1 如何自动布置窗口部件(也可以叫控件)
为了扩展我们第一节的HelloWorld程序使它不仅仅是显示一个文本标签,而是像图1.2那样按顺序显示两个QLabel,我们使用QT的布局系统。这个系统自动安排GUI元素:包括窗口小部件和控件。类似于在印刷领域中的术语,我们称之为布局。
图1.2显示的界面的源代码如下:
![](https://i-blog.csdnimg.cn/blog_migrate/cdec0645add3fc3c328197dda5c76203.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/81178cc93a2a3bb5048d90d76e7ec935.gif)
1 // layout/main.cpp 2 #include < QApplication > 3 #include < QVBoxLayout > 4 #include < QLabel > 5 int main( int argc, char * argv[]) 6 { 7 QApplication a(argc, argv); 8 QWidget window; 9 QVBoxLayout * mainLayout = new QVBoxLayout( & window); 10 QLabel * label1 = new QLabel( " One " ); 11 QLabel * label2 = new QLabel( " Two " ); 12 mainLayout -> addWidget(label1); 13 mainLayout -> addWidget(label2); 14 window.show(); 15 return a.exec(); 16 }
除了1.1节中我们已经使用过的QApplication和QLabel,我们现在还要包含另外一个用来垂直排列小部件的类:QVBoxLayout,名字中的V代表垂直(vertical)。这一次我们使用QWidget对象作为应用程序的主窗口,而不是上一节的QLabel对象。使用这个名字仅仅是出于习惯。只需要两步,QWidget对象就可以成为一个独立的窗口。第一步,调用QWidget对象的构造器,如果像在本例中一样没有提供参数,新对象就没有父窗口,因此,处于对象继承层次的最顶层。第二步,小部件只有调用show()方法显示出来时才能成为一个窗口。
创建完QWidget后,我们创建了一个QVBoxLayout对象。我们在这里使用new操作符代替:
QVBoxLayout mainLayout(&window);
具体将在下面的1.2.2节中解释。因此,新创建的QVBoxLayout知道这表示window窗口的布局,QWidget对象的指针作为一个参数传给了它的构造器。
类似地,我们还创建了One和Two两个文本标签对象。为了使他们接受窗口布局对象的管理,我们通过QVBoxLayout对象的addWidget()方法将它们添加到布局中。
这个程序除此之外的部分与我们的第一个例子几乎没有区别。注意到我们仅仅调用了show()方法使得QWidget对象可见,这将使得QWidget包含的所有窗口部件都显示在屏幕上,在本例中就是那两个标签。
最后,我们像以前一样开始了事件循环,而且通过事件循环传回来一个返回值作为应用程序的返回值。简单的返回0将立即结束程序并且不再显示窗口。
这个程序显示了使用布局管理的主要优点:你不必关注窗口部件的确切位置。另外,用户可以缩放布局窗口,布局管理器将自动的调整窗口组件,确保他们合理的填充可用空间,而不需要开发者写详细的代码来实现。
布局管理也允许开发者自定义单个部件的行为:例如,当一个控件应该占满尽可能多的空间,或者需要限制大小时,又或者处理不需要太多垂直空间的部件,如CheckBox时。第五章详细介绍了如何实现这样的布局。
1.2.2 对象层次的内存管理
图1.2展示的应用程不仅在自动布局方面异于第一节的HelloWorld程序,在其它方面也有显著的不同。变量声明QWidget window用于分配内存给QWidget对象,但给QVBoxLayout和QLabel对象时,我们使用了new操作符,使用:
QVBoxLayout* mainLayout = new QVBoxLayout(&window);
而不是:
QVBoxLayout mainLayout(&window);
创建布局对象。
我们详述这个是因为C++不提供自动内存管理。一般来说,程序员必须自己小心处理。然而,正如下面提到的,QT接管了部分的内存管理工作。所有的类对象都继承自QObject形成了一个树形结构:对象可以拥有子对象。如果一个对象删除了,QT自动删除它所有的子对象,以及子对象的后代。但有个条件:为了使自动内存管理生效,所有的后代(以及后代的后代)都必须分配在堆(Heap)上,即必须使用new操作符创建它们。(因为可以QVBoxLayout mainLayout(&window);)
父子对象的关系:
QWidget window;
QVBoxLayout* mainLayout = new QVBoxLayout(&window);
树形结构:
QWidget
| | |
QVBoxLayout QLabel QLabel
从另一方面说,如果对象树的根节点消失了,QT会自动删除整个树。这缓解了程序员跟踪对象后代和释放它们占用的内存的负担。然而,为了使自动内存管理生效,所有的后代(以及后代的后代)都必须分配在堆(Heap)上,即必须使用new操作符创建它们。使用new操作符创建的对象都引用了一个指向堆的指针。这就是为什么mainLayout被声明为一个指向QVBoxLayout的指针,而不是一个QVBoxLayout对象。
不在堆上创建对象(即不使用new分配内存)是一个初学者的常见误区:窗口部件只被创建在栈(Stack)上,(例如,在一个类的构造器中),当进程结束时他们被编译器删除。虽然应用程序并不仅是简单的产生部件,但它们对于人来说,永远是不可见的。
而且,这个声明不仅仅创建了一个QVBoxLayout对象,也使它成为Qwidget对象窗口的子对象,通过QVBoxLayout类的构造器实现。与此相反,两个QLabel对象初始化的时候没有父对象,QLabel构造器仅仅初始化了标签的文本:
QLabel* label1 = new QLabel("One");
我们随后使用QVBoxLayout::addWidget()方法确保QWidget对象承担作为每个新标签父对象的责任。(事实上,一个窗口部件包含的GUI元素必须成为该对象的子对象。由此,QWidget对象成为QLabel对象的父对象,而不是我们可能以为的QVBoxLayout对象的子对象。)图1.3展示的树结构的创建过程就是这样的。
窗口对象的子对象,布局对象和两个标签都必须使用new操作符在堆上创建从另一方面说,我们使用QWidget window;在栈上创建了窗口.因此当应用程序关闭时我们不必手动删除它。(只有当对象没有父对象是你才可以这样做。)因而,在多数情况下,你应该使用new操作符创建继承自QObject的类对象。
1.2.3 其它布局类型
QHBoxLayout类用来水平布局元素,和用来垂直布局元素的QVBoxLayout类一样,它的界面也类似于QVBoxLayout。如果你用QHBoxLayout替换掉例子中的QVBoxLayout,图1.2将会显示成如下图所示:
这里还有一个使用QGridLayout网格布局的例子:
![](https://i-blog.csdnimg.cn/blog_migrate/cdec0645add3fc3c328197dda5c76203.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/81178cc93a2a3bb5048d90d76e7ec935.gif)
1 // gridLayout/main.cpp 2 #include < QApplication > 3 #include < QGridLayout > 4 #include < QLabel > 5 int main( int argc, char * argv[]) 6 { 7 QApplication a(argc, argv); 8 QWidget window; 9 QGridLayout * mainLayout = new QGridLayout( & window); 10 QLabel * label1 = new QLabel( " One " ); 11 QLabel * label2 = new QLabel( " Two " ); 12 QLabel * label3 = new QLabel( " Three " ); 13 QLabel * label4 = new QLabel( " Four " ); 14 QLabel * label5 = new QLabel( " Five " ); 15 QLabel * label6 = new QLabel( " Six " ); 16 mainLayout -> addWidget(label1, 0 , 0 ); 17 mainLayout -> addWidget(label2, 0 , 1 ); 18 mainLayout -> addWidget(label3, 1 , 0 ); 19 mainLayout -> addWidget(label4, 1 , 1 ); 20 mainLayout -> addWidget(label5, 2 , 0 ); 21 mainLayout -> addWidget(label6, 2 , 1 ); 22 window.show(); 23 return a.exec(); 24 }
这个程序和前一个例子差不多,只不过用QGridLayout取代了QVBoxLayout作为容器,这次包含了六个QLabel对象。与水平或垂直布局类的addWidget()方法不同的是,QGridLayout::addWidget()方法需要三个参数:要被指派的部件,以及该部件放置在在网格中的行和列的位置。网格的第一个格子的坐标是(0,0),位于左上角。结果如图1.5所示:
如果文本Five无法正确显示,可能的原因是源文件是以UTF-8编码格式保存的。你需要转换为ISO-8859-1或者 ISO-8859-15格式。在KDE的编辑器Kate中,这个选项可以在另存为对话框中看到。(Windows系统中也是类似的。)
你将在第五章中了解到关于布局管理的更多细节。我将介绍如何设计复杂的布局,例如通过嵌套布局。还将介绍手动布局,分裂器(Splitters)和QStackedLayout类。
分裂器的行为类似于水平布局和垂直布局,但是在额外的空白空间里显示被称之为手柄(handle)的东西。用户可以直接拖动手柄使对面的部件获得根多的空间。当拖动一个手柄时,窗口部件朝手柄拖动的方向收缩。
另一方面,QStackedLayout类管理一些面板,这些面板每个都可以包含多组部件,但只有一个可以显示。设置对话框可以使用这个类,例如,当用户选择这样一个设置对话框左边面板的一个目录时,右边的面板显示用来更改所选目录选项的窗口部件。当用户更改左边的目录时,QStackedLayout对象知道应该改在右边显示一个不同的页面。