Tk関係のおぼえがき

TkのGUIを思ったように出すにはいろいろマニュアルを読んで勉強する機会がありますが、 使わないとすぐに忘れてしまうもの。 ということで、たまにしか役にたたない連中の覚え書きです。

●スクロールバーのついたリストボックスを作るテンプレート
垂直だけバージョン

set f [frame .fa -rel groove]
set lsta [listbox $f.lsta -selectmode single -yscrollcommand "$f.scrv set"]
scrollbar $f.scrv -ori v -command "$f.lsta yview"
pack $f.lsta $f.scrv -fill y -side left

両方バージョン

set f [frame .fa -rel groove]
text $f.lsta -width 30 -height 10 \
  -xscrollcommand "$f.scrh set" -yscrollcommand "$f.scrv set"
scrollbar $f.scrh -ori h -command "$f.lsta xview"
scrollbar $f.scrv -ori v -command "$f.lsta yview"
grid $f.lsta -column 1 -row 1
grid $f.scrh -column 1 -row 2 -sticky ew
grid $f.scrv -column 2 -row 1 -sticky ns

テキストボックス版

set f [frame .fa -rel groove]
text $f.txaa -width 80 -height 10 \
  -xscrollcommand "$f.scrh set" -yscrollcommand "$f.scrv set"
scrollbar $f.scrh -ori h -command "$f.txaa xview"
scrollbar $f.scrv -ori v -command "$f.txaa yview"
grid $f.txaa -column 1 -row 1
grid $f.scrh -column 1 -row 2 -sticky ew
grid $f.scrv -column 2 -row 1 -sticky ns

ちなみに-relは-relief、-oriは-orientの省略です。

●ポップアップメニューのつくりかた
メニューは下のように普通のメニューと同様に適当につくります。

menu .menu
.menu add command -label Menu1 -command exit
.menu add command -label Menu2 -command exit
.menu add command -label Menu3 -command exit

tk_popupコマンドでポップアップします。
    tk_popup .menu %X %Y
このコマンドにはポップアップする位置をスクリーン上の座標で指定します。 普通、tk_popupはbindコマンドのスクリプトで使うので、 そのとき利用できる %X と %Y を使うと、現在マウスがある位置が指定できます。

●キャンバスのスクロールのさせかた
 キャンバスもテキストやリストボックスのようにスクロールさせることができます。 どこからどこまでスクロールできるかを -scrollregion で指定します。

canvas .can -scrollregion {0 0 1000 1000}
とすると、1000x1000の広さのキャンバスがある感覚で使えます。

●スクロールするキャンバス上の正しい座標を知るには
 上のようにしてキャンバスをスクロールさせているとき、 ボタンでキャンバスをクリックしたりして発生するイベントの %x、%yなどは、 スクロールしてない状態のキャンバスの左上からその位置の座標を返してきます。 たとえば、上の場合なら1000x1000の広さがあるキャンバスでも、画面上の大きさが 200x200だった場合は、%xも%yも零から200までの値しかとりません。 そうでなく、スクロールして(800,600)のような位置をとっている場合、 次のようにします。

set x [$can canvasx %x]
set y [$can canvasy %y]

AH!これは便利ライオン。
実験室ではうらのが下のやり方がベストだと思って誰も疑わなかったのですな。 書籍読み直そうぜ。

set screg    [$can cget -scrollregion]
set scwidth  [lindex $screg 2]
set scheight [lindex $screg 3]
set xfrac    [lindex [$can xview] 0]
set yfrac    [lindex [$can yview] 0]
set x [expr int($scwidth  * $xfrac + %x)]
set y [expr int($scheight * $yfrac + %y)]

掲示板で情報を頂きました。ありがとうございました。

●インストールしているフォントを使うには
 Xウィンドウシステムの場合は、あの長ったらしい名前(*1)、つまり 「-foundry-family-weight-slant-setwidth-addstyle-pixel-point-resx-resy-spacing-width-charset-encoding」 というスタイルの名前でシステムに入っている任意のフォントを指定することができます。 Windowsでも、fontというコマンドを使って、「\Windows\Fonts」 などにインストールしたフォントをTcl/Tkで使うことができます。

  font create Banco -family {Banco Normal} -size 16
  label .laba -text Hello -font Banco
「Banco Normal」というのは、\Windows\Fonts ディレクトリをエクスプローラで見たときに表示される名前です (TTFファイルのファイル名ではありません)。 これは、「ダイナフォント・タイプミュージアム」 という製品に入っている、少しコミカルな欧文フォントです。
(*1)XLFDというそうです。
ただし、 指定したフォントがインストールされていない環境ではへんな表示になることがあるので注意しないといけません。
 ちなみに、Windowsで日本語フォントのフォント名は普通日本語ですが、 Tcl/Tk 8.1ではこれらも指定できます。 ただし、スクリプトをシフトJISコードで書く必要があります。 下の例を見てください。簡単ですね。
