WINDOWS FORMS - GDI+

Summary

GDI+ Namespaces

The .NET Framework provides a number of namespaces devoted to two-dimensional graphical rendering. In addition to the basic functionality such as Pens, Brushes, Fonts, etc., there are also types that enable geometric transformation, anti-aliasing, and palette blending. Collectively, these namespaces make up GDI+. The following table lists the major namespaces used in GDI+:

GDI+ Namespace Meaning
System.Drawing The core GDI+ namespaces which defines numerous types for basic rendering - pens, brushes and the Graphics type 
System.Drawing.Drawing2D Types used in more-advanced 2D graphics - gradient brushes, geometric transformations, etc.
System.Drawing.Imaging Types that allow you to manipulate images
System.Drawing.Printing Types that allow you to perform and manage various printing functionalities
System.Drawing.Text Types that allow you to manipulate collections of fonts.

When you wish to use the GDI+ system you must set a reference to the System.Drawing.dll assembly. Once a reference has been set, use the C# using keyword and you are ready to render.

Overview of the System.Drawing namespace

A vast majority of the types used when programming GDI+ applications are found in the System.Drawing namespace. There are classes that represent bitmaps, pens, brushes, fonts and a number of other related types such Color, Point, Size, and Rectangle, among many others. Many of these types make a substantial use of a number of related enumerations most of which are also defined in the System.Drawing namespace.

The following code examines some of the utility classes such as Point, Size, Rectangle. Note that these types also have a float version where the underlying data is represented as floats:

using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Data;

namespace GDI
{
    public class Form1 : System.Windows.Forms.Form
    {
        ...

        public Form1()
        {
            ...
            TestPoint();
            TestRectangle();
            TestRegion();
        }

        // The main entry point for the application.
        [STAThread]
        static void Main() 
        {
            Application.Run(new Form1());
        }

        /* this function tests some of the basic features of Point */
        private void TestPoint()
        {
            // Create a new point
            Point pt1 = new Point( 10, 10 );
            Console.WriteLine( "pt1 is : {0}, {1}", pt1.X, pt1.Y); // 10, 10

            // Offset pt1 10 points in the X-direction and -10 points in the Y-direction
            pt1.Offset( 10, -10);
            Console.WriteLine( "pt1 is : {0}, {1}", pt1.X, pt1.Y); // 20, 0

            // Create a _copy_ of pt1
            Point pt2 = pt1;
            pt2.X = 100;
            Console.WriteLine( "pt1 is : {0}, {1}", pt1.X, pt1.Y); // 20, 0
            Console.WriteLine( "pt2 is : {0}, {1}", pt2.X, pt2.Y); // 100,0
        }

        /* this function tests some of the basic features of Rectangle */
        private void TestRectangle()
        {
            // Create a new rectangle
            Rectangle rc = new Rectangle( 0, 0, 100, 200); // width is 100, height is 200

            // One of the more useful functions of Rectangle is Contains. It allows you
            // to determine if the given point or rectangle is within the bounds of
            // of the current rectangle
            Point p = new Point( 10, 10 );
            if (rc.Contains( p ))
            Console.WriteLine("Point ({0}, {1}) is within the rectangle");
        }

        /* this function tests some of the basic features of Region */
        private void TestRegion()
        {
            // The region class represents the interior of a geometric shape. Given the 
            // last statement, it would make sense for the constructors of Region to
            // require an instance of some existing geometric shape. The following code
            // gets the interior of a Rectangle and examines various attributes
            Rectangle rc = new Rectangle( 0, 0, 100, 200 );
            Region rg = new Region( rc );
        }
    }
}

Understanding Paint Sessions

The Control class defines a virtual method called OnPaint(). When a form or any descendant of Control wishes to render graphical information, you must override this method and extract a Graphics object from the incoming PaintEventArgs parameter. Recall that when responding to GUI events, you have two ways at your disposal: either override the appropriate virtual method, or use delegates (preferred).

OnPaint() is called whenever a paint message is places into the application's message queue. This can happen either automatically when the window becomes dirty - resized, covered by another window, or minimized/maximized/restored - or programmatically by inserting a paint message in the application's message queue by calling Invalidate().

Note: to correctly render images when resizing the control, either set the ResizeRedraw style to true, or handle the Resize event by calling Invalidate(). Here is what happens if you don't!

The following code illustrates the above concepts. The code redraw the string "Hello GDI+" in a new location and in a new size every time the window is repainted - by resizing and minimizing/maximizing/restoring:

using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Data;

namespace Paint
{
    public class Form1 : System.Windows.Forms.Form
    {
        // Data members
        private Point pt = new Point(0, 0 );    // new location where to draw string
        public Form1()
        {
            // A control must always have ResizeRedraw set to true. As an alternative, handle the 
            // Resize message and call Invalidate() in the handler
            Console.WriteLine( "ResizeRedraw is " + GetStyle( ControlStyles.ResizeRedraw ).ToString());
            SetStyle( ControlStyles.ResizeRedraw, true);

            // Commenting the above and uncommenting the following will have the same effect as setting
            // ResizeRedraw to true, as long as the Resize event handler called Invalidate()
            //this.Resize += new EventHandler( OnResize );
        }

        [STAThread]
        static void Main() 
        {
            Application.Run(new Form1());
        }

        // Overriding OnPaint() to handle painting event. Extract the Graphics object from the 
        // PaintEventArgs and use it to perfrom reqiured rendering
        protected override void OnPaint( PaintEventArgs e )
        {
            // Update rendering location. Pt.X is also used to set the font size
            pt.X = pt.X + 1;
            pt.Y = pt.Y + 1;
           Console.WriteLine("OnPaint - {0}, {1}", pt.X, pt.Y);

            // Extract the Graphics object
            Graphics g = e.Graphics;

            // Perform required rendering at a new location and in a new size
            g.DrawString( "Hello GDI+", new Font("Arial",pt.X), new SolidBrush( Color.Chocolate), pt.X, pt.Y); 
        }

        // Uncomment this code as an alternative to setting the ResizeRedraw bit in the form's style
        /*public void OnResize( object sender, EventArgs e)
        {
            Invalidate();
        }*/
    }
}

Rendering GDI+ Objects Outside Paint Handlers

You may need to render graphics outside the scope of a standard paint handler. For example, you wish to draw a small circle at a the position where a mouse was clicked. The first step is to obtain a Graphics object using Graphics.FromHwnd() and then do the drawing in the Click handler:

private void Form1_Click( Object sender, MouseEventArgs e)
{
    // Get a graphics object
    Graphics g = Graphics.FromHwnd( this.Handle );

    // Draw the circle
    g.DrawEllipse( new Pen(Color.Green), e.X, e,Y, 10, 10);
}

Unfortunately, this will not work because if a form is invalidated and hence redrawn, all the drawn circle will be erased. That's what Invalidate() does. It just erases the form (with the background brush, but more on that later). The general approach is as follows: OnPaint() should always handle painting. In this scenario, when a mouse is clicked, add a new point to an internal collection and call Invalidate(). This will erase the background of the control and call OnPaint(). OnPaint() can then iterate through each point in the collection and draw the circle in the location indicated by the current point.

Understanding the Graphics Class

Now that you know how to obtain a Graphics object - either within OnPaint() or outside the paint handler, you need to understand how to manipulate it. The System.Graphics class is the key to rendering GDI+ rendering functionality. This object represents a valid device context (i.e., DC in Win32) coupled with many methods that allow you to render text, images, as well as many numerous geometric patterns.

Some of the rendering methods include but are not limited to DrawArc(), DrawBeziers(), DrawEllipse(), FillPolygon(), MeasureString(), and many many others.

