at backyard

Color my life with the chaos of trouble.

tkinterでPython GUIプログラミング入門

PySimpleGUIは触っているが、その下のレイヤーで動くtkinter自体は触ったことがなかったので、このタイミングで入門してみようと思う。

一通りtkinterを使ってGUIアプリを作れるようになるところまでをこの記事内に書いていく。

長くなると思うので目次を要参照。

目次

環境

自身の試す環境は下記の通り。

参照するドキュメント

tkinterについて調べようとしたのだが、困ったことにまとまった公式のドキュメントが非常にわかりにくい or 古くて今でも有効なのかがよく分からなかった。

日本語でまとめられているドキュメントでPython 3系での記述もありのドキュメントを見つけたので、こちらを参照しながら一つずつ試してみようと思う。
(なお、途中で内容はアレンジしたり適宜飛ばしたりしていますのでご了承ください。気になる方は下記の記事もチェケラ!)

https://nnahito.gitbooks.io/tkinter/content/

ウィンドウだけを表示する

シンプルにウィンドウだけを表示する

import tkinter as tk
window = tk.Tk()
window.mainloop()

テキスト(Label)をウィンドウ上に表示させる

テキスト表示のサンプル

import tkinter as tk
window = tk.Tk()

label = tk.Label(text="ハローワールド")
label.pack()

window.mainloop()

一気に画面が小さくなった。。。

ウィンドウのサイズを指定

下記の方法(コメント参照)でウィンドウサイズを指定できる。

import tkinter as tk
window = tk.Tk()

# windowのサイズを指定
window.geometry("400x300")

label = tk.Label(text="ハローワールド")
label.pack()

window.mainloop()

ウィンドウタイトルを指定する

ウィンドウタイトルの指定方法は下記の通り。

import tkinter as tk
window = tk.Tk()

# window タイトルを指定
window.title("ハローワールド")

# windowのサイズを指定
window.geometry("400x300")

label = tk.Label(text="ハローワールド")
label.pack()

window.mainloop()

ラベル(Label)の内容を更新する

こちらについては別のポストとして書いた。

shinshin86.hateblo.jp

ここでは概要のみに留めるが、下記のようにすることでテキストを変更することが可能。

label["text"] = "変更後のテキスト"

ラベルの位置を指定する(pack)

ここまで何の説明もなく label.pack() というように packを用いてラベルを表示してきた。 このpackはラベルをGUI上にいい感じに要素を配置してくれる機能で、細かい指定はどうでもいいからいい感じにパーツを画面上に配置したい、というケースで重宝する。

ただ、この pack を利用する場合でも、ある程度位置を調整したいケースがある。

pack側では下記のようなオプションが用意されているので、そちらを用いて適宜対応していくような形となる。

anchor

配置可能なスペースに余裕がある場合、対象のパーツをどこに配置するかを指定できる。

例えば指定されたエリアの中で左寄せしたい、などのケースではこのオプションが有効。

指定可能な内容は以下の通り。
tk 内に定数が用意されているのでそちらを参照する形で指定するのが良い。

  • tk.CENTER - 中央
  • tk.W - 左寄せ
  • tk.E - 右寄せ
  • tk.N - 上寄せ
  • tk.S - 下寄せ
  • tk.NW - 左上
  • tk.SW - 左下
  • tk.NE - 右上
  • tk.SE - 右下

expand

親のウィジェットが大きくなった際に、一緒に大きくなるかどうかを指定できる expand=1という形で指定できる。 逆にあえて指定しない場合は 0

fill

スペースを埋めて表示するかどうかを指定できる。

例えば横方向に埋めたい場合、fill="x" と指定すると、親のウィジェットに対して対象のウィジェットが横方向に敷き詰められた状態で表示できる。

利用できるオプションは以下の通り。

  • fill="none" - 元のサイズを保持
  • fill="x" - 横に広がる
  • fill="y" - 縦に広がる
  • fill="both" - 縦横に広がる

padding系オプション

CSSで言う、いわゆるmargin・padding系の指定が下記となる。
以下のように隙間は数値で指定する(例では 1 を適用)

  • padx=1 外側の横の隙間を設定
  • pady=1 外側の縦の隙間を設定
  • ipadx=1 内側の横の隙間を設定
  • ipady=1 内側の縦の隙間を設定

side

使ってみた感じはCSSのfloatのような感じ。
どの方向に詰めていくかを指定できる。

  • side="top" - 上から詰めていく
  • side="left" - 左から詰めていく
  • side="right" - 右から詰めていく
  • side="bottom" - 下から詰めていく

packの指定については以上。

これらのオプションを使っても要件を満たせなさそうな場合は、次に書いていくplaceやgridなどを利用していくことになる。

ラベルの位置を指定する(place)

packはラベルをいい感じに配置してくれる機能だが、より細かく位置を指定したい場合は place()を利用するようだ。

import tkinter as tk
window = tk.Tk()

# window タイトルを指定
window.title("ハローワールド")

# windowのサイズを指定
window.geometry("400x300")

label = tk.Label(text="ハローワールド")
label.place(x=100, y=100)

window.mainloop()

数値で指定するというのは実際には難しいので、ここらへんは pack()を使って適宜対応していくのかどうなのか。

おそらくPySimpleGUIはここらへんの座標関係の処理もいい感じに行っていてくれているのだろう、と想像しながらも次に行く。

ラベルの位置を指定する(grid)

またpackplaceとは別に grid というものもある。

これはrowcolumn を用いて位置を指定することができる。

また sticky を用いることで左に寄せて配置などのようなことが行える。

gridについては下記のポストにまとめたので、こちらを参照してみてください。

shinshin86.hateblo.jp

入力フォームを作る

次はtkinterで入力フォームを作成する。

import tkinter as tk
window = tk.Tk()

# window タイトルを指定
window.title("ハローワールド")

# windowのサイズを指定
window.geometry("400x300")

label = tk.Label(text="あなたの気分を入力してください")
label.pack()

text_form = tk.Entry()
text_form.pack()

window.mainloop()

まあまあ と入力しようと思ったのに、困ったことに日本語入力にならない!

tkinterで日本語入力を行う方法(pyenv環境でも動くことを確認)

調べてみると、どうやらこれはTcl/Tkパッケージによるバグらしい、という記事を見た。
ちなみにこのバグに該当するバージョンが 8.5 のようだが、私の環境はまさにそれだった。

python -c "import tkinter;print(tkinter.TkVersion)"
# => 8.5

pyenvでインストールしたPythonmacOS 10.6以降のユーザでMacPythonインストーラを使っている方は該当するそうな。

で、どうすれば良いのか調べてみると下記のページに行き着いた。

www.python.org

結論から先に書くと、Pythonのversionを新しものにすれば、それに付随してついてくるTcl/Tkパッケージも新しいものになるので、新しいPythonを使えばOKとのこと。

私はpyenvを利用してPython環境を構築しているので、下記のようにPython3.10.1 をインストールした。

pyenv install 3.10.1

# 最近私はglobalで設定して使っていますが、環境ごとに分けたいならlocalにしておいてください
pyenv global 3.10.1

3.10.1を入れると、TkVersion8.6 になる。

python -c "import tkinter;print(tkinter.TkVersion)"
# => 8.6

これで日本語入力ができない問題は無事に解決。

問題なく日本語入力が行えている

入力フォームの値を取得して、ボタンを押したら入力内容をダイアログに表示する方法

次はtkinterを使って入力フォームの値を取得し、ボタンを押したらその内容をダイアログに表示する方法。

一気にやることを増やしてしまったが、まあ、より実践的な内容にシフトしていこうと思う。

import tkinter as tk
import tkinter.messagebox as tkm

window = tk.Tk()

# window タイトルを指定
window.title("ハローワールド")

# windowのサイズを指定
window.geometry("400x300")

