Qt Quick实现的疯狂算数游戏

使用 Qt Quick 写了个小游戏:疯狂算数。支持 Windows 和 Android 两个平台。

游戏简单,但牵涉到下面你的 Qt Quick 主题:

  • 自己实现一个按钮
  • 自适应分辨率
  • 国际化
  • QML与C++混合编程
  • APK图标设置
  • APK名称汉化
  • 动画

其实所有这些内容,在我的书《Qt Quick核心编程》里都讲到了,感兴趣的朋友可以看我的书。

大概来看一下吧,先看效果。

Android 手机运行效果

下面是 Android 应用列表:

Qt Quick实现的疯狂算数游戏

看到“疯狂算数”那个应用了吧,图标是我自己画的,名字是中文的。

再来看游戏进行中的效果:

Qt Quick实现的疯狂算数游戏

界面中间,第一行是倒计时,数秒的。第二行是算术题。第三行是两个按钮,选择对错;判断正确的话,继续下一题,如果选错了,游戏就结束了,可以看到下面的图。

Qt Quick实现的疯狂算数游戏

游戏结束时显示当前答对的题数、历史最好成绩。界面下方是两个按钮,点“再来”可以重玩,点“退出”就结束整个游戏。游戏结束的界面,使用了弹簧动画(SprintgAnimation),有一些动画效果。

源码分析

源码我们走马观花,摘重要的讲一下。

国际化

这个简单的示例里,只有 qml 文档中有需要翻译的字符串。在 pro 文件里有一些改动:

  1. TRANSLATIONS = madmath_zh_cn.ts
  2. lupdate_only {
  3. SOURCES = main.qml
  4. }

使用 Qt 的命令行开发环境,切换到项目目录,执行 lupdate MadMath.pro 即可生成 ts 文件,然后使用 Linguist 翻译、发布,再把 qm 文件添加到 qrc 里,最后在 main.cpp 中根据用户语言环境加载 qm 文件。

main.cpp 代码如下:

  1. #include <QGuiApplication>
  2. #include <QQmlApplicationEngine>
  3. #include <QFont>
  4. #include <QQmlContext>
  5. #include <QIcon>
  6. #include <QLocale>
  7. #include <QTranslator>
  8. #include "sizeUtil.h"
  9. #include "problem.h"
  10. int main(int argc, char *argv[])
  11. {
  12. QGuiApplication app(argc, argv);
  13. QFont f = app.font();
  14. f.setPointSize(24);
  15. app.setWindowIcon(QIcon(":/res/madmath_36.png"));
  16. QLocale locale = QLocale::system();
  17. if(locale.language() == QLocale::Chinese)
  18. {
  19. QTranslator *translator = new QTranslator(&app);
  20. if(translator->load(":/madmath_zh_cn.qm"))
  21. {
  22. app.installTranslator(translator);
  23. }
  24. }
  25. QQmlApplicationEngine engine;
  26. engine.rootContext()->setContextProperty("sizeUtil", new SizeUtil);
  27. engine.rootContext()->setContextProperty("problems", new MathProblem);
  28. engine.load(QUrl(QStringLiteral("qrc:///main.qml")));
  29. return app.exec();
  30. }

ImageButton

实现了一个简单的图片按钮—— ImageButton ,在 ImageButton.qml 文件内。所有源码:

  1. import QtQuick 2.0
  2. Rectangle {
  3. id: btn;
  4. property alias normalImage: normal.source;
  5. property alias pressedImage: pressed.source;
  6. signal clicked();
  7. Image {
  8. id: normal;
  9. anchors.fill: parent;
  10. }
  11. Image {
  12. id: pressed;
  13. anchors.fill: parent;
  14. visible: false;
  15. }
  16. implicitWidth: 64;
  17. implicitHeight: 48;
  18. MouseArea {
  19. anchors.fill: parent;
  20. onPressed: {
  21. pressed.visible = true;
  22. normal.visible = false;
  23. }
  24. onReleased: {
  25. pressed.visible = false;
  26. normal.visible = true;
  27. btn.clicked();
  28. }
  29. }
  30. }

