|
本帖最后由 piaomusic 于 2025-6-7 23:02 编辑
# -*- coding: utf-8 -*-
import os
import configparser
import tkinter as tk
from tkinter import ttk, messagebox
import subprocess
import sys
import threading
import psutil
class SoftwareInstaller:
def __init__(self, root):
self.root = root
self.root.title("软件安装管理器")
self.root.geometry("750x500")
self.root.resizable(False, False)
# 初始化变量
self.software_list = []
self.install_commands = {}
self.data_dir = os.path.join(os.path.dirname(sys.argv[0]), "data")
self.window_title = "软件安装管理器"
self.default_select_all = False
self.theme = "浅色" # 默认主题
# 安装相关变量
self.installation_processes = []
self.failed_installations = []
self.installation_complete = False
self.progress_window = None
# 检查并创建配置文件
self.init_config_file()
# 验证配置文件完整性
if not self.verify_author_info():
messagebox.showerror("启动失败", "文件被篡改,程序无法启动!")
sys.exit()
# 加载配置
self.load_config()
# 设置窗口居中
self.center_window()
# 设置样式(根据配置)
self.setup_styles()
# 创建界面
self.create_widgets()
# 加载软件列表
self.load_software_list()
self.populate_treeview()
def verify_author_info(self):
"""验证程序信息部分是否被修改"""
REQUIRED_AUTHOR_INFO = {
'作者': '缘起性空',
'版权': '版权所有 © 2025',
'联系': '32897251@qq.com'
}
if not os.path.exists('config.ini'):
return False
config = configparser.ConfigParser()
config.read('config.ini', encoding='utf-8')
if not config.has_section('程序信息'):
return False
for key, value in REQUIRED_AUTHOR_INFO.items():
if not config.has_option('程序信息', key):
return False
if config.get('程序信息', key) != value:
return False
return True
def init_config_file(self):
"""初始化配置文件"""
if not os.path.exists('config.ini'):
try:
with open('config.ini', 'w', encoding='utf-8') as f:
f.write("""[程序信息]
作者 = 缘起性空
版权 = 版权所有 © 2025
联系 = 32897251@qq.com
[全局设置]
窗口标题 = 软件安装管理器
默认全选 = 否
主题 = 浅色 # 可选值:浅色/深色
[软件列表]
# 格式:软件名称 = 安装程序 参数
# 示例:
# 谷歌浏览器 = chrome_installer.exe /silent
# 微信办公版 = wechat_setup.exe /S
""")
messagebox.showinfo("提示", "已创建默认配置文件 config.ini")
except Exception as e:
messagebox.showerror("错误", f"创建配置文件失败: {e}")
def center_window(self):
"""使窗口居中显示"""
self.root.update_idletasks()
width = self.root.winfo_width()
height = self.root.winfo_height()
x = (self.root.winfo_screenwidth() // 2) - (width // 2)
y = (self.root.winfo_screenheight() // 2) - (height // 2)
self.root.geometry(f'+{x}+{y}')
def load_config(self):
"""加载配置文件设置"""
config = configparser.ConfigParser()
try:
if os.path.exists('config.ini'):
config.read('config.ini', encoding='utf-8')
# 读取窗口标题
title = self.get_config_value(config, '全局设置', '窗口标题')
if title:
self.window_title = title
self.root.title(self.window_title)
# 读取默认全选设置
select_all = self.get_config_value(config, '全局设置', '默认全选')
if select_all:
select_all = select_all.strip().lower()
self.default_select_all = select_all in ('是', 'true', '1', 'yes', '开启', 'on')
# 读取主题设置
theme = self.get_config_value(config, '全局设置', '主题')
if theme:
self.theme = theme.strip()
except Exception as e:
messagebox.showerror("配置错误", f"读取配置文件失败: {e}")
def get_config_value(self, config, section, key):
"""安全获取配置值"""
try:
if section in config and key in config[section]:
return config[section][key]
except:
pass
return None
def setup_styles(self):
"""根据主题配置设置样式"""
self.style = ttk.Style()
self.style.theme_use('clam')
# 根据主题选择配色方案
if self.theme == "深色":
# 深色主题配色
bg_color = "#2c3e50" # 背景色
text_color = "#ecf0f1" # 文字颜色
tree_bg = "#34495e" # 表格背景
button_bg = "#34495e" # 按钮背景
primary_color = "#3498db" # 主色
secondary_color = "#2ecc71" # 辅助色
else:
# 浅色主题配色(默认)
bg_color = "#f5f5f5" # 背景色
text_color = "#333333" # 文字颜色
tree_bg = "#ffffff" # 表格背景
button_bg = "#e0e0e0" # 按钮背景
primary_color = "#4a90e2" # 主色
secondary_color = "#50c878" # 辅助色
# 设置窗口背景
self.root.configure(bg=bg_color)
# 标题样式
self.style.configure("Title.TLabel",
font=('Microsoft YaHei', 18, 'bold'),
background=bg_color,
foreground=text_color)
# 表格样式
self.style.configure("Treeview",
font=('Microsoft YaHei', 11),
rowheight=30,
background=tree_bg,
fieldbackground=tree_bg,
foreground=text_color,
bordercolor="#dddddd")
# 表头样式(保持默认)
self.style.configure("Treeview.Heading",
font=('Microsoft YaHei', 11, 'bold'),
relief="flat")
# 选中行样式
self.style.map("Treeview",
background=[('selected', primary_color)],
foreground=[('selected', 'white')])
# 按钮样式
self.style.configure("Exit.TButton",
background=button_bg,
foreground=text_color,
font=('Microsoft YaHei', 11),
padding=8)
self.style.configure("Install.TButton",
background=secondary_color,
foreground="white",
font=('Microsoft YaHei', 11),
padding=8)
# 按钮悬停效果
hover_bg = "#3d566e" if self.theme == "深色" else "#e8e8e8"
self.style.map("Exit.TButton",
background=[('active', hover_bg), ('pressed', '!disabled', button_bg)])
self.style.map("Install.TButton",
background=[('active', '#60c080'), ('pressed', '!disabled', secondary_color)])
# 进度条样式
self.style.configure("Custom.Horizontal.TProgressbar",
background=primary_color,
troughcolor=button_bg,
thickness=20)
def create_widgets(self):
"""创建界面组件"""
# 获取当前主题的背景色
bg_color = "#2c3e50" if self.theme == "深色" else "#f5f5f5"
text_color = "#ecf0f1" if self.theme == "深色" else "#333333"
# 标题区域
title_frame = tk.Frame(self.root, bg=bg_color)
title_frame.pack(pady=(20, 10))
ttk.Label(title_frame, text=self.window_title, style="Title.TLabel").pack()
# 表格区域
tree_frame = tk.Frame(self.root, bg=bg_color)
tree_frame.pack(fill=tk.BOTH, expand=True, padx=30, pady=(0, 15))
# 创建Treeview
self.tree = ttk.Treeview(
tree_frame,
columns=('选择', '序号', '软件名称', '软件大小'),
show='headings',
selectmode='none'
)
# 设置列
columns = {
'选择': {'width': 60, 'anchor': 'center'},
'序号': {'width': 60, 'anchor': 'center'},
'软件名称': {'width': 380, 'anchor': 'center'},
'软件大小': {'width': 120, 'anchor': 'center'}
}
for col, opts in columns.items():
self.tree.column(col, **opts)
self.tree.heading(col, text=col)
# 滚动条
scrollbar = ttk.Scrollbar(tree_frame, orient="vertical", command=self.tree.yview)
self.tree.configure(yscrollcommand=scrollbar.set)
self.tree.pack(side="left", fill="both", expand=True)
scrollbar.pack(side="right", fill="y")
# 绑定复选框点击事件
self.tree.bind('<Button-1>', self.on_tree_click)
# 底部按钮区域
bottom_frame = tk.Frame(self.root, bg=bg_color)
bottom_frame.pack(fill=tk.X, padx=30, pady=(0, 20))
# 左侧按钮区域(全选/反选)
left_button_frame = tk.Frame(bottom_frame, bg=bg_color)
left_button_frame.pack(side="left")
# 全选/反选复选框
self.select_all_var = tk.BooleanVar(value=self.default_select_all)
select_all_cb = tk.Checkbutton(
left_button_frame,
text="全选/反选",
variable=self.select_all_var,
font=("Microsoft YaHei", 10),
bg=bg_color,
fg=text_color,
activebackground=bg_color,
activeforeground=text_color,
selectcolor=bg_color,
command=self.toggle_select_all
)
select_all_cb.pack(side="left", padx=(0, 10))
# 右侧按钮区域
right_button_frame = tk.Frame(bottom_frame, bg=bg_color)
right_button_frame.pack(side="right")
# 退出按钮
exit_btn = ttk.Button(
right_button_frame,
text="退出安装",
command=self.root.quit,
style="Exit.TButton"
)
exit_btn.pack(side="left", padx=5)
# 开始安装按钮
install_btn = ttk.Button(
right_button_frame,
text="开始安装",
command=self.start_installation,
style="Install.TButton"
)
install_btn.pack(side="left", padx=5)
def on_tree_click(self, event):
"""处理复选框点击事件"""
region = self.tree.identify("region", event.x, event.y)
if region == "cell":
column = self.tree.identify_column(event.x)
if column == "#1": # 复选框列
item = self.tree.identify_row(event.y)
current_values = self.tree.item(item, 'values')
new_state = "☑" if current_values[0] == "☐" else "☐"
self.tree.item(item, values=(new_state, *current_values[1:]))
self.update_select_all_state()
def update_select_all_state(self):
"""更新全选复选框状态"""
all_selected = all(
self.tree.item(item, 'values')[0] == "☑"
for item in self.tree.get_children()
)
self.select_all_var.set(all_selected)
def load_software_list(self):
"""加载软件列表"""
config = configparser.ConfigParser()
try:
if os.path.exists('config.ini'):
config.read('config.ini', encoding='utf-8')
if not os.path.exists(self.data_dir):
os.makedirs(self.data_dir)
software_section = None
if '软件列表' in config:
software_section = config['软件列表']
elif 'Software' in config:
software_section = config['Software']
if software_section:
for idx, (name, cmd) in enumerate(software_section.items(), 1):
if name.startswith('#'):
continue
exe_name = cmd.split()[0]
exe_path = os.path.join(self.data_dir, exe_name)
software = {
'name': name,
'exe': exe_name,
'path': exe_path,
'cmd': cmd.replace(exe_name, exe_path),
'size': '未知'
}
if os.path.exists(exe_path):
try:
software['size'] = self.format_size(os.path.getsize(exe_path))
except Exception as e:
print(f"获取 {name} 大小失败: {e}")
else:
print(f"文件不存在: {exe_path}")
self.software_list.append(software)
self.install_commands[name] = software['cmd']
else:
messagebox.showerror("错误", "配置文件中缺少[软件列表]或[Software]段")
except Exception as e:
messagebox.showerror("错误", f"读取配置文件失败: {e}")
def format_size(self, size):
"""格式化文件大小"""
for unit in ['B', 'KB', 'MB', 'GB']:
if size < 1024.0:
return f"{size:.1f} {unit}"
size /= 1024.0
return f"{size:.1f} TB"
def populate_treeview(self):
"""填充表格数据"""
for idx, software in enumerate(self.software_list, 1):
initial_state = "☑" if self.default_select_all else "☐"
self.tree.insert('', 'end', values=(
initial_state,
idx,
software['name'],
software['size']
))
self.select_all_var.set(self.default_select_all)
def toggle_select_all(self):
"""全选/反选功能"""
new_state = "☑" if self.select_all_var.get() else "☐"
for item in self.tree.get_children():
current_values = self.tree.item(item, 'values')
self.tree.item(item, values=(new_state, *current_values[1:]))
def start_installation(self):
"""开始安装选中的软件"""
selected_software = []
for item in self.tree.get_children():
values = self.tree.item(item, 'values')
if values[0] == "☑":
for software in self.software_list:
if software['name'] == values[2]:
selected_software.append(software)
break
if not selected_software:
messagebox.showwarning("警告", "请至少选择一个软件进行安装")
return
# 禁用按钮
for widget in self.root.winfo_children():
if isinstance(widget, tk.Button):
widget.config(state="disabled")
# 创建安装进度窗口
self.progress_window = tk.Toplevel(self.root)
self.progress_window.title("安装进度")
self.progress_window.geometry("400x200")
self.progress_window.resizable(False, False)
bg_color = "#2c3e50" if self.theme == "深色" else "#f5f5f5"
text_color = "#ecf0f1" if self.theme == "深色" else "#333333"
self.progress_window.configure(bg=bg_color)
self.progress_window.attributes('-topmost', True)
# 居中窗口
self.progress_window.update_idletasks()
width = self.progress_window.winfo_width()
height = self.progress_window.winfo_height()
x = (self.progress_window.winfo_screenwidth() // 2) - (width // 2)
y = (self.progress_window.winfo_screenheight() // 2) - (height // 2)
self.progress_window.geometry(f"{width}x{height}+{x}+{y}")
# 进度窗口内容
tk.Label(
self.progress_window,
text="正在安装软件,请稍候...",
font=("Microsoft YaHei", 12),
bg=bg_color,
fg=text_color,
pady=20
).pack()
self.progress_var = tk.DoubleVar()
progress_bar = ttk.Progressbar(
self.progress_window,
variable=self.progress_var,
maximum=100,
mode="determinate",
style="Custom.Horizontal.TProgressbar"
)
progress_bar.pack(fill="x", padx=20, pady=10)
self.status_label = tk.Label(
self.progress_window,
text="准备安装...",
font=("Microsoft YaHei", 10),
bg=bg_color,
fg=text_color
)
self.status_label.pack()
# 重置安装状态
self.installation_processes = []
self.failed_installations = []
self.installation_complete = False
# 在新线程中执行安装
threading.Thread(
target=self.install_software,
args=(selected_software,),
daemon=True
).start()
# 检查安装状态
self.check_installation_status()
def install_software(self, software_list):
"""安装软件"""
total = len(software_list)
success_count = 0
for i, software in enumerate(software_list):
exe_path = os.path.join(self.data_dir, software["exe"])
if not os.path.exists(exe_path):
self.failed_installations.append(f"{software['name']} (文件缺失)")
continue
try:
self.root.after(0, lambda s=software: self.status_label.config(text=f"正在安装 {s['name']}..."))
cmd = self.install_commands.get(software["name"], "")
if not cmd:
self.failed_installations.append(f"{software['name']} (未找到安装命令)")
continue
process = subprocess.Popen(cmd, shell=True)
self.installation_processes.append((software["name"], process))
self.root.after(0, lambda p=(i + 1) / total * 100: self.progress_var.set(p))
success_count += 1
except Exception as e:
self.failed_installations.append(f"{software['name']} (错误: {str(e)})")
self.installation_complete = True
if success_count > 0:
self.root.after(0, lambda: self.status_label.config(text=f"已完成 {success_count}/{total} 个软件安装"))
def check_installation_status(self):
"""检查安装状态"""
if self.installation_complete and all(not self.is_process_running(p[1].pid) for p in self.installation_processes):
if self.progress_window:
self.progress_window.destroy()
if self.failed_installations:
failed_list = "\n".join(self.failed_installations)
messagebox.showwarning(
"安装完成",
f"安装完成,但有 {len(self.failed_installations)} 个软件安装失败:\n{failed_list}"
)
else:
messagebox.showinfo("完成", "所有软件安装已完成")
# 重新启用按钮
for widget in self.root.winfo_children():
if isinstance(widget, tk.Button):
widget.config(state="normal")
else:
self.root.after(1000, self.check_installation_status)
def is_process_running(self, pid):
"""检查进程是否在运行"""
try:
return psutil.pid_exists(pid)
except:
return False
def main():
root = tk.Tk()
app = SoftwareInstaller(root)
root.mainloop()
if __name__ == "__main__":
main()
|
|