Qt Tutorial 8 - Preparing for Battle
文件:
· tutorials/tutorial/t8/cannonfield.cpp
· tutorials/tutorial/t8/cannonfield.h
· tutorials/tutorial/t8/lcdrange.cpp
· tutorials/tutorial/t8/lcdrange.h
· tutorials/tutorial/t8/main.cpp
· tutorials/tutorial/t8/t8.pro
在这个例子中,我们介绍第一个可以自己画的自定义窗口部件。我们还加入了一个有用的键盘接口(仅用了两行代码)。
一行一行的品读
这个文件与第七章的lcdrange.h非常相似。我们添加了一个槽:setRange()。
void setRange(int minValue, int maxValue);
我们现在添加了设置LCDRange范围的可能性。直到现在,它被设置成了0到99。
这里构造函数有一个变化(我们会在后面进行讨论)。
void LCDRange::setRange(int minValue, int maxValue)
{
if (minValue < 0 || maxValue > 99 || minValue > maxValue) {
qWarning("LCDRange::setRange(%d, %d)/n"
"/tRange must be 0..99/n"
"/tand minValue must not be greater than maxValue",
minValue, maxValue);
return;
}
slider->setRange(minValue, maxValue);
}
setRange()槽设置了在LCDRange中的滑块的范围。因为我们已经把QLCDNumber设置为一直显示两位数字了,我们想通过限制范围值minVal和maxVal来避免QLCDNumber的溢出。(我们可以允许值低至-9但我们并没有选择这样做。)如果参数是非法的,我们使用qWarning()函数来向用户发出一个警告并立即返回。qWarning()是一个像printf的函数,默认发送它的输出到stderr。如果你想,你可以用qInstallMsgHandler()来安装你自己的处理函数。
CanonField是一个知道如何显示它自己的新自定义窗口部件。
CannonField 继承 QWidget. 我们使用了和LCDRange一样的方法.
int angle() const { return currentAngle; }
public slots:
void setAngle(int angle);
signals:
void angleChanged(int newAngle);
目前,CannonField只包含一个角度值,我们提供一个接口,这和在LCDRange中的值使用的是同样的方法。
protected:
void paintEvent(QPaintEvent *event);
这是我们在QWidget中遇到的众多事件处理器中的第二个。只要一个窗口部件需要刷新它自己(例如,画窗口部件的表面),这个虚函数就会被Qt调用。
又一次,我们试用和前一章的LCDRange同样的方法。
currentAngle = 45;
setPalette(QPalette(QColor(250, 250, 200)));
setAutoFillBackground(true);
}
构造函数初始化角度值为45度并为这个窗口部件设置了一个自定义调色板。
这个调色板用来表示背景颜色和选择其他合适的颜色。(对于这个窗口部件,实际上只有背景和文本的颜色将被用到。)然后我们调用setAutoFillBackground(true)来让Qt自动地填充背景。
QColor是一个形如RGB (red-green-blue) 三元组一样的指定值,每一个值介于0(暗)和255(亮)之间。我们也可以使用一个预定义的颜色如Qt::yellow来代替一个RGB指定值。
void CannonField::setAngle(int angle)
{
if (angle < 5)
angle = 5;
if (angle > 70)
angle = 70;
if (currentAngle == angle)
return;
currentAngle = angle;
update();
emit angleChanged(currentAngle);
}
这个函数设置角度值。我们选择了一个5~70的合法范围并据此调节度数的给定值。如果新的角度超出了范围,我们选择了不发出警告。
如果新的角度等于旧的,我们立即返回。这只对当角度真的发生变化时发射angleChanged()信号才有重要意义。
然后我们设置新的角度值并重画我们的窗口部件。QWidget::update()函数清除这个窗口部件(通常是用背景色来填充)并向窗口部件发送一个绘画事件。这个结果就是窗口部件绘画事件函数的一次调用。
最后,我们发射angleChanged()信号来告诉外面的世界角度变了。关键字emit是Qt独有的而不是标准C++的语法。实际上,它是一个宏。
void CannonField::paintEvent(QPaintEvent * /* event */)
{
QPainter painter(this);
painter.drawText(200, 200,
tr("Angle = ") + QString::number(currentAngle));
}
这是我们第一次试图去写一个绘画事件处理程序。这个事件参数包含关于这个绘画事件的信息,比如窗口部件中必须被刷新的区域的信息。目前,我们比较懒,并且只是画每一件事。
我们的代码在一个固定的位置显示窗口部件的角度值。要实现它我们创建一个在CannonField窗口部件上运行的QPainter并用它来画当前角度值的字符串表示。我们将会在后面回到QPainter,它可以做非常多的事。
#include "cannonfield.h"
我们包含了我们的新类的定义:
MyWidget类将包含一个单一的LCDRange和一个CannonField。
LCDRange *angle = new LCDRange;
在构造函数中,我们创建并设置窗口部件LCDRange。
angle->setRange(5, 70);
我们设置LCDRange可接受的范围是5到70度。
CannonField *cannonField = new CannonField;
我们创建我们的CannonField窗口部件。
connect(angle, SIGNAL(valueChanged(int)),
cannonField, SLOT(setAngle(int)));
connect(cannonField, SIGNAL(angleChanged(int)),
angle, SLOT(setValue(int)));
这里我们把LCDRange的valueChanged()信号与CannonField的setAngle()槽连接起来了。只要用户操作LCDRange,它就会刷新CannonField的角度值。我们也使用相反的连接,这样改变CannonField中的角度也会刷新LCDRange值。在我们的例子中我们从未直接改变CannonField的角度,但是通过最后一个connect()我们可以确保将来没有变化能中断这两个值之间的同步关系。
这说明了组件编程和正确封装的力量。
注意只有在角度确切的发生变化时才发射angleChanged()信号是非常重要的。如果LCDRange和CannonField都忽略了这个检查,程序就会在第一次数值发生改变后进入一个无限循环。
QGridLayout *gridLayout = new QGridLayout;
到现在为止,我们已经使用QVBoxLayout来进行几何管理。现在,然而我们想得到对我们的布局更多的控制,所以我们换成更强大的QGridLayout类。QGridLayout不是一个窗口部件,而是一个可以管理任何窗口部件的子对象的不同的类。
我们不需要为QGridLayout构造函数指定任何大小。QGridLayout将根据我们移居的网格单元来决定行列的数量。
上图显示了我们试着实现的布局。左边显示了一个概要图;右边是这个程序的一个真实截图。
gridLayout->addWidget(quit, 0, 0);
我们在网格的左上单元格—即坐标为(0,0)的单元格中添加Quit按钮。
gridLayout->addWidget(angle, 1, 0);
我们把角度LCDRange放到坐标为(1,0)的单元格中。
gridLayout->addWidget(cannonField, 1, 1, 2, 1);
我们让CannonField对象占领坐标为(1,1)和(2,1)的单元格。
gridLayout->setColumnStretch(1, 10);
我们告诉QGridLayout右边的列(列1)是可拉伸的,伸展因数为10。因为左边的列不是(它的拉伸因数为0,是默认值),QGridLayout会试着让左边的窗口部件的大小不变而在MyWidget被修改大小的时调整CannonField的大小。
在这个例子中,任何一个大于0的拉伸因数对列1将起到同样的效果。在更复杂的布局中,你可以使用这个拉伸因数,通过指定合适的拉伸因数来告知一个特定列或行应该拉伸至另一个的两倍。
angle->setValue(60);
我们设置了一个初始角度值。注意这会触发LCDRange到CannonField的连接。
angle->setFocus();
我们刚才做的这个是设置angle获得键盘焦点,这样键盘输入会默认到达LCDRange。
LCDRange未包含任何keyPressEvent(),因此这看起来并不十分有用。但是,它的构造函数有了新的一行:
setFocusProxy(slider);
LCDRange设置滑块作为它的焦点代理。这就意味着当程序或者用户想要给LCDRange键盘焦点的时候,滑块会注意到这个。QSlider有一个不错的键盘接口,所以我们给了LCDRange这样一行代码。
运行这个应用程序
键盘现在可以做一些事情了:方向键,Home,End,PageUp,以及PageDown都可以做一些事。
当滑块被操作的时候,CannonField会显示新角度值。如果调整大小,CannonField会得到尽可能多的空间。
练习
试着重定义窗口的大小。如果你把它变得很窄或很宽会发生什么?
如果你给左边的列一个非零拉伸因数,当你重定义窗口大小时会发生什么?
不考虑QWidget::setFocus()的调用,你更喜欢什么样的行为?
试着把“Quit”改成“&Quit”。按钮变成什么样了?(它是否变化取决于工作平台。)如果你在程序运行时按下Alt+Q 会发生什么?
将CannonField中的文本居中。
注:原文见http://doc.trolltech.com/4.4/tutorials-tutorial-t8.html