naritoブログ

【お知らせ】
・コメントで質問等をしたが返事が返ってこない場合、私はそれを見落としています。
その場合は再度コメントをするかメールをしてください(toritoritorina@gmail.com)。

・近いうちに新しいブログが作成されます。わーお!

Tkinterで、簡易電卓を作るシリーズ②ボタンにイベントを設定する

約400日前 2017年8月19日20:57
プログラミング関連
Python Tkinter
前回、ある程度の見た目を作りました。
今回はボタンを押したら反応するようにしていきます。

まず、非常に単純な例です。
これを起動するとボタン一つのウィンドウが立ち上がり、ボタンを押すとコンソールに「おされた」と表示されます。
from tkinter import *
from tkinter import ttk


class CalcApp(ttk.Frame):
    """電卓アプリ."""
 
    def __init__(self, master=None):
        super().__init__(master)
        self.create_widgets()
 
    def create_widgets(self):
        button = ttk.Button(self, text='押して', command=self.push)
        button.grid(column=0, row=0, sticky=(N, S, E, W))
        self.grid(column=0, row=0, sticky=(N, S, E, W))
    
    def push(self):
        print('おされた')
        

def main():
    root = Tk()
    root.title('簡単電卓')
    CalcApp(root)
    root.mainloop()


if __name__ == '__main__':
    main()



ボタンを押すと...


このように出力されます


command=self.push のようにして、押された際に呼び出す関数を渡しています。
ここには、関数オブジェクト自体を渡すことを覚えておきましょう。
    button = ttk.Button(self, text='押して', command=self.push)
    ...
    ...
    def push(self):
        print('おされた')



今回はクラスのメソッドとしてpushを定義しましたが、別に普通の関数でも大丈夫です。関数オブジェクトを渡せば動きます。
いくつかのウィジェットで共通して使うイベントなんかがあれば、こうして定義するのもアリです。
def push():
    print('おされた')


class CalcApp(ttk.Frame):
    """電卓アプリ."""
 
    def __init__(self, master=None):
        super().__init__(master)
        self.create_widgets()
 
    def create_widgets(self):
        button = ttk.Button(self, text='押して', command=push)
        button.grid(column=0, row=0, sticky=(N, S, E, W))
        self.grid(column=0, row=0, sticky=(N, S, E, W))



ボタンにはcommandというオプションがありますが、これを使わない汎用的な方法もあります。それはbindです。
以下のコードは、先程と同じように動作します。
class CalcApp(ttk.Frame):
    """電卓アプリ."""
 
    def __init__(self, master=None):
        super().__init__(master)
        self.create_widgets()
 
    def create_widgets(self):
        button = ttk.Button(self, text='押して')
        button.bind('<Button-1>', self.push)
        button.grid(column=0, row=0, sticky=(N, S, E, W))
        self.grid(column=0, row=0, sticky=(N, S, E, W))
        
    def push(self, event):
        print('おされた')



'<Button-1>'というのは、左クリックのことを指します。
また、pushメソッドにeventという引数が増えています。bindを使うとeventオブジェクトも渡されるのです。
    button.bind('<Button-1>', self.push)
    ...
    ...
    def push(self, event):
        print('おされた')



渡されたeventオブジェクトからは、色々取得できます。
例えば、押されたウィジェットのテキスト部分を取得する例は以下です。
print(event.widget['text'])



bindは汎用的で、ボタンに限りません。
試しに、ButtonをLabelに変えてみてもちゃんと動作します。
        label = ttk.Label(self, text='押して')
        label.bind('<Button-1>', self.push)
        label.grid(column=0, row=0, sticky=(N, S, E, W))



あるウィジェットに、複数のイベントを紐付けることだって可能です。
以下はボタンを離した際にも反応するようにしています。
label.bind('<ButtonRelease-1>', self.release)が、それに当たります。
class CalcApp(ttk.Frame):
    """電卓アプリ."""
 
    def __init__(self, master=None):
        super().__init__(master)
        self.create_widgets()
 
    def create_widgets(self):
        label = ttk.Label(self, text='押して')
        label.bind('<Button-1>', self.push)
        label.bind('<ButtonRelease-1>', self.release)
        label.grid(column=0, row=0, sticky=(N, S, E, W))
        self.grid(column=0, row=0, sticky=(N, S, E, W))
        
    def push(self, event):
        print('おされた')

    def release(self, event):
        print('はなした')



