DEV Community

Cover image for Build a GUI and package ๐Ÿ“ฆ your killer Python scripts ๐Ÿ“œwith Tkinter and Pyinstaller
Fady GA ๐Ÿ˜Ž
Fady GA ๐Ÿ˜Ž

Posted on

15

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

Postmark Image

Speedy emails, satisfied customers

Are delayed transactional emails costing you user satisfaction? Postmark delivers your emails almost instantly, keeping your customers happy and connected.

Sign up

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

๐Ÿ‘‹ Kindness is contagious

Explore a sea of insights with this enlightening post, highly esteemed within the nurturing DEV Community. Coders of all stripes are invited to participate and contribute to our shared knowledge.

Expressing gratitude with a simple "thank you" can make a big impact. Leave your thanks in the comments!

On DEV, exchanging ideas smooths our way and strengthens our community bonds. Found this useful? A quick note of thanks to the author can mean a lot.

Okay