Python库/包/模块管理工具
第三方库/包/模块,这些库通常发布在PyPI(Python Package Index)上。用户输入要安装的库名,工具通过pip从PyPI或镜像源下载并安装这些库/包/模块到指定的Python环境中。
【在Python中,通常所说的模块(Module)、包(Package)、库(Library)是有明确含义的:
模块:一个模块就是一个以.py为后缀的Python文件。
包:含多个模块的目录。包可以有多级,形成层次结构。
库:通常指一组相关的模块和包的集合,提供特定功能,提供完整功能。
有些库可能只有一个模块(单个文件),但大多数库都是包(包含多个模块)的形式。
公司组织比喻
• 模块 = 单个员工
• 包 = 部门(多个员工)
• 库 = 整个公司(多个部门协同工作)
日常交流中有时会混用。我们说的"安装库"指的是通过pip安装第三方库。
第三方库指的是不是由Python官方团队开发,而是由其他开发者、公司或社区创建的Python库。
三个"方"的概念含义:
• 第一方:Python官方 (python.org),提供的内置库如os, sys, math,Python自带。
• 第二方:你自己或你的团队,自己写的.py文件。
• 第三方:其他独立开发者或组织,提供的专业化功能解决方案,极大地扩展了Python的功能,正是因为有了丰富的第三方库生态系统,Python才能成为如此强大和流行的编程语言!】
pip 是 Python 的库/包/模块管理命令行工具,用于安装、管理、更新和卸载 Python 库/包/模块。用于第三方库的安装和管理过程,是 Python 开发中不可或缺的工具。但对于初学者来说,不太容易。因此设计一个pip图形化(GUI界面)辅助管理工具,提高Python库/包/模块管理的效率和成功率。
以前设计过一个,可见:Python之pip图形化(GUI界面)辅助管理工具https://blog.csdn.net/cnds123/article/details/147744623
现在设计一个功能更完善的界面更美观【界面布局、代码部分借鉴自网络】:
支持批量安装:支持逗号分隔输入多个库名。
集成7个国内镜像源(清华、阿里云、腾讯云等),失败时自动切换镜像源,最多重试3次。安全与稳定性处理更健壮,并在程序所在的目录自动创建的详细的日志系统(package_installer_gui.log)。
容错设计,解决了输错包名的问题:在用户点击"添加到安装列表"按钮时,对输入的每个包名进行验证,只将有效的包名添加到列表中,无效的给出明确提示。
这个工具,利用了多个模块/包/库:
标准库(不需要额外安装):
sys, subprocess, logging, time, os, glob, typing, re(在验证函数中动态导入)
第三方库(需要安装):
PyQt6
requests(在包名验证函数中动态导入)
matplotlib(在main函数中用于中文字体设置)
新版Python库/包/模块管理工具运行截图:

源码如下:
python
import sys
import subprocess
import logging
import time
import os
import glob
from typing import List, Dict, Optional, Tuple
from PyQt6.QtWidgets import (QApplication, QMainWindow, QTabWidget, QWidget, QVBoxLayout,
QHBoxLayout, QLabel, QPushButton, QListWidget, QListWidgetItem,
QTextEdit, QProgressBar, QFileDialog, QLineEdit, QGroupBox,
QCheckBox, QMessageBox, QScrollArea, QSplitter, QComboBox,
QMenuBar, QMenu, QTextBrowser, QTableWidget,
QTableWidgetItem, QHeaderView, QAbstractItemView, QFrame,
QRadioButton, QInputDialog)
from PyQt6.QtCore import Qt, QThread, pyqtSignal, QTimer
from PyQt6.QtGui import QFont, QColor, QIcon, QPalette, QAction, QTextCursor
# ==================== 配置与常量定义 ====================
# 日志配置
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler("package_installer_gui.log", encoding='utf-8'),
]
)
# 扩展所有国内镜像源
MIRRORS = {
"默认(PyPI)": None,
"清华": "https://pypi.tuna.tsinghua.edu.cn/simple",
"阿里": "https://mirrors.aliyun.com/pypi/simple/",
"腾讯云": "https://mirrors.cloud.tencent.com/pypi/simple/",
"中科大": "https://pypi.mirrors.ustc.edu.cn/simple/",
"华中科大": "http://pypi.hustunique.com/",
"豆瓣": "http://pypi.douban.com/simple/"
}
# 安装配置
MAX_RETRIES = 3 # 最大重试次数
RETRY_DELAY = 3 # 重试延迟(秒)
# 样式常量 - 现代美观的配色
PRIMARY_COLOR = "#165DFF" # 主色调:现代蓝色
SECONDARY_COLOR = "#69b1ff" # 次要色:浅蓝色
ACCENT_COLOR = "#36D399" # 强调色:绿色(成功)
WARNING_COLOR = "#FFAB00" # 警告色:橙色
DANGER_COLOR = "#F87272" # 危险色:红色(错误/取消)
LIGHT_COLOR = "#F9FAFB" # 浅色背景(更浅的灰色)
DARK_COLOR = "#1F2937" # 深色文本(深灰而非纯黑)
GRAY_COLOR = "#E5E7EB" # 灰色背景
BORDER_COLOR = "#D1D5DB" # 边框色(更浅的灰色)
# ==================== 核心安装与更新功能 ====================
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, update_mode: bool = False, uninstall_mode: bool = False):
super().__init__()
self.packages = packages
self.python_path = python_path # 接收Python路径参数
self.update_mode = update_mode # 是否为更新模式
self.uninstall_mode = uninstall_mode # 是否为卸载模式
self.is_running = True
def run(self):
try:
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.uninstall_mode and 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)
if self.uninstall_mode:
action = "卸载"
else:
action = "更新" if self.update_mode else "安装"
self.progress_updated.emit(progress, f"正在{action}: {package}")
result = self.process_single_package(package)
status = result["status"]
reason = result["reason"]
if status == "success":
results["success"].append(package)
self.log_updated.emit(f"✅ 成功{action}: {package}")
elif status == "skipped":
results["skipped"].append(package)
self.log_updated.emit(f"⏭️ 跳过{action}: {package}")
else:
results["failed"].append(f"{package} ({reason[:50]})")
self.log_updated.emit(f"❌ {action}失败: {package} ({reason[:50]})")
self.install_finished.emit(results)
except Exception as e:
logging.error(f"安装过程中发生未预期错误: {str(e)}")
self.log_updated.emit(f"⚠️ 安装过程中发生错误: {str(e)}")
# 发送空结果表示失败
self.install_finished.emit({
"success": [],
"failed": self.packages,
"skipped": [],
"total": len(self.packages)
})
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:
# 隐藏控制台窗口的启动参数
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
startupinfo.wShowWindow = subprocess.SW_HIDE
result = subprocess.run(
command,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding='utf-8',
timeout=300,
startupinfo=startupinfo # 隐藏控制台窗口
)
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 get_package_versions(self, package: str) -> Tuple[str, str]:
"""获取包的当前版本和最新版本"""
pure_package = package.split("[")[0]
current_version = "未知"
latest_version = "未知"
# 获取当前版本
try:
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
startupinfo.wShowWindow = subprocess.SW_HIDE
result = subprocess.run(
[os.path.join(self.python_path, "python.exe"), "-m", "pip", "show", pure_package],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding='utf-8',
timeout=10,
startupinfo=startupinfo # 隐藏控制台窗口
)
if result.returncode == 0:
for line in result.stdout.splitlines():
if line.startswith("Version:"):
current_version = line.split(":", 1)[1].strip()
break
except Exception as e:
logging.error(f"获取 {pure_package} 当前版本时出错: {str(e)}")
# 获取最新版本
try:
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
startupinfo.wShowWindow = subprocess.SW_HIDE
result = subprocess.run(
[os.path.join(self.python_path, "python.exe"), "-m", "pip", "index", "versions", pure_package],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding='utf-8',
timeout=15,
startupinfo=startupinfo # 隐藏控制台窗口
)
if result.returncode == 0:
lines = result.stdout.splitlines()
for line in lines:
if "可用版本:" in line:
versions = line.split("可用版本:")[1].strip()
latest_version = versions.split(",")[0].strip()
break
except Exception as e:
logging.warning(f"获取 {pure_package} 最新版本时出错: {str(e)}")
return (current_version, latest_version)
def process_single_package(self, package: str) -> Dict:
"""处理单个包的安装、更新或卸载"""
try:
result = {
"package": package,
"status": "failed",
"reason": ""
}
pure_package = package.split("[")[0]
# 卸载模式
if self.uninstall_mode:
if not self.is_package_installed(pure_package):
result["status"] = "skipped"
result["reason"] = "未安装,无法卸载"
return result
# 执行卸载命令
for retry in range(MAX_RETRIES):
if not self.is_running:
result["reason"] = "操作已取消"
return result
uninstall_cmd = [
os.path.join(self.python_path, "python.exe"),
"-m", "pip", "uninstall", "-y", pure_package
]
desc = f"卸载 {pure_package}(重试:{retry + 1}/{MAX_RETRIES})"
success, output = self.run_command(uninstall_cmd, desc)
if success and not self.is_package_installed(pure_package):
result["status"] = "success"
result["reason"] = "卸载成功"
return result
elif success:
result["reason"] = "卸载命令成功,但验证失败"
else:
result["reason"] = "卸载失败"
if retry < MAX_RETRIES - 1:
time.sleep(RETRY_DELAY)
result["reason"] = f"已尝试所有重试次数"
return result
# 更新模式
if self.update_mode:
if not self.is_package_installed(package):
result["status"] = "skipped"
result["reason"] = "未安装,无法更新"
return result
# 安装模式
if not self.update_mode and self.is_package_installed(package):
result["status"] = "skipped"
result["reason"] = "已安装,如需更新请使用更新功能"
return result
for retry in range(MAX_RETRIES):
if not self.is_running:
result["reason"] = "操作已取消"
return result
# 尝试所有镜像源
for mirror_name, mirror_url in MIRRORS.items():
# 构建命令,更新模式使用--upgrade
if self.update_mode:
install_cmd = [
os.path.join(self.python_path, "python.exe"),
"-m", "pip", "install", "--upgrade", pure_package
]
else:
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])
action = "更新" if self.update_mode else "安装"
desc = f"{action} {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"] = f"{action}成功(使用镜像:{mirror_name})"
return result
elif success:
result["reason"] = f"{action}命令成功,但验证失败"
else:
result["reason"] = f"{action}失败"
if retry < MAX_RETRIES - 1:
time.sleep(RETRY_DELAY)
result["reason"] = f"已尝试所有镜像源和重试次数"
return result
except Exception as e:
logging.error(f"处理包 {package} 时发生错误: {str(e)}")
result["status"] = "failed"
result["reason"] = f"处理过程中发生错误: {str(e)}"
return result
# ==================== 已安装库检查线程 ====================
class InstalledPackagesWorker(QThread):
progress_updated = pyqtSignal(int, str) # 进度值, 状态文本
packages_updated = pyqtSignal(list) # 包列表
finished = pyqtSignal() # 完成信号
def __init__(self, python_path: str):
super().__init__()
self.python_path = python_path
self.is_running = True
def run(self):
"""获取已安装的包列表"""
try:
self.progress_updated.emit(0, "正在获取已安装的包列表...")
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
startupinfo.wShowWindow = subprocess.SW_HIDE
result = subprocess.run(
[os.path.join(self.python_path, "python.exe"), "-m", "pip", "list", "--format=freeze"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding='utf-8',
timeout=30,
startupinfo=startupinfo
)
if result.returncode == 0:
packages = []
lines = result.stdout.strip().split('\n')
total = len(lines)
for i, line in enumerate(lines):
if not self.is_running:
break
if '==' in line:
package, version = line.split('==', 1)
packages.append({"name": package.strip(), "version": version.strip()})
progress = int((i + 1) / total * 100)
self.progress_updated.emit(progress, f"处理包: {package if 'package' in locals() else ''}")
self.packages_updated.emit(packages)
else:
self.progress_updated.emit(100, "获取包列表失败")
except Exception as e:
logging.error(f"获取已安装包列表时出错: {str(e)}")
self.progress_updated.emit(100, f"错误: {str(e)}")
self.finished.emit()
def stop(self):
self.is_running = False
# ==================== Python检测线程 ====================
class PythonDetectionWorker(QThread):
detection_finished = pyqtSignal(list) # 检测到的Python安装列表
def run(self):
"""检测系统中安装的Python版本,增强版检测逻辑"""
python_paths = []
# 常见的Python安装路径,扩展更多可能的位置
search_paths = [
"C:\\Python*",
"C:\\Program Files\\Python*",
"C:\\Program Files (x86)\\Python*",
os.path.expanduser("~") + "\\AppData\\Local\\Programs\\Python\\Python*",
os.path.expanduser("~") + "\\Anaconda*",
os.path.expanduser("~") + "\\Miniconda*",
"C:\\ProgramData\\Anaconda*",
"C:\\ProgramData\\Miniconda*",
"D:\\Python*", # 其他盘符
"E:\\Python*",
"F:\\Python*"
]
# 搜索常见路径
for path_pattern in search_paths:
for path in glob.glob(path_pattern):
# 检查是否包含python.exe
if os.path.exists(os.path.join(path, "python.exe")):
if path not in python_paths:
python_paths.append(path)
# 检查环境变量中的Python路径
try:
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
startupinfo.wShowWindow = subprocess.SW_HIDE
env_python = subprocess.check_output(
["where", "python"],
shell=True,
text=True,
timeout=10,
startupinfo=startupinfo # 隐藏控制台窗口
).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
# 检查py launcher (py.exe) 能找到的Python版本
try:
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
startupinfo.wShowWindow = subprocess.SW_HIDE
py_versions = subprocess.check_output(
["py", "-0p"],
shell=True,
text=True,
timeout=10,
startupinfo=startupinfo # 隐藏控制台窗口
).splitlines()
for line in py_versions:
if "Python" in line and "Installed at" in line:
path = line.split("Installed at:")[-1].strip()
if path and os.path.exists(os.path.join(path, "python.exe")) and path not in python_paths:
python_paths.append(path)
except:
pass
self.detection_finished.emit(python_paths)
# ==================== 主界面实现 ====================
class LibraryInstallerGUI(QMainWindow):
def __init__(self):
super().__init__()
self.install_worker = None
self.installed_packages_worker = None
self.detection_worker = None
self.python_path = self.get_default_python_path() # 默认Python路径
self.init_ui()
self.apply_styles() # 应用样式
self.start_python_detection() # 启动Python检测
self.python_installed = False # Python安装状态
self.detected_python_versions = {} # 存储检测到的Python版本信息
self.installed_packages = [] # 存储已安装的包列表
def init_ui(self):
"""初始化UI界面,包含增强的Python检测功能"""
self.setWindowTitle("Python库管理工具")
self.setGeometry(100, 100, 1000, 950) # 窗口初始大小
self.setMinimumSize(1000, 950) # 最小窗口大小
# 创建菜单栏和帮助菜单
self.create_menu_bar()
# 创建主部件和布局
central_widget = QWidget()
self.setCentralWidget(central_widget)
main_layout = QVBoxLayout(central_widget)
main_layout.setContentsMargins(8, 8, 8, 8)
main_layout.setSpacing(8)
# 添加Python安装状态提示标签 - 增强版
self.python_status_label = QLabel("正在检测Python安装...")
self.python_status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.python_status_label.setStyleSheet(f"padding: 5px; border-radius: 4px;")
main_layout.addWidget(self.python_status_label)
# 添加Python路径选择区域
python_group = QGroupBox("Python安装路径 (需指向包含python.exe的目录)")
python_layout = QVBoxLayout(python_group)
python_layout.setContentsMargins(8, 8, 8, 8)
python_layout.setSpacing(5)
# 路径选择布局
path_layout = QHBoxLayout()
path_layout.setSpacing(5)
self.python_path_edit = QLineEdit()
self.python_path_edit.setText(self.python_path)
# 路径变化时更新日志显示
self.python_path_edit.textChanged.connect(self.on_python_path_changed)
browse_btn = QPushButton("浏览...")
browse_btn.setMinimumHeight(28)
browse_btn.clicked.connect(self.browse_python_path)
find_btn = QPushButton("查找Python")
find_btn.setMinimumHeight(28)
find_btn.clicked.connect(self.start_python_detection)
path_layout.addWidget(self.python_path_edit, 5)
path_layout.addWidget(browse_btn, 1)
path_layout.addWidget(find_btn, 1)
# 已找到的Python版本列表
version_layout = QHBoxLayout()
version_layout.setSpacing(5)
version_layout.addWidget(QLabel("已检测到的Python版本:"))
self.python_versions_combo = QComboBox()
self.python_versions_combo.setToolTip("选择已检测到的Python版本")
self.python_versions_combo.setMinimumWidth(350)
self.python_versions_combo.currentIndexChanged.connect(self.on_python_version_selected)
version_layout.addWidget(self.python_versions_combo)
python_layout.addLayout(path_layout)
python_layout.addLayout(version_layout)
main_layout.addWidget(python_group)
# 添加分隔线增强视觉区分
line = QFrame()
line.setFrameShape(QFrame.Shape.HLine)
line.setFrameShadow(QFrame.Shadow.Sunken)
line.setStyleSheet(f"background-color: {BORDER_COLOR}; margin: 5px 0;")
main_layout.addWidget(line)
# 创建标题标签
title_label = QLabel("Python库管理工具")
title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
title_font = title_label.font()
title_font.setPointSize(14)
title_font.setBold(True)
title_label.setFont(title_font)
title_label.setStyleSheet(f"color: {PRIMARY_COLOR}; margin: 5px 0;")
main_layout.addWidget(title_label)
# 创建标签页控件
self.tabs = QTabWidget()
self.tabs.setElideMode(Qt.TextElideMode.ElideRight) # 标签文本过长时省略
self.tabs.setDocumentMode(True) # 文档模式,更现代
self.tabs.setMinimumHeight(400)
# 添加标签页
self.create_installed_tab() # 已安装库管理
self.create_manual_tab() # 手动安装
self.create_mirror_tab() # 镜像源管理
self.create_help_tab() # 帮助标签页
main_layout.addWidget(self.tabs)
# 创建进度区域
progress_group = QGroupBox("操作进度")
progress_layout = QVBoxLayout(progress_group)
progress_layout.setContentsMargins(8, 8, 8, 8)
progress_layout.setSpacing(5)
# 进度信息
status_layout = QHBoxLayout()
status_layout.setSpacing(5)
self.progress_label = QLabel("准备就绪")
self.progress_label.setMinimumWidth(150)
self.progress_bar = QProgressBar()
self.progress_bar.setMinimumHeight(22)
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)
log_layout.setContentsMargins(8, 8, 8, 8)
log_layout.setSpacing(5)
self.log_text = QTextEdit()
self.log_text.setReadOnly(True)
self.log_text.setLineWrapMode(QTextEdit.LineWrapMode.WidgetWidth)
self.log_text.setMinimumHeight(120)
log_layout.addWidget(self.log_text)
main_layout.addWidget(log_group)
# 创建底部按钮区域 - 只保留清空日志按钮
buttons_layout = QHBoxLayout()
buttons_layout.setSpacing(8)
self.clear_log_btn = QPushButton("清空日志")
self.clear_log_btn.setMinimumHeight(30)
self.clear_log_btn.setMinimumWidth(90)
buttons_layout.addStretch()
buttons_layout.addWidget(self.clear_log_btn)
# 连接按钮信号
self.clear_log_btn.clicked.connect(self.clear_log)
main_layout.addLayout(buttons_layout)
self.show()
def create_menu_bar(self):
"""创建菜单栏,包含帮助选项"""
menubar = self.menuBar()
# 创建帮助菜单
help_menu = menubar.addMenu('帮助')
# 使用帮助动作
usage_action = QAction('使用帮助', self)
usage_action.triggered.connect(self.show_help)
help_menu.addAction(usage_action)
# 关于动作
about_action = QAction('关于', self)
about_action.triggered.connect(self.show_about)
help_menu.addAction(about_action)
# Python下载动作 - 新增
python_download_action = QAction('下载Python', self)
python_download_action.triggered.connect(self.open_python_download)
help_menu.addAction(python_download_action)
def create_installed_tab(self):
"""创建已安装库管理标签页"""
tab = QWidget()
layout = QVBoxLayout(tab)
layout.setContentsMargins(8, 8, 8, 8)
layout.setSpacing(8)
# 操作按钮区域
buttons_group = QGroupBox("库管理操作")
buttons_layout = QHBoxLayout(buttons_group)
buttons_layout.setContentsMargins(8, 8, 8, 8)
buttons_layout.setSpacing(5)
self.check_btn = QPushButton("检查已安装库")
self.check_btn.setMinimumHeight(28)
self.check_btn.clicked.connect(self.check_installed_packages)
self.update_btn = QPushButton("更新选中库")
self.update_btn.setMinimumHeight(28)
self.update_btn.clicked.connect(self.update_selected_packages)
self.uninstall_btn = QPushButton("卸载选中库")
self.uninstall_btn.setMinimumHeight(28)
self.uninstall_btn.clicked.connect(self.uninstall_selected_packages)
self.find_btn = QPushButton("查找库")
self.find_btn.setMinimumHeight(28)
self.find_btn.clicked.connect(self.find_package)
buttons_layout.addWidget(self.check_btn)
buttons_layout.addWidget(self.update_btn)
buttons_layout.addWidget(self.uninstall_btn)
buttons_layout.addWidget(self.find_btn)
layout.addWidget(buttons_group)
# 已安装库列表
list_group = QGroupBox("已安装的库")
list_layout = QVBoxLayout(list_group)
list_layout.setContentsMargins(8, 8, 8, 8)
self.installed_list = QListWidget()
self.installed_list.setAlternatingRowColors(True)
self.installed_list.itemSelectionChanged.connect(self.on_package_selection_changed)
list_layout.addWidget(self.installed_list)
layout.addWidget(list_group)
self.tabs.addTab(tab, "已安装")
def create_manual_tab(self):
"""创建手动安装标签页"""
tab = QWidget()
layout = QVBoxLayout(tab)
layout.setContentsMargins(8, 8, 8, 8)
layout.setSpacing(8)
# 输入框区域
input_group = QGroupBox("安装新库 (用逗号分隔)")
input_layout = QVBoxLayout(input_group)
input_layout.setContentsMargins(8, 8, 8, 8)
input_layout.setSpacing(5)
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.setMinimumHeight(28)
add_btn.clicked.connect(self.add_manual_libs)
layout.addWidget(add_btn)
# 已添加的库列表
list_group = QGroupBox("待安装的库")
list_inner_layout = QVBoxLayout(list_group)
list_inner_layout.setContentsMargins(8, 8, 8, 8)
self.manual_libs_list = QListWidget()
self.manual_libs_list.setAlternatingRowColors(True)
list_inner_layout.addWidget(self.manual_libs_list)
layout.addWidget(list_group)
# 操作按钮区域 - 将开始安装和取消操作按钮移到这里
action_buttons_layout = QHBoxLayout()
action_buttons_layout.setSpacing(5)
self.install_btn = QPushButton("开始安装")
self.install_btn.setMinimumHeight(28)
self.install_btn.setEnabled(False) # 初始禁用,检测到Python后启用
self.install_btn.clicked.connect(self.start_installation)
self.cancel_btn = QPushButton("取消操作")
self.cancel_btn.setMinimumHeight(28)
self.cancel_btn.setEnabled(False)
self.cancel_btn.clicked.connect(self.cancel_installation)
remove_btn = QPushButton("移除选中项")
remove_btn.setMinimumHeight(28)
remove_btn.clicked.connect(self.remove_manual_lib)
action_buttons_layout.addWidget(self.install_btn)
action_buttons_layout.addWidget(self.cancel_btn)
action_buttons_layout.addWidget(remove_btn)
layout.addLayout(action_buttons_layout)
self.tabs.addTab(tab, "手动安装")
def create_mirror_tab(self):
"""创建镜像源管理标签页"""
tab = QWidget()
layout = QVBoxLayout(tab)
layout.setContentsMargins(8, 8, 8, 8)
layout.setSpacing(8)
# 镜像源信息
mirror_group = QGroupBox("可用镜像源")
mirror_layout = QVBoxLayout(mirror_group)
mirror_layout.setContentsMargins(8, 8, 8, 8)
# 镜像源列表
self.mirror_list = QTableWidget()
self.mirror_list.setColumnCount(2)
self.mirror_list.setHorizontalHeaderLabels(["镜像源名称", "URL地址"])
self.mirror_list.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
self.mirror_list.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
# 填充镜像源数据
self.mirror_list.setRowCount(len(MIRRORS))
for row, (name, url) in enumerate(MIRRORS.items()):
name_item = QTableWidgetItem(name)
url_item = QTableWidgetItem(url if url else "https://pypi.org/simple/")
self.mirror_list.setItem(row, 0, name_item)
self.mirror_list.setItem(row, 1, url_item)
mirror_layout.addWidget(self.mirror_list)
# 镜像源说明
info_label = QLabel("""
<p><strong>镜像源说明:</strong></p>
<p>本工具会自动尝试所有镜像源来安装Python库,以提高安装成功率。</p>
<p>国内镜像源通常比官方源速度更快,尤其是在中国大陆地区。</p>
<p>如果某个镜像源暂时不可用,工具会自动切换到下一个镜像源重试。</p>
""")
info_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
mirror_layout.addWidget(info_label)
layout.addWidget(mirror_group)
self.tabs.addTab(tab, "镜像源管理")
def validate_package_name(self, package_name: str) -> Tuple[bool, str]:
"""
增强的简化版包名验证(带网络异常处理)
"""
try:
pure_name = package_name.split('==')[0].split('[')[0].strip()
if not pure_name:
return False, "包名不能为空"
# 基本字符检查
import re
if not re.match(r'^[a-zA-Z0-9_\-\.]+$', pure_name):
return False, "包名包含非法字符"
# 尝试网络验证
try:
import requests
response = requests.get(
f"https://pypi.org/pypi/{pure_name}/json",
timeout=3
)
if response.status_code == 200:
return True, "包名有效"
elif response.status_code == 404:
return False, f"包 '{pure_name}' 不存在"
else:
# HTTP错误,但仍然允许添加(可能是临时问题)
return True, f"验证API异常 (HTTP {response.status_code}),仍可尝试安装"
except (requests.exceptions.Timeout, requests.exceptions.ConnectionError):
# 网络问题,允许添加(交给安装时处理)
return True, "网络验证跳过,将尝试安装"
except Exception as network_error:
# 其他网络相关错误,允许添加
return True, f"验证异常: {str(network_error)},将尝试安装"
except Exception as e:
# 整体异常,保守策略:允许添加
return True, f"验证过程异常: {str(e)},将尝试安装"
def create_help_tab(self):
"""创建帮助标签页"""
tab = QWidget()
layout = QVBoxLayout(tab)
layout.setContentsMargins(8, 8, 8, 8)
layout.setSpacing(8)
# 使用QTextBrowser显示帮助内容
self.help_browser = QTextBrowser()
self.help_browser.setOpenExternalLinks(True) # 允许打开外部链接
self.help_browser.setMinimumHeight(300)
layout.addWidget(self.help_browser)
# 设置帮助内容
self.set_help_content()
self.tabs.addTab(tab, "使用帮助")
def set_help_content(self):
"""设置帮助内容"""
help_text = """
<h2>Python库管理工具 - 使用帮助</h2>
<h3>1. 概述</h3>
<p>本工具旨在帮助用户便捷地管理Python库,包括安装、更新、卸载和查看已安装的库。</p>
<h3>2. 选择Python路径</h3>
<p>在使用工具前,需要指定Python安装路径:</p>
<ul>
<li>工具会自动检测系统中安装的Python版本</li>
<li>可以从下拉列表中选择已检测到的Python版本</li>
<li>也可以通过"浏览"按钮手动选择Python安装目录</li>
<li>如果未安装Python,可以通过"帮助"菜单中的"下载Python"选项访问官网下载</li>
</ul>
<h3>3. 已安装库管理</h3>
<p>在"已安装"标签页中,您可以:</p>
<ul>
<li><strong>检查已安装库</strong>:扫描当前Python环境中已安装的所有库</li>
<li><strong>更新选中库</strong>:更新在列表中选中的库到最新版本</li>
<li><strong>卸载选中库</strong>:卸载在列表中选中的库</li>
<li><strong>查找库</strong>:在已安装库列表中查找指定名称的库</li>
</ul>
<h3>4. 手动安装</h3>
<p>在"手动安装"标签页中,您可以:</p>
<ul>
<li>输入要安装的库名称,多个库用逗号分隔,可指定版本,如numpy,requests==2.25.1</li>
<li>在用户点击"添加到安装列表"按钮时,对输入的每个包名进行验证,只将有效的包名添加到列表中</li>
<li>添加到安装列表后,点击"开始安装"按钮进行安装</li>
</ul>
<h3>5. 镜像源管理</h3>
<p>本工具已集成多个国内镜像源,安装过程中会自动尝试所有镜像源,提高安装成功率。</p>
<h3>6. 常见问题</h3>
<p><strong>Q: 安装失败怎么办?</strong><br>
A: 查看日志区域的错误信息,工具会自动尝试多个镜像源和重试,如仍失败可尝试手动安装。</p>
<p><strong>Q: 如何查看安装日志?</strong><br>
A: 日志会显示在底部的日志区域,同时也会保存到package_installer_gui.log文件中。</p>
<p><strong>Q: 工具支持虚拟环境吗?</strong><br>
A: 支持,只需在Python路径选择中选择虚拟环境的Python可执行文件所在目录即可。</p>
"""
self.help_browser.setHtml(help_text)
# ==================== 已安装库管理功能 ====================
def check_installed_packages(self):
"""检查已安装的库"""
if not self.verify_python_path_for_operation():
return
self.log_text.append("开始检查已安装的库...")
# 初始化UI状态
self.check_btn.setEnabled(False)
self.update_btn.setEnabled(False)
self.uninstall_btn.setEnabled(False)
self.progress_bar.setValue(0)
self.progress_label.setText("正在检查已安装库...")
# 启动检查线程
self.installed_packages_worker = InstalledPackagesWorker(self.python_path)
self.installed_packages_worker.progress_updated.connect(self.update_install_progress)
self.installed_packages_worker.packages_updated.connect(self.handle_installed_packages)
self.installed_packages_worker.finished.connect(self.installed_check_finished)
self.installed_packages_worker.start()
def handle_installed_packages(self, packages: list):
"""处理已安装的包列表"""
self.installed_packages = packages
self.installed_list.clear()
for package in packages:
item = QListWidgetItem(f"{package['name']} == {package['version']}")
self.installed_list.addItem(item)
self.log_text.append(f"已加载 {len(packages)} 个已安装的库")
def installed_check_finished(self):
"""已安装库检查完成"""
self.check_btn.setEnabled(True)
self.progress_bar.setValue(100)
self.progress_label.setText("检查完成")
self.log_text.append("已安装库检查完成")
def on_package_selection_changed(self):
"""当包选择改变时更新按钮状态"""
selected_items = self.installed_list.selectedItems()
has_selection = len(selected_items) > 0
self.update_btn.setEnabled(has_selection)
self.uninstall_btn.setEnabled(has_selection)
def update_selected_packages(self):
try:
selected_items = self.installed_list.selectedItems()
if not selected_items:
QMessageBox.information(self, "未选择", "请先选择要更新的库")
return
# 检查Python路径
if not self.verify_python_path_for_operation():
self.log_text.append("错误: Python路径无效,无法执行更新")
return
packages = []
for item in selected_items:
package_name = item.text().split(" == ")[0]
packages.append(package_name)
self.log_text.append(f"准备更新 {len(packages)} 个库...")
# 确保之前的worker已停止
if self.install_worker and self.install_worker.isRunning():
self.log_text.append("正在停止之前的操作...")
self.install_worker.stop()
self.install_worker.wait(2000) # 等待2秒
# 初始化UI状态
self.install_btn.setEnabled(False)
self.cancel_btn.setEnabled(True)
self.progress_bar.setValue(0)
# 启动安装线程(更新模式)
self.install_worker = InstallWorker(packages, self.python_path, update_mode=True)
self.install_worker.progress_updated.connect(self.update_install_progress)
self.install_worker.log_updated.connect(self.append_log)
self.install_worker.install_finished.connect(self.handle_install_finished)
self.install_worker.start()
self.log_text.append("更新线程已启动")
except Exception as e:
self.log_text.append(f"更新操作异常: {str(e)}")
logging.error(f"更新操作异常: {str(e)}", exc_info=True)
QMessageBox.critical(self, "错误", f"更新操作发生异常: {str(e)}")
def uninstall_selected_packages(self):
"""卸载选中的包"""
selected_items = self.installed_list.selectedItems()
if not selected_items:
QMessageBox.information(self, "未选择", "请先选择要卸载的库")
return
packages = []
for item in selected_items:
package_name = item.text().split(" == ")[0]
packages.append(package_name)
reply = QMessageBox.question(
self, "确认卸载",
f"确定要卸载以下 {len(packages)} 个库吗?\n" + "\n".join(packages),
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.Yes:
self.log_text.append(f"准备卸载 {len(packages)} 个库...")
# 初始化UI状态
self.install_btn.setEnabled(False)
self.cancel_btn.setEnabled(True)
self.progress_bar.setValue(0)
# 启动安装线程(卸载模式)
self.install_worker = InstallWorker(packages, self.python_path, uninstall_mode=True)
self.install_worker.progress_updated.connect(self.update_install_progress)
self.install_worker.log_updated.connect(self.append_log)
self.install_worker.install_finished.connect(self.handle_install_finished)
self.install_worker.start()
def find_package(self):
"""查找包"""
package_name, ok = QInputDialog.getText(
self, "查找库", "请输入库名称(支持模糊匹配):"
)
if ok and package_name:
# 在列表中查找匹配的项
found = False
for i in range(self.installed_list.count()):
item = self.installed_list.item(i)
if package_name.lower() in item.text().lower():
item.setSelected(True)
self.installed_list.scrollToItem(item)
found = True
# 只选择第一个匹配项
break
if not found:
QMessageBox.information(self, "查找结果", f"未找到包含 '{package_name}' 的库")
else:
self.log_text.append(f"已找到并选中包含 '{package_name}' 的库")
# ==================== 其他功能方法 ====================
def get_default_python_path(self) -> str:
"""获取当前运行环境的Python路径"""
try:
return os.path.dirname(sys.executable)
except:
return ""
def start_python_detection(self):
"""启动增强版Python检测线程"""
self.log_text.append("开始检测系统中的Python安装...")
self.python_status_label.setText("正在检测Python安装...")
self.python_status_label.setStyleSheet(
f"background-color: {WARNING_COLOR}; color: white; padding: 5px; border-radius: 4px;")
# 禁用相关按钮
self.python_versions_combo.setEnabled(False)
# 启动检测线程
self.detection_worker = PythonDetectionWorker()
self.detection_worker.detection_finished.connect(self.handle_detection_result)
self.detection_worker.start()
def handle_detection_result(self, python_paths: list):
"""处理Python检测结果,增强版提示"""
self.log_text.append(f"Python检测完成,共发现 {len(python_paths)} 个安装")
self.detected_python_versions = {}
# 更新下拉列表
self.python_versions_combo.clear()
if not python_paths:
# 未检测到Python安装 - 增强提示
self.python_installed = False
self.python_status_label.setText("未检测到Python安装,请先安装Python")
self.python_status_label.setStyleSheet(
f"background-color: {DANGER_COLOR}; color: white; padding: 5px; border-radius: 4px;")
self.log_text.append("警告: 未检测到Python安装,请先安装Python")
# 显示详细的安装提示对话框
reply = QMessageBox.warning(
self, "未安装Python",
"未检测到Python安装。\n\n"
"请从官网下载并安装Python:\n"
"https://www.python.org/downloads/\n\n"
"安装时请勾选'Add Python to PATH'选项,"
"这将使Python在命令行中可直接访问。\n\n"
"需要打开Python官网下载页面吗?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.Yes:
import webbrowser
webbrowser.open("https://www.python.org/downloads/")
# 禁用需要Python的功能按钮
self.install_btn.setEnabled(False)
self.check_btn.setEnabled(False)
else:
# 检测到Python安装
self.python_installed = True
self.python_status_label.setText(f"已检测到 {len(python_paths)} 个Python安装")
self.python_status_label.setStyleSheet(
f"background-color: {ACCENT_COLOR}; color: white; padding: 5px; border-radius: 4px;")
# 获取每个Python路径的详细版本信息
for path in python_paths:
try:
# 执行python --version命令获取版本
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
startupinfo.wShowWindow = subprocess.SW_HIDE
version_output = subprocess.check_output(
[os.path.join(path, "python.exe"), "--version"],
stderr=subprocess.STDOUT, # python --version输出到stderr
text=True,
encoding='utf-8',
timeout=10,
startupinfo=startupinfo # 隐藏控制台窗口
).strip()
# 执行python -V获取更详细的版本信息
detailed_version = subprocess.check_output(
[os.path.join(path, "python.exe"), "-V"],
stderr=subprocess.STDOUT,
text=True,
encoding='utf-8',
timeout=10,
startupinfo=startupinfo # 隐藏控制台窗口
).strip()
# 存储版本信息
full_version = f"{version_output} ({detailed_version})"
self.detected_python_versions[path] = full_version
# 添加到下拉列表
self.python_versions_combo.addItem(f"{full_version} - {path}", path)
self.log_text.append(f"发现Python版本: {version_output} 在 {path}")
except Exception as e:
self.log_text.append(f"获取Python版本信息失败 for {path}: {str(e)}")
self.python_versions_combo.addItem(f"Python (版本未知) - {path}", path)
# 尝试自动选择一个Python版本
if self.python_path and self.python_path in self.detected_python_versions:
# 找到当前路径在下拉列表中的索引
for i in range(self.python_versions_combo.count()):
if self.python_versions_combo.itemData(i) == self.python_path:
self.python_versions_combo.setCurrentIndex(i)
break
else:
# 默认选择第一个
self.python_versions_combo.setCurrentIndex(0)
if self.python_versions_combo.count() > 0:
self.python_path = self.python_versions_combo.itemData(0)
self.python_path_edit.setText(self.python_path)
# 启用相关按钮
self.install_btn.setEnabled(True)
self.check_btn.setEnabled(True)
# 启用下拉列表
self.python_versions_combo.setEnabled(True)
self.detection_worker = None
# 显示Python路径信息
self.append_python_path_to_log()
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.Option.ShowDirsOnly | QFileDialog.Option.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
# 检查这个路径是否已经在下拉列表中
path_exists = False
for i in range(self.python_versions_combo.count()):
if self.python_versions_combo.itemData(i) == python_dir:
self.python_versions_combo.setCurrentIndex(i)
path_exists = True
break
# 如果不在下拉列表中,添加它
if not path_exists:
try:
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
startupinfo.wShowWindow = subprocess.SW_HIDE
version_output = subprocess.check_output(
[os.path.join(python_dir, "python.exe"), "--version"],
stderr=subprocess.STDOUT,
text=True,
encoding='utf-8',
timeout=10,
startupinfo=startupinfo # 隐藏控制台窗口
).strip()
self.python_versions_combo.addItem(f"{version_output} - {python_dir}", python_dir)
self.python_versions_combo.setCurrentIndex(self.python_versions_combo.count() - 1)
self.detected_python_versions[python_dir] = version_output
except:
self.python_versions_combo.addItem(f"Python - {python_dir}", python_dir)
self.python_versions_combo.setCurrentIndex(self.python_versions_combo.count() - 1)
# 更新日志显示
self.append_python_path_to_log()
# 清空相关UI
self.clear_ui_on_python_change()
else:
QMessageBox.warning(
self, "路径无效",
"所选目录不包含python.exe,请选择正确的Python安装目录"
)
def on_python_version_selected(self, index):
"""选择下拉列表中的Python版本"""
if index >= 0 and self.python_versions_combo.count() > 0:
python_path = self.python_versions_combo.itemData(index)
self.python_path_edit.setText(python_path)
self.python_path = python_path
# 显示版本信息
version = self.detected_python_versions.get(python_path, "未知版本")
self.python_status_label.setText(f"当前选择: {version}")
self.python_status_label.setStyleSheet(
f"background-color: {ACCENT_COLOR}; color: white; padding: 5px; border-radius: 4px;")
# 更新日志显示
self.append_python_path_to_log()
# 清空相关UI
self.clear_ui_on_python_change()
def on_python_path_changed(self):
"""当Python路径发生变化时更新日志"""
self.python_path = self.python_path_edit.text().strip()
self.append_python_path_to_log()
# 检查路径有效性
if os.path.exists(os.path.join(self.python_path, "python.exe")):
self.python_status_label.setText("Python路径有效")
self.python_status_label.setStyleSheet(
f"background-color: {ACCENT_COLOR}; color: white; padding: 5px; border-radius: 4px;")
self.install_btn.setEnabled(True)
self.check_btn.setEnabled(True)
# 切换Python版本时清空相关UI
self.clear_ui_on_python_change()
else:
self.python_status_label.setText("Python路径无效")
self.python_status_label.setStyleSheet(
f"background-color: {DANGER_COLOR}; color: white; padding: 5px; border-radius: 4px;")
self.install_btn.setEnabled(False)
self.check_btn.setEnabled(False)
# 路径无效时也清空UI
self.clear_ui_on_python_change()
def clear_ui_on_python_change(self):
"""切换Python版本时清空相关UI元素"""
# 清空已安装库列表
self.installed_list.clear()
self.installed_packages = []
# 重置进度条
self.progress_bar.setValue(0)
self.progress_label.setText("准备就绪")
# 重置已安装库管理按钮状态
self.update_btn.setEnabled(False)
self.uninstall_btn.setEnabled(False)
# 添加日志记录
self.log_text.append("Python路径已更改,已清空相关数据")
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:
# 获取并显示Python版本
try:
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
startupinfo.wShowWindow = subprocess.SW_HIDE
version_output = subprocess.check_output(
[os.path.join(self.python_path, "python.exe"), "--version"],
stderr=subprocess.STDOUT,
text=True,
encoding='utf-8',
timeout=10,
startupinfo=startupinfo # 隐藏控制台窗口
).strip()
self.log_text.append(f"Python版本: {version_output}")
except:
self.log_text.append("无法获取Python版本信息")
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};
font-size: 12px;
}}
/* 标签页样式 */
QTabWidget::pane {{
border: 1px solid {BORDER_COLOR};
border-radius: 6px;
background-color: white;
padding: 10px;
box-shadow: inset 0 1px 3px rgba(0,0,0,0.05);
}}
QTabBar::tab {{
background-color: {LIGHT_COLOR};
color: {DARK_COLOR};
padding: 6px 14px;
border: 1px solid {BORDER_COLOR};
border-bottom-color: {BORDER_COLOR};
border-radius: 4px 4px 0 0;
margin-right: 1px;
font-size: 12px;
}}
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: 8px;
padding: 8px 10px;
background-color: white;
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
}}
QGroupBox::title {{
color: {PRIMARY_COLOR};
subcontrol-origin: margin;
left: 5px;
padding: 0 3px 0 3px;
font-weight: bold;
font-size: 12px;
}}
/* 按钮样式 */
QPushButton {{
background-color: {PRIMARY_COLOR};
color: white;
border: none;
border-radius: 4px;
padding: 5px 12px;
font-size: 12px;
transition: all 0.2s ease;
}}
QPushButton:hover {{
background-color: {SECONDARY_COLOR};
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}}
QPushButton:pressed {{
background-color: {PRIMARY_COLOR};
transform: translateY(0);
box-shadow: none;
}}
QPushButton:disabled {{
background-color: {GRAY_COLOR};
color: #999;
}}
/* 特殊按钮样式 */
QPushButton#installBtn {{
background-color: {ACCENT_COLOR};
}}
QPushButton#installBtn:hover {{
background-color: #2DB886;
}}
QPushButton#cancelBtn {{
background-color: {DANGER_COLOR};
}}
QPushButton#cancelBtn:hover {{
background-color: #E6645F;
}}
QPushButton#updateBtn {{
background-color: #FFAB00;
}}
QPushButton#updateBtn:hover {{
background-color: #E69E00;
}}
/* 列表和表格样式 */
QListWidget, QTableWidget {{
border: 1px solid {BORDER_COLOR};
border-radius: 4px;
background-color: white;
padding: 5px;
alternate-background-color: {LIGHT_COLOR};
font-size: 12px;
}}
QListWidget::item {{
padding: 3px;
border-radius: 2px;
min-height: 20px;
}}
QListWidget::item:selected, QTableWidget::item:selected {{
background-color: rgba(22, 93, 255, 0.15);
color: {PRIMARY_COLOR};
border-radius: 2px;
}}
QListWidget::item:hover, QTableWidget::item:hover {{
background-color: {GRAY_COLOR};
}}
/* 进度条样式 */
QProgressBar {{
border: 1px solid {BORDER_COLOR};
border-radius: 4px;
text-align: center;
height: 20px;
font-size: 11px;
}}
QProgressBar::chunk {{
background-color: {PRIMARY_COLOR};
border-radius: 3px;
width: 10px;
margin: 0.5px;
}}
/* 文本编辑框样式 */
QTextEdit, QTextBrowser {{
border: 1px solid {BORDER_COLOR};
border-radius: 4px;
background-color: white;
padding: 6px;
font-family: "Consolas", "Monaco", monospace;
font-size: 11px;
line-height: 1.5;
}}
/* 输入框样式 */
QLineEdit {{
border: 1px solid {BORDER_COLOR};
border-radius: 4px;
padding: 5px 8px;
background-color: white;
font-size: 12px;
transition: border-color 0.2s ease;
}}
QLineEdit:focus {{
border-color: {PRIMARY_COLOR};
outline: none;
box-shadow: 0 0 0 2px rgba(22, 93, 255, 0.2);
}}
/* 复选框样式 */
QCheckBox {{
padding: 3px;
font-size: 12px;
}}
QCheckBox:hover {{
color: {PRIMARY_COLOR};
}}
/* 单选按钮样式 */
QRadioButton {{
padding: 3px;
font-size: 12px;
}}
QRadioButton:hover {{
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 8px;
background-color: white;
font-size: 12px;
transition: border-color 0.2s ease;
}}
QComboBox:focus {{
border-color: {PRIMARY_COLOR};
box-shadow: 0 0 0 2px rgba(22, 93, 255, 0.2);
}}
/* 滚动条样式 */
QScrollBar:vertical {{
border: none;
background: {LIGHT_COLOR};
width: 8px;
margin: 0px;
}}
QScrollBar::handle:vertical {{
background: {SECONDARY_COLOR};
min-height: 15px;
border-radius: 4px;
}}
QScrollBar:horizontal {{
border: none;
background: {LIGHT_COLOR};
height: 8px;
margin: 0px;
}}
QScrollBar::handle:horizontal {{
background: {SECONDARY_COLOR};
min-width: 15px;
border-radius: 4px;
}}
/* 菜单栏样式 */
QMenuBar {{
background-color: {LIGHT_COLOR};
color: {DARK_COLOR};
font-size: 12px;
}}
QMenuBar::item {{
background-color: {LIGHT_COLOR};
padding: 2px 6px;
}}
QMenuBar::item:selected {{
background-color: {SECONDARY_COLOR};
color: white;
}}
QMenu {{
background-color: white;
border: 1px solid {BORDER_COLOR};
font-size: 12px;
}}
QMenu::item {{
padding: 2px 15px;
}}
QMenu::item:selected {{
background-color: {SECONDARY_COLOR};
color: white;
}}
""")
# 设置按钮对象名以便应用特殊样式
if hasattr(self, 'install_btn'):
self.install_btn.setObjectName("installBtn")
if hasattr(self, 'cancel_btn'):
self.cancel_btn.setObjectName("cancelBtn")
if hasattr(self, 'update_btn'):
self.update_btn.setObjectName("updateBtn")
# ==================== 手动安装功能 ====================
def add_manual_libs(self):
"""添加手动输入的库(带验证功能)"""
text = self.manual_input.text().strip()
if not text:
QMessageBox.warning(self, "输入为空", "请输入库名称")
return
# 分割输入的库名
raw_libs = [lib.strip() for lib in text.split(',') if lib.strip()]
if not raw_libs:
QMessageBox.warning(self, "输入无效", "请输入有效的库名称")
return
# 验证并筛选有效的包名
valid_libs = []
invalid_libs = [] # 存储无效包名和原因
self.log_text.append("开始验证包名...")
for lib in raw_libs:
is_valid, message = self.validate_package_name(lib)
if is_valid:
valid_libs.append(lib)
self.log_text.append(f"✓ {lib} - 验证通过")
else:
invalid_libs.append((lib, message))
self.log_text.append(f"✗ {lib} - {message}")
# 去重检查(在现有列表中)
current_items = set()
for i in range(self.manual_libs_list.count()):
current_items.add(self.manual_libs_list.item(i).text())
# 只添加有效的且不重复的包名
added_count = 0
for lib in valid_libs:
if lib not in current_items:
self.manual_libs_list.addItem(lib)
current_items.add(lib)
added_count += 1
# 显示结果摘要
success_msg = f"已添加 {added_count} 个有效库到安装列表"
if invalid_libs:
# 显示无效包名的详细错误
error_details = "\n".join([f"• {lib}: {reason}" for lib, reason in invalid_libs])
QMessageBox.warning(
self,
"部分包名无效",
f"{success_msg}\n\n以下包名无效,已被忽略:\n{error_details}"
)
else:
self.log_text.append(success_msg)
# 如果所有包都有效且没有新添加的(都是重复的)
if added_count == 0 and valid_libs:
QMessageBox.information(self, "提示", "所有包名都已存在于安装列表中")
self.manual_input.clear()
def remove_manual_lib(self):
"""移除选中的手动添加库"""
selected_items = self.manual_libs_list.selectedItems()
if not selected_items:
QMessageBox.information(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 start_installation(self):
"""开始安装选中的库"""
# 收集所有要安装的库
to_install = set()
# 从手动输入标签页收集
for i in range(self.manual_libs_list.count()):
to_install.add(self.manual_libs_list.item(i).text())
to_install = list(to_install)
if not to_install:
QMessageBox.information(self, "未选择库", "请先选择要安装的库")
return
# 检查Python路径
if not self.verify_python_path_for_operation():
return
self.log_text.append(f"准备安装 {len(to_install)} 个库...")
# 初始化UI状态
self.install_btn.setEnabled(False)
self.cancel_btn.setEnabled(True)
self.progress_bar.setValue(0)
# 启动安装线程
self.install_worker = InstallWorker(to_install, self.python_path)
self.install_worker.progress_updated.connect(self.update_install_progress)
self.install_worker.log_updated.connect(self.append_log)
self.install_worker.install_finished.connect(self.handle_install_finished)
self.install_worker.start()
def update_install_progress(self, progress: int, status: str):
"""更新安装进度"""
self.progress_bar.setValue(progress)
self.progress_label.setText(status)
def append_log(self, text: str):
"""添加日志内容"""
self.log_text.append(text)
# 自动滚动到底部 - PyQt6 兼容版本
self.log_text.moveCursor(QTextCursor.MoveOperation.End)
# 同时写入日志文件
logging.info(text)
def handle_install_finished(self, results: dict):
"""处理安装完成事件"""
self.progress_bar.setValue(100)
self.progress_label.setText("操作完成")
self.install_btn.setEnabled(True)
self.cancel_btn.setEnabled(False)
self.install_worker = None
# 显示安装结果摘要
summary = (f"操作完成: 成功 {len(results['success'])}, "
f"失败 {len(results['failed'])}, "
f"跳过 {len(results['skipped'])}, "
f"总计 {results['total']}")
self.log_text.append(summary)
# 显示详细结果对话框
self.show_installation_summary(results)
def show_installation_summary(self, results: dict):
"""显示安装结果摘要对话框"""
msg = QMessageBox(self)
msg.setWindowTitle("操作完成")
if results['total'] == 0:
msg.setText("没有处理任何库")
msg.exec()
return
details = []
if results['success']:
details.append(f"✅ 成功 ({len(results['success'])}):")
details.append(", ".join(results['success'][:10]) + ("..." if len(results['success']) > 10 else ""))
if results['failed']:
details.append(f"\n❌ 失败 ({len(results['failed'])}):")
details.append(", ".join(results['failed'][:10]) + ("..." if len(results['failed']) > 10 else ""))
if results['skipped']:
details.append(f"\n⏭️ 跳过 ({len(results['skipped'])}):")
details.append(", ".join(results['skipped'][:10]) + ("..." if len(results['skipped']) > 10 else ""))
msg.setText(
f"操作已完成。成功: {len(results['success'])}, 失败: {len(results['failed'])}, 跳过: {len(results['skipped'])}")
msg.setDetailedText("\n".join(details))
msg.setIcon(QMessageBox.Icon.Information)
msg.exec()
def cancel_installation(self):
"""取消安装"""
if self.install_worker and self.install_worker.isRunning():
self.log_text.append("正在取消安装...")
self.install_worker.stop()
self.cancel_btn.setEnabled(False)
self.progress_label.setText("正在取消...")
def clear_log(self):
"""清空日志"""
self.log_text.clear()
self.log_text.append("日志已清空")
def verify_python_path_for_operation(self) -> bool:
try:
if not self.python_path or not self.python_path.strip():
QMessageBox.warning(self, "路径未设置", "请先设置Python安装路径")
return False
python_exe = os.path.join(self.python_path, "python.exe")
if not os.path.exists(python_exe):
QMessageBox.warning(self, "路径无效",
f"Python可执行文件不存在:\n{python_exe}\n\n"
f"请选择正确的Python安装目录")
return False
# 测试Python是否可执行
try:
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
startupinfo.wShowWindow = subprocess.SW_HIDE
result = subprocess.run(
[python_exe, "--version"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
timeout=10,
startupinfo=startupinfo
)
if result.returncode != 0:
QMessageBox.warning(self, "Python不可用",
"Python解释器无法正常执行")
return False
except subprocess.TimeoutExpired:
QMessageBox.warning(self, "Python超时",
"Python解释器响应超时")
return False
except Exception as e:
QMessageBox.warning(self, "Python错误",
f"Python解释器执行错误: {str(e)}")
return False
return True
except Exception as e:
self.log_text.append(f"路径验证异常: {str(e)}")
return False
def show_help(self):
"""显示帮助内容"""
self.tabs.setCurrentIndex(3) # 切换到帮助标签页
def show_about(self):
"""显示关于对话框"""
QMessageBox.about(self, "关于",
"Python库管理工具\n\n"
"版本: 1.0.0\n\n"
"一个方便的Python库管理工具,支持多镜像源自动切换,\n"
"可快速安装、更新、卸载和管理各种Python库。")
def open_python_download(self):
"""打开Python下载页面"""
import webbrowser
webbrowser.open("https://www.python.org/downloads/")
self.log_text.append("已打开Python官网下载页面")
def closeEvent(self, event):
"""窗口关闭事件处理"""
# 如果有正在运行的线程,先停止
if self.install_worker and self.install_worker.isRunning():
reply = QMessageBox.question(
self, "确认关闭",
"有操作正在进行中,确定要关闭吗?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.Yes:
self.install_worker.stop()
event.accept()
else:
event.ignore()
elif self.installed_packages_worker and self.installed_packages_worker.isRunning():
reply = QMessageBox.question(
self, "确认关闭",
"有检查操作正在进行中,确定要关闭吗?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.Yes:
self.installed_packages_worker.stop()
event.accept()
else:
event.ignore()
else:
event.accept()
if __name__ == "__main__":
def handle_exception(exc_type, exc_value, exc_traceback):
if issubclass(exc_type, KeyboardInterrupt):
sys.__excepthook__(exc_type, exc_value, exc_traceback)
return
logging.critical("未捕获的异常",
exc_info=(exc_type, exc_value, exc_traceback))
# 显示错误对话框
error_msg = f"程序发生未处理异常:\n\n{exc_type.__name__}: {exc_value}"
QMessageBox.critical(None, "程序错误", error_msg)
sys.excepthook = handle_exception
# 确保中文显示正常
import matplotlib
matplotlib.rcParams["font.family"] = ["SimHei", "WenQuanYi Micro Hei", "Heiti TC"]
# 隐藏控制台窗口
import ctypes
ctypes.windll.user32.ShowWindow(ctypes.windll.kernel32.GetConsoleWindow(), 0)
app = QApplication(sys.argv)
# 设置应用风格
app.setStyle("Fusion")
window = LibraryInstallerGUI()
sys.exit(app.exec())
OK!