filedialog.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479
  1. """File selection dialog classes.
  2. Classes:
  3. - FileDialog
  4. - LoadFileDialog
  5. - SaveFileDialog
  6. This module also presents tk common file dialogues, it provides interfaces
  7. to the native file dialogues available in Tk 4.2 and newer, and the
  8. directory dialogue available in Tk 8.3 and newer.
  9. These interfaces were written by Fredrik Lundh, May 1997.
  10. """
  11. from tkinter import *
  12. from tkinter.dialog import Dialog
  13. from tkinter import commondialog
  14. import os
  15. import fnmatch
  16. dialogstates = {}
  17. class FileDialog:
  18. """Standard file selection dialog -- no checks on selected file.
  19. Usage:
  20. d = FileDialog(master)
  21. fname = d.go(dir_or_file, pattern, default, key)
  22. if fname is None: ...canceled...
  23. else: ...open file...
  24. All arguments to go() are optional.
  25. The 'key' argument specifies a key in the global dictionary
  26. 'dialogstates', which keeps track of the values for the directory
  27. and pattern arguments, overriding the values passed in (it does
  28. not keep track of the default argument!). If no key is specified,
  29. the dialog keeps no memory of previous state. Note that memory is
  30. kept even when the dialog is canceled. (All this emulates the
  31. behavior of the Macintosh file selection dialogs.)
  32. """
  33. title = "File Selection Dialog"
  34. def __init__(self, master, title=None):
  35. if title is None: title = self.title
  36. self.master = master
  37. self.directory = None
  38. self.top = Toplevel(master)
  39. self.top.title(title)
  40. self.top.iconname(title)
  41. self.botframe = Frame(self.top)
  42. self.botframe.pack(side=BOTTOM, fill=X)
  43. self.selection = Entry(self.top)
  44. self.selection.pack(side=BOTTOM, fill=X)
  45. self.selection.bind('<Return>', self.ok_event)
  46. self.filter = Entry(self.top)
  47. self.filter.pack(side=TOP, fill=X)
  48. self.filter.bind('<Return>', self.filter_command)
  49. self.midframe = Frame(self.top)
  50. self.midframe.pack(expand=YES, fill=BOTH)
  51. self.filesbar = Scrollbar(self.midframe)
  52. self.filesbar.pack(side=RIGHT, fill=Y)
  53. self.files = Listbox(self.midframe, exportselection=0,
  54. yscrollcommand=(self.filesbar, 'set'))
  55. self.files.pack(side=RIGHT, expand=YES, fill=BOTH)
  56. btags = self.files.bindtags()
  57. self.files.bindtags(btags[1:] + btags[:1])
  58. self.files.bind('<ButtonRelease-1>', self.files_select_event)
  59. self.files.bind('<Double-ButtonRelease-1>', self.files_double_event)
  60. self.filesbar.config(command=(self.files, 'yview'))
  61. self.dirsbar = Scrollbar(self.midframe)
  62. self.dirsbar.pack(side=LEFT, fill=Y)
  63. self.dirs = Listbox(self.midframe, exportselection=0,
  64. yscrollcommand=(self.dirsbar, 'set'))
  65. self.dirs.pack(side=LEFT, expand=YES, fill=BOTH)
  66. self.dirsbar.config(command=(self.dirs, 'yview'))
  67. btags = self.dirs.bindtags()
  68. self.dirs.bindtags(btags[1:] + btags[:1])
  69. self.dirs.bind('<ButtonRelease-1>', self.dirs_select_event)
  70. self.dirs.bind('<Double-ButtonRelease-1>', self.dirs_double_event)
  71. self.ok_button = Button(self.botframe,
  72. text="OK",
  73. command=self.ok_command)
  74. self.ok_button.pack(side=LEFT)
  75. self.filter_button = Button(self.botframe,
  76. text="Filter",
  77. command=self.filter_command)
  78. self.filter_button.pack(side=LEFT, expand=YES)
  79. self.cancel_button = Button(self.botframe,
  80. text="Cancel",
  81. command=self.cancel_command)
  82. self.cancel_button.pack(side=RIGHT)
  83. self.top.protocol('WM_DELETE_WINDOW', self.cancel_command)
  84. # XXX Are the following okay for a general audience?
  85. self.top.bind('<Alt-w>', self.cancel_command)
  86. self.top.bind('<Alt-W>', self.cancel_command)
  87. def go(self, dir_or_file=os.curdir, pattern="*", default="", key=None):
  88. if key and key in dialogstates:
  89. self.directory, pattern = dialogstates[key]
  90. else:
  91. dir_or_file = os.path.expanduser(dir_or_file)
  92. if os.path.isdir(dir_or_file):
  93. self.directory = dir_or_file
  94. else:
  95. self.directory, default = os.path.split(dir_or_file)
  96. self.set_filter(self.directory, pattern)
  97. self.set_selection(default)
  98. self.filter_command()
  99. self.selection.focus_set()
  100. self.top.wait_visibility() # window needs to be visible for the grab
  101. self.top.grab_set()
  102. self.how = None
  103. self.master.mainloop() # Exited by self.quit(how)
  104. if key:
  105. directory, pattern = self.get_filter()
  106. if self.how:
  107. directory = os.path.dirname(self.how)
  108. dialogstates[key] = directory, pattern
  109. self.top.destroy()
  110. return self.how
  111. def quit(self, how=None):
  112. self.how = how
  113. self.master.quit() # Exit mainloop()
  114. def dirs_double_event(self, event):
  115. self.filter_command()
  116. def dirs_select_event(self, event):
  117. dir, pat = self.get_filter()
  118. subdir = self.dirs.get('active')
  119. dir = os.path.normpath(os.path.join(self.directory, subdir))
  120. self.set_filter(dir, pat)
  121. def files_double_event(self, event):
  122. self.ok_command()
  123. def files_select_event(self, event):
  124. file = self.files.get('active')
  125. self.set_selection(file)
  126. def ok_event(self, event):
  127. self.ok_command()
  128. def ok_command(self):
  129. self.quit(self.get_selection())
  130. def filter_command(self, event=None):
  131. dir, pat = self.get_filter()
  132. try:
  133. names = os.listdir(dir)
  134. except OSError:
  135. self.master.bell()
  136. return
  137. self.directory = dir
  138. self.set_filter(dir, pat)
  139. names.sort()
  140. subdirs = [os.pardir]
  141. matchingfiles = []
  142. for name in names:
  143. fullname = os.path.join(dir, name)
  144. if os.path.isdir(fullname):
  145. subdirs.append(name)
  146. elif fnmatch.fnmatch(name, pat):
  147. matchingfiles.append(name)
  148. self.dirs.delete(0, END)
  149. for name in subdirs:
  150. self.dirs.insert(END, name)
  151. self.files.delete(0, END)
  152. for name in matchingfiles:
  153. self.files.insert(END, name)
  154. head, tail = os.path.split(self.get_selection())
  155. if tail == os.curdir: tail = ''
  156. self.set_selection(tail)
  157. def get_filter(self):
  158. filter = self.filter.get()
  159. filter = os.path.expanduser(filter)
  160. if filter[-1:] == os.sep or os.path.isdir(filter):
  161. filter = os.path.join(filter, "*")
  162. return os.path.split(filter)
  163. def get_selection(self):
  164. file = self.selection.get()
  165. file = os.path.expanduser(file)
  166. return file
  167. def cancel_command(self, event=None):
  168. self.quit()
  169. def set_filter(self, dir, pat):
  170. if not os.path.isabs(dir):
  171. try:
  172. pwd = os.getcwd()
  173. except OSError:
  174. pwd = None
  175. if pwd:
  176. dir = os.path.join(pwd, dir)
  177. dir = os.path.normpath(dir)
  178. self.filter.delete(0, END)
  179. self.filter.insert(END, os.path.join(dir or os.curdir, pat or "*"))
  180. def set_selection(self, file):
  181. self.selection.delete(0, END)
  182. self.selection.insert(END, os.path.join(self.directory, file))
  183. class LoadFileDialog(FileDialog):
  184. """File selection dialog which checks that the file exists."""
  185. title = "Load File Selection Dialog"
  186. def ok_command(self):
  187. file = self.get_selection()
  188. if not os.path.isfile(file):
  189. self.master.bell()
  190. else:
  191. self.quit(file)
  192. class SaveFileDialog(FileDialog):
  193. """File selection dialog which checks that the file may be created."""
  194. title = "Save File Selection Dialog"
  195. def ok_command(self):
  196. file = self.get_selection()
  197. if os.path.exists(file):
  198. if os.path.isdir(file):
  199. self.master.bell()
  200. return
  201. d = Dialog(self.top,
  202. title="Overwrite Existing File Question",
  203. text="Overwrite existing file %r?" % (file,),
  204. bitmap='questhead',
  205. default=1,
  206. strings=("Yes", "Cancel"))
  207. if d.num != 0:
  208. return
  209. else:
  210. head, tail = os.path.split(file)
  211. if not os.path.isdir(head):
  212. self.master.bell()
  213. return
  214. self.quit(file)
  215. # For the following classes and modules:
  216. #
  217. # options (all have default values):
  218. #
  219. # - defaultextension: added to filename if not explicitly given
  220. #
  221. # - filetypes: sequence of (label, pattern) tuples. the same pattern
  222. # may occur with several patterns. use "*" as pattern to indicate
  223. # all files.
  224. #
  225. # - initialdir: initial directory. preserved by dialog instance.
  226. #
  227. # - initialfile: initial file (ignored by the open dialog). preserved
  228. # by dialog instance.
  229. #
  230. # - parent: which window to place the dialog on top of
  231. #
  232. # - title: dialog title
  233. #
  234. # - multiple: if true user may select more than one file
  235. #
  236. # options for the directory chooser:
  237. #
  238. # - initialdir, parent, title: see above
  239. #
  240. # - mustexist: if true, user must pick an existing directory
  241. #
  242. class _Dialog(commondialog.Dialog):
  243. def _fixoptions(self):
  244. try:
  245. # make sure "filetypes" is a tuple
  246. self.options["filetypes"] = tuple(self.options["filetypes"])
  247. except KeyError:
  248. pass
  249. def _fixresult(self, widget, result):
  250. if result:
  251. # keep directory and filename until next time
  252. # convert Tcl path objects to strings
  253. try:
  254. result = result.string
  255. except AttributeError:
  256. # it already is a string
  257. pass
  258. path, file = os.path.split(result)
  259. self.options["initialdir"] = path
  260. self.options["initialfile"] = file
  261. self.filename = result # compatibility
  262. return result
  263. #
  264. # file dialogs
  265. class Open(_Dialog):
  266. "Ask for a filename to open"
  267. command = "tk_getOpenFile"
  268. def _fixresult(self, widget, result):
  269. if isinstance(result, tuple):
  270. # multiple results:
  271. result = tuple([getattr(r, "string", r) for r in result])
  272. if result:
  273. path, file = os.path.split(result[0])
  274. self.options["initialdir"] = path
  275. # don't set initialfile or filename, as we have multiple of these
  276. return result
  277. if not widget.tk.wantobjects() and "multiple" in self.options:
  278. # Need to split result explicitly
  279. return self._fixresult(widget, widget.tk.splitlist(result))
  280. return _Dialog._fixresult(self, widget, result)
  281. class SaveAs(_Dialog):
  282. "Ask for a filename to save as"
  283. command = "tk_getSaveFile"
  284. # the directory dialog has its own _fix routines.
  285. class Directory(commondialog.Dialog):
  286. "Ask for a directory"
  287. command = "tk_chooseDirectory"
  288. def _fixresult(self, widget, result):
  289. if result:
  290. # convert Tcl path objects to strings
  291. try:
  292. result = result.string
  293. except AttributeError:
  294. # it already is a string
  295. pass
  296. # keep directory until next time
  297. self.options["initialdir"] = result
  298. self.directory = result # compatibility
  299. return result
  300. #
  301. # convenience stuff
  302. def askopenfilename(**options):
  303. "Ask for a filename to open"
  304. return Open(**options).show()
  305. def asksaveasfilename(**options):
  306. "Ask for a filename to save as"
  307. return SaveAs(**options).show()
  308. def askopenfilenames(**options):
  309. """Ask for multiple filenames to open
  310. Returns a list of filenames or empty list if
  311. cancel button selected
  312. """
  313. options["multiple"]=1
  314. return Open(**options).show()
  315. # FIXME: are the following perhaps a bit too convenient?
  316. def askopenfile(mode = "r", **options):
  317. "Ask for a filename to open, and returned the opened file"
  318. filename = Open(**options).show()
  319. if filename:
  320. return open(filename, mode)
  321. return None
  322. def askopenfiles(mode = "r", **options):
  323. """Ask for multiple filenames and return the open file
  324. objects
  325. returns a list of open file objects or an empty list if
  326. cancel selected
  327. """
  328. files = askopenfilenames(**options)
  329. if files:
  330. ofiles=[]
  331. for filename in files:
  332. ofiles.append(open(filename, mode))
  333. files=ofiles
  334. return files
  335. def asksaveasfile(mode = "w", **options):
  336. "Ask for a filename to save as, and returned the opened file"
  337. filename = SaveAs(**options).show()
  338. if filename:
  339. return open(filename, mode)
  340. return None
  341. def askdirectory (**options):
  342. "Ask for a directory, and return the file name"
  343. return Directory(**options).show()
  344. # --------------------------------------------------------------------
  345. # test stuff
  346. def test():
  347. """Simple test program."""
  348. root = Tk()
  349. root.withdraw()
  350. fd = LoadFileDialog(root)
  351. loadfile = fd.go(key="test")
  352. fd = SaveFileDialog(root)
  353. savefile = fd.go(key="test")
  354. print(loadfile, savefile)
  355. # Since the file name may contain non-ASCII characters, we need
  356. # to find an encoding that likely supports the file name, and
  357. # displays correctly on the terminal.
  358. # Start off with UTF-8
  359. enc = "utf-8"
  360. import sys
  361. # See whether CODESET is defined
  362. try:
  363. import locale
  364. locale.setlocale(locale.LC_ALL,'')
  365. enc = locale.nl_langinfo(locale.CODESET)
  366. except (ImportError, AttributeError):
  367. pass
  368. # dialog for openening files
  369. openfilename=askopenfilename(filetypes=[("all files", "*")])
  370. try:
  371. fp=open(openfilename,"r")
  372. fp.close()
  373. except:
  374. print("Could not open File: ")
  375. print(sys.exc_info()[1])
  376. print("open", openfilename.encode(enc))
  377. # dialog for saving files
  378. saveasfilename=asksaveasfilename()
  379. print("saveas", saveasfilename.encode(enc))
  380. if __name__ == '__main__':
  381. test()