wm title . {nihon marumoji international}
font create Marumoji -family {DFPまるもじ体} -size 24
message .laba -font Marumoji -fg red -bg white -width 300\
  -text {えー?うっそーまじー?やっだー
てゆーかー、やっぱヤだしー}
button .cmda -text {OK} -command exit
pack .laba .cmda

日本まるもじインターナショナルにようこそ!
わかった。いいから、もう帰れ!

●同じ種類のウィジェットのフォントを一括指定するには
optionコマンドを使うと楽です。下のように、Xリソース以外の書き方もできます。

option add *Listbox.font {Helvetica 10 bold}

Windowsでは日本語のフォント名も使えます。

option add *Label.font {{MS ゴシック} 10 bold}

●イベントを強制的に発生させるには
「event generate」というコマンドを使います。 例えば、テキスト上でリターンキーが押されたらまず行末に移動してから改行するには、

bind $text <KeyPress-Return> "event generate $text <Control-e>"

のようにして CTRL+E キーが押されたときと同じイベントを起こせばOKです。
なお、発生させるイベントが「ボタンが押されたのと同じ動作をさせる」というものならば、 「$button invoke」コマンドでOKです。よくある例ですが、 entryウィジェット(.e1とします)でリターンキーが押されたら、「実行」ボタン(.b1とします) が押されたのと同じ動作をさせるには、

bind $e1 <Key-Return> "$b1 invoke"

でOKです。

●entryウィジェットの入力文字数を制限するには
entryの-validateと-validatecommandオプションを使って、次のように書きます。

entry .e1 -width 8 -validate key -validatecommand {
    return [expr ([string length %P] > 8) ? 0 : 1]
}

これで、8文字までしか入力できなくなります。
-validateは、キー入力を検証する方式を指定するもので、none,focus,focusin,focusout,key,all の各種類があります。
none検証を行わない(デフォルト)
focusentryがフォーカスを得るか失うたびに検証を実行
focusinentryがフォーカスを得るたびに検証を実行
focusoutentryがフォーカスを失うたびに検証を実行
key文字単位で編集されるたびに検証を実行
all上記全ての場合に検証を実行
-validatecommandの値として、検証の内容をbindコマンドの引数と同じように Tclスクリプトで指定します。 このスクリプトは、最後に必ずreturnコマンドで1か0を返すように書かなければいけません。 1ならば入力内容が反映され、0なら拒否されます(=編集前の内容に戻されます)。 そしてbindコマンドと同様に、このスクリプトの中では幾つかの%記号による文字列置き換えができます。
%d何が起きてこの検証が実行されたか; 1=キー入力、0=DELキーによる削除、 -1=その他=フォーカスのIN/OUT、insertやtextvariableによる強制的な変更
%i追加、または削除されようとしている文字のインデックス。 追加や削除以外の場合は-1
%P今検証されている処理がOKだったと仮定した場合に、entryの内容となるであろう文字列
%s編集前のentryの内容
%S追加、または削除されようとしている文字。 追加や削除以外の場合は {}
%vこのentryに設定されている検証方式のリスト
%Vこの検証を起こした検証方式; key, focusin, focusout, forceのいずれか
%Wこのentryのウィジェット名
つまり上記の処理は、キーが押されるたびに検証し、キーが押された後になるであろう文字列の長さが8文字を越える場合は、 「return 0」で編集を拒否するので、8文字までしか入力できないことになります。
なお日本語の文字の場合ですが、上記の処理は「文字」単位なので、日本語の全角文字も1文字としてカウントされます。 バイト数で制限したい、というなら当然、上記のstring lengthをstring bytelengthに変えれば済むだろう、 という話なんですが、Tclの内部はUnicodeなので、日本語の全角文字は1文字3バイトとしてカウントされる点に大注意。 シフトJISや日本語EUCで何バイトまで、という制限をかけるには、さらに複雑な処理が必要になります。

●1つのスクロールバーで2つ以上の部品をスクロールさせるには
 これは、意外に難問です。 スクロールバーの-commandオプションで指定したスクリプトには、 自動的に後ろにそのときのスクロール状態を表す引数がつけられるので、 これを利用します。

    scrollbar .scrv -command "scroll2" -orient vertical
    # ...
    proc scroll2 {args} {
        eval .listbox1 yview $args
        eval .listbox2 yview $args
    }
evalが必要なのもわかりにくいですね。

●textvariableを使わずにエントリに表示させるには

    .entry delete 0 end
    .entry insert end $message
