Chapter 11 Bitmaps, Icons and Cursors

advertisement

Chapter 11

Bitmaps, Icons and Cursors

An important part of developing modern applications is the use of bitmap images. In addition to displaying images in the client area of a form we also use images for icons, cursors and other purposes such as displaying them as buttons. In this chapter I will cover the basics of using bitmap images.

Prior to the .NET FCL working with bitmaps was a tedious and complicated process. Now it's a breeze. As a matter of fact, it's lots of fun. Before we start to actually display bitmaps I want to review some aspects of images that you may or may not be familiar with.

Vector vs. Bitmap Graphics

There are two major ways in which we create images, vector and bitmap graphics. When we used such GDI+ methods as Draw Rectangle we are using vector graphics. With vector graphics we don't deal with the specific pixels of the ultimate image, but rather such parameters as the coordinate and dimensions of the graphic element. Vector graphics are resolution independent . In order to display a vector graphic it needs to be converted to a representation in pixels.

This process is fairly complex and any good book on computer graphics will provide lots of technical details such as the issue of aliasing . That's a characteristic where lines are sometimes appear jagged or stair step.

Vector graphics scale without losing detail. Bitmaps suffer badly in some cases when they are scaled. So why don't we use vector graphics all the time?

Unfortunately the more complex the image the more difficult it is to render it using vector graphics. Photographic images are essentially impossible to render as vector graphics. On the other hand, things like technical or architectural drawings are ideal for vector graphics. That's why there are essentially two types of graphics editors. The drawing feature of Microsoft Word, for instance, is a vector graphics editor. Windows Paint is a bitmap editor. We can usually go from vector to bitmap, but going the other way is nearly impossible except for some operations like tracing the outline of an object.

Colors

We have been using colors and I have mentioned some basics about working with colors, but let's take a closer look at how a color can be represented. Every bitmap will have an associated color resolution . The color resolution is the number of bits used for the color information associated with a pixel. For example, four bits per pixel would only allow the specification of 16 different colors. Internally the Windows operating system uses 24 bits per pixel.

This means there are 2

24

or 16,777,216 different colors. This is more than

1

adequate for photographic quality. But how are these 24 bits used to represent a color? Read on.

Color Models

There are many different ways to represent a color. Some of the most popular are RGB, HSL (also known as HSI) and CMYK.

RGB is the most popular with programmers since it maps to the internal representation of color in the operating system. RGB is an acronym for Red

Green and Blue . These are the three additive primary colors. Your computer monitor or television works on the use of these three primaries. When we mix them together in different proportions we get a gamut of colors. It may surprise you that the gamut of a computer monitor or TV set is not capable of displaying every color you can perceive. This has to do with the limitations of the specific primaries used. The phosphors used for monitors and televisions are limited.

Don't worry about it though, there are enough colors to go around. The same limitations are found with the inks used by printers.

Under normal circumstances the 24 bits for an RGB color are weighted equally, 8 bits for each color. This means that each 8 bit value can range from 0 to 255. Pure red would be represented by the RGB value 255,0,0. Internally RGB colors are normally stored in 32 bit integers. The high order bits are set to zero. It is also possible to add an additional 8 bits to represent a transparency component. This is referred to as the alpha channel. It determines how much of an underlying color bleeds through when two colored objects are overlaid.

HSL stands for Hue, Saturation, Luminance. The Hue is color component.

For example, an orange shade. The saturation is how much white is added to the color. When we add a lot of white we get a pastel color. For example, pink rather than red. Luminance is the same as brightness. I personally find working with

HSL to be difficult, but the Windows color dialog offers the option.

CMYK is used for photographic printing. CMYK stands for Cyan, Magenta

Yellow, and black. Cyan, magenta and yellow are known as the subtractive primaries. Instead of adding them together we subtract them from white. The black component adds black to the result.

In most color editors you can create a color using on model and then see what it's values are in another. Ultimately you probably want to determine the

RGB components. I will use RGB or ARGB (with the alpha value).

Bitmap Files

We are all used to creating and using graphic files. There are a plethora of file formats. Fortunately the number of popular formats is small. .NET supports most of these. If a 24 bit bitmap is saved in a file directly we would require three times the height times the width bytes to save the file with 24 bit color. Consider a 1024 by 768 bitmap. It would require 2,359,296 bytes or over two megabytes.

2

To conserve space, especially for images downloaded to web pages, compressed file formats have been developed. These fall into two categories, those that compromise detail and those that don't. Lossless compression results in larger files. Usually compressing to a reasonable degree does not result in images that are noticeably poorer quality than the original.

