Pysimplegui: [Bug & Addition] Table Element Update - Additional parameters & Row Color problems

Created on 22 May 2019  路  14Comments  路  Source: PySimpleGUI/PySimpleGUI

Type of Issues (Enhancement, Error, Bug, Question)

Bug (I think)

Operating System

Windows 10 64-bit

Python version

3.7.0

PySimpleGUI Port and Version

3.29.0

Code or partial code causing the problem

I have seen a couple bugs regarding the Table element. If you use the "alternating_row_color" parameter (I have it set as light blue) and you have a button that updates the Table element using the Update() call, it will make every row that color instead of keeping it as alternating.

The other bug that I found is when you use both the "row_colors" and "alternating_row_color" parameters together. The "alternating_row_color" parameter makes all of the even rows (0, 2, 4, 6, etc) the color that you choose. Now, let's say you use the "row_colors" parameter to make the first row the color red, you would put "row_colors=[(0, 'red')]". If you run the program with these two parameters and then press the button the uses the Update() call on the Table element, it will cause the color you chose for the "row_colors" parameter to be used as the color for the "alternating_row_color" parameter (so will make all of the even rows red instead of the original color) and the color used in the "alternating_row_color" parameter will then color all of the odd rows instead of the even rows (so row 1, 3, 5, 7 etc). If you use the "row_colors" parameter to make any other row a different color and then press a button that uses the Update() call, it just does the same thing as the first bug I mentioned (turns every row the same color as the color chosen for the parameter).

Here is the example code that I used:

import PySimpleGUI as sg
from random import randint as rand

def table_example():
    data = [list(str(rand(0,50)) for i in range(5)) for i in range(10)]
    header_list = ['C1', 'C2', 'C3', 'C4', 'C5']
    layout = [[sg.Table(values=data,
                        headings=header_list,
                        auto_size_columns=True,
                        justification='center',
                        num_rows=min(len(data), 20),
                        alternating_row_color='lightblue',
                        row_colors=((0, 'red'), (2, 'yellow')),
                        display_row_numbers=True,
                        key='table')],
              [sg.Button('Randomize'), sg.Button('Close')]]

    window = sg.Window('table', layout)
    while True:
        event, values = window.Read()
        if event in ('Close', None): break
        if event is 'Randomize':
            window.Element('table').Update(values=[list(str(rand(0,50)) for i in range(5)) for i in range(10)],
                                           num_rows=min(len(data), 20))

table_example()

All the code does is randomly generate rows using the random package and uses that to create a table. When you press the "Randomize" button, it basically rerolls the numbers for each row (same code as the 'data' variable) to update the table with new numbers. If you comment out the "row_colors" parameter line, you should see the first bug I am talking about when you press the "Randomize" button. If you run the program as is and then press the "Randomize" button, you should see the second bug I am talking about. I hope I explained myself well enough and hope that you can see the same thing I am using my example.

Bug

Most helpful comment

BRILLIANT!

You even did the right thing in how you created the window.... you did not "re-use" the layout and instead created a brand new one every time. This is 100% the correct way of doing it.

I'm impressed. I would have never thought to do it that way!

It's this kind of stuff that I love about this community. I learn something new from you guys every day.

All 14 comments

Yea, the reason is pretty straightforward.

Here is the code UPDATE code for a table

        if values is not None:
            children = self.TKTreeview.get_children()
            for i in children:
                self.TKTreeview.detach(i)
                self.TKTreeview.delete(i)
            children = self.TKTreeview.get_children()
            # self.TKTreeview.delete(*self.TKTreeview.get_children())
            for i, value in enumerate(values):
                if self.DisplayRowNumbers:
                    value = [i+self.StartingRowNumber] + value
                id = self.TKTreeview.insert('', 'end', text=i, iid=i + 1, values=value, tag=i % 2)
            if self.AlternatingRowColor is not None:
                self.TKTreeview.tag_configure(1, background=self.AlternatingRowColor)
            self.Values = values
            self.SelectedRows = []
        if visible is False:
            self.TKTreeview.pack_forget()
        elif visible is True:
            self.TKTreeview.pack()
        if num_rows is not None:
            self.TKTreeview.config(height=num_rows)

