◤Python無料教材配布◢ JobCodeメールマガジン実施中!

【Python】TkinterによるAIチャットボット開発とサンプルコード

python-tkinter-chatbot-005

Pythonの標準ライブラリであるTkinterを用いて簡単なGUIデスクトップアプリ開発を実施します。

また、Gemini APIを利用したAIチャットボットアプリの完成を目指します。

本記事の要点
  • 自作のAIチャットボットアプリをローカルで開発したい人
  • AIチャットボットアプリのサンプルコードを確認したい人
  • 無料利用できるGemini APIでチャットボットの作り方を知りたい人

これらの悩みを解決しながら、簡単なAIチャットボットアプリ作成を解説します。

▼【無料配布】Python基礎学習教材のプレゼント実施中!▼

本記事をお読み頂いているPython初学者向けに、メルマガ登録にてPython基礎学習教材の無料配布を実施しています。

以下に無料配布するPython資料をご紹介します。

無料配布するPython各資料
  • Python入門ガイド
  • Python基礎知識ガイド
  • tkinter基礎知識ガイド
  • 【tkinter製】デスクトップアプリフォルダ
Python入門ガイドの概要

Python入門ガイドは、Python初学者向けに市場の動向や今後のプログラミングのヒントをまとめた資料になります。

以下は、Python入門ガイドの目次になります。(大枠のみ記載)

Python入門ガイドの目次(大枠)
  • Pythonとは
  • Pythonの動向
  • Pythonを学習するメリット
  • Pythonからプログラミングを始める

上記の目次から、Pythonの特徴/開発領域/ビジネス市場の動向/仕事幅の増やし方など様々な観点で図解化しています。

Python基礎知識ガイドの概要

Python基礎知識ガイドは、Pythonをこれから始めたい人へコーディング中心にまとめた学習教材になります。

以下は、Python基礎知識ガイドの目次になります。(大枠のみ記載)

Python基礎知識ガイドの目次(大枠)
  1. Pythonの実行方法
  2. Pythonプログラムの基本構造
  3. 基本データ
  4. コレクション
  5. 条件分岐
  6. ループ
  7. 関数
  8. クラス
  9. モジュールとパッケージ

上記の目次から、コーディングルール/実例コードによる解説/各機能の注意点など初学者が理解しておくべき学習ポイントを集約させました。

tkinter基礎知識ガイドの概要

tkinter基礎知識ガイドは、Pythonによるデスクトップアプリ開発をこれから始めたい人へGUIコーディング中心にまとめた学習教材になります。

以下は、tkinter基礎知識ガイドの目次になります。

tkinter基礎知識ガイドの目次
  1. tkinterの特徴
  2. 基本的な使い方
  3. 代表的なウィジェット
  4. レイアウト管理
  5. イベントとコールバック
  6. カスタマイズと拡張
  7. tkinterの構造(オブジェクト指向的設計)
  8. tkinterを扱う際の注意点
  9. tkinter製デスクトップアプリケーション例

特にGUIのデザイン性やtkinterによるアプリ開発に利用しやすいよう設計しております。

【tkinter製】デスクトップアプリフォルダ

【tkinter製】デスクトップアプリフォルダは、tkinterによるデスクトップアプリ開発をこれから始めたい人へサンプルアプリをまとめた管理フォルダになります。

以下は、【tkinter製】デスクトップアプリフォルダに格納しているサンプルアプリになります。

【tkinter製】デスクトップアプリフォルダ
  • 【tkinter製】デスクトップ用メモアプリ
  • 【tkinter製】デスクトップ用ToDoアプリ
  • 【tkinter製】デスクトップ用AI(Gemini)機能付きメモアプリ
  • Excel業務効率化ツール
  • CSVファイル結合ツール
  • Webサイト監視ツール
  • GoogleMapsデータ収集ツール
  • Geminiを利用したAIチャットボットアプリ
python-tools-to-automate-business-workflows-004
python-tools-to-automate-business-workflows-007
python-tools-to-automate-business-workflows-008
python-tkinter-chatbot-006

また、メルマガ登録の特典も今後増やしていく予定です。

特典1Pythonに限らずビジネス/その他技術関連の資料配布
特典2メルマガ登録者限定のPython資料配布

各資料データに関しては不定期の更新になりますが、メルマガ登録者へ優先的にお知らせします。

記事ではお伝えできない内容を多分に含むため、メルマガ登録者限定にさせて頂きました。

STEP
メールマガジンに登録

JobCode メールマガジン登録画面にてメールアドレスのみ入力いただき、読者登録して頂きます。

STEP
登録完了後すぐにメールが届く

添付されているドライブURLにアクセスし、各無料配布ファイルを受け取りください。

\ メールアドレスのみで10秒登録! /

目次

Python製デスクトップアプリ版AIチャットボットの作成

python-tkinter-chatbot-006

上記のAIチャットボットを完成形として目指していきます。

以下は、AIチャットボットアプリの各機能になります。

AIチャットボットアプリの各機能
  • GoogleのGemini APIと会話できるチャット機能
  • チャット履歴をサイドバーに自動でリスト化
  • チャットタイトルをAIにて自動生成
  • 不要なチャット履歴は削除可能

DB連携していないため、アプリ再起動で初期化されます。

また、AIチャットボットアプリを開発するにあたって実行環境を準備する必要があります。

AIチャットボットアプリ開発の実行環境構築

ここでは、AIチャットボットアプリ開発を実行する環境構築になります。

必要になる各種ツールは以下になります。

