これまで作成してきた対話的プログラムは,入力と出力がそれぞれ 1つの流れでできていた。 GUI(Graphical User Interface)プログラミングでは, 利用者が働きかけを行なう対象が,ボタン,入力窓,メニューなど複数あり, どんな順番で働きかけが来ても対応できる形となっていなければならない。 ユーザからのキーボードやポインティングデバイス(マウスなど)を用いた 働きかけのことをイベントといい,なんらかのイベントが発生したら それに対応してあらかじめ登録しておいたプログラム部分が動くような 構成となっている。このような動きを取るプログラムを イベント駆動型プログラムといい,GUIプログラムの 典型的な形式である。
GUIプログラムでは,ウィンドウ部品を作ったり,イベント処理を 行なうライブラリを用いて行なう。GUI用の部品が一式揃ったライブラリの ことをツールキットといい,言語や用途に応じて様々な ツールキットが存在するが,その利用の基本的な考え方は 共通している。
GUI用のツールキットは多種多様である。 現在でも利用されているもののうち最も長い部類の歴史を持つツールキットが Tcl/Tk(http://www.tcl.tk) である。元々 Tcl/Tk はGUIを含むスクリプトを簡単に作成できることを目指して作られた スクリプト言語(Tcl)とGUI用ツールキット(Tk)の合わさったものであるが, そのツールキット部分を他の言語から使えるようにしたものが徐々に増えた。 Ruby/tk はそのRuby版であり,Rubyの文法でtkを制御できる。 Tk がシンプルで手軽なツールキットという地位を1990年代から 変わらずに保ち続けていることからも, Tk の習得が比較的容易で, なおかつ今後も長期に渡って通用することが予想できる。 また,基本的な概念は後発のツールキットにも共通するため, GUIプログラム入門用としても有用であると言える。
他のGUIツールキットを利用したプログラミングと同様, Ruby/tkでもイベント駆動型プログラムを作成していく。 そのおおざっぱな流れとしては,
ウィンドウの部品(ウィジェット)を作成する。
作成した部品を土台部品に貼り付ける
どのイベントにどのアクションを起こすかを登録する。
メインループを呼ぶ
となる。イベントに対する反応をとくに決めないものなら3は不要である。 まずは,ウィジェットを出すだけの簡単なプログラムを示す。
#!/usr/koeki/bin/ruby
# -*- coding: utf-8 -*-
require 'tk'
TkLabel.new("text" => "Hello, world!").pack   # 1.と2.に相当
Tk.mainloop                                   # 4.に相当
TkLabelは,ラベルとなるクラスで new
によって,ひとつのラベルを生成する。ラベルは,文字や画像などを
表示するためのウィジェットである。引数にどのようなラベルを生成するかの
情報を持たせた属性値をハッシュ形式で与えると,それに応じた
ラベルオブジェクトを生成する。実際には生成するだけでは表示されず,
pack メソッドを用いて初めて表示される。
pack は,あるウィジェットをどのようにウィンドウ上に
配置するかを決めるメソッドである。このように配置を
司るものをジオメトリマネージャといい,GUI部品を
効果的に配置する重要な約割を担っている。他のツールキットにも同様のものがあり
レイアウトマネージャなどと呼ぶこともある。
最初の例 tk-hello.rb に,イベントに対する反応を
登録する部分を追加してみる。書き方は様々だが,いくつかの実例を含んだ
以下のプログラムを示す。
#!/usr/koeki/bin/ruby
# -*- coding: utf-8 -*-
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(シーケンス, 処理) 
の形式で指定する。シーケンスの指定方法を 説明する前に,いくつかの指定を含む例題プログラムを示す。
#!/usr/koeki/bin/ruby
# -*- coding: utf-8 -*-
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は修飾(モディファイア)を示すシンボルで, 指定できる代表的なものを示すと以下のようになる。
| シンボル | 意味 | 
|---|---|
Alt | Alt | 
Shift | Shift | 
Control | Control | 
Lock | CapsLock | 
Meta | Metaキー | 
Mod1 | Mod1 | 
Mod2 | Mod2 | 
Mod3 | Mod3 | 
Mod4 | Mod4 | 
Mod5 | Mod5 | 
Button1 | マウス第1ボタン | 
Button2 | マウス第2ボタン | 
Button3 | マウス第3ボタン | 
Button4 | マウス第4ボタン | 
Button5 | マウス第5ボタン | 
Double | 2連 | 
Triple | 3連 | 
Quadruple | 4連 | 
Mod1からMod5 は,ウィンドウシステムの
モディファイアキーで,X Window System では5種類のモディファイアキーが
利用できる。現在どのキーがモディファイアとして登録されているかは,
コマンドラインで
xmodmap
と起動してみれば分かる。
キー入力で複数のキーを続けて押したものを表したいときなど, 複数のイベントの組み合わせをバインドすることもできる。 これにはイベントシーケンスを配列化したもので表現する。たとえば,
bind(['Control-x', 'Control-c'], proc{exit})
とすると,C-x に続けて C-c を押したときの処理を定義することになる。ただしこの場合 C-x のみを押したときの処理が別に定義されている場合は C-x が押された段階でそちらも呼ばれることに注意する。
type の部分はイベントタイプで発生したイベントの 種別を表すシンボルである。代表的なものを以下に示す。
| シンボル | 意味 | 
|---|---|
Button | マウスボタンのクリック | 
ButtonRelease | 押されていたマウスボタンが離された | 
Key | キーが押された | 
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 オブジェクトに渡すブロックは
その位置で有効な変数が評価される。
#!/usr/koeki/bin/ruby
# -*- coding: utf-8 -*-
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("text"=>"Exit", "command"=>proc{exit}).pack
Tk.mainloop
この例は,後者のラベルでクリックすると変数 x
が未定義でエラーを起こすという分かりやすいものだが,
クラス定義を用いたスクリプトではスコープによるエラーを引き起こしやすい。
あえてエラーを起こすものを示す。
#!/usr/koeki/bin/ruby # -*- coding: utf-8 -*- require 'tk' class Test def hogehoge puts "Hoge!" end def initialize @hello = "Hello" @path = "pathpathpath" # Tkで用いている変数なので上書きはよくないが... ohayo = "Ohayo" TkLabel.new("text"=>"変数確認") { bind("1", proc { printf("ohayo=%s\n", ohayo.inspect) printf("@hello=%s\n", @hello.inspect) printf("@path=%s\n", @path.inspect) }) }.pack myself = self # 現在のオブジェクトをローカル変数に記録 TkLabel.new("text"=>"メソッド確認") { bind("1", proc { p self hogehoge # エラーになり処理はここで停止 self.hogehoge # これもエラー myself.hogehoge # これならOK }) }.pack TkButton.new("text"=>"Exit", "command"=>proc{exit}).pack end end Test.new Tk.mainloop
実際に起動して「変数確認」の部分をクリックすると以下のように出力される。
ohayo="Ohayo" @hello=nil @path=".w00000"
initialize メソッドで定義した変数のうち,
代入どおりに出ているのはローカル変数 ohayo だけである。
@hello と @pathは,
評価されるのが TkButton クラス内なので,
そこでの値が得られているのが分かる。本来 @path
変数はTkのオブジェクトの論理的な位置を示す値が入る。
また,「メソッド確認」の部分をクリックすると以下の出力後にエラーが発生する。
#<Tk::Button:0x7f7ff6289d80 @path=".w00001">
ボタン型のウィジェットでは,bindによるイベントハンドラの
登録をせずに,command メソッドでボタンをクリックしたときの
挙動を定義できる。
#!/usr/koeki/bin/ruby
# -*- coding: utf-8 -*-
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座標が得られる。
#!/usr/koeki/bin/ruby
# -*- coding: utf-8 -*-
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引数に空白区切りの
%置換文字列を指定すると,それらが手続きオブジェクトに
引数化されて渡される。
| %記号 | 意味 | 
|---|---|
%% | %自身 | 
%# | イベントのシリアル番号 | 
%d | detailフィールドの値 | 
%f | Enter/Leaveイベントでのフォーカス値(0か1) | 
%k | keycode値 | 
%x, %y | イベントのウィンドウ内の相対x座標・y座標 | 
%X, %Y | イベントのx座標・y座標 | 
%A | Unicode値 | 
%T | イベントの種別 | 
%W | イベントを捕捉したウィジェット | 
文字入力を主目的としないウィジェットでは,キー入力イベントを
捕捉するようにはできていない。ウィジェットの種類を問わず
ショートカットキーなどの即時キー入力処理を行ないたい場合は,
ルートウィジェット(Tk.root)に対してキー入力を
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 で,土台となるウィジェット(最初はルートウィジェット)の空き領域の どこに次のウィジェットを置くかおおざっぱに「上の方」,「下の方」, 「右の方」,「左の方」いずれかで指定して配置を決定する。
土台となる部品があり,その上に3つの部品を
という順番でpackを使って配置した場合次のような配置状態の遷移を取る。
pack("side"=>"top")
     | 部品1 | 
| (空き領域) | 
pack("side"=>"left")
     | 部品1 | |
| 部品2 | (空き領域) | 
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
を実行すると以下のような配置結果となる。

「部品1」の背景部分の範囲を見ると分かるように,
部品と土台に隙間ができる場合がある。
隙間を埋めたい場合は "fill" を指定する。指定できる値は
"x" | x軸方向(左右両側)を埋める | 
"y" | y軸方向(上下両側)を埋める | 
"both" | x・y軸方向(上下左右)を埋める | 
"none" | 埋めない | 
のいずれかで,たとえば上記の「部品1」の貼り付けを
pack("side"=>"top", "fill"=>"both")
に変えた場合は,以下のような配置結果となる。

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

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

部品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)
として出したウィンドウを大きくすると以下のように隙間がなくなる。

