t-hom’s diary

主にVBAネタを扱っているブログ…とも言えなくなってきたこの頃。

Python Tkinterで画面遷移

前回作成したPythonのルーチン読み上げツールだが、GUIメニューを作ってそこから起動するような形にしてみた。
thom.hateblo.jp

動機は、将来的にツールを増やしていく場合、Bashスクリプトのダブルクリックで起動するのが面倒だから。机に立てかけたタッチスクリーンでダブルタップするのって結構難しく、よくファイル名の変更になってしまったりドラッグになってしまったりする。

そこで、メニュー画面を常時起動させて、そこでアプリケーションの切り替え操作を完結したいというのが動機。

パク参考にしたのは以下のQA。
www.reddit.com

モジュール構成

画面遷移に使用するのはメニューモジュールが一つと、未登録用のダミーアプリケーション、前回作成したお掃除ルーチンアプリケーションの3つ。他のはcleanup.pyから呼ばれるplaysound.pyがある。前回紹介してなかったのでついでにコードを掲載しておく。

  • menu.py
  • dummy.py
  • cleanup.py
    • playsound.py

お掃除ルーチンはメニューに対応させるため単体起動できなくなっている。

画面遷移のイメージ

f:id:t-hom:20190928071133p:plain

コード

menu.py

# https://www.reddit.com/r/learnpython/comments/776kd9/tkinterhow_can_i_destroyhide_the_root_window/
import tkinter as tk
from cleanup import CleanUpRoutine
from dummy import DummyApp
class FrameBase(tk.Tk):
    def __init__(self):
        tk.Tk.__init__(self)
        self.geometry("800x480")
        self.frame = StartPageFrame(self)
        self.frame.pack(expand=True, fill="both")
        #self.attributes("-fullscreen", True)

    def change(self, frame):
        self.frame.pack_forget() # delete currrent frame
        self.frame = frame(self)
        self.frame.pack(expand=True, fill="both") # make new frame

    def backToStart(self):
        self.frame.pack_forget()
        self.frame = StartPageFrame(self)
        self.frame.pack(expand=True, fill="both")

class StartPageFrame(tk.Frame):
    def __init__(self, master=None, **kwargs):
        tk.Frame.__init__(self, master, **kwargs)
        master.title("Start Page")

        self.grid(column=0, row=0, sticky=tk.NSEW)

        self.Applist = [
                    [ [CleanUpRoutine, "Clean up routine"], [DummyApp, "Not Registerd"], [DummyApp, "Not Registerd"] ], 
                    [ [DummyApp, "Not Registerd"], [DummyApp, "Not Registerd"], [DummyApp, "Not Registerd"] ]
                    ]

        lbl = tk.Label(master=self, text ="Start Page", font=("Migu 1M",14))
        lbl.grid(column=0,row=0,sticky=tk.NW, padx=10)
        btn = tk.Button(
            master = self,
            text="Close",
            width = 5,
            bg = "#dc143c",
            fg = "#ffffff",
            command=self.master.destroy)
        btn.grid(column=2, row=0,sticky=tk.NE)

        for r in range(1,3):
            for c in range(3):
                btn = tk.Button(
                        master=self,
                        wraplength=150,
                        justify=tk.LEFT,
                        text=self.Applist[r-1][c][1],
                        font=("Migu 1M", 16),
                        bg="#e6e6fa",
                        command=self.gotoApp(r-1,c))
                btn.grid(column=c, row=r, padx=10, pady=10, sticky=tk.NSEW)

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

        self.rowconfigure(1, weight=1)
        self.rowconfigure(2, weight=1)

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

    def gotoApp(self,r,c):
        return lambda :self.master.change(self.Applist[r][c][0])

if __name__=="__main__":
    app=FrameBase()
    app.mainloop()

dummy.py

import tkinter as tk
class DummyApp(tk.Frame):
    def __init__(self, master=None, **kwargs):
        tk.Frame.__init__(self, master, **kwargs)
        master.title("No application registered.")

        btn = tk.Button(master=self,
                text="Back",
                width=5,
                bg = "#00a4e4",
                fg = "#ffffff",
                command=self.master.backToStart)
        btn.pack(anchor=tk.NW)

        lbl = tk.Label(self, text="No application registered here.", height=5, font=("Migu 1M",20))
        lbl.pack()

cleanup.py

import tkinter as tk
import tkinter.font as font
import os
import playsound