ImageButton 有两个状态:正常和按下。两个状态各自有一个图片。鼠标事件里切换了两个图片。

还定义了一个 clicked() 信号。暴露了属性别名 normalImage 和 pressedImage 用来设置按钮需要的图片。

ImageButton 用起来也很简单,下面是 main.qml 中的使用示例:

  1. ImageButton {
  2. id: wrong;
  3. anchors.right: parent.horizontalCenter;
  4. anchors.rightMargin: 12;
  5. anchors.top: problem.bottom;
  6. anchors.topMargin: 20;
  7. normalImage: Qt.resolvedUrl("res/wrong_normal.png");
  8. pressedImage: Qt.resolvedUrl("res/wrong_selected.png");
  9. width: root.dpiFactor * 64;
  10. height: root.dpiFactor * 48;
  11. onClicked: root.check(false);
  12. }

QML与C++混合编程

算术题目的生成和结果判断,我放在了 C++ 中,在 MathProblem 里实现。另外还有 DPI 的一些信息,也在 C++ 中,在 SizeUtil 中实现。

有关 QML 与 C++ 混合编程的细节,请看我的博客或者我的书——《Qt Quick核心编程》,这里就不再细说了。我们只看一下题目是如何出的,部分源码:

  1. QString MathProblem::next()
  2. {
  3. ++m_index;
  4. if(m_index == sizeof(g_answers)/sizeof(g_answers[0]))
  5. {
  6. m_index = 0;
  7. }
  8. int var = qrand() % 2;
  9. if(var && (qrand() % 2)) var = -var;
  10. m_currentAnswer = g_answers[m_index] + qrand() % 2;
  11. m_currentRight = (g_answers[m_index] == m_currentAnswer);
  12. return QString("%1%2").arg(g_problems[m_index]).arg(m_currentAnswer);
  13. }
  14. bool MathProblem::test(bool right)
  15. {
  16. return right == m_currentRight;
  17. }

next() 方法生成一道算术题。 MathProblem 维护了一个索引,指向全局的问题数组和答案数组。 next() 递增 m_index ,答案用随机数混淆一下,然后判断混淆后的结果是否与正确答案一致。题目的结果保留在 m_currentRight 这个布尔变量里。

test() 用来测试用户的选择与实际结果是否一致。

题目在全局数组 g_problems 中,答案在全局数组 g_answers 中。

动画

当用户答错题时,会从应用顶部弹出一个提示界面。我使用了 SpringAnimation 为这个界面加入了一些动画效果。