packジオメトリマネージャの配置を変えるための引数では, 以下のパラメータが使える。
"side" | 
  空き領域のどちら側に配置するか。"top"(上),
   "bottom"(下),
   "left"(左),
   "right"(右) | 
"fill" | 
  配置するウィジェットが割り当て区画より小さいときに引き延ばして
  隙間を埋めるか。"x", "y",
  "both", "none" のいずれかで指定。 | 
"expand" | 
  空き領域を埋めるように領域拡張するかtrueかfalse | 
"before" | 
  指定したウィジェットより前に配置 | 
"after" | 
  指定したウィジェットのあとに配置 | 
"ipadx" | 
  ウィジェットの左右の縁の内側の隙間間隔 | 
"ipady" | 
  ウィジェットの上下の縁の内側の隙間間隔 | 
"padx" | 
  ウィジェットの左右の縁の外側の隙間間隔 | 
"pady" | 
  ウィジェットの上下の縁の外側の隙間間隔 | 
"ipadx","ipady","padx",
"pady" に指定するのは長さで,
整数を指定するとピクセル,
単位付きの整数文字列を指定するとその単位での長さになる。単位は
のいずれかで,たとえば "pady"=>"5c" のように指定する。
各部品を表形式で格子状に並べるのに適しているのが grid ジオメトリマネージャである。
#!/usr/koeki/bin/ruby
# -*- coding: utf-8 -*-
require 'tk'
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列
とすると,以下のような配置結果が得られる。

格子の各マス目の幅と高さは各列,各行が同じになるように調整される。
マス目の幅・高さより小さいウィジェットは中央に配置され隙間ができる。
"sticky" 属性を指定して縁に密着させる辺を指定することができる。
"n" | 上辺を密着 (North) | 
"s" | 下辺を密着 (South) | 
"w" | 左辺を密着 (West) | 
"e" | 右辺を密着 (East) | 
の1字以上を指定してどの辺を密着させるか決める。たとえば,
上のtk-grid0.rb の
「L4」ラベルのgridで "sticky"=>"wes"
の指定を追加すると以下のようになる
(tk-grid1.rb)。
#!/usr/koeki/bin/ruby
# -*- coding: utf-8 -*-
require 'tk'
TkLabel.new() { # 0行0列
  text("ラベルの1番"); bg("green")}.grid("row"=>0, "column"=>0)
TkLabel.new() { # 0行1列
  text("ラベル2"); bg("pink")}.grid("row"=>0, "column"=>1)
TkLabel.new() { # 1行0列
  text("ラ\nベ\nル3"); bg("pink")}.grid("row"=>1, "column"=>0)
TkLabel.new() { # 1行1列
  text("L 4"); bg("green")}.grid("row"=>1, "column"=>1, "sticky"=>"wes")
Tk.mainloop
実行例:

さて,余白が気になるので,すべて余白を消すことを試みる。
4つのラベルすべてのgridに,"sticky"=>"news"
を追加すると初期ウィンドウから余白は消える
(tk-grid2.rb)
#!/usr/koeki/bin/ruby
# -*- coding: utf-8 -*-
require 'tk'
TkLabel.new() { # 0行0列
  text("ラベルの1番"); bg("green")}.
  grid("row"=>0, "column"=>0, "sticky"=>"news")
TkLabel.new() { # 0行1列
  text("ラベル2"); bg("pink")}.
  grid("row"=>0, "column"=>1, "sticky"=>"news")
TkLabel.new() { # 1行0列
  text("ラ\nベ\nル3"); bg("pink")}.
  grid("row"=>1, "column"=>0, "sticky"=>"news")
TkLabel.new() { # 1行1列
  text("L 4"); bg("green")
}.grid("row"=>1, "column"=>1, "sticky"=>"nwes")
Tk.mainloop
実行例:

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

