转自:http://blog.sina.com.cn/s/blog_64d828dd0100q1zc.html
译者:Cavendish 整理: http://www.mculib.com/
译者:Cavendish
Qt教程一 ——第一章:Hello, World!
序所需要的最少的代码。上面的图片是这个程序的快照。
#include<qapplication.h>
#include<qpushbutton.h>
int main( intargc, char **argv )
{
}
一行一行地解说
使用一个QApplication对象。QApplication管理了各种各样的应用程序的广泛资
源,比如默认的字体和光标。
这一行包含了QPushButton类的定义。参考文档的文件的最上部分提到了使用哪个
类就必须包含哪个头文件的说明。
译者:Cavendish 整理: http://www.mculib.com/
QPushButton是一个经典的图形用户界面按钮,用户可以按下去,也可以放开。它
管理自己的观感,就像其它每一个QWidget。一个窗口部件就是一个可以处理用户
输入和绘制图形的用户界面对象。程序员可以改变它的全部观感和它的许多主要的
属性(比如颜色),还有这个窗口部件的内容。一个QPushButton可以显示一段文
本或者一个QPixmap。
main()函数是程序的入口。几乎在使用 Qt的所有情况下,main()只需要在把控制转
交给Qt库之前执行一些初始化,然后Qt库通过事件来向程序告知用户的行为。
argc是命令行变量的数量,argv 是命令行变量的数组。这是一个 C/C++特征。它
不是Qt专有的,无论如何Qt需要处理这些变量(请看下面)。
a是这个程序的QApplication。它在这里被创建并且处理这些命令行变量(比如在
X窗口下的-display)。请注意,所有被Qt识别的命令行参数都会从 argv 中被移
除(并且 argc也因此而减少)。关于细节请看 QApplication::argv()文档。
注意:在任何Qt的窗口系统部件被使用之前创建QApplication对象是必须的。
这里,在QApplication之后,接着的是第一个窗口系统代码:一个按钮被创建了。
这个按钮被设置成显示“Hello world!”并且它自己构成了一个窗口(因为在构造函
数指定0为它的父窗口,在这个父窗口中按钮被定位)。
这个按酒被设置成100像素宽,30像素高(加上窗口系统边框)。在这种情况
下,我们不用考虑按钮的位置,并且我们接受默认值。
这个按钮被选为这个应用程序的主窗口部件。如果用户关闭了主窗口部件,应用程
序就退出了。
你不用必须设置一个主窗口部件,但绝大多数程序都有一个。
译者:Cavendish 整理: http://www.mculib.com/
当你创建一个窗口部件的时候,它是不可见的。你必须调用 show()来使它变为可
见的。
这里就是main()把控制转交给Qt,并且当应用程序退出的时候 exec()就会返回。
在exec()中,Qt接受并处理用户和系统的事件并且把它们传递给适当的窗口部件。
你现在可以试着编译和运行这个程序了。
编译
编译一个C++应用程序,你需要创建一个 makefile。创建一个Qt的makefile的最
容易的方法是使用Qt提供的连编工具qmake。如果你已经把 main.cpp 保存到它自
己的目录了,你所要做的就是这些:
qmake-project
qmake
第一个命令调用qmake来生成一个.pro(项目)文件。第二个命令根据这个项目
文件来生成一个(系统相关的)makefile。你现在可以输入 make(或者nmake,如
果你使用VisualStudio),然后运行你的第一个Qt应用程序!
行为
当你运行它的时候,你就会看到一个被单一按钮充满的小窗口,在它上面你可以读
到著名的词:HellowWorld!
练习
试着改变窗口的大小。按下按钮。如果你在X窗口下运行,使用-geometry选项
(比如,-geometry100x200+10+20)来运行这个程序。
现在你可以进行第二章了。
译者:Cavendish 整理: http://www.mculib.com/
Qt教程一 ——第二章:调用退出
你已经在第一章中创建了一个窗口,我们现在使这个应用程序在用户让它退出的时
候退出。
我们也会使用一个比默认字体更好的一个字体。
#include<qapplication.h>
#include<qpushbutton.h>
#include<qfont.h>
int main( intargc, char **argv )
{
}
一行一行地解说
因为这个程序使用了QFont,所以它需要包含qfont.h。Qt的字体提取和X中提供
的可怕的字体提取大为不同,字体的载入和使用都已经被高度优化了。
译者:Cavendish 整理: http://www.mculib.com/
这时,按钮显示“Quit”,确切的说这就是当用户点击这个按钮时程序所要做的。这
不是一个巧合。因为这个按钮是一个顶层窗口,我们还是把 0作为它的父对象。
我们给这个按钮选择了另外一个大小,因为这个文本比“Hello world!”小一些。我
们也可以使用QFontMetrics来设置正确的大小。
这里我们给这个按钮选择了一个新字体,Times字体中的 18点加粗字体。注意在
这里我们调用了这个字体。
你也可以改变整个应用程序的默认字体(使用 QApplication::setFont())。
connect也许是Qt中最重要的特征了。注意 connect()是QObject中的一个静态函
数。不要把这个函数和socket库中的 connect()搞混了。
这一行在两个Qt对象(直接或间接继承QObject对象的对象)中建立了一种单向
的连接。每一个Qt对象都有signals(发送消息)和 slots(接收消息)。所有窗
口部件都是Qt对象。它们继承QWidget,而QWidget继承 QObject。
这里quit的clicked()信号和a的quit()槽连接起来了,所以当这个按钮被按下的时
候,这个程序就退出了。
信号和槽文档详细描述了这一主题。
行为
当你运行这个程序的时候,你会看到这个窗口比第一章中的那个小一些,并且被一
个更小的按钮充满。
(请看编译来学习如何创建一个makefile和连编应用程序。)
练习
试着改变窗口的大小。按下按钮。注意!connect()看起来会有一些不同。
是不是在QPushButton中还有其它的你可以连接到quit的信号?提示:
QPushButton继承了QButton的绝大多数行为。
译者:Cavendish 整理: http://www.mculib.com/
现在你可以进行第三章了。
Qt教程一 ——第三章:家庭价值
这个例子演示了如何创建一个父窗口部件和子窗口部件。
我们将会保持这个程序的简单性,并且只使用一个单一的父窗口部件和一个独立的
子窗口部件。
#include<qapplication.h>
#include<qpushbutton.h>
#include<qfont.h>
#include<qvbox.h>
int main( intargc, char **argv )
{
}
译者:Cavendish 整理: http://www.mculib.com/
一行一行地解说
我们添加了一个头文件qvbox.h用来获得我们要使用的布局类。
这里我们简单地创建了一个垂直的盒子容器。QVBox把它的子窗口部件排成一个
垂直的行,一个在其它的上面,根据每一个子窗口部件的QWidget::sizePolicy()来
安排空间。
我们它的高设置为120像素,宽为200像素。
子窗口部件产生了。
QPushButton通过一个文本(“text”)和一个父窗口部件(box)生成的。子窗口部
件总是放在它的父窗口部件的最顶端。当它被显示的时候,它被父窗口部件的边界
挡住了一部分。
父窗口部件,QVBox,自动地把这个子窗口部件添加到它的盒子中央。因为没有
其它的东西被添加了,这个按钮就获得了父窗口部件的所有空间。
当父窗口部件被显示的时候,它会调用所有子窗口部件的显示函数(除非在这些子
窗口部件中你已经明确地使用QWidget::hide())。
行为
这个按钮不再充满整个窗口部件。相反,它获得了一个“自然的”大小。这是因为现
在的这个新的顶层窗口,使用了按钮的大小提示和大小变化策略来设置这个按钮的
大小和位置。(请看QWidget::sizeHint()和QWidget::setSizePolicy()来获得关于这几
个函数的更详细的信息。)
(请看编译来学习如何创建一个makefile和连编应用程序。)
练习
译者:Cavendish 整理: http://www.mculib.com/
试着改变窗口的大小。按钮是如何变化的?按钮的大小变化策略是什么?如果你运
行这个程序的时候使用了一个大一些的字体,按钮的高度发生了什么变化?如果你
试图让这个窗口真的变小,发生了什么?
现在你可以进行第四章了。
Qt教程一 ——第四章:使用窗口部件
这个例子显示了如何创建一个你自己的窗口部件,描述如何控制一个窗口部件的最
小大小和最大大小,并且介绍了窗口部件的名称。
#include<qapplication.h>
#include<qpushbutton.h>
#include<qfont.h>
class MyWidget: public QWidget
{
public:
};
MyWidget::MyWidget( QWidget *parent, const char *name)
{
译者:Cavendish 整理: http://www.mculib.com/
}
int main( intargc, char **argv )
{
}
一行一行地解说
这里我们创建了一个新类。因为这个类继承了 QWidget,所以新类是一个窗口部
件,并且可以最为一个顶层窗口或者子窗口部件(像第三章里面的按钮)。
这个类只有一个成员函数,构造函数(加上从 QWidget继承来的成员函数)。这
个构造函数是一个标准的Qt窗口部件构造函数,当你创建窗口部件时,你应该总
是包含一个相似的构造函数。
第一个参数是它的父窗口部件。为了生成一个顶层窗口,你指定一个空指针作为父
窗口部件。就像你看到的那样,这个窗口部件默认地被认做是一个顶层窗口。
第二个参数是这个窗口部件的名称。这个不是显示在窗口标题栏或者按钮上的文
本。这只是分配给窗口部件的一个名称,以后可以用来查找这个窗口部件,并且这
里还有一个方便的调试功能可以完整地列出窗口部件层次。
构造函数的实现从这里开始。像大多数窗口部件一样,它把 parent 和 name 传递
给了QWidget的构造函数。
译者:Cavendish 整理: http://www.mculib.com/
因为这个窗口部件不知道如何处理重新定义大小,我们把它的最小大小和最大大小
设置为相等的值,这样我们就确定了它的大小。在下一章,我们将演示窗口部件如
何响应用户的重新定义大小事件。
这里我们创建并设置了这个窗口部件的一个名称为“quit”的子窗口部件(新窗口部
件的父窗口部件是this)。这个窗口部件名称和按钮文本没有关系,只是在这一
情况下碰巧相似。
注意 quit是这个构造函数中的局部变量。MyWidget不能跟踪它,但Qt可以,当
MyWidget被删除的时候,默认地它也会被删除。这就是为什么 MyWidget不需要
一个析构函数的原因。(另外一方面,如果你选择删除一个子窗口部件,也没什么
坏处,这个子窗口部件会自动告诉Qt它即将死亡。)
setGeometry()调用和上一章的 move()和 resize()是一样的。
因为MyWidget类不知道这个应用程序对象,它不得不连接到 Qt的指针,qApp。
一个窗口部件就是一个软件组件并且它应该尽量少地知道关于它的环境,因为它应
该尽可能的通用和可重用。
知道了应用程序的名称将会打破上述原则,所以在一个组件,比如MyWidget,需
要和应用程序对象对话的这种情况下,Qt提供了一个别名,qApp。
这里我们举例说明了我们的新子窗口部件,把它设置为主窗口部件,并且执行这个
应用程序。
行为
译者:Cavendish 整理: http://www.mculib.com/
这个程序和上一章的在行为上非常相似。不同点是我们实现的方式。无论如何它的
行为还是有一些小差别。试试改变它的大小,你会看到什么?
(请看编译来学习如何创建一个makefile和连编应用程序。)
练习
试着在main()中创建另一个 MyWidget对象。发生了什么?
试着添加更多的按钮或者把除了QPushButton之外的东西放到窗口部件中。
现在你可以进行第五章了。
译者:Cavendish 整理: http://www.mculib.com/
Qt教程一 ——第五章:组装积木
这个例子显示了创建几个窗口部件并用信号和槽把它们连接起来,和如何处理重新
定义大小事件。
#include<qapplication.h>
#include<qpushbutton.h>
#include<qslider.h>
#include<qlcdnumber.h>
#include<qfont.h>
#include<qvbox.h>
class MyWidget: public QVBox
{
public:
};
MyWidget::MyWidget( QWidget *parent, const char *name)
{
译者:Cavendish 整理: http://www.mculib.com/
SLOT(display(int)) );
}
int main( intargc, char **argv )
{
}
一行一行地解说
这里显示的是三个新的被包含的头文件。qslider.h和qlcdnumber.h在这里是因为我
们使用了两个新的窗口部件,QSlider和QLCDNumber。qvbox.h在这里是因为我
们使用了Qt的自动布局支持。
MyWidget现在继承了QVBox,而不是QWidget。我们通过这种方式来使用
QVBox的布局(它可以把它的子窗口部件垂直地放在自己里面)。重新定义大小
自动地被QVBox处理,因此现在也就被 MyWidget处理了。
lcd是一个QLCDNumber,一个可以按像 LCD的方式显示数字的窗口部件。这个
实例被设置为显示两个数字,并且是 this的子窗口部件。它被命名为“lcd”。
译者:Cavendish 整理: http://www.mculib.com/
QSlider是一个经典的滑块,用户可以通过在拖动一个东西在一定范围内调节一个
整数数值的方式来使用这个窗口部件。这里我们创建了一个水平的滑块,设置它的
范围是0~99(包括0和99,参见QSlider::setRange()文档)并且它的初始值是0。
SLOT(display(int)) );
这里我们是用了信号/槽机制把滑块的valueChanged()信号和LCD数字的display()
槽连接起来了。
无论什么时候滑块的值发生了变化,它都会通过发射valueChanged()信号来广播这
个新的值。因为这个信号已经和LCD数字的display()槽连接起来了,当信号被广
播的时候,这个槽就被调用了。这两个对象中的任何一个都不知道对方。这就是组
件编程的本质。
槽是和普通C++成员函数的方式不同,但有着普通C++成员函数的方位规则。
行为
LCD数字反应了你对滑块做的一切,并且这个窗口部件很好地处理了重新定义大
小事件。注意当窗口被重新定义大小(因为它可以)的时候,LDC数字窗口部件
也改变了大小,但是其它的还是和原来一样(因为如果它们变化了,看起来好像很
傻)。
(请看编译来学习如何创建一个makefile和连编应用程序。)
练习
试着改变LCD数字,添加更多的数字或者改变模式。你甚至可以添加四个按钮来
设置基数。
你也可以改变滑块的范围。
也许使用QSpinBox比滑块更好?
试着当LCD数字溢出的时候使这个应用程序退出。
现在你可以进行第六章了。
译者:Cavendish 整理: http://www.mculib.com/
Qt教程一 ——第六章:组装丰富的积
木!
这个例子显示了如何把两个窗口部件封装成一个新的组件和使用许多窗口部件是多
么的容易。首先,我们使用一个自定义的窗口部件作为一个子窗口部件。
#include<qapplication.h>
#include<qpushbutton.h>
#include<qslider.h>
#include<qlcdnumber.h>
#include<qfont.h>
#include<qvbox.h>
#include<qgrid.h>
class LCDRange: public QVBox
{
public:
};
译者:Cavendish 整理: http://www.mculib.com/
LCDRange::LCDRange( QWidget *parent, const char *name)
{
SLOT(display(int)) );
}
class MyWidget: public QVBox
{
public:
};
MyWidget::MyWidget( QWidget *parent, const char *name)
{
}
int main( intargc, char **argv )
{
}
一行一行地解说
译者:Cavendish 整理: http://www.mculib.com/
LCDRange窗口部件是一个没有任何API的窗口部件。它只有一个构造函数。这种
窗口部件不是很有用,所以我们一会儿会加入一些API。
SLOT(display(int)) );
这里直接利用了第五章里面的MyWidget的构造函数。唯一的不同是按钮被省略了
并且这个类被重新命名了。
MyWidget也是除了一个构造函数之外没有包含任何API。
这个按钮被放在LCDRange中,这样我们就有了一个“Quit”按钮和许多 LCDRange
对象。
我们创建了一个四列的QGrid对象。这个QGrid窗口部件可以自动地把自己地子
窗口部件排列到行列中,你可以指定行和列的数量,并且QGrid可以发现它的新
子窗口部件并且把它们安放到网格中。
四行,四列。
译者:Cavendish 整理: http://www.mculib.com/
我们创建了一个4*4个LCDRanges,所有这些都是这个 grid对象的子窗口部件。
这个QGrid窗口部件会安排它们。
这就是全部了。
行为
这个程序显示了在同一时间使用许多窗口部件是多么的容易。其中的滑块和 LCD
数字的行为在前一章已经提到过了。还有就是,就是实现的不同。
(请看编译来学习如何创建一个makefile和连编应用程序。)
练习
在开始的时候使用不同的或者随机的值初始化每个滑块。
源代码中的“4”出现了3次。如果你改变QGrid构造函数中调用的那个,会发生什
么?改变另外两个又会发生什么呢?为什么呢?
现在你可以进行第七章了。
译者:Cavendish 整理: http://www.mculib.com/
Qt教程一 ——第七章:一个事物领导另
一个
这个例子显示了如何使用信号和槽来创建自定义窗口部件,和如何使用更加复杂的
方式把它们连接起来。首先,源文件被我们分成几部分并放在放在 t7 目录下。
• t7/lcdrange.h包含 LCDRange类定义。
• t7/lcdrange.cpp包含 LCDRange类实现。
• t7/main.cpp包含 MyWidget和 main。
一行一行地解说
译者:Cavendish 整理: http://www.mculib.com/
t7/lcdrange.h
这个文件主要利用了第六章的main.cpp,在这里只是说明一下改变了哪些。
这里是一个经典的C语句,为了避免出现一个头文件被包含不止一次的情况。如
果你没有使用过它,这是开发中的一个很好的习惯。#ifndef需要把这个头文件的
全部都包含进去。
qvbox.h被包含了。LCDRange继承了QVBox,所以父类的头文件必须被包含。我
们在前几章里面偷了一点懒,我们通过包含其它一些头文件,比如qpushbutton.h,
这样就可以间接地包含qwidget.h。
这里是另外一个小伎俩,但是没有前一个用的多。因为我们在类的界面中不需要
QSlider,仅仅是在实现中,我们在头文件中使用一个前置的类声明,并且在.cpp
文件中包含一个QSlider的头文件。
这会使编译一个大的项目变得更快,因为当一个头文件改变的时候,很少的文件需
要重新编译。它通常可以给大型编译加速两倍或两倍以上。
meta objectfile. 注意 Q_OBJECT。这个宏必须被包含到所有使用信号和/或槽的
类。如果你很好奇,它定义了在元对象文件中实现的一些函数。
这三个成员函数构成了这个窗口部件和程序中其它组件的接口。直到现在,
LCDRange根本没有一个真正的接口。
译者:Cavendish 整理: http://www.mculib.com/
value()是一个可以访问LCDRange的值的公共函数。setValue()是我们第一个自定
义槽,并且valueChanged()是我们第一个自定义信号。
槽必须按通常的方式实现(记住槽也是一个C++成员函数)。信号可以在元对象
文件中自动实现。信号也遵守C++函数的保护法则(比如,一个类只能发射它自
己定义的或者继承来的信号)。
当LCDRange的值发生变化时,valueChanged()信号就会被使用——你从这个名字
中就可以猜到。这将不会是你将会看到的命名为somethingChanged()的最后一个信
号。
t7/lcdrange.cpp
这个文件主要利用了t6/main.cpp,在这里只是说明一下改变了哪些。
这个代码来自LCDRange的构造函数。
第一个connect和你在上一章中看到的一样。第二个是新的,它把滑块的
valueChanged()信号和这个对象的valueChanged信号连接起来了。带有三个参数的
connect()函数连接到this 对象的信号或槽。
是的,这是正确的。信号可以被连接到其它的信号。当第一个信号被发射时,第二
个信号也被发射。
让我们来看看当用户操作这个滑块的时候都发生了些什么。滑块看到自己的值发生
了改变,并发射了valueChanged()信号。这个信号被连接到QLCDNumber的
display()槽和LCDRange的valueChanged()信号。
所以,当这个信号被发射的时候,LCDRange发射它自己的valueChanged()信号。
另外,QLCDNumber::display()被调用并显示新的数字。
注意你并没有保证执行的任何顺序——LCDRange::valueChanged()也许在
QLCDNumber::display()之前或者之后发射,这是完全任意的。
译者:Cavendish 整理: http://www.mculib.com/
value()的实现是直接了当的,它简单地返回滑块的值。
setValue()的实现是相当直接了当的。注意因为滑块和 LCD数字是连接的,设置滑
块的值就会自动的改变LCD数字的值。另外,如果滑块的值超过了合法范围,它
会自动调节。
t7/main.cpp
main.cpp中所有的部分都是上一章复制的,除了 MyWidget的构造函数。当我们创
建16个RCDRange对象时,我们现在使用信号/槽机制连接它们。每一个的
valueChanged()信号都和前一个的 setValue()槽连接起来了。因为当LCDRange的值
发生改变的时候,发射一个valueChanged()信号(惊奇!),我们在这里创建了一
个信号和槽的“链”。
编译
为一个多文件的应用程序创建一个makefile和为一个单文件的应用程序创建一个
makefile是没有什么不同的。如果你已经把这个例子中的所有文件都保存到它们自
己的目录中,你所要做的就是这些:
qmake-project
qmake
第一个命令调用qmake来生成一个.pro(项目)文件。第二个命令根据这个项目
文件来生成一个(系统相关的)makefile。你现在可以输入 make(或者nmake,如
果你使用VisualStudio)。
行为
在开始的时候,这个程序看起来和上一章里的一样。试着操作滑块到右下角……
译者:Cavendish 整理: http://www.mculib.com/
练习
seven LCDs backto 50. 使用右下角的滑块并设置所有的 LCD到50。然后设置通过
点击这个滑块的左侧把它设置为40。现在,你可以通过把最后一个调到左边来把
前七个 LCD设置回50。
点击右下角滑块的滑块的左边。发生了什么?为什么只是正确的行为?
现在你可以进行第八章了。
Qt教程一 ——第八章:准备战斗
译者:Cavendish 整理: http://www.mculib.com/
在这个例子中,我们介绍可以画自己的第一个自定义窗口部件。我们也加入了一个
有用的键盘接口(只用了两行代码)。
• t8/lcdrange.h包含 LCDRange类定义。
• t8/lcdrange.cpp包含 LCDRange类实现。
• t8/cannon.h包含CannonField类定义。
• t8/cannon.cpp包含CannonField类实现。
• t8/main.cpp包含 MyWidget和 main。
一行一行地解说
t8/lcdrange.h
这个文件和第七章中的lcdrange.h很相似。我们添加了一个槽:setRange()。
现在我们添加了设置LCDRange范围的可能性。直到现在,它就可以被设置为 0~
99。
t8/lcdrange.cpp
在构造函数中有一个变化(稍后我们会讨论的)。
setRange()设置了LCDRange中滑块的范围。因为我们已经把 QLCDNumber设置为
只显示两位数字了,我们想通过限制 minVal 和 maxVal 为0~99来避免
QLCDNumber的溢出。(我们可以允许最小值为-9,但是我们没有那样做。)如
果参数是非法的,我们使用Qt的qWarning()函数来向用户发出警告并立即返回。
qWarning()是一个像printf一样的函数,默认情况下它的输出发送到stderr。如果
你想改变的话,你可以使用::qInstallMsgHandler()函数安装自己的处理函数。
译者:Cavendish 整理: http://www.mculib.com/
t8/cannon.h
CanonField是一个知道如何显示自己的新的自定义窗口部件。
CanonField继承了QWidget,我们使用了 LCDRange中同样的方式。
目前,CanonField只包含一个角度值,我们使用了 LCDRange中同样的方式。
这是我们在QWidget中遇到的许多事件处理器中的第二个。只要一个窗口部件需
要刷新它自己(比如,画窗口部件表面),这个虚函数就会被Qt调用。
t8/cannon.cpp
我们又一次使用和前一章中的LCDRange同样的方式。
构造函数把角度值初始化为45度并且给这个窗口部件设置了一个自定义调色板。
这个调色板只是说明背景色,并选择了其它合适的颜色。(对于这个窗口部件,只
有背景色和文本颜色是要用到的。)
译者:Cavendish 整理: http://www.mculib.com/
这个函数设置角度值。我们选择了一个5~70的合法范围,并根据这个范围来调节
给定的degrees的值。当新的角度值超过了范围,我们选择了不使用警告。
如果新的角度值和旧的一样,我们立即返回。这只对当角度值真的发生变化时,发
射angleChanged()信号有重要意义。
然后我们设置新的角度值并重新画我们的窗口部件。QWidget::repaint()函数清空窗
口部件(通常用背景色来充满)并向窗口部件发出一个绘画事件。这样的结构就是
调用窗口部件的绘画事件函数一次。
最后,我们发射angleChanged()信号来告诉外面的世界,角度值发生了变化。emit
关键字只是Qt中的关键字,而不是标准C++的语法。实际上,它只是一个宏。
这是我们第一次试图写一个绘画事件处理程序。这个事件参数包含一个绘画事件的
描述。QPaintEvent包含一个必须被刷新的窗口部件的区域。现在,我们比较懒
惰,并且只是画每一件事。
我们的代码在一个固定位置显示窗口部件的角度值。首先我们创建一个含有一些文
本和角度值的QString,然后我们创建一个操作这个窗口部件的 QPainter并使用它
来画这个字符串。我们一会儿会回到QPainter,它可以做很多事。
t8/main.cpp
我们包含了我们的新类:
译者:Cavendish 整理: http://www.mculib.com/
这一次我们在顶层窗口部件中只使用了一个 LCDRange和一个CanonField。
在构造函数中,我们创建并设置了我们的 LCDRange。
我们设置LCDRange能够接受的范围是5~70度。
我们创建了我们的CannonField。
这里我们把LCDRange的valueChanged()信号和CannonField的 setAngle()槽连接起
来了。只要用户操作LCDRange,就会刷新CannonField的角度值。我们也把它反
过来连接了,这样CannonField中角度的变化就可以刷新 LCDRange的值。在我们
的例子中,我们从来没有直接改变CannonField的角度,但是通过我们的最后一个
connect()我们就可以确保没有任何变化可以改变这两个值之间的同步关系。
这说明了组件编程和正确封装的能力。
注意只有当角度确实发生变化时,才发射 angleChanged()是多么的重要。如果
LCDRange和CanonField都省略了这个检查,这个程序就会因为第一次数值变化而
进入到一个无限循环当中。
到现在为止,我们没有因为几何管理把QVBox和 QGrid窗口部件集成到一起。现
在,无论如何,我们需要对我们的布局加一些控制,所以我们使用了更加强大的
QGridLayout类。QGridLayout不是一个窗口部件,它是一个可以管理任何窗口部
件作为子对象的不同的类。
就像注释中所说的,我们创建了一个以10像素为边界的2*2的数组。
(QGridLayout的构造函数有一点神秘,所以最好在这里加入一些注释。)
译者:Cavendish 整理: http://www.mculib.com/
我们在网格的左上的单元格中加入一个Quit按钮:0,0。
我们把 angle这个LCDRange放到左下的单元格,在单元格内向上对齐。(这只是
QGridLayout所允许的一种对齐方式,而 QGrid不允许。)
我们把CannonField对象放到右下的单元格。(右上的单元格是空的。)
我们告诉QGridLayout右边的列(列 1)是可拉伸的。因为左边的列不是(它的拉
伸因数是0,这是默认值),QGridLayout就会在MyWidget被重新定义大小的时
候试图让左面的窗口部件大小不变,而重新定义CannonField的大小。
我们设置了一个初始角度值。注意这将会引发从 LCDRange到CannonField的连
接。
我们刚才做的是设置 angle获得键盘焦点,这样默认情况下键盘输入会到达
LCDRange窗口部件。
LCDRange没有包含任何keyPressEvent(),所以这看起来不太可能有用。无论如
何,它的构造函数中有了新的一行:
LCDRange设置滑块作为它的焦点代理。这就是说当程序或者用户想要给
LCDRange一个键盘焦点,滑块就会就会注意到它。QSlider有一个相当好的键盘
接口,所以就会出现我们给LCDRange添加的这一行。
行为
键盘现在可以做一些事了——方向键、Home、End、PageUp和PageDown都可以
作一些事情。
译者:Cavendish 整理: http://www.mculib.com/
当滑块被操作,CannonFiled会显示新的角度值。如果重新定义大小,CannonField
会得到尽可能多的空间。
在8位的Windows机器上显示新的颜色会颤动的要命。下一章会处理这些的。
(请看编译来学习如何创建一个makefile和连编应用程序。)
练习
设置重新定义窗口的大小。如果你把它变窄或者变矮会发生什么?
如果你把AlignTop删掉,LCDRange的位置会发生什么变化?为什么?
如果你给左面的列一个非零的拉伸因数,当你重新定义窗口大小时会发生什么?
不考虑setFocus()调用。你更喜欢什么样的行为?
试着在QButton::setText()调用中把“Quit”改为“&Quit”。按钮看起来变成什么样子
了?如果你在程序运行的时候按下Alt+Q会发生什么?(在少量键盘中时
Meta+Q。)
把CannonField的文本放到中间。
现在你可以进行第九章了。
Qt教程一 ——第九章:你可以使用加农
炮了
译者:Cavendish 整理: http://www.mculib.com/
在这个例子中我们开始画一个蓝色可爱的小加农炮.只 cannon.cpp和上一章不同。
• t9/lcdrange.h包含 LCDRange类定义。
• t9/lcdrange.cpp包含 LCDRange类实现。
• t9/cannon.h包含CannonField类定义。
• t9/cannon.cpp包含CannonField类实现。
• t9/main.cpp包含 MyWidget和 main。
一行一行地解说
t9/cannon.cpp
我们现在开始认真地使用QPainter。我们创建一个绘画工具来操作这个窗口部件。
当一个QPainter填满一个矩形、圆或者其它无论什么,它会用它的画刷填满这个
图形。这里我们把画刷设置为蓝色。(我们也可以使用一个调色板。)
译者:Cavendish 整理: http://www.mculib.com/
并且QPainter使用画笔来画边界。这里我们设置为 NoPen,就是说我们在边界上什
么都不画,蓝色画刷会在我们画的东西的边界内画满全部。
QPainter::translate()函数转化QPainter的坐标系统,比如,它通过偏移谅来移动。
这里我们设置窗口部件的左下角为(0,0)。x和 y的方向没有改变,比如,窗口部件
中的所有y坐标现在都是负数(请看坐标系统获得有关Qt的坐标系统更多的信
息。)
drawPie()函数使用一个开始角度和弧长在一个指定的矩形内画一个饼型图。角度的
度量用的是一度的十六分之一。零度在三点的位置。画的方向是顺时针的。这里我
们在窗口部件的左下角画一个四分之一圆。这个饼图被蓝色充满,并且没有边框。
QPainter::rotate()函数绕QPainter坐标系统的初始位置旋转它。旋转的参数是一个
按度数给定的浮点数(不是一个像上面那样给的十六分之一的度数)并且是顺时针
的。这里我们顺时针旋转 ang度数。
QPainter::drawRect()函数画一个指定的矩形。这里我们画的是加农炮的炮筒。
很难想象当坐标系统被转换之后(转化、旋转、缩放或者修剪)的绘画结果。
在这种情况下,坐标系统先被转化后被旋转。如果矩形 QRect(33, -4, 15,8)被画到
这个转化后的坐标系统中,它看起来会是这样:
注意矩形被CannonField窗口部件的边界省略了一部分。当我们选装坐标系统,以
60度为例,矩形会以(0,0)为圆心被旋转,也就是左下角,因为我们已经转化了坐
标系统。结果会是这样:
译者:Cavendish 整理: http://www.mculib.com/
我们做完了,除了我们还没有解释为什么 Windows在这个时候没有发抖。
我们告诉Qt我们在这个程序中想使用一个不同的颜色分配策略。这里没有单一正
确的颜色分配策略。因为这个程序使用了不常用的黄色,但不是很多颜色,
CustomColor最好。这里有几个其它的分配策略,你可以在
QApplication::setColorSpec()文档中读到它们。
通常情况下你可以忽略这一点,因为默认的是好的。偶尔一些使用常用颜色的应用
程序看起来比较糟糕,因而改变分配策略通常会有所帮助。
行为
当滑块被操作的时候,所画的加农炮的角度会因此而变化。
Quit中的字母Q现在有下划线,并且 Alt+Q会实现你所要的。如果你不知道这
些,你一定是没有做第八章中的练习。
你也要注意加农炮的闪烁让人很烦,特别是在一个比较慢的机器上。我们将会在下
一章修正这一点。
(请看编译来学习如何创建一个makefile和连编应用程序。)
练习
设置一个不同的画笔代替NoPen。设置一个调色板的画刷。
试着用“Q&uit”或者“Qu&it”作为按钮的文本来提到“&Quit”。发生了什么?
现在你可以进行第十章了。
译者:Cavendish 整理: http://www.mculib.com/
Qt教程一 ——第十章:像丝一样滑
在这个例子中,我们介绍画一个pixmap来除去闪烁。我们也会加入一个力量控
制。
• t10/lcdrange.h包含 LCDRange类定义。
• t10/lcdrange.cpp包含 LCDRange类实现。
• t10/cannon.h包含CannonField类定义。
• t10/cannon.cpp包含CannonField类实现。
• t10/main.cpp包含 MyWidget和main。
一行一行地解说
t10/cannon.h
CannonField现在除了角度又多了一个力量值。
译者:Cavendish 整理: http://www.mculib.com/
力量的接口的实现和角度一样。
我们把加农炮封装的矩形的定义放到了一个单独的函数中。
力量被存储到一个整数 f 中。
t10/cannon.cpp
我们包含了QPixmap类定义。
力量(f)被初始化为0。
译者:Cavendish 整理: http://www.mculib.com/
我们在setAngle()函数中做了一个小的改变。它只重画窗口部件中含有加农炮的一
小部分。FALSE参数说明在一个绘画事件发送到窗口部件之前指定的矩形将不会
被擦去。这将会使绘画过程加速和平滑。
setForce()的实现和setAngle()很相似。唯一的不同是因为我们不显示力量值,我们
不需要重画窗口部件。
我们现在用只重画需要刷新得部分来优化绘画事件。首先我们检查是否不得不完全
重画任何事,我们返回是否不需要。
然后,我们创建一个临时的pixmap,我们用来不闪烁地画。所有的绘画操作都在
这个pixmap中完成,并且之后只用一步操作来把这个pixmap画到屏幕上。
这是不闪烁绘画的本质:一次准确地在每一个像素上画。更少,你会得到绘画错
误。更多,你会得到闪烁。在这个例子中这个并不重要——当代码被写时,仍然是
很慢的机器导致闪烁,但以后不会再闪烁了。我们由于教育目的保留了这些代码。
我们用这个pixmap来充满这个窗口部件的背景。
译者:Cavendish 整理: http://www.mculib.com/
我们就像第九章中一样画,但是现在我们是在 pixmap上画。
在这一点上,我们有一个绘画工具变量和一个pixmap看起来相当正确,但是我们
还没有在屏幕上画呢。
所以我们在CannonField上面打开绘图工具并在这之后画这个pixmap。
这就是全部了。在顶部和底部各有一对线,并且这个代码是 100%不闪烁的。
这个函数返回一个在窗口部件坐标中封装加农炮的矩形。首先我们创建一个50*50
大小的矩形,然后移动它,使它的左下角和窗口部件自己的左下角相等。
QWidget::rect()函数在窗口部件自己的坐标(左上角是0,0)中返回窗口部件封装的
矩形。
t10/main.cpp
构造函数也是一样,但是已经加入了一些东西。
我们加入了第二个LCDRange,用来设置力量。
我们把 force 窗口部件和cannonField 窗口部件连接起来,就和我们对 angle 窗
口部件做的一样。
译者:Cavendish 整理: http://www.mculib.com/
在第九章,我们把 angle放到了布局的左下单元。现在我们想在这个单元中放入
两个窗口部件,所一个我们用了一个垂直的盒子,把这个垂直的盒子放到这个网格
单元中,并且把 angle 和force 放到这个垂直的盒子中。
我们初始化力量的值为25。
行为
闪烁已经走了,并且我们还有一个力量控制。
(请看编译来学习如何创建一个makefile和连编应用程序。)
练习
让加农炮的炮筒的大小依赖于力量。
把加农炮放到右下角。
试着加入一个更好的键盘接口。例如,用+和-来增加或者减少力量,用 enter来发
射。提示:QAccel和在LCDRange中新建 addStep()和 subtractStep(),就像
QSlider::addStep()。如果你被左面和右面键所苦恼(我就是!),试着都改变!
现在你可以进行第十一章了。
译者:Cavendish 整理: http://www.mculib.com/
Qt教程一 ——第十一章:给它一个炮弹
在这个例子里我们介绍了一个定时器来实现动画的射击。
• t11/lcdrange.h包含 LCDRange类定义。
• t11/lcdrange.cpp包含 LCDRange类实现。
• t11/cannon.h包含CannonField类定义。
• t11/cannon.cpp包含CannonField类实现。
• t11/main.cpp包含 MyWidget和main。
一行一行地解说
译者:Cavendish 整理: http://www.mculib.com/
t11/cannon.h
CannonField现在就有了射击能力。
当炮弹不在空中中,调用这个槽就会使加农炮射击。
当炮弹正在空中时,这个私有槽使用一个定时器来移动射击。
这个函数来画射击。
当炮弹正在空中的时候,这个私有函数返回封装它所占用空间的矩形,否则它就返
回一个没有定义的矩形。
这些私有变量包含了描述射击的信息。timerCount 保留了射击进行后的时间。
shoot_ang是加农炮射击时的角度,shoot_f 是射击时加农炮的力量。
t11/cannon.cpp
我们包含了数学库,因为我们需要使用 sin()和 cos()函数。
译者:Cavendish 整理: http://www.mculib.com/
我们初始化我们新的私有变量并且把QTimer::timeout()信号和我们的moveShot()槽
相连。我们会在定时器超时的时候移动射击。
只要炮弹不在空中,这个函数就会进行一次射击。timerCount 被重新设置为零。
shoot_ang 和shoot_f 设置为当前加农炮的角度和力量。最后,我们开始这个定时
器。
moveShot()是一个移动射击的槽,当QTimer开始的时候,每 50毫秒被调用一次。
它的任务就是计算新的位置,重新画屏幕并把炮弹放到新的位置,并且如果需要的
话,停止定时器。
首先我们使用QRegion来保留旧的shotRect()。QRegion可以保留任何种类的区
域,并且我们可以用它来简化绘画过程。shotRect()返回现在炮弹所在的矩形——
稍后我们会详细介绍。
然后我们增加timerCount,用它来实现炮弹在它的轨迹中移动的每一步。
下一步我们算出新的炮弹的矩形。
译者:Cavendish 整理: http://www.mculib.com/
如果炮弹已经移动到窗口部件的右面或者下面的边界,我们停止定时器或者添加新
的shotRect()到QRegion。
最后,我们重新绘制QRegion。这将会发送一个单一的绘画事件,但仅仅有一个到
两个举行需要刷新。
绘画事件函数在前一章中已经被分成两部分了。现在我们得到的新的矩形区域需要
绘画,检查加农炮和/或炮弹是否相交,并且如果需要的话,调用paintCannon()和/
或paintShot()。
这个私有函数画一个黑色填充的矩形作为炮弹。
我们把paintCannon()的实现放到一边,它和前一章中的paintEvent()一样。
译者:Cavendish 整理: http://www.mculib.com/
这个私有函数计算炮弹的中心点并且返回封装炮弹的矩形。它除了使用自动增加所
过去的时间的timerCount 之外,还使用初始时的加农炮的力量和角度。
运算公式使用的是有重力的环境下光滑运动的经典牛顿公式。简单地说,我们已经
选择忽略爱因斯坦理论的结果。
我们在一个y坐标向上增加的坐标系统中计算中心点。在我们计算出中心点之后,
我们构造一个6*6大小的QRect,并把它的中心移动到我们上面所计算出的中心
点。同样的操作我们把这个点移动到窗口部件的坐标系统(请看坐标系统)。
qRound()函数是一个在qglobal.h中定义的内嵌函数(被其它所有 Qt头文件包
含)。qRound()把一个双精度实数变为最接近的整数。
t11/main.cpp
唯一的增加是 Shoot按钮。
在构造函数中我们创建和设置Shoot按钮就像我们对Quit按钮所做的那样。注意
构造函数的第一个参数是按钮的文本,并且第三个是窗口部件的名称。
把 Shoot按钮的clicked()信号和 CannonField的 shoot()槽连接起来。
行为
The cannon canshoot, but there's nothing to shoot at.
(请看编译来学习如何创建一个makefile和连编应用程序。)
练习
用一个填充的圆来表示炮弹。提示:QPainter::drawEllipse()会对你有所帮助。
译者:Cavendish 整理: http://www.mculib.com/
当炮弹在空中的时候,改变加农炮的颜色。
现在你可以进行第十二章了。
Qt教程一 ——第十一章:悬在空中的砖
在这个例子中,我们扩展我们的LCDRange类来包含一个文本标签。我们也会给
射击提供一个目标。
• t12/lcdrange.h包含 LCDRange类定义。
• t12/lcdrange.cpp包含 LCDRange类实现。
• t12/cannon.h包含CannonField类定义。
译者:Cavendish 整理: http://www.mculib.com/
• t12/cannon.cpp包含CannonField类实现。
• t12/main.cpp包含 MyWidget和main。
一行一行地解说
t12/lcdrange.h
LCDRange现在有了一个文本标签。
我们名称声明QLabel,因为我们将在这个类声明中使用一个QLabel的指针。
我们添加了一个新的构造函数,这个构造函数在父对象和名称之外还设置了标签文
本。
这个函数返回标签文本。
这个槽设置标签文本。
因为我们现在有了两个构造函数,我们选择把通常的初始化放在一个私有的 init()
函数。
我们还有一个新的私有变量:一个QLabel。QLabel是一个Qt标准窗口部件并且可
以显示一个有或者没有框架的文本或者pixmap。
t12/lcdrange.cpp
译者:Cavendish 整理: http://www.mculib.com/
这里我们包含了QLabel类定义。
这个构造函数调用了init()函数,它包括了通常的初始化代码。
这个构造函数首先调用了init()然后设置标签文本。
lcd 和 slider的设置和上一章一样。接下来我们创建一个 QLabel并且让它的内容
中间对齐(垂直方向和水平方向都是)。connect()语句也来自于上一章。
这个函数返回标签文本。
译者:Cavendish 整理: http://www.mculib.com/
这个函数设置标签文本。
t12/cannon.h
CannonField现在有两个新的信号:hit()和 missed()。另外它还包含一个目标。
这个槽在新的位置生成一个新的目标。
hit()信号是当炮弹击中目标的时候被发射的。missed()信号是当炮弹移动超出了窗
口部件的右面或者下面的边界时被发射的(例如,当然这种情况下它将不会击中目
标)。
这个私有函数绘制目标。
这个私有函数返回一个封装了目标的矩形。
这个私有变量包含目标的中心点。
t12/cannon.cpp
我们包含了QDate、QTime和 QDateTime类定义。
我们包含了stdlib库,因为我们需要 rand()函数。
这一行已经被添加到了构造函数中。它为目标创建一个“随机的”位置。实际上,
newTarget()函数还试图绘制目标。因为我们在一个构造函数中,CannonField窗口
译者:Cavendish 整理: http://www.mculib.com/
部件还是不可以见的。Qt保证在一个隐藏的窗口部件中调用 repaint()是没有害处
的。
这个私有函数创建了一个在新的“随机的”位置的目标中心点。
我们使用rand()函数来获得随机整数。rand()函数通常会在你每次运行这个程序的
时候返回同样一组值。这就会使每次运行的时候目标都出现在同样的位置。为了避
免这些,我们必须在这个函数第一次被调用的时候设置一个随机种子。为了避免同
样一组数据,随机种子也必须是随机的。解决方法就是使用从午夜到现在的秒数作
为一个假的随机值。
首先我们创建一个静态布尔型局域变量。静态变量就是在调用函数前后都保证它的
值不变。
if测试会成功,因为只有当这个函数第一次被调用的时候,我们在 if 块中把
first_time 设置为FALSE。
然后我们创建一个QTime对象midnight,它将会提供时间00:00:00。接下来我们
获得从午夜到现在所过的秒数并且使用它作为一个随机种子。请看QDate、QTime
和QDateTime文档来获得更多的信息。
最后我们计算目标的中心点。我们把它放在一个矩形中(x=200,y=35,
width=190,height=255),(例如,可能的x和y值是x=200~389和y=35~289)
在一个我们把窗口边界的下边界作为y的零点,并且 y向上增加,X轴向通常一
样,左边界为零点,并且x向右增加的坐标系统中。
通过经验,我们发现这都在炮弹的射程之内。
注意rand()返回一个>=0的随机整数。
译者:Cavendish 整理: http://www.mculib.com/
定时器时间这部分和上一章一样。
if语句检查炮弹矩形和目标矩形是否相交。如果是的,炮弹击中了目标(哎
哟!)。我们停止射击定时器并且发射hit()信号来告诉外界目标已经被破坏,并返
回。
注意,我们可以在这个点上创建一个新的目标,但是因为CannonField是一个组
件,所以我们要把这样的决定留给组件的使用者。
这个 if语句和上一章一样,除了现在它发射 missed()信号告诉外界这次失败。
函数的其余部分和以前一样。
CannonField::paintEvent() is as before, except that thishas been added:
这两行确认在需要的时候目标也被绘制。
这个私有函数绘制目标,一个由红色填充,有黑色边框的矩形。
译者:Cavendish 整理: http://www.mculib.com/
这个私有函数返回封装目标的矩形。从 newTarget()中所得的 target 点使用0点在
窗口部件的下边界的y。我们在调用QRect::moveCenter()之前在窗口坐标中计算这
个点。
我们选择这个坐标映射的原因是在目标和窗口部件的下边界之间垂直距离。记住这
些可以让用户或者程序在任何时候都可以重新定义窗口部件的大小。
t12/main.cpp
在MyWidget类中没有新的成员了,但是我们稍微改变了一下构造函数来设置新的
LCDRange的文本标签。
我们设置角度的文本标签为“ANGLE”。
我们设置力量的文本标签为“FORCE”。
行为
加农炮会向目标射击,当它射中目标的时候,一个新的目标会自动被创建。
LCDRange窗口部件看起来有一点奇怪——QVBox中内置的管理给了标签太多的
空间而其它的却不够。我们将会在下一章修正这一点。
(请看编译来学习如何创建一个makefile和连编应用程序。)
练习
创建一个作弊的按钮,当按下它的时候,让CannonField画出炮弹在五秒中的轨
迹。
如果你在上一章做了“圆形炮弹”的练习,试着改变 shotRect()为可以返回一个
QRegion的shotRegion(),这样你就可以真正的做到准确碰撞检测。
做一个移动目标。
译者:Cavendish 整理: http://www.mculib.com/
确认目标被完全创建在屏幕上。
确认加农炮窗口部件不能被重新定义大小,这样目标不是可见的。提示:
QWidget::setMinimumSize()是你的朋友。
不容易的是在同一时刻让几个炮弹在空中成为可能。提示:建立一个炮弹对象。
现在你可以进行第十三章了。
Qt教程一 ——第十三章:游戏结束
译者:Cavendish 整理: http://www.mculib.com/
在这个例子中我们开始研究一个带有记分的真正可玩的游戏。我们给 MyWidget一
个新的名字(GameBoard)并添加一些槽。
我们把定义放在gamebrd.h并把实现放在gamebrd.cpp。
CannonField现在有了一个游戏结束状态。
在LCDRange中的布局问题已经修好了。
• t13/lcdrange.h包含 LCDRange类定义。
• t13/lcdrange.cpp包含 LCDRange类实现。
• t13/cannon.h包含CannonField类定义。
• t13/cannon.cpp包含CannonField类实现。
• t13/gamebrd.h包含GameBoard类定义。
• t13/gamebrd.cpp包含GameBoard类实现。
• t13/main.cpp包含 MyWidget和main。
一行一行地解说
t13/lcdrange.h
我们继承了QWidget而不是QVBox。QVBox是非常容易使用的,但是它也显示了
它的局域性,所以我们选择使用更加强大和稍微有一些难的QVBoxLayout。(和
你记忆中的一样,QVBoxLayout不是一个窗口部件,它管理窗口部件。)
t13/lcdrange.cpp
我们现在需要包含qlayout.h来获得其它布局管理API。
我们使用一种平常的方式继承QWidget。
另外一个构造函数作了同样的改动。init()没有变化,除了我们在最后加了几行:
译者:Cavendish 整理: http://www.mculib.com/
我们使用所有默认值创建一个QVBoxLayout,管理这个窗口部件的子窗口部件。
At the top weadd the QLCDNumber with a non-zero stretch.
然后我们添加另外两个,它们都使用默认的零伸展因数。
这个伸展控制是QVBoxLayout(和QHBoxLayout,和QGridLayout)所提供的,
而像QVBox这样的类却不提供。在这种情况下我们让QLCDNumber可以伸展,
而其它的不可以。
t13/cannon.h
CannonField现在有一个游戏结束状态和一些新的函数。
如果游戏结束了,这个函数返回TRUE,或者如果游戏还在继续,返回 FALSE。
这里是两个新槽:setGameOver()和 restartGame()。
这个新的信号表明CannonField使shoot()槽生效的状态。我们将在下面使用它用来
使 Shoot按钮生效或失效。
这个私有变量包含游戏的状态。TRUE说明游戏结束,FALSE说明游戏还将继
续。
t13/cannon.cpp
这一行已经被加入到构造函数中。最开始的时候,游戏没有结束(对于玩家是很幸
运的 :-)。
译者:Cavendish 整理: http://www.mculib.com/
我们添加一个新的isShooting()函数,所以 shoot()使用它替代直接的测试。同样,
shoot告诉世界CannonField现在不可以射击。
这个槽终止游戏。它必须被CannonField外面的调用,因为这个窗口部件不知道什
么时候终止游戏。这是组件编程中一条重要设计原则。我们选择使组件可以尽可能
灵活以适应不同的规则(比如,在一个首先射中十次的人胜利的多人游戏版本可能
使用不变的CannonField)。
如果游戏已经被终止,我们立即返回。如果游戏会继续到我们的设计完成,设置游
戏结束标志,并且重新绘制整个窗口部件。
这个槽开始一个新游戏。如果炮弹还在空中,我们停止设计。然后我们重置
gameEnded变量并重新绘制窗口部件。
就像hit()或miss()一样,moveShot()同时也发射新的 canShoot(TRUE)信号。
CannonField::paintEvent()的修改:
译者:Cavendish 整理: http://www.mculib.com/
绘画事件已经通过如果游戏结束,比如 gameEnded 是TRUE,就显示文本“Game
Over”而被增强了。我们在这里不怕麻烦来检查更新矩形,是因为在游戏结束的时
候速度不是关键性的。
为了画文本,我们先设置了黑色的画笔,当画文本的时候,画笔颜色会被用到。接
下来我们选择Courier字体中的48号加粗字体。最后我们在窗口部件的矩形中央绘
制文本。不幸的是,在一些系统中(特别是使用Unicode的 X服务器)它会用一
小段时间来载入如此大的字体。因为 Qt缓存字体,我们只有第一次使用这个字体
的时候才会注意到这一点。
我们只有在设计的时候画炮弹,在玩游戏的时候画目标(这也就是说,当游戏没有
结束的时候)。
t13/gamebrd.h
这个文件是新的。它包含最后被用来作为 MyWidget的 GameBoard类的定义。
译者:Cavendish 整理: http://www.mculib.com/
我们现在已经添加了四个槽。这些槽都是被保护的,只在内部使用。我们也已经加
入了两个QLCDNumbers(hits 和 shotsLeft)用来显示游戏的状态。
t13/gamebrd.cpp
这个文件是新的。它包含最后被用来作为 MyWidget的 GameBoard类的实现,
我们已经在GameBoard的构造函数中做了一些修改。
cannonField现在是一个成员变量,所以我们在使用它的时候要小心地改变它的构
造函数。(Trolltech的好程序员从来不会忘记这点,但是我就忘了。告诫程序员-
如果“programmor”是拉丁语,至少。无论如何,返回代码。)
这次当炮弹射中或者射失目标的时候,我们想做些事情。所以我们把 CannonField
的hit()和missed()信号连接到这个类的两个被保护的同名槽。
以前我们直接把Shoot按钮的 clicked()信号连接到CannonField的 shoot()槽。这次
我们想跟踪射击的次数,所以我们把它改为连接到这个类里面一个被保护的槽。
注意当你用独立的组件工作的时候,改变程序的行为是多么的容易。
我们也使用cannonField的 canShoot()信号来适当地使 Shoot按钮生效和失效。
译者:Cavendish 整理: http://www.mculib.com/
我们创建、设置并且连接这个New Game按钮就像我们对其它按钮所做的一样。点
击这个按钮就会激活这个窗口部件的newGame()槽。
我们创建了四个新的窗口部件。注意我们不怕麻烦的把 QLabel窗口部件的指针保
留到GameBoard类中是因为我们不想再对它们做什么了。当GameBoard窗口部件
被销毁的时候,Qt将会删除它们,并且布局类会适当地重新定义它们的大小。
右上单元格的窗口部件的数量正在变大。从前它是空的,现在它是完全充足的,我
们把它们放到布局中来更好的看到它们。
注意我们让所有的窗口部件获得它们更喜欢的大小,改为在 New Game按钮的左边
加入了一个可以自由伸展的东西。
我们已经做完了所有关于GameBoard的构造,所以我们使用newGame()来开始。
(newGame()是一个槽,但是就像我们所说的,槽也可以像普通的函数一样使
用。)
译者:Cavendish 整理: http://www.mculib.com/
这个函数进行射击。如果游戏结束了或者还有一个炮弹在空中,我们立即返回。我
们减少炮弹的数量并告诉加农炮进行射击。
当炮弹击中目标的时候这个槽被激活。我们增加射中的数量。如果没有炮弹了,游
戏就结束了。否则,我们会让CannonField生成新的目标。
当炮弹射失目标的时候这个槽被激活,如果没有炮弹了,游戏就结束了。
当用户点击Restart按钮的时候这个槽被激活。它也会被构造函数调用。首先它把
炮弹的数量设置为15。注意这里是我们在程序中唯一设置炮弹数量的地方。把它
改变为你所想要的游戏规则。接下来我们重置射中的数量,重新开始游戏,并且生
成一个新的目标。
t13/main.cpp
这个文件仅仅被删掉了一部分。MyWidget没了,并且唯一剩下的是 main()函数,
除了名称的改变其它都没有改变。
行为
射中的和剩余炮弹的数量被显示并且程序继续跟踪它们。游戏可以结束了,并且还
有一个按钮可以开始一个新游戏。
译者:Cavendish 整理: http://www.mculib.com/
(请看编译来学习如何创建一个makefile和连编应用程序。)
练习
添加一个随机的风的因素并把它显示给用户看。
当炮弹击中目标的时候做一些飞溅的效果。
实现多个目标。
现在你可以进行第十四章了。
Qt教程一 ——第十四章:面对墙壁
译者:Cavendish 整理: http://www.mculib.com/
这是最后的例子:一个完整的游戏。
我们添加键盘快捷键并引入鼠标事件到CannonField。我们在CannonField周围放
一个框架并添加一个障碍物(墙)使这个游戏更富有挑战性。
• t14/lcdrange.h包含 LCDRange类定义。
• t14/lcdrange.cpp包含 LCDRange类实现。
• t14/cannon.h包含CannonField类定义。
• t14/cannon.cpp包含CannonField类实现。
• t14/gamebrd.h包含GameBoard类定义。
• t14/gamebrd.cpp包含GameBoard类实现。
• t14/main.cpp包含 MyWidget和main。
一行一行地解说
t14/cannon.h
CannonField现在可以接收鼠标事件,使得用户可以通过点击和拖拽炮筒来瞄准。
CannonField也有一个障碍物的墙。
译者:Cavendish 整理: http://www.mculib.com/
除了常见的事件处理器,CannonField实现了三个鼠标事件处理器。名称说明了一
切。
这个私有函数绘制了障碍物墙。
这个私有寒暑返回封装障碍物的矩形。
这个私有函数检查是否一个点在加农炮炮筒的内部。
当用户在炮筒上点击鼠标并且没有放开的话,这个私有变量为 TRUE。
t14/cannon.cpp
这一行被添加到构造函数中。最开始的时候,鼠标没有在炮筒上点击。
现在我们有了一个障碍物,这样就有了三种射失的方法。我们来测试一下第三种。
这是一个Qt事件处理器。当鼠标指针在窗口部件上,用户按下鼠标的按键时,它
被调用。
如果事件不是由鼠标左键产生的,我们立即返回。否则,我们检查鼠标指针是否在
加农炮的炮筒内。如果是的,我们设置 barrelPressed 为 TRUE。
译者:Cavendish 整理: http://www.mculib.com/
注意pos()函数返回的是窗口部件坐标系统中的点。
这是另外一个Qt事件处理器。当用户已经在窗口部件中按下了鼠标按键并且移动/
拖拽鼠标时,它被调用。(你可以让 Qt在没有鼠标按键被按下的时候发送鼠标移
动事件。请看QWidget::setMouseTracking()。)
这个处理器根据鼠标指针的位置重新配置加农炮的炮筒。
首先,如果炮筒没有被按下,我们返回。接下来,我们获得鼠标指针的位置。如果
鼠标指针到了窗口部件的左面或者下面,我们调整鼠标指针使它返回到窗口部件
中。
然后我们计算在鼠标指针和窗口部件的左下角所构成的虚构的线和窗口部件下边界
的角度。最后,我们把加农炮的角度设置为我们新算出来的角度。
记住要用setAngle()来重新绘制加农炮。
只要用户释放鼠标按钮并且它是在窗口部件中按下的时候,这个 Qt事件处理器就
会被调用。
如果鼠标左键被释放,我们就会确认炮筒不再被按下了。
绘画事件包含了下述额外的两行:
paintBarrier()做的和paintShot()、paintTarget()和paintCannon()是同样的事情。
译者:Cavendish 整理: http://www.mculib.com/
这个私有函数用一个黑色边界黄色填充的矩形作为障碍物。
这个私有函数返回障碍物的矩形。我们把障碍物的下边界和窗口部件的下边界放在
了一起。
如果点在炮筒内,这个函数返回TRUE;否则它就返回 FALSE。
这里我们使用QWMatrix类。它是在头文件qwmatrix.h中定义的,这个头文件被
qpainter.h包含。
QWMatrix定义了一个坐标系统映射。它可以执行和 QPainter中一样的转换。
这里我们实现同样的转换的步骤就和我们在paintCannon()函数中绘制炮筒的时候
所作的一样。首先我们转换坐标系统,然后我们旋转它。
现在我们需要检查点p(在窗口部件坐标系统中)是否在炮筒内。为了做到这一
点,我们倒置这个转换矩阵。倒置的矩阵就执行了我们在绘制炮筒时使用的倒置的
转换。我们通过使用倒置矩阵来映射点 p,并且如果它在初始的炮筒矩形内就返回
TRUE。
t14/gamebrd.cpp
我们包含QAccel的类定义。
译者:Cavendish 整理: http://www.mculib.com/
我们创建并设置一个QVBox,设置它的框架风格,并在之后创建 CannonField 作
为这个盒子的子对象。因为没有其它的东西在这个盒子里了,效果就是QVBox会
在CannonField周围生成了一个框架。
现在我们创建并设置一个加速键。加速键就是在应用程序中截取键盘事件并且如果
特定的键被按下的时候调用相应的槽。这种机制也被称为快捷键。注意快捷键是窗
口部件的子对象并且当窗口部件被销毁的时候销毁。QAccel不是窗口部件,并且
在它的父对象中没有任何可见的效果。
我们定义两个快捷键。我们希望在Enter键被按下的时候调用 fire()槽,在Ctrl+Q
键被按下的时候,应用程序退出。因为 Enter有时又被称为 Return,并且有时键盘
中两个键都有,所以我们让这两个键都调用 fire()。
并且之后我们设置Ctrl+Q和Alt+Q做同样的事情。一些人通常使用Ctrl+Q更多一
些(并且无论如何它显示了如果做到它)。
CTRL、Key_Enter、Key_Return和Key_Q都是Qt提供的常量。它们实际上就是
Qt::Key_Enter等等,但是实际上所有的类都继承了Qt这个命名空间类。
我们放置box(QVBox),不是CannonField,在右下的单元格中。
行为
现在当你按下Enter的时候,加农炮就会发射。你也可以用鼠标来确定加农炮的角
度。障碍物会使你在玩游戏的时候获得更多一点的挑战。我们还会在 CannnonField
周围看到一个好看的框架。
(请看编译来学习如何创建一个makefile和连编应用程序。)
译者:Cavendish 整理: http://www.mculib.com/
练习
写一个空间入侵者的游戏。
(这个练习首先被 IgorRafienko作出来了。你可以下载他的游戏。)
新的练习是:写一个突围游戏。
最后的劝告:现在向前进,创造编程艺术的杰作!