Build a GUI and package πŸ“¦ your killer Python scripts πŸ“œwith Tkinter and Pyinstaller

fadygrab

Fady GA 😎

Posted on January 24, 2023

Build a GUI and package πŸ“¦ your killer Python scripts πŸ“œwith Tkinter and Pyinstaller

I loooove the CLI! 😍😍
I can safely assume that you all agree with me when I say, it's fast and efficient. After all, the IT tech people πŸ‘¨β€πŸ’» are spending almost all of their time either using CLI tools/scripts or writing them πŸ˜‰.

In my line of work, I write a lot of Python scripts and CLI tools and I'm very happy using them as they are 😊!
But every now and then, you HAVE to build a GUI for your killer script and build and executable for it to widen its users base (if that's something you aim for).

Like one time, I've written a Python script that processes scanned images of a survey that uses "bubbles" as their user's responses and puts them in a csv file for further analysis. With just one command in the terminal, I could process hundreds of images just like magic πŸ§™β€β™‚οΈ.

I had only one problem, the users that are supposed to collect the surveys and process them don't know what a "terminal" is so they neither could create the necessary environment to run the script nor executing the script via CLI (duh!) πŸ€¦β€β™‚οΈ.

So ultimately, I had to revisit Tkinter - Python's native GUI module - to create a GUI for my scripts and to check out Pyinstaller as a final step to package the whole thing and then ship it the end users.

First of, Tkinter:

Prior to version 8.5, Tkinter generated GUI was ... very bad 🀒! It looked very 1990's! I didn't consider it much and often looked for alternatives πŸ€·β€β™‚οΈ. But now after v8.5, it - as written in the changelog - "makes use of platform-specific theming". Which means it will inherit whatever theme the OS is using. It isn't cutting-edge but still way better from older versions 🀩.

If you didn't work with Tkinter before, it might look intimidating at first but it's very intuitive once you get its pattern:

  1. You initiate your widget (in Tkinker, controls are called "widgets").
  2. You place your widget. That's it πŸ˜‰!

Tkinter app anatomy:

  • A Tkinter app always starts with either a Frame (container for other controls) or a Tk instance (which is technically a frame). ```python

form tkinter import Tk, Label, Button, StringVar

root = Tk()
root.title("My cool app")
root.geometry("400x200+100+100") # Width x Hieght + x + y

* To start your app, you have to call and endless loop so the application is always responding to your interactions with it.
```python


root.mainloop()


Enter fullscreen mode Exit fullscreen mode
  • Widgets have a set of default parameters that you can set across the majority of them. Things like "width", "height", "text", "title", ..., etc. ```python

my_label = Label(
root, # The parent of the widget
text="Hi this is my label!",
width=40
)

* To place a widget inside its container, you either use grid() or pack() but not both in the same container.
```python


my_label.pack(side="top")       # No need to specify the exact position
# or
my_label.grid(row=0, column=0)  # Grid transforms the container into a grid and you specify the row and column placement.


Enter fullscreen mode Exit fullscreen mode
  • A Button widget can take a "command" parameter that refers to the function/method it executes. If this function/method takes parameters of its own, you can wrap inside a "lambda" function. ```python

my_button = Button(
root,
text="Press Me",
command=lambda: print("Hello World!")
)
my_button.pack()

* In Tkinter, there are classes representing some python types like StringVar, IntVar, ..., etc. You can set/get their values with their ... set() and get() methods 😁. One benefit of using those, is that you can dynamically change the values of some widgets properties like a Label's "text" or Progressbar's "value".
```python


from tkinter import Tk, StringVar, Label, Button

root = Tk()
root.geometry("200x200")
root.title("Counter")


def increase_count():
    # A function that will be executed by the button
    count = int(count_var.get())
    count += 1
    count_var.set(str(count))

# StringVar initiation
count_var = StringVar(value="0")

label_count = Label(
    root,
    textvariable=count_var  # Binding the StringVar with the label
)
label_count.pack()

button_count = Button(
    root,
    text="Increase Count",
    command=increase_count  # Binding the function withe button
)
button_count.pack()

if __name__ == "__main__":
    root.mainloop()


Enter fullscreen mode Exit fullscreen mode

The previous code snippet will produce the following result:

code snippet result

  • If you used OOP (Object Oriented Programming) modeling to write your GUI app, you will save yourself from a LOT of trouble πŸ˜‰. We can rewrite the previous example as follow: ```python

from tkinter import Tk, StringVar, Label, Button

class MyApp(Tk):
def init(self, title, *args, **kwargs):
super().init(*args, **kwargs)

    self.geometry("200x200")
    self.title(title)
    self.counter = StringVar(value="0")

    self.creat_gui()

def creat_gui(self):
    label_count = Label(
        self,
        textvariable=self.counter   # Binding the StringVar with the label
    )
    label_count.pack()

    button_count = Button(
        self,
        text="Increase Count",
        command=self.increase_count  # Binding the method withe button
    )
    button_count.pack()

def increase_count(self):
    count = int(self.counter.get())
    count += 1
    self.counter.set(str(count))
Enter fullscreen mode Exit fullscreen mode

my_cool_app = MyApp(title="Counter")

if name == "main":
my_cool_app.mainloop()


You can find a lot more details and examples in the official Tkinter [docs](https://docs.python.org/3/library/tkinter.html) πŸ‘Œ

## Secondly, packaging with Pyinstaller:
I wished that I could write a lot of cool code snippets in this section but Pyinstaller is so simple that you can package your whole app with just the following command 😁:
```bash


pyinstaller myapp.py


Enter fullscreen mode Exit fullscreen mode

And assuming everything goes well, you will find your executable in the ./dist directory. But with Pyinstaller, there are more than meets the eye πŸ˜‰. For example, you can set the executable icon, hide the console window (it's shown by default) and generate one file executable on a windows machine with this command:



pyinstaller myapp.py --noconsole --icon ./myicon.ico --onefile


Enter fullscreen mode Exit fullscreen mode

There are lots of cool options for Pyinstaller that you can learn all about them from the official docs 🀩.

Finally

If you are eager to see a complete example on a Tkinter gui, I've created and example which is more or less simulating the actual situation which I told you about earlier in this post. You can find it in this github repo. I've separated my business logic (killer_script.py) from my GUI (myapp.py) and I used a launcher module (app_launcher.py) for further modularity.
I did my best commenting the py files πŸ™‚ but you will always find something that I missed and you didn't totally understand! That's completely normal. You only have to look it up online or in the docs! If that didn't work, just drop it below in the comments and I'll try my best to make it simpler for you πŸ˜‰

πŸ’– πŸ’ͺ πŸ™… 🚩
fadygrab
Fady GA 😎

Posted on January 24, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related