Untergeordnete Seiten
  • Layout system

  Wiki Navigation

    Loading...


 Recently Updated


 Latest Releases

 MediaPortal 1.32
            Releasenews | Download
 MediaPortal 2.5
            Releasenews | Download


Table of Contents

Note: This page contains very technical, internal design description. A normal user, even a normal MP2 programmer should not need to read it. The page is only intended for SkinEngine developers who need a very good understanding of the layout system.

Among other concepts which can be found in the SkinEngine, the implementation of the layout system is one of the most complicated. I changed the way how things are implemented multiple times to find a good solution.

The current implementation of the layout system

The idea (taken from WPF) is that we have two layout passes: A Measure pass and an Arrange pass. Those methods are defined in class FrameworkElement. Measure and Arrange both are not virtual; they call inner methods to be overridden by subclasses. Measure calls the   virtual method CalculateDesiredSize and Arrange calls the virtual method ArrangeOverride.

Measure and Arrange

In the first pass, the "measure pass", the system determines the DesiredSize of an element. That size is the optimal size in pixels which is needed for that element to be displayed. The Measure method is responsible for calculating the desired size. To do that, it subtracts the element's Margin from the given available size and applies its LayoutTransform to the size. With the remaining space, it calls CalculateInnerDesiredSize which measures the actual content. Method CalculateInnerDesiredSize is overridden from subclasses: "Pure" controls which will render their content without delegating to sub controls will calculate their size by their own while composed controls will call their children's Measure method here. After the Measure pass has finished, the DesiredSize properties of all elements in the hierarchy are filled with valid values. After the measure pass, all visible element's Measure methods have been called at least once.

 

In the second pass, the "arrange pass", the system calculates the actual positions and sizes of all elements. Each element gets a rectangle from its parent where the element should be arranged. To do that, it subtracts its Margin from the given rectangle, applies its LayoutTransform to the rectangle and calls ArrangeOverride, which arranges the element's actual content. As in CalculateInnerDesiredSize, ArrangeOverride arranges the actual content of the element being arranged, which means either a preparation of own primitives or calls of all sub element's Arrange methods. For each element, Arrange is never called before is Measure was called.

To handle the layout transforms, the methods have to cope with an outer and an inner coordinate system; Margin, DesiredSize, ActualWidth, ActualHeight and ActualBounds are all evaluated based on the outer (or parent's) coordinate system. All children are evaluated in the inner coordinate system. When measurement or arrangement are performed, all size/boundary parameters are transformed from the outer to the inner coordinate system. The opposite happens after the content has been measured; after that, the inner desired size has to be transformed from the inner to the outer coordinate system.

 

Both Measure and Arrange have a "shortcut handling": If the last measurement/arrangement is still valid (i.e. was not explicitly invalidated before) and the input parameters are the same, Measure and Arrange return at once without invoking their children. To achieve that, both store their former input values. Measure has a parameter SizeF totalSize, which is stored in the field _availableSize; Arrange has a parameter RectangleF outerRect, which is stored in field _outerRect.

 

The whole layouting is done from method UpdateLayout, which is defined in class FrameworkElement. That method is only called on the root element of a visual tree, i.e. the root element of a screen. The render thread calls that method each render pass. If there is something to do, that method will branch into Measure and Arrange.

Invaliating layout/Validating layout

When elements change their layout (maybe because a property was changed, lets say the text of a label was set to another value), they need to re-trigger the layouting system. That is done by calling InvalidateLayout. That method can invalidate the measurement as well as the arrangement independent from each other. To do that, the method has to boolean parameters. When that method is called, it invalidates the measurement and/or arrangement up to the root of the visual tree. The next call to UpdateLayout will re-calculate the whole layout.

Design decisions and optional ways how things can be done

  • The escalation of InvalidateLayout calls up to the visual tree's root is necessary because of our shortcut handling in Measure: If we would not invalidate the layout of all controls up to the root, some intermediate Measure call could break the call hierarchy because of its shortcut handling. That would result in an invalid child somewhere which is not measured again.
  • Basically, there is a second strategy to invalidate/validate the layout, which was implemented before SVN rev. 3684 and which also worked well. In that strategy, invalidating the layout was done by adding a reference to the element to be invalidated to a list of invalid elements in the parent screen. In the render loop, the screen had to iterate over the list and call UpdateLayout on the invalid elements.
    With that approach, it was necessary to implement a layout invalidation escalation strategy. Think of a label whose text was changed and thus has become bigger. That changed size could make an arbitrary number of parent elements become invalid too. So when an invalid control measured a different size than it measured before, the layout update needed to be escalated to the parent, i.e. the parent's UpdateLayout method was called from the child's UpdateLayout.
    When building the list of invalid controls in the screen, it was necessary that controls with a bigger distance from the visual tree's root were validated before elements with a smaller distance. The reason is the shortcut handling in the Measure method: If we would have updated the layout of a parent element first, although it has also invalid child elements, it could happen that the Measure call hierarchy starting at the parent could stop somewhere between the parent and the child because of a Measure call cutting short. That would mean during that call, the invalid child was not measured (it would be measured in its "own" UpdateLayout later, but not during the parent's UpdateLayout). But then, it could happen that the following Arrange call hierarchy hit the child. This is an invalid situation: An Arrange method call before a call to Measure. That situation could happen when a new element was inserted into the visual tree, for example.
    There were some problems with that approach:
    • It could take a long time to insert invalid controls into the list of invalid controls in the screen because 1) the calculation of the visual tree root distance takes some time and 2) the list of invalid controls could become quite big if there were many invalid controls.
    • If new elements were not initialized in the correct order during the runtime of a screen (first initializing VisualParent property, then Screen property), it could happen that the visual tree distance calculation calculated a distance which is too small (missing VP), which could mix up the list of invalid controls.
    • Under certain circumstances, the "optimistic layout invalidation/update" could become quite inefficient. For example if children, which invalidate their layout very often, had an impact on many parent containers. In that case, layout work could be done multiple times.
    • In the best case, that strategy took less time in the update layout pass because the list of invalid controls was already sorted and, in that best case, less layout update code had to be executed. On the other hand, the time was needed in the call which invalidated the layout (which was also done by the render thread...)

   

 

This page has no comments.