How To ... Develop a Selected Objects Inspector using ListView ActiveX Control - Part Two

This second tutorial will demonstrate how to customize the Listview ActiveX Control defined in Part One and update the display automatically when the selection set changes.

Note:

ActiveX Controls have been deprecated by Microsoft in the latest versions of the Windows operating system in favor of the DotNet framework and its controls.

While MAXScript still supports ActiveX controls, these have to be installed and registered on the system to be accessible to MAXScript.

As a replacement of ActiveX controls, MAXScript supports DotNet controls in 3ds Max 9 and higher.

Please see the topic Converting ActiveX ListView Control to DotNet ListView Control

Related topics:

ActiveX Controls in MAXScript Rollouts

ListView ActiveX Control

Listview ActiveX functions

ListView ActiveX Control Example

General Event Callback Mechanism

Callbacks.removeScripts

NATURAL LANGUAGE

Expand the existing macroScript.

Define the rollout as a global variable to be accessible to outside code.

Set the background color of the Listview to a nice pale cyan color.

Enable the checkboxes in the first column of the Listview and use them to represent the renderable state of the selected objects.

Control the width of each column using a second value per column in the layout_def array.

Clear the items from the list before filling it up with new data

Register a callback to update the list (which is now visible as a user interface element of a globally accessible rollout) whenever the scene selection set changes.

Unregister the callback whenever the rollout is closed to avoid errors.

SCRIPT:

   macroScript SceneListView category: "HowTo"
   (
   global listview_rollout
   try(destroyDialog listview_rollout)catch()
   rollou tlistview_rollout "ListView Selected"
   (
   fn initListView lv =
   (
    lv.gridLines = true  
    lv.View = #lvwReport  
    lv.fullRowSelect = true 

    lv.backColor = color 225 215 210
    lv.checkboxes = true

    layout_def = #(#("On",28), #("Object Name",120), #("Object Class",80), #("Verts",45), #("Faces",45), #("Material",120))

    for i inlayout_def do
    (
     column = lv.ColumnHeaders.add()
     column.text = i[1]
    )

    LV_FIRST = 0x1000
    LV_SETCOLUMNWIDTH = (LV_FIRST + 30)
    for i = 0 to layout_def.count-1 do
     windows.sendMessage lv.hwnd LV_SETCOLUMNWIDTH i layout_def[1+i][2]
   ) 

   fn fillInSpreadSheet lv =
   (
    lv.ListItems.clear()
    for o in selection do
    (
     li = lv.ListItems.add()
     li.checked = o.Renderable
     sub_li = li.ListSubItems.add()
     sub_li.text = o.name
     sub_li = li.ListSubItems.add()
     sub_li.text = (classof o) as string
     sub_li = li.ListSubItems.add()
     sub_li.text =try((o.mesh.numverts) as string)catch("--")
     sub_li = li.ListSubItems.add()
     sub_li.text =try((o.mesh.numfaces) as string)catch("--")
     sub_li = li.ListSubItems.add()
     sub_li.text = (o.material) as string
    )
   ) 

   activeXControl lv_objects "MSComctlLib.ListViewCtrl" width:490 height:190 align:#center
   on listview_rollout open do
   (
   initListView lv_objects
   fillInSpreadSheet lv_objects
   ) 
   on listview_rollout close do callbacks.removeScripts #selectionSetChanged id:#SceneListView
   )
   createDialog listview_rollout500 200
   callbacks.addScript #selectionSetChanged "listview_rollout.fillInSpreadSheet listview_rollout.lv_objects" id:#SceneListView
   )

RESULT:

Step-By-Step

macroScript SceneListView category:"HowTo"
( global listview_rollout

In order to be able to access the rollout from outside of the macroScript scope (in our case a callback script), we have to define the rollout variable as global.

rollout listview_rollout "ListView Selected"
(
fn initListView lv =
(
lv.gridLines = true  
lv.View = #lvwReport  
lv.fullRowSelect = true 

lv.backColor = color 225 215 210 

We assign a new color to the backColor property of the Listview.

Note:

Microsoft color values are expected as BGR (Blue/Green/Red) and NOT RGB as 3ds Max and MAXScript provide them. In MAXScript, the above color would appear as pale pink. You can visualize these colors by using something like

 display ( bitmap 128 128 color:(color 225 215 210) )--pale pink
display ( bitmap 128 128 color:(color 215 210 225) )--pale blue
lv.checkboxes = true

We will also enable the built-in checkboxes of the Listview control and use them to represent the renderable node-level property.

layout_def = #(#("On",28), #("Object Name",120), #("Object Class",80), #("Verts",45), #("Faces",45), #("Material",120))

We will extend the existing version of the layout definition array. Instead of storing only the names of the colums, we will store sub-arrays containing both the name and the width of the column. In the future, you could store any additional per-column data like bold text Boolean flag, foreground color of the text etc.)

for i in layout_def do
(
column = lv.ColumnHeaders.add()
column.text = i[1]
)
LV_FIRST = 0x1000
LV_SETCOLUMNWIDTH = (LV_FIRST + 30)

We define two user variables which will contain the first column's address and the message id to be sent to the windows handle of the Listview control responsible for setting the column width.This is "esoteric knowledge" available from literature on general Microsoft Windows and ActiveX Controls programming. Just use it the way it is provided here.

for i = 0 to layout_def.count-1 do
windows.sendMessage lv.hwnd LV_SETCOLUMNWIDTH i layout_def[1+i][2]

Now we loop from 0 to the number of columns minus one. This is because the windows.sendMessage method expects the index of the column to be set as a 0-based index, but MAXScript array indices are 1-based.

The windows.sendMessage method expects the windows handle of the ActiveX control (lv.hwnd), the message to be sent, the index of the column to set the width and the width value (which is stored in the 1-based array at index (i+1), in the second item of the sub-array)

) 
fn fillInSpreadSheet lv =
( lv.ListItems.clear()

Before we fill in the data into the Listview, we should make sure the list is emptied first. This is because this time around, the list will be updated on the fly without closing and reopening the dialog!

for o in selection do
(
li = lv.ListItems.add() li.checked = o.Renderable

The first column will contain only the checkbox. We set the .checked property to the boolean value returned by the node-level .renderable property of the current object.

sub_li = li.ListSubItems.add()

The former first column with the name of the object is now the second column, so we have to create a sub list item for it.

sub_li.text = o.name

We set the .text property of the sub list item to the name of the object just like before.

sub_li = li.ListSubItems.add()
sub_li.text = (classof o) as string
sub_li = li.ListSubItems.add()
sub_li.text = try((o.mesh.numverts) as string)catch("--")
sub_li = li.ListSubItems.add()
sub_li.text = try((o.mesh.numfaces) as string)catch("--")
sub_li = li.ListSubItems.add()
sub_li.text = (o.material) as string
)
) 
activeXControl lv_objects "MSComctlLib.ListViewCtrl" width:490 height:190 align:#center
on listview_rollout open do
(
initListView lv_objects
fillInSpreadSheet lv_objects
) 

on listview_rollout close do callbacks.removeScripts #selectionSetChanged id:#SceneListView 

Callbacks.removeScripts

Whenever the user closes the dialog by either pressing the [X] button in the upper right corner of the titlebar or by calling the MacroScript again, we will have to make sure that the callback we will register to update the display when the selection set changes is removed. Otherwise, selecting objects in the scene would cause the callback to try to access an already closed rollout and throw an error!

)
try(destroyDialog listview_rollout)catch()
createDialoglistview_rollout 500 200 callbacks.addScript #selectionSetChanged "listview_rollout.fillInSpreadSheet listview_rollout.lv_objects" id:#SceneListView

Finally, we register the callback to update the Listview whenever the user changes the selection set in the scene.

The callbacks.addScript function tells MAXScript that we are registering a new callback.

#selectionSetChanged is the name of the notification message broadcast by 3ds Max whenever the selection changes.

The string contains the actual script executed when the callback is activated, it accesses the Listview as a property of the now global rollout definition and calls the fillInSpreadSheet function we defined.

The id:#SceneListView is a user-defined name to be able to affect only this special callback (for example in the callbacks.removeScripts call above) without affecting other callbacks defined by other developers or the shipping 3ds Max version itself.

)

Using the Script

After evaluating the script, the SceneListView ActionItem in the "HowTo" category defined by the first part of the tutorial will be updated.

Press the button/select the menu item or press the keyboard shortcut corresponding to the SceneListView script - the dialog with the listView ActiveX control should appear. If there are no objects selected, the list will be empty.

Now select some object and watch the list updating automatically.Change the selection set again and the list will update dynamically! Select an object, right-click, go to Properties... and uncheck the Renderable checkbox. Note that the checkbox in the Listview will be unchecked, too!

Where to go from here?

The next logical step would be to add an even handler to link the checkbox in the Listview bi-directionally to the .Renderable property, so changing the state in the Listview would affect the node property, too!

Other things to try include adding more column definitions to the layout array to show other object properties you might be interested in.