では、前回作ったアプリのボタンにイベントを設定しましょう。
以下のようになります。
from tkinter import *
from tkinter import ttk
 
 
# 2次元配列のとおりに、gridでレイアウトを作成する
LAYOUT = [
    ['7', '8', '9', '/'],
    ['4', '5', '6', '*'],
    ['1', '2', '3', '-'],
    ['0', 'C', '=', '+'],
]
 
 
class CalcApp(ttk.Frame):
    """電卓アプリ."""
 
    def __init__(self, master=None):
        super().__init__(master)
        self.create_widgets()
 
    def create_widgets(self):
        """ウィジェットの作成."""
        # 計算結果の表示ラベル
        dispay_label = ttk.Label(self, text='0')
        dispay_label.grid(column=0, row=0, columnspan=4, sticky=(N, S, E, W))
 
        # レイアウトの作成
        for y, row in enumerate(LAYOUT, 1):
            for x, char in enumerate(row):
                button = ttk.Button(self, text=char)
                button.grid(column=x, row=y, sticky=(N, S, E, W))
                button.bind('<Button-1>', self.calc)
        self.grid(column=0, row=0, sticky=(N, S, E, W))
 
        # 横の引き伸ばし設定
        self.columnconfigure(0, weight=1)
        self.columnconfigure(1, weight=1)
        self.columnconfigure(2, weight=1)
        self.columnconfigure(3, weight=1)
 
        # 縦の引き伸ばし設定。0番目の結果表示欄だけ、元の大きさのまま
        self.rowconfigure(0, weight=0)
        self.rowconfigure(1, weight=1)
        self.rowconfigure(2, weight=1)
        self.rowconfigure(3, weight=1)
        self.rowconfigure(4, weight=1)
 
        # ウィンドウ自体の引き伸ばし設定
        self.master.columnconfigure(0, weight=1)
        self.master.rowconfigure(0, weight=1)
 
    def calc(self, event):
        print(event.widget['text'])  # 押されたウィジェットのテキスト部分を表示


def main():
    root = Tk()
    root.title('簡単電卓')
    CalcApp(root)
    root.mainloop()
 
 
if __name__ == '__main__':
    main()
 



追加したのはforループ中でのbutton.bind()と、calcメソッドです。
calcメソッドは、単純に押されたキーを表示するだけにしています。
押されたボタンやボタンの種類ごとに違うメソッドを呼んでもいいのですが、今回はシンプルにcalcメソッドだけにします。
                button.bind('<Button-1>', self.calc)
...
...
...
    def calc(self, event):
        print(event.widget['text'])



calcメソッドをもう少し改良しましょう。
ボタンが押されたら、結果表示欄のラベルに押されたボタンを表示するようにしてみます。
その下準備として、dispay_labelにアクセスするための手段が必要です。


方法は大まかに2つです。
self.display_label...とし、インスタンスの属性として利用できるようにします。
    def create_widgets(self):
        """ウィジェットの作成."""
        self.dispay_label = ttk.Label(self, text='0')
        self.dispay_label.grid(column=0, row=0, columnspan=4, sticky=(N, S, E, W))



そして、calcメソッドで以下のように表示テキストを変更します。
    def calc(self, event):
        char = event.widget['text']  # 押されたウィジェットのテキスト部分を取得
        self.dispay_label['text'] = char  # display_labelの表示テキストに、上のcharを入れる



そして次の方法です。基本的には、こちらを使う方が良いです。
    def create_widgets(self):
        """ウィジェットの作成."""
        self.display_var = StringVar()
        self.display_var.set('0')  # 初期値を0にする

        dispay_label = ttk.Label(self, textvariable=self.display_var)
        dispay_label.grid(column=0, row=0, columnspan=4, sticky=(N, S, E, W))


    def calc(self, event):
        char = event.widget['text']
        self.display_var.set(char)



ボタンを押すと、ちゃんと結果表示欄にボタンのテキストが表示されるようになりました。



StringVarは文字列を保持する変数で、ウィジェットに紐付けることができます。
self.var=StringVar()のようにし、それをラベル等のウィジェットにtextvariable=self.var とするだけです。
その後は、self.var.get()で値の取得が、self.var.set()で値の設定ができます。

self.varに値をset()すると、それが画面上のウィジェットにもすぐに反映されます。
また、例えばEntry等の入力ができるウィジェットであれば、画面上で入力をすると、その値がself.varに直ぐ格納されます。

以下はStringVarとEntryウィジェットを使ったサンプルです。(これはクラスやFrameを使わない、シンプルなプログラムの例でもあります)
画面で入力欄に文字を入力し、エンターを押すと、入力欄の文字が逆順になります。
from tkinter import *
from tkinter import ttk