スクロールできます
ツール名説明
Pythonインストールしていない場合は公式サイトからダウンロード
テキストエディタVisual Studio Codeなどがおすすめ
Gemini API KeyGoogle AI Studioにて無料でAPIKey取得可能
必要なツールの準備

PythonやVSCode(Visual Studio Code)の実行環境を手順に沿って準備したい人は「【Python】ダウンロードとインストール方法から実行環境構築まで」を一読ください。

GUI操作によるAIチャットボットの作り方|tkinter

ここでは、デスクトップAIチャットアプリ(Python + Tkinter + Gemini)を、ゼロから構築する手順を解説します。

以下の流れに沿って開発を進めていきます。

AIチャットボットアプリ開発の手順
  1. プロジェクトフォルダとファイルの作成
  2. 必要なライブラリのインストール
  3. APIキーの設定
  4. 最初のウィンドウを表示する
  5. アプリのレイアウト設計|UI構築
  6. チャット画面の作成|UI構築
  7. サイドバー(チャット履歴)の作成|UI構築
  8. Gemini APIとの連携|機能実装
  9. チャット管理機能|機能実装
  10. タイトル自動生成|機能実装
  11. 全体コードを調整|仕上げ

シンプルにデスクトップ版AIチャットボットによって、ブラウザのタブ切り替えや操作コストを削減できるのでおすすめです。

プロジェクトフォルダとファイルの作成

AIチャットボットアプリを開発するにあたって、自身のPCにてプロジェクト用のフォルダを作成します。(例:tk-chatbot-app)

フォルダ内に以下の2つのファイルを作成します。

スクロールできます
ファイル名説明
main.pyアプリケーションのメイン処理を書くPythonファイル
.envAPIキーなど外部に公開したくない情報を保存するファイル

APIキーの利用方法はいくつかパターンがあるため、やりやすい方法を選択してください。

必要なライブラリのインストール

今回開発するAIチャットボットアプリでは、いくつかのライブラリのインストールが必要です。

ターミナル(コマンドプロンプト)で以下のコマンドを実行してインストールしましょう。

pip install google-generativeai python-dotenv
スクロールできます
ライブラリ名説明
google-generativeaiGemini APIと通信するライブラリ
python-dotenv.envファイルから情報を読み込むライブラリ

APIキーの設定

作成した.envファイルを開き、取得したAPIキーを以下のように記述します。

GEMINI_API_KEY="ここに取得したAPIキーを貼り付けます"

まだGemini API Keyを取得していない場合は、Google AI StudioからAPIキーを取得してください。

最初のウィンドウを表示する

main.pyにコードを記述して最初のウィンドウを表示させてみましょう。

import tkinter as tk
import os
from dotenv import load_dotenv

# .envファイルからAPIキーを読み込む準備
load_dotenv()

# アプリケーションのメインクラス
class MainApplication(tk.Tk):
    def __init__(self):
        super().__init__() # 親クラス(tk.Tk)の初期化

        # APIキーが設定されているかチェック
        api_key = os.getenv("GEMINI_API_KEY")
        if not api_key:
            print("エラー: .envファイルにGEMINI_API_KEYを設定してください。")
            self.destroy() # APIキーがなければアプリを終了
            return
        
        # ウィンドウの基本設定
        self.title("Gemini Chatbot")
        self.geometry("1000x600") # 幅1000px, 高さ600px

# アプリケーションの実行
if __name__ == "__main__":
    app = MainApplication()
    app.mainloop() # ウィンドウを表示し、イベントを待ち受けるループを開始
スクロールできます
コード説明
import tkinter as tkPythonでGUIを作る標準ライブラリTkinterをtkに置き換え使えるようにします。
class MainApplication(tk.Tk):tk.Tkを継承し、アプリの土台となるクラスを作ります。
ウィンドウそのものを表します。
super().__init__()親クラスであるtk.Tkの初期化処理を呼び出しています。
これを行わないとTkinterが正しく動作しません。
os.getenv(“GEMINI_API_KEY”).envファイルからAPIキーを読み込みます。
self.title() / self.geometry()ウィンドウのタイトルと初期サイズを設定します。
app.mainloop()この命令でウィンドウが実際に表示され、ユーザーが閉じるまで待機状態になります。

上記のコードを実行すると、何もないウィンドウが1つ表示されます。

python-tkinter-chatbot-001

アプリのレイアウト設計|UI構築

次に、アプリの画面を「サイドバー」と「メインコンテンツ」の2つに分割します。

main.pyにAppLayoutクラスを追加し、MainApplicationクラスを修正します。

# main.py (抜粋・変更箇所)

# ... (StylishButtonクラスなどは後で追加)

class AppLayout(tk.Frame):
    def __init__(self, parent):
        super().__init__(parent)
        self.parent = parent

        self.pack(fill="both", expand=True)

        # --- 左側のサイドバー ---
        self.sidebar = tk.Frame(self, bg="#F0F4F8", width=300)
        self.sidebar.pack(side="left", fill="y")
        # このフレームのサイズが子ウィジェットによって変わらないようにする
        self.sidebar.pack_propagate(False) 

        # --- 右側のメインコンテンツ ---
        self.main_content = tk.Frame(self, bg="#FFFFFF")
        self.main_content.pack(side="left", fill="both", expand=True)

class MainApplication(tk.Tk):
    def __init__(self):
        super().__init__()

        # ... (APIキーのチェック) ...
        
        self.title("Gemini Chatbot")
        self.geometry("1000x600")
        self.configure(bg="#FFFFFF")

        # AppLayoutクラスのインスタンスを作成して配置
        self.app_layout = AppLayout(self)

