feat: 实现网络速度监控任务栏插件

- 添加 C++ 后端 NetworkMonitorApplet,监控网络下载/上传速度
- 添加 QML 前端 networkview.qml,显示实时速度和流量统计
- 支持多网络接口切换
- 任务栏图标显示实时速度,高速时变色
- 悬停显示速度摘要,点击显示详细信息弹窗
- 添加安装脚本 install.sh
- 添加 .gitignore 排除构建目录
This commit is contained in:
2026-07-18 10:57:23 +08:00
parent 0b3cc74bc8
commit 011a5c7e86
8 changed files with 969 additions and 29 deletions
+25
View File
@@ -0,0 +1,25 @@
# Build directories
build/
CMakeFiles/
cmake-build-*/
# IDE files
.vscode/
.idea/
*.swp
*.swo
*~
# Compiled files
*.o
*.so
*.so.*
*.dylib
# Qt Creator
*.pro.user
*.pro.user.*
# System files
.DS_Store
Thumbs.db
+35 -3
View File
@@ -1,7 +1,39 @@
cmake_minimum_required(VERSION 3.16)
project(JNetApplet)
project(JNetApplet VERSION 1.0.0 LANGUAGES CXX)
find_package(DDEShell REQUIRED NO_MODULE)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON)
ds_install_package(PACKAGE space.jokul.JNetApplet)
find_package(Qt6 REQUIRED COMPONENTS Core Quick DBus)
find_package(Dtk6 REQUIRED COMPONENTS Core)
find_package(DDEShell REQUIRED)
# 插件 ID
set(PLUGIN_ID "space.jokul.JNetApplet")
add_library(space.jokul.JNetApplet SHARED
networkmonitorapplet.h
networkmonitorapplet.cpp
)
set_target_properties(space.jokul.JNetApplet PROPERTIES PREFIX "")
target_include_directories(space.jokul.JNetApplet PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
/usr/include/dde-shell
)
target_link_libraries(space.jokul.JNetApplet PRIVATE
Qt6::Core
Qt6::Quick
Qt6::DBus
Dtk6::Core
Dde::Shell
)
# 安装到dde-shell插件目录
install(TARGETS space.jokul.JNetApplet DESTINATION /usr/lib/x86_64-linux-gnu/dde-shell)
install(FILES package/metadata.json DESTINATION /usr/share/dde-shell/${PLUGIN_ID})
install(FILES package/networkview.qml DESTINATION /usr/share/dde-shell/${PLUGIN_ID})
Executable
+25
View File
@@ -0,0 +1,25 @@
#!/bin/bash
# 网络速度监控任务栏插件安装脚本
set -e
echo "Building JNetApplet..."
# 清理旧的构建目录
rm -rf build
# 配置CMake
cmake -Bbuild
# 构建
cmake --build build
echo "Installing JNetApplet (requires sudo)..."
# 安装
sudo cmake --install build
echo "Installation complete!"
echo "Please restart dde-shell to load the plugin:"
echo " systemctl --user restart dde-shell@DDE"
+262
View File
@@ -0,0 +1,262 @@
// SPDX-FileCopyrightText: 2024 MyCompany
//
// SPDX-License-Identifier: LGPL-3.0-or-later
#include "networkmonitorapplet.h"
#include <pluginfactory.h>
#include <QFile>
#include <QTextStream>
#include <QDebug>
#include <QDir>
#include <QRegularExpression>
DS_BEGIN_NAMESPACE
NetworkMonitorApplet::NetworkMonitorApplet(QObject *parent)
: DApplet(parent)
, m_refreshTimer(nullptr)
, m_lastRxBytes(0)
, m_lastTxBytes(0)
, m_downloadSpeed(0)
, m_uploadSpeed(0)
, m_totalDownload(0)
, m_totalUpload(0)
, m_ready(false)
, m_firstUpdate(true)
{
}
NetworkMonitorApplet::~NetworkMonitorApplet()
{
}
bool NetworkMonitorApplet::load()
{
return DApplet::load();
}
bool NetworkMonitorApplet::init()
{
detectInterfaces();
// 启动定时刷新
m_refreshTimer = new QTimer(this);
connect(m_refreshTimer, &QTimer::timeout, this, &NetworkMonitorApplet::refresh);
m_refreshTimer->start(1000); // 每秒刷新一次
// 初始读取
readNetworkStats();
return DApplet::init();
}
qint64 NetworkMonitorApplet::downloadSpeed() const
{
return m_downloadSpeed;
}
qint64 NetworkMonitorApplet::uploadSpeed() const
{
return m_uploadSpeed;
}
qint64 NetworkMonitorApplet::totalDownload() const
{
return m_totalDownload;
}
qint64 NetworkMonitorApplet::totalUpload() const
{
return m_totalUpload;
}
QStringList NetworkMonitorApplet::networkInterfaces() const
{
return m_interfaceList;
}
QStringList NetworkMonitorApplet::interfaceStats() const
{
QStringList stats;
for (const QString &name : m_interfaceList) {
if (m_interfaces.contains(name)) {
const NetworkInterface &iface = m_interfaces[name];
stats << QString("%1|%2|%3|%4|%5")
.arg(iface.name)
.arg(iface.rxBytes)
.arg(iface.txBytes)
.arg(iface.rxPackets)
.arg(iface.txPackets);
}
}
return stats;
}
bool NetworkMonitorApplet::ready() const
{
return m_ready;
}
QString NetworkMonitorApplet::activeInterface() const
{
return m_activeInterface;
}
void NetworkMonitorApplet::refresh()
{
readNetworkStats();
}
void NetworkMonitorApplet::setActiveInterface(const QString &interface)
{
if (m_activeInterface != interface) {
m_activeInterface = interface;
m_firstUpdate = true;
emit activeInterfaceChanged();
}
}
void NetworkMonitorApplet::readNetworkStats()
{
QFile file("/proc/net/dev");
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
qDebug() << "Failed to open /proc/net/dev";
return;
}
QTextStream stream(&file);
QStringList lines = stream.readAll().split('\n', Qt::SkipEmptyParts);
file.close();
QMap<QString, NetworkInterface> newInterfaces;
// 跳过前两行标题
for (int i = 2; i < lines.size(); ++i) {
QString line = lines[i].trimmed();
if (line.isEmpty()) continue;
// 格式: "interface: rx_bytes rx_packets rx_errors rx_dropped ... tx_bytes tx_packets tx_errors tx_dropped ..."
QRegularExpression re("^([^:]+):\\s+(\\d+)\\s+(\\d+)\\s+(\\d+)\\s+(\\d+)\\s+\\d+\\s+\\d+\\s+\\d+\\s+\\d+\\s+(\\d+)\\s+(\\d+)\\s+(\\d+)\\s+(\\d+)");
QRegularExpressionMatch match = re.match(line);
if (match.hasMatch()) {
NetworkInterface iface;
iface.name = match.captured(1);
iface.rxBytes = match.captured(2).toLongLong();
iface.rxPackets = match.captured(3).toLongLong();
iface.rxErrors = match.captured(4).toLongLong();
iface.rxDropped = match.captured(5).toLongLong();
iface.txBytes = match.captured(6).toLongLong();
iface.txPackets = match.captured(7).toLongLong();
iface.txErrors = match.captured(8).toLongLong();
iface.txDropped = match.captured(9).toLongLong();
// 过滤掉回环接口和虚拟接口
if (iface.name != "lo" && !iface.name.startsWith("veth") &&
!iface.name.startsWith("docker") && !iface.name.startsWith("br-")) {
newInterfaces[iface.name] = iface;
}
}
}
// 检测接口变化
if (newInterfaces.keys() != m_interfaces.keys()) {
m_interfaceList = newInterfaces.keys();
m_interfaces = newInterfaces;
// 自动选择活动接口
if (m_activeInterface.isEmpty() && !m_interfaceList.isEmpty()) {
// 优先选择有流量的接口
for (const QString &name : m_interfaceList) {
const NetworkInterface &iface = m_interfaces[name];
if (iface.rxBytes > 0 || iface.txBytes > 0) {
m_activeInterface = name;
break;
}
}
if (m_activeInterface.isEmpty()) {
m_activeInterface = m_interfaceList.first();
}
emit activeInterfaceChanged();
}
m_ready = !m_interfaceList.isEmpty();
emit interfacesChanged();
emit readyChanged();
} else {
m_interfaces = newInterfaces;
}
calculateSpeed();
emit statsChanged();
}
void NetworkMonitorApplet::calculateSpeed()
{
qint64 currentRxBytes = getActiveRxBytes();
qint64 currentTxBytes = getActiveTxBytes();
if (m_firstUpdate) {
m_firstUpdate = false;
m_lastRxBytes = currentRxBytes;
m_lastTxBytes = currentTxBytes;
return;
}
// 计算速度(字节/秒)
m_downloadSpeed = currentRxBytes - m_lastRxBytes;
m_uploadSpeed = currentTxBytes - m_lastTxBytes;
// 更新总量
m_totalDownload = currentRxBytes;
m_totalUpload = currentTxBytes;
m_lastRxBytes = currentRxBytes;
m_lastTxBytes = currentTxBytes;
emit speedChanged();
emit totalChanged();
}
void NetworkMonitorApplet::detectInterfaces()
{
QDir sysClass("/sys/class/net");
if (!sysClass.exists()) return;
QStringList entries = sysClass.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
m_interfaceList.clear();
for (const QString &entry : entries) {
// 过滤回环接口
if (entry != "lo") {
m_interfaceList << entry;
}
}
m_ready = !m_interfaceList.isEmpty();
emit interfacesChanged();
emit readyChanged();
}
qint64 NetworkMonitorApplet::getActiveRxBytes() const
{
if (m_activeInterface.isEmpty() || !m_interfaces.contains(m_activeInterface)) {
return 0;
}
return m_interfaces[m_activeInterface].rxBytes;
}
qint64 NetworkMonitorApplet::getActiveTxBytes() const
{
if (m_activeInterface.isEmpty() || !m_interfaces.contains(m_activeInterface)) {
return 0;
}
return m_interfaces[m_activeInterface].txBytes;
}
D_APPLET_CLASS(NetworkMonitorApplet)
DS_END_NAMESPACE
#include "networkmonitorapplet.moc"
+95
View File
@@ -0,0 +1,95 @@
// SPDX-FileCopyrightText: 2024 MyCompany
//
// SPDX-License-Identifier: LGPL-3.0-or-later
#pragma once
#include <applet.h>
#include <QString>
#include <QStringList>
#include <QTimer>
#include <QVariantList>
#include <QMap>
DS_BEGIN_NAMESPACE
struct NetworkInterface {
QString name; // 接口名称,如 "eth0", "wlan0"
qint64 rxBytes; // 接收字节数
qint64 txBytes; // 发送字节数
qint64 rxPackets; // 接收包数
qint64 txPackets; // 发送包数
qint64 rxErrors; // 接收错误数
qint64 txErrors; // 发送错误数
qint64 rxDropped; // 接收丢弃数
qint64 txDropped; // 发送丢弃数
};
class NetworkMonitorApplet : public DApplet
{
Q_OBJECT
Q_PROPERTY(qint64 downloadSpeed READ downloadSpeed NOTIFY speedChanged)
Q_PROPERTY(qint64 uploadSpeed READ uploadSpeed NOTIFY speedChanged)
Q_PROPERTY(qint64 totalDownload READ totalDownload NOTIFY totalChanged)
Q_PROPERTY(qint64 totalUpload READ totalUpload NOTIFY totalChanged)
Q_PROPERTY(QStringList networkInterfaces READ networkInterfaces NOTIFY interfacesChanged)
Q_PROPERTY(QStringList interfaceStats READ interfaceStats NOTIFY statsChanged)
Q_PROPERTY(bool ready READ ready NOTIFY readyChanged)
Q_PROPERTY(QString activeInterface READ activeInterface NOTIFY activeInterfaceChanged)
public:
explicit NetworkMonitorApplet(QObject *parent = nullptr);
~NetworkMonitorApplet();
virtual bool load() override;
virtual bool init() override;
qint64 downloadSpeed() const;
qint64 uploadSpeed() const;
qint64 totalDownload() const;
qint64 totalUpload() const;
QStringList networkInterfaces() const;
QStringList interfaceStats() const;
bool ready() const;
QString activeInterface() const;
Q_INVOKABLE void refresh();
Q_INVOKABLE void setActiveInterface(const QString &interface);
signals:
void speedChanged();
void totalChanged();
void interfacesChanged();
void statsChanged();
void readyChanged();
void activeInterfaceChanged();
private:
void readNetworkStats();
void calculateSpeed();
void detectInterfaces();
qint64 getActiveRxBytes() const;
qint64 getActiveTxBytes() const;
QTimer *m_refreshTimer;
QMap<QString, NetworkInterface> m_interfaces;
QStringList m_interfaceList;
QString m_activeInterface;
// 速度计算
qint64 m_lastRxBytes;
qint64 m_lastTxBytes;
qint64 m_downloadSpeed;
qint64 m_uploadSpeed;
// 总量
qint64 m_totalDownload;
qint64 m_totalUpload;
bool m_ready;
bool m_firstUpdate;
};
DS_END_NAMESPACE
-25
View File
@@ -1,25 +0,0 @@
// SPDX-FileCopyrightText: 2024 MyCompany
// SPDX-License-Identifier: LGPL-3.0-or-later
import QtQuick 2.11
import QtQuick.Controls 2.4
import org.deepin.ds 1.0
AppletItem {
objectName: "JnetApplet"
implicitWidth: 100
implicitHeight: 100
Rectangle {
anchors.fill: parent
color: "#2ecc71"
radius: 8
Text {
anchors.centerIn: parent
text: "你好,世界!"
font.pixelSize: 14
color: "white"
}
}
}
+6 -1
View File
@@ -2,6 +2,11 @@
"Plugin": {
"Version": "1.0",
"Id": "space.jokul.JNetApplet",
"Url": "main.qml"
"Url": "networkview.qml",
"Parent": "org.deepin.ds.dock",
"Name": "Network Monitor",
"Name[zh_CN]": "网络速度监控",
"Description": "Monitor network speed and traffic",
"Description[zh_CN]": "监控网络速度和流量"
}
}
+521
View File
@@ -0,0 +1,521 @@
// SPDX-FileCopyrightText: 2024 MyCompany
//
// SPDX-License-Identifier: LGPL-3.0-or-later
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtQuick.Window 2.15
import org.deepin.ds 1.0
import org.deepin.ds.dock 1.0
import org.deepin.dtk 1.0
AppletItem {
id: root
objectName: "network monitor applet"
property int dockOrder: 21
property int dockSize: Panel.rootObject.dockItemMaxSize || 48
implicitWidth: dockSize
implicitHeight: dockSize
readonly property var applet: Applet
readonly property bool ready: applet ? applet.ready : false
readonly property qint64 downloadSpeed: applet ? applet.downloadSpeed : 0
readonly property qint64 uploadSpeed: applet ? applet.uploadSpeed : 0
readonly property qint64 totalDownload: applet ? applet.totalDownload : 0
readonly property qint64 totalUpload: applet ? applet.totalUpload : 0
readonly property string activeInterface: applet ? (applet.activeInterface || "") : ""
readonly property var networkInterfaces: applet ? (applet.networkInterfaces || []) : []
readonly property var interfaceStats: applet ? (applet.interfaceStats || []) : []
// 格式化速度显示
function formatSpeed(bytesPerSec) {
if (bytesPerSec < 1024) {
return bytesPerSec.toFixed(0) + " B/s"
} else if (bytesPerSec < 1024 * 1024) {
return (bytesPerSec / 1024).toFixed(1) + " KB/s"
} else if (bytesPerSec < 1024 * 1024 * 1024) {
return (bytesPerSec / (1024 * 1024)).toFixed(2) + " MB/s"
} else {
return (bytesPerSec / (1024 * 1024 * 1024)).toFixed(2) + " GB/s"
}
}
// 格式化总量显示
function formatTotal(bytes) {
if (bytes < 1024) {
return bytes.toFixed(0) + " B"
} else if (bytes < 1024 * 1024) {
return (bytes / 1024).toFixed(1) + " KB"
} else if (bytes < 1024 * 1024 * 1024) {
return (bytes / (1024 * 1024)).toFixed(1) + " MB"
} else {
return (bytes / (1024 * 1024 * 1024)).toFixed(2) + " GB"
}
}
// 根据速度计算图标颜色
readonly property color iconColor: {
if (downloadSpeed > 10 * 1024 * 1024) return Qt.rgba(220/255, 38/255, 38/255, 1) // 高速红色
if (downloadSpeed > 1 * 1024 * 1024) return Qt.rgba(245/255, 158/255, 11/255, 1) // 中速橙色
return root.primaryText // 正常颜色
}
property Palette basePalette: DockPalette.iconTextPalette
readonly property color primaryText: Qt.rgba(basePalette.r, basePalette.g, basePalette.b, 0.95)
readonly property color secondaryText: Qt.rgba(basePalette.r, basePalette.g, basePalette.b, 0.8)
readonly property color tertiaryText: Qt.rgba(basePalette.r, basePalette.g, basePalette.b, 0.65)
readonly property color cardBackground: Qt.rgba(basePalette.r, basePalette.g, basePalette.b, 0.06)
readonly property color cardBorder: Qt.rgba(basePalette.r, basePalette.g, basePalette.b, 0.1)
readonly property color accentBlue: Qt.rgba(20 / 255, 80 / 255, 160 / 255, 1)
readonly property color accentBlueLight: Qt.rgba(20 / 255, 80 / 255, 160 / 255, 0.12)
// 图标区域
Rectangle {
anchors.centerIn: parent
width: dockSize * 0.7
height: dockSize * 0.7
color: "transparent"
radius: width * 0.45
border.width: 1
border.color: root.iconColor
Column {
anchors.centerIn: parent
spacing: 2
// 下载速度
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: "↓"
font.pixelSize: root.dockSize * 0.18
color: root.iconColor
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: formatSpeed(root.downloadSpeed)
font.pixelSize: root.dockSize * 0.12
font.bold: true
color: root.iconColor
}
// 上传速度
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: "↑"
font.pixelSize: root.dockSize * 0.18
color: root.iconColor
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: formatSpeed(root.uploadSpeed)
font.pixelSize: root.dockSize * 0.12
font.bold: true
color: root.iconColor
}
}
}
// 悬停提示
PanelToolTip {
id: toolTip
text: root.ready ? buildToolTipText() : qsTr("No network interface detected")
toolTipX: DockPanelPositioner.x
toolTipY: DockPanelPositioner.y
}
// 统计数据刷新定时器
Timer {
id: statsRefreshTimer
interval: 1000
repeat: true
onTriggered: {
if (root.applet) {
root.applet.refresh()
}
}
}
Timer {
id: toolTipShowTimer
interval: 50
onTriggered: {
const point = root.mapToItem(null, root.width / 2, root.height / 2)
toolTip.DockPanelPositioner.bounding = Qt.rect(point.x, point.y, toolTip.width, toolTip.height)
toolTip.open()
}
}
HoverHandler {
onHoveredChanged: {
if (hovered && !networkPopup.popupVisible) {
toolTipShowTimer.start()
if (root.applet) {
root.applet.refresh()
}
statsRefreshTimer.start()
} else {
if (toolTipShowTimer.running) {
toolTipShowTimer.stop()
}
toolTip.close()
if (!networkPopup.popupVisible) {
statsRefreshTimer.stop()
}
}
}
}
// 弹出窗口
PanelPopup {
id: networkPopup
width: 400
height: 350
popupX: DockPanelPositioner.x
popupY: DockPanelPositioner.y
onPopupVisibleChanged: {
if (popupVisible) {
toolTip.close()
statsRefreshTimer.start()
if (root.applet) {
root.applet.refresh()
}
} else {
statsRefreshTimer.stop()
}
}
Control {
id: popupContainer
anchors.fill: parent
padding: 16
contentItem: ColumnLayout {
spacing: 14
// 标题区域
RowLayout {
Layout.fillWidth: true
spacing: 10
Item { Layout.fillWidth: true }
Rectangle {
width: 28
height: 28
color: accentBlueLight
radius: 8
Text {
anchors.centerIn: parent
text: "NET"
font.pixelSize: 12
font.bold: true
color: accentBlue
}
}
Text {
text: qsTr("Network Monitor")
font.pixelSize: 16
font.bold: true
color: root.primaryText
}
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.refresh()
}
}
}
// 旋转动画
RotationAnimator on rotation {
id: refreshSpin
running: false
from: 0
to: 360
duration: 500
}
Connections {
target: refreshIconArea
function onClicked() { refreshSpin.start() }
}
}
}
// 分隔线
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 1
color: root.cardBorder
}
// 当前接口信息
RowLayout {
Layout.fillWidth: true
spacing: 8
visible: root.ready
Text {
text: qsTr("Interface:")
font.pixelSize: 12
color: root.secondaryText
}
Text {
text: root.activeInterface
font.pixelSize: 12
font.bold: true
color: root.primaryText
Layout.fillWidth: true
elide: Text.ElideRight
}
}
// 速度统计卡片
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 120
color: root.cardBackground
radius: 10
border.width: 1
border.color: root.cardBorder
ColumnLayout {
anchors.fill: parent
anchors.margins: 12
spacing: 12
// 下载速度
RowLayout {
Layout.fillWidth: true
spacing: 8
Text {
text: "↓ " + qsTr("Download")
font.pixelSize: 13
color: root.secondaryText
}
Item { Layout.fillWidth: true }
Text {
text: formatSpeed(root.downloadSpeed)
font.pixelSize: 18
font.bold: true
color: root.accentBlue
}
}
// 上传速度
RowLayout {
Layout.fillWidth: true
spacing: 8
Text {
text: "↑ " + qsTr("Upload")
font.pixelSize: 13
color: root.secondaryText
}
Item { Layout.fillWidth: true }
Text {
text: formatSpeed(root.uploadSpeed)
font.pixelSize: 18
font.bold: true
color: Qt.rgba(22/255, 163/255, 74/255, 1)
}
}
}
}
// 总量统计卡片
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 80
color: root.cardBackground
radius: 10
border.width: 1
border.color: root.cardBorder
ColumnLayout {
anchors.fill: parent
anchors.margins: 12
spacing: 8
Text {
text: qsTr("Total Data")
font.pixelSize: 12
font.bold: true
color: root.secondaryText
}
RowLayout {
Layout.fillWidth: true
spacing: 20
// 下载总量
ColumnLayout {
spacing: 2
Text {
text: "↓ " + qsTr("Download")
font.pixelSize: 11
color: root.tertiaryText
}
Text {
text: formatTotal(root.totalDownload)
font.pixelSize: 14
font.bold: true
color: root.primaryText
}
}
// 上传总量
ColumnLayout {
spacing: 2
Text {
text: "↑ " + qsTr("Upload")
font.pixelSize: 11
color: root.tertiaryText
}
Text {
text: formatTotal(root.totalUpload)
font.pixelSize: 14
font.bold: true
color: root.primaryText
}
}
}
}
}
// 接口切换下拉框
ComboBox {
id: interfaceCombo
Layout.fillWidth: true
Layout.preferredHeight: 32
visible: root.networkInterfaces.length > 1
model: root.networkInterfaces
currentIndex: {
var idx = root.networkInterfaces.indexOf(root.activeInterface)
return idx >= 0 ? idx : 0
}
onActivated: {
if (root.applet && root.networkInterfaces[index]) {
root.applet.setActiveInterface(root.networkInterfaces[index])
}
}
background: Rectangle {
color: interfaceCombo.hovered ? root.accentBlueLight : root.cardBackground
radius: 6
border.width: 1
border.color: root.cardBorder
}
contentItem: Text {
text: interfaceCombo.displayText
font.pixelSize: 12
color: root.primaryText
verticalAlignment: Text.AlignVCenter
leftPadding: 10
}
}
// 未检测到网络接口提示
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 60
color: root.cardBackground
radius: 10
border.width: 1
border.color: root.cardBorder
visible: !root.ready
ColumnLayout {
anchors.centerIn: parent
spacing: 6
Text {
text: "⚠"
font.pixelSize: 20
}
Text {
text: qsTr("No network interface detected")
font.pixelSize: 12
color: root.secondaryText
}
}
}
Item { Layout.fillHeight: true }
}
}
Component.onCompleted: {
DockPanelPositioner.bounding = Qt.binding(function () {
const point = root.mapToItem(null, root.width / 2, root.height / 2)
return Qt.rect(point.x, point.y, networkPopup.width, networkPopup.height)
})
}
}
// 构建 tooltip 文本
function buildToolTipText() {
if (!root.ready) {
return qsTr("No network interface detected")
}
var lines = []
lines.push("↓ " + formatSpeed(root.downloadSpeed))
lines.push("↑ " + formatSpeed(root.uploadSpeed))
lines.push(root.activeInterface)
return lines.join(" | ")
}
// 点击处理
TapHandler {
acceptedButtons: Qt.LeftButton
gesturePolicy: TapHandler.ReleaseWithinBounds
onTapped: {
if (networkPopup.popupVisible) {
networkPopup.close()
} else {
Panel.requestClosePopup()
const point = root.mapToItem(null, root.width / 2, root.height / 2)
networkPopup.DockPanelPositioner.bounding = Qt.rect(point.x, point.y, networkPopup.width, networkPopup.height)
networkPopup.open()
}
toolTip.close()
}
}
}