|
好吧,贴出源码,看下吧,真不一样。
import configparser
import subprocess
import threading
import queue
import time
import tkinter as tk
from tkinter import messagebox, Checkbutton, Button, Label, Scrollbar, Frame, ttk
import os
import sys
import ctypes
import shlex
# 获取程序所在目录
def get_app_directory():
try:
if getattr(sys, 'frozen', False):
return os.path.dirname(sys.executable)
else:
return os.path.dirname(os.path.abspath(__file__))
except Exception as e:
print(f"Error getting app directory: {e}")
return os.getcwd() # 默认返回当前工作目录
# 创建配置文件路径
def get_config_path():
try:
app_dir = get_app_directory()
return os.path.join(app_dir, "install_config.ini")
except Exception as e:
print(f"Error getting config path: {e}")
return "install_config.ini"
# 获取data目录路径
def get_data_directory():
try:
app_dir = get_app_directory()
return os.path.join(app_dir, "data")
except Exception as e:
print(f"Error getting data directory: {e}")
return "data"
class InstallManager:
def set_dark_theme(self, window):
"""设置改进的深色主题"""
try:
# 使用深色主题配色方案
bg_color = "#2c3e50" # 背景色
text_color = "#ecf0f1" # 文字颜色
tree_bg = "#34495e" # 表格背景
primary_color = "#3498db" # 主色
secondary_color = "#2ecc71" # 辅助色
window.configure(bg=bg_color)
# 设置默认颜色
window.option_add("*Background", bg_color)
window.option_add("*Foreground", text_color)
window.option_add("*Checkbutton.Background", tree_bg)
window.option_add("*Checkbutton.Foreground", text_color)
window.option_add("*Label.Background", bg_color)
window.option_add("*Label.Foreground", text_color)
window.option_add("*Frame.Background", bg_color)
# 设置滚动条颜色
window.option_add("*Scrollbar.Background", bg_color)
window.option_add("*Scrollbar.TroughColor", tree_bg)
window.option_add("*Scrollbar.Slider", primary_color)
# 设置进度条样式
style = ttk.Style()
style.theme_use('default')
style.configure("Custom.Horizontal.TProgressbar",
background=primary_color,
troughcolor=tree_bg,
thickness=12,
lightcolor=primary_color,
darkcolor=bg_color,
bordercolor=bg_color)
except Exception as e:
print(f"设置深色主题时出错: {e}")
def __init__(self, master):
try:
# 设置 DPI 感知以兼容高 DPI 显示器
try:
if hasattr(ctypes.windll, 'shcore'):
ctypes.windll.shcore.SetProcessDpiAwareness(1)
except:
pass
self.master = master
master.title("程序安装管理器")
# 设置窗口大小并居中显示
width = 600
height = 600
screen_width = master.winfo_screenwidth()
screen_height = master.winfo_screenheight()
x = (screen_width - width) // 2
y = (screen_height - height) // 2
master.geometry(f"{width}x{height}+{x}+{y}")
# 设置深色主题
self.set_dark_theme(master)
# 安装队列和状态变量
self.install_queue = queue.Queue()
self.currently_installing = []
self.max_concurrent = 1
self.selected_count = 0
self.installations_completed = 0
self.total_to_install = 0
self.current_program = ""
self.success_count = 0 # 成功安装计数
# 创建主容器框架
self.main_frame = tk.Frame(master, bg="#2c3e50")
self.main_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=10)
# 创建带滚动条的容器
self.container_frame = Frame(self.main_frame, bg="#34495e")
self.container_frame.pack(fill=tk.BOTH, expand=True)
# 创建滚动条
self.scrollbar = Scrollbar(self.container_frame, orient=tk.VERTICAL)
self.scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
# 创建Canvas用于滚动
self.canvas = tk.Canvas(
self.container_frame,
bg="#34495e",
yscrollcommand=self.scrollbar.set,
highlightthickness=0
)
self.canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
# 配置滚动条
self.scrollbar.config(command=self.canvas.yview)
# 创建内部框架
self.inner_frame = Frame(self.canvas, bg="#34495e")
self.canvas_window = self.canvas.create_window(
(0, 0),
window=self.inner_frame,
anchor="nw"
)
# 绑定事件
self.inner_frame.bind("<Configure>", self._on_frame_configure)
self.canvas.bind("<Configure>", self._on_canvas_configure)
# 绑定鼠标滚轮事件
self.canvas.bind_all("<MouseWheel>", self._on_mousewheel)
self.inner_frame.bind("<MouseWheel>", self._on_mousewheel)
self.scrollbar.bind("<MouseWheel>", self._on_mousewheel)
# 加载配置
self.programs = []
self.check_vars = []
self.check_buttons = []
self.load_config()
# 创建底部控制面板
self.bottom_frame = tk.Frame(master, bg="#2c3e50")
self.bottom_frame.pack(fill=tk.X, padx=20, pady=(0, 10), side=tk.BOTTOM)
# 状态标签
self.status_frame = tk.Frame(self.bottom_frame, bg="#2c3e50")
self.status_frame.pack(side=tk.LEFT, fill=tk.X, expand=True)
# 使用更兼容的字体
try:
# 尝试使用系统默认字体
self.status_label = Label(
self.status_frame,
text="就绪",
fg="#2ecc71", # 使用辅助色
bg="#2c3e50",
font=("TkDefaultFont", 11),
anchor="w"
)
except:
# 回退到基本设置
self.status_label = Label(
self.status_frame,
text="就绪",
fg="#2ecc71", # 使用辅助色
bg="#2c3e50",
anchor="w"
)
self.status_label.pack(side=tk.LEFT, padx=(15, 0))
# 已选安装标签
try:
self.selected_label = Label(
self.bottom_frame,
text="已选安装: 0",
fg="#3498db", # 使用主色
bg="#2c3e50",
font=("TkDefaultFont", 12, "bold")
)
except:
self.selected_label = Label(
self.bottom_frame,
text="已选安装: 0",
fg="#3498db", # 使用主色
bg="#2c3e50"
)
self.selected_label.place(relx=0.5, rely=0.5, anchor=tk.CENTER)
# 创建安装按钮
try:
self.install_btn = Button(
self.bottom_frame,
text="立即安装",
command=self.start_installation,
height=1,
width=10,
bg="#34495e",
fg="white",
activebackground="#1A2530",
activeforeground="white",
font=("TkDefaultFont", 12, "bold"),
relief=tk.GROOVE,
bd=2
)
except:
self.install_btn = Button(
self.bottom_frame,
text="立即安装",
command=self.start_installation,
bg="#34495e",
fg="white"
)
self.install_btn.pack(side=tk.RIGHT, padx=(0, 15))
# 创建进度显示区域
self.progress_frame = tk.Frame(master, bg="#2c3e50", height=80)
# 初始隐藏进度区域
self.progress_frame.pack_forget()
# 正在安装标签
self.installing_label = Label(
self.progress_frame,
text="",
fg="#3498db", # 使用主色
bg="#2c3e50",
font=("TkDefaultFont", 10),
anchor="w"
)
self.installing_label.pack(fill=tk.X, padx=20, pady=(5, 0))
# 当前安装标签
self.current_label = Label(
self.progress_frame,
text="",
fg="#3498db", # 使用主色
bg="#2c3e50",
font=("TkDefaultFont", 10),
anchor="w"
)
self.current_label.pack(fill=tk.X, padx=20, pady=(0, 2))
# 进度标签
self.progress_text = Label(
self.progress_frame,
text="",
fg="#2ecc71", # 使用辅助色
bg="#2c3e50",
font=("TkDefaultFont", 10),
anchor="w"
)
self.progress_text.pack(fill=tk.X, padx=20, pady=(0, 2))
# 进度条
self.progress_bar = ttk.Progressbar(
self.progress_frame,
orient="horizontal",
length=500,
mode="determinate",
style="Custom.Horizontal.TProgressbar"
)
self.progress_bar.pack(fill=tk.X, padx=20, pady=(0, 5))
# 启动安装监控线程
self.monitor_thread = threading.Thread(target=self.monitor_installations, daemon=True)
self.monitor_thread.start()
except Exception as e:
# 在初始化失败时显示错误信息
error_msg = f"初始化失败: {str(e)}\n\n请检查配置文件是否存在且格式正确。"
messagebox.showerror("致命错误", error_msg)
self.master.destroy()
sys.exit(1)
def _on_frame_configure(self, event=None):
"""当内部框架大小改变时更新滚动区域"""
self.canvas.configure(scrollregion=self.canvas.bbox("all"))
def _on_canvas_configure(self, event):
"""当Canvas大小改变时调整内部框架宽度"""
canvas_width = event.width
self.canvas.itemconfig(self.canvas_window, width=canvas_width)
def _on_mousewheel(self, event):
"""处理鼠标滚轮事件"""
# Windows使用delta值
if event.delta > 0:
self.canvas.yview_scroll(-1, "units")
elif event.delta < 0:
self.canvas.yview_scroll(1, "units")
def update_status(self, message, color="#2ecc71"): # 使用辅助色
"""更新状态标签"""
self.status_label.config(text=message, fg=color)
def update_selected_count(self):
"""更新已选安装计数"""
self.selected_label.config(text=f"已选安装: {self.selected_count}")
def load_config(self):
"""加载配置"""
try:
config_file = get_config_path()
# 检查配置文件是否存在
if not os.path.exists(config_file):
# 尝试创建默认配置文件
try:
self.create_default_config(config_file)
# 创建后重新加载
self.programs = []
self.check_vars = []
self.check_buttons = []
except Exception as e:
error_msg = (
f"找不到配置文件且无法创建新文件:\n{config_file}\n"
f"错误: {str(e)}\n\n"
"请手动创建配置文件并重新启动程序。"
)
messagebox.showerror("配置错误", error_msg)
return
config = configparser.ConfigParser()
# 尝试不同编码读取配置文件
encodings = ['utf-8', 'gbk', 'utf-8-sig', 'latin-1']
read_ok = False
for encoding in encodings:
try:
config.read(config_file, encoding=encoding)
if 'programs' in config:
read_ok = True
break
except Exception as e:
print(f"尝试 {encoding} 编码失败: {e}")
continue
if not read_ok:
messagebox.showerror("错误", "无法读取配置文件,请检查文件格式和编码")
return
if 'programs' not in config:
messagebox.showerror("配置错误", f"配置文件中缺少[programs]部分\n配置文件路径: {config_file}")
return
# 创建复选框
row = 0
for program_name in config['programs']:
try:
full_config = config['programs'][program_name].strip()
if not full_config:
continue
parts = shlex.split(full_config)
if not parts:
continue
path = parts[0]
args = parts[1:] if len(parts) > 1 else []
if not os.path.isabs(path):
data_dir = get_data_directory()
path = os.path.join(data_dir, path)
# 检查路径是否存在 - 如果不存在则跳过
if not os.path.exists(path):
print(f"警告: 找不到 {program_name} 的安装路径: {path}")
continue
var = tk.BooleanVar()
# 创建复选框
try:
cb = Checkbutton(
self.inner_frame,
text=program_name,
variable=var,
anchor="w",
padx=15,
pady=8,
bg="#34495e",
fg="white",
activebackground="#2c3e50",
activeforeground="white",
selectcolor="#3498db", # 使用主色
font=("TkDefaultFont", 11),
command=lambda v=var: self.on_checkbox_change(v)
)
except:
# 字体回退
cb = Checkbutton(
self.inner_frame,
text=program_name,
variable=var,
anchor="w",
padx=15,
pady=8,
bg="#34495e",
fg="white",
command=lambda v=var: self.on_checkbox_change(v)
)
cb.grid(row=row, column=0, sticky="ew", padx=10)
self.programs.append({
'name': program_name,
'path': path,
'args': args
})
self.check_vars.append(var)
self.check_buttons.append(cb)
row += 1
except Exception as e:
print(f"加载程序 {program_name} 时出错: {e}")
continue
# 如果没有加载到任何程序
if not self.programs:
try:
label = tk.Label(
self.inner_frame,
text="未找到可安装的程序,请检查配置文件",
bg="#34495e",
fg="white",
font=("TkDefaultFont", 12)
)
label.grid(row=0, column=0, sticky="w", padx=20, pady=20)
except:
# 简单回退
label = tk.Label(
self.inner_frame,
text="未找到可安装的程序,请检查配置文件",
bg="#34495e",
fg="white"
)
label.grid(row=0, column=0, sticky="w", padx=20, pady=20)
except Exception as e:
exc_type, exc_obj, exc_tb = sys.exc_info()
error_msg = (
f"加载配置时出错: {str(e)}\n"
f"错误位置: 第 {exc_tb.tb_lineno} 行\n\n"
"请检查配置文件格式是否正确。"
)
messagebox.showerror("配置错误", error_msg)
def on_checkbox_change(self, var):
"""复选框状态改变时调用"""
# 更新已选择的数量
self.selected_count = sum(1 for v in self.check_vars if v.get())
self.update_selected_count()
def create_default_config(self, config_path):
"""创建默认配置文件"""
default_config = """[programs]
# 格式:程序名称 = "安装程序路径" [静默安装参数]
# 示例:
# Google Chrome = "ChromeInstaller.exe" /silent
# Visual Studio Code = "VSCodeSetup.exe" /verysilent /suppressmsgboxes
# WinRAR = "wrar.exe" /S
"""
with open(config_path, 'w', encoding='utf-8') as f:
f.write(default_config)
# 创建data目录(如果不存在)
data_dir = get_data_directory()
if not os.path.exists(data_dir):
os.makedirs(data_dir)
def start_installation(self):
"""开始安装选中的程序"""
self.selected_indices = [] # 存储选中程序的索引
for i, var in enumerate(self.check_vars):
if var.get():
self.selected_indices.append(i) # 记录选中程序的索引
# 使用索引获取选中的程序
selected = [self.programs for i in self.selected_indices]
if not selected:
messagebox.showinfo("提示", "请选择至少一个程序进行安装")
return
# 隐藏"已选安装"标签
self.selected_label.place_forget()
# 禁用所有复选框
for cb in self.check_buttons:
cb.config(state=tk.DISABLED)
# 隐藏安装按钮
self.install_btn.pack_forget()
# 清空队列并添加新任务
while not self.install_queue.empty():
self.install_queue.get()
for program in selected:
self.install_queue.put(program)
# 重置安装计数器
self.installations_completed = 0
self.total_to_install = len(selected)
self.success_count = 0 # 重置成功计数
# 显示进度区域 - 放在窗口底部
self.progress_frame.pack(fill=tk.X, padx=20, pady=(0, 0), side=tk.BOTTOM, anchor='s')
self.update_progress(0, "准备开始安装...")
# 更新正在安装标签
self.installing_label.config(text=f"正在安装 {self.total_to_install} 个程序")
# 清空状态标签(什么也不显示)
self.status_label.config(text="")
def update_progress(self, percent, message):
"""更新进度显示"""
self.progress_bar["value"] = percent
self.progress_text.config(text=message)
def start_installation_process(self, program):
"""启动单个安装进程"""
try:
# 设置当前安装程序
self.current_program = program['name']
# 根据是否有静默参数显示不同的安装类型
install_type = "静默安装" if program['args'] else "安装"
# 更新当前安装标签
self.current_label.config(text=f"当前{install_type}: {program['name']}")
# 更新进度显示
progress_percent = int((self.installations_completed / self.total_to_install) * 100)
self.update_progress(progress_percent, f"正在{install_type} {program['name']}...")
# 构建命令
command = f'"{program["path"]}"'
# 添加静默安装参数(如果有)
if program['args']:
command += " " + " ".join(program['args'])
# 执行安装命令
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
startupinfo.wShowWindow = 0 # 隐藏窗口
process = subprocess.Popen(
command,
shell=True,
startupinfo=startupinfo,
creationflags=subprocess.CREATE_NO_WINDOW
)
# 添加到当前安装列表
self.currently_installing.append(process)
# 启动线程等待安装完成
threading.Thread(target=self.wait_for_installation, args=(program, process), daemon=True).start()
except Exception as e:
self.installations_completed += 1
# 更新进度显示为错误信息
self.update_progress(100, f"安装失败: {program['name']}")
def monitor_installations(self):
"""监控安装队列并启动安装任务"""
while True:
# 检查活跃安装数量
self.currently_installing = [p for p in self.currently_installing if p.poll() is None]
# 如果有空闲槽位且队列中有任务
if len(self.currently_installing) < self.max_concurrent and not self.install_queue.empty():
program = self.install_queue.get()
self.start_installation_process(program)
# 检查是否所有安装都已完成
if self.install_queue.empty() and not self.currently_installing and self.total_to_install > 0:
if self.installations_completed >= self.total_to_install:
self.master.after(100, self.finalize_installation)
# 更新进度条
if self.total_to_install > 0:
progress_percent = int((self.installations_completed / self.total_to_install) * 100)
self.master.after(100, lambda: self.update_progress(progress_percent,
f"已完成 {self.installations_completed}/{self.total_to_install} 个程序"))
time.sleep(0.5) # 更频繁地检查(500毫秒)
def wait_for_installation(self, program, process):
"""等待安装完成并处理结果"""
try:
# 等待进程结束
return_code = process.wait()
# 更新安装计数
self.installations_completed += 1
# 从活跃列表中移除
if process in self.currently_installing:
self.currently_installing.remove(process)
# 记录成功安装
if return_code == 0:
self.success_count += 1
except Exception as e:
self.installations_completed += 1
if process in self.currently_installing:
self.currently_installing.remove(process)
def finalize_installation(self):
"""所有安装完成后调用"""
# 更新进度显示
success_message = f"成功安装 {self.success_count} 个软件"
self.update_progress(100, success_message)
# 2秒后重置界面
self.master.after(2000, self.reset_interface)
def reset_interface(self):
"""重置界面状态"""
# 取消选中所有已安装程序的复选框
for index in self.selected_indices:
self.check_vars[index].set(False)
# 更新已选安装计数
self.selected_count = 0
self.update_selected_count()
# 重新显示"已选安装"标签
self.selected_label.place(relx=0.5, rely=0.5, anchor=tk.CENTER)
# 启用所有复选框
for cb in self.check_buttons:
cb.config(state=tk.NORMAL)
# 重新显示安装按钮
self.install_btn.pack(side=tk.RIGHT, padx=(0, 15))
# 重置进度显示
self.progress_frame.pack_forget()
self.progress_bar["value"] = 0
self.progress_text.config(text="")
self.current_label.config(text="")
self.installing_label.config(text="") # 清空正在安装标签
self.current_program = ""
# 重置计数器
self.total_to_install = 0
self.installations_completed = 0
self.success_count = 0
# 恢复状态标签为"就绪"
self.update_status("就绪", "#2ecc71") # 使用辅助色
if __name__ == "__main__":
try:
root = tk.Tk()
app = InstallManager(root)
root.mainloop()
except Exception as e:
# 捕获并显示未处理的异常
error_msg = f"程序崩溃: {str(e)}\n\n请检查配置文件是否存在且格式正确。"
messagebox.showerror("致命错误", error_msg)
sys.exit(1)
|
|