OpenTheme is an open-source package of graphic user interface (GUI) toolkits. It is implemented in C#.Net. It contains a .Net foundation class library for developers and a graphic tool to create graphic presentations with arbitrary look and feel for developers, graphic designers and even end-users.
The design goal is to completely separate the graphic design from the application implementation. In order to achieve this goal a contract with minimum set of rules has been designed in the ThemeEngine foundation class. As long as both the software developers and graphic designers follow the rules in contract, the graphic implementation and application implementation can be done independently. Also the GUI presentation can be modified with maximum flexibility later on without modifying the application binary code. Multiple presentations with different localization support, or different look-and-feel or even different layout is also trivial.
With the current design it is safe to claim that all commonly used component like TextBox, ImageIcons, ScrollBars, Comboxes Buttons can be easily presented by OpenTheme with minimum effort from both developers and graphic designers.
This tutorial is targeted to all parties including software developers, graphics and end users. It is highly recommended that everybody interested in working with OpenTheme read this documentation first. After all, a picture is worth 1000 words. That's one of the reasons I built the OpenTheme.
Figure 1.1 is a ThemeEditor window with a working ThemePack loaded.
Figure 1.1
The ThemeEditor has several working areas. The left side is the ToolBox, which contains all available graphic components, such as TextBox, ColorBox, PictureBox, ScrollBar etc. Software developers can derive their own components, which can be compiled into DLLs and loaded and recognized by the ThemeEditor.
The middle area contains the actual WYSIWYG (what-you-see-is-what-you-get) presentation of your current design. Also a message box is shown in the lower part to display the status and error (if any) messages.
The right part contains two panels. The upper part is called the ThemePack Explorer, which shows the detail structural relationship of the current Theme collection. The lower panel is called the Properties panel, which enables the user to view and modify the properties of any single component in the current collection.
The GUI of ThemeEditor tool is very flexible. Users can dock the different panels in whatever way they like. Inside each panel, hot keys, mouse left/right click and drag-and-drop should all work as expacted.
Summary:
For everybody:
All components are derived from ThemeObject. A ThemeObject can physically
contain other ThemeObjects to create graphic presentation with layered
graphics. Users can build different ThemeObjects they need for an application
and package all those ThemeObjects into a Theme. So the Theme is a collection
of a set of ThemeObjects. Accordingly, a ThemePack is a collection of different
Themes. Any ThemeObject can be given a name (ID property) so that it can be
referenced from associated program.
Here is a possible use case. Graphic designers first build a set of
commonly used ThemeObjects, such as
TextLabel, Button,
RadioButton, Scrollbar,
ProgressBar
etc. Then they can build the ThemeObjects of actual user interfaces that
contain those basic components. All of them will be put into a Theme, which
contains the entire set of GUI components an application needs. Later on, the
graphic designers can build other Themes with different localization support or
different look-and-feels for the same application. All those Themes should be
put into one single ThemePack.
ThemePack file must be named as
OpenThemePack.xml. All Theme files contained in
that ThemePack should be put into the same directory as the ThemePack
file. All reference files (image files) must be put into the same or
sub-directories of the
ThemePack file. This arrangement guarantees that the entire Theme can be packed
up with directory structure associates with file reference without any file
goes beyond that scope.
For developers:
Output file is the serialization of the tree structure of the ThemeObjects.
Take a look at the source code of ThemeObject, ThemePack and Theme class for
additional information.
This chapter demonstrates some basic ideas about the design and implementation of the OpenTheme project. A GUI interface of OpenHTPC shown in Figure 1.1 will be used as the example.
In this example, we are going to create a new full screen GUI. It contains a title bar, a status bar and a menu box in the middle with several menu buttons. There's also a background consisting a static crustal blue background image and a dynamic semi-transparent image on the lower right part of the screen.
On the left side of the title bar, there are a set of icons indicating the currently connected media types, which can be turned on and off dynamically in program. On the right side there's a title of "OpenHTPC" and a small dynamic icon. The entire title bar is covered with a alpha-blended image to create a "hovering crystal" style.
Figure 2.1.1
Figure 2.1.1 shows a typical directory structure for a ThemePack. Everything is packed with in the Themes directory. The Themepack file (OpenThemepack.xml) locates in that directory. Other Theme files should be in the same directory as the ThemePack file. Any other resources (currently only images) should be located in the sub-directories. Any reference to the resource files in any xml file uses relative pathes.
Let first delete any existing ThemePack and Theme file (note the other resource files in sub-directories should NOT be deleted). Now let's create a new Theme from scratch.
First go to menu item, File > New > ThemePack.
Figure 2.2.1
Then create a new ThemePack in the Themes directory. Note the file name of ThemePack should only be "OpenThemePack.otp".
Figure 2.2.2
Select File > New > Theme to add a new Theme into the current ThemePack. We name the new Theme as "Crystal.English.ott". Now the ThemePackExplorer and Properties panels should look like Figure 2.2.3.
Figure 2.2.3
The ThemePackExplorer shows the tree structure of the entire ThemePack, while the Properties panel displays the detail information of each node in the ThemePack tree. Some properties are editable.
For example, user has to give each Theme a unique name, as shown in the ThemeName property in Figure 2.2.3. ThemeName doesn't have to be the same as the Theme file name. It is used to reference the Theme by name dynamically in program.
Here we give the Theme a name of "Crystal.English".
Probably you have already noticed the Theme has references of Color, Fonts, Images and Texts. Those references are specified by users and referenced by programs by name. For example, it's a common practice to programetically display dynamic images in program. Those images can be specified as references, so that the images can be changed for different look-and-feel later on without modifying the program.
All colors in OpenTheme are in ARGB format, where A mean alpha (transparency). Each ranges from 0 to 255.
It's also a common practice to define all text strings used by program in a resource file (in this case it is the Theme file) and give each text string a unique name. In program always get string from resource file by name reference, so that it's easy to port the entire application to a different language (localization, e.g. English to Chinese) by just creating another Theme.
Summary:
ThemePack is a collection of a set of Themes. ThemePack file, Theme
files and all resource image files should be kept in a structured directory in
the file system.
Theme has references like Font, Color, Text and Image. Those references can be
referred by name dynamically in program.
Right click on the Crystal.English Theme item inside the ThemePackExplorer panel. Then click "New ThemeObject".
Figure 2.3.1
A dialog will pop up as shown in Figure 2.3.2. Now we try to create a root window, with a pre-defined picture as the background. So we choose an OpenThemePictureBox. Initially we set the size as 640X512, just for demonstartion purposes.
Figure 2.3.2
By default the PictureBox, as most other components, is
completely transparent. In order to show the transparency, the
background is always set to a pattern of alternating gray and
white blocks.
The first thing you need to do is to give the new ThemeObject a name, which is
called "ID". The "ID" can be used to dynamically identify the
ThemeObjects in a program. First we use the mouse to highlight the new item
node in the ThemepackExplorer tree. Then we go to the property panel to set the
ID of this new item. Once it is done, the new ID is shown everywhere, as shown
in Figure 2.2.3.
Figure 2.3.3
Note: ID is a very important identifier in OpenTheme. In
ThemeEditor, every new component will be automatically assigned a new ID at
creation time. The new ID is unique within its level of tree. You can think the
tree as a structure of the file system; where there should be files with
duplicated file names within the same directory (I do use an XML path to
identify reference in the serialization).
ID also must follow a certain naming rules. Basically a name of an ID must
start with a 'letter' and should only contain letters, or digits, or '_' in its
name. So an ID should be used as a valid variable name in popular programming
languages and vice versa. The reason I set such limitation is because I found
the best way to represent ThemeObject IDs is to use the enumeration data type,
which could be further used in automatic validation.
As its name indicated, an OpenThemePictureBox should contain a picture. It is specified in the ThemeImage property. By default the OpenThemePictureBox doesn't have any image so this property shows "(None)". Figure 2.3.4 demonstrates how to set a picture.
Figure 2.3.4
By clicking "Browse", an Open image file dialog will pop up.
Note the dialog has a special preview panel for your convenience.
Load the "Crystal\Image\crystal_back.jpg" file. Now you can see the background
of this "MainScreen" ThemeObject.
A "ThemeImage" has the following properties:
Try to play around those properties to see different effect. Then set Alpha=1, HScale=VScale=Stretch and KeepAspectRatio=false.
In order to create the cover of the title bar, we need the following images.
frame_title_left.gif , frame_title.gif , and frame_title_right.gif .
We want the title bar has a 10% of total height of the screen, while keep a gap of 1% from the top and the left and right edges.
We first create the left part end of the title bar. In order to do this we need to create another OpenThemePictureBox inside MainScreen. Drag the "OpenThemePictureBox" from "ToolBox" panel to the "MainScreen" item in "ThemePackExplorer", as shown in Ficure 2.3.5.
Figure 2.3.5
Now a new OpenThemePictureBox has been created with the "MainScreen", as shown in Figure 2.3.6.
Figure 2.3.6
As the time of creation, a child object always takes 100 space of the parent object. In order to adjust the layout user has to modify the properties. I always believe in careful planning and I don't believe any kind of improvise can provide accurate position to pixel level with bare hand and naked eyes. And the snap-to-grid just never worked for me because it's not flexible enough.
Anyway, as a good habit, we give the newly created picture box a new of "TitleLeft" and load the image file "Crystal\Image\frame_title_left.gif" into the ThemeImage property. Now we see the new image completely covered the background.
As we mentioned before, the title bar should be 10% of the height of the screen and should leave 1% margin on each edge. So we set the X = Y = 0.01, Height = 0.1.
Figure 2.3.7 illustrates a look of the LeftBar with a fixed width of 0.05 (5% of the width of parent object, the MainScreen). Although it also looks beautiful, it apparently doesn't keep the aspect ratio.
Figure 2.3.7
Now let's modify other properties, as shown in Figure 2.3.8. The picture looks just right.
Figure 2.3.8
Let me explain the rule for properties we have known so far.
Now since we positioned the left part of the title bar, let's deal with the right end of the title bar. But we are having problem we didn't have before. The right end of title bar has to keep a gap of 1% of width/height from the right/top edge of the "MainScreen". Since the right bar should also have FixedWidth property turn on, the calculation of width will depend upon the OpenThemePictureBox itself.
But we can still correctly position the right end of title bar, with the introduction of other properties, HAlign and the object reference properties, e.g. RightAdjacent.
Let's first add another component, OpenThemeContainer, into MainScreen. We give it a name "RightPadding" Then we modify the property of "RightPadding" as shown in Figure 2.3.9. Note this time we change the value of a new property, HAlign, from "Left" to "Right". Visually from the center window we can see that component highlighted by a gray box. It's too slim (only 1% of the width of MainScreen component), though, as it should be.
Also please remember, by default an OpenThemeContainer, as many other component, is completely transparent, although it has bounds properties like X, Y, Width and Height.
Figure 2.3.9
Now we can add the right end of the title bar. Drag and drop another OpenThemePictureBox into "MainScreen". Set the ThemeImage property as "Crystal\Image\crystal_back.jpg", and other properties as shown in Figure 2.3.10.
Figure 2.3.10
You can try to add the middle part as an exercise. Just drag and drop another OpenThemeImage, set Y= 0.01, and LeftAdjacent= "LeftBar", RightAdjacent= "RightBar", and of course load the image of "Crystal\Image\frame_title.gif". Then give the new component a new "MiddleBar".
Before we save this ThemePack as a milestone, let first change the Alpha value of those three images from 1 to 0.25, to make them transparent. Now we have a title bar with 10% height in the "MainScreen", as shown in Figure 2.3.11.
Figure 2.3.11
Now let's go through the properties we introduced in this sub-chapter.
The "RightPadding" component also plays a very important role. Although it is completely transparent so it couldn't be seen by the end users, it actually provides a dimension for reference by other components.
With the help of left and right references, even if the FixedWidth is set to true, the width value can still be overridden by those reference properties. That's why I mentioned before that although FixedWidth and FixedHeight can not both be turned off may sound to be a problem at first but it can be worked around. In another word, the values FixedWidth and FixedHeight rather determines whether X or Y will be evaluated first.
Of course there's another way of creating the title bar. We can first create a container with a Width of 0.98 and Y=0.01, Height=0.1, then set HAlign=Center. Then we can create LeftBar, RightBar and MiddleBar inside that container.
The icons of media type indicators are just a series of OpenThemePictureBox adjacent to one another. This time we set the Height of each OpenThemePictureBox as 0.06 and let them floating in the center of "MiddleBar". In the example file those icons are named from "Device1" to "Device5".
Figure 2.3.12
Since I want each media icons can be turned on and off dynamically in program, I set another property "ShrinkInvisible" to "True". It works with the property "Visible". If the Visible property of a component is false, if the FixedWidth property is false, the width will be 0. The same is true for FixedHeight. The bounds of other components that reference this component may be affected accordingly. This property can be used to create "Flow layout".
There's another icon created on the right side call "MainIcon".
"OpenThemeTextBox" is another built in component. Apart from the standard properties, it also has additional properties, including font and height etc. In our example, we put an OpenThemeTextBox into the title bar, and we name it "Title".
As shown in Figure 2.3.13, we can customize the Theme to use different language as long as we have the font installed. And we can display text strings correctly in as many different languages as we want within one single screen.
Figure 2.3.13
As I mentioned before, a component can contain other components on and on. The structure is a tree from a single top level component.
When rendering, a component is clipped by the bounds of its parent component.
When calculating the bounds of components, the algorithm always starts from the top level component, then follow the so called "depth first" order. For those who are not familiar with the algorithms, the order is just the order displayed in the "ThemePackExplorer", from top to down.
For those component with adjacent/alignment properties set, the referenced components must have already had their bounds determined. So a component can only reference the objects displayed "on top" of it in the "ThemePackExplorer" window.
The order can be changed later on by right click mouse on the item in the tree list, as shown in Figure 2.3.14.
Figure 2.3.14
By default, the rendering follows the same order as shown in ThemepackExplorer window from top to bottom. For a component, being rendered first means it may soon be covered by those later rendered components.
Here comes a problem. Some times we have to put some component "before" some other component for reference reason, i.e. in order to use the former object as reference to determine the bounds of later objects. However, sometimes we don't want them to follow exactly the same order in rendering.
In order to solve that problem I introduced the RenderPriority property. It ranges from -128 to +128 with a default value of 0. In the render process, the component with a higher RenderPriority will be rendered first, if the RenderPriority is the same, the rendering order will follow their natural orders.
Figure 2.3.15 displays three different screen shot of the title bar. The first one is with the natural order (every component has a RenderPriority of 0). The second one set the RenderPriority of MiddleBar to -1 so that it will be render later to cover the icons and title text. The third one further set the RenderPriority of title text box to -2 to full it one the top (which means it is rendered last).
Figure 2.3.15
Summary:
So far we have learnt all basic properties:
One of the design principles of the OpenTheme project is to first define a bunch of 'atomic' components, then define a set of rules to manage the layout of the components. The components we learnt so far are all atomic components, such as OpenThemePictureBox, OpenThemeTextBox and OpenThemeColorBox (it's just a block filled with a color, with alpha transparency).
The container can contain sub-components, just like the atomic components. In addition, if the FixedWidth or FixedHeight is set to false, the container is able to determine its optimal size based on the optimal size of ALL components it contains.
Here is an example, in out sample ThemePack, there is a 'ContainerTest' object. As shown in Figure 2.4.1, it contains one container. The container contains three objects, one ColorBox as 'BackgroundColor', which fills the entire area, one PictureBox and one TextBox. The PictureBox is one the left and TextBox is one the right, both with FixedWidth set to false. The FixedWidth of the OpenThemeContainer is also set to false, of course.
Figure 2.4.1
Now let's modify the 'Text' property of the OpenThemeText object. We can see the optimal width of the TextBox adjusts with different text, so does the container.
Figure 2.4.2
The button derives from the container, it has additional states such as Enabled, Active, Focused and Toggled. The combination of different state is even more complicated. We have a total of eight different states, Enabled, Disabled, Active, Focused, Toggled, ToggleDisabled, ToggleActive, ToggleFocused. (Hopefully the button can not be focused or active when disabled). Figure 2.4.3 shows the look and feel of our 'ButtonTemplate' with different states.
Disabled
Enabled
Active
Focused
Toggled
ToggleDisabled
ToggleActive
ToggleFocused
Figure 2.4.3
To represent the different arbitrary look-and-feels of different state, a button is designed to have all eight different layers of components added to a single button. But only one layer will be visible according to the current state. The object layout of our sample 'ButtonTemplate' is shown in Figure 2.4.4.
Figure 2.4.4
In Figure 2.4.4, under the button object, there are 8 different layers, from LayerNormal to LayerToggleFocused. The sub-components of 'LayerNormal' is also shown.
With ThemeEditor user can customize which sub-components is visible for any given state of button, as shown in Figure 2.4.5. All sub-components are listed as a tree structure in the ThemeEditor. In Figure 2.4.5, the 'LayerNormal' component and all its sub-component is checked for 'EnabledObjectList', which means only those components will be visible if the button is in 'Enabled' state.
All text objects on a button can also be gathered together in the 'TextObjectList' collection. Whenever the 'text' property of a button is changed, the text of all TextBox components in the list will also be modified.
Figure 2.4.5
Some people may still feel that it is awkward. So does I. Fortunately, the copy and paste function will help. Users can first design the layers outside the button and copy them into the button later on. And remember; only one button need to be made as a 'template' and all other buttons can be cloned and modified from that template.
In the 'OpenHTPC' MainScreen example, there is a list of buttons in the middle of the screen. They have totally different look-and-feel from this example. Nevertheless, in program, they are exactly the same type of objects.
The OpenTheme also implemented several other specially designed containers.
Figure 2.4.6
The layout effect of OpenTheme design is quite persistent. Figure 2.4.7 shows the same theme with nothing changed but the screen aspect ratio. This time it's shown in a more modern 16:9 TV instead of the traditional 4:3 screen. It still looks good.
Figure 2.4.7
Summary:
The invention of Container made the logic of OpenTheme complete.
With the combination of simple rule and simple component, people are free to
create any look-and-feel they could possibly imagine. The introduction of
visual design tool completely changed the cost factor (just imagine how one
could possibly create any component above with a text editor and pure
imagination). That's exactly why we make and use tools. That's exactly what the
industrial revolution is about.
Nevertheless, sometimes we still need programmer to write some code. The
following is the complexity of our well-known components, in term of number of
lines.
OpenThemeColorBox |
26 lines |
OpenThemeContainer |
169 lines |
OpenThemeHGridBox |
148 lines |
OpenThemeVGridBox |
148 lines |
OpenThemeScrollBar (including HScrollBar and VScrollBar) |
320 lines |
OpenThemeTextBox |
240 lines |
OpenThemeButton |
393 lines |
Here is some guidelines I follow during the design time of the OpenTheme.
Copyright (c) 2004 Qingjun Wei, All right reserved!