Table 11-1 lists the most popular file formats that are recognized by the

.NET FCL. You can both read and write these files although you don't have total control of all of the details of some of the formats. Normally you will be reading these files and creating them with other tools. However, you can create these files in an application. I will explain how in a subsequent section.

Table 11-1 Bitmap File Formats (Partial list)

FIle Format

.bmp

.gif

.jpg

.png

.tiff

.exif

Standard Windows bitmap (not compressed).

Graphics Interchange Format (lossless compressions); popular for Web applications.

Joint Photographic Experts Group (compressed with loss of detail); extremely popular for all types of applications.

Portable Network Graphics (lossless compression) Newer standard for Web applications.

Tag Image File Format (various compression algorithms); older format that is still used.

Exchangeable Image File (uses JPEG compression); popular for digital cameras.

Something to keep in mind when using and creating bitmap files. The file extension is NOT used to determine the type of the file. Some books will tell you otherwise. You can change the name of a JPEG file to a BMP file and it will still be handled correctly. It is the format of the content that the .NET classes use to determine the type.

The Bitmap Class

Bitmap is actually derived from Image . Image is an abstract class and the primary class that derives from it is Bitmap . We can create a bitmap that is empty or create one from a file. There are quite a few constructors for a Bitmap but the most basic are shown in Table 11-2.

Table 11-2 Bitmap Constructors (Partial list)

Bitmao Constructor Description

Bitmap(Image original) Create a new bitmap from an existing Image object.

Bitmap(string filename) Creates a new bitmap from a file.

Bitmap(int width, int height) Creates a new bitmap that is empty (all pixels are zero) of the specified size.

3

Drawing a Bitmap

Displaying a newly created bitmap is a cinch. The DrawImage method of the Graphics class. There are many overloads as you might expect. These two statements will draw a bitmap file in the upper left hand corner of the client area:

Bitmap bm = new Bitmap("myfile.jpg"); g.DrawImage(bm, 0, 0); assuming that g is a Graphics object.

There are a couple of weaknesses to using these two simple statements.

Instantiating a new bitmap object every time we want to paint the client area is very expensive since the file has to be read over and over again. It is better to create all the bitmaps you will use when the program initializes. The form's constructor is a good place to do this. Another issue is that we should call thje

Dispose method of the Bitmap class when we are finished using it to free up the underlying Windows resource instead of waiting for garbage collection. Of course this isn't an issue of the bitmap is used for the life of the program. A major weakness is what happens if the image file can't be opened. An exception will be returned. An appropriate exception handler should be provided for a production application.

Table 11-1 shows the code for a really minimal application to display a bitmap. I stole the familiar bliss.bmp

image file from the Windows directory and placed it in the project's folder. Notice the relative path to access it. This image is

800 by 600 pixels and you can see a portion of it in the form shown in Table

11-2.

Bitmap1 - Form1.cs using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; namespace Bitmap1

{

public partial class Form1 : Form

{

private Bitmap bm;

public Form1()

{

InitializeComponent();

bm = new Bitmap(@"..\..\bliss.bmp");

}

protected override void OnPaint(PaintEventArgs e)

{

Graphics g = e.Graphics;

4

g.DrawImage(bm, 0, 0);

}

}

}

Figure 11-1

Figure 11-2

Resizing Bitmaps

But what is the real size of this image as displayed? Is it 800 by 600 or something else. As it turns out it is not 800 by 600 as you might expect. You can verify this by resizing the form to fill you screen. If you are running in at least

1024 by 768 then you can easily see the image is larger than 800 by 600. What's going on? Figure 11-3 shows the property summary for bliss.bmp

. I obtained this by right clicking the file name and selecting properties. The summary tab provides the information on the format of the image. As you can see it is 800 by

600 but the resolution is 72 dpi. This is not the same as the default 96 dpi resolution for the display. What happens is that the image is properly scaled to correspond. This means that the image is enlarged by a factor of 96/72. The result is 1067 by 800 pixels.

5

Figure 11-3

Maybe this is what you want and maybe it isn't. Perhaps the image came from a scanned photograph. If the image has been scanned at a high resolution, e.g., 600 dpi, then most likely you would want it to be scaled to more accurately represent the original picture size. On the other hand you might have created a bitmap using a bitmap editor where the resolution is unimportant and you really want to use the actual number of pixels. If you change the call to the

DrawImage method to g.DrawImage(bm, 0, 0, 160, 120); you get the result shown in Figure 11-4.

6

Figure 11-4

