MeasureOverride and ArrangeOverride. What is really AvailableSize/DesiredSize? - wpf

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.
Looking at the WPF source code, the MeasureOverride() method accepts an availablesize and then passes this to all the children. It then returns the largest width and largest height of the resulting desiredsize properties in the children. Shouldn't it divide the available space between the children? I would think that it would iterate over the children and then measure the first, then subtract the resulting desiredsize from the total availablesize so that the next child had less space to occupy. As I read the WPF, WrapPanel.MeasureOverride does not appear to set a desiredsize that it would need to fit all the children. It just gives the DesiredSize that any ONE of the children will fit in to.
Due to the nature of the wrappanel, I would expect that for a vertically oriented stackpanel a restriction in height would result in a wider DesiredSize (to fit more columns). Since a restriction in height affects the desired size of a wrap panel, doesn't this logic then belong in the MeasureOverride method? Why is the stacking then only reflected in the ArrangeOverride method?
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?

How to properly implement 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".
1. What are the components that affect control's layout?
It is important to be aware that there are numerous parameters that affect the size and arrangement of a control:
Contents (i.e. child controls)
Explicit width and height
Margins
Horizontal and vertical alignment
Layout transform
Layout rounding
Something else I might have overlooked
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.
2. How to implement 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?
Input
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:
If parent control calls Measure(100x100), and our control has margin set to 20 (on each side), the value of availableSize will be 60x60.
If parent control calls 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).
Output
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.
Relation to DesiredSize property value
Size 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:
Often we need to access child's desired size in ArrangeOverride, so calling Measure(Size) again would trigger redundant measure pass on child control (and its descendants).
It is possible to invalidate arrange without invalidating measure, which triggers layout pass skipping the measure phase and going straight to the arrange phase. For example, if we reorder controls in a StackPanel, the total size of the child controls does not change, only their arrangement.
Summary
This is how measure phase looks like from perspective of our control:
Parent control calls Measure(Size) on the control.
MeasureCore pre-corrects the provided size to account for margins etc.
MeasureOverride is called with adjusted availableSize.
We do our custom logic to determine the desired size of our control.
Resulting desired size is cached. It is later used to adjust the finalSize parameter of ArrangeOverride. More on that later.
The returned desired size is clipped not to exceed the availableSize.
Clipped desired size is post-corrected to account for margins etc. (step 2. is reverted).
Value from step 7. is set as value of 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.
3. How to implement ArrangeOverride?
The purpose of arrange phase in layout pass is to position all child controls in relation to the control itself.
Input
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.
Output
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:
Border
Canvas
Decorator
DockPanel
Grid
StackPanel
VirtualizingStackPanel
WrapPanel
Possibly others...
4. Epilogue
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.
Disclaimer
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.

#grx70 answer is great and amazingly detailed. However, there is much more to know about the WPF layouting system and I wrote a whole article about it on CodeProject: Deep Dive into WPF Layouting and Rendering
Here is an overview how Properties and overwriting of MeasureOverride(), ArrangeOverride() and OnRender() work together to produce and use DesiredSize and RenderSize (which is by the way the exactly same value like ActualHeight and ActuelWidth).
For a detailed description see the article.

Related

Is there any way to merge margins in WPF?

In HTML, for a table (at least), one can style the element so that margins are merged. I.e. two adjacent rows both have top and bottom margins of 10, so the gap between these two rows will be 20. When their margins are merged, the gap is only 10.
Is there any way to achieve this in WPF?
There is no easy way to do this, and when you look at how WPF handles layout you'll see why, but also see that there is a hard way if you're up to it.
As you can see from the WPF source code for FrameworkElement, the MeasureCore sealed override method (which prevents us from overriding it further) adds the element's margins before returning with its desired size. Annoyingly, they seem to have a BypassLayoutPolicies option which would prevent this, but for reasons possibly ranging from short-sighted to sadistic, they made this internal so it's not an option. Thus the fully margined size of the element is what always winds up being assigned to DesiredSize, which is basically what all layout panels (StackPanel) etc. naively use to arrange the items during ArrangeOverride.
But therein is the solution, if you're willing to subclass all the panels and override their MeasureOverride and ArrangeOverride. Knowing the margin values of two adjacent children you could collapse their respective right/left or bottom/top margins together during the measure and arrange passes, by just subtracting the duplicative portion of the margin from their `DesiredSize's.
But if you're gonna subclass anyway, another option, which I would consider cleaner and a better practice overall, is to add a Spacing property to your subclassed Panels, which exists in the WinUI version of StackPanel. This would require you to either ensure that your child items all have zero margins, or to subtract their actual margin values out from their DesiredSize's. You would then add this spacing instead between the items during the arrange pass.
StackPanel is easy enough to extend this way as both passes are very simple. Grid, unfortunately, is a lot more complicated. VirtualizingStackPanel is probably out of the question... So this is by no means a silver bullet, but given how common StackPanel is, even just extending that control would cover a whole lot of ground and lead to much cleaner layouts IMO.
If I had more reputation I'd mark this as a duplicate of Is it possible to emulate border-collapse (ala CSS) in a WPF ItemsControl? . It looks as if this is kludgeable (for a ListBox at least) using a DataTrigger to check the value of preceding entries and set borders accordingly for null values.