# ... (if __name__ == "__main__":) ...
スクロールできます
コード説明
class AppLayout(tk.Frame):tk.Frameはウィジェットをグループ化するフレームです。
これを使ってレイアウトを管理するクラスを作ります。
self.pack(fill=”both”, expand=True)AppLayoutフレームを親ウィジェット(ウィンドウ全体)いっぱいに広げます。
self.sidebar = tk.Frame(…)サイドバー用のフレームを作成します。
背景色(bg)と幅(width)を指定しています。
self.sidebar.pack(side=”left”, fill=”y”)サイドバーを左側(side=”left”)に配置し、縦方向(fill=”y”)いっぱいに広げます。
pack_propagate(False)通常、フレームサイズは中に入れたウィジェットに合わせて自動で伸縮します。
これをFalseにすることで、width=300で指定した幅を厳密に維持できます。
self.main_content = tk.Frame(…)メインコンテンツ用のフレームを作成します。
self.main_content.pack(side=”left”, fill=”both”, expand=True)メインコンテンツをサイドバーの隣(左側)に配置し、
残り全てのスペースを埋めるように縦横(fill=”both”)に引き伸ばします(expand=True)。

コード実行すると、画面が灰色と白色のエリアに分割されます。

python-tkinter-chatbot-002

チャット画面の作成|UI構築

メインコンテンツエリアにチャット表示欄とメッセージ入力欄を作ります。

ここでは、gridといった高機能な配置方法を使います。

# AppLayoutクラス内にメソッドを追加

    def create_main_chat_widgets(self):
        """メインのチャット画面ウィジェットを作成"""
        # --- main_contentをグリッドレイアウトに設定 ---
        # 2行1列のグリッドを作成し、各行・列の伸縮性を設定
        self.main_content.grid_rowconfigure(0, weight=1)  # 0行目(チャット表示)が垂直方向に伸縮
        self.main_content.grid_rowconfigure(1, weight=0)  # 1行目(入力欄)は伸縮しない
        self.main_content.grid_columnconfigure(0, weight=1) # 0列目が水平方向に伸縮

        # --- 下部のテキスト入力エリア (1行目に配置) ---
        input_frame = tk.Frame(self.main_content, bg="#FFFFFF")
        input_frame.grid(row=1, column=0, sticky="ew", padx=20, pady=10)
        input_frame.grid_columnconfigure(0, weight=1) # フレーム内のテキスト入力欄が伸縮するように

        self.message_input = tk.Text(input_frame, height=3, font=("Helvetica", 11))
        self.message_input.grid(row=0, column=0, sticky="ew")

        # ボタンは後で作成するカスタムボタンを使用
        send_button = tk.Button(input_frame, text="Send") 
        send_button.grid(row=0, column=1, sticky="e", padx=(10, 0))

        # --- 上部のチャット表示エリア (0行目に配置) ---
        chat_frame = tk.Frame(self.main_content, bg="#FFFFFF")
        chat_frame.grid(row=0, column=0, sticky="nsew", padx=20, pady=(20, 0))
        chat_frame.grid_rowconfigure(0, weight=1)
        chat_frame.grid_columnconfigure(0, weight=1)

        self.chat_display = tk.Text(chat_frame, wrap=tk.WORD, state="disabled")
        self.chat_display.grid(row=0, column=0, sticky="nsew")

        scrollbar = tk.Scrollbar(chat_frame, command=self.chat_display.yview)
        scrollbar.grid(row=0, column=1, sticky="ns")
        self.chat_display['yscrollcommand'] = scrollbar.set

# AppLayoutの__init__の最後に追記
        # --- メインコンテンツをチャット画面に変更 ---
        self.create_main_chat_widgets()
スクロールできます
コード説明
gridレイアウトpackが上下左右に詰めるのに対し、
gridは画面を格子状に分割してウィジェットを配置します。
grid_rowconfigure(0, weight=1)weight(重み)は、ウィンドウサイズが変わったときに、行(または列)が優先的にスペースをもらうかを決める値です。
weight=1の0行目(チャット表示欄)は伸縮し、weight=0の1行目(入力欄)はサイズが変わりません。
sticky=”ew”stickyはセルの中でウィジェットをどの方向に引き伸ばすかを指定します。
e(East/東), w(West/西)で、左右いっぱいに引き伸ばします。
nsewなら上下左右です。
tk.Text複数行のテキストを表示・編集できるウィジェットです。
state=”disabled”チャット表示欄をユーザーが編集できないように、読み取り専用にしています。
tk.Scrollbarスクロールバーです。
commandとyscrollcommandでtk.Textウィジェットと連動させています。
python-tkinter-chatbot-003

サイドバー(チャット履歴)の作成|UI構築

次に、サイドバーに「新規チャット」ボタン、チャット履歴を表示するスクロール可能なリストを作ります。

# AppLayoutの__init__内を修正

        # --- サイドバーのウィジェット ---
        self.new_chat_button = tk.Button(self.sidebar, text="+ 新規チャット", font=("Helvetica", 14))
        self.new_chat_button.pack(pady=10, padx=20, fill="x")

        # --- チャット履歴 (スクロール対応) ---
        history_container = tk.Frame(self.sidebar, bg="#F0F4F8")
        history_container.pack(pady=10, padx=20, fill="both", expand=True)

        self.history_canvas = tk.Canvas(history_container, bg="#F0F4F8", highlightthickness=0)
        history_scrollbar = tk.Scrollbar(history_container, orient="vertical", command=self.history_canvas.yview)
        self.scrollable_history_frame = tk.Frame(self.history_canvas, bg="#F0F4F8")

        # キャンバス内にフレームを配置し、そのIDを保存
        self.history_frame_id = self.history_canvas.create_window((0, 0), window=self.scrollable_history_frame, anchor="nw")
        
        # キャンバスのサイズが変更されたときに、内部のフレーム幅を追従させる
        self.history_canvas.bind("<Configure>", self._on_history_canvas_configure)
        self.history_canvas.configure(yscrollcommand=history_scrollbar.set)

        self.history_canvas.pack(side="left", fill="both", expand=True)
        history_scrollbar.pack(side="right", fill="y")

