Python Tkinter GUIプログラミング Text tag

 見に来てくださって、ありがとうございます。石川さんです。

 気が付けば、1ヶ月以上も更新していませんでした。採用活動とか、夏休みとか、もろもろあって、忙しかった、かも、と、言い訳してみましたが、よくよく考えてみると、Amazonプライムで海外ドラマの「Elementary」をずっと見ていたのが原因です。すみません。言い訳はよくないですね。

 さて、気を取り直して。ここのところ取り組んでいたtkinterの中にちょくちょく登場してくるタグ(tag)の考え方でちょっと気になったことを調べるためにプログラムを書いてみました。今回はTextで使われるタグについて、です。タグは通常、Textの文字列に色を付けたりフォントを変えたりするのにつかいます。気になりポイントは、同じ文字列にタグをセットしたとき、どちらが有効になるのか、ということです。

 急いでいる方のために結論を言いますと、タグは設定した順に積まれていき、最後の設定が有効になります。前に設定したものを優先するためには、積んだ順を入れ替えれば大丈夫です。

できあがりイメージ

タグの優先順位を変更するプログラム

 プログラムを実行すると、上部のテキストボックスに、赤、緑、青の文字列が初期値として表示された画面が表示されます。「Set Color」ボタンを押すことで、真ん中のラジオボタンで選択した色を、選択した文字列の文字色(foreground)か背景色(background)に、タグと色を付けます。

 下部の一覧は、タグ一覧です。行を選択して、「↑」、「↓」でタグを上下に移動、「DEL」ボタンでタグを削除できます。このボタンでタグを上下することで優先順位が変わった時の挙動が確認できます。行の内訳は、左側がタグ名で、括弧の中が[開始 – 終了]を表します。中の数字は、「行.列」を表していて、行は1オリジン、列は0オリジンの数値がセットされています。

 タグ一覧の中にある「sel」は、特殊なタグで、自動的に作成されます。現在選択されている範囲を表すタグです。

ソースコード

import tkinter as tk
from tkinter import colorchooser


