7
\$\begingroup\$

Bit of a pointless program (at the moment). It's intended more as practice for myself rather than anything else.

General idea is that you add details of a particular cat or dog, and it'll update a TreeView. A dog or cat can then be removed based on their assigned number.

Many thing I wasn't too sure on when creating this, such as best practices for creating a new form, accessing a class's parent attributes, and just general structure.

There's a lot more I want to add here (e.g. filters) - however I worry I'm repeating myself a lot throughout the code, and hardcoding things too much. The more functionality I try to add, the messier it seems to get.

import tkinter as tk
from tkinter import ttk

class App(tk.Frame):

    def __init__(self, master = None):
        self.master = master
        self.my_storage = Storage()

        # Setup Frames.
        self.f1 = ttk.Frame(self.master)
        self.f2 = ttk.Frame(self.master, padding = (10, 10, 10, 10))
        self.f1.pack(fill = tk.X)
        self.f2.pack(fill = tk.BOTH, expand = True)
        
        # Setup GUI widgets.
        self.widgets()

    def widgets(self):

        # Setup Tree widget to store animal objects.
        self.tree = ttk.Treeview(self.f2)
        headings = ["Type", "Name", "Breed", "Colour", "Size", "Bark", "Aggression", "Cuteness"]
        self.tree["columns"] = (0, 1, 2, 3, 4, 5, 6, 7)
        self.tree.column("#0", width = 45)
        self.tree.heading("#0", text = "No.")
        for index in range(len(headings)):
            self.tree.column(index, width = 110)
            self.tree.heading(index, text = headings[index])

        # Setup Buttons and Entry widgets.
        self.position = tk.IntVar()
        self.add_dog = ttk.Button(self.f1, text = "Add Dog", command = lambda: self.add_window("Dog"))
        self.add_cat = ttk.Button(self.f1, text = "Add Cat", command = lambda: self.add_window("Cat"))
        self.remove_animal = ttk.Button(self.f1, text = "Remove by ID", command = self.remove_animal)
        self.remove_animal_pos = ttk.Entry(self.f1, textvariable = self.position, width = 3)

        # Add Widgets to GUI.
        self.add_dog.pack(side = tk.LEFT)
        self.add_cat.pack(side = tk.LEFT)
        self.remove_animal.pack(side = tk.LEFT)
        self.remove_animal_pos.pack(side = tk.LEFT)
        self.tree.pack(expand = True, fill = tk.BOTH)

    # Remove entry from library based on it's index / No
    def remove_animal(self):
        self.my_storage.remove_animal(self.position.get())
        self.update_tree()

    # Update the tree with the latest version of our storage list.
    def update_tree(self):
        self.tree.delete(*self.tree.get_children())
        for i, x in enumerate(self.my_storage.storage):
            obj_type = x.__class__.__name__
            if obj_type == "Dog":
                self.tree.insert("", "end", text = i, values = (obj_type, x.name, x.breed, x.colour, x.size, x.bark_sound, "n/a", "n/a"))
            else:
                self.tree.insert("", "end", text = i, values = (obj_type, x.name, x.breed, x.colour, x.size, "n/a", x.aggression, x.cuteness))

    # Add a new window which allows us to input paramaters for our animal objects.
    def add_window(self, type_of_animal):
        self.window = tk.Toplevel(self.master)
        AddAnimal(self.window, type_of_animal, self.my_storage, self.tree)
        
class AddAnimal(App):

    def __init__(self, master, type_of_animal, my_storage, my_tree):
        self.tree = my_tree
        self.my_storage = my_storage
        self.master = master
        self.type_of_animal = type_of_animal
        self.widgets()
    
    def widgets(self):
        
        # Set titles depending on type of animal we're adding.
        if self.type_of_animal == "Dog":
            self.titles = ["Name", "Breed", "Colour", "Size", "Bark Sound"]
        else:
            self.titles = ["Name", "Breed", "Colour", "Size", "Aggression", "Cuteness"]
        
        # From titles, create the relevant entry boxes and labels.
        self.entries = {}
        for x in self.titles:
            frame = tk.Frame(self.master)
            frame.pack(side = "top", fill = "x")
            label = tk.Label(frame, width = 20, text = x, anchor = 'w')
            entry = tk.Entry(frame)
            label.pack(side = "left")
            entry.pack(side = "right", expand = True, fill = "x")
            self.entries[x] = entry
    
        self.add_bttn = tk.Button(self.master, text = "Add", command = lambda: self.add_to_tree(self.titles))
        self.add_bttn.pack()
    
    def add_to_tree(self, fields):

        # Store all entry values in list.
        lis = [self.entries[title].get() for title in self.titles]

        # Depending on type of animal, create new animal object with relevant entry values.
        if self.type_of_animal == "Dog":
            name = lis[0]; breed = lis[1]; colour = lis[2]; size = lis[3]; bark_sound = lis[4]; 
            obj = Dog(name, breed, colour, size, bark_sound)
        else:
            name = lis[0]; breed = lis[1]; colour = lis[2]; size = lis[3]; aggression = lis[4]; cuteness = lis[5]
            obj = Cat(name, breed, colour, size, aggression, cuteness)

        # Add object to our strorage list, update the tree with this list, and clos form.
        self.my_storage.add_animal(obj)
        self.update_tree()
        self.master.destroy()

class Storage:
     
    def __init__(self):
         self.storage = []
         
    def add_animal(self, animal):
        self.storage.append(animal)

    def remove_animal(self, position):
        self.storage.pop(position)

class Animal:

    def __init__(self, name, breed, colour, size):
        self.name = name
        self.breed = breed
        self.colour = colour
        self.size = size

class Dog(Animal):

    def __init__(self, name, breed, colour, size, bark_sound):
        super().__init__(name, breed, colour, size)
        self.bark_sound = bark_sound

class Cat(Animal):

    def __init__(self, name, breed, colour, size, aggression, cuteness):
        super().__init__(name, breed, colour, size)
        self.aggression = aggression
        self.cuteness = cuteness

root = tk.Tk()
App(master = root)
root.mainloop()

gui

\$\endgroup\$
0
5
\$\begingroup\$

A little for the styling of the GUI

Add an icon to your application

Tkinter has made it very easy to add an icon to any of our windows, it's a single line that makes your GUI look good.

All you need to do is go to a website like this one and download any .ico file of size 16x16, I haven't tested it with other sizes and formats, but usually, you'll find plenty in this size!

Once you have your .ico file, you need to place it in the same directory as your source files.

  • Tip: place them in a folder relative to the source files, so you just have to do images/, this keeps your project directory clean!
window.iconbitmap("path to your file")

That's all there is!


Add a title

This one is even easier, all you need to do is

window.title(" your title " )

Avoid unnecessary classes

you have a class Storage, but all it really does is hold a list of animals. You don't need to create a new class for this, a simple list in App works.

def __init__(...):
    self.animals = []

Since you have inherited your AddAnimal class from App, you can easily add an animal to self.animals, It's easier that way


Why is AddAnimal a class?

I understand what you are trying to do, but I don't see why AddAnimal should be a class

It should just be a function in your App class, it is very important that you structure your Tkinter application right otherwise it looks extremely convoluted, let me show you how this program should be structured ( or can be )

First, let's start with the animal structure

animal class

The good point is, your program follows this perfectly. Animal has

  • name
  • breed
  • dog
  • size

Dog has

  • bark sound

Cat has

  • Agression what?
  • Cuteness

It is a good structure, you have re-used Animal attributes in Cat and Dog, but now when you have to have the main application, your program is a little convoluted

Here is how it can be structured better

Main

After this, there won't be a need for extra classes like Storage and AddAnimal


Animal

f self.type_of_animal == "Dog":
    self.titles = ["Name", "Breed", "Colour", "Size", "Bark Sound"]
else:
    self.titles = ["Name", "Breed", "Colour", "Size", "Aggression", "Cuteness"]

These don't belong to AddAnimal, these should be a part of your Animal class since these are attributes of Animal, so your class misses one thing

class Animal:
    def __init__(self, name, breed, colour, size):
        self.name = name
        self.breed = breed
        self.colour = colour
        self.size = size
        self.all_attributes = [self.name, self.breed, self.colour, self.size]

class Dog(Animal):

    def __init__(self, name, breed, colour, size, bark_sound):
        super().__init__(name, breed, colour, size)
        self.bark_sound = bark_sound
        self.all_attributes.append(self.bark_sound)

class Cat(Animal):

    def __init__(self, name, breed, colour, size, aggression, cuteness):
        super().__init__(name, breed, colour, size)
        self.aggression = aggression
        self.cuteness = cuteness
        self.all_attributes.append(aggression)
        self.all_attributes.append(cuteness)

This way when you create a new animal, you already have its titles.

my_cat = Cat("milo","turkish","brown","small",aggression = "0",cuteness = "100")
print(my_cat.all_attributes)

['milo', 'turkish', 'brown', 'small', '0', '100']


App

As I said, App should hold a list of Animal objects, so everytime you need to add a new animal, it's extremely simple

class App:
    def new_cat():
        # get all input through the GUI widgets 
        self.animals.append( Cat(attributes...) )
    def new_dog():
        self.animals.append( Dog(attributes...) )

It is heavily nerfed to show only the important stuff, other Tk stuff remains


Avoid Magic Numbers

name = lis[0]; breed = lis[1]; colour = lis[2]; size = lis[3]; bark_sound = lis[4]; 

All are magic numbers, an enum would be perfect here

from enum import Enum

class Columns(Enum):
    name = 0
    breed = 1
    ...

Since the values change for Cat and Dog , the Enum can be a part of the respective class so there isn't any problem


Nitpicks

  • There still are many magic constants in your program like width, padding, etc
headings = ["Type", "Name", "Breed", "Colour", "Size", "Bark", "Aggression", "Cuteness"]
self.tree["columns"] = (0, 1, 2, 3, 4, 5, 6, 7)

I think you should change this to

self.headings = ("Type", "Name", "Breed", "Colour", "Size", "Bark", "Aggression", "Cuteness")
self.tree["columns"] = self.headings

Since headings is a part of our App , it should be a member

  • After adding a record to the tree, the details don't seem to be centered with the column heading, you should use anchor to center them
\$\endgroup\$
5
  • \$\begingroup\$ Thanks! First I'm hearing from Enum's. I'm not sure this would definitely work - as the number's change depending on whether it's a Cat or Dog object (e.g. 4 equals aggression in cats and bark sound for dogs). Regarding the AddAnimal class. I read somewhere that new windows should be a separate class - but this may have been wrong. For the Storage class, I kept this has a class as I planned on having extra functionality associated with it (e.g. filters). Adding the all_attributes attribute makes a lot of sense. I hadn't thought of this! \$\endgroup\$
    – user231641
    Oct 27 '20 at 9:41
  • \$\begingroup\$ @AaronWright Yes, you can keep then enum as a part of the Cat or Animal class, that way there isn't any confusion, I'll add that to my answer \$\endgroup\$
    – Parekh
    Oct 27 '20 at 9:47
  • \$\begingroup\$ Perekh Thanks for that. Regarding the AddAnimal class - how would I structure this if it wasn't a class? As in, should the widgets method under this class remain as a method, along with the 'add_window' method in the MainApp class. The only way I've been shown regarding the creation of new windows, is to add them as their own class. \$\endgroup\$
    – user231641
    Oct 27 '20 at 10:14
  • \$\begingroup\$ @AaronWright Join this chat room We can discuss here \$\endgroup\$
    – Parekh
    Oct 27 '20 at 11:07
  • \$\begingroup\$ Thanks Aryan! I'm at work right now, but I'll join the chat room tonight once I'm finished. \$\endgroup\$
    – user231641
    Oct 27 '20 at 11:31
1
\$\begingroup\$

It is often good practice to put the main program controller in an if __name__ == "__main__": so that in case you would want to import this program in another file you would not open the window up immediately, only if you run this file directly. In your case, that controller would be the last three lines:

...
        self.aggression = aggression
        self.cuteness = cuteness

if __name__ == "__main__":
    root = tk.Tk()
    App(master = root)
    root.mainloop()
\$\endgroup\$
3
  • 1
    \$\begingroup\$ Thanks! I'll add this in. I was always a bit confused as to what was meant by if __name__ == "__main__" \$\endgroup\$
    – user231641
    Oct 28 '20 at 10:16
  • \$\begingroup\$ @AaronWright no problem! here's the official documentation on it \$\endgroup\$
    – Dion
    Oct 28 '20 at 11:38
  • \$\begingroup\$ It is like the main() function of python \$\endgroup\$
    – Parekh
    Oct 28 '20 at 16:36

Your Answer

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