import
tkinter as tk
from
tkinter
import
filedialog, ttk, messagebox
from
PIL
import
Image, ImageTk
import
os
import
threading
import
json
from
tkinter.colorchooser
import
askcolor
import
sys
SUPPORTED_EXTENSIONS
=
(
'.png'
,
'.jpg'
,
'.jpeg'
,
'.gif'
,
'.bmp'
,
'.tiff'
,
'.webp'
)
CONFIG_FILE
=
os.path.join(os.path.dirname(os.path.abspath(__file__)),
'ico_converter_config.json'
)
DEFAULT_CONFIG
=
{
'last_directory'
: '',
'output_directory'
: '',
'theme'
:
'light'
,
'icon_sizes'
: [
16
,
32
,
48
,
64
,
128
,
256
]
}
THEMES
=
{
'light'
: {
'bg'
:
'#f0f0f0'
,
'fg'
:
'#000000'
,
'button_bg'
:
'#e0e0e0'
,
'button_fg'
:
'#000000'
,
'entry_bg'
:
'#ffffff'
,
'entry_fg'
:
'#000000'
,
'highlight_bg'
:
'#0078d7'
,
'highlight_fg'
:
'#ffffff'
},
'dark'
: {
'bg'
:
'#2d2d2d'
,
'fg'
:
'#ffffff'
,
'button_bg'
:
'#444444'
,
'button_fg'
:
'#ffffff'
,
'entry_bg'
:
'#3d3d3d'
,
'entry_fg'
:
'#ffffff'
,
'highlight_bg'
:
'#0078d7'
,
'highlight_fg'
:
'#ffffff'
}
}
class
DragDropListbox(tk.Listbox):
def
__init__(
self
, master,
*
*
kw):
super
().__init__(master,
*
*
kw)
self
.bind(
'<Button-1>'
,
self
.select_item)
self
.bind(
'<B1-Motion>'
,
self
.move_item)
self
.curIndex
=
None
def
select_item(
self
, event):
self
.curIndex
=
self
.nearest(event.y)
def
move_item(
self
, event):
new_index
=
self
.nearest(event.y)
if
new_index !
=
self
.curIndex
and
0
<
=
new_index <
self
.size():
item_text
=
self
.get(
self
.curIndex)
self
.delete(
self
.curIndex)
self
.insert(new_index, item_text)
self
.curIndex
=
new_index
self
.selection_set(
self
.curIndex)
class
ImageDropTarget:
def
__init__(
self
, widget, callback):
self
.widget
=
widget
self
.callback
=
callback
try
:
self
.widget.drop_target_register(
'DND_Files'
)
self
.widget.dnd_bind(
'<<Drop>>'
,
self
.on_drop)
except
:
pass
def
on_drop(
self
, event):
files
=
self
.widget.tk.splitlist(event.data)
valid_files
=
[]
for
file_path
in
files:
if
os.path.isfile(file_path)
and
any
(file_path.lower().endswith(ext)
for
ext
in
SUPPORTED_EXTENSIONS):
valid_files.append(file_path)
if
valid_files:
self
.callback(valid_files)
class
IcoConverterApp:
def
__init__(
self
, root):
self
.root
=
root
self
.root.title(
"增强版图像转ICO工具"
)
self
.root.geometry(
"800x600"
)
self
.root.minsize(
800
,
600
)
self
.config
=
self
.load_config()
self
.current_theme
=
self
.config[
'theme'
]
self
.apply_theme()
self
.main_frame
=
ttk.Frame(root)
self
.main_frame.pack(fill
=
tk.BOTH, expand
=
True
, padx
=
10
, pady
=
10
)
self
.left_frame
=
ttk.Frame(
self
.main_frame)
self
.left_frame.pack(side
=
tk.LEFT, fill
=
tk.BOTH, expand
=
True
, padx
=
(
0
,
5
))
self
.right_frame
=
ttk.Frame(
self
.main_frame)
self
.right_frame.pack(side
=
tk.RIGHT, fill
=
tk.BOTH, expand
=
True
, padx
=
(
5
,
0
))
self
.create_file_selection_area()
self
.create_preview_area()
self
.create_settings_area()
self
.create_conversion_controls()
self
.create_status_bar()
try
:
self
.root.tk.
eval
(
'package require tkdnd'
)
from
tkinterdnd2
import
TkinterDnD, DND_FILES
self
.setup_drag_drop()
except
:
self
.status_var.
set
(
"提示:安装 tkinterdnd2 包可启用拖放功能"
)
self
.root.protocol(
"WM_DELETE_WINDOW"
,
self
.on_close)
def
load_config(
self
):
try
:
if
os.path.exists(CONFIG_FILE):
with
open
(CONFIG_FILE,
'r'
, encoding
=
'utf-8'
) as f:
config
=
json.load(f)
for
key, value
in
DEFAULT_CONFIG.items():
if
key
not
in
config:
config[key]
=
value
return
config
except
Exception as e:
print
(f
"加载配置文件失败: {e}"
)
return
DEFAULT_CONFIG.copy()
def
save_config(
self
):
try
:
with
open
(CONFIG_FILE,
'w'
, encoding
=
'utf-8'
) as f:
json.dump(
self
.config, f, ensure_ascii
=
False
, indent
=
2
)
except
Exception as e:
print
(f
"保存配置文件失败: {e}"
)
def
apply_theme(
self
):
theme
=
THEMES[
self
.current_theme]
self
.root.configure(bg
=
theme[
'bg'
])
style
=
ttk.Style()
style.theme_use(
'clam'
)
style.configure(
'TFrame'
, background
=
theme[
'bg'
])
style.configure(
'TLabel'
, background
=
theme[
'bg'
], foreground
=
theme[
'fg'
])
style.configure(
'TButton'
, background
=
theme[
'button_bg'
], foreground
=
theme[
'button_fg'
])
style.configure(
'TCheckbutton'
, background
=
theme[
'bg'
], foreground
=
theme[
'fg'
])
style.configure(
'TRadiobutton'
, background
=
theme[
'bg'
], foreground
=
theme[
'fg'
])
style.configure(
'TEntry'
, fieldbackground
=
theme[
'entry_bg'
], foreground
=
theme[
'entry_fg'
])
style.configure(
'TCombobox'
, fieldbackground
=
theme[
'entry_bg'
], foreground
=
theme[
'entry_fg'
])
style.configure(
'Horizontal.TProgressbar'
, background
=
theme[
'highlight_bg'
])
self
.config[
'theme'
]
=
self
.current_theme
self
.save_config()
def
create_file_selection_area(
self
):
file_frame
=
ttk.LabelFrame(
self
.left_frame, text
=
"文件选择"
)
file_frame.pack(fill
=
tk.BOTH, expand
=
True
, pady
=
(
0
,
5
))
list_frame
=
ttk.Frame(file_frame)
list_frame.pack(fill
=
tk.BOTH, expand
=
True
, padx
=
5
, pady
=
5
)
self
.file_listbox
=
DragDropListbox(
list_frame,
selectmode
=
tk.EXTENDED,
bg
=
THEMES[
self
.current_theme][
'entry_bg'
],
fg
=
THEMES[
self
.current_theme][
'entry_fg'
],
selectbackground
=
THEMES[
self
.current_theme][
'highlight_bg'
],
selectforeground
=
THEMES[
self
.current_theme][
'highlight_fg'
]
)
scrollbar
=
ttk.Scrollbar(list_frame, orient
=
tk.VERTICAL, command
=
self
.file_listbox.yview)
self
.file_listbox.configure(yscrollcommand
=
scrollbar.
set
)
self
.file_listbox.pack(side
=
tk.LEFT, fill
=
tk.BOTH, expand
=
True
)
scrollbar.pack(side
=
tk.RIGHT, fill
=
tk.Y)
button_frame
=
ttk.Frame(file_frame)
button_frame.pack(fill
=
tk.X, padx
=
5
, pady
=
5
)
add_button
=
ttk.Button(button_frame, text
=
"添加文件"
, command
=
self
.add_files)
add_button.pack(side
=
tk.LEFT, padx
=
(
0
,
5
))
add_dir_button
=
ttk.Button(button_frame, text
=
"添加文件夹"
, command
=
self
.add_directory)
add_dir_button.pack(side
=
tk.LEFT, padx
=
(
0
,
5
))
remove_button
=
ttk.Button(button_frame, text
=
"移除选中"
, command
=
self
.remove_selected_files)
remove_button.pack(side
=
tk.LEFT, padx
=
(
0
,
5
))
clear_button
=
ttk.Button(button_frame, text
=
"清空列表"
, command
=
self
.clear_file_list)
clear_button.pack(side
=
tk.LEFT)
self
.file_listbox.bind(
'<Double-1>'
,
self
.preview_selected_image)
def
create_preview_area(
self
):
preview_frame
=
ttk.LabelFrame(
self
.right_frame, text
=
"图像预览"
)
preview_frame.pack(fill
=
tk.BOTH, expand
=
True
, pady
=
(
0
,
5
))
self
.preview_canvas
=
tk.Canvas(
preview_frame,
bg
=
THEMES[
self
.current_theme][
'entry_bg'
],
highlightthickness
=
0
)
self
.preview_canvas.pack(fill
=
tk.BOTH, expand
=
True
, padx
=
5
, pady
=
5
)
self
.preview_info
=
ttk.Label(preview_frame, text
=
"双击左侧列表中的图像进行预览"
)
self
.preview_info.pack(pady
=
(
0
,
5
))
self
.preview_image
=
None
def
create_settings_area(
self
):
settings_frame
=
ttk.LabelFrame(
self
.left_frame, text
=
"转换设置"
)
settings_frame.pack(fill
=
tk.X, pady
=
(
0
,
5
))
size_frame
=
ttk.Frame(settings_frame)
size_frame.pack(fill
=
tk.X, padx
=
5
, pady
=
5
)
ttk.Label(size_frame, text
=
"图标尺寸:"
).pack(side
=
tk.LEFT)
sizes_frame
=
ttk.Frame(size_frame)
sizes_frame.pack(side
=
tk.LEFT, fill
=
tk.X, expand
=
True
, padx
=
(
5
,
0
))
self
.size_vars
=
{}
available_sizes
=
[
16
,
32
,
48
,
64
,
128
,
256
]
for
i, size
in
enumerate
(available_sizes):
var
=
tk.BooleanVar(value
=
size
in
self
.config[
'icon_sizes'
])
self
.size_vars[size]
=
var
cb
=
ttk.Checkbutton(
sizes_frame,
text
=
f
"{size}x{size}"
,
variable
=
var,
command
=
self
.update_size_config
)
row, col
=
divmod
(i,
3
)
cb.grid(row
=
row, column
=
col, sticky
=
tk.W, padx
=
5
)
mode_frame
=
ttk.Frame(settings_frame)
mode_frame.pack(fill
=
tk.X, padx
=
5
, pady
=
5
)
ttk.Label(mode_frame, text
=
"输出模式:"
).pack(side
=
tk.LEFT)
self
.output_mode
=
tk.StringVar(value
=
"combined"
)
ttk.Radiobutton(
mode_frame,
text
=
"合并为单个ICO"
,
variable
=
self
.output_mode,
value
=
"combined"
).pack(side
=
tk.LEFT, padx
=
(
5
,
10
))
ttk.Radiobutton(
mode_frame,
text
=
"分别输出各尺寸"
,
variable
=
self
.output_mode,
value
=
"separate"
).pack(side
=
tk.LEFT)
output_frame
=
ttk.Frame(settings_frame)
output_frame.pack(fill
=
tk.X, padx
=
5
, pady
=
5
)
ttk.Label(output_frame, text
=
"输出目录:"
).pack(side
=
tk.LEFT)
self
.output_var
=
tk.StringVar(value
=
self
.config[
'output_directory'
])
output_entry
=
ttk.Entry(output_frame, textvariable
=
self
.output_var)
output_entry.pack(side
=
tk.LEFT, fill
=
tk.X, expand
=
True
, padx
=
(
5
,
5
))
output_button
=
ttk.Button(output_frame, text
=
"浏览..."
, command
=
self
.select_output_directory)
output_button.pack(side
=
tk.LEFT)
theme_frame
=
ttk.Frame(settings_frame)
theme_frame.pack(fill
=
tk.X, padx
=
5
, pady
=
5
)
ttk.Label(theme_frame, text
=
"界面主题:"
).pack(side
=
tk.LEFT)
self
.theme_var
=
tk.StringVar(value
=
self
.current_theme)
light_rb
=
ttk.Radiobutton(
theme_frame,
text
=
"浅色"
,
variable
=
self
.theme_var,
value
=
"light"
,
command
=
self
.change_theme
)
light_rb.pack(side
=
tk.LEFT, padx
=
(
5
,
10
))
dark_rb
=
ttk.Radiobutton(
theme_frame,
text
=
"深色"
,
variable
=
self
.theme_var,
value
=
"dark"
,
command
=
self
.change_theme
)
dark_rb.pack(side
=
tk.LEFT)
def
create_conversion_controls(
self
):
control_frame
=
ttk.Frame(
self
.right_frame)
control_frame.pack(fill
=
tk.X, pady
=
(
0
,
5
))
self
.convert_button
=
ttk.Button(
control_frame,
text
=
"开始转换"
,
command
=
self
.start_conversion
)
self
.convert_button.pack(side
=
tk.LEFT, fill
=
tk.X, expand
=
True
, padx
=
(
0
,
5
))
self
.cancel_button
=
ttk.Button(
control_frame,
text
=
"取消"
,
state
=
tk.DISABLED,
command
=
self
.cancel_conversion
)
self
.cancel_button.pack(side
=
tk.LEFT, fill
=
tk.X, expand
=
True
)
self
.progress_var
=
tk.DoubleVar()
self
.progress_bar
=
ttk.Progressbar(
self
.right_frame,
orient
=
tk.HORIZONTAL,
length
=
100
,
mode
=
'determinate'
,
variable
=
self
.progress_var
)
self
.progress_bar.pack(fill
=
tk.X, pady
=
(
0
,
5
))
self
.converting
=
False
self
.cancel_requested
=
False
def
create_status_bar(
self
):
self
.status_var
=
tk.StringVar(value
=
"就绪"
)
status_bar
=
ttk.Label(
self
.root,
textvariable
=
self
.status_var,
relief
=
tk.SUNKEN,
anchor
=
tk.W
)
status_bar.pack(side
=
tk.BOTTOM, fill
=
tk.X)
def
setup_drag_drop(
self
):
try
:
from
tkinterdnd2
import
DND_FILES
self
.root.drop_target_register(DND_FILES)
self
.root.dnd_bind(
'<<Drop>>'
,
self
.on_drop_files)
self
.file_listbox.drop_target_register(DND_FILES)
self
.file_listbox.dnd_bind(
'<<Drop>>'
,
self
.on_drop_files)
self
.preview_canvas.drop_target_register(DND_FILES)
self
.preview_canvas.dnd_bind(
'<<Drop>>'
,
self
.on_drop_files)
self
.status_var.
set
(
"拖放功能已启用"
)
except
Exception as e:
print
(f
"设置拖放支持失败: {e}"
)
def
on_drop_files(
self
, event):
files
=
self
.root.tk.splitlist(event.data)
self
.process_dropped_files(files)
def
process_dropped_files(
self
, files):
image_files
=
[]
for
file_path
in
files:
if
os.path.isdir(file_path):
for
root, _, filenames
in
os.walk(file_path):
for
filename
in
filenames:
full_path
=
os.path.join(root, filename)
if
any
(filename.lower().endswith(ext)
for
ext
in
SUPPORTED_EXTENSIONS):
image_files.append(full_path)
elif
os.path.isfile(file_path)
and
any
(file_path.lower().endswith(ext)
for
ext
in
SUPPORTED_EXTENSIONS):
image_files.append(file_path)
if
image_files:
for
file_path
in
image_files:
if
file_path
not
in
self
.get_file_list():
self
.file_listbox.insert(tk.END, file_path)
self
.status_var.
set
(f
"已添加 {len(image_files)} 个文件"
)
if
len
(image_files)
=
=
1
:
self
.preview_image_file(image_files[
0
])
def
add_files(
self
):
initial_dir
=
self
.config[
'last_directory'
]
if
self
.config[
'last_directory'
]
else
os.path.expanduser(
"~"
)
files
=
filedialog.askopenfilenames(
title
=
"选择图像文件"
,
initialdir
=
initial_dir,
filetypes
=
[
(
"所有支持的图像"
,
" "
.join(f
"*{ext}"
for
ext
in
SUPPORTED_EXTENSIONS)),
(
"PNG 文件"
,
"*.png"
),
(
"JPEG 文件"
,
"*.jpg *.jpeg"
),
(
"GIF 文件"
,
"*.gif"
),
(
"BMP 文件"
,
"*.bmp"
),
(
"TIFF 文件"
,
"*.tiff *.tif"
),
(
"WebP 文件"
,
"*.webp"
),
(
"所有文件"
,
"*.*"
)
]
)
if
files:
self
.config[
'last_directory'
]
=
os.path.dirname(files[
0
])
self
.save_config()
for
file_path
in
files:
if
file_path
not
in
self
.get_file_list():
self
.file_listbox.insert(tk.END, file_path)
self
.status_var.
set
(f
"已添加 {len(files)} 个文件"
)
if
len
(files)
=
=
1
:
self
.preview_image_file(files[
0
])
def
add_directory(
self
):
initial_dir
=
self
.config[
'last_directory'
]
if
self
.config[
'last_directory'
]
else
os.path.expanduser(
"~"
)
directory
=
filedialog.askdirectory(
title
=
"选择包含图像的文件夹"
,
initialdir
=
initial_dir
)
if
directory:
self
.config[
'last_directory'
]
=
directory
self
.save_config()
image_files
=
[]
for
root, _, filenames
in
os.walk(directory):
for
filename
in
filenames:
if
any
(filename.lower().endswith(ext)
for
ext
in
SUPPORTED_EXTENSIONS):
full_path
=
os.path.join(root, filename)
image_files.append(full_path)
for
file_path
in
image_files:
if
file_path
not
in
self
.get_file_list():
self
.file_listbox.insert(tk.END, file_path)
self
.status_var.
set
(f
"已从目录添加 {len(image_files)} 个文件"
)
def
remove_selected_files(
self
):
selected_indices
=
self
.file_listbox.curselection()
if
not
selected_indices:
return
for
index
in
sorted
(selected_indices, reverse
=
True
):
self
.file_listbox.delete(index)
self
.status_var.
set
(f
"已移除 {len(selected_indices)} 个文件"
)
self
.clear_preview()
def
clear_file_list(
self
):
if
self
.file_listbox.size() >
0
:
self
.file_listbox.delete(
0
, tk.END)
self
.status_var.
set
(
"已清空文件列表"
)
self
.clear_preview()
def
get_file_list(
self
):
return
[
self
.file_listbox.get(i)
for
i
in
range
(
self
.file_listbox.size())]
def
preview_selected_image(
self
, event
=
None
):
selected_indices
=
self
.file_listbox.curselection()
if
not
selected_indices:
return
file_path
=
self
.file_listbox.get(selected_indices[
0
])
self
.preview_image_file(file_path)
def
preview_image_file(
self
, file_path):
try
:
image
=
Image.
open
(file_path)
width, height
=
image.size
format_name
=
image.
format
mode
=
image.mode
canvas_width
=
self
.preview_canvas.winfo_width()
canvas_height
=
self
.preview_canvas.winfo_height()
if
canvas_width <
=
1
:
canvas_width
=
300
if
canvas_height <
=
1
:
canvas_height
=
300
scale
=
min
(canvas_width
/
width, canvas_height
/
height)
new_width
=
int
(width
*
scale)
new_height
=
int
(height
*
scale)
resized_image
=
image.resize((new_width, new_height), Image.Resampling.LANCZOS)
photo
=
ImageTk.PhotoImage(resized_image)
self
.preview_canvas.delete(
"all"
)
self
.preview_canvas.create_image(
canvas_width
/
/
2
,
canvas_height
/
/
2
,
image
=
photo,
anchor
=
tk.CENTER
)
self
.preview_image
=
photo
file_name
=
os.path.basename(file_path)
info_text
=
f
"{file_name} ({width}x{height}, {format_name}, {mode})"
self
.preview_info.config(text
=
info_text)
self
.status_var.
set
(f
"预览: {file_name}"
)
except
Exception as e:
self
.clear_preview()
self
.preview_info.config(text
=
f
"无法预览: {os.path.basename(file_path)}"
)
self
.status_var.
set
(f
"预览失败: {e}"
)
def
clear_preview(
self
):
self
.preview_canvas.delete(
"all"
)
self
.preview_info.config(text
=
"双击左侧列表中的图像进行预览"
)
self
.preview_image
=
None
def
select_output_directory(
self
):
initial_dir
=
self
.config[
'output_directory'
]
if
self
.config[
'output_directory'
]
else
os.path.expanduser(
"~"
)
directory
=
filedialog.askdirectory(
title
=
"选择输出目录"
,
initialdir
=
initial_dir
)
if
directory:
self
.output_var.
set
(directory)
self
.config[
'output_directory'
]
=
directory
self
.save_config()
self
.status_var.
set
(f
"已设置输出目录: {directory}"
)
def
update_size_config(
self
):
selected_sizes
=
[size
for
size, var
in
self
.size_vars.items()
if
var.get()]
if
not
selected_sizes:
self
.size_vars[
256
].
set
(
True
)
selected_sizes
=
[
256
]
self
.config[
'icon_sizes'
]
=
selected_sizes
self
.save_config()
def
change_theme(
self
):
self
.current_theme
=
self
.theme_var.get()
self
.apply_theme()
self
.file_listbox.config(
bg
=
THEMES[
self
.current_theme][
'entry_bg'
],
fg
=
THEMES[
self
.current_theme][
'entry_fg'
],
selectbackground
=
THEMES[
self
.current_theme][
'highlight_bg'
],
selectforeground
=
THEMES[
self
.current_theme][
'highlight_fg'
]
)
self
.preview_canvas.config(
bg
=
THEMES[
self
.current_theme][
'entry_bg'
]
)
selected_indices
=
self
.file_listbox.curselection()
if
selected_indices:
self
.preview_selected_image()
self
.status_var.
set
(f
"已切换到{self.current_theme}主题"
)
def
start_conversion(
self
):
file_list
=
self
.get_file_list()
if
not
file_list:
messagebox.showinfo(
"提示"
,
"请先添加图像文件"
)
return
selected_sizes
=
[size
for
size, var
in
self
.size_vars.items()
if
var.get()]
if
not
selected_sizes:
messagebox.showinfo(
"提示"
,
"请至少选择一个图标尺寸"
)
return
output_dir
=
self
.output_var.get()
if
not
output_dir:
output_dir
=
os.path.dirname(file_list[
0
])
self
.output_var.
set
(output_dir)
if
not
os.path.exists(output_dir):
try
:
os.makedirs(output_dir)
except
Exception as e:
messagebox.showerror(
"错误"
, f
"无法创建输出目录: {e}"
)
return
self
.convert_button.config(state
=
tk.DISABLED)
self
.cancel_button.config(state
=
tk.NORMAL)
self
.file_listbox.config(state
=
tk.DISABLED)
self
.progress_var.
set
(
0
)
self
.converting
=
True
self
.cancel_requested
=
False
self
.conversion_thread
=
threading.Thread(
target
=
self
.convert_images,
args
=
(file_list, selected_sizes, output_dir)
)
self
.conversion_thread.daemon
=
True
self
.conversion_thread.start()
self
.root.after(
100
,
self
.update_conversion_progress)
def
convert_images(
self
, file_list, sizes, output_dir):
total_files
=
len
(file_list)
processed_files
=
0
successful_files
=
0
failed_files
=
[]
for
file_path
in
file_list:
if
self
.cancel_requested:
break
try
:
file_name
=
os.path.basename(file_path)
self
.update_status(f
"正在转换: {file_name}"
)
if
not
os.path.exists(file_path):
failed_files.append((file_path,
"文件不存在"
))
continue
if
not
any
(file_path.lower().endswith(ext)
for
ext
in
SUPPORTED_EXTENSIONS):
failed_files.append((file_path,
"不支持的文件格式"
))
continue
image
=
Image.
open
(file_path)
file_name_without_ext
=
os.path.splitext(os.path.basename(file_path))[
0
]
images
=
[]
for
size
in
sorted
(sizes, reverse
=
True
):
resized_image
=
image.resize((size, size), Image.Resampling.LANCZOS)
images.append(resized_image)
if
self
.output_mode.get()
=
=
"combined"
:
ico_path
=
os.path.join(output_dir, f
"{file_name_without_ext}.ico"
)
images[
0
].save(
ico_path,
format
=
'ICO'
,
sizes
=
[(size, size)
for
size
in
sorted
(sizes, reverse
=
True
)],
append_images
=
images[
1
:]
if
len
(images) >
1
else
[],
bitmap_format
=
'bmp'
)
else
:
for
size, img
in
zip
(
sorted
(sizes, reverse
=
True
), images):
ico_path
=
os.path.join(output_dir, f
"{file_name_without_ext}_{size}x{size}.ico"
)
img.save(
ico_path,
format
=
'ICO'
,
sizes
=
[(size, size)],
bitmap_format
=
'bmp'
)
successful_files
+
=
1
except
Exception as e:
failed_files.append((file_path,
str
(e)))
processed_files
+
=
1
self
.update_progress(processed_files
/
total_files)
if
self
.cancel_requested:
self
.update_status(
"转换已取消"
)
elif
failed_files:
self
.update_status(f
"转换完成: {successful_files} 成功, {len(failed_files)} 失败"
)
self
.show_conversion_results(successful_files, failed_files, file_list, output_dir)
else
:
self
.update_status(f
"转换完成: 所有 {successful_files} 个文件转换成功"
)
self
.show_conversion_results(successful_files, [], file_list, output_dir)
self
.converting
=
False
def
update_conversion_progress(
self
):
if
self
.converting:
self
.root.after(
100
,
self
.update_conversion_progress)
else
:
self
.convert_button.config(state
=
tk.NORMAL)
self
.cancel_button.config(state
=
tk.DISABLED)
self
.file_listbox.config(state
=
tk.NORMAL)
def
update_progress(
self
, progress):
self
.progress_var.
set
(progress
*
100
)
def
update_status(
self
, status):
self
.status_var.
set
(status)
def
cancel_conversion(
self
):
if
self
.converting:
self
.cancel_requested
=
True
self
.status_var.
set
(
"正在取消转换..."
)
self
.cancel_button.config(state
=
tk.DISABLED)
def
show_conversion_results(
self
, successful_count, failed_files, file_list
=
None
, output_dir
=
None
):
if
not
failed_files:
messagebox.showinfo(
"转换完成"
, f
"所有 {successful_count} 个文件转换成功!"
)
if
successful_count
=
=
1
and
file_list
and
output_dir:
ico_path
=
os.path.join(output_dir, f
"{os.path.splitext(os.path.basename(file_list[0]))[0]}.ico"
)
self
.show_ico_info(ico_path)
return
result_dialog
=
tk.Toplevel(
self
.root)
result_dialog.title(
"转换结果"
)
result_dialog.geometry(
"500x300"
)
result_dialog.transient(
self
.root)
result_dialog.grab_set()
result_dialog.configure(bg
=
THEMES[
self
.current_theme][
'bg'
])
header_label
=
tk.Label(
result_dialog,
text
=
f
"转换完成: {successful_count} 成功, {len(failed_files)} 失败"
,
bg
=
THEMES[
self
.current_theme][
'bg'
],
fg
=
THEMES[
self
.current_theme][
'fg'
],
font
=
("
", 12, "
bold")
)
header_label.pack(pady
=
(
10
,
5
))
frame
=
tk.Frame(
result_dialog,
bg
=
THEMES[
self
.current_theme][
'bg'
]
)
frame.pack(fill
=
tk.BOTH, expand
=
True
, padx
=
10
, pady
=
5
)
scrollbar
=
tk.Scrollbar(frame)
scrollbar.pack(side
=
tk.RIGHT, fill
=
tk.Y)
text
=
tk.Text(
frame,
wrap
=
tk.WORD,
yscrollcommand
=
scrollbar.
set
,
bg
=
THEMES[
self
.current_theme][
'entry_bg'
],
fg
=
THEMES[
self
.current_theme][
'entry_fg'
],
height
=
10
)
text.pack(side
=
tk.LEFT, fill
=
tk.BOTH, expand
=
True
)
scrollbar.config(command
=
text.yview)
for
file_path, error
in
failed_files:
file_name
=
os.path.basename(file_path)
text.insert(tk.END, f
"{file_name}: {error}\n"
)
text.config(state
=
tk.DISABLED)
close_button
=
tk.Button(
result_dialog,
text
=
"关闭"
,
command
=
result_dialog.destroy,
bg
=
THEMES[
self
.current_theme][
'button_bg'
],
fg
=
THEMES[
self
.current_theme][
'button_fg'
]
)
close_button.pack(pady
=
(
5
,
10
))
def
on_close(
self
):
if
self
.converting:
if
messagebox.askyesno(
"确认"
,
"正在进行转换,确定要退出吗?"
):
self
.cancel_requested
=
True
self
.root.after(
500
,
self
.root.destroy)
else
:
self
.save_config()
self
.root.destroy()
def
show_ico_info(
self
, ico_path):
try
:
with Image.
open
(ico_path) as img:
sizes
=
[]
if
'sizes'
in
img.info:
sizes
=
[f
"{w}x{h}"
for
w, h
in
img.info[
'sizes'
]]
else
:
current
=
0
while
True
:
try
:
img.seek(current)
sizes.append(f
"{img.width}x{img.height}"
)
current
+
=
1
except
EOFError:
break
if
not
sizes:
sizes
=
[f
"{img.width}x{img.height}"
]
sizes.sort(key
=
lambda
x:
int
(x.split(
'x'
)[
0
]))
size_info
=
"、"
.join(sizes)
messagebox.showinfo(
"ICO文件信息"
,
f
"文件: {os.path.basename(ico_path)}\n"
f
"包含的尺寸: {size_info}"
)
print
(f
"ICO文件信息:"
)
print
(f
"sizes in info: {img.info.get('sizes', 'Not found')}"
)
print
(f
"detected sizes: {sizes}"
)
except
Exception as e:
messagebox.showerror(
"错误"
, f
"无法读取ICO文件信息: {e}"
)
print
(f
"Error reading ICO file: {e}"
)
def
main():
try
:
from
tkinterdnd2
import
TkinterDnD
root
=
TkinterDnD.Tk()
except
ImportError:
root
=
tk.Tk()
app
=
IcoConverterApp(root)
root.mainloop()
if
__name__
=
=
"__main__"
:
main()