Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Enhancement] Demo showing "Edit a cell" #4972

Open
2 of 7 tasks
PySimpleGUI opened this issue Nov 20, 2021 · 21 comments
Open
2 of 7 tasks

[Enhancement] Demo showing "Edit a cell" #4972

PySimpleGUI opened this issue Nov 20, 2021 · 21 comments
Labels
Demo Programs Problem is in the demo programs (or any other non-core code) enhancement New feature or request

Comments

@PySimpleGUI
Copy link
Owner

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

Enhancement


Environment

Operating System

Windows version 10

PySimpleGUI Port (tkinter, Qt, Wx, Web)

tkinter


Versions

Python version (sg.sys.version)

3.11.0a2 (tags/v3.11.0a2:e2b4e4b, Nov 5 2021, 20:00:05) [MSC v.1929 64 bit (AMD64)]

PySimpleGUI Version (sg.__version__)

4.55.1.4

GUI Version (tkinter (sg.tclversion_detailed), PySide2, WxPython, Remi)

8.6.11


Your Experience In Months or Years (optional)

43 Years Python programming experience
4 Years Programming experience overall
No Have used another Python GUI Framework? (tkinter, Qt, etc) (yes/no is fine)
This project and the users!


Troubleshooting

These items may solve your problem. Please check those you've done by changing - [ ] to - [X]

  • Searched main docs for your problem www.PySimpleGUI.org
  • Looked for Demo Programs that are similar to your goal Demos.PySimpleGUI.org
  • If not tkinter - looked for Demo Programs for specific port
  • For non tkinter - Looked at readme for your specific port if not PySimpleGUI (Qt, WX, Remi)
  • Run your program outside of your debugger (from a command line)
  • Searched through Issues (open and closed) to see if already reported Issues.PySimpleGUI.org
  • Tried using the PySimpleGUI.py file on GitHub. Your problem may have already been fixed but not released

Detailed Description

I'm working on the Table lesson for the new course.

It would be nice to be able to point to a demo that does an "edit" operation.

In Qt it's possible to do that.

Jason wrote the code that does the detect of click events for cells and headers. I'm hoping that maybe Jason can work his magic again and help make this example better. My focus really needs to be on the course more than working on this enhancement. I tend to add to elements as I record the lessons. This would be a fantastic one to have.

So far I have a little input that pops up when you click a cell. The problem is that it pops up where the mouse clicked. It would be ideal if it popped up exactly where that cell is located so that it looks like the cell is being typed in.

To use the example, click on a cell. When the input opens, type the new value. Press return to save it or Escape to cancel.

It's pretty close, kind of, but could be a lot better.

Code To Duplicate

import PySimpleGUI as sg
import random, string

# ------ Some functions to help generate data for the table ------
def word():
    return ''.join(random.choice(string.ascii_lowercase) for i in range(10))
def number(max_val=1000):
    return random.randint(0, max_val)

def make_table(num_rows, num_cols):
    data = [[j for j in range(num_cols)] for i in range(num_rows)]
    data[0] = [word() for _ in range(num_cols)]
    for i in range(0, num_rows):
        data[i] = [i, word(), *[number() for i in range(num_cols - 1)]]
    return data

def main_example1():
    def edit_cell(location):
        layout = [[sg.In(s=5, k='-IN-')],
                  [sg.B('Ok', visible=False, bind_return_key=True)]]

        window = sg.Window('', layout, no_titlebar=True, location=location, margins=(0,0), element_padding=(0,0), return_keyboard_events=True, keep_on_top=True, modal=True)
        while True:
            event, values = window.read()
            print(event, values)
            if event == 'Ok':
                window.close()
                return values['-IN-']
            elif event.startswith('Escape'):
                window.close()
                return None

    # ------ Make the Table Data ------
    # sg.Print('Creating table...')
    data = make_table(num_rows=10_000, num_cols=6)
    # headings = [str(data[0][x])+'     ..' for x in range(len(data[0]))]
    headings = [f'Col {col}' for col in range(len(data[0]))]
    # sg.Print('Done creating table.  Creating GUI...')
    layout = [[sg.Table(values=data, headings=headings, max_col_width=25,
                        auto_size_columns=True,
                        # display_row_numbers=True,
                        justification='right',
                        num_rows=20,
                        alternating_row_color=sg.theme_button_color()[1],
                        key='-TABLE-',
                        # selected_row_colors='red on yellow',
                        # enable_events=True,
                        # select_mode=sg.TABLE_SELECT_MODE_BROWSE,
                        expand_x=True,
                        expand_y=True,
                        enable_click_events=True,  # Comment out to not enable header and other clicks
                        )],
              [sg.Button('Read'), sg.Button('Double'), sg.Button('Change Colors')],
              [sg.Text('Cell clicked:'), sg.T(k='-CLICKED-')]]


    window = sg.Window('Table Element - Example 1', layout, resizable=True)

    while True:
        event, values = window.read()
        if event in (sg.WIN_CLOSED, 'Exit'):
            break
        if isinstance(event, tuple):
            cell = event[2]
            window['-CLICKED-'].update(cell)
            new_value = edit_cell(location=window.mouse_location())
            if new_value is not None:
                data[cell[0]][cell[1]] = new_value
                window['-TABLE-'].update(data)

    window.close()

main_example1()

Screenshot, Sketch, or Drawing


@PySimpleGUI PySimpleGUI added Demo Programs Problem is in the demo programs (or any other non-core code) enhancement New feature or request labels Nov 20, 2021
@jason990420
Copy link
Collaborator

jason990420 commented Nov 21, 2021

It's much difficult for me to build it by pure PySimpleGUI code.
If go it with new class inherited from sg.Table, some variables as argument or global variables not required.
Here, colors of cell not considered in Entry.

All tkinter code in function edit_cell of following example.

import PySimpleGUI as sg
import random, string

# ------ Some functions to help generate data for the table ------
def word():
    return ''.join(random.choice(string.ascii_lowercase) for i in range(10))
def number(max_val=1000):
    return random.randint(0, max_val)

def make_table(num_rows, num_cols):
    data = [[j for j in range(num_cols)] for i in range(num_rows)]
    data[0] = [word() for _ in range(num_cols)]
    for i in range(0, num_rows):
        data[i] = [i, word(), *[number() for i in range(num_cols - 1)]]
    return data

def edit_cell(window, key, row, col, justify='left'):

    global textvariable, edit

    def callback(event, row, col, text, key):
        global edit
        widget = event.widget
        if key == 'Return':
            text = widget.get()
            print(text)
        widget.destroy()
        widget.master.destroy()
        values = list(table.item(row, 'values'))
        values[col] = text
        table.item(row, values=values)
        edit = False

    if edit or row <= 0:
        return

    edit = True
    # root = window.TKroot            # Update 07/23/2022
    table = window[key].Widget
    root = table.master               # Update 07/23/2022

    text = table.item(row, "values")[col]
    x, y, width, height = table.bbox(row, col)

    frame = sg.tk.Frame(root)
    frame.place(x=x, y=y, anchor="nw", width=width, height=height)
    textvariable = sg.tk.StringVar()
    textvariable.set(text)
    entry = sg.tk.Entry(frame, textvariable=textvariable, justify=justify)
    entry.pack()
    entry.select_range(0, sg.tk.END)
    entry.icursor(sg.tk.END)
    entry.focus_force()
    entry.bind("<Return>", lambda e, r=row, c=col, t=text, k='Return':callback(e, r, c, t, k))
    entry.bind("<Escape>", lambda e, r=row, c=col, t=text, k='Escape':callback(e, r, c, t, k))

def main_example1():
    global edit

    edit = False
    # ------ Make the Table Data ------
    # sg.Print('Creating table...')
    data = make_table(num_rows=1_000, num_cols=6)
    # headings = [str(data[0][x])+'     ..' for x in range(len(data[0]))]
    headings = [f'Col {col}' for col in range(len(data[0]))]
    # sg.Print('Done creating table.  Creating GUI...')
    sg.set_options(dpi_awareness=True)
    layout = [[sg.Table(values=data, headings=headings, max_col_width=25,
                        auto_size_columns=True,
                        # display_row_numbers=True,
                        justification='right',
                        num_rows=20,
                        alternating_row_color=sg.theme_button_color()[1],
                        key='-TABLE-',
                        # selected_row_colors='red on yellow',
                        # enable_events=True,
                        # select_mode=sg.TABLE_SELECT_MODE_BROWSE,
                        expand_x=True,
                        expand_y=True,
                        enable_click_events=True,  # Comment out to not enable header and other clicks
                        )],
              [sg.Button('Read'), sg.Button('Double'), sg.Button('Change Colors')],
              [sg.Text('Cell clicked:'), sg.T(k='-CLICKED-')]]


    window = sg.Window('Table Element - Example 1', layout, resizable=True, finalize=True)

    while True:
        event, values = window.read()
        if event in (sg.WIN_CLOSED, 'Exit'):
            break
        elif isinstance(event, tuple):
            cell = row, col = event[2]
            window['-CLICKED-'].update(cell)
            edit_cell(window, '-TABLE-', row+1, col, justify='right')

    window.close()

main_example1()

Note: Here I found something special, I can use both methods place with pack at the same time, so no new window required, and there's no issue about window focus.

image

@jason990420
Copy link
Collaborator

jason990420 commented Nov 21, 2021

By using place method, try to create background image for window.
Here I set the location of window to out of screen, after everything done, then move it back to center of sreen, so it look well.

Demo example here

from io import BytesIO
from PIL import Image
import PySimpleGUI as sg

def image_to_data(im):

    with BytesIO() as output:
        im.save(output, format="PNG")
        data = output.getvalue()
    return data

def make_background(window, file, main_frame):

    global images

    def find_frames(widget):
        widgets = list(widget.children.values())
        if isinstance(widget, (sg.tk.Frame, sg.tk.LabelFrame)):
            widget.update()
            x, y = widget.winfo_rootx() - x0, widget.winfo_rooty() - y0
            width, height = widget.winfo_width(), widget.winfo_height()
            new_im = im_.crop((x, y, x+width, y+height))
            image = sg.tk.PhotoImage(data=image_to_data(new_im))
            images.append(image)
            label = sg.tk.Label(widget, image=image, padx=0, pady=0, bd=0, bg=bg)
            label.place(x=0, y=0)
            label.lower()
        for widget in widgets:
            find_frames(widget)

    size = window.size
    im_ = Image.open(file).resize(size)
    root = window.TKroot
    widgets = list(root.children.values())
    x0, y0 = root.winfo_rootx(), root.winfo_rooty()

    frame = sg.tk.Frame(root, padx=0, pady=0, bd=0, bg=bg)
    frame.place(x=0, y=0)
    images = []
    image = sg.tk.PhotoImage(data=image_to_data(im_))
    images.append(image)
    label = sg.tk.Label(frame, image=image, padx=0, pady=0, bd=0, bg=bg)
    label.pack()
    main_frame.Widget.master.place(in_=frame, anchor='center', relx=.5, rely=.5)
    frame.lower()
    frame.update()
    for widget in widgets:
        find_frames(widget)

bg = sg.theme_background_color()
background_image_file = 'd:/background.png'
w, h = size = 640, 480  # size of background image

sg.set_options(dpi_awareness=True)

frame = [
    [sg.Input(size=(30, 1), expand_x=True),
     sg.Button('Browse')],
    [sg.Multiline('', expand_x=True, expand_y=True)],
    [sg.Button('Exit')],
]
layout = [[sg.Frame('', frame, size=(350, 200), border_width=0, key='FRAME', background_color=bg)]]

location = sg.Window.get_screen_size()
window = sg.Window('Background Demo', layout, margins=(0, 0), grab_anywhere=True,
    size=size, keep_on_top=True, finalize=True,
    no_titlebar=True,
    transparent_color=bg,
)

images = []
make_background(window, background_image_file, window['FRAME'])

while True:
    event, values = window.read()
    if event in (sg.WINDOW_CLOSED, 'Cancel', 'Exit'):
        break
    print(event)

window.close()

image

@PySimpleGUI
Copy link
Owner Author

Thank you!!!

I will study this today! I'm more than happy to also pull some of this code into PySimpleGUI itself to help create this feature.

I could see helping users with additional parameters, additional methods, etc. I just didn't have the code but certainly, have code to look at now! You're a true programming 🧙‍♂️ Jason!

The screenshot is AMAZING. This will no doubt be a popular feature once it's discovered.

VERY clever use of place/pack.

One serious limitation of PySimpleGUI is that in the tkinter callbacks, I cannot call other PySimpleGUI calls that create windows. For example, I can't call popup_get_date.

It'll be interesting to merge your code with my table callback code.

This is the code that is in PySimpleGUI that does the header and cell detection. And, once again, I have you to thank for this code existing!

    def _table_clicked(self, event):
        """
        Not user callable.  Callback function that is called a click happens on a table.
        Stores the selected rows in Element as they are being selected. If events enabled, then returns from Read

        :param event: event information from tkinter
        :type event:  (unknown)
        """
        if not self._widget_was_created():  # if widget hasn't been created yet, then don't allow
            return
        try:
            region = self.Widget.identify('region', event.x, event.y)
            if region == 'heading':
                row = -1
            elif region == 'cell':
                row = int(self.Widget.identify_row(event.y))-1
            elif region == 'separator':
                row = None
            else:
                row = None
            col_identified = self.Widget.identify_column(event.x)
            if col_identified:      # Sometimes tkinter returns a value of '' which would cause an error if cast to an int
                column = int(self.Widget.identify_column(event.x)[1:])-1-int(self.DisplayRowNumbers is True)
            else:
                column = None
        except Exception as e:
            warnings.warn('Error getting table click data for table with key= {}\nError: {}'.format(self.Key, e), UserWarning)
            if not SUPPRESS_ERROR_POPUPS:
                _error_popup_with_traceback('Unable to complete operation getting the clicked event for table with key {}'.format(self.Key), _create_error_message(), e, 'Event data:', obj_to_string_single_obj(event))
            row = column = None

        self.last_clicked_position = (row, column)

        # update the rows being selected if appropriate
        self.ParentForm.TKroot.update()
        # self.TKTreeview.()
        selections = self.TKTreeview.selection()
        # print(selections)
        self.SelectedRows = [int(x) - 1 for x in selections]
        # print('The new selected rows = ', self.SelectedRows)
        if self.enable_click_events is True:
            if self.Key is not None:
                self.ParentForm.LastButtonClicked = (self.Key, '+CICKED+', (row, column))
            else:
                self.ParentForm.LastButtonClicked = ''
            self.ParentForm.FormRemainedOpen = True
            _exit_mainloop(self.ParentForm)

@PySimpleGUI
Copy link
Owner Author

BTW, 💗 that you use the Emoji's so much in the examples. For me it's adds a level of humanism or something.

In the new psgcompiler, the emoji shows the status. Its sleeping...
image

When there's a compile being worked on, it changes to this
image

If it fails, of course we get this:
image

And success looks like success!
image

@PySimpleGUI
Copy link
Owner Author

image

OMG Jason! You've done the impossible.

I added a few more elements to your layout to test to see if indeed any element is available, and it looks like ANYTHING can be used. WOW!! WOW!

I didn't even ask about this enhancement.... what a huge bonus!

@jason990420
Copy link
Collaborator

jason990420 commented Nov 21, 2021

Here, I found another issue, Window.move_to_center() may not work well if window changed and GUI not updated.
So sg.Window().refresh() required before sg.Window().move_to_center(),

location = sg.Window.get_screen_size()
window = sg.Window('Demo background', layout, margins=(0, 0), border_depth=0,
    location=location, finalize=True)
background(window, "D:/desktop.png")
window.refresh()                                       # required if window changed, and GUI not updated
window.move_to_center()

@PySimpleGUI
Copy link
Owner Author

Ah, I maybe should add a refresh inside of move_to_center both at the start of the function and just before returning? That seems like a good idea to do in general and I don't think it'll hurt performance as this function isn't called frequently.

@PySimpleGUI
Copy link
Owner Author

Ideally, I want to merge the code you provided for the Edit Cell into the code inside PySimpleGUI, specifically the callback used to determine if a header or cell was clicked. That seems like the best place to do the edit type of operation.

I was thinking of adding a few parms such as allow_cell_editing and perhaps a couple of indicators that the edit is confirmed (return key) or cancelled (escape key). I think those 3 parms added to Table would really really really be a big step forward for the tkinter port!

Gosh Jason, thank you so much for this code! I'm stunned every single time.

@jason990420
Copy link
Collaborator

IMO, there should be different when select a row/item or edit, maybe double clicks to edit ?

@PySimpleGUI
Copy link
Owner Author

IMO, there should be different when select a row/item or edit, maybe double clicks to edit ?

Yes yes!

Maybe a "bind string" can be passed in as an optional parm? The default being a double click of the left mouse. By making a parameter with a bind-string, it can be overridden.

Thank you for continuing to make this better and better. This is great stuff!

image

@PySimpleGUI
Copy link
Owner Author

I really think this is worth the investment of getting this feature into the code prior to recording the Udemy lesson on Tables.

I've got the basic examples written and this one feature would REALLY take it something special!

@PySimpleGUI
Copy link
Owner Author

I really think this is worth the investment of getting this feature into the code prior to recording the Udemy lesson on Tables.

Now that I've said this, I have to make sure to balance out the need, to get the course completed ASAP... so, I may end up delaying this implementation for a bit instead of trying to code it up quicky, jam it into a release, and then write a lesson that incorporates it.

I think I would prefer, now that I've taken a moment, to get the course completed and then go back and make this enhancement. One of the examples in the lesson does cover getting click events on cells and headers, so the viewers will be educated enough on the Table element to be able to easily understand the addition of a few extra parms.

It's really hard to resist adding these features that stand out as clearly being incredibly useful with getting this course completed.

Thank you for doing all this work. I'm going to wait until I get the course done and then go back to this particular feature FIRST. I can also always go back and make another recording of the Table lesson once the feature is completed.

@DirectWest
Copy link

@jason990420 I used the same edit_cell function that you provided but put the table into a group tab under an input text box, then the Entry (input text box) is in an incorrect location:

image

Do you have a clue on how to modify the position of the Entry?

    root = window.TKroot
    table = window[key].Widget

    text = table.item(row, "values")[col]
    x, y, width, height = table.bbox(row, col)

    frame = sg.tk.Frame(root)
    frame.place(x=x, y=y, anchor="nw", width=width, height=height)

    textvariable = sg.tk.StringVar()
    textvariable.set(text)
    entry = sg.tk.Entry(frame, textvariable=textvariable, justify=justify)
    entry.pack()

@jason990420
Copy link
Collaborator

Maybe you can try to change the root to master of the Table element.

table = window[key].Widget
root = table.master

@DirectWest
Copy link

Maybe you can try to change the root to master of the Table element.

table = window[key].Widget
root = table.master

It solved my problem. Thank you for the prompt reply!

@StephenTronchin
Copy link

@jason990420 Thank you for the edit_cell function. Very cool. I am trying to use it for a GUI with tabs. I have one table in each tab. When I select a cell in table 2 it edits the corresponding cell in Table 1. I am not too familiar with Tkinter.

I have included the code. If you select a cell in Table 2, the cell in Table 1 is highlighted instead.

Is there a way to edit a table in different tabs? Thanks!

import PySimpleGUI as sg

## Function to edit selected cell -- by jason990420 (Github) ##

def edit_cell(window, key, row, col, justify='left'):

    global textvariable, edit, data1, data2

    def callback(event, row, col, text, key):
        global edit
        widget = event.widget
        if key == 'Return':
            text = widget.get()
          
        widget.destroy()
        widget.master.destroy()
        values = list(table.item(row, 'values'))
        values[col] = text
        table.item(row, values=values)
        edit = False

    if edit or row <= 0:
        return

    edit = True
    table = window[key].Widget
    root = table.master

    text = table.item(row, "values")[col]
    x, y, width, height = table.bbox(row, col)

    frame = sg.tk.Frame(root)
    frame.place(x=x, y=y, anchor="nw", width=width, height=height)
    textvariable = sg.tk.StringVar()
    textvariable.set(text)
    entry = sg.tk.Entry(frame, textvariable=textvariable, justify=justify)
    entry.pack()
    entry.select_range(0, sg.tk.END)
    entry.icursor(sg.tk.END)
    entry.focus_force()
    entry.bind("<Return>", lambda e, r=row, c=col, t=text, k='Return':callback(e, r, c, t, k))
    entry.bind("<Escape>", lambda e, r=row, c=col, t=text, k='Escape':callback(e, r, c, t, k))


## Make GUI with tabs ##

col_labels = ['Col 1', 'Col 2', 'Col 3']
data1 = [[1, 1, 1],
         [1, 1, 1],
         [1, 1, 1]]

data2 = [[2, 2, 2],
         [2, 2, 2],
         [2, 2, 2]]

# Some options
sg.set_options(dpi_awareness=True)
sg.theme('LightBlue')
edit = False # Set edit to be false

## GUI Layout

# Table 1
t1_layout = [[sg.Table(values=data1, headings=col_labels, max_col_width=25,
                    auto_size_columns=True,
                    justification='center',
                    row_height=23,
                    num_rows=8,
                    alternating_row_color=sg.theme_button_color()[0],
                    key='-TABLE-1-',
                    selected_row_colors='red on yellow',
                    enable_events=True,
                    select_mode=sg.TABLE_SELECT_MODE_BROWSE,
                    expand_x=True,
                    expand_y=True,
                    enable_click_events=True,  # Comment out to not enable header and other clicks
                    )]]

# Table 2
t2_layout = [[sg.Table(data2, headings=col_labels, max_col_width=25,
                    auto_size_columns=True,
                    justification='center',
                    row_height=23,
                    num_rows=8,
                    alternating_row_color=sg.theme_button_color()[0],
                    key='-TABLE-2-',
                    selected_row_colors='red on yellow',
                    enable_events=True,
                    select_mode=sg.TABLE_SELECT_MODE_BROWSE,
                    expand_x=True,
                    expand_y=True,
                    enable_click_events=True,  # Comment out to not enable header and other clicks
                    )] ]


# Group the tabs
tab_group = [ 
    [sg.TabGroup(
        [[
            sg.Tab('Table 1', t1_layout, background_color=None),
            sg.Tab('Table 2', t2_layout, background_color=None) ]],
        tab_location='centertop',
        title_color='White', 
        tab_background_color='Purple',
        selected_title_color='Green',
        selected_background_color='Yellow',
        border_width=5,
        expand_x=True,
        expand_y=True,
        enable_events=True), 
        ], 
    [sg.Text('Cell clicked:'), sg.T(k='-CLICKED-')]
    ]


## GUI Window
window = sg.Window('Edit Table Test', tab_group, resizable=True, finalize=True)


## GUI Event
while True:
    event, values = window.read()
    if event in (sg.WIN_CLOSED, 'Exit'):
        break
    elif isinstance(event, tuple):
        cell = row, col = event[2]
        window['-CLICKED-'].update(cell)
        if row is not None: 
            #edit_cell(window, 'TABLE', row+1, col, justify='center')
            edit_cell(window, '-TABLE-1-', row+1, col, justify='center')
            edit_cell(window, '-TABLE-2-', row+1, col, justify='center')
            
window.close()

@jason990420
Copy link
Collaborator

jason990420 commented Aug 19, 2022

