WPF layout: Measure and Arrange



    A general idea of ​​what the 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 is assigned to the Layout System and is carried out in 2 stages (measure and arrange) and that WPF is retained mode graphic system, unlike ordinary Immediate and what are the advantages of this approach.

    However, when reading msdn, a number of questions arise that are not answered in the documentation, and you can only guess what is happening. For example, what happens if a child control in the measure stage requests for itself a size that exceeds the availableSize passed to it? Or - how, if necessary, to implement the MeasureOverride and ArrangeOverride methods correctly so that the written code does not contradict accepted conventions on how the Measure and Arrange steps should be performed ? Does the result obtained in the Measure stage influence the Arrange stage and rendering, or is it only the Arrange call that affects the rendering , and Measure- a purely informational stage?

    Let's try to understand in more detail what is happening behind the scenes.

    Retained mode system


    First, let's recall what retained mode graphic system is , which is WPF. This is just 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, it is WPF that is responsible for ensuring that every window and control is redrawn if necessary. The programmer is no longer worried about handling events like WM_PAINT , as was the case with the Win32 API and WindowsForms, but simply sets the rendering method for control if necessary, and WPF will determine when and which piece needs to be revalidated. Programmatically, it looks like this: instead of each time a WM_PAINT message arrivesdetermine the regions that need to be redrawn and perform this procedure, the programmer once indicates the sequence of commands needed to draw the control. This is done in the OnRender method . The OnRender method is called by the WPF system:
    • If you still don’t know how to draw this control
    • If some DependencyProperty has changed with the FrameworkPropertyMetadataOptions.AffectsRender flag (e.g. Button.Content ) or with other flags that implicitly lead to AffectsRender
    • If the control has been explicitly marked for revalidation (by calling InvalidateMeasure , InvalidateArrange or InvalidateVisual )


    image

    The OnRender method is called with the DrawingContext argument , in which we pull the supposedly drawing methods of the DrawEllipse type , etc. Allegedly, because in fact rendering does not occur, 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 lies the answer to a common 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 you need to reposition itself and redraw it? After all, we just changed the value of the property. And the following happens: the button text affects the rendering, and is marked with the corresponding 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 that, WPF will do a rendering update 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 the markup to be updated using the UpdateLayout () method , thenRenderSize will become the one that matches the changed text. Actually with this mechanism, by the way, and the priority associated Dispatcher.Invoke - among the available priorities have Priority.Render , which means that when you call a delegate to this priority, he will be executed on a par with the procedures performed to render the elements.

    Measuring


    Why is this so important? Because the steps Measure and Arrange , within which the positioning of the elements occurs, work in a similar way. They are called once, and the MeasureIsValid and ArrangeIsValid flags are set for the control . After this, the calls to Measure and Arrange return control immediately, doing nothing. In order to force the control to recount these things, one must again either change some DependencyProperty with the AffectsMeasure / AffectsArrange flag , or explicitly reset the flags by calling InvalidateMeasure / InvalidateArrange. Documentation also says Measure revalidationautomatically entails the re-validation of Arrange , however, this is already quite obvious. In general, the first conclusion is this: if your control has a certain property that, when changed, can change the size and / or placement of child controls, 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 to revalidate the control, then it’s better not to do this, but do a DependencyProperty with the given valueChangedCallback , in which, if necessary, call InvalidateMeasure / InvalidateArrange manually so as not to overload 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. It took me personally a lot of time to get a taste of this topic. I even had to delve into the source code of WPF, since they are available.

    Measure and Arrange


    The documentation for Measure is written in such a way that it seems that this is a purely informational stage and should not affect the display. Everything seems to be logical - the parental control polls the children, finds out from them how much they would like to occupy, well, it decides specifically to whom how much space to cut off, and calls Arrange . In general, this is the case at the PresentationCore level (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 descendants, and FrameworkElement is already defined in the PresentationFramework assembly.

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

    public void Measure (Size availableSize) - a method that, given the availableSize, determines the desired sizes and sets them to this.DesiredSize. The description of the method says that the resulting DesiredSize can be> availableSize, but this is not the case for FrameworkElement descendants.
    The essence of the Measure method is to do the following things:
    1. Call Measure recursively for all VisualChild elements (otherwise the IsMeasureValid flag will not be set for them and the child element will not be able to be drawn). Measure must be called at least once . Measure can be called repeatedly (for example, you can first call Measure with the argument Size = double.PositiveInfinity to determine the full size of the control), and the last call to Measure must be made with the dimensions that will actually be used to draw this child control.
    2. Prepare the state of the control so that it fits in 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 that exceeds availableSize, then the grid cell with fixed sizes will not know what to do. It seems to have fixed sizes, but no - the child is banging her fist on the table and wants more! Therefore, the documentation phrase that availableSize is a '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, a listbox when calling Measure , if it understands that it does not fit in dimensions, determines that a scrollbar appears. And when you call Arrange with larger sizes, the scrollbar will still remain. And if on the contrary, Measure was performed with PositiveInfinity , and Arrange - with real dimensions, everything that goes beyond arrangeRect is simply cut off.

    By the way, why exactly FrameworkElement ? Has the light converged on it? It turns out really converged, and FrameworkElement overrides UIElement.MeasureCore andUIElement.ArrangeCore with the sealed modifier, that is, all descendants of the FrameworkElement (all controls, windows, etc.) will not be able to change the behavior of MeasureCore and ArrangeCore if they wish. They can only leave wishes - for this the MeasureOverride and ArrangeOverride methods are made. And now in MeasureOverride availableSize is really a soft constraint, and you can quite legally return a value that exceeds the value of the argument.

    public void Arrange (Rect finalRect) - just tells the control its location (x, y) and the size of the rectangle. Everything that goes beyond these limits will be circumcised.

    The following relationship exists between Measure and Arrange - ideally, the last call to Measuremust accept a Size that matches the size the next time Arrange is called . If so, then the control will be drawn perfectly. If the condition is not met, then problems may occur. 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) moves the nura right (the scrollbar slides to the left along with the borders, but does not get cropped), and from the bottom it is cropped.

    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 own placement logic. Typically, the MeasureOverride algorithm should perform the following steps:
    1. Estimate the full size of children - by a recursive call to Measure with the parameters Size.Width and Size.Height = double.PositiveInfinity
    2. Rate your own full size according to the size of the children
    3. If we get into availableSize, then we return the value of our own full size
    4. Otherwise, we may need to call Measure again in children, but not with PositiveInfinity, but with specific values, to fit in the availableSize allocated to us. The specific implementation of this step depends on the placement logic that we want to implement.
    5. We return availableSize as DesiredSize if we manage to keep within availableSize, well, or the minimum value exceeding availableSize, which will allow our control to be fully rendered

    protected virtual Size ArrangeOverride (Size finalSize) - and here we just call the Arrange method for each child element with the corresponding borders and position.

    Note that a value greater than availableSize can be returned to MeasureOverride ! But if you do this and check the DesiredSize control, then we will be surprised that DesiredSize = availableSize. That is, someone ignored our result and wrote the value of the Measure argument there . However, when ArrangeOverride is further called as an argument, we again magically get our value, which we returned from MeasureOverride . What's happening? And this is what happens.



    If FrameworkElement.Measure is called with infinite constants, then regardless of what our MeasureOverride returns, FrameworkElement.MeasureCore will crop it and set DesiredSize <= availableSize. And our DesiredSize caches at home, subsequently passing it to us in ArrangeOverride. This is because the FrameworkElement ensures that when you call Measure, it will fit in the piece allocated to it, even if it has to crop our content.

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

    And when implementing the heirs of FrameworkElement, we don’t have to worry about clipping in such situations - he will do everything for us. And if we need a panel that processes DesiredSize > availableSize, then we are either doing something wrong, or we will have to go down one level to UIElement , 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 put the button in a grid cell with smaller sizes (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 methods from the WPF sources (UIElement and FrameworkElement classes) related to the considered material. Pieces with comments detailing the essence in sufficient detail.

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

    UPD: afsherman tells you that WPF sources are optional. Who installed ReSharper can use the built-in option to download the source by Ctrl + Click on the name of the class / method / property, etc.

    Also popular now: