diff --git a/.github/ISSUE_TEMPLATE/issue-form---must-fill-in-this-form-with-every-new-issue-submitted.md b/.github/ISSUE_TEMPLATE/issue-form---must-fill-in-this-form-with-every-new-issue-submitted.md index b7c36c524..a1fed6f02 100644 --- a/.github/ISSUE_TEMPLATE/issue-form---must-fill-in-this-form-with-every-new-issue-submitted.md +++ b/.github/ISSUE_TEMPLATE/issue-form---must-fill-in-this-form-with-every-new-issue-submitted.md @@ -1,7 +1,7 @@ --- -name: Issue Form - **Must fill in this form** with every new issue submitted. +name: Issue Form - **Must fill in this form** PSG4 is no longer supported. You MUST supply your Priority Support Code to log an issue. about: This form contains the information needed to help you solve your problem -title: "[ Enhancement/Bug/Question] NOTE - you can also call sg.main() or sg.main_open_github_issue() to post an issue" +title: "[ Enhancement/Bug/Question] NOTE You must supply your Priority Support Code in order to receive support- " labels: '' assignees: '' @@ -22,7 +22,7 @@ assignees: '' ---------------------------------------- -## Versions +## Versions (NOTE - PSG4 is no longer supported) Version information can be obtained by calling `sg.main_get_debug_data()` Or you can print each version shown in () @@ -35,10 +35,14 @@ Or you can print each version shown in () #### PySimpleGUI Version (`sg.__version__`) +#### GUI Version (tkinter (`sg.tclversion_detailed`), PySide2, WxPython, Remi) + #### GUI Version (tkinter (`sg.tclversion_detailed`), PySide2, WxPython, Remi) +### Priority Support Code - Only Commercially Licensed Users Receive Support as of Feb 2025 +Replace this text with your Priority Support Code --------------------- diff --git a/DemoPrograms/Browser_START_HERE_Demo_Programs_Browser.py b/DemoPrograms/Browser_START_HERE_Demo_Programs_Browser.py index 329a6ec12..00b41d515 100644 --- a/DemoPrograms/Browser_START_HERE_Demo_Programs_Browser.py +++ b/DemoPrograms/Browser_START_HERE_Demo_Programs_Browser.py @@ -14,7 +14,7 @@ import warnings import PySimpleGUI as sg -version = '5.0.0' +version = '5.3.0' __version__ = version.split()[0] @@ -52,7 +52,10 @@ Versions: 5.0.0 11-Feb-2024 The NEW Demo Browser for use with PySimpleGUI 5! - + 5.1.0 08-Apr-2024 Several new Demo Programs, updated Matplotlib ping demo, license ver 1.1 + 5.2.0 14-Aug-2024 Fixed erronous import error (when import line started with "from") + Added a new "Path" input so that an arbitrary file can be executed easily (or edited) + 5.3.0 15-Aug-2024 One last change for the new path input... clear other fields if chars are entered Copyright 2021, 2022, 2023, 2024 PySimpleSoft Inc. """ @@ -114,6 +117,18 @@ def get_file_list(): return sorted(list(get_file_list_dict().keys())) +def get_file_list_full_filename(): + """ + Returns list of filenames of files to display + No path is shown, only the short filename + + :return: List of filenames + :rtype: List[str] + """ + return sorted(list(get_file_list_dict().values())) + + + def get_demo_path(): """ Get the top-level folder path @@ -310,7 +325,7 @@ def check_imports_in_file(filename): # Check if the module exists if not check_modules_on_import_line(sline): all_passed = False - elif sline.startswith('from'): + elif sline.startswith('from') and 'import' in sline: module = re.search(r'from (\w+)', sline).group(1) if not check_module(module): all_passed = False @@ -336,6 +351,25 @@ def check_imports_in_file(filename): MMMMMMMMMMM ''' + + +# def search_files(file_list, search_string): +# found_list = [] +# for file in file_list: +# with open(file, 'r') as f: +# # Memory-map the file +# try: +# mmapped_file = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) +# except: +# continue +# # Search for the string in the file +# if mmapped_file.find(bytes(search_string, 'utf-8')) != -1: +# # print(f"String found in file: {file}") +# found_list.append(os.path.basename(file)) +# # else: +# # print(f"String not found in file: {file}") +# return found_list + def find_in_file(string, demo_files_dict, regex=False, verbose=False, window=None, ignore_case=True, show_first_match=True): """ Search through the demo files for a string. @@ -645,19 +679,21 @@ def make_window(): find_tooltip = "Find in file\nEnter a string in box to search for string inside of the files.\nFile list will update with list of files string found inside." filter_tooltip = "Filter files\nEnter a string in box to narrow down the list of files.\nFile list will update with list of files with string in filename." find_re_tooltip = "Find in file using Regular Expression\nEnter a string in box to search for string inside of the files.\nSearch is performed after clicking the FindRE button." + run_tooltip = "Run any python file\nEnter full absolute path and then click RUN button." left_col = sg.Column([ [sg.Listbox(values=get_file_list(), select_mode=sg.SELECT_MODE_EXTENDED, size=(50,20), bind_return_key=True, key='-DEMO LIST-', expand_x=True, expand_y=True)], - [sg.Text('Filter (F1):', tooltip=filter_tooltip), sg.Input(size=(25, 1), focus=True, enable_events=True, key='-FILTER-', tooltip=filter_tooltip), + [sg.Text('Filter (F1):', tooltip=filter_tooltip, s=8), sg.Input(size=(25, 1), focus=True, enable_events=True, key='-FILTER-', tooltip=filter_tooltip), sg.T(size=(15,1), k='-FILTER NUMBER-')], [sg.Button('Run'), sg.B('Edit'), sg.B('Clear'), sg.B('Open Folder'), sg.B('Copy Path')], - [sg.Text('Find (F2):', tooltip=find_tooltip), sg.Input(size=(25, 1), enable_events=True, key='-FIND-', tooltip=find_tooltip), + [sg.Text('Find (F2):', tooltip=find_tooltip, s=8), sg.Input(size=(25, 1), enable_events=True, key='-FIND-', tooltip=find_tooltip), sg.T(size=(15,1), k='-FIND NUMBER-')], + [sg.Text('Path (F3):', tooltip=run_tooltip, s=8), sg.Input(size=(25, 1), enable_events=True, key='-RUN PATH-', tooltip=run_tooltip)], ], element_justification='l', expand_x=True, expand_y=True) lef_col_find_re = sg.pin(sg.Col([ - [sg.Text('Find (F3):', tooltip=find_re_tooltip), sg.Input(size=(25, 1),key='-FIND RE-', tooltip=find_re_tooltip),sg.B('Find RE')]], k='-RE COL-')) + [sg.Text('Find (F4):', tooltip=find_re_tooltip, s=8), sg.Input(size=(25, 1),key='-FIND RE-', tooltip=find_re_tooltip),sg.B('Find RE')]], k='-RE COL-')) right_col = [ [sg.Multiline(size=(70, 21), write_only=True, expand_x=True, expand_y=True, key=ML_KEY, reroute_stdout=True, echo_stdout_stderr=True, reroute_cprint=True)], @@ -687,10 +723,7 @@ def make_window(): [options_at_bottom, sg.Sizegrip()]] # --------------------------------- Create Window --------------------------------- - # TODO Uncomment when deploy PSG5 - # window = sg.Window('PSG Demo & Project Browser', layout, finalize=True, resizable=True, use_default_focus=False, right_click_menu=sg.MENU_RIGHT_CLICK_EDITME_VER_EXIT, auto_save_location=True) - # TODO Remove when deploy PSG5 - window = sg.Window('PSG Demo & Project Browser', layout, finalize=True, resizable=True, use_default_focus=False, right_click_menu=sg.MENU_RIGHT_CLICK_EDITME_VER_EXIT) + window = sg.Window('PSG Demo & Project Browser', layout, finalize=True, resizable=True, use_default_focus=False, right_click_menu=sg.MENU_RIGHT_CLICK_EDITME_VER_EXIT, auto_save_location=True) window.set_min_size(window.size) @@ -698,7 +731,8 @@ def make_window(): window.bind('', '-FOCUS FILTER-') window.bind('', '-FOCUS FIND-') - window.bind('', '-FOCUS RE FIND-') + window.bind('', '-FOCUS RUN PATH-') + window.bind('', '-FOCUS RE FIND-') if not advanced_mode(): window['-FOLDER CHOOSE-'].update(visible=False) window['-RE COL-'].update(visible=False) @@ -906,9 +940,16 @@ def main(): elif event == 'Run': sg.cprint('Running....', c='white on green', end='') sg.cprint('') - - for file in values['-DEMO LIST-']: - file_to_run = str(file_list_dict[file]) + if values['-RUN PATH-']: # if a manual file was entered: + files_to_run = (values['-RUN PATH-'],) + else: + files_to_run = values['-DEMO LIST-'] + # for file in values['-DEMO LIST-']: + for file in files_to_run: + try: + file_to_run = str(file_list_dict[file]) + except: + file_to_run = file # sg.cprint('Checking Imports....', c='white on green') if sg.user_settings_get_entry('-check imports-', False) and not check_imports_in_file(file_to_run): sg.cprint(f'The demo program {os.path.basename(file_to_run)} depends on modules that are not installed.') @@ -942,14 +983,30 @@ def main(): window['-FILTER NUMBER-'].update(f'{len(new_list)} files') window['-FIND NUMBER-'].update('') window['-FIND-'].update('') + window['-RUN PATH-'].update('') + window['-FIND RE-'].update('') + elif event == '-RUN PATH-': + file_list = get_file_list() + window['-FILTER-'].update('') + window['-FILTER NUMBER-'].update(f'{len(file_list)} files') + window['-FIND-'].update('') + window['-DEMO LIST-'].update(file_list) + window['-FIND NUMBER-'].update('') window['-FIND RE-'].update('') + window['-ML-'].update('') elif event == '-FOCUS FIND-': window['-FIND-'].set_focus() elif event == '-FOCUS FILTER-': window['-FILTER-'].set_focus() elif event == '-FOCUS RE FIND-': window['-FIND RE-'].set_focus() + elif event == '-FOCUS RUN PATH-': + window['-RUN PATH-'].set_focus() elif event == '-FIND-' or event == '-FIRST MATCH ONLY-' or event == '-VERBOSE-' or event == '-FIND RE-': + # file_list = (search_files(get_file_list_full_filename(), values['-FIND-'])) + # window['-DEMO LIST-'].update(file_list) + # continue + is_ignore_case = values['-IGNORE CASE-'] old_ignore_case = False current_typed_value = str(values['-FIND-']) @@ -975,6 +1032,7 @@ def main(): window['-FILTER NUMBER-'].update('') window['-FIND RE-'].update('') window['-FILTER-'].update('') + window['-RUN PATH-'].update('') elif values['-FIND RE-']: window['-ML-'].update('') file_list = find_in_file(values['-FIND RE-'], get_file_list_dict(), regex=True, verbose=values['-VERBOSE-'],window=window) @@ -983,6 +1041,7 @@ def main(): window['-FILTER NUMBER-'].update('') window['-FIND-'].update('') window['-FILTER-'].update('') + window['-RUN PATH-'].update('') elif event == 'Find RE': window['-ML-'].update('') file_list = find_in_file(values['-FIND RE-'], get_file_list_dict(), regex=True, verbose=values['-VERBOSE-'],window=window) @@ -991,6 +1050,7 @@ def main(): window['-FILTER NUMBER-'].update('') window['-FIND-'].update('') window['-FILTER-'].update('') + window['-RUN PATH-'].update('') sg.cprint('Regular expression find completed') elif event == 'Settings': if settings_window() is True: @@ -1007,6 +1067,7 @@ def main(): window['-DEMO LIST-'].update(file_list) window['-FIND NUMBER-'].update('') window['-FIND RE-'].update('') + window['-RUN PATH-'].update('') window['-ML-'].update('') elif event == '-FOLDERNAME-': sg.user_settings_set_entry('-demos folder-', values['-FOLDERNAME-']) @@ -1019,6 +1080,7 @@ def main(): window['-FIND-'].update('') window['-FIND RE-'].update('') window['-FILTER-'].update('') + window['-RUN PATH-'].update('') elif event == 'Open Folder': explorer_program = get_explorer() if explorer_program: @@ -1054,6 +1116,7 @@ def main(): window['-FIND-'].update('') window['-FIND RE-'].update('') window['-FILTER-'].update('') + window['-RUN PATH-'].update('') window.close() diff --git a/DemoPrograms/Demo_Graph_Element.py b/DemoPrograms/Demo_Graph_Element.py index 879e7a463..1c1d10f0f 100644 --- a/DemoPrograms/Demo_Graph_Element.py +++ b/DemoPrograms/Demo_Graph_Element.py @@ -1,10 +1,22 @@ #!/usr/bin/env python import PySimpleGUI as sg -import ping -from threading import Thread +import random import time +import gc + +try: + import ping3 +except: + ping3 = None + if sg.popup_yes_no('This version of Python does not have the ping3 module installed. Would you like it to be installed?') == 'Yes': + sg.execute_pip_install_package('ping3') # pip install the ping3 package + sg.execute_restart(__file__) # restart this program so that it'll pick up the new ping3 installation + else: + sg.popup_quick_message('OK... Ping3 not installed so data will be simulated', font='_ 18', text_color='white', background_color='red', auto_close_duration=6) """ + Use a Graph element to show ping times to a URL using a line graph + Copyright 2023 PySimpleSoft, Inc. and/or its licensors. All rights reserved. Redistribution, modification, or any other use of PySimpleGUI or any portion thereof is subject to the terms of the PySimpleGUI License Agreement available at https://eula.pysimplegui.com. @@ -12,68 +24,80 @@ You may not redistribute, modify or otherwise use PySimpleGUI or its contents except pursuant to the PySimpleGUI License Agreement. """ +if ping3: + ping_url = 'google.com' +else: + ping_url = 'simulated data' -STEP_SIZE = 1 -SAMPLES = 1000 -CANVAS_SIZE = (1000, 500) - -# globale used to communicate with thread.. yea yea... it's working fine -g_exit = False -g_response_time = None - -def ping_thread(args): - global g_exit, g_response_time - while not g_exit: - g_response_time = ping.quiet_ping('google.com', timeout=1000) +def ping_thread(window: sg.Window): + while True: + if ping3: + ping_time = int(ping3.ping(ping_url) * 1000) + else: + time.sleep(.001) + ping_time = random.randint(0, 100) + if ping_time: + window.write_event_value('-THREAD-', ping_time) def main(): - global g_exit, g_response_time + global ping_url - # start ping measurement thread - thread = Thread(target=ping_thread, args=(None,)) - thread.start() + STEP_SIZE = 1 + SAMPLES = 100 + CANVAS_SIZE = (1000, 500) + Y_MAX = 500 + X_MAX = 500 sg.theme('Black') - sg.set_options(element_padding=(0, 0)) layout = [ - [sg.Text('Ping times to Google.com', font='Any 12'), - sg.Quit(pad=((100, 0), 0), button_color=('white', 'black'))], - [sg.Graph(CANVAS_SIZE, (0, 0), (SAMPLES, 500), - background_color='black', key='graph')] + sg.vbottom( + [sg.Column([[sg.T('Ping in MS'), sg.T(k='-TIME-', s=4)],[sg.Slider((50, Y_MAX), default_value=Y_MAX, orientation='v', size=(20, 20), k='-Y SLIDER-', expand_y=True, enable_events=True)]], expand_y=True, element_justification='r'), + sg.Column([ + [sg.Graph(CANVAS_SIZE, (0, 0), (SAMPLES, 200), background_color='black', key='-GRAPH-')], + [sg.Text('# Samples:'), sg.Slider((50, X_MAX), default_value=SAMPLES, orientation='h', size=(50, 20), k='-X SLIDER-', expand_x=True, enable_events=True)], + [sg.Text('Ping times to:'), sg.Input(ping_url, size=15, key='-URL-', readonly=not ping3, use_readonly_for_disable=True, disabled_readonly_text_color='black', disabled=not ping3), sg.B('Set', disabled=not ping3)],])], + expand_x=True, expand_y=True) ] - window = sg.Window('Canvas test', layout, - grab_anywhere=True, background_color='black', - no_titlebar=False, use_default_focus=False) + window = sg.Window('Ping Graph', layout, background_color='black', finalize=True, font='_ 16') - graph = window['graph'] - prev_response_time = None - i = 0 - prev_x, prev_y = 0, 0 + graph = window['-GRAPH-'] + + i = prev_x = prev_y = 0 + fig_list = [] + window.start_thread(lambda : ping_thread(window)) while True: - event, values = window.read(timeout=200) + event, values = window.read() if event == 'Quit' or event == sg.WIN_CLOSED: break - if g_response_time is None or prev_response_time == g_response_time: - continue - new_x, new_y = i, g_response_time[0] - prev_response_time = g_response_time + if event == '-THREAD-': + new_x, new_y = i, values[event] + window['-TIME-'].update(values[event]) if i >= SAMPLES: graph.move(-STEP_SIZE, 0) prev_x = prev_x - STEP_SIZE - graph.draw_line((prev_x, prev_y), (new_x, new_y), color='white') - # window['graph'].draw_point((new_x, new_y), color='red') + fig = fig_list[0] + fig_list.pop(0) + graph.delete_figure(fig) + # gc.collect() # Run garbage collect. Uncomment if you want the space freed immediately + fig = graph.draw_line((prev_x, prev_y), (new_x, new_y), color='white') + fig_list.append(fig) prev_x, prev_y = new_x, new_y i += STEP_SIZE if i < SAMPLES else 0 - - # tell thread we're done. wait for thread to exit - g_exit = True - thread.join() - + if event == '-X SLIDER-' or event == '-Y SLIDER-': + graph.delete_figure(fig_list) + graph.change_coordinates((0,0), (values['-X SLIDER-'], values['-Y SLIDER-'])) + graph.erase() + fig_list = [] + i = 0 + prev_x, prev_y = 0, 0 + SAMPLES = values['-X SLIDER-'] + if event == 'Set': # set a new URL to ping + ping_url = values['-URL-'] window.close() diff --git a/DemoPrograms/Demo_Layouts_Using_Walrus_Operator.py b/DemoPrograms/Demo_Layouts_Using_Walrus_Operator.py new file mode 100644 index 000000000..2d7883b9e --- /dev/null +++ b/DemoPrograms/Demo_Layouts_Using_Walrus_Operator.py @@ -0,0 +1,34 @@ +import PySimpleGUI as sg +import random + +""" + Using Python's Walrus Operator in Layouts + + Some elements you call many different memeber functions for. Rather than looking up the element by the key and storing + into a variable, you can use the walrus operator to store the element, right from the layout itself. + + Copyright 2024 PySimpleSoft, Inc. and/or its licensors. All rights reserved. + + Redistribution, modification, or any other use of PySimpleGUI or any portion thereof is subject to the terms of the PySimpleGUI License Agreement available at https://eula.pysimplegui.com. + + You may not redistribute, modify or otherwise use PySimpleGUI or its contents except pursuant to the PySimpleGUI License Agreement. +""" + +layout = [[sg.Text('Using Walrus Operator In Layouts', font='_ 16')], + [graph_elem := sg.Graph((500, 500), (0, 0), (100, 100), key='-GRAPH-')], + [sg.Button('Draw'), sg.Button('Exit')]] + +window = sg.Window('Walrus Operator In Layouts', layout, auto_save_location=True) + +# graph_elem = window['-GRAPH-'] # This is the way elements are normally looked up and stored in a variable + +while True: + event, values = window.read() + if event == sg.WIN_CLOSED or event == 'Exit': + break + if event == 'Draw': + emoji = random.choice(sg.EMOJI_BASE64_HAPPY_LIST) + location = random.randint(0, 100), random.randint(0, 100) + graph_elem.draw_image(data=emoji, location=location) + +window.close() diff --git a/DemoPrograms/Demo_Matplotlib_Ping_Graph_Large.py b/DemoPrograms/Demo_Matplotlib_Ping_Graph_Large.py index e15b973d2..9794db4ad 100644 --- a/DemoPrograms/Demo_Matplotlib_Ping_Graph_Large.py +++ b/DemoPrograms/Demo_Matplotlib_Ping_Graph_Large.py @@ -1,13 +1,18 @@ #!/usr/bin/env python -from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, FigureCanvasAgg -import matplotlib.backends.tkagg as tkagg +from matplotlib.backends.backend_tkagg import FigureCanvasAgg import matplotlib.pyplot as plt import PySimpleGUI as sg -import tkinter as tk -import ping +import io +import random +import time +import ping3 """ - Copyright 2023 PySimpleSoft, Inc. and/or its licensors. All rights reserved. + Shows ping time to google.com using Matplotlib and ping3 module. + + Note that you will need to pip install ping3 for this demo program. + + Copyright 2023-2024 PySimpleSoft, Inc. and/or its licensors. All rights reserved. Redistribution, modification, or any other use of PySimpleGUI or any portion thereof is subject to the terms of the PySimpleGUI License Agreement available at https://eula.pysimplegui.com. @@ -34,16 +39,13 @@ class MyGlobals: # ================================================================================ -def run_a_ping_and_graph(): +def graph_a_ping(ping_time): # graphs are global so that can be retained across multiple calls to this callback global g_my_globals #===================== Do the ping =====================# - response = ping.quiet_ping('google.com', timeout=1000) - if response[0] == 0: - ping_time = 1000 - else: - ping_time = response[0] + # Insert your code to run a ping + # ping_time = random.randint(0, 100) #===================== Store current ping in historical array =====================# g_my_globals.ping_x_array.append(len(g_my_globals.ping_x_array)) g_my_globals.ping_y_array.append(ping_time) @@ -69,6 +71,14 @@ def run_a_ping_and_graph(): # ================================================================================ +def ping_thread(window: sg.Window): + while True: + # time.sleep(.1) + # ping_time = random.randint(0, 100) + ping_time = ping3.ping('google.com') + window.write_event_value('-THREAD-', ping_time) + + def set_chart_labels(): global g_my_globals @@ -77,16 +87,25 @@ def set_chart_labels(): g_my_globals.axis_ping.set_title('Current Ping Duration', fontsize=12) -def draw(fig, canvas): - # Magic code that draws the figure onto the Canvas Element's canvas - figure_x, figure_y, figure_w, figure_h = fig.bbox.bounds - figure_w, figure_h = int(figure_w), int(figure_h) - photo = tk.PhotoImage(master=canvas, width=figure_w, height=figure_h) - canvas.create_image(640 / 2, 480 / 2, image=photo) - figure_canvas_agg = FigureCanvasAgg(fig) - figure_canvas_agg.draw() - tkagg.blit(photo, figure_canvas_agg.get_renderer()._renderer, colormode=2) - return photo +def draw(element, figure): + """ + Draws the previously created "figure" in the supplied Image Element + + :param element: an Image Element + :param figure: a Matplotlib figure + :return: The figure canvas + """ + + # plt.close('all') # erases previously drawn plots + canv = FigureCanvasAgg(figure) + buf = io.BytesIO() + canv.print_figure(buf, format='png') + if buf is not None: + buf.seek(0) + element.update(data=buf.read()) + return canv + else: + return None # ================================================================================ # Function: MAIN @@ -99,28 +118,28 @@ def main(): # define the form layout layout = [[sg.Text('Animated Ping', size=(40, 1), justification='center', font='Helvetica 20')], - [sg.Canvas(size=(640, 480), key='canvas')], + [sg.Image(size=(640, 480), key='-IMAGE-')], [sg.Button('Exit', size=(10, 2), pad=((280, 0), 3), font='Helvetica 14')]] # create the form and show it without the plot window = sg.Window( 'Demo Application - Embedding Matplotlib In PySimpleGUI', layout, finalize=True) - canvas_elem = window['canvas'] - canvas = canvas_elem.TKCanvas + image_elem = window['-IMAGE-'] fig = plt.figure() g_my_globals.axis_ping = fig.add_subplot(1, 1, 1) set_chart_labels() plt.tight_layout() - + window.start_thread(lambda: ping_thread(window)) while True: event, values = window.read(timeout=0) if event in ('Exit', None): break + if event == '-THREAD-': + graph_a_ping(values[event]) + draw(image_elem, fig) - run_a_ping_and_graph() - photo = draw(fig, canvas) if __name__ == '__main__': diff --git a/DemoPrograms/Demo_Multithreaded_Different_Threads.py b/DemoPrograms/Demo_Multithreaded_Different_Threads.py index b83c68f90..b74dc5e7a 100644 --- a/DemoPrograms/Demo_Multithreaded_Different_Threads.py +++ b/DemoPrograms/Demo_Multithreaded_Different_Threads.py @@ -1,5 +1,4 @@ #!/usr/bin/python3 -import threading import time import itertools import PySimpleGUI as sg @@ -15,7 +14,7 @@ The PySimpleGUI code is structured just like a typical PySimpleGUI program. A layout defined, a Window is created, and an event loop is executed. - Copyright 2020-2023 PySimpleSoft, Inc. and/or its licensors. All rights reserved. + Copyright 2020-2024 PySimpleSoft, Inc. and/or its licensors. All rights reserved. Redistribution, modification, or any other use of PySimpleGUI or any portion thereof is subject to the terms of the PySimpleGUI License Agreement available at https://eula.pysimplegui.com. @@ -96,7 +95,7 @@ def worker_thread3(thread_name, run_freq, window): # ###### ####### #### -def the_gui(): +def main(): """ Starts and executes the GUI Reads data from a Queue and displays the data to the window @@ -108,16 +107,16 @@ def the_gui(): layout = [[sg.Text('Multithreaded Window Example')], [sg.Text('', size=(15, 1), key='-OUTPUT-')], [sg.Multiline(size=(40, 26), key='-ML-', autoscroll=True)], - [sg.Button('Exit')], ] + [sg.Push(), sg.Button('Exit')], ] window = sg.Window('Multithreaded Window', layout, finalize=True) # -- Create a Queue to communicate with GUI -- # queue used to communicate between the gui and the threads # -- Start worker threads, each taking a different amount of time - threading.Thread(target=worker_thread1, args=('Thread 1', 500, window,), daemon=True).start() - threading.Thread(target=worker_thread2, args=('Thread 2', 200, window,), daemon=True).start() - threading.Thread(target=worker_thread3, args=('Thread 3', 1000, window,), daemon=True).start() + window.start_thread(lambda: worker_thread1('Thread 1', 500, window)) + window.start_thread(lambda: worker_thread2('Thread 2', 200, window)) + window.start_thread(lambda: worker_thread3('Thread 3', 1000, window)) # -- Start the GUI passing in the Queue -- sg.cprint_set_output_destination(window, '-ML-') @@ -135,14 +134,6 @@ def the_gui(): window.close() -## ## ### #### ## ## -### ### ## ## ## ### ## -#### #### ## ## ## #### ## -## ### ## ## ## ## ## ## ## -## ## ######### ## ## #### -## ## ## ## ## ## ### -## ## ## ## #### ## ## - if __name__ == '__main__': - the_gui() + main() diff --git a/DemoPrograms/Demo_Multithreaded_Signal_Thread_To_End.py b/DemoPrograms/Demo_Multithreaded_Signal_Thread_To_End.py new file mode 100644 index 000000000..9b7989599 --- /dev/null +++ b/DemoPrograms/Demo_Multithreaded_Signal_Thread_To_End.py @@ -0,0 +1,86 @@ +import time +import datetime +import PySimpleGUI as sg + +""" + Multithreading with signaling to thread when to stop. + If exiting the program, waits for the thread to finish. + + In this example, the thread runs at a rate of twice a second. It sends the time as a string + The main GUI checks the value sent by the thread to see if it differs from what is displayed. + If the display is different, then the GUI is updated with the new time. + + Copyright 2020-2024 PySimpleSoft, Inc. and/or its licensors. All rights reserved. + + Redistribution, modification, or any other use of PySimpleGUI or any portion thereof is subject to the terms of the PySimpleGUI License Agreement available at https://eula.pysimplegui.com. + + You may not redistribute, modify or otherwise use PySimpleGUI or its contents except pursuant to the PySimpleGUI License Agreement. + +""" + + +# dP dP dP +# 88 88 88 +# d8888P 88d888b. 88d888b. .d8888b. .d8888b. .d888b88 +# 88 88' `88 88' `88 88ooood8 88' `88 88' `88 +# 88 88 88 88 88. ... 88. .88 88. .88 +# dP dP dP dP `88888P' `88888P8 `88888P8 +# + +def the_thread(window): + + while window.job_running: + window.write_event_value('-THREAD-', datetime.datetime.now().strftime('%H:%M:%S')) + time.sleep(0.5) + + +# oo +# +# 88d8b.d8b. .d8888b. dP 88d888b. +# 88'`88'`88 88' `88 88 88' `88 +# 88 88 88 88. .88 88 88 88 +# dP dP dP `88888P8 dP dP dP + +def main(): + + layout = [ + [sg.Text('00:00:00', font=('Courier New', 20, 'bold'), justification='center', expand_x=True, key='-TIME-')], + [sg.Push(), sg.Button('Start'), sg.Button('Stop')]] + + window = sg.Window('Threading', layout, enable_close_attempted_event=True, + print_event_values=True, # enable to watch the events and values print out + ) + + window.job_running = False # Create a member variable to signal to the thread when to stop + exiting = False # Used when X is clicked + + while True: + + event, values = window.read() + + if event == sg.WINDOW_CLOSE_ATTEMPTED_EVENT: + if window.job_running: # if thread running, tell it to exit + window.job_running = False + exiting = True + else: + break # if thread not running then OK to exit + + if exiting and event == '-THREAD ENDED-': # If exiting and thread is finished then OK to exit + break + + elif event == 'Start': + window['Start'].update(disabled=True) + window.job_running = True + window.start_thread(lambda: the_thread(window), '-THREAD ENDED-') + + elif event == 'Stop': + window.job_running = False + window['Start'].update(disabled=False) + + elif event == '-THREAD-' and values[event] != window['-TIME-'].get(): + window['-TIME-'].update(values[event]) + + window.close() + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/DemoPrograms/Demo_Multithreaded_Write_Event_Value.py b/DemoPrograms/Demo_Multithreaded_Write_Event_Value.py index 0ebdb7f2c..e3629282e 100644 --- a/DemoPrograms/Demo_Multithreaded_Write_Event_Value.py +++ b/DemoPrograms/Demo_Multithreaded_Write_Event_Value.py @@ -1,4 +1,3 @@ -import threading import time import PySimpleGUI as sg @@ -6,24 +5,17 @@ """ Threaded Demo - Uses Window.write_event_value communications - Requires PySimpleGUI.py version 4.25.0 and later - - This is a really important demo to understand if you're going to be using multithreading in PySimpleGUI. - - Older mechanisms for multi-threading in PySimpleGUI relied on polling of a queue. The management of a communications - queue is now performed internally to PySimpleGUI. + The only PySimpleGUI call allowed from a thread is write_event_value. + Using a tuple for thread events makes processing them easier to see and understand. - The importance of using the new window.write_event_value call cannot be emphasized enough. It will hav a HUGE impact, in - a positive way, on your code to move to this mechanism as your code will simply "pend" waiting for an event rather than polling. - Copyright 2020-2023 PySimpleSoft, Inc. and/or its licensors. All rights reserved. + Copyright 2020-2024 PySimpleSoft, Inc. and/or its licensors. All rights reserved. Redistribution, modification, or any other use of PySimpleGUI or any portion thereof is subject to the terms of the PySimpleGUI License Agreement available at https://eula.pysimplegui.com. You may not redistribute, modify or otherwise use PySimpleGUI or its contents except pursuant to the PySimpleGUI License Agreement. """ -THREAD_EVENT = '-THREAD-' def the_thread(window): """ @@ -31,12 +23,13 @@ def the_thread(window): Once a second wakes and sends a new event and associated value to the window """ - i = 0 - while True: - time.sleep(1) - window.write_event_value('-THREAD-', (threading.current_thread().name, i)) # Data sent is a tuple of thread name and counter - i += 1 + window.write_event_value(('-THREAD-', '-STARTED-'), None) # Tell the GUI the thread started + + for i in range(5): + time.sleep(1) + window.write_event_value(('-THREAD-', '-PRINT-'), i) # Data sent is a tuple of thread name and counter + # Note that the thread ended event is sent automatically by PySimpleGUI if you started the thread using window.start_thread def main(): """ @@ -50,20 +43,26 @@ def main(): [sg.Multiline(size=(65,20), key='-ML-', autoscroll=True, reroute_stdout=True, write_only=True, reroute_cprint=True)], [sg.T('Input so you can see data in your dictionary')], [sg.Input(key='-IN-', size=(30,1))], - [sg.B('Start A Thread'), sg.B('Dummy'), sg.Button('Exit')] ] + [sg.B('Start A Thread', key='-START-'), sg.B('Dummy'), sg.Button('Exit')] ] - window = sg.Window('Window Title', layout) + window = sg.Window('Multithreading + Tuple Events', layout) while True: # Event Loop event, values = window.read() sg.cprint(event, values) if event == sg.WIN_CLOSED or event == 'Exit': break - if event.startswith('Start'): - threading.Thread(target=the_thread, args=(window,), daemon=True).start() - if event == THREAD_EVENT: - sg.cprint(f'Data from the thread ', colors='white on purple', end='') - sg.cprint(f'{values[THREAD_EVENT]}', colors='white on red') + if event == '-START-': + window.start_thread(lambda : the_thread(window), ('-THREAD-', '-ENDED-')) + if event[0] == '-THREAD-': + if event[1] == '-PRINT-': + sg.cprint(f'Data from the thread ', colors='white on purple', end='') + sg.cprint(f'{values[event]}', colors='white on red') + elif event[1] == '-ENDED-': + sg.cprint('Thread has ended', colors='white on blue') + elif event[1] == '-STARTED-': + sg.cprint('Thread has started', colors='white on green') + window.close() diff --git a/DemoPrograms/Demo_Net_API_Download_Image.py b/DemoPrograms/Demo_Net_API_Download_Image.py new file mode 100644 index 000000000..055e84984 --- /dev/null +++ b/DemoPrograms/Demo_Net_API_Download_Image.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python +import PySimpleGUI as sg + + +""" + Demo of Net API - Download a PNG Image using net_download_file_binary & Display in a Window + + This Demo Program shows how to download a binary file (an image) and display it in a window. + + + Copyright 2018-2023 PySimpleSoft, Inc. and/or its licensors. All rights reserved. + + Redistribution, modification, or any other use of PySimpleGUI or any portion thereof is subject to the terms of the PySimpleGUI License Agreement available at https://eula.pysimplegui.com. + + You may not redistribute, modify or otherwise use PySimpleGUI or its contents except pursuant to the PySimpleGUI License Agreement. +""" + + +sg.theme('black') + +# First the easy case.... display a PNG image + +sg.Window('PNG Download', [[sg.Image(sg.net_download_file_binary(r'https://pysimplegui.net/images/tests/powered-by-pysimplegui-5.png'))]]).read(close=True) + diff --git a/DemoPrograms/Demo_Net_API_Download_Image_jpg.py b/DemoPrograms/Demo_Net_API_Download_Image_jpg.py new file mode 100644 index 000000000..0736f884d --- /dev/null +++ b/DemoPrograms/Demo_Net_API_Download_Image_jpg.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python +import PySimpleGUI as sg + +# Only need these imports if using JPGs +from PIL import Image +from io import BytesIO + + +""" + Demo of Net API - Download JPG Image using net_download_file_binary and display in a window + + The download is the easiest part. It's the displaying of a JPG that's tricky. + + + Copyright 2018-2023 PySimpleSoft, Inc. and/or its licensors. All rights reserved. + + Redistribution, modification, or any other use of PySimpleGUI or any portion thereof is subject to the terms of the PySimpleGUI License Agreement available at https://eula.pysimplegui.com. + + You may not redistribute, modify or otherwise use PySimpleGUI or its contents except pursuant to the PySimpleGUI License Agreement. +""" + + + +# Download the binary file (into a variable, not a local file) +jpg_data = sg.net_download_file_binary(r'https://pysimplegui.net/images/tests/powered-by-pysimplegui-5.jpg') + +# Now display a JPG image. It requires converting the image to PNG before using with PySimpleGUI +# Note a commented out version below of this same code. Here the context manager was removed for simplicity. +jpg_image = Image.open(BytesIO(jpg_data)) +bio = BytesIO() +jpg_image.save(bio, format='PNG') + +sg.Window('Download JPG', [[sg.Image(data=bio.getvalue())]]).read(close=True) + +del bio # delete the BytesIO object since done with it + + + + +# This is another implementation using the io module with a context manager +# The "with" statement makes understanding what's going on a little more difficult +# but is perhaps the "preferred" method to use. +""" +jpg_image = Image.open(BytesIO(jpg_data)) + +with BytesIO() as bio: + jpg_image.save(bio, format='PNG') + data = bio.getvalue() + +sg.Window('Download JPG', [[sg.Image(data=data)]]).read(close=True) +""" \ No newline at end of file diff --git a/DemoPrograms/Demo_Pip_Installs.py b/DemoPrograms/Demo_Pip_Installs.py new file mode 100644 index 000000000..c5a58d17b --- /dev/null +++ b/DemoPrograms/Demo_Pip_Installs.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python +""" + Obtain the version number for a package installed on any versions of Python on your system by uysing + the sg.execute_pip_get_local_package_version() call. + + Copyright 2024 PySimpleSoft, Inc. and/or its licensors. All rights reserved. + + Redistribution, modification, or any other use of PySimpleGUI or any portion thereof is subject to the terms of the PySimpleGUI License Agreement available at https://eula.pysimplegui.com. + + You may not redistribute, modify or otherwise use PySimpleGUI or its contents except pursuant to the PySimpleGUI License Agreement. +""" + +import PySimpleGUI as sg + + +layout = [ [sg.T('Package Name:'), sg.In(s=15, setting='', k='-PACKAGE-')], + [sg.T('Full path to interpreter'), sg.In(setting='', k='-INT-')], + [sg.CB('Use threading', setting=False, k='-THREADED-')], + [sg.MLine(s=(80,20), reroute_cprint=True, k='-ML-')], + [sg.Button('Show', bind_return_key=True), sg.Button('Exit')] ] + +window = sg.Window('Get PIP Installed Versions', layout, print_event_values=False, enable_close_attempted_event=True, auto_save_location=True) + +while True: # Event Loop + event, values = window.read() + if event == sg.WINDOW_CLOSE_ATTEMPTED_EVENT: + window.settings_save(values) + break + if event in (None, 'Exit'): + break + if event == 'Show': + package = values['-PACKAGE-'] + if not package: + continue + + win = window if values['-THREADED-'] else None + + out = sg.execute_pip_get_local_package_version(package, interpreter=values['-INT-'] if values['-INT-'] else None, window=win, key='-PIP VER-') + if win is None: + if not out: + out = 'Not Installed' + sg.cprint(f'Not threaded {package} ', end='', c='white on green') + sg.cprint(f'version = {out}', end='', c='white on red') + sg.cprint('') + elif event == '-PIP VER-': + out = values[event] if values[event] else 'Not Installed' + sg.cprint(f'{package} ', end='', c='white on blue') + sg.cprint(f'version = {out}', end='', c='white on red') + sg.cprint('') + +window.close() diff --git a/DemoPrograms/Demo_LED_Clock_Weather.py b/DemoPrograms/Demo_Program_Desktop_Widget_LED_Clock_Weather.py similarity index 92% rename from DemoPrograms/Demo_LED_Clock_Weather.py rename to DemoPrograms/Demo_Program_Desktop_Widget_LED_Clock_Weather.py index 00fc8df76..246cbac5c 100644 --- a/DemoPrograms/Demo_LED_Clock_Weather.py +++ b/DemoPrograms/Demo_Program_Desktop_Widget_LED_Clock_Weather.py @@ -2,86 +2,102 @@ import PySimpleGUI as sg import datetime import calendar -import forecastio +import requests +import webbrowser ''' Example of a weather App, using: - - DARKSKY - - google maps coordinates + - openweathermap.org - Copyright 2023 PySimpleSoft, Inc. and/or its licensors. All rights reserved. + + Copyright 2023-2024 PySimpleSoft, Inc. and/or its licensors. All rights reserved. + Redistribution, modification, or any other use of PySimpleGUI or any portion thereof is subject to the terms of the PySimpleGUI License Agreement available at https://eula.pysimplegui.com. - + You may not redistribute, modify or otherwise use PySimpleGUI or its contents except pursuant to the PySimpleGUI License Agreement. ''' -##### CHANGE these settings to match your location... check Google Maps ##### -MY_LOCATION_LAT = 35.000000 -MY_LOCATION_LON = -79.000000 -# MY_LOCATION_LAT = 37.7568 -# MY_LOCATION_LON = -87.1191 -# MY_LOCATION_LAT = 41.5726 +NUM_COLS = 5 # Changes number of days in forecast (don't change this) -# MY_LOCATION_LON = -93.6102 +def settings_window(location=(None, None)): + tsize = 15 + layout = [[sg.Text('Settings')], + [sg.T('API Key', s=tsize, justification='r'), sg.I(s=35, setting='', k='-api key-')], + [sg.T('Latitude', s=tsize, justification='r'), sg.I(s=12, setting='', k='-lat-')], + [sg.T('Longitude', s=tsize, justification='r'), sg.I(s=12, setting='', k='-lon-')], + [sg.CB('Use F Degrees', s=tsize, setting=True, k='-faren-')], + [sg.OK(), sg.B('Register with openweathermap.org for API key', k='-register-'), sg.Cancel()], + ] + + window = sg.Window('Settings', layout, location=location) + + while True: + event, values = window.read() + if event == sg.WIN_CLOSED or event == 'Cancel': + break + if event == 'OK': + window.settings_save(values) + break + if event == '-register-': + webbrowser.open(r'https://home.openweathermap.org/users/sign_up') -##### You need a free dark-sky key. You get 1000 calls a month for free ##### -# *** INSERT YOUR DARKSKY KEY HERE ** -DARKSKY_KEY = "INSERT YOUR DARKSKY KEY HERE!" + window.close() + return event == 'OK' -NUM_COLS = 8 # Changes number of days in forecast -USE_CELCIUS = False class GUI(): - def __init__(self, location): - self.api_key = DARKSKY_KEY - self.lat = MY_LOCATION_LAT - self.lng = MY_LOCATION_LON + lat = 0 + lon = 0 + api_key = '' + faren = True + def __init__(self): self.blink_count = 0 - - sg.set_options(border_width=0, text_color='white', - background_color='black', text_element_background_color='black') + sg.theme('black') + sg.set_options(border_width=0) # Create clock layout clock = [ - [sg.T('', pad=((220,0),0)), - sg.Image(data=ledblank, key='-HOUR1-'), - sg.Image(data=ledblank, key='-HOUR2-'), - sg.Image(data=ledblank, key='-COLON-'), - sg.Image(data=ledblank, key='-MIN1-'), - sg.Image(data=ledblank, key='-MIN2-')], ] + [ + sg.Image(data=ledblank, key='-HOUR1-'), + sg.Image(data=ledblank, key='-HOUR2-'), + sg.Image(data=ledblank, key='-COLON-'), + sg.Image(data=ledblank, key='-MIN1-'), + sg.Image(data=ledblank, key='-MIN2-')]] # Create the weather columns layout weather_cols = [] for i in range(NUM_COLS): weather_cols.append( [[sg.T('', size=(4, 1), font='Any 20', justification='center', key='_DAY_' + str(i)), ], - [sg.Image(data=w1, background_color='black', key='-ICON-'+str(i), pad=((4, 0), 3)), ], + [sg.Image(data=w1, background_color='black', key='-ICON-' + str(i), pad=((4, 0), 3)), ], [sg.T('--', size=(3, 1), justification='center', font='Any 20', key='_high_' + str(i), pad=((10, 0), 3))], - [sg.T('--', size=(3, 1), justification='center', font='Any 20', key='_low_' + str(i), pad=((10, 0), 3))]]) + # [sg.T('--', size=(3, 1), justification='center', font='Any 20', key='_low_' + str(i), pad=((10, 0), 3))], + ]) # Create the overall layout - layout = [[sg.Column(clock, background_color='black')], - [sg.Column(weather_cols[x], background_color='black') for x in range(NUM_COLS)] + [sg.T('×',enable_events=True, key='Exit')] + [sg.T('C',enable_events=True, key='-CELCIUS-')], - # [sg.RButton('Exit', button_color=('black', 'black'), - # image_data=orangeround, tooltip='close window', pad=((450,0),(10,0)))] + layout = [[sg.Column(clock, background_color='black', justification='c')], + [sg.Column(weather_cols[x], background_color='black') for x in range(NUM_COLS)] + [sg.T('×', enable_events=True, key='Exit')] + [ + sg.T('C' if GUI.faren else 'F', enable_events=True, key='-CELCIUS-')], ] + right_click = sg.MENU_RIGHT_CLICK_EDITME_VER_EXIT + right_click[1].append('Settings') # Create the window self.window = sg.Window('DarkSky Weather Forecast Widget', layout, - background_color='black', - grab_anywhere=True, - use_default_focus=False, - no_titlebar=True, - alpha_channel=.8, # set an alpha channel if want transparent - location=location, - right_click_menu=[[''], ['Edit Me', 'Exit',]], - enable_close_attempted_event=True, - finalize=True) + background_color='black', + grab_anywhere=True, + use_default_focus=False, + no_titlebar=True, + alpha_channel=.8, # set an alpha channel if want transparent + right_click_menu=sg.MENU_RIGHT_CLICK_EDITME_VER_EXIT, + enable_close_attempted_event=True, + auto_save_location=True, + finalize=True) self.colon_elem = self.window.find_element('-COLON-') self.hour1 = self.window.find_element('-HOUR1-') @@ -89,8 +105,8 @@ def __init__(self, location): self.min1 = self.window.find_element('-MIN1-') self.min2 = self.window.find_element('-MIN2-') - self.window['Exit'].set_cursor('hand2') - self.window['-CELCIUS-'].set_cursor('hand2') + self.window['Exit'].set_cursor('hand1') + self.window['-CELCIUS-'].set_cursor('hand1') def update_clock(self): # update the clock @@ -102,49 +118,61 @@ def update_clock(self): self.min2.Update(data=led_digits[int(now.minute) % 10]) self.min1.Update(data=led_digits[int(now.minute) // 10]) # Blink the : - self.colon_elem.Update(data=ledcolon if self.blink_count %2 else ledblank) + self.colon_elem.Update(data=ledcolon if self.blink_count % 2 else ledblank) self.blink_count += 1 def update_weather(self): - forecast = forecastio.load_forecast(self.api_key, self.lat, self.lng) - daily = forecast.daily() today_weekday = datetime.datetime.today().weekday() - max_temps = [] - min_temps = [] - daily_icons = [] - for daily_data in daily.data: - daily_icons.append(daily_data.d['icon']) - max_temps.append(int(daily_data.d['temperatureMax'])) - min_temps.append(int(daily_data.d['temperatureMin'])) - - for i in range(NUM_COLS): - day_element = self.window.find_element('_DAY_' + str(i)) - max_element = self.window.find_element('_high_' + str(i)) - min_element = self.window.find_element('_low_' + str(i)) - icon_element = self.window.find_element('-ICON-' + str(i)) - day_element.Update(calendar.day_abbr[(today_weekday + i) % 7]) - max_temp = max_temps[i] - min_temp = min_temps[i] - if USE_CELCIUS: - max_temp = int((max_temp-32)/1.8) - min_temp = int((min_temp-32)/1.8) - max_element.Update(max_temp) - min_element.Update(min_temp) - icon_data = weather_icon_dict[daily_icons[i]] - # sg.Print(icon_data, wait=True) - try: - icon_element.update(icon_data) - except: - pass + try: + # Make the API + units = 'imperial' if GUI.faren else 'metric' + api_url = f'http://api.openweathermap.org/data/2.5/forecast?lat={GUI.lat}&lon={GUI.lon}&units={units}&appid={GUI.api_key}' + + response = requests.get(api_url) + data = response.json() + # print(data) + # Extract relevant information (e.g., temperature, weather conditions) + i = 0 + for forecast in data["list"]: + date_time = forecast["dt_txt"] + if not '12:00:00' in date_time: + continue + # print(forecast) + temp = forecast["main"]["temp"] + weather_desc = forecast["weather"][0]["description"] + + print(f"{date_time}: {temp}°F, {weather_desc}") + day_element = self.window.find_element('_DAY_' + str(i)) + max_element = self.window.find_element('_high_' + str(i)) + icon_element = self.window.find_element('-ICON-' + str(i)) + day_element.Update(calendar.day_abbr[(today_weekday + i) % 7]) + # sg.Print(icon_data, wait=True) + try: + icon_data = weather_icon_dict[weather_desc] + icon_element.update(icon_data) + except: + sg.Print(f'Missing icon data for description: "{weather_desc}"') + max_element.update(int(temp)) + i += 1 + except requests.RequestException as e: + print(f"Error fetching data: {e}") def main(): - global USE_CELCIUS + if not sg.user_settings_get_entry('-api key-', None): + if not settings_window(): + sg.popup_error('Cancelling setup and exiting') + exit() + GUI.lat = sg.user_settings_get_entry('-lat-', 0) + GUI.lon = sg.user_settings_get_entry('-lon-', 0) + GUI.api_key = sg.user_settings_get_entry('-api key-', '') + GUI.faren = sg.user_settings_get_entry('-faren-', True) + # Get the GUI object that is used to update the window - location = sg.user_settings_get_entry('-location-', (None, None)) + # location = sg.user_settings_get_entry('-location-', (None, None)) - gui = GUI(location) + gui = GUI() # ---------- EVENT LOOP ---------- last_update_time = 0 @@ -152,19 +180,28 @@ def main(): # Wake up once a second to update the clock and weather event, values = gui.window.read(timeout=1000) if event in (None, 'Exit', sg.WIN_CLOSE_ATTEMPTED_EVENT): - sg.user_settings_set_entry('-location-', gui.window.current_location()) # The line of code to save the position before exiting break elif event == '-CELCIUS-': - USE_CELCIUS = not USE_CELCIUS + GUI.faren = not GUI.faren + sg.user_settings_set_entry('-faren-', GUI.faren) + gui.window['-CELCIUS-'].update('C' if GUI.faren else 'F') elif event == 'Edit Me': sg.execute_editor(__file__) - elif event == 'Last': - sg.popup(gui.window.last_right_click_widget) - # update clock + elif event == 'Version': + sg.main_get_debug_data() + elif event == 'Settings': + if settings_window(gui.window.current_location()): # if settings changed + GUI.lat = sg.user_settings_get_entry('-lat-', 0) + GUI.lon = sg.user_settings_get_entry('-lon-', 0) + GUI.api_key = sg.user_settings_get_entry('-api key-', '') + GUI.faren = sg.user_settings_get_entry('-faren-', True) + gui.window['-CELCIUS-'].update('C' if GUI.faren else 'F') + gui.update_weather() + gui.update_clock() # update weather once ever 6 hours now = datetime.datetime.now() - if last_update_time == 0 or (now-last_update_time).seconds >= 60*60*6 or event == '-CELCIUS-': + if last_update_time == 0 or (now - last_update_time).seconds >= 60 * 60 * 6 or event == '-CELCIUS-': print('*** Updating Weather ***') last_update_time = now gui.update_weather() @@ -205,8 +242,8 @@ def main(): w5 = b'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' orangeround = b'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' -weather_icon_dict = {'clear-day': w1, 'clear-night': w1, 'rain': w3, 'snow': w3, 'sleet': w3, 'wind': w3, 'fog': w3, - 'cloudy': w4, 'partly-cloudy-day': w5, 'partly-cloudy-night': w5} +weather_icon_dict = {'clear sky': w1, 'rain': w3, 'light rain':w3, 'shower rain':w3, 'thunderstorm':w3, 'snow': w3, 'sleet': w3, 'wind': w3, 'mist': w3, + 'cloudy': w4, 'scattered clouds': w5, 'few clouds': w5,'broken clouds': w5, 'overcast clouds': w5, 'partly-cloudy-night': w5} led_digits = [led0, led1, led2, led3, led4, led5, led6, led7, led8, led9] diff --git a/DemoPrograms/Demo_Smart_Desktop_Icon.pyw b/DemoPrograms/Demo_Smart_Desktop_Icon.pyw new file mode 100644 index 000000000..380b6890f --- /dev/null +++ b/DemoPrograms/Demo_Smart_Desktop_Icon.pyw @@ -0,0 +1,59 @@ +""" + +Creates what appears to be an icon on your desktop, but is in reality a PySimpleGUI program. + +Copyright 2024 PySimpleSoft, Inc. and/or its licensors. All rights reserved. + +Redistribution, modification, or any other use of PySimpleGUI or any portion thereof is subject to the terms of the PySimpleGUI License Agreement available at https://eula.pysimplegui.com. + +You may not redistribute, modify or otherwise use PySimpleGUI or its contents except pursuant to the PySimpleGUI License Agreement. +""" + + + +import PySimpleGUI as sg +import random + +def main(): + + PROGRAM_TO_LAUNCH_WHEN_DOUBLE_CLICKED = 'explorer' # This will be run when icon is double-clicked. Change to any program you want. + + # Set to your own custom icon. This is dislayed on the desktop + # For fun, the icon is changed every 5 minutes to a random PSG Emoji + icon=sg.EMOJI_BASE64_COOL + + #------- GUI definition & setup --------# + + sg.theme('black') + + + layout = [[sg.Image(source=icon, key='-IMAGE-', p=0, enable_events=True)]] + + window = sg.Window('Desktop Icon Demo', layout, element_justification='center', finalize=True, resizable=True, no_titlebar=True, right_click_menu=sg.MENU_RIGHT_CLICK_EDITME_VER_EXIT, margins=(0,0), grab_anywhere=True, auto_save_location=True) + + window['-IMAGE-'].bind('', '+DOUBLE_CLICK+') + + window.timer_start(5*60*1000) # every 5 minutes, change the icon (totally optional... just for fun) + + #------------ The Event Loop ------------# + while True: + event, values = window.read() + if event == sg.WIN_CLOSED or event == 'Exit': + break + # Add your double-click action here... such as launching another program + if event == '-IMAGE-+DOUBLE_CLICK+': + sg.popup_quick_message('Double Clicked', location=window.current_location(), font='_ 20', background_color='red', text_color='white') + # Example of launch when the icon is double-clicked. Of course you can do some other action than launching a program + sg.execute_command_subprocess(PROGRAM_TO_LAUNCH_WHEN_DOUBLE_CLICKED, wait=False) + elif event == sg.TIMER_KEY: # Change the icon shown every TIMER event + window['-IMAGE-'].update(random.choice(sg.EMOJI_BASE64_HAPPY_LIST)) + elif event == 'Version': + sg.popup_scrolled(sg.get_versions(), f'This Program: {__file__}' ,keep_on_top=True, non_blocking=True, location=window.current_location()) + elif event == 'Edit Me': + sg.execute_editor(__file__) + + window.close() + +if __name__ == '__main__': + + main() \ No newline at end of file diff --git a/DemoPrograms/Demo_Table_Checkmark.py b/DemoPrograms/Demo_Table_Checkmark.py index da554f4d9..f2e580d82 100644 --- a/DemoPrograms/Demo_Table_Checkmark.py +++ b/DemoPrograms/Demo_Table_Checkmark.py @@ -61,7 +61,7 @@ def make_table(num_rows, num_cols): # ------ Window Layout ------ layout = [[sg.Table(values=data[1:][:], headings=headings, max_col_width=25, auto_size_columns=False, col_widths=[10, 10, 20, 20 ,30, 5], display_row_numbers=True, justification='center', num_rows=20, key='-TABLE-', selected_row_colors='red on yellow', - expand_x=False, expand_y=True, vertical_scroll_only=False, enable_click_events=True, font='_ 14'), + expand_x=False, expand_y=True, vertical_scroll_only=False, enable_click_events=True, select_mode=sg.TABLE_SELECT_MODE_NONE, font='_ 14'), sg.Sizegrip()]] # ------ Create Window ------ diff --git a/DemoPrograms/Demo_Table_Element_Header_or_Cell_Clicks.py b/DemoPrograms/Demo_Table_Element_Header_or_Cell_Clicks.py index cac64fd3f..ef5061c57 100644 --- a/DemoPrograms/Demo_Table_Element_Header_or_Cell_Clicks.py +++ b/DemoPrograms/Demo_Table_Element_Header_or_Cell_Clicks.py @@ -5,30 +5,33 @@ import operator """ - Table Element Demo With Sorting - + Table Element Demo With Sorting and Cell Editing + NOTE: release 5.0.6.5 needed in order to use the Cell Editing features. Comment out the parameters that contain cell_edit to remove from demo + The data for the table is assumed to have HEADERS across the first row. This is often the case for CSV files or spreadsheets - In release 4.48.0 a new enable_click_events parameter was added to the Table Element - This enables you to click on Column Headers and individual cells as well as the standard Row selection - This demo shows how you can use these click events to sort your table by columns - Copyright 2022-2023 PySimpleSoft, Inc. and/or its licensors. All rights reserved. - + Copyright 2022-2024 PySimpleSoft, Inc. and/or its licensors. All rights reserved. + Redistribution, modification, or any other use of PySimpleGUI or any portion thereof is subject to the terms of the PySimpleGUI License Agreement available at https://eula.pysimplegui.com. - + You may not redistribute, modify or otherwise use PySimpleGUI or its contents except pursuant to the PySimpleGUI License Agreement. """ sg.theme('Light green 6') + + # ------ Some functions to help generate data for the table ------ def word(): return ''.join(random.choice(string.ascii_lowercase) for i in range(10)) + + def number(max_val=1000): return random.randint(0, max_val) + def make_table(num_rows, num_cols): data = [[j for j in range(num_cols)] for i in range(num_rows)] data[0] = [word() for _ in range(num_cols)] @@ -36,10 +39,12 @@ def make_table(num_rows, num_cols): data[i] = [i, word(), *[number() for i in range(num_cols - 2)]] return data + # ------ Make the Table Data ------ data = make_table(num_rows=15, num_cols=6) # headings = [str(data[0][x])+' ..' for x in range(len(data[0]))] -headings = [f'Col {col}' for col in range(1,len(data[0])+1)] +headings = [f'Col {col}' for col in range(1, len(data[0]) + 1)] + def sort_table(table, cols): """ sort a table by multiple columns @@ -55,6 +60,7 @@ def sort_table(table, cols): sg.popup_error('Error in sort_table', 'Exception in sort_table', e) return table + # ------ Window Layout ------ layout = [[sg.Table(values=data[1:][:], headings=headings + ['Extra'], max_col_width=25, auto_size_columns=True, @@ -68,7 +74,10 @@ def sort_table(table, cols): enable_events=True, expand_x=True, expand_y=True, - enable_click_events=True, # Comment out to not enable header and other clicks + enable_click_events=True, # Comment out to not enable header and other clicks + enable_cell_editing=True, # Comment out to if your PSG version does not support cell edint + cell_edit_colors='white on blue', # Comment out to if your PSG version does not support cell edint + cell_edit_select_colors='yellow on red', # Comment out to if your PSG version does not support cell edint tooltip='This is a table')], [sg.Button('Read'), sg.Button('Double'), sg.Button('Change Colors')], [sg.Text('Cell clicked:'), sg.T(k='-CLICKED-')], @@ -77,39 +86,33 @@ def sort_table(table, cols): [sg.Text('Change Colors = Changes the colors of rows 8 and 9'), sg.Sizegrip()]] # ------ Create Window ------ -window = sg.Window('The Table Element', layout, - # ttk_theme='clam', - resizable=True, right_click_menu=sg.MENU_RIGHT_CLICK_EDITME_VER_EXIT, finalize=True) +window = sg.Window('The Table Element', layout, resizable=True, right_click_menu=sg.MENU_RIGHT_CLICK_EDITME_VER_EXIT, finalize=True, print_event_values=True) -# Add the ability to double-click a cell -window["-TABLE-"].bind('' , "+-double click-") # ------ Event Loop ------ while True: event, values = window.read() - print(event, values) if event == sg.WIN_CLOSED or event == 'Exit': break if event == 'Edit Me': sg.execute_editor(__file__) elif event == 'Version': sg.popup_scrolled(__file__, sg.get_versions(), location=window.current_location(), keep_on_top=True, non_blocking=True) + if event == 'Read': + [print(row) for row in window['-TABLE-'].values] if event == 'Double': for i in range(1, len(data)): - data.append(data[i]) + data.append(data[i].copy()) window['-TABLE-'].update(values=data[1:][:]) elif event == 'Change Colors': window['-TABLE-'].update(row_colors=((8, 'white', 'red'), (9, 'green'))) - elif event == '-TABLE-+-double click-': - print('Last cell clicked was', window['-TABLE-'].get_last_clicked_position()) - if isinstance(event, tuple): - # TABLE CLICKED Event has value in format ('-TABLE=', '+CLICKED+', (row,col)) - # You can also call Table.get_last_clicked_position to get the cell clicked - if event[0] == '-TABLE-': - if event[2][0] == -1 and event[2][1] != -1: # Header was clicked and wasn't the "row" column - col_num_clicked = event[2][1] - new_table = sort_table(data[1:][:],(col_num_clicked, 0)) - window['-TABLE-'].update(new_table) - data = [data[0]] + new_table - window['-CLICKED-'].update(f'{event[2][0]},{event[2][1]}') + + # See if was a table clicked or edited event by checking event[0] for table's key + if event[0] == '-TABLE-': # TABLE CELL Event has value in format ('-TABLE=', '+type of event+', (row,col)) + if event[2][0] == -1 and event[2][1] != -1: # Header was clicked and wasn't the "row" column + col_num_clicked = event[2][1] + new_table = sort_table(data[1:][:], (col_num_clicked, 0)) + window['-TABLE-'].update(new_table) + data = [data[0]] + new_table + window['-CLICKED-'].update(f'{event[2][0]},{event[2][1]}') window.close() diff --git a/DemoPrograms/Demo_Tree_Element.py b/DemoPrograms/Demo_Tree_Element.py index 27473c604..85532cf6f 100644 --- a/DemoPrograms/Demo_Tree_Element.py +++ b/DemoPrograms/Demo_Tree_Element.py @@ -50,6 +50,7 @@ def add_files_in_folder(parent, dirname): select_mode=sg.TABLE_SELECT_MODE_EXTENDED, num_rows=20, col0_width=40, + col0_heading='Files & Folders', key='-TREE-', show_expanded=False, enable_events=True, diff --git a/DemoPrograms/Demo_User_Settings_Element_setting_Parameter.py b/DemoPrograms/Demo_User_Settings_Element_setting_Parameter.py new file mode 100644 index 000000000..a9a17538c --- /dev/null +++ b/DemoPrograms/Demo_User_Settings_Element_setting_Parameter.py @@ -0,0 +1,48 @@ +import PySimpleGUI as sg + +""" + Demo - User Settings + + Using the PySimpleGUI 5 setting parameter. + + New in the PSG5 release is a capability to automatically save and restore values in elements. Each element that has the capability has a parameter called "setting" + + Copyright 2020-2023 PySimpleSoft, Inc. and/or its licensors. All rights reserved. + + Redistribution, modification, or any other use of PySimpleGUI or any portion thereof is subject to the terms of the PySimpleGUI License Agreement available at https://eula.pysimplegui.com. + + You may not redistribute, modify or otherwise use PySimpleGUI or its contents except pursuant to the PySimpleGUI License Agreement. +""" + + +def make_window(): + layout = [[sg.Text('Window with values saved between runs')], + [sg.Input(key='-IN-', setting='My initial value')], + [sg.Checkbox('Checkbox', key='-CB-', setting=True)], + [sg.Button('Re-create Window'), sg.Button('Exit')]] + + window = sg.Window('Setting Example', layout, enable_close_attempted_event=True, print_event_values=True, auto_save_location=True) + + return window + + +def main(): + + window = make_window() + + while True: + event, values = window.read() + if event == sg.WIN_CLOSED or event == 'Exit' or event == sg.WIN_CLOSE_ATTEMPTED_EVENT: + window.settings_save(values) + break + + if event == 'Re-create Window': # You can also close and re-open a window using the values previously entered + window.settings_save(values) + window.close() + window = make_window() + + window.close() + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt index d688c9b82..6a6cce6db 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,6 +1,6 @@ PySimpleGUI License Agreement -Version 1.0, Last updated: January 17, 2024 +Version 1.1, Last updated: March 26, 2024 This PySimpleGUI License Agreement (the "Agreement") governs the use, reproduction, distribution, modification and all other exploitation of @@ -58,9 +58,8 @@ follows. more of Licensee's own applications which make use of the Library as a dependency in accordance with Section 1.5 (collectively, "Licensee Applications") and is either (1) a Hobbyist Developer; or (2) a Commercial - Developer who has purchased an active PySimpleGUI paid license hereunder, in - effect at the time of development, which is fully paid up pursuant to Section - 3. + Developer who has purchased an active PySimpleGUI paid license hereunder + which is fully paid up pursuant to Section 3. * "Hobbyist Developer" means any individual who uses PySimpleGUI for development purposes solely for either or both of the following: (1) personal @@ -132,10 +131,11 @@ install, use, execute, reproduce and modify the Utilities, but not to distribute or publish the Utilities or any modified version. 1.2.5. Developer Key Required. The licenses granted in this Section 1.2 may -only be exercised by Authorized Developers within the period of time during -which each such Authorized Developer has a then-active Developer Key pursuant -to Section 3. Licensor may in its discretion permit recipients of PySimpleGUI -to make limited use of it for a limited trial period without a Developer Key. +only be exercised by Authorized Developers. For Hobbyist Developers, these +licenses may only be exercised within the period of time during which each such +Hobbyist Developer has a then-active Developer Key pursuant to Section 3. +Licensor may in its discretion permit recipients of PySimpleGUI to make limited +use of it for a limited trial period without a Developer Key. 1.2.6. Limitations for Hobbyist Developers. For Hobbyist Developers, the licenses granted in this Section 1.2 may only be exercised for the Permitted @@ -310,20 +310,23 @@ Licensor Marks shall inure to the benefit of Licensor. 3.1. Developer Keys. In order to develop Licensee Applications pursuant to Section 1.2 (and subject to any limited trial period usage as may be permitted by Licensor from time to time), each Authorized Developer shall obtain a -PySimpleGUI developer license key ("Developer Key") by registering on the Site -as set forth therein. Each Developer Key is personal to the specific Authorized -Developer, and Licensee shall not permit Authorized Developers to disclose, -share or reuse Developer Keys. For the avoidance of doubt, any disclosure, -sharing or reuse of a Developer Key by Licensee's Authorized Developers, -whether or not authorized by Licensee, shall be a material breach permitting -termination of this Agreement pursuant to Section 8.3. Developer Keys are -Licensor's Confidential Information pursuant to Section 5. Developer Keys are -limited to a specified time period (which shall be annual from the start date -of the Developer Key, unless otherwise explicitly stated by Licensor). Upon the -expiration of a Developer Key, the corresponding Authorized Developer may no -longer use the Developer Key and must obtain a new Developer Key from the Site -in order to continue using PySimpleGUI for development purposes pursuant to -Section 1.2. +PySimpleGUI developer license key ("Developer Key") by registering on the +Site as set forth therein. Each Developer Key is personal to the specific +Authorized Developer, and Licensee shall not permit Authorized Developers to +disclose, share or reuse Developer Keys. For the avoidance of doubt, any +disclosure, sharing or reuse of a Developer Key by Licensee's Authorized +Developers, whether or not authorized by Licensee, shall be a material breach +permitting termination of this Agreement pursuant to Section 8.3. Developer +Keys are Licensor's Confidential Information pursuant to Section 5. Developer +Keys are limited to a specified time period (which shall be annual from the +start date of the Developer Key, unless otherwise explicitly stated by +Licensor). Upon the expiration of a Developer Key for a Hobbyist Developer, +they may no longer use the Developer Key and must obtain a new Developer Key +from the Site in order to continue using PySimpleGUI for development purposes +pursuant to Section 1.2. Upon the expiration of a Developer Key for a +Commercial Developer, they may continue to use their Developer Key for versions +of PySimpleGUI released during that period, but may not obtain subsequent +updated versions under Section 4.2 unless they purchase a new Developer Key. 3.2. Fees for Commercial Developer Keys; Taxes. Before obtaining each Developer Key for a Commercial Developer, Licensee shall pay to Licensor the diff --git a/README.md b/README.md index a2d87bbf2..bd233387c 100644 --- a/README.md +++ b/README.md @@ -1,99 +1,96 @@ +

