个人使用了还是不错的,分享给大家也使用,发了好几次都没有成功,第一次发帖,不知道怎么发,如果有什么问题可以说一下,
https://wwtk.lanzoue.com/iXYvf32tr2ed密码:52pj
https://pan.baidu.com/s/17Gp-REIckl9WbmIatMryAA?pwd=52pj 提取码: 52pj
[Python] 纯文本查看 复制代码import sys
import subprocess
import logging
import time
import os
import glob
from typing import List, Dict, Optional
from PyQt5.QtWidgets import (QApplication, QMainWindow, QTabWidget, QWidget, QVBoxLayout,
QHBoxLayout, QLabel, QPushButton, QListWidget, QListWidgetItem,
QTextEdit, QProgressBar, QFileDialog, QLineEdit, QGroupBox,
QCheckBox, QMessageBox, QScrollArea, QSplitter, QComboBox,
QAction, QMenuBar, QMenu, QTextBrowser)
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QTimer
from PyQt5.QtGui import QFont, QIcon, QPalette, QColor, QBrush
# ==================== 配置与常量定义 ====================
# 日志配置
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler("package_installer_gui.log", encoding='utf-8'),
]
)
# 国内镜像源
MIRRORS = {
"清华": "https://pypi.tuna.tsinghua.edu.cn/simple",
"阿里": "https://mirrors.aliyun.com/pypi/simple/",
"中科大": "https://pypi.mirrors.ustc.edu.cn/simple/",
"默认": None
}
# 安装配置
MAX_RETRIES = 3 # 最大重试次数
RETRY_DELAY = 3 # 重试延迟(秒)
MAX_WORKERS = 5 # 最大线程数
# 样式常量
PRIMARY_COLOR = "#2C6ECB" # 主色调:蓝色
SECONDARY_COLOR = "#4A90E2" # 次要色:浅蓝色
ACCENT_COLOR = "#5CB85C" # 强调色:绿色(成功)
WARNING_COLOR = "#F0AD4E" # 警告色:橙色
DANGER_COLOR = "#D9534F" # 危险色:红色(错误/取消)
LIGHT_COLOR = "#F8F9FA" # 浅色背景
DARK_COLOR = "#292B2C" # 深色文本
GRAY_COLOR = "#E9ECEF" # 灰色背景
BORDER_COLOR = "#DEE2E6" # 边框色
# ==================== 核心安装功能 ====================
class InstallWorker(QThread):
progress_updated = pyqtSignal(int, str) # 进度值, 状态文本
log_updated = pyqtSignal(str) # 日志文本
install_finished = pyqtSignal(dict) # 安装结果
def __init__(self, packages: List[str], python_path: str):
super().__init__()
self.packages = packages
self.python_path = python_path # 接收Python路径参数
self.is_running = True
def run(self):
results = {
"success": [],
"failed": [],
"skipped": [],
"total": len(self.packages)
}
if not self.packages:
self.log_updated.emit("没有需要安装的包")
self.install_finished.emit(results)
return
# 验证Python路径有效性并在日志中显示
self.log_updated.emit(f"当前使用的Python安装路径: {self.python_path}")
self.log_updated.emit(f"Python可执行文件位置: {os.path.join(self.python_path, 'python.exe')}")
self.log_updated.emit(f"pip可执行文件位置: {os.path.join(self.python_path, 'Scripts', 'pip.exe')}")
if not self.verify_python_path():
self.install_finished.emit(results)
return
if not self.check_and_upgrade_pip():
self.install_finished.emit(results)
return
total = len(self.packages)
for i, package in enumerate(self.packages):
if not self.is_running:
self.log_updated.emit("安装已取消")
break
progress = int((i + 1) / total * 100)
self.progress_updated.emit(progress, f"正在处理: {package}")
result = self.install_single_package(package)
status = result["status"]
reason = result["reason"]
if status == "success":
results["success"].append(package)
self.log_updated.emit(f"✅ 成功安装: {package}")
elif status == "skipped":
results["skipped"].append(package)
self.log_updated.emit(f"⏭️ 已安装跳过: {package}")
else:
results["failed"].append(f"{package} ({reason[:50]})")
self.log_updated.emit(f"❌ 安装失败: {package} ({reason[:50]})")
self.install_finished.emit(results)
def stop(self):
self.is_running = False
def verify_python_path(self) -> bool:
"""验证Python路径是否有效"""
if not self.python_path:
self.log_updated.emit("错误:未指定Python路径")
return False
python_exe = os.path.join(self.python_path, "python.exe")
pip_exe = os.path.join(self.python_path, "Scripts", "pip.exe")
if not os.path.exists(python_exe):
self.log_updated.emit(f"错误:Python可执行文件不存在 - {python_exe}")
return False
if not os.path.exists(pip_exe):
self.log_updated.emit(f"警告:未找到pip可执行文件 - {pip_exe}")
self.log_updated.emit("尝试修复pip环境...")
return True
def run_command(self, command: List[str], description: str) -> (bool, str):
try:
result = subprocess.run(
command,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding='utf-8',
timeout=300
)
if result.returncode == 0:
self.log_updated.emit(f"{description} 成功")
return (True, result.stdout)
else:
error_msg = result.stderr.strip()[:500]
self.log_updated.emit(f"{description} 失败: {error_msg[:100]}")
return (False, error_msg)
except subprocess.TimeoutExpired:
msg = f"{description} 超时(超过5分钟)"
self.log_updated.emit(msg)
return (False, msg)
except Exception as e:
msg = f"执行命令时发生错误: {str(e)}"
self.log_updated.emit(msg)
return (False, msg)
def check_and_upgrade_pip(self) -> bool:
self.log_updated.emit("===== 检查pip环境 =====")
# 使用指定路径的Python和pip
success, _ = self.run_command(
[os.path.join(self.python_path, "python.exe"), "-m", "pip", "--version"],
"检查pip可用性"
)
if not success:
self.log_updated.emit("pip不可用,尝试修复...")
for i in range(MAX_RETRIES):
fix_success, _ = self.run_command(
[os.path.join(self.python_path, "python.exe"), "-m", "ensurepip", "--upgrade"],
f"修复pip(尝试 {i + 1}/{MAX_RETRIES})"
)
if fix_success:
success = True
break
time.sleep(RETRY_DELAY)
if not success:
self.log_updated.emit("pip修复失败,无法继续安装")
return False
self.log_updated.emit("升级pip到最新版本...")
self.run_command(
[os.path.join(self.python_path, "python.exe"), "-m", "pip", "install", "--upgrade", "pip"],
"升级pip"
)
return True
def is_package_installed(self, package: str) -> bool:
pure_package = package.split("[")[0]
success, _ = self.run_command(
[os.path.join(self.python_path, "python.exe"), "-m", "pip", "show", pure_package],
f"检查 {pure_package} 是否已安装"
)
return success
def install_single_package(self, package: str) -> Dict:
result = {
"package": package,
"status": "failed",
"reason": ""
}
if self.is_package_installed(package):
result["status"] = "skipped"
result["reason"] = "已安装"
return result
pure_package = package.split("[")[0]
for retry in range(MAX_RETRIES):
if not self.is_running:
result["reason"] = "安装已取消"
return result
for mirror_name, mirror_url in MIRRORS.items():
install_cmd = [
os.path.join(self.python_path, "python.exe"),
"-m", "pip", "install", package
]
if mirror_url:
host = mirror_url.split("//")[-1].split("/")[0]
install_cmd.extend(["-i", mirror_url, "--trusted-host", host])
desc = f"安装 {package}(镜像:{mirror_name},重试:{retry + 1}/{MAX_RETRIES})"
success, output = self.run_command(install_cmd, desc)
if success and self.is_package_installed(pure_package):
result["status"] = "success"
result["reason"] = "安装成功"
return result
elif success:
result["reason"] = f"安装命令成功,但验证失败"
else:
result["reason"] = f"安装失败"
if retry str:
"""获取当前运行环境的Python路径"""
try:
return os.path.dirname(sys.executable)
except:
return ""
def browse_python_path(self):
"""浏览选择Python安装目录"""
default_dir = self.python_path if self.python_path else "C:\\"
python_dir = QFileDialog.getExistingDirectory(
self, "选择Python安装目录", default_dir,
QFileDialog.ShowDirsOnly | QFileDialog.DontResolveSymlinks
)
if python_dir:
# 验证是否包含python.exe
if os.path.exists(os.path.join(python_dir, "python.exe")):
self.python_path_edit.setText(python_dir)
self.python_path = python_dir
# 更新日志显示
self.append_python_path_to_log()
else:
QMessageBox.warning(
self, "路径无效",
"所选目录不包含python.exe,请选择正确的Python安装目录"
)
def find_python_installations(self):
"""自动查找系统中的Python安装"""
self.log_text.append("正在查找系统中的Python安装...")
# 常见的Python安装路径
search_paths = [
"C:\\Python*",
"C:\\Program Files\\Python*",
"C:\\Program Files (x86)\\Python*",
os.path.expanduser("~") + "\\AppData\\Local\\Programs\\Python\\Python*",
"D:\\Python*", # 可能的其他盘符
"E:\\Python*"
]
python_paths = []
# 保留当前选择的路径(如果有效)
current_path = self.python_path_edit.text()
if current_path and os.path.exists(os.path.join(current_path, "python.exe")):
python_paths.append(current_path)
# 搜索常见路径
for path_pattern in search_paths:
for path in glob.glob(path_pattern):
if os.path.exists(os.path.join(path, "python.exe")):
if path not in python_paths:
python_paths.append(path)
# 添加环境变量中的Python路径
try:
env_python = subprocess.check_output(
["where", "python"],
shell=True,
text=True
).splitlines()
for path in env_python:
if path and os.path.exists(path):
python_dir = os.path.dirname(path)
if python_dir not in python_paths:
python_paths.append(python_dir)
except:
pass
# 更新下拉列表
self.python_versions_combo.clear()
for path in python_paths:
# 获取Python版本信息
try:
version = subprocess.check_output(
[os.path.join(path, "python.exe"), "--version"],
stderr=subprocess.STDOUT,
text=True
).strip()
self.python_versions_combo.addItem(f"{version} - {path}", path)
except:
self.python_versions_combo.addItem(f"Python - {path}", path)
self.log_text.append(f"已找到 {len(python_paths)} 个Python安装位置")
# 显示当前选中的Python路径
self.append_python_path_to_log()
def on_python_version_selected(self, index):
"""选择下拉列表中的Python版本"""
if index >= 0:
python_path = self.python_versions_combo.itemData(index)
self.python_path_edit.setText(python_path)
self.python_path = python_path
# 更新日志显示
self.append_python_path_to_log()
def on_python_path_changed(self):
"""当Python路径发生变化时更新日志"""
self.python_path = self.python_path_edit.text().strip()
self.append_python_path_to_log()
def append_python_path_to_log(self):
"""将Python路径信息添加到日志区域"""
if self.python_path and hasattr(self, 'log_text'):
# 检查路径有效性
python_exe_exists = os.path.exists(os.path.join(self.python_path, "python.exe"))
pip_exe_exists = os.path.exists(os.path.join(self.python_path, "Scripts", "pip.exe"))
status = "有效" if python_exe_exists else "无效"
self.log_text.append(f"当前Python安装路径 [{status}]: {self.python_path}")
if python_exe_exists:
self.log_text.append(f"Python可执行文件: {os.path.join(self.python_path, 'python.exe')}")
pip_status = "存在" if pip_exe_exists else "不存在"
self.log_text.append(
f"pip可执行文件 [{pip_status}]: {os.path.join(self.python_path, 'Scripts', 'pip.exe')}")
else:
self.log_text.append("警告: 所选路径不包含python.exe,无法执行安装操作")
def apply_styles(self):
"""应用样式表美化界面"""
self.setStyleSheet(f"""
/* 主窗口样式 */
QMainWindow, QWidget {{
background-color: {LIGHT_COLOR};
color: {DARK_COLOR};
}}
/* 标题标签 */
QLabel#titleLabel {{
color: {PRIMARY_COLOR};
padding: 5px;
}}
/* 标签页样式 */
QTabWidget::pane {{
border: 1px solid {BORDER_COLOR};
border-radius: 6px;
background-color: white;
padding: 10px;
}}
QTabBar::tab {{
background-color: {LIGHT_COLOR};
color: {DARK_COLOR};
padding: 8px 16px;
border: 1px solid {BORDER_COLOR};
border-bottom-color: {BORDER_COLOR};
border-radius: 6px 6px 0 0;
margin-right: 2px;
}}
QTabBar::tab:selected {{
background-color: white;
border-color: {BORDER_COLOR};
border-bottom-color: white;
font-weight: bold;
}}
QTabBar::tab:hover:!selected {{
background-color: {GRAY_COLOR};
border-color: {SECONDARY_COLOR};
}}
/* 分组框样式 */
QGroupBox {{
border: 1px solid {BORDER_COLOR};
border-radius: 6px;
margin-top: 10px;
padding: 10px;
background-color: white;
}}
QGroupBox::title {{
color: {PRIMARY_COLOR};
subcontrol-origin: margin;
left: 10px;
padding: 0 5px 0 5px;
font-weight: bold;
}}
/* 按钮样式 */
QPushButton {{
background-color: {PRIMARY_COLOR};
color: white;
border: none;
border-radius: 4px;
padding: 6px 12px;
font-size: 14px;
}}
QPushButton:hover {{
background-color: {SECONDARY_COLOR};
}}
QPushButton:pressed {{
background-color: {PRIMARY_COLOR};
padding: 7px 11px 5px 13px;
}}
QPushButton:disabled {{
background-color: {GRAY_COLOR};
color: #999;
}}
/* 特殊按钮样式 */
QPushButton#installBtn {{
background-color: {ACCENT_COLOR};
}}
QPushButton#installBtn:hover {{
background-color: #4CAE4C;
}}
QPushButton#cancelBtn {{
background-color: {DANGER_COLOR};
}}
QPushButton#cancelBtn:hover {{
background-color: #C9302C;
}}
/* 列表控件样式 */
QListWidget {{
border: 1px solid {BORDER_COLOR};
border-radius: 4px;
background-color: white;
padding: 5px;
alternate-background-color: {GRAY_COLOR};
}}
QListWidget::item {{
padding: 3px;
border-radius: 2px;
}}
QListWidget::item:selected {{
background-color: {SECONDARY_COLOR};
color: white;
}}
QListWidget::item:hover {{
background-color: {GRAY_COLOR};
}}
/* 进度条样式 */
QProgressBar {{
border: 1px solid {BORDER_COLOR};
border-radius: 4px;
text-align: center;
height: 20px;
}}
QProgressBar::chunk {{
background-color: {ACCENT_COLOR};
border-radius: 3px;
}}
/* 文本编辑框(日志)样式 */
QTextEdit, QTextBrowser {{
border: 1px solid {BORDER_COLOR};
border-radius: 4px;
background-color: white;
padding: 5px;
font-family: "Consolas", "Monaco", monospace;
font-size: 12px;
}}
/* 输入框样式 */
QLineEdit {{
border: 1px solid {BORDER_COLOR};
border-radius: 4px;
padding: 5px;
background-color: white;
}}
QLineEdit:focus {{
border-color: {SECONDARY_COLOR};
outline: none;
}}
/* 复选框样式 */
QCheckBox {{
padding: 3px;
}}
QCheckBox:hover {{
color: {PRIMARY_COLOR};
}}
QCheckBox::indicator:checked {{
color: {PRIMARY_COLOR};
}}
/* 分割器样式 */
QSplitter::handle {{
background-color: {BORDER_COLOR};
}}
QSplitter::handle:hover {{
background-color: {SECONDARY_COLOR};
}}
/* 下拉框样式 */
QComboBox {{
border: 1px solid {BORDER_COLOR};
border-radius: 4px;
padding: 5px;
background-color: white;
}}
QComboBox:focus {{
border-color: {SECONDARY_COLOR};
}}
/* 滚动区域样式 */
QScrollArea {{
border: 1px solid {BORDER_COLOR};
border-radius: 6px;
background-color: white;
}}
QScrollArea::viewport {{
background-color: transparent;
}}
/* 菜单栏样式 */
QMenuBar {{
background-color: {LIGHT_COLOR};
color: {DARK_COLOR};
}}
QMenuBar::item {{
background-color: {LIGHT_COLOR};
padding: 4px 8px;
}}
QMenuBar::item:selected {{
background-color: {SECONDARY_COLOR};
color: white;
}}
QMenu {{
background-color: white;
border: 1px solid {BORDER_COLOR};
}}
QMenu::item {{
padding: 4px 20px;
}}
QMenu::item:selected {{
background-color: {SECONDARY_COLOR};
color: white;
}}
""")
# 设置按钮对象名以便应用特殊样式
self.install_btn.setObjectName("installBtn")
self.cancel_btn.setObjectName("cancelBtn")
def create_category_tab(self):
tab = QWidget()
layout = QVBoxLayout(tab)
layout.setSpacing(8)
splitter = QSplitter(Qt.Vertical)
layout.addWidget(splitter)
# 左侧类别列表(增加滚动功能)
scroll_area = QScrollArea() # 滚动区域
scroll_area.setWidgetResizable(True) # 自适应大小
splitter.addWidget(scroll_area)
# 滚动区域内部的容器
scroll_content = QWidget()
scroll_layout = QVBoxLayout(scroll_content)
scroll_area.setWidget(scroll_content)
# 库类别选择分组框(放入滚动区域)
category_group = QGroupBox("选择库类别 (可多选)")
category_layout = QVBoxLayout(category_group)
category_layout.setSpacing(5)
self.category_checkboxes = {}
for category in CATEGORIES.keys():
cb = QCheckBox(category)
self.category_checkboxes[category] = cb
category_layout.addWidget(cb)
# 添加"全选"和"全不选"按钮
btn_layout = QHBoxLayout()
select_all_btn = QPushButton("全选")
select_all_btn.clicked.connect(lambda: self.check_all_categories(True))
deselect_all_btn = QPushButton("全不选")
deselect_all_btn.clicked.connect(lambda: self.check_all_categories(False))
btn_layout.addWidget(select_all_btn)
btn_layout.addWidget(deselect_all_btn)
category_layout.addLayout(btn_layout)
scroll_layout.addWidget(category_group) # 将分组框加入滚动区域
# 右侧选中的库列表
self.selected_libs_list = QListWidget()
self.selected_libs_list.setAlternatingRowColors(True)
splitter.addWidget(self.selected_libs_list)
# 添加更新按钮
update_btn = QPushButton("更新选中的库列表")
update_btn.clicked.connect(self.update_selected_libs)
layout.addWidget(update_btn)
self.tabs.addTab(tab, "按类别选择")
def create_manual_tab(self):
tab = QWidget()
layout = QVBoxLayout(tab)
layout.setSpacing(8)
# 输入框
input_group = QGroupBox("输入库名称 (用逗号分隔)")
input_layout = QVBoxLayout(input_group)
self.manual_input = QLineEdit()
self.manual_input.setPlaceholderText("例如: numpy,pandas,requests,flask")
input_layout.addWidget(self.manual_input)
layout.addWidget(input_group)
# 添加按钮
add_btn = QPushButton("添加到安装列表")
add_btn.clicked.connect(self.add_manual_libs)
layout.addWidget(add_btn)
# 已添加的库列表
list_group = QGroupBox("已添加的库")
list_layout = QVBoxLayout(list_group)
self.manual_libs_list = QListWidget()
self.manual_libs_list.setAlternatingRowColors(True)
list_layout.addWidget(self.manual_libs_list)
layout.addWidget(list_group)
# 添加移除按钮
remove_btn = QPushButton("移除选中项")
remove_btn.clicked.connect(self.remove_manual_lib)
layout.addWidget(remove_btn)
self.tabs.addTab(tab, "手动输入")
def create_file_tab(self):
tab = QWidget()
layout = QVBoxLayout(tab)
layout.setSpacing(8)
# 文件选择区域
file_group = QGroupBox("从文件导入库列表")
file_layout = QVBoxLayout(file_group)
file_path_layout = QHBoxLayout()
self.file_path_edit = QLineEdit()
self.file_path_edit.setReadOnly(True)
browse_btn = QPushButton("浏览...")
browse_btn.clicked.connect(self.browse_file)
file_path_layout.addWidget(self.file_path_edit, 3)
file_path_layout.addWidget(browse_btn, 1)
load_btn = QPushButton("加载文件")
load_btn.clicked.connect(self.load_file_libs)
file_layout.addLayout(file_path_layout)
file_layout.addWidget(load_btn)
layout.addWidget(file_group)
# 库列表显示
list_group = QGroupBox("从文件加载的库")
list_layout = QVBoxLayout(list_group)
self.file_libs_list = QListWidget()
self.file_libs_list.setAlternatingRowColors(True)
list_layout.addWidget(self.file_libs_list)
layout.addWidget(list_group)
self.tabs.addTab(tab, "文件导入")
def create_preset_tab(self):
tab = QWidget()
layout = QVBoxLayout(tab)
layout.setSpacing(8)
# 预设组合选择
preset_group = QGroupBox("选择预设组合 (可多选)")
preset_layout = QVBoxLayout(preset_group)
preset_layout.setSpacing(5)
self.preset_checkboxes = {}
for preset in PRESETS.keys():
cb = QCheckBox(preset)
self.preset_checkboxes[preset] = cb
preset_layout.addWidget(cb)
# 添加"全选"和"全不选"按钮
btn_layout = QHBoxLayout()
select_all_btn = QPushButton("全选")
select_all_btn.clicked.connect(lambda: self.check_all_presets(True))
deselect_all_btn = QPushButton("全不选")
deselect_all_btn.clicked.connect(lambda: self.check_all_presets(False))
btn_layout.addWidget(select_all_btn)
btn_layout.addWidget(deselect_all_btn)
preset_layout.addLayout(btn_layout)
layout.addWidget(preset_group)
# 选中的预设库列表
self.preset_libs_list = QListWidget()
self.preset_libs_list.setAlternatingRowColors(True)
layout.addWidget(self.preset_libs_list)
# 添加更新按钮
update_btn = QPushButton("更新选中的预设库列表")
update_btn.clicked.connect(self.update_preset_libs)
layout.addWidget(update_btn)
self.tabs.addTab(tab, "预设组合")
def create_help_tab(self):
"""创建帮助标签页"""
tab = QWidget()
layout = QVBoxLayout(tab)
# 创建带滚动功能的文本浏览器显示帮助内容
scroll_area = QScrollArea()
scroll_area.setWidgetResizable(True)
layout.addWidget(scroll_area)
help_content = QWidget()
help_layout = QVBoxLayout(help_content)
scroll_area.setWidget(help_content)
# 使用QTextBrowser显示富文本帮助信息
self.help_browser = QTextBrowser()
self.help_browser.setOpenExternalLinks(True) # 允许打开外部链接
help_layout.addWidget(self.help_browser)
# 设置帮助内容
self.set_help_content()
self.tabs.addTab(tab, "使用帮助")
def set_help_content(self):
"""设置帮助内容"""
help_text = """
Python库自动安装工具 - 使用帮助
1. 概述
本工具旨在帮助用户快速、便捷地安装各种Python库。支持多种选择方式,并自动处理安装过程中的常见问题。
2. 选择Python路径
在使用工具前,需要指定Python安装路径:
3. 选择库的方式
工具提供四种选择库的方式,通过顶部标签页切换:
3.1 按类别选择
从预设的类别中选择需要安装的库类别:
3.2 手动输入
手动输入需要安装的库名称:
3.3 文件导入
从文本文件导入库列表:
3.4 预设组合
选择预设的库组合:
4. 开始安装
选择好需要安装的库后,点击底部的"开始安装"按钮:
5. 其他功能
6. 常见问题
Q: 提示"Python可执行文件不存在"怎么办?
A: 请确保所选路径包含python.exe文件,通常位于Python安装目录的根目录。
Q: 安装失败的库如何处理?
A: 可尝试单独安装失败的库,或检查网络连接,也可尝试更换Python环境。
Q: 工具支持虚拟环境吗?
A: 支持,只需在Python路径选择中指定虚拟环境的Python目录即可。
"""
self.help_browser.setHtml(help_text)
def create_progress_area(self, main_layout):
# 进度条区域
progress_group = QGroupBox("安装进度")
progress_layout = QVBoxLayout(progress_group)
# 进度信息
status_layout = QHBoxLayout()
self.progress_label = QLabel("准备就绪")
self.progress_label.setMinimumWidth(200)
self.progress_bar = QProgressBar()
self.progress_bar.setValue(0)
status_layout.addWidget(self.progress_label)
status_layout.addWidget(self.progress_bar)
progress_layout.addLayout(status_layout)
main_layout.addWidget(progress_group)
# 日志区域
log_group = QGroupBox("安装日志")
log_layout = QVBoxLayout(log_group)
self.log_text = QTextEdit()
self.log_text.setReadOnly(True)
self.log_text.setLineWrapMode(QTextEdit.WidgetWidth)
log_layout.addWidget(self.log_text)
main_layout.addWidget(log_group)
def create_buttons_area(self, main_layout):
btn_layout = QHBoxLayout()
btn_layout.setSpacing(10)
self.install_btn = QPushButton("开始安装")
self.install_btn.clicked.connect(self.start_installation)
self.cancel_btn = QPushButton("取消安装")
self.cancel_btn.clicked.connect(self.cancel_installation)
self.cancel_btn.setEnabled(False)
self.clear_log_btn = QPushButton("清空日志")
self.clear_log_btn.clicked.connect(self.clear_log)
btn_layout.addWidget(self.install_btn)
btn_layout.addWidget(self.cancel_btn)
btn_layout.addStretch()
btn_layout.addWidget(self.clear_log_btn)
main_layout.addLayout(btn_layout)
# ==================== 帮助相关函数 ====================
def show_help(self):
"""显示帮助内容,切换到帮助标签页"""
self.tabs.setCurrentIndex(4) # 帮助标签页的索引
def show_about(self):
"""显示关于对话框"""
about_text = """
Python库自动安装工具 v1.0
一个便捷的Python库批量安装工具,支持多种选择方式,
自动处理安装过程中的常见问题,提高工作效率。
功能特点:
- 多方式选择需要安装的库
- 自动查找系统中的Python环境
- 支持国内镜像源,提高安装速度
- 自动跳过已安装的库
- 详细的安装日志和结果统计
"""
QMessageBox.about(self, "关于", about_text)
# ==================== 界面交互函数 ====================
def check_all_categories(self, check: bool):
for cb in self.category_checkboxes.values():
cb.setChecked(check)
def update_selected_libs(self):
self.selected_libs_list.clear()
selected_libs = []
for category, cb in self.category_checkboxes.items():
if cb.isChecked() and category in CATEGORIES:
selected_libs.extend(CATEGORIES[category])
unique_libs = list(dict.fromkeys(selected_libs))
for lib in unique_libs:
self.selected_libs_list.addItem(lib)
self.log_text.append(f"已更新选中的库,共 {len(unique_libs)} 个")
def add_manual_libs(self):
text = self.manual_input.text().strip()
if not text:
QMessageBox.warning(self, "提示", "请输入库名称")
return
libs = [lib.strip() for lib in text.split(',') if lib.strip()]
if not libs:
QMessageBox.warning(self, "提示", "请输入有效的库名称")
return
current_libs = [self.manual_libs_list.item(i).text()
for i in range(self.manual_libs_list.count())]
added_count = 0
for lib in libs:
if lib not in current_libs:
self.manual_libs_list.addItem(lib)
current_libs.append(lib)
added_count += 1
self.log_text.append(f"已添加 {added_count} 个库到手动安装列表")
self.manual_input.clear()
def remove_manual_lib(self):
selected_items = self.manual_libs_list.selectedItems()
if not selected_items:
QMessageBox.warning(self, "提示", "请先选择要移除的库")
return
for item in selected_items:
row = self.manual_libs_list.row(item)
self.manual_libs_list.takeItem(row)
self.log_text.append(f"已移除 {len(selected_items)} 个库")
def browse_file(self):
file_path, _ = QFileDialog.getOpenFileName(
self, "选择库列表文件", "", "文本文件 (*.txt);;所有文件 (*)"
)
if file_path:
self.file_path_edit.setText(file_path)
def load_file_libs(self):
file_path = self.file_path_edit.text()
if not file_path or not os.path.exists(file_path):
QMessageBox.warning(self, "错误", "请选择有效的文件")
return
try:
with open(file_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
libs = [line.strip() for line in lines
if line.strip() and not line.strip().startswith('#')]
if not libs:
QMessageBox.information(self, "提示", "文件中没有有效的库名称")
return
self.file_libs_list.clear()
for lib in libs:
self.file_libs_list.addItem(lib)
self.log_text.append(f"从文件加载了 {len(libs)} 个库")
except Exception as e:
QMessageBox.critical(self, "错误", f"读取文件失败: {str(e)}")
def check_all_presets(self, check: bool):
for cb in self.preset_checkboxes.values():
cb.setChecked(check)
def update_preset_libs(self):
self.preset_libs_list.clear()
selected_libs = []
for preset, cb in self.preset_checkboxes.items():
if cb.isChecked() and preset in PRESETS:
selected_libs.extend(PRESETS[preset])
unique_libs = list(dict.fromkeys(selected_libs))
for lib in unique_libs:
self.preset_libs_list.addItem(lib)
self.log_text.append(f"已更新选中的预设库,共 {len(unique_libs)} 个")
# ==================== 安装控制函数 ====================
def get_selected_packages(self) -> List[str]:
tab_index = self.tabs.currentIndex()
packages = []
if tab_index == 0: # 按类别选择
for category, cb in self.category_checkboxes.items():
if cb.isChecked() and category in CATEGORIES:
packages.extend(CATEGORIES[category])
elif tab_index == 1: # 手动输入
packages = [self.manual_libs_list.item(i).text()
for i in range(self.manual_libs_list.count())]
elif tab_index == 2: # 文件导入
packages = [self.file_libs_list.item(i).text()
for i in range(self.file_libs_list.count())]
elif tab_index == 3: # 预设组合
for preset, cb in self.preset_checkboxes.items():
if cb.isChecked() and preset in PRESETS:
packages.extend(PRESETS[preset])
return list(dict.fromkeys(packages))
def start_installation(self):
# 验证Python路径
self.python_path = self.python_path_edit.text().strip()
if not self.python_path:
QMessageBox.warning(self, "路径错误", "请指定Python安装路径")
return
if not os.path.exists(os.path.join(self.python_path, "python.exe")):
QMessageBox.warning(
self, "路径无效",
f"所选路径不包含python.exe: {self.python_path}"
)
return
packages = self.get_selected_packages()
if not packages:
QMessageBox.warning(self, "提示", "请先选择要安装的库")
return
reply = QMessageBox.question(
self, "确认安装",
f"即将使用 {os.path.basename(self.python_path)} 安装 {len(packages)} 个库,是否继续?",
QMessageBox.Yes | QMessageBox.No, QMessageBox.No
)
if reply != QMessageBox.Yes:
return
self.install_btn.setEnabled(False)
self.cancel_btn.setEnabled(True)
self.progress_bar.setValue(0)
self.log_text.append(f"\n===== 开始安装 {len(packages)} 个库 =====")
self.log_text.append(f"使用Python路径: {self.python_path}")
# 传递Python路径给安装线程
self.install_worker = InstallWorker(packages, self.python_path)
self.install_worker.progress_updated.connect(self.update_progress)
self.install_worker.log_updated.connect(self.append_log)
self.install_worker.install_finished.connect(self.installation_finished)
self.install_worker.start()
def cancel_installation(self):
if self.install_worker and self.install_worker.isRunning():
reply = QMessageBox.question(
self, "确认取消",
"确定要取消安装吗?",
QMessageBox.Yes | QMessageBox.No, QMessageBox.No
)
if reply == QMessageBox.Yes:
self.log_text.append("正在取消安装...")
self.install_worker.stop()
self.cancel_btn.setEnabled(False)
def update_progress(self, value: int, text: str):
self.progress_bar.setValue(value)
self.progress_label.setText(text)
def append_log(self, text: str):
self.log_text.append(text)
self.log_text.moveCursor(self.log_text.textCursor().End)
def clear_log(self):
self.log_text.clear()
# 清空日志后重新显示当前Python路径信息
self.append_python_path_to_log()
def installation_finished(self, results: Dict):
self.install_btn.setEnabled(True)
self.cancel_btn.setEnabled(False)
self.progress_label.setText("安装完成")
self.log_text.append("\n===== 安装结果汇总 =====")
self.log_text.append(f"总任务数:{results['total']}")
self.log_text.append(f"成功安装:{len(results['success'])}")
self.log_text.append(f"已安装跳过:{len(results['skipped'])}")
self.log_text.append(f"安装失败:{len(results['failed'])}")
if results['failed']:
self.log_text.append("\n失败的库(前10个):")
for fail in results['failed'][:10]:
self.log_text.append(f" - {fail}")
if len(results['failed']) > 10:
self.log_text.append(f" - ... 还有 {len(results['failed']) - 10} 个失败项")
if results['failed']:
QMessageBox.information(
self, "安装完成",
f"安装已完成,成功 {len(results['success'])} 个,失败 {len(results['failed'])} 个"
)
else:
QMessageBox.information(
self, "安装完成",
"所有库均安装成功!"
)
if __name__ == "__main__":
# 确保中文显示正常
font = QFont("SimHei")
app = QApplication(sys.argv)
app.setFont(font)
window = LibraryInstallerGUI()
sys.exit(app.exec_())
自动安装库GUI版0.5版.zip