告别系统限制:在 Qt 5.1 中手把手打造自定义虚拟键盘(工程级详解)

目录

 

​编辑

引言

一、整体设计与职责划分(高层思路)

二、源码展示

三、逐文件重点解析与实现细节

四、集成示例(最小可运行示例)

五、常见扩展与优化(工程实用点)

六、测试要点与边界情况

七、可复用改进建议(工程级)

八、结语(工程实战建议)


 

class 卑微码农:
    def __init__(self):
        self.技能 = ['能读懂十年前祖传代码', '擅长用Ctrl+C/V搭建世界', '信奉"能跑就别动"的玄学']
        self.发量 = 100  # 初始发量
        self.咖啡因耐受度 = '极限'
        
    def 修Bug(self, bug):
        try:
            # 试图用玄学解决问题
            if bug.严重程度 == '离谱':
                print("这一定是环境问题!")
            else:
                print("让我看看是谁又没写注释...哦,是我自己。")
        except Exception as e:
            # 如果try块都救不了,那就...
            print("重启一下试试?")
            self.发量 -= 1  # 每解决一个bug,头发-1
 
 
# 实例化一个我
我 = 卑微码农()

引言


本文基于工程中三个 QML 组件(CxInput.qml、CxKeypad.qml、CxLoaderKey.qml)逐文件解析实现思路、关键实现细节与 Qt 5.1 下的注意点,并给出完整的可复制示例与集成说明。面向工程师:你会学到组件职责划分、焦点与事件处理、键盘加载/卸载策略、特殊键处理、以及若干可扩展点(长按重复、候选词、与 native IME 集成)。

一、整体设计与职责划分(高层思路)

  • CxInput.qml:输入框组件,封装 TextInput、占位符、清除按钮、焦点变化时调用外部键盘加载器(keyboardLoader)。负责与键盘的绑定与显示/隐藏交互。
  • CxKeypad.qml:键盘视图(逻辑+视图),定义按键布局、按键事件(keyPressed / specialKeyPressed)、Shift/Caps 行为及空间计算。
  • CxLoaderKey.qml:键盘加载器(Loader 封装),通过 show(target) / hide() 管理键盘的加载、并把按键事件注入目标 TextInput(通过 cursorPosition 插入/删除)。职责是 lifecycle 管理与连接输入目标。

三者组合带来的优点:职责单一、键盘可复用(Loader 延迟实例化)、输入框逻辑与键盘视图解耦、可在不同界面共享同一键盘 loader。

二、源码展示

CxInput.qml:

import QtQuick 2.0
import QtQuick.Controls 1.1

Item {
    id: root
    width: 200
    height: 50

    // 信号
    signal inputTextChanged(string text)
    
    // 自定义属性
    property alias text: textInput.text    
    property string placeholder: "Please enter a text"
    property bool passwordMode: false
    property int radius: 0
    property int txtLeftMargin: 10
    property int fontSize: 24
    property int clearW: 48
    property int clearH: 50
    property color borderColor: "#FFFFFF"
    property color focusBorderColor: "#FFFFFF"
    property color bgColorNrm: "transparent"
    property color bgColorDis: "transparent"
    property color phColor: "#272626"
    property color txtColor: "#FFFFFF"
    property Item keyboardLoader: null  // 声明可绑定的键盘加载器
    
    // 背景矩形
    CxRect {
        id: background
        anchors.fill: parent
        radius: root.radius
        border.color: textInput.activeFocus ? root.focusBorderColor : root.borderColor
        border.width: textInput.activeFocus ? 2 : 1
        color: textInput.enabled ? root.bgColorNrm : root.bgColorDis
        
        // 占位符文本
        StyleText {
            id: placeholderText
            anchors.fill: parent
            anchors.leftMargin: root.txtLeftMargin
            verticalAlignment: Text.AlignVCenter
            text: root.placeholder
            color: root.phColor
            fontSize: root.fontSize
            visible: !textInput.text && !textInput.activeFocus
        }
    }

    // 清除按钮
    CxImage {
        id: clearIcon
        key: "CLOSE_ICON"
        anchors {
            left: parent.left
            leftMargin: root.txtLeftMargin
            verticalCenter: parent.verticalCenter
        }
        width: root.clearW
        height: root.clearH
        visible: textInput.text && !passwordMode
        
        MouseArea {
            anchors.fill: parent
            onClicked: textInput.text = ""
        }
    }
    
    // 文本输入框
    TextInput {
        id: textInput
        anchors.fill: parent
        anchors.margins: root.txtLeftMargin
        anchors.leftMargin: clearIcon.visible ? (root.txtLeftMargin*2 + clearIcon.width) : root.txtLeftMargin
        verticalAlignment: Text.AlignVCenter
        font.pixelSize: fontSize
        color: root.txtColor
        clip: true
        echoMode: passwordMode ? TextInput.Password : TextInput.Normal
        selectByMouse: true

        // 焦点控制键盘
        onActiveFocusChanged: {
            if (activeFocus && keyboardLoader) {
                keyboardLoader.show(this)  // 绑定当前输入框并显示键盘
            } else if (keyboardLoader) {
                keyboardLoader.hide()      // 隐藏键盘
            }
        }

        onTextChanged: {
            root.inputTextChanged(textInput.text)
        }
        
        // 密码可见性切换
        MouseArea {
            anchors.right: parent.right
            anchors.verticalCenter: parent.verticalCenter
            width: 20
            height: 20
            visible: passwordMode
            onClicked: {
                textInput.echoMode = (textInput.echoMode === TextInput.Password) 
                    ? TextInput.Normal : TextInput.Password
            }
            
            // Image {
            //     source: textInput.echoMode === TextInput.Password 
            //         ? "qrc:/icons/eye_open.png" : "qrc:/icons/eye_closed.png"
            //     anchors.centerIn: parent
            // }
        }
    }
    
    // 状态管理
    states: [
        State {
            name: "ERROR"
            when: !textInput.acceptableInput && textInput.activeFocus
            PropertyChanges {
                target: background
                borderColor: "#FF4757"
            }
        },
        State {
            name: "DISABLED"
            when: !textInput.enabled
            PropertyChanges {
                target: background
                opacity: 0.6
            }
        }
    ]
    
    Behavior on borderColor {
        ColorAnimation { duration: 200 }
    }
}

CxKeypad.qml:

import QtQuick 2.0
import QtQuick.Controls 1.1

CxRect {
    id: root
    width: 1024   // 更紧凑的宽度
    height: 375  // 更紧凑的高度
    color: "#FFFFFF"
    
    // 键盘状态
    property bool capsLock: false
    property bool shiftPressed: false
    
    // 键盘信号
    signal keyPressed(string key)
    signal specialKeyPressed(string keyType)
    
    // 紧凑布局数据
    property var layout: [
        ["1","2","3","4","5","6","7","8","9","0"],
        ["q","w","e","r","t","y","u","i","o","p"],
        ["a","s","d","f","g","h","j","k","l"],
        ["Shift","z","x","c","v","b","n","m","Backspace"],
        ["?", "/", "Space",",",".","Enter"]  
    ]
    
    // 特殊键宽度比例
    property var specialKeyRatios: {
        "Shift": 1.4,
        "Backspace": 1.4,
        "Space": 3.0,
        "Enter": 1.4
    }
    
    // 动态尺寸计算
    property real rowHeight: Math.max(35, height / 5.5)
    property real keySpacing: Math.min(4, width * 0.004)
    property real baseKeyWidth: (width - 16 - (9 * keySpacing)) / 10

    //=== 事件不透传区域 ======
    MouseArea {
        anchors.fill: parent
        preventStealing: true
        propagateComposedEvents: false
        onPressed: (mouse) => { mouse.accepted = true }
        onReleased: (mouse) => { mouse.accepted = true }
    }
    
    // 键盘布局
    Column {
        anchors {
            fill: parent
            margins: 8
        }
        spacing: keySpacing
        
        Repeater {
            model: root.layout
            
            Row {
                id: keyRow
                spacing: keySpacing
                height: rowHeight
                
                property var rowKeys: modelData
                property real rowWidth: {
                    let total = 0;
                    for (let key of rowKeys) {
                        total += getKeyWidth(key);
                    }
                    return total + ((rowKeys.length - 1) * spacing);
                }
                x: (parent.width - rowWidth) / 2
                
                Repeater {
                    model: keyRow.rowKeys
                    
                    TextButton {
                        id: keyButton
                        width: getKeyWidth(modelData)
                        height: rowHeight
                        text: getDisplayText(modelData)
                        fontSize: Math.min(18, rowHeight * 0.32)
                        
                        onClicked: handleKeyPress(modelData)
                    }
                }
            }
        }
    }
    
    // 获取按键显示文本
    function getDisplayText(key) {
        if (key === "Shift") return shiftPressed ? "SHIFT" : "Shift"
        if (/^[a-z]$/.test(key)) {
            return (capsLock || shiftPressed) ? key.toUpperCase() : key
        }
        return key
    }
    
    // 获取按键宽度
    function getKeyWidth(key) {
        return specialKeyRatios[key] ? baseKeyWidth * specialKeyRatios[key] : baseKeyWidth
    }
    
    // 按键处理
    function handleKeyPress(key) {
        switch(key) {
        case "Shift":
            shiftPressed = !shiftPressed
            root.specialKeyPressed("shift")
            break
        case "Backspace":
            root.specialKeyPressed("backspace")
            break
        case "Enter":
            root.specialKeyPressed("enter")
            break
        case "Space":
            root.keyPressed(" ")
            break
        default:
            root.keyPressed(getDisplayText(key))
            // 字母输入后自动取消shift状态
            if (shiftPressed && /^[a-z]$/.test(key)) {
                shiftPressed = false
            }
        }
    }
}

CxLoaderKey.qml:

import QtQuick 2.0
import QtQuick.Controls 1.1

Item {
    id: root
    property int pos_x: 0
    property int pos_y: 0
    property Item currentTarget: null  // 当前绑定的输入框
    
    function show(target) {
        root.currentTarget = target
        loader.active = true  // 激活Loader加载键盘
    }
    
    function hide() {
        loader.active = false
        root.currentTarget = null
    }
    
    Loader {
        id: loader
        active: false
        sourceComponent: CxKeypad {
            x: root.pos_x
            y: root.pos_y
            onKeyPressed: (key) => {
                // 参数防御:处理undefined或null
                let actualKey = (typeof key === 'undefined' || key === null) ? "" : key;
                root.currentTarget.insert(root.currentTarget.cursorPosition, String(actualKey))
            }
            onSpecialKeyPressed: (keyType) => {
                // 参数防御:处理undefined或null
                let actualKey = (typeof keyType === 'undefined' || keyType === null) ? "" : keyType;
                if (actualKey == "enter") {
                    root.hide()  // 处理键盘关闭按钮
                }
                else if (actualKey == "backspace") {
                    root.currentTarget.remove(root.currentTarget.cursorPosition - 1, root.currentTarget.cursorPosition)
                }                
            }
        }
    }
}

三、逐文件重点解析与实现细节

  1. CxInput.qml(输入端)
  • keyboardLoader 属性:外部通过把 CxLoaderKey 的实例绑定到 CxInput.keyboardLoader,实现输入框与键盘加载器解耦。好处是同一键盘 loader 能服务多个输入框、且可统一控制位置与显示策略。
  • onActiveFocusChanged:当 TextInput 获得焦点即调用 keyboardLoader.show(this),并传入自身作为 target。这是“软键盘常用模式”。注意:activeFocus 可能在短时间内波动(焦点切换),所以调用需防抖或配合 Loader 的生命周期管理。
  • 占位符与清除按钮:Placeholder 采用独立 StyleText 显示,避免与 TextInput.text 绑定冲突。清除按钮只有在有文本且非密码模式下可见。
  • 安全与状态:使用 states 管理可接受输入与禁用态,动画平滑更好。

