ミニペイントアプリ
Canvasにマウスで自由に線を描けるシンプルなお絵描きアプリ。ブラシサイズ・色の変更、消しゴム、保存機能付き。Canvasイベント処理を学ぶ。
1. アプリ概要
Canvasにマウスで自由に線を描けるシンプルなお絵描きアプリ。ブラシサイズ・色の変更、消しゴム、保存機能付き。Canvasイベント処理を学ぶ。
このアプリはtoolsカテゴリの実践的なPythonアプリです。使用ライブラリは tkinter、難易度は ★★☆ 普通 です。
このアプリは「ツール」カテゴリです。日々の作業を自動化する実用ツールは Python が最も得意とする領域です。短いコードで実用性のある成果物が作れる点が魅力です。tkinter を活かして実装するこの構造は、他のアプリにも応用が効きます。
コードを読む前に実行することをおすすめします。動いている挙動を先に把握しておくと、解説で出てくる関数や処理がどこに対応するかが頭に入りやすくなります。
応用のヒントは、機能を 1 つ増やす・見た目を整える・例外を一つでも丁寧に扱う、のいずれかから始めるのがおすすめです。
2. 機能一覧
- ミニペイントアプリのメイン機能
- 直感的なGUIインターフェース
- 入力値のバリデーション
- エラーハンドリング
- 結果の見やすい表示
- クリア機能付き
3. 事前準備・環境
Python 3.10 以上 / Windows・Mac・Linux すべて対応
以下の環境で動作確認しています。
- Python 3.10 以上
- OS: Windows 10/11・macOS 12+・Ubuntu 20.04+
4. 完全なソースコード
右上の「コピー」ボタンをクリックするとコードをクリップボードにコピーできます。
import tkinter as tk
from tkinter import colorchooser, filedialog, messagebox, ttk
class App057:
"""ミニペイントアプリ"""
def __init__(self, root):
self.root = root
self.root.title("ミニペイントアプリ")
self.root.geometry("820x580")
self.root.configure(bg="#f8f9fc")
self.color = "#1f2933"
self.brush_size = 4
self.eraser = False
self.last = None # 直前のマウス座標
self._build_ui()
def _build_ui(self):
"""UI を構築する"""
title = tk.Frame(self.root, bg="#3776ab", pady=10)
title.pack(fill=tk.X)
tk.Label(title, text="ミニペイントアプリ",
font=("Noto Sans JP", 16, "bold"),
bg="#3776ab", fg="white").pack()
ctrl = ttk.LabelFrame(self.root, text="ツール", padding=8)
ctrl.pack(fill=tk.X, padx=12, pady=8)
self.color_btn = tk.Button(ctrl, text=" ", bg=self.color, width=6,
command=self.pick_color)
self.color_btn.pack(side=tk.LEFT, padx=4)
ttk.Label(ctrl, text="太さ:").pack(side=tk.LEFT, padx=(10, 4))
self.size_var = tk.IntVar(value=self.brush_size)
ttk.Spinbox(ctrl, from_=1, to=40, textvariable=self.size_var,
width=4, command=self._sync_size).pack(side=tk.LEFT)
self.eraser_btn = ttk.Button(ctrl, text="消しゴム",
command=self.toggle_eraser)
self.eraser_btn.pack(side=tk.LEFT, padx=8)
ttk.Button(ctrl, text="全消去", command=self.clear).pack(side=tk.LEFT, padx=4)
ttk.Button(ctrl, text="保存(.ps)", command=self.save).pack(side=tk.LEFT, padx=4)
ttk.Label(ctrl, text="プリセット:").pack(side=tk.LEFT, padx=(20, 4))
for c in ("#1f2933", "#e53935", "#43a047", "#1e88e5", "#fbc02d", "#ffffff"):
tk.Button(ctrl, bg=c, width=2, relief=tk.RIDGE,
command=lambda col=c: self._set_color(col)).pack(side=tk.LEFT, padx=2)
self.canvas = tk.Canvas(self.root, bg="white",
highlightthickness=1, highlightbackground="#ccc")
self.canvas.pack(fill=tk.BOTH, expand=True, padx=12, pady=10)
self.canvas.bind("<Button-1>", self._on_press)
self.canvas.bind("<B1-Motion>", self._on_drag)
self.canvas.bind("<ButtonRelease-1>", self._on_release)
self.status = tk.Label(self.root, text="ドラッグでお絵描きできます",
bg="#f8f9fc", fg="#555")
self.status.pack()
def _sync_size(self):
"""Spinbox の値を内部に反映する"""
try:
self.brush_size = max(1, int(self.size_var.get()))
except (ValueError, tk.TclError):
self.brush_size = 4
def _set_color(self, c):
"""色を設定する (消しゴム解除)"""
self.color = c
self.color_btn.config(bg=c)
self.eraser = False
self.eraser_btn.config(text="消しゴム")
def pick_color(self):
"""カラーピッカーで色を選ぶ"""
_rgb, hexv = colorchooser.askcolor(color=self.color, title="色を選択")
if hexv:
self._set_color(hexv)
def toggle_eraser(self):
"""消しゴムモードを切替える"""
self.eraser = not self.eraser
self.eraser_btn.config(text="ペン" if self.eraser else "消しゴム")
self.status.config(text="消しゴムモード" if self.eraser else "ペンモード")
def _current_color(self):
"""現在の描画色(消しゴム時は白)"""
return "white" if self.eraser else self.color
def _on_press(self, event):
"""マウス押下: 開始点を記録"""
self._sync_size()
self.last = (event.x, event.y)
# 単発クリックでも点を打つ
r = self.brush_size
self.canvas.create_oval(event.x - r, event.y - r, event.x + r, event.y + r,
fill=self._current_color(), outline="")
def _on_drag(self, event):
"""ドラッグで線を引く"""
if self.last is None:
self.last = (event.x, event.y)
return
x0, y0 = self.last
self.canvas.create_line(x0, y0, event.x, event.y,
fill=self._current_color(),
width=self.brush_size, capstyle=tk.ROUND,
smooth=True)
self.last = (event.x, event.y)
def _on_release(self, _event):
"""マウスリリース: 履歴をリセット"""
self.last = None
def clear(self):
"""キャンバスを白紙に戻す"""
if messagebox.askyesno("確認", "描画を全消去しますか?"):
self.canvas.delete("all")
def save(self):
"""PostScript として保存する (tkinter 標準機能)"""
path = filedialog.asksaveasfilename(defaultextension=".ps",
filetypes=[("PostScript", "*.ps")])
if not path:
return
try:
self.canvas.postscript(file=path, colormode="color")
messagebox.showinfo("保存", f"保存しました:\n{path}")
except Exception as e:
messagebox.showerror("エラー", f"保存失敗: {e}")
if __name__ == "__main__":
root = tk.Tk()
app = App057(root)
root.mainloop()
5. コード解説
ミニペイントアプリのコードを詳しく解説します。クラスベースの設計で各機能を整理して実装しています。
クラス設計とコンストラクタ
App057クラスにアプリの全機能をまとめています。__init__でウィンドウ設定、_build_ui()でUI構築、process()でメイン処理を担当します。責任の分離により、コードが読みやすくなります。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
UIレイアウトの構築
LabelFrameで入力エリアと結果エリアを視覚的に分けています。pack()で縦に並べ、expand=Trueで結果エリアが画面いっぱいに広がるよう設定しています。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
イベント処理
ボタンのcommand引数でクリックイベントを、bind('
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
Textウィジェットでの結果表示
tk.Textウィジェットをstate=DISABLED(読み取り専用)で作成し、更新時はNORMALに変更してinsert()で内容を書き込み、再びDISABLEDに戻します。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
例外処理とエラーハンドリング
try-exceptでValueErrorとExceptionを捕捉し、messagebox.showerror()でエラーメッセージを表示します。予期しないエラーも処理することで、アプリの堅牢性が向上します。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
6. ステップバイステップガイド
このアプリをゼロから自分で作る手順を解説します。コードをコピーするだけでなく、実際に手順を追って自分で書いてみましょう。
-
1ファイルを作成する
新しいファイルを作成して app057.py と保存します。
-
2クラスの骨格を作る
App057クラスを定義し、__init__とmainloop()の最小構成を作ります。
-
3タイトルバーを作る
Frameを使ってカラーバー付きのタイトルエリアを作ります。
-
4入力フォームを実装する
LabelFrameとEntryウィジェットで入力エリアを作ります。
-
5処理ロジックを実装する
_execute()メソッドにメインロジックを実装します。
-
6結果表示を実装する
TextウィジェットかLabelに結果を表示する_show_result()を実装します。
-
7エラー処理を追加する
try-exceptとmessageboxでエラーハンドリングを追加します。
7. カスタマイズアイデア
基本機能を習得したら、以下のカスタマイズに挑戦してみましょう。
💡 ダークモードを追加する
bg色・fg色を辞書で管理し、ボタン1つでダークモード・ライトモードを切り替えられるようにしましょう。
💡 データの保存機能
処理結果をCSV・TXTファイルに保存する機能を追加しましょう。filedialog.asksaveasfilename()でファイル保存ダイアログが使えます。
💡 設定ダイアログ
フォントサイズや色などの設定をユーザーが変更できるオプションダイアログを追加しましょう。
8. よくある問題と解決法
❌ 日本語フォントが表示されない
原因:システムに日本語フォントが見つからない場合があります。
解決法:font引数を省略するかシステムに合ったフォントを指定してください。
❌ ライブラリのインポートエラー
原因:必要なライブラリがインストールされていません。
解決法:pip install コマンドで必要なライブラリをインストールしてください。
❌ ウィンドウサイズが合わない
原因:画面解像度や表示スケールによって異なる場合があります。
解決法:root.geometry()で適切なサイズに調整してください。
9. 練習問題
アプリの理解を深めるための練習問題です。
-
課題1:機能拡張
ミニペイントアプリに新しい機能を1つ追加してみましょう。
-
課題2:UIの改善
色・フォント・レイアウトを変更して、より使いやすいUIにカスタマイズしましょう。
-
課題3:保存機能の追加
処理結果をファイルに保存する機能を追加しましょう。