iCAD SXのツリー編集ソフトを作ってみた

2023/11/19 categories:iCAD SX| tags:iCAD SDK|iCAD SX|Python|PySide6|QTreeView|

動作の様子

取得をクリックするとツリーを取得して、取得したデータを書き換えるとCAD上のデータも書き換わります。図面名を変更したときに、変更後の名前のファイルが既に存在していたらエラーを表示するようにしています。また、切り出されていないパーツの図面名を変更したときもエラーを表示するようにしています。データを書き換えるときにCTRL+CやCTRL+Vが使えます。

Pythonのバージョン

Python 3.10.11 64-bit

使用しているパッケージ

Package                   Version
------------------------- ------------
PySide6                   6.5.2
pythonnet                 3.0.1

ツリーの取得

sxnetからアクティブなWFを取得して、WFからinf_part_treeを取得します。inf_part_tree.child_listがNoneでなければ、for child in childrenで子部品を再帰的に取得します。

......
    
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
    
......
    
def get_children(parent_item: QtGui.QStandardItem, inf_part_tree):

    ......

    children = inf_part_tree.child_list
    if children is not None:
        for child in children:
            get_children(items[0], child)

......

ツリーの変更

inf_part_treeにentpartがなければトップパーツなので、self.sxnet.SxWF.setNameTopPartでデータを変更します。entpartがあれば、ent_part.setNameでデータを変更しています。

......

ent_part = inf_part_tree.entpart
if ent_part is None:
    self.sxnet.SxWF.setNameTopPart(data[0], data[1])
else:
    ent_part.setName(data[0], data[1], data[2], False)

......

Pythonコード

import os
import time
import traceback
from pathlib import Path
from PySide6 import QtWidgets, QtGui, QtCore
from typing import Any


