-
Notifications
You must be signed in to change notification settings - Fork 1
Home
This is a simple, lightweight and effective library for creating native user interfaces on Windows. It pursues some modest goals with a few novel features. The main goal is a well tested framework solid enough to stand on.
- Tiles
- Controls
- Canvas
- Theme
- Window
All visual components are tiles that implement the ITile interface. All tiles have a bounding rectangle and can be drawn on a canvas. Tiles do not create window handles (eg. HWNDs). For visually complex applications that frequently build up and tear down display regions, this greatly reduces object handle churn.
- Arrow - displays an arrow shaped glyph pointing left, right, up or down.
- Fill - fills empty space with a solid color.
- Glyph - displays a small glyph - likely taken from one of several symbol fonts.
- Text - a static text control for labels and captions.
The Arrow tile is used by the Scroll and Combo controls below.
Tiles that respond to mouse and keyboard events are Controls. All controls implement the IControl interface - which is an extension of the ITile interface. In additional to drawing themselves on a canvas, controls sink (accept) mouse and keyboard events, receive or lose focus and may capture the mouse (receive mouse events even when the mouse isn't hovering over the control).
- Button - button control displays as up (raised) or down (depressed); a work in progress.
- Check - check box to set / display a binary flag as ✓ (or X-ed) or un-checked.
- Combo - a combo box with a floating drop-down list (needs scrollbar to support long lists).
- Edit - edit a text string inside a box. Cut/copy/paste not supported.
- Grid - display and edit tabular data much like a spreadsheet.
- List - works very much like the .NET PropertyGrid.
- PickColor - editor for RGB color values - displays the color, the R,G,B decimal values.
- PickFont - editor for font selection - displays the font face and height.
- PickPath - editor for file system path selection.
- Scroll - displays a scroll bar with two opposing arrows and a sliding thumbnail.
- Toggle - each click or space bar cycles through a small finite list of glyphs.
- Tree - uses a check box to manage display of a list of child controls.
The controls generally do not host content like traditional Window controls (via SetWindowText or SetItemData). For example, the Check and Edit controls accept an accessor interface to modify the underlying text or toggle. This allows decoupling the user interface from your data layer or business object model. If your object model supports "observable" objects where data change triggers notification, the triggering logic can be placed in the accessor.
Each control has a read-only property. Users can safely browse information without accidentally editing.
Layout is done through a nested collection of Pane containers. Each pane container operates on tiled objects. Because panes are IControls, they can be managed by another pane. A pane can arrange it's children horizontally or vertically. Panes also route mouse and keyboard events. Intra-form tab key navigation is automatic. Arrow keys, Escape key, Tab key, PgUp/PgDn keys should all act as you expect. A pane takes it's allocated screen area and divides it up among it's children. Child tiles specify a minimum and maximum extent. They also specify a relative weight value to be proportionately allocated excess pixels.
If you haven't noticed, there is no toolbar control. You don't need one. Create a horizontal Pane with a set of Buttons plus a Fill to soak up any excess space. This means you can include any type of control in your toolbar.
A horizontal pane contains three child tiles. The pane is 16 pixel high and 400 pixels wide. The child tiles will inherit the pane's height. The child tiles' have minimum widths of 16, 48, and 0. The child controls have maximum widths of 16, unlimited and unlimited respectively. The pane will allocate 16 pixels for the first tile and 48 pixels for the second tile. This leaves a surplus of 400 - (16 + 48) or 336 pixels. The child tiles have weights of 0, 1, and 1 respectively. The sum of the weights is 2 so the second tile receives one-half of the surplus pixels. The third tile also receives one-half of the surplus pixels. The first tile has a weight of zero so it receives no surplus pixels. The total pixel allocation becomes 16, 48 + 168 or 216, and 168 pixels. To confirm, 16 + 216 + 168 equals 400. Pixel allocation can occur in vertical or horizontal axis.
The Window class serves as an external adapter to translate native Windows events to generic mouse and keyboard events. It bridges the gap between the abstract realm of a Pane container and an HWND. Implementing IWindow, it tracks control focus and supports mouse capture. Controls request the input focus by calling the setFocus method on the desktop. Both new and previous focus controls are notified by the desktop via their setFocus methods. Controls should not call the setFocus method on self or other controls. In Microsoft Windows terminology, a Window will usually be the child control that occupies the client area of your program's main "frame" window. Your frame window is responsible for caption bar, size boxes, and menus (using WTL or MFC).
WTL and MFC implementations are provided.
A restricted variation of the Window class. This adapter is used for the drop-down list on Combo box controls and could serve as rectangular tool-tip (with the text provided by a Text tile). Popups don't take the input focus away from other controls. Popup controls likely disappear when clicked.
WTL and MFC implementations are provided.
A container that operates much like your old friend MessageBox. For somewhat disruptive "I need your attention now" uses.
WTL and MFC implementations are provided.
The Canvas is a native Windows implementation of the ICanvas interface. It maps the generic canvas API to native Windows graphics primitives.
WTL and MFC implementations are provided.
Here are the methods:
- DrawString
- DrawEditString
- FillRectangle
- Polyline
- Polygon
Unlike Windows dialog based forms, all controls draw themselves. The Windows message WM_ERASEBKGND is silently ignored. The result is a clean, flicker-free display. Keeping the set of graphics operations small should allow for easier unit testing. Note: the containing window's window-class structure should NOT specify a background brush. We don't want Windows trying to paint the background for us. This is very easy to do in MFC and WTL. The sample Window classes do this already.
Nearly all draw methods above include a "box" parameter. It's a rect_t type but serves a very different purpose from the "rect" parameter. The "rect" parameter specifies where in the parent window's client area to perform the canvas operation. The "box" parameter describes a scroll box. The width and height define the area of scroll box. The x,y coordinates specify a zero based horizontal and vertical scrolling offsets. Scrolling of a tile or control is managed by it's parent Pane object which will adjust the each tile's scroll box as necessary. Scrolling only applies when the drawable width or height is LESS than the scroll box width or height, respectively.
In summary, scrolling results in a control drawing only a portion of itself. All of the clipping and transforming needed for a control to draw a "slice" of itself is handled by the canvas. This greatly simplifies the logic of all tiles.
The plan is to cache any GDI objects (like fonts, pens and brushes) to reduce churn. The reason: at high create/destroy rates - Windows can briefly run out of GDI handles so holding and "recycling" frequently used objects is a good thing. As of this writing, only font handles are cached. Each tile/control has a setChanged method. Calling setChanged(true) will cause the hosting Window to immediately redraw the tile or control. It does not wait for a lazy paint request. You can control this in the Window implementation of the IRedraw handler. It can invalidate the tile area or instantly draw it on screen. The former yields better average CPU performance (screen drawing is batched into one operation) or more responsive (individual tiles/controls redrawn as needed).
Since none of the controls create an HWND object, their contents are immune to introspection through the Windows API. Tools like WinSpy or Microsoft's Spy++ - which are famously able to lift passwords out of edit boxes - will not work on the controls in this library. The only text these tools will yield is the caption text and window class name of the application window.
Generic ITile and IControl unit tests are a work in progress. Current tests use mock Canvas, mock desktop and mock watcher (draw notifier). These tests quickly caught a bug that was causing weirdness in the scroll control. Being able to instantiate tiles and controls without actual window handles greatly simplifies unit testing. More coverage is planned.
All controls bind to a theme object which stores user preferences for text foreground and background color, text font, and other visual style information. Updating your theme will update any forms whose controls are bound to that theme.
- Data: specifies the foreground and background colors for editable areas.
- Cell: specifies the foreground and background colors for an editable area that has the input focus.
- Row: specifies the foreground and background colors for an editable area that that is the selected row in a grid.
- Caption: specifies the foreground and background colors for static display text and arrows.
- Tool: specifies foreground, background, and hover colors for buttons.
- Arrow: specifies the font and glyphs for the four arrows.
- Text: specifies the font for all text - including Text, Button, Combo, and Edit.
Each control has default color selections that refer to one of the theme colors by index. However, you may programmatically override this to use a different index or explicitly provide a color value.
Each tile (and control) can save itself as a JSON object. Saving a pane also saves all nested tiles and controls. A separate function loads a collection of tiles and controls. The root level object must be a pane.
Persistence to XML will be added in the future. See the XML Reader / Writer library.
- align_t - text alignment - like orient_t but values may be combined (eg. up and down, or left and up, or none.).
- color_t - BGR color AKA 'COLORREF' (not RGB 'cuz it's Windows); alpha channel not supported.
- identity_t - identity type used to give components a unique ID.
- meter_t - screen measurement type (pixels).
- orient_t - orientation - up, down, left, right.
- point_t - screen position as (x,y) ordered pair.
- rect_t - screen rectangle - x, y, width, height.
- string_t - common string type.
- Flow - a flow descriptor - consists of min, max, and weight values.
- Font - a font descriptor - face, height, and style.
The Flow also has a flag that changes the unit of measure for min and max from pixels to the current font height. With this flag turned on, a min of 1 equals the font's pixel height. With the flag off, min and max are in pixels.
Canvas can turn a font descriptor into a font handle. The Canvas keeps a cache of font handles so repeated calls with the same font description use the same font handle.
An accessor is a small C++ interface with get and set methods. Accessors exist for strings, booleans, and integers. Controls use accessors to retrieve their content. This helps to keep data separate from user interface. Accessors that wrap other accessors can be used to convert from one type to another. The Edit control accepts a string accessor. An Integer accessor is used to convert a long integer to/from a text string for editing. Likewise for RGB separated color values and dotted decimal IP addresses. See the "Accessor.h" header file for details.
Binding is the process of retrieving a pointer or reference to a control, setting an accessor, and optionally attaching delegates for any events provided by the control.
An application can create an entire form by loading a JSON description. The app can then manipulate the controls of this form through binding. From the root Pane object, call the find method with the object's ID number and type name. An output parameter sets an IControl pointer to the desired control. The function returns false if the control was not found. Example:
Pane *pPane = NULL; Theme theme; Theme::load("Theme.json"); if ( loadForm("xyz.json", pPane) ) { Edit *pName = NULL; Check *pCheck = NULL; IControl *peek = NULL; if (pPane->find(1, "Edit", peek) ) pEdit = static_cast(peek); if (pPane->find(2, "Check", peek) ) pCheck = static_cast(peek); }
Once you've obtained suitable control pointers, you can complete the binding process by assigning an accessor the control will use to retrieve it's content. In the example above, that would be a string and a boolean. Tiles are not accessible. Controls created with an ID number of zero are anonymous in that they can't be retrieved through find() method.
Write your own tile objects by declaring a new class (or struct) that inherits from the Tile base class (all Tile objects do this). This is easiest as most of your effort is limited to the Draw method. See the Fill tile as a very easy to read example.
#include "Tile.h" struct Custom : public Tile { /* */ };
To create a custom control, the easiest route is a collection of existing tiles and/or controls. Declare your class with Pane as the base class and selectively override behavior as needed. See the Scroll control as an example of this.
#include "Pane.h" struct Custom : public Pane { /* */ };
For unique controls that can't be built up from tiles or other controls, you must implement the IControl interface. Control is a partial implementation of IControl that you can use as a base class. It delegates nearly all of the ITile methods to a Tile instance declared as a member. The remaining IControl methods (like Draw and the mouse/keyboard events) must be implemented by hand.
#include "Tile.h" #include "Control.h" struct Custom : public Control { /* */ };
Tip: when updating the content of a control - don't call setChanged(true) until you're done with all changes. This will prevent the control from being re-drawn unnecessarily.
Set is the abbreviated name for a property set. It's a collection of meta-data that describes a set of properties. A property contains a text label, a description, and an IControl (used to edit the property's value). Properties are grouped by section. A set contains two lists - a section list and a column list. A column list is just a list of properties. The section list is really a list of lists. The list of sections where each section is a list of properties specific to that section. Take a peek at the Theme Editor below to see a visual presentation of this.
The Set is used to populate Lists (as a list of Sections) and Grids (as a list of Columns). The library provides base classes for constructing your own application specific property sets. Once built, the same Set can drive your user interface through the List and Grid controls.
The SetT<T> template can be used to create a base Set that includes an accessor pointing the Set to the application object to be edited by the List or Grid control. Use the MemberAccessPtr<B, T> template to create an accessor on a specific member of your object. The Check, Combo, Edit and Pick(Color/Font/Path) controls can use the accessor to retrieve and modify a specific field.
See Property.h and Accessor.h for details. Look at the Theme Editor as a working example of this.
List is a scrollable vertical list of grouped properties much like the PropertyGrid in .NET. A list of sections is shown. Each section may be expanded to reveal a list of properties. A description for the currently selected property appears at the bottom - like a footnote - of the control.
The Theme Editor is a Set derived object use to populate a List control. The combination allows users to customize look and feel of your applications.
The Grid control leverages the same metadata that drives the List control to display and edit a list of same-type objects as cells in a scrollable grid. The collection is exposed through an additional layer called a Table. Note that section names and descriptions are not visible from the grid.
The Tab control adds a thin veneer over the Pane to manage a collection of buttons and button-click delegates. You provide a list of button labels and button click delegate pairs. This is adequate for a functioning tab control. The code you bind to each delegate must manage shuffling the contents of an associated panel. I may look into creating a more advanced version that auto-magically swaps out child panels for you but current functionality suffices for now.
These sample applications demonstrate the variety of user interfaces that can be created using this library.
- Calculator - a display-only four function calculator.
- Chess - a display-only (non-playable) chessboard.
- Tic Tac Toe - a functioning game of Tic Tac Toe (X's and O's on a 3x3 grid).
- Demo - demonstrates construction of custom Sets and Tables. Show use of List, Grid, and Tab controls.
- more to come ...
GDI+ - going beyond the plain GDI canvas, a GDI+ canvas should allow for some neat effects.
Why are so many classes declared as struct? Glad you noticed. That's a gimmick I use. I prefer to put all private members at the bottom of the class. Instead of ...
class Object { // private stuff. public: // public stuff. };
I like doing this ...
struct Object { // public stuff. private: // private stuff. };
This is preferable as all the public stuff is near the top of the class definition. I don't need to scroll down (and I don't like adding a half-dozen public/protected/private clauses to me classes).
There are no menus.
All lines and regions are solid colors. Gradients are not supported.
Sorry, no radio buttons. They're a waste of pixels. Use a combo box or check boxes. There are no bitmap tiles either. There is so much variety of iconic glyphs from various symbol fonts that, so far, bitmaps haven't been necessary.
So far, all object names have been short one or two syllable words. That was pure luck. Can't promise new components will adhere to this terse naming convention.
Tip: the OpenSymbol font that is bundled with OpenOffice (so it may be free) includes lots of neat stuff.
© 2011-2012 Rick Parrish