Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 

README.md

🔘 Radio Component - 单选框组件

状态:✅ 已完成
难度:⭐⭐
代码行数:~283 行


📋 组件概述

Radio 是一个交互式单选框组件,演示了互斥选择、圆形绘制和分组管理技术。

核心功能

  • 互斥选择:同一组内只能选中一个选项
  • 圆形外观:使用圆形代替方形复选框
  • 分组管理:支持多个独立的单选框组
  • 点击切换:鼠标点击单选框改变选择
  • 悬停效果:鼠标悬停时的高亮反馈
  • 键盘控制:数字键快速选择
  • 实时预览:可视化显示当前选择

🎨 视觉效果

单选框状态

状态 描述 视觉效果
未选中 默认状态 空的圆圈
选中 激活状态 圆圈内显示蓝色圆点
悬停 鼠标悬停 高亮圆圈 + 半透明外圈

组件布局

┌─────────────────────────────────────┐
│  Radio Button Component             │
│  [1-3] Select color   [ESC] Quit    │
│                                     │
│  Color:        Speed:               │
│  ◉ Red         ○ Slow               │
│  ○ Green       ○ Normal             │
│  ○ Blue        ◉ Fast               │
│                                     │
│  Size:         ┌───────────┐        │
│  ○ Small       │ Preview:  │        │
│  ◉ Medium      │           │        │
│  ○ Large       │     ●     │        │
│                │ Speed:... │        │
│                └───────────┘        │
└─────────────────────────────────────┘

🖱️ 交互演示

鼠标交互

// 1. 获取鼠标位置
float mouseX, mouseY;
input->getMousePosition(mouseX, mouseY);

// 2. 检测点击
if (isMousePressed && !wasMousePressed) {
    // 检查是否点击在单选框内
    for (int i = 0; i < 9; i++) {
        if (isPointInRadio(mouseX, mouseY, radios[i])) {
            // 设置该组的选中索引(互斥)
            *(radios[i].groupSelection) = radios[i].index;
            break;
        }
    }
}

键盘控制

按键 功能
1 选择颜色:红色
2 选择颜色:绿色
3 选择颜色:蓝色
ESC 退出程序

🎯 核心实现

1. 单选框数据结构

struct RadioData {
    float x, y, radius;          // 位置和半径
    int* groupSelection;         // 指向组选中索引的指针
    int index;                   // 当前选项在组内的索引
    const char* label;           // 标签文本
};

// 组选中状态
int colorChoice = 0;   // 0=红, 1=绿, 2=蓝
int sizeChoice = 1;    // 0=小, 1=中, 2=大
int speedChoice = 2;   // 0=慢, 1=中, 2=快

// 单选框数组(3组 x 3选项 = 9个)
RadioData radios[9] = {
    {x, y, r, &colorChoice, 0, "Red"},
    {x, y, r, &colorChoice, 1, "Green"},
    {x, y, r, &colorChoice, 2, "Blue"},
    // ... 其他组
};

2. 点击检测(圆形碰撞)

bool isPointInRadio(float x, float y, const RadioData& radio) const {
    // 计算点到圆心的距离
    float dx = x - radio.x;
    float dy = y - radio.y;
    float distSq = dx * dx + dy * dy;
    
    // 判断是否在圆内
    return distSq <= radio.radius * radio.radius;
}

3. 互斥选择逻辑

// 当点击单选框时
if (isPointInRadio(mouseX, mouseY, radios[i])) {
    // 将该组的选中索引设置为当前选项的索引
    // 同一组的其他选项自动变为未选中
    *(radios[i].groupSelection) = radios[i].index;
}

// 渲染时判断是否选中
bool isSelected = (*(radio.groupSelection) == radio.index);

4. 单选框渲染

void drawRadio(const RadioData& radio, bool isHovered) {
    // 1. 绘制外圆背景
    renderer->drawCircle(radio.x, radio.y, radio.radius, colorCircleBg);
    
    // 2. 绘制边框(双层圆圈模拟)
    float borderWidth = 2.0f;
    renderer->drawCircle(radio.x, radio.y, radio.radius, borderColor);
    renderer->drawCircle(radio.x, radio.y, radio.radius - borderWidth, colorCircleBg);
    
    // 3. 如果选中,绘制内部圆点
    bool isSelected = (*(radio.groupSelection) == radio.index);
    if (isSelected) {
        float dotRadius = radio.radius * 0.5f;
        renderer->drawCircle(radio.x, radio.y, dotRadius, colorDot);
    }
    
    // 4. 绘制标签
    renderer->drawText(radio.label, ...);
    
    // 5. 如果悬停,绘制高亮
    if (isHovered) {
        renderer->drawCircle(radio.x, radio.y, radio.radius + 4.0f, highlightColor);
    }
}

5. 实时预览

void drawPreview() {
    // 根据选择确定预览圆的属性
    
    // 颜色
    float previewColor[4];
    if (colorChoice == 0) {
        // 红色
        previewColor = {0.9f, 0.3f, 0.3f, 1.0f};
    } else if (colorChoice == 1) {
        // 绿色
        previewColor = {0.3f, 0.9f, 0.3f, 1.0f};
    } else {
        // 蓝色
        previewColor = {0.3f, 0.5f, 0.9f, 1.0f};
    }
    
    // 大小
    float previewRadius;
    if (sizeChoice == 0) {
        previewRadius = 15.0f;  //
    } else if (sizeChoice == 1) {
        previewRadius = 25.0f;  //
    } else {
        previewRadius = 35.0f;  //
    }
    
    // 绘制预览
    renderer->drawCircle(centerX, centerY, previewRadius, previewColor);
}

🔬 技术要点

1. 圆形碰撞检测

点对圆碰撞

distance² = (x - cx)² + (y - cy)²
if distance² <= radius²:
    点在圆内

优化:使用距离平方避免开方计算。

2. 互斥选择实现

关键思路

  • 每个组有一个 int 类型的选中索引
  • 所有单选框共享该索引的指针
  • 点击时只需修改索引值
  • 渲染时比较索引判断是否选中
// 同一组的单选框共享选中索引
int groupSelection = 0;

RadioData option1 = {..., &groupSelection, 0, ...};
RadioData option2 = {..., &groupSelection, 1, ...};
RadioData option3 = {..., &groupSelection, 2, ...};

// 点击 option2
groupSelection = 1;  // option1 和 option3 自动未选中

3. 圆形边框绘制

由于 drawCircle 绘制实心圆,使用双层圆圈模拟边框:

  1. 绘制外圆(边框颜色)
  2. 绘制稍小的内圆(背景颜色)
  3. 外圆和内圆的半径差即为边框宽度
renderer->drawCircle(x, y, radius, borderColor);       // 外圆
renderer->drawCircle(x, y, radius - 2.0f, bgColor);    // 内圆
// 结果:2 像素宽的边框

📊 性能分析

渲染调用统计

每帧渲染调用(3 组 x 3 选项 = 9 个单选框):

  • 外圆背景:9 次
  • 外圆边框:9 次
  • 内圆遮罩:9 次
  • 选中圆点:3 次(每组 1 个选中)
  • 悬停高亮:0-1 次
  • 文本渲染:13 次(9 个标签 + 3 个组标题 + 1 个说明)
  • 预览框:~8 次(背景 + 边框 + 圆 + 文本)

总计:约 50-60 次渲染调用/帧


🎓 学习要点

初学者

  1. 互斥选择逻辑

    • 如何实现"只能选一个"的约束
    • 使用共享变量管理组状态
  2. 圆形碰撞检测

    • 点对圆的距离计算
    • 距离平方优化技巧
  3. 分组管理

    • 多个独立组的数据结构
    • 指针在分组中的应用

进阶开发者

  1. 组件设计模式

    • 数据驱动的组件设计
    • 状态共享与隔离
  2. 图形技巧

    • 双层圆圈模拟边框
    • 圆形 UI 元素的实现
  3. 交互反馈

    • 实时预览的联动效果
    • 多种输入方式的支持

