Related
I'm using a custom Pandas Model(QAbstractTableModel) which is editable. I'm trying to add rows and then the user should be able to edit them and pass the values to the dataframe. Unfortunately, the example I found from PyQt5: Implement removeRows for pandas table model doesn't work when I add rows.
This is my model class:
class pandasModel(QAbstractTableModel):
def __init__(self, dataframe: pd.DataFrame, parent=None):
QAbstractTableModel.__init__(self, parent)
self._dataframe = dataframe
def rowCount(self, parent=QModelIndex()) -> int:
""" Override method from QAbstractTableModel
Return row count of the pandas DataFrame
"""
if parent == QModelIndex():
return len(self._dataframe)
return 0
def columnCount(self, parent=QModelIndex()) -> int:
"""Override method from QAbstractTableModel
Return column count of the pandas DataFrame
"""
if parent == QModelIndex():
return len(self._dataframe.columns)
return 0
def data(self, index: QModelIndex, role=Qt.ItemDataRole):
"""Override method from QAbstractTableModel
Return data cell from the pandas DataFrame
"""
if not index.isValid():
return None
if index.isValid():
if role == Qt.DisplayRole or role == Qt.EditRole:
try:
value = self._dataframe.iloc[index.row(), index.column()]
return str(value)
value=float(value)
except ValueError:
msg = QMessageBox()
msg.setIcon(QMessageBox.Critical)
msg.setText("Please, use integers or decimals.")
msg.setWindowTitle("Error")
msg.setStandardButtons(QMessageBox.Ok)
msg.exec_()
return 0
def headerData(
self, section: int, orientation: Qt.Orientation, role: Qt.ItemDataRole):
"""Override method from QAbstractTableModel
Return dataframe index as vertical header data and columns as horizontal header data.
"""
if role == Qt.DisplayRole:
if orientation == Qt.Horizontal:
return str(self._dataframe.columns[section])
if orientation == Qt.Vertical:
return str(self._dataframe.index[section])
return None
def setData(self, index, value, role):
if role == Qt.EditRole:
self._dataframe.iloc[index.row(), index.column()] = value
return True
return False
def flags(self, index):
return Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable
def removeRows(self, position, rows, parent=QModelIndex()):
start, end = position, position + rows - 1
if 0 <= start <= end and end < self.rowCount(parent):
self.beginRemoveRows(parent, start, end)
for index in range(start, end + 1):
self._dataframe.drop(index, inplace=True)
self._dataframe.reset_index(drop=True, inplace=True)
self.endRemoveRows()
return True
return False
def insertRows(self, position, rows, parent=QModelIndex()):
start, end = position, position + rows - 1
if 0 <= start <= end:
self.beginInsertRows(parent, start, end)
for index in range(start, end + 1):
default_row = [[None] for _ in range(self._dataframe.shape[1])]
new_df = pd.DataFrame(dict(zip(list(self._dataframe.columns), default_row)))
self._dataframe = pd.concat([self._dataframe, new_df])
self._dataframe = self._dataframe.reset_index(drop=True)
self.endInsertRows()
return True
return False
My global dataframe to be used:
regionalPop = {'Region':[1,1],'Starting_Index': [0,0], 'Ending_Index': [0,0]}
dfRegion=pd.DataFrame(data=regionalPop)
And the main class:
class regionalPlot(QtWidgets.QMainWindow,Ui_regionalWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setupUi(self)
#populating REGIONS table model
global dfRegion
regionalPop = {'Region':[1,1],'Starting_Index': [0,0], 'Ending_Index': [0,0]}
dfRegion=pd.DataFrame(data=regionalPop)
self.model1 = pandasModel(dfRegion)
#sorting enable
self.proxyModel = QSortFilterProxyModel()
self.proxyModel.setSourceModel(self.model1)
#binding model to table
self.tableView_2.setModel(self.proxyModel)
#-------------------------------------
#Button Actions
#add_button pressed
self.actionAdd_Region.triggered.connect(lambda: self.insert_row())
#minus button pressed
self.actionDelete_Region.triggered.connect(lambda: self.delete_row())
#update pressed
self.actionUpdate_Tables.triggered.connect(lambda: self.updateTable())
#ACTIONS
def delete_row(self):
if self.tableView_2.selectionModel().hasSelection():
indexes =[QPersistentModelIndex(index) for index in self.tableView_2.selectionModel().selectedRows()]
for index in indexes:
print('Deleting row %d...' % index.row())
self.model1.removeRow(index.row())
def insert_row(self):
global dfRegion
self.model1.insertRows(self.model1.rowCount(), 1)
print (dfRegion)
def updateTable(self):
#populating table model
self.model1 = pandasModel(dfRegion)
#sorting enable
self.proxyModel = QSortFilterProxyModel()
self.proxyModel.setSourceModel(self.model1)
#binding model to table
self.tableView_2.setModel(self.proxyModel)
And the Ui_regionalWindow:
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_regionalWindow(object):
def setupUi(self, regionalWindow):
regionalWindow.setObjectName("regionalWindow")
regionalWindow.resize(1184, 996)
self.centralwidget = QtWidgets.QWidget(regionalWindow)
self.centralwidget.setObjectName("centralwidget")
self.tableView_2 = QtWidgets.QTableView(self.centralwidget)
self.tableView_2.setGeometry(QtCore.QRect(10, 90, 256, 831))
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.tableView_2.sizePolicy().hasHeightForWidth())
self.tableView_2.setSizePolicy(sizePolicy)
self.tableView_2.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustIgnored)
self.tableView_2.setAlternatingRowColors(True)
self.tableView_2.setObjectName("tableView_2")
self.label_2 = QtWidgets.QLabel(self.centralwidget)
self.label_2.setGeometry(QtCore.QRect(10, 0, 241, 112))
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.label_2.sizePolicy().hasHeightForWidth())
self.label_2.setSizePolicy(sizePolicy)
font = QtGui.QFont()
font.setPointSize(14)
self.label_2.setFont(font)
self.label_2.setObjectName("label_2")
regionalWindow.setCentralWidget(self.centralwidget)
self.toolBar = QtWidgets.QToolBar(regionalWindow)
self.toolBar.setIconSize(QtCore.QSize(40, 40))
self.toolBar.setObjectName("toolBar")
regionalWindow.addToolBar(QtCore.Qt.TopToolBarArea, self.toolBar)
self.actionAdd_Region = QtWidgets.QAction(regionalWindow)
self.actionAdd_Region.setObjectName("actionAdd_Region")
self.actionDelete_Region = QtWidgets.QAction(regionalWindow)
self.actionDelete_Region.setObjectName("actionDelete_Region")
self.actionUpdate_Tables = QtWidgets.QAction(regionalWindow)
self.actionUpdate_Tables.setObjectName("actionUpdate_Tables")
self.toolBar.addAction(self.actionAdd_Region)
self.toolBar.addAction(self.actionDelete_Region)
self.toolBar.addSeparator()
self.toolBar.addAction(self.actionUpdate_Tables)
self.toolBar.addSeparator()
self.toolBar.addSeparator()
self.retranslateUi(regionalWindow)
QtCore.QMetaObject.connectSlotsByName(regionalWindow)
def retranslateUi(self, regionalWindow):
_translate = QtCore.QCoreApplication.translate
regionalWindow.setWindowTitle(_translate("regionalWindow", "Regional Analysis"))
self.label_2.setText(_translate("regionalWindow", "Regions"))
self.toolBar.setWindowTitle(_translate("regionalWindow", "toolBar"))
self.actionAdd_Region.setText(_translate("regionalWindow", "Add Region"))
self.actionAdd_Region.setToolTip(_translate("regionalWindow", "Add region of the signal. Left field is the start and right field is the end of the region of the signal."))
self.actionDelete_Region.setText(_translate("regionalWindow", "Delete Region"))
self.actionDelete_Region.setToolTip(_translate("regionalWindow", "Delete an existing region of the signal."))
self.actionUpdate_Tables.setText(_translate("regionalWindow", "Update Tables"))
self.actionUpdate_Tables.setToolTip(_translate("regionalWindow", "Click to update changes to the tabular data"))
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
regionalWindow = QtWidgets.QMainWindow()
ui = Ui_regionalWindow()
ui.setupUi(regionalWindow)
regionalWindow.show()
sys.exit(app.exec_())
I'm using three buttons one to add rows, one to remove rows, and one to update the table. Unfortunately, when I add rows to the dataframe it doesn't change anything and it keeps the same rows as defined before the editing, but when I trigger editing the values are changed.
To summarise:
I need to add rows to the dataframe "dfRegion" with this model. Even though I see a visual change in the GUI when I click self.actionAdd_Region, I print the dfRegion and there are no rows added in the dataframe.
I have a QTableView that has a column with True/False values. To change value in this column I use a QDataWidgetMapper checkBox.
I wish to change a value immediately after clicking a checkbox. Usually a change is made after checkbox looses focus (in other words you need to make extra click somewhere else in application).
I have tried to do it like this: checkBox.stateChanged.connect(lambda: data_mapper.submit()), but I don't like that using this method too many signals are emitted: first it emits signals that all mapped items were changed, and after checkbox looses focus - a signal that only one item was changed.
So the goal is to update model immediately after clicking a checkbox and have only one signal emitted.
Code:
from PyQt5 import QtCore, QtGui, QtWidgets
import sys
class Mainwindow(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self.table = QtWidgets.QTableView()
self.widget_1 = Widget_1(self)
self.layout_1 = QtWidgets.QHBoxLayout()
self.layout_1.addWidget(self.table)
self.layout_1.addWidget(self.widget_1)
self.setLayout(self.layout_1)
headers = ['State', 'Prorerty_1', 'Prorerty_2']
data = [
[True, '1', '2'],
[False, '3', '4']
]
self.model = ListModel(data, headers)
self.table.setModel(self.model)
self.table.setSelectionBehavior(self.table.SelectRows)
self.table.clicked.connect(self.set_data_mapper)
self.model.dataChanged.connect(lambda value: print(value.row(), value.column(), value.data()))
def set_data_mapper(self):
position = self.table.selectionModel().selectedIndexes()[0].row()
self.widget_1.data_mapper.setModel(self.model)
self.widget_1.data_mapper.addMapping(self.widget_1.checkBox, 0)
self.widget_1.data_mapper.addMapping(self.widget_1.lineEdit, 1)
self.widget_1.data_mapper.setCurrentIndex(position)
self.widget_1.checkBox.stateChanged.connect(lambda: self.widget_1.data_mapper.submit())
class Widget_1(QtWidgets.QWidget):
def __init__(self, parent):
super().__init__()
self.parent = parent
self.layout_1 = QtWidgets.QGridLayout()
self.state = QtWidgets.QLabel('State')
self.checkBox = QtWidgets.QCheckBox()
self.property = QtWidgets.QLabel('Prop.1')
self.lineEdit = QtWidgets.QLineEdit()
self.layout_1.addWidget(self.state, 0, 0)
self.layout_1.addWidget(self.checkBox, 0, 1)
self.layout_1.addWidget(self.property, 1, 0)
self.layout_1.addWidget(self.lineEdit, 1, 1)
self.setLayout(self.layout_1)
self.data_mapper = QtWidgets.QDataWidgetMapper()
class ListModel(QtCore.QAbstractTableModel):
def __init__(self, data_list = [[]], headers = [], parent = None):
super(ListModel, self).__init__()
self.data_list = data_list
self.headers = headers
def rowCount(self, parent):
return len(self.data_list)
def columnCount(self, parent):
return len(self.headers)
def data(self, index, role):
if role == QtCore.Qt.DisplayRole:
row = index.row()
column = index.column()
value = self.data_list[row][column]
return value
if role == QtCore.Qt.EditRole:
row = index.row()
column = index.column()
value = self.data_list[row][column]
return value
if role == QtCore.Qt.FontRole:
if index.column() == 0:
boldfont = QtGui.QFont()
boldfont.setBold(True)
return boldfont
def setData(self, index, value, role = QtCore.Qt.EditRole):
if role == QtCore.Qt.EditRole:
row = index.row()
column = index.column()
self.data_list[row][column] = value
self.dataChanged.emit(index, index)
return True
return False
def headerData(self, section, orientation, role):
if role == QtCore.Qt.DisplayRole:
if orientation == QtCore.Qt.Horizontal:
return self.headers[section]
if __name__ == '__main__':
app = QtWidgets.QApplication([])
application = Mainwindow()
application.show()
sys.exit(app.exec())
By default the QDataWidgetMapper policy is "AutoSubmit" which updates the model if any widget loses focus, so if you want it not to be emitted when it loses focus then you must use the "ManualSubmit" policy. On the other hand, the connection must be done only once since if you do it n times then n signals will be emitted and in your case "set_data_mapper is invoked every time the button is pressed.
class Mainwindow(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self.table = QtWidgets.QTableView(
selectionBehavior=QtWidgets.QAbstractItemView.SelectRows
)
self.widget_1 = Widget_1(self)
lay = QtWidgets.QHBoxLayout(self)
lay.addWidget(self.table)
lay.addWidget(self.widget_1)
headers = ["State", "Prorerty_1", "Prorerty_2"]
data = [[True, "1", "2"], [False, "3", "4"]]
self.model = ListModel(data, headers)
self.table.setModel(self.model)
self.table.setSelectionBehavior(self.table.SelectRows)
self.widget_1.data_mapper.setModel(self.model)
self.widget_1.data_mapper.addMapping(self.widget_1.checkBox, 0)
self.widget_1.data_mapper.addMapping(self.widget_1.lineEdit, 1)
self.widget_1.data_mapper.setSubmitPolicy(
QtWidgets.QDataWidgetMapper.ManualSubmit
)
self.widget_1.checkBox.stateChanged.connect(self.widget_1.data_mapper.submit)
self.widget_1.lineEdit.textChanged.connect(self.widget_1.data_mapper.submit)
self.table.clicked.connect(self.update_current_index)
self.model.dataChanged.connect(
lambda value: print(value.row(), value.column(), value.data())
)
def update_current_index(self, index):
position = index.row()
self.widget_1.data_mapper.setCurrentIndex(position)
I have figured out this problem: now only one signal is emitted immediately after change.
First, I used "ManualSubmit" policy for QDataWidgetMapper , as #eyllanesc told.
Second, I wrote my own custom function for both lineEdit and a checkBox instead data_mapper.submit().
Manual connection between mapped widgets and model is done once in 'for position in range(len(self.model.data_list))' loop.
Also, I want to pay attention that inside 'data_mapper_settings' function I used 'clicked' instead 'stateChanged' for checBox and 'textEdited' instead 'textChanged' for lineEdit to avoid emitting extra signals when other row is chosen.
Still I wonder if there's more elegant way to solve this problem
from PyQt5 import QtCore, QtGui, QtWidgets
import sys
class Mainwindow(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self.table = QtWidgets.QTableView()
self.widget_1 = Widget_1(self)
self.layout_1 = QtWidgets.QHBoxLayout()
self.layout_1.addWidget(self.table)
self.layout_1.addWidget(self.widget_1)
self.setLayout(self.layout_1)
headers = ['State', 'Prorerty_1', 'Prorerty_2']
data = [
[True, '1', '2'],
[False, '3', '4']
]
self.model = ListModel(data, headers)
self.table.setModel(self.model)
self.table.setSelectionBehavior(self.table.SelectRows)
self.table.clicked.connect(lambda index: self.set_data_mapper(index))
for position in range(len(self.model.data_list)):
self.data_mapper_settings(position)
self.model.dataChanged.connect(lambda index: print(index.row(), index.column(), index.data()))
def set_data_mapper(self, index):
position = index.row()
self.widget_1.data_mapper.setModel(self.model)
self.widget_1.data_mapper.addMapping(self.widget_1.checkBox, 0)
self.widget_1.data_mapper.addMapping(self.widget_1.lineEdit, 1)
self.widget_1.data_mapper.setCurrentIndex(position)
def data_mapper_settings(self, position):
self.widget_1.checkBox.clicked.connect(lambda value: self.submit_checkbox(value, position))
self.widget_1.lineEdit.textEdited.connect(lambda value: self.submit_lineEdit(value, position))
def submit_checkbox(self, value, position):
if position == self.table.selectionModel().selectedIndexes()[0].row():
self.model.setData(self.model.index(position, 0), value)
def submit_lineEdit(self, value, position):
if position == self.table.selectionModel().selectedIndexes()[0].row():
self.model.setData(self.model.index(position, 1), value)
class Widget_1(QtWidgets.QWidget):
def __init__(self, parent):
super().__init__()
self.parent = parent
self.layout_1 = QtWidgets.QGridLayout()
self.state = QtWidgets.QLabel('State')
self.checkBox = QtWidgets.QCheckBox()
self.property = QtWidgets.QLabel('Prop.1')
self.lineEdit = QtWidgets.QLineEdit()
self.layout_1.addWidget(self.state, 0, 0)
self.layout_1.addWidget(self.checkBox, 0, 1)
self.layout_1.addWidget(self.property, 1, 0)
self.layout_1.addWidget(self.lineEdit, 1, 1)
self.setLayout(self.layout_1)
self.data_mapper = QtWidgets.QDataWidgetMapper()
self.data_mapper.setSubmitPolicy(QtWidgets.QDataWidgetMapper.ManualSubmit)
class ListModel(QtCore.QAbstractTableModel):
def __init__(self, data_list = [[]], headers = [], parent = None):
super(ListModel, self).__init__()
self.data_list = data_list
self.headers = headers
def rowCount(self, parent):
return len(self.data_list)
def columnCount(self, parent):
return len(self.headers)
def data(self, index, role):
if role == QtCore.Qt.DisplayRole:
row = index.row()
column = index.column()
value = self.data_list[row][column]
return value
if role == QtCore.Qt.EditRole:
row = index.row()
column = index.column()
value = self.data_list[row][column]
return value
if role == QtCore.Qt.FontRole:
if index.column() == 0:
boldfont = QtGui.QFont()
boldfont.setBold(True)
return boldfont
def setData(self, index, value, role = QtCore.Qt.EditRole):
if role == QtCore.Qt.EditRole:
row = index.row()
column = index.column()
self.data_list[row][column] = value
self.dataChanged.emit(index, index)
return True
return False
def headerData(self, section, orientation, role):
if role == QtCore.Qt.DisplayRole:
if orientation == QtCore.Qt.Horizontal:
return self.headers[section]
def flags(self, index):
return QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsDragEnabled | QtCore.Qt.ItemIsDropEnabled | QtCore.Qt.ItemIsUserCheckable
if __name__ == '__main__':
app = QtWidgets.QApplication([])
application = Mainwindow()
application.show()
sys.exit(app.exec())
I'm using PyQt5 and would like to mark the line with a single click, that is, make it appear in a different color, e.g. in red.
After another click on the same line, the marking should be canceled again.
I have tried different approaches (how to see in my code marked as comments), but didn't find a solution.
class TableViewModel(QtCore.QAbstractTableModel):
def __init__(self, records, parent=None):
super().__init__(parent)
self.datatable = records
self.color_enabled = False
self.color_back = QtCore.Qt.magenta
self.data_changed = pyqtSignal(QtCore.QModelIndex,QtCore.QModelIndex)
def rowCount(self, parent=QtCore.QModelIndex):
return len(self.datatable)
def columnCount(self, parent=QtCore.QModelIndex):
return len(self.datatable[0]) if self.datatable else 0
def data(self, index, role, **kwargs): #role=QtCore.Qt.DisplayRole?
#reason = kwargs.get('r', None)
if index.isValid() and role == QtCore.Qt.DisplayRole:
return str(self.datatable[index.row()][index.column()])
if index.isValid() and role == QtCore.Qt.BackgroundRole and self.color_enabled:
return gui.QBrush(gui.QColor(255, 0, 43)) #red
def row_clicked(self, index, role = QtCore.Qt.BackgroundRole):
# numeric position of dataset
if index is not None:
row = index.row()
column = index.column()
if column == 0:
self.itemnumber = index.sibling(row, column)
self.itemname = index.sibling(row, column + 1)
self.itemsize = index.sibling(row, column + 2)
elif column == 1:
self.itemnumber = index.sibling(row, column - 1)
self.itemname = index.sibling(row, column)
self.itemsize = index.sibling(row, column + 1)
elif column == 2:
self.itemnumber = index.sibling(row, column - 2)
self.itemname = index.sibling(row, column - 1)
self.itemsize = index.sibling(row, column)
return (self.itemnumber, self.itemname, self.itemsize, index) #return objects!!! no real datas
#print("Ausgewählter Artikel: ",self.itemindex.data(), self.itemname.data(), self.itemsize.data())
def flags(self, index):
return QtCore.Qt.ItemIsEnabled
def headerData(self, section, orientation, role):
if role == QtCore.Qt.DisplayRole and orientation == QtCore.Qt.Horizontal:
return header_table_view[section]
return QtCore.QAbstractTableModel.headerData(self, section, orientation, role)
def setData(self, index, value, role=QtCore.Qt.DisplayRole):
if role == QtCore.Qt.DisplayRole:
row = index.row()
column = index.column()
color = gui.QColor(value)
#data = value
#self.datatable[row][column] = data
#self.dataChanged.emit(index, index)
return True
def changedData(self):
self.data_changed.emit(pyqtSignal("DataChanged(QModelIndex,QModelIndex)"), self.createIndex(0, 0), self.createIndex(self.rowCount(0)), self.columnCount(0))
def sort(self, column, order):
#self.emit(pyqtSignal("layoutAboutToBeChanged()"))
#self.layoutAboutToBeChanged.emit()
self.sortCol = column
self.sortOrder = order
try:
self.datatable = sorted(self.datatable, key = operator.itemgetter(column))
self.data_changed.emit(pyqtSignal("layoutChanged()"))
except:
print("keine Sortierung möglich")
#self.datatable.sort(key=itemgetter(column), reverse=order == QtCore.Qt.DescendingOrder)
def insertRow(self, item, index=QtCore.QModelIndex()):
""" Insert a row into the model. """
#self.beginInsertRows(QtCore.QModelIndex(), position, position + rows - 1)
TableViewModel.layoutAboutToBeChanged()
self.append(item)
TableViewModel.layoutChanged()
reply = QtWidgets.QMessageBox.information(self, "Meldung zum Artikelliste", "Artikel erfolgreich hinzugefügt!")
return True
Because you don't provide an MRE I will provide an example created from scratch. I will also assume that when you say line you mean row.
The strategy is to use a role that stores the state (a boolean) of the row, and then change that value when the row is pressed. According to that state a custom delegate will be used to change the backgroundBrush that will be used for painting the items.
import random
from PyQt5 import QtCore, QtGui, QtWidgets
StateRole = QtCore.Qt.UserRole + 1000
class BackgroundColorDelegate(QtWidgets.QStyledItemDelegate):
def initStyleOption(self, option, index):
super().initStyleOption(option, index)
print(index.data(StateRole))
if index.data(StateRole):
option.backgroundBrush = QtGui.QColor("red")
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.model = QtGui.QStandardItemModel()
for i in range(5):
for j in range(4):
value = random.randint(0, 10)
it = QtGui.QStandardItem()
it.setData(value, QtCore.Qt.DisplayRole)
self.model.setItem(i, j, it)
self.tableview = QtWidgets.QTableView()
self.tableview.setModel(self.model)
self.tableview.clicked.connect(self.on_clicked)
delegate = BackgroundColorDelegate(self.tableview)
self.tableview.setItemDelegate(delegate)
self.setCentralWidget(self.tableview)
#QtCore.pyqtSlot(QtCore.QModelIndex)
def on_clicked(self, index):
state = False
for i in range(self.model.columnCount()):
ix = self.model.index(index.row(), i)
value = ix.data(StateRole)
if value is not None:
state = value
break
for i in range(self.model.columnCount()):
ix = self.model.index(index.row(), i)
self.model.setData(ix, not state, StateRole)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())
I need a QTableWidget based on a QTabelModel and QTableView with some buttons added above the table. See the following figure:
The width of the QTableWidget should be adjusted so that it is not smaller than a reasonable minimum and not extend beyond the buttons above it; in particular, the size of the columns 1, 2, and 4 should be adjusted to their contents, and the 3rd column, Aberrations, should be expanded to fill in the gap on the right side. I'd like to know how to do this in code.
The following is a minimal example of the code I use for the custom QTableWidget (PyQt5, Python3):
from PyQt5 import QtGui, QtCore, QtWidgets
import numpy as np
#-- Table Model
class MyTableModel(QtCore.QAbstractTableModel):
def __init__(self, data, parent=None, *args):
super(MyTableModel, self).__init__(parent)
# table data
self.table_data = data
self.rows_nr, self.columns_nr = data.shape
# vertical & horizontal header labels
self.hheaders = ["Head-{}".format(i) for i in range(self.columns_nr)]
self.vheaders = ["Row-{}".format(i) for i in range(self.rows_nr)]
# nr of rows
def rowCount(self, parent):
return self.rows_nr
# nr of columns
def columnCount(self, parent):
return self.columns_nr
# row and column headers
def headerData(self, section, orientation, role):
if role == QtCore.Qt.DisplayRole:
if orientation == QtCore.Qt.Horizontal:
return self.hheaders[section]
#END if
#ELSE:
return QtCore.QVariant()
# display table contents
def data(self, index, role=QtCore.Qt.DisplayRole):
r_ = index.row()
c_ = index.column()
if role == QtCore.Qt.DisplayRole:
return "{}".format(data[r_, c_])
#ELSE:
return QtCore.QVariant()
# set data
def setData(self, index, value, role):
r_ = index.row()
c_ = index.column()
# editable fields
if role == QtCore.Qt.EditRole:
# interprete values
self.table_data[r_,c_] = str(value)
return True
# view/edit flags
def flags(self, index):
r_ = index.row()
c_ = index.column()
return QtCore.Qt.ItemIsEnabled
class MyTableWidget(QtWidgets.QWidget):
def __init__(self, data, *args):
super(MyTableWidget, self).__init__(*args)
#-- table model
tablemodel = MyTableModel(data=data, parent=self)
#-- table view
tableview = QtWidgets.QTableView()
tableview.setModel(tablemodel)
tableview.verticalHeader().hide() # hide vertical/row headers
# size policy
tableview.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents)
tableview.setSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum)
#-- layouts
#--- buttons
button_hlayout = QtWidgets.QHBoxLayout()
button_hlayout.addWidget(QtWidgets.QPushButton("Button 1"))
button_hlayout.addWidget(QtWidgets.QPushButton("Button 2"))
button_hlayout.addWidget(QtWidgets.QPushButton("Button 3"))
#--- table
table_layout = QtWidgets.QVBoxLayout()
table_layout.addLayout(button_hlayout)
table_layout.addWidget(tableview)
self.setLayout(table_layout)
#----------------------------------------
#-- produce sample data
data = np.empty(shape=(3,4), dtype=np.object)
for r in range(3):
for c in range(4):
data[r,c] = str(list(range((r+1) * (c+1))))
app = QtWidgets.QApplication([""])
w = MyTableWidget(data=data)
w.show()
app.exec_()
void QHeaderView::setSectionResizeMode(int logicalIndex, QHeaderView::ResizeMode mode)
Sets the constraints on how the section specified by logicalIndex in the header can be resized to those described by the given mode. The logical index should exist at the time this function is called.
from PyQt5 import QtGui, QtCore, QtWidgets
import numpy as np
#-- Table Model
class MyTableModel(QtCore.QAbstractTableModel):
def __init__(self, data, parent=None, *args):
super(MyTableModel, self).__init__(parent)
# table data
self.table_data = data
self.rows_nr, self.columns_nr = data.shape
# vertical & horizontal header labels
self.hheaders = ["Head-{}".format(i) for i in range(self.columns_nr)]
self.vheaders = ["Row-{}".format(i) for i in range(self.rows_nr)]
# nr of rows
def rowCount(self, parent):
return self.rows_nr
# nr of columns
def columnCount(self, parent):
return self.columns_nr
# row and column headers
def headerData(self, section, orientation, role):
if role == QtCore.Qt.DisplayRole:
if orientation == QtCore.Qt.Horizontal:
return self.hheaders[section]
#END if
#ELSE:
return QtCore.QVariant()
# display table contents
def data(self, index, role=QtCore.Qt.DisplayRole):
r_ = index.row()
c_ = index.column()
if role == QtCore.Qt.DisplayRole:
return "{}".format(data[r_, c_])
#ELSE:
return QtCore.QVariant()
# set data
def setData(self, index, value, role):
r_ = index.row()
c_ = index.column()
# editable fields
if role == QtCore.Qt.EditRole:
# interprete values
self.table_data[r_,c_] = str(value)
return True
# view/edit flags
def flags(self, index):
r_ = index.row()
c_ = index.column()
return QtCore.Qt.ItemIsEnabled
class MyTableWidget(QtWidgets.QWidget):
def __init__(self, data, *args):
super(MyTableWidget, self).__init__(*args)
#-- table model
tablemodel = MyTableModel(data=data, parent=self)
#-- table view
tableview = QtWidgets.QTableView()
tableview.setModel(tablemodel)
tableview.verticalHeader().hide() # hide vertical/row headers
#-- +++
tableview.setAlternatingRowColors(True)
tableview.horizontalHeader().setSectionResizeMode(QtWidgets.QHeaderView.Stretch)
tableview.horizontalHeader().setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents)
tableview.horizontalHeader().setSectionResizeMode(1, QtWidgets.QHeaderView.ResizeToContents)
tableview.horizontalHeader().setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeToContents)
# size policy
tableview.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents)
#tableview.setSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum) # ---
tableview.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)# +++
#-- layouts
#--- buttons
button_hlayout = QtWidgets.QHBoxLayout()
button_hlayout.addWidget(QtWidgets.QPushButton("Button 1"))
button_hlayout.addWidget(QtWidgets.QPushButton("Button 2"))
button_hlayout.addWidget(QtWidgets.QPushButton("Button 3"))
#--- table
table_layout = QtWidgets.QVBoxLayout()
table_layout.addLayout(button_hlayout)
table_layout.addWidget(tableview)
self.setLayout(table_layout)
#----------------------------------------
#-- produce sample data
data = np.empty(shape=(3,4), dtype=np.object)
for r in range(3):
for c in range(4):
data[r,c] = str(list(range((r+1) * (c+1))))
app = QtWidgets.QApplication([""])
w = MyTableWidget(data=data)
w.show()
app.exec_()
In the code above, tableview.horizontalHeader().SetSectionResizeMode(QtWidgets.QHeaderView.Stretch) applies the Stretch mode to all columns, and the remaining 3 operators set the corresponding columns to the ResizeToContents mode.
The resizing behaviour of the widget window is determined by setSizePolicy method. In this case, the policy can be also tableview.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum), which allows the user to enlarge or shrink the widget window.
I'm trying to create a custom TableModel class for QTableView. The cells containing 1 as data must have red outlining. Outlining is made by returning a pixmap (with red borders and text drawn on top) from TableModel instead of returning a simple string.
The problem is in the unexpected padding of the pixmap, which I return as DecorationRole. I checked if the pixmap is drawn correctly (and it actually is 21x21 px with well done outlining, without padding, just as planned) right before the return pixmap line.
Here is the right drawn pixmap, which was saved just before the return from TableModel:
Eventually, something shifts the returned pixmap by exactly 3px from the left border of the QTableView cell. I didn't set any padding in QtDesigner for the QTableView and didn't change it later in my code. I also tried to manually set up padding to zero using stylesheet, but it gave no different result.
Any ideas how to fix it? Thank you.
Here is the sample of my TableModel:
class TableModel(QtCore.QAbstractTableModel):
def __init__(self, topology=None):
super().__init__()
...
# Hardcode cell size and path to rectangle image
self.cell_width, self.cell_height = 21, 21
self.fpath_red_rect = './path/to/red_rect.png'
def rowCount(self, parent=QtCore.QModelIndex()):
return self.data.shape[0]
def columnCount(self, parent=QtCore.QModelIndex()):
return self.data.shape[1]
def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
...
def size(self):
return QtCore.QSize((self.columnCount() + 1) * self.cell_width,
(self.rowCount() + 1) * self.cell_height)
def data(self, index, role=QtCore.Qt.DisplayRole):
if not index.isValid():
return QtCore.QVariant()
i = index.row()
j = index.column()
if role == QtCore.Qt.DisplayRole:
if self.data[i, j] == 0: # empty
return ''
elif self.data[i, j] == 1: # cell with red rectangle
# the text will be drawn on pixmap manually later
return None
else:
return '{0}'.format(self.data[i, j]) # display default data
if role == QtCore.Qt.DecorationRole:
# Create pixmap, draw the rectangle on it and then draw text on top
pixmap = QtGui.QPixmap(self.cell_width, self.cell_height)
image = QtGui.QImage(self.fpath_red_rect).scaled(self.cell_width, self.cell_height)
painter = QtGui.QPainter(pixmap)
painter.drawImage(pixmap.rect().topLeft(), image)
painter.drawText(pixmap.rect(), QtCore.Qt.AlignCenter, '{0}'.format(self.data[i, j]))
painter.end()
# If we save the pixmap to PNG image here (see the link above),
# we get the expected 21 x 21 px image, with nice
# and properly drawn rectangle and centered text.
# But something goes wrong after returning
return pixmap
if role == QtCore.Qt.BackgroundRole:
return QtGui.QBrush(self.getQtColor(self.data[i, j]))
if role == QtCore.Qt.TextAlignmentRole:
return QtCore.Qt.AlignCenter
return QtCore.QVariant()
DecorationRole is used to draw the icon for that reason you observe the displacement, in your case you should not use that role, besides the painting task should not be done in the model since he only has to provide the data, if you want to modify the drawing a better option is use a delegate as I show below:
import sys
import numpy as np
from PyQt5 import QtCore, QtGui, QtWidgets
ValueRole = QtCore.Qt.UserRole + 1
max_val = 4
colors = [QtGui.QColor(*np.random.randint(255, size=3)) for i in range(max_val)]
class TableModel(QtCore.QAbstractTableModel):
def __init__(self, parent=None):
QtCore.QAbstractTableModel.__init__(self, parent)
self.data = np.random.randint(max_val, size=(10, 10))
def rowCount(self, parent=QtCore.QModelIndex()):
return self.data.shape[0]
def columnCount(self, parent=QtCore.QModelIndex()):
return self.data.shape[1]
def data(self, index, role=QtCore.Qt.DisplayRole):
if not index.isValid():
return QtCore.QVariant()
i = index.row()
j = index.column()
val = self.data[i, j]
if role == QtCore.Qt.DisplayRole:
return str(val)
elif role == QtCore.Qt.TextAlignmentRole:
return QtCore.Qt.AlignCenter
elif role == QtCore.Qt.BackgroundRole:
return colors[val]
if role == ValueRole:
return val
return QtCore.QVariant()
class Delegate(QtWidgets.QStyledItemDelegate):
def paint(self, painter, option, index):
QtWidgets.QStyledItemDelegate.paint(self, painter, option, index)
if index.data(ValueRole) == 1:
painter.save()
pen = painter.pen()
pen.setColor(QtCore.Qt.red)
painter.setPen(pen)
r = QtCore.QRect(option.rect)
r.adjust(0, 0, -pen.width(), -pen.width())
painter.drawRect(r)
painter.restore()
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
w = QtWidgets.QTableView()
w.setItemDelegate(Delegate(w))
model = TableModel()
w.setModel(model)
w.verticalHeader().setSectionResizeMode(QtWidgets.QHeaderView.Fixed)
w.horizontalHeader().setSectionResizeMode(QtWidgets.QHeaderView.Fixed)
for i in range(model.rowCount()):
w.verticalHeader().resizeSection(i, 21)
for j in range(model.columnCount()):
w.horizontalHeader().resizeSection(j, 21)
w.show()
sys.exit(app.exec_())