Python GUI 022 – SimpleDialog


In the last entry, I mentioned that my ASUS tablet died. I went to the local Bic Camera superstore to see what they had for tablets, and it looks like ASUS has gotten out of that game. The only thing that seemed like a decent replacement for around $300 was the Samsung A9+, but Bic doesn’t have it in stock here. I had to put in a special order and I’m told it won’t arrive until Thursday (as I write this on Halloween night).

Not having anything to read ebooks on or play escape room games with in the meantime, I decided to focus instead on the Python solver project. It was a coin flip between starting the transposition solver (Transpo) or continuing the thought of writing the module for taking the solutions from the _processed.txt files and populating the word lists from them.

The word list thing seemed to be the easier of the two to get out of the way, so that’s the one I picked. It was a fairly easy task to run through the CON solutions for the July-August issue of the Cm to determine all of the languages that appeared in the Xeno section, but when I displayed them in a listbox widget, I ran into the same problem I’d had when trying to move some of the other common functions from the GUI files into con_gui_utils.py. Further, when I tried asking for help in the Reddit tkinter group, I got zero response back.

That was depressing, and it kind of looked like I was facing an insurmountable challenge to get any farther into a tkinter-driven GUI-based solver project. So yeah, I gave up, bought myself a handful of weekly manga magazines (Shonen Jump, Shonen Sunday, and Shonen Magajin) and spent the next week reading those.

However, I’ve always argued that there has to be more than one way of getting a task done in any language, and at the back of my mind I was trying to work out alternative approaches or wordings for google searches. Eventually, I stumbled across .grab_set(), and I tried checking that out.

Essentially, the issue revolves around the general method shown by the tkinter tutorial sites for how to create a window or frame. Most of the examples make use of a function, which ends with a call to .mainloop().

def main():
... import tkinter as tk
... window = tk.Tk()
... tk.Label(text="Hello, Tkinter").pack()
... window.mainloop()

if __name__ == "__main__":
... main()

.mainloop() is a tkinter method that runs the app in a loop, looking for widget events.

So, what happens if you want to make a popup window to interact with the user, either to display a yes-no box, or to get optional user data?

Again, most of the examples take a functional approach, and end with .mainloop().

def popupmsg(msg):
... popup = tk.Tk()
... popup.wm_title("!")
... label = ttk.Label(popup, text=msg, font=NORM_FONT)
... label.pack(side="top", fill="x", pady=10)
... B1 = ttk.Button(popup, text="Okay", command = popup.destroy)
... B1.pack()
... popup.mainloop()

And main() can be modified to have a button that calls popupmsg().

def main():
... import tkinter as tk
... window = tk.Tk()
... tk.Label(text="Hello, Tkinter").pack()

... B1 = ttk.Button(popup, text="Click Here",
................... command = popupmsg('Look at me!')
... B1.pack()

... window.mainloop()

There are a few issues with these examples, but they’re not worth getting into. The primary problem is that we now have two mainloops running, and while popup.destroy destroys the popup *window*, it doesn’t actually do anything about the popupmsg mainloop. The result is that often any print statements will get trapped somehow, and won’t be displayed in the Idle environment window, or the command line, until you fully exit out of the app itself. This can also cause problems with changes to global variables not propagating to the functions that called the popup function. But, it’s an unpredictable issue that may not always surface the same way each time.

Anyway, having more than one .mainloop() in a program is bad.

The one alternative I found was .grab_set(), which is supposed to grab the focus to the specified window from the main parent window. In at least one tutorial page, it was presented as a “safer” method for activating a popup window. Turns out to be untrue. All that happened was that the popup window opened, and the program flow returned to the parent function. Clicking on the popup window’s buttons had no effect.

So now I’m back to where I’d started – stuck.

Fortunately, in all of my searching around, I’d constantly encountered the Stack Overflow forums. I tried asking for help there, and I got one reply that turned out to be useful.

See, the thing is that the built-in tkinter messagebox widgets do work right, such as askyesno() or showwarning(). Note that the below example is class-based, which is the basis for all of my GUI projects.

import tkinter as tk
from tkinter.messagebox import askyesno

class Test(tk.Tk):

... def builtin(self, wtitle):
....... answer = askyesno(
.................... title=wtitle, message='Test Built-in Widget',
.................... icon='info')

... def __init__(self, *args, **kwargs):
....... tk.Tk.__init__(self, *args, **kwargs)

....... frame = ttk.Frame(self)

....... button_one = ttk.Button(self, text='Built-in',
........... command=lambda: self.builtin('Built-In'))

....... button_one.pack(padx=5, pady=5)
....... frame.pack()

app = Test()
app.mainloop()


(Example of AskYesNo)

So I asked how we’re supposed to write a custom messagebox that behaves the same way the built-in message boxes do. The answer was also class-based, and a lot trickier than I’d expected. But it’s reliable, and can be modified to use as many other kinds of widgets as desired.

First, a bit of syntax.

We’re going to be using the simpledialog module, and specifically the simpledialog.Dialog widget. This widget is going to have the following methods:

__init__()
body()
validate()
apply()
buttonbox()

which we can override.

__init__() is the standard class initializer, which is where we can create and initialize our variables, the widgets we’re going to use, and the simpledialog.Dialog() class object itself.

simpledialog.Dialog.__init__() is used for creating our messagebox’s parameters and giving them names. For YesNoBox, I want the variables “wtitle” (the title for the new window), and “default” (in case the user doesn’t specify anything).

body() is where we make the messagebox we want. For YesNoBox, we’re creating a label widget, and two button widgets (“Yes” and “No”).

validate() is automatically called when the program tries to close the window. This is where we can check the user’s inputs and decide if we’ll accept them or not. If there’s no need for validation, just return 1 (True). If the data is checked and turns out to be invalid (such as leaving a required data field blank), return 0 (False) to prevent the window from destroying itself.

apply() is called when the user does something that causes the window to try to close itself, such as pressing an “Accept” or “Ok” button. It calls validate() invisibly, and if that returns 1, then apply() will return data to the calling function or method and the window will destroy itself. In the below example, apply() sets the contents of sample_var to the internal variable result.

def apply():
... self.result = self.sample_var.get()

buttonbox() is a default method that displays two buttons, Ok and Cancel. This can be suppressed by using “pass“.


(Example of YesNoBox)

from tkinter import simpledialog, ttk

class YesNoBox(simpledialog.Dialog):
... def __init__(self, master, *args, **kwargs):
....... self.sample_var = tk.StringVar()
....... self.sample_var.set("No")

....... self.label_var = tk.StringVar()
....... self.label_var.set(kwargs.pop("msg", "default msg"))

....... simpledialog.Dialog.__init__(
........... self, master, title=kwargs.pop("wtitle",

.......................................... "default title"),
........... *args, **kwargs
....... )

... def body(self, master):

....... ttk.Label(master, textvariable=self.label_var).
........... grid(row=0, columnspan=3)

....... self.btn_yes = ttk.Button(
........... master, text="Yes",

........... command=lambda: self._button_yes(master)
....... ).grid(row=1, column=0)

....... self.btn_no = ttk.Button(
........... master, text="No",

........... command=lambda: self._button_no(master)
....... ).grid(row=1, column=2)

... def _button_yes(self, master):
....... self.sample_var.set("Yes")
....... self.ok()

... def _button_no(self, master):
....... self.sample_var.set("No")
....... self.ok()

... def validate(self):
....... return 1

... def apply(self):
....... self.result = self.sample_var.get()

... def buttonbox(self):
....... pass

Note that sample_var and label_var are both defined as tk.StringVar().
The reason for this is that it’s easier to change the main data variable and the displayed label name programmatically if we use tk.StringVar(), than if we try to do something like:

var = ttk.Label(master).grid(row=0, columnspan=3)
var.set('whatever')

On the other hand, if we’re dealing with integers, we really need to be careful and specify tk.IntVar() (I keep getting bit by that when working with CheckBox widgets).

Alright. This brings us to the two custom methods in the above example. Let’s look at them in combination with the code for creating the “Yes” and “No” buttons.

....... self.btn_yes = ttk.Button(
........... master, text="Yes",

........... command=lambda: self._button_yes(master)
....... ).grid(row=1, column=0)

....... self.btn_no = ttk.Button(
........... master, text="No",

........... command=lambda: self._button_no(master)
....... ).grid(row=1, column=2)

... def _button_yes(self, master):
....... self.sample_var.set("Yes")
....... self.ok()

... def _button_no(self, master):
....... self.sample_var.set("No")
....... self.ok()

Clicking the “Yes” button calls method _button_yes() (don’t ask me why the method name is preceded by “_“; I think it’s just a convention). Conversely, clicking the “No” button calls _button_no(). Both methods set sample_var to a particular value, then call the internal method ok(), which causes the window to try to close. When the window tries to close, it activates validate() first, and if that returns 1, it activates apply() and then closes the window.

apply() in turn sets the value of sample_var to self.return, which can be used by the calling method as follows:

answer= YesNoBox(app, wtitle='Test Box')
if answer.result is not None:
... print('Button: %s' % answer.result)

Note that if the user closes the messagebox window by clicking the [X] button in the corner, simpledialog.Dialog() returns an empty result of NoneType. To avoid errors in this case, we need to test whether answer.result is None or not.

Lastly, yes I know that we don’t need two separate methods for the buttons to call. The example is just a good example for demonstrating the concept. We could just as easily use the following:

....... self.btn_yes = ttk.Button(
........... master, text="Yes",

........... command=lambda: self._button_pressed(master, 'Yes')
....... ).grid(row=1, column=0)

....... self.btn_no = ttk.Button(
........... master, text="No",

........... command=lambda: self._button_pressed(master, 'No')
....... ).grid(row=1, column=2)

... def _button_pressed(self, master, value):
....... self.sample_var.set(value)
....... self.ok()

I have tested this approach fairly extensively, and it seems to do what it’s advertised to, so I’m going to keep using it from now on. On the upside, I’ve been able to move blocks of the following code into button_utils.py:

set_display_settings()
set_hillclimbing_settings()
select_con_no()

These functions now stand as:

def set_display_settings(root, entries, con_type):
... global display_settings

... if not loaded_file:
....... popup_msg(root, 'Please load file first.')
....... return

... answer= DisplaySettingsBox(app, wtitle='Display records',
........... msg='\n\t\tSelect Record Types to Display\t\t\n',

........... dispset=display_settings)

... if answer.result is not None:

....... display_settings =
........... [answer.result[0], answer.result[1], answer.result[2]]
....... first, last, total =
........... check_have_matching_records(root, entries, con_type,

........... display_settings)

....... if total == 0:
........... popup_msg(root, 'No matching records.')
....... else:
........... edge_conditions[0], edge_conditions[1],

.............. edge_conditions[2] = first, last, total
........... root.con_records.recno = first
........... display_entry(root, root.con_records.recno, entries,
.............. root.con_records.records[root.con_records.recno])
........... test_record_endpoints(root, entries,

.............. root.con_records.recno)

def set_hillclimbing_settings(root, entries):
... global hillclimbing_settings

... answer= HillclimbSettingsBox(app, wtitle='Hillclimbing',
........... msg='\n\t\tSet Hillclimbing Conditions\t\t\n',
........... climbset=hillclimbing_settings)

... if answer.result is not None:
....... hillclimbing_settings =
........... (answer.result[0], answer.result[1], answer.result[2])

def select_con_no(root, entries, display_settings):

... if not loaded_file:
....... popup_msg(root, 'File not loaded. No records to display.')
....... return

... real_rec_nums = []
... disp_list = []
... rec_cntr = 0
... for record in root.con_records.records:
....... d = record[1][ConTitleFields.done.value]
....... s = record[1][ConTitleFields.skip.value]
....... t = record[1][ConTitleFields.con_type.value]
....... if meets_display_setting(d, s, t, CON_TYPE, display_settings):
........... con_no = record[1][ConTitleFields.con_no.value]
........... con_type = record[1][ConTitleFields.con_type.value]
........... spacing = ' ' * (9 - len(con_no))
........... temp = con_no + spacing + con_type
........... disp_list.append(temp)
........... real_rec_nums.append(rec_cntr)
....... rec_cntr += 1

... answer = GotoRecordBox(app, wtitle='Pick record to display',
.......................... recordset=disp_list)

... if answer.result is not None:
....... root.con_records.recno = real_rec_nums[answer.result[0]]
....... display_entry(root, root.con_records.recno, entries,
........... root.con_records.records[root.con_records.recno])
....... test_record_endpoints(root, entries, root.con_records.recno)

If you want to see the descriptions of what the above code does, check out the original sections on cons_gui.py and cryptarithm_gui.py.

The next step is to now move these three current functions into con_gui_utils.py and see if they still work right.

Next up: button_utils.py.

Published by The Chief

Who wants to know?

Leave a comment

Design a site like this with WordPress.com
Get started