GUI

Python Tkinter Menus(2) メニューバー

メニューバーの作成方法、メニューバーの中身、メニューバーの使い方などについて説明する。

メニューバーとそのメニューのセットを適切に設計することについては説明しないが、もし自分以外の誰かのためにアプリケーションを作成する場合のアドバイスを以下にまとめる。

  • 多くのメニュー、非常に長いメニュー、深くネストされたメニューなどは避けるべきで、UIの構成方法を見直す必要があるかもしれない。
  • 多くの人は、初めて使用するときメニューを使って何ができるかを探るため、主要な機能にはメニューからアクセスできるようにすること。
  • 対象とするプラットフォームごとに、アプリケーションがどのようにメニューを使用するかを熟知しておくこと。(デザイン、用語、ショートカットなどの詳細については、各プラットフォームのUIのガイドラインを参照すること。おそらく各プラットフォームごとにカスタマイズする必要がある。)

メニューウィジェットと階層構造

メニューは、ボタンやエントリーと同じように、Tkではウィジェットとして実装されている。各メニューウィジェットは、メニュー内のいくつかの異なるアイテムから構成されている。アイテムには、アイテムに表示するテキスト、キーボードアクセラレータ、起動するコマンドなど、さまざまな属性がある。

メニューは階層的に配置される。メニューバーは、それ自体がメニューウィジェットである。メニューバーにはいくつかのアイテム(「ファイル」、「編集」など)があり、それぞれのアイテムはさらに多くのアイテムを含むサブメニューになっている。これらのアイテムには、「ファイル」メニューの「開く…」コマンドのようなものから、他のアイテム間のセパレータも含まれることがある。さらに、それ自身のサブメニューを開くアイテム(いわゆるカスケードメニュー)を持つこともできる。Tkの他の機能から予想されるように、サブメニューは常に親メニューの子として作成されなければならない。

メニューは古典的な Tk ウィジェットの一部で、テーマ別の Tk ウィジェットセットにはメニューウィジェットは存在しない。

始める前に

メニューの作成を始める前に、アプリケーションのどこかに次の行を入れる必要がある。

root.option_add('*tearOff', FALSE)

(これがない場合、(WindowsやX11の)各メニューは破線のようなもので始まり、メニューを「切り離す」ことができるようになる。その結果、メニューはそれ自身のウィンドウに表示される。現代のUIのスタイルにそぐわないため、アプリケーションから排除すべき。)

メニューバーの作成

Tkでは、メニューバーは個々のウィンドウに関連付けられ、各トップレベルウィンドウは最大で1つのメニューバーを持つことができる。これは Windows や多くの X11 システムでは視覚的に明らかで、メニューは各ウィンドウの一部であり、タイトルバーのすぐ下に位置している。

しかし、macOSでは、画面の上部に1つのメニューバーがあり、各ウィンドウで共有される。Tkプログラムに関する限り、各ウィンドウはまだ独自のメニューバーを持っている。ウィンドウを切り替えると、Tk は正しいメニューバーが表示されるようにする。特定のウィンドウにメニューバーを指定しない場合、Tk はルートウィンドウに関連付けられたメニューバーを使用する。

ウィンドウにメニューバーを作成するには、まず、メニューウィジェットを作成する。次に、ウィンドウのメニュー設定オプションを使用して、メニューウィジェットをウィンドウに取り付ける。

win = Toplevel(root)
menubar = Menu(win)
win['menu'] = menubar

メニューの追加

メニューバーを作成したら中身のメニューを作成する。メニューごとにメニューウィジェットを作成し、それぞれをメニューバーの子ウィジェットとして作成する。そして、それらをすべてメニューバーに追加する。

menubar = Menu(parent)
menu_file = Menu(menubar)
menu_edit = Menu(menubar)
menubar.add_cascade(menu=menu_file, label='ファイル')
menubar.add_cascade(menu=menu_edit, label='編集')

メニューアイテムを追加

メニューにはいくつかのアイテムを追加できる。

コマンドアイテム

通常のメニューアイテムは、Tkではコマンドアイテムと呼ばれている。メニューアイテムはメニューの一部であることに注意が必要。それぞれに個別のメニューウィジェットを作成する必要はない。(サブメニューの場合は後述する。)

