1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include "qscrollarea.h"
5#include "private/qscrollarea_p.h"
6
7#include "qscrollbar.h"
8#include "qlayout.h"
9#include "qstyle.h"
10#include "qapplication.h"
11#include "qvariant.h"
12#include "qdebug.h"
13#include "private/qapplication_p.h"
14#include "private/qlayoutengine_p.h"
15
16QT_BEGIN_NAMESPACE
17
18/*!
19 \class QScrollArea
20
21 \brief The QScrollArea class provides a scrolling view onto
22 another widget.
23
24 \ingroup basicwidgets
25 \inmodule QtWidgets
26
27 A scroll area is used to display the contents of a child widget
28 within a frame. If the widget exceeds the size of the frame, the
29 view can provide scroll bars so that the entire area of the child
30 widget can be viewed. The child widget must be specified with
31 setWidget(). For example:
32
33 \snippet code/src_gui_widgets_qscrollarea.cpp 0
34
35 The code above creates a scroll area (shown in the images below)
36 containing an image label. When scaling the image, the scroll area
37 can provide the necessary scroll bars:
38
39 \table
40 \row
41 \li \inlineimage qscrollarea-noscrollbars.png
42 \li \inlineimage qscrollarea-onescrollbar.png
43 \li \inlineimage qscrollarea-twoscrollbars.png
44 \endtable
45
46 The scroll bars appearance depends on the currently set \l
47 {Qt::ScrollBarPolicy}{scroll bar policies}. You can control the
48 appearance of the scroll bars using the inherited functionality
49 from QAbstractScrollArea.
50
51 For example, you can set the
52 QAbstractScrollArea::horizontalScrollBarPolicy and
53 QAbstractScrollArea::verticalScrollBarPolicy properties. Or if you
54 want the scroll bars to adjust dynamically when the contents of
55 the scroll area changes, you can use the \l
56 {QAbstractScrollArea::horizontalScrollBar()}{horizontalScrollBar()}
57 and \l
58 {QAbstractScrollArea::verticalScrollBar()}{verticalScrollBar()}
59 functions (which enable you to access the scroll bars) and set the
60 scroll bars' values whenever the scroll area's contents change,
61 using the QScrollBar::setValue() function.
62
63 You can retrieve the child widget using the widget() function. The
64 view can be made to be resizable with the setWidgetResizable()
65 function. The alignment of the widget can be specified with
66 setAlignment().
67
68 Two convenience functions ensureVisible() and
69 ensureWidgetVisible() ensure a certain region of the contents is
70 visible inside the viewport, by scrolling the contents if
71 necessary.
72
73 \section1 Size Hints and Layouts
74
75 When using a scroll area to display the contents of a custom
76 widget, it is important to ensure that the
77 \l{QWidget::sizeHint}{size hint} of the child widget is set to a
78 suitable value. If a standard QWidget is used for the child
79 widget, it may be necessary to call QWidget::setMinimumSize() to
80 ensure that the contents of the widget are shown correctly within
81 the scroll area.
82
83 If a scroll area is used to display the contents of a widget that
84 contains child widgets arranged in a layout, it is important to
85 realize that the size policy of the layout will also determine the
86 size of the widget. This is especially useful to know if you intend
87 to dynamically change the contents of the layout. In such cases,
88 setting the layout's \l{QLayout::sizeConstraint}{size constraint}
89 property to one which provides constraints on the minimum and/or
90 maximum size of the layout (e.g., QLayout::SetMinAndMaxSize) will
91 cause the size of the scroll area to be updated whenever the
92 contents of the layout changes.
93
94 \sa QAbstractScrollArea, QScrollBar
95*/
96
97
98/*!
99 Constructs an empty scroll area with the given \a parent.
100
101 \sa setWidget()
102*/
103QScrollArea::QScrollArea(QWidget *parent)
104 : QScrollArea(*new QScrollAreaPrivate, parent)
105{
106}
107
108/*!
109 \internal
110*/
111QScrollArea::QScrollArea(QScrollAreaPrivate &dd, QWidget *parent)
112 : QAbstractScrollArea(dd, parent)
113{
114 Q_D(QScrollArea);
115 d->viewport->setBackgroundRole(QPalette::NoRole);
116 const auto singleStep = d->defaultSingleStep();
117 d->vbar->setSingleStep(singleStep);
118 d->hbar->setSingleStep(singleStep);
119 d->layoutChildren();
120}
121
122/*!
123 Destroys the scroll area and its child widget.
124
125 \sa setWidget()
126*/
127QScrollArea::~QScrollArea()
128{
129}
130
131void QScrollAreaPrivate::updateWidgetPosition()
132{
133 Q_Q(QScrollArea);
134 Qt::LayoutDirection dir = q->layoutDirection();
135 QRect scrolled = QStyle::visualRect(direction: dir, boundingRect: viewport->rect(), logicalRect: QRect(QPoint(-hbar->value(), -vbar->value()), widget->size()));
136 QRect aligned = QStyle::alignedRect(direction: dir, alignment, size: widget->size(), rectangle: viewport->rect());
137 widget->move(ax: widget->width() < viewport->width() ? aligned.x() : scrolled.x(),
138 ay: widget->height() < viewport->height() ? aligned.y() : scrolled.y());
139}
140
141void QScrollAreaPrivate::updateScrollBars()
142{
143 Q_Q(QScrollArea);
144 if (!widget)
145 return;
146 QSize p = viewport->size();
147 QSize m = q->maximumViewportSize();
148
149 QSize min = qSmartMinSize(w: widget);
150 QSize max = qSmartMaxSize(w: widget);
151
152 if (resizable) {
153 if ((widget->layout() ? widget->layout()->hasHeightForWidth() : widget->sizePolicy().hasHeightForWidth())) {
154 QSize p_hfw = p.expandedTo(otherSize: min).boundedTo(otherSize: max);
155 int h = widget->heightForWidth(p_hfw.width());
156 // If the height we calculated requires a vertical scrollbar,
157 // then we need to constrain the width and calculate the height again,
158 // otherwise we end up flipping the scrollbar on and off all the time.
159 if (vbarpolicy == Qt::ScrollBarAsNeeded) {
160 int vbarWidth = vbar->sizeHint().width();
161 QSize m_hfw = m.expandedTo(otherSize: min).boundedTo(otherSize: max);
162 // is there any point in searching?
163 if (widget->heightForWidth(m_hfw.width() - vbarWidth) <= m.height()) {
164 while (h > m.height() && vbarWidth) {
165 --vbarWidth;
166 --m_hfw.rwidth();
167 h = widget->heightForWidth(m_hfw.width());
168 }
169 }
170 max = QSize(m_hfw.width(), qMax(a: m_hfw.height(), b: h));
171 }
172 min = QSize(p_hfw.width(), qMax(a: p_hfw.height(), b: h));
173 }
174 }
175
176 if ((resizable && m.expandedTo(otherSize: min) == m && m.boundedTo(otherSize: max) == m)
177 || (!resizable && m.expandedTo(otherSize: widget->size()) == m))
178 p = m; // no scroll bars needed
179
180 if (resizable)
181 widget->resize(p.expandedTo(otherSize: min).boundedTo(otherSize: max));
182 QSize v = widget->size();
183
184 hbar->setRange(min: 0, max: v.width() - p.width());
185 hbar->setPageStep(p.width());
186 vbar->setRange(min: 0, max: v.height() - p.height());
187 vbar->setPageStep(p.height());
188 updateWidgetPosition();
189
190}
191
192/*!
193 Returns the scroll area's widget, or \nullptr if there is none.
194
195 \sa setWidget()
196*/
197
198QWidget *QScrollArea::widget() const
199{
200 Q_D(const QScrollArea);
201 return d->widget;
202}
203
204/*!
205 \fn void QScrollArea::setWidget(QWidget *widget)
206
207 Sets the scroll area's \a widget.
208
209 The \a widget becomes a child of the scroll area, and will be
210 destroyed when the scroll area is deleted or when a new widget is
211 set.
212
213 The widget's \l{QWidget::setAutoFillBackground()}{autoFillBackground}
214 property will be set to \c{true}.
215
216 If the scroll area is visible when the \a widget is
217 added, you must \l{QWidget::}{show()} it explicitly.
218
219 Note that You must add the layout of \a widget before you call
220 this function; if you add it later, the \a widget will not be
221 visible - regardless of when you \l{QWidget::}{show()} the scroll
222 area. In this case, you can also not \l{QWidget::}{show()} the \a
223 widget later.
224
225 \sa widget()
226*/
227void QScrollArea::setWidget(QWidget *widget)
228{
229 Q_D(QScrollArea);
230 if (widget == d->widget || !widget)
231 return;
232
233 delete d->widget;
234 d->widget = nullptr;
235 d->hbar->setValue(0);
236 d->vbar->setValue(0);
237 if (widget->parentWidget() != d->viewport)
238 widget->setParent(d->viewport);
239 if (!widget->testAttribute(attribute: Qt::WA_Resized))
240 widget->resize(widget->sizeHint());
241 d->widget = widget;
242 d->widget->setAutoFillBackground(true);
243 widget->installEventFilter(filterObj: this);
244 d->widgetSize = QSize();
245 d->updateScrollBars();
246 d->widget->show();
247
248}
249
250/*!
251 Removes the scroll area's widget, and passes ownership of the
252 widget to the caller.
253
254 \sa widget()
255 */
256QWidget *QScrollArea::takeWidget()
257{
258 Q_D(QScrollArea);
259 QWidget *w = d->widget;
260 d->widget = nullptr;
261 if (w)
262 w->setParent(nullptr);
263 return w;
264}
265
266/*!
267 \reimp
268 */
269bool QScrollArea::event(QEvent *e)
270{
271 Q_D(QScrollArea);
272 if (e->type() == QEvent::StyleChange || e->type() == QEvent::LayoutRequest) {
273 d->updateScrollBars();
274 }
275#ifdef QT_KEYPAD_NAVIGATION
276 else if (QApplicationPrivate::keypadNavigationEnabled()) {
277 if (e->type() == QEvent::Show)
278 QApplication::instance()->installEventFilter(this);
279 else if (e->type() == QEvent::Hide)
280 QApplication::instance()->removeEventFilter(this);
281 }
282#endif
283 return QAbstractScrollArea::event(e);
284}
285
286
287/*!
288 \reimp
289 */
290bool QScrollArea::eventFilter(QObject *o, QEvent *e)
291{
292 Q_D(QScrollArea);
293#ifdef QT_KEYPAD_NAVIGATION
294 if (d->widget && o != d->widget && e->type() == QEvent::FocusIn
295 && QApplicationPrivate::keypadNavigationEnabled()) {
296 if (o->isWidgetType())
297 ensureWidgetVisible(static_cast<QWidget *>(o));
298 }
299#endif
300 if (o == d->widget && e->type() == QEvent::Resize)
301 d->updateScrollBars();
302
303 return QAbstractScrollArea::eventFilter(o, e);
304}
305
306/*!
307 \reimp
308 */
309void QScrollArea::resizeEvent(QResizeEvent *)
310{
311 Q_D(QScrollArea);
312 d->updateScrollBars();
313
314}
315
316
317/*!\reimp
318 */
319void QScrollArea::scrollContentsBy(int, int)
320{
321 Q_D(QScrollArea);
322 if (!d->widget)
323 return;
324 d->updateWidgetPosition();
325}
326
327
328/*!
329 \property QScrollArea::widgetResizable
330 \brief whether the scroll area should resize the view widget
331
332 If this property is set to false (the default), the scroll area
333 honors the size of its widget. Regardless of this property, you
334 can programmatically resize the widget using widget()->resize(),
335 and the scroll area will automatically adjust itself to the new
336 size.
337
338 If this property is set to true, the scroll area will
339 automatically resize the widget in order to avoid scroll bars
340 where they can be avoided, or to take advantage of extra space.
341*/
342bool QScrollArea::widgetResizable() const
343{
344 Q_D(const QScrollArea);
345 return d->resizable;
346}
347
348void QScrollArea::setWidgetResizable(bool resizable)
349{
350 Q_D(QScrollArea);
351 d->resizable = resizable;
352 updateGeometry();
353 d->updateScrollBars();
354}
355
356/*!
357 \reimp
358 */
359QSize QScrollArea::sizeHint() const
360{
361 Q_D(const QScrollArea);
362 int f = 2 * d->frameWidth;
363 QSize sz(f, f);
364 int h = fontMetrics().height();
365 if (d->widget) {
366 if (!d->widgetSize.isValid())
367 d->widgetSize = d->resizable ? d->widget->sizeHint() : d->widget->size();
368 sz += d->widgetSize;
369 } else {
370 sz += QSize(12 * h, 8 * h);
371 }
372 if (d->vbarpolicy == Qt::ScrollBarAlwaysOn)
373 sz.setWidth(sz.width() + d->vbar->sizeHint().width());
374 if (d->hbarpolicy == Qt::ScrollBarAlwaysOn)
375 sz.setHeight(sz.height() + d->hbar->sizeHint().height());
376 return sz.boundedTo(otherSize: QSize(36 * h, 24 * h));
377}
378
379/*!
380 \reimp
381 */
382QSize QScrollArea::viewportSizeHint() const
383{
384 Q_D(const QScrollArea);
385 if (d->widget) {
386 return d->resizable ? d->widget->sizeHint() : d->widget->size();
387 }
388 const int h = fontMetrics().height();
389 return QSize(6 * h, 4 * h);
390}
391
392
393/*!
394 \reimp
395 */
396bool QScrollArea::focusNextPrevChild(bool next)
397{
398 if (QWidget::focusNextPrevChild(next)) {
399 if (QWidget *fw = focusWidget())
400 ensureWidgetVisible(childWidget: fw);
401 return true;
402 }
403 return false;
404}
405
406/*!
407 Scrolls the contents of the scroll area so that the point (\a x, \a y) is visible
408 inside the region of the viewport with margins specified in pixels by \a xmargin and
409 \a ymargin. If the specified point cannot be reached, the contents are scrolled to
410 the nearest valid position. The default value for both margins is 50 pixels.
411*/
412void QScrollArea::ensureVisible(int x, int y, int xmargin, int ymargin)
413{
414 Q_D(QScrollArea);
415
416 int logicalX = QStyle::visualPos(direction: layoutDirection(), boundingRect: d->viewport->rect(), logicalPos: QPoint(x, y)).x();
417
418 if (logicalX - xmargin < d->hbar->value()) {
419 d->hbar->setValue(qMax(a: 0, b: logicalX - xmargin));
420 } else if (logicalX > d->hbar->value() + d->viewport->width() - xmargin) {
421 d->hbar->setValue(qMin(a: logicalX - d->viewport->width() + xmargin, b: d->hbar->maximum()));
422 }
423
424 if (y - ymargin < d->vbar->value()) {
425 d->vbar->setValue(qMax(a: 0, b: y - ymargin));
426 } else if (y > d->vbar->value() + d->viewport->height() - ymargin) {
427 d->vbar->setValue(qMin(a: y - d->viewport->height() + ymargin, b: d->vbar->maximum()));
428 }
429}
430
431/*!
432 \since 4.2
433
434 Scrolls the contents of the scroll area so that the \a childWidget
435 of QScrollArea::widget() is visible inside the viewport with
436 margins specified in pixels by \a xmargin and \a ymargin. If the
437 specified point cannot be reached, the contents are scrolled to
438 the nearest valid position. The default value for both margins is
439 50 pixels.
440
441*/
442void QScrollArea::ensureWidgetVisible(QWidget *childWidget, int xmargin, int ymargin)
443{
444 Q_D(QScrollArea);
445
446 if (!d->widget->isAncestorOf(child: childWidget))
447 return;
448
449 const QRect microFocus = childWidget->inputMethodQuery(Qt::ImCursorRectangle).toRect();
450 const QRect defaultMicroFocus =
451 childWidget->QWidget::inputMethodQuery(Qt::ImCursorRectangle).toRect();
452 QRect focusRect = (microFocus != defaultMicroFocus)
453 ? QRect(childWidget->mapTo(d->widget, microFocus.topLeft()), microFocus.size())
454 : QRect(childWidget->mapTo(d->widget, QPoint(0,0)), childWidget->size());
455 const QRect visibleRect(-d->widget->pos(), d->viewport->size());
456
457 if (visibleRect.contains(r: focusRect))
458 return;
459
460 focusRect.adjust(dx1: -xmargin, dy1: -ymargin, dx2: xmargin, dy2: ymargin);
461
462 if (focusRect.width() > visibleRect.width())
463 d->hbar->setValue(focusRect.center().x() - d->viewport->width() / 2);
464 else if (focusRect.right() > visibleRect.right())
465 d->hbar->setValue(focusRect.right() - d->viewport->width() + 1);
466 else if (focusRect.left() < visibleRect.left())
467 d->hbar->setValue(focusRect.left());
468
469 if (focusRect.height() > visibleRect.height())
470 d->vbar->setValue(focusRect.center().y() - d->viewport->height() / 2);
471 else if (focusRect.bottom() > visibleRect.bottom())
472 d->vbar->setValue(focusRect.bottom() - d->viewport->height() + 1);
473 else if (focusRect.top() < visibleRect.top())
474 d->vbar->setValue(focusRect.top());
475}
476
477
478/*!
479 \property QScrollArea::alignment
480 \brief the alignment of the scroll area's widget
481 \since 4.2
482
483 A valid alignment is a combination of the following flags:
484 \list
485 \li \c Qt::AlignLeft
486 \li \c Qt::AlignHCenter
487 \li \c Qt::AlignRight
488 \li \c Qt::AlignTop
489 \li \c Qt::AlignVCenter
490 \li \c Qt::AlignBottom
491 \endlist
492 By default, the widget stays rooted to the top-left corner of the
493 scroll area.
494*/
495
496void QScrollArea::setAlignment(Qt::Alignment alignment)
497{
498 Q_D(QScrollArea);
499 d->alignment = alignment;
500 if (d->widget)
501 d->updateWidgetPosition();
502}
503
504Qt::Alignment QScrollArea::alignment() const
505{
506 Q_D(const QScrollArea);
507 return d->alignment;
508}
509
510QT_END_NAMESPACE
511
512#include "moc_qscrollarea.cpp"
513

source code of qtbase/src/widgets/widgets/qscrollarea.cpp