iCAD SXのツリーをQTreeViewに表示してみた

2023/09/02 categories:iCAD SX| tags:iCAD SDK|iCAD SX|Python|QTreeView|

iCAD SXのツリーには折り畳み可能な行はありますが、列が無いようです。NXならツリーの列に様々なモデルの属性を表示することができて使いやすいと感じています。そこで、iCAD SXのツリーをPyQt6のQTreeViewにモデル情報の列があるツリーとして表示してみました。

外観

ツリーの取得

ツリー読み込みボタンを押すとload_tree_from_iCADが実行されます。その関数では、self.sxnet.SxWF.getActive()でアクティブな3D図面を取得して、3D図面からgetInfPartTree()で取得できるパーツ階層情報のchild_listを再帰的に取得していきます。その階層情報を再帰的に取得するためにget_parts関数を作成しました。また、取得したデータが正しいか確認するために、json.dumpでjsonファイルとして保存して確認しました。

        def get_parts(inf_part_tree):
            parts0 = []
            if inf_part_tree.child_list is not None:
                for child_inf_part_tree in inf_part_tree.child_list:
                    child = { 'data' : infparttree_to_list(child_inf_part_tree), 'children' : [] }
                    if child_inf_part_tree.child_list is not None:
                        child['children'] = get_parts(child_inf_part_tree)
                    parts0.append(child)
            return parts0
        
        if self.sxnet is None:
            return
        
        active_wf = self.sxnet.SxWF.getActive()
        if active_wf is None:
            return
        
        inf_part_tree = active_wf.getInfPartTree()
        if inf_part_tree is None:
            return
        
        parts = { 'data' : infparttree_to_list(inf_part_tree) }
        if inf_part_tree.child_list is not None:
            parts['children'] = get_parts(inf_part_tree)
        
        with open('data.json', mode='w', encoding='utf8') as f:
            json.dump(parts, f, ensure_ascii=False, indent=4)
            
        self.set_dict(parts)

パーツ情報の取得

SxInfPartTreeからパーツ情報を取得するためにinfparttree_to_list関数を作成して、パーツ情報をリストとして取得しています。取得する情報はとりあえず、SxInfPartから取得できるパーツ名やコメント、ファイルパスなどにしました。

        def infparttree_to_list(inf_part_tree):
            inf_part = inf_part_tree.inf
            return [
                inf_part.name,
                inf_part.comment,
                inf_part.path if inf_part.is_external else '',
                inf_part.ref_model_name if inf_part.is_external else '',
                inf_part.is_external,
                inf_part.is_mirror,
                inf_part.is_read_only,
                inf_part.is_unloaded
            ]

作成される辞書

json.dumpで作成されるdata.jsonファイルを確認すると以下のように辞書データが作成されます。パーツ情報はdataにリストとして格納され、パーツの子部品はchildrenにリストとして格納し、階層を持つような構成にしました。一部を抜粋すると以下のようになります。asm_test01がツリートップのパーツで、asm_test02がasm_test01の子部品というような構成になっています。

{
    "data": [ "asm_test01", "組1", "", "", false, false, false, false ],
    "children": [
        {
            "data": [ "asm_test02", "組2", "", "", false, false, false, false ],
            "children": [
                {
                    "data": [ "asm_test03", "組3", "C:\\cad_file\\test01", "asm_test03", true, false, true, false ],
                    "children": [
                        {
                            "data": [ "M5x20_sems_cap", "六角穴付きボルト", "", "", false, false, true, false ],
                            "children": []
                        },
                        {
                            "data": [ "M6x25_sems_cap", "六角穴付きボルト", "", "", false, false, true, false ],
                            "children": []
                        }
                    ]
                },
                {
                    "data": [ "part_001", "パーツ1", "C:\\cad_file\\test01", "part_001", true, false, true, false ],
                    "children": []
                }
            ]
        },

作成したツリーの辞書をQStandardItemModelに追加

QTreeViewに表示するためにはQStandardItemModelにQStandardItemを追加する必要があります。上記の階層とパーツ情報を持つ辞書を再帰的に取得して、QStandardItemModelにQStandardItemアイテムを追加していくような処理にしました。iCADからツリーを取得するときにQStandardItemModelにQStandardItemアイテムを追加する処理の方が処理の回数が減るので良いかと思いましたが、CADからの読み込みとQStandardItemModelへの追加の処理を分けておくと、ツリーの保存や読み込みがしやすくなるかと思ってこのような処理にしています。

    def set_dict(self, data0: dict):
        def add_children(parent: QtGui.QStandardItem, data1):
            for row, child in enumerate( data1['children'] ):
                parent.appendRow([ QtGui.QStandardItem() for _ in range( self.model.columnCount() )])
                for column, column_data in enumerate( child['data'] ):
                    parent.child(row, column).setData( column_data, QtCore.Qt.ItemDataRole.DisplayRole )
                if len(child['children']) > 0:
                    add_children( parent.child(row, 0), child )
        self.model.invisibleRootItem().appendRow([ QtGui.QStandardItem() for _ in range( self.model.columnCount() )])
        for column, d in enumerate(data0['data']):
            self.model.invisibleRootItem().child(0, column).setData( d, QtCore.Qt.ItemDataRole.DisplayRole )
        add_children( self.model.invisibleRootItem().child(0, 0), data0 )

QTreeViewのフィルタを作成

iCADtreeクラスでフィルタにテキストを入力したらfitler_text_changedが実行されるようにして、ツリーのフィルタを出来るようにしています。QSortFilterProxyModelを使用して、ツリーのフィルタを行うと、親子関係なくすべてのアイテムがフィルタリングされるので、子部品が条件一致していてその親部品が条件一致していないときは子部品も親部品も表示されなくなってしまいます。CADのツリーの場合、子を持たない単品部品のみをフィルタリングしたいことが多いので、QSortFilterProxyModelを継承して、filterAcceptsRowをオーバーライドして、その関数内でアイテムに含まれる子部品の数が0の時はフィルタ処理を実行して、それ以外の時は常にフィルタされないようにしました。

class iCADtree(QtWidgets.QWidget):
    def fitler_text_changed(self, text):
        if text == '':
            self.proxy_model.setFilterKeyColumn(-1)
            self.proxy_model.setFilterRegularExpression('')
            return
        self.proxy_model.setFilterKeyColumn(0)
        self.proxy_model.setFilterRegularExpression(text)
        
class ProxyModel(QtCore.QSortFilterProxyModel):
    def filterAcceptsRow(self, source_row: int, source_parent: QtCore.QModelIndex) -> bool:
        source_index = self.sourceModel().index(source_row, 0, source_parent)
        child_count = self.sourceModel().rowCount(source_index)
        if child_count == 0:
            return super().filterAcceptsRow(source_row, source_parent)
        return True

動作の様子

iCAD SXでモデルを開いた状態でコードを実行して、画面が表示されてからツリー読み込みをクリックするとツリーが読み込まれます。

Pythonコード

import os
import json
from PyQt6 import QtWidgets, QtGui, QtCore


class iCADtree(QtWidgets.QWidget):
    def __init__(self) -> None:
        super().__init__()

        self.sxnet = None

        self.setWindowTitle('iCAD QTreeView test')
        self.model = QtGui.QStandardItemModel()
        self.model.setHorizontalHeaderLabels(['パーツ名', 'コメント', 'パス', '図面名', '外部', 'ミラー', '読み取り専用', '未解決'])
        self.proxy_model = ProxyModel()
        self.proxy_model.setSourceModel(self.model)

        self.view = QtWidgets.QTreeView()
        self.view.setModel(self.proxy_model)

        self.load_tree_from_iCAD_button = QtWidgets.QPushButton('ツリー読み込み')
        self.expand_all_button = QtWidgets.QPushButton('展開')
        self.collapse_all_button = QtWidgets.QPushButton('折り畳み')
        self.filter_text = QtWidgets.QLineEdit()

        menu = QtWidgets.QToolBar()
        menu.addWidget(self.load_tree_from_iCAD_button)
        menu.addSeparator()
        menu.addWidget(self.expand_all_button)
        menu.addWidget(self.collapse_all_button)
        menu.addWidget(QtWidgets.QLabel('フィルター'))
        menu.addWidget(self.filter_text)

        self.setLayout(QtWidgets.QVBoxLayout())
        self.layout().setSpacing(0)
        self.layout().setContentsMargins(0, 0, 0, 0)
        self.layout().addWidget(menu)
        self.layout().addWidget(self.view)

        self.load_tree_from_iCAD_button.clicked.connect(self.load_tree_from_iCAD)
        self.expand_all_button.clicked.connect(self.view.expandAll)
        self.collapse_all_button.clicked.connect(self.view.collapseAll)
        self.filter_text.textChanged.connect(self.fitler_text_changed)

        self.init_iCAD()

    def fitler_text_changed(self, text):
        if text == '':
            self.proxy_model.setFilterKeyColumn(-1)
            self.proxy_model.setFilterRegularExpression('')
            return
        self.proxy_model.setFilterKeyColumn(0)
        self.proxy_model.setFilterRegularExpression(text)

    def init_iCAD(self):
        try:
            import clr
            icad_dir = os.getenv('ICADDIR')
            clr.AddReference( '{}/bin/sxnet.dll'.format(icad_dir) )
            import sxnet
            self.sxnet = sxnet
            sxnet.SxSys.init(3999)
        except:
            self.sxnet = None
        
    def set_dict(self, data0: dict):
        def add_children(parent: QtGui.QStandardItem, data1):
            for row, child in enumerate( data1['children'] ):
                parent.appendRow([ QtGui.QStandardItem() for _ in range( self.model.columnCount() )])
                for column, column_data in enumerate( child['data'] ):
                    parent.child(row, column).setData( column_data, QtCore.Qt.ItemDataRole.DisplayRole )
                if len(child['children']) > 0:
                    add_children( parent.child(row, 0), child )
        self.model.invisibleRootItem().appendRow([ QtGui.QStandardItem() for _ in range( self.model.columnCount() )])
        for column, d in enumerate(data0['data']):
            self.model.invisibleRootItem().child(0, column).setData( d, QtCore.Qt.ItemDataRole.DisplayRole )
        add_children( self.model.invisibleRootItem().child(0, 0), data0 )

    def load_tree_from_iCAD(self):

        def infparttree_to_list(inf_part_tree):
            inf_part = inf_part_tree.inf
            return [
                inf_part.name,
                inf_part.comment,
                inf_part.path if inf_part.is_external else '',
                inf_part.ref_model_name if inf_part.is_external else '',
                inf_part.is_external,
                inf_part.is_mirror,
                inf_part.is_read_only,
                inf_part.is_unloaded
            ]
        
        def get_parts(inf_part_tree):
            parts0 = []
            if inf_part_tree.child_list is not None:
                for child_inf_part_tree in inf_part_tree.child_list:
                    child = { 'data' : infparttree_to_list(child_inf_part_tree), 'children' : [] }
                    if child_inf_part_tree.child_list is not None:
                        child['children'] = get_parts(child_inf_part_tree)
                    parts0.append(child)
            return parts0
        
        if self.sxnet is None:
            return
        
        active_wf = self.sxnet.SxWF.getActive()
        if active_wf is None:
            return
        
        inf_part_tree = active_wf.getInfPartTree()
        if inf_part_tree is None:
            return
        
        parts = { 'data' : infparttree_to_list(inf_part_tree) }
        if inf_part_tree.child_list is not None:
            parts['children'] = get_parts(inf_part_tree)
        
        with open('data.json', mode='w', encoding='utf8') as f:
            json.dump(parts, f, ensure_ascii=False, indent=4)
            
        self.set_dict(parts)


class ProxyModel(QtCore.QSortFilterProxyModel):
    def filterAcceptsRow(self, source_row: int, source_parent: QtCore.QModelIndex) -> bool:
        source_index = self.sourceModel().index(source_row, 0, source_parent)
        child_count = self.sourceModel().rowCount(source_index)
        if child_count == 0:
            return super().filterAcceptsRow(source_row, source_parent)
        return True

if __name__ == '__main__':
    import sys
    app = QtWidgets.QApplication(sys.argv)
    widget = iCADtree()
    widget.resize(800, 600)
    widget.show()
    app.exec()

Share post

Related Posts

コメント