Last active
February 5, 2025 13:01
-
-
Save roflsunriz/771e8881c4b7a7ff4119a6d7cfdf6d87 to your computer and use it in GitHub Desktop.
taskkill_wizard_gui : タスクキルをGUIで出来るウィザード
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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("910x600") # ウィンドウの初期サイズを設定 | |
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" | |
] | |
# メインフレームを2段に分割 | |
self.main_frame = ttk.Frame(root) | |
self.main_frame.pack(expand=True, fill='both', padx=5, pady=5) | |
# 上段:プロセスグリッド用フレーム | |
self.grid_container = ttk.Frame(self.main_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.log_area = scrolledtext.ScrolledText(self.main_frame, height=8) | |
self.log_area.pack(fill='x', padx=5, pady=5) | |
# 検索フレームを追加(グリッドの上部に) | |
self.search_frame = ttk.Frame(self.main_frame) | |
self.search_frame.pack(fill='x', padx=5, pady=5, before=self.grid_container) | |
# 検索エントリー | |
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.process_frames = [] | |
self.load_processes() | |
# スクロール関連のバインドを追加 | |
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) | |
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 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 = 5 | |
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', | |
command=lambda n=name, p=pids: self.kill_process(n, p)) | |
btn.image = icon | |
btn.pack(padx=2, pady=2) | |
# フレームとプロセス名を保存(検索用) | |
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