The Graphics class also defines a number of methods that encapsulate how the current rendering operation will look and feel. For example, InterpolationMode specifies how data is interpolated between endpoints using a related enumeration. CompositingMode property determines whether drawing overwrites the background or is blended with the background. Look up these and many more in MSDN for more details.

Default GDI+ Coordinate System

The default unit of measurement is pixel-based and places the origin in the upper-left corner as illustrated below:

The default graphics unit is the pixel. This can be changed setting the PageUnit property of the Graphics class object to any value of the GraphicsUnit enumeration:

g.PageUnit = GraphicsUnit.Millimeter;
g.DrawEllipse( new Pen(Color.Green), 10, 10 , 100, 100);  // x and y locations are 10 and 10 millimeters, respectively

What if you want to alter the location where rendering begins, i.e., you want to offset the point of origin. You can adjust the point of origin by calling Graphics.TranslateTransform(nNewXOrigin, nNewYOrigin) method.

Establishing an Active Color

The Color structure represents an Alpha-Red-Green-Blue color constant. Most of the functionality of the Color type comes by way of a number of static properties, which return a configured color:

// Get a Color object
Color c = Color.PaleVioletRed;

// Various properties and functions
float fBrightness = c.GetBrightness();
byte  bAlpha      = c.A;
int   nARGBValue  = c.ToArgb();

Manipulating Fonts

The System.Drawing.Font type represents a font installed on the user's machine. The Font class has a number of overloaded constructors. Here are some of the more common ones:

// font name is Arial and size is 12 points
Font f1 = new Font("Arial", 12);

// font name is Courier New and size is 12 points, and it bold and underlines
Font f2 = new Font("Courier New", 10, FontStyle.Bold | FontStyle.Underline );

Once you have created and configured your font, the next usual task is to pass it to Graphics.DrawString()method. Even though DrawString has been overloaded a few times, the basic required information is the same - string to draw, a font, a brush for rendering, and a location to draw the string. 

You can programmatically discover what fonts are installed on a user's machine. Doing so, gives the chance to explore another namespace, System.Drawing.Text:

// The following code lists the installed fonts
using System.Drawing.Text;

string strFonts = "";
InstalledFontCollection fonts = new InstalledFontCollection();
for (int i = 0; i < fonts.Families.Length; i++)
{
    strFonts += fonts.Families[i].Name + " ";
}
Console.WriteLine( "Installed fonts are: {0}", strFonts);

Survey of System.Drawing.Drawing2D Namespace

The following examines how to manipulate pens and brushes to render geometric patterns. You could do so using nothing more than the basic types defined in System.Drawing. However, many of the sexy pen and brush configurations - gradient brushes, etc. - require types defined within the System.Drawing.Drawing2D namespace.

This additional namespace provides a number of classes that allow you to modify the line cap - triangle, diamond, etc. - used for a given pen, build textured brushes, as well as work with vector graphics manipulations. Here are some of the core types to be aware of:

System.Drawing.Drawing32 Namespace Meaning
AdjustableArrowCap
CustomLineCap
Pen caps are used to paint the beginning and end points of a given line. These types represent an adjustable arrow-shaped and a user defined cap.
Blend
ColorBlend
Used to define a blend pattern used in conjunction with a LinearGradientBrush.
GraphicsPath
GraphicsPathIterator
PathData
A GraphicsPath object represents a series of connected lines and curves. This class allows you to insert just any type of geometrical pattern into the path
HatchBrush
LinearGradienBrush
PathGradientBrush
Exotic brush types

Establishing the Rendering Quality

Notice that some of the enumerations in System.Drawing.Drawing2D namespace - such as QualityMode and SmoothingMode - allow you to define the quality of the current rendering operation. When you obtain a Graphics object, it has a default rendering quality which somewhere in between with respect to speed and quality. When you wish to override the current rendering quality for a Graphics object, make use of the SmoothingMode property:

private void OnPaintHandler(Object sender, PaintEventArgs e)
{
    // Obtain the graphics object
    Graphics g = e.Graphics;

    // Do some rendering
    g.SmoothingMode = SmoothingMode.AntiAlias;
    g.DrawString( "Drawing2D", new Font("Arial", 20), new SolidBrush( Color.Tomato), 10,10);
}

Working with Pens

GDI+ Pen objects are used to draw lines. Typically, when you want to render certain graphical objects, you pass an instance of a pen. The Pen class defines a small number of constructors that allow you to determine the initial color of the pen, and the width of the pen nib. Some constructors of the Pen class allow you even to specify a brush (more on that later).

Remember that in addition to the Pen type, GDI+ also provides a Pens collection which allows you to retrieve standard pen on the fly without having to create one by hand. Be aware that the pens returned will always have a width of 1. If you require a more exotic pen, you will need to create one by hand.

private void OnPaintHandler(Object sender, PaintEventArgs e)
{
    // Obtain the graphics object
    Graphics g = e.Graphics;

    // Make a big pen by hand, and get another from the stock collection
    Pen p1 = new Pen( Color.IndianRed, 10);
    Pen p2 = Pens.LemonChiffon;

    // Render some shapes with the pens
    g.DrawLine( p2, new Point(10,10), new Point(100,10));
    g.DrawEllipse( p1, 30,30, 40,50);

    // Draw text inside a dashed rectangle
    Pen p3 = new Pen(Color.SeaGreen, 5 );
    p3.DashStyle = DashStyle.DashDot;
    Rectangle r = new Rectangle( 100,100, 50,50 );
    g.DrawRectangle( p3, r);
    g.DrawString( "Hello GDI+", new Font("Arial", 10), new SolidBrush( Color.DarkCyan ), r);
}

The result:

 

Working with Pen Caps

Using the LineCap enumeration you able to build pens where you specify how the end and beginning of the line will look like. The following code draws a series of lines using each of the available LineCap styles.

