Articles by " LoneRobot"

No Swear Box Required

Aug 13, 2010 by     No Comments    Posted under: DotNet

chunky

 

Normally, colourful language should be left in the snooker hall, but when it comes to max, there’s nothing more potty-mouth inducing when a custom UI doesn’t sit with the rest of the application. In my previous post about Rig Studio (my IDE for generating selection dialogs) I talked about using the maxtoolbar renderer to give the UI a more cohesive look. The problem with this is that although the menus are now similar in colour to the 3dsMax menus, the maxtoolbar obviously inherits from the managedrendermode. This means, that in my visually styled version of windows, the toolbar buttons end up being rendered rounded. While this doesn’t look out of place in other programs, max has has used it’s clunky square thing to good effect for the last 20 years, so it’s not about to stop soon. Also, as much as the menu are now styled correctly, you still have to manually colour the background and text according to your UI scheme.

You can control the overall look and feel of a toolstrip by specifiying the renderer property. There are four types,

System

Professional

ManagedRenderMode

Custom

You can see the differences between the Professional and Managed renderers below.

Toolstrip-Renderer

The professional renderer is more like max’s main toolbar. Unfortunately, the managed services version seems always give the manager render mode ‘look’ to toolstrip items. This is because it is inherited from the SystemRenderer for the toolstrip.

Examining the Renderer’s Constructor

If you examine the Professional toolstrip rendererer constructor, you will find that it is overloaded. This means that there multiple options to call (like having the same function with different arguments) when you create a new instance of it.

tsr

You’ll see from the constructor that it has an option to pass a dotnetobject into the New() constructor. The New() constructor is what is run whenever you create a dotnetobject -so when you call dotnetobject from a maxscript, you are calling this constructor. 

You can see that one of the methods is asking for a “ProfessionalColorTable”. This is a class that controls how the colours are applied by the toolstrip renderer. So by specifiying a different ColorTable, you can alter the look of a toolstrip. Note that you can’t just create a ColorTable class and change the properties. It does have them, but it is a read only class. In order to create a new one, you have to inherit this class and override the paint methods that you want to alter.

The initial setup of this would be as follows (assume that you have created your toolstrip and this in in the on open handler) –

Pct = dotnetobject "System.Windows.Forms.ProfessionalColorTable"
TheUltimateToolStrip.Renderer = dotnetobject "ToolStripProfessionalRenderer"

As it stands, all you would have done here is to create a toolstrip the same as if you had set the renderer property to Professional.

So, by building an inherited colortable, wouldn’t it be better to automatically decide within the class what UI colours you are running and generate the toolbar from that?

In a previous post, I worked out a method for using maxscript calls to the colorman interface from the dotnetassembly to get the max UI colours. After this post, Yannick Puech contacted me and pointed me to the CUIUpdater class in managedservices. I knew max must have had something like this, but hadn’t figured it out until now. Thanks Yannick!

Using CUIUpdater

You can’t create an instance of CUIUpdater via dotnetobject. You have to call the getinstance function instead.

dotnetcolorman = (dotnetclass "Managedservices.cuiupdater").getinstance()
--dotNetObject:ManagedServices.CuiUpdater
showmethods dotnetcolorman
--   .GetButtonDarkShadow()
--   .GetButtonLightShadow()
--   .GetButtonPressedColor()
--   .GetColor aColorIndex
--   .GetControlColor()
--   .GetEditControlColor()
--   .[static]GetInstance()
--   .GetMaxColor id
--   .GetTextColor()

As you can see, there are some useful things in there to call from an assembly.

dotnetcolorman.GetControlColor()
-- dotNetObject:System.Drawing.Color
-- this is the colour of the interface
dotnetcolorman.GetTextColor()
-- dotNetObject:System.Drawing.Color
-- the is colour of the text (if you hadn’t already guessed)

Overiding the Inherited ColorTable class

Now we have this dotnetmethod to get the max UI scheme, we can build the ColorTable. But before we do that, we will need to inherit the toolstrip renderer too before we can use it. We are going to keep much of the look and feel of the professional renderer but  we do need to change a few base rendering options in order to make it sit pretty in max.

Take a look at the following class –

Public Class MaxToolStripRenderer
    Inherits ToolStripProfessionalRenderer

    Private MaxColors As ManagedServices.CuiUpdater = ManagedServices.CuiUpdater.GetInstance()

    Public Sub New()
        MyBase.New()
    End Sub

    Public Sub New(ByVal maxcolortable As ProfessionalColorTable)
        MyBase.New(maxcolortable)
    End Sub

    Protected Overrides Sub OnRenderToolStripBorder(ByVal e As System.Windows.Forms.ToolStripRenderEventArgs)
        'Do nothing
    End Sub

    Protected Overrides Sub OnRenderArrow(ByVal e As System.Windows.Forms.ToolStripArrowRenderEventArgs)
        e.ArrowColor = MaxColors.GetTextColor
        MyBase.OnRenderArrow(e)
    End Sub

    Protected Overrides Sub OnRenderItemText(ByVal e As ToolStripItemTextRenderEventArgs)
        e.TextColor = MaxColors.GetTextColor
        MyBase.OnRenderItemText(e)
    End Sub
