ChatGPT-4 と始める爆速 Python GUI 開発


# 1. はじめに

　今年で10年目、そろそろ中堅か...と感じているメーカーのソフトウェアエンジニアです！

今朝 ChatGPT Plus に入って GPT-4.0 を使って遊び始めたのですが、

「自分の仕事、なくなるかも...」

と朝から頭を抱えることになったので、皆さんにその過程を共有したいと思います！

## 1.1. ちょっと前提記事紹介

　以下の記事で、Python の Tkinter をおしゃれにしたライブラリ [CustomTkinter](https://github.com/TomSchimansky/CustomTkinter) を紹介しました。

https://qiita.com/key353/items/e59927dcf20916f9d4ac

![demo.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3130775/a6208c0c-3b0f-4fca-44c0-9fc6b9f49f65.gif)


この記事では、CustomTkinterの紹介と，PythonでGUIをどうやって作るのかを自分なりに詳しく紹介したものなのですが、これと同じ GUI を作成するのに、ChatGPT-4 を使うとどこまで簡単になるか、試してみるかというのがこの記事の趣旨です。

長くなったので、この記事では GUI の完成まで行っていないですが、今回の範囲だけでも、自分としては驚愕の連続だったのでぜひ見て行ってください！

# 2. ChatGPT と始める GUI 開発

この記事では、上下にある2つのフレームのうち、上のフレームを作成するためのコードを作成します。

![sample_design2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3130775/1eaaba12-9c46-aa7c-5a2a-e3ac58ecb2f0.png)

## 2.1. はじめの指示

:::note info
本日（2023/3/18）時点では、ChatGPT-4 で画像の入力を受け付けていないため、文章で指示をします。
:::


そもそもChatGTP が指示した通りの GUI を作ってくれるのかと（この時点では）疑っていたので、まず以下のように指示をしました。

```md
以下の箇条書きで記載する機能を機能を持つ GUI を 、Python3.8 のTkinterで作成するコードを生成すること
1. GUIの大きさは横500px, 縦100px
2. 上部にフレームを配置し、その中に以下のウィジェットを横に配置する
   1. ファイルパスを入力できるテキストボックス
   2. その右横に、「開く」と表示されたボタン
   3. その右横に、「読み込み」と表示されたボタン
```

回答がこちらです。

![gpt01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3130775/b0693ec8-291c-7f46-5aeb-76f05ca4a881.png)

```python
import tkinter as tk
from tkinter import ttk
from tkinter import filedialog

def open_file():
    file_path = filedialog.askopenfilename()
    file_path_entry.delete(0, tk.END)
    file_path_entry.insert(0, file_path)

def read_file():
    file_path = file_path_entry.get()
    if file_path:
        with open(file_path, 'r') as file:
            content = file.read()
            # ここでファイルの内容を処理します

root = tk.Tk()
root.geometry('500x100')

frame_top = ttk.Frame(root)
frame_top.pack(side=tk.TOP, fill=tk.X)

file_path_entry = ttk.Entry(frame_top, width=50)
file_path_entry.pack(side=tk.LEFT, padx=(10, 10))

open_button = ttk.Button(frame_top, text='開く', command=open_file)
open_button.pack(side=tk.LEFT, padx=(0, 10))

read_button = ttk.Button(frame_top, text='読み込み', command=read_file)
read_button.pack(side=tk.LEFT, padx=(0, 10))

root.mainloop()
```

出力されたコードを実行すると以下になりました。

![gpt02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3130775/c2f4517f-ee9d-283a-0999-44ff465fa143.png)

あってる...

## 2.2. ブラッシュアップ

このコードが一瞬で出てきた時点で、まじかと驚いていたのですが、ここはプロのエンジニアとして、

* コメントがないこと
* main関数を使っていないこと(べた書きになっていること)

が気になるので、以下のような前提条件を追加しました。

```diff_markdown
以下の箇条書きで記載する機能を機能を持つ GUI を 、Python3.8 のTkinterで作成するコードを生成すること

+ # 前提条件
+ * コードにはコメントを記載すること
+ * main 関数を使用すること

# 機能条件
1. GUIの大きさは横500px, 縦100px
2. 上部にフレームを配置し、その中に以下のウィジェットを横に配置する
   1. ファイルパスを入力できるテキストボックス
   2. その右横に、「開く」と表示されたボタン
   3. その右横に、「読み込み」と表示されたボタン
```

回答がこちらです。

```python
import tkinter as tk
from tkinter import ttk
from tkinter import filedialog

def open_file():
    # ファイルダイアログを開いてファイルパスを取得
    file_path = filedialog.askopenfilename()
    file_path_entry.delete(0, tk.END)
    file_path_entry.insert(0, file_path)

def read_file():
    # テキストボックスからファイルパスを取得してファイルを読み込む
    file_path = file_path_entry.get()
    if file_path:
        with open(file_path, 'r') as file:
            content = file.read()
            # ここでファイルの内容を処理します

def main():
    root = tk.Tk()
    root.geometry('500x100')

    # 上部にフレームを配置
    frame_top = ttk.Frame(root)
    frame_top.pack(side=tk.TOP, fill=tk.X)

    # ファイルパスを入力できるテキストボックスを配置
    file_path_entry = ttk.Entry(frame_top, width=50)
    file_path_entry.pack(side=tk.LEFT, padx=(10, 10))

    # 「開く」ボタンを配置
    open_button = ttk.Button(frame_top, text='開く', command=open_file)
    open_button.pack(side=tk.LEFT, padx=(0, 10))

    # 「読み込み」ボタンを配置
    read_button = ttk.Button(frame_top, text='読み込み', command=read_file)
    read_button.pack(side=tk.LEFT, padx=(0, 10))

    root.mainloop()

if __name__ == '__main__':
    main()
```

はい。ありがとうございます。
（こんな適当な指示で、このレベルのコードをさっと出す新人が来たら大喜びするレベルですね...）


## 2.3. 機能を追加する

ウィジェットの配置は問題なさそうなので、機能についても指示していきたいと思います。

```diff_markdown
以下の箇条書きで記載する機能を機能を持つ GUI を 、Python3.8 のTkinterで作成するコードを生成すること

# 前提条件
* コードにはコメントを記載すること
* main 関数を使用すること

# 機能条件
1. GUIの大きさは横500px, 縦100px
2. 上部にフレームを配置し、その中に以下のウィジェットを横に配置する
   1. ファイルパスを入力できるテキストボックス
   2. その右横に、「開く」と表示されたボタン
   3. その右横に、「読み込み」と表示されたボタン
+ 3. 2.で配置したウィジェットは以下の機能を持つ
+   1. 2.1.のテキストボックスは，GUI の大きさを変更したときに、左右にテキストボックスの幅が連動するように設定する
+   2. 2.2.のボタンは、ファイル選択ダイアログを表示し、そこで選択されたファイルパスを2.1.のテキストボックスに表示させる
+   3. 2.3.のボタンは、2.1のテキストボックスに設定されたファイルパスのファイルを開き、その中身のテキストをprint表示する
```

:::note info
本当は「開く」ボタンを押したときに、GUIの下半部のフレームでプロット画面を表示したいのですが、この段階では仮の機能として実装していきます。
:::

```python
import tkinter as tk
from tkinter import ttk
from tkinter import filedialog

def open_file():
    # ファイルダイアログを開いてファイルパスを取得し、テキストボックスに表示する
    file_path = filedialog.askopenfilename()
    file_path_entry.delete(0, tk.END)
    file_path_entry.insert(0, file_path)

def read_file():
    # テキストボックスからファイルパスを取得し、ファイルの中身をprint表示する
    file_path = file_path_entry.get()
    if file_path:
        with open(file_path, 'r') as file:
            content = file.read()
            print(content)

def main():
    root = tk.Tk()
    root.geometry('500x100')

    # 上部にフレームを配置
    frame_top = ttk.Frame(root)
    frame_top.pack(side=tk.TOP, fill=tk.X)

    # ファイルパスを入力できるテキストボックスを配置
    file_path_entry = ttk.Entry(frame_top)
    file_path_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(10, 10))

    # 「開く」ボタンを配置
    open_button = ttk.Button(frame_top, text='開く', command=open_file)
    open_button.pack(side=tk.LEFT, padx=(0, 10))

    # 「読み込み」ボタンを配置
    read_button = ttk.Button(frame_top, text='読み込み', command=read_file)
    read_button.pack(side=tk.LEFT, padx=(0, 10))

    root.mainloop()

if __name__ == '__main__':
    main()
```

こちらのコードを実行すると、問題なくGUIは表示されるのですが、開くボタンを実行して，ファイルを選択すると以下のエラーが出ます。

```sh
Exception in Tkinter callback
Traceback (most recent call last):
  File "C:\Python38\lib\tkinter\__init__.py", line 1892, in __call__
    return self.func(*args)
  File "sample03.py", line 8, in open_file
    file_path_entry.delete(0, tk.END)
NameError: name 'file_path_entry' is not defined
```

聞いてみました。

![gpt03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3130775/9c41f92d-9de2-9789-a582-00a65e4cd0ab.png)


```python
import tkinter as tk
from tkinter import ttk
from tkinter import filedialog

def open_file(file_path_entry):
    # ファイルダイアログを開いてファイルパスを取得し、テキストボックスに表示する
    file_path = filedialog.askopenfilename()
    file_path_entry.delete(0, tk.END)
    file_path_entry.insert(0, file_path)

def read_file(file_path_entry):
    # テキストボックスからファイルパスを取得し、ファイルの中身をprint表示する
    file_path = file_path_entry.get()
    if file_path:
        with open(file_path, 'r') as file:
            content = file.read()
            print(content)

def main():
    root = tk.Tk()
    root.geometry('500x100')

    # 上部にフレームを配置
    frame_top = ttk.Frame(root)
    frame_top.pack(side=tk.TOP, fill=tk.X)

    # ファイルパスを入力できるテキストボックスを配置
    file_path_entry = ttk.Entry(frame_top)
    file_path_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(10, 10))

    # 「開く」ボタンを配置
    open_button = ttk.Button(frame_top, text='開く', command=lambda: open_file(file_path_entry))
    open_button.pack(side=tk.LEFT, padx=(0, 10))

    # 「読み込み」ボタンを配置
    read_button = ttk.Button(frame_top, text='読み込み', command=lambda: read_file(file_path_entry))
    read_button.pack(side=tk.LEFT, padx=(0, 10))

    root.mainloop()

if __name__ == '__main__':
    main()
```

秒で直りました。

## 2.4. エラーを発生させないための対策を聞いてみる

　さきほど発生したエラーは ChatGPT に聞けばすぐに直るものの、そもそもこのようなエラーを発生させないようにするにはどうすればよいかと考え...本人に聞いてみます。

![gpt05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3130775/724e81be-4ac2-0262-aed2-a17f7cbcffd9.png)


指示された条件を追加しました。さらに同じようにエンコードのエラーが発生したので文字コードを指定します。

```diff_markdown
# 前提条件
* コードにはコメントを記載すること
* main 関数を使用すること
+ * 関数の引数を介して必要な変数を渡すことで、グローバル変数の使用を避けること。
+ * 文字コードを指定できる場所では、utf-8 を使用すること。
```

これでエラーが発生することなく望みの機能で動くようになりました。

## 2.5. デザインも指定する

　生成された GUI に余白がなさすぎることが気になるので、（こんなに適当な指示でよいのかと思いながら）以下のデザイン条件も追加します。

```md
# デザインに関する条件
* 各ウィジェットやフレームの配置の際には、適度な余白を持たせること。特にフレームについてはより大きい余白を持たせること
```

```python
import tkinter as tk
from tkinter import filedialog

def open_file(file_path_entry):
    # ファイルダイアログを開いてファイルパスを取得し、テキストボックスに表示する
    file_path = filedialog.askopenfilename()
    file_path_entry.delete(0, tk.END)
    file_path_entry.insert(0, file_path)

def read_file(file_path_entry):
    # テキストボックスからファイルパスを取得し、ファイルの中身をprint表示する
    file_path = file_path_entry.get()
    if file_path:
        with open(file_path, 'r', encoding='utf-8') as file:
            content = file.read()
            print(content)

def main():
    root = tk.Tk()
    root.geometry('500x100')

    # 上部にフレームを配置
    frame_top = tk.Frame(root)
    frame_top.pack(side=tk.TOP, fill=tk.X, padx=20, pady=20)

    # ファイルパスを入力できるテキストボックスを配置
    file_path_entry = tk.Entry(frame_top)
    file_path_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 10))

    # 「開く」ボタンを配置
    open_button = tk.Button(frame_top, text='開く', command=lambda: open_file(file_path_entry))
    open_button.pack(side=tk.LEFT, padx=(0, 10))

    # 「読み込み」ボタンを配置
    read_button = tk.Button(frame_top, text='読み込み', command=lambda: read_file(file_path_entry))
    read_button.pack(side=tk.LEFT, padx=(0, 10))

    root.mainloop()

if __name__ == '__main__':
    main()
```

GUI の見た目はこちらでOK。

![gpt06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3130775/d6091652-07e5-7fcf-61a3-4cdac70628d5.png)

また機能についても指示通り動いていることが確認できました。

![gpt06.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3130775/f123486f-a45b-7623-33c6-be2c6bc12dbb.gif)


これで一つの目のフレームが完成しました。


# 3. CustomTkinter に変換する

　（さすがにこれはできないだろうと信じたかったのですが）Tkinter のデザイン改良版である [CustomTkinter](https://github.com/TomSchimansky/CustomTkinter) に変換できるか聞いてみました。

![gpt08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3130775/5f88441d-5fda-9185-e3cb-473daf47cb3f.png)

```python
import customtkinter as ctk
import tkinter as tk
from tkinter import filedialog

def open_file(file_path_entry):
    # ファイルダイアログを開いてファイルパスを取得し、テキストボックスに表示する
    file_path = filedialog.askopenfilename()
    file_path_entry.delete(0, tk.END)
    file_path_entry.insert(0, file_path)

def read_file(file_path_entry):
    # テキストボックスからファイルパスを取得し、ファイルの中身をprint表示する
    file_path = file_path_entry.get()
    if file_path:
        with open(file_path, 'r', encoding='utf-8') as file:
            content = file.read()
            print(content)

def main():
    root = tk.Tk()
    root.geometry('500x100')

    # 上部にフレームを配置
    frame_top = tk.Frame(root)
    frame_top.pack(side=tk.TOP, fill=tk.X, padx=20, pady=20)

    # ファイルパスを入力できるテキストボックスを配置
    file_path_entry = ctk.CTkEntry(frame_top)
    file_path_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 10))

    # 「開く」ボタンを配置
    open_button = ctk.CTkButton(frame_top, text='開く', command=lambda: open_file(file_path_entry))
    open_button.pack(side=tk.LEFT, padx=(0, 10))

    # 「読み込み」ボタンを配置
    read_button = ctk.CTkButton(frame_top, text='読み込み', command=lambda: read_file(file_path_entry))
    read_button.pack(side=tk.LEFT, padx=(0, 10))

    root.mainloop()

if __name__ == '__main__':
    main()
```

あっさりできるんですね...

![gpt07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3130775/18cf0d8a-3b4b-b9a4-2b3d-e1b325b9f4e8.png)

これで見た目も動作もまったく問題ないのですが、フレーム(`tk.Frame`)など一部のクラスが Tkinter のままなので修正します。

![gpt10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3130775/1bd1cb96-5826-b9bb-8c1e-3907b4d63e62.png)

```python
import customtkinter as ctk
from tkinter import filedialog

def open_file(file_path_entry):
    # ファイルダイアログを開いてファイルパスを取得し、テキストボックスに表示する
    file_path = filedialog.askopenfilename()
    file_path_entry.delete(0, ctk.END)
    file_path_entry.insert(0, file_path)

def read_file(file_path_entry):
    # テキストボックスからファイルパスを取得し、ファイルの中身をprint表示する
    file_path = file_path_entry.get()
    if file_path:
        with open(file_path, 'r', encoding='utf-8') as file:
            content = file.read()
            print(content)

def main():
    root = ctk.CTk()
    root.geometry('500x100')

    # 上部にフレームを配置
    frame_top = ctk.CTkFrame(root)
    frame_top.pack(side=ctk.TOP, fill=ctk.X, padx=20, pady=20)

    # ファイルパスを入力できるテキストボックスを配置
    file_path_entry = ctk.CTkEntry(frame_top)
    file_path_entry.pack(side=ctk.LEFT, fill=ctk.X, expand=True, padx=(0, 10))

    # 「開く」ボタンを配置
    open_button = ctk.CTkButton(frame_top, text='開く', command=lambda: open_file(file_path_entry))
    open_button.pack(side=ctk.LEFT, padx=(0, 10))

    # 「読み込み」ボタンを配置
    read_button = ctk.CTkButton(frame_top, text='読み込み', command=lambda: read_file(file_path_entry))
    read_button.pack(side=ctk.LEFT, padx=(0, 10))

    root.mainloop()

if __name__ == '__main__':
    main()
```

![gpt09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3130775/4907bd2b-ae9e-d274-e443-eb47ec33600c.png)

これで本当に完成です。

# 4. まとめ

　今日は ChatGPT-4 と一緒に Python GUI を開発してみました。
　冒頭にも貼った下記の記事ですが、正直にいうと作るのに丸3日くらいかかりました。それが，この記事ではChatGTPと遊ぶのに1時間、記事にまとめるのに2時間くらいでしょうか。ちゃちゃっと作業するだけで、こんなものができてしまうとは...

https://qiita.com/key353/items/e59927dcf20916f9d4ac

想像の数億倍くらいすごすぎて，自分の仕事がなくなるかもという不安はありますが，シンギュラリティとかドラえもんの世界はすぐそこなのかなと、すごくわくわくしています。

# 5. 関連記事

「ChatGPTと始める」シリーズ第2弾として、ChatGPTと協力してPythonでCSVデータの可視化を行いました!

https://qiita.com/key353/items/ae6131ffea178c960fdd

第3, 4弾はこちらです。

https://qiita.com/key353/items/d58b93b4c21a9cb44baf

https://qiita.com/key353/items/9b1a4855aa5bc198bb7f