5
\$\begingroup\$

This is a continuation of my earlier question, now updated to use a basic GUI. This is my first time using tkinter so I suspect there are a fair few things I'm missing! I've also added the ability to generate a single barcode without having to first save it in a CSV. I'm specifically interested in feedback on how I have implemented the GUI, since that's the bit I'm least confident about, but comments on anything else are very welcome too!

import csv
import barcode
from barcode.writer import ImageWriter
from pathlib import Path
from tkinter import *
from tkinter import messagebox
from tkinter import filedialog
from tkinter import ttk


FORBIDDEN_CHARS = str.maketrans({char: "" for char in ':;<>\'\"\\/?*|.'})


def remove_forbidden(string_in: str) -> str:
    """Removes characters forbidden from windows file names from a string"""
    return string_in.translate(FORBIDDEN_CHARS)


def get_code_png(isbn: str) -> barcode.ean.EuropeanArticleNumber13:
    """Creates a barcode, given an ISBN"""
    isbn13 = barcode.get_barcode_class('ean13')
    return isbn13(isbn, writer=ImageWriter())


def save_code_png(isbn: str, title: str, save_folder_path: Path):
    """Saves one PNG barcode in a given folder"""
    code = get_code_png(isbn)
    code.save(save_folder_path.joinpath(isbn + " " + remove_forbidden(title)))


def codes_from_csv(list_path: Path, save_folder_path: Path):
    """Creates PNG Barcodes from a CSV, placing them in a given folder"""
    with open(list_path, newline='') as csvfile:
        for row in csv.reader(csvfile, dialect='excel'):
            save_code_png(row[0], row[1], save_folder_path)


def gui():
    """Allows access to functions through a GUI"""
    def submit_lone():
        """Creates a single barcode from a button"""
        try:
            save_code_png(txt_isbn_lone.get(), txt_title_lone.get(),
                          Path(txt_save_path_lone.get()))
        except:
            messagebox.showerror('Error', 'Unexpected error:',
                                 sys.exc_info()[0])
        else:
            messagebox.showinfo('Success!', 'Code Created')

    def submit_csv():
        """Creates barcodes from a CSV from a button"""
        try:
            codes_from_csv(Path(txt_csv_path.get()),
                           Path(txt_save_path_csv.get()))
        except:
            messagebox.showerror('Error', 'Unexpected error:',
                                 sys.exc_info()[0])
        else:
            messagebox.showinfo('Success!', 'Codes Created')

    def save_path(txt_field: Entry):
        """Finds a folder and saves the path in a tkinter Entry widget"""
        txt_field.delete(0, END)
        txt_field.insert(0, filedialog.askdirectory())

    def csv_path():
        """Finds a csv file and saves the path in a tkinter Entry widget"""
        files = (("Comma Separated Variable", "*.csv"), ("All Files", "*.*"))
        txt_csv_path.delete(0, END)
        txt_csv_path.insert(0, filedialog.askopenfilename(filetypes=(files)))

#   Setting up the window
    window = Tk()
    window.geometry("400x180")
    window.title("Book Barcodes")
    tab_control = ttk.Notebook(window)
    lone = ttk.Frame(tab_control)
    csv = ttk.Frame(tab_control)
    tab_control.add(lone, text='Single Barcode')
    tab_control.add(csv, text='List from CSV')
    tab_control.pack(expand=1, fill='both')
#   Tab 1: Lone Code
    lbl_isbn_lone = Label(lone, text="ISBN:")
    lbl_isbn_lone.grid(column=0, row=0, ipadx=5, pady=5)
    txt_isbn_lone = Entry(lone, width=30)
    txt_isbn_lone.grid(column=1, row=0, ipadx=5, pady=5)
    lbl_title_lone = Label(lone, text="Title:")
    lbl_title_lone.grid(column=0, row=1, ipadx=5, pady=5)
    txt_title_lone = Entry(lone, width=30)
    txt_title_lone.grid(column=1, row=1, ipadx=5, pady=5)
    lbl_save_path_lone = Label(lone, text="Save Folder:")
    lbl_save_path_lone.grid(column=0, row=2, ipadx=5, pady=5)
    txt_save_path_lone = Entry(lone, width=30)
    txt_save_path_lone.grid(column=1, row=2, ipadx=5, pady=5)
    btn_save_path_lone = Button(lone, text="Find Folder",
                                command=lambda: save_path(txt_save_path_lone))
    btn_save_path_lone.grid(column=2, row=2, ipadx=5, pady=5)
    btn_submit_lone = Button(lone, text="Generate Code", command=submit_lone)
    btn_submit_lone.grid(column=2, row=3, ipadx=5, pady=5)
#   Tab 2: Codes from CSV
    instructions = Label(csv, text="""Please select a CSV file with ISBNs in the first
column and titles in the second.""")
    instructions.grid(column=0, row=0, columnspan=3, ipadx=5, pady=5)
    lbl_csv_path = Label(csv, text="CSV Location:")
    lbl_csv_path.grid(column=0, row=1, ipadx=5, pady=5)
    txt_csv_path = Entry(csv, width=30)
    txt_csv_path.grid(column=1, row=1, ipadx=5, pady=5)
    btn_list_csv = Button(csv, text="Find File", command=csv_path)
    btn_list_csv.grid(column=2, row=1, ipadx=5, pady=5)
    lbl_save_path_csv = Label(csv, text="Save Folder:")
    lbl_save_path_csv.grid(column=0, row=2, ipadx=5, pady=5)
    txt_save_path_csv = Entry(csv, width=30)
    txt_save_path_csv.grid(column=1, row=2, ipadx=5, pady=5)
    btn_save_path_csv = Button(csv, text="Find Folder",
                               command=lambda: save_path(txt_save_path_csv))
    btn_save_path_csv.grid(column=2, row=2, ipadx=5, pady=5)
    btn_submit_csv = Button(csv, text="Generate Codes", command=submit_csv)
    btn_submit_csv.grid(column=2, row=3, ipadx=5, pady=5)

    window.mainloop()


if __name__ == "__main__":
    gui()

\$\endgroup\$
2
\$\begingroup\$

Don't use wildcard imports

Instead of from tkinter import *, use import tkinter as tk. Then, wherever you reference a tkinter class or value, prefix it with tk. (eg: tk.Label, tk.Button`, etc).

This makes your code easier to understand, and helps keep the number of objects in the global namespace low. Plus, it keeps it consistent with PEP8 guidelines.

Move the GUI code to a class

Moving the GUI code to a class helps organize your code. It makes it clear which parts of your code are related to the GUI and which aren't. An example of how to do this can be found on stackoverflow at this question: Best way to structure a tkinter application?

Separate widget creation from widget layout

I consider the pattern where you create a widget, call grid, create a widget, call grid, ... to be an antipattern. It makes it difficult to visualize the layout and makes it more difficult to change the layout. I find that the layout code can change frequently during development as you add new widgets or improve the layout.

For example, instead of this:

lbl_isbn_lone = Label(lone, text="ISBN:")
lbl_isbn_lone.grid(column=0, row=0, ipadx=5, pady=5)
txt_isbn_lone = Entry(lone, width=30)
txt_isbn_lone.grid(column=1, row=0, ipadx=5, pady=5)
lbl_title_lone = Label(lone, text="Title:")
lbl_title_lone.grid(column=0, row=1, ipadx=5, pady=5)
txt_title_lone = Entry(lone, width=30)
txt_title_lone.grid(column=1, row=1, ipadx=5, pady=5)
...

... do it like this:

lbl_isbn_lone = Label(lone, text="ISBN:")
txt_isbn_lone = Entry(lone, width=30)
lbl_title_lone = Label(lone, text="Title:")
txt_title_lone = Entry(lone, width=30)
...
lbl_isbn_lone.grid(column=0, row=0, ipadx=5, pady=5)
txt_isbn_lone.grid(column=1, row=0, ipadx=5, pady=5)
lbl_title_lone.grid(column=0, row=1, ipadx=5, pady=5)
txt_title_lone.grid(column=1, row=1, ipadx=5, pady=5)

Use more whitespace

Right now you have a giant wall of code for creating widgets and laying them out. There's no way to tell if there's any sort of organization to your code, it's all just one giant block of code. You have some comments, but comments alone don't do a good job of helping our eye organize what we see.

Specifically, add a blank line before # Tab 1: Lone Code and before # Tab 2: Codes from CSV

Don't make complex function calls.

Instead of this:

save_code_png(txt_isbn_lone.get(), txt_title_lone.get(),
              Path(txt_save_path_lone.get()))

Do this:

isbn = txt_isbn_lone.get()
title = txt_title_lone.get()
path = Path(txt_save_path_lone.get())

save_code_png(isbn, title, path)

I think it makes the code easier to read, and it makes the code easier to debug since it's easier to examine the values before passing them to the function.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.