📜 ⬆️ ⬇️

WPF layout: Measure and Arrange



A general idea of ​​what a WPF Layout System is can be obtained from msdn ( 1 , 2 ). It says that the controls form a Visual-tree, that each of the controls has its own specific rectangle, within which it is drawn, that the definition of these rectangles rests on the Layout System and is performed in 2 steps (measure and arrange) and that WPF is retained mode graphic system, as opposed to ordinary Immediate and what are the advantages of this approach.

However, reading msdn raises a number of questions to which there are no answers in the documentation, and one can only guess what is happening. For example, what happens if a child control at the measure stage requests a size larger than the availableSize given to it? Or - how, if necessary, to implement the MeasureOverride and ArrangeOverride methods correctly, so that the written code does not contradict accepted agreements on how the Measure and Arrange stages should be performed? Does the result obtained at the Measure stage affect the Arrange stage and the drawing, or does only the call to Arrange affect the drawing, and the Measure the purely informational stage?
')
Let's try to understand in more detail what is happening behind the scenes.

Retained mode system


To begin with, let's remember what a retained mode graphic system is , for example, WPF. This is simply an approach to drawing graphics, in which the responsibility for identifying areas requiring redrawing and performing this redrawing is transferred to the graphics system. That is, WPF is responsible for ensuring that every window and control is redrawn if necessary. The programmer is no longer soared about handling WM_PAINT events, as was the case in the Win32 API and Windows Forms, but simply sets the draw method for the control, if necessary, and WPF will determine when and which piece it will need to revalidate. Programmatically, it looks like this: instead of each time the WM_PAINT message arrives , it is necessary to determine the regions that need to be redrawn and to perform this procedure, the programmer once specifies the sequence of commands necessary for drawing the control. This is done in the OnRender method. The OnRender method is called by the WPF system:


image

The OnRender method is called with the DrawingContext argument, in which we pull the supposedly drawing methods like DrawEllipse, etc. Allegedly, because in fact there is no rendering, and all our calls are added to the command queue, and will be used when WPF decides that the control needs to be redrawn.

Actually, here is the answer to a frequent question - let's say we have a button on which something is written, we change the text of this button - but at what point and how does it determine that we need to reposition ourselves and redraw? After all, we just changed the value of the property. And the following happens: the text of the button affects the rendering, and is marked with the appropriate flag, so changing the value of this property causes the button to be marked as “dirty”, that is, one that requires redrawing. And soon after this, WPF will update the rendering for it. This will not happen immediately, but only when WPF has time for this. That is, if immediately after the replacement you get RenderSize , then it will not change. But if you force a forced update of the markup using the UpdateLayout () method, then RenderSize will become the one that corresponds to the modified text. Actually, Dispatcher.Invoke is connected with this mechanism, by the way - Priority.Render is among the available priorities, which means that when a delegate is called with this priority, it will be executed along with the procedures performed for element rendering.

Measuring


Why is this so important? Because the Measure and Arrange stages, in which elements are positioned, work in a similar way. They are called once, and the control has the MeasureIsValid and ArrangeIsValid flags . After that, the Measure and Arrange calls return immediately, doing nothing. In order to force the control to recalculate these things, you must again either change some DependencyProperty with the AffectsMeasure / AffectsArrange flag , or explicitly reset the flags by calling InvalidateMeasure / InvalidateArrange. The documentation also suggests that the revalidation of a Measure automatically entails a revalidation of Arrange , however, this is already quite obvious. In general, the first conclusion is this: if there is a certain property in your control that can change the size and / or placement of child controls when changing, then you should make it DependencyProperty and set the AffectsMeasure / AffectsArrange flag . If not every change in the value of this property leads to the need for revalidation of the control, then it is better not to do this, but to do a DependencyProperty with the given valueChangedCallback , in which, if necessary, manually call InvalidateMeasure / InvalidateArrange not to burden WPF with unnecessary work.

Now consider these two methods in terms of software design. That is - what are their areas of responsibility and input / output data. This is probably the most important thing in understanding how the WPF Layout System works. I personally took a lot of time in order to enjoy this topic. It was necessary even to rummage in source codes WPF, the blessing they are accessible.

Measure and Arrange


The documentation for the Measure is written in such a way that it seems as if this is a purely informational step and should not affect the display. It seems to be all logical - the parent control polls the children, learns from them how much they would like to occupy space, well, it decides for whom how much space to cut off, and calls Arrange . In general, at the PresentationCore level this is the case (there UIElement contains empty virtual methods MeasureCore and ArrangeCore), but we are most likely interested in more specific behavior, the behavior of FrameworkElement and its heirs, and the FrameworkElement is already defined in the PresentationFramework assembly.

I will try to formulate as clearly and correctly as I can. If there are inaccuracies and errors, correct, make changes.

public void Measure (Size availableSize) - a method that, according to the specified availableSize, determines the desired sizes and sets them to this.DesiredSize. In the description of the method, it is written that the resulting DesiredSize may be> availableSize, but this is not so for the heirs of FrameworkElement.
The essence of the Measure method is to do the following things:
  1. Call recursively Measure for all VisualChild elements (otherwise, the IsMeasureValid flag will not be set and the child element will not be drawn). At least once Measure must be called. Measure can be called more than once (for example, you can first call Measure with the argument Size = double.PositiveInfinity to determine the full size of the control), with the last call to Measure should be made with the sizes that will actually be used to draw this child control.
  2. Prepare the state of the control to fit into the availableSize dimensions. This is the reason that the last call to Measure should determine the actual dimensions of the element. The reason is because if the control sets DesiredSize to a value greater than the availableSize, the grid cell with fixed dimensions will not know what to do. It seems that she has a fixed size, but no - the child knocks her fist on the table and wants more! Therefore, the documentation clause stating that availableSize is 'soft constraint' is strictly speaking incorrect in the context of FrameworkElement .


The second point is very important. Indeed, in the Measure phase, preparation for drawing takes place. For example, the listbox when calling Measure , if it understands that it does not fit in the dimensions, determines that a scrollbar will appear. And when you call Arrange with a larger size, the scrollbar will still remain. And if on the contrary, Measure was performed with PositiveInfinity , and Arrange - with real sizes, all that goes beyond the limits of arrangeRect - is simply cut off.

By the way, why the FrameworkElement ? Is the light on it? It turns out that it really came together, and FrameworkElement overrides UIElement.MeasureCore and UIElement.ArrangeCore with the sealed modifier, that is, all heirs of FrameworkElement (all controls, windows, etc.) will not change the behavior of MeasureCore and ArrangeCore. They can only leave wishes - for this, the MeasureOverride and ArrangeOverride methods are made. And here in MeasureOverride, availableSize is really a soft constraint, and it is quite legal to return a value that exceeds the value of the argument.

public void Arrange (Rect finalRect) - simply indicates the control of its place (x, y) and the size of the rectangle. Anything that goes beyond these limits will be cut off.

The following relationship exists between Measure and Arrange - ideally, the last call to Measure should take the Size, which is the same size as the subsequent call to Arrange . If so, then the control will be rendered perfectly. If the condition is not met, then problems are possible. That is, it is possible that everything will be rendered correctly, and maybe not quite. For example, the listbox in this situation (when arrangeSize <measureSize) on the right shifts the naura (the scrollbar slides to the left along with the borders, rather than being cut off), and from the bottom it is cut off.

Now it remains to consider the methods MeasureOverride and ArrangeOverride .

MeasureOverride and ArrangeOverride


protected virtual Size MeasureOverride (Size availableSize) is designed to enable developers to create their own control panels with their placement logic. Usually, the MeasureOverride algorithm should perform the following steps:
  1. To estimate the full size of children - by a recursive call Measure with the parameters Size.Width and Size.Height = double.PositiveInfinity
  2. Rate your own full size considering the size of the children.
  3. If we fit into availableSize, then we return the value of our own full size.
  4. Otherwise, we may need to re-call Measure in children, but not with PositiveInfinity but with specific values ​​in order to meet the availableSize allocated to us. The concrete implementation of this stage depends on the placement logic that we want to implement.
  5. We return availableSize as DesiredSize , if it turned out to be within the availableSize, well, or the minimum value exceeding the availableSize, which will allow our control to be fully rendered

protected virtual Size ArrangeOverride (Size finalSize) - and here we simply call the Arrange method for each child element with the appropriate bounds and position.

Note - in MeasureOverride, you can return a value greater than the availableSize! But if you do this and test DesiredSize controls, then we will be surprised that DesiredSize = availableSize. That is, someone ignored our result and wrote down the value of the Measure argument. However, if we further call ArrangeOverride , our argument will magically return to our value, which we returned from MeasureOverride .

What's happening? And that's what happens.

If FrameworkElement.Measure is called with finite constraints, regardless of what our MeasureOverride returns, FrameworkElement.MeasureCore will cut it and set DesiredSize <= availableSize. And our DesiredSize will cache it, later transferring it to ArrangeOverride. This happens because FrameworkElement ensures that when you call Measure, it will fit into the piece allotted to it, even if it has to trim our content.

Otherwise, it would be the case that grid cells with specific width / height values ​​would disperse on controls that return DesiredSize > availableSize. And so it turns out that FrameworkElement retains the real requirements for DesiredSize, and when the time comes to Arrange , it calls our ArrangeOverride method with the DesiredSize value that we returned to MeasureOverride . And we in ArrangeOverride arrange the children as we want.
After that, FrameworkElement.ArrangeCore , in the context of which our ArrangeOverride is called , performs the clipping of our content, and we see in the grid part of our control. And which part depends on the properties of Horizontal / VerticalAlignment , etc. But the content is virtually drawn as we wanted, because our DesiredSize was saved and transferred to ArrangeOverride .

And when we implement the heirs of FrameworkElement, we don’t have to sweat about clipping in such situations - he will do everything for us. And if we need a panel that processes DesiredSize > availableSize, then we either do something wrong, or we will have to go down a level to UIElement 's, which does not finalize (seal) MeasureCore and ArrangeCore .

To test all this, you can create a Button inheritor and return a fixed size to MeasureOverride , and place the button in a grid cell with a smaller size (say, 50x50). The button will be cropped.

protected override Size MeasureOverride(Size constraint) {
return new Size(80, 80);
}


This feature is described in http://social.msdn.microsoft.com/

In conclusion, I would like to bring the code to some of the methods from WPF sources (UIElement and FrameworkElement classes) related to the considered material. Pieces with comments, enough detailing the essence.

The whole source WPF can be downloaded from here (download .Net - 4).

UPD: afsherman suggests that the WPF source code download optional. Who has ReSharper installed can use the Ctrl + Click source download option built into it on the class / method / property name, etc.

Source: https://habr.com/ru/post/135990/


All Articles