This is the third of the eight chapters that deal with user interaction and the .NET Framework focused on how to display a dialog box or SDl or MDl window, and how to place various controls such as buttons, text boxes, and list boxed looked at how to work with data in Windows Forms using a number of the Windows Forms controls that work with the disparate data sources that you might encounter.
Although these standard controls are powerful and, by themselves, quite adequate for the complete user interface for many applications, some situations require more flexibility, For example, you might want to draw text in a given font in a precise position in a window, or display images without using a picture box control, or draw simple shapes or other graphics, None of this can be done with the controls,To display that kind of output, the application must instruct the operating system what to display and where in its window to display it.
Therefore, this chapter shows you know how to draw a variety of items including:
- Principles of drawing
- Lines and simple shapes
- BMP images and other image files
- Dealing with printing
In process, you will need to use a variety of helper objects, including pens (to define the characteristics of lines), brushes (to define how areas are filled in), and fonts (to define the shape of characters of text).
The chapter starts owever, by discussing a technology called GD1+. GDI+ consists of the set of .NETbase classes that are available to control custom drawing on the screen, These classes arrange for the appropriate instructions to be sent to graphics device drivers to ensure the correct output is placed on the screen (or printed to a hard copy).
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 CD! and the underlying technology on which CD!+ is based. It also shows how CD! and CD!+ are related. Then, we will move on to a couple of simple examples.
GDI and GDI+
In general, one of the strengths of Windows – and indeed of modem operating systems in generallies in its ability to abstract the details of particular devices without input from the developer,For example, you do not need to understand anything about your hard drive device driver to programmatically read and write files to and from 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, The Windows graphical device interface (GDI) has been around since the earliest versions of Windows cause of these reasons.
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 the client runs your particular piece of code, Not only does GDI accomplish 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 (covered shortly) 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 do not 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 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 barely seraches the surface of its features because trying to cover more than a tiny fraction of the library would have into a huge reference guide that simply listed classes and methods, It is more important to understand the fundamental principles involved in drawing so that you are in a good position to explore the available classes, FuIl lists of all the classes and methods available in GDI+ are, of course, available in the SDK documentation.
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 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 will find that a lot.
The following table provides an overview of the main namespaces you will 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.
Device Contexts and the Gaphics Object
In GO!, you identify which device you want your output to go to 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 is not capable of, the DC will normally detect this take appropriate action (which, depending on the situation, might mean throwing an or modifying the request to get the closest match that the device is actually capable of using).
How ever the DC does not deal only with the hardware device. It acts as a bridge to Windows and is able
of any requirements or restrictions placed on the drawing by Windows, For example, if s t only a portion of your application’s window needs to be redrawn, the DC can trap and nullify attempts to draw outside that area, Because of the DC’s relationship with Windows, working can simplify your code in other ways.
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 will quickly find that this example has a problem because 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 2008 and create a Windows Application. First, set the background color of the form to white, In the example, this line comes after the Ini tiali zeComponent () method so that Visual Studio 2008 recognizes the line and is able to alter the design view appearance of the form.
You can find the Intialize Component () method by first clicking the Show All Files button in the Visual Studio Solution Explorer and then clicking the plus sign next to the Forml . cs file, Here, you will find the Forml, Designer cs file, It is in this file that you will find the Ini tializeComponent () 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:
Then you add code to the Forml constructor, You create a Graphics- object using the forms CreateGraphics () method, This Graphics object contains the Windows DC that you need to draw with, The device context created is associated with the display device and also with this window:
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 cannot actually do any drawing until the window has been displayed. If the window is not 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 comer 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 the system that lines should be the color blue with a width of 3 pixels; the second pen instructs the system that the lines should be red and have a width of 2 pixels, The final four parameters are to ordinates and size. For the rectangle, they represent the (x.y) coordinates of the top-left comer 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 33-1 shows the result of running this code of course, because this book is not in color, you cannot see the colors.
Figure 33-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, Notice that the rectangle nestles up in the comer of this area, as you would expect when you specify the coordinates of (0,0) for it. Second, notice that the top of the ellipse overlaps the rectangle slightly, which you would not 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 is not always possible to do exactly because the line has to be drawn on pixels (obviously). 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 are next to each other have their borders overlapping by one pixel, You have 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 will notice that the form behaves a bit strangely, It is fine if you just leave it there. It is also fine if you drag it around the screen with the mouse, However, 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 so that it only obscures a portion of your shapes. When you drag the other window away again, you will find that the temporarily obscured portion has disappeared and you are 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 1024 x 768 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, (24-bit color is covered later in this chapter.) However, it is 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 was not minimized. If Windows actually stored the visual information those windows contained, ready for when the user restored them, then 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 few 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. Many people still have old machines, some of them with only 4MB graphic cards, Clearly, it would not 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 is the source of the problem for the sample application. You placed your drawing code in the Forml constructor, which is called just once when the application starts up, and you cannot 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 is one reason why, when programming controls, you do not 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 that 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 On Paint()
If the preceding explanation has made you worried that drawing your own user interface is going to be
terribly complicated, do not 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 do not need to add one yourself, The Form: handler for the Paint event will at some point in its processing call up a virtual method, On Paint (), passing to it a single Paint EventArgs parameter, This means that all you need to do is override on Paint, to perform your painting.
Although for this example you work by overriding On Paint (), it is equally possible to achieve the same results by simply adding your own event handler for the Paint event (a Forml_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 2008 properties window, saving yourself from typing some code, However, the approach of overriding on Paint () is slightly more flexible in terms of letting you control when the call to the base class window processing occurs, and it is the approach recommended in the documentation;
In this section, you create a new Windows Application called Draw Shapes to do this, As before,you set the background color to white, using the properties window, You will also change the form’s text to DrawShapes Sample, Then you add the following code to the generated code for the Forml class:
Notice that Paint () is declared as protected, because it is normally used internally within the class, so there is no reason for any other code outside the class to know about its existence.
Paint EventArgs is a class that is derived from the EventArgs class normally used to pass in information about events, Paint EventArgs has two additional properties, of which the more important one is graphics instance, already primed and optimized to paint the required portion of the window.
This means that you do not have to) call Create Graphics (} to get a DC in the On Paint () method have already been provided with one, You will look at the other additional property soon, This property contains more detailed information about which area of the window actually needs repainting.
Using the Clipping Region
The Draw Shapes sample from the previous section illustrates the main principles involved with drawing to a window, although the sample is not very efficient. The reason is that it attempts to draw everything in the window, regardless of how much needs to be drawn. Figure 33·2 shows the result of running the Draw Shapes example and opening another window and moving it over the Draw Shapes form so part of it it hidden.
However, when you move the overlapping window so that the Draw Shapes 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 comer of the client area, and so were visible all the time, Therefore, there 15 actually nothing that needs to be done in this case apart from repainting the white background area, However, Windows does not know that, so it thinks it should raise the Paint event, resulting in your On paint. (implementation being called on Paint () will then unnecessarily attempt to redraw the rectangle and ellipse.
Actually, in this case, the shapes will not be 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 was known as the invalidated region, but with CDT+ the terminology has largely changed to clipping region. The device context recognizes this region Therefore, it will intercept any attempts to draw outside this region and not pass the relev ant drawing commands on to the graphics card, That sounds good, but there is still a potential performance hit here, You do not know how much processing the device context had to do before it figured (Jut 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, 1 good graphics card will provide hardware acceleration to help with some of this).
Next, you need to decide what test you will use to determine whether drawing should take place You will 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 use 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 will check whether the top-left corner of the clipping region is inside this rectangle,If it is, then you will go ahead and redraw. If it is not, then you won’t bother.
The following is the code to do this:
Note that what is 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 for 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 is a balance here, You can make your tests in on Paint () more sophisticated, improving performance, but you will also make your own on Paint () code more complex, It is almost always worth putting some test in because you have written the code, you understand far more about what is being drawn than the Graphics instance, which just blindly follows drawing commands.