PyQt5とpython3によるGUIプログラミング［４］

#MVCサンプル
ここの動画をお勧めします。ソースもついています。
PyQt4で書かれていますが、PyQt5に書き換えるのは難しくありません。
youTubeで「PyQt4 Model View Tutorial Part」と打ち込む事で動画がリストされます。

ここを参考にこれを作ってみました。

```python:MyTableModel.py
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.Qt import *
import sys

class MyTableModel(QAbstractTableModel):
    def __init__(self, list, headers = [], parent = None):
        QAbstractTableModel.__init__(self, parent)
        self.list = list
        self.headers = headers

    def rowCount(self, parent):
        return len(self.list)

    def columnCount(self, parent):
        return len(self.list[0])

    def flags(self, index):
        return Qt.ItemIsEditable | Qt.ItemIsEnabled | Qt.ItemIsSelectable

    def data(self, index, role):
        if role == Qt.EditRole:
            row = index.row()
            column = index.column()
            return self.list[row][column]

        if role == Qt.DisplayRole:
            row = index.row()
            column = index.column()
            value = self.list[row][column]
            return value

    def setData(self, index, value, role = Qt.EditRole):
        if role == Qt.EditRole:
            row = index.row()
            column = index.column()
            self.list[row][column] = value
            self.dataChanged.emit(index, index)
            return True
        return False

    def headerData(self, section, orientation, role):

        if role == Qt.DisplayRole:

            if orientation == Qt.Horizontal:

                if section < len(self.headers):
                    return self.headers[section]
                else:
                    return "not implemented"
            else:
                return "item %d" % section

if __name__ == '__main__':

    app = QApplication(sys.argv)
    app.setStyle("plastique")

    listView = QListView()
    listView.show()

    comboBox = QComboBox()
    comboBox.show()


    tableView = QTableView()
    tableView.show()

    headers = ["000", "001", "002"]
    tableData0 = [
                 ['abc',100,200],
                 ['fff',130,260],
                 ['jjj',190,300],
                 ['ppp',700,500],
                 ['yyy',800,900]
                 ]

    model = MyTableModel(tableData0, headers)

    listView.setModel(model)
    comboBox.setModel(model)
    tableView.setModel(model)

    sys.exit(app.exec_())
```

<img width="741" alt="スクリーンショット 2016-07-09 10.05.51.png" src="https://qiita-image-store.s3.amazonaws.com/0/124460/d6965b05-82a5-1bd5-0748-51f771871523.png">

書き換えも可能です。

<img width="428" alt="スクリーンショット 2016-07-09 10.09.22.png" src="https://qiita-image-store.s3.amazonaws.com/0/124460/00d18b01-0662-fb02-560b-17c3a6816c8c.png">

数値のところはデフォルトのDelegateが働いてスピンボックスになっています。

#ComboBoxのDelegate

```python:ComboDelegate.py
import sys
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *

class ComboDelegate(QItemDelegate):
    comboItems=['コンボ--0', 'コンボ--1','コンボ--2']
    def createEditor(self, parent, option, proxyModelIndex):
        combo = QComboBox(parent)
        combo.addItems(self.comboItems)
        combo.currentIndexChanged.connect(self.currentIndexChanged)
        return combo

    def setModelData(self, combo, model, index):
        comboIndex=combo.currentIndex()
        text=self.comboItems[comboIndex]        
        model.setData(index, text)

    @pyqtSlot()
    def currentIndexChanged(self): 
        self.commitData.emit(self.sender())

class MyModel(QAbstractTableModel):
    def __init__(self, parent=None, *args):
        QAbstractTableModel.__init__(self, parent, *args)
        self.items=['Item01','Item02','Item03']

    def rowCount(self, parent=QModelIndex()):
        return len(self.items)

    def columnCount(self, parent=QModelIndex()):
        return 1

    def data(self, index, role):        
        if not index.isValid(): return QVariant()

        row=index.row()
        item=self.items[row]

        if row>len(self.items): return QVariant()

        if role == Qt.DisplayRole:
            return QVariant(item) 

    def flags(self, index):
        return Qt.ItemIsEditable | Qt.ItemIsEnabled

    def setData(self, index, text):
        self.items[index.row()]=text

if __name__ == '__main__':
    app = QApplication(sys.argv)

    model = MyModel()
    tableView = QTableView()
    tableView.setModel(model)

    delegate = ComboDelegate()

    tableView.setItemDelegate(delegate)
    tableView.resizeRowsToContents()

    tableView.show()
    sys.exit(app.exec_())
```

<img width="368" alt="スクリーンショット 2016-07-09 10.25.06.png" src="https://qiita-image-store.s3.amazonaws.com/0/124460/e0e7256f-0e9a-838c-da66-1dc04c757e60.png">