There're variables for settings of table editor, so it will not work for multiple tables and if table not shown.

The programming logic may work wrong if you click on one table and finished the cell edit only on one table, then next click on table to edit will not work in the right way. Anyway, IMO, It looks like wrong logic to edit the same cell of both tables at the same time.

To work for each table, I revise it to new class with new method.

import PySimpleGUI as sg

class Table(sg.Table):

    def __init__(self, *args, **kwargs):

        super().__init__(*args, **kwargs)
        self.edit = False

    def callback(self, event, row, col, text, key):

        widget = event.widget
        if key == 'Return':
            text = widget.get()
        widget.destroy()
        widget.master.destroy()
        values = list(self.table.item(row, 'values'))
        values[col] = text
        self.table.item(row, values=values)
        self.edit = False

    def edit_cell(self, row, col, justify='left'):

        if self.edit or row <= 0:
            return

        self.edit = True
        self.table = self.Widget
        root = self.table.master

        text = self.table.item(row, "values")[col]
        rec = self.table.bbox(row, col)
        print(repr(rec))
        x, y, width, height = rec

        frame = sg.tk.Frame(root)
        frame.place(x=x, y=y, anchor="nw", width=width, height=height)
        self.textvariable = sg.tk.StringVar()
        self.textvariable.set(text)
        entry = sg.tk.Entry(frame, textvariable=self.textvariable, justify=justify)
        entry.pack()
        entry.select_range(0, sg.tk.END)
        entry.icursor(sg.tk.END)
        entry.focus_force()
        entry.bind("<Return>", lambda e, r=row, c=col, t=text, k='Return':self.callback(e, r, c, t, k))
        entry.bind("<Escape>", lambda e, r=row, c=col, t=text, k='Escape':self.callback(e, r, c, t, k))

## Make GUI with tabs ##

col_labels = ['Col 1', 'Col 2', 'Col 3']
data1 = [[1, 1, 1],
         [1, 1, 1],
         [1, 1, 1]]

data2 = [[2, 2, 2],
         [2, 2, 2],
         [2, 2, 2]]

# Some options
sg.set_options(dpi_awareness=True)
sg.theme('LightBlue')
edit = False # Set edit to be false

## GUI Layout

# Table 1
t1_layout = [[Table(values=data1, headings=col_labels, max_col_width=25,
                    auto_size_columns=True,
                    justification='center',
                    row_height=23,
                    num_rows=8,
                    alternating_row_color=sg.theme_button_color()[0],
                    key='-TABLE-1-',
                    selected_row_colors='red on yellow',
                    enable_events=True,
                    select_mode=sg.TABLE_SELECT_MODE_BROWSE,
                    expand_x=True,
                    expand_y=True,
                    enable_click_events=True,  # Comment out to not enable header and other clicks
                    )]]

# Table 2
t2_layout = [[Table(data2, headings=col_labels, max_col_width=25,
                    auto_size_columns=True,
                    justification='center',
                    row_height=23,
                    num_rows=8,
                    alternating_row_color=sg.theme_button_color()[0],
                    key='-TABLE-2-',
                    selected_row_colors='red on yellow',
                    enable_events=True,
                    select_mode=sg.TABLE_SELECT_MODE_BROWSE,
                    expand_x=True,
                    expand_y=True,
                    enable_click_events=True,  # Comment out to not enable header and other clicks
                    )] ]

# Group the tabs
tab_group = [
    [sg.TabGroup(
        [[
            sg.Tab('Table 1', t1_layout, background_color=None, key='-TAB 1-'),
            sg.Tab('Table 2', t2_layout, background_color=None, key='-TAB 2-') ]],
        tab_location='centertop',
        title_color='White',
        tab_background_color='Purple',
        selected_title_color='Green',
        selected_background_color='Yellow',
        border_width=5,
        expand_x=True,
        expand_y=True,
        key='-TABGROUP-',
        enable_events=True),
        ],
    [sg.Text('Cell clicked:'), sg.T(k='-CLICKED-')]
    ]

## GUI Window
window = sg.Window('Edit Table Test', tab_group, resizable=True, finalize=True)
tab1 , tab2 = window['-TAB 1-'], window['-TAB 2-']
table1, table2 = window['-TABLE-1-'], window['-TABLE-2-']

## GUI Event
while True:
    event, values = window.read()
    if event in (sg.WIN_CLOSED, 'Exit'):
        break
    elif isinstance(event, tuple):
        cell = row, col = event[2]
        window['-CLICKED-'].update(cell)
        if row is not None:
            tab = values['-TABGROUP-']
            tab1.select()
            window.refresh()
            table1.edit_cell(row+1, col, justify='center')
            tab2.select()
            window.refresh()
            table2.edit_cell(row+1, col, justify='center')
            window[tab].select()

window.close()

Note: No testing on new code.

@StephenTronchin
Copy link

Awesome, thanks Jason! The only thing I included is an 'if' statement in the last block in the isinstance() call. Now the code will only edit the selected cell in the current tab. Thanks again Jason!

import PySimpleGUI as sg

class Table(sg.Table):

    def __init__(self, *args, **kwargs):

        super().__init__(*args, **kwargs)
        self.edit = False

    def callback(self, event, row, col, text, key):

        widget = event.widget
        if key == 'Return':
            text = widget.get()
        widget.destroy()
        widget.master.destroy()
        values = list(self.table.item(row, 'values'))
        values[col] = text
        self.table.item(row, values=values)
        self.edit = False

    def edit_cell(self, row, col, justify='left'):

        if self.edit or row <= 0:
            return

        self.edit = True
        self.table = self.Widget
        root = self.table.master

        text = self.table.item(row, "values")[col]
        rec = self.table.bbox(row, col)
        print(repr(rec))
        x, y, width, height = rec

        frame = sg.tk.Frame(root)
        frame.place(x=x, y=y, anchor="nw", width=width, height=height)
        self.textvariable = sg.tk.StringVar()
        self.textvariable.set(text)
        entry = sg.tk.Entry(frame, textvariable=self.textvariable, justify=justify)
        entry.pack()
        entry.select_range(0, sg.tk.END)
        entry.icursor(sg.tk.END)
        entry.focus_force()
        entry.bind("<Return>", lambda e, r=row, c=col, t=text, k='Return':self.callback(e, r, c, t, k))
        entry.bind("<Escape>", lambda e, r=row, c=col, t=text, k='Escape':self.callback(e, r, c, t, k))

## Make GUI with tabs ##

col_labels = ['Col 1', 'Col 2', 'Col 3']
data1 = [[1, 1, 1],
         [1, 1, 1],
         [1, 1, 1]]

data2 = [[2, 2, 2],
         [2, 2, 2],
         [2, 2, 2]]

# Some options
sg.set_options(dpi_awareness=True)
sg.theme('LightBlue')
edit = False # Set edit to be false

## GUI Layout

# Table 1
t1_layout = [[Table(values=data1, headings=col_labels, max_col_width=25,
                    auto_size_columns=True,
                    justification='center',
                    row_height=23,
                    num_rows=8,
                    alternating_row_color=sg.theme_button_color()[0],
                    key='-TABLE-1-',
                    selected_row_colors='red on yellow',
                    enable_events=True,
                    select_mode=sg.TABLE_SELECT_MODE_BROWSE,
                    expand_x=True,
                    expand_y=True,
                    enable_click_events=True,  # Comment out to not enable header and other clicks
                    )]]

# Table 2
t2_layout = [[Table(data2, headings=col_labels, max_col_width=25,
                    auto_size_columns=True,
                    justification='center',
                    row_height=23,
                    num_rows=8,
                    alternating_row_color=sg.theme_button_color()[0],
                    key='-TABLE-2-',
                    selected_row_colors='red on yellow',
                    enable_events=True,
                    select_mode=sg.TABLE_SELECT_MODE_BROWSE,
                    expand_x=True,
                    expand_y=True,
                    enable_click_events=True,  # Comment out to not enable header and other clicks
                    )] ]

# Group the tabs
tab_group = [
    [sg.TabGroup(
        [[
            sg.Tab('Table 1', t1_layout, background_color=None, key='-TAB 1-'),
            sg.Tab('Table 2', t2_layout, background_color=None, key='-TAB 2-') ]],
        tab_location='centertop',
        title_color='White',
        tab_background_color='Purple',
        selected_title_color='Green',
        selected_background_color='Yellow',
        border_width=5,
        expand_x=True,
        expand_y=True,
        key='-TABGROUP-',
        enable_events=True),
        ],
    [sg.Text('Cell clicked:'), sg.T(k='-CLICKED-')]
    ]

## GUI Window
window = sg.Window('Edit Table Test', tab_group, resizable=True, finalize=True)
tab1 , tab2 = window['-TAB 1-'], window['-TAB 2-']
table1, table2 = window['-TABLE-1-'], window['-TABLE-2-']

## GUI Event
while True:
    event, values = window.read()
    if event in (sg.WIN_CLOSED, 'Exit'):
        break
    elif isinstance(event, tuple):
        cell = row, col = event[2]
        window['-CLICKED-'].update(cell)
        if row is not None:
            tab = values['-TABGROUP-']
            if tab == '-TAB 1-': # Edits the cell only in Table 1
                tab1.select()
                window.refresh()
                table1.edit_cell(row+1, col, justify='center')
            if tab == '-TAB 2-': # Edits the cell only in Table 2
                tab2.select()
                window.refresh()
                table2.edit_cell(row+1, col, justify='center')
            window[tab].select()

