坐标处理
在 QML 中使用自定义输入处理器实现坐标拖动,并创建自定义坐标格式器。
坐标处理演示了与坐标相关的两种不同的自定义功能。这些功能在应用程序中都有自己独立的选项卡。
以下章节只集中介绍这些功能,省略了基本功能的解释 - 更多详细的 QML 示例文档,请参阅 简单散点图。
运行示例
要从 Qt Creator 运行示例,请打开 欢迎 模式并从 示例 中选择示例。更多信息,请访问 构建和运行示例。
坐标拖动
在 坐标拖动 选项卡中,使用 QML 实现 自定义输入处理器,从而可以通过拖动坐标标签来更改坐标范围。此外,使用正交投影并动态更新自定义项的属性。
覆盖默认输入处理
要禁用默认输入处理机制,将 Scatter3D 图表的 active 输入处理器设置为 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); ...
轴格式化器
在“轴格式化器”选项卡中,创建自定义轴格式化器。它还展示了如何使用预定义的轴格式化器。
自定义轴格式化器
自定义轴格式化器需要继承QValue3DAxisFormatter,这不能仅使用QML代码完成。在此示例中,轴将浮点值解释为时间戳,并在轴标签中显示日期。为此,引入了一个名为CustomFormatter
的新类,它是
class CustomFormatter : public QValue3DAxisFormatter { ...
由于QScatter3DSeries的浮点值无法直接转换为QDateTime值,因为数据宽度不同,需要在这两个值之间进行某种映射。为了进行映射,指定格式化器的起始日期,并将QScatter3DSeries中的浮点值解释为相对于该起始值的日期偏移。起始日期作为属性给出
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模块。有关如何做到这一点的信息,请参阅表面图库。
QML
在QML代码中,为每个维度定义不同的轴
axisZ: valueAxis axisY: logAxis axisX: dateAxis
Z轴只是一个常规的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 }
应用程序的其余部分包含用于修改轴和显示图表的相对直观的逻辑。
示例内容
© 2024 The Qt Company Ltd. 本文档中包含的贡献的版权属于其各自的所有者。本提供的文档是根据自由软件基金会发布的条款,并根据GNU自由文档许可证版本1.3许可的。Qt及其相关标志是The Qt Company Ltd.在芬兰及/或在其他国家/地区的商标。所有其他商标均属于其各自的所有者。