Tkinterでダイアログをカスタマイズする方法

## 概要

GUIオブジェクトを作っていく中で、既に用意されているダイアログだけでなく、
ユーザー自身でカスタマイズしたダイアログを使用したくなる場面があります。

カスタマイズ可能なダイアログとして、Tkinterには**simpledialog.Dialog**があります。

カスタマイズ方法について、本記事で解説いたします。

## 環境

Python3系

## デフォルト状態のダイアログ

何もカスタマイズしない状態では、**simpledialog.Dialog**に**OK**と**Cancel**のボタンしかありません。

![tkinter_demo_dialog.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302006/c4e60046-7c34-3db5-6621-38952923223d.jpeg)
上記は**root**ウィンドウです。**ダイアログ表示**ボタンを押すと以下のダイアログが表示されます。

![tkinter_demo_simple_dialog_no_custom.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302006/0107fa31-2ad3-077b-65a5-f53d5c9eb2d3.jpeg)

```python

import tkinter
from tkinter import *
from tkinter import simpledialog


if __name__ == "__main__":
    root = tkinter.Tk()

    def display_dialog():
        simpledialog.Dialog(root)

    button = Button(root)
    button["text"] = "ダイアログ表示"
    button["command"] = display_dialog
    button.grid(column=0, row=0, padx=10, pady=10)

    root.mainloop()

```

## ダイアログの基本的なカスタマイズ方法

### 任意のオブジェクトをダイアログへ埋め込む

ダイアログには基本的なオブジェクト（ラベル、エントリー、コンボボックス、ボタンなど）を埋め込むことができます。
以下のコードではエントリーオブジェクトを埋め込みます。

![tkinter_demo_simple_dialog_add_entry.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302006/fe748c48-5fdf-31ff-57cc-a5b3c588af98.jpeg)


```python
import tkinter
from tkinter import *
from tkinter import simpledialog


class CustomDialog(simpledialog.Dialog):
    def __init__(self, master, title=None) -> None:
        super(CustomDialog, self).__init__(parent=master, title=title)

    def body(self, master) -> None:
        """
        Dialogオブジェクトへ配置するオブジェクトを定義する。

        Parameters
        ----------
        master:
            Dialogオブジェクトの親オブジェクト

        Returns
        -------
        None
        """

        entry: Entry = Entry(master)
        entry.grid(column=0, row=0)


if __name__ == "__main__":
    root = tkinter.Tk()

    def display_dialog():
        CustomDialog(root)

    button = Button(root)
    button["text"] = "ダイアログ表示"
    button["command"] = display_dialog
    button.grid(column=0, row=0, padx=10, pady=10)

    root.mainloop()

```

任意のオブジェクトをダイアログへ埋め込むために以下を実施しています。

1. **simpledialog.Dialog**を継承した**CustomDialog**というクラスを作成します。
1. ダイアログを初期化するために、**simpledialog.Dialog**の**\_\_init\_\_**メソッドを実行します（super(CustomDialog, self).__init__(parent=master, title=title)）。
1. ダイアログへ配置するオブジェクトを、**body**メソッドへ記載します。

**simpledialog.Dialog**に実装されている**body**メソッドをオーバーライドすることで、ユーザーが自由にダイアログ内のオブジェクトを定義できます。

### OKボタンが押された後の処理

OKボタンが押された後、何らかの処理を行いたい場合があると思います。
例えば、エントリーオブジェクトに入力された文字列を出力したい場合などです。

そのような場合には**apply**メソッドをオーバーライドします。

以下のコードではエントリーオブジェクト内の文字列を取得して、コンソールへ出力します。

```python
import tkinter
from tkinter import *
from tkinter import simpledialog


class CustomDialog(simpledialog.Dialog):
    def __init__(self, master, title=None) -> None:
        super(CustomDialog, self).__init__(parent=master, title=title)

    def body(self, master) -> None:
        """
        Dialogオブジェクトへ配置するオブジェクトを定義する。

        Parameters
        ----------
        master:
            Dialogオブジェクトの親オブジェクト

        Returns
        -------
        None
        """

        self.entry: Entry = Entry(master)
        self.entry.grid(column=0, row=0)

    def apply(self) -> None:
        """
        このDialogオブジェクトが破棄される際に実行される処理を定義する。

        Parameters
        ----------

        Returns
        -------
        None
        """
        print(self.entry.get())


if __name__ == "__main__":
    root = tkinter.Tk()

    def display_dialog():
        CustomDialog(root)

    button = Button(root)
    button["text"] = "ダイアログ表示"
    button["command"] = display_dialog
    button.grid(column=0, row=0, padx=10, pady=10)

    root.mainloop()

```

