Enter a path or click “Browse” to select a folderClick “Scan” to start the processWatch the progress bar as it scansView the results with creation and modification datesSelect multiple folders (Ctrl+click or Shift+click)Delete using either the “Delete Selected” button or right-click context menu
Python
import os
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import shutil
from pathlib import Path
import win32com.client
import time
class EmptyFolderFinder:
def __init__(self, root):
self.root = root
self.root.title("Empty Folder Finder")
self.root.geometry("900x600")
# Style configuration
self.style = ttk.Style()
self.style.configure("Treeview", rowheight=25)
# Main frame
self.main_frame = ttk.Frame(root, padding="10")
self.main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
# Path selection frame
self.path_frame = ttk.LabelFrame(self.main_frame, text="Search Location", padding="5")
self.path_frame.grid(row=0, column=0, columnspan=3, pady=5, sticky=(tk.W, tk.E))
# Path entry
ttk.Label(self.path_frame, text="Path:").grid(row=0, column=0, padx=5)
self.path_var = tk.StringVar(value=str(Path.home()))
self.path_entry = ttk.Entry(self.path_frame, textvariable=self.path_var, width=60)
self.path_entry.grid(row=0, column=1, padx=5)
# Browse button
self.browse_btn = ttk.Button(self.path_frame, text="Browse", command=self.browse_path)
self.browse_btn.grid(row=0, column=2, padx=5)
# Buttons frame
self.btn_frame = ttk.Frame(self.main_frame)
self.btn_frame.grid(row=1, column=0, columnspan=3, pady=5)
self.scan_button = ttk.Button(self.btn_frame, text="Scan", command=self.scan_folders)
self.scan_button.grid(row=0, column=0, padx=5)
self.delete_selected_btn = ttk.Button(self.btn_frame, text="Delete Selected", command=self.delete_selected_folders)
self.delete_selected_btn.grid(row=0, column=1, padx=5)
# Progress bar
self.progress = ttk.Progressbar(self.main_frame, length=400, mode='determinate')
self.progress.grid(row=2, column=0, columnspan=3, pady=5, sticky=(tk.W, tk.E))
# Treeview for displaying folders
self.tree = ttk.Treeview(self.main_frame,
columns=("Path", "Status", "Created", "Modified"),
selectmode="extended")
self.tree.heading("#0", text="Select")
self.tree.heading("Path", text="Folder Path")
self.tree.heading("Status", text="Status")
self.tree.heading("Created", text="Created")
self.tree.heading("Modified", text="Last Modified")
self.tree.column("#0", width=50)
self.tree.column("Path", width=500)
self.tree.column("Status", width=100)
self.tree.column("Created", width=120)
self.tree.column("Modified", width=120)
self.tree.grid(row=3, column=0, columnspan=3, pady=10, sticky=(tk.W, tk.E, tk.N, tk.S))
# Scrollbar
scrollbar = ttk.Scrollbar(self.main_frame, orient=tk.VERTICAL, command=self.tree.yview)
scrollbar.grid(row=3, column=3, sticky=(tk.N, tk.S))
self.tree.configure(yscrollcommand=scrollbar.set)
# Context menu
self.context_menu = tk.Menu(self.root, tearoff=0)
self.context_menu.add_command(label="Delete Selected Folders?", command=self.delete_selected_folders)
self.tree.bind("<Button-3>", self.show_context_menu)
# Status bar
self.status_var = tk.StringVar(value="Ready")
self.status_bar = ttk.Label(self.main_frame, textvariable=self.status_var, relief=tk.SUNKEN)
self.status_bar.grid(row=4, column=0, columnspan=3, pady=5, sticky=(tk.W, tk.E))
# Configure grid weights
self.root.columnconfigure(0, weight=1)
self.root.rowconfigure(0, weight=1)
self.main_frame.columnconfigure(0, weight=1)
self.main_frame.rowconfigure(3, weight=1)
def browse_path(self):
"""Open folder browser dialog"""
selected_path = filedialog.askdirectory(initialdir=self.path_var.get())
if selected_path:
self.path_var.set(selected_path)
def scan_folders(self):
"""Scan for empty folders with progress bar"""
search_path = self.path_var.get()
if not os.path.exists(search_path):
messagebox.showerror("Error", "Invalid path specified!")
return
self.tree.delete(*self.tree.get_children())
self.status_var.set("Counting directories...")
self.progress['value'] = 0
self.root.update_idletasks()
# First pass: count total directories for progress
total_dirs = 0
try:
for root, dirs, files in os.walk(search_path, topdown=False):
total_dirs += len(dirs)
except Exception as e:
if "[WinError 3]" in str(e): # Handle path not found
pass
else:
messagebox.showerror("Error", f"Counting failed: {str(e)}")
self.status_var.set("Error occurred")
return
if total_dirs == 0:
self.status_var.set("No directories found")
self.progress['value'] = 100
return
self.progress['maximum'] = total_dirs
self.status_var.set("Scanning folders...")
empty_folders = []
processed_dirs = 0
try:
for root, dirs, files in os.walk(search_path, topdown=False):
for dir_name in dirs:
full_path = os.path.join(root, dir_name)
try:
if not any(Path(full_path).rglob('*')):
try:
created = time.ctime(os.path.getctime(full_path))
modified = time.ctime(os.path.getmtime(full_path))
empty_folders.append((full_path, created, modified))
except WindowsError as e:
if e.winerror == 3: # Skip WinError 3
continue
raise
except PermissionError:
continue
except Exception as e:
if "[WinError 3]" in str(e): # Skip path not found errors
continue
raise
processed_dirs += 1
self.progress['value'] = (processed_dirs / total_dirs) * 100
self.root.update_idletasks() # Force UI update
# Populate treeview
for folder, created, modified in empty_folders:
self.tree.insert("", "end", values=(folder, "Empty", created, modified))
self.status_var.set(f"Found {len(empty_folders)} empty folders")
self.progress['value'] = 100
except Exception as e:
messagebox.showerror("Error", f"Scan failed: {str(e)}")
self.status_var.set("Error occurred")
self.progress['value'] = 0
def show_context_menu(self, event):
"""Show right-click context menu"""
self.context_menu.post(event.x_root, event.y_root)
def delete_selected_folders(self):
"""Delete all selected folders after confirmation"""
selected_items = self.tree.selection()
if not selected_items:
messagebox.showinfo("Info", "Please select folders to delete")
return
folder_paths = [self.tree.item(item)['values'][0] for item in selected_items]
folder_list = "\n".join(folder_paths[:5]) + ("\n..." if len(folder_paths) > 5 else "")
if messagebox.askyesno("Confirm Delete",
f"Are you sure you want to delete {len(selected_items)} folder(s)?\n\n{folder_list}"):
deleted_count = 0
failed = []
for item in selected_items:
folder_path = self.tree.item(item)['values'][0]
try:
shutil.rmtree(folder_path)
self.tree.delete(item)
deleted_count += 1
# Refresh Windows Explorer
shell = win32com.client.Dispatch("Shell.Application")
shell.Namespace(os.path.dirname(folder_path)).Self.InvokeVerb("Refresh")
except Exception as e:
failed.append(f"{folder_path}: {str(e)}")
status = f"Deleted {deleted_count} folder(s)"
if failed:
status += f"\nFailed to delete {len(failed)} folder(s):\n" + "\n".join(failed[:5])
self.status_var.set(status)
if failed:
messagebox.showwarning("Deletion Report", status)
def main():
root = tk.Tk()
app = EmptyFolderFinder(root)
root.mainloop()
if __name__ == "__main__":
main()