I have created a simple window with two buttons and a table view in Qt designer and then save my ui as user_interface.ui. Here is the user_interface.
Then, I use the following code to generate some random data and display them on the window with the Generate data and display button click. I also have a save button to give the user the option to save the generated data as csv:
import sys
import pandas as pd
from numpy.random import randn
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.uic import loadUiType
ui,_=loadUiType('user_interface.ui')
class MainApp(QMainWindow, ui):
def __init__(self, parent=None):
QMainWindow.__init__(self)
self.setupUi(self)
self.generate_table.clicked.connect(self.generate_data)
def generate_data(self):
df = pd.DataFrame(randn(5,4),index='A B C D E'.split(),columns='W X Y Z'.split())
model = pandasModel(df)
self.tableView.setModel(model)
self.save_as_csv.clicked.connect(lambda: self.save_data_as_csv(df))
def save_data_as_csv(self, df):
name = QFileDialog.getSaveFileName(self, 'Save File', filter='*.csv')
if(name[0] == ''):
pass
else:
df.to_csv(name[0], index = False)
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
def main():
app=QApplication(sys.argv)
QApplication.processEvents()
window = MainApp()
window.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
When I hit the Generate data and display button, it works fine and I see the generated table. If I keep hitting this button, I do see the random numbers change and it is what I want.
The problem is that when I want to save the generated data, If I have clicked the Generate data and display 20 times to change the data, I will be asked 20 times (except if I hit Cancel in the save popup window) and it basically is trying to save all the dataframes that have been generated on each click of the Generate data and display button. All I want to save is the most recent generated data right before I hit the Save as csv button. I can't figure out what I need to do to achieve that?
This is occurring because another instance of the signal is connected to save_data_as_csv each time generate_data is called. Instead declare the signal connection one time in the constructor and keep a pointer to the most recent dataframe in your class (self.df) so it no longer requires passing the dataframe as an argument.
class MainApp(QMainWindow, ui):
def __init__(self, parent=None):
QMainWindow.__init__(self)
self.setupUi(self)
self.generate_table.clicked.connect(self.generate_data)
self.save_as_csv.clicked.connect(self.save_data_as_csv)
def generate_data(self):
self.df = pd.DataFrame(randn(5,4),index='A B C D E'.split(),columns='W X Y Z'.split())
model = pandasModel(self.df)
self.tableView.setModel(model)
def save_data_as_csv(self):
name = QFileDialog.getSaveFileName(self, 'Save File', filter='*.csv')
if(name[0] == ''):
pass
else:
self.df.to_csv(name[0], index = False)
Related
I'm trying to design a table widget in pyqt that gets the value in the first column of a row when navigating the table with the arrow keys. I'm able to do that using clicked.connect() on my table class using the pointer, but when using the arrow keys to navigate the table I can't manage to figure out a way to connect my function. I'm just getting my bearings in pyqt and have tried figuring this out from the docs but it doesn't seem any of the signal methods for QAbstractItemModel work. Not sure if I even tried the right thing. I tried adding a KeyPressEvent definition to my QAbstractTableView class but couldn't get that to work - also tried subclassing QTableView to no avail. Off course not sure any of those attempts were down properly. Here is my basic code that makes a table that highlights rows and prints the value in the first column of the selected row when that row is selected via a pointer click, but if you navigate with the arrow keys obviously nothing prints because the method to print the value isn't called.
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import Qt, QAbstractTableModel, QVariant
test_data = [[i,j,k] for i in range(2) for j in range(2) for k in range(2)]
class TableStaticModel(QAbstractTableModel):
def __init__(self, header, data):
super(TableStaticModel, self).__init__()
self._data = data
self.header = header
def data(self, index, role=Qt.DisplayRole):
if role==Qt.DisplayRole:
return self._data[index.row()][index.column()]
if role==Qt.TextAlignmentRole:
value = self._data[index.row()][index.column()]
return Qt.AlignCenter
def rowCount(self, index):
return len(self._data)
def columnCount(self,index):
return len(self._data[0])
def headerData(self, col, orientation, role):
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
return QVariant(self.header[col])
return QVariant()
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.table = QtWidgets.QTableView()
model = TableStaticModel(['A','B','C'],test_data)
self.table.setModel(model)
self.table.clicked.connect(self.get_table_row_value)
self.table.setSelectionBehavior(self.table.SelectRows)
self.table.resizeRowsToContents()
self.table.setColumnWidth(0,83)
self.table.setColumnWidth(1,85)
self.table.setColumnWidth(2,83)
self.setCentralWidget(self.table)
def get_table_row_value(self):
index=self.table.selectionModel().currentIndex()
value=index.sibling(index.row(),0).data()
print(value)
app=QtWidgets.QApplication(sys.argv)
window=MainWindow()
window.show()
app.exec_()
If you only want to select one row then you must set the selectionModel to SingleSelection. On the other hand you must use the selectionChanged signal of the selectionModel:
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.table = QtWidgets.QTableView(
selectionBehavior=QtWidgets.QTableView.SelectRows,
selectionMode=QtWidgets.QTableView.SingleSelection,
)
model = TableStaticModel(["A", "B", "C"], test_data)
self.table.setModel(model)
self.table.selectionModel().selectionChanged.connect(self.get_table_row_value)
self.table.resizeRowsToContents()
self.table.setColumnWidth(0, 83)
self.table.setColumnWidth(1, 85)
self.table.setColumnWidth(2, 83)
self.setCentralWidget(self.table)
def get_table_row_value(self):
rows = set()
for index in self.table.selectedIndexes():
rows.add(index.row())
for row in rows:
ix = self.table.model().index(row, 0)
print(ix.data())
how i can show a loading GIF image or a Qlabel update while i load data from database with QAbstractTableModel.i am new in pyqt5 and i tried it from last week but didn't understand how i can do that .
there are many example i found in stackoverflow but i really cannot deal with Qthread.i don't want to use timer for it as i can see in many examples are using qtimer. loading gif is automatically close while loading completed in table.
can anyone please show me how to do this and describes all things.
from PyQt5 import QtCore, QtWidgets
import pandas as pd
import numpy as np
import pyodbc
class NumpyArrayModel(QtCore.QAbstractTableModel):
def __init__(self, array, headers, parent=None):
QtCore.QAbstractTableModel.__init__(self, parent=parent)
self._array = array
self._headers = headers
self.r, self.c = np.shape(self.array)
#property
def array(self):
return self._array
#property
def headers(self):
return self._headers
def rowCount(self, parent=QtCore.QModelIndex()):
return self.r
def columnCount(self, parent=QtCore.QModelIndex()):
return self.c
def headerData(self, p_int, orientation, role):
if role == QtCore.Qt.DisplayRole:
if orientation == QtCore.Qt.Horizontal:
if p_int < len(self.headers):
return self.headers[p_int]
elif orientation == QtCore.Qt.Vertical:
return p_int + 1
return
def data(self, index, role=QtCore.Qt.DisplayRole):
if not index.isValid():
return None
row = index.row()
column = index.column()
if row < 0 or row >= self.rowCount():
return None
if column < 0 or column >= self.columnCount():
return None
if role == QtCore.Qt.DisplayRole:
return str(self.array[row, column])
return None
def setData(self, index, value, role):
if not index.isValid():
return False
if role != QtCore.Qt.EditRole:
return False
row = index.row()
column = index.column()
if row < 0 or row >= self.rowCount():
return False
if column < 0 or column >= self.columnCount():
return False
self.array.values[row][column] = value
self.dataChanged.emit(index, index)
return True
class Widget(QtWidgets.QWidget):
def __init__(self, parent=None):
QtWidgets.QWidget.__init__(self, parent=None)
vLayout = QtWidgets.QVBoxLayout(self)
hLayout = QtWidgets.QHBoxLayout()
self.pathLE = QtWidgets.QLabel(self)
hLayout.addWidget(self.pathLE)
self.loadBtn = QtWidgets.QPushButton("Load data", self)
hLayout.addWidget(self.loadBtn)
vLayout.addLayout(hLayout)
self.pandasTv = QtWidgets.QTableView(self)
vLayout.addWidget(self.pandasTv)
self.loadBtn.clicked.connect(self.loadFile)
self.pandasTv.setSortingEnabled(True)
def loadFile(self):
self.pathLE.setText("Loading data")
server = '190.11.71.09'
database = ''
username = 'Admin'
passwd = ''
conn= pyodbc.connect('DRIVER={ODBC Driver 17 for SQL Server};SERVER=' +
server+';DATABASE='+database+';UID='+username+';PWD=' + passwd)
query="select * from database.dbo.tableName(nolock)"
df = pd.read_sql_query(query, conn)
array = np.array(df.values)
headers = df.columns.tolist()
model = NumpyArrayModel(array, headers)
self.pandasTv.setModel(model)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = Widget()
w.show()
sys.exit(app.exec_())
Learn how to display a GIF image.
Then learn how to trigger an action via a button.
Do not fall into the typical pit by implementing your action to
display the GIF
load the data
hide the GIF
as this ties down the one thread that triggered your action. This one thread is crucial to keep the UI updating.
So what you need to do when the button gets pressed:
Spawn a new thread to execute some code
In the new thread the code to execute contains the above statements:
show GIF
load data
hide GIF (even if data loading fails)
For showing, hiding and finally rendering the loaded data you will have to look out because now you try to modify the UI from another thread - this will need some kind of treatment. BTW this behaviour is not specific to Qt. You will find similar things in Java AWT and Swing.
I'm looking for help regarding how to add a QTableView inside a QGroupBox (this is because i need to create 4 QTableView each displaying one of each possible Status 'In QC','Ready for QC','In Progress','Pending').
The following code currently generates a program that displays a Single QTableView, that refreshes every 5 seconds with new data, the only one that matters is the Status (currently represented as column F) as the rest of the data is displayed for identification purposes. (Please note that in this example i use a code that generates automatically data to display in the QTableView, as this Table actually feeds from an Excel file, going to attach the code that reads the excel file at the end of this post):
import sys
import pandas as pd
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QAbstractTableModel, QObject, Qt
from PyQt5.QtGui import QBrush
from PyQt5.QtWidgets import QApplication, QTableView
import threading
class PandasManager(QObject):
dataFrameChanged = pyqtSignal(pd.DataFrame)
def start(self):
self.t = threading.Timer(0, self.load)
self.t.start()
def load(self):
import random
headers = list("ABCDEFG")
data = [random.sample(range(255), len(headers)) for _ in headers]
for d in data:
d[5] = random.choice(["Ready for QC", "In Progress", "Pending", "In QC"])
df = pd.DataFrame(data, columns=headers,)
self.dataFrameChanged.emit(df)
self.t = threading.Timer(5.0, self.load)
self.t.start()
def stop(self):
self.t.cancel()
class PandasModel(QAbstractTableModel):
def __init__(self, df=pd.DataFrame()):
QAbstractTableModel.__init__(self)
self._df = df
#pyqtSlot(pd.DataFrame)
def setDataFrame(self, df):
self.beginResetModel()
self._df = df
self.endResetModel()
def rowCount(self, parent=None):
return self._df.shape[0]
def columnCount(self, parent=None):
return self._df.shape[1]
def data(self, index, role=Qt.DisplayRole):
if index.isValid():
if role == Qt.BackgroundRole:
if self.columnCount() >= 6:
it = self._df.iloc[index.row(), 5]
if it == "Ready for QC":
return QBrush(Qt.yellow)
if it == "In Progress":
return QBrush(Qt.green)
if role == Qt.DisplayRole:
return str(self._df.iloc[index.row(), index.column()])
def headerData(self, col, orientation, role):
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
return self._df.columns[col]
return None
if __name__ == "__main__":
app = QApplication(sys.argv)
w = QTableView()
model = PandasModel()
w.setModel(model)
w.show()
manager = PandasManager()
manager.dataFrameChanged.connect(model.setDataFrame)
manager.start()
ret = app.exec_()
manager.stop()
sys.exit(ret)
Hopefully this explains my question, as i have been struggling on how to use QGroupBox and how to add the QTableView as I'm using it this way.
Kind Regards,
PS: Attaching code that reads from an excel file
def load(self):
weekNumber = date.today().isocalendar()[1]
aux = pd.read_excel("PCS tasks 2020.xlsm", sheet_name="W" + str(weekNumber))
today = datetime.today()
df = aux[aux["Date Received"] == today.strftime("%Y-%d-%m")]
df = df[
[
"Requestor",
"Subject",
"Task type",
"Created by",
"QC Executive",
"Status",
]
].fillna("")
df = df[df["Status"] != "Completed"]
self.dataFrameChanged.emit(df)
self.t = threading.Timer(5.0, self.load)
self.t.start()
As the example of the QGroupBox docs shows, you must use a layout that allows you to distribute the widgets (QGridLayout, QHBoxLayout, QVBoxLayout, etc.) and set it in the QGroupBox:
if __name__ == "__main__":
app = QApplication(sys.argv)
w = QTableView()
model = PandasModel()
w.setModel(model)
groupbox = QGroupBox()
lay = QVBoxLayout()
lay.addWidget(w)
groupbox.setLayout(lay)
groupbox.show()
manager = PandasManager()
manager.dataFrameChanged.connect(model.setDataFrame)
manager.start()
ret = app.exec_()
manager.stop()
sys.exit(ret)
I am using QListView to show a list of files. When I am using its IconMode and file names are too long, I want it to split text in several lines. PySide documentation tells that setWordWrap function would work in other way:
even if wrapping is enabled, the cell will not be expanded to make room for the text
But that's what I need: break the text and expand item verically.
So, can I use QListView to align files in a way it is often done in a file manager? For example, Thunar:
This is my current code:
import sys
from random import randint
from PySide import QtGui
from PySide import QtCore
app = QtGui.QApplication(sys.argv)
def gen_random_qicon():
pixmap = QtGui.QPixmap(64, 64)
pixmap.fill(
QtGui.QColor(
randint(0,255), randint(0,255), randint(0,255), 255
)
)
icon = QtGui.QIcon()
icon.addPixmap(pixmap)
return(icon)
class Test_model(QtCore.QAbstractListModel):
def __init__(self, parent=None):
super(Test_model, self).__init__(parent)
self.__items = []
def appendItem(self, item):
index = len(self.__items)
self.beginInsertRows(QtCore.QModelIndex(), index, index)
self.__items.append(item)
self.endInsertRows()
def rowCount(self, parent):
return len(self.__items)
def data(self, index, role):
image = self.__items[index.row()]
if role == QtCore.Qt.DisplayRole:
return image['name']
if role == QtCore.Qt.DecorationRole:
return gen_random_qicon()
return None
test_names = ["AB", "UO0E5", "WTRE76", "OSBTTEJ", "M4T2GW4Y55", "LI QM6WJKBC",
"B4MO4 R6JD6"]
test_model = Test_model()
for tn in test_names:
test_model.appendItem({"name": tn})
lv = QtGui.QListView()
lv.setFlow(QtGui.QListView.LeftToRight)
lv.setResizeMode(QtGui.QListView.Adjust)
lv.setViewMode(QtGui.QListView.IconMode)
# Grid
lv.setGridSize(QtCore.QSize(64, 64))
lv.setTextElideMode(QtCore.Qt.ElideNone)
lv.setModel(test_model)
lv.resize(250, 200)
lv.show()
sys.exit(app.exec_())
When I use setGridSize function and set textElideMode as ElideNone, I have this result:
Otherwise, I see complete names, but I won't have the alignment.
I have a GUI that consists of a number of sliders, and instead of updating the sliders manually when the underlying data changes, I'd like to store the data in a subclass of QAbstractListModel and have the slider positions update automatically. My subclass looks like this:
from PyQt4 import QtCore
class myDataModel(QtCore.QAbstractListModel):
def __init__(self, initData, parent=None):
super(myDataModel, self).__init__(parent)
self.__data = initData
def data(self, index, role=QtCore.Qt.DisplayRole):
if not index.isValid():
return None
if index.row() > len(self.__data):
return None
if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole:
return self.__data[index.row()]
return None
def rowCount(self, parent=QtCore.QModelIndex()):
return len(self.__data)
def setData(self, index, value, role=QtCore.Qt.EditRole):
if not index.isValid() or role != QtCore.Qt.EditRole:
return False
self.__data[index.row()] = value
self.dataChanged.emit(index, index)
return True
How can I connect this model to the sliders in my GUI so that when the data in the model is changed, the sliders change, and vice versa?
Edit: Here is a mockup of the basic interface I have been working on:
Edit: I still haven't been able to get this to work. Here is my model class:
class dataModel(QtCore.QAbstractListModel):
def __init__(self, initData, parent=None):
super(dataModel, self).__init__(parent)
self.__data = initData
def data(self, index, role=QtCore.Qt.DisplayRole):
if not index.isValid():
return None
if index.row() > len(self.__data):
return None
if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole:
return self.__data[index.row()]
return None
def rowCount(self, parent=QtCore.QModelIndex()):
return len(self.__data)
def setData(self, index, value, role=QtCore.Qt.EditRole):
if not index.isValid() or role != QtCore.Qt.EditRole:
return False
self.__data[index.row()] = value
self.dataChanged.emit(index, index)
return True
Here is the Delegate class:
class sliderDelegate(QtGui.QItemDelegate):
'''
classdocs
'''
def __init__(self, parent=None):
'''
Constructor
'''
super(sliderDelegate, self).__init__(parent)
def setEditorData(self, editor, index):
editor.setValue(index.model().data(index, QtCore.Qt.EditRole))
def setModelData(self, editor, model, index):
model.setData(index, editor.value(), QtCore.Qt.EditRole)
And here is the setup code:
self._model = dataModel([0 for i in xrange(20)])
self._parameterMapper = QtGui.QDataWidgetMapper(mainWindowInstance)
self._parameterMapper.setModel(self._model)
self._parameterMapper.setItemDelegate(sliderDelegate(mainWindowInstance))
self._parameterMapper.addMapping(self._mainWindowInstance.ui.mySlider, 0)
self._parameterMapper.toFirst()
Unfortunately I get the following error when toFirst() is called:
editor.setValue(index.model().data(index, QtCore.Qt.EditRole))
AttributeError: 'NoneType' object has no attribute 'data'
Any help would be appreciated.
So I haven't used QDataWidgetMapper. It does look interesting, but looks more useful for when you want to have multiple widgets updated to a particular row in a model (and be able to switch between rows easily), rather than each row of a model corresponding to the value of a widget (which I think is what you are after).
So this is my rather rough implementation. Hopefully you'll be able to extend it to your application (might need a bit more error checking added, and maybe the ability to link multiple sliders to a single model row, and possibly then extending to other types of widgets)
When you drag the slider, the model is updated to the sliders new value. I've also added a text box where you can type in a number, and click the button, which will set the model to a specific value. You will notice the slider will update to this value!
import sys
from PyQt4 import QtGui
from PyQt4 import QtCore
class MainWindow(QtGui.QWidget):
def __init__(self):
super(MainWindow, self).__init__()
main_layout = QtGui.QVBoxLayout()
# Create the model
self.model = MyModel()
# Create a slider and link it to the model
self.slider1 = QtGui.QSlider()
self.model.add_slider(self.slider1)
main_layout.addWidget(self.slider1)
# Add a lineEdit and button to force update the model
# Note that the LineEdit is not linked to the model, so won't update with the slider
self.edit = QtGui.QLineEdit()
button = QtGui.QPushButton('update model')
button.clicked.connect(self.on_clicked)
main_layout.addWidget(self.edit)
main_layout.addWidget(button)
self.setLayout(main_layout)
def on_clicked(self):
self.model.update_model(int(self.edit.text()),self.slider1)
class MyModel(QtGui.QStandardItemModel):
def __init__(self,*args,**kwargs):
super(MyModel,self).__init__(*args,**kwargs)
self._slider_list = {}
self.itemChanged.connect(self.on_item_changed)
def add_slider(self,slider):
if slider in self._slider_list:
raise Exception('You cannot link a slider to the model twice')
item = QtGui.QStandardItem(str(slider.value()))
self._slider_list[slider] = item
self.appendRow(item)
slider.valueChanged.connect(lambda value: self.update_model(value,slider))
def update_model(self,value,slider):
if str(value) != self._slider_list[slider].text():
self._slider_list[slider].setText(str(value))
print 'update_model: %d'%value
def on_item_changed(self,item):
slider = self._slider_list.keys()[self._slider_list.values().index(item)]
if slider.value() != int(item.text()):
slider.setValue(int(item.text()))
print 'on_item_changed: %s'%item.text()
app = QtGui.QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
Hope that helps!