Python Tkinter - An Exercise in Wrapping the ComboBox
Tags: python, tkinter
Published: 2022-12-15 12:57:47

---
title: Python Tkinter - An Exercise in Wrapping the ComboBox
published: true
description: Annotation of my process for making a Combobox that uses key-value pairs instead of just single values
tags: python,tkinter
# cover_image: https://direct_url_to_image.jpg
# Use a ratio of 100:42 for best results.
# published_at: 2022-12-15 12:47 +0000
---

I recently solved a small problem by wrapping a sub-class around theTkinter ComboBox definition. Having made notes as I was doing so, I figured I may as well tidy and share what I did.

There are plenty of tutorials around about doing this kind of thing - I'm not claiming this to be anything special or better.

## The Problem

It so happens that the environment in which I have done most of my application coding over the years is one that provides a "ComboBox" control aka a drop-down list.

But in that environment I am very used to having the ComboBox fed by a small table, usually as rows with key and show columns. The control is bound to the key but displays the show columns - this being achieved by setting the dispay width of the first column to zero. Thus the Combobox has values just for displays but when I read what the user selected, I get the key. Most of the time I use numeric keys and string displays. When supplying these via a table, an order can be specified - that might be independent of the display values.  

Having come to writing applications in Python and using Tkinter, it has been frustrating that this only supports the idea of the displayed values being exactly what the control returns as the user's selection.

While annoying, this issue just hadn't been important enough to go out of my way to solve. For some reason, I suddenly felt like tackling - hence this annotated journey.

- Note: I am deliberately overlooking that we can fetch the *index* of the selected item, rather than the displayed value. Most of the time, I don't really care about where things are in the displayed drop-down list - and want to be free to re-order them, or leave an item out. However, as we'll later on, the index might be a useful once we've worked how to wrap one class inside another. 

## Enter Enumerations

As an old Pascal programmer, I quite like using defined types and enumerations in my programming. The most important reason for liking them - especially as a combination - is that a compile phase will pick up any attempt to stray from assigning across the type definitions.

However, Python as an interpreted language with dynamic typing doesn't really provide that benefit. Nonetheless I still quite like using enumerations as it lets me write symbolic values in the program code and ignore the specific values much of the time. 

To quote from [WikiBooks: Pascal Programming/Enumerations](https://en.wikibooks.org/wiki/Pascal_Programming/Enumerations)
``` Pascal
type
	weekday = (Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday);

var
	startOfWeek: weekday;
begin
startOfWeek := Sunday;
end.
```

For internal use for a compiled program, the actual (binary) values that will be used for the enumeration values simply don't matter. And being a strongly typed language, the Pascal compiler will give an error for any attempt to assign a non "weekday" value to the variable "startOfWeek".

Of course, Python is quite different, and I won't explain that in detail - see the standard Python documentation for [enum — Support for enumerations](https://docs.python.org/3/library/enum.html) - but suffice to say I find them useful enough to use them.

However this does pose a problem with comboboxes - because they need to be fed with strings to display and therefore they return those strings back as the selected value.

To get around this, I have been creating my enumerations, and then adding to them by writing functions to express each enumerated value as a display/select string and a converse function to get the enumeration that has a specific string. It all works, but seems clumsy.

What I would like is to have Comboboxes to which I can pass a list of pairs, such that the string halves get displayed to the user but I can get back the enumeration half (key) for that string.

## Current Approach 

Currently, what I do is to have a Tk variable that is bound to the `textvariable` property of the `Combobox`

Hence the setup portion of the code does:
- create Tk variable
- create Tk Combobox control with binding to the variable
- set the `values` for the Combobox control
- set the default value for the Combobox by setting the bound variable

```Python
	t06_t2_str_scheme = m_tkntr.StringVar() 
	t06_t2_cb_schemes = m_tkntr_ttk.Combobox( t06_t2_frame02, width = 50, textvariable = t06_t2_str_scheme )
	t06_t2_cb_schemes['values'] = cmntry.MatchScheme_GetTuple()
	t06_t2_str_scheme.set( cmntry.MatchScheme_Default_Value() )

```

In which I'm pulling from some support functions that I define along with my enumerations - namely:
- `MatchScheme_GetTuple()`
- `MatchScheme_Default_Value()`

Then, to later read the selection, we just `get` the value of the bound variable

```Python
	got_selection = t06_t2_str_scheme.get()

```

And for which I will then need to call another function to translate from the selected string to the corresponding enumeration.

```Python
	enum_selection = MatchScheme_EnumOfString( got_selection)

```

It's not that doing this is hard, it's just fiddly.

## What I Want To Do

I want to be able to
- have a list of tuples as what I set for the Combobox
- in which the first value in each tuple is the value I want to get back
- the second value in each tuple is the value I want displayed for selection

e.g. define the list of tuples
```Python
	the_ListTuplePair = [ ( 1, "One" ), ( 2, "Two"), ( 3, "Three"), ( 4, "Four") ]

```

e.g. create the Combobox object
```Python
	t06_t2_cb_schemes = TuplePairCombobox( t06_t2_frame02, width = 50, listTuplePair = the_ListTuplePair, defaultKey = 2 )

```

e.g. the collection of the selection
```Python
	the_selection = t06_t2_cb_schemes.getSelectedKey()

```

## Caveats and Cases

While mulling over some of the potential issues, I considered I would need to deal with:
- that no repeats of the keys should get processed and displayed, and hence not selected
- that repeats of the display values **is** allowed and catered for
- a default key that matches no provided key means no default value is set
- that no selection being made returns as None

## Implementation A - using the value bound to the control

### The Air Code

Reminding myself a little bit about how class definitions work in Python (I do tend to avoid them) I sat down and typed up the following (with a fair bit of toying around):

```Python
class TuplePairCombobox( m_tkntr_ttk.Combobox ):
	def _process_listTuplePair( ip_listTuplePair ):
		# ensure the listTuplePair has no repeats of the keys
		t_list_keys = [] # used to detect repeats
		r_dict_valu2key = {} # use both to populate the combobox and reverse lookup after selection
		for tpl in ip_listTuplePair:
			if tpl[0] in t_list_keys :
				pass
			else
				if tpl[1] in i_dict_valu2key:
					n = t_list_keys # number of space to pad the display string
					i_dict_valu2key[ tpl[1] + ( " "*n ) ] = tpl[0]
				else
					i_dict_valu2key[ tpl[1] ] = tpl[0]
				t_list_keys.append( tpl[0] )
		return r_dict_valu2key
	def __init__(self, container, p_listTuplePair, p_defaultKey, *args, **kwargs):
		self.i_StringVar = m_tkntr.StringVar() 
		i_dict_valu2key = _process_listTuplePair( p_listTuplePair )
		super().__init__(container, textvariable = self.i_StringVar, *args, **kwargs)
		self['values'] = tuple( list( i_dict_valu2key.keys() ) )
		if p_defaultKey in list( i_dict_valu2key.keys() ) :
			self.i_StringVar.set( self.i_defaultKey )
	def getSelectedKey():
		txt_selection = self.i_StringVar.get()
		r_key = i_dict_tpls[ txt_selection ]
		return r_key

```

That seemed to cover the most of how the idea would work. A short description might be:
- process the supplied list of tuples to build a dictionary that will act as both the reverse lookup - to go from the selected display to the tuple keys - and as the list of things to display in the combobox
- apply my logical rules about repeats, of keys and of values
- to deal with any repeated values, look for clashes and add extra spaces to their ends, with different numbers of spaces as we go along 

### The Actual Code

Here is what I had after pasting in the air code and fixing some of its oversights. 

#### The Class

```Python
class TuplePairCombobox( m_tkntr_ttk.Combobox ):
	def _process_listTuplePair( self, ip_listTuplePair ):
		# ensure the listTuplePair has no repeats of the keys
		t_list_keys = [] # used to detect repeats
		r_dict_valu2key = {} # use both to populate the combobox and reverse lookup after selection
		for tpl in ip_listTuplePair:
			if tpl[0] in t_list_keys :
				pass
			else :
				if tpl[1] in r_dict_valu2key:
					n = len( t_list_keys ) # number of space to pad the display string
					r_dict_valu2key[ tpl[1] + ( " "*n ) ] = tpl[0]
				else :
					r_dict_valu2key[ tpl[1] ] = tpl[0]
				t_list_keys.append( tpl[0] )
		return r_dict_valu2key
	def __init__(self, container, p_listTuplePair, p_defaultKey, *args, **kwargs):
		self.i_StringVar = m_tkntr.StringVar() 
		self.i_dict_valu2key = self._process_listTuplePair( p_listTuplePair )
		super().__init__(container, textvariable = self.i_StringVar, *args, **kwargs)
		self['values'] = tuple( list( self.i_dict_valu2key.keys() ) )
		if p_defaultKey in list( self.i_dict_valu2key.keys() ) :
			self.i_StringVar.set( self.i_defaultKey )
	def getSelectedKey( self ):
		txt_selection = self.i_StringVar.get()
		if txt_selection in self.i_dict_valu2key:
			r_key = self.i_dict_valu2key[ txt_selection ]
		else : 
			r_key = None
		return r_key

```

#### The Object

Here is how the new feature gets used.

First the creation of the ComboBox object, with the usual Tkinter pair of: create and place steps. 

```Python
	i_defaultKey = 3
	t02_cb_schemer = TuplePairCombobox( t02_frame50, i_listTuplePair, i_defaultKey, width = 50) 
	t02_cb_schemer.pack( side = m_tkntr.LEFT, padx=tk_padx(), pady=tk_pady() )

```

#### The Value Fetch

Here is how the value is collected from the ComboBox.

```Python
			print( t02_cb_schemer.getSelectedKey() )

```

#### The List of Tuples

Here is a mock example of a list of tuples for supplying to the new `TuplePairCombobox` object. 

```Python
	#i_listTuplePair = [ ( 1, "One" ), ( 2, "Two"), ( 3, "Three"), ( 4, "Four") ]

```

#### The List of Tuples with a Repeated Value

Just to check that my extra edge case code is working, here is an alternate list of tuples, in which there are two identical display strings. 

```Python
	i_listTuplePair = [ ( 1, "One" ), ( 2, "Two"), ( 3, "Three"), ( 4, "Two") ]

```

In that case, all four options are in the ComboBox and can each return a distinct key - i.e. the fact that two display strings are there is worked around.

Caveat:
- the mechanism for this is very simple, and **would** fail for some combinations in the source tuples. I will have to decide whether to write something more robust.

#### The List of Tuples with a Repeated Key

And similarly here is an alternate list of tuples, in which there are two identical key values. 

```Python
	i_listTuplePair = [ ( 1, "One" ), ( 2, "Two"), ( 2, "Three"), ( 4, "Four") ]

```

In this case, as per the logic I wrote in, the second appearance of the key `2` will be omitted.

## Method B - using the index of the selection

In Method A we just worked around the issue of a Combobox "returning" one of the display values.

But with the standard Tkinter Combobox we do also have the option of pulling out the index of the selected value. Can that be used to make a better method?

### Air code

For this, I copied the finished Method A code and changed the definition of `getSelectedKey` to use `current()` and then thought my way backwards about what data structure it would require to exist. I chose to build two lists in parallel, so that corresponding keys and show strings would be at the same index number.

That gave me the following code, ready to implant into my program. I reversed part of the name "TuplePair" becoming "PairTuple" so that the usage calls could be easily swapped to test it.

```Python
class PairTupleCombobox( m_tkntr_ttk.Combobox ):
	def _process_listTuplePair( self, ip_listTuplePair ):
		r_list_keys = [] 
		r_list_shows = [] 
		for tpl in ip_listTuplePair:
				r_list_keys.append( tpl[0] )
				r_list_shows.append( tpl[1] )
		return r_list_keys, r_list_shows
	def __init__(self, container, p_listTuplePair, p_defaultKey, *args, **kwargs):
		self.i_list_keys, self.i_list_shows = self._process_listTuplePair( p_listTuplePair )
		super().__init__(container, textvariable = self.i_StringVar, *args, **kwargs)
		self['values'] = tuple( self.i_list_shows )
		# still need to set the default value from the nominated key
	def getSelectedKey( self ):
		i_index = self.current()
		return self.i_list_keys[ i_index ]

```

### Actual code

And here is the final code.

```Python
class PairTupleCombobox( m_tkntr_ttk.Combobox ):
	def _process_listPairTuple( self, ip_listPairTuple ):
		r_list_keys = [] 
		r_list_shows = [] 
		for tpl in ip_listPairTuple:
			r_list_keys.append( tpl[ 0] )
			r_list_shows.append( tpl[ 1] )
		return r_list_keys, r_list_shows
	def __init__( self, container, p_listPairTuple, p_defaultKey, *args, **kwargs):
		self.i_list_keys, self.i_list_shows = self._process_listPairTuple( p_listPairTuple )
		super().__init__(container, *args, **kwargs)
		self['values'] = tuple( self.i_list_shows )
		# still need to set the default value from the nominated key
		try:
			t_default_key_index = self.i_list_keys.index( p_defaultKey ) 
			self.current( t_default_key_index )
		except:
			pass
	def getSelectedKey( self ):
		try:
			i_index = self.current()
			return self.i_list_keys[ i_index ]
		except:
			return None

```

The main change is that once it had passed initial testing, I add a `try` clauses to:
- handle finding the index of the default value and using `current` as a setter
- handle in case no default was set and no selection was made

## Summary

As an exercise, this was a nice combination of:
- giving myself the ability to rework and simplify most of the places where I use a Combobox control
- a rediscovery that using classes to rework other classes wasn't quite as tricky as I'd expected 

I do think I should find a better name for my new classes and/or choose which one of them to keep long term.