0

I am a teacher. Using Tkinter and Openpyxl I need to code a graphical user interface where I could click on a list of students from a “.xlsx” file. I want to “link” every student’s name with a checkbutton square and display his/her marks (saved on the “.xlsx” file) whenever his/her checkbutton is active. How can I create a set of widgets/checkbuttons that I can refer individually later on? In the code below every checkbutton has the same name!!!

# -*- coding: utf-8 -*-
from tkinter import *
import openpyxl

wb = openpyxl.load_workbook('marks.xlsx', data_only=True)
sheet = wb.active

names_and_rows = {}

for i in range(2, sheet.max_row + 1):
    name = sheet.cell(row=i, column=1).value
    names_and_rows[name] = i

root = Tk()
root.title("Student's marks")

students_names = Frame(root, bd=1, relief="solid")
students_names.pack(side="left")

student_marks = Frame(root, bd=1, relief="solid")
student_marks.pack(side="right")

message = Label(student_marks, text="You still haven't checked on any student's name")
message.pack()


def get_marks(v):
    marks = ""
    for i in range(2, sheet.max_column + 1):
        information = str(sheet.cell(row=1, column=i).value) + ": " + str(sheet.cell(row=v, column=i).value) + "\n"
        marks = marks + information
    if (v.get() == 1):
        message.config(text=marks)
    else:
        message.config(text="You still haven't checked on any student's name")


list_of_widgets = []

for k, v in names_and_rows.items():
    square = Checkbutton(students_names, variable=v, onvalue=1, offvalue=0, text=k, command=lambda: get_marks(v))
    list_of_widgets.append(square)
    square.pack()

root.mainloop()

Simplified Worksheet

Tkinter GUI

Reedinationer
  • 5,661
  • 1
  • 12
  • 33

1 Answers1

0

I think the best approach would be to store each widget in a list. Something like:

list_of_widgets = []
for student in students:
    square = Checkbutton(root, variable=value, text=student)
    list_of_widgets.append(square)
    square.pack()

Although I'm also pretty sure you'd need to assign a new variable to each, and you'd probably want to store that in a tuple within your list. Maybe something like this would be more appropriate:

list_of_widgets = []
for student in students:
    value = IntVar()
    square = Checkbutton(root, variable=value, text=student)
    list_of_widgets.append((square, value)) # appending a tuple ()
    square.pack()

You could even include other information that makes it easier to associate a button with a particular student:

list_of_widgets.append((student, square, value)) # appending a tuple ()

You could then have a thread looping forever and if a change of state occurs update your program:

import threading

def check_for_changes():
    global list_of_widgets
    students_selected = []
    while True: # loop forever
        for item in list_of_widgets:
            if item[2].get() != 0 and item[0] not in students_selected:
                students_selected.append(item)
                # or some other code

t = threading.Thread(target=check_for_changes)
t.start()

Note this only adds and you would need other logic to remove students that you deselect, but I this is generally how I would approach the problem!

edit

You need to have a look at the post here because you are assigning a reference to a variable and not the variable itself, so it will always evaluate to the last value of the loop for all buttons. I like the answer lambda i=i: self.open_this(i) and modifying your code to implement this would look more like:

from tkinter import *
import openpyxl
from collections import OrderedDict

wb = openpyxl.load_workbook('test.xlsx', data_only=True)
sheet = wb.active

names_and_rows = OrderedDict()

for i in range(2, sheet.max_row + 1):
    name = sheet.cell(row=i, column=1).value
    names_and_rows[name] = i

root = Tk()
root.title("Student's marks")

students_names = Frame(root, bd=1, relief="solid")
students_names.pack(side="left")

student_marks = Frame(root, bd=1, relief="solid")
student_marks.pack(side="right")

message = Label(student_marks, text="You still haven't checked on any student's name")
message.pack()


def get_marks(v):
    button_status = list_of_widgets[v][1].get()
    if button_status:
        row = list_of_widgets[v][2] + 2
        marks = ""
        for col in range(2, sheet.max_column + 1):
            information = str(sheet.cell(row=1, column=col).value) + ": " + str(sheet.cell(row=row, column=col).value) + "\n"
            marks = marks + information
        message.config(text=marks)
    else:
        message.config(text="You unselected a student")


list_of_widgets = []
i = 0
for k, v in names_and_rows.items():
    new_variable = IntVar()
    square = Checkbutton(students_names, variable=new_variable, onvalue=1, offvalue=0, text=k, command=lambda i=i: get_marks(i))
    list_of_widgets.append((square, new_variable, i))
    square.pack()
    i += 1

root.mainloop()

Of special note you need to use collections.OrderedDict() to maintain the order of your list of names. This will not only make them appear in the correct order from your Excel sheet (Imagine trying to find a name in a list of 100 names that are randomly scrambled every time your program runs...That's where you were headed), but will also allow you to determine what row you need to reference within your get_marks() function.

Community
  • 1
  • 1
Reedinationer
  • 5,661
  • 1
  • 12
  • 33
  • There is at least one problem in your code: `value = IntVar` does not do what you think it does. Also, if `list_of_widgets` has two-tuples, `item[2]` will throw an error. – Bryan Oakley Apr 08 '19 at 23:48
  • I was just copying that from OP, but you are right it should be `IntVar()` now that you point it out – Reedinationer Apr 08 '19 at 23:48