DEV Community

Cover image for Build a GUI and package πŸ“¦ your killer Python scripts πŸ“œwith Tkinter and Pyinstaller
Fady GA 😎
Fady GA 😎

Posted on

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).
form tkinter import Tk, Label, Button, StringVar

root = Tk()
root.title("My cool app")
root.geometry("400x200+100+100") # Width x Hieght + x + y
Enter fullscreen mode Exit fullscreen mode
  • To start your app, you have to call and endless loop so the application is always responding to your interactions with it.
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.
my_label = Label(
    root,           # The parent of the widget
    text="Hi this is my label!",
    width=40
)
Enter fullscreen mode Exit fullscreen mode
  • To place a widget inside its container, you either use grid() or pack() but not both in the same container.
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.
my_button = Button(
    root,
    text="Press Me",
    command=lambda: print("Hello World!")
)
my_button.pack()
Enter fullscreen mode Exit fullscreen mode
  • 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".
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:
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))


my_cool_app = MyApp(title="Counter")

if __name__ == "__main__":
    my_cool_app.mainloop()
Enter fullscreen mode Exit fullscreen mode

You can find a lot more details and examples in the official Tkinter docs πŸ‘Œ

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 😁:

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 πŸ˜‰

Top comments (0)