You can scale an image to increase or decrease its size and even change the aspect ratio . The latter operation will stretch the image horizontally or vertically as needed. Table 11-3 lists the Bitmap properties you can use to obtain this information at program execution time.

Table 11-3 Bitmap Properties (Partial List)

Bitmap Properties Description

Size Size int Width

Size in pixels (width and height)

Width in pixels int Height Height in pixels float HorizontalResolution Horizontal resolution in DPI float VerticalResolution Vertical resolution in DPI

If you always want to display a bitmap in its actual size in pixels you can use this method call: g.DrawImage(bm, 0, 0, bm.Width, bm.Height);

Here is a clever technique to display a bitmap that always fits in the client area without changing the aspect ratio: protected override void OnPaint(PaintEventArgs e)

{

Graphics g = e.Graphics;

SizeF cs = this.ClientSize; float ratio = Math.Min(cs.Height/bm.Height,

cs.Width/bm.Width); g.DrawImage(bm, 0, 0, bm.Width*ratio,

bm.Height*ratio);

}

7

Be sure to set the ResizeRedraw property of your form to true to ensure that the image is repainted when you resize the form. Also remember to do the calculation of the ratio using floating point arithmetic. Figure 11-5 shows a sample of the possible output. Of course you can add additional code to center the image and add a border around it.

Figure 11-5

Displaying Part of a Bitmap

Instead of resizing a bitmap we can select any rectangular area of the bitmap and display it at any location. This call to DrawImage displays a 100 by

100 pixel rectangle from the image in the upper left hand corner of the client area: g.DrawImage(bm, 0, 0, new Rectangle(0,0,100,100), GraphicsUnit.Pixel);

The result is shown in Figure 11-6.

Figure 11-6

8

Embedding Image Resources

If you always load bitmaps, icons, and cursors from files then there are several drawbacks:

1. You need to deploy the image file with the application.

2. If the image file is deleted the application will fail.

3. The end user can modify the application by editing the image file.

It is far better to embed the images in the executable file. This way they are always there and can't easily be modified.

In earlier versions of Visual Studio using embedded resources wasn't quite as convenient as it is with Visual Studio 2005. Some new features really automate the entire process. But before we take a look at the new features let's take a quick look at the older method.

You can easily add an image to your application by using an existing file or creating a new one. If you right click on the project name in the solution explorer and select Add/New Item the dialog in Figure 11-7 appears. Just select the type of image you want to create and the appropriate editor appears. If you want to add an existing image file just select Existing Item and an open file dialog appears to find the file you want.

Figure 11-7

9

After you add or create the new file it will appear in the solution explorer under the project node. In order to cause this image file to be embedded in the executable file (assembly) you need to set the Build Action property of the image file to Embedded Resource as shown in Figure 11-8. The file will now be available without the presence of the actual file. All you need to run the program is the .exe file.

Figure 11-8

The next step is to access the file from the assembly. In order to do this we need to know the name of the image that is created. The resource name is created by using the default namespace name followed by the relative pathname in the project folder hierarchy. In this case the file will appear as

MyNamespace.Bitmap.bmp

where MyNamespace is usually your project name unless you have changed it.

Bitmaps and other image classes such as Icon have an overloaded constructor that can create the resource from the resource name. However, it's not quite as simple as providing a string. The following is an example of constructing a Bitmap for Bitmap1.bmp

:

Bitmap bm = new Bitmap(GetType(), "Bitmap1.bmp");

The GetType method returns a Type object that corresponds to the form itself. The namespace associated with the Type is used as a prefix to the name of the file. If the image file was placed in a folder within the project then the relative pathname would need to be used. None of this is really tricky, but is not nearly as slick as the new capabilities on the Visual Studio 2005 designer. I won't go into a lot more detail here, but it gets a little more complicated to use this technique to place images on the surface of controls like a Button .

10

All this is a thing of the past with the use of the new Resource Designer available in Visual Studio 2005. The Resource Designer allows you to add the following resource types to the assembly:

1. Strings

2. Images

3. Icons

4. Audio

5. Files

6. Other types

The default is that these resources are placed in the Properties folder of the application. There are two specific files involved, Resources.resx

and

Resources.Desginer.cs

. Resources.resx

is an XML file with the specifics of the resources to be placed into the assembly. The information in this file is used to create the Resources.Desginer.cs

file. This is a C# file which has one class,

Resources . This class provides strongly typed get and set methods for the appropriate resources. Being strongly typed means we don't need to cast the type to what it actually represents. In earlier implementation when this class didn't exist it was necessary to cast the type that was returned from the

GetObject method of the ResourceManager class. The ResourceManager class is still used, but it's internal to the implementation of the Resources class.

Let's walk through a simple example that will display a bitmap from an embedded resource. I will use an existing image but you can just as easily create one from the internal bitmap editor or any bitmap editor you want. There are several ways to bring up the resource designer. One way is to double click on the

Resources.resx

file in the Properties folder. Select Images from the leftmost tab and then select Add Exisiting File from the drop down list on the Add Resource tab. Navigate to the file you want and select it. I added the file sadie.jpg

and the result is shown in Figure 11-9. As you can see the file has also been added to the project in the Resources folder. You can rename your images from the default which is the name of the image file without the file extension.

11

Figure 11-9

We're all set to use this bitmap in our program. This OnPaint method displays the bitmap: protected override void OnPaint(PaintEventArgs e)

{

Graphics g = e.Graphics;

Bitmap bm = Properties.Resources.sadie;

g.DrawImage(bm, 0, 0);

bm.Dispose();

}

The class is in the namespace MyProject.Properties

where

MyProject is the default namespace for the project. As you can see there is a property named sadie that is a strongly typed B itmap object that corresponds to our image. That's all there is to it. The output is shown in Figure 11-10. Sadie is one of my two Golden Retrievers. Both are Boston Red Sox fans as you might be able to see from Sadie's cap.

12

Figure 11-10

You might be interested on the actual implementation of the sadie property that is generated by the resource designer. Here it is: internal static System.Drawing.Bitmap sadie {

get {

object obj = ResourceManager.GetObject("sadie"),

resourceCulture);

return ((System.Drawing.Bitmap)(obj));

}

}

What's important to note is the cast of obj to Bitmap . That's where the strong typing comes from.

You can manage all your image resources very easily with the resource designer. Let's add an Icon and apply it to our application. I added the file user.ico

using the resource designer. I grabbed this icon from the Visual Studio

2005 Image Library I mention in Chapter <ref>. To set it as the application icon this line of code in the form's constructor is all we need:

Icon = Properties.Resources.user;

If you look carefully at Figure 11-11 you will see that the icon has changed.

13

Figure 11-11

Let’s explore icons in a bit more detail.

Icons

Icons are familiar to all users of any operating system with a graphical user interface. A basic Windows Forms application has two icons associated with it:

1. An icon for the application itself. This is associated with the .exe

file and is displayed when you view the folder where the file is located using the

Windows Explorer. A large size icon appears if you view the folder with the

Icon view, otherwise a small version is used. If you place a shortcut or the application itself on the desktop you will also see this icon.

2. An icon for the form itself. This icon appears in the upper left hand corner of the form and is used as a button to open the system menu. It also appears when you minimize the form to the task bar.

Standard icons are either 16 x 16 or 32 x 32 pixels in 16 colors. Other icon sizes and color depth are possible depending on the version of the operating system in use. You should always provide these standard icons. If a 32 x 32 icon is the only icon available then it will be scaled to 16 x 16 with sometimes marginal results. The reverse is also true. If a 32 x 32 pixel icon is required then the 16 x 16 pixel icon will be scaled up.

You can find lots of icons in the Visual Studio Image Library I discuss in

Chapter <ref>. If you want to create your own you can easily do it with the built in icon editor. Just right click on the project in the Solution Explorer and add a new icon type. The editor starts with an example 16 x 16 and 32 x 32 pixel icon. This editor is similar to the bitmap editor. Read the Visual Studio help files if you want all the fine points on its use. However, there is one detail I want to mention.

14

There are two special colors you can select for drawing your icon. One is the transparent color and the other is the inverting color. Wherever you draw the transparent color, the background pixels are shown when the icon is displayed.

Likewise the inverting color inverts the underlying background pixels. Inverting pixels is no longer in vogue. I am not very good at drawing icons so I will use one from the Visual Studio Image Library if it is appropriate.

To add an icon to your application requires opening the project options using the Project menu item. Under the Application tab you will find a text box to enter the path to an icon file. It should have the extension .ico

. You can also browse for this file. If you created the icon using the editor it should be in your project folder.

Changing the icon associated with the form is equally easy. There is a simpler method than the one I described in the previous section. You can do this all in one step in the designer by using the Icon property of the form itself. That lets you add the file to the resources and add a line of code to the hidden designer code to set up the icon. This same technique can be used to add images to buttons and any control that supports an image. You can use the same icon or a different one as you used for the application. The default icons are fine for testing, but you will probably want to add a little uniqueness to your application by changing both icons to ones of you choice.