And this is the code that is executed when a table element is created:

            elif element_type == ELEM_TYPE_TABLE:
                element = element       # type: Table
                frame = tk.Frame(tk_row_frame)

                height = element.NumRows
                if element.Justification == 'left':
                    anchor = tk.W
                elif element.Justification == 'right':
                    anchor = tk.E
                else:
                    anchor = tk.CENTER
                column_widths = {}
                for row in element.Values:
                    for i, col in enumerate(row):
                        col_width = min(len(str(col)), element.MaxColumnWidth)
                        try:
                            if col_width > column_widths[i]:
                                column_widths[i] = col_width
                        except:
                            column_widths[i] = col_width
                if element.ColumnsToDisplay is None:
                    displaycolumns = element.ColumnHeadings if element.ColumnHeadings is not None else element.Values[0]
                else:
                    displaycolumns = []
                    for i, should_display in enumerate(element.ColumnsToDisplay):
                        if should_display:
                            displaycolumns.append(element.ColumnHeadings[i])
                column_headings = element.ColumnHeadings
                if element.DisplayRowNumbers:  # if display row number, tack on the numbers to front of columns
                    displaycolumns = [element.RowHeaderText, ] + displaycolumns
                    column_headings = [element.RowHeaderText, ] + element.ColumnHeadings
                element.TKTreeview = element.Widget = ttk.Treeview(frame, columns=column_headings,
                                                  displaycolumns=displaycolumns, show='headings', height=height,
                                                  selectmode=element.SelectMode,)
                treeview = element.TKTreeview
                if element.DisplayRowNumbers:
                    treeview.heading(element.RowHeaderText, text=element.RowHeaderText)  # make a dummy heading
                    treeview.column(element.RowHeaderText, width=50, anchor=anchor)

                headings = element.ColumnHeadings if element.ColumnHeadings is not None else element.Values[0]
                for i, heading in enumerate(headings):
                    treeview.heading(heading, text=heading)
                    if element.AutoSizeColumns:
                        width = max(column_widths[i], len(heading))
                    else:
                        try:
                            width = element.ColumnWidths[i]
                        except:
                            width = element.DefaultColumnWidth
                    treeview.column(heading, width=width * CharWidthInPixels(), anchor=anchor)

                # Insert values into the tree
                for i, value in enumerate(element.Values):
                    if element.DisplayRowNumbers:
                        value = [i+element.StartingRowNumber] + value
                    id = treeview.insert('', 'end', text=value, iid=i + 1, values=value, tag=i)
                if element.AlternatingRowColor is not None:         # alternating colors
                    for row in range(0, len(element.Values), 2):
                        treeview.tag_configure(row, background=element.AlternatingRowColor)
                if element.RowColors is not None:                   # individual row colors
                    for row_def in element.RowColors:
                        if len(row_def) == 2:                       # only background is specified
                            treeview.tag_configure(row_def[0], background=row_def[1])
                        else:
                            treeview.tag_configure(row_def[0], background=row_def[2], foreground=row_def[1])

                if element.BackgroundColor is not None and element.BackgroundColor != COLOR_SYSTEM_DEFAULT:
                    ttk.Style().configure("Treeview", background=element.BackgroundColor,
                                          fieldbackground=element.BackgroundColor)
                if element.TextColor is not None and element.TextColor != COLOR_SYSTEM_DEFAULT:
                    ttk.Style().configure("Treeview", foreground=element.TextColor)
                if element.RowHeight is not None:
                    ttk.Style().configure("Treeview", rowheight=element.RowHeight)
                ttk.Style().configure("Treeview", font=font)
                # scrollable_frame.pack(side=tk.LEFT,  padx=elementpad[0], pady=elementpad[1], expand=True, fill='both')
                treeview.bind("<<TreeviewSelect>>", element.treeview_selected)
                if element.BindReturnKey:
                    treeview.bind('<Return>', element.treeview_double_click)
                    treeview.bind('<Double-Button-1>', element.treeview_double_click)

                if not element.HideVerticalScroll:
                    scrollbar = tk.Scrollbar(frame)
                    scrollbar.pack(side=tk.RIGHT, fill='y')
                    scrollbar.config(command=treeview.yview)
                    treeview.configure(yscrollcommand=scrollbar.set)

                if not element.VerticalScrollOnly:
                    hscrollbar = tk.Scrollbar(frame, orient=tk.HORIZONTAL)
                    hscrollbar.pack(side=tk.BOTTOM, fill='x')
                    hscrollbar.config(command=treeview.xview)
                    treeview.configure(xscrollcommand=hscrollbar.set)


                element.TKTreeview.pack(side=tk.LEFT, expand=True, padx=0, pady=0, fill='both')
                if element.Visible is False:
                    element.TKTreeview.pack_forget()
                frame.pack(side=tk.LEFT, expand=True, padx=0, pady=0)
                if element.Tooltip is not None:
                    element.TooltipObject = ToolTip(element.TKTreeview, text=element.Tooltip,
                                                    timeout=DEFAULT_TOOLTIP_TIME)
                if element.RightClickMenu or toplevel_form.RightClickMenu:
                    menu = element.RightClickMenu or toplevel_form.RightClickMenu
                    top_menu = tk.Menu(toplevel_form.TKroot, tearoff=False)
                    AddMenuItem(top_menu, menu[1], element)
                    element.TKRightClickMenu = top_menu
                    element.TKTreeview.bind('<Button-3>', element.RightClickMenuCallback)

As you as easily see, a fair amount of code needs to be added to the Update code to deal with row colors, etc.

I tried to make the Demo_Table_Element.py working in a browser with just changing

line 5    import PySimpleGUIWeb as sg

but got the error:

Traceback (most recent call last):
File "/home/giodegas/Documenti/dev/TaLTaC4/t4/scouting/table_test.py", line 47, in <module> window.FindElement('_table_').Update(values=data)
 File "/home/giodegas/.taltac/lib/python3.7/site-packages/PySimpleGUIWeb/PySimpleGUIWeb.py", line 2583, in Update
 children = self.TKTreeview.get_children()
 AttributeError: 'NoneType' object has no attribute 'get_children'

The Web port is the newest and may not always have matching parameters or work exactly the same way. The Table Element is one of the most complex of the elements. I believe that was the element I was working on when I took a break and started working on the documentation.

I'll have a look. I try to test Demo code against the other ports. If they work, I leave the other imports comments out at the top.

Yea, the Web port still has a way to go on Tables. The Update method is what is crashing. It's attempting to call old tkinter code as I haven't written the Table.Update method yet. The Web Readme file chronicles the Table Element development.

I'll add an action item to put asserts in the code for these user callable functions/methods that are not yet completed.

I can do this as part of the documentation work that's going on right now. I'm on the tkinter port still, but will be doing the same docstring stuff for all of the ports. Hmmmm... that may also be a good place to indicate methods that are not yet completed. Between those 2 changes it should add some clarity to what's implemented and what's not.

I eventually made it to work in a web browser, regenerating the window layout when data is changed.
Check it out at this gist: PySimpleGuiWeb_Table_test.py

BRILLIANT!

You even did the right thing in how you created the window.... you did not "re-use" the layout and instead created a brand new one every time. This is 100% the correct way of doing it.

I'm impressed. I would have never thought to do it that way!

It's this kind of stuff that I love about this community. I learn something new from you guys every day.

Well, thank you, but it is far from to be perfect. When closing the web page, the None event is not delivered any more.. So I have to stop the app manually.

Hmmmmmm...... oh.... I need to give you a way to close a window but not exit Remi. Like the Popups. I forget what I did to make Popups work.

Hi,

just a short question. I also would like to set the row colors in the Table.Update function.

Can you tell me roughly what priority this feature has for you at the moment? I am trying to decide whether I should wait or start looking for a solution for my specific use case.

I want to make sure that everyone on this thread is NOT running Python 3.7.4. That the form filled in with 3.7.0 is correct... you really are running 3.7.0.

@till314 - I'm not a fan of bugs. I'll get on this one soon since it's in the tkinter version which tends to get the highest priority as it's the '"done" one of the 4 ports.

Attention table-color-caring-people

You can now change the alternating row color and the individual row colors using the Table.Update function for PySimpleGUI (the tkinter version).

I don't have the bandwidth to document any further than the doc strings the I just completed. You may have to play with it to figure some sh*t out, but the code is there at least for you to try.

You'll want to check out the Demo Program named: Demo_Table_Element.py because it calls Update a couple of times to show how to update the data as well as the f*cking colors.

Row colors come in 2 formats. The text color is optional. Here is a single entry from a list of row colors:
(row#, [foreground/text color], background color)

This example shows 2 rows being specifically colored to have a red background. Row 12 also has white text.
rows_colors = ((12, 'white', 'red'), (13, 'red'))
If no background or text colors specified when the table was created, and the values are updated, the default is black text on a white background.

I believe this is the feature everyone is asking for. Please test it out and lemme know if this will fit your use cases. It should cover just about any combination you desire. It may take a little cleverness, I dunno.

Good luck.

To use, once again, download PySimpleGUI.py from GitHub and place in the folder with your application (in with whatever .py file that imports PySimpleGUI)

Thanks. Works fine here.

I found interesting update behavior with version "4.1.0.16 Unreleased - Anniversary Edition"

import PySimpleGUI as sg
data = [[str(x * y) for x in range(5)] for y in range(100)]
header = ["one", "two", "three", "four", "five"]
Table1 = sg.Table(
    values=data,
    headings=header,
    text_color="blue",
    row_height=20,
    background_color="green",,
    key="_table1_",)

Table2 = sg.Table(
    values=data,
    headings=header,
    text_color="lightgrey",
    row_height=20,
    background_color="blue",
    alternating_row_color="red",
    key="_table2_",)

layout = [[sg.Button("Exit", key="Exit", visible=True)],[sg.Button("Update1", size=(18,1), visible=True),sg.Button("Update2"  ,size=(18,1), visible=True)], [Table1,Table2]]


window = (
    sg.Window("Table Color", auto_size_buttons=False, default_element_size=(20, 5),    border_depth=1,  resizable=True, ).Layout(layout).Finalize())

while True:
    event, values = window.Read()
    if event is None or event == "Exit":
        break
    elif event is None or event == "Update1":
        window.FindElement("_table1_").Update(data)
    elif event is None or event == "Update2":
        window.FindElement("_table2_").Update(data)

That the different background-colors and text-colors are not respected before using Update is due to the style system in tk, could be changed to the same code used in Update, but not that interesting I think. But that text-color and background-color of the first row aren't updated is due to the fact that the id in tree_ids starts with one not with zero.

if element.BackgroundColor is not None and element.BackgroundColor != COLOR_SYSTEM_DEFAULT:
            for row in range(0, len(element.Values)):
                treeview.tag_configure(row, background=element.BackgroundColor)
if element.TextColor is not None and element.TextColor != COLOR_SYSTEM_DEFAULT:
            for row in range(0, len(element.Values)):
                treeview.tag_configure(row, foreground=element.TextColor)
Was this page helpful?
0 / 5 - 0 ratings