Archive:Development/Tutorials/Qt4 Ruby Tutorial/Chapter 12 (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)

Hanging in the Air the Way Bricks Don't

档案:

概览

在这个范例中,我们扩充我们的 LCDRange 类别来包含一个文字卷标。我们还提供一些用来射击的目标。

一行一行的浏览

lcdrange.rb

def initialize(s, parent = nil)
  super(parent)
  init()
  setText(s)
end

这个建构子先呼叫 init(),然后设定卷标文字。init() 是一个执行大部分初始化的独立函式,因为在原来的C++版本中是函式重载。

def init()
  lcd = Qt::LCDNumber.new(2)
  lcd.setSegmentStyle(Qt::LCDNumber::Filled)

  @slider = Qt::Slider.new(Qt::Horizontal)
  @slider.setRange(0, 99)
  @slider.setValue(0)

  @label = Qt::Label.new()
  @label.setAlignment(Qt::AlignHCenter.to_i | Qt::AlignTop.to_i)

  connect(@slider, SIGNAL('valueChanged(int)'),
  lcd, SLOT('display(int)'))
  connect(@slider, SIGNAL('valueChanged(int)'),
  self, SIGNAL('valueChanged(int)'))

  layout = Qt::VBoxLayout.new()
  layout.addWidget(lcd)
  layout.addWidget(@slider)
  layout.addWidget(@label)
  setLayout(layout)

  setFocusProxy(@slider)
end

lcd 的结构和前面章节的 slider 是一样的。接下来,我们建立一个 Qt::Label,并告诉它以水平置中和与垂直向上的方式对齐内容。Qt::Object::connect() 呼叫也取自前面章节。

def setText(s)
  @label.setText(s)
end

这个函式设定卷标文字。

cannon.rb

CannonField 现在有两个新的讯号:hit()missed()。此外,它还多了一个目标。

signals 'hit()', 'missed()' #...

一旦炮弹击中了目标,hit() 讯号就会被发出。而当炮弹越过了 widget 右侧或底部的边缘,missed() 讯号就会被发出(换言之,可以肯定的是它不会命中目标)。

newTarget()

这行已经加入到建构子中。它为目标建立了一个「随机」的位置。事实上,newTarget() 函式将尝试画出目标。因为我们是在建构子中,CannonField widget 是不可见的。Qt 保证在一个隐藏的 widget 中呼叫Qt::Widget::update() 是无害。

@@first_time = true

def newTarget()
  if @@first_time
    @@first_time = false
    midnight = Qt::Time.new(0, 0, 0)
    srand(midnight.secsTo(Qt::Time.currentTime()))
  end

  @target = Qt::Point.new(200 + rand(190), 10 + rand(255))
  update()
end

这个函式会在新的随机位置建立一个目标中心点。

我们建立了 Qt::Time 对象 midnight,代表时间00:00:00。接下来,我们取出从午夜到目前为止经过的秒数,并使用它作为随机数子(random seed)。请参阅文件 Qt::DateQt::Time 和Qt::DateTime 以取得更多信息。

最后,我们计算出目标的中心点。我们保持它在 widget 的底边为 y 值0,让 y 值向上增加,x 是正常的左侧边缘为0,以及右侧增加x值的坐标系统中的矩形(x=200,y=35,宽=190,高=255。换句话说,可能的 x 和 y 值分别是200至389和35至289)。

通过实验我们发现这样炮弹都可以击中。

def moveShot()
  region = Qt::Region.new(shotRect())
  @timerCount += 1

  shotR = shotRect()

来自前面章节的这部分定时器事件并没有改变。

if shotR.intersects(targetRect()) 
  @autoShootTimer.stop()
  emit hit()

这个 if 叙述检查炮弹矩形是否与目标矩形相交。如果是这样,炮弹会击中了目标(哎哟!)。我们停止射击定时器,并发出 hit() 讯号来告诉外界目标被摧毁,并且返回。请注意,我们可以在这个点上建立一个新的目标,但是因为 CannonField 是一个组件,我们把这样的决定留给组件的用户。

elsif shotR.x() > width() || shotR.y() > height()
  @autoShootTimer.stop()
  emit missed()

这与前面章节是一样的,但它现在发出 missed() 讯号来告诉外界射击失败。

  else
    region = region.unite(Qt::Region.new(shotR))
  end
    
  update(region)
end

而函式的剩下部分与之前一样。

CannonField::paintEvent() 和之前相同,除了加入这个:

    paintTarget(painter)

这行可以确保在必要时目标也会画出。

def paintTarget(painter)
  painter.setBrush(Qt::Brush.new(Qt::red))
  painter.setPen(Qt::Pen.new(Qt::Color.new(Qt::black)))
  painter.drawRect(targetRect())
end

这个函式绘制目标;一个填充了红色和黑色外框的长方形。

def targetRect()
  result = Qt::Rect.new(0, 0, 20, 10)
  result.moveCenter(Qt::Point.new(@target.x(), height() - 1 - @target.y()))
  return result
end

这个私有函式返回封装目标的矩形。还记得取自 newTarget()target 点使用 widget 的底部为 y 坐标0。,我们在呼叫 Qt::Rect::moveCenter() 之前,在 widget 坐标中计算这个点。

我们之所以选择这个坐标映像(coordinate mapping)是为了要固定目标和 widget 底部之间的距离。请记住,用户或程序可以在任何时候改变 widget 大小。

t12.rb

MyWidget 类别中没有新的成员。但我们现在稍微修改建构子来设定新的 LCDRange 文字卷标。

    angle = LCDRange.new(tr('ANGLE'))

我们设定了角度文字卷标为「ANGLE」。

    force  = LCDRange.new(tr('FORCE'))

我们设定了力量文字卷标为「FORCE」。

执行应用程序

LCDRange widgets 看起来有点怪:当 MyWidget 大小改变时,Qt::VBoxLayout 内建的布局管理给标签太多的空间了,其它的就不够用;使得两个 LCDRange widget 之间的空间改变大小。我们将在下一章修正。

练习

做一个作弊按钮。当按下时,使 CannonField 显示5秒的射击轨迹。

如果你做了前一章的「圆形炮弹」练习,请尝试把 shotRect() 更改为返回Qt::RegionshotRegion(),这样你就可以有相当精确的碰撞检测。

做一个移动的目标。

请确保目标建立时,总是完整的在屏幕上。

请确保这个 widget 不能重设大小,以免目标变成不可见的。[提示:Qt::Widget::setMinimumSize() 是你的好朋友。]

这不太容易。同一时间有多个炮弹在空中。[提示:建立 Shot 类别。]