Rendering |
This article describes how UI controls are drawn to the screen.
This topic contains the following sections:
The UIScreen is the root of the visual tree. It implements IDrawable (the XNA interface) which means it has a Draw method. This method must be called in each frame to draw the controls. The UIManager does not draw the controls! Therefore, you must not forget to call UIScreenDraw. You can call the method where ever it fits into your render pipeline. As the Samples show you can also render the whole screen into an off-screen render target.
UIScreenDraw calls Render, which renders the screen and recursively calls the Render methods of all controls in the visual tree.
The UIScreen owns a UI renderer (interface IUIRenderer) which is used by the controls to define the visual style and to do the actual rendering.
The UIControl has several properties and methods related to the rendering. The Render method is obviously one of them. This method is called by the UI system when the control should be rendered. It checks if the control is visible (property IsVisible), if the control is not completely transparent (property Opacity) and if the actual control size is larger than 0 – otherwise there is no need to render the control.
If the control must be rendered, Render calls OnRender to do the real work. The OnRender method can be overridden in derived controls classes. The base implementation simply calls the UI renderer of the current screen:
protected virtual void OnRender(float deltaTime) { Screen.Renderer.Render(this, deltaTime); }
It is not necessary that the OnRender methods of custom controls call the renderer to draw the control. Instead, custom controls can do the drawing right there in this method using, for example, the normal XNA SpriteBatch. This makes it very easy to quickly add new custom controls.
The UIControl further has a few properties that should be used by the renderer or the custom OnRender implementation: the Background color, the Foreground color, the Font name and the Opacity of the whole control.
Each UIControl has a RenderTransform. This is a transformation that is composed of RenderScale, RenderRotation, RenderTranslation and RenderTransformOrigin. Each property can be animated individually to scale, rotate and translate the control to create visual effects and transitions – have a look at the UIAnimationSample of the Samples.
The property HasRenderTransform is true if the control is scaled, rotated or translated. It is false, if the render transform is the identity transform that does nothing.
The RenderTransform is also considered by the control input handling. That means, it is possible to click a rotated and scaled button. But the RenderTransform is not used in the layout process. A scaled/rotated/translated control does not influence the layout. For example, a scaled control does not get more space in a stack panel. The rendered control may overlap other controls.
The property RendererInfo can be used by the renderer (or the custom OnRender method) to cache any information it likes to store with the control. The property IsVisualValid is automatically set to true after the control was rendered (assuming that the renderer has updated the cached information in RendererInfo). IsVisualValid is reset when InvalidateMeasure, InvalidateArrange or InvalidateVisual are called. Controls that influence the control size (e.g. Width), the layout arrangement (e.g. HorizontalAlignment) or the visual appearance (e.g. Foreground) call the invalidate methods automatically. The renderer can check IsVisualValid to determine if it can reuse the cached info in RendererInfo.
The VisualState property is a simple string that tells the renderer (or the custom OnRender method) in which state the control currently is. The base implementation of this property is simple:
public virtual string VisualState { get { return ActualIsEnabled ? "Default" : "Disabled"; } }
The rendering code can check this property and, for example, draw a disabled button using different button texture. Other controls can override this property to define more states. For example, here is the VisualState property of a button:
public override string VisualState { get { if (!ActualIsEnabled) return "Disabled"; if (IsDown) return "Pressed"; if (IsMouseOver) return "MouseOver"; if (IsFocused) return "Focused"; return "Default"; } }
The properties discussed above define the visual appearance of the control. The real rendering is separated from the UIControl code. This is the purpose of the interface IUIRenderer.
The UIScreen is the root of the control hierarchy. When a screen is created it takes the renderer as a parameter in the constructor. For example:
// Load a UI theme, which defines the appearance and default values of UI controls. var content = new ContentManager(Game.Services, "NeoforceTheme"); Theme theme = content.Load<Theme>("ThemeRed"); // Create a UI renderer, which uses the theme info to renderer UI controls. UIRenderer renderer = new UIRenderer(Game, theme); // Create a UIScreen and add it to the UI service. The screen is the root of the // tree of UI controls. Each screen can have its own renderer. _screen = new UIScreen("Menu", renderer); _uiService.Screens.Add(_screen);
To draw the GUI, UIScreenDraw must be called. UIScreenDraw calls Render. Render calls OnRender which in turn lets the renderer do the work using IUIRendererRender. When the renderer draws a control, it calls Render of all child controls. This way, Render of each control in the control tree is executed. Each child control can decide in its OnRender method if it wants to call IUIRendererRender or do the drawing manually.
The IUIRenderer manages a single SpriteBatch, which is used to batch all sprite batch draw calls. The UIScreen will call BeginBatch to start the batch (using SpriteBatch.Begin). Rendering code can use the sprite batch to draw textures and text. Please note: Whenever the render state is changed EndBatch must be called to commit the current batch. Don't forget this if you implement custom rendering code.
We strive to separate control logic from rendering. The control classes define the control logic (e.g. how a check box behaves), and the exchangeable IUIRenderer defines the visual appearance. But in order to fully control the visual appearance, the IUIRenderer must be able to supply default values for properties, like Foreground color, Width, etc. And here is how it works:
Each UIControl has a Style property. This is simply a string; usually equal to the control name.
The IUIRenderer has a dictionary called Templates. This dictionary stores an initialized UIControl instance for each style name. The template instances provide the default vales for controls.
When a control is loaded (= added to the visual tree), it locates the template instance for its current style in Templates and sets its own Template property to this instance. Template is a property of the game object system. (See Templates to read more about game objects and templates.) Whenever a control has not set a local property value, it uses the property value of the template object. This way you can, for example, quickly change the appearance of all buttons simply by changing the values of the template.
The templates are created on demand: If IUIRendererTemplates does not contain a template for a given style, then a new empty instance is created. All properties of this instance are initialized with the values provided by the IUIRendererGetAttributeT method. The default IUIRenderer implementation (class UIRenderer) reads the Theme.xml file and returns these values in GetAttributeT (see Themes). This way, the IUIRenderer can define the default values for control properties.
Here is a concrete example that demonstrate the power of styles. The full code (except comments) for a MenuItem looks like this:
public class MenuItem : ButtonBase { static MenuItem() { OverrideDefaultValue(typeof(MenuItem), FocusWhenMouseOverPropertyId, true); } public MenuItem() { Style = "MenuItem"; } }
It is simply a button (base class ButtonBase). Per default it gets input focus when the mouse is over the menu item (the OverrideDefaultValueT call). The style is set to "MenuItem" in the constructor. Since the menu item style is different from the style of a normal button, the control gets very different property values than a normal button, and the renderer will render this control in a totally different way.
Using styles, the control code can focus on the control logic. And the renderer has full control over how the control should appear.
To apply a new style to an already loaded control, the control must first be removed from the visual tree and afterwards added back again to the visual tree.
The class UIRenderer is the default implementation of the interface IUIRenderer. It implements the necessary interface members and adds a few new ones. It uses an XML file to define the control styles, see Themes. UIRendererGetAttributeT uses this Theme.xml file to define the default values of control properties.
Controls can call IUIRendererRender to draw the control – this is the main purpose of the UI renderer. When Render is called, the UI renderer clears the area of the control with its background color, precomputes a few values and stores them in a UIRenderContext instance.
The render context contains the following information:
This render context is then handed to a render callback. A render callback is a method that is stored in the UIRendererRenderCallbacks dictionary. The style name of the control defines which render callback should be used (the style is the key of the dictionary). Actually there are only very few render callbacks necessary to render all built-in controls. The render callback dictionary is initialized like this:
RenderCallbacks = new Dictionary<string, Action<UIRenderContext>>(); RenderCallbacks.Add("UIControl", RenderUIControl); RenderCallbacks.Add("TextBlock", RenderTextBlock); RenderCallbacks.Add("Image", RenderImageControl); RenderCallbacks.Add("Slider", RenderSlider); RenderCallbacks.Add("ProgressBar", RenderProgressBar); RenderCallbacks.Add("Console", RenderConsole); RenderCallbacks.Add("ContentControl", RenderContentControl); RenderCallbacks.Add("TextBox", RenderTextBox);
Styles inherit from each other. This inheritance is defined in the Theme.xml. If there is no render callback for a given style, then the render callback of the parent style is used. The style "UIControl" is the base for all styles, therefore the method RenderUIControl is the fallback rendering method for all controls.
A few examples:
There is no render callback for buttons. The style "Button" inherits from "ButtonBase" which inherits from "ContentControl". The method RenderContentControl is used to draw buttons.
Scroll bars have following style hierarchy: "ScrollBar" –> "RangeBase" –> "UIControl". The method RenderUIControl is used to render scroll bars.
Let’s have a look at one of these methods. Here is RenderContentControl
private void RenderContentControl(UIRenderContext context) { var contentControl = context.Control as ContentControl; if (contentControl == null || contentControl.Content == null || !contentControl.ClipContent) { // No content or no clipping - render as normal "UIControl". RenderUIControl(context); return; } // Background images. RenderImages(context, false); EndBatch(); // Render Content and clip with scissor rectangle. var originalScissorRectangle = GraphicsDevice.ScissorRectangle; var scissorRectangle = context.RenderTransform.Transform(contentControl.ContentBounds).ToRectangle(true); GraphicsDevice.ScissorRectangle = Rectangle.Intersect(scissorRectangle, originalScissorRectangle); BeginBatch(); contentControl.Content.Render(context.DeltaTime); EndBatch(); GraphicsDevice.ScissorRectangle = originalScissorRectangle; BeginBatch(); // Visual children except Content. foreach (var child in context.Control.VisualChildren) if (contentControl.Content != child) child.Render(context.DeltaTime); // Overlay images. RenderImages(context, true); }
If a content control does not contain a content or does not clip the content, the RenderContentControl method calls RenderUIControl to do the job. Otherwise, it renders the background images using RenderImages(context, false). This call renders all images for the current visual state that are defined in Theme.xml and are not marked as overlay images in the XML.
Then the current sprite batch is committed by calling EndBatch. This is very important because, next, a scissors rectangle is set. This scissors rectangle is used to clip the content. Since this changes the render state of the graphics device, we must not forget to call EndBatch to finish the current sprite batch with the current render settings.
The render transform of the render context is used to compute the correct rectangle. This is necessary because the control could be scaled or translated. (Side note: The current clipping mechanism uses the hardware scissors rectangle and therefore does not support clipping rotated controls.) The RenderTransform also offers very useful draw method that calls the sprite batch to do the drawing with the correct scaling, rotation and translation parameters.
Then the content (which is another UIControl) is rendered. And after that the visual children of the control are rendered. For example, a window is a content control and its icon image, caption text block and close button are the visual children of the window control. Finally, the overlay images (e.g. transparent glow images) are rendered on top of everything else. The sprite batch is not committed. It is left active, so that the next control can add its images to the batch.
If you want to implement custom rendering for an existing control or a new control, you have to do the following:
Have a look at the existing Theme.xml files and the DigitalRune Game UI source code for reference. It is best to start by copying existing render callback code and start from there.
To summarize: There are several ways to customize the appearance and rendering of controls: