PyQt5 QTreeViewのアイテムをJSONから開く・保存

Share on:

QTreeViewに表示するアイテムをJSONファイルから読み込み、保存するサンプルを作成しました。

ファイルの読み込みや保存、ツリービューへのアイテムの追加や削除はウィンドウのメニューから実行出来るようにしました。また、アイテムの追加や削除はコンテキストメニューからもできます。

JSONファイル

 1{
 2    "columns": [
 3        "ID",
 4        "Name"
 5    ],
 6    "parts": [
 7        {
 8            "ID": "Part 1",
 9            "Name": "Name A",
10            "parts": [
11                {
12                    "ID": "Part 3",
13                    "Name": "Name C",
14                    "parts": [
15                        {
16                            "ID": "Part 4",
17                            "Name": "Name D"
18                        }
19                    ]
20                },
21                {
22                    "ID": "Part 2",
23                    "Name": "Name B"
24                }
25            ]
26        }
27    ]
28}

QTreeViewに表示される列名はJSONのcolumnsの配列から取得するようにしましたので、JSONにcolumnsの配列があります。

QTreeViewのアイテムはJSONのparts配列に入れたオブジェクトから読み込みます。階層が深くなる場合は、オブジェクトにさらにparts配列を入れて、その中に入れたオブジェクトが子となるようにしました。

JSONファイルを開く

 1def open_json(self, filename):
 2
 3    def recursion(_part, _parent, _parts):
 4        d = { key:_part[key] for key in _part if not 'parts' == key }
 5        parent_index = self.model.addItem(d, _parent)
 6        if 'parts' in _part:
 7            for child in _part['parts']:
 8                recursion(child, parent_index, parts)
 9    
10    filename = QtWidgets.QFileDialog.getOpenFileName(self, 'Save file', '', 'JSON File (*.json)')
11    if not filename[0]:
12        return
13    
14    json_data = json.load( open(filename[0]) )
15    self.model.removeAllItems()
16    self.model.removeAllColumns()
17    self.model.addColumns( json_data['columns'] )
18    parts = json_data['parts']
19    for part in parts:
20        recursion(part, QtCore.QModelIndex(), parts)

上記のようなコードで下記のような処理を行いました。

  • ファイルダイアログから受け取ったファイル名をjson.loadで開く
  • json_dataからparts配列に入っているオブジェクトを取得して、それらに対して再帰的に処理を行う
  • 再帰の処理では、jsonから取得したオブジェクトをpythonの辞書としてmodelのaddItemメソッドで追加する
  • 追加したオブジェクトにparts配列があればrecursionメソッドを再帰的に呼び出して処理を行う

JSONファイルの保存

 1def save_json(self):
 2
 3    def recursion(parent):
 4        _dict1 = parent.dict
 5        if parent.hasChildren():
 6            _dict1['parts'] = [ recursion(child) for child in parent.children ]
 7        return _dict1
 8    
 9    parts = []
10    for child in self.model.root_item.children:
11        parts.append( recursion(child) )
12    
13    parts = {'columns':self.model.columns, 'parts':parts}
14
15    filename = QtWidgets.QFileDialog.getSaveFileName(self, 'Save file', '', 'JSON File (*.json)')
16    if filename[0]:
17        json.dump(parts, open(filename[0],'w'), indent=4)

上記のようなコードで下記のような処理を行いました。

modelのルートアイテムの子に対して再帰的に処理を行う 再帰の処理では、アイテムから辞書を取得してIDやNameなどのデータを取得する この時、そのアイテムに子が存在すればrecursionを再帰的に呼び出して子の辞書を取得して、partsリストに入れていく 再帰処理が終了したらmodelからcolumnsを取得してpartsに追加してJSONファイルとして保存する

ソースコード

コードはgithubにも保存しています。

main.py

  1import json
  2import sys
  3from mainwindow import Ui_MainWindow
  4from treeview import Model, Delegate, Item
  5from PyQt5 import QtWidgets, QtCore
  6
  7class MainWindow(QtWidgets.QMainWindow):
  8    def __init__(self, app):
  9        super().__init__()
 10        
 11        self.ui = Ui_MainWindow()
 12        self.ui.setupUi(self)
 13
 14        self.model = Model(self)
 15
 16        self.ui.treeView.setModel(self.model)
 17        self.ui.treeView.setItemDelegate(Delegate())
 18        self.ui.treeView.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
 19        self.ui.treeView.customContextMenuRequested.connect(self.contextMenu)
 20
 21        self.ui.actionAddChild.triggered.connect(self.add_child)
 22        self.ui.actionDelete.triggered.connect(self.delete_item)
 23        self.ui.actionOpen.triggered.connect(self.open_json)
 24        self.ui.actionSave.triggered.connect(self.save_json)
 25
 26    def open_json(self, filename):
 27
 28        def recursion(_part, _parent, _parts):
 29            d = { key:_part[key] for key in _part if not 'parts' == key }
 30            parent_index = self.model.addItem(d, _parent)
 31            if 'parts' in _part:
 32                for child in _part['parts']:
 33                    recursion(child, parent_index, parts)
 34        
 35        filename = QtWidgets.QFileDialog.getOpenFileName(self, 'Save file', '', 'JSON File (*.json)')
 36        if not filename[0]:
 37            return
 38        
 39        json_data = json.load( open(filename[0]) )
 40        self.model.removeAllItems()
 41        self.model.removeAllColumns()
 42        self.model.addColumns( json_data['columns'] )
 43        parts = json_data['parts']
 44        for part in parts:
 45            recursion(part, QtCore.QModelIndex(), parts)
 46        
 47    def save_json(self):
 48
 49        def recursion(parent):
 50            _dict1 = parent.dict
 51            if parent.hasChildren():
 52                _dict1['parts'] = [ recursion(child) for child in parent.children ]
 53            return _dict1
 54        
 55        parts = []
 56        for child in self.model.root_item.children:
 57            parts.append( recursion(child) )
 58        
 59        parts = {'columns':self.model.columns, 'parts':parts}
 60
 61        filename = QtWidgets.QFileDialog.getSaveFileName(self, 'Save file', '', 'JSON File (*.json)')
 62        if filename[0]:
 63            json.dump(parts, open(filename[0],'w'), indent=4)
 64
 65    def contextMenu(self, point):
 66        self.menu = QtWidgets.QMenu(self)
 67        self.menu.addAction('Add child', self.add_child)
 68        self.menu.addAction('Delete', self.delete_item)
 69        self.menu.exec_( self.focusWidget().mapToGlobal(point) )
 70 
 71    def add_child(self):
 72        indexes = self.ui.treeView.selectedIndexes()
 73        
 74        if len(indexes) == 0:
 75            self.model.addItem()
 76            return
 77        
 78        indexes2 = []
 79        for index in indexes:
 80            if not index.row() in [ i.row() for i in indexes2 if i.parent() == index.parent() ]:
 81                indexes2.append(index)
 82        
 83        for index in indexes2:
 84            self.model.addItem({}, index)
 85
 86    def delete_item(self):
 87        indexes = self.ui.treeView.selectedIndexes()
 88
 89        if len(indexes) == 0:
 90            return
 91
 92        indexes2 = []
 93        for index in indexes:
 94            if not index.row() in [ i.row() for i in indexes2 if i.parent() == index.parent() ]:
 95                indexes2.append(index)
 96        
 97        for index in indexes2:
 98            self.model.removeItem(index)
 99
100def main():
101    app = QtWidgets.QApplication(sys.argv)
102    window = MainWindow(app)
103    window.show()
104    app.exec_()
105 
106if __name__ == '__main__':
107    main()

treeview.py

  1# -*- coding: utf-8 -*-
  2from PyQt5 import QtWidgets, QtCore
  3 
  4class Item(object):
  5    def __init__(self, _parent=None, _dict={}):
  6        self.dict = _dict
  7        self.parent_item = _parent
  8        self.children = []
  9    
 10    def appendChild(self, item):
 11        self.children.append(item)
 12
 13    def data(self, column):
 14        if column in self.dict.keys():
 15            return self.dict[column]
 16        return ''
 17 
 18    def setData(self, column, data):
 19        self.dict[column] = data
 20    
 21    def child(self, row):
 22        return self.children[row]
 23
 24    def childrenCount(self):
 25        return len(self.children)
 26
 27    def hasChildren(self):
 28        return not self.childrenCount() == 0
 29
 30    def hasParent(self):
 31        return not self.parent_item is None
 32
 33    def parent(self):
 34        return self.parent_item
 35
 36    def removeChild(self, row):
 37        del self.children[row]
 38
 39    def row(self):
 40        if self.parent_item:
 41            return self.parent_item.children.index(self)
 42        return 0
 43
 44class Model(QtCore.QAbstractItemModel):
 45    def __init__(self, parent_=None):
 46        super(Model, self).__init__(parent_)
 47        self.root_item = Item()
 48        self.root_item.setData('ID', 'root item')
 49        self.columns = []
 50 
 51    def addColumns(self, columns, parent=QtCore.QModelIndex()):
 52        self.beginInsertColumns(parent, self.columnCount(), self.columnCount() + len(columns) - 1)
 53        self.columns.extend(columns)
 54        self.endInsertColumns()
 55 
 56    def addItem(self, _dict={}, parent=QtCore.QModelIndex()):
 57        if parent == QtCore.QModelIndex():
 58            parent_item = self.root_item
 59        else:
 60            parent_item = parent.internalPointer()
 61        item = Item(parent_item, _dict)
 62        row = parent_item.childrenCount()
 63        self.beginInsertRows(parent, row, row)
 64        parent_item.children.insert( row, item )
 65        self.endInsertRows()
 66        return self.index(row, 0, parent)
 67        
 68    def column(self, key):
 69        return self.columns[key]
 70 
 71    def columnCount(self, parent=QtCore.QModelIndex()):
 72        return len(self.columns)
 73 
 74    def data(self, index, role):
 75        if not index.isValid():
 76            return QtCore.QVariant()
 77        if role == QtCore.Qt.EditRole or role == QtCore.Qt.DisplayRole:
 78            return index.internalPointer().data( self.column(index.column()) )
 79        return QtCore.QVariant()
 80 
 81    def flags(self, index):
 82        return QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
 83 
 84    def headerData(self, i, orientation, role):
 85        if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
 86            return self.columns[i]
 87        if orientation == QtCore.Qt.Vertical and role == QtCore.Qt.DisplayRole:
 88            return i + 1
 89 
 90    def index(self, row, column, parent):
 91        if not parent.isValid():
 92            parent_item = self.root_item
 93        else:
 94            parent_item = parent.internalPointer()
 95
 96        if parent_item.childrenCount() > 0:
 97            return self.createIndex(row, column, parent_item.child(row))
 98        return QtCore.QModelIndex()
 99
100    def parent(self, index):
101        if not index.isValid():
102            return QtCore.QModelIndex()
103        child_item = index.internalPointer()
104        if not child_item:
105            return QtCore.QModelIndex()
106        parent_item = child_item.parent()
107        if parent_item == self.root_item:
108            return QtCore.QModelIndex()
109        return self.createIndex(parent_item.row(), 0, parent_item)
110        
111    def removeAllColumns(self):
112        self.beginRemoveColumns(QtCore.QModelIndex(), 0, self.columnCount()-1)
113        self.columns = []
114        self.endRemoveColumns()
115 
116    def removeAllItems(self):
117        self.beginRemoveRows(QtCore.QModelIndex(), 0, self.rowCount()-1)
118        self.root_item.children = []
119        self.endRemoveRows()
120
121    def removeItem(self, index):
122        parent = index.parent()
123        if parent == QtCore.QModelIndex():
124            parent_item = self.root_item
125        else:
126            parent_item = parent.internalPointer()
127        self.beginRemoveRows(parent, index.row(), index.row())
128        parent_item.removeChild(index.row())
129        self.endRemoveRows()
130 
131    def rowCount(self, parent=QtCore.QModelIndex()):
132        if not parent.isValid():
133            return self.root_item.childrenCount()
134        return parent.internalPointer().childrenCount()
135
136    def setData(self, index, value, role=QtCore.Qt.EditRole):
137        if role == QtCore.Qt.EditRole:
138            index.internalPointer().setData( self.column(index.column()), value )
139            return True
140        return False
141 
142class Delegate(QtWidgets.QStyledItemDelegate):
143    def __init__(self, parent=None, setModelDataEvent=None):
144        super(Delegate, self).__init__(parent)
145        self.setModelDataEvent = setModelDataEvent
146 
147    def createEditor(self, parent, option, index):
148        return QtWidgets.QLineEdit(parent)
149 
150    def setEditorData(self, editor, index):
151        value = index.model().data(index, QtCore.Qt.DisplayRole)
152        editor.setText(str(value))
153 
154    def setModelData(self, editor, model, index):
155        model.setData(index, editor.text())
156        if not self.setModelDataEvent is None:
157            self.setModelDataEvent()