End Class

You can see on the first line that we are inhertiing the Professional render class. Then we have a private variable that stores our instance of max’s CUI updater. We will use this to good effect. We also  have added the two constructors, allowing us to pass a proffesionalcolortable. Even if we inherit the professionalcolortable to build our own, since it is inherited from this base class, there wont be a casting error. This is evident when you examine the new constructor – we call the base class constructor from the inherited class when specifying the colortable. This is the only way of specifying a colortable with a toolstrip, as (if you remember) the colortable property is read-only.

The final three subs override the toolstrip renderer so that we can customise the general look of the toolstrip. OnRenderToolstripBorder was covered in the previous post, which just removes the unsightly border.

OnRenderArrow and OnRenderItemText both use the CUIUpdater to retrieve the max text colour and draw the objects correctly. The arrow is for when there is a sub menu on a dropdown menu, and the itemtext covers all text on the toolstrip. You can of course change individual items to anything you want, but this is just to control the initial styling.

Now, on to the color table – Watch out, this is a biggee..

Public Class MaxUIProfessionalColorTable
    Inherits ProfessionalColorTable
    Private MaxColors As ManagedServices.CuiUpdater = ManagedServices.CuiUpdater.GetInstance()

    Public Overrides ReadOnly Property ButtonCheckedGradientBegin() As System.Drawing.Color
        Get
            Return MaxColors.GetControlColor
        End Get
    End Property

    Public Overrides ReadOnly Property ButtonCheckedGradientEnd() As System.Drawing.Color
        Get
            Return MaxColors.GetControlColor
        End Get
    End Property

    Public Overrides ReadOnly Property ButtonCheckedGradientMiddle() As System.Drawing.Color
        Get
            Return MaxColors.GetControlColor
        End Get
    End Property

    Public Overrides ReadOnly Property ButtonPressedGradientBegin() As System.Drawing.Color
        Get
            Return MaxColors.GetButtonPressedColor
        End Get
    End Property

    Public Overrides ReadOnly Property ButtonPressedGradientEnd() As System.Drawing.Color
        Get
            Return MaxColors.GetButtonPressedColor
        End Get
    End Property

    Public Overrides ReadOnly Property ButtonPressedGradientMiddle() As System.Drawing.Color
        Get
            Return MaxColors.GetButtonPressedColor
        End Get
    End Property

    Public Overrides ReadOnly Property ButtonSelectedBorder() As System.Drawing.Color
        Get
            Return (MaxColors.GetButtonPressedColor())
        End Get
    End Property

    Public Overrides ReadOnly Property ButtonSelectedGradientBegin() As System.Drawing.Color
        Get
            Return MaxColors.GetButtonPressedColor
        End Get
    End Property

    Public Overrides ReadOnly Property ButtonSelectedGradientEnd() As System.Drawing.Color
        Get
            Return MaxColors.GetButtonPressedColor
        End Get
    End Property

    Public Overrides ReadOnly Property ButtonSelectedGradientMiddle() As System.Drawing.Color
        Get
            Return MaxColors.GetButtonPressedColor
        End Get
    End Property

    Public Overrides ReadOnly Property CheckBackground() As System.Drawing.Color
        Get
            Return MaxColors.GetControlColor
        End Get
    End Property

    Public Overrides ReadOnly Property GripDark() As System.Drawing.Color
        Get
            Return MaxColors.GetControlColor
        End Get
    End Property

    Public Overrides ReadOnly Property GripLight() As System.Drawing.Color
        Get
            Return MaxColors.GetTextColor
        End Get
    End Property

    Public Overrides ReadOnly Property ImageMarginGradientBegin() As System.Drawing.Color
        Get
            Return MaxColors.GetControlColor
        End Get
    End Property

    Public Overrides ReadOnly Property ImageMarginGradientEnd() As System.Drawing.Color
        Get
            Return MaxColors.GetControlColor
        End Get
    End Property

    Public Overrides ReadOnly Property ImageMarginGradientMiddle() As System.Drawing.Color
        Get
            Return MaxColors.GetControlColor
        End Get
    End Property

    Public Overrides ReadOnly Property ImageMarginRevealedGradientBegin() As System.Drawing.Color
        Get
            Return MaxColors.GetControlColor
        End Get
    End Property

    Public Overrides ReadOnly Property ImageMarginRevealedGradientEnd() As System.Drawing.Color
        Get
            Return MaxColors.GetControlColor
        End Get
    End Property

    Public Overrides ReadOnly Property ImageMarginRevealedGradientMiddle() As System.Drawing.Color
        Get
            Return MaxColors.GetControlColor
        End Get
    End Property

    Public Overrides ReadOnly Property MenuBorder() As System.Drawing.Color
        Get
            Return MaxColors.GetControlColor
        End Get
    End Property

    Public Overrides ReadOnly Property MenuItemBorder() As System.Drawing.Color
        Get
            Return Color.FromArgb(174, 207, 247)
        End Get
    End Property

    Public Overrides ReadOnly Property MenuItemPressedGradientBegin() As System.Drawing.Color
        Get
            Return MaxColors.GetButtonPressedColor
        End Get
    End Property

    Public Overrides ReadOnly Property MenuItemPressedGradientEnd() As System.Drawing.Color
        Get
            Return MaxColors.GetButtonPressedColor
        End Get
    End Property

    Public Overrides ReadOnly Property MenuItemPressedGradientMiddle() As System.Drawing.Color
        Get
            Return MaxColors.GetButtonPressedColor
        End Get
    End Property

    Public Overrides ReadOnly Property MenuItemSelected() As System.Drawing.Color
        Get
            Return MaxColors.GetButtonPressedColor
        End Get
    End Property

    Public Overrides ReadOnly Property MenuItemSelectedGradientBegin() As System.Drawing.Color
        Get
            Return MaxColors.GetButtonPressedColor
        End Get
    End Property

    Public Overrides ReadOnly Property MenuItemSelectedGradientEnd() As System.Drawing.Color
        Get
            Return MaxColors.GetButtonPressedColor
        End Get
    End Property

    Public Overrides ReadOnly Property MenuStripGradientBegin() As System.Drawing.Color
        Get
            Return MaxColors.GetControlColor
        End Get
    End Property

    Public Overrides ReadOnly Property MenuStripGradientEnd() As System.Drawing.Color
        Get
            Return MaxColors.GetControlColor
        End Get
    End Property

    Public Overrides ReadOnly Property OverflowButtonGradientBegin() As System.Drawing.Color
        Get
            Return MaxColors.GetTextColor
        End Get
    End Property

    Public Overrides ReadOnly Property OverflowButtonGradientEnd() As System.Drawing.Color
        Get
            Return MaxColors.GetTextColor
        End Get
    End Property

    Public Overrides ReadOnly Property OverflowButtonGradientMiddle() As System.Drawing.Color
        Get
            Return MaxColors.GetTextColor
        End Get
    End Property

    Public Overrides ReadOnly Property RaftingContainerGradientBegin() As System.Drawing.Color
        Get
            Return MaxColors.GetControlColor
        End Get
    End Property

    Public Overrides ReadOnly Property RaftingContainerGradientEnd() As System.Drawing.Color
        Get
            Return MaxColors.GetControlColor
        End Get
    End Property

    Public Overrides ReadOnly Property SeparatorDark() As System.Drawing.Color
        Get
            Return MaxColors.GetTextColor
        End Get
    End Property

    Public Overrides ReadOnly Property SeparatorLight() As System.Drawing.Color
        Get
            Return MaxColors.GetControlColor
        End Get
    End Property

    Public Overrides ReadOnly Property ToolStripBorder() As System.Drawing.Color
        Get
            Return MaxColors.GetTextColor
        End Get
    End Property


    Public Overrides ReadOnly Property ToolStripGradientBegin() As System.Drawing.Color
        Get
            Return MaxColors.GetControlColor
        End Get
    End Property

    Public Overrides ReadOnly Property ToolStripGradientEnd() As System.Drawing.Color
        Get
            Return MaxColors.GetControlColor
        End Get
    End Property

    Public Overrides ReadOnly Property ToolStripGradientMiddle() As System.Drawing.Color
        Get
            Return MaxColors.GetControlColor
        End Get
    End Property

    Public Overrides ReadOnly Property ToolStripContentPanelGradientBegin() As System.Drawing.Color
        Get
            Return MaxColors.GetControlColor
        End Get
    End Property

    Public Overrides ReadOnly Property ToolStripContentPanelGradientEnd() As System.Drawing.Color
        Get
            Return MaxColors.GetControlColor
        End Get

    End Property

    Public Overrides ReadOnly Property ToolStripDropDownBackground() As System.Drawing.Color
        Get
            Return MaxColors.GetControlColor
        End Get
    End Property

    Public Overrides ReadOnly Property StatusStripGradientBegin() As System.Drawing.Color
        Get
            Return MaxColors.GetControlColor
        End Get
    End Property

    Public Overrides ReadOnly Property StatusStripGradientEnd() As System.Drawing.Color
        Get
            Return MaxColors.GetControlColor
        End Get
    End Property

    Public Overrides ReadOnly Property ToolStripPanelGradientBegin() As System.Drawing.Color
        Get
            Return MaxColors.GetControlColor
        End Get
    End Property
    Public Overrides ReadOnly Property ToolStripPanelGradientEnd() As System.Drawing.Color
        Get
            Return MaxColors.GetControlColor
        End Get
    End Property

End Class

There are around 50 odd properties that you can override in this class, and you could probably get away with a much smaller implementation. This mainly makes sure that the menus are shaded correctly. I have gone for a flat style toolstrip with this colortable, It wasn’t important to me to have menus exactly like the 3dsmax main menus, just enough so that it looked swish and part of the package.

Compiling this into a new dll and using the method here –

Yes, not exactly interesting i know but the toolstrip has correctly identified the MaxUI scheme and looks rather swish.

If you noticed my class definition above, you can see that is to be used in Rig Studio. The great thing is that because of the colortable, the application looks right regardless of the scheme. Check the video, I run the app twice in the same session on max, changing the ui color inbetween.

 

 

With a little customisation, it is pretty easy to make a colortable that you can use that looks identical to the 3dsMax menu bar – this has a few more colours because there is a gradient effect over much of it.

I hope this has been useful, as much as it feels like it is a pain to have to go to extra lengths to set this up, I always feel it is better to handle this in the assembly rather than max, as it avoids any redraw issues between when the constructor is called and the inevitable flash on load. It means that your maxscript code is reduced.

I haven’t compiled this into a separate assembly as I use it primarily in Rig Studio, but if you need it, just let me know.


					

That’s Shallot

Aug 11, 2010 by     2 Comments    Posted under: DotNet

If you’ve got lots of different layers in the layer manager, its fine when you are in the actual file, but when you start having sets, characters and props in the same scene, it is sometimes difficult to navigate the interface. This is another snippet with a macro for adding a prefix to selected layers to group them so they are together when sorted alphabetically. Think of it like a sh*te version of The Onion, maybe I should call it The Shallot.

 Layer unordered

prefix dialog

 macroScript AddLayerPrefix
  category:"LoneRobot"
  toolTip:"Add Prefix to Selected Layers"
  buttontext:"LayerPrefix"

(
	local LR_LayerPrefixRO
	local clsColor = DotNetClass "System.Drawing.Color"
	local clsBorder =dotNetClass "System.Windows.Forms.BorderStyle"
	
	fn initdotnetcontrols =
	(	
	LR_LayerPrefixRO.layerlbl.text = "Select layers"	
	LR_LayerPrefixRO.layerlbl.forecolor = clsColor.GhostWhite
	LR_LayerPrefixRO.layerlbl.backcolor = clsColor.FromARGB 51 83 102
	LR_LayerPrefixRO.layerlbl.borderstyle =	clsborder.fixedsingle 
	)
	
	fn RefreshLayerlist =
	(
	LayersStringArray=#()
	LayerListInterfaces = #()
	NumberofLayers = LayerManager.count	
	LayerListInterfaces = for i = 1 to numberoflayers collect LayerManager.getLayer i
	
	for i in LayerListInterfaces do
		(
		if i != undefined then 
			(
			append LayersStringArray i.name
			)
		)
		
	LR_LayerPrefixRO.lbxLayers.items = LayersStringArray
	)
	
	fn AddLayerPrefix StrName =
	(
			local SelectedLayers = LR_LayerPrefixRO.lbxLayers.selection
			if true then 
			(	
				for i in selectedlayers do
					(
					layer = layermanager.getLayer i
					local Layername = layer.name
					Layer.setName (StrName + " " + Layername)
					)			
			RefreshLayerlist()
			)
	)
	
	fn RemoveLayerPrefix =
	(
			local SelectedLayers = LR_LayerPrefixRO.lbxLayers.selection
		
		--	if querybox "Are you sure you want to remove the prefix of the selected layers?" title:"Layer Prefix"  then 
			(	
				for i in selectedlayers do
					(
					layer = layermanager.getLayer i
					local Layername = dotnetobject "system.string" layer.name							
					StrExtentInt = Layername.LastIndexOf "}"
						
					if StrExtentInt > -1 then
						(
						Local TrimmedStr = Layername.Remove 0 (StrExtentInt+1)		
						layer.setName (trimLeft TrimmedStr) 
						)
				
					)
			)			
			RefreshLayerlist()			
	)
	
	fn isLayerNameValid strName =
	(
		strName = trimleft strName 
		strName = trimright strName 
		--assumes that the layer name is valid, flag to false if not
		local boolIsLayerNameValid = true
		
		--checks for an empty layer name:
		if (strName == "") do boolIsLayerNameValid = false
		
		--checks for existing name in the layers list:
		if (layermanager.getlayerfromname strName) != undefined do boolIsLayerNameValid = false
		--returns the flag
		boolIsLayerNameValid
	
	)

	rollout vRollout "Enter New Layer Prefix" width:205 height:55
	(
		edittext vEdittext "Prefix" pos:[6,4] width:195 height:17 --LOC_NOTES: localize this
		button vButtonOK "OK" pos:[37,27] width:65 height:21 --LOC_NOTES: localize this
		button vButtonCancel "Cancel" pos:[134,27] width:65 height:21 --LOC_NOTES: localize this

		on vRollout open do
		(	
		setfocus vEdittext
		)
		on vEdittext entered arg do
		(
			setfocus vButtonOK 
		)--end vEdittext entered arg
		on vEdittext changed arg do
		(
			vNameLayer = arg			
			if (isLayerNameValid vNameLayer)  --prevent layers with empty names or leading spaces from being created: pfbreton 13 aout 2003
			then vButtonOK.enabled = true
			else vButtonOK.enabled = false
		)
		on vButtonOK pressed do
		(
			AddLayerPrefix  ("{"+vEdittext.text+"}")			
			destroyDialog vRollout 
		)
		on vButtonCancel pressed do
		(
			destroyDialog vRollout
		)
	)
	

rollout LR_LayerPrefixRO "Add Layer Prefix" width:365 height:511
(
	multiListBox lbxLayers "" pos:[-1,32] width:370 height:34
	dotNetControl layerlbl "label" pos:[4,2] width:335 height:30
	button AddSetPrefix "{SET} Prefix" pos:[113,480] width:77 height:26 border:false
	button btnrefresh "R" pos:[341,2] width:20 height:30 border:false tooltip:"Refresh List"
	button AddPropPrefix "{PROP} Prefix" pos:[191,480] width:84 height:26 border:false
	button btnRemovePrefix "Remove Prefix" pos:[275,480] width:86 height:26 border:false
	button AddPrefix "Add Prefix" pos:[4,480] width:105 height:26 border:false
		
	on LR_LayerPrefixRO open do
	(
		initdotnetcontrols()		
		RefreshLayerlist()	
	)
	on AddSetPrefix pressed do
		AddLayerPrefix "{SET}"
	on btnrefresh pressed do
		RefreshLayerlist()
	on AddPropPrefix pressed do
		AddLayerPrefix "{PROP}"
	on btnRemovePrefix pressed do
		RemoveLayerPrefix()
	on AddPrefix pressed do
	(
		createdialog vRollout
	)
)


createdialog LR_LayerPrefixRO  style:#(#style_toolwindow, #style_sysmenu)


)

prefixed layer manager

Get Shirty

Aug 11, 2010 by     No Comments    Posted under: DotNet

A while back there was a competition on the wonderful tech site http://tech-artists.org. Rob Galankis, Webmaster of the geek portal wanted a few designs to be made into t-shirts to give out at GDC. To cut a long story short, such is the 3D community’s affection for the humble teapot, that Piers Morgan could stick one on his head and suddenly become the nation’s sweetheart. Anyhoo, my geek inspired Teapot-shirt won and was made into a stylish piece of menswear.

tshirt logo

If you fancied owning one, head over to tech-artists.org and follow the links to the t-shirts. There are some other great designs from the users of the site, not just mine.

http://techart.spreadshirt.com/

They are reasonably priced and will make you the envy of everyone, except girls.

Just watch if you’re in the UK that you don’t get hammered by customs, like I did. ack!

Oddly enough, since I’m a fan of t-shirts generally, I had thought about making some LoneRobot inspired t-shirts a while back. If you think this is a good idea and you’d like to be seen in a bit of robot sartorial elegance, then please let me know through the contact at the top of the page or this post. If enough people express an interest, perhaps I can get something set up.

The camera sometimes lies

Aug 11, 2010 by     1 Comment     Posted under: DotNet

Just a snippet posting this time, sharing a small file callback to lock and unlock a camera’s position. If you need to make sure that a camera in a file doesn’t get moved when it is handled by a different artist, this could be useful. Locking a camera is simple, all you need to do is set the transform lock flags on, and (Thanks to Jason Labbe for this suggestion on CGTalk)  lock the FOV of the camera with a script controller.

Added to a file open callback, you can lock all cameras when a file is opened. I have deployed this on the fix team’s computers as a callback, that way nothing gets moved after animation and the subsequent edit. There are a couple of macros to lock and unlock too, should this need to be altered at any time, but the main idea is that it avoids accidental ‘nudging’ of camera positions between file versions which can be easily done.

Unlocked

cams unlocked

Locked!

cams locked

The callback just sits in a file the script startup directory –

callbacks.removeScripts id:#CamLock   
callbacks.addScript #filePostOpen "macros.run "LoneRobot" "LockAllCameras"" id:#CamLock

And the macro is as follows –

macroScript LockAllCameras
category:"LoneRobot"
toolTip:"Lock all Scene Cameras"
buttontext:"Lock Cams"
(
	if cameras.count > 0 then
	(
	For cam in cameras do
		(
		setTransformLockFlags cam #all
		cam.wirecolor = orange
			if classof cam == TargetCamera then
			(
			LockScript = Float_Script()
			cam.fov.controller = lockscript
			)
		)
	-- check that it isn't the target - checking fot the type property is a good enough way as any
	Format "Camera lock applied to % cameras - %n" (for c in cameras where hasProperty c "type" collect c).count maxFileName
	)
)

macroScript UnlockAllCameras
category:"LoneRobot"
toolTip:"Unlock all Scene Cameras"
buttontext:"Unlock Cams"
(
	if cameras.count > 0 then
	(
		For cam in cameras do
		(
		setTransformLockFlags cam #none
		cam.wirecolor = (color 87 120 204)
			if classof cam == TargetCamera then
			(
			UnlockScript = Bezier_Float()
			cam.fov.controller = Unlockscript
			)
		)
	Format "% cameras unlocked - %n" (for c in cameras where hasProperty c "type" collect c).count maxFileName
	)
)

if someone else asks “Why did you choose orange?”, I’m going on a rampage.

Stuff the revolution, I’m thinking about a Character Selection Framework

Jun 1, 2010 by     10 Comments    Posted under: 3dsMax, Characters, DotNet, Rigging, Technical Research, User Controls

 

One of the most time consuming things always seems to be building pesky UI’s in Maxscript. The ever reliable Visual Maxscript editor, whilst having the reputation of Marmite, has been compromised by a spectacular bug in the last few releases that prints the last few characters of the previous line onto the next one, thus scrambling your code.

rollout MXSMashup "Untitled" width:169 height:171
     (
         button btn1 "Button" pos:[7,8] width:70 height:39
     39
         bitmap bmp1 "Bitmap" pos:[97,25] width:52 height:84
    84
        checkbutton ckb1 "CheckButton" pos:[28,138] width:132 height:20
    20
        combobox cbx1 "ComboBox" pos:[16,91] width:55 height:1
    1
        edittext edt1 "" pos:[14,60] width:55 height:20
    20
    )

All this means you end up manually  tweaking positions and sizes till the end of time, for a script  that is essentially doing something very simple. It’s always faster to have a small dialog with a picture of the rig with fast access to individual bones to remove the necessity of picking them in the viewport. However, generating these things up can usually take more time than we have to give in production. I thought it would be an interesting focus to my research to see how easy it would be to implement a system that allowed even a non-scripter to build specialised dialogs for selection, ultimately a task that you end up doing a billion times with a rig.

The IDE driven usercontrol approach

Now Visual Studio has a pretty nifty IDE, and you can layout windows forms pretty darned fast with it. It would be rather handy to have something similar but centred around 3dsMax, albeit with a basic layout toolset. The obvious approach (in terms of linking the dotnet framework to 3dsMax objects) was binding controls to the names of scene nodes. Okay, maybe scene node names might not be the most robust of solutions but it is fine for this sort of thing – but if someone starts renaming rigs just for fun, you’re going to be screwed on a bigger level than just the selection tools.

Look at this lot –

all dialogs

With Rig Studio, you can do this without any coding in no time at all. Here’s how –

A Runtime Design Surface

Have a look at the video below to see an example of how RigStudio sets up a basic control dialog and then stores it to be read by a user end interface.

You build up an interface by drag and drop, and there are preset sizes and shapes defined for speed. You can size these up to whatever you like. There are options to set the forecolor and background color, and to position text blocks to illustrate the different areas of the dialog.

You might recognise the control on the right of the UI – It’s a propertygrid and is a great little control for exactly this type of thing, as it takes a dotnetobject and displays an area so that you can adjust the properties easily. You can use this out of the box, but I have customised it to only display the properties that you need to adjust. I have also written a custom UITypeEditor class to handle the control list drop down method for choosing hierarchies.

TypeEditor

This is not a default behaviour of the propertygrid control. the Child property on a rigcontrol object is a string denoting the name of the next node in the hierarchy. In order to give better design-time support, I am asking the UITypeEditor  to take an array of controls from the design surface and build a sorted listbox from the results. It needs to be sorted as the controls could have been created in any order.

Custom Toolstrip Renderers

Alignment-Options_Dotnet Alignment-Options

Keen-eyed UI aficionados will notice the similarity of the drop down menu on the left to 3ds Max’s menu system, and not the default dotnet toolstrip renderer (which is on the right). Its a subtle difference, but this is due to the use of a custom toolstrip renderer. Autodesk has provided this as part of the MaxCustomControls namespace.

MaxToolStripSystemRenderer is a custom renderer and is assigned by simply giving an instance of it to the renderer property. So you can implement this into Maxscript pretty easilly too, if you want everything to look like it is part of the same application.

-- VB
Me.CtrlMenu.Renderer = New MaxCustomControls.MaxToolStripSystemRenderer()
-- MXS
CtrlMenu.Renderer = dotnetobject "MaxCustomControls.MaxToolStripSystemRenderer"

What might annoy you about the toolstrip, especially if you run the UI from the dark side of the force, is the border that windows paints around the toolstrip menu. This is just plain annoying if you are trying to keep a nice minimal looking UI.

toolstrip

It’s probably not so noticeable on a light UI scheme, but it would be great to get rid of it. With some poking around on some VB forums, I found this solution. There is a protected method called OnRenderToolstripBorder. All that you need to do is to override this and tell it to do nothing, as you don’t want the border. The great thing about class inheritance is that we can inherit the custom renderer class made by Autodesk, implement the functionality we need, and have a new class to use. By only overriding the methods that we want to change, we keep all the other existing render bits. It is actually a pretty short entry to have a version of the Autodesk Toolstrip renderer without the white border line –

Public Class BorderlessMaxToolStripRenderer
Inherits MaxCustomControls.MaxToolStripSystemRenderer
	Protected Overrides Sub OnRenderToolStripBorder _
				(ByVal e As System.Windows.Forms.ToolStripRenderEventArgs)
 	'Do nothing
 	End Sub
End Class

So now you can have the great functionality of the toolstrip without having an incongruous looking UI. It would be pretty easy to dynamically compile this class as part of a maxscript, so that you dont have to distribute an assembly too.

Toolstrip-CustomRenderer

Weirdly, you don’t get this border with a menustrip, which is fine for most things, but it’s not got the same choice of controls. Its a shame i didnt realise this until I had gone down this route!

Design-TIme Alignment Functions

Controls have a left, right, top and bottom property which returns their position relating to their parent container. This is a local position and unrelated to screen position. This makes the majority of options in the alignment menu relatively easy. Where it becomes more complicated is control spacing. Controls can be added in any order, so you need a method of deciding where a control is in dialog position terms. If you are performing a spacing arrangement, you need a method that can take an array of controls and return them sorted into order by their location property. As you can space vertically or horizionally, you’ll need a way of tells the method the direction that you want to sort them. It is precisiely this sort of logic that the Icomparer can handle, sorting abstract classes according to  defined properties.

Fortunately, the dotNet framework has some powerful sorting method called Icomparer for doing this job.

Public Enum AlignmentCompareOptions
    Vertical
    Horizontal
End Enum

Friend Class PointComparer
    Implements IComparer(Of Drawing.Point)
    Dim _CompareDirection As AlignmentCompareOptions

    Public Sub New(ByVal Direction As AlignmentCompareOptions)
        _CompareDirection = Direction
    End Sub
    Public Function Compare(ByVal x As Drawing.Point, ByVal y As Drawing.Point) As Integer Implements IComparer(Of System.Drawing.Point).Compare
        Dim pointX As Point = DirectCast(x, Point)
        Dim pointY As Point = DirectCast(y, Point)

        Select Case _CompareDirection
            Case AlignmentCompareOptions.Vertical
                Select Case True
                    Case pointX.Y > pointY.Y
                        Return 1
                    Case pointX.Y < pointY.Y
                        Return -1
                    Case pointX.Y = pointY.Y
                        Return 0
                End Select
            Case AlignmentCompareOptions.Horizontal
                Select Case True
                    Case pointX.X > pointY.X
                        Return 1
                    Case pointX.X < pointY.X
                        Return -1
                    Case pointX.X = pointY.X
                        Return 0
                End Select
        End Select

    End Function

End Class

In order to use this class, you add the controls to a SortedDictionary, one of the classes that you can supply an Icomparer to the constructor. You’ll see this SortedDictionary takes a point value, and a control value. As these are added the Sorted dictionary automatically sorts them into the order they are according to the direction (vertical or horizontal)

Dim CompareControlList As New SortedDictionary(Of Point, Control)(New PointComparer(Direction))

The Icomparer sorts it by returning an integer that relates to the evaluated expression within the IComparer, 1 if pointA is greater than pointB, –1 if B is greater than A, and 0 if they are the same.

The conclusion to all this is that you can use the results to calculate the difference between the first and last control instances, deduct the combined height or width of the controls and divide by the number of gaps to get the distance needed to space the controls evenly.

XML Control Serialization

This was a new thing for me to get my head around, and in doing so I genuinely believe I have only scratched the surface on the sort of things that you can do. In my case, XML serialization provided an elegant solution to this problem –

