admin 管理员组文章数量: 887021
2024年1月13日发(作者:计算机中count是什么意思)
如何学习Qt
我们假设你已经熟悉C++了!
请先阅读一下Qt白皮书。它包含一个关于Qt软件的概述,并且提供了一些用来示范使用Qt进行编程的代码的片断。它会给你一个“大的图画”。
如果你想要完全的在C++中进行编程,不使用任何设计工具的帮助下在代码中设计你的界面,请阅读教程。教程1就是被设计成把你带入Qt编程的一个教程,它更强调的是编写代码而不是一个特征的漫游。教程2是一个更加真实的例子,示范了如何编写菜单、工具条、文件的载入和保存、对话框等地那个。
如果你想使用一个设计工具来设计你的用户界面,那么你至少要先阅读Qt设计器手册的前几章。在这之后,学习一下上面提到的纯粹的C++教程(教程1和教程2)还是很值得的。
到现在为止,如果你已经完成了一些小的可以工作的应用程序并且对Qt编程有了一个主要的了解。你可以直接开始你自己的项目了,但我们建议你阅读一些关键的概述来加深你对Qt的理解:对象模型和信号和槽。
在这里我们建议你看一下概述并且阅读一些和你的项目相关的文章。你也许会发现浏览和你项目做相同事情的实例的源代码是非常有用的。你也可以阅读Qt的源代码,因为它们也被提供。
如果你运行demo这个应用程序(在$QTDIR/examples/demo),你就会看到很多运转中的Qt窗口部件
Qt提供了广泛的文档,完全前后参考的超文本,所以你可以很容易地按你喜欢的方式进行点击。在文档中,你最经常使用的部分可能就是API参考。每一个链接都提供了一个不同的方式来导航API参考,全都试试,看哪一个更适合你。
你现在应该已经准备好你的伟大工程:祝你好运,玩得开心!
Qt教程一 —— 共十四步
这个教程介绍了使用Qt工具包进行图形用户界面编程。它没有包括所有的东西:强调的是教授一种图形用户界面编程的编程思想,并且介绍Qt的特征也是必需的。一些通常情况下使用的特征在这个教程里没有用到。
第一章开始讲述一个十行的Hello World程序并且后来的每一章都介绍了一个或几个更多的概念。一直到第十四章,程序已经从第一章的十行变成了六百五十行的游戏。
如果你对Qt完全不熟悉,如果你还没有读过如何学习Qt的话,请读一下。
教程章节:
1.
2.
3.
4.
5.
6.
7.
Hello, World!
调用退出
家庭价值
使用窗口部件
组装积木
组装丰富的积木!
一个事物领导另一个
8. 准备战斗
9. 你可以使用加农炮了
10. 像丝一样滑
11. 给它一个炮弹
12. 悬在空中的砖
13. 游戏结束
14. 面对墙壁
这个小游戏看起来不像一个现代的图形用户界面应用程序。它只使用了有用的少数图形用户界面技术,但是如果你通过它工作之后,我们建议你阅读一下教程二。第二个教程更加正式一些,并且覆盖了包括菜单条、工具条、文件的载入和保存、对话框等典型应用程序的特征。
Qt教程一 —— 第一章:Hello, World!
第一个程序是一个简单的Hello World例子。它只包含你建立和运行Qt应用程序所需要的最少的代码。上面的图片是这个程序的快照。
/****************************************************************
**
** Qt教程一 - 2
**
****************************************************************/
#include
#include
int main( int argc, char **argv )
{
QApplication a( argc, argv );
QPushButton hello( "Hello world!", 0 );
( 100, 30 );
nWidget( &hello );
();
return ();
}
一行一行地解说
#include
这一行包含了QApplication类的定义。在每一个使用Qt的应用程序中都必须使用一个QApplication对象。QApplication管理了各种各样的应用程序的广泛资源,比如默认的字体和光标。
#include
这一行包含了QPushButton类的定义。参考文档的文件的最上部分提到了使用哪个类就必须包含哪个头文件的说明。
QPushButton是一个经典的图形用户界面按钮,用户可以按下去,也可以放开。它管理自己的观感,就像其它每一个QWidget。一个窗口部件就是一个可以处理用户输入和绘制图形的用户界面对象。程序员可以改变它的全部观感和它的许多主要的属性(比如颜色),还有这个窗口部件的内容。一个QPushButton可以显示一段文本或者一个QPixmap。
int main( int argc, char **argv )
{
main()函数是程序的入口。几乎在使用Qt的所有情况下,main()只需要在把控制转交给Qt库之前执行一些初始化,然后Qt库通过事件来向程序告知用户的行为。
argc是命令行变量的数量,argv是命令行变量的数组。这是一个C/C++特征。它不是Qt专有的,无论如何Qt需要处理这些变量(请看下面)。
QApplication a( argc, argv );
a是这个程序的QApplication。它在这里被创建并且处理这些命令行变量(比如在X窗口下的-display)。请注意,所有被Qt识别的命令行参数都会从argv中被移除(并且argc也因此而减少)。关于细节请看QApplication::argv()文档。
注意:在任何Qt的窗口系统部件被使用之前创建QApplication对象是必须的。
QPushButton hello( "Hello world!", 0 );
这里,在QApplication之后,接着的是第一个窗口系统代码:一个按钮被创建了。
这个按钮被设置成显示“Hello world!”并且它自己构成了一个窗口(因为在构造函数指定0为它的父窗口,在这个父窗口中按钮被定位)。
( 100, 30 );
这个按酒被设置成100像素宽,30像素高(加上窗口系统边框)。在这种情况下,我们不用考虑按钮的位置,并且我们接受默认值。
nWidget( &hello );
这个按钮被选为这个应用程序的主窗口部件。如果用户关闭了主窗口部件,应用程序就退出了。
你不用必须设置一个主窗口部件,但绝大多数程序都有一个。
();
当你创建一个窗口部件的时候,它是不可见的。你必须调用show()来使它变为可见的。
return ();
这里就是main()把控制转交给Qt,并且当应用程序退出的时候exec()就会返回。
在exec()中,Qt接受并处理用户和系统的事件并且把它们传递给适当的窗口部件。
}
你现在可以试着编译和运行这个程序了。
编译
编译一个C++应用程序,你需要创建一个makefile。创建一个Qt的makefile的最容易的方法是使用Qt提供的连编工具qmake。如果你已经把保存到它自己的目录了,你所要做的就是这些:
qmake -project
qmake
第一个命令调用qmake来生成一个.pro(项目)文件。第二个命令根据这个项目文件来生成一个(系统相关的)makefile。你现在可以输入make(或者nmake,如果你使用Visual Studio),然后运行你的第一个Qt应用程序!
行为
当你运行它的时候,你就会看到一个被单一按钮充满的小窗口,在它上面你可以读到著名的词:Hellow World!
练习
试着改变窗口的大小。按下按钮。如果你在X窗口下运行,使用-geometry选项(比如,-geometry 100x200+10+20)来运行这个程序。
现在你可以进行第二章了。
[下一章] [教程一主页]
Qt教程一 —— 第二章:调用退出
你已经在第一章中创建了一个窗口,我们现在使这个应用程序在用户让它退出的时候退出。
我们也会使用一个比默认字体更好的一个字体。
/****************************************************************
**
** Qt教程一 - 2
**
****************************************************************/
#include
#include
#include
int main( int argc, char **argv )
{
QApplication a( argc, argv );
QPushButton quit( "Quit", 0 );
( 75, 30 );
t( QFont( "Times", 18, QFont::Bold ) );
QObject::connect( &quit, SIGNAL(clicked()), &a, SLOT(quit()) );
nWidget( &quit );
();
return ();
}
一行一行地解说
#include
因为这个程序使用了QFont,所以它需要包含qfont.h。Qt的字体提取和X中提供的可怕的字体提取大为不同,字体的载入和使用都已经被高度优化了。
QPushButton quit( "Quit", 0 );
这时,按钮显示“Quit”,确切的说这就是当用户点击这个按钮时程序所要做的。这不是一个巧合。因为这个按钮是一个顶层窗口,我们还是把0作为它的父对象。
( 75, 30 );
我们给这个按钮选择了另外一个大小,因为这个文本比“Hello world!”小一些。我们也可以使用QFontMetrics来设置正确的大小。
t( QFont( "Times", 18, QFont::Bold ) );
这里我们给这个按钮选择了一个新字体,Times字体中的18点加粗字体。注意在这里我们调用了这个字体。
你也可以改变整个应用程序的默认字体(使用QApplication::setFont())。
QObject::connect( &quit, SIGNAL(clicked()), &a, SLOT(quit()) );
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的绝大多数行为。
现在你可以进行第三章了。
Qt教程一 —— 第三章:家庭价值
这个例子演示了如何创建一个父窗口部件和子窗口部件。
我们将会保持这个程序的简单性,并且只使用一个单一的父窗口部件和一个独立的子窗口部件。
/****************************************************************
**
** Qt教程一 - 3
**
****************************************************************/
#include
#include
#include
#include
int main( int argc, char **argv )
{
QApplication a( argc, argv );
QVBox box;
( 200, 120 );
QPushButton quit( "Quit", &box );
t( QFont( "Times", 18, QFont::Bold ) );
QObject::connect( &quit, SIGNAL(clicked()), &a, SLOT(quit()) );
nWidget( &box );
();
return ();
}
一行一行地解说
#include
我们添加了一个头文件qvbox.h用来获得我们要使用的布局类。
QVBox box;
这里我们简单地创建了一个垂直的盒子容器。QVBox把它的子窗口部件排成一个垂直的行,一个在其它的上面,根据每一个子窗口部件的QWidget::sizePolicy()来安排空间。
( 200, 120 );
我们它的高设置为120像素,宽为200像素。
QPushButton quit( "Quit", &box );
子窗口部件产生了。
QPushButton通过一个文本(“text”)和一个父窗口部件(box)生成的。子窗口部件总是放在它的父窗口部件的最顶端。当它被显示的时候,它被父窗口部件的边界挡住了一部分。
父窗口部件,QVBox,自动地把这个子窗口部件添加到它的盒子中央。因为没有其它的东西被添加了,这个按钮就获得了父窗口部件的所有空间。
();
当父窗口部件被显示的时候,它会调用所有子窗口部件的显示函数(除非在这些子窗口部件中你已经明确地使用QWidget::hide())。
行为
这个按钮不再充满整个窗口部件。相反,它获得了一个“自然的”大小。这是因为现在的这个新的顶层窗口,使用了按钮的大小提示和大小变化策略来设置这个按钮的大小和位置。(请看QWidget::sizeHint()和QWidget::setSizePolicy()来获得关于这几个函数的更详细的信息。)
(请看编译来学习如何创建一个makefile和连编应用程序。)
练习
试着改变窗口的大小。按钮是如何变化的?按钮的大小变化策略是什么?如果你运行这个程序的时候使用了一个大一些的字体,按钮的高度发生了什么变化?如果你试图让这个窗口真的变小,发生了什么?
现在你可以进行第四章了。
Qt教程一 —— 第四章:使用窗口部件
这个例子显示了如何创建一个你自己的窗口部件,描述如何控制一个窗口部件的最小大小和最大大小,并且介绍了窗口部件的名称。
/****************************************************************
**
** Qt教程一 - 4
**
****************************************************************/
#include
#include
#include
class MyWidget : public QWidget
{
public:
MyWidget( QWidget *parent=0, const char *name=0 );
};
MyWidget::MyWidget( QWidget *parent, const char *name )
: QWidget( parent, name )
{
setMinimumSize( 200, 120 );
setMaximumSize( 200, 120 );
QPushButton *quit = new QPushButton( "Quit", this, "quit" );
quit->setGeometry( 62, 40, 75, 30 );
quit->setFont( QFont( "Times", 18, QFont::Bold ) );
connect( quit, SIGNAL(clicked()), qApp, SLOT(quit()) );
}
int main( int argc, char **argv )
{
QApplication a( argc, argv );
MyWidget w;
metry( 100, 100, 200, 120 );
nWidget( &w );
();
return ();
}
一行一行地解说
class MyWidget : public QWidget
{
public:
MyWidget( QWidget *parent=0, const char *name=0 );
};
这里我们创建了一个新类。因为这个类继承了QWidget,所以新类是一个窗口部件,并且可以最为一个顶层窗口或者子窗口部件(像第三章里面的按钮)。
这个类只有一个成员函数,构造函数(加上从QWidget继承来的成员函数)。这个构造函数是一个标准的Qt窗口部件构造函数,当你创建窗口部件时,你应该总是包含一个相似的构造函数。
第一个参数是它的父窗口部件。为了生成一个顶层窗口,你指定一个空指针作为父窗口部件。就像你看到的那样,这个窗口部件默认地被认做是一个顶层窗口。
第二个参数是这个窗口部件的名称。这个不是显示在窗口标题栏或者按钮上的文本。这只是分配给窗口部件的一个名称,以后可以用来查找这个窗口部件,并且这里还有一个方便的调试功能可以完整地列出窗口部件层次。
MyWidget::MyWidget( QWidget *parent, const char *name )
: QWidget( parent, name )
构造函数的实现从这里开始。像大多数窗口部件一样,它把parent和name传递给了QWidget的构造函数。
{
setMinimumSize( 200, 120 );
setMaximumSize( 200, 120 );
因为这个窗口部件不知道如何处理重新定义大小,我们把它的最小大小和最大大小设置为相等的值,这样我们就确定了它的大小。在下一章,我们将演示窗口部件如何响应用户的重新定义大小事件。
QPushButton *quit = new QPushButton( "Quit", this, "quit" );
quit->setGeometry( 62, 40, 75, 30 );
quit->setFont( QFont( "Times", 18, QFont::Bold ) );
这里我们创建并设置了这个窗口部件的一个名称为“quit”的子窗口部件(新窗口部件的父窗口部件是this)。这个窗口部件名称和按钮文本没有关系,只是在这一情况下碰巧相似。
注意quit是这个构造函数中的局部变量。MyWidget不能跟踪它,但Qt可以,当MyWidget被删除的时候,默认地它也会被删除。这就是为什么MyWidget不需要一个析构函数的原因。(另外一方面,如果你选择删除一个子窗口部件,也没什么坏处,这个子窗口部件会自动告诉Qt它即将死亡。)
setGeometry()调用和上一章的move()和resize()是一样的。
connect( quit, SIGNAL(clicked()), qApp, SLOT(quit()) );
}
因为MyWidget类不知道这个应用程序对象,它不得不连接到Qt的指针,qApp。
一个窗口部件就是一个软件组件并且它应该尽量少地知道关于它的环境,因为它应该尽可能的通用和可重用。
知道了应用程序的名称将会打破上述原则,所以在一个组件,比如MyWidget,需要和应用程序对象对话的这种情况下,Qt提供了一个别名,qApp。
int main( int argc, char **argv )
{
QApplication a( argc, argv );
MyWidget w;
metry( 100, 100, 200, 120 );
nWidget( &w );
();
return ();
}
这里我们举例说明了我们的新子窗口部件,把它设置为主窗口部件,并且执行这个应用程序。
行为
这个程序和上一章的在行为上非常相似。不同点是我们实现的方式。无论如何它的行为还是有一些小差别。试试改变它的大小,你会看到什么?
(请看编译来学习如何创建一个makefile和连编应用程序。)
练习
试着在main()中创建另一个MyWidget对象。发生了什么?
试着添加更多的按钮或者把除了QPushButton之外的东西放到窗口部件中。
现在你可以进行第五章了。
| 注释的类 | 分组的类 | 函数
Qt教程一 —— 第五章:组装积木
这个例子显示了创建几个窗口部件并用信号和槽把它们连接起来,和如何处理重新定义大小事件。
/****************************************************************
**
** Qt教程一 - 5
**
****************************************************************/
#include
#include
#include
#include
#include
#include
class MyWidget : public QVBox
{
public:
MyWidget( QWidget *parent=0, const char *name=0 );
};
MyWidget::MyWidget( QWidget *parent, const char *name )
: QVBox( parent, name )
{
QPushButton *quit = new QPushButton( "Quit", this, "quit" );
quit->setFont( QFont( "Times", 18, QFont::Bold ) );
connect( quit, SIGNAL(clicked()), qApp, SLOT(quit()) );
QLCDNumber *lcd = new QLCDNumber( 2, this, "lcd" );
QSlider * slider = new QSlider( Horizontal, this, "slider" );
slider->setRange( 0, 99 );
slider->setValue( 0 );
connect( slider, SIGNAL(valueChanged(int)), lcd, SLOT(display(int)) );
}
int main( int argc, char **argv )
{
QApplication a( argc, argv );
MyWidget w;
nWidget( &w );
();
return ();
}
一行一行地解说
#include
#include
#include
#include
#include
#include
这里显示的是三个新的被包含的头文件。qslider.h和qlcdnumber.h在这里是因为我们使用了两个新的窗口部件,QSlider和QLCDNumber。qvbox.h在这里是因为我们使用了Qt的自动布局支持。
class MyWidget : public QVBox
{
public:
MyWidget( QWidget *parent=0, const char *name=0 );
};
MyWidget::MyWidget( QWidget *parent, const char *name )
: QVBox( parent, name )
{
MyWidget现在继承了QVBox,而不是QWidget。我们通过这种方式来使用QVBox的布局(它可以把它的子窗口部件垂直地放在自己里面)。重新定义大小自动地被QVBox处理,因此现在也就被MyWidget处理了。
QLCDNumber *lcd = new QLCDNumber( 2, this, "lcd" );
lcd是一个QLCDNumber,一个可以按像LCD的方式显示数字的窗口部件。这个实例被设置为显示两个数字,并且是this的子窗口部件。它被命名为“lcd”。
QSlider * slider = new QSlider( Horizontal, this, "slider" );
slider->setRange( 0, 99 );
slider->setValue( 0 );
QSlider是一个经典的滑块,用户可以通过在拖动一个东西在一定范围内调节一个整数数值的方式来使用这个窗口部件。这里我们创建了一个水平的滑块,设置它的范围是0~99(包括0和99,参见QSlider::setRange()文档)并且它的初始值是0。
connect( slider, SIGNAL(valueChanged(int)), lcd, SLOT(display(int)) );
这里我们是用了信号/槽机制把滑块的valueChanged()信号和LCD数字的display()槽连接起来了。
无论什么时候滑块的值发生了变化,它都会通过发射valueChanged()信号来广播这个新的值。因为这个信号已经和LCD数字的display()槽连接起来了,当信号被广播的时候,这个槽就被调用了。这两个对象中的任何一个都不知道对方。这就是组件编程的本质。
槽是和普通C++成员函数的方式不同,但有着普通C++成员函数的方位规则。
行为
LCD数字反应了你对滑块做的一切,并且这个窗口部件很好地处理了重新定义大小事件。注意当窗口被重新定义大小(因为它可以)的时候,LDC数字窗口部件也改变了大小,但是其它的还是和原来一样(因为如果它们变化了,看起来好像很傻)。
(请看编译来学习如何创建一个makefile和连编应用程序。)
练习
试着改变LCD数字,添加更多的数字或者改变模式。你甚至可以添加四个按钮来设置基数。
你也可以改变滑块的范围。
也许使用QSpinBox比滑块更好?
试着当LCD数字溢出的时候使这个应用程序退出。
现在你可以进行第六章了。
Qt教程一 —— 第六章:组装丰富的积木!
这个例子显示了如何把两个窗口部件封装成一个新的组件和使用许多窗口部件是多么的容易。首先,我们使用一个自定义的窗口部件作为一个子窗口部件。
/****************************************************************
**
** Qt教程一 - 6
**
****************************************************************/
#include
#include
#include
#include
#include
#include
#include
class LCDRange : public QVBox
{
public:
LCDRange( QWidget *parent=0, const char *name=0 );
};
LCDRange::LCDRange( QWidget *parent, const char *name )
: QVBox( parent, name )
{
QLCDNumber *lcd = new QLCDNumber( 2, this, "lcd" );
QSlider * slider = new QSlider( Horizontal, this, "slider" );
slider->setRange( 0, 99 );
slider->setValue( 0 );
connect( slider, SIGNAL(valueChanged(int)), lcd, SLOT(display(int)) );
}
class MyWidget : public QVBox
{
public:
MyWidget( QWidget *parent=0, const char *name=0 );
};
MyWidget::MyWidget( QWidget *parent, const char *name )
: QVBox( parent, name )
{
QPushButton *quit = new QPushButton( "Quit", this, "quit" );
quit->setFont( QFont( "Times", 18, QFont::Bold ) );
connect( quit, SIGNAL(clicked()), qApp, SLOT(quit()) );
QGrid *grid = new QGrid( 4, this );
for( int r = 0 ; r < 4 ; r++ )
for( int c = 0 ; c < 4 ; c++ )
(void)new LCDRange( grid );
}
int main( int argc, char **argv )
{
QApplication a( argc, argv );
MyWidget w;
nWidget( &w );
();
return ();
}
一行一行地解说
class LCDRange : public QVBox
{
public:
LCDRange( QWidget *parent=0, const char *name=0 );
};
LCDRange窗口部件是一个没有任何API的窗口部件。它只有一个构造函数。这种窗口部件不是很有用,所以我们一会儿会加入一些API。
LCDRange::LCDRange( QWidget *parent, const char *name )
: QVBox( parent, name )
{
QLCDNumber *lcd = new QLCDNumber( 2, this, "lcd" );
QSlider * slider = new QSlider( Horizontal, this, "slider" );
slider->setRange( 0, 99 );
slider->setValue( 0 );
connect( slider, SIGNAL(valueChanged(int)), lcd, SLOT(display(int)) );
}
这里直接利用了第五章里面的MyWidget的构造函数。唯一的不同是按钮被省略了并且这个类被重新命名了。
class MyWidget : public QVBox
{
public:
MyWidget( QWidget *parent=0, const char *name=0 );
};
MyWidget也是除了一个构造函数之外没有包含任何API。
MyWidget::MyWidget( QWidget *parent, const char *name )
: QVBox( parent, name )
{
QPushButton *quit = new QPushButton( "Quit", this, "quit" );
quit->setFont( QFont( "Times", 18, QFont::Bold ) );
connect( quit, SIGNAL(clicked()), qApp, SLOT(quit()) );
这个按钮被放在LCDRange中,这样我们就有了一个“Quit”按钮和许多LCDRange对象。
QGrid *grid = new QGrid( 4, this );
我们创建了一个四列的QGrid对象。这个QGrid窗口部件可以自动地把自己地子窗口部件排列到行列中,你可以指定行和列的数量,并且QGrid可以发现它的新子窗口部件并且把它们安放到网格中。
for( int r = 0 ; r < 4 ; r++ )
for( int c = 0 ; c < 4 ; c++ )
(void)new LCDRange( grid );
四行,四列。
我们创建了一个4*4个LCDRanges,所有这些都是这个grid对象的子窗口部件。这个QGrid窗口部件会安排它们。
}
这就是全部了。
行为
这个程序显示了在同一时间使用许多窗口部件是多么的容易。其中的滑块和LCD数字的行为在前一章已经提到过了。还有就是,就是实现的不同。
(请看编译来学习如何创建一个makefile和连编应用程序。)
练习
在开始的时候使用不同的或者随机的值初始化每个滑块。
源代码中的“4”出现了3次。如果你改变QGrid构造函数中调用的那个,会发生什么?改变另外两个又会发生什么呢?为什么呢?
现在你可以进行第七章了。
Qt教程一 —— 第七章:一个事物领导另一个
这个例子显示了如何使用信号和槽来创建自定义窗口部件,和如何使用更加复杂的方式把它们连接起来。首先,源文件被我们分成几部分并放在放在t7目录下。
•
•
•
t7/lcdrange.h包含LCDRange类定义。
t7/包含LCDRange类实现。
t7/包含MyWidget和main。
一行一行地解说
t7/lcdrange.h
这个文件主要利用了第六章的,在这里只是说明一下改变了哪些。
#ifndef LCDRANGE_H
#define LCDRANGE_H
这里是一个经典的C语句,为了避免出现一个头文件被包含不止一次的情况。如果你没有使用过它,这是开发中的一个很好的习惯。#ifndef需要把这个头文件的全部都包含进去。
#include
qvbox.h被包含了。LCDRange继承了QVBox,所以父类的头文件必须被包含。我们在前几章里面偷了一点懒,我们通过包含其它一些头文件,比如qpushbutton.h,这样就可以间接地包含qwidget.h。
class QSlider;
这里是另外一个小伎俩,但是没有前一个用的多。因为我们在类的界面中不需要QSlider,仅仅是在实现中,我们在头文件中使用一个前置的类声明,并且在.cpp文件中包含一个QSlider的头文件。
这会使编译一个大的项目变得更快,因为当一个头文件改变的时候,很少的文件需要重新编译。它通常可以给大型编译加速两倍或两倍以上。
class LCDRange : public QVBox
{
Q_OBJECT
public:
LCDRange( QWidget *parent=0, const char *name=0 );
meta object file. 注意Q_OBJECT。这个宏必须被包含到所有使用信号和/或槽的类。如果你很好奇,它定义了在元对象文件中实现的一些函数。
int value() const;
public slots:
void setValue( int );
signals:
void valueChanged( int );
这三个成员函数构成了这个窗口部件和程序中其它组件的接口。直到现在,LCDRange根本没有一个真正的接口。
value()是一个可以访问LCDRange的值的公共函数。setValue()是我们第一个自定义槽,并且valueChanged()是我们第一个自定义信号。
槽必须按通常的方式实现(记住槽也是一个C++成员函数)。信号可以在元对象文件中自动实现。信号也遵守C++函数的保护法则(比如,一个类只能发射它自己定义的或者继承来的信号)。
当LCDRange的值发生变化时,valueChanged()信号就会被使用——你从这个名字中就可以猜到。这将不会是你将会看到的命名为somethingChanged()的最后一个信号。
t7/
这个文件主要利用了t6/,在这里只是说明一下改变了哪些。
connect( slider, SIGNAL(valueChanged(int)),
lcd, SLOT(display(int)) );
connect( slider, SIGNAL(valueChanged(int)),
SIGNAL(valueChanged(int)) );
这个代码来自LCDRange的构造函数。
第一个connect和你在上一章中看到的一样。第二个是新的,它把滑块的valueChanged()信号和这个对象的valueChanged信号连接起来了。带有三个参数的connect()函数连接到this对象的信号或槽。
是的,这是正确的。信号可以被连接到其它的信号。当第一个信号被发射时,第二个信号也被发射。
让我们来看看当用户操作这个滑块的时候都发生了些什么。滑块看到自己的值发生了改变,并发射了valueChanged()信号。这个信号被连接到QLCDNumber的display()槽和LCDRange的valueChanged()信号。
所以,当这个信号被发射的时候,LCDRange发射它自己的valueChanged()信号。另外,QLCDNumber::display()被调用并显示新的数字。
注意你并没有保证执行的任何顺序——LCDRange::valueChanged()也许在QLCDNumber::display()之前或者之后发射,这是完全任意的。
int LCDRange::value() const
{
return slider->value();
}
value()的实现是直接了当的,它简单地返回滑块的值。
void LCDRange::setValue( int value )
{
slider->setValue( value );
}
setValue()的实现是相当直接了当的。注意因为滑块和LCD数字是连接的,设置滑块的值就会自动的改变LCD数字的值。另外,如果滑块的值超过了合法范围,它会自动调节。
t7/
LCDRange *previous = 0;
for( int r = 0 ; r < 4 ; r++ ) {
for( int c = 0 ; c < 4 ; c++ ) {
LCDRange* lr = new LCDRange( grid );
if ( previous )
connect( lr, SIGNAL(valueChanged(int)),
previous, SLOT(setValue(int)) );
previous = lr;
}
}
中所有的部分都是上一章复制的,除了MyWidget的构造函数。当我们创建16个RCDRange对象时,我们现在使用信号/槽机制连接它们。每一个的valueChanged()信号都和前一个的setValue()槽连接起来了。因为当LCDRange的值发生改变的时候,发射一个valueChanged()信号(惊奇!),我们在这里创建了一个信号和槽的“链”。
编译
为一个多文件的应用程序创建一个makefile和为一个单文件的应用程序创建一个makefile是没有什么不同的。如果你已经把这个例子中的所有文件都保存到它们自己的目录中,你所要做的就是这些:
qmake -project
qmake
第一个命令调用qmake来生成一个.pro(项目)文件。第二个命令根据这个项目文件来生成一个(系统相关的)makefile。你现在可以输入make(或者nmake,如果你使用Visual Studio)。
行为
在开始的时候,这个程序看起来和上一章里的一样。试着操作滑块到右下角……
练习
seven LCDs back to 50. 使用右下角的滑块并设置所有的LCD到50。然后设置通过点击这个滑块的左侧把它设置为40。现在,你可以通过把最后一个调到左边来把前七个LCD设置回50。
点击右下角滑块的滑块的左边。发生了什么?为什么只是正确的行为?
现在你可以进行第八章了。
Qt教程一 —— 第八章:准备战斗
在这个例子中,我们介绍可以画自己的第一个自定义窗口部件。我们也加入了一个有用的键盘接口(只用了两行代码)。
•
t8/lcdrange.h包含LCDRange类定义。
•
•
•
•
t8/包含LCDRange类实现。
t8/cannon.h包含CannonField类定义。
t8/包含CannonField类实现。
t8/包含MyWidget和main。
一行一行地解说
t8/lcdrange.h
这个文件和第七章中的lcdrange.h很相似。我们添加了一个槽:setRange()。
void setRange( int minVal, int maxVal );
现在我们添加了设置LCDRange范围的可能性。直到现在,它就可以被设置为0~99。
t8/
在构造函数中有一个变化(稍后我们会讨论的)。
void LCDRange::setRange( int minVal, int maxVal )
{
if ( minVal < 0 || maxVal > 99 || minVal > maxVal ) {
qWarning( "LCDRange::setRange(%d,%d)n"
"tRange must be 0..99n"
"tand minVal must not be greater than maxVal",
minVal, maxVal );
return;
}
slider->setRange( minVal, maxVal );
}
setRange()设置了LCDRange中滑块的范围。因为我们已经把QLCDNumber设置为只显示两位数字了,我们想通过限制minVal和maxVal为0~99来避免QLCDNumber的溢出。(我们可以允许最小值为-9,但是我们没有那样做。)如果参数是非法的,我们使用Qt的qWarning()函数来向用户发出警告并立即返回。qWarning()是一个像printf一样的函数,默认情况下它的输出发送到stderr。如果你想改变的话,你可以使用::qInstallMsgHandler()函数安装自己的处理函数。
t8/cannon.h
CanonField是一个知道如何显示自己的新的自定义窗口部件。
class CannonField : public QWidget
{
Q_OBJECT
public:
CannonField( QWidget *parent=0, const char *name=0 );
CanonField继承了QWidget,我们使用了LCDRange中同样的方式。
int angle() const { return ang; }
QSizePolicy sizePolicy() const;
public slots:
void setAngle( int degrees );
signals:
void angleChanged( int );
目前,CanonField只包含一个角度值,我们使用了LCDRange中同样的方式。
protected:
void paintEvent( QPaintEvent * );
这是我们在QWidget中遇到的许多事件处理器中的第二个。只要一个窗口部件需要刷新它自己(比如,画窗口部件表面),这个虚函数就会被Qt调用。
t8/
CannonField::CannonField( QWidget *parent, const char *name )
: QWidget( parent, name )
{
我们又一次使用和前一章中的LCDRange同样的方式。
ang = 45;
setPalette( QPalette( QColor( 250, 250, 200) ) );
}
构造函数把角度值初始化为45度并且给这个窗口部件设置了一个自定义调色板。
这个调色板只是说明背景色,并选择了其它合适的颜色。(对于这个窗口部件,只有背景色和文本颜色是要用到的。)
void CannonField::setAngle( int degrees )
{
if ( degrees < 5 )
degrees = 5;
if ( degrees > 70 )
degrees = 70;
if ( ang == degrees )
return;
ang = degrees;
repaint();
emit angleChanged( ang );
}
这个函数设置角度值。我们选择了一个5~70的合法范围,并根据这个范围来调节给定的degrees的值。当新的角度值超过了范围,我们选择了不使用警告。
如果新的角度值和旧的一样,我们立即返回。这只对当角度值真的发生变化时,发射angleChanged()信号有重要意义。
然后我们设置新的角度值并重新画我们的窗口部件。QWidget::repaint()函数清空窗口部件(通常用背景色来充满)并向窗口部件发出一个绘画事件。这样的结构就是调用窗口部件的绘画事件函数一次。
最后,我们发射angleChanged()信号来告诉外面的世界,角度值发生了变化。emit关键字只是Qt中的关键字,而不是标准C++的语法。实际上,它只是一个宏。
void CannonField::paintEvent( QPaintEvent * )
{
QString s = "Angle = " + QString::number( ang );
QPainter p( this );
xt( 200, 200, s );
}
这是我们第一次试图写一个绘画事件处理程序。这个事件参数包含一个绘画事件的描述。QPaintEvent包含一个必须被刷新的窗口部件的区域。现在,我们比较懒惰,并且只是画每一件事。
我们的代码在一个固定位置显示窗口部件的角度值。首先我们创建一个含有一些文本和角度值的QString,然后我们创建一个操作这个窗口部件的QPainter并使用它来画这个字符串。我们一会儿会回到QPainter,它可以做很多事。
t8/
#include "cannon.h"
我们包含了我们的新类:
class MyWidget: public QWidget
{
public:
MyWidget( QWidget *parent=0, const char *name=0 );
};
这一次我们在顶层窗口部件中只使用了一个LCDRange和一个CanonField。
LCDRange *angle = new LCDRange( this, "angle" );
在构造函数中,我们创建并设置了我们的LCDRange。
angle->setRange( 5, 70 );
我们设置LCDRange能够接受的范围是5~70度。
CannonField *cannonField
= new CannonField( this, "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和CanonField都省略了这个检查,这个程序就会因为第一次数值变化而进入到一个无限循环当中。
QGridLayout *grid = new QGridLayout( this, 2, 2, 10 );
//2×2,10像素的边界
到现在为止,我们没有因为几何管理把QVBox和QGrid窗口部件集成到一起。现在,无论如何,我们需要对我们的布局加一些控制,所以我们使用了更加强大的QGridLayout类。QGridLayout不是一个窗口部件,它是一个可以管理任何窗口部件作为子对象的不同的类。
就像注释中所说的,我们创建了一个以10像素为边界的2*2的数组。(QGridLayout的构造函数有一点神秘,所以最好在这里加入一些注释。)
grid->addWidget( quit, 0, 0 );
我们在网格的左上的单元格中加入一个Quit按钮:0,0。
grid->addWidget( angle, 1, 0, Qt::AlignTop );
我们把angle这个LCDRange放到左下的单元格,在单元格内向上对齐。(这只是QGridLayout所允许的一种对齐方式,而QGrid不允许。)
grid->addWidget( cannonField, 1, 1 );
我们把CannonField对象放到右下的单元格。(右上的单元格是空的。)
grid->setColStretch( 1, 10 );
我们告诉QGridLayout右边的列(列1)是可拉伸的。因为左边的列不是(它的拉伸因数是0,这是默认值),QGridLayout就会在MyWidget被重新定义大小的时候试图让左面的窗口部件大小不变,而重新定义CannonField的大小。
angle->setValue( 60 );
我们设置了一个初始角度值。注意这将会引发从LCDRange到CannonField的连接。
angle->setFocus();
我们刚才做的是设置angle获得键盘焦点,这样默认情况下键盘输入会到达LCDRange窗口部件。
LCDRange没有包含任何keyPressEvent(),所以这看起来不太可能有用。无论如何,它的构造函数中有了新的一行:
setFocusProxy( slider );
LCDRange设置滑块作为它的焦点代理。这就是说当程序或者用户想要给LCDRange一个键盘焦点,滑块就会就会注意到它。QSlider有一个相当好的键盘接口,所以就会出现我们给LCDRange添加的这一行。
行为
键盘现在可以做一些事了——方向键、Home、End、PageUp和PageDown都可以作一些事情。
当滑块被操作,CannonFiled会显示新的角度值。如果重新定义大小,CannonField会得到尽可能多的空间。
在8位的Windows机器上显示新的颜色会颤动的要命。下一章会处理这些的。
(请看编译来学习如何创建一个makefile和连编应用程序。)
练习
设置重新定义窗口的大小。如果你把它变窄或者变矮会发生什么?
如果你把AlignTop删掉,LCDRange的位置会发生什么变化?为什么?
如果你给左面的列一个非零的拉伸因数,当你重新定义窗口大小时会发生什么?
不考虑setFocus()调用。你更喜欢什么样的行为?
试着在QButton::setText()调用中把“Quit”改为“&Quit”。按钮看起来变成什么样子了?如果你在程序运行的时候按下Alt+Q会发生什么?(在少量键盘中时Meta+Q。)
把CannonField的文本放到中间。
现在你可以进行第九章了。
Qt教程一 —— 第九章:你可以使用加农炮了
在这个例子中我们开始画一个蓝色可爱的小加农炮.只和上一章不同。
•
t9/lcdrange.h包含LCDRange类定义。
•
•
•
•
t9/包含LCDRange类实现。
t9/cannon.h包含CannonField类定义。
t9/包含CannonField类实现。
t9/包含MyWidget和main。
一行一行地解说
t9/
void CannonField::paintEvent( QPaintEvent * )
{
QPainter p( this );
我们现在开始认真地使用QPainter。我们创建一个绘画工具来操作这个窗口部件。
sh( blue );
当一个QPainter填满一个矩形、圆或者其它无论什么,它会用它的画刷填满这个图形。这里我们把画刷设置为蓝色。(我们也可以使用一个调色板。)
( NoPen );
并且QPainter使用画笔来画边界。这里我们设置为NoPen,就是说我们在边界上什么都不画,蓝色画刷会在我们画的东西的边界内画满全部。
ate( 0, rect().bottom() );
QPainter::translate()函数转化QPainter的坐标系统,比如,它通过偏移谅来移动。这里我们设置窗口部件的左下角为(0,0)。x和y的方向没有改变,比如,窗口部件中的所有y坐标现在都是负数(请看坐标系统获得有关Qt的坐标系统更多的信息。)
e( QRect(-35, -35, 70, 70), 0, 90*16 );
drawPie()函数使用一个开始角度和弧长在一个指定的矩形内画一个饼型图。角度的度量用的是一度的十六分之一。零度在三点的位置。画的方向是顺时针的。这里我们在窗口部件的左下角画一个四分之一圆。这个饼图被蓝色充满,并且没有边框。
( -ang );
QPainter::rotate()函数绕QPainter坐标系统的初始位置旋转它。旋转的参数是一个按度数给定的浮点数(不是一个像上面那样给的十六分之一的度数)并且是顺时针的。这里我们顺时针旋转ang度数。
ct( QRect(33, -4, 15, 8) );
QPainter::drawRect()函数画一个指定的矩形。这里我们画的是加农炮的炮筒。
很难想象当坐标系统被转换之后(转化、旋转、缩放或者修剪)的绘画结果。
在这种情况下,坐标系统先被转化后被旋转。如果矩形QRect(33, -4, 15, 8)被画到这个转化后的坐标系统中,它看起来会是这样:
注意矩形被CannonField窗口部件的边界省略了一部分。当我们选装坐标系统,以60度为例,矩形会以(0,0)为圆心被旋转,也就是左下角,因为我们已经转化了坐标系统。结果会是这样:
我们做完了,除了我们还没有解释为什么Windows在这个时候没有发抖。
int main( int argc, char **argv )
{
QApplication::setColorSpec( QApplication::CustomColor );
QApplication a( argc, argv );
我们告诉Qt我们在这个程序中想使用一个不同的颜色分配策略。这里没有单一正确的颜色分配策略。因为这个程序使用了不常用的黄色,但不是很多颜色,CustomColor最好。这里有几个其它的分配策略,你可以在QApplication::setColorSpec()文档中读到它们。
通常情况下你可以忽略这一点,因为默认的是好的。偶尔一些使用常用颜色的应用程序看起来比较糟糕,因而改变分配策略通常会有所帮助。
行为
当滑块被操作的时候,所画的加农炮的角度会因此而变化。
Quit中的字母Q现在有下划线,并且Alt+Q会实现你所要的。如果你不知道这些,你一定是没有做第八章中的练习。
你也要注意加农炮的闪烁让人很烦,特别是在一个比较慢的机器上。我们将会在下一章修正这一点。
(请看编译来学习如何创建一个makefile和连编应用程序。)
练习
设置一个不同的画笔代替NoPen。设置一个调色板的画刷。
试着用“Q&uit”或者“Qu&it”作为按钮的文本来提到“&Quit”。发生了什么?
现在你可以进行第十章了。
Qt教程一 —— 第十章:像丝一样滑
在这个例子中,我们介绍画一个pixmap来除去闪烁。我们也会加入一个力量控制。
•
•
•
•
•
t10/lcdrange.h包含LCDRange类定义。
t10/包含LCDRange类实现。
t10/cannon.h包含CannonField类定义。
t10/包含CannonField类实现。
t10/包含MyWidget和main。
一行一行地解说
t10/cannon.h
CannonField现在除了角度又多了一个力量值。
int angle() const { return ang; }
int force() const { return f; }
public slots:
void setAngle( int degrees );
void setForce( int newton );
signals:
void angleChanged( int );
void forceChanged( int );
力量的接口的实现和角度一样。
private:
QRect cannonRect() const;
我们把加农炮封装的矩形的定义放到了一个单独的函数中。
int ang;
int f;
};
力量被存储到一个整数f中。
t10/
#include
我们包含了QPixmap类定义。
CannonField::CannonField( QWidget *parent, const char *name )
: QWidget( parent, name )
{
ang = 45;
f = 0;
setPalette( QPalette( QColor( 250, 250, 200) ) );
}
力量(f)被初始化为0。
void CannonField::setAngle( int degrees )
{
if ( degrees < 5 )
degrees = 5;
if ( degrees > 70 )
degrees = 70;
if ( ang == degrees )
return;
ang = degrees;
repaint( cannonRect(), FALSE );
emit angleChanged( ang );
}
我们在setAngle()函数中做了一个小的改变。它只重画窗口部件中含有加农炮的一小部分。FALSE参数说明在一个绘画事件发送到窗口部件之前指定的矩形将不会被擦去。这将会使绘画过程加速和平滑。
void CannonField::setForce( int newton )
{
if ( newton < 0 )
newton = 0;
if ( f == newton )
return;
f = newton;
emit forceChanged( f );
}
setForce()的实现和setAngle()很相似。唯一的不同是因为我们不显示力量值,我们不需要重画窗口部件。
void CannonField::paintEvent( QPaintEvent *e )
{
if ( !e->rect().intersects( cannonRect() ) )
return;
我们现在用只重画需要刷新得部分来优化绘画事件。首先我们检查是否不得不完全重画任何事,我们返回是否不需要。
QRect cr = cannonRect();
QPixmap pix( () );
然后,我们创建一个临时的pixmap,我们用来不闪烁地画。所有的绘画操作都在这个pixmap中完成,并且之后只用一步操作来把这个pixmap画到屏幕上。
这是不闪烁绘画的本质:一次准确地在每一个像素上画。更少,你会得到绘画错误。更多,你会得到闪烁。在这个例子中这个并不重要——当代码被写时,仍然是很慢的机器导致闪烁,但以后不会再闪烁了。我们由于教育目的保留了这些代码。
( this, t() );
我们用这个pixmap来充满这个窗口部件的背景。
QPainter p( &pix );
sh( blue );
( NoPen );
ate( 0, () - 1 );
e( QRect( -35,-35, 70, 70 ), 0, 90*16 );
( -ang );
ct( QRect(33, -4, 15, 8) );
();
我们就像第九章中一样画,但是现在我们是在pixmap上画。
在这一点上,我们有一个绘画工具变量和一个pixmap看起来相当正确,但是我们还没有在屏幕上画呢。
( this );
xmap( t(), pix );
所以我们在CannonField上面打开绘图工具并在这之后画这个pixmap。
这就是全部了。在顶部和底部各有一对线,并且这个代码是100%不闪烁的。
QRect CannonField::cannonRect() const
{
QRect r( 0, 0, 50, 50 );
ttomLeft( rect().bottomLeft() );
return r;
}
这个函数返回一个在窗口部件坐标中封装加农炮的矩形。首先我们创建一个50*50大小的矩形,然后移动它,使它的左下角和窗口部件自己的左下角相等。
QWidget::rect()函数在窗口部件自己的坐标(左上角是0,0)中返回窗口部件封装的矩形。
t10/
MyWidget::MyWidget( QWidget *parent, const char *name )
: QWidget( parent, name )
{
构造函数也是一样,但是已经加入了一些东西。
LCDRange *force = new LCDRange( this, "force" );
force->setRange( 10, 50 );
我们加入了第二个LCDRange,用来设置力量。
connect( force, SIGNAL(valueChanged(int)),
cannonField, SLOT(setForce(int)) );
connect( cannonField, SIGNAL(forceChanged(int)),
force, SLOT(setValue(int)) );
我们把force窗口部件和cannonField窗口部件连接起来,就和我们对angle窗口部件做的一样。
QVBoxLayout *leftBox = new QVBoxLayout;
grid->addLayout( leftBox, 1, 0 );
leftBox->addWidget( angle );
leftBox->addWidget( force );
在第九章,我们把angle放到了布局的左下单元。现在我们想在这个单元中放入两个窗口部件,所一个我们用了一个垂直的盒子,把这个垂直的盒子放到这个网格单元中,并且把angle和force放到这个垂直的盒子中。
force->setValue( 25 );
我们初始化力量的值为25。
行为
闪烁已经走了,并且我们还有一个力量控制。
(请看编译来学习如何创建一个makefile和连编应用程序。)
练习
让加农炮的炮筒的大小依赖于力量。
把加农炮放到右下角。
试着加入一个更好的键盘接口。例如,用+和-来增加或者减少力量,用enter来发射。提示:QAccel和在LCDRange中新建addStep()和subtractStep(),就像QSlider::addStep()。如果你被左面和右面键所苦恼(我就是!),试着都改变!
现在你可以进行第十一章了。
Qt教程一 —— 第十一章:给它一个炮弹
在这个例子里我们介绍了一个定时器来实现动画的射击。
•
•
•
•
•
t11/lcdrange.h包含LCDRange类定义。
t11/包含LCDRange类实现。
t11/cannon.h包含CannonField类定义。
t11/包含CannonField类实现。
t11/包含MyWidget和main。
一行一行地解说
t11/cannon.h
CannonField现在就有了射击能力。
void shoot();
当炮弹不在空中中,调用这个槽就会使加农炮射击。
private slots:
void moveShot();
当炮弹正在空中时,这个私有槽使用一个定时器来移动射击。
private:
void paintShot( QPainter * );
这个函数来画射击。
QRect shotRect() const;
当炮弹正在空中的时候,这个私有函数返回封装它所占用空间的矩形,否则它就返回一个没有定义的矩形。
int timerCount;
QTimer * autoShootTimer;
float shoot_ang;
float shoot_f;
};
这些私有变量包含了描述射击的信息。timerCount保留了射击进行后的时间。shoot_ang是加农炮射击时的角度,shoot_f是射击时加农炮的力量。
t11/
#include
我们包含了数学库,因为我们需要使用sin()和cos()函数。
CannonField::CannonField( QWidget *parent, const char *name )
: QWidget( parent, name )
{
ang = 45;
f = 0;
timerCount = 0;
autoShootTimer = new QTimer( this, "movement handler" );
connect( autoShootTimer, SIGNAL(timeout()),
this, SLOT(moveShot()) );
shoot_ang = 0;
shoot_f = 0;
setPalette( QPalette( QColor( 250, 250, 200) ) );
}
我们初始化我们新的私有变量并且把QTimer::timeout()信号和我们的moveShot()槽相连。我们会在定时器超时的时候移动射击。
void CannonField::shoot()
{
if ( autoShootTimer->isActive() )
return;
timerCount = 0;
shoot_ang = ang;
shoot_f = f;
autoShootTimer->start( 50 );
}
只要炮弹不在空中,这个函数就会进行一次射击。timerCount被重新设置为零。shoot_ang和shoot_f设置为当前加农炮的角度和力量。最后,我们开始这个定时器。
void CannonField::moveShot()
{
QRegion r( shotRect() );
timerCount++;
QRect shotR = shotRect();
if ( shotR.x() > width() || shotR.y() > height() )
autoShootTimer->stop();
else
r = ( QRegion( shotR ) );
repaint( r );
}
moveShot()是一个移动射击的槽,当QTimer开始的时候,每50毫秒被调用一次。
它的任务就是计算新的位置,重新画屏幕并把炮弹放到新的位置,并且如果需要的话,停止定时器。
首先我们使用QRegion来保留旧的shotRect()。QRegion可以保留任何种类的区域,并且我们可以用它来简化绘画过程。shotRect()返回现在炮弹所在的矩形——稍后我们会详细介绍。
然后我们增加timerCount,用它来实现炮弹在它的轨迹中移动的每一步。
下一步我们算出新的炮弹的矩形。
如果炮弹已经移动到窗口部件的右面或者下面的边界,我们停止定时器或者添加新的shotRect()到QRegion。
最后,我们重新绘制QRegion。这将会发送一个单一的绘画事件,但仅仅有一个到两个举行需要刷新。
void CannonField::paintEvent( QPaintEvent *e )
{
QRect updateR = e->rect();
QPainter p( this );
if ( ects( cannonRect() ) )
paintCannon( &p );
if ( autoShootTimer->isActive() &&
ects( shotRect() ) )
paintShot( &p );
}
绘画事件函数在前一章中已经被分成两部分了。现在我们得到的新的矩形区域需要绘画,检查加农炮和/或炮弹是否相交,并且如果需要的话,调用paintCannon()和/或paintShot()。
void CannonField::paintShot( QPainter *p )
{
p->setBrush( black );
p->setPen( NoPen );
p->drawRect( shotRect() );
}
这个私有函数画一个黑色填充的矩形作为炮弹。
我们把paintCannon()的实现放到一边,它和前一章中的paintEvent()一样。
QRect CannonField::shotRect() const
{
const double gravity = 4;
double time = timerCount / 4.0;
double velocity = shoot_f;
double radians = shoot_ang*3.14159265/180;
double velx = velocity*cos( radians );
double vely = velocity*sin( radians );
double x0 = ( () + 5 )*cos(radians);
double y0 = ( () + 5 )*sin(radians);
double x = x0 + velx*time;
double y = y0 + vely*time - 0.5*gravity*time*time;
QRect r = QRect( 0, 0, 6, 6 );
nter( QPoint( qRound(x), height() - 1 - qRound(y) ) );
return r;
}
这个私有函数计算炮弹的中心点并且返回封装炮弹的矩形。它除了使用自动增加所过去的时间的timerCount之外,还使用初始时的加农炮的力量和角度。
运算公式使用的是有重力的环境下光滑运动的经典牛顿公式。简单地说,我们已经选择忽略爱因斯坦理论的结果。
我们在一个y坐标向上增加的坐标系统中计算中心点。在我们计算出中心点之后,我们构造一个6*6大小的QRect,并把它的中心移动到我们上面所计算出的中心点。同样的操作我们把这个点移动到窗口部件的坐标系统(请看坐标系统)。
qRound()函数是一个在qglobal.h中定义的内嵌函数(被其它所有Qt头文件包含)。qRound()把一个双精度实数变为最接近的整数。
t11/
class MyWidget: public QWidget
{
public:
MyWidget( QWidget *parent=0, const char *name=0 );
};
唯一的增加是Shoot按钮。
QPushButton *shoot = new QPushButton( "&Shoot", this, "shoot" );
shoot->setFont( QFont( "Times", 18, QFont::Bold ) );
在构造函数中我们创建和设置Shoot按钮就像我们对Quit按钮所做的那样。注意构造函数的第一个参数是按钮的文本,并且第三个是窗口部件的名称。
connect( shoot, SIGNAL(clicked()), cannonField, SLOT(shoot()) );
把Shoot按钮的clicked()信号和CannonField的shoot()槽连接起来。
行为
The cannon can shoot, but there's nothing to shoot at.
(请看编译来学习如何创建一个makefile和连编应用程序。)
练习
用一个填充的圆来表示炮弹。提示:QPainter::drawEllipse()会对你有所帮助。
当炮弹在空中的时候,改变加农炮的颜色。
现在你可以进行第十二章了。
Qt教程一 —— 第十一章:悬在空中的砖
在这个例子中,我们扩展我们的LCDRange类来包含一个文本标签。我们也会给射击提供一个目标。
•
•
•
•
•
t12/lcdrange.h包含LCDRange类定义。
t12/包含LCDRange类实现。
t12/cannon.h包含CannonField类定义。
t12/包含CannonField类实现。
t12/包含MyWidget和main。
一行一行地解说
t12/lcdrange.h
LCDRange现在有了一个文本标签。
class QLabel;
我们名称声明QLabel,因为我们将在这个类声明中使用一个QLabel的指针。
class LCDRange : public QVBox
{
Q_OBJECT
public:
LCDRange( QWidget *parent=0, const char *name=0 );
LCDRange( const char *s, QWidget *parent=0,
const char *name=0 );
我们添加了一个新的构造函数,这个构造函数在父对象和名称之外还设置了标签文本。
const char *text() const;
这个函数返回标签文本。
void setText( const char * );
这个槽设置标签文本。
private:
void init();
因为我们现在有了两个构造函数,我们选择把通常的初始化放在一个私有的init()函数。
QLabel *label;
我们还有一个新的私有变量:一个QLabel。QLabel是一个Qt标准窗口部件并且可以显示一个有或者没有框架的文本或者pixmap。
t12/
#include
这里我们包含了QLabel类定义。
LCDRange::LCDRange( QWidget *parent, const char *name )
: QVBox( parent, name )
{
init();
}
这个构造函数调用了init()函数,它包括了通常的初始化代码。
LCDRange::LCDRange( const char *s, QWidget *parent,
const char *name )
: QVBox( parent, name )
{
init();
setText( s );
}
这个构造函数首先调用了init()然后设置标签文本。
void LCDRange::init()
{
QLCDNumber *lcd = new QLCDNumber( 2, this, "lcd" );
slider = new QSlider( Horizontal, this, "slider" );
slider->setRange( 0, 99 );
slider->setValue( 0 );
label = new QLabel( " ", this, "label" );
label->setAlignment( AlignCenter );
connect( slider, SIGNAL(valueChanged(int)),
lcd, SLOT(display(int)) );
connect( slider, SIGNAL(valueChanged(int)),
SIGNAL(valueChanged(int)) );
setFocusProxy( slider );
}
接下来我们创建一个QLabel并且让它的内容lcd和slider的设置和上一章一样。中间对齐(垂直方向和水平方向都是)。connect()语句也来自于上一章。
const char *LCDRange::text() const
{
return label->text();
}
这个函数返回标签文本。
void LCDRange::setText( const char *s )
{
label->setText( s );
}
这个函数设置标签文本。
t12/cannon.h
CannonField现在有两个新的信号:hit()和missed()。另外它还包含一个目标。
void newTarget();
这个槽在新的位置生成一个新的目标。
signals:
void hit();
void missed();
hit()信号是当炮弹击中目标的时候被发射的。missed()信号是当炮弹移动超出了窗口部件的右面或者下面的边界时被发射的(例如,当然这种情况下它将不会击中目标)。
void paintTarget( QPainter * );
这个私有函数绘制目标。
QRect targetRect() const;
这个私有函数返回一个封装了目标的矩形。
QPoint target;
这个私有变量包含目标的中心点。
t12/
#include
我们包含了QDate、QTime和QDateTime类定义。
#include
我们包含了stdlib库,因为我们需要rand()函数。
newTarget();
这一行已经被添加到了构造函数中。它为目标创建一个“随机的”位置。实际上,newTarget()函数还试图绘制目标。因为我们在一个构造函数中,CannonField窗口部件还是不可以见的。Qt保证在一个隐藏的窗口部件中调用repaint()是没有害处的。
void CannonField::newTarget()
{
static bool first_time = TRUE;
if ( first_time ) {
first_time = FALSE;
QTime midnight( 0, 0, 0 );
srand( (QTime::currentTime()) );
}
QRegion r( targetRect() );
target = QPoint( 200 + rand() % 190,
10 + rand() % 255 );
repaint( ( targetRect() ) );
}
这个私有函数创建了一个在新的“随机的”位置的目标中心点。
我们使用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的随机整数。
void CannonField::moveShot()
{
QRegion r( shotRect() );
timerCount++;
QRect shotR = shotRect();
定时器时间这部分和上一章一样。
if ( ects( targetRect() ) ) {
autoShootTimer->stop();
emit hit();
如果是的,炮弹击中了目标(哎哟!)。if语句检查炮弹矩形和目标矩形是否相交。我们停止射击定时器并且发射hit()信号来告诉外界目标已经被破坏,并返回。
注意,我们可以在这个点上创建一个新的目标,但是因为CannonField是一个组件,所以我们要把这样的决定留给组件的使用者。
} else if ( shotR.x() > width() || shotR.y() > height() ) {
autoShootTimer->stop();
emit missed();
这个if语句和上一章一样,除了现在它发射missed()信号告诉外界这次失败。
} else {
函数的其余部分和以前一样。
CannonField::paintEvent() is as before, except that this has been added:
if ( ects( targetRect() ) )
paintTarget( &p );
这两行确认在需要的时候目标也被绘制。
void CannonField::paintTarget( QPainter *p )
{
p->setBrush( red );
p->setPen( black );
p->drawRect( targetRect() );
}
这个私有函数绘制目标,一个由红色填充,有黑色边框的矩形。
QRect CannonField::targetRect() const
{
QRect r( 0, 0, 20, 10 );
nter( QPoint(target.x(),height() - 1 - target.y()) );
return r;
}
这个私有函数返回封装目标的矩形。从newTarget()中所得的target点使用0点在窗口部件的下边界的y。我们在调用QRect::moveCenter()之前在窗口坐标中计算这个点。
我们选择这个坐标映射的原因是在目标和窗口部件的下边界之间垂直距离。记住这些可以让用户或者程序在任何时候都可以重新定义窗口部件的大小。
t12/
在MyWidget类中没有新的成员了,但是我们稍微改变了一下构造函数来设置新的LCDRange的文本标签。
LCDRange *angle = new LCDRange( "ANGLE", this, "angle" );
我们设置角度的文本标签为“ANGLE”。
LCDRange *force = new LCDRange( "FORCE", this, "force" );
我们设置力量的文本标签为“FORCE”。
行为
加农炮会向目标射击,当它射中目标的时候,一个新的目标会自动被创建。
LCDRange窗口部件看起来有一点奇怪——QVBox中内置的管理给了标签太多的空间而其它的却不够。我们将会在下一章修正这一点。
(请看编译来学习如何创建一个makefile和连编应用程序。)
练习
创建一个作弊的按钮,当按下它的时候,让CannonField画出炮弹在五秒中的轨迹。
如果你在上一章做了“圆形炮弹”的练习,试着改变shotRect()为可以返回一个QRegion的shotRegion(),这样你就可以真正的做到准确碰撞检测。
做一个移动目标。
确认目标被完全创建在屏幕上。
确认加农炮窗口部件不能被重新定义大小,这样目标不是可见的。提示:QWidget::setMinimumSize()是你的朋友。
不容易的是在同一时刻让几个炮弹在空中成为可能。提示:建立一个炮弹对象。
现在你可以进行第十三章了。
Qt教程一 —— 第十三章:游戏结束
在这个例子中我们开始研究一个带有记分的真正可玩的游戏。我们给MyWidget一个新的名字(GameBoard)并添加一些槽。
我们把定义放在gamebrd.h并把实现放在。
CannonField现在有了一个游戏结束状态。
在LCDRange中的布局问题已经修好了。
•
•
•
•
•
•
•
t13/lcdrange.h包含LCDRange类定义。
t13/包含LCDRange类实现。
t13/cannon.h包含CannonField类定义。
t13/包含CannonField类实现。
t13/gamebrd.h包含GameBoard类定义。
t13/包含GameBoard类实现。
t13/包含MyWidget和main。
一行一行地解说
t13/lcdrange.h
#include
class QSlider;
class QLabel;
class LCDRange : public QWidget
我们继承了QWidget而不是QVBox。QVBox是非常容易使用的,但是它也显示了它的局域性,所以我们选择使用更加强大和稍微有一些难的QVBoxLayout。(和你记忆中的一样,QVBoxLayout不是一个窗口部件,它管理窗口部件。)
t13/
#include
我们现在需要包含qlayout.h来获得其它布局管理API。
LCDRange::LCDRange( QWidget *parent, const char *name )
: QWidget( parent, name )
我们使用一种平常的方式继承QWidget。
另外一个构造函数作了同样的改动。init()没有变化,除了我们在最后加了几行:
QVBoxLayout * l = new QVBoxLayout( this );
我们使用所有默认值创建一个QVBoxLayout,管理这个窗口部件的子窗口部件。
l->addWidget( lcd, 1 );
At the top we add the QLCDNumber with a non-zero stretch.
l->addWidget( slider );
l->addWidget( label );
然后我们添加另外两个,它们都使用默认的零伸展因数。
这个伸展控制是QVBoxLayout(和QHBoxLayout,和QGridLayout)所提供的,而像QVBox这样的类却不提供。在这种情况下我们让QLCDNumber可以伸展,而其它的不可以。
t13/cannon.h
CannonField现在有一个游戏结束状态和一些新的函数。
bool gameOver() const { return gameEnded; }
如果游戏结束了,这个函数返回TRUE,或者如果游戏还在继续,返回FALSE。
void setGameOver();
void restartGame();
这里是两个新槽:setGameOver()和restartGame()。
void canShoot( bool );
这个新的信号表明CannonField使shoot()槽生效的状态。我们将在下面使用它用来使Shoot按钮生效或失效。
bool gameEnded;
这个私有变量包含游戏的状态。TRUE说明游戏结束,FALSE说明游戏还将继续。
t13/
gameEnded = FALSE;
这一行已经被加入到构造函数中。最开始的时候,游戏没有结束(对于玩家是很幸运的 :-)。
void CannonField::shoot()
{
if ( isShooting() )
return;
timerCount = 0;
shoot_ang = ang;
shoot_f = f;
autoShootTimer->start( 50 );
emit canShoot( FALSE );
}
我们添加一个新的isShooting()函数,所以shoot()使用它替代直接的测试。同样,shoot告诉世界CannonField现在不可以射击。
void CannonField::setGameOver()
{
if ( gameEnded )
return;
if ( isShooting() )
autoShootTimer->stop();
gameEnded = TRUE;
repaint();
}
这个槽终止游戏。它必须被CannonField外面的调用,因为这个窗口部件不知道什么时候终止游戏。这是组件编程中一条重要设计原则。我们选择使组件可以尽可能灵活以适应不同的规则(比如,在一个首先射中十次的人胜利的多人游戏版本可能使用不变的CannonField)。
如果游戏已经被终止,我们立即返回。如果游戏会继续到我们的设计完成,设置游戏结束标志,并且重新绘制整个窗口部件。
void CannonField::restartGame()
{
if ( isShooting() )
autoShootTimer->stop();
gameEnded = FALSE;
repaint();
emit canShoot( TRUE );
}
这个槽开始一个新游戏。如果炮弹还在空中,我们停止设计。然后我们重置gameEnded变量并重新绘制窗口部件。
就像hit()或miss()一样,moveShot()同时也发射新的canShoot(TRUE)信号。
CannonField::paintEvent()的修改:
void CannonField::paintEvent( QPaintEvent *e )
{
QRect updateR = e->rect();
QPainter p( this );
if ( gameEnded ) {
( black );
t( QFont( "Courier", 48, QFont::Bold ) );
xt( rect(), AlignCenter, "Game Over" );
}
绘画事件已经通过如果游戏结束,比如gameEnded是TRUE,就显示文本“Game
Over”而被增强了。我们在这里不怕麻烦来检查更新矩形,是因为在游戏结束的时候速度不是关键性的。
为了画文本,我们先设置了黑色的画笔,当画文本的时候,画笔颜色会被用到。接下来我们选择Courier字体中的48号加粗字体。最后我们在窗口部件的矩形中央绘制文本。不幸的是,在一些系统中(特别是使用Unicode的X服务器)它会用一小段时间来载入如此大的字体。因为Qt缓存字体,我们只有第一次使用这个字体的时候才会注意到这一点。
if ( ects( cannonRect() ) )
paintCannon( &p );
if ( isShooting() && ects( shotRect() ) )
paintShot( &p );
if ( !gameEnded && ects( targetRect() ) )
paintTarget( &p );
}
我们只有在设计的时候画炮弹,在玩游戏的时候画目标(这也就是说,当游戏没有结束的时候)。
t13/gamebrd.h
这个文件是新的。它包含最后被用来作为MyWidget的GameBoard类的定义。
class QPushButton;
class LCDRange;
class QLCDNumber;
class CannonField;
#include "lcdrange.h"
#include "cannon.h"
class GameBoard : public QWidget
{
Q_OBJECT
public:
GameBoard( QWidget *parent=0, const char *name=0 );
protected slots:
void fire();
void hit();
void missed();
void newGame();
private:
QLCDNumber *hits;
QLCDNumber *shotsLeft;
CannonField *cannonField;
};
我们现在已经添加了四个槽。这些槽都是被保护的,只在内部使用。我们也已经加入了两个QLCDNumbers(hits和shotsLeft)用来显示游戏的状态。
t13/
这个文件是新的。它包含最后被用来作为MyWidget的GameBoard类的实现,
我们已经在GameBoard的构造函数中做了一些修改。
cannonField = new CannonField( this, "cannonField" );
cannonField现在是一个成员变量,所以我们在使用它的时候要小心地改变它的构造函数。(Trolltech的好程序员从来不会忘记这点,但是我就忘了。告诫程序员-如果“programmor”是拉丁语,至少。无论如何,返回代码。)
connect( cannonField, SIGNAL(hit()),
this, SLOT(hit()) );
connect( cannonField, SIGNAL(missed()),
this, SLOT(missed()) );
这次当炮弹射中或者射失目标的时候,我们想做些事情。所以我们把CannonField的hit()和missed()信号连接到这个类的两个被保护的同名槽。
connect( shoot, SIGNAL(clicked()), SLOT(fire()) );
以前我们直接把Shoot按钮的clicked()信号连接到CannonField的shoot()槽。这次我们想跟踪射击的次数,所以我们把它改为连接到这个类里面一个被保护的槽。
注意当你用独立的组件工作的时候,改变程序的行为是多么的容易。
connect( cannonField, SIGNAL(canShoot(bool)),
shoot, SLOT(setEnabled(bool)) );
我们也使用cannonField的canShoot()信号来适当地使Shoot按钮生效和失效。
QPushButton *restart
= new QPushButton( "&New Game", this, "newgame" );
restart->setFont( QFont( "Times", 18, QFont::Bold ) );
connect( restart, SIGNAL(clicked()), this, SLOT(newGame()) );
我们创建、设置并且连接这个New Game按钮就像我们对其它按钮所做的一样。点击这个按钮就会激活这个窗口部件的newGame()槽。
hits = new QLCDNumber( 2, this, "hits" );
shotsLeft = new QLCDNumber( 2, this, "shotsleft" );
QLabel *hitsL = new QLabel( "HITS", this, "hitsLabel" );
QLabel *shotsLeftL
= new QLabel( "SHOTS LEFT", this, "shotsleftLabel" );
我们创建了四个新的窗口部件。注意我们不怕麻烦的把QLabel窗口部件的指针保留到GameBoard类中是因为我们不想再对它们做什么了。当GameBoard窗口
部件被销毁的时候,Qt将会删除它们,并且布局类会适当地重新定义它们的大小。
QHBoxLayout *topBox = new QHBoxLayout;
grid->addLayout( topBox, 0, 1 );
topBox->addWidget( shoot );
topBox->addWidget( hits );
topBox->addWidget( hitsL );
topBox->addWidget( shotsLeft );
topBox->addWidget( shotsLeftL );
topBox->addStretch( 1 );
topBox->addWidget( restart );
右上单元格的窗口部件的数量正在变大。从前它是空的,现在它是完全充足的,我们把它们放到布局中来更好的看到它们。
注意我们让所有的窗口部件获得它们更喜欢的大小,改为在New Game按钮的左边加入了一个可以自由伸展的东西。
newGame();
}
我们已经做完了所有关于GameBoard的构造,所以我们使用newGame()来开始。(newGame()是一个槽,但是就像我们所说的,槽也可以像普通的函数一样使用。)
void GameBoard::fire()
{
if ( cannonField->gameOver() || cannonField->isShooting() )
return;
shotsLeft->display( shotsLeft->intValue() - 1 );
cannonField->shoot();
}
这个函数进行射击。如果游戏结束了或者还有一个炮弹在空中,我们立即返回。我们减少炮弹的数量并告诉加农炮进行射击。
void GameBoard::hit()
{
hits->display( hits->intValue() + 1 );
if ( shotsLeft->intValue() == 0 )
cannonField->setGameOver();
else
cannonField->newTarget();
}
当炮弹击中目标的时候这个槽被激活。我们增加射中的数量。如果没有炮弹了,游戏就结束了。否则,我们会让CannonField生成新的目标。
void GameBoard::missed()
{
if ( shotsLeft->intValue() == 0 )
cannonField->setGameOver();
}
当炮弹射失目标的时候这个槽被激活,如果没有炮弹了,游戏就结束了。
void GameBoard::newGame()
{
shotsLeft->display( 15 );
hits->display( 0 );
cannonField->restartGame();
cannonField->newTarget();
}
当用户点击Restart按钮的时候这个槽被激活。它也会被构造函数调用。首先它把炮弹的数量设置为15。注意这里是我们在程序中唯一设置炮弹数量的地方。把它改变为你所想要的游戏规则。接下来我们重置射中的数量,重新开始游戏,并且生成一个新的目标。
t13/
这个文件仅仅被删掉了一部分。MyWidget没了,并且唯一剩下的是main()函数,除了名称的改变其它都没有改变。
行为
射中的和剩余炮弹的数量被显示并且程序继续跟踪它们。游戏可以结束了,并且还有一个按钮可以开始一个新游戏。
(请看编译来学习如何创建一个makefile和连编应用程序。)
练习
添加一个随机的风的因素并把它显示给用户看。
当炮弹击中目标的时候做一些飞溅的效果。
实现多个目标。
现在你可以进行第十四章了。
Qt教程一 —— 第十四章:面对墙壁
这是最后的例子:一个完整的游戏。
我们添加键盘快捷键并引入鼠标事件到CannonField。我们在CannonField周围放一个框架并添加一个障碍物(墙)使这个游戏更富有挑战性。
•
•
•
•
•
•
•
t14/lcdrange.h包含LCDRange类定义。
t14/包含LCDRange类实现。
t14/cannon.h包含CannonField类定义。
t14/包含CannonField类实现。
t14/gamebrd.h包含GameBoard类定义。
t14/包含GameBoard类实现。
t14/包含MyWidget和main。
一行一行地解说
t14/cannon.h
CannonField现在可以接收鼠标事件,使得用户可以通过点击和拖拽炮筒来瞄准。CannonField也有一个障碍物的墙。
protected:
void paintEvent( QPaintEvent * );
void mousePressEvent( QMouseEvent * );
void mouseMoveEvent( QMouseEvent * );
void mouseReleaseEvent( QMouseEvent * );
除了常见的事件处理器,CannonField实现了三个鼠标事件处理器。名称说明了一切。
版权声明:本文标题:Qt入门教程 详细讲解版 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.freenas.com.cn/jishu/1705081840h472470.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论