Changing the widget appearance

The ocepgui.widgets module offers two possibilities to change the provided widget appearance. This section covers the necessary tasks, which have to be performed to change create an own look and feel to match your personal needs.

The first part will give you a rough overview about how the styling and drawing system of the ocempgui.widgets module words, while the second will explain the style files and syntax, that deal with basic attributes such as background colors or fonts. The last part then gives an in-depth explanation, how to provide own drawing methods for a widget to spend it a completely different look and feel (such as rounded borders for buttons for example).

How widgets are drawn

The rendering engine contains several methods to draw each widget and some generalized methods for some commonly used parts such as the basic widget surface. The widget to draw is passed to its matching drawing method, which will look up its style settings and then draw it.

Widget drawing process diagram.

Figure 3. Widget drawing process


The Style supports cascading the widget style settings. This means, that it will, when it retrieves the widget style and does not find the style setting it needs, walk through the inherited classes (the __mro__ attribute) of the widget until it finds the setting. The last instance it looks the setting up in is the "default" entry of its styles attribute.

Customizing the theme files

Theme files for OcempGUI define the most basic settings for the widgets, such as the background color or the font to use. They however do not cause the widget to look completely different. The basic widget layout will stay the same.

The theme files are read by the currently active Style class (which is set in base.GlobalStyle) and their information will be stored in its styles dictionary.

The currently active theme information for a specific widget class are stored in the styles attribute by using its lowercase classname. This means that the specific theme for a Button widget is stored in Style.styles["button"]. For a Label widget it would be Style.styles["label"] and so on.

Such a theme entry is a dictionary, that contains various information about the background and foreground color for the different supported widget states, the font settings for that widget and more. The complete list of possible settings can be found in the documentation of the Style, so it will not be covered in detail here.

Setting up individual styles should be done using an own file for them, so you can easily change them without touching your program code. Let us create a separate theme file, that will cause the buttons to use a blue background color and the labels a different font and foreground color.

After creating the file for that (the author recommends using the suffix ".rc" for theme files), we use the python syntax for the contents. First we have to let the file and its content know about the supported widget states, thus we will import the Constants of the ocempgui.widgets module.

from ocempgui.widgets import Constants
        

Note

Do not use any from ... import * within the theme files to avoid a bloated Style class. The theme loader give you much freedom including the possibility to make many mistakes. You also should not import anything except the Constants module into the theme file, too.

Now we will set up our theme dictionary for the Button widget.

from ocempgui.widgets import Constants

button = { "bgcolor" : { Constants.STATE_NORMAL : (18, 12, 135),
                         Constants.STATE_ENTERED : (32, 25, 164),
                         Constants.STATE_ACTIVE : (18, 12, 135),
                         Constants.STATE_INSENSITIVE : (54, 51, 106) }
           }
        
The above code will set, after loading the style file, the background color of any Button to different types of a dark blue color. It also will affect any inherited widget, that does not

  • implement and own style explicitly,

  • define the style setting in its own style.

You can easily see this by placing a Button and a CheckButton side by side.

Let us now set the styles for the Label widget. The first part sets up the font, its size and if antialiasing should be used, the second one sets up different colors to use for the text according to the widget its state.

from ocempgui.widgets import Constants

...
label = { "font" : { "name" : "Helvetica", "size" : 16, "alias" : True },
          "fgcolor" : { Constants.STATE_NORMAL : (250, 250, 250),
                        Constants.STATE_ENTERED : (150, 150, 150),
                        Constants.STATE_ACTIVE : (0, 0, 0),
                        Constants.STATE_INSENSITIVE : (235, 235, 235) }
          }
        

Typing in those Constants... entries can become a lot of work, especially with complex themes. Thus OcempGUI allows you to define variables to ease your life. Any entry, that is prefixed with an underscore, "_", will be handled as variable, ignored by the loading mechanisms and thus does not go into the style dictionary.

With this in mind, let's substitute those lengthy constants with some variables.

from ocempgui.widgets import Constants

# Variables, that will be used to ease the life.
_normal = Constants.STATE_NORMAL
_entered = Constants.STATE_ENTERED
_active = Constants.STATE_ACTIVE
_insensitive = Constants.STATE_INSENSITIVE

button = { "bgcolor" : { _normal : (18, 12, 135),
                         _entered : (32, 25, 164),
                         _active : (18, 12, 135),
                         _insensitive : (54, 51, 106) }
           }
...
        
Of course you can also use more than a single underscore. The only importance is, that the first letter of the entry starts with an underscore. Thus valid variables would be
_red = (255, 0, 0)
__myfont = "path/to/font.ttf"
____________a_pretty_long_variable_that_is_completely_useless = None
        

As you see, setting up own themes for widgets is not a complex task. Let us now examine the complete code and a small example, that will make use of the just created theme.

You can find the code and theme as python scripts under examples/theme_example.rc and examples/theme.py.

from ocempgui.widgets import Constants

# Variables, that will be used to ease the life.
_normal = Constants.STATE_NORMAL
_entered = Constants.STATE_ENTERED
_active = Constants.STATE_ACTIVE
_insensitive = Constants.STATE_INSENSITIVE

button = { "bgcolor" : { _normal : (18, 12, 135),
                         _entered : (32, 25, 164),
                         _active : (18, 12, 135),
                         _insensitive : (54, 51, 106) }
           }
label = { "font" : { "name" : "Helvetica", "size" : 16, "alias" : True },
          "fgcolor" : { _normal : (250, 250, 250),
                        _entered : (150, 150, 150),
                        _active : (0, 0, 0),
                        _insensitive : (235, 235, 235) }
          }
# Theme usage example.
from ocempgui.widgets import base, Renderer, Button, Label

# Load the theme.
base.GlobalStyle.load ("theme_example.rc")

# Create screen.
re = Renderer ()
re.create_screen (200, 100)
re.title = "Theme example"
re.color = (250, 250, 250)

# Create widgets.
button = Button ("Button")
button.topleft = 5, 5
label = Label ("Label")
label.topleft = 100, 5

re.add_widget (button, label)
re.start ()

Example 49. Customizing the themes


Using own drawing routines

Whenever you need an completely different layout, the creation of own theme files will not be enough. This section explains how to modify and/or override the drawing methods for the widgets.

First you should make yourself clear about if you need to provide an own layout only for specific instances of a widget or if those widgets always should be drawn that way. Dependant on your needs, you have several possibilites.

  • Subclass the widget and override its draw() and draw_bg() methods.

  • Bind the draw() and draw_bg() methods of the instance to your own methods or functions at runtime.

  • Provide an own drawing method for the widget type and bind it to the set drawing engine.

  • Implement an own drawing engine and bind it to the Style instance.

The first both entries should be self-explanatory. The only thing you have to keep in mind when overriding the widget's drawing methods are, that you must invoke at least the BaseWidget.draw() method. This method updates the internals of the widget after drawing the background, does surface conversions and more, so it is an absolutely necessary method to be invoked.

Of course you can also invoke the parent's draw() method, if you (just) want to add something to the widget's look instead of implementing all drawing yourself.

class OwnButton (Button):
    ...
    def draw_bg (self):
        ... # Own background drawing implementation.

    def draw (self):
        # We want the drawing routines of the Button to be executed and
        # just add something to them.
        Button.draw (self)
        ...

class OwnButton2 (Button):
    ...
    def draw_bg (self):
        ... # Own drawing implementation.

    def draw (self):
        # Do not process any drawing of the Button, but use the BaseWidget
        # directly to update the widget internals, because we do anything
        # ourselves
        BaseWidget.draw (self)
        ...
        

So far for the first both entries of the above list. Now let's look at manipulating the drawing engine by overriding it (binding a new method should not be any problem for you, not?).

We will create an own drawing engine subclass using the provided DefaultEngine and use an own drawing method for Label widgets, which adds a dropshadow to them by default. After creating this subclass we will use an instance of it for our own small test application.

The first we have to do is to inherit from the DefaultEngine subclass. The class is not located in the module directly, but installed as additional data and can be imported with help of the DEFAULTDATADIR constant.

