状态:✅ 已完成
难度:⭐⭐⭐
代码行数:~310 行
Input 是一个交互式输入框组件,演示了文本输入、光标管理和焦点控制技术。
- ✅ 文本输入:支持字母、数字、符号输入
- ✅ 光标显示:闪烁光标指示输入位置
- ✅ 焦点管理:点击聚焦、点击外部失焦
- ✅ 退格删除:删除字符
- ✅ 占位符:空时显示提示文本
- ✅ 多输入框:同时管理多个独立输入框
- ✅ Tab 切换:键盘切换焦点
- ✅ 字符限制:最大长度控制
- ✅ 视觉反馈:聚焦、悬停状态
| 状态 | 描述 | 视觉效果 |
|---|---|---|
| 空闲 | 无焦点、无内容 | 灰色边框 + 占位符 |
| 悬停 | 鼠标悬停 | 半透明高亮 |
| 聚焦 | 激活状态 | 蓝色边框 + 闪烁光标 |
| 有内容 | 已输入文本 | 显示文本内容 |
┌──────────────────────────────────┐
│ Input Component Demo │
│ [Tab] Switch [ESC] Quit │
│ │
│ Name: │
│ ┌────────────────────────────┐ │
│ │john_ │ │ ← 聚焦状态
│ └────────────────────────────┘ │
│ │
│ Email: │
│ ┌────────────────────────────┐ │
│ │example@email.com │ │ ← 占位符
│ └────────────────────────────┘ │
│ │
│ Password: │
│ ┌────────────────────────────┐ │
│ │******** │ │ ← 占位符
│ └────────────────────────────┘ │
│ │
│ Comment: │
│ ┌────────────────────────────┐ │
│ │hello world │ │ ← 已输入
│ └────────────────────────────┘ │
└──────────────────────────────────┘
// 1. 检测点击
if (isMousePressed && !wasMousePressed) {
float mouseX, mouseY;
input->getMousePosition(mouseX, mouseY);
// 检查是否点击输入框
for (int i = 0; i < 4; i++) {
if (isPointInInput(mouseX, mouseY, inputs[i])) {
focusedInput = i; // 设置焦点
break;
}
}
// 点击外部失去焦点
if (!clickedInput) {
focusedInput = -1;
}
}| 按键 | 功能 |
|---|---|
| A-Z | 输入字母(小写) |
| 0-9 | 输入数字 |
| Space | 输入空格 |
| Backspace | 删除最后一个字符 |
| Tab | 切换到下一个输入框 |
| ESC | 退出程序 |
struct InputData {
float x, y, width, height; // 位置和大小
char* text; // 文本内容指针
int maxLength; // 最大长度
const char* placeholder; // 占位符
const char* label; // 标签
};
// 文本缓冲区
static const int MAX_TEXT_LENGTH = 32;
char nameText[MAX_TEXT_LENGTH] = "";
char emailText[MAX_TEXT_LENGTH] = "";
// 输入框配置
InputData inputs[4] = {
{x, y, w, h, nameText, MAX_TEXT_LENGTH, "Enter your name...", "Name:"},
{x, y, w, h, emailText, MAX_TEXT_LENGTH, "example@email.com", "Email:"},
// ...
};// 全局焦点状态
int focusedInput = -1; // -1 表示无焦点
// 点击输入框获得焦点
if (isPointInInput(mouseX, mouseY, inputs[i])) {
focusedInput = i;
}
// 点击外部失去焦点
if (!clickedInput) {
focusedInput = -1;
}
// 只有聚焦时才处理键盘输入
if (focusedInput >= 0) {
handleKeyboardInput(inputs[focusedInput]);
}void handleKeyboardInput(InputData& inputBox) {
int len = strlen(inputBox.text);
// 退格键
if (input->isKeyJustPressed(8)) { // Backspace
if (len > 0) {
inputBox.text[len - 1] = '\0';
}
return;
}
// 长度限制
if (len >= inputBox.maxLength - 1) {
return;
}
// 字母 A-Z
for (int key = 'A'; key <= 'Z'; key++) {
if (input->isKeyJustPressed(key)) {
inputBox.text[len] = key + 32; // 转小写
inputBox.text[len + 1] = '\0';
return;
}
}
// 数字 0-9
for (int key = '0'; key <= '9'; key++) {
if (input->isKeyJustPressed(key)) {
inputBox.text[len] = key;
inputBox.text[len + 1] = '\0';
return;
}
}
// 空格和符号...
}// 光标状态
float cursorBlinkTime = 0.0f;
bool cursorVisible = true;
// 更新光标闪烁(每 0.5 秒切换)
void onUpdate(float deltaTime) {
cursorBlinkTime += deltaTime;
if (cursorBlinkTime >= 0.5f) {
cursorBlinkTime = 0.0f;
cursorVisible = !cursorVisible;
}
}
// 绘制光标(仅聚焦时)
if (isFocused && cursorVisible) {
float cursorX = textX + textLength * 8.0f; // 文本末尾
renderer->drawRectangle(cursorX, cursorY, 2.0f, cursorHeight, colorCursor);
}void drawInput(const InputData& inputBox, bool isFocused, bool isHovered) {
// 1. 绘制标签
renderer->drawText(inputBox.label, x, y - 20, colorLabel, 14.0f);
// 2. 绘制背景
renderer->drawRectangle(x, y, width, height, colorBoxBg);
// 3. 绘制边框(聚焦时使用不同颜色和宽度)
float* borderColor = isFocused ? colorBoxFocus : colorBox;
float borderWidth = isFocused ? 2.5f : 2.0f;
// ... 绘制四条边
// 4. 绘制文本或占位符
if (strlen(inputBox.text) > 0) {
renderer->drawText(inputBox.text, textX, textY, colorText, 14.0f);
} else if (!isFocused) {
renderer->drawText(inputBox.placeholder, textX, textY, colorPlaceholder, 14.0f);
}
// 5. 绘制光标(仅聚焦时)
if (isFocused && cursorVisible) {
renderer->drawRectangle(cursorX, cursorY, 2.0f, cursorHeight, colorCursor);
}
// 6. 绘制悬停效果
if (isHovered && !isFocused) {
renderer->drawRectangle(x, y, width, height, highlightColor);
}
}C 风格字符串:
char text[32] = ""; // 固定大小缓冲区
int len = strlen(text); // 获取长度
text[len] = 'a'; // 添加字符
text[len + 1] = '\0'; // null 终止符
text[len - 1] = '\0'; // 删除字符注意事项:
- 始终保留空间给
\0终止符 - 检查长度避免溢出
maxLength - 1是最多可输入的字符数
限制:
- API 只提供
isKeyJustPressed(keyCode) - 需要手动遍历所有可能的按键
- 没有大小写状态检测(简化为小写)
方案:
// 遍历所有字母
for (int key = 'A'; key <= 'Z'; key++) {
if (input->isKeyJustPressed(key)) {
// 处理输入
}
}
// 遍历所有数字
for (int key = '0'; key <= '9'; key++) {
if (input->isKeyJustPressed(key)) {
// 处理输入
}
}简化方案:
// 假设固定宽度字体,每个字符约 8 像素
float cursorX = textStartX + textLength * 8.0f;精确方案(需要 API 支持):
// 使用文本测量 API
float textWidth = renderer->measureText(text, fontSize);
float cursorX = textStartX + textWidth; 点击输入框
┌─────────────────┐
│ ▼
空闲 ────────────────> 聚焦
◄────────────────
点击外部/Tab切换
每帧渲染调用(4 个输入框):
- 背景矩形:4 次
- 边框矩形:16 次(每个输入框 4 条边)
- 文本/占位符:4 次
- 光标:0-1 次(仅聚焦时)
- 标签文本:4 次
- 悬停高亮:0-1 次
- 说明文本:2 次
总计:约 30-35 次渲染调用/帧
-
C 字符串操作
strlen、strcpy等函数- null 终止符的重要性
- 缓冲区溢出防护
-
焦点管理
- 全局焦点状态
- 点击获得/失去焦点
-
简单动画
- 光标闪烁的实现
- 时间累加和状态切换
-
输入处理
- 键盘事件的逐帧检测
- 字符映射和转换
- 输入限制和验证
-
状态同步
- 输入时重置光标闪烁
- 焦点改变时的状态更新
-
组件设计
- 数据与逻辑分离
- 统一的渲染接口
- 数字输入框:只允许输入数字
- 密码模式:显示 * 号代替实际字符
- 字符计数:显示已输入/最大字符数
- 光标移动:方向键移动光标到任意位置
- 文本选择:Shift+方向键选择文本
- 复制粘贴:Ctrl+C/V 支持
- 撤销重做:Ctrl+Z/Y 支持
- IME 输入:支持中文输入法
- 多行输入:文本域(textarea)
- 自动完成:建议列表
- 输入验证:正则表达式验证
- 格式化输入:电话号码、日期等
# 1. 配置项目
cd examples/cpp/input
cmake -S . -B build -G "Visual Studio 17 2022" -A x64
# 2. 编译
cmake --build build --config Release
# 3. 复制 DLL 到运行时目录
copy build\Release\Release\input.dll ..\..\..\..\native\build\Release\
# 4. 编译着色器
cd assets\shaders
compile_shaders.batcd ..\..\..\..\native
.\build\Release\bitui_native.exe .\build\Release\input.dllinput/
├── input.cpp # 组件实现(~310 行)
├── CMakeLists.txt # 构建配置
├── README.md # 本文档
├── assets/
│ └── shaders/ # 着色器目录
│ ├── triangle.vert # 顶点着色器(GLSL)
│ ├── triangle.frag # 片段着色器(GLSL)
│ ├── compile_shaders.bat # 着色器编译脚本
│ └── spv/ # 编译后的 SPIR-V
│ ├── triangle_vert.spv
│ └── triangle_frag.spv
└── build/ # 构建输出
└── Release/
└── Release/
└── input.dll # 组件库
| 组件 | 关系 | 说明 |
|---|---|---|
| Button | 兄弟组件 | 类似的点击交互 |
| Slider | 兄弟组件 | 类似的焦点管理 |
| Dropdown | 未实现 | 需要输入框作为基础 |
IInput::isMouseButtonPressed(int button)- 检测鼠标按键状态IInput::getMousePosition(float& x, float& y)- 获取鼠标位置IInput::isKeyJustPressed(int keyCode)- 检测键盘按键IRenderer::drawRectangle()- 绘制矩形IRenderer::drawText()- 绘制文本
strlen(const char* str)- 获取字符串长度strcpy(char* dest, const char* src)- 复制字符串strcat(char* dest, const char* src)- 连接字符串strcmp(const char* s1, const char* s2)- 比较字符串
-
字符串安全
- 始终检查长度避免溢出
- 确保 null 终止符存在
- 使用
maxLength - 1作为最大输入
-
焦点管理
- 明确的焦点状态
- 一次只有一个输入框聚焦
- 点击外部失去焦点
-
用户体验
- 光标闪烁提示输入位置
- 占位符提示输入内容
- 清晰的视觉反馈
-
输入处理
- 重置光标闪烁增强响应感
- 立即显示输入字符
- 防止重复输入(使用 JustPressed)
-
字符不显示
- 检查文本颜色是否与背景对比明显
- 验证文本位置计算
- 确认字体大小合适
-
光标不闪烁
- 检查时间累加逻辑
- 验证
cursorVisible状态切换 - 确认聚焦状态正确
-
无法输入
- 检查焦点状态
- 验证键码是否正确
- 确认长度未超限
-
缓冲区溢出
- 检查
maxLength限制 - 验证 null 终止符位置
- 确认数组大小足够
- 检查
- v1.0 (2025-10-12) - 初始版本
- 实现基础输入框功能
- 支持 4 个独立输入框
- 添加文本输入和删除
- 实现光标闪烁显示
- 添加焦点管理
- 支持 Tab 键切换
- 项目主页:
Bit HCI - 文档:
docs/guides/ - 示例:
examples/cpp/
本组件使用简化的输入处理方案:
- 只支持基本字符:字母、数字、空格、常用符号
- 固定小写:简化大小写处理
- 光标固定在末尾:简化光标管理
- 无文本选择:简化交互逻辑
这些简化使组件更容易理解和实现,同时保留了输入框的核心功能。在实际应用中,可以根据需求逐步添加更多功能。
Happy Coding! 🎉