-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathLayoutSystem.txt
More file actions
152 lines (128 loc) · 19.7 KB
/
LayoutSystem.txt
File metadata and controls
152 lines (128 loc) · 19.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
У элемента управления перед рендерингом должны быть известны следующие компоненты:
1. Слот, в котором размещается графическое представление. Представлен свойством RenderSlotRect -
rectangle относительно холста родительского элемента. Выставляется при вызове Arrange(Rect finalSize)
собственно этому finalSize и равен.
2. Размеры виртуального холста, в которое будет отрисован элемент управления.
Заданы свойством RenderSize, которое определяется при ревалидации Arrange-фазы.
Виртуальный холст может быть больше или меньше слота.
3. Инфа о том, как расположить виртуальный холст в слот, в рамках которого необходимо остаться.
Если размеры виртуального холста превышают размеры слота, отведенного для элемента управления, то
часть виртуального холста будет обрезана. Что именно будет обрезано - определяется комбинацией свойств
HorizontalAlignment, VerticalAlignment и Margin.
То есть по имеющимся свойствам RenderSlotRect, RenderSize и HorizontalAlignment, VerticalAlignment, Margin
можно вычислить положение виртуального холста в слоте, предоставленном родительским элементом управления.
А для рисования необходимо вычислить:
а) Смещение виртуального холста относительно слота - свойство ActualOffset (может быть отрицательным)
б) Прямоугольник, который бы отрезал лишнее из виртуального холста - эта штука появляется при заданном Margin.
(LayoutClip).
Margin определяет то, насколько сдвинуто "изображение" контрола относительно его же виртуального холста.
Поэтому клиппинг может происходить в следующих местах:
1. При применении Margin к виртуальному холсту при рендеринге контрола в виртуальный буфер
2. При применении RenderSlotRect + ActualOffset - так мы узнаем, как разместить в виртуальном холста парента наш
контрол.
Механизм обновления лайаута для конкретно взятого контрола:
1. Допустим, у него были установлены какие-то значения свойств, и допустим, что система лайаута уже вызывалась
(положим, был вызван Measure+Arrange, но не был вызван Render)
2. Кто-то изменил значение одного из свойств, которое влияет на отрисовку контрола. Для упрощения пока не будем разделять
разные типы side-эффектов (типа одно влияет только на Arrange, другое - на Measure+Arrange). Пока просто - необходимо
ревалидировать лайаут контрола полностью.
3. Все просчитанное что было в контроле (DesiredSize, RenderSize, LastMeasureArg, RenderSlotRect(по сути LastArrangeArg),
LayoutClip и ActualOffset) мы сохраняем в Last-values с пометкой о том, насколько валиден был лайаут для этого состояния контрола
(в рассматриваемом случае - были валидны все свойства, поскольку и Measure, и Arrange отработали).
4. Помечаем контрол как требующий обновления лайаута.
5. Рендерер начинает апдейт и если при пересчете лайаута будет обнаружено, что какие-то свойства не изменились (например DesiredSize, RenderSize)
то обновление будет оптимизировано в соответствии с логикой (к примеру, в некоторых случаях не нужно вызывать обновление лайаута дочерних контролов).
По поводу "приоритетности" значений, возвращаемых методом MeasureOverride и жестко установленных Width/Height.
Приоритетнее - то, что больше. То есть, если контрол затребовал 10 пикселей в ширину, а Width вы установили в 5,
то Width будет проигнорирован. Обратное тоже верно. Таким образом, контрол должен быть готов к тому, что его
ActualWidth и ActualHeight могут быть больше тех, которые он вернул в качестве Desired Size. Но меньше
требуемых они быть не могут. Если у родительского контрола не хватает места, то на ActualWidth и ActualHeight
это не повлияет - просто при рендеринге контрол будет обрезан.
Описание для разработчиков контролов и панелей.
MeasureOverride и ArrangeOverride - по аналогии с WPF, позволяют определить собственные размеры, а также
размеры и логику размещения дочерних элементов управления. Первый метод вызывается из метода Measure
класса Control и отвечает на следующий вопрос - "Вот у меня есть свободное место availableSize.
Скажи, сколько места тебе нужно для размещения вместе с твоими дочерними элементами?" и возвращает
UnclippedDesiredSize. Этот возвращаемый размер может быть равен availableSize, а может быть больше или меньше его.
Если он превышает availableSize, то Measure запомнит это, но DesiredSize установит = availableSize.
ArrangeOverride принимает finalSize - размер слота, который родительский элемент выделил этому контролу.
Слот уже не будет изменён, и теперь задача нашего контрола (или панели) - занять выделенное пространство.
Обычный контрол просто занимает некую часть этого слота и возвращает размер реально используемого пространства
Как правило, возвращается тот же finalSize, но можно вернуть как бОльший размер, так и меньший.
В случае, если возвращаемое значение больше переданного finalSize - рендеринг контрола будет обрезан.
Если же возвращаемое значение меньше переданного finalSize - контрол займет лишь часть площади слота.
Панель же в дополнение к этому должна разместить дочерние элементы вызовами Arrange(finalRect).
Итак, finalSize - это размер слота (RenderSlotRect), а возвращаемое значение становится размерами
виртуального буфера контрола, куда будет производиться рендеринг (RenderSize и соответственно ActualWidth, ActualHeight).
FAQ
1. Как сделать элемент управления, который занимает всё доступное пространство ?
Кажется, что для этого нужно переопределить MeasureOverride таким образом, чтобы он возвращал
переданное ему значение availableSize в качестве результата, а своё содержимое адаптировал соответствующим
образом. Однако это неправильное решение. Дело в том, что метод MeasureOverride может быть вызван с
availableSize, равным бесконечности. Но вернуть бесконечность мы не можем согласно протоколу размещения,
да и смысла это иметь не будет. Но можно действовать следующим образом: в MeasureOverride наш контрол
должен возвращать ровно столько, сколько потребуется для отрисовки нашего элемента в минимальном размере,
в ArrangeOverride возвращать finalSize (не уменьшая его до минимально необходимого, как мы это сделали
в MeasureOverride), а метод Render реализовать таким образом, чтобы он зависел от ActualWidth и ActualHeight
(собственно, так всегда надо поступать при реализации Render). И после этого задать значения по умолчанию
для свойств HorizontalAlignment и VerticalAlignment - Stretch. Работает это так: родительский контрол
опрашивает нас, сколько нам нужно места для счастья, при этом в качестве availableSize может быть передан
как конечный размер, так и бесконечность. Мы возвращаем минимально необходимые для элемента управления
размеры. Если родительский контрол имеет слот, больший чем наш предпочтительный размер, а HorizontalAlignment
(и/или VerticalAlignment) = Stretch то в ArrangeOverride придёт размер уже увеличенного слота - всё
пространство, которое доступно. И мы его полностью занимаем, возвращая finalSize. Если бы Alignment был бы
Left, Right или Center, то система размещения бы автоматически определила, что размер слота увеличивать
не нужно, и в ArrangeOverride попало бы значение DesiredSize. Итак, мы вернули finalSize, а значит, заняли
всё предложенное пространство, вуаля, осталось только отрендерить содержимое во время вызова Render.
Именно так работает Button, можно поисследовать его поведение отладчиком для полного понимания этого
механизма.
2. Как обрабатывать бесконечный availableSize в MeasureOverride ?
С этим вопросом сталкивается каждый программист при необходимости реализации своих панелей (менеджеров
размещения). С отдельными контролами проще, поскольку в них не нужно размещать дочерние элементы, достаточно
лишь определить свой собственный минимально необходимый размер (по значению свойств) и вернуть его. И то,
какой нам передали availableSize, в этом случае неважно. Если же хочется занять всё свободное место,
можно действовать точно так же (см. предыдущий вопрос). С панелями такой фокус не проходит, потому что
панель должна выполнить размещение дочерних элементов в имеющемся пространстве обычно по какому-то алгоритму.
Например, рассмотрим абстрактную панель, которая размещает элементы вокруг центра доступного пространства.
Если на входе у нас availableSize, равный бесконечности, то разместить элементы в центре этой бесконечности
мы не можем. Но мы можем прикинуть, сколько минимально пространства нужно для комфортного размещения
всех дочерних контролов, и вернуть именно этот размер, разместив дочерние элементы вокруг центра внутри
этого размера. А после этого подождать второго вызова MeasureOverride,
с уже конкретными размерами (который обязательно должен присутствовать при использовании такой панели,
не можем же мы засовывать панель, требующую ограниченного пространства, в StackPanel или ScrollViewer), и
уже выполнить фактическое размещение так, как надо. Для этого нужно будет явно проверить availableSize
на бесконечность. Например, панель Grid при вызове MeasureOverride с бесконечным availableSize игнорирует
Star-столбцы и строки, считая их ширину/высоту как Auto. А при вызове с конечным availableSize размещение
производится уже так, как надо. Поэтому, кстати, если Grid поместить в ScrollViewer, то все строки и столбцы
будут измерены по размеру содержимого, и изменение размеров ScrollViewer на Grid не влияет.
3. Как работают события LayoutInvalidated и LayoutRevalidated ? Зачем они нужны ?
Эти события вызываются строго после прохода системы размещения в вызове Renderer.UpdateRender().
Во время работы UpdateRender() контролы, layout validity которых сбрасывается в LayoutValidity.None
(из-за того, что они либо были явно добавлены в Invalidation Queue, либо косвенно связаны с одним из
контролов в этой очереди - например, все дочерние контролы), сохраняются в список. Потом, при обновлении
размещений, аналогичным образом сохраняются контролы, чьё LayoutValidity было восстановлено в Render.
После того, как проход системы размещения завершён, все события вызываются в том порядке, в котором они
произошли. Для оптимизации в список этих контролов не включаются те, у которых нет подписчиков на эти события.
Зачем всё это надо - для построения цепочек вызовов, каждый из которых зависит от результатов предыдущего с
размещением. Например, нужно открыть несколько окон, каждое из которых позиционируется относительно
размещения предыдущего.
4. Как работает отслеживание Z-Order в движке системы размещения / рендеринга ?
Эта штука отслеживает изменения в Z-Order дочерних контролов. Это необходимо, т.к. такие изменения
могут происходить без вызова Invalidate(), и движок должен корректно применить изменения на экран.
К примеру, обновить ту часть экрана, на которой выведено вперёд окно, часть которого была скрыта
под другим окном. Если этого не делать, то в этом месте останется мусор от старого окна, до тех пор,
пока не будет вызван Invalidate() для него или для одного из его родителей. Работает всё
следующим образом:
- В Control нет возможности напрямую менять порядок следования контролов. Это можно сделать только
при помощи функции SwapChildsZOrder(). При вызове эта функция добавляет контрол в список
Renderer.zOrderCheckControls.
- В каждом Control есть поле LastOverlappedRect, которое показывает, какая часть контрола
в текущий момент перекрыта соседями. Имеет смысл рассматривать только перекрытие соседями, т.к.
все остальные случаи обрабатываются автоматически (изменяется размещение, и изображение контролов с
изменённым размещением будет обновлено полностью, включая все дочерние).
LastOverlappedRect поддерживается системой размещения в актуальном состоянии на каждом проходе.
- В конце метода UpdateLayout() добавлен код, который который перебирает zOrderCheckControls,
для каждого контрола проверяя все его дочерние - не изменился ли их OverlappedRect ?
Если да, и изменился так, что бОльшая часть дочернего контрола стала видима - контрол
добавляется в список renderingUpdatedControls. Их содержимое после этого в методе
FinallyApplyChangesToCanvas будет выведено на экран.