Qt5.1 注意点:

  • TextInput.activeFocus 在某些 Qt 版本有不同触发时序(例如 createWindowContainer 混合时),测试焦点流转很重要。
  • 对于软键盘,需要确保 TextInput 的 cursorPosition 与 insert/remove 接口行为在 Qt 5.1 下一致;传入 loader 当前 target 时直接使用 cursorPosition 来插入是合理的,但要处理越界(例如 cursorPosition==0 时删除要边界检查)。
  1. CxKeypad.qml(键盘视图)
  • 布局策略:使用 layout 数组 + Repeater 构建键盘,getKeyWidth 与 specialKeyRatios 保持弹性宽度。rowWidth 计算用于水平居中。这个纯 QML 的做法简单且易扩展。
  • Shift / Caps 行为:通过两个布尔属性 capsLock 与 shiftPressed 管理。实现上:按 Shift 切换 shiftPressed,并在字母输入后自动取消 shift(常见行为)。未实现长按切换 CapsLock(可扩展)。
  • 按键事件分离:keyPressed(字符)与 specialKeyPressed(特殊键类型)用于上层区别处理,CxLoaderKey 捕获 specialKeyPressed 并做删除/回车/隐藏等。
  • MouseArea 的防透传:键盘顶部有一个全覆盖 MouseArea,preventStealing: true 与 propagateComposedEvents: false,确保键盘不会把事件传递到底层,使键盘成为事件“阻挡区”。在嵌入 overlay 场景下这是必须的。

Qt5.1 注意点与陷阱:

  • propagateComposedEvents 在旧 Qt 版本实现细微差异,如果遇到子项事件无法触发,先测试去掉 propagateComposedEvents 或改用 accept/reject 逻辑。
  • 在触摸设备上,MouseArea 的 preventStealing 可避免外部 Flickable 抢夺事件(重要)。
  • 字体尺寸计算:使用 fontSize 与 rowHeight 的关系避免在高 DPI 下按键过小。Qt5.1 对 DPI 支持有限,必要时以像素为基准并在启动时根据屏幕密度调整比例。
  1. CxLoaderKey.qml(Loader 封装)
  • Loader.active 管理:active: true 会创建 sourceComponent 的实例,active: false 会销毁。该策略节省内存与资源(不需要常驻键盘对象)。在 Qt5.1 上,Loader 的行为稳定且推荐用于软键盘场景。
  • currentTarget 的使用:在 show(target) 中保存 target,并在按键事件回调中调用 target.insert/remove 操作。这里的直接操作简单高效但也承担了对 target 接口的假定(必须有 cursorPosition、insert、remove)。若 target 不是 TextInput(比如 TextEdit 或自定义组件),需要适配接口。
  • 参数防御:代码中对 undefined/null 做处理,增加鲁棒性。建议在生产中进一步检测 target 是否存在并且可写。

四、集成示例(最小可运行示例)

主 QML 文件 main.qml 示例,展示如何把 loader 放到根层并把 keyboardLoader 绑定到 CxInput:

import QtQuick 2.0
import QtQuick.Controls 1.1

Rectangle {
    width: 800; height: 480; color: "#2b2b2b"

    // 键盘 loader 放在根上,便于控制位置(pos_x/pos_y)
    CxLoaderKey {
        id: keyboard
        pos_x: 50
        pos_y: 120
    }

    Column {
        anchors.centerIn: parent
        spacing: 16

        CxInput {
            id: input1
            width: 600
            height: 56
            placeholder: "请输入用户名"
            keyboardLoader: keyboard
            onInputTextChanged: console.log("input1 changed:", text)
        }

        CxInput {
            id: input2
            width: 600
            height: 56
            placeholder: "请输入密码"
            passwordMode: true
            keyboardLoader: keyboard
            onInputTextChanged: console.log("input2 changed:", text)
        }
    }
}

运行逻辑:点击任一输入框,TextInput 获得焦点 -> onActiveFocusChanged 调用 keyboard.show(this) -> Loader active 创建 CxKeypad -> CxKeypad 的按键信号经由 Loader 的 onKeyPressed/onSpecialKeyPressed 注入 target(TextInput)。

五、常见扩展与优化(工程实用点)

  1. 长按重复(Backspace 长按删除)
  • 在 CxKeypad 的 TextButton 上为 Backspace 增加 press-and-hold 逻辑:MouseArea.onPressed 启动 Timer(初始延时),Timer 超时后每隔 repeatInterval 触发删除事件,MouseArea.onReleased 停止 Timer。这样用户体验接近系统键盘。
  1. 候选词 / 输入法(IME)集成
  • 目前实现直接插入字符,若需中文输入或候选词栏,建议把 CxKeypad 抽象为“键事件产生器”并把高层汉字候选/拼音处理交给 JavaScript 模块或 C++ 服务(复杂但性能更好)。另外可在键盘上加一行候选区并在 CxLoaderKey 中处理选择插入。
  1. 软键盘位置与遮挡(尤其在 Widgets + QWindow 混合场景)
  • 如果界面混合 QWidget 与 QWindow(createWindowContainer),则键盘显示位置需要转换到屏幕坐标(mapToGlobal / mapFromGlobal),并动态计算 pos_x/pos_y 避免遮挡输入框。把 CxLoaderKey.show(target) 扩展为 show(target, preferredSide) 并在内部做坐标映射更稳健。
  1. 防止重复 Loader 实例化 / 状态保持
  • Loader.active=false 会销毁键盘实例,若希望保持状态(如 CapsLock),可以把 Loader.sourceComponent 改为 source(指向 qml 文件),并用 visible 控制显示,而不是销毁。权衡点:内存 vs 状态保持。

六、测试要点与边界情况

  • 焦点切换测试:在表单模式快速切换输入框,验证 keyboard.show/hide 时序是否会引起重复创建或漏调用。建议在 show() 中增加 debounce(短时间内忽略重复调用)。
  • 坐标映射测试:在不同 DPI、分辨率与旋转(车机)下测试键盘覆盖/遮挡问题。
  • touch/mouse 混合:在既有物理鼠标又有触摸的设备上,检查 MouseArea 的 preventStealing 是否影响外部 Flickable/滑动控件。
  • 性能:在低端嵌入式设备上确认 Repeater + many visual items 不导致卡顿。可在键盘渲染上减少动画、避免复杂绑定。

七、可复用改进建议(工程级)

  • 把键盘布局(layout)抽成外部 JSON 或 JS 配置,支持不同语言/符号集合动态加载。
  • 为键盘增加 state store(保存 caps/shift 状态),并在 Loader 重新激活时恢复状态(若需要)。
  • 将对 TextInput 的直接 insert/remove 操作封装为“adapter”接口,支持不同种类的目标(TextInput、TextEdit、C++ 控件)。
  • 对于复杂输入需求,考虑把字符流通过信号发送到 C++ 层进行统一处理(比如拼音解析、候选词管理),再把结果反馈回 QML。

八、结语(工程实战建议)


该三件套(CxInput + CxKeypad + CxLoaderKey)是典型的软键盘实现范式:输入端与键盘端解耦、Loader 管理生命周期、事件/信号用于解藕。对 Qt 5.1 的限制要有意识(Controls 1.x、有限的 DPI 自适应、少量平台差异),但通过良好的职责划分与明确的 adapter 层,能够在嵌入式或车机项目中长期维护。建议先把核心交互(焦点/插入/删除)覆盖完善,再逐步增加长按、候选、语言切换等功能。

 

 

使用 Qt 编写一个软键盘(虚拟键盘)是一个常见的需求,尤其是在嵌入式系统、触摸屏设备或无物理键盘的场景中。下面是一个基于 **PyQt5** 的完整软键盘实现示例,支持基本字母、数字、符号输入以及退格、空格、切换大小写和关闭功能。 --- ### ✅ 功能说明: - 支持小写/大写切换 - 数字与符号行 - 退格键(Backspace) - 空格键 - 切换键盘布局(字母 / 符号) - 发送字符到焦点控件(如 QLineEdit) --- ### 🧩 使用技术栈: - Python + PyQt5 - 面向对象设计 - 信号与槽机制 --- ### 💡 示例代码:`virtual_keyboard.py` ```python import sys from PyQt5.QtWidgets import ( QApplication, QWidget, QVBoxLayout, QGridLayout, QPushButton, QLineEdit, QHBoxLayout, QFrame ) from PyQt5.QtCore import Qt, pyqtSignal class VirtualKeyboard(QWidget): # 自定义信号,用于发送输入字符 keyPressed = pyqtSignal(str) def __init__(self): super().__init__() self.shift_active = False self.symbol_mode = False self.init_ui() self.setStyleSheet(""" QPushButton { font-size: 18px; padding: 10px; margin: 2px; background-color: #f0f0f0; border: 1px solid #ccc; min-height: 40px; } QPushButton:hover { background-color: #e0e0e0; } QPushButton:pressed { background-color: #d0d0d0; } """) def init_ui(self): main_layout = QVBoxLayout() self.setLayout(main_layout) # 创建按键网格 self.grid = QGridLayout() self.grid.setSpacing(2) main_layout.addLayout(self.grid) self.create_keys() # 控制按钮:Space, Backspace, Shift, Switch Mode, Close bottom_layout = QHBoxLayout() main_layout.addLayout(bottom_layout) self.btn_close = QPushButton("关闭") self.btn_close.clicked.connect(self.hide) self.btn_shift = QPushButton("⇧ Shift") self.btn_shift.setCheckable(True) self.btn_shift.toggled.connect(self.toggle_shift) self.btn_symbol = QPushButton("符 号") self.btn_symbol.setCheckable(True) self.btn_symbol.toggled.connect(self.toggle_symbol_mode) self.btn_backspace = QPushButton("⌫ Backspace") self.btn_backspace.clicked.connect(lambda: self.keyPressed.emit("Backspace")) self.btn_space = QPushButton("空格") self.btn_space.clicked.connect(lambda: self.keyPressed.emit(" ")) bottom_layout.addWidget(self.btn_close) bottom_layout.addWidget(self.btn_shift) bottom_layout.addWidget(self.btn_symbol) bottom_layout.addWidget(self.btn_backspace) bottom_layout.addWidget(self.btn_space) def create_keys(self): # 清除旧按键 for i in reversed(range(self.grid.count())): self.grid.itemAt(i).widget().setParent(None) if self.symbol_mode: self.add_symbol_keys() else: self.add_letter_keys() def add_letter_keys(self): # 键盘布局(小写) rows = [ ['`', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '=', '⌫'], ['q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '[', ']', '\\'], ['a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', "'", 'Enter'], ['↑', 'z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '/', '↑'], ['Ctrl', 'Win', 'Alt', 'Space', 'AltGr', 'Ctrl', '←', '→'] ] if self.shift_active: rows[0] = ['~', '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '_', '+', '⌫'] rows[1] = ['Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', '{', '}', '|'] rows[2] = ['A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', ':', '"', 'Enter'] rows[3] = ['↑', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', '<', '>', '?', '↑'] for row_idx, row in enumerate(rows): col_idx = 0 for key in row: btn = QPushButton(key) btn.clicked.connect(lambda _, k=key: self.on_key_click(k)) if key == '⌫': btn.setText("⌫") self.grid.addWidget(btn, row_idx, col_idx, 1, 2) col_idx += 2 elif key == 'Enter': self.grid.addWidget(btn, row_idx, col_idx, 1, 2) col_idx += 2 elif key == '↑': btn.setCheckable(False) self.grid.addWidget(btn, row_idx, col_idx, 1, 2) col_idx += 2 elif key == 'Space': self.grid.addWidget(btn, row_idx, col_idx, 1, 5) col_idx += 5 elif key in ['←', '→']: self.grid.addWidget(btn, row_idx, col_idx, 1, 1) col_idx += 1 else: self.grid.addWidget(btn, row_idx, col_idx, 1, 1) col_idx += 1 def add_symbol_keys(self): symbols = [ ['!', '@', '#', '$', '%', '^', '&', '*', '(', ')'], ['-', '_', '=', '+', '[', ']', '{', '}', ';', ':'], ['"', "'", '\\', '|', ',', '<', '.', '>', '/', '?'], ['~', '`', '/', '|', '⌫'] ] for row_idx, row in enumerate(symbols): for col_idx, key in enumerate(row): btn = QPushButton(key) btn.clicked.connect(lambda _, k=key: self.keyPressed.emit(k)) if key == '⌫': self.grid.addWidget(btn, row_idx, col_idx, 1, 2) else: self.grid.addWidget(btn, row_idx, col_idx, 1, 1) def on_key_click(self, key): if key == "⌫": self.keyPressed.emit("Backspace") elif key == "Enter": self.keyPressed.emit("\n") elif key == "↑": # 手动触发 shift 切换 self.btn_shift.toggle() elif key == "Space": self.keyPressed.emit(" ") elif key in ["Ctrl", "Win", "Alt", "AltGr", "←", "→"]: pass # 忽略功能键 else: self.keyPressed.emit(key) def toggle_shift(self, state): self.shift_active = state self.create_keys() def toggle_symbol_mode(self, state): self.symbol_mode = state self.create_keys() class KeyboardApp(QWidget): def __init__(self): super().__init__() self.init_ui() def init_ui(self): layout = QVBoxLayout() self.edit = QLineEdit() self.edit.setPlaceholderText("点击此处输入...") self.edit.setFocusPolicy(Qt.StrongFocus) # 允许获得焦点 self.edit.mousePressEvent = self.show_keyboard layout.addWidget(QLabel("输入框:")) layout.addWidget(self.edit) self.keyboard = VirtualKeyboard() self.keyboard.keyPressed.connect(self.handle_key_input) self.keyboard.setFixedHeight(300) layout.addWidget(self.keyboard) self.setLayout(layout) # 默认隐藏键盘 self.keyboard.hide() self.setWindowTitle("虚拟软键盘") self.resize(600, 400) def show_keyboard(self, event): QLineEdit.mousePressEvent(self.edit, event) # 原始事件处理 self.keyboard.show() def handle_key_input(self, key): if key == "Backspace": self.edit.backspace() elif key == " ": self.edit.insert(" ") elif key == "\n": self.edit.insert("\n") else: self.edit.insert(key.upper() if self.keyboard.shift_active and not self.keyboard.symbol_mode else key) if __name__ == '__main__': app = QApplication(sys.argv) window = KeyboardApp() window.show() sys.exit(app.exec_()) ``` > ⚠️ 注意:上面代码中漏了一个 `QLabel` 导入,请在文件顶部加上: ```python from PyQt5.QtWidgets import QLabel ``` --- ### 🔍 代码解释: | 部分 | 说明 | |------|------| | `VirtualKeyboard` 类 | 实现软键盘 UI 和逻辑,通过 `keyPressed` 信号向外发送按键信息 | | `keyPressed` 信号 | 向外发射用户按下的字符或指令(如 Backspace) | | `toggle_shift` / `toggle_symbol_mode` | 切换大小写和符号模式 | | `on_key_click()` | 处理特殊按键(如回车、退格、Shift) | | `KeyboardApp` | 主界面,包含 QLineEdit 和软键盘实例 | | `mousePressEvent` 重写 | 当点击输入框时弹出软键盘 | | `handle_key_input()` | 接收信号并更新文本框内容 | --- ### ✅ 运行环境要求: ```bash pip install PyQt5 ``` 然后运行脚本即可看到图形化软键盘。 --- ### 🛠️ 可扩展方向: - 添加多语言支持(中文拼音?) - 支持触摸优化(更大按钮) - 模态窗口形式显示键盘 - 自动跟随焦点控件位置 - 使用 QML 实现更美观动画效果 - 绑定至所有 `QLineEdit` 子类自动启用 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值