diff --git a/Demo_Recipes.py b/Demo_Recipes.py index d34fac49..529b2480 100644 --- a/Demo_Recipes.py +++ b/Demo_Recipes.py @@ -6,13 +6,13 @@ import PySimpleGUI as sg def SourceDestFolders(): with sg.FlexForm('Demo Source / Destination Folders') as form: form_rows = ([sg.Text('Enter the Source and Destination folders')], - [sg.Text('Source Folder', size=(15, 1), justification='right'), sg.InputText('Source'), sg.FolderBrowse()], - [sg.Text('Destination Folder', size=(15, 1), justification='right'), sg.InputText('Dest'), sg.FolderBrowse()], + [sg.Text('Source Folder', size=(15, 1), justification='right'), sg.InputText('Source', key='source'), sg.FolderBrowse()], + [sg.Text('Destination Folder', size=(15, 1), justification='right'), sg.InputText('Dest', key='dest'), sg.FolderBrowse()], [sg.Submit(), sg.Cancel()]) - button, (source, dest) = form.LayoutAndRead(form_rows) + button, values = form.LayoutAndRead(form_rows) if button == 'Submit': - sg.MsgBox('Submitted', 'The user entered source:', source, 'Destination folder:', dest, 'Using button', button) + sg.MsgBox('Submitted', values, 'The user entered source:', values['source'], 'Destination folder:', values['dest'], 'Using button', button) else: sg.MsgBoxError('Cancelled', 'User Cancelled') @@ -160,20 +160,26 @@ def NonBlockingPeriodicUpdateForm(): # Show a form that's a running counter form = sg.FlexForm('Running Timer', auto_size_text=True) text_element = sg.Text('', size=(10, 2), font=('Helvetica', 20), justification='center') - form_rows = [[sg.Text('Non blocking GUI with updates')], + form_rows = [[sg.Text('Stopwatch')], [text_element], - [sg.T(' ' * 15), sg.Quit()]] + [sg.T(' ' * 5), sg.ReadFormButton('Start/Stop', focus=True), sg.Quit()]] + form.LayoutAndRead(form_rows, non_blocking=True) - for i in range(1,50000): - text_element.Update('{:02d}:{:02d}.{:02d}'.format((i//100)//60, (i//100)%60, i%100)) + timer_running = True + i = 0 + while True: + i += 1 * (timer_running is True) button, values = form.ReadNonBlocking() if values is None or button == 'Quit': # if user closed the window using X or clicked Quit button break + elif button == 'Start/Stop': + timer_running = not timer_running + text_element.Update('{:02d}:{:02d}.{:02d}'.format((i//100)//60, (i//100)%60, i%100)) + time.sleep(.01) - else: # if the loop finished then need to close the form for the user - form.CloseNonBlockingForm() + form.CloseNonBlockingForm() del(form) def DebugTest(): @@ -199,29 +205,29 @@ def ChangeLookAndFeel(colors): #=---------------------------------- main ------------------------------ def main(): - # Green & tan color scheme - colors1 = {'BACKGROUND' : '#9FB8AD', 'TEXT': sg.COLOR_SYSTEM_DEFAULT, 'INPUT':'#F7F3EC', 'BUTTON': ('white', '#475841'),'PROGRESS':sg.DEFAULT_PROGRESS_BAR_COLOR } - # light green with tan - colors2 = {'BACKGROUND' : '#B7CECE', 'TEXT': 'black', 'INPUT':'#FDFFF7', 'BUTTON': ('white', '#658268'), 'PROGRESS':('#247BA0','#F8FAF0')} - # blue with light blue color scheme - colors3 = {'BACKGROUND' : '#A5CADD', 'TEXT': '#6E266E', 'INPUT':'#E0F5FF', 'BUTTON': ('white', '#303952'),'PROGRESS':sg.DEFAULT_PROGRESS_BAR_COLOR} - + Everything() ChatBot() - Everything() + sg.ChangeLookAndFeel('BrownBlue') SourceDestFolders() - ChangeLookAndFeel(colors2) - ProgressMeter() - ChangeLookAndFeel(colors3) + sg.ChangeLookAndFeel('BlueMono') Everything() - ChangeLookAndFeel(colors2) + sg.ChangeLookAndFeel('BluePurple') + Everything() + sg.ChangeLookAndFeel('LightGreen') + Everything() + sg.ChangeLookAndFeel('GreenMono') MachineLearningGUI() + sg.ChangeLookAndFeel('TealMono') + NonBlockingPeriodicUpdateForm() + ChatBot() + ProgressMeter() + sg.ChangeLookAndFeel('Purple') Everything_NoContextManager() NonBlockingPeriodicUpdateForm_ContextManager() - NonBlockingPeriodicUpdateForm() - DebugTest() sg.MsgBox('Done with all recipes') + DebugTest() if __name__ == '__main__': main() diff --git a/PySimpleGUI.py b/PySimpleGUI.py index 948c557f..dda7640a 100644 --- a/PySimpleGUI.py +++ b/PySimpleGUI.py @@ -14,7 +14,7 @@ DEFAULT_WINDOW_ICON = '' DEFAULT_ELEMENT_SIZE = (45,1) # In CHARACTERS DEFAULT_MARGINS = (10,5) # Margins for each LEFT/RIGHT margin is first term DEFAULT_ELEMENT_PADDING = (5,3) # Padding between elements (row, col) in pixels -DEFAULT_AUTOSIZE_TEXT = False +DEFAULT_AUTOSIZE_TEXT = True DEFAULT_AUTOSIZE_BUTTONS = True DEFAULT_FONT = ("Helvetica", 10) DEFAULT_TEXT_JUSTIFICATION = 'left' @@ -42,6 +42,7 @@ DEFAULT_ELEMENT_TEXT_COLOR = COLOR_SYSTEM_DEFAULT DEFAULT_TEXT_ELEMENT_BACKGROUND_COLOR = None DEFAULT_TEXT_COLOR = COLOR_SYSTEM_DEFAULT DEFAULT_INPUT_ELEMENTS_COLOR = COLOR_SYSTEM_DEFAULT +DEFAULT_INPUT_TEXT_COLOR = COLOR_SYSTEM_DEFAULT DEFAULT_SCROLLBAR_COLOR = None # DEFAULT_BUTTON_COLOR = (YELLOWS[0], PURPLES[0]) # (Text, Background) or (Color "on", Color) as a way to remember # DEFAULT_BUTTON_COLOR = (GREENS[3], TANS[0]) # Foreground, Background (None, None) == System Default @@ -133,6 +134,7 @@ ELEM_TYPE_IMAGE = 30 ELEM_TYPE_INPUT_SLIDER = 10 ELEM_TYPE_INPUT_LISTBOX = 11 ELEM_TYPE_OUTPUT = 300 +ELEM_TYPE_COLUMN = 555 ELEM_TYPE_PROGRESS_BAR = 200 ELEM_TYPE_BLANK = 100 @@ -218,10 +220,11 @@ class InputText(Element): ''' self.DefaultText = default_text self.PasswordCharacter = password_char - bg = background_color if background_color else DEFAULT_INPUT_ELEMENTS_COLOR + bg = background_color if background_color is not None else DEFAULT_INPUT_ELEMENTS_COLOR + fg = text_color if text_color is not None else DEFAULT_INPUT_TEXT_COLOR self.Focus = focus self.do_not_clear = do_not_clear - super().__init__(ELEM_TYPE_INPUT_TEXT, scale=scale, size=size, auto_size_text=auto_size_text, background_color=bg, text_color=text_color, key=key) + super().__init__(ELEM_TYPE_INPUT_TEXT, scale=scale, size=size, auto_size_text=auto_size_text, background_color=bg, text_color=fg, key=key) @@ -245,7 +248,9 @@ class InputCombo(Element): self.Values = values self.TKComboBox = None bg = background_color if background_color else DEFAULT_INPUT_ELEMENTS_COLOR - super().__init__(ELEM_TYPE_INPUT_COMBO, scale=scale, size=size, auto_size_text=auto_size_text, background_color=bg, text_color=text_color, key=key) + fg = text_color if text_color is not None else DEFAULT_INPUT_TEXT_COLOR + + super().__init__(ELEM_TYPE_INPUT_COMBO, scale=scale, size=size, auto_size_text=auto_size_text, background_color=bg, text_color=fg, key=key) def __del__(self): try: @@ -283,7 +288,8 @@ class Listbox(Element): else: self.SelectMode = DEFAULT_LISTBOX_SELECT_MODE bg = background_color if background_color else DEFAULT_INPUT_ELEMENTS_COLOR - super().__init__(ELEM_TYPE_INPUT_LISTBOX, scale=scale, size=size, auto_size_text=auto_size_text, font=font, background_color=bg, text_color=text_color, key=key) + fg = text_color if text_color is not None else DEFAULT_INPUT_TEXT_COLOR + super().__init__(ELEM_TYPE_INPUT_LISTBOX, scale=scale, size=size, auto_size_text=auto_size_text, font=font, background_color=bg, text_color=fg, key=key) def __del__(self): try: @@ -375,7 +381,9 @@ class Spin(Element): self.DefaultValue = initial_value self.TKSpinBox = None bg = background_color if background_color else DEFAULT_INPUT_ELEMENTS_COLOR - super().__init__(ELEM_TYPE_INPUT_SPIN, scale, size, auto_size_text, font=font,background_color=bg, text_color=text_color, key=key) + fg = text_color if text_color is not None else DEFAULT_INPUT_TEXT_COLOR + + super().__init__(ELEM_TYPE_INPUT_SPIN, scale, size, auto_size_text, font=font,background_color=bg, text_color=fg, key=key) return def __del__(self): @@ -404,7 +412,9 @@ class Multiline(Element): bg = background_color if background_color else DEFAULT_INPUT_ELEMENTS_COLOR self.Focus = focus self.do_not_clear = do_not_clear - super().__init__(ELEM_TYPE_INPUT_MULTILINE, scale=scale, size=size, auto_size_text=auto_size_text, background_color=bg, text_color=text_color, key=key) + fg = text_color if text_color is not None else DEFAULT_INPUT_TEXT_COLOR + + super().__init__(ELEM_TYPE_INPUT_MULTILINE, scale=scale, size=size, auto_size_text=auto_size_text, background_color=bg, text_color=fg, key=key) return def Update(self, NewValue): @@ -554,7 +564,9 @@ class Output(Element): ''' self.TKOut = None bg = background_color if background_color else DEFAULT_INPUT_ELEMENTS_COLOR - super().__init__(ELEM_TYPE_OUTPUT, scale=scale, size=size, background_color=bg, text_color=text_color) + fg = text_color if text_color is not None else DEFAULT_INPUT_TEXT_COLOR + + super().__init__(ELEM_TYPE_OUTPUT, scale=scale, size=size, background_color=bg, text_color=fg) def __del__(self): try: @@ -603,11 +615,11 @@ class Button(Element): def ButtonReleaseCallBack(self, parm): r, c = self.Position - self.ParentForm.Results[r][c] = False # mark this button's location in results + self.ParentForm.LastButtonClicked = None def ButtonPressCallBack(self, parm): r, c = self.Position - self.ParentForm.Results[r][c] = True # mark this button's location in results + self.ParentForm.LastButtonClicked = self.ButtonText # ------- Button Callback ------- # def ButtonCallBack(self): @@ -642,7 +654,8 @@ class Button(Element): # first, get the results table built # modify the Results table in the parent FlexForm object r,c = self.Position - self.ParentForm.Results[r][c] = True # mark this button's location in results + self.ParentForm.LastButtonClicked = self.ButtonText + self.ParentForm.FormRemainedOpen = False # if the form is tabbed, must collect all form's results and destroy all forms if self.ParentForm.IsTabbedForm: self.ParentForm.UberParent._Close() @@ -651,12 +664,13 @@ class Button(Element): self.ParentForm.TKroot.quit() if self.ParentForm.NonBlocking: self.ParentForm.TKroot.destroy() - _my_windows.NumOpenWindows -= 1 * (_my_windows.NumOpenWindows != 0) # decrement if not 0 + # _my_windows.NumOpenWindows -= 1 * (_my_windows.NumOpenWindows != 0) # decrement if not 0 elif self.BType == BUTTON_TYPE_READ_FORM: # LEAVE THE WINDOW OPEN!! DO NOT CLOSE # first, get the results table built # modify the Results table in the parent FlexForm object r,c = self.Position - self.ParentForm.Results[r][c] = True # mark this button's location in results + self.ParentForm.LastButtonClicked = self.ButtonText + self.ParentForm.FormRemainedOpen = True self.ParentForm.TKroot.quit() # kick the users out of the mainloop return @@ -671,12 +685,11 @@ class Button(Element): # ProgreessBar # # ---------------------------------------------------------------------- # class ProgressBar(Element): - def __init__(self, max_value, orientation=None, target=(None, None), scale=(None, None), size=(None, None), auto_size_text=None, bar_color=(None, None), style=None, border_width=None, relief=None): + def __init__(self, max_value, orientation=None, scale=(None, None), size=(None, None), auto_size_text=None, bar_color=(None, None), style=None, border_width=None, relief=None): ''' Progress Bar Element :param max_value: :param orientation: - :param target: :param scale: Adds multiplier to size (w,h) :param size: Size of field in characters :param auto_size_text: True if should shrink field to fit the default text @@ -692,7 +705,6 @@ class ProgressBar(Element): self.Orientation = orientation if orientation else DEFAULT_METER_ORIENTATION self.BarColor = bar_color self.BarStyle = style if style else DEFAULT_PROGRESS_BAR_STYLE - self.Target = target self.BorderWidth = border_width if border_width else DEFAULT_PROGRESS_BAR_BORDER_WIDTH self.Relief = relief if relief else DEFAULT_PROGRESS_BAR_RELIEF self.BarExpired = False @@ -702,12 +714,6 @@ class ProgressBar(Element): def UpdateBar(self, current_count): if self.ParentForm.TKrootDestroyed: return False - target = self.Target - if target[0] != None: # if there's a target, get it and update the strvar - target_element = self.ParentForm.GetElementAtLocation(target) - strvar = target_element.TKStringVar - rc = strvar.set(self.TextToDisplay) - self.TKProgressBar.Update(current_count) try: self.ParentForm.TKroot.update() @@ -771,6 +777,54 @@ class Slider(Element): super().__del__() +# ---------------------------------------------------------------------- # +# Column # +# ---------------------------------------------------------------------- # +class Column(Element): + def __init__(self, layout, background_color = None): + self.UseDictionary = False + self.ReturnValues = None + self.ReturnValuesList = [] + self.ReturnValuesDictionary = {} + self.DictionaryKeyCounter = 0 + self.ParentWindow = None + self.Rows = [] + self.ParentForm = None + self.TKFrame = None + + self.Layout(layout) + + super().__init__(ELEM_TYPE_COLUMN, background_color=background_color) + return + + def AddRow(self, *args): + ''' Parms are a variable number of Elements ''' + NumRows = len(self.Rows) # number of existing rows is our row number + CurrentRowNumber = NumRows # this row's number + CurrentRow = [] # start with a blank row and build up + # ------------------------- Add the elements to a row ------------------------- # + for i, element in enumerate(args): # Loop through list of elements and add them to the row + element.Position = (CurrentRowNumber, i) + CurrentRow.append(element) + if element.Key is not None: + self.UseDictionary = True + # ------------------------- Append the row to list of Rows ------------------------- # + self.Rows.append(CurrentRow) + + def Layout(self, rows): + for row in rows: + self.AddRow(*row) + + def __del__(self): + for row in self.Rows: + for element in row: + element.__del__() + try: + del(self.TKroot) + except: + pass + super().__del__() + # ------------------------------------------------------------------------- # # FlexForm CLASS # @@ -799,6 +853,7 @@ class FlexForm: self.NonBlocking = False self.TKroot = None self.TKrootDestroyed = False + self.FormRemainedOpen = False self.TKAfterID = None self.ProgressBarColor = progress_bar_color self.AutoCloseDuration = auto_close_duration @@ -806,8 +861,10 @@ class FlexForm: self.RootNeedsDestroying = False self.Shown = False self.ReturnValues = None - self.ReturnValuesDictionary = None - self.ResultsBuilt = False + self.ReturnValuesList = [] + self.ReturnValuesDictionary = {} + self.DictionaryKeyCounter = 0 + self.LastButtonClicked = None self.UseDictionary = False self.UseDefaultFocus = False @@ -911,7 +968,7 @@ class FlexForm: if self.RootNeedsDestroying: self.TKroot.destroy() _my_windows.NumOpenWindows -= 1 * (_my_windows.NumOpenWindows != 0) # decrement if not 0 - return BuildResults(self) + return BuildResults(self, False, self) def ReadNonBlocking(self, Message=''): if self.TKrootDestroyed: @@ -925,27 +982,15 @@ class FlexForm: except: self.TKrootDestroyed = True _my_windows.NumOpenWindows -= 1 * (_my_windows.NumOpenWindows != 0) # decrement if not 0 - return BuildResults(self) + return BuildResults(self, False, self) - # LEGACY version of ReadNonBlocking - def Refresh(self, Message=''): - if self.TKrootDestroyed: - return None, None - if Message: - print(Message) - try: - rc = self.TKroot.update() - except: - self.TKrootDestroyed = True - _my_windows.NumOpenWindows -= 1 * (_my_windows.NumOpenWindows != 0) # decrement if not 0 - return BuildResults(self) def _Close(self): try: self.TKroot.update() except: pass if not self.NonBlocking: - results = BuildResults(self) + results = BuildResults(self, False, self) if self.TKrootDestroyed: return None self.TKrootDestroyed = True @@ -972,10 +1017,10 @@ class FlexForm: for row in self.Rows: for element in row: element.__del__() - try: - del(self.TKroot) - except: - pass + # try: + # del(self.TKroot) + # except: + # pass # ------------------------------------------------------------------------- # # UberForm CLASS # @@ -1083,53 +1128,23 @@ def ReadFormButton(button_text, image_filename=None, image_size=(None, None),ima def RealtimeButton(button_text, image_filename=None, image_size=(None, None),image_subsample=None,border_width=None,scale=(None, None), size=(None, None), auto_size_button=None, button_color=None, font=None, bind_return_key=False, focus=False): return Button(BUTTON_TYPE_REALTIME, image_filename=image_filename, image_size=image_size, image_subsample=image_subsample, border_width=border_width, button_text=button_text, scale=scale, size=size, auto_size_button=auto_size_button, button_color=button_color, font=font, bind_return_key=bind_return_key, focus=focus) -#------------------------------------------------------------------------------------------------------# -# ------- FUNCTION InitializeResults. Sets up form results matrix ------- # +##################################### ----- RESULTS ------ ################################################## + +def AddToReturnDictionary(form, element, value): + if element.Key is None: + form.ReturnValuesDictionary[form.DictionaryKeyCounter] = value + form.DictionaryKeyCounter += 1 + else: + form.ReturnValuesDictionary[element.Key] = value + +def AddToReturnList(form, value): + form.ReturnValuesList.append(value) + + +#----------------------------------------------------------------------------# +# ------- FUNCTION InitializeResults. Sets up form results matrix --------# def InitializeResults(form): - # initial results for elements are: - # TEXT - None - # INPUT - Initial value - # Button - False - results = [] - return_vals = [] - for row_num,row in enumerate(form.Rows): - r = [] - for element in row: - if element.Type == ELEM_TYPE_TEXT: - r.append(None) - if element.Type == ELEM_TYPE_IMAGE: - r.append(None) - elif element.Type == ELEM_TYPE_INPUT_TEXT: - r.append(element.TextInputDefault) - return_vals.append(None) - elif element.Type == ELEM_TYPE_INPUT_MULTILINE: - r.append(element.TextInputDefault) - return_vals.append(None) - elif element.Type == ELEM_TYPE_BUTTON: - r.append(False) - elif element.Type == ELEM_TYPE_PROGRESS_BAR: - r.append(None) - elif element.Type == ELEM_TYPE_INPUT_CHECKBOX: - r.append(element.InitialState) - return_vals.append(element.InitialState) - elif element.Type == ELEM_TYPE_INPUT_RADIO: - r.append(element.InitialState) - return_vals.append(element.InitialState) - elif element.Type == ELEM_TYPE_INPUT_COMBO: - r.append(element.TextInputDefault) - return_vals.append(None) - elif element.Type == ELEM_TYPE_INPUT_LISTBOX: - r.append(None) - return_vals.append(None) - elif element.Type == ELEM_TYPE_INPUT_SPIN: - r.append(element.DefaultValue) - return_vals.append(None) - elif element.Type == ELEM_TYPE_INPUT_SLIDER: - r.append(element.DefaultValue) - return_vals.append(None) - results.append(r) - form.Results=results - form.ReturnValues = (None, return_vals) + BuildResults(form, True, form) return #===== Radio Button RadVar encoding and decoding =====# @@ -1146,131 +1161,476 @@ def EncodeRadioRowCol(row, col): # ------- FUNCTION BuildResults. Form exiting so build the results to pass back ------- # # format of return values is # (Button Pressed, input_values) -def BuildResults(form): +def BuildResults(form, initialize_only, top_level_form): # Results for elements are: # TEXT - Nothing # INPUT - Read value from TK # Button - Button Text and position as a Tuple # Get the initialized results so we don't have to rebuild - results=form.Results + form.DictionaryKeyCounter = 0 + form.ReturnValuesDictionary = {} + form.ReturnValuesList = [] + BuildResultsForSubform(form, initialize_only, top_level_form) + return form.ReturnValues + +def BuildResultsForSubform(form, initialize_only, top_level_form): button_pressed_text = None - input_values = [] - input_values_dictionary = {} - key_counter = 0 for row_num,row in enumerate(form.Rows): for col_num, element in enumerate(row): - if element.Type == ELEM_TYPE_INPUT_TEXT: - value=element.TKStringVar.get() - results[row_num][col_num] = value - input_values.append(value) - if not form.NonBlocking and not element.do_not_clear: - element.TKStringVar.set('') - if element.Key is None: - input_values_dictionary[key_counter] = value - key_counter +=1 - else: - input_values_dictionary[element.Key] = value - elif element.Type == ELEM_TYPE_INPUT_CHECKBOX: - value=element.TKIntVar.get() - results[row_num][col_num] = value - input_values.append(value != 0) - if element.Key is None: - input_values_dictionary[key_counter] = value - key_counter +=1 - else: - input_values_dictionary[element.Key] = value - elif element.Type == ELEM_TYPE_INPUT_RADIO: - RadVar=element.TKIntVar.get() - this_rowcol = EncodeRadioRowCol(row_num,col_num) - value = RadVar == this_rowcol - results[row_num][col_num] = value - input_values.append(value) - if element.Key is None: - input_values_dictionary[key_counter] = value - key_counter +=1 - else: - input_values_dictionary[element.Key] = value - elif element.Type == ELEM_TYPE_BUTTON: - if results[row_num][col_num] is True: - button_pressed_text = element.ButtonText - if element.BType != BUTTON_TYPE_REALTIME: # Do not clear realtime buttons - results[row_num][col_num] = False - elif element.Type == ELEM_TYPE_INPUT_COMBO: - value=element.TKStringVar.get() - results[row_num][col_num] = value - input_values.append(value) - if element.Key is None: - input_values_dictionary[key_counter] = value - key_counter +=1 - else: - input_values_dictionary[element.Key] = value - elif element.Type == ELEM_TYPE_INPUT_LISTBOX: - items=element.TKListbox.curselection() - value = [element.Values[int(item)] for item in items] - results[row_num][col_num] = value - input_values.append(value) - if element.Key is None: - input_values_dictionary[key_counter] = value - key_counter +=1 - else: - input_values_dictionary[element.Key] = value - elif element.Type == ELEM_TYPE_INPUT_SPIN: - try: + value = None + if element.Type == ELEM_TYPE_COLUMN: + element.DictionaryKeyCounter = top_level_form.DictionaryKeyCounter + element.ReturnValuesList = [] + element.ReturnValuesDictionary = {} + BuildResultsForSubform(element, initialize_only, top_level_form) + for item in element.ReturnValuesList: + AddToReturnList(top_level_form, item) + # for key in element.ReturnValuesDictionary: + # top_level_form.ReturnValuesDictionary[key] = element.ReturnValuesDictionary[key] + # top_level_form.DictionaryKeyCounter += element.DictionaryKeyCounter + if element.UseDictionary: + top_level_form.UseDictionary = True + if element.ReturnValues[0] is not None: # if a button was clicked + button_pressed_text = element.ReturnValues[0] + + if not initialize_only: + if element.Type == ELEM_TYPE_INPUT_TEXT: value=element.TKStringVar.get() - except: - value = 0 - results[row_num][col_num] = value - input_values.append(value) - if element.Key is None: - input_values_dictionary[key_counter] = value - key_counter +=1 - else: - input_values_dictionary[element.Key] = value - elif element.Type == ELEM_TYPE_INPUT_SLIDER: - try: - value=element.TKIntVar.get() - except: - value = 0 - results[row_num][col_num] = value - input_values.append(value) - if element.Key is None: - input_values_dictionary[key_counter] = value - key_counter +=1 - else: - input_values_dictionary[element.Key] = value - elif element.Type == ELEM_TYPE_INPUT_MULTILINE: - try: - value=element.TKText.get(1.0, tk.END) - if not form.NonBlocking and not element.do_not_clear: - element.TKText.delete('1.0', tk.END) - except: - value = None - results[row_num][col_num] = value - input_values.append(value) - if element.Key is None: - input_values_dictionary[key_counter] = value - key_counter +=1 - else: - input_values_dictionary[element.Key] = value + if not top_level_form.NonBlocking and not element.do_not_clear: + element.TKStringVar.set('') + elif element.Type == ELEM_TYPE_INPUT_CHECKBOX: + value = element.TKIntVar.get() + value = (value != 0) + elif element.Type == ELEM_TYPE_INPUT_RADIO: + RadVar=element.TKIntVar.get() + this_rowcol = EncodeRadioRowCol(row_num,col_num) + value = RadVar == this_rowcol + elif element.Type == ELEM_TYPE_BUTTON: + if top_level_form.LastButtonClicked == element.ButtonText: + button_pressed_text = top_level_form.LastButtonClicked + if element.BType != BUTTON_TYPE_REALTIME: # Do not clear realtime buttons + top_level_form.LastButtonClicked = None + elif element.Type == ELEM_TYPE_INPUT_COMBO: + value=element.TKStringVar.get() + elif element.Type == ELEM_TYPE_INPUT_LISTBOX: + try: + items=element.TKListbox.curselection() + value = [element.Values[int(item)] for item in items] + except: + value = '' + elif element.Type == ELEM_TYPE_INPUT_SPIN: + try: + value=element.TKStringVar.get() + except: + value = 0 + elif element.Type == ELEM_TYPE_INPUT_SLIDER: + try: + value=element.TKIntVar.get() + except: + value = 0 + elif element.Type == ELEM_TYPE_INPUT_MULTILINE: + try: + value=element.TKText.get(1.0, tk.END) + if not top_level_form.NonBlocking and not element.do_not_clear: + element.TKText.delete('1.0', tk.END) + except: + value = None + else: + value = None + + # if an input type element, update the results + if element.Type != ELEM_TYPE_BUTTON and element.Type != ELEM_TYPE_TEXT and element.Type != ELEM_TYPE_IMAGE and\ + element.Type != ELEM_TYPE_OUTPUT and element.Type != ELEM_TYPE_PROGRESS_BAR and element.Type!= ELEM_TYPE_COLUMN: + AddToReturnList(form, value) + AddToReturnDictionary(top_level_form, element, value) try: - input_values_dictionary.pop(None, None) # clean up dictionary include None was included + form.ReturnValuesDictionary.pop(None, None) # clean up dictionary include None was included except: pass if not form.UseDictionary: - form.ReturnValues = button_pressed_text, input_values + form.ReturnValues = button_pressed_text, form.ReturnValuesList else: - form.ReturnValues = button_pressed_text, input_values_dictionary - form.ReturnValuesDictionary = button_pressed_text, input_values_dictionary - form.ResultsBuilt = True + form.ReturnValues = button_pressed_text, form.ReturnValuesDictionary + return form.ReturnValues # ------------------------------------------------------------------------------------------------------------------ # # ===================================== TK CODE STARTS HERE ====================================================== # # ------------------------------------------------------------------------------------------------------------------ # + +def PackFormIntoFrame(form, containing_frame, toplevel_form): + def CharWidthInPixels(): + return tkinter.font.Font().measure('A') # single character width + # only set title on non-tabbed forms + border_depth = toplevel_form.BorderDepth if toplevel_form.BorderDepth is not None else DEFAULT_BORDER_WIDTH + # --------------------------------------------------------------------------- # + # **************** Use FlexForm to build the tkinter window ********** ----- # + # Building is done row by row. # + # --------------------------------------------------------------------------- # + focus_set = False + ######################### LOOP THROUGH ROWS ######################### + # *********** ------- Loop through ROWS ------- ***********# + for row_num, flex_row in enumerate(form.Rows): + ######################### LOOP THROUGH ELEMENTS ON ROW ######################### + # *********** ------- Loop through ELEMENTS ------- ***********# + # *********** Make TK Row ***********# + tk_row_frame = tk.Frame(containing_frame) + for col_num, element in enumerate(flex_row): + element.ParentForm = toplevel_form # save the button's parent form object + if toplevel_form.Font and (element.Font == DEFAULT_FONT or not element.Font): + font = toplevel_form.Font + elif element.Font is not None: + font = element.Font + else: + font = DEFAULT_FONT + # ------- Determine Auto-Size setting on a cascading basis ------- # + if element.AutoSizeText is not None: # if element overide + auto_size_text = element.AutoSizeText + elif toplevel_form.AutoSizeText is not None: # if form override + auto_size_text = toplevel_form.AutoSizeText + else: + auto_size_text = DEFAULT_AUTOSIZE_TEXT + # Determine Element size + element_size = element.Size + if (element_size == (None, None)): # user did not specify a size + element_size = toplevel_form.DefaultElementSize + else: auto_size_text = False # if user has specified a size then it shouldn't autosize + # Apply scaling... Element scaling is higher priority than form level + if element.Scale != (None, None): + element_size = (int(element_size[0] * element.Scale[0]), int(element_size[1] * element.Scale[1])) + elif toplevel_form.Scale != (None, None): + element_size = (int(element_size[0] * toplevel_form.Scale[0]), int(element_size[1] * toplevel_form.Scale[1])) + # Set foreground color + text_color = element.TextColor + element_type = element.Type + # ------------------------- COLUMN element ------------------------- # + if element_type == ELEM_TYPE_COLUMN: + col_frame = tk.Frame(tk_row_frame) + PackFormIntoFrame(element, col_frame, toplevel_form) + col_frame.pack(side=tk.LEFT) + if element.BackgroundColor is not None: + col_frame.configure(background=element.BackgroundColor, highlightbackground=element.BackgroundColor, highlightcolor=element.BackgroundColor) + # ------------------------- TEXT element ------------------------- # + elif element_type == ELEM_TYPE_TEXT: + display_text = element.DisplayText # text to display + if auto_size_text is False: + width, height=element_size + else: + lines = display_text.split('\n') + max_line_len = max([len(l) for l in lines]) + num_lines = len(lines) + if max_line_len > element_size[0]: # if text exceeds element size, the will have to wrap + width = element_size[0] + else: + width=max_line_len + height=num_lines + # ---===--- LABEL widget create and place --- # + stringvar = tk.StringVar() + element.TKStringVar = stringvar + stringvar.set(display_text) + if auto_size_text: + width = 0 + justify = tk.LEFT if element.Justification == 'left' else tk.CENTER if element.Justification == 'center' else tk.RIGHT + anchor = tk.NW if element.Justification == 'left' else tk.N if element.Justification == 'center' else tk.NE + tktext_label = tk.Label(tk_row_frame, textvariable=stringvar, width=width, height=height, justify=justify, bd=border_depth) + # tktext_label = tk.Label(tk_row_frame,anchor=tk.NW, text=display_text, width=width, height=height, justify=tk.LEFT, bd=border_depth) + # Set wrap-length for text (in PIXELS) == PAIN IN THE ASS + wraplen = tktext_label.winfo_reqwidth() # width of widget in Pixels + tktext_label.configure(anchor=anchor, font=font, wraplen=wraplen*2 ) # set wrap to width of widget + if element.BackgroundColor is not None: + tktext_label.configure(background=element.BackgroundColor) + if element.TextColor != COLOR_SYSTEM_DEFAULT and element.TextColor is not None: + tktext_label.configure(fg=element.TextColor) + tktext_label.pack(side=tk.LEFT) + # ------------------------- BUTTON element ------------------------- # + elif element_type == ELEM_TYPE_BUTTON: + element.Location = (row_num, col_num) + btext = element.ButtonText + btype = element.BType + if element.AutoSizeButton is not None: + auto_size = element.AutoSizeButton + else: auto_size = toplevel_form.AutoSizeButtons + if auto_size is False: width=element_size[0] + else: width = 0 + height=element_size[1] + lines = btext.split('\n') + max_line_len = max([len(l) for l in lines]) + num_lines = len(lines) + if element.ButtonColor != (None, None)and element.ButtonColor != DEFAULT_BUTTON_COLOR: + bc = element.ButtonColor + elif toplevel_form.ButtonColor != (None, None) and toplevel_form.ButtonColor != DEFAULT_BUTTON_COLOR: + bc = toplevel_form.ButtonColor + else: + bc = DEFAULT_BUTTON_COLOR + border_depth = element.BorderWidth + if btype != BUTTON_TYPE_REALTIME: + tkbutton = tk.Button(tk_row_frame, text=btext, width=width, height=height,command=element.ButtonCallBack, justify=tk.LEFT, bd=border_depth) + else: + tkbutton = tk.Button(tk_row_frame, text=btext, width=width, height=height, justify=tk.LEFT, bd=border_depth) + tkbutton.bind('', element.ButtonReleaseCallBack) + tkbutton.bind('', element.ButtonPressCallBack) + if bc != (None, None) and bc != COLOR_SYSTEM_DEFAULT: + tkbutton.config(foreground=bc[0], background=bc[1]) + element.TKButton = tkbutton # not used yet but save the TK button in case + wraplen = tkbutton.winfo_reqwidth() # width of widget in Pixels + if element.ImageFilename: # if button has an image on it + photo = tk.PhotoImage(file=element.ImageFilename) + if element.ImageSize != (None, None): + width, height = element.ImageSize + if element.ImageSubsample: + photo = photo.subsample(element.ImageSubsample) + else: + width, height = photo.width(), photo.height() + tkbutton.config(image=photo, width=width, height=height) + tkbutton.image = photo + tkbutton.configure(wraplength=wraplen+10, font=font) # set wrap to width of widget + tkbutton.pack(side=tk.LEFT, padx=element.Pad[0], pady=element.Pad[1]) + if element.Focus is True or (toplevel_form.UseDefaultFocus and not focus_set): + focus_set = True + element.TKButton.bind('', element.ReturnKeyHandler) + element.TKButton.focus_set() + toplevel_form.TKroot.focus_force() + # ------------------------- INPUT (Single Line) element ------------------------- # + elif element_type == ELEM_TYPE_INPUT_TEXT: + default_text = element.DefaultText + element.TKStringVar = tk.StringVar() + element.TKStringVar.set(default_text) + show = element.PasswordCharacter if element.PasswordCharacter else "" + element.TKEntry = tk.Entry(tk_row_frame, width=element_size[0], textvariable=element.TKStringVar, bd=border_depth, font=font, show=show) + element.TKEntry.bind('', element.ReturnKeyHandler) + if element.BackgroundColor is not None and element.BackgroundColor != COLOR_SYSTEM_DEFAULT: + element.TKEntry.configure(background=element.BackgroundColor) + if text_color is not None and text_color != COLOR_SYSTEM_DEFAULT: + element.TKEntry.configure(fg=text_color) + element.TKEntry.pack(side=tk.LEFT,padx=element.Pad[0], pady=element.Pad[1]) + if element.Focus is True or (toplevel_form.UseDefaultFocus and not focus_set): + focus_set = True + element.TKEntry.focus_set() + # ------------------------- COMBO BOX (Drop Down) element ------------------------- # + elif element_type == ELEM_TYPE_INPUT_COMBO: + max_line_len = max([len(str(l)) for l in element.Values]) + if auto_size_text is False: width=element_size[0] + else: width = max_line_len + element.TKStringVar = tk.StringVar() + if element.BackgroundColor and element.BackgroundColor != COLOR_SYSTEM_DEFAULT: + combostyle = ttk.Style() + try: + combostyle.theme_create('combostyle', + settings={'TCombobox': + {'configure': + {'selectbackground': element.BackgroundColor, + 'fieldbackground': element.BackgroundColor, + 'foreground': text_color, + 'background': element.BackgroundColor} + }}) + except: + try: + combostyle.theme_settings('combostyle', + settings={'TCombobox': + {'configure': + {'selectbackground': element.BackgroundColor, + 'fieldbackground': element.BackgroundColor, + 'foreground': text_color, + 'background': element.BackgroundColor} + }}) + except: pass + # ATTENTION: this applies the new style 'combostyle' to all ttk.Combobox + combostyle.theme_use('combostyle') + element.TKCombo = ttk.Combobox(tk_row_frame, width=width, textvariable=element.TKStringVar,font=font ) + # element.TKCombo['state']='readonly' + element.TKCombo['values'] = element.Values + # if element.BackgroundColor is not None: + # element.TKCombo.configure(background=element.BackgroundColor) + element.TKCombo.pack(side=tk.LEFT,padx=element.Pad[0], pady=element.Pad[1]) + element.TKCombo.current(0) + # ------------------------- LISTBOX element ------------------------- # + elif element_type == ELEM_TYPE_INPUT_LISTBOX: + max_line_len = max([len(str(l)) for l in element.Values]) + if auto_size_text is False: width=element_size[0] + else: width = max_line_len + + element.TKStringVar = tk.StringVar() + element.TKListbox= tk.Listbox(tk_row_frame, height=element_size[1], width=width, selectmode=element.SelectMode, font=font) + for item in element.Values: + element.TKListbox.insert(tk.END, item) + element.TKListbox.selection_set(0,0) + if element.BackgroundColor is not None and element.BackgroundColor != COLOR_SYSTEM_DEFAULT: + element.TKListbox.configure(background=element.BackgroundColor) + if text_color is not None and text_color != COLOR_SYSTEM_DEFAULT: + element.TKListbox.configure(fg=text_color) + # vsb = tk.Scrollbar(tk_row_frame, orient="vertical", command=element.TKListbox.yview) + # element.TKListbox.configure(yscrollcommand=vsb.set) + element.TKListbox.pack(side=tk.LEFT,padx=element.Pad[0], pady=element.Pad[1]) + # vsb.pack(side=tk.LEFT, fill='y') + # ------------------------- INPUT MULTI LINE element ------------------------- # + elif element_type == ELEM_TYPE_INPUT_MULTILINE: + default_text = element.DefaultText + width, height = element_size + element.TKText = tk.scrolledtext.ScrolledText(tk_row_frame, width=width, height=height, wrap='word', bd=border_depth,font=font) + element.TKText.insert(1.0, default_text) # set the default text + if element.BackgroundColor is not None and element.BackgroundColor != COLOR_SYSTEM_DEFAULT: + element.TKText.configure(background=element.BackgroundColor) + element.TKText.vbar.config(troughcolor=DEFAULT_SCROLLBAR_COLOR) + element.TKText.pack(side=tk.LEFT,padx=element.Pad[0], pady=element.Pad[1]) + if element.EnterSubmits: + element.TKText.bind('', element.ReturnKeyHandler) + if element.Focus is True or (toplevel_form.UseDefaultFocus and not focus_set): + focus_set = True + element.TKText.focus_set() + if text_color is not None and text_color != COLOR_SYSTEM_DEFAULT: + element.TKText.configure(fg=text_color) + # ------------------------- INPUT CHECKBOX element ------------------------- # + elif element_type == ELEM_TYPE_INPUT_CHECKBOX: + width = 0 if auto_size_text else element_size[0] + default_value = element.InitialState + element.TKIntVar = tk.IntVar() + element.TKIntVar.set(default_value if default_value is not None else 0) + element.TKCheckbutton = tk.Checkbutton(tk_row_frame, anchor=tk.NW, text=element.Text, width=width, variable=element.TKIntVar, bd=border_depth, font=font) + if default_value is None: + element.TKCheckbutton.configure(state='disable') + if element.BackgroundColor is not None: + element.TKCheckbutton.configure(background=element.BackgroundColor) + if text_color is not None and text_color != COLOR_SYSTEM_DEFAULT: + element.TKCheckbutton.configure(fg=text_color) + element.TKCheckbutton.pack(side=tk.LEFT,padx=element.Pad[0], pady=element.Pad[1]) + # ------------------------- PROGRESS BAR element ------------------------- # + elif element_type == ELEM_TYPE_PROGRESS_BAR: + # save this form because it must be 'updated' (refreshed) solely for the purpose of updating bar + width = element_size[0] + fnt = tkinter.font.Font() + char_width = fnt.measure('A') # single character width + progress_length = width*char_width + progress_width = element_size[1] + direction = element.Orientation + if element.BarColor != (None, None): # if element has a bar color, use it + bar_color = element.BarColor + else: + bar_color = DEFAULT_PROGRESS_BAR_COLOR + element.TKProgressBar = TKProgressBar(tk_row_frame, element.MaxValue, progress_length, progress_width, orientation=direction, BarColor=bar_color, border_width=element.BorderWidth, relief=element.Relief, style=element.BarStyle ) + # element.TKProgressBar = TKProgressBar(tk_row_frame, element.MaxValue, progress_length, progress_width, orientation=direction, BarColor=bar_color, border_width=element.BorderWidth, relief=element.Relief) + element.TKProgressBar.TKProgressBarForReal.pack(side=tk.LEFT, padx=element.Pad[0], pady=element.Pad[1]) + # ------------------------- INPUT RADIO BUTTON element ------------------------- # + elif element_type == ELEM_TYPE_INPUT_RADIO: + width = 0 if auto_size_text else element_size[0] + default_value = element.InitialState + ID = element.GroupID + # see if ID has already been placed + value = EncodeRadioRowCol(row_num, col_num) # value to set intvar to if this radio is selected + if ID in toplevel_form.RadioDict: + RadVar = toplevel_form.RadioDict[ID] + else: + RadVar = tk.IntVar() + toplevel_form.RadioDict[ID] = RadVar + element.TKIntVar = RadVar # store the RadVar in Radio object + if default_value: # if this radio is the one selected, set RadVar to match + element.TKIntVar.set(value) + element.TKRadio = tk.Radiobutton(tk_row_frame, anchor=tk.NW, text=element.Text, width=width, + variable=element.TKIntVar, value=value, bd=border_depth, font=font) + if element.BackgroundColor is not None: + element.TKRadio.configure(background=element.BackgroundColor) + if text_color is not None and text_color != COLOR_SYSTEM_DEFAULT: + element.TKRadio.configure(fg=text_color) + element.TKRadio.pack(side=tk.LEFT, padx=element.Pad[0],pady=element.Pad[1]) + # ------------------------- INPUT SPIN Box element ------------------------- # + elif element_type == ELEM_TYPE_INPUT_SPIN: + width, height = element_size + width = 0 if auto_size_text else element_size[0] + element.TKStringVar = tk.StringVar() + element.TKSpinBox = tk.Spinbox(tk_row_frame, values=element.Values, textvariable=element.TKStringVar, width=width, bd=border_depth) + element.TKStringVar.set(element.DefaultValue) + element.TKSpinBox.configure(font=font) # set wrap to width of widget + if element.BackgroundColor is not None and element.BackgroundColor != COLOR_SYSTEM_DEFAULT: + element.TKSpinBox.configure(background=element.BackgroundColor) + element.TKSpinBox.pack(side=tk.LEFT, padx=element.Pad[0], pady=element.Pad[1]) + if text_color is not None and text_color != COLOR_SYSTEM_DEFAULT: + element.TKSpinBox.configure(fg=text_color) + # ------------------------- OUTPUT element ------------------------- # + elif element_type == ELEM_TYPE_OUTPUT: + width, height = element_size + element.TKOut = TKOutput(tk_row_frame, width=width, height=height, bd=border_depth, background_color=element.BackgroundColor, text_color=text_color) + element.TKOut.pack(side=tk.LEFT,padx=element.Pad[0], pady=element.Pad[1]) + # ------------------------- IMAGE Box element ------------------------- # + elif element_type == ELEM_TYPE_IMAGE: + photo = tk.PhotoImage(file=element.Filename) + if element_size == (None, None) or element_size == None or element_size == toplevel_form.DefaultElementSize: + width, height = photo.width(), photo.height() + else: + width, height = element_size + tktext_label = tk.Label(tk_row_frame, image=photo, width=width, height=height, bd=border_depth) + tktext_label.image = photo + # tktext_label.configure(anchor=tk.NW, image=photo) + tktext_label.pack(side=tk.LEFT) + # ------------------------- SLIDER Box element ------------------------- # + elif element_type == ELEM_TYPE_INPUT_SLIDER: + slider_length = element_size[0] * CharWidthInPixels() + slider_width = element_size[1] + element.TKIntVar = tk.IntVar() + element.TKIntVar.set(element.DefaultValue) + if element.Orientation[0] == 'v': + range_from = element.Range[1] + range_to = element.Range[0] + slider_length += DEFAULT_MARGINS[1]*(element_size[0]*2) # add in the padding + else: + range_from = element.Range[0] + range_to = element.Range[1] + tkscale = tk.Scale(tk_row_frame, orient=element.Orientation, variable=element.TKIntVar, from_=range_from, to_=range_to, length=slider_length, width=slider_width , bd=element.BorderWidth, relief=element.Relief, font=font) + # tktext_label.configure(anchor=tk.NW, image=photo) + if element.BackgroundColor is not None: + tkscale.configure(background=element.BackgroundColor) + tkscale.config(troughcolor=DEFAULT_SCROLLBAR_COLOR) + if text_color is not None and text_color != COLOR_SYSTEM_DEFAULT: + tkscale.configure(fg=text_color) + tkscale.pack(side=tk.LEFT) + #............................DONE WITH ROW pack the row of widgets ..........................# + # done with row, pack the row of widgets + tk_row_frame.grid(row=row_num+2, sticky=tk.NW, padx=DEFAULT_MARGINS[0]) + + if form.BackgroundColor is not None: + tk_row_frame.configure(background=form.BackgroundColor) + if not toplevel_form.IsTabbedForm: + toplevel_form.TKroot.configure(padx=DEFAULT_MARGINS[0], pady=DEFAULT_MARGINS[1]) + else: toplevel_form.ParentWindow.configure(padx=DEFAULT_MARGINS[0], pady=DEFAULT_MARGINS[1]) + return + + def ConvertFlexToTK(MyFlexForm): + master = MyFlexForm.TKroot + # only set title on non-tabbed forms + if not MyFlexForm.IsTabbedForm: + master.title(MyFlexForm.Title) + InitializeResults(MyFlexForm) + PackFormIntoFrame(MyFlexForm, master, MyFlexForm) + #....................................... DONE creating and laying out window ..........................# + if MyFlexForm.IsTabbedForm: + master = MyFlexForm.ParentWindow + master.attributes('-alpha', 0) # hide window while getting info and moving + screen_width = master.winfo_screenwidth() # get window info to move to middle of screen + screen_height = master.winfo_screenheight() + if MyFlexForm.Location != (None, None): + x,y = MyFlexForm.Location + elif DEFAULT_WINDOW_LOCATION != (None, None): + x,y = DEFAULT_WINDOW_LOCATION + else: + master.update_idletasks() # don't forget + win_width = master.winfo_width() + win_height = master.winfo_height() + x = screen_width/2 -win_width/2 + y = screen_height/2 - win_height/2 + if y+win_height > screen_height: + y = screen_height-win_height + if x+win_width > screen_width: + x = screen_width-win_width + + move_string = '+%i+%i'%(int(x),int(y)) + master.geometry(move_string) + master.attributes('-alpha', 255) # Make window visible again + master.update_idletasks() # don't forget + return + +def ConvertFlexToTKOld(MyFlexForm): def CharWidthInPixels(): return tkinter.font.Font().measure('A') # single character width master = MyFlexForm.TKroot @@ -1626,6 +1986,7 @@ def ConvertFlexToTK(MyFlexForm): # ----====----====----====----====----==== STARTUP TK ====----====----====----====----====----# def ShowTabbedForm(title, *args, auto_close=False, auto_close_duration=DEFAULT_AUTOCLOSE_TIME, fav_icon=DEFAULT_WINDOW_ICON): + # takes as input (form, rows, tab name) for each tab global _my_windows uber = UberForm() @@ -1685,6 +2046,7 @@ def StartupTK(my_flex_form): global _my_windows ow = _my_windows.NumOpenWindows + # print('Starting TK open Windows = {}'.format(ow)) root = tk.Tk() if not ow else tk.Toplevel() if my_flex_form.BackgroundColor is not None: root.configure(background=my_flex_form.BackgroundColor) @@ -1701,10 +2063,12 @@ def StartupTK(my_flex_form): my_flex_form.TKAfterID = root.after(duration * 1000, my_flex_form._AutoCloseAlarmCallback) if my_flex_form.NonBlocking: my_flex_form.TKroot.protocol("WM_WINDOW_DESTROYED", my_flex_form.OnClosingCallback()) - pass else: # it's a blocking form + # print('..... CALLING MainLoop') my_flex_form.TKroot.mainloop() - _my_windows.NumOpenWindows -= 1 * (_my_windows.NumOpenWindows != 0) # decrement if not 0 + # print('..... BACK from MainLoop') + if not my_flex_form.FormRemainedOpen: + _my_windows.NumOpenWindows -= 1 * (_my_windows.NumOpenWindows != 0) # decrement if not 0 if my_flex_form.RootNeedsDestroying: my_flex_form.TKroot.destroy() my_flex_form.RootNeedsDestroying = False @@ -1757,7 +2121,8 @@ def MsgBox(*args, button_color=None, button_type=MSG_BOX_OK, auto_close=False, a local_line_width = line_width else: local_line_width = MESSAGE_BOX_LINE_WIDTH - with FlexForm(args_to_print[0], auto_size_text=True, button_color=button_color, auto_close=auto_close, auto_close_duration=auto_close_duration, icon=icon, font=font) as form: + title = args_to_print[0] if args_to_print[0] is not None else 'None' + with FlexForm(title, auto_size_text=True, button_color=button_color, auto_close=auto_close, auto_close_duration=auto_close_duration, icon=icon, font=font) as form: max_line_total, total_lines = 0,0 for message in args_to_print: # fancy code to check if string and convert if not is not need. Just always convert to string :-) @@ -1916,7 +2281,7 @@ def ConvertArgsToSingleString(*args): # ============================== ProgressMeter =====# # ===================================================# -def ProgressMeter(title, max_value, *args, orientation=None, bar_color=(None,None), button_color=None, size=DEFAULT_PROGRESS_BAR_SIZE, scale=(None, None), border_width=None): +def _ProgressMeter(title, max_value, *args, orientation=None, bar_color=(None,None), button_color=None, size=DEFAULT_PROGRESS_BAR_SIZE, scale=(None, None), border_width=None): ''' Create and show a form on tbe caller's behalf. :param title: @@ -1932,8 +2297,7 @@ def ProgressMeter(title, max_value, *args, orientation=None, bar_color=(None,Non ''' local_orientation = DEFAULT_METER_ORIENTATION if orientation is None else orientation local_border_width = DEFAULT_PROGRESS_BAR_BORDER_WIDTH if border_width is None else border_width - target = (0,0) if local_orientation[0].lower() == 'h' else (0,1) - bar2 = ProgressBar(max_value, orientation=local_orientation, size=size, bar_color=bar_color, scale=scale, target=target, border_width=local_border_width, relief=DEFAULT_PROGRESS_BAR_RELIEF) + bar2 = ProgressBar(max_value, orientation=local_orientation, size=size, bar_color=bar_color, scale=scale, border_width=local_border_width, relief=DEFAULT_PROGRESS_BAR_RELIEF) form = FlexForm(title, auto_size_text=True) # Form using a horizontal bar @@ -1942,7 +2306,8 @@ def ProgressMeter(title, max_value, *args, orientation=None, bar_color=(None,Non bar2.TextToDisplay = single_line_message bar2.MaxValue = max_value bar2.CurrentValue = 0 - form.AddRow(Text(single_line_message, size=(width, height + 3), auto_size_text=True)) + bar_text = Text(single_line_message, size=(width, height + 3), auto_size_text=True) + form.AddRow(bar_text) form.AddRow((bar2)) form.AddRow((Cancel(button_color=button_color))) else: @@ -1950,15 +2315,16 @@ def ProgressMeter(title, max_value, *args, orientation=None, bar_color=(None,Non bar2.TextToDisplay = single_line_message bar2.MaxValue = max_value bar2.CurrentValue = 0 - form.AddRow(bar2, Text(single_line_message, size=(width, height + 3), auto_size_text=True)) + bar_text = Text(single_line_message, size=(width, height + 3), auto_size_text=True) + form.AddRow(bar2, bar_text) form.AddRow((Cancel(button_color=button_color))) form.NonBlocking = True form.Show(non_blocking= True) - return bar2 + return bar2, bar_text # ============================== ProgressMeterUpdate =====# -def ProgressMeterUpdate(bar, value, *args): +def _ProgressMeterUpdate(bar, value, text_elem, *args): ''' Update the progress meter for a form :param form: class ProgressBar @@ -1969,8 +2335,8 @@ def ProgressMeterUpdate(bar, value, *args): if bar == None: return False if bar.BarExpired: return False message, w, h = ConvertArgsToSingleString(*args) - - bar.TextToDisplay = message + text_elem.Update(message) + # bar.TextToDisplay = message bar.CurrentValue = value rc = bar.UpdateBar(value) if value >= bar.MaxValue or not rc: @@ -1998,6 +2364,7 @@ class EasyProgressMeterDataClass(): self.StatMessages = stat_messages self.ParentForm = None self.MeterID = None + self.MeterText = None # =========================== COMPUTE PROGRESS STATS ======================# def ComputeProgressStats(self): @@ -2059,7 +2426,7 @@ def EasyProgressMeter(title, current_value, max_value, *args, orientation=None, EasyProgressMeter.EasyProgressMeterData = EasyProgressMeterDataClass(title, 1, int(max_value), datetime.datetime.utcnow(), []) EasyProgressMeter.EasyProgressMeterData.ComputeProgressStats() message = "\n".join([line for line in EasyProgressMeter.EasyProgressMeterData.StatMessages]) - EasyProgressMeter.EasyProgressMeterData.MeterID = ProgressMeter(title, int(max_value), message, *args, orientation=orientation, bar_color=bar_color, size=size, scale=scale, button_color=button_color, border_width=local_border_width) + EasyProgressMeter.EasyProgressMeterData.MeterID, EasyProgressMeter.EasyProgressMeterData.MeterText= _ProgressMeter(title, int(max_value), message, *args, orientation=orientation, bar_color=bar_color, size=size, scale=scale, button_color=button_color, border_width=local_border_width) EasyProgressMeter.EasyProgressMeterData.ParentForm = EasyProgressMeter.EasyProgressMeterData.MeterID.ParentForm return True # if exactly the same values as before, then ignore. @@ -2079,7 +2446,8 @@ def EasyProgressMeter(title, current_value, max_value, *args, orientation=None, message = message + str(line) + '\n' message = "\n".join(EasyProgressMeter.EasyProgressMeterData.StatMessages) args= args + (message,) - rc = ProgressMeterUpdate(EasyProgressMeter.EasyProgressMeterData.MeterID, current_value, *args) + rc = _ProgressMeterUpdate(EasyProgressMeter.EasyProgressMeterData.MeterID, current_value, + EasyProgressMeter.EasyProgressMeterData.MeterText, *args) # if counter >= max then the progress meter is all done. Indicate none running if current_value >= EasyProgressMeter.EasyProgressMeterData.MaxValue or not rc: EasyProgressMeter.EasyProgressMeterData.MeterID = None @@ -2141,7 +2509,7 @@ class DebugWin(): # print(1, 2, 3, sep='-') # if end is None: # print("") - self.form.Refresh() + self.form.ReadNonBlocking() def Close(self): self.form.CloseNonBlockingForm() @@ -2295,7 +2663,7 @@ def SetOptions(icon=None, button_color=None, element_size=(None,None), margins=( progress_meter_border_depth=None, progress_meter_style=None, progress_meter_relief=None, progress_meter_color=None, progress_meter_size=None, text_justification=None, background_color=None, element_background_color=None, - text_element_background_color=None, input_elements_background_color=None, + text_element_background_color=None, input_elements_background_color=None, input_text_color=None, scrollbar_color=None, text_color=None, element_text_color = None, debug_win_size=(None,None), window_location=(None,None)): global DEFAULT_ELEMENT_SIZE @@ -2326,6 +2694,7 @@ def SetOptions(icon=None, button_color=None, element_size=(None,None), margins=( global DEFAULT_TEXT_COLOR global DEFAULT_WINDOW_LOCATION global DEFAULT_ELEMENT_TEXT_COLOR + global DEFAULT_INPUT_TEXT_COLOR global _my_windows if icon: @@ -2420,8 +2789,62 @@ def SetOptions(icon=None, button_color=None, element_size=(None,None), margins=( if element_text_color != None: DEFAULT_ELEMENT_TEXT_COLOR = element_text_color + if input_text_color is not None: + DEFAULT_INPUT_TEXT_COLOR = input_text_color return True + +#################### ChangeLookAndFeel ####################### +# Predefined settings that will change the colors and styles # +# of the elements. # +############################################################## +def ChangeLookAndFeel(index): + # look and feel table + look_and_feel = {'GreenTan': {'BACKGROUND' : '#9FB8AD', 'TEXT': COLOR_SYSTEM_DEFAULT, 'INPUT':'#F7F3EC','TEXT_INPUT' : 'black','SCROLL': '#F7F3EC', 'BUTTON': ('white', '#475841'), 'PROGRESS':DEFAULT_PROGRESS_BAR_COLOR}, + + 'LightGreen' :{'BACKGROUND' : '#B7CECE', 'TEXT': 'black', 'INPUT':'#FDFFF7','TEXT_INPUT' : 'black', 'SCROLL': '#FDFFF7','BUTTON': ('white', '#658268'), 'PROGRESS':('#247BA0','#F8FAF0')}, + + 'BluePurple': {'BACKGROUND' : '#A5CADD', 'TEXT': '#6E266E', 'INPUT':'#E0F5FF','TEXT_INPUT' : 'black', 'SCROLL': '#E0F5FF','BUTTON': ('white', '#303952'),'PROGRESS':DEFAULT_PROGRESS_BAR_COLOR}, + + 'Purple': {'BACKGROUND': '#B0AAC2', 'TEXT': 'black', 'INPUT': '#F2EFE8','SCROLL': '#F2EFE8','TEXT_INPUT' : 'black', + 'BUTTON': ('black', '#C2D4D8'), 'PROGRESS': DEFAULT_PROGRESS_BAR_COLOR}, + + 'BlueMono': {'BACKGROUND': '#AAB6D3', 'TEXT': 'black', 'INPUT': '#F1F4FC','SCROLL': '#F1F4FC','TEXT_INPUT' : 'black', + 'BUTTON': ('white', '#7186C7'), 'PROGRESS': DEFAULT_PROGRESS_BAR_COLOR}, + + 'GreenMono': {'BACKGROUND': '#A8C1B4', 'TEXT': 'black', 'INPUT': '#DDE0DE', 'SCROLL': '#E3E3E3','TEXT_INPUT' : 'black', + 'BUTTON': ('white', '#6D9F85'), 'PROGRESS': DEFAULT_PROGRESS_BAR_COLOR}, + + 'BrownBlue': {'BACKGROUND': '#64778d', 'TEXT': 'white', 'INPUT': '#f0f3f7', 'SCROLL': '#A6B2BE','TEXT_INPUT' : 'black', + 'BUTTON': ('white', '#283b5b'), 'PROGRESS': DEFAULT_PROGRESS_BAR_COLOR}, + + 'BrightColors': {'BACKGROUND': '#b4ffb4', 'TEXT': 'black', 'INPUT': '#ffff64','SCROLL': '#ffb482','TEXT_INPUT' : 'black', + 'BUTTON': ('black', '#ffa0dc'), 'PROGRESS': DEFAULT_PROGRESS_BAR_COLOR}, + + 'TealMono': {'BACKGROUND': '#a8cfdd', 'TEXT': 'black', 'INPUT': '#dfedf2','SCROLL': '#dfedf2', 'TEXT_INPUT' : 'black', 'BUTTON': ('white', '#3b7f97'), 'PROGRESS': DEFAULT_PROGRESS_BAR_COLOR} + } + try: + colors = look_and_feel[index] + + SetOptions(background_color=colors['BACKGROUND'], + text_element_background_color=colors['BACKGROUND'], + element_background_color=colors['BACKGROUND'], + text_color=colors['TEXT'], + input_elements_background_color=colors['INPUT'], + button_color=colors['BUTTON'], + progress_meter_color=colors['PROGRESS'], + border_width=0, + slider_border_width=0, + progress_meter_border_depth=0, + scrollbar_color=(colors['SCROLL']), + element_text_color=colors['TEXT'], + input_text_color=colors['TEXT_INPUT']) + except: # most likely an index out of range + pass + + + + # ============================== sprint ======# # Is identical to the Scrolled Text Box # # Provides a crude 'print' mechanism but in a # @@ -2443,12 +2866,12 @@ def ObjToString(obj, extra=' '): def main(): - with FlexForm('Demo form..', auto_size_text=True) as form: + with FlexForm('Demo form..') as form: form_rows = [[Text('You are running the PySimpleGUI.py file itself')], [Text('You should be importing it rather than running it\n')], [Text('Here is your sample input form....')], - [Text('Source Folder', size=(15, 1), auto_size_text=False, justification='right'), InputText('Source', focus=True),FolderBrowse()], - [Text('Destination Folder', size=(15, 1), auto_size_text=False, justification='right'), InputText('Dest'), FolderBrowse()], + [Text('Source Folder', size=(15, 1), justification='right'), InputText('Source', focus=True),FolderBrowse()], + [Text('Destination Folder', size=(15, 1), justification='right'), InputText('Dest'), FolderBrowse()], [Submit(bind_return_key=True), Cancel()]] button, (source, dest) = form.LayoutAndRead(form_rows) diff --git a/docs/cookbook.md b/docs/cookbook.md index 821d5065..15cb14a3 100644 --- a/docs/cookbook.md +++ b/docs/cookbook.md @@ -536,4 +536,3 @@ Perhaps you don't want all the statistics that the EasyProgressMeter provides an ---- - diff --git a/docs/index.md b/docs/index.md index 0d55aa85..392287c2 100644 --- a/docs/index.md +++ b/docs/index.md @@ -116,13 +116,14 @@ Here is the code that produced the above screenshot. sg.Slider(range=(1, 100), orientation='v', size=(10, 20), default_value=10)], [sg.Text('_' * 100, size=(70, 1))], [sg.Text('Choose Source and Destination Folders', size=(35, 1))], - [sg.Text('Source Folder', size=(15, 1), auto_size_text=False, justification='right'), sg.InputText('Source'), sg.FolderBrowse()], + [sg.Text('Source Folder', size=(15, 1), auto_size_text=False, justification='right'), sg.InputText('Source'), + sg.FolderBrowse()], [sg.Text('Destination Folder', size=(15, 1), auto_size_text=False, justification='right'), sg.InputText('Dest'), sg.FolderBrowse()], [sg.Submit(), sg.Cancel(), sg.SimpleButton('Customized', button_color=('white', 'green'))] - ] + ] - button, values = form.LayoutAndRead(layout) + button, values = form.LayoutAndRead(layout) **A note on screen shots** You will see a number of different styles of buttons, data entry fields, etc, in this readme. They were all made with the same SDK, the only difference is in the settings that are specified on a per-element, row, form, or global basis. One setting in particular, border_width, can make a big difference on the look of the form. Some of the screenshots had a border_width of 6, others a value of 1. @@ -143,7 +144,6 @@ You will see a number of different styles of buttons, data entry fields, etc, in - Return values are a list of button presses and input values. - Return values can also be represented as a dictionary -It's stunning that after so many years Python still hasn't put forth a GUI framework that truly fits the language's basic data structures, especially lists. It's hard to argue with the success to be had in quickly building GUI applications using this package's syntax. ----- ## Getting Started with PySimpleGUI @@ -379,12 +379,94 @@ Two other types of forms exist. 1. Persistent form - rather than closing on button clicks, the show form function returns and the form continues to be visible. This is good for applications like a chat window. 2. Asynchronous form - the trickiest of the lot. Great care must be exercised. Examples are an MP3 player or status dashboard. Async forms are updated (refreshed) on a periodic basis. -It's both not enjoyable nor helpful to immediately jump into tweaking each and every little thing available to you. Let's start with a basic Browse for a file and do something with it. +It's both not enjoyable nor helpful to immediately jump into tweaking each and every little thing available to you. + +## The Form Designer +The good news to newcomers to GUI programming is that PySimpleGUI has a form designer. Better yet, the form designer requires no training and everyone knows how to use it. + +![gui0_1](https://user-images.githubusercontent.com/13696193/44159598-e2257400-a085-11e8-9b02-343e72cc75c3.JPG) + +It's a manual process, but if you follow the instructions, it will take only a minute to do and the result will be a nice looking GUI. The steps you'll take are: +1. Sketch your GUI on paper +2. Divide your GUI up into rows +3. Label each Element with the Element name +4. Write your Python code using the labels as pseudo-code + +Let's take a couple of examples. + +**Enter a number**.... Popular beginner programs are often based on a game or logic puzzle that requires the user to enter something, like a number. The "high-low" answer game comes to mind where you try to guess the number based on high or low tips. + +**Step 1- Sketch the GUI** +![gui1_1](https://user-images.githubusercontent.com/13696193/44160127-6a584900-a087-11e8-8fec-09099a8e16f6.JPG) + +**Step 2 - Divide into rows** + +![gui2_1](https://user-images.githubusercontent.com/13696193/44160128-6a584900-a087-11e8-9973-af866fb94c56.JPG) + +Step 3 - Label elements + +![gui6_1](https://user-images.githubusercontent.com/13696193/44160116-64626800-a087-11e8-8b57-671c0461b508.JPG) + +Step 4 - Write the code +The code we're writing is the layout of the GUI itself. This tutorial only focuses on getting the window code written, not the stuff to display it, get results. + +We have only 1 element on the first row, some text. Rows are written as a "list of elements", so we'll need [ ] to make a list. Here's the code for row 1 + + [ sg.Text('Enter a number') ] + +Row 2 has 1 elements, an input field. + + [ sg.Input() ] +Row 3 has an OK button + + [ sg.OK() ] + +Now that we've got the 3 rows defined, they are put into a list that represents the entire window. + + layout = [ [sg.Text('Enter a Number')], + [sg.Input()], + [sg.OK()] ] + +Finally we can put it all together into a program that will display our window. + + import PySimpleGUI as sg + + layout = [[sg.Text('Enter a Number')], + [sg.Input()], + [sg.OK()] ] + + button, (number,) = sg.FlexForm('Enter a number example').LayoutAndRead(layout) + + sg.MsgBox(button, number) + +### Example 2 - Get a filename +Let's say you've got a utility you've written that operates on some input file and you're ready to use a GUI to enter than filename rather than the command line. Follow the same steps as the previous example - draw your form on paper, break it up into rows, label the elements. + +![gui4_1](https://user-images.githubusercontent.com/13696193/44160132-6a584900-a087-11e8-862f-7d791a67ee5d.JPG) +![gui5_1](https://user-images.githubusercontent.com/13696193/44160133-6af0df80-a087-11e8-9dec-bb4d4c59393d.JPG) + +Writing the code for this one is just as straightforward. There is one tricky thing, that browse for a file button. Thankfully PySimpleGUI takes care of associating it with the input field next to it. As a result, the code looks almost exactly like the form on the paper. + + import PySimpleGUI as sg + + layout = [[sg.Text('Filename')], + [sg.Input(), sg.FileBrowse()], + [sg.OK(), sg.Cancel()] ] + + button, (number,) = sg.FlexForm('Get filename example').LayoutAndRead(layout) + + sg.MsgBox(button, number) + + +Read on for detailed instructions on the calls that show the form and return your results. + + + # Copy these design patterns! ## Pattern 1 - With Context Manager - with sg.FlexForm('SHA-1 & 256 Hash', auto_size_text=True) as form: + with sg.FlexForm('SHA-1 & 256 Hash') as form: form_rows = [[sg.Text('SHA-1 and SHA-256 Hashes for the file')], [sg.InputText(), sg.FileBrowse()], [sg.Submit(), sg.Cancel()]] @@ -393,22 +475,34 @@ It's both not enjoyable nor helpful to immediately jump into tweaking each and e ## Pattern 2 - No Context Manager - form = sg.FlexForm('SHA-1 & 256 Hash', auto_size_text=True) + form = sg.FlexForm('SHA-1 & 256 Hash') form_rows = [[sg.Text('SHA-1 and SHA-256 Hashes for the file')], [sg.InputText(), sg.FileBrowse()], [sg.Submit(), sg.Cancel()]] button, (source_filename,) = form.LayoutAndRead(form_rows) + ---- + +## Pattern 3 - Short Form -These 2 design patters both produce this custom form: + form_rows = [[sg.Text('SHA-1 and SHA-256 Hashes for the file')], + [sg.InputText(), sg.FileBrowse()], + [sg.Submit(), sg.Cancel()]] + button, (source_filename,) = sg.FlexForm('SHA-1 & 256 Hash').LayoutAndRead(form_rows) + + + +These 3 design patterns both produce this custom form: ![snap0134](https://user-images.githubusercontent.com/13696193/43162410-e7775466-8f58-11e8-8d6a-da4772c00dd8.jpg) -It's important to use the "with" context manager so that resources are freed as quickly as possible, using the currently executing thread. PySimpleGUI uses `tkinter`. `tkinter` is very picky about who releases objects and when. The `with` takes care of disposing of everything properly for you. +When you're code leaves forms open or you show many forms, then it's important to use the "with" context manager so that resources are freed as quickly as possible. PySimpleGUI uses `tkinter`. `tkinter` is very picky about who releases objects and when. The `with` takes care of disposing of everything properly for you. The second design pattern is not context manager based. If you are struggling with an unknown error, try modifying the code to run without a context manager. To do so, you simple remove the with, stick the form on the front of that statement, and un-indent the with-block code. +The third is the 'compact form'. It compacts down into 2 lines of code. One line is your form definition. The next is the call that shows the form and returns the values. You can use this pattern for simple, short programs where resource allocation isn't an issue. + You will use these design patterns or code templates for all of your "normal" (blocking) types of input forms. Copy it and modify it to suit your needs. This is the quickest way to get your code up and running with PySimpleGUI. This is the most basic / normal of the design patterns. ### How GUI Programming in Python Should Look @@ -450,6 +544,24 @@ In the statement that shows and reads the form, the two input fields are directl Isn't this what almost every Python programmer looking for a GUI wants?? Something easy to work with to get the values and move on to the rest of the program, where the real action is taking place. Why write pages of tkinter code when the same layout can be achieved with PySimpleGUI in 3 or 4 lines of code. 4 lines or 40? I chose 4. + + +### The Auto-Packer + +Once you've laid out your elements into, it's the job of the Auto-Packer to place your elements into a window frame. + +The layout of custom GUIs is made trivial by the use of the Auto-Packer. GUI frameworks often use a grid system and sometimes have a "pack" function that's used to place widgets into a window. It's almost always a confusing exercise to use them. + +PySimpleGUI uses a "row by row" approach to building GUIs. When you were to sketch your GUI out on a sheet of paper and then draw horizontal lines across the page under each widget then you would have a several "rows" of widgets. + +For each row in your GUI, you will have a list of elements. In Python this list is a simple Python list. An entire GUI window is a list of rows, one after another. + +This is how your GUI is created, one row at a time, with one row stacked on top of another. This visual form of coding makes GUI creation go so much quicker. + + layout = [ [ Row 1 Elements], + [ Row 2 Elements] ] + + ### Laying out your form Your form is a 2 dimensional list in Python. The first dimension are rows, the second is a list of Elements for each row. The first thing you want to do is layout your form on paper. @@ -1102,7 +1214,7 @@ Somewhere later in your code will be your main event loop. This is where you do break time.sleep(.01) -This loop will read button values and print them. When one of the Realtime buttons is clicked, the call to `form.ReadNonBlocking` will return a button name matching the name on the button that was depressed. It will continue to return values as long as the button remains depressed. Once released, the ReadNonBlocking will return None for buttons ules anutton was clicked. +This loop will read button values and print them. When one of the Realtime buttons is clicked, the call to `form.ReadNonBlocking` will return a button name matching the name on the button that was depressed. It will continue to return values as long as the button remains depressed. Once released, the ReadNonBlocking will return None for buttons until a button is again clicked. **File Types** The `FileBrowse` button has an additional setting named `file_types`. This variable is used to filter the files shown in the file dialog box. The default value for this setting is @@ -1183,7 +1295,54 @@ Here's a complete solution for a chat-window using an Async form with an Output print(value) else: break +------------------- +## Columns +Starting in version 2.9 you'll be able to do more complex layouts by using the Column Element. Think of a Column as a form within a form. And, yes, you can have a Column within a Column if you want. +Columns are specified in exactly the same way as a form is, as a list of lists. + +Columns are needed when you have an element that has a height > 1 line on the left, with single-line elements on the right. Here's an example of this kind of layout: + + +![column example](https://user-images.githubusercontent.com/13696193/44215113-b1097a00-a13f-11e8-96d0-f3511036494e.jpg) + +This code produced the above window. + + + import PySimpleGUI as sg + + # Demo of how columns work + # Form has on row 1 a vertical slider followed by a COLUMN with 7 rows + # Prior to the Column element, this layout was not possible + # Columns layouts look identical to form layouts, they are a list of lists of elements. + + form = sg.FlexForm('Columns') # blank form + + # Column layout + col = [[sg.Text('col Row 1')], + [sg.Text('col Row 2'), sg.Input('col input 1')], + [sg.Text('col Row 3'), sg.Input('col input 2')], + [sg.Text('col Row 4'), sg.Input('col input 3')], + [sg.Text('col Row 5'), sg.Input('col input 4')], + [sg.Text('col Row 6'), sg.Input('col input 5')], + [sg.Text('col Row 7'), sg.Input('col input 6')]] + + layout = [[sg.Slider(range=(1,100), default_value=10, orientation='v', size=(8,20)), sg.Column(col)], + [sg.In('Last input')], + [sg.OK()]] + + # Display the form and get values + # If you're willing to not use the "context manager" design pattern, then it's possible + # to collapse the form display and read down to a single line of code. + button, values = sg.FlexForm('Compact 1-line form with column').LayoutAndRead(layout) + + sg.MsgBox(button, values, line_width=200) + +The Column Element has 1 required parameter and 1 optional (the layout and the background color). Setting the background color has the same effect as setting the form's background color, except it only affects the column rectangle. + + Column(layout, background_color=None) + +The default background color for Columns is the same as the default window background color. If you change the look and feel of the form, the column background will match the form background automatically. ## Tabbed Forms Tabbed forms are shown using the `ShowTabbedForm` call. The call has the format @@ -1246,24 +1405,27 @@ Let's have some fun customizing! Make PySimpleGUI look the way you want it to l auto_size_buttons=None font=None border_width=None - slider_border_width=None - slider_relief=None - slider_orientation=None - autoclose_time=None - message_box_line_width=None - progress_meter_border_depth=None - progress_meter_style=None - progress_meter_relief=None - progress_meter_color=None - progress_meter_size=None - text_justification=None - background_color=None - element_background_color=None - text_element_background_color=None - input_elements_background_color=None - scrollbar_color=None, text_color=None - debug_win_size=(None,None) - window_location=(None,None) + slider_border_width=None + slider_relief=None + slider_orientation=None + autoclose_time=None + message_box_line_width=None + progress_meter_border_depth=None + progress_meter_style=None + progress_meter_relief=None + progress_meter_color=None + progress_meter_size=None + text_justification=None + text_color=None + background_color=None + element_background_color=None + text_element_background_color=None + input_elements_background_color=None + element_text_color=None + input_text_color=None + scrollbar_color=None, text_color=None + debug_win_size=(None,None) + window_location=(None,None) Explanation of parameters @@ -1290,6 +1452,8 @@ Explanation of parameters element_background_color - Background color of the elements text_element_background_color - Text element background color input_elements_background_color - Input fields background color + element_text_color - Text color of elements that have text, like Radio Buttons + input_text_color - Color of the text that you type in scrollbar_color - Color for scrollbars (may not always work) text_color - Text element default text color text_justification - justification to use on Text Elements. Values are strings - 'left', 'right', 'center' @@ -1314,7 +1478,7 @@ When do you use a non-blocking form? A couple of examples are * Progress Meters - when you want to make your own progress meters * Output using print to a scrolled text element. Good for debugging. -Word of warning... version 2.2, the currently released, and upcoming version 2.3 differ in the return code for the `ReadNonBlocking` call. Previously the function returned 2 values, except when the form is closed using the "X" which returned a single value of `None`. The *new* way is that `ReadNonBlocking` always returns 2 values. If the user closed the form with the "X" then the return values will be None, None. You will want to key off the second value to catch this case. +Word of warning... starting with version 2.2 there is a change in the return values from the`ReadNonBlocking` call. Previously the function returned 2 values, except when the form is closed using the "X" which returned a single value of `None`. The *new* way is that `ReadNonBlocking` always returns 2 values. If the user closed the form with the "X" then the return values will be None, None. You will want to key off the second value to catch this case. The proper code to check if the user has exited the form will be a polling-loop that looks something like this: while True: @@ -1393,13 +1557,19 @@ That's it... this example follows the async design pattern well. ## Sample Applications Use the example programs as a starting basis for your GUI. Copy, paste, modify and run! The demo files are: -`Demo Recipes.py` - Sample forms for all major form types and situations. This is the place to get your code template from. Includes asynchronous forms, etc. +`Demo_Recipes.py` - Sample forms for all major form types and situations. This is the place to get your code template from. Includes asynchronous forms, etc. Start here! -`Demo DisplayHash1and256.py` - Demonstrates using High Level API calls to get a filename +`Demo_Compare_Files` - Takes 2 filenames as input. Does a byte for byte compare and returns the results. -`Demo DupliucateFileFinder.py` - Demonstrates High Level API to get a folder & Easy Progress Meter to show progress of the file scanning + `Demo_Dictionary` - Simple form demonstrating how return values in dictionary form work. -`Demo HowDoI.py` - An amazing little application. Acts as a front-end to HowDoI. This one program could forever change how you code. It does searches on Stack Overflow and returns the CODE found in the best answer for your query. If anyone wants to help me package this application up, I could use a hand. + `Demo_DisplayHash1and256` - Presents 3 methods of gathering the same user input using both high-level APIs and lower-level. + +Demo_Func_Callback_Simulation - Shows how callback functions can be simulated. This is particularly good for the Raspberry Pi and other embedded type applications. + +`Demo_DuplicateFileFinder.py` - Demonstrates High Level API to get a folder & Easy Progress Meter to show progress of the file scanning + +`Demo_HowDoI.py` - An amazing little application. Acts as a front-end to HowDoI. This one program **could forever change how you code**. It does searches on Stack Overflow and returns the CODE found in the best answer for your query. If anyone wants to help me package this application up and release as a standalone application, then speak up on the GitHub! ## Fun Stuff Here are some things to try if you're bored or want to further customize @@ -1416,6 +1586,22 @@ This will turn all of your print statements into prints that display in a window **Look and Feel** Dial in the look and feel that you like with the `SetOptions` function. You can change all of the defaults in one function call. One line of code to customize the entire GUI. +Or beginning in version 2.9 you can choose from a look and feel using pre-defined color schemes. Call ChangeLookAndFeel with a description string. + + sg.ChangeLookAndFeel('GreenTan') + +Valid values for the description string are: + + GreenTan + LightGreen + BluePurple + Purple + BlueMono + GreenMono + BrownBlue + BrightColors + TealMono + **ObjToString** Ever wanted to easily display an objects contents easily? Use ObjToString to get a nicely formatted recursive walk of your objects. @@ -1469,7 +1655,7 @@ A MikeTheWatchGuy production... entirely responsible for this code.... unless it | 2.6.0 | July 27, 2018 - auto_size_button setting. License changed to LGPL 3+ | 2.7.0 | July 30, 2018 - realtime buttons, window_location default setting | 2.8.0 | Aug 9, 2018 - New None default option for Checkbox element, text color option for all elements, return values as a dictionary, setting focus, binding return key -| 2.9.0 | Aug XX,2018 - Screen flash fix, do_not_clear input field option, +| 2.9.0 | Aug XX,2018 - Screen flash fix, `do_not_clear` input field option, `autosize_text` defaults to `True` now, return values as ordered dict, removed text target from progress bar, rework of return values and initial return values, removed legacy Form.Refresh() method (replaced by Form.ReadNonBlockingForm()), COLUMN elements!!, ### Release Notes @@ -1521,6 +1707,9 @@ In Python, functions behave just like object. When you're placing a Text Element **Lists** It seemed quite natural to use Python's powerful list constructs when possible. The form is specified as a series of lists. Each "row" of the GUI is represented as a list of Elements. When the form read returns the results to the user, all of the results are presented as a single list. This makes reading a form's values super-simple to do in a single line of Python code. +**Dictionaries** +Want to view your form's results as a dictionary instead of a list... no problem, just use the `key` keyword on your elements. For complex forms with a lot of values that need to be changed frequently, this is by far the best way of consuming the results. + ## Authors MikeTheWatchGuy @@ -1533,6 +1722,7 @@ GNU Lesser General Public License (LGPL 3) + * Jorj McKie was the motivator behind the entire project. His wxsimpleGUI concepts sparked PySimpleGUI into existence * [Fredrik Lundh](https://wiki.python.org/moin/FredrikLundh) for his work on `tkinter` +* [Ruud van der Ham](https://forum.pythonistacafe.com/u/Ruud) for all the help he's provided as a Python-mentor. Quite a few tricky bits of logic was supplied by Ruud. The dual-purpose return values scheme is Ruud's for example ## How Do I @@ -1548,7 +1738,7 @@ Here are the steps to run that application To run it: Python HowDoI.py -The pip command is all there is to the setup. +The pip command is all there is to the setup. The way HowDoI works is that it uses your search term to look through stack overflow posts. It finds the best answer, gets the code from the answer, and presents it as a response. It gives you the correct answer OFTEN. It's a miracle that it work SO well. For Python questions, I simply start my query with 'Python'. Let's say you forgot how to reverse a list in Python. When you run HowDoI and ask this question, this is what you'll see. @@ -1556,4 +1746,5 @@ For Python questions, I simply start my query with 'Python'. Let's say you forg In the hands of a competent programmer, this tool is **amazing**. It's a must-try kind of program that has completely changed my programming process. I'm not afraid of asking for help! You just have to be smart about using what you find. -The PySimpleGUI window that the results are shown in is an 'input' field which means you can copy and paste the results right into your code. \ No newline at end of file +The PySimpleGUI window that the results are shown in is an 'input' field which means you can copy and paste the results right into your code. + diff --git a/readme.md b/readme.md index 32008eb5..392287c2 100644 --- a/readme.md +++ b/readme.md @@ -144,7 +144,6 @@ You will see a number of different styles of buttons, data entry fields, etc, in - Return values are a list of button presses and input values. - Return values can also be represented as a dictionary -It's stunning that after so many years Python still hasn't put forth a GUI framework that truly fits the language's basic data structures, especially lists. It's hard to argue with the success to be had in quickly building GUI applications using this package's syntax. ----- ## Getting Started with PySimpleGUI @@ -380,12 +379,94 @@ Two other types of forms exist. 1. Persistent form - rather than closing on button clicks, the show form function returns and the form continues to be visible. This is good for applications like a chat window. 2. Asynchronous form - the trickiest of the lot. Great care must be exercised. Examples are an MP3 player or status dashboard. Async forms are updated (refreshed) on a periodic basis. -It's both not enjoyable nor helpful to immediately jump into tweaking each and every little thing available to you. Let's start with a basic Browse for a file and do something with it. +It's both not enjoyable nor helpful to immediately jump into tweaking each and every little thing available to you. + +## The Form Designer +The good news to newcomers to GUI programming is that PySimpleGUI has a form designer. Better yet, the form designer requires no training and everyone knows how to use it. + +![gui0_1](https://user-images.githubusercontent.com/13696193/44159598-e2257400-a085-11e8-9b02-343e72cc75c3.JPG) + +It's a manual process, but if you follow the instructions, it will take only a minute to do and the result will be a nice looking GUI. The steps you'll take are: +1. Sketch your GUI on paper +2. Divide your GUI up into rows +3. Label each Element with the Element name +4. Write your Python code using the labels as pseudo-code + +Let's take a couple of examples. + +**Enter a number**.... Popular beginner programs are often based on a game or logic puzzle that requires the user to enter something, like a number. The "high-low" answer game comes to mind where you try to guess the number based on high or low tips. + +**Step 1- Sketch the GUI** +![gui1_1](https://user-images.githubusercontent.com/13696193/44160127-6a584900-a087-11e8-8fec-09099a8e16f6.JPG) + +**Step 2 - Divide into rows** + +![gui2_1](https://user-images.githubusercontent.com/13696193/44160128-6a584900-a087-11e8-9973-af866fb94c56.JPG) + +Step 3 - Label elements + +![gui6_1](https://user-images.githubusercontent.com/13696193/44160116-64626800-a087-11e8-8b57-671c0461b508.JPG) + +Step 4 - Write the code +The code we're writing is the layout of the GUI itself. This tutorial only focuses on getting the window code written, not the stuff to display it, get results. + +We have only 1 element on the first row, some text. Rows are written as a "list of elements", so we'll need [ ] to make a list. Here's the code for row 1 + + [ sg.Text('Enter a number') ] + +Row 2 has 1 elements, an input field. + + [ sg.Input() ] +Row 3 has an OK button + + [ sg.OK() ] + +Now that we've got the 3 rows defined, they are put into a list that represents the entire window. + + layout = [ [sg.Text('Enter a Number')], + [sg.Input()], + [sg.OK()] ] + +Finally we can put it all together into a program that will display our window. + + import PySimpleGUI as sg + + layout = [[sg.Text('Enter a Number')], + [sg.Input()], + [sg.OK()] ] + + button, (number,) = sg.FlexForm('Enter a number example').LayoutAndRead(layout) + + sg.MsgBox(button, number) + +### Example 2 - Get a filename +Let's say you've got a utility you've written that operates on some input file and you're ready to use a GUI to enter than filename rather than the command line. Follow the same steps as the previous example - draw your form on paper, break it up into rows, label the elements. + +![gui4_1](https://user-images.githubusercontent.com/13696193/44160132-6a584900-a087-11e8-862f-7d791a67ee5d.JPG) +![gui5_1](https://user-images.githubusercontent.com/13696193/44160133-6af0df80-a087-11e8-9dec-bb4d4c59393d.JPG) + +Writing the code for this one is just as straightforward. There is one tricky thing, that browse for a file button. Thankfully PySimpleGUI takes care of associating it with the input field next to it. As a result, the code looks almost exactly like the form on the paper. + + import PySimpleGUI as sg + + layout = [[sg.Text('Filename')], + [sg.Input(), sg.FileBrowse()], + [sg.OK(), sg.Cancel()] ] + + button, (number,) = sg.FlexForm('Get filename example').LayoutAndRead(layout) + + sg.MsgBox(button, number) + + +Read on for detailed instructions on the calls that show the form and return your results. + + + # Copy these design patterns! ## Pattern 1 - With Context Manager - with sg.FlexForm('SHA-1 & 256 Hash', auto_size_text=True) as form: + with sg.FlexForm('SHA-1 & 256 Hash') as form: form_rows = [[sg.Text('SHA-1 and SHA-256 Hashes for the file')], [sg.InputText(), sg.FileBrowse()], [sg.Submit(), sg.Cancel()]] @@ -394,22 +475,34 @@ It's both not enjoyable nor helpful to immediately jump into tweaking each and e ## Pattern 2 - No Context Manager - form = sg.FlexForm('SHA-1 & 256 Hash', auto_size_text=True) + form = sg.FlexForm('SHA-1 & 256 Hash') form_rows = [[sg.Text('SHA-1 and SHA-256 Hashes for the file')], [sg.InputText(), sg.FileBrowse()], [sg.Submit(), sg.Cancel()]] button, (source_filename,) = form.LayoutAndRead(form_rows) + ---- + +## Pattern 3 - Short Form -These 2 design patters both produce this custom form: + form_rows = [[sg.Text('SHA-1 and SHA-256 Hashes for the file')], + [sg.InputText(), sg.FileBrowse()], + [sg.Submit(), sg.Cancel()]] + button, (source_filename,) = sg.FlexForm('SHA-1 & 256 Hash').LayoutAndRead(form_rows) + + + +These 3 design patterns both produce this custom form: ![snap0134](https://user-images.githubusercontent.com/13696193/43162410-e7775466-8f58-11e8-8d6a-da4772c00dd8.jpg) -It's important to use the "with" context manager so that resources are freed as quickly as possible, using the currently executing thread. PySimpleGUI uses `tkinter`. `tkinter` is very picky about who releases objects and when. The `with` takes care of disposing of everything properly for you. +When you're code leaves forms open or you show many forms, then it's important to use the "with" context manager so that resources are freed as quickly as possible. PySimpleGUI uses `tkinter`. `tkinter` is very picky about who releases objects and when. The `with` takes care of disposing of everything properly for you. The second design pattern is not context manager based. If you are struggling with an unknown error, try modifying the code to run without a context manager. To do so, you simple remove the with, stick the form on the front of that statement, and un-indent the with-block code. +The third is the 'compact form'. It compacts down into 2 lines of code. One line is your form definition. The next is the call that shows the form and returns the values. You can use this pattern for simple, short programs where resource allocation isn't an issue. + You will use these design patterns or code templates for all of your "normal" (blocking) types of input forms. Copy it and modify it to suit your needs. This is the quickest way to get your code up and running with PySimpleGUI. This is the most basic / normal of the design patterns. ### How GUI Programming in Python Should Look @@ -451,6 +544,24 @@ In the statement that shows and reads the form, the two input fields are directl Isn't this what almost every Python programmer looking for a GUI wants?? Something easy to work with to get the values and move on to the rest of the program, where the real action is taking place. Why write pages of tkinter code when the same layout can be achieved with PySimpleGUI in 3 or 4 lines of code. 4 lines or 40? I chose 4. + + +### The Auto-Packer + +Once you've laid out your elements into, it's the job of the Auto-Packer to place your elements into a window frame. + +The layout of custom GUIs is made trivial by the use of the Auto-Packer. GUI frameworks often use a grid system and sometimes have a "pack" function that's used to place widgets into a window. It's almost always a confusing exercise to use them. + +PySimpleGUI uses a "row by row" approach to building GUIs. When you were to sketch your GUI out on a sheet of paper and then draw horizontal lines across the page under each widget then you would have a several "rows" of widgets. + +For each row in your GUI, you will have a list of elements. In Python this list is a simple Python list. An entire GUI window is a list of rows, one after another. + +This is how your GUI is created, one row at a time, with one row stacked on top of another. This visual form of coding makes GUI creation go so much quicker. + + layout = [ [ Row 1 Elements], + [ Row 2 Elements] ] + + ### Laying out your form Your form is a 2 dimensional list in Python. The first dimension are rows, the second is a list of Elements for each row. The first thing you want to do is layout your form on paper. @@ -1103,7 +1214,7 @@ Somewhere later in your code will be your main event loop. This is where you do break time.sleep(.01) -This loop will read button values and print them. When one of the Realtime buttons is clicked, the call to `form.ReadNonBlocking` will return a button name matching the name on the button that was depressed. It will continue to return values as long as the button remains depressed. Once released, the ReadNonBlocking will return None for buttons ules anutton was clicked. +This loop will read button values and print them. When one of the Realtime buttons is clicked, the call to `form.ReadNonBlocking` will return a button name matching the name on the button that was depressed. It will continue to return values as long as the button remains depressed. Once released, the ReadNonBlocking will return None for buttons until a button is again clicked. **File Types** The `FileBrowse` button has an additional setting named `file_types`. This variable is used to filter the files shown in the file dialog box. The default value for this setting is @@ -1184,7 +1295,54 @@ Here's a complete solution for a chat-window using an Async form with an Output print(value) else: break +------------------- +## Columns +Starting in version 2.9 you'll be able to do more complex layouts by using the Column Element. Think of a Column as a form within a form. And, yes, you can have a Column within a Column if you want. +Columns are specified in exactly the same way as a form is, as a list of lists. + +Columns are needed when you have an element that has a height > 1 line on the left, with single-line elements on the right. Here's an example of this kind of layout: + + +![column example](https://user-images.githubusercontent.com/13696193/44215113-b1097a00-a13f-11e8-96d0-f3511036494e.jpg) + +This code produced the above window. + + + import PySimpleGUI as sg + + # Demo of how columns work + # Form has on row 1 a vertical slider followed by a COLUMN with 7 rows + # Prior to the Column element, this layout was not possible + # Columns layouts look identical to form layouts, they are a list of lists of elements. + + form = sg.FlexForm('Columns') # blank form + + # Column layout + col = [[sg.Text('col Row 1')], + [sg.Text('col Row 2'), sg.Input('col input 1')], + [sg.Text('col Row 3'), sg.Input('col input 2')], + [sg.Text('col Row 4'), sg.Input('col input 3')], + [sg.Text('col Row 5'), sg.Input('col input 4')], + [sg.Text('col Row 6'), sg.Input('col input 5')], + [sg.Text('col Row 7'), sg.Input('col input 6')]] + + layout = [[sg.Slider(range=(1,100), default_value=10, orientation='v', size=(8,20)), sg.Column(col)], + [sg.In('Last input')], + [sg.OK()]] + + # Display the form and get values + # If you're willing to not use the "context manager" design pattern, then it's possible + # to collapse the form display and read down to a single line of code. + button, values = sg.FlexForm('Compact 1-line form with column').LayoutAndRead(layout) + + sg.MsgBox(button, values, line_width=200) + +The Column Element has 1 required parameter and 1 optional (the layout and the background color). Setting the background color has the same effect as setting the form's background color, except it only affects the column rectangle. + + Column(layout, background_color=None) + +The default background color for Columns is the same as the default window background color. If you change the look and feel of the form, the column background will match the form background automatically. ## Tabbed Forms Tabbed forms are shown using the `ShowTabbedForm` call. The call has the format @@ -1247,24 +1405,27 @@ Let's have some fun customizing! Make PySimpleGUI look the way you want it to l auto_size_buttons=None font=None border_width=None - slider_border_width=None - slider_relief=None - slider_orientation=None - autoclose_time=None - message_box_line_width=None - progress_meter_border_depth=None - progress_meter_style=None - progress_meter_relief=None - progress_meter_color=None - progress_meter_size=None - text_justification=None - background_color=None - element_background_color=None - text_element_background_color=None - input_elements_background_color=None - scrollbar_color=None, text_color=None - debug_win_size=(None,None) - window_location=(None,None) + slider_border_width=None + slider_relief=None + slider_orientation=None + autoclose_time=None + message_box_line_width=None + progress_meter_border_depth=None + progress_meter_style=None + progress_meter_relief=None + progress_meter_color=None + progress_meter_size=None + text_justification=None + text_color=None + background_color=None + element_background_color=None + text_element_background_color=None + input_elements_background_color=None + element_text_color=None + input_text_color=None + scrollbar_color=None, text_color=None + debug_win_size=(None,None) + window_location=(None,None) Explanation of parameters @@ -1291,6 +1452,8 @@ Explanation of parameters element_background_color - Background color of the elements text_element_background_color - Text element background color input_elements_background_color - Input fields background color + element_text_color - Text color of elements that have text, like Radio Buttons + input_text_color - Color of the text that you type in scrollbar_color - Color for scrollbars (may not always work) text_color - Text element default text color text_justification - justification to use on Text Elements. Values are strings - 'left', 'right', 'center' @@ -1315,7 +1478,7 @@ When do you use a non-blocking form? A couple of examples are * Progress Meters - when you want to make your own progress meters * Output using print to a scrolled text element. Good for debugging. -Word of warning... version 2.2, the currently released, and upcoming version 2.3 differ in the return code for the `ReadNonBlocking` call. Previously the function returned 2 values, except when the form is closed using the "X" which returned a single value of `None`. The *new* way is that `ReadNonBlocking` always returns 2 values. If the user closed the form with the "X" then the return values will be None, None. You will want to key off the second value to catch this case. +Word of warning... starting with version 2.2 there is a change in the return values from the`ReadNonBlocking` call. Previously the function returned 2 values, except when the form is closed using the "X" which returned a single value of `None`. The *new* way is that `ReadNonBlocking` always returns 2 values. If the user closed the form with the "X" then the return values will be None, None. You will want to key off the second value to catch this case. The proper code to check if the user has exited the form will be a polling-loop that looks something like this: while True: @@ -1394,13 +1557,19 @@ That's it... this example follows the async design pattern well. ## Sample Applications Use the example programs as a starting basis for your GUI. Copy, paste, modify and run! The demo files are: -`Demo Recipes.py` - Sample forms for all major form types and situations. This is the place to get your code template from. Includes asynchronous forms, etc. +`Demo_Recipes.py` - Sample forms for all major form types and situations. This is the place to get your code template from. Includes asynchronous forms, etc. Start here! -`Demo DisplayHash1and256.py` - Demonstrates using High Level API calls to get a filename +`Demo_Compare_Files` - Takes 2 filenames as input. Does a byte for byte compare and returns the results. -`Demo DupliucateFileFinder.py` - Demonstrates High Level API to get a folder & Easy Progress Meter to show progress of the file scanning + `Demo_Dictionary` - Simple form demonstrating how return values in dictionary form work. -`Demo HowDoI.py` - An amazing little application. Acts as a front-end to HowDoI. This one program could forever change how you code. It does searches on Stack Overflow and returns the CODE found in the best answer for your query. If anyone wants to help me package this application up, I could use a hand. + `Demo_DisplayHash1and256` - Presents 3 methods of gathering the same user input using both high-level APIs and lower-level. + +Demo_Func_Callback_Simulation - Shows how callback functions can be simulated. This is particularly good for the Raspberry Pi and other embedded type applications. + +`Demo_DuplicateFileFinder.py` - Demonstrates High Level API to get a folder & Easy Progress Meter to show progress of the file scanning + +`Demo_HowDoI.py` - An amazing little application. Acts as a front-end to HowDoI. This one program **could forever change how you code**. It does searches on Stack Overflow and returns the CODE found in the best answer for your query. If anyone wants to help me package this application up and release as a standalone application, then speak up on the GitHub! ## Fun Stuff Here are some things to try if you're bored or want to further customize @@ -1417,6 +1586,22 @@ This will turn all of your print statements into prints that display in a window **Look and Feel** Dial in the look and feel that you like with the `SetOptions` function. You can change all of the defaults in one function call. One line of code to customize the entire GUI. +Or beginning in version 2.9 you can choose from a look and feel using pre-defined color schemes. Call ChangeLookAndFeel with a description string. + + sg.ChangeLookAndFeel('GreenTan') + +Valid values for the description string are: + + GreenTan + LightGreen + BluePurple + Purple + BlueMono + GreenMono + BrownBlue + BrightColors + TealMono + **ObjToString** Ever wanted to easily display an objects contents easily? Use ObjToString to get a nicely formatted recursive walk of your objects. @@ -1470,7 +1655,7 @@ A MikeTheWatchGuy production... entirely responsible for this code.... unless it | 2.6.0 | July 27, 2018 - auto_size_button setting. License changed to LGPL 3+ | 2.7.0 | July 30, 2018 - realtime buttons, window_location default setting | 2.8.0 | Aug 9, 2018 - New None default option for Checkbox element, text color option for all elements, return values as a dictionary, setting focus, binding return key -| 2.9.0 | Aug XX,2018 - Screen flash fix, `do_not_clear` input field option, `autosize_text` defaults to `True` now, return values as ordered dict, +| 2.9.0 | Aug XX,2018 - Screen flash fix, `do_not_clear` input field option, `autosize_text` defaults to `True` now, return values as ordered dict, removed text target from progress bar, rework of return values and initial return values, removed legacy Form.Refresh() method (replaced by Form.ReadNonBlockingForm()), COLUMN elements!!, ### Release Notes @@ -1522,6 +1707,9 @@ In Python, functions behave just like object. When you're placing a Text Element **Lists** It seemed quite natural to use Python's powerful list constructs when possible. The form is specified as a series of lists. Each "row" of the GUI is represented as a list of Elements. When the form read returns the results to the user, all of the results are presented as a single list. This makes reading a form's values super-simple to do in a single line of Python code. +**Dictionaries** +Want to view your form's results as a dictionary instead of a list... no problem, just use the `key` keyword on your elements. For complex forms with a lot of values that need to be changed frequently, this is by far the best way of consuming the results. + ## Authors MikeTheWatchGuy @@ -1550,7 +1738,7 @@ Here are the steps to run that application To run it: Python HowDoI.py -The pip command is all there is to the setup. +The pip command is all there is to the setup. The way HowDoI works is that it uses your search term to look through stack overflow posts. It finds the best answer, gets the code from the answer, and presents it as a response. It gives you the correct answer OFTEN. It's a miracle that it work SO well. For Python questions, I simply start my query with 'Python'. Let's say you forgot how to reverse a list in Python. When you run HowDoI and ask this question, this is what you'll see. @@ -1558,4 +1746,5 @@ For Python questions, I simply start my query with 'Python'. Let's say you forg In the hands of a competent programmer, this tool is **amazing**. It's a must-try kind of program that has completely changed my programming process. I'm not afraid of asking for help! You just have to be smart about using what you find. -The PySimpleGUI window that the results are shown in is an 'input' field which means you can copy and paste the results right into your code. \ No newline at end of file +The PySimpleGUI window that the results are shown in is an 'input' field which means you can copy and paste the results right into your code. +