Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WPF

Deep Dive into WPF Layouting and Rendering

5.00/5 (18 votes)
13 Feb 2022Public Domain13 min read 36.4K  
Did you ever wonder what WPF is doing under the hood to place your control on the screen?
WPF documentation can be very minimalistic and confusing, especially when it comes to how layouting and rendering really work and what you need to do in your code to make best use of it. This article is for you if you are interested in technical details.

WPF Control Inheritance

Before we go into the details of the layouting (i.e., measuring how much screen space a control needs and placing it on the screen), we need to briefly discuss the objects which get layouted. In this article, I will call them control, which can be anything that inherits from FrameworkElement like Control or TextBox or ...

Object -> DispatcherObject -> DependencyObject -> Visual -> UIElement -> FrameworkElement -> Control -> TextBoxBase -> TextBox

The following classes are important for the layouting discussion:

  • DispatcherObject: is very simple and has only the property Dispatcher, which controls the execution of the WPF UI thread. The Dispatcher also manages in which sequence controls get measured, arranged and rendered.
  • DependencyObject: an object that can own WPF dependency properties
  • Visual: is an abstract class holding the properties needed to render (paint) a control to the screen
  • UIElement: contains among other stuff the properties deciding if layouting and rendering is needed for this control. Some layout related properties: DesiredSize, IsArrangeValid, IsMeasureValid, RenderSize
  • FrameworkElement: contains among other stuff logical tree (Parent, LogicalChildren) and visual tree information (VisualChildrenCount, GetVisualChild()) and some sizing information (ActualHeight, Height, VerticalAlignment, Margin, Padding).

Overview WPF Layouting Process

WPF uses two threads, one for rendering and one for managing the UI. The WPF code you write runs on the UI thread. The rendering thread runs behind the scene, filling the GPU (graphics processing unit) with rendering instructions. There is hardly any documentation about the rendering thread, but luckily we don't need to know any details about it.

WPF using only one thread for all our code has the huge advantage that there are no multithreading problems, i.e., two threads trying to change the same data at the same time.

The execution of the UI code is not linear but runs in three phases, which get repeated again and again:

Phase 1

At the beginning of a Window lifetime, code calling its constructors or executing XAML creates a tree consisting of Visuals, where a Window might be the root of everything else the user can see in the window. Once construction has completed, phase 2 starts. At a later time, phase 1 might run again due to some events raised because a property has changed, the mouse was clicked, the window size changed and for many other reasons. The advantage of finishing all phase 1 code before phase 2 starts is that if different code parts require that a control gets layouted again, that layouting gets performed only once instead immediately each time it gets requested.

Phase 2

Once WPF has done everything that wants to run in phase 1, it measures all controls by travelling the tree and asking every child in the tree to measure itself.

Phase 3

  1. Once the children are measured, they get arranged, i.e., each parent tells its children where they should get painted and how much space they get.
  2. If necessary, once the child is arranged, it gets a DrawingContext, which it can use to write drawing instructions (= rendering).

Image 1

The UI thread is controlled by the Dispatcher. The Dispatcher selects activities based on their priority. Activities from phase 1 have the highest priority (Normal). Even if a method running in phase 1 marks a Visual for layouting, this doesn’t happen immediately. Instead, that Visual gets assigned to the MeasureQueue in the ContextLayoutManager. Once the Dispatcher has processed all activities with Normal priority, it tells its ContextLayoutManager to process the Visuals in the MeasureQueue (phase 2). Once all of them are processed, the Dispatcher tells its ContextLayoutManager to process all the Visuals in the ArrangeQueue (phase 3, DispatcherPriority.Render).

Initiating Layouting From Your Own Code

WPF often knows when a control needs to be layouted again, for example, if the user changes the size of the Window in which that control is located. But sometimes, only your code knows that your control needs to be layouted again, for example, because the user clicked on a Button or a timer ticked. Your control might still have the same size, so strictly speaking no new measurement and arranging is needed, just a rendering, alas, WPF does not let you request only rendering.

