Treeview heading off by one column? - python

I cannot for the life of me find any post or documentation that explains why the Treeview heading is off by one column from the rest of the data.
Even the documentation I found shows this issue in an example but does not describe the issue.
import tkinter as tk
import tkinter.ttk as ttk
import random
n, m = 40, 10
table = []
for i in range(n):
line = []
# line.append('')
# This adds a blank string to start of row data to off set the data.
# It is my current work around but does not look great.
for j in range(m):
line.append(random.randint(0, 999))
table.append(line)
class Demo(tk.Tk):
def __init__(self):
super().__init__()
tree = ttk.Treeview(self)
tree.pack()
for i in range(n):
tree.insert('', 'end', text=table[i][0], values=table[i][1:])
tree['columns'] = list(range(m - 1))
headings = list('ABCDEFGHI')
for j in range(m - 1):
tree.column(j, width=50, anchor='e')
tree.heading(j, text=headings[j])
if __name__ == '__main__':
Demo().mainloop()
As you can see the headings are off by one column. I cannot figure out why this is or how to fix it properly.
I did manage a bad workaround that appends an empty string to the start of the row data so it lines up with the correct headings but this cannot be the correct or best way of fixing this.
Am I missing something here? Is this normal for Treeview?

As I know first column has special meaning - it can be used to display tree with nested elements - like on image in tkdoc - and it may need special method to set header.
tree.heading('#0', text='Hello')
Result:
You may also use "#1", "#2", etc. to set other headers.
You can use f'#{j}' in loop.
headings = list('ABCDEFGHIJ')
for j in range(m):
tree.column(f'#{j}', width=50, anchor='e')
tree.heading(f'#{j}', text=headings[j])
Full working code:
import tkinter as tk
import tkinter.ttk as ttk
import random
n, m = 40, 10
table = []
for i in range(n):
line = []
# line.append('')
# This adds a blank string to start of row data to off set the data.
# It is my current work around but does not look great.
for j in range(m):
line.append(random.randint(0, 999))
table.append(line)
class Demo(tk.Tk):
def __init__(self):
super().__init__()
tree = ttk.Treeview(self)
tree.pack()
for i in range(n):
tree.insert('', 'end', text=table[i][0], values=table[i][1:])
tree['columns'] = list(range(m - 1))
headings = list('ABCDEFGHIJ')
for j in range(m):
tree.column(f'#{j}', width=50, anchor='e')
tree.heading(f'#{j}', text=headings[j])
#tree.heading('#0', text='Hello')
if __name__ == '__main__':
Demo().mainloop()
Doc: Column identifiers

Related

Build a fixed-size data table with tkinter / tksheet in Python -- bug in sheet_display_dimensions

I am building a user interface with several (as many as the user wants) tabular (spreadsheet-like) forms of user-specified size (but the size won't change once initialized). The user populates these tables either by copy-pasting data (usually from excel) or directly typing data to the cells. I am using the Tksheet Tkinter add-on.
It seems that there are several options in Tksheet to achieve the goal of opening an empty table of i rows and j columns:
a) set_sheet_data_and_display_dimensions(total_rows = None, total_columns = None).
This routine throws a TypeError. The error is raised in:
GetLinesHeight(self, n, old_method = False)
The subroutine expects the parameter n to be an integer, but receives a tuple.
The calling routine is sheet_display_dimensions, and the relevant line is:
height = self.MT.GetLinesHeight(self.MT.default_rh).
MT.default_rh is apparently a complex object, it can be an integer, but also a string or a tuple. Other routines that use it in Tksheet perform elaborate manipulation to make sure it is handed to the subroutine in integer form, but not so sheet_display_dimensions.
b) sheet_data_dimensions(total_rows = None, total_columns = None)
This seems to work programmatically, but does not display the table to the user.
One may add the line sheet_display_dimensions(i,j) but--you guessed it--this raises an error...
Sample code:
from tksheet import Sheet
import tkinter as tk
from tkinter import ttk
# This class builds and displays a test table. It is not part of the question but merely used to illustrate it
class SeriesTable(tk.Frame):
def __init__(self, master):
super().__init__(master) # call super class init to build frame
self.grid_columnconfigure(0, weight=1) # This configures the window's escalators
self.grid_rowconfigure(0, weight=1)
self.grid(row=0, column=0, sticky="nswe")
self.sheet = Sheet(self, data=[[]]) # set up empty table inside the frame
self.sheet.grid(row=0, column=0, sticky="nswe")
self.sheet.enable_bindings(bindings= # enable table behavior
("single_select",
"select_all",
"column_select",
"row_select",
"drag_select",
"arrowkeys",
"column_width_resize",
"double_click_column_resize",
"row_height_resize",
"double_click_row_resize",
"right_click_popup_menu",
"rc_select", # rc = right click
"copy",
"cut",
"paste",
"delete",
"undo",
"edit_cell"
))
# Note that options that change the structure/size of the table (e.g. insert/delete col/row) are disabled
# make sure that pasting data won't change table size
self.sheet.set_options(expand_sheet_if_paste_too_big=False)
# bind specific events to my own functions
self.sheet.extra_bindings("end_edit_cell", func=self.cell_edited)
self.sheet.extra_bindings("end_paste", func=self.cells_pasted)
label = "Change column name" # Add option to the right-click menu for column headers
self.sheet.popup_menu_add_command(label, self.column_header_change, table_menu=False, index_menu=False, header_menu=True)
# Event functions
def cell_edited(self, info_tuple):
r, c, key_pressed, updated_value = info_tuple # break the info about the event to individual variables
'''
updated_value checked here
'''
# passed tests
pass # go do stuff
def cells_pasted(self, info_tuple):
key_pressed, rc_tuple, updated_array = info_tuple # break the info about the event to individual variables
r, c = rc_tuple # row & column where paste begins
err_flag = False # will be switched if errors are encountered
'''
updated_array is checked here
'''
# passed tests
if err_flag: # error during checks is indicated
self.sheet.undo() # undo change
else:
pass # go do stuff
def column_header_change(self):
r, c = self.sheet.get_currently_selected()
col_name = sd.askstring("User Input", "Enter column name:")
if col_name is not None and col_name != "": # if user cancelled (or didn't enter anything), do nothing
self.sheet.headers([col_name], index=c) # This does not work - it always changes the 1st col
self.sheet.redraw()
# from here down is test code
tk_win = tk.Tk() # establish the root tkinter window
tk_win.title("Master Sequence")
tk_win.geometry("600x400")
tk_win.config(bg='red')
nb = ttk.Notebook(tk_win) # a notebook in ttk is a [horizontal] list of tabs, each associated with a page
nb.pack(expand=True, fill='both') # widget packing strategy
settings_page = tk.Frame(nb) # initiate 1st tab object in the notebook
nb.add(settings_page, text = "Settings") # add it as top page in the notebook
test = SeriesTable(nb) # creates a 1 row X 0 column table
nb.add(test, text = "Table Test") # add it as second page in the notebook
i = 4
j = 3
#test.sheet.set_sheet_data_and_display_dimensions(total_rows=i, total_columns=j) # raises TypeError
#test.sheet.sheet_data_dimensions(total_rows=i, total_columns=j) # extends the table to 4 X 3, but the display is still 1 X 0
#test.sheet.sheet_display_dimensions(total_rows=i, total_columns=j) # raises TypeError
test.sheet.insert_columns(j) # this works
test.sheet.insert_rows(i - 1) # note that we insert i-1 because table already has one row
test.mainloop()
I figured out a work-around with:
c)
insert_columns(j)
insert_rows(i - 1)
Note that you have to insert i-1 rows. This is because the sheet object is initiated with 1 row and 0 columns. (But does it say so in the documentation? No it does not...)

