Theming in Kivy

Adding consistency to Kivy's Python UI tools

Kivy has a wonderful set of built-in widgets that can be extended in numerous ways. They have very useful behaviors, but their look and feel may not integrate well with your App or the platforms you are targeting. Kivy doesn’t support theming out of the box right now, but if you poke around enough, there are a range of options you can use to customize the default look of widgets without having to define your own inherited versions of them.

I’ll first introduce you to Kivy’s image atlases, which are less mysterious than they sound, and are important groundwork for understanding theming in Kivy. Then you’ll learn two different ways to do manual theming in Kivy, with an eye to future automation.

Introducing Atlases

To understand theming, you must first understand atlases. An atlas is essentially a collection of distinct images combined into a single image file for loading efficiency. A JSON file describes the location of the separate images inside that master image file so that Kivy can access them directly. If you’ve ever worked with CSS sprites, you know exactly what I’m talking about. If you haven’t, the following example should explain everything.

I started with three images named light_circle.png, dark_circle.png, and red_star.png:

light_circle
dark_circle
red_star

These three images are combined into a single image named custom_atlas-0.png:

custom_atlas-0

This bit of JSON describes where each of the original images lives in the atlas:

{
   "custom_atlas-0.png": {
       "red_star": [
           2,
           48,
           64,
           64
       ],
       "light_circle": [
           68,
           114,
           64,
           64
       ],
       "dark_circle": [
           2,
           114,
           64,
           64
       ]
   }
}

The JSON opens with the filename of the master image it is describing, and then has four lists for each image that was packed into that image. Each list specifies, in order, the x and y coordinates of the original image within the master image, and the original width and height of the image.

If you look closely at those numbers, you’ll probably see that they don’t make much sense at first sight. If you’re used to standard image editors and web browsers, you’re used to image sizes and positions starting in the upper left corner of the file. However, OpenGL, and therefore Kivy, which is built on top of OpenGL, generally bases it’s coordinate systems in the lower left corner. The JSON therefore describes the positions from the bottom up.

Creating an atlas in Kivy

So, as you can imagine, there would be quite a bit of tedious mathematics to create that atlas JSON file by hand. Luckily, Kivy provides a useful tool to automatically create an atlas (comprised of both the master image and the JSON file) from a discrete set of images. For the example above, I first created the three images light_circle.png, dark_circle.png, and red_star.png in the GIMP. I saved these in an images/ subdirectory of my new kivy project folder. Then I ran the kivy atlas create command:

python -m kivy.atlas images/custom_atlas.atlas 256 images/*

This creates both the custom_atlas-0.png and the custom_atlas.atlas JSON file described above. python -m kivy.atlas accepts the name of the new atlas file (which holds the JSON), and the size of the output image, followed by the list of source images. If all images won’t fit in a single output image of that size, multiple output images are described inside the atlas JSON file.

Using an Atlas

You can accesses images in a Kivy atlas anywhere you would normally access an image filename. Kivy’s core loader knows how to look inside an atlas file to find the image you originally requested. For example, with a basic main.py, you can load atlas images into an image or button background using this simple KV language file:

BoxLayout:
   Image:
       source: 'atlas://images/custom_atlas/red_star'
   Button:
       background_normal: 'atlas://images/custom_atlas/dark_circle'
       background_down: 'atlas://images/custom_atlas/light_circle'

One way to create a theme in Kivy: Atlases

As you can see, atlases can be a cool way to organize your custom images, and you can use them in your own widgets as you see fit. But wouldn’t it be nice if you could change the look of all the default widgets in your Kivy file at once? This is possible, but it requires a bit of a hack.

After poking through the Kivy source code, I determined that the default Kivy atlas is named atlas://data/images/defaulttheme. You can see the names of all the images available in that atlas using this code in your interpreter prompt:

import kivy
json.load(open(os.path.dirname(kivy.__file__) + '/data/images/defaulttheme.atlas'))['defaulttheme-0.png'].keys()

There are nearly 90 images available in the Kivy 1.8 default widget set, all combined into the atlas at https://github.com/kivy/kivy/blob/master/kivy/data/images/defaulttheme-0.png.

You probably don’t feel like defining an entire theme right now, but for a custom app, you only really need to theme those widgets you actually use. Let’s use the Button class as an example. By looking at the source code for the button module, you can see that it references four images in the atlas:

  • button
  • button_pressed
  • button_disabled
  • button_disabled_pressed

You can create an atlas with those same image names and then replace the default atlas with those images. Be aware that you should replace the images of ALL the widgets your app uses, since you are actually overwriting the atlas, not amending it.

To theme the Button, you can create four images named
button.png
button_pressed.png
button_disabled.png, and
button_disabled_pressed.png.

Put these in a directory and create an appropriate atlas:

python -m kivy.atlas button_images/button_images 180 button_images/*

Now this atlas has the same keys (for buttons) as the default atlas that comes with Kivy. All you have to do now is tell Kivy to use this atlas instead of the default, and all your buttons will have the background images you specified. It took me a while to work out how to do this. It’s not as clean as I would have liked, but this complete main.py illustrates:

from kivy.app import App
from kivy.cache import Cache
from kivy.atlas import Atlas


class TestApp(App):

   def build(self):
       atlas = Atlas("button_images/button_images.atlas")
       Cache.append("kv.atlas", 'data/images/defaulttheme', atlas)
       super(TestApp, self).build()

if __name__ == '__main__':
   TestApp().run()

This method comes with some caveats. The atlas trick seems to work, but the word “cache” scares me. I don’t know a lot about how Kivy caching works, but I see from the source that like most caches, entries can expire or be invalidated. It would be really weird if the cache suddenly expired and reverted to the original theme. Further, this method only allows theming images, and there is more to a Kivy application than images. Fonts and sizes, for example, are specified using properties that are not stored in atlases.

Theming default properties

It turns out that by a combination of Python’s awesomeness and Kivy’s elegant design, it’s possible to change the default properties on widgets with very little overhead. You can either set the default value on a property or create a new property. The latter is more useful if you want to set a value on a child class without touching the parent class (font_size, for example, is defined on Label and inherited by Button).

from kivy.app import App
from kivy.uix.button import Button
from kivy.properties import NumericProperty

Button.background_normal.defaultvalue = 'atlas://images/custom_atlas/dark_circle'
Button.font_size = NumericProperty(40)


class TestApp(App):
   pass

if __name__ == '__main__':
   TestApp().run()

Notice that the property is being set on the class, not any instantiated object. This means that any object instantiated later will use the new value. I think Kivy uses some metaclass magic to set up the properties on instantiated objects from these values.

Using this simple technique, I worked out a simple theme manager that allows themes to be defined in JSON files during the pycon 2014 sprints. The pull request is still open, but I am optimistic that you can expect to see theming included with Kivy in the next major release!

tags: , , , ,