Giter Club home page Giter Club logo

pysimplegui-solution's People

Contributors

jason990420 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

pysimplegui-solution's Issues

Simulate button click

from time import sleep
import PySimpleGUI as sg

def click(element):
    element.Widget.config(relief = "sunken")
    window.refresh()
    sleep(0.1)
    element.Widget.config(relief = "raised")
    window.refresh()
    element.click()

sg.theme('DarkAmber')

layout = [
    [sg.Text('Some text on Row 1')],
    [sg.Text('Enter something on Row 2'), sg.InputText()],
    [sg.Button('Ok'), sg.Button('Cancel')]
]

window = sg.Window('Window Title', layout, finalize=True)
window.bind('<Key-F3>', 'F3')
window.bind('<Key-F5>', 'F5')
ok = window['Ok']
cancel = window['Cancel']

while True:
    event, values = window.read()
    print(event)
    if event == sg.WIN_CLOSED or event == 'Cancel':
        break
    if event == 'F3':
        # click(ok)
        ok.click()
    elif event == 'F5':
        # click(cancel)
        cancel.click()

window.close()

Move node up and down in sg.Tree for one layer only

image

import PySimpleGUI as sg


def hide_header(tree):
    tree.Widget.configure(show='tree')

def key_to_id(key):
    for k, v in tree.IdToKey.items():
        if v == key:
            return k
    return None

def select(key=''):
    iid = key_to_id(key)
    if iid:
        tree.Widget.see(iid)
        tree.Widget.selection_set(iid)

def where():
    item = tree.Widget.selection()
    return '' if len(item) == 0 else tree.IdToKey[item[0]]

def move_up():
    key =  where()
    if key == '':
        return
    node = treedata.tree_dict[key]
    parent_node = treedata.tree_dict[node.parent]
    index = parent_node.children.index(node)
    if index != 0:
        parent_node.children[index-1], parent_node.children[index] = (
            parent_node.children[index], parent_node.children[index-1])
    tree.update(values=treedata)
    select(key)

def move_down():
    key = where()
    if key == '':
        return
    node = treedata.tree_dict[key]
    parent_node = treedata.tree_dict[node.parent]
    index = parent_node.children.index(node)
    if index != len(parent_node.children)-1:
        parent_node.children[index+1], parent_node.children[index] = (
            parent_node.children[index], parent_node.children[index+1])
    tree.update(values=treedata)
    select(key)

fruits = [
    "Apple", "Banana", "Cherry", "Durian", "Elderberry", "Guava", "Jackfruit",
    "Kiwi", "Lemon", "Mango", "Orange", "Papaya", "Strawberry", "Tomato",
    "Watermelon",
]

treedata = sg.TreeData()
for i, fruit in enumerate(fruits):
    treedata.Insert('', i, fruit, values=[f'Fruit {i:0>2d}'])

layout = [
    [sg.Button('Move Up'), sg.Button('Move Down')],
    [sg.Tree(data=treedata, key='TREE', headings=['Nothing'],
        select_mode=sg.TABLE_SELECT_MODE_BROWSE)],
]

window = sg.Window('Tree', layout, finalize=True)
tree = window['TREE']
hide_header(tree)

while True:

    event, values = window.read()
    print(event, values)
    if event == sg.WINDOW_CLOSED:
        break
    elif event == 'Move Up':
        move_up()
    elif event == 'Move Down':
        move_down()

window.close()

Helper for PySimpleGUI

image

import ctypes
from inspect import getfullargspec, signature
import PySimpleGUI as sg

def Tree(key, treedata, font=('Courier New', 12, 'bold'), width=22):
    return sg.Tree(data=treedata, headings=['Notes',], pad=(1, 0),
    show_expanded=True, col0_width=width, auto_size_columns=False,
    visible_column_map=[False,], select_mode=sg.TABLE_SELECT_MODE_BROWSE,
    enable_events=True, text_color=None, background_color=None,
    font=font, num_rows=20, row_height=26, key=key)

def get_new_key(treedata):
    i = 0
    while i in treedata.tree_dict:
        i += 1
    return i

def update_attributes(element, tree_attribute):
    item = getfullargspec(element)
    args = item.args[1:]
    defaults = item.defaults
    treedata = sg.TreeData()
    text_list = sorted([f'{arg:35}= {repr(default):30}' for arg, default in
        zip(args, defaults)], key=lambda x:(not x.islower(), x))
    for text in text_list:
        treedata.insert('', get_new_key(treedata), text, [])
    tree_attribute.update(values=treedata)
    return treedata

def update_methods(element, tree_methods):
    methods = []
    names = []
    for name in sorted(dir(element), key=lambda x:(not x.islower(), x)):
        attr = getattr(element, name)
        if not name.startswith('_') and attr not in methods and callable(attr):
            names.append(name)
            methods.append(attr)
    treedata = sg.TreeData()
    for text in names:
        treedata.insert('', get_new_key(treedata), text, [])
    tree_methods.update(values=treedata)
    return treedata

element_classes = sg.Element.__subclasses__()
elements = {element.__name__: element for element in element_classes}
elements['Window'] = sg.Window

ctypes.windll.user32.SetProcessDPIAware()   # Set unit of GUI to pixels
sg.theme('DarkBlue')

treedata_element = sg.TreeData()
treedata_attribute = sg.TreeData()
treedata_method = sg.TreeData()

for item in sorted(elements.keys()):
    treedata_element.insert('', get_new_key(treedata_element), item, [])

layout = [
    [
        Tree('TREE_ELEMENT', treedata_element),
        Tree('TREE_ATTRIBUTE', treedata_attribute, width=63),
        Tree('TREE_METHOD', treedata_method, width=35),
    ],
    [
        sg.Multiline('', font=('Courier New', 12, 'bold'), pad=(1, 4),
            size=(124, 10), background_color='DarkBlue', key='STATUS'),
    ]
]
window = sg.Window("PySimpleGUI Helper", layout, finalize=True)

tree_element    = window['TREE_ELEMENT']
tree_attribute  = window['TREE_ATTRIBUTE']
tree_method     = window['TREE_METHOD']
status          = window['STATUS']
tree_element.Widget.heading("#0", text='PySimleGUI Elements')
tree_attribute.Widget.heading("#0", text='Arguments To Initialize & Default Value')
tree_method.Widget.heading("#0", text='Element Methods')

for key, element in window.AllKeysDict.items(): # Remove focus
    element.Widget.configure(takefocus=0)

while True:
    event, values = window.read()
    if event == sg.WINDOW_CLOSED:
        break
    elif event == 'TREE_ELEMENT':
        key = values[event][0]
        element_text = treedata_element.tree_dict[key].text
        element = elements[element_text]
        treedata_attribute = update_attributes(element, tree_attribute)
        treedata_method = update_methods(element, tree_method)
        status.update(value='')
    elif event == 'TREE_METHOD':
        key = values[event][0]
        text = treedata_method.tree_dict[key].text
        method = getattr(element, text)
        string = '\n'.join(map(str.strip, method.__doc__.split('\n')))
        status.update(value='def '+text+str(signature(method))+'\n'+string)

window.close()

Auto Complete for sg.Input

image

from datetime import datetime
from random import randint
import PySimpleGUI as sg

class Auto_Input(sg.Input):

    user_event = True
    window = None
    win, element = None, None
    dx, dy = 0, 0

    def __init__(self, **kw):
        super().__init__(**kw)

    def binding(self):
        self.bind('<FocusIn>', '+FocusIn')
        self.bind('<FocusOut>', '+FocusOut')

    def write_value(self, text):
        self.update(text)
        self.set_focus()
        Auto_Input.user_event = False

    def auto_complete(self, text):
        Auto_Input.element = self
        if not Auto_Input.user_event:
            Auto_Input.user_event = True
            return
        if text:
            widget = self.Widget
            width, height = widget.winfo_width(), widget.winfo_height()
            x, y = widget.winfo_rootx(), widget.winfo_rooty()
            text_list = [f'{text}{i}' for i in range(10)]
            location = (x, y+height+2)
            if Auto_Input.win and Auto_Input.win.TKroot:
                Auto_Input.win.close()
            self.listbox(text_list, location)
            self.set_focus()
            Auto_Input.user_event = False
        else:
            if Auto_Input.win:
                Auto_Input.win.close()

    def listbox(self, text_list, location):
        x, y = self.Size
        layout = [[sg.Listbox(values=text_list, size=(x, 5), pad=(0, 0),
            enable_events=True, bind_return_key=True, key='Auto')]]
        Auto_Input.win = sg.Window("", layout, default_element_size=(10, 1),
            margins=(0, 0), auto_size_text=False, return_keyboard_events=True,
            keep_on_top=True, no_titlebar=True, auto_size_buttons=False,
            location=location, finalize=True)
        root1 = Auto_Input.window.TKroot
        root2 = Auto_Input.win.TKroot
        x1, y1 = root1.winfo_x(), root1.winfo_y()
        x2, y2 = root2.winfo_x(), root2.winfo_y()
        Auto_Input.dx, Auto_Input.dy = x2-x1, y2-y1

    def follow(event):
        if Auto_Input.win and Auto_Input.win.TKroot:
            root = Auto_Input.window.TKroot
            x, y = root.winfo_x(), root.winfo_y()
            Auto_Input.win.TKroot.geometry(
                f"+{x+Auto_Input.dx}+{y+Auto_Input.dy}")

def focus_binding(window):
    Auto_Input.window = window
    Auto_Input.window.TKroot.bind("<Configure>", Auto_Input.follow)
    for element in window.AllKeysDict.values():
        if isinstance(element, Auto_Input):
            element.binding()

def main():

    sg.theme("DarkBlue")
    sg.set_options(font=("Courier New", 12))

    layout = [[Auto_Input(size=(randint(10, 20), 1), enable_events=True,
        key=f'INPUT,{i},{j}') for j in range(4)] for i in range(3)]
    window = sg.Window("Title", layout, finalize=True)
    focus_binding(window)

    while True:
        win, event, values = sg.read_all_windows()
        print(event)
        if event == sg.WINDOW_CLOSED:
            break
        elif event.startswith('INPUT') and not event.endswith('FocusOut'):
            key = event.split("+")[0]
            element, text = window[key], values[key]
            element.auto_complete(text)
        elif event.endswith('FocusOut'):
            if Auto_Input.win and Auto_Input.win.TKroot:
                Auto_Input.win.hide()
        elif event == 'Auto':
            if values['Auto'] and Auto_Input.element:
                text = values['Auto'][0]
                Auto_Input.element.write_value(text)

    if Auto_Input.win:
        Auto_Input.win.close()
    window.close()

if __name__ == '__main__':
    main()

Remove focus to elements

There're three places to set the focus box.

  • Option focus of sg.Button: if True, initial focus will be put on this button, default value is False.
  • Option use_default_focus of sg.Window: If True will use the default focus algorithm to set the focus to the "Correct" element
  • Method block_focus(block=True) of element: If True, this element will not be given focus by using the keyboard (TAB or ctrl-TAB) to go from one element to another.

