Phone: 905 409-1589
Email: info@penproductions.ca
RSS LinkedIn Twitter Twitter
GDI+ Drawing
Navigation:
Overview:
Graphics drawing with dotNet allows for more flexibility in the look and feel of how a tool can be developed. Text, shapes, colors and even transforms can be delt with through the graphics drawing interface called GDI+ (Graphics Devise Interface). GDI drawing isn't exceptionaly fast, espcialy via Max script so it should be used with care. At PEN we have created some very complex tool sets using it how ever and rely on it for any complex UI system that is being developed in Max script only.

In the example we will be using a pictureBox control but we can do it with just about any control there is. All controls allow for the creation of a graphics object so that drawing can be done.

In the example to the left I'm only using a "system.windows.forms.form", GDI+ drawing methods and one bitmap of the Walter character. All of the rest of the graphics are being drawn when the form is created by way of the graphics engine.

Getting Started:
I will start with a function for formating all the properties, methods, events and constructors to the listen so that we can inspect the dotNet objects that we are creating.

A dotNet form is being used for this example, you can read more about how to use the in the Form & MaxForm tutorial.

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
		format "\nConstructors:\n"
		dotNet.showConstructors dn
	)else
	(
		format "% is not a dotNetObject or dotNetClass\n" dn
	)
)

--Create a form and display it in Max. 
--Read about this process in the form & MaxForm tutorial. 
form=dotNetObject "form"
sysPointer=dotNetObject "system.intPtr" (windows.getMaxHWND())
maxHandle=dotNetObject "maxCustomControls.win32HandleWrapper" sysPointer
form.show maxHandle
form.bounds=dotNetObject "system.drawing.rectangle" 10 90 400 400
form.text="DotNet GDI+ Drawing"                        
                        

For this tutorial we will use a pictureBox control as the object that we will do all the drawing in. You could do the whole tutorial without any control added how ever and just drawing the graphics to the form it self.

A pictureBox control is created in the example below and then added to the controls array for the form. We now have all the pieces in place to get started with painting graphics into the pictureBox.

Code:
--Add a pictureBox control to the form
pBox=dotNetObject "pictureBox" --"system.windows.forms.pictureBox"

--Set the size and location of the pictureBox
pBox.bounds=dotNetObject "System.drawing.rectangle" 2 2 388 362

--Set the back ground color
pBox.backColor=(dotNetClass "system.drawing.color").gray

--Add the pictureBox to the form
form.controls.add pBox
                        
Paint Event Handler
The paint event handler is where painting to controls occures. When ever the control is refreshed the paint handler is fire and the contents of the function are run.

In the example below we are painting a red square to the pictureBox when ever it is updated. This differs to creating a label and coloring the backColor red and adding it to the pictureBoxs controls array. A label is an object, it has properties, methods and events of its own. A red square is just a red square and isn't an object. It is just a collection of red pixels. There are no properties, methods or events for the shape that is drawn. This has advantages and disadvantages.

An advantage to using GDI+ in this case over a label with a back color of red is that it will draw the GDI+ drawing far faster then it is to create and add a label. For one label this might not be a problem how ever if the label was constantly having to update and change its position, color and other properties in real time it could get to slow.

The disadvantage of using GDI+ in this case over a label is that the GDI+ drawing doesn't have the properties, methods and events that the label does. This means it is harder to track if a user has clicked on the area or the mouse has passed over it. With a label this is easy as we could just add event handlers to the label to track this.

The thrid last line is where the paint event is added and then the next line where the life time of the control is set to #dotNet, this ensures that the event handler isn't garbage collected with Max script as that would remove it from memory and it would stop working. Setting it to #dotNet sets it in the scope of dotNet for garbage collection.

At the top of the example we create a rectangle and a brush. The rectangle is the area that we are going to draw into and the brush is going to describe how we are going to fill it in. The "system.drawing.rectangle" takes four parameters, X and Y for the starting position and then width and height. The "solidBrush" object requires a color object to be passed to it to set the color of the brush.

The pBoxPaint function has been setup to do all the drawing. The paint event is called when ever the dialog needs to be refreshed. This happens for instance when another dialog is move off the top of the form we created exposing it. As will all the dotNet handlers it passes two arguments to the function that is being called, "sender" and "arg". "Sender" is the an instance of the control that is calling it and arg will return a range of properties that can be used in the function. If we use "formatProps arg" in the function you can see that we have a small number of properties to deal with. The one that we are interested in is "graphics".

The last line of the code below is "form.refresh()", this is needed to force a refresh on the form to fire the paint handler when it is added. We have done things out of order to make this tutorial easier to follow. Really we should have created and added the pictureBox control with its event handlers before we displayed the form using "form.show()" If we did this we wouldn't need a "refresh()" method called.

Listener: arg properties.
Properties:
  .ClipRectangle : <System.Drawing.Rectangle>, read-only
  .Graphics : <System.Drawing.Graphics>, read-only
  .Empty : <System.EventArgs>, read-only, static                        
                        

Next we can use "formatProps arg.graphics" to check to see what graphics properties and methods are available to us. In this case you will see about 18 properties and many more methods. We are interested in both the properties and methods to paint what we will be doing. We will start with just one of the methods and that is "fillRectangle".

"fillRectangle" has several construction methods available but all will essentialy do the same thing except the last two that will create several rectangles if it is passed an array of rectangle objects. We will use the second example for this tutorial. "fillRectangle" in this case is looking for two properties to be passed do it, a brush and a rectangle area to paint in. This is where the "solidBrush" and "rectangle" objects are needed that we created before the function.

Listener: arg.graphcis methods.
Methods:
  .FillRectangle <System.Drawing.Brush>brush <System.Drawing.RectangleF>rect
  .FillRectangle <System.Drawing.Brush>brush <System.Drawing.Rectangle>rect
  .FillRectangle <System.Drawing.Brush>brush <System.Int32>x <System.Int32>y <System.Int32>width <System.Int32>height
  .FillRectangle <System.Drawing.Brush>brush <System.Single>x <System.Single>y <System.Single>width <System.Single>height
  .FillRectangles <System.Drawing.Brush>brush <System.Drawing.RectangleF[]>rects
  .FillRectangles <System.Drawing.Brush>brush <System.Drawing.Rectangle[]>rects
                        

Once again we can use the "formatProps" function to examine "System.Drawing.Brush" and "System.Drawing.Rectangle" to see how we can construct them. The best way to do this is on the class and not the object since it is the object we are trying to understand how to create. So we can do what is below to find the constructors.

Code:
formatProps (dotnetClass "System.Drawing.Brush")
Listener:
Properties:

Methods:
  .[static]Equals objA objB
  .[static]ReferenceEquals objA objB

Events:

Constructors:
                        

Here is one of those cases where we have to go to MDSN to get more information since the above that was returned from "formatProps" isn't telling us much. So if you search google for "system.drawing.brush" is should bring you directly to the MSDN site. Once at that page you will see a comments like what is below. What this is telling you that you can't directly make a "system.drawing.brush" but instead you need to create one of the classes that is derived from it.

 Remarks:
This is an abstract base class and cannot be instantiated. To create a brush object, use classes derived from Brush, such as SolidBrush, TextureBrush, and LinearGradientBrush.

--And
 Inheritance Hierarchy:
System.Object
  System.MarshalByRefObject
    System.Drawing.Brush
      System.Drawing.Drawing2D.HatchBrush
      System.Drawing.Drawing2D.LinearGradientBrush
      System.Drawing.Drawing2D.PathGradientBrush
      System.Drawing.SolidBrush
      System.Drawing.TextureBrush                        
                        

If you now run "formatProps (dotNetClass "system.drawing.solidBrush")" you can see that a constructor shows up that tells you how to build the object. In this case the only parameter that needs to be passed is a color object. Use "formatProps (dotNetClass "system.drawing.color")" to find out what you can do with color objects when you create them.

Listener:
Constructors:
  System.Drawing.SolidBrush <System.Drawing.Color>color                        
                        

Once we have all the parts for creating the rectangle we can create a grpahics object an then call the "fillRectangle" method and pass the two properties that we created before the function. The "solidBrush" and "color" objects where created out side of the function to ensure that it remained as fast as possible. Doing it inside would allow for changing the color on the fly but would need to be creating objects to do it. This might not be a problem in some cases where the graphics are not getting updated often but in cases like the facial UI controls seen on the dotNet home page or the composer interface speed was something that I was concerned with as it had to constrantly update as the mouse was moved or the time line scrubbed.

Code:
--Rectangle and brush for painted box	
boxRec=dotNetObject "system.drawing.rectangle" 10 10 100 100
boxBrush=dotNetObject "system.drawing.solidBrush" (dotNetClass "system.drawing.color").red

--Add the paint function
fn pBoxPaint sender arg=
(
	formatProps arg.graphics
	--Create graphics object
	g=arg.graphics
	
	--Paint a filled rectangle into the pictureBox
	g.fillRectangle boxBrush boxrec
	
)
--Add the paint event handler 
dotNet.addEventHandler pBox "paint" pBoxPaint

--Set the life time control
dotNet.setLifeTimeControl pBox #dotNet

--Force an update to the form to fire the paint event
form.refresh()
                        
Drawing Ellipses & Curves:
When you inspect the "graphics" method with "formatProps" you see there is a lot you can do with the graphics engine. Drawing curved shapes is one of those things and can add some unique design ideas to you user interfaces. In this section we are going to look at drawing lines, bezier lines and ellipses.

We will start with drawing an ellipse that is not filled in and has a solid line around it 40 pixels wide. We need to create two main variables for this, the rectangle area that we are going to draw into and a pen that will be used to ink the line that will be drawn. Once again use "formatProps (dotNetClass "system.drawing.pen")" to inspect the constructor for creating a pen object. You can see that it will need a "solidBrush" and a width in pixels passed as arguments to create the pen object.

Code:
--Rectangle and brush for painted ellipse 
ellipseRec=dotNetObject "system.drawing.rectangle" 0 2 200 400
ellipseBrush=dotNetObject "system.drawing.solidBrush" (dotNetClass "system.drawing.color").blue
ellipsePen=dotNetObject "system.drawing.pen" ellipseBrush 40
                        

The "pBoxPaint" function is updated with the new "drawEllipse" command and it passed the pen and rectangle as parametes. The order that you drawin the graphics in will determine which is on top. In this case the ellipse is drawn on top of the rectangle.

Code:
fn pBoxPaint sender arg=
(
	--Paint a filled rectangle into the pictureBox
	g.fillRectangle boxBrush boxrec
    
	--Draw Ellipes
	g.DrawEllipse ellipsePen ellipseRec
)
                        

It is also possible to drawing bezier curves using the "drawCurve" method. This method will require that an array of points to be drawn is created first. To do this we will use "formatProps" function to get the constructor for creating an array. Arrays are created the same way as any value type is with the added "[]" on the end. The constructor tells us that we need to pass and integer value to it when creating the object. The interger is the amount of items that we will want in the array. Unlike Max script arrays we need to set the array size first and then add point objects to it.

Listener:
--Run this line
formatProps (dotNetClass "system.drawing.point[]")
                        
Constructors:
  System.Drawing.Point[] 
                        

Now that we can create an empty array we need to know how to populate it with point objects. Once again use the "formatProps" function to inspect the methods available to us. The "set" method will allow new point objects to be added to the array. Make note that dotNet, and just about every other programing language is 0 based with the arrays, Max script is 1 based. So the first item in the array is 0 and not 1.

Listener:
--Run this line
formatProps (dotNetOBject "system.drawing.point[]" 6)
                        
.Set <System.Int32> <System.Drawing.Point>                        
                        

Below is code that should be before the function. First create a "solidBrush" and then a "pen" object. We can then create the array of points and use the "set" method to add each point to the array.

Code:
--Objects for creating a curve. 
curveBrush=dotNetObject "system.drawing.solidBrush" ((dotNetClass "system.drawing.color").fromArgb 200 200 20)
curvePen=dotNetObject "system.drawing.pen" curveBrush 2

--Array of points for the curve to follow. 
curveArray=dotNetObject "system.drawing.point[]" 6
curveArray.set 0 (dotNetObject "system.drawing.point" 10 10)
curveArray.set 1 (dotNetObject "system.drawing.point" 240 20)
curveArray.set 2 (dotNetObject "system.drawing.point" 350 160)
curveArray.set 3 (dotNetObject "system.drawing.point" 130 280)
curveArray.set 4 (dotNetObject "system.drawing.point" 220 140)
curveArray.set 5 (dotNetObject "system.drawing.point" 10 320)
                        

Once the array has been created we can add this line to the "pBoxPaint" function to draw the line for us. There are other parameters that we can pass to this method, if you pass a "system.single" value after the array this will control the "tension" at each point. If a value of 0 is passed you will recieve a curve with hard corners. .5 will return the default and values higher will start to curve the line even more. As the value goes over 1 it will start to loop back on it self.

Code:
	--Draw a curve
	g.DrawCurve curvePen curveArray
    
	--Draw a curve and control the tension. 
	g.DrawCurve curvePen curveArray 1
                        
Transforms:
Transforms allow you to move, rotate and scale the drawing process as it is happening. It is a little different from dealing with transforms on objects in Max as this needs to be done before the drawing occures. The best way to think of it is each time you draw a new graphic it is placed on a new layer and the rectangle drawing area or the point at which it is being drawn is an offset value from the upper left corner of the layer. If you transform the layer you are about to draw on before doing the drawing it is like moving, rotating and scaleing that upper left corner reference point. This way the drawing occures at a different location compared to the control that you are drawing into. The rectangle or point which you are drawing is relative to the layers transform not the control it self.

Transforms are simple to set up, there are three main methods that can be used, "TranslateTransform", "RotateTransform" and "ScaleTransform", with these three we can perform the main transforms. One of the issues is knowing where the transform will place the drawing. In testing I found that all transforms are working around the 0,0 point in the upper left corner of the control that is being drawn into.

In the example below we once again draw an ellipse but this time we translate, rotate and scale the drawing area first and then draw the ellipse. The rectangle area that we are drawing the ellipse into is still relative to the 0,0 point but the whole drawing area has been transformed so the ellipse is now drawn in a new location. To draw the line we have decided that we want that relative to the 0,0 point of the original control that we are drawing into so we reset the transform and then draw the curve.

Code:
fn pBoxPaint sender arg=
(
	--Transform all drawing below. 
	g.TranslateTransform 180 180
	g.RotateTransform -45
	g.ScaleTransform 1.5 .25

	--Fill Ellipes
	g.DrawEllipse ellipsePen ellipseRec
    
	--Reset the trasnforms for all below
	g.ResetTransform()

	--Draw a curve
	g.DrawCurve curvePen curveArray 1
)                        
                        
Code:
                        
                        
Code: