使用 Qt 开发嵌入式 Linux
2001 年 1 月,我开始在 Trolltech 位于澳大利亚布里斯班的嵌入式开发部门实习。我的目标是双重的:学习 C++ 和学习如何开发嵌入式应用程序。我在 Trolltech 工作了五个星期,这段时间足够我实现这两个目标(当然,真正掌握 C++ 需要一生)。应该注意的是,在加入 Trolltech 之前,我的编程经验仅限于大学一年的 Java 学习,而且我从未使用过 C++ 或 Qt。在五个星期结束时,我已经开发了两个完整的 2D 游戏,这两个游戏现在都包含在 Qt Palmtop Environment (QPE) 中。
对于那些不了解的人来说,Qt/Embedded 是 Trolltech 的 Qt 的嵌入式版本,Qt 是一个跨平台的 C++ GUI 应用程序框架,支持 Windows、UNIX、Mac 和嵌入式 Linux。Qt 和 Qt/Embedded 通过优雅的双重许可程序提供给开发人员。对于开发免费软件的程序员,Qt 是免费提供的(在 GPL 和 QPL 下;Qt/Embedded 仅在 GPL 下提供)。对于开发商业应用程序的程序员,该框架在商业许可下提供。商业和开源版本均可直接从 Trolltech 获取,网址为 http://www.trolltech.com/。
我相信任何具备基本编程技能的人都可以学习使用 Qt,并快速开发出实用、外观专业的应用程序。我发现 API 非常直观,这大大缩短了学习时间。虽然 C++ 在语言特性方面可能令人眼花缭乱,但 Qt 使用的是一个适度的子集,因此您不必成为 C++ 专家也能使用它。
本文解释了用于开发一个简单游戏的技术。但在尝试这样做之前,您应该熟悉以下四个领域:Qt 教程、基本编程、面向对象编程和 Qt 参考文档。
Qt 教程的前几课将引导您完成开发一个简单的应用程序,例如“hello world”程序。它们涵盖了几个具体的技能,包括创建源文件和头文件,以及如何编译和运行 Qt 程序。
Qt 是一个用于 C++ 的应用程序框架,但我同时学习了框架和基本编程。如果您不懂 C++,我建议您使用一本好的教科书作为参考,特别是学习指针。
我在大学学习过 Java,发现面向对象编程技术是学习 Qt 的极佳基础。Qt 的设计目的是使面向对象编程更容易,因此熟悉多个类、对象和其他组件编程技术会有所帮助。
要使用 Qt,您需要确保已安装必要的软件并熟悉 Qt 参考文档。C++ 编译器是必要的,调试器也非常有用,这两者都包含在所有 Linux 发行版中。文本编辑器,例如 vi 或 Emacs,也是创建源文件所必需的。最重要的是,您需要安装用于 X11 和嵌入式 Linux 的 Qt。
我用来创建 Qt/Embedded 应用程序的工具随大多数 Linux 发行版一起提供,包括 GCC 和 GDB。我还使用了 TMake,这是 Trolltech 提供的一个简单工具,用于创建和维护 makefile;Qt 文档(可在 http://doc.trolltech.com/ 获取);以及 Qt 教科书,Kalle Dalheimer 的 Programming with Qt。您应该查看 Qt 文档中的功能,包括关于 Qt 如何使创建简单应用程序变得非常容易的解释。它的 API 包含几个旨在使常见任务更容易和更快的类。
特别是,对于创建 2D 游戏,您应该查看三个主要功能以入门:QMainWindow,它提供了一个典型的应用程序窗口,带有工具栏或菜单栏和状态栏;QCanvas,一个 2D 图形区域,可以在其上放置 QCanvasItems(图形对象);以及 QCanvasView,它提供了画布的视图。查看 Qt 文档是一个很好的起点,因为它会让您了解 Qt 提供的丰富功能。此外,许多您想做的事情都已经完成。
本文的下一部分重点介绍了 Qt 的功能,这些功能使创建简单游戏变得容易,使用了我编写的 Snake 游戏中的示例代码(见图 1)。这里解释的原理可以应用于创建许多其他 2D 游戏。

MainWindow
您首先需要的是一个主类。这应该位于名为 main.cpp 的文件中。它将仅包含一个 main 方法,用于创建程序的实例。Snake 中的 main 方法如下所示。
int main(int argc, char **argv) { QPEApplication app(argc,argv); SnakeGame* m = new SnakeGame; // creates an instance of Snake m->resize(m->sizeHint()); qApp->setMainWidget(m); m->show(); return app.exec(); }
如果您已经编写过 Qt 程序,您会发现其中一些代码很熟悉。这是启动程序的标准方法。
现在您需要一个类来设置程序。它的构造函数将创建一个程序的实例,如上面出现的主方法中所用。它将继承自 QMainWindow,并将创建菜单或工具栏,并可能创建一个状态栏。它还需要创建一个 QCanvas 和一个 QCanvasView。所有这些都显示在列表 1 中。
列表 1. 创建 QCanvas 和 QCanvasView
此类还将包含程序中要使用的一些其他方法。在 Snake 中,此类包含显示标题屏幕、开始新游戏、更新分数、更改级别、清除屏幕、结束游戏和处理键盘交互(按键事件)的方法。这些方法将特定于您的程序,因此我没有在此处包含代码。
现在您有了一个 QCanvas,您可以开始在其上放置 QCanvasItems。QCanvasItems 有许多类型,要创建一个图形项目,我们将使用 QCanvasSprite。有关其他类型的 QCanvasItems 的信息,请参阅 Qt 文档。在 Snake 游戏中,目标鼠标是一个从图像创建的 QCanvasSprite。鼠标在其自己的类中定义,并继承自 QCanvasSprite。以下代码显示了名为 Target 的类中鼠标的构造函数
Target::Target(QCanvas* canvas) : QCanvasSprite(0, canvas) //inherits from QCanvasSprite { mouse = new QCanvasPixmapArray setSequence(mouse); newTarget(); }
此代码首先将图像加载到名为“mouse”的像素图数组中。如果您有多个图像并希望为精灵制作动画,则可以使用 readPixmaps() 函数将所有图像加载到数组中。然后,您可以使用 QCanvasSprite 中的 setSequence() 函数告诉您的精灵使用刚刚加载到数组中的图像。此构造函数还调用一个函数 newTarget()。这只是生成可以放置新目标的随机整数 x 和 y 值。然后,它调用 QCanvasSprite 中的 move (int x, int y) 函数来设置其位置,并调用 show() 使其可见。墙壁也以相同的方式创建。
现在您可以使您的程序具有交互性。Qt 类 keyEvent 收集有关按键事件的信息。事件处理程序 keyPressEvent 和 keyReleaseEvent 接收此信息。要使用这些,您需要在程序中将它们实现为函数。keyPressEvent 的典型实现是使用 switch 语句在用户按下某些键时调用函数。
在游戏中,当按下箭头键之一时,蛇会改变方向。这是通过一个 switch 语句实现的,该语句调用一个 move() 函数,并将按下的方向作为其参数。以下是 keyPressEvent() 函数的实现。它非常简单,因为它调用了 Snake 类中的一个函数,该函数记住最后按下的键并调用另一个函数 moveSnake() 来确定蛇应该朝哪个方向移动。要实现这一点,请将以下行添加到 interface.cpp 文件中
void SnakeGame::keyPressEvent(QKeyEvent* event) { int newkey = event->key(); snake->go(newkey) }
添加到 snake.cpp 文件中
void Snake::moveSnake() { switch (last) { case Key_Up: move(up); break; case Key_Left: move(left); break; case Key_Right: move(right); break; case Key_Down: move(down); break; } detectCrash(); }如果用户按下向上键,程序将调用 move(up)。move 函数执行,蛇朝按下的键的方向移动:在本例中为向上。move() 函数中的代码将特定于创建的特定游戏。
蛇的实际移动相当复杂,因为蛇的身体是由小图像集合组成的。要移动,将末端图像放在前面,然后移动头部和尾部。在其他情况下,例如按下键发射子弹,可以使用 Qt 中的 setVelocity() 函数设置子弹将以恒定速度移动。
在这个阶段,您有一个 QMainWindow,其中包含一个 QCanvasView,该视图具有一个画布,其中包含项目,其中一些项目可能会移动。下一步是通过检测项目之间的碰撞来使您的程序更有趣。您可以使用 collidesWith() 和 collisions() 获取有关 QCanvasItems 之间碰撞的信息。collidesWith() 用于测试一个项目是否会与另一个特定项目发生碰撞,而 collisions() 返回与特定项目发生碰撞的所有项目的列表。在 Snake 游戏中,使用了 collisions(),因为蛇可能会与自身、墙壁或目标发生碰撞。列表 2 显示了游戏中如何处理碰撞。
蛇头碰撞的 QCanvasItems 列表被分配给一个名为“l”的 QCanvasItemList。collisions() 的 false 参数指定以非精确模式测试碰撞。这将返回靠近头部的项目,但它的工作速度比精确模式快得多。如果该项目是一个有用的候选项,则使用 collidesWith() 对其进行测试。
“for”循环用于迭代列表 l,将每个项目分配给一个名为“item”的变量。画布上的每个 QCanvasItem 都可以有一个指定的 rtti() 值,这是一个整数,允许您识别项目。目标的 rtti() 值为 1,500,此信息用于 if 语句中以测试列表中的项目是否为目标。如果是目标,则函数返回;否则,它继续执行下一个 if 语句以测试项目是否为墙壁。请注意,此代码段仅用于说明目的。在实际的 Snake 游戏中,for 循环中还有其他 if 语句来处理其他情况。“do something”被替换为代码。
开发任何程序时,另一个重要的考虑因素是设计。Qt 通过其信号和槽机制为组件编程(也称为面向对象编程)提供帮助。这是一个很棒的功能,可以节省大量时间和精力,因为它允许程序中不同对象之间轻松通信。它允许一个对象发出 signal(),激活另一个对象中的 slot()。槽可以是普通的成员函数,参数也可以传递给它们。
我发现这个功能在开发 Snake 时特别有用。信号和槽的使用示例发生在游戏结束时。当蛇撞到墙壁时,蛇对象发出 dead() 信号。然后,蛇的身体或屏幕边缘连接到界面类中的 gameOver() 槽,以结束游戏并开始新游戏。此代码在 snake.cpp 中
//check if snake hit itself for (uint i = 3; i < snakelist.count(); i++) { if (head->collidesWith(snakelist.at(i)) ) { emit dead(); // signal to end game autoMoveTimer->stop(); return; } }
文件 interface.cpp 中 newGame() 函数中的代码为
// connect the signal to the slot connect(snake, SIGNAL(dead()), this, SLOT(gameOver()) );
如果应用程序框架的测试标准是快速构建专业、强大且无错误代码的能力,那么 Qt/Embedded 完全合格。作为一名编程经验相当有限的实习生,我能够创建一个 2D 电脑游戏,其质量足以包含在 Qt Palmtop Environment 的应用程序套件中。我只用了不到五个星期的实际开发时间就完成了。Qt/Embedded 的标语是“Small Enough for the Job”。我认为这非常准确。它功能强大,具有丰富的功能集,使编程变得非常容易,并且它占用的主内存和闪存空间惊人地小。如果我职业生涯中遇到的其他工具能有它一半好,我就会成为一个快乐的开发人员。

Natalie Watson (nwatson04@optusnet.com.au) 目前是格里菲斯大学的学生,她正在攻读信息技术学士学位。她的编程经验包括 Java 和 C++。