ウィンドウサイズを変えたときに,中味のウィジェットも連動して 大きさを変える設定が可能である。これは,特定の列全体あるいは特定の行全体 に対して,拡大するときの他の列・行との伸縮負担の重み付けを行なうことで 制御する。上記の4ラベル配置例で,第0列と第1列の伸縮配分を1:3にするには 以下の文を追加する。
TkGrid.columnconfigure(Tk.root, 0, "weight"=>1) TkGrid.columnconfigure(Tk.root, 1, "weight"=>3)
#!/usr/koeki/bin/ruby
# -*- coding: utf-8 -*-
require 'tk'
TkLabel.new() { # 0行0列
  text("ラベルの1番"); bg("green")}.
  grid("row"=>0, "column"=>0, "sticky"=>"news")
TkLabel.new() { # 0行1列
  text("ラベル2"); bg("pink")}.
  grid("row"=>0, "column"=>1, "sticky"=>"news")
TkLabel.new() { # 1行0列
  text("ラ\nベ\nル3"); bg("pink")}.
  grid("row"=>1, "column"=>0, "sticky"=>"news")
TkLabel.new() { # 1行1列
  text("L 4"); bg("green")
}.grid("row"=>1, "column"=>1, "sticky"=>"nwes")
TkGrid.columnconfigure(Tk.root, 0, "weight"=>1)
TkGrid.columnconfigure(Tk.root, 1, "weight"=>3)
Tk.mainloop
第1引数の TkRoot は,今回gridジオメトリマネージャで土台
となっているウィジェットで,新たな土台を作らない場合の最初の土台は
Tk.Root,つまりルートウィジェットとなる。
この記述を追加したウィンドウを大きくすると以下のような結果となる。

上下に隙間があるのは,行方向の設定をしていないからで,
上下の隙間を埋めさせるためには TkGrid.rowconfigure
で同様の設定をすればよい。
place は,ウィジェットの配置位置をx座標,y座標で直接指定できる ジオメトリマネージャである。大きさの決まっている土台に 座標を決めて部品を置いたり,ウィジェット間に重なりのある 配置をしたい場合に有用である。
#!/usr/koeki/bin/ruby
# -*- coding: utf-8 -*-
require 'tk'
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

デフォルトでは配置するウィジェットの左上位置を基準とするが,
"anchor" 属性でこれを変えることもできる。
属性値には
"n" | 上辺中央 | 
"s" | 下辺中央 | 
"w" | 左辺中央 | 
"e" | 右辺中央 | 
"nw" | 左上角 | 
"ne" | 右上角 | 
"sw" | 左下角 | 
"se" | 右下角 | 
"center" | 中央 | 
のいずれかを指定する。
frame
は,複数のウィジェットを内部に配置するためのウィジェットで
Rubyでは TkFrame で生成する。
既に述べたとおり,1つの土台に対しては 1つのジオメトリマネージャしか使えない。 複雑な部品レイアウトを実現したいとき,複数のジオメトリマネージャを 組み合わせたいことがある。このような場合,複数のフレームを ルートウィジェットに配置し,さらに各フレームごとに違う ジオメトリマネージャを適用して内部のウィジェットを配置するようにするとよい。
たとえば次のようなレイアウトを考える。

上半分は何かの値の入力を促すラベルとエントリを対にしたものの集合, 下半分は左右に分かれたボタン。 上半分については,項目名とエントリの桁位置を揃えたいので grid ジオメトリマネージャを, 下半分についてはボタンを「左の方と右の方」とラフに置きたいので pack ジオメトリマネージャを使うことにする。
具体的な組み合わせ方としては,土台の上半分を占めるフレームを 上からpack,残った下半分を左と右からpack,さらに上半分の フレーム内をgridで制御してラベルとボタンを配置する。
      
  | ||||||||
| 左のボタン | 右のボタン | |||||||
このような配置を行なうプログラムの例を以下に示す。
#!/usr/koeki/bin/ruby
# -*- coding: utf-8 -*-
require 'tk'
=begin
+-------------------------+       +------------+
|住所      [            ] |       | j1   j2    |
|おなまえ  [       ]      |   =>  | n1   n2    |
|                         |       |            |
| [登録]         [クリア] |       | b1     b2  |
+-------------------------+       +------------+
=end
TkFrame.new {|f|
  # bg("yellow")	# レイアウトデバッグ時には背景色が有用
  j1 = TkLabel.new(f, "text"=>"住所")
  j2 = TkEntry.new(f, "width"=>20)
  n1 = TkLabel.new(f, "text"=>"おなまえ")
  n2 = TkEntry.new(f, "width"=>12)
  j1.grid("row"=>0, "column"=>0, "sticky"=>"w")
  j2.grid("row"=>0, "column"=>1, "sticky"=>"w")
  n1.grid("row"=>1, "column"=>0, "sticky"=>"w")
  n2.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
b1 = TkButton.new("text"=>"登録")	# 押しても何も起こらない
b2 = TkButton.new("text"=>"クリア")	# 押しても何も起こらない
b1.pack("side"=>"left", "padx"=>10, "pady"=>5)
b2.pack("side"=>"right", "padx"=>10, "pady"=>5)
Tk.mainloop
フレームウィジェットに限らず,新規のウィジェットを
フレームなど別の親の子として生成するときには,ウィジェット生成
のnewメソッドの第1引数に親とするウィジェットのオブジェクト
を指定する。
画像を扱うには, まず元となる画像ファイルを画像オブジェクトに変換し, そののち画像を配置できるウィジェットに貼り付けるという手順をとる。
tkの標準では gif, ppm, pgm のみ扱える。例としてgif画像
(cool.gif)
をラベル上に貼り付けて表示するものを示す。
cool.gif
を同一ディレクトリにコピーしてから実行する。
#!/usr/koeki/bin/ruby
require 'tk'
img = TkPhotoImage.new("file"=>"cool.gif")
TkLabel.new("image"=>img).pack
TkButton.new("text"=>"quit", "command"=>proc{exit(0)}).pack
Tk.mainloop
JPGやPNGなど,他の画像形式を利用する場合は,
tkextlib/tkimg/FORMAT が必要で,
たとえば,PNG画像を使うには
require 'tkextlib/tkimg/png'
を追加記述する。例として,透過部分を含むPNG画像
(nikusoba.png)
を表示するものを示す。
#!/usr/koeki/bin/ruby
# -*- coding: utf-8 -*-
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
ライブラリのあるディレクトリ中の tkimg/
にあるファイル一覧を見れば分かる。
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/tkimg/
を探せばよい。
なお,tkimg の利用には,Rubyのライブラリだけでなくシステムに
tkImg (http://sourceforge.net/projects/tkimg/)
パッケージが必要である。
作成したプログラムを他者に渡して利用してもらう場合, 以上2つの例では,画像ファイルをあらかじめ保存しておかせる必要がある。 利用者に画像を用意する手間を省かせたい場合は,
などの方法が使える。それぞれの具体例を示す。
画像ファイルがあまり大きくない場合はこの方法が有効である。 まず,元画像をbase64エンコードした文字列に変換する。 以下のいずれかの方法で,エンコード文字列が得られることを確認する。
uuencode -m image.jpg image.jpg | tail +2 ruby -rbase64 -e 'Base64.b64encode(ARGF.read)' image.jpg
uuencode プログラムは
BSD系やSolaris系システムでは標準装備されている。Linux系システムでは
sharutilsパッケージを追加インストールすることで利用できる。
画像を使いたいRubyプログラムを開き,ヒアドキュメントで base64エンコード文字列を代入する。
image = <<_EOS_ _EOS_
のように入力しておき,挟まれた部分にエンコード文字列を挿入する。
<< の次の行にポイントを置いて
	   C-SPC C-u M-| をタイプし,
	   エンコードするコマンドを入力する。
<< の行にポイントを置いて
	   :r! とタイプしてから
	   エンコードするコマンドを入力する。
実際のプログラムは以下のような構成になる。
#!/usr/koeki/bin/ruby
# -*- coding: utf-8 -*-
require 'tk'
require 'tkextlib/tkimg/jpeg'
shell = <<_EOS_                 # JPEG画像のbase64エンコード文字列
/9j/4AAQSkZJRgABAQEASABIAAD//gAGS2FtZf/bAEMAEQwNDw0LEQ8ODxMSERUaKxwaGB
  〜〜 この部分に Base64 文字列が続く 〜〜
IQgEIQgEIQgEIQgEIQgEIQg//Z
_EOS_
TkLabel.new() {
  image(TkPhotoImage.new("data"=>shell))
}.pack
TkButton.new() {
  text("kick")
  command(proc {exit(0)})
}.pack
Tk.mainloop
実例を tk-imgheredoc.rb
     に示す。
プログラムで利用する画像ファイルを,利用者が Web
     アクセスできる場所に置く。そのURLを
     
     open-uri 拡張込みの open で開き,
     
     read メソッドですべて読み取った文字列を
     TkPhotoImage.new に渡す。ただし,
     通常行なわれる自動漢字コード変換をさせないよう,
     Tk::BinaryString メソッドに渡した結果を渡す。
     
#!/usr/koeki/bin/ruby
# -*- coding: utf-8 -*-
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)で指定する。
#!/usr/koeki/bin/ruby
# -*- coding: utf-8 -*-
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要素を空白で区切って指定する(後ろのものは順に省略可能)。
tkで使えるフォントファミリの一覧は
TkFont.families で得られる。
irb -rtk print TkFont.families.sort.join(", ")
フォント名に空白が含まれる場合は,各要素を配列化するか,
空白を含む文字列部分を { } で囲む。
下記の2つは同じ指定となる。
f = TkFont.new(["vl gothic", 30])
f = TkFont.new("{vl gothic} 30")
また,フォントファミリ,サイズ,太さ,傾きを個別に
属性設定する指定方法もある。それぞれ,
"family", "size", 
"weight", "slant" で指定する。たとえば,
TkFont.new("mikachan 24 italic")
という指定は,
TkFont.new("family"=>"mikachan", "size"=>24, "slant"=>"italic")
と同様の指定に置き換えられる。
フォント指定は必ずしもフォントオブジェクトを介さず,
TkLabel.new("text"=>"hello", "font"=>"times 24 bold").pack
のようにしてもよいが,その都度フォントオブジェクトが作られ, あとから制御できないことから,共通フォントを複数のオブジェクトで 使う場合や,動的にフォントを変えたい場合はフォントオブジェクトを 利用した方がよい。
fnt = TkFont.new("family"=>"aquafont", "size"=>20)
l = TkLabel.new("text"=>"Hello", "font"=>fnt)
としてラベル生成しておくと,
fnt.family = "y.ozfont" fnt.size = 40
などとして,既存のラベルのフォントを変えられる。
GUI部品を作る上で有用なウィジェットを列挙する。
tk の
label
に基づくラベルは
表示するのみのテキストを配置することを主目的としたウィジェットで,
テキストの色やフォントを変えたり,背景として画像を表示することが容易で,
手軽に使える。
最初の tk-helloプログラムが
よい例となっている。
ポインティングデバイス関連のイベントに反応して色を変える例を示す。
#!/usr/koeki/bin/ruby
# -*- coding: utf-8 -*-
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
Enterイベント(ラベル内にマウスポインタが入ること)で背景色を #dfd (淡い緑)に,Leaveイベント(同じく出ること)で背景色を #fdd (淡い赤)に変更する。
複数行に渡る文章を提示するには
message
が使いやすい。Rubyでは TkMessage のオブジェクトとして
生成する。パラグラフの縦横比を百分率で指定する(aspect)か,
折り返し幅をピクセル数(数値)か,
文字幅(文字列)で指定する(width)。
#!/usr/koeki/bin/ruby
# -*- coding: utf-8 -*-
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)しておく方がよい(例参照)。
#!/usr/koeki/bin/ruby
# -*- coding: utf-8 -*-
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 メソッドで定義しておく。
#!/usr/koeki/bin/ruby
# -*- coding: utf-8 -*-
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 {
            if v.value == "" then     # 何も選んでいないとき
              puts("何か選んでね。")
            else
              printf("%s食べよう!\n", v.value)
              exit 0
            end
          })
}.pack
TkButton.new {
  text("リセット")
  command(proc {v.value = ""})
}.pack
Tk.mainloop
1行内の短文入力には
entry
ウィジェットを利用する。Rubyでは TkEntry を利用する。
入力窓のみのウィジェットであるため,その直前に入力ガイドを表示する
label を配置するのが望ましい。
entry ウィジェット利用時には
入力された値を保持するためのtk変数を割り当て,これを TkEntry
の textvariable メソッドで指定する。
このtk変数を他のウィジェットでも用いると,入力途中の値も共有される。
以下の例では,名前を入力するためのentryウィジェット(e1)と,
それとは全く別の label ウィジェット(n2) でtk変数
vname を共有させている。
#!/usr/koeki/bin/ruby
# -*- coding: utf-8 -*-
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
で取得する。
#!/usr/koeki/bin/ruby
# -*- coding: utf-8 -*-
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
textウィジェットの編集領域内には,特定の文字挿入ポイント (文字と文字の間)に好きな名前の「マーク」を付けられる。 また,2つのポイントで囲まれた領域に好きな名前の「タグ」を付けられる。 タグ付けされた領域はさらに,他の部分とは独立して属性を変えることができ, たとえば特定部分だけ背景色を変えたりフォントを変えたり, あるいはbindメソッドで特定のイベントに対するアクションを定義できたりする。
以下のマークとタグは特別な意味を持つ。
insert マーク | 
  次の入力文字が入る場所(テキストカーソルの左位置) | 
