summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLukas Kosinski <lukasz@scythe-studio.com>2021-04-15 18:00:53 +0200
committerLukas Kosinski <lukasz@scythe-studio.com>2021-05-07 15:42:44 +0200
commit8ce2fd52e0d5ea8189c193db0138488bafedbd10 (patch)
treeb65dd85c09ee226f8826f5bf8e6c6d21be41b0be
parent512078eea7e96c0a2cc25db46078fc1dc66f3d8f (diff)
QXYSeries: Add light marker
Now you can setLightMarker(QImage) on any QXYSeries. Which will result in that image being painted on all points of the series. Setting to a null/empty-image (also default value) will disable this again. Mouse events supported added as well. Task-number: QTBUG-92884 Change-Id: Ic28e67e74b190fdb249cdc3ac48af59d0cc29bd6 Reviewed-by: Miikka Heikkinen <miikka.heikkinen@qt.io>
-rw-r--r--src/charts/linechart/linechartitem.cpp73
-rw-r--r--src/charts/qabstractseries.cpp4
-rw-r--r--src/charts/scatterchart/scatterchartitem.cpp13
-rw-r--r--src/charts/splinechart/splinechartitem.cpp69
-rw-r--r--src/charts/xychart/qxyseries.cpp63
-rw-r--r--src/charts/xychart/qxyseries.h5
-rw-r--r--src/charts/xychart/qxyseries_p.h1
-rw-r--r--src/charts/xychart/xychart.cpp29
-rw-r--r--src/charts/xychart/xychart_p.h2
9 files changed, 244 insertions, 15 deletions
diff --git a/src/charts/linechart/linechartitem.cpp b/src/charts/linechart/linechartitem.cpp
index f2b30df2..8a794508 100644
--- a/src/charts/linechart/linechartitem.cpp
+++ b/src/charts/linechart/linechartitem.cpp
@@ -309,6 +309,22 @@ void LineChartItem::updateGeometry()
QPainterPath checkShapePath = stroker.createStroke(fullPath);
+ // For mouse interactivity, we have to add the rects *after* the 'createStroke',
+ // as we don't need the outline - we need it filled up.
+ if (!m_series->lightMarker().isNull()) {
+ const QImage &marker = m_series->lightMarker();
+ // '+1': a margin to guarantee we cover all of the pixmap
+ int markerHalfWidth = (marker.width() / 2) + 1;
+ int markerHalfHeight = (marker.height() / 2) + 1;
+
+ // '+2': see above comment about margin
+ for (const auto &point : qAsConst(m_linePoints)) {
+ checkShapePath.addRect(point.x() - markerHalfWidth,
+ point.y() - markerHalfHeight,
+ marker.width() + 2, marker.height() + 2);
+ }
+ }
+
// Only zoom in if the bounding rects of the paths fit inside int limits. QWidget::update() uses
// a region that has to be compatible with QRect.
if (checkShapePath.boundingRect().height() <= INT_MAX
@@ -409,12 +425,28 @@ void LineChartItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *opt
painter->drawLine(m_linePoints.at(i - 1), m_linePoints.at(i));
}
+ int pointLabelsOffset = m_linePen.width() / 2;
+
+ // Draw markers if a marker has been set (set to QImage() to disable)
+ if (!m_series->lightMarker().isNull()) {
+ const QImage &marker = m_series->lightMarker();
+ int markerHalfWidth = marker.width() / 2;
+ int markerHalfHeight = marker.height() / 2;
+ pointLabelsOffset = markerHalfHeight;
+
+ for (const auto &point : qAsConst(m_linePoints)) {
+ painter->drawImage(point.x() - markerHalfWidth,
+ point.y() - markerHalfHeight,
+ marker);
+ }
+ }
+
if (m_pointLabelsVisible) {
if (m_pointLabelsClipping)
painter->setClipping(true);
else
painter->setClipping(false);
- m_series->d_func()->drawSeriesPointLabels(painter, m_linePoints, m_linePen.width() / 2);
+ m_series->d_func()->drawSeriesPointLabels(painter, m_linePoints, pointLabelsOffset);
}
painter->restore();
@@ -443,7 +475,12 @@ void LineChartItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *opt
void LineChartItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
- emit XYChart::pressed(domain()->calculateDomainPoint(event->pos()));
+ QPointF matchedP = matchForLightMarker(event->pos());
+ if (!qIsNaN(matchedP.x()))
+ emit XYChart::pressed(matchedP);
+ else
+ emit XYChart::pressed(domain()->calculateDomainPoint(event->pos()));
+
m_lastMousePos = event->pos();
m_mousePressed = true;
QGraphicsItem::mousePressEvent(event);
@@ -451,30 +488,52 @@ void LineChartItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
void LineChartItem::hoverEnterEvent(QGraphicsSceneHoverEvent *event)
{
- emit XYChart::hovered(domain()->calculateDomainPoint(event->pos()), true);
+ QPointF matchedP = matchForLightMarker(event->pos());
+ if (!qIsNaN(matchedP.x()))
+ emit XYChart::hovered(matchedP, true);
+ else
+ emit XYChart::hovered(domain()->calculateDomainPoint(event->pos()), true);
+
// event->accept();
QGraphicsItem::hoverEnterEvent(event);
}
void LineChartItem::hoverLeaveEvent(QGraphicsSceneHoverEvent *event)
{
- emit XYChart::hovered(domain()->calculateDomainPoint(event->pos()), false);
+ QPointF matchedP = matchForLightMarker(event->pos());
+ if (!qIsNaN(matchedP.x()))
+ emit XYChart::hovered(matchedP, false);
+ else
+ emit XYChart::hovered(domain()->calculateDomainPoint(event->pos()), false);
+
// event->accept();
QGraphicsItem::hoverEnterEvent(event);
}
void LineChartItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
{
- emit XYChart::released(domain()->calculateDomainPoint(m_lastMousePos));
+ QPointF result;
+ QPointF matchedP = matchForLightMarker(m_lastMousePos);
+ if (!qIsNaN(matchedP.x()))
+ result = matchedP;
+ else
+ result = domain()->calculateDomainPoint(m_lastMousePos);
+
+ emit XYChart::released(result);
if (m_mousePressed)
- emit XYChart::clicked(domain()->calculateDomainPoint(m_lastMousePos));
+ emit XYChart::clicked(result);
m_mousePressed = false;
QGraphicsItem::mouseReleaseEvent(event);
}
void LineChartItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
{
- emit XYChart::doubleClicked(domain()->calculateDomainPoint(m_lastMousePos));
+ QPointF matchedP = matchForLightMarker(event->pos());
+ if (!qIsNaN(matchedP.x()))
+ emit XYChart::doubleClicked(matchedP);
+ else
+ emit XYChart::doubleClicked(domain()->calculateDomainPoint(m_lastMousePos));
+
QGraphicsItem::mouseDoubleClickEvent(event);
}
diff --git a/src/charts/qabstractseries.cpp b/src/charts/qabstractseries.cpp
index 0d193a3c..c3d5cd7d 100644
--- a/src/charts/qabstractseries.cpp
+++ b/src/charts/qabstractseries.cpp
@@ -181,7 +181,7 @@ QT_BEGIN_NAMESPACE
\list
\li Series animations are not supported for accelerated series.
\li Point labels are not supported for accelerated series.
- \li Pen styles and marker shapes are ignored for accelerated series.
+ \li Pen styles, marker shapes and light markers are ignored for accelerated series.
Only solid lines and plain scatter dots are supported.
The scatter dots may be circular or rectangular, depending on the underlying graphics
hardware and drivers.
@@ -234,7 +234,7 @@ QT_BEGIN_NAMESPACE
\list
\li Series animations are not supported for accelerated series.
\li Point labels are not supported for accelerated series.
- \li Pen styles and marker shapes are ignored for accelerated series.
+ \li Pen styles, marker shapes and light markers are ignored for accelerated series.
Only solid lines and plain scatter dots are supported.
The scatter dots may be circular or rectangular, depending on the underlying graphics
hardware and drivers.
diff --git a/src/charts/scatterchart/scatterchartitem.cpp b/src/charts/scatterchart/scatterchartitem.cpp
index 0f81ee54..de66bf02 100644
--- a/src/charts/scatterchart/scatterchartitem.cpp
+++ b/src/charts/scatterchart/scatterchartitem.cpp
@@ -238,6 +238,19 @@ void ScatterChartItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *
if (m_series->useOpenGL())
return;
+ // Draw markers if a marker has been set (set to QImage() to disable)
+ if (!m_series->lightMarker().isNull()) {
+ const QImage &marker = m_series->lightMarker();
+ int markerHalfWidth = marker.width() / 2;
+ int markerHalfHeight = marker.height() / 2;
+
+ for (const auto &point : qAsConst(m_points)) {
+ painter->drawImage(point.x() - markerHalfWidth,
+ point.y() - markerHalfHeight,
+ marker);
+ }
+ }
+
QRectF clipRect = QRectF(QPointF(0, 0), domain()->size());
painter->save();
diff --git a/src/charts/splinechart/splinechartitem.cpp b/src/charts/splinechart/splinechartitem.cpp
index dc77197c..db70a42c 100644
--- a/src/charts/splinechart/splinechartitem.cpp
+++ b/src/charts/splinechart/splinechartitem.cpp
@@ -304,6 +304,23 @@ void SplineChartItem::updateGeometry()
// Only zoom in if the bounding rects of the path fit inside int limits. QWidget::update() uses
// a region that has to be compatible with QRect.
QPainterPath checkShapePath = stroker.createStroke(fullPath);
+
+ // For mouse interactivity, we have to add the rects *after* the 'createStroke',
+ // as we don't need the outline - we need it filled up.
+ if (!m_series->lightMarker().isNull()) {
+ const QImage &marker = m_series->lightMarker();
+ // '+1': a margin to guarantee we cover all of the pixmap
+ int markerHalfWidth = (marker.width() / 2) + 1;
+ int markerHalfHeight = (marker.height() / 2) + 1;
+
+ // '+2': see above comment about margin
+ for (const auto &point : qAsConst(points)) {
+ checkShapePath.addRect(point.x() - markerHalfWidth,
+ point.y() - markerHalfHeight,
+ marker.width() + 2, marker.height() + 2);
+ }
+ }
+
if (checkShapePath.boundingRect().height() <= INT_MAX
&& checkShapePath.boundingRect().width() <= INT_MAX
&& splinePath.boundingRect().height() <= INT_MAX
@@ -475,6 +492,19 @@ void SplineChartItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *o
painter->drawPoints(geometryPoints());
}
+ // Draw markers if a marker has been set (set to QImage() to disable)
+ if (!m_series->lightMarker().isNull()) {
+ const QImage &marker = m_series->lightMarker();
+ int markerHalfWidth = marker.width() / 2;
+ int markerHalfHeight = marker.height() / 2;
+
+ for (const auto &point : qAsConst(m_points)) {
+ painter->drawImage(point.x() - markerHalfWidth,
+ point.y() - markerHalfHeight,
+ marker);
+ }
+ }
+
if (m_pointLabelsVisible) {
if (m_pointLabelsClipping)
painter->setClipping(true);
@@ -488,7 +518,12 @@ void SplineChartItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *o
void SplineChartItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
- emit XYChart::pressed(domain()->calculateDomainPoint(event->pos()));
+ QPointF matchedP = matchForLightMarker(event->pos());
+ if (!qIsNaN(matchedP.x()))
+ emit XYChart::pressed(matchedP);
+ else
+ emit XYChart::pressed(domain()->calculateDomainPoint(event->pos()));
+
m_lastMousePos = event->pos();
m_mousePressed = true;
QGraphicsItem::mousePressEvent(event);
@@ -496,28 +531,50 @@ void SplineChartItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
void SplineChartItem::hoverEnterEvent(QGraphicsSceneHoverEvent *event)
{
- emit XYChart::hovered(domain()->calculateDomainPoint(event->pos()), true);
+ QPointF matchedP = matchForLightMarker(event->pos());
+ if (!qIsNaN(matchedP.x()))
+ emit XYChart::hovered(matchedP, true);
+ else
+ emit XYChart::hovered(domain()->calculateDomainPoint(event->pos()), true);
+
QGraphicsItem::hoverEnterEvent(event);
}
void SplineChartItem::hoverLeaveEvent(QGraphicsSceneHoverEvent *event)
{
- emit XYChart::hovered(domain()->calculateDomainPoint(event->pos()), false);
+ QPointF matchedP = matchForLightMarker(event->pos());
+ if (!qIsNaN(matchedP.x()))
+ emit XYChart::hovered(matchedP, false);
+ else
+ emit XYChart::hovered(domain()->calculateDomainPoint(event->pos()), false);
+
QGraphicsItem::hoverLeaveEvent(event);
}
void SplineChartItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
{
- emit XYChart::released(domain()->calculateDomainPoint(m_lastMousePos));
+ QPointF result;
+ QPointF matchedP = matchForLightMarker(m_lastMousePos);
+ if (!qIsNaN(matchedP.x()))
+ result = matchedP;
+ else
+ result = domain()->calculateDomainPoint(m_lastMousePos);
+
+ emit XYChart::released(result);
if (m_mousePressed)
- emit XYChart::clicked(domain()->calculateDomainPoint(m_lastMousePos));
+ emit XYChart::clicked(result);
m_mousePressed = false;
QGraphicsItem::mouseReleaseEvent(event);
}
void SplineChartItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
{
- emit XYChart::doubleClicked(domain()->calculateDomainPoint(m_lastMousePos));
+ QPointF matchedP = matchForLightMarker(event->pos());
+ if (!qIsNaN(matchedP.x()))
+ emit XYChart::doubleClicked(matchedP);
+ else
+ emit XYChart::doubleClicked(domain()->calculateDomainPoint(m_lastMousePos));
+
QGraphicsItem::mouseDoubleClickEvent(event);
}
diff --git a/src/charts/xychart/qxyseries.cpp b/src/charts/xychart/qxyseries.cpp
index ca93f07a..7ea24d86 100644
--- a/src/charts/xychart/qxyseries.cpp
+++ b/src/charts/xychart/qxyseries.cpp
@@ -459,6 +459,13 @@ QT_BEGIN_NAMESPACE
*/
/*!
+ \fn void QXYSeries::lightMarkerChanged(const QImage &lightMarker)
+ This signal is emitted when the light marker image changes to \a lightMarker.
+ \sa QXYSeries::setLightMarker();
+ \since 6.2
+*/
+
+/*!
\fn void QXYSeriesPrivate::updated()
\internal
*/
@@ -1092,6 +1099,62 @@ bool QXYSeries::pointLabelsClipping() const
}
/*!
+ Sets the image used for drawing markers on each point of the series.
+
+ The default value is a default-QImage() (QImage::isNull() == true), meaning no light marker
+ will be painted.
+ You can reset back to default (disabled) by calling this function with a null QImage (QImage()).
+
+ The light markers visualize the data points of this series and as such are an alternative
+ to setPointsVisible(true).
+ Both features can be enabled independently from each other.
+
+ Unlike the elements of \l {QScatterSeries}{QScatterSeries} the light markers
+ are not represented by QGraphicsItem, but are just painted (no objects created).
+ However, the mouse-event-signals of QXYSeries behave the same way,
+ meaning that you'll get the exact domain value of the point if you click/press/hover
+ the light marker. You'll still get the in between domain value if you click on the line.
+ The light markers are above the line in terms of painting as well as events.
+
+ \sa QXYSeries::lightMarker()
+ \since 6.2
+*/
+void QXYSeries::setLightMarker(const QImage &lightMarker)
+{
+ Q_D(QXYSeries);
+ if (d->m_lightMarker == lightMarker)
+ return;
+
+ d->m_lightMarker = lightMarker;
+ emit lightMarkerChanged(d->m_lightMarker);
+}
+
+/*!
+ Gets the image used for drawing markers on each point of the series.
+
+ The default value is QImage(), meaning no light marker will be painted.
+
+ The light markers visualize the data points of this series and as such are an alternative
+ to setPointsVisible(true).
+ Both features can be enabled independently from each other.
+
+ Unlike the elements of \l {QScatterSeries}{QScatterSeries} the light markers
+ are not represented by QGraphicsItem, but are just painted (no objects created).
+ However, the mouse-event-signals of QXYSeries behave the same way,
+ meaning that you'll get the exact domain value of the point if you click/press/hover
+ the light marker. You'll still get the in between domain value if you click on the line.
+ The light markers are above the line in terms of painting as well as events.
+ \sa QXYSeries::setLightMarker()
+ \since 6.2
+*/
+const QImage &QXYSeries::lightMarker() const
+{
+ Q_D(const QXYSeries);
+ return d->m_lightMarker;
+}
+
+
+/*!
Stream operator for adding the data point \a point to the series.
\sa append()
*/
diff --git a/src/charts/xychart/qxyseries.h b/src/charts/xychart/qxyseries.h
index 43d96e9e..45c83590 100644
--- a/src/charts/xychart/qxyseries.h
+++ b/src/charts/xychart/qxyseries.h
@@ -34,6 +34,7 @@
#include <QtCharts/QAbstractSeries>
#include <QtGui/QPen>
#include <QtGui/QBrush>
+#include <QtGui/QImage>
QT_BEGIN_NAMESPACE
class QModelIndex;
@@ -129,6 +130,9 @@ public:
void toggleSelection(const QList<int> &indexes);
QList<int> selectedPoints() const;
+ void setLightMarker(const QImage &lightMarker);
+ const QImage &lightMarker() const;
+
Q_SIGNALS:
void clicked(const QPointF &point);
void hovered(const QPointF &point, bool state);
@@ -149,6 +153,7 @@ Q_SIGNALS:
void pointsRemoved(int index, int count);
void penChanged(const QPen &pen);
void selectedPointsChanged();
+ void lightMarkerChanged(const QImage &lightMarker);
private:
Q_DECLARE_PRIVATE(QXYSeries)
diff --git a/src/charts/xychart/qxyseries_p.h b/src/charts/xychart/qxyseries_p.h
index f9518c07..53498e3c 100644
--- a/src/charts/xychart/qxyseries_p.h
+++ b/src/charts/xychart/qxyseries_p.h
@@ -85,6 +85,7 @@ protected:
QFont m_pointLabelsFont;
QColor m_pointLabelsColor;
bool m_pointLabelsClipping;
+ QImage m_lightMarker;
private:
Q_DECLARE_PUBLIC(QXYSeries)
diff --git a/src/charts/xychart/xychart.cpp b/src/charts/xychart/xychart.cpp
index 2348e848..79c8c40c 100644
--- a/src/charts/xychart/xychart.cpp
+++ b/src/charts/xychart/xychart.cpp
@@ -251,6 +251,35 @@ bool XYChart::isEmpty()
return domain()->isEmpty() || m_series->points().isEmpty();
}
+QPointF XYChart::matchForLightMarker(const QPointF &eventPos)
+{
+ if (m_series->lightMarker().isNull())
+ return QPointF(qQNaN(), qQNaN()); // 0,0 could actually be in points()
+
+ int markerWidth = m_series->lightMarker().width();
+ int markerHeight = m_series->lightMarker().height();
+
+ for (const QPointF &dp : m_series->points()) {
+ bool ok;
+ const QPointF gp = domain()->calculateGeometryPoint(dp, ok);
+ if (ok) {
+ // '+2' and '+4': There is an addRect for the (mouse-)shape
+ // in LineChartItem::updateGeometry()
+ // This has a margin of 1 to make sure a press in the icon will always be detected,
+ // but as there is a bunch of 'translations' and therefore inaccuracies,
+ // so it is necessary to increase that margin to 2
+ // (otherwise you can click next to an icon, get a click event but not match it)
+ QRectF r(gp.x() - (markerWidth / 2 + 2),
+ gp.y() - (markerHeight / 2 + 2),
+ markerWidth + 4, markerHeight + 4);
+
+ if (r.contains(eventPos))
+ return dp;
+ }
+ }
+ return QPointF(qQNaN(), qQNaN()); // 0,0 could actually be in points()
+}
+
QT_END_NAMESPACE
#include "moc_xychart_p.cpp"
diff --git a/src/charts/xychart/xychart_p.h b/src/charts/xychart/xychart_p.h
index 66849218..007e2537 100644
--- a/src/charts/xychart/xychart_p.h
+++ b/src/charts/xychart/xychart_p.h
@@ -92,6 +92,8 @@ protected:
virtual void updateGlChart();
virtual void refreshGlChart();
+ QPointF matchForLightMarker(const QPointF &eventPos);
+
private:
inline bool isEmpty();