前两篇讲了 SpinBox 的基础用法和自定义样式,这篇加点手感:按钮按下去有缩放反馈,数值变化时数字弹一下;或者干脆把输入区换成列表,数字像滚轮一样滚过去。改动不大,交互质感提升不少。
- 缩放动画 --- 加减号按下缩放,数值变化时弹出再回弹,附带阴影
- 滚动列表 --- 用
ListView替换默认输入区,数值滚动切换
Demo 1:缩放动画
在 Basic 自定义的基础上加了三层效果:按钮按下缩放、数值变化时文本弹出、整体阴影。

演示代码
qml
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Basic
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
FadeInAnimation {
ColumnLayout {
anchors.fill: parent
anchors.margins: 20
spacing: 15
// ... 省略标题组件 TitleSeparator ...
RowLayout {
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: 20
spacing: 10
SpinBox {
id: control
editable: true
from: 0
to: 100
background: Rectangle {
implicitWidth: 140
implicitHeight: 40
radius: height / 2
color: control.enabled ? (control.up.pressed || control.down.pressed ? "#eee" : "#fff") : "#f0f0f0"
border.color: control.focus ? "#1296ff" : "#ccc"
border.width: control.focus ? 2 : 1
// 阴影效果
layer.enabled: true
layer.effect: DropShadow {
transparentBorder: true
color: "#20000000"
radius: 4
samples: 8
}
}
up.indicator: Rectangle {
x: parent.width - width
implicitWidth: 40
implicitHeight: 40
radius: height / 2
color: control.up.pressed ? "#eee" : "transparent"
// 使用 Rectangle 实现加号
Rectangle {
x: (parent.width - width) / 2
y: (parent.height - height) / 2
width: parent.width / 3
height: 2
color: enabled ? "#666" : "#999"
}
Rectangle {
x: (parent.width - width) / 2
y: (parent.height - height) / 2
width: 2
height: parent.width / 3
color: enabled ? "#666" : "#999"
}
// 点击时的缩放动画
scale: control.up.pressed ? 0.9 : 1.0
Behavior on scale { NumberAnimation { duration: 100 } }
}
down.indicator: Rectangle {
x: 0
implicitWidth: 40
implicitHeight: 40
radius: height / 2
color: control.down.pressed ? "#eee" : "transparent"
// 使用 Rectangle 实现减号
Rectangle {
x: (parent.width - width) / 2
y: (parent.height - height) / 2
width: parent.width / 3
height: 2
color: enabled ? "#666" : "#999"
}
// 点击时的缩放动画
scale: control.down.pressed ? 0.9 : 1.0
Behavior on scale { NumberAnimation { duration: 100 } }
}
contentItem: TextInput {
id: textInput
text: control.textFromValue(control.value, control.locale)
font.pointSize: 11
color: "#333"
selectionColor: "#1296ff"
selectedTextColor: "#fff"
horizontalAlignment: Qt.AlignHCenter
verticalAlignment: Qt.AlignVCenter
readOnly: !control.editable
validator: control.validator
inputMethodHints: Qt.ImhFormattedNumbersOnly
// 数值改变时的动画:弹出并改变透明度
Behavior on text {
SequentialAnimation {
NumberAnimation { target: textInput; property: "scale"; from: 1.0; to: 1.2; duration: 50; easing.type: Easing.OutQuad }
NumberAnimation { target: textInput; property: "scale"; from: 1.2; to: 1.0; duration: 150; easing.type: Easing.OutBack }
}
}
}
}
}
Text {
Layout.fillWidth: true
color: "#999"
font.pointSize: 10
text: "提示:加减号按下时缩放至 0.9 倍,数值变化时弹出放大再回弹"
}
Item { Layout.fillHeight: true }
}
}
关键逻辑解析
这个 demo 里其实藏着三层效果:
1. 按钮按下缩放
qml
scale: control.up.pressed ? 0.9 : 1.0
Behavior on scale { NumberAnimation { duration: 100 } }
up.pressed 在按钮按下时为 true,松开恢复 false,scale 跟着在 0.9 和 1.0 之间切换。关键是 Behavior on scale:它给 scale 加了一层过渡,数值一变就播放一小段动画,而不是瞬间跳变。没有它,按钮是"啪"地一下变小;有了它,才是"弹"地一下缩回去。
2. 数值变化弹出
qml
Behavior on text {
SequentialAnimation {
NumberAnimation { target: textInput; property: "scale"; from: 1.0; to: 1.2; duration: 50; easing.type: Easing.OutQuad }
NumberAnimation { target: textInput; property: "scale"; from: 1.2; to: 1.0; duration: 150; easing.type: Easing.OutBack }
}
}
Behavior on text 在输入框文字变化时触发,也就是数值一变就动。SequentialAnimation 把两个动画串起来:先快速放大,再回弹到原样。回弹用的是 OutBack 缓动,它会先冲过头一点再落回来,就是这一下过冲做出了弹性感。放大多大、弹多快,都按自己的手感调。
3. 阴影
DropShadow 从 Qt5Compat.GraphicalEffects 里来,给背景加一层淡淡的投影,SpinBox 就像浮在界面上。transparentBorder: true 别忘了,不然阴影会被边缘裁掉。
Demo 2:滚动列表
数值跳变太生硬?这个 demo 把 SpinBox 的输入区换成了 ListView,点按钮时数字不是直接换,而是像滚轮一样滑到下一个。

演示代码
qml
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Basic
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
FadeInAnimation {
ColumnLayout {
anchors.fill: parent
anchors.margins: 20
spacing: 15
// ... 省略标题组件 TitleSeparator ...
RowLayout {
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: 20
spacing: 10
SpinBox {
id: control
editable: false
from: 0
to: 100
background: Rectangle {
implicitWidth: 140
implicitHeight: 40
radius: height / 2
color: control.enabled ? "#fff" : "#f0f0f0"
border.color: control.focus ? "#1296ff" : "#bdbdbd"
border.width: control.focus ? 2 : 1
// 阴影效果
layer.enabled: true
layer.effect: DropShadow {
transparentBorder: true
color: "#20000000"
radius: 4
samples: 8
}
}
up.indicator: Rectangle {
x: parent.width - width
height: parent.height
implicitWidth: 40
implicitHeight: 40
radius: height / 2
color: control.up.pressed ? "#eee" : "transparent"
// 使用 Rectangle 实现加号
Rectangle {
x: (parent.width - width) / 2
y: (parent.height - height) / 2
width: parent.width / 3
height: 2
color: enabled ? "#666" : "#999"
}
Rectangle {
x: (parent.width - width) / 2
y: (parent.height - height) / 2
width: 2
height: parent.width / 3
color: enabled ? "#666" : "#999"
}
}
down.indicator: Rectangle {
x: 0
height: parent.height
implicitWidth: 40
implicitHeight: 40
radius: height / 2
color: control.down.pressed ? "#eee" : "transparent"
// 使用 Rectangle 实现减号
Rectangle {
x: (parent.width - width) / 2
y: (parent.height - height) / 2
width: parent.width / 3
height: 2
color: enabled ? "#666" : "#999"
}
}
// 核心滚动区域
contentItem: ListView {
id: rollingList
clip: true
model: control.to - control.from + 1
currentIndex: control.value - control.from
// 禁止手动拖拽,只通过按钮控制
interactive: false
// 设置滚动动画速度和曲线
highlightMoveDuration: 300
highlightMoveVelocity: -1 // 强制使用 duration
preferredHighlightBegin: 0
preferredHighlightEnd: height
highlightRangeMode: ListView.ApplyRange
delegate: Item {
width: rollingList.width
height: rollingList.height
Text {
text: index + control.from
anchors.centerIn: parent
color: "#333"
font.pointSize: 11
}
}
}
}
}
Text {
Layout.fillWidth: true
color: "#999"
font.pointSize: 10
text: "提示:仅可通过按钮控制滚动,禁用手动拖拽,高亮动画时长 300ms"
}
Item { Layout.fillHeight: true }
}
}
关键逻辑解析
核心思路一句话:把数值变化和列表滚动绑在一起。
qml
contentItem: ListView {
model: control.to - control.from + 1
currentIndex: control.value - control.from
interactive: false
highlightMoveDuration: 300
}
model生成from到to的全部数字(0~100 共 101 个),一个数字一个列表项;currentIndex绑定value - from,值一变 index 跟着变,列表自动滚到对应项;interactive: false禁掉手动拖拽------列表只负责展示,操作权全在按钮;highlightMoveDuration是滚动动画的时长,想快想慢随意调。
有个细节容易踩坑:highlightMoveVelocity: -1。ListView 默认按速度算动画时长,条目越多,同样速度下看起来滚得越快。设成 -1 是强制改按时长算,保证每次滚动节奏一致。
滚动范围用 highlightRangeMode: ListView.ApplyRange 加上 preferredHighlightBegin/End 控制,让当前项始终停在可视区中央,不会滚到一半卡在边缘。
这个 demo 设了 editable: false------ListView 本身不支持编辑。想同时支持滚动和直接输入,得在两种模式之间做切换,复杂度会高不少。
运行验证
- 用 Qt Creator 打开
qml_spinbox/CMakeLists.txt; - 按
Ctrl+R运行; - 左侧导航切换到缩放动画和滚动列表 demo,点加减按钮看效果。
扩展复用方向
- 把缩放的
scale换成rotation,按钮按下就转一下,效果也很俏皮。 - 想更接近 iOS 的滚轮选择器,把
ListView换成PathView,做成弧形滚动。 - 给滚动列表上下加渐变遮罩,让数字"沉"进画面里,滚动感更自然。
已验证环境:
- Qt 版本:Qt 6.8.2 / Qt 6.11.1
- 操作系统:Windows 11
- GitHub:QML-Minimal-Demos/qml_spinbox