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