The code below creates QTableView with a single column. How to make the header column stretch along the entire width of the QTableView view?
from PyQt4 import QtCore, QtGui
app=QtGui.QApplication(sys.argv)
class TableModel(QtCore.QAbstractTableModel):
def __init__(self):
QtCore.QAbstractTableModel.__init__(self)
def rowCount(self, parent=QtCore.QModelIndex()):
return 0
def columnCount(self, index=QtCore.QModelIndex()):
return 1
def headerData(self, column, orientation, role=QtCore.Qt.DisplayRole):
if role!=QtCore.Qt.DisplayRole: return QtCore.QVariant()
if orientation==QtCore.Qt.Horizontal: return QtCore.QVariant('Column Name')
class TableView(QtGui.QTableView):
def __init__(self):
super(TableView, self).__init__()
model=TableModel()
self.setModel(model)
self.show()
view=TableView()
sys.exit(app.exec_())
What you're looking for is the QHeaderView::setResizeMode function. I would recommend checking out the docs, but here's the code
self.horizontalHeader().setResizeMode(QtGui.QHeaderView.Stretch)
or, if you want to only stretch the least header item:
self.horizontalHeader().setStretchLastSection(True)
Related
I have a QTableView with a boolean value in a column, which is interpreted as a checkbox.
In PySide2 when I click on the checkbox, it correctly updates the UI and the stored value. But in PySide6 the same code sample will not work.
Here is the sample code that runs fine on PySide2 but breaks on PySide6:
from PySide6 import QtCore, QtWidgets
class TableModel(QtCore.QAbstractTableModel):
def __init__(self, parent) -> None:
super().__init__(parent)
self.model = False
def rowCount(self, _):
return 1
def columnCount(self, _):
return 1
def headerData(self, section, orientation, role):
if role != QtCore.Qt.ItemDataRole.DisplayRole:
return None
return "Test"
def flags(self, index):
return QtCore.Qt.ItemFlag.ItemIsEnabled | QtCore.Qt.ItemFlag.ItemIsUserCheckable
def data(self, index, role):
if role == QtCore.Qt.ItemDataRole.CheckStateRole:
print("Get data : ",self.model)
return QtCore.Qt.CheckState.Checked if self.model else QtCore.Qt.CheckState.Unchecked
def setData(self, index, value, role):
if role == QtCore.Qt.ItemDataRole.CheckStateRole:
self.model = bool(value)
print("Set data : ", self.model)
self.dataChanged.emit(index, index)
return True
return False
class MainWindow(QtWidgets.QMainWindow):
def __init__(self) -> None:
super().__init__()
self.table = QtWidgets.QTableView()
self.table_model = TableModel(self)
self.setCentralWidget(self.table)
self.table.setModel(self.table_model)
self.show()
if __name__ == "__main__":
app = QtWidgets.QApplication([])
conv = MainWindow()
app.exec_()
After clicking once on the checkbox, self.model is updated, but the UI does not change and keeps sending True when calling setData method.
I looked at the changes between PySide2 and PySide6 (Porting from PySide2 to PySide6) and there's nothing that could trigger this change of behaviour.
Edit :
This is a known bug (PYSIDE-1930) that should be fixed in version 6.3.2. Thanks to musicamente for pointing it out.
I would like to add a column of checkbox to my qtableview. I need the checkboxes to be in the center of column (i.e. aligned center). I have this example which works fine, BUT the checkboxes are aligned left.
import sys
import pandas as pd
from PyQt5 import QtCore, Qt
from PyQt5.QtCore import pyqtSlot
from PyQt5.QtWidgets import QCheckBox, QHBoxLayout, QItemDelegate, QTableView, QWidget, QApplication, QMainWindow
class MyCheckboxDelegate(QItemDelegate):
def __init__(self, parent):
QItemDelegate.__init__(self, parent)
def createEditor(self, parent, option, index):
check = QCheckBox(parent)
check.clicked.connect(self.currentIndexChanged)
return check
def setModelData(self, editor, model, index):
model.setData(index, editor.checkState())
#pyqtSlot()
def stateChanged(self):
self.commitData.emit(self.sender())
class TableModel(QtCore.QAbstractTableModel):
def __init__(self, data):
super().__init__()
print(data)
self._data = data
def rowCount(self, index=None):
return self._data.shape[0]
def columnCount(self, parnet=None):
return self._data.shape[1]
def data(self, index, role=QtCore.Qt.DisplayRole):
if index.isValid():
if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole:
if self._data.columns[index.column()]=='Delete':
return ''
value = self._data.iloc[index.row(), index.column()]
return str(value)
class MyWindow(QMainWindow):
def __init__(self, *args):
QWidget.__init__(self, *args)
table_model = TableModel(pd.DataFrame([['', ''], ['','']]))
self.table_view = QTableView()
self.table_view.setModel(table_model)
self.table_view.setItemDelegateForColumn(0, MyCheckboxDelegate(self))
for row in range(0, table_model.rowCount()):
self.table_view.openPersistentEditor(table_model.index(row, 0))
self.setCentralWidget(self.table_view)
app = QApplication(sys.argv)
window = MyWindow()
window.show()
sys.exit(app.exec_())
To force them to come at center, I create a widget, add a layout and add the check box to the layout. In other words, I change the createEditor function of MyCheckboxDelegate as follows:
def createEditor(self, parent, option, index):
w = QWidget(parent)
layout = QHBoxLayout(w)
check = QCheckBox(parent)
check.clicked.connect(self.currentIndexChanged)
check.setStyleSheet("color: red;")
layout.addWidget(check)
layout.setAlignment(Qt.AlignCenter)
return w
The problem is that now, the setModelData will not be called anymore. I need to access `model' after a checkbox is clicked.
Has anybody an idea how to fix it?
Item delegates are able to set model data as long as the editor has a user property, and a basic QWidget doesn't.
The solution is to create a QWidget subclass that implements that property, and connect the checkbox to a signal that will actually do the same as before:
class CenterCheckBox(QWidget):
toggled = pyqtSignal(bool)
def __init__(self, parent):
super().__init__(parent)
layout = QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
self.check = QCheckBox()
layout.addWidget(self.check, alignment=Qt.AlignCenter)
self.check.setFocusProxy(self)
self.check.toggled.connect(self.toggled)
# set a 0 spacing to avoid an empty margin due to the missing text
self.check.setStyleSheet('color: red; spacing: 0px;')
#pyqtProperty(bool, user=True) # note the user property parameter
def checkState(self):
return self.check.isChecked()
#checkState.setter
def checkState(self, state):
self.check.setChecked(state)
class MyCheckboxDelegate(QStyledItemDelegate):
def __init__(self, parent):
super().__init__(parent)
def createEditor(self, parent, option, index):
check = CenterCheckBox(parent)
check.toggled.connect(lambda: self.commitData.emit(check))
return check
def setModelData(self, editor, model, index):
model.setData(index, editor.checkState)
Note that:
it's usually better to use QStyledItemDelegate, which is more consistent with the overall application appearance;
you should always check the column (or row) before returning the editor and do the same (or at least check the editor) in setModelData(), or, alternatively, use setItemDelegateForColumn();
Here is a simple example of a qtableView where every cell have a different item. In the example here i used numbers, but ultimately i wish to display animated gifs for every cells. I wish to keep the MVC design and insert a custom widget proceduraly.
import random
import math
from PyQt4 import QtCore, QtGui
class TableModel(QtCore.QAbstractTableModel):
def __init__(self, data, columns, parent=None):
super(TableModel, self).__init__(parent)
self._columns = columns
self._data = data[:]
def rowCount(self, parent=QtCore.QModelIndex()):
if parent.isValid() or self._columns == 0:
return 0
return math.ceil(len(self._data )*1.0/self._columns)
def columnCount(self, parent=QtCore.QModelIndex()):
if parent.isValid():
return 0
return self._columns
def data(self, index, role=QtCore.Qt.DisplayRole):
if not index.isValid():
return
if role == QtCore.Qt.DisplayRole:
try:
value = self._data[ index.row() * self._columns + index.column() ]
return value
except:
pass
class Widget(QtGui.QWidget):
def __init__(self, parent=None):
QtGui.QWidget.__init__(self, parent)
data = [random.choice(range(10)) for i in range(20)]
l = QtGui.QHBoxLayout(self)
splitter = QtGui.QSplitter()
l.addWidget(splitter)
tvf = QtGui.QTableView()
model = TableModel(data, 3, self)
tvf.setModel(model)
splitter.addWidget( tvf )
if __name__=="__main__":
import sys
a=QtGui.QApplication(sys.argv)
w=Widget()
w.show()
sys.exit(a.exec_())
After reading a bit more and with the help of #eyllanesc, i understand that there is 2 ways of achieving this: delegates or itemWidget.
It seem that delegates won't work for me since it's not possible to insert widget into cells that way.
ItemWidget seems the way to go but this is not MVC compatible ? i do not wish to loop the function to insert into the table...
is there a third way ?
The code below creates a single QListView. An instance of MyClass (it is inherited from QAbstractListModel) is assigned to its self.setModel(self.model). If I click the view I can select the items in a list (so they do exist). But no names of the items displayed. How do I control how the QListView items are displayed?
from PyQt4 import QtCore, QtGui
app=QtGui.QApplication(sys.argv)
class MyClass(QtCore.QAbstractListModel):
def __init__(self):
super(MyClass, self).__init__()
self.elements={'Animals':['Bison','Panther','Elephant'],'Birds':['Duck','Hawk','Pigeon'],'Fish':['Shark','Salmon','Piranha']}
def rowCount(self, index=QtCore.QModelIndex()):
return len(self.elements)
def data(self, index, role=QtCore.Qt.DisplayRole):
print'MyClass.data():',index,role
class ListView(QtGui.QListView):
def __init__(self):
super(ListView, self).__init__()
self.model=MyClass()
self.setModel(self.model)
self.show()
window=ListView()
sys.exit(app.exec_())
I don't know how to use a dictionary for the elements, but using lists:
self.elements=['Bison','Panther','Elephant','Duck','Hawk','Pigeon','Shark','Salmon','Piranha']
You just have to return self.elements[index.row()] in the data() method. For instance:
class MyClass(QtCore.QAbstractListModel):
def __init__(self):
super(MyClass, self).__init__()
self.elements=['Bison','Panther','Elephant','Duck','Hawk','Pigeon','Shark','Salmon','Piranha']
def rowCount(self, index=QtCore.QModelIndex()):
return len(self.elements)
def data(self, index, role=QtCore.Qt.DisplayRole):
print'MyClass.data():',index,role
if index.isValid() and role == QtCore.Qt.DisplayRole:
return QtCore.QVariant(self.elements[index.row()])
else:
return QtCore.QVariant()
A code below create a window with QListView on a left side and QTableView on a right.
Using .setModel() QListView was assigned ListModel and QTableView was assigned TableModel.
On a window start-up only a List View gets populated with the items. A right-table-view gets populated only when left-side-list-view gets clicked.
Question: Why this code crashes? Is it because two models in use in the same time?
import sys, os
from PyQt4 import QtCore, QtGui
app=QtGui.QApplication(sys.argv)
elements={'Animals':{1:'Bison',2:'Panther',3:'Elephant'},'Birds':{1:'Duck',2:'Hawk',3:'Pigeon'},'Fish':{1:'Shark',2:'Salmon',3:'Piranha'}}
class ListModel(QtCore.QAbstractTableModel):
def __init__(self):
QtCore.QAbstractTableModel.__init__(self)
self.items=[]
def rowCount(self, parent=QtCore.QModelIndex()):
return len(self.items)
def columnCount(self, index=QtCore.QModelIndex()):
return 1
def data(self, index, role):
if not index.isValid() or not (0<=index.row()<len(self.items)): return QtCore.QVariant()
key=str(self.items[index.row()])
if role==QtCore.Qt.UserRole:
return key
if role==QtCore.Qt.DisplayRole:
return key
def addItem(self, key=None, column=0):
totalItems=self.rowCount()+1
self.beginInsertRows(QtCore.QModelIndex(), totalItems, column)
self.items.append(str(key))
self.endInsertRows()
def buildItems(self):
for key in elements:
self.addItem(key)
class TableModel(QtCore.QAbstractTableModel):
def __init__(self):
QtCore.QAbstractTableModel.__init__(self)
self.items=[]
def rowCount(self, parent=QtCore.QModelIndex()):
return len(self.items)
def columnCount(self, index=QtCore.QModelIndex()):
return 4
def data(self, index, role):
if not index.isValid() or not (0<=index.row()<len(self.items)): return QtCore.QVariant()
if role==QtCore.Qt.DisplayRole:
return key
def addItem(self, each=None, column=0):
totalItems=self.rowCount()+1
self.beginInsertRows(QtCore.QModelIndex(), totalItems, column)
self.items.append(str(each))
self.endInsertRows()
def rebuildItems(self, index):
key = index.data(QtCore.Qt.UserRole)
if not key: return
key=str(key.toString())
for each in elements[key]:
self.addItem(str(each))
class Window(QtGui.QWidget):
def __init__(self):
super(Window, self).__init__()
mainLayout=QtGui.QHBoxLayout()
self.setLayout(mainLayout)
self.dataModel=ListModel()
self.dataModel.buildItems()
self.dataModelB=TableModel()
self.viewA=QtGui.QListView()
self.viewA.setModel(self.dataModel)
self.viewA.clicked.connect(self.onClick)
self.viewB=QtGui.QTableView()
self.viewB.setModel(self.dataModelB)
mainLayout.addWidget(self.viewA)
mainLayout.addWidget(self.viewB)
self.show()
def onClick(self, index):
self.viewB.model().rebuildItems(index)
window=Window()
sys.exit(app.exec_())
EDITED LATER:
Below is a fixed code. In the original example the problem was caused by improper usage of .beginInsertRows() method. I mistakenly thought that the last argument to be supplied is a column number. But according to the documentation (thanks to three_pineapples for pointing out) the last argument should be the last row-number to be inserted.
import os,sys
from PyQt4 import QtCore, QtGui
app=QtGui.QApplication(sys.argv)
elements={'Animals':{1:'Bison',2:'Panther',3:'Elephant'},'Birds':{1:'Duck',2:'Hawk',3:'Pigeon'},'Fish':{1:'Shark',2:'Salmon',3:'Piranha'}}
class ListModel(QtCore.QAbstractTableModel):
def __init__(self):
QtCore.QAbstractTableModel.__init__(self)
self.items=[]
def rowCount(self, parent=QtCore.QModelIndex()):
return len(self.items)
def columnCount(self, index=QtCore.QModelIndex()):
return 1
def data(self, index, role):
if not index.isValid() or not (0<=index.row()<len(self.items)): return QtCore.QVariant()
key=str(self.items[index.row()])
if role==QtCore.Qt.UserRole:
return key
if role==QtCore.Qt.DisplayRole:
return key
def addItem(self, key=None):
self.beginInsertRows(QtCore.QModelIndex(), self.rowCount(), self.rowCount())
self.items.append(str(key))
self.endInsertRows()
def buildItems(self):
for key in elements:
self.addItem(key)
class TableModel(QtCore.QAbstractTableModel):
def __init__(self):
QtCore.QAbstractTableModel.__init__(self)
self.items=[]
def rowCount(self, parent=QtCore.QModelIndex()):
return len(self.items)
def columnCount(self, index=QtCore.QModelIndex()):
return 4
def data(self, index, role):
key=str(self.items[index.row()])
column=index.column()
if not index.isValid() or not (0<=index.row()<len(self.items)): return QtCore.QVariant()
if role==QtCore.Qt.DisplayRole:
if not column: return key
else:
print key, column, elements.get(key,{}).get(column)
return elements.get(key,{}).get(column)
def rebuildItems(self, index):
key=index.data(QtCore.Qt.UserRole).toString()
self.beginInsertRows(QtCore.QModelIndex(), self.rowCount(), self.rowCount())
self.items.append(key)
self.endInsertRows()
class Window(QtGui.QWidget):
def __init__(self):
super(Window, self).__init__()
mainLayout=QtGui.QHBoxLayout()
self.setLayout(mainLayout)
self.dataModel=ListModel()
self.dataModel.buildItems()
self.dataModelB=TableModel()
self.viewA=QtGui.QListView()
self.viewA.setModel(self.dataModel)
self.viewA.clicked.connect(self.onClick)
self.viewB=QtGui.QTableView()
self.viewB.setModel(self.dataModelB)
mainLayout.addWidget(self.viewA)
mainLayout.addWidget(self.viewB)
self.show()
def onClick(self, index):
self.viewB.model().rebuildItems(index)
window=Window()
sys.exit(app.exec_())
I can't reproduce your crash on PyQt v4.11.1, 32-bit Python 2.7, Windows 8.1.
However, your TableModel implementation is completely broken, so presumably that would explain why it crashes on your Mac?
Specifically:
The signature for beginInsertRows appears to be wrong. It doesn't follow the documentation here (linked to from the QAbstractTableModel page here). The signature is not beginInsertRows(parent, row, column) but rather beginInsertRows(parent, row, numRows).
The value for the row you are inserting to should be self.rowCount() as row indexing starts from 0. So when you have 0 items in your model, you insert to row 0 (the first row). When you have 1 item in your model, you insert into row 1 (the second row), etc.
The TableModel.data() method is broken. Specifically it appears to be missing the line key=str(self.items[index.row()])
My question would be, since you seem to be having trouble with models quite regularly (I feel like I've seen many questions on here from you in regards to implementing a custom model), why aren't you using the predefined Qt model QStandardItemModel which does all of the complex stuff for you? (You don't need to subclass it to use it)
If you want help translating the example you've posted above to using QStandardItemModel, please post a new question. I'm sure either I or someone else will answer it quickly.