Basic and Advanced Graphics via C#
One of the most underrated aspects of C# and the .NET Framework is the advanced graphic capabilities. This article will focus on those capabilities, but will first begin with some of the basics in an effort to present the content sequentially. The advanced student in .NET graphics should overlook this article. But, in my limited knowledge, I will try and explain (by example) how to gain a working knowledge of these graphics capabilities.
For starters, it is common knowledge that to draw on a form, we first create a Graphics
object by calling the System.Windows.Forms.Control.CreateGraphics
method. We then create a Pen
object and call a member of the Graphics
class to draw on the control using the Pen
. By default, pens draw solid lines. To draw a dotted line, set the Pen.DashStyle
property to one of these values:
DashStyle.Dash
DashStyle.DashDot
DashStyle.DashDotDot
DashStyle.Dot
DashStyle.Solid
Here is a code example of these set properties:
namespace PenApp
{
using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Data;
public class MainForm : System.Windows.Forms.Form
{
private System.ComponentModel.Container components;
public MainForm()
{
InitializeComponent();
CenterToScreen();
SetStyle(ControlStyles.ResizeRedraw, true);
}
protected override void Dispose( bool disposing )
{
if( disposing )
{
if (components != null)
{
components.Dispose();
}
}
base.Dispose( disposing );
}
private void InitializeComponent()
{
this.AutoScaleBaseSize = new System.Drawing.Size(4, 13);
this.ClientSize = new System.Drawing.Size(320, 273);
this.Text = "Pens...";
this.Paint += new System.Windows.Forms.PaintEventHandler(this.MainForm_Paint);
}
[STAThread]
static void Main()
{
Application.Run(new MainForm());
}
private void MainForm_Paint(object sender, System.Windows.Forms.PaintEventArgs e)
{
Graphics g = e.Graphics;
Pen p = new Pen(Color.Red, 7);
p.DashStyle = DashStyle.Dot;
g.DrawLine(p, 50, 25, 400, 25);
p.DashStyle = DashStyle.Dash;
g.DrawLine(p, 50, 50, 400, 50);
p.DashStyle = DashStyle.DashDot;
g.DrawLine(p, 50, 75, 400, 75);
p.DashStyle = DashStyle.DashDotDot;
g.DrawLine(p, 50, 100, 400, 100);
p.DashStyle = DashStyle.Solid;
g.DrawLine(p, 50, 125, 400, 125);
}
}
}
Result:
For most of the Draw
methods, the Graphics
class also has Fill
methods that draw a shape and fill the contents. These methods work exactly like the Draw
methods, except they require an instance of the Brush
class instead of the Pen
class. The Brush
class is abstract
, so you must instantiate one of the child classes:
System.Drawing.Drawing2D.HatchBrush
System.Drawing.Drawing2D.LinearGradientBrush
System.Drawing.Drawing2D.PathGradientBrush.
System.Drawing.SolidBrush
System.Drawing.TextureBrush
This very basic code example below draws a solid, maroon, five-sided polygon:
using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Data;
public class MainForm : System.Windows.Forms.Form
{
private System.ComponentModel.Container components;
public MainForm()
{
InitializeComponent();
CenterToScreen();
SetStyle(ControlStyles.ResizeRedraw, true);
}
protected override void Dispose( bool disposing )
{
if( disposing )
{
if (components != null)
{
components.Dispose();
}
}
base.Dispose( disposing );
}
private void InitializeComponent()
{
this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);
this.ClientSize = new System.Drawing.Size(292, 273);
this.Text = "Fun with graphics";
this.Paint +=
new System.Windows.Forms.PaintEventHandler(this.MainForm_Paint);
}
[STAThread]
static void Main()
{
Application.Run(new MainForm());
}
private void MainForm_Paint(object sender,
System.Windows.Forms.PaintEventArgs e)
{
Graphics g = this.CreateGraphics();
Brush b = new SolidBrush(Color.Maroon);
Point [] points = new Point[]
{new Point(10, 10),
new Point(10, 100),
new Point(50, 65),
new Point(100, 100),
new Point(85, 40)};
g.FillPolygon(b, points);
}
}
Result:
General Paths
Our next example demonstrates the use of a general path. A general path is a shape constructed from straight lines and complex curves. An object of class GraphicsPath
(namespace System.Drawing.Drawing2D
) represents a general path. The GraphicsPath
class provides functionality that enables the creation of complex shapes from vector-based primitive graphics objects. A GraphicsPath
object consists of figures defined by simple shapes. The start point of each vector-graphics object (such as a line or arc) that is added to the path is connected by a straight line to the end point of the previous object. When called, the CloseFigure
method attaches the final vector-graphic object end point to the initial starting point for the current figure by a straight line, then starts a new figure. The method StartFigure
begins a new figure within the path without closing the previous figure.
So the program below draws general paths in the shape of five-pointed stars. We define two int
arrays, representing the x- and y-coordinates of the points in the star, to then define a GraphicsPath
object star. A loop then creates lines to connect the points of the star and adds these lines to the star. We use the GraphicsPath
method AddLine
to append a line to the shape. The arguments of AddLine
specify the coordinates for the line's endpoints; each new call to AddLine
adds a line from the previous point to the current point. We use the GraphicsPath
method CloseFigure
to complete the shape.
Afterwards, we set the origin of the Graphics
object. The arguments to the method TranslateTransform
indicate that the origin should be translated to the coordinates (150, 150). The loop in lines draws the star 18 times, rotating it around the origin. The Graphics
method RotateTransform
is called to move to the next position on the form; the argument specifies the rotation angle in degrees. The Graphics
method FillPath
then draws a filled version of the star with the Brush
created. The application determines the SolidBrush
's color randomly, using the Random
method Next
.
using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;
public class DrawStarsForm : Form
{
public DrawStarsForm()
{
InitializeComponent();
}
private System.ComponentModel.IContainer components = null;
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
private void InitializeComponent()
{
this.SuspendLayout();
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(313, 302);
this.Name = "DrawStarsForm";
this.Text = "Drawing Stars";
this.Paint += new System.Windows.Forms.PaintEventHandler(
this.DrawStarsForm_Paint);
this.ResumeLayout(false);
}
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.Run(new DrawStarsForm());
}
private void DrawStarsForm_Paint(object sender, PaintEventArgs e)
{
Graphics graphicsObject = e.Graphics;
Random random = new Random();
SolidBrush brush = new SolidBrush( Color.DarkMagenta );
int[] xPoints = { 55, 67, 109, 73, 83, 55, 27, 37, 1, 43 };
int[] yPoints = { 0, 36, 36, 54, 96, 72, 96, 54, 36, 36 };
GraphicsPath star = new GraphicsPath();
for ( int i = 0; i <= 8; i += 2 )
star.AddLine( xPoints[ i ], yPoints[ i ],
xPoints[ i + 1 ], yPoints[ i + 1 ] );
star.CloseFigure();
graphicsObject.TranslateTransform( 150, 150 );
for ( int i = 1; i <= 18; i++ )
{
graphicsObject.RotateTransform( 20 );
brush.Color = Color.FromArgb(
random.Next( 200, 255 ), random.Next( 255 ),
random.Next( 255 ), random.Next( 255 ) );
graphicsObject.FillPath( brush, star );
}
}
}
Result:
The next example sort of integrates some of the concepts discussed thus far. In C#, all linear gradients are defined along a line that determines the gradient endpoints. This line can be specified either by the starting and ending points, or by the diagonal of a rectangle. The first argument, Rectangle drawArea1
, represents the endpoints of the linear gradient: the upper-left corner is the starting point and the bottom-right corner is the ending point. The second and third arguments specify the colors that the gradient will use. In this case, the color of the ellipse will gradually change from Color.Blue
to Color.Yellow
. The last argument, a type from the enumeration LinearGradientMode
, specifies the linear gradient's direction. In our case, we use LinearGradientMode.ForwardDiagonal
, which creates a gradient from the upper-left to the lower-right corner. We then use the Graphics
method FillEllipse
to draw an ellipse with linearBrush
; the color gradually changes from blue to yellow, as described above. We create a Pen
object thickRedPen
. We pass to thickRedPen
's constructor Color.Red
an int
argument 10, indicating that we want thickRedPen
to draw red lines that are 10 pixels wide.
We then create a new Bitmap
image, which initially is empty. The class Bitmap
can produce images in color and gray scale; this particular Bitmap
is 10 pixels wide and 10 pixels tall. The method FromImage
is a static member of the Graphics
class, and retrieves the Graphics
object associated with an Image
, which may be used to draw on an image. Lines 52-65 draw on the Bitmap
a pattern consisting of black, blue, red, and yellow rectangles and lines. A TextureBrush
is a brush that fills the interior of a shape with an image, rather than a solid color. In line 71, the TextureBrush
object, textureBrush
, fills a rectangle with our Bitmap
. The TextureBrush
constructor used takes as an argument an image that defines its texture.
Next, we draw a pie-shaped arc with a thick white line. We then set coloredPen
's color to White
and modify its width to be 6 pixels. We then draw the pie on the form by specifying the Pen
, the x-coordinate, y-coordinate, width and height of the bounding rectangle, and the start and sweep angles.
Going further, we draw a five-pixel-wide green line. Finally, we use the enumerations DashCap
and DashStyle
(namespace System.Drawing.Drawing2D
) to specify settings for a dashed line. We then follow to set the DashCap
property of coloredPen
(not to be confused with the DashCap
enumeration) to a member of the DashCap
enumeration. The DashCap
enumeration specifies the styles for the start and end of a dashed line. In this case, we want both ends of the dashed line to be rounded, so we use DashCap.Round
. We then set the DashStyle
property of coloredPen
(not to be confused with the DashStyle
enumeration) to DashStyle.Dash
, indicating that we want our line to consist entirely of dashes:
using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;
public partial class DrawShapesForm : Form
{
public DrawShapesForm()
{
InitializeComponent();
}
private System.ComponentModel.IContainer components = null;
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
private void InitializeComponent()
{
this.SuspendLayout();
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(449, 188);
this.Name = "DrawShapesForm";
this.Text = "Drawing Shapes";
this.Paint += new System.Windows.Forms.PaintEventHandler(
this.DrawShapesForm_Paint);
this.ResumeLayout(false);
}
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.Run(new DrawShapesForm());
}
private void DrawShapesForm_Paint( object sender, PaintEventArgs e )
{
Graphics graphicsObject = e.Graphics;
Rectangle drawArea1 = new Rectangle( 5, 35, 30, 100 );
LinearGradientBrush linearBrush =
new LinearGradientBrush( drawArea1, Color.Blue,
Color.Yellow, LinearGradientMode.ForwardDiagonal );
graphicsObject.FillEllipse( linearBrush, 5, 30, 65, 100 );
Pen thickRedPen = new Pen( Color.Red, 10 );
Rectangle drawArea2 = new Rectangle( 80, 30, 65, 100 );
graphicsObject.DrawRectangle( thickRedPen, drawArea2 );
Bitmap textureBitmap = new Bitmap( 10, 10 );
Graphics graphicsObject2 =
Graphics.FromImage( textureBitmap );
SolidBrush solidColorBrush =
new SolidBrush( Color.Red );
Pen coloredPen = new Pen( solidColorBrush );
solidColorBrush.Color = Color.Yellow;
graphicsObject2.FillRectangle( solidColorBrush, 0, 0, 10, 10 );
coloredPen.Color = Color.Black;
graphicsObject2.DrawRectangle( coloredPen, 1, 1, 6, 6 );
solidColorBrush.Color = Color.Blue;
graphicsObject2.FillRectangle( solidColorBrush, 1, 1, 3, 3 );
solidColorBrush.Color = Color.Red;
graphicsObject2.FillRectangle( solidColorBrush, 4, 4, 3, 3 );
TextureBrush texturedBrush =
new TextureBrush( textureBitmap );
graphicsObject.FillRectangle( texturedBrush, 155, 30, 75, 100 );
coloredPen.Color = Color.White;
coloredPen.Width = 6;
graphicsObject.DrawPie( coloredPen, 240, 30, 75, 100, 0, 270 );
coloredPen.Color = Color.Green;
coloredPen.Width = 5;
graphicsObject.DrawLine( coloredPen, 395, 30, 320, 150 );
coloredPen.Color = Color.Yellow;
coloredPen.DashCap = DashCap.Round;
coloredPen.DashStyle = DashStyle.Dash;
graphicsObject.DrawLine( coloredPen, 320, 30, 395, 150 );
}
}
Result:
Image Processing
The processing of an image consists of modifying its pixels based on certain mathematical operations. Each pixel is encoded using three integer values, one for red, one for green, and one for blue. The range of values depends on the number of bits per pixel. As of the writing of this paper, the best quality possible is obtained with 24 bits per pixel, a value between 0 and 255 for each component, which means a little over 16 million colors. The example below show image processing by involving the inversion of colors. For example, suppose that the number of bits per pixel is 24. The inversion of a color consists of assigning the 255-complement to each of the three components, for each pixel of the image. Run the code, notice the image, and then the colors will be inverted:
using System;
using System.Threading;
using System.Drawing;
using System.Windows.Forms;
using System.Drawing.Imaging;
using System.ComponentModel;
using System.Drawing.Drawing2D;
using System.Data;
public class MainForm : Form
{
private System.ComponentModel.Container components;
public MainForm()
{
InitializeComponent();
CenterToScreen();
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
if (components != null)
{
components.Dispose();
}
}
base.Dispose( disposing );
}
private void InitializeComponent()
{
this.AutoScaleBaseSize= new System.Drawing.Size(5, 13);
this.ClientSize = new System.Drawing.Size(292, 272);
this.Text = "Picture.jpg";
this.Paint += new System.Windows.Forms.PaintEventHandler(
this.MainForm_Paint);
}
[STAThread]
static void Main() {
Application.Run(new MainForm());
}
private void MainForm_Paint(object sender, PaintEventArgs e)
{
using ( Graphics g = CreateGraphics() ) {
Bitmap m_Bmp = new Bitmap(@"C:\garden.jpg");
g.DrawImage( m_Bmp, new Point(5,5) );
Thread.Sleep(1000);
int width = m_Bmp.Width;
int height = m_Bmp.Height;
Color cSrc, cDest;
for (int y = 0; y < height; y++)
for (int x = 0; x < width; x++) {
cSrc = m_Bmp.GetPixel(x, y);
cDest = Color.FromArgb( 255-cSrc.R, 255-cSrc.G, 255-cSrc.B);
m_Bmp.SetPixel(x, y, cDest);
}
g.DrawImage(m_Bmp, new Point (5, 5));
}
}
}
Here is the initial image:
Here is the image after the processing. Just let the image be and the colors will invert:
Buffered Graphics
The BufferedGraphics
class allows you to implement custom double buffering for your graphics. It provides a wrapper for a graphics buffer, along with methods that you can use to write to the buffer and render its contents to an output device. Graphics that use double buffering can reduce or eliminate flicker that is caused by redrawing a display surface. When you use double buffering, updated graphics are first drawn to a buffer in memory, and the contents of this buffer are then quickly written to some or the entire displayed surface. This relatively brief overwrite of the displayed graphics typically reduces or eliminates the flicker that sometimes occurs when graphics are updated. The BufferedGraphics
class has no public constructor, and must be created by the BufferedGraphicsContext
for an application domain, using its Allocate
method. You can retrieve the BufferedGraphicsContext
for the current application domain from the static BufferedGraphicsManager.Current
property:
using System;
using System.ComponentModel;
using System.Drawing;
using System.Windows.Forms;
namespace BufferingExample
{
public class BufferingExample : Form
{
private BufferedGraphicsContext context;
private BufferedGraphics grafx;
private byte bufferingMode;
private string[] bufferingModeStrings =
{
"Draw to Form without OptimizedDoubleBufferring control style",
"Draw to Form using OptimizedDoubleBuffering control style",
"Draw to HDC for form" };
private System.Windows.Forms.Timer timer1;
private byte count;
public BufferingExample() : base()
{
this.Text = "User double buffering";
this.MouseDown += new MouseEventHandler(this.MouseDownHandler);
this.Resize += new EventHandler(this.OnResize);
this.SetStyle( ControlStyles.AllPaintingInWmPaint |
ControlStyles.UserPaint, true );
timer1 = new System.Windows.Forms.Timer();
timer1.Interval = 200;
timer1.Tick += new EventHandler(this.OnTimer);
bufferingMode = 2;
count = 0;
context = BufferedGraphicsManager.Current;
context.MaximumBuffer = new Size(this.Width+1, this.Height+1);
grafx = context.Allocate(this.CreateGraphics(),
new Rectangle( 0, 0, this.Width, this.Height ));
DrawToBuffer(grafx.Graphics);
}
private void MouseDownHandler(object sender, MouseEventArgs e)
{
if( e.Button == MouseButtons.Right )
{
if( ++bufferingMode > 2 )
bufferingMode = 0;
if( bufferingMode == 1 )
this.SetStyle( ControlStyles.OptimizedDoubleBuffer, true );
if( bufferingMode == 2 )
this.SetStyle( ControlStyles.OptimizedDoubleBuffer, false );
count = 6;
DrawToBuffer(grafx.Graphics);
this.Refresh();
}
else
{
if( timer1.Enabled )
timer1.Stop();
else
timer1.Start();
}
}
private void OnTimer(object sender, EventArgs e)
{
DrawToBuffer(grafx.Graphics);
if( bufferingMode == 2 )
grafx.Render(Graphics.FromHwnd(this.Handle));
else
this.Refresh();
}
private void OnResize(object sender, EventArgs e)
{
context.MaximumBuffer = new Size(this.Width+1, this.Height+1);
if( grafx != null )
{
grafx.Dispose();
grafx = null;
}
grafx = context.Allocate(this.CreateGraphics(),
new Rectangle( 0, 0, this.Width, this.Height ));
count = 6;
DrawToBuffer(grafx.Graphics);
this.Refresh();
}
private void DrawToBuffer(Graphics g)
{
if( ++count > 5 )
{
count = 0;
grafx.Graphics.FillRectangle(Brushes.Black, 0, 0,
this.Width, this.Height);
}
Random rnd = new Random();
for( int i=0; i<20; i++ )
{
int px = rnd.Next(20,this.Width-40);
int py = rnd.Next(20,this.Height-40);
g.DrawEllipse(new Pen(Color.FromArgb(rnd.Next(0, 255), rnd.Next(0,255),
rnd.Next(0,255)), 1), px, py,
px+rnd.Next(0, this.Width-px-20),
py+rnd.Next(0, this.Height-py-20));
}
g.DrawString("Buffering Mode: "+bufferingModeStrings[bufferingMode],
new Font("Arial", 8), Brushes.White, 10, 10);
g.DrawString("Right-click to cycle buffering mode",
new Font("Arial", 8), Brushes.White, 10, 22);
g.DrawString("Left-click to toggle timed display refresh",
new Font("Arial", 8), Brushes.White, 10, 34);
}
protected override void OnPaint(PaintEventArgs e)
{
grafx.Render(e.Graphics);
}
[STAThread]
public static void Main(string[] args)
{
Application.Run(new BufferingExample());
}
}
}
Result:
The Graphics
property can be used for drawing to the graphics buffer. This property provides access to the Graphics
object that draws to the graphics buffer allocated for this BufferedGraphics
object. The Render
method with no arguments draws the contents of the graphics buffer to the surface specified when the buffer was allocated. Other overloads of the Render
method allow you to specify a Graphics
object or an IntPtr
object that points, or rather makes reference to, a device context to which to draw the contents of the graphics buffer.
References
- The MSDN Library - Graphics section.