PyQt5 QTreeViewのアイテムをJSONから開く・保存
2020/01/31 categories:PyQt5| tags:Python|PyQt5|QTreeView|JSON|
QTreeViewに表示するアイテムをJSONファイルから読み込み、保存するサンプルを作成しました。
ファイルの読み込みや保存、ツリービューへのアイテムの追加や削除はウィンドウのメニューから実行出来るようにしました。また、アイテムの追加や削除はコンテキストメニューからもできます。
JSONファイル
{
"columns": [
"ID",
"Name"
],
"parts": [
{
"ID": "Part 1",
"Name": "Name A",
"parts": [
{
"ID": "Part 3",
"Name": "Name C",
"parts": [
{
"ID": "Part 4",
"Name": "Name D"
}
]
},
{
"ID": "Part 2",
"Name": "Name B"
}
]
}
]
}
QTreeViewに表示される列名はJSONのcolumnsの配列から取得するようにしましたので、JSONにcolumnsの配列があります。
QTreeViewのアイテムはJSONのparts配列に入れたオブジェクトから読み込みます。階層が深くなる場合は、オブジェクトにさらにparts配列を入れて、その中に入れたオブジェクトが子となるようにしました。
JSONファイルを開く
def open_json(self, filename):
def recursion(_part, _parent, _parts):
d = { key:_part[key] for key in _part if not 'parts' == key }
parent_index = self.model.addItem(d, _parent)
if 'parts' in _part:
for child in _part['parts']:
recursion(child, parent_index, parts)
filename = QtWidgets.QFileDialog.getOpenFileName(self, 'Save file', '', 'JSON File (*.json)')
if not filename[0]:
return
json_data = json.load( open(filename[0]) )
self.model.removeAllItems()
self.model.removeAllColumns()
self.model.addColumns( json_data['columns'] )
parts = json_data['parts']
for part in parts:
recursion(part, QtCore.QModelIndex(), parts)
上記のようなコードで下記のような処理を行いました。
- ファイルダイアログから受け取ったファイル名をjson.loadで開く
- json_dataからparts配列に入っているオブジェクトを取得して、それらに対して再帰的に処理を行う
- 再帰の処理では、jsonから取得したオブジェクトをpythonの辞書としてmodelのaddItemメソッドで追加する
- 追加したオブジェクトにparts配列があればrecursionメソッドを再帰的に呼び出して処理を行う
JSONファイルの保存
def save_json(self):
def recursion(parent):
_dict1 = parent.dict
if parent.hasChildren():
_dict1['parts'] = [ recursion(child) for child in parent.children ]
return _dict1
parts = []
for child in self.model.root_item.children:
parts.append( recursion(child) )
parts = {'columns':self.model.columns, 'parts':parts}
filename = QtWidgets.QFileDialog.getSaveFileName(self, 'Save file', '', 'JSON File (*.json)')
if filename[0]:
json.dump(parts, open(filename[0],'w'), indent=4)
上記のようなコードで下記のような処理を行いました。
modelのルートアイテムの子に対して再帰的に処理を行う 再帰の処理では、アイテムから辞書を取得してIDやNameなどのデータを取得する この時、そのアイテムに子が存在すればrecursionを再帰的に呼び出して子の辞書を取得して、partsリストに入れていく 再帰処理が終了したらmodelからcolumnsを取得してpartsに追加してJSONファイルとして保存する
ソースコード
コードはgithubにも保存しています。
main.py
import json
import sys
from mainwindow import Ui_MainWindow
from treeview import Model, Delegate, Item
from PyQt5 import QtWidgets, QtCore
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, app):
super().__init__()
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.model = Model(self)
self.ui.treeView.setModel(self.model)
self.ui.treeView.setItemDelegate(Delegate())
self.ui.treeView.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.ui.treeView.customContextMenuRequested.connect(self.contextMenu)
self.ui.actionAddChild.triggered.connect(self.add_child)
self.ui.actionDelete.triggered.connect(self.delete_item)
self.ui.actionOpen.triggered.connect(self.open_json)
self.ui.actionSave.triggered.connect(self.save_json)
def open_json(self, filename):
def recursion(_part, _parent, _parts):
d = { key:_part[key] for key in _part if not 'parts' == key }
parent_index = self.model.addItem(d, _parent)
if 'parts' in _part:
for child in _part['parts']:
recursion(child, parent_index, parts)
filename = QtWidgets.QFileDialog.getOpenFileName(self, 'Save file', '', 'JSON File (*.json)')
if not filename[0]:
return
json_data = json.load( open(filename[0]) )
self.model.removeAllItems()
self.model.removeAllColumns()
self.model.addColumns( json_data['columns'] )
parts = json_data['parts']
for part in parts:
recursion(part, QtCore.QModelIndex(), parts)
def save_json(self):
def recursion(parent):
_dict1 = parent.dict
if parent.hasChildren():
_dict1['parts'] = [ recursion(child) for child in parent.children ]
return _dict1
parts = []
for child in self.model.root_item.children:
parts.append( recursion(child) )
parts = {'columns':self.model.columns, 'parts':parts}
filename = QtWidgets.QFileDialog.getSaveFileName(self, 'Save file', '', 'JSON File (*.json)')
if filename[0]:
json.dump(parts, open(filename[0],'w'), indent=4)
def contextMenu(self, point):
self.menu = QtWidgets.QMenu(self)
self.menu.addAction('Add child', self.add_child)
self.menu.addAction('Delete', self.delete_item)
self.menu.exec_( self.focusWidget().mapToGlobal(point) )
def add_child(self):
indexes = self.ui.treeView.selectedIndexes()
if len(indexes) == 0:
self.model.addItem()
return
indexes2 = []
for index in indexes:
if not index.row() in [ i.row() for i in indexes2 if i.parent() == index.parent() ]:
indexes2.append(index)
for index in indexes2:
self.model.addItem({}, index)
def delete_item(self):
indexes = self.ui.treeView.selectedIndexes()
if len(indexes) == 0:
return
indexes2 = []
for index in indexes:
if not index.row() in [ i.row() for i in indexes2 if i.parent() == index.parent() ]:
indexes2.append(index)
for index in indexes2:
self.model.removeItem(index)
def main():
app = QtWidgets.QApplication(sys.argv)
window = MainWindow(app)
window.show()
app.exec_()
if __name__ == '__main__':
main()
treeview.py
## -*- coding: utf-8 -*-
from PyQt5 import QtWidgets, QtCore
class Item(object):
def __init__(self, _parent=None, _dict={}):
self.dict = _dict
self.parent_item = _parent
self.children = []
def appendChild(self, item):
self.children.append(item)
def data(self, column):
if column in self.dict.keys():
return self.dict[column]
return ''
def setData(self, column, data):
self.dict[column] = data
def child(self, row):
return self.children[row]
def childrenCount(self):
return len(self.children)
def hasChildren(self):
return not self.childrenCount() == 0
def hasParent(self):
return not self.parent_item is None
def parent(self):
return self.parent_item
def removeChild(self, row):
del self.children[row]
def row(self):
if self.parent_item:
return self.parent_item.children.index(self)
return 0
class Model(QtCore.QAbstractItemModel):
def __init__(self, parent_=None):
super(Model, self).__init__(parent_)
self.root_item = Item()
self.root_item.setData('ID', 'root item')
self.columns = []
def addColumns(self, columns, parent=QtCore.QModelIndex()):
self.beginInsertColumns(parent, self.columnCount(), self.columnCount() + len(columns) - 1)
self.columns.extend(columns)
self.endInsertColumns()
def addItem(self, _dict={}, parent=QtCore.QModelIndex()):
if parent == QtCore.QModelIndex():
parent_item = self.root_item
else:
parent_item = parent.internalPointer()
item = Item(parent_item, _dict)
row = parent_item.childrenCount()
self.beginInsertRows(parent, row, row)
parent_item.children.insert( row, item )
self.endInsertRows()
return self.index(row, 0, parent)
def column(self, key):
return self.columns[key]
def columnCount(self, parent=QtCore.QModelIndex()):
return len(self.columns)
def data(self, index, role):
if not index.isValid():
return QtCore.QVariant()
if role == QtCore.Qt.EditRole or role == QtCore.Qt.DisplayRole:
return index.internalPointer().data( self.column(index.column()) )
return QtCore.QVariant()
def flags(self, index):
return QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
def headerData(self, i, orientation, role):
if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
return self.columns[i]
if orientation == QtCore.Qt.Vertical and role == QtCore.Qt.DisplayRole:
return i + 1
def index(self, row, column, parent):
if not parent.isValid():
parent_item = self.root_item
else:
parent_item = parent.internalPointer()
if parent_item.childrenCount() > 0:
return self.createIndex(row, column, parent_item.child(row))
return QtCore.QModelIndex()
def parent(self, index):
if not index.isValid():
return QtCore.QModelIndex()
child_item = index.internalPointer()
if not child_item:
return QtCore.QModelIndex()
parent_item = child_item.parent()
if parent_item == self.root_item:
return QtCore.QModelIndex()
return self.createIndex(parent_item.row(), 0, parent_item)
def removeAllColumns(self):
self.beginRemoveColumns(QtCore.QModelIndex(), 0, self.columnCount()-1)
self.columns = []
self.endRemoveColumns()
def removeAllItems(self):
self.beginRemoveRows(QtCore.QModelIndex(), 0, self.rowCount()-1)
self.root_item.children = []
self.endRemoveRows()
def removeItem(self, index):
parent = index.parent()
if parent == QtCore.QModelIndex():
parent_item = self.root_item
else:
parent_item = parent.internalPointer()
self.beginRemoveRows(parent, index.row(), index.row())
parent_item.removeChild(index.row())
self.endRemoveRows()
def rowCount(self, parent=QtCore.QModelIndex()):
if not parent.isValid():
return self.root_item.childrenCount()
return parent.internalPointer().childrenCount()
def setData(self, index, value, role=QtCore.Qt.EditRole):
if role == QtCore.Qt.EditRole:
index.internalPointer().setData( self.column(index.column()), value )
return True
return False
class Delegate(QtWidgets.QStyledItemDelegate):
def __init__(self, parent=None, setModelDataEvent=None):
super(Delegate, self).__init__(parent)
self.setModelDataEvent = setModelDataEvent
def createEditor(self, parent, option, index):
return QtWidgets.QLineEdit(parent)
def setEditorData(self, editor, index):
value = index.model().data(index, QtCore.Qt.DisplayRole)
editor.setText(str(value))
def setModelData(self, editor, model, index):
model.setData(index, editor.text())
if not self.setModelDataEvent is None:
self.setModelDataEvent()
mainwindow.py
## -*- coding: utf-8 -*-
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
MainWindow.setObjectName("MainWindow")
MainWindow.resize(400, 500)
self.centralwidget = QtWidgets.QWidget(MainWindow)
self.centralwidget.setObjectName("centralwidget")
self.verticalLayout = QtWidgets.QVBoxLayout(self.centralwidget)
self.verticalLayout.setObjectName("verticalLayout")
self.treeView = QtWidgets.QTreeView(self.centralwidget)
self.treeView.setAlternatingRowColors(True)
self.treeView.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
self.treeView.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectItems)
self.treeView.setObjectName("treeView")
self.verticalLayout.addWidget(self.treeView)
MainWindow.setCentralWidget(self.centralwidget)
self.menubar = QtWidgets.QMenuBar(MainWindow)
self.menubar.setGeometry(QtCore.QRect(0, 0, 400, 21))
self.menubar.setObjectName("menubar")
self.menuFile = QtWidgets.QMenu(self.menubar)
self.menuFile.setObjectName("menuFile")
self.menuEdit = QtWidgets.QMenu(self.menubar)
self.menuEdit.setObjectName("menuEdit")
MainWindow.setMenuBar(self.menubar)
self.statusbar = QtWidgets.QStatusBar(MainWindow)
self.statusbar.setObjectName("statusbar")
MainWindow.setStatusBar(self.statusbar)
self.actionOpen = QtWidgets.QAction(MainWindow)
self.actionOpen.setObjectName("actionOpen")
self.actionSave = QtWidgets.QAction(MainWindow)
self.actionSave.setObjectName("actionSave")
self.actionAddChild = QtWidgets.QAction(MainWindow)
self.actionAddChild.setObjectName("actionAddChild")
self.actionDelete = QtWidgets.QAction(MainWindow)
self.actionDelete.setObjectName("actionDelete")
self.menuFile.addAction(self.actionOpen)
self.menuFile.addAction(self.actionSave)
self.menuEdit.addAction(self.actionAddChild)
self.menuEdit.addAction(self.actionDelete)
self.menubar.addAction(self.menuFile.menuAction())
self.menubar.addAction(self.menuEdit.menuAction())
self.retranslateUi(MainWindow)
QtCore.QMetaObject.connectSlotsByName(MainWindow)
def retranslateUi(self, MainWindow):
_translate = QtCore.QCoreApplication.translate
MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
self.menuFile.setTitle(_translate("MainWindow", "File"))
self.menuEdit.setTitle(_translate("MainWindow", "Edit"))
self.actionOpen.setText(_translate("MainWindow", "Open"))
self.actionSave.setText(_translate("MainWindow", "Save"))
self.actionAddChild.setText(_translate("MainWindow", "Add child"))
self.actionDelete.setText(_translate("MainWindow", "Delete"))