menu_file.add_command(label='新規作成', command=newFile)
menu_file.add_command(label='開く', command=openFile)
menu_file.add_command(label='閉じる', command=closeFile)

各メニューアイテムには、ウィジェットの設定オプションに類似した、いくつかの設定オプションが関連付けられている。メニューアイテムの種類によって、利用できるオプションのセットは異なる。カスケードメニューアイテムには、サブメニューを指定するためのメニューオプションがあり、コマンドメニューアイテムには、アイテムが選択されたときに呼び出すコマンドを指定するためのコマンドオプションがある。どちらもラベルオプションがあり、アイテムに表示するテキストを指定する。

サブメニュー

既存のメニューにサブメニューを追加したい場合も、同じようにカスケードメニューアイテムを使用する。たとえば、「最近のファイル」サブメニューを作成するために使用できる。

menu_recent = Menu(menu_file)
menu_file.add_cascade(menu=menu_recent, label='最近のファイル')
for f in recent_files:
    menu_recent.add_command(label=os.path.basename(f), command=lambda f=f: openFile(f))

セパレータ

セパレーターは、区切り線を生成する。(異なるメニューアイテムの間に使用することが多い。)

menu_file.add_separator()

チェックボタン・ラジオボタンアイテム

checkbuttonとradiobuttonウィジェットと同様の動作をするcheckbuttonとradiobuttonのメニューアイテムがある。これらのメニューアイテムには、変数が関連付けられている。その値に応じて、ラベルの横にインジケータ(チェックマークや選択されたラジオボタンなど)が表示されることがある。

check = StringVar()
menu_file.add_checkbutton(label='チェック', variable=check, onvalue=1, offvalue=0)
radio = StringVar()
menu_file.add_radiobutton(label='イチ', variable=radio, value=1)
menu_file.add_radiobutton(label='ニ', variable=radio, value=2)

ユーザーがまだチェックされていないチェックボタンアイテムを選択すると、関連する変数にonvalueの値が設定される。すでにチェックされているアイテムを選択すると、offvalueの値が設定される。radiobuttonのアイテムを選択すると、関連する変数にvalueの値が設定される。両タイプのアイテムは、関連する変数に変更を加えた場合にも反応する。

コマンドアイテムと同様に、チェックボタンとラジオボタンのメニューアイテムは、メニューアイテムが選択されたときに呼び出されるコマンド設定オプションをサポートしている。関連する変数とメニューアイテムの状態は、コールバックが起動される前に更新される。

メニューアイテムを操作する

メニューの末尾にアイテムを追加するだけでなく、insertメソッドを使うことでメニューの途中にアイテムを挿入することも可能。また、deleteメソッドを使えば、1つまたは複数のメニューアイテムを削除することができる。

menu_recent.delete(0, 'end')

アイテムはインデックスで参照され、メニュー内のアイテムの位置を示す数値(0〜n-1)である。また、メニューアイテムのラベルを指定することもできる(実際には、アイテムのラベルとマッチする「グロブスタイル」パターンを指定することもできる)。

print( menu_file.entrycget(0, 'label')) # メニューの一番上のアイテムのラベルを取得する
print( menu_file.entryconfigure(0))     # アイテムのすべてのオプションを表示する

状態

メニューアイテムを無効にして、ユーザーが選択できないようにすることができる。これは、状態オプションを使用して、値disabledに設定することで可能。値をnormalにすると、そのアイテムが再び有効になる。

メニューは常にアプリケーションの現在の状態を反映したものであるべき。メニューアイテムが現在関連していない場合(例えば、「コピー」アイテムは、アプリケーション内の何かが選択されている場合にのみ適用される)、そのアイテムを無効にする必要がある。アプリケーションの状態が変化して、そのアイテムが適用できるようになったら、必ずそのアイテムを有効にすること。

menu_file.entryconfigure('Close', state=DISABLED)

メニューアイテムが無効になるのではなく、アプリケーションの状態の変化に応じて名前が変わるメニューアイテムがある場合がありある。例えば、ウェブブラウザには、ブックマークペインが隠されたり表示されたりすると、「ブックマークを表示」と「ブックマークを隠す」の間で変化するメニューアイテムがあるかもしれない。

menu_bookmarks.entryconfigure(3, label="ブックマークを隠す")

アクセラレータキー

アクセラレータオプションは、メニューアイテムに対応するキーボードの等価物を示すために使用される。これは実際にアクセラレータを作成するのではなく、メニューアイテムの横に表示するだけ。アクセラレータのイベントバインディングを自分で作成する必要がある。

アクセラレータは、どのキーがどの操作に使われるかだけでなく、メニューアクセラレータにどの修飾キーが使われるか(例えば、macOSでは「コマンド」キー、WindowsやX11では通常「コントロール」キー)など、プラットフォームによって異なる。有効なアクセラレータオプションの例としては、Command-N、Shift+Ctrl+X、Command-Option-Bがある。一般的に使用される修飾語は、Control、Ctrl、Option、Opt、Alt、Shift、「Command」、Cmd、Meta。

m_edit.entryconfigure('Paste', accelerator='Command+V')

アンダーライン

すべてのプラットフォームで、矢印キーによるメニューバーのキーボード操作をサポートしている。Windows と X11 では、他のキーを使って特定のメニューやメニューアイテムにジャンプすることもできる。これらのジャンプのきっかけとなるキーは、メニューアイテムのラベルに下線付きの文字で表示されている。メニューアイテムにこれらのキーを追加するには、そのアイテムの下線設定オプションを使用する。その値は、下線を引きたい文字のインデックス(0から文字列の長さ-1まで)である必要がある。アクセラレータキーとは異なり、メニューはキーストロークを監視するため、個別のイベントバインディングは不要。

m.add_command(label='Path Browser', underline=5)  # "B"にアンダーラインを設定する

画像

メニューアイテムのラベルの横に、またはラベルを完全に置き換えて、メニューアイテムに画像を使用することも可能。このためには、ラベル・ウィジェットの場合と同様に、imageとcompoundオプションを使用する。imageの値はTk画像オブジェクトでなければならず、compoundの値はbottom, center, left, right, top, noneでなければならない。

メニュー 仮想イベント

メニューに関するプラットフォームの規約は、ほとんどのアプリケーションで利用可能な標準的なメニューとアイテムを示唆している。例えば、ほとんどのアプリケーションには「編集」メニューがあり、「コピー」「貼り付け」などのメニューアイテムがある。エントリやテキストなどの Tk ウィジェットは、これらのメニューアイテムが選択されると、適切に反応する。

Tk はこれを仮想イベントによって処理する。Tk の概念の章の通り、これは高レベルのアプリケーションイベントであって、低レベルのオペレーティングシステムのイベントではない。Tk のウィジェットは、特定のイベントを監視する。メニューを作成するとき、コールバック関数を直接呼び出すのではなく、それらのイベントを生成することができる。アプリケーションは、これらのイベントを監視するためのイベントバインディングを作成することもできる。

以下では「編集」メニューに、標準的な「貼り付け」アイテムと、アプリケーション固有の「検索」というアイテムを追加して、何かを探したりするためのダイアログを開く方法を、最小限の例として紹介する。ここでは、”貼り付け”が機能することを確認できるように、入力ウィジェットを追加している。

from tkinter import *
from tkinter import ttk, messagebox

root = Tk()
ttk.Entry(root).grid()
m = Menu(root)
m_edit = Menu(m)
m.add_cascade(menu=m_edit, label="編集")
m_edit.add_command(label="貼り付け", command=lambda: root.focus_get().event_generate("<<Paste>>"))
m_edit.add_command(label="検索", command=lambda: root.event_generate("<<OpenFindDialog>>"))
root['menu'] = m

def launchFindDialog(*args):
    messagebox.showinfo(message="ファイルを探す")
    
root.bind("<<OpenFindDialog>>", launchFindDialog)
root.mainloop()

Tkは以下の仮想イベントを事前定義している。<<Clear>>, <<Copy>>, <<Cut>>, <<Paste>>, <<PasteSelection>>, <<PrevWindow>>, <<Redo>>, <<Undo>> 。詳しくは、イベントコマンドリファレンスを参照すること。