Plantasia: A Python GUI aimed to organize plants/muchrooms (Second Part)

in Programming & Dev2 months ago

Plantasia is a python GUI aimed to organize plants and muchrooms. Please check the First Part of this project to get the overall idea.

In the last post, both add and remove options were still unavailable. The later options, and several corrections were implemented and the GUI is now finished. However, some minor optimizations can be applied depending on the user's needs.

Let's start by the option to add a new specie.

image.png

For this, I made three main functions inside the App class. The first is create_specie(), this one is called by clicking New in the top menu bar, and it is mainly to structure and design the page.

def create_specie(self):
    self.mainFrame.destroy()
    self.create_mainFrame()
    self.label_new_specie = tk.Label(self.mainFrame, text='New Specie', foreground='#5A8A29', bg='#FFFEC7')
    self.label_new_specie['font'] = Fonts().font0
    self.label_new_specie.grid(sticky='n', pady = 20, column = 0, row = 0)
    self.sub_frame = tk.Frame(self.mainFrame, bg='#FFFEC7')
    self.sub_frame.grid(sticky = 'nw', column = 0, row = 1)
    self.frame_new_specie = tk.LabelFrame(self.sub_frame, bg='#FFFEC7', text= "Details", relief= tk.RIDGE)
    self.frame_new_specie.grid(sticky = 'nw', column = 0, row = 0, pady = 5, padx = 5)
    self.frame_right = tk.Frame(self.sub_frame, bg='#FFFEC7')
    self.frame_right.grid(sticky = 'n', column = 1, row = 0, pady = 5, padx = 20)
    self.frame_description = tk.LabelFrame(self.frame_right, bg='#FFFEC7', text= "Description", relief= tk.RIDGE)
    self.frame_description.grid(sticky = 'nw', column = 0, row = 0, pady = 5, padx = 20)
    self.frame_image = tk.LabelFrame(self.frame_right, bg='#FFFEC7', text= "Upload an Image", relief= tk.RIDGE)
    self.frame_image.grid(sticky = 'nw', column = 0, row = 1, pady = 15, padx = 20)
    #name
    self.specie_name_label = tk.Label(self.frame_new_specie, text="Name:", bg='#FFFEC7')
    self.specie_name_label.grid(sticky = 'nw', column = 0, row = 0, pady = 10, padx = 5)
    self.specie_name_entry = tk.Entry(self.frame_new_specie)
    self.specie_name_entry.grid(sticky = 'nw', column = 1, row = 0, pady = 10, padx = 5)
    #family
    self.specie_family_label = tk.Label(self.frame_new_specie, text="Family:", bg='#FFFEC7')
    self.specie_family_label.grid(sticky = 'nw', column = 0, row = 1, pady = 10, padx = 5)
    self.specie_family_combobox = ttk.Combobox(self.frame_new_specie, values=families, state='normal', width= 17)
    self.specie_family_combobox.grid(sticky = 'nw', column = 1, row = 1, pady = 10, padx = 5)
    #description
    self.specie_description_st = ScrolledText(self.frame_description, width=25, height=5)
    self.specie_description_st.grid(sticky = 'nw', column = 0, row = 0, pady = 10, padx = 5)
    #properties
    self.properties_container = tk.Label(self.frame_new_specie, bg='#FFFEC7')
    self.properties_container.grid(sticky = 'nw', column = 1, row = 2, pady = 10, padx = 4)
    self.specie_properties_label = tk.Label(self.frame_new_specie, text="Properties:", bg='#FFFEC7')
    self.specie_properties_label.grid(sticky = 'nw', column = 0, row = 2, pady = 10, padx = 4)
    self.properties_entries = []
    for i in range(7):
        self.specie_properties_combobox = ttk.Combobox(self.properties_container, values=properties, state='normal', width = 17)
        self.specie_properties_combobox.grid(sticky = 'nw', column = 1, row = i, pady = 0)
        self.properties_entries.append(self.specie_properties_combobox)
    #sideeffects
    self.sides_container = tk.Label(self.frame_new_specie, bg='#FFFEC7')
    self.sides_container.grid(sticky = 'nw', column = 1, row = 3, pady = 10, padx = 4)
    self.specie_sides_label = tk.Label(self.frame_new_specie, text="Side Effects:", bg='#FFFEC7')
    self.specie_sides_label.grid(sticky = 'nw', column = 0, row = 3, pady = 10, padx = 3)
    self.sides_entries = []
    for i in range(7):
        self.specie_sides_combobox = ttk.Combobox(self.sides_container, values=side_effects, state='normal', width = 17)
        self.specie_sides_combobox.grid(sticky = 'nw', column = 1, row = i, pady = 0)
        self.sides_entries.append(self.specie_sides_combobox)
    #image
    img = Image.open(TOOLS_DIR + 'upload_image.png')
    img = img.resize((80, 60))
    self.imgTK = ImageTk.PhotoImage(img)
    self.button_img = tk.Button(self.frame_image, image = self.imgTK, bg='#FFFEC7', command=self.upload_image)
    self.button_img.grid(sticky = 'n', column=0, row=0, pady=5, padx=10)
    self.label_img = tk.Label(self.frame_image, text="*please name the picture \n exactly as the specie's name", foreground='#FF0000', bg='#FFFEC7')
    self.label_img.grid(sticky = 'n', column = 0, row = 1)
    #submit
    self.button_select = tk.Button(self.frame_right, text=' Submit ', command= self.upload_specie, bg='#62AC3D', activebackground='#5A8A29', fg='#FFFFFF')
    self.button_select['font'] = Fonts().font3
    self.button_select.grid(sticky = 'w', column = 0, row = 2, pady = 10, padx=80)
    #back
    self.button_back = tk.Button(self.mainFrame, text=' Back ', bg='#156823', command=self.click_back, activebackground='#5A8A29', fg='#FFFFFF')
    self.button_back['font'] = Fonts().font3
    self.button_back.grid(sticky = 'n', row = 2, pady = 10, column = 0) 

The second function is called, upload_image(), and as it says, it's used to upload an image related with the Specie, and it's called when clicked on the upload image icon (see image above). It allows only .jpg images. The function splits the directory where the image is saved, and returns only the name of the image (that must be the same as the specie's name), it is then saved in the desired directory, among the others species images.

def upload_image(self):
    global set_file
    self.files = [('.jpg', '*.jpg')]
    self.image = askopenfilename(filetypes = self.files, defaultextension = self.files)
    self.img_split = self.image.split('/')
    self.file = self.img_split[len(self.img_split)-1]
    shutil.copyfile(self.image, IMAGES_DIR + self.file)
    self.filename = self.file.split('.')
    self.filename = self.filename[0]
    set_file = 1

Finally, after filling all the entries and comboboxes, one last function, upload_specie(), is called to save the new specie in the Data Frame and Database (excel worksheet in this case).

    def upload_specie(self):
        global species
        global families
        global descriptions
        global properties
        global side_effects
        global df
        self.properties_array = []
        self.sides_array = []
        for combobox in self.properties_entries:
            get_combobox = combobox.get()
            self.properties_array.append(get_combobox)
        for combobox in self.sides_entries:
            get_combobox = combobox.get()
            self.sides_array.append(get_combobox)
        if set_file == 0:
            self.file = ''
            self.filename = ''
        self.error = 1
        while self.error == 1:
            if self.specie_name_entry.get() == '' or self.specie_family_combobox.get() == '' or self.properties_entries[0] == '' or self.sides_entries[0] == '' or self.specie_description_st.get("1.0",'end-1c') == '' or self.file == '':
                msg.showerror('Error', 'Missing field!')
                return
            elif self.filename != self.specie_name_entry.get():
                os.remove(IMAGES_DIR + self.file)
                msg.showerror('Error', "Please save the image with the specie name.")
                return
            elif self.specie_name_entry.get() in species:
                msg.showerror('Error', "This Specie's name already exists. Please insert a different one. ")
                return
            else:
                for property in self.properties_array:
                    if property == '':
                        self.properties_array.remove(property)
                self.join_properties = "|".join(self.properties_array)
                self.join_properties = self.join_properties.split('||')
                self.join_properties = self.join_properties[0]
                for side in self.sides_array:
                    if side == '':
                        self.sides_array.remove(side)
                self.join_sides = "|".join(self.sides_array)
                self.join_sides = self.join_sides.split('||')
                self.join_sides = self.join_sides[0]
                self.specie_description_st = self.specie_description_st.get("1.0",'end-1c').split('  ')
                self.specie_description_st = self.specie_description_st[0]
                self.new_row = {
                    tabs[0]:self.specie_name_entry.get(),
                    tabs[1]:self.specie_family_combobox.get(),
                    tabs[2]:self.specie_description_st,
                    tabs[3]:self.join_properties,
                    tabs[4]:self.join_sides}
                df = df.append(self.new_row, ignore_index=True)
                print(df)
                species = get_species_array(df)
                descriptions = get_descriptions_array(df)
                properties = get_properties_array(df)
                if '' in properties:
                    properties.remove('')
                families = get_families_array(df)
                side_effects = get_side_effects_array(df)
                if '' in side_effects:
                    side_effects.remove('')
                self.error = 0
                df.reset_index(inplace=False)
                df.to_excel(TEMPLATE_DIR, sheet_name='Sheet1', index=False, startrow=0, startcol=0)
                msg.showinfo(title='Info', message='A Specie has been added to the list!')
                self.create_specie()
                return

This function was the most difficult among the three, it generates a row (Data Frame), that is then added to the main Data Frame. The variables df, species, properties, descriptions and side-effects, are updated in the overall code, by defining them as globals. It also prompts several error messages according to certain conditions.

In order to remove a specie, another set of functions were implemented.

image.png

The first one is a callback function (callback()), it prompts the structure of the page , when clicking Remove at the upper menu.

    def callback(self):
        self.mainFrame.destroy()
        self.create_mainFrame()
        self.search_frame = tk.Frame(self.mainFrame, bg='#FFFEC7')
        self.search_frame.pack(side='top', pady=10)
        self.species_frame_label = tk.Label(self.search_frame, text='Select Specie To Remove:', bg='#FFFEC7')
        self.species_frame_label['font'] = Fonts().font2
        self.species_frame_label.grid(sticky="s", column=0, row=0, pady=5)
        self.combobox_species_remove = ttk.Combobox(self.search_frame, values=species, width=20, state='normal')
        self.combobox_species_remove.current(0)  # to give first element in species array
        self.combobox_species_remove.grid(sticky="s", column=0, row=1, pady=5, padx=5)
        self.button_select = tk.Button(self.search_frame, text=' Select ', command= self.remove_specie, bg='#62AC3D', activebackground='#5A8A29', fg='#FFFFFF')
        self.button_select['font'] = Fonts().font3
        self.button_select.grid(sticky="s", column=0, row=2, pady=10)
        self.button_back = tk.Button(self.search_frame, text=' Back ', bg='#156823', command=self.click_back, activebackground='#5A8A29', fg='#FFFFFF')
        self.button_back['font'] = Fonts().font3
        self.button_back.grid(sticky="s", column=0, row=3, pady=230)

One last function was needed, in order to drop the removed specie from the Data Frame and database, which I called remove_specie().

    def remove_specie(self):
        global species
        global families
        global descriptions
        global properties
        global side_effects
        global df
        if self.combobox_species_remove.get() == str_no_species:
            msg.showerror('Error', 'Please you need to add a specie first')
            return
        else:
            result = msg.askyesno(title='Warning', message='Are you sure you want to remove this specie?')
            if result:
                remove_specie = self.combobox_species_remove.get()
                row_to_remove = df.loc[df['Species'] == remove_specie].index
                print(row_to_remove[0])
                book = openpyxl.load_workbook(TEMPLATE_DIR)
                sheet = book['Sheet1']
                sheet.delete_rows(row_to_remove[0]+2)  # for single row
                book.save(TEMPLATE_DIR)
                print(Fore.GREEN + 'Specie removed from template')
                df = df.drop(row_to_remove[0])
                species = get_species_array(df)
                descriptions = get_descriptions_array(df)
                properties = get_properties_array(df)
                families = get_families_array(df)
                side_effects = get_side_effects_array(df)
                os.remove(IMAGES_DIR + remove_specie + '.jpg')
                self.callback()
            else:
                return

After these changes, the GUI is now fully operational. The last step will be to create an executable with all the dependencies, using Pyinstaller.

The full code is quite big and some parts are repetitive, so I decided not to share everything in the posts (both First Part and Second Part).

The code can be easily changed and customized for other subjects, so please feel free to download it from my github:

🤖 https://github.com/macrodrigues/Plantasia 🤖

Open to all sort of critics to be able to improve my code! Please leave them in the comment section

Sort:  

The idea is really good, however, the interface looks bad. You could work more on the design of the application and try to make it better and more appealing.

HI @ppopescu, thank you for your comment. I was more focus on learning how to develop a functional GUI, then on the design. But I agree with you, indeed the design could be better. Have you ever worked with tkinter? It seems to lack on pretty widgets. Beside that, have you downloaded the GUI on my github? Do you you find it user-friendly? Thanks again!