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.
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 am new to Qt. Currently I am trying to learn how to update a table model from a different thread and then how to get an immediate display update for it. I read the documentation and found the dataChanged() and layoutChanged() signals. While dataChanged() works fine, any attempt to emit layoutChanged() fails with:
'QObject::connect: Cannot queue arguments of type 'QList<QPersistentModelIndex>' (Make sure 'QList<QPersistentModelIndex>' is registered using qRegisterMetaType().)
Searching for this particular error didn't give me anything that I could turn into working code. I am not using any QList or QPersistentModelIndex explicitly, but of course that can be implicitly used due to the constructs that I chose.
What am I doing wrong?
class TimedModel(QtCore.QAbstractTableModel):
def __init__(self, table, view):
super(TimedModel, self).__init__()
self.table = table
self.view = view
self.setHeaderData(0, Qt.Horizontal, Qt.AlignLeft, Qt.TextAlignmentRole)
self.rows = 6
self.columns = 4
self.step = 5
self.timer = Thread(
name = "Timer",
target = self.tableTimer,
daemon = True)
self.timer.start()
self.random = Random()
self.updated = set()
#staticmethod
def encode(row, column):
return row << 32 | column
def data(self, index, role):
if role == Qt.DisplayRole or role == Qt.EditRole:
return f'Data-{index.row()}-{index.column()}'
if role == Qt.ForegroundRole:
encoded = TimedModel.encode(index.row(), index.column())
return QBrush(Qt.red if encoded in self.updated else Qt.black)
return None
def rowCount(self, index):
return self.rows
def columnCount(self, index):
return self.columns
def headerData(self, col, orientation, role):
if orientation == Qt.Vertical:
# Vertical
return super().headerData(col, orientation, role)
# Horizontal
if not 0 <= col < self.columns:
return None
if role == Qt.DisplayRole:
return f'Data-{col}'
if role == Qt.TextAlignmentRole:
return int(Qt.AlignLeft | Qt.AlignVCenter)
return super().headerData(col, orientation, role)
def tableTimer(self):
while True:
time.sleep(5.0)
randomRow = self.random.randint(0, self.rows)
randomColumn = self.random.randint(0, self.columns)
encodedRandom = TimedModel.encode(randomRow, randomColumn)
if encodedRandom in self.updated:
self.updated.remove(encodedRandom)
else:
self.updated.add(encodedRandom)
updatedIndex = self.createIndex(randomRow, randomColumn)
self.dataChanged.emit(updatedIndex, updatedIndex)
'''this here does not work:'''
self.layoutAboutToBeChanged.emit()
self.rows += self.step
self.layoutChanged.emit()
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
QtWidgets.QMainWindow.__init__(self)
self.timedTable = QTableView()
self.model = TimedModel(self.timedTable, self)
self.timedTable.setModel(self.model)
headerView = self.timedTable.horizontalHeader()
headerView.setStretchLastSection(True)
self.setCentralWidget(self.timedTable)
self.setGeometry(300, 300, 1000, 600)
self.setWindowTitle('Timed Table')
self.show()
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
app.name = "Timed Table Application"
window = MainWindow()
window.show()
app.exec_()
The following code:
self.layoutAboutToBeChanged.emit()
self.rows += self.step
self.layoutChanged.emit()
create new model elements that have QPersistentModelIndex associated that are not thread-safe and that Qt monitors its creation to warn its misuse as in this case since modifying that element is unsafe since it implies modifying the GUI from another thread (Read here for more information).
So you see that message warning that what you are trying to do is unsafe.
Instead dataChanged only emits a signal, does not create any element belonging to Qt, and you have been lucky that the modification of "self.updated" has not generated bottlenecks since you modify a property that belongs to the main thread from a secondary thread without use guards as mutexes.
Qt points out that the GUI and the elements that the GUI uses should only be updated in the GUI thread, and if you want to modify the GUI with information from another thread, then you must send that information, for example, using the signals that are thread- safe:
import random
import sys
import threading
import time
from PySide2 import QtCore, QtGui, QtWidgets
class TimedModel(QtCore.QAbstractTableModel):
random_signal = QtCore.Signal(object)
def __init__(self, table, view):
super(TimedModel, self).__init__()
self.table = table
self.view = view
self.setHeaderData(
0, QtCore.Qt.Horizontal, QtCore.Qt.AlignLeft, QtCore.Qt.TextAlignmentRole
)
self.rows = 6
self.columns = 4
self.step = 5
self.updated = set()
self.random_signal.connect(self.random_slot)
self.timer = threading.Thread(name="Timer", target=self.tableTimer, daemon=True)
self.timer.start()
#staticmethod
def encode(row, column):
return row << 32 | column
def data(self, index, role):
if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole):
return f"Data-{index.row()}-{index.column()}"
if role == QtCore.Qt.ForegroundRole:
encoded = TimedModel.encode(index.row(), index.column())
return QtGui.QBrush(
QtCore.Qt.red if encoded in self.updated else QtCore.Qt.black
)
return None
def rowCount(self, index):
return self.rows
def columnCount(self, index):
return self.columns
def headerData(self, col, orientation, role):
if orientation == QtCore.Qt.Vertical:
# Vertical
return super().headerData(col, orientation, role)
# Horizontal
if not 0 <= col < self.columns:
return None
if role == QtCore.Qt.DisplayRole:
return f"Data-{col}"
if role == QtCore.Qt.TextAlignmentRole:
return QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter
return super().headerData(col, orientation, role)
def tableTimer(self):
while True:
time.sleep(5.0)
randomRow = random.randint(0, self.rows)
randomColumn = random.randint(0, self.columns)
encodedRandom = TimedModel.encode(randomRow, randomColumn)
self.random_signal.emit(encodedRandom)
#QtCore.Slot(object)
def random_slot(self, encodedRandom):
if encodedRandom in self.updated:
self.updated.remove(encodedRandom)
else:
self.updated.add(encodedRandom)
self.layoutAboutToBeChanged.emit()
self.rows += self.step
self.layoutChanged.emit()
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.timedTable = QtWidgets.QTableView()
self.model = TimedModel(self.timedTable, self)
self.timedTable.setModel(self.model)
headerView = self.timedTable.horizontalHeader()
headerView.setStretchLastSection(True)
self.setCentralWidget(self.timedTable)
self.setGeometry(300, 300, 1000, 600)
self.setWindowTitle("Timed Table")
self.show()
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
app.name = "Timed Table Application"
window = MainWindow()
window.show()
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_())
Having trouble trying to figure out how to catch a row in a QTableView that's being edited has been canceled. For example, if I am editing a newly inserted row in a QTableView and the ESC, up/down arrows keys have been pressed, I need to remove the row because (in my mind) has been cancelled. Also holds true if the user clicks away from the row. I can't really post any code as I have no idea how to implement something like this. Any ideas?
I have a an example of, what I believe, is what you want (at least for key pressed issue). From there you can do something similar for the clicking issue.
My solution uses a custom QItemDelegate that overrides the eventFilter method. Also it uses a naive model (because you want to use QTableView), the use of the layoutChanged signal on the model is due to functionality of the example, read the docs for more suitable add/delete data features according to your needs.
Hope it helps.
The sample ui:
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'test.ui',
# licensing of 'test.ui' applies.
#
# Created: Wed Nov 7 16:10:12 2018
# by: pyside2-uic running on PySide2 5.11.0
#
# WARNING! All changes made in this file will be lost!
from PySide2 import QtCore, QtGui, QtWidgets
class Ui_Test(object):
def setupUi(self, Test):
Test.setObjectName("Test")
Test.resize(538, 234)
self.horizontalLayout = QtWidgets.QHBoxLayout(Test)
self.horizontalLayout.setObjectName("horizontalLayout")
self.gridLayout = QtWidgets.QGridLayout()
self.gridLayout.setObjectName("gridLayout")
self.tableView = QtWidgets.QTableView(Test)
self.tableView.setObjectName("tableView")
self.gridLayout.addWidget(self.tableView, 0, 0, 1, 1)
self.addRow = QtWidgets.QPushButton(Test)
self.addRow.setObjectName("addRow")
self.gridLayout.addWidget(self.addRow, 0, 1, 1, 1)
self.horizontalLayout.addLayout(self.gridLayout)
self.retranslateUi(Test)
QtCore.QMetaObject.connectSlotsByName(Test)
def retranslateUi(self, Test):
Test.setWindowTitle(QtWidgets.QApplication.translate("Test", "Dialog", None, -1))
self.addRow.setText(QtWidgets.QApplication.translate("Test", "add row", None, -1))
The actual classes involved (I use PySide2):
from PySide2 import QtWidgets, QtCore, QtGui
from _test import Ui_Test
class MyDialog(QtWidgets.QDialog):
def __init__(self, parent = None):
super(MyDialog, self).__init__(parent = parent)
self.ui = Ui_Test()
self.ui.setupUi(self)
self._model = MyModel([["first row 1 col", "first row 2"],["second row 1", "second row 2"]])
self.ui.tableView.setModel(self._model)
self.ui.addRow.clicked.connect(self._model.addRow)
self.ui.tableView.setItemDelegate(MyDelegate(self.ui.tableView))
# this is crucial: we need to be sure that the selection is single on the view
self.ui.tableView.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectItems)
self.ui.tableView.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection)
class MyModel(QtCore.QAbstractTableModel):
def __init__(self, table_data = None, parent = None):
super(MyModel, self).__init__(parent = parent)
if not table_data: self._data = []
self._data = table_data
def rowCount(self, parent = None):
return len(self._data)
def columnCount(self, parent = None):
return 2
def addRow(self):
self._data.append(["new item", "new item"])
self.layoutChanged.emit()
def removeRow(self, row):
if 0 <= row < self.rowCount():
del self._data[row]
self.layoutChanged.emit()
def data(self, index, role = QtCore.Qt.DisplayRole):
if index.isValid():
if role == QtCore.Qt.DisplayRole:
row = index.row()
col = index.column()
return self._data[row][col]
def setData(self, index, value, role = QtCore.Qt.EditRole):
if index.isValid():
if role == QtCore.Qt.EditRole:
row = index.row()
col = index.column()
self._data[row][col] = str(value)
return True
else:
return False
else:
return False
def flags(self, index):
return QtCore.Qt.ItemIsSelectable|QtCore.Qt.ItemIsEditable|QtCore.Qt.ItemIsEnabled
class MyDelegate(QtWidgets.QItemDelegate):
def __init__(self, parent = None):
super(MyDelegate, self).__init__(parent)
self.view = parent
def eventFilter(self, editor, event):
# there is a lot of checking in order to identify the desired situation
# and avoid errors
if isinstance(event, QtGui.QKeyEvent):
if event.type() == QtCore.QEvent.KeyPress:
if event.key() == QtCore.Qt.Key_Escape:
# we should have a list here of length one (due to selection restrictions on the view)
index = self.view.selectedIndexes()
if index:
if index[0].isValid():
row = index[0].row()
self.view.model().removeRow(row)
return super(MyDelegate, self).eventFilter(editor, event)
if __name__ == '__main__':
app = QtWidgets.QApplication()
diag = MyDialog()
diag.show()
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())