轴处理
在 QML 中使用自定义输入处理程序实现轴拖动,并创建自定义轴格式化器。
“轴处理”演示了轴的两种不同自定义功能。这些功能在应用程序中各有独立的选项卡。
以下各节仅重点介绍这些功能,将省略对基本功能的说明——如需更详细的 QML 示例文档,请参阅《简单的散点图》。

运行示例
您可以通过以下方式运行示例:
- Qt Creator
打开Welcome 模式,并从Examples 中选择该示例。有关更多信息,请参阅Qt Creator :教程:构建与运行。
- Qt Extension for Visual Studio Code
在Command Palette 中执行Qt: Open Qt examples 命令,并从列表中选择该示例。有关更多信息,请参阅Qt Extension for Visual Studio Code :教程:构建和运行。
轴拖动
在“Axis Dragging ”选项卡中,使用QML实现一个自定义输入处理程序,使您能够通过拖动坐标轴标签来更改坐标轴范围。此外,请使用正交投影并动态更新自定义项的属性。
覆盖默认输入处理
要禁用默认输入处理机制,请将 Scatter3D 图的活跃输入处理程序设置为“null ”:
Scatter3D {
id: scatterGraph
inputHandler: null
...然后,添加一个MouseArea ,并将其设置为填充父容器——即包含我们scatterGraph 的Item 。此外,将其设置为仅接受鼠标左键点击,因为在此示例中不需要其他按键:
MouseArea {
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton
...接着,监听鼠标点击事件,并在捕获到事件时向图发送选择查询:
onPressed: (mouse)=> {
scatterGraph.scene.selectionQueryPosition = Qt.point(mouse.x, mouse.y);
}onPositionChanged 信号处理程序会捕获当前鼠标位置,该位置将用于移动距离的计算:
onPositionChanged: (mouse)=> {
currentMouseX = mouse.x;
currentMouseY = mouse.y;
...在 `onPositionChanged` 结束时,保存上一次鼠标位置,以便用于稍后将介绍的移动距离计算:
...
previousMouseX = currentMouseX;
previousMouseY = currentMouseY;
}将鼠标移动转换为轴范围变化
在scatterGraph 中,监听onSelectedElementChanged 信号。该信号在inputArea 的onPressed 中发出选择查询后触发。将元素类型设置为主组件中你定义的属性(property int selectedAxisLabel: -1 ),因为这是你感兴趣的类型:
onSelectedElementChanged: {
if (selectedElement >= AbstractGraph3D.ElementAxisXLabel
&& selectedElement <= AbstractGraph3D.ElementAxisZLabel) {
selectedAxisLabel = selectedElement;
} else {
selectedAxisLabel = -1;
}
}然后,回到inputArea 的onPositionChanged 中,检查鼠标按钮是否被按下,以及当前是否选择了轴标签。如果满足这些条件,则调用将鼠标移动转换为轴范围更新的函数:
...
if (pressed && selectedAxisLabel != -1)
axisDragView.dragAxis();
...在此情况下,转换非常简单,因为相机旋转角度是固定的。您可以使用一些预先计算好的值,计算鼠标移动距离,并将这些值应用到所选的轴范围中:
function dragAxis() {
// Do nothing if previous mouse position is uninitialized
if (previousMouseX === -1)
return;
// Directional drag multipliers based on rotation. Camera is locked to 45 degrees, so we
// can use one precalculated value instead of calculating xx, xy, zx and zy individually
var cameraMultiplier = 0.70710678;
// Calculate the mouse move amount
var moveX = currentMouseX - previousMouseX;
var moveY = currentMouseY - previousMouseY;
// Adjust axes
switch (selectedAxisLabel) {
case AbstractGraph3D.ElementAxisXLabel:
var distance = ((moveX - moveY) * cameraMultiplier) / dragSpeedModifier;
// Check if we need to change min or max first to avoid invalid ranges
if (distance > 0) {
scatterGraph.axisX.min -= distance;
scatterGraph.axisX.max -= distance;
} else {
scatterGraph.axisX.max -= distance;
scatterGraph.axisX.min -= distance;
}
break;
case AbstractGraph3D.ElementAxisYLabel:
distance = moveY / dragSpeedModifier;
// Check if we need to change min or max first to avoid invalid ranges
if (distance > 0) {
scatterGraph.axisY.max += distance;
scatterGraph.axisY.min += distance;
} else {
scatterGraph.axisY.min += distance;
scatterGraph.axisY.max += distance;
}
break;
case AbstractGraph3D.ElementAxisZLabel:
distance = ((moveX + moveY) * cameraMultiplier) / dragSpeedModifier;
// Check if we need to change min or max first to avoid invalid ranges
if (distance > 0) {
scatterGraph.axisZ.max += distance;
scatterGraph.axisZ.min += distance;
} else {
scatterGraph.axisZ.min += distance;
scatterGraph.axisZ.max += distance;
}
break;
}
}有关将鼠标移动转换为轴范围更新的更复杂方法,请参阅“图形图库”。
其他功能
该示例还演示了如何使用正交投影,以及如何实时更新自定义项的属性。
正交投影非常简单。您只需修改scatterGraph 的orthoProjection 属性即可。示例中提供了一个用于开启和关闭该功能的按钮:
Button {
id: orthoToggle
width: axisDragView.portraitMode ? parent.width : parent.width / 3
text: "Display Orthographic"
anchors.left: axisDragView.portraitMode ? parent.left : rangeToggle.right
anchors.top: axisDragView.portraitMode ? rangeToggle.bottom : parent.top
onClicked: {
if (scatterGraph.orthoProjection) {
text = "Display Orthographic";
scatterGraph.orthoProjection = false;
// Orthographic projection disables shadows, so we need to switch them back on
scatterGraph.shadowQuality = AbstractGraph3D.ShadowQualityMedium
} else {
text = "Display Perspective";
scatterGraph.orthoProjection = true;
}
}
}对于自定义项目,请在scatterGraph 的customItemList 中添加一项:
customItemList: [
Custom3DItem {
id: qtCube
meshFile: ":/qml/qmlaxishandling/cube.obj"
textureFile: ":/qml/qmlaxishandling/cubetexture.png"
position: Qt.vector3d(0.65, 0.35, 0.65)
scaling: Qt.vector3d(0.3, 0.3, 0.3)
}
]您需要实现一个定时器来添加、移除和旋转图表中的所有项目,并使用同一个定时器来旋转自定义项目:
onTriggered: {
rotationAngle = rotationAngle + 1;
qtCube.setRotationAxisAndAngle(Qt.vector3d(1, 0, 1), rotationAngle);
...坐标轴格式器
在“Axis Formatter ”选项卡中,创建一个自定义坐标轴格式化器。该示例还演示了如何使用预定义的坐标轴格式化器。
自定义轴格式化器
要自定义坐标轴格式化器,需要继承 QValue3DAxisFormatter 类,而仅靠 QML 代码无法实现这一点。 在此示例中,坐标轴将浮点值解释为时间戳,并在坐标轴标签中显示日期。为实现此功能,引入了一个名为 `CustomFormatter` 的新类,该类继承自 `QValue3DAxisFormatter`:
class CustomFormatter : public QValue3DAxisFormatter
{
...由于数据宽度不同,QScatter3DSeries 的 float 值无法直接转换为QDateTime 的值,因此需要在两者之间建立某种映射关系。要实现这种映射,需为格式器指定一个起始日期,并将 QScatter3DSeries 中的 float 值解释为相对于该起始值的日期偏移量。 起始日期通过以下属性指定:
Q_PROPERTY(QDate originDate READ originDate WRITE setOriginDate NOTIFY originDateChanged)对于从值到QDateTime 的映射,请使用valueToDateTime() 方法:
QDateTime CustomFormatter::valueToDateTime(qreal value) const
{
return m_originDate.startOfDay().addMSecs(qint64(oneDayMs * value));
}要作为轴格式化器工作,CustomFormatter 需要重写一些虚方法:
virtual QValue3DAxisFormatter *createNewInstance() const;
virtual void populateCopy(QValue3DAxisFormatter ©) const;
virtual void recalculate();
virtual QString stringForValue(qreal value, const QString &format) const;前两个方法很简单,只需创建一个CustomFormatter 的新实例,并将必要数据复制到其中。使用这两个方法来创建和更新用于渲染的格式化器缓存。请记住调用populateCopy() 的超类实现:
QValue3DAxisFormatter *CustomFormatter::createNewInstance() const
{
return new CustomFormatter();
}
void CustomFormatter::populateCopy(QValue3DAxisFormatter ©) const
{
QValue3DAxisFormatter::populateCopy(copy);
CustomFormatter *customFormatter = static_cast<CustomFormatter *>(©);
customFormatter->m_originDate = m_originDate;
customFormatter->m_selectionFormat = m_selectionFormat;
}CustomFormatter 其主要工作在 `recalculate() ` 方法中完成,在此处,我们的格式化器会计算网格、子网格和标签的位置,并格式化标签字符串。在自定义格式化器中,请忽略坐标轴的段数,并始终在午夜位置绘制一条网格线。子段数和标签定位则按常规方式处理:
void CustomFormatter::recalculate()
{
// We want our axis to always have gridlines at date breaks
// Convert range into QDateTimes
QDateTime minTime = valueToDateTime(qreal(axis()->min()));
QDateTime maxTime = valueToDateTime(qreal(axis()->max()));
// Find out the grid counts
QTime midnight(0, 0);
QDateTime minFullDate(minTime.date(), midnight);
int gridCount = 0;
if (minFullDate != minTime)
minFullDate = minFullDate.addDays(1);
QDateTime maxFullDate(maxTime.date(), midnight);
gridCount += minFullDate.daysTo(maxFullDate) + 1;
int subGridCount = axis()->subSegmentCount() - 1;
// Reserve space for position arrays and label strings
gridPositions().resize(gridCount);
subGridPositions().resize((gridCount + 1) * subGridCount);
labelPositions().resize(gridCount);
labelStrings().reserve(gridCount);
// Calculate positions and format labels
qint64 startMs = minTime.toMSecsSinceEpoch();
qint64 endMs = maxTime.toMSecsSinceEpoch();
qreal dateNormalizer = endMs - startMs;
qreal firstLineOffset = (minFullDate.toMSecsSinceEpoch() - startMs) / dateNormalizer;
qreal segmentStep = oneDayMs / dateNormalizer;
qreal subSegmentStep = 0;
if (subGridCount > 0)
subSegmentStep = segmentStep / qreal(subGridCount + 1);
for (int i = 0; i < gridCount; i++) {
qreal gridValue = firstLineOffset + (segmentStep * qreal(i));
gridPositions()[i] = float(gridValue);
labelPositions()[i] = float(gridValue);
labelStrings() << minFullDate.addDays(i).toString(axis()->labelFormat());
}
for (int i = 0; i <= gridCount; i++) {
if (subGridPositions().size()) {
for (int j = 0; j < subGridCount; j++) {
float position;
if (i)
position = gridPositions().at(i - 1) + subSegmentStep * (j + 1);
else
position = gridPositions().at(0) - segmentStep + subSegmentStep * (j + 1);
if (position > 1.0f || position < 0.0f)
position = gridPositions().at(0);
subGridPositions()[i * subGridCount + j] = position;
}
}
}
}轴标签的格式设置仅显示日期。但是,为了提高选择标签时间戳的分辨率,请为自定义格式化器指定另一个属性,以便用户进行自定义:
Q_PROPERTY(QString selectionFormat READ selectionFormat WRITE setSelectionFormat NOTIFY selectionFormatChanged)此选择格式属性用于重新实现的stringToValue 方法中,该方法会忽略提交的格式,并用自定义选择格式替换它:
QString CustomFormatter::stringForValue(qreal value, const QString &format) const
{
Q_UNUSED(format);
return valueToDateTime(value).toString(m_selectionFormat);
}要将我们新的自定义格式化器暴露给 QML,请对其进行声明并将其设为 QML 模块。有关具体操作方法的详细信息,请参阅《Surface Graph 图库》。
QML
在 QML 代码中,为每个维度定义一个不同的坐标轴:
axisZ: valueAxis
axisY: logAxis
axisX: dateAxisZ 轴只需使用普通的 ValueAxis3D:
ValueAxis3D {
id: valueAxis
segmentCount: 5
subSegmentCount: 2
labelFormat: "%.2f"
min: 0
max: 10
}对于 Y 轴,定义一个对数轴。要使 ValueAxis3D 显示对数刻度,请为该轴的 `formatter ` 属性指定 `LogValueAxis3DFormatter`:
ValueAxis3D {
id: logAxis
formatter: LogValueAxis3DFormatter {
id: logAxisFormatter
base: 10
autoSubGrid: true
showEdgeLabels: true
}
labelFormat: "%.2f"
}最后,对于 X 轴,请使用新的CustomFormatter :
ValueAxis3D {
id: dateAxis
formatter: CustomFormatter {
originDate: "2023-01-01"
selectionFormat: "yyyy-MM-dd HH:mm:ss"
}
subSegmentCount: 2
labelFormat: "yyyy-MM-dd"
min: 0
max: 14
}应用程序的其余部分包含一些相当直观的逻辑,用于修改坐标轴和显示图表。
示例内容
© 2026 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.