Convert-O-Matic

The Convert-O-Matic interfaceIn this part of my Convert-O-Matic tutorial which began in here. Part 2 is going to cover re-factoring the code and adding some more features to the program. This tutorial will focus on the main program.

The feature set of this new version is slightly more complicated than that created in part 1.

  • Convert a numeric value entered using the specified conversion
  • Update the interface text to show the proper conversion.
  • Notify the user about errors
  • Have a multi-lingual interface
  • Easily add a new translation
  • Easily add a new conversion

There were many problems with the code produced in the first part of this tutorial. The noteworthy ones were violation of the open-closed principle, the single responsibility principle. It is also considered risky to change code that has minimal encapsulation and data hiding.

Once again, the complete code for this project will be at the end of the tutorial.

On with the tutorial code

This is a python 3 project for which the tkinter imports should look familiar. The remaining imports will be discussed as they are used.

#!/usr/bin/env python3
import sys
import inspect
from tkinter import *
from tkinter import ttk
from interfacetext import InterfaceText
import conversions

By extending the Tk class, our ConvertOMatic class essentially becomes its own window to which we can directly add the widgets.

class ConvertOMatic(Tk):

Starting a method name with an underscore is a pythonic convention to show a private method. Althugh Python is a language of convention and not contract, methods that start with an underscore will not be imported from a module unless it defines them all in __all__ method. Unlike other object orientated languages such as Java and C#, another class could directly use this method but we trust that other programmers will honour the convention.

_action is a convention I have adopted to name an event handler. This function loads the selected language and resets the user interface. Rather than do all the work in one method, I have split it into a series of smaller tasks helping to honour the single responsibility principle.

def _action_update_language(self, to_language): 
    self._interface_text.load_for_language(to_language) 
    self._create_menu() 
    self._set_text_labels() 
    self._conversions['values'] = self._load_conversions() 
    self._conversions.current(0)

This method updates the user interface labels with the correctly translated from and to values.

def _action_conversion_changed(self, e): 
    cl = self._conversion_classes[self._conversions.current()]
    module = <strong>import</strong>('conversions')
    converter = getattr(module, cl) 
    self._from_label.set(self._interface_text.get(converter.KEY_FROM))
    self._to_label.set(self._interface_text.get(converter.KEY_TO))
    self._to_value.set('')

This method simply works out which class in the conversions module is for the chosen conversion and then farms the work out to the proper class.

def _action_do_conversion(self, *args): 
    cl = self._conversion_classes[self._conversions.current()]
    module = <strong>import</strong>('conversions')
    converter = getattr(module, cl)() 
    to_val = converter.convert(self._from_value.get()) 
    if to_val: 
        self._to_value.set(str(to_val)) 
    else: 
        self._to_value.set('!! Error !!')

This method does the same job as in part 1 but it delegates the retrieval of the translated text to the InterfaceText instance. The advantage of encapsulation can be seen here. The InterfaceText class could be used in many projects which gives us code re-use and even better, the code inside the get() method could be completely re-written to use a more standard l10n translation mechanism and our code would not care. As long as the method’s interface does not change.

def _set_text_labels(self):
    self.title(self._interface_text.get('window_title'))
    self._convert_label.set(self._interface_text.get('convert'))
    self._from_label.set(self._interface_text.get('feet'))
    self._to_label.set(self._interface_text.get('metres'))
    self._equivalent_label.set(self._interface_text.get('equivalent'))
    self._button_text.set(self._interface_text.get('calculate'))

This is a private utility function that creates the menu bar. There is not much unusual in here although you can see that the menu is attached directly to the converter instance by use of the inherited configure() method. The only thing in here that may be confusing if you have never see it before is command=lambda l=l: self._action_update_language(l) from the last line of the method.

The command option adds a function to call when the item is clicked but what the heck is this lambda thing? It’s an in-line unnamed function that is created on the fly. In this case, I needed to pass the l variable into the action for the menu item which couldn’t be done directly. So I created a lambda to do it for me.

def _create_menu(self):
    self.menubar = Menu(self)
    self.configure(menu=self.menubar)

<pre><code>self.menu_file = Menu(self.menubar, tearoff=False)
self.menu_languages = Menu(self.menubar, tearoff=False)

label_text = self._interface_text.get('menu_file')
self.menubar.add_cascade(menu=self.menu_file, label=label_text)
label_text = self._interface_text.get('menu_file_quit')
self.menu_file.add_command(label=label_text, command=self._action_quit)

self.menubar.add_cascade(menu=self.menu_languages, label=self._interface_text.get('menu_languages'))
languages = self._interface_text.available_languages()
for language in languages:
    l = language.capitalize()
    self.menu_languages.add_command(label=l, command=lambda l=l: self._action_update_language(l))

This function is another private utility method whose job is to update all the labels on the user interface. If you remember from part 1 this is done by setting StringVar type variables that are bound to the controls. The InterfaceText class handles the text to display using the get() method.

def _set_text_labels(self):
    self.title(self._interface_text.get('window_title'))
    self._convert_label.set(self._interface_text.get('convert'))
    self._from_label.set(self._interface_text.get('feet'))
    self._to_label.set(self._interface_text.get('metres'))
    self._equivalent_label.set(self._interface_text.get('equivalent'))
    self._button_text.set(self._interface_text.get('calculate'))

This is a more interesting method which uses one of the imports, inspect which allows a program to look at its own code do something based on that. This is more generally known as reflection. In this case I am looking through each thing in the conversions module and if it is a class, I am setting up a translated list to show in the user interface as well as a list of conversion class names to be used by the program.

def _load_conversions(self):
    c = []
    self._conversion_classes = []

<pre><code>for name, obj in inspect.getmembers(conversions):
    if inspect.isclass(obj):
        c.append(self._interface_text.get(obj.KEY_DESCRIPTION))
        self._conversion_classes.append(name)

return c

This should look pretty similar to the constructor from part 1. The only difference is that I am adding the widgets directly to the class and not a Tk class instance that is passed in.

def _create_widgets(self):

<pre><code>self.title(self._interface_text.get('window_title'))
self._create_menu()

mainframe = ttk.Frame(self, padding="3 3 12 12")
mainframe.grid(column=0, row=0, sticky=("N", "W", "E", "S"))
mainframe.columnconfigure(0, weight=1)
mainframe.rowconfigure(0, weight=1)

ttk.Label(mainframe, textvariable=self._convert_label).grid(column=1, row=1, sticky="W")
self._conversions = ttk.Combobox(mainframe, textvariable=self._convert_value)
self._conversions.grid(column=2, row=1, sticky="W")
self._conversions['values'] = self._load_conversions()
self._conversions['state'] = 'readonly'
self._conversions.bind("&lt;&gt;", self._action_conversion_changed)
self._conversions.current(0)

from_entry = ttk.Entry(mainframe, width=7, textvariable=self._from_value)
from_entry.grid(column=2, row=2, sticky=(W, E))

ttk.Label(mainframe, textvariable=self._from_label).grid(column=3, row=2, sticky="W")
ttk.Label(mainframe, textvariable=self._to_value).grid(column=2, row=3, sticky="W")
ttk.Label(mainframe, textvariable=self._to_label).grid(column=3, row=3, sticky="W")
ttk.Label(mainframe, textvariable=self._equivalent_label).grid(column=1, row=3, sticky="E")
ttk.Button(mainframe, textvariable=self._button_text, command=self._action_do_conversion).grid(column=3, row=4, sticky="W")

self._set_text_labels()

for child in mainframe.winfo_children():
    child.grid_configure(padx=5, pady=5)

from_entry.focus()
self.bind('<KP_Return>', self._action_do_conversion)
self.bind('<KP_Enter>', self._action_do_conversion)

The constructor in this version delegates much of the work to helper methods and other classes.

 def <strong>init</strong>(self, **kwargs):

<pre><code>Tk.__init__(self)

self._from_value = StringVar()
self._to_value = StringVar()
self._convert_value = StringVar()
self._convert_label = StringVar()
self._from_label = StringVar()
self._equivalent_label = StringVar()
self._to_label = StringVar()
self._button_text = StringVar()

self._interface_text = InterfaceText()
self._interface_text.load_for_language("english")
self._create_widgets()

That’s it for now, if there is anything here you would like to know more about, let me know and I’ll try to do a post about it. The next instalment in this series will look at the interfacetext module.

Convert-O-Matic source code

Leave a Reply

Your email address will not be published. Required fields are marked *