【Python GUI tkinter実用サンプル】tkinterのWidget群を使用して、UIを直感的に作成するソフトウェアのサンプル

<tkinterトップページに戻る>

概要&使い方

直感的にUIの作成ができるソフトウェアのtkinterのサンプルコード。

①widgetsから配置したいパーツを選択

②canvas内でクリックして配置したい箇所へ移動

③必要があればoptionで編集しupdate

④作成メニュー>outputでcanvas内に配置したWidgetを持つアプリケーション(test.py)が作成される

ソースコード説明

ソースコード内に記載されているコメントを参照ください

サンプル画像

①uiBuilderControl.py実行時

uibuilderrun

②複数Widgetを配置

uibuildercreate

③作成メニュー>output押下

uibuildersucceed

④スクリプトが置かれているフォルダにtest.pyが作成される

testpy

⑤作成されたtest.py実行

testpyrun

 

サンプルコード

実行方法

view用スクリプト、コントロール&ロジック用スクリプトどちらも取得ください

※.pyファイルは可能であればフルパスで指定すると良い

python uiBuilderControl.py

View用スクリプト

・uiBuilderView.py

import tkinter.ttk as ttk
from tkinter import *


class LabelEntryWidget(ttk.Frame):
    """
    LabelとEntryがくっついたWidget
    """
    def __init__(self, master,text="property"):
        super().__init__(master)
        self.value = StringVar()
        self.createWidgets(text)

    def createWidgets(self,text="property"):
        self.label = ttk.Label(self,text=text)
        self.label.pack(side="left")
        self.entry = ttk.Entry(self,textvariable=self.value)
        self.entry.pack(side="left")

    def getVar(self):
        """
        値を取得するためのWidget変数の取得
        """
        return self.value


    def setLabelOption(self,key_dict):
        """
        Labelのオプション指定(オプションはdictで渡す)
        """
        for k in key_dict.keys():
            self.label[k] = key_dict[k]


    def setEntryOption(self,key_dict):
        """
        Entryのオプション指定(オプションはdictで渡す)
        """
        for k in key_dict.keys():
            self.entry[k] = key_dict[k]


class WidgetsParts():
    """
    Widget群とCanvasFrameサイズを指定するパーツ
    """
    def __init__(self, parent):
        self.parent = parent
        self.width_var = None
        self.height_var = None
        self.command = None
        self.add_frame = None
        self.option_parts = None
        self.start_xy =None
        self.x_y=None
        self.createWidgets()

    def createWidgets(self):
        self.inputWHWidgets()
        self.inputWidgets()


    def inputWHWidgets(self):
        """
        widthとheightを入力する
        """

        option = {"width":"5"}
        frame = ttk.LabelFrame(self.parent,text="windowsize")
        frame.pack()
        input_frame = ttk.Frame(frame)
        input_frame.pack()
        width_parts = LabelEntryWidget(input_frame,text="width")
        width_parts.pack(side="left")
        width_parts.setEntryOption(option)
        height_parts = LabelEntryWidget(input_frame,text="height")
        height_parts.pack(side="left")
        height_parts.setEntryOption(option)

        self.update_button = ttk.Button(frame,text="update")
        self.update_button.pack()
        self.width_var = width_parts.getVar()
        self.height_var = height_parts.getVar()

    def getWidthVar(self):
        return self.width_var

    def getHeightVar(self):
        return self.height_var

    def setUpdateCommand(self,command):
        self.update_button["command"]  = command

    def inputWidgets(self):
        """
        Widget毎にボタンを作成
        commandには自身のクラスを引数にaddWidgetを登録
        """
        ttk.Button(self.parent,text= "Label",command = lambda : self.addWidget(ttk.Label)).pack()
        ttk.Button(self.parent,text= "Button",command = lambda : self.addWidget(ttk.Button)).pack()
        ttk.Button(self.parent,text= "Entry",command = lambda : self.addWidget(ttk.Entry)).pack()
        ttk.Button(self.parent,text= "CheckBox",command = lambda : self.addWidget(ttk.Checkbutton)).pack()
        ttk.Button(self.parent,text= "RadioButton",command = lambda : self.addWidget(ttk.Radiobutton)).pack()
        ttk.Button(self.parent,text= "ComboBox",command = lambda : self.addWidget(ttk.Combobox)).pack()

    def addWidget(self,widget):
        """
        widgetは作成するWidgetのクラス
        ボタン押下時にWidgetのクラスをオブジェクト化する
        """
        if self.add_frame is None:
            print("none")
            return
        widget = widget(self.add_frame)
        widget.place(x=50,y=50)
        if "text" in widget.keys():
            widget["text"] = "sample"
        widget.bind("<Button-1>",self.move_start)
        widget.bind("<B1-Motion>",self.move_now)
        widget.bind("<ButtonRelease-1>",self.move_end)


    def move_start(self,event):
        """
        Widgetが選択されたときの処理
        ①プロパティパーツの更新をする
        ②マウスカーソルの座標取得(スクリーン位置)
        ③位置情報取得(Canvas内の位置)
        """
        self.option_parts.make_child(event.widget)
        self.start_xy = (event.x_root,event.y_root)
        place_info = event.widget.place_info()
        x = int(place_info['x'])
        y = int(place_info['y'])
        self.x_y = (x,y)
        print(self.add_frame.winfo_reqwidth())


    def move_now(self,event):
        """
        移動中処理
        ①move_startで取得したマウスカーソル位置と現在のマウスカーソル位置で距離を計算
        ②計算した距離を対象のWidget位置に加算
        ③x、yの移動後の座標を検査(Canvas内からはみ出る場合は調整)
        ④再配置
        """
        if self.start_xy is None:
            return
        # 移動距離を調べる
        distance = (event.x_root-self.start_xy[0],event.y_root-self.start_xy[1])
        # 再度座標を設定する
        place_info = event.widget.place_info()

        self.option_parts.setPlaceinfo(place_info)
        x = self.x_y[0] + distance[0]
        y = self.x_y[1] + distance[1]

        if x < 5:
            x = 5
        elif x >self.add_frame.winfo_reqwidth()-event.widget.winfo_reqwidth() - 10:
            x = self.add_frame.winfo_reqwidth()-event.widget.winfo_reqwidth() - 10
        if y < 5:
            y = 5
        elif y>self.add_frame.winfo_reqheight()-event.widget.winfo_reqheight() - 20:
            y = self.add_frame.winfo_reqheight()-event.widget.winfo_reqheight() - 20
        place_info['x'] = x
        place_info['y'] = y
        event.widget.place_configure(place_info)


    def move_end(self,event):
        """
        移動処理が終わったら座標類を初期化
        """
        self.start_xy = None
        self.x_y = None

    def setAddFrame(self,add_frame):
        self.add_frame = add_frame
        self.width_var.set(self.add_frame["width"])
        self.height_var.set(self.add_frame["height"])

    def setOptionParts(self,option_parts):
        self.option_parts = option_parts


class CanvasParts(ttk.Frame):
    """
    Widgetを配置するCanvas
    """
    def __init__(self, master,**kw):
        super().__init__(master,**kw)
        self.pack()

class OptionParts():
    """
    各Widgetのオプション値を編集するパーツ
    """
    def __init__(self, parent):
        self.parent = parent
        self.widget = None
        self.x = None
        self.y = None

    def make_child(self,widgets):
        """
        ①以前のパーツを削除
        ②与えられたWidgetによって編集可能なオプション値を探す。
        ③WidgetのCanvas内位置を変更する編集可能Widgetに座標を入力
        ④target_keyに当てはまるオプションの編集可能Widgetを作成
        """
        self.delete(destroy=False)
        self.widget = widgets
        target_key = ("width","height","text","state")
        option_dict={"width":"7"}
        itemdict = {}
        place_info = widgets.place_info()
        self.createWidgets()
        self.x.set(place_info['x'])
        self.y.set(place_info['y'])
        for key in widgets.keys():
            if key in target_key:
                label = LabelEntryWidget(self.parent,text=key)
                label.pack()
                label.setLabelOption(option_dict)
                itemdict[key] = label.getVar()
                itemdict[key].set(widgets[key])
        def _addCommand():
            """
            update用コマンド
            """
            for item in itemdict.keys():
                widgets[item] = itemdict[item].get()
            place_info['x']=self.x.get()
            place_info['y']=self.y.get()
            widgets.place_configure(place_info)

        update = ttk.Button(self.parent,text = "update",command = _addCommand)
        update.pack()
        delete = ttk.Button(self.parent,text = "destroy",command = self.delete)
        delete.pack()

    def setPlaceinfo(self,place_info):
        """
        Widgetの座標位置を更新(マウスで動かされたときに同期する用)
        """
        self.x.set(place_info['x'])
        self.y.set(place_info['y'])

    def createWidgets(self):
        """
        操作(編集)対象のWidget共通項目
        Widgetの座標位置を編集するWidget
        """
        option_dict={"width":"7"}
        xlabel = LabelEntryWidget(self.parent,text="x")
        xlabel.pack()
        xlabel.setLabelOption(option_dict)
        self.x  = xlabel.getVar()
        ylabel = LabelEntryWidget(self.parent,text="y")
        ylabel.pack()
        ylabel.setLabelOption(option_dict)
        self.y  = ylabel.getVar()

    def delete(self,destroy=True):
        """
        現在編集対象Widgetの編集項目を削除する
        """
        children = self.parent.winfo_children()
        for child in children:
            child.destroy()
        if destroy:
            self.widget.destroy()



class UIBuilderApp(ttk.Frame):
    """
    各パーツを組み立てるメインView
    """
    def __init__(self, master):
        super().__init__(master)
        self.widgets_parts  = None
        self.canvs_parts = None
        self.option_parts = None
        self.output_command = lambda : print("none")
        self.setupMenu()
        self.createWidgets()
        self.pack()

    def createWidgets(self):
        widgets_frame  = ttk.Labelframe(self,text = "widgets",width="190",height="580")
        widgets_frame.propagate(False)
        widgets_frame.pack(side = "left")
        self.widgets_parts = widgetparts = WidgetsParts(widgets_frame)
        canvs_frame  = ttk.Labelframe(self,text = "canvas",width="400",height="580")
        canvs_frame.propagate(False)
        canvs_frame.pack(side = "left")
        self.canvs_parts = CanvasParts(canvs_frame,width="400",height="580")
        option_frame  = ttk.Labelframe(self,text = "option",width="190",height="580")
        option_frame.propagate(False)
        option_frame.pack(side = "left")
        self.option_parts = OptionParts(option_frame)
        self.widgets_parts.setAddFrame(self.canvs_parts)
        self.widgets_parts.setOptionParts(self.option_parts)
        self.setUpdateCommand()

    def getWidgetParts(self):
        return self.widgets_parts
    def getCsanvasParts(self):
        return self.canvs_parts

    def setUpdateCommand(self):

        def command():
            width = self.widgets_parts.getWidthVar().get()
            height = self.widgets_parts.getHeightVar().get()
            self.canvs_parts["width"] = width
            self.canvs_parts["height"] = height
            # 親のFrameもサイズを変更する
            parent = self.canvs_parts.master
            parent["width"] = width
            parent["height"] = height
        self.widgets_parts.setUpdateCommand(command)
    def setupMenu(self):

        menu = Menu(self.master)
        createMenu = Menu(menu,tearoff = 0)
        createMenu.add_command(label="output",command=lambda:self.output_command())
        exitMenu = Menu(menu,tearoff = 0)
        exitMenu.add_command(label="exit",command=lambda:self.master.destroy())
        menu.add_cascade(label="作成",menu = createMenu)
        menu.add_cascade(label="終了",menu = exitMenu)
        self.master.config(menu=menu)

    def getCanvasParts(self):
        """
        Canvasパーツを取得する
        """
        return self.canvs_parts

    def setOutputCommand(self,command):
        """
        ファイル出力コマンドの設定
        """
        print("set")
        self.output_command = command

if __name__ == '__main__':
    master = Tk()
    master.title("UIBuilder")
    master.geometry("800x600")
    UIBuilderApp(master)
    master.mainloop()

コントロール&ロジック用スクリプト

・uiBuilderControl.py

import tkinter.ttk as ttk
from tkinter import *
from uiBuilderView import UIBuilderApp
import os
import tkinter.messagebox as messagebox

class UIBuilderLogic():
    """
    UIBuilderで組み立てたUIのサンプルコードを出力するロジッククラス
    """
    def __init__(self,canvas_parts):
        """
        初期化
        出力ディレクトリはこのファイルと同じ階層
        出力ファイル名はtest.pyとして出力する
        canvas_parts:パーツを配置した親フレーム
        """
        self.output_dir = os.path.dirname(__file__)
        self.output_file_name = os.path.join(self.output_dir,"test.py")
        self.canvas_parts = canvas_parts

    def outputCommand(self):
        """
        実際に出力させるメインメソッド
        ①インポート文出力
        ②UISampleクラス出力
        ③main文出力
        """
        ret = False
        try:
            fo = open(self.output_file_name,"w",newline= "\n")
            self.writeImport(fo)
            self.writeUISample(fo)
            self.writeMain(fo)
            fo.close()
            ret = True
        except IOError as e:
            print(e)
            print("ファイルの出力に失敗しました。")
            ret = False
        return ret

    def writeImport(self,file):
        """
        import文の出力
        file:ファイルオブジェクト
        """
        import_list =[
        "import tkinter.ttk as ttk",
        "from tkinter import *",
        ]
        for item in import_list:
            print(item,file=file)

    def writeUISample(self,file):
        """
        実際のWidget作成
        UISampleクラスを作成しCanvasに配置したWidgetを組み立てる
        フレームサイズは指定サイズで作成
        file:ファイルオブジェクト
        """
        width = self.canvas_parts["width"]
        height = self.canvas_parts["height"]
        print("class UISample(ttk.Frame):",file=file)
        print("\tdef __init__(self, master):",file=file)
        print("\t\tsuper().__init__(master,width='{}',height='{}')".format(width,height),file=file)
        print("\t\tself.createWidgets()",file=file)
        print("\t\tself.propagate(False)",file=file)
        print("\t\tself.pack()",file=file)
        print("\tdef createWidgets(self):",file=file)
        child_idx = 0
        button_command = []
        for child in self.canvas_parts.winfo_children():
            place_info = child.place_info()
            x = place_info['x']
            y = place_info['y']
            class_name = child.__class__.__name__

            option = ""
            target_key = ("width","height","text","state","command")
            for ck in child.keys():
                if ck in target_key:
                    if ck == "command":
                        command_function = "self.commandFunction{}".format(child_idx)
                        button_command.append(command_function)
                        option = option + "{} = {},".format(ck,command_function)
                    else:
                        option  =option + "{} = \"{}\",".format(ck,child[ck])
            print("\t\twidget_{} = ttk.{}(self,{})".format(child_idx,class_name,option),file=file)
            print("\t\twidget_{}.place(x={},y={})".format(child_idx,x,y),file=file)
            child_idx +=1
        self.writeCommandFunction(file,button_command)

    def writeCommandFunction(self,file,command_list):
        """
        ボタンコマンドを作成する
        とりあえずなにもしないpass文を書いておく
        command_list:ボタンの関数リスト
        """
        for command in command_list:
            func_name =  command.replace("self.","")
            print("\tdef {}(self):".format(func_name),file=file)
            print("\t\tpass",file=file)

    def writeMain(self,file):
        """
        main文の作成
        file:ファイルオブジェクト
        """
        width = self.canvas_parts["width"]
        height = self.canvas_parts["height"]
        main_list=[
        "if __name__ == '__main__':",
        "\tmaster = Tk()",
        "\tmaster.title('UISample')",
        "\tmaster.geometry(\"{}x{}\")".format(width,height),
        "\tUISample(master)",
        "\tmaster.mainloop()"
        ]
        for item in main_list:
            print(item,file=file)


class UIBuilderControl():
    """
    UIBuilderのViewとLogicの管理を行うコントロール
    """
    def __init__(self):
        """
        UIBilderの起動を行う
        """
        master = Tk()
        master.title("UIBuilder")
        master.geometry("800x600")
        self.view =view= UIBuilderApp(master)
        self.logic = UIBuilderLogic(view.getCanvasParts())
        self.setupMenu()
        master.mainloop()
    def setupMenu(self):
        """
        作成メニューのoutputにsetOutputCommandを紐づける
        """
        self.view.setOutputCommand(self.setOutputCommand)

    def setOutputCommand(self):
        """
        ロジックの出力メソッドを実行する
        実行結果によってメッセージボックスを切り替え
        """
        ret = self.logic.outputCommand()
        if ret:
            messagebox.showinfo("output", "succeed")
        else:
            messagebox.showerror("output", "failed")


if __name__ == '__main__':
    UIBuilderControl()

あわせて読みたい