ragardner / tksheet

Python tkinter table widget for displaying tabular data
https://pypi.org/project/tksheet/
MIT License
408 stars 50 forks source link

Added UI edit events, bugfixes #171

Closed CalJaDav closed 1 year ago

CalJaDav commented 1 year ago

Hello! I have added events for external listeners so that programs know when changes are being made to the sheet via the UI. This can be useful in cases where parts of the sheet or program need to be updated dynamically based on what is on the sheet without comparing the sheet data every mainloop iteration. Each edit to the table should produce one event that occurs after tksheet has made all the internal changes to its data.

Note that edits to the sheet data through code should produce no event, this is because the code should know if it is making changes and it is easy to create infinite loops of editing events if the code changes to sheet data produce events which spawn more changes.

This is somewhat a response to issue #170. I think this is a better way for users to handle calculated cells going forward as the actual logic sits outside the widget itself.

Here is a small code example where column C is the sum of A and B:

from tksheet import Sheet, float_formatter
import tkinter as tk

class demo(tk.Tk):
    def __init__(self):
        tk.Tk.__init__(self)
        self.bind('<<SheetDataChangeEvent>>', self.on_data_change)
        self.i = 0
        self.grid_columnconfigure(0, weight = 1)
        self.grid_rowconfigure(0, weight = 1)
        self.frame = tk.Frame(self)
        self.frame.grid_columnconfigure(0, weight = 1)
        self.frame.grid_rowconfigure(0, weight = 1)
        self.sheet = Sheet(self.frame, total_rows=2, total_columns=2
                           )
        self.sheet.enable_bindings()
        self.frame.grid(row = 0, column = 0, sticky = "nswe")
        self.sheet.grid(row = 0, column = 0, sticky = "nswe")
        self.sheet.align_columns(0, "center")
        self.sheet.align_columns(1, "left")
        self.sheet.format_sheet(formatter_options = float_formatter(decimals = 7))
        self.sheet.set_sheet_data([[None, None, None],[1,1, None]])
        self.sheet.readonly_columns(2)
        self.perform_calcs()

    def on_data_change(self, event):
        self.i += 1
        self.perform_calcs()

    def perform_calcs(self):
        for r, row in enumerate(self.sheet.get_sheet_data()):
            try: 
                row[2] = row[0] + row[1]
            except:
                row[2] = None
            print(row)
            self.sheet.set_row_data(r, row)
        self.sheet.redraw()

app = demo()
app.mainloop()
ragardner commented 1 year ago

Hey,

Thanks for the bugfixes and suggestion. I agree and think that it is currently awkward to monitor every sheet modified event, having to create extra_bindings() for everything

I am thinking I might add some information on what sort of modification as well

ragardner commented 1 year ago

I may or may not plug this into the existing extra_bindings(), with "sheet_modified" or something, I am undecided at the moment so don't write the docs for it

ragardner commented 1 year ago

I decided that I will add both something for extra_bindings and also I might change which object the event is emitted from so that you can do something like if self.my_sheet == event.widget: in a later release so I won't advertise this just yet but will do a release with these changes for the moment anyway

Thanks again!

CalJaDav commented 1 year ago

Agreed. tkinter does not handle binding data to custom virtual events very well. There are ways around it, but it is fairly hacky, so we will need to provide some custom functions for the user to use to unpack the event data. I'll attach the way I handle passing data through custom virtual events in my apps.

I think it would be useful pass data the following inside our event.

def bind_event_with_data(widget, sequence, func, add = None):
    def _substitute(*args):
        e = lambda: None 
        e.data = eval(args[0])
        e.widget = widget
        return (e,)

    funcid = widget._register(func, _substitute, needcleanup=1)
    cmd = '{0}if {{"[{1} %d]" == "break"}} break\n'.format('+' if add else '', funcid)
    widget.tk.call('bind', widget._w, sequence, cmd)

if __name__ == '__main__':
    class demo(tk.Tk):

        def __init__(self):
            super().__init__()
            self.button = tk.Button(self, text='Click me', command = self.on_click)
            self.button.pack()
            bind_event_with_data(self, '<<CustomEventWithData>>', self.on_event)

        def on_click(self):
            self.event_generate('<<CustomEventWithData>>', data={'var1': '1', 'var2': '2'}) # Note that only strings can be passed in this way

        def on_event(self, event):
            print(f"var1: {event.data['var1']}, var2: {event.data['var1']}")

    demo().mainloop()
ragardner commented 1 year ago

Thanks for the additional info and code, I will get back to you about it soon

ragardner commented 1 year ago

What I've done in 6.0.3 is put your function inside Sheet(), I haven't attached any data to it yet except an empty dict but you use it like so:

self.sheet.bind_event("<<SheetModified>>", self.on_event)

It'll send the sheet object and the data dict

I think this is okay but let me know if not

juddiny03 commented 1 year ago

Cool, lo he estado usando y me esta funcionando esa solucion...

juddiny03 commented 1 year ago

Lo que hice fue 6.0.3poner su función dentro de Sheet(), todavía no le he adjuntado ningún dato, excepto un dictado vacío, pero lo usa así:

self.sheet.bind_event("<<SheetModified>>", self.on_event)

Enviará el objeto de hoja y el dictado de datos.

Creo que esto está bien, pero avísame si no

Hola, me funciona hasta el momento, es una buena opcio que se pueda hacer calculos.