自動スクリーンショット
指定間隔でスクリーンショットを自動撮影しフォルダに保存するキャプチャーツール。日時で自動命名。
1. アプリ概要
指定間隔でスクリーンショットを自動撮影しフォルダに保存するキャプチャーツール。日時で自動命名。
このアプリはutilカテゴリの実践的なPythonアプリです。使用ライブラリは tkinter(標準ライブラリ)・Pillow、難易度は ★★☆ です。
このアプリは「ユーティリティ」カテゴリです。小回りの効くユーティリティは Python の真骨頂で、汎用的なユーザー操作を組み合わせる発想が他のアプリにもそのまま流用できます。tkinter(標準ライブラリ)・Pillow を活かして実装するこの構造は、他のアプリにも応用が効きます。
動かしながら読むことが理解の最短経路です。まずはコードをコピーして実行し、想定どおりに動くことを確認したうえで解説と照らし合わせてください。
カスタマイズでは「機能追加」「UI 改善」「エラー耐性」の三方向で考えると視野が広がります。練習問題にもそれぞれの方向の具体例を用意しています。
2. 機能一覧
- 「参照」ボタン(
_pick_dir())・「▶ 開始」ボタン(_start())・「⏹ 停止」ボタン(_stop())・「📂 保存先を開く」ボタン(_open_dir())・「🗑 履歴クリア」ボタン(_clear())で操作 - イベント
<<ListboxSelect>>から_on_select()を実行 tk.Listboxによる一覧表示(スクロールバー付き)filedialog.askdirectory()によるファイル選択ダイアログmessagebox.showerror()によるダイアログ通知- ウィンドウはタイトル「自動スクリーンショット」・サイズ 860x560 で起動
3. 事前準備・環境
Python 3.10 以上 / Windows 11(実機)で起動・基本操作を確認 / Linux Mint 22.3(仮想マシン)で起動・画面表示を確認(macOS は未検証)
Windows 11(実機)で起動と基本操作を確認しています(全機能の網羅テストではありません)。Linux Mint 22.3(仮想マシン)では起動と画面表示を確認しました。macOS は必要なライブラリ(pip install)を入れれば動作する想定ですが、未検証です。
- Python 3.10 以上
- OS: Windows 11(実機で起動・基本操作を確認)・Linux Mint 22.3(仮想マシンで起動・画面表示を確認)
インストールが必要なライブラリ
pip install pillow
4. 完全なソースコード
右上の「コピー」ボタンをクリックするとコードをクリップボードにコピーできます。
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import threading
import time
import os
from datetime import datetime
try:
from PIL import ImageGrab, Image, ImageTk
PIL_AVAILABLE = True
except ImportError:
PIL_AVAILABLE = False
class App092:
"""自動スクリーンショット"""
def __init__(self, root):
self.root = root
self.root.title("自動スクリーンショット")
self.root.geometry("860x560")
self.root.configure(bg="#1e1e1e")
self._running = False
self._count = 0
self._history = [] # [(path, PIL.Image), ...]
self._tk_thumb = None
self._build_ui()
self.root.protocol("WM_DELETE_WINDOW", self._on_close)
def _build_ui(self):
header = tk.Frame(self.root, bg="#252526", pady=6)
header.pack(fill=tk.X)
tk.Label(header, text="📷 自動スクリーンショット",
font=("Noto Sans JP", 12, "bold"),
bg="#252526", fg="#4fc3f7").pack(side=tk.LEFT, padx=12)
if not PIL_AVAILABLE:
tk.Label(self.root,
text="⚠ Pillow が未インストールです (pip install Pillow)",
bg="#fff3cd", fg="#856404", font=("Arial", 9),
anchor="w", padx=8).pack(fill=tk.X)
# 設定パネル
cfg = tk.LabelFrame(self.root, text="設定", bg="#252526",
fg="#ccc", font=("Arial", 9), padx=8, pady=6)
cfg.pack(fill=tk.X, padx=8, pady=6)
tk.Label(cfg, text="保存先フォルダ:", bg="#252526", fg="#ccc",
font=("Arial", 9)).grid(row=0, column=0, sticky="w")
self.dir_var = tk.StringVar(value=os.path.join(
os.path.expanduser("~"), "Screenshots"))
ttk.Entry(cfg, textvariable=self.dir_var,
width=36).grid(row=0, column=1, padx=4)
ttk.Button(cfg, text="参照",
command=self._pick_dir).grid(row=0, column=2, padx=2)
tk.Label(cfg, text="撮影間隔 (秒):", bg="#252526", fg="#ccc",
font=("Arial", 9)).grid(row=1, column=0, sticky="w", pady=4)
self.interval_var = tk.DoubleVar(value=5.0)
ttk.Spinbox(cfg, from_=0.5, to=3600, increment=0.5,
textvariable=self.interval_var,
width=8).grid(row=1, column=1, sticky="w", padx=4)
tk.Label(cfg, text="最大枚数 (0=無制限):", bg="#252526", fg="#ccc",
font=("Arial", 9)).grid(row=1, column=2, sticky="w", padx=(12, 0))
self.max_var = tk.IntVar(value=0)
ttk.Spinbox(cfg, from_=0, to=9999, textvariable=self.max_var,
width=6).grid(row=1, column=3, padx=4)
tk.Label(cfg, text="ファイル名プレフィックス:", bg="#252526", fg="#ccc",
font=("Arial", 9)).grid(row=2, column=0, sticky="w")
self.prefix_var = tk.StringVar(value="screenshot")
ttk.Entry(cfg, textvariable=self.prefix_var,
width=16).grid(row=2, column=1, sticky="w", padx=4)
tk.Label(cfg, text="形式:", bg="#252526", fg="#ccc",
font=("Arial", 9)).grid(row=2, column=2, sticky="w", padx=(12, 0))
self.fmt_var = tk.StringVar(value="PNG")
ttk.Combobox(cfg, textvariable=self.fmt_var,
values=["PNG", "JPEG", "BMP"],
state="readonly", width=7).grid(row=2, column=3, padx=4)
# コントロールボタン
ctrl = tk.Frame(self.root, bg="#1e1e1e")
ctrl.pack(fill=tk.X, padx=8, pady=2)
self.start_btn = tk.Button(
ctrl, text="▶ 開始", command=self._start,
bg="#2e7d32", fg="white", relief=tk.FLAT,
font=("Arial", 12, "bold"), padx=20, pady=6,
activebackground="#1b5e20", bd=0)
self.start_btn.pack(side=tk.LEFT, padx=4)
self.stop_btn = tk.Button(
ctrl, text="⏹ 停止", command=self._stop,
bg="#c62828", fg="white", relief=tk.FLAT,
font=("Arial", 12), padx=20, pady=6,
activebackground="#b71c1c", bd=0, state=tk.DISABLED)
self.stop_btn.pack(side=tk.LEFT, padx=4)
ttk.Button(ctrl, text="📂 保存先を開く",
command=self._open_dir).pack(side=tk.LEFT, padx=8)
ttk.Button(ctrl, text="🗑 履歴クリア",
command=self._clear).pack(side=tk.LEFT, padx=2)
# メイン: 左=履歴リスト, 右=プレビュー
main = tk.Frame(self.root, bg="#1e1e1e")
main.pack(fill=tk.BOTH, expand=True, padx=8, pady=4)
left = tk.Frame(main, bg="#1e1e1e", width=220)
left.pack(side=tk.LEFT, fill=tk.Y)
left.pack_propagate(False)
tk.Label(left, text="撮影履歴", bg="#1e1e1e", fg="#888",
font=("Arial", 9)).pack(anchor="w")
self.listbox = tk.Listbox(left, bg="#0d1117", fg="#c9d1d9",
selectbackground="#1f6feb",
font=("Arial", 8), relief=tk.FLAT)
lsb = ttk.Scrollbar(left, command=self.listbox.yview)
self.listbox.configure(yscrollcommand=lsb.set)
lsb.pack(side=tk.RIGHT, fill=tk.Y)
self.listbox.pack(fill=tk.BOTH, expand=True)
self.listbox.bind("<<ListboxSelect>>", self._on_select)
right = tk.Frame(main, bg="#1e1e1e")
right.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(8, 0))
tk.Label(right, text="プレビュー", bg="#1e1e1e", fg="#888",
font=("Arial", 9)).pack(anchor="w")
self.canvas = tk.Canvas(right, bg="#0d1117", highlightthickness=0)
self.canvas.pack(fill=tk.BOTH, expand=True)
# プログレス
self.progress = ttk.Progressbar(self.root, mode="determinate",
maximum=100)
self.progress.pack(fill=tk.X, padx=8)
self.status_var = tk.StringVar(value="設定して「開始」を押してください")
tk.Label(self.root, textvariable=self.status_var,
bg="#252526", fg="#858585", font=("Arial", 9),
anchor="w", padx=8).pack(fill=tk.X, side=tk.BOTTOM)
def _pick_dir(self):
d = filedialog.askdirectory()
if d:
self.dir_var.set(d)
def _open_dir(self):
d = self.dir_var.get()
if os.path.isdir(d):
import subprocess
subprocess.Popen(f'explorer "{d}"' if os.name == "nt" else ["xdg-open", d],
shell=(os.name == "nt"))
def _start(self):
if not PIL_AVAILABLE:
messagebox.showerror("エラー", "pip install Pillow")
return
save_dir = self.dir_var.get().strip()
os.makedirs(save_dir, exist_ok=True)
self._running = True
self._count = 0
self.start_btn.config(state=tk.DISABLED)
self.stop_btn.config(state=tk.NORMAL)
threading.Thread(target=self._capture_loop, daemon=True).start()
def _stop(self):
self._running = False
self.start_btn.config(state=tk.NORMAL)
self.stop_btn.config(state=tk.DISABLED)
self.status_var.set(f"停止 (撮影数: {self._count})")
def _capture_loop(self):
interval = self.interval_var.get()
max_cnt = self.max_var.get()
prefix = self.prefix_var.get()
fmt = self.fmt_var.get().upper()
ext = ".jpg" if fmt == "JPEG" else f".{fmt.lower()}"
save_dir = self.dir_var.get()
while self._running:
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
path = os.path.join(save_dir, f"{prefix}_{ts}{ext}")
try:
img = ImageGrab.grab()
if fmt == "JPEG":
img = img.convert("RGB")
img.save(path)
self._count += 1
self.root.after(0, self._add_history, path, img)
except Exception as e:
self.root.after(0, self.status_var.set, f"エラー: {e}")
if max_cnt > 0 and self._count >= max_cnt:
self.root.after(0, self._stop)
break
# 待機 (プログレスバー更新)
steps = max(1, int(interval / 0.1))
for i in range(steps):
if not self._running:
break
self.root.after(0, self.progress.configure,
{"value": (i + 1) / steps * 100})
time.sleep(0.1)
def _add_history(self, path, img):
self._history.append((path, img))
fname = os.path.basename(path)
self.listbox.insert(0, fname)
self.listbox.selection_clear(0, tk.END)
self.listbox.selection_set(0)
self._show_preview(img)
self.status_var.set(f"撮影: {self._count} 枚 最後: {fname}")
def _on_select(self, _=None):
sel = self.listbox.curselection()
if not sel:
return
# リストは新しい順 (0が最新)
idx = sel[0]
rev_idx = len(self._history) - 1 - idx
if 0 <= rev_idx < len(self._history):
_, img = self._history[rev_idx]
self._show_preview(img)
def _show_preview(self, img):
self.canvas.update_idletasks()
cw = max(100, self.canvas.winfo_width())
ch = max(100, self.canvas.winfo_height())
thumb = img.copy()
thumb.thumbnail((cw - 4, ch - 4),
Image.LANCZOS if hasattr(Image, "LANCZOS") else Image.ANTIALIAS)
self._tk_thumb = ImageTk.PhotoImage(thumb)
self.canvas.delete("all")
self.canvas.create_image(cw // 2, ch // 2,
image=self._tk_thumb, anchor="center")
def _clear(self):
self._history.clear()
self.listbox.delete(0, tk.END)
self.canvas.delete("all")
self.status_var.set("履歴をクリアしました")
def _on_close(self):
self._running = False
self.root.destroy()
if __name__ == "__main__":
root = tk.Tk()
app = App092(root)
root.mainloop()
5. コード解説
自動スクリーンショットのコードを、実際に書かれている実装に沿って解説します。
クラス設計とコンストラクタ
App092 クラスにアプリの全機能をまとめています(メソッド12個・全247行)。__init__ ではタイトル「自動スクリーンショット」とウィンドウサイズ 860x560 を設定します。あわせて状態を保持する変数(self._running・self._count・self._tk_thumb)を初期化し、最後に _build_ui() で画面を組み立てます。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
ウィジェット構成
画面は tk.Frame×5・tk.Label×10・tk.LabelFrame・ttk.Entry×2・ttk.Button×3・ttk.Spinbox×2・ttk.Combobox・tk.Button×2・tk.Listbox・ttk.Scrollbar・tk.Canvas・ttk.Progressbar で構成しています。見出し付きの枠(LabelFrame)は「設定」のラベルでエリアを区切っています。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
イベント処理とボタンの接続
「参照」ボタン(_pick_dir())・「▶ 開始」ボタン(_start())・「⏹ 停止」ボタン(_stop())・「📂 保存先を開く」ボタン(_open_dir())・「🗑 履歴クリア」ボタン(_clear())を command= で接続しています。また bind("<<ListboxSelect>>", ...) から _on_select() を呼べるようにイベントも登録しています。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
例外処理
try-except で Exception・ImportError を捕捉しています。あわせて messagebox.showerror() のダイアログで、ユーザーへの通知・確認を行います。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
6. ステップバイステップガイド
このアプリをゼロから自分で作る手順を解説します。コードをコピーするだけでなく、実際に手順を追って自分で書いてみましょう。
-
1ファイルを作成する
新しいファイルを作成して app092.py と保存します。外部ライブラリ
PILを使います。先にpip install Pillowを実行してください。(import 名と pip のパッケージ名が異なる点に注意:PILはPillowでインストールします) -
2クラスの骨格を作る
App092クラスを定義し、__init__と、末尾のroot = tk.Tk()~mainloop()の最小構成を書いて、まず空のウィンドウが出ることを確認します。 -
3ウィンドウを設定する
title("自動スクリーンショット")・geometry("860x560")・configure(bg="#1e1e1e")をコンストラクタで設定します。 -
4画面部品を並べる
_build_ui()の中で、tk.Frame×5・tk.Label×10・tk.LabelFrame・ttk.Entry×2・ttk.Button×3・ttk.Spinbox×2・ttk.Combobox・tk.Button×2・tk.Listbox・ttk.Scrollbar・tk.Canvas・ttk.Progressbarを作って配置します(掲載コードと同じ並び順で書くとレイアウトが一致します)。まず表示だけ確認しましょう。 -
5イベントを接続する
「2. 機能一覧」で挙げた各ボタンを
command=で対応するメソッド(_pick_dir()・_start()・_stop()・_open_dir()・_clear())につなぎます。bind("<<ListboxSelect>>", ...)の登録も忘れずに。 -
6中心になるメソッドを実装する
アプリの本体である
_stop()(5行)・_add_history()(8行)・_start()(11行)など を実装します。 -
7動作確認する
python app092.pyで起動し、各ボタンが反応することを確認します。
7. カスタマイズアイデア
基本機能を習得したら、以下のカスタマイズに挑戦してみましょう。
💡 ダークモードを追加する
bg色・fg色を辞書で管理し、ボタン1つでダークモード・ライトモードを切り替えられるようにしましょう。
💡 配色をまとめて管理する
背景色 #1e1e1e をはじめ、色コードが各所に直書きされています。色を1つの辞書にまとめると、テーマの切り替えが1か所の変更で済むようになります。
💡 設定ダイアログ
フォントサイズや色などの設定をユーザーが変更できるオプションダイアログを追加しましょう。
8. よくある問題と解決法
❌ 文字のフォントが崩れる・見た目が違う
原因:このコードは font=("Noto Sans JP", ...) のようにフォント名を直接指定しています。環境にこのフォントが入っていないと、OS が代わりのフォントで表示するため、見本と見た目が変わることがあります。
解決法:動作には支障ありません。気になる場合は font 引数の名前を自分の OS に入っているフォントへ書き換えるか、font 引数を省略してください。
❌ ウィンドウの大きさが画面に合わない
原因:geometry("860x560") の固定サイズで起動するためです。
解決法:この数値を書き換えて起動サイズを調整してください。なお、このコードにはウィンドウサイズを固定する設定(resizable の指定)が無いため、ウィンドウの端をドラッグしたサイズ変更は既定どおり可能です。
9. 練習問題
アプリの理解を深めるための練習問題です。気になるものから挑戦してみてください(課題3は定型の発展課題です)。このアプリは画面全体をそのまま画像として保存します。他の人が写る場所や、他人の情報が映っている画面では撮らないでください。試すのは自分のPCだけにしてください(勤務先や共用のPCは、画面の自動撮影が社内規程で禁じられていることがあります)。メールやチャットなど自分の個人情報が出ている窓は、先に閉じておくと安全です。課題1は1枚も撮らずに確かめられます。ただし Pillow が無いと「▶ 開始」はその手前で止まるので、先に入れておいてください(pip install Pillow)。課題2では画像が増えるため、保存先には練習用の空フォルダを指定してください。
-
課題1:保存先フォルダを作れないときに知らせる
「▶ 開始」は保存先フォルダを作ってから撮影を始めます。ところが作れなかったときは、画面に何も出ません。押した人が気づけるように、エラーを知らせてください。
期待結果:保存先フォルダの欄を空にして「▶ 開始」を押す。いまはボタンの見た目も帯の文字も変わらず、実行したコンソールにだけ
FileNotFoundErrorが出る。直したあとは「保存先フォルダを指定してください」のようなエラーダイアログが出る。合格条件
- 正しいフォルダを指定したときは、これまでどおり撮影が始まる
- 失敗したときは「▶ 開始」が押せるままで、「⏹ 停止」は押せないまま
- エラーのあと保存先を直せば、そのまま開始できる
ヒント①(どこを触るか): 触るのは
_start()の1か所です。os.makedirs(save_dir, exist_ok=True)の行をtryで囲みます。
ヒント②(使うもの): 同じ_start()の先頭にあるmessagebox.showerror("エラー", "pip install Pillow")が同じ形の手本です。except OSError as e:で受けてメッセージを出し、そのままreturnします。欄が空のときはif not save_dir:で先に弾けます。
つまずきやすい点:self._running = Trueとボタンの切り替えはos.makedirs()より後ろにあります。順番を入れ替えて先にボタンを無効にすると、失敗したときに「▶ 開始」が押せなくなります。tkinter はボタンの中で出た例外を画面に出さずコンソールへ流すので、いまは失敗そのものに気づけません。 -
課題2:撮影中に変えた「撮影間隔」をすぐ効かせる
撮影の間隔は、開始を押した時点の値を1回だけ読んでいます。だから撮影中に欄を変えても、次の1枚までの時間は変わりません。待つ直前に読み直してください。
期待結果:撮影間隔を
5のままにして「▶ 開始」を押し、1枚撮れたところで欄を1に変える。いまは次の1枚まで約5秒かかったままで、帯の撮影: 2 枚もその間は出ない。直したあとは、変えた次の待ちから約1秒で撮れる。合格条件
- 欄を変えない限り、これまでどおり最初の間隔で撮り続ける
- 「⏹ 停止」は、これまでどおり押してすぐ止まる
- 保存先・接頭辞・形式の扱いは変わらない
ヒント①(どこを触るか): 触るのは
_capture_loop()の2か所です。先頭のinterval = self.interval_var.get()を消し、待機の直前で読み直します。
ヒント②(使うもの): 刻み数を決めているsteps = max(1, int(interval / 0.1))の直前へ移すのがいちばん簡単です。self.interval_varはtk.DoubleVarなのでget()は小数を返します。
つまずきやすい点: 待機は 0.1 秒ずつ刻んでいるため、変更が効くのは次の待機に入ってからです。いま撮り終えた1回分の待ちは最後まで続きます。欄を空にするとget()がtkinter.TclErrorを出し、撮影スレッドだけが止まります。画面は動いたままなので、tryで受けて前の値を使うと安全です。 -
課題3:新しいボタンを追加する
既存のボタンと同じ書き方で新しいボタンを1つ足し、押されたときに動くメソッドを自作してみましょう。ボタンの作成と
command=の接続がセットで身に付きます。
写経中に赤いエラー文が出たら、Pythonエラー一覧&解決法(英語メッセージ逆引き)で原因と直し方をすぐ確認できます。
このレベルのアプリが作れたら、入門書の次の「作るための本」へ進む時期です。実践におすすめのPython本(当サイトの参考書ランキング総合3〜4位)で自動化とコードの書き方の2冊を、その直後の用途別専門書の節でデータ分析・Web・AIの本を比較しています。