### バリデーション

OKボタンが押され、ダイアログが削除される際にバリデーション（検証）を行うこともできます。
**validate**メソッドをオーバーライドします。

例えば、エントリーオブジェクトに入力した文字列の文字数が不足している場合に、エラーメッセージを出すことが可能です。

![tkinter_demo_simple_dialog_add_validate.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302006/1e83a468-034c-50b8-4da7-30f8598e9676.jpeg)

```python
import tkinter
from tkinter import *
from tkinter import simpledialog


class CustomDialog(simpledialog.Dialog):
    def __init__(self, master, title=None) -> None:
        super(CustomDialog, self).__init__(parent=master, title=title)

    def body(self, master) -> None:
        """
        Dialogオブジェクトへ配置するオブジェクトを定義する。

        Parameters
        ----------
        master:
            Dialogオブジェクトの親オブジェクト

        Returns
        -------
        None
        """
        self.label: Label = Label(master)
        self.label["text"] = "文字数が足りません。"
        self.label["fg"] = "red"

        self.entry: Entry = Entry(master)
        self.entry.grid(column=0, row=0)

    def apply(self) -> None:
        """
        このDialogオブジェクトが破棄される際に実行される処理を定義する。

        Parameters
        ----------

        Returns
        -------
        None
        """
        print(self.entry.get())

    def validate(self) -> bool:
        """
        このDialogオブジェクトが破棄される際に行う検証を定義する。

        Parameters
        ----------

        Returns
        -------
        None
        """
        if len(self.entry.get()) > 4:
            return True
        else:
            self.label.grid(column=0, row=1)
            return False


if __name__ == "__main__":
    root = tkinter.Tk()

    def display_dialog():
        CustomDialog(root)

    button = Button(root)
    button["text"] = "ダイアログ表示"
    button["command"] = display_dialog
    button.grid(column=0, row=0, padx=10, pady=10)

    root.mainloop()

```

## ダイアログの少し発展的なカスタマイズ方法

### 背景色変更

ダイアログの背景色を変更したい場合は、**\_\_init\_\_**メソッドをオーバーライドします。

![tkinter_demo_simple_dialog_bg_color_change.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302006/021b4d23-e4ef-43b6-f0db-ae6d99494c76.jpeg)


```python
import tkinter
from tkinter import *
from tkinter import simpledialog


class CustomDialog(simpledialog.Dialog):
    def __init__(self, master, title=None) -> None:
        parent = master

        '''
        ダイアログの初期化
        背景色を変えるためにオーバーライドしている。
        '''
        Toplevel.__init__(self, parent, bg="red")  # 背景色の変更

        self.withdraw()  # remain invisible for now
        # If the master is not viewable, don't
        # make the child transient, or else it
        # would be opened withdrawn
        if parent.winfo_viewable():
            self.transient(parent)

        if title:
            self.title(title)

        self.parent = parent

        self.result = None

        body = Frame(self)
        self.initial_focus = self.body(body)
        body.pack(padx=5, pady=5)

        self.buttonbox()

        if not self.initial_focus:
            self.initial_focus = self

        self.protocol("WM_DELETE_WINDOW", self.cancel)

        if self.parent is not None:
            self.geometry("+%d+%d" % (parent.winfo_rootx() + 50,
                                      parent.winfo_rooty() + 50))

        self.deiconify()  # become visible now

        self.initial_focus.focus_set()

        # wait for window to appear on screen before calling grab_set
        self.wait_visibility()
        self.grab_set()
        self.wait_window(self)


if __name__ == "__main__":
    root = tkinter.Tk()

    def display_dialog():
        CustomDialog(root)

    button = Button(root)
    button["text"] = "ダイアログ表示"
    button["command"] = display_dialog
    button.grid(column=0, row=0, padx=10, pady=10)

    root.mainloop()

```



## 参考資料

[simpledialogのソースコード](https://github.com/python/cpython/blob/041bfafeb00b0e5e565986b01c00bf9cddce3b4c/Lib/tkinter/simpledialog.py)


[How to set background color of tk.simpledialog?](https://stackoverflow.com/q/61566043)
[質問者：Tasmotanizer](https://stackoverflow.com/users/10937995/tasmotanizer)
[参考にした回答](https://stackoverflow.com/a/61579653)
[回答者：TheFluffDragon9](https://stackoverflow.com/users/13319888/thefluffdragon9)


[Python Tkinter のダイアログボックスをカスタマイズしてみた](https://www.sunbit.co.jp/blog/2020/08/21843/)