0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

無料のGeminiでLinuxのGUIアプリを作った話

0
Last updated at Posted at 2026-01-10

以前に書いたものをリメイク

以前に「標準ライブラリでメモデータベース」をGUIで更に1枚の画像もデータベース化するために今回はGeminiと相談しながら作ってみたのでその過程をご紹介します。
尚ソースは、最終的に使えるものだけ掲載します。
途中経過のプログラムはこちらの動画で見ていただけます。
ただし、編集なしのクソくだらない動画です。
zorin osと無料で使えるGeminiで必要なソフトを作って使う。

はじめに出したプロンプト

「python12でsqliteをデータベースエンジンにした画像1枚とテキストを格納できるtkinterのデスクトップアプリを描いてください。」
と指示しました。
出来上がったプログラムは、動きませんでした。

エラーについて聞いてみた

「ImportError: cannot import name 'ImageTk' from 'PIL' (/usr/lib/python3/dist-packages/PIL/init.py). Did you mean: 'Image'? というエラーが出ます。」
すると

”そのエラーは、主に**Linux環境(UbuntuやDebianなど)**で発生するもので、Pillow本体とは別に、Tkinter用の拡張モジュールがインストールされていないことが原因です。”
答えは的確でした。
対処方法も教えてくれたので問題解決!!

でも出来上がったアプリはテキストのみでは記録できないので。

次の指示として

「テキストのみでも記録できるようにしてください。」
テキストのみでも記録できるプログラムになりましたが、メモの表示が最新のメモしか表示できないので、

「メモタイトルを入れてデータの一覧からメモを選択できるようにしてください」
と指示
次に出来上がったアプリは記録の際、新規のデータとしてしか書き込みできないので

「上書き保存できるボタンと削除ボタンをついかして」
と指示
これで満足に使えるものができました。

ということで

ソース

gmemo.py
import tkinter as tk
from tkinter import filedialog, messagebox, ttk
import sqlite3
from PIL import Image, ImageTk
import io

class ImageDbApp:
    def __init__(self, root):
        self.root = root
        self.root.title("SQLite メモ管理システム (更新・削除機能付)")
        self.root.geometry("850x650")

        self.init_db()

        self.selected_image_binary = None
        self.image_preview = None
        self.current_id = None  # 現在編集・表示中のメモID

        # --- 全体レイアウト ---
        self.main_frame = tk.Frame(root)
        self.main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)

        self.left_frame = tk.Frame(self.main_frame)
        self.left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)

        self.right_frame = tk.Frame(self.main_frame, width=250)
        self.right_frame.pack(side=tk.RIGHT, fill=tk.Y, padx=(10, 0))

        # --- 左側コンポーネント ---
        tk.Label(self.left_frame, text="タイトル (必須):").pack(anchor="w")
        self.title_entry = tk.Entry(self.left_frame, font=("", 12))
        self.title_entry.pack(fill=tk.X, pady=(0, 10))

        tk.Label(self.left_frame, text="テキスト内容:").pack(anchor="w")
        self.text_entry = tk.Text(self.left_frame, height=8)
        self.text_entry.pack(fill=tk.X, pady=(0, 10))

        # 画像操作
        img_btn_frame = tk.Frame(self.left_frame)
        img_btn_frame.pack(pady=5)
        tk.Button(img_btn_frame, text="画像を選択", command=self.select_image).pack(side=tk.LEFT, padx=5)
        tk.Button(img_btn_frame, text="画像を外す", command=self.clear_image_selection).pack(side=tk.LEFT, padx=5)

        self.canvas = tk.Label(self.left_frame, text="画像なし", bg="#f0f0f0", width=40, height=12)
        self.canvas.pack(pady=10, fill=tk.BOTH, expand=True)

        # --- 操作ボタンエリア (ここに追加) ---
        action_btn_frame = tk.Frame(self.left_frame)
        action_btn_frame.pack(pady=10)
        
        tk.Button(action_btn_frame, text="新規保存", command=self.save_new_data, bg="#ccffcc", width=10).pack(side=tk.LEFT, padx=2)
        tk.Button(action_btn_frame, text="上書き保存", command=self.update_data, bg="#cceeff", width=10).pack(side=tk.LEFT, padx=2)
        tk.Button(action_btn_frame, text="削除", command=self.delete_data, bg="#ffcccc", width=10).pack(side=tk.LEFT, padx=2)
        tk.Button(action_btn_frame, text="入力クリア", command=self.reset_form, width=10).pack(side=tk.LEFT, padx=2)

        # --- 右側コンポーネント (一覧) ---
        tk.Label(self.right_frame, text="保存済みメモ一覧:").pack(anchor="w")
        list_scroll = tk.Scrollbar(self.right_frame)
        list_scroll.pack(side=tk.RIGHT, fill=tk.Y)

        self.memo_listbox = tk.Listbox(self.right_frame, yscrollcommand=list_scroll.set, font=("", 10))
        self.memo_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        list_scroll.config(command=self.memo_listbox.yview)

        self.memo_listbox.bind('<<ListboxSelect>>', self.on_list_select)

        self.refresh_list()

    def init_db(self):
        with sqlite3.connect("memo_app.db") as conn:
            cursor = conn.cursor()
            cursor.execute('''
                CREATE TABLE IF NOT EXISTS memos (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    title TEXT NOT NULL,
                    text_content TEXT,
                    image_data BLOB
                )
            ''')
            conn.commit()

    def select_image(self):
        file_path = filedialog.askopenfilename(filetypes=[("Image files", "*.jpg *.jpeg *.png *.gif")])
        if file_path:
            with open(file_path, 'rb') as f:
                self.selected_image_binary = f.read()
            self.display_image(self.selected_image_binary)

    def clear_image_selection(self):
        self.selected_image_binary = None
        self.image_preview = None
        self.canvas.config(image="", text="画像なし")

    def display_image(self, binary_data):
        if binary_data:
            img = Image.open(io.BytesIO(binary_data))
            img.thumbnail((300, 250))
            self.image_preview = ImageTk.PhotoImage(img)
            self.canvas.config(image=self.image_preview, text="")
        else:
            self.canvas.config(image="", text="画像なし")

    def reset_form(self):
        self.current_id = None
        self.title_entry.delete(0, tk.END)
        self.text_entry.delete("1.0", tk.END)
        self.clear_image_selection()

    def save_new_data(self):
        """新規レコードとして保存"""
        title = self.title_entry.get().strip()
        text = self.text_entry.get("1.0", tk.END).strip()
        if not title:
            messagebox.showwarning("警告", "タイトルを入力してください。")
            return

        try:
            with sqlite3.connect("memo_app.db") as conn:
                cursor = conn.cursor()
                cursor.execute(
                    "INSERT INTO memos (title, text_content, image_data) VALUES (?, ?, ?)",
                    (title, text, self.selected_image_binary)
                )
                conn.commit()
            messagebox.showinfo("完了", "新規保存しました。")
            self.reset_form()
            self.refresh_list()
        except Exception as e:
            messagebox.showerror("エラー", f"保存失敗: {e}")

    def update_data(self):
        """現在選択中のレコードを更新"""
        if self.current_id is None:
            messagebox.showwarning("警告", "一覧からメモを選択するか、一度保存してから上書きしてください。")
            return

        title = self.title_entry.get().strip()
        text = self.text_entry.get("1.0", tk.END).strip()
        if not title:
            messagebox.showwarning("警告", "タイトルは空にできません。")
            return

        try:
            with sqlite3.connect("memo_app.db") as conn:
                cursor = conn.cursor()
                cursor.execute('''
                    UPDATE memos 
                    SET title = ?, text_content = ?, image_data = ? 
                    WHERE id = ?
                ''', (title, text, self.selected_image_binary, self.current_id))
                conn.commit()
            messagebox.showinfo("完了", "データを更新しました。")
            self.refresh_list()
        except Exception as e:
            messagebox.showerror("エラー", f"更新失敗: {e}")

    def delete_data(self):
        """現在選択中のレコードを削除"""
        if self.current_id is None:
            messagebox.showwarning("警告", "削除するメモを選択してください。")
            return

        if not messagebox.askyesno("確認", "このメモを削除してもよろしいですか?"):
            return

        try:
            with sqlite3.connect("memo_app.db") as conn:
                cursor = conn.cursor()
                cursor.execute("DELETE FROM memos WHERE id = ?", (self.current_id,))
                conn.commit()
            messagebox.showinfo("完了", "削除しました。")
            self.reset_form()
            self.refresh_list()
        except Exception as e:
            messagebox.showerror("エラー", f"削除失敗: {e}")

    def refresh_list(self):
        self.memo_listbox.delete(0, tk.END)
        self.memo_ids = []
        try:
            with sqlite3.connect("memo_app.db") as conn:
                cursor = conn.cursor()
                cursor.execute("SELECT id, title FROM memos ORDER BY id DESC")
                for row in cursor.fetchall():
                    self.memo_listbox.insert(tk.END, f" {row[1]}")
                    self.memo_ids.append(row[0])
        except Exception as e:
            print(f"リスト更新エラー: {e}")

    def on_list_select(self, event):
        selection = self.memo_listbox.curselection()
        if not selection:
            return
        
        index = selection[0]
        memo_id = self.memo_ids[index]

        try:
            with sqlite3.connect("memo_app.db") as conn:
                cursor = conn.cursor()
                cursor.execute("SELECT title, text_content, image_data FROM memos WHERE id = ?", (memo_id,))
                row = cursor.fetchone()

                if row:
                    title, text, image_data = row
                    self.reset_form()
                    self.current_id = memo_id  # 選択したIDを保持
                    self.title_entry.insert(0, title)
                    self.text_entry.insert(tk.END, text)
                    if image_data:
                        self.selected_image_binary = image_data
                        self.display_image(image_data)
        except Exception as e:
            messagebox.showerror("エラー", f"読み込み失敗: {e}")

if __name__ == "__main__":
    root = tk.Tk()
    app = ImageDbApp(root)
    root.mainloop()

アプリの使い方の想定

想定している使い方としては、Googleドライブで携帯でとった写真をgoogle driveで取得して情報メモとして使うという想定です。

で最後の指示はこのソフトの仕様書を作ってもらって完了です。
Geminiは吐き出すテキストがマークダウンなのできれいな状態の仕様書ができますね!

最後に

このような小さいプログラムをたくさん作りデータベースをPostgreSQLのようにネットでも使えるものを選ぶことでLinuxを端末とした情報収集や連絡に使える独自端末を作ることが可能であることが分かりました。
無料のGeminiで十分開発に使えますね!!
Windowsはいらねー

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?