簡単でした。

●時間がかかるときにカーソルを砂時計にするには
 カーソルは部品の-cursorオプションで指定できます。どんなものが指定できるかは、 Tcl/Tkをインストールしたディレクトリの include/X11/cursorfont.h を見ればわかります。先頭の「XC_」を削った名前が使え、一般的な矢印は arrow、砂時計は watch です。

   .canvas configure -cursor watch
   # 時間がかかる処理
   .canvas configure -cursor arrow
これでOK、と思いますか?これだと、処理系サイドとしては、 ウィンドウのカーソルを変えて再表示する前に時間がかかる処理に突入してしまうことが多く、 普通カーソルが変わったことがわかりません。より確実には、 update か after コマンドを使います。
   .canvas configure -cursor watch
   update
   # 時間がかかる処理
   .canvas configure -cursor arrow
これらのコマンドをはさむことで GUIイベントをトラップするチャンスを処理系にいったんあげるのがみそです。

●ディスプレイの幅と高さを得るには

  set width  [winfo vrootwidth .]
  set height [winfo vrootheight .]
簡単、簡単。

●同じ部品に bind <KeyPress>とbind <Shift-KeyPress> を指定したらどうなる?
 例えば Shift+A を押すとき、普通はShiftキーをまず押し、押したままA キーを押しますが、このときはShiftキーが押された瞬間に bind <KeyPress> で指定した処理が実行されます。 このときの %K には "Shift_L" などの「きーしむ」(*1)が入っています。 で、Aキーを押した段階で bind <Shift-KeyPress> で指定した処理が実行されます。 このときは bind <KeyPress> で指定した処理は呼び出されません。 また %K は英大文字の "A" が入っています(*2)。

(*1)keysym。Xウィンドウシステム起源の言葉です。きーしむは、 普通の英文字のキーの場合はそのアルファベット、 記号やファンクションキーの場合は「comma」「F1」などの名前が決められています。
(*2)英文字のキーだけを押した場合とCtrlキーと同時に押した場合のきーしむは小文字ですが、 Shiftキーと同時に押した場合だけ、きーしむが大文字になります。

●メインウィンドウが出る前にダイアログを出したら以後操作できなくなった
例えば

tk_messageBox -message Hello
button .cmda -text Bye -command exit
pack .cmda
このように、プログラムの開始直後にいきなり tk_messageBoxやtk_getOpenFileなどのダイアログを使ってしまうと、 それが消えた後に出たメインウィンドウでの操作が一切できなくなります。 これと同じようなことをやらせるには、 Tk部品が「画面に出る」という意味のMapというイベントを使うとうまくいきます。
button .cmda -command exit
bind .cmda <Map> {
    tk_messageBox -message Hello
    .cmda configure -text Bye
}
pack .cmda

●階層状のメニューを作るには
 あるメニューボタンを選ぶと項目が出て、それを選ぶとまたメニューが出て… という階層状のメニュー構造は、 メニューに cascade 型の項目を加えればOKです。 例えば、「表示」→「並び替え」→「名前順」「日付順」 という階層メニューを作るには、

  frame .fm -rel ridge -bd 3
  menubutton .fm.mbview -text {表示} -menu .fm.mbview.m1
  pack .fm.mbview -side left

  menu .fm.mbview.m1
  .fm.mbview.m add cascade -label {並び替え} -menu .fm.mbview.m2
  menu .fm.mbview.m2
  .fm.mbview.m2 add command -label {名前順} -command {####}
  .fm.mbview.m2 add command -label {日付順} -command {####}
こんな感じになります。 下階層のメニューはどのウィジェットの子でも構わないようですが、 上のように最上層のメニューボタンの子にすればとりあえずOKです。

●ウィンドウのメニューバーのテンプレート
frameとmenubuttonではなく、toplevelの-menuオプションで指定するメニューバーのテンプレートです。

set m [menu .m]
$m add cascade -label ファイル -menu $m.mfile
$m add cascade -label 編集     -menu $m.medit

set mfile [menu $m.mfile]
$mfile add command -label "開く..." -command open_dummy
$mfile add command -label "名前をつけて保存..." -command saveas_dummy
$mfile add separator
$mfile add command -label "終了" -command exit

pack [canvas .can]

. configure -menu $m

●GUI部品の名前から、それを載せているウィンドウやフレームの名前を取り出すには
ぎゃはは。これは盲点でした。実験室では最近まで

regexp {^(.+)\.[^\.]+$} $childname all parentname
とこんなふうに強引に自分を乗せているウィンドウやフレーム(親ウィジェット) の名前をとりだしていましたが、
set parentname [winfo parent $childname]
で一発でした。

●disabledにしたエントリの動作が変
 ユーザーからの入力を受けつけないエントリを作るには entryコマンドに -state disabled スイッチをつければいいのですが、 これをするとプログラムの内部からもいっさいエントリの内容を変更できなくなります (エラーにはなりませんが、無視されます)。 プログラム内部から変更するには、まずエントリの-stateをnormalに戻し、 変更を加え、再度-stateをdisabledにします。

.entry configure -state normal
.entry delete 0 end
.entry insert end $newmessage
.entry configure -state disabled

●テキスト部品にセットしたばかりの文字列が元のと違う
たとえば

.text delete 1.0 end
.text insert end $string1
set string2 [.text get 1.0 end]
と、今テキスト部品にセットしたばかりの文字列string1をstring2に取り出したとき、 string2の内容はstring1とは違います。 (末尾に余計に改行文字が1コついています)

●Enterキーにボタンを反応させるには
ユーザーインターフェースの操作性を考えると、Enterキーが押されたとき、 フォーカスを持っているボタンにマウスで押したのと同じ動作をさせられると便利です。 これはスクリプトの冒頭に一行こう書いておけばOKです。

bind Button <Key-Return> {%W invoke}

●ウィンドウの「×」ボタンが押されたときに何か処理をするには
ウィンドウ右上の「×」ボタンが押されたときに、 何かの処理(典型的には、一時ファイルを削除するとか)をするには次のようにします。

wm protocol .window WM_DELETE_WINDOW deleteProc

proc deleteProc {} {
  tk_messageBox -message "Good Bye."
  exit 0
}

ここで、この処理中で終了(exit)しないようにしておけば、 実質的に「×」の動作を封じることができます。しかしその場合、特にWindowsでは、 他の終了手段をちゃんと設けておかないといけません。

●ラバーバンド
掲示板でご質問頂いた、ラバーバンド(マウスをクリックしたまま引っ張るとそれに応じて引かれる長方形) のサンプルです。

set can [canvas .can -bg white -width 300 -height 300]

bind $can <Button-1> {
    set startx [expr int([%W canvasx %x])]
    set starty [expr int([%W canvasy %y])]
}
bind $can <Button1-Motion> {
    set endx [expr int([%W canvasx %x])]
    set endy [expr int([%W canvasy %y])]
    if {[catch {%W coords $rect $startx $starty $endx $endy}]} {
        set rect [%W create rectangle $startx $starty $endx $endy]
    }
}
bind $can <ButtonRelease-1> {
    global ItemRect
    catch { %W delete $ItemRect }
    set ItemRect [%W create rectangle $startx $starty $endx $endy \
      -fill #000060 -outline #00ffff]
}
pack $can

●画像を連結する
image画像を横に連結するには、次のようにします。
(.jpg画像を読むにはImg拡張を入手し、「package require Img」を先に実行する必要があります。)

package require Img

image create photo Image1 -file IMAGE1.jpg
image create photo Image2 -file IMAGE2.jpg
image create photo Image3 -file IMAGE3.jpg

# Image1の右にImage2を連結する
Image1 copy Image2 -to [image width Image1] 0
# さらに右にImage3を連結する
Image1 copy Image3 -to [image width Image1] 0

label .la -image Image1
pack .la

●ウィンドウの透明度を変えて、フェードアウト、フェードインができるようにする
ウィジェット単独ではできませんが、ウィンドウ全体をフェードアウトさせることはできます。
wm attributes コマンドの -alpha オプションを使います。

wm attributes . -alpha 0.5

Windowsでは、-alphaの値は1が標準で、 小さくなるほど透明度が高く(つまり、見えなく)なっていきます。
また、ウィジェットはトップレベル(toplevel)のものしか指定できないので、 ラベルやキャンバスだけを明滅させることはできません。ちょっと残念。

●複数のリストボックスの選択状態を維持する
同じウィンドウに複数のリストボックスA,Bがあるとします。 普通にやると、Aからマウスで項目を一つ選択した後、 Bからマウスで項目を選択した瞬間、Aの選択状態がクリアされてしまいます。
Bの項目を選択しても、Aの選択状態がクリアされないようにするには、 listboxコマンドの-exportselectionオプションをfalse(または0、no)にすればOKです。

●リストボックスの下線を消す
listboxで項目を選択すると反転しますが、その前に以前選択していた項目に下線が表示されることがあります。 これを非表示にするには、-activestyleをnoneにすればOKです。これのデフォルトがunderlineです。

listbox .lsta -height 4 -font {Helvetica 24 bold} -listvariable gvListItems -activestyle none

やねうらの top
(first uploaded 2000/04/10 last updated 2022/04/24, MISUMI URANO)