From X-Plane SDK
This chapter describes the basics of the X-Plane widgets library.
What Is the Widgets Library
The X-Plane Widgets library is a code library that implements a high-level cross-platform UI framework inside X-Plane. The widgets library makes it easy to create complex user interfaces and provides for most of the common UI components a plugin will need.
A widget is a single user interface element, like a push button, scroll bar, text entry field, etc. The widgets library provides many kinds of widgets and the code necessary to work with them.
Why Use the Widgets Library
There are many reasons to use the Widgets library:
- Widgets are cross platform. With the widgets library we can easily create one user interface for Macintosh and PC.
- Widgets match X-Plane's appearance and look consistent within the simulator.
- Widgets are high-level. We need to write no OpenGL code and very little event-handling code to use widgets.
- Widgets are fast. They use X-Plane's built in UI textures and have been tuned to run without slowing the sim down.
- Widgets are extensible. We can easily write our own widgets and integrate them with the existing ones for complex user interfaces.
- Widgets provide an API that is similar to what we might find on Macintosh or Windows.
- Widgets provide many premade user interface elements that require very little or no modification.
How Using Widgets are Different from the XPLM UI APIs
The XPLM display APIs are not persistent. Every time the sim redraws the screen, we are asked to redraw the window. Widgets are persistent. We create a widget once and it manages itself, calling us back only when something interesting happens (like the user clicking a button).
The XPLM display APIs provide a very general "window" with no special behaviors. The Widgets premade window classes have title bars, are draggable, select themselves, and have several preset styles; these are "windows" as users know them.
The XPLM display APIs provide a flat space; a plugin gets a window and fills in the blanks. Widgets are nestable into widget hierarchies that can represent complex user interfaces.
A widget is a single roughly rectangular user interface element. Examples of widgets include windows, buttons, text entry fields and scroll bars.
Widgets are refered to via *widget ID* s. A widget ID is an opaque 32-bit handle to the widget. A widget ID is similar to an HWND on Windows or a ControlRef on Macintosh. We never manipulate the widget ID directly; we only pass it back to the widgets library.
Each instance of a widget has a number of *properties*. A property is a 32-bit value assigned to a widget. Each property has a 32-bit *property ID* that describes it. Any widget can have any number of properties. Typically each widget will have properties that are useful to its widget callback (see below). Examples of properties include the style of a window or the state of a check box (checked or unchecked).
A widget also has a *descriptor*, a variable-length string attached to the widget. The widget's descriptor may be interpretted differently depending on the widget, but often is used for the title of a window or the text for a text field/caption.
Widget Callback Functions and Messages
Each widget has a callback function that implements the widget's behavior. Widgets send each other messages to cause actions to happen. The widgets library also sends widgets messages, as can the plugin. Examples of messages include "draw ourselves", "a child was added" and "a push button was clicked".
Widgets can actually have multiple callback functions. When a widget has multiple callback functions, they are called in order. This allows us to specialize a widget's behavior. Adding a callback to a widget is called *subclassing* a widget. A widget with multiple callback functions is similar to a class that inherits from another class; behavior not provided by the subclassing callback is handled by the original callback.
We use subclassing to customize widget behavior and to receive messages from the widget system. For example, we can subclass a push button. When the push button is clicked, it sends itself an "I was pushed" message. The callback function for the push button can intercept this message and take action.
Widget Hierarchy, Coordinates and Exposed Areas
Widgets are arranged in a tree known as a *widget heirarchy*. If one widget contains another, they are known as *parent* and *child* widgets. All widgets may have zero or more children and zero or one parents.
A widget may be an actual window in the plugin system, or it may not. If it is, it is said to be a *root* widget. If a widget is nested inside a widget that is a root wiget then the widget hierarchy is *rooted*. Rooted widget hierarchies may be seen by the user, but rootless ones will not be. In other words, for the widget to be seen, it must have a parent that is a root widget.
Widgets have a rectangular area described by its top, bottom, left and right, called its *geometry*. These coordinates are always in global screen coordinates, e.g. 0,0 is the bottom-left corner of the screen, and 1024, 768 is the top right. (The size of the screen may vary with user settings and the version of x-plane; use the XPLM Display APIs to determine the upper right corner of the screen.) The motivation for this coordinate system will be described below.
Only the portion of a widget's area that is contained in all of its parent widget's area can be seen by the user. This property is recursive; each widget is clipped by all of its parents. This guarantees that a button will not draw outside of a window, for example. The widgets libary uses the OpenGL scissor function to clip widgets to their parents' rectangles. Just about any OpenGL hardware capable of running X-Plane provides this function in hardware, so the clipping does not have a frame-rate penalty. The area of a widget that is actually visible is its *exposed geometry*.
Exposed geometry can be used to implement a scrolling display. Simply move a large widget that is inside a window up and down in screen space; only the portion of the widget that is visible through the window will be drawn. For performance we can determine the exposed geometry and only draw that area.
The motivation for using a single global coordinate space is the following:
- It eliminates all coordinate conversions when moving between widgets in a hierarchy, which makes development and debugging of complex UI code easier.
- For any widget that is expensive to draw and not fully exposed the widget should only draw its exposed geometry, so the area it draws will not be anchored at 0,0 even when using widget-specific coordinates.
- For most OpenGL code an offset must be provided as well as a width and height, so having a widget's upper-left always be 0,0 doesn't provide a significant simplification of performance enhancement to the code.
The global coordinate space is probably one of the least conventional design decisions for the widgets system, but so far it has proven to be significantly simpler to work with than nested coordinate-system frameworks.
Standard Widget Types
The widgets library comes with a number of built in widgets. They are identified by widget classes. Each widget class represents a built-in premade widget that implements a useful behavior. Often several similar widgets will have the same class. For example, push buttons, radio buttons and check boxes are all one widget class. A property of the widget identifies which type of button to implement.
The standard widgets are not special; they are simply implementations layered on top of the widgets library. There is nothing in the standard widgets that a plugin could not implement by hand.
Procedural Vs. Object-Oriented Programming
For programmers that are used to UI frameworks in object oriented languages (such as would be found in Delphi, or PowerPlant or MFC in the C++ world) programming widgets in C will be an adjustment.
A widget callback function essentially implements all of the overridden virtual functions for a subclass of a widget. Multiple callback functions provide inheritence. The widget message ID describes which virtual function is being called. The default widget implementation does nothing, so we do not need to provide implementations for 'empty' virtual functions.
Widget properties represent member data on a widget. Users can set and get these properties arbitrarily, and extra properties may be present; widgets do not have as strong encapsulation of data as some object oriented languages.
Using the Widgets Library
This section describes how to create user interfaces using the Widgets library.
Creating Widget Hierarchies
Widget hierarchies can be created in any order, but usually they are created from the top down. The basic step is to create the widgets and set their properties.
The function XPCreateWidget is used to create a widget and takes a number of arguments: the location of the widget on screen (in screen coordinates; the bottom left is 0, 0), whether the widget is initially visible, whether it is a root widget, its descriptor string, any widget containing it, and its class.
< Code Example: using XPCreateWidget to create a few widgets.>
The top widget in the hierarchy must be a root widget. This means that it will be visible on the screen. All other widgets should not be root widgets, but should have a parent widget. Pass 0 or NULL for the parent widget of the root widget to indicate that it has no parent.
Given a widget or widget hierarchy, we can rearrange the hierarchy by calling XPPlaceWidgetWithin. XPPlaceWidgetWithin will remove the moving widget from its parent (if it has one) and embed it in a new parent. The most recently added child of a parent will be visually "on top" of its sibblings but clipped to its parent.
We can use XPUCreateWidgets to easily create a number of widgets in a hierarchy. We pass in an array of structures describing each widget and the widgets are created and nested for we. Often when dealing with a complex widget hierarchy it may be easiest to work with the widgets in an array; this function makes building this array from a table of geometry straightforward.
< Code Example: a dialog box using XPUCreateWidgets. >
Once we have created the widgets, we use XPSetWidgetProperty to configure them by setting properties.
< Code Example: setting XPSetWidgetProperty to set up the dialog box. >
Most widgets will initialize their properties to reasonable values when created; set widget properties to access customized behavior (to change a window's style, for example).
Sometimes we may have to change the appearance of the widgets. The most common task is to show or hide a widget. This can be done using the XPShowWidget and XPHideWidget routines. Being visible is recursive; if we hide a widget, all of its children are hidden too.
We can position a widget or widget hierarchy on screen using the XPSetWidgetGeometry function. The movement of the children of the widget we position is a function of the wiget class. Most root widgets will move their children with them to keep the relative position of the window's elements constant.
Handling Messages from Widgets
Once we have created the widget hierarchy, it takes care of itself until the user wants to do something. The widget hiearchy will draw itself, buttons will be clickable, the user will be able to type. When we need to respond to an action (for example, the enter or escape keys being pressed or an 'OK' button being clicked) we write a widget function and subclass one of the widgets to receive messages we are interested in.
Widgets are constantly sending messages to each other to accomplish the basic user interface management tasks. When we attach our own widget function to a widget, we will receive all of those messages before the widget's normal handler. We return either 0 if we did not handle the function or 1 if we did. The widget function can then respond to a few widget functions uniquely.
Sometimes widget messages are sent up the widget hiearchy for convenience. For example, when the push button widget is clicked, it sends an "I was pushed" message to itself. If the push button does not handle this message (which by default it does not), the message is sent to every widget in the hierarchy up the root until someone handles it. One of the parameters is the ID of the widget that first originated the click.
This pattern of sending widget messages up the hierarchy is done for most messages that a plugin would be interested in overriding. This allows us to provide one widget message function for an entire widget hierarchy rather than one per widget.
< Example code: writing a widget message function >
We use the function XPAddWidgetCallback to add a new widget function to an existing widget, changing its behavior. Typically we install the widget function when we initialize the widget.
Cleaning Up Widgets
When we are done with the widgets, we must dispose of them or else we will leak memory! To dispose widgets, use the XPDestroyWidget function. If we destroy widgets we can recursively destroy their children too (which will clean up the whole hierarchy), or just destroy the root widget, leaving all children parentless and rootless.
<Example Code: Cleaning up Widgets>
Introduction to the Standard Widgets
The widgets library comes with a number of standard widgets that provide many user interface features. This document will not get into the details of the standard widgets; the standard widgets web page in the reference section of the SDK describes each widget in detail. This section describes some of the design ideas behind the standard widgets.
The standard widgets are divided into widget classes. Each class handles a similar set of widget behaviors. For example, one widget class handles push buttons and check box buttons. We set a property on a widget to customize its behavior and appearance.
< TODO: include a general table of the widget class and its overall purpose. >
The standard widgets send messages to themselves that then go up the hierarchy to notify us of user interactions. Each standard widget defines unique message IDs for widget events and property IDs and enumerations to control widget behaviors.
Creating Our Own Widgets
We can create our own widgets. Simply provide a widget function that will handle the entire widget's set of behaviors. We then use XPCreateCustomWidget to create the widget, passing in the widget function.
Widget Message Dispatching
Widget messages can be dispatched based on a number of modes:
- Direct. The message is sent to each widget function of the widget from the most recently added to the original widget function until one returns that the message has been handled.
- Up Chain. The message is sent to each widget function of the widget from the most recently added to the original widget function until one returns that the message has been handled. If no handler handles the widget function, the message is then sent to the parent of the widget (if it has one) in Up Chain mode.
- Recursive. The message is sent to each widget function of the widget from the most recently added to the original widget function until one returns that the message has been handled. The message is then sent to all of the widget's children in recursive mode.
- Direct All Callbacks. The message is sent to each widget function of the widget from the most recently added to the original widget function, even if the callback is handled.
- Once. The message is only sent to the most recent widget function of the widget. This mode isn't commonly used.
Use the routine XPSendMessageToWidget from the plugin or within the wigets to send messages to the widgets.
Key Widget Messages
The widgets library defines some basic messages that are sent to the custom widget.
- Create, Destroy. The create and destroy messages are sent to the widget function when a widget that uses the function is created and destroyed. We should set any properties we must set on the widget and use these functions for setup and cleanup of the widget.
- Paint, Draw. The draw message is sent to the widget once per frame in X-Plane to redraw the widget. We can intercept the paint message instead for lower level drawing control, or draw to have the widgets library take care of clipping and child widgets for us..
- KeyPress, KeyTakeFocus, KeyLoseFocus. These messages are sent when keys are pressed or when the widget gains or loses keyboard focus. Key presses are passed up the widget hierarchy if they are not handled by a child widget, but only one widget can have 'focus' at a time in the entire system; this is the widget that gets keystrokes first.
- MouseDown, MouseDrag, MouseUp. These messages are sent to the widget to track the mouse. There is no roll-over message; to implemenet roll-over behaviors, simply check whether the mouse is over the widget's exposed geometry from our draw function.
- Reshape, ExposedChanged, AcceptChild, Shown, Hidden. These messages are sent to the widget when it is moved, shown, hidden, or topology is changed.
- DescriptorChanged, PropertyChanged. These messages are sent to the widget when the properties or descriptors change. This can be useful if the widget calculates internal cached state when these values change.
Remember that multiple widgets might use the widget function. Attach widget-specific information to widgets using properties. Do not use global variables for per-widget data!
We can define the own widget properties and widget messages to provide an API to the widget's unique functionality.
See the reference section for the specific functional areas:
- XPWidgetDefs - Basic widget definitions.
- XPWidgets - Core widget management functions.
- XPStandardWidgets - Built in widget types.
- XPWidgetUtils - Helper routines for implementing our own widgets and using widgets.
- XPUIGraphics - Routines that draw standard UI elements using X-plane's UI bitmaps. These are used by the standard widgets.