PySimpleGUI/PySimpleGUI.py
MikeTheWatchGuy a77dc1c724 Fixed message box text width, renamed Display Hash, added Duplicate file finder
FINALLY got the message box text width sizing correct.  Required change to Text Elements so watch out for possible side effects.
Added a new Duplicate File Finder demo program that uses an input form an a progress meter
2018-07-15 19:21:06 -04:00

1731 lines
79 KiB
Python

#!/usr/bin/env Python3
import tkinter as tk
from tkinter import filedialog
from tkinter import ttk
import tkinter.scrolledtext as tkst
import tkinter.font
from random import randint
import datetime
import sys
import textwrap
# ----====----====----==== Constants the use CAN safely change ====----====----====----#
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_FONT = ("Helvetica", 10)
DEFAULT_BORDER_WIDTH = 6
DEFAULT_AUTOCLOSE_TIME = 3 # time in seconds to show an autoclose form
MAX_SCROLLED_TEXT_BOX_HEIGHT = 50
#################### COLOR STUFF ####################
BLUES = ("#082567","#0A37A3","#00345B")
PURPLES = ("#480656","#4F2398","#380474")
GREENS = ("#01826B","#40A860","#96D2AB", "#00A949","#003532")
YELLOWS = ("#F3FB62", "#F0F595")
TANS = ("#FFF9D5","#F4EFCF","#DDD8BA")
NICE_BUTTON_COLORS = ((GREENS[3], TANS[0]), ('#000000','#FFFFFF'),('#FFFFFF', '#000000'), (YELLOWS[0], PURPLES[1]),
(YELLOWS[0], GREENS[3]), (YELLOWS[0], BLUES[2]))
# 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
# DEFAULT_BUTTON_COLOR = (YELLOWS[0], GREENS[4]) # Foreground, Background (None, None) == System Default
DEFAULT_BUTTON_COLOR = ('white', 'black') # Foreground, Background (None, None) == System Default
# DEFAULT_BUTTON_COLOR = (YELLOWS[0], PURPLES[2]) # Foreground, Background (None, None) == System Default
DEFAULT_ERROR_BUTTON_COLOR =("#FFFFFF", "#FF0000")
DEFAULT_CANCEL_BUTTON_COLOR = (GREENS[3], TANS[0])
BarColor=()
# DEFAULT_PROGRESS_BAR_COLOR = (GREENS[2], GREENS[0]) # a nice green progress bar
DEFAULT_PROGRESS_BAR_COLOR = (GREENS[3], GREENS[3]) # a nice green progress bar
# DEFAULT_PROGRESS_BAR_COLOR = (BLUES[1], BLUES[1]) # a nice green progress bar
# DEFAULT_PROGRESS_BAR_COLOR = (BLUES[0], BLUES[0]) # a nice green progress bar
# DEFAULT_PROGRESS_BAR_COLOR = (PURPLES[1],PURPLES[0]) # a nice purple progress bar
DEFAULT_PROGRESS_BAR_SIZE = (30,25) # Size of Progress Bar (characters for length, pixels for width)
DEFAULT_PROGRESS_BAR_BORDER_WIDTH=8
DEFAULT_PROGRESS_BAR_RELIEF = tk.SUNKEN
DEFAULT_PROGRESS_BAR_STYLE = 'default'
DEFAULT_METER_ORIENTATION = 'Horizontal'
# DEFAULT_METER_ORIENTATION = 'Vertical'
# ----====----====----==== Constants the user should NOT f-with ====----====----====----#
ThisRow = 555666777 # magic number
# Progress Bar Relief Choices
# -relief
RAISED='raised'
SUNKEN='sunken'
FLAT='flat'
RIDGE='ridge'
GROOVE='groove'
SOLID = 'solid'
PROGRESS_BAR_STYLES = ('default','winnative', 'clam', 'alt', 'classic', 'vista', 'xpnative')
# DEFAULT_WINDOW_ICON = ''
MESSAGE_BOX_LINE_WIDTH = 60
# a shameful global variable. This represents the top-level window information. Needed because opening a second window is different than opening the first.
class MyWindows():
def __init__(self):
self.NumOpenWindows = 0
self.user_defined_icon = None
_my_windows = MyWindows() # terrible hack using globals... means need a class for collecing windows
# ====================================================================== #
# One-liner functions that are handy as f_ck #
# ====================================================================== #
def RGB(red,green,blue): return '#%02x%02x%02x' % (red,green,blue)
# ====================================================================== #
# Enums for types #
# ====================================================================== #
# ------------------------- Button types ------------------------- #
#todo Consider removing the Submit, Cancel types... they are just 'RETURN' type in reality
#uncomment this line and indent to go back to using Enums
# class ButtonType(Enum):
BROWSE_FOLDER = 1
BROWSE_FILE = 2
CLOSES_WIN = 5
READ_FORM = 7
# ------------------------- Element types ------------------------- #
# class ElementType(Enum):
TEXT = 1
INPUT_TEXT = 20
INPUT_COMBO = 21
INPUT_RADIO = 5
INPUT_MULTILINE = 7
INPUT_CHECKBOX = 8
INPUT_SPIN = 9
BUTTON = 3
OUTPUT = 300
PROGRESS_BAR = 200
BLANK = 100
# ------------------------- MsgBox Buttons Types ------------------------- #
MSG_BOX_YES_NO = 1
MSG_BOX_CANCELLED = 2
MSG_BOX_ERROR = 3
MSG_BOX_OK_CANCEL = 4
MSG_BOX_OK = 0
# ---------------------------------------------------------------------- #
# Cascading structure.... Objects get larger #
# Button #
# Element #
# Row #
# Form #
# ---------------------------------------------------------------------- #
# ------------------------------------------------------------------------- #
# Element CLASS #
# ------------------------------------------------------------------------- #
class Element():
def __init__(self, Type, Scale=(None, None), Size=(None, None), AutoSizeText=None, Font=None):
self.Size = Size
self.Type = Type
self.AutoSizeText = AutoSizeText
self.Scale = Scale
self.Pad = DEFAULT_ELEMENT_PADDING
self.Font = Font
self.TKStringVar = None
self.TKIntVar = None
self.TKText = None
self.TKEntry = None
self.ParentForm=None
self.TextInputDefault = None
self.Position = (0,0) # Default position Row 0, Col 0
return
def __del__(self):
try:
self.TKStringVar.__del__()
except:
pass
try:
self.TKIntVar.__del__()
except:
pass
try:
self.TKText.__del__()
except:
pass
try:
self.TKEntry.__del__()
except:
pass
# ---------------------------------------------------------------------- #
# Input Class #
# ---------------------------------------------------------------------- #
class InputText(Element):
def __init__(self, DefaultText = '', Scale=(None, None), Size=(None, None), AutoSizeText=None):
self.DefaultText = DefaultText
super().__init__(INPUT_TEXT, Scale, Size, AutoSizeText)
return
def ReturnKeyHandler(self, event):
MyForm = self.ParentForm
# search through this form and find the first button that will exit the form
for row in MyForm.Rows:
for element in row.Elements:
if element.Type == BUTTON:
if element.BType == CLOSES_WIN or element.BType == READ_FORM:
element.ButtonCallBack()
return
def __del__(self):
super().__del__()
# ---------------------------------------------------------------------- #
# Combo #
# ---------------------------------------------------------------------- #
class InputCombo(Element):
def __init__(self, Values, Scale=(None, None), Size=(None, None), AutoSizeText=None):
self.Values = Values
self.TKComboBox = None
super().__init__(INPUT_COMBO, Scale, Size, AutoSizeText)
return
def __del__(self):
try:
self.TKComboBox.__del__()
except:
pass
super().__del__()
# ---------------------------------------------------------------------- #
# Radio #
# ---------------------------------------------------------------------- #
class Radio(Element):
def __init__(self, Text, GroupID, Default=False, Scale=(None, None), Size=(None, None), AutoSizeText=None,Font=None):
self.InitialState = Default
self.Text = Text
self.TKRadio = None
self.GroupID = GroupID
self.Value = None
super().__init__(INPUT_RADIO, Scale, Size, AutoSizeText, Font)
return
def __del__(self):
try:
self.TKRadio.__del__()
except:
pass
super().__del__()
# ---------------------------------------------------------------------- #
# Checkbox #
# ---------------------------------------------------------------------- #
class Checkbox(Element):
def __init__(self, Text, Default=False, Scale=(None, None), Size=(None, None), AutoSizeText=None, Font=None):
self.Text = Text
self.InitialState = Default
self.Value = None
self.TKCheckbox = None
super().__init__(INPUT_CHECKBOX, Scale, Size, AutoSizeText, Font)
return
def __del__(self):
try:
self.TKCheckbox.__del__()
except:
pass
super().__del__()
# ---------------------------------------------------------------------- #
# Spin #
# ---------------------------------------------------------------------- #
class Spin(Element):
# Values = None
# TKSpinBox = None
def __init__(self, Values, InitialValue=None, Scale=(None, None), Size=(None, None), AutoSizeText=None, Font=None):
self.Values = Values
self.DefaultValue = InitialValue
self.TKSpinBox = None
super().__init__(INPUT_SPIN, Scale, Size, AutoSizeText, Font=Font)
return
def __del__(self):
try:
self.TKSpinBox.__del__()
except:
pass
super().__del__()
# ---------------------------------------------------------------------- #
# Multiline #
# ---------------------------------------------------------------------- #
class Multiline(Element):
def __init__(self, DefaultText='', EnterSubmits = False, Scale=(None, None), Size=(None, None), AutoSizeText=None):
self.DefaultText = DefaultText
self.EnterSubmits = EnterSubmits
super().__init__(INPUT_MULTILINE, Scale, Size, AutoSizeText)
return
def ReturnKeyHandler(self, event):
MyForm = self.ParentForm
# search through this form and find the first button that will exit the form
for row in MyForm.Rows:
for element in row.Elements:
if element.Type == BUTTON:
if element.BType == CLOSES_WIN or element.BType == READ_FORM:
element.ButtonCallBack()
return
def __del__(self):
super().__del__()
# ---------------------------------------------------------------------- #
# Text #
# ---------------------------------------------------------------------- #
class Text(Element):
def __init__(self, Text, Scale=(None, None), Size=(None, None), AutoSizeText=None, Font=None, TextColor=None):
self.DisplayText = Text
self.TextColor = TextColor if TextColor else 'black'
# self.Font = Font if Font else DEFAULT_FONT
# i=1/0
super().__init__(TEXT, Scale, Size, AutoSizeText, Font=Font if Font else DEFAULT_FONT)
return
def Update(self, NewValue):
self.DisplayText=NewValue
stringvar = self.TKStringVar
stringvar.set(NewValue)
def __del__(self):
super().__del__()
# ---------------------------------------------------------------------- #
# TKProgressBar #
# Emulate the TK ProgressBar using canvas and rectangles
# ---------------------------------------------------------------------- #
class TKProgressBar():
def __init__(self, root, Max, Length=400, Width=20, Highlightt=0, Relief='sunken', Borderwidth=4, Orientation='horizontal', BarColor=DEFAULT_PROGRESS_BAR_COLOR):
self.Length = Length
self.Width = Width
self.Max = Max
self.Orientation = Orientation
self.Count = None
self.PriorCount = 0
if Orientation[0].lower() == 'h':
self.TKCanvas = tk.Canvas(root, width=Length, height=Width, highlightt=Highlightt, relief=Relief, borderwidth=Borderwidth)
self.TKRect = self.TKCanvas.create_rectangle(0, 0, -(Length * 1.5), Width * 1.5, fill=BarColor[0], tags='bar')
# self.canvas.pack(padx='10')
else:
self.TKCanvas = tk.Canvas(root, width=Width, height=Length, highlightt=Highlightt, relief=Relief, borderwidth=Borderwidth)
self.TKRect = self.TKCanvas.create_rectangle(Width * 1.5, 2 * Length + 40, 0, Length * .5, fill=BarColor[0], tags='bar')
# self.canvas.pack()
def Update(self,Count):
if Count > self.Max: return
if self.Orientation[0].lower() == 'h':
try:
if Count != self.PriorCount:
delta = Count - self.PriorCount
self.TKCanvas.move(self.TKRect, delta*(self.Length / self.Max), 0)
if 0: self.TKCanvas.update()
except:
return False # the window was closed by the user on us
else:
try:
if Count != self.PriorCount:
delta = Count - self.PriorCount
self.TKCanvas.move(self.TKRect, 0, delta*(-self.Length / self.Max))
if 0: self.TKCanvas.update()
except:
return False # the window was closed by the user on us
self.PriorCount = Count
return True
def __del__(self):
try:
self.TKCanvas.__del__()
self.TKRect.__del__()
except:
pass
# ---------------------------------------------------------------------- #
# Output #
# New Type of Widget that's a Text Widget in disguise #
# ---------------------------------------------------------------------- #
class TKOutput(tk.Frame):
''' Demonstrate python interpreter output in Tkinter Text widget
type python expression in the entry, hit DoIt and see the results
in the text pane.'''
# previous_stderr = None
# previous_stdout = None
def __init__(self, parent, width, height, bd):
tk.Frame.__init__(self, parent)
self.output = tk.Text(parent, width=width, height=height, bd=bd)
self.vsb = tk.Scrollbar(parent, orient="vertical", command=self.output.yview)
self.vsb.pack(side="right", fill="y")
self.output.configure(yscrollcommand=self.vsb.set)
self.output.pack(side="left", fill="both", expand=True)
self.previous_stdout = sys.stdout
self.previous_stderr = sys.stderr
sys.stdout = self
sys.stderr = self
self.pack()
def write(self, txt):
try:
self.output.insert(tk.END, str(txt))
self.output.see(tk.END)
except:
pass
def Close(self):
sys.stdout = self.previous_stdout
sys.stderr = self.previous_stderr
def flush(self):
sys.stdout = self.previous_stdout
sys.stderr = self.previous_stderr
def __del__(self):
sys.stdout = self.previous_stdout
class Output(Element):
def __init__(self, Scale=(None, None), Size=(None, None)):
self.TKOut = None
super().__init__(OUTPUT, Scale, Size)
def __del__(self):
try:
self.TKOut.__del__()
except:
pass
super().__del__()
# ---------------------------------------------------------------------- #
# Button Class #
# ---------------------------------------------------------------------- #
class Button(Element):
def __init__(self, ButtonType=CLOSES_WIN, Target=(None, None), ButtonText='', FileTypes=(("ALL Files", "*.*"),), Scale=(None, None), Size=(None, None), AutoSizeText=None, ButtonColor=None, Font=None):
self.BType = ButtonType
self.FileTypes = FileTypes
self.TKButton = None
self.Target = Target
self.ButtonText = ButtonText
self.ButtonColor = ButtonColor if ButtonColor else DEFAULT_BUTTON_COLOR
self.UserData = None
super().__init__(BUTTON, Scale, Size, AutoSizeText, Font=Font)
return
# ------- Button Callback ------- #
def ButtonCallBack(self):
global _my_windows
# Buttons modify targets or return from the form
# If modifying target, get the element object at the target and modify its StrVar
target = self.Target
if target[0] == ThisRow:
target = [self.Position[0], target[1]]
if target[1] < 0:
target[1] = self.Position[1] + target[1]
strvar = None
if target[0] != None:
if target[0] < 0:
target = [self.Position[0] + target[0], target[1]]
target_element = self.ParentForm.GetElementAtLocation(target)
try:
strvar = target_element.TKStringVar
except: pass
else:
strvar = None
filetypes = [] if self.FileTypes is None else self.FileTypes
if self.BType == BROWSE_FOLDER:
folder_name = tk.filedialog.askdirectory() # show the 'get folder' dialog box
try:
strvar.set(folder_name)
except: pass
elif self.BType == BROWSE_FILE:
file_name = tk.filedialog.askopenfilename(filetypes=filetypes) # show the 'get file' dialog box
strvar.set(file_name)
elif self.BType == CLOSES_WIN: # this is a return type button so GET RESULTS and destroy window
# 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
# if the form is tabbed, must collect all form's results and destroy all forms
if self.ParentForm.IsTabbedForm:
self.ParentForm.UberParent.Close()
else:
self.ParentForm.Close()
self.ParentForm.TKroot.quit()
if self.ParentForm.NonBlocking:
self.ParentForm.TKroot.destroy()
_my_windows.NumOpenWindows -= 1 * (_my_windows.NumOpenWindows != 0) # decrement if not 0
elif self.BType == 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.TKroot.quit() # kick the users out of the mainloop
return
def ReturnKeyHandler(self, event):
MyForm = self.ParentForm
# search through this form and find the first button that will exit the form
for row in MyForm.Rows:
for element in row.Elements:
if element.Type == BUTTON:
if element.BType == CLOSES_WIN or element.BType == READ_FORM:
element.ButtonCallBack()
return
def __del__(self):
try:
self.TKButton.__del__()
except:
pass
super().__del__()
# ---------------------------------------------------------------------- #
# ProgreessBar #
# ---------------------------------------------------------------------- #
class ProgressBar(Element):
def __init__(self, MaxValue, Orientation=None, Target=(None,None), Scale=(None, None), Size=(None, None), AutoSizeText=None, BarColor=(None,None), Style=None, BorderWidth=None, Relief=None):
self.MaxValue = MaxValue
self.TKProgressBar = None
self.Cancelled = False
self.NotRunning = True
self.Orientation = Orientation if Orientation else DEFAULT_METER_ORIENTATION
self.BarColor = BarColor
self.BarStyle = Style if Style else DEFAULT_PROGRESS_BAR_STYLE
self.Target = Target
self.BorderWidth = BorderWidth if BorderWidth else DEFAULT_PROGRESS_BAR_BORDER_WIDTH
self.Relief = Relief if Relief else DEFAULT_PROGRESS_BAR_RELIEF
self.BarExpired = False
super().__init__(PROGRESS_BAR, Scale, Size, AutoSizeText)
return
def UpdateBar(self, CurrentCount):
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)
# update the progress bar counter
# self.TKProgressBar['value'] = self.CurrentValue
self.TKProgressBar.Update(CurrentCount)
try:
self.ParentForm.TKroot.update()
except:
_my_windows.NumOpenWindows -= 1 * (_my_windows.NumOpenWindows != 0) # decrement if not 0
return False
return True
def __del__(self):
try:
self.TKProgressBar.__del__()
except:
pass
super().__del__()
# ------------------------------------------------------------------------- #
# Row CLASS #
# ------------------------------------------------------------------------- #
class Row():
def __init__(self, AutoSizeText = None):
self.AutoSizeText = AutoSizeText # Setting to override the form's policy on autosizing.
self.Elements = [] # List of Elements in this Rrow
return
# ------------------------- AddElement ------------------------- #
def AddElement(self, element):
self.Elements.append(element)
return
# ------------------------- Print ------------------------- #
def __str__(self):
outstr = ''
for i, element in enumerate(self.Elements):
outstr += 'Element #%i = %s'%(i,element)
# outstr += f'Element #{i} = {element}'
return outstr
# ------------------------------------------------------------------------- #
# FlexForm CLASS #
# ------------------------------------------------------------------------- #
class FlexForm:
'''
Display a user defined for and return the filled in data
'''
def __init__(self, title, DefaultElementSize=(DEFAULT_ELEMENT_SIZE[0], DEFAULT_ELEMENT_SIZE[1]), AutoSizeText=DEFAULT_AUTOSIZE_TEXT, Scale=(None, None),Size=(None, None), Location=(None, None), ButtonColor=None, Font=None, ProgressBarColor=(None,None), IsTabbedForm=False,BorderDepth=None, AutoClose=False, AutoCloseDuration=DEFAULT_AUTOCLOSE_TIME, Icon=DEFAULT_WINDOW_ICON):
self.AutoSizeText = AutoSizeText
self.Title = title
self.Rows = [] # a list of ELEMENTS for this row
self.DefaultElementSize = DefaultElementSize
self.Size = Size
self.Scale = Scale
self.Location = Location
self.ButtonColor = ButtonColor if ButtonColor else DEFAULT_BUTTON_COLOR
self.IsTabbedForm = IsTabbedForm
self.ParentWindow = None
self.Font = Font if Font else DEFAULT_FONT
self.RadioDict = {}
self.BorderDepth = BorderDepth
self.WindowIcon = Icon if not _my_windows.user_defined_icon else _my_windows.user_defined_icon
self.AutoClose = AutoClose
self.NonBlocking = False
self.TKroot = None
self.TKrootDestroyed = False
self.TKAfterID = None
self.ProgressBarColor = ProgressBarColor
self.AutoCloseDuration = AutoCloseDuration
self.UberParent = None
self.RootNeedsDestroying = False
self.Shown = False
self.ReturnValues = None
# ------------------------- Add ONE Row to Form ------------------------- #
def AddRow(self, *args,AutoSizeText=None):
''' 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 = Row(AutoSizeText) # 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.Elements.append(element)
CurrentRow.AutoSizeText = AutoSizeText
# ------------------------- Append the row to list of Rows ------------------------- #
self.Rows.append(CurrentRow)
# ------------------------- Add Multiple Rows to Form ------------------------- #
def AddRows(self,rows):
for row in rows:
self.AddRow(*row)
def LayoutAndShow(self,rows):
self.AddRows(rows)
self.Show()
return self.ReturnValues
# ------------------------- ShowForm THIS IS IT! ------------------------- #
def Show(self, NonBlocking=False):
self.Shown = True
# Compute num rows & num cols (it'll come in handy debugging)
self.NumRows = len(self.Rows)
self.NumCols = max(len(row.Elements) for row in self.Rows)
self.NonBlocking=NonBlocking
# -=-=-=-=-=-=-=-=- RUN the GUI -=-=-=-=-=-=-=-=- ##
StartupTK(self)
return self.ReturnValues
# ------------------------- SetIcon - set the window's fav icon ------------------------- #
def SetIcon(self, Icon):
self.WindowIcon = Icon
try:
self.TKroot.iconbitmap(Icon)
except: pass
def GetElementAtLocation(self,Location):
(row_num,col_num) = Location
row = self.Rows[row_num]
element = row.Elements[col_num]
return element
def GetDefaultElementSize(self):
return self.DefaultElementSize
def AutoCloseAlarmCallback(self):
try:
if self.UberParent:
window = self.UberParent
else:
window = self
if window:
window.Close()
self.TKroot.quit()
self.RootNeedsDestroying = True
except:
pass
def Read(self):
if self.TKrootDestroyed: return None
if not self.TKrootDestroyed and not self.Shown:
self.Show()
elif not self.TKrootDestroyed:
self.TKroot.mainloop()
if self.RootNeedsDestroying:
self.TKroot.destroy()
return(BuildResults(self))
def OutputFlush(self, Message=''):
if self.TKrootDestroyed: return None
if Message:
print(Message)
try:
self.TKroot.update()
except:
self.TKrootDestroyed = True
return(BuildResults(self))
def Close(self):
try:
self.TKroot.update()
except: pass
results = BuildResults(self)
if self.TKrootDestroyed:
return results
self.TKrootDestroyed = True
self.RootNeedsDestroying = True
return results
def OnClosingCallback(self):
return
def __enter__(self):
return self
def __exit__(self, *a):
self.__del__()
return self
def __del__(self):
for row in self.Rows:
for element in row.Elements:
element.__del__()
try:
del(self.TKroot)
except:
pass
# ------------------------------------------------------------------------- #
# UberForm CLASS #
# Used to make forms into TABS (it's trick) #
# ------------------------------------------------------------------------- #
class UberForm():
FormList = None # list of all the forms in this window
FormReturnValues = None
TKroot = None # tk root for the overall window
TKrootDestroyed = False
def __init__(self):
self.FormList = []
self.FormReturnValues = []
self.TKroot = None
self.TKrootDestroyed = False
def AddForm(self, Form):
self.FormList.append(Form)
def Close(self):
self.FormReturnValues = []
for form in self.FormList:
form.Close()
self.FormReturnValues.append(form.ReturnValues)
if not self.TKrootDestroyed:
self.TKrootDestroyed = True
self.TKroot.destroy()
def __del__(self):
return
# ====================================================================== #
# BUTTON Lazy Functions #
# ====================================================================== #
# ------------------------- INPUT TEXT Element lazy functions ------------------------- #
def In(DefaultText = '', Scale=(None, None), Size=(None, None), AutoSizeText=None):
return InputText(DefaultText=DefaultText, Scale=Scale, Size=Size, AutoSizeText=AutoSizeText)
def Input(DefaultText = '', Scale=(None, None), Size=(None, None), AutoSizeText=None):
return InputText(DefaultText=DefaultText, Scale=Scale, Size=Size, AutoSizeText=AutoSizeText)
# ------------------------- TEXT Element lazy functions ------------------------- #
def Txt(DisplayText, Scale=(None, None), Size=(None, None), AutoSizeText=None, Font=None, TextColor=None):
return Text(DisplayText, Scale=Scale, Size=Size, AutoSizeText=AutoSizeText, Font=Font, TextColor=TextColor)
def T(DisplayText, Scale=(None, None), Size=(None, None), AutoSizeText=None, Font=None, TextColor=None):
return Text(DisplayText, Scale=Scale, Size=Size, AutoSizeText=AutoSizeText, Font=Font, TextColor=TextColor)
# ------------------------- FOLDER BROWSE Element lazy function ------------------------- #
def FolderBrowse(Target=(ThisRow, -1), ButtonText='Browse', Scale=(None, None), Size=(None, None), AutoSizeText=None, ButtonColor=None):
return Button(BROWSE_FOLDER, Target=Target, ButtonText=ButtonText, Scale=Scale, Size=Size, AutoSizeText=AutoSizeText, ButtonColor=ButtonColor)
# ------------------------- FILE BROWSE Element lazy function ------------------------- #
def FileBrowse(Target=(ThisRow, -1), FileTypes=(("ALL Files", "*.*"),),ButtonText='Browse',Scale=(None, None),Size=(None, None), AutoSizeText=None, ButtonColor=None):
return Button(BROWSE_FILE, Target, ButtonText=ButtonText, FileTypes=FileTypes, Scale=Scale, Size=Size, AutoSizeText=AutoSizeText, ButtonColor=ButtonColor)
# ------------------------- SUBMIT BUTTON Element lazy function ------------------------- #
def Submit(ButtonText='Submit', Scale=(None, None), Size=(None, None), AutoSizeText=None, ButtonColor=None):
return Button(CLOSES_WIN, ButtonText=ButtonText, Scale=Scale, Size=Size, AutoSizeText=AutoSizeText, ButtonColor=ButtonColor)
# ------------------------- OK BUTTON Element lazy function ------------------------- #
def OK(ButtonText='OK', Scale=(None, None), Size=(None, None), AutoSizeText=None, ButtonColor=None):
return Button(CLOSES_WIN, ButtonText=ButtonText, Scale=Scale, Size=Size, AutoSizeText=AutoSizeText, ButtonColor=ButtonColor)
# ------------------------- YES BUTTON Element lazy function ------------------------- #
def Ok(ButtonText='Ok', Scale=(None, None), Size=(None, None), AutoSizeText=None, ButtonColor=None):
return Button(CLOSES_WIN, ButtonText=ButtonText, Scale=Scale, Size=Size, AutoSizeText=AutoSizeText, ButtonColor=ButtonColor)
# ------------------------- CANCEL BUTTON Element lazy function ------------------------- #
def Cancel(ButtonText='Cancel', Scale=(None, None), Size=(None, None), AutoSizeText=None, ButtonColor=None, Font=None):
return Button(CLOSES_WIN, ButtonText=ButtonText, Scale=Scale, Size=Size, AutoSizeText=AutoSizeText, ButtonColor=ButtonColor, Font=Font)
# ------------------------- YES BUTTON Element lazy function ------------------------- #
def Yes(ButtonText='Yes', Scale=(None, None), Size=(None, None), AutoSizeText=None, ButtonColor=None):
return Button(CLOSES_WIN, ButtonText=ButtonText, Scale=Scale, Size=Size, AutoSizeText=AutoSizeText, ButtonColor=ButtonColor)
# ------------------------- NO BUTTON Element lazy function ------------------------- #
def No(ButtonText='No', Scale=(None, None), Size=(None, None), AutoSizeText=None, ButtonColor=None):
return Button(CLOSES_WIN, ButtonText=ButtonText, Scale=Scale, Size=Size, AutoSizeText=AutoSizeText, ButtonColor=ButtonColor)
# ------------------------- GENERIC BUTTON Element lazy function ------------------------- #
# this is the only button that REQUIRES button text field
def SimpleButton(ButtonText, Scale=(None, None), Size=(None, None), AutoSizeText=None, ButtonColor=None, Font=None):
return Button(CLOSES_WIN, ButtonText=ButtonText, Scale=Scale, Size=Size, AutoSizeText=AutoSizeText, ButtonColor=ButtonColor, Font=Font)
# ------------------------- GENERIC BUTTON Element lazy function ------------------------- #
# this is the only button that REQUIRES button text field
def ReadFormButton(ButtonText, Scale=(None, None),Size=(None, None), AutoSizeText=None, ButtonColor=None, Font=None):
return Button(READ_FORM, ButtonText=ButtonText, Scale=Scale, Size=Size, AutoSizeText=AutoSizeText, ButtonColor=ButtonColor, Font=Font)
#------------------------------------------------------------------------------------------------------#
# ------- 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.Elements:
if element.Type == TEXT:
r.append(None)
elif element.Type == INPUT_TEXT:
r.append(element.TextInputDefault)
return_vals.append(None)
elif element.Type == INPUT_MULTILINE:
r.append(element.TextInputDefault)
return_vals.append(None)
elif element.Type == BUTTON:
r.append(False)
elif element.Type == PROGRESS_BAR:
r.append(None)
elif element.Type == INPUT_CHECKBOX:
r.append(element.InitialState)
return_vals.append(element.InitialState)
elif element.Type == INPUT_RADIO:
r.append(element.InitialState)
return_vals.append(element.InitialState)
elif element.Type == INPUT_COMBO:
r.append(element.TextInputDefault)
return_vals.append(None)
elif element.Type == INPUT_SPIN:
r.append(element.TextInputDefault)
return_vals.append(None)
results.append(r)
form.Results=results
form.ReturnValues = (None, return_vals)
return
#===== Radio Button RadVar encoding and decoding =====#
#===== The value is simply the row * 1000 + col =====#
def DecodeRadioRowCol(RadValue):
row = RadValue//1000
col = RadValue%1000
return row,col
def EncodeRadioRowCol(Row, Col):
RadValue = Row*1000 + Col
return RadValue
# ------- FUNCTION BuildResults. Form exiting so build the results to pass back ------- #
# format of return values is
# (Button Pressed, input_values)
def BuildResults(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
button_pressed_text = None
input_values = []
for row_num,row in enumerate(form.Rows):
for col_num, element in enumerate(row.Elements):
if element.Type == INPUT_TEXT:
value=element.TKStringVar.get()
results[row_num][col_num] = value
input_values.append(value)
elif element.Type == INPUT_CHECKBOX:
value=element.TKIntVar.get()
results[row_num][col_num] = value
input_values.append(value != 0)
elif element.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)
elif element.Type == BUTTON:
if results[row_num][col_num] is True:
button_pressed_text = element.ButtonText
results[row_num][col_num] = False
elif element.Type == INPUT_COMBO:
value=element.TKStringVar.get()
results[row_num][col_num] = value
input_values.append(value)
elif element.Type == INPUT_SPIN:
try:
value=element.TKStringVar.get()
except:
value = 0
results[row_num][col_num] = value
input_values.append(value)
elif element.Type == INPUT_MULTILINE:
try:
value=element.TKText.get(1.0, tk.END)
element.TKText.delete('1.0', tk.END)
except:
value = None
results[row_num][col_num] = value
input_values.append(value)
return_value = (button_pressed_text,input_values)
form.ReturnValues = return_value
form.ResultsBuilt = True
return return_value
# ------------------------------------------------------------------------------------------------------------------ #
# ===================================== TK CODE STARTS HERE ====================================================== #
# ------------------------------------------------------------------------------------------------------------------ #
def ConvertFlexToTK(MyFlexForm):
master = MyFlexForm.TKroot
# only set title on non-tabbed forms
if not MyFlexForm.IsTabbedForm:
master.title(MyFlexForm.Title)
font = MyFlexForm.Font
InitializeResults(MyFlexForm)
border_depth = MyFlexForm.BorderDepth if MyFlexForm.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(MyFlexForm.Rows):
######################### LOOP THROUGH ELEMENTS ON ROW #########################
# *********** ------- Loop through ELEMENTS ------- ***********#
# *********** Make TK Row ***********#
tk_row_frame = tk.Frame(master)
for col_num, element in enumerate(flex_row.Elements):
element.ParentForm = MyFlexForm # save the button's parent form object
if MyFlexForm.Font and (element.Font == DEFAULT_FONT or not element.Font):
font = MyFlexForm.Font
elif element.Font is not None:
font = element.Font
# ------- Determine Auto-Size setting on a cascading basis ------- #
if element.AutoSizeText is not None: # if element overide
auto_size_text = element.AutoSizeText
elif flex_row.AutoSizeText is not None: # if Row override
auto_size_text = flex_row.AutoSizeText
elif MyFlexForm.AutoSizeText is not None: # if form override
auto_size_text = MyFlexForm.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 = MyFlexForm.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 MyFlexForm.Scale != (None, None):
element_size = (int(element_size[0] * MyFlexForm.Scale[0]), int(element_size[1] * MyFlexForm.Scale[1]))
# ------------------------- TEXT element ------------------------- #
element_type = element.Type
if element_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
tktext_label = tk.Label(tk_row_frame,anchor=tk.NW, textvariable=stringvar, width=width, height=height, justify=tk.LEFT, bd=border_depth, fg=element.TextColor)
# 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=tk.NW, font=font, wraplen=wraplen*2 ) # set wrap to width of widget
tktext_label.pack(side=tk.LEFT)
# ------------------------- BUTTON element ------------------------- #
elif element_type == BUTTON:
element.Location = (row_num, col_num)
btext = element.ButtonText
btype = element.BType
if auto_size_text 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 MyFlexForm.ButtonColor != (None, None) and MyFlexForm.ButtonColor != DEFAULT_BUTTON_COLOR:
bc = MyFlexForm.ButtonColor
else:
bc = DEFAULT_BUTTON_COLOR
if bc == 'Random' or bc == 'random':
bc = GetRandomColorPair()
tkbutton = tk.Button(tk_row_frame, text=btext, width=width, height=height,command=element.ButtonCallBack, justify=tk.LEFT, foreground=bc[0], background=bc[1], bd=border_depth)
element.TKButton = tkbutton # not used yet but save the TK button in case
wraplen = tkbutton.winfo_reqwidth() # width of widget in Pixels
tkbutton.configure(wraplength=wraplen, font=font) # set wrap to width of widget
tkbutton.pack(side=tk.LEFT, padx=element.Pad[0], pady=element.Pad[1])
if not focus_set and btype == CLOSES_WIN:
focus_set = True
element.TKButton.bind('<Return>', element.ReturnKeyHandler)
element.TKButton.focus_set()
MyFlexForm.TKroot.focus_force()
# ------------------------- INPUT (Single Line) element ------------------------- #
elif element_type == INPUT_TEXT:
default_text = element.DefaultText
element.TKStringVar = tk.StringVar()
element.TKStringVar.set(default_text)
element.TKEntry = tk.Entry(tk_row_frame, width=element_size[0], textvariable=element.TKStringVar, bd=border_depth, font=font)
element.TKEntry.bind('<Return>', element.ReturnKeyHandler)
element.TKEntry.pack(side=tk.LEFT,padx=element.Pad[0], pady=element.Pad[1])
if not focus_set:
focus_set = True
element.TKEntry.focus_set()
# ------------------------- COMBO BOX (Drop Down) element ------------------------- #
elif element_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()
element.TKCombo = ttk.Combobox(tk_row_frame, width=width, textvariable=element.TKStringVar,font=font )
element.TKCombo['values'] = element.Values
element.TKCombo.pack(side=tk.LEFT,padx=element.Pad[0], pady=element.Pad[1])
element.TKCombo.current(0)
# ------------------------- INPUT MULTI LINE element ------------------------- #
elif element_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
element.TKText.pack(side=tk.LEFT,padx=element.Pad[0], pady=element.Pad[1])
if element.EnterSubmits:
element.TKText.bind('<Return>', element.ReturnKeyHandler)
if not focus_set:
focus_set = True
element.TKText.focus_set()
# ------------------------- INPUT CHECKBOX element ------------------------- #
elif element_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)
element.TKCheckbutton = tk.Checkbutton(tk_row_frame, anchor=tk.NW, text=element.Text, width=width, variable=element.TKIntVar, bd=border_depth, font=font)
element.TKCheckbutton.pack(side=tk.LEFT,padx=element.Pad[0], pady=element.Pad[1])
# ------------------------- PROGRESS BAR element ------------------------- #
elif element_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 == 'Random' or element.BarColor == 'random':
bar_color = GetRandomColorPair()
elif 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, Borderwidth=element.BorderWidth, Relief=element.Relief)
s = ttk.Style()
element.TKProgressBar.TKCanvas.pack(side=tk.LEFT, padx=element.Pad[0], pady=element.Pad[1])
# ------------------------- INPUT RADIO BUTTON element ------------------------- #
elif element_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 MyFlexForm.RadioDict:
RadVar = MyFlexForm.RadioDict[ID]
else:
RadVar = tk.IntVar()
MyFlexForm.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)
element.TKRadio.pack(side=tk.LEFT, padx=element.Pad[0], pady=element.Pad[1])
# ------------------------- INPUT SPIN Box element ------------------------- #
elif element_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
element.TKSpinBox.pack(side=tk.LEFT, padx=element.Pad[0], pady=element.Pad[1])
# ------------------------- OUTPUT element ------------------------- #
elif element_type == OUTPUT:
width, height = element_size
element.TKOut = TKOutput(tk_row_frame, width=width, height=height, bd=border_depth)
#............................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.W, padx=DEFAULT_MARGINS[0])
if not MyFlexForm.IsTabbedForm:
MyFlexForm.TKroot.configure(padx=DEFAULT_MARGINS[0], pady=DEFAULT_MARGINS[1])
else: MyFlexForm.ParentWindow.configure(padx=DEFAULT_MARGINS[0], pady=DEFAULT_MARGINS[1])
#....................................... DONE creating and laying out window ..........................#
if MyFlexForm.IsTabbedForm:
master = MyFlexForm.ParentWindow
screen_width = master.winfo_screenwidth() # get window info to move to middle of screen
screen_height = master.winfo_screenheight()
if MyFlexForm.Location != (None, None):
loc = MyFlexForm.Location
x,y = MyFlexForm.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.update_idletasks() # don't forget
return
# ----====----====----====----====----==== STARTUP TK ====----====----====----====----====----#
def ShowTabbedForm(Title, *args,AutoClose=False, AutoCloseDuration=DEFAULT_AUTOCLOSE_TIME,FavIcon=DEFAULT_WINDOW_ICON):
global _my_windows
uber = UberForm()
root = tk.Tk()
uber.TKroot = root
if Title is not None:
root.title(Title)
if not len(args):
('******************* SHOW TABBED FORMS ERROR .... no arguments')
return
tab_control = ttk.Notebook(root)
for num,x in enumerate(args):
form, rows, tab_name = x
form.AddRows(rows)
tab = ttk.Frame(tab_control) # Create tab 1
tab_control.add(tab, text=tab_name) # Add tab 1
# tab_control.configure(text='new text')
tab_control.grid(row=0, sticky=tk.W)
form.TKTabControl = tab_control
form.TKroot = tab
form.IsTabbedForm = True
form.ParentWindow = root
ConvertFlexToTK(form)
form.UberParent = uber
uber.AddForm(form)
uber.FormReturnValues.append(form.ReturnValues)
# dangerous?? or clever? use the final form as a callback for autoclose
id = root.after(AutoCloseDuration*1000, form.AutoCloseAlarmCallback) if AutoClose else 0
icon = FavIcon if not _my_windows.user_defined_icon else _my_windows.user_defined_icon
try: uber.TKroot.iconbitmap(icon)
except: pass
root.mainloop()
if id: root.after_cancel(id)
uber.TKrootDestroyed = True
return uber.FormReturnValues
# ----====----====----====----====----==== STARTUP TK ====----====----====----====----====----#
def StartupTK(MyFlexForm):
global _my_windows
ow = _my_windows.NumOpenWindows
root = tk.Tk() if not ow else tk.Toplevel()
_my_windows.NumOpenWindows += 1
MyFlexForm.TKroot = root
# root.protocol("WM_DELETE_WINDOW", MyFlexForm.DestroyedCallback())
# root.bind('<Destroy>', MyFlexForm.DestroyedCallback())
ConvertFlexToTK(MyFlexForm)
MyFlexForm.SetIcon(MyFlexForm.WindowIcon)
if MyFlexForm.AutoClose:
duration = DEFAULT_AUTOCLOSE_TIME if MyFlexForm.AutoCloseDuration is None else MyFlexForm.AutoCloseDuration
MyFlexForm.TKAfterID = root.after(duration*1000, MyFlexForm.AutoCloseAlarmCallback)
if MyFlexForm.NonBlocking:
MyFlexForm.TKroot.protocol("WM_WINDOW_DESTROYED", MyFlexForm.OnClosingCallback())
pass
else: # it's a blocking form
MyFlexForm.TKroot.mainloop()
_my_windows.NumOpenWindows -= 1 * (_my_windows.NumOpenWindows != 0) # decrement if not 0
if MyFlexForm.RootNeedsDestroying:
MyFlexForm.TKroot.destroy()
MyFlexForm.RootNeedsDestroying = False
return
# ==============================_GetNumLinesNeeded ==#
# Helper function for determining how to wrap text #
# ===================================================#
def _GetNumLinesNeeded(text, max_line_width):
if max_line_width == 0:
return 1
lines = text.split('\n')
num_lines = len(lines) # number of original lines of text
max_line_len = max([len(l) for l in lines]) # longest line
lines_used = []
for L in lines:
lines_used.append(len(L)//max_line_width + (len(L) % max_line_width > 0)) # fancy math to round up
total_lines_needed = sum(lines_used)
return total_lines_needed
# ------------------------------------------------------------------------------------------------------------------ #
# ===================================== Upper PySimpleGUI ============================================================== #
# Pre-built dialog boxes for all your needs #
# ------------------------------------------------------------------------------------------------------------------ #
# ==================================== MSG BOX =====#
# Display a message wrapping at 60 characters #
# Exits via an OK button2 press #
# Returns nothing #
# ===================================================#
def MsgBox(*args, ButtonColor=None, ButtonType=MSG_BOX_OK, AutoClose=False, AutoCloseDuration=None, Icon=DEFAULT_WINDOW_ICON, LineWidth=MESSAGE_BOX_LINE_WIDTH, Font=None):
'''
Show message box. Displays one line per user supplied argument. Takes any Type of variable to display.
:param args:
:param ButtonColor:
:param ButtonType:
:param AutoClose:
:param AutoCloseDuration:
:param Icon:
:param LineWidth:
:param Font:
:return:
'''
if not args:
args_to_print = ['']
else:
args_to_print = args
with FlexForm(args_to_print[0], AutoSizeText=True, ButtonColor=ButtonColor, AutoClose=AutoClose, AutoCloseDuration=AutoCloseDuration, 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 :-)
# if not isinstance(message, str): message = str(message)
message = str(message)
if message.count('\n'):
message_wrapped = message
else:
message_wrapped = textwrap.fill(message, LineWidth)
message_wrapped_lines = message_wrapped.count('\n')+1
longest_line_len = max([len(l) for l in message.split('\n')])
width_used = min(longest_line_len, LineWidth)
max_line_total = max(max_line_total, width_used)
# height = _GetNumLinesNeeded(message, width_used)
height = message_wrapped_lines
form.AddRow(Text(message_wrapped, AutoSizeText=True))
total_lines += height
pad = max_line_total-15 if max_line_total > 15 else 1
pad =1
# show either an OK or Yes/No depending on paramater
if ButtonType is MSG_BOX_YES_NO:
form.AddRow(Text('', Size=(pad,1), AutoSizeText=False), Yes(ButtonColor=ButtonColor), No(ButtonColor=ButtonColor))
(button_text, values) = form.Show()
return button_text == 'Yes'
elif ButtonType is MSG_BOX_CANCELLED:
form.AddRow(Text('', Size=(pad,1), AutoSizeText=False), SimpleButton('Cancelled', ButtonColor=ButtonColor))
elif ButtonType is MSG_BOX_ERROR:
form.AddRow(Text('', Size=(pad,1), AutoSizeText=False), SimpleButton('ERROR', Size=(5,1), ButtonColor=ButtonColor))
elif ButtonType is MSG_BOX_OK_CANCEL:
form.AddRow(Text('', Size=(pad,1), AutoSizeText=False), SimpleButton('OK', Size=(5,1), ButtonColor=ButtonColor),
SimpleButton('Cancel', Size=(5, 1), ButtonColor=ButtonColor))
else:
form.AddRow(Text('', Size=(pad,1), AutoSizeText=False), SimpleButton('OK', Size=(5,1), ButtonColor=ButtonColor))
button, values = form.Show()
return button
# ============================== MsgBoxAutoClose====#
# Lazy function. Same as calling MsgBox with parms #
# ===================================================#
def MsgBoxAutoClose(*args, ButtonColor=None,AutoClose=True, AutoCloseDuration=DEFAULT_AUTOCLOSE_TIME, Font=None):
'''
Display a standard MsgBox that will automatically close after a specified amount of time
:param args:
:param ButtonColor:
:param AutoClose:
:param AutoCloseDuration:
:param Font:
:return:
'''
MsgBox(*args, ButtonColor=ButtonColor, AutoClose=AutoClose, AutoCloseDuration=AutoCloseDuration, Font=Font)
return
# ============================== MsgBoxError =====#
# Like MsgBox but presents RED BUTTONS #
# ===================================================#
def MsgBoxError(*args, ButtonColor=DEFAULT_ERROR_BUTTON_COLOR,AutoClose=False, AutoCloseDuration=None, Font=None):
'''
Display a MsgBox with a red button
:param args:
:param ButtonColor:
:param AutoClose:
:param AutoCloseDuration:
:param Font:
:return:
'''
MsgBox(*args, ButtonColor=ButtonColor, AutoClose=AutoClose, AutoCloseDuration=AutoCloseDuration, Font=Font)
return
# ============================== MsgBoxCancel =====#
# #
# ===================================================#
def MsgBoxCancel(*args,ButtonColor=DEFAULT_CANCEL_BUTTON_COLOR,AutoClose=False, AutoCloseDuration=None, Font=None):
'''
Display a MsgBox with a single "Cancel" button.
:param args:
:param ButtonColor:
:param AutoClose:
:param AutoCloseDuration:
:param Font:
:return:
'''
MsgBox(*args, ButtonType=MSG_BOX_CANCELLED, ButtonColor=ButtonColor, AutoClose=AutoClose, AutoCloseDuration=AutoCloseDuration, Font=Font)
return
# ============================== MsgBoxOK =====#
# Like MsgBox but only 1 button #
# ===================================================#
def MsgBoxOK(*args,ButtonColor=('white', 'black'),AutoClose=False, AutoCloseDuration=None, Font=None):
'''
Display a MsgBox with a single buttoned labelled "OK"
:param args:
:param ButtonColor:
:param AutoClose:
:param AutoCloseDuration:
:param Font:
:return:
'''
MsgBox(*args, ButtonType=MSG_BOX_OK, ButtonColor=ButtonColor, AutoClose=AutoClose, AutoCloseDuration=AutoCloseDuration, Font=Font)
return
# ============================== MsgBoxOKCancel ====#
# Like MsgBox but presents OK and Cancel buttons #
# ===================================================#
def MsgBoxOKCancel(*args,ButtonColor=None,AutoClose=False, AutoCloseDuration=None, Font=None):
'''
Display MsgBox with 2 buttons, "OK" and "Cancel"
:param args:
:param ButtonColor:
:param AutoClose:
:param AutoCloseDuration:
:param Font:
:return:
'''
result = MsgBox(*args, ButtonType=MSG_BOX_OK_CANCEL, ButtonColor=ButtonColor, AutoClose=AutoClose, AutoCloseDuration=AutoCloseDuration, Font=Font)
return result
# ==================================== YesNoBox=====#
# Like MsgBox but presents Yes and No buttons #
# Returns True if Yes was pressed else False #
# ===================================================#
def MsgBoxYesNo(*args,ButtonColor=None,AutoClose=False, AutoCloseDuration=None, Font=None):
'''
Display MsgBox with 2 buttons, "Yes" and "No"
:param args:
:param ButtonColor:
:param AutoClose:
:param AutoCloseDuration:
:param Font:
:return:
'''
result = MsgBox(*args,ButtonType=MSG_BOX_YES_NO, ButtonColor=ButtonColor, AutoClose=AutoClose, AutoCloseDuration=AutoCloseDuration, Font=Font)
return result
# ============================== PROGRESS METER ========================================== #
def ConvertArgsToSingleString(*args):
max_line_total, width_used , total_lines, = 0,0,0
single_line_message = ''
# loop through args and built a SINGLE string from them
for message in args:
# fancy code to check if string and convert if not is not need. Just always convert to string :-)
# if not isinstance(message, str): message = str(message)
message = str(message)
longest_line_len = max([len(l) for l in message.split('\n')])
width_used = min(longest_line_len, MESSAGE_BOX_LINE_WIDTH)
max_line_total = max(max_line_total, width_used)
lines_needed = _GetNumLinesNeeded(message, width_used)
total_lines += lines_needed
single_line_message += message + '\n'
return single_line_message, width_used, total_lines
# ============================== ProgressMeter =====#
# ===================================================#
def ProgressMeter(Title, MaxValue, *args, Orientation=None, BarColor=DEFAULT_PROGRESS_BAR_COLOR, ButtonColor=None,Size=DEFAULT_PROGRESS_BAR_SIZE, Scale=(None, None), BorderWidth=DEFAULT_PROGRESS_BAR_BORDER_WIDTH):
'''
Create and show a form on tbe caller's behalf.
:param Title:
:param MaxValue:
:param args: ANY number of arguments the caller wants to display
:param Orientation:
:param BarColor:
:param Size:
:param Scale:
:param Style:
:param StyleOffset:
:return: ProgressBar object that is in the form
'''
orientation = DEFAULT_METER_ORIENTATION if Orientation is None else Orientation
target = (0,0) if orientation[0].lower() == 'h' else (0,1)
bar2 = ProgressBar(MaxValue, Orientation=orientation, Size=Size, BarColor=BarColor, Scale=Scale, Target=target, BorderWidth=BorderWidth)
form = FlexForm(Title, AutoSizeText=True)
# Form using a horizontal bar
if orientation[0].lower() == 'h':
single_line_message, width, height = ConvertArgsToSingleString(*args)
bar2.TextToDisplay = single_line_message
bar2.MaxValue = MaxValue
bar2.CurrentValue = 0
form.AddRow(Text(single_line_message,Size=(width+20, height+3), AutoSizeText=True))
form.AddRow((bar2))
form.AddRow((Cancel(ButtonColor=ButtonColor)))
else:
single_line_message, width, height = ConvertArgsToSingleString(*args)
bar2.TextToDisplay = single_line_message
bar2.MaxValue = MaxValue
bar2.CurrentValue = 0
form.AddRow(bar2, Text(single_line_message,Size=(width+20, height+3), AutoSizeText=True))
form.AddRow((Cancel(ButtonColor=ButtonColor)))
form.NonBlocking = True
form.Show(NonBlocking = True)
return bar2
# ============================== ProgressMeterUpdate =====#
def ProgressMeterUpdate(bar, Value, *args):
'''
Update the progress meter for a form
:param form: class ProgressBar
:param Value: int
:return: True if not cancelled, OK....False if Error
'''
global _my_windows
if bar == None: return False
if bar.BarExpired: return False
message, w, h = ConvertArgsToSingleString(*args)
bar.TextToDisplay = message
bar.CurrentValue = Value
rc = bar.UpdateBar(Value)
if Value >= bar.MaxValue or not rc:
bar.BarExpired = True
bar.ParentForm.Close()
if bar.ParentForm.RootNeedsDestroying:
try:
bar.ParentForm.TKroot.destroy()
_my_windows.NumOpenWindows -= 1 * (_my_windows.NumOpenWindows != 0) # decrement if not 0
except: pass
bar.ParentForm.RootNeedsDestroying = False
bar.ParentForm.__del__()
return False
return rc
# ============================== EASY PROGRESS METER ========================================== #
# class to hold the easy meter info (a global variable essentialy)
class EasyProgressMeterDataClass():
def __init__(self, Title='', CurrentValue=1, MaxValue=10, StartTime=None, StatMessages=()):
self.Title = Title
self.CurrentValue = CurrentValue
self.MaxValue = MaxValue
self.StartTime = StartTime
self.StatMessages = StatMessages
self.ParentForm = None
self.MeterID = None
# =========================== COMPUTE PROGRESS STATS ======================#
def ComputeProgressStats(self):
utc = datetime.datetime.utcnow()
time_delta = utc - self.StartTime
total_seconds = time_delta.total_seconds()
if not total_seconds:
total_seconds = 1
try:
time_per_item = total_seconds / self.CurrentValue
except:
time_per_item = 1
seconds_remaining = (self.MaxValue - self.CurrentValue) * time_per_item
time_remaining = str(datetime.timedelta(seconds=seconds_remaining))
time_remaining_short = (time_remaining).split(".")[0]
time_delta_short = str(time_delta).split(".")[0]
total_time = time_delta + datetime.timedelta(seconds=seconds_remaining)
total_time_short = str(total_time).split(".")[0]
self.StatMessages = [
'{} of {}'.format(self.CurrentValue, self.MaxValue),
'{} %'.format(100*self.CurrentValue//self.MaxValue),
'',
' {:6.2f} Iterations per Second'.format(self.CurrentValue/total_seconds),
' {:6.2f} Seconds per Iteration'.format(total_seconds/(self.CurrentValue if self.CurrentValue else 1)),
'',
'{} Elapsed Time'.format(time_delta_short),
'{} Time Remaining'.format(time_remaining_short),
'{} Estimated Total Time'.format(total_time_short)]
return
# ============================== EasyProgressMeter =====#
def EasyProgressMeter(Title, CurrentValue, MaxValue,*args, Orientation=None, BarColor=DEFAULT_PROGRESS_BAR_COLOR, ButtonColor=None, Size=DEFAULT_PROGRESS_BAR_SIZE, Scale=(None, None),BorderWidth=DEFAULT_PROGRESS_BAR_BORDER_WIDTH):
'''
A ONE-LINE progress meter. Add to your code where ever you need a meter. No need for a second
function call before your loop. You've got enough code to write!
:param Title: Title will be shown on the window
:param CurrentValue: Current count of your items
:param MaxValue: Max value your count will ever reach. This indicates it should be closed
:param args: VARIABLE number of arguements... you request it, we'll print it no matter what the item!
:param Orientation:
:param BarColor:
:param Size:
:param Scale:
:param Style:
:param StyleOffset:
:return: False if should stop the meter
'''
# STATIC VARIABLE!
# This is a very clever form of static variable using a function attribute
# If the variable doesn't yet exist, then it will create it and initialize with the 3rd parameter
EasyProgressMeter.EasyProgressMeterData = getattr(EasyProgressMeter, 'EasyProgressMeterData', EasyProgressMeterDataClass())
# if no meter currently running
if EasyProgressMeter.EasyProgressMeterData.MeterID is None: # Starting a new meter
if int(CurrentValue) >= int(MaxValue):
return False
del(EasyProgressMeter.EasyProgressMeterData)
EasyProgressMeter.EasyProgressMeterData = EasyProgressMeterDataClass(Title, 1, int(MaxValue), datetime.datetime.utcnow(), [])
EasyProgressMeter.EasyProgressMeterData.ComputeProgressStats()
message = "\n".join([line for line in EasyProgressMeter.EasyProgressMeterData.StatMessages])
EasyProgressMeter.EasyProgressMeterData.MeterID = ProgressMeter(Title, int(MaxValue), message, *args, Orientation=Orientation, BarColor=BarColor, Size=Size, Scale=Scale, ButtonColor=ButtonColor,BorderWidth=BorderWidth)
EasyProgressMeter.EasyProgressMeterData.ParentForm = EasyProgressMeter.EasyProgressMeterData.MeterID.ParentForm
return True
# if exactly the same values as before, then ignore.
if EasyProgressMeter.EasyProgressMeterData.MaxValue == MaxValue and EasyProgressMeter.EasyProgressMeterData.CurrentValue == CurrentValue:
return True
if EasyProgressMeter.EasyProgressMeterData.MaxValue != int(MaxValue):
EasyProgressMeter.EasyProgressMeterData.MeterID = None
EasyProgressMeter.EasyProgressMeterData.ParentForm = None
del(EasyProgressMeter.EasyProgressMeterData)
EasyProgressMeter.EasyProgressMeterData = EasyProgressMeterDataClass() # setup a new progress meter
return True # HAVE to return TRUE or else the new meter will thing IT is failing when it hasn't
EasyProgressMeter.EasyProgressMeterData.CurrentValue = int(CurrentValue)
EasyProgressMeter.EasyProgressMeterData.MaxValue = int(MaxValue)
EasyProgressMeter.EasyProgressMeterData.ComputeProgressStats()
message = ''
for line in EasyProgressMeter.EasyProgressMeterData.StatMessages:
message = message + str(line) + '\n'
message = "\n".join(EasyProgressMeter.EasyProgressMeterData.StatMessages)
rc = ProgressMeterUpdate(EasyProgressMeter.EasyProgressMeterData.MeterID, CurrentValue,*args, message )
# if counter >= max then the progress meter is all done. Indicate none running
if CurrentValue >= EasyProgressMeter.EasyProgressMeterData.MaxValue or not rc:
EasyProgressMeter.EasyProgressMeterData.MeterID = None
del(EasyProgressMeter.EasyProgressMeterData)
EasyProgressMeter.EasyProgressMeterData = EasyProgressMeterDataClass() # setup a new progress meter
return False # even though at the end, return True so don't cause error with the app
return rc # return whatever the update told us
def EasyProgressMeterCancel(Title, *args):
EasyProgressMeter.EasyProgressMeterData = getattr(EasyProgressMeter, 'EasyProgressMeterData', EasyProgressMeterDataClass())
if EasyProgressMeter.EasyProgressMeterData.MeterID is not None:
# tell the normal meter update that we're at max value which will close the meter
rc = EasyProgressMeter(Title, EasyProgressMeter.EasyProgressMeterData.MaxValue, EasyProgressMeter.EasyProgressMeterData.MaxValue, ' *** CANCELLING ***', 'Caller requested a cancel', *args)
return rc
return True
def GetRandomColor():
nums = randint(0,255), randint(0,255), randint(0,255)
color_code ='#' + ''.join('{:02X}'.format(a) for a in nums)
return color_code
def GetRandomColorPair():
fg = GetRandomColor()
bg = GetComplimentaryHex(fg)
color_code = (fg, bg)
return color_code
# input is #RRGGBB
# output is #RRGGBB
def GetComplimentaryHex(color):
# strip the # from the beginning
color = color[1:]
# convert the string into hex
color = int(color, 16)
# invert the three bytes
# as good as substracting each of RGB component by 255(FF)
comp_color = 0xFFFFFF ^ color
# convert the color back to hex by prefixing a #
comp_color = "#%06X" % comp_color
# return the result
return comp_color
# ======================== Scrolled Text Box =====#
# ===================================================#
def ScrolledTextBox(*args, ButtonColor=None, YesNo=False, AutoClose=False, AutoCloseDuration=None, Height=None):
if not args: return
with FlexForm(args[0], AutoSizeText=True, ButtonColor=ButtonColor, AutoClose=AutoClose, AutoCloseDuration=AutoCloseDuration) as form:
max_line_total, max_line_width, total_lines, height = 0,0,0,0
complete_output = ''
for message in args:
# fancy code to check if string and convert if not is not need. Just always convert to string :-)
# if not isinstance(message, str): message = str(message)
message = str(message)
longest_line_len = max([len(l) for l in message.split('\n')])
width_used = min(longest_line_len, MESSAGE_BOX_LINE_WIDTH)
max_line_total = max(max_line_total, width_used)
max_line_width = MESSAGE_BOX_LINE_WIDTH
lines_needed = _GetNumLinesNeeded(message, width_used)
height += lines_needed
complete_output += message + '\n'
total_lines += lines_needed
height = MAX_SCROLLED_TEXT_BOX_HEIGHT if height > MAX_SCROLLED_TEXT_BOX_HEIGHT else height
if Height:
height = Height
form.AddRow(Multiline(complete_output, Size=(max_line_width, height)), AutoSizeText=True)
pad = max_line_total-15 if max_line_total > 15 else 1
# show either an OK or Yes/No depending on paramater
if YesNo:
form.AddRow(Text('', Size=(pad,1), AutoSizeText=False), Yes(), No())
(button_text, values) = form.Show()
return button_text == 'Yes'
else:
form.AddRow(Text('', Size=(pad,1), AutoSizeText=False), SimpleButton('OK', Size=(5,1), ButtonColor=ButtonColor))
form.Show()
# ---------------------------------------------------------------------- #
# GetPathBox #
# Pre-made dialog that looks like this roughly #
# MESSAGE #
# __________________________ #
# |__________________________| (BROWSE) #
# (SUBMIT) (CANCEL) #
# RETURNS two values: #
# True/False, path #
# (True if Submit was pressed, false otherwise) #
# ---------------------------------------------------------------------- #
def GetPathBox(Title, Message, DefaultPath='', ButtonColor=None, Size=(None,None)):
with FlexForm(Title, AutoSizeText=True, ButtonColor=ButtonColor) as form:
layout = [[Text(Message,AutoSizeText=True)],
[InputText(DefaultText=DefaultPath, Size=Size), FolderBrowse()],
[Submit(), Cancel()]]
(button, input_values) = form.LayoutAndShow(layout)
if button != 'Submit':
return False,None
else:
path = input_values[0]
return True, path
# ============================== GetFileBox =========#
# Like the Get folder box but for files #
# ===================================================#
def GetFileBox(Title, Message, DefaultPath='',FileTypes=(("ALL Files", "*.*"),), ButtonColor=None, Size=(None,None)):
with FlexForm(Title, AutoSizeText=True, ButtonColor=ButtonColor) as form:
layout = [[Text(Message,AutoSizeText=True)],
[InputText(DefaultText=DefaultPath, Size=Size), FileBrowse(FileTypes=FileTypes)],
[Submit(), Cancel()]]
(button, input_values) = form.LayoutAndShow(layout)
if button != 'Submit':
return False,None
else:
path = input_values[0]
return True, path
# ============================== GetTextBox =========#
# Get a single line of text #
# ===================================================#
def GetTextBox(Title, Message, Default='', ButtonColor=None, Size=(None, None)):
with FlexForm(Title, AutoSizeText=True, ButtonColor=ButtonColor) as form:
layout = [[Text(Message,AutoSizeText=True)],
[InputText(DefaultText=Default, Size=Size)],
[Submit(), Cancel()]]
(button, input_values) = form.LayoutAndShow(layout)
if button != 'Submit':
return False,None
else:
return True, input_values[0]
# ============================== SetGlobalIcon ======#
# Sets the icon to be used by default #
# ===================================================#
def SetGlobalIcon(Icon):
global _my_windows
try:
with open(Icon, 'r') as icon_file:
pass
except:
raise FileNotFoundError
_my_windows.user_defined_icon = Icon
return True
# ============================== SetGlobalIcon ======#
# Sets the icon to be used by default #
# ===================================================#
def SetButtonColor(foreground, background):
global DEFAULT_BUTTON_COLOR
DEFAULT_BUTTON_COLOR = (foreground, background)
# ============================== sprint ======#
# Is identical to the Scrolled Text Box #
# Provides a crude 'print' mechanism but in a #
# GUI environment #
# ============================================#
sprint=ScrolledTextBox