Pythonで作る！シンプルで使いやすいフローチャートツール

## はじめに

こんにちは！今回は、Pythonとpygameを使って、シンプルで使いやすいフローチャート作成ツールを作ってみました。この記事では、ツールの要件、仕様、実装方法、そして使い方までを詳しく解説します。

## 📋 要件

1. 複数の図形（開始/終了、処理、判断、入出力）を配置できること
2. 図形間を線で接続できること
3. 図形内にテキストを入力できること
4. 直感的な操作で図形の配置と接続ができること
5. シンプルで分かりやすいユーザーインターフェース

## 🛠️ 仕様

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/33205b3d-b49e-4171-bcee-532157ff813c.png)


1. 画面レイアウト
   - 左側にパレット（図形選択用サイドバー）
   - 右側にキャンバス（フローチャート作成エリア）

2. 図形
   - 種類：楕円（開始/終了）、長方形（処理）、ひし形（判断）、平行四辺形（入出力）
   - 各図形に4つの接続端子（上下左右）

3. 操作方法
   - 図形の追加：パレットからドラッグ＆ドロップ
   - 図形の移動：ドラッグ
   - 線の接続：端子をクリックして別の端子をクリック
   - テキスト入力：図形をクリックして選択後、キーボード入力

4. 視覚的フィードバック
   - 線を引いている途中で仮の線を表示

## 🚀 実装

それでは、実際のコードを見ていきましょう。

```python
import pygame
import sys

# 色の定義
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
LIGHT_BLUE = (173, 216, 230)
LIGHT_GREEN = (144, 238, 144)
LIGHT_PINK = (255, 182, 193)
LIGHT_YELLOW = (255, 255, 224)
GRAY = (200, 200, 200)

# 画面サイズ
WIDTH = 1000
HEIGHT = 600
SIDEBAR_WIDTH = 200

class Shape:
    def __init__(self, x, y, width, height, color, shape_type):
        self.rect = pygame.Rect(x, y, width, height)
        self.color = color
        self.text = ""
        self.shape_type = shape_type
        self.connectors = [
            pygame.Rect(x, y + height // 2 - 5, 10, 10),
            pygame.Rect(x + width - 10, y + height // 2 - 5, 10, 10),
            pygame.Rect(x + width // 2 - 5, y, 10, 10),
            pygame.Rect(x + width // 2 - 5, y + height - 10, 10, 10)
        ]

    def draw(self, screen):
        if self.shape_type == "rectangle":
            pygame.draw.rect(screen, self.color, self.rect)
        elif self.shape_type == "oval":
            pygame.draw.ellipse(screen, self.color, self.rect)
        elif self.shape_type == "diamond":
            points = [
                (self.rect.centerx, self.rect.top),
                (self.rect.right, self.rect.centery),
                (self.rect.centerx, self.rect.bottom),
                (self.rect.left, self.rect.centery)
            ]
            pygame.draw.polygon(screen, self.color, points)
        elif self.shape_type == "parallelogram":
            offset = self.rect.width // 4
            points = [
                (self.rect.left + offset, self.rect.top),
                (self.rect.right, self.rect.top),
                (self.rect.right - offset, self.rect.bottom),
                (self.rect.left, self.rect.bottom)
            ]
            pygame.draw.polygon(screen, self.color, points)

        for connector in self.connectors:
            pygame.draw.rect(screen, BLACK, connector)

        font = pygame.font.Font(None, 24)
        text_surface = font.render(self.text, True, BLACK)
        text_rect = text_surface.get_rect(center=self.rect.center)
        screen.blit(text_surface, text_rect)

    def contains(self, pos):
        return self.rect.collidepoint(pos)

    def get_connector(self, pos):
        for i, connector in enumerate(self.connectors):
            if connector.collidepoint(pos):
                return i
        return None

    def move(self, dx, dy):
        self.rect.move_ip(dx, dy)
        for connector in self.connectors:
            connector.move_ip(dx, dy)

class FlowchartTool:
    def __init__(self):
        pygame.init()
        self.screen = pygame.display.set_mode((WIDTH, HEIGHT))
        pygame.display.set_caption("フローチャートメーカー")
        self.clock = pygame.time.Clock()
        self.shapes = []
        self.lines = []
        self.dragging_shape = None
        self.selected_shape = None
        self.line_start = None
        self.font = pygame.font.Font(None, 24)
        self.palette = [
            Shape(20, 20, 160, 80, LIGHT_BLUE, "oval"),
            Shape(20, 120, 160, 80, LIGHT_GREEN, "rectangle"),
            Shape(20, 220, 160, 80, LIGHT_PINK, "diamond"),
            Shape(20, 320, 160, 80, LIGHT_YELLOW, "parallelogram")
        ]
        self.dragging_new = False

    def run(self):
        while True:
            self.handle_events()
            self.draw()
            self.clock.tick(60)

    def handle_events(self):
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                sys.exit()
            elif event.type == pygame.MOUSEBUTTONDOWN:
                if event.button == 1:  # 左クリック
                    self.handle_left_click(event.pos)
            elif event.type == pygame.MOUSEBUTTONUP:
                if event.button == 1:
                    self.handle_left_release(event.pos)
            elif event.type == pygame.MOUSEMOTION:
                self.handle_mouse_motion(event.pos, event.rel)
            elif event.type == pygame.KEYDOWN:
                if self.selected_shape:
                    if event.key == pygame.K_BACKSPACE:
                        self.selected_shape.text = self.selected_shape.text[:-1]
                    elif event.key == pygame.K_RETURN:
                        self.selected_shape = None
                    else:
                        self.selected_shape.text += event.unicode

    def handle_left_click(self, pos):
        for shape in self.palette:
            if shape.contains(pos):
                self.dragging_new = True
                new_shape = Shape(pos[0], pos[1], 160, 80, shape.color, shape.shape_type)
                self.shapes.append(new_shape)
                self.dragging_shape = new_shape
                return

        for shape in self.shapes:
            connector = shape.get_connector(pos)
            if connector is not None:
                if self.line_start is None:
                    self.line_start = (shape, connector)
                else:
                    start_shape, start_connector = self.line_start
                    if start_shape != shape:
                        self.lines.append((self.line_start, (shape, connector)))
                    self.line_start = None
                return

            if shape.contains(pos):
                self.dragging_shape = shape
                self.selected_shape = shape
                return

        self.selected_shape = None
        self.line_start = None

    def handle_left_release(self, pos):
        if self.dragging_new:
            self.dragging_new = False
            if pos[0] < SIDEBAR_WIDTH:
                self.shapes.pop()
            self.dragging_shape = None
            return

        self.dragging_shape = None

    def handle_mouse_motion(self, pos, rel):
        if self.dragging_shape:
            self.dragging_shape.move(rel[0], rel[1])

    def draw(self):
        self.screen.fill(WHITE)
        pygame.draw.rect(self.screen, GRAY, (0, 0, SIDEBAR_WIDTH, HEIGHT))
        
        for shape in self.shapes:
            shape.draw(self.screen)
        
        for line in self.lines:
            start_shape, start_connector = line[0]
            end_shape, end_connector = line[1]
            start_pos = start_shape.connectors[start_connector].center
            end_pos = end_shape.connectors[end_connector].center
            pygame.draw.line(self.screen, BLACK, start_pos, end_pos, 2)
        
        if self.line_start:
            start_shape, start_connector = self.line_start
            start_pos = start_shape.connectors[start_connector].center
            pygame.draw.line(self.screen, BLACK, start_pos, pygame.mouse.get_pos(), 2)
        
        for shape in self.palette:
            shape.draw(self.screen)
        
        pygame.display.flip()

if __name__ == "__main__":
    tool = FlowchartTool()
    tool.run()
```

## 🎨 主要な機能の解説

1. **Shape クラス**
   - 各図形（楕円、長方形、ひし形、平行四辺形）を表現
   - 図形の描画、移動、テキスト表示を管理
   - 4つの接続端子を持つ

