経過分秒表示(続)

前に同じような事もあった(time.rb 経過分秒表示 - Rubyとか Illustratorとか SFとか折紙とか)のだが、やはりライトニングトーク絡みで時間表示を作ってみた(申し込んだLT自体は reject)。取敢えずソース

LT_timer.rb (このソースはその後更新されています、11/1/2)

#coding:Windows-31J
# $Id: LT_timer.rb 3582 2010-07-04 09:32:14Z hs9587 $
# $Date: 2010-07-04 18:32:14 +0900 (譌・, 04 7 2010) $
# $Name$ $Author: hs9587 $ $Revision: 3582 $
Version = "0.0 ($Revision: 3582 $)"
require 'vr/vruby'
require 'vr/vrcontrol'
require 'vr/vrtimer'
require 'vr/vrlayout'
require 'time'
Min = Time.parse('00:01')-Time.parse('00:00')

class TimerControl < VRPanel
  include VRTimerFeasible
  include VRDrawable
  SrartResetStruct= Struct.new :name, :button, :body
  StartReset = [
    SrartResetStruct.new('start', '!' \
      , lambda{@start_time = Time.now; def anchor_time; @start_time; end}),
    SrartResetStruct.new('reset', '0' \
      , lambda{def anchor_time; Time.now; end}),
    SrartResetStruct.new('exit', '.', lambda{exit}),
    ]
  attr_writer :time_out, :time_color
  
  def construct
    self.caption = 'LTTimer'
    #addTimer 1000
    addTimer 50
    @font = @screen.factory.newfont 'MS ゴシック', -72
    #@font = @screen.factory.newfont 'HG ゴシックE', -72
    #@font = @screen.factory.newfont 'HGS ゴシックE', -72
    #@font = @screen.factory.newfont 'Tahoma', -72, 400, 0, 0, 0, 34, 0
    #@font = @screen.factory.newfont 'Tahoma', -72
    @time_out = 5.0
    @time_color = 3.0
  end # def construct
  
  def self_paint
    elapse = Time.at((Time.now - anchor_time).abs)
    self.textColor = RGB elapsed(elapse), 0, 0
    mm_ss = elapse.strftime '%M:%S'
    setFont @font
    #p textExtent(mm_ss)
    drawText mm_ss, 0,0, *textExtent(mm_ss)
  end # def self_paint
  
  def self_timer
    refresh
  end
  
  StartReset.each do |start_reset|
    define_method start_reset.name, start_reset.body
  end # StartReset.each do |start_reset|
  
  private
  def anchor_time
    Time.now
  end # def anchor_time
  
  def elapsed(sec)
    sec = sec.to_i
    case sec
      when 0 ... Min*@time_color then
        0
      when Min*@time_color ... Min*@time_out then
        256 * (sec - Min*@time_color) / (Min*(@time_out - @time_color))
      else
        sec < 0 ? 0 : 255
    end # case sec
  end # def elapsed(sec)
end # class TimerControl < VRPanel

class StartResetControl < VRPanel
  include VRVertLayoutManager
  include VRStdControlContainer
  
  def construct
    TimerControl::StartReset.each do |start_reset|
      addControl VRButton, start_reset.name, start_reset.button
      send_parent start_reset.name, 'clicked'
    end # TimerControl::StartReset.each do |start_reset|
  end # def construct
end # class StartResetControl < VRPanel

module LTTimer
  attr_reader :timer
  def construct
    self.caption = 'LTTimer'
    move 0,0, 198,104
    addControl TimerControl, 'timer', 'Timer', 0,0, 180,72
    addControl StartResetControl, 'buttons', 'Buttons', 181,0, 10,72
    top 0xffffffff
  end # def construct
  
  TimerControl::StartReset.each do |start_reset|
    define_method("buttons_#{start_reset.name}_clicked") do
      @timer.send start_reset.name
    end # define_method("buttons_#{start_reset.name}_clicked") do
  end # TimerControl::StartReset.each do |start_reset|
end # module LTTimer

time_out, time_color, start = 5, 3, false
require 'optparse'
ARGV.options do |opt|
  opt.on('-o', '--timeout=VAL', 'default: 5(min)'){|v| time_out = v.to_f}
  opt.on('-c', '--timecolor=VAL', 'default: 3(min)'){|v| time_color = v.to_f}
  opt.on('-s', '--[no-]start', 'default: no'){|v| start = v}
  opt.banner << "\nLeft upper topmost timer"
  begin # rescue OptionParser::ParseError => evar
    opt.parse!
  rescue OptionParser::ParseError => evar
    evar.display $stderr; "\n".display $stderr
    opt.help.display $stderr
    exit 1
  end # rescue OptionParser::ParseError => evar
end # ARGV.options do |opt|

begin # rescue SystemStackError => evar
  #VRLocalScreen.start LTTimer
  lt = VRLocalScreen.showForm LTTimer
  lt.timer.time_out = time_out
  lt.timer.time_color = time_color
  lt.timer.start if start
  VRLocalScreen.messageloop
rescue SystemStackError => evar
end # rescue SystemStackError => evar

WindowsVisualuRuby を使って時間(分秒)表示する GUI時計を作ります。一応 5分計で、途中 3分から表示色を赤っぽくし始め 5分で赤くします。コマンドラインオプションからその辺のタイミングを設定するのはOptionParser (optparse) です。

その表示色なんですが 3分から RGBの値を変えていくのに、実際のところ 3分では全然分かりません。3分半でもしかしてら色変えてますかという程度。4分でそれは褐色かもと見えるようになり、4分半でかなり赤く、そして4分45秒くらいでもう真っ赤、そのまま 5分以降も赤いです。

以下、順次メモ書き。

Version とか require

#coding:Windows-31J
# $Id: LT_timer.rb 3582 2010-07-04 09:32:14Z hs9587 $
# $Date: 2010-07-04 18:32:14 +0900 (譌・, 04 7 2010) $
# $Name$ $Author: hs9587 $ $Revision: 3582 $
Version = "0.0 ($Revision: 3582 $)"
require 'vr/vruby'
require 'vr/vrcontrol'
require 'vr/vrtimer'
require 'vr/vrlayout'
require 'time'
Min = Time.parse('00:01')-Time.parse('00:00')

Version定数は後で OptionParser (optparse) の --version 表示に拾って貰う用、subversion の更新文字列を混ぜてる。
そして VisuzluRuby関係の require、vruby は全体的なもの、vrcontrol は釦とか使うので、vrtimer はタイマーの自動画面更新用、vrlayout は釦を配置するときのレイアウトマネージャー。
time はそして Min = 60 の計算用、一分が60秒ってのは即値で書いちゃってもいいのかな。定数 Min の名前はちょっと迷う、最小とか誤解しないか、でもあんまり長いのもなんだし。

タイマー表示のVRコントロール

class TimerControl < VRPanel
タイマー表示の導入
class TimerControl < VRPanel
  include VRTimerFeasible
  include VRDrawable
  SrartResetStruct= Struct.new :name, :button, :body
  StartReset = [
    SrartResetStruct.new('start', '!' \
      , lambda{@start_time = Time.now; def anchor_time; @start_time; end}),
    SrartResetStruct.new('reset', '0' \
      , lambda{def anchor_time; Time.now; end}),
    SrartResetStruct.new('exit', '.', lambda{exit}),
    ]
  attr_writer :time_out, :time_color

include はまあそんな感じ。Struct構造体を定義しての StartReset配列にはこの後のコントロール釦の名前や表示、動作を登録する。このコントロールを含む三つのクラス(モジュール)定義で共用するものなので一箇所に登録する。lambdaブロックの動作の為か、このクラス内で定義しないとうまく動かなかったのでここに。
釦の動作は、exitはまあ良いとして、あとの二つは後ろの方で定義するプライベートメソッド anchor_time の再定義。釦押下時点を記録してその時間を返すか、現在時刻を返すか。
そして :time_out, :time_color はコマンドラインオプションからやってきたりする文字色変更のタイミング、というかタイマー期限。

コンストラクタとフォントの事
  def construct
    self.caption = 'LTTimer'
    #addTimer 1000
    addTimer 50
    @font = @screen.factory.newfont 'MS ゴシック', -72
    #@font = @screen.factory.newfont 'HG ゴシックE', -72
    #@font = @screen.factory.newfont 'HGS ゴシックE', -72
    #@font = @screen.factory.newfont 'Tahoma', -72, 400, 0, 0, 0, 34, 0
    #@font = @screen.factory.newfont 'Tahoma', -72
    @time_out = 5.0
    @time_color = 3.0
  end # def construct

コンストラクタの意義としては addTimer かな。あんまり長いと秒表示のずれがひどいし短いとちらつく。もう少しだけ長くてもいいかも。コメントアウトしてる 1000 は参考にした「フォントを選べる時計」での値。
とはいえそれを参考にしてもフォントの指定方法は良く分からなかったので 'MS ゴシック' 72サイズ。本当は Tahoma とか使いたかったんだけどどうも位置がずれる、大体「-72」の「-(マイナス)」って何だ。

分秒の表示
  def self_paint
    elapse = Time.at((Time.now - anchor_time).abs)
    self.textColor = RGB elapsed(elapse), 0, 0
    mm_ss = elapse.strftime '%M:%S'
    setFont @font
    #p textExtent(mm_ss)
    drawText mm_ss, 0,0, *textExtent(mm_ss)
  end # def self_paint
  
  def self_timer
    refresh
  end

実際の表示と再表示、メソッド名は VisualuRuby に従うもの。
表示時間は achor_timeプライベートメソッドと現在時刻の差、achor_timeメソッドの動作によってタイマーが進むのか止まってるのか決まる。そしてそれを elapsedプライベートメソッドに渡して文字色を調整する。

コントロール系のメソッド
  StartReset.each do |start_reset|
    define_method start_reset.name, start_reset.body
  end # StartReset.each do |start_reset|

釦押下時の動作(メソッド)を定義、前掲配列を当たって三つ。

プライベートメソッド
  private
  def anchor_time
    Time.now
  end # def anchor_time
  
  def elapsed(sec)
    sec = sec.to_i
    case sec
      when 0 ... Min*@time_color then
        0
      when Min*@time_color ... Min*@time_out then
        256 * (sec - Min*@time_color) / (Min*(@time_out - @time_color))
      else
        sec < 0 ? 0 : 255
    end # case sec
  end # def elapsed(sec)
end # class TimerControl < VRPanel

タイマー開始時を返す anchor_time と、タイマー経過時間に対して文字色を返す elapsed。色の計算には起動時にセットされるインスタンス変数 @time_out, @time_color を用いる

操作用釦のVRコントロール

class StartResetControl < VRPanel
  include VRVertLayoutManager
  include VRStdControlContainer
  
  def construct
    TimerControl::StartReset.each do |start_reset|
      addControl VRButton, start_reset.name, start_reset.button
      send_parent start_reset.name, 'clicked'
    end # TimerControl::StartReset.each do |start_reset|
  end # def construct
end # class StartResetControl < VRPanel

レイアウトマネージャを使って釦を配置、釦定義には前掲配列を当たる。

タイマー本体

module LTTimer
  attr_reader :timer
  def construct
    self.caption = 'LTTimer'
    move 0,0, 198,104
    addControl TimerControl, 'timer', 'Timer', 0,0, 180,72
    addControl StartResetControl, 'buttons', 'Buttons', 181,0, 10,72
    top 0xffffffff
  end # def construct
  
  TimerControl::StartReset.each do |start_reset|
    define_method("buttons_#{start_reset.name}_clicked") do
      @timer.send start_reset.name
    end # define_method("buttons_#{start_reset.name}_clicked") do
  end # TimerControl::StartReset.each do |start_reset|
end # module LTTimer

本体の定義、既に定義したタイマー表示部分と釦配置の二つのコントロールを配置する。サイズは即値でここに書く、表示してみないと表示領域の大きさが分からないのでいろいろやってみた。大きさ動的にとるの難しい。あと、top の指定でタイマーを最前面表示する。
それとやはり前掲配列を当たって、釦のコントローラから釦押下を受け取ってタイマー表示のコントローラに受け渡すメソッドを定義。タイマー表示のコントローラは @timerインスタンス変数にいれてる。

コマンドラインとオプション変数

time_out, time_color, start = 5, 3, false
require 'optparse'
ARGV.options do |opt|
  opt.on('-o', '--timeout=VAL', 'default: 5(min)'){|v| time_out = v.to_f}
  opt.on('-c', '--timecolor=VAL', 'default: 3(min)'){|v| time_color = v.to_f}
  opt.on('-s', '--[no-]start', 'default: no'){|v| start = v}
  opt.banner << "\nLeft upper topmost timer"
  begin # rescue OptionParser::ParseError => evar
    opt.parse!
  rescue OptionParser::ParseError => evar
    evar.display $stderr; "\n".display $stderr
    opt.help.display $stderr
    exit 1
  end # rescue OptionParser::ParseError => evar
end # ARGV.options do |opt|

optparse を使ってコマンドラインオプションを受け取る。文字色を変えるタイミングと起動時にタイマーをスタートさせてるか止めてるかの選択。opt.banner にて Usage文字列に説明を付加。

実行

begin # rescue SystemStackError => evar
  #VRLocalScreen.start LTTimer
  lt = VRLocalScreen.showForm LTTimer
  lt.timer.time_out = time_out
  lt.timer.time_color = time_color
  lt.timer.start if start
  VRLocalScreen.messageloop
rescue SystemStackError => evar
end # rescue SystemStackError => evar

セットされたコマンドオプションを設定して .messageloopを起動。オプションを設定する必要があるのでコメントアウトしてるような .startで一気に実行というのはない。
exit を呼ぶとちょっと階層深くなってるのか SystemStackError が発生するので begin rescue で囲ってある。

VisualuRuby

GUI表示には、Windows という事で VisualuRuby(VisualuRuby計画(仮称))を用いた。なかなか説明とか資料が見付からなくて苦労する。上記プロジェクトのページと、そこにも記載のあるサンプル(VisualuRuby計画(仮称) サンプル)を参考に考える。

SystemStackErrorのこと(/7/25 追記)

メイン(実行)パートの SystemStackError だけど単純な僕の間違いだった。
StartReset定数の3項目

SrartResetStruct.new('exit', '.', lambda{exit}),

これは TimerControlコントローラの中ではメソッド定義に用いる。その実行ブロックでは「Kernel.#exit」メソッドを呼んでプログラム全体を終了しようとするつもりのに、メソッド名自体を「exit」にしてるので自身を呼ぶ事になって循環してしまう。そしてこのエラーになる。

ということで StartReset定数は書き換え、メソッド名は「close」にでもしましょう。

  StartReset = [
    SrartResetStruct.new('start', '!' \
      , lambda{@start_time = Time.now; def anchor_time; @start_time; end}),
    SrartResetStruct.new('reset', '0' \
      , lambda{def anchor_time; Time.now; end}),
    SrartResetStruct.new('close', '.', lambda{exit}),
    ]

そしてメイン(実行)ブロックは begin, rescue節で囲う事もなく。

#VRLocalScreen.start LTTimer
lt = VRLocalScreen.showForm LTTimer
lt.timer.time_out = time_out
lt.timer.time_color = time_color
lt.timer.start if start
VRLocalScreen.messageloop