Tkinter: multiple choice quiz app, can't change question (and other issues)

I'm trying to create a multiple choice question app with tkinter but I'm having three problems that I can't solve by myself:
The question don't change, it stuck on the first question.
I have a file with all my questions (more then 50) what I'd like to do is select randomly only 10 of them from the list (five from easy_question list and 5 from the hard_question list).
Is there a way to save in an exel file the ten code of the question that where selected and to know what that person answered (wrong or right doesn't metter)? something like this:
1 2 3 [...]
question 1e 3h 2e
answer 2 3 4
This is a simple version of my file with all my questions, options and correct answers:
easy_questions2=[
"1e. Name?",
"2e. Last name?",
"3e. Birthdate?",
"4e. Food?"
]
easy_options=[
['Marck', 'Mary','Joseph','John'],
['Smith', 'Hartnett','Pitt','Pacino'],
['June', 'October','November','April'],
['All', 'Fries','Pasta','Chicken']
]
easy_answers=[
1,
2,
3,
3
]
hard_questions2=[
"1h. Number?",
"2h. Word?",
"3h. Hour?",
"4h. Color?"
]
hard_options=[
['10', '11','21','55'],
['Book', 'Table','en','Pacino'],
['11', '21','24','18'],
['Yellow', 'Blue','Red','Green']
]
hard_answers=[
3,
4,
1,
2
]
This is my code:
import tkinter as tk
import tkinter.ttk as ttk
from questionslist import easy_questions2, easy_answers, easy_options
from openpyxl import load_workbook
from tkinter import messagebox as mb
class MainApplication(tk.Frame):
def __init__(self, parent, *args, **kwargs):
tk.Frame.__init__(self, parent, *args, **kwargs)
self.q_no=0
self.display_title()
self.display_question()
self.opt_selected=tk.IntVar()
self.opts=self.radio_buttons()
self.display_options()
self.buttons()
self.data_size=len(question)
self.correct=0
def display_result(self):
wrong_count = self.data_size - self.correct
correct = f"Correct: {self.correct}"
wrong = f"Wrong: {wrong_count}"
score = int(self.correct / self.data_size * 100)
result = f"Score: {score}%"
mb.showinfo("Result", f"{result}\n{correct}\n{wrong}")
def check_ans(self, q_no):
if self.opt_selected.get() == answer[q_no]:
return True
def next_button(self):
if self.check_ans(self.q_no):
self.correct += 1
self.q_no += 1
if self.q_no==self.data_size:
self.display_result()
self.submit()
self.destroy()
else:
self.display_question()
self.display_options()
def buttons(self):
next_button = tk.Button(self, text="Next",command=self.next_button, width=10)
next_button.pack(pady=50, side="bottom")
def display_options(self):
val=0
self.opt_selected.set(0)
for option in options[self.q_no]:
self.opts[val]['text']=option
val+=1
def display_question(self):
q_no = tk.Label(self, text=question[self.q_no], width=60)
q_no.pack(padx=19, pady=31, anchor="w")
def radio_buttons(self):
q_list = []
while len(q_list) < 4:
radio_button = ttk.Radiobutton(self,text=" ",variable=self.opt_selected,
value = len(q_list)+1)
q_list.append(radio_button)
radio_button.pack(padx=19, anchor="w")
return q_list
def submit(self):
wb = load_workbook('D:\\Python\\quiz\\template.xlsx')
sheet = wb.active
sheet.cell(row=1, column=2).value = "first_name"
sheet.cell(row=1, column=3).value = "correct"
sheet.cell(row=1, column=4).value = "wrong"
sheet.cell(row=2, column=3).value = self.correct
sheet.cell(row=2, column=4).value = self.data_size - self.correct
excel_filename = "D:\\Python\\quiz\\" + "data" + ".xlsx"
wb.save(excel_filename)
question = easy_questions2
options = easy_options
answer = easy_answers
if __name__ == "__main__":
root = tk.Tk()
MainApplication(root).pack(side="top", fill="both", expand=True)
root.title("Quiz")
root.geometry('800x450')
root.mainloop()
If you want to read the questions from a file (or more), you could use (suppose the file is a .txt and you've wrote a question per line):
question_list = []
with open('filename.txt', 'r+') as question_file:
lines = question_file.readlines()
# Let's move the pointer of the file back to the start to resave the questions
# as we don't want to lose them.
question_file.seek(0)
question_file.truncate()
for line in lines:
question_list.append(line) # Put each question at the end of the list.
question_file.write(line)
Use this method to retrieve all the questions and answers from all your files and to put them into lists.
Now you should have a list for the easy questions and one for the hard questions, so, to pick a random question from one list you can use a specific function from the random module, randint(a, b) which is a function that return a value in between 'a' and 'b' ('a' and 'b' included). Try it this way:
from random import randint
# You have a list of 25 hard question, let's call it hard_list
# and a list of 25 easy question, let's call it easy_list
# Now let's use our randint function to retrieve 10 random value between 0 and 24.
# As you haven't done any statement about how to choose the order of the questions,
# I'll take the first five from the hards one and the last 5 from the easy,
# but you can do it in any way you want.
# It may be a little tricky to manage the possibility of same random numbers to come out,
# surely there are better ways to do it, but this works fine.
random_questions = [] # Here I write the questions randomly chosen.
random_numbers = [] # I use this to take into account the random values.
i = 0 # Our counter.
while i < 10:
# If 5 questions have already been chosens, we change the list
# and clear the random values counter once.
if i >= 5:
if i == 5:
random_numbers.clear()
val = randint(0, 24)
if val not in random_numbers:
i += 1
random_numbers.append(val)
random_questions.append(easy_list[val])
else:
val = randint(0, 24)
if val not in random_numbers:
i += 1
random_numbers.append(val)
random_questions.append(hard_list[val])
Now, with your code, I see that the 'check_button' function doesn't check if the user have select an option or not, but maybe it's what you wanted, secondly, your button don't change the question because.. you're assigning a new Label to q_no (which, I suppose, is the number of questions answered so far)? You should assign the Label to another variable and update its text value each time the user click the button.
For example:
self.question_label = tk.Label(self, text=question[self.q_no], ....)
self.question_label.pack(...)
And when the user press the button to the next question, you check if the answer is correct, then:
self.q_no += 1
if self.q_no==(self.data_size + 1):
#
# Do what you want to do if that was the last question.
#
else:
self.question_label['text'] = question[self.q_no]
#
# Here change the value of the buttons with the answers.
#
For the third question, what format do you exactly want? There are differences between .csv, .xlsx, .xlsb, etc. But for the conversion you can use the pandas module.
These methods need a specific type of object for the conversion, so be shure to format your data in that specific way before using them:
to csv to xlsx. This is the class you can use as data structure.
From personal experience I know that you can format your datas as json to convert them to csv, but I'm not shure if it works for xlsx and other formats too.
Hope I've helped a little bit.

Code is not working when using inside function. why?

I am creating a SQlite3 GUI interface in tkinter (ttk), but i am stuck in half way. Please see the below code
import sqlite3
from tkinter import ttk
import tkinter as tk
root = tk.Tk()
conn = sqlite3.connect('contact.sqlite3')
conn_cursor = conn.cursor()
res = conn.execute("SELECT name FROM sqlite_master WHERE type='table';")
tables = tuple([tbl_name[0] for tbl_name in res])
table_chooser = ttk.Combobox(root)
table_chooser['values'] = tables
table_chooser.grid(row=0, column=0)
def view_table():
selected_table = 'contacts' #table_chooser.get()
table_names = conn.execute(f'''SELECT * FROM pragma_table_info('%{selected_table}%')''')
column_header = [nam[1] for nam in table_names]
stringvar_header = [tk.StringVar() for _ in column_header]
alldb = conn.execute(f'''SELECT * FROM {selected_table} ''').fetchall()
stringvar = [[tk.StringVar() for _ in alldb[0]] for _ in alldb]
def header_entry(i):
stringvar_header[i].set(column_header[i])
return ttk.Entry(root, textvariable=stringvar_header[i], state="disabled").grid(row=1, column=i)
def create_entry(i, j):
stringvar[i][j].set(alldb[i][j])
return ttk.Entry(root, textvariable=stringvar[i][j]).grid(row=i+2, column=j)
h_entry = [header_entry(i) for i in range(len(column_header))]
entry = [[create_entry(i, j) for j in range(len(alldb[0]))] for i in range(len(alldb))]
view_table()
#ttk.Button(root,text="view",command=view_table).grid(row=0,column=2)
root.mainloop()
the problem is that, if we run the below code we can see only empty cells of Entry widget, but if we write the contents of view_table() function directly in the code(.ie without using function ) it is working fine
and you can see all the contents of table 'contacts'. in Entry widget.
so what is happening when using function and without function

Tkinter labels not returning the proper value

Using python 2.7 and Tkinter.
I am creating four lables in a loop and binding them to . I want the label to return
the name in the label's text.
The problem is that no matter which label I press it returns the name in the last label.
I found this question Python Tkinter: Bind function with labels in for loop with exactly my problem but the solution given does not work for me even if I copied the code exactly.
Please anyone? here is my original code:
# labelbind.py
from Tkinter import *
root = Tk()
root.title('Label choices')
root.geometry('1160x900+650+50')
root.option_readfile('setstyle2.txt')
def lblpress(x):
print 'Label pressed', x
names = ['AMEX', 'CIBC', 'VISA', 'BMO']
col = 150
row = 45
num = 1
for name in names:
bobo = 'lbl' + str(num)
print bobo, name
bobo = Label(root, text = name)
bobo.bind('<ButtonRelease-1>', lambda x = name : lblpress(name))
bobo.place(x = col, y = row)
row += 40
num += 1
root.mainloop()
You don't need to pass anything to the callback. The event object that is given to the callback contains a reference to the widget, and from the widget you can get the text.
For example:
import Tkinter as tk
def lblpress(event):
print 'Label pressed:', event.widget.cget("text")
root = tk.Tk()
names = ['AMEX', 'CIBC', 'VISA', 'BMO']
for name in names:
label = tk.Label(root, text=name)
label.bind("<ButtonRelease-1>", lblpress)
label.pack(side="top")
root.mainloop()

Python Tkinter Treeview - Iterating 'get_children' output

I am trying to later iterate through the data inside of the Treeview. I will then hope to be able to sort through it.
from tkinter import *
from tkinter.ttk import *
import pickle
root = Tk()
def treeData(event):
children = tree.get_children()
print(children)
entry = StringVar()
a = Entry(root, textvariable=entry)
a.grid(column=0,row=0)
a.bind("<Key>", function)
file_data = []
file = open('data.dat', 'rb')
while True:
try:
file_data.append(pickle.load(file))
except EOFError:
break
file.close()
column_names = ("Column 1", "Column 2")
tree = Treeview(root, columns=column_names)
tree['show'] = 'headings'
for x in file_data:
a = tree.insert('', 'end', values=x)
for col in column_names:
tree.heading(col, text=col)
tree.grid(column=0, row=1)
In the function, called 'treeData' when I print(children) it outputs a list that looks similar to this - ('I001', 'I002', 'I003', 'I004')
I am hoping someone will know how to convert these pieces of data into what is actually shown in the row of the Treeview?
Thanks,
What you are asking is documented in the official tkinter documentation for the Treeview widget.
The get_children method returns a list of item IDs, one for each child. The item method of the treeview will return a dictionary of data for a given item. Thus, you can iterate over the values with something like this:
for child in tree.get_children():
print(tree.item(child)["values"])

Categories

Resources