class CleanUpRoutine(tk.Frame):
    def __init__(self, master=None, **kwargs):
        tk.Frame.__init__(self, master, **kwargs)
        self.load_resources()
        self.create_widgets()
        self.n = 0

    def load_resources(self):
        self.base = os.path.dirname(os.path.abspath(__file__))
        self.icon1 = tk.PhotoImage(file=self.base + '/next.png')
        self.icon2 = tk.PhotoImage(file=self.base + '/next_disabled.png')

    def create_widgets(self):
        #Root Setting
        self.master.title("Cleanup Routine")

        #Generate Parts
        frame1 = tk.Frame(master = self)

        quit_button = tk.Button(
            master = frame1,
            command = self.master.backToStart,
            width = 5,
            text = "Back",
            bg = "#00a4e4",
            fg = "#ffffff")

        next_button = tk.Canvas(
            master = self,
            width = 128,
            height = 128)
        self.imagearea = next_button.create_image(
                0, 0, image=self.icon1, anchor=tk.NW)
        next_button.bind("<Button-1>", self.next_clicked)

        self.var = tk.StringVar()
        my_font = font.Font(self,family="Migu 1M",size=20,weight="normal")
        lbl = tk.Label(self, textvariable=self.var, height=5, font=("Migu 1M",20))
        #Layout
        frame1.pack(fill="x")
        quit_button.pack(side="left")
        lbl.pack()
        next_button.pack()

        #Localization
        self.frame1 = frame1
        self.quit_button = quit_button
        self.next_button = next_button
        self.lbl = lbl

    def play(self, filenumber):
        txt = open(self.base + "/wav/CleanupRoutine-" + str(filenumber) + ".txt", 'r', encoding='utf-8')
        self.var.set(txt.read())
        self.master.update()
        txt.close
    
        filename=self.base + "/wav/CleanupRoutine-" + str(filenumber) + ".wav"
        playsound.playAudio(filename)

    def next_clicked(self, event):
        self.next_button.itemconfig(self.imagearea, image = self.icon2)
        self.master.update()
        n = self.n
        self.play(n)
        n = n + 1
        if n > 14:
            n = 0
        self.n = n
        self.next_button.itemconfig(self.imagearea, image = self.icon1)
        self.master.update()
playsound.py
import pyaudio
import wave

def playAudio(fname):
    CHUNK = 1024

    wf = wave.open(fname, 'rb')
    
    p = pyaudio.PyAudio()
    
    stream = p.open(format=p.get_format_from_width(wf.getsampwidth()),
                    channels=wf.getnchannels(),
                    rate=wf.getframerate(),
                    output=True)
    
    """ 
       format  : ストリームを読み書きするときのデータ型
       channels: ステレオかモノラルかの選択 1でモノラル 2でステレオ
       rate    : サンプル周波数
       output  : 出力モード
    
    """
    
    # 1024個読み取り
    data = wf.readframes(CHUNK)
    
    while data != b'':
        stream.write(data)          # ストリームへの書き込み(バイナリ)
        data = wf.readframes(CHUNK) # ファイルから1024個*2個の
    
    stream.stop_stream()
    stream.close()
    
    p.terminate()

ざっくりアプリケーション切り替えの仕組み

Tkinterでは下図のようにメイン画面に直接ボタンやラベル等のGUIパーツを配置してアプリケーションを作成できる。
f:id:t-hom:20190928065032p:plain

TkinterにはGUIパーツを纏めるフレームというものが用意されているので、今回は直接GUIを配置せずにアプリケーションのGUIは全てフレームに乗せている。こうすることで、フレームの乗せ換えによってアプリケーション画面の切り替えを実現することができる。
f:id:t-hom:20190928065237p:plain

具体的には前回の記事でcleanup.pyのクラスはtk.Tkを継承していたが、今回はtk.Frameから継承させて1枚のフレームとして構成している。

class CleanUpRoutine(tk.Frame):

実際のフレーム切り替えは、menu.pyのFrameBaseクラスのメソッド「change」で行われる。

    def change(self, frame):
        self.frame.pack_forget() # delete currrent frame
        self.frame = frame(self)
        self.frame.pack(expand=True, fill="both") # make new frame

インスタンス変数「self.frame」と、仮引数「frame」が同名なのでやや混乱するかもしれないが、これはもともと参考にしたサイトがそうだったたし、pythonのコーディングの流儀みたいなものはまだわかってないので下手にいじらないでおこうと思ったためそのままにした。PEP見ろって話だろうけど、まだ私のPythonコーディングはお試し期間みたいなものなので、そこまで本腰入れるのはもう少し後。

当ブログは、amazon.co.jpを宣伝しリンクすることによってサイトが紹介料を獲得できる手段を提供することを目的に設定されたアフィリエイト宣伝プログラムである、 Amazonアソシエイト・プログラムの参加者です。