Icons added in the manner I described above are automatically embedded in your application and so the original icon file only needs to be available if you rebuild your application. Be sure to provide both the 16 x 16 and 32 x 32 icons.

You can paint icons just like you can paint bitmaps. The DrawIcon method of the Graphics class is used. Just like with bitmaps we can use a file or an embedded resource. The following code displays both types:

Graphics g = e.Graphics;

Icon ic1 = new Icon(@"..\..\Icon1.ico"); g.DrawIcon(ic1, 10,10); g.DrawIcon(Properties.Resources.Icon1, 10, 40);

Cursors

In Chapter <ref> I showed you how to change the cursor while dragging an object. There are other situations in which you want to change the cursor from the default pointer. If you initiate a time consuming operation you might want to change the cursor to the hour glass cursor. The Cursors class has a large number of static properties that return a suitable instance of the Cursor class. If you change the cursor using the designer you can select from a drop down list. If you do this then the cursor will be changed for the form when it is initially displayed. We can change the cursor used with our form in a running program using the Cursor property like this:

Cursor = Cursors.WaitCursor;

15

resulting in the hour glass cursor being displayed. You can change it back with:

Cursor = Cursors.Arrow;

You can create your own cursor with the Visual Studio 2005 Cursor Editor.

The standard cursor is 32 x 32 monochrome. The cursor editor is very similar to the icon editor. It is very important to use the transparent color for a cursor in order for it to look good. Since the cursor is monochrome that means you have only black and white pixels. You can make shadow effects with patterns of black and white. The editor allows selecting a variety of these patterns. The result looks grey when viewed from a distance. Be sure to use both a black and a white outline for you cursor. This is essential if you want it to be visible when displayed over a very dark background or a very light background. A cursor that uses just the transparent color and white will completely disappear when over a white background.

A very important step in creating your own cursor is to set the hot spot .

The hot spot is the pixel position in the cursor that determines what the mouse coordinate is when the mouse buttons are pressed. You normally select an obvious point if the cursor is shaped anything like a pointer or a target. For cursors like the hour glass the center pixel might be appropriate. However, the purpose of such cursors is not to use them for selection purposes. You can set the cursor's hot spot by using the set hot spot tool in the cursor editor.

Zoom Example

Let's put things together with a nifty program that can be used to view graphics files. The Windows Picture and Fax Viewer is a similar application. The application you will see here lacks some of the capabilities of the Windows application, but it adds a feature as well. With our application you can zoom in by holding the left mouse button down and dragging a box around the area you would like to zoom in on. This area is expanded to fill the display area of the form. Sometimes the box you form around the area to be expanded is referred to as a rubber band box . I will also explore some problems that we haven't addressed before, such as flicker when painting.

Figure 11-12 shows the Bitmap Zoom application with the File menu opened. As you can see there is a menu strip. What you can't see is that there is a Panel that fills the remainder of the client area. The Dock style of the Panel has been set to Fill so that it takes up only the remaining client area below the menu.

If we don't use a panel and paint directly to the form then we would always need to account for the space taken by the menu strip. It's easy to paint to a Panel control as you learned in Chapter <ref>. In fact we will adding scrolling to this panel as well.

16

Figure 11-12

Clicking on Open brings up a standard open file dialog with the default file types set to .jpg, .bmp, and .gif. Figure 11-13 shows a sample directory.

Figure 11-13

17

Opening the file tawnie.jpg

is shown in Figure 11-14. The image is two large to fit. We can resize the form to see the whole image as shown in Figure

11-15.

Figure 11-14

Figure 11-15

Notice that a new menu item has appeared, Zoom. This allows us to zoom in and out. I also added hot keys F1 - F3 by using the properties for each menu item. These keys make zooming in and out easier than repetitively clicking on the menu. Figure 11-16 shows the menu options.

18

Figure 11-16

By zooming out we can view the entire image in a smaller window as shown in Figure 11-17. Notice that the scroll bars are no longer displayed since they are not needed. This action is automatic as you may recall from Chapter

<ref>. By the way, Tawnie is Sadie's sister and she is also a Red Sox fan. Figure

11-18 shows the image after zooming in on the letter B. If you zoom in or out the program zooms relative to the center of the displayed part of the image. You can restore the image to its original size as well.

Figure 11-17

19

Figure 11-18

The last feature to demonstrate is the ability to select an area of the image to blow up to fill the current client area. Figures Figure 11-19 and Figure 11-20 show the result of zooming in on a nose.

Figure 11-19

20

Figure 11-20

Figure 11-21 shows the complete program code. Let's take a look it each feature of the program in detail. As I mentioned, a menu strip and panel are placed in the client area of the form. The designer can be used to create the menu strip, but the panel must be handled explicitly for reasons I will discuss a bit later when I cover double buffering. For now let's just assume that we paint to a panel named client . The bitmap is painted with the method client_Paint which is the event handler for the Paint event for the panel. Scrolling is handled with the translate transform. The image is scaled to width and height which are initialized to the size of the bitmap and then later adjusted to the current zoom factor. If we are in the process of dragging the corner of the rubber band box then a rectangle is drawn corresponding to dragRect . This rectangle is specified in device coordinates and we need to adjust its position to take into account the scroll position.

Opening and closing the file are very straightforward. The OpenFileDialog object is used to obtain the file path. The bitmap is loaded and its size initialized.

The scrolling region is also set up. I also enable and make visible the appropriate menu items. Closing the file merely disposes of the current bitmap and changes the state of the relevant menu items. I make sure to set bm to null so that we don't attempt to paint a non-existent object.

Zooming gets a little messy. I selected a zoom out factor of 1.5. In other words the image is enlarged by 50% if we zoom out. I could have used a zoom factor of 0.5 to zoom in but I wanted the two to cancel out. In other words a zoom out followed by a zoom in would return the image to the same initial size. To accomplish that a zoom factor of 1/1.5 is required for the zoom in, or about 0.67.

To return to the initial bitmap size I use a zoom factor of 1/<current zoom>.

There may be some very slight round off errors by using this technique, but it reduces the code complexity since one common method, SetZoom can be called with one of these three values.

21

All zooms attempt to preserve the center position where possible. It is always possible when zooming out, but not always when zooming in. SetZoom also checks to make sure we don't try to zoom in or out beyond the capabilities of the operating system. I stop zooming in when the size gets smaller than 10 by 10 pixels or when the size exceeds a 16-bit integer's maximum value. The latter is an internal limitation in the GDI. The variable zf is the zoom factor and the following lines of code adjust the scroll position and the scrolling region's size:

Size cs = client.ClientSize;

PointF p = client.AutoScrollPosition;

//get current center point float dx = -p.X+cs.Width/2; float dy = -p.Y+cs.Height/2;

//scale to new position dx *= zf; dy *= zf;

//subtract client offset dx -= cs.Width / 2; dy -= cs.Height / 2;

//set new bounds client.AutoScrollMinSize = new Size((int)width, (int)height);

//scroll to new center point client.AutoScrollPosition = new Point((int)dx, (int)dy);

Basically what happens here is that the current center position of the displayed portion of the bitmap is scaled to the new position and the scroll position is set to place this same coordinate in the center as well. Remember that

AutoScrollPosition is negative. However, when setting its value you need to provide positive values for the coordinate. A bit confusing but that's the way the

.NET designers wanted it to be. This method works equally well for scrolling in or out. Both width and height are appropriately adjusted in this method as well.

Handling the rubber band box is very similar to the technique of dragging the recatngle I discussed in Chapter <ref>. The most complicated step is the

MouseUp event handler. We need to calculate the zoom factor as well as position the center point. Determining the zoom factor is just a matter of taking the smaller ratio of the box dimensions with the current panel size, This line of code does the trick: zf = Math.Min(cs.Width / dragRect.Width, cs.Height / dragRect.Height);

Adjusting the scrolling region and the scroll position are similar to the way it is done when zooming by explicit amounts except that the center position is not the previous center of the client area. This position is calculated with this code:

//find center of zoom box dx = (-p.X+dragRect.X+dragRect.Width/2); dy = (-p.Y+dragRect.Y+dragRect.Width/2);

//scale to new position dx *= zf; dy *= zf;

//subtract client offset

22

dx -= cs.Width / 2; dy -= cs.Height / 2; and then used after the call to SetZoom . If we don't do this then the center position will be at the previous center of the client area and not the center of the rubber band box.

BitmapZoom - Form1.cs using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; namespace BitmapZoom

{ public partial class Form1 : Form

{ private Bitmap bm; private RectangleF dragRect = new Rectangle (); private bool dragging = false ; private float zoom = 1.0f; //zoom factor private float width, height; public Form1()

{

InitializeComponent();

zoomToolStripMenuItem.Visible = false ; //hide initially

closeToolStripMenuItem.Enabled = false ;

ResizeRedraw = true ;

} private void client_Paint( object sender, PaintEventArgs e)

{ if (bm == null ) return ;

Graphics g = e.Graphics;

g.TranslateTransform(client.AutoScrollPosition.X,

client.AutoScrollPosition.Y);

g.DrawImage(bm, 0, 0, width, height); if (dragging)

{

RectangleF r = dragRect;

r.X -= client.AutoScrollPosition.X;

r.Y -= client.AutoScrollPosition.Y;

g.DrawRectangle( Pens .Black, r.X, r.Y, r.Width, r.Height);

}

} private void openToolStripMenuItem_Click( object sender,

EventArgs e)

{ if (openFile.ShowDialog( this ) == DialogResult .OK)

23

{ try

{

bm = new Bitmap (openFile.FileName);

width = bm.Width;

height = bm.Height;

zoom = 1.0f;

client.AutoScrollMinSize = new Size (( int ) width,

( int )height);

zoomToolStripMenuItem.Visible = true ;

closeToolStripMenuItem.Enabled = true ;

client.Invalidate();

} catch

{

MessageBox .Show( "Not an image file!" );

}

}

} private void closeToolStripMenuItem_Click( object sender,

EventArgs e)

{ if (bm!= null ) bm.Dispose();

bm = null ;

zoomToolStripMenuItem.Visible = false ;

closeToolStripMenuItem.Enabled = false ;

client.Invalidate();

} private void client_MouseDown( object sender, MouseEventArgs e)

{ if (e.Button == MouseButtons .Left)

{

dragRect.X = e.X;

dragRect.Y = e.Y;

dragging = true ;

}

} private void client_MouseMove( object sender, MouseEventArgs e)

{ if (e.Button == MouseButtons .Left)

{

dragRect.Width = e.X-dragRect.X;

dragRect.Height = e.Y-dragRect.Y;

client.Invalidate();

}

} private void client_MouseUp( object sender, MouseEventArgs e)

{ if (e.Button == MouseButtons .Left)

{

Size cs = client.ClientSize;

dragging = false ; float zf, dx, dy;

PointF p = client.AutoScrollPosition;

//ignore if rectangle is too small

24

if (dragRect.Width < 10 || dragRect.Height < 10)

{

client.Invalidate(); return ;

}

//calculate zoom factor

zf = Math .Min(cs.Width / dragRect.Width, cs.Height / dragRect.Height);

//find center of zoom box

dx = (-p.X+dragRect.X+dragRect.Width/2);

dy = (-p.Y+dragRect.Y+dragRect.Width/2);

//scale to new position

dx *= zf;

dy *= zf;

//subtract client offset

dx -= cs.Width / 2;

dy -= cs.Height / 2;

//zoom in/out

SetZoom(zf);

//scroll to center point

client.AutoScrollPosition = new Point (( int )dx,

( int )dy);

client.Invalidate();

}

} private void actualSizeToolStripMenuItem_Click( object sender,

EventArgs e)

{

SetZoom(1/zoom);

} private void inToolStripMenuItem_Click( object sender, EventArgs e)

{

SetZoom(1.5f);

} private void outToolStripMenuItem_Click( object sender,

EventArgs e)

{

SetZoom(1/1.5f);

} private void SetZoom( float zf)

{ if (bm == null ) return ; float lastzoom = zoom;

zoom *= zf;

width = bm.Width * zoom;

height = bm.Height * zoom; if (width < 10 || height < 10 || width > Int16 .MaxValue || height > Int16 .MaxValue)

{

MessageBox .Show( "Zoom range exceeded!" );

zoom = lastzoom;

width = bm.Width * zoom;

25

height = bm.Height * zoom;

} else

{

//adjust scroll position

Size cs = client.ClientSize;

PointF p = client.AutoScrollPosition;

//get current center point float dx = -p.X+cs.Width/2; float dy = -p.Y+cs.Height/2;

//scale to new position

dx *= zf;

dy *= zf;

//subtract client offset

dx -= cs.Width / 2;

dy -= cs.Height / 2;

//set new bounds

client.AutoScrollMinSize = new Size (( int )width,

( int )height);

//scroll to new center point

client.AutoScrollPosition = new Point (( int )dx,

( int )dy);

client.Invalidate();

}

} private void exitToolStripMenuItem_Click( object sender,

EventArgs e)

{

Close();

}

}

//derive from panel to allow double buffering to be set public class Client : Panel

{ public Client()

{

DoubleBuffered = true ;

}

}

}

Figure 11-21

Eliminating Flicker

If we were to create the panel named client using the designer the program would work except that there would be flicker as we dragged the mouse to enlarge or reduce the size of the rubber band box. This is due to the fact that when we invalidate any portion of the client area of a control that area is first erased by painting the control's background color. The brief appearance of this background color causes the image to flicker. We could reduce the size of the

