Tkinter how to sort dynamically added buttons - python

I am trying to create a pop up window in customtkinter with dynamically added checkboxes (this creates 80 checkboxes).
def open_secondary_window(self):
# Create secondary (or popup) window.
secondary_window = customtkinter.CTkToplevel()
secondary_window.geometry(f"{400}x{200}")
secondary_window.title("Object Selection")
# Create a button to close (destroy) this window.
for i in range(len(class_names)):
c = customtkinter.CTkCheckBox(secondary_window, text=class_names[i])
c.pack()
button_close = customtkinter.CTkButton(
secondary_window,
text="Close window",
command=secondary_window.destroy)
button_close.place(x=75, y=75)`
This is the result
What I was thinking of doing is to iterate trough the list of checkboxes and every 10 to go to a new column:
for i in range(len(class_names)):
c = customtkinter.CTkCheckBox(secondary_window, text=class_names[i])
c.grid(row=i, column=0)
the hard part is that I don't know how to iterate after every 10 columns to start from row 0 and column +1
Would anyone know a better solution on how to handle this
enter image description here

You can use python's divmod function to convert an integer into a row and column.
for i in range(80):
column, row = divmod(i, 10)
cb = ctk.CTkCheckBox(root, text=f"{i+1}")
cb.grid(row=row, column=column, sticky="nsew")

Related

Tkinter Formatting Buttons in Same Row using .grid

How can I display a row of buttons across the bottom of the screen using Tkinter? The number of buttons is a variable. Could be 2 or could be 10. The number of items being displayed is also a variable. In this case it's 5, but could be 2 or could be 10. I am using Tkinter and currently have a working program that outputs a grid that looks like this:
-------------------------
| |Title| |
|Item1| |Quantity1|
|Item2| |Quantity2|
|Item3| |Quantity3|
|Item4| |Quantity4|
| | (Intentionally blank)
|Item5| |Quantity5|
-------------------------- (end)
I am outputting like so:
from tkinter import *
window = Tk()
itemLabel = Label(
window,
text = "Item1",
).grid(row = 2, column = 0, sticky = 'w')
However, when I try to add buttons, I can't seem to get the formatting correct. If I do it similarly to the label, with "sticky = 'w'", then it overlaps on the left. I only have 3 columns so if I have more than 3 buttons I run out of columns. Below is my desired output (hopefully all of the buttons will be of equal width):
-------------------------
| |Title| |
|Item1| |Quantity1|
|Item2| |Quantity2|
|Item3| |Quantity3|
|Item4| |Quantity4|
| | (Intentionally blank)
|Item5| |Quantity5|
--------------------------
|B#1| B#2|B#3|B#4| B#5|B#6|
--------------------------- (end)
I had a similar problem, but for radiobuttons - a variable number of options which can change, in this case updated on the click of a button. I set the max number of columns as I spill over into more rows if need be.
def labelling_add_radiobutton(self):
'''add selected values as radiobuttons for labelling'''
# destroy any existing radiobuttons in frame
for child in self.frame3_labelling_labels.winfo_children():
if isinstance(child, ttk.Radiobutton):
child.destroy()
# label variable and create radio button
self.checkbox_validation_output = tk.StringVar(value = '')
num_cols = 8 # sets max number of columns, rows are variable
for count, value in enumerate(list_of_buttons):
row = int((count + .99) / num_cols) + 1
col = (((row - 1) + count) - ((row - 1) * num_cols) - (row - 1)) + 1
ttk.Radiobutton(self.frame3_labelling_labels, text = value,
variable = self.checkbox_validation_output, value = value,
style = 'UL_radiobutton.TRadiobutton').grid(column = col, row = row,
sticky = 'w', padx = 10, pady = 10)

Skipping certain amount of cells if a condition is true in a for loop (Python)

I'm trying to tell python to insert "No work" in certain amount of cells, depending on how many days has passed since the codes was run.
So basically, let imagine that the code is run on a Friday and then a few day passes before it's started again. I want python to understand the number of days that has passed and insert "No work" in x numbers of cells in a excel file.
I've been using datetime module to calculate the number of days the code has been inactive in the "excel_update" function but find it very difficult to skip corresponding number of days in the for loop.
Can someone please tell me how this is done and explain you'r reasoning?
# ***** IMPORTS *****
import tkinter as tk
from unittest import skip
from openpyxl import Workbook
from openpyxl import load_workbook
import datetime as dt
# ***** VARIABLES *****
# use a boolean variable to help control state of time (running or not running)
running = False
# time variables initially set to 0
hours, minutes, seconds, total_time = 0, 0, 0, 0
# ***** NOTES ON GLOBAL *****
# global will be used to modify variables outside functions
# another option would be to use a class and subclass Frame
# ***** FUNCTIONS *****
# start, pause, and reset functions will be called when the buttons are clicked
# start function
def start():
global running
if not running:
update()
running = True
# pause function
def pause():
global running
if running:
# cancel updating of time using after_cancel()
stopwatch_label.after_cancel(update_time)
running = False
# reset function
def reset():
global running
if running:
# cancel updating of time using after_cancel()
stopwatch_label.after_cancel(update_time)
running = False
# set variables back to zero
global hours, minutes, seconds
hours, minutes, seconds = 0, 0, 0
# set label back to zero
stopwatch_label.config(text='00:00:00')
# update stopwatch function
def update():
# update seconds with (addition) compound assignment operator
global hours, minutes, seconds, total_time
seconds += 1
total_time += 1
if seconds == 60:
minutes += 1
seconds = 0
if minutes == 60:
hours += 1
minutes = 0
# format time to include leading zeros
hours_string = f'{hours}' if hours > 9 else f'0{hours}'
minutes_string = f'{minutes}' if minutes > 9 else f'0{minutes}'
seconds_string = f'{seconds}' if seconds > 9 else f'0{seconds}'
# update timer label after 1000 ms (1 second)
stopwatch_label.config(text=hours_string + ':' + minutes_string + ':' + seconds_string)
# after each second (1000 milliseconds), call update function
# use update_time variable to cancel or pause the time using after_cancel
global update_time
update_time = stopwatch_label.after(1000, update)
# Fill in the excel file with times
def excel_update():
global hours, minutes, seconds, total_time
dest_filename = r"C:\Users\abbas\OneDrive\Desktop\Prog\Flex\tider.xlsx"
wb = load_workbook(dest_filename)
ws = wb.active
days = [0]
todays_date = dt.datetime.today()
the_day = todays_date.day
days.append(the_day)
time_difference = the_day - days[-2]
#time_difference = 4
# Put value in next empty cell in specified region (if there is one).
try:
for row in ws.iter_rows(min_row=1, max_row=3, max_col=3):
for cell in row:
if time_difference >=2:
next
if cell.value is None:
cell.value = f"{hours} h and {minutes} min"
raise StopIteration
except StopIteration:
pass
wb.save(dest_filename) # Update file.
# ***** WIDGETS *****
# create main window
root = tk.Tk()
#root.geometry('450x200')
root.resizable(width=False, height=False)
root.title('Stopwatch')
# label to display time
stopwatch_label = tk.Label(text='00:00:00', font=('Arial', 20))
stopwatch_label.pack()
# start, pause, reset, stop buttons
start_button = tk.Button(text='start', height=3, width=5, font=('Arial', 10), command=start)
start_button.pack(side=tk.LEFT)
stop_button = tk.Button(text='stop', height=3, width=5, font=('Arial', 10), command=lambda:[root.quit(), excel_update()])
stop_button.pack(side=tk.LEFT)
pause_button = tk.Button(text='pause', height=3, width=5, font=('Arial', 10), command=pause)
pause_button.pack(side=tk.LEFT)
reset_button = tk.Button(text='reset', height=3, width=5, font=('Arial', 10), command=reset)
reset_button.pack(side=tk.LEFT)
# ***** MAINLOOP *****
# run app
root.mainloop()
What I see in your code is that you are just inserting an hour/min text entry in the Excel cell when clicking the stop button (after clicking start button); f"{hours} h and {minutes} min".
So when you run the app again there is just the 'Xh and Ymin' entry in the cell.
When excel_update is called it;
Creates a list 'date' with value 0
Gets todays date assigned to 'todays_date'
Gets the current dates day and appends that to the list 'date'
Calculates 'time_difference' as date[-2] which will always be the first element you assigned when creating the list, subtracted from the current date i.e. will always be the current date (The list only has two elements so [-2] is always the first element you assigned at creation)
Either way the main point is you would not be able to calculate the time difference without a record of when the last entry was made. You probably need to include the current date/time as well in another cell either as a single 'last update' value or per entry made, whichever works best.
Then when running update_excel again read the 'last update' date in order to get the date and this would be the first entry in your date list. Bear in mind that such a calculation is too simplistic however as it doesn't account for the next month(s) so you should use the full date, i.e. if you run it on April 30 then May 6 the days difference is not 6 - 30.
Additional details
The following is some additional information that should help. There will likely be more issues but this should help work on the main requirements as stated
(1) As mentioned to record the last time an update was made it is probably easiest to just update a cell with the current date before saving the workbook. You have the variable
todays_date = dt.datetime.today()
so you can just use that. It presumably should be fine to use the same cell each time, eg 'E2' (if we are leaving row 1 for Headers).
ws['E2'].value = todays_date
wb.save(dest_filename) # Update file.
Then when running the update_excel function one of the first lines after the definitions would be to read this last update value.
last_update = ws['E2'].value
Calculation of the days missed can then be made by subtracting this value from the todays_date variable using the 'days' attribute to get the actual number of days. The .days attribute will provide an integer value corresponding to the number of days between the two dates and will give the correct value across different months.
time_difference = (todays_date-last_update).days
(2) The cell update code updates the 6 cells; A2, B2, C2, A3, B3, C3 in sequence based on the cell being empty. The value being either
'in-active'??? if a day(s) is missed based on the time_difference calculation being 2 or greater
the h/m time stamp.
From what you are saying each of these 6 cells corresponds to 1 day. This means the app is only ever run one time in a day? If so there is no protection if its run a second or subsequent time in the same day, the app will write a new entry in the next empty cell and there is no way you'd know.
Therefore you might want to also use the last_update value in this part of the code to determine if/how the cell should be updated. You'll have to decide how this should be handled.
Ignore the new h/m value
Overwrite the existing h/m value
Add the new h/m value from the stop watch to the last update h/m and overwrite the new value to the same cell.
(3) The next issue is once the 6 cells are filled, no more updates will be written since your conditional check is for an empty cell. Therefore you need to determine how you want to handle that situation;
Start overwriting from the starting cell A2 again. If you do that I'm not sure how you would know which cell was last updated.
Clear all cells the next run after cell C3 is updated so all cells are empty before writing to cell A2 again, this will allow you to always see what cell was last updated.
Given the static nature of your cell range you can just check the cell C3, ws['C3'].value, first. If it has a value then clear all cells before going onto the ws.iter_rows part of the code.
(4) The 'in-active' check will also need to change. As its currently written if the time difference is greater than a day (time_difference >= 2) then all 6 cells will be overwritten as that would be true before any check for the next empty cell. This should be incorporated into the same conditional check like the example below.
In the example below; if the current cell in the iteration is empty then based on the time difference either write 'in-active' or the h/m text. If time_difference greater or equal to 2 its value is reduced by 1 each cycle after writing the 'in-active' text so after each missed day is filled in the current day h/m value is then written in the next empty cell. The loop breaks out of the iteration at that point so no more cells are updated.
for row in ws.iter_rows(min_row=2, max_row=3, max_col=3):
for cell in row:
if cell.value is None:
if time_difference >= 2:
cell.value = 'in-active'
time_difference -= 1
else:
cell.value = f"{hours} h and {minutes} min"
break
The above code may change depending on the incorporation of other checks mentioned in other points.
You are looking for continue.

Tkinter stop similar tags from joining together

I'm working on a dynamic scrolling application that somewhat emulates a table format. Unfortunately, due to the speed of the data, the only method that is quick enough to do this is the text widget (Displaying real time stock data in a scrolling table in Python).
Ideally I would like to place a border of every around every "cell" to emulate column and row dividers in a table. However, it seems like similar tags join together if placed next to each other, meaning the border stretches out to the bottom of the text widget.
I have tried using 2 different tag identifiers to trick it into not joining the borders, but it still merges the borders/tags. However, if I change the colour or even the borderwidth to a different value, they no longer join up. Unfortunately, I need a uniform border width.
For a simple example, I have defined 3 tags, 2 have identical properties, and one has a slight difference (colour) to show the effect.
The first two lines use the 2 identical tags for each cell, and the border is fully merged.
The 4th line shows the correct border, but this uses the tag with a different property (colour). The functionality I desire is to have the exact same properties but with a border per cell:
import tkinter as tk
root = tk.Tk()
text = tk.Text(root)
text.pack()
# Define example tags, 2 with identical properties but different identifiers, one with a different property (colour)
text.tag_config("tag1", background="lightgray", borderwidth=1, relief="solid")
text.tag_config("tag2", background="lightgray", borderwidth=1, relief="solid")
text.tag_config("tag3", background="lightyellow", borderwidth=1, relief="solid")
# Insert 4 example lines
text.insert(tk.INSERT, "Line 1 Cell 1 Line 1 Cell 2 Line 1 Cell 3\n")
text.insert(tk.INSERT, "Line 2 Cell 1 Line 2 Cell 2 Line 2 Cell 3\n")
text.insert(tk.INSERT, "\n")
text.insert(tk.INSERT, "Line 4 Cell 1 Line 4 Cell 2 Line 4 Cell 3\n")
# Different tag identities with same properties "join up", so the borders are
text.tag_add("tag1", "1.0", "1.14")
text.tag_add("tag2", "1.14", "1.28")
text.tag_add("tag1", "1.28", "1.42")
# The tags also merge when on a new line, with the line above
text.tag_add("tag1", "2.0", "2.14")
text.tag_add("tag2", "2.14", "2.28")
text.tag_add("tag1", "2.28", "2.42")
# Line 4 has the correct borders, but only because colour for tag3 is different
text.tag_add("tag1", "4.0", "4.14")
text.tag_add("tag3", "4.14", "4.28")
text.tag_add("tag1", "4.28", "4.42")
root.mainloop()
Is there a way around this?
You cannot stop tags from being merged. That is fundamental to how tags work in tkinter.
That being said, there are ways to emulate columns. For example, you can give each column a unique tag ("col1", "col2", etc), and/or give a tag to the characters between each column to create a break between columns.
Note: when you call the insert method you can give pairs of text and tags so that you don't have to add the tags after the fact.
For example, the following function will insert three values at the end of the text widget, adding the tags for each column and column separator. It will insert a blank line if all of the parameters are blank to emulate what your example is doing. This code uses the tags "col0", "col1", "col2", and "sep" rather than your more generic tag names.
def insert_row(col0="", col1="", col2=""):
if col0 == "" and col1 == "" and col2 == "":
text.insert("end", "\n")
else:
text.insert("end",
col0, "col0",
"\t", "sep",
col1, "col1",
"\t", "sep",
col2, "col2",
"\n"
)
Here is an example of its use:
insert_row("Line 1 Cell 1", "Line 1 Cell 2", "Line 1 Cell 3")
insert_row("Line 2 Cell 1", "Line 2 Cell 2", "Line 2 Cell 3")
insert_row()
insert_row("Line 3 Cell 1", "Line 3 Cell 2", "Line 3 Cell 3")
In the following screenshot I changed the color of the middle row to make it stand out:

Delete a Row in gtk.grid (GTK3+ / Python) in upper populating direction

i want to dynamically populate a grid with rows and columns in python with GTK3+ Grid (http://lazka.github.io/pgi-docs/Gtk-3.0/classes/Grid.html#Gtk.Grid), it works fine but until i want to delete attached rows. In that situation it just deletes every second row. No idea why...
Generating new Rows in a Loop:
self.__builder.attach_next_to(cell[0],None,Gtk.PositionType.TOP,1,1) #Place a new Row
self.__builder includes the Grid, cell[0] a Label to fill the Column. This is how i try to deleting them:
self.__builder.remove_row(-self.__v_size_value)
i wondered, because it just works with negative sign, but just on every second row. Maybe because i populate them in the upper direction? (i tried it out that way, and its true, when populating down there "position" value must be positive) self.__v_size_value includes the Temporary Row-Amount.
This is how it look when Generating Rows, and trying to deleting them again:
First -> Generate Rows in a Loop with that Command above (Example: 6 Rows)
Second -> Try to delete the Rows with that Command
Row 6 / remove_row(-6) / Row 6 gets deleted
Row 5 / remove_row(-5) / Row 4 gets deleted ??? (Row 5 still remains)
Row 4 / remove_row(-4) / Row 2 gets deleted ??? (Row 3 still remains)
Row 3 / remove_row(-3) / nothing happens any more (Row 1 still remains)
I wrote a small script that shows this malfunction:
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
class MyWindow(Gtk.Window):
row = 1
def __init__(self):
Gtk.Window.__init__(self, title="Hello World")
self.box = Gtk.Box.new(Gtk.Orientation.VERTICAL,4)
self.add(self.box)
self.button = Gtk.Button(label="Insert Row")
self.button.connect("clicked", self.on_button_clicked)
self.box.pack_start(self.button, True, True, 0)
self.button2 = Gtk.Button(label="Remove Row")
self.button2.connect("clicked", self.on_button2_clicked)
self.box.pack_start(self.button2, True, True, 0)
self.grid = Gtk.Grid()
self.box.pack_start(self.grid, True, True, 0)
def on_button_clicked(self, widget):
print("Increasing Row" + str(self.row))
label = Gtk.Label(self.row)
self.grid.attach_next_to(label,None,Gtk.PositionType.TOP,1,1)
self.row = self.row + 1
self.grid.show_all()
def on_button2_clicked(self, widget):
self.row = self.row - 1
print ("Decrease Row #" + str(self.row))
self.grid.remove_row(-self.row)
self.grid.show_all()
win = MyWindow()
win.connect("delete-event", Gtk.main_quit)
win.show_all()
Gtk.main()
I have a solution, i asked Lazka and he was so kind to help me out.
As mentioned above, Grid use negative positions for attached columns on top. As it do so, removing of a row (for example -10) will cause the other rows to get a a new number up to by one. So the first row is for example -10 again.
Thanks Lazka for helping me out!

Align Columns to the right of the "table" in ListCtrl

My current setup is like this:
With the columns in the ListCtrl being created as so:
self.list = wx.ListCtrl(self, style=wx.LC_REPORT | wx.SUNKEN_BORDER | wx.LC_HRULES | wx.LC_VRULES)
self.list.Show(True)
col_rank = self.list.InsertColumn(0, "Rank")
col_name = self.list.InsertColumn(1, "Team Name")
col_country = self.list.InsertColumn(2, "Country")
col_pinned = self.list.InsertColumn(3, "Pinned")
However I want the Country and Pinned Columns to be aligned to the right of the window, not to the left as they currently are. I attempted to do this by setting the width of Team Name very wide but this make the application unable to be resized without breaking the view. Any help would be much appreciated.
There is no right alignment of columns. You can set column widths (possibly dynamically, i.e. update the width of the right most column to take up all the remaining space in wxEVT_SIZE handler) and you can align the text inside the column to the right using wxLIST_FORMAT_RIGHT with AppendColumn().

Categories