Creating a flexible selection dialog with WindowsForms and ListBox in Inventor-rules
A selection dialog that automatically displays the names of most Inventor API objects, returns the selected object and DialogResult, and is customizable
Links on the topic: The Video about the flexible Inventor-API selection dialog at Youtube or DailyMotion .
Why a self-built selection dialog?
The Inventor API offers only the iLogic InputListBox
or the InputListBoxDialog
to let users choose something from a list. These require a list of strings or objects, whose .ToString
. To compensate for this, I know only the workaround proposed by Jelte de Jong, which is sufficient for many cases: First, all objects in the list are wrapped in new objects of a wrapper class with a customized .ToString
method, and then the InputListBox is passed a list of wrapper objects, which sometimes seems a bit cumbersome.
In addition, the return value of the InputListBox is then a wrapper object and not the object with which you continue to work.
If you declare an InputListBoxDialog
and call ShowDialog
, you can get a DialogResult
and access some properties such as the selected object and the selected index after closing the dialog. However, apparently there is no way to influence the content of the displayed list here either, as it is filled only after the call to ShowDialog
. So you cannot exchange the displayed strings for the actual object names.
That is why I wrote my own SelectionDialog
class, which reads and displays the usual name properties from a list of objects, returns the selected object and also the DialogResult. Since the dialog is created as an object as usual with WindowsForms before it is displayed, all properties of the dialog can be adjusted.
Here I explain the code section by section first, at the end follows the complete code.
Main method and use of the dialog
First the introduction: We import the window management for Windows apps and Reflection to be able to query type properties. Then comes the main method. These are the prerequisites for using the SelectionDialog
class.
In order to place the dialog in the middle of the Inventor main window, we need its geometry.
Imports System.Windows.Forms Imports System.Reflection Sub Main() Dim invApp As Inventor.Application = ThisApplication Dim width As Integer = invApp.Width Dim height As Integer = invApp.Height Dim links As Integer = invApp.Left Dim oben As Integer = invApp.Top Dim windowCenter As Point2d = invApp.TransientGeometry.CreatePoint2d(links + width \ 2, oben + height \ 2)
Then the parametrically typed dialog window is created. What exactly happens there will be explained in the class explanation. In this case, a list of materials is to be output, which are contained in an arbitrarily chosen material library as an example. The dialog window size is adjusted to the number of materials with the adjustment method. The height is the height of the Inventor window, because I was not able to read the screen height from Inventor. The same applies to the position, which is set to the center of the Inventor window. One button is renamed.
Dim materialSelectionDialog As New SelectionDialog(Of Asset)(invApp.AssetLibraries(2).MaterialAssets) materialSelectionDialog.Text = "Select material" materialSelectionDialog.ButtonOK.Text = "Select"
The next section shows the created dialog modally, waits for input and processes the user selection. If the user chooses "OK", the selected asset is stored in the variable material. If a material has been selected, its name is output; here, of course, something would normally happen with the selected material. In case of cancellation, a message is logged and the subroutine is ended. This could also be done earlier if you check for DialogResult.Cancel
.
Dim material As Asset = Nothing Dim response As DialogResult = materialSelectionDialog.ShowDialog() If response = DialogResult.OK Then material = materialSelectionDialog.SelectedItem End If If material IsNot Nothing Then Logger.Info(material.DisplayName & " selected.") Else Logger.Info("No material selected.") Exit Sub End If End Sub
Structure of the dialog
The next code section defines the core of the topic: The SelectionDialog
class, which inherits from Form (a Windows window) and was mostly automatically generated by Visual Studio. It also
uses a generic type T
. The type allows us to return exactly the object we received in the list. The controls ButtonOK
and ButtonCancel
as well as properties for the selected index (SelectedIndex
), the selected element (SelectedItem
), an adapter (Adapter
; more on that later) and the items to be displayed (Items
). The SelectedItem
should take the parametric type. The constructor of the class initializes the dialog by determining the number of elements in the passed IEnumerable
. Different methods are tried to determine the number (Length
property, Count
property, Count
method or by iteration) because there can be different types of object collections in Inventor. If no direct way to determine the number is found, we simply count through.
Public Class SelectionDialog(Of T) Inherits Form Private ReadOnly ListBox1 As New ListBox() Friend ButtonOK As New Button() Friend ButtonCancel As New Button() Public Property SelectedIndex As Integer Public Property SelectedItem As T Private Property Adapter As IObjectAdapter Private Property Items As IEnumerable Public Sub New(items As IEnumerable, Optional adapter As IObjectAdapter = Nothing, Optional startIndex As Integer = -1) Dim count As Integer = 0 Dim listTypeInfo As Type = items.GetType() Dim prop As System.Reflection.PropertyInfo prop = listTypeInfo.GetProperty("Length") If prop Is Nothing Then prop = listTypeInfo.GetProperty("Count") End If If prop IsNot Nothing Then count = prop.GetValue(items) Else Dim met As System.Reflection.MethodInfo met = listTypeInfo.GetMethod("Count") If met IsNot Nothing Then count = listTypeInfo.GetMethod("Count").Invoke(items, Nothing) count = met.Invoke(items, Nothing) Else For Each item As Object In items count += 1 Next End If End If
Determining the evaluation type for creating the list display
This section determines how the elements should be displayed in the ListBox
. If no adapter was passed, the code tries to automatically determine the values to be displayed. To do this, it checks whether the elements have a DisplayName
property, a Name
property or a ToString
method. If an adapter is present that contains the method for creating the string list, this is used for display. The code dynamically accesses the properties of the objects at runtime and handles exceptions if the expected properties are not present. Which display option was chosen is determined in the Enum evaluation
.
Dim evaluation As EvaluationType = EvaluationType.NoEvaluation Me.Items = items If adapter Is Nothing Then Dim typeInfo As Type = Nothing If typeInfo Is Nothing Then typeInfo = GetType(T) End If If typeInfo Is GetType(String) Then evaluation = EvaluationType.EvaluationString ElseIf typeInfo.GetProperty("DisplayName") IsNot Nothing Or typeInfo.GetMethod("DisplayName") IsNot Nothing Then evaluation = EvaluationType.EvaluationDisplayName ElseIf typeInfo.GetProperty("Name") IsNot Nothing Then evaluation = EvaluationType.EvaluationName ElseIf typeInfo.GetMethod("ToString") IsNot Nothing Then evaluation = EvaluationType.EvaluationToString End If If evaluation = EvaluationType.NoEvaluation Then Dim test As String Try test = Me.Items(0).DisplayName evaluation = EvaluationType.EvaluationDisplayName Catch Try test = Me.Items(0).Name evaluation = EvaluationType.EvaluationName Catch End Try End Try
Configuration of the dialog and filling the listbox
In addition to some properties of the dialog, the ListBox
is configured here, and the window is assembled. Much of this is automatically generated, only the content of the listbox is manually written.
The elements are entered into the list, taking into account the previously determined evaluation type. If an adapter was passed, the string list is obtained from it. The selection is set to the passed start index, if it is valid. The buttons are configured and the size of the dialog is adjusted.
Me.Text = "Make a selection" Me.Width = 320 Me.Height = 300 Me.StartPosition = FormStartPosition.CenterScreen If Me.Adapter IsNot Nothing Then ListBox1.Items.AddRange(Me.Adapter.GetStringArray(Me.Items, count)) Else For i As Integer = 0 To count - 1 Select Case evaluation Case EvaluationType.EvaluationString ListBox1.Items.Add(CType(items(i), String)) Case EvaluationType.EvaluationDisplayName ListBox1.Items.Add(items(i).DisplayName) Case EvaluationType.EvaluationName ListBox1.Items.Add(items(i).Name) Case EvaluationType.EvaluationToString ListBox1.Items.Add(items(i).ToString()) Case Else ListBox1.Items.Add("no suitable evaluation type found") End Select Next End If
Methods for adjusting position and size
The two methods SetPos
and AdjustWindowHeight
are used to set the dialog to the desired position and adjust the height of the dialog. The position is set relative to the Inventor window, the height is adjusted so that the list does not become too long and the buttons do not stand too far down. The height of the dialog is set to the height of the buttons, the list and some distances. Here, a lot of tinkering was necessary because some often used methods and properties (e.g. ClientSize
) for the window layout use classes from System.Drawing.Primitives
that are apparently not available in Inventor. Since I was not able to import the required classes, I rewrote the methods to work without these classes. If anyone can tell me how to use these classes (e.g. Rectangle
or Point
), please let me know.
Public Sub SetPos(left As Integer, top As Integer) Me.StartPosition = FormStartPosition.Manual Me.Left = left Me.Top = top End Sub Public Sub AdjustWindowHeight(Optional maxHeight As Integer = 1000) Dim screenHeight As Integer = maxHeight Dim maxListBoxHeight As Integer = screenHeight \ 2 Dim listBoxHeight As Integer = Math.Min(ListBox1.ItemHeight * ListBox1.Items.Count + 30, maxListBoxHeight) ListBox1.Height = listBoxHeight Dim buttonSpacing As Integer = 10 Me.Height = ButtonOK.Height + 2 * buttonSpacing + ListBox1.Height + 50 End Sub
Event handler for the listbox and the buttons
The event handlers determine what should happen when the user interacts. When an element in the list is selected, the index and the element are saved and the OK button is activated. When the OK button is pressed, the dialog is closed and DialogResult is set to OK. When the cancel button is pressed, DialogResult is set to Cancel and the dialog is closed.
In addition, an enumeration EvaluationType
is defined, which allows the evaluation type to be easily set. Then the class ends.
Private Sub ListBox_SelectedIndexChanged(sender As Object, e As EventArgs) SelectedIndex = ListBox1.SelectedIndex SelectedItem = CType(Items(SelectedIndex),T) ButtonOK.Enabled = (SelectedIndex >= 0) End Sub Private Sub ButtonOK_Click(sender As Object, e As EventArgs) Me.DialogResult = DialogResult.OK Me.Close() End Sub Private Sub ButtonCancel_Click(sender As Object, e As EventArgs) Me.DialogResult = DialogResult.Cancel Me.SelectedIndex = -1 Me.Close() End Sub Private Enum EvaluationType NoEvaluation EvaluationString EvaluationToString EvaluationDisplayName EvaluationName End Enum End Class
The adapter
In case the objects in the list do not have the desired properties, an adapter can be passed that reads the desired properties. The adapter must implement the IObjectAdapter
interface and contain the GetStringArray
method, which receives the objects in the list and the number of elements and returns a string array. Here is a simple example of an adapter that reads the DisplayName
property from an Asset
, just like the SelectionDialog
class itself. Here you can write your own adapter for each object type that reads the desired properties, if you do not want to change the SelectionDialog
for a single case.
Public Interface IObjectAdapter Function GetStringArray(items As IEnumerable, count As Integer) As String() End Interface Class AssetAdapter Implements IObjectAdapter Public Function GetStringArray(items As IEnumerable, count As Integer) As String() Implements IObjectAdapter.GetStringArray Dim names(count - 1) As String For i As Integer = 0 To count - 1 names(i) = CType(items(i), Asset).DisplayName Next Return names End Function End Class
The complete code
To use the SelectionDialog
, you have to copy the imports, the SelectionDialog
class and, if you want to use the adapter, also the IObjectAdapter
interface and the adapter class you created into the iLogic rule. If the adapter is not needed at all, the argument in the constructor and the lines of code that use the adapter can be removed.
Here is the complete code that allows the selection of a material from the second material library.
Imports System.Windows.Forms Imports System.Reflection Sub Main() Dim invApp As Inventor.Application = ThisApplication Dim width As Integer = invApp.Width Dim height As Integer = invApp.Height Dim left As Integer = invApp.Left Dim top As Integer = invApp.Top Dim center As Point2d = invApp.TransientGeometry.CreatePoint2d(left + width \ 2, top + height \ 2) Dim materialSelectionDialog As New SelectionDialog(Of Asset)(invApp.AssetLibraries(2).MaterialAssets) materialSelectionDialog.Text = "Select material" materialSelectionDialog.ButtonOK.Text = "Select" materialSelectionDialog.AdjustWindowHeight(height) materialSelectionDialog.SetPos(center.X - materialSelectionDialog.Width \ 2, center.Y - materialSelectionDialog.Height \ 2) Dim material As Asset = Nothing Dim response As DialogResult = materialSelectionDialog.ShowDialog() If response = DialogResult.OK Then material = materialSelectionDialog.SelectedItem End If If material IsNot Nothing Then Logger.Info(material.DisplayName & " selected.") Else Logger.Info("No material selected.") Exit Sub End If End Sub Public Class SelectionDialog(Of T) Inherits Form Private ReadOnly ListBox1 As New ListBox() Friend ButtonOK As New Button() Friend ButtonCancel As New Button() Public Property SelectedIndex As Integer Public Property SelectedItem As T Private Property Adapter As IObjectAdapter Private Property Items As IEnumerable Public Sub New(items As IEnumerable, Optional adapter As IObjectAdapter = Nothing, Optional startIndex As Integer = -1) Dim count As Integer = 0 Dim listTypeInfo As Type = items.GetType() Dim prop As System.Reflection.PropertyInfo prop = listTypeInfo.GetProperty("Length") If prop Is Nothing Then prop = listTypeInfo.GetProperty("Count") End If If prop IsNot Nothing Then count = prop.GetValue(items) Else Dim met As System.Reflection.MethodInfo met = listTypeInfo.GetMethod("Count") If met IsNot Nothing Then count = listTypeInfo.GetMethod("Count").Invoke(items, Nothing) count = met.Invoke(items, Nothing) Else For Each item As Object In items count += 1 Next End If End If Dim evaluation As EvaluationType = EvaluationType.NoEvaluation Me.Items = items 'If adapter is Nothing, try the standard evaluation If adapter Is Nothing Then Dim typeInfo As Type = Nothing If typeInfo Is Nothing Then typeInfo = GetType(T) End If If typeInfo Is GetType(String) Then evaluation = EvaluationType.EvaluationString ElseIf typeInfo.GetProperty("DisplayName") IsNot Nothing Or typeInfo.GetMethod("DisplayName") IsNot Nothing Then evaluation = EvaluationType.EvaluationDisplayName ElseIf typeInfo.GetProperty("Name") IsNot Nothing Then evaluation = EvaluationType.EvaluationName ElseIf typeInfo.GetMethod("ToString") IsNot Nothing Then evaluation = EvaluationType.EvaluationToString End If ' try to find the evaluation by trial and error If evaluation = EvaluationType.NoEvaluation Then Dim test As String Try test = Me.Items(0).DisplayName evaluation = EvaluationType.EvaluationDisplayName Catch Try test = Me.Items(0).Name evaluation = EvaluationType.EvaluationName Catch End Try End Try End If Else Me.Adapter = adapter End If ' Form settings Me.Text = "Make a selection" Me.Width = 320 Me.Height = 300 Me.StartPosition = FormStartPosition.CenterScreen ' Add ListBox ' If we have an adapter, we use it If Me.Adapter IsNot Nothing Then ListBox1.Items.AddRange(Me.Adapter.GetStringArray(Me.Items, count)) Else For i As Integer = 0 To count - 1 Select Case evaluation Case EvaluationType.EvaluationString ListBox1.Items.Add(CType(items(i), String)) Case EvaluationType.EvaluationDisplayName ListBox1.Items.Add(items(i).DisplayName) Case EvaluationType.EvaluationName ListBox1.Items.Add(items(i).Name) Case EvaluationType.EvaluationToString ListBox1.Items.Add(items(i).ToString()) Case Else ListBox1.Items.Add("no suitable evaluation type found") End Select Next End If ListBox1.Dock = DockStyle.Top AddHandler ListBox1.SelectedIndexChanged, AddressOf ListBox_SelectedIndexChanged Me.Controls.Add(ListBox1) ' Panel for buttons Dim panel As New FlowLayoutPanel() panel.FlowDirection = FlowDirection.RightToLeft panel.Dock = DockStyle.Bottom panel.Height = 40 ' OK button ButtonOK.Text = "OK" ButtonOK.Enabled = False AddHandler ButtonOK.Click, AddressOf ButtonOK_Click panel.Controls.Add(ButtonOK) If startIndex < count - 1 Then Me.SelectedIndex = startIndex Else Me.SelectedIndex = -1 End If If Me.SelectedIndex > -1 Then ListBox1.SelectedIndex = startIndex Me.SelectedItem = CType(Me.Items(startIndex),T) End If ' Cancel button ButtonCancel.Text = "Cancel" AddHandler ButtonCancel.Click, AddressOf ButtonCancel_Click panel.Controls.Add(ButtonCancel) Me.Controls.Add(panel) Me.MaximizeBox = False Me.MinimizeBox = False Me.AutoSize = True Me.AdjustWindowHeight() End Sub Public Sub SetPos(left As Integer, top As Integer) Me.StartPosition = FormStartPosition.Manual Me.Left = left Me.Top = top End Sub Public Sub AdjustWindowHeight(Optional maxHeight As Integer = 1000) ' Take over screen height Dim screenHeight As Integer = maxHeight ' Maximum height of the listBox (half screen height) Dim maxListBoxHeight As Integer = screenHeight \ 2 ' Calculation of listBox height based on entries Dim listBoxHeight As Integer = Math.Min(ListBox1.ItemHeight * ListBox1.Items.Count + 30, maxListBoxHeight) ' Set listBox height ListBox1.Height = listBoxHeight ' Adjust window height (including buffer for edges) Dim buttonSpacing As Integer = 10 Me.Height = ButtonOK.Height + 2 * buttonSpacing + ListBox1.Height + 50 End Sub Private Sub ListBox_SelectedIndexChanged(sender As Object, e As EventArgs) SelectedIndex = ListBox1.SelectedIndex SelectedItem = CType(Items(SelectedIndex), T) ButtonOK.Enabled = (SelectedIndex >= 0) End Sub Private Sub ButtonOK_Click(sender As Object, e As EventArgs) Me.DialogResult = DialogResult.OK Me.Close() End Sub Private Sub ButtonCancel_Click(sender As Object, e As EventArgs) Me.DialogResult = DialogResult.Cancel Me.SelectedIndex = -1 Me.Close() End Sub 'Enumerator of evaluation types Private Enum EvaluationType NoEvaluation EvaluationString EvaluationToString EvaluationDisplayName EvaluationName End Enum End Class Public Interface IObjectAdapter Function GetStringArray(items As IEnumerable, count As Integer) As String() End Interface Class AssetAdapter Implements IObjectAdapter Public Function GetStringArray(items As IEnumerable, count As Integer) As String() Implements IObjectAdapter.GetStringArray Dim names(count - 1) As String For i As Integer = 0 To count - 1 names(i) = CType(items(i), Asset).DisplayName Next Return names End Function End Class
If you need more in-depth advice on CAD methods, please click on Contact.
Click the links to copy to clipboard
This page: https://r-kon.eu/cad-auswahldialog.php
The video: https://youtu.be/y739jWsr_fg (Youtube) / https://dai.ly/k4ssNpTHZtJkEhCvKHW (DailyMotion)