状态:✅ 已完成
难度:⭐⭐
代码行数:~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 | 退出程序 |
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", "%"},
// ...
};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;
}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));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, ...);
}为什么使用 0.0-1.0?
- 内部统一使用归一化值(0.0-1.0)
- 便于位置计算:
thumbX = x + width * value - 渲染时才映射到实际范围
优点:
- 代码简洁,易于理解
- 适用于任何范围
- 便于插值和动画
// 全局状态
int draggedSlider = -1; // -1 表示没有拖动
// 按下时记录
if (mouseDown && !wasMouseDown) {
draggedSlider = findClickedSlider();
}
// 拖动中持续更新
if (mouseDown && draggedSlider >= 0) {
updateSlider(draggedSlider);
}
// 释放时清除
if (!mouseDown) {
draggedSlider = -1;
}滑动条需要检测两个区域:
- 滑块(圆形):精确控制
- 滑轨(矩形):快速跳转
// 先检测滑块(优先级高)
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 次渲染调用/帧
-
鼠标拖动
- 如何检测拖动开始和结束
- 拖动中的状态管理
-
数值映射
- 归一化值的概念
- 线性映射公式
-
交互反馈
- 不同状态的视觉反馈
- 实时数值显示
-
状态机设计
- 空闲 → 悬停 → 拖动状态转换
- 全局拖动状态管理
-
碰撞检测优化
- 复合碰撞(圆形 + 矩形)
- 优先级处理
-
数据驱动设计
- 使用数组管理多个滑块
- 统一的渲染和交互逻辑
- 步进值:按固定步长调整(如 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.batcd ..\..\..\..\native
.\build\Release\bitui_native.exe .\build\Release\slider.dllslider/
├── 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 | 兄弟组件 | 简化的二值滑动条 |
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))
-
归一化存储
- 内部使用 0.0-1.0 存储
- 显示时映射到实际范围
-
拖动状态
- 使用全局标记防止多个滑块同时拖动
- 明确拖动的开始和结束
-
碰撞检测
- 优先检测滑块(精确控制)
- 其次检测滑轨(快速跳转)
- 给滑轨增加垂直方向的容差
-
视觉反馈
- 不同状态使用不同颜色
- 添加阴影增强立体感
- 实时显示数值
-
拖动不流畅
- 检查每帧是否正确更新值
- 验证鼠标位置是否准确获取
- 确认
draggedSlider状态正确维护
-
值跳动
- 检查归一化计算是否正确
- 验证范围限制逻辑
- 确认滑轨宽度计算准确
-
点击无响应
- 检查碰撞检测区域是否足够大
- 验证鼠标按下/释放状态
- 确认
wasMousePressed正确更新
-
多个滑块同时响应
- 检查
draggedSlider是否正确管理 - 确认只有一个滑块被标记为拖动中
- 验证
break语句正确跳出循环
- 检查
- v1.0 (2025-10-12) - 初始版本
- 实现基础滑动条功能
- 支持 4 个独立滑块
- 添加拖动和点击交互
- 实现数值映射和显示
- 添加实时预览功能
- 项目主页:
Bit HCI - 文档:
docs/guides/ - 示例:
examples/cpp/
现代 UI 设计中的滑动条通常具有:
- 扁平化:简洁的视觉风格
- 响应式:明确的交互反馈
- 可访问性:足够大的点击区域
- 一致性:与其他组件风格统一
本组件实现了这些设计原则,可作为实际应用的参考。
Happy Coding! 🎉