A list of UIElement methods your code can call to force layouting and rendering:

  • InvalidateMeasure(): Adds the Visual to the MeasurementQueue and sets its MeasureDirty flag. The measurement executes at a later time (Phase 2). If then the DesiredSize of that Visual changes, that Visual's Arrange() will be called after all Visuals are measured.
  • InvalidateArrange(): Adds the Visual to the ArrangeQueue and sets its ArrangeDirty flag. The arranging executes at a later time (Phase 3). If then the RenderSize of that Visual changes, UIElement.Arrange() calls immediately OnRender() as part of its code. Note that when using InvalidateArrange(), no rendering is performed if your control's size doesn't change, even your control's content might have changed.
  • InvalidateVisual(): One would expect that your code could tell WPF that only rendering of your Visual is needed, but no layouting. Alas, WPF doesn't allow you to do that. OnRender() only gets called from within UIElement.Arrange(). Therefore InvalidateVisual() has to add the Visual to the ArrangeQueue and also sets the RenderingInvalidated flag.

DependencyProperty and Layouting

WPF uses its own property system. When defining a WPF property, one can indicate if changing that property's value requires a new layouting of that FrameworkElement. For example, the Width property of a FrameworkElement is defined like this:

C#
public static readonly DependencyProperty WidthProperty =
  DependencyProperty.Register(
    "Width",
    typeof(double),
    typeof(FrameworkElement),
    new FrameworkPropertyMetadata(
      Double.NaN,
      FrameworkPropertyMetadataOptions.AffectsMeasure,
      new PropertyChangedCallback(OnTransformDirty)),
      new ValidateValueCallback(IsWidthHeightValid));

For our discussion, interesting is FrameworkPropertyMetadataOptions.AffectsMeasure, which indicates that if the property value changes, the FrameworkElement needs to get measured. That FrameworkElement gets added automatically to the MeasureQueue and its MeasureDirty flag set.

There are actually five different FrameworkPropertyMetadataOptions.AffectsXxx flags:

  1. AffectsArrange
  2. AffectsMeasure
  3. AffectsParentArrange
  4. AffectsParentMeasure
  5. AffectsRender

Interestingly, a property value change cannot only force a new layout of the control it belongs to, but also a new layouting of the control's Parent. Another interesting point is that a property value change can signal that just rendering is needed, which is impossible with the InvalidateXxx() methods.

Who Calls UIElement.Measure() and UIElement.Arrange()?

A WPF container like Window or Grid are the parents of other WPF controls. It is the parent who calls Measure() and Arrange() of its child or children, because only the parent knows how much screen space is available for a child. Therefore, it doesn't make sense to measure or arrange a child before its parent. When layouting is needed, WPF has to start at the root of the tree, for example, with Window and traverse from there through the complete tree, or, if only a part of the tree needs layouting, start at the root of that part tree. Finding the right controls for layouting is the job of the ContextLayoutManager.

ContextLayoutManager

I discovered the ContextLayoutManager only while writing this article, meaning my following description might not be 100% accurate. On the other hand, the ContextLayoutManager does its job behind the scene and we don’t really need to know all the details.

The Dispatcher has a queue of all activities the UI thread might need to execute, like reacting to a mouse click or timer tick or opening a window or doing layouting or… These activities are sorted by priority, one reason being to ensure that all normal UI activities are completed before the layouting starts. The Dispatcher also owns a ContextLayoutManager, which basically maintains a queue for all Visuals needing to be measured and a queue for all Visuals needing to be arranged. Once the time has come for measurement (=Phase 2, all higher priority actions have been completed and at least part of the tree needs layouting), the ContextLayoutManager searches the Visual closest to the root which needs measuring and calls its Measure(), which then calls all the Measure() of its children, which do the same thing for all their children and so on. Each Visual which gets measured removes itself from the MeasureQueue. Once that subtree is completely measured, some Visuals might be left in the MeasureQueue, because they belong to another subtree and ContextLayoutManager starts to process that tree, until MeasureQueue is empty. Upon which the same thing happens again for the ArrangeQueue (=Phase3).

It can happen that only measurement gets executed, but no arranging, if no size has changed and no arranging is explicitly required by setting the ArrangeDirty flag.

It can happen that only arranging gets executed, but no measurement, if measurement had been executed earlier on and no new measurement is required.

Phase 2: Measurement

C#
YourControl.Measure()
UIElement.Measure()

  FrameworkElement.MeasurementCore()

    virtual FrameworkElement.MeasureOverride()
    override YourControl.MeasureOverride()

A parent calls YourControl.Measure() which is actually a call of UIElement.Measure() which later calls FrameworkElement.MeasureCore() which later calls FrameworkElement.MeasureOverride() which is overridden by your YourControl.MeasureOverride() where your control has the code measuring itself.

Note: The following is pseudo code, which tries to show only the essentials. The actual code is much more complicated. The pseudo code includes code from different classes, like when the UI code calls MeasureCore() which is overridden by FrameworkElement.MeasurementCore().

You can find the actual source code here:

UIElement

FrameworkElement

C#
void UIElement.Measure(avialableSize){
  //1)
  if (IsNaN(availableSize) throw Exception
  if (Visibility==Collapsed) return;
  if (avialableSize==previousAvialableSize && !isMeasureDirty) return;

  //2)
  ArrangeDirty = true;
  desiredSize = MeasureCore(availableSize);

    virtual Size UIElement.MeasureCore(Size availableSize) {}
    override Size FrameworkElement.MeasureCore(availableSize){
       //3)
       frameworkAvailableSize = availableSize - Margin;
       if (frameworkAvailableSize>UIElement.MaxSize)
         frameworkAvailableSize = UIElement.MaxSize;
       if (frameworkAvailableSize<UIElement.MinSize)
         frameworkAvailableSize = UIElement.MinSize;
       desiredSize = MeasureOverride(frameworkAvailableSize);

        virtual Size FrameworkElement.MeasureOverride(Size availableSize){}
        override Size YourControl.MeasureOverride(Size constraint){
            //4) here you write the code measuring the control
            return desiredSize;
         }

       //5)
       desiredSize += Margin
       if (desiredSize<UIElement.MinSize)
         desiredSize = UIElement.MinSize;
       if (desiredSize>UIElement.MaxSize)
         desiredSize = UIElement.MaxSize;
       return desiredSize;
    }

  //6)
  if (IsNaN(desiredSize) throw Exception
  if (IsPositiveInfinity(desiredSize) throw Exception
  MeasureDirty = false;     
}

The pseudo code might look a bit confusing. The important points are:

  1. If your control is collapsed or the available space has not changed since the last Measure() call, the code returns immediately. Measure() will not force to run Arrange() later, if the available space has not changed.
  2. If the available size has changed, Arranged() will later get executed, even if desiredSize does not change !
  3.  FrameworkElement subtracts Margin from availableSize. The FrameworkElement makes sure that the availableSize is smaller equal than your control’s MaxSize. MaxSize is not really a WPF property. I use it as shorthand for MaxWidth and MaxHeight. The FrameworkElement makes sure that the availableSize is greater equal than your control's MinSize.
  4. To do your own measurement of your control, override MeasureOverride() in your control.
  5. Once your code has returned the desiredSize, FrameworkElement adds Margin to desiredSize. FrameworkElement makes sure that desiredSize is greater equal MinSize and smaller equal MaxSize. If Width or Height are defined, they overrule MinSize and desiredSize becomes whatever Width or Height dictate.
  6. Finally, UIELement throws exceptions when desiredSize is not a number or infinite.

Interesting here is that the input availableSize can be infinite, but the output desiredSize cannot be infinite. Infinite size as input makes sense for example when the parent is a ScrollViewer. In a ScrollViewer, every child can use as much space as it wants. Basically, when the parent gives the child infinite space, it asks the child how much space it wants, without any limitation.

If the child doesn’t know how much space it should use, it could just return constraint, but it is also possible to return new Size(0,0). During Arrangement, the parent might not care about how much space the child asked for and give it even more space than required. An example would be when the child's Alignment is set to Stretch, in which case the parent gives all available space, even the child asked for less.

Note that FrameworkElement takes care of Margin, MaxSize and MinSize, but does nothing for Border and Padding, which means you have to handle Padding in your code, if your control should support it.

Phase 3: Arrangement (Including Rendering)

C#
YourControl.Arrange()
UIElement.Arrange()

  FrameworkElement.ArrangeCore()

    virtual FrameworkElement.ArrangeOverride()
    override YourControl.ArrangeOverride()

  virtual UIElement.OnRender()
  override YourControl.OnRender ()

A parent calls YourControl.Arrange() which is actually a call of UIElement.Arrange() which later calls FrameworkElement.ArrangeCore() which later calls FrameworkElement.ArrangeOverride() which is overridden by your YourControl.ArrangeOverride() where your control has the code measuring itself.

If the control’s RenderSize has changed or its RenderingInvalidated flag is set, UIElement also calls UIElement.OnRender() which is overridden by your YourControl.OnRender() where your control has the code creating the rendering instructions.

C#
void UIElement.Arrange(Rect finalRect) {
  //1)
  if (IsNaN(finalRect) throw Exception
  if (IsPositiveInfinity(finalRect) throw Exception
  if (Visibility==Collapsed) return;
  //2)
    if (MeasureDirty){
    UIElement.Measure(PreviousConstraint)
  }
  //3)
  if (finalRect==previousFinalRect && !isArrangeDirty) return;
  UIElement.ArrangeCore(finalRect);

    virtual void UIElement.ArrangeCore(Rect finalRect){}
    override void FrameworkElement.ArrangeCore(Rect finalRect){
      //4)
      Size arrangeSize = finalRect.Size;
      arrangeSize = Math.Max(arrangeSize - Margin, 0);
      if (Alignment!= Stretch) arrangeSize = desiredSize;
      if (arrangeSize>MaxSize) arrangeSize = MaxSize;
      RenderSize = ArrangeOverride(arrangeSize);

        virtual Size UIElement.ArrangeOverride(Size finalSize){}
        override Size YourControl.ArrangeOverride(Size arrangeBounds) {
           //5) here, you write the code measuring the control
         }

      //6) this is followed by some complicated code doing clipping and LayoutTransform
    }
   
  ArrangeDirty = false;
  //7)
  if ((sizeChanged || RenderingInvalidated || firstArrange){
    DrawingContext dc = RenderOpen();
     OnRender(dc);

    virtual void UIElement.OnRender(DrawingContext drawingContext)
    override void YourControl.OnRender(DrawingContext drawingContext) {
      //9) here you write the code rendering the control
    }
  }
  1. For measurement, it is ok if the parent gives infinite space. But if the parent passes infinite space for arrangement, an Exception is thrown. If your control is collapsed, Arrange() just returns.
  2. In theory, a control should always be measured before arranged. But when Arrange() notices that Measure() was not properly called before, i.e., the MeasureDirty is still set, Arrange() calls Measure() immediately.
  3. If the available space has not changed and isArrangeDirty is not set, Arrange() returns.
  4. arrangeSize might also be adjusted because of clipping, which is not shown in the pseudo code. arrangeSize might also be adjusted because of LayoutTransform, which is not shown in the pseudo code.
  5. Arrange(Rect finalRect) receives a Rect, which contains the coordinates X, Y and a Size, whereas ArrangeOverride(Size finalSize) receives only finalRect.Size. When the child arranges itself, it doesn't know its own X and Y coordinates within its Parent. To do your own arrangement and rendering of your control, override ArrangeOverride() in your control.
  6. Once your control has finished arranging and writing rendering instructions, UIElement.Arrange() continues with some clipping and layout transformation calculations.
  7. If the rendering size has changed or the RenderingInvalidated flag is set, UIElement.Arrange() calls UIElement.OnRender() which is overridden in your control. There, you place your rendering instructions.

Note that if alignment is set to stretch, the parent gives the child all available space and not just the desiredSize required by the child.

Meaning of Different Size Properties

When I started with WPF, I often wrongly assumed that Control.Width will tell me what is the width of the control on the screen, which not true at all:

  • Width: A property that can be used to suggest what width your control should have, could be set in XAML, default is double.Nan (i.e., not used).
  • Height: A property that can be used to suggest what height your control should have, could be set in XAML, default is double.Nan (i.e., not used).
  • DesiredSize: The size your control requested at the end of MeasureOverride(), plus Margin gets added and Width/Height, MinWidth/MinHeight and MaxWidth/MaxHeight get enforced, if they are defined. DesiredSize is used by parents for arrangement.
  • RenderSize: The size the parent gives your control for rendering. RenderSize is different from DesiredSize because a) DesiredSize is with Margin while RenderSize is without and b) parent might decide to give child a different size than requested.
  • ActualWidth: is actually RenderSize.Width
  • ActualHeight: is actually RenderSize.Height

Future Articles

I was writing a new article explaining how you can write your own user control from code behind, hosting other controls and writing directly to the screen, when I realised that explaining how layouting and rendering work in WPF requires its own article, which is the one you are presently reading. However, this article does not explain how to write the code for layouting and rendering in your control, for that, you have to wait for my next article. Which will be followed by a third explaining how to test and debug your control, which is rather complex.

Update: I published the "third" article (testing) already and the "second" comes later:

Must Read for WPF Developers: WPF Control Test Bench

Writing the last article might take a while. In the meantime, I recommend you to read some other highly rated WPF articles I wrote here on CodeProject:

History

  • 13th February, 2022: Initial version
  • 4th March, 2022: Improved description of DesiredSize and RenderedSize
  • 8th March, 2022: Corrected error in Layouting.png

License

This article, along with any associated source code and files, is licensed under A Public Domain dedication