feat: 设置窗口、合并温度趋势图、UI优化

- 新增设置窗口(右键打开):无边框毛玻璃风格,温度告警开关,QSettings持久化
- 合并温度趋势图:多GPU多色折线,图例,Y轴刻度,填充区域,最新温度标注
- 显卡排序:核显在前独显在后(std::stable_sort by boot_vga)
- 卡片优化:删除温度行和频率/功耗行,驱动名+版本两端对齐,高度压缩至78
- 弹窗优化:宽度480,右上角刷新图标替代底部按钮
- tooltip修复:删除自定义Rectangle tooltip改用标准ToolTip
- 翻译更新:设置/常规设置/温度告警/温度趋势等
This commit is contained in:
2026-07-14 21:31:00 +08:00
parent 2570a028ea
commit f7c6801793
5 changed files with 520 additions and 242 deletions
+439 -242
View File
@@ -5,7 +5,7 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import Qt.labs.platform 1.1 as LP
import QtQuick.Window 2.15
import org.deepin.ds 1.0
import org.deepin.ds.dock 1.0
import org.deepin.dtk 1.0
@@ -125,8 +125,8 @@ AppletItem {
// 弹出窗口
PanelPopup {
id: driverPopup
width: 360
height: 420
width: 480
height: 440
popupX: DockPanelPositioner.x
popupY: DockPanelPositioner.y
@@ -180,6 +180,46 @@ AppletItem {
}
Item { Layout.fillWidth: true }
// 刷新图标
Rectangle {
width: 28
height: 28
radius: 6
color: refreshIconArea.containsMouse ? root.accentBlueLight : "transparent"
Text {
anchors.centerIn: parent
text: "⟳"
font.pixelSize: 18
color: refreshIconArea.containsMouse ? root.accentBlue : root.secondaryText
}
MouseArea {
id: refreshIconArea
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
if (root.applet) {
root.applet.refreshDeviceInfo()
}
}
}
// 旋转动画
RotationAnimator on rotation {
id: refreshSpin
running: false
from: 0
to: 360
duration: 500
}
Connections {
target: refreshIconArea
function onClicked() { refreshSpin.start() }
}
}
}
// 分隔线
@@ -292,7 +332,7 @@ AppletItem {
delegate: Rectangle {
id: gpuCard
width: gpuColumn.width
height: 120
height: 78
color: root.cardBackground
radius: 10
border.width: 1
@@ -360,48 +400,15 @@ AppletItem {
return v === "-1" || v === "" ? "-" : v
}
// 自定义深色主题悬浮提示(替代系统默认白色 tooltip)
// 第一张卡片 tooltip 显示在下方,避免被弹框顶部裁剪
Rectangle {
visible: cardHover.hovered
anchors.top: index === 0 ? parent.bottom : undefined
anchors.topMargin: index === 0 ? 6 : 0
anchors.bottom: index === 0 ? undefined : parent.top
anchors.bottomMargin: index === 0 ? 0 : 6
anchors.horizontalCenter: parent.horizontalCenter
width: tooltipContent.implicitWidth + 24
height: tooltipContent.implicitHeight + 16
color: Qt.rgba(0, 0, 0, 0.85)
radius: 8
z: 100
ColumnLayout {
id: tooltipContent
anchors.centerIn: parent
spacing: 3
Text {
text: qsTr("GPU: ") + parseGpuName(modelData)
color: "white"
font.pixelSize: 11
font.bold: true
}
Text {
text: qsTr("Driver: ") + parseDriverName(modelData)
color: Qt.rgba(1, 1, 1, 0.65)
font.pixelSize: 10
}
Text {
text: qsTr("Version: ") + (parseDriverVersion(modelData) || qsTr("Unknown"))
color: Qt.rgba(1, 1, 1, 0.65)
font.pixelSize: 10
}
}
}
HoverHandler {
id: cardHover
}
ToolTip.text: qsTr("GPU: ") + parseGpuName(modelData) + "\n"
+ qsTr("Driver: ") + parseDriverName(modelData) + "\n"
+ qsTr("Version: ") + (parseDriverVersion(modelData) || qsTr("Unknown"))
ToolTip.visible: cardHover.hovered
ToolTip.delay: 300
ColumnLayout {
anchors.fill: parent
@@ -465,12 +472,23 @@ AppletItem {
}
}
Text {
text: parseDriverInfo(modelData)
font.pixelSize: 11
color: root.secondaryText
RowLayout {
Layout.fillWidth: true
elide: Text.ElideRight
spacing: 4
Text {
text: parseDriverName(modelData)
font.pixelSize: 11
color: root.secondaryText
}
Item { Layout.fillWidth: true }
Text {
text: parseDriverVersion(modelData) || qsTr("Unknown")
font.pixelSize: 11
color: root.tertiaryText
}
}
}
}
@@ -484,142 +502,48 @@ AppletItem {
Layout.fillWidth: true
spacing: 12
// 温度
Text {
text: tempStr !== "—" ? tempStr + "°C" : "—"
font.pixelSize: 12
color: gpuCard.tempColor
font.bold: tempStr !== "—"
}
Item { Layout.fillWidth: true }
// 显存信息
Text {
text: (memUsedStr !== "" && memTotalStr !== "") ? memUsedStr + "/" + memTotalStr + " MB" : ""
text: (memUsedStr !== "-" && memTotalStr !== "-") ? memUsedStr + "/" + memTotalStr + " MB" : "-"
font.pixelSize: 11
color: root.secondaryText
}
}
// GPU 使用率进度条
RowLayout {
Layout.fillWidth: true
spacing: 8
Text {
text: "GPU"
font.pixelSize: 11
color: root.tertiaryText
}
Rectangle {
Layout.fillWidth: true
height: 4
color: root.cardBorder
radius: 2
Rectangle {
width: Math.min(gpuCard.gpuUsagePercent, 100) * parent.width / 100
height: parent.height
color: root.accentBlue
radius: 2
}
}
Text {
text: gpuUsageStr !== "-" ? gpuUsageStr + "%" : "-"
font.pixelSize: 11
color: root.secondaryText
font.bold: gpuUsageStr !== "-"
}
}
// 频率/功耗/风扇信息行
RowLayout {
Layout.fillWidth: true
spacing: 8
// GPU 频率
Text {
text: gpuClockStr !== "-" ? gpuClockStr + " MHz" : "-"
font.pixelSize: 10
color: root.tertiaryText
visible: gpuClockStr !== "-"
}
// 功耗
Text {
text: powerStr !== "-" ? powerStr + " W" : ""
font.pixelSize: 10
color: root.tertiaryText
visible: powerStr !== "-"
}
// 风扇转速
Text {
text: fanStr !== "-" ? qsTr("Fan") + " " + fanStr + "%" : ""
font.pixelSize: 10
color: root.tertiaryText
visible: fanStr !== "-"
}
Item { Layout.fillWidth: true }
// 显存频率
Text {
text: memClockStr !== "-" ? memClockStr + " MHz" : ""
font.pixelSize: 10
color: root.tertiaryText
visible: memClockStr !== "-"
}
}
// GPU 使用率进度条
RowLayout {
spacing: 8
// 温度趋势迷你图
Canvas {
id: tempCanvas
Layout.fillWidth: true
Layout.preferredHeight: 24
visible: {
if (!root.applet || !root.applet.tempHistory) return false
var h = root.applet.tempHistory[index]
return h && h.length > 1
}
onPaint: {
var ctx = getContext("2d")
ctx.reset()
var w = width
var h = height
if (!root.applet || !root.applet.tempHistory) return
var history = root.applet.tempHistory[index]
if (!history || history.length < 2) return
// 有效温度范围 20-100°C
var minT = 20, maxT = 100
var len = history.length
ctx.strokeStyle = gpuCard.tempColor
ctx.lineWidth = 1.5
ctx.beginPath()
for (var i = 0; i < len; i++) {
var x = (i / (len - 1)) * w
var t = history[i]
if (t < 0) t = minT
var y = h - ((t - minT) / (maxT - minT)) * h
if (y < 0) y = 0
if (y > h) y = h
if (i === 0) ctx.moveTo(x, y)
else ctx.lineTo(x, y)
Text {
text: "GPU"
font.pixelSize: 11
color: root.tertiaryText
}
ctx.stroke()
}
// 温度历史变化时重绘
Connections {
target: root.applet
function onTempHistoryChanged() { tempCanvas.requestPaint() }
Rectangle {
Layout.preferredWidth: 140
height: 4
color: root.cardBorder
radius: 2
Rectangle {
width: Math.min(gpuCard.gpuUsagePercent, 100) * parent.width / 100
height: parent.height
color: root.accentBlue
radius: 2
}
}
Text {
text: gpuUsageStr !== "-" ? gpuUsageStr + "%" : "-"
font.pixelSize: 11
color: root.secondaryText
font.bold: gpuUsageStr !== "-"
}
}
}
}
}
}
@@ -655,6 +579,179 @@ AppletItem {
}
}
// 合并温度趋势图
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 90
color: root.cardBackground
radius: 10
border.width: 1
border.color: root.cardBorder
visible: {
if (!root.applet || !root.applet.tempHistory) return false
var th = root.applet.tempHistory
for (var i = 0; i < th.length; i++) {
if (th[i] && th[i].length > 1) return true
}
return false
}
ColumnLayout {
anchors.fill: parent
anchors.margins: 10
spacing: 6
// 图例
RowLayout {
Layout.fillWidth: true
spacing: 12
Repeater {
model: root.ready ? root.deviceInfo.split("\n").filter(function(line) { return line.trim().length > 0 }) : []
RowLayout {
spacing: 4
Rectangle {
width: 10
height: 10
radius: 2
color: {
var colors = [
Qt.rgba(22/255, 163/255, 74/255, 1),
Qt.rgba(20/255, 80/255, 160/255, 1),
Qt.rgba(245/255, 158/255, 11/255, 1)
]
return colors[index] || colors[0]
}
}
Text {
text: parseGpuVendorShort(modelData)
font.pixelSize: 10
color: root.secondaryText
}
}
}
Item { Layout.fillWidth: true }
Text {
text: qsTr("Temperature Trend")
font.pixelSize: 10
color: root.tertiaryText
}
}
// 趋势图 Canvas
Canvas {
id: mergedTempCanvas
Layout.fillWidth: true
Layout.fillHeight: true
onPaint: {
var ctx = getContext("2d")
ctx.reset()
var w = width
var h = height
if (!root.applet || !root.applet.tempHistory) return
var th = root.applet.tempHistory
var minT = 20, maxT = 100
var pad = 4
var colors = [
Qt.rgba(22/255, 163/255, 74/255, 1),
Qt.rgba(20/255, 80/255, 160/255, 1),
Qt.rgba(245/255, 158/255, 11/255, 1)
]
// 1. 绘制网格线和刻度
ctx.font = "9px sans-serif"
ctx.fillStyle = Qt.rgba(1, 1, 1, 0.25)
var temps = [40, 60, 80]
for (var ti = 0; ti < temps.length; ti++) {
var t = temps[ti]
var y = h - ((t - minT) / (maxT - minT)) * (h - pad * 2) - pad
ctx.strokeStyle = Qt.rgba(1, 1, 1, 0.06)
ctx.lineWidth = 1
ctx.setLineDash(t === 80 ? [3, 3] : [])
ctx.beginPath()
ctx.moveTo(24, y)
ctx.lineTo(w, y)
ctx.stroke()
ctx.setLineDash([])
ctx.fillText(t + "°C", 2, y + 3)
}
// 2. 绘制每个 GPU 的温度线
for (var g = 0; g < th.length; g++) {
var history = th[g]
if (!history || history.length < 2) continue
var len = history.length
var color = colors[g] || colors[0]
// 填充区域
ctx.beginPath()
for (var i = 0; i < len; i++) {
var x = 24 + (i / (len - 1)) * (w - 24)
var temp = history[i]
if (temp < 0) temp = minT
var y2 = h - ((temp - minT) / (maxT - minT)) * (h - pad * 2) - pad
if (i === 0) ctx.moveTo(x, y2)
else ctx.lineTo(x, y2)
}
ctx.lineTo(24 + (w - 24), h)
ctx.lineTo(24, h)
ctx.closePath()
var grad = ctx.createLinearGradient(0, 0, 0, h)
grad.addColorStop(0, Qt.rgba(color.r, color.g, color.b, 0.15))
grad.addColorStop(1, Qt.rgba(color.r, color.g, color.b, 0.01))
ctx.fillStyle = grad
ctx.fill()
// 折线
ctx.strokeStyle = color
ctx.lineWidth = 2
ctx.beginPath()
for (var j = 0; j < len; j++) {
var x2 = 24 + (j / (len - 1)) * (w - 24)
var t2 = history[j]
if (t2 < 0) t2 = minT
var y3 = h - ((t2 - minT) / (maxT - minT)) * (h - pad * 2) - pad
if (j === 0) ctx.moveTo(x2, y3)
else ctx.lineTo(x2, y3)
}
ctx.stroke()
// 最新数据点
var lastIdx = len - 1
var lastX = 24 + (lastIdx / (len - 1)) * (w - 24)
var lastT = history[lastIdx]
if (lastT < 0) lastT = minT
var lastY = h - ((lastT - minT) / (maxT - minT)) * (h - pad * 2) - pad
ctx.fillStyle = color
ctx.beginPath()
ctx.arc(lastX, lastY, 3, 0, Math.PI * 2)
ctx.fill()
// 最新温度标注
ctx.font = "bold 10px sans-serif"
ctx.fillStyle = color
var label = lastT + "°C"
var labelX = lastX + 6
if (labelX + 30 > w) labelX = lastX - 30
ctx.fillText(label, labelX, lastY + 3)
}
}
Connections {
target: root.applet
function onTempHistoryChanged() { mergedTempCanvas.requestPaint() }
}
}
}
}
// NVIDIA 进程列表(仅有进程时显示)
Rectangle {
Layout.fillWidth: true
@@ -772,34 +869,6 @@ AppletItem {
onClicked: if (root.applet) root.applet.switchGpu("on-demand")
}
}
// 刷新按钮
Button {
id: refreshBtn
text: qsTr("Refresh")
Layout.fillWidth: true
Layout.preferredHeight: 36
font.pixelSize: 13
background: Rectangle {
color: refreshBtn.hovered ? Qt.darker(root.accentBlue, 1.2) : root.accentBlue
radius: 8
}
contentItem: Text {
text: refreshBtn.text
color: "white"
font.pixelSize: refreshBtn.font.pixelSize
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
onClicked: {
if (root.applet) {
root.applet.refreshDeviceInfo()
}
}
}
}
}
@@ -903,53 +972,181 @@ AppletItem {
}
}
// 右键菜单(使用 Qt.labs.platform 原生菜单,通过 dock 的 MenuHelper 弹出
// 设置窗口(无边框 + 毛玻璃风格
Window {
id: settingsWindow
title: qsTr("Settings")
width: 650
height: 350
flags: Qt.FramelessWindowHint | Qt.Window
color: "transparent"
// 居中屏幕
x: (Screen.width - width) / 2
y: (Screen.height - height) / 2
// 毛玻璃风格背景
Rectangle {
anchors.fill: parent
radius: 16
color: Qt.rgba(0.12, 0.12, 0.14, 0.88)
border.width: 1
border.color: Qt.rgba(1, 1, 1, 0.1)
ColumnLayout {
anchors.fill: parent
anchors.margins: 0
spacing: 0
// 自定义标题栏
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 48
color: "transparent"
radius: 16
Rectangle {
anchors.fill: parent
anchors.bottomMargin: 0
color: Qt.rgba(1, 1, 1, 0.04)
radius: 16
RowLayout {
anchors.fill: parent
anchors.leftMargin: 20
anchors.rightMargin: 16
spacing: 10
Text {
text: qsTr("General Settings")
font.pixelSize: 15
font.bold: true
color: "#e8e8e8"
}
Item { Layout.fillWidth: true }
// 关闭按钮
Rectangle {
width: 28
height: 28
radius: 14
color: closeBtnArea.containsMouse ? Qt.rgba(255/255, 95/255, 86/255, 0.8) : "transparent"
Text {
anchors.centerIn: parent
text: "✕"
font.pixelSize: 12
color: closeBtnArea.containsMouse ? "white" : "#999999"
}
MouseArea {
id: closeBtnArea
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: settingsWindow.close()
}
}
}
}
// 窗口拖拽(使用系统移动,避免幻影)
MouseArea {
anchors.fill: parent
anchors.rightMargin: 44
onPressed: settingsWindow.startSystemMove()
}
}
// 分隔线
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 1
color: Qt.rgba(1, 1, 1, 0.08)
}
// 内容区域
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.leftMargin: 24
Layout.rightMargin: 24
Layout.topMargin: 20
spacing: 20
// 温度告警通知开关
RowLayout {
Layout.fillWidth: true
spacing: 12
ColumnLayout {
spacing: 2
Text {
text: qsTr("Temperature Alert")
font.pixelSize: 14
color: "#e0e0e0"
}
Text {
text: qsTr("Show notification when GPU temperature exceeds threshold")
font.pixelSize: 11
color: "#777777"
Layout.fillWidth: true
wrapMode: Text.WordWrap
}
}
Item { Layout.fillWidth: true }
Switch {
checked: root.applet ? root.applet.tempAlertEnabled : false
onToggled: {
if (root.applet) {
root.applet.tempAlertEnabled = checked
}
}
}
}
// 分隔线
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 1
color: Qt.rgba(1, 1, 1, 0.06)
}
// 温度告警阈值
RowLayout {
Layout.fillWidth: true
spacing: 12
Text {
text: qsTr("Alert Temperature Threshold")
font.pixelSize: 14
color: "#e0e0e0"
}
Item { Layout.fillWidth: true }
Text {
text: "80°C"
font.pixelSize: 14
font.bold: true
color: Qt.rgba(245/255, 158/255, 11/255, 1)
}
}
Item { Layout.fillHeight: true }
}
}
}
}
// 右键点击显示设置窗口
TapHandler {
acceptedButtons: Qt.RightButton
gesturePolicy: TapHandler.ReleaseWithinBounds
onTapped: {
contextMenuLoader.active = true
if (contextMenuLoader.item) {
MenuHelper.openMenu(contextMenuLoader.item)
}
}
}
Loader {
id: contextMenuLoader
active: false
sourceComponent: LP.Menu {
LP.MenuItem {
text: qsTr("Refresh")
onTriggered: {
if (root.applet) root.applet.refreshDeviceInfo()
}
}
LP.MenuItem {
text: qsTr("Switch to NVIDIA")
visible: root.gpuMode === "PRIME"
onTriggered: {
if (root.applet) root.applet.switchGpu("nvidia")
}
}
LP.MenuItem {
text: qsTr("Switch to Intel")
visible: root.gpuMode === "PRIME"
onTriggered: {
if (root.applet) root.applet.switchGpu("intel")
}
}
LP.MenuItem {
text: qsTr("Switch to On-Demand")
visible: root.gpuMode === "PRIME"
onTriggered: {
if (root.applet) root.applet.switchGpu("on-demand")
}
}
settingsWindow.show()
}
}
}