current マーク | 
  マウスポインタのある場所(流動的) | 
sel タグ | 
  選択された領域で次のコピーやカット操作の対象範囲 | 
これらの値は,ユーザのカーソル移動や領域選択操作によって自動的に 更新されると同時に,プログラムからも操作することができる。
textウィジェットの編集領域内に対して文字列挿入や領域削除など, 様々な操作を行なえる。操作対象となる場所の指定にはtk固有の index 記法を用いる。
base modifier modifier modifier ...
という書式で,base は基準となる位置,modifier
は,その位置からどれだけずらすかを指定する。
たとえば,"1.0 + 3 chars" という表記は
「1行目の0カラム目から3文字後ろ」を意味し,"1.0"
が base に,"+ 3 chars" が modifier
に相当する。base
として使える表記の主なものを示す。
line.char | 
  line 行目の char 文字目。行数は1から,
  文字数は0から数える。char に end
  を指定すると行末の改行位置を示す。 | 
@x,y | 
  textウィンドウの座標 (x, y) 位置に最も近い文字位置を示す。 | 
end | 
  テキスト末尾。 | 
mark | 
  mark という名前のマークの保持する位置。 | 
| tag.first | tag という名前のタグの保持する領域の開始位置。 | 
| tag.last | tag という名前のタグの領域の終了位置。 | 
また,位置指定の modifier には以下のものがある。
+ count chars | 
  基準位置から count 文字分後ろ | 
- count chars | 
  基準位置から count 文字分前 | 
+ count lines | 
  基準位置から count 行分下 | 
- count lines | 
  基準位置から count 行分上 | 
linestart | 
  基準位置の行頭 | 
lineend | 
  基準位置の行末 | 
wordstart | 
  基準位置の単語先頭 | 
wordend | 
  基準位置の単語末 | 
chars と lines
によるずらし量はテキスト全体の先頭や末尾を越えない。また,
count の前後の空白は省略してもよい。
insert(index, text
     [, *tags])
     位置 index に text を挿入する。 オプションの可変長引数 tags には任意個のタグ名リストを 指定でき,文字列挿入と同時に挿入部分に指定したタグを割り当てる。
mark_set(mark, index)
     mark という名前のマークに index
     の位置を設定する。特別なマーク "insert"
     に位置を設定するとカーソルがそこに移動する。
mark_unset(mark)
     mark という名前のマークを削除する。
index(mark)
     mark という名前のマーク位置を
     line.char
     の形式で取得する。
tag_add(tag, index1,
     index2)
     tag という名前のタグを index1 から index2 の領域で設定する。
tag_delete(tag)
     tag という名前のタグを削除する(複数指定可)。
see(index)
     位置 index が見える位置に(必要なら)スクロールする。
text_copy
     選択領域(selタグ)をクリップボードにコピーする。
text_cut
     選択領域(selタグ)をカットしクリップボードに記憶する。
text_paste
     クリップボードのテキストをペーストする。
その他,テキスト系ウィジェットへの操作メソッドは, http://docs.ruby-lang.org/ja/2.1.0/class/TkText.html に一覧がある。ただし,具体的な使い方の詳細は http://www.tcl.tk/man/tcl8.5/TkCmd/text.htm を併せて参照する必要がある(8.5の部分は導入されている TclTk のバージョンに置き換える)。
限られた面積で長い文を入れさせたいときはスクロールバー (scrollbar) を付ける。 スクロールバーはテキストエリアと一体化させ,ウィンドウサイズを変えても 操作できるように,スクロールバーを先にpackする。
#!/usr/koeki/bin/ruby
# -*- coding: utf-8 -*-
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
tk-scroll.rb を実行した結果を示す。
複数の候補から1つ,または複数の値を選ばせるときは
listbox
を用いる。Rubyでは TkListbox のオブジェクトを生成する。
選ばせるアイテムは insert メソッドで足していく。
ユーザがアイテムの選択状態を変えるたびにインスタンス変数の
curselection に選んだものの添字番号が入る。
デフォルトでは1つのアイテムしか選べないが,selectmode
を変えることにより選択操作の体系が変わる。
single | 常に1つ選べる。 | 
browse | 
  常に1つ選べる。ボタン1で選択をドラッグできる。 | 
multiple | 
  複数選べる。ボタン1での選択が他の選択に影響を与えない。 | 
extended | 
  複数選べる。ボタン1単体で押すとそれを選んで他を解除する。 SHIFTを押しながらの範囲選択や, CTRLを押しながらの追加選択/解除が使える。 | 
#!/usr/koeki/bin/ruby
# -*- coding: utf-8 -*-
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-listbox.rb を実行して,
[何種類も] ボタンを押して selectmode
を "extended" にしてから CTRL+クリック
で複数の候補を選択している様子を示す。
アイテムが多いときは,スクロールバーを付けることもできる。
#!/usr/koeki/bin/ruby
# -*- coding: utf-8 -*-
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
100個のダミーアイテムを生成したリストボックスに スクロールバーを付けた様子を示す。
一定範囲の整数を選ばせるためには数直線状のスケールを出す
scale
を用いる。Rubyでは TkScale を利用する。
1から12までの整数を数直線状のスケールで選べるプログラムの例を示す。
#!/usr/koeki/bin/ruby
# -*- coding: utf-8 -*-
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
このプログラムでは,入力値を保持するtk変数
v を TkEntry ウィジェットと共有させ,
スケール部分のスライドでも,入力窓への直接数値入力どちらでも
数値指定ができるようになっている。さらに,TkScale
ウィジェットで値が変更されたときの処理を command
で設定(STDERR.printf の部分)しているため,
端末画面にも選択途中の数値がその都度出力される。
spinbox は,指定した範囲の数値をエントリボックスで直接入力させつつ, マウスクリックでも数値の増減を制御できる(下図参照)。

