Thinking in C# phần 9 pot

130 192 0
Thinking in C# phần 9 pot

Đ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

659 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. 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. //:c15:GraphicsHandle.cs //Accessing the drawing surface of a control using System; using System.Drawing; using System.Windows.Forms; class GraphicsHandle : Form { Button target; GraphicsHandle(){ 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); Chapter 15: GDI+ Overview661 Controls.Add(controller); } public void OnClick(object src, EventArgs ea){ Graphics canvas = target.CreateGraphics(); using(canvas){ canvas.Clear(Color.Red); } } public static void Main(){ Application.Run(new GraphicsHandle()); } }///:~ After OnClick( ) calls target.CreateGraphics( ), it wraps the use of the resulting object in using, which as discussed in Chapter 11 expands behind-the- scenes into a try…finally block that calls Dispose( ) on its IDisposable 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? 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. //:c15:SineWave.cs //Demonstrates GDI+ Drawing using System; using System.Drawing; using System.Windows.Forms; class OwnerDrawPanel : Panel { internal OwnerDrawPanel(){ ResizeRedraw = true; } Color c = Color.Blue; static int drawCount = 0; protected override void OnPaint(PaintEventArgs e){ base.OnPaint(e); Console.WriteLine("PaintSine called"); Graphics g = e.Graphics; g.Clear(Color.White); Pen pen = null; if (drawCount == 0) { pen = new Pen(Color.Blue); Chapter 15: GDI+ Overview663 } else { pen = new Pen(Color.Red); } drawCount++; double inc = Math.PI * 4 / Width; int x = 0; for (double d = 0; d < Math.PI * 4; d += inc) { double sin = Math.Sin(d); int y = (int) (this.Height / 2 * sin); y += this.Height / 2; Rectangle rec = new Rectangle(x, y, 1, 1); g.DrawRectangle(pen,rec); x++; } } } class SineWave : Form { SineWave(){ Panel p = new Panel(); p.Dock = DockStyle.Left; p.Width = 120; Splitter s = new Splitter(); s.Dock = DockStyle.Left; Controls.Add(s); Controls.Add(p); OwnerDrawPanel ownerDraw = new OwnerDrawPanel(); ownerDraw.Dock = DockStyle.Fill; Controls.Add(ownerDraw); } public static void Main(){ Application.Run(new SineWave()); } }///:~ The OwnerDrawPanel( ) constructor specifies that the ResizeRedraw property inherited from Control is true. This should be set to true if, as in this case, the control needs to redraw its entire client area on a resize event. The 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. Chapter 15: GDI+ Overview665 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 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 Math.Sin( ): //:c15:SineLine.cs //Demonstrates Scaling and Transform using System; using System.Drawing; using System.Windows.Forms; class TransformPanel : Panel { internal TransformPanel(){ ResizeRedraw = true; } protected override void OnPaint(PaintEventArgs e){ base.OnPaint(e); Graphics g = e.Graphics; g.Clear(Color.White); Pen pen = new Pen(Color.Red); float widthScale = (float) (Width / (Math.PI * 4)); float heightScale = Height / 2; float invertHeightScale = -heightScale; invertHeightScale *= .9f; Console.WriteLine("scale {0} {1}", widthScale, invertHeightScale); pen.Width = 1 / widthScale; //Set transforms for Graphics g.TranslateTransform(0, Height / 2); g.ScaleTransform(widthScale, invertHeightScale); PointF lastPoint = new PointF(0f, 0f); double inc = Math.PI * 4 / Width; for (float f = 0; f < Math.PI * 4; f += .1f) { Chapter 15: GDI+ Overview667 float sin = (float) Math.Sin(f); PointF newPoint = new PointF(f, sin); g.DrawLine(pen, lastPoint, newPoint); lastPoint = newPoint; } } } class SineLine : Form { SineLine(){ TransformPanel tPanel = new TransformPanel(); tPanel.Dock = DockStyle.Fill; Controls.Add(tPanel); } public static void Main(){ Application.Run(new SineLine()); } }///:~ 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. The line g.TranslateTransform(0, Height / 2); tells the Graphics to add nothing to all x values passed in and to add half the height to all the y values passed in, i.e., put the y axis halfway down the form. The line 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 length: //:c15:SpinTheBottle.cs //Demonstrates rotation transforms using System; using System.Drawing; using System.Windows.Forms; class BottleSpinner : Panel { internal BottleSpinner(){ ResizeRedraw = true; } PointF[] pointer = new PointF[]{ new PointF(0, 0), new PointF(.1f, .05f), [...]... example, we define a simple form that calls both the PrintPreviewDialog and PrintDialog common dialogs: //:c15:Printing.cs //Demonstrates printing from Windows Forms using System; using System.Drawing; using System.Drawing.Printing; using System.Windows.Forms; 690 Thinking in C# www.ThinkingIn.NET class Printing : Form { PaperWaster pw = new PaperWaster(); Printing(){ MainMenu menu = new MainMenu(); Menu... determine if the mouse was clicked within the desired shape: //:c15:GraphicsPathHitTest.cs //Demonstrates hit testing with a GraphicsPath 686 Thinking in C# www.ThinkingIn.NET using using using using System; System.Drawing; System.Drawing.Drawing2D; System.Windows.Forms; class GraphicsPathHitTest : Form { GraphicsPath shape = new GraphicsPath(); GraphicsPathHitTest(){ shape.AddLine(10, 10, 30,10); Point[]... drawPoint = new PointF(0.05f, 0.1f); string s = els[0].ToString("0.00"); g.DrawString(s, f, Brushes.Black, drawPoint); s = els[1].ToString("0.00"); drawPoint.X = 0.4f; g.DrawString(s, f, Brushes.Black, drawPoint); s = els[2].ToString("0.00"); drawPoint.X = 0.05f; drawPoint.Y = 0.4f; g.DrawString(s, f, Brushes.Black, drawPoint); 680 Thinking in C# www.MindView.net s = els[3].ToString("0.00"); drawPoint.X = 0.4f;... Controls.Add(bs); 670 Thinking in C# www.ThinkingIn.NET } 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; bs.PointerRotation = angle; bs.Invalidate(); } public static void Main(){ Application.Run(new SpinTheBottle()); } }///:~ The BottleSpinner panel first defines an... draw text using a tiled image: //:c15:FontDrawing.cs //Basic text output in GDI+, using custom Brush using System; using System.Drawing; using System.Drawing.Drawing2D; using System.Windows.Forms; class FontDrawing : Form { protected override void OnPaint(PaintEventArgs ea){ base.OnPaint(ea); Graphics g = ea.Graphics; Font arial = new Font("Arial", 96 , FontStyle.Regular); string hw = "Hello, C#" ; SizeF... g.DrawString(s, f, Brushes.Black, drawPoint); s = els[4].ToString("0.00"); drawPoint.X = 0.05f; drawPoint.Y = 0.7f; g.DrawString(s, f, Brushes.Black, drawPoint); s = els[5].ToString("0.00"); drawPoint.X = 0.4f; g.DrawString(s, f, Brushes.Black, drawPoint); //Draw 3rd col of affine drawPoint.X = 7f; drawPoint.Y = 1f; g.DrawString( "0.00", f, Brushes.Black, drawPoint); drawPoint.Y = 4f; g.DrawString( "0.00",... PointF(.09f, PointF(.02f, PointF(-.1f, PointF(-.1f, 2f), new PointF(.1f, 5f), 1f), new PointF(-.02f, 1f), 5f), new PointF(-.09f, 2f), 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;} } protected override void OnPaint(PaintEventArgs... g.DrawRectangle(Pens.Green, 10, 10, 100, 100); Pen pointer = new Pen(Color.Black); pointer.EndCap = LineCap.ArrowAnchor; g.DrawLine(pointer, 100, 7, 1 09, 7); g.DrawLine(pointer, 120, 7, 111, 7); } 672 Thinking in C# www.MindView.net public static void Main(){ Application.Run(new RegionFill()); } }///:~ Unlike previous examples, the owner-drawn Control in RegionFill is descended from Form, not Panel This... 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 application windows //:c15:BinocularForm.cs //Creates a non-rectangular application window using System; using System.Windows.Forms; using System.Drawing; using System.Drawing.Drawing2D; class BinocularForm : Form { BinocularForm(){ GraphicsPath gp = new GraphicsPath();... sizeOfString = g.MeasureString(hw, arial); if (Width < sizeOfString.Width + 20) { Width = (int) sizeOfString.Width; } Image img = Image.FromFile("images.jpg"); 688 Thinking in C# www.MindView.net TextureBrush tb = new TextureBrush(img); Point p = new Point(10, 10); g.DrawString(hw, arial, tb, p); Rectangle txtOrg = new Rectangle(10, 10, 2, 2); g.DrawEllipse(Pens.Red, txtOrg); } public static void Main(){ . repainted as a normal button, while the portion that was not obscured remains a red rectangle. What’s going on? 662 Thinking in C# www.ThinkingIn.NET The answer lies in the underlying Windows. float sin = (float) Math.Sin(f); PointF newPoint = new PointF(f, sin); g.DrawLine(pen, lastPoint, newPoint); lastPoint = newPoint; } } } class SineLine : Form { SineLine(){ . 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

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

Tài liệu cùng người dùng

  • Đang cập nhật ...

Tài liệu liên quan