| """File selection dialog classes. | |
| Classes: | |
| - FileDialog | |
| - LoadFileDialog | |
| - SaveFileDialog | |
| This module also presents tk common file dialogues, it provides interfaces | |
| to the native file dialogues available in Tk 4.2 and newer, and the | |
| directory dialogue available in Tk 8.3 and newer. | |
| These interfaces were written by Fredrik Lundh, May 1997. | |
| """ | |
| __all__ = ["FileDialog", "LoadFileDialog", "SaveFileDialog", | |
| "Open", "SaveAs", "Directory", | |
| "askopenfilename", "asksaveasfilename", "askopenfilenames", | |
| "askopenfile", "askopenfiles", "asksaveasfile", "askdirectory"] | |
| import fnmatch | |
| import os | |
| import tkinter | |
| from tkinter import ( | |
| ACTIVE, CENTER, EW, NORMAL, NS, NSEW, RAISED, W, YES, BOTTOM, TOP, Tk, X, | |
| Toplevel, END, Listbox, BOTH, | |
| ) | |
| from tkinter import ttk | |
| from tkinter import messagebox | |
| from tkinter import commondialog | |
| from tkinter.simpledialog import (_setup_dialog, _place_window, _temp_grab_focus, | |
| _underline_ampersand, _find_alt_key_target) | |
| dialogstates = {} | |
| class FileDialog: | |
| """Standard file selection dialog -- no checks on selected file. | |
| The layout and behavior follow the classic Motif file selection dialog. | |
| Usage: | |
| d = FileDialog(master) | |
| fname = d.go(dir_or_file, pattern, default, key) | |
| if fname is None: ...canceled... | |
| else: ...open file... | |
| All arguments to go() are optional. | |
| The 'key' argument specifies a key in the global dictionary | |
| 'dialogstates', which keeps track of the values for the directory | |
| and pattern arguments, overriding the values passed in (it does | |
| not keep track of the default argument!). If no key is specified, | |
| the dialog keeps no memory of previous state. Note that memory is | |
| kept even when the dialog is canceled. (All this emulates the | |
| behavior of the Macintosh file selection dialogs.) | |
| """ | |
| title = "File Selection Dialog" | |
| def _widget(self, klass, master, **kw): | |
| # Create a themed (ttk) or classic (tkinter) widget. ttk has no | |
| # Listbox, so the directory and file lists stay classic. | |
| return getattr(ttk if self.use_ttk else tkinter, klass)(master, **kw) | |
| def _frame(self, master): | |
| # A structural frame. The classic file dialog gives its frames a | |
| # raised border on X11; the themed one does not (cf. tk_dialog). | |
| frame = self._widget('Frame', master) | |
| if not self.use_ttk and self.top._windowingsystem == 'x11': | |
| frame.configure(relief=RAISED, bd=1) | |
| return frame | |
| def _make_list(self, column, label, browse, activate): | |
| # Create a labelled listbox with vertical and horizontal scrollbars in | |
| # the middle frame, like the Motif file dialog (cf. xmfbox.tcl | |
| # MotifFDialog_MakeSList). | |
| frame = self._widget('Frame', self.midframe) | |
| frame.grid(row=0, column=column, sticky=NSEW, padx='3p', pady='3p') | |
| frame.grid_rowconfigure(1, weight=1) | |
| frame.grid_columnconfigure(0, weight=1) | |
| vbar = self._widget('Scrollbar', frame, takefocus=0) | |
| hbar = self._widget('Scrollbar', frame, orient='horizontal', takefocus=0) | |
| listbox = Listbox(frame, exportselection=0, | |
| xscrollcommand=(hbar, 'set'), | |
| yscrollcommand=(vbar, 'set')) | |
| vbar.configure(command=(listbox, 'yview')) | |
| hbar.configure(command=(listbox, 'xview')) | |
| self._label(frame, label, listbox).grid( | |
| row=0, column=0, columnspan=2, sticky=EW, padx='1.5p', pady='1.5p') | |
| listbox.grid(row=1, column=0, sticky=NSEW) | |
| vbar.grid(row=1, column=1, sticky=NS) | |
| hbar.grid(row=2, column=0, sticky=EW) | |
| # Instance bindings fire after the Listbox class bindings, which have | |
| # already updated the selection. | |
| btags = listbox.bindtags() | |
| listbox.bindtags(btags[1:] + btags[:1]) | |
| listbox.bind('<<ListboxSelect>>', browse) | |
| listbox.bind('<Double-ButtonRelease-1>', activate) | |
| listbox.bind('<Return>', lambda e: (browse(e), activate(e))) | |
| # Type a few characters to jump to a matching entry, like the Motif | |
| # file dialog (cf. xmfbox.tcl ListBoxKeyAccel). | |
| listbox.bind('<Key>', self._listbox_keyaccel) | |
| return listbox, vbar, hbar | |
| def __init__(self, master, title=None, *, use_ttk=True): | |
| if title is None: title = self.title | |
| self.master = master | |
| self.use_ttk = use_ttk | |
| self.directory = None | |
| self._keyaccel = {} # listbox -> recently typed prefix | |
| self._keyaccel_after = {} # listbox -> pending reset callback id | |
| self.top = Toplevel(master) | |
| self.top.withdraw() # remain invisible until placed by go() | |
| self.top.title(title) | |
| self.top.iconname(title) | |
| # Keep the dialog above its parent, like SimpleDialog and Dialog (cf. | |
| # xmfbox.tcl). Skip it when the master is not viewable, or the dialog | |
| # would itself be opened withdrawn. | |
| if master.winfo_viewable(): | |
| self.top.transient(master) | |
| _setup_dialog(self.top) | |
| if self.use_ttk: | |
| # Use a single themed background for the whole dialog so it blends | |
| # with the ttk widgets (cf. tk::MessageBox). | |
| self.top.configure( | |
| background=ttk.Style(self.top).lookup('.', 'background')) | |
| # Tk traverses focus in widget-creation order, so the widgets are | |
| # created top to bottom -- filter, lists, selection, buttons -- to make | |
| # Tab follow the visual layout, like the Motif file dialog (cf. | |
| # xmfbox.tcl). | |
| self.filter = self._widget('Entry', self.top) | |
| self.filter.bind('<Return>', self.filter_command) | |
| self.filter_label = self._label(self.top, 'Fil&ter:', self.filter) | |
| self.filter_label.pack(side=TOP, fill=X, padx='4.5p', pady='3p') | |
| self.filter.pack(side=TOP, fill=X, padx='3p') | |
| self.midframe = self._frame(self.top) | |
| self.midframe.pack(side=TOP, expand=YES, fill=BOTH) | |
| # Directory list (left) and file list (right), each with a label and | |
| # vertical and horizontal scrollbars, like the Motif file dialog (cf. | |
| # xmfbox.tcl). | |
| self.dirs, self.dirsbar, self.dirshbar = self._make_list( | |
| 0, '&Directory:', self.dirs_select_event, self.dirs_double_event) | |
| self.files, self.filesbar, self.fileshbar = self._make_list( | |
| 1, 'Fi&les:', self.files_select_event, self.files_double_event) | |
| # Give the file list twice the width of the directory list, like the | |
| # Motif file dialog (cf. xmfbox.tcl). | |
| self.midframe.grid_rowconfigure(0, weight=1) | |
| self.midframe.grid_columnconfigure(0, weight=1) | |
| self.midframe.grid_columnconfigure(1, minsize=150, weight=2) | |
| self.selection = self._widget('Entry', self.top) | |
| self.selection.bind('<Return>', self.ok_event) | |
| self.selection_label = self._label(self.top, '&Selection:', self.selection) | |
| self.selection.pack(side=BOTTOM, fill=X, padx='3p', pady='3p') | |
| self.selection_label.pack(side=BOTTOM, fill=X, padx='4.5p') | |
| # Created last so the buttons traverse last, but packed below the | |
| # selection field. | |
| self.botframe = self._frame(self.top) | |
| self.botframe.pack(side=BOTTOM, fill=X, before=self.selection) | |
| # tk::MessageBox and tk_dialog space the buttons differently. | |
| padx, pady = ('3m', '2m') if self.use_ttk else ('7.5p', '3p') | |
| for i, (name, label, command) in enumerate(( | |
| ('ok', '&OK', self.ok_command), | |
| ('filter', '&Filter', self.filter_command), | |
| ('cancel', '&Cancel', self.cancel_command))): | |
| # Create a button with an accelerator key marked by "&" in the text, | |
| # like SimpleDialog and Dialog (cf. tk::AmpWidget). | |
| text, underline = _underline_ampersand(label) | |
| button = self._widget('Button', self.botframe, name=name, text=text, | |
| underline=underline, command=command, | |
| default=ACTIVE if name == 'ok' else NORMAL) | |
| button.bind('<<AltUnderlined>>', lambda e: e.widget.invoke()) | |
| button.grid(column=i, row=0, sticky=EW, padx=padx, pady=pady) | |
| # tk::MessageBox makes the buttons equal width; tk_dialog does not. | |
| self.botframe.grid_columnconfigure( | |
| i, uniform='buttons' if self.use_ttk else '') | |
| if self.top._windowingsystem == 'aqua': | |
| self.botframe.grid_columnconfigure(i, minsize=90) | |
| button.grid_configure(pady=7) | |
| setattr(self, name + '_button', button) | |
| self.botframe.grid_anchor(CENTER) | |
| self.top.protocol('WM_DELETE_WINDOW', self.cancel_command) | |
| # Alt + an underlined character invokes the matching button (cf. | |
| # ::tk::AltKeyInDialog), like SimpleDialog and Dialog. | |
| self.top.bind('<Alt-Key>', self._alt_key) | |
| # The default ring follows the keyboard focus among the buttons (cf. | |
| # tk::MessageBox), like SimpleDialog and Dialog. | |
| self.top.bind('<FocusIn>', lambda e: self._set_default(e.widget, ACTIVE)) | |
| self.top.bind('<FocusOut>', lambda e: self._set_default(e.widget, NORMAL)) | |
| self.top.bind('<Escape>', self.cancel_command) | |
| def go(self, dir_or_file=os.curdir, pattern="*", default="", key=None): | |
| if key and key in dialogstates: | |
| self.directory, pattern = dialogstates[key] | |
| else: | |
| dir_or_file = os.path.expanduser(dir_or_file) | |
| if os.path.isdir(dir_or_file): | |
| self.directory = dir_or_file | |
| else: | |
| self.directory, default = os.path.split(dir_or_file) | |
| self.set_filter(self.directory, pattern) | |
| self.set_selection(default) | |
| self.filter_command() | |
| # Center the dialog over its parent and make it visible, like | |
| # SimpleDialog and Dialog (cf. xmfbox.tcl). | |
| _place_window(self.top, self.master) | |
| self.selection.select_range(0, END) # so the user can type a new name | |
| self.top.wait_visibility() # window needs to be visible for the grab | |
| self.how = None | |
| # Grab the input and restore the previous focus and grab afterwards, | |
| # like SimpleDialog and Dialog. go() destroys the window itself below, | |
| # so leave it in place here. | |
| with _temp_grab_focus(self.top, self.selection, destroy=False): | |
| self.master.mainloop() # Exited by self.quit(how) | |
| if key: | |
| directory, pattern = self.get_filter() | |
| if self.how: | |
| directory = os.path.dirname(self.how) | |
| dialogstates[key] = directory, pattern | |
| self.top.destroy() | |
| return self.how | |
| def quit(self, how=None): | |
| self.how = how | |
| self.master.quit() # Exit mainloop() | |
| def _alt_key(self, event): | |
| # Invoke the button whose accelerator matches the Alt key (cf. | |
| # SimpleDialog and Dialog). | |
| target = _find_alt_key_target(self.top, event.char) | |
| if target is not None: | |
| target.event_generate('<<AltUnderlined>>') | |
| def _set_default(self, widget, state): | |
| # Set a button's default ring. | |
| if widget.winfo_class() in ('Button', 'TButton'): | |
| widget.configure(default=state) | |
| def _label(self, master, label, target): | |
| # Create a field label whose "&" accelerator focuses the target widget, | |
| # like the labels of the Motif file dialog (cf. xmfbox.tcl). | |
| text, underline = _underline_ampersand(label) | |
| widget = self._widget('Label', master, text=text, underline=underline, | |
| anchor=W) | |
| widget.bind('<<AltUnderlined>>', lambda e: target.focus_set()) | |
| return widget | |
| def _listbox_keyaccel(self, event): | |
| # Append the typed character and jump to a matching entry, resetting | |
| # after a short pause (cf. xmfbox.tcl ListBoxKeyAccel_Key). | |
| char = event.char | |
| if not char or not char.isprintable(): | |
| return | |
| listbox = event.widget | |
| prefix = self._keyaccel.get(listbox, '') + char | |
| self._keyaccel[listbox] = prefix | |
| self._listbox_keyaccel_goto(listbox, prefix) | |
| after_id = self._keyaccel_after.get(listbox) | |
| if after_id is not None: | |
| listbox.after_cancel(after_id) | |
| self._keyaccel_after[listbox] = listbox.after( | |
| 500, lambda: self._keyaccel.pop(listbox, None)) | |
| def _listbox_keyaccel_goto(self, listbox, prefix): | |
| # Select the first entry not preceding the typed prefix (cf. xmfbox.tcl | |
| # ListBoxKeyAccel_Goto). | |
| prefix = prefix.lower() | |
| index = -1 | |
| for i in range(listbox.size()): | |
| item = listbox.get(i).lower() | |
| if prefix >= item: | |
| index = i | |
| if prefix <= item: | |
| index = i | |
| break | |
| if index >= 0: | |
| listbox.selection_clear(0, END) | |
| listbox.selection_set(index) | |
| listbox.activate(index) | |
| listbox.see(index) | |
| listbox.event_generate('<<ListboxSelect>>') | |
| def dirs_double_event(self, event): | |
| self.filter_command() | |
| # Highlight an entry so the directory list stays keyboard-navigable | |
| # after entering a directory, like the Motif file dialog (cf. | |
| # xmfbox.tcl); prefer the first subdirectory over the ".." entry. | |
| index = 1 if self.dirs.size() > 1 else 0 | |
| self.dirs.selection_set(index) | |
| self.dirs.activate(index) | |
| def dirs_select_event(self, event): | |
| # Show a selection in only one list at a time (cf. xmfbox.tcl). | |
| self.files.selection_clear(0, END) | |
| dir, pat = self.get_filter() | |
| subdir = self.dirs.get('active') | |
| dir = os.path.normpath(os.path.join(self.directory, subdir)) | |
| self.set_filter(dir, pat) | |
| # Show the end of a long path, like the Motif file dialog (cf. | |
| # xmfbox.tcl). | |
| self.filter.xview(END) | |
| def files_double_event(self, event): | |
| self.ok_command() | |
| def files_select_event(self, event): | |
| # Show a selection in only one list at a time (cf. xmfbox.tcl). | |
| self.dirs.selection_clear(0, END) | |
| file = self.files.get('active') | |
| self.set_selection(file) | |
| self.selection.xview(END) | |
| def ok_event(self, event): | |
| self.ok_command() | |
| def ok_command(self): | |
| self.quit(self.get_selection()) | |
| def filter_command(self, event=None): | |
| dir, pat = self.get_filter() | |
| try: | |
| names = os.listdir(dir) | |
| except OSError: | |
| self.master.bell() | |
| return | |
| self.directory = dir | |
| self.set_filter(dir, pat) | |
| names.sort() | |
| subdirs = [os.pardir] | |
| matchingfiles = [] | |
| for name in names: | |
| fullname = os.path.join(dir, name) | |
| if os.path.isdir(fullname): | |
| subdirs.append(name) | |
| elif fnmatch.fnmatch(name, pat): | |
| matchingfiles.append(name) | |
| self.dirs.delete(0, END) | |
| for name in subdirs: | |
| self.dirs.insert(END, name) | |
| self.files.delete(0, END) | |
| for name in matchingfiles: | |
| self.files.insert(END, name) | |
| head, tail = os.path.split(self.get_selection()) | |
| if tail == os.curdir: tail = '' | |
| self.set_selection(tail) | |
| def get_filter(self): | |
| filter = self.filter.get() | |
| filter = os.path.expanduser(filter) | |
| if filter[-1:] == os.sep or os.path.isdir(filter): | |
| filter = os.path.join(filter, "*") | |
| return os.path.split(filter) | |
| def get_selection(self): | |
| file = self.selection.get() | |
| file = os.path.expanduser(file) | |
| return file | |
| def cancel_command(self, event=None): | |
| self.quit() | |
| def set_filter(self, dir, pat): | |
| if not os.path.isabs(dir): | |
| try: | |
| pwd = os.getcwd() | |
| except OSError: | |
| pwd = None | |
| if pwd: | |
| dir = os.path.join(pwd, dir) | |
| dir = os.path.normpath(dir) | |
| self.filter.delete(0, END) | |
| self.filter.insert(END, os.path.join(dir or os.curdir, pat or "*")) | |
| def set_selection(self, file): | |
| self.selection.delete(0, END) | |
| self.selection.insert(END, os.path.join(self.directory, file)) | |
| class LoadFileDialog(FileDialog): | |
| """File selection dialog which checks that the file exists.""" | |
| title = "Load File Selection Dialog" | |
| def ok_command(self): | |
| file = self.get_selection() | |
| if not os.path.isfile(file): | |
| self.master.bell() | |
| else: | |
| self.quit(file) | |
| class SaveFileDialog(FileDialog): | |
| """File selection dialog which checks that the file may be created.""" | |
| title = "Save File Selection Dialog" | |
| def ok_command(self): | |
| file = self.get_selection() | |
| if os.path.exists(file): | |
| if os.path.isdir(file): | |
| self.master.bell() | |
| return | |
| if not messagebox.askyesno( | |
| "Overwrite Existing File", | |
| "Overwrite existing file %r?" % (file,), | |
| icon=messagebox.WARNING, parent=self.top): | |
| return | |
| else: | |
| head, tail = os.path.split(file) | |
| if not os.path.isdir(head): | |
| self.master.bell() | |
| return | |
| self.quit(file) | |
| # For the following classes and modules: | |
| # | |
| # options (all have default values): | |
| # | |
| # - defaultextension: added to filename if not explicitly given | |
| # | |
| # - filetypes: sequence of (label, pattern) tuples. the same pattern | |
| # may occur with several patterns. use "*" as pattern to indicate | |
| # all files. | |
| # | |
| # - initialdir: initial directory. preserved by dialog instance. | |
| # | |
| # - initialfile: initial file (ignored by the open dialog). preserved | |
| # by dialog instance. | |
| # | |
| # - parent: which window to place the dialog on top of | |
| # | |
| # - title: dialog title | |
| # | |
| # - multiple: if true user may select more than one file | |
| # | |
| # options for the directory chooser: | |
| # | |
| # - initialdir, parent, title: see above | |
| # | |
| # - mustexist: if true, user must pick an existing directory | |
| # | |
| class _Dialog(commondialog.Dialog): | |
| def _fixoptions(self): | |
| try: | |
| # make sure "filetypes" is a tuple | |
| self.options["filetypes"] = tuple(self.options["filetypes"]) | |
| except KeyError: | |
| pass | |
| def _fixresult(self, widget, result): | |
| if not result: | |
| result = '' # normalize the cancelled result (gh-103878) | |
| else: | |
| # keep directory and filename until next time | |
| # convert Tcl path objects to strings | |
| try: | |
| result = result.string | |
| except AttributeError: | |
| # it already is a string | |
| pass | |
| path, file = os.path.split(result) | |
| self.options["initialdir"] = path | |
| self.options["initialfile"] = file | |
| self.filename = result # compatibility | |
| return result | |
| # | |
| # file dialogs | |
| class Open(_Dialog): | |
| "Ask for a filename to open" | |
| command = "tk_getOpenFile" | |
| def _fixresult(self, widget, result): | |
| if self.options.get("multiple"): | |
| # multiple results: a tuple of filenames | |
| if not isinstance(result, tuple): | |
| result = widget.tk.splitlist(result) | |
| result = tuple([getattr(r, "string", r) for r in result]) | |
| if result: | |
| path, file = os.path.split(result[0]) | |
| self.options["initialdir"] = path | |
| # don't set initialfile or filename, as we have multiple of these | |
| return result | |
| return _Dialog._fixresult(self, widget, result) | |
| class SaveAs(_Dialog): | |
| "Ask for a filename to save as" | |
| command = "tk_getSaveFile" | |
| # the directory dialog has its own _fix routines. | |
| class Directory(commondialog.Dialog): | |
| "Ask for a directory" | |
| command = "tk_chooseDirectory" | |
| def _fixresult(self, widget, result): | |
| if not result: | |
| result = '' # normalize the cancelled result (gh-103878) | |
| else: | |
| # convert Tcl path objects to strings | |
| try: | |
| result = result.string | |
| except AttributeError: | |
| # it already is a string | |
| pass | |
| # keep directory until next time | |
| self.options["initialdir"] = result | |
| self.directory = result # compatibility | |
| return result | |
| # | |
| # convenience stuff | |
| def askopenfilename(**options): | |
| "Ask for a filename to open" | |
| return Open(**options).show() | |
| def asksaveasfilename(**options): | |
| "Ask for a filename to save as" | |
| return SaveAs(**options).show() | |
| def askopenfilenames(**options): | |
| """Ask for multiple filenames to open | |
| Returns a list of filenames or empty list if | |
| cancel button selected | |
| """ | |
| options["multiple"]=1 | |
| return Open(**options).show() | |
| # FIXME: are the following perhaps a bit too convenient? | |
| def askopenfile(mode = "r", **options): | |
| "Ask for a filename to open, and returned the opened file" | |
| filename = Open(**options).show() | |
| if filename: | |
| return open(filename, mode) | |
| return None | |
| def askopenfiles(mode = "r", **options): | |
| """Ask for multiple filenames and return the open file | |
| objects | |
| returns a list of open file objects or an empty list if | |
| cancel selected | |
| """ | |
| import warnings | |
| warnings._deprecated( | |
| "tkinter.filedialog.askopenfiles", | |
| message=f"{warnings._DEPRECATED_MSG}; iterate over the names returned " | |
| "by askopenfilenames() and open them instead", | |
| remove=(3, 19)) | |
| files = askopenfilenames(**options) | |
| return [open(filename, mode) for filename in files] | |
| def asksaveasfile(mode = "w", **options): | |
| "Ask for a filename to save as, and returned the opened file" | |
| filename = SaveAs(**options).show() | |
| if filename: | |
| return open(filename, mode) | |
| return None | |
| def askdirectory (**options): | |
| "Ask for a directory, and return the file name" | |
| return Directory(**options).show() | |
| # -------------------------------------------------------------------- | |
| # test stuff | |
| def test(): | |
| """Simple test program.""" | |
| root = Tk() | |
| use_ttk = tkinter.BooleanVar(root, value=True) | |
| def test_load(): | |
| print('load:', LoadFileDialog(root, use_ttk=use_ttk.get()).go(key='test')) | |
| def test_save(): | |
| print('save:', SaveFileDialog(root, use_ttk=use_ttk.get()).go(key='test')) | |
| def test_open(): | |
| print('open:', askopenfilename(filetypes=[('all files', '*')])) | |
| def test_saveas(): | |
| print('saveas:', asksaveasfilename()) | |
| def test_directory(): | |
| print('directory:', askdirectory()) | |
| def test_directory_mustexist(): | |
| print('directory (mustexist):', askdirectory(mustexist=True)) | |
| tkinter.Checkbutton(root, text='Use themed (ttk) widgets', | |
| variable=use_ttk).pack(fill=X) | |
| tkinter.Button(root, text='LoadFileDialog', command=test_load).pack(fill=X) | |
| tkinter.Button(root, text='SaveFileDialog', command=test_save).pack(fill=X) | |
| tkinter.Button(root, text='askopenfilename', command=test_open).pack(fill=X) | |
| tkinter.Button(root, text='asksaveasfilename', command=test_saveas).pack(fill=X) | |
| tkinter.Button(root, text='askdirectory', command=test_directory).pack(fill=X) | |
| tkinter.Button(root, text='askdirectory (mustexist)', | |
| command=test_directory_mustexist).pack(fill=X) | |
| tkinter.Button(root, text='Quit', command=root.quit).pack(fill=X) | |
| root.mainloop() | |
| if __name__ == '__main__': | |
| test() |