QTableView + QSqlTableModel - Изменение значений в ячейках

Нужна помощь по настройке и редактированию таблицы для моего приложения. Есть база данных SQlite с одной простой таблицей, из которой берутся значения и вставляются в таблицу QTableView. Я создал кнопки "Add", "Change", "Delete" для редактированию строк в таблице. Но они работают неправильно.

  1. Как сохранить добавленную строку в БД? После перезагрузки приложения новая строка не отображается и не создается в БД.

  2. Не получается отредактировать уже имеющиеся строки. После ввода данных все значения пропадают и появляется знак !

  3. Не получается внести изменения с помощью кнопки "Change", выходит ошибка "edit: editing failed".

Я сделал минимальный пример моего приложения. Пример создает в папке с приложением маленькую базу данных SQlite3, состоящую из 1 таблицы и 2 строк.

main.py

import sys, os, sqlite3
from datetime import datetime 
from PyQt5 import QtWidgets
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtSql import *
from PyQt5.QtCore import *

CONFIG_NAME = 'config.ini'
DB_NAME = 'nsi.db'

class MainWindow(QtWidgets.QMainWindow):
    def __init__(self, parent=None):
        super().__init__() 
        self.window_pref()
        self.show_widgets()

    def window_pref(self):
        self.setWindowTitle('PyQt5 APP')
        self.def_width = 800
        self.def_height = 400
        self.def_size = self.setMinimumSize(self.def_width, self.def_height)

    def show_widgets(self):
        self.createConnection()        
        self.fillDB()       
        self.setupMainWidgets()  

    def createConnection(self):
        db = QSqlDatabase.addDatabase("QSQLITE")
        db.setDatabaseName(DB_NAME)

        if not db.open():
            QMessageBox.warning(self, 'PyQt5 APP', 
                'Error:{}'.format(db.lastError().text()))
            sys.exit(1)

    def fillDB(self):
        query = QSqlQuery()
        query.exec_("""\
            CREATE TABLE sprav (
                id_nsi INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
                nsi_name TEXT UNIQUE NOT NULL,
                file_date TEXT NOT NULL,
                file_name TEXT NOT NULL)
                """)

        query.prepare("""\
            INSERT INTO sprav (nsi_name, file_date, file_name)VALUES (?, ?, ?)
            """)


        sample_list = (('nsi1', 'january', 'file1'), ('nsi2', 'may', 'file2'))

        for i in sample_list:
            query.addBindValue(i[0])           
            query.addBindValue(i[1]) 
            query.addBindValue(i[2])    
            query.exec_()
            
    def setupMainWidgets(self):
        mw_widget = QWidget()               
        main_panel = QHBoxLayout(mw_widget) 

        # SQL Table
        self.modelSql = QSqlTableModel()
        self.modelSql.setTable('sprav') 

        self.modelSql.setQuery(QSqlQuery(
                    'SELECT nsi_name, file_date, file_name FROM sprav'))

        self.modelSql.setHeaderData(self.modelSql.fieldIndex('nsi_name'),
                                Qt.Horizontal, 'Name')
        self.modelSql.setHeaderData(self.modelSql.fieldIndex('file_date'),
                                Qt.Horizontal, 'Date')
        self.modelSql.setHeaderData(self.modelSql.fieldIndex('file_name'),
                                Qt.Horizontal, 'File')
        self.modelSql.setEditStrategy(QSqlTableModel.OnFieldChange)
        self.modelSql.select()

        # QTableView() 
        self.table_view = QTableView()
        self.table_view.setSelectionBehavior(1) 
        self.table_view.setAlternatingRowColors(True) 
        self.table_view.setModel(self.modelSql)     

        self.table_view.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch)

        main_panel.addWidget(self.table_view)

        # QVBoxLayout()
        right_panel = QVBoxLayout() 
        line = QFrame()
        line.setFrameShape(QFrame.HLine)

        self.add_record = QPushButton('Add', self)
        self.add_record.clicked.connect(self.addRecord)

        self.change_record = QPushButton('Change', self)
        self.change_record.clicked.connect(self.changeRecord)      
        self.delete_record = QPushButton('Delete', self)
        self.delete_record.clicked.connect(self.delRecord)

        right_panel.addSpacing(20)
        right_panel.addWidget(line)
        right_panel.addWidget(self.add_record)
        right_panel.addWidget(self.change_record)
        right_panel.addWidget(self.delete_record)
        right_panel.addStretch()

        main_panel.addLayout(right_panel) 

        self.setCentralWidget(mw_widget)

    def addRecord(self):
        row = self.modelSql.rowCount()
        self.modelSql.insertRow(row)

        index = self.modelSql.index(row, 0)
        self.table_view.setCurrentIndex(index) 
        self.table_view.edit(index)

    def delRecord(self):
        cur_item = self.table_view.selectedIndexes()
        for index in cur_item:
            self.modelSql.removeRow(index.row())
        self.modelSql.select()

    def changeRecord(self):
        self.table_view.edit(self.table_view.currentIndex())      

if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec_())

Ответы (1 шт):

Автор решения: S. Nick

Попробуйте так:

import sys
import os
import sqlite3
from datetime import datetime 
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.Qt import *
from PyQt5.QtSql import *


CONFIG_NAME = 'config.ini'
DB_NAME = 'nsi.db'

# +++ vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
class Dialog(QDialog):
    def __init__(self):
        super().__init__()
        self.setWindowTitle('Input Dialog')
        self.line_edit_name = QLineEdit()
        self.line_edit_date = QLineEdit()   
        self.line_edit_file = QLineEdit()   

        form_layout = QFormLayout()
        form_layout.addRow('Name:', self.line_edit_name)
        form_layout.addRow('Date:', self.line_edit_date)
        form_layout.addRow('File:', self.line_edit_file)

        button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
        button_box.accepted.connect(self.accept)
        button_box.rejected.connect(self.reject)

        main_layout = QVBoxLayout()
        main_layout.addLayout(form_layout)
        main_layout.addWidget(button_box)
        self.setLayout(main_layout)
# +++ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^        

class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()
        
        self.window_pref()
        self.show_widgets()

    def window_pref(self):
        self.setWindowTitle('PyQt5 APP')
        self.def_width = 800
        self.def_height = 400
        self.def_size = self.setMinimumSize(self.def_width, self.def_height)

    def show_widgets(self):
        self.createConnection()        
        self.fillDB()       
        self.setupMainWidgets()  

    def createConnection(self):
        db = QSqlDatabase.addDatabase("QSQLITE")
        db.setDatabaseName(DB_NAME)

        if not db.open():
            QMessageBox.warning(self, 'PyQt5 APP', 
                'Error:{}'.format(db.lastError().text()))
            sys.exit(1)

    def fillDB(self):
        query = QSqlQuery()
        query.exec_("""\
            CREATE TABLE sprav (
                id_nsi INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
                nsi_name TEXT UNIQUE NOT NULL,
                file_date TEXT NOT NULL,
                file_name TEXT NOT NULL)
                """)

        query.prepare("""\
            INSERT INTO sprav (nsi_name, file_date, file_name)VALUES (?, ?, ?)
            """)

        sample_list = (('nsi1', 'january', 'file1'), ('nsi2', 'may', 'file2'))
        for i in sample_list:
            print(i)
            query.addBindValue(i[0])           
            query.addBindValue(i[1]) 
            query.addBindValue(i[2])    
            query.exec_()
            
    def setupMainWidgets(self):
        mw_widget = QWidget()               
        main_panel = QHBoxLayout(mw_widget) 

        # QSqlTableModel
        self.modelSql = QSqlTableModel()           
        self.modelSql.setTable('sprav') 
        self.modelSql.setEditStrategy(QSqlTableModel.OnFieldChange)
        self.modelSql.select()
        
# -       self.modelSql.setQuery(QSqlQuery(
# -                   'SELECT nsi_name, file_date, file_name FROM sprav'))

        self.modelSql.setHeaderData(0, Qt.Horizontal, "Id")
        self.modelSql.setHeaderData(1, Qt.Horizontal, 'Name')
        self.modelSql.setHeaderData(2, Qt.Horizontal, 'Date')
        self.modelSql.setHeaderData(3, Qt.Horizontal, 'File')

        # QTableView() 
        self.table_view = QTableView()
        self.table_view.setSelectionBehavior(1) 
        self.table_view.setAlternatingRowColors(True) 
        self.table_view.setModel(self.modelSql)  
        
        # https://doc.qt.io/qt-5/qtableview.html#setColumnHidden
        self.table_view.setColumnHidden(0, True)            

# ???        [self.table_view.resizeColumnToContents(i) for i in (1, 2, 3)] 
        self.table_view.horizontalHeader().setSectionResizeMode(1, QHeaderView.Stretch)
        
        
# ??? -        self.table_view.selectionModel().selectionChanged.connect(
# ??? -                                           self.on_selection_changed)

        main_panel.addWidget(self.table_view)

        # QVBoxLayout()
        right_panel = QVBoxLayout() 
        line = QFrame()
        line.setFrameShape(QFrame.HLine)

        self.add_record = QPushButton('Add', self)
        self.add_record.clicked.connect(self.addRecord)

        self.change_record = QPushButton('Change', self)
        self.change_record.clicked.connect(self.changeRecord)
#        self.change_record.setEnabled(False) 

        self.delete_record = QPushButton('Delete', self)
        self.delete_record.clicked.connect(self.delRecord)
#         self.delete_record.setEnabled(False)

        right_panel.addSpacing(20)
        right_panel.addWidget(line)
        right_panel.addWidget(self.add_record)
        right_panel.addWidget(self.change_record)
        right_panel.addWidget(self.delete_record)
        right_panel.addStretch()

        main_panel.addLayout(right_panel) 

        self.setCentralWidget(mw_widget)
        self.add_record.setFocus()                              # +++

    def addRecord(self):
        '''
        row = self.modelSql.rowCount()
        self.modelSql.insertRow(row)
        index = self.modelSql.index(row, 0)
        self.table_view.setCurrentIndex(index) 
        self.table_view.edit(index)
        '''        
        inputDialog = Dialog()
        rez = inputDialog.exec()
        if not rez:
            msg = QMessageBox.information(self, 'Внимание', 'Диалог сброшен.')
            return  
        name = inputDialog.line_edit_name.text()
        date = inputDialog.line_edit_date.text()
        file = inputDialog.line_edit_file.text()
        if not name or not date or not file:
            msg = QMessageBox.information(self, 'Внимание', 'Заполните пожалуйста все поля.')
            return             
    
        r = self.modelSql.record()
        r.setValue("nsi_name", name)
        r.setValue("file_date", date)
        r.setValue("file_name", file)
        self.modelSql.insertRecord(-1, r)
        self.modelSql.select()        

    def delRecord(self):
        '''
        cur_item = self.table_view.selectedIndexes()
        for index in cur_item:
            self.modelSql.removeRow(index.row())
        self.modelSql.select()
        '''
       
        row = self.table_view.currentIndex().row()
        if row == -1:
            msg = QMessageBox.information(self, 'Внимание', 'Выберите запись для удаления.')
            return          

        name = self.modelSql.record(row).value(1)
        date = self.modelSql.record(row).value(2)
        file = self.modelSql.record(row).value(3)
        
        inputDialog = Dialog()
        inputDialog.setWindowTitle('Удалить запись ???')
        inputDialog.line_edit_name.setText(name)
        inputDialog.line_edit_date.setText(date)
        inputDialog.line_edit_file.setText(file)       
        rez = inputDialog.exec()
        if not rez:
            msg = QMessageBox.information(self, 'Внимание', 'Диалог сброшен.')
            return  

        self.modelSql.removeRow(self.table_view.currentIndex().row())              
        self.modelSql.select()                                             
        
        msg = QMessageBox.information(self, 'Успех', 'Запись удалена.')

    def changeRecord(self):
        self.table_view.edit(self.table_view.currentIndex()) 
    

''' ??? 
    def getDirectory(self):                                              
        dirlist = QFileDialog.getExistingDirectory(self, "Choose folder")
        return dirlist
     
    def on_selection_changed(self):
        self.change_record.setEnabled(
            bool(self.table_view.selectionModel().selectedRows()))
        self.delete_record.setEnabled(
            bool(self.table_view.selectionModel().selectedRows()))
'''


if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec_())

введите сюда описание изображения

введите сюда описание изображения

введите сюда описание изображения


Update:

введите сюда описание изображения

Я попробовал ваши вариант, проблема осталась - если удалить строку, а затем открыть приложение заново, то строка появляется снова. Надо чтобы изменения сохранялись в БД. Причем, если добавить запись, то при повторном открытии новая запись отобразится. Не сохраняется только удаленная запись.

  1. Вы про удаление в своем вопросе ничего не спрашивали и это от меня шло вам бонусом.

  2. Все работает как надо.

  3. Вы видимо удаляете одну из записей, которую в методе fillDB() постоянно создаете:

     sample_list = (('nsi1', 'january', 'file1'), ('nsi2', 'may', 'file2'))
     for i in sample_list:
         print(i)
         query.addBindValue(i[0])           
         query.addBindValue(i[1]) 
         query.addBindValue(i[2])    
         query.exec_()
    

Удалите вызов метода:

#        self.fillDB()

после того как вы первый раз создали таблицу.

→ Ссылка