For a PyQT5 widget, I need to display data from an SQL-query to an SQLite database with columns and rows inverted/rotated. Ideally, in a QTableView. (This table will only have 2 columns, one for the previous column names and one for their values. The table is meant to show stats which will be aggregated in the SQL query, which will return only one row. So I want to go from one row with multiple columns, to 2 columns with multiple rows.)
I have come up with a workaround that does the right thing using a QFormLayout instead, but it looks ugly and seems very inelegant. (See the display_data(self) method.)
#!/usr/bin/python3
from PyQt5 import QtSql
from PyQt5.QtWidgets import (QFormLayout, QWidget,
QLabel, QLineEdit, QApplication)
import sys
class InvertedTable(QWidget):
def __init__(self, company):
super().__init__()
self.db_file = "test.db"
self.company = company
self.create_connection()
self.fill_table()
self.init_UI()
self.display_data()
def create_connection(self):
self.db = QtSql.QSqlDatabase.addDatabase("QSQLITE")
self.db.setDatabaseName(self.db_file)
if not self.db.open():
print("Cannot establish a database connection to {}!".format(self.db_file))
return False
def fill_table(self):
self.db.transaction()
q = QtSql.QSqlQuery()
q.exec_("DROP TABLE IF EXISTS Cars;")
q.exec_("""CREATE TABLE Cars (Company TEXT, Model TEXT, Cars TEXT)""")
q.exec_("INSERT INTO Cars VALUES ('Honda', 'Civic', 5)")
q.exec_("INSERT INTO Cars VALUES ('Volkswagen', 'Golf', 3)")
self.db.commit()
def init_UI(self):
self.resize(300,100)
self.layout = QFormLayout()
self.setLayout(self.layout)
def display_data(self):
query = "select * from cars where company = '{}'".format(self.company)
q = QtSql.QSqlQuery()
q.exec_(query)
self.check_error(q)
record = q.record()
columns = record.count()
q.next()
for i in range(columns):
column_name = record.field(i).name()
col_field = QLabel(column_name, self)
value = q.value(i)
value_field = QLineEdit(self)
value_field.setText(value)
self.layout.addRow(col_field, value_field)
def closeEvent(self, e):
if (self.db.open()):
self.db.close()
def check_error(self, q):
lasterr = q.lastError()
if lasterr.isValid():
print(lasterr.text())
self.db.close()
exit(1)
def main():
app = QApplication(sys.argv)
ex = InvertedTable("Honda")
ex.show()
result = app.exec_()
sys.exit(result)
if __name__ == '__main__':
main()
What is the proper way to accomplish this using QTableView?
The proper way to work with QTableView would be to have a QTableModel.
As luck would have it, there is a QSqlTableModel that allows you to build a table model against a SQL table.
Funt answered a similar question by pointing to QIdentityProxyModel that can be used "on top of that" to change the representation of a data model by redefining mapToSource and mapFromSource methods.
There are also ways to transpose the result of a SQL request directly from the SQL command. See here.
Also worth reading : Model-View Programming with Qt. It's the C++ version but PyQt follows the same principles (and the classes have the same name).
Hope that helps.
After some more searching and reading up on the helpful pointers left by #PlikPlok, I found a solution here:
Apparently, this functionality is not provided by any Qt-classes out of the box, so you have to subclass both QAbstractProxyModel and QSqlRelationalDelegate, and then use these on your table:
#!/usr/bin/python3
import sys
from PyQt5 import QtSql
from PyQt5.QtWidgets import (QWidget, QApplication,
QGridLayout, QTableView)
from PyQt5.Qt import (QModelIndex, QAbstractProxyModel, QSqlRelationalDelegate)
from PyQt5.QtCore import Qt
class FlippedProxyModel(QAbstractProxyModel):
def __init__(self, parent=None):
super().__init__(parent)
def mapFromSource(self, index):
return self.createIndex(index.column(), index.row())
def mapToSource(self, index):
return self.sourceModel().index(index.column(), index.row(), QModelIndex())
def columnCount(self, parent):
return self.sourceModel().rowCount(QModelIndex())
def rowCount(self, parent):
return self.sourceModel().columnCount(QModelIndex())
def index(self, row, column, parent):
return self.createIndex(row, column)
def parent(self, index):
return QModelIndex()
def data(self, index, role):
return self.sourceModel().data(self.mapToSource(index), role)
def headerData(self, section, orientation, role):
if orientation == Qt.Horizontal:
return self.sourceModel().headerData(section, Qt.Vertical, role)
if orientation == Qt.Vertical:
return self.sourceModel().headerData(section, Qt.Horizontal, role)
class FlippedProxyDelegate(QSqlRelationalDelegate):
def createEditor(self, parent, option, index):
proxy = index.model()
base_index = proxy.mapToSource(index)
return super().createEditor(parent, option, base_index)
def setEditorData(self, editor, index):
proxy = index.model()
base_index = proxy.mapToSource(index)
return super().setEditorData(editor, base_index)
def setModelData(self, editor, model, index):
base_model = model.sourceModel()
base_index = model.mapToSource(index)
return super().setModelData(editor, base_model, base_index)
class InvertedTable(QWidget):
def __init__(self, company):
super().__init__()
self.db_file = "test.db"
self.company = company
self.create_connection()
self.fill_table()
self.create_model()
self.init_UI()
def create_connection(self):
self.db = QtSql.QSqlDatabase.addDatabase("QSQLITE")
self.db.setDatabaseName(self.db_file)
if not self.db.open():
print("Cannot establish a database connection to {}!".format(self.db_file))
return False
def fill_table(self):
self.db.transaction()
q = QtSql.QSqlQuery()
q.exec_("DROP TABLE IF EXISTS Cars;")
q.exec_("""CREATE TABLE Cars (Company TEXT, Model TEXT, Cars TEXT)""")
q.exec_("INSERT INTO Cars VALUES ('Honda', 'Civic', 5)")
q.exec_("INSERT INTO Cars VALUES ('Volkswagen', 'Golf', 3)")
self.db.commit()
def create_model(self):
self.model = QtSql.QSqlTableModel()
q = QtSql.QSqlQuery()
query = """SELECT * from cars where company = 'Honda'
"""
q.exec_(query)
self.model.setQuery(q)
self.proxy = FlippedProxyModel() # use flipped proxy model
self.proxy.setSourceModel(self.model)
def init_UI(self):
self.grid = QGridLayout()
self.setLayout(self.grid)
self.table = QTableView()
self.table.setModel(self.proxy)
self.table.setItemDelegate(FlippedProxyDelegate(self.table)) # use flipped proxy delegate
self.table.horizontalHeader().hide()
self.grid.addWidget(self.table, 0, 0)
def closeEvent(self, e):
if (self.db.open()):
self.db.close()
def check_error(self, q):
lasterr = q.lastError()
if lasterr.isValid():
print(lasterr.text())
self.db.close()
exit(1)
def main():
app = QApplication(sys.argv)
ex = InvertedTable("Honda")
ex.show()
result = app.exec_()
sys.exit(result)
if __name__ == '__main__':
main()
Related
I'm trying to reorder this QSqlTableModel from a QListView but it seems impossible i tried every thing i found on the internet ( offical documentation, examples forums blogs ) but Nothing happend,i activated the moving action and override flags method mimeData method, honnestly i don't know if i did it correctly. the drag action is working but the problem is on dragging i think.
Here is my advanced programm
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtSql import *
import sys, os
def create_connection():
db = QSqlDatabase.addDatabase("QSQLITE")
db.setDatabaseName("medias.sqlite")
if not db.open():
print(db.lastError().text())
return False
q = QSqlQuery()
if not q.exec(
"""
CREATE TABLE IF NOT EXISTS fichiers (
id INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE NOT NULL,
path VARCHAR(300) NOT NULL
)
"""
):
print(q.lastError().text())
return False
print(db.tables())
return True
class listModel(QSqlTableModel):
def __init__(self,*args, **kwargs):
super(listModel, self).__init__(*args, **kwargs)
self.setEditStrategy(QSqlTableModel.OnFieldChange)
print('mimetype', self.mimeTypes())
self.setTable("fichiers")
self.select()
def flags(self, index):
return Qt.ItemIsEditable | Qt.ItemIsDragEnabled | Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsDropEnabled
def ajouter(self, fichier):
rec = self.record()
rec.setValue("path", fichier)
self.insertRecord(-1, rec)
self.select()
def mimeData(self, indexes):
types = self.mimeTypes()
mimeData = QMimeData()
print('mimedata', mimeData)
encodedData = QByteArray()
stream = QDataStream(encodedData, QIODevice.WriteOnly)
for index in indexes:
if not index.isValid():
continue
if index.isValid():
node = index.internalPointer()
text = self.data(index, Qt.DisplayRole)
mimeData.setData(types[0], encodedData)
return mimeData
def supportedDragActions(self):
return Qt.MoveAction
def supportedDropActions(self):
return Qt.MoveAction
def dropMimeData(self, data, action, row, column, parent):
if action == Qt.MoveAction:
print ("Moving")
self.insertRows(row, 1, QModelIndex())
idx = self.index(row, 0, QModelIndex())
self.setData(idx, self.mimeTypes())
return True
if row != -1:
beginRow = row
elif parent.isValid():
beginRow = parent.row()
else:
beginRow = rowCount(QModelIndex())
class StyledItemDelegate(QStyledItemDelegate):
def initStyleOption(self, option, index):
super().initStyleOption(option, index)
data = str(index.data())
text = data.split('/')[-1]
option.text = f"{index.row() + 1} - {text}"
class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
self.setGeometry(900, 180, 800, 600)
self.setWindowTitle("Media Display")
self.setWindowIcon(QIcon('favicon.png'))
self.model = listModel()
self.listview = QListView()
delegate = StyledItemDelegate(self.listview)
self.listview.setItemDelegate(delegate)
self.listview.setModel(self.model)
self.listview.setModelColumn(1)
self.listview.setAcceptDrops(True)
self.listview.setDragEnabled(True)
self.listview.setDragDropOverwriteMode(True)
self.listview.viewport().setAcceptDrops(True)
# self.listview.setSelectionMode(QAbstractItemView.ExtendedSelection)
self.listview.setDropIndicatorShown(True)
self.listview.setDragDropMode(QAbstractItemView.InternalMove)
self.init_ui()
def addImage(self):
fichier_base, _ = QFileDialog.getOpenFileName(
self, "select video", QDir.homePath(), "Images (*.png *.xpm *.jpg *.jpeg)"
)
if fichier_base:
self.model.ajouter(fichier_base)
def clearDb(self):
query = QSqlQuery()
query.exec("DELETE FROM fichiers")
self.model.select()
def init_ui(self):
self.add_img_btn = QPushButton("Add image ")
self.add_img_btn.setFixedWidth(100)
self.add_img_btn.clicked.connect(self.addImage)
self.clear_db_btn = QPushButton("clear DB")
self.clear_db_btn.setFixedWidth(100)
self.clear_db_btn.clicked.connect(self.clearDb)
group_btns = QHBoxLayout()
main_app = QVBoxLayout()
main_app.addWidget(self.listview)
main_app.addLayout(group_btns)
group_btns.addWidget(self.add_img_btn)
group_btns.addWidget(self.clear_db_btn)
widget = QWidget()
vboxlay = QHBoxLayout(widget)
vboxlay.addLayout(main_app)
self.setCentralWidget(widget)
if __name__ == "__main__":
app = QApplication(sys.argv)
if not create_connection():
sys.exit(-1)
window = MainWindow()
window.setStyleSheet("background-color:#fff;")
window.show()
sys.exit(app.exec_())
I hate to be "that guy" but... what do you actually expect to be able to do, here?
A QSQLTableModel is a representation of an SQL table, which will have its specific ordering. You are able to sort the records in different ways (with .sort() or, if you need anything more complex, by piping it through a QSortFilterProxyModel). Implementing an arbitrary ordering where you can drag and drop elements in different places, though, is not something I would expect to be able to do with an SQL table. If you really need something like that, you would probably have to implement your own custom model, on which you "load" the sql-fetched data but which you then manage on your own. There you can implement your own ordering scheme, which you can manipulate from the view.
i want to display in a QlistView the index and the file name, so i subclassed QSqlQueryModel to override the data() method but i'm always getting None, it seems like i'm displaying the data befor adding it or some thing like that here is the concerned part of my code :
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtSql import *
import sys, os
import pathlib
CURRENT_PATH = pathlib.Path(__file__).parent
connection = QSqlDatabase.addDatabase("QSQLITE")
connection.setDatabaseName("medias.sqlite")
connection.open()
print(connection.open())
createTableQuery = QSqlQuery()
createTableQuery.exec(
"""
CREATE TABLE fichiers (
id INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE NOT NULL,
path VARCHAR(300) NOT NULL
)
"""
)
print(connection.tables())
class PlaylistModel(QSqlQueryModel):
def __init__(self, playlist,*args, **kwargs):
super(PlaylistModel, self).__init__(*args, **kwargs)
self.playlist = playlist or [[]]
def data(self, index, role):
row = index.row()
if role == Qt.DisplayRole:
try:
text = self.playlist[index.row()][1]
except IndexError:
text = None
return text # always getting None
class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
self.play_list = []
self.setGeometry(900,180,800,600)
self.setWindowTitle("Media Display")
self.model = PlaylistModel(self.play_list)
self.model.setQuery("SELECT path FROM fichiers")
self.listview = QListView()
self.listview.setModel(self.model)
self.listview.setModelColumn(1)
self.main_layout()
self.DbConnect()
def DbConnect(self):
self.connection = QSqlDatabase.addDatabase("QSQLITE")
self.connection.setDatabaseName("medias.sqlite")
self.connection.open()
createTableQuery = QSqlQuery()
createTableQuery.exec(
""" CREATE TABLE fichiers (
id INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE NOT NULL,
path VARCHAR(300) NOT NULL
)
"""
)
self.model.setQuery("SELECT path FROM fichiers")
return True
def addToPlaylist(self):
self.play_list.clear()
model = self.listview.model()
for row in range(model.rowCount()):
index = model.index(row , 0)
item = model.data(index, Qt.DisplayRole)
self.play_list.append(item)
print('the playlist',self.play_list)
def addImage(self):
fichier_base, _ = QFileDialog.getOpenFileName(self, 'select video', QDir.homePath(),"Images (*.png *.xpm *.jpg *.jpeg)")
if fichier_base:
query = QSqlQuery()
query.prepare("""INSERT INTO fichiers (path) VALUES (?)""")
query.addBindValue(fichier_base)
if query.exec_():
last_query = self.model.query().executedQuery()
self.model.setQuery("")
self.model.setQuery(last_query)
else:
print(query.lastError().text())
def clearDb(self):
query = QSqlQuery(self.connection)
if self.connection.open():
query.exec("DELETE FROM fichiers")
query.clear()
last_query = self.model.query().executedQuery()
self.model.setQuery("")
self.model.setQuery(last_query)
def main_layout(self):
self.add_img_btn = QPushButton("Add image ")
self.add_img_btn.setFixedWidth(150)
self.add_img_btn.clicked.connect(self.addImage)
self.clear_db_btn = QPushButton("clear DB")
self.clear_db_btn.setFixedWidth(150)
self.clear_db_btn.clicked.connect(self.clearDb)
self.refresh_btn = QPushButton("refresh")
self.refresh_btn.setFixedWidth(150)
self.refresh_btn.clicked.connect(self.addToPlaylist)
group_btns = QHBoxLayout()
main_app = QVBoxLayout()
main_app.addWidget(self.listview)
main_app.addLayout(group_btns)
group_btns.addWidget(self.add_img_btn)
group_btns.addWidget(self.clear_db_btn)
group_btns.addWidget(self.refresh_btn)
vboxlay = QHBoxLayout()
vboxlay.addLayout(main_app)
widget = QWidget(self)
self.setCentralWidget(widget)
widget.setLayout(vboxlay)
if __name__ == '__main__':
app= QApplication(sys.argv)
window = MainWindow()
window.setStyleSheet('background-color:#fff;')
window.show()
sys.exit(app.exec_())
on a previous app i subclassed the QAbstractListModel Class and i did it like that
class PlaylistModel(QAbstractListModel):
def __init__(self, playlist, *args, **kwargs):
super(PlaylistModel, self).__init__(*args, **kwargs)
self.playlist = playlist
def data(self, index, role):
if role == Qt.DisplayRole:
media = self.playlist.media(index.row())
print('mediaaaaaaa', media )
print('plaaaaaylist', self.playlist )
name_video = media.canonicalUrl().fileName()
i = index.row() + 1
return f"{i} - {name_video}"
def rowCount(self, index):
return self.playlist.mediaCount()
The following should be taken into account:
If you are going to show information from a table then you must use a QSqlTableModel. QSqlQueryModel is a read-only model whose objective is to show very particular query information. On the other hand, QSqlTableModel has several methods to handle the tables.
If you are going to modify how the information of the model is shown in a view then you must use a delegate. This makes the modifications more flexible since you can have different models applying the same modifications to the views.
Considering the above, the solution is:
import sys
from PyQt5.QtCore import QDir
from PyQt5.QtSql import QSqlDatabase, QSqlTableModel, QSqlQuery
from PyQt5.QtWidgets import (
QApplication,
QFileDialog,
QHBoxLayout,
QListView,
QMainWindow,
QPushButton,
QStyledItemDelegate,
QVBoxLayout,
QWidget,
)
def create_connection():
db = QSqlDatabase.addDatabase("QSQLITE")
db.setDatabaseName("medias.sqlite")
if not db.open():
print(db.lastError().text())
return False
q = QSqlQuery()
if not q.exec(
"""
CREATE TABLE IF NOT EXISTS fichiers (
id INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE NOT NULL,
path VARCHAR(300) NOT NULL
)
"""
):
print(q.lastError().text())
return False
print(db.tables())
return True
class StyledItemDelegate(QStyledItemDelegate):
def initStyleOption(self, option, index):
super().initStyleOption(option, index)
option.text = f"{index.row() + 1} - {index.data()}"
class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
self.play_list = []
self.setGeometry(900, 180, 800, 600)
self.setWindowTitle("Media Display")
self.model = QSqlTableModel()
self.model.setTable("fichiers")
self.model.select()
self.listview = QListView()
delegate = StyledItemDelegate(self.listview)
self.listview.setItemDelegate(delegate)
self.listview.setModel(self.model)
self.listview.setModelColumn(1)
self.init_ui()
def addImage(self):
fichier_base, _ = QFileDialog.getOpenFileName(
self, "select video", QDir.homePath(), "Images (*.png *.xpm *.jpg *.jpeg)"
)
if fichier_base:
rec = self.model.record()
rec.setValue("path", fichier_base)
self.model.insertRecord(-1, rec)
self.model.select()
def clearDb(self):
query = QSqlQuery()
query.exec("DELETE FROM fichiers")
self.model.select()
def init_ui(self):
self.add_img_btn = QPushButton("Add image ")
self.add_img_btn.setFixedWidth(150)
self.add_img_btn.clicked.connect(self.addImage)
self.clear_db_btn = QPushButton("clear DB")
self.clear_db_btn.setFixedWidth(150)
self.clear_db_btn.clicked.connect(self.clearDb)
self.refresh_btn = QPushButton("refresh")
self.refresh_btn.setFixedWidth(150)
group_btns = QHBoxLayout()
main_app = QVBoxLayout()
main_app.addWidget(self.listview)
main_app.addLayout(group_btns)
group_btns.addWidget(self.add_img_btn)
group_btns.addWidget(self.clear_db_btn)
group_btns.addWidget(self.refresh_btn)
widget = QWidget()
vboxlay = QHBoxLayout(widget)
vboxlay.addLayout(main_app)
self.setCentralWidget(widget)
if __name__ == "__main__":
app = QApplication(sys.argv)
if not create_connection():
sys.exit(-1)
window = MainWindow()
window.setStyleSheet("background-color:#fff;")
window.show()
sys.exit(app.exec_())
I will briefly describe my program first
Program Description
Below are the 2 widows used in the program:
Main Window (Group List)
Input Window (Student Table)
The Main Window allows users to create/edit/delete entries in Groups list (QListWidget), which shows title of each entry read from a Sqlite database(DB). The 'New' button opens a new Input Window, which allows users to insert entries in Student table (QTableWidget). '+' button adds new row to table, and '-' button removes selected row. 'Name' and 'Age' of each entry can be edited directly by clicking on a cell in the table. The 'Save' button saves the title and the table inputs(entries of students: 'Name' and 'Age') to DB and updates the 'Groups' list in the Main Window. The 'Cancel' button closes the Input Window without saving the changes made in the window.
Database
Group list (QListView)
Student table (QTableWidget)
I am using SQLite using PyQt5's Qtsql class for database. 'group_id' in Student table is FK to Group list's 'id'.
My Problem
I would like to be able to select an entry in 'Groups' list, press 'Edit' button, and show Input Window's Student table filled with data from DB, just as shown above. I tried using QDataWidgetMapper, as it worked well with populating QTextEdit and QLineEdit, but I am having trouble populating QTableWidget with it.
How to populate QTableWidget from database in this context. Is there a method I am missing in QDataWidgetMapper?
Codes
Relevant snippets of my code where I attempt to map Student table in DB to PyQt5 (Most of this is my adapted version of eyllanesc's answer to Accessing SQL data from a list entry)
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self._groups_model = QtSql.QSqlTableModel(self)
self.groups_model.setTable("Groups")
self.groups_model.select()
self._student_model = QtSql.QSqlTableModel(self)
self.student_model.setTable("Student")
self.student_model.select()
self.sql_list_view = QtWidgets.QListView()
self.sql_list_view.setModel(self.groups_model)
self.sql_list_view.setModelColumn(self.groups_model.record().indexOf("group_name"))
...
#property
def macro_model(self):
return self._macro_model
#property
def sheets_model(self):
return self._sheets_model
#QtCore.pyqtSlot()
def edit(self):
ixs = self.listView_macros.selectionModel().selectedIndexes()
if ixs:
print(ixs)
d = EditDialog(self.groups_model, ixs[0].row(), self.student_model)
d.exec_()
class EditDialog(QtWidgets.QDialog):
def __init__(self, gr_model, gr_idx, std_model, parent=None):
super().__init__(parent)
self.title_le = QtWidgets.QLineEdit()
self.student_table = QtWidgets.QTableWidget(self)
groups_mapper = QtWidgets.QDataWidgetMapper(
self, submitPolicy=QtWidgets.QDataWidgetMapper.ManualSubmit
)
groups_mapper.setModel(gr_model)
groups_mapper.addMapping(self.title_le, gr_model.record().indexOf("group_name"))
groups_mapper.setCurrentIndex(gr_idx)
student_mapper = QtWidgets.QDataWidgetMapper(
self, submitPolicy=QtWidgets.QDataWidgetMapper.ManualSubmit
)
student_mapper.setModel(std_model)
student_mapper.addMapping(self.student_table, ????) # << I am having trouble here
You are confusing the concepts(I recommend you review the official Qt documentation and test the PyQt5 examples that are in its source code). QDataWidgetMapper is used to map a single row of a model so it will not be useful to handle several rows. You should not use QTableWidget but a QTableView with a QSqlTableModel with a filter based on the FK. Then applying the same logic that was implemented to add groups to add students.
from PyQt5 import QtCore, QtGui, QtWidgets, QtSql
def create_connection(database):
db = QtSql.QSqlDatabase.addDatabase("QSQLITE")
db.setDatabaseName(database)
if not db.open():
print("Cannot open database")
print(
"Unable to establish a database connection.\n"
"This example needs SQLite support. Please read "
"the Qt SQL driver documentation for information "
"how to build it.\n\n"
"Click Cancel to exit."
)
return False
query = QtSql.QSqlQuery()
if not query.exec_(
"""CREATE TABLE IF NOT EXISTS Groups (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"group_name" TEXT)"""
):
print(query.lastError().text())
return False
if not query.exec_(
"""CREATE TABLE IF NOT EXISTS Student (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"group_id" INTEGER,
"name" TEXT,
"age" INTEGER,
FOREIGN KEY(group_id) REFERENCES Groups(id))"""
):
print(query.lastError().text())
return False
return True
class AddGroupDialog(QtWidgets.QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.title_le = QtWidgets.QLineEdit()
button_box = QtWidgets.QDialogButtonBox(self)
button_box.setOrientation(QtCore.Qt.Horizontal)
button_box.setStandardButtons(
QtWidgets.QDialogButtonBox.Cancel | QtWidgets.QDialogButtonBox.Ok
)
button_box.accepted.connect(self.accept)
button_box.rejected.connect(self.reject)
lay = QtWidgets.QVBoxLayout(self)
lay.addWidget(self.title_le)
lay.addWidget(button_box)
#property
def title(self):
return self.title_le.text()
class EditMacroDialog(QtWidgets.QDialog):
def __init__(self, model, index, parent=None):
super().__init__(parent)
self._group_id = model.record(index).value("id")
self.title_le = QtWidgets.QLineEdit()
self.student_table_model = QtSql.QSqlTableModel()
self.student_table_model.setEditStrategy(QtSql.QSqlTableModel.OnFieldChange)
self.student_table_model.setTable("Student")
self.student_table_model.setFilter("group_id={}".format(self.group_id))
self.student_table_model.select()
self.table_view = QtWidgets.QTableView(selectionBehavior=QtWidgets.QAbstractItemView.SelectRows)
self.table_view.setModel(self.student_table_model)
self.table_view.horizontalHeader().setSectionResizeMode(QtWidgets.QHeaderView.Stretch)
for name in ("group_id", "id"):
self.table_view.hideColumn(self.student_table_model.record().indexOf(name))
self.table_view.verticalHeader().hide()
self.plus_button = QtWidgets.QPushButton(self.tr("+"))
self.minus_button = QtWidgets.QPushButton(self.tr("-"))
self.save_button = QtWidgets.QPushButton(self.tr("Save"))
mapper = QtWidgets.QDataWidgetMapper(
self, submitPolicy=QtWidgets.QDataWidgetMapper.ManualSubmit
)
mapper.setModel(model)
mapper.addMapping(self.title_le, model.record().indexOf("group_name"))
mapper.setCurrentIndex(index)
self.plus_button.clicked.connect(self.addRow)
self.minus_button.clicked.connect(self.removeRow)
self.save_button.clicked.connect(mapper.submit)
self.save_button.clicked.connect(self.accept)
hlay = QtWidgets.QHBoxLayout(self)
vlay = QtWidgets.QVBoxLayout()
vlay.addWidget(self.title_le)
vlay.addWidget(self.table_view)
hlay.addLayout(vlay)
vlay2 = QtWidgets.QVBoxLayout()
vlay2.addWidget(self.plus_button)
vlay2.addWidget(self.minus_button)
vlay2.addWidget(self.save_button)
hlay.addLayout(vlay2)
#property
def group_id(self):
return self._group_id
#QtCore.pyqtSlot()
def addRow(self):
r = self.student_table_model.record()
r.setValue("group_id", self.group_id)
if self.student_table_model.insertRecord(
self.student_table_model.rowCount(), r
):
self.student_table_model.select()
#QtCore.pyqtSlot()
def removeRow(self):
ixs = self.table_view.selectionModel().selectedIndexes()
if ixs:
self.student_table_model.removeRow(ixs[0].row())
self.student_table_model.select()
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self._model = QtSql.QSqlTableModel(self)
self.model.setTable("Groups")
self.model.select()
self.sql_list_view = QtWidgets.QListView()
self.sql_list_view.setModel(self.model)
self.sql_list_view.setModelColumn(self.model.record().indexOf("group_name"))
self.new_button = QtWidgets.QPushButton(self.tr("New"))
self.edit_button = QtWidgets.QPushButton(self.tr("Edit"))
self.remove_button = QtWidgets.QPushButton(self.tr("Remove"))
central_widget = QtWidgets.QWidget()
self.setCentralWidget(central_widget)
grid_layout = QtWidgets.QGridLayout(central_widget)
grid_layout.addWidget(
QtWidgets.QLabel(self.tr("Groups"), alignment=QtCore.Qt.AlignCenter)
)
grid_layout.addWidget(self.sql_list_view, 1, 0)
vlay = QtWidgets.QVBoxLayout()
vlay.addWidget(self.new_button)
vlay.addWidget(self.edit_button)
vlay.addWidget(self.remove_button)
grid_layout.addLayout(vlay, 1, 1)
self.resize(640, 480)
self.new_button.clicked.connect(self.new)
self.edit_button.clicked.connect(self.edit)
self.remove_button.clicked.connect(self.remove)
self.sql_list_view.selectionModel().selectionChanged.connect(
self.onSelectionChanged
)
self.onSelectionChanged()
#property
def model(self):
return self._model
#QtCore.pyqtSlot()
def new(self):
d = AddGroupDialog()
if d.exec_() == QtWidgets.QDialog.Accepted:
r = self.model.record()
r.setValue("group_name", d.title)
if self.model.insertRecord(self.model.rowCount(), r):
self.model.select()
#QtCore.pyqtSlot()
def edit(self):
ixs = self.sql_list_view.selectionModel().selectedIndexes()
if ixs:
d = EditMacroDialog(self.model, ixs[0].row())
d.exec_()
#QtCore.pyqtSlot()
def remove(self):
ixs = self.sql_list_view.selectionModel().selectedIndexes()
if ixs:
row = ixs[0].row()
id_ = self.model.record(row).value("id")
query = QtSql.QSqlQuery()
query.prepare("DELETE FROM Student WHERE group_id = ?")
query.addBindValue(id_)
if not query.exec_():
print(query.lastError().text())
return
self.model.removeRow(row)
self.model.select()
#QtCore.pyqtSlot()
def onSelectionChanged(self):
state = bool(self.sql_list_view.selectionModel().selectedIndexes())
self.edit_button.setEnabled(state)
self.remove_button.setEnabled(state)
if __name__ == "__main__":
import sys
database = "database.db" # ":memory:"
app = QtWidgets.QApplication(sys.argv)
if not create_connection(database):
sys.exit(app.exec_())
w = MainWindow()
w.show()
sys.exit(app.exec_())
What I'm trying to do, is to create a subclass of QTableView that will accept a standardized model (which is the result of a mysql query), and populate it. With it, I'd like to define a custom context menu which allows me to add or remove a row. This approach allows me to define the model and subclass once, and then call them both to populate them with a number of different queries without duplicating code.
The add_row and remove_row QActions would essentially call the model methods insertRows and removeRows.
Here's a functioning example (apart from the mariadb module which is separate):
from PySide2 import QtWidgets, QtCore, Qt, QtGui
import maria_db
import numpy
app = QtWidgets.QApplication([])
#Define model for tablePersonnel
class TableModel(QtCore.QAbstractTableModel):
def __init__(self, db,query,tableName,id):
super(TableModel, self).__init__()
self.db=db
self.query=query
self.tableName=tableName
self.id=id
self.pullData()
def pullData(self):
self.datasheet=self.db.RunQuery(self.query)
self.rows = numpy.array(self.datasheet.fetchall())
self.headers = numpy.array(self.datasheet.column_names)
self.idIndex=int(numpy.where(self.headers==self.id)[0])
def data(self, index, role):
if role == QtCore.Qt.DisplayRole:
return str(self.rows[index.row()][index.column()])
def rowCount(self, parent):
return len(self.rows)
def columnCount(self, parent):
return len(self.headers)
def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
return self.headers[section]
def flags(self,index):
if index.column()!=self.idIndex:
return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsSelectable
else:
return QtCore.Qt.ItemIsSelectable
def setData(self, index,value,role=QtCore.Qt.EditRole):
if index.isValid():
sel_id=self.rows[index.row()][0]
sel_column=self.headers[index.column()]
if self.db.UpdateCell(self.tableName,sel_id,self.id,sel_column,value)==True:
self.rows[self.idFinder(self.rows,sel_id)][1]=value
self.dataChanged.emit(index,index)
return True
def insertRows(self):
print("inserting row!!!")
def removeRows(self):
pass
#find index of selected ID
def idFinder(self,data, search):
for i in range(len(data)):
for j in range(len(data[i])):
if data[i][j] == search:
return i
else:
return False
class EditTable(QtWidgets.QTableView):
def __init__(self):
super(EditTable, self).__init__()
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self.context_menu)
def context_menu(self):
context=QtWidgets.QMenu()
add_row=context.addAction("Add row")
add_row.triggered.connect(lambda: self.personnelTableModel.insertRows())
rem_row=context.addAction("Remove row")
rem_row.triggered.connect(lambda: self.personnelTableModel.removeRows())
cursor=QtGui.QCursor()
context.exec_(cursor.pos())
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
widget = QtWidgets.QWidget()
layout = QtWidgets.QVBoxLayout()
widget.setLayout(layout)
button = QtWidgets.QPushButton("Push me")
self.db = maria_db.ConnectDb("localhost", "root", "1234", "test")
self.personnelTableModel = TableModel(self.db,"SELECT * FROM personnel","personnel","personnel_id")
table = EditTable()
table.setModel(self.personnelTableModel)
layout.addWidget(table)
layout.addWidget(button)
self.setCentralWidget(widget)
window = MainWindow()
window.show()
app.exec_()
So my problem is that when I call add_row.triggered.connect(lambda: self.personnelTableModel.insertRows()), I get a 'EditTable' object has no attribute 'personnelTableModel' which essentially means that there is no method insertRows in self. How can I refer to the model assigned to the object that is constructed using the subclass of the QTableView EditTable, through the subclass itself? Am I going about this the wrong way?
What is personnelTableModel for EditTable? the model, and how can I access the model from the view? Using the model() method. On the other hand, a lambda method is not necessary.
def context_menu(self):
context=QtWidgets.QMenu()
add_row=context.addAction("Add row")
add_row.triggered.connect(self.model().insertRows)
rem_row=context.addAction("Remove row")
rem_row.triggered.connect(self.model().removeRows)
cursor=QtGui.QCursor()
context.exec_(cursor.pos())
I'm creating a GUI in PyQT5 that should provide multiple views on data from multiple related SQLite tables. I have implemented these views to be displayed via a QStackedWidget.
Now, some of these views are for overview purposes, others for more detailled views on a subset of the data displayed in the overviews. I want to access the detailled views from the overviews via rightclick.
I have included a minimal example with cars below. (Sorry, it's a bit long, but that was needed to provide a full working example. I have reduced it as much as I could.) The goal is to show the DetailledView with the cars of the company selected in the Overview.
This already provides access from the Overview to the DetailledView on Rightclick, but the company information is not passed along. So even when accessing the DetailledView from 'VW', self.mycompany gets updated but car_widget doesn't, so the DetailledView shows the info about 'Honda' cars (the default).
Is there a way to update car_widgit with the right company? Or do I need to create the DetailledView at runtime? (Does that even work? But I would prefer not to, anyway, as it could make the index of the Stack unreliable...)
How can I update the QTableModel of one view in a QStackedWidget according to what was selected in another view?
Here's the code:
#!/usr/bin/python3
from PyQt5 import QtSql
from PyQt5.QtWidgets import (QMainWindow, QWidget, QApplication, QGridLayout,
QStackedWidget, QTableView, QMenu)
from PyQt5.QtCore import Qt, QModelIndex
import sys
class MainGUI(QMainWindow):
def __init__(self):
super().__init__()
self.mycompany = "Honda"
self.create_connection()
self.fill_tables()
self.init_UI()
def create_connection(self):
self.db = QtSql.QSqlDatabase.addDatabase("QSQLITE")
self.db.setDatabaseName("test.db")
if not self.db.open():
print("Cannot establish a database connection to {}!".format(self.db_file))
return False
def fill_tables(self):
self.db.transaction()
q = QtSql.QSqlQuery()
q.exec_("DROP TABLE IF EXISTS Manufacturers;")
q.exec_("CREATE TABLE Manufacturers (CompanyId INT PRIMARY KEY, Name TEXT, Country TEXT);")
q.exec_("INSERT INTO Manufacturers VALUES (1, 'VW', 'Germany');")
q.exec_("INSERT INTO Manufacturers VALUES (2, 'Honda' , 'Japan');")
q.exec_("DROP TABLE IF EXISTS Cars;")
q.exec_("CREATE TABLE Cars (Model TEXT, Year INT, Company INT);")
q.exec_("INSERT INTO Cars VALUES ('Civic', 2009, 'Honda');")
q.exec_("INSERT INTO Cars VALUES ('Golf', 2013, 'VW');")
q.exec_("INSERT INTO Cars VALUES ('Polo', 1999, 'VW');")
self.db.commit()
def init_UI(self):
self.central_widget = QWidget()
self.setCentralWidget(self.central_widget)
self.grid = QGridLayout()
self.central_widget.setLayout(self.grid)
self.setLayout(self.grid)
self.make_stack()
self.show()
def make_stack(self):
self.Stack = QStackedWidget(self)
company_view = QWidget()
layout = QGridLayout()
company_view.setLayout(layout)
self.company_widget = Overview()
layout.addWidget(self.company_widget, 0, 0)
self.company_widget.table.customContextMenuRequested.connect(self.open_menu)
self.Stack.addWidget(company_view)
car_view = QWidget()
layout2 = QGridLayout()
car_view.setLayout(layout2)
car_widget = DetailedView(self.mycompany)
layout2.addWidget(car_widget, 0, 0)
self.Stack.addWidget(car_view)
self.grid.addWidget(self.Stack, 0,1)
def open_menu(self, pos):
menu = QMenu()
show_act = menu.addAction("Show cars")
action = menu.exec_(self.company_widget.table.mapToGlobal(pos))
if action == show_act:
row = self.company_widget.table.indexAt(pos).row()
myindex = self.company_widget.model.index(row, 1, QModelIndex())
company = self.company_widget.model.data(myindex)
self.mycompany = company
self.Stack.setCurrentIndex(1)
class MyTable(QWidget):
def __init__(self):
super().__init__()
self.create_connection()
self.create_model()
self.init_UI()
def create_connection(self):
self.db = QtSql.QSqlDatabase.addDatabase("QSQLITE")
self.db.setDatabaseName("test.db")
if not self.db.open():
print("Cannot establish a database connection to {}!".format(self.db_file))
return False
def create_model(self):
self.model = None
def init_UI(self):
self.grid = QGridLayout()
self.setLayout(self.grid)
self.table = QTableView()
self.table.setModel(self.model)
self.table.setContextMenuPolicy(Qt.CustomContextMenu)
self.grid.addWidget(self.table, 0, 0)
def closeEvent(self, e):
if (self.db.open()):
self.db.close()
def check_error(self, q):
lasterr = q.lastError()
if lasterr.isValid():
print(lasterr.text())
self.db.close()
exit(1)
class Overview(MyTable):
def __init__(self):
super().__init__()
def create_model(self):
self.model = QtSql.QSqlTableModel()
q = QtSql.QSqlQuery()
query = "SELECT * from Manufacturers"
q.exec_(query)
self.model.setQuery(q)
class DetailedView(MyTable):
def __init__(self, company):
self.company = company
super().__init__()
def create_model(self):
self.model = QtSql.QSqlTableModel()
q = QtSql.QSqlQuery()
query = "SELECT * from cars where company = '{}'".format(self.company)
q.exec_(query)
self.model.setQuery(q)
def main():
app = QApplication(sys.argv)
ex = MainGUI()
ex.show()
result = app.exec_()
sys.exit(result)
if __name__ == '__main__':
main()
One of the objectives of the inheritance is that the class implements the common tasks that the children can do, and in your case you do not observe those tasks, for example the creation of the model must be done in the father since all the children will do it.
On the other hand the goal of using QSqlTableModel is not to use the QSqlQuery but more friendly requests like setTable(), select() and setFilter(), but it is simply a waste because you could use QSqlQueryModel.
On the other hand I see that you are assuming that the self.mycompany of MainGui is the same as the self.company of DetailedView for what you are going through in the creation of the DetailView object, and the truth is that they are not the same, in the creation of the object only the value of that moment has been copied, so if you change the self.company of MainGui it will not change the self.company of DetailView.
Restructuring your project you get the following:
#!/usr/bin/python3
from PyQt5.QtSql import QSqlDatabase, QSqlQuery, QSqlTableModel
from PyQt5.QtWidgets import (QMainWindow, QWidget, QApplication, QVBoxLayout,
QStackedWidget, QTableView, QMenu)
from PyQt5.QtCore import Qt, QModelIndex, pyqtSignal
import sys
DB_PATH = "test.db"
def create_connection():
db = QSqlDatabase.addDatabase("QSQLITE")
db.setDatabaseName(DB_PATH)
if not db.open():
print("Cannot establish a database connection to {}!".format(DB_PATH))
return False
return True
def fill_tables():
q = QSqlQuery()
q.exec_("DROP TABLE IF EXISTS Manufacturers;")
q.exec_("CREATE TABLE Manufacturers (CompanyId INT PRIMARY KEY, Name TEXT, Country TEXT);")
q.exec_("INSERT INTO Manufacturers VALUES (1, 'VW', 'Germany');")
q.exec_("INSERT INTO Manufacturers VALUES (2, 'Honda' , 'Japan');")
q.exec_("DROP TABLE IF EXISTS Cars;")
q.exec_("CREATE TABLE Cars (Model TEXT, Year INT, Company INT);")
q.exec_("INSERT INTO Cars VALUES ('Civic', 2009, 'Honda');")
q.exec_("INSERT INTO Cars VALUES ('Golf', 2013, 'VW');")
q.exec_("INSERT INTO Cars VALUES ('Polo', 1999, 'VW');")
class MainGUI(QMainWindow):
def __init__(self):
super().__init__()
self.init_UI()
def init_UI(self):
self.central_widget = QWidget()
self.setCentralWidget(self.central_widget)
self.lay = QVBoxLayout(self.central_widget)
self.make_stack()
def make_stack(self):
self.stack = QStackedWidget()
self.lay.addWidget(self.stack)
self.company_widget = Overview()
self.car_widget = DetailedView()
self.stack.addWidget(self.company_widget)
self.stack.addWidget(self.car_widget)
self.company_widget.changedCompany.connect(self.changedCompany)
self.car_widget.backSignal.connect(lambda: self.stack.setCurrentIndex(0))
def changedCompany(self, company):
self.car_widget.filter(company)
self.stack.setCurrentIndex(1)
class SQLTable(QTableView):
def __init__(self, table):
super().__init__()
self.init_UI()
self.create_model(table)
def create_model(self, table):
self.model.setTable(table)
self.model.select()
def init_UI(self):
self.model = QSqlTableModel()
self.setModel(self.model)
self.setContextMenuPolicy(Qt.CustomContextMenu)
class Overview(SQLTable):
changedCompany = pyqtSignal(str)
def __init__(self):
SQLTable.__init__(self, "Manufacturers")
self.customContextMenuRequested.connect(self.open_menu)
def open_menu(self, pos):
menu = QMenu()
show_act = menu.addAction("Show cars")
action = menu.exec_(self.mapToGlobal(pos))
if action == show_act:
row = self.indexAt(pos).row()
ix = myindex = self.model.index(row, 1)
company = self.model.data(ix)
self.changedCompany.emit(company)
class DetailedView(SQLTable):
backSignal = pyqtSignal()
def __init__(self):
SQLTable.__init__(self, "cars")
self.customContextMenuRequested.connect(self.open_menu)
def open_menu(self, pos):
menu = QMenu()
back_act = menu.addAction("Show Manufacturers")
action = menu.exec_(self.mapToGlobal(pos))
if action == back_act:
self.backSignal.emit()
def filter(self, company):
self.model.setFilter("company='{}'".format(company))
def main():
app = QApplication(sys.argv)
if not create_connection():
sys.exit(-1)
fill_tables()
ex = MainGUI()
ex.show()
result = app.exec_()
sys.exit(result)
if __name__ == '__main__':
main()