I'm trying to put together a simple table that shows my model's data. I need row selection behaviour so I set:
self.setSelectionBehavior(QAbstractItemView.SelectRows)
All is good until I implement selectionChanged() which causes the redraw to get a bit confused every time a row is selected (cell's dont' seem to update their selection state). Here is some test code that causes the problem for me:
import sys
from PySide.QtGui import *
from PySide.QtCore import *
class Item( QStandardItem ):
def __init__( self, parent=None ):
super( Item, self).__init__( parent )
self.pixmap = QPixmap("colour.png")
#def data(self, role=Qt.UserRole + 1):
#'''with this method in place the cells get a checkbox and are not selectable'''
#return 'test'
class Model (QStandardItemModel):
def __init__( self, parent=None ):
super( Model, self).__init__( parent )
self.setHorizontalHeaderLabels(['a', 'b', 'c'])
self.init_data()
def init_data(self):
for row in range(0, 15):
for col in range(0, 10):
col_item = Item( '%s, %s' % (row, col) )
self.setItem(row, col, col_item)
class TableView( QTableView ):
def __init__( self, parent=None ):
super( TableView, self).__init__( parent )
model = Model()
self.setModel(model)
self.setSelectionBehavior(QAbstractItemView.SelectRows)
self.setSelectionMode(QAbstractItemView.ContiguousSelection)
self.setMouseTracking(True)
def selectionChanged(self, selected, deselected):
print selected
if __name__ == '__main__':
app = QApplication([])
table = TableView()
table.show()
sys.exit(app.exec_())
I'm also a bit confused about why the cell's all get a checkbox and become un-selectable if the data() method is implemented in the QStandardItem. Can somebody please help?
Cheers,
frank
You are overriding the QTableViews selectionChanged. That is (probably) used internally by the view and you prevent that. I am not sure why you would want to do that. You should use the selectionChanged signal of the selectionModel() for the view if you want to do custom things when the selection changes.
But if you insist on overriding selectionChanged on TableView at least call the parents function too, so that view could do its work:
class TableView( QTableView ):
def __init__( self, parent=None ):
super( TableView, self).__init__( parent )
model = Model()
self.setModel(model)
self.setSelectionBehavior(QAbstractItemView.SelectRows)
self.setSelectionMode(QAbstractItemView.ContiguousSelection)
self.setMouseTracking(True)
def selectionChanged(self, selected, deselected):
print selected
super(TableView, self).selectionChanged(selected, deselected)
But, you don't need to and you really shouldn't subclass a QTableView just to set a few properties. You can do that for an instance. Actually, all your sub-classes are unnecessary. You could write that code like this:
import sys
from PySide.QtGui import *
from PySide.QtCore import *
if __name__ == '__main__':
app = QApplication([])
# create model
model = QStandardItemModel()
model.setHorizontalHeaderLabels(['a', 'b', 'c'])
# fill data
for row in range(15):
model.appendRow([QStandardItem('%d, %d' % (row, col)) for col in range(10)])
# create table view
table = QTableView()
# set parameters
table.setModel(model)
table.setSelectionBehavior(QAbstractItemView.SelectRows)
table.setSelectionMode(QAbstractItemView.ContiguousSelection)
table.setMouseTracking(True)
# show
table.show()
sys.exit(app.exec_())
As for overriding the data method for the model: data method is responsible for returning many different values depending on the role that view asks. You are always returning "test" for all of them. That is, to say the least, bad. If you want to create your own models, you should at least read about how models/views work in Qt. Official docs have a nice section about it. You can also find some decent video tutorials in the web.
Related
I'm having a QListView with a QFileSystemModel. Based on a selection in a QTreeView, the QListView shows the content of the folder.
Now I need to change the color of the filenames depending on some condition.
The initial idea would be to iterate over the items in the QListView and set the color for each item depending on whether the condition is fulfilled. However this seems to be impossible, since the setData() method of QFileSystemModel only accepts changes to the EditRole, ignoring something like [see this]
self.FileModel.setData(index, QtGui.QBrush(QtCore.Qt.red), role=QtCore.Qt.ForegroundRole)
This has also been pointed out here
and the suggestion in the latter was to subclass QItemDelegate for the purpose of colorizing items in the QListView.
I therefore subclassed QStyledItemDelegate and reimplemented its paint() method to show the filename in green, if the condition is fulfilled - which works fine. However it now looks kind of ugly: File icons are lost and the "mouse_over" effect is not working anymore.
While this subclassing is anyway a messy work-around, my top-level question would be
Is there a way to colorize items in a QListView connected to a QFileSystemModel based on a condition?
Now provided that this might not be the case and sticking to the subclassing of QItemDelegate,
Is there a way to get the original behaviour with nice selections and icons back?
Does anyone know which ItemDelegate is originally used for QFileSystemModel in a QListView and how to use it?
Is it possible to get its source code and copy the paint method from there ?
Here is a minimal code that uses subclassing and shows the descibed behaviour. It uses a QLineEdit where one can type in a string, such that all files containing that string are highlighted in green.
import sys
from PyQt4 import QtGui, QtCore
class MyFileViewDelegate(QtGui.QStyledItemDelegate ):
def __init__(self, parent=None, *args, **kwargs):
QtGui.QItemDelegate.__init__(self, parent, *args)
self.condition = None
self.isMatch = False
self.brush_active = QtGui.QBrush(QtGui.QColor("#79b9ed"))
self.brush_active_matched = QtGui.QBrush(QtGui.QColor("#58cd1c"))
self.pen = QtGui.QPen(QtGui.QColor("#414141") )
self.pen_matched = QtGui.QPen(QtGui.QColor("#39c819") )
self.pen_active = QtGui.QPen(QtGui.QColor("#eef2fd") )
self.pen_active_matched = QtGui.QPen(QtGui.QColor("#e7fade") )
def paint(self, painter, option, index):
text = index.data(QtCore.Qt.DisplayRole)
self.matchText(text)
painter.save()
######## set background
painter.setPen(QtGui.QPen(QtCore.Qt.NoPen))
if option.state & QtGui.QStyle.State_Selected:
if self.isMatch:
painter.setBrush(self.brush_active_matched)
else:
painter.setBrush(self.brush_active)
painter.drawRect(option.rect)
######## set font color
if option.state & QtGui.QStyle.State_Selected:
if self.isMatch:
painter.setPen(self.pen_active_matched)
else:
painter.setPen(self.pen_active)
else:
if self.isMatch:
painter.setPen(self.pen_matched)
else:
painter.setPen(self.pen)
painter.drawText(option.rect, QtCore.Qt.AlignLeft, text)
painter.restore()
def matchText(self, filename):
# testing condition. In the real case this is much more complicated
if (self.condition != None) and (self.condition != "") and (self.condition in filename):
self.isMatch = True
else:
self.isMatch = False
def setCondition(self, condition):
self.condition = condition
class MainWidget(QtGui.QWidget):
def __init__(self, parent=None, useDelegate = False):
super(MainWidget, self).__init__(parent)
self.setLayout(QtGui.QVBoxLayout())
self.FolderModel = QtGui.QFileSystemModel()
self.FolderModel.setFilter(QtCore.QDir.NoDotAndDotDot | QtCore.QDir.AllDirs)
self.FolderModel.setRootPath("")
self.FolderView = QtGui.QTreeView(parent=self)
self.FolderView.setModel(self.FolderModel)
self.FolderView.setHeaderHidden(True)
self.FolderView.hideColumn(1)
self.FolderView.hideColumn(2)
self.FolderView.hideColumn(3)
self.FolderView.expanded.connect(self.FolderView.scrollTo)
self.FolderView.clicked[QtCore.QModelIndex].connect(self.browserClicked)
self.FileModel = QtGui.QFileSystemModel()
self.FileModel.setFilter(QtCore.QDir.NoDotAndDotDot | QtCore.QDir.Files)
self.FileView = QtGui.QListView(parent=self)
self.FileView.setModel(self.FileModel)
self.FileViewDelegate = None
if useDelegate:
self.FileViewDelegate = MyFileViewDelegate()
self.FileView.setItemDelegate(self.FileViewDelegate)
self.FileView.setSelectionMode( QtGui.QAbstractItemView.ExtendedSelection )
self.LineEdit = QtGui.QLineEdit()
self.LineEdit.textChanged.connect(self.changeCondition)
# Add Widgets to layout
self.layout().addWidget(self.FolderView)
self.layout().addWidget(self.FileView)
self.layout().addWidget(self.LineEdit)
def changeCondition(self, text):
if self.FileViewDelegate:
self.FileViewDelegate.setCondition(text)
def browserClicked(self, index):
# the signal passes the index of the clicked item
# set the FileView's root_index to the clicked index
dir_path = self.FileModel.filePath(index)
root_index = self.FileModel.setRootPath(dir_path)
self.FileView.setRootIndex(root_index)
class App(QtGui.QMainWindow):
def __init__(self, parent=None, useDelegate=False):
super(App, self).__init__(parent)
self.central = MainWidget(parent =self, useDelegate=useDelegate)
self.setCentralWidget(self.central)
if __name__=='__main__':
app = QtGui.QApplication(sys.argv)
thisapp = App(None, True) # set False to view App without custom FileViewDelegate
thisapp.show()
sys.exit(app.exec_())
This is the comparison of how it looks with and without subclassing QItemDelegate:
just to mention, another problem with this code is, that once the condition is changed, one needs to move the mouse into the QFileView to initiate the repainting. I wonder which slot I could use to connect to the LineEdit.textChange signal to do that directly.
There's no need for an item-delegate. It can be achieved much more simply by reimplementing the data method of the QFileSystemModel:
class FileSystemModel(QtGui.QFileSystemModel):
def __init__(self, *args, **kwargs):
super(FileSystemModel, self).__init__(*args, **kwargs)
self.condition = None
def setCondition(self, condition):
self.condition = condition
self.dataChanged.emit(QtCore.QModelIndex(), QtCore.QModelIndex())
def data(self, index, role=QtCore.Qt.DisplayRole):
if self.condition and role == QtCore.Qt.TextColorRole:
text = index.data(QtCore.Qt.DisplayRole)
if self.condition in text:
return QtGui.QColor("#58cd1c")
return super(FileSystemModel, self).data(index, role)
class MainWidget(QtGui.QWidget):
def __init__(self, parent=None, useDelegate = False):
super(MainWidget, self).__init__(parent)
...
self.FileModel = FileSystemModel(self)
...
def changeCondition(self, text):
self.FileModel.setCondition(text)
How do I receive notification that a QStandardItem is dragged and then dropped onto another QStandardItem, thus becoming a child of the latter?
I thought I could do this by re-implementing QStandardItemModel.moveRows, but it is not getting called after a drop :( Below is an example of what I'm trying to do. To test, run the program and drop one item in the tree view onto another. If it worked, you should see confirmation in the console that moveRows has been called.
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
class Model(QStandardItemModel):
def moveRows(
self, source_parent, source_row, count, destination_parent,
destination_child):
print(
'Moving {} row(s) from row {} of parent {} to row {} of parent {}'
.format(
count, source_row, source_parent, destination_child,
destination_parent)
)
super().moveRows(
source_parent, source_row, count, destination_parent,
destination_child)
return True
def _create_item(text):
item = QStandardItem(text)
flags = Qt.ItemIsDragEnabled | Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsDropEnabled
item.setFlags(flags)
return item
model = Model()
model.appendRow([_create_item('Item 1')])
model.appendRow([_create_item('Item 2')])
app = QApplication([])
view = QTreeView()
view.setDragDropMode(view.InternalMove)
view.setModel(model)
view.show()
app.exec_()
From studying the QStandardItemModel source code, I realized that one has to override QAbstractItemModel.dropMimeData, in order to react to item drops. QStandardItemModel moves items around in its implementation of dropMimeData, but in a way that a subclass can't hook into, so you have to handle dropMimeData yourself.
This is my dropMimeData based solution, item representations are included with MIME data as Python pickles:
import pickle
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
_mimeType = 'application/x-standarditemmodeldatalist'
class Model(QStandardItemModel):
def dropMimeData(
self, data, action, row, column, parent):
# Access parent before calling super implementation, because it may mutate parent
dest_str = 'parent {}'.format(parent.data(Qt.DisplayRole)) if \
parent.isValid() else 'root'
ret_val = super().dropMimeData(data, action, row, column, parent)
if action != Qt.MoveAction or not data.hasFormat(_mimeType):
return ret_val
item_data = pickle.loads(data.data(_mimeType))[0]
print('Moving {} to {}'.format(item_data, dest_str))
return True
# Override in order to add custom MIME data
def mimeData(self, indexes):
mimeData = super().mimeData(indexes)
data = [index.data(Qt.DisplayRole) for index in indexes]
mimeData.setData(_mimeType, pickle.dumps(data))
return mimeData
# Override in order to announce our custom MIME type
def mimeTypes(self):
return super().mimeTypes() + [_mimeType]
def _create_item(text):
item = QStandardItem(text)
flags = Qt.ItemIsDragEnabled | Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsDropEnabled
item.setFlags(flags)
return item
model = Model()
model.appendRow([_create_item('Item 1')])
model.appendRow([_create_item('Item 2')])
app = QApplication([])
view = QTreeView()
view.setDragDropMode(view.InternalMove)
view.setModel(model)
view.show()
app.exec_()
So I have a QTableView and I only want to let column sorting on column 1 but not column2.
Naturally I tried to installEventFilter on QHeaderView or QTableView, but MouseButtonPress event is not being passed unless you installEventFilter on QApplication
Now if when eventFilter is called, the target object is always the top level widget although event.pos() is actually relative to the header or tablecell depending on where you click.
So we cannot use QHeaderView.rect().contains(event.pos()) to find out if the user clicks on the header because you get false positive when you click on the top edge of the very first table cell.
You can still however calculate this using globalPos but then your eventFilter's logic has to change when you change layout or add more widgets above the tableview.
I believe it is a bug that event.pos() returns the relative pos even the object argument always refer to the same top level widget.
A more logical API would be that there is a event.target() method to return the target where it calculates the relative pos.
But I don't see a target() method or a way to find the target in this event filter.
Maybe I'm missing something?
# -*- coding: utf-8 -*-
# pyqt windows 4.10.3
# python 2.7.5 32 bits
from PyQt4.QtCore import *
from PyQt4.QtGui import *
app = None
tableHeader = None
class MyModel(QAbstractTableModel):
def rowCount(self, QModelIndex_parent=None, *args, **kwargs):
return 2
def columnCount(self, QModelIndex_parent=None, *args, **kwargs):
return 2
def data(self, modelIndex, role=None):
if modelIndex.isValid():
row = modelIndex.row()
col = modelIndex.column()
if role == Qt.DisplayRole:
return "%02d,%02d" % (row, col)
def flags(self, index):
if index.isValid():
return Qt.ItemIsEnabled
def headerData(self, section, Qt_Orientation, role=None):
if role == Qt.DisplayRole and Qt_Orientation == Qt.Horizontal:
return "Column " + str(section+1)
class MyEventFilter(QObject):
def eventFilter(self, object, event):
if event.type() == QEvent.MouseButtonPress:
# object is always app/top level widget
print 'MouseButtonPress target :' + repr(object)
# even though event.pos() gives pos relative to the header when you click on header,
# and pos relative to table cells when you click on table cell
print repr(event.pos())
# however we can get the mouse's global position
print repr(event.globalPos())
# given the top level widget's geometry
print repr(app.activeWindow().geometry())
# and the table header's left, top and height
print repr(tableHeader.rect())
# we can find out whether mouse click is targeted at the header
print repr(event.globalPos().y() - app.activeWindow().geometry().y())
# BUT WHAT IF THE LAYOUT CHANGE OR WE ADD MORE WIDGETS ABOVE THE TABLEVIEW?
# WE HAVE TO ADJUST THE CALCULATION ABOVE!
return False
if __name__ == "__main__":
import sys
app = QApplication(sys.argv)
w = QMainWindow()
t = QTableView()
tableHeader = t.horizontalHeader()
t.setModel(MyModel())
w.setCentralWidget(t)
ef = MyEventFilter()
# installing in QMainWindow or QTableView won't catch MouseButtonPress
# https://qt-project.org/forums/viewthread/9347
#w.installEventFilter(ef)
#t.installEventFilter(ef)
app.installEventFilter(ef)
w.show()
sys.exit(app.exec_())
There's a much easier solution: reimplement the sort method of the model, and only permit sorting for the appropriate column.
Also, as an added refinement, use the sortIndicatorChanged signal of the header to restore the current sort indicator when appropriate.
Here's a demo script:
from PyQt4 import QtGui, QtCore
class TableModel(QtGui.QStandardItemModel):
_sort_order = QtCore.Qt.AscendingOrder
def sortOrder(self):
return self._sort_order
def sort(self, column, order):
if column == 0:
self._sort_order = order
QtGui.QStandardItemModel.sort(self, column, order)
class Window(QtGui.QWidget):
def __init__(self, rows, columns):
QtGui.QWidget.__init__(self)
self.table = QtGui.QTableView(self)
model = TableModel(rows, columns, self.table)
for row in range(rows):
for column in range(columns):
item = QtGui.QStandardItem('(%d, %d)' % (row, column))
item.setTextAlignment(QtCore.Qt.AlignCenter)
model.setItem(row, column, item)
self.table.setModel(model)
self.table.setSortingEnabled(True)
self.table.horizontalHeader().sortIndicatorChanged.connect(
self.handleSortIndicatorChanged)
layout = QtGui.QVBoxLayout(self)
layout.addWidget(self.table)
def handleSortIndicatorChanged(self, index, order):
if index != 0:
self.table.horizontalHeader().setSortIndicator(
0, self.table.model().sortOrder())
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
window = Window(5, 5)
window.show()
window.setGeometry(600, 300, 600, 250)
sys.exit(app.exec_())
when the contents of the QFileSystemModel is displayed in a QTableView , the alignment of the text in the first row header section is right-aligned ,while the others is left-aligned,I wonder why ?
how to make the alignment of the text in each header section to be left-aligned?
setDefaultSectionSize() seems doesn't work here
my code
import sys
from PyQt4.QtGui import *
from PyQt4.QtCore import *
if __name__ == '__main__':
app =QApplication(sys.argv)
ui =QMainWindow()
model= QFileSystemModel ()
model.setRootPath(QDir.currentPath())
model.sort(3)
table = QTableView()
#print(table.verticalHeader().defaultAlignment()) #
table.verticalHeader().setDefaultAlignment(Qt.AlignRight)
table.setModel(model);
table.setRootIndex(model.index(QDir.currentPath())) #
ui.setCentralWidget(table)
ui.resize(800, 600)
ui.show()
app.exec_()
I'm using the QFileSystemModel in my own code and was surprised to see you get this strange behaviour. Then I dug deeper and saw that I had actually subclassed the QFileSystemModel and overridden the headerData method.
It seems that when role is Qt.DecorationRole and section==0 the default headerData function returns a QImage which messes things up. Also, setDefaultAlignment doesn't seem to actually set the default alignment.
In your case the problem will go away if you use the class given below. You can specify the alignment in the constructor to MyFileSystemModel (e.g. model= MyFileSystemModel(h_align = Qt.AlignRight))
class MyFileSystemModel(QFileSystemModel):
def __init__(self, h_align = Qt.AlignLeft, v_align = Qt.AlignLeft, parent = None):
super(MyFileSystemModel, self).__init__(parent)
self.alignments = {Qt.Horizontal:h_align, Qt.Vertical:v_align}
def headerData(self, section, orientation, role):
if role==Qt.TextAlignmentRole:
return self.alignments[orientation]
elif role == Qt.DecorationRole:
return None
else:
return QFileSystemModel.headerData(self, section, orientation, role)
I have an app with hundreds of custom buttons, each one needs multiple signal connections. The connect calls seem to be pretty slow, so I'm trying to connect/disconnect each button's signals via the main window's eventFilter using the enter and leave events.
However, sometimes those events seems to be called multiple times, causing RuntimeErrors (when trying to disconnect an event that is already gone).
Here is a snippet of code that shows a similar (and hopefully related) problem using default PushButtons.
To see the runtime error here, run the code, push one of the buttons, then close the window. That's when I see this:
RuntimeError: Fail to disconnect signal clicked().
Here is the code. Does anybody know if this is a PySide bug?
from PySide.QtGui import *
from PySide.QtCore import *
import sys
class TestWindow( QWidget ):
def __init__( self, parent=None ):
super( TestWindow, self ).__init__( parent )
self.setLayout( QGridLayout() )
def addWidget( self, w ):
self.layout().addWidget( w )
def testCB( self ):
print 'button connected'
def eventFilter( self, obj, event ):
'''Connect signals on mouse over'''
if event.type() == QEvent.Enter:
print 'enter',
obj.clicked.connect( self.testCB )
elif event.type() == QEvent.Leave:
print 'leave'
obj.clicked.disconnect( self.testCB )
return False
app = QApplication( sys.argv )
w = TestWindow()
for i in xrange(10):
btn = QPushButton( 'test %s' % i )
w.addWidget( btn )
btn.installEventFilter(w)
w.show()
sys.exit( app.exec_() )
In few cases, when I tested mouse events, showed better performance while events are attached to item class... so don't subclass. Rather :
class Button(QPushButton):
def __init__(self, label):
super(Button, self).__init__()
self.setText(label)
app = QApplication( sys.argv )
w = TestWindow()
for i in xrange(10):
btn = Button( 'test %s' % i )
w.addWidget( btn )
...then define mouse event for class.