# AppLayoutクラスにメソッドを追加
    def _on_history_canvas_configure(self, event):
        """キャンバスの幅に合わせて、内部のスクロール可能フレームの幅を更新する。"""
        canvas_width = event.width
        self.history_canvas.itemconfig(self.history_frame_id, width=canvas_width)

スクロール可能なフレームの仕組みに関して、Tkinterには直接スクロールできるフレームがありません。

そのため、Canvasウィジェットの上にFrameを乗せるという方法で実現します。

Canvas(キャンバス)とScrollbar(スクロールバー)を用意します。

scrollable_history_frame(実際に履歴ボタンを置くフレーム)を作成します。

canvas.create_window(…)で、キャンバスの上にフレームを「描画」します。

_on_history_canvas_configureメソッドが重要で、サイドバーの幅が変わったときに、キャンバスの幅に合わせて内部フレーム幅も強制的に更新します。

これをしないと、内部のフレームの幅が足りずにレイアウトが崩れます。

python-tkinter-chatbot-004

Gemini APIとの連携|機能実装

いよいよAIと会話する機能を実装します。

GUIが固まらないようにthreading(スレッディング)を使います。

# main.pyの上部にライブラリをインポート
import google.generativeai as genai
import threading
import uuid

# AppLayoutクラスの__init__内
        # --- チャットデータ管理 ---
        self.chat_sessions = {}  # {chat_id: {"title": str, "history": [...]}}
        self.current_chat_id = None
        # Geminiモデルを初期化
        self.ai_model = genai.GenerativeModel('gemini-1.5-flash-latest')

# AppLayoutクラスにメソッドを追加
    def send_message(self):
        """メッセージを送信し、AIの応答を要求する"""
        message = self.message_input.get("1.0", tk.END).strip()
        if message:
            # 画面に自分のメッセージを追加
            self.add_message("user", message, save_history=True)
            self.message_input.delete("1.0", tk.END)

            # AIの応答を別スレッドで取得(GUIが固まるのを防ぐ)
            threading.Thread(target=self.get_ai_response, args=(message,)).start()

        return "break"  # Enterキーでの改行を防ぐ

    def get_ai_response(self, user_message):
        """Gemini APIにリクエストを送信し、応答を取得する"""
        try:
            history = self.chat_sessions[self.current_chat_id]["history"][:-1]
            chat = self.ai_model.start_chat(history=history)
            response = chat.send_message(user_message)

            # GUIの更新はメインスレッドで行う
            self.after(0, self.add_message, "model", response.text, True)
        except Exception as e:
            error_message = f"AI応答の取得中にエラーが発生しました: {e}"
            print(error_message)
            self.after(0, self.add_message, "model", error_message, False)

    def add_message(self, role, message, save_history=True):
        """チャット表示エリアにメッセージを追加し、履歴に保存する"""
        self.chat_display.config(state="normal") # 編集可能にする
        tag = "user" if role == "user" else "ai"
        self.chat_display.insert(tk.END, f"{message}\n\n", (tag, "spacing"))
        self.chat_display.config(state="disabled") # 読み取り専用に戻す
        self.chat_display.see(tk.END)  # 最新のメッセージが見えるようにスクロール

        if save_history and self.current_chat_id:
            session = self.chat_sessions[self.current_chat_id]
            session["history"].append({"role": role, "parts": [message]})
            # ... (タイトル生成の処理は後で追加)
スクロールできます
コード説明
threadingAPIからの応答には数秒かかることがあります。
その間、GUIの処理を止めるとアプリが「応答なし」になります。
threading.Threadを使うと、APIとの通信をバックグラウンドで実施しGUIは快適に操作し続けられます。
get_ai_responseGemini APIに実際にリクエストを送るメソッドです。
過去の会話履歴(history)を一緒に送ることで文脈を理解した会話ができます。
self.after(0, …)バックグラウンドスレッドから直接GUIのウィジェット(tk.Textなど)を操作するのは危険です。
self.afterを使うことで「この処理をメインスレッドで安全に実行して」と依頼できます。
add_messageメッセージを画面に追加し、データとして保存するメソッドです。

チャット管理機能|機能実装

サイドバーのリストを機能させ、「チャットの新規作成」「切り替え」「削除」ができるようにします。

