import
sys
import
subprocess
import
json
import
os
from
datetime
import
datetime
from
PyQt5.QtWidgets
import
*
from
PyQt5.QtCore
import
*
from
PyQt5.QtGui
import
*
class
PipManager(QMainWindow):
def
__init__(
self
):
super
().__init__()
self
.is_admin
=
False
try
:
import
ctypes
self
.is_admin
=
ctypes.windll.shell32.IsUserAnAdmin()
except
:
pass
title
=
'pip图形化管理工具'
if
self
.is_admin:
title
+
=
' (管理员)'
self
.setWindowTitle(title)
self
.setGeometry(
100
,
100
,
800
,
600
)
self
.current_mirror
=
None
self
.installed_packages
=
[]
self
.operation_history
=
[]
self
.searched_packages
=
[]
self
.requests_available
=
False
try
:
import
requests
self
.requests_available
=
True
except
ImportError:
self
.requests_available
=
False
QMessageBox.warning(
self
,
'警告'
,
'需要requests库来搜索远程包,请先安装: pip install requests'
)
self
.init_ui()
self
.load_config()
self
.refresh_package_list()
self
.init_context_menu()
def
init_ui(
self
):
self
.init_menu_bar()
self
.init_tool_bar()
self
.init_status_bar()
self
.init_main_content()
self
.init_context_menu()
def
init_menu_bar(
self
):
file_menu
=
self
.menuBar().addMenu(
'文件(&F)'
)
refresh_action
=
QAction(
'刷新包列表'
,
self
)
refresh_action.triggered.connect(
self
.refresh_package_list)
file_menu.addAction(refresh_action)
exit_action
=
QAction(
'退出'
,
self
)
exit_action.setShortcut(
'Ctrl+Q'
)
exit_action.triggered.connect(
self
.close)
file_menu.addAction(exit_action)
edit_menu
=
self
.menuBar().addMenu(
'设置(&E)'
)
mirror_action
=
QAction(
'设置镜像源'
,
self
)
mirror_action.triggered.connect(
self
.show_mirror_dialog)
edit_menu.addAction(mirror_action)
self
.admin_action
=
QAction(
'以管理员身份运行'
,
self
)
self
.admin_action.triggered.connect(
self
.run_as_admin)
if
self
.is_admin:
self
.admin_action.setEnabled(
False
)
edit_menu.addAction(
self
.admin_action)
help_menu
=
self
.menuBar().addMenu(
'帮助(&H)'
)
about_action
=
QAction(
'关于'
,
self
)
about_action.triggered.connect(
self
.show_about)
help_menu.addAction(about_action)
def
run_as_admin(
self
):
try
:
import
ctypes
import
sys
if
ctypes.windll.shell32.IsUserAnAdmin():
QMessageBox.information(
self
,
'提示'
,
'当前已经是管理员权限'
)
return
ctypes.windll.shell32.ShellExecuteW(
None
,
"runas"
, sys.executable,
" "
.join(sys.argv),
None
,
1
)
self
.close()
except
Exception as e:
QMessageBox.critical(
self
,
'错误'
, f
'请求管理员权限失败: {str(e)}'
)
def
init_tool_bar(
self
):
toolbar
=
QToolBar(
'主工具栏'
)
toolbar.setIconSize(QSize(
16
,
16
))
self
.addToolBar(toolbar)
install_btn
=
QPushButton(
'安装包'
)
install_btn.clicked.connect(
self
.install_package)
toolbar.addWidget(install_btn)
uninstall_btn
=
QPushButton(
'卸载包'
)
uninstall_btn.clicked.connect(
self
.uninstall_package)
toolbar.addWidget(uninstall_btn)
upgrade_btn
=
QPushButton(
'升级包'
)
upgrade_btn.clicked.connect(
self
.upgrade_package)
toolbar.addWidget(upgrade_btn)
refresh_btn
=
QPushButton(
'刷新'
)
refresh_btn.clicked.connect(
self
.refresh_package_list)
toolbar.addWidget(refresh_btn)
jxy_btn
=
QPushButton(
'设置镜像源'
)
jxy_btn.clicked.connect(
self
.show_mirror_dialog)
toolbar.addWidget(jxy_btn)
def
init_status_bar(
self
):
self
.status_bar
=
QStatusBar()
self
.setStatusBar(
self
.status_bar)
self
.mirror_label
=
QLabel(
'镜像源: 未设置'
)
self
.status_bar.addPermanentWidget(
self
.mirror_label)
self
.status_bar.showMessage(
'准备就绪'
,
3000
)
def
init_main_content(
self
):
main_widget
=
QWidget()
self
.setCentralWidget(main_widget)
main_layout
=
QHBoxLayout()
main_widget.setLayout(main_layout)
left_layout
=
QVBoxLayout()
self
.search_box
=
QLineEdit()
self
.search_box.setPlaceholderText(
'搜索包名...'
)
self
.search_box.textChanged.connect(
self
.filter_packages)
left_layout.addWidget(
self
.search_box)
self
.package_list
=
QListWidget()
self
.package_list.setAlternatingRowColors(
True
)
left_layout.addWidget(
self
.package_list)
right_layout
=
QVBoxLayout()
self
.package_detail
=
QTextEdit()
self
.package_detail.setReadOnly(
True
)
right_layout.addWidget(
self
.package_detail)
main_layout.addLayout(left_layout,
3
)
main_layout.addLayout(right_layout,
2
)
def
init_context_menu(
self
):
self
.package_list.setContextMenuPolicy(Qt.CustomContextMenu)
self
.package_list.customContextMenuRequested.connect(
self
.show_package_context_menu)
self
.package_menu
=
QMenu(
self
)
install_action
=
QAction(
'安装'
,
self
)
install_action.triggered.connect(
self
.install_selected_package)
self
.package_menu.addAction(install_action)
uninstall_action
=
QAction(
'卸载'
,
self
)
uninstall_action.triggered.connect(
self
.uninstall_package)
self
.package_menu.addAction(uninstall_action)
upgrade_action
=
QAction(
'升级'
,
self
)
upgrade_action.triggered.connect(
self
.upgrade_package)
self
.package_menu.addAction(upgrade_action)
self
.package_menu.addSeparator()
info_action
=
QAction(
'查看详情'
,
self
)
info_action.triggered.connect(
self
.show_package_info)
self
.package_menu.addAction(info_action)
def
install_selected_package(
self
):
selected_item
=
self
.package_list.currentItem()
if
not
selected_item:
QMessageBox.warning(
self
,
'警告'
,
'请先选择一个包'
)
return
pkg_name
=
selected_item.text().split(
'=='
)[
0
]
mirror_info
=
""
if
self
.current_mirror:
mirror_info
=
f
"\n镜像源: {self.get_mirror_name(self.current_mirror)}"
confirm
=
QMessageBox.question(
self
,
'确认安装'
,
f
'确定要安装 {pkg_name} 吗?{mirror_info}'
,
QMessageBox.Yes | QMessageBox.No
)
if
confirm
=
=
QMessageBox.Yes:
try
:
self
.run_pip_command(
'install'
, pkg_name)
except
Exception as e:
QMessageBox.critical(
self
,
'安装错误'
, f
'安装过程中出错\n{str(e)}'
)
def
load_config(
self
):
config_path
=
os.path.expanduser(
'~/.pip-gui-config'
)
self
.mirrors
=
{
"清华"
:
"https://pypi.tuna.tsinghua.edu.cn/simple"
,
"阿里云"
:
"https://mirrors.aliyun.com/pypi/simple"
,
"腾讯云"
:
"https://mirrors.cloud.tencent.com/pypi/simple"
,
"官方源"
:
"https://pypi.org/simple"
}
try
:
if
os.path.exists(config_path):
with
open
(config_path,
'r'
, encoding
=
'utf-8'
) as f:
config
=
json.load(f)
self
.current_mirror
=
config.get(
'current_mirror'
)
self
.mirror_label.setText(f
'镜像源: {self.get_mirror_name(self.current_mirror)}'
)
except
Exception as e:
QMessageBox.warning(
self
,
'配置错误'
, f
'加载配置文件失败: {str(e)}'
)
def
save_config(
self
):
config_path
=
os.path.expanduser(
'~/.pip-gui-config'
)
config
=
{
'current_mirror'
:
self
.current_mirror
}
try
:
with
open
(config_path,
'w'
, encoding
=
'utf-8'
) as f:
json.dump(config, f, ensure_ascii
=
False
, indent
=
4
)
except
Exception as e:
QMessageBox.warning(
self
,
'配置错误'
, f
'保存配置文件失败: {str(e)}'
)
def
get_mirror_name(
self
, url):
for
name, mirror_url
in
self
.mirrors.items():
if
mirror_url
=
=
url:
return
name
return
'自定义源'
def
refresh_package_list(
self
):
try
:
result
=
subprocess.run(
[sys.executable,
'-m'
,
'pip'
,
'list'
,
'--format=json'
],
capture_output
=
True
, text
=
True
)
if
result.returncode
=
=
0
:
self
.installed_packages
=
json.loads(result.stdout)
self
.update_package_list_display()
self
.status_bar.showMessage(
'包列表刷新成功'
,
3000
)
else
:
raise
Exception(result.stderr)
except
Exception as e:
QMessageBox.critical(
self
,
'错误'
, f
'获取包列表失败: {str(e)}'
)
self
.status_bar.showMessage(
'获取包列表失败'
,
3000
)
def
update_package_list_display(
self
):
self
.package_list.clear()
sorted_packages
=
sorted
(
self
.installed_packages, key
=
lambda
x: x[
'name'
].lower())
for
pkg
in
sorted_packages:
item
=
QListWidgetItem(f
"{pkg['name']}=={pkg['version']}"
)
item.setForeground(QColor(
'black'
))
self
.package_list.addItem(item)
for
pkg
in
self
.searched_packages:
item
=
QListWidgetItem(f
"{pkg['name']}=={pkg['version']} (可安装)"
)
item.setForeground(QColor(
'blue'
))
self
.package_list.addItem(item)
def
search_remote_package(
self
, package_name):
try
:
import
requests
print
(f
"正在搜索: {package_name}"
)
headers
=
{
"Accept"
:
"application/json"
,
"User-Agent"
:
"pip-gui-tool/1.0"
}
response
=
requests.get(
f
"https://pypi.org/pypi/{package_name}/json"
,
headers
=
headers,
timeout
=
10
)
print
(f
"响应状态码: {response.status_code}"
)
print
(f
"响应内容: {response.text[:200]}..."
)
response.raise_for_status()
data
=
response.json()
self
.searched_packages
=
[]
if
'info'
in
data:
self
.searched_packages.append({
'name'
: data[
'info'
][
'name'
],
'version'
: data[
'info'
].get(
'version'
,
'最新'
)
})
if
self
.searched_packages:
self
.update_package_list_display()
self
.status_bar.showMessage(f
"找到包: {self.searched_packages[0]['name']}"
,
3000
)
else
:
self
.status_bar.showMessage(
"未找到匹配包"
,
3000
)
except
requests.exceptions.HTTPError as e:
if
e.response.status_code
=
=
404
:
self
.status_bar.showMessage(f
"未找到包: {package_name}"
,
3000
)
else
:
error_msg
=
f
"网络请求失败: {str(e)}"
print
(error_msg)
QMessageBox.critical(
self
,
'错误'
, error_msg)
except
Exception as e:
error_msg
=
f
"搜索失败: {str(e)}"
print
(error_msg)
QMessageBox.critical(
self
,
'错误'
, error_msg)
def
filter_packages(
self
):
search_text
=
self
.search_box.text().strip()
if
not
search_text:
self
.searched_packages
=
[]
self
.update_package_list_display()
return
if
not
self
.requests_available:
QMessageBox.warning(
self
,
'警告'
,
'requests库未安装,无法搜索远程包'
)
return
has_local_match
=
False
for
i
in
range
(
self
.package_list.count()):
item
=
self
.package_list.item(i)
match
=
search_text.lower()
in
item.text().lower()
item.setHidden(
not
match)
if
match:
has_local_match
=
True
if
not
has_local_match:
self
.search_remote_package(search_text)
def
show_package_context_menu(
self
, pos):
if
self
.package_list.currentItem():
self
.package_menu.exec_(
self
.package_list.mapToGlobal(pos))
def
show_package_info(
self
):
selected_item
=
self
.package_list.currentItem()
if
not
selected_item:
return
pkg_name
=
selected_item.text().split(
'=='
)[
0
]
try
:
result
=
subprocess.run(
[sys.executable,
'-m'
,
'pip'
,
'show'
, pkg_name],
capture_output
=
True
, text
=
True
)
if
result.returncode
=
=
0
:
self
.package_detail.setText(result.stdout)
else
:
raise
Exception(result.stderr)
except
Exception as e:
QMessageBox.critical(
self
,
'错误'
, f
'获取包信息失败: {str(e)}'
)
def
install_package(
self
):
dialog
=
QDialog(
self
)
dialog.setWindowTitle(
'安装包'
)
layout
=
QFormLayout()
pkg_name_edit
=
QLineEdit()
layout.addRow(
'包名:'
, pkg_name_edit)
version_edit
=
QLineEdit()
version_edit.setPlaceholderText(
'可选,如: 1.0.0'
)
layout.addRow(
'版本号:'
, version_edit)
btn_box
=
QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
btn_box.accepted.connect(dialog.accept)
btn_box.rejected.connect(dialog.reject)
layout.addRow(btn_box)
dialog.setLayout(layout)
if
dialog.exec_()
=
=
QDialog.Accepted:
pkg_name
=
pkg_name_edit.text().strip()
version
=
version_edit.text().strip()
if
not
pkg_name:
QMessageBox.warning(
self
,
'警告'
,
'包名不能为空'
)
return
if
version
and
not
all
(c.isdigit()
or
c
=
=
'.'
for
c
in
version):
QMessageBox.warning(
self
,
'错误'
,
'版本号格式不正确,请使用数字和点(.)'
)
return
mirror_info
=
""
if
self
.current_mirror:
mirror_info
=
f
"\n镜像源: {self.get_mirror_name(self.current_mirror)}"
confirm
=
QMessageBox.question(
self
,
'确认安装'
,
f
'确定要安装 {pkg_name}{"=="+version if version else ""} 吗?{mirror_info}'
,
QMessageBox.Yes | QMessageBox.No
)
if
confirm
=
=
QMessageBox.Yes:
try
:
self
.run_pip_command(
'install'
, pkg_name, version)
except
Exception as e:
if
"Could not find a version"
in
str
(e):
QMessageBox.warning(
self
,
'版本错误'
, f
"找不到指定版本的包\n{e}"
)
elif
"No matching distribution"
in
str
(e):
QMessageBox.warning(
self
,
'包名错误'
, f
"找不到指定的包\n{e}"
)
else
:
QMessageBox.critical(
self
,
'安装错误'
, f
"安装过程中出错\n{e}"
)
def
uninstall_package(
self
):
selected_item
=
self
.package_list.currentItem()
if
not
selected_item:
QMessageBox.warning(
self
,
'警告'
,
'请先选择一个包'
)
return
pkg_name
=
selected_item.text().split(
'=='
)[
0
]
confirm
=
QMessageBox.question(
self
,
'确认卸载'
,
f
'确定要卸载 {pkg_name} 吗?'
,
QMessageBox.Yes | QMessageBox.No
)
if
confirm
=
=
QMessageBox.Yes:
self
.run_pip_command(
'uninstall'
, pkg_name)
def
upgrade_package(
self
):
selected_item
=
self
.package_list.currentItem()
if
not
selected_item:
QMessageBox.warning(
self
,
'警告'
,
'请先选择一个包'
)
return
pkg_name
=
selected_item.text().split(
'=='
)[
0
]
confirm
=
QMessageBox.question(
self
,
'确认升级'
,
f
'确定要升级 {pkg_name} 吗?'
,
QMessageBox.Yes | QMessageBox.No
)
if
confirm
=
=
QMessageBox.Yes:
self
.run_pip_command(
'install'
, pkg_name, upgrade
=
True
)
def
run_pip_command(
self
, command, pkg_name, version
=
None
, upgrade
=
False
):
cmd
=
[sys.executable,
'-m'
,
'pip'
, command, pkg_name]
if
command
=
=
'uninstall'
:
cmd.append(
'--yes'
)
if
version:
cmd.append(f
'=={version}'
)
if
upgrade:
cmd.append(
'--upgrade'
)
if
self
.current_mirror
and
command !
=
'uninstall'
:
cmd.extend([
'-i'
,
self
.current_mirror])
host
=
self
.current_mirror.split(
'//'
)[
1
].split(
'/'
)[
0
]
cmd.extend([
'--trusted-host'
, host])
try
:
process
=
subprocess.Popen(
cmd,
stdout
=
subprocess.PIPE,
stderr
=
subprocess.PIPE,
text
=
True
,
bufsize
=
1
,
universal_newlines
=
True
)
self
.package_detail.clear()
while
True
:
output
=
process.stdout.readline()
if
output
=
=
''
and
process.poll()
is
not
None
:
break
if
output:
self
.package_detail.append(output.strip())
return_code
=
process.wait()
self
.operation_history.append({
'command'
:
' '
.join(cmd),
'time'
: datetime.now().strftime(
'%Y-%m-%d %H:%M:%S'
),
'success'
: return_code
=
=
0
,
'output'
:
self
.package_detail.toPlainText()
})
if
len
(
self
.operation_history) >
20
:
self
.operation_history.pop(
0
)
if
return_code
=
=
0
:
QMessageBox.information(
self
,
'操作成功'
,
f
'操作执行成功'
)
self
.refresh_package_list()
else
:
error_output
=
process.stderr.read()
self
.package_detail.append(f
"\n错误信息:\n{error_output}"
)
raise
Exception(error_output)
except
Exception as e:
QMessageBox.critical(
self
,
'操作失败'
,
f
'操作执行失败:\n{str(e)}'
)
self
.package_detail.append(f
"\n错误信息:\n{str(e)}"
)
def
show_mirror_dialog(
self
):
dialog
=
QDialog(
self
)
dialog.setWindowTitle(
'设置镜像源'
)
layout
=
QVBoxLayout()
mirror_group
=
QButtonGroup()
for
i, (name, url)
in
enumerate
(
self
.mirrors.items()):
radio
=
QRadioButton(name)
radio.url
=
url
if
url
=
=
self
.current_mirror:
radio.setChecked(
True
)
mirror_group.addButton(radio, i)
layout.addWidget(radio)
custom_radio
=
QRadioButton(
'自定义'
)
mirror_group.addButton(custom_radio,
len
(
self
.mirrors))
layout.addWidget(custom_radio)
custom_edit
=
QLineEdit()
if
self
.current_mirror:
custom_edit.setText(
self
.current_mirror)
else
:
custom_edit.setPlaceholderText(
'输入镜像源URL'
)
if
not
self
.current_mirror:
mirror_group.buttons()[
0
].setChecked(
True
)
custom_edit.setText(mirror_group.buttons()[
0
].url)
layout.addWidget(custom_edit)
def
on_radio_clicked(button):
if
button !
=
custom_radio:
custom_edit.setText(button.url)
mirror_group.buttonClicked.connect(on_radio_clicked)
btn_box
=
QDialogButtonBox(
QDialogButtonBox.Ok | QDialogButtonBox.Cancel
)
btn_box.accepted.connect(dialog.accept)
btn_box.rejected.connect(dialog.reject)
layout.addWidget(btn_box)
dialog.setLayout(layout)
if
dialog.exec_()
=
=
QDialog.Accepted:
selected
=
mirror_group.checkedButton()
if
selected
=
=
custom_radio
and
custom_edit.text():
self
.current_mirror
=
custom_edit.text()
elif
selected !
=
custom_radio:
self
.current_mirror
=
selected.url
else
:
return
self
.mirror_label.setText(f
'镜像源: {self.get_mirror_name(self.current_mirror)}'
)
self
.save_config()
self
.status_bar.showMessage(
'镜像源设置成功'
,
3000
)
def
show_about(
self
):
QMessageBox.about(
self
,
'关于 pip图形化管理工具'
,
'pip图形化管理工具 v1.0\n\n'
'一个简单的pip包管理图形界面\n'
'支持安装/卸载/升级和镜像源切换'
)
def
closeEvent(
self
, event):
self
.save_config()
event.accept()
def
main():
app
=
QApplication(sys.argv)
window
=
PipManager()
window.show()
sys.exit(app.exec_())
if
__name__
=
=
'__main__'
:
main()