目录
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)
}
}
}
}
}
三、逐文件重点解析与实现细节
- 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 时删除要边界检查)。
- 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 支持有限,必要时以像素为基准并在启动时根据屏幕密度调整比例。
- 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)。
五、常见扩展与优化(工程实用点)
- 长按重复(Backspace 长按删除)
- 在 CxKeypad 的 TextButton 上为 Backspace 增加 press-and-hold 逻辑:MouseArea.onPressed 启动 Timer(初始延时),Timer 超时后每隔 repeatInterval 触发删除事件,MouseArea.onReleased 停止 Timer。这样用户体验接近系统键盘。
- 候选词 / 输入法(IME)集成
- 目前实现直接插入字符,若需中文输入或候选词栏,建议把 CxKeypad 抽象为“键事件产生器”并把高层汉字候选/拼音处理交给 JavaScript 模块或 C++ 服务(复杂但性能更好)。另外可在键盘上加一行候选区并在 CxLoaderKey 中处理选择插入。
- 软键盘位置与遮挡(尤其在 Widgets + QWindow 混合场景)
- 如果界面混合 QWidget 与 QWindow(createWindowContainer),则键盘显示位置需要转换到屏幕坐标(mapToGlobal / mapFromGlobal),并动态计算 pos_x/pos_y 避免遮挡输入框。把 CxLoaderKey.show(target) 扩展为 show(target, preferredSide) 并在内部做坐标映射更稳健。
- 防止重复 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 层,能够在嵌入式或车机项目中长期维护。建议先把核心交互(焦点/插入/删除)覆盖完善,再逐步增加长按、候选、语言切换等功能。

2143

被折叠的 条评论
为什么被折叠?



