Creating a widget
It's possible to implement custom widgets. A custom widget can be declared and implemented outside of the µGFX library. This means that it can be part of the actual project and that it's not necessary to do any modification of the µGFX library itself. The first half of this article will explain how widgets are implemented an whats needed to create a custom widget. The second half of the article is a step-by-step guide that will show how to actually implement a custom widget based on an example.
It's vital that you've read the GWIN article and the corresponding sub-articles about windows, widgets and containers before following this guide.
Note: If you just want to change the look of an existing widget, please have a look at WidgetStyles and custom rendering functions.
Contents
Theory of operation
A widget consists of three main parts: The object structure, the VMT and the rendering routine.
A widget is usually split into two source files: The header file and the source file (for example mywidget.h
and mywidget.c
. The header file contains the object structure and the public API functions. The source file contains the VMT, the rendering routine and the implementations of all the required functions.
Object structure
The C language is not object oriented - yet it's possible to write object oriented software without any problems. The philosophy is to create a struct which is the object and to pass that struct as the first parameter to any function that has to have access to the object. A widget is an object and hence it needs a struct that defines the object. The only thing that makes the widget object struct become a widget struct is that the very first field is a GWidgetObject
:
typedef struct StatusbarObject_t { GWidgetObject w; // Base Class } StatusbarObject;
Note that it is absolutely mandatory that the GWidgetObject
is the very first field in the struct. This is needed to implement inheritance and polymorphism. After that first field you can add any field you want to the widget object struct. For example, a slider would add fields to hold the minimum, maximum and current value. A list widget would hold a list of items here. A text edit (line edit) would hold the current cursor position and so on. In terms of an object oriented language this is the private
area of the class.
typedef struct widgetObject_t { GWidgetObject w; // Base Class uint8_t myVariable1; char* someOtherStuff; uint16_t currentValue; } WidgetObject;
The GWidgetObject
itself is based on a GWindowObject
as explained in the article about widgets. Therefore, the following attributes are already part of our WidgetObject
and don't need to be added manually:
- Position relative to parent (x, y)
- Dimensions (width, height)
- Parent
- Font
- Text
- WidgetStyle
- Widget Tag
- Flags
The widget object is the widget itself: It's what the user of the widget will create an instance of. The widget object must be available to the outside world. Therefore, the widget object must be declared in the header file (referred to as mywidget.h
above).
Flags
The flags need some explanation: The GWindowObject
contains a field of the type uint32_t
and can therefore represent 32 flags. Some of these flags are used by the GWIN module internally to store certain information like the enable state, the visibility, whether the widget was allocated dynamically and so on. However, the first 8 bits are reserved for widget internal use. For example, the pushbutton widget uses one of these bits to keep record of the pressed/release state. The label widget uses the flags to keep the information whether the a border around the text should be rendered and so on.
Flags are usually used to define the state of a widget and are therefore often used in the rendering routine. For example, the PushButton widget uses the flags to store the pressed/released state. As it's possible to create a custom rendering routine outside of the widget the author of the custom rendering routine needs to have access to those flags. Therefore, the flags should be defined in the header file.
VMT
The VMT (virtual method table) is a mechanism to implement dynamic function binding. In case of C it's a simple struct containing function pointers. The VMT technique allows to implement virtual methods in our C-classes. The VMT is defined by the GWIN module. The custom widget simply creates an instance of that struct and fills in the functions pointers. The GWIN module defines a VMT for each type of GWIN element: Window, widget and container. As a widget inherits from the window the widget VMT actually contains the window VMT. The VMTs declarations look like this (taken from /src/gwin/gwin_class.h):
/** * @brief The Virtual Method Table for a GWIN window * @{ */ typedef struct gwinVMT { const char * classname; /**< The GWIN classname (mandatory) */ size_t size; /**< The size of the class object */ void (*Destroy) (GWindowObject *gh); /**< The GWIN destroy function (optional) */ void (*Redraw) (GWindowObject *gh); /**< The GWIN redraw routine (optional) */ void (*AfterClear) (GWindowObject *gh); /**< The GWIN after-clear function (optional) */ } gwinVMT; /** @} */ /** * @brief The Virtual Method Table for a widget * @note A widget must have a destroy function. Either use @p _gwidgetDestroy() or use your own function * which internally calls @p _gwidgetDestroy(). * @note A widget must have a redraw function. Use @p _gwidgetRedraw(). * @note If toggleroles != 0, ToggleAssign(), ToggleGet() and one or both of ToggleOff() and ToggleOn() must be specified. * @note If dialroles != 0, DialAssign(), DialGet() and DialMove() must be specified. * @{ */ typedef struct gwidgetVMT { struct gwinVMT g; /**< This is still a GWIN */ void (*DefaultDraw) (GWidgetObject *gw, void *param); /**< The default drawing routine (mandatory) */ #if GINPUT_NEED_MOUSE struct { void (*MouseDown) (GWidgetObject *gw, coord_t x, coord_t y); /**< Process mouse down events (optional) */ void (*MouseUp) (GWidgetObject *gw, coord_t x, coord_t y); /**< Process mouse up events (optional) */ void (*MouseMove) (GWidgetObject *gw, coord_t x, coord_t y); /**< Process mouse move events (optional) */ }; #endif #if GINPUT_NEED_KEYBOARD || GWIN_NEED_KEYBOARD struct { void (*KeyboardEvent) (GWidgetObject *gw, GEventKeyboard *pke); /**< Process keyboard events (optional) */ }; #endif #if GINPUT_NEED_TOGGLE struct { uint16_t toggleroles; /**< The roles supported for toggles (0->toggleroles-1) */ void (*ToggleAssign) (GWidgetObject *gw, uint16_t role, uint16_t instance); /**< Assign a toggle to a role (optional) */ uint16_t (*ToggleGet) (GWidgetObject *gw, uint16_t role); /**< Return the instance for a particular role (optional) */ void (*ToggleOff) (GWidgetObject *gw, uint16_t role); /**< Process toggle off events (optional) */ void (*ToggleOn) (GWidgetObject *gw, uint16_t role); /**< Process toggle on events (optional) */ }; #endif #if GINPUT_NEED_DIAL struct { uint16_t dialroles; /**< The roles supported for dials (0->dialroles-1) */ void (*DialAssign) (GWidgetObject *gw, uint16_t role, uint16_t instance); /**< Test the role and save the dial instance handle (optional) */ uint16_t (*DialGet) (GWidgetObject *gw, uint16_t role); /**< Return the instance for a particular role (optional) */ void (*DialMove) (GWidgetObject *gw, uint16_t role, uint16_t value, uint16_t max); /**< Process dial move events (optional) */ }; #endif } gwidgetVMT; /** @} */ /** * @brief The Virtual Method Table for a container * @note A container must have a destroy function. Either use @p _gcontainerDestroy() or use your own function * which internally calls @p _gcontainerDestroy(). * @note A container must have a gwin redraw function. Use @p _containerRedraw(). * @note If toggleroles != 0, ToggleAssign(), ToggleGet() and one or both of ToggleOff() and ToggleOn() must be specified. * @note If dialroles != 0, DialAssign(), DialGet() and DialMove() must be specified. * @{ */ typedef struct gcontainerVMT { gwidgetVMT gw; coord_t (*LeftBorder) (GHandle gh); /**< The size of the left border (mandatory) */ coord_t (*TopBorder) (GHandle gh); /**< The size of the top border (mandatory) */ coord_t (*RightBorder) (GHandle gh); /**< The size of the right border (mandatory) */ coord_t (*BottomBorder) (GHandle gh); /**< The size of the bottom border (mandatory) */ void (*NotifyAdd) (GHandle gh, GHandle ghChild); /**< Notification that a child has been added (optional) */ void (*NotifyDelete) (GHandle gh, GHandle ghChild); /**< Notification that a child has been deleted (optional) */ } gcontainerVMT; /** @} */
Note how the VMTs reflect the inheritance of the GWIN elements: Windows, widgets and containers. If you want to implement a widget, you don't have to implement the container VMT.
As the VMT is private to the widget itself it's declared in the source file (earlier referred to as mywidget.c
). To have access to the different predefined VMTs the source file must include the file src/gwin/gwin_class.h: #include "src/gwin/gwin_class.h"
.
Function pointers
The function pointer declarations in the VMT specify the signature of a function (the return type and the parameters the function takes). In order to be able to register a function in the VMT, the function must match that exact signature. Let's have a look at the function to render/draw the widget:
void (*DefaultDraw) (GWidgetObject* gw, void* param);
This means that our own function that we will write to render the widget must look like this:
void myRenderingFunction(GWidgetObject* gw, void* param) { ... }
Note that the function pointer in the VMT doesn't specify the name of the function. The name DefaultDraw is only used internally by the GWIN module to call the function. You can assign a function with any name as long as the function signature matches the one from the function pointer declaration. Once a function has been declared it can be added to the VMT by simply typing the actual function name in the corresponding field in the VMT:
void myRenderingFunction(GWidgetObject* gw, void* param){ ... } ... static const gwidgetVMT mywidgetVMT = { { "MyWideget", // The classname sizeof(MyWidgetObject), // The object size _gwidgetDestroy, // The destroy routine _gwidgetRedraw, // The redraw routine 0, // The after-clear routine }, myRenderingFunction, // The default drawing routine #if GINPUT_NEED_MOUSE { 0, // Process mouse down events 0, // Process mouse up events ... ... ... };
Available functions
The following table contains a short explanation for each field in the VMT:
Name | Type | Required | Description |
---|---|---|---|
classname | const char* | Yes | The name of the class (the widget name). |
size | size_t | Yes | The size of the class (usually sizeof(WidgetObject) ).
|
Destroy | Function Pointer | Yes | A function that is called when the object is destroyed. Use _gwidgetDestroy as a default value.
|
Redraw | Function Pointer | Yes | A function that is called when the need to be redrawn. Note that for widget types this is not the rendering routine. Use _gwidgetRedraw as a default value.
|
AfterClear | Function Pointer | No | A function that will be called after the widget has been redrawn. Can be used to manually redraw some features as windows don't know how to draw themselves. Example: graph. |
DefaultDraw | Function Pointer | Yes | The default rendering function for a widget. |
MouseDown | Function Pointer | No | The function that is called when a mouse-down event occurs inside the widget area. Coordinates passed as parameters are relative to the widgets coordinates. |
MouseUp | Function Pointer | No | The function that is called when a mouse-up event occurs inside the widget area. Coordinates passed as parameters are relative to the widgets coordinates. |
MouseMove | Function Pointer | No | The function that is called when a mouse-move event occurs inside the widget area. Coordinates passed as parameters are relative to the widgets coordinates. |
KeyboardEvent | Function Pointer | No | The function that is called when a keyboard key has been pressed while the widget has focus. Note that the source of the key press can be a physical keyboard connected through the GINPUT module or the keyboard widget. |
Note that the "Required" value just specified whether the value can be a null pointer or not. The VMT struct must be fully filled. See the example in the second part of this article.
Rendering routine
This is the part that usually takes the most time when writing a custom widget: The rendering routine. A widget must have at least one built-in rendering routine which is registered as the default rendering routine in the VMT. The rendering routine is the function which draws/paints the widget on the screen. This function is called by the GWIN module whenever the widget needs to be redrawn (eg. when the visibility changed).
For further information about rendering routines it's recommended to carefully read the article about custom rendering routines.
Creating the widget
So far we looked at how a widget is defined and how the VMT is filled in. However, we still need to actually create the widget so we can use it - the widget needs a constructor. The constructor (from here on also referred to as "the create function") is responsible for allocation and initializing the widget object and therefore the GWidgetObject
base class.
The create function has to follow a couple of rules in order to blend into the GWIN module:
- It returns a GHandle (See GHandle)
- It takes a
GDisplay*
pointer (See GDisplay) - It takes a pointer to the widget object which it initializes
- It takes a pointer to a
GWidgetInit
structure (See widget initialization) - If the widget object pointer is a null pointer the function has to dynamically allocate the widget object
An example would look like this:
GHandle mywidgetGCreate(GDisplay* g, WidgetObject* wo, GWidgetInit* pInit) { // Create the base class (the actual widget) if (!(wo = (WidgetObject*)_gwidgetCreate(g, &wo->w, pInit, &mywidgetVMT))) { return 0; } // Initialize the object struct wo->myVariable1 = 0; wo->currentValue = 0; // Set the initial visibility gwinSetVisible((GHandle)wo, pInit->g.show); // Return a proper GHandle return (GHandle)wo; }
Furthermore, it's recommended to have a #define
that creates an alias for the create function that doesn't take the GDisplay*
pointer but uses the default display instead:
#define mywidgetCreate(so, pI) mywidgetGCreate(GDISP, so, pI)
Besides those rules it's possible to pass additional parameters that change the behavior of the widget. For example, the frame widget takes an additional flags parameters which specify whether there are close and min-max buttons in the widget decoration.
Examples
Various examples can be found in the download section: https://community.ugfx.io/files/category/1-%C2%B5gfx-library/