プロセス管理ツール
psutilで実行中プロセス一覧を表示しCPU・メモリ使用率でソート・強制終了できるタスクマネージャー。
1. アプリ概要
psutilで実行中プロセス一覧を表示しCPU・メモリ使用率でソート・強制終了できるタスクマネージャー。
このアプリはutilカテゴリの実践的なPythonアプリです。使用ライブラリは tkinter(標準ライブラリ)・psutil、難易度は ★★★ です。
このアプリは「ユーティリティ」カテゴリです。小回りの効くユーティリティは Python の真骨頂で、汎用的なユーザー操作を組み合わせる発想が他のアプリにもそのまま流用できます。tkinter(標準ライブラリ)・psutil を活かして実装するこの構造は、他のアプリにも応用が効きます。
完成形を見てから細部の解説に進む流れが効果的です。実行→気になる箇所を解説で確認→自分の手で改造、というサイクルで定着が早まります。
カスタマイズ章では具体的な拡張アイデアを示しています。一つ実装するごとに動作確認することで、変更が予期せぬ副作用を起こさないかも体感できます。
2. 機能一覧
- 「🔃 更新」ボタン(
_refresh())・「🛑 強制終了」ボタン(_kill())・「⏸ 一時停止」ボタン(_suspend())・「▶ 再開」ボタン(_resume())で操作 - イベント
<<TreeviewSelect>>から_on_select()を実行 ttk.Treeviewによる表形式の一覧表示messagebox.askyesno()・messagebox.showerror()・messagebox.showinfo()によるダイアログ通知- ウィンドウはタイトル「プロセス管理ツール」・サイズ 1000x600 で起動
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(仮想マシンで起動・画面表示を確認)
「強制終了」「一時停止」は選んだプロセスを本当に止めます。ほかのアプリや Windows 自身のプロセスを止めると、保存していない作業が消えたり画面が固まったりします。とくに「一時停止」と「再開」は確認なしで即実行されます(強制終了だけ確認が出ます)。試すときは自分で起動したメモ帳のような、止まって困らないプロセスだけにしてください(詳しくは9章)。
インストールが必要なライブラリ
pip install psutil
4. 完全なソースコード
右上の「コピー」ボタンをクリックするとコードをクリップボードにコピーできます。
import tkinter as tk
from tkinter import ttk, messagebox
import threading
import os
import signal
try:
import psutil
PSUTIL_AVAILABLE = True
except ImportError:
PSUTIL_AVAILABLE = False
class App094:
"""プロセス管理ツール"""
def __init__(self, root):
self.root = root
self.root.title("プロセス管理ツール")
self.root.geometry("1000x600")
self.root.configure(bg="#0d1117")
self._auto_refresh = True
self._refresh_interval = 3000 # ms
self._build_ui()
if PSUTIL_AVAILABLE:
self._refresh()
self.root.protocol("WM_DELETE_WINDOW", self._on_close)
def _build_ui(self):
header = tk.Frame(self.root, bg="#161b22", pady=6)
header.pack(fill=tk.X)
tk.Label(header, text="⚙ プロセス管理ツール",
font=("Noto Sans JP", 12, "bold"),
bg="#161b22", fg="#4fc3f7").pack(side=tk.LEFT, padx=12)
if not PSUTIL_AVAILABLE:
tk.Label(self.root,
text="⚠ psutil が未インストールです (pip install psutil)",
bg="#fff3cd", fg="#856404", font=("Arial", 9),
anchor="w", padx=8).pack(fill=tk.X)
# コントロール
ctrl = tk.Frame(self.root, bg="#161b22", pady=4)
ctrl.pack(fill=tk.X)
ttk.Button(ctrl, text="🔃 更新",
command=self._refresh).pack(side=tk.LEFT, padx=4)
self.auto_var = tk.BooleanVar(value=True)
tk.Checkbutton(ctrl, text="自動更新", variable=self.auto_var,
bg="#161b22", fg="#ccc", selectcolor="#0d1117",
activebackground="#161b22",
command=self._toggle_auto).pack(side=tk.LEFT, padx=4)
tk.Label(ctrl, text="間隔(秒):", bg="#161b22", fg="#8b949e",
font=("Arial", 9)).pack(side=tk.LEFT, padx=(8, 2))
self.interval_var = tk.IntVar(value=3)
ttk.Spinbox(ctrl, from_=1, to=60, textvariable=self.interval_var,
width=4).pack(side=tk.LEFT)
tk.Label(ctrl, text="検索:", bg="#161b22", fg="#8b949e",
font=("Arial", 9)).pack(side=tk.LEFT, padx=(12, 2))
self.search_var = tk.StringVar()
ttk.Entry(ctrl, textvariable=self.search_var,
width=16).pack(side=tk.LEFT)
self.search_var.trace_add("write", lambda *_: self._apply_filter())
tk.Label(ctrl, text="ソート:", bg="#161b22", fg="#8b949e",
font=("Arial", 9)).pack(side=tk.LEFT, padx=(8, 2))
self.sort_var = tk.StringVar(value="CPU%")
ttk.Combobox(ctrl, textvariable=self.sort_var,
values=["PID", "名前", "CPU%", "メモリ%", "ステータス"],
state="readonly", width=10).pack(side=tk.LEFT)
self.sort_var.trace_add("write", lambda *_: self._apply_filter())
# 操作ボタン
tk.Button(ctrl, text="🛑 強制終了", command=self._kill,
bg="#c62828", fg="white", relief=tk.FLAT,
font=("Arial", 10), padx=12, pady=3,
activebackground="#b71c1c", bd=0).pack(side=tk.RIGHT, padx=4)
tk.Button(ctrl, text="⏸ 一時停止", command=self._suspend,
bg="#f57c00", fg="white", relief=tk.FLAT,
font=("Arial", 10), padx=12, pady=3,
activebackground="#e65100", bd=0).pack(side=tk.RIGHT, padx=2)
tk.Button(ctrl, text="▶ 再開", command=self._resume,
bg="#2e7d32", fg="white", relief=tk.FLAT,
font=("Arial", 10), padx=12, pady=3,
activebackground="#1b5e20", bd=0).pack(side=tk.RIGHT, padx=2)
# プロセスリスト
cols = ("pid", "name", "status", "cpu", "memory", "threads", "user")
self.tree = ttk.Treeview(self.root, columns=cols, show="headings",
selectmode="browse")
headers = [
("pid", "PID", 70, "center"),
("name", "プロセス名", 200, "w"),
("status", "ステータス", 80, "center"),
("cpu", "CPU%", 70, "center"),
("memory", "メモリ%", 70, "center"),
("threads", "スレッド", 70, "center"),
("user", "ユーザー", 120, "w"),
]
for col, text, width, anchor in headers:
self.tree.heading(col, text=text,
command=lambda c=col: self._sort_by(c))
self.tree.column(col, width=width, anchor=anchor)
tsb = ttk.Scrollbar(self.root, command=self.tree.yview)
self.tree.configure(yscrollcommand=tsb.set)
tsb.pack(side=tk.RIGHT, fill=tk.Y)
self.tree.pack(fill=tk.BOTH, expand=True, padx=8, pady=4)
self.tree.tag_configure("running", foreground="#26a69a")
self.tree.tag_configure("sleeping", foreground="#8b949e")
self.tree.tag_configure("zombie", foreground="#ef5350")
self.tree.tag_configure("high_cpu", background="#3d1a1a")
# 詳細エリア
detail_f = tk.Frame(self.root, bg="#161b22")
detail_f.pack(fill=tk.X, padx=8, pady=2)
self.detail_var = tk.StringVar()
tk.Label(detail_f, textvariable=self.detail_var,
bg="#161b22", fg="#8b949e", font=("Courier New", 8),
anchor="w").pack(fill=tk.X)
self.tree.bind("<<TreeviewSelect>>", self._on_select)
# システム情報
self.sys_var = tk.StringVar()
tk.Label(self.root, textvariable=self.sys_var,
bg="#21262d", fg="#8b949e", font=("Arial", 9),
anchor="w", padx=8).pack(fill=tk.X, side=tk.BOTTOM)
self._procs = [] # [(pid, name, status, cpu, mem, threads, user)]
def _toggle_auto(self):
self._auto_refresh = self.auto_var.get()
if self._auto_refresh:
self._refresh()
def _refresh(self):
if not PSUTIL_AVAILABLE:
return
threading.Thread(target=self._do_refresh, daemon=True).start()
if self._auto_refresh and self.auto_var.get():
interval = max(1000, self.interval_var.get() * 1000)
self.root.after(interval, self._refresh)
def _do_refresh(self):
procs = []
for p in psutil.process_iter(
["pid", "name", "status", "cpu_percent",
"memory_percent", "num_threads", "username"]):
try:
info = p.info
procs.append((
info["pid"],
info["name"] or "",
info["status"] or "",
info["cpu_percent"] or 0.0,
info["memory_percent"] or 0.0,
info["num_threads"] or 0,
info["username"] or "",
))
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
cpu_total = psutil.cpu_percent()
mem = psutil.virtual_memory()
sys_str = (f"CPU: {cpu_total:.1f}% "
f"メモリ: {mem.percent:.1f}% "
f"({mem.used//1024//1024:,}MB / {mem.total//1024//1024:,}MB) "
f"プロセス数: {len(procs)}")
self._procs = procs
self.root.after(0, self._update_table, sys_str)
def _update_table(self, sys_str):
self.sys_var.set(sys_str)
self._apply_filter()
def _apply_filter(self):
kw = self.search_var.get().strip().lower()
sort = self.sort_var.get()
procs = self._procs
if kw:
procs = [p for p in procs
if kw in str(p[0]).lower() or kw in p[1].lower()]
sort_key = {"PID": 0, "名前": 1, "ステータス": 2,
"CPU%": 3, "メモリ%": 4, "スレッド": 5}.get(sort, 3)
procs = sorted(procs, key=lambda p: p[sort_key], reverse=(sort_key in (3, 4, 5)))
self.tree.delete(*self.tree.get_children())
for pid, name, status, cpu, mem, threads, user in procs:
tags = []
if status == "running": tags.append("running")
elif status == "sleeping": tags.append("sleeping")
elif status in ("zombie",): tags.append("zombie")
if cpu > 50: tags.append("high_cpu")
self.tree.insert("", tk.END,
values=(pid, name, status,
f"{cpu:.1f}", f"{mem:.2f}",
threads, (user or "")[:20]),
tags=tags)
def _sort_by(self, col):
mapping = {"pid": "PID", "name": "名前", "status": "ステータス",
"cpu": "CPU%", "memory": "メモリ%"}
if col in mapping:
self.sort_var.set(mapping[col])
def _get_selected_pid(self):
sel = self.tree.selection()
if not sel:
messagebox.showinfo("情報", "プロセスを選択してください")
return None
return int(self.tree.item(sel[0], "values")[0])
def _kill(self):
pid = self._get_selected_pid()
if pid is None:
return
if not messagebox.askyesno("確認", f"PID {pid} を強制終了しますか?"):
return
try:
psutil.Process(pid).kill()
self.sys_var.set(f"PID {pid} を終了しました")
self._refresh()
except Exception as e:
messagebox.showerror("エラー", str(e))
def _suspend(self):
pid = self._get_selected_pid()
if pid is None:
return
try:
psutil.Process(pid).suspend()
self.sys_var.set(f"PID {pid} を一時停止しました")
except Exception as e:
messagebox.showerror("エラー", str(e))
def _resume(self):
pid = self._get_selected_pid()
if pid is None:
return
try:
psutil.Process(pid).resume()
self.sys_var.set(f"PID {pid} を再開しました")
except Exception as e:
messagebox.showerror("エラー", str(e))
def _on_select(self, _=None):
sel = self.tree.selection()
if not sel or not PSUTIL_AVAILABLE:
return
try:
pid = int(self.tree.item(sel[0], "values")[0])
p = psutil.Process(pid)
with p.oneshot():
exe = p.exe() if hasattr(p, "exe") else ""
cwd = ""
try:
cwd = p.cwd()
except Exception:
pass
self.detail_var.set(
f"PID: {pid} 実行ファイル: {exe[:60]} 作業ディレクトリ: {cwd[:60]}")
except Exception:
pass
def _on_close(self):
self._auto_refresh = False
self.root.destroy()
if __name__ == "__main__":
root = tk.Tk()
app = App094(root)
root.mainloop()
5. コード解説
プロセス管理ツールのコードを、実際に書かれている実装に沿って解説します。
クラス設計とコンストラクタ
App094 クラスにアプリの全機能をまとめています(メソッド14個・全276行)。__init__ ではタイトル「プロセス管理ツール」とウィンドウサイズ 1000x600 を設定します。あわせて状態を保持する変数(self._auto_refresh)を初期化し、最後に _build_ui() で画面を組み立てます。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
ウィジェット構成
画面は tk.Frame×3・tk.Label×7・ttk.Button・tk.Checkbutton・ttk.Spinbox・ttk.Entry・ttk.Combobox・tk.Button×3・ttk.Treeview・ttk.Scrollbar で構成しています。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
イベント処理とボタンの接続
「🔃 更新」ボタン(_refresh())・「🛑 強制終了」ボタン(_kill())・「⏸ 一時停止」ボタン(_suspend())・「▶ 再開」ボタン(_resume())を command= で接続しています。また bind("<<TreeviewSelect>>", ...) から _on_select() を呼べるようにイベントも登録しています。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
例外処理
try-except で Exception・ImportError・psutil.AccessDenied・psutil.NoSuchProcess を捕捉しています。あわせて messagebox.askyesno()・messagebox.showerror()・messagebox.showinfo() のダイアログで、ユーザーへの通知・確認を行います。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
6. ステップバイステップガイド
このアプリをゼロから自分で作る手順を解説します。コードをコピーするだけでなく、実際に手順を追って自分で書いてみましょう。
-
1ファイルを作成する
新しいファイルを作成して app094.py と保存します。外部ライブラリ
psutilを使います。先にpip install psutilを実行してください。 -
2クラスの骨格を作る
App094クラスを定義し、__init__と、末尾のroot = tk.Tk()~mainloop()の最小構成を書いて、まず空のウィンドウが出ることを確認します。 -
3ウィンドウを設定する
title("プロセス管理ツール")・geometry("1000x600")・configure(bg="#0d1117")をコンストラクタで設定します。 -
4画面部品を並べる
_build_ui()の中で、tk.Frame×3・tk.Label×7・ttk.Button・tk.Checkbutton・ttk.Spinbox・ttk.Entry・ttk.Combobox・tk.Button×3・ttk.Treeview・ttk.Scrollbarを作って配置します(掲載コードと同じ並び順で書くとレイアウトが一致します)。まず表示だけ確認しましょう。 -
5イベントを接続する
「2. 機能一覧」で挙げた各ボタンを
command=で対応するメソッド(_refresh()・_kill()・_suspend()・_resume())につなぎます。bind("<<TreeviewSelect>>", ...)の登録も忘れずに。 -
6中心になるメソッドを実装する
アプリの本体である
_refresh()(7行)・_update_table()(3行)・_on_select()(18行)など を実装します。 -
7動作確認する
python app094.pyで起動し、各ボタンが反応することを確認します。
7. カスタマイズアイデア
基本機能を習得したら、以下のカスタマイズに挑戦してみましょう。
💡 ダークモードを追加する
bg色・fg色を辞書で管理し、ボタン1つでダークモード・ライトモードを切り替えられるようにしましょう。
💡 結果をファイルに保存する
このコードに保存処理はないため、表示中の数値や結果はアプリを閉じると消えます。テキストファイルへ書き出して、次回起動時に読み込む機能を追加してみましょう。
💡 設定ダイアログ
フォントサイズや色などの設定をユーザーが変更できるオプションダイアログを追加しましょう。
8. よくある問題と解決法
❌ 文字のフォントが崩れる・見た目が違う
原因:このコードは font=("Noto Sans JP", ...) のようにフォント名を直接指定しています。環境にこのフォントが入っていないと、OS が代わりのフォントで表示するため、見本と見た目が変わることがあります。
解決法:動作には支障ありません。気になる場合は font 引数の名前を自分の OS に入っているフォントへ書き換えるか、font 引数を省略してください。
❌ ウィンドウの大きさが画面に合わない
原因:geometry("1000x600") の固定サイズで起動するためです。
解決法:この数値を書き換えて起動サイズを調整してください。なお、このコードにはウィンドウサイズを固定する設定(resizable の指定)が無いため、ウィンドウの端をドラッグしたサイズ変更は既定どおり可能です。
9. 練習問題
アプリの理解を深めるための練習問題です。気になるものから挑戦してみてください(課題3は定型の発展課題です)。「🛑 強制終了」と「⏸ 一時停止」は、選んだプロセスを本当に止めます(216-247行)。ほかのアプリや Windows 自身のプロセスを止めると、保存していない作業が消えたり動作が不安定になったりします。練習では操作ボタン(🛑 ⏸ ▶)を押さないでください。一覧にはログイン中のユーザー名や実行ファイルのパスが出るので、画面を人に見せるときは注意してください。下の課題1・課題2は、どちらもこの3つのボタンを押さずに確かめられます。表を出すには psutil が必要で(7-11行)、入っていないときは警告だけが出て一覧は空のままです(36-40行)。
-
課題1:自動更新のたびに選択が外れるのに、下の詳細表示は前のプロセスのまま残る
_apply_filter()は毎回self.tree.delete(*self.tree.get_children())で表を作り直します(190行)。行が作り直されると選択も消えます。いっぽう詳細ラベルを書き換えるのは_on_select()だけで(249-264行)、空に戻す処理はどこにもありません。期待結果:一覧から「ユーザー」の列に自分のログイン名が出ている行を1つクリックします。自分で起動した
python.exeなどが対象です。表の下にPID: 1234 実行ファイル: ...のような行が出る。ほかのユーザーやシステムのプロセスは中身を読めず、265-266行で握りつぶされるため何も出ないことがあります。そのまま3秒ほど待つと、行の選択色だけが消える(間隔の初期値は 56行の3)。下の詳細表示は、選んでいたプロセスの内容を出したまま残る。検索欄に文字を入れたときも同じで、選択だけが外れる(65行)。直したあとは、選んでいたプロセスが一覧に残っている限り選択も残る。合格条件
- 自動更新のあとも、選んでいたプロセスの行が選ばれたまま残る
- 選んでいたプロセスが一覧から消えたときは、下の詳細表示が空になる
- 検索やソートを変えたときの並びと件数は、いままでどおり
ヒント①(どこを触るか): 触るのは
_apply_filter()の中の2か所です。表を消す前(190行)と、行を入れ終えたあと(197-201行の後ろ)。
ヒント②(使うもの): いま選ばれている PID はself.tree.selection()とself.tree.item(sel[0], "values")[0]で読めます(210-214行と同じ書き方)。self.tree.insert()は作った行の iid を返すので、PID が一致した行の iid を覚えておき、最後にself.tree.selection_set(iid)を呼びます。見つからなかったときはself.detail_var.set("")で詳細を空にしてください。
つまずきやすい点:_apply_filter()の入口は3つあり、検索欄(65行)・ソート欄(73行)・更新の直後(175行)から呼ばれます。表から取り出した PID は文字列で返ることがあるため、int()を通してから比べると確実です(214行が同じことをしています)。更新の間隔を決めているのは 56行の初期値と 142行の式で、23行のself._refresh_intervalはどこからも読まれていません。 -
課題2:「スレッド」と「ユーザー」の見出しをクリックしても並び替わらない
表の見出しは7列ぶんすべて
_sort_by()につながっています(90行・102-104行)。ところが_sort_by()の対応表には5列しか載っていません(204-205行)。残る2列はここに無いため、押しても何も起きずに終わります。期待結果:「PID」「プロセス名」「CPU%」「メモリ%」「ステータス」の見出しをクリックすると、上のソート欄の表示がその名前に変わって並びも変わる。「スレッド」と「ユーザー」の見出しをクリックした場合は、ソート欄も並びも変わらない。エラーの表示も出ないので、押し方を間違えたようにしか見えない。直したあとは、この2列でも並び替えができて、ソート欄からも選べるようになる。
合格条件
- 「スレッド」の見出しをクリックすると、スレッド数の多い順に並ぶ
- 「ユーザー」の見出しをクリックすると、ユーザー名で並び替わる
- ソート欄からも同じ2つを選べて、ほかの5列の動きは変わらない
ヒント①(どこを触るか): 触るのは3か所です。
_sort_by()の対応表(204-205行)、_build_ui()のソート欄のvalues(70-72行)、_apply_filter()のsort_key(186-187行)。
ヒント②(使うもの):sort_keyには"スレッド": 5が最初から入っているので、足りないのはユーザー列の6だけです。数の大きい順に並べる列は 188行のsort_key in (3, 4, 5)で決まります。ユーザー名は文字なので、昇順のままのほうが読みやすいはずです。
つまずきやすい点: 187行の取り出しは.get(sort, 3)なので、辞書に無い名前を選んでもエラーは出ず、黙って CPU% 順になります。ソート欄はstate="readonly"です(72行)。valuesに無い名前をコードから入れると、表示はされてもドロップダウンから選び直せなくなります。 -
課題3:保存機能の追加
処理結果をファイルに保存する機能を追加しましょう。
写経中に赤いエラー文が出たら、Pythonエラー一覧&解決法(英語メッセージ逆引き)で原因と直し方をすぐ確認できます。
このレベルのアプリが作れたら、入門書の次の「作るための本」へ進む時期です。実践におすすめのPython本(当サイトの参考書ランキング総合3〜4位)で自動化とコードの書き方の2冊を、その直後の用途別専門書の節でデータ分析・Web・AIの本を比較しています。