To remove focus box in sg.Button,

  • Set option focus=False in sg.Button, it is default
  • Set option use_default_focus=False in sg.Window (It looks like there's no difference now)
  • Call method block_focus() of element sg.Button after window finalized.
import PySimpleGUI as sg

sg.theme('DarkAmber')

layout = [
    [sg.Button('tk Button'),sg.Button('ttk Button', use_ttk_buttons=True)],
    [sg.Input()],
    [sg.Input()],
]
window = sg.Window('Window Title', layout, finalize=True)

for element in window.key_dict.values():
    if isinstance(element, sg.Button):
        element.block_focus()

while True:
    event, values = window.read()
    if event == sg.WIN_CLOSED:
        break

window.close()

Wrap text in Tree/Table element

There's no option to wrap text in sg.Tree or ttk.Treeview.

As I know, the only way is to insert '\n' into your text, then it will wrap to next line.

To show all lines in a row, you have to set option row_height large enough to fit all lines.
Here, another problem will be generated for row_height.

  • You won't see next line if row height is too small to shown all wrap lines.
  • If you set row_height large enough, you will still see same space occupied if only one line.
  • Text will be aligned vertical center

It seems there's no way to set row height for different rows.

Here's example code about how I do it.

image

import textwrap
from random import randint, choice
import PySimpleGUI as sg

def wrap(string, lenght=20):
    return '\n'.join(textwrap.wrap(string, lenght))

sg.theme('DarkBlue')
sg.set_options(font=('Courier New', 12))

treedata = sg.TreeData()

for i in range(10):
    parent = choice(list(treedata.tree_dict.keys()))
    values = [' '.join([f'This is Text {j}' for j in range(1, randint(2, 4))])]
    treedata.insert(parent, i, f'Node {i}', values=list(map(wrap, values)))

layout = [
    [sg.Tree(data=treedata, headings=['Data',], auto_size_columns=False,
        num_rows=10, col0_width=20, col_widths=[20], key='-TREE-',
        row_height=18*3, justification='left', show_expanded=True,
        enable_events=True),],
]

window = sg.Window('Tree Element Test', layout, finalize=True)
tree = window['-TREE-'].Widget

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

window.close()

Another way is more difficult to implement, using other elements to simulate a tree or a table.
I have nothing for it now.

Height of Listbox and Multiline are different

image

tkinter code required

import PySimpleGUI as sg

biglist = [
    ('Abundant Life','John 10:10'),
    ('Angels','Psalm 103:20'),
    ('Boldness','Proverbs 28:1')
]
attrib_dict = {key:value for key, value in biglist}
attrib_list = sorted(attrib_dict.keys())

sg.theme('Tan Blue')
sg.set_options(font=('Courier New', 12))

layout = [
    [sg.Text('Pick an attribute and see verse of promise.')],
    [sg.Listbox(attrib_list, size=(20, 10), key='-LIST-', enable_events=True),
     sg.Multiline(size = (60, 10), key = '-MULTILINE-', autoscroll = False, disabled=True)],
    [sg.Button('Exit')]]

window = sg.Window("God's Promises", layout, finalize=True)
window['-MULTILINE-'].Widget.configure(spacing1=1)  # tkinter code here

while True:

    event, values = window.read()
    if event in (sg.WIN_CLOSED, 'Exit'):
        break
    elif event == '-LIST-':
        selection = values[event][0]
        window['-MULTILINE-'].update(value=attrib_dict[selection])

window.close()

Expand and Collapse node in sg.Tree

Following code show my way for how to expand or collapse node in Tree, also show what node expanded with what text, you can also change it to with what key to avoid duplicates.

import PySimpleGUI as sg


class Tree_Data():

    def __init__(self):
        self.treedata = sg.TreeData()

    def new_key(self):
        i = 0
        while i in self.treedata.tree_dict:
            i += 1
        return i

    def insert(self, data):
        for i, first in enumerate(data):
            key = self.new_key()
            self.treedata.Insert('', key, first[0], values=[])
            for j, second in enumerate(first[1]):
                self.treedata.Insert(key, self.new_key(), second, values=[])

def get_expanded():
    opens = []
    for id_, key in tree.IdToKey.items():   # id_ of item for tkinter , key of item for PySimpleGUI
        item = tree.Widget.item(id_)
        if item['open']:                    # Keys of item: 'image', 'open', 'tags', 'text', 'values'
            opens.append(item['text'])
    return opens

def key_to_id(key):
    return tree.KeyToID[key] if key in tree.KeyToID else None

def expand_all():
    for key in tree_data.treedata.tree_dict:
        tree.Widget.item(key_to_id(key), open=True)

def collapse_all():
    for key in tree_data.treedata.tree_dict:
        tree.Widget.item(key_to_id(key), open=False)

sg.theme('dark')
sg.set_options(font=('Helvetica', 10))
tree_data = Tree_Data()
data = [
    ['Fruit',     ['Apple', 'Mongo']],
    ['Seasoning', ['Cat', 'Dog', 'Tiger']],
    ['Meat', ['Pork', 'Beef', 'Fish']],
    ['Vegetable', ['Broccoli', 'Cabbages', 'Mushrooms']],
]
tree_data.insert(data)

layout = [
    [sg.Tree(data=tree_data.treedata, headings=['Notes',], show_expanded=False,
     col0_width=20, select_mode=sg.TABLE_SELECT_MODE_BROWSE, enable_events=True,
     visible_column_map=[False,], key='TREE')],
    [sg.Button('Expand'), sg.Button('Collapse')]
]
window = sg.Window('TREE', layout, finalize=True)
tree = window['TREE']
tree.Widget.configure(show='tree')  # Hide header

while True:

    event, values = window.read()
    if event == sg.WINDOW_CLOSED:
        break
    print(event)
    if event == 'Expand':
        expand_all()
    elif event == 'Collapse':
        collapse_all()

    print(get_expanded())

window.close()

image

TREE
['Seasoning']
TREE
['Seasoning', 'Vegetable']

Set view narrower than width of table to get hor_scrollbar working, also column_width

It is necessary to explain some arguments about the column width in sg.Table

auto_size_columns

  • True, set width of each column by large one between
    • max width of all data on that column which limited by argument max_col_width, and
    • heading width of that column.
  • False, set width of each column by col_widths, if None, defined by def_col_width

Actually, horizontal scrollbar is useless for table because table always keep the width as sum of widths of all columns.
So all content of table show and that's why horizontal scrollbar is greyed out.

There's no way to work under PySimpleGUI, so the only way is to use tkinter code.
Here's example to show how it work.

image

tkinter code required here

from tkinter.font import Font
import numpy as np
import PySimpleGUI as sg

sg.theme('DarkBlue')
font_family, font_size = font = ('Courier New', 11)
sg.set_options(font=font)

headings = ['ID', 'Description']
data = [['ID', ' '.join([f'({i},{j})' for j in range(100)])] for i in range(100)]

frm_input_layout = [
    [sg.Table(values=data, headings=headings, justification='left',
        enable_events=True, key='tbl_variants',
        auto_size_columns=False, col_widths=[2, 40],    # Set width of window: 2+40 = 42 chars only
        hide_vertical_scroll=False, vertical_scroll_only=False,
    )]
]
layout = [[sg.Frame('Input', frm_input_layout)]]

window = sg.Window('App', layout, finalize=True)

# Set real table width after here
window.TKroot.update()
tree = window['tbl_variants'].Widget
tkfont = Font(family=font_family, size=font_size)
data_array = np.array([headings] + data)
column_widths = [max(map(lambda item:tkfont.measure(item), data_array[:, i]))
    for i in range(data_array.shape[1])]
for heading, width in zip(headings, column_widths):
    tree.column(heading, width=width+font_size)
# Set real table width before here

while True:
    event, values = window.read()
    if event == sg.WIN_CLOSED:
        break
window.close()

Visible and expand in Layout of window

image

import PySimpleGUI as sg

sg.theme("DarkBlue")
sg.set_options(font=('Courier New', 12))
input_size1, input_size2 = (34, 1), (10, 1)
frame_filename   = [
    [sg.Text("Filename")],
    [sg.pin(sg.Column([[sg.Input("*.ppt", size=input_size1)]], pad=(0, 0)))]
]
frame_directory  = [
    [sg.Checkbox("In directory", enable_events=True, key='col1')],
    [sg.pin(sg.Column(
        [[sg.Input("D:\\", size=input_size1)]], pad=(0,0), key='COL1'))],
]
frame_content    = [
    [sg.Checkbox("In contents", enable_events=True, key='col2')],
    [sg.pin(sg.Column(
        [[sg.Input("American", size=input_size1)]], pad=(0,0), key='COL2'))],
]
frame_modify     = [
    [sg.Checkbox("Modified between", enable_events=True, key='col3')],
    [sg.pin(sg.Column(
        [[sg.Input("2021/01/01", size=input_size2, key='IN1'), sg.Text("-"),
          sg.Input("2021/01/14", size=input_size2, key='IN2')]],
        pad=(0,0), key='COL3'))],
]
unit = ["B", "KB", "MB", "GB"]
frame_size_range = [
    [sg.Checkbox("Size range", enable_events=True, key='col4')],
    [sg.pin(sg.Column(
        [[sg.Input("500", size=input_size2, key='IN3'), sg.Text("-"),
          sg.Input("1000", size=input_size2, key='IN4'),
          sg.Combo(unit, unit[1], size=(4, 1), readonly=True)]],
        pad=(0,0), key='COL4'))]
]
frame_parameters = [
    [sg.Frame('', frame_filename, key='FRAME1')],
    [sg.Frame('', frame_directory, key='FRAME2')],
    [sg.Frame('', frame_content, key='FRAME3')],
    [sg.Frame('', frame_modify, key='FRAME4')],
    [sg.Frame('', frame_size_range, key='FRAME5')],
]
frame_main_search = [
    [sg.Input("*.pdf", size=input_size1, key='IN5')],
    [sg.Checkbox("All"), sg.Checkbox("Directory"), sg.Checkbox("Document"),
     sg.Checkbox("Executable"), sg.Checkbox("Picture"), sg.Checkbox("Music"),
     sg.Checkbox("Video")],
    [sg.Multiline("No files", border_width=5, key='ML')],
]
frame_top = [
    [sg.Frame('',
        [[sg.Frame('', frame_parameters, pad=(0, 0), vertical_alignment='top',
            border_width=0, key='FRAME6')]],
        vertical_alignment='top', pad=(0, 0), border_width=0),
     sg.Frame('', frame_main_search, pad=(0, 0), border_width=0, key='FRAME7')],
]
layout = [
    [sg.Frame('', frame_top, pad=(0, 0), border_width=0,
        vertical_alignment='top', key='FRAME8')],
    [sg.Frame('', [[sg.StatusBar("Status: OK", pad=(0, 0))]], pad=(0, 0),
        vertical_alignment='top', border_width=0, key='FRAME9')],
]

window = sg.Window("Title", layout, size=(1200, 500), resizable=True,
    finalize=True)

# Expand element as window resize, it will always make element visible
for key in ['IN5', 'FRAME2', 'FRAME3', 'FRAME4', 'FRAME5', 'FRAME9']:
    window[key].expand(expand_x=True, expand_row=False)
for key in ['FRAME6']:
    window[key].expand(expand_y=True, expand_row=False)
for key in ['ML', 'FRAME7', 'FRAME8']:
    window[key].expand(expand_x=True, expand_y=True)
columns = [f'col{i+1}' for i in range(4)]

# Hide some unchecked elements
for col in columns:
    window[col.upper()].update(visible=False)

while True:
    event, values = window.read()
    if event == sg.WINDOW_CLOSED:
        break
    if event in columns:
        toggle = values[event]
        window[event.upper()].update(visible=toggle)

window.close()

Delete, Detach, Reattach row in table

iid string start from '1', not '0'. method move index from 0.

    if event == 'Delete':
        for iid in ('5', '6', '7'):
            if table.exists(iid):
                table.delete(iid)
                window['-TABLE-'].tree_ids.remove(iid)
    if event == 'Detach':
        for iid in ('2', '3'):
            table.detach(iid)
    if event == 'Reattach':
        for iid in ('2', '3'):
            table.move(iid, '', int(iid)-1)
    if event == 'Empty':
        window['-TABLE-'].update([])

Circular Progress Bar in sg.Graph by png

image

import pickle
from random import randint
import PySimpleGUI as sg

class Progress_Bar():
    data = None
    def __init__(self, graph, filename, text_color='white',
            font=('Courier New', 12)):
        self.graph = graph
        if Progress_Bar.data is None:
            with open (filename, 'rb') as file:
                Progress_Bar.data = pickle.load(file)
        self.text_color = text_color
        self.font = font
        self.p, self.t   = None, None
        self.initial()

    def initial(self, angle=0):
        self.circle = self.graph.draw_image(
            data=Progress_Bar.data[361], location=(-50, 50))
        self.set_now(angle)
        self.set_target(angle)

    def set_target(self, angle=0, step=10):
        self.target = min(360, max(0, int(angle)))
        self.step = min(360, max(1, int(step)))

    def set_now(self, angle=0):
        self.angle = min(360, max(0, int(angle)))

    def move(self):
        if self.target == self.angle:
            return True
        if self.angle < self.target:
            self.angle = min(self.target, self.angle+self.step)
        else:
            self.angle = max(self.target, self.angle-self.step)
        if self.p:
            self.graph.delete_figure(self.p)
        if self.t:
            self.graph.delete_figure(self.t)
        text = f'{self.angle/3.6:.1f}%'
        self.p = self.graph.draw_image(data=Progress_Bar.data[self.angle],
            location=(-50, 50))
        self.t = self.graph.draw_text(text, (0, 0), color=self.text_color,
            font=self.font, text_location=sg.TEXT_LOCATION_CENTER)
        return False
rows, columns = 4, 6
layout = [[sg.Graph((111, 111), (-55, -55), (55, 55), background_color='green',
    key=f'Graph ({j}, {i})') for i in range(columns)] for j in range(rows)]
window = sg.Window('Circular Progressbar', layout, finalize=True)

filename = 'D:/arc_png.dat'
progress_bar = [[Progress_Bar(layout[row][column], filename)
    for column in range(columns)] for row in range(rows)]
for row in range(rows):
    for column in range(columns):
        progress_bar[row][column].set_target(randint(0, 360))

while True:
    event, values = window.read(timeout=5)
    if event == sg.WINDOW_CLOSED:
        break
    elif event == '__TIMEOUT__':
        for row in range(rows):
            for column in range(columns):
                p = progress_bar[row][column]
                if p.move():
                    p.set_target(randint(0, 360))

window.close()

Create 'D:/arc_png.dat' file

import math
import pickle
from io import BytesIO
from PIL import Image, ImageColor

class Point(tuple):

    def __new__(cls, x, y):
        return tuple.__new__(cls, (x, y))

    @property
    def x(self):
        return self[0]

    @property
    def y(self):
        return self[1]

    @property
    def angle(self):
        degree = math.atan2(self[1], self[0])*180/math.pi
        if degree < 0:
            degree += 360
        return degree

    @property
    def distance(self):
        return (self[0]**2 + self[1]**2)**0.5

    def __add__(self, other):
        if isinstance(other, (Point, tuple)):
            return Point(self[0]+other[0], self[1]+other[1])
        raise ValueError

    def __repr__(self):
        return f'({self.x}, {self.y})'

class Arc():

    def __init__(self, width=101, radius=40, start_angle=0, stop_angle=360,
            color='yellow', thickness=20):
        self.width = width
        self.half = self.width//2
        self.radius = radius
        self.start_angle = self.limit(start_angle)
        self.stop_angle = self.limit(self.start_angle + self.limit(
            stop_angle - self.start_angle))
        self.color = self.color_to_rgb(color)
        self.radius_inner = radius - thickness//2
        self.radius_outer = radius + thickness//2

    def limit(self, degree):
        return min(360, max(0, degree))

    def color_to_rgb(self, color):
        if isinstance(color, str):
            if color.startswith('#'):
                return tuple(bytes.fromhex(color[1:]))
            elif color in ImageColor.colormap:
                return  ImageColor.getcolor(color, 'RGB')
        elif isinstance(color, tuple):
            return color
        raise ValueError

    def check(self, point):
        if ((-self.half <= point.x <= self.half) and
                -self.half <= point.y <= self.half):
            angle = point.angle
            alpha_1 = 1
            if (self.start_angle-1 < angle < self.stop_angle+1):
                if angle < self.start_angle:
                    alpha_1 = 1 - self.start_angle + angle
                elif angle > self.stop_angle:
                    alpha_1 = 1 - angle + self.stop_angle
            else:
                return
            distance = point.distance
            alpha_2 = 1
            if (self.radius_inner-1 < distance < self.radius_outer+1):
                if distance < self.radius_inner:
                    alpha_2 = 1 - self.radius_inner + distance
                elif distance > self.radius_outer:
                    alpha_2 = 1 - distance + self.radius_outer
            else:
                return
            return int(alpha_1*alpha_2*255)
        return

    @property
    def data(self):
        r, g, b = self.color
        checked = []
        im = Image.new('RGBA', (self.width, self.width), (255, 255, 255, 0))
        radian = self.start_angle/180*math.pi
        to_check = [Point(
            int(self.radius_inner*math.cos(radian)),
            int(self.radius_inner*math.sin(radian)))]
        while to_check:
            tmp = []
            for p in to_check:
                alpha = self.check(p)
                checked.append(p)
                if alpha is not None:
                    x, y = p.x + self.half, self.half-p.y
                    im.putpixel((x, y), (r, g, b, alpha))
                    for dp in (Point(1, 0), Point(-1, 0), Point(0, 1),
                            Point(0, -1)):
                        new_point = p + dp
                        if ((new_point not in checked)
                                and (new_point not in to_check)
                                and (new_point not in tmp)):
                            tmp.append(new_point)
            to_check = list(set(tmp))
        with BytesIO() as output:
            im.save(output, format="PNG")
            data = output.getvalue()
        return data

frames = []
for angle in range(361):
    print(angle)
    frames.append(Arc(stop_angle=angle, color='blue').data)
frames.append(Arc(stop_angle=360, color='yellow').data)

filename = 'D:/arc_png.dat'
with open(filename, 'wb') as file:
    pickle.dump(frames, file)

How a sg.Table with expand_y=True, and a sg.Multiline with expand_x=True and expand_y=True

Before resize,

image

After resized

image

import PySimpleGUI as sg

sg.theme("DarkBlue")
sg.set_options(font=('Courier New', 16))

layout = [
    [sg.Table([[f'Item {i}'] for i in range(10)], headings=["Item"],
        justification='center', auto_size_columns=False, col_widths=[20],
        num_rows=10, key='table'),
     sg.Multiline("", size=(20, 5), key='multiline'),
    ]
]
window = sg.Window("Title", layout, resizable=True, finalize=True)
window['multiline'].expand(expand_x=True, expand_y=True)
window['table'].Widget.pack(fill='y')
window['table'].Widget.master.pack(fill='y', expand=False)

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

window.close()

Window close button X handling

It will generate event 'WIN_CLOSE".

window = sg.Window("Title", layout, finalize=True)
window.TKroot.protocol("WM_DESTROY_WINDOW", lambda:window.write_event_value("WIN_CLOSE", ()))
window.TKroot.protocol("WM_DELETE_WINDOW",  lambda:window.write_event_value("WIN_CLOSE", ()))

Two ways to bind return key for sg.InputText

import PySimpleGUI as sg

layout = [
    [sg.Input('', key='-INPUT1-')],
    [sg.Input('', key='-INPUT2-'), sg.Button('Go', bind_return_key=True, visible=False)],
]

window = sg.Window("Title", layout, finalize=True)
window['-INPUT1-'].bind("<Return>", "Return-")

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

window.close()
-INPUT1-Return- {'-INPUT1-': '', '-INPUT2-': ''} # return key pressed in input 1
Go {'-INPUT1-': '', '-INPUT2-': ''}              # return key pressed in input 2

Using Tabgroup to simulate switching between multiple windows

Following code show how to switch between multiple windows.
Note:

  • No duplicate key allowed in all layouts for windows.
  • It may look not like a tab or tab group, in fact, it is a tab of a tab group.
  • It won't flash when window switching.

tkinter code required

image

from tkinter import ttk
import PySimpleGUI as sg

sg.theme("DarkBlue")
sg.set_options(font=('Courier New', 16))

win_1 = [
    [sg.Text('Name'), sg.Input(key='name')],
    [sg.Text('Password'), sg.Input(key='pass')],
    [sg.Button('Sign In'), sg.Button('Exit')]
]

win_2 = [
    [sg.Text('WELCOME', justification='center', size=(25, 2), pad=(25, 25))],
    [sg.Text('What do you want to do?')],
    [sg.Button('Submit'), sg.Button('Cancel')]
]

tab_group = [
    [sg.Tab("TAB 1", win_1, key="WIN 1")],
    [sg.Tab("TAB 2", win_2, key="WIN 2")],
]
layout = [
    [sg.TabGroup(tab_group, border_width=0, pad=(0, 0), key='TABGROUP')],
]

window = sg.Window("Title", layout, finalize=True)
style = ttk.Style()
style.layout('TNotebook.Tab', [])                           # Hide tab bar
#window['TABGROUP'].Widget.configure(width=600, height=400) # Set size

while True:

    event, values = window.read()
    if event in (sg.WINDOW_CLOSED, "Exit"):
        break
    elif event == "Sign In":
        window["WIN 2"].select()
    elif event == "Cancel":
        window["WIN 1"].select()

window.close()

Get tk.PhotoImage from sg.Image and convert to PIL Image

tk.PhotoImage not just for accessing of image, no image.

If you set image for sg.Image by option data, you can get base64 data back from

image1_base64 = window['-IMAGE1-'].Widget.image.cget('data')
image2_base64 = window['-IMAGE2-'].Widget.image.cget('data')

Convert base64 to PIL Image,

from base64 import b64decode
from io import BytesIO
from PIL import Image

img = b64decode(image1_base64)
buf = BytesIO(img)
im = Image.open(buf)

Not all the same for first argument of update method of all elements

first option is value for sg.Text, sg.Checkbox, sg.InputText, sg.Multiline, and text for sg.Button

def update(key, value):
    element = window[key]
    if isinstance(element, (sg.Text, sg.Checkbox, sg.InputText, sg.Multiline)):
        window[key].update(value=value)
    elif isinstance(element, sg.Button):
        window[key].update(text=value)

User-defined popup

image

import PySimpleGUI as sg

def my_popup(window):
    layout = [
        [sg.Text("Here's the \nuser-defined \npopup !!!")],
        [sg.Button("OK"), sg.Button("CANCEL")]
    ]
    win = sg.Window("My Popup", layout, modal=True,
        grab_anywhere=True, enable_close_attempted_event=True)
    event, value = win.read()
    if event == sg.WINDOW_CLOSE_ATTEMPTED_EVENT:
        event = "CANCEL"
    win.close()
    window.write_event_value(event, None)

sg.theme("DarkBlue")
sg.set_options(font=("Courier New", 16))

layout = [
    [sg.Text("Click following button to open a user-defined popup")],
    [sg.Button("POPUP")],
]
window = sg.Window("Title", layout, finalize=True)

while True:
    event, values = window.read()
    if event == sg.WINDOW_CLOSED:
        break
    elif event == "POPUP":
        my_popup(window)
    elif event in ("OK", "CANCEL"):
        print(event, repr(values[event]))

window.close()

Undo & Redo mechanism for sg.Multiline

No option 'undo' for element sg.Multiline now.

Tkinter code required here

import PySimpleGUI as sg

def redo(event, text):
    try:    # if nothing to redo will cause "_tkinter.TclError: nothing to redo"
        text.edit_redo()
    except:
        pass

layout = [
    [sg.Multiline('', size=(50, 10), key='MULTILINE')]
]

window = sg.Window("Title", layout, finalize=True)
text = window['MULTILINE'].Widget
# Enable the undo mechanism
text.configure(undo=True)
# Bind redo mechanism to key Ctrl-Shift-Z
text.bind('<Control-Shift-Key-Z>', lambda event, text=text:redo(event, text))

while True:
    event, values = window.read()

    if event == sg.WINDOW_CLOSED:
        break

window.close()

Modification for option 'grab_anywhere' of sg.Window

Event binding

Unless you specify otherwise, the bindings happen in the following order:

if there is a binding directly on the widget it will be fired before any other bindings.
if there is a binding on the widget's class, it is fired next
if there is a binding on the toplevel widget that contains the widget, it is fired next (note: the root window is considered a toplevel window in this context)
if there is a binding on "all" it will fire next.

There should be internal binding for 'grab' on some widgets, like scrollbar,.....
They generate events from '<B1-Motion>', '<ButtonRelease-1>', then '<ButtonPress-1>'. So add one variable to confirm '<ButtonPress-1>' should go first, else variable will be None and no grab action.

import tkinter as tk
from tkinter import ttk
import PySimpleGUI as sg

class Window(sg.Window):

    NO_DRAG_WIDGETS = (tk.Button, tk.Entry, tk.Scrollbar, tk.Listbox, tk.Message,
        tk.PanedWindow, tk.Scale, tk.Text, ttk.Entry, ttk.PanedWindow, ttk.Scale,
        ttk.Scrollbar, ttk.Sizegrip, ttk.Treeview, type(None))
    drag_widget = None

    def _StartMove(self, event):
        """
        Used by "Grab Anywhere" style windows. This function is bound to mouse-down. It marks the beginning of a drag.
        :param event: event information passed in by tkinter. Contains x,y position of mouse
        :type event: (event)
        """
        try:
            self.TKroot.x = event.x
            self.TKroot.y = event.y
            self.drag_widget = event.widget
        except:
            pass

    def _StopMove(self, event):
        """
        Used by "Grab Anywhere" style windows. This function is bound to mouse-up. It marks the ending of a drag.
        Sets the position of the window to this final x,y coordinates
        :param event: event information passed in by tkinter. Contains x,y position of mouse
        :type event: (event)
        """

        try:
            self.TKroot.x = event.x
            self.TKroot.y = event.y
            self.drag_widget = None
        except Exception as e:
            print('stop move error', e, event)

    def _OnMotion(self, event):
        """
        Used by "Grab Anywhere" style windows. This function is bound to mouse motion. It actually moves the window
        :param event: event information passed in by tkinter. Contains x,y position of mouse
        :type event: (event)
        """
        try:
            if isinstance(self.drag_widget, Window.NO_DRAG_WIDGETS):
                return
            deltax = event.x - self.TKroot.x
            deltay = event.y - self.TKroot.y
            x = self.TKroot.winfo_x() + deltax
            y = self.TKroot.winfo_y() + deltay
            self.TKroot.geometry("+%s+%s" % (x, y))  # this is what really moves the window
            # print('{},{}'.format(x,y))

            if Window._move_all_windows:
                for window in Window._active_windows:
                    x = window.TKroot.winfo_x() + deltax
                    y = window.TKroot.winfo_y() + deltay
                    window.TKroot.geometry("+%s+%s" % (x, y))  # this is what really moves the window
        except Exception as e:
            pass


headings = ['President', 'Date of Birth']
data = [
    ['Ronald Reagan', 'February 6'],
    ['Abraham Lincoln', 'February 12'],
    ['George Washington', 'February 22'],
    ['Andrew Jackson', 'March 15'],
    ['Thomas Jefferson', 'April 13'],
    ['Harry Truman', 'May 8'],
    ['John F. Kennedy', 'May 29'],
    ['George H. W. Bush', 'June 12'],
    ['George W. Bush', 'July 6'],
    ['John Quincy Adams', 'July 11'],
    ['Garrett Walker', 'July 18'],
    ['Bill Clinton', 'August 19'],
    ['Jimmy Carter', 'October 1'],
    ['John Adams', 'October 30'],
    ['Theodore Roosevelt', 'October 27'],
    ['Frank Underwood', 'November 5'],
    ['Woodrow Wilson', 'December 28'],
]

layout = [
    [sg.Text("President to search:")],
    [sg.Input(size=(33, 1), key='-INPUT-'), sg.Button('Search')],
    [sg.Table(data, headings=headings, justification='left', key='-TABLE-')],
]
window = Window("Title", layout, grab_anywhere=True, finalize=True)
table = window['-TABLE-']
entry = window['-INPUT-']
entry.bind('<Return>', 'RETURN-')
widget = table.Widget

while True:
    event, values = window.read()
    if event == sg.WINDOW_CLOSED:
        break
    # print(event, values)
    if event in ('Search', '-INPUT-RETURN-'):
        text = values['-INPUT-'].lower()
        if text == '':
            continue
        row_colors = []
        for row, row_data in enumerate(data):
            if text in row_data[0].lower():
                row_colors.append((row, 'green'))
            else:
                row_colors.append((row, sg.theme_background_color()))
        table.update(row_colors=row_colors)
        for iid in widget.get_children():
            print(iid, widget.item(iid))

window.close()

Remove outline of button with PNG image

import PySimpleGUI as sg


layout = [
    [sg.Button('Guardar', key='INPUT_1', image_filename='button.png',
        border_width=0, button_color=('black', sg.theme_background_color()))],
]
window = sg.Window("Title", layout, finalize=True)

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

window.close()

Change checkbox to button and set select color

No option indicatoron for checkbox or button used as indicator now, so tkinter core required.
The same for option selectcolor for background color of selected item, 'anchor' for position of text.

image

tkinter code required,

import PySimpleGUI as sg

items = [
    "Apple", "Apricots", "Avocado", "Banana", "Blackberries", "Blackcurrant",
    "Blueberries", "Breadfruit", "Cantaloupe", "Carambola", "Cherimoya",
    "Cherries", "Clementine", "Cranberries", "Date Fruit", "Durian",
    "Elderberries", "Feijoa", "Figs", "Gooseberries", "Grapefruit",
    "Grapes", "Guava", "Jackfruit", "Java-Plum", "Kiwifruit", "Kumquat",
    "Lemon", "Longan", "Loquat", "Lychee", "Mandarin", "Mango", "Mangosteen",
    "Mulberries", "Nectarine", "Olives", "Orange", "Papaya", "Peaches", "Pear",
    "Persimmon", "Pitaya", "Pineapple", "Pitanga", "Plantain", "Plums",
    "Pomegranate", "Prunes", "Pummelo", "Quince", "Raspberries", "Rhubarb",
    "Sapodilla", "Soursop", "Strawberries", "Tamarind", "Tangerine",
    "Watermelon",
]

sg.theme('DarkBlue')

width   = 80
size    = (max(map(len, items)), 1)
columns = int(width/size[0])
layout = [[sg.Text("Check below items"), sg.Button("Done")]]
row = []
for i, item in enumerate(items):
    row.append(sg.Checkbox(item, size=size, key=f'Item {i}'))
    j = i + 1
    if j % columns == 0:
        layout.append(row)
        row = []
if row != []:
    layout.append(row)

window = sg.Window("Title", layout, finalize=True)
length = len(items)

for i in range(length):
    window[f'Item {i}'].Widget.configure(indicatoron=False, selectcolor='blue',  # tkinter code required here
        anchor='center')

while True:
    event, values = window.read()
    #print(event, values)
    if event == sg.WINDOW_CLOSED:
        break
    elif event == 'Done':
        checked = [items[i] for i in range(length) if values[f'Item {i}']]
        print(checked)

window.close()
['Apple', 'Breadfruit', 'Cantaloupe', 'Kumquat', 'Lemon']

Create a new element Gauge by using sg.Graph

image

import math
import random
import PySimpleGUI as sg

class Gauge(sg.Graph):
    """
    Guage same as sg.Graph,
        but with three extra arguments, radius, value and gap
        gap for internal padding

    1. Set gauge = windwo[key], where key is '-Gauge-' defined by yourself.
    2. Call gauge.new() after window finalize to get clock drawn.
    3. Call gauge.change(value=0, step=1) to set initial values
       value as target, step for rotate step, lower step means slow
    4. Call guage.change() to rotate pointer by one step
       return True if pointer already arrive value set in step 3.
    5. Call guage.goto(value) to rotate pointer to value directly.
    6. value not defined in then range 0 to 100, type is float or int.
    """
    def __new__(cls, radius=100, value=0, gap=5, **kwargs):
        """
        To define Guage same as sg.Graph
        """
        return sg.Graph.__new__(cls)

    def __init__(self, radius=100, value=0, gap=5, **kwargs):
        """
        canvas_size, graph_bottom_left, graph_top_right in sg.Graph
        supplied automatically here.
        :param radius: radius of Guage
        :type  radius: int
        :param value: value of pointer, from 0 to 100
        :type  value: int | float
        :param gap: internal padding of Graph
        :type gap: int
        :param kwargs: refer sg.Graph, arguments for canvas_size, graph_bottom_left, graph_top_right are not necessary.
        :type kwargs: dict
        """
        self.radius             = radius
        self.value              = value
        self.pointer_stop       = 0
        self.pointer_step       = 1
        self.start, self.stop   = 0, 100
        self.minor_tick_step    = 5
        self.major_tick_step    = 10
        self.figure             = []
        self.figure_pointer     = []
        self.gap                = gap
        self.element_color()
        self.element_radius()
        self.element_width()
        super().__init__((2*radius+2*gap, int(1.2*radius+2*gap)),
            (-radius-gap, int(-0.2*radius-gap)), (radius+gap, radius+gap),
            **kwargs)

    def new(self):
        """
        Draw Gauge
        """
        self.new_clock()
        self.new_minor_tick()
        self.new_major_tick()
        self.new_pointer()

    def element_color(self):
        """
        Colors of all elements in Guage defined here,
        you can update them directly
        """
        self.clock_color            = None
        self.clock_line_color       = sg.theme_text_color()
        self.minor_tick_color       = sg.theme_input_background_color()
        self.major_tick_color       = sg.theme_text_color()
        self.pointer_outer_color    = sg.theme_text_color()
        self.pointer_color          = sg.theme_text_color()
        self.origin_outer_color     = sg.theme_text_color()
        self.origin__color          = sg.theme_text_color()

    def element_radius(self):
        """
        Size of all elements in Guage defined here,
        you can update them directly
        """
        self.clock_radius           = self.radius
        self.minor_tick_radius      = 0.9*self.radius
        self.major_tick_radius      = 0.8*self.radius
        self.pointer_inner_radius   = 0.1*self.radius
        self.pointer_outer_radius   = 0.7*self.radius

    def element_width(self):
        """
        Line width of all elements in Guage defined here,
        you can update them directly
        """
        self.clock_width            = 1
        self.minor_tick_width       = 1
        self.major_tick_width       = 2
        self.pointer_width          = 1
        self.origin_width           = 2

    def new_clock(self):
        """
        Draw clock
        """
        self.figure.append(self.draw_arc((-self.radius, +self.radius),
            (+self.radius, -self.radius), 180, 0, style='arc',
            arc_color=self.clock_line_color, fill_color=self.clock_color,
            line_width=self.clock_width))


    def new_minor_tick(self):
        """
        Draw minor tick
        """
        self.new_tick(self.minor_tick_radius, self.minor_tick_color,
            self.minor_tick_width, self.minor_tick_step)

    def new_major_tick(self):
        """
        Draw major tick
        """
        self.new_tick(self.major_tick_radius, self.major_tick_color,
            self.major_tick_width, self.major_tick_step)

    def new_tick(self, inner_radius, color, width, step):
        """
        Draw tick
        :param inner_radius: distance from center to tick
        :type inner_radius: int
        :param color: line color of tick
        :type color: tkinter color representation
        :param width: line width of tick
        :type width: int
        :param step: how many differences in value for one tick, must > 0
        :type step: int
        """
        for i in range(0, 101, step):
            j = (100 - i)/100*math.pi
            start_x = inner_radius*math.cos(j)
            start_y = inner_radius*math.sin(j)
            stop_x  = self.radius*math.cos(j)
            stop_y  = self.radius*math.sin(j)
            self.figure.append(self.draw_line((start_x, start_y),
                (stop_x, stop_y), color=color, width=width))

    def new_pointer(self):
        """
        Draw pointer
        """
        if self.figure_pointer:
            for figure in self.figure_pointer:
                self.DeleteFigure(figure)
            self.figure_pointer = []
        d = self.value*1.8 - 90
        dx1 = int(2*self.pointer_inner_radius*math.sin(d/180*math.pi))
        dy1 = int(2*self.pointer_inner_radius*math.cos(d/180*math.pi))
        dx2 = int(self.pointer_outer_radius*math.sin(d/180*math.pi))
        dy2 = int(self.pointer_outer_radius*math.cos(d/180*math.pi))
        self.figure_pointer.append(self.draw_line(
            (-dx1, -dy1), (+dx2, +dy2), color=self.pointer_color,
            width=self.pointer_width))
        self.figure_pointer.append(self.draw_circle(
            (0, 0), self.pointer_inner_radius, fill_color=self.origin__color,
            line_color=self.origin_outer_color, line_width=self.origin_width))

    def change(self, value=None, step=1):
        """
        Rotation of pointer
        call it with degree and step to set initial options for rotation.
        Without any option to start rotation.
        :param value: value of pointer, from 0 to 100
        :type value: int | float
        :param step: step for rotate step, lower step means slow, must > 0
        :type step: int
        """
        if value != None:
            self.pointer_stop = value
            self.pointer_step = step if self.value <= value else -step
            return True
        now = self.value
        step = self.pointer_step
        new_value = now + step
        if ((step > 0 and new_value < self.pointer_stop) or
            (step < 0 and new_value > self.pointer_stop)):
                self.value = new_value
                self.new_pointer()
                return False
        else:
            self.value = self.pointer_stop
            self.new_pointer()
            return True

    def goto(self, value=0):
        """
        Rotate pointer to value directly
        :param value: value of pointer, from 0 to 100
        :type value: int | float
        """
        self.change(value=value, step=abs(self.value-value))
        self.change()

def column(row, col):
    layout_column = [
        [Gauge(radius=50, gap=2, pad=(0, 0), key=f'Gauge ({row},{col})')],
        [sg.Text(size=(10, 1), pad=(3, 0), justification='center', key=f'Value ({row},{col})')],
    ]
    return sg.Column(layout_column, pad=(0, 0))

sg.theme('DarkPurple6')
sg.set_options(font=('Courier New', 14))

rows, cols = 3, 5

layout = [
    [sg.Button('Reset', size=(6, 1),  key='Reset')]] + [
    [column(row, col) for col in range(cols)] for row in range(rows)
]
window = sg.Window('Meter', layout=layout, finalize=True,
    element_justification='c')

for row in range(rows):
    for col in range(cols):
        window[f'Gauge ({row},{col})'].new()
        window[f'Gauge ({row},{col})'].change(value=0, step=2)

while True:

    event, values = window.read(timeout=100)

    if event in ('Quit', sg.WIN_CLOSED):
        break

    if event == 'Reset':
        new_value = 0
        for row in range(rows):
            for col in range(cols):
                window[f'Gauge ({row},{col})'].goto(new_value)

    for row in range(rows):
        for col in range(cols):
            if window[f'Gauge ({row},{col})'].change():  # check if target arrived
                new_value = random.randint(0, 1000)/10
                window[f'Value ({row},{col})'].update(f'{new_value}%')
                window[f'Gauge ({row},{col})'].change(value=new_value, step=10)

window.close()

Selection in Tree

image

tkinter code required for "UpdateSelections"

from random import choice
import PySimpleGUI as sg

def all_nodes(item=''):
    children = tree.get_children(item)
    all_items = list(children).copy()
    for child in children:
        all_items.extend(all_nodes(child))
    return all_items

def expand_all():
    for key in all_nodes(''):
        tree.item(key, open=True)

treedata = sg.TreeData()

for i in range(10):
    parent = choice(list(treedata.tree_dict.keys()))
    treedata.insert(parent, i, f'Node {i}', values=[f'Data {i}'])

layout = [
    [sg.Tree(data=treedata, headings=['Data', ], auto_size_columns=True,
        num_rows=10, col0_width=40, key='-TREE-', show_expanded=False,
        enable_events=True),],
    [sg.Button('Select All'), sg.Button('Select All Top'),
     sg.Button('Select None'), sg.Button('Cancel')]]

window = sg.Window('Tree Element Test', layout, finalize=True)
tree = window['-TREE-'].Widget
expand_all()

while True:
    event, values = window.read()
    if event in (sg.WIN_CLOSED, 'Cancel'):
        break
    if event == 'Select All Top':
        children = tree.get_children('')
        tree.selection_set(children)
    elif event == 'Select All':
        children = all_nodes('')
        tree.selection_set(children)
    elif event == 'Select None':
        tree.selection_set([])

window.close()

Using Tree for Table with checkbox

image

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

def icon(check):
    box = (32, 32)
    background = (255, 255, 255, 0)
    rectangle = (3, 3, 29, 29)
    line = ((9, 17), (15, 23), (23, 9))
    im = Image.new('RGBA', box, background)
    draw = ImageDraw.Draw(im, 'RGBA')
    draw.rectangle(rectangle, outline='black', width=3)
    if check == 1:
        draw.line(line, fill='black', width=3, joint='curve')
    elif check == 2:
        draw.line(line, fill='grey', width=3, joint='curve')
    with BytesIO() as output:
        im.save(output, format="PNG")
        png = output.getvalue()
    return png

check = [icon(0), icon(1), icon(2)]

headings = ['President', 'Date of Birth', '1', '2', '3']
data = [
    ['Ronald Reagan', 'February 6'],
    ['Abraham Lincoln', 'February 12'],
    ['George Washington', 'February 22'],
    ['Andrew Jackson', 'March 15'],
    ['Thomas Jefferson', 'April 13'],
    ['Harry Truman', 'May 8'],
    ['John F. Kennedy', 'May 29'],
    ['George H. W. Bush', 'June 12'],
    ['George W. Bush', 'July 6'],
    ['John Quincy Adams', 'July 11'],
    ['Garrett Walker', 'July 18'],
    ['Bill Clinton', 'August 19'],
    ['Jimmy Carter', 'October 1'],
    ['John Adams', 'October 30'],
    ['Theodore Roosevelt', 'October 27'],
    ['Frank Underwood', 'November 5'],
    ['Woodrow Wilson', 'December 28'],
]

treedata = sg.TreeData()
for president, birthday in data:
    treedata.Insert('', president, president, values=[birthday]+[1,2,3],
    icon=check[0])

sg.theme('LightPurple')
sg.set_options(font=('Helvetica', 16))
layout = [
    [sg.Tree(data=treedata, headings=headings[1:], auto_size_columns=True,
        num_rows=10, col0_width=20, key='-TREE-', row_height=48, metadata=[],
        show_expanded=False, enable_events=True,
        select_mode=sg.TABLE_SELECT_MODE_BROWSE)],
    [sg.Button('Quit')]
]
window = sg.Window('Tree as Table', layout, finalize=True)
tree = window['-TREE-']
tree.Widget.heading("#0", text=headings[0]) # Set heading for column #0

while True:
    event, values = window.read()
    if event in (sg.WIN_CLOSED, 'Quit'):
        break
    elif event == '-TREE-':
        president = values['-TREE-'][0]
        print(president)
        if president in tree.metadata:
            tree.metadata.remove(president)
            tree.update(key=president, icon=check[0])
        else:
            tree.metadata.append(president)
            tree.update(key=president, icon=check[1])

window.close()

How to show image file by using sg.Image and fit center of sg.Image

image

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

def update_listbox(listbox_element, folder, extension, substring):
    path = Path(folder)
    filter_ = substring.lower()
    lst = []
    if folder != '' and path.is_dir():
        files = list(path.glob("*.*"))
        lst = [file for file in files if file.suffix.lower() in extension
            and filter_ in str(file).lower() and file.is_file()]
    listbox_element.update(lst)

def update_image(image_element, filename):
    im = Image.open(filename)
    w, h = size_of_image
    scale = max(im.width/w, im.height/h)
    if scale <= 1:
        image_element.update(filename=filename)
    else:
        im = im.resize((int(im.width/scale), int(im.height/scale)),
            resample=Image.CUBIC)
        with BytesIO() as output:
            im.save(output, format="PNG")
            data = output.getvalue()
        image_element.update(data=data)

sg.theme('Dark')
sg.set_options(font=('Courier New', 11))

w, h = size_of_image = (700, 600)

layout_top = [
    [sg.InputText(enable_events=True, key='-FOLDER-'),
     sg.FolderBrowse('Browse', size=(7, 1), enable_events=True)],
    [sg.InputText(enable_events=True, key='-FILTER-'),
     sg.Button('Search', size=(7, 1))],
]
layout_bottom = [
    [sg.Listbox([], size=(52, 30), enable_events=True,
        select_mode=sg.LISTBOX_SELECT_MODE_SINGLE, key='-LISTBOX-')],
]
layout_left = [
    [sg.Column(layout_top, pad=(0, 0))],
    [sg.Column(layout_bottom, pad=(0, 0))],
]
layout_right = [[sg.Image(background_color='green', key='-IMAGE-')]]
layout = [
    [sg.Column(layout_left), sg.Column(layout_right, pad=(0, 0), size=(w+15, h+15), background_color='blue', key='-COLUMN-')],
]

window = sg.Window("PNG/GIF Viewer", layout, finalize=True)
window['-IMAGE-'].Widget.pack(fill='both', expand=True)
window['-IMAGE-'].Widget.master.pack(fill='both', expand=True)
window['-IMAGE-'].Widget.master.master.pack(fill='both', expand=True)
window['-COLUMN-'].Widget.pack_propagate(0)

while True:
    event, values = window.read()
    if event == sg.WINDOW_CLOSED:
        break
    # print(event, values)
    if event in ('-FOLDER-', '-FILTER-', 'Search'):
        update_listbox(window['-LISTBOX-'], values['-FOLDER-'],
            ('.png', '.gif'), values['-FILTER-'])
    elif event == '-LISTBOX-':
        lst = values['-LISTBOX-']
        if lst != []:
            update_image(window['-IMAGE-'], values['-LISTBOX-'][0])

window.close()

Options target and enable_events for sg.Button or sg.FolderBrowse

For example, using sg.FolderBrowse here.

The option target is default as (sg.ThisRow, -1), means element just before your sg.FolderBrowse at same row.

If there's no target in your layout, no event will be generated.
If there's target, event of target will be generated only when target with option enable_events=True.

So, you should have a target element in your layout, most of time, it is sg.InputText with option enable_events=True.

image

import PySimpleGUI as sg

layout = [
    [sg.InputText(enable_events=True), sg.FolderBrowse('Select folder...')],
]

window = sg.Window("Title", layout, finalize=True)

while True:
    event, values = window.read()
    if event == sg.WINDOW_CLOSED:
        break
    print(event, values)
    if values['Select folder...'] != '':
        print(values['Select folder...'])

window.close()
0 {0: 'D:/Document', 'Select folder...': 'D:/Document'}
D:/Document

There'll be problem if any event generated from `sg.InputText`, like key 'A' pressed. That's not the event you need, and maybe neither element `sg.InputText`. So next example shown only `sg.FolderBrowse`.

image

import PySimpleGUI as sg

layout = [
    [sg.InputText(disabled=True, visible=False, enable_events=True), sg.FolderBrowse('Select folder...')],
]

window = sg.Window("Title", layout, finalize=True)

while True:
    event, values = window.read()
    if event == sg.WINDOW_CLOSED:
        break
    print(event, values)
    if values['Select folder...'] != '':
        print(values['Select folder...'])

window.close()
0 {0: 'D:/Document', 'Select folder...': 'D:/Document'}
D:/Document

Refresh window after element update

GUI not update after element update when not window.read called, can be done by call window.refresh

    element.update(option=new_value)
    window.refresh()

Simulate a Table by using sg.Text

My case is to simulate a table by using text elements, also with vertical/horizontal scrollbar by slider element.
Mouse moved to any element in this table and wheel up/down without focus here.

Here's my script, but not with few tkinter code for

  • Set takefocus=0 of text element to remove focus hightlight dash box.
  • Bind all elements of table for events <MouseHweel> and <Shift-MouseWheel> to vertical/horizontal scroll callbacks.
    It work well, may change it to all PSG code without tkinter code if above two conditions implemented in PSG.

image

import PySimpleGUI as sg

def scroll(direction):
    for y in range(rows):
        for x in range(cols):
            if ((x == 0 and y == 0) or (direction == 'V' and y == 0) or
                (direction == 'H' and x == 0)):
                continue
            elif direction == 'V' and x == 0:
                window[str((y, 0))].update(data[y+y0-1][0])
            elif direction == 'H' and y == 0:
                window[str((0, x))].update(data[0][x+x0-1])
            else:
                window[str((y, x))].update(data[y+y0-1][x+x0-1])

def hscroll(event):
    global x0
    delta = int(event.delta/120)
    x0 = min(max(1, x0-delta), headers-cols+1)
    scroll('H')
    window['H_Scrollbar'].update(value=x0)
    window.refresh()

def vscroll(event):
    global y0
    delta = int(event.delta/120)
    y0 = min(max(1, y0-delta), size-rows+1)
    scroll('V')
    window['V_Scrollbar'].update(value=size-rows-y0+2)
    window.refresh()

def create_data(headers, size):
    data = [[f'({row}, {col})' for col in range(headers)]
        for row in range(size)]
    data[0] = [f'Column {col}' for col in range(headers)]
    for row in range(size):
        data[row][0] = f'Row {row}'
    data[0][0] = 'Features'
    return data

headers = 20
size = 50
data = create_data(headers, size)

gap = 2
cols, rows = 5, 15
x0, y0 = 1, 1

sg.theme('Darkgrey')

table = []
for y in range(0, rows):
    line = []
    for x in range(0, cols):
        x_pad = (gap, gap) if x==cols-1 else (gap, 0)
        y_pad = (gap, gap) if y==rows-1 else (gap, 0)
        pad = (x_pad, y_pad)
        bg = 'green' if (x == 0 or y == 0) else 'blue'
        line.append(
            sg.Text(data[y][x], size=(10, 1), pad=pad, justification='c',
                text_color='white', background_color=bg, key=str((y, x)))
         )
    table.append(line)

option2 = {'resolution':1, 'pad':(0, 0), 'disable_number_display':True,
    'enable_events':True}
layout = [
    [sg.Column(table, background_color='black', pad=(0, 0), key='Table'),
     sg.Slider(range=(1, size-rows+1),    size=(18, 20), orientation='v',
        **option2, key='V_Scrollbar', expand_y=True)],
    [sg.Slider(range=(1, headers-cols+1), size=(46, 20), orientation='h',
        **option2, key='H_Scrollbar', expand_x=True)]]

window = sg.Window('Simulate Table', layout, use_default_focus=False,
    finalize=True)

for y in range(rows):
    for x in range(cols):
        element = window[str((y, x))]
        element.Widget.configure(takefocus=0)
        element.Widget.bind('<MouseWheel>', vscroll)
        element.Widget.bind('<Shift-MouseWheel>', hscroll)
        element.ParentRowFrame.bind('<MouseWheel>', vscroll)
        element.ParentRowFrame.bind('<Shift-MouseWheel>', hscroll)

window['V_Scrollbar'].Update(value=size-rows+1)
window['V_Scrollbar'].Widget.bind('<MouseWheel>', vscroll)
window['H_Scrollbar'].Widget.bind('<Shift-MouseWheel>', hscroll)

while True:

    event, values = window.read()
    if event == sg.WINDOW_CLOSED:
        break
    elif event == 'V_Scrollbar':
        y0 = size - rows - int(values['V_Scrollbar']) + 2
        scroll('V')
    elif event == 'H_Scrollbar':
        x0 = int(values['H_Scrollbar'])
        scroll('H')

window.close()

Request - function to help manipulate Menu variables

It is easy for people to make a Menu in PySimpleGUI.

    menu_def = [['&File', ['&Open     Ctrl-O', '&Save       Ctrl-S', '&Properties', 'E&xit']],
                ['&Edit', [['Special', 'Normal',['Normal1', 'Normal2'] ], 'Undo'], ],
                ['&Toolbar', ['---', 'Command &1::Command_Key', 'Command &2', '---', 'Command &3', 'Command &4']],
                ['&Help', ['&About...']], ]

But people complain that they don't understand / have a way to Disable an item.

A function could search this data structure and change the value / values.

I don't know what operations to offer other than enable/disable.

Can you write a function to help users?

Simulation for Tab with close button by new window

image

import ctypes
from pathlib import Path
import PySimpleGUI as sg

class Tab():

    tabs = {}
    keys = []
    now = None

    def __init__(self, filename=''):
        if filename == '':
            tab = 'Untitled'
            text = ''
            key = '0'
        else:
            tab = Path(filename).name
            try:
                text = open(file, 'rt').read()
            except:
                text = ''
            if '0' in Tab.keys:
                del Tab.tabs['0']
                Tab.keys.remove('0')
            key = self.get_new_key()
        Tab.keys.append(key)
        Tab.tabs[key] = {'tab':tab, 'text':text}
        Tab.now = key

    def layout_frame(self, key):
        tab = Tab.tabs[key]['tab']
        layout_column = [
            [sg.Text(tab, enable_events=True, key=f'File {key}'),
             sg.Button("X", pad=(0, 0), border_width=0, key=f'Close {key}',
                button_color=("red", sg.theme_background_color()),
                font=('Arial', 9, 'bold'))],
        ]
        return [[sg.Column(layout_column, pad=(0, 0), key=f'Column {key}')]]

    def layout(self):
        menu_def = [['&File', ['&Open', 'E&xit']]]
        text = Tab.tabs[Tab.now]['text'] if Tab.now else ''
        option = {'default_text':text, 'key':'Multiline', 'size':(130, 35)}
        return [
            [sg.Menu(menu_def),],
            [sg.Frame("", self.layout_frame(key), pad=(0, 0)) for key in Tab.keys],
            [sg.Multiline(**option),],
        ]

    def get_new_key(self):
        i = 1
        while str(i) in Tab.tabs:
            i += 1
        return str(i)

    def tab_color(window, key, color):
        window[f'Close {key}'].update(button_color=('white', color))
        window[f'File {key}'].update(background_color=color)

def new_window(window):
    win = sg.Window("Title", tab.layout(), use_default_focus=False,
        size=(1600, 800), element_padding=(0, 0), finalize=True)
    for key in Tab.keys:
        win[f'Close {key}'].Widget.configure(takefocus=0)
    if window:
        window.close()
    if Tab.now:
        Tab.tab_color(win, Tab.now, 'blue')
    win.force_focus()
    return win

ctypes.windll.user32.SetProcessDPIAware()   # Set unit of GUI to pixels

sg.theme("DarkBlue")
sg.set_options(font=('Courier New', 12))
tab = Tab()
window = new_window(None)

while True:
    event, values = window.read()
    if event == sg.WINDOW_CLOSED:
        break
    elif event == 'Open':
        file = sg.popup_get_file("", no_window=True, keep_on_top=True)
        if Path(file).is_file():
            Tab(file)
            window = new_window(window)
    elif event.startswith("Close"):
        key = event.split(" ")[1]
        Tab.keys.remove(key)
        del Tab.tabs[key]
        Tab.now = Tab.keys[-1] if Tab.keys else None
        window = new_window(window)
        if Tab.now:
            Tab.tab_color(window, Tab.now, 'blue')
    elif event.startswith("File"):
        key = event.split(" ")[1]
        Tab.tab_color(window, Tab.now, sg.theme_background_color())
        Tab.now = key
        Tab.tab_color(window, Tab.now, 'blue')
        window["Multiline"].update(value=Tab.tabs[key]['text'])

    print(event)

window.close()

Request - Add parms, change interface a little on your Gauge

I modified your Gauge program.

I now can collapse the Class Gauge in PyCharm so that it's 1 line of code that I copy.

Adding it to my PySimpleGUI code is easy.

USING the gauge I struggle with. I don't understand the order of the calls to change, etc.

This program makes this GUI:

image

It is in the Demo Programs folder.

Can you maybe add the parms like I did and make the interface a little different / less confusing?

import PySimpleGUI as sg
import psutil
import sys
import math

"""
    Another simple Desktop Widget using PySimpleGUI
    This time a CPU Usage indicator using a custom Gauge element
    
    The Gauge class was developed by the brilliant PySimpleGUI user and support-helper @jason990420 
    It has been hacked on a bit, had classes and functions moved around.  It could be cleaned up some
    but it's "good enough" at this point to release as a demo.
    
    This is a good example of how you can use Graph Elements to create your own custom elements.
    This Gauge element is created from a Graph element. 

    Copyright 2020 PySimpleGUI.org
"""

class Gauge():
    def mapping(func, sequence, *argc):
        """
        Map function with extra argument, not for tuple.
        : Parameters
          func - function to call.
          sequence - list for iteration.
          argc - more arguments for func.
        : Return
          list of func(element of sequence, *argc)
        """
        if isinstance(sequence, list):
            return list(map(lambda i: func(i, *argc), sequence))
        else:
            return func(sequence, *argc)

    def add(number1, number2):
        """
        Add two number
        : Parameter
          number1 - number to add.
          numeer2 - number to add.
        : Return
          Addition result for number1 and number2.
        """
        return number1 + number1

    def limit(number):
        """
        Limit angle in range 0 ~ 360
        : Parameter
          number: angle degree.
        : Return
          angel degree in 0 ~ 360, return 0 if number < 0, 360 if number > 360.
        """
        return max(min(360, number), 0)
    class Clock():
        """
        Draw background circle or arc
        All angles defined as clockwise from negative x-axis.
        """

        def __init__(self, center_x=0, center_y=0, radius=100, start_angle=0,
                     stop_angle=360, fill_color='white', line_color='black', line_width=2, graph_elem=None):

            instance = Gauge.mapping(isinstance, [center_x, center_y, radius, start_angle,
                                            stop_angle, line_width], (int, float)) + Gauge.mapping(isinstance,
                                                                                             [fill_color, line_color], str)
            if False in instance:
                raise ValueError
            start_angle, stop_angle = Gauge.limit(start_angle), Gauge.limit(stop_angle)
            self.all = [center_x, center_y, radius, start_angle, stop_angle,
                        fill_color, line_color, line_width]
            self.figure = []
            self.graph_elem = graph_elem
            self.new()

        def new(self):
            """
            Draw Arc or circle
            """
            x, y, r, start, stop, fill, line, width = self.all
            start, stop = (180 - start, 180 - stop) if stop < start else (180 - stop, 180 - start)
            if start == stop % 360:
                self.figure.append(self.graph_elem.DrawCircle((x, y), r, fill_color=fill,
                                                              line_color=line, line_width=width))
            else:
                self.figure.append(self.graph_elem.DrawArc((x - r, y + r), (x + r, y - r), stop - start,
                                                           start, style='arc', arc_color=fill))

        def move(self, delta_x, delta_y):
            """
            Move circle or arc in clock by delta x, delta y
            """
            if False in Gauge.mapping(isinstance, [delta_x, delta_y], (int, float)):
                raise ValueError
            self.all[0] += delta_x
            self.all[1] += delta_y
            for figure in self.figure:
                self.graph_elem.MoveFigure(figure, delta_x, delta_y)

    class Pointer():
        """
        Draw pointer of clock
        All angles defined as clockwise from negative x-axis.
        """

        def __init__(self, center_x=0, center_y=0, angle=0, inner_radius=20,
                     outer_radius=80, outer_color='white', pointer_color='blue',
                     origin_color='black', line_width=2, graph_elem=None):

            instance = Gauge.mapping(isinstance, [center_x, center_y, angle, inner_radius,
                                            outer_radius, line_width], (int, float)) + Gauge.mapping(isinstance,
                                                                                               [outer_color, pointer_color, origin_color], str)
            if False in instance:
                raise ValueError

            self.all = [center_x, center_y, angle, inner_radius, outer_radius,
                        outer_color, pointer_color, origin_color, line_width]
            self.figure = []
            self.stop_angle = angle
            self.graph_elem = graph_elem
            self.new(degree=angle)

        def new(self, degree=0):
            """
            Draw new pointer by angle, erase old pointer if exist
            degree defined as clockwise from negative x-axis.
            """
            (center_x, center_y, angle, inner_radius, outer_radius,
             outer_color, pointer_color, origin_color, line_width) = self.all
            if self.figure != []:
                for figure in self.figure:
                    self.graph_elem.DeleteFigure(figure)
                self.figure = []
            d = degree - 90
            self.all[2] = degree
            dx1 = int(2 * inner_radius * math.sin(d / 180 * math.pi))
            dy1 = int(2 * inner_radius * math.cos(d / 180 * math.pi))
            dx2 = int(outer_radius * math.sin(d / 180 * math.pi))
            dy2 = int(outer_radius * math.cos(d / 180 * math.pi))
            self.figure.append(self.graph_elem.DrawLine((center_x - dx1, center_y - dy1),
                                                        (center_x + dx2, center_y + dy2),
                                                        color=pointer_color, width=line_width))
            self.figure.append(self.graph_elem.DrawCircle((center_x, center_y), inner_radius,
                                                          fill_color=origin_color, line_color=outer_color, line_width=line_width))

        def move(self, delta_x, delta_y):
            """
            Move pointer with delta x and delta y
            """
            if False in Gauge.mapping(isinstance, [delta_x, delta_y], (int, float)):
                raise ValueError
            self.all[:2] = [self.all[0] + delta_x, self.all[1] + delta_y]
            for figure in self.figure:
                self.graph_elem.MoveFigure(figure, delta_x, delta_y)

    class Tick():
        """
        Create tick on click for minor tick, also for major tick
        All angles defined as clockwise from negative x-axis.
        """

        def __init__(self, center_x=0, center_y=0, start_radius=90, stop_radius=100,
                     start_angle=0, stop_angle=360, step=6, line_color='black', line_width=2, graph_elem=None):

            instance = Gauge.mapping(isinstance, [center_x, center_y, start_radius,
                                            stop_radius, start_angle, stop_angle, step, line_width],
                               (int, float)) + [Gauge.mapping(isinstance, line_color, (list, str))]
            if False in instance:
                raise ValueError
            start_angle, stop_angle = Gauge.limit(start_angle), Gauge.limit(stop_angle)
            self.all = [center_x, center_y, start_radius, stop_radius,
                        start_angle, stop_angle, step, line_color, line_width]
            self.figure = []
            self.graph_elem = graph_elem

            self.new()

        def new(self):
            """
            Draw ticks on clock
            """
            (x, y, start_radius, stop_radius, start_angle, stop_angle, step,
             line_color, line_width) = self.all
            start_angle, stop_angle = (180 - start_angle, 180 - stop_angle
                                       ) if stop_angle < start_angle else (180 - stop_angle, 180 - start_angle)
            for i in range(start_angle, stop_angle + 1, step):
                start_x = x + start_radius * math.cos(i / 180 * math.pi)
                start_y = y + start_radius * math.sin(i / 180 * math.pi)
                stop_x = x + stop_radius * math.cos(i / 180 * math.pi)
                stop_y = y + stop_radius * math.sin(i / 180 * math.pi)
                self.figure.append(self.graph_elem.DrawLine((start_x, start_y),
                                                            (stop_x, stop_y), color=line_color, width=line_width))

        def move(self, delta_x, delta_y):
            """
            Move ticks by delta x and delta y
            """
            if False in Gauge.mapping(isinstance, [delta_x, delta_y], (int, float)):
                raise ValueError
            self.all[0] += delta_x
            self.all[1] += delta_y
            for figure in self.figure:
                self.graph_elem.MoveFigure(figure, delta_x, delta_y)

    """
    Create Gauge
    All angles defined as count clockwise from negative x-axis.
    Should create instance of clock, pointer, minor tick and major tick first.
    """
    def __init__(self, center=(0, 0), start_angle=0, stop_angle=180, major_tick_width=5, minor_tick_width=2,major_tick_start_radius=90, major_tick_stop_radius=100, major_tick_step=30, clock_radius=100, pointer_line_width=5, pointer_inner_radius=10, pointer_outer_radius=75, pointer_color='white', pointer_origin_color='black', pointer_outer_color='white', pointer_angle=0, degree=0, clock_color='white', major_tick_color='black', minor_tick_color='black', minor_tick_start_radius=90, minor_tick_stop_radius=100, graph_elem=None):

        self.clock = Gauge.Clock(start_angle=start_angle, stop_angle=stop_angle, fill_color=clock_color, radius=clock_radius, graph_elem=graph_elem)
        self.minor_tick = Gauge.Tick(start_angle=start_angle, stop_angle=stop_angle, line_width=minor_tick_width, line_color=minor_tick_color, start_radius=minor_tick_start_radius, stop_radius=minor_tick_stop_radius, graph_elem=graph_elem)
        self.major_tick = Gauge.Tick(start_angle=start_angle, stop_angle=stop_angle, line_width=major_tick_width, start_radius=major_tick_start_radius, stop_radius=major_tick_stop_radius, step=major_tick_step, line_color=major_tick_color, graph_elem=graph_elem)
        self.pointer = Gauge.Pointer(angle=pointer_angle, inner_radius=pointer_inner_radius, outer_radius=pointer_outer_radius, pointer_color=pointer_color, outer_color=pointer_outer_color, origin_color=pointer_origin_color, line_width=pointer_line_width, graph_elem=graph_elem)

        self.center_x, self.center_y = self.center = center
        self.degree = degree
        self.dx = self.dy = 1

    def move(self, delta_x, delta_y):
        """
        Move gauge to move all componenets in gauge.
        """
        self.center_x, self.center_y =self.center = (
            self.center_x+delta_x, self.center_y+delta_y)
        if self.clock:
            self.clock.move(delta_x, delta_y)
        if self.minor_tick:
            self.minor_tick.move(delta_x, delta_y)
        if self.major_tick:
            self.major_tick.move(delta_x, delta_y)
        if self.pointer:
            self.pointer.move(delta_x, delta_y)

    def change(self, degree=None, step=1):
        """
        Rotation of pointer
        call it with degree and step to set initial options for rotation.
        Without any option to start rotation.
        """
        if self.pointer:
            if degree != None:
                self.pointer.stop_degree = degree
                self.pointer.step = step if self.pointer.all[2] < degree else -step
                return True
            now = self.pointer.all[2]
            step = self.pointer.step
            new_degree = now + step
            if ((step > 0 and new_degree < self.pointer.stop_degree) or
                (step < 0 and new_degree > self.pointer.stop_degree)):
                    self.pointer.new(degree=new_degree)
                    return False
            else:
                self.pointer.new(degree=self.pointer.stop_degree)
                return True

ALPHA = 0.8
THEME = 'Dark purple 6 '
UPDATE_FREQUENCY_MILLISECONDS = 2 * 1000


def human_size(bytes, units=(' bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB')):
    """ Returns a human readable string reprentation of bytes"""
    return str(bytes) + ' ' + units[0] if bytes < 1024 else human_size(bytes >> 10, units[1:])

def main(location):
    sg.theme(THEME)
    gsize = (100, 55)
    layout = [
        [sg.T('CPU', font='Any 20', background_color='black')],
        [sg.Graph(gsize, (-gsize[0] // 2, 0), (gsize[0] // 2, gsize[1]), key='-Graph-')],
        [sg.T(size=(5, 1), font='Any 20', justification='c', background_color='black', k='-gauge VALUE-')]]


    window = sg.Window('CPU Usage Widget Square', layout, location=location, no_titlebar=True, grab_anywhere=True, margins=(0, 0), element_padding=(0, 0), alpha_channel=ALPHA, background_color='black', element_justification='c', finalize=True, right_click_menu=[[''], 'Exit'])

    gauge = Gauge(pointer_color=sg.theme_text_color(), clock_color=sg.theme_text_color(), major_tick_color=sg.theme_text_color(),
                  minor_tick_color=sg.theme_input_background_color(), pointer_outer_color=sg.theme_text_color(), major_tick_start_radius=45,
                  minor_tick_start_radius=45, minor_tick_stop_radius=50, major_tick_stop_radius=50, major_tick_step=30, clock_radius=50, pointer_line_width=3, pointer_inner_radius=10, pointer_outer_radius=50, graph_elem=window['-Graph-'])

    gauge.change(degree=0)

    while True:  # Event Loop
        cpu_percent = psutil.cpu_percent(interval=1)

        if gauge.change():
            new_angle = cpu_percent*180/100
            window['-gauge VALUE-'].update(f'{int(cpu_percent)}%')
            gauge.change(degree=new_angle, step=180)
            gauge.change()
        # ----------- update the graphics and text in the window ------------
        # update the window, wait for a while, then check for exit
        event, values = window.read(timeout=UPDATE_FREQUENCY_MILLISECONDS)
        if event == sg.WIN_CLOSED or event == 'Exit':
            break
    window.close()


if __name__ == '__main__':

    if len(sys.argv) > 1:
        location = sys.argv[1].split(',')
        location = (int(location[0]), int(location[1]))
    else:
        location = (None, None)
    main(location)

Creat a new Menu to be with enable/disable menu item

image

from copy import deepcopy
import PySimpleGUI as sg

class Menu(sg.Menu):

    def __new__(cls, menu_def, **kwargs):
        """
        To define Guage same as sg.Graph
        """
        return sg.Menu.__new__(cls)

    def __init__(self, menu_def, **kwargs):
        self.def_ = deepcopy(menu_def)
        super().__init__(menu_def, **kwargs)

    def replace(self, menu_def, old, new):
        for i, item in enumerate(menu_def):
            if isinstance(item, str):
                if item == old:
                    menu_def[i] = new
                    return True
            else:
                if self.replace(item, old, new):
                    return True
        return False

    def enable(self, item):
        if item[0] == "!" and self.replace(self.def_, item, item[1:]):
            self.update(menu_definition=deepcopy(self.def_))

    def disable(self, item):
        if item[0] != "!" and self.replace(self.def_, item, "!"+item):
            self.update(menu_definition=deepcopy(self.def_))

menu_def = [['&File', ['&Open     Ctrl-O', '&Save       Ctrl-S', '&Properties', 'E&xit']],
            ['&Edit', [['Special', 'Normal',['Normal1', 'Normal2'] ], 'Undo'], ],
            ['&Toolbar', ['---', 'Command &1::Command_Key', 'Command &2', '---', 'Command &3', 'Command &4']],
            ['&Help', ['&About...']], ]

sg.theme("DarkBlue")
sg.set_options(font=('Courier New', 16))

menu_def = [['&File', ['&Open     Ctrl-O', '&Save       Ctrl-S', '&Properties', 'E&xit']],
            ['&Edit', [['Special', 'Normal',['Normal1', 'Normal2'] ], 'Undo'], ],
            ['&Toolbar', ['---', 'Command &1::Command_Key', 'Command &2', '---', 'Command &3', 'Command &4']],
            ['&Help', ['&About...']], ]

item_list = ['&File', '&Open     Ctrl-O', '&Save       Ctrl-S', '&Properties',
    'E&xit', '&Edit', 'Special', 'Normal', 'Normal1', 'Normal2', 'Undo',
    '&Toolbar', 'Command &1::Command_Key', 'Command &2', 'Command &3',
    'Command &4', '&Help', '&About...']

layout = [
    [Menu(menu_def, key='MENU')],
    [sg.Button('Enable'), sg.Button('Disable')],
    [sg.Combo(item_list, default_value='&File', size=(25, 1), readonly=True,
        key='Combo')],
]
window = sg.Window("Title", layout, finalize=True)
menu = window['MENU']

while True:
    event, values = window.read()
    if event == sg.WINDOW_CLOSED:
        break
    item = values['Combo']
    if event == 'Enable':
        item = item if item[0] == '!' else '!'+item
        menu.enable(item)
    elif event == 'Disable':
        item = item if item[0] != '!' else item[1:]
        menu.disable(item)

window.close()

function pop_get_time to set hour and minute

image

from datetime import datetime
import PySimpleGUI as sg

def popup_get_time(start_hour=None, start_minute=None):
    """
    Using mouse wheel to select hour and minute
    :param start_hour: Default value for hour. 0 ~ 23
    :type start_hour: int
    :param start_minute: Default value for hour. 0 ~ 59
    :type start_minute: int
    """
    option = {
        "font": ("Courier New", 48, "bold"),
        "enable_events": True,
        "background_color": None,
        "text_color": "white",
        "justification": "center",
        "pad": (0, 0)
    }
    now = datetime.now()
    hour, minute = now.hour, now.minute
    hour = hour if start_hour is None else max(0, min(int(start_hour), 23))
    minute = minute if start_minute is None else max(0, min(int(start_minute), 59))
    layout = [
        [sg.Text(f"{hour:0>2d}", **option, size=(3, 1), key="Hour"),
         sg.Text(":",  **option),
         sg.Text(f"{minute:0>2d}", **option, size=(3, 1), key="Minute")],
        [sg.Button("OK"), sg.Button("Cancel")],
    ]
    window = sg.Window("Select Time", layout, grab_anywhere=True,
        keep_on_top=True, modal=True, finalize=True)
    hour_element, minute_element = window['Hour'], window['Minute']
    hour_element.bind("<MouseWheel>", "_Wheel")
    minute_element.bind("<MouseWheel>", "_Wheel")

    while True:
        event, values = window.read()
        if event in (sg.WINDOW_CLOSED, "Cancel"):
            window.close()
            return (None, None)
        elif event == "OK":
            window.close()
            return (hour, minute)
        elif event == "Hour_Wheel":
            delta = -int(hour_element.user_bind_event.delta/120)
            hour = (hour+delta) % 24
            hour_element.update(f'{hour:0>2d}')
        elif event == "Minute_Wheel":
            delta = -int(minute_element.user_bind_event.delta/120)
            minute = (minute+delta) % 60
            minute_element.update(f'{minute:0>2d}')

print(popup_get_time(5, 20))

Next one for user element

import PySimpleGUI as sg

class Time(sg.Column):

    elements = []

    def __init__(self, hour=0, minute=0, second=0, **options):
        self.hour_value = hour
        self.minute_value = minute
        self.second_value = second
        self.hour   = sg.Text(f"{hour:0>2d}"  , **options)
        self.minute = sg.Text(f"{minute:0>2d}", **options)
        self.second = sg.Text(f"{minute:0>2d}", **options)
        layout = [[self.hour, sg.Text(":",  **options), self.minute, sg.Text(":",  **options), self.second]]
        super().__init__(layout)
        Time.elements.append(self)

    @classmethod
    def initial(self):
        for element in Time.elements:
            element.hour  .Widget.bind("<MouseWheel>", element.callback)
            element.minute.Widget.bind("<MouseWheel>", element.callback)
            element.second.Widget.bind("<MouseWheel>", element.callback)

    def callback(self, event):
        widget = event.widget
        delta = int(event.delta/120)
        if widget == self.hour.Widget:
            self.hour_value = (self.hour_value+delta) % 24
            self.hour.update(f'{self.hour_value:0>2d}')
        elif widget == self.minute.Widget:
            self.minute_value = (self.minute_value+delta) % 60
            self.minute.update(f'{self.minute_value:0>2d}')
        else:
            self.second_value = (self.second_value+delta) % 60
            self.second.update(f'{self.second_value:0>2d}')

    def get(self):
        return f'{self.hour_value:0>2d}:{self.minute_value:0>2d}:{self.second_value:0>2d}'

font0 = ("Courier New", 11)
font1 = ("Courier New", 48)
sg.theme("DarkBlue3")
sg.set_options(font=font0)

time1 = Time(font=font1, pad=(0, 0), background_color='white', text_color='blue')
time2 = Time(font=font1, pad=(0, 0), background_color='blue', text_color='white')
layout = [[time1], [time2], [sg.Push(), sg.Button("Time")]]

window = sg.Window("test", layout, finalize=True)
Time.initial()

while True:
    event, values = window.read()
    if event in (sg.WINDOW_CLOSED, 'Exit'):
        break
    elif event == 'Time':
        print(time1.get(), time2.get())

window.close()

Handling expand and collapse event in sg.Tree

from random import choice
import PySimpleGUI as sg

treedata = sg.TreeData()

for i in range(10):
    parent = choice(list(treedata.tree_dict.keys()))
    treedata.insert(parent, i, f'Node {i}', values=[f'Data {i}'])

layout = [
    [sg.Tree(data=treedata, headings=['Data', ], auto_size_columns=True,
        num_rows=10, col0_width=40, key='-TREE-', show_expanded=False,
        enable_events=True),],
]

window = sg.Window('Tree Element Test', layout, finalize=True)
tree = window['-TREE-'].Widget
window['-TREE-'].bind("<<TreeviewOpen>>", "EXPAND-")
window['-TREE-'].bind("<<TreeviewClose>>", "COLLAPSE-")

while True:
    event, values = window.read()
    if event in (sg.WIN_CLOSED, 'Cancel'):
        break
    elif event == '-TREE-EXPAND-':
        item = tree.item(tree.focus())
        print(f"Expand at item {item}")
    elif event == '-TREE-COLLAPSE-':
        item = tree.item(tree.focus())
        print(f"Collapse at item {item}")

window.close()
Expand at item {'text': 'Node 0', 'image': '', 'values': ['Data 0'], 'open': True, 'tags': ''}
Collapse at item {'text': 'Node 0', 'image': '', 'values': ['Data 0'], 'open': False, 'tags': ''}
Expand at item {'text': 'Node 2', 'image': '', 'values': ['Data 2'], 'open': True, 'tags': ''}
Expand at item {'text': 'Node 4', 'image': '', 'values': ['Data 4'], 'open': True, 'tags': ''}

Search in sg.Table

image

import PySimpleGUI as sg

headings = ['President', 'Date of Birth']
data = [
    ['Ronald Reagan', 'February 6'],
    ['Abraham Lincoln', 'February 12'],
    ['George Washington', 'February 22'],
    ['Andrew Jackson', 'March 15'],
    ['Thomas Jefferson', 'April 13'],
    ['Harry Truman', 'May 8'],
    ['John F. Kennedy', 'May 29'],
    ['George H. W. Bush', 'June 12'],
    ['George W. Bush', 'July 6'],
    ['John Quincy Adams', 'July 11'],
    ['Garrett Walker', 'July 18'],
    ['Bill Clinton', 'August 19'],
    ['Jimmy Carter', 'October 1'],
    ['John Adams', 'October 30'],
    ['Theodore Roosevelt', 'October 27'],
    ['Frank Underwood', 'November 5'],
    ['Woodrow Wilson', 'December 28'],
]

layout = [
    [sg.Text("President to search:")],
    [sg.Input(size=(33, 1), key='-INPUT-'), sg.Button('Search')],
    [sg.Table(data, headings=headings, justification='left', key='-TABLE-')],
]
window = sg.Window("Title", layout, finalize=True)
table = window['-TABLE-']
entry = window['-INPUT-']
entry.bind('<Return>', 'RETURN-')

while True:
    event, values = window.read()
    if event == sg.WINDOW_CLOSED:
        break
    # print(event, values)
    if event in ('Search', '-INPUT-RETURN-'):
        text = values['-INPUT-']
        if text:
            index = None
            for i, item in enumerate(data):
                if text.lower() in item[0].lower():
                    index = i
                    break
            if index is not None:
                table.set_vscroll_position(index/len(data))
                table.update(select_rows=[index])

window.close()

Combination key in Window

By setting option return_keyboard_events=True in class sg.Window, all events generated as seperate events after KeyRelease.
It is impossible to confirm what combination is.

import PySimpleGUI as sg

layout = [
    [sg.Text('Hello World !')]
]
window = sg.Window("Title", layout, return_keyboard_events=True, finalize=True)

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

window.close()

After Ctrl+Alt+Shift+C released, will get following events

C:67
Alt_L:18
Shift_L:16
Control_L:17

So you have to record status of keys, like ALT, CTRL, SHIFT and others, here example for it, but tkinter code required.
Here keyboard auto-repeat function removed, you can check if not special key, like alt, shift and control keys.

Tkinter code required

import PySimpleGUI as sg

def key_event(event, key):
    window.write_event_value(key, event.keysym)

layout = [
    [sg.Text('Hello World !')],
    [sg.Input()],
]
window = sg.Window("Title", layout, finalize=True)
root = window.TKroot
root.bind('<KeyPress>', lambda event, key='-PRESS-':key_event(event, key))
root.bind('<KeyRelease>', lambda event, key='-RELEASE-':key_event(event, key))

key = {}
while True:
    event, values = window.read()
    if event == sg.WINDOW_CLOSED:
        break
    #print(event, values)
    if event == '-PRESS-':
        key_sym = values[event]
        if key_sym in key:
            continue
        key[key_sym] = key_sym
        print('-'.join([value for key, value in key.items()]))
    elif event == '-RELEASE-':
        key_sym = values[event]
        if key_sym in key:
            del key[key_sym]
        else:
            continue
        if key != {}:
            print('-'.join([value for key, value in key.items()]))

window.close()

Key pressed by order Alt_L, Control_L, Shift_L, C, then released by reverse order, will get following output.

Alt_L
Alt_L-Control_L
Alt_L-Control_L-Shift_L
Alt_L-Control_L-Shift_L-C
Alt_L-Control_L-Shift_L
Alt_L-Control_L
Alt_L

Multi-Listbox with only one scrollbar

image

import PySimpleGUI as sg

sg.theme('DarkBlue')
sg.set_options(font=('Courier New', 12))

cols = 3
rows = 50
rows_show = 10
col_width = 15

data = [[f'Cell ({j:0>2d} ,{i:0>2d})' for i in range(rows)] for j in range(cols)]

all_listbox = [[sg.Listbox(data[i], size=(15, rows), pad=(0, 0),
    no_scrollbar=True, enable_events=True, key=f'listbox {i}',
    select_mode=sg.LISTBOX_SELECT_MODE_SINGLE) for i in range(cols)]]

layout = [
    [sg.Text('Product'.center(col_width), pad=(0, 0)),
     sg.Text('Unit Price'.center(col_width), pad=(0, 0)),
     sg.Text('Price'.center(col_width), pad=(0, 0))],
    [sg.Column(all_listbox, pad=(0, 0), scrollable=True,
        vertical_scroll_only=True)],
]
window = sg.Window("Title", layout, finalize=True)
# Align content of list to center & remove underline in listbox
for i in range(cols):
    listbox = window[f'listbox {i}'].Widget
    listbox.configure(justify='center', activestyle='none')

while True:
    event, values = window.read()
    if event == sg.WINDOW_CLOSED:
        break
    if event.startswith('listbox'):
        row = window[event].get_indexes()[0]
        for i in range(cols):
            window[f'listbox {i}'].update(set_to_index=row)

window.close()

All required actions for dynamic sg.Table

image

import PySimpleGUI as sg

class Table(sg.Table):

    def get_all_iids(self):
        """
        Return list of unique id for each row in table, all iid start from '1'
        """
        return list(self.Widget.get_children())

    def select_to_iid(self, select):
        """
        Convert id of selected row into iid
        """
        return list(map(lambda item:str(item+1), select))

    def delete_iids(self, *iids):
        self.Widget.delete(*iids)

    def delete(self, select, position):
        """
        Delete selected rows in table
        select: list of id of selected rows, all PSG id start from 0
        """
        if select == []:
            self.status.update('No row selected to delete !')
            return
        if sg.popup_yes_no("Are you sure to delete ?") == 'Yes':
            select_iid = self.select_to_iid(select)
            self.delete_iids(*select_iid)
            self.tree_ids = self.get_all_iids() # update iid list of PSG table
            self.update(select_rows=[])         # Deselect rows of table

    def move_up(self, select, position):
        """
        Move selected rows one step up
        select: list of id of selected rows, all PSG id start from 0
        """
        if select == []:
            self.status.update('No row selected to move up !')
            return
        iids = self.get_all_iids()
        select_iid = self.select_to_iid(select)
        index = list(map(iids.index, select_iid))   # move index one step up
        if index[0] == 0:                           # if on the top of table
            self.status.update('Selected rows are already on the top of table !')
            return
        for i in index:                             # switch with previous one
            iids[i], iids[i-1] = iids[i-1], iids[i]
        self.Widget.set_children('', *iids)         # update iid list of table
        self.Widget.see(select_iid[0])              # ensure top one visible
        self.tree_ids = iids                        # update PSG iid table

    def move_down(self, select, position):
        """
        Move selected rows one step down
        select: list of id of selected rows, all PSG id start from 0
        """
        if select == []:
            self.status.update('No row selected to move down !')
            return
        iids = self.get_all_iids()
        select_iid = self.select_to_iid(select)
        index = list(map(iids.index, select_iid))
        if index[-1] == len(iids)-1:                # if on bottom of table
            self.status.update('Selected rows are already on the bottom of table !')
            return
        for i in reversed(index):                   # switch with next one
            iids[i], iids[i+1] = iids[i+1], iids[i]
        self.Widget.set_children('', *iids)         # update iid list of table
        self.Widget.see(select_iid[-1])             # ensure bottom one visible
        self.tree_ids = iids                        # update PSG iid table

    def edit(self, select, position):
        """
        Edit data of selected row on another popup window
        select: list of id of selected rows, all PSG id start from 0
        """
        if len(select) != 1:                        # Only one row to go edit
            self.status.update('Select only one row to edit !')
            return
        iid = self.select_to_iid(select)[0]         # get first iid
        values = self.Widget.set(iid)               # get values of row iid
        size = (max(map(len, self.header)), 1)      # size of header text
        layout = [
            [sg.Text(column, size=size), sg.Input(values[column], key=column)]
                for column in self.header] + [[sg.OK(), sg.Cancel()]]
        win = sg.Window('Edit', layout, modal=True, finalize=True)
        self.remove_dash_box(win)
        while True:
            evt, vals = win.read()
            if evt in (sg.WINDOW_CLOSED, 'Cancel'):
                break
            elif evt == 'OK':
                for column in self.header:          # update value of all columns
                    self.Widget.set(iid, column=column, value=vals[column])
                break
        win.close()

    def get_new_iid(self):
        """
        Generate one new unique iid
        """
        iids = self.get_all_iids()
        i = 1
        while str(i) in iids:
            i += 1
        return str(i)

    def insert(self, select, position):
        """
        Insert one blank row into table.
        select: list of id of selected rows, all PSG id start from 0
        position: where to insert, can be 'Before' or 'After' selected rows,
            'Top' or 'Bottom' of table.
        """
        if position in ('Before', 'After') and select == []:
            self.status.update(f'Select one row to insert {position.lower()} !')
            return
        layout = [
            [sg.Text(column, size=(12, 1)), sg.Input('', key=column)]
                for column in self.header
        ] + [[sg.OK(), sg.Cancel()]]
        win = sg.Window('Insert', layout, modal=True, finalize=True)
        self.remove_dash_box(win)

        while True:
            evt, vals = win.read()
            if evt in (sg.WINDOW_CLOSED, 'Cancel'):
                break
            elif evt == 'OK':
                iids = self.get_all_iids()
                select_iid = self.select_to_iid(select)
                if position == 'Before':            # Find index for insertion
                    index = iids.index(select_iid[0])
                elif position == 'After':
                    index = iids.index(select_iid[-1])+1
                elif position == 'Top':
                    index = 0
                else:
                    index = len(iids)
                # Insert new row with new values into table
                values = [vals[column] for column in self.header]
                iid = self.get_new_iid()
                self.Widget.insert('', index, iid=iid, values=values)
                self.Widget.see(iid)                # ensure new one visible
                self.tree_ids = self.get_all_iids() # update PSG iid table
                # new row iid converted into selected id, then new row selected
                self.update(select_rows=[int(iid)-1])
                break
        win.close()

    def copy(self, select, position):
        """
        Copy values of each selected row to clipboard
        select: list of id of selected rows, all PSG id start from 0
        """
        if select == []:
            self.status.update('No row selected to copy !')
            return
        select_iid = self.select_to_iid(select)
        self.clipboard = []
        for iid in select_iid:
            data_dict = self.Widget.set(iid)        # get row data
            self.clipboard.append([data_dict[column] for column in self.header])

    def paste(self, select, position):
        """
        Paste content of clipboard into table
        select: list of id of selected rows, all PSG id start from 0
        position: where to paste, can be 'Before' or 'After' selected rows,
            'Top' or 'Bottom' of table.
        """
        if self.clipboard == None:
            self.status.update('No data to paste !')
            return
        if position in ('Before', 'After') and select == []:
            self.status.update(f'Select rows to paste {position.lower()} !')
            return
        iids = self.get_all_iids()
        select_iid = self.select_to_iid(select)
        if position == 'Before':            # Find index for insertion
            index = iids.index(select_iid[0])
        elif position == 'After':
            index = iids.index(select_iid[-1])+1
        elif position == 'Top':
            index = 0
        else:
            index = len(iids)
        id_list = []
        for i, values in enumerate(self.clipboard): # paste content of clipboard
            iid = self.get_new_iid()
            id_list.append(int(iid)-1)
            self.Widget.insert('', index+i, iid=iid, values=values)
        self.tree_ids = self.get_all_iids()         # update PSG iid list
        self.update(select_rows=id_list)            # pasted rows selected
        self.Widget.see(str(id_list[-1]+1))         # ensure last row visible
        self.Widget.see(str(id_list[0]+1))          # ensure first row visible

    def align_headings(self, alignment):
        """
        Aligment for headings of table
        alignment: list of anchor string for each column of headings
            anchor string can be one of
                'nw', 'n', 'ne', 'w', 'center', 'e', 'sw', 's', 'se'.
        """
        for cid, anchor in enumerate(alignment):
            self.Widget.heading(cid, anchor=anchor)

    def align_columns(self, alignment):
        """
        Aligment for columns of table
        alignment: list of anchor string for each column of table
            anchor string can be one of
                'nw', 'n', 'ne', 'w', 'center', 'e', 'sw', 's', 'se'.
        """
        for cid, anchor in enumerate(alignment):
            self.Widget.column(cid, anchor=anchor)

    def double_click(self, event):
        """
        Additional event for double-click on header
        event: class event
        """
        self.status.update('')  # Widget event, so clear status bar here.
        # what part clicked
        region = self.Widget.identify("region", event.x, event.y)
        if region == 'heading': # Only care double-clock on headings
            # check which column clicked
            cid = int(self.Widget.identify_column(event.x)[1:])-1
            values = self.get_all_values()  # gete all values and sorting by key
            values = sorted(values, key=lambda item:String(item[cid]),
                reverse=self.reverse[cid])  # reverse all the time
            self.update(values=values)        # update full table with sorted values
            self.clipboard = None           # clear clipboard after sorting
            self.reverse[cid] = not self.reverse[cid]   # reverse sorting

    def initial(self, status, header, header_alignment, column_alignment):
        """
        Initial setting for related information
        status: statusbar element to show status
        header: list of string, headings of table.
        header_alignment: list of anchor string for headings
        column_alignment: list of anchor string for columns
        """
        self.status, self.header = status, header
        self.align_headings(header_alignment)
        self.align_columns(column_alignment)
        # Initialize reverse settings of sorting for each heading
        self.reverse = [False]*len(header)
        # Add binding of double-click event on headings
        self.Widget.bind('<Double-1>', self.double_click, add='+')

    def remove_dash_box(self, win):
        """
        Remove dash box of element, skip Input-related elements.
        """
        for key, element in win.AllKeysDict.items():    # all elements with key
            if not isinstance(element,
                    (sg.InputText, sg.InputCombo, sg.InputOptionMenu)):
                element.Widget.configure(takefocus=0)   # no keyboard focus

    def get_all_values(self):
        """
        Get values of all rows in existing table.
        """
        return [self.Widget.item(row)['values'] for row in self.get_all_iids()]

class String():
    """
    String for comparison in sorting
    """
    def __init__(self, value):
        self.value = value
        try:
            self.value = float(value)
            self.type = 'number'
        except:
            self.value = value
            self.type = 'string'

    def __gt__(self, value):
        if self.type == value.type:
            result = self.value > value.value
        else:
            result = False if self.type == 'string' else True
        return result

    def __ge__(self, value):
        if self.type == value.type:
            result = self.value >= value.value
        else:
            result = False if self.type == 'string' else True
        return result

    def __lt__(self, value):
        if self.type == value.type:
            result = self.value < value.value
        else:
            result = True if self.type == 'string' else False
        return result

    def __le__(self, value):
        if self.type == value.type:
            result = self.value <= value.value
        else:
            result = True if self.type == 'string' else False
        return result

    def __eq__(self, value):
        if self.type == value.type:
            result = self.value == value.value
        else:
            result = False
        return result

    def __repr__(self):
        return str(self.value)


# All data should be in string format for no conversion when edit in sg.Input
data = [
    ["Name",         "Cases/All", "Case/Day", "Deaths/All", "Death/Day"],
    ["Global",       "80773033",    "563983",    "1783619",     "11784"],
    ["USA",          "19147627",    "174814",     "332423",      "1779"],
    ["India",        "10244852",     "20549",     "148439",       "286"],
    ["Brazil",        "7504833",     "20548",     "191570",       "431"],
    ["Russian",       "3131550",     "26513",      "56426",       "599"],
    ["France",        "2530400",     "11295",      "63701",       "969"],
    ["UK",            "2382869",     "53135",      "71567",       "458"],
    ["Italy",         "2067487",     "11210",      "73029",       "659"],
    ["Spain",         "1893502",      "7717",      "50442",        "36"],
    ["Germany",       "1687185",     "22459",      "32107",      "1129"],
    ["Colombia",      "1603807",      "9310",      "42374",       "203"],
    ["Argentina",     "1590513",      "6586",      "42868",       "218"],
    ["Mexico",        "1389430",      "5996",     "122855",       "429"],
    ["Turkey",        "1364242",     "15805",      "20388",       "253"],
    ["Poland",        "1281414",     "12780",      "28019",       "565"],
    ["Iran",          "1212481",      "6108",      "54946",       "132"],
    ["Ukraine",       "1045348",      "7986",      "18324",       "243"],
    ["South Africa",  "1021451",      "9580",      "27568",       "497"],
    ["Peru",          "1008908",      "1251",      "37525",        "51"],
    ["Netherlands",    "778293",      "7561",      "11218",       "171"],
]

sg.theme('DarkBlue')
sg.set_options(button_element_size=(6, 1), auto_size_buttons=False,
    button_color=('white', 'blue'), font='Courier 11')

frame_button = [[sg.Button('Edit'), sg.Button('Delete'), sg.Button('Insert')]]
frame_copy   = [[sg.Button('Copy')], [sg.Button('Paste')]]
frame_move   = [[sg.Button('MoveUp')], [sg.Button('MoveDn')]]
frame_radio  = [
    [sg.Radio('Before', 'Position', size=(8, 1), key='Before'),
     sg.Radio('After',  'Position', size=(8, 1), key='After')],
    [sg.Radio('Top',    'Position', size=(8, 1), key='Top'),
     sg.Radio('Bottom', 'Position', size=(8, 1), key='Bottom', default=True)]]

layout = [
    [sg.Column(frame_button), sg.Column(frame_radio), sg.Column(frame_copy),
     sg.Column(frame_move)],
    [Table(data[1:], headings=data[0], auto_size_columns=False,
        def_col_width=13, enable_events=True, key='-TABLE-')],
    [sg.StatusBar('Double click header to do sorting', size=(60, 1),
        key='-STATUS-')],
]

window = sg.Window('Table', layout, use_default_focus=False, finalize=True)
table, status = window['-TABLE-'], window['-STATUS-']
table.initial(status, data[0], ['center']*5, ['center']+['e']*4)
table.remove_dash_box(window)

function = {
    'Edit'  :table.edit, 'Delete':table.delete, 'Insert':table.insert,
    'Copy'  :table.copy, 'Paste' :table.paste,  'MoveUp':table.move_up,
    'MoveDn':table.move_down}

while True:
    event, values = window.read()
    if event == sg.WINDOW_CLOSED:
        break
    # print(event, values)
    status.update('')   # Clear status bar
    select = values['-TABLE-']
    position = 'Bottom'
    for key in ('Before', 'After', 'Top', 'Bottom'):
        if values[key]:
            position = key
            break
    if event in function:
        function[event](select, position)

window.close()

Set window keep on top or not after window finalized

There's no method to set window keep on top or not in PySimpleGUI.

Tkinter code required here

import PySimpleGUI as sg

def main():
    layout = [
        [sg.Button('Keep on Top'), sg.Button('Not Keep on Top')]
    ]
    window = sg.Window("Title", layout, keep_on_top=True, finalize=True)
    setting = {'Keep on Top':1, 'Not Keep on Top':0}
    while True:
        event, values = window.read()
        print(event, values)
        if event == sg.WINDOW_CLOSED:
            break
        if event in setting:
            window.TKroot.wm_attributes("-topmost", setting[event])

    window.close()

if __name__ == '__main__':
    main()

data of Poker cards

image

import ctypes
from io import BytesIO
from PIL import Image, ImageDraw, ImageFont, ImageOps
import PySimpleGUI as sg

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

def blank_image(poker_size, background_color='#f0f0f0ff'):
    w, h = poker_size
    r = 10
    im = Image.new('RGBA', poker_size, (0, 0, 0, 0))
    draw = ImageDraw.Draw(im, "RGBA")
    option = {'fill':background_color, 'width':0}
    draw.rectangle((0, r, w, h-r),     **option)
    draw.rectangle((r, 0, w-r, h),     **option)
    draw.ellipse((0, 0, 2*r, 2*r),     **option)
    draw.ellipse((w-2*r, 0, w, 2*r),   **option)
    draw.ellipse((0, h-2*r, 2*r, h),   **option)
    draw.ellipse((w-2*r, h-2*r, w, h), **option)
    return im

def text_image(text, color, size):
    font = ImageFont.truetype(font='arial.ttf', size=100)
    w, h = font.getsize(text)
    im = Image.new('RGBA', (w, h), (0, 0, 0, 0))
    draw = ImageDraw.Draw(im, "RGBA")
    draw.text((0, 0), text, fill=color, font=font, anchor='lt')
    im = im.crop(im.getbbox())
    if text == '10':
        w, h = size
        size = (int(w*1.6), h)
    im = im.resize(size, resample=Image.CUBIC)
    return im

def all_cards(card_size):

    card_text = ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K']
    card_color = ['black', '#800000ff', '#800000ff', 'black']
    card_symbol = ['โ™ ', 'โ™ฅ', 'โ™ฆ', 'โ™ฃ']

    r = 5
    size = (12, 18)
    blank = blank_image(card_size)
    im_text   = [[text_image(card_text[i], card_color[j], size)
        for i in range(13)] for j in range(4)]
    size = (12, 24)
    im_symbol = [text_image(card_symbol[i], card_color[i], size)
        for i in range(4)]

    size = (20, 30)
    dx, dy = size[0]//2, size[1]//2
    w, h = card_size
    hw, hh = w//2, h//2
    xg, yg = 25, 15
    g1 = (h - 3*size[1]-2*yg)/4
    g2 = (h - 4*size[1]-2*yg)/6
    x = [xg, hw-dx, w-size[0]-xg]
    y = [yg, hh-dy, h-yg-size[1], int(yg+dy+g1), h-yg-size[1]-dy, int(yg+dy+g2),
        int(yg+2.5*size[1]+6*g2), int(yg+size[1]+2*g2), int(yg+2*size[1]+4*g2)]
    points_list = {
        0:[(1, 1)],
        1:[(1, 0), (1, 2)],
        2:[(1, 0), (1, 1), (1, 2)],
        3:[(0, 0), (2, 0), (0, 2), (2, 2)],
        4:[(0, 0), (2, 0), (1, 1), (0, 2), (2, 2)],
        5:[(0, 0), (2, 0), (0, 1), (2, 1), (0, 2), (2, 2)],
        6:[(0, 0), (2, 0), (0, 1), (2, 1), (0, 2), (2, 2), (1, 3)],
        7:[(0, 0), (2, 0), (0, 1), (2, 1), (0, 2), (2, 2), (1, 3), (1, 4)],
        8:[(0, 0), (0, 7), (0, 8), (0, 2), (2, 0), (2, 7), (2, 8), (2, 2), (1, 1)],
        9:[(0, 0), (0, 7), (0, 8), (0, 2), (2, 0), (2, 7), (2, 8), (2, 2), (1, 5), (1, 6)]}

    large_symbol = [text_image(card_symbol[i], card_color[i], size)
        for i in range(4)]

    result = []
    for j in range(4):
        line = []
        for i in range(13):
            im = blank.copy()
            draw = ImageDraw.Draw(im, "RGBA")

            text = im_text[j][i]
            d = 5 if i == 9 else 0
            im.paste(text, box=(r-d//2, r), mask=text)
            im.paste(text, box=(w-r-12-d,r), mask=text)
            text = text.transpose(Image.ROTATE_180)
            im.paste(text, box=(r-d//2, h-r-18), mask=text)
            im.paste(text, box=(w-r-12-d, h-r-18), mask=text)

            symbol = im_symbol[j]
            im.paste(im_symbol[j], box=(r, r+18+4), mask=symbol)
            im.paste(im_symbol[j], box=(w-r-12,r+18+4), mask=symbol)
            symbol = symbol.transpose(Image.ROTATE_180)
            im.paste(symbol, box=(r, h-r-18*2-10), mask=symbol)
            im.paste(symbol, box=(w-r-12, h-r-18*2-10), mask=symbol)

            if i == 0:
                s = (40, 60)    # Very large symbol
                offset = 3
                box1 = (hw-s[0]//2+offset, hh-s[1]//2+offset)
                box2 = (hw-s[0]//2, hh-s[1]//2)
                symbol_1 = text_image(card_symbol[j], 'grey', s)
                symbol_2 = text_image(card_symbol[j], card_color[j], s)
                im.paste(symbol_1, box=box1, mask=symbol_1)
                im.paste(symbol_2, box=box2, mask=symbol_2)
            elif i < 10:
                for point in points_list[i]:
                    symbol = large_symbol[j]
                    if point[1] in (2, 4, 6, 8):
                        symbol = symbol.transpose(Image.ROTATE_180)
                        pass
                    im.paste(symbol, box=(x[point[0]], y[point[1]]), mask=symbol)
            else:
                text = "JACK" if i==10 else "QUEEN" if i==11 else "KING"
                symbol = text_image(text, card_color[j], (74, 102))
                im.paste(symbol, box=(57-37, 80-51), mask=symbol)

            line.append(get_data(im))
        result.append(line)
    return result

w, h = card_size = (115, 161)
gap = 10
width, height = 13*w+14*gap, 4*h+5*gap
cards = all_cards(card_size)

ctypes.windll.user32.SetProcessDPIAware()   # Set unit of GUI to pixels
sg.theme('DarkBlue')
layout = [[sg.Graph((width, height), (0, 0), (width, height), key='GRAPH',
    background_color='green', pad=(0, 0))]]
window = sg.Window("Title", layout, margins=(0, 0), finalize=True)
top     = [[window['GRAPH'].draw_image(data=cards[y][x],
    location=(gap+(w+gap)*x, height-(gap+(h+gap)*y)))
    for x in range(13)] for y in range(4)]

while True:
    event, values = window.read(timeout=1000)
    if event == sg.WINDOW_CLOSED:
        break

window.close()

Text progressbar by sg.Text

image

from random import randint
import PySimpleGUI as sg

sg.theme('DarkBlue')

layout = [[sg.Text('', size=(50, 1), relief='sunken', font=('Courier', 11),
    text_color='yellow', background_color='black',key='TEXT')]]
window = sg.Window('Title', layout, finalize=True)
text = window['TEXT']
state = 0
while True:

    event, values = window.read(timeout=100)

    if event == sg.WINDOW_CLOSED:
        break
    state = (state+1)%51
    text.update('โ–ˆ'*state)

window.close()

Measure dimension of screen and monospaced-font

Tkinter code required

image

import ctypes
import PySimpleGUI as sg

def window_dimension(window, monospaced_font):
    from tkinter.font import Font
    width, height = window.get_screen_dimensions()
    tkfont = Font(font=monospaced_font)
    w, h = tkfont.measure("A"), tkfont.metrics("linespace")
    return width//w, height//h

ctypes.windll.user32.SetProcessDPIAware()   # Set unit of GUI to pixels

sg.theme("DarkBlue")
font = ('Courier New', 64, 'bold')
sg.set_options(font=font)

layout = [
    [sg.Text("", size=(30, 3), justification='center', key='TEXT')],
    [sg.Text("", size=(30, 1), justification='center')],
    [sg.Text("1234567890"*3, size=(30, 1), pad=(0, 0), justification='center')],
]
window = sg.Window("Title", layout, element_padding=(0, 0), margins=(0, 0),
    finalize=True)
text = window['TEXT']
window.maximize()
w, h = window_dimension(window, font)
text.update(value=f'Dimension of window is\n{w}x{h}\nin chars')

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

window.close()

Alignment for headings of table

    table = window['-TABLE-'].Widget
    # Aligment for Headings
    for cid, anchor in enumerate(['w', 'w', 'center', 'e', 'e', 'e', 'e']):
        table.heading(cid, anchor=anchor)

Set row color and get row data of Table

image

import PySimpleGUI as sg

headings = ['President', 'Date of Birth']
data = [
    ['Ronald Reagan', 'February 6'],
    ['Abraham Lincoln', 'February 12'],
    ['George Washington', 'February 22'],
    ['Andrew Jackson', 'March 15'],
    ['Thomas Jefferson', 'April 13'],
    ['Harry Truman', 'May 8'],
    ['John F. Kennedy', 'May 29'],
    ['George H. W. Bush', 'June 12'],
    ['George W. Bush', 'July 6'],
    ['John Quincy Adams', 'July 11'],
    ['Garrett Walker', 'July 18'],
    ['Bill Clinton', 'August 19'],
    ['Jimmy Carter', 'October 1'],
    ['John Adams', 'October 30'],
    ['Theodore Roosevelt', 'October 27'],
    ['Frank Underwood', 'November 5'],
    ['Woodrow Wilson', 'December 28'],
]

layout = [
    [sg.Text("President to search:")],
    [sg.Input(size=(33, 1), key='-INPUT-'), sg.Button('Search')],
    [sg.Table(data, headings=headings, justification='left', key='-TABLE-')],
]
window = sg.Window("Title", layout, finalize=True)
table = window['-TABLE-']
entry = window['-INPUT-']
entry.bind('<Return>', 'RETURN-')

while True:
    event, values = window.read()
    if event == sg.WINDOW_CLOSED:
        break
    # print(event, values)
    if event in ('Search', '-INPUT-RETURN-'):
        text = values['-INPUT-'].lower()
        if text == '':
            continue
        row_colors = []
        for row, row_data in enumerate(data):
            if text in row_data[0].lower():
                row_colors.append((row, 'green'))
            else:
                row_colors.append((row, sg.theme_background_color()))
        table.update(row_colors=row_colors)

window.close()

To get row data (here, iid for each row is type str)

widget = window['-TABLE-'].Widget
for iid in widget.get_children():
    print(iid, widget.item(iid))
1 {'text': '{Ronald Reagan} {February 6}', 'image': '', 'values': ['Ronald Reagan', 'February 6'], 'open': 0, 'tags': [0]}
2 {'text': '{Abraham Lincoln} {February 12}', 'image': '', 'values': ['Abraham Lincoln', 'February 12'], 'open': 0, 'tags': [1]}
3 {'text': '{George Washington} {February 22}', 'image': '', 'values': ['George Washington', 'February 22'], 'open': 0, 'tags': [2]}
4 {'text': '{Andrew Jackson} {March 15}', 'image': '', 'values': ['Andrew Jackson', 'March 15'], 'open': 0, 'tags': [3]}
5 {'text': '{Thomas Jefferson} {April 13}', 'image': '', 'values': ['Thomas Jefferson', 'April 13'], 'open': 0, 'tags': [4]}
6 {'text': '{Harry Truman} {May 8}', 'image': '', 'values': ['Harry Truman', 'May 8'], 'open': 0, 'tags': [5]}
7 {'text': '{John F. Kennedy} {May 29}', 'image': '', 'values': ['John F. Kennedy', 'May 29'], 'open': 0, 'tags': [6]}
8 {'text': '{George H. W. Bush} {June 12}', 'image': '', 'values': ['George H. W. Bush', 'June 12'], 'open': 0, 'tags': [7]}
9 {'text': '{George W. Bush} {July 6}', 'image': '', 'values': ['George W. Bush', 'July 6'], 'open': 0, 'tags': [8]}
10 {'text': '{John Quincy Adams} {July 11}', 'image': '', 'values': ['John Quincy Adams', 'July 11'], 'open': 0, 'tags': [9]}
11 {'text': '{Garrett Walker} {July 18}', 'image': '', 'values': ['Garrett Walker', 'July 18'], 'open': 0, 'tags': [10]}
12 {'text': '{Bill Clinton} {August 19}', 'image': '', 'values': ['Bill Clinton', 'August 19'], 'open': 0, 'tags': [11]}
13 {'text': '{Jimmy Carter} {October 1}', 'image': '', 'values': ['Jimmy Carter', 'October 1'], 'open': 0, 'tags': [12]}
14 {'text': '{John Adams} {October 30}', 'image': '', 'values': ['John Adams', 'October 30'], 'open': 0, 'tags': [13]}
15 {'text': '{Theodore Roosevelt} {October 27}', 'image': '', 'values': ['Theodore Roosevelt', 'October 27'], 'open': 0, 'tags': [14]}
16 {'text': '{Frank Underwood} {November 5}', 'image': '', 'values': ['Frank Underwood', 'November 5'], 'open': 0, 'tags': [15]}
17 {'text': '{Woodrow Wilson} {December 28}', 'image': '', 'values': ['Woodrow Wilson', 'December 28'], 'open': 0, 'tags': [16]}

Circular progress bar by using sg.Graph by draw_arc and draw_circle

image

from random import randint
import PySimpleGUI as sg

class Circular_Progressbar():

    def __init__(self, graph_bg='green', bar_radius=50, bar_width=10, gap=5,
        bar_color='yellow', progress_color='blue', text_color='white',
        text_font=('Courier New', 16)):

        self.text_font      = text_font
        self.text_color     = text_color
        self.bar_radius     = bar_radius
        self.bar_width      = bar_width
        self.bar_color      = bar_color
        self.graph_bg       = graph_bg
        self.progress_color = progress_color
        self.gap            = gap + (self.bar_width+1)//2
        self.degree         = 0
        self.target         = 0
        self.graph  = sg.Graph(
            (2*(self.bar_radius+self.gap), 2*(self.bar_radius+self.gap)),
            (-self.bar_radius-self.gap, -self.bar_radius-self.gap),
            ( self.bar_radius+self.gap,  self.bar_radius+self.gap),
            background_color=self.graph_bg)
        self.p, self.t   = None, None

    def initial(self, angle=0):
        self.graph.draw_circle((0, 0), self.bar_radius,
            line_color=self.bar_color, line_width=self.bar_width)
        angle = min(360, max(0, angle))
        self.set_now(0)
        self.set_target(0)

    def set_target(self, angle=0, step=5):
        self.target = min(360, max(0, int(angle)))
        self.step = min(360, max(1, int(step)))

    def set_now(self, angle=0):
        self.angle = min(360, max(0, int(angle)))

    def move(self):
        if self.target == self.angle:
            return True
        if self.angle < self.target:
            self.angle = min(self.target, self.angle+self.step)
        else:
            self.angle = max(self.target, self.angle-self.step)
        if self.p:
            self.graph.delete_figure(self.p)
        if self.t:
            self.graph.delete_figure(self.t)
        text = f'{self.angle/3.6:.1f}%'
        r = self.bar_radius
        if self.angle == 360:
            self.p = self.graph.draw_circle((0, 0), self.bar_radius,
                line_color=self.progress_color, line_width=self.bar_width+1)
        else:
            self.p = self.graph.draw_arc((-r, r), (r, -r), self.angle, 0,
                style='arc', arc_color=self.progress_color,
                line_width=self.bar_width+1)
        self.t = self.graph.draw_text(
            text, (0, 0), color=self.text_color, font=self.text_font,
            text_location=sg.TEXT_LOCATION_CENTER)
        return False

progress_bar = Circular_Progressbar()
layout = [[progress_bar.graph]]

window = sg.Window('Circular Progressbar', layout, finalize=True)
progress_bar.initial()
progress_bar.set_target(randint(0, 360))

while True:

    event, values = window.read(timeout=10)
    print(event)
    if event == sg.WINDOW_CLOSED:
        break
    elif event == '__TIMEOUT__':
        if progress_bar.move():
            progress_bar.set_target(randint(0, 360))

window.close()

Scroll region issue for Column with values of Listbox updated

After content of listbox change, the option size not changed. After size changed, call method contents_changed of element sg.Column, then the new scroll area is computed to match the new contents.

Another issue for wrong scroll region can be solved by new scroll function, PySimpleGUI may update in the future.

image

import ctypes
from random import randint
import PySimpleGUI as sg

def yscroll(self, event):   # tkinter code
    if self.canvas.yview() == (0.0, 1.0):
        return
    if event.num == 5 or event.delta < 0:
        self.canvas.yview_scroll(1, "unit")
    elif event.num == 4 or event.delta > 0:
        self.canvas.yview_scroll(-1, "unit")

ctypes.windll.user32.SetProcessDPIAware()   # Set unit of GUI to pixels
sg.theme('DarkBlue')
sg.set_options(font=('Courier New', 12))

sg.TkScrollableFrame.yscroll = yscroll      # For issue scroll region

cols = 3
rows = randint(5, 20)
rows_show = 10
col_width = 15

data = [[f'Cell ({j:0>2d} ,{i:0>2d})' for i in range(rows)] for j in range(cols)]

all_listbox = [[sg.Listbox(data[i], size=(15, rows), pad=(0, 0),
    no_scrollbar=True, enable_events=True, key=f'listbox {i}',
    select_mode=sg.LISTBOX_SELECT_MODE_SINGLE) for i in range(cols)]]

layout = [
    [sg.Text('Product'.center(col_width), pad=(0, 0)),
     sg.Text('Unit Price'.center(col_width), pad=(0, 0)),
     sg.Text('Price'.center(col_width), pad=(0, 0))],
    [sg.Column(all_listbox, size=(555, 300), pad=(0, 0), scrollable=True,
        vertical_scroll_only=True, key='Column')],
    [sg.Button('Update'), sg.Text(f"Total rows = {rows}", key='Rows')],
]
window = sg.Window("Title", layout, finalize=True)
for i in range(cols):
    listbox = window[f'listbox {i}'].Widget
    listbox.configure(justify='center')     # tkinter code

while True:
    event, values = window.read()
    if event == sg.WINDOW_CLOSED:
        break
    elif event.startswith('listbox'):
        row = window[event].get_indexes()[0]
        user_event = False
        for i in range(cols):
            listbox = window[f'listbox {i}']
            listbox.set_value([])
            listbox.Widget.selection_set(row)   # tkinter code
    elif event == 'Update':
        rows = randint(5, 20)
        print(rows)
        data = [[f'Cell ({j:0>2d} ,{i:0>2d})' for i in range(rows)] for j in range(cols)]
        for i in range(cols):
            listbox = window[f'listbox {i}']
            listbox.update(values=data[i])
            listbox.Widget.configure(height=rows)       # tkinter code to update height of listbox
        window.refresh()                                # refresh required here
        window['Column'].contents_changed()             # update scrollbar
        window['Rows'].update(f"Total rows = {rows}")

window.close()

Insert row with specify color in table after window finalized

table = window['-TABLE-'].Widget

table.tag_configure("RED", foreground='red')
table.tag_configure("GREEN", foreground='green')

# tag = "GREEN" or "RED"

ids = table.insert('', 'end', iid=i, text=new_row, values=new_row, tag=tag)
window['-TABLE-'].tree_ids.append(ids)    # update key table

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.