# ボタンが押されたらこの関数が呼ばれる
def show_msg():
    # text_formの値を取得
    value = text_form.get()

    # ダイアログに表示
    tkm.showinfo("入力されたテキスト", value)


label = tk.Label(text="あなたの気分を入力してください")
label.pack()

text_form = tk.Entry()
text_form.pack()


# ボタンを設置(commandに実行時のイベントを設定)
button = tk.Button(text="入力値を表示", width=50, command=show_msg) 
button.pack()

window.mainloop()

これでボタンを押すとテキストフォームに入力された値をダイアログに表示することができる。

ボタンに設定したイベント内でそのまま引数を渡したい時

上のやり方だと呼び出されたshow_msg関数内で入力値を取得しているが、その値を関数に引数として渡したいこともある。

そういうときは下記のように command=lambda と書いてlambda式を使うことで実現できるようだ。

import tkinter as tk
import tkinter.messagebox as tkm

window = tk.Tk()

# window タイトルを指定
window.title("ハローワールド")

# windowのサイズを指定
window.geometry("400x300")

# ボタンが押されたらこの関数が呼ばれる
def show_msg(value):
    # ダイアログに表示
    tkm.showinfo("入力されたテキスト", value)


label = tk.Label(text="あなたの気分を入力してください")
label.pack()

text_form = tk.Entry()
text_form.pack()


# ボタンを設置(引数を渡す場合はlambdaを使う)
button = tk.Button(text="入力値を表示", width=50, command=lambda: show_msg(text_form.get()))
button.pack()

window.mainloop()

tkinterで使えるダイアログの種類

ダイアログには様々な種類があるので、そちらを画像とともに載せておく。

下記のようなコードを用意してそれぞれ試した。

import tkinter as tk
import tkinter.messagebox as tkm

window = tk.Tk()
window.title("ダイアログサンプル")
window.geometry("400x300")

def show_info():
    tkm.showinfo("普通のダイアログ", "テストメッセージ")

def show_warn():
    tkm.showwarning("警告ダイアログ", "テストメッセージ")

def show_error():
    tkm.showerror("エラーダイアログ", "エラーメッセージ")

# YESがクリックされたら戻り値がTrue、NOならFalse
def show_askyesno():
    print(tkm.askyesno("YES or NO で答えられるダイアログ", "テストメッセージ"))

# リトライがクリックされたら戻り値がTrue、キャンセルならFalse
def show_askretrycancel():
    print(tkm.askretrycancel("Retry or Cancel で答えられるダイアログ", "テストメッセージ"))

# Yesがクリックされたら戻り値がyes、Noならno
def show_askquestion():
    print(tkm.askquestion("OK or NO で答えられるダイアログ", "テストメッセージ"))

# OKがクリックされたら戻り値がTrue、CancelならFalse
def show_askokcancel():
    print(tkm.askokcancel("OK or Cancel で答えられるダイアログ", "テストメッセージ"))

info_btn = tk.Button(text="Info", width=20, command=show_info)
info_btn.pack()
warn_btn = tk.Button(text="Warn", width=20, command=show_warn)
warn_btn.pack()
error_btn = tk.Button(text="Error", width=20, command=show_error)
error_btn.pack()
askyesno_btn = tk.Button(text="Yes/No", width=20, command=show_askyesno)
askyesno_btn.pack()
retry_btn = tk.Button(text="Retry/Cancel", width=20, command=show_askretrycancel)
retry_btn.pack()
question_btn = tk.Button(text="OK/No", width=20, command=show_askquestion)
question_btn.pack()
ok_btn = tk.Button(text="OK/Cancel", width=20, command=show_askokcancel)
ok_btn.pack()

window.mainloop()

普通のダイアログ

普通のダイアログ

警告ダイアログ

警告ダイアログ

エラーダイアログ

エラーダイアログ。だが、見た目がそこまでエラーっぽくない...

Yes/Noダイアログ

Yesがクリックされたら戻り値がTrue、NoならFalse

Retry/Cancelダイアログ

Retryがクリックされたら戻り値がTrue、CancelならFalse

Yes/Noダイアログ(askquestion)

Yesがクリックされたら戻り値がyes、Noならno

OK/Cancelダイアログ

OKがクリックされたら戻り値がTrue、CancelならFalse

tkinterを使ってログ出力エリアにログを出力する。

tkinterのログについては下記のポストがとても参考になった。

beenje.github.io

こちらの内容を参考にさせていただきつつ、実際にログ出力のサンプルとして書いてみたコードが下記となる。

import queue
import logging
import signal
import tkinter as tk
from tkinter.scrolledtext import ScrolledText
from tkinter import ttk, VERTICAL, HORIZONTAL, N, S, E, W

logger = logging.getLogger(__name__)


class QueueHandler(logging.Handler):
    def __init__(self, log_queue):
        super().__init__()
        self.log_queue = log_queue

    def emit(self, record):
        self.log_queue.put(record)


class ConsoleUi:
    def __init__(self, frame):
        self.frame = frame

        # ScrolledTextウィジェットを作成する
        self.scrolled_text = ScrolledText(frame, state="disabled", height=12)

        # gridもpackやplaceと同じようにウィジェットの配置に関する関数
        self.scrolled_text.grid(row=0, column=0, sticky=(N, S, W, E))

        # フォントの設定
        self.scrolled_text.configure(font="TkFixedFont")

        # タグ名に対応するオプションを設定する(例: CRITCALが指定された場合、赤文字の下線ありで表示される)
        self.scrolled_text.tag_config('INFO', foreground='black')
        self.scrolled_text.tag_config('DEBUG', foreground='gray')
        self.scrolled_text.tag_config('WARNING', foreground='orange')
        self.scrolled_text.tag_config('ERROR', foreground='red')
        self.scrolled_text.tag_config('CRITICAL', foreground='red', underline=1)

        # queueを用いてlogging handlerを作成する
        self.log_queue = queue.Queue()
        self.queue_handler = QueueHandler(self.log_queue)

        # ログのフォーマットの設定
        formatter = logging.Formatter('%(asctime)s: %(message)s')
        self.queue_handler.setFormatter(formatter)

        # handlerをloggerに追加
        logger.addHandler(self.queue_handler)

        # queueからのメッセージをポーリングしていく
        # after関数を用いて、100msecごとにpoll_log_queueを実施
        self.frame.after(100, self.poll_log_queue)

    def display(self, record):
        # state="normal"にしないと書き込みが行えない。そのため一時的にnormalを設定
        self.scrolled_text.configure(state="normal")

        msg = self.queue_handler.format(record)
        
        # insert関数を用いてscrolled_textにログを渡す。3つめの引数にはタグを渡している(INFO, WARNINGなど)
        self.scrolled_text.insert(tk.END, record.levelname + ":" + msg + "\n", record.levelname)

        # ユーザが編集できないように再び"disabled"に戻す
        self.scrolled_text.configure(state='disabled')

        # 下にオートスクロール
        self.scrolled_text.yview(tk.END)

    def poll_log_queue(self):
        # 表示するメッセージがキューに存在するかどうかを100msecごとに確認する
        while True:
            try:
                record = self.log_queue.get(block=False)
            except queue.Empty:
                break
            else:
                self.display(record)
        self.frame.after(100, self.poll_log_queue)


class InputLogUi:

    def __init__(self, frame):
        self.frame = frame
        tk.Label(self.frame, text="出力したいログメッセージを入力してください").grid(column=0, row=1, sticky=W)
        log_form = tk.Entry(self.frame)
        log_form.grid(column=0, row=2, sticky=W)
        tk.Button(self.frame, text="DEBUGログ", width=10, command=lambda: self.debug_log(log_form.get())).grid(column=0, row=3, sticky=W)
        tk.Button(self.frame, text="INFOログ", width=10, command=lambda: self.info_log(log_form.get())).grid(column=0, row=4, sticky=W)
        tk.Button(self.frame, text="WARNログ", width=10, command=lambda: self.warn_log(log_form.get())).grid(column=0, row=5, sticky=W)
        tk.Button(self.frame, text="ERRORログ", width=10, command=lambda: self.error_log(log_form.get())).grid(column=0, row=6, sticky=W)
        tk.Button(self.frame, text="CRITICALログ", width=10, command=lambda: self.critical_log(log_form.get())).grid(column=0, row=7, sticky=W)
    
    def debug_log(self, log_msg):
        level = logging.DEBUG
        logger.log(level, log_msg)

    def info_log(self, log_msg):
        level = logging.INFO
        logger.log(level, log_msg)
    
    def warn_log(self, log_msg):
        level = logging.WARNING
        logger.log(level, log_msg)

    def error_log(self, log_msg):
        level = logging.ERROR
        logger.log(level, log_msg)

    def critical_log(self, log_msg):
        level = logging.CRITICAL
        logger.log(level, log_msg)




class App:

    def __init__(self, root):
        self.root = root
        root.title("Sample Logging Handler")
        root.columnconfigure(0, weight=1)
        root.rowconfigure(0, weight=1)

        # PanedWindowを作成
        vertical_pane = ttk.PanedWindow(self.root, orient=VERTICAL)
        vertical_pane.grid(row=0, column=0, sticky="nsew")
        horizontal_pane = ttk.PanedWindow(vertical_pane, orient=HORIZONTAL)
        vertical_pane.add(horizontal_pane)

        # Consoleフレームを作成
        console_frame = ttk.Labelframe(horizontal_pane, text="ログコンソール")
        console_frame.columnconfigure(0, weight=1)
        console_frame.rowconfigure(0, weight=1)
        horizontal_pane.add(console_frame, weight=1)

        input_log_frame = ttk.Labelframe(vertical_pane, text="ログ入力フォーム")
        vertical_pane.add(input_log_frame, weight=1)

        self.console = ConsoleUi(console_frame)
        self.input_log = InputLogUi(input_log_frame)

        ### アプリの終了に関する処理
        # 終了イベント(WM_DELETE_WINDOW)をquit関数に置き換え
        self.root.protocol('WM_DELETE_WINDOW', self.quit)
        # ctrl + qでquit関数を呼び出す
        self.root.bind('<Control-q>', self.quit)
        # SIGINTシグナルが送られたらquit関数
        signal.signal(signal.SIGINT, self.quit)


    def quit(self, *args):
        logging.log(logging.INFO, "アプリを終了します")
        self.root.destroy()


def main():
    logging.basicConfig(level=logging.DEBUG)
    root = tk.Tk()
    app = App(root)
    app.root.mainloop()


if __name__ == "__main__":
    main()

こちらのコードを実行してみると、下記のようなウィンドウが起動する。
フォームにログメッセージを入力して各ボタンを押下することで、対応したログ出力を再現できる。

ちなみにこのログ出力のサンプルコードを書くにあたって、tkinterに関するその他の機能についても同時に学ぶことができたので、そちらの内容についても書いていこうと思う。

Queueを用いてログ出力を行う

これは上に貼ったポストに書いてあったものだが、スレッド間でデータを共有するためにQueueが利用されている。

class QuereHandler(logging.Handler):
    def __init__(self, log_queue):
        super().__init__()
        self.log_queue = log_queue

    def emit(self, record):
        self.log_queue.put(record)
・
・
・
        # queueを用いてlogging handlerを作成する
        self.log_queue = queue.Queue()
        self.queue_handler = QueueHandler(self.log_queue)

ハンドラー自体はメッセージをキューに入れるだけとなっており、キューからメッセージをポーリングして、ログを出力させるというような設計になっている。

キューからログメッセージを取得して表示させる部分については下記の poll_log_queue 関数で行われている。

def poll_log_queue(self):
        # 表示するメッセージがキューに存在するかどうかを100msecごとに確認する
        while True:
            try:
                record = self.log_queue.get(block=False)
            except queue.Empty:
                break
            else:
                self.display(record)
        self.frame.after(100, self.poll_log_queue)

ScrolledTextに書き込みを行う方法

今回ScrolledTextクラスを用いてログを表示させているが、ScrolledTextには state という状態を管理する属性があり、こちらが normal のときでしか書き込みを行うことができない。
だが、normal のままにするとログ表示箇所にユーザが自由に書き込みできてしまうため、普段はユーザが書き込みできないように state=disabled を指定しておく必要がある。

よって、下記のように書き込み処理を行う間だけ一時的にstateを変更している。

下記はログの出力処理を行うdisplay関数。

    def display(self, record):
        # state="normal"にしないと書き込みが行えない。そのため一時的にnormalを設定
        self.scrolled_text.configure(state="normal")

        msg = self.queue_handler.format(record)
        
        # insert関数を用いてscrolled_textにログを渡す。3つめの引数にはタグを渡している(INFO, WARNINGなど)
        self.scrolled_text.insert(tk.END, record.levelname + ":" + msg + "\n", record.levelname)

        # ユーザが編集できないように再び"disabled"に戻す
        self.scrolled_text.configure(state='disabled')

        # 下にオートスクロール
        self.scrolled_text.yview(tk.END)

ScrolledTextにタグを設定する方法

また、このScrolledTextにはタグを設定することが可能で、下記のように指定したタグに応じた文字の装飾などを行うことができる。

        # タグ名に対応するオプションを設定する(例: CRITCALが指定された場合、赤文字の下線ありで表示される)
        self.scrolled_text.tag_config('INFO', foreground='black')
        self.scrolled_text.tag_config('DEBUG', foreground='gray')
        self.scrolled_text.tag_config('WARNING', foreground='orange')
        self.scrolled_text.tag_config('ERROR', foreground='red')
        self.scrolled_text.tag_config('CRITICAL', foreground='red', underline=1)

このように tag_config関数でタグを設定した場合、scrolled_textに対して出力処理を行う insert 関数内の第3引数に指定したタグを与えれば良い。

        # insert関数を用いてscrolled_textにログを渡す。3つめの引数にはタグを渡している(INFO, WARNINGなど)
        self.scrolled_text.insert(tk.END, record.levelname + ":" + msg + "\n", record.levelname)

afterを用いて継続的に処理を実施

tkinterにはafterメソッドというものが用意されており、こちらも用いることで、指定した処理を一定間隔で定期的に実行することが可能となる。

今回の場合、下記のようなログのポーリング処理で利用している。

        # queueからのメッセージをポーリングしていく
        # after関数を用いて、100msecごとにpoll_log_queueを実施
        self.frame.after(100, self.poll_log_queue)

追記:ログ出力のサンプルについて

上のコードをリファクタリングして、GitHubにあげた。

github.com

表示させたログ(ScrolledText)の内容をクリップボードに貼り付ける

上のログサンプルを進化させて、ボタンを押したら表示されているログをクリップボードに貼り付けるようにした。

クリップボードを利用するには、 clipboard_append を利用すれば良いようだが、ScrolledTextの値については scrolled_text.get() だけでは取得できない。

こちらについては下記のstack overflowが参考になった。

stackoverflow.com

どうやら下記のように引数を渡すことで値を読み取ることができた。
クリップボードにコピーする部分も含めて下記のように書くことで処理は実現可能。

    def copy_clipboard(self):
        self.scrolled_text.clipboard_append(self.scrolled_text.get("1.0", tk.END))
        tkm.showinfo("Copy to clipboard", "Copied log to clipboard.")

実際に上に書いたログ出力サンプルにクリップボードへのコピー機能を足した際のコミットがこちら。

github.com

