EDIT2: model.hasChildren(parentIndex) returns True, but model.rowCount(parentIndex) returns 0. Is QFileSystemModel just fubar in PyQt?
EDIT: With a bit of adaptation this all works exactly as it should if I use QDirModel. This is deprecated, but maybe QFileSystemModel hasn't been fully implemented in PyQt?
I'm learning the Qt Model/View architecture at the moment, and I've found something that doesn't work as I'd expect it to. I've got the following code (adapted from Qt Model Classes):
from PyQt4 import QtCore, QtGui
model = QtGui.QFileSystemModel()
parentIndex = model.index(QtCore.QDir.currentPath())
print model.isDir(parentIndex) #prints True
print model.data(parentIndex).toString() #prints name of current directory
rows = model.rowCount(parentIndex)
print rows #prints 0 (even though the current directory has directory and file children)
The question:
Is this a problem with PyQt, have I just done something wrong, or am I completely misunderstanding QFileSystemModel? According to the documentation, model.rowCount(parentIndex) should return the number of children in the current directory. (I'm running this under Ubuntu with Python 2.6)
The QFileSystemModel docs say that it needs an instance of a Gui application, so I've also placed the above code in a QWidget as follows, but with the same result:
import sys
from PyQt4 import QtCore, QtGui
class Widget(QtGui.QWidget):
def __init__(self, parent=None):
QtGui.QWidget.__init__(self, parent)
model = QtGui.QFileSystemModel()
parentIndex = model.index(QtCore.QDir.currentPath())
print model.isDir(parentIndex)
print model.data(parentIndex).toString()
rows = model.rowCount(parentIndex)
print rows
def main():
app = QtGui.QApplication(sys.argv)
widget = Widget()
widget.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
I've solved it.
The reason to use QFileSystemModel as opposed to QDirModel is because QFileSystemModel loads the data from the filesystem in a separate thread. The problem with that is that if you try to print the number of children just after it's been constructed is that it won't have loaded the children yet. The way to fix the above code is to add the following:
self.timer = QtCore.QTimer(self)
self.timer.singleShot(1, self.printRowCount)
to the end of the constructor, and add a printRowCount method which will print the correct number of children. Phew.
Since you've already figured it out, just a couple of extra thoughts on what was going on with your model: QFileSystemModel::rowCount returns rows from the visibleChildren collection; I guess you're correctly identified the problem: at the time when you're checking row count it was not yet populated. I've changed your example without using timers; pls, check if it works for you:
class Widget(QtGui.QWidget):
def __init__(self, parent=None):
QtGui.QWidget.__init__(self, parent)
self.model = QtGui.QFileSystemModel()
self.model.setRootPath(QtCore.QDir.currentPath())
def checkParent(self):
parentIndex = self.model.index(QtCore.QDir.currentPath())
print self.model.isDir(parentIndex)
print self.model.data(parentIndex).toString()
rows = self.model.rowCount(parentIndex)
print "row count:", rows
def main():
app = QtGui.QApplication(sys.argv)
widget = Widget()
widget.show()
app.processEvents(QtCore.QEventLoop.AllEvents)
widget.checkParent()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
I believe your code should work correctly on any UI event after widget constructed is shown on the screen
regards
Related
The slot detectClipboardUrl of QClipboard::dataChanged() was called twice sometimes when I copy url in Google Chrome's address bar in this code, tested with PyQt5.7,Python3.5 on Win7 32bit, also on Linux Mint 18,
while I need the slot to be called only once , is this a bug ? any solutions ?
import sys
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
class MainWindow(QTableView):
def __init__(self, parent=None):
super().__init__(parent)
self.clipboard = QApplication.clipboard()
self.clipboard.dataChanged.connect(self.detectClipboardUrl)
#pyqtSlot()
def detectClipboardUrl(self):
print(self.clipboard.text())
if __name__ == "__main__":
app = QApplication(sys.argv)
ui = MainWindow()
ui.show()
sys.exit(app.exec_())
If the changes are duplicates, you can do something like:
class MainWindow(QTableView):
def __init__(self, parent=None):
self.clipboard = QApplication.clipboard()
self._cb_last = hash(self.clipboard.text())
self.clipboard.dataChanged.connect(self.detectClipboardUrl)
#pyqtSlot()
def detectClipboardUrl(self):
text = self.clipboard.text()
cb_current = hash(text)
if cb_current != self._cb_last:
print('clipboard text changed:', text)
self._cb_last = cb_current
The reason for using hash is simply to avoid keeping very large strings in memory.
Alternatively, if the two signals arrive very close together, you could use a QTimer to block any changes that occur within a few milliseconds of the first one.
UPDATE:
As I suspected, the problem is caused by a bug in Chromium: see Issue 173691.
I have created a simple test program in Qt Designer that allows you to select a folder and display its contents on a window. It looks like this:
I have successfully converted the .ui file to .py without fail. Next, here is my code to run the program, aptly named main.py:
from PyQt4 import QtGui
import sys
import design
import os
class ExampleApp(QtGui.QMainWindow, design.Ui_MainWindow):
def _init_(self):
super(self._class_, self)._init_()
self.setupUI(self)
self.btnBrowse.clicked.connect(self.browse_folder)
def browse_folder(self):
self.listWidget.clear()
directory = QtGui.QFileDialog.getExistingDirectory(self,"Pick a Folder")
if directory:
for file_name in os.listdir(directory):
self.listWidget.addItem(file_name)
def main():
app = QtGui.QApplication(sys.argv)
form = ExampleApp()
form.show()
app.exec_()
if __name__ == '__main__':
main()
In my command prompt, I run the following code:
python main.py
It proceeds to load for a second or two, and then I get this:
Is there something that I am doing wrong? Why isn't my program showing up the way it should be? Any help is appreciated!
These lines are wrong:
def _init_(self):
super(self._class_, self)._init_()
Instead you want something like:
def __init__(self, parent=None):
super(ExampleApp, self).__init__(parent)
Note the double underscores, the different super argument, and passing parent to the super class. I can't test this right now, but it should be much closer to working.
By naming your __init__ method incorrectly it never would've been called. That explains why you get a window but not the one you designed.
I'm using PyQt to build a simple IDE and getting weird errors if you load an empty file. A small example script is posted below:
#!/usr/bin/env python
import sys
from PyQt4 import QtGui
class TestApp(QtGui.QMainWindow):
def __init__(self, filename=None):
super(TestApp, self).__init__()
self._editor = QtGui.QPlainTextEdit()
self._editor.modificationChanged.connect(self._change_modified)
self.setCentralWidget(self._editor)
self._editor.setPlainText('a')
def _change_modified(self, have_change):
print(have_change)
if __name__ == '__main__':
a = QtGui.QApplication([])
app = TestApp()
app.show()
sys.exit(a.exec_())
As expected, this shows a window with a plain text editor. As soon as the setPlainText method is called, the editor emits two events: One modificationChanged event with changes=True, a second with changes=False.
A bit weird, but fine.
However, if you change setPlainText('a') to setPlainText(''), only a single event is emitted, this time with changes=True. Even worse, after telling the editor it's not modified with setModified(False), it insists it's been changed somehow.
Does anyone know what's causing this and how I can work around this issue?
Update: It seems to be a bug & also affects QPlainTextEdit.clear().
The workaround below places a wrapper around the QPlainTextEdit to fix clear() and setPlainText('').
#!/usr/bin/env python
import sys
from PyQt4 import QtGui
class TestApp(QtGui.QMainWindow):
def __init__(self, filename=None):
super(TestApp, self).__init__()
self._editor = PlainTextEdit()
self._editor.modificationChanged.connect(self._change_modified)
self.setCentralWidget(self._editor)
self._editor.setPlainText('')
def _change_modified(self, have_change):
print(have_change)
class PlainTextEdit(QtGui.QPlainTextEdit):
def clear(self):
self.selectAll()
cursor = self.textCursor()
cursor.removeSelectedText()
doc = self.document()
doc.clearUndoRedoStacks()
doc.setModified(False)
self.modificationChanged.emit(False)
def setPlainText(self, text):
if text:
super(PlainTextEdit, self).setPlainText(text)
else:
self.clear()
if __name__ == '__main__':
a = QtGui.QApplication([])
app = TestApp()
app.show()
sys.exit(a.exec_())
It's a Qt bug, and the straightforward workaround is to check for empty contents if modifications are indicated.
I am trying to intercept paste() for a specific edit box. After much reading and head scratching I decided to try the big hammer and monkey patch. This didn't work for me either. Anyone know why?
import sys
from PyQt4 import QtGui
def myPaste():
print("paste") # Never gets here
if __name__ == "__main__":
# QtGui.QLineEdit.paste = myPaste # Try #1
app = QtGui.QApplication(sys.argv)
window = QtGui.QMainWindow()
window.setWindowTitle("monkey")
centralWidget = QtGui.QWidget(window)
edit = QtGui.QLineEdit(centralWidget)
# QtGui.QLineEdit.paste = myPaste # Try #2
edit.paste = myPaste # Try #3
window.setCentralWidget(centralWidget)
window.show()
app.exec_()
Based on feedback..i was able to use the event filter suggestion to solve my problem. Updated example code follows...
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
window = QtGui.QMainWindow()
window.setWindowTitle("monkey")
centralWidget = QtGui.QWidget(window)
edit = QtGui.QLineEdit(centralWidget)
window.setCentralWidget(centralWidget)
def eventFilter(obj, e):
if isinstance(obj, QtGui.QLineEdit):
if (e.type() == QtCore.QEvent.KeyPress):
if (e.matches(QtGui.QKeySequence.Paste)):
obj.paste()
t=str(obj.text()).title() # Special handling here...uppercase each word for example
obj.setText(t)
return True
return False
else:
return QtGui.QMainWindow.eventFilter(obj, e)
window.eventFilter = eventFilter
edit.installEventFilter(window)
window.show()
app.exec_()
The reason you can't "monkey-patch" QLineEdit.paste() is because it's not a virtual function. The important thing about virtual functions is that when they are overridden, the reimplemented function will get called internally by Qt; whereas non-virtual overrides will only get called by Python code. So, since QLinedit.paste() isn't virtual, you will instead have to intercept all the events that would normally result in it being called internally by Qt.
That will mean reimplementing QLineEdit.keyPressEvent, so that you can trap the shortcuts for the default key bindings; and also QLineEdit.contextMenuEvent, so that you can modify the default context menu. And, depending on what you're trying to do, you might also need to override the default drag-and-drop handling. (If you prefer not to use a subclass, an event-filter can be used to monitor all the relevant events).
The QClipboard class provides access to the system clipboard, which will allow you to intercept the text before it is pasted. Every application has a single clipboard object, which can be accessed via QApplication.clipboard() or qApp.clipboard().
In order to do what you want you can subclass QLineEdit and create a method that provides the custom paste functionality that you want (paste method isn't virtual so if it is overriden it won't be called from Qt code). In addition you will need an event filter to intercept the shortcut for CTRL+V. Probably you will have to filter the middle mouse button too which is also used to paste the clipboard content. From the event filter you can call your replacement of paste method.
You can use the following code as starting point:
import sys
from PyQt4.QtGui import *
from PyQt4.QtCore import *
class myEditor(QLineEdit):
def __init__(self, parent=None):
super(myEditor, self).__init__(parent)
def myPaste(self):
self.insert("custom text pasted! ")
class myWindow(QMainWindow):
def __init__(self, parent=None):
super(myWindow, self).__init__(parent)
self.customEditor = myEditor(self)
self.setCentralWidget(self.customEditor)
self.customEditor.installEventFilter(self)
def eventFilter(self, obj, e):
if (obj == self.customEditor):
if (e.type() == QEvent.KeyPress):
if (e.matches(QKeySequence.Paste)):
self.customEditor.myPaste()
return True
return False
else:
return QMainWindow.eventFilter(obj, e)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = myWindow()
window.show()
app.exec_()
The event filter here only takes care of the keyboard shortcut for pasting. As I said you need to consider also other sources of the paste operation.
Good Day to All,
Been writing code for years, but still a bit green when it comes to PyQt,
so please forgive my syntactically lacking question(s) ;-)
I'm hacking a derivative of the (famous?) packaged example that
comes with PyQt4 (and Qt), namely "basicsortfiltermodel.pyw"
from "../examples/itemviews" in PyQt4...
I've added a little popup menu (let's call this B.py) that one can launch
from the BasicSort-derivative (let's call this A.py).
I believe I'm correcting adding new data (a new record) to the
QSortFilterProxyModel(). (I think this because I'm not getting
any errors now, after some effort) But I seem to be unable to get
the QTreeView to refresh. I've scoured the Qt class docs and
Google'd the heck out of it (seems like a common question from
the looks of it, lol)..
Now I know this is an ugly hack, but just to try to get it to work
(elegance can come later is my theory)...
At the bottom of A.py, I declared a global "wX",
global wX;
[...]
window = Window()
wX = Window()
window.setSourceModel(createMailModel(window))
so that when I hit a button later, I could more easily get a hold
of the "parent" value found in the runtime "createMailModel".
From which I get the "model" handle..
model = QtGui.QStandardItemModel(0, 17, WinX)
addMail(model, "image",
"tabl00",
etc
etc)
Anyways,..I think this is working....
But after adding a new record via addMail(), I can't seem to get
self.proxyModel to refresh itself..
I'm pretty sure this a stupid newbie issue, lol....but could anyone
help shed some light on how to make this work?
Many Thanks,
I believe you have to add new items into your original model not the proxy one. Once items is added proxy model and view will updates themselves accordingly. See if an example below would work for you:
import sys
from PyQt4 import QtGui
class MainForm(QtGui.QMainWindow):
def __init__(self, parent=None):
super(MainForm, self).__init__(parent)
self.setMinimumSize(300, 400)
self.model = QtGui.QStandardItemModel()
self.sortModel = QtGui.QSortFilterProxyModel()
self.sortModel.setSourceModel(self.model)
parentItem = self.model.invisibleRootItem()
parentItem.appendRow(QtGui.QStandardItem("3"))
parentItem.appendRow(QtGui.QStandardItem("1"))
parentItem.appendRow(QtGui.QStandardItem("4"))
parentItem.appendRow(QtGui.QStandardItem("2"))
self.view = QtGui.QListView(self)
self.view.setModel(self.sortModel)
self.view.setGeometry(0, 0, 200, 400)
self.button = QtGui.QPushButton("add items", self)
self.button.move(200, 0)
self.button.clicked.connect(self.on_button_clicked)
self.layout = QtGui.QVBoxLayout(self.centralWidget())
self.layout.addWidget(self.view)
self.layout.addWidget(self.button)
self.sortModel.sort(0)
def on_button_clicked(self):
parentItem = self.model.invisibleRootItem()
parentItem.appendRow(QtGui.QStandardItem("222"))
parentItem.appendRow(QtGui.QStandardItem("333"))
parentItem.appendRow(QtGui.QStandardItem("444"))
def main():
app = QtGui.QApplication(sys.argv)
form = MainForm()
form.show()
app.exec_()
if __name__ == '__main__':
main()
hope this helps, regards