I've been stuck for two days trying to understand the layout principles of WPF.
I have read a bunch of articles and explored the wpf source code.
I know that measure/measureoverride method accepts a availablesize and then sets the DesiredSize property. This makes sense. It recursively calls to the children and ask them to set their respective desired size.
There are two things (at least) I don't understand. Let us consider a WrapPanel.
I think I have some fundamental misunderstanding about the mechanics of these two method.
Can anybody give me a verbal description of DesiredSize and/or AvailableSize that makes this implementation make sense?
MeasureOverride
and ArrangeOverride
?As I think this is the actual question you're asking, I will try to give you as much as I know about this topic.
Before we begin, you may want to start with reading Measuring and Arranging Children on MS Docs. It gives you a general idea of how the layout process works, although it doesn't really offer any information on how you should actually implement MeasureOverride
and ArrangeOverride
.
Note: For the sake of brevity, from here on out, whenever I say "control", I really mean "any class deriving from FrameworkElement
".
It is important to be aware that there are numerous parameters that affect the size and arrangement of a control:
Luckily, the only component we need to worry about when implementing custom layout, are child controls. This is because the other components are common to all controls, and are handled by the framework completely outside of MeasureOverride
and ArrangeOverride
. And by completely outside I mean that both input and output are adjusted to account for those components.
In fact, if you inspect the FrameworkElement
API, you'll notice that measurement procedure is split into MeasureCore
and MeasureOverride
, the former taking care of all the required corrections, and that in fact you never call them directly on the child controls - you call Measure(Size)
which does all the magic. Same goes to ArrangeCore
and ArrangeOverride
.
MeasureOverride
?The purpose of measure phase in layout pass is to provide feedback to the parent control on the size that our control would like to be. You may think of it as a hypothetical question:
Given that much available space, what is the minimal space you need to accommodate all your contents?
It goes without saying that this is (usually) required to determine the size of the parent control - after all, we (usually) measure our child controls to determine the size of our control, don't we?
From docs:
The available size that this element can give to child elements. Infinity can be specified as a value to indicate that the element will size to whatever content is available.
The availableSize
parameter tells us how much space do we have at our disposal. Be aware though that this might be an arbitrary value (including infinite width and/or height), and you should not expect to be given the exact same amount of space upon arrangement phase. After all, the parent control may call Measure(Size)
on our control many times with whatever parameters, and then completely ignore it in arrangement phase.
As mentioned before, this parameter is already pre-corrected. For example:
Measure(100x100)
, and our control has margin set to 20
(on each side), the value of availableSize
will be 60x60
.Measure(100x100)
, and our control has width set to 200
, the value of availableSize
will be 200x100
(hopefully it will become clear why as you continue reading).From docs:
The size that this element determines it needs during layout, based on its calculations of child element sizes.
The resulting desired size should be minimal size required to accommodate all contents. This value must have finite width and height. It typically is, but is not required to be, smaller than availableSize
in either dimension.
This value affects the value of DesiredSize
property, and affects the value of finalSize
parameter of subsequent ArrangeOverride
call.
Again, the returned value is subsequently adjusted, so we should not pay attention to anything but child controls when determining this value.
DesiredSize
property valueSize returned by MeasureOverride
affects, but not necessarily becomes the value of DesiredSize
. The key thing here is that this property is not really intended to be used by the control itself, but rather is a way of communicating the desired size to parent control. Note that Measure
does not return any value - parent control needs to access DesiredSize
to know the result of the call. Because of that, its value is actually tailored to be viewed by parent control. In particular, it is guaranteed not to exceed the original size passed as parameter of Measure
, regardless of the result of child's MeasureOverride
.
You may ask "Why do we need this property? Couldn't we simply make Measure
return the size?". This I think was done for optimization reasons:
ArrangeOverride
, so calling Measure(Size)
again would trigger redundant measure pass on child control (and its descendants).StackPanel
, the total size of the child controls does not change, only their arrangement.This is how measure phase looks like from perspective of our control:
Measure(Size)
on the control.MeasureCore
pre-corrects the provided size to account for margins etc.MeasureOverride
is called with adjusted availableSize
.finalSize
parameter of ArrangeOverride
. More on that later.availableSize
.DesiredSize
.
Possibly this value is clipped again not to exceed the original size passed as Measure(Size)
parameter, but I think that should already be guaranteed by step 6.ArrangeOverride
?The purpose of arrange phase in layout pass is to position all child controls in relation to the control itself.
From docs:
The final area within the parent that this element should use to arrange itself and its children.
The finalSize
parameter tells us how much space do we have to arrange child controls. We should treat it as final constraint (hence the name), and do not violate it.
Its value is affected by the size of rectangle passed as parameter to Arrange(Rect)
by the parent control, but also, as mentioned, by the desired size returned from MeasureOverride
. Specifically, it is the maximum of both in either dimension, the rule being that this size is guaranteed not to be smaller than the desired size (let me re-emphasize this pertains to the value returned from MeasureOverride
and not the value of DesiredSize
). See this comment for reference.
In the light of that, if we use the same logic we used for measurement, we do not need any extra precautions to ensure we'll not violate the constraint.
You may wonder why there's this discrepancy between DesiredSize
and finalSize
. Well, that's what clipping mechanism benefits from. Consider this - if clipping was disabled (e.g. Canvas
), how would the framework render the "overflowed" contents unless they were properly arranged?
To be honest, I'm not sure what will happen if you violate the constraint. Personally, I would consider it a bug if you report a desired size and then are not able to fit in it.
From docs:
The actual size used.
This is the frontier of my ignorance, where knowledge ends and speculation begins.
I'm not really sure how this value affects the whole layout (and rendering) process. I know this affects the value of RenderSize
property - it becomes the initial value, which is later modified to account for clipping, rounding, etc. But I have no idea what practical implications it might have.
My personal take on this is that we had our chance to be finicky in MeasureOverride
; now it's time put our words into actions. If we're told to arrange the contents within given size, that's exactly what we should do - arrange child controls within finalSize
, not less, not more. We don't have to tightly cover the whole area with child controls, and there may be gaps, but these gaps are accounted for, and are part of our control.
Having said that, my recommendation would be to simply return finalSize
, as if saying "That's what you instructed me to be, so that's what I am" to the parent control. This approach seems to be notoriously practiced in stock WPF controls, such as:
I guess that's all I know on the subject, or at least all I can think of. I bet you dollars to donuts there's more to it, but I believe this should be enough to get you going and enable you to create some non-trivial layout logic.
Provided information is merely my understanding of the WPF layout process, and is not guaranteed to be correct. It is combined from experience gathered over the years, some poking around the WPF .NET Core source code, and playing around with code in a good old "throw spaghetti at the wall and see what sticks" fashion.