これまで作成してきた対話的プログラムは、入力と出力がそれぞれ 1つの流れでできていた。 GUI(Graphical User Interface)プログラミングでは、 利用者が働きかけを行なう対象が、ボタン、入力窓、メニューなど複数あり、 どんな順番で働きかけが来ても対応できる形となっていなければならない。 ユーザからのキーボードやポインティングデバイス(マウスなど)を用いた 働きかけのことをイベントといい、なんらかのイベントが発生したら それに対応してあらかじめ登録しておいたプログラム部分が動くような 構成となっている。このような動きを取るプログラムを イベント駆動型プログラムといい、GUIプログラムの 典型的な形式である。
GUIプログラムでは、ウィンドウ部品を作ったり、イベント処理を 行なうライブラリを用いて行なう。GUI用の部品が一式揃ったライブラリの ことをツールキットといい、言語や用途に応じて様々な ツールキットが存在するが、その利用の基本的な考え方は 共通している。
他のGUIツールキットを利用したプログラミングと同様、 Ruby/tkでもイベント駆動型プログラムを作成していく。 そのおおざっぱな流れとしては、
ウィンドウの部品(ウィジェット)を作成する。
作成した部品を土台部品に貼り付ける
どのイベントにどのアクションを起こすかを登録する。
メインループを呼ぶ
となる。イベントに対する反応をとくに決めないものなら3は不要である。 まずは、ウィジェットを出すだけの簡単なプログラムを示す。
#!/usr/koeki/bin/ruby -Ke 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 -Ke
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 -Ke 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(['C-x', 'C-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 -Ke 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
ボタン型のウィジェットでは、bind
によるイベントハンドラの
登録をせずに、command メソッドでボタンをクリックしたときの
挙動を定義できる。
#!/usr/koeki/bin/ruby -Ke 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 -Ke 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 | イベントを捕捉したウィジェット |
文字入力を主目的としないウィジェットでは、キー入力イベントを 捕捉するようにはできていない。そのようなウィジェットでキー入力 による処理切り替えを行ないたい場合は、ルートウィジェットに対して キー入力を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" |
|
"expand" |
空き領域を埋めるように領域拡張するかtrue かfalse |
"before" |
指定したウィジェットより前に配置 |
"after" |
指定したウィジェットのあとに配置 |
"ipadx" |
ウィジェットの左右の縁の内側の隙間間隔 |
"ipady" |
ウィジェットの上下の縁の内側の隙間間隔 |
"padx" |
ウィジェットの左右の縁の外側の隙間間隔 |
"pady" |
ウィジェットの上下の縁の外側の隙間間隔 |
"ipadx"
、"ipady"
、"padx"
、
"pady"
に指定するのは長さで、
整数を指定するとピクセル、
単位つきの整数文字列を指定するとその単位での長さになる。単位は
のいずれかで、たとえば "pady"=>"5c"
のように指定する。
各部品を表形式で格子状に並べるのに適しているのが gridジオメトリマネージャである。
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
)。
さて、余白が気になるので、全て余白を消すことを試みる。
4つのラベル全てのgridに、"sticky"=>"news"
を追加すると初期ウィンドウから余白は消える
(tk-grid2.rb
)
gridで作成したマス目はウィンドウサイズを変えたときにも変わらない。 このため上に示したウィンドウを大きくしても周りに余白ができるだけである。
ウィンドウサイズを変えたときに、中味のウィジェットも連動して
大きさを変える設定が可能である。これは、特定の列全体あるいは特定の行全体
に対して、拡大するときの他の列・行との伸縮負担の重み付けを行なうことで
制御する。上記の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
、つまりルートウィジェットとなる。
この記述を追加したウィンドウを大きくすると以下のような結果となる。
上下に隙間があるのは、行方向の設定をしていないからで、
上下の隙間を埋めさせるためには TkGrid.rowconfigure
で同様の設定をすればよい。
placeは、ウィジェットの配置位置をx座標、y座標で直接指定できる ジオメトリマネージャである。大きさの決まっている土台に 座標を決めて部品を置いたり、ウィジェット間に重なりのある 配置をしたい場合に有用である。
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 -Ke 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
を同一ディレクトリにコピーしてから実行のこと。
#!/usr/koeki/bin/ruby -Ke 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 -Ke 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つの例では、画像ファイルをあらかじめ保存しておかせる必要がある。 利用者に画像を用意する手間を省かせたい場合は、
などの方法が使える。それぞれの具体例を示す。
画像ファイルがあまり大きくない場合はこの方法が有効である。 まず、元画像を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_
のように入力しておき、挟まれた部分にエンコード文字列を挿入する。
<<
の次の行にポイントを置いて
C-SPC C-u M-| をタイプし、
エンコードするコマンドを入力する。
<<
の行にポイントを置いて
:r! とタイプしてから
エンコードするコマンドを入力する。
実例を tk-imgheredoc.rb
に示す。
プログラムで利用する画像ファイルを、利用者が Web
アクセスできる場所に置く。そのURLを
open-url 拡張込みの open
で開き、
read
メソッドで全て読み取った文字列を
TkPhotoImage.new
に渡す。ただし、
通常行なわれる自動漢字コード変換をさせないよう、
Tk::BinaryString
メソッドに渡した結果を渡す。
#!/usr/koeki/bin/ruby -Ke 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 -Ke 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
プログラムが
よい例となっている。
ポインティングデバイス関連のイベントに反応して色を変える例を示す。
#!/usr/koeki/bin/ruby -Ke 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
)。
#!/usr/koeki/bin/ruby -Ke 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 -Ke 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 -Ke 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
を利用する。
#!/usr/koeki/bin/ruby -Ke 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 -Ke 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する。
#!/usr/koeki/bin/ruby -Ke 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を押しながらの追加選択/解除が使える。 |
#!/usr/koeki/bin/ruby -Ke 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
アイテムが多いときは、スクロールバーを付けることもできる。
#!/usr/koeki/bin/ruby -Ke 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
数直線状のスケールで整数値を選べるウィジェット。
#!/usr/koeki/bin/ruby -Ke 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 は、指定した範囲の数値をエントリボックスで直接入力させつつ、 マウスクリックでも数値の増減を制御できる(下図参照)。
Rubyでは TkSpinbox
で作成する。
#!/usr/koeki/bin/ruby -Ke 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
に与えて以下のようにする。
#!/usr/koeki/bin/ruby -Ke 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
を直接制御する必要がある。
サンプルプログラムのリンクのみ示す。
→ tk-menu.rb
ユーザになんらかの明示的な確認をさせたいときに新たな小ウィンドウを出して
メッセージとともに確認ボタンを押させるものが
messageBox
で、Rubyでは Tk.messageBox
メソッドで作成する(クラスではなくメソッド)。
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に貼り付けられ、
生成した後でも属性を変更することができる。
#!/usr/koeki/bin/ruby -Ke 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 -Ke 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 -Ke 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')
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'
イメージオブジェクトは 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 | 丸く角取り |
projection | 角ありで太さ分だけ長く |
デフォルトはbutt。
折れ線の折れている部分の鋭角側の尖らせ方を以下の3つから選べる。
bevel | 尖った部分を直線で切り落とす |
miter | 尖らせる |
round | 丸くする |
デフォルトはmiter。
線のパターンを dash
属性で指定できる。
数値、あるいは数値のリストを指定できる。数値は先頭から順に
線を書く長さ、書かない長さとして解釈される。値に、
ピリオド、カンマ、ハイフン、アンダースコアの各文字やその並びを指定
することもできる。この長さは、線の太さに対する相対値と解釈される。
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')
対角2頂点の座標を4値で指定する。
re = TkcRectangle.new(cv, 120, 170, 60, 230, 'fill'=>'green', 'outline'=>'orange')
指定した座標にテキストを配置。デフォルトでは 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('exc', *img.bbox)
とすると、画像ウィジェット img を囲むバウンディングボックスが 検索範囲となる。
メソッド呼出しの引数の最後に渡す配列の前に *
を付けると、配列が展開されてメソッドに渡される。たとえば、
img.bbox
の値が [100, 50, 220, 150]
だとすると、
cv.find('exclosed', *img.bbox)
は、
cv.find('exclosed', 100, 50, 220, 150)
に開かれてメソッドが呼び出される。
overlapping
enclosed と同様に用い、重なりを持つウィジェット群を返す
withtag
次の引数に指定した TkcTag
オブジェクトに
含まれるウィジェット群を返す。
以上のCanvasウィジェット群のirb操作をまとめたものを
tkc-irb.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
cursesの例で示したキャラクタがジャンプするプログラム
cur-jump.rb
と同等のものをRuby/tkで作成したものを示す。
#!/usr/koeki/bin/ruby -Ke 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