I created a FileSaveAs button in my PySimpleGUI application, and defined the available file_types to be 'png' and 'jpg', but I have no way of knowing which of these two options was selected by the user. In other words, unless explicitly entered by the user, the value I get does not include the file extension.
Here's the code:
import PySimpleGUI as sg
layout = [[
sg.InputText(visible=False, enable_events=True, key='fig_path'),
sg.FileSaveAs(
key='fig_save',
file_types=(('PNG', '.png'), ('JPG', '.jpg')), # TODO: better names
)
]]
window = sg.Window('Demo Application', layout, finalize=True)
fig_canvas_agg = None
while True: # Event Loop
event, values = window.Read()
if (event == 'fig_path') and (values['fig_path'] != ''):
print('Saving to:', values['fig_path'])
if event is None:
break
Example:
In the above case, the value will be "[some path]\Test\hello", instead of ending with "hello.png".
Any way of either getting the returned path to include the extension, or getting the extension value separately?
Add defaultextension="*.*" to tk.filedialog.asksaveasfilename ()
It's around line 3332 in ver 4.30.0 of pysimplegui.py
Related
When a "choose files" dialog is displayed I want to pre-select files in a project which are already configured as being "part of" that project, so the user can select new files OR unselect existing (i.e. previously chosen) files.
This answer suggests multiple selection should be possible.
For this MRE, please make 3 files and put them in a suitable ref_dir:
from PyQt5 import QtWidgets
import sys
class Window(QtWidgets.QWidget):
def __init__(self):
super(Window, self).__init__()
self.button = QtWidgets.QPushButton('Test', self)
self.button.clicked.connect(self.handle_button)
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(self.button)
def handle_button(self):
options = QtWidgets.QFileDialog.Options()
options |= QtWidgets.QFileDialog.DontUseNativeDialog
ref_dir = 'D:\\temp'
files_list = ['file1.txt', 'file2.txt', 'file3.txt']
fd = QtWidgets.QFileDialog(None, 'Choose project files', ref_dir, '(*.txt)')
fd.setFileMode(QtWidgets.QFileDialog.ExistingFiles)
fd.setOptions(options)
# fd.setVisible(True)
for file in files_list:
print(f'selecting file |{file}|')
fd.selectFile(file)
string_list = fd.exec()
print(f'string list {string_list}')
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
window = Window()
window.show()
sys.exit(app.exec_())
Unfortunately, despite ExistingFiles having been chosen as the file mode, I find that it is only the last file selected which has the selection... but I want all three to be selected when the dialog is displayed.
I tried experimenting with setVisible to see whether the multiple selection could be achieved somehow after the dialog is displayed, but this didn't work.
Since a non-native file dialog is being used, we can access its child widgets to control its behavior.
At first I thought about using the selection model of the item views, but this won't update the line edit, which is responsible of checking if the files exist and enabling the Ok button in that case; considering this, the obvious solution is to directly update the line edit instead:
def handle_button(self):
# ...
existing = []
for file in files_list:
if fd.directory().exists(file):
existing.append('"{}"'.format(file))
lineEdit = fd.findChild(QtWidgets.QLineEdit, 'fileNameEdit')
lineEdit.setText(' '.join(existing))
if fd.exec():
print('string list {}'.format(fd.selectedFiles()))
The only drawback of this approach is that the fileSelected and filesSelected signals are not sent.
Musicamante's answer was very, very helpful, in particular showing that the selection is in fact triggered by filling the QLE with path strings.
But in fact there is a fatal flaw when the purpose is as I have stated: unfortunately, if you try to deselect the final selected file in a directory, actually this name is not then removed from the QLE. And in fact, if the QLE is set to blank this disables the "Choose" button. All this is by design: the function of a QFileDialog is either to "open" or to "save", not to "modify".
But I did find a solution, which involves finding the QListView which lists the files in the directory, and then using a signal on its selection model.
Another thing this caters for is what happens when you change directory: obviously, you then want the selection to be updated on the basis of the project's files as found (or not found) inside that directory. I've in fact changed the text of the "choose" button to show that "modification" is the name of the game.
fd = QtWidgets.QFileDialog(app.get_main_window(), 'Modify project files', start_directory, '(*.docx)')
fd.setFileMode(QtWidgets.QFileDialog.ExistingFiles)
fd.setViewMode(QtWidgets.QFileDialog.List)
fd.setLabelText(QtWidgets.QFileDialog.Reject, '&Cancel')
fd.setLabelText(QtWidgets.QFileDialog.Accept, '&Modify')
fd.setOptions(options)
file_name_line_edit = fd.findChild(QtWidgets.QLineEdit, 'fileNameEdit')
list_view = fd.findChild(QtWidgets.QListView, 'listView')
# utility to cope with all permutations of backslashes and forward slashes in path strings:
def split_file_path_str(path_str):
dir_path_str, filename = ntpath.split(path_str)
return dir_path_str, (filename or ntpath.basename(dir_path_str))
fd.displayed_dir = None
sel_model = list_view.selectionModel()
def sel_changed():
if not fd.displayed_dir:
return
selected_file_paths_in_shown_dir = []
sel_col_0s = sel_model.selectedRows()
for sel_col_0 in sel_col_0s:
file_path_str = os.path.join(fd.displayed_dir, sel_col_0.data())
selected_file_paths_in_shown_dir.append(file_path_str)
already_included = file_path_str in self.files_list
if not already_included:
fd.project_files_in_shown_dir.append(file_path_str)
# now find if there are any project files which are now NOT selected
for project_file_path_str in fd.project_files_in_shown_dir:
if project_file_path_str not in selected_file_paths_in_shown_dir:
fd.project_files_in_shown_dir.remove(project_file_path_str)
sel_model.selectionChanged.connect(sel_changed)
def file_dlg_dir_entered(displayed_dir):
displayed_dir = os.path.normpath(displayed_dir)
# this is set to None to prevent unwanted selection processing triggered by setText(...) below
fd.displayed_dir = None
fd.project_files_in_shown_dir = []
existing = []
for file_path_str in self.files_list:
dir_path_str, filename = split_file_path_str(file_path_str)
if dir_path_str == displayed_dir:
existing.append(f'"{file_path_str}"')
fd.project_files_in_shown_dir.append(file_path_str)
file_name_line_edit.setText(' '.join(existing))
fd.displayed_dir = displayed_dir
fd.directoryEntered.connect(file_dlg_dir_entered)
# set the initially displayed directory...
file_dlg_dir_entered(start_directory)
if fd.exec():
# for each file, if not present in self.files_list, add to files list and make self dirty
for project_file_in_shown_dir in fd.project_files_in_shown_dir:
if project_file_in_shown_dir not in self.files_list:
self.files_list.append(project_file_in_shown_dir)
# also add to list widget...
app.get_main_window().ui.files_list.addItem(project_file_in_shown_dir)
if not self.is_dirty():
self.toggle_dirty()
# but we also have to make sure that a file has not been UNselected...
docx_files_in_start_dir = [f for f in os.listdir(fd.displayed_dir) if os.path.isfile(os.path.join(fd.displayed_dir, f)) and os.path.splitext(f)[1] == '.docx' ]
for docx_file_in_start_dir in docx_files_in_start_dir:
docx_file_path_str = os.path.join(fd.displayed_dir, docx_file_in_start_dir)
if docx_file_path_str in self.files_list and docx_file_path_str not in fd.project_files_in_shown_dir:
self.files_list.remove(docx_file_path_str)
list_widget = app.get_main_window().ui.files_list
item_for_removal = list_widget.findItems(docx_file_path_str, QtCore.Qt.MatchExactly)[0]
list_widget.takeItem(list_widget.row(item_for_removal))
if not self.is_dirty():
self.toggle_dirty()
After a few hours of playing around, here's a pretty good way to programatically pre-select multiple files in a QFileDialog:
from pathlib import Path
from PyQt5.QtCore import QItemSelectionModel
from PyQt5.QtWidgets import QFileDialog, QListView
p_files = Path('/path/to/your/files')
dlg = QFileDialog(
directory=str(p_files),
options=QFileDialog.DontUseNativeDialog)
# get QListView which controls item selection
file_view = dlg.findChild(QListView, 'listView')
# filter files which we want to select based on any condition (eg only .txt files)
# anything will work here as long as you get a list of Path objects or just str filepaths
sel_files = [p for p in p_files.iterdir() if p.suffix == '.txt']
# get selection model (QItemSelectionModel)
sel_model = file_view.selectionModel()
for p in sel_files:
# get idx (QModelIndex) from model() (QFileSystemModel) using str of Path obj
idx = sel_model.model().index(str(p))
# set the active selection using each QModelIndex
# IMPORTANT - need to include the selection type
# see dir(QItemSelectionModel) for all options
sel_model.select(idx, QItemSelectionModel.Select | QItemSelectionModel.Rows)
dlg.exec_()
dlg.selectedFiles()
>>> ['list.txt', 'of.txt', 'selected.txt', 'files.txt']
In my code, Im trying to make a calculator. So there is a 1 button which when pressed, updates the Question: text by adding 1 to it's text. So when I press 1, the text will convert from Question: to Question: 1. But its not updating. I have faced this problem before too. I think when I do the .update, it will only update the value till its the same number of letters as the text already has. If it has 2 letters and I try to .update('123'), it will only update to 12. Is there any way to get around this???
import PySimpleGUI as sg
layout = [
[sg.Text('Question: ', key='-IN-')],
[sg.Text('Answer will be shown here', key='-OUT-')],
[sg.Button('1'), sg.Button('2'), sg.Button('3')],
[sg.Button('4'), sg.Button('5'), sg.Button('6')],
[sg.Button('7'), sg.Button('8'), sg.Button('9')],
[sg.Button('Enter'), sg.Button('Exit')]
]
window = sg.Window('calculator', layout)
while True:
event, values = window.read()
if event is None or event == 'Exit':
break
elif event == '1':
bleh = window['-IN-'].get()
teh = f'{bleh}1'
window['-IN-'].update(value=teh)
window.close()
As above comment, Example like this,
import PySimpleGUI as sg
layout = [
[sg.InputText('Question: ', readonly=True, key='-IN-')],
[sg.Text('Answer will be shown here', key='-OUT-')],
[sg.Button('1'), sg.Button('2'), sg.Button('3')],
[sg.Button('4'), sg.Button('5'), sg.Button('6')],
[sg.Button('7'), sg.Button('8'), sg.Button('9')],
[sg.Button('Enter'), sg.Button('Exit')]
]
window = sg.Window('calculator', layout)
input = window['-IN-']
while True:
event, values = window.read()
if event is None or event == 'Exit':
break
elif event in '1234567890':
bleh = window['-IN-'].get()
teh = f'{bleh}{event}'
input.update(value=teh)
input.Widget.xview("end") # view end if text is too long to fit element
window.close()
Firstly, PySimpleGUI is amazing! However, I cannot figure out how to show all the files in a folder when using folderbrowse() ?
Alternatively, would it be possible to print the filenames in the selected in an outbox box? Please could I get some guidance on this.
Thanks!
FileBrowse() and FolderBrowse() are different widgets.
FolderBrowse() is for selecting only folder so it doesn't display files.
FileBrowse() is for selecting file so it show files and folders (but you can't select folder to get it).
FileBrowse() gives full path to selected folder and later you should use
os.listdir(folder) to get names for all files and folders in selected folder (but without names in subfolders)
os.walk(folder) to get for all files and folders in this folder and subfolders.
glob.glob(pattern) to get only some names - ie. glob.glob(f"{folder}/*.png")
and when you get names then you can print in console or update text in widget.
This minimal example display filenames in console after clicking Submit
import PySimpleGUI as sg
import os
#help(sg.FolderBrowse)
#help(sg.FileBrowse)
layout = [
[sg.Input(), sg.FileBrowse('FileBrowse')],
[sg.Input(), sg.FolderBrowse('FolderBrowse')],
[sg.Submit(), sg.Cancel()],
]
window = sg.Window('Test', layout)
while True:
event, values = window.read()
#print('event:', event)
#print('values:', values)
print('FolderBrowse:', values['FolderBrowse'])
print('FileBrowse:', values['FileBrowse'])
if event is None or event == 'Cancel':
break
if event == 'Submit':
# if folder was not selected then use current folder `.`
foldername = values['FolderBrowse'] or '.'
filenames = os.listdir(foldername)
print('folder:', foldername)
print('files:', filenames)
print("\n".join(filenames))
window.close()
Similar way you can put text in some widget - ie. MultiLine() - after pressing Submit
import PySimpleGUI as sg
import os
layout = [
[sg.Input(), sg.FolderBrowse('FolderBrowse')],
[sg.Submit(), sg.Cancel()],
[sg.Text('Files')],
[sg.Multiline(key='files', size=(60,30), autoscroll=True)],
]
window = sg.Window('Test', layout)
while True:
event, values = window.read()
if event is None or event == 'Cancel':
break
if event == 'Submit':
foldername = values['FolderBrowse'] or '.'
filenames = os.listdir(foldername)
# it uses `key='files'` to access `Multiline` widget
window['files'].update("\n".join(filenames))
window.close()
BTW: system may give filenames in order of creation so you may have to sort them
filenames = sorted(os.listdir(foldername))
EDIT:
To get filenames without Submit you may have to use normal Button which will execute code with foldername = PopupGetFolder(..., no_window=True).
import PySimpleGUI as sg
import os
layout = [
[sg.Input(), sg.Button('FolderBrowse')],
[sg.Text('Files')],
[sg.Multiline(key='files', size=(60,30), autoscroll=True)],
[sg.Exit()],
]
window = sg.Window('Test', layout)
while True:
event, values = window.read()
print(event)
if event is None or event == 'Exit':
window.close()
break
if event == 'FolderBrowse':
foldername = sg.PopupGetFolder('Select folder', no_window=True)
if foldername: # `None` when clicked `Cancel` - so I skip it
filenames = sorted(os.listdir(foldername))
# it use `key='files'` to `Multiline` widget
window['files'].update("\n".join(filenames))
First, I have read the docs. I understand that the information is stored in Key= x. My issue is when I call a function from another file it does not recognize x.
I have read the docs, but failing to understand how to use the key
I tried putting x into a variable and passing it to the function.
File 1
def add_details():
today1 = date.today()
today2 = today1.strftime("%Y/%m/%d")
create = str(today2)
name = str(_name_)
reason = str(_reason_)
startDate = str(_startDate_)
endDate = str(_endDate_)
add_data(create,name,reason,startDate, endDate)
def add_data(create,name,reason,startDate, endDate):
engine.execute('INSERT INTO schedule(Created_On, Fullname, reason, Start_Date, End_Date ) VALUES (?,?,?,?,?)',(create,name,reason,startDate,endDate))
File 2
while True:
event, values = window.Read()
print(event, values)
if event in (None, 'Exit'):
break
if event == '_subdate_': #subdate is the button Submit
sf.add_details()
My expected results are that the inputs of the GUI are passed to the function, then off to a SQLite db.
Error: name 'name' not defined
(or any key variable)
This is an example that you'll find running on Trinket (https://pysimplegui.trinket.io/demo-programs#/demo-programs/design-pattern-2-persistent-window-with-updates)
It shows how keys are defined in elements and used after a read call.
import PySimpleGUI as sg
"""
DESIGN PATTERN 2 - Multi-read window. Reads and updates fields in a window
"""
# 1- the layout
layout = [[sg.Text('Your typed chars appear here:'), sg.Text(size=(15,1), key='-OUTPUT-')],
[sg.Input(key='-IN-')],
[sg.Button('Show'), sg.Button('Exit')]]
# 2 - the window
window = sg.Window('Pattern 2', layout)
# 3 - the event loop
while True:
event, values = window.read()
print(event, values)
if event in (None, 'Exit'):
break
if event == 'Show':
# Update the "output" text element to be the value of "input" element
window['-OUTPUT-'].update(values['-IN-'])
# In older code you'll find it written using FindElement or Element
# window.FindElement('-OUTPUT-').Update(values['-IN-'])
# A shortened version of this update can be written without the ".Update"
# window['-OUTPUT-'](values['-IN-'])
# 4 - the close
window.close()
I am trying to create a simple file browser using python and GTK3. Inspired by an another question here I was able to make a small working example
#!/usr/bin/python
import os
from gi.repository import Gtk
window = Gtk.Window()
window.connect("delete-event", Gtk.main_quit)
filesystemTreeStore = Gtk.TreeStore(str)
parents = {}
for (path, dirs, files) in os.walk("/home"):
for subdir in dirs:
parents[os.path.join(path, subdir)] = filesystemTreeStore.append(parents.get(path, None), [subdir])
for item in files:
filesystemTreeStore.append(parents.get(path, None), [item])
filesystemTreeView = Gtk.TreeView(filesystemTreeStore)
renderer = Gtk.CellRendererText()
filesystemColumn = Gtk.TreeViewColumn("Title", renderer, text=0)
filesystemTreeView.append_column(filesystemColumn)
window.add(filesystemTreeView)
window.show_all()
Gtk.main()
The code works, but the result feels not much effective. I was able to read and display the whole linux filesystem, but it took a very long time. One reason could be the usage of os.walk.
Another thing is, that such code does not allow opening empty directories.
For this reason I would like to display only the content of the parent directory for which the listing is made and expand the tree gradually as the user is exploring the tree structure.
I was not able to find a solution for this yet using Python and GTK3. There is a similar solution but for Tkinter
i was able to come with a solution. There could be a better solution, but I am quite happy that it is working as I expected. I append "dummy" nodes to make the folders expandable, even if the are ampty. Had to deal with adding and removing the tree content on expanding/collapsing the treeView.
Here is my solution:
#!/usr/bin/python
import os, stat
from gi.repository import Gtk
from gi.repository.GdkPixbuf import Pixbuf
def populateFileSystemTreeStore(treeStore, path, parent=None):
itemCounter = 0
# iterate over the items in the path
for item in os.listdir(path):
# Get the absolute path of the item
itemFullname = os.path.join(path, item)
# Extract metadata from the item
itemMetaData = os.stat(itemFullname)
# Determine if the item is a folder
itemIsFolder = stat.S_ISDIR(itemMetaData.st_mode)
# Generate an icon from the default icon theme
itemIcon = Gtk.IconTheme.get_default().load_icon("folder" if itemIsFolder else "empty", 22, 0)
# Append the item to the TreeStore
currentIter = treeStore.append(parent, [item, itemIcon, itemFullname])
# add dummy if current item was a folder
if itemIsFolder: treeStore.append(currentIter, [None, None, None])
#increment the item counter
itemCounter += 1
# add the dummy node back if nothing was inserted before
if itemCounter < 1: treeStore.append(parent, [None, None, None])
def onRowExpanded(treeView, treeIter, treePath):
# get the associated model
treeStore = treeView.get_model()
# get the full path of the position
newPath = treeStore.get_value(treeIter, 2)
# populate the subtree on curent position
populateFileSystemTreeStore(treeStore, newPath, treeIter)
# remove the first child (dummy node)
treeStore.remove(treeStore.iter_children(treeIter))
def onRowCollapsed(treeView, treeIter, treePath):
# get the associated model
treeStore = treeView.get_model()
# get the iterator of the first child
currentChildIter = treeStore.iter_children(treeIter)
# loop as long as some childern exist
while currentChildIter:
# remove the first child
treeStore.remove(currentChildIter)
# refresh the iterator of the next child
currentChildIter = treeStore.iter_children(treeIter)
# append dummy node
treeStore.append(treeIter, [None, None, None])
window = Gtk.Window()
window.connect("delete-event", Gtk.main_quit)
# initialize the filesystem treestore
fileSystemTreeStore = Gtk.TreeStore(str, Pixbuf, str)
# populate the tree store
populateFileSystemTreeStore(fileSystemTreeStore, '/home')
# initialize the TreeView
fileSystemTreeView = Gtk.TreeView(fileSystemTreeStore)
# Create a TreeViewColumn
treeViewCol = Gtk.TreeViewColumn("File")
# Create a column cell to display text
colCellText = Gtk.CellRendererText()
# Create a column cell to display an image
colCellImg = Gtk.CellRendererPixbuf()
# Add the cells to the column
treeViewCol.pack_start(colCellImg, False)
treeViewCol.pack_start(colCellText, True)
# Bind the text cell to column 0 of the tree's model
treeViewCol.add_attribute(colCellText, "text", 0)
# Bind the image cell to column 1 of the tree's model
treeViewCol.add_attribute(colCellImg, "pixbuf", 1)
# Append the columns to the TreeView
fileSystemTreeView.append_column(treeViewCol)
# add "on expand" callback
fileSystemTreeView.connect("row-expanded", onRowExpanded)
# add "on collapse" callback
fileSystemTreeView.connect("row-collapsed", onRowCollapsed)
scrollView = Gtk.ScrolledWindow()
scrollView.add(fileSystemTreeView)
# append the scrollView to the window (this)
window.add(scrollView)
window.connect("delete-event", Gtk.main_quit)
window.show_all()
Gtk.main()
What you need is commonly called lazy loading, which is currently not supported by/on the ideas page of GtkTreeStore but you can still create your own YourTreeStoreLazy which implements the GtkTreeModel interface. This was done a couple of times in the past but I can not seem to find any reasonable code examples. Have a look at this post and its comments(link gone)wayback archive copy for some ideas on how to approach the implementation of getters.