Your canvas: the Graphics Class The Control class is at the center of Windows Forms programming; you place a Control, you set certain attributes of it, you associate it with business l
Trang 115: GDI+ Overview
While Windows Forms provides a great basis for the large majority of user interfaces, the NET Framework allows access to the full rendering capabilities of Windows XP Windows Forms interfaces are based on the concept of
Controls that, among other things, know how to draw
themselves If your interface requires drawing that’s
beyond the capabilities of the Controls at your disposal,
you’ll need to turn to NET’s GDI+ namespaces
GDI+ provides a range of drawing, coordinate, and measurement tools ranging from simple line-drawing to complex gradient fills The advantage of this is that virtually any kind of interface can be created using GDI+ (3D interfaces require DirectX, which will be discussed later) The disadvantage is that GDI+ is stateless
and you must write code capable of re-rendering the entire GDI+ interface at any
time Most GDI+ work will also involve writing custom input code
The amount of detail involved in handling redraws and input means that you must pay even more attention to separating domain logic from your interface code It’s difficult for the FEC architecture to handle the complexity of a GDI+ interface PAC should still be where the discussion begins, but the power of MVC can become more attractive as one contemplates building UIs with innovative
display or input characteristics The sample code in this chapter does not
separate domain logic from display and should not be used as a starting place for your designs
Your canvas: the Graphics Class
The Control class is at the center of Windows Forms programming; you place a
Control, you set certain attributes of it, you associate it with business logic All
of these hold true in GDI+ programs, except that you will be responsible for
drawing everything within the client area of your control Typicallly, you will
create a new class inheriting from Panel, and define your own properties to
control your object’s appearance You’ll sometimes hear people referring to this
process as developing an owner-draw control
Trang 2660 Thinking in C# www.MindView.net
The canvas on which you draw is an instance of the Graphics class This class
encapsulates the GDI+ drawing surface for your Control You do not have to
worry about other windows (or even other Controls), screen location, and so
forth You can still use properties such as Dock, Anchor, and Position to
handle the task of placing your custom Control within a general Windows
Forms interface
Every instance of Graphics that you use consumes a low-level operating system
resource (a Win32 handle) This leads to two restrictions:
♦ You must always call Dispose( ) on a Graphics object when you are
done with it; you can either do this in a try…finally block or with the
using keyword
♦ You must not maintain a reference to a Graphics( ) object outside of the
event handler which obtained it, as the underlying handle is not
guaranteed to be valid over time
There are several ways to obtain a reference to a Graphics object The most
direct is to call Control.CreateGraphics( ), a method whose name highlights
the transient nature of the resulting object This example places two buttons on a
Form When the controller is clicked, it gets a reference to a Graphics object
for the target and fills the target’s client area with red
target = new Button();
target.Location = new Point(10, 10);
Controls.Add(target);
Button controller = new Button();
controller.Location = new Point(10, 60);
controller.Text = "Clear target Graphics";
controller.Width = 150;
controller.Click += new EventHandler(OnClick);
Trang 3Controls.Add(controller);
}
public void OnClick(object src, EventArgs ea){
Graphics canvas = target.CreateGraphics();
argument
Understanding repaints
The GraphicsHandle sample can illustrate some Windows behavior that can be
confusing Run the program and press the “Clear target Graphics” button The
target button will disappear, replaced by a red rectangle Now minimize or
otherwise obscure the GraphicsHandle application and then uncover it The
red rectangle is now replaced by the appearance of the normal button So far, this
seems logical: When the target button is redrawn, it draws itself as a button, when the controller button is clicked, the Clear(Color.Red) call temporarily
replaces the button’s “real” appearance
Now press “Clear target Graphics” and move the application window around the screen; the red rectangle remains This might make you go “Hmm…,” since moving a window involves turning pixels on and off, i.e., repainting Why doesn’t the button redraw itself in its normal way?
Now do something that partially obscures the red rectangle (move a window
edge over the control, or move the GraphicsHandle demo off the edge of the screen) and then uncover it Now you’ll see that the portion of the target button
that was obscured gets repainted as a normal button, while the portion that was not obscured remains a red rectangle What’s going on?
Trang 4662 Thinking in C# www.ThinkingIn.NET
The answer lies in the underlying Windows system for controlling the display
Essentially, Windows tries to avoid asking for a repaint If the top-level window is
being moved, Windows doesn’t ask for a repaint at all, it just moves the pixels in
the display card’s memory If a window is partially obscured and then revealed,
Windows only repaints the affected area If Windows used a different
architecture, in which the entire client area was repainted, applications would
show noticeable flicker even on fast machines
This underlying argues strongly for not grabbing another Controls Graphics
context, drawing on it, and then disposing it; any drawing that you do in this
manner is, as shown in the GraphicsHandle demo, temporary
Control: paint thyself
In Windows Forms, the Paint event triggers the redrawing of the client area All
Controls have a protected OnPaint( ) method which is responsible for
rendering This is the preferred method for creating an owner-drawn control –
inherit from an existing control and override OnPaint( ) This example shows a
custom Panel that draws a sine wave from individual pixels
static int drawCount = 0;
protected override void OnPaint(PaintEventArgs e){
Trang 5for (double d = 0; d < Math.PI * 4; d += inc) {
double sin = Math.Sin(d);
int y = (int) (this.Height / 2 * sin);
Trang 6664 Thinking in C# www.MindView.net
downside to setting this property to true is that the Control is much more likely
to flicker during a resizing operation than if it is left at its default false value
When you override Control.OnXxx( ) methods such as OnPaint( ), you
should always have the first line in your method call base.OnXxx( ) in order to
assure that all the vdelegates attached to the event get called After calling
base.OnPaint( ), the first order of business is getting a reference to a
Graphics Instead of calling Control.CreateGraphics( ), an appropriate
Graphics comes in as part of the PaintEventArgs You do not have to worry
about disposing of this Graphics at the end of the method (the Windows Forms
infrastructure calls its Dispose( ) method at the appropriate time)
To draw lines on a Graphics, you use an instance of the Pen class Pen’s have
various properties to control their appearance, but a Pen without a Color is
meaningless, so you must specify a Color in the Pen( ) constructor (A shortcut
for a simple pen of 1-pixel width with a predefined Color such as is used in this
demo would be to use the Pens class: Pens.Red or Pens.Blue.)
The first time OwnerDraw.OnPaint( ) is called, the Pen used is blue,
subsequent paintings use a red one The next several lines of OnPaint( ) specify
the sine wave: We’re interested in drawing two sine wave cycles, and we want to
draw the sine wave value at each pixel in the Control’s Width So the inc
variable holds the amount by which we’ll count from 0 to 4π radians The value
returned from Math.Sin( ) varies from -1 to 1 In order to fit these to the client
area, the result is multipled by half the height and then half the height added to
the result This scales and transforms the values to fit in the client area (we’ll talk
about more efficient ways to do such steps later in the chapter)
We wish to draw a dot for each value we calculate, not a connected line We
accomplish this by specifying a Rectangle that is 1 unit in size at the calculated x
and y coordinates
The methods used for drawing betray how close GDI+ is to the underlying
operating system The Graphics.DrawXxx( ) methods are primitives, each one
is implemented in some specialized, speed-optimized manner at the operating
system level This is also true of the Graphics.FillXxx( ) methods that will be
discussed shortly
In this case, the drawing is done with a call to Graphics.DrawRectangle( )
that takes the Pen and the Rectangle calculated previously Once the rectangle
is drawn, we increment the value of x and continue the loop
Trang 7The SineWave( ) constructor first creates and places a blank Panel and a
Splitter that are set to DockStyle.Left The OwnerDraw is then set to DockStyle.Fill When run, the Panel p will obscure the first part of the
OwnerDraw’s client area: since OwnerDraw has no knowledge of the
Splitter, the OwnerDraw actually fills the SineWave’s entire client area, p
just obscures it If you drag the Splitter to the left, you’ll see more of the
OwnerDraw come into view, but only the just-revealed portion will be drawn in
red, as Windows will avoid repainting the still-exposed portion of the
OwnerDraw
Now, grab a corner of the SineWave application and resize it On some
computers, you’ll see a flicker during redraw, but the console output will
demonstrate that this is because OnPaint( ) is constantly being called You’ll
also see a large number of repaints if you take another window and drag it over
the SineWave application
Scaling and transforms
One thing that may have taken you aback when running SineWave is that the
sine wave appears inverted – instead of starting at 0 and rising, it starts at 0 but
moves towards the bottom of the SineWave Form This is because Windows Forms default coordinate system is like that of a typewriter: x increases from
right to left and y increases from the top to the bottom of the page:
Figure 15-1: The default coordinate system of Windows Forms
If we wanted to have our sine wave appear so that positive is towards the top of
the Form and negative towards the bottom, we could add the line
y *= -1;
to our calculations Similarly, if instead of reaching all the way to the top and
bottom, we wanted to consume only 90% of the space, we could use y *= -0.9f
instead If we wanted to combine this inversion and scaling with the
transformation we need to make negative numbers appear, we could write:
y = -0.9f *(Height / 2 + Sin(d));
x
y
Trang 8666 Thinking in C# www.ThinkingIn.NET
Naturally, we could do similar math with the x coordinate Or we could use the
Graphics.ScaleTransform( ) to automatically do the multiplication for all
values written to the context and Graphics.TranslateTransform( ) to
automatically add some value to all values written to the context
This example uses these two methods to work directly with the values returned by
Pen pen = new Pen(Color.Red);
float widthScale = (float)
(Width / (Math.PI * 4));
float heightScale = Height / 2;
float invertHeightScale = -heightScale;
PointF lastPoint = new PointF(0f, 0f);
double inc = Math.PI * 4 / Width;
for (float f = 0; f < Math.PI * 4; f += 1f) {
Trang 9float sin = (float) Math.Sin(f);
PointF newPoint = new PointF(f, sin);
g.DrawLine(pen, lastPoint, newPoint);
Since we know that we’re interested in drawing two cycles (4π radians) of the sine
wave, we know that the resolution of our graph is Width / 4π We calculate this value as widthScale in the OnPaint( ) method of our TransformPanel
Similarly, we know that since the sine values range from -1 to 1, multiplying those
values by ½ the Height will end up consuming the entire vertical space of the
Control This is the heightScale value that’s calculated; invertHeightScale is
the negative of that value (as we want positive numbers to be closer to the top of
the Form) Finally, we multiply the invertHeightScale by 9, so that instead of
taking up the entire vertical height, the results will consume 90% of the height
The Pen.Width of the pen we’re using begins with a default value of 1
Graphics.TransformScale( ) works on everything in the context, though, so a
Pen with Width = 1 will draw a line Width / 4π pixels wide! Therefore, we set Pen.Width = 1 / widthScale, which brings it back to being one pixel
Trang 10668 Thinking in C# www.MindView.net
g.ScaleTransform(widthScale, invertHeightScale);
multiplies all x values by widthScale and all y values by invertHeightScale
Transforms applied to the Graphics are cumulative (but obviously do not persist
between one call to OnPaint( ) and the next, as every time you are dealing with
a new Graphics object) You can reset to the default, no-rotation, no-translation,
transform (the identity transform) by calling Graphics.ResetTransform( )
Although transforms are cumulative, they are generally order-dependent (that is,
translating and then rotating will have a different effect than rotating then
translating) The mathematics of transforms will be covered in more detail a bit
later
Now that we’re dealing with a scaled Graphics, we can no longer use integers to
specify Points on the canvas The Point(1, 1) is at the top and more than 1/12th
of the way across the form Instead, we switch to the PointF structure, which
allows us to specify locations in floating point
Before entering our sine-calculating loop, we initialize Point lastPoint to the
origin Then, our loop increments f from 0 to 4 π in increments of 1/10th The
sine of f is calculated and f and sin are used directly to initialize a PointF value
If you stretch the original SineWave example, it breaks up into individual
values; SineLine uses Graphics.DrawLine( ) to connect the individual values
as they’re calculated
It may seem to you that SineLine is not superior to SineWave, which may be
true, but this example shows how transforms can dramatically reduce code
PointF[] pointer = new PointF[]{
new PointF(0, 0), new PointF(.1f, 05f),
Trang 11new PointF(.09f, 2f), new PointF(.1f, 5f),
new PointF(.02f, 1f), new PointF(-.02f, 1f), new PointF(-.1f, 5f), new PointF(-.09f, 2f), new PointF(-.1f, 05f), new PointF(0, 0),
};
private float scale = 5f;
public int PointerScale{
get { return(int) (scale * 100);}
set { scale = (float) value / 100;}
}
private int rot = 90;
public int PointerRotation{
get { return rot;}
set { rot = value;}
Pen p = new Pen(Color.Red);
p.Width = 1 / scaleTransform;
g.DrawCurve(p, pointer);
Trang 13}
public void OnScaleChange(object src, EventArgs a){
int scale = scaler.Value;
bs.PointerScale = scale;
bs.Invalidate();
}
public void OnSpinChange(object src, EventArgs a){
int angle = spinner.Value;
shape
OnPaint( ) calls base.OnPaint( ) (as should always be done), clears the
canvas, and calculates the desired offset of the origin halfway across and down
the BottleSpinner Three transforms are then applied:
TranslateTransform( ) sets the origin, ScaleTransform( ) sets the
y-axis to increase towards the top of the screen, and RotateTransform( ) sets the rotation equal to the value of the rot variable Surprisingly,
RotateTransform( ) takes an angle in degrees, not radians
After these transforms are applied, the real scaling transform is calculated from
the value of the scale variable and the size of the BottleSpinner panel
ScaleTransform( ) is called again; since transforms are additive, this scaling
transform works with the previous ScaleTransform( ) that flipped the y axis A new Pen is created and its width set à la DemoLine
Finally, Graphics.DrawCurve( ) is used to draw the shape DrawCurve( )
draws cardinal splines, a smooth curve that passes through all the points in the
passed-in array
Trang 14672 Thinking in C# www.MindView.net
The SpinTheBottle Form contains both a BottleSpinner panel and two
TrackBar controls TrackBars, also called “slider” controls, are used to
manipulate an integer value in a specified range In this case, we specify that the
scaler can have a range of 1 to 100 and the spinner a range of 0 to 360 Both
have ValueChanged delegates that set the corresponding property in the
BottleSpinner bs and then call Invalidate( ), which triggers the OnPaint( )
event of our custom control
Filling regions
So far, we have just used lines to draw on our Graphics context Generally, in
addition to (or in place of) drawing an outline, you’ll want to fill a region Just as
lines are drawn with a series of DrawXxx( ) methods in the Graphics class,
fills are drawn with a series of FillXxx( ) methods However, instead of using a
Pen as the drawing tool, the FillXxx( ) methods use a Brush This example
contrasts drawing and filling:
class RegionFill : Form {
protected override void OnPaint(PaintEventArgs e){
Trang 15public static void Main(){
Application.Run(new RegionFill());
}
}///:~
Unlike previous examples, the owner-drawn Control in RegionFill is
descended from Form, not Panel This allows the sample programs to be
slightly shorter, at the cost of losing any claim to decent object design The
DrawRectangle call uses a Pen from the Pens class and FillRectangle uses a Brush from the corresponding Brushes class
The Pen pointer uses the LineCap enumeration that is part of the
System.Drawing.Drawing2D namespace to add an arrow to lines drawn with
the Pointer Two lines are drawn to bracket the 110th pixel in the Form When you run RegionFill, you’ll see that the green rectangle is not entirely covered by
the red fill even though both are given the same extents; edges of the green
rectangle are still visible The lines drawn by the pointer indicate that the
DrawXxx( ) methods draw the boundary specified (in this case, [{10, 10}, {110,
110}]), while the FillXxx( ) methods draw the interior (what is filled is [{9, 9},
{109, 109}])
Although the Pens and Brushes classes are convenient, they do not expose an important feature of GDI+’s color model The Color structure encapsulates a 32-
bit color representation that includes an 8-bit transparent component (also
known as an alpha channel) in addition to 8-bit components for each of the Red,
Green, and Blue components.1 An alpha value of 255 corresponds to a totally opaque color, while a value of 0 is totally transparent In this example, we create
alphaGreen, a somewhat transparent green, create a new Brush of that color,
and overlay a rectangle filled with alphaGreen on a rectangle with Color.Red
1 While there are many color models, RGB is the dominant one for computer graphics, as
it corresponds to the display components in monitors The Color structure has methods
to convert between RGB and Hue-Saturation-Brightness, a color model more popular with graphics designers
Trang 16674 Thinking in C# www.ThinkingIn.NET
class AlphaFill : Form {
protected override void OnPaint(PaintEventArgs e){
The line where the Brush is instantiated contains an upcast from SolidBrush
to Brush The next example illustrates all but one of the other subtypes of the
abstract Brush class:
class BrushFill : Form {
protected override void OnPaint(PaintEventArgs e){
base.OnPaint(e);
Graphics g = e.Graphics;
ClientSize = new Size(250, 250);
BackColor = Color.White;
Image img = Image.FromFile("images.jpg");
Brush tBrush = new TextureBrush(img);
g.FillRectangle(tBrush, 10, 10, 100, 100);
Trang 17Brush hBrush =
new HatchBrush(HatchStyle.DiagonalCross,
Color.Black, Color.White);
g.FillRectangle(hBrush, 30, 90, 100, 100);
Point startGradient = new Point(10, 120);
Point endGradient = new Point(110, 220);
Background properties
The LinearGradientBrush creates a smooth blend from one Color to another, from one Point to another A LinearGradientBrush has a large number of
properties to fine-tune the way the gradient is constructed The
LinearGradientBrush constructs a logical gradient between two Points
These Points need not be within the actual region being filled
The only type of Brush not yet discussed is the PathGradientBrush which, like the LinearGradientBrush, is used to fill a region with a smooth blend of two colors However, while the LinearGradientBrush creates a blend based on two logical Points, the PathGradientBrush creates a blend based on the center and boundaries of a GraphicsPath
A GraphicsPath is a series of connected lines and curves The GraphicsPath used by the PathGradientBrush is considered to be closed (the last point on
the path is considered connected to the first point on the path), so even if you create a path from just two lines, for the purposes of the gradient, the path will be
Trang 18676 Thinking in C# www.MindView.net
a wide variety of properties that can fine-tune the creation of the gradient, but
this example demonstrates a basic GraphicsPath and a basic
class PathGradientDemo : Form {
protected override void OnPaint(PaintEventArgs ea){
pgb.SurroundColors = new Color[]{
Color.Red, Color.Green, Color.Blue};
The GraphicsPath path is a right triangle with legs of length 250; one leg and
the hypotenuse are added explicitly, while we count on the
PathGradientBrush to implicitly derive the third edge We specify that we
want a gradient with a khaki center The
PathGradientBrush.SurroundColors property specifies an array of colors
corresponding to the endpoints of the components of the GraphicsPath The
color at any given point is a blend between the CenterColor and the two
SurroundColors corresponding to the nearest points in the GraphicsPath
Trang 19Finally, to show the gradient, we use FillRectangle( ) Although the fill is for a rectangle, the GraphicsPath is triangular, so the appearance of the gradient is a
triangle with a khaki center and red, green, and blue vertices
Non-rectangular windows
Many multimedia applications have customizable interfaces (“skins”) that
prominently feature non-rectangular shapes Programming this type of interface has traditionally required some pretty hard-core low-level stuff, but Windows Forms and GDI+ combine to make customized control shapes very simple Each
Control has a Region property that can be set to a Region containing a
GraphicsPath The GraphicsPath determines the shape of the Control
Since a Form is itself a Control, this can be used to create custom-shaped
“through” your application and activates the application you’re running on top of
In order to create a “skinned” application, you would create a resource file (perhaps in XML) describing the graphics paths of all the customizable controls and their back- and foreground-colors, fonts, and so forth To change the skin,
Trang 20678 Thinking in C# www.ThinkingIn.NET
you’d simply create new GraphicsPaths and assign them to the appropriate
controls
Matrix transforms
GraphicsPath objects can be transformed, independently of the Graphics
transforms by using the Matrix class To understand the Matrix transforms,
you must understand a small amount of matrix math
An affine transformation is a rotation around the origin followed by a translation
and is represented in matrix notation as:
yScale Sin(θ) Cos(θ)xScale
Cos(θ)yScale xScale
-Sin(θ)
Figure 15-2: The elements of an affine transformation
This transformation would be expressed in this code:
newX = Math.Cos(theta) * xScale * x
The final column in an affine matrix is always the same You can see how the
newX value is derived by multiplying the first column of the 2-by-1 matrix (i.e.,
x) by each of the values in the first column of the 3-by-3 matrix, and then
summing those results Similarly, newY is derived by summing the products of
the second columns
Mostly, you will have no reason to calculate the Matrix elements directly
Instead, you’ll start with the identity transform:
Trang 210 0 1
0 1 0
1 0 0
Figure 15-3: The identity transform
If you put these values into the above code, you’ll see that the result is unrotated, unscaled, and untranslated Then, you will call methods such as
Matrix.Rotate( ), Matrix.Scale( ), and Matrix.Translate( ) to calculate the
new Matrix values
A transform Matrix can be assigned to a Graphics.Transform property or passed as an argument to GraphicsPath.Transform( ) This example shows a custom control that displays the elements of an affine Matrix, another that displays a rectangle transformed by the Matrix, and an example of how the
Matrix elements can be set directly
private Matrix matrix;
public Matrix Matrix{
get { return matrix;}
set { matrix = value; Invalidate();}
}
Trang 22private void DrawBrackets(Graphics g){
g.ScaleTransform(scale * Width, scale * Height);
Pen p = new Pen(Color.Red);
Font f = new Font("Arial", 1f);
float[] els = matrix.Elements;
PointF drawPoint = new PointF(0.05f, 0.1f);
Trang 23g.DrawString(s, f, Brushes.Black, drawPoint);
//Draw 3rd col of affine
}///:~ (Continues with TransformDisplay.cs)
The GraphicsPath bracket defines the shape of the tall square brackets that are used to display a matrix The bracket shape is initialized in the
MatrixPanel( ) constructor that also sets ResizeRedraw to true
The Matrix property of the MatrixPanel is used to get and set the associated
Matrix If the Matrix is assigned, the display should update to reflect its values,
so a call to Invalidate( ) is placed in the set method
MatrixPanel.OnPaint( ) calls DrawBrackets( ) and then DrawMatrix( ) DrawBrackets scales the Graphics so that a value of 1.0 is 90% of the Height
or Width of the Panel
Graphics.BeginContainer( ) and EndContainer( ) can be used during
complex transformation sequences to save the current state of the Graphics( ) Here, for instance, we save the state in a GraphicsContainer gState after the scaling transform, but then rotate and translate the Graphics to draw the right-
Trang 24682 Thinking in C# www.ThinkingIn.NET
draw the closing bracket, we have to flip it and move it over to the right-hand side
of the Panel) After we’re done, though, instead of reversing the translations, we
just call Graphics.EndContainer( ) with the state we wish to restore as an
argument Then, we can draw the left-hand bracket with just two lines of code
MatrixPanel.DrawMatrix( ) first has to create a Font that’s small enough to
display on the scaled Panel Then, the Matrix elements are retrieved and
displayed in their proper positions String formatting is used to constrain the
lengths of the displayed data to two decimal places
internal Matrix Matrix{
set{ matrix = value;}
get{ return matrix;}
}///:~ (Continues with MatrixAndTransform.cs)
TransformDisplay is a simple owner-drawn Panel that has a Matrix
property, and, in OnPaint( ), applies this Matrix to its Graphics before
drawing a red Rectangle from 0, 0 to 100, 100
//:c15:MatrixAndTransform.cs
Trang 25//Compile with
/*
csc MatrixAndTransform.cs TransformDisplay.cs MatrixElements.cs
Trang 26float el1 = (float) (Math.Cos(rot) * xScale);
float el2 = (float) (Math.Sin(rot) * yScale);
float el3 = (float) (-Math.Sin(rot) * xScale);
float el4 = (float) (Math.Cos(rot) * yScale);
public static void Main(){
MatrixElements me = new MatrixElements();
Application.Run(me);
}
}///:~
The third custom control of this program is MatrixAndTransform, a Panel
that combines a MatrixPanel and a TransformDisplay and sets them both to
have the same Matrix
MatrixElements is a Form that contains several MatrixAndTransforms
The MatrixAndTransform mt1 is given the identity matrix to display mt2
uses Matrix.Rotate( ) to rotate 45 degrees around the origin before drawing
mt3 shows how transformations can accumulate First, Matrix.Scale( ) is used
to scale the x and y dimensions by different amounts Second, the Matrix is
Trang 27rotated 30 degrees Finally, the resulting scaled and rotated matrix is translated
125 units along the x axis and -70 on the y axis Remember that this translation occurs after scaling and rotating, so these values are added along scaled, rotated
axes (as will be apparent when compared to mt4)
For our final MatrixAndTransform, we’re going to calculate the matrix’s
elements directly Here we see the inconsistency between the rotation
transformation methods (Graphics.RotateTransform( ) and
Matrix.Rotate( )) that use degrees, and the trigonometric functions of the Math class (Math.Sin( ) and Math.Cos( ) are needed here) that use radians
While mt3 was rotated 30 degrees, the equivalent is π / 6 radians This value, and the xScale and yScale values, are used to calculate the first 4 elements of the Matrix The 5th and 6th elements, which are the translation elements, are
set directly These values will be added directly to the screen coordinates: setting
them to 36 and 0 will end up having the same effect as the m3.Translate(125,
-70) translation
Figure 15-4 shows mt4 on top and mt3 below You can see how the rotation and
scaling elements (the four elements in the upper-left corner of both matrices) are identical, while the translation elements (the first two elements in the lowest row)
are set precisely in mt4 and a little off in mt3
Trang 28686 Thinking in C# www.ThinkingIn.NET
Figure 15-4: Various transforms and their effect
Trigonometry, matrix math, and linear algebra are the most important
mathematical disciplines for programmers They are of constant use, especially in
game programming
Hit detection
When programming GDI+, you’ll generally want to react to mouse clicks near the
shapes you are drawing The GraphicsPath class has several methods to assist
you This example uses GraphicsPath.IsVisible( ), which returns true if a
given point is within the GraphicsPath, to determine if the mouse was clicked
within the desired shape:
//:c15:GraphicsPathHitTest.cs
//Demonstrates hit testing with a GraphicsPath
Trang 29using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;
class GraphicsPathHitTest : Form {
GraphicsPath shape = new GraphicsPath();
GraphicsPathHitTest(){
shape.AddLine(10, 10, 30,10);
Point[] curves = new Point[]{
new Point(30, 10), new Point(70, 120),
public void HitTest(object src, MouseEventArgs ea){
Point mouseLocation = new Point(ea.X, ea.Y);
Trang 30688 Thinking in C# www.MindView.net
is used to connect the final point to the initial point (this is not necessary for this
particular shape, which is already closed, but is a good habit to develop) After
constructing the shape, an event handler is added to the MouseUp event
The OnPaint( ) method shows the Graphics.DrawPath( ) method, which
takes a Pen and a GraphicsPath (naturally, there is a Graphics.FillPath( )
method as well) The HitTest( ) method extracts the location of the mouse from
the MouseEventArgs that are passed in, constructs a Point from them, and
then uses GraphicsPath.IsVisible( ) to determine if the mouse was clicked
within the shape
Fonts and text
Although the RichTextBox control can be used in many interfaces for creating a
user interface featuring formatted text, the full power of Windows text support
requires GDI+ We’ve already used the Font class as a property of a Windows
Forms Control and in the FontDialog common dialog, but GDI+ allows a Font
to be drawn with a Brush of any type This example demonstrates that you can
even draw text using a tiled image:
class FontDrawing : Form {
protected override void OnPaint(PaintEventArgs ea){
Trang 31TextureBrush tb = new TextureBrush(img);
Point p = new Point(10, 10);
Graphics.MeasureString( ) to determine the size that the string “Hello, C#”
will require when drawn in the given context with the specified Font If the current size of the Form is not big enough to accommodate the full text, the
Width of the Form is increased (however, since ResizeRedraw is left at its
default false value, the application window can be made smaller than the
displayed text without triggering a repainting event)
A TextureBrush is created from a local image file, and
Graphics.DrawString( ) is called with the string to draw, the Font to use,
the Brush to render the Font with, and the Point that corresponds to the
upper-left corner of the rendered text That point is circled in red; when you run this program, you may be surprised by how far from the text this appears
If you find yourself using GDI+ to draw text on the screen, you probably are interested in drawing the text in strange ways (vertically, diagonally, etc.) You
can use the various transformation methods in the Graphics class, as this
example shows (before running this program, see if you can predict what the output will look like):
Trang 32Rectangle r = new Rectangle(10, 10, 100, 100);
g.DrawString(hw, arial, Brushes.Black, r);
In addition to rotating and scaling the drawing canvas, a Rectangle, not a
Point, is used as the final argument to Graphics.DrawString( ) This overload
of DrawString( ) wraps and clips the text to the Rectangle
Printing
Now that you’ve had a whirlwind tour of GDI+, you can finally print from
Windows Forms Printing is done with a PrintDocument, an object with a
PrintPage event You attach a delegate to this event and receive an instance of
PrintPageEventArgs, which includes a Graphics This Graphics object
corresponds to one page of your output device You draw graphics on the
Graphics, just as you would in an OnPaint( ) method When done, the page
will be printed
In this example, we define a simple form that calls both the
PrintPreviewDialog and PrintDialog common dialogs:
Trang 33class Printing : Form {
PaperWaster pw = new PaperWaster();
prtMenu.Click += new EventHandler(OnPrint); }
public void OnPreview(object src, EventArgs ea){ PrintPreviewDialog ppd =
new PrintPreviewDialog();
ppd.Document = pw.Document;
if (ppd.ShowDialog() == DialogResult.OK) { //Dialog showed okay
Trang 34692 Thinking in C# www.MindView.net
}
class PaperWaster {
PrintDocument pd = new PrintDocument();
internal PrintDocument Document{
Font f = new Font("Arial", 36);
SolidBrush b = new SolidBrush(Color.Black);
PointF p = new PointF(10.0f, 10.0f);
g.DrawString("Reduce, Reuse, Recycle", f, b, p);
ea.HasMorePages = false;
}
}///:~
Both the PrintPreview and PrintDialog classes have a Document property
which must be set to an instance of class PrintDocument This is done in the
OnPreview( ) and OnPrint( ) event handlers in the Printing form Our
domain class PaperWaster has a Document property that returns a
PrintDocument whose PrintPage event has been delegated to
PaperWaster.PrintAPage( )
The only part of PrintAPage( ) that is new is setting the HasMorePages
property of the PrintPageEventArgs argument to false This is actually
unnecessary, as false is its default value, but if set to true, PrintAPage will be
called again It is up to you to maintain the state of your domain object so that a
sequence of calls to PrintAPage( ) properly output pages in order and then
terminate by setting HasMorePages to false
Bitmaps
Since we’ve already shown the use of an Image inside of a TextureBrush, it
shouldn’t be a shock that GDI+ supports displaying Images directly To display
an Image you already have in memory, you use Graphics.DrawImage( ),
Trang 35which has a variety of overloads that allows you to display the image or a portion
of it in original size or scaled and as a parallelogram This example shows some of these overloads:
g.DrawImage(bmp, new Point(70, 70));
Rectangle scaled = new Rectangle(20, 20, 60, 60);
The demo loads a Bitmap that is included in the book’s source code file The first
DrawImage( ) call draws the Image, unscaled, at the specified Point The
second DrawImage( ) scales the bitmap to fit in the Rectangle
Trang 36694 Thinking in C# www.ThinkingIn.NET
The third overload of DrawImage( ) accepts an array of 3 Points; these Points
define a parallelogram The first Point is the origin, the second point is the
upper-right corner of the parallelogram The Image will be drawn to follow the
slope defined by these two points The third Point defines the lower-left corner
of the parallelogram and the fourth corner of the parallelogram is inferred If you
pass an incorrectly sized array to this method, DrawImage( ) will throw an
exception
The easiest way to draw on an Image is to use the static method
Graphics.FromImage( ), which returns a Graphics on which you can use the
gamut of GDI+ drawing tools This example loads a bitmap, draws on it, and
saves the result to disk
MenuItem oMenu = new MenuItem("&Open ");
oMenu.Click += new EventHandler(OpenImage);
fMenu.MenuItems.Add(oMenu);
MenuItem sMenu = new MenuItem("&Save ");
sMenu.Click += new EventHandler(SaveImage);
fMenu.MenuItems.Add(sMenu);
pb = new PictureBox();
pb.Dock = DockStyle.Fill;
Controls.Add(pb);
Trang 37}
public void OpenImage(object src, EventArgs ea){ OpenFileDialog ofd = new OpenFileDialog(); ofd.Filter = "Image files (*.bmp;*.jpg;*.gif)" + "|*.bmp;*.jpg;*.gif;*.png";
DialogResult fileChosen = ofd.ShowDialog();
Trang 38696 Thinking in C# www.MindView.net
}///:~
The program uses OpenFileDialog and SaveFileDialog as discussed in the
previous chapter After the Image is opened, DrawOnImage( ) generates a
Graphics for it and draws a red rectangle on the image In order to ensure that
the Graphics is disposed of properly, it’s given as an argument to a using block
Once drawn upon, pb.Invalidate( ) is called to trigger a repaint of the
PictureBox
Although using a Graphics allows the use of all of GDI+ tools, when an Image
is a Bitmap, you can directly set and get the color of individual pixels using
Bitmap.GetPixel( ) and Bitmap.GetPixel( ) This example randomly
speckles a bitmap with randomly colored pixels:
MenuItem oMenu = new MenuItem("&Open ");
oMenu.Click += new EventHandler(OpenImage);
public void OpenImage(object src, EventArgs ea){
OpenFileDialog ofd = new OpenFileDialog();
Trang 39ofd.Filter = "Image files (*.bmp;*.jpg;*.gif)"
Random rand = new Random();
int imgSize = bmp.Width * bmp.Height;
int iToChange = (int) (imgSize * 25);
for (int i = 0; i < iToChange; i++) {
Trang 40698 Thinking in C# www.ThinkingIn.NET
pixels in the image are changed (only approximately because the same pixel may
be chosen in the loop) A random coordinate (x, y) is chosen and a random
Color created Bitmap.SetPixel( ) makes the change When the loop is done,
pb.Invalidate( ) causes a repaint Bitmap.GetPixel( ) is similarly
straightforward: given a coordinate, it returns a Color
Rich clients with interop
One of the real joys of working with the NET Framework after spending several
years programming for browser-based interfaces is the rediscovery of the power
of the client machine Heck, just having normal menus again is a thrill Windows
Forms provides many powerful components, but there are many additional
components available to Windows users You can use these components in two
ways:
♦ If the component supports Microsoft’s Component Object Model (COM),
you can generate a Runtime Callable Wrapper (RCW) proxy that allows you to use the component much as if it were a native NET object
♦ If the component is a DLL that exports functions, you can write your
own wrapper class that accesses the functions as if they were static methods
There are two significant problems with working with Interop: documentation of
the non-.NET component and the component’s implementation quality Sadly,
neither of these can be taken for granted and there’s little that can be done about
it .NET’s threading and memory models are the best kind: sophisticated enough
to be easy Before using any component, NET or otherwise, you should perform
due diligence by searching newsgroup archives for pointers to bugs, resource
issues, and programming quirks
COM Interop and the WebBrowser
control
If the RichTextBox doesn’t provide the display capabilities you need, perhaps
the core HTML component of Internet Explorer will be sufficient Since Internet
Explorer 4, Microsoft has made its browser’s components available as COM
components
COM Interop is enabled by the generation of a Runtime Callable Wrapper, an
assembly that mediates between the NET world and the COM world Tools
provided in the NET Framework SDK automatically can generate these wrappers
from COM typelibs The general solution to creating a wrapper is to use the tool