| 1 | // Copyright (C) 2017 The Qt Company Ltd. |
| 2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only |
| 3 | |
| 4 | #include <QtCharts/QChartView> |
| 5 | #include <private/qchartview_p.h> |
| 6 | #include <private/qchart_p.h> |
| 7 | #include <QtWidgets/QGraphicsScene> |
| 8 | #include <QtWidgets/QRubberBand> |
| 9 | #include <QtWidgets/QGraphicsItem> |
| 10 | |
| 11 | /*! |
| 12 | \enum QChartView::RubberBand |
| 13 | |
| 14 | This enum describes the different types of rubber band effects that can |
| 15 | be applied to the rectangular zooming area. |
| 16 | |
| 17 | \value NoRubberBand |
| 18 | No zooming area is specified, and therefore zooming is not enabled. |
| 19 | \value VerticalRubberBand |
| 20 | The rubber band is locked to the size of the chart horizontally |
| 21 | and can be pulled vertically to specify the zooming area. |
| 22 | \value HorizontalRubberBand |
| 23 | The rubber band is locked to the size of the chart vertically |
| 24 | and can be pulled horizontally to specify the zooming area. |
| 25 | \value RectangleRubberBand |
| 26 | The rubber band is fixed to the point that was clicked and can be |
| 27 | pulled both vertically and horizontally. |
| 28 | \value ClickThroughRubberBand |
| 29 | An option on the above rubber band choices that allows left clicks |
| 30 | to be passed on to chart items if those chart items accept clicks. |
| 31 | To select this, OR it with one of the rubber band selection modes. |
| 32 | \since 6.2 |
| 33 | */ |
| 34 | |
| 35 | /*! |
| 36 | \class QChartView |
| 37 | \inmodule QtCharts |
| 38 | \brief The QChartView is a standalone widget that can display charts. |
| 39 | |
| 40 | A chart view does not require a QGraphicsScene object to work. To display |
| 41 | a chart in an existing QGraphicsScene, the QChart or QPolarChart class should |
| 42 | be used instead. |
| 43 | |
| 44 | \sa QChart, QPolarChart |
| 45 | */ |
| 46 | |
| 47 | QT_BEGIN_NAMESPACE |
| 48 | |
| 49 | /*! |
| 50 | Constructs a chart view object with the parent \a parent. |
| 51 | */ |
| 52 | |
| 53 | QChartView::QChartView(QWidget *parent) |
| 54 | : QGraphicsView(parent), |
| 55 | d_ptr(new QChartViewPrivate(this)) |
| 56 | { |
| 57 | |
| 58 | } |
| 59 | |
| 60 | /*! |
| 61 | Constructs a chart view object with the parent \a parent to display the |
| 62 | chart \a chart. The ownership of the chart is passed to the chart view. |
| 63 | */ |
| 64 | |
| 65 | QChartView::QChartView(QChart *chart, QWidget *parent) |
| 66 | : QGraphicsView(parent), |
| 67 | d_ptr(new QChartViewPrivate(this, chart)) |
| 68 | { |
| 69 | |
| 70 | } |
| 71 | |
| 72 | |
| 73 | /*! |
| 74 | Deletes the chart view object and the associated chart. |
| 75 | */ |
| 76 | QChartView::~QChartView() |
| 77 | { |
| 78 | } |
| 79 | |
| 80 | /*! |
| 81 | Returns the pointer to the associated chart. |
| 82 | */ |
| 83 | QChart *QChartView::chart() const |
| 84 | { |
| 85 | return d_ptr->m_chart; |
| 86 | } |
| 87 | |
| 88 | /*! |
| 89 | Sets the current chart to \a chart. The ownership of the new chart is passed to |
| 90 | the chart view and the ownership of the previous chart is released. |
| 91 | |
| 92 | To avoid memory leaks, the previous chart must be deleted. |
| 93 | */ |
| 94 | |
| 95 | void QChartView::setChart(QChart *chart) |
| 96 | { |
| 97 | d_ptr->setChart(chart); |
| 98 | } |
| 99 | |
| 100 | /*! |
| 101 | Sets the rubber band flags to \a rubberBand. |
| 102 | The selected flags determine the way zooming is performed. |
| 103 | |
| 104 | \note Rubber band zooming is not supported for polar charts. |
| 105 | */ |
| 106 | void QChartView::setRubberBand(const RubberBands &rubberBand) |
| 107 | { |
| 108 | #ifndef QT_NO_RUBBERBAND |
| 109 | d_ptr->m_rubberBandFlags = rubberBand; |
| 110 | |
| 111 | if (!(d_ptr->m_rubberBandFlags & ~RubberBands(ClickThroughRubberBand))) { |
| 112 | delete d_ptr->m_rubberBand; |
| 113 | d_ptr->m_rubberBand = nullptr; |
| 114 | return; |
| 115 | } |
| 116 | |
| 117 | if (!d_ptr->m_rubberBand) { |
| 118 | d_ptr->m_rubberBand = new QRubberBand(QRubberBand::Rectangle, this); |
| 119 | d_ptr->m_rubberBand->setEnabled(true); |
| 120 | } |
| 121 | #else |
| 122 | Q_UNUSED(rubberBand); |
| 123 | qWarning("Unable to set rubber band because Qt is configured without it." ); |
| 124 | #endif |
| 125 | } |
| 126 | |
| 127 | /*! |
| 128 | Returns the rubber band flags that are currently being used by the chart view. |
| 129 | */ |
| 130 | QChartView::RubberBands QChartView::rubberBand() const |
| 131 | { |
| 132 | return d_ptr->m_rubberBandFlags; |
| 133 | } |
| 134 | |
| 135 | /*! |
| 136 | If the left mouse button is pressed and the rubber band is enabled, the event |
| 137 | \a event is accepted and the rubber band is displayed on the screen. This |
| 138 | enables the user to select the zoom area. |
| 139 | |
| 140 | If some other mouse button is pressed or the rubber band is disabled, the event |
| 141 | is passed to QGraphicsView::mousePressEvent(). |
| 142 | */ |
| 143 | void QChartView::mousePressEvent(QMouseEvent *event) |
| 144 | { |
| 145 | #ifndef QT_NO_RUBBERBAND |
| 146 | QGraphicsItem *itemUnderCursor = itemAt(pos: event->pos()); |
| 147 | bool itemUnderCursorAcceptsLMB = itemUnderCursor && (itemUnderCursor->acceptedMouseButtons() & Qt::LeftButton); |
| 148 | bool clickThrough = d_ptr->m_rubberBandFlags.testFlag(flag: ClickThroughRubberBand); |
| 149 | QRectF plotArea = d_ptr->m_chart->plotArea(); |
| 150 | if (d_ptr->m_rubberBand && d_ptr->m_rubberBand->isEnabled() |
| 151 | && event->button() == Qt::LeftButton |
| 152 | && plotArea.contains(p: event->pos()) |
| 153 | && !(clickThrough && itemUnderCursorAcceptsLMB)) { |
| 154 | d_ptr->m_rubberBandOrigin = event->pos(); |
| 155 | d_ptr->m_rubberBand->setGeometry(QRect(d_ptr->m_rubberBandOrigin, QSize())); |
| 156 | d_ptr->m_rubberBand->show(); |
| 157 | event->accept(); |
| 158 | } else { |
| 159 | #endif |
| 160 | QGraphicsView::mousePressEvent(event); |
| 161 | #ifndef QT_NO_RUBBERBAND |
| 162 | } |
| 163 | #endif |
| 164 | } |
| 165 | |
| 166 | /*! |
| 167 | If the rubber band rectangle is displayed in the press event specified by |
| 168 | \a event, the event data is used to update the rubber band geometry. |
| 169 | Otherwise, the default QGraphicsView::mouseMoveEvent() implementation is called. |
| 170 | */ |
| 171 | void QChartView::mouseMoveEvent(QMouseEvent *event) |
| 172 | { |
| 173 | #ifndef QT_NO_RUBBERBAND |
| 174 | if (d_ptr->m_rubberBand && d_ptr->m_rubberBand->isVisible()) { |
| 175 | QRect rect = d_ptr->m_chart->plotArea().toRect(); |
| 176 | int width = event->pos().x() - d_ptr->m_rubberBandOrigin.x(); |
| 177 | int height = event->pos().y() - d_ptr->m_rubberBandOrigin.y(); |
| 178 | if (!d_ptr->m_rubberBandFlags.testFlag(flag: VerticalRubberBand)) { |
| 179 | d_ptr->m_rubberBandOrigin.setY(rect.top()); |
| 180 | height = rect.height(); |
| 181 | } |
| 182 | if (!d_ptr->m_rubberBandFlags.testFlag(flag: HorizontalRubberBand)) { |
| 183 | d_ptr->m_rubberBandOrigin.setX(rect.left()); |
| 184 | width = rect.width(); |
| 185 | } |
| 186 | d_ptr->m_rubberBand->setGeometry(QRect(d_ptr->m_rubberBandOrigin.x(), d_ptr->m_rubberBandOrigin.y(), width, height).normalized()); |
| 187 | } else { |
| 188 | #endif |
| 189 | QGraphicsView::mouseMoveEvent(event); |
| 190 | #ifndef QT_NO_RUBBERBAND |
| 191 | } |
| 192 | #endif |
| 193 | } |
| 194 | |
| 195 | /*! |
| 196 | If the left mouse button is released and the rubber band is enabled, the |
| 197 | event \a event is accepted and the view is zoomed into the rectangle |
| 198 | specified by the rubber band. If releasing the right mouse button triggered |
| 199 | the event, the view is zoomed out. |
| 200 | */ |
| 201 | void QChartView::mouseReleaseEvent(QMouseEvent *event) |
| 202 | { |
| 203 | #ifndef QT_NO_RUBBERBAND |
| 204 | if (d_ptr->m_rubberBand && d_ptr->m_rubberBand->isVisible()) { |
| 205 | if (event->button() == Qt::LeftButton) { |
| 206 | d_ptr->m_rubberBand->hide(); |
| 207 | QRectF rect = d_ptr->m_rubberBand->geometry(); |
| 208 | // Since plotArea uses QRectF and rubberband uses QRect, we can't just blindly use |
| 209 | // rubberband's dimensions for vertical and horizontal rubberbands, where one |
| 210 | // dimension must match the corresponding plotArea dimension exactly. |
| 211 | if (!d_ptr->m_rubberBandFlags.testFlag(flag: RectangleRubberBand)) { |
| 212 | if (d_ptr->m_rubberBandFlags.testFlag(flag: VerticalRubberBand)) { |
| 213 | rect.setX(d_ptr->m_chart->plotArea().x()); |
| 214 | rect.setWidth(d_ptr->m_chart->plotArea().width()); |
| 215 | } else if (d_ptr->m_rubberBandFlags.testFlag(flag: HorizontalRubberBand)) { |
| 216 | rect.setY(d_ptr->m_chart->plotArea().y()); |
| 217 | rect.setHeight(d_ptr->m_chart->plotArea().height()); |
| 218 | } |
| 219 | } |
| 220 | d_ptr->m_chart->zoomIn(rect); |
| 221 | event->accept(); |
| 222 | } |
| 223 | } else if (d_ptr->m_rubberBand && event->button() == Qt::RightButton) { |
| 224 | // If vertical or horizontal rubberband mode, restrict zoom out to specified axis. |
| 225 | // Since there is no suitable API for that, use zoomIn with rect bigger than the |
| 226 | // plot area. |
| 227 | if (d_ptr->m_rubberBandFlags.testFlag(flag: VerticalRubberBand) |
| 228 | || d_ptr->m_rubberBandFlags.testFlag(flag: HorizontalRubberBand)) { |
| 229 | QRectF rect = d_ptr->m_chart->plotArea(); |
| 230 | if (d_ptr->m_rubberBandFlags.testFlag(flag: VerticalRubberBand)) { |
| 231 | qreal adjustment = rect.height() / 2; |
| 232 | rect.adjust(xp1: 0, yp1: -adjustment, xp2: 0, yp2: adjustment); |
| 233 | } |
| 234 | if (d_ptr->m_rubberBandFlags.testFlag(flag: HorizontalRubberBand)) { |
| 235 | qreal adjustment = rect.width() / 2; |
| 236 | rect.adjust(xp1: -adjustment, yp1: 0, xp2: adjustment, yp2: 0); |
| 237 | } |
| 238 | d_ptr->m_chart->zoomIn(rect); |
| 239 | } |
| 240 | event->accept(); |
| 241 | } else { |
| 242 | #endif |
| 243 | QGraphicsView::mouseReleaseEvent(event); |
| 244 | #ifndef QT_NO_RUBBERBAND |
| 245 | } |
| 246 | #endif |
| 247 | } |
| 248 | |
| 249 | #ifdef Q_OS_MACOS |
| 250 | #if QT_CONFIG(wheelevent) |
| 251 | void QChartView::wheelEvent(QWheelEvent *event) |
| 252 | { |
| 253 | QGraphicsView::wheelEvent(event); |
| 254 | } |
| 255 | #endif |
| 256 | #endif |
| 257 | |
| 258 | /*! |
| 259 | Resizes and updates the chart area using the data specified by \a event. |
| 260 | */ |
| 261 | void QChartView::resizeEvent(QResizeEvent *event) |
| 262 | { |
| 263 | QGraphicsView::resizeEvent(event); |
| 264 | d_ptr->resize(); |
| 265 | } |
| 266 | |
| 267 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
| 268 | |
| 269 | QChartViewPrivate::QChartViewPrivate(QChartView *q, QChart *chart) |
| 270 | : q_ptr(q), |
| 271 | m_scene(new QGraphicsScene(q)), |
| 272 | m_chart(chart), |
| 273 | #ifndef QT_NO_RUBBERBAND |
| 274 | m_rubberBand(nullptr), |
| 275 | #endif |
| 276 | m_rubberBandFlags(QChartView::NoRubberBand) |
| 277 | { |
| 278 | q_ptr->setFrameShape(QFrame::NoFrame); |
| 279 | q_ptr->setBackgroundRole(QPalette::Window); |
| 280 | q_ptr->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); |
| 281 | q_ptr->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); |
| 282 | q_ptr->setScene(m_scene); |
| 283 | q_ptr->setSizePolicy(hor: QSizePolicy::Expanding, ver: QSizePolicy::Expanding); |
| 284 | if (!m_chart) |
| 285 | m_chart = new QChart(); |
| 286 | m_scene->addItem(item: m_chart); |
| 287 | } |
| 288 | |
| 289 | QChartViewPrivate::~QChartViewPrivate() |
| 290 | { |
| 291 | } |
| 292 | |
| 293 | void QChartViewPrivate::setChart(QChart *chart) |
| 294 | { |
| 295 | Q_ASSERT(chart); |
| 296 | |
| 297 | if (m_chart == chart) |
| 298 | return; |
| 299 | |
| 300 | if (m_chart) |
| 301 | m_scene->removeItem(item: m_chart); |
| 302 | |
| 303 | m_chart = chart; |
| 304 | m_scene->addItem(item: m_chart); |
| 305 | |
| 306 | resize(); |
| 307 | } |
| 308 | |
| 309 | void QChartViewPrivate::resize() |
| 310 | { |
| 311 | // Fit the chart into view if it has been rotated |
| 312 | qreal sinA = qAbs(t: q_ptr->transform().m21()); |
| 313 | qreal cosA = qAbs(t: q_ptr->transform().m11()); |
| 314 | QSize chartSize = q_ptr->size(); |
| 315 | |
| 316 | if (sinA == 1.0) { |
| 317 | chartSize.setHeight(q_ptr->size().width()); |
| 318 | chartSize.setWidth(q_ptr->size().height()); |
| 319 | } else if (sinA != 0.0) { |
| 320 | // Non-90 degree rotation, find largest square chart that can fit into the view. |
| 321 | qreal minDimension = qMin(a: q_ptr->size().width(), b: q_ptr->size().height()); |
| 322 | qreal h = (minDimension - (minDimension / ((sinA / cosA) + 1.0))) / sinA; |
| 323 | chartSize.setHeight(h); |
| 324 | chartSize.setWidth(h); |
| 325 | } |
| 326 | |
| 327 | m_chart->resize(size: chartSize); |
| 328 | q_ptr->setMinimumSize(m_chart->minimumSize().toSize().expandedTo(otherSize: q_ptr->minimumSize())); |
| 329 | q_ptr->setMaximumSize(q_ptr->maximumSize().boundedTo(otherSize: m_chart->maximumSize().toSize())); |
| 330 | q_ptr->setSceneRect(m_chart->geometry()); |
| 331 | } |
| 332 | |
| 333 | QT_END_NAMESPACE |
| 334 | |
| 335 | #include "moc_qchartview.cpp" |
| 336 | |