Why does ArrangeOverride recalculate the final size

For myself, It would be logic if MeasureOverride calculates the size of the FrameworkElement and ArrangeOverride just positions the child elements. But ArrangeOverride does not only arrange the child elements. It does also calculate the size of the FrameworkElement again which I do not understand. Why can't MeasureOverride calculate the final size and thats it?
Because your element is potentially not the only one on the screen.
Layout is not that simple. WPF has to work out the actual physical space it has to use, then calculate how much space each element wants, scale the requested amount if it can, then apply it. Additionally some elements may want to make changes based on the exact amount of space allocated.
This previous answer of mine gives it to you as an analogy.
If you implement your own Panel, with your own layout algorithm and in your ArrangeOverride() call
child.Arrange(rect1)
with rect1 being different than the child's DesiredSize then the system may arbitrarily decide to ignore rect1 size and use something different. I've seen DesiredSize being used, I'm not sure if it's always the case. I'd say that this is something resembling a bug :)
If rect1 is calculated in the MeasureOverride() pass of the Panel then a workaround for this behavior is to call Measure() on the child for a second time passing size of rect1, so DesiredSize of the child is recalculated before the Arrange() pass.

WPF: Custom control layout

I'm working on a custom control in WPF that implements OnRender by calling a visit function with itself as the visitor. The control implements the visitor interface that draws lines, circles, etc. as appropriate. There are no child controls.
This all works, when the control renders I can see the primitives being rendered from my OnRender call.
However, what I'm struggling with is controlling the layout resulting from this. The Image control does exactly what I want to do. How do I replicate that behavior? Specifically:
If the user does not specify width or height, I want to set the rendered width/height of the control (either in my constructor or from another function that controls the layout).
If the user sets width or height to a specific value, I want to transform my drawing such that the aspect ratio of the control and drawing is preserved.
I'm trying to use the MeasureOverride functions to implement the behavior I want, but I'm not getting any results. My control is laid out with zero width/height, and then all my drawings get drawn on top of neighboring controls. Here is what I'm trying so far, hopefully this illustrates what I'm attempting to do:
protected override Size MeasureOverride(Size constraint)
{
SymbolLayout symbol = new SymbolLayout(this);
component.LayoutSymbol(symbol);
Point b1 = MapToPoint(symbol.LowerBound);
Point b2 = MapToPoint(symbol.UpperBound);
return new Size(b2.X - b1.X, b2.Y - b1.Y);
}
I'm not even sure that MeasureOverride is the right function to be using to accomplish this...
You also need to override the ArrangeOverride method. Layout in WPF is performed by a pair of recursive operations, Measure and Arrange. Measuring simply asks the visual tree what the required size of all the child controls is. This is done recursively, so if you had child controls, you would need to call Measure on all of your child elements as part of your Measure Override. Arranging is the second step in which the framework tells the control how much space it actually gets. Like Measuring, this is done recursively, and normally requires calling Arrange for each child control if there are any.

WPF: find how much space a control needs

In my application I have an area in the main window that at any time can contain one of several different controls.
This controls are generated at runtime and their contents can vary depending on underlying data, so I do not know beforehand how much space they'll take up.
What I want to know is: is there a way to determine at runtime how much space a control needs in order not to be "cut off" or need a scroll? ie: how much space does it need to be COMPLETELY visible?
I tried the "DesiredSize" property and it kinda works, but not always: if the control has been used already (it has already a size) it returns it's last used size rather than the correct one, even if I call "InvalidateMeasure()".
Any ideas??
Call Measure on the control. Give it infinite space as the available size for the calculation. Then check the DesiredSize to get the needed width (and/or height).

How does WPF solve cyclic dependent sizing?

I'm trying to figure out how come WPF doesn't go crazy when I set a window to resize to its content and then set its content to take all the available space... Any idea?
There are two passes in the layout system - measure and arrange.
First in the measure pass each child is given a proposed available size, and this propagates down the tree. This sets each child's DesiredSize property.
In the next, arrange pass, the DesiredSize is taken into account, but the parent has the final word on how much actual available size it will give to a child, and the parent places each child accordingly, informing it of the actual size it gets, and so on down the tree.
Also, consider several imaginable corner cases:
The child demands double.Infinity desired size: explicitly not allowed, raises an exception
The child sets its HorizontalAlignment to "Stretch": the parent doesn't offer infinite actual size
Window set to SizeToConent, child set to "Stretch", no other constraint set:
Window first offers infinite measure size
Child demands the size measured by its children (text width, margins, etc) - not infinite size
Arrange: window sets its size to a system-defined minimum or as desired by child, whichever is greater (can't be infinite)
The child stretches to fit the size determined in previous step
It's not that difficult: if the window is set to stretch to match its content, then it doesn't have a static size, so it assumes default dimensions. Going down the visual tree, the control wants to stretch to fill the window -- it doesn't force any dimensions, but instead accepts whatever size the window has (the default one).
My guess is that elements always have a size (default one if none specified above them in the hierarchy) and the mention whether that size is fixed or negotiable.
I'm only guessing here, but it's just to prove that the situation is not as bad as it seems. With a good update strategy in place, the elements can adjust in relation to each other in a sane manner.

Resources