🚀 扩展方向

简单扩展

  • 默认选项:初始化时指定默认选中
  • 禁用状态:灰色显示且无法选择
  • 动画效果:圆点出现/消失的缩放动画

中等扩展

  • 动态分组:运行时添加/删除选项
  • 自定义样式:不同组使用不同颜色
  • 描述文本:在标签下方添加详细说明

高级扩展

  • 键盘导航:Tab 键切换焦点,方向键移动,Enter 确认
  • 数据绑定:与数据模型双向绑定
  • 事件回调:选择改变时触发回调函数

🏗️ 构建与运行

构建步骤

# 1. 配置项目
cd examples/cpp/radio
cmake -S . -B build -G "Visual Studio 17 2022" -A x64

# 2. 编译
cmake --build build --config Release

# 3. 复制 DLL 到运行时目录
copy build\Release\Release\radio.dll ..\..\..\..\native\build\Release\

# 4. 编译着色器
cd assets\shaders
compile_shaders.bat

运行组件

cd ..\..\..\..\native
.\build\Release\bitui_native.exe .\build\Release\radio.dll

📁 文件结构

radio/
├── radio.cpp                         # 组件实现(~283 行)
├── 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/
            └── radio.dll             # 组件库

🔗 相关组件

组件 关系 说明
Checkbox 兄弟组件 复选框允许多选,单选框只能选一个
Button 兄弟组件 相似的点击交互模式
Switch 兄弟组件 同样是状态切换组件

📚 参考资料

API 使用

  • IInput::isMouseButtonPressed(int button) - 检测鼠标按键状态
  • IInput::getMousePosition(float& x, float& y) - 获取鼠标位置
  • IInput::isKeyJustPressed(int keyCode) - 检测键盘按键
  • IRenderer::drawCircle() - 绘制圆形
  • IRenderer::drawRectangle() - 绘制矩形
  • IRenderer::drawText() - 绘制文本

数学基础

  • 距离公式distance = sqrt((x2-x1)² + (y2-y1)²)
  • 距离平方distSq = (x2-x1)² + (y2-y1)²(避免开方)
  • 圆形碰撞distSq <= radius²

💡 最佳实践

  1. 互斥选择实现

    • 使用共享索引而非布尔值
    • 便于扩展到更多选项
  2. 指针使用

    • 通过指针共享组状态
    • 减少数据冗余
  3. 碰撞检测优化

    • 使用距离平方避免开方
    • 先检查矩形包围盒再精确检测
  4. 视觉反馈

    • 提供悬停、选中多种状态
    • 实时预览增强用户体验

🐛 调试建议

常见问题

  1. 点击无响应

    • 检查圆形碰撞检测公式
    • 验证鼠标坐标系统
    • 确认 wasMousePressed 状态更新
  2. 选中状态不正确

    • 检查指针是否正确指向组索引
    • 验证索引比较逻辑
    • 确认组的初始值
  3. 边框显示异常

    • 检查双层圆圈的半径差
    • 验证颜色 alpha 通道
    • 确认绘制顺序(外圆在前,内圆在后)
  4. 多个选项同时选中

    • 检查是否正确使用同一个组索引
    • 验证指针是否指向正确的变量

📈 版本历史

  • v1.0 (2025-10-12) - 初始版本
    • 实现基础单选框功能
    • 支持 3 组独立单选框(9 个选项)
    • 添加悬停和点击交互
    • 实现圆形边框绘制
    • 添加实时预览功能

📞 技术支持

  • 项目主页:Bit HCI
  • 文档:docs/guides/
  • 示例:examples/cpp/

🎨 与 Checkbox 的对比

特性 Radio Checkbox
形状 圆形 方形
选择模式 单选(互斥) 多选(独立)
选中标记 实心圆点 勾选符号 ✓
状态管理 组索引 独立布尔值
适用场景 单项选择题 多项选择题

Happy Coding! 🎉