# AppLayoutクラスにメソッドを追加/修正

    def create_new_chat(self):
        """新しいチャットセッションを開始する"""
        new_chat_id = str(uuid.uuid4()) # 一意のIDを生成
        self.chat_sessions[new_chat_id] = {"title": "新規チャット", "history": []}
        self.switch_chat(new_chat_id)
        initial_ai_message = "こんにちは!どのようなご用件でしょうか?"
        self.add_message("model", initial_ai_message, save_history=True)

    def switch_chat(self, chat_id):
        """指定されたIDのチャットに切り替える"""
        self.current_chat_id = chat_id
        self.clear_chat_display() # 表示をクリア

        session = self.chat_sessions.get(chat_id)
        if not session:
            self.create_new_chat()
            return

        # 履歴を再表示
        for message in session.get("history", []):
            self.add_message(message["role"], message["parts"][0], save_history=False)

        self.update_sidebar_history() # サイドバーの見た目を更新

    def update_sidebar_history(self):
        """サイドバーのチャット履歴リストを再描画する"""
        for widget in self.scrollable_history_frame.winfo_children():
            widget.destroy() # 古いリストを全削除

        for chat_id in reversed(list(self.chat_sessions.keys())):
            # ... (各履歴項目をgridで作成する処理) ...
            # ここでタイトルボタンと削除ボタンを作成し、gridで配置します。
            # commandには、それぞれ switch_chat と delete_chat をラムダ式で渡します。
            # 例: command=lambda id=chat_id: self.switch_chat(id)

    def delete_chat(self, chat_id_to_delete):
        """指定されたIDのチャットを削除する"""
        if chat_id_to_delete in self.chat_sessions:
            del self.chat_sessions[chat_id_to_delete]

        if self.current_chat_id == chat_id_to_delete:
            if self.chat_sessions:
                # 他のチャットが残っていれば最新のものに切り替え
                latest_chat_id = list(self.chat_sessions.keys())[-1]
                self.switch_chat(latest_chat_id)
            else:
                # これが最後のチャットなら新規作成
                self.create_new_chat()
        else:
            self.update_sidebar_history()
スクロールできます
コード説明
chat_sessions辞書すべての会話データを保持します。
{チャットID: {“title”: …, “history”: […]}}という構造になっています。
create_new_chat新しい空のチャットをchat_sessionsに追加します。
switch_chat表示するチャットを切り替えます。
チャット表示欄をクリアし、指定されたIDの履歴を再表示します。
update_sidebar_historychat_sessionsのデータをもとにサイドバーのリストを1から作り直します。
delete_chatchat_sessionsからデータを削除し、表示を更新します。

タイトル自動生成|機能実装

最初の質問を元に、AIにチャットのタイトルを考えさせます。

これもthreadingを使ってバックグラウンドで実行します。

# AppLayoutクラスにメソッドを追加
    def generate_chat_title(self, chat_id):
        """会話履歴からタイトルを自動生成する"""
        try:
            history = self.chat_sessions[chat_id]["history"]
            user_message = next((msg["parts"][0] for msg in history if msg["role"] == "user"), None)
            if not user_message: return

            prompt = f"以下の質問に対する非常に短いタイトルを、日本語で7単語以内で生成してください。タイトルのみを返答してください。\n\n質問:{user_message}"
            response = self.ai_model.generate_content(prompt)
            new_title = response.text.strip().replace("\n", " ").replace("「", "").replace("」", "").replace("*", "")

            if new_title and chat_id in self.chat_sessions:
                self.chat_sessions[chat_id]["title"] = new_title
                self.after(0, self.update_sidebar_history) # メインスレッドでサイドバーを更新
        except Exception as e:
            print(f"タイトル生成中にエラーが発生しました: {e}")

# add_messageメソッド内を修正
        if save_history and self.current_chat_id:
            # ... (history.appendの処理) ...
            # 最初のユーザーメッセージが送信されたら、タイトル生成を開始
            if role == "user" and len([msg for msg in history if msg["role"] == "user"]) == 1:
                threading.Thread(target=self.generate_chat_title, args=(self.current_chat_id,)).start()
スクロールできます
コード説明
generate_chat_titleタイトル生成専用のメソッドです。
プロンプト「…という短いタイトルを生成してください」のように、
AIにやってほしいことを具体的に指示するテキストが「プロンプト」です。
add_messageからの呼び出し最初のユーザーメッセージが送信されたタイミングで、このメソッドをバックグラウンドで呼び出します。

全体コードを調整|仕上げ

最後に、ボタンデザインを整えるStylishButtonクラスやWindowsの高DPI対応コードなどを追加すれば、完成させたアプリケーションの全コードになります。

部分的なUI実装や機能実装を解説してきたので、全コードをコピペして利用してください。

import tkinter as tk
from tkinter import ttk
import os
import ctypes
import google.generativeai as genai
import threading
import uuid
from dotenv import load_dotenv

# .envファイルから環境変数を読み込む
# .envファイルに GEMINI_API_KEY="YOUR_API_KEY" を設定してください
load_dotenv()
# 高DPI対応 (Windowsのみ)
if os.name == 'nt':
    try:
        # Windows 8.1以降
        ctypes.windll.shcore.SetProcessDpiAwareness(1)
    except (AttributeError, OSError):
        try:
            # Windows Vista以降
            ctypes.windll.user32.SetProcessDPIAware()
        except (AttributeError, OSError):
            pass

class StylishButton(tk.Button):
    def __init__(self, master=None, **kwargs):
        super().__init__(master, **kwargs)
        self.config(
            bg="#4A90E2",
            fg="white",
            activebackground="#5DA0E2",
            activeforeground="white",
            relief=tk.FLAT,
            padx=10,
            pady=5,
            font=("Arial", 10)
        )
        self.bind("<Enter>", self.on_enter)
        self.bind("<Leave>", self.on_leave)

    def on_enter(self, e):
        self['bg'] = self['activebackground']

    def on_leave(self, e):
        self['bg'] = "#4A90E2"

