Understanding Drawing Principles


This section examines the basic principles that you need to understand to start drawing to the screen. It starts by giving an overview of GDI, the underlying technology on which GDI+ is based, and shows how GDI and GDI+ are related. Then you move on to a couple of simple examples.

GDI and GDI+

In general, one of the strengths of Windows — and indeed of modern operating systems in general — lies in its ability to abstract the details of particular devices without input from the developer. For example, you don't need to understand anything about your hard drive device driver in order to programmatically read and write files to disk; you simply call the appropriate methods in the relevant .NET classes(or in pre-.NET days, the equivalent Windows API functions). This principle is also true when it comes to drawing. When the computer draws anything to the screen, it does so by sending instructions to the video card. However, many hundreds of different video cards are on the market, most of which have different instruction sets and capabilities. If you had to take that into account and write specific code for each video driver, writing any such application would be an almost impossible task. This is why the Windows graphical device interface (GDI) has been around because the earliest versions of Windows.

GDI provides a layer of abstraction, hiding the differences between the different video cards. You simply call the Windows API function to do the specific task, and internally the GDI figures out how to get the client's particular video card to do whatever it is you want when they run your particular piece of code. Not only this, but if the client has several display devices — for example, monitors and printers — GDI achieves the remarkable feat of making the printer look the same as the screen as far as the application is concerned. If the client wants to print something instead of displaying it, your application will simply inform the system that the output device is the printer and then call the same API functions in exactly the same way.

As you can see, the device-context (DC) object is a very powerful object and you won't be surprised to learn that under GDI all drawing had to be done through a device context. The DC was even used for operations that don't involve drawing to the screen or to any hardware device, such as modifying images in memory.

Although GDI exposes a relatively high-level API to developers, it is still an API that is based on the old Windows API, with C-style functions. GDI+ to a large extent sits as a layer between GDI and your application, providing a more intuitive, inheritance-based object model. Although GDI+ is basically a wrapper around GDI, Microsoft has been able through GDI+ to provide new features and some performance improvements to some of the older features of GDI as well.

The GDI+ part of the .NET Base Class Library is huge, and this chapter scarcely scratches the surface of its features. That's a deliberate decision, because trying to cover more than a tiny fraction of the library would have turned this chapter into a huge reference guide that simply listed classes and methods. It's more important to understand the fundamental principles involved in drawing, so that you are in a good position to explore the available classes. Full lists of all the classes and methods available in GDI+ are of course available in the SDK documentation.

Note

Visual Basic 6 developers are likely to find the concepts involved in drawing quite unfamiliar, because Visual Basic 6 focuses on controls that handle their own painting. C++/MFC developers are likely to be in more familiar territory because MFC does require developers to take control of more of the drawing process, using GDI. However, even if you have a strong background in the classic GDI, you'll find a lot of the material presented in this chapter is new.

GDI+ namespaces

The following table provides an overview of the main namespaces you'll need to explore to find the GDI+ base classes.

You should note that almost all of the classes and structs used in this chapter are taken from the System.Drawing namespace.

Namespace

Description

System.Drawing

Contains most of the classes, structs, enums, and delegates con- cerned with the basic functionality of drawing

System.Drawing.Drawing2D

Provides most of the support for advanced 2D and vector draw- ing, including anti-aliasing, geometric transformations, and graphics paths

System.Drawing.Imaging

Contains various classes that assist in the manipulation of images (bitmaps, GIF files, and so on)

System.Drawing.Printing

Contains classes to assist when specifically targeting a printer or print preview window as the "output device"

System.Drawing.Design

Contains some predefined dialog boxes, property sheets, and other user interface elements concerned with extending the design-time user interface

System.Drawing.Text

Contains classes to perform more advanced manipulation of fonts and font families

Device contexts and the Graphics object

In GDI, the way that you identify which device you want your output to go to is through an object known as the device context (DC). The DC stores information about a particular device and is able to translate calls to the GDI API functions into whatever instructions need to be sent to that device. You can also query the device context to find out what the capabilities of the corresponding device are (for example, whether a printer prints in color or only in black and white), so the output can be adjusted accordingly. If you ask the device to do something it's not capable of, the DC will normally detect this and take appropriate action (which, depending on the situation, might mean throwing an error or modifying the request to get the closest match that the device is actually capable of using).

However, the DC doesn't only deal with the hardware device. It acts as a bridge to Windows and is able to take account of any requirements or restrictions placed on the drawing by Windows. For example, if Windows knows that only a portion of your application's window needs to be redrawn, the DC can trap and nullify attempts to draw outside that area. Due to the DC's relationship with Windows, working through the device context can simplify your code in other ways.

For example, hardware devices need to be told where to draw objects, and they usually want coordinates relative to the top-left corner of the screen (or output device). Usually, however, your application will be thinking of drawing something at a certain position within the client area (the area reserved for drawing) of its own window, possibly using its own coordinate system. Because the window might be positioned anywhere on the screen, and a user might move it at any time, translating between the two coordinate systems is potentially a difficult task. However, the DC always knows where your window is and is able to perform this translation automatically.

With GDI+, the device context is wrapped up in the .NET base class System.Drawing.Graphics. Most drawing is done by calling methods on an instance of Graphics. In fact, because the Graphics class is the class that is responsible for handling most drawing operations, very little gets done in GDI+ that doesn't involve a Graphics instance somewhere, so understanding how to manipulate this object is the key to understanding how to draw to display devices with GDI+.

Drawing Shapes

To show this at work, this section starts off with a short example, DisplayAtStartup, to illustrate drawing to an application's main window. The examples in this chapter are all created in Visual Studio 2005 as C# Windows Applications. Recall that for this type of project the code wizard gives you a class called Form1, derived from System.Windows.Form, which represents the application's main window. Also generated for you is a class called Program (found in the Program.cs file), which represents the application's main starting point. Unless otherwise stated, in all code samples, new or modified code means code that you've added to the wizard-generated code. (You can download the sample code from the Wrox Web site at www.wrox.com.)

Note

In .NET usage, when we are talking about applications that display various controls, the terminology"form" has largely replaced "window" to represent the rectangular object that occupies an area of the screen on behalf of an application. In this chapter, we've tended to stick to the term window, because in the context of manually drawing items it's rather more meaningful. We'll also talk about the form whenwe're referring to the .NET class used to instantiate the form/window. Finally, we'll use the terms "drawing" and "painting" interchangeably to describe the process of displaying some item on the screen or other display device.

The first example simply creates a form and draws to it in the constructor when the form starts up. Note that this is not actually the best or the correct way to draw to the screen — you'll quickly find that this example has a problem in that it is unable to redraw anything after starting up. However, this example illustrates quite a few points about drawing without your having to do very much work.

For this example, start Visual Studio 2005 and create a Windows Application. First, set the background color of the form to white. In the example this line is after the InitializeComponent() method so that Visual Studio 2005 recognizes the line and is able to alter the design view appearance of the form. You can find the InitializeComponent() method by first clicking the Show All Files button in the Visual

Studio Solution Explorer and then expanding the plus sign next to the Form1.cs file. Here you will find the Form1.Designer.cs file. It is here in this file where you will find the InitializeComponent() method. You could have used the design view to set the background color, but this would have resulted in pretty much the same line being added automatically:

        private void InitializeComponent()         { //  // Form1 //              this.AutoScaleBaseSize = new System.Drawing.Size(5, 13); this.BackColor = System.Drawing.Color.White;             this.ClientSize = new System.Drawing.Size(292, 266);             this.Name = "Form1";             this.Text = "Form1";

Then you add code to the Form1 constructor. You create a Graphics object using the form's CreateGraphics() method. This Graphics object contains the Windows DC you need to draw with. The device context created is associated with the display device, and also with this window:

public Form1() {    InitializeComponent(); Graphics dc = this.CreateGraphics(); this.Show(); Pen bluePen = new Pen(Color.Blue, 3); dc.DrawRectangle(bluePen, 0,0,50,50); Pen redPen = new Pen(Color.Red, 2); dc.DrawEllipse(redPen, 0, 50, 80, 60); }

As you can see, you then call the Show() method to display the window. This is really done to force the window to display immediately, because you can't actually do any drawing until the window has been displayed. If the window isn't displayed, there's nothing for you to draw onto.

Finally, you display a rectangle at coordinates (0,0) and with width and height 50, and an ellipse with coordinates (0,50) and with width 80 and height 50. Note that coordinates (x,y) translate to x pixels to the right and y pixels down from the top-left corner of the client area of the window — and these coordinates start from the top-left corner of the shape to be displayed.

The overloads that you are using of the DrawRectangle() and DrawEllipse() methods each take five parameters. The first parameter of each is an instance of the class System.Drawing.Pen. A Pen is one of a number of supporting objects to help with drawing — it contains information about how lines are to be drawn. Your first pen instructs that lines should be the color blue with a width of 3 pixels; the second pen instructs that lines should be red and have a width of 2 pixels. The final four parameters are coordinates and size. For the rectangle, they represent the (x,y) coordinates of the top left-hand corner of the rectangle in addition to its width and height. For the ellipse, these numbers represent the same thing, except that you are talking about a hypothetical rectangle that the ellipse just fits into, rather than the ellipse itself. Figure 25-1 shows the result of running this code. Of course, because this is not a color book, you cannot see the colors.

image from book
Figure 25-1

Figure 25-1 demonstrates a couple of points. First, you can see clearly where the client area of the window is located. It's the white area — the area that has been affected by setting the BackColor property. And notice that the rectangle nestles up in the corner of this area, as you'd expect when you specified coordinates of (0,0) for it. Second, notice that the top of the ellipse overlaps the rectangle slightly, which you wouldn't expect from the coordinates given in the code. The culprit here is Windows itself and where it places the lines that border the rectangle and ellipse. By default, Windows will try to center the line on the border of the shape — that's not always possible to do exactly, because the line has to be drawn on pixels (obviously), but normally the border of each shape theoretically lies between two pixels. The result is that lines that are 1 pixel thick will get drawn just inside the top and left sides of a shape, but just outside the bottom and right sides — which means that shapes that strictly speaking are next to each other will have their borders overlap by one pixel. You've specified wider lines; therefore the overlap is greater. It is possible to change the default behavior by setting the Pen.Alignment property, as detailed in the SDK documentation, but for these purposes the default behavior is adequate.

Unfortunately, if you actually run the sample you'll notice the form behaves a bit strangely. It's fine if you just leave it there, and it's fine if you drag it around the screen with the mouse. If you try minimizing the window and then restoring it, then your carefully drawn shapes just vanish! The same thing happens if you drag another window across the sample. If you drag another window across it so that it only obscures a portion of your shapes, then drag the other window away again, you'll find the temporarily obscured portion has disappeared and you're left with half an ellipse or half a rectangle!

So what's going on? The problem arises when part of a window is hidden, because Windows usually discards immediately all the information concerning exactly what has been displayed. This is something Windows has to do or else the memory usage for storing screen data would be astronomical. A typical computer might be running with the video card set to display 1024768 pixels, perhaps in a 24-bit color mode, which implies that each pixel on the screen occupies 3 bytes — 2.25MB to display the screen. (What 24-bit color means is covered later in this chapter.) However, it's not uncommon for a user to work with 10 or 20 minimized windows in the taskbar. In a worst-case scenario, you might have 20 windows, each of which would occupy the whole screen if it wasn't minimized. If Windows actually stored the visual information those windows contained, ready for when the user restores them, that would amount to some 45MB! These days, a good graphics card might have 64MB of memory and be able to cope with that, but it was only a couple of years ago that 4MB was considered generous in a graphics card — and the excess would need to be stored in the computer's main memory. A lot of people still have old machines, some of them with only 4MB graphic cards. Clearly it wouldn't be practical for Windows to manage its user interface like that.

The moment any part of a window is hidden, the "hidden" pixels get lost, because Windows frees the memory that was holding those pixels. It does, however, note that a portion of the window is hidden, and when it detects that it is no longer hidden, it asks the application that owns the window to redraw its contents. There are a couple of exceptions to this rule — generally for cases in which a small portion of a window is hidden very temporarily (a good example is when you select an item from the main menu and that menu item drops down, temporarily obscuring part of the window below). In general, however, you can expect that if part of your window is hidden, your application will need to redraw it later.

That's the source of the problem for the sample application. You placed your drawing code in the Form1 constructor, which is called just once when the application starts up, and you can't call the constructor again to redraw the shapes when required later on.

When working with Windows Forms server controls, there is no need to know anything about how to accomplish this task. This is because the standard controls are pretty sophisticated and they are able to redraw themselves correctly whenever Windows asks them to. That's one reason why when programming controls you don't need to worry about the actual drawing process at all. If you are taking responsibility for drawing to the screen in your application, you also need to make sure your application will respond correctly whenever Windows asks it to redraw all or part of its window. In the next section, you modify the sample to do just that.

Painting Shapes Using OnPaint()

If the preceding explanation has made you worried that drawing your own user interface is going to be terribly complicated, don't worry. Getting your application to redraw itself when necessary is actually quite easy.

Windows notifies an application that some repainting needs to be done by raising a Paint event. Interestingly, the Form class has already implemented a handler for this event, so you don't need to add one yourself. The Form1 handler for the Paint event will at some point in its processing call up a virtual method, OnPaint(), passing to it a single PaintEventArgs parameter. This means that all you need to do is override OnPaint() to perform your painting.

Although for this example you work by overriding OnPaint(), it's equally possible to achieve the same results by simply adding your own event handler for the Paint event (a Form1_Paint() method, say) — in much the same way as you would for any other Windows Forms event. This other approach is arguably more convenient, because you can add a new event handler through the Visual Studio 2005 properties window, saving yourself from typing some code. However, the approach of overriding OnPaint() is slightly more flexible in terms of letting you control when the call to the base class window processing occurs, and is the approach recommended in the documentation. We suggest you use this approach for consistency.

In this section, you create a new Windows Application called DrawShapes to do this. As before, you set the background color to white using the Properties Window. You'll also change the form's text to DrawShapes Sample. Then you add the following code to the generated code for the Form1 class:

 protected override void OnPaint( PaintEventArgs e ) { base.OnPaint(e); Graphics dc = e.Graphics; Pen bluePen = new Pen(Color.Blue, 3); dc.DrawRectangle(bluePen, 0,0,50,50); Pen redPen = new Pen(Color.Red, 2); dc.DrawEllipse(redPen, 0, 50, 80, 60); } 

Notice that OnPaint() is declared as protected, because it is normally used internally within the class, so there's no reason for any other code outside the class to know about its existence.

PaintEventArgs is a class that is derived from the EventArgs class normally used to pass in information about events. PaintEventArgs has two additional properties, of which the more important one is a Graphics instance, already primed and optimized to paint the required portion of the window. This means that you don't have to call CreateGraphics() to get a DC in the OnPaint() method — you've already been provided with one. You look at the other additional property soon; it contains more detailed information about which area of the window actually needs repainting.

In your implementation of OnPaint(), you first get a reference to the Graphics object from Paint EventArgs, then you draw your shapes exactly as you did before. At the end you call the base class's OnPaint() method. This step is important. You've overridden OnPaint() to do your own painting, but it's possible that Windows may have some additional work of its own to do in the painting process — any such work will be dealt with in an OnPaint() method in one of the .NET base classes.

Note

For this example, you'll find that removing the call to base.OnPaint() doesn't seem to have any effect, but don't ever be tempted to leave this call out. You might be stopping Windows from doing its work properly and the results could be unpredictable.

OnPaint() will also be called when the application first starts up and your window is displayed for the first time, so there is no need to duplicate the drawing code in the constructor.

Running this code gives the same results initially as for the previous example, except that now your application behaves itself properly when you minimize it or hide parts of the window.

Using the Clipping Region

The DrawShapes sample from the previous section illustrates the main principles involved with drawing to a window, although it's not very efficient. The reason is that it attempts to draw everything in the window, irrespective of how much needs to be drawn. Figure 25-2 shows the result of running the DrawShapes example and opening another window and moving it over the DrawShapes form so part of it is hidden.

image from book
Figure 25-2

So far, so good. However, when you move the overlapping window so that the DrawShapes window is fully visible again, Windows will as usual send a Paint event to the form, asking it to repaint itself. The rectangle and ellipse both lie in the top-left corner of the client area, and so were visible all the time; therefore, there's actually nothing that needs to be done in this case apart from repainting the white background area. However, Windows doesn't know that, so it thinks it should raise the Paint event, resulting in your OnPaint() implementation being called. OnPaint() will then unnecessarily attempt to redraw the rectangle and ellipse.

Actually, in this case, the shapes will not get repainted because of the device context. Windows has preinitialized the device context with information concerning what area actually needed repainting. In the days of GDI, the region marked for repainting used to be known as the invalidated region, but with GDI+ the terminology has largely changed to clipping region. The device context knows what this region is; therefore, it will intercept any attempts to draw outside this region and not pass the relevant drawing commands on to the graphics card. That sounds good, but there's still a potential performance hit here. You don't know how much processing the device context had to do before it figured out that the drawing was outside the invalidated region. In some cases it might be quite a lot, because calculating which pixels need to be changed to what color can be very processor-intensive (although a good graphics card will provide hardware acceleration to help with some of this).

The bottom line to this is that asking the Graphics instance to do some drawing outside the invalidated region is almost certainly wasting processor time and slowing your application down. In a well-designed application, your code will help out the device context by carrying out a few simple checks, to see if the proposed drawing work is likely to be needed before it calls the relevant Graphics instance methods. In this section you code a new example, DrawShapesWithClipping, by modifying the DisplayShapes example to do just that. In your OnPaint() code, you'll do a simple test to see whether the invalidated region intersects the area you need to draw in and only call the drawing methods if it does.

First, you need to obtain the details of the clipping region. This is where an extra property, ClipRectangle, on PaintEventArgs comes in. ClipRectangle contains the coordinates of the region to be repainted, wrapped up in an instance of a struct, System.Drawing.Rectangle. Rectangle is quite a simple struct — it contains four properties of interest: Top, Bottom, Left, and Right. These respectively contain the vertical coordinates of the top and bottom of the rectangle and the horizontal coordinates of the left and right edges.

Next, you need to decide what test you'll use to determine whether drawing should take place. You'll go for a simple test here. Notice that in your drawing, the rectangle and ellipse are both entirely contained within the rectangle that stretches from point (0,0) to point (80,130) of the client area; actually, point(82,132) to be on the safe side, because you know that the lines might stray a pixel or so outside this area. So you'll check whether the top-left corner of the clipping region is inside this rectangle. If it is, you'll go ahead and redraw. If it isn't, you won't bother.

Here is the code to do this:

protected override void OnPaint( PaintEventArgs e ) {    base.OnPaint(e);    Graphics dc = e.Graphics; if (e.ClipRectangle.Top < 132 && e.ClipRectangle.Left < 82) {       Pen bluePen = new Pen(Color.Blue, 3);       dc.DrawRectangle(bluePen, 0,0,50,50);       Pen redPen = new Pen(Color.Red, 2);       dc.DrawEllipse(redPen, 0, 50, 80, 60); } } 

Note that what gets displayed is exactly the same as before. However, performance is improved now by the early detection of some cases in which nothing needs to be drawn. Notice also that the example uses a fairly crude test of whether to proceed with the drawing. A more refined test might be to check separately whether the rectangle or the ellipse needs to be redrawn. However, there's a balance here. You can make your tests in OnPaint() more sophisticated, improving performance, but you'll also make your own OnPaint() code more complex. It's almost always worth putting some test in, because you've written the code so you understand far more about what is being drawn than the Graphics instance, which just blindly follows drawing commands.




Professional C# 2005
Pro Visual C++ 2005 for C# Developers
ISBN: 1590596080
EAN: 2147483647
Year: 2005
Pages: 351
Authors: Dean C. Wills

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net