Phone: 905 409-1589
Email: info@penproductions.ca
RSS LinkedIn Twitter Twitter
Dotnet Form
Navigation:
Overview:
Normally in Max script we are creating dialogs using createDialog or newRolloutFloater, with Dotnet we can go one step further and create floating dialogs that are floating windows instead of just on top of Max. What this can get you is a dialog that can be behind Max. Forms also allow for Dotnet objects such as UI items like listview or treeview to be placed in them. Forms also have far more control then a createDialog does in Max, just about anything is possible when working with them. Something to note is you can't use any of the Max UI items in a Dotnet form, spinner, listBoxs and other UI items can only be placed in a Max rollout. For this reason you will need to decide in advance if you really need to create your floating dialogs with all Dotnet or if you can use just Max script native tools with dotNet controls added to them. Note that adding dotNet controls to a Max script rollout directly also limits what is possible with the dotNet control.

There are advantages and disadvantages to using a Dotnet Form and an all Dotnet UI. Disadvantages are mostly complexity of code as you will have to manage much more of it your self, registering event handlers, placement on the dialog and many other steps will need to be taken to creating and working with Forms. Advantages to using Forms is that you can do just about anything and there are less restrictions on what is possible. Even when using a Dotnet UI item like a listview in a rollout you will end up with certain limitations that are hard or impossible to get around. With a Dotnet only UI starting with a Form it is possible to control just about every aspect of how it will look and work.

In the image below a Dotnet Form has been used as a transparent overlay to the Max viewport. In the form I'm drawing graphics on the fly for heads up data about the object. This wouldn't be possible with a Max rollout and createDialog as you can't make it transparent and you can't draw graphics into it. Also the small box in the lower right corner of the viewport is a Dotnet Form that expands to show all the options for the graphics drawing. Forms can look like just about anything that you need or they can look just like a createDialog in Max script.

Creating Forms:
Below are two ways to create a dotNet form. The first is the long form using the full name space path to the form control. Often these paths are already loaded in Max when it started and you can use the short form. In this case in the second example you only need to provide the dotNetObject with "form" and it will understand that you are looking for the form control that is found in "system.windows.forms"

Once the form has been created you need to show it. Just like when creating a rollout you need to use createDialog or newRolloutFloater we need to use a method that is available in a form called "show". We can also close the form using the "close" method.

Code:
--Creates a dotNet form.
form=dotNetObject "system.windows.forms.form"

--You can also use the sort cut for this.
form=dotNetObject "form"

--Show the form
form.show()

--Close the form
--form.close()
                        
The problem with using a "form" is it doesn't respect the rest of the settings that are being used in Max like the background colors. For this a custom dotNet maxForm was created that is inherited from "form". To create this we need to use the "maxCustomControls" path to access the new "maxForm".

Code:
form=dotNetObject "maxCustomControls.maxForm"                        
                        

Usage of the form when created in any one of these three ways is exactly the same.
Checking properties, methods and events:
To make things easier as we work we will create a function that will format all of the properties, methods and events of a dotNetObject or dotNetClass to the listener so that we can inspect what we have to work with.

The function below takes one argument which is the dotNetObject or dotNetClass that has been created or returned.

Code:
fn formatProps dn=
(
	if classOf dn==dotNetObject or classOf dn==dotNetClass then
	(
		clearListener()
		format "Properties:\n"
		showProperties dn
		format "\nMethods:\n"
		showMethods dn
		format "\nEvents:\n"
		showEvents dn
	)else
	(
		format "% is not a dotNetObject or dotNetClass\n" dn
	)
)

--Format all the properties to the listener. 
formatProps form                        
                        
Adding Controls:
Adding controls that are needed to the form is very easy. All we need to do is create the control and then use the .add method that we found when formatting the properties to the listener.

In the example below we are creating a "label", the "label" just creates a box that we can do add a title to, draw graphics into or add other controls to.

The label is also having the back ground color of it changed using the property "backColor" and a dotNet color class and then creating a red object from it. This is the way to create color to be added to any control or other object like text to set the color of it.

The size and location of it, with respect to the form that it is being added to, is then set using a dotNet rectangle. The rectangle is also part of the "system.drawing" name space. In the example below it has a position relative to the form of 10 units in X and Y and then a width and length of 100 units. We set the "bounds" property to the rectangle so that when it is added to the form it is set with those dimensions.

The label is then added to the form using the "form.controls.add" method. To find the "add" method we would have had to use "formatProps form" to find the controls property and then running "formatProps form.controls" to find out what properties, methods and events for it. This is where dotNet is very different from Max script, being object oriented you will find that it is far deeper and how it is structured.

Code:
--Add controls to the form. In this case a red label.
--Create the label
dnLabel=dotNetObject "label" --"system.windows.forms.label"

--Set the backColor property to red
dnLabel.backColor=(dotNetClass "system.drawing.color").red

--Set the location and size of the label using a rectangle. 
dnLabel.bounds=dotNetObject "system.drawing.rectangle" 10 10 100 100

--Add the label to the form. 
form.controls.add dnLabel                        
                        
Result:
This form was created using "maxCustomControls.maxForm" and for that reason the backColor property of the form is ready set to the same that is used by the rest of Max. This tutorials was created using Max 2010 and the default colors.
Setting Properties:
When the "formatProps" function is used on the form you can see that there are many properties that can be set. Setting some of these properties is very straight forward, others need to have a better understanding of what they are looking for. Lets have a look at how to determine what needs to be passed to a property and how.

Certain value types for dotNet are being converted for us by Max script, you need to be careful which version of Max you are using as older versions like in Max 9 some of the automatic conversions were not set up and dotNet value types needed to be passed. We are looking at what the normal processes are now in Max 2010 and above.

The "text" property shows that it needs a "System.String" object passed to it. This is one of the value types that Max will automatically convert from a Max script to a "system.string" object for use in dotNet.

The "opacity" property requires "System.Double" object, most of the number type values are converted for you automatically as well so we only need to pass it a value from 0 to 1 to set the amount of opacity.

Code:
--Set the title text. 
form.text="My First Form"

--Set an over all opacity
--Value range is 0-1
form.Opacity=.7
                        

For other properties we will need to create dotNet objects as Max either doesn't have an equivalent data type or it will not automatically convert it for us. With the "formBorderStyle" property it requires a "System.Windows.Forms.FormBorderStyle" object to be passed to it. There are two ways that we can do this.

The first way fits closest to the way that we have been setting other properties like the backColor of the label. The formBorderStyle object needs to be created and one of the properties set for it. If we create a "dotNetClass "system.windows.forms.formBorderStyle"" and pass that to the "formatProps function we will see several options that can be set. In the example below we use the "sizableToolWindow" which creates a typical tool window dialog that has a small title bar and close button.

Listener:
Properties:
  .Fixed3D : , read-only, static
  .FixedDialog : , read-only, static
  .FixedSingle : , read-only, static
  .FixedToolWindow : , read-only, static
  .None : , read-only, static
  .Sizable : , read-only, static
  .SizableToolWindow : , read-only, static                        
                        
Code:
borderStyle=(dotNetClass "System.Windows.Forms.FormBorderStyle").SizableToolWindow
form.formBorderStyle=borderStyle

--or
form.FormBorderStyle=form.FormBorderStyle.SizableToolWindow
                        

The next property we have already used with the label and that is a rectangle. The "system.drawing" name space is used for creating objects that will control graphics and drawing styles. In this case the "bounds" property we need to create a new rectangle object with the location and size passed to it. We could also do the same using two other properties, the "location" and the "size" property. In this case a "system.drawing.point" and a "system.drawing.size" object with the needed values passed to it.

Listener:
.Bounds : <System.Drawing.Rectangle>                        
.Location : <System.Drawing.Point>
.Size : <System.Drawing.Size>                        
                        
Code:
--Set the size and location of the form in screen space
form.bounds=dotNetObject "system.drawing.rectangle" 5 20 300 200

--Or
form.location=dotNetObject "system.drawing.point" 5 20
form.size=dotNetObject "System.Drawing.Size" 300 200
                        
Result:
Notice that the whole form is 30% transparent and you can see through it to the listener and editor in my viewport. It also has the small title bar that was set with the formBorderStyle.

Two other interesting properties are "backColor" and "transparencyKey", when used together it is possible to create dialogs that are completely transparent. This is how the heads up display tool was created in the first image on this page. If you set the "transparencyKey" to be the same color as the backColor property the form becomes 100% transparent. This is different then setting the opacity to 0 since it would also make all the controls that have been added to it become transparent as well.

Code:
--Set the background color
form.backColor=(dotNetClass "system.drawing.color").black

--Set the transparent color
form.TransparencyKey=(dotNetClass "system.drawing.color").black
                        
Result:
If you also set the "formBorderStyle" to "none" the whole form will disappear and you will only see the controls and graphics that are drawing to it.
Event Handlers:
The event handlers for forms are no different then any other dotNet control. They are how ever different then using event handlers for dotNet controls that are being used directly in a Max script rollout. When a dotNetControl is added to a Max script rollout you use the same syntax for the event handler as you would any Max script UI item. When building dotNet only UI's you will need to manage the event handlers your self. What this means is you have to add each event handler that you will be using as well as the functions that they call.

In the example below we set three event handlers for the form, mouseDown, mouseUp and mouseMove. Checking what event handlers you can add using the formatProp functions you will see that there are far more available then when using a createDialog in Max script.

Lets inspect the first event handler. The first function is added called formMouseDown which has two arguments. These two arguments are passed to it automatically so you don't need to send them. The event handler for mouse down is then added, "dotNet.addEventHandlers" is used to do this. Three parameters are passed to it, the control that the event handler is being added to, the name of the event handler and then the function that it will call.

