Akascape / CTkTable

Customtkinter Table widget (extension/add-on)
MIT License
281 stars 16 forks source link

i wan't to add a row for a tag #5

Closed H1B0B0 closed 1 year ago

H1B0B0 commented 1 year ago

Hello I have a problem with my code I try to create a row in the table for each tag read with per column each time the corresponding info. but when I add a new line this one appears and then triggers an error which erases the entire table. The error:

Exception in Tkinter callback
Traceback (most recent call last):
  File "/usr/lib64/python3.11/tkinter/__init__.py", line 1948, in __call__
    return self.func(*args)
           ^^^^^^^^^^^^^^^^
  File "/home/#######/.local/lib/python3.11/site-packages/customtkinter/windows/widgets/core_widget_classes/ctk_base_class.py", line 188, in _update_dimensions_event
    self._draw(no_color_updates=True)  # faster drawing without color changes
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/#######/.local/lib/python3.11/site-packages/customtkinter/windows/widgets/ctk_button.py", line 187, in _draw
    self._draw_engine.draw_background_corners(self._apply_widget_scaling(self._current_width),
  File "/home/#######/.local/lib/python3.11/site-packages/customtkinter/windows/widgets/core_rendering/draw_engine.py", line 72, in draw_background_corners
    if not self._canvas.find_withtag("background_corner_top_left"):
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib64/python3.11/tkinter/__init__.py", line 2922, in find_withtag
    return self.find('withtag', tagOrId)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib64/python3.11/tkinter/__init__.py", line 2889, in find
    self.tk.call((self._w, 'find') + args)) or ()
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_tkinter.TclError: invalid command name ".!ctktable.!ctkbutton4427.!ctkcanvas"

and my code is that :

self.table = CTkTable(master=self, column=8, values=self.headers)
        self.table.grid(row=5, pady=5, padx=5)

I create the Table and after that i give the table object to my fonction. in my fonction i make that :

def start_reading(self, table):
        try:
            response = requests.get(self.anthubinfo, timeout=3)
        except:
            msg = CTkMessagebox(title="Warning Message!", message="check the connection with the reader", icon="warning", option_1="Cancel", option_2="Retry")
            if msg.get() == "Retry":
                self.start_reading(table)
        self.check_response(response, print_body=False)
        for event in response.iter_lines():
            eventdict = json.loads(event)
            if self.antennahub:
                if eventdict["status"] == "Enabled":
                    pass
                else:
                    self.check_response(requests.post(self.enableanthub, timeout=3))
                    self.check_response(requests.post(self.reboot, timeout=3))
                    sleep(5)
            else :
                if eventdict["status"] == "Disabled":
                    pass
                else:
                    self.check_response(requests.post(self.disableanthub, timeout=3))
                    self.check_response(requests.post(self.reboot, timeout=3))
                    sleep(5)

        if not self.running:
            if self.ping_reader(120):
                self.running = True
                self.send_preset()
                self.data = []
                response = requests.post(url=self.urlstart)
                if response.status_code == 204:
                    print("in reading")
                    self.future_to_data = self.executor.submit(self.process_stream)
                else:
                    print(response)  
                while True:
                    try:
                        info = ""
                        info = self.q.get_nowait()
                        if "inventoryStatusEvent" in info and info["inventoryStatusEvent"]["inventoryStatus"] == "idle":
                            break
                        else:
                            value = []
                            value = [
                                    info["timestamp"],
                                    info["tagInventoryEvent"]["epcHex"],
                                    info["tagInventoryEvent"]["tidHex"],
                                    info["tagInventoryEvent"]["antennaPort"],
                                    info["tagInventoryEvent"]["peakRssiCdbm"],
                                    info["tagInventoryEvent"]["frequency"],
                                    info["tagInventoryEvent"]["transmitPowerCdbm"],
                                    info["tagInventoryEvent"]["lastSeenTime"],
                                    info["tagInventoryEvent"]["phaseAngle"]
                            ]
                            table.add_row(value)        
                    except:
                        pass
                #TODO make a fonction for the error 
H1B0B0 commented 1 year ago

I found a solution ! In fact, when you create a new line it deletes all the old lines and that's what was causing the problem I was having. now I make sure to create only 1 single line each time, but the code slows down when the tags pile up so I have to optimize it a bit to get better performance.

def add_row(self, values, index=None):
        """ add a new row """
        if index is None:
            index = len(self.values)
        self.values.insert(index, values)
        self.rows += 1

        for i in range(self.columns):
            if self.phase == "rows":
                if index % 2 == 0:
                    fg = self.fg_color
                else:
                    fg = self.fg_color2
            else:
                if i % 2 == 0:
                    fg = self.fg_color
                else:
                    fg = self.fg_color2

            if self.header_color:
                if index == 0:
                    fg = self.header_color

            corner_radius = self.corner
            if index == 0 and i == 0:
                corners = ["", fg, fg, fg]
            elif index == self.rows - 1 and i == self.columns - 1:
                corners = [fg, fg, "", fg]
            elif index == self.rows - 1 and i == 0:
                corners = [fg, fg, fg, ""]
            elif index == 0 and i == self.columns - 1:
                corners = [fg, "", fg, fg]
            else:
                corners = [fg, fg, fg, fg]
                corner_radius = 0

            value = values[i] if i < len(values) else " "

            self.data = {"row": index, "column": i, "value": value}
            self.frame[index, i] = customtkinter.CTkButton(
                self,
                background_corner_colors=corners,
                corner_radius=corner_radius,
                fg_color=fg,
                hover=self.hover,
                text=value,
                command=(lambda e=self.data: self.command(e)) if self.command else None,
            )
            self.frame[index, i].grid(
                column=i, row=index, padx=self.padx, pady=self.pady, sticky="nsew"
            )

        self.rowconfigure(index, weight=1)
        self.update_idletasks() 
