Events and Properties - Creating Apps in Kivy (2014)

Creating Apps in Kivy (2014)

Chapter 2. Events and Properties

In this chapter, you’ll learn what Kivy means, specifically, by the words event and property. You’ll also learn how to respond to events using event handlers and how changing properties automatically fires events on those properties. I’ll include a digression on choosing client libraries. By the end of the chapter, you will be able to search for weather locations anywhere in the world.

What Is an Event?

Dictionary.com defines an event as “something that happens, especially something important.” That’s a perfect description of events in Kivy. Kivy is firing events all the time, but you only have to pay attention to those that you consider important. Every graphical toolkit has some concept of events. The difference between Kivy and those other toolkits is that in Kivy, event dispatch and handling are sane and uncomplicated.

Like most user interface toolkits, Kivy provides an event loop. This is executed by your Python code when you call the run method on WeatherApp. Underneath the hood, this method is constantly cycling through events such as touch or mouse motion, clock ticks, keyboard entry, accelerometer input, and more. When something interesting happens, it does the necessary processing to make sure that your code knows the event has happened and has a chance to respond.

So if an event is something that happens, an event handler is something that responds to something that happens. In Kivy, an event handler is just a function or method. By the end of this chapter, your event handler is going to search for potential locations to retrieve weather for and add them to the ListView. But you can start a bit smaller: how about just printing a message to the console from which your Kivy app is running?

Adding Logic to a Custom Widget

I have a guideline. It’s a strict guideline—you might call it a rule—but there are legitimate reasons to break it. The guideline is this: layout and graphical information should always go in the KV language file. Logic (the calculations and activities that make up a program) should always go in a Python file. Keeping these two types of information separate will save hours in the long-term maintenance of your app.

You may have noticed in the previous chapter that I didn’t touch the Python file at all. All the changes happened in the KV language file. This is because that chapter was entirely related to the user interface. This chapter is going to do some logic. Granted, that logic is going to change the user interface (for example, by updating the values in the ListView), but such activity still belongs in the Python file.

The Python file, therefore, needs to know about the AddLocationForm custom widget that you defined in Chapter 1. I kind of cheated in that chapter by allowing the KV language file to create a class dynamically using the @BoxLayout syntax. Take that out first, as shown in Example 2-1.

Example 2-1. Making AddLocationForm into a normal, rather than dynamic, class

AddLocationForm:

<AddLocationForm>: # 1

orientation: "vertical"

BoxLayout:

height: "40dp"

size_hint_y: None

TextInput:

size_hint_x: 50

Button:

text: "Search"

size_hint_x: 25

Button:

text: "Current Location"

size_hint_x: 25

ListView:

item_strings: ["Palo Alto, MX", "Palo Alto, US"]

1

The @BoxLayout was removed, changing this into a normal class.

You won’t be able to run this KV language file because you took out the Kivy magic that allows it to know what kind of class it’s supposed to be. Dynamic classes are a shortcut in Kivy that are most often useful if you want to reuse the same widget layout settings—without logic—in multiple locations. For example, if you had a group of buttons that all needed to be styled similarly, you could create a dynamic class that extends @Button and set the relevant properties on them. Then you could use instances of that class in multiple locations, instead of having a bunch of duplicate code for all the buttons.

However, AddLocationForm is a rather normal class that needs logic attached to it. Start by adding a class definition to the main.py file, as shown in Example 2-2.

Example 2-2. Adding a class to main.py

fromkivy.appimport App

fromkivy.uix.boxlayoutimport BoxLayout # 1

classAddLocationForm(BoxLayout): # 2

pass

classWeatherApp(App):

pass

if __name__ == '__main__':

WeatherApp().run()

1

Remember to import the class you are extending.

2

Inheritance is used to create a new subclass of BoxLayout with no extra logic just yet. The rule in the KV language file, which matches this class based on the class name, will apply all the additional styling for this form. This styled class is then set as the root class in the KV language file.

If you now run python main.py, it will behave exactly the same as at the end of Chapter 1. Not much gain, since all you’ve done is make the code more verbose, but you’ll be adding logic to this class right away.

Responding to Events

Quickly add a method to the new class that prints a short maxim to the console when it is called, as shown in Example 2-3.

Example 2-3. Adding some very simple logic

classAddLocationForm(BoxLayout):

def search_location(self): 1

print("Explicit is better than implicit.")

1

The method doesn’t accept any arguments. This is just a normal method; it’s not an event handler.

You’ll be adding an event handler to the KV language file now. It’s just one line of code. The boundary between user interface and logic should, in my opinion, always be one line of code in the KV language file. That single line of code can, and should, call a method in the Python file that does as much processing as is required. Have a look at the modified code for the search button in Example 2-4.

Example 2-4. Hooking up the event handler

Button:

text: "Search"

size_hint_x: 25

on_press: root.search_location() 1

1

The event handler is accessed as a property on the Button object with a prefix of on_. There are specific types of events for different widgets; for a button, the press event is kicked off by a mouse press or touch event. When the press event happens, the code following the colon—in this case, root.search_location()—is executed as regular Python code.

When you run this code, you should see the phrase Explicit is better than implicit display in the console every time you press the Search button in the interface. But what is actually happening?

When the press event fires, it triggers the event handler, which is essentially just a method on the Button class named on_press. Then it executes the contents of that method, which has been defined in the KV language file to contain the single line of code root.search_location().

Assume, for a second, that the root variable points at an instance of the class that is leftmost indented in this KV language block—that is to say, the <AddLocationForm> class rule. This object has a search_location method, since you added it just a few moments ago, and that method is being called. So, each time you touch the button, the print statement inside search_location is executed.

In fact, that assumption is correct. When the KV language executes anything as raw Python code, as in these event handlers, it makes a few “magic” variables available. You just saw root in action; it refers to the leftmost indented object: the current class rule. The self variable refers to the rightmost indented object. If you accessed the self.size property from inside the on_press handler, you’d know how big the button was. Finally, the app variable refers to the subclass of App on which your code originally called the run method. In this code, it would be an instance ofWeatherApp. This isn’t that useful in your current code, but when you start adding methods to WeatherApp, the app magic variable will be the way to access them.

Accessing Properties of KV Language Widgets

Before you can search for the value that the user entered into the text box, you’ll need to be able to access that value from inside the Python code. To do that, you need to give the widget in question an identifier, and then provide a way for the Python file to access that named object.

This is a good time to delve into the Kivy concept of properties. Kivy properties are somewhat magical beings. At their most basic, they are special objects that can be attached to widgets in the Python code and have their values accessed and set in the KV language file. But they add a few special features.

First, Kivy properties have type-checking features. You can always be sure that a String property does not have an integer value, for example. You can also do additional validation, like ensuring that a number is within a specific range.

More interestingly, Kivy properties can automatically fire events when their values change. This can be incredibly useful, as you will see in later chapters. It’s also possible to link the value of one property directly to the value of another property. Thus, when the bound property changes, the linked property’s value can be updated to some value calculated from the former.

Finally, Kivy properties contain all sorts of knowledge that is very useful when you’re interfacing between the KV language layout file and the actual Python program.

For now, just know that the ObjectProperty property can be bound to any Python object. Your KV language file will be set up to attach this property to the TextInput object. First, though, set up your main.py code to import ObjectProperty and add an instance of it with an empty value to AddLocationForm, as shown in Example 2-5.

Example 2-5. Adding a property to point at the search input widget

fromkivy.propertiesimport ObjectProperty 1

classAddLocationForm(BoxLayout):

search_input = ObjectProperty() 2

def search_location(self):

print("Explicit is better than Implicit")

1

Remember to import the class.

2

The property is created at the class level as an instance of the ObjectProperty class.

Next, modify the weather.kv file to do two things. First, you want to give the TextInput an id property so that it can be internally referenced from other parts of the KV language file. Note that these ids aren’t useful outside of the KV language rules. That means you’ll also have to set the value of the search_input property you just created to this id. The KV language will take care of setting the value in your Python code to point directly at the TextInput widget object. Make the two modifications shown in Example 2-6.

Example 2-6. Setting the search input id and property value

AddLocationForm:

<AddLocationForm>:

orientation: "vertical"

search_input: search_box # 1

BoxLayout:

height: "40dp"

size_hint_y: None

TextInput:

id: search_box # 2

size_hint_x: 50

Button:

text: "Search"

size_hint_x: 25

on_press: root.search_location()

Button:

text: "Current Location"

size_hint_x: 25

ListView:

item_strings: ["Palo Alto, MX", "Palo Alto, US"]

2

First, add an id attribute to the TextInput so it can be referenced by name elsewhere in the KV language file.

1

Then set the value of the property, which was defined in Example 2-5, to that id.

THE DIFFERENCE BETWEEN KIVY PROPERTIES AND PYTHON PROPERTIES

Kivy and the Python language both have concepts they call properties. In both cases, properties sort of represent named values on objects, but they are not the same thing, and you have to be careful to distinguish between them. They are not interchangeable at all. In the context of Kivy development, Kivy properties are more useful, but there is nothing stopping you from using Python properties as well, for other purposes.

A Python property is a method (or set of methods) that can be accessed as if it were an attribute. Different methods are called if the property is retrieved or set. This can be a useful form of encapsulation.

Kivy properties, on the other hand, are not a language feature, but are simply objects that wrap a bunch of logic for the various features described in the text. They are specified on widget classes, and the Kivy internals know how to seamlessly map them to each instance of those properties as used in the Python code or KV language file.

Kivy will take care of making sure that the ObjectProperty in the Python code directly references the TextInput widget object, with all the properties and methods that the TextInput has. Specifically, you can now access the value the user entered from inside the search_locationmethod using the text property, as shown in Example 2-7.

Example 2-7. Accessing a widget attribute via ObjectProperty

def search_location(self):

print("The user searched for '{}'".format(self.search_input.text)) 1

1

Access the text property on the search_input widget. In this example, just print it out.

Populating the Search Result List

Now that you can access the value the user searched for, the next step is to look up possible matching cities. To do this, you need weather data to query.

One option, of course, would be to invest millions of dollars into setting up weather stations across the world; then you’d have your own private network of data to query. Unfortunately, that’s slightly outside the scope of this book. Instead, you can take advantage of other services that have already done this part and have made their data publicly available.

I had a look around the vast reserves of the Internet and discovered Open Weather Map, which supplies an international API for looking up weather data. I don’t know how accurate it is, but this is what you’ll be using to create your Kivy interface. It is free, allows essentially unlimited requests, and doesn’t require an API key (though it is recommended for production systems). It’s also founded on “open” principles like Wikipedia, OpenStreetMaps, Creative Commons, Open Source, and Gittip. I believe strongly in these principles.

CHOOSING APIS AND LIBRARIES

I just arbitrarily told you what API you are going to use to access weather data. This took a huge amount of effort out of the programming process for you. This is unfortunate, as determining what libraries to base your program on is a major part of the development effort. Programming books tend to introduce the subject as if you will be writing all of your work from scratch. This isn’t true. The success of any new application is strongly impacted by what third-party libraries or APIs are used to create it. I spend a vast amount of time up front researching available options and trying to figure out which one will maximize the effectiveness of my app.

For any given problem that you need to solve, search the Web to see what existing libraries are available. Programmers hate duplicating work, and you’ll find most tasks have already been completed! Once you have a list of available libraries, study each one, paying attention to questions such as these:

§ Is the license compatible with the license you want to use for your application?

§ If not open source, how much does it cost to license the library?

§ If open source, does the source code for the library look well maintained and easy to read?

§ Does the library appear to be actively developed and has there been a recent release?

§ Is there an active user community talking about the library?

§ Does the library appear to have useful (readable, up-to-date, complete) documentation?

§ What avenues of support are available if you have trouble?

§ If Python-based, does the library boast Python 3 support?

Open Weather Map is a web service that returns data in JavaScript Object Notation (JSON). Python has a superb built-in JSON parsing library that converts the incoming data to Python dictionaries and lists. However, you still need a way to retrieve that incoming data. There are built-in tools to handle this in the Python standard library, but you’re much better off using the UrlRequest class that Kivy provides. This class supplies a fully asynchronous API, which means that you can initiate a request and allow the user interface to keep running while you wait for a response.

Since this book is about Kivy, I’m not going to give you a lot of detail about the structure of the Open Weather Map data. Its website explains this in detail. The examples include all the code you’ll need to actually perform these tasks.

The next step is to connect to the weather map service when the user clicks the Search button and retrieve a list of cities that match those search results. As Example 2-8 illustrates, this is not a difficult task.

Example 2-8. Retrieving map data using requests and printing the list of cities to the console

def search_location(self):

search_template = "http://api.openweathermap.org/data/2.5/" +

"find?q={}&type=like" 1

search_url = search_template.format(self.search_input.text)

request = UrlRequest(search_url, self.found_location) 2

def found_location(self, request, data): 3

cities = ["{} ({})".format(d['name'], d['sys']['country'])

for d indata['list']] 4

print("\n".join(cities))

1

The {} in the URL is a placeholder for the user’s query. The str.format method is used in the next line to replace this value with the value that the user actually searched for.

2

You’ll need from kivy.network.urlrequest import UrlRequest at the top of the file. This line is doing a lot of work on your behalf. It connects to the Open Weather Map URL and downloads the response. It then returns control to the UI, but when the response comes back from the network, it will call the found_location method, which is passed in as an argument.

3

The data passed by UrlRequest is a parsed dictionary of JSON code.

4

This list comprehension is also doing a huge amount of work. This command is iterating over one list and translating it into a different list, where the elements are in the same order but contain different data. The data, in this case, is a string with the name of the city and the country in which it’s located.

CAUTION

There is a bug in Kivy 1.8.0 under Python 3. When you are using Kivy 1.8.0 and Python 3, UrlRequest fails to convert the incoming data to JSON. If you are using this combination, you’ll need to add import json and put data = json.loads(data.decode()) at the top of found_location.

COMPREHENSIONS

Comprehensions can be pretty incomprehensible to the uninitiated (and are therefore poorly named). However, they are one of Python’s strongest language features, in my opinion. Newer Python users tend to overlook them, so I thought I’d give you a crash course here.

Python has built-in support for iterators; that is, container items that can have their elements looped over one by one. Built-in iterators include lists, tuples, strings, dictionaries, sets, and generators. It’s trivial to extend these objects or create new objects that can be iterated over. The for loop is the traditional method of iteration.

However, one of the most common tasks of a for loop is to convert a sequence of objects into another, transformed sequence. Comprehensions allow us to do this in a compact and easy-to-read syntax. The transformations that can happen inside a comprehension include:

§ Changing the container to a different type of sequence. For example, you can iterate over a set and store the results in a list or dictionary.

§ Altering each value into a different format. For example, you can convert a list of strings containing numbers into a list of integers, or you can convert a list of strings into a list of strings with newlines on the end.

§ Filtering out values that don’t conform to a specific condition. For example, you can convert a list of integers into a list of only the even integers in the list, or you can convert a list of strings into a list of nonempty strings.

You’ll see examples of comprehensions throughout this book, but if you want more information, search the Web for list comprehensions, dictionary comprehensions, or set comprehensions in Python.

Now all you need to do is figure out how to update the list of results on the screen instead of printing to the console. This is also surprisingly simple since the data is stored in a Kivy property on the ListView widget.

First, add an id to the ListView and set it up as an assigned property in the weather.kv file, as shown in Example 2-9.

Example 2-9. Updating search results

AddLocationForm:

<AddLocationForm>:

orientation: "vertical"

search_input: search_box

search_results: search_results_list # 1

BoxLayout:

height: "40dp"

size_hint_y: None

TextInput:

id: search_box

size_hint_x: 50

Button:

text: "Search"

size_hint_x: 25

on_press: root.search_location()

Button:

text: "Current Location"

size_hint_x: 25

ListView:

id: search_results_list # 2

item_strings: [] # 3

1

You don’t have an ObjectProperty in main.py yet, but assign it here anyway.

2

Set an id on the ListView so it can be referenced in the property assignment above.

3

Empty the default values from the list, since you won’t be needing them for layout testing anymore.

Then, add the search_results ObjectProperty to the AddLocationForm class and update the contents of the property in the found_location method instead of printing results to the screen, as shown in Example 2-10.

Example 2-10. Updating the list of search results on search click

def found_location(self, request, data):

data = json.loads(data.decode()) ifnotisinstance(data, dict) else data 1

cities = ["{} ({})".format(d['name'], d['sys']['country'])

for d indata['list']]

self.search_results.item_strings = cities 2

1

Work around the aforementioned bug in which UrlRequest doesn’t parse JSON under Python 3. This line of code works on both Python 2 and Python 3 and should also work with future versions of Kivy that fix the bug. It is an ugly workaround, but at least the ugliness is constrained to one line. This line uses Python’s ternary operator to check if the data has been converted to a dict already, and if not, it loads it using the JSON module. You’ll need to add an import json at the top of the file.

2

End the chapter with the easiest line of code imaginable: set the item_strings property on the self.search_results list to the list of cities.

And that’s it. If you now run python main.py, you can search for Vancouver and get two expected results, as shown in Figure 2-1.

CAUTION

When you test this search form, you can crash the application by searching for a place with no matches. Search for a real place. You’ll explore error checking in the exercises.

The search results update when search is clicked

Figure 2-1. Rendering of search results in the basic container widget

File It All Away

We covered two important Kivy topics in this chapter: events and properties. I didn’t get into a great deal of detail about either of them yet. You might want to try some of the following tasks to enhance and lock in your new knowledge:

§ Accessing the GPS device in Android is covered in Chapter 9, but for now, you could invite users to enter a latitude,longitude pair into the text field. If they click the Current Location button, it could signify that they intended to search by latitude and longitude instead of city name. If you look at the documentation for the Open Weather Map database, you can see that it allows searching by latitude and longitude. Try hooking up this button.

§ This app crashes if your search doesn’t have any matches. I had a great internal debate about this, but I have left error checking out of most of the examples to make sure the code you see is comprehensible. However, that makes the code unrealistic. All real-world code needs to have solid error checking. Try to make the search_location and found_location methods resilient against searching for places that don’t exist, transient network failures, and as many other problems as you can come up with.

§ You’ve only seen the on_press event for buttons. See if you can figure out how to make on_enter on the TextInput object call the same function and perform a similar search.

§ Read through the Kivy documentation for some of the basic widgets you’ve heard about to see what kinds of properties and events are supported for each.

§ Try making a new toy app that links the text of a label to the text of an input, such that typing into the input changes the label. Hint: this is easier than you might guess and does not require setting up an on_text handler. It can be done with just a boilerplate main.py file; all the connections can happen in the KV language.