Модель рисования GTK+
Модель рисования GTK+ — Подробное описание модели рисования GTK+
Обзор модели рисования
В этой главе подробно описывается модель рисования GTK+. Если вас интересует процедура, которую GTK+ использует для отрисовки своих виджетов и окон, вам следует прочитать эту главу; это будет полезно, если вы решите реализовать свои собственные виджеты. Эта глава также прояснит причины, по которым определенные вещи делаются в GTK+; например, почему вы не можете изменить цвет фона всех виджетов одним и тем же методом.
Окна и события
Программы, работающие в оконной системе, обычно создают прямоугольные области на экране, называемые окнами. Традиционные оконные системы не сохраняют графическое содержимое окон автоматически и вместо этого просят клиентские программы перерисовывать эти окна всякий раз, когда это необходимо. Например, если окно, расположенное под другими окнами, поднимается наверх, то клиентская программа должна перерисовать область, которая была ранее скрыта. Когда оконная система просит клиентскую программу перерисовать часть окна, она отправляет событие экспонирования программе для этого окна.
Здесь под «окнами» подразумеваются «прямоугольные области с автоматическим обрезанием», а не «главные окна приложения». Большинство оконных систем поддерживают вложенные окна, где содержимое дочерних окон обрезается границами их родительских окон. Хотя GTK+ и GDK, в частности, могут работать в оконной системе без такого понятия вложенных окон, GDK создаёт иллюзию работы в такой системе. Главное окно может содержать много под-окон и под-под-окон, например, одно для панели меню, одно для области документа, одно для каждой полосы прокрутки и одно для строки состояния. Кроме того, элементы управления, которые принимают пользовательский ввод, такие как нажимаемые кнопки, скорее всего, также будут иметь свои под-окна.
На практике большинство окон в современных приложениях GTK+ являются клиентскими конструкциями. Только несколько окон (в частности, главные окна) являются нативными, что означает, что они представляют окно в базовой оконной системе, на которой работает GTK+. Например, в X11 это соответствует Окно; в Win32 — HANDLE.
Обычно цикл рисования начинается, когда GTK+ получает событие экспонирования от базовой оконной системы: если пользователь перетаскивает окно поверх другого, оконная система сообщит базовому окну, что оно должно перерисовать себя. Цикл рисования также может быть инициирован, когда сам виджет решит, что ему нужно обновить свой дисплей. Например, когда пользователь вводит символ в виджете GtkEntry, поле ввода запрашивает у GTK+ добавить в очередь операцию перерисовки для себя.
Оконная система генерирует события для нативных окон. Интерфейс GDK к оконной системе преобразует такие нативные события в структуры GdkEvent и отправляет их в слой GTK. В свою очередь, слой GTK находит виджет, соответствующий конкретному GdkWindow и излучает соответствующие сигналы событий на этом виджете.
В следующих разделах описывается, как GTK+ определяет, какие виджеты необходимо перерисовать в ответ на такие события, и как виджеты работают во внутренней структуре с точки зрения ресурсов, которые они используют из оконной системы.
Кадр часов
Все приложения GTK+ управляются основным циклом, что означает, что большую часть времени приложение простаивает внутри цикла, просто ожидая, что что-то произойдёт, а затем вызывает правильное место, когда это происходит. Помимо этого, GTK+ имеет таймер кадров, который даёт «импульс» приложению. Эти часы бьют с постоянной скоростью, которая связана со скоростью кадров вывода (это синхронизировано с монитором через диспетчер окон/композитор). Часы имеют несколько фаз:
События
Обновление
Макет
Рисование
Фазы происходят в таком порядке, и мы всегда будем выполнять каждую фазу до возвращения к началу.
Фаза событий — это длинный промежуток времени между каждым перерисовыванием, когда мы получаем входные события от пользователя и другие события (например, сетевой ввод-вывод). Некоторые события, такие как перемещение мыши, сжимаются, так что мы получаем только одно событие движения мыши за цикл часов.
После завершения фазы событий мы приостанавливаем все внешние события и запускаем цикл перерисовки. Сначала идёт фаза обновления, где все анимации выполняются для расчёта нового состояния на основе оценки времени, когда следующий кадр будет виден (доступен через таймер кадров). Это часто включает в себя изменения геометрии, которые приводят к следующей фазе, макету. Если есть какие-либо изменения в требованиях к размеру виджета, мы рассчитываем новый макет для иерархии виджетов (то есть мы назначаем размеры и позиции).
Затем переходим к фазе рисования, где мы перерисовываем области окна, которые нуждаются в перерисовке.
Если ничего не требует фаз обновления/макета/рисования, мы будем вечно оставаться в фазе событий, так как мы не хотим перерисовывать, если ничего не изменилось. Каждая фаза может запросить дальнейшую обработку в следующих фазах (например, фаза обновления заставит произойти работу макета, а изменения макета вызовут перерисовки).
Существует несколько способов управления часами. На низком уровне вы можете запросить конкретную фазу с помощью gdk_frame_clock_request_phase(), которая запланирует отсчёт часов по мере необходимости, так что он в конечном итоге достигнет запрошенной фазы. Однако на практике большинство вещей происходят на более высоких уровнях:
Если вы выполняете анимацию, вы можете использовать gtk_widget_add_tick_callback(), что заставит часы регулярно бить с обратным вызовом в фазе обновления, пока вы не остановите отсчёт.
Если какие-то изменения состояния приводят к изменению размера вашего виджета, вы вызываете gtk_widget_queue_resize(), что запросит фазу макета и пометит ваш виджет как требующий перемакета.
Если какие-то изменения состояния требуют перерисовки какой-либо области вашего виджета, вы используете обычный набор функций gtk_widget_queue_draw(). Эти функции запросят фазу рисования и пометят область как требующую перерисовки.
Также есть множество неявных триггеров от слоя CSS (который выполняет анимации, изменения размеров и перерисовки по мере необходимости).
Иерархическое рисование
Во время фазы рисования мы отправим единственное событие экспонирования в главное окно. Обработчик событий создаст контекст cairo для окна и излучит сигнал GtkWidget::draw() на нём, который будет распространяться по всей иерархии виджетов в порядке «сзади вперёд», используя обрезание и преобразование контекста cairo. Это позволяет каждому виджету отрисовывать своё содержимое в нужном месте и время, правильно обрабатывая такие вещи, как частичная прозрачность и перекрывающиеся виджеты.
При генерации события GDK также настраивает буферизацию с двойным буфером, чтобы избежать мерцания, которое возникло бы, если бы каждый виджет рисовал себя по очереди. Раздел, посвящённый «Двойной буферизации», подробно описывает механизм двойной буферизации.
Обычно используется только один контекст cairo, который используется во всей перерисовке, а не по одному на каждое GdkWindow. Это означает, что вы должны уважать (и не сбрасывать) существующие клип и преобразования, установленные на нём.
Большинство виджетов, включая те, которые создают свои собственные GdkWindows, имеют прозрачный фон, поэтому они рисуются поверх тех виджетов, которые находятся под ними. Это не было так в GTK+ 2, где тема устанавливала фон большинства виджетов в цвет фона по умолчанию. (На самом деле, прозрачные GdkWindows раньше были невозможны.)
Вся иерархия рендеринга захватывается в стеке вызовов, а не с помощью нескольких отдельных вызовов draw, так что вы можете использовать такие эффекты, как, например, cairo_push/pop_group(), которые повлияют на все виджеты ниже вас в иерархии. Это позволяет, например, иметь частично прозрачные контейнеры.
Прокрутка
Традиционно GTK+ использовал операции самокопирования для реализации прокрутки с нативными окнами. С прозрачными фонами это больше не работает. Вместо этого мы просто помечаем всю затронутую область для перерисовки при использовании этих операций. Это позволяет (частично) прозрачные фоны, а также более точно моделирует современное оборудование, где операции самокопирования проблематичны (они нарушают конвейер рендеринга).
Поскольку вышеуказанное вызывает некоторую нагрузку, мы вводим механизм кэширования. Контейнеры, которые много прокручиваются (GtkViewport, GtkTextView, GtkTreeView и т. д.), выделяют внеэкранное изображение во время прокрутки и рисуют свои дочерние элементы в нём (что возможно, так как рисование полностью иерархическое). Внеэкранное изображение немного больше, чем видимая область, поэтому большую часть времени при прокрутке ему просто нужно нарисовать внеэкранное изображение в другом положении. Это гораздо лучше соответствует современному графическому оборудованию, а также позволяет эффективно использовать прозрачные фоны. Для работы таких контейнеров необходимо определить, когда дочерние виджеты перерисовываются, чтобы можно было обновить внеэкранное изображение. Это можно сделать с помощью новой функции gdk_window_set_invalidate_handler().
Двойная буферизация
Если каждый вызов рисования, сделанный обработчиком draw каждого подвиджета, отправлялся бы напрямую в систему окон, могла бы возникнуть мерцание. Это происходит потому, что области могут перерисовываться многократно: фон, затем декоративные рамки, затем текстовые метки и т.д. Чтобы избежать мерцания, GTK+ использует систему двойной буферизации на уровне GDK. Виджеты обычно не знают, что рисуют в буфер за кадром; они просто выполняют свои обычные команды рисования, а буфер отправляется в систему окон, когда все операции рисования завершены.
Две основные функции в GDK образуют основу механизма двойной буферизации: gdk_window_begin_paint_region() и gdk_window_end_paint(). Первая функция сообщает GdkWindow, чтобы создать временный буфер за кадром для рисования. Все последующие операции рисования для этого окна автоматически перенаправляются в этот буфер. Вторая функция фактически отрисовывает буфер на экранном окне и освобождает буфер.
Автоматическая двойная буферизация
Было бы неудобно, если бы все виджеты вызывали gdk_window_begin_paint_region() и gdk_window_end_paint() в начале и в конце своих обработчиков рисования.
Чтобы упростить задачу, GTK+ обычно вызывает gdk_window_begin_paint_region() перед отправкой сигнала #GtkWidget::draw, а затем вызывает gdk_window_end_paint() после отправки сигнала. Это удобно для большинства виджетов, так как им не нужно беспокоиться о создании своих временных буферов рисования или о вызове этих функций.
Однако некоторые виджеты могут предпочесть отключить автоматическую двойную буферизацию и выполнять действия самостоятельно. Для этого вызовите функцию gtk_widget_set_double_buffered() в конструкторе вашего виджета. Двойная буферизация может быть отключена только для виджетов, имеющих родное окно.
Пример 5. Отключение автоматической двойной буферизации
static void
my_widget_init (MyWidget *widget)
{
...
gtk_widget_set_double_buffered (widget, FALSE);
...
}Когда удобно отключить двойную буферизацию? Как правило, это происходит только в том случае, если ваш виджет отрисовывается таким образом, что различные операции рисования не перекрывают друг друга. Например, это может быть в случае простого просмотрщика изображений: он может просто нарисовать изображение в одной операции. Это не будет иметь место в текстовом редакторе, так как ему необходимо будет нарисовать и перерисовать фон страницы, затем фон выделенного текста и затем сам текст.
Даже если вы отключите двойную буферизацию на виджете, вы все еще можете вручную вызывать gdk_window_begin_paint_region() и gdk_window_end_paint(), чтобы использовать временные буферы рисования.
Виджеты с пользовательским рисованием
Обычно приложения используют предопределенные виджеты в GTK+ и не рисуют дополнительные вещи поверх них (исключением являются GtkDrawingArea). Однако приложениям иногда может быть удобно рисовать непосредственно на определенных виджетах, таких как окна верхнего уровня или ящики событий. В этом случае GTK+ нужно указать, чтобы он не перезаписывал ваше рисование впоследствии, когда окно начинает отрисовывать свои стандартные содержимое.
GtkWindow и GtkEventBox — это два виджета, которые позволяют отключить рисование стандартного содержимого, вызвав gtk_widget_set_app_paintable(). Если вы вызовете эту функцию, они не будут отрисовывать своё содержимое и позволят вам сделать это вместо них.
Поскольку сигнал #GtkWidget::draw выполняет подключённые обработчики пользователя до стандартного обработчика виджета, обычно происходит следующее:
Выполняется ваш собственный обработчик рисования. Он рисует что-то на окне или в ящике событий.
Выполняется стандартный обработчик рисования виджета. Если
gtk_widget_set_app_paintable()не был вызван для отключения отрисовки виджета (это значение по умолчанию), ваше рисование будет перезаписано. Виджет с пользовательским рисованием не будет отрисовывать стандартное содержимое, а сохранит ваше рисование.Выполняется обработчик рисования родительского класса. Так как и
GtkWindowиGtkEventBoxявляются потомкамиGtkContainer, их дочерние элементы без окна будут просить нарисовать себя рекурсивно, как описано в разделе "Иерархическое рисование".
Резюме виджетов с пользовательским рисованием. Вызовите gtk_widget_set_app_paintable() если вы собираетесь отрисовывать собственное содержимое непосредственно на GtkWindow и GtkEventBox. Вам редко нужно рисовать поверх других виджетов, а GtkDrawingArea игнорирует этот флаг, поскольку он предназначен для отрисовки на нём.
© 2005–2020 The GNOME Project
Licensed under the GNU Lesser General Public License version 2.1 or later.
https://developer.gnome.org/gtk3/3.24/chap-drawing-model.html