import
os
import
collections
import
tkinter as tk
from
tkinter
import
filedialog, scrolledtext, messagebox
from
tkinter
import
ttk
def
find_duplicate_filenames(directory):
file_paths
=
collections.defaultdict(
list
)
try
:
for
root, _, files
in
os.walk(directory):
for
filename
in
files:
full_path
=
os.path.join(root, filename)
file_paths[filename.lower()].append(full_path)
except
Exception as e:
messagebox.showerror(
"错误"
, f
"扫描过程中发生错误: {e}"
)
return
None
duplicates_data
=
{filename: paths
for
filename, paths
in
file_paths.items()
if
len
(paths) >
1
}
duplicates_final
=
collections.defaultdict(
list
)
original_filenames
=
{}
for
root, _, files
in
os.walk(directory):
for
filename
in
files:
lower_filename
=
filename.lower()
if
lower_filename
in
duplicates_data:
if
lower_filename
not
in
original_filenames:
original_filenames[lower_filename]
=
filename
full_path
=
os.path.join(root, filename)
if
full_path
in
duplicates_data[lower_filename]:
duplicates_final[original_filenames[lower_filename]].append(full_path)
duplicates_final
=
{filename: paths
for
filename, paths
in
duplicates_final.items()
if
len
(paths) >
1
}
return
duplicates_final
def
browse_directory():
directory
=
filedialog.askdirectory()
if
directory:
entry_path.delete(
0
, tk.END)
entry_path.insert(
0
, directory)
checkbox_vars
=
{}
checkbox_widgets
=
{}
selection_mode
=
"delete"
def
toggle_selection_mode():
global
selection_mode
if
selection_mode
=
=
"delete"
:
selection_mode
=
"keep"
btn_toggle_mode.config(text
=
"当前模式:保留所选(点击切换)"
)
else
:
selection_mode
=
"delete"
btn_toggle_mode.config(text
=
"当前模式:删除所选(点击切换)"
)
apply_selection_to_checkboxes()
def
apply_selection_to_checkboxes():
paths_by_filename
=
collections.defaultdict(
list
)
for
path
in
checkbox_vars.keys():
filename
=
os.path.basename(path)
paths_by_filename[filename].append(path)
for
filename, paths
in
paths_by_filename.items():
if
len
(paths) <
=
1
:
continue
if
selection_mode
=
=
"delete"
:
for
i, path
in
enumerate
(paths):
checkbox_vars[path].
set
(i >
0
)
else
:
for
i, path
in
enumerate
(paths):
checkbox_vars[path].
set
(i
=
=
0
)
def
start_scan():
global
checkbox_vars, checkbox_widgets
target_directory
=
entry_path.get()
if
not
target_directory
or
target_directory
=
=
"(当前脚本目录)"
:
target_directory
=
os.path.dirname(os.path.abspath(__file__))
entry_path.delete(
0
, tk.END)
entry_path.insert(
0
, target_directory)
if
not
os.path.isdir(target_directory):
messagebox.showerror(
"错误"
, f
"指定的路径 '{target_directory}' 不是一个有效的文件夹。"
)
return
for
widget
in
result_frame.winfo_children():
widget.destroy()
checkbox_vars.clear()
checkbox_widgets.clear()
btn_delete.pack_forget()
btn_toggle_mode.pack_forget()
status_label.config(text
=
f
"开始扫描文件夹: {target_directory}\n请稍候..."
)
app.update_idletasks()
duplicates
=
find_duplicate_filenames(target_directory)
status_label.config(text
=
"扫描完成。"
)
if
duplicates
is
None
:
return
if
duplicates:
row_num
=
0
for
filename, paths
in
duplicates.items():
lbl_filename
=
ttk.Label(result_frame, text
=
f
"文件名: {filename}"
, style
=
'Header.TLabel'
)
lbl_filename.grid(row
=
row_num, column
=
0
, columnspan
=
2
, sticky
=
'w'
, padx
=
5
, pady
=
(
8
,
2
))
row_num
+
=
1
for
path
in
paths:
var
=
tk.BooleanVar()
path_frame
=
ttk.Frame(result_frame)
path_frame.grid(row
=
row_num, column
=
0
, columnspan
=
2
, sticky
=
'w'
, padx
=
20
, pady
=
1
)
cb
=
ttk.Checkbutton(path_frame, text
=
path, variable
=
var, style
=
'TCheckbutton'
)
cb.pack(side
=
tk.LEFT, anchor
=
'w'
)
checkbox_vars[path]
=
var
checkbox_widgets[path]
=
cb
row_num
+
=
1
apply_selection_to_checkboxes()
btn_toggle_mode.config(text
=
"当前模式:删除所选(点击切换)"
if
selection_mode
=
=
"delete"
else
"当前模式:保留所选(点击切换)"
)
btn_toggle_mode.pack(side
=
tk.BOTTOM, pady
=
(
0
,
5
))
btn_delete.pack(side
=
tk.BOTTOM, pady
=
(
5
,
10
))
else
:
lbl_no_duplicates
=
ttk.Label(result_frame, text
=
"未找到重复的文件名。"
)
lbl_no_duplicates.grid(row
=
0
, column
=
0
, padx
=
5
, pady
=
5
)
def
delete_selected_files():
selected_files
=
[path
for
path, var
in
checkbox_vars.items()
if
var.get()]
if
not
selected_files:
messagebox.showinfo(
"提示"
,
"请先选择要处理的文件。"
)
return
if
selection_mode
=
=
"delete"
:
confirm_message
=
f
"确定要删除选中的 {len(selected_files)} 个文件吗?\n此操作不可恢复!"
else
:
files_to_delete
=
[path
for
path, var
in
checkbox_vars.items()
if
not
var.get()]
confirm_message
=
f
"确定要保留选中的 {len(selected_files)} 个文件,删除其余 {len(files_to_delete)} 个文件吗?\n此操作不可恢复!"
confirm
=
messagebox.askyesno(
"确认操作"
, confirm_message)
if
confirm:
if
selection_mode
=
=
"keep"
:
selected_files
=
[path
for
path, var
in
checkbox_vars.items()
if
not
var.get()]
deleted_count
=
0
failed_count
=
0
errors
=
[]
remaining_duplicates
=
collections.defaultdict(
list
)
all_paths_after_delete
=
set
(checkbox_vars.keys())
for
file_path
in
selected_files:
try
:
os.remove(file_path)
deleted_count
+
=
1
checkbox_widgets[file_path].destroy()
del
checkbox_widgets[file_path]
del
checkbox_vars[file_path]
all_paths_after_delete.remove(file_path)
except
Exception as e:
failed_count
+
=
1
errors.append(f
"删除 {file_path} 失败: {e}"
)
remaining_duplicates[os.path.basename(file_path)].append(file_path)
for
path
in
list
(all_paths_after_delete):
if
path
in
checkbox_vars:
remaining_duplicates[os.path.basename(path)].append(path)
for
widget
in
result_frame.winfo_children():
widget.destroy()
if
remaining_duplicates:
row_num
=
0
final_remaining
=
{name: paths
for
name, paths
in
remaining_duplicates.items()
if
len
(paths) >
1
}
if
final_remaining:
for
filename, paths
in
final_remaining.items():
lbl_filename
=
ttk.Label(result_frame, text
=
f
"文件名: {filename}"
, style
=
'Header.TLabel'
)
lbl_filename.grid(row
=
row_num, column
=
0
, columnspan
=
2
, sticky
=
'w'
, padx
=
5
, pady
=
(
8
,
2
))
row_num
+
=
1
for
path
in
paths:
var
=
tk.BooleanVar()
path_frame
=
ttk.Frame(result_frame)
path_frame.grid(row
=
row_num, column
=
0
, columnspan
=
2
, sticky
=
'w'
, padx
=
20
, pady
=
1
)
cb
=
ttk.Checkbutton(path_frame, text
=
path, variable
=
var, style
=
'TCheckbutton'
)
cb.pack(side
=
tk.LEFT, anchor
=
'w'
)
checkbox_vars[path]
=
var
checkbox_widgets[path]
=
cb
row_num
+
=
1
else
:
lbl_no_duplicates
=
ttk.Label(result_frame, text
=
"所有选中的重复文件已被处理或不再重复。"
)
lbl_no_duplicates.grid(row
=
0
, column
=
0
, padx
=
5
, pady
=
5
)
btn_delete.pack_forget()
else
:
lbl_no_duplicates
=
ttk.Label(result_frame, text
=
"所有选中的重复文件已被处理。"
)
lbl_no_duplicates.grid(row
=
0
, column
=
0
, padx
=
5
, pady
=
5
)
btn_delete.pack_forget()
result_message
=
f
"删除完成:成功 {deleted_count} 个"
if
failed_count >
0
:
result_message
+
=
f
",失败 {failed_count} 个。\n失败详情:\n"
+
"\n"
.join(errors)
messagebox.showerror(
"删除失败"
, result_message)
else
:
messagebox.showinfo(
"删除成功"
, result_message)
status_label.config(text
=
"删除操作完成。"
)
app
=
tk.Tk()
app.title(
"查找并删除重复文件名工具"
)
app.geometry(
"700x550"
)
app.configure(bg
=
'#f0f0f0'
) # 设置窗口背景色
style
=
ttk.Style(app)
style.theme_use(
'clam'
)
style.configure(
'TFrame'
, background
=
'#f0f0f0'
)
style.configure(
'TLabel'
, background
=
'#f0f0f0'
, font
=
(
'Microsoft YaHei UI'
,
10
))
style.configure(
'TButton'
, font
=
(
'Microsoft YaHei UI'
,
10
), padding
=
5
)
style.configure(
'TEntry'
, font
=
(
'Microsoft YaHei UI'
,
10
), padding
=
5
)
style.configure(
'Header.TLabel'
, font
=
(
'Microsoft YaHei UI'
,
11
,
'bold'
))
style.configure(
'TCheckbutton'
, background
=
'#f0f0f0'
, font
=
(
'Microsoft YaHei UI'
,
9
))
style.configure(
'Horizontal.TScrollbar'
, gripcount
=
0
, background
=
'#c1c1c1'
, troughcolor
=
'#f0f0f0'
, borderwidth
=
1
, arrowsize
=
13
)
frame_input
=
ttk.Frame(app, padding
=
"10 10 10 5"
)
frame_input.pack(fill
=
tk.X)
lbl_path
=
ttk.Label(frame_input, text
=
"扫描文件夹:"
)
lbl_path.pack(side
=
tk.LEFT, padx
=
(
0
,
5
))
entry_path
=
ttk.Entry(frame_input)
entry_path.pack(side
=
tk.LEFT, expand
=
True
, fill
=
tk.X)
entry_path.insert(
0
,
"(当前脚本目录)"
)
entry_path.config(foreground
=
'grey'
)
def
on_entry_click(event):
if
entry_path.get()
=
=
"(当前脚本目录)"
:
entry_path.delete(
0
,
"end"
)
entry_path.insert(
0
, '')
entry_path.config(foreground
=
'black'
)
def
on_focusout(event):
if
entry_path.get()
=
=
'':
entry_path.insert(
0
,
"(当前脚本目录)"
)
entry_path.config(foreground
=
'grey'
)
entry_path.bind(
'<FocusIn>'
, on_entry_click)
entry_path.bind(
'<FocusOut>'
, on_focusout)
btn_browse
=
ttk.Button(frame_input, text
=
"浏览..."
, command
=
browse_directory, style
=
'TButton'
)
btn_browse.pack(side
=
tk.LEFT, padx
=
(
5
,
0
))
btn_scan
=
ttk.Button(app, text
=
"开始扫描"
, command
=
start_scan, style
=
'TButton'
)
btn_scan.pack(pady
=
5
)
status_label
=
ttk.Label(app, text
=
"请选择要扫描的文件夹。\n提示:按住Shift键+鼠标滚轮可以左右滚动查看完整路径。"
, wraplength
=
680
, justify
=
tk.LEFT)
status_label.pack(pady
=
(
0
,
5
), padx
=
10
, fill
=
tk.X)
btn_toggle_mode
=
ttk.Button(app, text
=
"当前模式:删除所选(点击切换)"
, command
=
toggle_selection_mode, style
=
'TButton'
)
scroll_container
=
ttk.Frame(app)
scroll_container.pack(fill
=
"both"
, expand
=
True
, padx
=
10
, pady
=
(
0
,
5
))
result_canvas
=
tk.Canvas(scroll_container, borderwidth
=
0
, background
=
"#ffffff"
)
result_frame
=
ttk.Frame(result_canvas, style
=
'TFrame'
)
vsb
=
ttk.Scrollbar(scroll_container, orient
=
"vertical"
, command
=
result_canvas.yview)
hsb
=
ttk.Scrollbar(scroll_container, orient
=
"horizontal"
, command
=
result_canvas.xview, style
=
"Horizontal.TScrollbar"
)
result_canvas.configure(yscrollcommand
=
vsb.
set
, xscrollcommand
=
hsb.
set
)
vsb.pack(side
=
"right"
, fill
=
"y"
)
hsb.pack(side
=
"bottom"
, fill
=
"x"
)
result_canvas.pack(side
=
"left"
, fill
=
"both"
, expand
=
True
)
result_canvas.create_window((
4
,
4
), window
=
result_frame, anchor
=
"nw"
, tags
=
"result_frame"
)
import
platform
system
=
platform.system().lower()
def
_on_mousewheel(event):
scroll_amount
=
int
(
-
1
*
(event.delta
/
120
))
result_canvas.yview_scroll(scroll_amount,
"units"
)
if
event.state &
0x1
:
result_canvas.xview_scroll(scroll_amount,
"units"
)
def
_on_mousewheel_linux(event):
if
event.num
=
=
4
:
if
event.state &
0x1
:
result_canvas.xview_scroll(
-
1
,
"units"
)
else
:
result_canvas.yview_scroll(
-
1
,
"units"
)
elif
event.num
=
=
5
:
if
event.state &
0x1
:
result_canvas.xview_scroll(
1
,
"units"
)
else
:
result_canvas.yview_scroll(
1
,
"units"
)
if
system
=
=
'darwin'
:
def
_on_mousewheel_macos(event):
if
event.state &
0x1
:
result_canvas.xview_scroll(
int
(
-
1
*
event.delta),
"units"
)
else
:
result_canvas.yview_scroll(
int
(
-
1
*
event.delta),
"units"
)
def
_bind_to_mousewheel(event
=
None
):
if
system
=
=
'windows'
:
app.bind_all(
"<MouseWheel>"
, _on_mousewheel)
elif
system
=
=
'darwin'
:
app.bind_all(
"<MouseWheel>"
, _on_mousewheel_macos)
else
:
app.bind_all(
"<Button-4>"
, _on_mousewheel_linux)
app.bind_all(
"<Button-5>"
, _on_mousewheel_linux)
def
_unbind_from_mousewheel(event
=
None
):
if
system
=
=
'windows'
:
app.unbind_all(
"<MouseWheel>"
)
elif
system
=
=
'darwin'
:
app.unbind_all(
"<MouseWheel>"
)
else
:
app.unbind_all(
"<Button-4>"
)
app.unbind_all(
"<Button-5>"
)
_bind_to_mousewheel()
def
_configure_frame(event):
width
=
result_frame.winfo_reqwidth()
height
=
result_frame.winfo_reqheight()
result_canvas.configure(scrollregion
=
(
0
,
0
, width, height))
result_frame.update_idletasks()
result_canvas.config(width
=
min
(width, app.winfo_width()
-
30
))
result_frame.bind(
"<Configure>"
, _configure_frame)
btn_delete
=
ttk.Button(app, text
=
"删除选中文件"
, command
=
delete_selected_files, style
=
'TButton'
)
if
__name__
=
=
"__main__"
:
app.mainloop()