Pythonで作る！半透明付箋アプリケーション

## はじめに

デスクトップ上に付箋を貼り付けてメモを取る習慣がある方も多いのではないでしょうか？今回は、Pythonを使って半透明の付箋アプリケーションを実装する方法をご紹介します。このアプリケーションを使えば、デスクトップ上に半透明の付箋を表示し、簡単にメモを取ることができます。

## 要件

このアプリケーションの主な要件は以下の通りです：

1. デスクトップ上に複数の半透明な付箋を表示できること
2. 付箋にテキストを入力し、編集できること
3. 付箋の位置を自由に移動できること
4. 付箋の色をカスタマイズできること
5. アプリケーション終了時に付箋の状態を保存し、次回起動時に復元できること
6. 付箋を整理するための自動配列機能があること
7. 使いやすく、視覚的に魅力的なインターフェースであること

## 仕様

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/0741e899-17b1-8974-b1fd-9c1c7ab46b66.png)
新規付箋で付箋を追加できる
付箋はDrag&Dropで移動できる

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/b4bd3562-2759-d0a9-8674-0c49118cf060.png)
付箋の色の変更画面

上記の要件を満たすために、以下の仕様を定めました：

1. **複数の半透明付箋**
   - 各付箋は個別のウィンドウとして実装
   - 付箋の透明度は90%に設定（透明度10%）

2. **テキスト入力と編集**
   - 各付箋にテキスト入力エリアを実装
   - フォントは Arial、サイズは 12pt に設定

3. **付箋の移動**
   - マウスでドラッグして移動可能
   - 画面端でのはみ出しは考慮しない（ユーザーの責任で配置）

4. **色のカスタマイズ**
   - 各付箋に「色変更」ボタンを実装
   - 標準のカラーピッカーを使用して色を選択

5. **状態の保存と復元**
   - 付箋の位置、色、テキスト内容をJSONファイルに保存
   - アプリケーション起動時に保存されたデータを読み込んで復元

6. **自動配列機能**
   - メインウィンドウに「自動配列」ボタンを実装
   - クリック時に全ての付箋を画面上にグリッド状に整列

7. **UI デザイン**
   - メインウィンドウはシンプルに、必要最小限の機能のみ表示
   - 付箋のボタンは標準的なtkinterボタンを使用

8. **その他**
   - 新規付箋作成時は、ランダムな位置に配置
   - 付箋の初期サイズは 200x200 ピクセルに固定

## 機能概要

- 複数の半透明付箋を作成可能
- 付箋の色を自由に変更可能
- 付箋の内容と配置を保存し、次回起動時に復元
- ボタン一つで付箋を自動配列
- 適度な透明度で視認性を確保

## 実装環境

- Python 3.x
- 必要なライブラリ：tkinter, PIL (Python Imaging Library)

## クラス図


```mermaid
classDiagram
    class Main {
        +main()
    }
    class StickyNoteApp {
        +create_note()
        +auto_arrange()
        +save_notes()
        +load_notes()
        +run()
    }
    class StickyNote {
        +change_color()
        +close()
        +setup_drag()
        +get_data()
    }
    Main --> StickyNoteApp : creates and runs
    StickyNoteApp "1" --> "*" StickyNote : contains
```

## シーケンス図

```mermaid
sequenceDiagram
    User->>StickyNoteApp: アプリケーション起動
    StickyNoteApp->>JSONFile: load_notes()
    JSONFile-->>StickyNoteApp: 保存済みのノートデータ
    StickyNoteApp->>StickyNote: create_note() (各保存ノート)
    StickyNote-->>StickyNoteApp: ノートオブジェクト
    StickyNoteApp-->>User: メインウィンドウ表示
    User->>StickyNoteApp: 「新規付箋」ボタンクリック
    StickyNoteApp->>StickyNote: create_note()
    StickyNote-->>User: 新しい付箋表示
    User->>StickyNote: テキスト入力
    StickyNote-->>User: テキスト表示更新
    User->>StickyNote: ドラッグ操作
    StickyNote->>StickyNote: on_drag()
    StickyNote-->>User: 付箋位置更新
    User->>StickyNote: 「色変更」ボタンクリック
    StickyNote->>StickyNote: change_color()
    StickyNote-->>User: 付箋色更新
    User->>StickyNoteApp: 「自動配列」ボタンクリック
    StickyNoteApp->>StickyNoteApp: auto_arrange()
    StickyNoteApp->>StickyNote: geometry更新 (各ノート)
    StickyNote-->>User: 全付箋位置更新
    User->>StickyNote: 「閉じる」ボタンクリック
    StickyNote->>StickyNote: close()
    StickyNote->>StickyNoteApp: notes.remove()
    StickyNote-->>User: 付箋削除
    User->>StickyNoteApp: アプリケーション終了
    StickyNoteApp->>StickyNoteApp: on_closing()
    StickyNoteApp->>JSONFile: save_notes()
    JSONFile-->>StickyNoteApp: 保存完了
    StickyNoteApp-->>User: アプリケーション終了
```

