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.
Related
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'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_())
The code below creates a single QTableView linked to QAbstractTableModel with three columns:
To assign a horizontal red-blue gradient to the items in the last column I create a gradient with
gradient = QtGui.QLinearGradient(0, 0, COLUMN_WIDTH, 0)
In order to divide the gradient in half (one painted red and another blue) I need to supply
QLinearGradient with the exact COLUMN_WIDTH value.
How to get COLUMN_WIDTH?
from PyQt4 import QtCore, QtGui
app = QtGui.QApplication([])
class Model(QtCore.QAbstractTableModel):
def __init__(self, parent=None):
super(Model, self).__init__(parent)
self.items = [['Row%s Col%s'%(row,col) for col in range(3)] for row in range(5)]
def rowCount(self, parent=None):
return len(self.items)
def columnCount(self, parent=None):
return len(self.items[0])
def data(self, index, role=QtCore.Qt.DisplayRole):
row = index.row()
column = index.column()
if role == QtCore.Qt.DisplayRole:
if 0 <= row < self.rowCount() and 0 <= column < self.columnCount():
return self.items[row][column]
if role == QtCore.Qt.BackgroundRole and column==2:
COLUMN_WIDTH = 50
gradient = QtGui.QLinearGradient(0, 0, COLUMN_WIDTH, 0)
gradient.setColorAt(0.5, QtGui.QColor('red'))
gradient.setColorAt(0.5001, QtGui.QColor('blue'))
brush = QtGui.QBrush(gradient)
return brush
view = QtGui.QTableView()
model = Model(view)
view.setModel(model)
view.show()
app.exec_()
columnWidth() is a property of QTableView:
COLUMN_WIDTH = self.parent().columnWidth(index.column())
I want to sort a QTableView in PyQT5.
I found an example that uses PyQT4, but in PyQT5 SIGNALs are not existing anymore.
This is my example code
class MainWindow(QWidget):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
# create table
self.get_table_data()
table = self.createTable()
# layout
layout = QVBoxLayout()
layout.addWidget(table)
self.setLayout(layout)
def get_table_data(self):
stdouterr = os.popen("dir c:\\").read()
lines = stdouterr.splitlines()
lines = lines[5:]
lines = lines[:-2]
self.tabledata = [re.split(r"\s+", line, 4)
for line in lines]
def createTable(self):
# create the view
tv = QTableView()
# set the table model
header = ['date', 'time', '', 'size', 'filename']
tm = MyTableModel(self.tabledata, header, self)
tv.setModel(tm)
# set the minimum size
tv.setMinimumSize(400, 300)
# hide grid
tv.setShowGrid(False)
tv.setSelectionBehavior(QAbstractItemView.SelectRows)
# set the font
# hide vertical header
vh = tv.verticalHeader()
vh.setVisible(False)
# set horizontal header properties
hh = tv.horizontalHeader()
hh.setStretchLastSection(True)
# set column width to fit contents
tv.resizeColumnsToContents()
# set row height
nrows = len(self.tabledata)
for row in range(nrows):
tv.setRowHeight(row, 18)
# enable sorting
tv.setSortingEnabled(True)
return tv
self.setWindowTitle("Finance")
class MyTableModel(QAbstractTableModel):
def __init__(self, datain, headerdata, parent=None, *args):
""" datain: a list of lists
headerdata: a list of strings
"""
QAbstractTableModel.__init__(self, parent, *args)
self.arraydata = datain
self.headerdata = headerdata
def rowCount(self, parent):
return len(self.arraydata)
def columnCount(self, parent):
return len(self.arraydata[0])
def data(self, index, role):
if not index.isValid():
return QVariant()
elif role != Qt.DisplayRole:
return QVariant()
return QVariant(self.arraydata[index.row()][index.column()])
def headerData(self, col, orientation, role):
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
return QVariant(self.headerdata[col])
return QVariant()
def sort(self, Ncol, order):
"""Sort table by given column number.
"""
self.emit(SIGNAL("layoutAboutToBeChanged()"))
self.arraydata = sorted(self.arraydata, key=operator.itemgetter(Ncol))
if order == Qt.DescendingOrder:
self.arraydata.reverse()
self.emit(SIGNAL("layoutChanged()"))
if __name__ == '__main__':
from PyQt5.QtWidgets import QApplication
app = QApplication(sys.argv)
helloPythonWidget = MainWindow()
helloPythonWidget.show()
sys.exit(app.exec_())
I tried many different ways to use self.layoutAboutToBeChanged() and pyqtSignal, but to be honest, I don't understand it, since I'm new to python and PyQT in general yet.
I tried to get infos from the Documentation, but i didn't get a clue from documentation and didn't found an good example on the web.
UPDATE:
I solved the puzzle:
self.layoutAboutToBeChanged.emit() emits the signal (codecompletion in eclipse is a bit misleading)
def sort(self, Ncol, order):
"""Sort table by given column number.
"""
self.layoutAboutToBeChanged.emit()
self.arraydata = sorted(self.arraydata, key=operator.itemgetter(Ncol))
if order == Qt.DescendingOrder:
self.arraydata.reverse()
self.layoutChanged.emit()
This is the solution
for those of you guys who wants to import pandas dataframe into qt model try this:
class PandasModel(QtCore.QAbstractTableModel):
def __init__(self, data, parent=None):
"""
:param data: a pandas dataframe
:param parent:
"""
QtCore.QAbstractTableModel.__init__(self, parent)
self._data = data
# self.headerdata = data.columns
def rowCount(self, parent=None):
return len(self._data.values)
def columnCount(self, parent=None):
return self._data.columns.size
def data(self, index, role=QtCore.Qt.DisplayRole):
if index.isValid():
if role == QtCore.Qt.DisplayRole:
return str(self._data.values[index.row()][index.column()])
return None
def headerData(self, rowcol, orientation, role):
# print(self._data.columns[rowcol])
# print(self._data.index[rowcol])
if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
return self._data.columns[rowcol]
if orientation == QtCore.Qt.Vertical and role == QtCore.Qt.DisplayRole:
return self._data.index[rowcol]
return None
def flags(self, index):
flags = super(self.__class__, self).flags(index)
flags |= QtCore.Qt.ItemIsEditable
flags |= QtCore.Qt.ItemIsSelectable
flags |= QtCore.Qt.ItemIsEnabled
flags |= QtCore.Qt.ItemIsDragEnabled
flags |= QtCore.Qt.ItemIsDropEnabled
return flags
def sort(self, Ncol, order):
"""Sort table by given column number.
"""
try:
self.layoutAboutToBeChanged.emit()
self._data = self._data.sort_values(self._data.columns[Ncol], ascending=not order)
self.layoutChanged.emit()
except Exception as e:
print(e)
I have forgotten where I got the base pandasmodel, probably from here