Im using PyQt5 to develop an MVC application using sqlite DB.
I have a custom model inheriting from QAbstractTableModel. For the view im using in QTableView.
In the custom model i have added function setdata() and flags() needed to make the table cells in the view editable.
But when i edit the cells in the QtableView ,they do not persist in the sqlite DB.
When creating the view i use setModel() function to link the view and model.
What i want is a view with an editable table which on edit persists in the view and updates the sqlite database as well.
eg If i edit first row for result1 column from 1.23 to 1.234,althought the View now shows 1.234, the sqlite DB shows 1.23 instead of 1.234 :-(
#musicamante Below is my code so far
from PyQt5 import QtCore, QtGui,QtWidgets
from PyQt5.QtWidgets import (
QApplication,
QMainWindow,
)
import sys, sqlite3
import pandas as pd
from PyQt5.QtCore import Qt
class TestUI():
def __init__(self):
self.db = sqlite3.connect("test.db")
data = pd.read_sql_query("SELECT * FROM Reports",self.db) # where Reports is the table name, which i have already created beforehand
self.model = TableModel(data)
def createView(self, title):
view = QtWidgets.QTableView(self.centralwidget)
view.setGeometry(QtCore.QRect(60, 130, 541, 301))
view.resizeColumnsToContents()
view.setObjectName("view")
view.setModel(self.model)
view.setWindowTitle(title)
return view
def setupUi(self, MainWindow):
MainWindow.resize(800, 480)
self.centralwidget = QtWidgets.QWidget(MainWindow)
self.centralwidget.setObjectName("centralwidget")
self.createView("TableView")
MainWindow.setCentralWidget(self.centralwidget)
class TableModel(QtCore.QAbstractTableModel):
def __init__(self, data):
super(TableModel, self).__init__()
self._data = data
self.initializeModel()
def initializeModel(self):
self.setHeaderData(2, QtCore.Qt.Horizontal, "serialNo")
self.setHeaderData(3, QtCore.Qt.Horizontal, "timestamp")
self.setHeaderData(4, QtCore.Qt.Horizontal, "result1")
def data(self, index, role):
if index.isValid():
if role == Qt.DisplayRole or role == Qt.EditRole:
value = self._data.iloc[index.row(), index.column()]
return str(value)
if role == Qt.ForegroundRole or role == Qt.EditRole:
value = self._data.iloc[index.row(), index.column()]
if (
(isinstance(value, int) or isinstance(value, float))
and value > 0
):
return QtGui.QColor('red')
def rowCount(self, index):
return self._data.shape[0]
def columnCount(self, index):
return self._data.shape[1]
def headerData(self, section, orientation, role):
# section is the index of the column/row.
if role == Qt.DisplayRole:
if orientation == Qt.Horizontal:
return str(self._data.columns[section])
if orientation == Qt.Vertical:
return str(self._data.index[section])
def setData(self, index, value, role):
if role == Qt.EditRole:
self._data.iloc[index.row(), index.column()] = value
return True
def flags(self, index):
return QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
if __name__ == "__main__":
app = QApplication(sys.argv)
MainWindow = QMainWindow()
ui = TestUI()
ui.setupUi(MainWindow)
MainWindow.show()
sys.exit(app.exec_())
Related
I have a problem with the line below self.tableView.set??????????(df) that supposed to display the data frame in PyQt5. I put ??? there where I am missing the code I need.
def btn_clk(self):
path = self.lineEdit.text()
df = pd.read_csv(path)
self.tableView.set??????????(df)
The rest of the code works, because if I use print(df) in the above code, the data frame is printed in the IPython console. So, Pandas reads the CSV and prints it.
But, I tried many things to get it displayed in PyQt5 and nothing works. I am not very familiar with PyQt, just started to play around with it and I am stuck here.
Here is my code:
from PyQt5 import QtCore, QtGui, QtWidgets
import pandas as pd
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
MainWindow.setObjectName("MainWindow")
MainWindow.resize(662, 512)
self.centralwidget = QtWidgets.QWidget(MainWindow)
self.centralwidget.setObjectName("centralwidget")
self.horizontalLayout = QtWidgets.QHBoxLayout(self.centralwidget)
self.horizontalLayout.setObjectName("horizontalLayout")
self.verticalLayout = QtWidgets.QVBoxLayout()
self.verticalLayout.setObjectName("verticalLayout")
self.lineEdit = QtWidgets.QLineEdit(self.centralwidget)
self.lineEdit.setObjectName("lineEdit")
self.verticalLayout.addWidget(self.lineEdit)
self.tableView = QtWidgets.QTableView(self.centralwidget)
self.tableView.setObjectName("tableView")
self.verticalLayout.addWidget(self.tableView)
self.pushButton = QtWidgets.QPushButton(self.centralwidget)
self.pushButton.setObjectName("pushButton")
self.verticalLayout.addWidget(self.pushButton)
self.horizontalLayout.addLayout(self.verticalLayout)
MainWindow.setCentralWidget(self.centralwidget)
self.menubar = QtWidgets.QMenuBar(MainWindow)
self.menubar.setGeometry(QtCore.QRect(0, 0, 662, 21))
self.menubar.setObjectName("menubar")
MainWindow.setMenuBar(self.menubar)
self.statusbar = QtWidgets.QStatusBar(MainWindow)
self.statusbar.setObjectName("statusbar")
MainWindow.setStatusBar(self.statusbar)
self.retranslateUi(MainWindow)
QtCore.QMetaObject.connectSlotsByName(MainWindow)
def retranslateUi(self, MainWindow):
_translate = QtCore.QCoreApplication.translate
MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
self.pushButton.setText(_translate("MainWindow", "PushButton"))
self.pushButton.clicked.connect(self.btn_clk)
MainWindow.show()
def btn_clk(self):
path = self.lineEdit.text()
df = pd.read_csv(path)
self.tableView.set????????????(df)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
MainWindow = QtWidgets.QMainWindow()
ui = Ui_MainWindow()
ui.setupUi(MainWindow)
MainWindow.show()
sys.exit(app.exec_())
In the case of QTableView the data must be provided through a model since it implements the MVC (Model-View-Controller) paradigm, in the case of pandas there is no default model but we can create a custom as shown in the following part:
class PandasModel(QtCore.QAbstractTableModel):
def __init__(self, df = pd.DataFrame(), parent=None):
QtCore.QAbstractTableModel.__init__(self, parent=parent)
self._df = df.copy()
def toDataFrame(self):
return self._df.copy()
def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
if role != QtCore.Qt.DisplayRole:
return QtCore.QVariant()
if orientation == QtCore.Qt.Horizontal:
try:
return self._df.columns.tolist()[section]
except (IndexError, ):
return QtCore.QVariant()
elif orientation == QtCore.Qt.Vertical:
try:
# return self.df.index.tolist()
return self._df.index.tolist()[section]
except (IndexError, ):
return QtCore.QVariant()
def data(self, index, role=QtCore.Qt.DisplayRole):
if role != QtCore.Qt.DisplayRole:
return QtCore.QVariant()
if not index.isValid():
return QtCore.QVariant()
return QtCore.QVariant(str(self._df.ix[index.row(), index.column()]))
def setData(self, index, value, role):
row = self._df.index[index.row()]
col = self._df.columns[index.column()]
if hasattr(value, 'toPyObject'):
# PyQt4 gets a QVariant
value = value.toPyObject()
else:
# PySide gets an unicode
dtype = self._df[col].dtype
if dtype != object:
value = None if value == '' else dtype.type(value)
self._df.set_value(row, col, value)
return True
def rowCount(self, parent=QtCore.QModelIndex()):
return len(self._df.index)
def columnCount(self, parent=QtCore.QModelIndex()):
return len(self._df.columns)
def sort(self, column, order):
colname = self._df.columns.tolist()[column]
self.layoutAboutToBeChanged.emit()
self._df.sort_values(colname, ascending= order == QtCore.Qt.AscendingOrder, inplace=True)
self._df.reset_index(inplace=True, drop=True)
self.layoutChanged.emit()
And then use it:
def btn_clk(self):
path = self.lineEdit.text()
df = pd.read_csv(path)
model = PandasModel(df)
self.tableView.setModel(model)
The complete code is here
Update 03-07-2019:
Some Pandas methods are deprecated so I have implemented a new version (which can also be used in QML as this answer shows):
class DataFrameModel(QtCore.QAbstractTableModel):
DtypeRole = QtCore.Qt.UserRole + 1000
ValueRole = QtCore.Qt.UserRole + 1001
def __init__(self, df=pd.DataFrame(), parent=None):
super(DataFrameModel, self).__init__(parent)
self._dataframe = df
def setDataFrame(self, dataframe):
self.beginResetModel()
self._dataframe = dataframe.copy()
self.endResetModel()
def dataFrame(self):
return self._dataframe
dataFrame = QtCore.pyqtProperty(pd.DataFrame, fget=dataFrame, fset=setDataFrame)
#QtCore.pyqtSlot(int, QtCore.Qt.Orientation, result=str)
def headerData(self, section: int, orientation: QtCore.Qt.Orientation, role: int = QtCore.Qt.DisplayRole):
if role == QtCore.Qt.DisplayRole:
if orientation == QtCore.Qt.Horizontal:
return self._dataframe.columns[section]
else:
return str(self._dataframe.index[section])
return QtCore.QVariant()
def rowCount(self, parent=QtCore.QModelIndex()):
if parent.isValid():
return 0
return len(self._dataframe.index)
def columnCount(self, parent=QtCore.QModelIndex()):
if parent.isValid():
return 0
return self._dataframe.columns.size
def data(self, index, role=QtCore.Qt.DisplayRole):
if not index.isValid() or not (0 <= index.row() < self.rowCount() \
and 0 <= index.column() < self.columnCount()):
return QtCore.QVariant()
row = self._dataframe.index[index.row()]
col = self._dataframe.columns[index.column()]
dt = self._dataframe[col].dtype
val = self._dataframe.iloc[row][col]
if role == QtCore.Qt.DisplayRole:
return str(val)
elif role == DataFrameModel.ValueRole:
return val
if role == DataFrameModel.DtypeRole:
return dt
return QtCore.QVariant()
def roleNames(self):
roles = {
QtCore.Qt.DisplayRole: b'display',
DataFrameModel.DtypeRole: b'dtype',
DataFrameModel.ValueRole: b'value'
}
return roles
Like #DanielR says, the marked answer isn't working any more, but this tutorial seems to be.
pandas.version='1.0.1', PYQT_VERSION_STR = 5.11.3
class pandasModel(QAbstractTableModel):
def __init__(self, data):
QAbstractTableModel.__init__(self)
self._data = data
def rowCount(self, parent=None):
return self._data.shape[0]
def columnCount(self, parnet=None):
return self._data.shape[1]
def data(self, index, role=Qt.DisplayRole):
if index.isValid():
if role == Qt.DisplayRole:
return str(self._data.iloc[index.row(), index.column()])
return None
def headerData(self, col, orientation, role):
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
return self._data.columns[col]
return None
To fix the sorting...
from natsort import natsorted, index_natsorted, order_by_index
def sort(self, column, order):
if order == 0:
self._dataframe = self._dataframe.reindex(index=order_by_index(self._dataframe.index, index_natsorted(self._dataframe[column])))
else:
self._dataframe = self._dataframe.reindex(index=order_by_index(self._dataframe.index, reversed(index_natsorted(self._dataframe[column]))))
self._dataframe.reset_index(inplace=True, drop=True)
self.setDataFrame(self._dataframe)
I have a QTableView with QAbstractTableModel. I want to change row backgroundcolor when I click one cell. I know at least two methods that can change row backgroundcolor when clicking one cell. One is use delegate, and another is use setData method in QAbstractTable. But I have got none of them,,,oops. Here I tried using setData method in QAbstractTable to just change the selected cell backgroundcolor, but failed! Could you pls help me to correct my code in order to change a whole row color not just a cell. Anyway, changing cell color is not even ok! Much thanks! Code below
import sys
import typing
import numpy as np
import pandas as pd
from PyQt5 import QtCore
from PyQt5.QtWidgets import QApplication, QMainWindow, \
QWidget, QTableView, QVBoxLayout
from PyQt5.QtCore import QAbstractTableModel, Qt, QModelIndex
class MyTableModel(QAbstractTableModel):
def __init__(self, data:pd.DataFrame):
super().__init__()
self._data = data
def data(self, index: QModelIndex, role: int = ...) -> typing.Any:
if role==Qt.DisplayRole:
value = str(self._data.iloc[index.row()][index.column()])
return value
def setData(self, index: QModelIndex, value: typing.Any, role: int = ...) -> bool:
if not index.isValid():
return False
else:
if role==Qt.BackgroundColorRole:
self.dataChanged.emit(index, index, [role])
return True
def rowCount(self, parent: QModelIndex = ...) -> int:
return self._data.shape[0]
def columnCount(self, parent: QModelIndex = ...) -> int:
return self._data.shape[1]
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.myTable = QTableView()
df = self.get_DataFrame_Data()
self.model = MyTableModel(df)
self.myTable.setModel(self.model)
self.myTable.clicked.connect(self.change_row_bgcolor)
hlayout = QVBoxLayout()
hlayout.addWidget(self.myTable)
dummy_widget = QWidget()
dummy_widget.setLayout(hlayout)
self.setCentralWidget(dummy_widget)
self.setFixedSize(600, 600)
def get_DataFrame_Data(self):
ndarray = np.random.randint(10, 50, (7, 3))
df = pd.DataFrame(data=ndarray, columns=['col1','col2','col3'])
return df
def change_row_bgcolor(self, index):
self.model.setData(index,Qt.red,Qt.BackgroundColorRole)
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()
Solved! Two ways to changed QTableView Row background color when user mouse clicking.
Use QStyledItemDelegate.
Subclass QStyledItemDelegate. You should set a class property (etc tableview's currentindex) which can be reset value from outside the class, by this, the delegate's default loop will compare the tableview's currentindex.Code:
class TableDelegate(QStyledItemDelegate):
select_index = None
def paint(self, painter: QtGui.QPainter, option: 'QStyleOptionViewItem', index: QtCore.QModelIndex) -> None:
# option.state
row = index.row()
column = index.column()
select_row = self.select_index.row()
# self.initStyleOption(option,index)
if row == select_row:
# option.font.setItalic(True)
option.font.setStyle(QFont.StyleOblique)
bg = QColor(135, 206, 255)
painter.fillRect(option.rect, bg)
# painter.eraseRect(option.rect)
QStyledItemDelegate.paint(self, painter, option, index)
Use QAbstractTableModel.Also, you should set a class property, than the method data()'s default loop will compare with the class property(tableview's current index). and set backgroud color.Code:
class MyTableModel(QAbstractTableModel):
def __init__(self, data:pd.DataFrame):
super().__init__()
self._data = data
self.color_enabled = False
self.color_back = Qt.magenta
self.target_row = -1
def data(self, index: QModelIndex, role: int) -> typing.Any:
if role==Qt.DisplayRole:
# print(index.row())
value = str(self._data.iloc[index.row()][index.column()])
return value
if role == Qt.BackgroundRole and index.row()==self.target_row \
and self.color_enabled==True:
return QBrush(self.color_back)
And,,! there is another special problem that should emphasized here. When user click one cell, there is a default backgroud which I see in my computer is blue. If you want to whole row background color is same when clicking, you should do this after creating a QTableView:
self.myTable.setStyleSheet("QTableView::item:selected{"
"background:rgb(135, 206, 255)}")
This means,you set the selected cell bgcolor by QSS, and then, either when you use QAbstractTableModel' data() method or pain() method in QStyledItemDelege, you should set the same color. Then everything is ok!
I have a program that has you drag and drop files into a QListWidget box and then you click a button to upload those files to a bucket. I would like to say which files have been uploaded and which one is currently being uploaded with an icon. Is there a way to add Icons inside/next to the QListWidget box?
Here is some of the code for the QListWidget and the drag and drop feature. I am just hoping there is a way to add icons
import sys, os
from PyQt5.QtWidgets import QApplication, QMainWindow, QListWidget, QListWidgetItem, QPushButton
from PyQt5.QtCore import Qt, QUrl
class ListBoxWidget(QListWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setAcceptDrops(True)
self.resize(600, 600)
def dragEnterEvent(self, event):
if event.mimeData().hasUrls:
event.accept()
else:
event.ignore()
def dragMoveEvent(self, event):
if event.mimeData().hasUrls():
event.setDropAction(Qt.CopyAction)
event.accept()
else:
event.ignore()
def dropEvent(self, event):
if event.mimeData().hasUrls():
event.setDropAction(Qt.CopyAction)
event.accept()
links = []
for url in event.mimeData().urls():
# https://doc.qt.io/qt-5/qurl.html
if url.isLocalFile():
links.append(str(url.toLocalFile()))
else:
links.append(str(url.toString()))
self.addItems(links)
else:
event.ignore()
class AppDemo(QMainWindow):
def __init__(self):
super().__init__()
self.resize(1200, 600)
self.listbox_view = ListBoxWidget(self)
self.btn = QPushButton('Get Value', self)
self.btn.setGeometry(850, 400, 200, 50)
self.btn.clicked.connect(lambda: print(self.getSelectedItem()))
def getSelectedItem(self):
item = QListWidgetItem(self.listbox_view.currentItem())
return item.text()
if __name__ == '__main__':
app = QApplication(sys.argv)
demo = AppDemo()
demo.show()
sys.exit(app.exec_())
I have tried adding QIcon to the self.addItems(links) line but it continues to give me an error about arguments.
Instead of adding items using addItems, create indivual QListWidgetItems and add them one by one using addItem(QListWidgetItem).
def dropEvent(self, event):
if event.mimeData().hasUrls():
event.setDropAction(Qt.CopyAction)
event.accept()
for url in event.mimeData().urls():
# https://doc.qt.io/qt-5/qurl.html
if url.isLocalFile():
address = str(url.toLocalFile())
icon = QIcon('localIcon.png')
else:
address = str(url.toString())
icon = QIcon('remoteIcon.png')
self.addItem(QListWidgetItem(icon, address))
If you want to change the icon of an existing item, access it using item() and use setIcon():
def setIconForItem(self, row, icon):
self.listbox_view.item(row).setIcon(icon)
first, see code below:
import sys
from PyQt5.QtCore import (Qt, pyqtSignal, pyqtSlot)
from PyQt5.QtWidgets import (QWidget, QLCDNumber, QSlider,
QVBoxLayout, QApplication)
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def printLabel(self, str):
print(str)
#pyqtSlot(int)
def on_sld_valueChanged(self, value):
self.lcd.display(value)
self.printLabel(value)
def initUI(self):
self.lcd = QLCDNumber(self)
self.sld = QSlider(Qt.Horizontal, self)
vbox = QVBoxLayout()
vbox.addWidget(self.lcd)
vbox.addWidget(self.sld)
self.setLayout(vbox)
self.sld.valueChanged.connect(self.on_sld_valueChanged)
self.setGeometry(300, 300, 250, 150)
self.setWindowTitle('Signal & slot')
self.show()
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
I'm a little puzzled about how the true value in sld is transmitted to the formal parameter 'value' in the slot function : def sld_valChanged(self, value).
Because i can't see something like this: self.sld.valueChanged.connect(partial(self.sld_valChanged, self.sld.value))
Could someone explain that?
I am not able to emit dataChanged signal from my model. I am under python 3.5.2, PyQt5.9.1.
I tried at least 4 different syntaxes, none of this works for me: different veiws of this model are only updated when I click on them...
#pyqtSlot()
def setData(self, index: QModelIndex, Any, role=None):
if role == QtCore.Qt.EditRole:
row = index.row()
color = QtGui.QColor(Any)
if color.isValid():
self._datas[row] = color
# self.dataChanged.emit(index,index) # doesn't work because PyQt5 changed signature
# self.dataChanged.emit(index, index, []) # doesn't update other views of the same model
# self.dataChanged.emit(index,index,[QtCore.Qt.EditRole,]) # neither
# self.data_changed.emit(index,index) # class method is 'data_changed = pyqtSignal(QModelIndex,QModelIndex)', doesn't work
return True
return False
This question How to emit dataChanged in PyQt5 is marked as solved, however, I am not able to reproduce
EDIT:
A Verifiable exemple, with several views of the same model. I am expecting all views to be updated, whenever I change the color
EDIT_2 solved... Just a typo... This exemple works as expected
from PyQt5 import QtGui, QtCore
from PyQt5 import QtWidgets
from PyQt5.QtWidgets import QApplication
from PyQt5.QtCore import QModelIndex, pyqtSignal,QAbstractListModel, pyqtSlot
import sys
class listModel(QAbstractListModel):
def __init__(self, colors=None):
super(QAbstractListModel, self).__init__()
self._datas = colors
def data(self, index: QModelIndex, role=None):
row = index.row()
value = self._datas[row]
if role == QtCore.Qt.DisplayRole:
return value.name()
elif role == QtCore.Qt.DecorationRole:
pixmap = QtGui.QPixmap(12,12)
pixmap.fill(value)
icon = QtGui.QPixmap(pixmap)
return icon
elif role == QtCore.Qt.ToolTipRole:
return "Hex code: " + self._datas[row].name()
def rowCount(self, parent=None, *args, **kwargs):
return len(self._datas)
def headerData(self, p_int, Qt_Orientation, role=None):
if role == QtCore.Qt.DisplayRole:
if Qt_Orientation == QtCore.Qt.Horizontal:
return "Palette"
else:
return "Color {a}".format(a=p_int)
def flags(self, QModelIndex: QModelIndex):
# check state editable or not?
return QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled
#pyqtSlot()
def setData(self, QModelIndex, Any, role=None):
if role == QtCore.Qt.EditRole:
row = QModelIndex.row()
color = QtGui.QColor(Any)
if color.isValid():
self._datas[row] = color
self.dataChanged.emit(QModelIndex, QModelIndex, [])
return True
return False
if __name__ == '__main__':
app = QApplication(sys.argv)
red = QtGui.QColor(255,0,0)
green = QtGui.QColor(0, 255, 0)
blue = QtGui.QColor(0, 0, 255)
colors = [red,green,blue]
model = listModel(colors)
listView = QtWidgets.QListView()
listView.setModel(model)
listView.setWindowTitle('list')
listView.show()
treeV = QtWidgets.QTreeView()
treeV.setModel(model)
treeV.setWindowTitle('tree')
treeV.show()
tableV = QtWidgets.QTableView()
tableV.setModel(model)
tableV.setWindowTitle('table')
tableV.show()
sys.exit(app.exec_())
self.dataChanged.emit(index, index, [QtCore.Qt.EditRole]) is correct. There must be a bug elsewhere in your code.