Инвертирование столбцов и строк в QTableView с использованием PyQt5

Для виджета PyQT5 мне нужно отображать данные из SQL-запроса в базу данных SQLite с инвертированными/повернутыми столбцами и строками. В идеале в QTableView. (В этой таблице будет только 2 столбца: один для имен предыдущих столбцов и один для их значений. Таблица предназначена для отображения статистики, которая будет агрегирована в запросе SQL, который вернет только одну строку. Поэтому я хочу перейти от одна строка с несколькими столбцами, до 2 столбцов с несколькими строками.)

Я придумал обходной путь, который делает правильные вещи, используя вместо этого QFormLayout, но он выглядит уродливым и кажется очень неэлегантным. (См. метод display_data(self).)

#!/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()      

Каков правильный способ сделать это с помощью QTableView?


person CodingCat    schedule 27.03.2018    source источник
comment
Я действительно не понимаю, в чем проблема. Почему нельзя просто заполнить таблицу нужными значениями? Почему вы решили, что использование макета формы будет необходимо?   -  person ekhumoro    schedule 27.03.2018
comment
@ekhumoro: да, я могу это сделать. Стало казаться неуклюжим и неэлегантным запрашивать, а затем заполнять таблицу, но я не знал, как заставить QTableModel вести себя так, как я хочу, поэтому я использовал FormLayout, чтобы проиллюстрировать, что мне нужно.   -  person CodingCat    schedule 28.03.2018
comment
Было бы намного проще заполнить таблицу с помощью QStandardItemModel - все, что вам нужно сделать, это инвертировать обычные циклы строк/столбцов. Так что я до сих пор не понимаю, в чем смысл макета формы. (И, конечно, я знаю, что это не самый эффективный способ взаимодействия с базой данных - мне просто было любопытно, почему вы не можете использовать таблицу).   -  person ekhumoro    schedule 28.03.2018


Ответы (2)


Правильным способом работы с QTableView было бы иметь файл QTableModel.

Как назло, существует QSqlTableModel, который позволяет вам построить табличная модель по сравнению с таблицей SQL. Funt ответил на аналогичный вопрос, указав на QIdentityProxyModel, который можно использовать "помимо этого" для изменения представления модели данных путем переопределения методов mapToSource и mapFromSource.

Существуют также способы транспонирования результата SQL-запроса непосредственно из команды SQL. См. здесь.

Также стоит прочитать: Программирование Model-View с помощью Qt. Это версия C++, но PyQt следует тем же принципам (и классы имеют одно и то же имя).

Надеюсь, это поможет.

person PlikPlok    schedule 27.03.2018
comment
Спасибо за указатели. (Особенно последний был полезен - большая часть документации Qt не очень удобна для начинающих, и трудно найти эти сложные части.) Я действительно надеялся, что любая из ProxyModels Qt предоставляет переворачивание столбцов и строк в качестве опции, не имея в подкласс, так как мне кажется, что это, вероятно, распространенная проблема. - person CodingCat; 28.03.2018

После еще нескольких поисков и чтения полезных указателей, оставленных @PlikPlok, я нашел решение здесь:

Судя по всему, эта функциональность не предоставляется никакими Qt-классами из коробки, поэтому вам нужно создать подкласс как QAbstractProxyModel, так и QSqlRelationalDelegate, а затем использовать их в своей таблице:

#!/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()      
person CodingCat    schedule 28.03.2018