Skip to content

Instantly share code, notes, and snippets.

@stephensmitchell
Last active July 17, 2026 00:21
Show Gist options
  • Select an option

  • Save stephensmitchell/2458ea2d1bc749cee3e06c39d18f1b18 to your computer and use it in GitHub Desktop.

Select an option

Save stephensmitchell/2458ea2d1bc749cee3e06c39d18f1b18 to your computer and use it in GitHub Desktop.
# created with Alibre Script Genie by Stephen S. Mitchell, https://github.com/stephensmitchell
import clr
import sys
clr.AddReference("System.Windows.Forms")
clr.AddReference("System.Drawing")
clr.AddReference("System.ComponentModel")
from System.Windows.Forms import *
from System.Drawing import *
from System.ComponentModel import *
from System import *
def printTraceBack():
import traceback
print("\n--------------\n" + str(traceback.format_exc()) + "--------------\n")
def show_error(message, title="Custom Property Explorer"):
try:
Win = Windows()
Win.ErrorDialog(str(message), str(title))
except:
print("ERROR: " + str(message))
def show_info(message, title="Custom Property Explorer"):
try:
Win = Windows()
Win.InfoDialog(str(message), str(title))
except:
print(str(message))
def safe_try(action_name, func):
try:
return True, func()
except Exception as ex:
print("Action failed: " + str(action_name))
print("Reason: " + str(ex))
printTraceBack()
return False, None
def CreateCustomPropertyExplorer():
# Resolve current model
MyModel = None
IsAssembly = False
try:
MyModel = CurrentPart()
except:
try:
MyModel = CurrentAssembly()
IsAssembly = True
except:
show_error("No active part or assembly found.\n\nPlease open a part or assembly first.")
return
# Default visible custom properties from the original script
custom_property_names = ["A", "B", "C", "D", "E", "F", "G"]
# DPI scaling
try:
dpi_scale = Graphics.FromHwnd(IntPtr.Zero).DpiX / 96.0
except:
dpi_scale = 1.0
def scale(value):
return int(value * dpi_scale)
class Colors:
PRIMARY = Color.FromArgb(45, 45, 48)
SECONDARY = Color.FromArgb(62, 62, 66)
ACCENT = Color.FromArgb(0, 122, 204)
SURFACE = Color.FromArgb(37, 37, 38)
TEXT_PRIMARY = Color.FromArgb(241, 241, 241)
TEXT_SECONDARY = Color.FromArgb(204, 204, 204)
BORDER = Color.FromArgb(63, 63, 70)
SUCCESS = Color.FromArgb(73, 156, 84)
WARNING = Color.FromArgb(252, 196, 25)
ERROR = Color.FromArgb(255, 100, 100)
# -------------------------
# COMMENTED OUT / REMOVED:
# - real-time Alibre selection tracking
# - geometry tree population
# - feature/sketch/plane/axis/point discovery
# - non-custom property wrappers
# -------------------------
class CustomPropertyWrapper(object):
def __init__(self, model, prop_names):
self._model = model
self._properties = {}
self._load_custom_properties(prop_names)
def _load_custom_properties(self, prop_names):
for cp_name in prop_names:
try:
val = self._model.GetCustomProperty(cp_name)
if val is None:
self._properties["Custom: " + cp_name] = ""
else:
self._properties["Custom: " + cp_name] = str(val)
except:
self._properties["Custom: " + cp_name] = "(not defined)"
def GetAllProperties(self):
return self._properties
class CustomPropertyDisplayObject(object):
def __init__(self, model, prop_names):
self._model = model
self._prop_names = list(prop_names)
wrapper = CustomPropertyWrapper(model, prop_names)
for prop_name, prop_value in wrapper.GetAllProperties().items():
clean_prop_name = prop_name.replace(" ", "_").replace("-", "_").replace("(", "").replace(")", "")
setattr(self, clean_prop_name, prop_value)
form = Form()
form.Text = "Custom Property Explorer - " + str(MyModel.Name)
form.Width = scale(1000)
form.Height = scale(700)
form.StartPosition = FormStartPosition.CenterScreen
form.BackColor = Colors.PRIMARY
form.ForeColor = Colors.TEXT_PRIMARY
form.Font = Font("Segoe UI", scale(9), FontStyle.Regular)
try:
form.AutoScaleMode = AutoScaleMode.Dpi
form.AutoScaleDimensions = SizeF(96, 96)
except:
pass
statusStrip = StatusStrip()
statusStrip.BackColor = Colors.SURFACE
statusStrip.ForeColor = Colors.TEXT_SECONDARY
statusStrip.Font = Font("Segoe UI", scale(9))
statusStrip.Padding = Padding(scale(12), scale(6), scale(12), scale(6))
statusLabel = ToolStripStatusLabel()
statusLabel.Text = "Ready - Model: " + str(MyModel.Name)
statusLabel.Spring = True
statusLabel.TextAlign = ContentAlignment.MiddleLeft
selectionLabel = ToolStripStatusLabel()
selectionLabel.Text = "Custom properties only"
statusStrip.Items.AddRange(Array[ToolStripItem]((statusLabel, selectionLabel)))
form.Controls.Add(statusStrip)
mainSplitter = SplitContainer()
mainSplitter.Dock = DockStyle.Fill
mainSplitter.Orientation = Orientation.Vertical
mainSplitter.SplitterDistance = scale(380)
mainSplitter.BackColor = Colors.BORDER
mainSplitter.SplitterWidth = scale(8)
form.Controls.Add(mainSplitter)
leftPanel = Panel()
leftPanel.Dock = DockStyle.Fill
leftPanel.BackColor = Colors.SECONDARY
leftPanel.Padding = Padding(scale(12))
mainSplitter.Panel1.Controls.Add(leftPanel)
rightPanel = Panel()
rightPanel.Dock = DockStyle.Fill
rightPanel.BackColor = Colors.SECONDARY
rightPanel.Padding = Padding(scale(12))
mainSplitter.Panel2.Controls.Add(rightPanel)
treeHeader = Label()
treeHeader.Text = "Custom Properties"
treeHeader.Font = Font("Segoe UI", scale(10), FontStyle.Bold)
treeHeader.ForeColor = Colors.TEXT_PRIMARY
treeHeader.BackColor = Colors.SURFACE
treeHeader.Height = scale(40)
treeHeader.TextAlign = ContentAlignment.MiddleLeft
treeHeader.Padding = Padding(scale(12), 0, 0, 0)
treeHeader.Dock = DockStyle.Top
propHeader = Label()
propHeader.Text = "Property Grid"
propHeader.Font = Font("Segoe UI", scale(10), FontStyle.Bold)
propHeader.ForeColor = Colors.TEXT_PRIMARY
propHeader.BackColor = Colors.SURFACE
propHeader.Height = scale(40)
propHeader.TextAlign = ContentAlignment.MiddleLeft
propHeader.Padding = Padding(scale(12), 0, 0, 0)
propHeader.Dock = DockStyle.Top
toolStrip = ToolStrip()
toolStrip.BackColor = Colors.SURFACE
toolStrip.ForeColor = Colors.TEXT_PRIMARY
toolStrip.Font = Font("Segoe UI", scale(9))
toolStrip.Renderer = ToolStripProfessionalRenderer()
toolStrip.GripStyle = ToolStripGripStyle.Hidden
toolStrip.Padding = Padding(scale(10), scale(6), scale(10), scale(6))
toolStrip.AutoSize = False
toolStrip.Height = scale(52)
toolStrip.Dock = DockStyle.Top
customPropButton = ToolStripButton("Lookup Custom Prop")
customPropButton.ForeColor = Colors.WARNING
customPropButton.ToolTipText = "Lookup a custom property by name"
customPropButton.AutoSize = True
customPropButton.Font = Font("Segoe UI", scale(9))
setPropButton = ToolStripButton("Set Custom Prop")
setPropButton.ForeColor = Colors.WARNING
setPropButton.ToolTipText = "Set a custom property value"
setPropButton.AutoSize = True
setPropButton.Font = Font("Segoe UI", scale(9))
toolStrip.Items.AddRange(Array[ToolStripItem]((customPropButton, setPropButton)))
treeView = TreeView()
treeView.Dock = DockStyle.Fill
treeView.Font = Font("Segoe UI", scale(9))
treeView.BackColor = Colors.PRIMARY
treeView.ForeColor = Colors.TEXT_PRIMARY
treeView.LineColor = Colors.BORDER
treeView.HideSelection = False
treeView.FullRowSelect = True
treeView.BorderStyle = BorderStyle.None
treeView.Indent = scale(24)
treeView.ItemHeight = scale(28)
propertyGrid = PropertyGrid()
propertyGrid.Dock = DockStyle.Fill
propertyGrid.BackColor = Colors.PRIMARY
propertyGrid.LineColor = Colors.BORDER
propertyGrid.CategoryForeColor = Colors.TEXT_PRIMARY
propertyGrid.CategorySplitterColor = Colors.BORDER
propertyGrid.HelpBackColor = Colors.SURFACE
propertyGrid.HelpForeColor = Colors.TEXT_SECONDARY
propertyGrid.ViewBackColor = Colors.PRIMARY
propertyGrid.ViewForeColor = Colors.TEXT_PRIMARY
propertyGrid.ToolbarVisible = True
propertyGrid.Font = Font("Segoe UI", scale(9))
propertyGrid.PropertySort = PropertySort.Categorized
propertyGrid.HelpVisible = True
leftPanel.Controls.Add(treeView)
leftPanel.Controls.Add(toolStrip)
leftPanel.Controls.Add(treeHeader)
rightPanel.Controls.Add(propertyGrid)
rightPanel.Controls.Add(propHeader)
def UpdateStatusBar(message, selection_text=""):
statusLabel.Text = str(message)
selectionLabel.Text = str(selection_text)
def EnsureCustomPropertyInList(prop_name):
if prop_name is None:
return
prop_name = str(prop_name).strip()
if prop_name == "":
return
if prop_name not in custom_property_names:
custom_property_names.append(prop_name)
def GetDisplayedCustomPropertyPairs():
pairs = []
for cp_name in custom_property_names:
try:
val = MyModel.GetCustomProperty(cp_name)
if val is None:
pairs.append((cp_name, ""))
else:
pairs.append((cp_name, str(val)))
except:
pairs.append((cp_name, "(not defined)"))
return pairs
def BuildTreeNodes():
treeView.BeginUpdate()
treeView.Nodes.Clear()
rootNode = TreeNode(str(MyModel.Name))
rootNode.Tag = MyModel
treeView.Nodes.Add(rootNode)
pairs = GetDisplayedCustomPropertyPairs()
cpNode = TreeNode("Custom Properties (%d)" % len(pairs))
cpNode.Tag = MyModel
rootNode.Nodes.Add(cpNode)
for cp_name, cp_val in pairs:
childNode = TreeNode(str(cp_name) + " = " + str(cp_val))
childNode.Tag = MyModel
cpNode.Nodes.Add(childNode)
rootNode.Expand()
cpNode.Expand()
treeView.SelectedNode = cpNode
treeView.EndUpdate()
propertyGrid.SelectedObject = CustomPropertyDisplayObject(MyModel, custom_property_names)
UpdateStatusBar("Ready - Model: " + str(MyModel.Name), "Loaded custom properties")
def OnNodeSelect(sender, args):
try:
if treeView.SelectedNode is None:
propertyGrid.SelectedObject = None
UpdateStatusBar("Ready - Model: " + str(MyModel.Name), "No selection")
return
propertyGrid.SelectedObject = CustomPropertyDisplayObject(MyModel, custom_property_names)
UpdateStatusBar("Ready - Model: " + str(MyModel.Name), "Selected: " + str(treeView.SelectedNode.Text))
except:
propertyGrid.SelectedObject = None
printTraceBack()
def OnCustomPropClick(sender, args):
dlg = Form()
dlg.Text = "Lookup Custom Property"
dlg.Width = scale(400)
dlg.Height = scale(220)
dlg.StartPosition = FormStartPosition.CenterParent
dlg.BackColor = Colors.PRIMARY
dlg.ForeColor = Colors.TEXT_PRIMARY
dlg.Font = Font("Segoe UI", scale(9))
dlg.FormBorderStyle = FormBorderStyle.FixedDialog
dlg.MaximizeBox = False
dlg.MinimizeBox = False
lbl = Label()
lbl.Text = "Property Name:"
lbl.Location = Point(scale(20), scale(20))
lbl.Size = Size(scale(340), scale(25))
lbl.ForeColor = Colors.TEXT_PRIMARY
dlg.Controls.Add(lbl)
txtName = TextBox()
txtName.Location = Point(scale(20), scale(50))
txtName.Size = Size(scale(340), scale(28))
txtName.BackColor = Colors.SURFACE
txtName.ForeColor = Colors.TEXT_PRIMARY
txtName.Font = Font("Segoe UI", scale(9))
dlg.Controls.Add(txtName)
lblResult = Label()
lblResult.Text = ""
lblResult.Location = Point(scale(20), scale(90))
lblResult.Size = Size(scale(340), scale(40))
lblResult.ForeColor = Colors.TEXT_SECONDARY
dlg.Controls.Add(lblResult)
def OnLookup(s, e):
prop_name = txtName.Text.strip()
if prop_name == "":
lblResult.Text = "Please enter a property name."
lblResult.ForeColor = Colors.ERROR
return
try:
val = MyModel.GetCustomProperty(prop_name)
EnsureCustomPropertyInList(prop_name)
if val is not None:
lblResult.Text = prop_name + " = " + str(val)
lblResult.ForeColor = Colors.SUCCESS
else:
lblResult.Text = prop_name + " = (empty)"
lblResult.ForeColor = Colors.TEXT_SECONDARY
BuildTreeNodes()
UpdateStatusBar("Ready - Model: " + str(MyModel.Name), "Looked up: " + prop_name)
except Exception as ex:
lblResult.Text = "Not found or error: " + str(ex)
lblResult.ForeColor = Colors.ERROR
UpdateStatusBar("Ready - Model: " + str(MyModel.Name), "Lookup failed")
btnLookup = Button()
btnLookup.Text = "Lookup"
btnLookup.Location = Point(scale(20), scale(140))
btnLookup.Size = Size(scale(100), scale(32))
btnLookup.BackColor = Colors.ACCENT
btnLookup.ForeColor = Colors.TEXT_PRIMARY
btnLookup.FlatStyle = FlatStyle.Flat
btnLookup.Click += OnLookup
dlg.Controls.Add(btnLookup)
dlg.AcceptButton = btnLookup
btnClose = Button()
btnClose.Text = "Close"
btnClose.Location = Point(scale(260), scale(140))
btnClose.Size = Size(scale(100), scale(32))
btnClose.BackColor = Colors.SECONDARY
btnClose.ForeColor = Colors.TEXT_PRIMARY
btnClose.FlatStyle = FlatStyle.Flat
btnClose.Click += lambda s, e: dlg.Close()
dlg.Controls.Add(btnClose)
dlg.CancelButton = btnClose
dlg.ShowDialog()
def OnSetPropClick(sender, args):
dlg = Form()
dlg.Text = "Set Custom Property"
dlg.Width = scale(400)
dlg.Height = scale(280)
dlg.StartPosition = FormStartPosition.CenterParent
dlg.BackColor = Colors.PRIMARY
dlg.ForeColor = Colors.TEXT_PRIMARY
dlg.Font = Font("Segoe UI", scale(9))
dlg.FormBorderStyle = FormBorderStyle.FixedDialog
dlg.MaximizeBox = False
dlg.MinimizeBox = False
lblName = Label()
lblName.Text = "Property Name:"
lblName.Location = Point(scale(20), scale(20))
lblName.Size = Size(scale(340), scale(25))
lblName.ForeColor = Colors.TEXT_PRIMARY
dlg.Controls.Add(lblName)
txtName = TextBox()
txtName.Location = Point(scale(20), scale(50))
txtName.Size = Size(scale(340), scale(28))
txtName.BackColor = Colors.SURFACE
txtName.ForeColor = Colors.TEXT_PRIMARY
txtName.Font = Font("Segoe UI", scale(9))
dlg.Controls.Add(txtName)
lblVal = Label()
lblVal.Text = "Property Value:"
lblVal.Location = Point(scale(20), scale(90))
lblVal.Size = Size(scale(340), scale(25))
lblVal.ForeColor = Colors.TEXT_PRIMARY
dlg.Controls.Add(lblVal)
txtVal = TextBox()
txtVal.Location = Point(scale(20), scale(120))
txtVal.Size = Size(scale(340), scale(28))
txtVal.BackColor = Colors.SURFACE
txtVal.ForeColor = Colors.TEXT_PRIMARY
txtVal.Font = Font("Segoe UI", scale(9))
dlg.Controls.Add(txtVal)
lblResult = Label()
lblResult.Text = ""
lblResult.Location = Point(scale(20), scale(160))
lblResult.Size = Size(scale(340), scale(40))
lblResult.ForeColor = Colors.TEXT_SECONDARY
dlg.Controls.Add(lblResult)
def OnSet(s, e):
prop_name = txtName.Text.strip()
prop_val = txtVal.Text
if prop_name == "":
lblResult.Text = "Please enter a property name."
lblResult.ForeColor = Colors.ERROR
return
try:
MyModel.SetCustomProperty(prop_name, prop_val)
EnsureCustomPropertyInList(prop_name)
BuildTreeNodes()
lblResult.Text = "Set " + prop_name + " = " + prop_val
lblResult.ForeColor = Colors.SUCCESS
UpdateStatusBar("Ready - Model: " + str(MyModel.Name), "Set: " + prop_name)
except Exception as ex:
lblResult.Text = "Error: " + str(ex)
lblResult.ForeColor = Colors.ERROR
btnSet = Button()
btnSet.Text = "Set"
btnSet.Location = Point(scale(20), scale(210))
btnSet.Size = Size(scale(100), scale(32))
btnSet.BackColor = Colors.ACCENT
btnSet.ForeColor = Colors.TEXT_PRIMARY
btnSet.FlatStyle = FlatStyle.Flat
btnSet.Click += OnSet
dlg.Controls.Add(btnSet)
dlg.AcceptButton = btnSet
btnClose = Button()
btnClose.Text = "Close"
btnClose.Location = Point(scale(260), scale(210))
btnClose.Size = Size(scale(100), scale(32))
btnClose.BackColor = Colors.SECONDARY
btnClose.ForeColor = Colors.TEXT_PRIMARY
btnClose.FlatStyle = FlatStyle.Flat
btnClose.Click += lambda s, e: dlg.Close()
dlg.Controls.Add(btnClose)
dlg.CancelButton = btnClose
dlg.ShowDialog()
def OnPropertyValueChanged(sender, args):
try:
label = args.ChangedItem.Label
if label is None:
return
if label.startswith("Custom:_"):
cp_name = label[len("Custom:_"):]
new_val = str(args.ChangedItem.Value)
try:
MyModel.SetCustomProperty(cp_name, new_val)
EnsureCustomPropertyInList(cp_name)
BuildTreeNodes()
UpdateStatusBar("Ready - Model: " + str(MyModel.Name), "Updated: " + cp_name + " = " + new_val)
except Exception as ex:
show_error(
"Failed to set custom property '" + str(cp_name) + "':\n" + str(ex) +
"\n\nThe property must already be defined on the part/assembly or on your PC.",
"Custom Property Error"
)
except:
printTraceBack()
treeView.AfterSelect += OnNodeSelect
customPropButton.Click += OnCustomPropClick
setPropButton.Click += OnSetPropClick
propertyGrid.PropertyValueChanged += OnPropertyValueChanged
BuildTreeNodes()
form.ShowDialog()
safe_try("CreateCustomPropertyExplorer", CreateCustomPropertyExplorer)
# created with Alibre Script Genie by Stephen S. Mitchell, https://github.com/stephensmitchell
import sys
import clr
import math
# Import Windows Forms
clr.AddReference("System.Windows.Forms")
clr.AddReference("System.Drawing")
clr.AddReference("System.ComponentModel")
from System.Windows.Forms import *
from System.Drawing import *
from System.ComponentModel import *
from System import *
def CreatePropertyExplorer():
# Get the current model (try part first, then assembly)
MyModel = None
IsAssembly = False
try:
#Units.Current = UnitTypes.Centimeters
#Units.Current = UnitTypes.Millimeters
Units.Current = UnitTypes.Inches
MyModel = CurrentPart()
except:
try:
MyModel = CurrentAssembly()
IsAssembly = True
except:
Win = Windows()
Win.ErrorDialog("No active part or assembly found.\n\nPlease open or create a part/assembly first.", "Alibre Property Explorer")
return
# High DPI support - detect current DPI scaling
import System.Windows.Forms.Application as WinApp
try:
# Get current DPI scaling factor
dpi_scale = System.Drawing.Graphics.FromHwnd(System.IntPtr.Zero).DpiX / 96.0
except:
dpi_scale = 2.0 # Default to 200% for 5K displays
# Scale factors for high DPI
def scale(value):
return int(value * dpi_scale)
# Professional color scheme
class Colors:
PRIMARY = Color.FromArgb(45, 45, 48) # Dark gray
SECONDARY = Color.FromArgb(62, 62, 66) # Medium gray
ACCENT = Color.FromArgb(0, 122, 204) # Blue accent
SURFACE = Color.FromArgb(37, 37, 38) # Darker surface
TEXT_PRIMARY = Color.FromArgb(241, 241, 241) # Light text
TEXT_SECONDARY = Color.FromArgb(204, 204, 204) # Medium text
BORDER = Color.FromArgb(63, 63, 70) # Border color
SUCCESS = Color.FromArgb(73, 156, 84) # Green
WARNING = Color.FromArgb(252, 196, 25) # Orange
# Create the main form with high DPI awareness
form = Form()
form.Text = "Alibre Property Explorer - " + MyModel.Name
form.Width = scale(1600)
form.Height = scale(1000)
form.StartPosition = FormStartPosition.CenterScreen
form.BackColor = Colors.PRIMARY
form.ForeColor = Colors.TEXT_PRIMARY
form.Font = Font("Segoe UI", scale(9), FontStyle.Regular)
form.MinimumSize = Size(scale(1300), scale(800))
# Enable high DPI support
try:
form.AutoScaleMode = AutoScaleMode.Dpi
form.AutoScaleDimensions = SizeF(scale(96), scale(96))
except:
pass
# Add professional icon if possible
try:
form.ShowIcon = True
except:
pass
# Create status bar with DPI scaling
statusStrip = StatusStrip()
statusStrip.BackColor = Colors.SURFACE
statusStrip.ForeColor = Colors.TEXT_SECONDARY
statusStrip.Font = Font("Segoe UI", scale(9))
statusStrip.Padding = Padding(scale(12), scale(6), scale(12), scale(6))
statusLabel = ToolStripStatusLabel()
statusLabel.Text = "Ready - Model: " + MyModel.Name
statusLabel.Spring = True
statusLabel.TextAlign = ContentAlignment.MiddleLeft
statusLabel.Margin = Padding(scale(8), scale(3), scale(8), scale(3))
selectionLabel = ToolStripStatusLabel()
selectionLabel.Text = "No selection"
selectionLabel.AutoSize = True
selectionLabel.Margin = Padding(scale(8), scale(3), scale(8), scale(3))
statusStrip.Items.AddRange(Array[ToolStripItem]((statusLabel, selectionLabel)))
form.Controls.Add(statusStrip)
# Main layout container with DPI scaling
mainSplitter = SplitContainer()
mainSplitter.Dock = DockStyle.Fill
mainSplitter.Orientation = Orientation.Vertical
mainSplitter.SplitterDistance = scale(800)
mainSplitter.BackColor = Colors.BORDER
mainSplitter.SplitterWidth = scale(8)
form.Controls.Add(mainSplitter)
# LEFT PANEL for TreeView with DPI scaling
leftPanel = Panel()
leftPanel.Dock = DockStyle.Fill
leftPanel.BackColor = Colors.SECONDARY
leftPanel.Padding = Padding(scale(16), scale(16), scale(12), scale(16))
mainSplitter.Panel1.Controls.Add(leftPanel)
# RIGHT PANEL for PropertyGrid with DPI scaling
rightPanel = Panel()
rightPanel.Dock = DockStyle.Fill
rightPanel.BackColor = Colors.SECONDARY
rightPanel.Padding = Padding(scale(12), scale(16), scale(16), scale(16))
mainSplitter.Panel2.Controls.Add(rightPanel)
# Add header labels with high DPI scaling FIRST
treeHeader = Label()
treeHeader.Text = "Model Structure"
treeHeader.Font = Font("Segoe UI", scale(10), FontStyle.Bold)
treeHeader.ForeColor = Colors.TEXT_PRIMARY
treeHeader.BackColor = Colors.SURFACE
treeHeader.Height = scale(50) # Extra height for high DPI
treeHeader.TextAlign = ContentAlignment.MiddleLeft
treeHeader.Padding = Padding(20, 0, 0, 0)
treeHeader.Dock = DockStyle.Top
propHeader = Label()
propHeader.Text = "Properties"
propHeader.Font = Font("Segoe UI", scale(10), FontStyle.Bold)
propHeader.ForeColor = Colors.TEXT_PRIMARY
propHeader.BackColor = Colors.SURFACE
propHeader.Height = scale(50) # Extra height for high DPI
propHeader.TextAlign = ContentAlignment.MiddleLeft
propHeader.Padding = Padding(scale(20), 0, 0, 0)
propHeader.Dock = DockStyle.Top
# Professional Toolbar with high DPI scaling
toolStrip = ToolStrip()
toolStrip.BackColor = Colors.SURFACE
toolStrip.ForeColor = Colors.TEXT_PRIMARY
toolStrip.Font = Font("Segoe UI", scale(9))
toolStrip.Renderer = ToolStripProfessionalRenderer()
toolStrip.GripStyle = ToolStripGripStyle.Hidden
toolStrip.Padding = Padding(scale(20), scale(8), scale(20), scale(8))
toolStrip.AutoSize = False
toolStrip.Height = scale(60) # Extra height for high DPI
toolStrip.LayoutStyle = ToolStripLayoutStyle.HorizontalStackWithOverflow
toolStrip.Dock = DockStyle.Top
# Create toolbar buttons with high DPI scaling
refreshButton = ToolStripButton("🔄 Refresh")
refreshButton.ForeColor = Colors.TEXT_PRIMARY
refreshButton.ToolTipText = "Refresh the model tree"
refreshButton.Margin = Padding(scale(10), scale(5), scale(10), scale(5))
refreshButton.Padding = Padding(scale(12), scale(6), scale(12), scale(6))
refreshButton.AutoSize = True
refreshButton.Font = Font("Segoe UI", scale(9))
expandAllButton = ToolStripButton("⊞ Expand All")
expandAllButton.ForeColor = Colors.TEXT_PRIMARY
expandAllButton.ToolTipText = "Expand all tree nodes"
expandAllButton.Margin = Padding(scale(10), scale(5), scale(10), scale(5))
expandAllButton.Padding = Padding(scale(12), scale(6), scale(12), scale(6))
expandAllButton.AutoSize = True
expandAllButton.Font = Font("Segoe UI", scale(9))
collapseAllButton = ToolStripButton("⊟ Collapse All")
collapseAllButton.ForeColor = Colors.TEXT_PRIMARY
collapseAllButton.ToolTipText = "Collapse all tree nodes"
collapseAllButton.Margin = Padding(scale(10), scale(5), scale(10), scale(5))
collapseAllButton.Padding = Padding(scale(12), scale(6), scale(12), scale(6))
collapseAllButton.AutoSize = True
collapseAllButton.Font = Font("Segoe UI", scale(9))
realTimeButton = ToolStripButton("🔗 Real-time Sync")
realTimeButton.CheckOnClick = True
realTimeButton.Checked = True
realTimeButton.ForeColor = Colors.SUCCESS
realTimeButton.ToolTipText = "Automatically sync with Alibre selection"
realTimeButton.Margin = Padding(scale(10), scale(5), scale(10), scale(5))
realTimeButton.Padding = Padding(scale(12), scale(6), scale(12), scale(6))
realTimeButton.AutoSize = True
realTimeButton.Font = Font("Segoe UI", scale(9))
# Separator styling with high DPI scaling
sep1 = ToolStripSeparator()
sep1.Margin = Padding(scale(16), 0, scale(16), 0)
sep2 = ToolStripSeparator()
sep2.Margin = Padding(scale(16), 0, scale(16), 0)
toolStrip.Items.AddRange(Array[ToolStripItem]((
refreshButton, sep1, expandAllButton, collapseAllButton, sep2, realTimeButton
)))
# Professional TreeView with high DPI scaling
treeView = TreeView()
treeView.Dock = DockStyle.Fill
treeView.Font = Font("Segoe UI", scale(9))
treeView.BackColor = Colors.PRIMARY
treeView.ForeColor = Colors.TEXT_PRIMARY
treeView.LineColor = Colors.BORDER
treeView.ShowLines, treeView.ShowPlusMinus, treeView.ShowRootLines = True, True, True
treeView.HideSelection, treeView.FullRowSelect = False, True
treeView.BorderStyle = BorderStyle.None
treeView.Indent = scale(32)
treeView.ItemHeight = scale(32) # Extra height for high DPI
treeView.Margin = Padding(100, scale(8), 0, 0)
# Professional PropertyGrid with high DPI scaling
propertyGrid = PropertyGrid()
propertyGrid.Dock = DockStyle.Fill
propertyGrid.BackColor = Colors.PRIMARY
propertyGrid.LineColor = Colors.BORDER
propertyGrid.CategoryForeColor = Colors.TEXT_PRIMARY
propertyGrid.CategorySplitterColor = Colors.BORDER
propertyGrid.HelpBackColor = Colors.SURFACE
propertyGrid.HelpForeColor = Colors.TEXT_SECONDARY
propertyGrid.ViewBackColor = Colors.PRIMARY
propertyGrid.ViewForeColor = Colors.TEXT_PRIMARY
propertyGrid.ToolbarVisible = True
propertyGrid.Font = Font("Segoe UI", scale(9))
propertyGrid.PropertySort = PropertySort.Categorized
propertyGrid.HelpVisible = True
propertyGrid.Margin = Padding(0, scale(8), 0, 0)
# --- CORRECTED CONTROL ADDITION ORDER ---
# To fix the layout, add the control with Dock.Fill first. Then, add the
# controls with Dock.Top. The last 'Top' control added will be at the very top.
# Add controls to the left panel
leftPanel.Controls.Add(treeView) # Add FILL control first
leftPanel.Controls.Add(toolStrip) # This will sit below the header
leftPanel.Controls.Add(treeHeader) # This will be at the very top
# Add controls to the right panel
rightPanel.Controls.Add(propertyGrid) # Add FILL control first
rightPanel.Controls.Add(propHeader) # This will be at the very top
# Custom Property Wrapper Classes (unchanged functionality)
class PropertyWrapper(object):
"""Base wrapper to expose all API properties"""
def __init__(self, obj, obj_type):
self._wrapped_object = obj
self._object_type = obj_type
self._properties = {}
self._load_properties()
def _load_properties(self):
"""Load all available properties based on object type"""
# This dictionary dispatches to the correct loading function
loader_map = {
"Part": self._load_part_properties,
"Assembly": self._load_assembly_properties,
"AssembledPart": self._load_assembled_part_properties,
"AssembledSubAssembly": self._load_assembled_sub_assembly_properties,
"Face": self._load_face_properties,
"Edge": self._load_edge_properties,
"Vertex": self._load_vertex_properties,
"Sketch": self._load_sketch_properties,
"Sketch3D": self._load_sketch_3d_properties,
"Parameter": self._load_parameter_properties,
"Configuration": self._load_configuration_properties,
"Feature": self._load_feature_properties,
"Plane": self._load_plane_properties,
"Axis": self._load_axis_properties,
"Point": self._load_point_properties,
"Line": self._load_line_properties,
"Line3D": self._load_line3d_properties,
"Circle": self._load_circle_properties,
"CircularArc": self._load_circular_arc_properties,
"CircularArc3D": self._load_circular_arc3d_properties,
"Ellipse": self._load_ellipse_properties,
"EllipticalArc": self._load_elliptical_arc_properties,
"Bspline": self._load_bspline_properties,
"Bspline3D": self._load_bspline3d_properties,
"SketchPoint": self._load_sketchpoint_properties,
"SketchPoint3D": self._load_sketchpoint3d_properties,
}
# Get the loader function from the map or use the generic one
loader_func = loader_map.get(self._object_type, self._load_generic_properties)
loader_func()
def _safe_get_property(self, prop_name, default_value="N/A"):
"""Safely get a property value, handling potential errors."""
try:
if hasattr(self._wrapped_object, prop_name):
value = getattr(self._wrapped_object, prop_name)
if value is None:
return "None"
# Format lists/tuples for better readability
if isinstance(value, (list, tuple, Array)):
return ", ".join(map(str, value))
return str(value)
return default_value
except Exception as e:
return "Error: " + str(e)
def _format_bbox(self, bbox_method_name):
"""Helper to get and format a bounding box."""
try:
bbox = getattr(self._wrapped_object, bbox_method_name)()
if bbox and len(bbox) == 8:
return "; ".join(["({:.3f}, {:.3f}, {:.3f})".format(p[0], p[1], p[2]) for p in bbox])
return "Invalid BBox Data"
except:
return "N/A"
def _load_base_file_properties(self):
"""Loads common properties for Part and Assembly objects."""
self._properties.update({
"Name": self._safe_get_property("Name"),
"FileName": self._safe_get_property("FileName"),
"Material": self._safe_get_property("Material"),
"Density": self._safe_get_property("Density"),
"Mass": self._safe_get_property("Mass"),
"Description": self._safe_get_property("Description"),
"Number": self._safe_get_property("Number"),
"Comment": self._safe_get_property("Comment"),
"CostCenter": self._safe_get_property("CostCenter"),
"CreatedBy": self._safe_get_property("CreatedBy"),
"CreatedDate": self._safe_get_property("CreatedDate"),
"CreatingApplication": self._safe_get_property("CreatingApplication"),
"DocumentNumber": self._safe_get_property("DocumentNumber"),
"EngineeringApprovalDate": self._safe_get_property("EngineeringApprovalDate"),
"EngineeringApprovedBy": self._safe_get_property("EngineeringApprovedBy"),
"EstimatedCost": self._safe_get_property("EstimatedCost"),
"Keywords": self._safe_get_property("Keywords"),
"LastAuthor": self._safe_get_property("LastAuthor"),
"LastUpdateDate": self._safe_get_property("LastUpdateDate"),
"ExtendedMaterialInformation": self._safe_get_property("ExtendedMaterialInformation"),
"ManufacturingApprovedBy": self._safe_get_property("ManufacturingApprovedBy"),
"ManufacturingApprovedDate": self._safe_get_property("ManufacturingApprovedDate"),
"ModifiedInformation": self._safe_get_property("ModifiedInformation"),
"Product": self._safe_get_property("Product"),
"ReceivedFrom": self._safe_get_property("ReceivedFrom"),
"Revision": self._safe_get_property("Revision"),
"StockSize": self._safe_get_property("StockSize"),
"Supplier": self._safe_get_property("Supplier"),
"Title": self._safe_get_property("Title"),
"Vendor": self._safe_get_property("Vendor"),
"WebLink": self._safe_get_property("WebLink"),
})
try:
self._properties["DisplayUnits"] = str(self._wrapped_object.DisplayUnits())
except:
self._properties["DisplayUnits"] = "N/A"
def _load_part_properties(self):
self._load_base_file_properties()
self._properties["BoundingBox"] = self._format_bbox("GetBoundingBox")
def _load_assembly_properties(self):
self._load_base_file_properties()
# Assembly does not have GetBoundingBox, it is calculated from components.
def _load_assembled_part_properties(self):
self._properties["Name"] = self._safe_get_property("Name")
self._properties["BoundingBox (Assembly Coords)"] = self._format_bbox("GetAssemblyBoundingBox")
try:
self._properties["ConfigurationCount"] = str(len(self._wrapped_object.Configurations))
except:
self._properties["ConfigurationCount"] = "0"
def _load_assembled_sub_assembly_properties(self):
self._properties["Name"] = self._safe_get_property("Name")
try:
self._properties["ConfigurationCount"] = str(len(self._wrapped_object.Configurations))
except:
self._properties["ConfigurationCount"] = "0"
def _load_face_properties(self):
self._properties["Name"] = self._safe_get_property("Name")
try: self._properties["Area"] = str(self._wrapped_object.GetArea())
except: self._properties["Area"] = "N/A"
try: self._properties["IsRectangle"] = str(self._wrapped_object.IsRectangle())
except: self._properties["IsRectangle"] = "N/A"
try: self._properties["EdgeCount"] = str(len(self._wrapped_object.GetEdges()))
except: self._properties["EdgeCount"] = "N/A"
try: self._properties["VertexCount"] = str(len(self._wrapped_object.GetVertices()))
except: self._properties["VertexCount"] = "N/A"
try: self._properties["AdjoiningFaceCount"] = str(len(self._wrapped_object.GetAdjoiningFaces()))
except: self._properties["AdjoiningFaceCount"] = "N/A"
def _load_edge_properties(self):
self._properties["Name"] = self._safe_get_property("Name")
self._properties["Length"] = self._safe_get_property("Length")
self._properties["Diameter"] = self._safe_get_property("Diameter")
try: self._properties["VertexCount"] = str(len(self._wrapped_object.GetVertices()))
except: self._properties["VertexCount"] = "N/A"
def _load_vertex_properties(self):
self._properties.update({
"Name": self._safe_get_property("Name"),
"X": self._safe_get_property("X"),
"Y": self._safe_get_property("Y"),
"Z": self._safe_get_property("Z")
})
def _load_sketch_properties(self):
self._properties["Name"] = self._safe_get_property("Name")
try: self._properties["FigureCount"] = str(len(self._wrapped_object.Figures))
except: self._properties["FigureCount"] = "0"
try: self._properties["Surface"] = self._wrapped_object.GetSurface().Name
except: self._properties["Surface"] = "N/A"
def _load_sketch_3d_properties(self):
self._properties["Name"] = self._safe_get_property("Name")
try: self._properties["FigureCount"] = str(len(self._wrapped_object.Figures))
except: self._properties["FigureCount"] = "0"
def _load_parameter_properties(self):
self._properties.update({
"Name": self._safe_get_property("Name"),
"Value": self._safe_get_property("Value"),
"Equation": self._safe_get_property("Equation"),
"Comment": self._safe_get_property("Comment"),
"Type": self._safe_get_property("Type"),
"Units": self._safe_get_property("Units"),
"RawValue": self._safe_get_property("RawValue"),
"ExcelWorkbook": self._safe_get_property("ExcelWorkbook"),
"ExcelSheet": self._safe_get_property("ExcelSheet"),
"ExcelCell": self._safe_get_property("ExcelCell"),
})
def _load_configuration_properties(self):
self._properties.update({
"Name": self._safe_get_property("Name"),
"IsActive": self._safe_get_property("IsActive")
})
def _load_feature_properties(self):
self._properties["Name"] = self._safe_get_property("Name")
# Color is a method SetColor, not a property to get.
def _load_plane_properties(self):
self._properties["Name"] = self._safe_get_property("Name")
def _load_axis_properties(self):
self._properties["Name"] = self._safe_get_property("Name")
def _load_point_properties(self):
self._properties["Name"] = self._safe_get_property("Name")
try:
coords = self._wrapped_object.GetCoordinates()
self._properties["X"] = str(coords[0])
self._properties["Y"] = str(coords[1])
self._properties["Z"] = str(coords[2])
except:
self._properties["X"] = self._safe_get_property("X")
self._properties["Y"] = self._safe_get_property("Y")
self._properties["Z"] = self._safe_get_property("Z")
# Sketch Figure Properties
def _load_line_properties(self):
self._properties["Type"] = "Line"
self._properties["StartPoint"] = self._safe_get_property("StartPoint")
self._properties["EndPoint"] = self._safe_get_property("EndPoint")
self._properties["Length"] = self._safe_get_property("Length")
self._properties["IsReference"] = self._safe_get_property("IsReference")
def _load_line3d_properties(self):
self._properties["Type"] = "3D Line"
self._properties["StartPoint"] = self._safe_get_property("StartPoint")
self._properties["EndPoint"] = self._safe_get_property("EndPoint")
self._properties["Length"] = self._safe_get_property("Length")
self._properties["IsReference"] = self._safe_get_property("IsReference")
def _load_circle_properties(self):
self._properties["Type"] = "Circle"
self._properties["Center"] = self._safe_get_property("Center")
self._properties["Radius"] = self._safe_get_property("Radius")
self._properties["Length"] = self._safe_get_property("Length")
self._properties["IsReference"] = self._safe_get_property("IsReference")
def _load_circular_arc_properties(self):
self._properties["Type"] = "Circular Arc"
self._properties["Center"] = self._safe_get_property("Center")
self._properties["StartPoint"] = self._safe_get_property("StartPoint")
self._properties["EndPoint"] = self._safe_get_property("EndPoint")
self._properties["Radius"] = self._safe_get_property("Radius")
self._properties["Angle"] = self._safe_get_property("Angle")
self._properties["IsReference"] = self._safe_get_property("IsReference")
def _load_circular_arc3d_properties(self):
self._properties["Type"] = "3D Circular Arc"
self._properties["Center"] = self._safe_get_property("Center")
self._properties["StartPoint"] = self._safe_get_property("StartPoint")
self._properties["EndPoint"] = self._safe_get_property("EndPoint")
self._properties["Radius"] = self._safe_get_property("Radius")
self._properties["Angle"] = self._safe_get_property("Angle")
self._properties["IsReference"] = self._safe_get_property("IsReference")
def _load_ellipse_properties(self):
self._properties["Type"] = "Ellipse"
self._properties["Center"] = self._safe_get_property("Center")
self._properties["Radius"] = self._safe_get_property("Radius")
self._properties["MajorAxisAngle"] = self._safe_get_property("MajorAxisAngle")
self._properties["MinorMajorRatio"] = self._safe_get_property("MinorMajorRatio")
self._properties["IsReference"] = self._safe_get_property("IsReference")
def _load_elliptical_arc_properties(self):
self._properties["Type"] = "Elliptical Arc"
self._properties["Center"] = self._safe_get_property("Center")
self._properties["StartPoint"] = self._safe_get_property("StartPoint")
self._properties["EndPoint"] = self._safe_get_property("EndPoint")
self._properties["Radius"] = self._safe_get_property("Radius")
self._properties["MajorAxisAngle"] = self._safe_get_property("MajorAxisAngle")
self._properties["MinorMajorRatio"] = self._safe_get_property("MinorMajorRatio")
self._properties["IsReference"] = self._safe_get_property("IsReference")
def _load_bspline_properties(self):
self._properties["Type"] = "B-spline"
self._properties["Order"] = self._safe_get_property("Order")
self._properties["Length"] = self._safe_get_property("Length")
self._properties["ControlPoints"] = self._safe_get_property("ControlPoints")
self._properties["KnotVectors"] = self._safe_get_property("KnotVectors")
self._properties["Weights"] = self._safe_get_property("Weights")
self._properties["IsReference"] = self._safe_get_property("IsReference")
def _load_bspline3d_properties(self):
self._properties["Type"] = "3D B-spline"
self._properties["Order"] = self._safe_get_property("Order")
self._properties["Length"] = self._safe_get_property("Length")
self._properties["ControlPoints"] = self._safe_get_property("ControlPoints")
self._properties["KnotVectors"] = self._safe_get_property("KnotVectors")
self._properties["Weights"] = self._safe_get_property("Weights")
self._properties["IsReference"] = self._safe_get_property("IsReference")
def _load_sketchpoint_properties(self):
self._properties["Type"] = "Sketch Point"
self._properties["X"] = self._safe_get_property("X")
self._properties["Y"] = self._safe_get_property("Y")
self._properties["IsReference"] = self._safe_get_property("IsReference")
def _load_sketchpoint3d_properties(self):
self._properties["Type"] = "3D Sketch Point"
self._properties["X"] = self._safe_get_property("X")
self._properties["Y"] = self._safe_get_property("Y")
self._properties["Z"] = self._safe_get_property("Z")
self._properties["IsReference"] = self._safe_get_property("IsReference")
def _load_generic_properties(self):
"""Load generic properties for unknown types"""
self._properties["Name"] = self._safe_get_property("Name", "N/A")
self._properties["ObjectType"] = str(type(self._wrapped_object).__name__)
def GetAllProperties(self):
return self._properties
# Enhanced property display using a custom object
class PropertyDisplayObject(object):
def __init__(self, wrapped_obj):
self._original_object = wrapped_obj
obj_type_name = type(wrapped_obj).__name__
# Use a dictionary for more robust type mapping
type_map = {
"ADPart": "Part",
"ADAssembly": "Assembly",
"ADFace": "Face",
"ADEdge": "Edge",
"ADVertex": "Vertex",
"ADSketch": "Sketch",
"ADSketch3D": "Sketch3D",
"ADParameter": "Parameter",
"ADConfiguration": "Configuration",
"ADFeature": "Feature",
"ADPlane": "Plane",
"ADAxis": "Axis",
"ADPoint": "Point",
"ADAssembledPart": "AssembledPart",
"ADAssembledSubAssembly": "AssembledSubAssembly",
"ADLine": "Line",
"ADLine3D": "Line3D",
"ADCircle": "Circle",
"ADCircularArc": "CircularArc",
"ADCircularArc3D": "CircularArc3D",
"ADEllipse": "Ellipse",
"ADEllipticalArc": "EllipticalArc",
"ADBspline": "Bspline",
"ADBspline3D": "Bspline3D",
"ADSketchPoint": "SketchPoint",
"ADSketchPoint3D": "SketchPoint3D"
}
obj_type = type_map.get(obj_type_name, obj_type_name)
self._wrapper = PropertyWrapper(wrapped_obj, obj_type)
# Dynamically add properties for the PropertyGrid
for prop_name, prop_value in self._wrapper.GetAllProperties().items():
clean_prop_name = prop_name.replace(" ", "_").replace("-", "_").replace("(", "").replace(")", "")
setattr(self, clean_prop_name, prop_value)
# --- REAL-TIME SELECTION TRACKING ---
last_alibre_selection = None
manual_selection_active = False
selectionTimer = Timer()
selectionTimer.Interval = 500 # Check for new selections every 500ms
def UpdateStatusBar(message, selection_text=""):
"""Update the status bar with current information"""
statusLabel.Text = message
selectionLabel.Text = selection_text
def OnTimerTick(sender, args):
"""Checks the active selection in Alibre and updates the PropertyGrid"""
global last_alibre_selection, manual_selection_active
if not realTimeButton.Checked or manual_selection_active:
return
try:
selections = MyModel.Selections
if selections and len(selections) > 0:
current_object = selections[0]
if current_object != last_alibre_selection:
display_object = PropertyDisplayObject(current_object)
propertyGrid.SelectedObject = display_object
last_alibre_selection = current_object
# Update status bar
obj_name = "Unknown"
try:
if hasattr(current_object, 'Name') and current_object.Name:
obj_name = current_object.Name
else:
obj_name = type(current_object).__name__
except:
obj_name = type(current_object).__name__
UpdateStatusBar("Ready - Model: " + MyModel.Name, "Selected: " + obj_name)
elif last_alibre_selection is not None:
if not manual_selection_active:
propertyGrid.SelectedObject = None
UpdateStatusBar("Ready - Model: " + MyModel.Name, "No selection")
last_alibre_selection = None
except:
selectionTimer.Stop()
selectionTimer.Tick += OnTimerTick
selectionTimer.Start()
form.FormClosing += lambda s, e: selectionTimer.Stop()
def AddObjectNode(parentNode, item, name=None):
"""Helper to add an object node to the tree - only if object is valid"""
if item is None:
return None
try:
if name:
display_name = name
elif hasattr(item, 'Name') and item.Name:
display_name = item.Name
else:
display_name = str(type(item).__name__)
except:
display_name = str(type(item).__name__)
node = TreeNode(display_name)
node.Tag = item
# Add visual indicators with Unicode symbols
obj_type = type(item).__name__
if "Assembly" in obj_type:
display_name = "🗂️ " + display_name
elif "Part" in obj_type:
display_name = "🔧 " + display_name
elif "Face" in obj_type:
display_name = "📄 " + display_name
elif "Edge" in obj_type:
display_name = "📏 " + display_name
elif "Vertex" in obj_type:
display_name = "📍 " + display_name
elif "Sketch" in obj_type:
display_name = "✏️ " + display_name
elif "Feature" in obj_type:
display_name = "⚙️ " + display_name
elif "Parameter" in obj_type:
display_name = "🔢 " + display_name
elif "Configuration" in obj_type:
display_name = "⚙️ " + display_name
elif "Plane" in obj_type:
display_name = "🔲 " + display_name
elif "Axis" in obj_type:
display_name = "↔️ " + display_name
elif "Point" in obj_type:
display_name = "🎯 " + display_name
node.Text = display_name
if parentNode:
parentNode.Nodes.Add(node)
else:
treeView.Nodes.Add(node)
return node
def AddCollectionNode(parentNode, collectionName, collection_provider):
"""Helper to add a collection node with all its items - only if collection has real items"""
collection = None
try:
collection = collection_provider() if callable(collection_provider) else collection_provider
if collection is None or len(collection) == 0:
return None
except:
return None
collectionNode = TreeNode("📁 %s (%d)" % (collectionName, len(collection)))
collectionNode.Tag = collection
parentNode.Nodes.Add(collectionNode)
for i, item in enumerate(collection):
if item is None:
continue
try:
if hasattr(item, 'Name') and item.Name:
item_name = item.Name
else:
item_name = "%s[%d]" % (type(item).__name__, i)
itemNode = AddObjectNode(collectionNode, item, item_name)
# Recursively add nodes for complex objects like sketches or assembled parts
if itemNode:
obj_type_name = type(item).__name__
if "Sketch" in obj_type_name and hasattr(item, 'Figures'):
AddCollectionNode(itemNode, "Figures", item.Figures)
elif obj_type_name == "ADAssembledPart":
PopulateAssembledPartNode(itemNode, item)
elif obj_type_name == "ADAssembledSubAssembly":
PopulateSubAssemblyNode(itemNode, item)
except Exception as e:
# Skip invalid items silently
pass
return collectionNode
def PopulateAssembledPartNode(partNode, part):
"""Adds geometry collections for an AssembledPart"""
AddCollectionNode(partNode, "Faces", part.GetFaces)
AddCollectionNode(partNode, "Edges", part.GetEdges)
# GetAssemblyVertices returns a list of lists, not vertex objects, so we handle it differently
try:
vertices = part.GetAssemblyVertices()
if vertices and len(vertices) > 0:
v_node = TreeNode("📁  Vertices (%d)" % len(vertices))
partNode.Nodes.Add(v_node)
except:
pass
def PopulateSubAssemblyNode(assemblyNode, subAssembly):
"""Recursively populates a sub-assembly node"""
AddCollectionNode(assemblyNode, "Parts", subAssembly.Parts)
AddCollectionNode(assemblyNode, "SubAssemblies", subAssembly.SubAssemblies)
def BuildTreeNodes():
"""Builds the tree structure using only real model data"""
treeView.BeginUpdate()
treeView.Nodes.Clear()
UpdateStatusBar("Building model tree...", "")
# Root node
rootNode = AddObjectNode(None, MyModel, MyModel.Name)
# Add common collections that are directly accessible
AddCollectionNode(rootNode, "Configurations", MyModel.Configurations)
AddCollectionNode(rootNode, "Parameters", MyModel.Parameters)
if IsAssembly:
AddCollectionNode(rootNode, "Parts", MyModel.Parts)
AddCollectionNode(rootNode, "SubAssemblies", MyModel.SubAssemblies)
else: # It's a Part
AddCollectionNode(rootNode, "Faces", MyModel.GetFaces)
AddCollectionNode(rootNode, "Edges", MyModel.GetEdges)
AddCollectionNode(rootNode, "Vertices", MyModel.GetVertices)
# --- BRUTE-FORCE DISCOVERY ---
# The Alibre API does not provide direct collections for items like Features, Sketches,
# Planes, Axes, or Points. The only way to find them is to try getting them by name.
# The following sections attempt to find these items by iterating through common naming patterns.
def find_named_items(collection_name, get_method, name_patterns, try_3d_sketch=False):
found_items = []
found_names = set()
for name in name_patterns:
try:
item = get_method(name)
if item and name not in found_names:
found_items.append((name, item))
found_names.add(name)
except: pass
if try_3d_sketch:
try:
item3d = MyModel.Get3DSketch(name)
if item3d and (name + "_3D") not in found_names:
found_items.append((name + " (3D)", item3d))
found_names.add(name + "_3D")
except: pass
if found_items:
node = TreeNode("📁  %s (%d)" % (collection_name, len(found_items)))
rootNode.Nodes.Add(node)
for item_name, item_obj in found_items:
itemNode = AddObjectNode(node, item_obj, item_name)
# For sketches, also add their figures
if itemNode and "Sketch" in collection_name and hasattr(item_obj, 'Figures'):
AddCollectionNode(itemNode, "Figures", item_obj.Figures)
# Define name patterns
base_names = {
"Feature": ["Extrude", "Extrusion", "Cut", "Fillet", "Chamfer", "Hole", "Pattern", "Revolve", "Loft", "Sweep", "Shell", "Draft", "Mirror", "Feature", "Boss", "stack"],
"Sketch": ["Sketch", "Profile", "Path", "Section", "2D Sketch", "3D Sketch"],
"Plane": ["Plane"],
"Axis": ["Axis"],
"Point": ["Point", "Origin"]
}
names_to_try = {}
for key, prefixes in base_names.items():
current_list = []
for prefix in prefixes:
# Also try the prefix by itself, e.g., for a feature named just "stack"
current_list.append(prefix)
for i in range(1, 51): # Check up to 50 of each type
current_list.extend([
"%s<%d>" % (prefix, i), # e.g., Sketch<1>
"%s%d" % (prefix, i), # e.g., Sketch1
"%s <%d>" % (prefix, i), # e.g., Sketch <1>
"%s %d" % (prefix, i) # e.g., Sketch 1
])
names_to_try[key] = current_list
# Add standard planes/axes/origin if they exist
try:
if MyModel.XYPlane: AddObjectNode(rootNode, MyModel.XYPlane, "XY-Plane")
if MyModel.YZPlane: AddObjectNode(rootNode, MyModel.YZPlane, "YZ-Plane")
if MyModel.ZXPlane: AddObjectNode(rootNode, MyModel.ZXPlane, "ZX-Plane")
if MyModel.XAxis: AddObjectNode(rootNode, MyModel.XAxis, "X-Axis")
if MyModel.YAxis: AddObjectNode(rootNode, MyModel.YAxis, "Y-Axis")
if MyModel.ZAxis: AddObjectNode(rootNode, MyModel.ZAxis, "Z-Axis")
if MyModel.Origin: AddObjectNode(rootNode, MyModel.Origin, "Origin")
except: pass
# Find and add items
find_named_items("Features", MyModel.GetFeature, names_to_try["Feature"])
find_named_items("Sketches", MyModel.GetSketch, names_to_try["Sketch"], try_3d_sketch=True)
find_named_items("Planes", MyModel.GetPlane, names_to_try["Plane"])
find_named_items("Axes", MyModel.GetAxis, names_to_try["Axis"])
find_named_items("Points", MyModel.GetPoint, names_to_try["Point"])
treeView.EndUpdate()
rootNode.Expand()
UpdateStatusBar("Ready - Model: " + MyModel.Name, "Tree built successfully")
def OnNodeSelect(sender, args):
"""When a node is selected in the tree, show its properties with full API exposure"""
global manual_selection_active
manual_selection_active = True
if treeView.SelectedNode and treeView.SelectedNode.Tag is not None:
selected_object = treeView.SelectedNode.Tag
# Check if tag is a collection, if so, select the parent model
if isinstance(selected_object, (list, tuple, Array)):
if treeView.SelectedNode.Parent and treeView.SelectedNode.Parent.Tag:
selected_object = treeView.SelectedNode.Parent.Tag
else:
selected_object = MyModel
propertyGrid.SelectedObject = PropertyDisplayObject(selected_object)
# Update status bar
try:
obj_name = treeView.SelectedNode.Text
if obj_name.startswith("📁  "):
obj_name = obj_name[3:] # Remove folder emoji and extra space
elif len(obj_name) > 3 and obj_name[2:3] == " ":
obj_name = obj_name[3:] # Remove other emojis and extra space
UpdateStatusBar("Ready - Model: " + MyModel.Name, "Selected: " + obj_name)
except:
UpdateStatusBar("Ready - Model: " + MyModel.Name, "Tree selection")
else:
propertyGrid.SelectedObject = None
UpdateStatusBar("Ready - Model: " + MyModel.Name, "No selection")
# Reset manual selection flag after a short delay
def ResetManualFlag(s, e):
global manual_selection_active
manual_selection_active = False
s.Stop()
resetTimer = Timer()
resetTimer.Interval = 1000
resetTimer.Tick += ResetManualFlag
resetTimer.Start()
# Enhanced Event Handlers
def OnRefreshClick(sender, args):
BuildTreeNodes()
UpdateStatusBar("Ready - Model: " + MyModel.Name, "Model refreshed")
def OnExpandAllClick(sender, args):
treeView.ExpandAll()
UpdateStatusBar("Ready - Model: " + MyModel.Name, "All nodes expanded")
def OnCollapseAllClick(sender, args):
treeView.CollapseAll()
UpdateStatusBar("Ready - Model: " + MyModel.Name, "All nodes collapsed")
def OnRealTimeToggle(sender, args):
if realTimeButton.Checked:
realTimeButton.ForeColor = Colors.SUCCESS
selectionTimer.Start()
UpdateStatusBar("Ready - Model: " + MyModel.Name, "Real-time sync enabled")
else:
realTimeButton.ForeColor = Colors.TEXT_PRIMARY
selectionTimer.Stop()
UpdateStatusBar("Ready - Model: " + MyModel.Name, "Real-time sync disabled")
# Assign Event Handlers
treeView.AfterSelect += OnNodeSelect
refreshButton.Click += OnRefreshClick
expandAllButton.Click += OnExpandAllClick
collapseAllButton.Click += OnCollapseAllClick
realTimeButton.Click += OnRealTimeToggle
# Build the initial tree and show the form
BuildTreeNodes()
UpdateStatusBar("Ready - Model: " + MyModel.Name, "Explorer initialized")
form.ShowDialog()
# Run the script
CreatePropertyExplorer()
# created with Alibre Script Genie by Stephen S. Mitchell, https://github.com/stephensmitchell
import sys
import clr
import math
# Import Windows Forms
clr.AddReference("System.Windows.Forms")
clr.AddReference("System.Drawing")
clr.AddReference("System.ComponentModel")
from System.Windows.Forms import *
from System.Drawing import *
from System.ComponentModel import *
from System import *
def CreatePropertyExplorer():
# Get the current model (try part first, then assembly)
MyModel = None
IsAssembly = False
try:
#Units.Current = UnitTypes.Centimeters
#Units.Current = UnitTypes.Millimeters
Units.Current = UnitTypes.Inches
MyModel = CurrentPart()
except:
try:
MyModel = CurrentAssembly()
IsAssembly = True
except:
Win = Windows()
Win.ErrorDialog("No active part or assembly found.\n\nPlease open or create a part/assembly first.", "Alibre Property Explorer")
return
# High DPI support - detect current DPI scaling
import System.Windows.Forms.Application as WinApp
try:
# Get current DPI scaling factor
dpi_scale = System.Drawing.Graphics.FromHwnd(System.IntPtr.Zero).DpiX / 96.0
except:
dpi_scale = 2.0 # Default to 200% for 5K displays
# Scale factors for high DPI
def scale(value):
return int(value * dpi_scale)
# Professional color scheme
class Colors:
PRIMARY = Color.FromArgb(45, 45, 48) # Dark gray
SECONDARY = Color.FromArgb(62, 62, 66) # Medium gray
ACCENT = Color.FromArgb(0, 122, 204) # Blue accent
SURFACE = Color.FromArgb(37, 37, 38) # Darker surface
TEXT_PRIMARY = Color.FromArgb(241, 241, 241) # Light text
TEXT_SECONDARY = Color.FromArgb(204, 204, 204) # Medium text
BORDER = Color.FromArgb(63, 63, 70) # Border color
SUCCESS = Color.FromArgb(73, 156, 84) # Green
WARNING = Color.FromArgb(252, 196, 25) # Orange
# Create the main form with high DPI awareness
form = Form()
form.Text = "Alibre Property Explorer - " + MyModel.Name
form.Width = scale(1600)
form.Height = scale(1000)
form.StartPosition = FormStartPosition.CenterScreen
form.BackColor = Colors.PRIMARY
form.ForeColor = Colors.TEXT_PRIMARY
form.Font = Font("Segoe UI", scale(9), FontStyle.Regular)
form.MinimumSize = Size(scale(1300), scale(800))
# Enable high DPI support
try:
form.AutoScaleMode = AutoScaleMode.Dpi
form.AutoScaleDimensions = SizeF(scale(96), scale(96))
except:
pass
# Add professional icon if possible
try:
form.ShowIcon = True
except:
pass
# Create status bar with DPI scaling
statusStrip = StatusStrip()
statusStrip.BackColor = Colors.SURFACE
statusStrip.ForeColor = Colors.TEXT_SECONDARY
statusStrip.Font = Font("Segoe UI", scale(9))
statusStrip.Padding = Padding(scale(12), scale(6), scale(12), scale(6))
statusLabel = ToolStripStatusLabel()
statusLabel.Text = "Ready - Model: " + MyModel.Name
statusLabel.Spring = True
statusLabel.TextAlign = ContentAlignment.MiddleLeft
statusLabel.Margin = Padding(scale(8), scale(3), scale(8), scale(3))
selectionLabel = ToolStripStatusLabel()
selectionLabel.Text = "No selection"
selectionLabel.AutoSize = True
selectionLabel.Margin = Padding(scale(8), scale(3), scale(8), scale(3))
statusStrip.Items.AddRange(Array[ToolStripItem]((statusLabel, selectionLabel)))
form.Controls.Add(statusStrip)
# Main layout container with DPI scaling
mainSplitter = SplitContainer()
mainSplitter.Dock = DockStyle.Fill
mainSplitter.Orientation = Orientation.Vertical
mainSplitter.SplitterDistance = scale(800)
mainSplitter.BackColor = Colors.BORDER
mainSplitter.SplitterWidth = scale(8)
form.Controls.Add(mainSplitter)
# LEFT PANEL for TreeView with DPI scaling
leftPanel = Panel()
leftPanel.Dock = DockStyle.Fill
leftPanel.BackColor = Colors.SECONDARY
leftPanel.Padding = Padding(scale(16), scale(16), scale(12), scale(16))
mainSplitter.Panel1.Controls.Add(leftPanel)
# RIGHT PANEL for PropertyGrid with DPI scaling
rightPanel = Panel()
rightPanel.Dock = DockStyle.Fill
rightPanel.BackColor = Colors.SECONDARY
rightPanel.Padding = Padding(scale(12), scale(16), scale(16), scale(16))
mainSplitter.Panel2.Controls.Add(rightPanel)
# Add header labels with high DPI scaling FIRST
treeHeader = Label()
treeHeader.Text = "Model Structure"
treeHeader.Font = Font("Segoe UI", scale(10), FontStyle.Bold)
treeHeader.ForeColor = Colors.TEXT_PRIMARY
treeHeader.BackColor = Colors.SURFACE
treeHeader.Height = scale(50) # Extra height for high DPI
treeHeader.TextAlign = ContentAlignment.MiddleLeft
treeHeader.Padding = Padding(20, 0, 0, 0)
treeHeader.Dock = DockStyle.Top
propHeader = Label()
propHeader.Text = "Properties"
propHeader.Font = Font("Segoe UI", scale(10), FontStyle.Bold)
propHeader.ForeColor = Colors.TEXT_PRIMARY
propHeader.BackColor = Colors.SURFACE
propHeader.Height = scale(50) # Extra height for high DPI
propHeader.TextAlign = ContentAlignment.MiddleLeft
propHeader.Padding = Padding(scale(20), 0, 0, 0)
propHeader.Dock = DockStyle.Top
# Professional Toolbar with high DPI scaling
toolStrip = ToolStrip()
toolStrip.BackColor = Colors.SURFACE
toolStrip.ForeColor = Colors.TEXT_PRIMARY
toolStrip.Font = Font("Segoe UI", scale(9))
toolStrip.Renderer = ToolStripProfessionalRenderer()
toolStrip.GripStyle = ToolStripGripStyle.Hidden
toolStrip.Padding = Padding(scale(20), scale(8), scale(20), scale(8))
toolStrip.AutoSize = False
toolStrip.Height = scale(60) # Extra height for high DPI
toolStrip.LayoutStyle = ToolStripLayoutStyle.HorizontalStackWithOverflow
toolStrip.Dock = DockStyle.Top
# Create toolbar buttons with high DPI scaling
refreshButton = ToolStripButton("🔄 Refresh")
refreshButton.ForeColor = Colors.TEXT_PRIMARY
refreshButton.ToolTipText = "Refresh the model tree"
refreshButton.Margin = Padding(scale(10), scale(5), scale(10), scale(5))
refreshButton.Padding = Padding(scale(12), scale(6), scale(12), scale(6))
refreshButton.AutoSize = True
refreshButton.Font = Font("Segoe UI", scale(9))
expandAllButton = ToolStripButton("⊞ Expand All")
expandAllButton.ForeColor = Colors.TEXT_PRIMARY
expandAllButton.ToolTipText = "Expand all tree nodes"
expandAllButton.Margin = Padding(scale(10), scale(5), scale(10), scale(5))
expandAllButton.Padding = Padding(scale(12), scale(6), scale(12), scale(6))
expandAllButton.AutoSize = True
expandAllButton.Font = Font("Segoe UI", scale(9))
collapseAllButton = ToolStripButton("⊟ Collapse All")
collapseAllButton.ForeColor = Colors.TEXT_PRIMARY
collapseAllButton.ToolTipText = "Collapse all tree nodes"
collapseAllButton.Margin = Padding(scale(10), scale(5), scale(10), scale(5))
collapseAllButton.Padding = Padding(scale(12), scale(6), scale(12), scale(6))
collapseAllButton.AutoSize = True
collapseAllButton.Font = Font("Segoe UI", scale(9))
realTimeButton = ToolStripButton("🔗 Real-time Sync")
realTimeButton.CheckOnClick = True
realTimeButton.Checked = True
realTimeButton.ForeColor = Colors.SUCCESS
realTimeButton.ToolTipText = "Automatically sync with Alibre selection"
realTimeButton.Margin = Padding(scale(10), scale(5), scale(10), scale(5))
realTimeButton.Padding = Padding(scale(12), scale(6), scale(12), scale(6))
realTimeButton.AutoSize = True
realTimeButton.Font = Font("Segoe UI", scale(9))
customPropButton = ToolStripButton("Custom Props")
customPropButton.ForeColor = Colors.WARNING
customPropButton.ToolTipText = "Lookup a custom property by name"
customPropButton.Margin = Padding(scale(10), scale(5), scale(10), scale(5))
customPropButton.Padding = Padding(scale(12), scale(6), scale(12), scale(6))
customPropButton.AutoSize = True
customPropButton.Font = Font("Segoe UI", scale(9))
setPropButton = ToolStripButton("Set Custom Prop")
setPropButton.ForeColor = Colors.WARNING
setPropButton.ToolTipText = "Set a custom property value"
setPropButton.Margin = Padding(scale(10), scale(5), scale(10), scale(5))
setPropButton.Padding = Padding(scale(12), scale(6), scale(12), scale(6))
setPropButton.AutoSize = True
setPropButton.Font = Font("Segoe UI", scale(9))
# Separator styling with high DPI scaling
sep1 = ToolStripSeparator()
sep1.Margin = Padding(scale(16), 0, scale(16), 0)
sep2 = ToolStripSeparator()
sep2.Margin = Padding(scale(16), 0, scale(16), 0)
sep3 = ToolStripSeparator()
sep3.Margin = Padding(scale(16), 0, scale(16), 0)
toolStrip.Items.AddRange(Array[ToolStripItem]((
refreshButton, sep1, expandAllButton, collapseAllButton, sep2, realTimeButton, sep3, customPropButton, setPropButton
)))
# Professional TreeView with high DPI scaling
treeView = TreeView()
treeView.Dock = DockStyle.Fill
treeView.Font = Font("Segoe UI", scale(9))
treeView.BackColor = Colors.PRIMARY
treeView.ForeColor = Colors.TEXT_PRIMARY
treeView.LineColor = Colors.BORDER
treeView.ShowLines, treeView.ShowPlusMinus, treeView.ShowRootLines = True, True, True
treeView.HideSelection, treeView.FullRowSelect = False, True
treeView.BorderStyle = BorderStyle.None
treeView.Indent = scale(32)
treeView.ItemHeight = scale(32) # Extra height for high DPI
treeView.Margin = Padding(100, scale(8), 0, 0)
# Professional PropertyGrid with high DPI scaling
propertyGrid = PropertyGrid()
propertyGrid.Dock = DockStyle.Fill
propertyGrid.BackColor = Colors.PRIMARY
propertyGrid.LineColor = Colors.BORDER
propertyGrid.CategoryForeColor = Colors.TEXT_PRIMARY
propertyGrid.CategorySplitterColor = Colors.BORDER
propertyGrid.HelpBackColor = Colors.SURFACE
propertyGrid.HelpForeColor = Colors.TEXT_SECONDARY
propertyGrid.ViewBackColor = Colors.PRIMARY
propertyGrid.ViewForeColor = Colors.TEXT_PRIMARY
propertyGrid.ToolbarVisible = True
propertyGrid.Font = Font("Segoe UI", scale(9))
propertyGrid.PropertySort = PropertySort.Categorized
propertyGrid.HelpVisible = True
propertyGrid.Margin = Padding(0, scale(8), 0, 0)
# --- CORRECTED CONTROL ADDITION ORDER ---
# To fix the layout, add the control with Dock.Fill first. Then, add the
# controls with Dock.Top. The last 'Top' control added will be at the very top.
# Add controls to the left panel
leftPanel.Controls.Add(treeView) # Add FILL control first
leftPanel.Controls.Add(toolStrip) # This will sit below the header
leftPanel.Controls.Add(treeHeader) # This will be at the very top
# Add controls to the right panel
rightPanel.Controls.Add(propertyGrid) # Add FILL control first
rightPanel.Controls.Add(propHeader) # This will be at the very top
# Custom Property Wrapper Classes (unchanged functionality)
class PropertyWrapper(object):
"""Base wrapper to expose all API properties"""
def __init__(self, obj, obj_type):
self._wrapped_object = obj
self._object_type = obj_type
self._properties = {}
self._load_properties()
def _load_properties(self):
"""Load all available properties based on object type"""
# This dictionary dispatches to the correct loading function
loader_map = {
"Part": self._load_part_properties,
"Assembly": self._load_assembly_properties,
"AssembledPart": self._load_assembled_part_properties,
"AssembledSubAssembly": self._load_assembled_sub_assembly_properties,
"Face": self._load_face_properties,
"Edge": self._load_edge_properties,
"Vertex": self._load_vertex_properties,
"Sketch": self._load_sketch_properties,
"Sketch3D": self._load_sketch_3d_properties,
"Parameter": self._load_parameter_properties,
"Configuration": self._load_configuration_properties,
"Feature": self._load_feature_properties,
"Plane": self._load_plane_properties,
"Axis": self._load_axis_properties,
"Point": self._load_point_properties,
"Line": self._load_line_properties,
"Line3D": self._load_line3d_properties,
"Circle": self._load_circle_properties,
"CircularArc": self._load_circular_arc_properties,
"CircularArc3D": self._load_circular_arc3d_properties,
"Ellipse": self._load_ellipse_properties,
"EllipticalArc": self._load_elliptical_arc_properties,
"Bspline": self._load_bspline_properties,
"Bspline3D": self._load_bspline3d_properties,
"SketchPoint": self._load_sketchpoint_properties,
"SketchPoint3D": self._load_sketchpoint3d_properties,
}
# Get the loader function from the map or use the generic one
loader_func = loader_map.get(self._object_type, self._load_generic_properties)
loader_func()
def _safe_get_property(self, prop_name, default_value="N/A"):
"""Safely get a property value, handling potential errors."""
try:
if hasattr(self._wrapped_object, prop_name):
value = getattr(self._wrapped_object, prop_name)
if value is None:
return "None"
# Format lists/tuples for better readability
if isinstance(value, (list, tuple, Array)):
return ", ".join(map(str, value))
return str(value)
return default_value
except Exception as e:
return "Error: " + str(e)
def _format_bbox(self, bbox_method_name):
"""Helper to get and format a bounding box."""
try:
bbox = getattr(self._wrapped_object, bbox_method_name)()
if bbox and len(bbox) == 8:
return "; ".join(["({:.3f}, {:.3f}, {:.3f})".format(p[0], p[1], p[2]) for p in bbox])
return "Invalid BBox Data"
except:
return "N/A"
def _load_base_file_properties(self):
"""Loads common properties for Part and Assembly objects."""
self._properties.update({
"Name": self._safe_get_property("Name"),
"FileName": self._safe_get_property("FileName"),
"Material": self._safe_get_property("Material"),
"Density": self._safe_get_property("Density"),
"Mass": self._safe_get_property("Mass"),
"Description": self._safe_get_property("Description"),
"Number": self._safe_get_property("Number"),
"Comment": self._safe_get_property("Comment"),
"CostCenter": self._safe_get_property("CostCenter"),
"CreatedBy": self._safe_get_property("CreatedBy"),
"CreatedDate": self._safe_get_property("CreatedDate"),
"CreatingApplication": self._safe_get_property("CreatingApplication"),
"DocumentNumber": self._safe_get_property("DocumentNumber"),
"EngineeringApprovalDate": self._safe_get_property("EngineeringApprovalDate"),
"EngineeringApprovedBy": self._safe_get_property("EngineeringApprovedBy"),
"EstimatedCost": self._safe_get_property("EstimatedCost"),
"Keywords": self._safe_get_property("Keywords"),
"LastAuthor": self._safe_get_property("LastAuthor"),
"LastUpdateDate": self._safe_get_property("LastUpdateDate"),
"ExtendedMaterialInformation": self._safe_get_property("ExtendedMaterialInformation"),
"ManufacturingApprovedBy": self._safe_get_property("ManufacturingApprovedBy"),
"ManufacturingApprovedDate": self._safe_get_property("ManufacturingApprovedDate"),
"ModifiedInformation": self._safe_get_property("ModifiedInformation"),
"Product": self._safe_get_property("Product"),
"ReceivedFrom": self._safe_get_property("ReceivedFrom"),
"Revision": self._safe_get_property("Revision"),
"StockSize": self._safe_get_property("StockSize"),
"Supplier": self._safe_get_property("Supplier"),
"Title": self._safe_get_property("Title"),
"Vendor": self._safe_get_property("Vendor"),
"WebLink": self._safe_get_property("WebLink"),
})
try:
self._properties["DisplayUnits"] = str(self._wrapped_object.DisplayUnits())
except:
self._properties["DisplayUnits"] = "N/A"
def _load_custom_properties(self):
"""Load custom properties using GetCustomProperty API"""
default_custom_names = ["A", "B", "C", "D", "E", "F", "G"]
for cp_name in default_custom_names:
try:
val = self._wrapped_object.GetCustomProperty(cp_name)
if val is not None:
self._properties["Custom: " + cp_name] = str(val)
else:
self._properties["Custom: " + cp_name] = ""
except:
self._properties["Custom: " + cp_name] = "(not defined)"
def _load_part_properties(self):
self._load_base_file_properties()
self._load_custom_properties()
self._properties["BoundingBox"] = self._format_bbox("GetBoundingBox")
def _load_assembly_properties(self):
self._load_base_file_properties()
self._load_custom_properties()
# Assembly does not have GetBoundingBox, it is calculated from components.
def _load_assembled_part_properties(self):
self._properties["Name"] = self._safe_get_property("Name")
self._properties["BoundingBox (Assembly Coords)"] = self._format_bbox("GetAssemblyBoundingBox")
try:
self._properties["ConfigurationCount"] = str(len(self._wrapped_object.Configurations))
except:
self._properties["ConfigurationCount"] = "0"
def _load_assembled_sub_assembly_properties(self):
self._properties["Name"] = self._safe_get_property("Name")
try:
self._properties["ConfigurationCount"] = str(len(self._wrapped_object.Configurations))
except:
self._properties["ConfigurationCount"] = "0"
def _load_face_properties(self):
self._properties["Name"] = self._safe_get_property("Name")
try: self._properties["Area"] = str(self._wrapped_object.GetArea())
except: self._properties["Area"] = "N/A"
try: self._properties["IsRectangle"] = str(self._wrapped_object.IsRectangle())
except: self._properties["IsRectangle"] = "N/A"
try: self._properties["EdgeCount"] = str(len(self._wrapped_object.GetEdges()))
except: self._properties["EdgeCount"] = "N/A"
try: self._properties["VertexCount"] = str(len(self._wrapped_object.GetVertices()))
except: self._properties["VertexCount"] = "N/A"
try: self._properties["AdjoiningFaceCount"] = str(len(self._wrapped_object.GetAdjoiningFaces()))
except: self._properties["AdjoiningFaceCount"] = "N/A"
def _load_edge_properties(self):
self._properties["Name"] = self._safe_get_property("Name")
self._properties["Length"] = self._safe_get_property("Length")
self._properties["Diameter"] = self._safe_get_property("Diameter")
try: self._properties["VertexCount"] = str(len(self._wrapped_object.GetVertices()))
except: self._properties["VertexCount"] = "N/A"
def _load_vertex_properties(self):
self._properties.update({
"Name": self._safe_get_property("Name"),
"X": self._safe_get_property("X"),
"Y": self._safe_get_property("Y"),
"Z": self._safe_get_property("Z")
})
def _load_sketch_properties(self):
self._properties["Name"] = self._safe_get_property("Name")
try: self._properties["FigureCount"] = str(len(self._wrapped_object.Figures))
except: self._properties["FigureCount"] = "0"
try: self._properties["Surface"] = self._wrapped_object.GetSurface().Name
except: self._properties["Surface"] = "N/A"
def _load_sketch_3d_properties(self):
self._properties["Name"] = self._safe_get_property("Name")
try: self._properties["FigureCount"] = str(len(self._wrapped_object.Figures))
except: self._properties["FigureCount"] = "0"
def _load_parameter_properties(self):
self._properties.update({
"Name": self._safe_get_property("Name"),
"Value": self._safe_get_property("Value"),
"Equation": self._safe_get_property("Equation"),
"Comment": self._safe_get_property("Comment"),
"Type": self._safe_get_property("Type"),
"Units": self._safe_get_property("Units"),
"RawValue": self._safe_get_property("RawValue"),
"ExcelWorkbook": self._safe_get_property("ExcelWorkbook"),
"ExcelSheet": self._safe_get_property("ExcelSheet"),
"ExcelCell": self._safe_get_property("ExcelCell"),
})
def _load_configuration_properties(self):
self._properties.update({
"Name": self._safe_get_property("Name"),
"IsActive": self._safe_get_property("IsActive")
})
def _load_feature_properties(self):
self._properties["Name"] = self._safe_get_property("Name")
# Color is a method SetColor, not a property to get.
def _load_plane_properties(self):
self._properties["Name"] = self._safe_get_property("Name")
def _load_axis_properties(self):
self._properties["Name"] = self._safe_get_property("Name")
def _load_point_properties(self):
self._properties["Name"] = self._safe_get_property("Name")
try:
coords = self._wrapped_object.GetCoordinates()
self._properties["X"] = str(coords[0])
self._properties["Y"] = str(coords[1])
self._properties["Z"] = str(coords[2])
except:
self._properties["X"] = self._safe_get_property("X")
self._properties["Y"] = self._safe_get_property("Y")
self._properties["Z"] = self._safe_get_property("Z")
# Sketch Figure Properties
def _load_line_properties(self):
self._properties["Type"] = "Line"
self._properties["StartPoint"] = self._safe_get_property("StartPoint")
self._properties["EndPoint"] = self._safe_get_property("EndPoint")
self._properties["Length"] = self._safe_get_property("Length")
self._properties["IsReference"] = self._safe_get_property("IsReference")
def _load_line3d_properties(self):
self._properties["Type"] = "3D Line"
self._properties["StartPoint"] = self._safe_get_property("StartPoint")
self._properties["EndPoint"] = self._safe_get_property("EndPoint")
self._properties["Length"] = self._safe_get_property("Length")
self._properties["IsReference"] = self._safe_get_property("IsReference")
def _load_circle_properties(self):
self._properties["Type"] = "Circle"
self._properties["Center"] = self._safe_get_property("Center")
self._properties["Radius"] = self._safe_get_property("Radius")
self._properties["Length"] = self._safe_get_property("Length")
self._properties["IsReference"] = self._safe_get_property("IsReference")
def _load_circular_arc_properties(self):
self._properties["Type"] = "Circular Arc"
self._properties["Center"] = self._safe_get_property("Center")
self._properties["StartPoint"] = self._safe_get_property("StartPoint")
self._properties["EndPoint"] = self._safe_get_property("EndPoint")
self._properties["Radius"] = self._safe_get_property("Radius")
self._properties["Angle"] = self._safe_get_property("Angle")
self._properties["IsReference"] = self._safe_get_property("IsReference")
def _load_circular_arc3d_properties(self):
self._properties["Type"] = "3D Circular Arc"
self._properties["Center"] = self._safe_get_property("Center")
self._properties["StartPoint"] = self._safe_get_property("StartPoint")
self._properties["EndPoint"] = self._safe_get_property("EndPoint")
self._properties["Radius"] = self._safe_get_property("Radius")
self._properties["Angle"] = self._safe_get_property("Angle")
self._properties["IsReference"] = self._safe_get_property("IsReference")
def _load_ellipse_properties(self):
self._properties["Type"] = "Ellipse"
self._properties["Center"] = self._safe_get_property("Center")
self._properties["Radius"] = self._safe_get_property("Radius")
self._properties["MajorAxisAngle"] = self._safe_get_property("MajorAxisAngle")
self._properties["MinorMajorRatio"] = self._safe_get_property("MinorMajorRatio")
self._properties["IsReference"] = self._safe_get_property("IsReference")
def _load_elliptical_arc_properties(self):
self._properties["Type"] = "Elliptical Arc"
self._properties["Center"] = self._safe_get_property("Center")
self._properties["StartPoint"] = self._safe_get_property("StartPoint")
self._properties["EndPoint"] = self._safe_get_property("EndPoint")
self._properties["Radius"] = self._safe_get_property("Radius")
self._properties["MajorAxisAngle"] = self._safe_get_property("MajorAxisAngle")
self._properties["MinorMajorRatio"] = self._safe_get_property("MinorMajorRatio")
self._properties["IsReference"] = self._safe_get_property("IsReference")
def _load_bspline_properties(self):
self._properties["Type"] = "B-spline"
self._properties["Order"] = self._safe_get_property("Order")
self._properties["Length"] = self._safe_get_property("Length")
self._properties["ControlPoints"] = self._safe_get_property("ControlPoints")
self._properties["KnotVectors"] = self._safe_get_property("KnotVectors")
self._properties["Weights"] = self._safe_get_property("Weights")
self._properties["IsReference"] = self._safe_get_property("IsReference")
def _load_bspline3d_properties(self):
self._properties["Type"] = "3D B-spline"
self._properties["Order"] = self._safe_get_property("Order")
self._properties["Length"] = self._safe_get_property("Length")
self._properties["ControlPoints"] = self._safe_get_property("ControlPoints")
self._properties["KnotVectors"] = self._safe_get_property("KnotVectors")
self._properties["Weights"] = self._safe_get_property("Weights")
self._properties["IsReference"] = self._safe_get_property("IsReference")
def _load_sketchpoint_properties(self):
self._properties["Type"] = "Sketch Point"
self._properties["X"] = self._safe_get_property("X")
self._properties["Y"] = self._safe_get_property("Y")
self._properties["IsReference"] = self._safe_get_property("IsReference")
def _load_sketchpoint3d_properties(self):
self._properties["Type"] = "3D Sketch Point"
self._properties["X"] = self._safe_get_property("X")
self._properties["Y"] = self._safe_get_property("Y")
self._properties["Z"] = self._safe_get_property("Z")
self._properties["IsReference"] = self._safe_get_property("IsReference")
def _load_generic_properties(self):
"""Load generic properties for unknown types"""
self._properties["Name"] = self._safe_get_property("Name", "N/A")
self._properties["ObjectType"] = str(type(self._wrapped_object).__name__)
def GetAllProperties(self):
return self._properties
# Enhanced property display using a custom object
class PropertyDisplayObject(object):
def __init__(self, wrapped_obj):
self._original_object = wrapped_obj
obj_type_name = type(wrapped_obj).__name__
# Use a dictionary for more robust type mapping
type_map = {
"ADPart": "Part",
"ADAssembly": "Assembly",
"ADFace": "Face",
"ADEdge": "Edge",
"ADVertex": "Vertex",
"ADSketch": "Sketch",
"ADSketch3D": "Sketch3D",
"ADParameter": "Parameter",
"ADConfiguration": "Configuration",
"ADFeature": "Feature",
"ADPlane": "Plane",
"ADAxis": "Axis",
"ADPoint": "Point",
"ADAssembledPart": "AssembledPart",
"ADAssembledSubAssembly": "AssembledSubAssembly",
"ADLine": "Line",
"ADLine3D": "Line3D",
"ADCircle": "Circle",
"ADCircularArc": "CircularArc",
"ADCircularArc3D": "CircularArc3D",
"ADEllipse": "Ellipse",
"ADEllipticalArc": "EllipticalArc",
"ADBspline": "Bspline",
"ADBspline3D": "Bspline3D",
"ADSketchPoint": "SketchPoint",
"ADSketchPoint3D": "SketchPoint3D"
}
obj_type = type_map.get(obj_type_name, obj_type_name)
self._wrapper = PropertyWrapper(wrapped_obj, obj_type)
# Dynamically add properties for the PropertyGrid
for prop_name, prop_value in self._wrapper.GetAllProperties().items():
clean_prop_name = prop_name.replace(" ", "_").replace("-", "_").replace("(", "").replace(")", "")
setattr(self, clean_prop_name, prop_value)
# --- REAL-TIME SELECTION TRACKING ---
last_alibre_selection = None
manual_selection_active = False
selectionTimer = Timer()
selectionTimer.Interval = 500 # Check for new selections every 500ms
def UpdateStatusBar(message, selection_text=""):
"""Update the status bar with current information"""
statusLabel.Text = message
selectionLabel.Text = selection_text
def OnTimerTick(sender, args):
"""Checks the active selection in Alibre and updates the PropertyGrid"""
global last_alibre_selection, manual_selection_active
if not realTimeButton.Checked or manual_selection_active:
return
try:
selections = MyModel.Selections
if selections and len(selections) > 0:
current_object = selections[0]
if current_object != last_alibre_selection:
display_object = PropertyDisplayObject(current_object)
propertyGrid.SelectedObject = display_object
last_alibre_selection = current_object
# Update status bar
obj_name = "Unknown"
try:
if hasattr(current_object, 'Name') and current_object.Name:
obj_name = current_object.Name
else:
obj_name = type(current_object).__name__
except:
obj_name = type(current_object).__name__
UpdateStatusBar("Ready - Model: " + MyModel.Name, "Selected: " + obj_name)
elif last_alibre_selection is not None:
if not manual_selection_active:
propertyGrid.SelectedObject = None
UpdateStatusBar("Ready - Model: " + MyModel.Name, "No selection")
last_alibre_selection = None
except:
selectionTimer.Stop()
selectionTimer.Tick += OnTimerTick
selectionTimer.Start()
form.FormClosing += lambda s, e: selectionTimer.Stop()
def AddObjectNode(parentNode, item, name=None):
"""Helper to add an object node to the tree - only if object is valid"""
if item is None:
return None
try:
if name:
display_name = name
elif hasattr(item, 'Name') and item.Name:
display_name = item.Name
else:
display_name = str(type(item).__name__)
except:
display_name = str(type(item).__name__)
node = TreeNode(display_name)
node.Tag = item
# Add visual indicators with Unicode symbols
obj_type = type(item).__name__
if "Assembly" in obj_type:
display_name = "🗂️ " + display_name
elif "Part" in obj_type:
display_name = "🔧 " + display_name
elif "Face" in obj_type:
display_name = "📄 " + display_name
elif "Edge" in obj_type:
display_name = "📏 " + display_name
elif "Vertex" in obj_type:
display_name = "📍 " + display_name
elif "Sketch" in obj_type:
display_name = "[S] " + display_name
elif "Feature" in obj_type:
display_name = "⚙️ " + display_name
elif "Parameter" in obj_type:
display_name = "🔢 " + display_name
elif "Configuration" in obj_type:
display_name = "⚙️ " + display_name
elif "Plane" in obj_type:
display_name = "🔲 " + display_name
elif "Axis" in obj_type:
display_name = "↔️ " + display_name
elif "Point" in obj_type:
display_name = "🎯 " + display_name
node.Text = display_name
if parentNode:
parentNode.Nodes.Add(node)
else:
treeView.Nodes.Add(node)
return node
def AddCollectionNode(parentNode, collectionName, collection_provider):
"""Helper to add a collection node with all its items - only if collection has real items"""
collection = None
try:
collection = collection_provider() if callable(collection_provider) else collection_provider
if collection is None or len(collection) == 0:
return None
except:
return None
collectionNode = TreeNode("📁 %s (%d)" % (collectionName, len(collection)))
collectionNode.Tag = collection
parentNode.Nodes.Add(collectionNode)
for i, item in enumerate(collection):
if item is None:
continue
try:
if hasattr(item, 'Name') and item.Name:
item_name = item.Name
else:
item_name = "%s[%d]" % (type(item).__name__, i)
itemNode = AddObjectNode(collectionNode, item, item_name)
# Recursively add nodes for complex objects like sketches or assembled parts
if itemNode:
obj_type_name = type(item).__name__
if "Sketch" in obj_type_name and hasattr(item, 'Figures'):
AddCollectionNode(itemNode, "Figures", item.Figures)
elif obj_type_name == "ADAssembledPart":
PopulateAssembledPartNode(itemNode, item)
elif obj_type_name == "ADAssembledSubAssembly":
PopulateSubAssemblyNode(itemNode, item)
except Exception as e:
# Skip invalid items silently
pass
return collectionNode
def PopulateAssembledPartNode(partNode, part):
"""Adds geometry collections for an AssembledPart"""
AddCollectionNode(partNode, "Faces", part.GetFaces)
AddCollectionNode(partNode, "Edges", part.GetEdges)
# GetAssemblyVertices returns a list of lists, not vertex objects, so we handle it differently
try:
vertices = part.GetAssemblyVertices()
if vertices and len(vertices) > 0:
v_node = TreeNode("📁 Vertices (%d)" % len(vertices))
partNode.Nodes.Add(v_node)
except:
pass
def PopulateSubAssemblyNode(assemblyNode, subAssembly):
"""Recursively populates a sub-assembly node"""
AddCollectionNode(assemblyNode, "Parts", subAssembly.Parts)
AddCollectionNode(assemblyNode, "SubAssemblies", subAssembly.SubAssemblies)
def BuildTreeNodes():
"""Builds the tree structure using only real model data"""
treeView.BeginUpdate()
treeView.Nodes.Clear()
UpdateStatusBar("Building model tree...", "")
# Root node
rootNode = AddObjectNode(None, MyModel, MyModel.Name)
# Add common collections that are directly accessible
AddCollectionNode(rootNode, "Configurations", MyModel.Configurations)
AddCollectionNode(rootNode, "Parameters", MyModel.Parameters)
# Add Custom Properties node
default_custom_names = ["A", "B", "C", "D", "E", "F", "G"]
custom_props_found = []
for cp_name in default_custom_names:
try:
val = MyModel.GetCustomProperty(cp_name)
if val is not None:
custom_props_found.append((cp_name, str(val)))
else:
custom_props_found.append((cp_name, ""))
except:
custom_props_found.append((cp_name, "(not defined)"))
cpNode = TreeNode("Custom Properties (%d)" % len(custom_props_found))
cpNode.Tag = MyModel
rootNode.Nodes.Add(cpNode)
for cp_name, cp_val in custom_props_found:
display_text = cp_name + " = " + cp_val
childNode = TreeNode(display_text)
childNode.Tag = MyModel
cpNode.Nodes.Add(childNode)
if IsAssembly:
AddCollectionNode(rootNode, "Parts", MyModel.Parts)
AddCollectionNode(rootNode, "SubAssemblies", MyModel.SubAssemblies)
else: # It's a Part
AddCollectionNode(rootNode, "Faces", MyModel.GetFaces)
AddCollectionNode(rootNode, "Edges", MyModel.GetEdges)
AddCollectionNode(rootNode, "Vertices", MyModel.GetVertices)
# --- BRUTE-FORCE DISCOVERY ---
# The Alibre API does not provide direct collections for items like Features, Sketches,
# Planes, Axes, or Points. The only way to find them is to try getting them by name.
# The following sections attempt to find these items by iterating through common naming patterns.
def find_named_items(collection_name, get_method, name_patterns, try_3d_sketch=False):
found_items = []
found_names = set()
for name in name_patterns:
try:
item = get_method(name)
if item and name not in found_names:
found_items.append((name, item))
found_names.add(name)
except: pass
if try_3d_sketch:
try:
item3d = MyModel.Get3DSketch(name)
if item3d and (name + "_3D") not in found_names:
found_items.append((name + " (3D)", item3d))
found_names.add(name + "_3D")
except: pass
if found_items:
node = TreeNode("📁 %s (%d)" % (collection_name, len(found_items)))
rootNode.Nodes.Add(node)
for item_name, item_obj in found_items:
itemNode = AddObjectNode(node, item_obj, item_name)
# For sketches, also add their figures
if itemNode and "Sketch" in collection_name and hasattr(item_obj, 'Figures'):
AddCollectionNode(itemNode, "Figures", item_obj.Figures)
# Define name patterns
base_names = {
"Feature": ["Extrude", "Extrusion", "Cut", "Fillet", "Chamfer", "Hole", "Pattern", "Revolve", "Loft", "Sweep", "Shell", "Draft", "Mirror", "Feature", "Boss", "stack"],
"Sketch": ["Sketch", "Profile", "Path", "Section", "2D Sketch", "3D Sketch"],
"Plane": ["Plane"],
"Axis": ["Axis"],
"Point": ["Point", "Origin"]
}
names_to_try = {}
for key, prefixes in base_names.items():
current_list = []
for prefix in prefixes:
# Also try the prefix by itself, e.g., for a feature named just "stack"
current_list.append(prefix)
for i in range(1, 51): # Check up to 50 of each type
current_list.extend([
"%s<%d>" % (prefix, i), # e.g., Sketch<1>
"%s%d" % (prefix, i), # e.g., Sketch1
"%s <%d>" % (prefix, i), # e.g., Sketch <1>
"%s %d" % (prefix, i) # e.g., Sketch 1
])
names_to_try[key] = current_list
# Add standard planes/axes/origin if they exist
try:
if MyModel.XYPlane: AddObjectNode(rootNode, MyModel.XYPlane, "XY-Plane")
if MyModel.YZPlane: AddObjectNode(rootNode, MyModel.YZPlane, "YZ-Plane")
if MyModel.ZXPlane: AddObjectNode(rootNode, MyModel.ZXPlane, "ZX-Plane")
if MyModel.XAxis: AddObjectNode(rootNode, MyModel.XAxis, "X-Axis")
if MyModel.YAxis: AddObjectNode(rootNode, MyModel.YAxis, "Y-Axis")
if MyModel.ZAxis: AddObjectNode(rootNode, MyModel.ZAxis, "Z-Axis")
if MyModel.Origin: AddObjectNode(rootNode, MyModel.Origin, "Origin")
except: pass
# Find and add items
find_named_items("Features", MyModel.GetFeature, names_to_try["Feature"])
find_named_items("Sketches", MyModel.GetSketch, names_to_try["Sketch"], try_3d_sketch=True)
find_named_items("Planes", MyModel.GetPlane, names_to_try["Plane"])
find_named_items("Axes", MyModel.GetAxis, names_to_try["Axis"])
find_named_items("Points", MyModel.GetPoint, names_to_try["Point"])
treeView.EndUpdate()
rootNode.Expand()
UpdateStatusBar("Ready - Model: " + MyModel.Name, "Tree built successfully")
def OnNodeSelect(sender, args):
"""When a node is selected in the tree, show its properties with full API exposure"""
global manual_selection_active
manual_selection_active = True
if treeView.SelectedNode and treeView.SelectedNode.Tag is not None:
selected_object = treeView.SelectedNode.Tag
# Check if tag is a collection, if so, select the parent model
if isinstance(selected_object, (list, tuple, Array)):
if treeView.SelectedNode.Parent and treeView.SelectedNode.Parent.Tag:
selected_object = treeView.SelectedNode.Parent.Tag
else:
selected_object = MyModel
propertyGrid.SelectedObject = PropertyDisplayObject(selected_object)
# Update status bar
try:
obj_name = treeView.SelectedNode.Text
if obj_name.startswith("📁 "):
obj_name = obj_name[3:] # Remove folder emoji and extra space
elif len(obj_name) > 3 and obj_name[2:3] == " ":
obj_name = obj_name[3:] # Remove other emojis and extra space
UpdateStatusBar("Ready - Model: " + MyModel.Name, "Selected: " + obj_name)
except:
UpdateStatusBar("Ready - Model: " + MyModel.Name, "Tree selection")
else:
propertyGrid.SelectedObject = None
UpdateStatusBar("Ready - Model: " + MyModel.Name, "No selection")
# Reset manual selection flag after a short delay
def ResetManualFlag(s, e):
global manual_selection_active
manual_selection_active = False
s.Stop()
resetTimer = Timer()
resetTimer.Interval = 1000
resetTimer.Tick += ResetManualFlag
resetTimer.Start()
# Enhanced Event Handlers
def OnRefreshClick(sender, args):
BuildTreeNodes()
UpdateStatusBar("Ready - Model: " + MyModel.Name, "Model refreshed")
def OnExpandAllClick(sender, args):
treeView.ExpandAll()
UpdateStatusBar("Ready - Model: " + MyModel.Name, "All nodes expanded")
def OnCollapseAllClick(sender, args):
treeView.CollapseAll()
UpdateStatusBar("Ready - Model: " + MyModel.Name, "All nodes collapsed")
def OnRealTimeToggle(sender, args):
if realTimeButton.Checked:
realTimeButton.ForeColor = Colors.SUCCESS
selectionTimer.Start()
UpdateStatusBar("Ready - Model: " + MyModel.Name, "Real-time sync enabled")
else:
realTimeButton.ForeColor = Colors.TEXT_PRIMARY
selectionTimer.Stop()
UpdateStatusBar("Ready - Model: " + MyModel.Name, "Real-time sync disabled")
def OnCustomPropClick(sender, args):
"""Show a dialog to lookup a custom property by name"""
dlg = Form()
dlg.Text = "Lookup Custom Property"
dlg.Width = scale(400)
dlg.Height = scale(220)
dlg.StartPosition = FormStartPosition.CenterParent
dlg.BackColor = Colors.PRIMARY
dlg.ForeColor = Colors.TEXT_PRIMARY
dlg.Font = Font("Segoe UI", scale(9))
dlg.FormBorderStyle = FormBorderStyle.FixedDialog
dlg.MaximizeBox = False
dlg.MinimizeBox = False
lbl = Label()
lbl.Text = "Property Name:"
lbl.Location = Point(scale(20), scale(20))
lbl.Size = Size(scale(340), scale(25))
lbl.ForeColor = Colors.TEXT_PRIMARY
dlg.Controls.Add(lbl)
txtName = TextBox()
txtName.Location = Point(scale(20), scale(50))
txtName.Size = Size(scale(340), scale(28))
txtName.BackColor = Colors.SURFACE
txtName.ForeColor = Colors.TEXT_PRIMARY
txtName.Font = Font("Segoe UI", scale(9))
dlg.Controls.Add(txtName)
lblResult = Label()
lblResult.Text = ""
lblResult.Location = Point(scale(20), scale(90))
lblResult.Size = Size(scale(340), scale(40))
lblResult.ForeColor = Colors.TEXT_SECONDARY
dlg.Controls.Add(lblResult)
def OnLookup(s, e):
prop_name = txtName.Text.strip()
if prop_name == "":
lblResult.Text = "Please enter a property name."
return
try:
val = MyModel.GetCustomProperty(prop_name)
if val is not None:
lblResult.Text = prop_name + " = " + str(val)
lblResult.ForeColor = Colors.SUCCESS
else:
lblResult.Text = prop_name + " = (empty)"
lblResult.ForeColor = Colors.TEXT_SECONDARY
except Exception as ex:
lblResult.Text = "Not found or error: " + str(ex)
lblResult.ForeColor = Color.FromArgb(255, 100, 100)
UpdateStatusBar("Ready - Model: " + MyModel.Name, "Looked up: " + prop_name)
btnLookup = Button()
btnLookup.Text = "Lookup"
btnLookup.Location = Point(scale(20), scale(140))
btnLookup.Size = Size(scale(100), scale(32))
btnLookup.BackColor = Colors.ACCENT
btnLookup.ForeColor = Colors.TEXT_PRIMARY
btnLookup.FlatStyle = FlatStyle.Flat
btnLookup.Click += OnLookup
dlg.Controls.Add(btnLookup)
dlg.AcceptButton = btnLookup
btnClose = Button()
btnClose.Text = "Close"
btnClose.Location = Point(scale(260), scale(140))
btnClose.Size = Size(scale(100), scale(32))
btnClose.BackColor = Colors.SECONDARY
btnClose.ForeColor = Colors.TEXT_PRIMARY
btnClose.FlatStyle = FlatStyle.Flat
btnClose.Click += lambda s, e: dlg.Close()
dlg.Controls.Add(btnClose)
dlg.CancelButton = btnClose
dlg.ShowDialog()
def OnSetPropClick(sender, args):
"""Show a dialog to set a custom property value"""
dlg = Form()
dlg.Text = "Set Custom Property"
dlg.Width = scale(400)
dlg.Height = scale(280)
dlg.StartPosition = FormStartPosition.CenterParent
dlg.BackColor = Colors.PRIMARY
dlg.ForeColor = Colors.TEXT_PRIMARY
dlg.Font = Font("Segoe UI", scale(9))
dlg.FormBorderStyle = FormBorderStyle.FixedDialog
dlg.MaximizeBox = False
dlg.MinimizeBox = False
lblName = Label()
lblName.Text = "Property Name:"
lblName.Location = Point(scale(20), scale(20))
lblName.Size = Size(scale(340), scale(25))
lblName.ForeColor = Colors.TEXT_PRIMARY
dlg.Controls.Add(lblName)
txtName = TextBox()
txtName.Location = Point(scale(20), scale(50))
txtName.Size = Size(scale(340), scale(28))
txtName.BackColor = Colors.SURFACE
txtName.ForeColor = Colors.TEXT_PRIMARY
txtName.Font = Font("Segoe UI", scale(9))
dlg.Controls.Add(txtName)
lblVal = Label()
lblVal.Text = "Property Value:"
lblVal.Location = Point(scale(20), scale(90))
lblVal.Size = Size(scale(340), scale(25))
lblVal.ForeColor = Colors.TEXT_PRIMARY
dlg.Controls.Add(lblVal)
txtVal = TextBox()
txtVal.Location = Point(scale(20), scale(120))
txtVal.Size = Size(scale(340), scale(28))
txtVal.BackColor = Colors.SURFACE
txtVal.ForeColor = Colors.TEXT_PRIMARY
txtVal.Font = Font("Segoe UI", scale(9))
dlg.Controls.Add(txtVal)
lblResult = Label()
lblResult.Text = ""
lblResult.Location = Point(scale(20), scale(160))
lblResult.Size = Size(scale(340), scale(40))
lblResult.ForeColor = Colors.TEXT_SECONDARY
dlg.Controls.Add(lblResult)
def OnSet(s, e):
prop_name = txtName.Text.strip()
prop_val = txtVal.Text
if prop_name == "":
lblResult.Text = "Please enter a property name."
return
try:
MyModel.SetCustomProperty(prop_name, prop_val)
lblResult.Text = "Set " + prop_name + " = " + prop_val
lblResult.ForeColor = Colors.SUCCESS
UpdateStatusBar("Ready - Model: " + MyModel.Name, "Set: " + prop_name)
# Refresh tree to show updated values
BuildTreeNodes()
except Exception as ex:
lblResult.Text = "Error: " + str(ex)
lblResult.ForeColor = Color.FromArgb(255, 100, 100)
btnSet = Button()
btnSet.Text = "Set"
btnSet.Location = Point(scale(20), scale(210))
btnSet.Size = Size(scale(100), scale(32))
btnSet.BackColor = Colors.ACCENT
btnSet.ForeColor = Colors.TEXT_PRIMARY
btnSet.FlatStyle = FlatStyle.Flat
btnSet.Click += OnSet
dlg.Controls.Add(btnSet)
dlg.AcceptButton = btnSet
btnClose = Button()
btnClose.Text = "Close"
btnClose.Location = Point(scale(260), scale(210))
btnClose.Size = Size(scale(100), scale(32))
btnClose.BackColor = Colors.SECONDARY
btnClose.ForeColor = Colors.TEXT_PRIMARY
btnClose.FlatStyle = FlatStyle.Flat
btnClose.Click += lambda s, e: dlg.Close()
dlg.Controls.Add(btnClose)
dlg.CancelButton = btnClose
dlg.ShowDialog()
def OnPropertyValueChanged(sender, args):
"""Write custom property changes back to the model"""
try:
label = args.ChangedItem.Label
if label is not None and label.startswith("Custom:_"):
cp_name = label[len("Custom:_"):]
new_val = str(args.ChangedItem.Value)
try:
MyModel.SetCustomProperty(cp_name, new_val)
UpdateStatusBar("Ready - Model: " + MyModel.Name, "Updated: " + cp_name + " = " + new_val)
BuildTreeNodes()
except Exception as ex:
Win = Windows()
Win.ErrorDialog("Failed to set custom property '" + cp_name + "':\n" + str(ex) + "\n\nThe property must already be defined on the part/assembly or on your PC.", "Custom Property Error")
except:
pass
# Assign Event Handlers
treeView.AfterSelect += OnNodeSelect
refreshButton.Click += OnRefreshClick
expandAllButton.Click += OnExpandAllClick
collapseAllButton.Click += OnCollapseAllClick
realTimeButton.Click += OnRealTimeToggle
customPropButton.Click += OnCustomPropClick
setPropButton.Click += OnSetPropClick
propertyGrid.PropertyValueChanged += OnPropertyValueChanged
# Build the initial tree and show the form
BuildTreeNodes()
UpdateStatusBar("Ready - Model: " + MyModel.Name, "Explorer initialized")
form.ShowDialog()
# Run the script
CreatePropertyExplorer()
@stephensmitchell

Copy link
Copy Markdown
Author
image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment