initiated venv, installed FreeSimpleGUI and PySerial

This commit is contained in:
2025-10-22 17:20:59 +03:00
parent 434f9c0df3
commit f921bec1c3
1510 changed files with 218990 additions and 27193 deletions

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,923 @@
from __future__ import annotations
import copy
import tkinter as tk
import warnings
from tkinter import ttk
import FreeSimpleGUI
from FreeSimpleGUI import BROWSE_FILES_DELIMITER
from FreeSimpleGUI import BUTTON_DISABLED_MEANS_IGNORE
from FreeSimpleGUI import BUTTON_TYPE_BROWSE_FILE
from FreeSimpleGUI import BUTTON_TYPE_BROWSE_FILES
from FreeSimpleGUI import BUTTON_TYPE_BROWSE_FOLDER
from FreeSimpleGUI import BUTTON_TYPE_CALENDAR_CHOOSER
from FreeSimpleGUI import BUTTON_TYPE_CLOSES_WIN
from FreeSimpleGUI import BUTTON_TYPE_CLOSES_WIN_ONLY
from FreeSimpleGUI import BUTTON_TYPE_COLOR_CHOOSER
from FreeSimpleGUI import BUTTON_TYPE_READ_FORM
from FreeSimpleGUI import BUTTON_TYPE_SAVEAS_FILE
from FreeSimpleGUI import COLOR_SYSTEM_DEFAULT
from FreeSimpleGUI import ELEM_TYPE_BUTTON
from FreeSimpleGUI import ELEM_TYPE_BUTTONMENU
from FreeSimpleGUI import FILE_TYPES_ALL_FILES
from FreeSimpleGUI import MENU_SHORTCUT_CHARACTER
from FreeSimpleGUI import running_mac
from FreeSimpleGUI import theme_background_color
from FreeSimpleGUI import theme_button_color
from FreeSimpleGUI import theme_input_background_color
from FreeSimpleGUI import theme_input_text_color
from FreeSimpleGUI import ThisRow
from FreeSimpleGUI.elements.base import Element
from FreeSimpleGUI.elements.helpers import AddMenuItem
from FreeSimpleGUI.elements.helpers import button_color_to_tuple
class Button(Element):
"""
Button Element - Defines all possible buttons. The shortcuts such as Submit, FileBrowse, ... each create a Button
"""
def __init__(
self,
button_text='',
button_type=BUTTON_TYPE_READ_FORM,
target=(None, None),
tooltip=None,
file_types=FILE_TYPES_ALL_FILES,
initial_folder=None,
default_extension='',
disabled=False,
change_submits=False,
enable_events=False,
image_filename=None,
image_data=None,
image_size=(None, None),
image_subsample=None,
image_zoom=None,
image_source=None,
border_width=None,
size=(None, None),
s=(None, None),
auto_size_button=None,
button_color=None,
disabled_button_color=None,
highlight_colors=None,
mouseover_colors=(None, None),
use_ttk_buttons=None,
font=None,
bind_return_key=False,
focus=False,
pad=None,
p=None,
key=None,
k=None,
right_click_menu=None,
expand_x=False,
expand_y=False,
visible=True,
metadata=None,
):
"""
:param button_text: Text to be displayed on the button
:type button_text: (str)
:param button_type: You should NOT be setting this directly. ONLY the shortcut functions set this
:type button_type: (int)
:param target: key or (row,col) target for the button. Note that -1 for column means 1 element to the left of this one. The constant ThisRow is used to indicate the current row. The Button itself is a valid target for some types of button
:type target: str | (int, int)
:param tooltip: text, that will appear when mouse hovers over the element
:type tooltip: (str)
:param file_types: the filetypes that will be used to match files. To indicate all files: (("ALL Files", "*.* *"),).
:type file_types: Tuple[(str, str), ...]
:param initial_folder: starting path for folders and files
:type initial_folder: (str)
:param default_extension: If no extension entered by user, add this to filename (only used in saveas dialogs)
:type default_extension: (str)
:param disabled: If True button will be created disabled. If BUTTON_DISABLED_MEANS_IGNORE then the button will be ignored rather than disabled using tkinter
:type disabled: (bool | str)
:param change_submits: DO NOT USE. Only listed for backwards compat - Use enable_events instead
:type change_submits: (bool)
:param enable_events: Turns on the element specific events. If this button is a target, should it generate an event when filled in
:type enable_events: (bool)
:param image_source: Image to place on button. Use INSTEAD of the image_filename and image_data. Unifies these into 1 easier to use parm
:type image_source: (str | bytes)
:param image_filename: image filename if there is a button image. GIFs and PNGs only.
:type image_filename: (str)
:param image_data: Raw or Base64 representation of the image to put on button. Choose either filename or data
:type image_data: bytes | str
:param image_size: Size of the image in pixels (width, height)
:type image_size: (int, int)
:param image_subsample: amount to reduce the size of the image. Divides the size by this number. 2=1/2, 3=1/3, 4=1/4, etc
:type image_subsample: (int)
:param image_zoom: amount to increase the size of the image. 2=twice size, 3=3 times, etc
:type image_zoom: (int)
:param border_width: width of border around button in pixels
:type border_width: (int)
:param size: (w, h) w=characters-wide, h=rows-high. If an int instead of a tuple is supplied, then height is auto-set to 1
:type size: (int | None, int | None) | (None, None) | int
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int | None, int | None) | (None, None) | int
:param auto_size_button: if True the button size is sized to fit the text
:type auto_size_button: (bool)
:param button_color: Color of button. default is from theme or the window. Easy to remember which is which if you say "ON" between colors. "red" on "green". Normally a tuple, but can be a simplified-button-color-string "foreground on background". Can be a single color if want to set only the background.
:type button_color: (str, str) | str
:param disabled_button_color: colors to use when button is disabled (text, background). Use None for a color if don't want to change. Only ttk buttons support both text and background colors. tk buttons only support changing text color
:type disabled_button_color: (str, str) | str
:param highlight_colors: colors to use when button has focus (has focus, does not have focus). None will use colors based on theme. Only used by Linux and only for non-TTK button
:type highlight_colors: (str, str)
:param mouseover_colors: Important difference between Linux & Windows! Linux - Colors when mouse moved over button. Windows - colors when button is pressed. The default is to switch the text and background colors (an inverse effect)
:type mouseover_colors: (str, str) | str
:param use_ttk_buttons: True = use ttk buttons. False = do not use ttk buttons. None (Default) = use ttk buttons only if on a Mac and not with button images
:type use_ttk_buttons: (bool)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param bind_return_key: If True then pressing the return key in an Input or Multiline Element will cause this button to appear to be clicked (generates event with this button's key
:type bind_return_key: (bool)
:param focus: if True, initial focus will be put on this button
:type focus: (bool)
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param key: Used with window.find_element and with return values to uniquely identify this element to uniquely identify this element
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param right_click_menu: A list of lists of Menu items to show when this element is right clicked. See user docs for exact format.
:type right_click_menu: List[List[ List[str] | str ]]
:param expand_x: If True the element will automatically expand in the X direction to fill available space
:type expand_x: (bool)
:param expand_y: If True the element will automatically expand in the Y direction to fill available space
:type expand_y: (bool)
:param visible: set visibility state of the element
:type visible: (bool)
:param metadata: User metadata that can be set to ANYTHING
:type metadata: (Any)
"""
self.AutoSizeButton = auto_size_button
self.BType = button_type
if file_types is not None and len(file_types) == 2 and isinstance(file_types[0], str) and isinstance(file_types[1], str):
warnings.warn(
'file_types parameter not correctly specified. This parameter is a LIST of TUPLES. You have passed (str,str) rather than ((str, str),). Fixing it for you this time.\nchanging {} to {}\nPlease correct your code'.format(file_types, ((file_types[0], file_types[1]),)),
UserWarning,
)
file_types = ((file_types[0], file_types[1]),)
self.FileTypes = file_types
self.Widget = self.TKButton = None # type: tk.Button
self.Target = target
self.ButtonText = str(button_text)
self.RightClickMenu = right_click_menu
# Button colors can be a tuple (text, background) or a string with format "text on background"
self.ButtonColor = button_color_to_tuple(button_color)
self.DisabledButtonColor = button_color_to_tuple(disabled_button_color) if disabled_button_color is not None else (None, None)
if image_source is not None:
if isinstance(image_source, bytes):
image_data = image_source
elif isinstance(image_source, str):
image_filename = image_source
self.ImageFilename = image_filename
self.ImageData = image_data
self.ImageSize = image_size
self.ImageSubsample = image_subsample
self.zoom = int(image_zoom) if image_zoom is not None else None
self.UserData = None
self.BorderWidth = border_width if border_width is not None else FreeSimpleGUI.DEFAULT_BORDER_WIDTH
self.BindReturnKey = bind_return_key
self.Focus = focus
self.TKCal = None
self.calendar_default_date_M_D_Y = (None, None, None)
self.calendar_close_when_chosen = False
self.calendar_locale = None
self.calendar_format = None
self.calendar_location = (None, None)
self.calendar_no_titlebar = True
self.calendar_begin_at_sunday_plus = 0
self.calendar_month_names = None
self.calendar_day_abbreviations = None
self.calendar_title = ''
self.calendar_selection = ''
self.default_button = None
self.InitialFolder = initial_folder
self.DefaultExtension = default_extension
self.Disabled = disabled
self.ChangeSubmits = change_submits or enable_events
self.UseTtkButtons = use_ttk_buttons
self._files_delimiter = BROWSE_FILES_DELIMITER # used by the file browse button. used when multiple files are selected by user
if use_ttk_buttons is None and running_mac():
self.UseTtkButtons = True
if key is None and k is None:
_key = self.ButtonText
if FreeSimpleGUI.DEFAULT_USE_BUTTON_SHORTCUTS is True:
pos = _key.find(MENU_SHORTCUT_CHARACTER)
if pos != -1:
if pos < len(MENU_SHORTCUT_CHARACTER) or _key[pos - len(MENU_SHORTCUT_CHARACTER)] != '\\':
_key = _key[:pos] + _key[pos + len(MENU_SHORTCUT_CHARACTER) :]
else:
_key = _key.replace('\\' + MENU_SHORTCUT_CHARACTER, MENU_SHORTCUT_CHARACTER)
else:
_key = key if key is not None else k
if highlight_colors is not None:
self.HighlightColors = highlight_colors
else:
self.HighlightColors = self._compute_highlight_colors()
if mouseover_colors != (None, None):
self.MouseOverColors = button_color_to_tuple(mouseover_colors)
elif button_color is not None:
self.MouseOverColors = (self.ButtonColor[1], self.ButtonColor[0])
else:
self.MouseOverColors = (theme_button_color()[1], theme_button_color()[0])
pad = pad if pad is not None else p
self.expand_x = expand_x
self.expand_y = expand_y
sz = size if size != (None, None) else s
super().__init__(ELEM_TYPE_BUTTON, size=sz, font=font, pad=pad, key=_key, tooltip=tooltip, visible=visible, metadata=metadata)
return
def _compute_highlight_colors(self):
"""
Determines the color to use to indicate the button has focus. This setting is only used by Linux.
:return: Pair of colors. (Highlight, Highlight Background)
:rtype: (str, str)
"""
highlight_color = highlight_background = COLOR_SYSTEM_DEFAULT
if self.ButtonColor != COLOR_SYSTEM_DEFAULT and theme_background_color() != COLOR_SYSTEM_DEFAULT:
highlight_background = theme_background_color()
if self.ButtonColor != COLOR_SYSTEM_DEFAULT and self.ButtonColor[0] != COLOR_SYSTEM_DEFAULT:
if self.ButtonColor[0] != theme_background_color():
highlight_color = self.ButtonColor[0]
else:
highlight_color = 'red'
return (highlight_color, highlight_background)
# Realtime button release callback
def ButtonReleaseCallBack(self, parm):
"""
Not a user callable function. Called by tkinter when a "realtime" button is released
:param parm: the event info from tkinter
:type parm:
"""
self.LastButtonClickedWasRealtime = False
self.ParentForm.LastButtonClicked = None
# Realtime button callback
def ButtonPressCallBack(self, parm):
"""
Not a user callable method. Callback called by tkinter when a "realtime" button is pressed
:param parm: Event info passed in by tkinter
:type parm:
"""
self.ParentForm.LastButtonClickedWasRealtime = True
if self.Key is not None:
self.ParentForm.LastButtonClicked = self.Key
else:
self.ParentForm.LastButtonClicked = self.ButtonText
_exit_mainloop(self.ParentForm)
def _find_target(self):
target = self.Target
target_element = None
if target[0] == ThisRow:
target = [self.Position[0], target[1]]
if target[1] < 0:
target[1] = self.Position[1] + target[1]
strvar = None
should_submit_window = False
if target == (None, None):
strvar = self.TKStringVar
else:
# Need a try-block because if the target is not hashable, the "in" test will raise exception
try:
if target in self.ParentForm.AllKeysDict:
target_element = self.ParentForm.AllKeysDict[target]
except:
pass
# if target not found or the above try got exception, then keep looking....
if target_element is None:
if not isinstance(target, str):
if target[0] < 0:
target = [self.Position[0] + target[0], target[1]]
target_element = self.ParentContainer._GetElementAtLocation(target)
else:
target_element = self.ParentForm.find_element(target)
try:
strvar = target_element.TKStringVar
except:
pass
try:
if target_element.ChangeSubmits:
should_submit_window = True
except:
pass
return target_element, strvar, should_submit_window
# ------- Button Callback ------- #
def ButtonCallBack(self):
"""
Not user callable! Called by tkinter when a button is clicked. This is where all the fun begins!
"""
if self.Disabled == BUTTON_DISABLED_MEANS_IGNORE:
return
target_element, strvar, should_submit_window = self._find_target()
filetypes = FILE_TYPES_ALL_FILES if self.FileTypes is None else self.FileTypes
if self.BType == BUTTON_TYPE_BROWSE_FOLDER:
if running_mac(): # macs don't like seeing the parent window (go firgure)
folder_name = tk.filedialog.askdirectory(initialdir=self.InitialFolder) # show the 'get folder' dialog box
else:
folder_name = tk.filedialog.askdirectory(initialdir=self.InitialFolder, parent=self.ParentForm.TKroot) # show the 'get folder' dialog box
if folder_name:
try:
strvar.set(folder_name)
self.TKStringVar.set(folder_name)
except:
pass
else: # if "cancel" button clicked, don't generate an event
should_submit_window = False
elif self.BType == BUTTON_TYPE_BROWSE_FILE:
if running_mac():
# Workaround for the "*.*" issue on Mac
is_all = [(x, y) for (x, y) in filetypes if all(ch in '* .' for ch in y)]
if not len(set(filetypes)) > 1 and (len(is_all) != 0 or filetypes == FILE_TYPES_ALL_FILES):
file_name = tk.filedialog.askopenfilename(initialdir=self.InitialFolder)
else:
file_name = tk.filedialog.askopenfilename(initialdir=self.InitialFolder, filetypes=filetypes) # show the 'get file' dialog box
else:
file_name = tk.filedialog.askopenfilename(filetypes=filetypes, initialdir=self.InitialFolder, parent=self.ParentForm.TKroot) # show the 'get file' dialog box
if file_name:
strvar.set(file_name)
self.TKStringVar.set(file_name)
else: # if "cancel" button clicked, don't generate an event
should_submit_window = False
elif self.BType == BUTTON_TYPE_COLOR_CHOOSER:
color = tk.colorchooser.askcolor(parent=self.ParentForm.TKroot, color=self.default_color) # show the 'get file' dialog box
color = color[1] # save only the #RRGGBB portion
if color is not None:
strvar.set(color)
self.TKStringVar.set(color)
elif self.BType == BUTTON_TYPE_BROWSE_FILES:
if running_mac():
# Workaround for the "*.*" issue on Mac
is_all = [(x, y) for (x, y) in filetypes if all(ch in '* .' for ch in y)]
if not len(set(filetypes)) > 1 and (len(is_all) != 0 or filetypes == FILE_TYPES_ALL_FILES):
file_name = tk.filedialog.askopenfilenames(initialdir=self.InitialFolder)
else:
file_name = tk.filedialog.askopenfilenames(filetypes=filetypes, initialdir=self.InitialFolder)
else:
file_name = tk.filedialog.askopenfilenames(filetypes=filetypes, initialdir=self.InitialFolder, parent=self.ParentForm.TKroot)
if file_name:
file_name = self._files_delimiter.join(file_name) # normally a ';'
strvar.set(file_name)
self.TKStringVar.set(file_name)
else: # if "cancel" button clicked, don't generate an event
should_submit_window = False
elif self.BType == BUTTON_TYPE_SAVEAS_FILE:
# show the 'get file' dialog box
if running_mac():
# Workaround for the "*.*" issue on Mac
is_all = [(x, y) for (x, y) in filetypes if all(ch in '* .' for ch in y)]
if not len(set(filetypes)) > 1 and (len(is_all) != 0 or filetypes == FILE_TYPES_ALL_FILES):
file_name = tk.filedialog.asksaveasfilename(defaultextension=self.DefaultExtension, initialdir=self.InitialFolder)
else:
file_name = tk.filedialog.asksaveasfilename(filetypes=filetypes, defaultextension=self.DefaultExtension, initialdir=self.InitialFolder)
else:
file_name = tk.filedialog.asksaveasfilename(
filetypes=filetypes,
defaultextension=self.DefaultExtension,
initialdir=self.InitialFolder,
parent=self.ParentForm.TKroot,
)
if file_name:
strvar.set(file_name)
self.TKStringVar.set(file_name)
else: # if "cancel" button clicked, don't generate an event
should_submit_window = False
elif self.BType == BUTTON_TYPE_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
if self.Key is not None:
self.ParentForm.LastButtonClicked = self.Key
else:
self.ParentForm.LastButtonClicked = self.ButtonText
self.ParentForm.FormRemainedOpen = False
self.ParentForm._Close()
_exit_mainloop(self.ParentForm)
if self.ParentForm.NonBlocking:
self.ParentForm.TKroot.destroy()
Window._DecrementOpenCount()
elif self.BType == BUTTON_TYPE_READ_FORM: # LEAVE THE WINDOW OPEN!! DO NOT CLOSE
# This is a PLAIN BUTTON
# first, get the results table built
# modify the Results table in the parent FlexForm object
if self.Key is not None:
self.ParentForm.LastButtonClicked = self.Key
else:
self.ParentForm.LastButtonClicked = self.ButtonText
self.ParentForm.FormRemainedOpen = True
_exit_mainloop(self.ParentForm)
elif self.BType == BUTTON_TYPE_CLOSES_WIN_ONLY: # special kind of button that does not exit main loop
self.ParentForm._Close(without_event=True)
self.ParentForm.TKroot.destroy() # close the window with tkinter
Window._DecrementOpenCount()
elif self.BType == BUTTON_TYPE_CALENDAR_CHOOSER: # this is a return type button so GET RESULTS and destroy window
# ------------ new chooser code -------------
self.ParentForm.LastButtonClicked = self.Key # key should have been generated already if not set by user
self.ParentForm.FormRemainedOpen = True
should_submit_window = False
_exit_mainloop(self.ParentForm)
# elif self.BType == BUTTON_TYPE_SHOW_DEBUGGER:
# **** DEPRICATED *****
# if self.ParentForm.DebuggerEnabled:
# show_debugger_popout_window()
if should_submit_window:
self.ParentForm.LastButtonClicked = target_element.Key
self.ParentForm.FormRemainedOpen = True
_exit_mainloop(self.ParentForm)
return
def update(
self,
text=None,
button_color=(None, None),
disabled=None,
image_source=None,
image_data=None,
image_filename=None,
visible=None,
image_subsample=None,
image_zoom=None,
disabled_button_color=(None, None),
image_size=None,
):
"""
Changes some of the settings for the Button Element. Must call `Window.Read` or `Window.Finalize` prior
Changes will not be visible in your window until you call window.read or window.refresh.
If you change visibility, your element may MOVE. If you want it to remain stationary, use the "layout helper"
function "pin" to ensure your element is "pinned" to that location in your layout so that it returns there
when made visible.
:param text: sets button text
:type text: (str)
:param button_color: Color of button. default is from theme or the window. Easy to remember which is which if you say "ON" between colors. "red" on "green". Normally a tuple, but can be a simplified-button-color-string "foreground on background". Can be a single color if want to set only the background.
:type button_color: (str, str) | str
:param disabled: True/False to enable/disable at the GUI level. Use BUTTON_DISABLED_MEANS_IGNORE to ignore clicks (won't change colors)
:type disabled: (bool | str)
:param image_source: Image to place on button. Use INSTEAD of the image_filename and image_data. Unifies these into 1 easier to use parm
:type image_source: (str | bytes)
:param image_data: Raw or Base64 representation of the image to put on button. Choose either filename or data
:type image_data: bytes | str
:param image_filename: image filename if there is a button image. GIFs and PNGs only.
:type image_filename: (str)
:param disabled_button_color: colors to use when button is disabled (text, background). Use None for a color if don't want to change. Only ttk buttons support both text and background colors. tk buttons only support changing text color
:type disabled_button_color: (str, str)
:param visible: control visibility of element
:type visible: (bool)
:param image_subsample: amount to reduce the size of the image. Divides the size by this number. 2=1/2, 3=1/3, 4=1/4, etc
:type image_subsample: (int)
:param image_zoom: amount to increase the size of the image. 2=twice size, 3=3 times, etc
:type image_zoom: (int)
:param image_size: Size of the image in pixels (width, height)
:type image_size: (int, int)
"""
if not self._widget_was_created(): # if widget hasn't been created yet, then don't allow
return
if self._this_elements_window_closed():
_error_popup_with_traceback('Error in Button.update - The window was closed')
return
if image_source is not None:
if isinstance(image_source, bytes):
image_data = image_source
elif isinstance(image_source, str):
image_filename = image_source
if self.UseTtkButtons:
style_name = self.ttk_style_name # created when made initial window (in the pack)
# style_name = str(self.Key) + 'custombutton.TButton'
button_style = ttk.Style()
if text is not None:
btext = text
if FreeSimpleGUI.DEFAULT_USE_BUTTON_SHORTCUTS is True:
pos = btext.find(MENU_SHORTCUT_CHARACTER)
if pos != -1:
if pos < len(MENU_SHORTCUT_CHARACTER) or btext[pos - len(MENU_SHORTCUT_CHARACTER)] != '\\':
btext = btext[:pos] + btext[pos + len(MENU_SHORTCUT_CHARACTER) :]
else:
btext = btext.replace('\\' + MENU_SHORTCUT_CHARACTER, MENU_SHORTCUT_CHARACTER)
pos = -1
if pos != -1:
self.TKButton.config(underline=pos)
self.TKButton.configure(text=btext)
self.ButtonText = text
if button_color != (None, None) and button_color != COLOR_SYSTEM_DEFAULT:
bc = button_color_to_tuple(button_color, self.ButtonColor)
if self.UseTtkButtons:
if bc[0] not in (None, COLOR_SYSTEM_DEFAULT):
button_style.configure(style_name, foreground=bc[0])
if bc[1] not in (None, COLOR_SYSTEM_DEFAULT):
button_style.configure(style_name, background=bc[1])
else:
if bc[0] not in (None, COLOR_SYSTEM_DEFAULT):
self.TKButton.config(foreground=bc[0], activebackground=bc[0])
if bc[1] not in (None, COLOR_SYSTEM_DEFAULT):
self.TKButton.config(background=bc[1], activeforeground=bc[1])
self.ButtonColor = bc
if disabled is True:
self.TKButton['state'] = 'disabled'
elif disabled is False:
self.TKButton['state'] = 'normal'
elif disabled == BUTTON_DISABLED_MEANS_IGNORE:
self.TKButton['state'] = 'normal'
self.Disabled = disabled if disabled is not None else self.Disabled
if image_data is not None:
image = tk.PhotoImage(data=image_data)
if image_subsample:
image = image.subsample(image_subsample)
if image_zoom is not None:
image = image.zoom(int(image_zoom))
if image_size is not None:
width, height = image_size
else:
width, height = image.width(), image.height()
if self.UseTtkButtons:
button_style.configure(style_name, image=image, width=width, height=height)
else:
self.TKButton.config(image=image, width=width, height=height)
self.TKButton.image = image
if image_filename is not None:
image = tk.PhotoImage(file=image_filename)
if image_subsample:
image = image.subsample(image_subsample)
if image_zoom is not None:
image = image.zoom(int(image_zoom))
if image_size is not None:
width, height = image_size
else:
width, height = image.width(), image.height()
if self.UseTtkButtons:
button_style.configure(style_name, image=image, width=width, height=height)
else:
self.TKButton.config(highlightthickness=0, image=image, width=width, height=height)
self.TKButton.image = image
if visible is False:
self._pack_forget_save_settings()
elif visible is True:
self._pack_restore_settings()
if disabled_button_color != (None, None) and disabled_button_color != COLOR_SYSTEM_DEFAULT:
if not self.UseTtkButtons:
self.TKButton['disabledforeground'] = disabled_button_color[0]
else:
if disabled_button_color[0] is not None:
button_style.map(style_name, foreground=[('disabled', disabled_button_color[0])])
if disabled_button_color[1] is not None:
button_style.map(style_name, background=[('disabled', disabled_button_color[1])])
self.DisabledButtonColor = (
disabled_button_color[0] if disabled_button_color[0] is not None else self.DisabledButtonColor[0],
disabled_button_color[1] if disabled_button_color[1] is not None else self.DisabledButtonColor[1],
)
if visible is not None:
self._visible = visible
def get_text(self):
"""
Returns the current text shown on a button
:return: The text currently displayed on the button
:rtype: (str)
"""
return self.ButtonText
def click(self):
"""
Generates a click of the button as if the user clicked the button
Calls the tkinter invoke method for the button
"""
try:
self.TKButton.invoke()
except:
print('Exception clicking button')
Click = click
GetText = get_text
Update = update
class ButtonMenu(Element):
"""
The Button Menu Element. Creates a button that when clicked will show a menu similar to right click menu
"""
def __init__(
self,
button_text,
menu_def,
tooltip=None,
disabled=False,
image_source=None,
image_filename=None,
image_data=None,
image_size=(None, None),
image_subsample=None,
image_zoom=None,
border_width=None,
size=(None, None),
s=(None, None),
auto_size_button=None,
button_color=None,
text_color=None,
background_color=None,
disabled_text_color=None,
font=None,
item_font=None,
pad=None,
p=None,
expand_x=False,
expand_y=False,
key=None,
k=None,
tearoff=False,
visible=True,
metadata=None,
):
"""
:param button_text: Text to be displayed on the button
:type button_text: (str)
:param menu_def: A list of lists of Menu items to show when this element is clicked. See docs for format as they are the same for all menu types
:type menu_def: List[List[str]]
:param tooltip: text, that will appear when mouse hovers over the element
:type tooltip: (str)
:param disabled: If True button will be created disabled
:type disabled: (bool)
:param image_source: Image to place on button. Use INSTEAD of the image_filename and image_data. Unifies these into 1 easier to use parm
:type image_source: (str | bytes)
:param image_filename: image filename if there is a button image. GIFs and PNGs only.
:type image_filename: (str)
:param image_data: Raw or Base64 representation of the image to put on button. Choose either filename or data
:type image_data: bytes | str
:param image_size: Size of the image in pixels (width, height)
:type image_size: (int, int)
:param image_subsample: amount to reduce the size of the image. Divides the size by this number. 2=1/2, 3=1/3, 4=1/4, etc
:type image_subsample: (int)
:param image_zoom: amount to increase the size of the image. 2=twice size, 3=3 times, etc
:type image_zoom: (int)
:param border_width: width of border around button in pixels
:type border_width: (int)
:param size: (w, h) w=characters-wide, h=rows-high. If an int instead of a tuple is supplied, then height is auto-set to 1
:type size: (int, int) | (None, None) | int
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int, int) | (None, None) | int
:param auto_size_button: if True the button size is sized to fit the text
:type auto_size_button: (bool)
:param button_color: of button. Easy to remember which is which if you say "ON" between colors. "red" on "green"
:type button_color: (str, str) | str
:param background_color: color of the background
:type background_color: (str)
:param text_color: element's text color. Can be in #RRGGBB format or a color name "black"
:type text_color: (str)
:param disabled_text_color: color to use for text when item is disabled. Can be in #RRGGBB format or a color name "black"
:type disabled_text_color: (str)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param item_font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike, for the menu items
:type item_font: (str or (str, int[, str]) or None)
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param expand_x: If True the element will automatically expand in the X direction to fill available space
:type expand_x: (bool)
:param expand_y: If True the element will automatically expand in the Y direction to fill available space
:type expand_y: (bool)
:param key: Used with window.find_element and with return values to uniquely identify this element to uniquely identify this element
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param tearoff: Determines if menus should allow them to be torn off
:type tearoff: (bool)
:param visible: set visibility state of the element
:type visible: (bool)
:param metadata: User metadata that can be set to ANYTHING
:type metadata: (Any)
"""
self.MenuDefinition = copy.deepcopy(menu_def)
self.AutoSizeButton = auto_size_button
self.ButtonText = button_text
self.ButtonColor = button_color_to_tuple(button_color)
self.BackgroundColor = background_color if background_color is not None else theme_input_background_color()
self.TextColor = text_color if text_color is not None else theme_input_text_color()
self.DisabledTextColor = disabled_text_color if disabled_text_color is not None else COLOR_SYSTEM_DEFAULT
self.ItemFont = item_font
self.BorderWidth = border_width if border_width is not None else FreeSimpleGUI.DEFAULT_BORDER_WIDTH
if image_source is not None:
if isinstance(image_source, str):
image_filename = image_source
elif isinstance(image_source, bytes):
image_data = image_source
else:
warnings.warn(f'ButtonMenu element - image_source is not a valid type: {type(image_source)}', UserWarning)
self.ImageFilename = image_filename
self.ImageData = image_data
self.ImageSize = image_size
self.ImageSubsample = image_subsample
self.zoom = int(image_zoom) if image_zoom is not None else None
self.Disabled = disabled
self.IsButtonMenu = True
self.MenuItemChosen = None
self.Widget = self.TKButtonMenu = None # type: tk.Menubutton
self.TKMenu = None # type: tk.Menu
self.part_of_custom_menubar = False
self.custom_menubar_key = None
# self.temp_size = size if size != (NONE, NONE) else
key = key if key is not None else k
sz = size if size != (None, None) else s
pad = pad if pad is not None else p
self.expand_x = expand_x
self.expand_y = expand_y
super().__init__(
ELEM_TYPE_BUTTONMENU,
size=sz,
font=font,
pad=pad,
key=key,
tooltip=tooltip,
text_color=self.TextColor,
background_color=self.BackgroundColor,
visible=visible,
metadata=metadata,
)
self.Tearoff = tearoff
def _MenuItemChosenCallback(self, item_chosen): # ButtonMenu Menu Item Chosen Callback
"""
Not a user callable function. Called by tkinter when an item is chosen from the menu.
:param item_chosen: The menu item chosen.
:type item_chosen: (str)
"""
# print('IN MENU ITEM CALLBACK', item_chosen)
self.MenuItemChosen = item_chosen
self.ParentForm.LastButtonClicked = self.Key
self.ParentForm.FormRemainedOpen = True
_exit_mainloop(self.ParentForm)
def update(
self,
menu_definition=None,
visible=None,
image_source=None,
image_size=(None, None),
image_subsample=None,
image_zoom=None,
button_text=None,
button_color=None,
):
"""
Changes some of the settings for the ButtonMenu Element. Must call `Window.Read` or `Window.Finalize` prior
Changes will not be visible in your window until you call window.read or window.refresh.
If you change visibility, your element may MOVE. If you want it to remain stationary, use the "layout helper"
function "pin" to ensure your element is "pinned" to that location in your layout so that it returns there
when made visible.
:param menu_definition: (New menu definition (in menu definition format)
:type menu_definition: List[List]
:param visible: control visibility of element
:type visible: (bool)
:param image_source: new image if image is to be changed. Can be a filename or a base64 encoded byte-string
:type image_source: (str | bytes)
:param image_size: Size of the image in pixels (width, height)
:type image_size: (int, int)
:param image_subsample: amount to reduce the size of the image. Divides the size by this number. 2=1/2, 3=1/3, 4=1/4, etc
:type image_subsample: (int)
:param image_zoom: amount to increase the size of the image. 2=twice size, 3=3 times, etc
:type image_zoom: (int)
:param button_text: Text to be shown on the button
:type button_text: (str)
:param button_color: Normally a tuple, but can be a simplified-button-color-string "foreground on background". Can be a single color if want to set only the background.
:type button_color: (str, str) | str
"""
if not self._widget_was_created(): # if widget hasn't been created yet, then don't allow
return
if self._this_elements_window_closed():
_error_popup_with_traceback('Error in ButtonMenu.update - The window was closed')
return
if menu_definition is not None:
self.MenuDefinition = copy.deepcopy(menu_definition)
top_menu = self.TKMenu = tk.Menu(self.TKButtonMenu, tearoff=self.Tearoff, font=self.ItemFont, tearoffcommand=self._tearoff_menu_callback)
if self.BackgroundColor not in (COLOR_SYSTEM_DEFAULT, None):
top_menu.config(bg=self.BackgroundColor)
if self.TextColor not in (COLOR_SYSTEM_DEFAULT, None):
top_menu.config(fg=self.TextColor)
if self.DisabledTextColor not in (COLOR_SYSTEM_DEFAULT, None):
top_menu.config(disabledforeground=self.DisabledTextColor)
if self.ItemFont is not None:
top_menu.config(font=self.ItemFont)
AddMenuItem(self.TKMenu, self.MenuDefinition[1], self)
self.TKButtonMenu.configure(menu=self.TKMenu)
if image_source is not None:
filename = data = None
if image_source is not None:
if isinstance(image_source, bytes):
data = image_source
elif isinstance(image_source, str):
filename = image_source
else:
warnings.warn(
f'ButtonMenu element - image_source is not a valid type: {type(image_source)}',
UserWarning,
)
image = None
if filename is not None:
image = tk.PhotoImage(file=filename)
if image_subsample is not None:
image = image.subsample(image_subsample)
if image_zoom is not None:
image = image.zoom(int(image_zoom))
elif data is not None:
# if type(data) is bytes:
try:
image = tk.PhotoImage(data=data)
if image_subsample is not None:
image = image.subsample(image_subsample)
if image_zoom is not None:
image = image.zoom(int(image_zoom))
except Exception:
image = data
if image is not None:
if type(image) is not bytes:
width, height = (
image_size[0] if image_size[0] is not None else image.width(),
image_size[1] if image_size[1] is not None else image.height(),
)
else:
width, height = image_size
self.TKButtonMenu.config(image=image, compound=tk.CENTER, width=width, height=height)
self.TKButtonMenu.image = image
if button_text is not None:
self.TKButtonMenu.configure(text=button_text)
self.ButtonText = button_text
if visible is False:
self._pack_forget_save_settings()
elif visible is True:
self._pack_restore_settings()
if visible is not None:
self._visible = visible
if button_color != (None, None) and button_color != COLOR_SYSTEM_DEFAULT:
bc = button_color_to_tuple(button_color, self.ButtonColor)
if bc[0] not in (None, COLOR_SYSTEM_DEFAULT):
self.TKButtonMenu.config(foreground=bc[0], activeforeground=bc[0])
if bc[1] not in (None, COLOR_SYSTEM_DEFAULT):
self.TKButtonMenu.config(background=bc[1], activebackground=bc[1])
self.ButtonColor = bc
def click(self):
"""
Generates a click of the button as if the user clicked the button
Calls the tkinter invoke method for the button
"""
try:
self.TKMenu.invoke(1)
except:
print('Exception clicking button')
Update = update
Click = click
from FreeSimpleGUI._utils import _error_popup_with_traceback, _exit_mainloop
from FreeSimpleGUI.window import Window

View File

@ -0,0 +1,222 @@
from __future__ import annotations
import calendar
import tkinter as tk
import tkinter.font
from tkinter import ttk
class TKCalendar(ttk.Frame):
"""
This code was shamelessly lifted from moshekaplan's repository - moshekaplan/tkinter_components
NONE of this code is user callable. Stay away!
"""
# XXX ToDo: cget and configure
datetime = calendar.datetime.datetime
timedelta = calendar.datetime.timedelta
def __init__(self, master=None, target_element=None, close_when_chosen=True, default_date=(None, None, None), **kw):
"""WIDGET-SPECIFIC OPTIONS: locale, firstweekday, year, month, selectbackground, selectforeground"""
self._TargetElement = target_element
default_mon, default_day, default_year = default_date
# remove custom options from kw before initializating ttk.Frame
fwday = kw.pop('firstweekday', calendar.MONDAY)
year = kw.pop('year', default_year or self.datetime.now().year)
month = kw.pop('month', default_mon or self.datetime.now().month)
locale = kw.pop('locale', None)
sel_bg = kw.pop('selectbackground', '#ecffc4')
sel_fg = kw.pop('selectforeground', '#05640e')
self.format = kw.pop('format')
if self.format is None:
self.format = '%Y-%m-%d %H:%M:%S'
self._date = self.datetime(year, month, default_day or 1)
self._selection = None # no date selected
self._master = master
self.close_when_chosen = close_when_chosen
ttk.Frame.__init__(self, master, **kw)
# instantiate proper calendar class
if locale is None:
self._cal = calendar.TextCalendar(fwday)
else:
self._cal = calendar.LocaleTextCalendar(fwday, locale)
self.__setup_styles() # creates custom styles
self.__place_widgets() # pack/grid used widgets
self.__config_calendar() # adjust calendar columns and setup tags
# configure a canvas, and proper bindings, for selecting dates
self.__setup_selection(sel_bg, sel_fg)
# store items ids, used for insertion later
self._items = [self._calendar.insert('', 'end', values='') for _ in range(6)]
# insert dates in the currently empty calendar
self._build_calendar()
def __setitem__(self, item, value):
if item in ('year', 'month'):
raise AttributeError("attribute '%s' is not writeable" % item)
elif item == 'selectbackground':
self._canvas['background'] = value
elif item == 'selectforeground':
self._canvas.itemconfigure(self._canvas.text, item=value)
else:
ttk.Frame.__setitem__(self, item, value)
def __getitem__(self, item):
if item in ('year', 'month'):
return getattr(self._date, item)
elif item == 'selectbackground':
return self._canvas['background']
elif item == 'selectforeground':
return self._canvas.itemcget(self._canvas.text, 'fill')
else:
r = ttk.tclobjs_to_py({item: ttk.Frame.__getitem__(self, item)})
return r[item]
def __setup_styles(self):
# custom ttk styles
style = ttk.Style(self.master)
def arrow_layout(dir):
return [('Button.focus', {'children': [('Button.%sarrow' % dir, None)]})]
style.layout('L.TButton', arrow_layout('left'))
style.layout('R.TButton', arrow_layout('right'))
def __place_widgets(self):
# header frame and its widgets
hframe = ttk.Frame(self)
lbtn = ttk.Button(hframe, style='L.TButton', command=self._prev_month)
rbtn = ttk.Button(hframe, style='R.TButton', command=self._next_month)
self._header = ttk.Label(hframe, width=15, anchor='center')
# the calendar
self._calendar = ttk.Treeview(self, show='', selectmode='none', height=7)
# pack the widgets
hframe.pack(in_=self, side='top', pady=4, anchor='center')
lbtn.grid(in_=hframe)
self._header.grid(in_=hframe, column=1, row=0, padx=12)
rbtn.grid(in_=hframe, column=2, row=0)
self._calendar.pack(in_=self, expand=1, fill='both', side='bottom')
def __config_calendar(self):
cols = self._cal.formatweekheader(3).split()
self._calendar['columns'] = cols
self._calendar.tag_configure('header', background='grey90')
self._calendar.insert('', 'end', values=cols, tag='header')
# adjust its columns width
font = tkinter.font.Font()
maxwidth = max(font.measure(col) for col in cols)
for col in cols:
self._calendar.column(col, width=maxwidth, minwidth=maxwidth, anchor='e')
def __setup_selection(self, sel_bg, sel_fg):
self._font = tkinter.font.Font()
self._canvas = canvas = tk.Canvas(self._calendar, background=sel_bg, borderwidth=0, highlightthickness=0)
canvas.text = canvas.create_text(0, 0, fill=sel_fg, anchor='w')
canvas.bind('<ButtonPress-1>', lambda evt: canvas.place_forget())
self._calendar.bind('<Configure>', lambda evt: canvas.place_forget())
self._calendar.bind('<ButtonPress-1>', self._pressed)
def __minsize(self, evt):
width, height = self._calendar.master.geometry().split('x')
height = height[: height.index('+')]
self._calendar.master.minsize(width, height)
def _build_calendar(self):
year, month = self._date.year, self._date.month
# update header text (Month, YEAR)
header = self._cal.formatmonthname(year, month, 0)
self._header['text'] = header.title()
# update calendar shown dates
cal = self._cal.monthdayscalendar(year, month)
for indx, item in enumerate(self._items):
week = cal[indx] if indx < len(cal) else []
fmt_week = [('%02d' % day) if day else '' for day in week]
self._calendar.item(item, values=fmt_week)
def _show_selection(self, text, bbox):
"""Configure canvas for a new selection."""
x, y, width, height = bbox
textw = self._font.measure(text)
canvas = self._canvas
canvas.configure(width=width, height=height)
canvas.coords(canvas.text, width - textw, height / 2 - 1)
canvas.itemconfigure(canvas.text, text=text)
canvas.place(in_=self._calendar, x=x, y=y)
# Callbacks
def _pressed(self, evt):
"""Clicked somewhere in the calendar."""
x, y, widget = evt.x, evt.y, evt.widget
item = widget.identify_row(y)
column = widget.identify_column(x)
if not column or item not in self._items:
# clicked in the weekdays row or just outside the columns
return
item_values = widget.item(item)['values']
if not len(item_values): # row is empty for this month
return
text = item_values[int(column[1]) - 1]
if not text: # date is empty
return
bbox = widget.bbox(item, column)
if not bbox: # calendar not visible yet
return
# update and then show selection
text = '%02d' % text
self._selection = (text, item, column)
self._show_selection(text, bbox)
year, month = self._date.year, self._date.month
now = self.datetime.now()
try:
self._TargetElement.Update(self.datetime(year, month, int(self._selection[0]), now.hour, now.minute, now.second).strftime(self.format))
if self._TargetElement.ChangeSubmits:
self._TargetElement.ParentForm.LastButtonClicked = self._TargetElement.Key
self._TargetElement.ParentForm.FormRemainedOpen = True
self._TargetElement.ParentForm.TKroot.quit() # kick the users out of the mainloop
except:
pass
if self.close_when_chosen:
self._master.destroy()
def _prev_month(self):
"""Updated calendar to show the previous month."""
self._canvas.place_forget()
self._date = self._date - self.timedelta(days=1)
self._date = self.datetime(self._date.year, self._date.month, 1)
self._build_calendar() # reconstuct calendar
def _next_month(self):
"""Update calendar to show the next month."""
self._canvas.place_forget()
year, month = self._date.year, self._date.month
self._date = self._date + self.timedelta(days=calendar.monthrange(year, month)[1] + 1)
self._date = self.datetime(self._date.year, self._date.month, 1)
self._build_calendar() # reconstruct calendar
# Properties
@property
def selection(self):
if not self._selection:
return None
year, month = self._date.year, self._date.month
return self.datetime(year, month, int(self._selection[0]))

View File

@ -0,0 +1,122 @@
from __future__ import annotations
import FreeSimpleGUI
from FreeSimpleGUI import COLOR_SYSTEM_DEFAULT
from FreeSimpleGUI import ELEM_TYPE_CANVAS
from FreeSimpleGUI import Element
from FreeSimpleGUI._utils import _error_popup_with_traceback
class Canvas(Element):
def __init__(
self,
canvas=None,
background_color=None,
size=(None, None),
s=(None, None),
pad=None,
p=None,
key=None,
k=None,
tooltip=None,
right_click_menu=None,
expand_x=False,
expand_y=False,
visible=True,
border_width=0,
metadata=None,
):
"""
:param canvas: Your own tk.Canvas if you already created it. Leave blank to create a Canvas
:type canvas: (tk.Canvas)
:param background_color: color of background
:type background_color: (str)
:param size: (width in char, height in rows) size in pixels to make canvas
:type size: (int,int) | (None, None)
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int, int) | (None, None) | int
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param key: Used with window.find_element and with return values to uniquely identify this element
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param tooltip: text, that will appear when mouse hovers over the element
:type tooltip: (str)
:param right_click_menu: A list of lists of Menu items to show when this element is right clicked. See user docs for exact format.
:type right_click_menu: List[List[ List[str] | str ]]
:param expand_x: If True the element will automatically expand in the X direction to fill available space
:type expand_x: (bool)
:param expand_y: If True the element will automatically expand in the Y direction to fill available space
:type expand_y: (bool)
:param visible: set visibility state of the element
:type visible: (bool)
:param border_width: width of border around element in pixels. Not normally used with Canvas element
:type border_width: (int)
:param metadata: User metadata that can be set to ANYTHING
:type metadata: (Any)
"""
self.BackgroundColor = background_color if background_color is not None else FreeSimpleGUI.DEFAULT_BACKGROUND_COLOR
self._TKCanvas = self.Widget = canvas
self.RightClickMenu = right_click_menu
self.BorderWidth = border_width
key = key if key is not None else k
sz = size if size != (None, None) else s
pad = pad if pad is not None else p
self.expand_x = expand_x
self.expand_y = expand_y
super().__init__(
ELEM_TYPE_CANVAS,
background_color=background_color,
size=sz,
pad=pad,
key=key,
tooltip=tooltip,
visible=visible,
metadata=metadata,
)
return
def update(self, background_color=None, visible=None):
"""
:param background_color: color of background
:type background_color: (str)
:param visible: set visibility state of the element
:type visible: (bool)
"""
if not self._widget_was_created(): # if widget hasn't been created yet, then don't allow
return
if self._this_elements_window_closed():
_error_popup_with_traceback('Error in Canvas.update - The window was closed')
return
if background_color not in (None, COLOR_SYSTEM_DEFAULT):
self._TKCanvas.configure(background=background_color)
if visible is False:
self._pack_forget_save_settings()
elif visible is True:
self._pack_restore_settings()
if visible is not None:
self._visible = visible
@property
def tk_canvas(self):
"""
Returns the underlying tkiner Canvas widget
:return: The tkinter canvas widget
:rtype: (tk.Canvas)
"""
if self._TKCanvas is None:
print('*** Did you forget to call Finalize()? Your code should look something like: ***')
print('*** window = sg.Window("My Form", layout, finalize=True) ***')
return self._TKCanvas
TKCanvas = tk_canvas

View File

@ -0,0 +1,240 @@
from __future__ import annotations
import tkinter as tk # noqa
from FreeSimpleGUI import _hex_to_hsl
from FreeSimpleGUI import _hsl_to_rgb
from FreeSimpleGUI import COLOR_SYSTEM_DEFAULT
from FreeSimpleGUI import ELEM_TYPE_INPUT_CHECKBOX
from FreeSimpleGUI import Element
from FreeSimpleGUI import rgb
from FreeSimpleGUI import theme_background_color
from FreeSimpleGUI import theme_text_color
from FreeSimpleGUI._utils import _error_popup_with_traceback
class Checkbox(Element):
"""
Checkbox Element - Displays a checkbox and text next to it
"""
def __init__(
self,
text,
default=False,
size=(None, None),
s=(None, None),
auto_size_text=None,
font=None,
background_color=None,
text_color=None,
checkbox_color=None,
highlight_thickness=1,
change_submits=False,
enable_events=False,
disabled=False,
key=None,
k=None,
pad=None,
p=None,
tooltip=None,
right_click_menu=None,
expand_x=False,
expand_y=False,
visible=True,
metadata=None,
):
"""
:param text: Text to display next to checkbox
:type text: (str)
:param default: Set to True if you want this checkbox initially checked
:type default: (bool)
:param size: (w, h) w=characters-wide, h=rows-high. If an int instead of a tuple is supplied, then height is auto-set to 1
:type size: (int, int) | (None, None) | int
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int, int) | (None, None) | int
:param auto_size_text: if True will size the element to match the length of the text
:type auto_size_text: (bool)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param background_color: color of background
:type background_color: (str)
:param text_color: color of the text
:type text_color: (str)
:param checkbox_color: color of background of the box that has the check mark in it. The checkmark is the same color as the text
:type checkbox_color: (str)
:param highlight_thickness: thickness of border around checkbox when gets focus
:type highlight_thickness: (int)
:param change_submits: DO NOT USE. Only listed for backwards compat - Use enable_events instead
:type change_submits: (bool)
:param enable_events: Turns on the element specific events. Checkbox events happen when an item changes
:type enable_events: (bool)
:param disabled: set disable state
:type disabled: (bool)
:param key: Used with window.find_element and with return values to uniquely identify this element
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param tooltip: text, that will appear when mouse hovers over the element
:type tooltip: (str)
:param right_click_menu: A list of lists of Menu items to show when this element is right clicked. See user docs for exact format.
:type right_click_menu: List[List[ List[str] | str ]]
:param expand_x: If True the element will automatically expand in the X direction to fill available space
:type expand_x: (bool)
:param expand_y: If True the element will automatically expand in the Y direction to fill available space
:type expand_y: (bool)
:param visible: set visibility state of the element
:type visible: (bool)
:param metadata: User metadata that can be set to ANYTHING
:type metadata: (Any)
"""
self.Text = text
self.InitialState = bool(default)
self.Value = None
self.TKCheckbutton = self.Widget = None # type: tk.Checkbutton
self.Disabled = disabled
self.TextColor = text_color if text_color else theme_text_color()
self.RightClickMenu = right_click_menu
self.highlight_thickness = highlight_thickness
# ---- compute color of circle background ---
if checkbox_color is None:
try: # something in here will fail if a color is not specified in Hex
text_hsl = _hex_to_hsl(self.TextColor)
background_hsl = _hex_to_hsl(background_color if background_color else theme_background_color())
l_delta = abs(text_hsl[2] - background_hsl[2]) / 10
if text_hsl[2] > background_hsl[2]: # if the text is "lighter" than the background then make background darker
bg_rbg = _hsl_to_rgb(background_hsl[0], background_hsl[1], background_hsl[2] - l_delta)
else:
bg_rbg = _hsl_to_rgb(background_hsl[0], background_hsl[1], background_hsl[2] + l_delta)
self.CheckboxBackgroundColor = rgb(*bg_rbg)
except:
self.CheckboxBackgroundColor = background_color if background_color else theme_background_color()
else:
self.CheckboxBackgroundColor = checkbox_color
self.ChangeSubmits = change_submits or enable_events
key = key if key is not None else k
sz = size if size != (None, None) else s
pad = pad if pad is not None else p
self.expand_x = expand_x
self.expand_y = expand_y
super().__init__(
ELEM_TYPE_INPUT_CHECKBOX,
size=sz,
auto_size_text=auto_size_text,
font=font,
background_color=background_color,
text_color=self.TextColor,
key=key,
pad=pad,
tooltip=tooltip,
visible=visible,
metadata=metadata,
)
def get(self):
# type: (Checkbox) -> bool
"""
Return the current state of this checkbox
:return: Current state of checkbox
:rtype: (bool)
"""
return self.TKIntVar.get() != 0
def update(
self,
value=None,
text=None,
background_color=None,
text_color=None,
checkbox_color=None,
disabled=None,
visible=None,
):
"""
Changes some of the settings for the Checkbox Element. Must call `Window.Read` or `Window.Finalize` prior.
Note that changing visibility may cause element to change locations when made visible after invisible
Changes will not be visible in your window until you call window.read or window.refresh.
If you change visibility, your element may MOVE. If you want it to remain stationary, use the "layout helper"
function "pin" to ensure your element is "pinned" to that location in your layout so that it returns there
when made visible.
:param value: if True checks the checkbox, False clears it
:type value: (bool)
:param text: Text to display next to checkbox
:type text: (str)
:param background_color: color of background
:type background_color: (str)
:param text_color: color of the text. Note this also changes the color of the checkmark
:type text_color: (str)
:param disabled: disable or enable element
:type disabled: (bool)
:param visible: control visibility of element
:type visible: (bool)
"""
if not self._widget_was_created(): # if widget hasn't been created yet, then don't allow
return
if self._this_elements_window_closed():
_error_popup_with_traceback('Error in Checkbox.update - The window was closed')
return
if value is not None:
value = bool(value)
try:
self.TKIntVar.set(value)
self.InitialState = value
except:
print('Checkbox update failed')
if disabled is True:
self.TKCheckbutton.configure(state='disabled')
elif disabled is False:
self.TKCheckbutton.configure(state='normal')
self.Disabled = disabled if disabled is not None else self.Disabled
if text is not None:
self.Text = str(text)
self.TKCheckbutton.configure(text=self.Text)
if background_color not in (None, COLOR_SYSTEM_DEFAULT):
self.TKCheckbutton.configure(background=background_color)
self.BackgroundColor = background_color
if text_color not in (None, COLOR_SYSTEM_DEFAULT):
self.TKCheckbutton.configure(fg=text_color)
self.TextColor = text_color
# Color the checkbox itself
if checkbox_color not in (None, COLOR_SYSTEM_DEFAULT):
self.CheckboxBackgroundColor = checkbox_color
self.TKCheckbutton.configure(selectcolor=self.CheckboxBackgroundColor) # The background of the checkbox
elif text_color or background_color:
if self.CheckboxBackgroundColor is not None and self.TextColor is not None and self.BackgroundColor is not None and self.TextColor.startswith('#') and self.BackgroundColor.startswith('#'):
# ---- compute color of checkbox background ---
text_hsl = _hex_to_hsl(self.TextColor)
background_hsl = _hex_to_hsl(self.BackgroundColor if self.BackgroundColor else theme_background_color())
l_delta = abs(text_hsl[2] - background_hsl[2]) / 10
if text_hsl[2] > background_hsl[2]: # if the text is "lighter" than the background then make background darker
bg_rbg = _hsl_to_rgb(background_hsl[0], background_hsl[1], background_hsl[2] - l_delta)
else:
bg_rbg = _hsl_to_rgb(background_hsl[0], background_hsl[1], background_hsl[2] + l_delta)
self.CheckboxBackgroundColor = rgb(*bg_rbg)
self.TKCheckbutton.configure(selectcolor=self.CheckboxBackgroundColor) # The background of the checkbox
if visible is False:
self._pack_forget_save_settings()
elif visible is True:
self._pack_restore_settings()
if visible is not None:
self._visible = visible
Get = get
Update = update

View File

@ -0,0 +1,442 @@
from __future__ import annotations
import tkinter as tk
import warnings
import FreeSimpleGUI
from FreeSimpleGUI import _make_ttk_scrollbar
from FreeSimpleGUI import _random_error_emoji
from FreeSimpleGUI import ELEM_TYPE_COLUMN
from FreeSimpleGUI import popup_error
from FreeSimpleGUI import VarHolder
from FreeSimpleGUI._utils import _error_popup_with_traceback
from FreeSimpleGUI.elements.base import Element
class TkFixedFrame(tk.Frame):
"""
A tkinter frame that is used with Column Elements that do not have a scrollbar
"""
def __init__(self, master, **kwargs):
"""
:param master: The parent widget
:type master: (tk.Widget)
:param **kwargs: The keyword args
:type **kwargs:
"""
tk.Frame.__init__(self, master, **kwargs)
self.canvas = tk.Canvas(self)
self.canvas.pack(side='left', fill='both', expand=True)
# reset the view
self.canvas.xview_moveto(0)
self.canvas.yview_moveto(0)
# create a frame inside the canvas which will be scrolled with it
self.TKFrame = tk.Frame(self.canvas, **kwargs)
self.frame_id = self.canvas.create_window(0, 0, window=self.TKFrame, anchor='nw')
self.canvas.config(borderwidth=0, highlightthickness=0)
self.TKFrame.config(borderwidth=0, highlightthickness=0)
self.config(borderwidth=0, highlightthickness=0)
class TkScrollableFrame(tk.Frame):
"""
A frame with one or two scrollbars. Used to make Columns with scrollbars
"""
def __init__(self, master, vertical_only, element, window, **kwargs):
"""
:param master: The parent widget
:type master: (tk.Widget)
:param vertical_only: if True the only a vertical scrollbar will be shown
:type vertical_only: (bool)
:param element: The element containing this object
:type element: (Column)
"""
tk.Frame.__init__(self, master, **kwargs)
# create a canvas object and a vertical scrollbar for scrolling it
self.canvas = tk.Canvas(self)
element.Widget = self.canvas
# Okay, we're gonna make a list. Containing the y-min, x-min, y-max, and x-max of the frame
element.element_frame = self
_make_ttk_scrollbar(element, 'v', window)
# element.vsb = tk.Scrollbar(self, orient=tk.VERTICAL)
element.vsb.pack(side='right', fill='y', expand='false')
if not vertical_only:
_make_ttk_scrollbar(element, 'h', window)
# self.hscrollbar = tk.Scrollbar(self, orient=tk.HORIZONTAL)
element.hsb.pack(side='bottom', fill='x', expand='false')
self.canvas.config(xscrollcommand=element.hsb.set)
self.canvas.config(yscrollcommand=element.vsb.set)
self.canvas.pack(side='left', fill='both', expand=True)
element.vsb.config(command=self.canvas.yview)
if not vertical_only:
element.hsb.config(command=self.canvas.xview)
# reset the view
self.canvas.xview_moveto(0)
self.canvas.yview_moveto(0)
# create a frame inside the canvas which will be scrolled with it
self.TKFrame = tk.Frame(self.canvas, **kwargs)
self.frame_id = self.canvas.create_window(0, 0, window=self.TKFrame, anchor='nw')
self.canvas.config(borderwidth=0, highlightthickness=0)
self.TKFrame.config(borderwidth=0, highlightthickness=0)
self.config(borderwidth=0, highlightthickness=0)
# Canvas can be: master, canvas, TKFrame
self.unhookMouseWheel(None)
self.canvas.bind('<Enter>', self.hookMouseWheel)
self.canvas.bind('<Leave>', self.unhookMouseWheel)
self.bind('<Configure>', self.set_scrollregion)
def hookMouseWheel(self, e):
# print("enter")
VarHolder.canvas_holder = self.canvas
self.canvas.bind_all('<4>', self.yscroll, add='+')
self.canvas.bind_all('<5>', self.yscroll, add='+')
self.canvas.bind_all('<MouseWheel>', self.yscroll, add='+')
self.canvas.bind_all('<Shift-MouseWheel>', self.xscroll, add='+')
# Chr0nic
def unhookMouseWheel(self, e):
# print("leave")
VarHolder.canvas_holder = None
self.canvas.unbind_all('<4>')
self.canvas.unbind_all('<5>')
self.canvas.unbind_all('<MouseWheel>')
self.canvas.unbind_all('<Shift-MouseWheel>')
def resize_frame(self, e):
self.canvas.itemconfig(self.frame_id, height=e.height, width=e.width)
def yscroll(self, event):
if self.canvas.yview() == (0.0, 1.0):
return
if event.num == 5 or event.delta < 0:
self.canvas.yview_scroll(1, 'unit')
elif event.num == 4 or event.delta > 0:
self.canvas.yview_scroll(-1, 'unit')
def xscroll(self, event):
if event.num == 5 or event.delta < 0:
self.canvas.xview_scroll(1, 'unit')
elif event.num == 4 or event.delta > 0:
self.canvas.xview_scroll(-1, 'unit')
def bind_mouse_scroll(self, parent, mode):
# ~~ Windows only
parent.bind('<MouseWheel>', mode)
# ~~ Unix only
parent.bind('<Button-4>', mode)
parent.bind('<Button-5>', mode)
def set_scrollregion(self, event=None):
"""Set the scroll region on the canvas"""
self.canvas.configure(scrollregion=self.canvas.bbox('all'))
class Column(Element):
"""
A container element that is used to create a layout within your window's layout
"""
def __init__(
self,
layout,
background_color=None,
size=(None, None),
s=(None, None),
size_subsample_width=1,
size_subsample_height=2,
pad=None,
p=None,
scrollable=False,
vertical_scroll_only=False,
right_click_menu=None,
key=None,
k=None,
visible=True,
justification=None,
element_justification=None,
vertical_alignment=None,
grab=None,
expand_x=None,
expand_y=None,
metadata=None,
sbar_trough_color=None,
sbar_background_color=None,
sbar_arrow_color=None,
sbar_width=None,
sbar_arrow_width=None,
sbar_frame_color=None,
sbar_relief=None,
):
"""
:param layout: Layout that will be shown in the Column container
:type layout: List[List[Element]]
:param background_color: color of background of entire Column
:type background_color: (str)
:param size: (width, height) size in pixels (doesn't work quite right, sometimes only 1 dimension is set by tkinter. Use a Sizer Element to help set sizes
:type size: (int | None, int | None)
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int | None, int | None)
:param size_subsample_width: Determines the size of a scrollable column width based on 1/size_subsample * required size. 1 = match the contents exactly, 2 = 1/2 contents size, 3 = 1/3. Can be a fraction to make larger than required.
:type size_subsample_width: (float)
:param size_subsample_height: Determines the size of a scrollable height based on 1/size_subsample * required size. 1 = match the contents exactly, 2 = 1/2 contents size, 3 = 1/3. Can be a fraction to make larger than required..
:type size_subsample_height: (float)
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param scrollable: if True then scrollbars will be added to the column. If you update the contents of a scrollable column, be sure and call Column.contents_changed also
:type scrollable: (bool)
:param vertical_scroll_only: if True then no horizontal scrollbar will be shown if a scrollable column
:type vertical_scroll_only: (bool)
:param right_click_menu: A list of lists of Menu items to show when this element is right clicked. See user docs for exact format.
:type right_click_menu: List[List[ List[str] | str ]]
:param key: Value that uniquely identifies this element from all other elements. Used when Finding an element or in return values. Must be unique to the window
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param visible: set visibility state of the element
:type visible: (bool)
:param justification: set justification for the Column itself. Note entire row containing the Column will be affected
:type justification: (str)
:param element_justification: All elements inside the Column will have this justification 'left', 'right', 'center' are valid values
:type element_justification: (str)
:param vertical_alignment: Place the column at the 'top', 'center', 'bottom' of the row (can also use t,c,r). Defaults to no setting (tkinter decides)
:type vertical_alignment: (str)
:param grab: If True can grab this element and move the window around. Default is False
:type grab: (bool)
:param expand_x: If True the column will automatically expand in the X direction to fill available space
:type expand_x: (bool)
:param expand_y: If True the column will automatically expand in the Y direction to fill available space
:type expand_y: (bool)
:param metadata: User metadata that can be set to ANYTHING
:type metadata: (Any)
:param sbar_trough_color: Scrollbar color of the trough
:type sbar_trough_color: (str)
:param sbar_background_color: Scrollbar color of the background of the arrow buttons at the ends AND the color of the "thumb" (the thing you grab and slide). Switches to arrow color when mouse is over
:type sbar_background_color: (str)
:param sbar_arrow_color: Scrollbar color of the arrow at the ends of the scrollbar (it looks like a button). Switches to background color when mouse is over
:type sbar_arrow_color: (str)
:param sbar_width: Scrollbar width in pixels
:type sbar_width: (int)
:param sbar_arrow_width: Scrollbar width of the arrow on the scrollbar. It will potentially impact the overall width of the scrollbar
:type sbar_arrow_width: (int)
:param sbar_frame_color: Scrollbar Color of frame around scrollbar (available only on some ttk themes)
:type sbar_frame_color: (str)
:param sbar_relief: Scrollbar relief that will be used for the "thumb" of the scrollbar (the thing you grab that slides). Should be a constant that is defined at starting with "RELIEF_" - RELIEF_RAISED, RELIEF_SUNKEN, RELIEF_FLAT, RELIEF_RIDGE, RELIEF_GROOVE, RELIEF_SOLID
:type sbar_relief: (str)
"""
self.UseDictionary = False
self.ReturnValues = None
self.ReturnValuesList = []
self.ReturnValuesDictionary = {}
self.DictionaryKeyCounter = 0
self.ParentWindow = None
self.ParentPanedWindow = None
self.Rows = []
self.TKFrame = None
self.TKColFrame = None # type: tk.Frame
self.Scrollable = scrollable
self.VerticalScrollOnly = vertical_scroll_only
self.RightClickMenu = right_click_menu
bg = background_color if background_color is not None else FreeSimpleGUI.DEFAULT_BACKGROUND_COLOR
self.ContainerElemementNumber = Window._GetAContainerNumber()
self.ElementJustification = element_justification
self.Justification = justification
self.VerticalAlignment = vertical_alignment
key = key if key is not None else k
self.Grab = grab
self.expand_x = expand_x
self.expand_y = expand_y
self.Layout(layout)
sz = size if size != (None, None) else s
pad = pad if pad is not None else p
self.size_subsample_width = size_subsample_width
self.size_subsample_height = size_subsample_height
super().__init__(
ELEM_TYPE_COLUMN,
background_color=bg,
size=sz,
pad=pad,
key=key,
visible=visible,
metadata=metadata,
sbar_trough_color=sbar_trough_color,
sbar_background_color=sbar_background_color,
sbar_arrow_color=sbar_arrow_color,
sbar_width=sbar_width,
sbar_arrow_width=sbar_arrow_width,
sbar_frame_color=sbar_frame_color,
sbar_relief=sbar_relief,
)
return
def add_row(self, *args):
"""
Not recommended user call. Used to add rows of Elements to the Column Element.
:param *args: The list of elements for this row
:type *args: List[Element]
"""
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
if type(element) is list:
popup_error(
'Error creating Column layout',
'Layout has a LIST instead of an ELEMENT',
'This sometimes means you have a badly placed ]',
'The offensive list is:',
element,
'This list will be stripped from your layout',
keep_on_top=True,
image=_random_error_emoji(),
)
continue
elif callable(element) and not isinstance(element, Element):
popup_error(
'Error creating Column layout',
'Layout has a FUNCTION instead of an ELEMENT',
'This likely means you are missing () from your layout',
'The offensive list is:',
element,
'This item will be stripped from your layout',
keep_on_top=True,
image=_random_error_emoji(),
)
continue
if element.ParentContainer is not None:
warnings.warn(
'*** YOU ARE ATTEMPTING TO REUSE AN ELEMENT IN YOUR LAYOUT! Once placed in a layout, an element cannot be used in another layout. ***',
UserWarning,
)
popup_error(
'Error creating Column layout',
'The layout specified has already been used',
'You MUST start witha "clean", unused layout every time you create a window',
'The offensive Element = ',
element,
'and has a key = ',
element.Key,
'This item will be stripped from your layout',
'Hint - try printing your layout and matching the IDs "print(layout)"',
keep_on_top=True,
image=_random_error_emoji(),
)
continue
element.Position = (CurrentRowNumber, i)
element.ParentContainer = self
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):
"""
Can use like the Window.Layout method, but it's better to use the layout parameter when creating
:param rows: The rows of Elements
:type rows: List[List[Element]]
:return: Used for chaining
:rtype: (Column)
"""
for row in rows:
try:
iter(row)
except TypeError:
popup_error(
'Error creating Column layout',
'Your row is not an iterable (e.g. a list)',
f'Instead of a list, the type found was {type(row)}',
'The offensive row = ',
row,
'This item will be stripped from your layout',
keep_on_top=True,
image=_random_error_emoji(),
)
continue
self.AddRow(*row)
return self
def _GetElementAtLocation(self, location):
"""
Not user callable. Used to find the Element at a row, col position within the layout
:param location: (row, column) position of the element to find in layout
:type location: (int, int)
:return: The element found at the location
:rtype: (Element)
"""
(row_num, col_num) = location
row = self.Rows[row_num]
element = row[col_num]
return element
def update(self, visible=None):
"""
Changes some of the settings for the Column Element. Must call `Window.Read` or `Window.Finalize` prior
Changes will not be visible in your window until you call window.read or window.refresh.
If you change visibility, your element may MOVE. If you want it to remain stationary, use the "layout helper"
function "pin" to ensure your element is "pinned" to that location in your layout so that it returns there
when made visible.
:param visible: control visibility of element
:type visible: (bool)
"""
if not self._widget_was_created(): # if widget hasn't been created yet, then don't allow
return
if self._this_elements_window_closed():
_error_popup_with_traceback('Error in Column.update - The window was closed')
return
if visible is False:
if self.TKColFrame:
self._pack_forget_save_settings()
if self.ParentPanedWindow:
self.ParentPanedWindow.remove(self.TKColFrame)
elif visible is True:
if self.TKColFrame:
self._pack_restore_settings()
if self.ParentPanedWindow:
self.ParentPanedWindow.add(self.TKColFrame)
if visible is not None:
self._visible = visible
def contents_changed(self):
"""
When a scrollable column has part of its layout changed by making elements visible or invisible or the
layout is extended for the Column, then this method needs to be called so that the new scroll area
is computed to match the new contents.
"""
self.TKColFrame.canvas.config(scrollregion=self.TKColFrame.canvas.bbox('all'))
AddRow = add_row
Layout = layout
Update = update
from FreeSimpleGUI.window import Window

View File

@ -0,0 +1,329 @@
from __future__ import annotations
import tkinter as tk
import tkinter.font
from tkinter import ttk
import FreeSimpleGUI
from FreeSimpleGUI import COLOR_SYSTEM_DEFAULT
from FreeSimpleGUI import ELEM_TYPE_INPUT_COMBO
from FreeSimpleGUI import Element
from FreeSimpleGUI import theme_button_color
from FreeSimpleGUI._utils import _error_popup_with_traceback
class Combo(Element):
"""
ComboBox Element - A combination of a single-line input and a drop-down menu. User can type in their own value or choose from list.
"""
def __init__(
self,
values,
default_value=None,
size=(None, None),
s=(None, None),
auto_size_text=None,
background_color=None,
text_color=None,
button_background_color=None,
button_arrow_color=None,
bind_return_key=False,
change_submits=False,
enable_events=False,
enable_per_char_events=None,
disabled=False,
key=None,
k=None,
pad=None,
p=None,
expand_x=False,
expand_y=False,
tooltip=None,
readonly=False,
font=None,
visible=True,
metadata=None,
):
"""
:param values: values to choose. While displayed as text, the items returned are what the caller supplied, not text
:type values: List[Any] or Tuple[Any]
:param default_value: Choice to be displayed as initial value. Must match one of values variable contents
:type default_value: (Any)
:param size: width, height. Width = characters-wide, height = NOTE it's the number of entries to show in the list. If an Int is passed rather than a tuple, then height is auto-set to 1 and width is value of the int
:type size: (int, int) | (None, None) | int
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int, int) | (None, None) | int
:param auto_size_text: True if element should be the same size as the contents
:type auto_size_text: (bool)
:param background_color: color of background
:type background_color: (str)
:param text_color: color of the text
:type text_color: (str)
:param button_background_color: The color of the background of the button on the combo box
:type button_background_color: (str)
:param button_arrow_color: The color of the arrow on the button on the combo box
:type button_arrow_color: (str)
:param bind_return_key: If True, then the return key will cause a the Combo to generate an event when return key is pressed
:type bind_return_key: (bool)
:param change_submits: DEPRICATED DO NOT USE. Use `enable_events` instead
:type change_submits: (bool)
:param enable_events: Turns on the element specific events. Combo event is when a choice is made
:type enable_events: (bool)
:param enable_per_char_events: Enables generation of events for every character that's input. This is like the Input element's events
:type enable_per_char_events: (bool)
:param disabled: set disable state for element
:type disabled: (bool)
:param key: Used with window.find_element and with return values to uniquely identify this element
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param expand_x: If True the element will automatically expand in the X direction to fill available space
:type expand_x: (bool)
:param expand_y: If True the element will automatically expand in the Y direction to fill available space
:type expand_y: (bool)
:param tooltip: text that will appear when mouse hovers over this element
:type tooltip: (str)
:param readonly: make element readonly (user can't change). True means user cannot change
:type readonly: (bool)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param visible: set visibility state of the element
:type visible: (bool)
:param metadata: User metadata that can be set to ANYTHING
:type metadata: (Any)
"""
self.Values = values
self.DefaultValue = default_value
self.ChangeSubmits = change_submits or enable_events
self.Widget = self.TKCombo = None # type: ttk.Combobox
self.Disabled = disabled
self.Readonly = readonly
self.BindReturnKey = bind_return_key
bg = background_color if background_color else FreeSimpleGUI.DEFAULT_INPUT_ELEMENTS_COLOR
fg = text_color if text_color is not None else FreeSimpleGUI.DEFAULT_INPUT_TEXT_COLOR
key = key if key is not None else k
sz = size if size != (None, None) else s
pad = pad if pad is not None else p
self.expand_x = expand_x
self.expand_y = expand_y
if button_background_color is None:
self.button_background_color = theme_button_color()[1]
else:
self.button_background_color = button_background_color
if button_arrow_color is None:
self.button_arrow_color = theme_button_color()[0]
else:
self.button_arrow_color = button_arrow_color
self.enable_per_char_events = enable_per_char_events
super().__init__(
ELEM_TYPE_INPUT_COMBO,
size=sz,
auto_size_text=auto_size_text,
background_color=bg,
text_color=fg,
key=key,
pad=pad,
tooltip=tooltip,
font=font or FreeSimpleGUI.DEFAULT_FONT,
visible=visible,
metadata=metadata,
)
def update(
self,
value=None,
values=None,
set_to_index=None,
disabled=None,
readonly=None,
font=None,
visible=None,
size=(None, None),
select=None,
text_color=None,
background_color=None,
):
"""
Changes some of the settings for the Combo Element. Must call `Window.Read` or `Window.Finalize` prior.
Note that the state can be in 3 states only.... enabled, disabled, readonly even
though more combinations are available. The easy way to remember is that if you
change the readonly parameter then you are enabling the element.
Changes will not be visible in your window until you call window.read or window.refresh.
If you change visibility, your element may MOVE. If you want it to remain stationary, use the "layout helper"
function "pin" to ensure your element is "pinned" to that location in your layout so that it returns there
when made visible.
:param value: change which value is current selected based on new list of previous list of choices
:type value: (Any)
:param values: change list of choices
:type values: List[Any]
:param set_to_index: change selection to a particular choice starting with index = 0
:type set_to_index: (int)
:param disabled: disable or enable state of the element
:type disabled: (bool)
:param readonly: if True make element readonly (user cannot change any choices). Enables the element if either choice are made.
:type readonly: (bool)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param visible: control visibility of element
:type visible: (bool)
:param size: width, height. Width = characters-wide, height = NOTE it's the number of entries to show in the list
:type size: (int, int)
:param select: if True, then the text will be selected, if False then selection will be cleared
:type select: (bool)
:param background_color: color of background
:type background_color: (str)
:param text_color: color of the text
:type text_color: (str)
"""
if size != (None, None):
if isinstance(size, int):
size = (size, 1)
if isinstance(size, tuple) and len(size) == 1:
size = (size[0], 1)
if not self._widget_was_created(): # if widget hasn't been created yet, then don't allow
return
if self._this_elements_window_closed():
_error_popup_with_traceback('Error in Combo.update - The window was closed')
return
if values is not None:
try:
self.TKCombo['values'] = values
# self.TKCombo.current(0) # don't set any value if a new set of values was made
except:
pass
self.Values = values
if value is None:
self.TKCombo.set('')
if size == (None, None):
max_line_len = max([len(str(line)) for line in self.Values]) if len(self.Values) else 0
if self.AutoSizeText is False:
width = self.Size[0]
else:
width = max_line_len + 1
self.TKCombo.configure(width=width)
else:
self.TKCombo.configure(height=size[1])
self.TKCombo.configure(width=size[0])
if value is not None:
if value not in self.Values:
self.TKCombo.set(value)
else:
for index, v in enumerate(self.Values):
if v == value:
try:
self.TKCombo.current(index)
except:
pass
self.DefaultValue = value
break
if set_to_index is not None:
try:
self.TKCombo.current(set_to_index)
self.DefaultValue = self.Values[set_to_index]
except:
pass
if readonly:
self.Readonly = True
self.TKCombo['state'] = 'readonly'
elif readonly is False:
self.Readonly = False
self.TKCombo['state'] = 'enable'
if disabled is True:
self.TKCombo['state'] = 'disable'
elif disabled is False and self.Readonly is True:
self.TKCombo['state'] = 'readonly'
elif disabled is False and self.Readonly is False:
self.TKCombo['state'] = 'enable'
self.Disabled = disabled if disabled is not None else self.Disabled
combostyle = self.ttk_style
style_name = self.ttk_style_name
if text_color is not None:
combostyle.configure(style_name, foreground=text_color)
combostyle.configure(style_name, selectforeground=text_color)
combostyle.configure(style_name, insertcolor=text_color)
combostyle.map(style_name, fieldforeground=[('readonly', text_color)])
self.TextColor = text_color
if background_color is not None:
combostyle.configure(style_name, selectbackground=background_color)
combostyle.map(style_name, fieldbackground=[('readonly', background_color)])
combostyle.configure(style_name, fieldbackground=background_color)
self.BackgroundColor = background_color
if self.Readonly is True:
if text_color not in (None, COLOR_SYSTEM_DEFAULT):
combostyle.configure(style_name, selectforeground=text_color)
if background_color not in (None, COLOR_SYSTEM_DEFAULT):
combostyle.configure(style_name, selectbackground=background_color)
if font is not None:
self.Font = font
self.TKCombo.configure(font=font)
self._dropdown_newfont = tkinter.font.Font(font=font)
self.ParentRowFrame.option_add('*TCombobox*Listbox*Font', self._dropdown_newfont)
# make tcl call to deal with colors for the drop-down formatting
try:
if self.BackgroundColor not in (None, COLOR_SYSTEM_DEFAULT) and self.TextColor not in (
None,
COLOR_SYSTEM_DEFAULT,
):
self.Widget.tk.eval(
'[ttk::combobox::PopdownWindow {}].f.l configure -foreground {} -background {} -selectforeground {} -selectbackground {} -font {}'.format(
self.Widget,
self.TextColor,
self.BackgroundColor,
self.BackgroundColor,
self.TextColor,
self._dropdown_newfont,
)
)
except Exception:
pass # going to let this one slide
if visible is False:
self._pack_forget_save_settings()
# self.TKCombo.pack_forget()
elif visible is True:
self._pack_restore_settings()
# self.TKCombo.pack(padx=self.pad_used[0], pady=self.pad_used[1])
if visible is not None:
self._visible = visible
if select is True:
self.TKCombo.select_range(0, tk.END)
elif select is False:
self.TKCombo.select_clear()
def get(self):
"""
Returns the current (right now) value of the Combo. DO NOT USE THIS AS THE NORMAL WAY OF READING A COMBO!
You should be using values from your call to window.read instead. Know what you're doing if you use it.
:return: Returns the value of what is currently chosen
:rtype: Any | None
"""
try:
if self.TKCombo.current() == -1: # if the current value was not in the original list
value = self.TKCombo.get() # then get the value typed in by user
else:
value = self.Values[self.TKCombo.current()] # get value from original list given index
except:
value = None # only would happen if user closes window
return value
Get = get
Update = update

View File

@ -0,0 +1,47 @@
from __future__ import annotations
from FreeSimpleGUI import ELEM_TYPE_ERROR
from FreeSimpleGUI.elements.base import Element
class ErrorElement(Element):
"""
A "dummy Element" that is returned when there are error conditions, like trying to find an element that's invalid
"""
def __init__(self, key=None, metadata=None):
"""
:param key: Used with window.find_element and with return values to uniquely identify this element
:type key:
"""
self.Key = key
super().__init__(ELEM_TYPE_ERROR, key=key, metadata=metadata)
def update(self, silent_on_error=True, *args, **kwargs):
"""
Update method for the Error Element, an element that should not be directly used by developer
:param silent_on_error: if False, then a Popup window will be shown
:type silent_on_error: (bool)
:param *args: meant to "soak up" any normal parameters passed in
:type *args: (Any)
:param **kwargs: meant to "soak up" any keyword parameters that were passed in
:type **kwargs: (Any)
:return: returns 'self' so call can be chained
:rtype: (ErrorElement)
"""
print('** Your update is being ignored because you supplied a bad key earlier **')
return self
def get(self):
"""
One of the method names found in other Elements. Used here to return an error string in case it's called
:return: A warning text string.
:rtype: (str)
"""
return 'This is NOT a valid Element!\nSTOP trying to do things with it or I will have to crash at some point!'
Get = get
Update = update

View File

@ -0,0 +1,273 @@
from __future__ import annotations
import tkinter as tk
import warnings
import FreeSimpleGUI
from FreeSimpleGUI import _random_error_emoji
from FreeSimpleGUI import ELEM_TYPE_FRAME
from FreeSimpleGUI import Element
from FreeSimpleGUI import popup_error
from FreeSimpleGUI._utils import _error_popup_with_traceback
from FreeSimpleGUI.window import Window
class Frame(Element):
"""
A Frame Element that contains other Elements. Encloses with a line around elements and a text label.
"""
def __init__(
self,
title,
layout,
title_color=None,
background_color=None,
title_location=None,
relief=FreeSimpleGUI.DEFAULT_FRAME_RELIEF,
size=(None, None),
s=(None, None),
font=None,
pad=None,
p=None,
border_width=None,
key=None,
k=None,
tooltip=None,
right_click_menu=None,
expand_x=False,
expand_y=False,
grab=None,
visible=True,
element_justification='left',
vertical_alignment=None,
metadata=None,
):
"""
:param title: text that is displayed as the Frame's "label" or title
:type title: (str)
:param layout: The layout to put inside the Frame
:type layout: List[List[Elements]]
:param title_color: color of the title text
:type title_color: (str)
:param background_color: background color of the Frame
:type background_color: (str)
:param title_location: location to place the text title. Choices include: TITLE_LOCATION_TOP TITLE_LOCATION_BOTTOM TITLE_LOCATION_LEFT TITLE_LOCATION_RIGHT TITLE_LOCATION_TOP_LEFT TITLE_LOCATION_TOP_RIGHT TITLE_LOCATION_BOTTOM_LEFT TITLE_LOCATION_BOTTOM_RIGHT
:type title_location: (enum)
:param relief: relief style. Values are same as other elements with reliefs. Choices include RELIEF_RAISED RELIEF_SUNKEN RELIEF_FLAT RELIEF_RIDGE RELIEF_GROOVE RELIEF_SOLID
:type relief: (enum)
:param size: (width, height) Sets an initial hard-coded size for the Frame. This used to be a problem, but was fixed in 4.53.0 and works better than Columns when using the size paramter
:type size: (int, int)
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int, int) | (None, None) | int
:param font: specifies the font family, size, etc. for the TITLE. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param border_width: width of border around element in pixels
:type border_width: (int)
:param key: Value that uniquely identifies this element from all other elements. Used when Finding an element or in return values. Must be unique to the window
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param tooltip: text, that will appear when mouse hovers over the element
:type tooltip: (str)
:param right_click_menu: A list of lists of Menu items to show when this element is right clicked. See user docs for exact format.
:type right_click_menu: List[List[ List[str] | str ]]
:param expand_x: If True the element will automatically expand in the X direction to fill available space
:type expand_x: (bool)
:param expand_y: If True the element will automatically expand in the Y direction to fill available space
:type expand_y: (bool)
:param grab: If True can grab this element and move the window around. Default is False
:type grab: (bool)
:param visible: set visibility state of the element
:type visible: (bool)
:param element_justification: All elements inside the Frame will have this justification 'left', 'right', 'center' are valid values
:type element_justification: (str)
:param vertical_alignment: Place the Frame at the 'top', 'center', 'bottom' of the row (can also use t,c,r). Defaults to no setting (tkinter decides)
:type vertical_alignment: (str)
:param metadata: User metadata that can be set to ANYTHING
:type metadata: (Any)
"""
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.Title = title
self.Relief = relief
self.TitleLocation = title_location
self.BorderWidth = border_width
self.BackgroundColor = background_color if background_color is not None else FreeSimpleGUI.DEFAULT_BACKGROUND_COLOR
self.RightClickMenu = right_click_menu
self.ContainerElemementNumber = Window._GetAContainerNumber()
self.ElementJustification = element_justification
self.VerticalAlignment = vertical_alignment
self.Widget = None # type: tk.LabelFrame
self.Grab = grab
self.Layout(layout)
key = key if key is not None else k
sz = size if size != (None, None) else s
pad = pad if pad is not None else p
self.expand_x = expand_x
self.expand_y = expand_y
super().__init__(
ELEM_TYPE_FRAME,
background_color=background_color,
text_color=title_color,
size=sz,
font=font,
pad=pad,
key=key,
tooltip=tooltip,
visible=visible,
metadata=metadata,
)
return
def add_row(self, *args):
"""
Not recommended user call. Used to add rows of Elements to the Frame Element.
:param *args: The list of elements for this row
:type *args: List[Element]
"""
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
if type(element) is list:
popup_error(
'Error creating Frame layout',
'Layout has a LIST instead of an ELEMENT',
'This sometimes means you have a badly placed ]',
'The offensive list is:',
element,
'This list will be stripped from your layout',
keep_on_top=True,
)
continue
elif callable(element) and not isinstance(element, Element):
popup_error(
'Error creating Frame layout',
'Layout has a FUNCTION instead of an ELEMENT',
'This likely means you are missing () from your layout',
'The offensive list is:',
element,
'This item will be stripped from your layout',
keep_on_top=True,
)
continue
if element.ParentContainer is not None:
warnings.warn(
'*** YOU ARE ATTEMPTING TO REUSE AN ELEMENT IN YOUR LAYOUT! Once placed in a layout, an element cannot be used in another layout. ***',
UserWarning,
)
_error_popup_with_traceback(
'Error creating Frame layout',
'The layout specified has already been used',
'You MUST start witha "clean", unused layout every time you create a window',
'The offensive Element = ',
element,
'and has a key = ',
element.Key,
'This item will be stripped from your layout',
'Hint - try printing your layout and matching the IDs "print(layout)"',
)
continue
element.Position = (CurrentRowNumber, i)
element.ParentContainer = self
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):
"""
Can use like the Window.Layout method, but it's better to use the layout parameter when creating
:param rows: The rows of Elements
:type rows: List[List[Element]]
:return: Used for chaining
:rtype: (Frame)
"""
for row in rows:
try:
iter(row)
except TypeError:
popup_error(
'Error creating Frame layout',
'Your row is not an iterable (e.g. a list)',
f'Instead of a list, the type found was {type(row)}',
'The offensive row = ',
row,
'This item will be stripped from your layout',
keep_on_top=True,
image=_random_error_emoji(),
)
continue
self.AddRow(*row)
return self
def _GetElementAtLocation(self, location):
"""
Not user callable. Used to find the Element at a row, col position within the layout
:param location: (row, column) position of the element to find in layout
:type location: (int, int)
:return: (Element) The element found at the location
:rtype: (Element)
"""
(row_num, col_num) = location
row = self.Rows[row_num]
element = row[col_num]
return element
def update(self, value=None, visible=None):
"""
Changes some of the settings for the Frame Element. Must call `Window.Read` or `Window.Finalize` prior
Changes will not be visible in your window until you call window.read or window.refresh.
If you change visibility, your element may MOVE. If you want it to remain stationary, use the "layout helper"
function "pin" to ensure your element is "pinned" to that location in your layout so that it returns there
when made visible.
:param value: New text value to show on frame
:type value: (Any)
:param visible: control visibility of element
:type visible: (bool)
"""
if not self._widget_was_created(): # if widget hasn't been created yet, then don't allow
return
if self._this_elements_window_closed():
_error_popup_with_traceback('Error in Frame.update - The window was closed')
return
if visible is False:
self._pack_forget_save_settings()
# self.TKFrame.pack_forget()
elif visible is True:
self._pack_restore_settings()
# self.TKFrame.pack(padx=self.pad_used[0], pady=self.pad_used[1])
if value is not None:
self.TKFrame.config(text=str(value))
if visible is not None:
self._visible = visible
AddRow = add_row
Layout = layout
Update = update

View File

@ -0,0 +1,817 @@
from __future__ import annotations
import tkinter as tk
from math import floor
from FreeSimpleGUI import COLOR_SYSTEM_DEFAULT
from FreeSimpleGUI import ELEM_TYPE_GRAPH
from FreeSimpleGUI import Element
from FreeSimpleGUI import TEXT_LOCATION_CENTER
from FreeSimpleGUI._utils import _error_popup_with_traceback
from FreeSimpleGUI._utils import _exit_mainloop
class Graph(Element):
"""
Creates an area for you to draw on. The MAGICAL property this Element has is that you interact
with the element using your own coordinate system. This is an important point!! YOU define where the location
is for (0,0). Want (0,0) to be in the middle of the graph like a math 4-quadrant graph? No problem! Set your
lower left corner to be (-100,-100) and your upper right to be (100,100) and you've got yourself a graph with
(0,0) at the center.
One of THE coolest of the Elements.
You can also use float values. To do so, be sure and set the float_values parameter.
Mouse click and drag events are possible and return the (x,y) coordinates of the mouse
Drawing primitives return an "id" that is referenced when you want to operation on that item (e.g. to erase it)
"""
def __init__(
self,
canvas_size,
graph_bottom_left,
graph_top_right,
background_color=None,
pad=None,
p=None,
change_submits=False,
drag_submits=False,
enable_events=False,
motion_events=False,
key=None,
k=None,
tooltip=None,
right_click_menu=None,
expand_x=False,
expand_y=False,
visible=True,
float_values=False,
border_width=0,
metadata=None,
):
"""
:param canvas_size: size of the canvas area in pixels
:type canvas_size: (int, int)
:param graph_bottom_left: (x,y) The bottoms left corner of your coordinate system
:type graph_bottom_left: (int, int)
:param graph_top_right: (x,y) The top right corner of your coordinate system
:type graph_top_right: (int, int)
:param background_color: background color of the drawing area
:type background_color: (str)
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param change_submits: * DEPRICATED DO NOT USE. Use `enable_events` instead
:type change_submits: (bool)
:param drag_submits: if True and Events are enabled for the Graph, will report Events any time the mouse moves while button down. When the mouse button is released, you'll get an event = graph key + '+UP' (if key is a string.. if not a string, it'll be made into a tuple)
:type drag_submits: (bool)
:param enable_events: If True then clicks on the Graph are immediately reported as an event. Use this instead of change_submits
:type enable_events: (bool)
:param motion_events: If True then if no button is down and the mouse is moved, an event is generated with key = graph key + '+MOVE' (if key is a string, it not a string then a tuple is returned)
:type motion_events: (bool)
:param key: Value that uniquely identifies this element from all other elements. Used when Finding an element or in return values. Must be unique to the window
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param tooltip: text, that will appear when mouse hovers over the element
:type tooltip: (str)
:param right_click_menu: A list of lists of Menu items to show when this element is right clicked. See user docs for exact format.
:type right_click_menu: List[List[ List[str] | str ]]
:param expand_x: If True the element will automatically expand in the X direction to fill available space
:type expand_x: (bool)
:param expand_y: If True the element will automatically expand in the Y direction to fill available space
:type expand_y: (bool)
:param visible: set visibility state of the element (Default = True)
:type visible: (bool)
:param float_values: If True x,y coordinates are returned as floats, not ints
:type float_values: (bool)
:param border_width: width of border around element in pixels. Not normally used for Graph Elements
:type border_width: (int)
:param metadata: User metadata that can be set to ANYTHING
:type metadata: (Any)
"""
self.CanvasSize = canvas_size
self.BottomLeft = graph_bottom_left
self.TopRight = graph_top_right
# self._TKCanvas = None # type: tk.Canvas
self._TKCanvas2 = self.Widget = None # type: tk.Canvas
self.ChangeSubmits = change_submits or enable_events
self.DragSubmits = drag_submits
self.ClickPosition = (None, None)
self.MouseButtonDown = False
self.Images = {}
self.RightClickMenu = right_click_menu
self.FloatValues = float_values
self.BorderWidth = border_width
key = key if key is not None else k
pad = pad if pad is not None else p
self.expand_x = expand_x
self.expand_y = expand_y
self.motion_events = motion_events
super().__init__(
ELEM_TYPE_GRAPH,
background_color=background_color,
size=canvas_size,
pad=pad,
key=key,
tooltip=tooltip,
visible=visible,
metadata=metadata,
)
return
def _convert_xy_to_canvas_xy(self, x_in, y_in):
"""
Not user callable. Used to convert user's coordinates into the ones used by tkinter
:param x_in: The x coordinate to convert
:type x_in: int | float
:param y_in: The y coordinate to convert
:type y_in: int | float
:return: (int, int) The converted canvas coordinates
:rtype: (int, int)
"""
if None in (x_in, y_in):
return None, None
try:
scale_x = (self.CanvasSize[0] - 0) / (self.TopRight[0] - self.BottomLeft[0])
scale_y = (0 - self.CanvasSize[1]) / (self.TopRight[1] - self.BottomLeft[1])
except:
scale_x = scale_y = 0
new_x = 0 + scale_x * (x_in - self.BottomLeft[0])
new_y = self.CanvasSize[1] + scale_y * (y_in - self.BottomLeft[1])
return new_x, new_y
def _convert_canvas_xy_to_xy(self, x_in, y_in):
"""
Not user callable. Used to convert tkinter Canvas coords into user's coordinates
:param x_in: The x coordinate in canvas coordinates
:type x_in: (int)
:param y_in: (int) The y coordinate in canvas coordinates
:type y_in:
:return: The converted USER coordinates
:rtype: (int, int) | Tuple[float, float]
"""
if None in (x_in, y_in):
return None, None
scale_x = (self.CanvasSize[0] - 0) / (self.TopRight[0] - self.BottomLeft[0])
scale_y = (0 - self.CanvasSize[1]) / (self.TopRight[1] - self.BottomLeft[1])
new_x = x_in / scale_x + self.BottomLeft[0]
new_y = (y_in - self.CanvasSize[1]) / scale_y + self.BottomLeft[1]
if self.FloatValues:
return new_x, new_y
else:
return floor(new_x), floor(new_y)
def draw_line(self, point_from, point_to, color='black', width=1):
"""
Draws a line from one point to another point using USER'S coordinates. Can set the color and width of line
:param point_from: Starting point for line
:type point_from: (int, int) | Tuple[float, float]
:param point_to: Ending point for line
:type point_to: (int, int) | Tuple[float, float]
:param color: Color of the line
:type color: (str)
:param width: width of line in pixels
:type width: (int)
:return: id returned from tktiner or None if user closed the window. id is used when you
:rtype: int | None
"""
if point_from == (None, None):
return
converted_point_from = self._convert_xy_to_canvas_xy(point_from[0], point_from[1])
converted_point_to = self._convert_xy_to_canvas_xy(point_to[0], point_to[1])
if self._TKCanvas2 is None:
print('*** WARNING - The Graph element has not been finalized and cannot be drawn upon ***')
print('Call Window.Finalize() prior to this operation')
return None
try: # in case window was closed with an X
id = self._TKCanvas2.create_line(converted_point_from, converted_point_to, width=width, fill=color)
except:
id = None
return id
def draw_lines(self, points, color='black', width=1):
"""
Draw a series of lines given list of points
:param points: list of points that define the polygon
:type points: List[(int, int) | Tuple[float, float]]
:param color: Color of the line
:type color: (str)
:param width: width of line in pixels
:type width: (int)
:return: id returned from tktiner or None if user closed the window. id is used when you
:rtype: int | None
"""
converted_points = [self._convert_xy_to_canvas_xy(point[0], point[1]) for point in points]
try: # in case window was closed with an X
id = self._TKCanvas2.create_line(*converted_points, width=width, fill=color)
except:
if self._TKCanvas2 is None:
print('*** WARNING - The Graph element has not been finalized and cannot be drawn upon ***')
print('Call Window.Finalize() prior to this operation')
id = None
return id
def draw_point(self, point, size=2, color='black'):
"""
Draws a "dot" at the point you specify using the USER'S coordinate system
:param point: Center location using USER'S coordinate system
:type point: (int, int) | Tuple[float, float]
:param size: Radius? (Or is it the diameter?) in user's coordinate values.
:type size: int | float
:param color: color of the point to draw
:type color: (str)
:return: id returned from tkinter that you'll need if you want to manipulate the point
:rtype: int | None
"""
if point == (None, None):
return
converted_point = self._convert_xy_to_canvas_xy(point[0], point[1])
size_converted = self._convert_xy_to_canvas_xy(point[0] + size, point[1])
size = size_converted[0] - converted_point[0]
if self._TKCanvas2 is None:
print('*** WARNING - The Graph element has not been finalized and cannot be drawn upon ***')
print('Call Window.Finalize() prior to this operation')
return None
try: # needed in case window was closed with an X
point1 = converted_point[0] - size // 2, converted_point[1] - size // 2
point2 = converted_point[0] + size // 2, converted_point[1] + size // 2
id = self._TKCanvas2.create_oval(point1[0], point1[1], point2[0], point2[1], width=0, fill=color, outline=color)
except:
id = None
return id
def draw_circle(self, center_location, radius, fill_color=None, line_color='black', line_width=1):
"""
Draws a circle, cenetered at the location provided. Can set the fill and outline colors
:param center_location: Center location using USER'S coordinate system
:type center_location: (int, int) | Tuple[float, float]
:param radius: Radius in user's coordinate values.
:type radius: int | float
:param fill_color: color of the point to draw
:type fill_color: (str)
:param line_color: color of the outer line that goes around the circle (sorry, can't set thickness)
:type line_color: (str)
:param line_width: width of the line around the circle, the outline, in pixels
:type line_width: (int)
:return: id returned from tkinter that you'll need if you want to manipulate the circle
:rtype: int | None
"""
if center_location == (None, None):
return
converted_point = self._convert_xy_to_canvas_xy(center_location[0], center_location[1])
radius_converted = self._convert_xy_to_canvas_xy(center_location[0] + radius, center_location[1])
radius = radius_converted[0] - converted_point[0]
# radius = radius_converted[1]-5
if self._TKCanvas2 is None:
print('*** WARNING - The Graph element has not been finalized and cannot be drawn upon ***')
print('Call Window.Finalize() prior to this operation')
return None
try: # needed in case the window was closed with an X
id = self._TKCanvas2.create_oval(
int(converted_point[0]) - int(radius),
int(converted_point[1]) - int(radius),
int(converted_point[0]) + int(radius),
int(converted_point[1]) + int(radius),
fill=fill_color,
outline=line_color,
width=line_width,
)
except:
id = None
return id
def draw_oval(self, top_left, bottom_right, fill_color=None, line_color=None, line_width=1):
"""
Draws an oval based on coordinates in user coordinate system. Provide the location of a "bounding rectangle"
:param top_left: the top left point of bounding rectangle
:type top_left: (int, int) | Tuple[float, float]
:param bottom_right: the bottom right point of bounding rectangle
:type bottom_right: (int, int) | Tuple[float, float]
:param fill_color: color of the interrior
:type fill_color: (str)
:param line_color: color of outline of oval
:type line_color: (str)
:param line_width: width of the line around the oval, the outline, in pixels
:type line_width: (int)
:return: id returned from tkinter that you'll need if you want to manipulate the oval
:rtype: int | None
"""
converted_top_left = self._convert_xy_to_canvas_xy(top_left[0], top_left[1])
converted_bottom_right = self._convert_xy_to_canvas_xy(bottom_right[0], bottom_right[1])
if self._TKCanvas2 is None:
print('*** WARNING - The Graph element has not been finalized and cannot be drawn upon ***')
print('Call Window.Finalize() prior to this operation')
return None
try: # in case windows close with X
id = self._TKCanvas2.create_oval(
converted_top_left[0],
converted_top_left[1],
converted_bottom_right[0],
converted_bottom_right[1],
fill=fill_color,
outline=line_color,
width=line_width,
)
except:
id = None
return id
def draw_arc(self, top_left, bottom_right, extent, start_angle, style=None, arc_color='black', line_width=1, fill_color=None):
"""
Draws different types of arcs. Uses a "bounding box" to define location
:param top_left: the top left point of bounding rectangle
:type top_left: (int, int) | Tuple[float, float]
:param bottom_right: the bottom right point of bounding rectangle
:type bottom_right: (int, int) | Tuple[float, float]
:param extent: Andle to end drawing. Used in conjunction with start_angle
:type extent: (float)
:param start_angle: Angle to begin drawing. Used in conjunction with extent
:type start_angle: (float)
:param style: Valid choices are One of these Style strings- 'pieslice', 'chord', 'arc', 'first', 'last', 'butt', 'projecting', 'round', 'bevel', 'miter'
:type style: (str)
:param arc_color: color to draw arc with
:type arc_color: (str)
:param fill_color: color to fill the area
:type fill_color: (str)
:return: id returned from tkinter that you'll need if you want to manipulate the arc
:rtype: int | None
"""
converted_top_left = self._convert_xy_to_canvas_xy(top_left[0], top_left[1])
converted_bottom_right = self._convert_xy_to_canvas_xy(bottom_right[0], bottom_right[1])
tkstyle = tk.PIESLICE if style is None else style
if self._TKCanvas2 is None:
print('*** WARNING - The Graph element has not been finalized and cannot be drawn upon ***')
print('Call Window.Finalize() prior to this operation')
return None
try: # in case closed with X
id = self._TKCanvas2.create_arc(
converted_top_left[0],
converted_top_left[1],
converted_bottom_right[0],
converted_bottom_right[1],
extent=extent,
start=start_angle,
style=tkstyle,
outline=arc_color,
width=line_width,
fill=fill_color,
)
except Exception as e:
print('Error encountered drawing arc.', e)
id = None
return id
def draw_rectangle(self, top_left, bottom_right, fill_color=None, line_color=None, line_width=None):
"""
Draw a rectangle given 2 points. Can control the line and fill colors
:param top_left: the top left point of rectangle
:type top_left: (int, int) | Tuple[float, float]
:param bottom_right: the bottom right point of rectangle
:type bottom_right: (int, int) | Tuple[float, float]
:param fill_color: color of the interior
:type fill_color: (str)
:param line_color: color of outline
:type line_color: (str)
:param line_width: width of the line in pixels
:type line_width: (int)
:return: int | None id returned from tkinter that you'll need if you want to manipulate the rectangle
:rtype: int | None
"""
converted_top_left = self._convert_xy_to_canvas_xy(top_left[0], top_left[1])
converted_bottom_right = self._convert_xy_to_canvas_xy(bottom_right[0], bottom_right[1])
if self._TKCanvas2 is None:
print('*** WARNING - The Graph element has not been finalized and cannot be drawn upon ***')
print('Call Window.Finalize() prior to this operation')
return None
if line_width is None:
line_width = 1
try: # in case closed with X
id = self._TKCanvas2.create_rectangle(
converted_top_left[0],
converted_top_left[1],
converted_bottom_right[0],
converted_bottom_right[1],
fill=fill_color,
outline=line_color,
width=line_width,
)
except:
id = None
return id
def draw_polygon(self, points, fill_color=None, line_color=None, line_width=None):
"""
Draw a polygon given list of points
:param points: list of points that define the polygon
:type points: List[(int, int) | Tuple[float, float]]
:param fill_color: color of the interior
:type fill_color: (str)
:param line_color: color of outline
:type line_color: (str)
:param line_width: width of the line in pixels
:type line_width: (int)
:return: id returned from tkinter that you'll need if you want to manipulate the rectangle
:rtype: int | None
"""
converted_points = [self._convert_xy_to_canvas_xy(point[0], point[1]) for point in points]
if self._TKCanvas2 is None:
print('*** WARNING - The Graph element has not been finalized and cannot be drawn upon ***')
print('Call Window.Finalize() prior to this operation')
return None
try: # in case closed with X
id = self._TKCanvas2.create_polygon(converted_points, fill=fill_color, outline=line_color, width=line_width)
except:
id = None
return id
def draw_text(self, text, location, color='black', font=None, angle=0, text_location=TEXT_LOCATION_CENTER):
"""
Draw some text on your graph. This is how you label graph number lines for example
:param text: text to display
:type text: (Any)
:param location: location to place first letter
:type location: (int, int) | Tuple[float, float]
:param color: text color
:type color: (str)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param angle: Angle 0 to 360 to draw the text. Zero represents horizontal text
:type angle: (float)
:param text_location: "anchor" location for the text. Values start with TEXT_LOCATION_
:type text_location: (enum)
:return: id returned from tkinter that you'll need if you want to manipulate the text
:rtype: int | None
"""
text = str(text)
if location == (None, None):
return
converted_point = self._convert_xy_to_canvas_xy(location[0], location[1])
if self._TKCanvas2 is None:
print('*** WARNING - The Graph element has not been finalized and cannot be drawn upon ***')
print('Call Window.Finalize() prior to this operation')
return None
try: # in case closed with X
id = self._TKCanvas2.create_text(
converted_point[0],
converted_point[1],
text=text,
font=font,
fill=color,
angle=angle,
anchor=text_location,
)
except:
id = None
return id
def draw_image(self, filename=None, data=None, location=(None, None)):
"""
Places an image onto your canvas. It's a really important method for this element as it enables so much
:param filename: if image is in a file, path and filename for the image. (GIF and PNG only!)
:type filename: (str)
:param data: if image is in Base64 format or raw? format then use instead of filename
:type data: str | bytes
:param location: the (x,y) location to place image's top left corner
:type location: (int, int) | Tuple[float, float]
:return: id returned from tkinter that you'll need if you want to manipulate the image
:rtype: int | None
"""
if location == (None, None):
return
if filename is not None:
image = tk.PhotoImage(file=filename)
elif data is not None:
# if type(data) is bytes:
try:
image = tk.PhotoImage(data=data)
except:
return None # an error likely means the window has closed so exit
converted_point = self._convert_xy_to_canvas_xy(location[0], location[1])
if self._TKCanvas2 is None:
print('*** WARNING - The Graph element has not been finalized and cannot be drawn upon ***')
print('Call Window.Finalize() prior to this operation')
return None
try: # in case closed with X
id = self._TKCanvas2.create_image(converted_point, image=image, anchor=tk.NW)
self.Images[id] = image
except:
id = None
return id
def erase(self):
"""
Erase the Graph - Removes all figures previously "drawn" using the Graph methods (e.g. DrawText)
"""
if self._TKCanvas2 is None:
print('*** WARNING - The Graph element has not been finalized and cannot be drawn upon ***')
print('Call Window.Finalize() prior to this operation')
return None
self.Images = {}
try: # in case window was closed with X
self._TKCanvas2.delete('all')
except:
pass
def delete_figure(self, id):
"""
Remove from the Graph the figure represented by id. The id is given to you anytime you call a drawing primitive
:param id: the id returned to you when calling one of the drawing methods
:type id: (int)
"""
try:
self._TKCanvas2.delete(id)
except:
print(f'DeleteFigure - bad ID {id}')
try:
del self.Images[id] # in case was an image. If wasn't an image, then will get exception
except:
pass
def update(self, background_color=None, visible=None):
"""
Changes some of the settings for the Graph Element. Must call `Window.Read` or `Window.Finalize` prior
Changes will not be visible in your window until you call window.read or window.refresh.
If you change visibility, your element may MOVE. If you want it to remain stationary, use the "layout helper"
function "pin" to ensure your element is "pinned" to that location in your layout so that it returns there
when made visible.
:param background_color: color of background
:type background_color: ???
:param visible: control visibility of element
:type visible: (bool)
"""
if not self._widget_was_created(): # if widget hasn't been created yet, then don't allow
return
if self._this_elements_window_closed():
_error_popup_with_traceback('Error in Graph.update - The window was closed')
return
if background_color is not None and background_color != COLOR_SYSTEM_DEFAULT:
self._TKCanvas2.configure(background=background_color)
if visible is False:
self._pack_forget_save_settings()
elif visible is True:
self._pack_restore_settings()
if visible is not None:
self._visible = visible
def move(self, x_direction, y_direction):
"""
Moves the entire drawing area (the canvas) by some delta from the current position. Units are indicated in your coordinate system indicated number of ticks in your coordinate system
:param x_direction: how far to move in the "X" direction in your coordinates
:type x_direction: int | float
:param y_direction: how far to move in the "Y" direction in your coordinates
:type y_direction: int | float
"""
zero_converted = self._convert_xy_to_canvas_xy(0, 0)
shift_converted = self._convert_xy_to_canvas_xy(x_direction, y_direction)
shift_amount = (shift_converted[0] - zero_converted[0], shift_converted[1] - zero_converted[1])
if self._TKCanvas2 is None:
print('*** WARNING - The Graph element has not been finalized and cannot be drawn upon ***')
print('Call Window.Finalize() prior to this operation')
return None
self._TKCanvas2.move('all', shift_amount[0], shift_amount[1])
def move_figure(self, figure, x_direction, y_direction):
"""
Moves a previously drawn figure using a "delta" from current position
:param figure: Previously obtained figure-id. These are returned from all Draw methods
:type figure: (id)
:param x_direction: delta to apply to position in the X direction
:type x_direction: int | float
:param y_direction: delta to apply to position in the Y direction
:type y_direction: int | float
"""
zero_converted = self._convert_xy_to_canvas_xy(0, 0)
shift_converted = self._convert_xy_to_canvas_xy(x_direction, y_direction)
shift_amount = (shift_converted[0] - zero_converted[0], shift_converted[1] - zero_converted[1])
if figure is None:
print('* move_figure warning - your figure is None *')
return None
self._TKCanvas2.move(figure, shift_amount[0], shift_amount[1])
def relocate_figure(self, figure, x, y):
"""
Move a previously made figure to an arbitrary (x,y) location. This differs from the Move methods because it
uses absolute coordinates versus relative for Move
:param figure: Previously obtained figure-id. These are returned from all Draw methods
:type figure: (id)
:param x: location on X axis (in user coords) to move the upper left corner of the figure
:type x: int | float
:param y: location on Y axis (in user coords) to move the upper left corner of the figure
:type y: int | float
"""
# zero_converted = self._convert_xy_to_canvas_xy(0, 0)
shift_converted = self._convert_xy_to_canvas_xy(x, y)
# shift_amount = (shift_converted[0] - zero_converted[0], shift_converted[1] - zero_converted[1])
if figure is None:
print('*** WARNING - Your figure is None. It most likely means your did not Finalize your Window ***')
print('Call Window.Finalize() prior to all graph operations')
return None
xy = self._TKCanvas2.coords(figure)
self._TKCanvas2.move(figure, shift_converted[0] - xy[0], shift_converted[1] - xy[1])
def send_figure_to_back(self, figure):
"""
Changes Z-order of figures on the Graph. Sends the indicated figure to the back of all other drawn figures
:param figure: value returned by tkinter when creating the figure / drawing
:type figure: (int)
"""
self.TKCanvas.tag_lower(figure) # move figure to the "bottom" of all other figure
def bring_figure_to_front(self, figure):
"""
Changes Z-order of figures on the Graph. Brings the indicated figure to the front of all other drawn figures
:param figure: value returned by tkinter when creating the figure / drawing
:type figure: (int)
"""
self.TKCanvas.tag_raise(figure) # move figure to the "top" of all other figures
def get_figures_at_location(self, location):
"""
Returns a list of figures located at a particular x,y location within the Graph
:param location: point to check
:type location: (int, int) | Tuple[float, float]
:return: a list of previously drawn "Figures" (returned from the drawing primitives)
:rtype: List[int]
"""
x, y = self._convert_xy_to_canvas_xy(location[0], location[1])
ids = self.TKCanvas.find_overlapping(x, y, x, y)
return ids
def get_bounding_box(self, figure):
"""
Given a figure, returns the upper left and lower right bounding box coordinates
:param figure: a previously drawing figure
:type figure: object
:return: upper left x, upper left y, lower right x, lower right y
:rtype: Tuple[int, int, int, int] | Tuple[float, float, float, float]
"""
box = self.TKCanvas.bbox(figure)
top_left = self._convert_canvas_xy_to_xy(box[0], box[1])
bottom_right = self._convert_canvas_xy_to_xy(box[2], box[3])
return top_left, bottom_right
def change_coordinates(self, graph_bottom_left, graph_top_right):
"""
Changes the corrdinate system to a new one. The same 2 points in space are used to define the coorinate
system - the bottom left and the top right values of your graph.
:param graph_bottom_left: The bottoms left corner of your coordinate system
:type graph_bottom_left: (int, int) (x,y)
:param graph_top_right: The top right corner of your coordinate system
:type graph_top_right: (int, int) (x,y)
"""
self.BottomLeft = graph_bottom_left
self.TopRight = graph_top_right
@property
def tk_canvas(self):
"""
Returns the underlying tkiner Canvas widget
:return: The tkinter canvas widget
:rtype: (tk.Canvas)
"""
if self._TKCanvas2 is None:
print('*** Did you forget to call Finalize()? Your code should look something like: ***')
print('*** form = sg.Window("My Form").Layout(layout).Finalize() ***')
return self._TKCanvas2
# button release callback
def button_release_call_back(self, event):
"""
Not a user callable method. Used to get Graph click events. Called by tkinter when button is released
:param event: (event) event info from tkinter. Note not used in this method
:type event:
"""
if not self.DragSubmits:
return # only report mouse up for drag operations
self.ClickPosition = self._convert_canvas_xy_to_xy(event.x, event.y)
self.ParentForm.LastButtonClickedWasRealtime = False
if self.Key is not None:
self.ParentForm.LastButtonClicked = self.Key
else:
self.ParentForm.LastButtonClicked = '__GRAPH__' # need to put something rather than None
_exit_mainloop(self.ParentForm)
if isinstance(self.ParentForm.LastButtonClicked, str):
self.ParentForm.LastButtonClicked = self.ParentForm.LastButtonClicked + '+UP'
else:
self.ParentForm.LastButtonClicked = (self.ParentForm.LastButtonClicked, '+UP')
self.MouseButtonDown = False
# button callback
def button_press_call_back(self, event):
"""
Not a user callable method. Used to get Graph click events. Called by tkinter when button is released
:param event: (event) event info from tkinter. Contains the x and y coordinates of a click
:type event:
"""
self.ClickPosition = self._convert_canvas_xy_to_xy(event.x, event.y)
self.ParentForm.LastButtonClickedWasRealtime = self.DragSubmits
if self.Key is not None:
self.ParentForm.LastButtonClicked = self.Key
else:
self.ParentForm.LastButtonClicked = '__GRAPH__' # need to put something rather than None
_exit_mainloop(self.ParentForm)
self.MouseButtonDown = True
def _update_position_for_returned_values(self, event):
"""
Updates the variable that's used when the values dictionary is returned from a window read.
Not called by the user. It's called from another method/function that tkinter calledback
:param event: (event) event info from tkinter. Contains the x and y coordinates of a click
:type event:
"""
"""
Updates the variable that's used when the values dictionary is returned from a window read.
Not called by the user. It's called from another method/function that tkinter calledback
:param event: (event) event info from tkinter. Contains the x and y coordinates of a click
:type event:
"""
self.ClickPosition = self._convert_canvas_xy_to_xy(event.x, event.y)
# button callback
def motion_call_back(self, event):
"""
Not a user callable method. Used to get Graph mouse motion events. Called by tkinter when mouse moved
:param event: (event) event info from tkinter. Contains the x and y coordinates of a mouse
:type event:
"""
if not self.MouseButtonDown and not self.motion_events:
return
self.ClickPosition = self._convert_canvas_xy_to_xy(event.x, event.y)
self.ParentForm.LastButtonClickedWasRealtime = self.DragSubmits
if self.Key is not None:
self.ParentForm.LastButtonClicked = self.Key
else:
self.ParentForm.LastButtonClicked = '__GRAPH__' # need to put something rather than None
if self.motion_events and not self.MouseButtonDown:
if isinstance(self.ParentForm.LastButtonClicked, str):
self.ParentForm.LastButtonClicked = self.ParentForm.LastButtonClicked + '+MOVE'
else:
self.ParentForm.LastButtonClicked = (self.ParentForm.LastButtonClicked, '+MOVE')
_exit_mainloop(self.ParentForm)
BringFigureToFront = bring_figure_to_front
ButtonPressCallBack = button_press_call_back
ButtonReleaseCallBack = button_release_call_back
DeleteFigure = delete_figure
DrawArc = draw_arc
DrawCircle = draw_circle
DrawImage = draw_image
DrawLine = draw_line
DrawOval = draw_oval
DrawPoint = draw_point
DrawPolygon = draw_polygon
DrawLines = draw_lines
DrawRectangle = draw_rectangle
DrawText = draw_text
GetFiguresAtLocation = get_figures_at_location
GetBoundingBox = get_bounding_box
Erase = erase
MotionCallBack = motion_call_back
Move = move
MoveFigure = move_figure
RelocateFigure = relocate_figure
SendFigureToBack = send_figure_to_back
TKCanvas = tk_canvas
Update = update

View File

@ -0,0 +1,199 @@
from __future__ import annotations
import tkinter as tk
def AddMenuItem(top_menu, sub_menu_info, element, is_sub_menu=False, skip=False, right_click_menu=False):
"""
Only to be used internally. Not user callable
:param top_menu: ???
:type top_menu: ???
:param sub_menu_info: ???
:type sub_menu_info:
:param element: ???
:type element: idk_yetReally
:param is_sub_menu: (Default = False)
:type is_sub_menu: (bool)
:param skip: (Default = False)
:type skip: (bool)
"""
return_val = None
if type(sub_menu_info) is str:
if not is_sub_menu and not skip:
pos = sub_menu_info.find(MENU_SHORTCUT_CHARACTER)
if pos != -1:
if pos < len(MENU_SHORTCUT_CHARACTER) or sub_menu_info[pos - len(MENU_SHORTCUT_CHARACTER)] != '\\':
sub_menu_info = sub_menu_info[:pos] + sub_menu_info[pos + len(MENU_SHORTCUT_CHARACTER) :]
if sub_menu_info == '---':
top_menu.add('separator')
else:
try:
item_without_key = sub_menu_info[: sub_menu_info.index(MENU_KEY_SEPARATOR)]
except:
item_without_key = sub_menu_info
if item_without_key[0] == MENU_DISABLED_CHARACTER:
top_menu.add_command(
label=item_without_key[len(MENU_DISABLED_CHARACTER) :],
underline=pos - 1,
command=lambda: element._MenuItemChosenCallback(sub_menu_info),
)
top_menu.entryconfig(item_without_key[len(MENU_DISABLED_CHARACTER) :], state='disabled')
else:
top_menu.add_command(
label=item_without_key,
underline=pos,
command=lambda: element._MenuItemChosenCallback(sub_menu_info),
)
else:
i = 0
while i < (len(sub_menu_info)):
item = sub_menu_info[i]
if i != len(sub_menu_info) - 1:
if type(sub_menu_info[i + 1]) is list:
new_menu = tk.Menu(top_menu, tearoff=element.Tearoff)
# if a right click menu, then get styling from the top-level window
if right_click_menu:
window = element.ParentForm
if window.right_click_menu_background_color not in (COLOR_SYSTEM_DEFAULT, None):
new_menu.config(bg=window.right_click_menu_background_color)
new_menu.config(activeforeground=window.right_click_menu_background_color)
if window.right_click_menu_text_color not in (COLOR_SYSTEM_DEFAULT, None):
new_menu.config(fg=window.right_click_menu_text_color)
new_menu.config(activebackground=window.right_click_menu_text_color)
if window.right_click_menu_disabled_text_color not in (COLOR_SYSTEM_DEFAULT, None):
new_menu.config(disabledforeground=window.right_click_menu_disabled_text_color)
if window.right_click_menu_font is not None:
new_menu.config(font=window.right_click_menu_font)
else:
if element.Font is not None:
new_menu.config(font=element.Font)
if element.BackgroundColor not in (COLOR_SYSTEM_DEFAULT, None):
new_menu.config(bg=element.BackgroundColor)
new_menu.config(activeforeground=element.BackgroundColor)
if element.TextColor not in (COLOR_SYSTEM_DEFAULT, None):
new_menu.config(fg=element.TextColor)
new_menu.config(activebackground=element.TextColor)
if element.DisabledTextColor not in (COLOR_SYSTEM_DEFAULT, None):
new_menu.config(disabledforeground=element.DisabledTextColor)
if element.ItemFont is not None:
new_menu.config(font=element.ItemFont)
return_val = new_menu
pos = sub_menu_info[i].find(MENU_SHORTCUT_CHARACTER)
if pos != -1:
if pos < len(MENU_SHORTCUT_CHARACTER) or sub_menu_info[i][pos - len(MENU_SHORTCUT_CHARACTER)] != '\\':
sub_menu_info[i] = sub_menu_info[i][:pos] + sub_menu_info[i][pos + len(MENU_SHORTCUT_CHARACTER) :]
if sub_menu_info[i][0] == MENU_DISABLED_CHARACTER:
top_menu.add_cascade(
label=sub_menu_info[i][len(MENU_DISABLED_CHARACTER) :],
menu=new_menu,
underline=pos,
state='disabled',
)
else:
top_menu.add_cascade(label=sub_menu_info[i], menu=new_menu, underline=pos)
AddMenuItem(new_menu, sub_menu_info[i + 1], element, is_sub_menu=True, right_click_menu=right_click_menu)
i += 1 # skip the next one
else:
AddMenuItem(top_menu, item, element, right_click_menu=right_click_menu)
else:
AddMenuItem(top_menu, item, element, right_click_menu=right_click_menu)
i += 1
return return_val
def button_color_to_tuple(color_tuple_or_string, default=(None, None)):
"""
Convert a color tuple or color string into 2 components and returns them as a tuple
(Text Color, Button Background Color)
If None is passed in as the first parameter, then the theme's button color is
returned
:param color_tuple_or_string: Button color - tuple or a simplied color string with word "on" between color
:type color_tuple_or_string: str | (str, str)
:param default: The 2 colors to use if there is a problem. Otherwise defaults to the theme's button color
:type default: (str, str)
:return: (str | (str, str)
:rtype: str | (str, str)
"""
if default == (None, None):
color_tuple = _simplified_dual_color_to_tuple(color_tuple_or_string, default=theme_button_color())
elif color_tuple_or_string == COLOR_SYSTEM_DEFAULT:
color_tuple = (COLOR_SYSTEM_DEFAULT, COLOR_SYSTEM_DEFAULT)
else:
color_tuple = _simplified_dual_color_to_tuple(color_tuple_or_string, default=default)
return color_tuple
def _simplified_dual_color_to_tuple(color_tuple_or_string, default=(None, None)):
"""
Convert a color tuple or color string into 2 components and returns them as a tuple
(Text Color, Button Background Color)
If None is passed in as the first parameter, theme_
:param color_tuple_or_string: Button color - tuple or a simplied color string with word "on" between color
:type color_tuple_or_string: str | (str, str} | (None, None)
:param default: The 2 colors to use if there is a problem. Otherwise defaults to the theme's button color
:type default: (str, str)
:return: (str | (str, str)
:rtype: str | (str, str)
"""
if color_tuple_or_string is None or color_tuple_or_string == (None, None):
color_tuple_or_string = default
if color_tuple_or_string == COLOR_SYSTEM_DEFAULT:
return (COLOR_SYSTEM_DEFAULT, COLOR_SYSTEM_DEFAULT)
text_color = background_color = COLOR_SYSTEM_DEFAULT
try:
if isinstance(color_tuple_or_string, (tuple, list)):
if len(color_tuple_or_string) >= 2:
text_color = color_tuple_or_string[0] or default[0]
background_color = color_tuple_or_string[1] or default[1]
elif len(color_tuple_or_string) == 1:
background_color = color_tuple_or_string[0] or default[1]
elif isinstance(color_tuple_or_string, str):
color_tuple_or_string = color_tuple_or_string.lower()
split_colors = color_tuple_or_string.split(' on ')
if len(split_colors) >= 2:
text_color = split_colors[0].strip() or default[0]
background_color = split_colors[1].strip() or default[1]
elif len(split_colors) == 1:
split_colors = color_tuple_or_string.split('on')
if len(split_colors) == 1:
text_color, background_color = default[0], split_colors[0].strip()
else:
split_colors = split_colors[0].strip(), split_colors[1].strip()
text_color = split_colors[0] or default[0]
background_color = split_colors[1] or default[1]
# text_color, background_color = color_tuple_or_string, default[1]
else:
text_color, background_color = default
else:
if not SUPPRESS_ERROR_POPUPS:
_error_popup_with_traceback('** Badly formatted dual-color... not a tuple nor string **', color_tuple_or_string)
else:
print('** Badly formatted dual-color... not a tuple nor string **', color_tuple_or_string)
text_color, background_color = default
except Exception as e:
if not SUPPRESS_ERROR_POPUPS:
_error_popup_with_traceback('** Badly formatted button color **', color_tuple_or_string, e)
else:
print('** Badly formatted button color... not a tuple nor string **', color_tuple_or_string, e)
text_color, background_color = default
if isinstance(text_color, int):
text_color = '#%06X' % text_color
if isinstance(background_color, int):
background_color = '#%06X' % background_color
# print('converted button color', color_tuple_or_string, 'to', (text_color, background_color))
return (text_color, background_color)
from FreeSimpleGUI._utils import _error_popup_with_traceback
from FreeSimpleGUI import COLOR_SYSTEM_DEFAULT
from FreeSimpleGUI import MENU_DISABLED_CHARACTER
from FreeSimpleGUI import MENU_KEY_SEPARATOR
from FreeSimpleGUI import MENU_SHORTCUT_CHARACTER
from FreeSimpleGUI import SUPPRESS_ERROR_POPUPS
from FreeSimpleGUI import theme_button_color

View File

@ -0,0 +1,319 @@
from __future__ import annotations
import time
import tkinter as tk
import warnings
from FreeSimpleGUI import ELEM_TYPE_IMAGE
from FreeSimpleGUI._utils import _error_popup_with_traceback
from FreeSimpleGUI.elements.base import Element
class Image(Element):
"""
Image Element - show an image in the window. Should be a GIF or a PNG only
"""
def __init__(
self,
source=None,
filename=None,
data=None,
background_color=None,
size=(None, None),
s=(None, None),
pad=None,
p=None,
key=None,
k=None,
tooltip=None,
subsample=None,
zoom=None,
right_click_menu=None,
expand_x=False,
expand_y=False,
visible=True,
enable_events=False,
metadata=None,
):
"""
:param source: A filename or a base64 bytes. Will automatically detect the type and fill in filename or data for you.
:type source: str | bytes | None
:param filename: image filename if there is a button image. GIFs and PNGs only.
:type filename: str | None
:param data: Raw or Base64 representation of the image to put on button. Choose either filename or data
:type data: bytes | str | None
:param background_color: color of background
:type background_color:
:param size: (width, height) size of image in pixels
:type size: (int, int)
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int, int) | (None, None) | int
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param key: Used with window.find_element and with return values to uniquely identify this element to uniquely identify this element
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param tooltip: text, that will appear when mouse hovers over the element
:type tooltip: (str)
:param subsample: amount to reduce the size of the image. Divides the size by this number. 2=1/2, 3=1/3, 4=1/4, etc
:type subsample: (int)
:param zoom: amount to increase the size of the image.
:type zoom: (int)
:param right_click_menu: A list of lists of Menu items to show when this element is right clicked. See user docs for exact format.
:type right_click_menu: List[List[ List[str] | str ]]
:param expand_x: If True the element will automatically expand in the X direction to fill available space
:type expand_x: (bool)
:param expand_y: If True the element will automatically expand in the Y direction to fill available space
:type expand_y: (bool)
:param visible: set visibility state of the element
:type visible: (bool)
:param enable_events: Turns on the element specific events. For an Image element, the event is "image clicked"
:type enable_events: (bool)
:param metadata: User metadata that can be set to ANYTHING
:type metadata: (Any)
"""
if source is not None:
if isinstance(source, bytes):
data = source
elif isinstance(source, str):
filename = source
else:
warnings.warn(f'Image element - source is not a valid type: {type(source)}', UserWarning)
self.Filename = filename
self.Data = data
self.Widget = self.tktext_label = None # type: tk.Label
self.BackgroundColor = background_color
if data is None and filename is None:
self.Filename = ''
self.EnableEvents = enable_events
self.RightClickMenu = right_click_menu
self.AnimatedFrames = None
self.CurrentFrameNumber = 0
self.TotalAnimatedFrames = 0
self.LastFrameTime = 0
self.ImageSubsample = subsample
self.zoom = int(zoom) if zoom is not None else None
self.Source = filename if filename is not None else data
key = key if key is not None else k
sz = size if size != (None, None) else s
pad = pad if pad is not None else p
self.expand_x = expand_x
self.expand_y = expand_y
super().__init__(
ELEM_TYPE_IMAGE,
size=sz,
background_color=background_color,
pad=pad,
key=key,
tooltip=tooltip,
visible=visible,
metadata=metadata,
)
return
def update(self, source=None, filename=None, data=None, size=(None, None), subsample=None, zoom=None, visible=None):
"""
Changes some of the settings for the Image Element. Must call `Window.Read` or `Window.Finalize` prior.
To clear an image that's been displayed, call with NONE of the options set. A blank update call will
delete the previously shown image.
Changes will not be visible in your window until you call window.read or window.refresh.
If you change visibility, your element may MOVE. If you want it to remain stationary, use the "layout helper"
function "pin" to ensure your element is "pinned" to that location in your layout so that it returns there
when made visible.
:param source: A filename or a base64 bytes. Will automatically detect the type and fill in filename or data for you.
:type source: str | bytes | None
:param filename: filename to the new image to display.
:type filename: (str)
:param data: Base64 encoded string OR a tk.PhotoImage object
:type data: str | tkPhotoImage
:param size: (width, height) size of image in pixels
:type size: Tuple[int,int]
:param subsample: amount to reduce the size of the image. Divides the size by this number. 2=1/2, 3=1/3, 4=1/4, etc
:type subsample: (int)
:param zoom: amount to increase the size of the image
:type zoom: (int)
:param visible: control visibility of element
:type visible: (bool)
"""
if not self._widget_was_created(): # if widget hasn't been created yet, then don't allow
return
if self._this_elements_window_closed():
_error_popup_with_traceback('Error in Image.update - The window was closed')
return
if source is not None:
if isinstance(source, bytes):
data = source
elif isinstance(source, str):
filename = source
else:
warnings.warn(f'Image element - source is not a valid type: {type(source)}', UserWarning)
image = None
if filename is not None:
try:
image = tk.PhotoImage(file=filename)
if subsample is not None:
image = image.subsample(subsample)
if zoom is not None:
image = image.zoom(int(zoom))
except Exception as e:
_error_popup_with_traceback('Exception updating Image element', e)
elif data is not None:
# if type(data) is bytes:
try:
image = tk.PhotoImage(data=data)
if subsample is not None:
image = image.subsample(subsample)
if zoom is not None:
image = image.zoom(int(zoom))
except Exception:
image = data
# return # an error likely means the window has closed so exit
if image is not None:
self.tktext_label.configure(image='') # clear previous image
if self.tktext_label.image is not None:
del self.tktext_label.image
if type(image) is not bytes:
width, height = (
size[0] if size[0] is not None else image.width(),
size[1] if size[1] is not None else image.height(),
)
else:
width, height = size
try: # sometimes crashes if user closed with X
self.tktext_label.configure(image=image, width=width, height=height)
except Exception as e:
_error_popup_with_traceback('Exception updating Image element', e)
self.tktext_label.image = image
if visible is False:
self._pack_forget_save_settings()
elif visible is True:
self._pack_restore_settings()
# if everything is set to None, then delete the image
if filename is None and image is None and visible is None and size == (None, None):
# Using a try because the image may have been previously deleted and don't want an error if that's happened
try:
self.tktext_label.configure(image='', width=1, height=1, bd=0)
self.tktext_label.image = None
except:
pass
if visible is not None:
self._visible = visible
def update_animation(self, source, time_between_frames=0):
"""
Show an Animated GIF. Call the function as often as you like. The function will determine when to show the next frame and will automatically advance to the next frame at the right time.
NOTE - does NOT perform a sleep call to delay
:param source: Filename or Base64 encoded string containing Animated GIF
:type source: str | bytes | None
:param time_between_frames: Number of milliseconds to wait between showing frames
:type time_between_frames: (int)
"""
if self.Source != source:
self.AnimatedFrames = None
self.Source = source
if self.AnimatedFrames is None:
self.TotalAnimatedFrames = 0
self.AnimatedFrames = []
# Load up to 1000 frames of animation. stops when a bad frame is returns by tkinter
for i in range(1000):
if type(source) is not bytes:
try:
self.AnimatedFrames.append(tk.PhotoImage(file=source, format='gif -index %i' % (i)))
except Exception:
break
else:
try:
self.AnimatedFrames.append(tk.PhotoImage(data=source, format='gif -index %i' % (i)))
except Exception:
break
self.TotalAnimatedFrames = len(self.AnimatedFrames)
self.LastFrameTime = time.time()
self.CurrentFrameNumber = -1 # start at -1 because it is incremented before every frame is shown
# show the frame
now = time.time()
if time_between_frames:
if (now - self.LastFrameTime) * 1000 > time_between_frames:
self.LastFrameTime = now
self.CurrentFrameNumber = (self.CurrentFrameNumber + 1) % self.TotalAnimatedFrames
else: # don't reshow the frame again if not time for new frame
return
else:
self.CurrentFrameNumber = (self.CurrentFrameNumber + 1) % self.TotalAnimatedFrames
image = self.AnimatedFrames[self.CurrentFrameNumber]
try: # needed in case the window was closed with an "X"
self.tktext_label.configure(image=image, width=image.width(), heigh=image.height())
except Exception as e:
print('Exception in update_animation', e)
def update_animation_no_buffering(self, source, time_between_frames=0):
"""
Show an Animated GIF. Call the function as often as you like. The function will determine when to show the next frame and will automatically advance to the next frame at the right time.
NOTE - does NOT perform a sleep call to delay
:param source: Filename or Base64 encoded string containing Animated GIF
:type source: str | bytes
:param time_between_frames: Number of milliseconds to wait between showing frames
:type time_between_frames: (int)
"""
if self.Source != source:
self.AnimatedFrames = None
self.Source = source
self.frame_num = 0
now = time.time()
if time_between_frames:
if (now - self.LastFrameTime) * 1000 > time_between_frames:
self.LastFrameTime = now
else: # don't reshow the frame again if not time for new frame
return
# read a frame
while True:
if type(source) is not bytes:
try:
self.image = tk.PhotoImage(file=source, format='gif -index %i' % (self.frame_num))
self.frame_num += 1
except:
self.frame_num = 0
else:
try:
self.image = tk.PhotoImage(data=source, format='gif -index %i' % (self.frame_num))
self.frame_num += 1
except:
self.frame_num = 0
if self.frame_num:
break
try: # needed in case the window was closed with an "X"
self.tktext_label.configure(image=self.image, width=self.image.width(), heigh=self.image.height())
except:
pass
Update = update
UpdateAnimation = update_animation

View File

@ -0,0 +1,301 @@
from __future__ import annotations
import tkinter as tk
import FreeSimpleGUI
from FreeSimpleGUI import COLOR_SYSTEM_DEFAULT
from FreeSimpleGUI import ELEM_TYPE_INPUT_TEXT
from FreeSimpleGUI import Element
from FreeSimpleGUI._utils import _error_popup_with_traceback
class Input(Element):
"""
Display a single text input field. Based on the tkinter Widget `Entry`
"""
def __init__(
self,
default_text='',
size=(None, None),
s=(None, None),
disabled=False,
password_char='',
justification=None,
background_color=None,
text_color=None,
font=None,
tooltip=None,
border_width=None,
change_submits=False,
enable_events=False,
do_not_clear=True,
key=None,
k=None,
focus=False,
pad=None,
p=None,
use_readonly_for_disable=True,
readonly=False,
disabled_readonly_background_color=None,
disabled_readonly_text_color=None,
selected_text_color=None,
selected_background_color=None,
expand_x=False,
expand_y=False,
right_click_menu=None,
visible=True,
metadata=None,
):
"""
:param default_text: Text initially shown in the input box as a default value(Default value = ''). Will automatically be converted to string
:type default_text: (Any)
:param size: w=characters-wide, h=rows-high. If an int is supplied rather than a tuple, then a tuple is created width=int supplied and heigh=1
:type size: (int, int) | (int, None) | int
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int, int) | (None, None) | int
:param disabled: set disable state for element (Default = False)
:type disabled: (bool)
:param password_char: Password character if this is a password field (Default value = '')
:type password_char: (char)
:param justification: justification for data display. Valid choices - left, right, center
:type justification: (str)
:param background_color: color of background in one of the color formats
:type background_color: (str)
:param text_color: color of the text
:type text_color: (str)
:param font: specifies the font family, size. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param tooltip: text, that will appear when mouse hovers over the element
:type tooltip: (str)
:param border_width: width of border around element in pixels
:type border_width: (int)
:param change_submits: * DEPRICATED DO NOT USE. Use `enable_events` instead
:type change_submits: (bool)
:param enable_events: If True then changes to this element are immediately reported as an event. Use this instead of change_submits (Default = False)
:type enable_events: (bool)
:param do_not_clear: If False then the field will be set to blank after ANY event (button, any event) (Default = True)
:type do_not_clear: (bool)
:param key: Value that uniquely identifies this element from all other elements. Used when Finding an element or in return values. Must be unique to the window
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param focus: Determines if initial focus should go to this element.
:type focus: (bool)
:param pad: Amount of padding to put around element. Normally (horizontal pixels, vertical pixels) but can be split apart further into ((horizontal left, horizontal right), (vertical above, vertical below)). If int is given, then converted to tuple (int, int) with the value provided duplicated
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param use_readonly_for_disable: If True (the default) tkinter state set to 'readonly'. Otherwise state set to 'disabled'
:type use_readonly_for_disable: (bool)
:param readonly: If True tkinter state set to 'readonly'. Use this in place of use_readonly_for_disable as another way of achieving readonly. Note cannot set BOTH readonly and disabled as tkinter only supplies a single flag
:type readonly: (bool)
:param disabled_readonly_background_color: If state is set to readonly or disabled, the color to use for the background
:type disabled_readonly_background_color: (str)
:param disabled_readonly_text_color: If state is set to readonly or disabled, the color to use for the text
:type disabled_readonly_text_color: (str)
:param selected_text_color: Color of text when it is selected (using mouse or control+A, etc)
:type selected_text_color: (str)
:param selected_background_color: Color of background when it is selected (using mouse or control+A, etc)
:type selected_background_color: (str)
:param expand_x: If True the element will automatically expand in the X direction to fill available space
:type expand_x: (bool)
:param expand_y: If True the element will automatically expand in the Y direction to fill available space
:type expand_y: (bool)
:param right_click_menu: A list of lists of Menu items to show when this element is right clicked. See user docs for exact format.
:type right_click_menu: List[List[ List[str] | str ]]
:param visible: set visibility state of the element (Default = True)
:type visible: (bool)
:param metadata: User metadata that can be set to ANYTHING
:type metadata: (Any)
"""
self.DefaultText = default_text if default_text is not None else ''
self.PasswordCharacter = password_char
bg = background_color if background_color is not None else FreeSimpleGUI.DEFAULT_INPUT_ELEMENTS_COLOR
fg = text_color if text_color is not None else FreeSimpleGUI.DEFAULT_INPUT_TEXT_COLOR
self.selected_text_color = selected_text_color
self.selected_background_color = selected_background_color
self.Focus = focus
self.do_not_clear = do_not_clear
self.Justification = justification
self.Disabled = disabled
self.ChangeSubmits = change_submits or enable_events
self.RightClickMenu = right_click_menu
self.UseReadonlyForDisable = use_readonly_for_disable
self.disabled_readonly_background_color = disabled_readonly_background_color
self.disabled_readonly_text_color = disabled_readonly_text_color
self.ReadOnly = readonly
self.BorderWidth = border_width if border_width is not None else FreeSimpleGUI.DEFAULT_BORDER_WIDTH
self.TKEntry = self.Widget = None # type: tk.Entry
key = key if key is not None else k
sz = size if size != (None, None) else s
pad = pad if pad is not None else p
self.expand_x = expand_x
self.expand_y = expand_y
super().__init__(
ELEM_TYPE_INPUT_TEXT,
size=sz,
background_color=bg,
text_color=fg,
key=key,
pad=pad,
font=font,
tooltip=tooltip,
visible=visible,
metadata=metadata,
)
def update(
self,
value=None,
disabled=None,
select=None,
visible=None,
text_color=None,
background_color=None,
font=None,
move_cursor_to='end',
password_char=None,
paste=None,
readonly=None,
):
"""
Changes some of the settings for the Input Element. Must call `Window.Read` or `Window.Finalize` prior.
Changes will not be visible in your window until you call window.read or window.refresh.
If you change visibility, your element may MOVE. If you want it to remain stationary, use the "layout helper"
function "pin" to ensure your element is "pinned" to that location in your layout so that it returns there
when made visible.
:param value: new text to display as default text in Input field
:type value: (str)
:param disabled: disable or enable state of the element (sets Entry Widget to readonly or normal)
:type disabled: (bool)
:param select: if True, then the text will be selected
:type select: (bool)
:param visible: change visibility of element
:type visible: (bool)
:param text_color: change color of text being typed
:type text_color: (str)
:param background_color: change color of the background
:type background_color: (str)
:param font: specifies the font family, size. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param move_cursor_to: Moves the cursor to a particular offset. Defaults to 'end'
:type move_cursor_to: int | str
:param password_char: Password character if this is a password field
:type password_char: str
:param paste: If True "Pastes" the value into the element rather than replacing the entire element. If anything is selected it is replaced. The text is inserted at the current cursor location.
:type paste: bool
:param readonly: if True make element readonly (user cannot change any choices). Enables the element if either choice are made.
:type readonly: (bool)
"""
if not self._widget_was_created(): # if widget hasn't been created yet, then don't allow
return
if self._this_elements_window_closed():
_error_popup_with_traceback('Error in Input.update - The window was closed')
return
if background_color not in (None, COLOR_SYSTEM_DEFAULT):
self.TKEntry.configure(background=background_color)
self.BackgroundColor = background_color
if text_color not in (None, COLOR_SYSTEM_DEFAULT):
self.TKEntry.configure(fg=text_color)
self.TextColor = text_color
if disabled is True:
if self.UseReadonlyForDisable:
if self.disabled_readonly_text_color not in (None, COLOR_SYSTEM_DEFAULT):
self.TKEntry.configure(fg=self.disabled_readonly_text_color)
self.TKEntry['state'] = 'readonly'
else:
if self.TextColor not in (None, COLOR_SYSTEM_DEFAULT):
self.TKEntry.configure(fg=self.TextColor)
self.TKEntry['state'] = 'disabled'
self.Disabled = True
elif disabled is False:
self.TKEntry['state'] = 'normal'
if self.TextColor not in (None, COLOR_SYSTEM_DEFAULT):
self.TKEntry.configure(fg=self.TextColor)
self.Disabled = False
if readonly is True:
self.TKEntry['state'] = 'readonly'
elif readonly is False:
self.TKEntry['state'] = 'normal'
if value is not None:
if paste is not True:
try:
self.TKStringVar.set(value)
except:
pass
self.DefaultText = value
if paste is True:
try:
self.TKEntry.delete('sel.first', 'sel.last')
except:
pass
self.TKEntry.insert('insert', value)
if move_cursor_to == 'end':
self.TKEntry.icursor(tk.END)
elif move_cursor_to is not None:
self.TKEntry.icursor(move_cursor_to)
if select:
self.TKEntry.select_range(0, 'end')
if visible is False:
self._pack_forget_save_settings()
# self.TKEntry.pack_forget()
elif visible is True:
self._pack_restore_settings()
# self.TKEntry.pack(padx=self.pad_used[0], pady=self.pad_used[1])
# self.TKEntry.pack(padx=self.pad_used[0], pady=self.pad_used[1], in_=self.ParentRowFrame)
if visible is not None:
self._visible = visible
if password_char is not None:
self.TKEntry.configure(show=password_char)
self.PasswordCharacter = password_char
if font is not None:
self.TKEntry.configure(font=font)
def set_ibeam_color(self, ibeam_color=None):
"""
Sets the color of the I-Beam that is used to "insert" characters. This is oftens called a "Cursor" by
many users. To keep from being confused with tkinter's definition of cursor (the mouse pointer), the term
ibeam is used in this case.
:param ibeam_color: color to set the "I-Beam" used to indicate where characters will be inserted
:type ibeam_color: (str)
"""
if not self._widget_was_created():
return
if ibeam_color is not None:
try:
self.Widget.config(insertbackground=ibeam_color)
except Exception:
_error_popup_with_traceback(
'Error setting I-Beam color in set_ibeam_color',
'The element has a key:',
self.Key,
'The color passed in was:',
ibeam_color,
)
def get(self):
"""
Read and return the current value of the input element. Must call `Window.Read` or `Window.Finalize` prior
:return: current value of Input field or '' if error encountered
:rtype: (str)
"""
try:
text = self.TKStringVar.get()
except:
text = ''
return text
Get = get
Update = update

View File

@ -0,0 +1,392 @@
from __future__ import annotations
import tkinter as tk
import warnings
from typing import Any # noqa
from typing import List # noqa
import FreeSimpleGUI
from FreeSimpleGUI import ELEM_TYPE_INPUT_LISTBOX
from FreeSimpleGUI import Element
from FreeSimpleGUI import LISTBOX_SELECT_MODE_BROWSE
from FreeSimpleGUI import LISTBOX_SELECT_MODE_EXTENDED
from FreeSimpleGUI import LISTBOX_SELECT_MODE_MULTIPLE
from FreeSimpleGUI import LISTBOX_SELECT_MODE_SINGLE
from FreeSimpleGUI import SELECT_MODE_BROWSE
from FreeSimpleGUI import SELECT_MODE_EXTENDED
from FreeSimpleGUI import SELECT_MODE_MULTIPLE
from FreeSimpleGUI import SELECT_MODE_SINGLE
from FreeSimpleGUI import theme_input_background_color
from FreeSimpleGUI import theme_input_text_color
from FreeSimpleGUI._utils import _error_popup_with_traceback
class Listbox(Element):
"""
A List Box. Provide a list of values for the user to choose one or more of. Returns a list of selected rows
when a window.read() is executed.
"""
def __init__(
self,
values,
default_values=None,
select_mode=None,
change_submits=False,
enable_events=False,
bind_return_key=False,
size=(None, None),
s=(None, None),
disabled=False,
justification=None,
auto_size_text=None,
font=None,
no_scrollbar=False,
horizontal_scroll=False,
background_color=None,
text_color=None,
highlight_background_color=None,
highlight_text_color=None,
sbar_trough_color=None,
sbar_background_color=None,
sbar_arrow_color=None,
sbar_width=None,
sbar_arrow_width=None,
sbar_frame_color=None,
sbar_relief=None,
key=None,
k=None,
pad=None,
p=None,
tooltip=None,
expand_x=False,
expand_y=False,
right_click_menu=None,
visible=True,
metadata=None,
):
"""
:param values: list of values to display. Can be any type including mixed types as long as they have __str__ method
:type values: List[Any] or Tuple[Any]
:param default_values: which values should be initially selected
:type default_values: List[Any]
:param select_mode: Select modes are used to determine if only 1 item can be selected or multiple and how they can be selected. Valid choices begin with "LISTBOX_SELECT_MODE_" and include: LISTBOX_SELECT_MODE_SINGLE LISTBOX_SELECT_MODE_MULTIPLE LISTBOX_SELECT_MODE_BROWSE LISTBOX_SELECT_MODE_EXTENDED
:type select_mode: [enum]
:param change_submits: DO NOT USE. Only listed for backwards compat - Use enable_events instead
:type change_submits: (bool)
:param enable_events: Turns on the element specific events. Listbox generates events when an item is clicked
:type enable_events: (bool)
:param bind_return_key: If True, then the return key will cause a the Listbox to generate an event when return key is pressed
:type bind_return_key: (bool)
:param size: w=characters-wide, h=rows-high. If an int instead of a tuple is supplied, then height is auto-set to 1
:type size: (int, int) | (int, None) | int
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int, int) | (None, None) | int
:param disabled: set disable state for element
:type disabled: (bool)
:param justification: justification for items in listbox. Valid choices - left, right, center. Default is left. NOTE - on some older versions of tkinter, not available
:type justification: (str)
:param auto_size_text: True if element should be the same size as the contents
:type auto_size_text: (bool)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param no_scrollbar: Controls if a scrollbar should be shown. If True, no scrollbar will be shown
:type no_scrollbar: (bool)
:param horizontal_scroll: Controls if a horizontal scrollbar should be shown. If True a horizontal scrollbar will be shown in addition to vertical
:type horizontal_scroll: (bool)
:param background_color: color of background
:type background_color: (str)
:param text_color: color of the text
:type text_color: (str)
:param highlight_background_color: color of the background when an item is selected. Defaults to normal text color (a reverse look)
:type highlight_background_color: (str)
:param highlight_text_color: color of the text when an item is selected. Defaults to the normal background color (a rerverse look)
:type highlight_text_color: (str)
:param sbar_trough_color: Scrollbar color of the trough
:type sbar_trough_color: (str)
:param sbar_background_color: Scrollbar color of the background of the arrow buttons at the ends AND the color of the "thumb" (the thing you grab and slide). Switches to arrow color when mouse is over
:type sbar_background_color: (str)
:param sbar_arrow_color: Scrollbar color of the arrow at the ends of the scrollbar (it looks like a button). Switches to background color when mouse is over
:type sbar_arrow_color: (str)
:param sbar_width: Scrollbar width in pixels
:type sbar_width: (int)
:param sbar_arrow_width: Scrollbar width of the arrow on the scrollbar. It will potentially impact the overall width of the scrollbar
:type sbar_arrow_width: (int)
:param sbar_frame_color: Scrollbar Color of frame around scrollbar (available only on some ttk themes)
:type sbar_frame_color: (str)
:param sbar_relief: Scrollbar relief that will be used for the "thumb" of the scrollbar (the thing you grab that slides). Should be a constant that is defined at starting with "RELIEF_" - RELIEF_RAISED, RELIEF_SUNKEN, RELIEF_FLAT, RELIEF_RIDGE, RELIEF_GROOVE, RELIEF_SOLID
:type sbar_relief: (str)
:param key: Used with window.find_element and with return values to uniquely identify this element
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param tooltip: text, that will appear when mouse hovers over the element
:type tooltip: (str)
:param expand_x: If True the element will automatically expand in the X direction to fill available space
:type expand_x: (bool)
:param expand_y: If True the element will automatically expand in the Y direction to fill available space
:type expand_y: (bool)
:param right_click_menu: A list of lists of Menu items to show when this element is right clicked. See user docs for exact format.
:type right_click_menu: List[List[ List[str] | str ]]
:param visible: set visibility state of the element
:type visible: (bool)
:param metadata: User metadata that can be set to ANYTHING
:type metadata: (Any)
"""
if values is None:
_error_popup_with_traceback(
'Error in your Listbox definition - The values parameter cannot be None',
'Use an empty list if you want no values in your Listbox',
)
self.Values = values
self.DefaultValues = default_values
self.TKListbox = None
self.ChangeSubmits = change_submits or enable_events
self.BindReturnKey = bind_return_key
self.Disabled = disabled
if select_mode == LISTBOX_SELECT_MODE_BROWSE:
self.SelectMode = SELECT_MODE_BROWSE
elif select_mode == LISTBOX_SELECT_MODE_EXTENDED:
self.SelectMode = SELECT_MODE_EXTENDED
elif select_mode == LISTBOX_SELECT_MODE_MULTIPLE:
self.SelectMode = SELECT_MODE_MULTIPLE
elif select_mode == LISTBOX_SELECT_MODE_SINGLE:
self.SelectMode = SELECT_MODE_SINGLE
else:
self.SelectMode = FreeSimpleGUI.DEFAULT_LISTBOX_SELECT_MODE
bg = background_color if background_color is not None else theme_input_background_color()
fg = text_color if text_color is not None else theme_input_text_color()
self.HighlightBackgroundColor = highlight_background_color if highlight_background_color is not None else fg
self.HighlightTextColor = highlight_text_color if highlight_text_color is not None else bg
self.RightClickMenu = right_click_menu
self.vsb = None # type: tk.Scrollbar or None
self.hsb = None # type: tk.Scrollbar | None
self.TKListbox = self.Widget = None # type: tk.Listbox
self.element_frame = None # type: tk.Frame
self.NoScrollbar = no_scrollbar
self.HorizontalScroll = horizontal_scroll
key = key if key is not None else k
sz = size if size != (None, None) else s
pad = pad if pad is not None else p
self.expand_x = expand_x
self.expand_y = expand_y
self.justification = justification
super().__init__(
ELEM_TYPE_INPUT_LISTBOX,
size=sz,
auto_size_text=auto_size_text,
font=font,
background_color=bg,
text_color=fg,
key=key,
pad=pad,
tooltip=tooltip,
visible=visible,
metadata=metadata,
sbar_trough_color=sbar_trough_color,
sbar_background_color=sbar_background_color,
sbar_arrow_color=sbar_arrow_color,
sbar_width=sbar_width,
sbar_arrow_width=sbar_arrow_width,
sbar_frame_color=sbar_frame_color,
sbar_relief=sbar_relief,
)
def update(self, values=None, disabled=None, set_to_index=None, scroll_to_index=None, select_mode=None, visible=None):
"""
Changes some of the settings for the Listbox Element. Must call `Window.Read` or `Window.Finalize` prior
Changes will not be visible in your window until you call window.read or window.refresh.
If you change visibility, your element may MOVE. If you want it to remain stationary, use the "layout helper"
function "pin" to ensure your element is "pinned" to that location in your layout so that it returns there
when made visible.
:param values: new list of choices to be shown to user
:type values: List[Any]
:param disabled: disable or enable state of the element
:type disabled: (bool)
:param set_to_index: highlights the item(s) indicated. If parm is an int one entry will be set. If is a list, then each entry in list is highlighted
:type set_to_index: int | list | tuple
:param scroll_to_index: scroll the listbox so that this index is the first shown
:type scroll_to_index: (int)
:param select_mode: changes the select mode according to tkinter's listbox widget
:type select_mode: (str)
:param visible: control visibility of element
:type visible: (bool)
"""
if not self._widget_was_created(): # if widget hasn't been created yet, then don't allow
return
if self._this_elements_window_closed():
_error_popup_with_traceback('Error in Listbox.update - The window was closed')
return
if disabled is True:
self.TKListbox.configure(state='disabled')
elif disabled is False:
self.TKListbox.configure(state='normal')
self.Disabled = disabled if disabled is not None else self.Disabled
if values is not None:
self.TKListbox.delete(0, 'end')
for item in list(values):
self.TKListbox.insert(tk.END, item)
# self.TKListbox.selection_set(0, 0)
self.Values = list(values)
if set_to_index is not None:
self.TKListbox.selection_clear(0, len(self.Values)) # clear all listbox selections
if type(set_to_index) in (tuple, list):
for i in set_to_index:
try:
self.TKListbox.selection_set(i, i)
except:
warnings.warn(f'* Listbox Update selection_set failed with index {set_to_index}*')
else:
try:
self.TKListbox.selection_set(set_to_index, set_to_index)
except:
warnings.warn(f'* Listbox Update selection_set failed with index {set_to_index}*')
if visible is False:
self._pack_forget_save_settings(self.element_frame)
elif visible is True:
self._pack_restore_settings(self.element_frame)
if scroll_to_index is not None and len(self.Values):
self.TKListbox.yview_moveto(scroll_to_index / len(self.Values))
if select_mode is not None:
try:
self.TKListbox.config(selectmode=select_mode)
except:
print('Listbox.update error trying to change mode to: ', select_mode)
if visible is not None:
self._visible = visible
def set_value(self, values):
"""
Set listbox highlighted choices
:param values: new values to choose based on previously set values
:type values: List[Any] | Tuple[Any]
"""
for index, item in enumerate(self.Values):
try:
if item in values:
self.TKListbox.selection_set(index)
else:
self.TKListbox.selection_clear(index)
except:
pass
self.DefaultValues = values
def get_list_values(self):
# type: (Listbox) -> List[Any]
"""
Returns list of Values provided by the user in the user's format
:return: List of values. Can be any / mixed types -> []
:rtype: List[Any]
"""
return self.Values
def get_indexes(self):
"""
Returns the items currently selected as a list of indexes
:return: A list of offsets into values that is currently selected
:rtype: List[int]
"""
return self.TKListbox.curselection()
def get(self):
"""
Returns the list of items currently selected in this listbox. It should be identical
to the value you would receive when performing a window.read() call.
:return: The list of currently selected items. The actual items are returned, not the indexes
:rtype: List[Any]
"""
try:
items = self.TKListbox.curselection()
value = [self.Values[int(item)] for item in items]
except:
value = []
return value
def select_index(self, index, highlight_text_color=None, highlight_background_color=None):
"""
Selects an index while providing capability to setting the selected color for the index to specific text/background color
:param index: specifies which item to change. index starts at 0 and goes to length of values list minus one
:type index: (int)
:param highlight_text_color: color of the text when this item is selected.
:type highlight_text_color: (str)
:param highlight_background_color: color of the background when this item is selected
:type highlight_background_color: (str)
"""
if not self._widget_was_created(): # if widget hasn't been created yet, then don't allow
return
if self._this_elements_window_closed():
_error_popup_with_traceback('Error in Listbox.select_item - The window was closed')
return
if index >= len(self.Values):
_error_popup_with_traceback('Index {} is out of range for Listbox.select_index. Max allowed index is {}.'.format(index, len(self.Values) - 1))
return
self.TKListbox.selection_set(index, index)
if highlight_text_color is not None:
self.widget.itemconfig(index, selectforeground=highlight_text_color)
if highlight_background_color is not None:
self.widget.itemconfig(index, selectbackground=highlight_background_color)
def set_index_color(self, index, text_color=None, background_color=None, highlight_text_color=None, highlight_background_color=None):
"""
Sets the color of a specific item without selecting it
:param index: specifies which item to change. index starts at 0 and goes to length of values list minus one
:type index: (int)
:param text_color: color of the text for this item
:type text_color: (str)
:param background_color: color of the background for this item
:type background_color: (str)
:param highlight_text_color: color of the text when this item is selected.
:type highlight_text_color: (str)
:param highlight_background_color: color of the background when this item is selected
:type highlight_background_color: (str)
"""
if not self._widget_was_created(): # if widget hasn't been created yet, then don't allow
return
if self._this_elements_window_closed():
_error_popup_with_traceback('Error in Listbox.set_item_color - The window was closed')
return
if index >= len(self.Values):
_error_popup_with_traceback('Index {} is out of range for Listbox.set_index_color. Max allowed index is {}.'.format(index, len(self.Values) - 1))
return
if text_color is not None:
self.widget.itemconfig(index, fg=text_color)
if background_color is not None:
self.widget.itemconfig(index, bg=background_color)
if highlight_text_color is not None:
self.widget.itemconfig(index, selectforeground=highlight_text_color)
if highlight_background_color is not None:
self.widget.itemconfig(index, selectbackground=highlight_background_color)
GetIndexes = get_indexes
GetListValues = get_list_values
SetValue = set_value
Update = update

View File

@ -0,0 +1,192 @@
from __future__ import annotations
import copy
import tkinter as tk
from FreeSimpleGUI import COLOR_SYSTEM_DEFAULT
from FreeSimpleGUI import ELEM_TYPE_MENUBAR
from FreeSimpleGUI import MENU_DISABLED_CHARACTER
from FreeSimpleGUI import MENU_SHORTCUT_CHARACTER
from FreeSimpleGUI import theme_input_background_color
from FreeSimpleGUI import theme_input_text_color
from FreeSimpleGUI._utils import _error_popup_with_traceback
from FreeSimpleGUI._utils import _exit_mainloop
from FreeSimpleGUI.elements.base import Element
from FreeSimpleGUI.elements.helpers import AddMenuItem
class Menu(Element):
"""
Menu Element is the Element that provides a Menu Bar that goes across the top of the window, just below titlebar.
Here is an example layout. The "&" are shortcut keys ALT+key.
Is a List of - "Item String" + List
Where Item String is what will be displayed on the Menubar itself.
The List that follows the item represents the items that are shown then Menu item is clicked
Notice how an "entry" in a mennu can be a list which means it branches out and shows another menu, etc. (resursive)
menu_def = [['&File', ['!&Open', '&Save::savekey', '---', '&Properties', 'E&xit']],
['!&Edit', ['!&Paste', ['Special', 'Normal', ], 'Undo'], ],
['&Debugger', ['Popout', 'Launch Debugger']],
['&Toolbar', ['Command &1', 'Command &2', 'Command &3', 'Command &4']],
['&Help', '&About...'], ]
Important Note! The colors, font, look of the Menubar itself cannot be changed, only the menus shown AFTER clicking the menubar
can be changed. If you want to change the style/colors the Menubar, then you will have to use the MenubarCustom element.
Finally, "keys" can be added to entries so make them unique. The "Save" entry has a key associated with it. You
can see it has a "::" which signifies the beginning of a key. The user will not see the key portion when the
menu is shown. The key portion is returned as part of the event.
"""
def __init__(
self,
menu_definition,
background_color=None,
text_color=None,
disabled_text_color=None,
size=(None, None),
s=(None, None),
tearoff=False,
font=None,
pad=None,
p=None,
key=None,
k=None,
visible=True,
metadata=None,
):
"""
:param menu_definition: The Menu definition specified using lists (docs explain the format)
:type menu_definition: List[List[Tuple[str, List[str]]]
:param background_color: color of the background of menus, NOT the Menubar
:type background_color: (str)
:param text_color: text color for menus, NOT the Menubar. Can be in #RRGGBB format or a color name "black".
:type text_color: (str)
:param disabled_text_color: color to use for text when item in submenu, not the menubar itself, is disabled. Can be in #RRGGBB format or a color name "black"
:type disabled_text_color: (str)
:param size: Not used in the tkinter port
:type size: (int, int)
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int, int) | (None, None)
:param tearoff: if True, then can tear the menu off from the window ans use as a floating window. Very cool effect
:type tearoff: (bool)
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param font: specifies the font family, size, etc. of submenus. Does NOT apply to the Menubar itself. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param key: Value that uniquely identifies this element from all other elements. Used when Finding an element or in return values. Must be unique to the window
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param visible: set visibility state of the element
:type visible: (bool)
:param metadata: User metadata that can be set to ANYTHING
:type metadata: (Any)
"""
self.BackgroundColor = background_color if background_color is not None else theme_input_background_color()
self.TextColor = text_color if text_color is not None else theme_input_text_color()
self.DisabledTextColor = disabled_text_color if disabled_text_color is not None else COLOR_SYSTEM_DEFAULT
self.MenuDefinition = copy.deepcopy(menu_definition)
self.Widget = self.TKMenu = None # type: tk.Menu
self.MenuItemChosen = None
key = key if key is not None else k
sz = size if size != (None, None) else s
pad = pad if pad is not None else p
super().__init__(
ELEM_TYPE_MENUBAR,
background_color=self.BackgroundColor,
text_color=self.TextColor,
size=sz,
pad=pad,
key=key,
visible=visible,
font=font,
metadata=metadata,
)
# super().__init__(ELEM_TYPE_MENUBAR, background_color=COLOR_SYSTEM_DEFAULT, text_color=COLOR_SYSTEM_DEFAULT, size=sz, pad=pad, key=key, visible=visible, font=None, metadata=metadata)
self.Tearoff = tearoff
return
def _MenuItemChosenCallback(self, item_chosen): # Menu Menu Item Chosen Callback
"""
Not user callable. Called when some end-point on the menu (an item) has been clicked. Send the information back to the application as an event. Before event can be sent
:param item_chosen: the text that was clicked on / chosen from the menu
:type item_chosen: (str)
"""
# print('IN MENU ITEM CALLBACK', item_chosen)
self.MenuItemChosen = item_chosen
self.ParentForm.LastButtonClicked = item_chosen
self.ParentForm.FormRemainedOpen = True
_exit_mainloop(self.ParentForm)
def update(self, menu_definition=None, visible=None):
"""
Update a menubar - can change the menu definition and visibility. The entire menu has to be specified
Changes will not be visible in your window until you call window.read or window.refresh.
If you change visibility, your element may MOVE. If you want it to remain stationary, use the "layout helper"
function "pin" to ensure your element is "pinned" to that location in your layout so that it returns there
when made visible.
:param menu_definition: The menu definition list
:type menu_definition: List[List[Tuple[str, List[str]]]
:param visible: control visibility of element
:type visible: (bool)
"""
if not self._widget_was_created(): # if widget hasn't been created yet, then don't allow
return
if self._this_elements_window_closed():
_error_popup_with_traceback('Error in Menu.update - The window was closed')
return
if menu_definition is not None:
self.MenuDefinition = copy.deepcopy(menu_definition)
if self.TKMenu is None: # if no menu exists, make one
self.TKMenu = tk.Menu(self.ParentForm.TKroot, tearoff=self.Tearoff, tearoffcommand=self._tearoff_menu_callback) # create the menubar
menubar = self.TKMenu
# Delete all the menu items (assuming 10000 should be a high enough number to cover them all)
menubar.delete(0, 10000)
self.Widget = self.TKMenu # same the new menu so user can access to extend PySimpleGUI
for menu_entry in self.MenuDefinition:
baritem = tk.Menu(menubar, tearoff=self.Tearoff, tearoffcommand=self._tearoff_menu_callback)
if self.BackgroundColor not in (COLOR_SYSTEM_DEFAULT, None):
baritem.config(bg=self.BackgroundColor)
if self.TextColor not in (COLOR_SYSTEM_DEFAULT, None):
baritem.config(fg=self.TextColor)
if self.DisabledTextColor not in (COLOR_SYSTEM_DEFAULT, None):
baritem.config(disabledforeground=self.DisabledTextColor)
if self.Font is not None:
baritem.config(font=self.Font)
if self.Font is not None:
baritem.config(font=self.Font)
pos = menu_entry[0].find(MENU_SHORTCUT_CHARACTER)
# print(pos)
if pos != -1:
if pos == 0 or menu_entry[0][pos - len(MENU_SHORTCUT_CHARACTER)] != '\\':
menu_entry[0] = menu_entry[0][:pos] + menu_entry[0][pos + len(MENU_SHORTCUT_CHARACTER) :]
if menu_entry[0][0] == MENU_DISABLED_CHARACTER:
menubar.add_cascade(label=menu_entry[0][len(MENU_DISABLED_CHARACTER) :], menu=baritem, underline=pos)
menubar.entryconfig(menu_entry[0][len(MENU_DISABLED_CHARACTER) :], state='disabled')
else:
menubar.add_cascade(label=menu_entry[0], menu=baritem, underline=pos)
if len(menu_entry) > 1:
AddMenuItem(baritem, menu_entry[1], self)
if visible is False:
self.ParentForm.TKroot.configure(menu=[]) # this will cause the menubar to disappear
elif self.TKMenu is not None:
self.ParentForm.TKroot.configure(menu=self.TKMenu)
if visible is not None:
self._visible = visible
Update = update

View File

@ -0,0 +1,685 @@
from __future__ import annotations
import sys
import tkinter as tk
import FreeSimpleGUI
from FreeSimpleGUI import _print_to_element
from FreeSimpleGUI import COLOR_SYSTEM_DEFAULT
from FreeSimpleGUI import ELEM_TYPE_INPUT_MULTILINE
from FreeSimpleGUI._utils import _error_popup_with_traceback
from FreeSimpleGUI.elements.base import Element
from FreeSimpleGUI.window import Window
class Multiline(Element):
"""
Multiline Element - Display and/or read multiple lines of text. This is both an input and output element.
Other PySimpleGUI ports have a separate MultilineInput and MultilineOutput elements. May want to split this
one up in the future too.
"""
def __init__(
self,
default_text='',
enter_submits=False,
disabled=False,
autoscroll=False,
autoscroll_only_at_bottom=False,
border_width=None,
size=(None, None),
s=(None, None),
auto_size_text=None,
background_color=None,
text_color=None,
selected_text_color=None,
selected_background_color=None,
horizontal_scroll=False,
change_submits=False,
enable_events=False,
do_not_clear=True,
key=None,
k=None,
write_only=False,
auto_refresh=False,
reroute_stdout=False,
reroute_stderr=False,
reroute_cprint=False,
echo_stdout_stderr=False,
focus=False,
font=None,
pad=None,
p=None,
tooltip=None,
justification=None,
no_scrollbar=False,
wrap_lines=None,
sbar_trough_color=None,
sbar_background_color=None,
sbar_arrow_color=None,
sbar_width=None,
sbar_arrow_width=None,
sbar_frame_color=None,
sbar_relief=None,
expand_x=False,
expand_y=False,
rstrip=True,
right_click_menu=None,
visible=True,
metadata=None,
):
"""
:param default_text: Initial text to show
:type default_text: (Any)
:param enter_submits: if True, the Window.read call will return is enter key is pressed in this element
:type enter_submits: (bool)
:param disabled: set disable state
:type disabled: (bool)
:param autoscroll: If True the contents of the element will automatically scroll as more data added to the end
:type autoscroll: (bool)
:param autoscroll_only_at_bottom: If True the contents of the element will automatically scroll only if the scrollbar is at the bottom of the multiline
:type autoscroll_only_at_bottom: (bool)
:param border_width: width of border around element in pixels
:type border_width: (int)
:param size: (w, h) w=characters-wide, h=rows-high. If an int instead of a tuple is supplied, then height is auto-set to 1
:type size: (int, int) | (None, None) | int
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int, int) | (None, None) | int
:param auto_size_text: if True will size the element to match the length of the text
:type auto_size_text: (bool)
:param background_color: color of background
:type background_color: (str)
:param text_color: color of the text
:type text_color: (str)
:param selected_text_color: Color of text when it is selected (using mouse or control+A, etc)
:type selected_text_color: (str)
:param selected_background_color: Color of background when it is selected (using mouse or control+A, etc)
:type selected_background_color: (str)
:param horizontal_scroll: Controls if a horizontal scrollbar should be shown. If True a horizontal scrollbar will be shown in addition to vertical
:type horizontal_scroll: (bool)
:param change_submits: DO NOT USE. Only listed for backwards compat - Use enable_events instead
:type change_submits: (bool)
:param enable_events: If True then any key press that happens when the element has focus will generate an event.
:type enable_events: (bool)
:param do_not_clear: if False the element will be cleared any time the Window.read call returns
:type do_not_clear: (bool)
:param key: Used with window.find_element and with return values to uniquely identify this element to uniquely identify this element
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param write_only: If True then no entry will be added to the values dictionary when the window is read
:type write_only: bool
:param auto_refresh: If True then anytime the element is updated, the window will be refreshed so that the change is immediately displayed
:type auto_refresh: (bool)
:param reroute_stdout: If True then all output to stdout will be output to this element
:type reroute_stdout: (bool)
:param reroute_stderr: If True then all output to stderr will be output to this element
:type reroute_stderr: (bool)
:param reroute_cprint: If True your cprint calls will output to this element. It's the same as you calling cprint_set_output_destination
:type reroute_cprint: (bool)
:param echo_stdout_stderr: If True then output to stdout and stderr will be output to this element AND also to the normal console location
:type echo_stdout_stderr: (bool)
:param focus: if True initial focus will go to this element
:type focus: (bool)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param tooltip: text, that will appear when mouse hovers over the element
:type tooltip: (str)
:param justification: text justification. left, right, center. Can use single characters l, r, c.
:type justification: (str)
:param no_scrollbar: If False then a vertical scrollbar will be shown (the default)
:type no_scrollbar: (bool)
:param wrap_lines: If True, the lines will be wrapped automatically. Other parms affect this setting, but this one will override them all. Default is it does nothing and uses previous settings for wrapping.
:type wrap_lines: (bool)
:param sbar_trough_color: Scrollbar color of the trough
:type sbar_trough_color: (str)
:param sbar_background_color: Scrollbar color of the background of the arrow buttons at the ends AND the color of the "thumb" (the thing you grab and slide). Switches to arrow color when mouse is over
:type sbar_background_color: (str)
:param sbar_arrow_color: Scrollbar color of the arrow at the ends of the scrollbar (it looks like a button). Switches to background color when mouse is over
:type sbar_arrow_color: (str)
:param sbar_width: Scrollbar width in pixels
:type sbar_width: (int)
:param sbar_arrow_width: Scrollbar width of the arrow on the scrollbar. It will potentially impact the overall width of the scrollbar
:type sbar_arrow_width: (int)
:param sbar_frame_color: Scrollbar Color of frame around scrollbar (available only on some ttk themes)
:type sbar_frame_color: (str)
:param sbar_relief: Scrollbar relief that will be used for the "thumb" of the scrollbar (the thing you grab that slides). Should be a constant that is defined at starting with "RELIEF_" - RELIEF_RAISED, RELIEF_SUNKEN, RELIEF_FLAT, RELIEF_RIDGE, RELIEF_GROOVE, RELIEF_SOLID
:type sbar_relief: (str)
:param expand_x: If True the element will automatically expand in the X direction to fill available space
:type expand_x: (bool)
:param expand_y: If True the element will automatically expand in the Y direction to fill available space
:type expand_y: (bool)
:param rstrip: If True the value returned in will have whitespace stripped from the right side
:type rstrip: (bool)
:param right_click_menu: A list of lists of Menu items to show when this element is right clicked. See user docs for exact format.
:type right_click_menu: List[List[ List[str] | str ]]
:param visible: set visibility state of the element
:type visible: (bool)
:param metadata: User metadata that can be set to ANYTHING
:type metadata: (Any)
"""
self.DefaultText = str(default_text)
self.EnterSubmits = enter_submits
bg = background_color if background_color else FreeSimpleGUI.DEFAULT_INPUT_ELEMENTS_COLOR
self.Focus = focus
self.do_not_clear = do_not_clear
fg = text_color if text_color is not None else FreeSimpleGUI.DEFAULT_INPUT_TEXT_COLOR
self.selected_text_color = selected_text_color
self.selected_background_color = selected_background_color
self.Autoscroll = autoscroll
self.Disabled = disabled
self.ChangeSubmits = change_submits or enable_events
self.RightClickMenu = right_click_menu
self.BorderWidth = border_width if border_width is not None else FreeSimpleGUI.DEFAULT_BORDER_WIDTH
self.TagCounter = 0
self.TKText = self.Widget = None # type: tk.Text
self.element_frame = None # type: tk.Frame
self.HorizontalScroll = horizontal_scroll
self.tags = set()
self.WriteOnly = write_only
self.AutoRefresh = auto_refresh
key = key if key is not None else k
self.reroute_cprint = reroute_cprint
self.echo_stdout_stderr = echo_stdout_stderr
self.Justification = 'left' if justification is None else justification
self.justification_tag = self.just_center_tag = self.just_left_tag = self.just_right_tag = None
pad = pad if pad is not None else p
self.expand_x = expand_x
self.expand_y = expand_y
self.rstrip = rstrip
self.wrap_lines = wrap_lines
self.reroute_stdout = reroute_stdout
self.reroute_stderr = reroute_stderr
self.no_scrollbar = no_scrollbar
self.hscrollbar = None # The horizontal scrollbar
self.auto_scroll_only_at_bottom = autoscroll_only_at_bottom
sz = size if size != (None, None) else s
super().__init__(
ELEM_TYPE_INPUT_MULTILINE,
size=sz,
auto_size_text=auto_size_text,
background_color=bg,
text_color=fg,
key=key,
pad=pad,
tooltip=tooltip,
font=font or FreeSimpleGUI.DEFAULT_FONT,
visible=visible,
metadata=metadata,
sbar_trough_color=sbar_trough_color,
sbar_background_color=sbar_background_color,
sbar_arrow_color=sbar_arrow_color,
sbar_width=sbar_width,
sbar_arrow_width=sbar_arrow_width,
sbar_frame_color=sbar_frame_color,
sbar_relief=sbar_relief,
)
return
def update(
self,
value=None,
disabled=None,
append=False,
font=None,
text_color=None,
background_color=None,
text_color_for_value=None,
background_color_for_value=None,
visible=None,
autoscroll=None,
justification=None,
font_for_value=None,
):
"""
Changes some of the settings for the Multiline Element. Must call `Window.read` or set finalize=True when creating window.
Changes will not be visible in your window until you call window.read or window.refresh.
If you change visibility, your element may MOVE. If you want it to remain stationary, use the "layout helper"
function "pin" to ensure your element is "pinned" to that location in your layout so that it returns there
when made visible.
:param value: new text to display
:type value: (Any)
:param disabled: disable or enable state of the element
:type disabled: (bool)
:param append: if True then new value will be added onto the end of the current value. if False then contents will be replaced.
:type append: (bool)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike for the entire element
:type font: (str or (str, int[, str]) or None)
:param text_color: color of the text
:type text_color: (str)
:param background_color: color of background
:type background_color: (str)
:param text_color_for_value: color of the new text being added (the value paramter)
:type text_color_for_value: (str)
:param background_color_for_value: color of the new background of the text being added (the value paramter)
:type background_color_for_value: (str)
:param visible: set visibility state of the element
:type visible: (bool)
:param autoscroll: if True then contents of element are scrolled down when new text is added to the end
:type autoscroll: (bool)
:param justification: text justification. left, right, center. Can use single characters l, r, c. Sets only for this value, not entire element
:type justification: (str)
:param font_for_value: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike for the value being updated
:type font_for_value: str | (str, int)
"""
if not self._widget_was_created(): # if widget hasn't been created yet, then don't allow
return
if self._this_elements_window_closed():
# _error_popup_with_traceback('Error in Multiline.update - The window was closed')
return
if autoscroll is not None:
self.Autoscroll = autoscroll
current_scroll_position = self.TKText.yview()[1]
if justification is not None:
if justification.startswith('l'):
just_tag = 'left'
if justification.startswith('r'):
just_tag = 'right'
if justification.startswith('c'):
just_tag = 'center'
else:
just_tag = self.justification_tag
tag = None
if value is not None:
value = str(value)
if background_color_for_value is not None or text_color_for_value is not None or font_for_value is not None:
try:
tag = 'Multiline(' + str(text_color_for_value) + ',' + str(background_color_for_value) + ',' + str(font_for_value) + ')'
if tag not in self.tags:
self.tags.add(tag)
if background_color_for_value is not None:
self.TKText.tag_configure(tag, background=background_color_for_value)
if text_color_for_value is not None:
self.TKText.tag_configure(tag, foreground=text_color_for_value)
if font_for_value is not None:
self.TKText.tag_configure(tag, font=font_for_value)
except Exception as e:
print('* Multiline.update - bad color likely specified:', e)
if self.Disabled:
self.TKText.configure(state='normal')
try:
if not append:
self.TKText.delete('1.0', tk.END)
if tag is not None or just_tag is not None:
self.TKText.insert(tk.END, value, (just_tag, tag))
else:
self.TKText.insert(tk.END, value)
except Exception as e:
print('* Error setting multiline *', e)
if self.Disabled:
self.TKText.configure(state='disabled')
self.DefaultText = value
if self.Autoscroll:
if not self.auto_scroll_only_at_bottom or (self.auto_scroll_only_at_bottom and current_scroll_position == 1.0):
self.TKText.see(tk.END)
if disabled is True:
self.TKText.configure(state='disabled')
elif disabled is False:
self.TKText.configure(state='normal')
self.Disabled = disabled if disabled is not None else self.Disabled
if background_color not in (None, COLOR_SYSTEM_DEFAULT):
self.TKText.configure(background=background_color)
if text_color not in (None, COLOR_SYSTEM_DEFAULT):
self.TKText.configure(fg=text_color)
if font is not None:
self.TKText.configure(font=font)
if visible is False:
self._pack_forget_save_settings(alternate_widget=self.element_frame)
elif visible is True:
self._pack_restore_settings(alternate_widget=self.element_frame)
if self.AutoRefresh and self.ParentForm:
try: # in case the window was destroyed
self.ParentForm.refresh()
except:
pass
if visible is not None:
self._visible = visible
def get(self):
"""
Return current contents of the Multiline Element
:return: current contents of the Multiline Element (used as an input type of Multiline
:rtype: (str)
"""
value = str(self.TKText.get(1.0, tk.END))
if self.rstrip:
return value.rstrip()
return value
def print(
self,
*args,
end=None,
sep=None,
text_color=None,
background_color=None,
justification=None,
font=None,
colors=None,
t=None,
b=None,
c=None,
autoscroll=True,
):
"""
Print like Python normally prints except route the output to a multiline element and also add colors if desired
colors -(str, str) or str. A combined text/background color definition in a single parameter
There are also "aliases" for text_color, background_color and colors (t, b, c)
t - An alias for color of the text (makes for shorter calls)
b - An alias for the background_color parameter
c - (str, str) - "shorthand" way of specifying color. (foreground, backgrouned)
c - str - can also be a string of the format "foreground on background" ("white on red")
With the aliases it's possible to write the same print but in more compact ways:
cprint('This will print white text on red background', c=('white', 'red'))
cprint('This will print white text on red background', c='white on red')
cprint('This will print white text on red background', text_color='white', background_color='red')
cprint('This will print white text on red background', t='white', b='red')
:param args: The arguments to print
:type args: (Any)
:param end: The end char to use just like print uses
:type end: (str)
:param sep: The separation character like print uses
:type sep: (str)
:param text_color: The color of the text
:type text_color: (str)
:param background_color: The background color of the line
:type background_color: (str)
:param justification: text justification. left, right, center. Can use single characters l, r, c. Sets only for this value, not entire element
:type justification: (str)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike for the args being printed
:type font: (str or (str, int[, str]) or None)
:param colors: Either a tuple or a string that has both the text and background colors. Or just the text color
:type colors: (str) or (str, str)
:param t: Color of the text
:type t: (str)
:param b: The background color of the line
:type b: (str)
:param c: Either a tuple or a string that has both the text and background colors or just tex color (same as the color parm)
:type c: (str) or (str, str)
:param autoscroll: If True the contents of the element will automatically scroll as more data added to the end
:type autoscroll: (bool)
"""
kw_text_color = text_color or t
kw_background_color = background_color or b
dual_color = colors or c
try:
if isinstance(dual_color, tuple):
kw_text_color = dual_color[0]
kw_background_color = dual_color[1]
elif isinstance(dual_color, str):
if ' on ' in dual_color: # if has "on" in the string, then have both text and background
kw_text_color = dual_color.split(' on ')[0]
kw_background_color = dual_color.split(' on ')[1]
else: # if no "on" then assume the color string is just the text color
kw_text_color = dual_color
except Exception as e:
print('* multiline print warning * you messed up with color formatting', e)
_print_to_element(
self,
*args,
end=end,
sep=sep,
text_color=kw_text_color,
background_color=kw_background_color,
justification=justification,
autoscroll=autoscroll,
font=font,
)
def reroute_stdout_to_here(self):
"""
Sends stdout (prints) to this element
"""
# if nothing on the stack, then need to save the very first stdout
if len(Window._rerouted_stdout_stack) == 0:
Window._original_stdout = sys.stdout
Window._rerouted_stdout_stack.insert(0, (self.ParentForm, self))
sys.stdout = self
def reroute_stderr_to_here(self):
"""
Sends stderr to this element
"""
if len(Window._rerouted_stderr_stack) == 0:
Window._original_stderr = sys.stderr
Window._rerouted_stderr_stack.insert(0, (self.ParentForm, self))
sys.stderr = self
def restore_stdout(self):
"""
Restore a previously re-reouted stdout back to the original destination
"""
Window._restore_stdout()
def restore_stderr(self):
"""
Restore a previously re-reouted stderr back to the original destination
"""
Window._restore_stderr()
def write(self, txt):
"""
Called by Python (not tkinter?) when stdout or stderr wants to write
:param txt: text of output
:type txt: (str)
"""
try:
self.update(txt, append=True)
# if need to echo, then send the same text to the destinatoin that isn't thesame as this one
if self.echo_stdout_stderr:
if sys.stdout != self:
sys.stdout.write(txt)
elif sys.stderr != self:
sys.stderr.write(txt)
except:
pass
def flush(self):
"""
Flush parameter was passed into a print statement.
For now doing nothing. Not sure what action should be taken to ensure a flush happens regardless.
"""
# try:
# self.previous_stdout.flush()
# except:
# pass
return
def set_ibeam_color(self, ibeam_color=None):
"""
Sets the color of the I-Beam that is used to "insert" characters. This is oftens called a "Cursor" by
many users. To keep from being confused with tkinter's definition of cursor (the mouse pointer), the term
ibeam is used in this case.
:param ibeam_color: color to set the "I-Beam" used to indicate where characters will be inserted
:type ibeam_color: (str)
"""
if not self._widget_was_created():
return
if ibeam_color is not None:
try:
self.Widget.config(insertbackground=ibeam_color)
except Exception:
_error_popup_with_traceback(
'Error setting I-Beam color in set_ibeam_color',
'The element has a key:',
self.Key,
'The color passed in was:',
ibeam_color,
)
def __del__(self):
"""
AT ONE TIME --- If this Widget is deleted, be sure and restore the old stdout, stderr
Now the restore is done differently. Do not want to RELY on Python to call this method
in order for stdout and stderr to be restored. Instead explicit restores are called.
"""
return
Get = get
Update = update
class Output(Multiline):
"""
Output Element - a multi-lined text area to where stdout, stderr, cprint are rerouted.
The Output Element is now based on the Multiline Element. When you make an Output Element, you're
creating a Multiline Element with some specific settings set:
auto_refresh = True
auto_scroll = True
reroute_stdout = True
reroute_stderr = True
reroute_cprint = True
write_only = True
If you choose to use a Multiline element to replace an Output element, be sure an turn on the write_only paramter in the Multiline
so that an item is not included in the values dictionary on every window.read call
"""
def __init__(
self,
size=(None, None),
s=(None, None),
background_color=None,
text_color=None,
pad=None,
p=None,
autoscroll_only_at_bottom=False,
echo_stdout_stderr=False,
font=None,
tooltip=None,
key=None,
k=None,
right_click_menu=None,
expand_x=False,
expand_y=False,
visible=True,
metadata=None,
wrap_lines=None,
horizontal_scroll=None,
sbar_trough_color=None,
sbar_background_color=None,
sbar_arrow_color=None,
sbar_width=None,
sbar_arrow_width=None,
sbar_frame_color=None,
sbar_relief=None,
):
"""
:param size: (w, h) w=characters-wide, h=rows-high. If an int instead of a tuple is supplied, then height is auto-set to 1
:type size: (int, int) | (None, None) | int
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int, int) | (None, None) | int
:param background_color: color of background
:type background_color: (str)
:param text_color: color of the text
:type text_color: (str)
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param autoscroll_only_at_bottom: If True the contents of the element will automatically scroll only if the scrollbar is at the bottom of the multiline
:type autoscroll_only_at_bottom: (bool)
:param echo_stdout_stderr: If True then output to stdout will be output to this element AND also to the normal console location
:type echo_stdout_stderr: (bool)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param tooltip: text, that will appear when mouse hovers over the element
:type tooltip: (str)
:param key: Used with window.find_element and with return values to uniquely identify this element to uniquely identify this element
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param right_click_menu: A list of lists of Menu items to show when this element is right clicked. See user docs for exact format.
:type right_click_menu: List[List[ List[str] | str ]]
:param expand_x: If True the element will automatically expand in the X direction to fill available space
:type expand_x: (bool)
:param expand_y: If True the element will automatically expand in the Y direction to fill available space
:type expand_y: (bool)
:param visible: set visibility state of the element
:type visible: (bool)
:param metadata: User metadata that can be set to ANYTHING
:type metadata: (Any)
:param wrap_lines: If True, the lines will be wrapped automatically. Other parms affect this setting, but this one will override them all. Default is it does nothing and uses previous settings for wrapping.
:type wrap_lines: (bool)
:param horizontal_scroll: Controls if a horizontal scrollbar should be shown. If True, then line wrapping will be off by default
:type horizontal_scroll: (bool)
:param sbar_trough_color: Scrollbar color of the trough
:type sbar_trough_color: (str)
:param sbar_background_color: Scrollbar color of the background of the arrow buttons at the ends AND the color of the "thumb" (the thing you grab and slide). Switches to arrow color when mouse is over
:type sbar_background_color: (str)
:param sbar_arrow_color: Scrollbar color of the arrow at the ends of the scrollbar (it looks like a button). Switches to background color when mouse is over
:type sbar_arrow_color: (str)
:param sbar_width: Scrollbar width in pixels
:type sbar_width: (int)
:param sbar_arrow_width: Scrollbar width of the arrow on the scrollbar. It will potentially impact the overall width of the scrollbar
:type sbar_arrow_width: (int)
:param sbar_frame_color: Scrollbar Color of frame around scrollbar (available only on some ttk themes)
:type sbar_frame_color: (str)
:param sbar_relief: Scrollbar relief that will be used for the "thumb" of the scrollbar (the thing you grab that slides). Should be a constant that is defined at starting with "RELIEF_" - RELIEF_RAISED, RELIEF_SUNKEN, RELIEF_FLAT, RELIEF_RIDGE, RELIEF_GROOVE, RELIEF_SOLID
:type sbar_relief: (str)
"""
super().__init__(
size=size,
s=s,
background_color=background_color,
autoscroll_only_at_bottom=autoscroll_only_at_bottom,
text_color=text_color,
pad=pad,
p=p,
echo_stdout_stderr=echo_stdout_stderr,
font=font,
tooltip=tooltip,
wrap_lines=wrap_lines,
horizontal_scroll=horizontal_scroll,
key=key,
k=k,
right_click_menu=right_click_menu,
write_only=True,
reroute_stdout=True,
reroute_stderr=True,
reroute_cprint=True,
autoscroll=True,
auto_refresh=True,
expand_x=expand_x,
expand_y=expand_y,
visible=visible,
metadata=metadata,
sbar_trough_color=sbar_trough_color,
sbar_background_color=sbar_background_color,
sbar_arrow_color=sbar_arrow_color,
sbar_width=sbar_width,
sbar_arrow_width=sbar_arrow_width,
sbar_frame_color=sbar_frame_color,
sbar_relief=sbar_relief,
)

View File

@ -0,0 +1,168 @@
from __future__ import annotations
import tkinter as tk
import FreeSimpleGUI
from FreeSimpleGUI import ELEM_TYPE_INPUT_OPTION_MENU
from FreeSimpleGUI import Element
from FreeSimpleGUI._utils import _error_popup_with_traceback
class OptionMenu(Element):
"""
Option Menu is an Element available ONLY on the tkinter port of PySimpleGUI. It is a widget that is unique
to tkinter. However, it looks much like a ComboBox. Instead of an arrow to click to pull down the list of
choices, another little graphic is shown on the widget to indicate where you click. After clicking to activate,
it looks like a Combo Box that you scroll to select a choice.
"""
def __init__(
self,
values,
default_value=None,
size=(None, None),
s=(None, None),
disabled=False,
auto_size_text=None,
expand_x=False,
expand_y=False,
background_color=None,
text_color=None,
key=None,
k=None,
pad=None,
p=None,
tooltip=None,
visible=True,
metadata=None,
):
"""
:param values: Values to be displayed
:type values: List[Any] or Tuple[Any]
:param default_value: the value to choose by default
:type default_value: (Any)
:param size: (width, height) size in characters (wide), height is ignored and present to be consistent with other elements
:type size: (int, int) (width, UNUSED)
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int, int) | (None, None) | int
:param disabled: control enabled / disabled
:type disabled: (bool)
:param auto_size_text: True if size of Element should match the contents of the items
:type auto_size_text: (bool)
:param expand_x: If True the element will automatically expand in the X direction to fill available space
:type expand_x: (bool)
:param expand_y: If True the element will automatically expand in the Y direction to fill available space
:type expand_y: (bool)
:param background_color: color of background
:type background_color: (str)
:param text_color: color of the text
:type text_color: (str)
:param key: Used with window.find_element and with return values to uniquely identify this element
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param tooltip: text that will appear when mouse hovers over this element
:type tooltip: (str)
:param visible: set visibility state of the element
:type visible: (bool)
:param metadata: User metadata that can be set to ANYTHING
:type metadata: (Any)
"""
self.Values = values
self.DefaultValue = default_value
self.Widget = self.TKOptionMenu = None # type: tk.OptionMenu
self.Disabled = disabled
bg = background_color if background_color else FreeSimpleGUI.DEFAULT_INPUT_ELEMENTS_COLOR
fg = text_color if text_color is not None else FreeSimpleGUI.DEFAULT_INPUT_TEXT_COLOR
key = key if key is not None else k
sz = size if size != (None, None) else s
pad = pad if pad is not None else p
self.expand_x = expand_x
self.expand_y = expand_y
super().__init__(
ELEM_TYPE_INPUT_OPTION_MENU,
size=sz,
auto_size_text=auto_size_text,
background_color=bg,
text_color=fg,
key=key,
pad=pad,
tooltip=tooltip,
visible=visible,
metadata=metadata,
)
def update(self, value=None, values=None, disabled=None, visible=None, size=(None, None)):
"""
Changes some of the settings for the OptionMenu Element. Must call `Window.Read` or `Window.Finalize` prior
Changes will not be visible in your window until you call window.read or window.refresh.
If you change visibility, your element may MOVE. If you want it to remain stationary, use the "layout helper"
function "pin" to ensure your element is "pinned" to that location in your layout so that it returns there
when made visible.
:param value: the value to choose by default
:type value: (Any)
:param values: Values to be displayed
:type values: List[Any]
:param disabled: disable or enable state of the element
:type disabled: (bool)
:param visible: control visibility of element
:type visible: (bool)
:param size: (width, height) size in characters (wide), height is ignored and present to be consistent with other elements
:type size: (int, int) (width, UNUSED)
"""
if not self._widget_was_created(): # if widget hasn't been created yet, then don't allow
return
if self._this_elements_window_closed():
_error_popup_with_traceback('Error in OptionMenu.update - The window was closed')
return
if values is not None:
self.Values = values
self.TKOptionMenu['menu'].delete(0, 'end')
# Insert list of new options (tk._setit hooks them up to var)
# self.TKStringVar.set(self.Values[0])
for new_value in self.Values:
self.TKOptionMenu['menu'].add_command(label=new_value, command=tk._setit(self.TKStringVar, new_value))
if value is None:
self.TKStringVar.set('')
if size == (None, None):
max_line_len = max([len(str(line)) for line in self.Values]) if len(self.Values) else 0
if self.AutoSizeText is False:
width = self.Size[0]
else:
width = max_line_len + 1
self.TKOptionMenu.configure(width=width)
else:
self.TKOptionMenu.configure(width=size[0])
if value is not None:
self.DefaultValue = value
self.TKStringVar.set(value)
if disabled is True:
self.TKOptionMenu['state'] = 'disabled'
elif disabled is False:
self.TKOptionMenu['state'] = 'normal'
self.Disabled = disabled if disabled is not None else self.Disabled
if visible is False:
self._pack_forget_save_settings()
# self.TKOptionMenu.pack_forget()
elif visible is True:
self._pack_restore_settings()
# self.TKOptionMenu.pack(padx=self.pad_used[0], pady=self.pad_used[1])
if visible is not None:
self._visible = visible
Update = update

View File

@ -0,0 +1,127 @@
from __future__ import annotations
import FreeSimpleGUI
from FreeSimpleGUI import ELEM_TYPE_PANE
from FreeSimpleGUI import Element
from FreeSimpleGUI import RELIEF_RAISED
from FreeSimpleGUI._utils import _error_popup_with_traceback
class Pane(Element):
"""
A sliding Pane that is unique to tkinter. Uses Columns to create individual panes
"""
def __init__(
self,
pane_list,
background_color=None,
size=(None, None),
s=(None, None),
pad=None,
p=None,
orientation='vertical',
show_handle=True,
relief=RELIEF_RAISED,
handle_size=None,
border_width=None,
key=None,
k=None,
expand_x=None,
expand_y=None,
visible=True,
metadata=None,
):
"""
:param pane_list: Must be a list of Column Elements. Each Column supplied becomes one pane that's shown
:type pane_list: List[Column] | Tuple[Column]
:param background_color: color of background
:type background_color: (str)
:param size: (width, height) w=characters-wide, h=rows-high How much room to reserve for the Pane
:type size: (int, int)
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int, int) | (None, None)
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param orientation: 'horizontal' or 'vertical' or ('h' or 'v'). Direction the Pane should slide
:type orientation: (str)
:param show_handle: if True, the handle is drawn that makes it easier to grab and slide
:type show_handle: (bool)
:param relief: relief style. Values are same as other elements that use relief values. RELIEF_RAISED RELIEF_SUNKEN RELIEF_FLAT RELIEF_RIDGE RELIEF_GROOVE RELIEF_SOLID
:type relief: (enum)
:param handle_size: Size of the handle in pixels
:type handle_size: (int)
:param border_width: width of border around element in pixels
:type border_width: (int)
:param key: Value that uniquely identifies this element from all other elements. Used when Finding an element or in return values. Must be unique to the window
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param expand_x: If True the column will automatically expand in the X direction to fill available space
:type expand_x: (bool)
:param expand_y: If True the column will automatically expand in the Y direction to fill available space
:type expand_y: (bool)
:param visible: set visibility state of the element
:type visible: (bool)
:param metadata: User metadata that can be set to ANYTHING
:type metadata: (Any)
"""
self.UseDictionary = False
self.ReturnValues = None
self.ReturnValuesList = []
self.ReturnValuesDictionary = {}
self.DictionaryKeyCounter = 0
self.ParentWindow = None
self.Rows = []
self.TKFrame = None
self.PanedWindow = None
self.Orientation = orientation
self.PaneList = pane_list
self.ShowHandle = show_handle
self.Relief = relief
self.HandleSize = handle_size or 8
self.BorderDepth = border_width
bg = background_color if background_color is not None else FreeSimpleGUI.DEFAULT_BACKGROUND_COLOR
self.Rows = [pane_list]
key = key if key is not None else k
sz = size if size != (None, None) else s
pad = pad if pad is not None else p
self.expand_x = expand_x
self.expand_y = expand_y
super().__init__(ELEM_TYPE_PANE, background_color=bg, size=sz, pad=pad, key=key, visible=visible, metadata=metadata)
return
def update(self, visible=None):
"""
Changes some of the settings for the Pane Element. Must call `Window.Read` or `Window.Finalize` prior
Changes will not be visible in your window until you call window.read or window.refresh.
If you change visibility, your element may MOVE. If you want it to remain stationary, use the "layout helper"
function "pin" to ensure your element is "pinned" to that location in your layout so that it returns there
when made visible.
:param visible: control visibility of element
:type visible: (bool)
"""
if not self._widget_was_created(): # if widget hasn't been created yet, then don't allow
return
if self._this_elements_window_closed():
_error_popup_with_traceback('Error in Pane.update - The window was closed')
return
if visible is False:
self._pack_forget_save_settings()
elif visible is True:
self._pack_restore_settings()
if visible is not None:
self._visible = visible
Update = update

View File

@ -0,0 +1,304 @@
from __future__ import annotations
import tkinter as tk
from tkinter import ttk
import FreeSimpleGUI
from FreeSimpleGUI import _change_ttk_theme
from FreeSimpleGUI import COLOR_SYSTEM_DEFAULT
from FreeSimpleGUI import ELEM_TYPE_PROGRESS_BAR
from FreeSimpleGUI import Element
from FreeSimpleGUI._utils import _error_popup_with_traceback
from FreeSimpleGUI.elements.helpers import _simplified_dual_color_to_tuple
from FreeSimpleGUI.window import Window
class TKProgressBar:
uniqueness_counter = 0
def __init__(
self,
root,
max,
length=400,
width=FreeSimpleGUI.DEFAULT_PROGRESS_BAR_SIZE[1],
ttk_theme=FreeSimpleGUI.DEFAULT_TTK_THEME,
style_name='',
relief=FreeSimpleGUI.DEFAULT_PROGRESS_BAR_RELIEF,
border_width=FreeSimpleGUI.DEFAULT_PROGRESS_BAR_BORDER_WIDTH,
orientation='horizontal',
BarColor=(None, None),
key=None,
):
"""
:param root: The root window bar is to be shown in
:type root: tk.Tk | tk.TopLevel
:param max: Maximum value the bar will be measuring
:type max: (int)
:param length: length in pixels of the bar
:type length: (int)
:param width: width in pixels of the bar
:type width: (int)
:param style_name: Progress bar style to use. Set in the packer function
:type style_name: (str)
:param ttk_theme: Progress bar style defined as one of these 'default', 'winnative', 'clam', 'alt', 'classic', 'vista', 'xpnative'
:type ttk_theme: (str)
:param relief: relief style. Values are same as progress meter relief values. Can be a constant or a string: `RELIEF_RAISED RELIEF_SUNKEN RELIEF_FLAT RELIEF_RIDGE RELIEF_GROOVE RELIEF_SOLID` (Default value = DEFAULT_PROGRESS_BAR_RELIEF)
:type relief: (str)
:param border_width: The amount of pixels that go around the outside of the bar
:type border_width: (int)
:param orientation: 'horizontal' or 'vertical' ('h' or 'v' work) (Default value = 'vertical')
:type orientation: (str)
:param BarColor: The 2 colors that make up a progress bar. One is the background, the other is the bar
:type BarColor: (str, str)
:param key: Used with window.find_element and with return values to uniquely identify this element to uniquely identify this element
:type key: str | int | tuple | object
"""
self.Length = length
self.Width = width
self.Max = max
self.Orientation = orientation
self.Count = None
self.PriorCount = 0
self.style_name = style_name
TKProgressBar.uniqueness_counter += 1
if orientation.lower().startswith('h'):
s = ttk.Style()
_change_ttk_theme(s, ttk_theme)
if BarColor != COLOR_SYSTEM_DEFAULT and BarColor[0] != COLOR_SYSTEM_DEFAULT:
s.configure(
self.style_name,
background=BarColor[0],
troughcolor=BarColor[1],
troughrelief=relief,
borderwidth=border_width,
thickness=width,
)
else:
s.configure(self.style_name, troughrelief=relief, borderwidth=border_width, thickness=width)
self.TKProgressBarForReal = ttk.Progressbar(root, maximum=self.Max, style=self.style_name, length=length, orient=tk.HORIZONTAL, mode='determinate')
else:
s = ttk.Style()
_change_ttk_theme(s, ttk_theme)
if BarColor != COLOR_SYSTEM_DEFAULT and BarColor[0] != COLOR_SYSTEM_DEFAULT:
s.configure(
self.style_name,
background=BarColor[0],
troughcolor=BarColor[1],
troughrelief=relief,
borderwidth=border_width,
thickness=width,
)
else:
s.configure(self.style_name, troughrelief=relief, borderwidth=border_width, thickness=width)
self.TKProgressBarForReal = ttk.Progressbar(root, maximum=self.Max, style=self.style_name, length=length, orient=tk.VERTICAL, mode='determinate')
def Update(self, count=None, max=None):
"""
Update the current value of the bar and/or update the maximum value the bar can reach
:param count: current value
:type count: (int)
:param max: the maximum value
:type max: (int)
"""
if max is not None:
self.Max = max
try:
self.TKProgressBarForReal.config(maximum=max)
except:
return False
if count is not None:
try:
self.TKProgressBarForReal['value'] = count
except:
return False
return True
class ProgressBar(Element):
"""
Progress Bar Element - Displays a colored bar that is shaded as progress of some operation is made
"""
def __init__(
self,
max_value,
orientation=None,
size=(None, None),
s=(None, None),
size_px=(None, None),
auto_size_text=None,
bar_color=None,
style=None,
border_width=None,
relief=None,
key=None,
k=None,
pad=None,
p=None,
right_click_menu=None,
expand_x=False,
expand_y=False,
visible=True,
metadata=None,
):
"""
:param max_value: max value of progressbar
:type max_value: (int)
:param orientation: 'horizontal' or 'vertical'
:type orientation: (str)
:param size: Size of the bar. If horizontal (chars long, pixels wide), vert (chars high, pixels wide). Vert height measured using horizontal chars units.
:type size: (int, int) | (int, None)
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int, int) | (None, None)
:param size_px: Size in pixels (length, width). Will be used in place of size parm if specified
:type size_px: (int, int) | (None, None)
:param auto_size_text: Not sure why this is here
:type auto_size_text: (bool)
:param bar_color: The 2 colors that make up a progress bar. Either a tuple of 2 strings or a string. Tuple - (bar, background). A string with 1 color changes the background of the bar only. A string with 2 colors separated by "on" like "red on blue" specifies a red bar on a blue background.
:type bar_color: (str, str) or str
:param style: Progress bar style defined as one of these 'default', 'winnative', 'clam', 'alt', 'classic', 'vista', 'xpnative'
:type style: (str)
:param border_width: The amount of pixels that go around the outside of the bar
:type border_width: (int)
:param relief: relief style. Values are same as progress meter relief values. Can be a constant or a string: `RELIEF_RAISED RELIEF_SUNKEN RELIEF_FLAT RELIEF_RIDGE RELIEF_GROOVE RELIEF_SOLID` (Default value = DEFAULT_PROGRESS_BAR_RELIEF)
:type relief: (str)
:param key: Used with window.find_element and with return values to uniquely identify this element to uniquely identify this element
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param right_click_menu: A list of lists of Menu items to show when this element is right clicked. See user docs for exact format.
:type right_click_menu: List[List[ List[str] | str ]]
:param expand_x: If True the element will automatically expand in the X direction to fill available space
:type expand_x: (bool)
:param expand_y: If True the element will automatically expand in the Y direction to fill available space
:type expand_y: (bool)
:param visible: set visibility state of the element
:type visible: (bool)
:param metadata: User metadata that can be set to ANYTHING
:type metadata: (Any)
"""
self.MaxValue = max_value
self.TKProgressBar = None # type: TKProgressBar
self.Cancelled = False
self.NotRunning = True
self.Orientation = orientation if orientation else FreeSimpleGUI.DEFAULT_METER_ORIENTATION
self.RightClickMenu = right_click_menu
# Progress Bar colors can be a tuple (text, background) or a string with format "bar on background" - examples "red on white" or ("red", "white")
if bar_color is None:
bar_color = FreeSimpleGUI.DEFAULT_PROGRESS_BAR_COLOR
else:
bar_color = _simplified_dual_color_to_tuple(bar_color, default=FreeSimpleGUI.DEFAULT_PROGRESS_BAR_COLOR)
self.BarColor = bar_color # should be a tuple at this point
self.BarStyle = style if style else FreeSimpleGUI.DEFAULT_TTK_THEME
self.BorderWidth = border_width if border_width else FreeSimpleGUI.DEFAULT_PROGRESS_BAR_BORDER_WIDTH
self.Relief = relief if relief else FreeSimpleGUI.DEFAULT_PROGRESS_BAR_RELIEF
self.BarExpired = False
key = key if key is not None else k
sz = size if size != (None, None) else s
pad = pad if pad is not None else p
self.expand_x = expand_x
self.expand_y = expand_y
self.size_px = size_px
super().__init__(
ELEM_TYPE_PROGRESS_BAR,
size=sz,
auto_size_text=auto_size_text,
key=key,
pad=pad,
visible=visible,
metadata=metadata,
)
# returns False if update failed
def update_bar(self, current_count, max=None):
"""
DEPRECATED BUT STILL USABLE - has been combined with the normal ProgressBar.update method.
Change what the bar shows by changing the current count and optionally the max count
:param current_count: sets the current value
:type current_count: (int)
:param max: changes the max value
:type max: (int)
"""
if self.ParentForm.TKrootDestroyed:
return False
self.TKProgressBar.Update(current_count, max=max)
try:
self.ParentForm.TKroot.update()
except:
Window._DecrementOpenCount()
# _my_windows.Decrement()
return False
return True
def update(self, current_count=None, max=None, bar_color=None, visible=None):
"""
Changes some of the settings for the ProgressBar Element. Must call `Window.Read` or `Window.Finalize` prior
Now has the ability to modify the count so that the update_bar method is not longer needed separately
Changes will not be visible in your window until you call window.read or window.refresh.
If you change visibility, your element may MOVE. If you want it to remain stationary, use the "layout helper"
function "pin" to ensure your element is "pinned" to that location in your layout so that it returns there
when made visible.
:param current_count: sets the current value
:type current_count: (int)
:param max: changes the max value
:type max: (int)
:param bar_color: The 2 colors that make up a progress bar. Easy to remember which is which if you say "ON" between colors. "red" on "green".
:type bar_color: (str, str) or str
:param visible: control visibility of element
:type visible: (bool)
:return: Returns True if update was OK. False means something wrong with window or it was closed
:rtype: (bool)
"""
if not self._widget_was_created(): # if widget hasn't been created yet, then don't allow
return False
if self._this_elements_window_closed():
_error_popup_with_traceback('Error in ProgressBar.update - The window was closed')
return
if self.ParentForm.TKrootDestroyed:
return False
if visible is False:
self._pack_forget_save_settings()
elif visible is True:
self._pack_restore_settings()
if visible is not None:
self._visible = visible
if bar_color is not None:
bar_color = _simplified_dual_color_to_tuple(bar_color, default=FreeSimpleGUI.DEFAULT_PROGRESS_BAR_COLOR)
self.BarColor = bar_color
style = ttk.Style()
style.configure(self.ttk_style_name, background=bar_color[0], troughcolor=bar_color[1])
if current_count is not None:
self.TKProgressBar.Update(current_count, max=max)
try:
self.ParentForm.TKroot.update()
except:
return False
return True
Update = update
UpdateBar = update_bar

View File

@ -0,0 +1,252 @@
from __future__ import annotations
import tkinter as tk # noqa
from FreeSimpleGUI import _hex_to_hsl
from FreeSimpleGUI import _hsl_to_rgb
from FreeSimpleGUI import COLOR_SYSTEM_DEFAULT
from FreeSimpleGUI import ELEM_TYPE_INPUT_RADIO
from FreeSimpleGUI import Element
from FreeSimpleGUI import rgb
from FreeSimpleGUI import theme_background_color
from FreeSimpleGUI import theme_text_color
from FreeSimpleGUI._utils import _error_popup_with_traceback
class Radio(Element):
"""
Radio Button Element - Used in a group of other Radio Elements to provide user with ability to select only
1 choice in a list of choices.
"""
def __init__(
self,
text,
group_id,
default=False,
disabled=False,
size=(None, None),
s=(None, None),
auto_size_text=None,
background_color=None,
text_color=None,
circle_color=None,
font=None,
key=None,
k=None,
pad=None,
p=None,
tooltip=None,
change_submits=False,
enable_events=False,
right_click_menu=None,
expand_x=False,
expand_y=False,
visible=True,
metadata=None,
):
"""
:param text: Text to display next to button
:type text: (str)
:param group_id: Groups together multiple Radio Buttons. Any type works
:type group_id: (Any)
:param default: Set to True for the one element of the group you want initially selected
:type default: (bool)
:param disabled: set disable state
:type disabled: (bool)
:param size: (w, h) w=characters-wide, h=rows-high. If an int instead of a tuple is supplied, then height is auto-set to 1
:type size: (int, int) | (None, None) | int
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int, int) | (None, None) | int
:param auto_size_text: if True will size the element to match the length of the text
:type auto_size_text: (bool)
:param background_color: color of background
:type background_color: (str)
:param text_color: color of the text
:type text_color: (str)
:param circle_color: color of background of the circle that has the dot selection indicator in it
:type circle_color: (str)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param key: Used with window.find_element and with return values to uniquely identify this element
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param tooltip: text, that will appear when mouse hovers over the element
:type tooltip: (str)
:param change_submits: DO NOT USE. Only listed for backwards compat - Use enable_events instead
:type change_submits: (bool)
:param enable_events: Turns on the element specific events. Radio Button events happen when an item is selected
:type enable_events: (bool)
:param right_click_menu: A list of lists of Menu items to show when this element is right clicked. See user docs for exact format.
:type right_click_menu: List[List[ List[str] | str ]]
:param expand_x: If True the element will automatically expand in the X direction to fill available space
:type expand_x: (bool)
:param expand_y: If True the element will automatically expand in the Y direction to fill available space
:type expand_y: (bool)
:param visible: set visibility state of the element
:type visible: (bool)
:param metadata: User metadata that can be set to ANYTHING
:type metadata: (Any)
"""
self.InitialState = default
self.Text = text
self.Widget = self.TKRadio = None # type: tk.Radiobutton
self.GroupID = group_id
self.Value = None
self.Disabled = disabled
self.TextColor = text_color if text_color else theme_text_color()
self.RightClickMenu = right_click_menu
if circle_color is None:
# ---- compute color of circle background ---
try: # something in here will fail if a color is not specified in Hex
text_hsl = _hex_to_hsl(self.TextColor)
background_hsl = _hex_to_hsl(background_color if background_color else theme_background_color())
l_delta = abs(text_hsl[2] - background_hsl[2]) / 10
if text_hsl[2] > background_hsl[2]: # if the text is "lighter" than the background then make background darker
bg_rbg = _hsl_to_rgb(background_hsl[0], background_hsl[1], background_hsl[2] - l_delta)
else:
bg_rbg = _hsl_to_rgb(background_hsl[0], background_hsl[1], background_hsl[2] + l_delta)
self.CircleBackgroundColor = rgb(*bg_rbg)
except:
self.CircleBackgroundColor = background_color if background_color else theme_background_color()
else:
self.CircleBackgroundColor = circle_color
self.ChangeSubmits = change_submits or enable_events
self.EncodedRadioValue = None
key = key if key is not None else k
sz = size if size != (None, None) else s
pad = pad if pad is not None else p
self.expand_x = expand_x
self.expand_y = expand_y
super().__init__(
ELEM_TYPE_INPUT_RADIO,
size=sz,
auto_size_text=auto_size_text,
font=font,
background_color=background_color,
text_color=self.TextColor,
key=key,
pad=pad,
tooltip=tooltip,
visible=visible,
metadata=metadata,
)
def update(
self,
value=None,
text=None,
background_color=None,
text_color=None,
circle_color=None,
disabled=None,
visible=None,
):
"""
Changes some of the settings for the Radio Button Element. Must call `Window.read` or `Window.finalize` prior
Changes will not be visible in your window until you call window.read or window.refresh.
If you change visibility, your element may MOVE. If you want it to remain stationary, use the "layout helper"
function "pin" to ensure your element is "pinned" to that location in your layout so that it returns there
when made visible.
:param value: if True change to selected and set others in group to unselected
:type value: (bool)
:param text: Text to display next to radio button
:type text: (str)
:param background_color: color of background
:type background_color: (str)
:param text_color: color of the text. Note this also changes the color of the selection dot
:type text_color: (str)
:param circle_color: color of background of the circle that has the dot selection indicator in it
:type circle_color: (str)
:param disabled: disable or enable state of the element
:type disabled: (bool)
:param visible: control visibility of element
:type visible: (bool)
"""
if not self._widget_was_created(): # if widget hasn't been created yet, then don't allow
return
if self._this_elements_window_closed():
_error_popup_with_traceback('Error in Radio.update - The window was closed')
return
if value is not None:
try:
if value is True:
self.TKIntVar.set(self.EncodedRadioValue)
elif value is False:
if self.TKIntVar.get() == self.EncodedRadioValue:
self.TKIntVar.set(0)
except:
print('Error updating Radio')
self.InitialState = value
if text is not None:
self.Text = str(text)
self.TKRadio.configure(text=self.Text)
if background_color not in (None, COLOR_SYSTEM_DEFAULT):
self.TKRadio.configure(background=background_color)
self.BackgroundColor = background_color
if text_color not in (None, COLOR_SYSTEM_DEFAULT):
self.TKRadio.configure(fg=text_color)
self.TextColor = text_color
if circle_color not in (None, COLOR_SYSTEM_DEFAULT):
self.CircleBackgroundColor = circle_color
self.TKRadio.configure(selectcolor=self.CircleBackgroundColor) # The background of the radio button
elif text_color or background_color:
if self.TextColor not in (None, COLOR_SYSTEM_DEFAULT) and self.BackgroundColor not in (None, COLOR_SYSTEM_DEFAULT) and self.TextColor.startswith('#') and self.BackgroundColor.startswith('#'):
# ---- compute color of circle background ---
text_hsl = _hex_to_hsl(self.TextColor)
background_hsl = _hex_to_hsl(self.BackgroundColor if self.BackgroundColor else theme_background_color())
l_delta = abs(text_hsl[2] - background_hsl[2]) / 10
if text_hsl[2] > background_hsl[2]: # if the text is "lighter" than the background then make background darker
bg_rbg = _hsl_to_rgb(background_hsl[0], background_hsl[1], background_hsl[2] - l_delta)
else:
bg_rbg = _hsl_to_rgb(background_hsl[0], background_hsl[1], background_hsl[2] + l_delta)
self.CircleBackgroundColor = rgb(*bg_rbg)
self.TKRadio.configure(selectcolor=self.CircleBackgroundColor) # The background of the checkbox
if disabled is True:
self.TKRadio['state'] = 'disabled'
elif disabled is False:
self.TKRadio['state'] = 'normal'
self.Disabled = disabled if disabled is not None else self.Disabled
if visible is False:
self._pack_forget_save_settings()
elif visible is True:
self._pack_restore_settings()
if visible is not None:
self._visible = visible
def reset_group(self):
"""
Sets all Radio Buttons in the group to not selected
"""
self.TKIntVar.set(0)
def get(self):
# type: (Radio) -> bool
"""
A snapshot of the value of Radio Button -> (bool)
:return: True if this radio button is selected
:rtype: (bool)
"""
return self.TKIntVar.get() == self.EncodedRadioValue
Get = get
ResetGroup = reset_group
Update = update

View File

@ -0,0 +1,62 @@
from __future__ import annotations
from FreeSimpleGUI import ELEM_TYPE_SEPARATOR
from FreeSimpleGUI import Element
from FreeSimpleGUI import theme_text_color
class VerticalSeparator(Element):
"""
Vertical Separator Element draws a vertical line at the given location. It will span 1 "row". Usually paired with
Column Element if extra height is needed
"""
def __init__(self, color=None, pad=None, p=None, key=None, k=None):
"""
:param color: Color of the line. Defaults to theme's text color. Can be name or #RRGGBB format
:type color: (str)
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param key: Value that uniquely identifies this element from all other elements. Used when Finding an element or in return values. Must be unique to the window
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
"""
key = key if key is not None else k
pad = pad if pad is not None else p
self.expand_x = None
self.expand_y = None
self.Orientation = 'vertical' # for now only vertical works
self.color = color if color is not None else theme_text_color()
super().__init__(ELEM_TYPE_SEPARATOR, pad=pad, key=key)
class HorizontalSeparator(Element):
"""
Horizontal Separator Element draws a Horizontal line at the given location.
"""
def __init__(self, color=None, pad=None, p=None, key=None, k=None):
"""
:param color: Color of the line. Defaults to theme's text color. Can be name or #RRGGBB format
:type color: (str)
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param key: Value that uniquely identifies this element from all other elements. Used when Finding an element or in return values. Must be unique to the window
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
"""
self.Orientation = 'horizontal' # for now only vertical works
self.color = color if color is not None else theme_text_color()
self.expand_x = True
self.expand_y = None
key = key if key is not None else k
pad = pad if pad is not None else p
super().__init__(ELEM_TYPE_SEPARATOR, pad=pad, key=key)

View File

@ -0,0 +1,34 @@
from __future__ import annotations
from FreeSimpleGUI import ELEM_TYPE_SIZEGRIP
from FreeSimpleGUI import Element
from FreeSimpleGUI import theme_background_color
class Sizegrip(Element):
"""
Sizegrip element will be added to the bottom right corner of your window.
It should be placed on the last row of your window along with any other elements on that row.
The color will match the theme's background color.
"""
def __init__(self, background_color=None, pad=None, p=(0, 0), key=None, k=None):
"""
Sizegrip Element
:param background_color: color to use for the background of the grip
:type background_color: str
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param key: Value that uniquely identifies this element from all other elements. Used when Finding an element or in return values. Must be unique to the window
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
"""
bg = background_color if background_color is not None else theme_background_color()
pad = pad if pad is not None else p
key = key if key is not None else k
super().__init__(ELEM_TYPE_SIZEGRIP, background_color=bg, key=key, pad=pad)

View File

@ -0,0 +1,199 @@
from __future__ import annotations
import tkinter as tk
import FreeSimpleGUI
from FreeSimpleGUI import ELEM_TYPE_INPUT_SLIDER
from FreeSimpleGUI import Element
from FreeSimpleGUI._utils import _error_popup_with_traceback
from FreeSimpleGUI._utils import _exit_mainloop
class Slider(Element):
"""
A slider, horizontal or vertical
"""
def __init__(
self,
range=(None, None),
default_value=None,
resolution=None,
tick_interval=None,
orientation=None,
disable_number_display=False,
border_width=None,
relief=None,
change_submits=False,
enable_events=False,
disabled=False,
size=(None, None),
s=(None, None),
font=None,
background_color=None,
text_color=None,
trough_color=None,
key=None,
k=None,
pad=None,
p=None,
expand_x=False,
expand_y=False,
tooltip=None,
visible=True,
metadata=None,
):
"""
:param range: slider's range (min value, max value)
:type range: (int, int) | Tuple[float, float]
:param default_value: starting value for the slider
:type default_value: int | float
:param resolution: the smallest amount the slider can be moved
:type resolution: int | float
:param tick_interval: how often a visible tick should be shown next to slider
:type tick_interval: int | float
:param orientation: 'horizontal' or 'vertical' ('h' or 'v' also work)
:type orientation: (str)
:param disable_number_display: if True no number will be displayed by the Slider Element
:type disable_number_display: (bool)
:param border_width: width of border around element in pixels
:type border_width: (int)
:param relief: relief style. Use constants - RELIEF_RAISED RELIEF_SUNKEN RELIEF_FLAT RELIEF_RIDGE RELIEF_GROOVE RELIEF_SOLID
:type relief: str | None
:param change_submits: * DEPRICATED DO NOT USE. Use `enable_events` instead
:type change_submits: (bool)
:param enable_events: If True then moving the slider will generate an Event
:type enable_events: (bool)
:param disabled: set disable state for element
:type disabled: (bool)
:param size: (l=length chars/rows, w=width pixels)
:type size: (int, int)
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int, int) | (None, None)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param background_color: color of slider's background
:type background_color: (str)
:param text_color: color of the slider's text
:type text_color: (str)
:param trough_color: color of the slider's trough
:type trough_color: (str)
:param key: Value that uniquely identifies this element from all other elements. Used when Finding an element or in return values. Must be unique to the window
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param expand_x: If True the element will automatically expand in the X direction to fill available space
:type expand_x: (bool)
:param expand_y: If True the element will automatically expand in the Y direction to fill available space
:type expand_y: (bool)
:param tooltip: text, that will appear when mouse hovers over the element
:type tooltip: (str)
:param visible: set visibility state of the element
:type visible: (bool)
:param metadata: User metadata that can be set to ANYTHING
:type metadata: (Any)
"""
self.TKScale = self.Widget = None # type: tk.Scale
self.Range = (1, 10) if range == (None, None) else range
self.DefaultValue = self.Range[0] if default_value is None else default_value
self.Orientation = orientation if orientation else FreeSimpleGUI.DEFAULT_SLIDER_ORIENTATION
self.BorderWidth = border_width if border_width else FreeSimpleGUI.DEFAULT_SLIDER_BORDER_WIDTH
self.Relief = relief if relief else FreeSimpleGUI.DEFAULT_SLIDER_RELIEF
self.Resolution = 1 if resolution is None else resolution
self.ChangeSubmits = change_submits or enable_events
self.Disabled = disabled
self.TickInterval = tick_interval
self.DisableNumericDisplay = disable_number_display
self.TroughColor = trough_color or FreeSimpleGUI.DEFAULT_SCROLLBAR_COLOR
sz = size if size != (None, None) else s
temp_size = sz
if temp_size == (None, None):
temp_size = (20, 20) if self.Orientation.startswith('h') else (8, 20)
key = key if key is not None else k
pad = pad if pad is not None else p
self.expand_x = expand_x
self.expand_y = expand_y
super().__init__(
ELEM_TYPE_INPUT_SLIDER,
size=temp_size,
font=font,
background_color=background_color,
text_color=text_color,
key=key,
pad=pad,
tooltip=tooltip,
visible=visible,
metadata=metadata,
)
return
def update(self, value=None, range=(None, None), disabled=None, visible=None):
"""
Changes some of the settings for the Slider Element. Must call `Window.Read` or `Window.Finalize` prior
Changes will not be visible in your window until you call window.read or window.refresh.
If you change visibility, your element may MOVE. If you want it to remain stationary, use the "layout helper"
function "pin" to ensure your element is "pinned" to that location in your layout so that it returns there
when made visible.
:param value: sets current slider value
:type value: int | float
:param range: Sets a new range for slider
:type range: (int, int) | Tuple[float, float
:param disabled: disable or enable state of the element
:type disabled: (bool)
:param visible: control visibility of element
:type visible: (bool)
"""
if not self._widget_was_created(): # if widget hasn't been created yet, then don't allow
return
if self._this_elements_window_closed():
_error_popup_with_traceback('Error in Slider.update - The window was closed')
return
if range != (None, None):
self.TKScale.config(from_=range[0], to_=range[1])
if value is not None:
try:
self.TKIntVar.set(value)
except:
pass
self.DefaultValue = value
if disabled is True:
self.TKScale['state'] = 'disabled'
elif disabled is False:
self.TKScale['state'] = 'normal'
self.Disabled = disabled if disabled is not None else self.Disabled
if visible is False:
self._pack_forget_save_settings()
elif visible is True:
self._pack_restore_settings()
if visible is not None:
self._visible = visible
def _SliderChangedHandler(self, event):
"""
Not user callable. Callback function for when slider is moved.
:param event: (event) the event data provided by tkinter. Unknown format. Not used.
:type event:
"""
if self.Key is not None:
self.ParentForm.LastButtonClicked = self.Key
else:
self.ParentForm.LastButtonClicked = ''
self.ParentForm.FormRemainedOpen = True
_exit_mainloop(self.ParentForm)
Update = update

View File

@ -0,0 +1,252 @@
from __future__ import annotations
import tkinter as tk
import FreeSimpleGUI
from FreeSimpleGUI import ELEM_TYPE_INPUT_SPIN
from FreeSimpleGUI import Element
from FreeSimpleGUI._utils import _error_popup_with_traceback
from FreeSimpleGUI._utils import _exit_mainloop
class Spin(Element):
"""
A spinner with up/down buttons and a single line of text. Choose 1 values from list
"""
def __init__(
self,
values,
initial_value=None,
disabled=False,
change_submits=False,
enable_events=False,
readonly=False,
size=(None, None),
s=(None, None),
auto_size_text=None,
bind_return_key=None,
font=None,
background_color=None,
text_color=None,
key=None,
k=None,
pad=None,
p=None,
wrap=None,
tooltip=None,
right_click_menu=None,
expand_x=False,
expand_y=False,
visible=True,
metadata=None,
):
"""
:param values: List of valid values
:type values: Tuple[Any] or List[Any]
:param initial_value: Initial item to show in window. Choose from list of values supplied
:type initial_value: (Any)
:param disabled: set disable state
:type disabled: (bool)
:param change_submits: DO NOT USE. Only listed for backwards compat - Use enable_events instead
:type change_submits: (bool)
:param enable_events: Turns on the element specific events. Spin events happen when an item changes
:type enable_events: (bool)
:param readonly: If True, then users cannot type in values. Only values from the values list are allowed.
:type readonly: (bool)
:param size: (w, h) w=characters-wide, h=rows-high. If an int instead of a tuple is supplied, then height is auto-set to 1
:type size: (int, int) | (None, None) | int
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int, int) | (None, None) | int
:param auto_size_text: if True will size the element to match the length of the text
:type auto_size_text: (bool)
:param bind_return_key: If True, then the return key will cause a the element to generate an event when return key is pressed
:type bind_return_key: (bool)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param background_color: color of background
:type background_color: (str)
:param text_color: color of the text
:type text_color: (str)
:param key: Used with window.find_element and with return values to uniquely identify this element
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param wrap: Determines if the values should "Wrap". Default is False. If True, when reaching last value, will continue back to the first value.
:type wrap: (bool)
:param tooltip: text, that will appear when mouse hovers over the element
:type tooltip: (str)
:param right_click_menu: A list of lists of Menu items to show when this element is right clicked. See user docs for exact format.
:type right_click_menu: List[List[ List[str] | str ]]
:param expand_x: If True the element will automatically expand in the X direction to fill available space
:type expand_x: (bool)
:param expand_y: If True the element will automatically expand in the Y direction to fill available space
:type expand_y: (bool)
:param visible: set visibility state of the element
:type visible: (bool)
:param metadata: User metadata that can be set to ANYTHING
:type metadata: (Any)
"""
self.Values = values
self.DefaultValue = initial_value
self.ChangeSubmits = change_submits or enable_events
self.TKSpinBox = self.Widget = None # type: tk.Spinbox
self.Disabled = disabled
self.Readonly = readonly
self.RightClickMenu = right_click_menu
self.BindReturnKey = bind_return_key
self.wrap = wrap
bg = background_color if background_color else FreeSimpleGUI.DEFAULT_INPUT_ELEMENTS_COLOR
fg = text_color if text_color is not None else FreeSimpleGUI.DEFAULT_INPUT_TEXT_COLOR
key = key if key is not None else k
sz = size if size != (None, None) else s
pad = pad if pad is not None else p
self.expand_x = expand_x
self.expand_y = expand_y
super().__init__(
ELEM_TYPE_INPUT_SPIN,
size=sz,
auto_size_text=auto_size_text,
font=font,
background_color=bg,
text_color=fg,
key=key,
pad=pad,
tooltip=tooltip,
visible=visible,
metadata=metadata,
)
return
def update(self, value=None, values=None, disabled=None, readonly=None, visible=None):
"""
Changes some of the settings for the Spin Element. Must call `Window.Read` or `Window.Finalize` prior
Note that the state can be in 3 states only.... enabled, disabled, readonly even
though more combinations are available. The easy way to remember is that if you
change the readonly parameter then you are enabling the element.
Changes will not be visible in your window until you call window.read or window.refresh.
If you change visibility, your element may MOVE. If you want it to remain stationary, use the "layout helper"
function "pin" to ensure your element is "pinned" to that location in your layout so that it returns there
when made visible.
:param value: set the current value from list of choices
:type value: (Any)
:param values: set available choices
:type values: List[Any]
:param disabled: disable. Note disabled and readonly cannot be mixed. It must be one OR the other
:type disabled: (bool)
:param readonly: make element readonly. Note disabled and readonly cannot be mixed. It must be one OR the other
:type readonly: (bool)
:param visible: control visibility of element
:type visible: (bool)
"""
if not self._widget_was_created(): # if widget hasn't been created yet, then don't allow
return
if self._this_elements_window_closed():
_error_popup_with_traceback('Error in Spin.update - The window was closed')
return
if values is not None:
old_value = self.TKStringVar.get()
self.Values = values
self.TKSpinBox.configure(values=values)
self.TKStringVar.set(old_value)
if value is not None:
try:
self.TKStringVar.set(value)
self.DefaultValue = value
except:
pass
if readonly is True:
self.Readonly = True
self.TKSpinBox['state'] = 'readonly'
elif readonly is False:
self.Readonly = False
self.TKSpinBox['state'] = 'normal'
if disabled is True:
self.TKSpinBox['state'] = 'disable'
elif disabled is False:
if self.Readonly:
self.TKSpinBox['state'] = 'readonly'
else:
self.TKSpinBox['state'] = 'normal'
self.Disabled = disabled if disabled is not None else self.Disabled
if visible is False:
self._pack_forget_save_settings()
elif visible is True:
self._pack_restore_settings()
if visible is not None:
self._visible = visible
def _SpinChangedHandler(self, event):
"""
Callback function. Used internally only. Called by tkinter when Spinbox Widget changes. Results in Window.Read() call returning
:param event: passed in from tkinter
:type event:
"""
# first, get the results table built
if self.Key is not None:
self.ParentForm.LastButtonClicked = self.Key
else:
self.ParentForm.LastButtonClicked = ''
self.ParentForm.FormRemainedOpen = True
_exit_mainloop(self.ParentForm)
# if self.ParentForm.CurrentlyRunningMainloop:
# Window._window_that_exited = self.ParentForm
# self.ParentForm.TKroot.quit() # kick the users out of the mainloop
def set_ibeam_color(self, ibeam_color=None):
"""
Sets the color of the I-Beam that is used to "insert" characters. This is oftens called a "Cursor" by
many users. To keep from being confused with tkinter's definition of cursor (the mouse pointer), the term
ibeam is used in this case.
:param ibeam_color: color to set the "I-Beam" used to indicate where characters will be inserted
:type ibeam_color: (str)
"""
if not self._widget_was_created():
return
if ibeam_color is not None:
try:
self.Widget.config(insertbackground=ibeam_color)
except Exception:
_error_popup_with_traceback(
'Error setting I-Beam color in set_ibeam_color',
'The element has a key:',
self.Key,
'The color passed in was:',
ibeam_color,
)
def get(self):
"""
Return the current chosen value showing in spinbox.
This value will be the same as what was provided as list of choices. If list items are ints, then the
item returned will be an int (not a string)
:return: The currently visible entry
:rtype: (Any)
"""
value = self.TKStringVar.get()
for v in self.Values:
if str(v) == value:
value = v
break
return value
Get = get
Update = update

View File

@ -0,0 +1,165 @@
from __future__ import annotations
import tkinter as tk
import FreeSimpleGUI
from FreeSimpleGUI import COLOR_SYSTEM_DEFAULT
from FreeSimpleGUI import ELEM_TYPE_STATUSBAR
from FreeSimpleGUI import Element
from FreeSimpleGUI import RELIEF_SUNKEN
from FreeSimpleGUI._utils import _error_popup_with_traceback
class StatusBar(Element):
"""
A StatusBar Element creates the sunken text-filled strip at the bottom. Many Windows programs have this line
"""
def __init__(
self,
text,
size=(None, None),
s=(None, None),
auto_size_text=None,
click_submits=None,
enable_events=False,
relief=RELIEF_SUNKEN,
font=None,
text_color=None,
background_color=None,
justification=None,
pad=None,
p=None,
key=None,
k=None,
right_click_menu=None,
expand_x=False,
expand_y=False,
tooltip=None,
visible=True,
metadata=None,
):
"""
:param text: Text that is to be displayed in the widget
:type text: (str)
:param size: (w, h) w=characters-wide, h=rows-high. If an int instead of a tuple is supplied, then height is auto-set to 1
:type size: (int, int) | (int, None) | int
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int, int) | (None, None) | int
:param auto_size_text: True if size should fit the text length
:type auto_size_text: (bool)
:param click_submits: DO NOT USE. Only listed for backwards compat - Use enable_events instead
:type click_submits: (bool)
:param enable_events: Turns on the element specific events. StatusBar events occur when the bar is clicked
:type enable_events: (bool)
:param relief: relief style. Values are same as progress meter relief values. Can be a constant or a string: `RELIEF_RAISED RELIEF_SUNKEN RELIEF_FLAT RELIEF_RIDGE RELIEF_GROOVE RELIEF_SOLID`
:type relief: (enum)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param text_color: color of the text
:type text_color: (str)
:param background_color: color of background
:type background_color: (str)
:param justification: how string should be aligned within space provided by size. Valid choices = `left`, `right`, `center`
:type justification: (str)
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param key: Used with window.find_element and with return values to uniquely identify this element to uniquely identify this element
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param right_click_menu: A list of lists of Menu items to show when this element is right clicked. See user docs for exact format.
:type right_click_menu: List[List[ List[str] | str ]]
:param expand_x: If True the element will automatically expand in the X direction to fill available space
:type expand_x: (bool)
:param expand_y: If True the element will automatically expand in the Y direction to fill available space
:type expand_y: (bool)
:param tooltip: text, that will appear when mouse hovers over the element
:type tooltip: (str)
:param visible: set visibility state of the element
:type visible: (bool)
:param metadata: User metadata that can be set to ANYTHING
:type metadata: (Any)
"""
self.DisplayText = text
self.TextColor = text_color if text_color else FreeSimpleGUI.DEFAULT_TEXT_COLOR
self.Justification = justification
self.Relief = relief
self.ClickSubmits = click_submits or enable_events
if background_color is None:
bg = FreeSimpleGUI.DEFAULT_TEXT_ELEMENT_BACKGROUND_COLOR
else:
bg = background_color
self.TKText = self.Widget = None # type: tk.Label
key = key if key is not None else k
self.RightClickMenu = right_click_menu
sz = size if size != (None, None) else s
pad = pad if pad is not None else p
self.expand_x = expand_x
self.expand_y = expand_y
super().__init__(
ELEM_TYPE_STATUSBAR,
size=sz,
auto_size_text=auto_size_text,
background_color=bg,
font=font or FreeSimpleGUI.DEFAULT_FONT,
text_color=self.TextColor,
pad=pad,
key=key,
tooltip=tooltip,
visible=visible,
metadata=metadata,
)
return
def update(self, value=None, background_color=None, text_color=None, font=None, visible=None):
"""
Changes some of the settings for the Status Bar Element. Must call `Window.Read` or `Window.Finalize` prior
Changes will not be visible in your window until you call window.read or window.refresh.
If you change visibility, your element may MOVE. If you want it to remain stationary, use the "layout helper"
function "pin" to ensure your element is "pinned" to that location in your layout so that it returns there
when made visible.
:param value: new text to show
:type value: (str)
:param background_color: color of background
:type background_color: (str)
:param text_color: color of the text
:type text_color: (str)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param visible: set visibility state of the element
:type visible: (bool)
"""
if not self._widget_was_created(): # if widget hasn't been created yet, then don't allow
return
if self._this_elements_window_closed():
_error_popup_with_traceback('Error in StatusBar.update - The window was closed')
return
if value is not None:
self.DisplayText = value
stringvar = self.TKStringVar
stringvar.set(value)
if background_color not in (None, COLOR_SYSTEM_DEFAULT):
self.TKText.configure(background=background_color)
if text_color not in (None, COLOR_SYSTEM_DEFAULT):
self.TKText.configure(fg=text_color)
if font is not None:
self.TKText.configure(font=font)
if visible is False:
self._pack_forget_save_settings()
elif visible is True:
self._pack_restore_settings()
if visible is not None:
self._visible = visible
Update = update

View File

@ -0,0 +1,26 @@
from __future__ import annotations
from FreeSimpleGUI.elements.text import Text
def Push(background_color=None):
"""
Acts like a Stretch element found in the Qt port.
Used in a Horizontal fashion. Placing one on each side of an element will enter the element.
Place one to the left and the element to the right will be right justified. See VStretch for vertical type
:param background_color: color of background may be needed because of how this is implemented
:type background_color: (str)
:return: (Text)
"""
return Text(font='_ 1', background_color=background_color, pad=(0, 0), expand_x=True)
def VPush(background_color=None):
"""
Acts like a Stretch element found in the Qt port.
Used in a Vertical fashion.
:param background_color: color of background may be needed because of how this is implemented
:type background_color: (str)
:return: (Text)
"""
return Text(font='_ 1', background_color=background_color, pad=(0, 0), expand_y=True)

View File

@ -0,0 +1,697 @@
from __future__ import annotations
import tkinter as tk
import warnings
from tkinter import ttk
import FreeSimpleGUI
from FreeSimpleGUI import _add_right_click_menu
from FreeSimpleGUI import _random_error_emoji
from FreeSimpleGUI import COLOR_SYSTEM_DEFAULT
from FreeSimpleGUI import ELEM_TYPE_TAB
from FreeSimpleGUI import ELEM_TYPE_TAB_GROUP
from FreeSimpleGUI import Element
from FreeSimpleGUI import LOOK_AND_FEEL_TABLE
from FreeSimpleGUI import PackFormIntoFrame
from FreeSimpleGUI import popup_error
from FreeSimpleGUI import popup_error_with_traceback
from FreeSimpleGUI import ToolTip
from FreeSimpleGUI._utils import _error_popup_with_traceback
from FreeSimpleGUI.window import Window
class Tab(Element):
"""
Tab Element is another "Container" element that holds a layout and displays a tab with text. Used with TabGroup only
Tabs are never placed directly into a layout. They are always "Contained" in a TabGroup layout
"""
def __init__(
self,
title,
layout,
title_color=None,
background_color=None,
font=None,
pad=None,
p=None,
disabled=False,
border_width=None,
key=None,
k=None,
tooltip=None,
right_click_menu=None,
expand_x=False,
expand_y=False,
visible=True,
element_justification='left',
image_source=None,
image_subsample=None,
image_zoom=None,
metadata=None,
):
"""
:param title: text to show on the tab
:type title: (str)
:param layout: The element layout that will be shown in the tab
:type layout: List[List[Element]]
:param title_color: color of the tab text (note not currently working on tkinter)
:type title_color: (str)
:param background_color: color of background of the entire layout
:type background_color: (str)
:param font: NOT USED in the tkinter port
:type font: (str or (str, int[, str]) or None)
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param disabled: If True button will be created disabled
:type disabled: (bool)
:param border_width: NOT USED in tkinter port
:type border_width: (int)
:param key: Value that uniquely identifies this element from all other elements. Used when Finding an element or in return values. Must be unique to the window
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param tooltip: text, that will appear when mouse hovers over the element
:type tooltip: (str)
:param right_click_menu: A list of lists of Menu items to show when this element is right clicked. See user docs for exact format.
:type right_click_menu: List[List[ List[str] | str ]]
:param expand_x: If True the element will automatically expand in the X direction to fill available space
:type expand_x: (bool)
:param expand_y: If True the element will automatically expand in the Y direction to fill available space
:type expand_y: (bool)
:param visible: set visibility state of the element
:type visible: (bool)
:param element_justification: All elements inside the Tab will have this justification 'left', 'right', 'center' are valid values
:type element_justification: (str)
:param image_source: A filename or a base64 bytes of an image to place on the Tab
:type image_source: str | bytes | None
:param image_subsample: amount to reduce the size of the image. Divides the size by this number. 2=1/2, 3=1/3, 4=1/4, etc
:type image_subsample: (int)
:param image_zoom: amount to increase the size of the image. 2=twice size, 3=3 times, etc
:type image_zoom: (int)
:param metadata: User metadata that can be set to ANYTHING
:type metadata: (Any)
"""
filename = data = None
if image_source is not None:
if isinstance(image_source, bytes):
data = image_source
elif isinstance(image_source, str):
filename = image_source
else:
warnings.warn(f'Image element - source is not a valid type: {type(image_source)}', UserWarning)
self.Filename = filename
self.Data = data
self.ImageSubsample = image_subsample
self.zoom = int(image_zoom) if image_zoom is not None else None
self.UseDictionary = False
self.ReturnValues = None
self.ReturnValuesList = []
self.ReturnValuesDictionary = {}
self.DictionaryKeyCounter = 0
self.ParentWindow = None
self.Rows = []
self.TKFrame = None
self.Widget = None # type: tk.Frame
self.Title = title
self.BorderWidth = border_width
self.Disabled = disabled
self.ParentNotebook = None
self.TabID = None
self.BackgroundColor = background_color if background_color is not None else FreeSimpleGUI.DEFAULT_BACKGROUND_COLOR
self.RightClickMenu = right_click_menu
self.ContainerElemementNumber = Window._GetAContainerNumber()
self.ElementJustification = element_justification
key = key if key is not None else k
pad = pad if pad is not None else p
self.expand_x = expand_x
self.expand_y = expand_y
self.Layout(layout)
super().__init__(
ELEM_TYPE_TAB,
background_color=background_color,
text_color=title_color,
font=font,
pad=pad,
key=key,
tooltip=tooltip,
visible=visible,
metadata=metadata,
)
return
def add_row(self, *args):
"""
Not recommended use call. Used to add rows of Elements to the Frame Element.
:param *args: The list of elements for this row
:type *args: List[Element]
"""
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
if type(element) is list:
popup_error_with_traceback(
'Error creating Tab layout',
'Layout has a LIST instead of an ELEMENT',
'This sometimes means you have a badly placed ]',
'The offensive list is:',
element,
'This list will be stripped from your layout',
)
continue
elif callable(element) and not isinstance(element, Element):
popup_error_with_traceback(
'Error creating Tab layout',
'Layout has a FUNCTION instead of an ELEMENT',
'This likely means you are missing () from your layout',
'The offensive list is:',
element,
'This item will be stripped from your layout',
)
continue
if element.ParentContainer is not None:
warnings.warn(
'*** YOU ARE ATTEMPTING TO REUSE AN ELEMENT IN YOUR LAYOUT! Once placed in a layout, an element cannot be used in another layout. ***',
UserWarning,
)
popup_error_with_traceback(
'Error creating Tab layout',
'The layout specified has already been used',
'You MUST start witha "clean", unused layout every time you create a window',
'The offensive Element = ',
element,
'and has a key = ',
element.Key,
'This item will be stripped from your layout',
'Hint - try printing your layout and matching the IDs "print(layout)"',
)
continue
element.Position = (CurrentRowNumber, i)
element.ParentContainer = self
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):
"""
Not user callable. Use layout parameter instead. Creates the layout using the supplied rows of Elements
:param rows: List[List[Element]] The list of rows
:type rows: List[List[Element]]
:return: (Tab) used for chaining
:rtype:
"""
for row in rows:
try:
iter(row)
except TypeError:
popup_error(
'Error creating Tab layout',
'Your row is not an iterable (e.g. a list)',
f'Instead of a list, the type found was {type(row)}',
'The offensive row = ',
row,
'This item will be stripped from your layout',
keep_on_top=True,
image=_random_error_emoji(),
)
continue
self.AddRow(*row)
return self
def update(self, title=None, disabled=None, visible=None):
"""
Changes some of the settings for the Tab Element. Must call `Window.Read` or `Window.Finalize` prior
Changes will not be visible in your window until you call window.read or window.refresh.
If you change visibility, your element may MOVE. If you want it to remain stationary, use the "layout helper"
function "pin" to ensure your element is "pinned" to that location in your layout so that it returns there
when made visible.
:param title: tab title
:type title: (str)
:param disabled: disable or enable state of the element
:type disabled: (bool)
:param visible: control visibility of element
:type visible: (bool)
"""
if not self._widget_was_created(): # if widget hasn't been created yet, then don't allow
return
if self._this_elements_window_closed():
_error_popup_with_traceback('Error in Tab.update - The window was closed')
return
state = 'normal'
if disabled is not None:
self.Disabled = disabled
if disabled:
state = 'disabled'
if visible is False:
state = 'hidden'
if visible is not None:
self._visible = visible
self.ParentNotebook.tab(self.TabID, state=state)
if title is not None:
self.Title = str(title)
self.ParentNotebook.tab(self.TabID, text=self.Title)
return self
def _GetElementAtLocation(self, location):
"""
Not user callable. Used to find the Element at a row, col position within the layout
:param location: (row, column) position of the element to find in layout
:type location: (int, int)
:return: The element found at the location
:rtype: (Element)
"""
(row_num, col_num) = location
row = self.Rows[row_num]
element = row[col_num]
return element
def select(self):
"""
Create a tkinter event that mimics user clicking on a tab. Must have called window.Finalize / Read first!
"""
# Use a try in case the window has been destoyed
try:
self.ParentNotebook.select(self.TabID)
except Exception as e:
print(f'Exception Selecting Tab {e}')
AddRow = add_row
Layout = layout
Select = select
Update = update
class TabGroup(Element):
"""
TabGroup Element groups together your tabs into the group of tabs you see displayed in your window
"""
def __init__(
self,
layout,
tab_location=None,
title_color=None,
tab_background_color=None,
selected_title_color=None,
selected_background_color=None,
background_color=None,
focus_color=None,
font=None,
change_submits=False,
enable_events=False,
pad=None,
p=None,
border_width=None,
tab_border_width=None,
theme=None,
key=None,
k=None,
size=(None, None),
s=(None, None),
tooltip=None,
right_click_menu=None,
expand_x=False,
expand_y=False,
visible=True,
metadata=None,
):
"""
:param layout: Layout of Tabs. Different than normal layouts. ALL Tabs should be on first row
:type layout: List[List[Tab]]
:param tab_location: location that tabs will be displayed. Choices are left, right, top, bottom, lefttop, leftbottom, righttop, rightbottom, bottomleft, bottomright, topleft, topright
:type tab_location: (str)
:param title_color: color of text on tabs
:type title_color: (str)
:param tab_background_color: color of all tabs that are not selected
:type tab_background_color: (str)
:param selected_title_color: color of tab text when it is selected
:type selected_title_color: (str)
:param selected_background_color: color of tab when it is selected
:type selected_background_color: (str)
:param background_color: color of background area that tabs are located on
:type background_color: (str)
:param focus_color: color of focus indicator on the tabs
:type focus_color: (str)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param change_submits: * DEPRICATED DO NOT USE. Use `enable_events` instead
:type change_submits: (bool)
:param enable_events: If True then switching tabs will generate an Event
:type enable_events: (bool)
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param border_width: width of border around element in pixels
:type border_width: (int)
:param tab_border_width: width of border around the tabs
:type tab_border_width: (int)
:param theme: DEPRICATED - You can only specify themes using set options or when window is created. It's not possible to do it on an element basis
:type theme: (enum)
:param key: Value that uniquely identifies this element from all other elements. Used when Finding an element or in return values. Must be unique to the window
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param size: (width, height) w=pixels-wide, h=pixels-high. Either item in tuple can be None to indicate use the computed value and set only 1 direction
:type size: (int|None, int|None)
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int|None, int|None)
:param tooltip: text, that will appear when mouse hovers over the element
:type tooltip: (str)
:param right_click_menu: A list of lists of Menu items to show when this element is right clicked. See user docs for exact format.
:type right_click_menu: List[List[ List[str] | str ]]
:param expand_x: If True the element will automatically expand in the X direction to fill available space
:type expand_x: (bool)
:param expand_y: If True the element will automatically expand in the Y direction to fill available space
:type expand_y: (bool)
:param visible: DEPRECATED - Should you need to control visiblity for the TabGroup as a whole, place it into a Column element
:type visible: (bool)
:param metadata: User metadata that can be set to ANYTHING
:type metadata: (Any)
"""
self.UseDictionary = False
self.ReturnValues = None
self.ReturnValuesList = []
self.ReturnValuesDictionary = {}
self.DictionaryKeyCounter = 0
self.ParentWindow = None
self.SelectedTitleColor = selected_title_color if selected_title_color is not None else LOOK_AND_FEEL_TABLE[FreeSimpleGUI.CURRENT_LOOK_AND_FEEL]['TEXT']
self.SelectedBackgroundColor = selected_background_color if selected_background_color is not None else LOOK_AND_FEEL_TABLE[FreeSimpleGUI.CURRENT_LOOK_AND_FEEL]['BACKGROUND']
title_color = title_color if title_color is not None else LOOK_AND_FEEL_TABLE[FreeSimpleGUI.CURRENT_LOOK_AND_FEEL]['TEXT_INPUT']
self.TabBackgroundColor = tab_background_color if tab_background_color is not None else LOOK_AND_FEEL_TABLE[FreeSimpleGUI.CURRENT_LOOK_AND_FEEL]['INPUT']
self.Rows = []
self.TKNotebook = None # type: ttk.Notebook
self.Widget = None # type: ttk.Notebook
self.tab_index_to_key = {} # has a list of the tabs in the notebook and their associated key
self.TabCount = 0
self.BorderWidth = border_width
self.BackgroundColor = background_color if background_color is not None else FreeSimpleGUI.DEFAULT_BACKGROUND_COLOR
self.ChangeSubmits = change_submits or enable_events
self.TabLocation = tab_location
self.ElementJustification = 'left'
self.RightClickMenu = right_click_menu
self.TabBorderWidth = tab_border_width
self.FocusColor = focus_color
key = key if key is not None else k
sz = size if size != (None, None) else s
pad = pad if pad is not None else p
self.expand_x = expand_x
self.expand_y = expand_y
self.Layout(layout)
super().__init__(
ELEM_TYPE_TAB_GROUP,
size=sz,
background_color=background_color,
text_color=title_color,
font=font,
pad=pad,
key=key,
tooltip=tooltip,
visible=visible,
metadata=metadata,
)
return
def add_row(self, *args):
"""
Not recommended user call. Used to add rows of Elements to the Frame Element.
:param *args: The list of elements for this row
:type *args: List[Element]
"""
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
if type(element) is list:
popup_error(
'Error creating Tab layout',
'Layout has a LIST instead of an ELEMENT',
'This sometimes means you have a badly placed ]',
'The offensive list is:',
element,
'This list will be stripped from your layout',
keep_on_top=True,
image=_random_error_emoji(),
)
continue
elif callable(element) and not isinstance(element, Element):
popup_error(
'Error creating Tab layout',
'Layout has a FUNCTION instead of an ELEMENT',
'This likely means you are missing () from your layout',
'The offensive list is:',
element,
'This item will be stripped from your layout',
keep_on_top=True,
image=_random_error_emoji(),
)
continue
if element.ParentContainer is not None:
warnings.warn(
'*** YOU ARE ATTEMPTING TO REUSE AN ELEMENT IN YOUR LAYOUT! Once placed in a layout, an element cannot be used in another layout. ***',
UserWarning,
)
popup_error(
'Error creating Tab layout',
'The layout specified has already been used',
'You MUST start witha "clean", unused layout every time you create a window',
'The offensive Element = ',
element,
'and has a key = ',
element.Key,
'This item will be stripped from your layout',
'Hint - try printing your layout and matching the IDs "print(layout)"',
keep_on_top=True,
image=_random_error_emoji(),
)
continue
element.Position = (CurrentRowNumber, i)
element.ParentContainer = self
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):
"""
Can use like the Window.Layout method, but it's better to use the layout parameter when creating
:param rows: The rows of Elements
:type rows: List[List[Element]]
:return: Used for chaining
:rtype: (Frame)
"""
for row in rows:
try:
iter(row)
except TypeError:
popup_error(
'Error creating Tab layout',
'Your row is not an iterable (e.g. a list)',
f'Instead of a list, the type found was {type(row)}',
'The offensive row = ',
row,
'This item will be stripped from your layout',
keep_on_top=True,
image=_random_error_emoji(),
)
continue
self.AddRow(*row)
return self
def _GetElementAtLocation(self, location):
"""
Not user callable. Used to find the Element at a row, col position within the layout
:param location: (row, column) position of the element to find in layout
:type location: (int, int)
:return: The element found at the location
:rtype: (Element)
"""
(row_num, col_num) = location
row = self.Rows[row_num]
element = row[col_num]
return element
def find_key_from_tab_name(self, tab_name):
"""
Searches through the layout to find the key that matches the text on the tab. Implies names should be unique
:param tab_name: name of a tab
:type tab_name: str
:return: Returns the key or None if no key found
:rtype: key | None
"""
for row in self.Rows:
for element in row:
if element.Title == tab_name:
return element.Key
return None
def find_currently_active_tab_key(self):
"""
Returns the key for the currently active tab in this TabGroup
:return: Returns the key or None of no key found
:rtype: key | None
"""
try:
current_index = self.TKNotebook.index('current')
key = self.tab_index_to_key.get(current_index, None)
except:
key = None
return key
def get(self):
"""
Returns the current value for the Tab Group, which will be the currently selected tab's KEY or the text on
the tab if no key is defined. Returns None if an error occurs.
Note that this is exactly the same data that would be returned from a call to Window.read. Are you sure you
are using this method correctly?
:return: The key of the currently selected tab or None if there is an error
:rtype: Any | None
"""
try:
current_index = self.TKNotebook.index('current')
key = self.tab_index_to_key.get(current_index, None)
except:
key = None
return key
def add_tab(self, tab_element):
"""
Add a new tab to an existing TabGroup
This call was written so that tabs can be added at runtime as your user performs operations.
Your Window should already be created and finalized.
:param tab_element: A Tab Element that has a layout in it
:type tab_element: Tab
"""
self.add_row(tab_element)
tab_element.TKFrame = tab_element.Widget = tk.Frame(self.TKNotebook)
form = self.ParentForm
form._BuildKeyDictForWindow(form, tab_element, form.AllKeysDict)
form.AllKeysDict[tab_element.Key] = tab_element
# Pack the tab's layout into the tab. NOTE - This does NOT pack the Tab itself... for that see below...
PackFormIntoFrame(tab_element, tab_element.TKFrame, self.ParentForm)
# - This is below - Perform the same operation that is performed when a Tab is packed into the window.
# If there's an image in the tab, then do the imagey-stuff
# ------------------- start of imagey-stuff -------------------
try:
if tab_element.Filename is not None:
photo = tk.PhotoImage(file=tab_element.Filename)
elif tab_element.Data is not None:
photo = tk.PhotoImage(data=tab_element.Data)
else:
photo = None
if tab_element.ImageSubsample and photo is not None:
photo = photo.subsample(tab_element.ImageSubsample)
# print('*ERROR laying out form.... Image Element has no image specified*')
except Exception as e:
photo = None
_error_popup_with_traceback(
'Your Window has an Tab Element with an IMAGE problem',
'The traceback will show you the Window with the problem layout',
f'Look in this Window\'s layout for an Image tab_element that has a key of {tab_element.Key}',
'The error occuring is:',
e,
)
tab_element.photo = photo
# add the label
if photo is not None:
width, height = photo.width(), photo.height()
tab_element.tktext_label = tk.Label(tab_element.ParentRowFrame, image=photo, width=width, height=height, bd=0)
else:
tab_element.tktext_label = tk.Label(tab_element.ParentRowFrame, bd=0)
# ------------------- end of imagey-stuff -------------------
state = 'normal'
if tab_element.Disabled:
state = 'disabled'
if tab_element.visible is False:
state = 'hidden'
if photo is not None:
self.TKNotebook.add(tab_element.TKFrame, text=tab_element.Title, compound=tk.LEFT, state=state, image=photo)
else:
self.TKNotebook.add(tab_element.TKFrame, text=tab_element.Title, state=state)
tab_element.ParentNotebook = self.TKNotebook
tab_element.TabID = self.TabCount
tab_element.ParentForm = self.ParentForm
self.TabCount += 1
if tab_element.BackgroundColor != COLOR_SYSTEM_DEFAULT and tab_element.BackgroundColor is not None:
tab_element.TKFrame.configure(
background=tab_element.BackgroundColor,
highlightbackground=tab_element.BackgroundColor,
highlightcolor=tab_element.BackgroundColor,
)
if tab_element.BorderWidth is not None:
tab_element.TKFrame.configure(borderwidth=tab_element.BorderWidth)
if tab_element.Tooltip is not None:
tab_element.TooltipObject = ToolTip(tab_element.TKFrame, text=tab_element.Tooltip, timeout=FreeSimpleGUI.DEFAULT_TOOLTIP_TIME)
_add_right_click_menu(tab_element, form)
def update(self, visible=None):
"""
Enables changing the visibility
:param visible: control visibility of element
:type visible: (bool)
"""
if not self._widget_was_created(): # if widget hasn't been created yet, then don't allow
return
if self._this_elements_window_closed():
_error_popup_with_traceback('Error in TabGroup.update - The window was closed')
return
if visible is False:
self._pack_forget_save_settings()
elif visible is True:
self._pack_restore_settings()
if visible is not None:
self._visible = visible
AddRow = add_row
FindKeyFromTabName = find_key_from_tab_name
Get = get
Layout = layout

View File

@ -0,0 +1,463 @@
from __future__ import annotations
import warnings
from tkinter import ttk
import FreeSimpleGUI
from FreeSimpleGUI import COLOR_SYSTEM_DEFAULT
from FreeSimpleGUI import ELEM_TYPE_TABLE
from FreeSimpleGUI import Element
from FreeSimpleGUI import LOOK_AND_FEEL_TABLE
from FreeSimpleGUI import obj_to_string_single_obj
from FreeSimpleGUI import running_mac
from FreeSimpleGUI import TABLE_CLICKED_INDICATOR
from FreeSimpleGUI import theme_button_color
from FreeSimpleGUI._utils import _create_error_message
from FreeSimpleGUI._utils import _error_popup_with_traceback
from FreeSimpleGUI._utils import _exit_mainloop
class Table(Element):
def __init__(
self,
values,
headings=None,
visible_column_map=None,
col_widths=None,
cols_justification=None,
def_col_width=10,
auto_size_columns=True,
max_col_width=20,
select_mode=None,
display_row_numbers=False,
starting_row_number=0,
num_rows=None,
row_height=None,
font=None,
justification='right',
text_color=None,
background_color=None,
alternating_row_color=None,
selected_row_colors=(None, None),
header_text_color=None,
header_background_color=None,
header_font=None,
header_border_width=None,
header_relief=None,
row_colors=None,
vertical_scroll_only=True,
hide_vertical_scroll=False,
border_width=None,
sbar_trough_color=None,
sbar_background_color=None,
sbar_arrow_color=None,
sbar_width=None,
sbar_arrow_width=None,
sbar_frame_color=None,
sbar_relief=None,
size=(None, None),
s=(None, None),
change_submits=False,
enable_events=False,
enable_click_events=False,
right_click_selects=False,
bind_return_key=False,
pad=None,
p=None,
key=None,
k=None,
tooltip=None,
right_click_menu=None,
expand_x=False,
expand_y=False,
visible=True,
metadata=None,
):
"""
:param values: Your table data represented as a 2-dimensions table... a list of rows, with each row representing a row in your table.
:type values: List[List[str | int | float]]
:param headings: The headings to show on the top line
:type headings: List[str]
:param visible_column_map: One entry for each column. False indicates the column is not shown
:type visible_column_map: List[bool]
:param col_widths: Number of characters that each column will occupy
:type col_widths: List[int]
:param cols_justification: Justification for EACH column. Is a list of strings with the value 'l', 'r', 'c' that indicates how the column will be justified. Either no columns should be set, or have to have one for every colun
:type cols_justification: List[str] or Tuple[str] or None
:param def_col_width: Default column width in characters
:type def_col_width: (int)
:param auto_size_columns: if True columns will be sized automatically
:type auto_size_columns: (bool)
:param max_col_width: Maximum width for all columns in characters
:type max_col_width: (int)
:param select_mode: Select Mode. Valid values start with "TABLE_SELECT_MODE_". Valid values are: TABLE_SELECT_MODE_NONE TABLE_SELECT_MODE_BROWSE TABLE_SELECT_MODE_EXTENDED
:type select_mode: (enum)
:param display_row_numbers: if True, the first column of the table will be the row #
:type display_row_numbers: (bool)
:param starting_row_number: The row number to use for the first row. All following rows will be based on this starting value. Default is 0.
:type starting_row_number: (int)
:param num_rows: The number of rows of the table to display at a time
:type num_rows: (int)
:param row_height: height of a single row in pixels
:type row_height: (int)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param justification: 'left', 'right', 'center' are valid choices
:type justification: (str)
:param text_color: color of the text
:type text_color: (str)
:param background_color: color of background
:type background_color: (str)
:param alternating_row_color: if set then every other row will have this color in the background.
:type alternating_row_color: (str)
:param selected_row_colors: Sets the text color and background color for a selected row. Same format as button colors - tuple ('red', 'yellow') or string 'red on yellow'. Defaults to theme's button color
:type selected_row_colors: str or (str, str)
:param header_text_color: sets the text color for the header
:type header_text_color: (str)
:param header_background_color: sets the background color for the header
:type header_background_color: (str)
:param header_font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type header_font: (str or (str, int[, str]) or None)
:param header_border_width: Border width for the header portion
:type header_border_width: (int | None)
:param header_relief: Relief style for the header. Values are same as other elements that use relief. RELIEF_RAISED RELIEF_SUNKEN RELIEF_FLAT RELIEF_RIDGE RELIEF_GROOVE RELIEF_SOLID
:type header_relief: (str | None)
:param row_colors: list of tuples of (row, background color) OR (row, foreground color, background color). Sets the colors of listed rows to the color(s) provided (note the optional foreground color)
:type row_colors: List[Tuple[int, str] | Tuple[Int, str, str]]
:param vertical_scroll_only: if True only the vertical scrollbar will be visible
:type vertical_scroll_only: (bool)
:param hide_vertical_scroll: if True vertical scrollbar will be hidden
:type hide_vertical_scroll: (bool)
:param border_width: Border width/depth in pixels
:type border_width: (int)
:param sbar_trough_color: Scrollbar color of the trough
:type sbar_trough_color: (str)
:param sbar_background_color: Scrollbar color of the background of the arrow buttons at the ends AND the color of the "thumb" (the thing you grab and slide). Switches to arrow color when mouse is over
:type sbar_background_color: (str)
:param sbar_arrow_color: Scrollbar color of the arrow at the ends of the scrollbar (it looks like a button). Switches to background color when mouse is over
:type sbar_arrow_color: (str)
:param sbar_width: Scrollbar width in pixels
:type sbar_width: (int)
:param sbar_arrow_width: Scrollbar width of the arrow on the scrollbar. It will potentially impact the overall width of the scrollbar
:type sbar_arrow_width: (int)
:param sbar_frame_color: Scrollbar Color of frame around scrollbar (available only on some ttk themes)
:type sbar_frame_color: (str)
:param sbar_relief: Scrollbar relief that will be used for the "thumb" of the scrollbar (the thing you grab that slides). Should be a constant that is defined at starting with "RELIEF_" - RELIEF_RAISED, RELIEF_SUNKEN, RELIEF_FLAT, RELIEF_RIDGE, RELIEF_GROOVE, RELIEF_SOLID
:type sbar_relief: (str)
:param size: DO NOT USE! Use num_rows instead
:type size: (int, int)
:param change_submits: DO NOT USE. Only listed for backwards compat - Use enable_events instead
:type change_submits: (bool)
:param enable_events: Turns on the element specific events. Table events happen when row is clicked
:type enable_events: (bool)
:param enable_click_events: Turns on the element click events that will give you (row, col) click data when the table is clicked
:type enable_click_events: (bool)
:param right_click_selects: If True, then right clicking a row will select that row if multiple rows are not currently selected
:type right_click_selects: (bool)
:param bind_return_key: if True, pressing return key will cause event coming from Table, ALSO a left button double click will generate an event if this parameter is True
:type bind_return_key: (bool)
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param key: Used with window.find_element and with return values to uniquely identify this element to uniquely identify this element
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param tooltip: text, that will appear when mouse hovers over the element
:type tooltip: (str)
:param right_click_menu: A list of lists of Menu items to show when this element is right clicked. See user docs for exact format.
:type right_click_menu: List[List[ List[str] | str ]]
:param expand_x: If True the element will automatically expand in the X direction to fill available space
:type expand_x: (bool)
:param expand_y: If True the element will automatically expand in the Y direction to fill available space
:type expand_y: (bool)
:param visible: set visibility state of the element
:type visible: (bool)
:param metadata: User metadata that can be set to ANYTHING
:type metadata: (Any)
"""
self.Values = values
self.ColumnHeadings = headings
self.ColumnsToDisplay = visible_column_map
self.ColumnWidths = col_widths
self.cols_justification = cols_justification
self.MaxColumnWidth = max_col_width
self.DefaultColumnWidth = def_col_width
self.AutoSizeColumns = auto_size_columns
self.BackgroundColor = background_color if background_color is not None else FreeSimpleGUI.DEFAULT_BACKGROUND_COLOR
self.TextColor = text_color
self.HeaderTextColor = header_text_color if header_text_color is not None else LOOK_AND_FEEL_TABLE[FreeSimpleGUI.CURRENT_LOOK_AND_FEEL]['TEXT_INPUT']
self.HeaderBackgroundColor = header_background_color if header_background_color is not None else LOOK_AND_FEEL_TABLE[FreeSimpleGUI.CURRENT_LOOK_AND_FEEL]['INPUT']
self.HeaderFont = header_font
self.Justification = justification
self.InitialState = None
self.SelectMode = select_mode
self.DisplayRowNumbers = display_row_numbers
self.NumRows = num_rows if num_rows is not None else size[1]
self.RowHeight = row_height
self.Widget = self.TKTreeview = None # type: ttk.Treeview
self.AlternatingRowColor = alternating_row_color
self.VerticalScrollOnly = vertical_scroll_only
self.HideVerticalScroll = hide_vertical_scroll
self.SelectedRows = []
self.ChangeSubmits = change_submits or enable_events
self.BindReturnKey = bind_return_key
self.StartingRowNumber = starting_row_number # When displaying row numbers, where to start
self.RowHeaderText = 'Row'
self.enable_click_events = enable_click_events
self.right_click_selects = right_click_selects
self.last_clicked_position = (None, None)
self.HeaderBorderWidth = header_border_width
self.BorderWidth = border_width
self.HeaderRelief = header_relief
self.table_ttk_style_name = None # the ttk style name for the Table itself
if selected_row_colors == (None, None):
# selected_row_colors = DEFAULT_TABLE_AND_TREE_SELECTED_ROW_COLORS
selected_row_colors = theme_button_color()
else:
try:
if isinstance(selected_row_colors, str):
selected_row_colors = selected_row_colors.split(' on ')
except Exception as e:
print('* Table Element Warning * you messed up with color formatting of Selected Row Color', e)
self.SelectedRowColors = selected_row_colors
self.RightClickMenu = right_click_menu
self.RowColors = row_colors
self.tree_ids = [] # ids returned when inserting items into table - will use to delete colors
key = key if key is not None else k
sz = size if size != (None, None) else s
pad = pad if pad is not None else p
self.expand_x = expand_x
self.expand_y = expand_y
super().__init__(
ELEM_TYPE_TABLE,
text_color=text_color,
background_color=background_color,
font=font,
size=sz,
pad=pad,
key=key,
tooltip=tooltip,
visible=visible,
metadata=metadata,
sbar_trough_color=sbar_trough_color,
sbar_background_color=sbar_background_color,
sbar_arrow_color=sbar_arrow_color,
sbar_width=sbar_width,
sbar_arrow_width=sbar_arrow_width,
sbar_frame_color=sbar_frame_color,
sbar_relief=sbar_relief,
)
return
def update(self, values=None, num_rows=None, visible=None, select_rows=None, alternating_row_color=None, row_colors=None):
"""
Changes some of the settings for the Table Element. Must call `Window.Read` or `Window.Finalize` prior
Changes will not be visible in your window until you call window.read or window.refresh.
If you change visibility, your element may MOVE. If you want it to remain stationary, use the "layout helper"
function "pin" to ensure your element is "pinned" to that location in your layout so that it returns there
when made visible.
:param values: A new 2-dimensional table to show
:type values: List[List[str | int | float]]
:param num_rows: How many rows to display at a time
:type num_rows: (int)
:param visible: if True then will be visible
:type visible: (bool)
:param select_rows: List of rows to select as if user did
:type select_rows: List[int]
:param alternating_row_color: the color to make every other row
:type alternating_row_color: (str)
:param row_colors: list of tuples of (row, background color) OR (row, foreground color, background color). Changes the colors of listed rows to the color(s) provided (note the optional foreground color)
:type row_colors: List[Tuple[int, str] | Tuple[Int, str, str]]
"""
if not self._widget_was_created(): # if widget hasn't been created yet, then don't allow
return
if self._this_elements_window_closed():
_error_popup_with_traceback('Error in Table.update - The window was closed')
return
if values is not None:
for id in self.tree_ids:
self.TKTreeview.item(id, tags=())
if self.BackgroundColor is not None and self.BackgroundColor != COLOR_SYSTEM_DEFAULT:
self.TKTreeview.tag_configure(id, background=self.BackgroundColor)
else:
self.TKTreeview.tag_configure(id, background='#FFFFFF', foreground='#000000')
if self.TextColor is not None and self.TextColor != COLOR_SYSTEM_DEFAULT:
self.TKTreeview.tag_configure(id, foreground=self.TextColor)
else:
self.TKTreeview.tag_configure(id, foreground='#000000')
children = self.TKTreeview.get_children()
for i in children:
self.TKTreeview.detach(i)
self.TKTreeview.delete(i)
children = self.TKTreeview.get_children()
self.tree_ids = []
for i, value in enumerate(values):
if self.DisplayRowNumbers:
value = [i + self.StartingRowNumber] + value
id = self.TKTreeview.insert('', 'end', text=value, iid=i + 1, values=value, tag=i)
if self.BackgroundColor is not None and self.BackgroundColor != COLOR_SYSTEM_DEFAULT:
self.TKTreeview.tag_configure(id, background=self.BackgroundColor)
else:
self.TKTreeview.tag_configure(id, background='#FFFFFF')
self.tree_ids.append(id)
self.Values = values
self.SelectedRows = []
if visible is False:
self._pack_forget_save_settings(self.element_frame)
elif visible is True:
self._pack_restore_settings(self.element_frame)
if num_rows is not None:
self.TKTreeview.config(height=num_rows)
if select_rows is not None:
rows_to_select = [i + 1 for i in select_rows]
self.TKTreeview.selection_set(rows_to_select)
if alternating_row_color is not None: # alternating colors
self.AlternatingRowColor = alternating_row_color
if self.AlternatingRowColor is not None:
for row in range(0, len(self.Values), 2):
self.TKTreeview.tag_configure(row, background=self.AlternatingRowColor)
if row_colors is not None: # individual row colors
self.RowColors = row_colors
for row_def in self.RowColors:
if len(row_def) == 2: # only background is specified
self.TKTreeview.tag_configure(row_def[0], background=row_def[1])
else:
self.TKTreeview.tag_configure(row_def[0], background=row_def[2], foreground=row_def[1])
if visible is not None:
self._visible = visible
def _treeview_selected(self, event):
"""
Not user callable. Callback function that is called when something is selected from Table.
Stores the selected rows in Element as they are being selected. If events enabled, then returns from Read
:param event: event information from tkinter
:type event: (unknown)
"""
# print('**-- in treeview selected --**')
selections = self.TKTreeview.selection()
self.SelectedRows = [int(x) - 1 for x in selections]
if self.ChangeSubmits:
if self.Key is not None:
self.ParentForm.LastButtonClicked = self.Key
else:
self.ParentForm.LastButtonClicked = ''
self.ParentForm.FormRemainedOpen = True
_exit_mainloop(self.ParentForm)
def _treeview_double_click(self, event):
"""
Not user callable. Callback function that is called when something is selected from Table.
Stores the selected rows in Element as they are being selected. If events enabled, then returns from Read
:param event: event information from tkinter
:type event: (unknown)
"""
selections = self.TKTreeview.selection()
self.SelectedRows = [int(x) - 1 for x in selections]
if self.BindReturnKey: # Signifies BOTH a return key AND a double click
if self.Key is not None:
self.ParentForm.LastButtonClicked = self.Key
else:
self.ParentForm.LastButtonClicked = ''
self.ParentForm.FormRemainedOpen = True
_exit_mainloop(self.ParentForm)
def _table_clicked(self, event):
"""
Not user callable. Callback function that is called a click happens on a table.
Stores the selected rows in Element as they are being selected. If events enabled, then returns from Read
:param event: event information from tkinter
:type event: (unknown)
"""
if not self._widget_was_created(): # if widget hasn't been created yet, then don't allow
return
# popup(obj_to_string_single_obj(event))
try:
region = self.Widget.identify('region', event.x, event.y)
if region == 'heading':
row = -1
elif region == 'cell':
row = int(self.Widget.identify_row(event.y)) - 1
elif region == 'separator':
row = None
else:
row = None
col_identified = self.Widget.identify_column(event.x)
if col_identified: # Sometimes tkinter returns a value of '' which would cause an error if cast to an int
column = int(self.Widget.identify_column(event.x)[1:]) - 1 - int(self.DisplayRowNumbers is True)
else:
column = None
except Exception as e:
warnings.warn(f'Error getting table click data for table with key= {self.Key}\nError: {e}', UserWarning)
if not FreeSimpleGUI.SUPPRESS_ERROR_POPUPS:
_error_popup_with_traceback(
f'Unable to complete operation getting the clicked event for table with key {self.Key}',
_create_error_message(),
e,
'Event data:',
obj_to_string_single_obj(event),
)
row = column = None
self.last_clicked_position = (row, column)
# update the rows being selected if appropriate
self.ParentForm.TKroot.update()
# self.TKTreeview.()
selections = self.TKTreeview.selection()
if self.right_click_selects and len(selections) <= 1:
if (event.num == 3 and not running_mac()) or (event.num == 2 and running_mac()):
if row != -1 and row is not None:
selections = [row + 1]
self.TKTreeview.selection_set(selections)
# print(selections)
self.SelectedRows = [int(x) - 1 for x in selections]
# print('The new selected rows = ', self.SelectedRows, 'selections =', selections)
if self.enable_click_events is True:
if self.Key is not None:
self.ParentForm.LastButtonClicked = (self.Key, TABLE_CLICKED_INDICATOR, (row, column))
else:
self.ParentForm.LastButtonClicked = ''
self.ParentForm.FormRemainedOpen = True
_exit_mainloop(self.ParentForm)
def get(self):
"""
Get the selected rows using tktiner's selection method. Returns a list of the selected rows.
:return: a list of the index of the selected rows (a list of ints)
:rtype: List[int]
"""
selections = self.TKTreeview.selection()
selected_rows = [int(x) - 1 for x in selections]
return selected_rows
def get_last_clicked_position(self):
"""
Returns a tuple with the row and column of the cell that was last clicked.
Headers will have a row == -1 and the Row Number Column (if present) will have a column == -1
:return: The (row,col) position of the last cell clicked in the table
:rtype: (int | None, int | None)
"""
return self.last_clicked_position
Update = update
Get = get

View File

@ -0,0 +1,413 @@
from __future__ import annotations
import tkinter.font
import FreeSimpleGUI
from FreeSimpleGUI import _get_hidden_master_root
from FreeSimpleGUI import COLOR_SYSTEM_DEFAULT
from FreeSimpleGUI import ELEM_TYPE_TEXT
from FreeSimpleGUI._utils import _error_popup_with_traceback
from FreeSimpleGUI.elements.base import Element
class Text(Element):
"""
Text - Display some text in the window. Usually this means a single line of text. However, the text can also be multiple lines. If multi-lined there are no scroll bars.
"""
def __init__(
self,
text='',
size=(None, None),
s=(None, None),
auto_size_text=None,
click_submits=False,
enable_events=False,
relief=None,
font=None,
text_color=None,
background_color=None,
border_width=None,
justification=None,
pad=None,
p=None,
key=None,
k=None,
right_click_menu=None,
expand_x=False,
expand_y=False,
grab=None,
tooltip=None,
visible=True,
metadata=None,
):
"""
:param text: The text to display. Can include /n to achieve multiple lines. Will convert (optional) parameter into a string
:type text: Any
:param size: (w, h) w=characters-wide, h=rows-high. If an int instead of a tuple is supplied, then height is auto-set to 1
:type size: (int, int) | (int, None) | (None, None) | (int, ) | int
:param s: Same as size parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, size will be used
:type s: (int, int) | (int, None) | (None, None) | (int, ) | int
:param auto_size_text: if True size of the Text Element will be sized to fit the string provided in 'text' parm
:type auto_size_text: (bool)
:param click_submits: DO NOT USE. Only listed for backwards compat - Use enable_events instead
:type click_submits: (bool)
:param enable_events: Turns on the element specific events. Text events happen when the text is clicked
:type enable_events: (bool)
:param relief: relief style around the text. Values are same as progress meter relief values. Should be a constant that is defined at starting with RELIEF - RELIEF_RAISED, RELIEF_SUNKEN, RELIEF_FLAT, RELIEF_RIDGE, RELIEF_GROOVE, RELIEF_SOLID
:type relief: (str)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param text_color: color of the text
:type text_color: (str)
:param background_color: color of background
:type background_color: (str)
:param border_width: number of pixels for the border (if using a relief)
:type border_width: (int)
:param justification: how string should be aligned within space provided by size. Valid choices = `left`, `right`, `center`
:type justification: (str)
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param key: Used with window.find_element and with return values to uniquely identify this element to uniquely identify this element
:type key: str or int or tuple or object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param right_click_menu: A list of lists of Menu items to show when this element is right clicked. See user docs for exact format.
:type right_click_menu: List[List[ List[str] | str ]]
:param expand_x: If True the element will automatically expand in the X direction to fill available space
:type expand_x: (bool)
:param expand_y: If True the element will automatically expand in the Y direction to fill available space
:type expand_y: (bool)
:param grab: If True can grab this element and move the window around. Default is False
:type grab: (bool)
:param tooltip: text, that will appear when mouse hovers over the element
:type tooltip: (str)
:param visible: set visibility state of the element
:type visible: (bool)
:param metadata: User metadata that can be set to ANYTHING
:type metadata: (Any)
"""
self.DisplayText = str(text)
self.TextColor = text_color if text_color else FreeSimpleGUI.DEFAULT_TEXT_COLOR
self.Justification = justification
self.Relief = relief
self.ClickSubmits = click_submits or enable_events
if background_color is None:
bg = FreeSimpleGUI.DEFAULT_TEXT_ELEMENT_BACKGROUND_COLOR
else:
bg = background_color
self.RightClickMenu = right_click_menu
self.TKRightClickMenu = None
self.BorderWidth = border_width
self.Grab = grab
key = key if key is not None else k
sz = size if size != (None, None) else s
pad = pad if pad is not None else p
self.expand_x = expand_x
self.expand_y = expand_y
super().__init__(
ELEM_TYPE_TEXT,
auto_size_text=auto_size_text,
size=sz,
background_color=bg,
font=font if font else FreeSimpleGUI.DEFAULT_FONT,
text_color=self.TextColor,
pad=pad,
key=key,
tooltip=tooltip,
visible=visible,
metadata=metadata,
)
def update(self, value=None, background_color=None, text_color=None, font=None, visible=None):
"""
Changes some of the settings for the Text Element. Must call `Window.Read` or `Window.Finalize` prior
Changes will not be visible in your window until you call window.read or window.refresh.
If you change visibility, your element may MOVE. If you want it to remain stationary, use the "layout helper"
function "pin" to ensure your element is "pinned" to that location in your layout so that it returns there
when made visible.
:param value: new text to show
:type value: (Any)
:param background_color: color of background
:type background_color: (str)
:param text_color: color of the text
:type text_color: (str)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param visible: set visibility state of the element
:type visible: (bool)
"""
if not self._widget_was_created(): # if widget hasn't been created yet, then don't allow
return
if self._this_elements_window_closed():
_error_popup_with_traceback('Error in Text.update - The window was closed')
return
if value is not None:
self.DisplayText = str(value)
self.TKStringVar.set(str(value))
if background_color not in (None, COLOR_SYSTEM_DEFAULT):
self.TKText.configure(background=background_color)
if text_color not in (None, COLOR_SYSTEM_DEFAULT):
self.TKText.configure(fg=text_color)
if font is not None:
self.TKText.configure(font=font)
if visible is False:
self._pack_forget_save_settings()
elif visible is True:
self._pack_restore_settings()
if visible is not None:
self._visible = visible
def get(self):
"""
Gets the current value of the displayed text
:return: The current value
:rtype: (str)
"""
try:
text = self.TKStringVar.get()
except:
text = ''
return text
@classmethod
def fonts_installed_list(cls):
"""
Returns a list of strings that tkinter reports as the installed fonts
:return: List of the installed font names
:rtype: List[str]
"""
# A window must exist before can perform this operation. Create the hidden master root if it doesn't exist
_get_hidden_master_root()
fonts = list(tkinter.font.families())
fonts.sort()
return fonts
@classmethod
def char_width_in_pixels(cls, font, character='W'):
"""
Get the with of the character "W" in pixels for the font being passed in or
the character of your choosing if "W" is not a good representative character.
Cannot be used until a window has been created.
If an error occurs, 0 will be returned
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike, to be measured
:type font: (str or (str, int[, str]) or None)
:param character: specifies a SINGLE CHARACTER character to measure
:type character: (str)
:return: Width in pixels of "A"
:rtype: (int)
"""
# A window must exist before can perform this operation. Create the hidden master root if it doesn't exist
_get_hidden_master_root()
size = 0
try:
size = tkinter.font.Font(font=font).measure(character) # single character width
except Exception as e:
_error_popup_with_traceback('Exception retrieving char width in pixels', e)
return size
@classmethod
def char_height_in_pixels(cls, font):
"""
Get the height of a string if using the supplied font in pixels.
Cannot be used until a window has been created.
If an error occurs, 0 will be returned
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike, to be measured
:type font: (str or (str, int[, str]) or None)
:return: Height in pixels of "A"
:rtype: (int)
"""
# A window must exist before can perform this operation. Create the hidden master root if it doesn't exist
_get_hidden_master_root()
size = 0
try:
size = tkinter.font.Font(font=font).metrics('linespace')
except Exception as e:
_error_popup_with_traceback('Exception retrieving char height in pixels', e)
return size
@classmethod
def string_width_in_pixels(cls, font, string):
"""
Get the with of the supplied string in pixels for the font being passed in.
If an error occurs, 0 will be returned
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike, to be measured
:type font: (str or (str, int[, str]) or None)
:param string: the string to measure
:type string: str
:return: Width in pixels of string
:rtype: (int)
"""
# A window must exist before can perform this operation. Create the hidden master root if it doesn't exist
_get_hidden_master_root()
size = 0
try:
size = tkinter.font.Font(font=font).measure(string) # string's width
except Exception as e:
_error_popup_with_traceback('Exception retrieving string width in pixels', e)
return size
def _print_to_element(
self,
*args,
end=None,
sep=None,
text_color=None,
background_color=None,
autoscroll=None,
justification=None,
font=None,
append=None,
):
"""
Print like Python normally prints except route the output to a multiline element and also add colors if desired
:param multiline_element: The multiline element to be output to
:type multiline_element: (Multiline)
:param args: The arguments to print
:type args: List[Any]
:param end: The end char to use just like print uses
:type end: (str)
:param sep: The separation character like print uses
:type sep: (str)
:param text_color: color of the text
:type text_color: (str)
:param background_color: The background color of the line
:type background_color: (str)
:param autoscroll: If True (the default), the element will scroll to bottom after updating
:type autoscroll: (bool)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike for the value being updated
:type font: str | (str, int)
"""
end_str = str(end) if end is not None else '\n'
sep_str = str(sep) if sep is not None else ' '
outstring = ''
num_args = len(args)
for i, arg in enumerate(args):
outstring += str(arg)
if i != num_args - 1:
outstring += sep_str
outstring += end_str
if append:
outstring = self.get() + outstring
self.update(outstring, text_color=text_color, background_color=background_color, font=font)
try: # if the element is set to autorefresh, then refresh the parent window
if self.AutoRefresh:
self.ParentForm.refresh()
except:
pass
def print(
self,
*args,
end=None,
sep=None,
text_color=None,
background_color=None,
justification=None,
font=None,
colors=None,
t=None,
b=None,
c=None,
autoscroll=True,
append=True,
):
"""
Print like Python normally prints except route the output to a multiline element and also add colors if desired
colors -(str, str) or str. A combined text/background color definition in a single parameter
There are also "aliases" for text_color, background_color and colors (t, b, c)
t - An alias for color of the text (makes for shorter calls)
b - An alias for the background_color parameter
c - (str, str) - "shorthand" way of specifying color. (foreground, backgrouned)
c - str - can also be a string of the format "foreground on background" ("white on red")
With the aliases it's possible to write the same print but in more compact ways:
cprint('This will print white text on red background', c=('white', 'red'))
cprint('This will print white text on red background', c='white on red')
cprint('This will print white text on red background', text_color='white', background_color='red')
cprint('This will print white text on red background', t='white', b='red')
:param args: The arguments to print
:type args: (Any)
:param end: The end char to use just like print uses
:type end: (str)
:param sep: The separation character like print uses
:type sep: (str)
:param text_color: The color of the text
:type text_color: (str)
:param background_color: The background color of the line
:type background_color: (str)
:param justification: text justification. left, right, center. Can use single characters l, r, c. Sets only for this value, not entire element
:type justification: (str)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike for the args being printed
:type font: (str or (str, int[, str]) or None)
:param colors: Either a tuple or a string that has both the text and background colors. Or just the text color
:type colors: (str) or (str, str)
:param t: Color of the text
:type t: (str)
:param b: The background color of the line
:type b: (str)
:param c: Either a tuple or a string that has both the text and background colors or just tex color (same as the color parm)
:type c: (str) or (str, str)
:param autoscroll: If True the contents of the element will automatically scroll as more data added to the end
:type autoscroll: (bool)
"""
kw_text_color = text_color or t
kw_background_color = background_color or b
dual_color = colors or c
try:
if isinstance(dual_color, tuple):
kw_text_color = dual_color[0]
kw_background_color = dual_color[1]
elif isinstance(dual_color, str):
if ' on ' in dual_color: # if has "on" in the string, then have both text and background
kw_text_color = dual_color.split(' on ')[0]
kw_background_color = dual_color.split(' on ')[1]
else: # if no "on" then assume the color string is just the text color
kw_text_color = dual_color
except Exception as e:
print('* multiline print warning * you messed up with color formatting', e)
self._print_to_element(
*args,
end=end,
sep=sep,
text_color=kw_text_color,
background_color=kw_background_color,
justification=justification,
autoscroll=autoscroll,
font=font,
append=append,
)
Get = get
Update = update

View File

@ -0,0 +1,493 @@
from __future__ import annotations
import tkinter as tk
from tkinter import ttk
from typing import Any
from typing import Dict
from typing import List
import FreeSimpleGUI
from FreeSimpleGUI import ELEM_TYPE_TREE
from FreeSimpleGUI import Element
from FreeSimpleGUI import LOOK_AND_FEEL_TABLE
from FreeSimpleGUI import theme_button_color
from FreeSimpleGUI._utils import _error_popup_with_traceback
from FreeSimpleGUI._utils import _exit_mainloop
class Tree(Element):
"""
Tree Element - Presents data in a tree-like manner, much like a file/folder browser. Uses the TreeData class
to hold the user's data and pass to the element for display.
"""
def __init__(
self,
data=None,
headings=None,
visible_column_map=None,
col_widths=None,
col0_width=10,
col0_heading='',
def_col_width=10,
auto_size_columns=True,
max_col_width=20,
select_mode=None,
show_expanded=False,
change_submits=False,
enable_events=False,
click_toggles_select=None,
font=None,
justification='right',
text_color=None,
border_width=None,
background_color=None,
selected_row_colors=(None, None),
header_text_color=None,
header_background_color=None,
header_font=None,
header_border_width=None,
header_relief=None,
num_rows=None,
sbar_trough_color=None,
sbar_background_color=None,
sbar_arrow_color=None,
sbar_width=None,
sbar_arrow_width=None,
sbar_frame_color=None,
sbar_relief=None,
row_height=None,
vertical_scroll_only=True,
hide_vertical_scroll=False,
pad=None,
p=None,
key=None,
k=None,
tooltip=None,
right_click_menu=None,
expand_x=False,
expand_y=False,
visible=True,
metadata=None,
):
"""
:param data: The data represented using a PySimpleGUI provided TreeData class
:type data: (TreeData)
:param headings: List of individual headings for each column
:type headings: List[str]
:param visible_column_map: Determines if a column should be visible. If left empty, all columns will be shown
:type visible_column_map: List[bool]
:param col_widths: List of column widths so that individual column widths can be controlled
:type col_widths: List[int]
:param col0_width: Size of Column 0 which is where the row numbers will be optionally shown
:type col0_width: (int)
:param col0_heading: Text to be shown in the header for the left-most column
:type col0_heading: (str)
:param def_col_width: default column width
:type def_col_width: (int)
:param auto_size_columns: if True, the size of a column is determined using the contents of the column
:type auto_size_columns: (bool)
:param max_col_width: the maximum size a column can be
:type max_col_width: (int)
:param select_mode: Use same values as found on Table Element. Valid values include: TABLE_SELECT_MODE_NONE TABLE_SELECT_MODE_BROWSE TABLE_SELECT_MODE_EXTENDED
:type select_mode: (enum)
:param show_expanded: if True then the tree will be initially shown with all nodes completely expanded
:type show_expanded: (bool)
:param change_submits: DO NOT USE. Only listed for backwards compat - Use enable_events instead
:type change_submits: (bool)
:param enable_events: Turns on the element specific events. Tree events happen when row is clicked
:type enable_events: (bool)
:param click_toggles_select: If True then clicking a row will cause the selection for that row to toggle between selected and deselected
:type click_toggles_select: (bool)
:param font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type font: (str or (str, int[, str]) or None)
:param justification: 'left', 'right', 'center' are valid choices
:type justification: (str)
:param text_color: color of the text
:type text_color: (str)
:param border_width: Border width/depth in pixels
:type border_width: (int)
:param background_color: color of background
:type background_color: (str)
:param selected_row_colors: Sets the text color and background color for a selected row. Same format as button colors - tuple ('red', 'yellow') or string 'red on yellow'. Defaults to theme's button color
:type selected_row_colors: str or (str, str)
:param header_text_color: sets the text color for the header
:type header_text_color: (str)
:param header_background_color: sets the background color for the header
:type header_background_color: (str)
:param header_font: specifies the font family, size, etc. Tuple or Single string format 'name size styles'. Styles: italic * roman bold normal underline overstrike
:type header_font: (str or (str, int[, str]) or None)
:param header_border_width: Border width for the header portion
:type header_border_width: (int | None)
:param header_relief: Relief style for the header. Values are same as other elements that use relief. RELIEF_RAISED RELIEF_SUNKEN RELIEF_FLAT RELIEF_RIDGE RELIEF_GROOVE RELIEF_SOLID
:type header_relief: (str | None)
:param num_rows: The number of rows of the table to display at a time
:type num_rows: (int)
:param row_height: height of a single row in pixels
:type row_height: (int)
:param vertical_scroll_only: if True only the vertical scrollbar will be visible
:type vertical_scroll_only: (bool)
:param hide_vertical_scroll: if True vertical scrollbar will be hidden
:type hide_vertical_scroll: (bool)
:param sbar_trough_color: Scrollbar color of the trough
:type sbar_trough_color: (str)
:param sbar_background_color: Scrollbar color of the background of the arrow buttons at the ends AND the color of the "thumb" (the thing you grab and slide). Switches to arrow color when mouse is over
:type sbar_background_color: (str)
:param sbar_arrow_color: Scrollbar color of the arrow at the ends of the scrollbar (it looks like a button). Switches to background color when mouse is over
:type sbar_arrow_color: (str)
:param sbar_width: Scrollbar width in pixels
:type sbar_width: (int)
:param sbar_arrow_width: Scrollbar width of the arrow on the scrollbar. It will potentially impact the overall width of the scrollbar
:type sbar_arrow_width: (int)
:param sbar_frame_color: Scrollbar Color of frame around scrollbar (available only on some ttk themes)
:type sbar_frame_color: (str)
:param sbar_relief: Scrollbar relief that will be used for the "thumb" of the scrollbar (the thing you grab that slides). Should be a constant that is defined at starting with "RELIEF_" - RELIEF_RAISED, RELIEF_SUNKEN, RELIEF_FLAT, RELIEF_RIDGE, RELIEF_GROOVE, RELIEF_SOLID
:type sbar_relief: (str)
:param pad: Amount of padding to put around element in pixels (left/right, top/bottom) or ((left, right), (top, bottom)) or an int. If an int, then it's converted into a tuple (int, int)
:type pad: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param p: Same as pad parameter. It's an alias. If EITHER of them are set, then the one that's set will be used. If BOTH are set, pad will be used
:type p: (int, int) or ((int, int),(int,int)) or (int,(int,int)) or ((int, int),int) | int
:param key: Used with window.find_element and with return values to uniquely identify this element to uniquely identify this element
:type key: str | int | tuple | object
:param k: Same as the Key. You can use either k or key. Which ever is set will be used.
:type k: str | int | tuple | object
:param tooltip: text, that will appear when mouse hovers over the element
:type tooltip: (str)
:param right_click_menu: A list of lists of Menu items to show when this element is right clicked. See user docs for exact format.
:type right_click_menu: List[List[str] | str]]
:param expand_x: If True the element will automatically expand in the X direction to fill available space
:type expand_x: (bool)
:param expand_y: If True the element will automatically expand in the Y direction to fill available space
:type expand_y: (bool)
:param visible: set visibility state of the element
:type visible: (bool)
:param metadata: User metadata that can be set to ANYTHING
:type metadata: (Any)
"""
self.image_dict = {}
self.TreeData = data
self.ColumnHeadings = headings
self.ColumnsToDisplay = visible_column_map
self.ColumnWidths = col_widths
self.MaxColumnWidth = max_col_width
self.DefaultColumnWidth = def_col_width
self.AutoSizeColumns = auto_size_columns
self.BackgroundColor = background_color if background_color is not None else FreeSimpleGUI.DEFAULT_BACKGROUND_COLOR
self.TextColor = text_color
self.HeaderTextColor = header_text_color if header_text_color is not None else LOOK_AND_FEEL_TABLE[FreeSimpleGUI.CURRENT_LOOK_AND_FEEL]['TEXT_INPUT']
self.HeaderBackgroundColor = header_background_color if header_background_color is not None else LOOK_AND_FEEL_TABLE[FreeSimpleGUI.CURRENT_LOOK_AND_FEEL]['INPUT']
self.HeaderBorderWidth = header_border_width
self.BorderWidth = border_width
self.HeaderRelief = header_relief
self.click_toggles_select = click_toggles_select
if selected_row_colors == (None, None):
# selected_row_colors = DEFAULT_TABLE_AND_TREE_SELECTED_ROW_COLORS
selected_row_colors = theme_button_color()
else:
try:
if isinstance(selected_row_colors, str):
selected_row_colors = selected_row_colors.split(' on ')
except Exception as e:
print('* Table Element Warning * you messed up with color formatting of Selected Row Color', e)
self.SelectedRowColors = selected_row_colors
self.HeaderFont = header_font
self.Justification = justification
self.InitialState = None
self.SelectMode = select_mode
self.ShowExpanded = show_expanded
self.NumRows = num_rows
self.Col0Width = col0_width
self.col0_heading = col0_heading
self.TKTreeview = None # type: ttk.Treeview
self.element_frame = None # type: tk.Frame
self.VerticalScrollOnly = vertical_scroll_only
self.HideVerticalScroll = hide_vertical_scroll
self.SelectedRows = []
self.ChangeSubmits = change_submits or enable_events
self.RightClickMenu = right_click_menu
self.RowHeight = row_height
self.IconList = {}
self.IdToKey = {'': ''}
self.KeyToID = {'': ''}
key = key if key is not None else k
pad = pad if pad is not None else p
self.expand_x = expand_x
self.expand_y = expand_y
super().__init__(
ELEM_TYPE_TREE,
text_color=text_color,
background_color=background_color,
font=font,
pad=pad,
key=key,
tooltip=tooltip,
visible=visible,
metadata=metadata,
sbar_trough_color=sbar_trough_color,
sbar_background_color=sbar_background_color,
sbar_arrow_color=sbar_arrow_color,
sbar_width=sbar_width,
sbar_arrow_width=sbar_arrow_width,
sbar_frame_color=sbar_frame_color,
sbar_relief=sbar_relief,
)
return
def _treeview_selected(self, event):
"""
Not a user function. Callback function that happens when an item is selected from the tree. In this
method, it saves away the reported selections so they can be properly returned.
:param event: An event parameter passed in by tkinter. Not used
:type event: (Any)
"""
selections = self.TKTreeview.selection()
selected_rows = [self.IdToKey[x] for x in selections]
if self.click_toggles_select:
if set(self.SelectedRows) == set(selected_rows):
for item in selections:
self.TKTreeview.selection_remove(item)
selections = []
self.SelectedRows = [self.IdToKey[x] for x in selections]
if self.ChangeSubmits:
if self.Key is not None:
self.ParentForm.LastButtonClicked = self.Key
else:
self.ParentForm.LastButtonClicked = ''
self.ParentForm.FormRemainedOpen = True
_exit_mainloop(self.ParentForm)
def add_treeview_data(self, node):
"""
Not a user function. Recursive method that inserts tree data into the tkinter treeview widget.
:param node: The node to insert. Will insert all nodes from starting point downward, recursively
:type node: (TreeData)
"""
if node.key != '':
if node.icon:
try:
if node.icon not in self.image_dict:
if type(node.icon) is bytes:
photo = tk.PhotoImage(data=node.icon)
else:
photo = tk.PhotoImage(file=node.icon)
self.image_dict[node.icon] = photo
else:
photo = self.image_dict.get(node.icon)
node.photo = photo
id = self.TKTreeview.insert(
self.KeyToID[node.parent],
'end',
iid=None,
text=node.text,
values=node.values,
open=self.ShowExpanded,
image=node.photo,
)
self.IdToKey[id] = node.key
self.KeyToID[node.key] = id
except:
self.photo = None
else:
id = self.TKTreeview.insert(
self.KeyToID[node.parent],
'end',
iid=None,
text=node.text,
values=node.values,
open=self.ShowExpanded,
)
self.IdToKey[id] = node.key
self.KeyToID[node.key] = id
for node in node.children:
self.add_treeview_data(node)
def update(self, values=None, key=None, value=None, text=None, icon=None, visible=None):
"""
Changes some of the settings for the Tree Element. Must call `Window.Read` or `Window.Finalize` prior
Changes will not be visible in your window until you call window.read or window.refresh.
If you change visibility, your element may MOVE. If you want it to remain stationary, use the "layout helper"
function "pin" to ensure your element is "pinned" to that location in your layout so that it returns there
when made visible.
:param values: Representation of the tree
:type values: (TreeData)
:param key: identifies a particular item in tree to update
:type key: str | int | tuple | object
:param value: sets the node identified by key to a particular value
:type value: (Any)
:param text: sets the node identified by key to this string
:type text: (str)
:param icon: can be either a base64 icon or a filename for the icon
:type icon: bytes | str
:param visible: control visibility of element
:type visible: (bool)
"""
if not self._widget_was_created(): # if widget hasn't been created yet, then don't allow
return
if self._this_elements_window_closed():
_error_popup_with_traceback('Error in Tree.update - The window was closed')
return
if values is not None:
children = self.TKTreeview.get_children()
for i in children:
self.TKTreeview.detach(i)
self.TKTreeview.delete(i)
children = self.TKTreeview.get_children()
self.TreeData = values
self.IdToKey = {'': ''}
self.KeyToID = {'': ''}
self.add_treeview_data(self.TreeData.root_node)
self.SelectedRows = []
if key is not None:
for id in self.IdToKey.keys():
if key == self.IdToKey[id]:
break
else:
id = None
print('** Key not found **')
else:
id = None
if id:
# item = self.TKTreeview.item(id)
if value is not None:
self.TKTreeview.item(id, values=value)
if text is not None:
self.TKTreeview.item(id, text=text)
if icon is not None:
try:
if type(icon) is bytes:
photo = tk.PhotoImage(data=icon)
else:
photo = tk.PhotoImage(file=icon)
self.TKTreeview.item(id, image=photo)
self.IconList[key] = photo # save so that it's not deleted (save reference)
except:
pass
# item = self.TKTreeview.item(id)
if visible is False:
self._pack_forget_save_settings(self.element_frame)
elif visible is True:
self._pack_restore_settings(self.element_frame)
if visible is not None:
self._visible = visible
return self
Update = update
class TreeData:
"""
Class that user fills in to represent their tree data. It's a very simple tree representation with a root "Node"
with possibly one or more children "Nodes". Each Node contains a key, text to display, list of values to display
and an icon. The entire tree is built using a single method, Insert. Nothing else is required to make the tree.
"""
class Node:
"""
Contains information about the individual node in the tree
"""
def __init__(self, parent, key, text, values, icon=None):
"""
Represents a node within the TreeData class
:param parent: The parent Node
:type parent: (TreeData.Node)
:param key: Used to uniquely identify this node
:type key: str | int | tuple | object
:param text: The text that is displayed at this node's location
:type text: (str)
:param values: The list of values that are displayed at this node
:type values: List[Any]
:param icon: just a icon
:type icon: str | bytes
"""
self.parent = parent # type: TreeData.Node
self.children = [] # type: List[TreeData.Node]
self.key = key # type: str
self.text = text # type: str
self.values = values # type: List[Any]
self.icon = icon # type: str | bytes
def _Add(self, node):
self.children.append(node)
def __init__(self):
"""
Instantiate the object, initializes the Tree Data, creates a root node for you
"""
self.tree_dict = {} # type: Dict[str, TreeData.Node]
self.root_node = self.Node('', '', 'root', [], None) # The root node
self.tree_dict[''] = self.root_node # Start the tree out with the root node
def _AddNode(self, key, node):
"""
Adds a node to tree dictionary (not user callable)
:param key: Uniquely identifies this Node
:type key: (str)
:param node: Node being added
:type node: (TreeData.Node)
"""
self.tree_dict[key] = node
def insert(self, parent, key, text, values, icon=None):
"""
Inserts a node into the tree. This is how user builds their tree, by Inserting Nodes
This is the ONLY user callable method in the TreeData class
:param parent: the parent Node
:type parent: (Node)
:param key: Used to uniquely identify this node
:type key: str | int | tuple | object
:param text: The text that is displayed at this node's location
:type text: (str)
:param values: The list of values that are displayed at this node
:type values: List[Any]
:param icon: icon
:type icon: str | bytes
"""
node = self.Node(parent, key, text, values, icon)
self.tree_dict[key] = node
parent_node = self.tree_dict[parent]
parent_node._Add(node)
def __repr__(self):
"""
Converts the TreeData into a printable version, nicely formatted
:return: (str) A formatted, text version of the TreeData
:rtype:
"""
return self._NodeStr(self.root_node, 1)
def _NodeStr(self, node, level):
"""
Does the magic of converting the TreeData into a nicely formatted string version
:param node: The node to begin printing the tree
:type node: (TreeData.Node)
:param level: The indentation level for string formatting
:type level: (int)
"""
return '\n'.join([str(node.key) + ' : ' + str(node.text) + ' [ ' + ', '.join([str(v) for v in node.values]) + ' ]'] + [' ' * 4 * level + self._NodeStr(child, level + 1) for child in node.children])
Insert = insert