Rubyでは TkSpinbox で作成する。
#!/usr/koeki/bin/ruby
# -*- coding: utf-8 -*-
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
scaleウィジェット同様,command
によって値が変更されたときの動きを定義できる。
GUIアプリケーションのためのメニューは
menu
ウィジェットで作成する。Rubyからは,一括でメニューバーを作れる
TkMenubar クラスを用いると手軽に構築できる
たとえば,以下のようなメニュー構成を作るものとする。
このメニュー階層を表す配列を TkMenubar
に与えて以下のようにする。
#!/usr/koeki/bin/ruby
# -*- coding: utf-8 -*-
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アイテムを選ぶとさらにメニューが出てくるもの)
は作れない。その他,メニューのアイテムにはチェックボタンや
ラジオボタンも作れるが,これらは TkMenu や
TkMenubutton を直接制御する必要がある。
メニューのみ作成するサンプルプログラムを示す。
#!/usr/koeki/bin/ruby
# -*- coding: utf-8 -*-
require 'tk'
TkOption.add("*font", "ipagothic 20")
menubar = TkFrame.new {
  relief	"raised"
  borderwidth	3
}.pack("fill"=>"x")
menu_f = TkMenubutton.new(menubar) {
  text "File"
  underline 0
}.pack("side"=>"left")
menu_e = TkMenubutton.new(menubar) {
  text "Edit"
  underline 0
}.pack("side"=>"left")
filemenu = TkMenu.new(menu_f, "title"=>"ファイルメニュー")
filemenu.add('command',
             "label"=>"O 開く",
             "command"=>proc {puts "open"},
             "underline"=>0)
filemenu.add('command',
             "label"=>"C 閉じる",
             "command"=>proc {puts "close"},
             "underline"=>0)
filemenu.add('separator')
filemenu.add('command',
             "label"=>"Q 終了",
             "command"=>"exit",
             "underline"=>0)
editmenu = TkMenu.new(menu_e, "title"=>"編集メニュー")
editmenu.add('command',
             "label"=>"C コピー",
             "background"=>"pink",
             "command"=>proc{puts "copy"},
             "underline"=>0)
editmenu.add('command',
             "label"=>"X カット",
             "command"=>proc{puts "cut"},
             # "columnbreak"=>1,	# 次の行に行く
             "underline"=>0)
editmenu.add('command',
             "label"=>"V ペースト",
             "command"=>proc{puts "paste"},
             "underline"=>0)
zoom = TkVariable.new("100")
zoommenu = TkMenu.new(menu_e)
zoommenu.add("radiobutton",
             "label"=>"50%",
             "variable"=>zoom,
             "value"=>"50",
             "command"=>proc{
               puts zoom.value
               zoom.value="50"
             },
             "underline"=>0,
             "indicatoron"=>true)
zoommenu.add("radiobutton",
             "label"=>"100%",
             "variable"=>zoom,
             "value"=>"100",
             "underline"=>0,
             "indicatoron"=>true)
zoommenu.add("radiobutton",
             "label"=>"200%",
             "variable"=>zoom,
             "value"=>"200",
             "underline"=>0,
             "indicatoron"=>true)
zoommenu.add("command",
             "label"=>"see zoom",
             "underline"=>0,
             "command"=>proc {STDERR.puts zoom.value})
editmenu.add('cascade',
             "label"=>"Z ズーム",
             "menu"=>zoommenu,
             "underline"=>0)
menu_f.menu(filemenu)
menu_e.menu(editmenu)
TkScrollbar.new {|s|
  pack("side"=>"right", "fill"=>"y")
  TkText.new("width"=>40, "height"=>10, "bg"=>"#f8f0f0") {|t|
    yscrollbar(s)
    # 第3ボタンクリックでzoommenuを出す
    bind('Button-3', proc{|x, y| zoommenu.popup(x, y)}, "%X %Y")
  }.pack("side"=>"right")
}
Tk.mainloop
このプログラムでは以下のような階層メニューが出る(実際に機能するのは 「ファイル→終了」のみ)。

ユーザになんらかの明示的な確認をさせたいときに新たな小ウィンドウを出して
メッセージとともに確認ボタンを押させるものが
messageBox
で,Rubyでは Tk.messageBox
メソッドで作成する(クラスではなくメソッド)。
独立したウィンドウを作成するため既存ウィジェットにpack
したりする必要はない。
ダイアログウィンドウの作成は,
Tk.messageBox(ハッシュ)
の形で行なう。 ハッシュには以下のキーと値の組み合わせが指定できる。
default | デフォルトで選択されるボタン | 
icon | 
  アイコンの種類
   ("error", "info", "question",
   "warning" のいずれかでデフォルトは "info")
   | 
message | 出力するメッセージ文字列 | 
parent | 親ウィンドウ(その上に出現する) | 
title | ウィンドウタイトルとする文字列 | 
type | 提示されるボタンセットのタイプ
				
  | 
このメソッドを呼ぶと,選択されたボタンの名前の文字列が返る。 たとえば,
Tk.messageBox('type'=>'yesnocancel',
	'default'=>'cancel',
	'message'=>"ファイルがありません\n作成しますか",
	"icon"=>"question")
とすると,
のようなウィンドウが出され,そのままReturnを押すと
デフォルトの Cancel が選ばれ,メソッドの返却値として
"cancel" が返る。別のボタンを押すとそれに対応する値が
全部小文字の文字列として返る。
丸や多角形,直線などの部品だけでなく,他のウィジェットの 土台ともなりうる多機能なウィジェットが canvas である。
Canvas内に配置できるそれ専用のウィジェットが各種揃っている。
それらは,Tkc で始まる名前のもので,newのときの
第1引数に親となるcanvasウィジェット,残りの引数に
座標や大きさなど,図形の形に則した値を指定して生成する。
一度生成した図形は自動的に親となるCanvasに貼り付けられ,
生成した後でも属性を変更することができる。たとえば,canvas内の
座標や大きさを決める属性値を後から変更し,
画面上でアニメーションのように動かしたりするなどのことが容易にできる。
#!/usr/koeki/bin/ruby
# -*- coding: utf-8 -*-
require 'tk'
w = 400
c = TkCanvas.new("width"=>w, "height"=>w).pack		# 正方形のCanvas
TkButton.new("text"=>'quit', 'command'=>'exit').pack	# 終了ボタン
r = 100                         			# 円の初期半径=100
width = c.width
cx, cy = c.width/2, c.height/2
# 円は,外接する四角形の対角頂点座標を4値で指定して生成する
ovl = TkcOval.new(c, cx-r, cx-r, cx+r, cy+r,
                  'fill'=>'yellow', 'outline'=>'black')
Thread.new {			# Tk.mainloopをメインスレッドで走らすため
  while true			# この無限ループは別スレッドで動かす
    r = (r+8)%(width/2)		# rを8ずつ増やして枠を超えたら戻るように
    # あとで(楕)円のパラメータを変更できる
    ovl.coords(cx-r, cx-r, cx+r, cy+r)
    sleep 0.1
  end
}
Tk.mainloop
上記の例では,0.1秒ごとに円の面積を変えている。
この部分は 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: utf-8 -*-
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
マウスでオブジェクトをつかみ,ドラッグで移動する
#!/usr/koeki/bin/ruby
# -*- coding: utf-8 -*-
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* で始まる様々な図形オブジェクトを示す。
irbでルートウィジェットとCanvasを出しそれに順次以下の
ウィジェットを貼り付けていくと分かりやすい。まず,irb
を起動しスレッドで Tk.mainloop を起動しておく。
irb -rtk cv = TkCanvas.new('width'=>400, 'height'=>300).pack Thread.new { Tk.mainloop }
以下,cv の保有するキャンバスに貼り付けていく
形式で例示を進める。
TkcRectangle.new(親, x1, y1, x2, y2)
     # (50,50) - (100,100) を頂点とする枠が青の長方形 r = TkcRectangle.new(cv, 50, 50, 100, 100, 'outline'=>'blue') # 塗りつぶしは fill で re = TkcRectangle.new(cv, 120, 170, 60, 230, 'fill'=>'green', 'outline'=>'orange')