## コード全体

以下が完全なソースコードです：

```python
import tkinter as tk
from tkinter import colorchooser
from PIL import Image, ImageTk
import json
import random

class StickyNote(tk.Toplevel):
    def __init__(self, master, x, y, color, text=""):
        super().__init__(master)
        self.master = master
        self.overrideredirect(True)
        self.attributes("-alpha", 0.9)  # 透過度を0.9に設定（より不透明に）
        self.attributes("-topmost", True)

        self.geometry(f"200x200+{x}+{y}")
        self.color = color

        self.create_widgets()
        self.setup_drag()
        
        if text:
            self.text_widget.insert(tk.END, text)

    def create_widgets(self):
        self.canvas = tk.Canvas(self, width=200, height=200, highlightthickness=0)
        self.canvas.pack()

        self.update_background()

        self.text_widget = tk.Text(self, wrap=tk.WORD, bg=self.color, fg="black", font=("Arial", 12), bd=0)
        self.text_widget.place(x=10, y=10, width=180, height=160)

        self.color_button = tk.Button(self, text="色変更", command=self.change_color)
        self.color_button.place(x=10, y=175, width=50, height=20)

        self.close_button = tk.Button(self, text="閉じる", command=self.close)
        self.close_button.place(x=140, y=175, width=50, height=20)

    def update_background(self):
        self.background = Image.new("RGBA", (200, 200), self.color)
        self.background = ImageTk.PhotoImage(self.background)
        self.canvas.create_image(0, 0, anchor="nw", image=self.background)

    def change_color(self):
        color = colorchooser.askcolor(color=self.color)[1]
        if color:
            self.color = color
            self.update_background()
            self.text_widget.config(bg=self.color)

    def close(self):
        self.master.notes.remove(self)
        self.destroy()

    def setup_drag(self):
        self.bind("<ButtonPress-1>", self.start_drag)
        self.bind("<B1-Motion>", self.on_drag)

    def start_drag(self, event):
        self._drag_start_x = event.x
        self._drag_start_y = event.y

    def on_drag(self, event):
        x = self.winfo_x() - self._drag_start_x + event.x
        y = self.winfo_y() - self._drag_start_y + event.y
        self.geometry(f"+{x}+{y}")

    def get_data(self):
        return {
            "x": self.winfo_x(),
            "y": self.winfo_y(),
            "color": self.color,
            "text": self.text_widget.get("1.0", tk.END).strip()
        }

class StickyNoteApp(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Sticky Note App")
        self.geometry("200x100")

        self.notes = []
        self.create_widgets()
        self.load_notes()

    def create_widgets(self):
        tk.Button(self, text="新規付箋", command=self.create_note).pack(pady=10)
        tk.Button(self, text="自動配列", command=self.auto_arrange).pack(pady=10)

    def create_note(self, x=None, y=None, color=None, text=""):
        if x is None or y is None:
            x = random.randint(0, self.winfo_screenwidth() - 200)
            y = random.randint(0, self.winfo_screenheight() - 200)
        if color is None:
            color = f"#{random.randint(0, 0xFFFFFF):06x}"
        note = StickyNote(self, x, y, color, text)
        self.notes.append(note)

    def auto_arrange(self):
        screen_width = self.winfo_screenwidth()
        screen_height = self.winfo_screenheight()
        rows = int((len(self.notes) ** 0.5) // 1 + 1)
        cols = (len(self.notes) + rows - 1) // rows
        width = screen_width // cols
        height = screen_height // rows

        for i, note in enumerate(self.notes):
            row = i // cols
            col = i % cols
            x = col * width
            y = row * height
            note.geometry(f"+{x}+{y}")

    def save_notes(self):
        data = [note.get_data() for note in self.notes]
        with open("notes.json", "w") as f:
            json.dump(data, f)

    def load_notes(self):
        try:
            with open("notes.json", "r") as f:
                data = json.load(f)
            for note_data in data:
                self.create_note(
                    note_data.get("x", 100),
                    note_data.get("y", 100),
                    note_data.get("color", "#FFFF00"),
                    note_data.get("text", "")
                )
        except FileNotFoundError:
            pass
        except json.JSONDecodeError:
            print("Invalid JSON file. Starting with no notes.")
        except Exception as e:
            print(f"An error occurred while loading notes: {e}")

    def on_closing(self):
        self.save_notes()
        self.destroy()

if __name__ == "__main__":
    app = StickyNoteApp()
    app.protocol("WM_DELETE_WINDOW", app.on_closing)
    app.mainloop()
```

## 主要な実装ポイント

### 1. 半透明の付箋

付箋の透明度を設定することで、背景のデスクトップが少し透けて見えるようになっています。

```python
self.attributes("-alpha", 0.9)  # 透過度を0.9に設定（より不透明に）
```

### 2. 付箋のドラッグ機能

付箋をドラッグして移動できるようにするために、マウスイベントをバインドしています。

```python
def setup_drag(self):
    self.bind("<ButtonPress-1>", self.start_drag)
    self.bind("<B1-Motion>", self.on_drag)

def start_drag(self, event):
    self._drag_start_x = event.x
    self._drag_start_y = event.y

def on_drag(self, event):
    x = self.winfo_x() - self._drag_start_x + event.x
    y = self.winfo_y() - self._drag_start_y + event.y
    self.geometry(f"+{x}+{y}")
```

### 3. 付箋の自動配列

画面サイズに応じて付箋を自動的に配列する機能を実装しています。

```python
def auto_arrange(self):
    screen_width = self.winfo_screenwidth()
    screen_height = self.winfo_screenheight()
    rows = int((len(self.notes) ** 0.5) // 1 + 1)
    cols = (len(self.notes) + rows - 1) // rows
    width = screen_width // cols
    height = screen_height // rows

    for i, note in enumerate(self.notes):
        row = i // cols
        col = i % cols
        x = col * width
        y = row * height
        note.geometry(f"+{x}+{y}")
```

### 4. 付箋の保存と読み込み

アプリケーション終了時に付箋の情報をJSONファイルに保存し、起動時に読み込むことで、前回の状態を復元しています。

```python
def save_notes(self):
    data = [note.get_data() for note in self.notes]
    with open("notes.json", "w") as f:
        json.dump(data, f)

def load_notes(self):
    try:
        with open("notes.json", "r") as f:
            data = json.load(f)
        for note_data in data:
            self.create_note(
                note_data.get("x", 100),
                note_data.get("y", 100),
                note_data.get("color", "#FFFF00"),
                note_data.get("text", "")
            )
    except FileNotFoundError:
        pass
    except json.JSONDecodeError:
        print("Invalid JSON file. Starting with no notes.")
    except Exception as e:
        print(f"An error occurred while loading notes: {e}")
```

## 使用方法

1. 上記のコードを `sticky_notes.py` として保存します。
2. 必要なライブラリをインストールします：`pip install pillow`
3. コマンドラインで `python sticky_notes.py` を実行します。
4. メインウィンドウが表示されたら、「新規付箋」ボタンをクリックして付箋を作成します。
5. 付箋をドラッグして移動したり、テキストを入力したりできます。
6. 「色変更」ボタンをクリックすると、付箋の色を変更できます。
7. 「自動配列」ボタンをクリックすると、すべての付箋が画面上に整列されます。
8. アプリケーションを終了すると、付箋の状態が自動的に保存されます。

## まとめ

このPythonスクリプトを使用することで、シンプルながら機能的な半透明付箋アプリケーションを作成できました。tkinterとPILを組み合わせることで、カスタマイズ性の高いGUIアプリケーションが比較的簡単に実装できることがわかります。

要件と仕様に基づいて設計することで、目的に沿った効果的なアプリケーションを実装することができました。

## 発展的なアイデア

さらなる改良のアイデアとしては、以下のようなものが考えられます：

1. 付箋のサイズ変更機能の追加
2. ホットキーによる新規付箋作成
3. 付箋のグループ化やタグ付け機能
4. クラウド同期機能の実装
5. マークダウン記法のサポート
6. 付箋のアラーム/リマインダー機能

ぜひ、このコードを基に自分好みにカスタマイズして、生産性向上に役立