def reverse_string(event):
    input_value = var.get()  # 画面で入力した文字列を取得
    var.set(input_value[::-1])  # 取得した文字列を、逆順にしてsetする


root = Tk()
var = StringVar()
entry = ttk.Entry(root, textvariable=var)
entry.grid(column=0, row=0)
entry.bind('<Return>', reverse_string)
root.mainloop()




文字列を入力しエンターを押すと...


ちゃんと逆順になります
生徒 約121日前 2018年5月25日21:49 返信する
いつも楽しく拝見しています。
質問させて頂きます。
クリックすると数値が1上がるだけのコードを書きたいのですが、
データベースに値を保存することなく、一時的に値をどこかに保存して、2,3,4と上げていく方法はありますでしょうか。
以下のような簡単なコードを書いたのですが、このコードだと実行するたびに結果は1になります。

class Counter:

def__init__(self):
 self.a=0

def bbb(self):
 self.a+=1
 b=self.a
 return b

if__name__=="__main__":
 ccc=Counter()
 ddd=ccc.bbb()
 print(ddd)


この質問自体が的外れなことを言っている可能性があると思っていますので、その場合はご指摘いただければ嬉しく思います。
また、今回の質問はこの記事に紐づけて質問するのが適切でないかも知れませんが、
他に適切な質問箇所が見当たらなかった事と、クリックした際のイベント発生という内容が似ているかと思い、この記事に質問させて頂きます。
いつもお仕事の邪魔をしてしまい、申し訳ございません。
なりと 約121日前 2018年5月26日4:56
ボタンをクリックというのはTkinter上ででしょうか、Django等を使ったHTML上でしょうか。
HTML上の場合は、そのカウンターは全てのユーザーで共有しますか、ユーザー毎に管理しますか。
生徒 約120日前 2018年5月26日13:27
お世話になっております。
ご返信頂きまして有難うございます。
上記のコードは、Django等を使ったHTML上での実験です。
また、カウント数はユーザー毎に管理したいと思っています。
なりと 約120日前 2018年5月26日13:49
ユーザー毎であれば、セッションを使うのが簡単です。
セッションはrequest.sessionとしてアクセスでき、辞書のように扱えます。

# countというキーがあればそれを返し、countキーがなければ作って値を0にし、返す
count = request.session.setdefault('count', 0)
count += 1

ブラウザにはクッキーと呼ばれる、ブラウザ毎にデータを管理できる機能があります。
セッションはこれを利用しており、ユーザー毎(ブラウザ毎)に何か値を設定したり、Djangoではログイン中かどうかの判断もセッションを使っています。
ただブラウザのクッキーを基にしているので、ユーザーがブラウザのクッキーを削除するとカウントが消えることに注意してください。それが困る場合は、モデルの利用を検討してください。
生徒 約120日前 2018年5月26日14:33
ご回答ありがとうございます。
恐れ入ります、web開発の基礎がまだ出来上がってないので、何度も質問する無礼をお許し下さい。
ユーザーがブラウザを変えたり、ブラウザのクッキーを削除したりする可能性があるならば、
データベーステーブルにカウンターデータを保存する方向性を選んだほうが良いのでしょうか。

なぜ、このような質問をするかというと、例えば、アクションゲームの製作において弾が10発充填されている武器の弾数を管理する場合、データベースに保存するほうがよいのか、それとも今回ご指導頂いたようなセッションを使うなどの別の方法が良いのかを考えています。
その方法が分かると、上記のようなカウンターデータの管理に最適な応用の仕方が見えてくると思いました。
なりと 約120日前 2018年5月26日15:44
DBは永続的にデータを保存しますが、セッションはクッキーの削除等で消えます。
データが消えてもそこまで問題がなく、ユーザー毎に管理させたいデータならばセッションが合っています。

例を挙げると、ショッピングサイトにおけるショッピングカートの中身であったり、見た商品等の履歴であったり、このブログならば一度コメントした方はユーザー名をセッションに保存して、次回以降のコメントではそのユーザー名を入力済みにしておく、等も考えられます。

ただ、リアルタイムなアクションゲームをブラウザで行う予定ならば、Django側でなくJavaScriptでの処理をメインにしたほうが恐らく良いです。弾数はJavaScriptのある変数で扱うことになると思います。
生徒 約120日前 2018年5月26日18:00
詳細な回答を頂きまして誠に有難うございます。
データを保存するときのそれぞれの問題点についてよく理解できました。
頂いた意見を基にして、開発を進めます。
有難うございました!