Akascape commented 1 year ago

@H1B0B0 I don't know what to implement, this library is not that performance friendly because of so many ctkbuttons. I will try to implement a new method for the cells.

H1B0B0 commented 1 year ago

Hello thank for the response. Actually I have this code it's a little bit more fast but it's not full optimised.

import customtkinter

class CTkTable(customtkinter.CTkFrame):
    def __init__(
        self,
        master: any = None,
        row: int = None,
        column: int = None,
        padx: int = 1, 
        pady: int = 0,
        values: list = [[None]],
        colors: list = [None, None],
        color_phase: str = "rows",
        header_color: str = None,
        corner_radius: int = 25,
        hover: bool = False,
        command = None,
        **kwargs):

        super().__init__(master, fg_color="transparent")

        self.master = master
        self.rows = row if row else len(values)
        self.columns = column if column else len(values[0])
        self.padx = padx
        self.pady = pady
        self.command = command
        self.values = values
        self.colors = colors
        self.header_color = header_color
        self.phase = color_phase
        self.corner = corner_radius
        self.hover = hover
        self.fg_color = customtkinter.ThemeManager.theme["CTkFrame"]["fg_color"] if not self.colors[0] else self.colors[0]
        self.fg_color2 = customtkinter.ThemeManager.theme["CTkFrame"]["top_fg_color"] if not self.colors[1] else self.colors[1]

        if self.colors[0] is None and self.colors[1] is None:
            if self.fg_color==self.master.cget("fg_color"):
                self.fg_color = customtkinter.ThemeManager.theme["CTk"]["fg_color"]
            if self.fg_color2==self.master.cget("fg_color"):
                self.fg_color2 = customtkinter.ThemeManager.theme["CTk"]["fg_color"]

        self.widgets = []

        self.draw_table(**kwargs)

    def draw_table(self, **kwargs):
        """ draw the table """
        for i in range(len(self.values)):
            row_widgets = []
            for j in range(len(self.values[i])):
                if self.phase == "rows":
                    if i % 2 == 0:
                        fg = self.fg_color
                    else:
                        fg = self.fg_color2
                else:
                    if j % 2 == 0:
                        fg = self.fg_color
                    else:
                        fg = self.fg_color2

                if self.header_color:
                    if i == 0:
                        fg = self.header_color

                corner_radius = self.corner
                if i == 0 and j == 0:
                    corners = ["", fg, fg, fg]
                elif i == len(self.values) - 1 and j == len(self.values[i]) - 1:
                    corners = [fg, fg, "", fg]
                elif i == len(self.values) - 1 and j == 0:
                    corners = [fg, fg, fg, ""]
                elif i == 0 and j == len(self.values[i]) - 1:
                    corners = [fg, "", fg, fg]
                else:
                    corners = [fg, fg, fg, fg]
                    corner_radius = 0

                value = self.values[i][j] if i < len(self.values) and j < len(self.values[i]) else " "

                widget = customtkinter.CTkLabel(
                    self,
                    corner_radius=corner_radius,
                    fg_color=fg,
                    text=value,
                    **kwargs
                )
                widget.grid(column=j, row=i, padx=self.padx, pady=self.pady, sticky="nsew")
                row_widgets.append(widget)

            self.widgets.append(row_widgets)

        self.grid_rowconfigure(0, weight=1)
        self.grid_columnconfigure(0, weight=1)

    def edit_row(self, row, **kwargs):
        """Edit all parameters of a single row"""
        row_widgets = self.widgets[row]

        for i, widget in enumerate(row_widgets):
            widget.configure(**kwargs)

    def edit_column(self, column, **kwargs):
        """Edit all parameters of a single column"""
        for i in range(self.rows):
            widget = self.widgets[i][column]
            widget.configure(**kwargs)

    def update_values(self, values, **kwargs):
        """ update all values at once """
        self.values = values
        for i in range(len(self.values)):
            for j in range(len(self.values[i])):
                self.widgets[i][j].configure(text=self.values[i][j], **kwargs)

    def add_row(self, values, index=None):
        """ add a new row """
        if index is None:
            index = len(self.values)
        self.values.insert(index, values)
        self.rows += 1

        row_widgets = []

        for i in range(self.columns):
            if self.phase == "rows":
                if index % 2 == 0:
                    fg = self.fg_color
                else:
                    fg = self.fg_color2
            else:
                if i % 2 == 0:
                    fg = self.fg_color
                else:
                    fg = self.fg_color2

            if self.header_color:
                if index == 0:
                    fg = self.header_color

            corner_radius = self.corner
            if index == 0 and i == 0:
                corners = ["", fg, fg, fg]
            elif index == self.rows - 1 and i == self.columns - 1:
                corners = [fg, fg, "", fg]
            elif index == self.rows - 1 and i == 0:
                corners = [fg, fg, fg, ""]
            elif index == 0 and i == self.columns - 1:
                corners = [fg, "", fg, fg]
            else:
                corners = [fg, fg, fg, fg]
                corner_radius = 0

            value = values[i] if i < len(values) else " "

            data = {"row": index, "column": i, "value": value}
            widget = customtkinter.CTkLabel(
                self,
                corner_radius=corner_radius,
                fg_color=fg,
                text=value,
            )
            widget.grid(column=i, row=index, padx=self.padx, pady=self.pady, sticky="nsew")
            row_widgets.append(widget)

        self.widgets.insert(index, row_widgets)
        self.rowconfigure(index, weight=1)

    def add_column(self, values, index=None):
        """Add a new column"""
        if index is None:
            index = len(self.values[0])

        for row_values, row_widgets in zip(self.values, self.widgets):
            row_values.insert(index, None)

            if self.phase == "columns":
                if index % 2 == 0:
                    fg = self.fg_color
                else:
                    fg = self.fg_color2
            else:
                if index % 2 == 0:
                    fg = self.fg_color
                else:
                    fg = self.fg_color2

            if self.header_color:
                if index == 0:
                    fg = self.header_color

            corner_radius = self.corner
            if index == 0 and row_widgets[0] == self.widgets[0][0]:
                corners = ["", fg, fg, fg]
            elif index == self.columns and row_widgets[-1] == self.widgets[-1][0]:
                corners = [fg, fg, "", fg]
            elif index == self.columns and row_widgets[0] == self.widgets[-1][0]:
                corners = [fg, fg, fg, ""]
            elif index == 0 and row_widgets[-1] == self.widgets[0][-1]:
                corners = [fg, "", fg, fg]
            else:
                corners = [fg, fg, fg, fg]
                corner_radius = 0

            value = values[row_values.index(None)] if None in row_values else " "

            data = {"row": row_values.index(None), "column": index, "value": value}
            widget = customtkinter.CTkLabel(
                self,
                corner_radius=corner_radius,
                fg_color=fg,
                text=value,
            )
            widget.grid(column=index, row=row_widgets[0].grid_info()["row"], padx=self.padx, pady=self.pady, sticky="nsew")
            row_widgets.insert(index, widget)

        self.columns += 1
        self.columnconfigure(index, weight=1)

    def delete_row(self, index):
        """ delete a row """
        if index < len(self.values):
            del self.values[index]
            row_widgets = self.widgets.pop(index)
            for widget in row_widgets:
                widget.destroy()
            self.rows -= 1

    def delete_column(self, index):
        """Delete a column"""
        if index < len(self.values[0]):
            for row_values, row_widgets in zip(self.values, self.widgets):
                del row_values[index]
                widget = row_widgets.pop(index)
                widget.destroy()
            self.columns -= 1

    def insert(self, row, column, value, **kwargs):
        widget = self.widgets[row][column]
        widget.configure(text=value, **kwargs)

    def delete(self, row, column, **kwargs):
        """ delete a value from a specific block [row, column] """
        self.widgets[row][column].configure(text="", **kwargs)

    def get(self):
        return self.values

    def get_value(self, row, column):
        return self.widgets[row][column].cget("text")

    def configure(self, **kwargs):
        """ configure table widget attributes"""

        if "colors" in kwargs:
            self.colors = kwargs.pop("colors")
            self.fg_color = self.colors[0]
            self.fg_color2 = self.colors[1]
        if "header_color" in kwargs:
            self.header_color = kwargs.pop("header_color")
        if "rows" in kwargs:
            self.rows = kwargs.pop("rows")
        if "columns" in kwargs:
            self.columns = kwargs.pop("columns")
        if "values" in kwargs:
            self.values = kwargs.pop("values")
        if "padx" in kwargs:
            self.padx = kwargs.pop("padx")
        if "pady" in kwargs:
            self.pady = kwargs.pop("pady")

        self.update_values(self.values, **kwargs)
H1B0B0 commented 1 year ago

Currently I am working on a method to create all cells in a row at once and not one by one.

Akascape commented 1 year ago

@H1B0B0 Replacing the cells with CTkLabel will remove many functionalities.

H1B0B0 commented 1 year ago

I know but I have no choice since I only need a single table capable of displaying 8 columns and an ultra large number of rows.

H1B0B0 commented 1 year ago

Here you can the my and why I have some problemes with table optimisation. The Timestamp, lastseentime, rssi and phase angle and for finish counter are dynamics. image

Akascape commented 1 year ago

@H1B0B0 Thats a lot of data in that table. Try something like tksheet if you want optimisation: https://github.com/ragardner/tksheet

H1B0B0 commented 1 year ago

I have fund a solution but thank for your help