2. **FlowchartTool クラス**
   - メインのアプリケーションロジックを管理
   - イベント処理（マウス操作、キーボード入力）
   - 図形の配置、線の接続、画面の描画を制御

3. **図形の追加と移動**
   - パレットからドラッグ＆ドロップで新しい図形を追加
   - 既存の図形をドラッグして移動

4. **線の接続**
   - 2つの端子を順にクリックして線を引く
   - 接続中は仮の線を表示してフィードバックを提供

5. **テキスト入力**
   - 図形をクリックして選択し、キーボードで入力

```mermaid
classDiagram
    class Shape {
        +rect: Rect
        +color: Tuple
        +text: String
        +shape_type: String
        +connectors: List[Rect]
        +__init__(x: int, y: int, width: int, height: int, color: Tuple, shape_type: String)
        +draw(screen: Surface)
        +contains(pos: Tuple) bool
        +get_connector(pos: Tuple) int
        +move(dx: int, dy: int)
    }

    class FlowchartTool {
        -screen: Surface
        -clock: Clock
        -shapes: List[Shape]
        -lines: List[Tuple]
        -dragging_shape: Shape
        -selected_shape: Shape
        -line_start: Tuple
        -font: Font
        -palette: List[Shape]
        -dragging_new: bool
        +__init__()
        +run()
        -handle_events()
        -handle_left_click(pos: Tuple)
        -handle_left_release(pos: Tuple)
        -handle_mouse_motion(pos: Tuple, rel: Tuple)
        -draw()
    }

    FlowchartTool "1" -- "*" Shape : contains
```

```mermaid
sequenceDiagram
    actor User
    participant FT as FlowchartTool
    participant S as Shape

    %% 新しい図形の追加
    User->>FT: 図形をドラッグ
    activate FT
    FT->>S: 新しいShapeオブジェクトを作成
    activate S
    S-->>FT: Shapeオブジェクト
    deactivate S
    FT->>FT: shapesリストに追加
    FT-->>User: 図形を表示
    deactivate FT

    %% 図形の移動
    User->>FT: 図形をドラッグ
    activate FT
    FT->>S: move()
    activate S
    S->>S: 位置を更新
    S-->>FT: 完了
    deactivate S
    FT-->>User: 図形の新しい位置を表示
    deactivate FT

    %% 図形間の接続
    User->>FT: 最初の端子をクリック
    activate FT
    FT->>FT: line_startを設定
    User->>FT: 二つ目の端子をクリック
    FT->>FT: linesリストに新しい接続を追加
    FT-->>User: 接続線を表示
    deactivate FT

    %% テキスト入力
    User->>FT: 図形をクリック
    activate FT
    FT->>FT: selected_shapeを設定
    User->>FT: テキストを入力
    FT->>S: textプロパティを更新
    activate S
    S-->>FT: 完了
    deactivate S
    FT-->>User: 更新されたテキストを表示
    deactivate FT
```
## 🖱️ 使い方

1. 左側のパレットから必要な図形を選び、ドラッグしてキャンバスに配置します。
2. 図形をクリック＆ドラッグで移動できます。
3. 線を引くには、以下の手順で行います：
   - 最初の図形の端子（小さな黒い四角）をクリックします。
   - マウスを動かすと、仮の線が表示されます。
   - 別の図形の端子をクリックすると、線が確定します。
4. 図形をクリックして選択し、キーボードでテキストを入力できます。

## 🌈 今後の改善案

1. 線の種類（直線、曲線、矢印）を選択できる機能
2. 図形のリサイズ機能
3. フローチャートの保存と読み込み機能
4. テキストのフォントやサイズの変更機能
5. 取り消し（Undo）と再実行（Redo）機能

## 🎉 さいごに

このツールを使えば、簡単にフローチャートを作成できます。プログラムの流れやビジネスプロセスの可視化に役立つでしょう。ぜひ、このコードを基に自分だけのカスタマイズを加えて、さらに素晴らしいツールに進化させてください！

Happy Flowcharting! 🚀✨