1. Trang chủ
  2. » Công Nghệ Thông Tin

Thinking in C# phần 9 pot

130 194 0

Đang tải... (xem toàn văn)

Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống

THÔNG TIN TÀI LIỆU

Thông tin cơ bản

Tiêu đề GDI+ Overview
Trường học University of Computer Science and Technology
Chuyên ngành Computer Science
Thể loại Essay
Năm xuất bản 2023
Thành phố Hanoi
Định dạng
Số trang 130
Dung lượng 711,73 KB

Các công cụ chuyển đổi và chỉnh sửa cho tài liệu này

Nội dung

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 1

15: 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 2

660 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 3

Controls.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 4

662 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 5

for (double d = 0; d < Math.PI * 4; d += inc) {

double sin = Math.Sin(d);

int y = (int) (this.Height / 2 * sin);

Trang 6

664 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 7

The 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 8

666 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 9

float 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 10

668 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 11

new 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 14

672 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 15

public 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 16

674 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 17

Brush 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 18

676 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 19

Finally, 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 20

678 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 21

0 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 22

private 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 23

g.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 24

682 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 26

float 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 27

rotated 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 28

686 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 29

using 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 30

688 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 31

TextureBrush 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 32

Rectangle 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 33

class 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 34

692 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 35

which 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 36

694 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 38

696 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 39

ofd.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 40

698 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

Ngày đăng: 06/08/2014, 20:20

TỪ KHÓA LIÊN QUAN