Qt自定义控件之SeekBar

在客户端软件开发中,SeekBar(拖动条)是一种常见的控件,经常用于播放器,控制面板等窗口中。Qt默认提供了QSlider来提供相应的功能,然而QSlider和我们常见(如安卓中的SeekBar)的SeekBar相比,有几处不同

  1. 无法点击跳转,QSlider默认行为是点击后向点击处移动特定长度
  2. 滑块中心无法移动到两端端点
  3. 存在信号冗余(调用setValue方法后会发出valueChanged信号,往往会带来问题)

为了解决以上问题,我们需要继承QSlider,实现自己的SeekBar类。实现效果如下
SeekBar

使用QSS进行修饰美化

QSlider是一个QSS比较复杂的控件,其Subcontrol较多,具体可参考QSS Subcontrol#QSlider。简单来说,QSlider下包括一个滑槽(groove),groove中又包括已滑过区域(sub-page),未滑过区域(add-page)和滑块(handle)三个部分。下文中sub-pageadd-page将统称为进度条。SeekBar的QSS如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
QSlider {
height: %1px; /* SeekBar的高度 */
background: transparent;
}

QSlider::groove:horizontal {
background: transparent;
height: %1px; /* 滑槽与控件高度一致 */
}

QSlider::sub-page:horizontal {
height: %2px; /* 已滑过区域的高度,这里设置的是SeekBar高度的1/5 */
background-color: #0094FF; /* 蓝色 */
margin: %3px 0px %3px 0px; /* 将上下多余空白用margin填充,否则会被拉伸导致height属性无效 */
border-radius: %4px; /* 划过区域高度的一半,实现圆角效果 */
margin-left:%5px; /* 左侧留白1/2 滑块宽度,实现滑块在左端时中心位于进度条左端点 */
}

QSlider::add-page:horizontal {
height: %2px; /* 未滑过区域的高度,这里设置的是SeekBar高度的1/5 */
background-color: #E6FFFFFF;
margin: %3px 0px %3px 0px;
border-radius: %4px;
margin-left:%5px; /* 右侧留白1/2 滑块宽度,实现滑块在右端时中心位于进度条右端点 */
}

QSlider::handle:horizontal {
height: %1px; /* 滑块宽度与控件高度一致 */
width: %1px; /* 滑块为圆形 */
background-color: #FFFFFF; /* 未按下时显示为白色 */
border-radius: %5px; /* 高度的一半,实现圆形效果 */
}

QSlider::handle:pressed:horizontal {
background-color: #0094FF; /* 按下时为蓝色 */
}

为了提高灵活性,我们将QSS中的具体数值暂时使用QString的占位符代替,以便在加载中再根据SeekBar的高度指定,也方便在运行期间SeekBar的大小改变时进行调整。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void SeekBar::LoadStyleSheet() {
QFile file(":/seekbar.qss");
if (file.open(QFile::ReadOnly)) {
QString style_sheet = QString::fromLatin1(file.readAll())
.arg(height())
.arg(height() * 0.2)
.arg(height() * 0.8 / 2)
.arg(height() * 0.1)
.arg(height() / 2);
setStyleSheet(style_sheet);
file.close();
}
}

void SeekBar::resizeEvent(QResizeEvent *event) {
LoadStyleSheet();
QWidget::resizeEvent(event);
}

屏蔽setValue()时的信号

由于在大多数情况下,QSlider只是作为一个进度指示的控件,我们往往会监听QSlider::valueChanged信号做出相应的跳转动作,也会在进度改变时使用QSlider::setValue()设置值,这就导致我们可能会在设置值后收到valueChanged信号并在做出相应处理后又调用setValue,从而导致死循环。如在一个播放器中,我们可能会监听QSlider::valueChanged信号,在用户手动调整进度时调用QMediaPlayer::setPosition调整播放进度,而且会在QMediaPlayer发出positionChanged信号后调用QSlider::setValue。一旦这样,就会发生以下循环:
死循环
为了避免这个死循环,我们在调用QSlider::setValue时需要屏蔽QSlider::valueChanged信号。因此我们重写该方法。

1
2
3
4
5
void SeekBar::setValue(int value) {
blockSignals(true);
QSlider::setValue(value);
blockSignals(false);
}

实现点击跳转

该效果的实现方法在网上有很多,如QSlider mouse direct jump,然而如果直接使用回答中的方法,会发现存在一些问题。

  1. 由于我们在QSS中为进度条左右各设置了宽度为1/2滑块高度的margin,进度条的的左端点并非控件的最左端,进度条的宽度也并非控件的宽度,导致当我们点击进度条左右端点时,预期应该设置为最小/最大值,然而实际上却并非如此。因此,我们在通过位置计算比例确定值时,需要进行一些调整。
  2. 滑块拖动释放后也会触发,触发了两次valueChanged信号,在存在误差的情况下会导致细微的抖动

经过修正后的代码如下

1
2
3
4
5
6
7
8
9
void SeekBar::mouseReleaseEvent(QMouseEvent *event) {
if (isSliderDown() && orientation() == Qt::Horizontal) { // 判断滑块是否被按下
// 此处由于左右margin被设置为滑块半径,即控件高度一半,因此使用height()数值进行调整,需根据实际设置调整
QSlider::setValue(minimum() + ((maximum() - minimum()) *
(event->x() - height() / 2)) /
(width() - height()));
}
QSlider::mouseReleaseEvent(event);
}

总结

经过以上调整后,SeekBar已基本满足日常使用,用户可根据具体设计再做调整。完整代码