class iCADtree(QtWidgets.QWidget):

    def __init__(self, app: QtWidgets.QApplication) -> None:
        super().__init__()

        self.sxnet = None
        self.header = ['パーツ名', 'コメント', '図面名', '外部', 'ミラー', '読取専用', '未解決', 'パス']
        self.data_changer_thread = QtCore.QThread()
        self.data_changer = DataChanger()
        self.data_changer.moveToThread(self.data_changer_thread)
        self.data_changer.errored.connect(self.show_error)
        self.data_changer_thread.started.connect(self.data_changer.run)

        self.model = Model()
        self.model.setHorizontalHeaderLabels(self.header)
        self.model.errored.connect(self.show_error)
        
        self.proxy_model = ProxyModel()
        self.proxy_model.setSourceModel(self.model)

        self.view = TreeView(app)
        self.view.setModel(self.proxy_model)
        self.view.setColumnWidth(0, 200)
        self.view.setColumnWidth(3, 32)
        self.view.setColumnWidth(4, 32)
        self.view.setColumnWidth(5, 57)
        self.view.setColumnWidth(6, 43)

        self.filter_column = QtWidgets.QComboBox()
        self.filter_column.addItems(self.header)

        self.load_from_iCAD_button = Button('読込')
        self.set_iCAD_button = Button('読込')
        self.expand_all_button = Button('展開')
        self.collapse_all_button = Button('折畳み')
        self.filter_text = QtWidgets.QLineEdit()
        self.filter_text.setPlaceholderText('フィルター文字列')
        self.sort_checkbox = QtWidgets.QCheckBox('ソート')

        menu = QtWidgets.QToolBar()
        menu.layout().setSpacing(3)
        menu.addWidget(self.load_from_iCAD_button)
        menu.addSeparator()
        menu.addWidget(self.expand_all_button)
        menu.addWidget(self.collapse_all_button)
        menu.addSeparator()
        menu.addWidget(self.sort_checkbox)
        menu.addSeparator()
        menu.addWidget(QtWidgets.QLabel('フィルター列'))
        menu.addWidget(self.filter_column)
        menu.addWidget(self.filter_text)

        self.setWindowFlags(self.windowFlags() | QtCore.Qt.WindowType.WindowStaysOnTopHint)
        self.resize(800, 600)
        self.setWindowTitle('iCAD TreeView')
        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_from_iCAD_button.clicked.connect(self.load_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_changed)
        self.filter_column.currentIndexChanged.connect(self.fitler_changed)
        self.sort_checkbox.stateChanged.connect(self.sort_checkbox_changed)
        self.model.dataChanged.connect(self.model_data_changed)

        self.init_iCAD()
        self.data_changer.sxnet = self.sxnet
        self.data_changer_thread.start()

    def show_error(self, message):
        m = QtWidgets.QMessageBox(self)
        m.setText(message)
        m.setWindowTitle('エラー')
        m.setWindowFlags(m.windowFlags() | QtCore.Qt.WindowType.WindowStaysOnTopHint)
        m.show()

    def closeEvent(self, event: QtGui.QCloseEvent) -> None:
        self.data_changer.stopped = True
        self.data_changer_thread.quit()
        return super().closeEvent(event)

    def model_data_changed(self, lefttop: QtCore.QModelIndex, rightbottom: QtCore.QModelIndex, roles: list[int]):
        if lefttop.column() in [0, 1, 2]:
            inf_part_tree = self.model.data(lefttop.sibling(lefttop.row(), 0), self.model.INF_PART_TREE_ROLE)
            data = [
                self.model.data(lefttop.sibling(lefttop.row(), 0), QtCore.Qt.ItemDataRole.DisplayRole),
                self.model.data(lefttop.sibling(lefttop.row(), 1), QtCore.Qt.ItemDataRole.DisplayRole),
                self.model.data(lefttop.sibling(lefttop.row(), 1), QtCore.Qt.ItemDataRole.DisplayRole)
            ]
            self.data_changer.changes.append([inf_part_tree, data])

    def sort_checkbox_changed(self, state):
        self.view.setSortingEnabled(state == QtCore.Qt.CheckState.Checked.value)

    def fitler_changed(self):
        filter_text = '{}*'.format( self.filter_text.text() )
        regular_expression = QtCore.QRegularExpression()
        regular_expression.setPatternOptions(regular_expression.PatternOption.CaseInsensitiveOption)
        text = regular_expression.wildcardToRegularExpression(filter_text, regular_expression.WildcardConversionOption.DefaultWildcardConversion)
        regular_expression.setPattern(text)
        self.proxy_model.setFilterKeyColumn(self.filter_column.currentIndex())
        self.proxy_model.setFilterRegularExpression(regular_expression)

    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.removeRows(0, self.model.rowCount())
        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_from_iCAD(self):

        def get_children(parent_item: QtGui.QStandardItem, inf_part_tree):
            inf_part = inf_part_tree.inf
            items = [
                QtGui.QStandardItem(inf_part.name),
                QtGui.QStandardItem(inf_part.comment),
                QtGui.QStandardItem(inf_part.ref_model_name if inf_part.is_external else ''),
                QtGui.QStandardItem('〇' if inf_part.is_external else ''),
                QtGui.QStandardItem('〇' if inf_part.is_mirror else ''),
                QtGui.QStandardItem('〇' if inf_part.is_read_only else ''),
                QtGui.QStandardItem('〇' if inf_part.is_unloaded else ''),
                QtGui.QStandardItem(inf_part.path if inf_part.is_external else '')
            ]
            items[0].setData(inf_part_tree, self.model.INF_PART_TREE_ROLE)

            parent_item.appendRow(items)

            children = inf_part_tree.child_list
            if children is not None:
                for child in children:
                    get_children(items[0], child)

        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
        
        self.data_changer.paused = True
        self.model.dataChanged.disconnect(self.model_data_changed)
        self.model.removeRows(0, self.model.rowCount())
        get_children(self.model.invisibleRootItem(), inf_part_tree)
        self.model.dataChanged.connect(self.model_data_changed)
        self.data_changer.changes = []
        self.data_changer.paused = False

        self.view.expandAll()


class DataChanger(QtCore.QObject):

    errored = QtCore.Signal(str)

    def __init__(self) -> None:
        super().__init__()
        self.sxnet = None
        self.stopped = False
        self.paused = False
        self.changes = []

    def run(self):
        while not self.stopped:
            time.sleep(0.1)

            if self.paused:
                while self.paused:
                    time.sleep(0.1)
                    pass

            if len(self.changes) == 0:
                continue
            
            while len(self.changes) > 0:
                try:
                    inf_part_tree, data = self.changes.pop(0)
                    ent_part = inf_part_tree.entpart
                    if ent_part is None:
                        self.sxnet.SxWF.setNameTopPart(data[0], data[1])
                    else:
                        ent_part.setName(data[0], data[1], data[2], False)
                except:
                    self.errored.emit(traceback.format_exc())


class Model(QtGui.QStandardItemModel):

    # [0'パーツ名', 1'コメント', 2'図面名', 3'外部', 4'ミラー', 5'読取専用', 6'未解決', 7'パス']
    INF_PART_TREE_ROLE = QtCore.Qt.ItemDataRole.UserRole + 1
    errored = QtCore.Signal(str)

    def flags(self, index: QtCore.QModelIndex | QtCore.QPersistentModelIndex) -> QtCore.Qt.ItemFlag:
        if index.column() in [0, 1, 2]:
            return super().flags(index)
        else:
            return QtCore.Qt.ItemFlag.ItemIsEnabled | QtCore.Qt.ItemFlag.ItemIsSelectable
    
    def setData(self, index: QtCore.QModelIndex | QtCore.QPersistentModelIndex, value: Any, role: int = ...) -> bool:
        if index.column() == 2:
            path = self.data( index.sibling(index.row(), 7), QtCore.Qt.ItemDataRole.DisplayRole )
            if path is None or path == '':
                self.errored.emit('パーツが切り出されていないので、図面名「{}」への変更はできません'.format(value))
                return False
            path = Path(path) / '{}.icd'.format(value)
            if path.exists():
                self.errored.emit('図面名「{}」がパス「{}」に存在するので、その図面名への変更はできません'.format(value, path))
                return False
            return False
        return super().setData(index, value, role)
    
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


