A lot of beginner tutorials start with “Hello World” examples. There are plenty of websites that use a calculator application as a kind of “Hello World” for GUI beginners. Calculators are a good way to learn because they have a set of widgets that you need to lay out in an orderly fashion. They also require a certain amount of logic to make them work correctly. For this calculator, let’s focus on being able to do the following:
Addition
Subtraction
Multiplication
Division
I think that supporting these four functions is a great starting place and also give you plenty of room for enhancing the application on your own.
Figuring Out the Logic
One of the first items that you will need to figure out is how actually to execute the equations that you build. For example, let’s say that you have the following equation:
1 + 2 * 5
What is the solution? If you read it left-to-right, the solution would seem to be 3 * 5 or 15. But multiplication has a higher precedence than addition, so it would be 10 + 1 or 11. How do you figure out precedence in code? You could spend a lot of time creating a string parser that groups numbers by the operand or you could use Python’s built-in `eval` function. The eval() function is short for evaluate and will evaluate a string as if it was Python code.
A lot of Python programmers actually discourage the user of eval(). Let’s find out why.
Is eval() Evil?
The eval() function has been called “evil” in the past because it allows you to run strings as code, which can open up your application’s to nefarious evil-doers. You have probably read about SQL injection where some websites don’t properly escape strings and accidentally allowed dishonest people to edit their database tables by running SQL commands via strings. The same concept can happen in Python when using the eval() function. A common example of how eval could be used for evil is as follows:
eval("__import__('os').remove('file')")
This code will import Python’s os module and call its remove() function, allowing your users to delete files you might not want them to delete. There are a couple of approaches for avoiding this issue:
Don’t use eval()
Control what characters are allowed to go to eval()
Since you will create the user interface for this application, you will also have complete control over how the user enters characters. This can protect you from insidiousness in a straightforward manner. You will learn two methods of using wxPython to control what gets passed to eval(), and then you will learn how to create a custom eval() function at the end of the article.
Designing the Calculator
Let’s take a moment and try to design a calculator using the constraints mentioned at the beginning of the chapter. Here is the sketch I came up with:
Note that you only care about basic arithmetic here. You won’t have to create a scientific calculator, although that might be a fun enhancement to challenge yourself with. Instead, you will create a nice, basic calculator.
Let’s get started!
Creating the Initial Calculator
You must consider where the code will go whenever you create a new application. Does it go in the wx.Frame class, the wx.Panel class, some other class or what? It is almost always a mix of different classes when it comes to wxPython. As with most wxPython applications, you will want to start by creating a name for your application. For simplicity’s sake, let’s call it wxcalculator.py for now.
The first step is to add some imports and subclass the Frame widget. Let’s take a look:
This code is very similar to what you have seen in the past. You subclass wx.Frame and give it a title and initial size. Then, you instantiate the panel class, CalcPanel (not shown), and call the SetSizeHints() method. This method takes the smallest (width, height) and the largest (width, height) the frame is allowed to be. You may use this to control how much your frame can be resized or in this case, prevent any resizing. You can also modify the frame’s style flags so that it cannot be resized.
Here’s how:
Take a look at the no_resize variable. It is creating a wx.DEFAULT_FRAME_STYLE and then using bitwise operators to remove the resizable border and the maximize button from the frame.
Let’s move on and create the CalcPanel:
You don’t need to put all your interfacer creation code in the __init__() method. This is an example of that concept. You instantiate the class, set the last_button_pressed attribute to None and then call create_ui(). That is all you need to do here.
Of course, that begs the question. What goes in the create_ui() method? Well, let’s find out!
This is a decent chunk of code, so let’s break it down a bit:
Here, you create the sizer needed to help organize the user interface. You also create a wx.Font object, which is used to modify the default font of widgets like wx.TextCtrl or wx.StaticText. This is helpful when you want a larger font size or a different font face for your widget than what comes as the default.
These next three lines create the wx.TextCtrl, set it to right-justified (wx.TE_RIGHT), set the font, and `Disable()` the widget. You want to disable the widget because you don’t want the user to be able to type any string of text into the control.
As you may recall, you will be using eval() to evaluate the strings in that widget, so you can’t allow the user to abuse that. Instead, you want fine-grained control over what the user can enter into that widget.
Some calculator applications have a running total widget underneath the actual “display”. A simple way to add this widget is via the wx.StaticText widget.
Now let’s add the main buttons you will need to use a calculator effectively:
Here you create a list of lists. In this data structure, you have the primary buttons your calculator uses. You will note that a blank string in the last list will be used to create a button that doesn’t do anything. This is to keep the layout correct. Theoretically, you could update this calculator so that that button could be a percentage or do another function.
The next step is to create the buttons, which you can do by looping over the list. Each nested list represents a row of buttons. So, you will create a horizontally oriented wx for each row of buttons.BoxSizer and then loop over the row of widgets to add them to that sizer. Once every button is added to the row sizer, you will add that sizer to your main sizer. Note that each of these button’s is also bound to the `update_equation` event handler.
Now you need to add the equals button and the button that you may use to clear your calculator:
In this code snippet, you create the “equals” button, which you then bind to the on_total event handler method. You also create the “Clear” button to clear your calculator and start over. The last line sets the panel’s sizer.
Let’s move on and learn what most of the buttons in your calculator are bound to:
This is an example of binding multiple widgets to the same event handler. To get information about which widget has called the event handler, you can call the `event` object’s GetEventObject() method. This will return whatever widget it was that called the event handler. In this case, you know you called it with a wx.Button instance, so you know that wx.Button has a `GetLabel()` method which will return the label on the button. Then you get the current value of the solution text control.
Next, you want to check if the button’s label is an operator (i.e.,/, *, -, +). If it is, you will change the text controls value to whatever is currently in it plus the label. On the other hand, if the label is not an operator, you want to put a space between whatever is currently in the text box and the new label. This is for presentation purposes. You could technically skip the string formatting if you wanted to.
The last step is to loop over the operands and check if they are currently in the equation string. If they are, you will call the update_solution() method and break out of the loop.
Now you need to write the update_solution() method:
Here is where the “evil” eval() makes its appearance. You will extract the current value of the equation from the text control and pass that string to eval(). Then, convert that result to a string to set the text control to the newly calculated solution. You want to wrap the whole thing in a try/except statement to catch errors, such as the ZeroDivisionError. The last except statement is known as a bare except and should be avoided in most cases. I left it in there for simplicity, but feel free to delete those last two lines if they offend you.
The next method you will want to take a look at is the on_clear() method:
This code is straightforward. All you need to do is call your solution text control’s Clear() method to empty it. You will also want to clear the `running_total` widget, an instance of wx.StaticText. That widget does not have a Clear() method, so instead, you will call SetLabel() and pass in an empty string.
The last method you will need to create is the on_total() event handler, which will calculate the total and also clear out your running total widget:
Here you can call the update_solution() method and get the result. Assuming all went well, the solution will appear in the main text area and the running total will be emptied.
Here is what the calculator looks like when I ran it on a Mac:
And here is what the calculator looks like on Windows 10:
Wrapping Up
Now you know the basics of creating a simple calculator with the wxPython GUI toolkit.
This newsletter is based on a chapter from my book, Creating GUI Applications with wxPython. If you’d like to learn more about this code and several other examples, you should check out that book.
This has been an awesome Tut. But... I am getting a SyntaxWarning: "is not" with a literal. Did you mean "!="?
elif label in operators and current equation is not '' \
Not sure what I did :(
VSCode, windows 11 pro, python3+
Thanks for your time!!!