519 lines
18 KiB
Python
519 lines
18 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
现代化圆形进度条更新对话框
|
||
展示完整更新流程:下载 → UAC授权 → 安装 → 重启
|
||
"""
|
||
|
||
from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QLabel,
|
||
QPushButton, QHBoxLayout, QWidget)
|
||
from PyQt5.QtCore import Qt, QTimer, QPropertyAnimation, QEasingCurve, pyqtProperty
|
||
from PyQt5.QtGui import QFont, QPainter, QColor, QPen, QLinearGradient, QConicalGradient
|
||
from PyQt5.QtCore import QRectF, QPointF
|
||
import math
|
||
|
||
|
||
class CircularProgressBar(QWidget):
|
||
"""
|
||
自定义圆形进度条组件
|
||
|
||
特性:
|
||
- 顺时针旋转动画
|
||
- 渐变色圆环
|
||
- 中心显示百分比
|
||
- 支持平滑过渡
|
||
"""
|
||
|
||
def __init__(self, parent=None):
|
||
super().__init__(parent)
|
||
self._value = 0 # 当前进度值(0-100)
|
||
self._max_value = 100
|
||
self._text = "0%"
|
||
self.setMinimumSize(200, 200)
|
||
|
||
# 动画效果
|
||
self.animation = QPropertyAnimation(self, b"value")
|
||
self.animation.setDuration(500) # 500ms过渡动画
|
||
self.animation.setEasingCurve(QEasingCurve.OutCubic)
|
||
|
||
@pyqtProperty(int)
|
||
def value(self):
|
||
return self._value
|
||
|
||
@value.setter
|
||
def value(self, val):
|
||
self._value = val
|
||
self.update() # 触发重绘
|
||
|
||
def setValue(self, val):
|
||
"""设置进度值(带动画)"""
|
||
if val != self._value:
|
||
self.animation.stop()
|
||
self.animation.setStartValue(self._value)
|
||
self.animation.setEndValue(val)
|
||
self.animation.start()
|
||
|
||
def setText(self, text):
|
||
"""设置中心文本"""
|
||
self._text = text
|
||
self.update()
|
||
|
||
def paintEvent(self, event):
|
||
"""绘制圆形进度条"""
|
||
painter = QPainter(self)
|
||
painter.setRenderHint(QPainter.Antialiasing) # 抗锯齿
|
||
|
||
# 计算中心和半径
|
||
width = self.width()
|
||
height = self.height()
|
||
size = min(width, height)
|
||
center = QPointF(width / 2, height / 2)
|
||
radius = size / 2 - 15 # 留出边距
|
||
|
||
# 绘制背景圆环(灰色)
|
||
pen = QPen()
|
||
pen.setWidth(12)
|
||
pen.setColor(QColor(230, 230, 230))
|
||
pen.setCapStyle(Qt.RoundCap)
|
||
painter.setPen(pen)
|
||
|
||
rect = QRectF(center.x() - radius, center.y() - radius,
|
||
radius * 2, radius * 2)
|
||
painter.drawArc(rect, 90 * 16, -360 * 16) # 绘制完整圆环
|
||
|
||
# 绘制进度圆环(渐变色)
|
||
if self._value > 0:
|
||
# 创建圆锥渐变(顺时针旋转效果)
|
||
gradient = QConicalGradient(center, 90) # 从顶部开始
|
||
gradient.setColorAt(0.0, QColor(66, 133, 244)) # 蓝色
|
||
gradient.setColorAt(0.5, QColor(25, 118, 210)) # 深蓝
|
||
gradient.setColorAt(1.0, QColor(21, 101, 192)) # 更深蓝
|
||
|
||
pen.setBrush(gradient)
|
||
pen.setColor(QColor(66, 133, 244))
|
||
painter.setPen(pen)
|
||
|
||
# 计算进度角度(顺时针,从顶部开始)
|
||
span_angle = int(-(self._value / self._max_value) * 360 * 16)
|
||
painter.drawArc(rect, 90 * 16, span_angle)
|
||
|
||
# 绘制中心文本(百分比)
|
||
painter.setPen(QColor(44, 62, 80))
|
||
font = QFont('Microsoft YaHei', 28, QFont.Bold)
|
||
painter.setFont(font)
|
||
painter.drawText(rect, Qt.AlignCenter, self._text)
|
||
|
||
|
||
class UpdateProgressDialog(QDialog):
|
||
"""
|
||
现代化更新进度对话框
|
||
|
||
更新流程阶段:
|
||
1. 下载安装包 (0-50%)
|
||
2. 等待UAC授权 (50-60%)
|
||
3. 执行安装 (60-90%)
|
||
4. 等待程序重启 (90-100%)
|
||
"""
|
||
|
||
# 流程阶段定义
|
||
STAGE_DOWNLOAD = 0 # 下载阶段 (0-50%)
|
||
STAGE_UAC = 1 # UAC授权阶段 (50-60%)
|
||
STAGE_INSTALL = 2 # 安装阶段 (60-90%)
|
||
STAGE_RESTART = 3 # 重启阶段 (90-100%)
|
||
|
||
def __init__(self, version, parent=None):
|
||
super().__init__(parent)
|
||
self.version = version
|
||
self.downloader = None
|
||
self.current_stage = self.STAGE_DOWNLOAD
|
||
self.download_progress = 0 # 下载进度(0-100)
|
||
self.restart_progress = 0 # 重启阶段进度(0-100),用于外部检查
|
||
|
||
self.init_ui()
|
||
|
||
def init_ui(self):
|
||
"""初始化UI"""
|
||
self.setWindowTitle("正在更新")
|
||
self.setFixedSize(500, 600)
|
||
# 禁止关闭按钮(只能通过取消按钮关闭)
|
||
self.setWindowFlags(Qt.Dialog | Qt.WindowTitleHint | Qt.CustomizeWindowHint)
|
||
|
||
# 主布局
|
||
layout = QVBoxLayout()
|
||
layout.setSpacing(20)
|
||
layout.setContentsMargins(30, 30, 30, 30)
|
||
|
||
# ============ 标题区域 ============
|
||
self.title_label = QLabel(f"正在更新到 v{self.version}")
|
||
self.title_label.setFont(QFont('Microsoft YaHei', 16, QFont.Bold))
|
||
self.title_label.setAlignment(Qt.AlignCenter)
|
||
self.title_label.setStyleSheet("color: #2c3e50; margin-bottom: 10px;")
|
||
layout.addWidget(self.title_label)
|
||
|
||
# ============ 圆形进度条 ============
|
||
self.circular_progress = CircularProgressBar()
|
||
layout.addWidget(self.circular_progress, alignment=Qt.AlignCenter)
|
||
|
||
# ============ 阶段提示 ============
|
||
self.stage_label = QLabel("📥 正在下载安装包...")
|
||
self.stage_label.setFont(QFont('Microsoft YaHei', 12))
|
||
self.stage_label.setAlignment(Qt.AlignCenter)
|
||
self.stage_label.setStyleSheet("color: #34495e; margin-top: 10px;")
|
||
layout.addWidget(self.stage_label)
|
||
|
||
# ============ 详细状态 ============
|
||
self.status_label = QLabel("准备开始下载...")
|
||
self.status_label.setAlignment(Qt.AlignCenter)
|
||
self.status_label.setStyleSheet("color: #7f8c8d; font-size: 11px;")
|
||
self.status_label.setMinimumHeight(25)
|
||
layout.addWidget(self.status_label)
|
||
|
||
# ============ 重试信息(默认隐藏)============
|
||
self.retry_label = QLabel("")
|
||
self.retry_label.setAlignment(Qt.AlignCenter)
|
||
self.retry_label.setStyleSheet("color: #ff9800; font-size: 11px; font-weight: bold;")
|
||
self.retry_label.setMinimumHeight(25)
|
||
self.retry_label.hide()
|
||
layout.addWidget(self.retry_label)
|
||
|
||
# ============ 按钮区域 ============
|
||
button_layout = QHBoxLayout()
|
||
button_layout.addStretch()
|
||
|
||
self.cancel_button = QPushButton("取消更新")
|
||
self.cancel_button.setFixedWidth(120)
|
||
self.cancel_button.setFixedHeight(36)
|
||
self.cancel_button.clicked.connect(self.cancel_download)
|
||
button_layout.addWidget(self.cancel_button)
|
||
|
||
button_layout.addStretch()
|
||
layout.addLayout(button_layout)
|
||
|
||
self.setLayout(layout)
|
||
|
||
# 应用样式
|
||
self.apply_styles()
|
||
|
||
def apply_styles(self):
|
||
"""应用现代化样式"""
|
||
self.setStyleSheet("""
|
||
QDialog {
|
||
background: qlineargradient(
|
||
x1:0, y1:0, x2:0, y2:1,
|
||
stop:0 #f8fafb,
|
||
stop:0.5 #f1f4f6,
|
||
stop:1 #e8ecef
|
||
);
|
||
}
|
||
QLabel {
|
||
background: transparent;
|
||
}
|
||
QPushButton {
|
||
background: qlineargradient(
|
||
x1:0, y1:0, x2:0, y2:1,
|
||
stop:0 #f5f5f5,
|
||
stop:1 #e0e0e0
|
||
);
|
||
border: 2px solid #ccc;
|
||
border-radius: 8px;
|
||
padding: 8px 20px;
|
||
font-size: 12px;
|
||
font-weight: bold;
|
||
font-family: 'Microsoft YaHei';
|
||
color: #555;
|
||
}
|
||
QPushButton:hover {
|
||
background: qlineargradient(
|
||
x1:0, y1:0, x2:0, y2:1,
|
||
stop:0 #e8e8e8,
|
||
stop:1 #d0d0d0
|
||
);
|
||
border: 2px solid #999;
|
||
}
|
||
QPushButton:pressed {
|
||
background: #d0d0d0;
|
||
}
|
||
QPushButton:disabled {
|
||
background: #f0f0f0;
|
||
color: #aaa;
|
||
border: 2px solid #ddd;
|
||
}
|
||
""")
|
||
|
||
def update_progress(self, downloaded, total):
|
||
"""
|
||
更新下载进度(阶段1:0-50%)
|
||
|
||
Args:
|
||
downloaded: 已下载字节数
|
||
total: 总字节数
|
||
"""
|
||
if self.current_stage != self.STAGE_DOWNLOAD:
|
||
return # 不在下载阶段,忽略
|
||
|
||
if total > 0:
|
||
# 计算下载百分比(0-100)
|
||
download_percent = int((downloaded / total) * 100)
|
||
self.download_progress = download_percent
|
||
|
||
# 映射到总进度的0-50%
|
||
overall_progress = int(download_percent * 0.5)
|
||
|
||
# 更新圆形进度条
|
||
self.circular_progress.setValue(overall_progress)
|
||
self.circular_progress.setText(f"{overall_progress}%")
|
||
|
||
# 转换为MB显示
|
||
downloaded_mb = downloaded / (1024 * 1024)
|
||
total_mb = total / (1024 * 1024)
|
||
|
||
# 更新状态文本
|
||
self.status_label.setText(f"已下载 {downloaded_mb:.1f} MB / {total_mb:.1f} MB")
|
||
else:
|
||
# 无法获取总大小时显示不确定状态
|
||
self.status_label.setText("正在下载,请稍候...")
|
||
|
||
def show_retry_info(self, current_retry, max_retries):
|
||
"""
|
||
显示重试信息
|
||
|
||
Args:
|
||
current_retry: 当前重试次数
|
||
max_retries: 最大重试次数
|
||
"""
|
||
self.retry_label.setText(f"⚠️ 下载失败,正在重试... ({current_retry}/{max_retries})")
|
||
self.retry_label.show()
|
||
|
||
# 重置下载进度
|
||
self.download_progress = 0
|
||
self.circular_progress.setValue(0)
|
||
self.circular_progress.setText("0%")
|
||
self.status_label.setText("正在重试连接...")
|
||
|
||
# 更新标题
|
||
self.title_label.setText(f"正在重试下载 v{self.version}")
|
||
|
||
def download_finished(self, file_path):
|
||
"""
|
||
下载完成,进入UAC授权阶段(阶段2:50-60%)
|
||
|
||
Args:
|
||
file_path: 下载的文件路径
|
||
"""
|
||
self.current_stage = self.STAGE_UAC
|
||
|
||
# 更新进度到50%
|
||
self.circular_progress.setValue(50)
|
||
self.circular_progress.setText("50%")
|
||
|
||
# 更新阶段提示
|
||
self.stage_label.setText("🔐 等待管理员授权...")
|
||
self.title_label.setText(f"✅ 下载完成")
|
||
self.status_label.setText("请在UAC窗口点击「允许」以继续...")
|
||
self.status_label.setStyleSheet("color: #28a745; font-size: 11px; font-weight: bold;")
|
||
|
||
# 禁用取消按钮
|
||
self.cancel_button.setEnabled(False)
|
||
self.retry_label.hide()
|
||
|
||
# 启动UAC等待动画(50% → 60%)
|
||
self.start_uac_waiting_animation()
|
||
|
||
def start_uac_waiting_animation(self):
|
||
"""启动UAC等待动画(进度条在50-60%之间缓慢移动)"""
|
||
self.uac_timer = QTimer(self)
|
||
self.uac_progress = 50
|
||
|
||
def update_uac_progress():
|
||
self.uac_progress += 0.5 # 每次增加0.5%
|
||
if self.uac_progress <= 60:
|
||
self.circular_progress.setValue(int(self.uac_progress))
|
||
self.circular_progress.setText(f"{int(self.uac_progress)}%")
|
||
else:
|
||
# 超过60%停止(等待用户授权)
|
||
self.uac_timer.stop()
|
||
|
||
self.uac_timer.timeout.connect(update_uac_progress)
|
||
self.uac_timer.start(500) # 每500ms更新一次
|
||
|
||
def uac_authorized(self):
|
||
"""
|
||
用户已授权UAC,进入安装阶段(阶段3:60-90%)
|
||
"""
|
||
# 停止UAC等待动画
|
||
if hasattr(self, 'uac_timer'):
|
||
self.uac_timer.stop()
|
||
|
||
self.current_stage = self.STAGE_INSTALL
|
||
|
||
# 更新进度到60%
|
||
self.circular_progress.setValue(60)
|
||
self.circular_progress.setText("60%")
|
||
|
||
# 更新阶段提示
|
||
self.stage_label.setText("⚙️ 正在安装新版本...")
|
||
self.title_label.setText("正在安装")
|
||
self.status_label.setText("正在替换程序文件,请稍候...")
|
||
self.status_label.setStyleSheet("color: #007bff; font-size: 11px; font-weight: bold;")
|
||
|
||
# 启动安装进度动画(60% → 90%)
|
||
self.start_install_animation()
|
||
|
||
def start_install_animation(self):
|
||
"""启动安装进度动画(60% → 90%)"""
|
||
self.install_timer = QTimer(self)
|
||
self.install_progress = 60
|
||
|
||
def update_install_progress():
|
||
self.install_progress += 1 # 每次增加1%
|
||
if self.install_progress <= 90:
|
||
self.circular_progress.setValue(self.install_progress)
|
||
self.circular_progress.setText(f"{self.install_progress}%")
|
||
else:
|
||
# 安装完成,进入重启阶段
|
||
self.install_timer.stop()
|
||
self.prepare_restart()
|
||
|
||
self.install_timer.timeout.connect(update_install_progress)
|
||
self.install_timer.start(800) # 每800ms增加1%(共24秒)
|
||
|
||
def prepare_restart(self):
|
||
"""
|
||
准备重启(阶段4:90-100%)
|
||
"""
|
||
self.current_stage = self.STAGE_RESTART
|
||
|
||
# 更新进度到90%
|
||
self.circular_progress.setValue(90)
|
||
self.circular_progress.setText("90%")
|
||
|
||
# 更新阶段提示
|
||
self.stage_label.setText("🚀 准备启动新版本...")
|
||
self.title_label.setText("即将完成")
|
||
self.status_label.setText("程序即将重启,请稍候...")
|
||
self.status_label.setStyleSheet("color: #28a745; font-size: 11px; font-weight: bold;")
|
||
|
||
# 启动重启倒计时动画(90% → 100%)
|
||
self.start_restart_animation()
|
||
|
||
def start_restart_animation(self):
|
||
"""启动重启倒计时动画(90% → 100%)"""
|
||
self.restart_timer = QTimer(self)
|
||
self.restart_progress = 90
|
||
|
||
def update_restart_progress():
|
||
self.restart_progress += 2 # 每次增加2%
|
||
if self.restart_progress <= 100:
|
||
self.circular_progress.setValue(self.restart_progress)
|
||
self.circular_progress.setText(f"{self.restart_progress}%")
|
||
|
||
if self.restart_progress == 100:
|
||
# 🔥 到达100%,更新提示但不自动关闭
|
||
self.status_label.setText("✅ 安装完成!等待启动确认...")
|
||
self.stage_label.setText("🎉 更新成功!")
|
||
self.restart_timer.stop() # 停止定时器
|
||
else:
|
||
self.restart_timer.stop()
|
||
|
||
self.restart_timer.timeout.connect(update_restart_progress)
|
||
self.restart_timer.start(500) # 每500ms增加2%(共2.5秒)
|
||
|
||
def download_error(self, error_msg):
|
||
"""
|
||
下载失败
|
||
|
||
Args:
|
||
error_msg: 错误信息
|
||
"""
|
||
self.title_label.setText(f"❌ 下载失败")
|
||
|
||
# 错误信息截断(避免太长)
|
||
if len(error_msg) > 80:
|
||
display_msg = error_msg[:80] + "..."
|
||
else:
|
||
display_msg = error_msg
|
||
|
||
self.stage_label.setText("⚠️ 更新失败")
|
||
self.status_label.setText(display_msg)
|
||
self.status_label.setStyleSheet("color: #dc3545; font-size: 11px; font-weight: bold;")
|
||
|
||
self.cancel_button.setText("关闭")
|
||
self.cancel_button.setEnabled(True)
|
||
self.retry_label.hide()
|
||
|
||
def cancel_download(self):
|
||
"""取消下载"""
|
||
if self.current_stage == self.STAGE_DOWNLOAD and self.downloader and self.downloader.isRunning():
|
||
# 下载阶段可以取消
|
||
from PyQt5.QtWidgets import QMessageBox
|
||
reply = QMessageBox.question(
|
||
self,
|
||
"确认取消",
|
||
"确定要取消更新吗?",
|
||
QMessageBox.Yes | QMessageBox.No,
|
||
QMessageBox.No
|
||
)
|
||
|
||
if reply == QMessageBox.Yes:
|
||
self.downloader.cancel()
|
||
self.downloader.wait()
|
||
self.status_label.setText("更新已取消")
|
||
self.reject() # 会自动清理定时器
|
||
else:
|
||
# 其他阶段或下载已完成,直接关闭
|
||
self.reject() # 会自动清理定时器
|
||
|
||
def cleanup_timers(self):
|
||
"""清理所有定时器"""
|
||
if hasattr(self, 'uac_timer'):
|
||
self.uac_timer.stop()
|
||
if hasattr(self, 'install_timer'):
|
||
self.install_timer.stop()
|
||
if hasattr(self, 'restart_timer'):
|
||
self.restart_timer.stop()
|
||
|
||
def accept(self):
|
||
"""对话框接受时清理资源"""
|
||
self.cleanup_timers()
|
||
super().accept()
|
||
|
||
def reject(self):
|
||
"""对话框拒绝时清理资源"""
|
||
self.cleanup_timers()
|
||
super().reject()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
# 测试代码
|
||
import sys
|
||
from PyQt5.QtWidgets import QApplication
|
||
|
||
app = QApplication(sys.argv)
|
||
|
||
# 创建测试对话框
|
||
dialog = UpdateProgressDialog("1.5.55")
|
||
dialog.show()
|
||
|
||
# 模拟完整更新流程
|
||
def simulate_update():
|
||
# 阶段1:模拟下载 (0-50%)
|
||
def simulate_download():
|
||
total_size = 100 * 1024 * 1024 # 100MB
|
||
for i in range(0, 101, 5):
|
||
downloaded = int(i * total_size / 100)
|
||
dialog.update_progress(downloaded, total_size)
|
||
QApplication.processEvents()
|
||
QTimer.singleShot(100, lambda: None) # 延迟
|
||
|
||
# 下载完成
|
||
QTimer.singleShot(2000, lambda: dialog.download_finished("/tmp/test.exe"))
|
||
|
||
# 阶段2:模拟UAC授权 (50-60%)
|
||
QTimer.singleShot(5000, lambda: dialog.uac_authorized())
|
||
|
||
simulate_download()
|
||
|
||
QTimer.singleShot(500, simulate_update)
|
||
|
||
sys.exit(app.exec_())
|