Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 

README.md

🎚️ Slider Component - 滑动条组件

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


📋 组件概述

Slider 是一个交互式滑动条组件,演示了鼠标拖动、数值映射和范围控制技术。

核心功能

  • 鼠标拖动:拖动滑块改变值
  • 点击跳转:点击滑轨直接跳转到目标位置
  • 数值映射:位置 (0-1) 映射到自定义范围
  • 范围控制:支持不同的最小值和最大值
  • 多滑块支持:同时管理多个独立滑块
  • 实时反馈:显示当前数值和单位
  • 视觉状态:悬停、拖动不同状态
  • 实时预览:可视化显示当前设置

🎨 视觉效果

滑块状态

状态 描述 视觉效果
正常 默认状态 蓝色滑块
悬停 鼠标悬停 亮蓝色滑块
拖动 正在拖动 深蓝色滑块

组件布局

┌─────────────────────────────────────┐
│  Slider Component Demo              │
│  [R] Reset   [ESC] Quit             │
│                                     │
│  Volume              ┌─────────┐    │
│  ━━━━━━━━━━━━━━●─────  │ Preview:│    │
│                 70%  │         │    │
│  Brightness          │    ●    │    │
│  ━━━━━━━●────────────  │         │    │
│                 50%  │ Volume  │    │
│  Speed               │ -> Red  │    │
│  ━━━●────────────────  │ Size    │    │
│                3.0x  │ -> Spd  │    │
│  Opacity             └─────────┘    │
│  ━━━━━━━━━━━━━━━━━━●─               │
│                 1.00                │
└─────────────────────────────────────┘

🖱️ 交互演示

鼠标拖动

// 1. 检测鼠标按下
if (isMousePressed && !wasMousePressed) {
    // 检查是否点击在滑块上
    for (int i = 0; i < 4; i++) {
        if (isPointOnSlider(mouseX, mouseY, sliders[i])) {
            draggedSlider = i;
            updateSliderValue(mouseX, sliders[i]);
            break;
        }
    }
}

// 2. 拖动中更新值
if (isMousePressed && draggedSlider >= 0) {
    updateSliderValue(mouseX, sliders[draggedSlider]);
}

// 3. 鼠标释放停止拖动
if (!isMousePressed) {
    draggedSlider = -1;
}

键盘控制

按键 功能
R 重置所有滑块到默认值
ESC 退出程序

🎯 核心实现

1. 滑动条数据结构

struct SliderData {
    float x, y, width, height;  // 滑轨位置和大小
    float* value;               // 值指针(0.0 - 1.0)
    float min, max;             // 实际值范围
    const char* label;          // 标签
    const char* unit;           // 单位
};

// 滑块值(0.0 - 1.0)
float volumeValue = 0.7f;
float brightnessValue = 0.5f;

// 滑块配置
SliderData sliders[4] = {
    {x, y, w, h, &volumeValue, 0.0f, 100.0f, "Volume", "%"},
    {x, y, w, h, &brightnessValue, 0.0f, 100.0f, "Brightness", "%"},
    // ...
};

2. 点击检测(滑块 + 滑轨)

bool isPointOnSlider(float x, float y, const SliderData& slider) const {
    // 滑块位置
    float thumbX = slider.x + slider.width * (*(slider.value));
    float thumbRadius = 12.0f;
    
    // 检查是否在滑块上(圆形碰撞)
    float dx = x - thumbX;
    float dy = y - (slider.y + slider.height / 2);
    if (dx * dx + dy * dy <= thumbRadius * thumbRadius) {
        return true;
    }
    
    // 检查是否在滑轨上(矩形碰撞)
    return x >= slider.x && x <= slider.x + slider.width &&
           y >= slider.y - 10 && y <= slider.y + slider.height + 10;
}

3. 数值映射

void updateSliderValue(float mouseX, const SliderData& slider) {
    // 鼠标位置 -> 归一化值(0.0 - 1.0)
    float newValue = (mouseX - slider.x) / slider.width;
    
    // 限制范围
    if (newValue < 0.0f) newValue = 0.0f;
    if (newValue > 1.0f) newValue = 1.0f;
    
    // 更新值
    *(slider.value) = newValue;
}

// 渲染时:归一化值 -> 实际值
float actualValue = slider.min + (slider.max - slider.min) * (*(slider.value));

4. 滑动条渲染

void drawSlider(const SliderData& slider, bool isHovered, bool isDragging) {
    // 1. 绘制滑轨背景
    renderer->drawRectangle(slider.x, slider.y, slider.width, slider.height, 
                           colorTrack);
    
    // 2. 绘制滑轨填充(已滑动部分)
    float fillWidth = slider.width * (*(slider.value));
    renderer->drawRectangle(slider.x, slider.y, fillWidth, slider.height, 
                           colorTrackFill);
    
    // 3. 绘制滑块
    float thumbX = slider.x + slider.width * (*(slider.value));
    float thumbY = slider.y + slider.height / 2;
    
    // 根据状态选择颜色
    float* thumbColor = isDragging ? colorThumbDrag : 
                       (isHovered ? colorThumbHover : colorThumb);
    
    // 绘制阴影
    renderer->drawCircle(thumbX + 2, thumbY + 2, 12.0f, shadowColor);
    
    // 绘制滑块
    renderer->drawCircle(thumbX, thumbY, 12.0f, thumbColor);
    
    // 绘制边框
    renderer->drawCircle(thumbX, thumbY, 12.0f, borderColor);
    renderer->drawCircle(thumbX, thumbY, 10.0f, thumbColor);
    
    // 4. 绘制数值
    float actualValue = slider.min + (slider.max - slider.min) * (*(slider.value));
    renderer->drawText(valueText, ...);
}

🔬 技术要点

1. 归一化值的使用

为什么使用 0.0-1.0?

  • 内部统一使用归一化值(0.0-1.0)
  • 便于位置计算:thumbX = x + width * value
  • 渲染时才映射到实际范围

优点

  • 代码简洁,易于理解
  • 适用于任何范围
  • 便于插值和动画

2. 拖动状态管理

// 全局状态
int draggedSlider = -1;  // -1 表示没有拖动

// 按下时记录
if (mouseDown && !wasMouseDown) {
    draggedSlider = findClickedSlider();
}

// 拖动中持续更新
if (mouseDown && draggedSlider >= 0) {
    updateSlider(draggedSlider);
}

// 释放时清除
if (!mouseDown) {
    draggedSlider = -1;
}

3. 复合碰撞检测

滑动条需要检测两个区域:

  1. 滑块(圆形):精确控制
  2. 滑轨(矩形):快速跳转
// 先检测滑块(优先级高)
if (isPointInCircle(x, y, thumbX, thumbY, radius)) {
    return true;
}
// 再检测滑轨
if (isPointInRect(x, y, trackX, trackY, trackW, trackH)) {
    return true;
}
return false;

📊 性能分析

渲染调用统计

每帧渲染调用(4 个滑块):

  • 滑轨背景:4 次
  • 滑轨填充:4 次
  • 滑块阴影:4 次
  • 滑块主体:4 次
  • 滑块边框:8 次(外圈 + 内圈)
  • 文本渲染:12 次(4 个标签 + 4 个数值 + 4 个说明)
  • 预览框:~10 次(背景 + 边框 + 圆 + 文本)

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


🎓 学习要点

初学者

  1. 鼠标拖动

    • 如何检测拖动开始和结束
    • 拖动中的状态管理
  2. 数值映射

    • 归一化值的概念
    • 线性映射公式
  3. 交互反馈

    • 不同状态的视觉反馈
    • 实时数值显示

进阶开发者

  1. 状态机设计

    • 空闲 → 悬停 → 拖动状态转换
    • 全局拖动状态管理
  2. 碰撞检测优化

    • 复合碰撞(圆形 + 矩形)
    • 优先级处理
  3. 数据驱动设计

    • 使用数组管理多个滑块
    • 统一的渲染和交互逻辑

🚀 扩展方向

简单扩展

  • 步进值:按固定步长调整(如 0.1)
  • 双滑块:范围选择器(最小值-最大值)
  • 垂直滑动条:支持垂直方向

中等扩展

  • 键盘控制:方向键微调、PageUp/Down 大幅调整
  • 动画效果:释放后回弹到最近的步进值
  • 刻度标记:显示刻度线和标签

高级扩展

  • 非线性映射:对数、指数映射
  • 手势支持:触摸拖动、缩放
  • 值约束:与其他滑块联动(如最小值不能超过最大值)

🏗️ 构建与运行

构建步骤

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

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

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

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

运行组件

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

📁 文件结构

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

🔗 相关组件

组件 关系 说明
ProgressBar 类似组件 进度条是只读的滑动条
Button 兄弟组件 类似的鼠标交互
Switch 兄弟组件 简化的二值滑动条

📚 参考资料

API 使用

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

数学公式

  • 归一化映射normalized = (x - min) / (max - min)
  • 反归一化actual = min + normalized * (max - min)
  • 限制范围clamp(x, min, max) = max(min, min(x, max))

💡 最佳实践

  1. 归一化存储

    • 内部使用 0.0-1.0 存储
    • 显示时映射到实际范围
  2. 拖动状态

    • 使用全局标记防止多个滑块同时拖动
    • 明确拖动的开始和结束
  3. 碰撞检测

    • 优先检测滑块(精确控制)
    • 其次检测滑轨(快速跳转)
    • 给滑轨增加垂直方向的容差
  4. 视觉反馈

    • 不同状态使用不同颜色
    • 添加阴影增强立体感
    • 实时显示数值

🐛 调试建议

常见问题

  1. 拖动不流畅

    • 检查每帧是否正确更新值
    • 验证鼠标位置是否准确获取
    • 确认 draggedSlider 状态正确维护
  2. 值跳动

    • 检查归一化计算是否正确
    • 验证范围限制逻辑
    • 确认滑轨宽度计算准确
  3. 点击无响应

    • 检查碰撞检测区域是否足够大
    • 验证鼠标按下/释放状态
    • 确认 wasMousePressed 正确更新
  4. 多个滑块同时响应

    • 检查 draggedSlider 是否正确管理
    • 确认只有一个滑块被标记为拖动中
    • 验证 break 语句正确跳出循环

📈 版本历史

  • v1.0 (2025-10-12) - 初始版本
    • 实现基础滑动条功能
    • 支持 4 个独立滑块
    • 添加拖动和点击交互
    • 实现数值映射和显示
    • 添加实时预览功能

📞 技术支持

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

🎨 设计灵感

现代 UI 设计中的滑动条通常具有:

  • 扁平化:简洁的视觉风格
  • 响应式:明确的交互反馈
  • 可访问性:足够大的点击区域
  • 一致性:与其他组件风格统一

本组件实现了这些设计原则,可作为实际应用的参考。


Happy Coding! 🎉