Wiki

Быстро и без мерцаний

Автор: Reginald Stadlbauer Перевод: Andi Peredri Многие приложения написаны с использованием стандартных Qt-виджетов. Но иногда ни один из них не подходит, и возникает необходимость написать собственный виджет. Создать его не так уж трудно, особенно, если взять за основу такие классы, как QWidget, QFrame или QScrollView, и переопределить paintEvent() или drawContents(). Но существует одна общая проблема, которая может касаться собственных виджетов - это мерцание. В данной статье мы объясним, почему возникает мерцание и как его избежать. > Неофициальный перевод статьи Fast and Flicker-Free выполнен с любезного разрешения Trolltech.

Рисование с мерцанием

Почему возникает мерцание? Давайте посмотрим, что происходит, когда мы выводим текст на виджет:
  1. прямоугольник, в котором будет размещаться текст, заливается цветом фона.
  2. текст отображается в прямоугольнике.
Глаз человека замечает мерцание, потому что существует крошечный период времени между отрисовкой фона (после стирания) и выводом на него текста. Многократное повторение этого приводит к видимому мерцанию. Для решения этой проблемы мы должны разобраться в природе стирания и отрисовки. Наиболее очевидный пример - это случай, когда событие отрисовки генерируется оконной системой для области, лежащей под перемещаемым окном. Но возможны и другие случаи, например, при изменении размеров виджета, прокрутке его содержимого, а также при программном вызове update() или repaint(). Теперь, когда нам стала известна природа мерцания, мы перейдем к решению этой проблемы и в следующих разделах рассмотрим несколько приемов для уменьшения и устранения мерцания.

Статическое содержимое в сравнении с динамическим

Событие отрисовки генерируется для всего виджета при каждом изменении его размеров, т.е. весь фон стирается и все содержимое отрисовывается заново. Современные оконные менеджеры позволяют изменять размер окна синхронно с перемещением мыши (без использования размерной рамки), вследствие чего наблюдается частое мерцание. Но действительно ли нам необходимо перерисовывать весь виджет при изменении его размеров? Если в текстовом редакторе ширина страницы установлена по ширине окна, текст должен переформатироваться при каждом изменении его размеров, и все содержимое будет перерисовываться. Содержимое такого виджета называется динамическим. Если же в текстовом редакторе используется фиксированная ширина страницы (как в большинстве текстовых процессоров), нам не нужно перерисовывать видимую часть виджета при изменении его размеров, поскольку видимая область не изменяется. Это - виджет со статическим содержимым.

Visible Area

С помощью флага WStaticContents мы можем сообщить Qt, что у виджета статическое содержимое, т.е. перерисовываться должны только вновь показываемые области:
    MyWidget::MyWidget(QWidget *parent, const char *name, WFlags flags)
        : QWidget(parent, name, flags|WStaticContents)
    {
        // etc.
    }

Кто заливает фон?

Независимо от типа содержимого во многих случаях нам придется перерисовывать по крайней мере какую-то часть виджета, в результате чего может появиться мерцание. Проблема остается: оконная система заливает окно цветом фона виджета, и затем виджет перерисовывает содержимое. Если процесс перерисовки происходит "медленно", например, если сперва производятся вычисления (т.е. форматирование текста), то задержка между заливкой фона и отрисовкой содержимого может быть достаточно ощутимой для человеческого глаза. И чем дольше эта задержка, тем сильнее будет мерцание. Одним из решений является самостоятельная заливка фона и минимизация промежутка времени между стиранием и отрисовкой. Это потребует дополнительного программирования, зато позволит полностью контролировать процесс отрисовки. Мы можем сообщить Qt, что хотим получить полный контроль над отрисовкой, передав в конструктор QWidget два флага:
  • WRepaintNoErase - этот флаг говорит Qt не заливать фон, когда виджет обновляется, прокручивается и т.п.
  • WResizeNoErase - этот флаг запрещает Qt заливать фон при изменении размеров виджета.
При использовании этих флагов мы должны заливать фон самостоятельно, например, с помощью QPainter::drawRect() или QPainter::fillRect() . (В Qt 3.2. появился новый флаг, комбинирующий два вышеуказанных: WNoAutoErase).

Техника рисования полосами

Итак, мы узнали, как уменьшить число перерисовок у виджетов со статическим содержимым и управлять заливкой фона. Самостоятельная заливка позволяет нам производить любые необходимые вычисления перед заливкой и отрисовкой, а не между ними, и снизить таким образом мерцание. Но здесь остается одна проблема. Рассмотрим процесс отрисовки содержимого текстового редактора:
   заливка фона
   вывод 1-ой строки
   вывод 2-ой строки
   ...
   вывод n-ой строки 
Задержка между заливкой фона и выводом первой строки очень короткая (1 * t, где t - время, необходимое для отрисовки одной строки). Это значит, что при выводе первой строки мерцания практически не будет. Но задержка между заливкой фона и выводом последней строки будет равна n * t, что может значительно превышать величину 1 * t. Мы можем решить эту проблему, заливая фон не весь сразу, а полосами:
   заливка фона 1-ой строки
   вывод 1-ой строки
   заливка фона 2-ой строки
   вывод 2-ой строки
   ...
   заливка фона n-ой строки
   вывод n-ой строки
   заливка оставшегося фона 
Заметьте, что мы должны залить "оставшийся" фон отдельно, так как отображаемые строки (и их фон) могут не покрывать весь виджет.

Painting Stripes

Оставшийся фон редко бывает одним прямоугольником, обычно это - совокупность нескольких прямоугольников, которые мы назовем областью. На первый взгляд, задача вычисления и отрисовки области кажется непростой. Но для решения этой проблемы Qt предлагает использовать класс QRegion, который позволяет объединить совокупность прямоугольников, эллипсов и многоугольников. Чтобы увидеть, как это работает, мы напишем функцию paintEvent(), отображающую массив строк. Начнем с определения области unpainted, которую нужно будет при окончании залить:
    void MyWidget::paintEvent(QPaintEvent *evt)
    {
        // any initialization
        QPainter painter(this);
        QRegion unpainted(evt->clipRegion());
 
	for (int i = 0; i < lines.count(); ++i) {
            Line *line = lines[i];
            painter.fillRect(line->boundingRect(), backgroundColor);
            painter.translate(0, line->boundingRect().y());
            line->draw(&painter);
            painter.translate(0, -line->boundingRect().y());
            unpainted -= line->boundingRect();
        }
Для каждой строки мы заливаем ограничивающий ее прямоугольник, перемещаем точку начала координат в левый верхний угол прямоугольника и выводим строку. Далее вычитаем отрисованный прямоугольник из всей неотрисованной области. После обработки всех строк мы получаем область, которую нужно будет залить цветом фона. Qt не предоставляет функций для заливки области QRegion, но это не является проблемой - мы передадим нашу область в QPainter и зальем ее:
	painter.setClipRegion(unpainted);
	painter.fillRect(unpainted.boundingRect(), backgroundColor);
    }
Такая техника сокращает задержку между заливкой фона и отрисовкой содержимого до фиксированного времени t. Если необходимые вычисления произвести перед началом отрисовки и минимизировать вычисления в цикле, это время станет настолько мизерным, что мерцания не будет совсем. Но что делать, если минимальная задержка, которой нам удалось достичь, все равно слишком большая? Мы можем пойти дальше в технике рисования полосами, рисуя фон и содержимое в отдельности для каждого символа. Однако такой подход может оказаться ресурсоемким. Поэтому более популярна альтернативная техника, при которой приходится жертвовать частью памяти ради быстрой отрисовки без мерцания. Эту технику мы рассмотрим в следующем разделе.

Двойная буферизация

Мерцания можно полностью избежать, если не будет паузы между заливкой фона и отрисовкой содержимого виджета. Для этого нужно создать в памяти изображение с соответствующим цветом фона, поместить на него содержимое и перенести полученное изображение на виджет. Такая техника известна под названием двойной буферизации. Самый простой способ этого добиться - создать в памяти изображение необходимого размера, затем отрисовать все в созданном изображении, после чего поместить его на виджет. Мерцания при этом не будет, но придется задействовать дополнительную память и вычислительные ресурсы. При каждом получении сообщения о необходимости перерисовки вам нужно выделить и освободить память размером: ширина * высота * глубина цвета изображения. В случае, если виджет развернут на весь экран, то памяти может потребоваться очень много, например, 1280 * 1024 * (32 бит/пиксел). Чтобы повторно не перераспределять память, можно сохранить изображение в глобальной области, однако максимально возможный размер виджета останется прежним и затраты памяти не сократятся. Более лучшим решением является сочетание двойной буферизации и техники рисования полосами. Вместо того, чтобы создавать в памяти изображение всего виджета, мы можем сделать это только для одной строки. После заливки изображения в памяти цветом фона мы отрисовываем содержимое и затем помещаем полученное изображение на виджет. И так для каждой строки. Тогда размер изображения в памяти составит: ширина строки * высота строки * глубина цвета, что значительно меньше размера целого виджета. Мы можем хранить изображение в глобальной области памяти, увеличивая его размеры по мере необходимости. Во второй версии написанной нами ранее функции paintEvent() используется двойная буферизация, повторное использование выделенной памяти и техника отрисовки полосами:
    void MyWidget::paintEvent(QPaintEvent *evt)
    {
        // any initialization
        QPainter painter(this);
        QRegion unpainted(evt->clipRegion());
        static QPixmap *doubleBuffer = 0;
        if (!doubleBuffer)
            doubleBuffer = new QPixmap;
        QPainter dbPainter(doubleBuffer);
 
	for (int i = 0; i < lines.count(); i++) {
            Line *line = lines[i];
            doubleBuffer->resize(QMAX(doubleBuffer->width(),
 line->boundingRect().width()),
				 QMAX(doubleBuffer->height(), line->boundingRect().height()));
            doubleBuffer->fill(backgroundColor);
            line->draw(&dbPainter);
            painter.drawPixmap(0, line->boundingRect().y(), *doubleBuffer, 0,
 0,
                               line->boundingRect().width(),
 line->boundingRect().height());
            unpainted -= line->boundingRect();
        }
        painter.setClipRegion(unpainted);
        painter.fillRect(unpainted.boundingRect(), backgroundColor);
    }
Мы объявляем статический указатель на изображение и, если он равен нулю (при первой отрисовке), то выделяем память. Затем вместо непосредственной отрисовки виджета мы формируем изображение в памяти необходимого размера. Код отрисовки аналогичен предыдущему, потому что для отрисовки Qt предоставляет API, независимый от устройств. Напоследок остается лишь скопировать полученное изображение на виджет с помощью drawPixmap(). Для этой цели мы можем также использовать функцию bitBlt(), но drawPixmap() работает быстрее в случае, если контекст для рисования уже создан. Эта техника полностью устраняет мерцание при небольших сопутствующих затратах памяти. Такой подход используется в Qt во многих стандартных виджетах, включая QTextEdit, QListView и QMenuBar. В действительности в Qt память используется более оптимально. Вместо двойной буферизации всех виджетов Qt создает глобальные изображения для каждого типа виджета, независимо от их общего количества.

Рисование без затирания

При использовании полос подразумевается, что содержимое виджета может быть разбито на отдельные элементы, например, такие как строки в текстовом редакторе или отдельные пункты в списке. Такая разбивка присутствует во многих виджетах, но даже если ее нет, использование техники полос все еще возможно. Эта техника может быть использована для рисования любых областей, если их предварительно разбить на узкие (100-200 пикселов высотой) полосы. Для каждой полосы, в свою очередь, заливается фон, а затем отрисовываются все элементы, лежащие на этой полосе (либо непосредственно, либо с использованием техники двойной буферизации). Такой подход особенно полезен в ситуациях, когда элементы могут взаимно перекрываться, например, как в QIconView. Если в этом случае использовать первый из рассмотренных нами методов рисования полосами, то элементы станут затирать друг друга.

Overlapping Items

Например, если элемент beta перекрывает элемент alpha, то при использовании первого из рассмотренных нами методов рисования полосами произойдет следующее: заливается фон элемента alpha, затем отрисовывается элемент alpha, после чего заливается фон элемента beta, затирая при этом частично или полностью элемент alpha, и затем отрисовывается сам элемент beta. При использовании второго метода рисования полосами такой проблемы не возникнет. Сначала полоса, на которой расположены элементы, заливается фоном, затем отрисовываются поочередно элементы alpha и beta. Элемент beta может частично или полностью закрыть элемент alpha, но фон элемента beta никогда не перекроет изображение элемента alpha. Замечание: второй метод эффективен только тогда, когда вы можете быстро определить, какие части содержимого виджета лежат на данной полосе.

Заключение

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