class AppLayout(tk.Frame):
    def __init__(self, parent):
        super().__init__(parent)
        self.parent = parent

        # --- チャットデータ管理 ---
        self.chat_sessions = {}  # {chat_id: {"title": str, "history": [...]}}
        self.current_chat_id = None
        # Geminiモデルを初期化
        self.ai_model = genai.GenerativeModel('gemini-1.5-flash-latest')

        self.pack(fill="both", expand=True)

        self.sidebar = tk.Frame(self, bg="#F0F4F8", width=300)
        self.sidebar.pack(side="left", fill="y")
        self.sidebar.pack_propagate(False)

        self.main_content = tk.Frame(self, bg="#FFFFFF")
        self.main_content.pack(side="left", fill="both", expand=True)

        # --- サイドバーのウィジェット (変更) ---
        self.new_chat_button = StylishButton(self.sidebar, text="+ 新規チャット", font=("Helvetica", 14), command=self.create_new_chat)
        self.new_chat_button.pack(pady=10, padx=20, fill="x")

        # --- チャット履歴 (スクロール対応) ---
        history_container = tk.Frame(self.sidebar, bg="#F0F4F8")
        history_container.pack(pady=10, padx=20, fill="both", expand=True)

        self.history_canvas = tk.Canvas(history_container, bg="#F0F4F8", highlightthickness=0)
        history_scrollbar = tk.Scrollbar(history_container, orient="vertical", command=self.history_canvas.yview)
        self.scrollable_history_frame = tk.Frame(self.history_canvas, bg="#F0F4F8")

        self.scrollable_history_frame.bind(
            "<Configure>",
            lambda e: self.history_canvas.configure(
                scrollregion=self.history_canvas.bbox("all")
            )
        )

        # キャンバス内にフレームを配置し、そのIDを保存
        self.history_frame_id = self.history_canvas.create_window((0, 0), window=self.scrollable_history_frame, anchor="nw")
        # キャンバスのサイズが変更されたときに、内部のフレーム幅を追従させる
        self.history_canvas.bind("<Configure>", self._on_history_canvas_configure)
        self.history_canvas.configure(yscrollcommand=history_scrollbar.set)

        self.history_canvas.pack(side="left", fill="both", expand=True)
        history_scrollbar.pack(side="right", fill="y")

        # マウスホイールイベントをキャンバスとフレームにバインド
        self.history_canvas.bind("<MouseWheel>", self._on_mousewheel)
        self.scrollable_history_frame.bind("<MouseWheel>", self._on_mousewheel)

        # --- メインコンテンツをチャット画面に変更 ---
        self.create_main_chat_widgets()

    def _on_history_canvas_configure(self, event):
        """キャンバスの幅に合わせて、内部のスクロール可能フレームの幅を更新する。"""
        canvas_width = event.width
        self.history_canvas.itemconfig(self.history_frame_id, width=canvas_width)

    def create_main_chat_widgets(self):
        """メインのチャット画面ウィジェットを作成"""
        # --- 色設定 ---
        self.COLOR_MAIN_BG = "#FFFFFF"
        self.COLOR_TEXT = "#2C3E50"
        self.COLOR_BUTTON_BG = "#4A90E2"
        self.COLOR_BUTTON_FG = "white"
        self.COLOR_USER_MSG_BG = "#E0F7FA"  # 薄い水色
        self.COLOR_AI_MSG_BG = "#FFFFFF"   # 白色

        # --- main_contentをグリッドレイアウトに設定 ---
        # 2行1列のグリッドを作成し、各行・列の伸縮性を設定
        self.main_content.grid_rowconfigure(0, weight=1)  # 0行目(チャット表示)が垂直方向に伸縮
        self.main_content.grid_rowconfigure(1, weight=0)  # 1行目(入力欄)は伸縮しない
        self.main_content.grid_columnconfigure(0, weight=1) # 0列目が水平方向に伸縮

        # 下部のテキスト入力エリア
        input_frame = tk.Frame(self.main_content, bg=self.COLOR_MAIN_BG)
        input_frame.grid(row=1, column=0, sticky="ew", padx=20, pady=10)
        input_frame.grid_columnconfigure(0, weight=1) # フレーム内のテキスト入力欄が伸縮するように

        self.message_input = tk.Text(input_frame, height=3, font=("Helvetica", 11), relief=tk.SOLID, borderwidth=1, bd=1)
        self.message_input.grid(row=0, column=0, sticky="ew")
        self.message_input.bind("<Return>", self.send_message_on_enter)

        send_button = StylishButton(input_frame, text="Send", command=self.send_message)
        send_button.grid(row=0, column=1, sticky="e", padx=(10, 0))

        # 上部のチャット表示エリア
        chat_frame = tk.Frame(self.main_content, bg=self.COLOR_MAIN_BG)
        chat_frame.grid(row=0, column=0, sticky="nsew", padx=20, pady=(20, 0))
        chat_frame.grid_rowconfigure(0, weight=1)
        chat_frame.grid_columnconfigure(0, weight=1)

        self.chat_display = tk.Text(
            chat_frame,
            wrap=tk.WORD,
            state="disabled",
            bg=self.COLOR_MAIN_BG,
            fg=self.COLOR_TEXT,
            font=("Helvetica", 12),
            borderwidth=0,
            highlightthickness=0,
            padx=10,
            pady=10,
        )
        self.chat_display.grid(row=0, column=0, sticky="nsew")

        scrollbar = tk.Scrollbar(chat_frame, command=self.chat_display.yview)
        scrollbar.grid(row=0, column=1, sticky="ns")
        self.chat_display['yscrollcommand'] = scrollbar.set

        # メッセージのスタイル設定
        self.chat_display.tag_configure("user", justify='left', background=self.COLOR_USER_MSG_BG, lmargin1=10, lmargin2=10, rmargin=10, spacing3=5, relief="raised", borderwidth=1)
        self.chat_display.tag_configure("ai", justify='left', background=self.COLOR_AI_MSG_BG, lmargin1=10, lmargin2=10, rmargin=10, spacing3=5, relief="raised", borderwidth=1)
        self.chat_display.tag_configure("spacing", spacing1=10)  # メッセージ間のスペース
        
        # 最初のチャットを作成
        self.create_new_chat()

    def create_new_chat(self):
        """新しいチャットセッションを開始する"""
        new_chat_id = str(uuid.uuid4())
        self.chat_sessions[new_chat_id] = {"title": "新規チャット", "history": []}
        self.switch_chat(new_chat_id)
        # AIからの最初のメッセージ
        initial_ai_message = "こんにちは!どのようなご用件でしょうか?"
        self.add_message("model", initial_ai_message, save_history=True)
        # self.update_sidebar_history() # switch_chatで呼ばれるので不要

    def switch_chat(self, chat_id):
        """指定されたIDのチャットに切り替える"""
        self.current_chat_id = chat_id
        self.clear_chat_display()

        session = self.chat_sessions.get(chat_id)
        if not session:
            # セッションが見つからない場合は新しいチャットを作成
            self.create_new_chat()
            return

        for message in session.get("history", []):
            # 'parts'はリストなので、最初の要素を取得
            self.add_message(message["role"], message["parts"][0], save_history=False)

        self.update_sidebar_history()

    def update_sidebar_history(self):
        """サイドバーのチャット履歴リストを再描画する"""
        # 既存のボタンをクリア
        for widget in self.scrollable_history_frame.winfo_children():
            widget.destroy()

        # セッションを逆順(新しいものが上)に表示
        for chat_id in reversed(list(self.chat_sessions.keys())):
            session = self.chat_sessions[chat_id]
            title = session.get("title", "無題のチャット")
            # タイトルが長すぎる場合に省略記号(...)を追加。表示文字数を調整。
            display_text = (title[:22] + '…') if len(title) > 22 else title

            # 各履歴項目を囲むフレーム
            item_frame = tk.Frame(self.scrollable_history_frame)
            item_frame.pack(fill="x", padx=5, pady=1)

            # --- item_frame内をgridでレイアウトし、ボタンの見切れを完全に防ぐ ---
            item_frame.grid_columnconfigure(0, weight=1)  # 0列目(タイトル)が伸縮
            item_frame.grid_columnconfigure(1, weight=0)  # 1列目(削除ボタン)は伸縮しない

            # タイトル表示・切り替えボタン (0列目に配置)
            title_btn = tk.Button(
                item_frame, text=display_text,
                anchor="w",
                relief=tk.FLAT,
                font=("Arial", 10),
                pady=8,
                padx=10,
                command=lambda id=chat_id: self.switch_chat(id)
            )
            title_btn.grid(row=0, column=0, sticky="ew")

            # 削除ボタン (1列目に配置)
            delete_btn = tk.Button(
                item_frame, text="削除", relief=tk.FLAT, font=("Arial", 9),
                command=lambda id=chat_id: self.delete_chat(id)
            )
            delete_btn.grid(row=0, column=1, sticky="e", padx=(0, 10))

            # アクティブ/非アクティブで色を変更
            if chat_id == self.current_chat_id:
                bg_color, fg_color = "#DDEAFB", "#2C3E50"
            else:
                bg_color, fg_color = "#F0F4F8", "#2C3E50"

            item_frame.config(bg=bg_color)
            title_btn.config(bg=bg_color, fg=fg_color)
            delete_btn.config(bg=bg_color, fg=fg_color, activebackground=bg_color, activeforeground=fg_color)

            # マウスホイールイベントをバインド
            for widget in [item_frame, title_btn, delete_btn]:
                widget.bind("<MouseWheel>", self._on_mousewheel)

    def clear_chat_display(self):
        """チャット表示エリアをクリアする"""
        self.chat_display.config(state="normal")
        self.chat_display.delete("1.0", tk.END)
        self.chat_display.config(state="disabled")

    def add_message(self, role, message, save_history=True):
        """チャット表示エリアにメッセージを追加し、履歴に保存する"""
        self.chat_display.config(state="normal")
        tag = "user" if role == "user" else "ai"
        self.chat_display.insert(tk.END, f"{message}\n\n", (tag, "spacing"))
        self.chat_display.config(state="disabled")
        self.chat_display.see(tk.END)  # 最新のメッセージが見えるようにスクロール

        if save_history and self.current_chat_id:
            session = self.chat_sessions[self.current_chat_id]
            session["history"].append({"role": role, "parts": [message]})

            # 最初のユーザーメッセージが送信されたら、タイトル生成を開始
            history = session["history"]
            if role == "user" and len([msg for msg in history if msg["role"] == "user"]) == 1:
                # バックグラウンドでタイトル生成
                threading.Thread(target=self.generate_chat_title, args=(self.current_chat_id,)).start()

    def send_message(self):
        """メッセージを送信し、AIの応答を要求する"""
        if not self.current_chat_id:
            return "break"

        message = self.message_input.get("1.0", tk.END).strip()
        if message:
            self.add_message("user", message, save_history=True)
            self.message_input.delete("1.0", tk.END)

            # AIの応答を別スレッドで取得
            threading.Thread(target=self.get_ai_response, args=(message,)).start()

        return "break"  # Textウィジェットのデフォルトの改行を防ぐ

    def send_message_on_enter(self, event):
        """Enterキーでメッセージを送信する"""
        return self.send_message()

    def get_ai_response(self, user_message):
        """Gemini APIにリクエストを送信し、応答を取得する"""
        try:
            # チャット履歴を渡してコンテキストを維持
            # 最後のユーザーメッセージはsend_messageに渡すため、履歴からは除外
            history = self.chat_sessions[self.current_chat_id]["history"][:-1]
            chat = self.ai_model.start_chat(history=history)
            response = chat.send_message(user_message)

            # GUIの更新はメインスレッドで行う
            self.after(0, self.add_message, "model", response.text, True)
        except Exception as e:
            # エラーメッセージをGUIに表示
            error_message = f"AI応答の取得中にエラーが発生しました: {e}"
            print(error_message)  # コンソールにも出力
            self.after(0, self.add_message, "model", error_message, False)  # エラーは履歴に保存しない

    def delete_chat(self, chat_id_to_delete):
        """指定されたIDのチャットを削除する"""
        if chat_id_to_delete in self.chat_sessions:
            del self.chat_sessions[chat_id_to_delete]

        # 削除したチャットが現在表示中のものだった場合
        if self.current_chat_id == chat_id_to_delete:
            # 他にチャットが残っていれば、最新のものに切り替える
            if self.chat_sessions:
                latest_chat_id = list(self.chat_sessions.keys())[-1]
                self.switch_chat(latest_chat_id)
            # これが最後のチャットだった場合、新しいチャットを作成
            else:
                self.create_new_chat()
        else:
            # 表示中のチャット以外を削除した場合は、サイドバーを更新するだけ
            self.update_sidebar_history()

    def generate_chat_title(self, chat_id):
        """会話履歴からタイトルを自動生成する"""
        try:
            if chat_id not in self.chat_sessions:
                return

            # 最初のユーザーの質問を要約の材料にする
            history = self.chat_sessions[chat_id]["history"]
            user_message = next((msg["parts"][0] for msg in history if msg["role"] == "user"), None)
            if not user_message:
                return

            prompt = f"以下の質問に対する非常に短いタイトルを、日本語で7単語以内で生成してください。タイトルのみを返答してください。\n\n質問:{user_message}"
            response = self.ai_model.generate_content(prompt)
            # レスポンスからタイトル部分だけを抽出・整形
            new_title = response.text.strip().replace("\n", " ").replace("「", "").replace("」", "").replace("*", "")

            if new_title and chat_id in self.chat_sessions:
                self.chat_sessions[chat_id]["title"] = new_title
                self.after(0, self.update_sidebar_history) # GUIの更新はメインスレッドで
        except Exception as e:
            print(f"タイトル生成中にエラーが発生しました: {e}")

    def _on_mousewheel(self, event):
        """サイドバーの履歴をマウスホイールでスクロールする"""
        # event.deltaはWindowsでは120の倍数, macOSでは1の倍数
        self.history_canvas.yview_scroll(int(-1 * (event.delta / 120)), "units")


class MainApplication(tk.Tk):
    def __init__(self):
        super().__init__()

        # --- Gemini APIのセットアップ ---
        api_key = os.getenv("GEMINI_API_KEY")
        if not api_key:
            # ここでエラーダイアログを表示する方が親切です
            print("エラー: GEMINI_API_KEYが見つかりません。.envファイルを作成し、APIキーを設定してください。")
            self.destroy()
            return
        genai.configure(api_key=api_key)

        self.title("Gemini Chatbot")
        self.geometry("1000x600")
        self.configure(bg="#FFFFFF")

        self.app_layout = AppLayout(self)


if __name__ == "__main__":
    app = MainApplication()
    app.mainloop()
python-tkinter-chatbot-005

以下に、本アプリの実施事項をまとめています。

スクロールできます
レイアウト管理packとgridを適切に使い分けることで柔軟で崩れないレイアウトを実現します。
スクロール領域CanvasとFrameを組み合わせ、<Configure>イベントで内部フレームの幅を同期させることで
コンテンツがはみ出さないスクロール可能なリストを作成しています。
非同期処理threadingとself.afterを使うことで、
API通信中もGUIが固まることなく快適なユーザー体験を提供できます。
データとUIの分離会話データは全てchat_sessions辞書で管理し、
UI(サイドバーやチャット画面)はそのデータを元に表示を更新する役割分担がコードを整理しやすくします。