window.close()

@mingersming
Copy link

First of all, thanks for the feature.
But I struggle with one issue - how can I use the edited table for further purposes, e.g. export the new table?
The table values are visually updated but the variable for the table seems not to be updated.
The value of the assigned table is overwritten with the value of the clicked row.

import PySimpleGUI as sg
import random, string

# ------ Some functions to help generate data for the table ------
def word():
    return ''.join(random.choice(string.ascii_lowercase) for i in range(10))
def number(max_val=1000):
    return random.randint(0, max_val)

def make_table(num_rows, num_cols):
    data = [[j for j in range(num_cols)] for i in range(num_rows)]
    data[0] = [word() for _ in range(num_cols)]
    for i in range(0, num_rows):
        data[i] = [i, word(), *[number() for i in range(num_cols - 1)]]
    return data

def edit_cell(window, key, row, col, justify='left'):

    global textvariable, edit

    def callback(event, row, col, text, key):
        global edit
        widget = event.widget
        if key == 'Return':
            text = widget.get()
            print(text)
        widget.destroy()
        widget.master.destroy()
        values = list(table.item(row, 'values'))
        values[col] = text
        table.item(row, values=values)
        edit = False

    if edit or row <= 0:
        return

    edit = True
    # root = window.TKroot            # Update 07/23/2022
    table = window[key].Widget
    root = table.master               # Update 07/23/2022

    text = table.item(row, "values")[col]
    x, y, width, height = table.bbox(row, col)

    frame = sg.tk.Frame(root)
    frame.place(x=x, y=y, anchor="nw", width=width, height=height)
    textvariable = sg.tk.StringVar()
    textvariable.set(text)
    entry = sg.tk.Entry(frame, textvariable=textvariable, justify=justify)
    entry.pack()
    entry.select_range(0, sg.tk.END)
    entry.icursor(sg.tk.END)
    entry.focus_force()
    entry.bind("<Return>", lambda e, r=row, c=col, t=text, k='Return':callback(e, r, c, t, k))
    entry.bind("<Escape>", lambda e, r=row, c=col, t=text, k='Escape':callback(e, r, c, t, k))

def main_example1():
    global edit

    edit = False
    # ------ Make the Table Data ------
    # sg.Print('Creating table...')
    data = make_table(num_rows=1_000, num_cols=6)
    # headings = [str(data[0][x])+'     ..' for x in range(len(data[0]))]
    headings = [f'Col {col}' for col in range(len(data[0]))]
    # sg.Print('Done creating table.  Creating GUI...')
    sg.set_options(dpi_awareness=True)
    layout = [[sg.Table(values=data, headings=headings, max_col_width=25,
                        auto_size_columns=True,
                        # display_row_numbers=True,
                        justification='right',
                        num_rows=20,
                        alternating_row_color=sg.theme_button_color()[1],
                        key='-TABLE-',
                        # selected_row_colors='red on yellow',
                        # enable_events=True,
                        # select_mode=sg.TABLE_SELECT_MODE_BROWSE,
                        expand_x=True,
                        expand_y=True,
                        enable_click_events=True,  # Comment out to not enable header and other clicks
                        )],
              [sg.Button('Read'), sg.Button('Double'), sg.Button('Change Colors'), sg.Button('Exit')],
              [sg.Text('Cell clicked:'), sg.T(k='-CLICKED-')]]


    window = sg.Window('Table Element - Example 1', layout, resizable=True, finalize=True)

    while True:
        event, values = window.read()
        if event in (sg.WIN_CLOSED, 'Exit'):
            sg.popup('Check',
                     'The results of the window.',
                     'The button clicked was "{}"'.format(event),
                     'The values are', values)
            break
        elif isinstance(event, tuple):
            cell = row, col = event[2]
            window['-CLICKED-'].update(cell)
            edit_cell(window, '-TABLE-', row+1, col, justify='right')

    window.close()
    
main_example1()

@jason990420
Copy link
Collaborator

There're still lot of issues for the edit function., like

  • Validation for the entry.
  • Value type of the cell.
  • Update the values of table.
  • else.

You can find the index in the values of your table by (row, col), and update the value by data[row][col] = convert_type(text) if entry is valid where convert_type is a new function to convert the text to correct data type in your table.

        if key == 'Return':
            text = widget.get()    # Get the new value of cell, then convert to the data type and update the values of your table.
            print(text)

@mingersming
Copy link

Thanks a lot for the reply and the useful hints.
But to get it working, I had to insert the data table as an argument in the edit_cell function and decline the row number by one.

if key == 'Return':
            text = widget.get()    # Get the new value of cell, then convert to the data type and update the values of your table.
            print(text)
            data[row-1][col] = text

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Demo Programs Problem is in the demo programs (or any other non-core code) enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

5 participants