naritoブログ

このブログはDjangoとBootstrap4で作成されました
ソースコード

tkinterで、簡易電卓を作る①レイアウトを作成する

プログラミング関連 Python Tkinter 約62日前
2017年8月16日17:58
まずは、とりあえず画面が表示されるものを作っていきましょう。
コードは以下です。
from tkinter import *
from tkinter import ttk


class CalcApp(ttk.Frame):
    """電卓アプリ(予定)."""

    def __init__(self, master=None):
        super().__init__(master)


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


if __name__ == '__main__':
    main()


これで実行すると、以下のように表示されます。



tkinterでは、以下のようにimportすることが多いです。
from tkinter import *


ttkは、Tk 8.5から導入されたテーマ付きウィジェットです。
これを使うことで見た目を簡単に切り替えることができたり、Combobox, Notebook, Progressbar, Separator, Sizegrip, Treeviewといった新しいウィジェットが利用できるようになります。
from tkinter import ttk



公式ドキュメントには、以下のような書き方が紹介されています。
こう書くと基本的な Tk ウィジェットをttkのウィジェットで上書きできるというものです。
これは便利でもありますが、一部のウィジェットをttkにしたり、一部をプレーンな従来のウィジェットにしたい、ということはできなくなります。
from tkinter import *
from tkinter.ttk import *



今回、ttkのテーマ付きウィジェットに関しては「ttk.Button」のようにアクセスすることにします。
ttkにしかないもの、ttkにはないものがそれぞれありますので、混乱しないように最低限片方は名前空間を利用すると良いのではないでしょうか。
from tkinter import *
from tkinter import ttk


もちろん、以下のように両方とも名前空間を利用するのも良いブラクティクスです。これが一番丁寧ですね。
import tkinter as tk
import tkinter.ttk as ttk  # from tkinter import ttk でも良い。import tkinterの書き方に合わせてimport tkinter.ttk としただけ


main関数です。
root = Tk()でTk のトップレベルウィジェットを生成し、title()でタイトルを設定します。
CalcApp(root)は、ボタンやラベルなどを載せるための枠組みを作成します。
イメージとしては、遠足なんかで使う敷物です。地面(root=Tk()、トップレベルウィジェット)にモノ(ボタンやラベル)を直接置くと汚いですよね?そこで、CalcAppという敷物を利用し、この上にボタンなどを置いていきます。敷物を作る際は、ttk.Frameクラスを継承するのが一般的です。
最後、mainloop()で実際に動作させていきます。
基本的にはこの流れになります。
def main():
    root = Tk()
    root.title('簡単電卓')
    CalcApp(root)
    root.mainloop()


やろうと思えば、Tk()で作成したトップレベルのウィジェットに直接ボタンを配置することもできます。
実際、ちょっとした小物ならこれで十分なこともあります。
しかし、このトップレベルのウィジェットはttkのテーマ、スタイル機能をサポートしていません。
なので、ある程度キレイな見た目にしようと思うと、ttk.Frame などにボタンなどを配置することになります。
これがどういう影響があるかは、③で少し触れます。


これがメインのウィンドウとなるクラスです。
まだ特に処理は書いていません。
class CalcApp(ttk.Frame):
    """電卓アプリ."""

    def __init__(self, master=None):
        super().__init__(master)




tkinterでのレイアウトの配置ですが、3つの種類があります。
placeという各位置をハードコーディングするものと、縦や横に一列に配置するpack、y行目のx列目、というように配置するgridです。
今回はgridを使っていきます。htmlのtableレイアウトが分かる人ならば、すぐに覚えることができるでしょう。

試しに3*3のレイアウトをgridで作成してみます。
class CalcApp(ttk.Frame):
    """電卓アプリ."""

    def __init__(self, master=None):
        super().__init__(master)
        self.create_widgets()

    def create_widgets(self):
        """ウィジェットの作成、配置."""
        # 1列目
        ttk.Button(self, text='横0, 縦0').grid(column=0, row=0)
        ttk.Button(self, text='横1, 縦0').grid(column=1, row=0)
        ttk.Button(self, text='横2, 縦0').grid(column=2, row=0)

        # 2列目
        ttk.Button(self, text='横0, 縦1').grid(column=0, row=1)
        ttk.Button(self, text='横1, 縦1').grid(column=1, row=1)
        ttk.Button(self, text='横2, 縦1').grid(column=2, row=1)

        # 3列目
        ttk.Button(self, text='横0, 縦2').grid(column=0, row=2)
        ttk.Button(self, text='横1, 縦2').grid(column=1, row=2)
        ttk.Button(self, text='横2, 縦2').grid(column=2, row=2)

        # Frame自身もトップレベルウィジェットに配置
        self.grid(column=0, row=0)



以下のような表示になります。


ウィジェットの作成と配置は、丁寧に書くと以下のようになります。上のコードは、これを1行で書いているだけです。
大体の意味は分かるかと思います。
button =  ttk.Button(self, text='横0, 縦0')
button.grid(column=0, row=0)


以下を忘れると表示されません。気をつけましょう。
このCalcApp(self)自身も、トップレベルのウィジェットに明示的に配置する必要があります。
self.grid(column=0, row=0)



ここで、一つ問題があります。拡大してみると、以下の画像のようにコンテンツ部分が変化しません。


場合によりますが、一般的には拡大されるとコンテンツも引き伸ばされるのが理想でしょう。
そこで、以下のように追記します。
    def create_widgets(self):
        """ウィジェットの作成、配置."""
        ...
        ...
        ...
        # 各列の引き伸ばし設定
        self.columnconfigure(0, weight=1)
        self.columnconfigure(1, weight=1)
        self.columnconfigure(2, weight=1)

        # 各行の引き伸ばし設定
        self.rowconfigure(0, weight=1)
        self.rowconfigure(1, weight=1)
        self.rowconfigure(2, weight=1)

        # トップレベルのウィジェットも引き伸ばしに対応させる
        self.master.columnconfigure(0, weight=1)
        self.master.rowconfigure(0, weight=1)


以下は、0, 1, 2列目の引き伸ばし設定です。
weightは、割合のことで、ウィンドウが横に伸ばされると0,1,2列目が1:1:1の割合で広がります。
ウィンドウが30px伸ばされれば、各列は10pxずつ伸びる、ということになりますね。
self.columnconfigure(0, weight=1)
self.columnconfigure(1, weight=1)
self.columnconfigure(2, weight=1)


こちらは0,1,2行目の引き伸ばし設定です。中身はcolumnconfigureと同様です。
self.rowconfigure(0, weight=1)
self.rowconfigure(1, weight=1)
self.rowconfigure(2, weight=1)



このCalcApp(self)自身もまた、トップレベルのウィジェットに配置しているというのはさっき言いました。grid(column=0, row=0)としました。
トップレベルのウィジェットも、同様に引き伸ばしの設定をする必要があります。それが以下の設定です。
self.master.columnconfigure(0, weight=1)
self.master.rowconfigure(0, weight=1)



ウィンドウを引き伸ばすと、以下のようになります。
何らかの力が働いているのはわかりますが、まだ上手く動作しないようです。



もう少し修正しましょう。
    def create_widgets(self):
        """ウィジェットの作成、配置."""
        # 1列目
        ttk.Button(self, text='横0, 縦0').grid(column=0, row=0, sticky=(N, S, E, W))
        ttk.Button(self, text='横1, 縦0').grid(column=1, row=0, sticky=(N, S, E, W))
        ttk.Button(self, text='横2, 縦0').grid(column=2, row=0, sticky=(N, S, E, W))

        # 2列目
        ttk.Button(self, text='横0, 縦1').grid(column=0, row=1, sticky=(N, S, E, W))
        ttk.Button(self, text='横1, 縦1').grid(column=1, row=1, sticky=(N, S, E, W))
        ttk.Button(self, text='横2, 縦1').grid(column=2, row=1, sticky=(N, S, E, W))

        # 3列目
        ttk.Button(self, text='横0, 縦2').grid(column=0, row=2, sticky=(N, S, E, W))
        ttk.Button(self, text='横1, 縦2').grid(column=1, row=2, sticky=(N, S, E, W))
        ttk.Button(self, text='横2, 縦2').grid(column=2, row=2, sticky=(N, S, E, W))

        # Frame自身もトップレベルウィジェットに配置
        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.rowconfigure(0, weight=1)
        self.rowconfigure(1, weight=1)
        self.rowconfigure(2, weight=1)

        # トップレベルのウィジェットも引き伸ばしに対応させる
        self.master.columnconfigure(0, weight=1)
        self.master.rowconfigure(0, weight=1)


grid()にsticky=(N, S, E, W)を足しています。
Button(self, text='横0, 縦0').grid(column=0, row=0, sticky=(N, S, E, W))
...
...
...
self.grid(column=0, row=0, sticky=(N, S, E, W))


すると、今度はしゃんとしました。


stickyとは何でしょうか。これは粘着するとか、そんな意味を持つ単語です。
その意味のとおり、ウィジェットをどこに貼り付けるか、の指定です。
N, S, E, Wは、それぞれNorth(北), South(南), East(東), West(西)のことです。


試しに、以下のようなレイアウトを作りましょう。1行で、4列あります。
    def create_widgets(self):
        """ウィジェットの作成、配置."""
        ttk.Button(self, text='上にくっつく').grid(column=0, row=0, sticky=N)
        ttk.Button(self, text='下にくっつく').grid(column=1, row=0, sticky=S)
        ttk.Button(self, text='左側にくっつく').grid(column=2, row=0, sticky=W)
        ttk.Button(self, text='右側にくっつく').grid(column=3, row=0, sticky=E)
        self.grid(column=0, row=0, sticky=(N, S, E, W))

        self.rowconfigure(0, weight=1)
        self.columnconfigure(0, weight=1)
        self.columnconfigure(1, weight=1)
        self.columnconfigure(2, weight=1)
        self.columnconfigure(3, weight=1)

        self.master.columnconfigure(0, weight=1)
        self.master.rowconfigure(0, weight=1)



すると、以下のように表示されます。sticky自体の指定は上手くいっており、各方角にウィジェットが寄っています。
お分かりかもしれませんが、grid()で分割し、引き伸ばしの設定をしても、ボタン等のウィジェット自体の大きさは元のままです。



今度は以下のようにしてみましょう。
    def create_widgets(self):
        """ウィジェットの作成、配置."""
        ttk.Button(self, text='北にくっつく').grid(column=0, row=0, sticky=N)
        ttk.Button(self, text='上下').grid(column=1, row=0, sticky=(N, S))
        ttk.Button(self, text='左右').grid(column=2, row=0, sticky=(E, W))
        ttk.Button(self, text='上下左右').grid(column=3, row=0, sticky=(N, S, E, W))
        self.grid(column=0, row=0, sticky=(N, S, E, W))

        self.rowconfigure(0, weight=1)
        self.columnconfigure(0, weight=1)
        self.columnconfigure(1, weight=1)
        self.columnconfigure(2, weight=1)
        self.columnconfigure(3, weight=1)

        self.master.columnconfigure(0, weight=1)
        self.master.rowconfigure(0, weight=1)



すると、以下のようになりました。ちゃんとウィンドウのサイズに応じて引き伸ばされています。


stickyに複数の方角を渡すと、それらの方向にウィジェットが引き伸ばされます。(W, E)ならば左右一杯になりますし、(N, S)なら上下一杯になります。
(これはhtmlに慣れている方ならば、{position: absolute; right: 0; left: 0;} のような指定と同じです。)
(N, S, W, S)ならば上下左右一杯、つまり枠を全て使った大きさになります。


話を戻すと、以下の指定はボタンが(割り当てられた)枠一杯になるための指定だった、ということです。
ttk.Button(self, text='横0, 縦0').grid(column=0, row=0, sticky=(N, S, E, W))


ボタンだけでなく、CalcApp(self)自身も同様にするのを忘れないようにしておきましょう。
self.grid(column=0, row=0, sticky=(N, S, E, W))



ここまでくれば、電卓のレイアウトも理解できるはずです。
まずは計算結果の表示欄となるラベルを作りましょう。
columnspan=4は、横に4つ使うという指定です。通常は1列使いますが、これを使うと複数列に配置することができます。rowspanという縦バージョンもあります。
    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)
        )
    


この後は、各ボタンの作成です。素直に実装すると、以下のように続きます。
        ...
        ...
        ttk.Button(self, text='7').grid(column=0, row=1, sticky=(N, S, E, W))
        ttk.Button(self, text='8').grid(column=0, row=1, sticky=(N, S, E, W))
        ttk.Button(self, text='9').grid(column=0, row=1, sticky=(N, S, E, W))
        ttk.Button(self, text='/').grid(column=0, row=1, sticky=(N, S, E, W))


この作業は途中で飽きてきます。
幸いにも、ボタン部分のレイアウトは非常にシンプルです。シンプルなものであれば、forループで作成してきましょう。
# 2次元配列のとおりに、gridでレイアウトを作成する
LAYOUT = [
    ['7', '8', '9', '/'],
    ['4', '5', '6', '*'],
    ['1', '2', '3', '-'],
    ['0', 'C', '=', '+'],
]
...
...
    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))
        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)



まず、以下のような2次元なリストを定義しておきます。
LAYOUT = [
    ['7', '8', '9', '/'],
    ['4', '5', '6', '*'],
    ['1', '2', '3', '-'],
    ['0', 'C', '=', '+'],
]


そして、そこからforループで取り出し、ウィジェットの作成・配置をしているのが以下の部分です。
row=0(1行目)は、結果表示用のラベルを既に作成しています。なので、row=1(2行目)から始まるようにしています。
        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))



縦の引き伸ばし設定にで、self.rowconfigure(0, weight=0)としています。
1行目の結果表示欄は、ウィンドウが引き伸ばされても大きさが変化しない、ということです。(weight=0)
今回のように明示的に書いてもいいですし、面倒ならば、そもそも書かなくても大丈夫です。
        # 縦の引き伸ばし設定。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)


実行すると、以下のように表示されます。


引き伸ばすと、各ボタンは一緒に大きくなっていますが、1行目の結果表示欄の大きさはそのままですね。



全体のソースコードをもう一度貼っておきます。
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))
        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 main():
    root = Tk()
    root.title('簡単電卓')
    CalcApp(root)
    root.mainloop()


if __name__ == '__main__':
    main()