- -
- For more information visit PySimpleGUI.com + +

+![](https://PySimpleGUI.net/images/emojis/news_112.png) +# Two Important updates about PySimpleGUI -## +![](https://PySimpleGUI.net/images/emojis/search_56.png) -

- -

User Interfaces for HumansTM

-

+## 1. New Package Location -# Welcome to PySimpleGUI 5 !! +We were recently informed by PyPI that PySimpleGUI does not meet updated PyPI Terms of Service, that it needs to be removed, and hosted on a private server. As a result, you’ll need to add a parameter to your pip install commands in order to access the PySimpleGUI private PyPI server. +The parameter to add is: -Do you use PySimpleGUI 4? [Here is what you need to know.](https://docs.pysimplegui.com/en/latest/readme/sunset/) +`--extra-index-url https://PySimpleGUI.net/install ` -**PySimpleGUI creates desktop applications easily**, enhancing the tkinter, Qt, WxPython, and Remi frameworks with a much simpler programming interface: +### To force a reinstall of PySimpleGUI from new server -1. PySimpleGUI user interfaces are defined using core Python data types (lists and dictionaries) that are easily understood by beginners. -2. PySimpleGUI event handling changes from a complex callback-based model to a simple message passing one. -3. PySimpleGUI uses simple Python code and has no requirement for object oriented architecture. +`python -m pip install --force-reinstall --extra-index-url https://PySimpleGUI.net/install PySimpleGUI` -PySimpleGUI is more than a GUI library: PySimpleGUI simplifies much of your Python development process. Sure, it makes developing user interfaces much easier, but PySimpleGUI also tames advanced Python functionality (such as threading) and makes it easy for all users to take their Python applications to the next level. PySimpleGUI is a robust toolkit. -## Introducing PySimpleGUI 5 +### Performing an upgrade -For the last 5 years, PySimpleGUI offered free software with the hope of sustaining the -company by donations. We appreciate the support we received, but the amount has been too -small to support the PySimpleGUI project. For this reason, PySimpleGUI is switching to a -commercial model, where commercial users are expected to pay a nominal license fee. +This command will also install needed modules like rsa from PyPI automatically +The basic install/upgrade command **was**: -PySimpleGUI is now part of PySimpleSoft, Inc., whose mission is to make the best Python -application development environment much, much better. Since launching in 2018, PySimpleGUI -has helped hobbyists and professionals alike create Python GUIs in a fraction of the time. -PySimpleGUI 5 takes PySimpleGUI to the next level, providing hundreds of improvements, -including new features, enhanced security, and priority support. +`python -m pip install –-upgrade PySimpleGUI` +or for Linux/Mac -PySimpleGUI 5 is licensed software. As the [License Agreement](LICENSE.txt) explains, after a trial -period, all PySimpleGUI 5 users must register at PySimpleGUI.com to obtain a Developer Key. -For most users (Hobbyist Users), the license is at NO COST. If you are a Commercial User, you must buy a license. +`python3 -m pip install –-upgrade PySimpleGUI` -

- -

+The **new command** with the new parameter is: -[Register Now](https://pricing.PySimpleGUI.com) and help support the PySimpleGUI community. +`python -m pip install --upgrade --extra-index-url https://PySimpleGUI.net/install PySimpleGUI` -## Examples +### Uninstall May Be Needed If Error -PySimpleGUI users have created thousands of amazing desktop applications. Here are a few screen shots. For more examples, see the [PySimpleGUI gallery](https://gallery.PySimpleGUI.com/). +If you're getting errors, please uninstall PySimpleGUI entirely and install again using the new parameter. -

- -   - -   - -

-## Get Started at No Cost +### BUG - Commercial Key Expiration - Upgrade to 5.0.10 + +There is a bug in versions of PySimpleGUI older than 5.0.10 that causes an erroneous expired error when using a Commercial Developer key. These keys do not expire and shouldn't not be generating the error. + +A fix was released in version 5.0.10 on 2-Apr-2025. **Please upgrade to version 5.0.10** so that your key doesn't generate an expiration error. + +![](https://PySimpleGUI.net/images/emojis/wave_56.png) + + +## 2. PySimpleGUI Shutdown + +We gave it our best shot…. After 7 years of attempting to make the PySimpleGUI project sustainable, we are stopping the PySimpleGUI project. + +If you've followed the project over the years, you'll have read about the difficulties that all open-source projects face in generating enough income to pay for the project, seen the requests for sponsorships, and attempts to generate income via a Udemy course. There was not enough income to cover the costs of running a business and, of course, wasn’t able to generate any income for our small team. This isn’t a sustainable situation. + +## One Year Update PySimpleGUI 5 + +It's been a little over a year since the release of PySimpleGUI 5. Of the 100,000’s of Version 5 users, 10,000's of which were large corporate users, only 600 people paid the $99 for a commercial license. + +## End of PySimpleGUI Project + +The revenue generated was not enough to cover the basic costs, so we've made the difficult decision to end the PySimpleGUI project. + +## Support for Commercial Users + +Unlike traditional software companies, where stopping business means support ends immediately, we thought it would be classier to go the extra mile by continuing to provide support to Commercial License users this year as a way of saying "thank you" for your help in trying to give the project a viable future. Please provide your Priority Support code or your submission will be automatically blocked. We'll do the best we can to help with the limited resources we've got. + +Your license doesn’t expire so you’ll be able to continue to run your applications at the end of the year we’re providing maintenance and beyond. We’ll be distributing an offline version of the documentation so that you’ll have it for the future. + +## Hobbyists -Whether you are a Hobbyist User or Commercial User, you can start using PySimpleGUI at no cost. -To get started with a 30-day trial period, first install Python and then +Hobbyists can continue to use PySimpleGUI 5 until their keys expire. After that you'll need to switch to version 4, which you'll find 1,000s of copies on GitHub with at least 1 being community supported. - python -m pip install pysimplegui +If you wish to use PySimpleGUI without the key expiring or want support, then you can buy a Commercial License which is good perpetually. -and run some code, like +## Websites Availability - import PySimpleGUI as sg - layout = [ [sg.Text('Hello, world!')] ] - window = sg.Window('Hello Example', layout) - while True: - event, values = window.read() - if event == sg.WIN_CLOSED: - break - window.close() +The PySimpleGUI registration and documentation websites will continue to operate for a couple of months to give commercial customers an opportunity to create distribution keys. No new Hobbyist keys will be available. + +![](https://PySimpleGUI.net/images/emojis/pray_56.png) + +## Thank you to everyone -(You might need to use `python3` instead of `python`.) +PySimpleGUI has been an experience of a lifetime, and we’ve +enjoyed watching & helping people create incredible applications. -You can try PySimpleGUI for 30 days, after which you will need to register. Hobbyist users register at no cost, and Commercial Users must buy a license. For more details, see [PySimpleGUI.com/pricing](https://pricing.PySimpleGUI.com). +## Business Partnership Inquires -## Documentation +If you're a business with a serious partnership that you wish to discuss, email mike@PySimpleGUI.com. -PySimpleGUI provides extensive documentation. Here are some starting points, depending on your needs and expertise: -* [Documentation](https://docs.pysimplegui.com/) - Extensive PySimpleGUI documentation -* [Examples](https://examples.pysimplegui.com/) - Hundreds of sample PySimpleGUI applications. -* [SDK Reference](https://sdk.pysimplegui.com/) - details for each PySimpleGUI element -* [Home Website](https://PySimpleGUI.com) - New PySimpleGUI home page -* [GitHub Repo](https://github.PySimpleGUI.com) - Informational only. Download from PyPi with pip. -* [Updated Documentation](https://docs.PySimpleGUI.com) - Everything you need to know about the latest and best PySimpleGUI - * [Cookbook](https://cookbook.PySimpleGUI.com) - Hundreds of basic PySimpleGUI examples. Find a starting point that is close to what you need. - * [Call Reference](https://cookbook.PySimpleGUI.com) - Just the facts, Ma'am -* [Udemy Course](https://udemy.PySimpleGUI.com) - Become a PySimpleGUI expert in no time. Bundled with Commercial Developer License. diff --git a/development_build_changelog.txt b/development_build_changelog.txt index d0abf7c4c..78ce3e8b7 100644 --- a/development_build_changelog.txt +++ b/development_build_changelog.txt @@ -1,8 +1,7 @@ Changelog since last major release -5.0.3 Released 3-Mar-2024 +5.0.10 Released 02-Apr-2025 + +5.0.10.3 Fix for a potential problem with trial periods +5.0.10.4 More fixes -5.0.3.1 popup_get_text - setting focus so that the window focuses immediate and the cursor goes to the input. Something about modal wasn't doing this. -5.0.3.2 execute_restart - changed how the restart is executed. Now uses the execute python call and works corectly. execute_restart(__file__) is a handy pattern -5.0.3.3 changed "development build" to "maintanence release"... documenation changes underway to match. -5.0.3.4 fixed key error message