# Drawing engine usage example.
from ocempgui.widgets import *
from ocempgui.widgets.Constants import DEFAULTDATADIR

# Get the path where the theme engines are usually installed.
import sys
sys.path.append (DEFAULTDATADIR)
from themes import default

# Override the DefaultEngine with its drawing definitions so that we can
# implement our own draw_label() method.
class DropShadowEngine (default.DefaultEngine):
    def __init__ (self, style):
        default.DefaultEngine.__init__ (self, style)
        
Now our own drawing engine contains anything, the DefaultEngine class contains. To provide our own drawing method for the Label, we have to override the matching method, which is draw_label().

As you can see from its signature, it receives one argument, the Label object, for which the surface should be created.

Note

All widget drawing methods follow the same naming scheme, which is draw_WIDGETTYPE (self, widget) with widgettype being the all-lowercase classname.

    def draw_label (self, label):
        """Overrides the label drawing method and adds a dropshadow."""
        # We do not care about multiline labels. Thus pass those to the
        # parent.
        if label.multiline:
            return DefaultEngine.draw_label (self, label)
        
To keep it simple, we do not take care of multiline labels for now, thus we let its parent handle those.

Now we shorten some attributes, which we will need in our method more often.

    def draw_label (self, label):
        ...
        cls = label.__class__
        state = label.state
        
        # Peek the style of the label so we can get the colors later.
        st = label.style or self.create_style (cls)
        
The __class__ and state attributes are used by many methods of the DefaultEngine class to look up the theme information of the widget. The st variable will be used to get and set the matching color for the dropshadow of our Label.

Now we have to get the foreground color to use for the text and store it temporarily as we will modify it in the st variable later.

    def draw_label (self, label):
        ...
        # Save the label color temporarily as we will change it for the
        # dropshadow. Because we are using references, not plain style
        # copies, we have to do this.
        fgcolor = self.get_style_entry (cls, st, "fgcolor", state)

        # The 2px added here are used for the dropshadow.
        width = 2 * label.padding + 2
        height = 2 * label.padding + 2
        
We also set up the width and height, our Label surface will occupy. Because the Label offers a padding attribute, we have to take it into account for the surface size.

The basics are set up, so that we can proceed with drawing the surfaces for our widget.

    def draw_label (self, label):
        ...
        if label.mnemonic[0] != -1:
            front = self.draw_string_with_mnemonic \
                    (label.text, state, label.mnemonic[0], cls, st)
            # Swap colors.
            st["fgcolor"] = { state : (100, 100, 100) }
            drop = self.draw_string_with_mnemonic \
                   (label.text, state, label.mnemonic[0], cls, st)
        else:
            front = self.draw_string (label.text, state, cls, st)
            # Swap colors.
            st["fgcolor"] = { state : (100, 100, 100) }
            drop = self.draw_string (label.text, state, cls, st)
        
As you can see, we first look, if a mnemonic is set for the Label and then use either the mnemonic string drawing method or the simple string drawing method. We do that twice, for the normal text and its dropshadow. In line eleven and nineteen we swap the foreground color of the st variable with a bright grey one.

So we have our both surfaces now and are nearly done. But we have to respect some definitions of the BaseWidget and we have to reset the swapped colors.

    def draw_label (self, label):
        ...
        # Surface creation done. Restore the colors.
        st["fgcolor"][state] = fgcolor

        # Get the size of the surface(s) and add it to the complete
        # width and height, the label will occupy.
        rect = front.get_rect ()
        width += rect.width
        height += rect.height
        
        # Guarantee size.
        width, height = label.check_sizes (width, height)
        
Now we are ready to blit the surfaces and return the complete label surface.
    def draw_label (self, label):
        ...
        # Blit all and return the label surface to the caller.
        surface = self.draw_rect (width, height, label.state, cls, st)
        surface.blit (drop, (label.padding + 2, label.padding + 2))
        surface.blit (front, (label.padding, label.padding))
        return surface
        

So far so good. We now have our own drawing engine, that will draw Label widgets with a dropshadow by default. The very last thing to do is to make the omcepgui.widgets module use our own class instead of the default. This is done by assigning the base.GlobalStyle.engine attributes an instance of our own class in the code. The drawing methods will then make use of it.

# Set out own drawing engine in the Style class.
base.GlobalStyle.engine = DropShadowEngine (base.GlobalStyle)
        
Now all Label widgets will use the just created draw_label() method.

The complete code of the previous example follows. It includes some test code, so you can easily evaluate the results.

You can find the code as python script under examples/drawing_engine.py.

# Drawing engine usage example.
from ocempgui.widgets import *
from ocempgui.widgets.Constants import DEFAULTDATADIR

# Get the path where the theme engines are usually installed.
import sys
sys.path.append (DEFAULTDATADIR)
from themes import default

# Override the DefaultEngine with its drawing definitions so that we can
# implement our own draw_label() method.
class DropShadowEngine (default.DefaultEngine):

    def __init__ (self, style):
        default.DefaultEngine.__init__ (self, style)
    
    def draw_label (self, label):
        """Overrides the label drawing method and adds a dropshadow."""
        # We do not care about multiline labels. Thus pass those to the
        # parent.
        if label.multiline:
            return DefaultEngine.draw_label (self, label)

        cls = label.__class__
        state = label.state
        
        # Peek the style of the label so we can get the colors later.
        st = label.style or self.style.get_style (cls)

        # Save the label colour temporarily as we will change it for the
        # dropshadow. Because we are using references, not plain style
        # copies, we have to do this.
        fgcolor = self.style.get_style_entry (cls, st, "fgcolor", state)

        # The 2px added here are used for the dropshadow.
        width = 2 * label.padding + 2
        height = 2 * label.padding + 2

        # Backup the color.
        tmp = None
        if st.has_key ("fgcolor"): # Is fgcolor defined?
            tmp = st["fgcolor"]
        
        # Draw the text.
        front = None
        drop = None
        if label.mnemonic[0] != -1:
            front = self.draw_string_with_mnemonic \
                    (label.text, state, label.mnemonic[0], cls, st)
            # Swap colors.
            st["fgcolor"] = WidgetStyle ({ state : (100, 100, 100) })
            drop = self.draw_string_with_mnemonic \
                   (label.text, state, label.mnemonic[0], cls, st)
        else:
            front = self.draw_string (label.text, state, cls, st)
            # Swap colors.
            st["fgcolor"] = WidgetStyle ({ state : (100, 100, 100) })
            drop = self.draw_string (label.text, state, cls, st)

        # Surface creation done. Restore the colors.
        if not tmp:
            del st["fgcolor"]
        else:
            st["fgcolor"] = tmp

        # Get the size of the surface(s) and add it to the complete
        # width and height, the label will occupy.
        rect = front.get_rect ()
        width += rect.width
        height += rect.height
        
        # Guarantee size.
        width, height = label.check_sizes (width, height)

        # Blit all and return the label surface to the caller.
        surface = self.draw_rect (width, height, label.state, cls, st)
        surface.blit (drop, (label.padding + 2, label.padding + 2))
        surface.blit (front, (label.padding, label.padding))
        return surface

if __name__ == "__main__":
    re = Renderer ()
    re.create_screen (200, 200)
    re.title = "Style example."
    re.color = (234, 228, 223)

    # Set out own drawing engine in the Style class.
    base.GlobalStyle.engine = DropShadowEngine (base.GlobalStyle)

    label = Label ("Example label")
    label.create_style ()["font"]["size"] = 30
    label.topleft = 10, 10

    label2 = Label ("#Mnemonic")
    label2.create_style ()["font"]["size"] = 30
    label2.topleft = 10, 60

    button = CheckButton ("Dropshadow")
    button.topleft = 10, 100
    re.add_widget (label, label2, button)
    re.start ()

Example 50. Customizing the widget drawing routines


Changing the appearance of single instances

While the last sections dealt with changing the appearance of whole widget classes, this section will explain how to change particular bits of a single widget instance.