2010年9月 8日

[ruby-list:47380] Re: Ruby/Tk Listbox

永井@知能.九工大です.

# 現実逃避で長文になってしまいましたが,御容赦を (^_^;

From: Masutoyo Kawamura <m-kwmr@xxxxx>
Subject: [ruby-list:47379] Ruby/Tk Listbox
Date: Wed, 8 Sep 2010 19:10:38 +0900
Message-ID: <20100908191200.44b532df.m-kwmr@xxxxx>

> Ruby/Tk の Listbox に 1秒おきに、1行 表示したいのですが、
> 5秒後に一気に5行表示されます。

まずは前置きですが,イベントループである Tk.mainloop は,
「ボタンが押された」などのイベントに対応した処理のほか,
画面表示の更新も受け持っています.
時間のかかる画面表示の更新を変化がある度に毎回必ず行っていると
イベントに対する反応が遅くなってしまいますから,
即応が必要なイベントが途切れた時 (idle loop) を見計らって
画面表示の更新をまとめて行います.

# もちろん,即応が必要なイベントが届けばそちらが優先されます.

さて,そうした前置きの下に河村さんのサンプルを見ると,
ボタンが押された際のコールバックが 5 秒間終了しないことがわかります.
これは「ボタンが押された」という一つのイベントに対する処理ですから,
イベントループはそれに付きっきりになってしまいます.
そのため,表示に限らず,あらゆるイベントに対して反応できなくなります.

しかしながら,発生したイベントはイベントキューに溜まっていますから,
コールバックが終了した後に溜まっていたイベントが
一気に処理されることになってしまいます.
多分,これは嬉しくない状況でしょうね.

> 意図したようにするには、どうしたら良いのでしょうか。

表示の更新の問題もありますが,
こうした時間がかかるコールバックを有する GUI の設計として,
同時に複数回のボタンクリックを許すのかという問題もあります.
多分,この例の場合にはコールバックが終了するまでは
並行して動いてほしくはないのでしょうね.

順に考えていきましょう.
まずは「表示が更新されない」ということだけを解消したい場合です.
これについては望むタイミングで強制的に表示を更新してやればいいわけです.
この↓ように sleep 1 の前に Tk.update を入れてやると,
------------------------
#!/usr/bin/env ruby
require 'tk'
l = TkListbox.new(:width=>40, :height=>10).pack(:fill=>:both, :expand=>:true)
TkButton.new(
:text => 'Button',
:command => proc{
for i in 1 .. 5
l.insert('end',"This is line #{i}")
Tk.update
sleep 1
end
}
).pack

Tk.mainloop
------------------------
表示更新を含めて溜まっているイベント処理がなくなるまで
その位置でサブイベントループが動くような形となります.

もう一つは Thread を使うやり方です.
この↓ように
------------------------
#!/usr/bin/env ruby
l = TkListbox.new(:width=>40, :height=>10).pack(:fill=>:both, :expand=>:true)
TkButton.new(
:text => 'Button',
:command => proc{
Thread.new{
for i in 1 .. 5
l.insert('end',"This is line #{i}")
sleep 1
end
}
}
).pack

Tk.mainloop
------------------------
時間がかかる処理は別 Thread にしてしまって
コールバック処理自体はさっさと終了させてしまうわけです.
そうすることでイベントループ自体は
時間がかかる処理に煩わされなくなりますから,
表示もスムーズに更新されます.

さて,上記のように修正した場合,
いずれの例でもコールバック処理の実行中に
更なるボタンクリックを受け付けてしまい,
複数回のクリックの表示が混ざってしまうと思います.

これを避ける場合の GUI のデザインとしては
表示が終了するまではボタンの動作を禁止するという方法と,
ボタンの入力だけは受け付けておいて,
終了するごとに順に実行するという方法とがあると思います.
処理としては後者の方がやや面倒です.

前者については,ボタンの state 属性を操作してやればいいので,
例えば
------------------------
#!/usr/bin/env ruby
l = TkListbox.new(:width=>40, :height=>10).pack(:fill=>:both, :expand=>:true)
b = TkButton.new(
:text => 'Button',
:command => proc{
Thread.new{
b.state :disabled
for i in 1 .. 5
l.insert('end',"This is line #{i}")
sleep 1
end
b.state :normal
}
}
).pack

Tk.mainloop
------------------------
のようにすれば (ローカル変数の使い方があまり良くないかもしれませんが)
コールバック (を実行中のスレッド) の動作中はボタン操作が禁止されます.

もう少し丁寧に,コールバック処理中に異常が発生しても
ボタンの状態をきちんと戻したいなら,
------------------------
#!/usr/bin/env ruby
l = TkListbox.new(:width=>40, :height=>10).pack(:fill=>:both, :expand=>:true)
b = TkButton.new(
:text => 'Button',
:command => proc{
Thread.new{
b.state :disabled
begin
for i in 1 .. 5
l.insert('end',"This is line #{i}")
sleep 1
end
ensure
b.state :normal
end
}
}
).pack

Tk.mainloop
------------------------
くらいのことをしておいた方がいいかもしれませんね.

最後に,コールバック中もボタン操作を受け付ける
(ただし,実行は順次) としたい場合です.
考えれば色々な方法がありそうですが,
一例として,ここではコールバックを順次処理するための queue を
使った例を示して終わりにします.
------------------------
#!/usr/bin/env ruby
l = TkListbox.new(:width=>40, :height=>10).pack(:fill=>:both, :expand=>:true)

callback_queue = Queue.new
Thread.new{
loop{
begin
cb = callback_queue.pop
cb.call
rescue Exception
end
}
}

b = TkButton.new(
:text => 'Button',
:command => proc{
callback_queue.push proc{
for i in 1 .. 5
l.insert('end',"This is line #{i}")
sleep 1
end
}
}
).pack

Tk.mainloop
------------------------
--
永井 秀利 (nagai@xxxxx)
九州工業大学大学院情報工学研究院知能情報工学研究系知能情報メディア部門助教


投稿者 xml-rpc : 2010年9月 8日 20:39
役に立ちました?:
過去のフィードバック 平均:(0) 総合:(0) 投票回数:(0)
本記事へのTrackback: http://hoop.euqset.org/blog/mt-tb2006.cgi/98246
トラックバック
コメント
コメントする




画像の中に見える文字を入力してください。