优化wifi设置页面,添加悬浮键盘

This commit is contained in:
2026-05-10 01:39:16 +08:00
parent 65c342219d
commit 649677f564
5 changed files with 1380 additions and 18 deletions

423
utils/floating_keyboard.py Normal file
View File

@@ -0,0 +1,423 @@
"""
悬浮虚拟键盘
- 美式 QWERTY 键盘排列(字母 + 数字 + 常用符号)
- Shift / Caps Lock / 退格 / 空格 / 回车 / 关闭
- 可拖拽悬浮,自动跟随焦点输入框
- 暗色主题,大按钮适合触屏
"""
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
QApplication, QLineEdit, QTextEdit,
)
from PyQt6.QtCore import Qt, QEvent, QPoint, pyqtSignal
from PyQt6.QtGui import QMouseEvent, QTextCursor
# ─── 键盘布局定义 ──────────────────────────────────────────────
KEY_ROWS = {
"normal": [
["q", "w", "e", "r", "t", "y", "u", "i", "o", "p"],
["a", "s", "d", "f", "g", "h", "j", "k", "l"],
["z", "x", "c", "v", "b", "n", "m"],
],
"shift": [
["Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P"],
["A", "S", "D", "F", "G", "H", "J", "K", "L"],
["Z", "X", "C", "V", "B", "N", "M"],
],
}
NUMBER_ROW = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"]
# 符号键:按 Shift 或符号切换时与数字行互换
SYMBOL_ROW = ["!", "@", "#", "$", "%", "^", "&", "*", "(", ")"]
# 右侧额外符号(放在字母区行末或单独行)
EXTRA_SYMBOLS = [
["-", "=", "[", "]", "\\", ";", "'", ",", ".", "/"],
["_", "+", "{", "}", "|", ":", "\"", "<", ">", "?"],
]
KEY_STYLE = """
QPushButton {
min-width: 52px;
min-height: 52px;
font-size: 22px;
font-weight: 600;
color: #f2f2f2;
background-color: #4a4a4a;
border: 2px solid #646464;
border-radius: 8px;
padding: 4px 4px;
}
QPushButton:hover {
background-color: #5a5a5a;
border-color: #888888;
}
QPushButton:pressed {
background-color: #2f6f91;
border-color: #5a9fcf;
}
"""
CTRL_KEY_STYLE = """
QPushButton {
min-width: 72px;
min-height: 52px;
font-size: 18px;
font-weight: 600;
color: #f2f2f2;
background-color: #555555;
border: 2px solid #707070;
border-radius: 8px;
padding: 4px 8px;
}
QPushButton:hover {
background-color: #666666;
border-color: #909090;
}
QPushButton:pressed {
background-color: #2f6f91;
border-color: #5a9fcf;
}
"""
SPACE_STYLE = """
QPushButton {
min-width: 280px;
min-height: 52px;
font-size: 18px;
color: transparent;
background-color: #4a4a4a;
border: 2px solid #646464;
border-radius: 8px;
}
QPushButton:hover {
background-color: #5a5a5a;
border-color: #888888;
}
QPushButton:pressed {
background-color: #2f6f91;
border-color: #5a9fcf;
}
"""
ACTIVE_CTRL_STYLE = """
QPushButton {
min-width: 72px;
min-height: 52px;
font-size: 18px;
font-weight: 600;
color: #ffffff;
background-color: #2f6f91;
border: 2px solid #5a9fcf;
border-radius: 8px;
padding: 4px 8px;
}
QPushButton:hover {
background-color: #3a85b3;
border-color: #6fb8dd;
}
"""
class FloatingKeyboard(QWidget):
"""可悬浮拖拽的虚拟键盘,自动跟随焦点输入框"""
key_pressed = pyqtSignal(str)
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowFlags(
Qt.WindowType.FramelessWindowHint
| Qt.WindowType.WindowStaysOnTopHint
| Qt.WindowType.Tool
)
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
self.setStyleSheet("background: transparent;")
self._drag_pos = None
self._shift_on = False
self._caps_on = False
self._symbol_mode = False
self._target_widget = None
self.init_ui()
# ── UI 构建 ──────────────────────────────────────────────
def init_ui(self):
outer = QWidget()
outer.setStyleSheet(
"background-color: #333333; border: 2px solid #555555; border-radius: 12px;"
)
layout = QVBoxLayout(outer)
layout.setContentsMargins(8, 8, 8, 8)
layout.setSpacing(4)
# ── 顶部拖拽条 ──
title_bar = QWidget()
title_bar.setFixedHeight(28)
title_bar.setStyleSheet("background: transparent;")
title_bar.mousePressEvent = self._title_mouse_press
title_bar.mouseMoveEvent = self._title_mouse_move
title_layout = QHBoxLayout(title_bar)
title_layout.setContentsMargins(8, 0, 8, 0)
drag_label = QPushButton("≡ 键盘")
drag_label.setStyleSheet(
"""
QPushButton {
background: transparent;
color: #aaaaaa;
font-size: 16px;
border: none;
text-align: left;
}
"""
)
title_layout.addWidget(drag_label)
title_layout.addStretch()
close_btn = QPushButton("")
close_btn.setFixedSize(28, 28)
close_btn.setStyleSheet(
"""
QPushButton {
background: transparent;
color: #aaaaaa;
font-size: 18px;
border: none;
border-radius: 14px;
}
QPushButton:hover {
background-color: #555555;
color: #ffffff;
}
"""
)
close_btn.clicked.connect(self.hide)
title_layout.addWidget(close_btn)
layout.addWidget(title_bar)
# ── 数字/符号行 ──
self.num_layout = QHBoxLayout()
self.num_layout.setSpacing(4)
self.num_buttons = []
for ch in NUMBER_ROW:
btn = self._make_key(ch)
self.num_buttons.append(btn)
self.num_layout.addWidget(btn)
layout.addLayout(self.num_layout)
# ── 字母行 ──
self._letter_buttons = [] # flat list for shift toggle
for row_keys in KEY_ROWS["normal"]:
row_layout = QHBoxLayout()
row_layout.setSpacing(4)
row_layout.addStretch()
for ch in row_keys:
btn = self._make_key(ch)
self._letter_buttons.append(btn)
row_layout.addWidget(btn)
row_layout.addStretch()
layout.addLayout(row_layout)
# ── 额外符号行(- = [ ] 等) ──
self.extra_layout = QHBoxLayout()
self.extra_layout.setSpacing(4)
self.extra_layout.addStretch()
self.extra_buttons = []
for ch in EXTRA_SYMBOLS[0]:
btn = self._make_key(ch)
self.extra_buttons.append(btn)
self.extra_layout.addWidget(btn)
self.extra_layout.addStretch()
layout.addLayout(self.extra_layout)
# ── 功能键行 ──
ctrl_layout = QHBoxLayout()
ctrl_layout.setSpacing(4)
self.shift_btn = QPushButton("")
self.shift_btn.setStyleSheet(CTRL_KEY_STYLE)
self.shift_btn.clicked.connect(self._toggle_shift)
ctrl_layout.addWidget(self.shift_btn)
self.caps_btn = QPushButton("A/a")
self.caps_btn.setStyleSheet(CTRL_KEY_STYLE)
self.caps_btn.clicked.connect(self._toggle_caps)
ctrl_layout.addWidget(self.caps_btn)
self.sym_btn = QPushButton("?123")
self.sym_btn.setStyleSheet(CTRL_KEY_STYLE)
self.sym_btn.clicked.connect(self._toggle_symbol)
ctrl_layout.addWidget(self.sym_btn)
backspace_btn = QPushButton("")
backspace_btn.setStyleSheet(CTRL_KEY_STYLE)
backspace_btn.clicked.connect(lambda: self._send_key("\b"))
ctrl_layout.addWidget(backspace_btn)
ctrl_layout.addStretch()
enter_btn = QPushButton("↵ 回车")
enter_btn.setStyleSheet(CTRL_KEY_STYLE)
enter_btn.clicked.connect(lambda: self._send_key("\n"))
ctrl_layout.addWidget(enter_btn)
layout.addLayout(ctrl_layout)
# ── 空格行 ──
space_layout = QHBoxLayout()
space_layout.setSpacing(4)
space_layout.addStretch()
self.space_btn = QPushButton(" ") # full-width space as placeholder
self.space_btn.setStyleSheet(SPACE_STYLE)
self.space_btn.clicked.connect(lambda: self._send_key(" "))
space_layout.addWidget(self.space_btn)
space_layout.addStretch()
layout.addLayout(space_layout)
outer_layout = QVBoxLayout(self)
outer_layout.setContentsMargins(0, 0, 0, 0)
outer_layout.addWidget(outer)
self.setFixedWidth(680)
# ── 按键工厂 ──
def _make_key(self, text):
btn = QPushButton(text)
btn.setStyleSheet(KEY_STYLE)
btn.clicked.connect(lambda checked, t=text: self._send_key(t))
return btn
# ── 按键发送 ──
def _send_key(self, text):
"""发送按键到目标控件"""
if text == "\b":
self._backspace()
return
if self._symbol_mode:
self._toggle_symbol()
widget = self._get_target()
if widget is None:
return
if isinstance(widget, QLineEdit):
cursor = widget.cursorPosition()
current = widget.text()
new_text = current[:cursor] + text + current[cursor:]
widget.setText(new_text)
widget.setCursorPosition(cursor + len(text))
elif isinstance(widget, QTextEdit):
tc = widget.textCursor()
tc.insertText(text)
widget.setTextCursor(tc)
def _backspace(self):
widget = self._get_target()
if widget is None:
return
if isinstance(widget, QLineEdit):
cursor = widget.cursorPosition()
current = widget.text()
if cursor > 0:
new_text = current[:cursor - 1] + current[cursor:]
widget.setText(new_text)
widget.setCursorPosition(cursor - 1)
elif isinstance(widget, QTextEdit):
tc = widget.textCursor()
if not tc.hasSelection():
pos = tc.position()
if pos > 0:
tc.setPosition(pos - 1)
tc.movePosition(QTextCursor.MoveOperation.Right, QTextCursor.MoveMode.KeepAnchor)
tc.removeSelectedText()
def _get_target(self):
"""获取当前有效的目标输入控件"""
if self._target_widget and self._target_widget.hasFocus():
return self._target_widget
w = QApplication.focusWidget()
if isinstance(w, (QLineEdit, QTextEdit)):
return w
return None
# ── Shift / Caps / Symbol 切换 ──
def _toggle_shift(self):
self._shift_on = not self._shift_on
self._apply_shift_caps()
self.shift_btn.setStyleSheet(ACTIVE_CTRL_STYLE if self._shift_on else CTRL_KEY_STYLE)
def _toggle_caps(self):
self._caps_on = not self._caps_on
self._apply_shift_caps()
self.caps_btn.setStyleSheet(ACTIVE_CTRL_STYLE if self._caps_on else CTRL_KEY_STYLE)
def _toggle_symbol(self):
self._symbol_mode = not self._symbol_mode
if self._symbol_mode:
for btn, ch in zip(self.num_buttons, SYMBOL_ROW):
btn.setText(ch)
for btn, ch in zip(self.extra_buttons, EXTRA_SYMBOLS[1]):
btn.setText(ch)
self.sym_btn.setStyleSheet(ACTIVE_CTRL_STYLE)
self.sym_btn.setText("ABC")
else:
for btn, ch in zip(self.num_buttons, NUMBER_ROW):
btn.setText(ch)
for btn, ch in zip(self.extra_buttons, EXTRA_SYMBOLS[0]):
btn.setText(ch)
self.sym_btn.setStyleSheet(CTRL_KEY_STYLE)
self.sym_btn.setText("?123")
def _apply_shift_caps(self):
use_shift = self._shift_on != self._caps_on # XOR
rows = KEY_ROWS["shift"] if use_shift else KEY_ROWS["normal"]
flat = [ch for row in rows for ch in row]
for btn, ch in zip(self._letter_buttons, flat):
btn.setText(ch)
# ── 窗口拖拽 ──
def _title_mouse_press(self, event: QMouseEvent):
if event.button() == Qt.MouseButton.LeftButton:
self._drag_pos = event.globalPosition().toPoint() - self.frameGeometry().topLeft()
def _title_mouse_move(self, event: QMouseEvent):
if event.buttons() & Qt.MouseButton.LeftButton and self._drag_pos is not None:
self.move(event.globalPosition().toPoint() - self._drag_pos)
# ── 目标绑定 ──
def attach(self, widget):
"""绑定到一个输入控件,键盘输入将发送到此控件"""
self._target_widget = widget
def detach(self):
self._target_widget = None
def show_at(self, x, y):
"""在屏幕坐标 (x, y) 处显示"""
self.move(x, y)
self.show()
self.raise_()
def show_below(self, widget):
"""显示在指定控件下方"""
pos = widget.mapToGlobal(QPoint(0, widget.height() + 4))
screen = QApplication.primaryScreen().availableGeometry()
if pos.x() + self.width() > screen.width():
pos.setX(screen.width() - self.width() - 8)
self.show_at(pos.x(), pos.y())