frameを跨いだ場合の関数の渡し方はどうするのが良いのかまだ理解できていないが、ひとまずこれで別フレームで定義したクリップボードコピーボタンに対して、ScrolledTextで表示されている最初から最後までの文章をすべてクリップボードにコピーできるようになった。

表示させたログ(ScrolledText)の内容を削除する

上に続いて、今度はScrolledTextの中身を削除する方法。

再び、上に書いたログ出力サンプルに実際に機能を足したので、そちらのコードをこちらに記載する。

    def clear_log(self):
        if tkm.askyesno("Clear log", "Are you sure you want to delete the log?") is True:
            self.scrolled_text.configure(state='normal')
            self.scrolled_text.delete("1.0", tk.END)
            self.scrolled_text.configure(state='disabled')

このようにscrolled_text.delete("1.0", tk.END) とし、値を取得するときと同様に範囲を指定する必要があるが、全て取得するのであれば delete("1.0", tk.END)で問題ない。

なお、ScrolledTextはあくまでログ出力の表示コンソールとして設定しているもののため、普段はユーザが内容を変更できないように self.scrolled_text.configure(state='disabled') を指定して、編集無効としている。

そのため値の削除の前に、 state='normal' を指定して値を編集可能な状態にする必要がある。

これは ScrolledTextに書き込みを行う方法 にも書いた内容と同じである。

実際に機能追加した際のコミット内容は下記となる。

github.com

ログの表示がリアルタイムに反映されない時

上に書いたログ表示に関する続き。

例えばtkinterからボタンを押して下記のような関数を実行した場合。

def all_log(self, log_msg):
    log_levels = [logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR, logging.CRITICAL]

    # ここでログ出力を行っているが、これはリアルタイムに反映されない
    for level in log_levels:
        self.log(level, log_msg)
        sleep(1)

この self.log という関数が実行されると、tkinterのログ出力用の窓にログが表示されていく、という仕組みだとして、この all_log関数をtkinterのボタン経由でそのまま実行してもログはリアルタイムに表示されない。

ログが表示されるタイミングは all_log 関数がすべて実行され終えたあとに、まとめてログが表示されるような流れとなる。

これはこの関数が実行されている間はGUIの描画処理は行われないためである。

そのため、このような処理でリアルタイムにログの描画も行うためにはこの all_log 関数を別スレッドで実行していくようにする。

具体的には下記のようなコードに書き換えると、この関数自体は別スレッドでの処理となるため、ログ表示に関するGUI側の処理も関数処理と並行して行われるようになる。

import threading
・
・
・
def all_log(self, log_msg):
    def thread_all_log():
        log_levels = [logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR, logging.CRITICAL]

        for level in log_levels:
            self.log(level, log_msg)
            sleep(1)

     thread = threading.Thread(target=thread_all_log)
     thread.start()

なお、実際にこちらの修正はGitHub上でも確認可能となっている。

Add thread · shinshin86/sample-tkinter-log-output@e9b5529 · GitHub

tkinter上からSeleniumを起動させる

上のスレッドの話と関連する話となるが、例えばtkinter上でボタンを押すとSeleniumが起動して何らかのスクレイピング処理が動くものとする。

その際に普通にSeleniumを用いた処理を実行していると、実行している間はGUIが操作不能となる。

これは上にも書いたログの描画と同様に、別の処理の実行中に同時にGUIの描画ができないためとなる。

そのためこのようなケースに置いてもスレッドを別途作成して、Seleniumによる処理を別スレッドでの処理するようにすることで、Seleniumを用いた処理が動いている間もGUIの操作が行えるようになる。

サンプルコード的には上に書いたログ表示のものとそれほど変わらないものとなるので割愛するが、そのうち時間を見つけてこちらに関するサンプルコードも追記するかもしれない。

tkinterとsqlite3を組み合わせてアプリを作る

sqlite3(SQLite)と組み合わせてtkinterGUIアプリを作ることも可能。

こちらの内容については別ポストとして書いた。

shinshin86.hateblo.jp