LoginSignup
11
11

More than 3 years have passed since last update.

Python3とtkinterでボタンの実装にてこずる話

Last updated at Posted at 2020-04-19

経緯

退屈なことはPythonにやらせなさいと教わったので、そうしようと思いました。
とりあえずCUIベースでいろいろ作って、それはそれは満足のいくものができました。また、僕の作ったスクリプトは、いろんな人のめんどくさいを解消できるものだったので、みんなに使ってもらおうと思いました。GUIを作ろう。コンマ2秒の思いつき。
僕はかつてC言語でGUIをを実装しようと思って、あきらめているので、割と不安でしたが、Qiitaの皆様のおかげで、サクサク進みました。

どうやらtkinterがいいらしい

知らんけど。kivyとかもやったけど、ちょっと複雑になるし、文献も少ないかな?
地味でよければtkinterで十分だし、応用が自分でもできる気がします。ファイルダイアログとか簡単に実装できます。そう思って、よくまとまったチュートリアルてきな記事を見ながら実装します。
だがしかし、ボタンをみんなが言う通りに実装しても、うまく動いてくれません。

問題点

  • ボタンを押すとGUIが止まる。(画面が応答しない)
  • ボタンが見た目上、押しっぱなしになってしまう
  • ボタンが何回も押せてしまう。(関数が複数回呼び出される)

最終形

たぶん、みんなこれが知りたい!

app.py
import tkinter
import threading
import time
from tkinter import messagebox

class Button(tkinter.Button):
    def __init__(self):
        super().__init__(
            master=None,
            text="Act",
            width=100,
            command=self.Button_click
            )

    def Button_click(self):
        text_message.set("実行中です。しばらくお待ちください。")
        thread = threading.Thread(target = tmp)
        thread.start()

def tmp():
    Button_act["state"] = tkinter.DISABLED
    try:
        time.sleep(10)
    except Exception:
        messagebox.showwarning("warning", "エラーです")
    else:
        messagebox.showinfo("message", "正常終了です")
    finally:
        text_message.set("入力を行い、Actを押してください。")
        Button_act["state"] = tkinter.NORMAL

root = tkinter.Tk()
root.title(u"Test App")

Button_act = Button()
Button_act.grid(row = 0, column = 0)

text_message = tkinter.StringVar()
text_message.set("入力を行い、Actを押してください。")
label_info = tkinter.Label(textvariable = text_message, font = ("", 10,"bold"), justify = "left")
label_info.grid(row = 1, column = 0)

tkinter.mainloop()

このコードは一般的なきれいなボタンと同じように動きます。

原因と解決策

  • 僕の作った関数がとんでもなく時間かかる(もともとCUIのmainをそのまま使っているため)
    • これはそんなに問題ではない。細かく分けたほうが融通利く気がするけど。
  • ボタンを押した後に、関数にて、Button_act['status'] = tkinter.DISABLEDをしていない。
    • 見た目はボタン押せなくなってるけど、実は押せてる
  • bindを用いて関数を実行している。
    • これが結構大きかった。tkinter.Buttonクラスcommandを上書きしよう。

最初の状態から見ていきます

app0.py
import tkinter
import threading
import time
from tkinter import messagebox


def action(event):
    text_message.set("実行中です。しばらくお待ちください。")
    time.sleep(10)
    text_message.set("入力を行い、Actを押してください。")
    messagebox.showinfo("message", "正常終了です")



root = tkinter.Tk()
root.title(u"Test App")

Button_act = tkinter.Button(text="Act", width=100)
Button_act.grid(row = 0, column = 0)
Button_act.bind("<1>", action)

text_message = tkinter.StringVar()
text_message.set("入力を行い、Actを押してください。")
label_info = tkinter.Label(textvariable = text_message, font = ("", 10,"bold"), justify = "left")
label_info.grid(row = 1, column = 0)

tkinter.mainloop()

このプログラムの挙動
app0_1.png
まず、Actを押した直後は応答しなくなります。(たぶんwindowも動かせない)
そのわりに、ボタンはへこんでません。ボタンらしくない。
app0_2.png
無事終了したと思ったら、やっとボタンがへこみました。
おーい?今度はへこんだまま戻ってこないぞ?

どんな感じで動いてるのか確かめるので、クリックした後の動作する関数を以下のように定義しなおします。

app0.py>action(event)
def action(event):

    print("start") #追記

    text_message.set("実行中です。しばらくお待ちください。")
    time.sleep(10)
    text_message.set("入力を行い、Actを押してください。")
    messagebox.showinfo("message", "正常終了です")

    print("終了") #追記

何となく見た目にわかりやすいので、英語と日本語です。(理由はあとづけ)
結果です。
app0_3.png
実行中もActボタンを押しまくってます。しかしstartの後は終了ですし、ログの出現タイミング的にも、bindの関数実行中はGUIはほかの動作を全く受け付けないことがわかりました。(あたりまえですかね :sweat: )
つまり、やるべきことはbindで実行される関数をなるべく早く終わらせることです。

ここでスレッドという仕組みを使います。
初心者的にはこんな雰囲気
app0_5.png
これが
app0_4.png
こういうことですね。なんか授業でやった気がしたり、しなかったり。
たぶん、細かく言うと、2つのスレッドを行ったり来たりしてるとかいうやつなんですが、わかりやすいからこれでいいことにしましょう。

スレッドを使ったバージョン

app1.py

