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 : QAbstractScrollArea(*new QScrollAreaPrivate,parent)
105{
106 Q_D(QScrollArea);
107 d->viewport->setBackgroundRole(QPalette::NoRole);
108 d->vbar->setSingleStep(20);
109 d->hbar->setSingleStep(20);
110 d->layoutChildren();
111}
112
113/*!
114 \internal
115*/
116QScrollArea::QScrollArea(QScrollAreaPrivate &dd, QWidget *parent)
117 : QAbstractScrollArea(dd, parent)
118{
119 Q_D(QScrollArea);
120 d->viewport->setBackgroundRole(QPalette::NoRole);
121 d->vbar->setSingleStep(20);
122 d->hbar->setSingleStep(20);
123 d->layoutChildren();
124}
125
126/*!
127 Destroys the scroll area and its child widget.
128
129 \sa setWidget()
130*/
131QScrollArea::~QScrollArea()
132{
133}
134
135void QScrollAreaPrivate::updateWidgetPosition()
136{
137 Q_Q(QScrollArea);
138 Qt::LayoutDirection dir = q->layoutDirection();
139 QRect scrolled = QStyle::visualRect(direction: dir, boundingRect: viewport->rect(), logicalRect: QRect(QPoint(-hbar->value(), -vbar->value()), widget->size()));
140 QRect aligned = QStyle::alignedRect(direction: dir, alignment, size: widget->size(), rectangle: viewport->rect());
141 widget->move(ax: widget->width() < viewport->width() ? aligned.x() : scrolled.x(),
142 ay: widget->height() < viewport->height() ? aligned.y() : scrolled.y());
143}
144
145void QScrollAreaPrivate::updateScrollBars()
146{
147 Q_Q(QScrollArea);
148 if (!widget)
149 return;
150 QSize p = viewport->size();
151 QSize m = q->maximumViewportSize();
152
153 QSize min = qSmartMinSize(w: widget);
154 QSize max = qSmartMaxSize(w: widget);
155
156 if (resizable) {
157 if ((widget->layout() ? widget->layout()->hasHeightForWidth() : widget->sizePolicy().hasHeightForWidth())) {
158 QSize p_hfw = p.expandedTo(otherSize: min).boundedTo(otherSize: max);
159 int h = widget->heightForWidth(p_hfw.width());
160 // If the height we calculated requires a vertical scrollbar,
161 // then we need to constrain the width and calculate the height again,
162 // otherwise we end up flipping the scrollbar on and off all the time.
163 if (vbarpolicy == Qt::ScrollBarAsNeeded) {
164 int vbarWidth = vbar->sizeHint().width();
165 QSize m_hfw = m.expandedTo(otherSize: min).boundedTo(otherSize: max);
166 // is there any point in searching?
167 if (widget->heightForWidth(m_hfw.width() - vbarWidth) <= m.height()) {
168 while (h > m.height() && vbarWidth) {
169 --vbarWidth;
170 --m_hfw.rwidth();
171 h = widget->heightForWidth(m_hfw.width());
172 }
173 }
174 max = QSize(m_hfw.width(), qMax(a: m_hfw.height(), b: h));
175 }
176 min = QSize(p_hfw.width(), qMax(a: p_hfw.height(), b: h));
177 }
178 }
179
180 if ((resizable && m.expandedTo(otherSize: min) == m && m.boundedTo(otherSize: max) == m)
181 || (!resizable && m.expandedTo(otherSize: widget->size()) == m))
182 p = m; // no scroll bars needed
183
184 if (resizable)
185 widget->resize(p.expandedTo(otherSize: min).boundedTo(otherSize: max));
186 QSize v = widget->size();
187
188 hbar->setRange(min: 0, max: v.width() - p.width());
189 hbar->setPageStep(p.width());
190 vbar->setRange(min: 0, max: v.height() - p.height());
191 vbar->setPageStep(p.height());
192 updateWidgetPosition();
193
194}
195
196/*!
197 Returns the scroll area's widget, or \nullptr if there is none.
198
199 \sa setWidget()
200*/
201
202QWidget *QScrollArea::widget() const
203{
204 Q_D(const QScrollArea);
205 return d->widget;
206}
207
208/*!
209 \fn void QScrollArea::setWidget(QWidget *widget)
210
211 Sets the scroll area's \a widget.
212
213 The \a widget becomes a child of the scroll area, and will be
214 destroyed when the scroll area is deleted or when a new widget is
215 set.
216
217 The widget's \l{QWidget::setAutoFillBackground()}{autoFillBackground}
218 property will be set to \c{true}.
219
220 If the scroll area is visible when the \a widget is
221 added, you must \l{QWidget::}{show()} it explicitly.
222
223 Note that You must add the layout of \a widget before you call
224 this function; if you add it later, the \a widget will not be
225 visible - regardless of when you \l{QWidget::}{show()} the scroll
226 area. In this case, you can also not \l{QWidget::}{show()} the \a
227 widget later.
228
229 \sa widget()
230*/
231void QScrollArea::setWidget(QWidget *widget)
232{
233 Q_D(QScrollArea);
234 if (widget == d->widget || !widget)
235 return;
236
237 delete d->widget;
238 d->widget = nullptr;
239 d->hbar->setValue(0);
240 d->vbar->setValue(0);
241 if (widget->parentWidget() != d->viewport)
242 widget->setParent(d->viewport);
243 if (!widget->testAttribute(attribute: Qt::WA_Resized))
244 widget->resize(widget->sizeHint());
245 d->widget = widget;
246 d->widget->setAutoFillBackground(true);
247 widget->installEventFilter(filterObj: this);
248 d->widgetSize = QSize();
249 d->updateScrollBars();
250 d->widget->show();
251
252}
253
254/*!
255 Removes the scroll area's widget, and passes ownership of the
256 widget to the caller.
257
258 \sa widget()
259 */
260QWidget *QScrollArea::takeWidget()
261{
262 Q_D(QScrollArea);
263 QWidget *w = d->widget;
264 d->widget = nullptr;
265 if (w)
266 w->setParent(nullptr);
267 return w;
268}
269
270/*!
271 \reimp
272 */
273bool QScrollArea::event(QEvent *e)
274{
275 Q_D(QScrollArea);
276 if (e->type() == QEvent::StyleChange || e->type() == QEvent::LayoutRequest) {
277 d->updateScrollBars();
278 }
279#ifdef QT_KEYPAD_NAVIGATION
280 else if (QApplicationPrivate::keypadNavigationEnabled()) {
281 if (e->type() == QEvent::Show)
282 QApplication::instance()->installEventFilter(this);
283 else if (e->type() == QEvent::Hide)
284 QApplication::instance()->removeEventFilter(this);
285 }
286#endif
287 return QAbstractScrollArea::event(e);
288}
289
290
291/*!
292 \reimp
293 */
294bool QScrollArea::eventFilter(QObject *o, QEvent *e)
295{
296 Q_D(QScrollArea);
297#ifdef QT_KEYPAD_NAVIGATION
298 if (d->widget && o != d->widget && e->type() == QEvent::FocusIn
299 && QApplicationPrivate::keypadNavigationEnabled()) {
300 if (o->isWidgetType())
301 ensureWidgetVisible(static_cast<QWidget *>(o));
302 }
303#endif
304 if (o == d->widget && e->type() == QEvent::Resize)
305 d->updateScrollBars();
306
307 return QAbstractScrollArea::eventFilter(o, e);
308}
309
310/*!
311 \reimp
312 */
313void QScrollArea::resizeEvent(QResizeEvent *)
314{
315 Q_D(QScrollArea);
316 d->updateScrollBars();
317
318}
319
320
321/*!\reimp
322 */
323void QScrollArea::scrollContentsBy(int, int)
324{
325 Q_D(QScrollArea);
326 if (!d->widget)
327 return;
328 d->updateWidgetPosition();
329}
330
331
332/*!
333 \property QScrollArea::widgetResizable
334 \brief whether the scroll area should resize the view widget
335
336 If this property is set to false (the default), the scroll area
337 honors the size of its widget. Regardless of this property, you
338 can programmatically resize the widget using widget()->resize(),
339 and the scroll area will automatically adjust itself to the new
340 size.
341
342 If this property is set to true, the scroll area will
343 automatically resize the widget in order to avoid scroll bars
344 where they can be avoided, or to take advantage of extra space.
345*/
346bool QScrollArea::widgetResizable() const
347{
348 Q_D(const QScrollArea);
349 return d->resizable;
350}
351
352void QScrollArea::setWidgetResizable(bool resizable)
353{
354 Q_D(QScrollArea);
355 d->resizable = resizable;
356 updateGeometry();
357 d->updateScrollBars();
358}
359
360/*!
361 \reimp
362 */
363QSize QScrollArea::sizeHint() const
364{
365 Q_D(const QScrollArea);
366 int f = 2 * d->frameWidth;
367 QSize sz(f, f);
368 int h = fontMetrics().height();
369 if (d->widget) {
370 if (!d->widgetSize.isValid())
371 d->widgetSize = d->resizable ? d->widget->sizeHint() : d->widget->size();
372 sz += d->widgetSize;
373 } else {
374 sz += QSize(12 * h, 8 * h);
375 }
376 if (d->vbarpolicy == Qt::ScrollBarAlwaysOn)
377 sz.setWidth(sz.width() + d->vbar->sizeHint().width());
378 if (d->hbarpolicy == Qt::ScrollBarAlwaysOn)
379 sz.setHeight(sz.height() + d->hbar->sizeHint().height());
380 return sz.boundedTo(otherSize: QSize(36 * h, 24 * h));
381}
382
383/*!
384 \reimp
385 */
386QSize QScrollArea::viewportSizeHint() const
387{
388 Q_D(const QScrollArea);
389 if (d->widget) {
390 return d->resizable ? d->widget->sizeHint() : d->widget->size();
391 }
392 const int h = fontMetrics().height();
393 return QSize(6 * h, 4 * h);
394}
395
396
397/*!
398 \reimp
399 */
400bool QScrollArea::focusNextPrevChild(bool next)
401{
402 if (QWidget::focusNextPrevChild(next)) {
403 if (QWidget *fw = focusWidget())
404 ensureWidgetVisible(childWidget: fw);
405 return true;
406 }
407 return false;
408}
409
410/*!
411 Scrolls the contents of the scroll area so that the point (\a x, \a y) is visible
412 inside the region of the viewport with margins specified in pixels by \a xmargin and
413 \a ymargin. If the specified point cannot be reached, the contents are scrolled to
414 the nearest valid position. The default value for both margins is 50 pixels.
415*/
416void QScrollArea::ensureVisible(int x, int y, int xmargin, int ymargin)
417{
418 Q_D(QScrollArea);
419
420 int logicalX = QStyle::visualPos(direction: layoutDirection(), boundingRect: d->viewport->rect(), logicalPos: QPoint(x, y)).x();
421
422 if (logicalX - xmargin < d->hbar->value()) {
423 d->hbar->setValue(qMax(a: 0, b: logicalX - xmargin));
424 } else if (logicalX > d->hbar->value() + d->viewport->width() - xmargin) {
425 d->hbar->setValue(qMin(a: logicalX - d->viewport->width() + xmargin, b: d->hbar->maximum()));
426 }
427
428 if (y - ymargin < d->vbar->value()) {
429 d->vbar->setValue(qMax(a: 0, b: y - ymargin));
430 } else if (y > d->vbar->value() + d->viewport->height() - ymargin) {
431 d->vbar->setValue(qMin(a: y - d->viewport->height() + ymargin, b: d->vbar->maximum()));
432 }
433}
434
435/*!
436 \since 4.2
437
438 Scrolls the contents of the scroll area so that the \a childWidget
439 of QScrollArea::widget() is visible inside the viewport with
440 margins specified in pixels by \a xmargin and \a ymargin. If the
441 specified point cannot be reached, the contents are scrolled to
442 the nearest valid position. The default value for both margins is
443 50 pixels.
444
445*/
446void QScrollArea::ensureWidgetVisible(QWidget *childWidget, int xmargin, int ymargin)
447{
448 Q_D(QScrollArea);
449
450 if (!d->widget->isAncestorOf(child: childWidget))
451 return;
452
453 const QRect microFocus = childWidget->inputMethodQuery(Qt::ImCursorRectangle).toRect();
454 const QRect defaultMicroFocus =
455 childWidget->QWidget::inputMethodQuery(Qt::ImCursorRectangle).toRect();
456 QRect focusRect = (microFocus != defaultMicroFocus)
457 ? QRect(childWidget->mapTo(d->widget, microFocus.topLeft()), microFocus.size())
458 : QRect(childWidget->mapTo(d->widget, QPoint(0,0)), childWidget->size());
459 const QRect visibleRect(-d->widget->pos(), d->viewport->size());
460
461 if (visibleRect.contains(r: focusRect))
462 return;
463
464 focusRect.adjust(dx1: -xmargin, dy1: -ymargin, dx2: xmargin, dy2: ymargin);
465
466 if (focusRect.width() > visibleRect.width())
467 d->hbar->setValue(focusRect.center().x() - d->viewport->width() / 2);
468 else if (focusRect.right() > visibleRect.right())
469 d->hbar->setValue(focusRect.right() - d->viewport->width() + 1);
470 else if (focusRect.left() < visibleRect.left())
471 d->hbar->setValue(focusRect.left());
472
473 if (focusRect.height() > visibleRect.height())
474 d->vbar->setValue(focusRect.center().y() - d->viewport->height() / 2);
475 else if (focusRect.bottom() > visibleRect.bottom())
476 d->vbar->setValue(focusRect.bottom() - d->viewport->height() + 1);
477 else if (focusRect.top() < visibleRect.top())
478 d->vbar->setValue(focusRect.top());
479}
480
481
482/*!
483 \property QScrollArea::alignment
484 \brief the alignment of the scroll area's widget
485 \since 4.2
486
487 A valid alignment is a combination of the following flags:
488 \list
489 \li \c Qt::AlignLeft
490 \li \c Qt::AlignHCenter
491 \li \c Qt::AlignRight
492 \li \c Qt::AlignTop
493 \li \c Qt::AlignVCenter
494 \li \c Qt::AlignBottom
495 \endlist
496 By default, the widget stays rooted to the top-left corner of the
497 scroll area.
498*/
499
500void QScrollArea::setAlignment(Qt::Alignment alignment)
501{
502 Q_D(QScrollArea);
503 d->alignment = alignment;
504 if (d->widget)
505 d->updateWidgetPosition();
506}
507
508Qt::Alignment QScrollArea::alignment() const
509{
510 Q_D(const QScrollArea);
511 return d->alignment;
512}
513
514QT_END_NAMESPACE
515
516#include "moc_qscrollarea.cpp"
517

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