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 (see the section called “Using own drawing routines” for details about the currently active class) 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 an theme entry is a dictionary, that contains various information such as 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
        

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) }
          }
        

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

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) }
           }
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) }
          }
# Theme usage example.
from ocempgui.widgets import *

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

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

# Create widgets.
button = Button ("Button")
button.position = 5, 5
label = Label ("Label")
label.position = 100, 5
re.add_widget (button, label)
re.start ()

Example 40. 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 and 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() method.

  • Bind the draw() method of the instance to your own method or function at runtime.

  • Provide an own Style subclass with that particular drawing method for the widget type.

While the first both entries should not need any explanation, the last one includes a bit more work, so let us take a closer look at it.

We will create an own Style subclass 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 Style subclass.

from ocempgui.widgets import Style

class OwnStyle (Style):
    def __init__ (self):
        Style.__init__ (self)
        
Now our own style class contains anything, the Style class contains, too. 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).

We will use the existing draw_label() method as template for our own one and modify it, so it matches our needs.

    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 Style.draw_label (self, label)
        
To keep it simple, we do not take care of multiline labels for now, thus we let our 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.get_style (cls)
        
The __class__ and state attributea are used by many methods of the Style 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 will 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):
        ...
        # 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,
                                                    label.style)
            # Swap colors.
            st["fgcolor"] = { state : self.get_style_entry (cls, st,
                                                            "lightcolor",
                                                            state) }
            drop = self.draw_string_with_mnemonic (label.text, state,
                                                   label.mnemonic[0], cls, st)
        else:
            front = self.draw_string (label.text, state, cls, label.style)
            # Swap colors.
            st["fgcolor"] = { state : self.get_style_entry (cls, st,
                                                            "lightcolor",
                                                            state) }
            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 the brighter one, that is set in st dictionary.

You might wonder, why we use the get_style_entry() method here, as we could simply assign their both variables. If you remember correctly, what was stated in the section called “Changing the widget appearance”, the style information are cascaded, thus it is not guaranteed, that the lightcolor values are set. Therefore we will use this (more or less) failsafe method to retrieve a valid style entry for the lightcolor.

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.
        if width < label.size[0]:
            width = label.size[0]
        if height < label.size[1]:
            height = label.size[1]
        
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, label.style)
        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 Style subclass, 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 variable an instance of our own class in the code, it should make use of it.

base.GlobalStyle = OwnStyle ()
        
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/style.py.

# Style usage example.
from ocempgui.widgets import *

class OwnStyle (Style):
    def __init__ (self):
        Style.__init__ (self)

    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 Style.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.get_style (cls)

        # 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

        # 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,
                                                    label.style)
            # Swap colors.
            st["fgcolor"] = { state : self.get_style_entry (cls, st,
                                                            "lightcolor",
                                                            state) }
            drop = self.draw_string_with_mnemonic (label.text, state,
                                                   label.mnemonic[0], cls, st)
        else:
            front = self.draw_string (label.text, state, cls, label.style)
            # Swap colors.
            st["fgcolor"] = { state : self.get_style_entry (cls, st,
                                                            "lightcolor",
                                                            state) }
            drop = self.draw_string (label.text, state, cls, st)

        # 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.
        if width < label.size[0]:
            width = label.size[0]
        if height < label.size[1]:
            height = label.size[1]

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

if __name__ == "__main__":
    base.GlobalStyle = OwnStyle ()

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

    label = Label ("Example label")
    label.get_style ()["font"]["size"] = 30
    label.position = 10, 10

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

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

Example 41. Customizing the widget drawing routines