#table model viewで新規作成、オープン、保存、行追加、行削除を実装してみた

保存はcsv形式です。
行の挿入は選択した行セルの上の行に追加します。
何も選択なしにボタンを押した場合は最終行の後に追加します。
削除は、基本選択した行、何も選択なしにボタンを押した場合は最終行を削除します。

とりあえずはサンプル提示で、解説は追ってしていきます。

```python:View_TableModel.py
from PyQt5.QtGui import *
from PyQt5.Qt import *
import sys, csv

class Window (QWidget):
    def __init__ (self):
        QWidget.__init__(self, None)
        self.setWindowTitle('Table')

class View (Window):
    def __init__ (self, parent=None):
        super(View, self).__init__()

        #サイズ固定
        self.setFixedSize(850, 500)

        #Viewを作成
        self.tableView = QTableView()
        self.tableView.clicked.connect(self.viewClicked)

        #Viewの罫をブラックにする
        self.tableView.setStyleSheet("QTableView{gridline-color: black}")

        self.headers = ["▽", "AAAAAA", "BBBBBB", "CCCCCCC", "DDDDD", "EEEEE", "FFFFF", "GGGGG"]
        tableData0 = [
                     [QCheckBox(''), "ああああああ", "てすと", "ﾃｽﾄ", "評価", "000000", "000000", 1],
                     ]

        #モデルを作成
        self.model = MyTableModel(tableData0, self.headers)
        self.tableView.setModel(self.model)

        #Insert、remove用の選択行に行の最大値をセット
        self.selectRow = self.model.rowCount(QModelIndex())

        #csv用のファイルフィルタをセット
        self.filters = "CSV files (*.csv)"

        #ファイル名を初期化
        self.fileName = None

        #ボタン作成
        self.buttonNew = QPushButton('NEW', self)
        self.buttonOpen = QPushButton('Open', self)
        self.buttonSave = QPushButton('Save', self)
        self.buttonAdd = QPushButton('add', self)
        self.buttonDell = QPushButton('Dell', self)

        #ボタングループをセット
        self.group = QButtonGroup()
        self.group.addButton(self.buttonNew)
        self.group.addButton(self.buttonOpen)
        self.group.addButton(self.buttonSave)
        self.group.addButton(self.buttonAdd)
        self.group.addButton(self.buttonDell)

        #Signal、Slotを設定
        self.buttonNew.clicked.connect(self.handleNew)
        self.buttonOpen.clicked.connect(self.handleOpen)
        self.buttonSave.clicked.connect(self.handleSave)
        self.buttonAdd.clicked.connect(self.insertRows)
        self.buttonDell.clicked.connect(self.removeRows)

        #水平レイアウトを設定
        layout = QHBoxLayout()

        layout.addWidget(self.buttonNew)
        layout.addWidget(self.buttonOpen)
        layout.addWidget(self.buttonSave)
        layout.addWidget(self.buttonAdd)
        layout.addWidget(self.buttonDell)

        #垂直レイアウトを設定
        Vlayout = QVBoxLayout()
        Vlayout.addWidget(self.tableView)
        Vlayout.addLayout(layout)

        #全体のレイアウトをセット
        self.setLayout(Vlayout)

    #保存処理→CSVで保存
    def handleSave(self):
        print("handleSave")
        if self.fileName == None or self.fileName == '':
            self.fileName, self.filters = QFileDialog.getSaveFileName(self, \
            filter=self.filters)
        if(self.fileName != ''):
            with open(self.fileName, 'wt') as stream:
                csvout = csv.writer(stream, lineterminator='\n')
                csvout.writerow(self.headers)
                for row in range(self.model.rowCount(QModelIndex())):
                    print(self.model.rowCount(QModelIndex()))
                    rowdata = []
                    for column in range(self.model.columnCount(QModelIndex())):
                        item = self.model.index( row, column, QModelIndex() ).data( Qt.DisplayRole )
                        if column == 0:
                            rowdata.append('')
                            continue

                        if item is not None:
                            rowdata.append(item)
                        else:
                            rowdata.append('')
                    csvout.writerow(rowdata)
                    print(rowdata)

    #ファイルオープン処理
    def handleOpen(self):
        print("handleOpen")
        self.fileName, self.filterName = QFileDialog.getOpenFileName(self)

        if self.fileName != '':
            with open(self.fileName, 'r') as f:
                reader = csv.reader(f)
                header = next(reader)
                buf = []
                for row in reader:
                    row[0] = QCheckBox("-")
                    buf.append(row)

                self.model = None
                self.model = MyTableModel(buf, self.headers)
                self.tableView.setModel(self.model)
                self.fileName = ''

    #新規作成
    def handleNew(self):
        print ("handleNew")
        self.fileName = ''

        defaultValue =[
        [QCheckBox(''), "ああああああ", "てすと", "ﾃｽﾄ", "評価", "000000", "000000", 1]
        ]

        self.model = None
        self.model = MyTableModel(defaultValue, self.headers)
        print(defaultValue)
        self.tableView.setModel(self.model)

    #Viewとモデルに行追加→選択されている行の上に1行挿入します
    def insertRows(self, position, rows=1, index=QModelIndex()):
        print("position: %d"%position)
        print("rows: %d" % rows)
        print("rowCount: %d" % self.model.rowCount(QModelIndex()))

        position = self.selectRow
        self.model.beginInsertRows(QModelIndex(), position, position + rows - 1)
        for row in range(rows):
            self.model.list.insert(position, [QCheckBox(''), "ああああああ", "てすと", "ﾃｽﾄ", "評価", "000000", "000000", 1])

        self.model.endInsertRows()
        return True

    #Viewとモデルから行削除→選択位置の行を削除します
    def removeRows(self, position, rows=1, index=QModelIndex()):
        print("Removing at position: %s"%position)
        position = self.selectRow
        self.model.beginRemoveRows(QModelIndex(), position, position + rows - 1)
        self.model.list = self.model.list[:position] + self.model.list[position + rows:]
        self.model.endRemoveRows()
        return True

    #Viewをクリックしたときの行の位置を取得
    def viewClicked(self, indexClicked):
        print('indexClicked() row: %s  column: %s'%(indexClicked.row(), indexClicked.column() ))
        self.selectRow = indexClicked.row()


class MyTableModel(QAbstractTableModel):

    def __init__(self, list, headers = [], parent = None):
        QAbstractTableModel.__init__(self, parent)
        self.list = list
        self.headers = headers

    def rowCount(self, parent):
        return len(self.list)

    def columnCount(self, parent):
        return len(self.list[0])

    def flags(self, index):
        row = index.row()
        column = index.column()
        if column == 0:
            return Qt.ItemIsUserCheckable | Qt.ItemIsEnabled
        else:
            return Qt.ItemIsEditable | Qt.ItemIsEnabled | Qt.ItemIsSelectable

    def data(self, index, role):

        row = index.row()
        column = index.column()

        if role == Qt.EditRole:
            return self.list[row][column]

        if role == Qt.CheckStateRole and column == 0:

            if self.list[row][column].isChecked():
                return QVariant(Qt.Checked)
            else:
                return QVariant(Qt.Unchecked)

        """  CheckBoxのテキストを表示させたい場合
        #if role == Qt.DisplayRole and column == 0:
            #return self.list[row][column].text()
        """

        if role == Qt.DisplayRole:

            row = index.row()
            column = index.column()
            value = self.list[row][column]

            return value

    def setData(self, index, value, role = Qt.EditRole):
        row = index.row()
        column = index.column()

        if role == Qt.EditRole:
            self.list[row][column] = value
            self.dataChanged.emit(index, index)
            return True

        if role == Qt.CheckStateRole and column == 0:
            self.list[row][column] = QCheckBox('')
            if value == Qt.Checked:
                self.list[row][column].setChecked(True)
            else:
                self.list[row][column].setChecked(False)
            self.dataChanged.emit(index, index)
            return True
        return False

    def headerData(self, section, orientation, role):

        if role == Qt.DisplayRole:

            if orientation == Qt.Horizontal:

                if section < len(self.headers):
                    return self.headers[section]
                else:
                    return "not implemented"
            else:
                return "%d" % (section + 1)


if __name__ == '__main__':

    app = QApplication(sys.argv)
    table = View()
    table.show()
    app.exec_()
```
 
<img width="962" alt="スクリーンショット 2016-08-02 23.39.51.png" src="https://qiita-image-store.s3.amazonaws.com/0/124460/f2d77a93-e083-8021-e468-b2539a3aee8c.png">

はじめの部分はこれでも動きます。

```python:View_TableModel.py
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.Qt import *
import sys, csv

"""
class Window (QWidget):
    def __init__ (self):
        QWidget.__init__(self, None)
        self.setWindowTitle('Table')
"""

class View (QWidget):
    def __init__ (self, parent=None):
        super(View, self).__init__()

        #サイズ固定
        self.setFixedSize(850, 500)
           :
           :
```



<script>
  (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
  (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
  m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
  })(window,document,'script','https://www.google-analytics.com/analytics.js','ga');

  ga('create', 'UA-84964934-1', 'auto');
  ga('send', 'pageview');

</script>