Archive:Development/Tutorials/Qt4 Ruby Tutorial/Chapter 14 (zh CN)

From KDE TechBase
The printable version is no longer supported and may have rendering errors. Please update your browser bookmarks and please use the default browser print function instead.

Template:I18n/Language Navigation Bar (zh CN) Template:TutorialBrowser (zh CN)

Facing the Wall

档案:

概览

这是最后的范例:一个完整的游戏。

我们加上键盘快捷键(accelerator)和引入鼠标事件到 CannonField。我们放置了一个环绕 CannonField 的外框,并加上一个障碍(墙),使游戏更具挑战性。

一行一行的浏览

cannon.rb

CannonField 现在可以接收鼠标事件,让用户透过按下并拖动炮管来瞄准。CannonField 也有障碍墙了。

@barrelPressed = false

这行加入到建构子。最初,鼠标没有在炮管上按下。

elsif shotR.x() > width() || shotR.y() > height() ||
    shotR.intersects(barrierRect())

现在,我们有一个障碍,也就是第三种不击中的方法。我们也测试第三种。(在 moveShot()。)

def mousePressEvent(event)
  unless event.button() == Qt::LeftButton
    return
  end

  if barrelHit(event.pos())
    @barrelPressed = true
  end
end

这是一个 Qt 事件处理器。当光标在 widget 上,且用户按下鼠标按钮时,它会被呼叫。

如果事件不是鼠标左键产生的,我们立即返回。否则,我们检查光标的位置是否在加农炮的炮管内。假如是,我们就设定 barrelPressed 为 true。

请注意,Qt::MouseEvent::pos() 函式会返回一个 Widget 坐标系统中的点。

def mouseMoveEvent(event)
  unless @barrelPressed
    return
  end

  pos = event.pos();

  if pos.x() <= 0
    pos.setX(1)
  end

  if pos.y() >= height()
    pos.setY(height() - 1)
  end

  rad = atan2((rect().bottom() - pos.y()), pos.x())
  setAngle((rad * 180 / 3.14159265).round())
end

这是另一个 Qt 事件处理器。当用户已经在这个 widget 内按下鼠标按钮,然后移动/拖动鼠标时被呼叫。(即使没有任何按键被按下,您也可以让 Qt 发送鼠标移动事件。参阅 Qt::Widget::setMouseTracking()。)

这个处理器会根据光标的位置,重新定位炮管。

首先,如果炮管没有被按下,我们返回。下一步,我们取得光标的位置。如果光标移到 widget 的左边或底部,我们调整这个点到 widget 的内部。

然后,我们计算 widget 底边,与 widget 左下角和光标位置构成假想线之间的角度。最后,我们设定加农炮的角度为换算成度的新值。

记得用 setAngle() 重绘加农炮。

def mouseReleaseEvent(event)
  if event.button() == Qt::LeftButton
    @barrelPressed = false
  end
end

无论用户何时释放鼠标按钮,且它是在这个 widget 里面按下时,这个 Qt 事件处理器被呼叫。

如果左边的按钮被释放,我们可以确定炮管已不再被按住了。

绘图事件还多了一行:

paintBarrier(painter)

paintBarrier()paintShot()paintTarget(),和 paintCannon() 作同类的事情。

def paintBarrier( painter )
  painter.setBrush(Qt::Brush.new(Qt::yellow))
  painter.setPen(Qt::Color.new(Qt::black))
  painter.drawRect(barrierRect())
end

这个函式绘制一个填充黄色和黑色外框的矩形作为障碍。

def barrierRect()
  return Qt::Rect.new(145, height() - 100, 15, 99)
end

这个函式返回障碍的矩形。我们把障碍的底边固定在 widget 的底边。

def barrelHit(pos)
  matrix = Qt::Matrix.new()
  matrix.translate(0, height())
  matrix.rotate(-@currentAngle)
  matrix = matrix.inverted()
  return @barrelRect.contains(matrix.map(pos))
end

如果点在炮管里,这个函式返回 true;否则返回 false

这里我们使用 Qt::Matrix 类别。Qt::Matrix 定义了一个坐标系统映像。它可以执行与 Qt::Painter 相同的转换。

这里,我们执行与 paintCannon() 函式中绘制炮管的同样转换步骤。首先,我们转移坐标系,然后旋转。

现在我们需要检查点 pos(在 widget 坐标)是否位于炮管内。为了做到这一点,我们倒转这个转移矩阵。倒转矩阵执行了我们使用在绘制炮管的倒转转移。我们使用倒转矩阵来映射点 pos。如果它在原来炮管的矩形里面,返回 true

gamebrd.rb

cannonBox = Qt::Frame.new()
cannonBox.setFrameStyle(Qt::Frame::WinPanel | Qt::Frame::Sunken)

我们建立及设定了一个 Qt::Frame,并设定框架的风格。这样的结果是一个围绕 CannonField 的3D框架。

Qt::Shortcut.new(Qt::KeySequence.new(Qt::Key_Enter.to_i),
                 self, SLOT('fire()'))
Qt::Shortcut.new(Qt::KeySequence.new(Qt::Key_Return.to_i),
                 self, SLOT('fire()'))
Qt::Shortcut.new(Qt::KeySequence.new(Qt::CTRL.to_i + Qt::Key_Q.to_i),
                 self, SLOT('close()'))

这里,我们建立并设定三个 Qt::Shortcut 物件。这些对象会撷取(intercept)键盘事件到 widget,并在某些键被按下时呼叫槽。请注意,Qt::Shortcut 物件是 widget 的子 widget。且在 widget 毁灭时,也会被毁灭。Qt::Shortcut 本身不是一个 widget,在 widget 中也没有可见的影响。

我们定义了三个快捷键。我们希望当使用者按下 Enter 或 Return 时,呼叫 fire() 槽。我们也希望当按下 Ctrl+Q 键时,应用程序退出。这次我们连接到 Qt::Widget::close(),而不是 Qt::CoreApplication::quit()。由于 GameBoard 是应用程序主要的 widget,这样做与 QCoreApplication::quit() 有同样的效果。

Qt::CTRL、Qt::Key_Enter、Qt::Key_Return,和 Qt::Key_Q 都是宣告在 Qt 名称空间里的常数。不幸的是,在 qtruby 的当前版本,他们需要转换为整数,才能使用它们在我们的快捷键。

leftLayout = Qt::VBoxLayout.new()
leftLayout.addWidget(angle)
leftLayout.addWidget(force)

gridLayout = Qt::GridLayout.new()
gridLayout.addWidget(quit, 0, 0)
gridLayout.addLayout(topLayout, 0, 1)
gridLayout.addLayout(leftLayout, 1, 0)
gridLayout.addWidget(@cannonField, 1, 1, 2, 1)
gridLayout.setColumnStretch(1, 10)
setLayout(gridLayout)

我们给 cannonBox 它自己的 Qt::VBoxLayout,并且加入 CannonField 到布局中。这间接使得 CannonField 成为 cannonBox 的一个子 widget。因为没有其它东西在这个盒子里,结果就是 Qt::VBoxLayout 会放置一个环绕着 CannonField 的框架。我们放置 cannonBox,而不是 CannonField,到网格布局中。

执行应用程序

现在加农炮会在您按下 Enter 时射击。您还可以使用鼠标定位加农炮的角度。障碍使得游戏变得更具挑战性。我们也有一个好看的外框围绕CannonField

练习

写一个太空入侵者游戏。

新的练习是:写一个打砖块游戏。

最后的忠告:继续往前走,并创造程序设计艺术的杰作!