protected override void OnPaint(PaintEventArgs e)
{
    // Obtain the graphics object
    Graphics g = e.Graphics;

    // Create a pen
    Pen p = new Pen( Color.DarkViolet, 20);

    // Get all members of the LineCap enum. Note how this is done
    Array ar = Enum.GetValues( typeof( LineCap) );

    // Loop through the array of LineCap values and draw each line cap
    for (int i = 0; i < ar.Length; i++)
    {
        // Set the start and end cap
        LineCap lc = (LineCap)ar.GetValue(i);
        p.StartCap = lc;
        p.EndCap = lc;

        // Now draw a line with the selected caps
        g.DrawLine( p, new Point(20, (i+1)*50), new Point(200, (i+1)*50));
        g.DrawString( lc.ToString(), new Font("Arial", 12), new SolidBrush( Color.DarkBlue), 220, (i+1)*50);
}

The output looks like the following:

Working with Solid Brushes

GDI+ Brush-derived types are used to fill the space between lines with a given color, pattern, or image. Recall that the Brush type is an abstract type that cannot be instantiated directly. Rather, this class serves as a base class for other related brush types such as SolidBrush, HatchBrush, and LinearGradientBrush. System.Drawing also defines two types that return stock or standard brushes - Brushes and SystemBrushes. Using a properly configured brush you are able to call any number of methods such as DrawString() and FillXXX() functions. Recall that you can also create a pen by making use of a brush which allows you to render geometric patterns as you draw lines.

private void TestBrushes( PaintEventArgs e )
{
    Graphics g = e.Graphics;

    SolidBrush br1 = (SolidBrush)Brushes.Moccasin;
    g.FillRectangle( br1, 50, 20, 200, 200);
}

Working with Hatch Style Brushes

HatchBrush allows you to fill a region using a very large number of predefined patterns, represented by the HatchStyle enumeration. When you create a hatch brush you need to specify foreground and background colors:

private void DrawHatchBrushes( PaintEventArgs e )
{
    Graphics g = e.Graphics;

    // Get all members of the HatchStyle enum. Note how this is done
    Array ar = Enum.GetValues( typeof( HatchStyle ) );

    // Loop through the array of HatchStyle values and draw each hatch brush
    for (int i = 0; i < ar.Length; i++)
    {
        // Create brush based on the curent hatch style
        HatchStyle CurrentHS = (HatchStyle)ar.GetValue(i);
        HatchBrush hbr = new HatchBrush( CurrentHS, Color.Blue, Color.Red);

        g.FillRectangle( hbr, 20, (i+1)*40, 100, 20);
    }
}

Working with Textured Brushes

A TextureBrush allows you to attach a bitmap image to a brush, and is typically used in conjunction with a fill operation. A TextureBrush is assigned an Image reference for use during its lifetime. The image is usually either a separate file (.gif, .bmp, .jpg, etc.) or is embedded into a .NET assembly.

private void DrawTextureBrush( PaintEventArgs e )
{
    Graphics g = e.Graphics;

    // Load images
    Image imgBackgroundBrush = new Bitmap( "Water.bmp");
    Image imgString = new Bitmap("Zapotec.bmp");

    // Create texture brushes based on the images
    TextureBrush tbr1 = new TextureBrush( imgBackgroundBrush );
    TextureBrush tbr2 = new TextureBrush( imgString );

    // Draw using the textured brushes
    Rectangle r = this.ClientRectangle;
    g.FillRectangle( tbr1, r );
    g.DrawString( "Hello Texture Brush", new Font("Arial", 60), tbr2, r);
}

Working with Gradient Brushes

The LinearGradientBrush is used whenever you want to blend two colors together in a gradient pattern. The only point of interest is that you need to specify the direction of the blend, using a value from the LinearGradientMode enumeration.

private void DrawLinearGradientBrush(PaintEventArgs e)
{
    Graphics g = e.Graphics;

    // Create a linear gradient brush
    LinearGradientBrush lgb1 = new LinearGradientBrush( new Rectangle( 0,0, 100,100), 
                                                        Color.Blue, Color.Green, 
                                                        LinearGradientMode.BackwardDiagonal );

    // Draw a rectangle with the gradient brush
    g.FillRectangle( lgb1, 50, 50, 400, 100);
}


Rendering Images

System.Drawing.Image class defines a number of methods and properties that hold information regarding the underlying pixel set it represents. In addition, a number of types in System.Drawing.Imaging namespace can be used to facilitate advanced image transformations. In fact, a whole book can be written on the topic of image transformation in GDI+.

Given that the abstract Image class cannot be instantiated, you typically assign objects of type Image to a new instance of the Bitmap class, or simply make a direct instance of the Bitmap type:

Image  img1 = new Bitmap( "Image1.bmp" );
Bitmap img2 = new Bitmap( "Image2.bmp" );
g.DrawImage( img1, 10, 10, 200, 200 );

Dragging and Hit-Testing

While you are free to render bitmap images directly into a Control-derived type, you will gain far greater control and functionality if the bitmap image is rendered in a PictureBox. Because the PictureBox control derives from Control, you inherit lots of functionality such as the ability to capture events for a particular image, assign a tooltip or a context menu, and so forth.

Understanding the .NET Resource Format

Recall that an assembly is a collection of types and optional resources. Bundling external resources into an assembly involves the following steps:

As you might expect, all these steps are done automatically when using Visual Studio.NET IDE.

System.Resource Namespace

The key to understanding the .NET resource format is to know the types defined within System.Resource namespace. This set of types provides the programmatic means to manipulate both .resx and .resource files. The following are the core types in System.Resource namespace:

system.Resource type Meaning
IResourceReader
IResourceWriter
These interfaces are implemented by types that know how to read and write .NET resources. You do not need to implement these interfaces unless you want to write your own custom resource reader/writer.
ResourceReader
ResourceWriter
These classes provide an implementation of IResourceReader and IResourceWriter interfaces. Using these classes you are able to read from and write to binary .resource files.
ResXResourceReader
ResXResourceWriter
These classes provide an implementation of IResourceReader and IResourceWriter interfaces. Using these classes you are able to read from and write to the XML-based .resx files.
ResourceManager Provides easy access to culture-specific resources (BLOBS and strings) at runtime.

Similar to Visual Studio 6, resources such as strings, icons, bitmaps, dialogs, etc. are added to .resx file. To gain access to those resources programmatically use ResourceManager class:

public void foo()
{
    // Open the resource file
    ResourceManager rm = new ResourceManger( typeof(MainForm) );

    // Load string resource
    string strWelcome = rm.GetString("WelcomeString");

    // Load image resource
    Bitmap bmp = rm.GetObject("BackgroundImage");

    ...
}

Working with the ErrorProvider

The ErrorProvider type is used as part of validating user input and is mainly used to provide a visual cue of user input error. To support the ErrorProvider type, you need to understand the following properties of the Control class:

Control Property Meaning
CausesValidation Gets/Sets a value indicating whether entering the control causes validation for all controls that require validation.
Validated Occurs when the control finished performing its validation logic
Validating Occurs when the control is validating user input (i.e., when the Control loses focus)

Focus events occur in the following order:

  1. Enter
  2. GotFocus
  3. Leave
  4. Validating
  5. Validated
  6. LostFocus

If CausesValidation property for a control is set to false, the Validating and Validated event will be suppressed. To validate user input, provide set the CausesValidation property for the control to true and provide handlers for both Validating and Validated events. In the Validating event, check user input and if incorrect, set the Cancel property of the CancelEventArgs to true and set the ErrorProvider object as required. This would cancel all the events that would normally occur after the Validating event. If not error occurs, just return. Validated event will then be called. In there, reset the ErrorProvider error. In the following, assume we have a simple form that has a TextBox called txtName and we wish to validate its value:

private void InitializeComponent()
{
    ...

    txtName.Validating += new System.ComponentModel.CancelEventHandler(this.txtName_Validating);
    txtName.Validated  += new System.EventHandler(this.txtName_Validated);

    ...
}
...

protected void txtName_Validating(object sender, System.ComponentModel.CancelEventArgs e)
{
    Console.WriteLine("txtName_Validating");

    // Validate user input. Must enter 3 letters or more.
    if (txtName.Text.Length < 3)
    {
        errorProvider1.SetError( txtName, "Cannot be less than 3 characters");
        e.Cancel = true; // Validated event will not be fired
    }
}

protected void txtName_Validated(object sender, System.EventArgs e)
{
    Console.WriteLine("txtName_Validated");

    // All conditions have been met. Reset the error provider.
    errorProvider1.SetError( txtName, "");
}

Building Custom Dialog Boxes

A dialog box is nothing more that a stylized Form. In general, all dialog boxes should have set the following properties as follows:

To  configure how dialog box buttons should respond with respect to dialog box processing, you need to set the DialogResult property. When a dialog button has been assigend DialogResult.OK or DialogResult.Cancel, the dialog box will automatically close. Also you can query this property back in the code that launched the dialog box to see which button was clicked.

Assume the main form has a Button control called button1 - when clicked, it displays a dialog box of type Form2:

// Main Form
private void button1_Click(object sender, System.EventArgs e)
{
    Form2 frm2 = new Form2();

    // Show modal dialog box (use frm2.Show() to display a modless dialog box)
    frm2.ShowDialog( this );

    // Get result of user input - OK or Cancel?
    if (frm2.DialogResult == DialogResult.OK)
        MessageBox.Show("You clicke OK");
    else
        MessageBox.Show("You clicke Cancel");
}

// Dialog box form - Form2

public Form2()
{
    ...

    // Center the dialog box with respect to its parent
    StartPosition = FormStartPosition.CenterParent;

    // Configure dialog buttons
    btnClose.DialogResult = DialogResult.OK;
    btnCancel.DialogResult = DialogResult.Cancel;
}

...