diff --git a/Demo_MIDI_Player.py b/Demo_MIDI_Player.py new file mode 100644 index 00000000..3da9b17d --- /dev/null +++ b/Demo_MIDI_Player.py @@ -0,0 +1,228 @@ +import os +import PySimpleGUI as g +import mido +import time + +PLAYER_COMMAND_NONE = 0 +PLAYER_COMMAND_EXIT = 1 +PLAYER_COMMAND_PAUSE = 2 +PLAYER_COMMAND_NEXT = 3 +PLAYER_COMMAND_RESTART_SONG = 4 + +# ---------------------------------------------------------------------- # +# PlayerGUI CLASS # +# ---------------------------------------------------------------------- # +class PlayerGUI(): + ''' + Class implementing GUI for both initial screen but the player itself + ''' + + def __init__(self): + self.Form = None + self.TextElem = None + self.PortList = mido.get_output_names() # use to get the list of midi ports + self.PortList = self.PortList[::-1] # reverse the list so the last one is first + + # ---------------------------------------------------------------------- # + # PlayerChooseSongGUI # + # Show a GUI get to the file to playback # + # ---------------------------------------------------------------------- # + def PlayerChooseSongGUI(self): + + # ---------------------- DEFINION OF CHOOSE WHAT TO PLAY GUI ---------------------------- + with g.FlexForm('MIDI File Player', auto_size_text=False, + default_element_size=(30, 1), + font=("Helvetica", 12)) as form: + layout = [[g.Text('MIDI File Player', font=("Helvetica", 15), size=(20, 1), text_color='green')], + [g.Text('File Selection', font=("Helvetica", 15), size=(20, 1))], + [g.Text('Single File Playback', justification='right'), g.InputText(size=(65, 1), key='midifile'), g.FileBrowse(size=(10, 1), file_types=(("MIDI files", "*.mid"),))], + [g.Text('Or Batch Play From This Folder', auto_size_text=False, justification='right'), g.InputText(size=(65, 1), key='folder'), g.FolderBrowse(size=(10, 1))], + [g.Text('_' * 250, auto_size_text=False, size=(100, 1))], + [g.Text('Choose MIDI Output Device', size=(22, 1)), + g.Listbox(values=self.PortList, size=(30, len(self.PortList) + 1), key='device')], + [g.Text('_' * 250, auto_size_text=False, size=(100, 1))], + [g.SimpleButton('PLAY!', size=(10, 2), button_color=('red', 'white'), font=("Helvetica", 15), bind_return_key=True), g.Text(' ' * 2, size=(4, 1)), g.Cancel(size=(8, 2), font=("Helvetica", 15))]] + + + self.Form = form + return form.LayoutAndRead(layout) + + + def PlayerPlaybackGUIStart(self, NumFiles=1): + # ------- Make a new FlexForm ------- # + + image_pause = './ButtonGraphics/Pause.png' + image_restart = './ButtonGraphics/Restart.png' + image_next = './ButtonGraphics/Next.png' + image_exit = './ButtonGraphics/Exit.png' + + self.TextElem = g.T('Song loading....', size=(85, 5 + NumFiles), font=("Helvetica", 14), auto_size_text=False) + form = g.FlexForm('MIDI File Player', default_element_size=(30, 1),font=("Helvetica", 25)) + layout = [ + [g.T('MIDI File Player', size=(30, 1), font=("Helvetica", 25))], + [self.TextElem], + [g.ReadFormButton('PAUSE', button_color=g.TRANSPARENT_BUTTON, + image_filename=image_pause, image_size=(50,50),image_subsample=2, border_width=0, + font=("Helvetica", 15), size=(10, 2)), g.T(' ' * 3), + g.ReadFormButton('NEXT', button_color=g.TRANSPARENT_BUTTON, + image_filename=image_next, image_size=(50,50),image_subsample=2, border_width=0, + size=(10, 2), font=("Helvetica", 15)), g.T(' ' * 3), + g.ReadFormButton('Restart Song', button_color=g.TRANSPARENT_BUTTON, + image_filename=image_restart, image_size=(50,50), image_subsample=2,border_width=0, + size=(10, 2), font=("Helvetica", 15)), g.T(' ' * 3), + g.T(' '*2), g.SimpleButton('EXIT', button_color=g.TRANSPARENT_BUTTON, + image_filename=image_exit, image_size=(50,50), image_subsample=2,border_width=0, + size=(10, 2), font=("Helvetica", 15))] + ] + + form.LayoutAndRead(layout, non_blocking=True) + self.Form = form + + + + # ------------------------------------------------------------------------- # + # PlayerPlaybackGUIUpdate # + # Refresh the GUI for the main playback interface (must call periodically # + # ------------------------------------------------------------------------- # + def PlayerPlaybackGUIUpdate(self, DisplayString): + form = self.Form + if 'form' not in locals() or form is None: # if the form has been destoyed don't mess with it + return PLAYER_COMMAND_EXIT + self.TextElem.Update(DisplayString) + button, (values) = form.ReadNonBlocking() + if values is None: + return PLAYER_COMMAND_EXIT + if button == 'PAUSE': + return PLAYER_COMMAND_PAUSE + elif button == 'EXIT': + return PLAYER_COMMAND_EXIT + elif button == 'NEXT': + return PLAYER_COMMAND_NEXT + elif button == 'Restart Song': + return PLAYER_COMMAND_RESTART_SONG + return PLAYER_COMMAND_NONE + + +# ---------------------------------------------------------------------- # +# MAIN - our main program... this is it # +# Runs the GUI to get the file / path to play # +# Decodes the MIDI-Video into a MID file # +# Plays the decoded MIDI file # +# ---------------------------------------------------------------------- # +def main(): + def GetCurrentTime(): + ''' + Get the current system time in milliseconds + :return: milliseconds + ''' + return int(round(time.time() * 1000)) + + g.SetOptions(border_width=1, element_padding=(4, 6), font=("Helvetica", 10), button_color=('white', g.BLUES[0]), + progress_meter_border_depth=1, slider_border_width=1) + pback = PlayerGUI() + + button, values = pback.PlayerChooseSongGUI() + if button != 'PLAY!': + g.MsgBoxCancel('Cancelled...\nAutoclose in 2 sec...', auto_close=True, auto_close_duration=2) + exit(69) + if values['device'] is not None: + midi_port = values['device'][0] + else: + g.MsgBoxCancel('No devices found\nAutoclose in 2 sec...', auto_close=True, auto_close_duration=2) + + batch_folder = values['folder'] + midi_filename = values['midifile'] + # ------ Build list of files to play --------------------------------------------------------- # + if batch_folder: + filelist = os.listdir(batch_folder) + filelist = [batch_folder+'/'+f for f in filelist if f.endswith(('.mid', '.MID'))] + filetitles = [os.path.basename(f) for f in filelist] + elif midi_filename: # an individual filename + filelist = [midi_filename,] + filetitles = [os.path.basename(midi_filename),] + else: + g.MsgBoxError('*** Error - No MIDI files specified ***') + exit(666) + + # ------ LOOP THROUGH MULTIPLE FILES --------------------------------------------------------- # + pback.PlayerPlaybackGUIStart(NumFiles=len(filelist) if len(filelist) <=10 else 10) + port = None + # Loop through the files in the filelist + for now_playing_number, current_midi_filename in enumerate(filelist): + display_string = 'Playing Local File...\n{} of {}\n{}'.format(now_playing_number+1, len(filelist), current_midi_filename) + midi_title = filetitles[now_playing_number] + # --------------------------------- REFRESH THE GUI ----------------------------------------- # + pback.PlayerPlaybackGUIUpdate(display_string) + + # ---===--- Output Filename is .MID --- # + midi_filename = current_midi_filename + + # --------------------------------- MIDI - STARTS HERE ----------------------------------------- # + if not port: # if the midi output port not opened yet, then open it + port = mido.open_output(midi_port if midi_port else None) + + try: + mid = mido.MidiFile(filename=midi_filename) + except: + print('****** Exception trying to play MidiFile filename = {}***************'.format(midi_filename)) + g.MsgBoxError('Exception trying to play MIDI file:', midi_filename, 'Skipping file') + continue + + # Build list of data contained in MIDI File using only track 0 + midi_length_in_seconds = mid.length + display_file_list = '>> ' + '\n'.join([f for i, f in enumerate(filelist[now_playing_number:]) if i < 10]) + paused = cancelled = next_file = False + ######################### Loop through MIDI Messages ########################### + while(True): + start_playback_time = GetCurrentTime() + port.reset() + + for midi_msg_number, msg in enumerate(mid.play()): + #################### GUI - read values ################## + if not midi_msg_number % 4: # update the GUI every 4 MIDI messages + t = (GetCurrentTime() - start_playback_time)//1000 + display_midi_len = '{:02d}:{:02d}'.format(*divmod(int(midi_length_in_seconds),60)) + display_string = 'Now Playing {} of {}\n{}\n {:02d}:{:02d} of {}\nPlaylist:'.\ + format(now_playing_number+1, len(filelist), midi_title, *divmod(t, 60), display_midi_len) + # display list of next 10 files to be played. + rc = pback.PlayerPlaybackGUIUpdate(display_string + '\n' + display_file_list) + else: # fake rest of code as if GUI did nothing + rc = PLAYER_COMMAND_NONE + if paused: + rc = PLAYER_COMMAND_NONE + while rc == PLAYER_COMMAND_NONE: # TIGHT-ASS loop waiting on a GUI command + rc = pback.PlayerPlaybackGUIUpdate(display_string) + time.sleep(.25) + + ####################################### MIDI send data ################################## + port.send(msg) + + # ------- Execute GUI Commands after sending MIDI data ------- # + if rc == PLAYER_COMMAND_EXIT: + cancelled = True + break + elif rc == PLAYER_COMMAND_PAUSE: + paused = not paused + port.reset() + elif rc == PLAYER_COMMAND_NEXT: + next_file = True + break + elif rc == PLAYER_COMMAND_RESTART_SONG: + break + + if cancelled or next_file: + break + #------- DONE playing the song ------- # + port.reset() # reset the midi port when done with the song + + if cancelled: + break + exit(69) + +# ---------------------------------------------------------------------- # +# LAUNCH POINT -- program starts and ends here # +# ---------------------------------------------------------------------- # +if __name__ == '__main__': + main() + + exit(69) diff --git a/PySimpleGUI.py b/PySimpleGUI.py index 9fa1ca91..0fc5f76a 100644 --- a/PySimpleGUI.py +++ b/PySimpleGUI.py @@ -290,7 +290,7 @@ class Listbox(Element): :param auto_size_text: True if should shrink field to fit the default text :param background_color: Color for Element. Text or RGB Hex ''' self.Values = values - self.TKListBox = None + self.TKListbox = None if select_mode == LISTBOX_SELECT_MODE_BROWSE: self.SelectMode = SELECT_MODE_BROWSE elif select_mode == LISTBOX_SELECT_MODE_EXTENDED: @@ -305,6 +305,12 @@ class Listbox(Element): 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 Update(self, values): + self.TKListbox.delete(0, 'end') + for item in values: + self.TKListbox.insert(tk.END, item) + self.TKListbox.selection_set(0, 0) + def __del__(self): try: self.TKListBox.__del__() @@ -476,10 +482,15 @@ class Text(Element): super().__init__(ELEM_TYPE_TEXT, scale, size, auto_size_text, background_color=bg, font=font if font else DEFAULT_FONT, text_color=self.TextColor) return - def Update(self, NewValue): - self.DisplayText=NewValue - stringvar = self.TKStringVar - stringvar.set(NewValue) + def Update(self, new_value = None, background_color=None, text_color=None): + if new_value is not None: + self.DisplayText=new_value + stringvar = self.TKStringVar + stringvar.set(new_value) + if background_color is not None: + self.TKText.configure(background=background_color) + if text_color is not None: + self.TKText.configure(fg=text_color) def __del__(self): super().__del__() @@ -704,6 +715,14 @@ class Button(Element): self.ParentForm.TKroot.quit() # kick the users out of the mainloop return + def Update(self, new_text, button_color=(None, None)): + try: + self.TKButton.configure(text=new_text) + if button_color != (None, None): + self.TKButton.config(foreground=button_color[0], background=button_color[1]) + except: + return + def __del__(self): try: self.TKButton.__del__() @@ -821,6 +840,9 @@ class Slider(Element): super().__init__(ELEM_TYPE_INPUT_SLIDER, scale=scale, size=size, font=font, background_color=background_color, text_color=text_color, key=key) return + def Update(self, value): + self.TKIntVar.set(value) + def __del__(self): super().__del__() @@ -1026,8 +1048,6 @@ class FlexForm: if self.RootNeedsDestroying: self.TKroot.destroy() _my_windows.Decrement() - # if self.ReturnValues[0] is not None: # keyboard events build their own return values - # return self.ReturnValues if self.LastKeyboardEvent is not None or self.LastButtonClicked is not None: return BuildResults(self, False, self) else: @@ -1046,6 +1066,7 @@ class FlexForm: except: self.TKrootDestroyed = True _my_windows.Decrement() + # return None, None return BuildResults(self, False, self) def GetScreenDimensions(self): @@ -1452,7 +1473,7 @@ def PackFormIntoFrame(form, containing_frame, toplevel_form): stringvar = tk.StringVar() element.TKStringVar = stringvar stringvar.set(display_text) - if auto_size_text: + if element.AutoSizeText: width = 0 if element.Justification is not None: justification = element.Justification @@ -1463,16 +1484,18 @@ def PackFormIntoFrame(form, containing_frame, toplevel_form): justify = tk.LEFT if justification == 'left' else tk.CENTER if justification == 'center' else tk.RIGHT anchor = tk.NW if justification == 'left' else tk.N if 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+40) # set wrap to width of widget + wraplen = tktext_label.winfo_reqwidth()+40 # width of widget in Pixels + if not auto_size_text: + wraplen = 0 + # print("wraplen, width, height", wraplen, width, height) + tktext_label.configure(anchor=anchor, font=font, wraplen=wraplen) # 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) - # print(f'Text element placed w = {width}, h = {height}, wrap = {wraplen}') + element.TKText = tktext_label # ------------------------- BUTTON element ------------------------- # elif element_type == ELEM_TYPE_BUTTON: element.Location = (row_num, col_num) @@ -1933,6 +1956,7 @@ def MsgBox(*args, button_color=None, button_type=MSG_BOX_OK, auto_close=False, a max_line_total = max(max_line_total, width_used) # height = _GetNumLinesNeeded(message, width_used) height = message_wrapped_lines + # print('Msgbox width, height', width_used, height) form.AddRow(Text(message_wrapped, auto_size_text=True, size=(width_used, height))) total_lines += height diff --git a/docs/cookbook.md b/docs/cookbook.md index 61e581c0..256b2cbf 100644 --- a/docs/cookbook.md +++ b/docs/cookbook.md @@ -57,7 +57,7 @@ Browse for a filename that is populated into the input field. import PySimpleGUI as sg - 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()]] @@ -101,7 +101,7 @@ Example of nearly all of the widgets in a single form. Uses a customized color progress_meter_border_depth=0, scrollbar_color='#F7F3EC') - with sg.FlexForm('Everything bagel', auto_size_text=True, default_element_size=(40, 1)) as form: + with sg.FlexForm('Everything bagel', default_element_size=(40, 1)) as form: layout = [ [sg.Text('All graphic widgets in one form!', size=(30, 1), font=("Helvetica", 25))], [sg.Text('Here is some text.... and a place to enter text')], @@ -141,7 +141,7 @@ Example of nearly all of the widgets in a single form. Uses a customized color progress_meter_border_depth=0, scrollbar_color='#F7F3EC') - form = sg.FlexForm('Everything bagel', auto_size_text=True, default_element_size=(40, 1)) + form = sg.FlexForm('Everything bagel', default_element_size=(40, 1)) layout = [ [sg.Text('All graphic widgets in one form!', size=(30, 1), font=("Helvetica", 25))], [sg.Text('Here is some text.... and a place to enter text')], @@ -175,7 +175,7 @@ An async form that has a button read loop. A Text Element is updated periodical import PySimpleGUI as sg import time - form = sg.FlexForm('Running Timer', auto_size_text=True) + form = sg.FlexForm('Running Timer') # create a text element that will be updated periodically text_element = sg.Text('', size=(10, 2), font=('Helvetica', 20), justification='center') @@ -210,7 +210,7 @@ Like the previous recipe, this form is an async form. The difference is that th import PySimpleGUI as sg import time - with sg.FlexForm('Running Timer', auto_size_text=True) as form: + with sg.FlexForm('Running Timer') as form: text_element = sg.Text('', size=(10, 2), font=('Helvetica', 20), text_color='red', justification='center') layout = [[sg.Text('Non blocking GUI with updates', justification='center')], [text_element], @@ -249,7 +249,7 @@ The architecture of some programs works better with button callbacks instead of # Create a standard form form = sg.FlexForm('Button callback example') # Layout the design of the GUI - layout = [[sg.Text('Please click a button', auto_size_text=True)], + layout = [[sg.Text('Please click a button')], [sg.ReadFormButton('1'), sg.ReadFormButton('2'), sg.Quit()]] # Show the form to the user form.Layout(layout) @@ -593,3 +593,44 @@ To make it easier to see the Column in the window, the Column background has bee button, values = sg.FlexForm('Compact 1-line form with column').LayoutAndRead(layout) sg.MsgBox(button, values, line_width=200) + + +## Persistent Form With Text Element Updates + +This simple program keep a form open, taking input values until the user terminates the program using the "X" button. + +![math game](https://user-images.githubusercontent.com/13696193/44537842-c9444080-a6cd-11e8-94bc-6cdf1b765dd8.jpg) + + + + import PySimpleGUI as sg + + form = sg.FlexForm('Math') + + output = sg.Txt('', size=(8,1)) + + layout = [ [sg.Txt('Enter values to calculate')], + [sg.In(size=(8,1), key='numerator')], + [sg.Txt('_' * 10)], + [sg.In(size=(8,1), key='denominator')], + [output], + [sg.ReadFormButton('Calculate', bind_return_key=True)]] + + form.Layout(layout) + + while True: + button, values = form.Read() + + if button is not None: + try: + numerator = float(values['numerator']) + denominator = float(values['denominator']) + calc = numerator / denominator + except: + calc = 'Invalid' + + output.Update(calc) + else: + break + +