The functions two arguments contain information that you can use in the function. You can call the two arguments anything that you want but sender and arg are used often so it will keep it clear with other information on the internet. Sender is the actually control it self and running "formatProps sender" on it would yield the same results as "formatProps form" in this case. The arg argument is different for every event handler and you will have to check what it will return for you. In the cases below all return the same properties. For mouse down there are several properties return and are sown below.

In the example when the user clicks on the form and drags the mouse the label will stick to the mouse and follow it around. For this two of the properties of arg are being used. "button" is the first and isn't necessarily needed in this case but shown as it is very useful. "button" returns a "System.Windows.Forms.MouseButtons" class and you can check its properties to see that there are six that will allow you to track all the buttons on a five button mouse, one of the properties is "none"

In the mouse move event the mouse is being tracked over the form, the "arg.location" property returns a "system.drawing.point" object that can be used directly with the labels "location" property to set where it will be located in the form. I'm using a variable called mouseIsDown and setting it to true if the left mouse button has been clicked and then in the mouseUp handler I'm setting it back to false.

The last line sets the life time control. There is a problem created when Max script does a GC() operation, it will clear all the event handlers from memory as Max doesn't think that they are necessary. Setting the life time control to "#dotNet" stops Max script from clearing them and allows dotNet to manage if they are needed or not.

Listener:
Properties:
  .Button : <System.Windows.Forms.MouseButtons>, read-only
  .Clicks : <System.Int32>, read-only
  .Delta : <System.Int32>, read-only
  .Location : <System.Drawing.Point>, read-only
  .X : <System.Int32>, read-only
  .Y : <System.Int32>, read-only
  .Empty : <System.EventArgs>, read-only, static

Methods:
  .<System.Boolean>Equals <System.Object>obj
  .[static]<System.Boolean>Equals <System.Object>objA <System.Object>objB
  .<System.Int32>GetHashCode()
  .<System.Type>GetType()
  .[static]<System.Boolean>ReferenceEquals <System.Object>objA <System.Object>objB
  .<System.String>ToString()                        
                        
Code:
--Variable used in mouseMove handler. 
mouseIsDown=false

--Add event handlers for the form.
fn formMouseDown sender arg=
(
	--Check to see which mouse button is being pressed. 
	if arg.button==arg.button.left do formatProps arg;mouseIsDown=true
	if arg.button==arg.button.right do print #right
)
fn formMouseUp sender arg=
(
	mouseIsDown=false
)
fn formMouseMove sender arg=
(
	if mouseIsDown do
	(
		format "Mouse Position:[%,%]\n" arg.x arg.y
		dnLabel.location=arg.location
	)
)

--Add the event handlers. 
dotNet.addEventHandler form "mouseDown" formMouseDown
dotNet.addEventHandler form "mouseUp" formMouseUp
dotNet.addEventHandler form "mouseMove" formMouseMove

--Set the life time of the control to #dotNet so that it is not garbage collected with the rest of Max script. 
--For Max 2009 and previous you will need the avg extension to set this. 
dotNet.setLifeTimeControl form #dotNet
                        
Wrapping Up:
The one problem with forms is that they don't sit on top of Max like any other Max dialog. What I mean by that is if you click on any other dialog in Max the form will be set behind Max and not just behind the dialog that was selected. This behavior can be changed in two ways. The easiest way is to set a property called "topMost" to true, that will force the dialog to always stay on top of everything. The problem then is you can't get other Max dialogs to be on top of the form. What we need to do it set the parent of the form to be Max so that it is directly associated with Max.

The "show" method will also take an argument, that argument needs to be a pointer to the HWND window handle. If you don't know what that is here is a very brief explanation. In windows every window is assigned a ID, that ID can then be used to access that window. A window is just about any object, so, a rollout, a button in a rollout, a dotNet form and any other things To set the form to be parented to Max and not windows we need to do a four step process.

Step one is to get the pointer value that represents the main Max window, this is done with a Max script command called the "windows" structure. This will return a value that looks something like this "2426114P"

Next we need to turn the pointer into a dotNet value type, this is one of those cases where Max script doesn't convert the value type automatically. A "system.IntPtr" object is used and set to the value that we received in step one.

Step three we create a wrapper for the system pointer, I'm not really quite sure at this point why this is needed but it is. We use the "maxCustomControls" structure to access the "win32HandlerWrapper" method and pass it the system pointer.

Once we have the Max handler wrapper we can pass that to the "show" method and now when the dialog opens it will be parented to Max and will always stay on top of it but not other dialogs.

Code:
--Set the parent of the form to be Max. 
--Get the max handle pointer.
maxHandlePointer=(Windows.GetMAXHWND())

--Convert the HWND handle of Max to a dotNet system pointer
sysPointer = DotNetObject "System.IntPtr" maxHandlePointer

--Create a dotNet wrapper containing the maxHWND
maxHwnd = DotNetObject "MaxCustomControls.Win32HandleWrapper" sysPointer

--Show the Max form using the wrapper. 
form.Show (maxHwnd)