I have a subclassed QAbstractTableModel
to display the data in the Table View in full size without scrolling I have turned of the Scrollbars
to get ridd of the white space around the Table View I have set the vertical/horizontal table length to a specific value.
Problem is that
I have added a add/deleate row Method to the Model so the Table View expands/shrinks now
to adjust the Table View behavior to display the data in full size and without white space I have set the horizontal Header to
table_view.horizontalHeader().setStretchLastSection(True)
which cuts off the white space in horicontal direction correctly
the same operation for the vertical header cuts of the white space too but does over strech the last row
I tryed to set each row to a default size with
table_view.verticalHeader().setSectionResizeMode(qtw.QHeaderView.Fixed)
table_view.verticalHeader().setDefaultSectionSize(40)
but this turns the white space on again
In short form: Im looking for a way to display the model data in the Table View in Full Size without white Space while beeing able to deleate/insert a row
code example
#!/usr/bin/env python
"""
"""
import sys
import re
from PyQt5 import QtWidgets as qtw
from PyQt5 import QtCore as qtc
from PyQt5.QtCore import Qt
from PyQt5 import QtGui as qtg
class ViewModel(qtc.QAbstractTableModel):
def __init__(self, input_data=None):
super().__init__()
self.input_data = input_data or [["data","data","data","data"],["data","data","data","data"]]
#
def data(self, index, role): # parameter index, role are needed !
"""
"""
if role == qtc.Qt.DisplayRole:
try:
text = self.input_data[index.row()][index.column()]
except IndexError:
text = None
return text
def rowCount(self, index=qtc.QModelIndex()):
return 0 if index.isValid() else len(self.input_data)
def columnCount(self, index):
return len(self.input_data[0])
def insertRows(self, position, rows, parent=qtc.QModelIndex()):
print(position) # -1
position = (position + self.rowCount()) if position < 0 else position
start = position
end = position + rows - 1
if end <= 8:
self.beginInsertRows(parent, start, end)
self.input_data.append([])
self.endInsertRows()
return True
else:
return False
def removeRows(self, position, rows, parent=qtc.QModelIndex()):
position = (position + self.rowCount()) if position < 0 else position
start = position
end = position + rows - 1
if end >= 1:
self.beginRemoveRows(parent, start, end)
del self.input_data[start:end + 1]
self.endRemoveRows()
return True
else:
return False
def headerData(self, section, orientation, role):
if role == qtc.Qt.DisplayRole:
if orientation == qtc.Qt.Horizontal:
return "hight " + str(section+1) + " /mm"
if orientation == qtc.Qt.Vertical:
return "width " + str(section+1)
def flags(self, index):
return qtc.Qt.ItemIsEditable | qtc.Qt.ItemIsSelectable | qtc.Qt.ItemIsEnabled
def setData(self, index, value, role=qtc.Qt.EditRole):
if role == qtc.Qt.EditRole:
try:
row = index.row()
column = index.column()
pattern = '^[\d]+(?:,[\d]+)?$'
if re.fullmatch(pattern, value, flags=0):
print("true")
self.input_data[row][column] = value # float
else:
print("nope")
pass
return True
except ValueError:
print("not a number")
return False
def display_model_data(self):
print(self.input_data)
class MainWindow(qtw.QWidget):
def __init__(self):
super().__init__()
# geometry
self.setGeometry(900, 360, 700, 800)
# View
table_view = qtw.QTableView()
# done # turn scroll bars off
table_view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
table_view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.model = ViewModel()
table_view.setModel(self.model)
table_view.horizontalHeader().setStretchLastSection(True)
# table_view.verticalHeader().setStretchLastSection(True)
table_view.verticalHeader().setSectionResizeMode(qtw.QHeaderView.Fixed)
table_view.verticalHeader().setDefaultSectionSize(24)
table_view.verticalHeader().setStretchLastSection(True)
# verticalHeader->setSectionResizeMode(QHeaderView::Fixed);
# verticalHeader->setDefaultSectionSize(24);
# widgets
self.insert_row_button = qtw.QPushButton("insert row")
self.deleate_row_button = qtw.QPushButton("deleate row")
# layout
layout = qtw.QVBoxLayout()
layout.addWidget(table_view)
layout.addWidget(self.insert_row_button)
layout.addWidget(self.deleate_row_button)
self.setLayout(layout)
self.show()
# function
self.insert_row_button.clicked.connect(lambda: self.model.insertRows(-1, 1))
self.deleate_row_button.clicked.connect(lambda: self.model.removeRows(-1, 1))
if __name__ == '__main__':
app = qtw.QApplication(sys.argv)
w = MainWindow()
sys.exit(app.exec_())
space can't disappear magically. let's say the total table height is 600. if there are two rows in the table, the first row is 40. then, the second one is 600 - 40 = 560 if you don't wan't blank at the bottom of the table. if you set height of each row to 40, the height of the blank space would be 600 - 2 * 40 = 520. you can't require (a total height 600) + (two rows, 40 for each) + (no blank space at the bottom).
So, let me guess, you want (a. no blank space at the bottom) + (b, space is evenly split into row, so that the last row won't look weird.). If that if the case, I've edited your code to below which explains everything:
"""
"""
import sys
import re
from PyQt5 import QtWidgets as qtw
from PyQt5 import QtCore as qtc
from PyQt5.QtCore import Qt
from PyQt5 import QtGui as qtg
class ViewModel(qtc.QAbstractTableModel):
def __init__(self, input_data=None):
super().__init__()
self.input_data = input_data or [["data","data","data","data"],["data","data","data","data"]]
#
def data(self, index, role): # parameter index, role are needed !
"""
"""
if role == qtc.Qt.DisplayRole:
try:
text = self.input_data[index.row()][index.column()]
except IndexError:
text = None
return text
def rowCount(self, index=qtc.QModelIndex()):
return 0 if index.isValid() else len(self.input_data)
def columnCount(self, index):
return len(self.input_data[0])
def insertRows(self, position, rows, parent=qtc.QModelIndex()):
print(position) # -1
position = (position + self.rowCount()) if position < 0 else position
start = position
end = position + rows - 1
if end <= 8:
self.beginInsertRows(parent, start, end)
self.input_data.append([])
self.endInsertRows()
return True
else:
return False
def removeRows(self, position, rows, parent=qtc.QModelIndex()):
position = (position + self.rowCount()) if position < 0 else position
start = position
end = position + rows - 1
if end >= 1:
self.beginRemoveRows(parent, start, end)
del self.input_data[start:end + 1]
self.endRemoveRows()
return True
else:
return False
def headerData(self, section, orientation, role):
if role == qtc.Qt.DisplayRole:
if orientation == qtc.Qt.Horizontal:
return "hight " + str(section+1) + " /mm"
if orientation == qtc.Qt.Vertical:
return "width " + str(section+1)
def flags(self, index):
return qtc.Qt.ItemIsEditable | qtc.Qt.ItemIsSelectable | qtc.Qt.ItemIsEnabled
def setData(self, index, value, role=qtc.Qt.EditRole):
if role == qtc.Qt.EditRole:
try:
row = index.row()
column = index.column()
pattern = '^[\d]+(?:,[\d]+)?$'
if re.fullmatch(pattern, value, flags=0):
print("true")
self.input_data[row][column] = value # float
else:
print("nope")
pass
return True
except ValueError:
print("not a number")
return False
def display_model_data(self):
print(self.input_data)
class NoBlankSpaceAtBottomEnvenlySplitTableView(qtw.QTableView):
def sizeHintForRow(self, row):
row_count = self.model().rowCount()
height = self.viewport().height()
row_height = int(height/row_count)
if row < row_count - 1:
return row_height
else:
return super().sizeHintForRow(row)
class MainWindow(qtw.QWidget):
def __init__(self):
super().__init__()
# geometry
self.setGeometry(900, 360, 700, 800)
# View
# table_view = qtw.QTableView()
table_view = NoBlankSpaceAtBottomEnvenlySplitTableView()
# done # turn scroll bars off
table_view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
table_view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.model = ViewModel()
table_view.setModel(self.model)
table_view.horizontalHeader().setStretchLastSection(True)
table_view.verticalHeader().setStretchLastSection(True)
# table_view.verticalHeader().setSectionResizeMode(qtw.QHeaderView.Fixed)
#table_view.verticalHeader().setDefaultSectionSize(24)
table_view.verticalHeader().setSectionResizeMode(
qtw.QHeaderView.ResizeToContents) # Add this line
table_view.verticalHeader().setStretchLastSection(True)
# verticalHeader->setSectionResizeMode(QHeaderView::Fixed);
# verticalHeader->setDefaultSectionSize(24);
# widgets
self.insert_row_button = qtw.QPushButton("insert row")
self.deleate_row_button = qtw.QPushButton("deleate row")
# layout
layout = qtw.QVBoxLayout()
layout.addWidget(table_view)
layout.addWidget(self.insert_row_button)
layout.addWidget(self.deleate_row_button)
self.setLayout(layout)
self.show()
# function
self.insert_row_button.clicked.connect(lambda: self.model.insertRows(-1, 1))
self.deleate_row_button.clicked.connect(lambda: self.model.removeRows(-1, 1))
if __name__ == '__main__':
app = qtw.QApplication(sys.argv)
w = MainWindow()
sys.exit(app.exec_())
Edit: Table auto adjusts its height according to rows
import sys
import re
from PyQt5 import QtWidgets as qtw
from PyQt5 import QtCore as qtc
from PyQt5.QtCore import Qt, QSize
from PyQt5.QtWidgets import QSizePolicy
from PyQt5 import QtGui as qtg
class ViewModel(qtc.QAbstractTableModel):
def __init__(self, input_data=None):
super().__init__()
self.input_data = input_data or [["data","data","data","data"],["data","data","data","data"]]
#
def data(self, index, role): # parameter index, role are needed !
"""
"""
if role == qtc.Qt.DisplayRole:
try:
text = self.input_data[index.row()][index.column()]
except IndexError:
text = None
return text
def rowCount(self, index=qtc.QModelIndex()):
return 0 if index.isValid() else len(self.input_data)
def columnCount(self, index):
return len(self.input_data[0])
def insertRows(self, position, rows, parent=qtc.QModelIndex()):
print(position) # -1
position = (position + self.rowCount()) if position < 0 else position
start = position
end = position + rows - 1
if end <= 8:
self.beginInsertRows(parent, start, end)
self.input_data.append([])
self.endInsertRows()
return True
else:
return False
def removeRows(self, position, rows, parent=qtc.QModelIndex()):
position = (position + self.rowCount()) if position < 0 else position
start = position
end = position + rows - 1
if end >= 1:
self.beginRemoveRows(parent, start, end)
del self.input_data[start:end + 1]
self.endRemoveRows()
return True
else:
return False
def headerData(self, section, orientation, role):
if role == qtc.Qt.DisplayRole:
if orientation == qtc.Qt.Horizontal:
return "hight " + str(section+1) + " /mm"
if orientation == qtc.Qt.Vertical:
return "width " + str(section+1)
def flags(self, index):
return qtc.Qt.ItemIsEditable | qtc.Qt.ItemIsSelectable | qtc.Qt.ItemIsEnabled
def setData(self, index, value, role=qtc.Qt.EditRole):
if role == qtc.Qt.EditRole:
try:
row = index.row()
column = index.column()
pattern = '^[\d]+(?:,[\d]+)?$'
if re.fullmatch(pattern, value, flags=0):
print("true")
self.input_data[row][column] = value # float
else:
print("nope")
pass
return True
except ValueError:
print("not a number")
return False
def display_model_data(self):
print(self.input_data)
class AutoExpandingTableView(qtw.QTableView):
# def sizeHintForRow(self, row):
# row_count = self.model().rowCount()
# height = self.viewport().height()
# row_height = int(height/row_count)
# if row < row_count - 1:
# return row_height
# else:
# return super().sizeHintForRow(row)
def sizeHint(self):
viewport_size_hint = self.viewportSizeHint()
return QSize(
self.width(),
viewport_size_hint.height()
)
class MainWindow(qtw.QWidget):
def __init__(self):
super().__init__()
# geometry
self.setGeometry(900, 360, 700, 800)
# View
# table_view = qtw.QTableView()
table_view = AutoExpandingTableView()
table_view.setSizePolicy(
QSizePolicy.Expanding,
QSizePolicy.Preferred
)
# done # turn scroll bars off
table_view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
table_view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.model = ViewModel()
table_view.setModel(self.model)
table_view.model().rowsInserted.connect(table_view.adjustSize)
table_view.model().rowsRemoved.connect(table_view.adjustSize)
table_view.horizontalHeader().setStretchLastSection(True)
# table_view.verticalHeader().setStretchLastSection(True)
# table_view.verticalHeader().setSectionResizeMode(qtw.QHeaderView.Fixed)
#table_view.verticalHeader().setDefaultSectionSize(24)
table_view.verticalHeader().setSectionResizeMode(
qtw.QHeaderView.ResizeToContents) # Add this line
# widgets
self.insert_row_button = qtw.QPushButton("insert row")
self.deleate_row_button = qtw.QPushButton("deleate row")
# layout
layout = qtw.QVBoxLayout()
layout.addWidget(table_view)
layout.addStretch()
layout.addWidget(self.insert_row_button)
layout.addWidget(self.deleate_row_button)
self.setLayout(layout)
self.show()
# function
self.insert_row_button.clicked.connect(lambda: self.model.insertRows(-1, 1))
self.deleate_row_button.clicked.connect(lambda: self.model.removeRows(-1, 1))
if __name__ == '__main__':
app = qtw.QApplication(sys.argv)
w = MainWindow()
sys.exit(app.exec_())
The most important aspect to consider is the sizeHint(), that is the recommended size a widget suggests to the layout that contains it.
Item views are tricky, though. They might have headers, their content could change many times during the lifespan of the program, and each item might have different sizes (which the user could interactively modify).
To achieve what you want, you have to use updateGeometry():
Notifies the layout system that this widget has changed and may need to change geometry.
Call this function if the sizeHint() or sizePolicy() have changed.
Note that calling adjustSize() is not suggested for this.
The size hint of an item view then must take into account the (visible) headers and the frame width, since all QAbstractItemView descendants inherit from QFrame.
Finally, to ensure that the size hint is dynamically adjusted and the layout system is notified about it, you should also connect all the correct signals that the model AND the header might send.
Note that, while you can connect all those signals externally, it is usually better to let the class itself take care of it internally.
class ExpandingTableView(qtw.QTableView):
shown = False
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
self.verticalHeader().sectionResized.connect(self.updateGeometry)
self.verticalHeader().sectionCountChanged.connect(self.updateGeometry)
def setVerticalHeader(self, header):
self.verticalHeader().sectionResized.disconnect(self.updateGeometry)
self.verticalHeader().sectionCountChanged.disconnect(self.updateGeometry)
super().setVerticalHeader(header)
header.sectionResized.connect(self.updateGeometry)
header.sectionCountChanged.connect(self.updateGeometry)
def setModel(self, model):
if self.model():
self.model().rowsInserted.disconnect(self.updateGeometry)
self.model().rowsRemoved.disconnect(self.updateGeometry)
super().setModel(model)
if model:
model.rowsInserted.connect(self.updateGeometry)
model.rowsRemoved.connect(self.updateGeometry)
self.updateGeometry()
# optional, if you want to ensure that a minimum height is always respected
def updateGeometry(self):
self.setMinimumHeight(min(self.sizeHint().height(),
self.verticalHeader().defaultSectionSize() * 8))
super().updateGeometry()
def sizeHint(self):
height = 0
if self.horizontalHeader().isVisible():
height += self.horizontalHeader().height()
height += self.verticalHeader().length() + self.frameWidth() * 2
return QSize(super().sizeHint().width(), height)
def showEvent(self, event):
super().showEvent(event)
# when the view is shown the first time it might not have computed the
# correct size hint, let's ensure that we notify the underlying
# layout manager(s)
if not self.shown:
self.shown = True
self.updateGeometry()
Related
I am using QTableView to display Pandas.Thank to the codes from StackOverFlow, I can do it.
I want to update the value of the cell on the right of a ComboBox when I check or uncheck some values of the ComboBox but I am new to PyQt5 and I don't know how to do.
When I select 'A' => the cell on the right will be 'A', then I check 'B' => the cell on the right will be 'A,B'. If I deselect A => the cell on the right will be 'B'
I have been searching but I cant not find a solution.
I really need your help.
This is what I am trying to do.
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtWidgets import QApplication, QWidget, QTableWidget, QComboBox, QVBoxLayout
from PyQt5.QtCore import Qt
dept_list = ['A','B','C','D','E','F','G','H']
class TableModel(QtCore.QAbstractTableModel):
def __init__(self, data):
super().__init__()
self._data = data
def data(self, index, role):
if role == Qt.DisplayRole or role == Qt.EditRole:
# See below for the nested-list data structure.
# .row() indexes into the outer list,
# .column() indexes into the sub-list
value = self._data.iloc[index.row(), index.column()]
return str(value)
def rowCount(self, index):
# The length of the outer list.
return self._data.shape[0]
def columnCount(self, index):
# The following takes the first sub-list, and returns
# the length (only works if all rows are an equal length)
return self._data.shape[1]
def flags(self, index):
return Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable
def setData(self, index, value, role):
if role == Qt.EditRole:
self._data.iloc[index.row(), index.column()] = value
self._data.iat[index.row(), self._data.shape[1]-1] = value
self.dataChanged.emit(index, index)
return True
def headerData(self, section, orientation, role):
if role == Qt.DisplayRole:
if orientation == Qt.Horizontal:
return str(self._data.columns[section])
if orientation == Qt.Vertical:
return str(self._data.index[section])
class CheckableComboBox(QComboBox):
def __init__(self):
super().__init__()
self._changed = False
self.view().pressed.connect(self.handleItemPressed)
self.checked_item=[]
for i in range(len(dept_list)):
self.addItem(dept_list[i])
self.setItemChecked(i, False)
def setItemChecked(self, index, checked=False):
item = self.model().item(index, self.modelColumn()) # QStandardItem object
if checked:
item.setCheckState(Qt.Checked)
else:
item.setCheckState(Qt.Unchecked)
def handleItemPressed(self, index):
item = self.model().itemFromIndex(index)
if item.checkState() == Qt.Checked:
item.setCheckState(Qt.Unchecked)
if item.text() in self.checked_item:
self.checked_item.remove(item.text())
print(self.checked_item)
else:
item.setCheckState(Qt.Checked)
self.checked_item.append(item.text())
print(self.checked_item)
self._changed = True
self.check_items()
def hidePopup(self):
if not self._changed:
super().hidePopup()
self._changed = False
def item_checked(self, index):
# getting item at index
item = self.model().item(index, 0)
# return true if checked else false
return item.checkState() == Qt.Checked
def check_items(self):
# blank list
checkedItems = []
# traversing the items
for i in range(self.count()):
# if item is checked add it to the list
if self.item_checked(i):
checkedItems.append(i)
# checkedItems.append(self.model().item(i, 0).text())
# call this method
self.update_labels(checkedItems)
# method to update the label
def update_labels(self, item_list):
n = ''
count = 0
# traversing the list
for i in item_list:
# if count value is 0 don't add comma
if count == 0:
n += ' % s' % i
# else value is greater then 0
# add comma
else:
n += ', % s' % i
# increment count
count += 1
# loop
for i in range(self.count()):
# getting label
text_label = self.model().item(i, 0).text()
# default state
if text_label.find('-') >= 0:
text_label = text_label.split('-')[0]
# shows the selected items
item_new_text_label = text_label + ',' + n
# setting text to combo box
self.setItemText(i, item_new_text_label)
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.table = QtWidgets.QTableView()
self.combo_box_list=[]
import pandas as pd
df = pd.read_excel(r"E:\092021-12 2022 1001.xlsx", skiprows=7)
# data = pd.DataFrame([[1, 9, 2], [1, 0, -1], [3, 5, 2], [3, 3, 2], [5, 8, 9],],
# columns=["A", "B", "C"],
# index=["Row 1", "Row 2", "Row 3", "Row 4", "Row 5"],)
df['Allocation Selection'] = ''
df['Allocation']=''
self.model = TableModel(df)
self.table.setModel(self.model)
# combo = CheckableComboBox()
# for i in range(len(dept_list)):
# combo.addItem(dept_list[i])
# combo.setItemChecked(i, False)
for i in range(df.shape[0]):
combo = CheckableComboBox()
self.combo_box_list.append(combo)
# pix = QPersistentModelIndex(index)
# combo.currentIndexChanged[str].connect(lambda txt, pix=pix: self.tableView.model().setData(QModelIndex(pix), txt))
self.table.setIndexWidget(self.model.index(i, df.shape[1] - 2), combo)
self.setCentralWidget(self.table)
self.setGeometry(600, 100, 400, 200)
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()
If I understand correctly, you are using QTableView to display a pandas dataframe. In this case it should be quite simple. You need to use the signal that comes from the QComboBox when an item is selected - probably currentIndexChanged(). You need to connect this to a function that will identify which items in your QComboBox are checked, and from this build the string that will go into the dataframe at the right place. Once the dataframe is updated, your QTableView should update to display the new cell of the dataframe. Without knowing more about your exact setup (the checkable combobox) I can't be more specific.
Edit: Here is a working solution. The first step was to make the combobox aware of what row it is in - in this case I just passed the row number as a variable to the combobox on creation. It's a quick and dirty way to do it - it would be better to find the row of the table that the widget is in dynamically. The second step is to define a custom signal which passes the row and the checked item list to a function in the main class which updates the dataframe at that row with the new checked list.
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtWidgets import QApplication, QWidget, QTableWidget, QComboBox, QVBoxLayout
from PyQt5.QtCore import Qt, pyqtSignal
dept_list = ['A','B','C','D','E','F','G','H']
class TableModel(QtCore.QAbstractTableModel):
def __init__(self, data):
super().__init__()
self._data = data
def data(self, index, role):
if role == Qt.DisplayRole or role == Qt.EditRole:
# See below for the nested-list data structure.
# .row() indexes into the outer list,
# .column() indexes into the sub-list
value = self._data.iloc[index.row(), index.column()]
return str(value)
def rowCount(self, index):
# The length of the outer list.
return self._data.shape[0]
def columnCount(self, index):
# The following takes the first sub-list, and returns
# the length (only works if all rows are an equal length)
return self._data.shape[1]
def flags(self, index):
return Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable
def setData(self, index, value, role):
if role == Qt.EditRole:
self._data.iloc[index.row(), index.column()] = value
self._data.iat[index.row(), self._data.shape[1]-1] = value
self.dataChanged.emit(index, index)
return True
def headerData(self, section, orientation, role):
if role == Qt.DisplayRole:
if orientation == Qt.Horizontal:
return str(self._data.columns[section])
if orientation == Qt.Vertical:
return str(self._data.index[section])
class CheckableComboBox(QComboBox):
check_changed = pyqtSignal(int, list)
def __init__(self, row):
super().__init__()
self._changed = False
self.view().pressed.connect(self.handleItemPressed)
self.checked_item=[]
self.row = row
for i in range(len(dept_list)):
self.addItem(dept_list[i])
self.setItemChecked(i, False)
def setItemChecked(self, index, checked=False):
item = self.model().item(index, self.modelColumn()) # QStandardItem object
if checked:
item.setCheckState(Qt.Checked)
else:
item.setCheckState(Qt.Unchecked)
def handleItemPressed(self, index):
item = self.model().itemFromIndex(index)
if item.checkState() == Qt.Checked:
item.setCheckState(Qt.Unchecked)
if item.text() in self.checked_item:
self.checked_item.remove(item.text())
print(self.checked_item)
else:
item.setCheckState(Qt.Checked)
self.checked_item.append(item.text())
print(self.checked_item)
self._changed = True
self.check_changed.emit(self.row, self.checked_item)
self.check_items()
def hidePopup(self):
if not self._changed:
super().hidePopup()
self._changed = False
def item_checked(self, index):
# getting item at index
item = self.model().item(index, 0)
# return true if checked else false
return item.checkState() == Qt.Checked
def check_items(self):
# blank list
checkedItems = []
# traversing the items
for i in range(self.count()):
# if item is checked add it to the list
if self.item_checked(i):
checkedItems.append(i)
# checkedItems.append(self.model().item(i, 0).text())
# call this method
self.update_labels(checkedItems)
# method to update the label
def update_labels(self, item_list):
n = ''
count = 0
# traversing the list
for i in item_list:
# if count value is 0 don't add comma
if count == 0:
n += ' % s' % i
# else value is greater then 0
# add comma
else:
n += ', % s' % i
# increment count
count += 1
# loop
for i in range(self.count()):
# getting label
text_label = self.model().item(i, 0).text()
# default state
if text_label.find('-') >= 0:
text_label = text_label.split('-')[0]
# shows the selected items
item_new_text_label = text_label + ',' + n
# setting text to combo box
self.setItemText(i, item_new_text_label)
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.table = QtWidgets.QTableView()
self.combo_box_list=[]
import pandas as pd
#df = pd.read_excel(r"E:\092021-12 2022 1001.xlsx", skiprows=7)
self.df = pd.DataFrame([[1, 9, 2], [1, 0, -1], [3, 5, 2], [3, 3, 2], [5, 8, 9],],
columns=["A", "B", "C"])
self.df['Allocation Selection'] = ''
self.df['Allocation']=''
self.model = TableModel(self.df)
self.table.setModel(self.model)
for i in range(self.df.shape[0]):
combo = CheckableComboBox(i)
combo.check_changed.connect(self.update_df)
self.combo_box_list.append(combo)
self.table.setIndexWidget(self.model.index(i, self.df.shape[1] - 2), combo)
self.setCentralWidget(self.table)
self.setGeometry(600, 100, 400, 200)
def update_df(self, row, checked):
print(row, checked)
self.df.at[row,'Allocation'] = checked
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()
Consider this example, modified from QStyledItemDelegate paint refresh issues :
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
class MyElement(object):
def __init__(self, numid):
self.numid = numid
self.strid = "Hello world {}".format(numid)
self.param = 'a' if numid%2==0 else 'b'
def __repr__(self):
return "(numid {}, strid '{}', param '{}')".format(self.numid, self.strid, self.param)
elements = [ MyElement(i) for i in range(20) ]
print(elements)
class ElementListModel(QtCore.QAbstractListModel):
def __init__(self, elements = [], parent = None):
super(ElementListModel, self).__init__()
self.__elements = elements
def rowCount(self, parent):
return len(self.__elements)
def data(self, index, role):
thiselement = self.__elements[index.row()]
if role == QtCore.Qt.DisplayRole:
return str( thiselement.strid )
elif role == QtCore.Qt.DecorationRole:
return QtGui.QColor(thiselement.numid*10,thiselement.numid,0)
class ElementThumbDelegate(QtWidgets.QStyledItemDelegate): #(QtGui.QStyledItemDelegate):
def __init__(self, view, parent=None):
super(ElementThumbDelegate, self).__init__(parent)
def paint(self, painter, options, index):
super(ElementThumbDelegate, self).paint(painter, options, index)
#painter.setRenderHint(QtGui.QPainter.Antialiasing)
#painter.setPen(QtGui.QColor(255, 255, 255))
#painter.setBrush(QtGui.QColor(10, 10, 10))
#painter.drawRect(options.rect)
#painter.drawText(options.rect, QtCore.Qt.AlignHCenter | QtCore.Qt.AlignVCenter, str(index.data()))
#def sizeHint(self, options, index):
# return QtCore.QSize(50, 50)
def main():
app = QtWidgets.QApplication(sys.argv)
viewer = QtWidgets.QListView()
viewModel = ElementListModel(elements)
viewer.setModel(viewModel)
#viewer.setViewMode(QtWidgets.QListView.IconMode)
viewer.setItemDelegate(ElementThumbDelegate(viewer))
viewer.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
It results with this:
Note that there is a box by default to the left of the item, which you can "color" via DecorationRole in the data method of the ListModel (and apparently, you can also store an icon there, if you return a QIcon instead of QColor, but I've never tried it).
My question is:
How can I draw a border around that icon space/box, depending on some property? In the example above, if MyElement.param == 'a' of a given element in the list, then I would want a light blue (RGB: (38, 76, 100), or #62c2ff) border of width 2 pixels drawn around the "icon space/box" - just like I've manually done in the mockup image in the circled area; otherwise I would not want a border
How could I additionally draw a single letter in the center of that space/box, depending on some property? For instance, if a MyElement.param == 'b' of a given element in the list, then I'd like the letter 'b' written in white in the middle of the "icon space/box" - otherwise, I would not want an extra text written in that space.
The paint() method of ElementThumbDelegate should have otherwise been enough of a pointer on how to do this; but if you uncomment it, you'll see the entire item is changed - not just the left icon box/space.
The class that performs the painting is the delegate who takes the information from the DecorationRole role to create the icon, so the solution is to create the custom icon depending on the item information. That creation can be done in the model or in the delegate, in this case I will use the second option but for this the item must be exposed through a custom role like Qt.UserRole:
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
class MyElement(object):
def __init__(self, numid):
self.numid = numid
self.strid = "Hello world {}".format(numid)
self.param = "a" if numid % 2 == 0 else "b"
def __repr__(self):
return "(numid {}, strid '{}', param '{}')".format(
self.numid, self.strid, self.param
)
elements = [MyElement(i) for i in range(20)]
class ElementListModel(QtCore.QAbstractListModel):
def __init__(self, elements=[], parent=None):
super(ElementListModel, self).__init__()
self.__elements = elements
def rowCount(self, parent=QtCore.QModelIndex()):
return len(self.__elements)
def data(self, index, role):
if not index.isValid() or not (0 <= index.row() < self.rowCount()):
return
thiselement = self.__elements[index.row()]
if role == QtCore.Qt.DisplayRole:
return str(thiselement.strid)
if role == QtCore.Qt.UserRole:
return thiselement
class ElementThumbDelegate(QtWidgets.QStyledItemDelegate):
def initStyleOption(self, option, index):
super().initStyleOption(option, index)
thiselement = index.data(QtCore.Qt.UserRole)
if isinstance(thiselement, MyElement):
if thiselement.param == "a":
option.features |= QtWidgets.QStyleOptionViewItem.HasDecoration
pixmap = QtGui.QPixmap(option.decorationSize)
pixmap.fill(QtGui.QColor("#62c2ff"))
painter = QtGui.QPainter(pixmap)
color = QtGui.QColor(thiselement.numid * 10, thiselement.numid, 0)
painter.fillRect(pixmap.rect().adjusted(2, 2, -2, -2), color)
painter.end()
option.icon = QtGui.QIcon(pixmap)
if thiselement.param == "b":
option.features |= QtWidgets.QStyleOptionViewItem.HasDecoration
pixmap = QtGui.QPixmap(option.decorationSize)
color = QtGui.QColor(thiselement.numid * 10, thiselement.numid, 0)
pixmap.fill(color)
painter = QtGui.QPainter(pixmap)
painter.setPen(QtGui.QColor("white"))
painter.drawText(pixmap.rect(), QtCore.Qt.AlignCenter, "b")
painter.end()
option.icon = QtGui.QIcon(pixmap)
def main():
app = QtWidgets.QApplication(sys.argv)
viewer = QtWidgets.QListView()
viewModel = ElementListModel(elements)
viewer.setModel(viewModel)
viewer.setItemDelegate(ElementThumbDelegate(viewer))
viewer.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
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_())
Using PySide 1.2
Grabbing the index of a clicked item in a QTableView returns a different result wether there is a horizontal header or not.
If there is a horizontal header, it's seems to be included in the computing of visualRect. So the first line, first column index is no longer 0, 0 but 1, 0.
Is this intended ? Because it is really confusing and inconvenient because the last line index will be invalid.
To reproduce :
import re
import operator
import os
import sys
# from Qt.QtCore import *
# from Qt.QtGui import *
# from Qt.QtWidgets import *
from PySide.QtCore import *
from PySide.QtGui import *
def main():
app = QApplication(sys.argv)
w = MyWindow()
w.show()
sys.exit(app.exec_())
class CustomTableView(QTableView):
"""
"""
def __init__(self, parent=None):
"""
"""
super(CustomTableView, self).__init__(parent=parent)
def mousePressEvent(self, mouse_event):
"""
"""
view_pos = self.mapFromGlobal(mouse_event.globalPos())
index = self.indexAt(view_pos)
print('mouse press index not cutting header : ' + str((index.row(), index.column())))
view_pos.setY(view_pos.y() - self.horizontalHeader().sizeHint().height())
index = self.indexAt(view_pos)
print('mouse press index cutting header : ' + str((index.row(), index.column())))
super(CustomTableView, self).mousePressEvent(mouse_event)
class MyWindow(QWidget):
def __init__(self, *args):
QWidget.__init__(self, *args)
# 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.popen4("dir c:\\")[1].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 = CustomTableView()
# set the table model
header = ['date', 'time', '', 'size', 'filename']
tm = MyTableModel(self.tabledata, header, self)
tv.setModel(tm)
return tv
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 None
elif role != Qt.DisplayRole:
return None
return self.arraydata[index.row()][index.column()]
def headerData(self, col, orientation, role):
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
return self.headerdata[col]
return None
if __name__ == "__main__":
main()
You do not have to use the global position and then pass it to the local position since you will get the position with respect to the upper corner of the QTableView, but indexAt requires the position with respect to the content according to the docs:
PySide.QtGui.QAbstractItemView.indexAt(point)
Parameters: point – PySide.QtCore.QPoint
Return type: PySide.QtCore.QModelIndex
Returns the model index of the item at the viewport coordinates point.
You must use the position of the event: mouse_event.pos()
def mousePressEvent(self, mouse_event):
index = self.indexAt(mouse_event.pos())
print('mouse press index: ' + str((index.row(), index.column())))
super(CustomTableView, self).mousePressEvent(mouse_event)
If you want to use the global position then you must convert it to a local position with respect to the viewport():
def mousePressEvent(self, mouse_event):
view_pos = self.viewport().mapFromGlobal(mouse_event.globalPos())
index = self.indexAt(view_pos)
print('mouse press index : ' + str((index.row(), index.column())))
super(CustomTableView, self).mousePressEvent(mouse_event)