26

invalidated area to just include the total bounds of the old and new rectangle position but we would still have flicker within the bounds of the invalidated area.

A very popular technique to eliminate flicker is to employ double buffering .

Basically what double buffering does is to paint the updated area of the display to a memory buffer rather than the video display. That way we never see the momentary display of the background color. When the painting is complete the resulting pixels are pushed to the video display. Voila, flicker free operation.

It used to be a manual procedure to do double buffering, but the .NET FCL provides an option for double buffering for all controls. In fact, it is even easier with .NET 2 with the inclusion of the new DoubleBuffered property. All we seemingly would have to do to turn this on would be t set that property to true .

We could easily do that for our form, but not for controls we add with the designer. Why? Well the .NET designers decided to make this property, and the earlier SetStyle method call, protected . That means we can only set this property within the class itself or a derived class. If we were to try we would get a compiler error.

My solution was to create a class, Client , that is derived from Panel . The

DoubleBuffered property can then be set in the constructor. public class Client : Panel

{

public Client()

{

DoubleBuffered = true;

}

}

At this point you might be wondering if you can use you derived class just like you would a panel. In other words, can you drag it to you form using the toolbox. Visual Studio 2005 does some incredible things to make your life easier.

If you create a class derived from a control it is automatically added to the toolbox when you build your project. The secret is to just create the new class and then build your project before you try to access your new class in the toolbox. Figure 11-22 shows a cog wheel symbol beside your new panel class.

Just use it as you normally would any other control.

Figure 11-22

27

Figure 11-23 shows the panel after I dragged it to the form. Next set up the docking style to DockStyle.Fill

and add event handlers for the three mouse events. Be sure you add these event handlers to your new panel control. Also be sure to add the panel after the menu.

Figure 11-23

Drawing on a Bitmap

It is extremely simple to create your own bitmaps programmatically. The uses are endless. You could, of course, create your own custom bitmap editor.

But simple applications abound. Have you seen the use of small bitmaps that are displayed on web pages with a password that needs to be typed in to register for something using an online form. The purpose is to foil attempts by spammers to automatically register an account to propagate spam. It is much more difficult to extract this password from a bitmap image, especially if strange fonts are used.

Another simple application might be to generate a stylistic hit counter to display on a web page.

You can draw anything on a bitmap that you can paint to the display. We can obtain a Graphics object associated with a bitmap and use it as we would normally use it in the OnPaint method. The first step is to create an empty bitmap.

Bitmap bm = new bm(160, 120);

A newly created bitmap is initialized with zero for each pixel. That means that unless we set the background it will be black. The Clear method can be called to set all the pixels to a specific color. We can then obtain a Graphics object.

Graphics g = Graphics.FromImage(bm);

28

Then we can use this Graphics object to draw whatever we want. Figure 11-24 demonstrates a simple program to draw on a bitmap and then display it. A left mouse click creates and draws the bitmap and a right click deletes the bitmap.

The output is shown in Figure 11-25.

Bitmap Draw - Form1.cs using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; namespace BitmapDraw

{

public partial class Form1 : Form

{

Bitmap bm = null;

public Form1()

{

InitializeComponent();

}

private void Form1_MouseDown(object sender, MouseEventArgs e)

{

if (e.Button == MouseButtons.Left)

{

if (bm == null) bm = new Bitmap(160, 120);

Graphics g = Graphics.FromImage(bm);

g.Clear(Color.White);

g.DrawString("Bitmap Draw.", Font, Brushes.Black,

10, 10);

g.FillRectangle(Brushes.Red, 10, 30, 50, 50);

g.Dispose();

Invalidate();

}

else

{

bm.Dispose();

bm = null;

Invalidate();

}

}

protected override void OnPaint(PaintEventArgs e)

{

if (bm!=null)

e.Graphics.DrawImage(bm, 0, 0, bm.Width, bm.Height);

}

}

}

Figure 11-24

29

Figure 11-25

Saving a Bitmap to a File

This is very simple. The Save method of the Image class will write an image file in one of the supported formats. For example, we can save a bitmap in the JPEG format using the following call: bm.Save("filename.jpg", ImageFormat.Jpeg);

If we leave out the second argument the image is drawn in PNG format. It is important to remember that the file extension you use has nothing to do with the actual format. You can name the file anything you want even if it makes no sense. However, you can't save an image to the same file it was created from.

This really couldn't be easier

Using Transparency

<TBA>

Remapping Colors

<TBA>

30

Download