枠の描き方に関して,以下の属性が設定できる。
outline | 枠の色 | 
fill | 塗りつぶしの色 | 
width | 枠の線の太さ | 
dash | 枠の線のパターン | 
これらの指定は,囲みのある他の Tkc
     ウィジェットに対しても指定できる。枠線パターンの指定を行なう
     dash については TkcLine
     のところで詳しく説明する。
TkcArc.new(親, x1, y1, y2, y2, "start"=>開始度,
     "extent"=>角度)
     
# ピンクに塗りつぶした 0°〜60°の扇形
a = TkcArc.new(cv, 50, 50, 100, 100, "start"=>0, "extent"=>60, "fill"=>"pink")
# 120度までに拡げてみる
a.configure('extent'=>120)
# 枠の外に移動
a.coords(50, 25, 100, 75)
# moveは相対移動
a.move(0, 25)
# スタイルを変える pieslice, chord, arc のいずれか
a.style = 'chord'
a.style = 'arc'
a.style = 'pieslice'
「XBMファイル」にはXBM形式(白黒)のファイル名を,
     「イメージオブジェクト」には TkPhotoImage
     で生成したものを渡す。ここでは,ネットワークの先(HTTP)から
     画像を取得してくるために open-uri ライブラリを,
     PNG画像を処理するために tkextlib/tkimg/png をロードする。
require 'open-uri'
require 'tkextlib/tkimg/png'
url = 'http://www.yatex.org/lect/ruby/star.png'
star = TkPhotoImage.new('data'=>Tk.BinaryString(open(url){|s| s.read}))
# 画像の中央が座標基準となるので扇形に重なる
img=TkcImage.new(cv, 100, 50, 'image'=>star)
# 消すには delete(他のオブジェクトも同じ)
img.delete
# 画像の左上を座標基準とするには 'anchor' 属性を 'nw' (North West)に
img=TkcImage.new(cv, 100, 50, 'image'=>star, 'anchor'=>'nw')
# TkcImageはそのままで画像を差し替える
require 'tkextlib/tkimg/jpeg'
url2='http://www.yatex.org/lect/ruby/shell.jpg'
kame = TkPhotoImage.new('data'=>Tk.BinaryString(open(url2){|s| s.read}))
img.image = kame
img.image = star
直線は折れ線用の TkcLine で描画する。
     繋ぎたいxy座標を任意個指定する。
l = TkcLine.new(cv, 111, 80, 212, 80, 121, 145, 162, 52, 191, 142, 111,80, 'fill'=>'red', 'width'=>3) # 矢印は arrow 属性: first, both, last ya = TkcLine.new(cv, 120, 200, 300, 200, 'arrow'=>'first', 'width'=>3) ya.arrow = 'last' ya['arrow'] = 'both'
矢尻の形を arrowshape で決めることができる。
     3つの整数値を指定し,それぞれ
を意味する。感覚的には第1値を大きくすると「太い」感じの 矢尻となり,第2,第3値の比率で第2値を大きくするとより尖った感じ 第3値を大きくするとより開いた感じになる。
矢印にしない場合の線の端点の形状を
     capstyle で変えられる。
     
     上から butt, round, projecting
| butt | 角ありで長さを変えない | 
| round | 丸く角取り | 
| projecting | 角ありで太さ分だけ長く | 
デフォルトはbutt。
     
     上から bevel, miter, round
折れ線の折れている部分の鋭角側の尖らせ方を
     joinstyle で変えられる。指定できるのは以下の3つ。
| bevel | 尖った部分を直線で切り落とす | 
| miter | 尖らせる | 
| round | 丸くする | 
デフォルトは round。
線のパターンを dash 属性で指定できる。
     数値,あるいは数値のリストを指定できる。数値は先頭から順に
     線を書く長さ,書かない長さ(ピクセル値)として解釈される。値に,
     ピリオド("."),
     カンマ(","),
     ハイフン("-"),
     アンダースコア("_")の各文字やその並びを指定
     することもできる。ピリオドが最も細かい点の並びで,
     カンマ,ハイフン,アンダースコアに行くに従って
     点線間隔が長くなる。なお,文字列で指定する場合の点間隔は,
     ピクセル値ではなく線の太さに対して相対的に解釈される。
     たとえば,"." で指定したときは
     [線の太さ×2, 線の太さ×4]
     と解釈される。また,文字列の途中に空白を含めると,
     その分だけ線を書かない間隔が取られる。
なお,dash は線を描く他の Tkc
     ウィジェットでも指定できる。
TkcLine と同様の座標指定で多角形を描く。
     終点と始点を結ぶ。
# シアンで塗りつぶした四角形 po = TkcPolygon.new(cv, 30, 30, 30, 160, 250, 150, 220, 35, 'fill'=>'cyan') # 折れ線でなくなめらかに. smooth属性の true/false で決める po.smooth = true # smooth=trueの場合に,何分割してなめらかにするか po.splinesteps = 2 po.splinesteps = 12 # 部品の上下関係を下に(lower)または上に(raise) po.lower img.raise l.raise
部品間の重なりを変える raise,lower
     の引数に別のウィジェットを指定すると,
     そのウィジェットのすぐ上か下になるよう配置する。
描きたい楕円を内接する長方形の対角2頂点を指定する。
cx, cy, r = 300, 200, 30 ov = TkcOval.new(cv, cx-r, cy-r, cx+r, cy+r, 'fill'=>'navy') # ↑は↓と同じ位置 # TkcOval.new(cv, 270, 170, 330, 230, 'fill'=>'navy')
指定した座標にテキストを配置する。デフォルトの anchor 属性は
     "center" でボックスの中央が配置座標の基準となる。
txt = TkcText.new(cv, 300, 120) {
  text 'ぐるぐる'
  fill 'darkgreen'
  font 'times 20'
}
中味のテキストの左右配置は justify
     で変えられる。left,right,
     center のいずれか。
上記のように配置済みのウィジェットの属性を後から変更して 移動や変形・変色などを自由に処理できる。
def guru(x, y, r, ya, ov)
  theta = 2*Math::PI*Time.now.to_f
  vx = x + r*Math.cos(theta)
  vy = y + r*Math.sin(theta)
  ya.coords(120, 200, vx, vy)
  ov.coords(vx-r, vy-r, vx+r, vy+r)
end
job = TkAfter.new(50, 100, proc {guru(cx, cy, r, ya, ov)})
job.start
座標の取り方
Canvasにウィジェットを配置する位置を決めるときは, Canvas内の相対座標を取得するリスナをバインドしておくとよい。 上記のcv変数にあるCanvasであれば,
cv.bind('1', proc{|x, y|
	printf("(%d,%d)\n", x, y)}, "%x %y")
としておくと,第1ボタンのクリックで知りたい位置の座標が分かる。
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には子供となる多数のウィジェットを配置して使うことになるため,
処理対象となる特定の子ウィジェットを選別する必要が発生する。
このときに使うのが TkCanvas の find
メソッドである。
canvas.find(Command[, Args])
第1引数 Command の部分には以下のいずれかが指定できる (指定時には他と区別の付く範囲で省略が可能)。
all
     すべての子ウィジェットを返す。
below,above
     第2引数にウィジェットID,または TkcTag
     のタグオブジェクトを指定し,そのウィジェット(群)よりも
     下(below)あるいは上(above)にあるウィジェット群を配列で返す。
cv.find('below', img.id)
→ imgウィジェットより下にあるウィジェット群の配列
      closest
     
canvas.find("closest", x, y)
のように指定し,Canvas内座標 (x,y) に最も近いウィジェットを返す。
enclosed
     
canvas.find("enclosed", x1, y1, x2, y2)
のように利用し,第2〜第5引数で指定した座標を対角頂点とする 矩形内部に含まれるウィジェット群を配列で返す。たとえば,
cv.find('enc', *img.bbox)
とすると,画像ウィジェット img
     を囲むバウンディングボックスが検索範囲となる。
メソッド呼び出しの引数の最後に渡す配列の前に *
     を付けると,配列が展開されてメソッドに渡される。たとえば,
     img.bbox の値が [100, 50, 220, 150]
     だとすると,
cv.find('enclosed', *img.bbox)
は,
cv.find('enclosed', 100, 50, 220, 150)
に開かれてメソッドが呼び出される。
overlapping
     上記 enclosed
     と同様に用い,重なりを持つウィジェット群を返す
withtag
     次の引数に指定した TkcTag オブジェクトに
     含まれるウィジェット群を返す。
以上のCanvasウィジェット群のirb操作をまとめたものを
tkc-irb.rb に示しておく。
# -*- coding: utf-8 -*-
cv = TkCanvas.new('width'=>400, 'height'=>300).pack
Thread.new { Tk.mainloop }
r = TkcRectangle.new(cv, 50, 50, 100, 100, 'outline'=>'blue')
re = TkcRectangle.new(cv, 120, 170, 60, 230,
                      'fill'=>'green', 'outline'=>'orange')
a = TkcArc.new(cv, 50, 50, 100, 100,
               "start"=>0, "extent"=>60, "fill"=>"pink")
a.configure('extent'=>120)
a.coords(50, 25, 100, 75)
a.move(0, 25)
a.style = 'chord'
a.style = 'arc'
a.style = 'pieslice'
require 'open-uri'
require 'tkextlib/tkimg/png'
url = 'http://www.yatex.org/lect/ruby/star.png'
star = TkPhotoImage.new('data'=>
	Tk.BinaryString(open(url){|s| s.read}))
img=TkcImage.new(cv, 100, 50, 'image'=>star)
img.delete
img=TkcImage.new(cv, 100, 50, 'image'=>star, 'anchor'=>'nw')
require 'tkextlib/tkimg/jpeg'
url2='http://www.yatex.org/lect/ruby/shell.jpg'
kame = TkPhotoImage.new('data' =>
	Tk.BinaryString(open(url2){|s| s.read}))
img.image = kame
img.image = star
l = TkcLine.new(cv, 111, 80, 212, 80,
                121, 145, 162, 52, 191, 142,
                111, 80, 'fill'=>'red', 'width'=>3)
ya = TkcLine.new(cv, 120, 200, 300, 200,
	 'arrow'=>'first', 'width'=>3)
ya.arrow = 'last'
ya['arrow'] = 'both'
po = TkcPolygon.new(cv, 30, 30, 30, 160, 250, 150, 220, 35,
                    'fill'=>'cyan')
po.smooth = true
po.splinesteps = 2
po.splinesteps = 12
po.lower
img.raise
l.raise
cx, cy, r = 300, 200, 30
ov = TkcOval.new(cv, cx-r, cy-r, cx+r, cy+r, 'fill'=>'navy')
txt = TkcText.new(cv, 300, 120) {
  text 'ぐるぐる'
  fill 'darkgreen'
  font 'times 20'
}
def guru(x, y, r, ya, ov)
  theta = 2*Math::PI*Time.now.to_f
  vx = x + r*Math.cos(theta)
  vy = y + r*Math.sin(theta)
  ya.coords(120, 200, vx, vy)
  ov.coords(vx-r, vy-r, vx+r, vy+r)
end
job = TkAfter.new(50, 100, proc {guru(cx, cy, r, ya, ov)})
job.start
grp = TkcTag.new(cv)
l.tags = grp
img.tags = grp
grp.move(100, 50)
grp.move(-100, -50)
grp.state = 'hidden'
grp.state = 'normal'
box = TkcRectangle.new(cv, grp.bbox, 'dash'=>'.')
cursesの例で示したキャラクタがジャンプするプログラム
cur-jump.rb
と同等のものをRuby/tkで作成したものを示す。
#!/usr/koeki/bin/ruby
# -*- coding: utf-8 -*-
require 'tk'
require 'tkextlib/tkimg/jpeg'
require 'tkextlib/tkimg/png'
require 'open-uri'
class Jump
  def initialize(width = 600, height = 400)
    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()
    @bt0 = TkButton.new(f, "text"=>"Start(TkAfter)").pack('side'=>'left')
    @bt1 = TkButton.new(f, "text"=>"Start(sleep)").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
    @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()})
    TkRoot.bind('q', proc{exit()})
    # TkAfterとsleep,それぞれでアニメーションを行なう
    @move = TkAfter.new(@wait, -1, proc {mv()})
    TkRoot.bind('x', proc{reset(); @move.start})
    @bt0.command = proc {reset(); @move.start}
    @bt1.command = proc {reset(); mvBySleep()}
  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
  def mvBySleep()		# 同じ処理をsleepで記述
    @x = @i_x
    while @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
      @jnow -= 1 if @jnow > 0
      @stage.update		# canvasをupdateしないと画面に出ない!!
      sleep(@wait/1000.0)
    end
    @tx.text = "You " + (@getstar ? "WIN!" : "Lose...")
  end
end
k = Jump.new
Tk.mainloop
Ruby/tkの情報を得るために有用なURLを示す。