6.4. Области просмотра с прокруткой.

Класс QScrollView представляет собой область просмотра с двумя полосами прокрутки и "угловым" компонентом, находящимся в правом нижнем углу (обычно -- пустой QWidget). Если необходимо добавить полосы прокрутки к своему виджету, то намного проще воспользоваться готовым QScrollView, чем добавлять компоненты QScrollBar к своему виджету и писать код, реализующий их функциональность.

Рисунок 6.9. Виджеты, составляющие QScrollView.


Самый простой способ добавить визуальный компонент в QScrollView -- это вызвать метод addChild(), указав необходимый подчиненный виджет в качестве аргумента. QScrollView автоматически переподчинит визуальный компонент, став его владельцем. Например, пусть необходимо окружить компонент IconEditor, который был разработан нами в Главе 5, полосами прокрутки. Для этого можно было бы написать следующий код: #include <qapplication.h> #include <qscrollview.h> #include "iconeditor.h" int main(int argc, char *argv[]) { QApplication app(argc, argv); QScrollView scrollView; scrollView.setCaption(QObject::tr("Icon Editor")); app.setMainWidget(&scrollView); IconEditor *iconEditor = new IconEditor; scrollView.addChild(iconEditor); scrollView.show(); return app.exec(); } По-умолчанию, полосы прокрутки отображаются только в том случае, когда подчиненный виджет не умещается в область просмотра (viewport). Однако, следующий код вынудит QScrollView всегда показывать их: scrollView.setHScrollBarMode(QScrollView::AlwaysOn); scrollView.setVScrollBarMode(QScrollView::AlwaysOn); Когда изменяется "идеальный" размер подчиненного виджета, QScrollView автоматически адаптируется под новые условия.

Рисунок 6.10. Изменение размеров QScrollView.


Еще один способ добавить полосы прокрутки к своему виджету -- использовать QScrollView в качестве класса-предка и перекрыть метод drawContents(). Такой подход реализован в классах QIconView, QListBox, QListView, QTable и QTextEdit. Если вашему виджету необходимы полосы прокрутки, то лучшим решением будет породить класс виджета от QScrollView.

Чтобы продемонстрировать это на примере, попробуем написать новую версию класса IconEditor, породив его от QScrollView. Назовем новый класс ImageEditor, поскольку полосы прокрутки дают нам возможность работать с изображениями большого размера.

#ifndef IMAGEEDITOR_H #define IMAGEEDITOR_H #include <qimage.h> #include <qscrollview.h> class ImageEditor : public QScrollView { Q_OBJECT Q_PROPERTY(QColor penColor READ penColor WRITE setPenColor) Q_PROPERTY(QImage image READ image WRITE setImage) Q_PROPERTY(int zoomFactor READ zoomFactor WRITE setZoomFactor) public: ImageEditor(QWidget *parent = 0, const char *name = 0); void setPenColor(const QColor &newColor); QColor penColor() const { return curColor; } void setZoomFactor(int newZoom); int zoomFactor() const { return zoom; } void setImage(const QImage &newImage); const QImage &image() const { return curImage; } protected: void contentsMousePressEvent(QMouseEvent *event); void contentsMouseMoveEvent(QMouseEvent *event); void drawContents(QPainter *painter, int x, int y, int width, int height); private: void drawImagePixel(QPainter *painter, int i, int j); void setImagePixel(const QPoint &pos, bool opaque); void resizeContents(); QColor curColor; QImage curImage; int zoom; }; #endif Заголовочный файл очень похож на предыдущий. Основное отличие состоит в том, что теперь предком является не QWidget, а QScrollView. Другие, менее значимые отличия, мы рассмотрим в процессе описания реализации класса. ImageEditor::ImageEditor(QWidget *parent, const char *name) : QScrollView(parent, name, WStaticContents | WNoAutoErase) { curColor = black; zoom = 8; curImage.create(16, 16, 32); curImage.fill(qRgba(0, 0, 0, 0)); curImage.setAlphaBuffer(true); resizeContents(); } Родительскому конструктору передаются флаги WStaticContents и WNoAutoErase. Они необходимы для области просмотра. Мы не назначаем политики изменения размеров, поскольку значения по-умолчанию (Expanding, Expanding) нас вполне устраивают. В конструкторе ранней версии мы не вызывали updateGeometry(), поскольку начальные размеры виджета могли зависеть от действий менеджеров размещения. Однако в данном случае, нам необходимо задать начальные размеры компонента, что мы и делаем вызовом resizeContents(). void ImageEditor::resizeContents() { QSize size = zoom * curImage.size(); if (zoom >= 3) size += QSize(1, 1); QScrollView::resizeContents(size.width(), size.height()); } Приватная функция resizeContents() вызывает унаследованный метод QScrollView::resizeContents(), передавая ему начальные размеры содержимого QScrollView, который в свою очередь отображает полосы прокрутки, в зависимости от размеров содержимого и области просмотра.

Нам нет необходимости перекрывать функцию sizeHint(). Компонент QScrollView автоматически вычисляет "идеальный" размер, отталкиваясь от размера содержимого области просмотра.

void ImageEditor::setImage(const QImage &newImage) { if (newImage != curImage) { curImage = newImage.convertDepth(32); curImage.detach(); resizeContents(); updateContents(); } } В большинстве случаев, в оригинальном IconEditor, когда необходимо было послать компоненту событие "paint", мы вызывали методы update() и updateGeometry() -- чтобы объявить об изменении "идеальных" размеров. В новой версии, эти вызовы заменены на updateContents() и resizeContents(), соответственно. void ImageEditor::drawContents(QPainter *painter, int, int, int, int) { if (zoom >= 3) { painter->setPen(colorGroup().foreground()); for (int i = 0; i <= curImage.width(); ++i) painter->drawLine(zoom * i, 0, zoom * i, zoom * curImage.height()); for (int j = 0; j <= curImage.height(); ++j) painter->drawLine(0, zoom * j, zoom * curImage.width(), zoom * j); } for (int i = 0; i < curImage.width(); ++i) { for (int j = 0; j < curImage.height(); ++j) drawImagePixel(painter, i, j); } } QScrollViewвызывает функцию drawContents(), чтобы перерисовать содержимое области просмотра. Объект QPainter уже инициализирован, в соответствии с позициями движков в полосах прокрутки, поэтому мы просто "рисуем", точно так же как в обработчике события paintEvent().

Второй, третий, четвертый и пятый аргументы определяют координаты прямоугольника, который должен быть перерисован. Мы могли бы использовать их, чтобы перерисовывать только видимую часть изображения, но для упрощения примера мы перерисовыаем все изображение.

Функция drawImagePixel(), обращение к которой стоит в конце drawContents(), осталась без изменений (см. оригинальную версию), поэтому здесь мы ее рассматривать не будем.

void ImageEditor::contentsMousePressEvent(QMouseEvent *event) { if (event->button() == LeftButton) setImagePixel(event->pos(), true); else if (event->button() == RightButton) setImagePixel(event->pos(), false); } void ImageEditor::contentsMouseMoveEvent(QMouseEvent *event) { if (event->state() & LeftButton) setImagePixel(event->pos(), true); else if (event->state() & RightButton) setImagePixel(event->pos(), false); } События от мыши, направляемые содержимому QScrollView, обрабатываются специальными функциями обработчиками, имена которых начинаются со слова contents. Прежде, чем события будут переданы обработчикам, QScrollView выполнит преобразование координат из системы координат области просмотра в систему координат содержимого, поэтому у нас не возникает необходимости в написании дополнительного кода, выполняющего эти действия. void ImageEditor::setImagePixel(const QPoint &pos, bool opaque) { int i = pos.x() / zoom; int j = pos.y() / zoom; if (curImage.rect().contains(i, j)) { if (opaque) curImage.setPixel(i, j, penColor().rgb()); else curImage.setPixel(i, j, qRgba(0, 0, 0, 0)); QPainter painter(viewport()); painter.translate(-contentsX(), -contentsY()); drawImagePixel(&painter, i, j); } } Функция setImagePixel() вызывается из contentsMousePressEvent() и contentsMouseMoveEvent(), для закрашивания и очистки пикселей. Код функций, по большей части, остался без изменений, за исключением способа инициализации объекта QPainter. В данном случае, мы передаем ему viewport(), в качестве владельца, поскольку рисование будет производиться на поверхности области просмотра, а затем выполняем преобразование системы координат, чтобы учесть положение движков на полосах прокрутки.

Последние три строки, которые работают с QPainter, можно было бы заменить одной строкой:

updateContents(i * zoom, j * zoom, zoom, zoom); Которая сообщила бы QScrollView о необходимости перерисовать один квадратик, который соответствует текущему пикселю. Но поскольку у нас функция drawContents() не оптимизирована, то приходится создавать QPainter и рисовать изображение пикселя самостоятельно.

Если теперь мы попробуем поработать с ImageEditor, то мы практически не заметим разницы с оригинальным IconEditor, вставленным в QScrollView. Однако, другие виджеты, порожденные от QScrollView, используют дополнительные преимущества родительского класса. Например, QTextEdit выполняет перенос текста по словам.

Обратите внимание: вам наверняка придется использовать класс QScrollView, в качестве предка, если размеры отображаемого содержимого очень велики, поскольку некоторые оконные подсистемы не в состоянии отобразить виджеты, размеры которых превышают величину 32767 пикселей.

Еще один важный момент, которого мы не коснулись здесь: мы можем вставлять подчиненные виджеты в область просмотра, вызовом функции addWidget(), и перемещать вызовом moveWidget(). Всякий раз, когда пользователь перемещается по области просмотра, с помощью полос прокрутки, QScrollView автоматически перемещает подчиненные виджеты на экране. (Если подчиненных виджетов слишком много, то прокрутка может существенно замедляться. Чтобы оптимизировать этот процесс, можно вызвать enableClipper(true).) В качестве примера, использующего подобный подход, можно привести web-браузер, в котором большая часть содержимого может отрисовываться непосредственно в области просмотра, но кнопки и поля ввода на формах должны быть представлены в виде виджетов.