見出し画像

Python tkinterを使ってOpenAIで画像を生成させるプログラム⑤

今回はリファクタリングネタです。
コードがかなり粗雑になっていたのは勿論ですが、tkinterのButtonとCanvasの仕様に満足できなかったためクラスを継承して機能の拡張を実施しました。その結果は良好でコードの可読性があがりました。
まずは新規にoagui_controls.pyを作成してButtonとCanvasの派生クラスを実装しましたのでそちらから。

# oagui_controls.py

import tkinter as tk
from tkinter import messagebox

class Button(tk.Button):
    def __init__(self, master=None, Tag=None, cnf={}, **kw):
        tk.Button.__init__(self, master, cnf, **kw)
        self.Tag = Tag

    def bindCommand(self, target):
        binds = {"<space>", "<Return>", "<ButtonRelease-1>"}
        for b in binds:
            self.bind(b, target)

class Canvas(tk.Canvas):
    TAG_ID = "photo"
    OriginImage = None
    def __init__(self, master=None, cnf={}, **kw):
        tk.Canvas.__init__(self, master, cnf, **kw)
    
    def hasImage(self):
        return self.OriginImage != None
    
    def deleteImage(self):
        self.photo = None
        self.OriginImage = None
        super().delete(self.TAG_ID)
    
    def create_image(self, tag=TAG_ID, *args, **kw) -> int:
        return super().create_image(tag, *args, **kw)

Buttonの派生クラスについて、Tagフィールドを追加しました。バリエーションボタンを配置するとき対になるCanvasのインスタンスを指定しておくことによってボタンクリック時に対象のCanvasを素早くスマートに取得することが可能です。
bindCommandですがマウスクリックの他、フォーカスがある時のスペースキーやエンターキー押下に対応するためのBindを一度に行なってしまおうというためのメソッドになります。
Buttonは通常commandに任意のハンドラを渡すことが想定されていて、クリック、スペースキー、エンターキーなどのアクションに対して任意のメソッドが実行できるのですが、残念ならがどのボタンから呼び出されたかの判定ができません。Button毎に異なるハンドラを用意すれば良いのですが、それでは同じようなコードが連続してよろしくありませんのでcommandに頼るのはやめておくことにし、bindCommandメソッドを実装しました。bindであればevent.widgetでイベント発火元にアクセスできるからです。

Canvasの派生クラスについて、このプログラムでは1024x1024の画像をAPIで取得したあと、200x200(現在の仕様)に縮小してCanvasに貼り付けています。オリジナルイメージをどこかに保存する必要があるのですが前回まではoriginImagesという辞書で管理していましたがコードが煩雑になってしまうので派生クラス内にフィールドとしてOriginImageを実装しました。
これのおかげでメインモジュールはスッキリして可読性がかなり高まったと思います。またcreate_imageメソッドをオーバーライドして貼り付ける画像のタグ(Canvasから削除する時に必要)を"photo"を既定値にしてメインモジュール側で意識しなくとも良い仕様になっています。その他関連するメソッドを追加しました。

次にメインモジュールです。

# oagui.py

import tkinter as tk
from tkinter import messagebox
from PIL import Image, ImageTk
from oacont import OpenAIController, OpenAIError
from dlcont import DeepLController
from io import BytesIO
from oagui_imagedialog import ImageDialog
import oagui_controls as cnt

OPENAI_API_KEY = "あなたのOpenAIのAPIキーどすえ"
DEEPL_API_KEY = "あなたのDeepLのAPIキーどすえ"
MODEL_DAVINCI = 0
MODEL_CURIE = 1
MODEL_BABBAGE = 2
MODEL_DEEPL = 3
MODEL_NONE = 4

