Rob
Rob

Reputation: 95

Python Tkinter get() value of combobox not changing

Thanks for your help in advance.

I am creating a GUI and one part of it has me stumped - I create a Combobox using Tkinter and the values are from a CSV file - I then allow the user to select one of the names and delete that person and this causes the line for that user in the CSV file to be removed and the dropdown is refreshed with just the remaining users - this part works fine - my issue is when I go to delete a second user - my get value for the Combobox always remains the value of the first user.

I am a python noob which you will quickly determine from my code but I have been unable to locate a solution to what I am doing wrong which I am sure is very simple.

Here is the CSV file I am reading from:

id,name,username,password,val1,val2
1,Billy,bsmith,GoodPassword,JZ4Z3ATP6,Test1
2,Amanda,asmith,GoodPassword1,NRROAZ6JP6,Test2
3,Sammy,ssmith,GoodPassword2,NRRZ3ATP6,Test3

My python code is here:

from tkinter import *
from tkinter import ttk
import tkinter as tk
from tkinter import messagebox as msg
import os.path
from csv import reader, writer
import csv
import shutil

root = Tk()
root.title('Why am I an idiot')


labelFrame3 = ttk.LabelFrame(text = "Delete Button")
labelFrame3.grid(column = 0, row = 2, padx = 20, pady = 20)

def clicking(event):
    to_delete = delete_combo.get()
    print("CLICKING")
    print(to_delete)

def read_connections_file(conns_dict):
    conns = conns_dict
    with open('file.csv', 'r') as read_obj:
        csv_reader = reader(read_obj)
        header = next(csv_reader)
        # Check file as empty
        if header != None:
            # Iterate over each row after the header in the csv
            for row in csv_reader:
                # row variable is a list that represents a row in csv
                conns[row[0]] = {'name':row[1], 'username':row[2], 'password':row[3], 'val1':row[4], 'val2':row[5]}

def get_button_names():
    conns = {}
    button_names = []
    read_connections_file(conns)
    for i in conns:
        name_ = conns[i]["name"]
        button_names.append(name_)
    return button_names

buttons1 = get_button_names()
delete_combo = ttk.Combobox(labelFrame3, value=buttons1)
delete_combo.bind("<<ComboboxSelected>>", clicking)
delete_combo.grid(row=0, column=1, columnspan=3, padx=10, pady=10)

def clicking(event):
    to_delete = delete_combo.get()
    print("CLICKING")
    print(to_delete)

def create_delete_dropdown():
    buttons2 = get_button_names()
    print(buttons2)
    delete_combo = ttk.Combobox(labelFrame3, value=buttons2)
    delete_combo.bind("<<ComboboxSelected>>", clicking)
    delete_combo.grid(row=0, column=1, columnspan=3, padx=10, pady=10)
    print("FUNC")
    help = delete_combo.get()
    print(help)

def delete_saved_button():
    original = r'file.csv'
    target = r'file.bak'
    shutil.copyfile(original, target)
    to_delete = delete_combo.get()
    print(to_delete)

    answer = msg.askyesno("Delete Button", ("Are you sure you want to Delete " + to_delete + "?"))

    if answer == True:
        conns = []
        with open('file.csv', 'r') as read_obj:
            csv_reader = reader(read_obj)
            header = next(csv_reader)
            # Check file as empty
            if header != None:
                # Iterate over each row after the header in the csv
                for row in csv_reader:
                    # row variable is a list that represents a row in csv
                    conns.append({'id':row[0],'name':row[1], 'username':row[2], 'password':row[3], 'val1':row[4], 'val2':row[5]})

        new_list = None
        for item in conns:
            if item['name'] == to_delete:
                new_list = item
                break
        new_cons = conns.copy()
        new_cons.remove(new_list)

        i = 1
        for d in new_cons:
            d['id'] = i
            i += 1

        msg.showinfo("Button Deleted", (to_delete + " Button Deleted"))

        csv_file_name = "file.csv"
        with open(csv_file_name, 'w', newline='') as csvfile:
            writer = csv.DictWriter(csvfile, fieldnames=header)
            writer.writeheader()
            for data in new_cons:
                writer.writerow(data)

    create_delete_dropdown()

# LabelFrame 3 Content
delete_label = ttk.Label(labelFrame3, text = "Delete Who?: ")
delete_label.grid(row=0, column=0)

deleteButton = tk.Button(master=labelFrame3, text="Delete", command=delete_saved_button)
deleteButton.grid(row=1, column=0)

root.mainloop()

I have left a few print statements in to try to help me work out what I am doing wrong.

If I delete Billy first he is deleted from the CSV and the dropdown which is great.

If I then select Amanda and try to delete her second I get an error - my output from this sequence including my print statements is:

CLICKING
Billy
Billy
['Amanda', 'Sammy']
FUNC

