We are looking at deploying a PyQt application on an Azure server, and the application works well enough, albeit a little slow to respond to user actions.
We have a problem, however, and that is that the QFileDialog allows pretty much any explore action: copy a file from the virtual machine to the user's local drive, open a file within 'Program Files (x86)' in Notepad, etc.
Approaches already considered:
As the python application has to have read and write permissions to
run under 'Program Files (x86)', we can't use file permissions to
control access.
We can turn the Python into an inscrutable .exe, but this could
still be copied using the context menus in the file dialog.
We could use the file filters and then hide them, so you can only
see (and mess with) the relevant files, but the user could still
copy entire directories.
The only thing we can think of is to create our own file dialog from scratch, but that's very tedious. Are there any 'out of the box' solutions?
The QFileDialog class already has this functionality:
dialog = QtGui.QFileDialog()
dialog.setOption(QtGui.QFileDialog.ReadOnly, True)
dialog.exec_()
This only seems to work with Qt's built-in file-dialog, though. If you use the static functions to open a native file-dialog, the ReadOnly option seems to be ignored (I've only tested this on Linux, though).
looking at exemple of qtreeview they show a file explorer so i think it's actually not a big task to implement a simple file system explorer. it's specialy easy thanks to QFileSystemModel http://doc.qt.io/qt-5/model-view-programming.html#using-models-and-views
Here's what I actually did, based on #ekhumoro's advice:
from PyQt4 import QtGui
import guidata
import re
class _DirectoryFilterProxyModel(QtGui.QSortFilterProxyModel):
""" A basic filter to be applied to the file items to be displayed.
Based on C++ example at:
https://stackoverflow.com/questions/2101100/qfiledialog-filtering-folders. """
def __init__(self, ignore_directories=[], *args, **kw):
""" Constructor
:param ignore_directories: A list of directories to exclude. These
can be regular expressions or straight names. """
QtGui.QSortFilterProxyModel.__init__(self, *args, **kw)
self.ignore_directories = ignore_directories
def filterAcceptsRow(self, sourceRow, sourceParent):
fileModel = self.sourceModel()
index0 = fileModel.index(sourceRow, 0, sourceParent)
if fileModel:
if fileModel.isDir(index0):
for directory in self.ignore_directories:
if re.match(directory, fileModel.fileName(index0)):
return False
return True
else: # For files
return True
else:
return False
And instantiated:
app = guidata.qapplication()
dialog = QtGui.QFileDialog()
proxyModel = _DirectoryFilterProxyModel(ignore_directories=["Program Files", "Program Files (x86)", "Windows"])
dialog.setProxyModel(proxyModel)
dialog.setOption(QtGui.QFileDialog.ReadOnly, True)
dialog.setOption(QtGui.QFileDialog.HideNameFilterDetails, True)
dialog.exec_()
My thanks to #serge_gubenko and #Gayan on the page qfiledialog - Filtering Folders? for providing the C++ implementation, from which I derived the above.
Related
This question already has an answer here:
QLabel is not updated unless the mainWindow is unfocused
(1 answer)
Closed 1 year ago.
I've been building a fairly simple program using PyQt5 for the GUI for the past few days and I have the following problem which I can't seem to find a solution for.
Here's an oversimplified version of my code (kept the absolute basics to keep things short):
def run_blueprint(file):
# doing stuff to the file and returning the path to the output file
return full_path
class Window(QMainWindow, Ui_MainWindow):
# some variables here
def __init__(self, parent=None):
super().__init__(parent)
self.setupUi(self)
self.connectSignalsSlots()
def connectSignalsSlots(self):
self.pushButton.clicked.connect(self.processFiles)
def processFiles(self):
self.toggleElements()
for i, file in enumerate(self.selected_files_directories):
temp_full_path = run_blueprint(file)
self.listWidget_3.insertItem(i, temp_full_path)
self.toggleElements()
def toggleElements(self):
self.pushButton_2.setEnabled(not self.pushButton_2.isEnabled())
self.listWidget.setEnabled(not self.listWidget.isEnabled())
if __name__ == "__main__":
app = QApplication(sys.argv)
win = Window()
win.show()
sys.exit(app.exec())
Let's assume that I have selected an item from the listWidget and also have a list of file paths stored in the selected_files_directories[] variable.
If I run the code as is, when it's time for processFiles(self) to run I get some weird behavior. For some reason the for loop is ran first, then the first toggleElements() and after that the second toggleElements(). This results in the two elements that I want to temporarily disable until the for loop is over staying enabled the whole time.
However if I don't call the run_blueprint(file) method at all, but instead run any other code inside the for loop or even completely discard the loop, those two elements are disabled first, then the rest of the code runs and finally they are enabled again.
I believe the problem has something to do with me calling a static method outside the class. Do you have any ideas on how to solve this issue? Thanks in advance!
Edit: Below is the simplified version of the run_blueprint() function.
The basic functionality of this program is as follows: I select a template file (.docx) and input file(s) (.pdf). Then using the docxtpl and pdfplumber libraries, for each of the selected input files I get specific text pieces from them and place them inside the template document and save that as a .docx file.
In the following code assume that template_path and bp_path show the path to the template file and blueprint file respectively.
def run_blueprint(file):
doc = DocxTemplate(template_path) # docxtpl method
global lns
lns = export_data(file) # function I made that uses pdfplumber to get all the text from a pdf file
global context
context = {}
with open(bp_path, 'r', encoding='utf8') as f:
blueprint = f.read() # blueprint contains python code that populates the context variable. I do it this way to easily change between blueprints
exec(blueprint, globals(), locals()) # I know it's not a good practice to use exec but it's intended for personal use
doc.render(context) # docxtpl method
output_path = "D:\\Desktop" # could be any path
doc.save(output_path) # docxtpl method
return output_path
The blueprint code usually contains API calls that take a few milliseconds to send a response. Apart from that it's mostly regex used to locate the text pieces I'm looking for.
This is an issue with the canvas being redrawn. Add a repaint call after the toggleElements call.
def processFiles(self):
self.toggleElements()
self.repaint()
for i, file in enumerate(self.selected_files_directories):
temp_full_path = run_blueprint(file)
self.listWidget_3.insertItem(i, temp_full_path)
self.toggleElements()
For more context based off the suggestions of the comments by #musicamante and to help clarify:
There isn't an issue with the canvas being repainted. The issue is that the function blocks the main thread. A better solution, given the context, would be to offload the blocking task to a QThread or QProcess to unblock the main thread.
I've got a problem with saving file with extension (get path to file and append extension) in PyQt4 with QFileDialog. My Python code looks like that:
dialog = QtGui.QFileDialog()
dialog.setDefaultSuffix(".json")
file = dialog.getSaveFileName(None, "Title", "", "JSON (.json)")
It works, path is correct, dialog title and filter are in dialog window, but second line was ignored. File doesn't have any extension.
How to add extension by default? What am I doing wrong?
Calling setDefaultSuffix on an instance of QFileDialog has no effect when you use the static functions. Those functions will create their own internal file-dialog, and so the only options that can be set on it are whatever is made available via the arguments.
Of course, setDefaultSuffix will work if the instance of QFileDialog is shown directly:
dialog = QtGui.QFileDialog()
dialog.setFilter(dialog.filter() | QtCore.QDir.Hidden)
dialog.setDefaultSuffix('json')
dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
dialog.setNameFilters(['JSON (*.json)'])
if dialog.exec_() == QtGui.QDialog.Accepted:
print(dialog.selectedFiles())
else:
print('Cancelled')
But note that you cannot get a native file-dialog using this method.
If the file-name filters are specified correctly (see above, and
Barmak Shemirani's answer), the native file-dialog may provide a means of automatically selecting the filename extension (this is certainly the case with KDE on Linux, but I don't know about other platforms).
Try with *.json instead of .json
file = dialog.getSaveFileName(None, "Title", "", "JSON (*.json)");
I'm attempting to dump a pickle file in my PyQt app only its seems to be completely ignoring the statement.
import cPickle as pickle
class MyActions(QtGui.QMainWindow):
def __init__(self, parent=None):
QtGui.QWidget.__init__(self, parent)
#Create blank dictionary and save it.
self.employee_dict = {}
pickle.dump(self.employee_dict, open("pickle file", 'wb'))
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
QtCore.QObject.connect(self.ui.pushButton, QtCore.SIGNAL('clicked()'), self.emp_display )
QtCore.QObject.connect(self.ui.pushButton_2, QtCore.SIGNAL('clicked()'), self.admin_display)
am i doing something wrong here or is pickle just not compatible with dumping dictionaries in side a PyQt gui.
Even though your question doesn't have enough information to go on, I'm willing to bet I know the answer.
You're creating a file called pickle file in whatever the current directory happens to be.
Then you're going and looking for the pickle file right next to the script, which is in general not going to be the current directory.
In the special case where you're running a script from the command line as ./foo.py or python ./foo.py, of course, the two happen to be the same. But when you double-click a PyQt app, it's… well, that depends on what platform you're on, and a variety of Explorer/Finder/Nautilus/etc. settings (either global, or per-app), but it's generally not going to be the same place the script is.
If you just want to force it to write the file next to the script (which is usually a bad idea), you can save the script directory at startup, like this:
import os
import sys
scriptdir = os.path.dirname(sys.argv[0])
Then, instead of writing to 'pickle file', you can write to os.path.join(scriptdir, 'pickle file').
However, a better solution is to use a platform-appropriate "app data" directory or "home" or "documents" directory (depending on whether you want the file to be visible to novice users); Qt has nice wrappers that make it easy to do that in the platform-appropriate way with platform-independent code.
I've built a (Linux) GUI application that can be launched from a terminal and accepts an undefined number of files as arguments. The app reads sys.argv and lists the name of these files in a QListWidget.
The code is something like:
import sys
from PyQt4.QtGui import QApplication, QMainWindow, QCoreApplication
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
# parse command line arguments
for i in QCoreApplication.argv()[1:]:
...
def main():
app = QApplication(sys.argv)
...
What I want to do is to be able to select multiple files from a file manager and open them with my app through the "Open with..." option provided by file managers. How this can be achieved?
With the current code, when I try it only one of the selected files is shown on the QListWidget.
Edit:
It finally seems that it depends to the file manager.
I tried with a few file managers and...
pcmanfm: It only opens one of the selected files.
spacefm: Works properly!
dolphin: It opens each file to a different instance of my program. If
I select 3 files it will open my app 3 times, one for each file.
nautilus: I didn't manage to open any files with it. My program is not listed in the suggested applications and I didn't find any way to do it.
There's not really enough information to give a definite answer, but...
First, have you checked that a print sys.argv at the top of the code looks like you were expecting?
If so, does it work if you change the line...
for i in QCoreApplication.argv()[1:]:
...to...
for i in sys.argv[1:]:
For debugging purposes, you might also like to include the line...
assert QCoreApplication.argv()[1:] == sys.argv[1:]
...just before you start the for-loop.
Use a QFileDialog: Documentation
I'm programming an excel-add on and I don't need main windows and all the shiny widgets/toolkits, but I need dialogs.
here's my attempt at creating one(using pywin32):
import win32ui, win32con
def openFileDialog(extensions = None, multi = False):
"""open a Windows File 'Open' Dialog. returns a list of path of files selected by user.
Keyword arguments:
extensions (default None) - a list or tuple containing (description, ext)
pair of acceptable files.
ext can be a string - i.e) '*.txt', or a list or tuple of many strings ['*.txt', '*.htm']
description is a string describing corresponding ext.
multi - (default False) True to allow user to select more than one files.
"""
openFlags = win32con.OFN_OVERWRITEPROMPT|win32con.OFN_FILEMUSTEXIST
if multi:
openFlags|=win32con.OFN_ALLOWMULTISELECT
dlg = win32ui.CreateFileDialog(1,
None,
None,
openFlags,
extensionFilterString(extensions))
if dlg.DoModal()!=win32con.IDOK:
return None
return dlg.GetPathNames()
although they work, I think that is really un-pythonic. I had a look at pyside and they have QFileDialog - but it requires QApplication and seems like that Qapplication takes over the main loop. I don't think I need all the hassle and I really don't know about python gui programming. What is the most convenient way to open file dialogs in python?