class Application(tk.Tk):
    ''' Textタグの優先順位を確認します。
    タグをセットするためのTextの作成。
    色をセットするためのフレームの作成。
    タグの内容を表示するためのリストボックスを作成。
    リストボックスの名前を移動したり削除したりするためのフレームを作成。
    '''
    def __init__(self):
        super().__init__()
        self.title("Tag Priority")

        self.text = tk.Text(self)
        self.text.pack()
        self.text.bind("<KeyRelease>", self.refresh_list)
        self.text.bind("<ButtonRelease>", self.refresh_list)

        frame = tk.Frame(self)
        self.color = tk.StringVar(value="red")
        radio_red = tk.Radiobutton(frame, text="Red",
                                   variable=self.color, value="red")
        radio_red.pack(side=tk.LEFT)
        radio_green = tk.Radiobutton(frame, text="Green",
                                     variable=self.color, value="green")
        radio_green.pack(side=tk.LEFT)
        radio_blue = tk.Radiobutton(frame, text="Blue",
                                    variable=self.color, value="blue")
        radio_blue.pack(side=tk.LEFT)
        radio_color = tk.Radiobutton(frame, text="Color",
                                     variable=self.color, value="color",
                                     command=self.set_palet_color)
        radio_color.pack(side=tk.LEFT)

        self.color_palette = tk.Canvas(frame, width=20, height=20,
                                       background="white")
        self.color_palette.pack(side=tk.LEFT)
        self.color_palette.bind("<Button-1>", self.set_palet_color, add=True)

        self.option = tk.StringVar(self)
        self.option.set("foreground")
        combo = tk.OptionMenu(frame, self.option,
                              *{"foreground", "background"})
        combo.pack(side=tk.LEFT)

        button = tk.Button(frame, text="Set Color",
                           command=self.set_color)
        button.pack(side=tk.RIGHT)
        frame.pack()

        self.list = tk.Listbox(self)
        self.list.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)

        updown_frame = tk.Frame(self)
        up_button = tk.Button(updown_frame, text="↑", command=self.tag_up)
        up_button.pack()
        del_button = tk.Button(updown_frame, text="DEL", command=self.tag_del)
        del_button.pack()
        down_button = tk.Button(updown_frame, text="↓", command=self.tag_down)
        down_button.pack()
        updown_frame.pack(side=tk.LEFT)

        # テキストの初期値
        self.text.insert(1.0, "*****Red******\n*****Green****\n*****Blue*****")
        self.text.tag_add("red", 1.0, "1.end")
        self.text.tag_config("red", foreground="red")
        self.text.tag_add("green", 2.0, "2.end")
        self.text.tag_config("green", foreground="green")
        self.text.tag_add("blue", 3.0, "3.end")
        self.text.tag_config("blue", foreground="blue")

        self.refresh_list()

    def set_color(self):
        ''' 選択されたTextの文字列に色を付けます。 '''
        tag_range = self.text.tag_ranges('sel')
        if not tag_range:
            return
        option = self.option.get()
        color = self.color.get()
        if color == "color":
            color = self.color_palette.cget("background")
        tagname = "t_"+option+"_"+color+"_"+str(len(self.text.tag_names()))
        self.text.tag_add(tagname, *tag_range)
        self.text.tag_config(tagname, {option: color})
        self.refresh_list()
        for tag_name in reversed(self.text.tag_names()):
            print(tag_name, self.text.tag_ranges(tag_name))

    def refresh_list(self, event=None):
        ''' リストを更新します。 '''
        # pylint: disable=unused-argument
        self.list.delete(0, tk.END)
        for i, tag in enumerate(reversed(self.text.tag_names())):
            tag_range = self.text.tag_ranges(tag)
            if tag_range:
                self.list.insert(i, tag + f"[{tag_range[0]} - {tag_range[1]}]")
            else:
                self.list.insert(i, tag + "[未選択]")

    def tag_up(self):
        ''' タグを一行上に移動します。'''
        self.tag_move(-1)

    def tag_down(self):
        ''' タグを一行下に移動します。'''
        self.tag_move(1)

    def tag_move(self, offset):
        ''' タグを移動します。'''
        active_position = self.list.index(tk.ACTIVE)
        active_value = self.list.get(tk.ACTIVE)
        self.list.delete(active_position)
        self.list.insert(active_position + offset, active_value)
        self.list.activate(active_position + offset)
        self.list.see(active_position + offset)

        active_tag = active_value.split("[")[0]
        above_or_below_tag = self.list.get(active_position).split("[")[0]
        if offset == 1:
            self.text.tag_lower(active_tag, above_or_below_tag)
        else:
            self.text.tag_raise(active_tag, above_or_below_tag)
        print('*'*20)
        for tag_name in reversed(self.text.tag_names()):
            print(tag_name, self.text.tag_ranges(tag_name))
        print('*'*20)

    def set_palet_color(self, event=None):
        ''' パレットの色をセットするためのダイアログを呼び出し、取得した色をセットします。 '''
        # pylint: disable=unused-argument
        initial_color = self.color_palette.cget("background")
        color = colorchooser.askcolor(color=initial_color)
        if color:
            print(color)
            self.color_palette.configure(background=color[1])
        self.color.set("color")

    def tag_del(self):
        ''' リストで選択されたタグを削除します。'''
        active_position = self.list.index(tk.ACTIVE)
        active_value = self.list.get(tk.ACTIVE)
        self.list.delete(active_position)

        active_tag = active_value.split("[")[0]
        self.text.tag_delete(active_tag)


def main():
    ''' メインプログラム(呼び出し用) '''
    main_application = Application()
    main_application.mainloop()