gameOverUI 的代码如下:

  1. Rectangle {
  2. id: gameOverUI;
  3. border.width: 2;
  4. border.color: "white";
  5. color: "lightsteelblue";
  6. width: root.width * 0.75;
  7. height: root.height * 0.75;
  8. x: root.width * 0.125;
  9. y: -height-1;
  10. visible: false;
  11. Text {
  12. id: overTitle;
  13. anchors.top: parent.top;
  14. anchors.topMargin: sizeUtil.defaultFontHeight();
  15. anchors.horizontalCenter: parent.horizontalCenter;
  16. font.pointSize: 30;
  17. text: qsTr("Game Over");
  18. color: "red";
  19. }
  20. Text {
  21. anchors.bottom: parent.verticalCenter;
  22. anchors.bottomMargin: 10;
  23. anchors.right: parent.horizontalCenter;
  24. anchors.rightMargin: 8;
  25. text: qsTr("New:");
  26. horizontalAlignment: Text.AlignRight;
  27. color: "black";
  28. }
  29. Text {
  30. id: current;
  31. anchors.bottom: parent.verticalCenter;
  32. anchors.bottomMargin: 10;
  33. anchors.left: parent.horizontalCenter;
  34. anchors.leftMargin: 8;
  35. horizontalAlignment: Text.AlignLeft;
  36. color: "blue";
  37. font.bold: true;
  38. }
  39. Text {
  40. anchors.top: current.bottom;
  41. anchors.topMargin: 20;
  42. anchors.right: parent.horizontalCenter;
  43. anchors.rightMargin: 8;
  44. text: qsTr("Best:");
  45. horizontalAlignment: Text.AlignRight;
  46. color: "black";
  47. }
  48. Text {
  49. id: best;
  50. anchors.top: current.bottom;
  51. anchors.topMargin: 20;
  52. anchors.left: parent.horizontalCenter;
  53. anchors.leftMargin: 8;
  54. horizontalAlignment: Text.AlignLeft;
  55. color: "blue";
  56. font.bold: true;
  57. }
  58. Button {
  59. anchors.bottom: parent.bottom;
  60. anchors.bottomMargin: 40;
  61. anchors.right: parent.horizontalCenter;
  62. anchors.rightMargin: 16;
  63. text: qsTr("Restart");
  64. onClicked: {
  65. gameOverUI.dismiss();
  66. root.start();
  67. }
  68. }
  69. Button {
  70. anchors.bottom: parent.bottom;
  71. anchors.bottomMargin: 40;
  72. anchors.left: parent.horizontalCenter;
  73. anchors.leftMargin: 16;
  74. text: qsTr("Quit");
  75. onClicked: Qt.quit();
  76. }
  77. SpringAnimation {
  78. id: overAnimation;
  79. target: gameOverUI;
  80. from: -height - 1;
  81. to: root.height * 0.125;
  82. spring: 2;
  83. damping: 0.2;
  84. duration: 1000;
  85. property: "y";
  86. onStarted: {
  87. gameOverUI.visible = true;
  88. }
  89. }
  90. function dismiss() {
  91. y = -height - 1;
  92. visible = false;
  93. }
  94. function fire(currentRecord, bestRecord) {
  95. current.text = currentRecord;
  96. best.text = bestRecord;
  97. overAnimation.start();
  98. }
  99. }

SpringAnimation 的 target 属性指向 gameOverUI ,操作 gameOverUI 的 y 属性。一开始 gameOverUI 是不可见的,当动作开始时设置为可见(在 onStarted 信号处理中)。

fire() 方法会在游戏进行中被调用,它启动动画,讲游戏结果赋值给提示界面里的 Text 元素。

SpringAnimation 的具体用法,参考 Qt 帮助,或者《Qt Quick核心编程》一书,它对 Qt Quick 里的动画类库作了非常详尽的介绍。

APK 设置

给 Android 版本创建一个 AndroidManifest.xml ,在项目视图里双击就可以打开图形化编辑界面,可以选择你设计的图标。参考《Qt on Android核心编程》一书,或者“Qt on Android:图文详解Hello World全过程”。

自适应屏幕分辨率

我在“Qt on Android:创建可伸缩界面”一文中讲了 Qt on Android 如何适应 Android 手机多变的分辨率。这里就不再细说了。只看一下 QML 里如何根据 DPI  来设置图片按钮的大小,代码:

  1. ImageButton {
  2. id: right;
  3. anchors.left: parent.horizontalCenter;
  4. anchors.leftMargin: 12;
  5. anchors.top: problem.bottom;
  6. anchors.topMargin: 20;
  7. normalImage: Qt.resolvedUrl("res/right_normal.png");
  8. pressedImage: Qt.resolvedUrl("res/right_selected.png");
  9. width: root.dpiFactor * 64;
  10. height: root.dpiFactor * 48;
  11. onClicked: root.check(true);
  12. }

如你所见,我设置 right 按钮的 width 为 root.dpiFactor * 64 。 64 是图片的宽度,单位是像素。 dpiFactor 来自 SizeUtil :

  1. qreal SizeUtil::dpiFactor()
  2. {
  3. QScreen *screen = qApp->primaryScreen();
  4. return screen->logicalDotsPerInch() / 72;
  5. }

好啦,到此结束了。完整的项目代码下载:点击下载

--------

回顾一下我的Qt Quick系列文章:

上一篇:Qt 学习之路 :Qt Quick Controls


下一篇:MVC学习系列1--什么是MVC