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"))

コメント

タイトルとURLをコピーしました