if __name__ == "__main__":
    main()

 今回は、ソースコードにpylintで静的コード分析を実施してみました。意外といろいろとうるさくて、クラスドキュメンテーション文字列がない、メソッドドキュメンテーション文字列がない、変数名、メソッド名のルールがおかしい、使用していない引数がある、カンマの後ろに空白がない、コロンの後ろに空白がない、空行が足りない、等々、いろいろとご指摘いただきました。

 リファクタリングの提案もしてくれていて、クラスのアトリビュートが多すぎるので、見直した方がよいですよ、と、言われていました。

ソースコードの説明

 12行目、__init__(self)でこのアプリケーションの全体を定義しています。16~19行目でTextを作成、定義しています。イベントの<KeyRelease><ButtonRelease>が発生したときに、一覧を更新するように定義しています。何らかのキー入力が終了したときか、マウスボタンクリックが終わった時に、refresh_list()を呼び出します。

 21~51行目、ラジオボタン、カラーパレットのキャンバス、オプションメニュー、「Set color」ボタンを作っています。特に34行目のラジオボタン「Color」が押されたときはカラーパレットを表示するように「command=self.set_palet_color」を定義しています。ここの部分は、もし40行目のbindと同じようにマウスクリックに定義してしまうと、ラジオボタンが選択できなくなってしまいますので、注意が必要です。

 53、54行目、リストボックスを作成、56~63行目で、「↑」「DEL」「↓」のボタンを作成しています。

 65~72行目でTextの初期値をタグ付きの色をセットして、74行目でリストボックスのリフレッシュしています。

 76~90行は、set_color()メソッドで、選択されたテキストの範囲にタグを定義しつつ色をセットしています。現在選択されている範囲を取得するのに、「tag_range = self.text.tag_ranges('sel')」を使用できます。’sel’は「tk.SEL」として定義されていましたので、こちらを利用した方がよかったかもしれません。tag_ranges()で範囲が取得できないとタグをセットするときにエラーになりますので、チェックしています。
 89行目、self.text.tag_names()でタグ名の一覧が取得できますが、作成した順番に格納されていくので、スタックされているイメージに合いません。なので、組み込み関数のreversed()を使って逆順リストを出力するようにしました。

 103~129行目まで、タグが移動できるように定義しています。ポイントは123行目と125行目のself.text.tag_lower(active_tag, above_or_below_tag)self.text.tag_raise(active_tag, above_or_below_tag)です。これらのメソッドは、二つ目の引数がないと一番上か一番下までタグが移動してしまうので、設定しました。上下ボタンでひとつずつ移動してほしかったので。二つ目の引数で指定したタグの下、または上に移動します。

 131~139行目はカラーを選択してもらうためのカラーパレットを表示する機能です。ラジオボタンの「color」を選択するか、色をクリックすると以下のようなウィンドウで色を選択できるようにしています。単にcolor = colorchooser.askcolor(color=initial_color)と、呼び出すだけで使えます。あ、colorchooserは、先頭の方でfrom tkinter import colorchooserとインポートしてあります。

色の設定ダイアログ

 141~148行目まで、タグを削除しています。リストからは、self.list.delete(active_position)を使って削除して、タグの一覧からはself.text.tag_delete(active_tag)でタグを取り除きます。

 151~154行目は、main()関数を定義しました。これまでは、main_application = Application()の行を、if __name__ == "__main__":の下へ記述していましたが、それだとpylintに叱られてしまいまして。なんと、コンスタント値の変数名は、UPPER_CASEの形式で宣言することが望ましいとのことです。Pythonにはコンスタント値という考え方がないので、モジュールレベルの変数はすべてコンスタント値でしょ、というふうに考えられているようです。main()関数の中に記述することでモジュールレベルの変数になることを避ける、というのが一般的な解決方法のようです。

まとめ

 今回はTextで利用されているタグを調べました。タグを使うとテキストの文字色や、背景色、フォントの設定などを変更することができます。複数のタグを使用した場合の優先順位は最後に定義されたタグの方が優先されます。あとからtag_raise()メソッド、tag_lower()メソッドを使って優先順位を変更することも可能です。