
•. 介绍
•. Qt的布局类
•. 在代码中对部件进行布局
•. 使用布局器的技巧
•. 向一個布局器中加入部件
•. 拉伸因子
•. 在布局器中放置自定义部件
•. 布局器的问题
•. 手动布局
•. 其它注意事项
Qt布局系统提供咯简单而又强大的在部件中自动放置子代部件的方法,它能确保部件们适当地使用咯它们的空间。
Qt包含咯一系列的布局管理类,它们被用来描述在程序的用户界面中的部件是如何排列的。这些布局器在自身可用的空间发生改变时会自动确定其中的部件的位置和大小,以确保它们的排列方式是不变的以及用户界面整体仍然是可用的。
所有的 QWidget 子类都可以使用布局器来管理它们的子代部件。 QWidget::setLayout ()函数将一個布局器应用到一個部件上。当某個布局器被以这种方式设置到一個部件上时,它会接管以下工作:
•.确定子代部件的位置。
•.为窗口确定适当的默认尺寸。
•.为窗口确定适当的最小尺寸。
•.对尺寸的改变(Resize)进行处理。
•.当以下内容发生改变时,自动更新:
•.子代部件的字体尺寸、文字或其它内容。
•.隐藏或显示一個子代部件。
•.删除子代部件。
Qt的布局类是为手写的C++代码而设计的,允许将单位简单地写成像素,∴它们非常容易理解及使用。针对那些使用Qt 设计师所创建的窗体而生成的代码也使用咯布局类。在设计界面时,Qt 设计师很有用,∵它避免咯通常的编译、链接、运行循环。
|
水平或竖直地将子代部件排列起来 |
|
|
用来组织多组按钮部件的容器 |
|
|
管理由输入部件和它们的关联文字标签组成的表单 |
|
|
在一個QGraphicsAnchorLayout中表示两個条目之间的锚(anchor) |
|
|
在这個布局器中,妳可以将Graphics View(图形视图)中的部件锚接(anchor)在一起 |
|
|
在网格中排列部件 |
|
|
带有标题的分组框 |
|
|
水平排列部件 |
|
|
几何管理器的基类 |
|
|
被QLayout 操作的抽象条目 |
|
|
布局属性,用来描述水平和竖直方向的大小调整策略 |
|
|
布局器中的空白 |
|
|
将部件层叠(Stack)起来,任何时候只能看到一個部件 |
|
|
将部件层叠(Stack)起来,任何时候只能看到一個部件 |
|
|
竖直排列部件 |
|
|
布局器中的条目,表示一個部件 |
最简单的为你的部件们提供一個良好布局的方法就是使用内置的布局管理器: QHBoxLayout 、 QVBoxLayout 、 QGridLayout 和 QFormLayout 。这些类都继承自 QLayout , 而它又是继承自 QObject (不是 QWidget )的。它们为一组部件提供几何管理功能。要创建更复杂的布局的话,可以将布局管理器嵌套起来使用。
•. QHBoxLayout 将部件们放置在水平的一行上,顺序是从左到右(或者,对于right-to-left(从右到左)语言,是从右到左)。
•. QVBoxLayout 将部件们放置在竖直的一列上,顺序是从上到下。
•. QGridLayout 将部件们放置在二维的网格中。部件可占据多個单元格。
•. QFormLayout 以一個2列的“描述标签-域”(descriptive label- field)风格来放置部件。
以下的代码创建咯一個 QHBoxLayout ,它管理着5個 QPushButtons 的几何属性,上面的第一個截屏中显示的就是代码的效果:
QWidget * window = new QWidget ;
QPushButton * button1 = new QPushButton ( "One" );
QPushButton * button2 = new QPushButton ( "Two" );
QPushButton * button3 = new QPushButton ( "Three" );
QPushButton * button4 = new QPushButton ( "Four" );
QPushButton * button5 = new QPushButton ( "Five" );
QHBoxLayout * layout = new QHBoxLayout ;
layout -> addWidget(button1);
layout -> addWidget(button2);
layout -> addWidget(button3);
layout -> addWidget(button4);
layout -> addWidget(button5);
window -> setLayout(layout);
window -> show();
针对 QVBoxLayout 的代码是同样的,只有创建布局器的那一行不同。针对 QGridLayout 的代码有些不同,∵我们需要指定子代部件的行和列位置:
QWidget * window = new QWidget ;
QPushButton * button1 = new QPushButton ( "One" );
QPushButton * button2 = new QPushButton ( "Two" );
QPushButton * button3 = new QPushButton ( "Three" );
QPushButton * button4 = new QPushButton ( "Four" );
QPushButton * button5 = new QPushButton ( "Five" );
QGridLayout * layout = new QGridLayout ;
layout -> addWidget(button1 , 0 , 0 );
layout -> addWidget(button2 , 0 , 1 );
layout -> addWidget(button3 , 1 , 0 , 1 , 2 );
layout -> addWidget(button4 , 2 , 0 );
layout -> addWidget(button5 , 2 , 1 );
window -> setLayout(layout);
window -> show();
第三個 QPushButton 跨越咯2列。通过将 QGridLayout::addWidget ()的第四個参数指定为2就可以做到这一点。
QFormLayout 会在一行添加2個部件,通常是一個 QLabel 和一個 QLineEdit ,这样来创建表单。将一個 QLabel 和一個 QLineEdit 添加到同一行的话,就会将那個 QLineEdit 设置成那個 QLabel 的伙伴(buddy)。以下代码会在一個 QFormLayout 中放置3個 QPushButtons ,并在每一行放置一個对应的 QLineEdit 。
QWidget * window = new QWidget ;
QPushButton * button1 = new QPushButton ( "One" );
QLineEdit * lineEdit1 = new QLineEdit ();
QPushButton * button2 = new QPushButton ( "Two" );
QLineEdit * lineEdit2 = new QLineEdit ();
QPushButton * button3 = new QPushButton ( "Three" );
QLineEdit * lineEdit3 = new QLineEdit ();
QFormLayout * layout = new QFormLayout ;
layout -> addRow(button1 , lineEdit1);
layout -> addRow(button2 , lineEdit2);
layout -> addRow(button3 , lineEdit3);
window -> setLayout(layout);
window -> show();
当妳使用布局器的时候,妳不需要在构造子代部件的时候指定亲代部件(parent)。布局器会自动(使用 QWidget::setParent ())指定它们的亲代部件,这样的话,它们就成为安装咯这個布局器的部件的子代部件。
注意:位于布局器中的部件是安装咯该布局器的那個部件的子代部件,而不是该布局器的子代部件。部件的亲代对象必须也是部件,而不能是布局器。
妳可以使用 addLayout() 来在一個布局器中嵌套其它的布局器;内部的布局器会成为外面的布局器的子代对象。
当妳向布局器中添加部件时,布局过程是这样的:
1. 所有的部件都会根据它们的 QWidget::sizePolicy ()和 QWidget::sizeHint ()获得一块空间。
2.如果有任何部件设置咯大于0的拉伸因子的话,那么它们会按照拉伸因子的比例获得空间(下面细说)。
3. 如果有任何部件的拉伸因子被设置成0的话,那麽它们会在没有其它部件想要更多空间的情况下获得更多的空间。在这些部件中,拥有 Expanding (扩张)尺寸策略的部件会先获得空间。
4.所获得的空间小于它们的最小尺寸(或者在没有设置最小尺寸的情况下是按照最小尺寸提示来判断)的部件会获得它们所要求的最小尺寸的空间。(在拉伸因子成为它们的决定性因素的情况下,部件不需要拥有最小尺寸或最小尺寸提示。)
5.所获得的空间大于它们的最大尺寸的部件会获得它们所要求的最大空间。(在拉伸因子成为它们的决定性因素的情况下,部件不需要拥有最大尺寸。)
通常,部件的创建的时候没有指定拉伸因子。当它们被放置到布局器中的时候,会按照 QWidget::sizePolicy ()或最小尺寸提示中较大的值来分配空间。拉伸因子被用来改变部件们互相之间的空间分配比例。
如果我们使用 QHBoxLayout 来放置三个没有拉伸因子的部件,则布局将是这样的:
如果我们为每個部件设置拉伸因子,则它们会按照比例进行布局(但绝不会小于它们的最小尺寸提示),例如
当妳创建妳自己的部件类时,妳也应该传递(communicate)它的布局属性。如果这個部件拥有Qt 的布局器中的一個,则这個问题已经处理好咯。如果那個部件没有子代部件,或者是使用手动布局的话,那么妳可以使用以下机制中的部分或全部来改变那個部件的行为:
•. 重载 QWidget::sizeHint (),返回这個部件的偏好尺寸。
•. 重载 QWidget::minimumSizeHint (),返回这個部件可以承受的最小尺寸。
•. 调用 QWidget::setSizePolicy ()来指定这個部件的空间要求。
每当尺寸提示、最小尺寸提示或尺寸策略发生改变时,调用 QWidget::updateGeometry ()。这会导致重新计算布局。连续多次调用 QWidget::updateGeometry ()只会引起一次布局重新计算。
如果妳的部件的偏好高度取决于它的实际宽度(比如,一個自动换行的文本标签)的话,则在那個部件的尺寸策略( size policy )中设置 height-for-width 标志并且重载 QWidget::heightForWidth ()。
即使妳重载咯 QWidget::heightForWidth (),也最好还是提供一個合理的sizeHint()。
要了解关于重载这些函数的更多提示,则参考 Qt Quarterly 中的文章 依据宽度确定高度 。
在文本标签部件中使用富文本可能会引起它的亲代部件的布局器的某些问题。原因是,当文本标签是自动换行的时候,Qt 的布局管理器处理富文本的方式有问题。
在特定情况下,亲代布局器会被置于 QLayout::FreeResize 模式,这意味着,它不会在极小的窗口中调整它的内容的布局,甚至可能会阻止用户将窗口变得太小。这一点可以克服,只要对有问题的部件进行子类化,再重载实现合适的 sizeHint() 和 minimumSizeHint() 函数。
某些情况下,向部件添加一個布局器是有特殊作用的(relevant)。当妳(使用 QDockWidget::setWidget ()和 QScrollArea::setWidget ())设置一個 QDockWidget 或 QScrollArea 的部件时,那個部件上必须已经设置有布局器。否则,那個部件就不可见。
如果妳是要创建一個独一无二(one-of-a-kind)的布局,那麽妳也可以按照上面说的创建一個自定义的部件。重载 QWidget::resizeEvent ()以便计算尺寸的分布,并且对每個子代部件调用 setGeometry() 。
当布局需要重新计算时,这個部件会收到一個 QEvent::LayoutRequest 类型的事件。 重载 QWidget::event ()以处理 QEvent::LayoutRequest 事件。
除咯手动布局之外,还可以通过子类化 QLayout 来写出妳自己的布局管理器。 Border Layout 和 Flow Layout 示例展示咯如何做到这一点。
这里,我们详细说明一個示例。 CardLayout 类是按照Java 中同名的布局管理器来设计的。它将条目们(部件或是嵌套的布局器)摞在一起,每個条目的偏移值是由 QLayout::spacing ()设置的。
要写出妳自己的布局类,妳必须定义以下东西:
•. 一個数据结构,用来储存被这個布局器管理的条目。每個条目都是一個 QLayoutItem 。在这個示例中,我们会使用 QList 。
•. addItem() ,如何将条目添加到这個布局器。
•. setGeometry() ,如何进行布局操作。
•. sizeHint() ,这個布局器的偏好尺寸。
•. itemAt() ,如何遍历这個布局。
•. takeAt() ,如何从这個布局器中删除条目。
在大部分情况下,妳还会重载 minimumSize() 。
#ifndef CARD_H
#define CARD_H
#include <QtGui>
#include <QList>
class CardLayout : public QLayout
{
public :
CardLayout( QWidget * parent , int dist): QLayout (parent , 0 , dist) {}
CardLayout( QLayout * parent , int dist): QLayout (parent , dist) {}
CardLayout( int dist): QLayout (dist) {}
~ CardLayout();
void addItem( QLayoutItem * item);
QSize sizeHint() const ;
QSize minimumSize() const ;
QLayoutItem * count() const ; //(☯,注意,这里的代码有错)
QLayoutItem * itemAt( int ) const ;
QLayoutItem * takeAt( int );
void setGeometry( const QRect & rect);
private :
QList < QLayoutItem *> list;
};
#endif
//#include "card.h"
首先,我们定义 count() ,用来获取列表中的条目的个数。
QLayoutItem * CardLayout :: count() const //(☯,注意,这里的代码有错)
{
// QList::size()返回列表中的QLayoutItem的个数
return list . size();
}
然后我们定义两个函数,它们会遍历布局: itemAt() 和 takeAt()。这些函数被布局系统内部用来删除部件。程序猿也可以使用这些函数。
itemAt() 返回指定索引的条目。 takeAt() 删除指定索引的条目,并且返回它。在这种情况下我们将列表索引用作布局索引。在其它有更复杂的数据结构的情况下,我们可能要费更大的劲来定义这些条目的线性顺序。
QLayoutItem * CardLayout :: itemAt( int idx) const
{
// QList::value()会对索引进行检查,如果我们超出咯有效范围,则返回0
return list . value(idx);
}
QLayoutItem * CardLayout :: takeAt( int idx)
{
// QList::take不做索引检查
return idx >= 0 && idx < list . size() ? list . takeAt(idx) : 0 ;
}
addItem() 为布局条目实现咯默认的位置策略。这個函数必须实现。它被 QLayout::add ()以及用某個布局作为亲代对象的 QLayout 构造函数使用。如果妳的布局类具有要求参数的高级位置选项的话,那么妳必须提供额外的访问函数,例如 QGridLayout::addItem ()、 QGridLayout::addWidget ()和 QGridLayout::addLayout ()的行和列跨越参数的重载。
void CardLayout :: addItem( QLayoutItem * item)
{
list . append(item);
}
布局器会接管那些被添加到其中的条目。由于 QLayoutItem 不继承 QObject ,∴我们必须手动删除条目。在析构函数中,我们使用 takeAt() 删除列表中的每个条目,再删除它。
CardLayout ::~ CardLayout()
{
QLayoutItem * item;
while ((item = takeAt( 0 )))
delete item;
}
setGeometry() 函数是真正进行布局的函数。以参数传入的矩形不包含 margin() (边距)。如果合适(relevant)的话,则使用 spacing() 作为条目之间的空隙。
void CardLayout :: setGeometry( const QRect & r)
{
QLayout :: setGeometry(r);
if (list . size() == 0 )
return ;
int w = r . width() - (list . count() - 1 ) * spacing();
int h = r . height() - (list . count() - 1 ) * spacing();
int i = 0 ;
while (i < list . size()) {
QLayoutItem * o = list . at(i);
QRect geom(r . x() + i * spacing() , r . y() + i * spacing() , w , h);
o -> setGeometry(geom);
++ i;
}
}
sizeHint() 和 minimumSize() 的具体实现在一般情况下是很相似的。这两个函数返回的尺寸都应当包含 spacing() ,但不包含 margin() 。
QSize CardLayout :: sizeHint() const
{
QSize s( 0 , 0 );
int n = list . count();
if (n > 0 )
s = QSize ( 100 , 70 ); //首先使用一個较合适的默认尺寸
int i = 0 ;
while (i < n) {
QLayoutItem * o = list . at(i);
s = s . expandedTo(o -> sizeHint());
++ i;
}
return s + n * QSize (spacing() , spacing());
}
QSize CardLayout :: minimumSize() const
{
QSize s( 0 , 0 );
int n = list . count();
int i = 0 ;
while (i < n) {
QLayoutItem * o = list . at(i);
s = s . expandedTo(o -> minimumSize());
++ i;
}
return s + n * QSize (spacing() , spacing());
}
•.这個自定义的布局器不处理针对宽度的高度(height for width)。
•. 我们无视咯 QLayoutItem::isEmpty ();这意味着这個布局器会将隐藏的部件当成可见的部件来处理。
•. 对于复杂的布局器,通过对计算值进行缓存可以显著地提高速度。在那种情况下,实现 QLayoutItem::invalidate ()以便将缓存数据标记为脏的(dirty)。
•. 调用 QLayoutItem::sizeHint ()等等函数可能会很耗时间(expensive)。∴,如果妳稍后在同一個函数里需要使用这個值的话,就应当存储在一個局部变量里。
•. 妳不应当在同一個函数中对同一個条目调用再次 QLayoutItem::setGeometry ()。如果那個条目有多個子代部件的话,这個调用可能会很耗时,∵布局管理器每次都必须进行一個完整的布局计算。作为替代,妳应当先计算好几何属性,再设置。(这一点不仅限于布局器,比如说,如果妳实现妳自己的resizeEvent(),则也应当这样做。)
HxLauncher: Launch Android applications by voice commands