このチュートリアルがあなたのPython開発の助けとなれば幸いです。

Python製AIチャットボットアプリのサンプルコード

無料で実施できるPython学習教材は開発環境構築と基礎学習が中心であり、アプリ開発となると無料教材がないことが多いです。

メルマガ登録してい頂くと、AIチャットボットアプリのサンプルコードを含めて様々なサンプルコードを無料配布しています。

さらに、学習手順である開発環境→基礎学習→ライブラリ/フレームワーク→アプリ開発に至るまでのスプレッドシートも配布しています。

本サイトでは「Python学習に特化した網羅的な無料教材」を配布しています。

無料教材における各Python資料
  • Python入門ガイド
  • Python基礎知識ガイド
  • tkinter基礎知識ガイド
  • 【tkinter製】デスクトップアプリフォルダ

Python学習に役立つ基礎知識ガイドを始め、アプリ開発時の基礎学習教材を用意しています。

各資料データに関しては不定期の更新になりますが、メルマガ登録者へ優先的にお知らせします。

記事ではお伝えできない内容を多分に含むため、メルマガ登録者限定にさせて頂きました。

ご興味がある人は以下からメルマガ登録を実施頂けますと幸いです。

\ メールアドレスのみで10秒登録! /

この記事を書いた人

sugiのアバター sugi Site operator

【経歴】玉川大学工学部卒業→新卒SIer企業入社→2年半後に独立→プログラミングスクール運営/受託案件→フリーランスエンジニア&SEOコンサル→Python特化のコンテンツサイトJob Code運営中

目次