class TreeView(QtWidgets.QTreeView):

    def __init__(self, app: QtWidgets.QApplication, context_menu_slot=None) -> None:
        super().__init__()
        self.app: QtWidgets.QApplication = app
        self.header().setSectionsClickable(True)
        self.setSelectionMode(self.SelectionMode.ExtendedSelection)
        self.setSelectionBehavior(self.SelectionBehavior.SelectItems)
        self.sortByColumn(0, QtCore.Qt.SortOrder.AscendingOrder)
        
        if context_menu_slot is not None:
            self.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.CustomContextMenu)
            self.customContextMenuRequested.connect(context_menu_slot)

        self.setStyleSheet('''
            QTreeView {
                font-size: 13px;
            }
            QTreeView::item {
                color: #000;
                border: 1px solid #ccc;
                border-right-color: transparent;
                border-top-color: transparent;
            }
            QTreeView::item:hover {
                border: 2px solid #BBD8FF;
            }
            QTreeView::item:selected{
                border: 2px solid #3986FF;
            }
            QHeaderView { 
                border: 1px solid #ccc; 
            }
        ''')

    def event(self, event: QtGui.QKeyEvent) -> bool:
        if event.type() == QtCore.QEvent.Type.KeyPress:
            if event.modifiers() == QtCore.Qt.KeyboardModifier.ControlModifier:
                if event.key() == QtCore.Qt.Key.Key_C:
                    self.view_to_clipboard()
                    return True
                if event.key() == QtCore.Qt.Key.Key_V:
                    self.clipboard_to_view()
                    return True
        return super().event(event)
    
    def clipboard_to_view(self):
        clipboard = self.app.clipboard()
        text = clipboard.text()
        data = [ line.split('\t') for line in text.splitlines() ]

        if len(data) == 0:
            return

        selected_indexes = self.selectedIndexes()
        model: QtCore.QSortFilterProxyModel = self.model()

        if len(data) == 1 and len(data[0]) == 1:
            value = data[0][0]
            for index in selected_indexes:
                model.setData(index, value, QtCore.Qt.ItemDataRole.DisplayRole)
            return
        
        last_index = selected_indexes[0]
        last_row = last_index.row()
        last_parent = model.sibling(last_row, 0, last_index).parent()
        start_column = last_index.column()
        
        for row_data in data:

            for c, value in enumerate(row_data):
                column = start_column + c

                index = model.index(last_row, column, last_parent)
                model.setData(index, value, QtCore.Qt.ItemDataRole.DisplayRole)

            index = model.index(last_row, 0, last_parent)
            if model.rowCount(index) > 0:
                last_parent = index
                last_row = 0
                continue
            
            last_row += 1

            if model.rowCount(last_parent) <= last_row:
                parent = last_parent
                for _ in range(100):
                    row = parent.row() + 1
                    parent = parent.parent()
                    if model.rowCount(parent) <= row:
                        continue
                    last_parent = parent
                    last_row = row
                    break
                continue


    def view_to_clipboard(self):

        def selected_data_to_list(parent):
            last_row, datas, selected = -1, [], self.selectedIndexes()
            for row in range(self.model().rowCount(parent)):

                for column in range(self.model().columnCount()):
                    index = self.model().index(row, column, parent)
                    if not index in selected:
                        continue

                    if last_row != row:
                        datas.append([])
                        last_row = row
                    datas[-1].append(index.data())

                child_datas = selected_data_to_list( self.model().index(row, 0, parent) )
                datas.extend(child_datas)
            return datas
        
        datas = selected_data_to_list(QtCore.QModelIndex())
        text = '\n'.join(['\t'.join([ str(data) for data in row_data ]) for row_data in datas])
        clipboard = self.app.clipboard()
        clipboard.setText(text)


class Button(QtWidgets.QToolButton):
    def __init__(self, text: str):
        super().__init__()
        self.setText(text)
        self.setSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum)
        self.setStyleSheet('''
            QToolButton{ border: 1px solid #cccccc; border-radius: 3px; padding: 1px 5px 1px 5px; }
            QToolButton:hover{ background-color: #dddddd }
            QToolButton:pressed{ background-color: #eeeeee }
        ''')


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

Share post

Related Posts

コメント