Skip to content

Instantly share code, notes, and snippets.

@roflsunriz
Last active March 22, 2025 14:16
Show Gist options
  • Save roflsunriz/771e8881c4b7a7ff4119a6d7cfdf6d87 to your computer and use it in GitHub Desktop.
Save roflsunriz/771e8881c4b7a7ff4119a6d7cfdf6d87 to your computer and use it in GitHub Desktop.
taskkill_wizard_gui : タスクキルをGUIで出来るウィザード
import tkinter as tk
from tkinter import ttk, scrolledtext, messagebox
import psutil
import os
import win32gui
import win32process
import win32con
import win32api
from PIL import Image, ImageTk
import io
import subprocess
import sys
import pkg_resources
import win32ui
from itertools import groupby
# グローバル変数を追加
MISSING_PACKAGES = None
def install_required_packages():
global MISSING_PACKAGES
required = {'psutil', 'pywin32', 'pillow'}
installed = {pkg.key.lower() for pkg in pkg_resources.working_set}
MISSING_PACKAGES = required - installed
if MISSING_PACKAGES:
# 確認ダイアログを表示
root = tk.Tk()
root.withdraw()
answer = messagebox.askyesno(
"パッケージインストール",
f"次のパッケージをインストールしますか?\n{', '.join(MISSING_PACKAGES)}\n(管理者権限が必要な場合があるのじゃ)",
parent=root
)
root.destroy()
if answer:
print("必要なパッケージをインストールするのじゃ!")
for package in MISSING_PACKAGES:
subprocess.check_call([sys.executable, '-m', 'pip', 'install', package])
MISSING_PACKAGES = None # インストール完了フラグ
class TaskKillWizard:
def __init__(self, root):
self.root = root
self.root.title("タスクキル ウィザード")
self.root.geometry("1300x800") # ウィンドウの幅を1300にする
self.exclude_processes = [
"System", "smss.exe", "csrss.exe", "wininit.exe", "services.exe",
"lsass.exe", "winlogon.exe", "svchost.exe", "taskhostw.exe", "explorer.exe",
"RuntimeBroker.exe", "SearchUI.exe", "ShellExperienceHost.exe", "TextInputHost.exe",
"StartMenuExperienceHost.exe", "ApplicationFrameHost.exe", "LockApp.exe", "SystemSettings.exe",
"SettingSyncHost.exe", "SecurityHealthService.exe", "spoolsv.exe", "fontdrvhost.exe",
"ctfmon.exe", "dwm.exe", "audiodg.exe", "taskeng.exe", "wlanext.exe", "WUDFHost.exe",
"conhost.exe", "sihost.exe", "dllhost.exe", "WerFault.exe", "smartscreen.exe",
"backgroundTaskHost.exe", "unsecapp.exe", "msdtc.exe", "msiexec.exe", "InstallService.exe",
"TrustedInstaller.exe", "MoNotificationUx.exe", "MoNotificationUxBroker.exe", "WmiPrvSE.exe",
"SearchProtocolHost.exe", "SearchIndexer.exe", "CompatTelRunner.exe", "OneDrive.exe",
"YourPhone.exe", "Microsoft.Photos.exe", "Microsoft.Windows.Cortana.exe", "Microsoft.WindowsStore.exe",
"Video.UI.exe", "GameBar.exe", "GameBarElevatedFT.exe", "TextInputHost.exe",
"WindowsInternal.ComposableShell.Experiences.TextInput.InputApp.exe",
"WindowsInternal.Shell.Broker.exe", "WindowsInternal.Shell.Experiences.exe",
"WindowsInternal.Shell.ShellExperienceHost.exe", "WindowsInternal.Shell.StartMenuExperienceHost.exe",
"WindowsInternal.Shell.SearchApp.exe", "WindowsInternal.Shell.SearchHost.exe",
"WindowsInternal.Shell.SearchIndexer.exe", "WindowsInternal.Shell.SearchProtocolHost.exe",
"WindowsInternal.Shell.SearchUI.exe", "WindowsInternal.Shell.SystemSettings.exe",
"WindowsInternal.Shell.SettingSyncHost.exe", "WindowsInternal.Shell.LockApp.exe",
"WindowsInternal.Shell.ApplicationFrameHost.exe", "WindowsInternal.Shell.RuntimeBroker.exe",
"WindowsInternal.Shell.SecurityHealthService.exe", "WindowsInternal.Shell.MoNotificationUx.exe",
"WindowsInternal.Shell.MoNotificationUxBroker.exe", "WindowsInternal.Shell.CompatTelRunner.exe",
"WindowsInternal.Shell.OneDrive.exe", "WindowsInternal.Shell.YourPhone.exe",
"WindowsInternal.Shell.Microsoft.Photos.exe", "WindowsInternal.Shell.Microsoft.Windows.Cortana.exe",
"WindowsInternal.Shell.Microsoft.WindowsStore.exe", "WindowsInternal.Shell.Video.UI.exe",
"WindowsInternal.Shell.GameBar.exe", "WindowsInternal.Shell.GameBarElevatedFT.exe"
]
# メインフレームを3段に分割(左:プロセスグリッド、右:詳細情報ペイン)
self.main_frame = ttk.Frame(root)
self.main_frame.pack(expand=True, fill='both', padx=0, pady=0) # 余白を削除
# 検索フレームを最上部に配置
self.search_frame = ttk.Frame(self.main_frame)
self.search_frame.grid(row=0, column=0, columnspan=2, sticky='ew', padx=5, pady=2)
# 検索エントリー
self.search_var = tk.StringVar()
self.search_var.trace('w', self.filter_processes)
self.search_entry = ttk.Entry(self.search_frame, textvariable=self.search_var)
self.search_entry.pack(side='left', fill='x', expand=True, padx=(0, 5))
# クリアボタン
self.clear_btn = ttk.Button(self.search_frame, text="クリア", command=self.clear_search)
self.clear_btn.pack(side='right')
# 左側:プロセスグリッド用フレーム
self.left_frame = ttk.Frame(self.main_frame, width=200)
self.left_frame.grid(row=1, column=0, sticky='nsew', padx=0) # row=1に変更、余白削除
# カラムの重みを設定
self.main_frame.columnconfigure(0, weight=1) # 左側のカラムのみに重みを設定
# 行の重みを設定
self.main_frame.rowconfigure(1, weight=1) # プロセスグリッドに重みを設定
self.main_frame.rowconfigure(2, weight=0) # ログエリアに重みを設定しない
# 下段:ログエリア(最小化)
self.log_area = scrolledtext.ScrolledText(self.main_frame, height=3)
self.log_area.grid(row=2, column=0, columnspan=2, sticky='ew', padx=5, pady=(0, 15))
# プロセス情報を保持するリスト
self.process_frames = []
# プロセスグリッド関連のコードをleft_frameに移動
self.grid_container = ttk.Frame(self.left_frame)
self.grid_container.pack(expand=True, fill='both')
# スクロール可能なキャンバス
self.canvas = tk.Canvas(self.grid_container)
self.scrollbar_y = ttk.Scrollbar(self.grid_container, orient="vertical", command=self.canvas.yview)
self.scrollbar_x = ttk.Scrollbar(self.grid_container, orient="horizontal", command=self.canvas.xview)
# プロセス表示用フレーム
self.process_frame = ttk.Frame(self.canvas)
# スクロールバーの設定
self.canvas.configure(yscrollcommand=self.scrollbar_y.set, xscrollcommand=self.scrollbar_x.set)
# レイアウトの配置
self.scrollbar_y.pack(side='right', fill='y')
self.scrollbar_x.pack(side='bottom', fill='x')
self.canvas.pack(side='left', fill='both', expand=True)
# キャンバスにプロセスフレームを配置
self.canvas_window = self.canvas.create_window((0, 0), window=self.process_frame, anchor='nw')
# スクロール関連のバインドを追加
self.canvas.bind_all("<MouseWheel>", self._on_mousewheel)
self.process_frame.bind('<Configure>', self._on_frame_configure)
self.canvas.bind('<Configure>', self._on_canvas_configure)
# プロセスを読み込む
self.load_processes()
def get_process_icon(self, process):
try:
exe_path = process.exe()
if not exe_path:
return self.get_default_icon()
# アイコンを抽出(まずExtractIconExを試す)
ico_x = win32api.GetSystemMetrics(win32con.SM_CXSMICON)
ico_y = win32api.GetSystemMetrics(win32con.SM_CYSMICON)
try:
large, small = win32gui.ExtractIconEx(exe_path, 0)
if small:
icon_handle = small[0]
else:
# ExtractIconExが失敗した場合、SHGetFileInfoを試す
shinfo = win32gui.SHFILEINFO()
flags = win32con.SHGFI_ICON | win32con.SHGFI_SMALLICON
if win32gui.SHGetFileInfo(exe_path, 0, shinfo, len(shinfo), flags):
icon_handle = shinfo.hIcon
else:
return self.get_default_icon()
# アイコンをビットマップに変換
hdc = win32ui.CreateDCFromHandle(win32gui.GetDC(0))
hbmp = win32ui.CreateBitmap()
hbmp.CreateCompatibleBitmap(hdc, ico_x, ico_y)
hdc_mem = hdc.CreateCompatibleDC()
hdc_mem.SelectObject(hbmp)
# アイコンを描画
win32gui.DrawIconEx(
hdc_mem.GetHandleOutput(), 0, 0, icon_handle,
ico_x, ico_y, 0, None, win32con.DI_NORMAL
)
# ビットマップ情報を取得
bmpinfo = hbmp.GetInfo()
bmpstr = hbmp.GetBitmapBits(True)
# リソースを解放
win32gui.DestroyIcon(icon_handle)
if large: win32gui.DestroyIcon(large[0])
hdc_mem.DeleteDC()
hdc.DeleteDC()
win32gui.DeleteObject(hbmp.GetHandle())
# PILイメージに変換
img = Image.frombuffer(
'RGBA',
(ico_x, ico_y),
bmpstr, 'raw', 'BGRA', 0, 1
)
return ImageTk.PhotoImage(img)
except Exception:
return self.get_default_icon()
except Exception:
return self.get_default_icon()
def get_default_icon(self):
try:
shinfo = win32gui.SHFILEINFO()
flags = win32con.SHGFI_ICON | win32con.SHGFI_SMALLICON | win32con.SHGFI_USEFILEATTRIBUTES
if win32gui.SHGetFileInfo("dummy.exe", win32con.FILE_ATTRIBUTE_NORMAL, shinfo, len(shinfo), flags):
ico_x = win32api.GetSystemMetrics(win32con.SM_CXSMICON)
ico_y = win32api.GetSystemMetrics(win32con.SM_CYSMICON)
hdc = win32ui.CreateDCFromHandle(win32gui.GetDC(0))
hbmp = win32ui.CreateBitmap()
hbmp.CreateCompatibleBitmap(hdc, ico_x, ico_y)
hdc_mem = hdc.CreateCompatibleDC()
hdc_mem.SelectObject(hbmp)
win32gui.DrawIconEx(
hdc_mem.GetHandleOutput(), 0, 0, shinfo.hIcon,
ico_x, ico_y, 0, None, win32con.DI_NORMAL
)
bmpinfo = hbmp.GetInfo()
bmpstr = hbmp.GetBitmapBits(True)
win32gui.DestroyIcon(shinfo.hIcon)
hdc_mem.DeleteDC()
hdc.DeleteDC()
win32gui.DeleteObject(hbmp.GetHandle())
img = Image.frombuffer(
'RGBA',
(ico_x, ico_y),
bmpstr, 'raw', 'BGRA', 0, 1
)
return ImageTk.PhotoImage(img)
except:
pass
# 完全に失敗した場合は空の16x16画像を返す
return ImageTk.PhotoImage(Image.new('RGBA', (16, 16), (200, 200, 200, 255)))
def kill_process(self, name, pids):
success = 0
failed = 0
for pid in pids:
try:
process = psutil.Process(pid)
# 子プロセスも含めて終了
for child in process.children(recursive=True):
try:
child.terminate()
except:
try:
child.kill()
except:
pass
# メインプロセスの終了
try:
process.terminate() # まずは通常終了を試みる
process.wait(timeout=3) # 3秒待機
except psutil.TimeoutExpired:
try:
process.kill() # 強制終了
except:
failed += 1
self.log(f"PID {pid} の終了に失敗: 強制終了できませんでした")
continue
except:
failed += 1
self.log(f"PID {pid} の終了に失敗: プロセスにアクセスできません")
continue
success += 1
except psutil.NoSuchProcess:
self.log(f"PID {pid} は既に終了しています")
except psutil.AccessDenied:
failed += 1
self.log(f"PID {pid} の終了に失敗: アクセス権限がありません")
except Exception as e:
failed += 1
self.log(f"PID {pid} の終了に失敗: {str(e)}")
if success > 0:
self.log(f"プロセス {name} の {success} 個のインスタンスを終了したのじゃ!")
if failed > 0:
self.log(f"プロセス {name} の {failed} 個のインスタンスの終了に失敗したのじゃ...")
def log(self, message):
self.log_area.insert(tk.END, message + "\n")
self.log_area.see(tk.END)
def clear_search(self):
self.search_var.set("")
def filter_processes(self, *args):
search_text = self.search_var.get().lower()
for frame, name in self.process_frames:
if search_text in name.lower():
frame.grid()
else:
frame.grid_remove()
def search_on_google(self, process):
"""Googleでプロセス名を検索"""
import webbrowser
query = f"{process.name()} exe file"
webbrowser.open(f"https://www.google.com/search?q={query}")
def show_process_details(self, process):
"""プロセスの詳細情報を表示する"""
try:
details = self.get_process_details(process)
detail_window = tk.Toplevel(self.root)
detail_window.title(f"プロセス詳細: {process.name()}")
# 詳細情報を表示するテキストボックス
detail_text = scrolledtext.ScrolledText(detail_window, width=80, height=20)
detail_text.pack(padx=10, pady=10)
# 詳細情報を挿入
detail_text.insert(tk.END, details)
detail_text.config(state=tk.DISABLED) # 編集不可にする
# 閉じるボタン
close_btn = ttk.Button(detail_window, text="閉じる", command=detail_window.destroy)
close_btn.pack(pady=(0, 10))
except Exception as e:
self.log(f"詳細情報の取得に失敗: {str(e)}")
def get_process_details(self, process):
"""プロセスの詳細情報を取得する"""
try:
details = f"プロセス名: {process.name()}\n"
details += f"PID: {process.pid}\n"
details += f"実行ファイル: {process.exe()}\n"
details += f"起動時間: {process.create_time()}\n"
details += f"CPU使用率: {process.cpu_percent()}%\n"
details += f"メモリ使用量: {process.memory_info().rss / 1024 / 1024:.2f} MB\n"
details += f"スレッド数: {process.num_threads()}\n"
details += f"状態: {process.status()}\n"
details += f"ユーザー: {process.username()}\n"
details += f"親プロセスID: {process.ppid()}\n"
return details
except Exception as e:
return f"詳細情報の取得に失敗しました:\n{str(e)}"
def load_processes(self):
# プロセス情報を収集してグループ化
processes = []
for process in psutil.process_iter(['pid', 'name', 'exe']):
if process.name() not in self.exclude_processes:
try:
processes.append(process)
except:
continue
# プロセス名でソート(大文字小文字を区別しない)
processes.sort(key=lambda x: x.name().lower())
# プロセス名でグループ化
grouped_processes = {}
for process in processes:
name = process.name()
if name not in grouped_processes:
grouped_processes[name] = []
grouped_processes[name].append(process.pid)
# グリッドにプロセスを配置
row = 0
col = 0
max_cols = 7
for name, pids in grouped_processes.items():
try:
frame = ttk.Frame(self.process_frame)
frame.grid(row=row, column=col, padx=5, pady=5, sticky='nsew')
process = psutil.Process(pids[0])
icon = self.get_process_icon(process)
if not icon:
icon = tk.PhotoImage(width=32, height=32)
instances_text = f"({len(pids)}個)" if len(pids) > 1 else ""
btn = ttk.Button(frame,
text=f"{name}\n{instances_text}",
image=icon,
compound='top')
btn.image = icon
btn.pack(padx=2, pady=2)
# 右クリックメニューを作成
menu = tk.Menu(btn, tearoff=0)
menu.add_command(label="詳細情報", command=lambda p=process: self.show_process_details(p)) # 詳細情報を追加
menu.add_command(label="Googleで検索", command=lambda p=process: self.search_on_google(p))
menu.add_command(label="強制終了", command=lambda n=name, p=pids: self.kill_process(n, p))
# 右クリックでメニューを表示
btn.bind("<Button-3>", lambda e, m=menu: m.post(e.x_root, e.y_root))
# 左クリックで強制終了
btn.bind("<Button-1>", lambda e, n=name, p=pids: self.kill_process(n, p))
# フレームとプロセス名を保存(検索用)
self.process_frames.append((frame, name))
col += 1
if col >= max_cols:
col = 0
row += 1
except:
continue
def _on_mousewheel(self, event):
self.canvas.yview_scroll(int(-1*(event.delta/120)), "units")
def _on_frame_configure(self, event=None):
self.canvas.configure(scrollregion=self.canvas.bbox("all"))
def _on_canvas_configure(self, event):
width = event.width
self.canvas.itemconfig(self.canvas_window, width=width)
def main():
# パッケージチェックのみ実行(実際のインストールはユーザー確認後)
install_required_packages()
root = tk.Tk()
app = TaskKillWizard(root)
root.mainloop()
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment