Модель рисования GTK+
Модель рисования GTK+ — Подробное описание модели рисования GTK+
Обзор модели рисования
В этом разделе подробно описывается модель рисования GTK+. Если вас интересует процесс, который GTK+ использует для отрисовки своих виджетов и окон, вам следует прочитать этот раздел; это будет полезно, если вы решите реализовать свои собственные виджеты. Этот раздел также прояснит причины, по которым определенные вещи выполняются в GTK+; например, почему вы не можете изменить цвет фона всех виджетов одним и тем же методом.
Окна и события
Программы, работающие в системе окон, обычно создают прямоугольные области на экране, называемые окнами. Традиционные системы окон не сохраняют графическое содержимое окон автоматически и вместо этого запрашивают клиентские программы перерисовывать эти окна всякий раз, когда это необходимо. Например, если окно, расположенное ниже других окон, поднимается наверх, то клиентская программа должна перерисовать область, которая ранее была скрыта. Когда система окон запрашивает клиентскую программу перерисовать часть окна, она отправляет событие экспонирования программе для этого окна.
Здесь «окна» означают «прямоугольные области с автоматической обрезкой», а не «главные окна приложения». Большинство систем окон поддерживают вложенные окна, где содержимое дочерних окон обрезается границами родительских окон. Хотя GTK+ и GDK, в частности, могут работать в системе окон без такого понятия вложенных окон, GDK создает иллюзию работы в такой системе. Например, главное окно может содержать множество дочерних окон и поддочерних окон: одно для панели меню, одно для области документа, одно для каждой полосы прокрутки и одно для строки состояния. Кроме того, элементы управления, которые получают ввод пользователя, например, нажимаемые кнопки, скорее всего, также будут иметь свои собственные дочерние окна.
На практике большинство окон в современных приложениях GTK+ являются клиентскими конструкциями. Только немногие окна (в частности, главные окна) являются родными, что означает, что они представляют окно из основной системы окон, на которой работает GTK+. Например, в X11 это соответствует Window, в 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.22/chap-drawing-model.html