import tkinter
import threading
import time
from tkinter import messagebox


def action(event):
    thread = threading.Thread(target=function) #追記
    thread.start() #追記

def function(): #新しく定義
    print("start") 
    text_message.set("実行中です。しばらくお待ちください。")
    time.sleep(10)
    text_message.set("入力を行い、Actを押してください。")
    messagebox.showinfo("message", "正常終了です")
    print("終了") 

root = tkinter.Tk()
root.title(u"Test App")

Button_act = tkinter.Button(text="Act", width=100)
Button_act.grid(row = 0, column = 0)
Button_act.bind("<1>", action)

text_message = tkinter.StringVar()
text_message.set("入力を行い、Actを押してください。")
label_info = tkinter.Label(textvariable = text_message, font = ("", 10,"bold"), justify = "left")
label_info.grid(row = 1, column = 0)

tkinter.mainloop()

これだと、action関数は一瞬で終わるので、画面が固まらずに済むんです!
しかし、次の問題も出てきます。それは、Actボタンが何回も押せてしまう問題です
app1_1.png

こんなかんじで、終了が来る前に実行できてしまいます。
実行する関数がtime.sleep()なら、これはこれで楽しいんですけど、ここに実際は重たい処理が入ってるので、、、複数回実行できてしまうのは、あまりよろしくなさそう。ボタンに変化がないと、ボタン連打しがちだしね。(僕だけ?)

実は、実際の開発環境だとここで、ボタンが押したまま現象も起きていた気がするのですが、起来ませんでした。

というわけで、次に、ボタンが押せないようにします!

ボタン押せないやつ

ボタンを押せなくするには、

Button_act['status'] = tkinter.DISABLED 

をします。
コードがこちらです。def function()のみ書き換えました。

app1.py
def function():
    Button_act["state"] = tkinter.DISABLED
    print("start")
    text_message.set("実行中です。しばらくお待ちください。")
    time.sleep(10)
    text_message.set("入力を行い、Actを押してください。")
    messagebox.showinfo("message", "正常終了です")
    print("終了")
    Button_act["state"] = tkinter.NORMAL

実行してみました。
app1_2.png
お!押せなくなってる!
これで完璧やな!となるはずだったんですが、裏で動いてるんですよね、、、
新しくスクショするのめんどくさいので流用しますが、要するに
app1_1.png
こうなってました。どうやらボタンは押せなくなった判定でも押せてしまっているらしい。

たぶん、僕の憶測ですけど、Button_act.bind(ほにゃほにゃ)で実装したものは、ボタンが押されたタイミングではなく、マウスがクリックされたタイミングで関数が呼ばれてるんだと思います。

そうですね、例えるならば、自宅の電気のスイッチがButton_actだとすると、押そうという意思をもって触れた瞬間、スイッチ押し込んでないのに電気がつく感じですね。あるあるです。

このbind、labelとかにでも使えるらしくて、ボタンじゃなくても文字とかにも使えるんです確か。逆に言えば、ボタンの機能は全く使ってないってことですね。

では、ボタン特有の機能はいずこへ?というのが、次です。

最終形態

app.py
import tkinter
import threading
import time
from tkinter import messagebox

class Button(tkinter.Button):
    def __init__(self):
        super().__init__(
            master=None,
            text="Act",
            width=100,
            command=self.Button_click
            )

    def Button_click(self):
        text_message.set("実行中です。しばらくお待ちください。")
        thread = threading.Thread(target = tmp)
        thread.start()

def tmp():
    Button_act["state"] = tkinter.DISABLED
    try:
        time.sleep(10)
    except Exception:
        messagebox.showwarning("warning", "エラーです")
    else:
        messagebox.showinfo("message", "正常終了です")
    finally:
        text_message.set("入力を行い、Actを押してください。")
        Button_act["state"] = tkinter.NORMAL

root = tkinter.Tk()
root.title(u"Test App")

Button_act = Button()
Button_act.grid(row = 0, column = 0)

text_message = tkinter.StringVar()
text_message.set("入力を行い、Actを押してください。")
label_info = tkinter.Label(textvariable = text_message, font = ("", 10,"bold"), justify = "left")
label_info.grid(row = 1, column = 0)

tkinter.mainloop()

では最終形態を見ていきましょう。

class Button

ここでは、継承クラスを作ってます。さっきのやつでは、

(変数)= tkinter.Button()

で作ってたんですけど、今度から

(変数)= Button()

で作れるようになりました。

そんで、ただ複製するだけでは意味がないので、元のやつの中身を書き換えます。
def __init__() 的なやつの中身が書き換えた内容です。__init__()関数はButton()で作った瞬間に呼び出されるので、その時に中身書き換えたものにしましょうということだと思います。
ここで今回重要なcommandを書き換えています。初期状態は無しになってるのかな?ここに関数の名前を入れることで、ボタンの機能として関数を呼び出しています。
呼び出す関数はどこにおいてもいいと思いますが、Buttonに関係のあるものなので、classの中で定義しています。class内においてselfはButtonを指すのでButton_Click()の中はselfです。
呼び出す関数をクラス外に宣言するときは、引数にeventを指定してあげてください。(確か)

これでできるようになると思います!!

最後に

初めて記事を書いてみました。これ結構大変ですね。
また、Pythonも今回の開発で触った超初心者なので、間違ったこと言ってる可能性がめちゃくちゃあるので、指摘してくださるとありがたいです。

11
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
11