CLICKING
Billy
Billy
Exception in Tkinter callback
Traceback (most recent call last):
  File "C:\Users\reeno\AppData\Local\Programs\Python\Python37\lib\tkinter\__init__.py", line 1705, in __call__
    return self.func(*args)
  File ".\fucked.py", line 90, in delete_saved_button
    new_cons.remove(new_list)
ValueError: list.remove(x): x not in list

Here is the script being run:

Gif Running Script

I appreciate if you can advise what I am doing wrong or point me to a resource that might give me the answer.

Remember I am a noob but I would also love any suggestions to improve my code in general too.

Thank you for your help!!

Upvotes: 0

Views: 1522

Answers (1)

furas
furas

Reputation: 142631

Your problem is local and global variable delete_combo.

When you create new Combobox() in create_delete_dropdown() then it creates local variable delete_combo and it assings new combobox to this local variable but it doesn't change value in global variable delete_combo which is used by other functions - so other functions still use old Combobox() (which is hidden behind new Combobox() in grid()).

You have to use global delete_combo in function create_delete_dropdown() to inform this function that you want to assign to external/global variable, not local one

def create_delete_dropdown():
    global delete_combo # inform function that you will assign (=) to external/global variable

    buttons2 = get_button_names()
    print(buttons2)

    delete_combo = ttk.Combobox(labelFrame3, value=buttons2)
    delete_combo.bind("<<ComboboxSelected>>", clicking)
    delete_combo.grid(row=0, column=1, columnspan=3, padx=10, pady=10)

    print("FUNC")
    help = delete_combo.get()
    print(help)

But instead of creating new Combobox() you can change values in existing Combobox() as @TheFluffDragon9 mentioned in comment.

def create_delete_dropdown():

    buttons2 = get_button_names()
    print(buttons2)

    delete_combo['value'] = buttons2
    delete_combo.set('') 

    print("FUNC")
    help = delete_combo.get()
    print(help)

EDIT: Code with other changes.

I use DictReader to get data in list of rows (dictionares) to keep original order of rows (older Python didn't have to keep order in dictionary)

import os.path
import csv
import shutil
import tkinter as tk # PEP8: `import *` is not preferred
from tkinter import ttk
from tkinter import messagebox as msg

# --- constants --- (UPPER_CASE_NAMES)

ORIGINAL = 'file.csv'
BACKUP   = 'file.bak'

# --- function --- (lower_case_names)

def read_data():
    global header # to keep in global variable

    with open(ORIGINAL) as csvfile:
        csv_reader = csv.DictReader(csvfile)
        header = csv_reader.fieldnames
        data = list(csv_reader)
        #print('header:', header)

    return data


def write_data(data, make_backup=True):
    if make_backup:
        shutil.copyfile(ORIGINAL, BACKUP)

    with open(ORIGINAL, 'w', newline='') as csvfile:
        csv_writer = csv.DictWriter(csvfile, fieldnames=header)
        csv_writer.writeheader()
        csv_writer.writerows(data)


def clicking(event):
    print("CLICKING:", delete_combo.get())
    print("CLICKING:", event.widget.get())


def get_column(column="name"):
    data = read_data()
    values = []

    for row in data:
        values.append(row[column])

    return values


def delete_saved_button():
    to_delete = delete_combo.get()
    print('to_delete:', to_delete)

    if not to_delete:
        msg.showinfo("Button Deleted", "You have to select name.")
        return

    answer = msg.askyesno("Delete Button", "Are you sure you want to Delete {}?".format(to_delete))

    if answer == True:
        # read old data
        data = read_data()

        # search element to delete
        number_to_delete = None
        for number, row in enumerate(data):
            if row['name'] == to_delete:
                number_to_delete = number
                break

        if number_to_delete is not None: # it has to compare with None because number can be 0 and bool(0) gives also False
            del data[number_to_delete]
        else:
            msg.showinfo("Button Deleted", "Name '{}' doesn't exist.".format(to_delete))
            return

        for number, row in enumerate(data, 1):
            row['id'] = number

        # write new data
        write_data(data)

        # update dropdown
        delete_combo["value"] = get_column("name")
        delete_combo.set("")

        msg.showinfo("Button Deleted", "{} Button Deleted".format(to_delete))


# --- main --- (lower_case_names)

root = tk.Tk()
root.title('Why am I an idiot')

label_frame = ttk.LabelFrame(text="Delete Button") # 
label_frame.grid(column=0, row=2, padx=20, pady=20) # PEP8: without spaces around =

delete_label = ttk.Label(label_frame, text="Delete Who?: ")
delete_label.grid(row=0, column=0)

names = get_column("name")

delete_combo = ttk.Combobox(label_frame, value=names)
delete_combo.bind("<<ComboboxSelected>>", clicking)
delete_combo.grid(row=0, column=1, columnspan=3, padx=10, pady=10)

delete_button = tk.Button(master=label_frame, text="Delete", command=delete_saved_button)
delete_button.grid(row=1, column=0)

root.mainloop()

PEP 8 -- Style Guide for Python Code

Upvotes: 1

Related Questions