Ruby/tk

GUIプログラミング

これまで作成してきた対話的プログラムは、入力と出力がそれぞれ 1つの流れでできていた。 GUI(Graphical User Interface)プログラミングでは、 利用者が働きかけを行なう対象が、ボタン、入力窓、メニューなど複数あり、 どんな順番で働きかけが来ても対応できる形となっていなければならない。 ユーザからのキーボードやポインティングデバイス(マウスなど)を用いた 働きかけのことをイベントといい、なんらかのイベントが発生したら それに対応してあらかじめ登録しておいたプログラム部分が動くような 構成となっている。このような動きを取るプログラムを イベント駆動型プログラムといい、GUIプログラムの 典型的な形式である。

GUIプログラムでは、ウィンドウ部品を作ったり、イベント処理を 行なうライブラリを用いて行なう。GUI用の部品が一式揃ったライブラリの ことをツールキットといい、言語や用途に応じて様々な ツールキットが存在するが、その利用の基本的な考え方は 共通している。

様々なツールキット

Ruby/tkの初歩

他のGUIツールキットを利用したプログラミングと同様、 Ruby/tkでもイベント駆動型プログラムを作成していく。 そのおおざっぱな流れとしては、

  1. ウィンドウの部品(ウィジェット)を作成する。

  2. 作成した部品を土台部品に貼り付ける

  3. どのイベントにどのアクションを起こすかを登録する。

  4. メインループを呼ぶ

となる。イベントに対する反応をとくに決めないものなら3は不要である。 まずは、ウィジェットを出すだけの簡単なプログラムを示す。

tk-hello.rb

#!/usr/koeki/bin/ruby
# coding: euc-jp
require 'tk'

TkLabel.new("text" => "Hello, world!").pack   # 1.と2.に相当
Tk.mainloop                                   # 4.に相当

TkLabelは、ラベルとなるクラスで new によって、ひとつのラベルを生成する。ラベルは、文字や画像などを 表示するためのウィジェットである。引数にどのようなラベルを生成するかの 情報を持たせた属性値をハッシュ形式で与えると、それに応じた ラベルオブジェクトを生成する。実際には生成するだけでは表示されず、 pack メソッドを用いて初めて表示される。 pack は、あるウィジェットをどのようにウィンドウ上に 配置するかを決めるメソッドである。このように配置を 司るものをジオメトリマネージャといい、GUI部品を 効果的に配置する重要な約割を担っている。他のツールキットにも同様のものがあり レイアウトマネージャなどと呼ぶこともある。

最初の例 tk-hello.rb に、イベントに対する反応を 登録する部分を追加してみる。書き方は様々だが、いくつかの実例を含んだ 以下のプログラムを示す。

tk-hello-ev.rb

#!/usr/koeki/bin/ruby
# coding: euc-jp
require 'tk'

TkLabel.new("text" => " Hello, world! ") {
  bind('1', proc {exit})
}.pack
Tk.mainloop

色を変えた部分が追加した部分である。 追加部分は、TkLabel.newメソッドに与えたブロックで、 このブロック内は TkLabel クラスに属すメソッド呼出し が列挙できる。上記リストは以下のいずれの書き方でも同じ働きをする。

その1: ブロックへの仮引数はそのオブジェクト自身を指す値となる。

TkLabel.new("text" => " Hello, world! ") {|x|
  x.bind('1', proc {exit})
  x.pack
}
Tk.mainloop

その2: オブジェクトをローカル変数に格納してあとで使う場合。

lab = TkLabel.new("text" => " Hello, world! ")
lab.bind('1', proc {exit})
lab.pack
Tk.mainloop

その3: テキスト属性指定もメソッド呼び出しで。

TkLabel.new() {
  text(" Hello, world! ")
  bind('1', proc {exit})
  pack
}
Tk.mainloop

その4: packがオブジェクト自身を返すのでbindメソッドが呼べる。

TkLabel.new() {
  text(" Hello, world! ")
}.pack.bind('1', proc {exit})
Tk.mainloop

イベント処理

ウィンドウ上で発生する様々なイベントに対して、 処理を行なう部分をイベントハンドラという。この登録には 上述の例のとおり bind メソッドを使う。

bind メソッドは

bind(シーケンス, 処理)

の形式で指定する。シーケンスの指定方法を 説明する前に、いくつかの指定を含む例題プログラムを示す。

tk-ev.rb

#!/usr/koeki/bin/ruby
# coding: euc-jp
require 'tk'

def erase(widget)
  widget.value = ""             # Entryの入力文字列を消す
end

TkLabel.new("text" => " Hello, world! ") {
  # ラベルでは、マウス第3ボタンが効き、キーは効かない
  bind('Button-3', proc {exit}) 	# 右ボタンがクリックされたら
  bind('Key-3', proc {exit})    	# キー3が押されたら(でも効かない)
}.pack
TkEntry.new {|tke|
  # 入力窓では、マウス第3ボタン、第2ボタン、キー'q'、'x'が効く
  bind('Key-3', proc {erase(tke)}) 	# キー3が押されたら(これは効く)
  bind('2', proc {erase(tke)})          # 1,2,3とだけ書くとマウスボタン
  bind('Key-q', proc {                  # Key-q でも q でもよい
         erase(tke)
         Tk.callback_break		# q そのものの入力を回避
       })
  bind('x', proc {erase(tke)})          # xならキー 'x'
}.pack
puts "ラベル上のボタン3で終了"
Tk.mainloop

イベントパターンとシーケンス

シーケンスの部分は発生するイベントにマッチするパターンを独自の記法で 表したものの並びを指定する。このパターンをイベントパターン といい、

modifier-modifier-type-detail

の形式、あるいはその省略できる部分を省いた形式で記述する。 modifierは修飾(モディファイア)を示すシンボルで、 指定できる代表的なものを示すと以下のようになる。

AltAlt
ShiftShift
ControlControl
LockCapsLock
Meta
M
Metaキー
Mod1
M1
Mod1
Mod2
M2
Mod2
Mod3
M3
Mod3
Mod4
M4
Mod4
Mod5
M5
Mod5
Button1
B1
マウス第1ボタン
Button2
B2
マウス第2ボタン
Button3
B3
マウス第3ボタン
Button4
B4
マウス第4ボタン
Button5
B5
マウス第5ボタン
Double2連
Triple3連
Quadruple4連

Mod1からMod5 は、ウィンドウシステムの モディファイアキーで、X Window System では5種類のモディファイアキーが 利用できる。現在どのキーがモディファイアとして登録されているかは、 コマンドラインで

xmodmap

と起動してみれば分かる。

キー入力で複数のキーを続けて押したものを表したいときなど、 複数のイベントの組み合わせをバインドすることもできる。 これにはイベントシーケンスを配列化したもので表現する。たとえば、

bind(['C-x', 'C-c'], proc{exit})

とすると、C-x に続けて C-c を押したときの処理を定義することになる。ただしこの場合 C-x のみを押したときの処理が別に定義されている場合は C-x が押された段階でそちらも呼ばれることに注意する。

type の部分はイベントタイプで発生したイベントの 種別を表すシンボルである。代表的なものを以下に示す。

Button
ButtonPress
マウスボタンのクリック
ButtonRelease押されていたマウスボタンが離された
Key
KeyPress
キーが押された
KeyRelease押されているキーが離された
Destroyウィンドウが強制終了された
FucusInウィンドウがフォーカスされた
FucusOutウィンドウフォーカスが外れた
Enterウィンドウ内にポインタが入った
Leaveウィンドウからポインタが出た
Motionウィンドウ内でポインタが動いた

イベントシーケンス最後の部分、detail はイベント発生源の 具体的な指定で、マウスボタンならば 1、2、3、4、5のいずれか、 キー入力ならばそのキーを表すキーシンボル(keysym)を指定する。 英数字キーは文字そのものがkeysymであり、たとえば r と 書けばRのキーを押した場合を意味する。したがって、イベントシーケンス

Control-B3-Triple-Key-r

は、Control キーとマウス第3ボタンを押しながら r のキーを3連打した場合の イベントを意味する。 各種記号やReturnキーやBSキーなどのkeysym は、 システムによってあらかじめ決められている。 これらを調べるにはコマンドラインで、

xev

と起動し、出てきたウィンドウ内部で調べたいキーをタイプするか、

xmodmap -pke | less

で割り当てキーの一覧を見るとよい(X Window Systemの場合)。

ただし、ウィジェットによって反応できるイベントは異なり、 割り当てたイベントシーケンスがどこでも効くとは限らない。たとえば ラベルウィジェットではキー入力のイベントに反応させることはできない。

イベントハンドラ

bind メソッドの第2引数には、 なんらかのイベントに結びつけるイベントハンドラを指定する。 ここにはRubyの Procオブジェクトを指定する。Procオブジェクトに渡すブロックは その位置で有効な変数の状態で評価される。

tk-proc.rb

#!/usr/koeki/bin/ruby
# coding: euc-jp
require 'tk'

TkLabel.new("text"=>"変数xが有効なブロック\nこっちでクリックするとx=5") {
  x = 5
  bind('1', proc {printf("x=%d\n", x)})
  bg("pink")
}.pack("fill"=>"x")
TkLabel.new("text"=>"変数xが有効ではないブロック\nこっちはエラー") {
  bind('1', proc {printf("x=%d\n", x)})
  bg("#aef")
}.pack

TkButton.new
Tk.mainloop

command

ボタン型のウィジェットでは、bindによるイベントハンドラの 登録をせずに、command メソッドでボタンをクリックしたときの 挙動を定義できる。

tk-command.rb

#!/usr/koeki/bin/ruby
# coding: euc-jp
require 'tk'

TkButton.new("text"=>"Button") {
  command(proc {puts "これはボタン"})
}.pack
TkCheckbutton.new("text"=>"Check Button") {
  command(proc {puts "これはチェックボタン"})
}.pack
TkFrame.new {|f|
  v = TkVariable.new
  TkRadiobutton.new(f, "text"=>"Radio Button") {
    command(proc {puts "ラジオボタン-1"})
    variable(v)
    value("1")
  }.pack("side"=>"left")
  TkRadiobutton.new(f, "text"=>"Radio Button") {
    command(proc {puts "ラジオボタン-2"})
    variable(v)
    value("2")
  }.pack("side"=>"left")
}.pack

Tk.mainloop

イベントハンドラへの情報

bindメソッドに指定する手続きに対し、 発生したイベントの詳細情報を渡すことができる。 たとえば以下のようにするとクリックが起きたときの画面上のポインタの X座標、Y座標が得られる。

tk-xy.rb

#!/usr/koeki/bin/ruby
# coding: euc-jp
require 'tk'

TkCanvas.new {
  width(400)
  height(300)
  bind('1', proc {|x, y, a, b|
         printf("絶対:(%d,%d)\t", x, y)
         printf("相対:(%d,%d)\n", a, b)
       }, "%X %Y %x %y")
}.pack
TkButton.new() {		# なくてもいいがquitボタンを足す
  text("quit")
  command(proc{exit(0)})
}.pack("side"=>"right")

Tk.mainloop

上記のように、bind メソッドの第3引数に空白区切りの %置換文字列を指定すると、それらが手続きオブジェクトに 引数化されて渡される。

%%%自身
%#イベントのシリアル番号
%ddetailフィールドの値
%fEnter/Leaveイベントでのフォーカス値(0か 1)
%kkeycode値
%x, %yイベントのウィンドウ内の相対x座標・y座標
%X, %Yイベントのx座標・y座標
%AUnicode値
%Tイベントの種別
%Wイベントを捕捉したウィジェット

即時キー入力処理

文字入力を主目的としないウィジェットでは、キー入力イベントを 捕捉するようにはできていない。そのようなウィジェットでキー入力 による処理切り替えを行ないたい場合は、ルートウィジェットに対して キー入力をbindして処理を行なう。

仮想イベント

同じ機能に複数のキー割り当てを用意したいときや、 別のプラットフォーム用に異なるキー割り当てを用意したいときなどは、 複数のイベントシーケンスからなる仮想イベントを作って、 それに機能を割り当てるとよい。仮想イベントは TkVirtualEvent クラスのオブジェクトとして、 登録したい複数のイベントシーケンスを渡して生成する。

たとえば、C-q のみ、C-x C-c の連続押し 両方のキーバインドを設定したいときは以下のようにする。

event_quit = TkVirtualEvent.new('Control-q', ['Control-x', 'Control-c'])
# この例では2個だがイベントシーケンスは何個でも
  :
# 特定のオブジェクトのbind部分で以下のように仮想イベントを使う
 bind(event_quit, proc{exit})

ジオメトリマネージャ

生成した部品は土台となる部品に配置される。一つの土台には複数の部品を 装着できる。このときにここの部品をどのように配置するかを決めるのが ジオメトリマネージャ(レイアウトマネージャ)である。tkでは pack、grid、place のジオメトリマネージャが使える。

いずれも土台となるウィジェット1つに対して 1つのジオメトリマネージャが使える (複数のマネージャを混合利用すると暴走する恐れがある)。 複数のジオメトリマネージャを混在させて使いたいときは、 後述するフレームウィジェットを 新たな土台として組み合わせる。

pack

最もラフに部品を配置できるジオメトリマネージャが pack で、土台となるウィジェット(最初はルートウィジェット)の空き領域の どこに次のウィジェットを置くかおおざっぱに「上の方」、「下の方」、 「右の方」、「左の方」いずれかで指定して配置を決定する。

土台となる部品があり、その上に3つの部品を

  1. 上の方に
  2. 左の方に
  3. 下の方に

という順番でpackを使って配置した場合次のような配置状態の遷移を取る。

  1. 上の方に配置 (pack("side"=>"top")
    部品1
    (空き領域)
  2. 左の方に配置 (pack("side"=>"left")
    部品1
    部品2 (空き領域)
  3. 下の方に配置 (pack("side"=>"bottom")
    部品1
    部品2 (空き領域)
    部品3

以上ですべての部品追加が完了した場合、空き領域が詰められ 各部品が必要最小限の大きさに調整される。 最終的な部品配置は以下のようなものとなる。

部品1
部品2 部品3

実際に上記の3手順で配置するプログラム

TkLabel.new("text"=>"部品1", "bg"=>"green").pack("side"=>"top")
TkLabel.new("text"=>"部品2", "bg"=>"pink").pack("side"=>"left")
TkLabel.new("text"=>"部品3", "bg"=>"yellow").pack("side"=>"bottom")
Tk.mainloop

を実行すると以下のような配置結果となる。

result of tk-pack0

「部品1」の左右のように、部品サイズと土台と隙間ができる場合がある。 隙間を埋めたい場合は "fill" を指定する。指定できる値は

"x"x軸方向(左右両側)を埋める
"y"y軸方向(上下両側)を埋める
"both"x・y軸方向(上下左右)を埋める
"none"埋めない

のいずれかで、たとえば上記の「部品1」の貼り付けを

pack("side"=>"top", "fill"=>"both")

に変えた場合は、以下のような配置結果となる。

result of tk-pack0

ただし、できあがったこのウィンドウも、ウィンドウサイズを大きくすると 次のようになる。

Enlarged window

これは、部品1だけが隙間埋める設定になっていたからで、 部品2、部品3も "fill"=>"both" でpackすると ウィンドウサイズを大きくしたときに以下のようになる。

enlarged window(2)

部品3の上にまだ隙間があるのは、そこが空き領域だからで、 空き領域を侵蝕するように隙間を埋めさせるためには、 "expand" を指定する。"expand" は、本来の持ち領域を超えてウィジェットを拡大させるかを決めるもので これに true を設定すると有効になる。部品3に "expand"=>true を設定し、最終的に

TkLabel.new("text"=>"部品1", "bg"=>"green").
 pack("side"=>"top", "fill"=>"both")
TkLabel.new("text"=>"部品2", "bg"=>"pink").
 pack("side"=>"left", "fill"=>"both")
TkLabel.new("text"=>"部品3", "bg"=>"yellow").
 pack("side"=>"bottom", "fill"=>"both", "expand"=>true)

として出したウィンドウを大きくすると以下のようになり隙間がなくなる。

enlarged window(3)

packジオメトリマネージャの配置を変えるための引数では、 以下のパラメータが使える。

"side" 空き領域のどちら側に配置するか。
"top"(上)、 "bottom"(下)、 "left"(左)、 "right"(右)
"fill"
"expand" 空き領域を埋めるように領域拡張するか
truefalse
"before" 指定したウィジェットより前に配置
"after" 指定したウィジェットのあとに配置
"ipadx" ウィジェットの左右の縁の内側の隙間間隔
"ipady" ウィジェットの上下の縁の内側の隙間間隔
"padx" ウィジェットの左右の縁の外側の隙間間隔
"pady" ウィジェットの上下の縁の外側の隙間間隔

"ipadx""ipady""padx""pady" に指定するのは長さで、 整数を指定するとピクセル、 単位つきの整数文字列を指定するとその単位での長さになる。単位は

のいずれかで、たとえば "pady"=>"5c" のように指定する。

grid

各部品を表形式で格子状に並べるのに適しているのが gridジオメトリマネージャである。

tk-grid0.rb

TkLabel.new() {
  text("ラベルの1番"); bg("green")}.grid("row"=>0, "column"=>0) # 0行0列
TkLabel.new() {
  text("ラベル2"); bg("pink")}.grid("row"=>0, "column"=>1)      # 0行1列
TkLabel.new() {
  text("ラ\nベ\nル3"); bg("pink")}.grid("row"=>1, "column"=>0)  # 1行0列
TkLabel.new() {
  text("L 4"); bg("green")}.grid("row"=>1, "column"=>1)         # 1行1列

とすると、以下のような配置結果が得られる。

Grid

格子の各マス目の幅と高さは各列、各行が同じになるように調整される。 マス目の幅・高さより小さいウィジェットは中央に配置され隙間ができる。 "sticky" 属性を指定して縁に密着させる辺を指定することができる。

"n"上辺を密着 (North)
"s"下辺を密着 (South)
"w"左辺を密着 (West)
"e"右辺を密着 (East)

の1字以上を指定してどの辺を密着させるか決める。たとえば、 上のtk-grid0.rb の 「L4」ラベルのgridで "sticky"=>"wes" の指定を追加すると以下のようになる (tk-grid1.rb)。

sticky=>wes

さて、余白が気になるので、全て余白を消すことを試みる。 4つのラベル全てのgridに、"sticky"=>"news" を追加すると初期ウィンドウから余白は消える (tk-grid2.rb)

all 'news'

gridで作成したマス目はウィンドウサイズを変えたときにも変わらない。 このため上に示したウィンドウを大きくしても周りに余白ができるだけである。

grid+enlarged

ウィンドウサイズを変えたときに、中味のウィジェットも連動して 大きさを変える設定が可能である。これは、特定の列全体あるいは特定の行全体 に対して、拡大するときの他の列・行との伸縮負担の重み付けを行なうことで 制御する。上記の4ラベル配置例で、第0列と第1列の伸縮配分を1:3にするには 以下の文を追加する (tk-grid3.rb)。

TkGrid.columnconfigure(Tk.root, 0, "weight"=>1)
TkGrid.columnconfigure(Tk.root, 1, "weight"=>3)

第1引数の TkRoot は、今回gridジオメトリマネージャで土台 となっているウィジェットで、新たな土台を作らない場合の最初の土台は Tk.Root、つまりルートウィジェットとなる。 この記述を追加したウィンドウを大きくすると以下のような結果となる。

grid+weight

上下に隙間があるのは、行方向の設定をしていないからで、 上下の隙間を埋めさせるためには TkGrid.rowconfigure で同様の設定をすればよい。

place

placeは、ウィジェットの配置位置をx座標、y座標で直接指定できる ジオメトリマネージャである。大きさの決まっている土台に 座標を決めて部品を置いたり、ウィジェット間に重なりのある 配置をしたい場合に有用である。

tk-place.rb

TkOption.add("*font", "ipagothic 20")
Tk.root.width = 200
Tk.root.height= 80
TkLabel.new("text"=>"その1", "bg"=>"pink").place("x"=>10, "y"=>10)
TkLabel.new("text"=>"その2", "bg"=>"yellow").place("x"=>50, "y"=>30)
Tk.mainloop

tk-place

デフォルトでは配置するウィジェットの左上位置を基準とするが、 "anchor" 属性でこれを変えることもできる。 属性値には

"n"上辺中央
"s"下辺中央
"w"左辺中央
"e"右辺中央
"nw"左上角
"ne"右上角
"sw"左下角
"se"右下角
"center"中央

のいずれかを指定する。

フレームウィジェット

frame は、複数のウィジェットを内部に配置するためのウィジェットで Rubyでは TkFrame で生成する。

既に述べたとおり、1つの土台に対しては 1つのジオメトリマネージャしか使えない。 複雑な部品レイアウトを実現したいとき、複数のジオメトリマネージャを 組み合わせたいことがある。このような場合、複数のフレームを ルートウィジェットに配置し、さらに各フレームごとに違う ジオメトリマネージャを適用して内部のウィジェットを配置するようにするとよい。

たとえば次のようなレイアウトを考える。

Nested Layout

上半分は何かの値の入力を促す、 ラベルとエントリを対にしたものの集合、下半分は左右に分かれたボタン。 この構成の上は項目名とエントリの桁位置を揃えたいので gridジオメトリマネージャを、下は「左の方と右の方」とラフに置きたいので packジオメトリマネージャを使うことにする。

具体的な組み合わせ方としては、土台の上半分を占めるフレームを 上からpack、残った下半分を左と右からpack、さらに上半分の フレーム内をgridで制御してラベルとボタンを配置する。

(フレーム)
項目エントリ
項目エントリ
左のボタン右のボタン

このような配置を行なうプログラムの例を以下に示す。

tk-frame.rb

#!/usr/koeki/bin/ruby
# coding: euc-jp
require 'tk'

TkFrame.new {|f|
  TkLabel.new(f, "text"=>"住所").grid("row"=>0, "column"=>0, "sticky"=>"w")
  TkEntry.new(f, "width"=>20).grid("row"=>0, "column"=>1,  "sticky"=>"w")
  TkLabel.new(f, "text"=>"おなまえ").grid("row"=>1, "column"=>0, "sticky"=>"w")
  TkEntry.new(f, "width"=>12).grid("row"=>1, "column"=>1, "sticky"=>"w")
  TkGrid.columnconfigure(f, 0, "weight"=>4) # 項目名の列
  TkGrid.columnconfigure(f, 1, "weight"=>1) # Entryの列
}.pack("fill"=>"x", "expand"=>true, "padx"=>10)
TkLabel.new("text"=>"").pack   # spacer
TkButton.new("text"=>"登録").pack("side"=>"left", "padx"=>10, "pady"=>5)
TkButton.new("text"=>"クリア").pack("side"=>"right", "padx"=>10, "pady"=>5)
Tk.mainloop

フレームウィジェットに限らず、新規のウィジェットを フレームなど別の親の子として生成するときには、ウィジェット生成 のnewメソッドの第1引数に親とするウィジェットのオブジェクト を指定する。

画像

画像を扱うには、まず元となる画像ファイルを、 画像オブジェクトに変換し、そののち画像を配置できるウィジェットに 貼り付けるという手順を踏む。

画像のラベルへの貼り付け

tkの標準では gif, ppm, pgm のみ扱える。例としてgif画像 (cool.gif)をラベル上に 貼り付けて表示するものを示す。 cool.gif を同一ディレクトリにコピーしてから実行のこと。

tk-img.rb

#!/usr/koeki/bin/ruby
# coding: euc-jp
require 'tk'
img = TkPhotoImage.new("file"=>"cool.gif")
TkLabel.new("image"=>img).pack
TkButton.new("text"=>"quit", "command"=>proc{exit(0)}).pack
Tk.mainloop

tkimg拡張ライブラリ

JPGやPNGなど、他の画像形式を利用する場合は、 tkextlib/tkimg/FORMAT が必要で、 たとえば、PNG画像を使うには

require 'tkextlib/tkimg/png'

を追加記述する。例として、透過部分を含むPNG画像 (nikusoba.png) を表示するものを示す。

tk-imgpng.rb

#!/usr/koeki/bin/ruby
# coding: euc-jp
require 'tk'
require 'tkextlib/tkimg/png' # PNGを利用する場合必要

img = TkPhotoImage.new("file"=>"nikusoba.png")
TkLabel.new("image"=>img, "bg"=>"pink").pack
TkButton.new("text"=>"食べる",
             "command"=>proc{puts "ごちそうさま"; exit(0)}).pack
Tk.mainloop

対応しているフォーマットは、Rubyのtkextlibライブラリのある ディレクトリにある tkim/ にあるファイル一覧を見れば分かる。

ls `ruby -e 'puts $:[-3]'`/tkextlib/tkimg
bmp.rb     jpeg.rb    png.rb     setup.rb   tga.rb     xbm.rb
gif.rb     pcx.rb     ppm.rb     sgi.rb     tiff.rb    xpm.rb
ico.rb     pixmap.rb  ps.rb      sun.rb     window.rb

上記で得られない場合は、ruby -e 'puts $:' で 得られる各ディレクトリについて tkextlib/tkim/ を探せばよい。

画像の手配方法

作成したプログラムを他者に渡して利用してもらう場合、 以上2つの例では、画像ファイルをあらかじめ保存しておかせる必要がある。 利用者に画像を用意する手間を省かせたい場合は、

  1. ソースプログラムに埋め込んでロードする
  2. Web経由でロードする

などの方法が使える。それぞれの具体例を示す。

  1. base64でソースプログラムに埋め込む場合

    画像ファイルがあまり大きくない場合はこの方法が有効である。 まず、元画像をbase64エンコードした文字列に変換する。 以下のいずれかの方法で、エンコード文字列が得られることを確認する。

    mewencode image.jpg
    uuencode -m image.jpg image.jpg | tail +2
    ruby -rbase64 -e 'Base64.b64encode(ARGF.read)' image.jpg
    

    画像を使いたいRubyプログラムを開き、ヒアドキュメントで base64エンコード文字列を代入する。

    image = <<_EOS_
    
    _EOS_
    

    のように入力しておき、挟まれた部分にエンコード文字列を挿入する。

    実例を tk-imgheredoc.rb に示す。

  2. openurlでHTTPで取得する場合

    プログラムで利用する画像ファイルを、利用者が Web アクセスできる場所に置く。そのURLを open-url 拡張込みの open で開き、 read メソッドで全て読み取った文字列を TkPhotoImage.new に渡す。ただし、 通常行なわれる自動漢字コード変換をさせないよう、 Tk::BinaryString メソッドに渡した結果を渡す。

    tk-imghttp.rb

    #!/usr/koeki/bin/ruby
    # coding: euc-jp
    require 'tk'
    require 'open-uri'
    require 'tkextlib/tkimg/png'
    
    img = open("http://www.yatex.org/lect/ruby/star.png", "r") do |s|
      s.read
    end
    
    TkLabel.new() {
      image(TkPhotoImage.new("data"=>Tk::BinaryString(img)))
      bg("white")
    }.pack
    TkButton.new() {
      text("exit")
      command(proc {exit(0)})
    }.pack
    Tk.mainloop
    

フォント

文字を表示できるウィジェットでは、表示する文字のフォントを選べる。 フォントはフォントオブジェクト(TkFont)で指定する。

tk-font.rb

#!/usr/koeki/bin/ruby
# coding: euc-jp
require 'tk'

txv = TkVariable.new('24のフォント')
def enlarge(me)		# meには下のラベルオブジェクトが渡されて来る
  f = me.font
  v = me.textvariable
  f.size = (f.size.to_f*1.2).to_i
  v.value = sprintf("%dのフォント", f.size)
end

f = TkFont.new("ipagothic 24 italic")
TkLabel.new() {
  textvariable txv
  text("24のフォント")
  font(f)
  bind('Button-1', proc {enlarge(self)})
}.pack
Tk.mainloop

フォント名の指定は、Xのフォントファミリ名、サイズ、variant の3要素を空白で区切って指定する。後ろのものは省略可能。フォントファミリは xlsfonts コマンドの出力で得られるXLFD(X Logical Font Description)の 2つ目のハイフンの後ろにある名前で、システムにより利用できるフォントが 異なる。フォント名に空白が含まれる場合は、各要素を配列化するか、 空白を含む文字列部分を { } で囲む。 下記の2つは同じ指定となる。

f = TkFont.new(["vl gothic", 30])
f = TkFont.new("{vl gothic} 30")

フォント指定は必ずしもフォントオブジェクトを介さず、

TkLabel.new("text"=>"hello", "font"=>"times 24 bold").pack

のようにしてもよいが、その都度フォントオブジェクトが作られ、 あとから制御できないことから、共通フォントを複数のオブジェクトで 使う場合や、動的にフォントを変えたい場合はフォントオブジェクトを 利用した方がよい。

代表的なウィジェット

ラベル

tk の label に基づくラベルは 表示するのみのテキストを配置することを主目的としたウィジェットで、 テキストの色やフォントを変えたり、背景として画像を表示することが容易で、 手軽に使える。 最初の tk-helloプログラムが よい例となっている。

ポインティングデバイス関連のイベントに反応して色を変える例を示す。

tk-labelev.rb

#!/usr/koeki/bin/ruby
# coding: euc-jp
require 'tk'

TkLabel.new() {
  text("Hello, world!")
  bg("#fdd"); fg("black")
  bind("Enter", proc {bg("#dfd")})
  bind("Leave", proc {bg("#fdd")})
  bind("Button-1", proc {exit(0)})
}.pack
Tk.mainloop

メッセージ

複数行に渡る文章を提示するには message が使いやすい。Rubyでは TkMessage のオブジェクトとして 生成する。パラグラフの縦横比を百分率で指定する(aspect)か、 折り返し幅をピクセル数(数値)か、 文字幅(文字列)で指定する(width)。

tk-message.rb

#!/usr/koeki/bin/ruby
# coding: euc-jp
require 'tk'

TkMessage.new() {
  aspect(400)
  text("これはアスペクト比400%に指定したメッセージエリアである。
ソース中の改行はそのまま改行として反映されるが、
必ずしも改行させたくない\
位置にはバックスラッシュをいれる。")
  bg("pink")
}.pack
TkMessage.new() {
  width(200)
  text("これは幅200ピクセルに指定したメッセージエリアである。")
  bg("yellow")
}.pack
TkMessage.new() {
  width("10c")
  text("これは幅10cmに指定したメッセージエリアである。")
  bg("#aef")
}.pack

Tk.mainloop

ボタン

tkの button に基づくボタンも、ラベルと同様画像、テキストを設定できる。押したときの アクションは command メソッドにて指定するのは 既に出た例のとおり。

チェックボタン

チェックボタン(checkbutton)は、 ボタンが押されている(ON)か解除されている(OFF)かで、 2値を取得するメソッドである。Rubyでは TkCheckButton オブジェクトとして生成する。ボタンには状態を保存するための tk変数を割り当てて、それ経由で値を取得する。デフォルトではOFFのとき "0"を、ONのとき"1"が得られる。 tk変数は TkVariable で生成し、それを variable メソッドで割り当てる。ただし、このtk変数は、 チェックボタンの選択・解除操作をして初めて値が入るので、 チェックボタン生成時に選択(select)か、 解除(deselect)しておくほうがよい(例参照)。

tk-checkbutton.rb

#!/usr/koeki/bin/ruby
# coding: euc-jp
require 'tk'

v = TkVariable.new
TkCheckButton.new {
  text("抜ける")
  width("10")
  height("5")
  variable v
  deselect
}.pack
TkButton.new() {
  text(" GO! ")
  command(proc {
            if v == "1"
              puts "抜けます。"; exit 0
            else		# "0" のはず
              puts "まだまだ"
            end
          })
}.pack
Tk.mainloop

ラジオボタン

radiobutton は、複数のボタンでグループをなし、どれか1つだけが 選択された状態になるものである。TkCheckButton と ほぼ同様の使い方だが、同じtk変数を使うものどうしがグループとなる。 当該ボタンが押されたときにtk変数に設定する値は value メソッドで定義しておく。

tk-radiobutton.rb

#!/usr/koeki/bin/ruby
# coding: euc-jp
require 'tk'

v = TkVariable.new
TkRadioButton.new {
  text("あじ")
  variable v
  value "鯵"
}.pack("fill"=>'x')
TkRadioButton.new {
  text("いか")
  variable v
  value "烏賊"
}.pack("fill"=>'x')
TkRadioButton.new {
  text("うなぎ")
  variable v
  value "鰻"
}.pack("fill"=>'x')
TkButton.new {
  text("決定")
  command(proc {
            printf("%s食べよう!\n", v)
            exit 0
          })
}.pack
Tk.mainloop

文字列入力

1行内の短文入力には entry ウィジェットを利用する。Rubyでは TkEntry を利用する。

tk-entry.rb

#!/usr/koeki/bin/ruby
# coding: euc-jp
require 'tk'

# 入力中の値を別のウィジェットで共有させることができる。
# それには同じ TkVariable を指定する。
vname = TkVariable.new("名無")

# Entryに何をいれるべきかのラベルを付ける。
# 桁が揃った方が気持ちよいので、グリッドマネージャを使う。
TkFrame.new() {|f|
  # f はフレーム自身。内部でnewするウィジェットは親にfを指定すること
  # l1 = TkFrame.new(f) {|f2|
  #   bg("yellow")
  #   TkLabel.new(f2, "text"=>"名前?:", "justify"=>"left", "bg"=>"yellow") {
  #     pack("side"=>"left", "fill"=>"both")
  #   }
  # }
  l1 = TkLabel.new(f, "text"=>"名前は?:")
  e1 = TkEntry.new(f, "bg"=>"pink", "textvariable"=>vname)
  l2 = TkLabel.new(f, "text"=>"じゅうしょは?:")
  e2 = TkEntry.new(f, "bg"=>"pink")
  TkGrid(l1, e1, "sticky"=>"news")
  TkGrid.columnconfigure(f, 0, "weight"=>1)
  TkGrid(l2, e2, "sticky"=>"e")
  TkOption.add("*foreground", "#915711")
  n1 = TkLabel.new(f) {
    textvariable vname
  }
  n2 = TkLabel.new(f) {
    text("さん こんにちは")
  }
  TkGrid(n1, n2)
  TkButton.new(f) {
    text(" 登録 ")
    command(proc {
              printf("%sにおすまいの%sさんですね!\n5万円になります。\n",
                     e2.value, e1.value)
              exit(0)
            })
  }.grid("columnspan"=>2)
}.pack("fill"=>"both", "expand"=>true)
Tk.mainloop

テキストとスクロールバー

text ウィジェットは、短くないテキストの入力に有用で、Rubyでは TkText を利用する。入力された値は value で取得する。

tk-text.rb

#!/usr/koeki/bin/ruby
# coding: euc-jp
require 'tk'

TkLabel.new("text"=>"今日の一言").pack
txt = TkText.new("width"=>40, "height"=>3).pack
TkButton.new("text"=>"登録") {
  command(proc {
            printf("復唱: %s\n", txt.value) if txt.value > ""
            exit(0)
          })
}.pack("side"=>"left", "padx"=>5, "pady"=>5)
Tk.mainloop

限られた面積で長い文を入れさせたいときはスクロールバー (scrollbar) を付ける。 スクロールバーはテキストエリアと一体化させ、ウィンドウサイズを変えても 操作できるように、スクロールバーを先にpackする。

tk-scroll.rb

#!/usr/koeki/bin/ruby
# coding: euc-jp
require 'tk'

ysb = TkScrollbar.new.pack("fill"=>"y", "side"=>"right")
txt = TkText.new("width"=>40, "height"=>5, "bg"=>"#fee") {
  selectbackground("pink")
  yscrollbar(ysb)
}.pack("side"=>"right")
TkButton.new("text"=>"決定") {
  command(proc {
            printf("[%s]\n", txt.value)
            exit(0)
          })
}.pack("before"=>ysb, "side"=>"top")
Tk.mainloop

リストボックス

複数の候補から1つ、または複数の値を選ばせるときは listbox を用いる。Rubyでは TkListbox のオブジェクトを生成する。

選ばせるアイテムは insert メソッドで足して行く。 ユーザがアイテムの選択状態を変えるたびにインスタンス変数の curselection に選んだものの添字番号が入る。 デフォルトでは1つのアイテムしか選べないが、selectmode を変えることにより選択操作の体系が変わる。

single常に1つ選べる。
browse 常に1つ選べる。ボタン1で選択をドラッグできる。
multiple 複数選べる。ボタン1での選択が他の選択に影響を与えない。
extended 複数選べる。ボタン1単体で押すとそれを選んで他を解除する。 SHIFTを押しながらの範囲選択や、 CTRLを押しながらの追加選択/解除が使える。

tk-listbox.rb

#!/usr/koeki/bin/ruby
# coding: euc-jp
require 'tk'

TkLabel.new("text"=>"何ラーメンにしますか?", "bg"=>"white").pack("fill"=>"x")
TkListbox.new {|men| # Listboxウィジェット自身が men に入る
  mode = TkLabel.new("text"=>"(1杯のみ)").pack # あとで値を変える
  insert("end", "塩")		# 末尾にアイテムを追加
  insert("end", "しょうゆ")
  insert("end", "味噌")
  insert(2, "とんこつ")		# 2番目の位置にアイテムを追加
  insert("end", "四川")
  selection_set(1)		# 明示的に選択する
  pack("side"=>"right")
  # 以下のボタンではListboxを持つブロック変数(men)にアクセスしたいので
  # ブロック内に記述してグローバル変数化せずに済ます。
  TkButton.new("text"=>"1杯のみ") {
    command(proc{
              men.selectmode = "single"		# 1つだけ選べる
              mode.text("(1杯のみ)")		# 連動してラベルを変える
              # 選ばれたものの添字の配列が curselection に入っている
              men.curselection[1..-1].each do |i|
                men.selection_clear(i)	# 明示的に選択解除
              end
            })
  }.pack("fill"=>"x")           # デフォルトは "side"=>"top"
  TkButton.new("text"=>"何種類も") {
    command(proc{
              men.selectmode = "extended"	# 何個でも選べる
              mode.text("(何種類も)")		# 連動してラベルを変える
            })
  }.pack("fill"=>"x")
  TkButton.new("text"=>"決定") {
    bg("#efe")
    command(proc{
              for i in men.curselection
                printf("%sラーメン一丁\n", men.get(i))
              end
            })
  }.pack("fill"=>"x")
}
TkButton.new("text"=>"店を出る") {
  bg("#ecc");  command(proc{exit})
}.pack("side"=>"bottom", "fill"=>"x")

Tk.mainloop

アイテムが多いときは、スクロールバーを付けることもできる。

tk-listboxscr.rb

#!/usr/koeki/bin/ruby
# coding: euc-jp
require 'tk'

TkListbox.new() {
  yscrollbar(TkScrollbar.new.pack("fill"=>"y", "side"=>"right"))
  0.upto(100) do |i| insert("end", i.to_s+"番のアイテム") end
}.pack("side"=>"right")
TkButton.new("text"=>"quit", "command"=>"exit").pack
Tk.mainloop

スケール

数直線状のスケールで整数値を選べるウィジェット。

tk-scale.rb

#!/usr/koeki/bin/ruby
# coding: euc-jp
require 'tk'

v = TkVariable.new
TkScale.new() {
  variable v
  from	1
  to	12
  set	Time.now.month
  # showvalue false
  label "開始月"
  orient "horizontal"	# or "vertical"
  command(proc {STDERR.printf("\r%d", v)})
}.pack
# 同じtk変数で連動するウィジェットを作れる(なくてもよい)
TkEntry.new("textvariable"=>v).pack
TkButton.new("text"=>"Set") {
  command(proc {printf("\n%s", `cal #{v} #{Time.now.year}`); exit(0)})
}.pack
Tk.mainloop

スピンボックス

spinbox は、指定した範囲の数値をエントリボックスで直接入力させつつ、 マウスクリックでも数値の増減を制御できる(下図参照)。

spinbox

Rubyでは TkSpinbox で作成する。

tk-spinbox.rb

#!/usr/koeki/bin/ruby
# coding: euc-jp
require 'tk'

v = TkVariable.new
TkSpinbox.new() {
  textvariable v		# tk変数を利用するならtextvariableで
  to	12			# toを先に指定する必要がある。
  from	1
  font	"times 18"
  # values [1,3,5,7,8,10,12]	# 有効な値を配列で与えることも可
  set	Time.now.month
  width	4			# 入力窓の幅
  bg	"khaki"
  command(proc {STDERR.printf("\r%d", v)})
}.pack
TkButton.new("text"=>"Set") {
  command(proc {printf("\n%s", `cal #{v} #{Time.now.year}`); exit(0)})
}.pack
Tk.mainloop

メニュー

GUIアプリケーションのためのメニューは menu ウィジェットで作成する。Rubyからは、一括でメニューバーを作れる TkMenubar クラスを用いると手軽に構築できる

たとえば、以下のようなメニュー構成を作るものとする。

このメニュー階層を表す配列を TkMenubar に与えて以下のようにする。

tk-menubar.rb

#!/usr/koeki/bin/ruby
# coding: euc-jp
require 'tk'

menuspec=
  [[["F ファイル", 0],
    ["O 開く", proc {puts "open"}, 0],
    ["C 閉じる", proc {puts "close"}, 0],
    ["--"],
    ["Q 終了", "exit", 0]],
   [["E 編集", 0],
    ["C コピー", proc {puts "copy"}, 0],
    ["X カット", proc {puts "cut"}, 0],
    ["V ペースト", proc {puts "paste"}, 0]]]

TkFrame.new {|f|
  pack("fill"=>"x")
  TkMenubar.new(f, menuspec).pack("side"=>"left")
}
TkScrollbar.new {|s|
  pack("side"=>"right", "fill"=>"y")
  TkText.new("width"=>40, "height"=>10, "bg"=>"#f8f0f0") {
    yscrollbar(s)
  }.pack("side"=>"right")
}
Tk.mainloop

menuspec の値にある整数 0 はアクセラレータ指定で、 たとえば、

["Preference", proc {setpref()}, 7]

とすると、メニュー文字列 "Preference" の0から数えて7バイト目、つまり n に下線が引かれ、 キーボードで n をタイプしたときにその項目が選ばれるようになる。 日本語メニューを作りたい場合でもメニュー文字列を日本語だけにせず、 アクセラレータキーを先頭に書いておくとよい。

ただし、TkMenubar によるメニューにはカスケードメニュー (メニューの1アイテムを選ぶとさらにメニューが出てくるもの) は作れない。その他、メニューのアイテムにはチェックボタンや ラジオボタンも作れるが、これらは TkMenuTkMenubutton を直接制御する必要がある。 サンプルプログラムのリンクのみ示す。
tk-menu.rb

ダイアログ

ユーザになんらかの明示的な確認をさせたいときに新たな小ウィンドウを出して メッセージとともに確認ボタンを押させるものが messageBox で、Rubyでは Tk.messageBox メソッドで作成する(クラスではなくメソッド)。

Tk.messageBox(ハッシュ)

の形で、ハッシュには以下のキーと値の組み合わせが指定できる。

defaultデフォルトで選択されるボタン
icon アイコンの種類 ("error", "info", "question", "warning" のいずれかでデフォルトは "info")
message出力するメッセージ文字列
parent親ウィンドウ(その上に出現する)
titleウィンドウタイトルとする文字列
type提示されるボタンセットのタイプ
  • abortretryignore

    [abort] [retry] [ignore] の 3つのボタン

  • ok

    [ok] ボタンのみ

  • okcancel

    [ok] と [cancel] ボタン

  • retrycancel

    [retry] と [cancel] ボタン

  • yesno

    [yes] と [no] ボタン

  • yesnocancel

    [yes] [no] [cancel] の 3つのボタン

このメソッドを呼ぶと、選択されたボタンの名前の文字列が返る。 たとえば、

Tk.messageBox('type'=>'yesnocancel',
	'default'=>'cancel',
	'message'=>"ファイルがありません\n作成しますか",
	"icon"=>"question")

とすると、 Tk.messageBox のようなウィンドウが出され、そのままReturnを押すと デフォルトの Cancel が選ばれ、メソッドの返却値として "cancel" が返る。別のボタンを押すとそれに対応する値が 全部小文字の文字列として返る。

Canvas

丸や多角形、直線などの部品だけでなく、他のウィジェットの 土台ともなりうる多機能なウィジェットが canvas である。

Canvas内に配置できるそれ専用のウィジェットが各種揃っている。 それらは、Tkc で始まる名前のもので、newのときの 第1引数に親となるcanvasウィジェット、残りの引数に 座標や大きさなど、図形の形に則した値を指定して生成する。 一度生成した図形は自動的に親となるCanvasに貼り付けられ、 生成した後でも属性を変更することができる。

tk-canvas.rb

#!/usr/koeki/bin/ruby
# coding: euc-jp
require 'tk'

TkCanvas.new {|c|
  width(400)
  height(400)
  cx, cy = c.width/2, c.height/2
  r = 100
  # 円は、外接する四角形の対角頂点座標を4値で指定して生成する
  ovl = TkcOval.new(c, cx-r, cx-r, cx+r, cy+r,
                    'fill'=>'yellow', 'outline'=>'black')
  Thread.new {
    while true
      r = (r+8)%(width/2)
      # あとで円のパラメータを変更できる
      ovl.coords(cx-r, cx-r, cx+r, cy+r)
      sleep 0.0001
    end
  }
}.pack
TkButton.new("text"=>'quit', 'command'=>'exit').pack
Tk.mainloop

上記の例では Thread を利用しているが、 ツールキット付属のタイマである TkAfter 利用する方がよい。

TkAfter.new(ミリ秒, 繰り返し回数, 処理)

の形式でタイマオブジェクトを生成し、以下のメソッドで制御する。

start開始する
stop停める
cancel停める
skip処理を1回飛ばす
restart再開する
continue再開する(待ち時間を再設定可)
info情報配列を返す

第1引数の実行間隔は整数で与えることもできるが、 手続オブジェクトを渡して、その返却値で各回の待ち時間を動的に変えることも できる。第2引数に -1 を指定すると処理を実行し続ける。
参考: http://jp.rubyist.net/svn/rurema/doctree/trunk/refm/api/tk_off/tkafter.rd.off

弾むボール tk-ball.rb

#!/usr/koeki/bin/ruby
# coding: euc-jp
require 'tk'
require 'tkextlib/tkimg/png'

img = TkPhotoImage.new("file"=>ENV["IMG"]||"ball.png")
$top = img.height*2
$base = $top - img.height/2
$e = 0.8
def gety(t)
  # y = -(x-1)^2 + 1
  x = ((t-10)%20)/10
  y = -(x-1.0)**2 + 1.0
  return $base - $top*y/2
end

wait = TkVariable.new('50')
TkCanvas.new {|c|
  width img.width*3
  height $top
  cx = width/2; cy = $base
  ball = TkcImage.new(c, cx, cy, "image"=>img)
  i = 0.0
  tm = TkAfter.new(proc{val=wait.value}, -1, 
                   proc {ball.coords(cx, gety(i+=1))
                   }).start
  TkScale.new {
    variable wait
    to(100)
    from(1)
    label 'wait'
  }.pack('side'=>'left')
  TkButton.new("text"=>"stop", "command"=>proc{tm.stop}).pack("side"=>"left")
  TkButton.new("text"=>"start", "command"=>proc{tm.start}).pack("side"=>"left")
  TkButton.new("text"=>"quit", "command"=>"exit").pack("side"=>"left")
}.pack
Tk.mainloop

マウスでオブジェクトをつかみ、ドラッグで移動する

tkc-drag.rb

#!/usr/koeki/bin/ruby
# coding: euc-jp
require 'tk'

TkCanvas.new() {|c|
  width 400
  height 300
  TkcRectangle.new(self, 50, 50, 100, 80) {
    fill("yellow")
    ox, oy = nil, nil           # 以前の座標値
    bind("ButtonPress-1", proc {|x, y|
           ox, oy = x, y        # ボタン押された瞬間の座標を記憶
           c.cursor("fleur")    # 移動カーソルに変更
         }, "%x %y")            # %x %y はウィンドウ内の相対座標
    bind('Motion', proc {|x, y|
           next unless ox
           move(x-ox, y-oy)     # 前回からの差分だけ移動
           ox, oy = x, y
         }, "%x %y")
    bind('ButtonRelease-1', proc {
           c.cursor("")         # カーソルを戻す
           ox, oy = nil, nil}
         )
  }
}.pack
Tk.mainloop

Tkcウィジェット

Tkc* で始まる様々な図形オブジェクトを示す。 irbでルートウィジェットとCanvasを出しそれに順次以下の ウィジェット貼り付けて行くと分かりやすい。まず、irbを起動し スレッドで Tk.mainloop を起動しておく。

irb -rtk
cv = TkCanvas.new('width'=>400, 'height'=>300).pack
Thread.new { Tk.mainloop }

以下、cv の保有するキャンバスに貼り付けて行く 形式で例示を進める。

座標の取り方

Canvasにウィジェットを配置する位置を決めるときは、 Canvas内の相対座標を取得するリスナをバインドしておくとよい。 上記のcv変数にあるCanvasであれば、

cv.bind('1', proc{|x, y| printf("(%d,%d)\n", x, y)}, "%x %y")

としておくと、第1ボタンのクリックで知りたい位置の座標が分かる。

TkcTag

TkcTag を利用すると、 複数のキャンバスウィジェットをグループ化して、メンバー全ての 属性を変えるなどのことができる。上記の例の折れ線(l)と 画像(img)をまとめて移動してみる。

grp = TkcTag.new(cv)
l.tags = grp
img.tags = grp

grp.move(100, 50)
grp.move(-100, -50)

# state属性で隠す hidden, normal, disabled
grp.state = 'hidden'
grp.state = 'normal'

バウンディングボックス

単一ウィジェット、あるいはグループ化したウィジェット群を 全て包含する長方形の対角座標を bbox メソッドで取得できる。上記の例では、

grp.bbox
=> [100, 48, 220, 150]

この矩形に線を引けばドローイングツールの 矩形選択のような枠を描くことができる。

box = TkcRectangle.new(cv, grp.bbox, 'dash'=>'.')

Canvas内ウィジェットの検索

Canvasには子供となる多数のウィジェットを配置して使うことになるため、 処理対象称となる特定の子ウィジェットを選別する必要が発生する。 このときに使うのが TkCanvasfind メソッドである。

canvas.find(Command[, Args])

第1引数 Command の部分には以下のいずれかが指定できる。

以上のCanvasウィジェット群のirb操作をまとめたものを tkc-irb.rb に示しておく。

Canvasウィジェット使用例

cursesの例で示したキャラクタがジャンプするプログラム cur-jump.rb と同等のものをRuby/tkで作成したものを示す。

tkc-jump.rb

#!/usr/koeki/bin/ruby
# coding: euc-jp
require 'tk'
require 'tkextlib/tkimg/jpeg'
require 'tkextlib/tkimg/png'
require 'open-uri'

class Jump
  def initialize(width = 600, height = 400)
    @me = self
    dir='http://www.yatex.org/lect/ruby/'
    imgsrc = {'data'=>Tk.BinaryString(open(dir+'star.png'){|s| s.read})}
    @star = TkPhotoImage.new(imgsrc)
    imgsrc['data'] = Tk.BinaryString(open(dir+'shell.png'){|s| s.read})
    @kame = TkPhotoImage.new(imgsrc)
    imgsrc['data'] = Tk.BinaryString(open(dir+'shell2.png'){|s| s.read})
    @kame2 = TkPhotoImage.new(imgsrc)
    imgsrc['data'] = Tk.BinaryString(open(dir+'shell3.png'){|s| s.read})
    @kame3 = TkPhotoImage.new(imgsrc)
    @manimg = [@kame, @kame2, @kame3]
    @getstar = nil
    @st_x, @st_y = 400, 220
    @job = nil
    f = TkFrame.new()
    @btn = TkButton.new(f, "text"=>"Start").pack('side'=>'left')
    @bt2 = TkButton.new(f, "text"=>"QUIT",
                        'command'=>"exit").pack('side'=>'left')
    f.pack
    @stage = TkCanvas.new('width'=>width, 'height'=>height) {
      bind('1', proc{|x, y| printf("(%d,%d)\n", x, y)}, "%x %y")
    }.pack 
    @i_x, @i_y = 20, height-20
    @g_x, @g_y = width-@kame.width/2, @i_y
    @tgt = TkcImage.new(@stage, @st_x, @st_y, 'image'=>@star)
    @man = TkcImage.new(@stage, @i_x, @i_y, 'image'=>@manimg[0])
    @unit_x, @unit_y = 10, 20
    @jmax = 6; @jnow = 0
    @wait = 20 #msec
    me = @me
    @tx = TkcText.new(@stage, 10, 10,
                      "anchor"=>:nw, "text"=>"Start",
                      "font"=>"Times 24")
    @tx.bind('1', proc{reset(); @move.start()})
    TkRoot.bind('space', proc{jump()})
    # sleep を使ってはダメ!
    @move = TkAfter.new(@wait, -1, proc {mv()})
    TkRoot.bind('x', proc{reset(); @move.start})
    @btn.command = proc {reset(); @move.start}
  end
  def jump()
    @jnow = @jmax
  end
  def reset()
    @getstar = nil
    @tx.text = "Jump!"
    @x = @i_x
  end
  def mv()
    if @x < @g_x                # 右端に着く前は描画処理
      @y = @g_y - ((@jmax-@jnow)*@jnow/2)*@unit_y
      @man.coords(@x, @y)
      step = (@x-@i_x)/@unit_x/6%3
      @man.image = @manimg[step]
      if @stage.find('overlapping', *@man.bbox)[0] == @tgt
        @getstar = true
      end
      @x += @unit_x
      if @jnow > 0 then
        @jnow -= 1              # ジャンプ中の処理
      end
    else                        # 右端に着いたら終了
      @move.stop
      @tx.text = "You " + (@getstar ? "WIN!" : "Lose...")
    end
  end
end
k = Jump.new
Tk.mainloop

状態遷移の実現

tk-widgets.rb

リンク集


目次
[Arc]
start, extent
fill, outline
style = pieslice|chord|arc
tags
width

[bitmap]
anchor, foreground, background, bitmap, tags

[image]
anchor, image, tags

[line]
arrow = first|last|both|none
arrowshape = 
dash = 
capstyle = 
fill
joinstyle bevel|miter|round
smooth = true|false
splisteps = Number
stipple = Bitmap
tags, width

[rectangle]
fill, outline, stipple, tags, width
メモ
obj.configinfo で各種属性一覧
----------------------------------------------------------------------
http://www27.cs.kobe-u.ac.jp/~masa-n/misc/cmc/perltk/basic/grid.html
■ 相対配置

-row, -column で格子点の絶対値を指定する以外に,相対的に配置する方法も用
 意されています.行ごとに互いの縦横の関係を並べるものです.

$but00->grid($but01,    '-', $but03, -sticky=>'news');
$but10->grid(   '^', $but12,    'x', -sticky=>'news');
----------------------------------------------------------------------
packの属性
  :side		top, left, right, bottom
  :fill		x, y
----------------------------------------------------------------------
menu
  tearoff メニューを切り離せるか
----------------------------------------------------------------------
TkListbox の選択モード
  :selectmode	single, browse, multiple, extended
----------------------------------------------------------------------
text/TkEntry
   obj.focus	ここにフォーカス
   obj.cursor	カーソル位置
   obj.selection_range(0, 'end')
   sel.first, sel.last
----------------------------------------------------------------------
Checkbutton
  variable	変数
  text		説明文
  onvalue	on値
  offvalue	off値
  select/deselect
----------------------------------------------------------------------
Radiobutton
  variable	変数
  text		説明文
  value		それが選ばれたときの変数値
----------------------------------------------------------------------
Panedwindow
  add(obj, obj, ...) でペイン分けされたオブジェクト
----------------------------------------------------------------------
イベントシーケンス
	modifier-modifier-type-detail
  Activate, Deactivate
  Button(ButtonPress), ButtonRelease
  Circulate	ウィンドウの重なりが変わった
  Colormap	カラーマップが変わった
  Configure	ウィンドウの位置・サイズが変わった
  Destroy	強制終了された
  Enter		マウスポインタが入った
  Expose	ウィンドウが配置・表示された
  FocusIn, FocusOut
  Gravite	親ウィンドウのサイズ変更で自分が変化した
  Key(KeyPress), KeyRelease
  Leave		マウスが出た
  Map		ウィンドウがスクリーンに配置された
  Motion	ウィンドウ内部でのマウス移動
  Property	ウィンドウ属性が変わった
  Reparent
  Unmap		ウィンドウ最小化
  Visibility	表示属性変化
イベントキーワード
  %%	%自身
  %#	イベントのシリアルナンバー
  %a	above
  %b	ボタン番号
  %c	oountフィールド
  %h	height
  %w	width
  %k	keycode
  %s	state (VisibilityUnobscured等)
  %t	time
  %x, %X
  %y, %Y
  %A	ascii code
  %K	keysym
  %N	keysym number
  %T	type
  %W	path