Files
AIO_3D_Print_Local_Screen/pages/setting_page.py
2026-05-11 00:21:16 +08:00

1257 lines
48 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from PyQt6.QtWidgets import (
QWidget,
QVBoxLayout,
QLabel,
QHBoxLayout,
QListWidget,
QStackedWidget,
QPushButton,
QLineEdit,
QMessageBox,
QFormLayout,
QComboBox,
QListWidgetItem,
QFrame,
QScrollArea,
QAbstractItemView,
QApplication,
QCheckBox,
)
from PyQt6.QtCore import Qt, QEvent, QObject, QThread, QTimer, QSize, pyqtSignal
from PyQt6.QtGui import QMouseEvent, QPixmap, QImage, QFont
import codecs
import io
import os
import subprocess
import qrcode
from utils.wifi_manager import WifiManager
from utils.floating_keyboard import FloatingKeyboard
class DragScrollArea(QScrollArea):
"""支持鼠标/触屏点击拖拽滑动的滚动区域"""
def __init__(self, parent=None):
super().__init__(parent)
self._press_pos = None
self._start_value = 0
self._dragging = False
def viewportEvent(self, event):
etype = event.type()
if etype == QEvent.Type.MouseButtonPress:
if event.button() == Qt.MouseButton.LeftButton:
self._press_pos = event.position().toPoint()
self._start_value = self.verticalScrollBar().value()
self._dragging = False
elif etype == QEvent.Type.MouseMove:
if self._press_pos and event.buttons() & Qt.MouseButton.LeftButton:
pos = event.position().toPoint()
delta = pos - self._press_pos
# 移动超过15px阈值才触发拖拽避免误触
if self._dragging or delta.manhattanLength() > 15:
self._dragging = True
new_val = self._start_value - delta.y()
# 限制在有效范围内
self.verticalScrollBar().setValue(
max(0, min(new_val, self.verticalScrollBar().maximum()))
)
return True # 拖拽中拦截事件,不传递给子控件
elif etype == QEvent.Type.MouseButtonRelease:
if event.button() == Qt.MouseButton.LeftButton and self._press_pos is not None:
was_dragging = self._dragging
self._press_pos = None
self._dragging = False
if was_dragging:
return True # 拖拽结束,拦截释放事件,避免触发子控件点击
return super().viewportEvent(event)
class DraggableListWidget(QListWidget):
"""支持鼠标/触屏点击拖拽滑动的列表控件"""
def __init__(self, parent=None):
super().__init__(parent)
self.setVerticalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel)
self._press_pos = None
self._start_value = 0
self._dragging = False
def viewportEvent(self, event):
etype = event.type()
if etype == QEvent.Type.MouseButtonPress:
if event.button() == Qt.MouseButton.LeftButton:
self._press_pos = event.position().toPoint()
self._start_value = self.verticalScrollBar().value()
self._dragging = False
elif etype == QEvent.Type.MouseMove:
if self._press_pos and event.buttons() & Qt.MouseButton.LeftButton:
pos = event.position().toPoint()
delta = pos - self._press_pos
# 移动超过15px阈值才触发拖拽避免误触
if self._dragging or delta.manhattanLength() > 15:
self._dragging = True
new_val = self._start_value - int(delta.y())
# 限制在有效范围内
self.verticalScrollBar().setValue(
max(0, min(new_val, self.verticalScrollBar().maximum()))
)
return True # 拖拽中拦截事件,不传递给子控件
elif etype == QEvent.Type.MouseButtonRelease:
if event.button() == Qt.MouseButton.LeftButton and self._press_pos is not None:
was_dragging = self._dragging
self._press_pos = None
self._dragging = False
if was_dragging:
return True # 拖拽结束,拦截释放事件,避免触发子控件点击
return super().viewportEvent(event)
class WifiScanWorker(QObject):
"""在后台线程中执行WiFi扫描避免阻塞UI"""
scan_finished = pyqtSignal(list)
scan_error = pyqtSignal(str)
def __init__(self, wifi_manager):
super().__init__()
self.wifi_manager = wifi_manager
def run(self):
try:
networks = self.wifi_manager.scan_networks()
self.scan_finished.emit(networks)
except Exception as e:
self.scan_error.emit(str(e))
class SettingPage(QWidget):
def __init__(self, api_client, parent=None):
super().__init__(parent)
self.api_client = api_client
self.wifi_manager = WifiManager()
self._saved_networks_cache = [] # 缓存上次的已保存网络列表
# 让页面自身能接收焦点,用于点击扫描按钮后转移焦点防止跳动
self.setFocusPolicy(Qt.FocusPolicy.ClickFocus)
# 创建悬浮键盘
self._keyboard = FloatingKeyboard()
self._keyboard_attached = False
# 监听全局焦点变化,可靠处理键盘消失
self._focus_changed_connected = False
self.init_ui()
self._start_status_timer()
def _on_app_focus_changed(self, old_widget, new_widget):
"""全局焦点变化时检查是否需要关闭键盘"""
if not self._keyboard_attached:
return
# 如果新焦点在输入控件上,什么都不做
if isinstance(new_widget, (QLineEdit, QComboBox, QPushButton)):
return
# 如果新焦点是键盘自身的子控件,也不关闭
if new_widget and self._keyboard.isAncestorOf(new_widget):
return
# 如果新焦点是键盘本身,不关闭
if new_widget is self._keyboard:
return
# 如果是热点开关按钮的 ON/OFF 文本区域,不关闭
if isinstance(new_widget, QWidget) and new_widget.parent() is self.hotspot_toggle:
return
# 其他情况 → 关闭键盘
self._dismiss_keyboard()
def _connect_focus_signal(self):
"""连接全局焦点信号"""
if not self._focus_changed_connected:
QApplication.instance().focusChanged.connect(self._on_app_focus_changed)
self._focus_changed_connected = True
def _disconnect_focus_signal(self):
"""断开全局焦点信号"""
if self._focus_changed_connected:
QApplication.instance().focusChanged.disconnect(self._on_app_focus_changed)
self._focus_changed_connected = False
def _attach_keyboard(self, widget):
"""将悬浮键盘绑定到指定输入框并显示"""
self._keyboard.attach(widget)
self._keyboard.show_below(widget)
self._keyboard_attached = True
self._connect_focus_signal()
def _dismiss_keyboard(self):
"""关闭悬浮键盘"""
self._keyboard.hide()
self._keyboard.detach()
self._keyboard_attached = False
self._disconnect_focus_signal()
def _on_input_focus_in(self, widget):
"""输入框获得焦点时的处理"""
if self._keyboard_attached:
self._keyboard.attach(widget)
self._keyboard.show_below(widget)
else:
self._attach_keyboard(widget)
def eventFilter(self, obj, event):
if event.type() == QEvent.Type.FocusIn:
if obj in (self.ssid_input, self.identity_input, self.password_input,
self.hotspot_ssid, self.hotspot_password):
self._on_input_focus_in(obj)
# 如果不是输入框获得焦点且键盘正显示,则关闭键盘
elif self._keyboard_attached and not isinstance(obj, (QLineEdit, QComboBox, QPushButton)):
self._dismiss_keyboard()
elif event.type() == QEvent.Type.FocusOut:
# 延迟检查:如果焦点真的离开了所有输入框,就关闭键盘
if obj in (self.ssid_input, self.identity_input, self.password_input,
self.hotspot_ssid, self.hotspot_password):
QTimer.singleShot(100, self._check_dismiss_keyboard)
return super().eventFilter(obj, event)
def _check_dismiss_keyboard(self):
"""检查当前焦点是否还在任何输入控件上,不在则关闭键盘"""
w = self.focusWidget()
if w not in (self.ssid_input, self.identity_input, self.password_input,
self.auth_combo, self.hotspot_ssid, self.hotspot_password):
self._dismiss_keyboard()
@staticmethod
def _styled_message(icon, parent, title, text, buttons=QMessageBox.StandardButton.Ok):
"""显示与整体风格一致的暗色主题消息框"""
msg = QMessageBox(parent)
msg.setIcon(icon)
msg.setWindowTitle(title)
msg.setText(text)
msg.setStandardButtons(buttons)
msg.setStyleSheet("""
QMessageBox {
background-color: #3f3f3f;
color: #f2f2f2;
border: 2px solid #646464;
border-radius: 12px;
padding: 20px;
}
QMessageBox QLabel {
color: #f2f2f2;
font-size: 20px;
padding: 10px;
}
QMessageBox QPushButton {
min-width: 130px;
min-height: 50px;
font-size: 20px;
font-weight: 600;
color: #f8f8f8;
background-color: #555555;
border: 2px solid #888888;
border-radius: 10px;
padding: 8px 24px;
}
QMessageBox QPushButton:hover {
background-color: #636363;
border-color: #aaaaaa;
}
QMessageBox QPushButton:pressed {
background-color: #3d3d3d;
border-color: #5a9fcf;
}
""")
return msg.exec()
def init_ui(self):
layout = QHBoxLayout(self)
layout.setContentsMargins(14, 14, 14, 14)
layout.setSpacing(14)
left_panel = QFrame()
left_panel.setStyleSheet("background-color: #3f3f3f; border-radius: 10px;")
left_layout = QVBoxLayout(left_panel)
left_layout.setContentsMargins(10, 10, 10, 10)
title = QLabel("系统设置")
title.setAlignment(Qt.AlignmentFlag.AlignCenter)
title.setStyleSheet("color: #efefef; font-size: 24px; font-weight: 600;")
left_layout.addWidget(title)
# 左侧设置项列表(启用平滑/按像素滚动,增大触摸目标)
self.item_list = QListWidget()
self.item_list.setVerticalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel)
self.item_list.setSpacing(6)
self.item_list.setUniformItemSizes(False)
self.item_list.setStyleSheet(
"""
QListWidget {
background-color: #4a4a4a;
color: #f2f2f2;
border: 1px solid #646464;
border-radius: 8px;
font-size: 22px;
outline: none;
}
QListWidget::item {
height: 56px;
padding-left: 10px;
border-radius: 6px;
}
QListWidget::item:selected {
background-color: #2f6f91;
color: white;
}
"""
)
self.item_list.addItem("WiFi设置")
self.item_list.addItem("热点设置")
self.item_list.addItem("电源设置")
self.item_list.addItem("其它设置(待定)")
self.item_list.currentRowChanged.connect(self.display_setting)
left_layout.addWidget(self.item_list)
layout.addWidget(left_panel, 1)
# 右侧设置参数区域
self.settings_stack = QStackedWidget()
self.settings_stack.setStyleSheet("background-color: #444444; border-radius: 10px;")
# 每个 init 方法返回一个包裹了 DragScrollArea 的页面
self.init_wifi_settings()
self.init_hotspot_settings()
self.init_power_settings()
self.init_todo_settings()
layout.addWidget(self.settings_stack, 3)
self.item_list.setCurrentRow(0)
def init_wifi_settings(self):
wifi_widget = QWidget()
wifi_widget.setStyleSheet(
"""
/* ------ 通用标签 ------ */
QLabel {
color: #e8e8e8;
}
/* ------ 大按钮:清晰边框、足够高度 ------ */
QPushButton {
min-height: 50px;
font-size: 20px;
font-weight: 600;
color: #f8f8f8;
background-color: #555555;
border: 2px solid #888888;
border-radius: 10px;
padding: 10px 24px;
}
QPushButton:hover {
background-color: #636363;
border-color: #aaaaaa;
}
QPushButton:pressed {
background-color: #3d3d3d;
border-color: #5a9fcf;
}
QPushButton:disabled {
color: #808080;
background-color: #404040;
border-color: #555555;
}
/* ------ 输入框 ------ */
QLineEdit {
min-height: 44px;
font-size: 18px;
color: #f2f2f2;
background-color: #3a3a3a;
border: 2px solid #707070;
border-radius: 8px;
padding: 4px 12px;
}
QLineEdit:focus {
border-color: #5a9fcf;
}
/* ------ 下拉框 ------ */
QComboBox {
min-height: 44px;
font-size: 18px;
color: #f2f2f2;
background-color: #3a3a3a;
border: 2px solid #707070;
border-radius: 8px;
padding: 4px 12px;
}
QComboBox:hover {
border-color: #aaaaaa;
}
QComboBox QAbstractItemView {
background-color: #3a3a3a;
color: #f2f2f2;
selection-background-color: #2f6f91;
border: 1px solid #707070;
font-size: 18px;
}
/* ------ 列表控件 ------ */
QListWidget {
min-height: 300px;
max-width: 500%;
background-color: #3a3a3a;
color: #f2f2f2;
border: 2px solid #707070;
border-radius: 8px;
font-size: 18px;
outline: none;
}
QListWidget::item {
min-height: 40px;
padding: 4px 10px;
border-radius: 4px;
}
QListWidget::item:selected {
background-color: #2f6f91;
color: #ffffff;
}
QListWidget::item:selected QLabel {
color: #ffffff;
}
QListWidget::item:hover {
background-color: #505050;
}
"""
)
wifi_layout = QVBoxLayout(wifi_widget)
wifi_layout.setContentsMargins(16, 16, 16, 16)
wifi_layout.setSpacing(14)
wifi_title = QLabel("WiFi设置")
wifi_title.setStyleSheet("color: #f2f2f2; font-size: 26px; font-weight: 700;")
wifi_layout.addWidget(wifi_title)
self.current_status_label = QLabel("当前连接:未知")
self.current_status_label.setStyleSheet("color: #cccccc; font-size: 19px; font-weight: 500;")
wifi_layout.addWidget(self.current_status_label)
# 显示保存的WiFi列表
saved_title = QLabel("已保存网络")
saved_title.setStyleSheet("color: #dcdcdc; font-size: 22px; font-weight: 600;")
wifi_layout.addWidget(saved_title)
self.saved_wifi_list = DraggableListWidget()
wifi_layout.addWidget(self.saved_wifi_list)
saved_buttons_layout = QHBoxLayout()
saved_buttons_layout.setSpacing(12)
connect_saved_button = QPushButton("连接到此网络")
connect_saved_button.clicked.connect(self.connect_to_saved_wifi)
saved_buttons_layout.addWidget(connect_saved_button)
remove_saved_button = QPushButton("删除选中")
remove_saved_button.clicked.connect(self.remove_selected_saved_wifi)
saved_buttons_layout.addWidget(remove_saved_button)
wifi_layout.addLayout(saved_buttons_layout)
nearby_title = QLabel("附近网络")
nearby_title.setStyleSheet("color: #dcdcdc; font-size: 22px; font-weight: 600;")
wifi_layout.addWidget(nearby_title)
self.nearby_wifi_list = DraggableListWidget()
self.nearby_wifi_list.itemClicked.connect(self.fill_ssid_from_scan)
wifi_layout.addWidget(self.nearby_wifi_list)
self.scan_button = QPushButton("扫描网络")
self.scan_button.clicked.connect(self.scan_nearby_wifi)
wifi_layout.addWidget(self.scan_button)
# 连接新WiFi
connect_title = QLabel("连接新网络")
connect_title.setStyleSheet("color: #dcdcdc; font-size: 22px; font-weight: 600;")
wifi_layout.addWidget(connect_title)
form = QFormLayout()
form.setLabelAlignment(Qt.AlignmentFlag.AlignRight)
form.setFormAlignment(Qt.AlignmentFlag.AlignLeft)
form.setHorizontalSpacing(16)
form.setVerticalSpacing(12)
# 表单标签使用浅色
ssid_label = QLabel("SSID")
ssid_label.setStyleSheet("color: #d0d0d0; font-size: 19px; font-weight: 500;")
self.ssid_input = QLineEdit()
self.ssid_input.setStyleSheet("max-width: 400%;")
self.ssid_input.setPlaceholderText("输入WiFi名称")
self.ssid_input.installEventFilter(self)
self.ssid_input.textChanged.connect(self._on_ssid_text_changed)
form.addRow(ssid_label, self.ssid_input)
auth_label = QLabel("认证方式")
auth_label.setStyleSheet("color: #d0d0d0; font-size: 19px; font-weight: 500;")
self.auth_combo = QComboBox()
self.auth_combo.setStyleSheet(
"""
QComboBox {
max-width: 400%;
font-size: 22px;
min-height: 44px;
}
"""
)
# 直接设置下拉列表视图的样式,避免被父级样式覆盖
self.auth_combo.view().setStyleSheet(
"""
QListView {
font-size: 24px;
min-height: 50px;
padding: 6px 12px;
}
QListView::item {
min-height: 50px;
padding: 8px 12px;
}
"""
)
self.auth_combo.addItem("开放网络(无密码)", "open")
self.auth_combo.addItem("WPA/WPA2-PSK", "psk")
self.auth_combo.addItem("WPA-EAP (PEAP/MSCHAPv2)", "eap")
self.auth_combo.currentIndexChanged.connect(self.update_auth_fields)
form.addRow(auth_label, self.auth_combo)
identity_label = QLabel("身份")
identity_label.setStyleSheet("color: #d0d0d0; font-size: 19px; font-weight: 500;")
self.identity_label = identity_label
self.identity_input = QLineEdit()
self.identity_input.setStyleSheet("max-width: 400%;")
self.identity_input.setPlaceholderText("企业网络用户名/身份")
self.identity_input.installEventFilter(self)
form.addRow(identity_label, self.identity_input)
password_label = QLabel("密码")
password_label.setStyleSheet("color: #d0d0d0; font-size: 19px; font-weight: 500;")
self.password_label = password_label
self.password_input = QLineEdit()
self.password_input.setStyleSheet("max-width: 400%;")
self.password_input.setPlaceholderText("输入WiFi密码")
self.password_input.installEventFilter(self)
self.password_input.setEchoMode(QLineEdit.EchoMode.Password)
form.addRow(password_label, self.password_input)
wifi_layout.addLayout(form)
# 连接按钮使用醒目的强调色
connect_button = QPushButton("连接")
connect_button.setStyleSheet(
"""
QPushButton {
min-height: 52px;
font-size: 22px;
font-weight: 700;
color: #ffffff;
background-color: #2f6f91;
border: 2px solid #4a9fc8;
border-radius: 10px;
padding: 10px 24px;
}
QPushButton:hover {
background-color: #3a85b3;
border-color: #6fb8dd;
}
QPushButton:pressed {
background-color: #1e4d66;
border-color: #2f6f91;
}
"""
)
connect_button.clicked.connect(self.connect_to_wifi)
wifi_layout.addWidget(connect_button)
wifi_layout.addStretch()
self.settings_stack.addWidget(self._wrap_scroll(wifi_widget))
self.update_auth_fields()
self.refresh_saved_wifi()
self.refresh_current_status()
def init_hotspot_settings(self):
hotspot_widget = QWidget()
hotspot_layout = QVBoxLayout(hotspot_widget)
hotspot_layout.setContentsMargins(20, 20, 20, 20)
hotspot_layout.setSpacing(16)
title = QLabel("热点设置")
title.setStyleSheet("color: #f2f2f2; font-size: 26px; font-weight: 700;")
hotspot_layout.addWidget(title)
# ── 左右分栏 ──
split_row = QHBoxLayout()
split_row.setSpacing(20)
# ====== 左栏:信息设置 ======
left_col = QVBoxLayout()
left_col.setSpacing(12)
desc = QLabel("将设备设置为WiFi热点其他设备可扫描连接")
desc.setStyleSheet("color: #aaaaaa; font-size: 16px;")
desc.setWordWrap(True)
left_col.addWidget(desc)
# SSID 输入
ssid_label = QLabel("热点名称 (SSID)")
ssid_label.setStyleSheet("color: #d0d0d0; font-size: 18px; font-weight: 500;")
left_col.addWidget(ssid_label)
self.hotspot_ssid = QLineEdit("PrinterScreen-AP")
self.hotspot_ssid.setPlaceholderText("输入热点名称")
self.hotspot_ssid.setStyleSheet(
"min-height: 44px; font-size: 20px; color: #f2f2f2; "
"background-color: #3a3a3a; border: 2px solid #707070; "
"border-radius: 8px; padding: 4px 12px;"
)
self.hotspot_ssid.installEventFilter(self)
left_col.addWidget(self.hotspot_ssid)
# 密码输入
pw_label = QLabel("热点密码")
pw_label.setStyleSheet("color: #d0d0d0; font-size: 18px; font-weight: 500;")
left_col.addWidget(pw_label)
self.hotspot_password = QLineEdit("12345678")
self.hotspot_password.setPlaceholderText("至少8位")
self.hotspot_password.setStyleSheet(
"min-height: 44px; font-size: 20px; color: #f2f2f2; "
"background-color: #3a3a3a; border: 2px solid #707070; "
"border-radius: 8px; padding: 4px 12px;"
)
self.hotspot_password.installEventFilter(self)
left_col.addWidget(self.hotspot_password)
# 热点开关行
toggle_row = QHBoxLayout()
toggle_row.setSpacing(16)
toggle_label = QLabel("热点开关")
toggle_label.setStyleSheet("color: #d0d0d0; font-size: 20px; font-weight: 600;")
toggle_row.addWidget(toggle_label)
self.hotspot_toggle = QPushButton()
self.hotspot_toggle.setCheckable(True)
self.hotspot_toggle.setFixedSize(80, 40)
self.hotspot_toggle.setCursor(Qt.CursorShape.PointingHandCursor)
self._apply_toggle_style(False)
self.hotspot_toggle.toggled.connect(self._on_hotspot_toggled)
toggle_row.addWidget(self.hotspot_toggle)
toggle_row.addStretch()
left_col.addLayout(toggle_row)
# 热点状态标签
self.hotspot_status = QLabel("热点状态:关闭")
self.hotspot_status.setStyleSheet("color: #cccccc; font-size: 17px;")
left_col.addWidget(self.hotspot_status)
left_col.addStretch()
split_row.addLayout(left_col, 1)
# ====== 右栏:二维码 ======
right_col = QVBoxLayout()
right_col.setSpacing(8)
right_col.setAlignment(Qt.AlignmentFlag.AlignCenter)
qr_title = QLabel("扫码连接")
qr_title.setStyleSheet("color: #dcdcdc; font-size: 20px; font-weight: 600;")
qr_title.setAlignment(Qt.AlignmentFlag.AlignCenter)
right_col.addWidget(qr_title)
self.qr_label = QLabel()
self.qr_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.qr_label.setFixedSize(240, 240)
self.qr_label.setStyleSheet(
"background-color: #ffffff; border: 2px solid #646464; border-radius: 8px;"
)
right_col.addWidget(self.qr_label, alignment=Qt.AlignmentFlag.AlignCenter)
self.qr_hint = QLabel("开启热点后自动生成二维码")
self.qr_hint.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.qr_hint.setStyleSheet("color: #909090; font-size: 15px;")
right_col.addWidget(self.qr_hint)
right_col.addStretch()
split_row.addLayout(right_col, 1)
hotspot_layout.addLayout(split_row)
self.settings_stack.addWidget(self._wrap_scroll(hotspot_widget))
def _apply_toggle_style(self, checked):
if checked:
self.hotspot_toggle.setStyleSheet("""
QPushButton {
background-color: #4CAF50;
border: 2px solid #66BB6A;
border-radius: 20px;
font-size: 16px;
font-weight: bold;
color: white;
}
QPushButton:checked {
background-color: #4CAF50;
border: 2px solid #66BB6A;
}
""")
self.hotspot_toggle.setText("ON")
else:
self.hotspot_toggle.setStyleSheet("""
QPushButton {
background-color: #555555;
border: 2px solid #707070;
border-radius: 20px;
font-size: 16px;
font-weight: bold;
color: #aaaaaa;
}
QPushButton:checked {
background-color: #4CAF50;
border: 2px solid #66BB6A;
}
""")
self.hotspot_toggle.setText("OFF")
def _on_hotspot_toggled(self, checked):
if checked:
ssid = self.hotspot_ssid.text().strip()
password = self.hotspot_password.text().strip()
if not ssid:
self._styled_message(QMessageBox.Icon.Warning, self, "提示", "请输入热点名称")
self.hotspot_toggle.blockSignals(True)
self.hotspot_toggle.setChecked(False)
self.hotspot_toggle.blockSignals(False)
return
if len(password) < 8:
self._styled_message(QMessageBox.Icon.Warning, self, "提示", "密码至少需要8位")
self.hotspot_toggle.blockSignals(True)
self.hotspot_toggle.setChecked(False)
self.hotspot_toggle.blockSignals(False)
return
try:
ret = self.wifi_manager.open_hotspot(ssid, password)
if ret:
self._apply_toggle_style(True)
self.hotspot_status.setText(f"热点状态:已开启 ({ssid})")
self.hotspot_ssid.setEnabled(False)
self.hotspot_password.setEnabled(False)
self._generate_qr_code(ssid, password)
else:
raise RuntimeError("wpa_cli 返回失败")
except Exception as e:
self._styled_message(QMessageBox.Icon.Critical, self, "错误", f"开启热点失败: {str(e)}")
self.hotspot_toggle.blockSignals(True)
self.hotspot_toggle.setChecked(False)
self.hotspot_toggle.blockSignals(False)
else:
try:
self.wifi_manager.close_hotspot()
except Exception:
pass
self._apply_toggle_style(False)
self.hotspot_status.setText("热点状态:关闭")
self.hotspot_ssid.setEnabled(True)
self.hotspot_password.setEnabled(True)
self.qr_label.clear()
self.qr_hint.setText("开启热点后自动生成二维码")
def _generate_qr_code(self, ssid, password):
"""生成 WiFi 二维码并显示"""
wifi_str = f"WIFI:S:{ssid};T:WPA;P:{password};;"
try:
qr = qrcode.QRCode(box_size=6, border=2)
qr.add_data(wifi_str)
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
# 转换为 QPixmap
buffer = io.BytesIO()
img.save(buffer, format="PNG")
buffer.seek(0)
pixmap = QPixmap()
pixmap.loadFromData(buffer.getvalue())
scaled = pixmap.scaled(220, 220, Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation)
self.qr_label.setPixmap(scaled)
self.qr_hint.setText(f"扫码连接 WiFi: {ssid}")
except Exception as e:
self.qr_hint.setText(f"二维码生成失败: {str(e)}")
def init_power_settings(self):
power_widget = QWidget()
power_layout = QVBoxLayout(power_widget)
power_layout.setContentsMargins(20, 20, 20, 20)
power_layout.setSpacing(16)
title = QLabel("电源设置")
title.setStyleSheet("color: #f2f2f2; font-size: 26px; font-weight: 700;")
power_layout.addWidget(title)
desc = QLabel("重启或关闭设备")
desc.setStyleSheet("color: #aaaaaa; font-size: 16px;")
power_layout.addWidget(desc)
power_layout.addStretch()
# 重启按钮
reboot_btn = QPushButton("↻ 重启系统")
reboot_btn.setStyleSheet("""
QPushButton {
min-height: 70px;
font-size: 26px;
font-weight: 700;
color: #ffffff;
background-color: #2f6f91;
border: 2px solid #4a9fc8;
border-radius: 14px;
padding: 10px 24px;
}
QPushButton:hover {
background-color: #3a85b3;
border-color: #6fb8dd;
}
QPushButton:pressed {
background-color: #1e4d66;
border-color: #2f6f91;
}
""")
reboot_btn.clicked.connect(self._confirm_reboot)
power_layout.addWidget(reboot_btn)
power_layout.addSpacing(20)
# 关机按钮
shutdown_btn = QPushButton("⏻ 关机")
shutdown_btn.setStyleSheet("""
QPushButton {
min-height: 70px;
font-size: 26px;
font-weight: 700;
color: #ffffff;
background-color: #c0392b;
border: 2px solid #e74c3c;
border-radius: 14px;
padding: 10px 24px;
}
QPushButton:hover {
background-color: #e74c3c;
border-color: #ff6b6b;
}
QPushButton:pressed {
background-color: #922b21;
border-color: #c0392b;
}
""")
shutdown_btn.clicked.connect(self._confirm_shutdown)
power_layout.addWidget(shutdown_btn)
power_layout.addStretch()
self.settings_stack.addWidget(self._wrap_scroll(power_widget))
def _confirm_reboot(self):
reply = QMessageBox.question(
self, "确认重启",
"确定要重启系统吗?\n所有未保存的数据将丢失。",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.Yes:
self._styled_message(
QMessageBox.Icon.Information, self, "重启",
"系统正在重启..."
)
QTimer.singleShot(500, lambda: os.system("sudo reboot"))
def _confirm_shutdown(self):
reply = QMessageBox.question(
self, "确认关机",
"确定要关闭系统吗?\n关闭后需要手动重新开机。",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.Yes:
self._styled_message(
QMessageBox.Icon.Information, self, "关机",
"系统正在关机..."
)
QTimer.singleShot(500, lambda: os.system("sudo poweroff"))
def init_todo_settings(self):
todo_widget = QWidget()
todo_widget.setStyleSheet(
"""
QLabel {
color: #e4e4e4;
}
QPushButton {
min-height: 50px;
font-size: 20px;
font-weight: 600;
color: #f8f8f8;
background-color: #555555;
border: 2px solid #888888;
border-radius: 10px;
padding: 10px 24px;
}
QPushButton:hover {
background-color: #636363;
border-color: #aaaaaa;
}
"""
)
todo_layout = QVBoxLayout(todo_widget)
todo_layout.setContentsMargins(20, 20, 20, 20)
label = QLabel("其它配置项待定")
label.setAlignment(Qt.AlignmentFlag.AlignCenter)
label.setStyleSheet("color: #cecece; font-size: 26px; font-weight: 600;")
todo_layout.addWidget(label)
hint = QLabel("更多设置项将在后续版本中添加")
hint.setAlignment(Qt.AlignmentFlag.AlignCenter)
hint.setStyleSheet("color: #909090; font-size: 18px; margin-top: 12px;")
todo_layout.addWidget(hint)
todo_layout.addStretch()
self.settings_stack.addWidget(self._wrap_scroll(todo_widget))
def refresh_saved_wifi(self):
try:
saved_networks = self.wifi_manager.list_saved_networks()
except Exception as e:
self._styled_message(QMessageBox.Icon.Critical, self, "错误", f"无法加载保存的WiFi: {str(e)}")
return
# 比较与上次缓存的网络列表是否有变化
cache_key = [(n.get("network_id"), n.get("ssid"), n.get("flags")) for n in saved_networks]
if cache_key == self._saved_networks_cache:
return # 无变化,不清空列表
self._saved_networks_cache = cache_key
# 列表有变化时才真正刷新
current_item = self.saved_wifi_list.currentItem()
current_net_id = current_item.data(Qt.ItemDataRole.UserRole).get("network_id") if current_item else None
self.saved_wifi_list.clear()
for network in saved_networks:
item_text = f"[{network.get('network_id', '-')}] {network.get('ssid', '<hidden>')} {network.get('flags', '')}"
item = QListWidgetItem(item_text)
item.setData(Qt.ItemDataRole.UserRole, network)
self.saved_wifi_list.addItem(item)
# 恢复选中
if current_net_id is not None and network.get("network_id") == current_net_id:
self.saved_wifi_list.setCurrentItem(item)
def scan_nearby_wifi(self):
"""在后台线程中扫描WiFi防止界面卡死"""
self.nearby_wifi_list.clear()
# 把焦点交给页面自身,防止跳到输入框导致画面跳动
self.setFocus()
self.scan_button.setEnabled(False)
self.scan_button.setText("扫描中……")
self._scan_thread = QThread()
self._scan_worker = WifiScanWorker(self.wifi_manager)
self._scan_worker.moveToThread(self._scan_thread)
self._scan_thread.started.connect(self._scan_worker.run)
self._scan_worker.scan_finished.connect(self._on_scan_finished)
self._scan_worker.scan_error.connect(self._on_scan_error)
# 清理线程资源
self._scan_worker.scan_finished.connect(self._scan_thread.quit)
self._scan_worker.scan_error.connect(self._scan_thread.quit)
self._scan_worker.scan_finished.connect(self._scan_worker.deleteLater)
self._scan_worker.scan_error.connect(self._scan_worker.deleteLater)
self._scan_thread.finished.connect(self._scan_thread.deleteLater)
self._scan_thread.start()
def _on_scan_finished(self, networks):
"""扫描完成后的UI更新主线程中执行"""
self.scan_button.setEnabled(True)
self.scan_button.setText("扫描网络")
processed = self._deduplicate_networks(networks)
if not processed:
self._styled_message(QMessageBox.Icon.Information, self, "提示", "未扫描到可用网络")
return
for network in processed:
ssid = network.get("ssid", "")
if not ssid:
continue
decoded_ssid = self._decode_ssid(ssid)
signal = network.get("signal_level", "")
# 自定义列表项SSID靠左信号强度靠右
item_widget = QWidget()
item_widget.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
# 为了比较好看的列表项目,下面的不要删
item_widget.setStyleSheet("""
QWidget {
min-height: 40px;
padding: 4px 10px;
border-radius: 4px;
}
QWidget:selected {
background-color: #2f6f91;
color: #ffffff;
}
QWidget:selected QLabel {
color: #ffffff;
}
QWidget:hover {
background-color: #505050;
}
""")
item_layout = QHBoxLayout(item_widget)
item_layout.setContentsMargins(8, 2, 12, 2)
ssid_label = QLabel(decoded_ssid)
ssid_label.setStyleSheet("background: transparent; color: #f2f2f2; font-size: 18px;")
signal_label = QLabel(f"{signal} dBm" if signal else "")
signal_label.setStyleSheet("background: transparent; color: #aaaaaa; font-size: 16px;")
item_layout.addWidget(ssid_label)
item_layout.addStretch()
item_layout.addWidget(signal_label)
item = QListWidgetItem()
item.setData(Qt.ItemDataRole.UserRole, network)
item.setSizeHint(item_widget.sizeHint())
self.nearby_wifi_list.addItem(item)
self.nearby_wifi_list.setItemWidget(item, item_widget)
def _on_scan_error(self, error_msg):
"""扫描出错后的UI恢复主线程中执行"""
self.scan_button.setEnabled(True)
self.scan_button.setText("扫描网络")
self._styled_message(QMessageBox.Icon.Critical, self, "错误", f"扫描网络失败: {error_msg}")
def _on_ssid_text_changed(self, text):
"""SSID输入框文本变化时如果清空则重置为开放网络"""
if not text.strip():
self.auth_combo.blockSignals(True)
self.auth_combo.setCurrentIndex(0)
self.auth_combo.blockSignals(False)
self.update_auth_fields()
@staticmethod
def _decode_ssid(ssid):
"""尝试解码非英文WiFi名称处理 \\xe9\\x83\\xbd 等转义序列)"""
if not ssid:
return ssid
# 尝试 unicode_escape 解码(处理 \x 转义序列,返回 bytes
try:
decoded = ssid.encode('latin-1').decode('unicode-escape').encode('latin-1').decode()
if decoded != ssid:
return decoded
except Exception:
pass
# 尝试 latin-1 → UTF-8 双重转换
try:
decoded = bytes(ssid, encoding='utf-8').decode('utf-8')
if decoded != ssid:
return decoded
except Exception:
pass
return ssid
@staticmethod
def _deduplicate_networks(networks):
"""去重同名网络每个SSID只保留信号最强的一个"""
best = {}
for net in networks:
ssid = net.get("ssid", "")
if not ssid:
continue
raw = net.get("signal_level", -100)
try:
signal = int(raw)
except (ValueError, TypeError):
signal = -100
if ssid not in best or signal > best[ssid].get("_signal_int", -100):
net["_signal_int"] = signal
best[ssid] = net
return list(best.values())
@staticmethod
def _detect_auth_mode(network):
"""根据 wpa_supplicant 返回的标准 flags 判断认证方式"""
flags = network.get("flags", "").strip()
# 无加密标记 → 开放网络
if not flags or flags in ("", "[ESS]", "[NONE]", "NONE"):
return "open"
# 检查是否含企业级认证标记
eap_keywords = (
"WPA2-EAP", "WPA-EAP", "WPA3-EAP",
"EAP", "SUITE-B",
"802.1X", "IEEE8021X", "ENTERPRISE",
"FT/EAP",
)
if any(kw in flags.upper() for kw in eap_keywords):
return "eap"
# 检查是否含 PSK 类标记(个人级加密)
psk_keywords = (
"PSK", "SAE", "WPA2", "WPA3", "WPA",
"CCMP", "TKIP",
)
if any(kw in flags.upper() for kw in psk_keywords):
return "psk"
# 兜底:有标记但无法识别,默认 psk
return "psk"
def fill_ssid_from_scan(self, item):
network = item.data(Qt.ItemDataRole.UserRole) or {}
ssid = network.get("ssid", "")
if ssid:
decoded = self._decode_ssid(ssid)
self.ssid_input.setText(decoded)
# 根据网络标志自动选择认证方式
auth_mode = self._detect_auth_mode(network)
index = self.auth_combo.findData(auth_mode)
if index >= 0:
self.auth_combo.blockSignals(True)
self.auth_combo.setCurrentIndex(index)
self.auth_combo.blockSignals(False)
self.update_auth_fields()
def update_auth_fields(self):
auth_mode = self.auth_combo.currentData()
if auth_mode == "open":
self.identity_label.setVisible(False)
self.identity_input.setVisible(False)
self.password_label.setVisible(False)
self.password_input.setVisible(False)
elif auth_mode == "psk":
self.identity_label.setVisible(False)
self.identity_input.setVisible(False)
self.password_label.setVisible(True)
self.password_input.setVisible(True)
else:
self.identity_label.setVisible(True)
self.identity_input.setVisible(True)
self.password_label.setVisible(True)
self.password_input.setVisible(True)
def refresh_current_status(self):
try:
status = self.wifi_manager.get_current_status()
ssid = status.get("ssid", "未连接")
ip_addr = status.get("ip_address", "-")
state = status.get("wpa_state", "UNKNOWN")
self.current_status_label.setText(f"当前连接:{ssid} | IP: {ip_addr} | 状态: {state}")
except Exception:
self.current_status_label.setText("当前连接:未知")
def _start_status_timer(self):
"""启动定时器,每秒刷新已保存网络列表和当前连接状态"""
self._status_timer = QTimer(self)
self._status_timer.timeout.connect(self._on_status_timer_tick)
self._status_timer.start(1000)
def _on_status_timer_tick(self):
self.refresh_saved_wifi()
self.refresh_current_status()
def connect_to_saved_wifi(self):
"""连接已保存列表中选中的网络"""
item = self.saved_wifi_list.currentItem()
if item is None:
self._styled_message(QMessageBox.Icon.Warning, self, "提示", "请先选择一个已保存网络")
return
network = item.data(Qt.ItemDataRole.UserRole) or {}
network_id = network.get("network_id")
ssid = network.get("ssid", "")
if network_id is None:
self._styled_message(QMessageBox.Icon.Warning, self, "提示", "选中网络无效")
return
try:
ok = self.wifi_manager.connect_network_id(network_id)
if not ok:
self._styled_message(QMessageBox.Icon.Critical, self, "错误", "连接请求下发失败")
return
self._styled_message(QMessageBox.Icon.Information, self, "成功", f"已发起连接: {ssid}")
except Exception as e:
self._styled_message(QMessageBox.Icon.Critical, self, "错误", f"连接失败: {str(e)}")
def remove_selected_saved_wifi(self):
item = self.saved_wifi_list.currentItem()
if item is None:
self._styled_message(QMessageBox.Icon.Warning, self, "提示", "请先选择一个已保存网络")
return
network = item.data(Qt.ItemDataRole.UserRole) or {}
network_id = network.get("network_id")
ssid = network.get("ssid", "")
if network_id is None:
self._styled_message(QMessageBox.Icon.Warning, self, "提示", "选中网络无效,无法删除")
return
try:
self.wifi_manager.remove_network(network_id)
self._styled_message(QMessageBox.Icon.Information, self, "成功", f"已删除网络: {ssid}")
self.refresh_saved_wifi()
self.refresh_current_status()
except Exception as e:
self._styled_message(QMessageBox.Icon.Critical, self, "错误", f"删除失败: {str(e)}")
def connect_to_wifi(self):
ssid = self.ssid_input.text().strip()
password = self.password_input.text()
identity = self.identity_input.text().strip()
auth_mode = self.auth_combo.currentData()
if not ssid:
self._styled_message(QMessageBox.Icon.Warning, self, "警告", "WiFi名称不能为空")
return
if auth_mode == "psk" and not password:
self._styled_message(QMessageBox.Icon.Warning, self, "警告", "WPA/WPA2 认证需要密码")
return
if auth_mode == "eap" and (not identity or not password):
self._styled_message(QMessageBox.Icon.Warning, self, "警告", "WPA-EAP 认证需要身份和密码")
return
try:
if auth_mode == "open":
ok = self.wifi_manager.connect_wifi(ssid, None)
elif auth_mode == "psk":
ok = self.wifi_manager.connect_wifi(ssid, password)
else:
ok = self.wifi_manager.connect_eap(ssid, identity, password)
if not ok:
self._styled_message(QMessageBox.Icon.Critical, self, "错误", "连接请求下发失败,请检查系统日志")
return
self._styled_message(QMessageBox.Icon.Information, self, "成功", f"已发起连接: {ssid}")
self.refresh_saved_wifi()
self.refresh_current_status()
except Exception as e:
self._styled_message(QMessageBox.Icon.Critical, self, "错误", f"连接WiFi失败: {str(e)}")
def display_setting(self, index):
if index < 0:
return
self.settings_stack.setCurrentIndex(index)
@staticmethod
def _wrap_scroll(widget):
"""将页面 widget 放入独立的 DragScrollArea 中"""
scroll = DragScrollArea()
scroll.setWidgetResizable(True)
scroll.setFrameShape(QFrame.Shape.NoFrame)
scroll.setWidget(widget)
scroll.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
return scroll