XMLfile.ChildNodes.ItemOf(i).ChildNodes.ItemOf(i).ChildNodes.ItemOf(i).InnerText

Sometimes, negotiating an XML tree manually becomes difficult to track, but in this case, you explicitly know what type of information you are dealing with. XML serialization allows you to deconstruct dotnetobjects, with the idea that you can use the serializer’s logic to re-assemble them at another time, perhaps even to send data to a compliant application on a different computer.

To use the example of, say a bog-standard button, we could serialize this but we have to address a couple of issues first. XML needs a specific data type to store the information. You couldn’t just pass it an enum style like “borderstyle.fixedsingle” as a string and expect it to know what to do with it. Also, there are a sh*t load of properties that we have on a button, many of which don’t particularly represent anything to do with the visual state. You would be serializing a lot of useless information into the XML file, making the file larger than it needs to be.

Writing a Custom Serialization class

I ended up by writing a class specifically to handle the serialization of the design surface to XML. It actually consists of three classes, the RigControlSerializer is where the action happens. It stores all the dialog information necessary to recreate the size and shape of the stored data. RigControl and RigControlText are subclasses that allow the serializer to loop through the controls on the design surface and store them into an array list. So the serializer property controlist actually has an array of the RigControl class, not the actual control itself, but is perfect as it is all the information that the deserializer needs to recreate the control faithfully on another surface.

Custom Serializer Classes

The great thing about writing a serializer class is that each property becomes a branch in the xml file automatically,  and subsequent paths are made as each property is serialized in order. So an entire dialog can be written out to XML as follows –

Private Function SerializeDesignSurfacetoXML(ByVal XMLFilename As String) As Boolean

        Try
            Dim SavedDialog As RigControlSerializer = New RigControlSerializer()

            With SavedDialog
                .DialogSize = DesignSurface.Size
                .DialogBackcolor = (CType(DesignSurface.BackColor, Color)).ToArgb
                .ControlList = CreateRigControlCopyArray()
                .TextLabelList = CreateTextLabelCopyArray()
                .BackgroundImage = CType(Me.DesignSurface.BackgroundImage, System.Drawing.Bitmap)
                .CharacterName = Me.CharacterName
            End With

            Dim writer As New XmlSerializer(GetType(RigControlSerializer))
            Dim file As New StreamWriter(XMLFilename)
            writer.Serialize(file, SavedDialog)
            file.Close()
            writer = Nothing
            Return True

        Catch ex As Exception
            Return False
            MessageBox.Show(ex.InnerException.ToString)
        End Try
    End Function

That, for me is a pretty straightforward way of converting a whole bunch of dotnet objects into an XML file!

As you control the headings of the XML branches via the class, its pretty easy to see what is going on in the XML file itself. As much as it seems like more work, I think it is a quite elegant method.

xml

One other useful thing to crop up with this is Binary Serialization. You use the same serializer class to dump all the control positions to a temporary .dat file. Why is this useful? You’ve just used the serializer to introduce an undo buffer, in case the control alignment didn’t happen as you expected, or as I do pick the wrong direction to align.

The RigSelector User Control

RigSelectorVS

The ultimate payoff to the IDE approach is providing a simple way to rebuild the selection dialog from the XML data. The RigSelector usercontrol is just a composite control that only really features a panel. The key is having a method to deserialize the XML to populate the panel with the stored node hierarchy.

We have reused the BorderlessAutodeskToolstripRenderer that we used earlier for the button.

It can resize the parent form to the dimensions of the stored dialog control.

RigSelectorClass

The rest is done by adding the handlers when deserializing the control to fire the selection within max. This perhaps the simplest part. To fire Maxscript code from within a dotnetclass you use –

>ManagedServices.MaxscriptSDK.ExecuteMaxscriptCommand(<<MXS_String>>)

Lastly, my best piece of advice about implementing ANY form of custom dotnet control class in 3dsMax.

Don’t be shy about calling enable accelerators

to pass focus back to your max shortcuts!

As much as the maxform control is supposed to handle all this stuff, using a custom class can put a spanner in the works, and there’s nothing worse than realising that you’ve just knocked out your hotkeys.

ManagedServices.AppSDK.EnableAccelerators()

I call this after every button click, which might be overkill but it’s better than losing focus to your dotnetcontrol and being powerless to get them back without a restart or frenzied maxscript call. Any one wanting to do this sort of thing should really look into ManagedServices.dll, there is some great stuff in there.

Polar-DesignSurface

I hope this article has been of interest! Comments are always welcome!

Rig Studio Update!

I recently added automatic rig generation to Rig Studio – This speeds things up considerably.

I have also added a new control – A layer control. This allows you to store a name string of a corresponding layer. This has hide and unhide functionality build in, so you can control character rig and mesh visibility without having to open the layer manager.