Files
shuidrop_gui/update_dialog.py

519 lines
18 KiB
Python
Raw Permalink Normal View History

#!/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):
"""
更新下载进度阶段10-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授权阶段阶段250-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进入安装阶段阶段360-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):
"""
准备重启阶段490-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_())