class Application(tk.Frame):
    NUMBER_OF_IMAGES = 4
    CANVAS_BASE_X = 14
    CANVAS_BASE_Y = 200
    cans = {}

    def __init__(self,master = None):
        super().__init__(master)
        self.pack()
        self.model = tk.IntVar()
        self.model.set(MODEL_DEEPL)

        master.geometry("860x450")
        master.title("OpenAiで画像生成しちゃうよん")
        self.oac = OpenAIController(OPENAI_API_KEY)
        self.dlc = DeepLController(DEEPL_API_KEY)
        self.__create_controls()
    
    def __create_canvases(self):
        i = 0
        for i in range(0, self.NUMBER_OF_IMAGES):
            can = cnt.Canvas(self.master, bg="white", width=200, height=200)
            can.place(x=self.CANVAS_BASE_X + (i * 210), y=self.CANVAS_BASE_Y)
            can.bind("<ButtonRelease-1>", self.__canvas_Click)
            self.cans[i] = can
            vbtn = cnt.Button(self.master, text="バリエーション", Tag=can)
            vbtn.place(x=self.CANVAS_BASE_X + (i * 210), y=self.CANVAS_BASE_Y + 204)
            vbtn.bindCommand(self.__variation_button_Click)

    def __create_controls(self):
        tk.Label(text="生成呪文").place(x=20, y=20)
        tk.Label(text="日英翻訳").place(x=20, y=82)
        self.tbKeyword = tk.Text(width=92, height=4)
        self.tbKeyword.place(x=90, y=22)
        cap = ["Davinci", "Curie", "Babbage", "DeepL", "翻訳なし(英文直接入力)"]
        for i in range(0, 5):
            rb = tk.Radiobutton(self.master, value=i, variable=self.model, text=cap[i])
            rb.place(x=90 + (i * 90), y=80)

        self.btnGen = tk.Button(self.master, text="生成しちゃうぞ!",
                          command=self.__btnCreate_Click, width=12, height=5)
        self.btnGen.place(x=750, y=18)
        btnClear = tk.Button(self.master, text="結果を全クリア", command=self.__all_clear)
        btnClear.place(x=16, y=176)
        self.__create_canvases()

    def __all_clear(self):
        for i in range(0, 4):
            self.cans[i].deleteImage()
        self.master.update()
    
    def __canvas_Click(self, event):
        can = event.widget
        if not can.hasImage(): return
        ImageDialog.show(self, can.OriginImage)
    
    def __variation_button_Click(self, event):
        btn = event.widget
        can = btn.Tag
        if not can.hasImage(): return
        try:
            images = self.oac.GenerateVariations(can.OriginImage, self.NUMBER_OF_IMAGES)
        except OpenAIError as e:
            messagebox.showerror("エラーっす", e)
            return
        self.__showGeneratedImages(images)

    def __btnCreate_Click(self):
        images = {}
        wd = self.tbKeyword.get("1.0", "end")
        if (len(wd) == 1): return
        try:
            model = self.model.get() 
            if model == 3:
                wd = self.dlc.Translate(wd)
                print(wd)
            elif model != 4:
                wd = self.oac.TranslateRequestWord(wd, model)
            images = self.oac.GenerateImages(wd, self.NUMBER_OF_IMAGES)
        except OpenAIError as e:
            messagebox.showerror("エラーっす", e)
            return
        self.__showGeneratedImages(images)
    
    def __showGeneratedImages(self, images):
        for i in range(0, self.NUMBER_OF_IMAGES):
            origin = images[i]
            img = Image.open(BytesIO(origin)).resize((200, 200))
            self.cans[i].deleteImage()
            self.cans[i].OriginImage = origin
            self.cans[i].photo = ImageTk.PhotoImage(img)
            self.cans[i].create_image(0, 0, image=self.cans[i].photo, anchor=tk.NW)
    
def main():
    win = tk.Tk()
    app = Application(master=win)
    app.mainloop()

if __name__ == "__main__":
    main()

新規の2つのクラス利用とリファクタリングによって前回に比べてかなりすっきりしました。特にOriginalImageの取り扱いがスマートになったと思います。
他のファイルの変更点はありません。

リファクタリングについて思うこと

個人的な動けばいいやっていうプログラムを作るにあたってリファクタリングは要りません。しかし、私個人はついついリファクタリングに走ってしまいます。誰にも見せるわけでもないコードでもどうしてもリファクタリングしてしまいます。これは性格ですね。
今まで他の人が書いたコード見ることが幾度となくあったのですが、リファクタリング出来てない人ほどバグが多かったり、処理に時間がかかったりすることが多いように思います。偏見かも知れません。
後々のためにもリファクタリングはしていきたいものです。このプログラムもメソッドやフィールドの命名規則に迷いがあります。Pythonプログラミングの作法が色々と分かってきたら完全体にしていきたいなと思います。

この記事が気に入ったらサポートをしてみませんか?