mainwindow.py

 1# -*- coding: utf-8 -*-
 2from PyQt5 import QtCore, QtGui, QtWidgets
 3
 4class Ui_MainWindow(object):
 5    def setupUi(self, MainWindow):
 6        MainWindow.setObjectName("MainWindow")
 7        MainWindow.resize(400, 500)
 8        self.centralwidget = QtWidgets.QWidget(MainWindow)
 9        self.centralwidget.setObjectName("centralwidget")
10        self.verticalLayout = QtWidgets.QVBoxLayout(self.centralwidget)
11        self.verticalLayout.setObjectName("verticalLayout")
12        self.treeView = QtWidgets.QTreeView(self.centralwidget)
13        self.treeView.setAlternatingRowColors(True)
14        self.treeView.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
15        self.treeView.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectItems)
16        self.treeView.setObjectName("treeView")
17        self.verticalLayout.addWidget(self.treeView)
18        MainWindow.setCentralWidget(self.centralwidget)
19        self.menubar = QtWidgets.QMenuBar(MainWindow)
20        self.menubar.setGeometry(QtCore.QRect(0, 0, 400, 21))
21        self.menubar.setObjectName("menubar")
22        self.menuFile = QtWidgets.QMenu(self.menubar)
23        self.menuFile.setObjectName("menuFile")
24        self.menuEdit = QtWidgets.QMenu(self.menubar)
25        self.menuEdit.setObjectName("menuEdit")
26        MainWindow.setMenuBar(self.menubar)
27        self.statusbar = QtWidgets.QStatusBar(MainWindow)
28        self.statusbar.setObjectName("statusbar")
29        MainWindow.setStatusBar(self.statusbar)
30        self.actionOpen = QtWidgets.QAction(MainWindow)
31        self.actionOpen.setObjectName("actionOpen")
32        self.actionSave = QtWidgets.QAction(MainWindow)
33        self.actionSave.setObjectName("actionSave")
34        self.actionAddChild = QtWidgets.QAction(MainWindow)
35        self.actionAddChild.setObjectName("actionAddChild")
36        self.actionDelete = QtWidgets.QAction(MainWindow)
37        self.actionDelete.setObjectName("actionDelete")
38        self.menuFile.addAction(self.actionOpen)
39        self.menuFile.addAction(self.actionSave)
40        self.menuEdit.addAction(self.actionAddChild)
41        self.menuEdit.addAction(self.actionDelete)
42        self.menubar.addAction(self.menuFile.menuAction())
43        self.menubar.addAction(self.menuEdit.menuAction())
44
45        self.retranslateUi(MainWindow)
46        QtCore.QMetaObject.connectSlotsByName(MainWindow)
47
48    def retranslateUi(self, MainWindow):
49        _translate = QtCore.QCoreApplication.translate
50        MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
51        self.menuFile.setTitle(_translate("MainWindow", "File"))
52        self.menuEdit.setTitle(_translate("MainWindow", "Edit"))
53        self.actionOpen.setText(_translate("MainWindow", "Open"))
54        self.actionSave.setText(_translate("MainWindow", "Save"))
55        self.actionAddChild.setText(_translate("MainWindow", "Add child"))
56        self.actionDelete.setText(_translate("MainWindow", "Delete"))

関連記事