Teach Yourself Borland Delphi 4 in 21 Days

Previous chapterNext chapterContents


- 12 -

Graphics and Multimedia Programming


Graphics and multimedia programming represent the fun part of programming. In this chapter, you are introduced to graphics and multimedia programming with Delphi. In the case of graphics programming, most of that introduction comes in the form of an examination of the TCanvas and TBitmap classes.

I start with a look at the easiest ways to display graphics in Delphi. After that, you learn about the Windows Graphics Device Interface and the components that make up that interface. Along the way, you learn about the various line and shape drawing routines and the different ways to display bitmaps. Later in the day, you learn about offscreen bitmaps and how they can benefit you. The multi-media programming sections deal with how to play sound files with the Windows API. You also learn how to play wave audio, MIDI, and AVI video files using the TMediaPlayer class. So let's get started!

Graphics the Easy Way

Graphics programming does not have to be difficult. Sometimes all you need to do is display a picture or a simple shape on a form. VCL provides ready-made components for those times. I'll take a brief look at some of these components before moving on to real graphics programming.

The Shape component (found on the Additional tab of the Component palette) can be used to add simple shapes to your forms. Using the Shape component is easy. Just drop one on a form and change the Brush, Pen, and Shape properties as desired. You can draw circles, ellipses, squares, rectangles, and rectangles with rounded edges. Change the Brush property to modify the background color of the shape. Change the Pen property to change the color or thickness of the border surrounding the shape.

An Image component can be used to display a bitmap on a form. This component is great for many graphics operations, including a bitmap background for a form. The Picture property of TImage is an instance of the TPicture class. You can select the picture at design time through the Object Inspector, or you can load a picture at runtime. For example, here's how you change the image in the component at runtime:

Image1.Picture.Bitmap.LoadFromFile(`bkgnd.bmp');

The Stretch property determines whether the image will be enlarged or compressed to fit the size of the component. The Center property determines whether the bitmap is centered in the component. The AutoSize property can be used to force the component to size itself according to the size of the image.

I'll also mention the PaintBox component here. If you want drawing confined to a certain area of a form, this component provides a canvas on which you can draw. The only significant property of the PaintBox component is the Canvas property. This property is an instance of the TCanvas class. It is with this class that most drawing is done in a Delphi application. Let's look at the TCanvas class now.

Device Contexts and TCanvas

Windows uses the term device context to describe a canvas on which you can draw. A device context can be used to draw on many surfaces:

These are just a few examples. There are other, more obscure device contexts (menus, for example), but those listed above are the device contexts that you will be most interested in.

Dealing with device contexts at the API level can be a bit complex. First, you have to obtain a handle to a device context from Windows. Then you have to select various objects into the device context (pens, brushes, or fonts). After that, you can draw on the device context. When you are done drawing, you have to be sure that the objects you select into the device context are removed before deleting the device context. If you forget to remove objects selected into the device context, your application will leak memory (use up memory that is never released back to the system). It's a tedious process to say the least.

The good news is that VCL provides the TCanvas class to make dealing with device contexts easier. Let me give you a quick example. The following code uses the Windows API to draw a circle on the screen with a blue outline and red interior:

procedure TForm1.Button1Click(Sender: TObject);
var
  DC : HDC;
  Brush, OldBrush : HBrush;
  Pen, OldPen : HPen;
begin
  DC := GetDC(Handle);
  Brush := CreateSolidBrush(RGB(255, 0, 0));
  Pen := CreatePen(PS_SOLID, 1, RGB(0, 0, 255));
  OldBrush := SelectObject(DC, Brush);
  OldPen := SelectObject(DC, Pen);
  Ellipse(DC, 20, 20, 120, 120);
  SelectObject(DC, OldBrush);
  SelectObject(DC, OldPen);
  ReleaseDC(Handle, DC);
end;

This code isn't so terribly bad, but it's easy to forget to restore objects when you are done with them. When that happens, your application will leak resources. Now look at the equivalent VCL code:

Canvas.Brush.Color := clRed;
Canvas.Pen.Color := clBlue;
Canvas.Ellipse(20, 20, 120, 120);

Not only is this code shorter and more readable, it is also much more robust. The TCanvas class takes care of freeing resources as needed, so you don't have to worry about it. TCanvas is a simpler and more effective approach than using the API.

The TCanvas class has many properties and methods. I'll look at some of these properties and methods while working through today's material. Table 12.1 lists the primary properties of TCanvas and Table 12.2 lists the primary methods.

TABLE 12.1. PRIMARY TCanvas PROPERTIES.

Property Description
Brush The brush color or pattern used for filling shapes.
ClipRect The current clipping rectangle for the canvas. Any drawing is confined to this rectangle. This property is read-only.
CopyMode Determines how drawing will be performed (normal, inverse, xor, and so on).
Font The font the canvas uses for drawing text.
Handle The handle (HDC) of the canvas. Used when calling the Windows API directly.
Pen Determines the style and color of lines drawn on the canvas.
PenPos The current drawing position in x and y coordinates.
Pixels An array of the canvas' pixels.

TABLE 12.2. PRIMARY TCanvas METHODS.

Method Description

ARC

DRAWS AN ARC ON THE CANVAS USING THE CURRENT PEN.

BrushCopy Displays a bitmap with a transparent background.
CopyRect Copies part of an image to the canvas.
Draw Copies an image from memory to the canvas.
Ellipse Using the current pen, draws an ellipse that is filled with the current brush on the canvas.
FloodFill Fills an area of the canvas with the current brush.
LineTo Draws a line from the current drawing position to the location specified in the x and y parameters.
MoveTo Sets the current drawing position.
Pie Draws a pie shape on the canvas.
Polygon Draws a polygon on the canvas from an array of points and fills it with the current brush.
Polyline Using the current pen, draws a line on the canvas from an array of points. The points are not automatically closed.
Rectangle Draws a rectangle on the canvas outlined by the current pen and filled with the current brush.
RoundRect Draws a filled rectangle with rounded corners.
Method Description
StretchDraw Copies a bitmap from memory to the canvas. The bitmap is stretched or reduced according to the size of the destination rectangle.
TextExtent Returns the width and height in pixels of the string passed in the Text parameter. The width is calculated using the current font of the canvas.
TextHeight Returns the height in pixels of the string passed in the Text parameter. The width is calculated using the current font of the canvas.
TextOut Using the current font, draws text on the canvas at a specified
location.
TextRect Draws text within a clipping rectangle.

Believe it or not, these properties and methods represent just a small part of the functionality of a Windows device context. The good news is that these properties and methods cover 80 percent of the tasks you will need to perform when working with graphics. However, before I can talk more about the TCanvas class in detail, I need to talk about graphics objects used in Windows programming.

GDI Objects

The Windows Graphics Device Interface (GDI) has many types of objects that define how a device context functions. The most commonly used GDI objects are pens, brushes, and fonts. Other GDI objects include palettes, bitmaps, and regions. Let's take a look at pens, brushes, and fonts first and then move on to more complex objects.

Pens, Brushes, and Fonts

Pens, brushes, and fonts are straightforward. Let's take a brief look at each of these GDI objects and how the TCanvas class uses them.


Pens

A pen defines the object that is being used to draw lines. A line might be a single line from one point to the next, or it might be the border drawn around rectangles, ellipses, and polygons. The pen is accessed through the Pen property of the TCanvas class and is an instance of the TPen class. Table 12.3 lists the properties of TPen. There are no methods or events of TPen that are worthy of note.

TABLE 12.3. TPen PROPERTIES.

Property Description
Color Sets the color of the line.
Handle The handle to the pen (HPEN). Used when calling the GDI directly.
Mode Determines how lines will be drawn (normal, inverse, xor, and so on).
Style The pen's style. Styles can be solid, dotted, dashed, dash-dot, clear, and more.
Width The width of the pen in pixels.

For the most part, these properties are used exactly as you would expect. The following example draws a red, dashed line:

Canvas.Pen.Color := clRed;
Canvas.Pen.Style := psDash;
Canvas.MoveTo(20, 20);
Canvas.LineTo(120, 20);

To test this code, drop a button on a form and type the code in the OnClick handler for the button. When you click the button, the line will be drawn on the form.


NOTE: The simple examples in this chapter can all be tested in this manner. However, if you cover the application and bring it to the top, the drawing will be gone. This is because the drawing is temporary. If you want the drawing to be persistent, place the drawing code in the OnPaint event handler for the form. Any time a window needs to be repainted, its OnPaint event is generated and your drawing code will be executed.

The dashed and dotted pen styles can be used only with a pen width of 1. The psClear pen style can be used to eliminate the line that the Windows GDI draws around the outside of objects such as rectangles, ellipses, and filled polygons.


TIP: You can experiment with the various properties of TPen by dropping a Shape component on a form and modifying the shape's Pen property. This is especially useful for seeing the effects of the Mode property of the TPen class.

Brushes

A brush represents the filled area of a graphical shape. When you draw an ellipse, rectangle, or polygon, the shape will be filled with the current brush. When you think of a brush you probably think of a solid color. Many times this is the case, but a brush is more than just a color. A brush can also include a pattern or a bitmap. The TCanvas class has a property called Brush, which you can use to control the appearance of the brush. The Brush property is an instance of the TBrush class. As with TPen, the TBrush class doesn't have any methods or events of note. The TBrush properties are listed in Table 12.4.

TABLE 12.4. TBrush PROPERTIES.

Property Description
Bitmap The bitmap to be used as the brush's background. For Windows 95 the bitmap must be no larger than 8¥8.
Color Sets the color of the brush.
Handle The handle to the brush (HBRUSH). Used when calling the GDI
directly.
Style The brush's style. Styles include solid, clear, or one of several patterns.

By default, the Style property is set to bsSolid. If you want a pattern fill, you should set the Style property to one of the pattern styles (bsHorizontal, bsVertical, bsFDiagonal, bsBDiagonal, bsCross, or bsDiagCross). The following example draws a circle on the form using a 45 degree hatched pattern. Figure 12.1 shows the form when this code executes.

Canvas.Brush.Color := clBlue;
Canvas.Brush.Style := bsDiagCross;
Canvas.Ellipse(20, 20, 220, 220);

FIGURE 12.1. A circle filled with a hatched brush.

When using a pattern brush, the brush's Color property defines the color of the lines that make up the pattern. For some reason, VCL automatically forces the background mode to transparent when using a pattern fill. This means that the background color of the brush will be the same as the background color of the window on which the shape is drawn.

Take another look at Figure 12.1 and you will see that the background color of the circle is the same color as the form (I know it's not easy to see in grayscale). If you want to specify a background color, you need to circumvent VCL and go to the API. Here's how the code would look if you want to use a blue hatch on a white background:

Canvas.Brush.Color := clBlue;
Canvas.Brush.Style := bsDiagCross;
SetBkMode(Canvas.Handle, OPAQUE);
SetBkColor(Canvas.Handle, clWhite);
Canvas.Ellipse(20, 20, 220, 220);

Now the background color of the brush will be white. Figure 12.2 shows the circle with the new code applied.

FIGURE 12.2. The hatched brush with a white background.

Another interesting feature of brushes is the bitmap background option. First look at the code and then I'll tell you more about bitmap brushes. Here it is:

Canvas.Brush.Bitmap := TBitmap.Create;
Canvas.Brush.Bitmap.LoadFromFile(`bkgnd.bmp');
Canvas.Ellipse(20, 20, 220, 220);
Canvas.Brush.Bitmap.Free;

The first line in this code snippet creates a TBitmap object and assigns it to the Bitmap property of the brush. The Bitmap property is not assigned by default, so you have to specifically create a TBitmap object and assign it to the Bitmap property. The second line loads a bitmap from a file. The bitmap must be no larger than 8 pixels by 8 pixels. You can use a larger bitmap, but it will be cropped to 8¥8. The third line in this example draws the ellipse. After the ellipse has been drawn, the Brush property is deleted. It is necessary to delete the Brush property because VCL won't do it for you in this case. If you fail to delete the Brush property, your program will leak memory. Figure 12.3 shows the ellipse drawn with a bitmap brush.

FIGURE 12.3. An ellipse with a bitmap brush.

Sometimes you need a hollow brush. A hollow (or clear) brush enables the background to show through. To create a hollow brush, just set the brush's Style property to bsClear. Let's take the previous example and add a second circle inside the first using a hollow brush. Figure 12.4 shows the results. Here's the code:

Canvas.Pen.Width := 1;
Canvas.Brush.Bitmap := TBitmap.Create;
Canvas.Brush.Bitmap.LoadFromFile(`bkgnd.bmp');
Canvas.Ellipse(20, 20, 220, 220);
Canvas.Brush.Style := bsClear;
Canvas.Pen.Width := 5;
Canvas.Ellipse(70, 70, 170, 170);
Canvas.Brush.Bitmap.Free;

FIGURE 12.4. A circle with a hollow brush.

You can do other things with brushes if you go directly to the API. Most of the time, though, the VCL TBrush class does the job.

Fonts

Fonts aren't anything new to you; you have been using them throughout the book. Fonts used with the TCanvas class are no different than those used with forms or other components. The Font property of TCanvas is the same as the Font property of 
any other component. To change the font for the canvas, just do this:
Canvas.Font.Name := `Courier New';
Canvas.Font.Size := 14;
Canvas.Font.Style := Canvas.Font.Style + [fsBold];
Canvas.TextOut(20, 20, `Testing');

That's all there is to it. I'll talk more about what to do with fonts a little later in the section "Drawing Text."

Bitmaps and Palettes

Bitmaps and palettes go together most of the time. The TBitmap class encapsulates a bitmap object in Delphi. Loading and displaying bitmaps is easy when using this class. You already saw TBitmap in action in the Jumping Jack program on Day 8, "Creating Applications in Delphi." The TBitmap class is used in a wide variety of situations. I'll look at some of those situations later today when talking about drawing bitmaps and about memory bitmaps. The TBitmap class is complex, so I won't go over every property and method here.

Palettes are one of the most confusing aspects of Windows programming. Most of the time the palette is maintained by the TBitmap object, so you really don't have to worry about it. Rather than trying to explain the importance of palettes, let me show you an example.

Start a new application and type the following code in the OnPaint event handler, or use a button click event. Be sure to enter the correct path to the HANDSHAK.BMP file. (You should find it in the Borland Shared Files/Images/Splash/256Color directory.) Here's the code:

var
  Bitmap : TBitmap;
begin
  Bitmap := TBitmap.Create;
  { Bitmap.IgnorePalette := True; } 
  Bitmap.LoadFromFile(`handshak.bmp');
  Canvas.Draw(0, 0, Bitmap);
  Bitmap.Free;
end;
Notice that one line is commented out with curly braces. Run the program and you will see a nice bitmap on the form. Now uncomment the line that is commented out. This line tells the VCL to ignore the palette information when displaying the 
bitmap. Run the program again. This time you should notice that the bitmap's colors are all wrong (you might not notice this effect if your video adapter is set to display more than 256 colors). This is because the palette is not being applied. The 
palette makes sure that the correct colors for the bitmap are mapped to the system palette.

Bitmap and palette objects play an important role in graphics operations. It takes some time to understand everything that they entail, so don't feel bad if you don't catch onto it right away.

Clipping Regions

Regions are areas of the screen that can be used to control the parts of the canvas that can be drawn on. The TCanvas class has a ClipRect property, but it is read-only. To change the clipping region, you have to use the Windows API. Let's take the previous example and modify it slightly to illustrate how clipping regions work. Here's the code:

var
  Bitmap : TBitmap;
  Rgn : HRGN;
begin
  Bitmap := TBitmap.Create;
  Bitmap.LoadFromFile(`handshak.bmp');
  Rgn := CreateRectRgn(50, 50, 250, 250);
  SelectClipRgn(Canvas.Handle, Rgn);
  Canvas.Draw(0, 0, Bitmap);
  Bitmap.Free;
end;

Now when you run the program you will see that only a portion of the bitmap is displayed. The SelectClipRgn function sets the canvas' clipping region to the rectangle identified by the coordinates 50, 50, 250, and 250. The bitmap is still being drawn in the same location it was before, yet now only a portion of the bitmap (defined by the clipping region) can be seen. Everything outside the clipping region is ignored.

Regions don't have to be rectangular. Let's take the previous example and make it more interesting. Remove the line that creates the rectangular region and replace it with this line:

Rgn := CreateEllipticRgn(30, 30, 170, 170);
Now run the program again. This time, the bitmap is limited to a circular region.  Figure 12.5 shows the program with the elliptical region in place.

FIGURE 12.5. An elliptical clipping region.

Let's try another type of region. Again, remove the line that defines the region and replace it with these lines:

const
  Points : array[0..3] of TPoint =
    ((X:80;Y:0), (X:0;Y:80), (X:80;Y:160), (X:160;Y:80));
var
  Bitmap : TBitmap;
  Rgn : HRGN;
begin
  Bitmap := TBitmap.Create;
  Bitmap.LoadFromFile(`handshak.bmp');
  Rgn := CreatePolygonRgn(Points, 4, ALTERNATE);
  SelectClipRgn(Canvas.Handle, Rgn);
  Canvas.Draw(0, 0, Bitmap);
  Bitmap.Free;
end;

This time you are using a polygon region. The points array defines the points that create the region. The CreatePolygonRgn function creates a region from a series of points. You can use as many points as you want. You don't even have to specify the closing point because the region is automatically closed between the first point and the last point. Run the program again and see what you get. Figure 12.6 shows the results of this exercise.

FIGURE 12.6. A polygon clipping region.


NOTE: This code snippet also shows how to initialize a const array of records. Here's the code I'm referring to:

const
  Points : array[0..3] of TPoint =
    ((X:80;Y:0), (X:0;Y:80), (X:80;Y:160), (X:160;Y:80));



This code declares an array of TPoint records and initializes it with data. TPoint has two fields: X and Y. Notice that the field name is listed, followed by a colon and the value to be assigned to that field (X:80, for example). Notice also that both the X and Y fields are assigned values and those values are paired with parentheses. This is done four times because the array has four elements. This is the only way to declare and initialize a const array of records.


Regions can be very useful when you are doing certain kinds of drawing operations. You might not need to use clipping regions often, but when you need them, they are invaluable.

Basic Drawing Operations

You have already encountered some of the basic graphics routines as you have worked through the book. By now you know that the Rectangle method is used to draw squares and rectangles, the Ellipse method is used to draw circles and ovals, and that the MoveTo and LineTo methods are used to draw lines.

There is also the Arc method for drawing arcs and the Pie method for drawing pie-shaped objects. All in all, it's fairly basic. There's not much point in going into a lot of detail on the methods of TCanvas. Instead, let's move on to the more interesting (and troublesome) graphics operations you are likely to encounter when writing Delphi applications.

Drawing Text

Drawing text doesn't sound like it should be too difficult, does it? The truth is that there are several little pitfalls that, if you aren't aware of them, can make drawing text a difficult experience. In addition, there are several nice text drawing features that you should know about.

The TextOut and TextRect Methods

The TextOut method is the most basic way to draw text on a canvas. There really isn't too much to say about TextOut. You just pass it the X position, the Y position, and the text to display--for example,

Canvas.TextOut(20, 20, `Joshua Reisdorph');

This code displays the given string at position 20, 20 on the form. The X and Y coordinates specify the top left corner of the text to be drawn, not the baseline. To illustrate what I mean, test this code:

Canvas.TextOut(20, 20, `This is a test.');
Canvas.MoveTo(20, 20);
Canvas.LineTo(100, 20);

This code displays some text at position 20, 20 and then draws a line from that same position to position 100, 20. Figure 12.7 shows the results of this code. Notice that the line is drawn across the top of the text.

FIGURE 12.7. Text drawn with TextOut.

Use TextOut whenever you have text to display that doesn't need a lot of fine positioning.

The TextRect method enables you to specify a clipping rectangle in addition to the text to be displayed. Use this method when the text needs to be constrained within certain boundaries. Any of the text that falls outside the boundary will be clipped. The following code snippet ensures that no more than 100 pixels of the text will be displayed:

Canvas.TextRect(Rect(20, 50, 120, 70), 20, 50,
  `This is a very long line that might get clipped.');
Both TextOut and TextRect can only draw single lines of text. No wrapping of the text is performed.


TIP: To draw text with tab stops, see the Windows API function TabbedTextOut.

Text Backgrounds

Refer to Figure 12.7. Notice that the text has a white background--not very appealing on a gray form. The text background is obtained from the current brush (white by default). To remedy the unsightly results of Figure 12.7, you need to take one of two actions: Either change the color of the canvas brush, or make the text's background transparent.

Changing the background color of the text is fairly easy. The question is, do you know what color to make the text's background? In this case, the text's background can be the same color as the form, so you can do this:

Canvas.Brush.Color := Color;

This code will work for most situations, but in some cases you need more control. It would be easier if you could just make the background of the text transparent. The good news is, you can. Here's how the code would look:

var
  OldStyle : TBrushStyle;
begin
  OldStyle := Canvas.Brush.Style;
  Canvas.Brush.Style := bsClear;
  Canvas.TextOut(20, 20, `This is a test.');
  Canvas.Brush.Style := OldStyle;
end;
First, save the current brush style. Next, set the brush style to transparent (bsClear). After you display the text, set the brush style back to what it was before. You should get into the habit of saving the previous style and resetting it when 
you are done drawing the text. It is unlikely that you'll want to leave the brush style set to transparent, so resetting the previous style is always a good idea.

Using a transparent background has other advantages as well. Let's say you want to display some text over a bitmap background. In that case you can't use a solid background. Here's a code snippet that illustrates the point (the FACTORY.BMP file can be found in your Borland Shared Files56Color directory):

var
  OldStyle : TBrushStyle;
  Bitmap   : TBitmap;
begin
  Bitmap := TBitmap.Create;
  Bitmap.LoadFromFile(`factory.bmp');
  Canvas.Draw(0, 0, Bitmap);
  Canvas.Font.Name := `Arial Bold';
  Canvas.Font.Size := 13;
  OldStyle := Canvas.Brush.Style;
  Canvas.Brush.Style := bsClear;
  Canvas.TextOut(20, 5, `Transparent Background');
  Canvas.Brush.Style := OldStyle;
  Canvas.TextOut(20, 30, `Solid Background');
  Bitmap.Free;
end;

This code draws a bitmap on the form. Next, text is drawn on the form (over the bitmap) with a transparent background. After that, more text is drawn with the regular background. Figure 12.8 shows the results of this code. As you can see, making the background transparent creates a much more appealing image.

FIGURE 12.8. Text drawn over a bitmap with transparent and solid backgrounds.

Another reason for using transparent backgrounds for text is illustrated on Day 13 in the section, "Owner-Drawn Status Panels." There you give the status bar text a 3D look by drawing the text once in white and drawing it again, slightly offset, in black. The only way that code works is by using a transparent background. As you can see, sometimes a transparent background is the only option for achieving the effect you are looking for.

The DrawText Function

The Windows API DrawText function gives you much greater control over text that is drawn on the canvas than does TextOut. For some reason, the TCanvas class does not have a DrawText method. To use DrawText, then, means using the API directly. First, let's look at a basic DrawText example and then I'll tell you more about the power of this function:

var
  R : TRect;
begin
  R := Rect(20, 20, 220, 80);
  Canvas.Rectangle(20,20, 220, 80);
  DrawText(Canvas.Handle, `An example of DrawText.',
    -1, R, DT_SINGLELINE or DT_VCENTER or DT_CENTER);
end;

Figure 12.9 shows the results of this code as well as results from the following examples.

FIGURE 12.9. Examples of the DrawText function.


First, a TRect record is initialized using the Windows API Rect function. After that, a regular rectangle is drawn on the canvas. (The rectangle is drawn on the canvas so that you can visualize the size of the rectangle that you are about to draw on.) Finally, the DrawText function is called to draw the text.

Let's take a minute to discuss the various parameters of this function, as follows:

The previous example illustrates one of the DrawText function's most common uses: to center text either horizontally, vertically, or both. This is a great feature when doing owner-drawing of components. In particular, owner-drawn list boxes, combo boxes, and menus often need to center text. You might not realize the benefit of this function right now, but you will if you start doing owner-drawn components, or if you start writing your own graphical components.

Another interesting flag of DrawText is the DT_END_ELLPSIS flag. If the text is too long to fit in the specified rectangle, Windows truncates the string and adds an ellipsis to the end, indicating the string was truncated. Take this code, for example:

var
  R : TRect;
begin
  R := Rect(20, 100, 120, 150);
  DrawText(Canvas.Handle, `This text is too long to fit.',
    -1, R, DT_END_ELLIPSIS);
end;

When this code is executed, it will result in this text being displayed:

This text is too long...

You can use this flag any time you anticipate text that could be too long for the rectangle in which the text is drawn.

DT_CALCRECT is another flag that is invaluable. It calculates the height of the rectangle needed to hold the specified text. When you use this flag, Windows calculates the needed height and returns that height but doesn't draw the text. You tell Windows how wide the rectangle should be, and Windows will tell you how high the rectangle needs to be to contain the text. In fact, Windows also modifies the bottom and left members of the rectangle passed in so that it contains the needed values. This is particularly important when drawing multiple lines of text.

The following example asks Windows how high the rectangle needs to be to contain all of the text. After that, a rectangle is drawn onscreen. Finally, the text is drawn in the rectangle. Here's the code:

var
  R : TRect;
  S : string;
begin
  R := Rect(20, 150, 150, 200);
  S := `This is a very long string which will ` +
    `run into multiple lines of text.';
  DrawText(Canvas.Handle, PChar(S),
    -1, R, DT_CALCRECT or DT_WORDBREAK);
  Canvas.Brush.Style := bsSolid;
  Canvas.Rectangle(R.left, R.top, R.right, R.bottom);
  Canvas.Brush.Style := bsClear;
  DrawText(Canvas.Handle, PChar(S), -1, R, DT_WORDBREAK);
end;

Notice that you have to cast S to a PChar for the second parameter of DrawText. This is necessary because DrawText wants a pointer to a character array for the text parameter and not a string type.


NOTE: If you are writing components that can be used with all versions of Delphi, you cannot use the cast from string to PChar. This cast won't compile under Delphi 1. Instead, you have to use the StrPCopy function as follows:

var
  Temp : array [0..50] of Char;
  R : TRect;
  S : string;
begin
  DrawText(Canvas.Handle, StrPCopy(Temp, S),
    -1, R, DT_CALCRECT or DT_WORDBREAK);
{ etc. } 



If you don't have to worry about supporting Delphi 1, you can cast the string to a PChar as shown previously.


Place this code in the OnPaint event handler for a form. Run the program several times and modify the length of the text string that is displayed. Notice that no matter how much text you add to the string, the rectangle will always be drawn precisely around the text. Refer to Figure 12.9 for the results of this exercise as well as the results of the previous exercises on DrawText.


TIP: If you need even more text drawing options, you can use the DrawTextEx function. See the Win32 API online help for full details.

NOTE: Drawing text with DrawText is slightly slower than using TextOut. If your drawing operations are speed-sensitive, you should use TextOut rather than DrawText. You'll have to do more work on your own, but the execution speed will likely be better. For most drawing, though, you won't notice any difference between TextOut and DrawText.

DrawText is a very useful and powerful function. When you start writing your own components, you will no doubt use this function a great deal.

Drawing Bitmaps

Drawing bitmaps sounds like it should be difficult, yet as you have seen several times up to this point, drawing bitmaps is very easy. The TCanvas class has several methods for drawing bitmaps. The most often-used method is the Draw method. This method simply draws a bitmap (represented by a descendant of the TGraphic class) onto the canvas at the specified location. You've seen several examples up to this point, but here's another short example:

var
  Bitmap : TBitmap;
begin
  Bitmap := TBitmap.Create;
  Bitmap.LoadFromFile(`c:\winnt\winnt256.bmp');
  Canvas.Draw(0, 0, Bitmap);
  Bitmap.Free;
end;

This code creates a TBitmap object, loads the file called WINNT256.BMP, and displays it in the upper-left corner of the form. Use Draw when you want to display bitmaps without modification.

The StretchDraw method is used when you want to alter a bitmap's size. You specify a rectangle for the location of the bitmap and an image to draw. If the supplied rectangle is larger than the original size of the bitmap, the bitmap will be stretched. If the rectangle is smaller than the bitmap's original size, the bitmap will be reduced to fit. Here's an example:

var
  Bitmap : TBitmap;
  R : TRect;
begin
  Bitmap := TBitmap.Create;
  Bitmap.LoadFromFile(`c:.bmp');
  R := Rect(0, 0, 100, 100);
  Canvas.StretchDraw(R, Bitmap);
  Bitmap.Free;
end;


NOTE: No attempt is made by StretchDraw to maintain the bitmap's original aspect ratio. It's up to you to be sure that the bitmap retains its original width-to-height ratio.

Another bitmap drawing method is CopyRect. This method enables you to specify both a source rectangle and a destination rectangle. This enables you to split a bitmap into sections when displaying it. Take this code, for example:

var
  Bitmap : TBitmap;
  Src : TRect;
  Dst : TRect;
  I, X, Y : Integer;
  Strips : Integer;
  Stripsize : Integer;
  OldPal : HPalette;
begin
  Bitmap := TBitmap.Create;
  Bitmap.LoadFromFile(`factory.bmp');
  Strips := 6;
  Stripsize := (Bitmap.Height div Strips);
  OldPal := SelectPalette(Canvas.Handle, Bitmap.Palette, True);
  for I := 0 to Pred(Strips) do begin
    Src := Rect(0, i * Stripsize,
      Bitmap.Width, (i * Stripsize) + Stripsize);
    X := Random(Width - Bitmap.Width);
    Y := Random(Height - Stripsize);
    Dst := Rect(X, Y, X + Bitmap.Width, Y + Stripsize);
    Canvas.CopyRect(Dst, Bitmap.Canvas, Src);
  end;
  SelectPalette(Canvas.Handle, oldPal, True);
  Bitmap.Free;
end;

This code loads a bitmap, dissects it into strips, and then displays the strips in random locations on the form. Figure 12.10 shows a sample run of this code. Enter this code in the OnPaint handler of your main form and run the program. Cover up the main form and then bring it to the top again. The images will be redrawn each time the form is repainted.

FIGURE 12.10. Sections of a bitmap written randomly to the screen with CopyRect.

Copying sections of a bitmap like FACTORY.BMP might not appear to make a lot of sense at first glance. A common graphics programming technique, however, is to create one large bitmap made up of several smaller images and copy just the image you want to the screen. In that type of situation, CopyRect is the way to go.


NOTE: In the previous code example I used the SelectPalette function to set the form's palette to the Palette property of the bitmap. For some strange reason, the TCanvas class doesn't have a Palette property, so you have to go to the API to set the palette for the form. If I didn't set the palette for the form, the colors would be wrong when the bitmap strips are displayed on the form. The CopyRect method uses a different mechanism for displaying a bitmap on the canvas, so taking this extra step is necessary when using this method.

There is one other bitmap drawing method I want to mention. The BrushCopy method enables you to specify a source rectangle, a destination rectangle, an image, and a transparent color. The online help for BrushCopy says to use the ImageList component rather than using this method. That's a bit extreme, in my opinion. There are times when BrushCopy works nicely, and it's a whole lot easier to use than the ImageList component. Don't overlook BrushCopy if you are using bitmaps with transparent backgrounds.

Offscreen Bitmaps

Offscreen bitmaps, also called memory bitmaps, are used commonly in Windows programming. Offscreen bitmaps enable you to draw an image in memory and then display the image onscreen by using the Draw method. Offscreen bitmaps help avoid the flicker that you see when you try to draw too much directly to the screen in a short period of time. Offscreen bitmaps are also good for complex drawing programs. You can prepare the image in memory and then display it when ready. Offscreen bitmaps are used in animation, and the most popular new technology for animation is Microsoft's DirectX SDK.

The principle behind offscreen bitmaps is a simple three step process:

1. Create a memory bitmap.

2. Draw on the memory bitmap.

3. Copy the memory bitmap to the screen.

Creating a Memory Bitmap

Creating a memory bitmap is easy. In fact, you've already done it several times in this chapter. Surprised? Each time you created a TBitmap object you were creating a memory bitmap. In those cases, you were loading a file into the memory bitmap. In other cases you will create a memory bitmap, set the its size, and then draw on it--for example,

var
  Bitmap : TBitmap;
  I, X, Y, W, H : Integer;
  Red, Green, Blue : Integer;
begin
  Bitmap := TBitmap.Create;
  Bitmap.Width := 500;
  Bitmap.Height := 500;
  for I := 0 to 19 do begin
    X := Random(400);
    Y := Random(400);
    W := Random(100) + 50;
    H := Random(100) + 50;
    Red := Random(255);
    Green := Random(255);
    Blue := Random(255);
    Bitmap.Canvas.Brush.Color := RGB(Red, Green, Blue);
    Bitmap.Canvas.Rectangle(X, Y, W, H);
  end;
  Canvas.Draw(0, 0, Bitmap);
  Bitmap.Free;
end;

To try out this code, place a button on a form and type the code in the event handler for the button's OnClick event. Each time you click the button, a new random set of rectangles is drawn onscreen. This code simply draws on the memory bitmap and then copies the bitmap to the form's canvas. If you are using a 256-color video adapter, the colors will be dithered because you are not implementing a palette for this exercise.


NOTE: When you create a memory bitmap, the bitmap will have the same color depth as the current video display settings. In other words, if you have your video display set for 256 colors, the memory bitmap will be a 256-color bitmap. If you have your video display set for 24- or 32-bit video, your memory bitmap will contain 32K, 64K, or 16 million colors.

Saving a Memory Bitmap

Saving a memory bitmap to a file is ridiculously easy. Here's all it takes:

BITMAP.SAVETOFILE(`TEST.BMP');

Yes, that's it. In fact, you can easily create your own screen capture program. All you have to do is copy the appropriate part of the desktop to a memory bitmap and then save it to file. It looks something like Listing 12.1.

LISTING 12.1. SCREEN CAPTURE ROUTINE.

procedure MainForm.CaptureButtonClick(Sender: TObject);
var
  DtCanvas  : TCanvas;
  Bitmap    : TBitmap;
  NumColors : Integer;
  LogPal    : PLogPalette;
  Src, Dst  : TRect;
begin
  { Create a TCanvas object for the desktop DC. } 
  DtCanvas := TCanvas.Create;
  DtCanvas.Handle := GetDC(0);
  { Create a new TBitmap object and set its } 
  { size to the size of the form.           } 
  Bitmap := TBitmap.Create;
  Bitmap.Width := Width;
  Bitmap.Height := Height;
  { Create a palette from the form's Canvas  } 
  { and assign that palette to the Bitmap's  } 
  { Palette property.                        } 
  NumColors := GetDeviceCaps(Canvas.Handle, SizePalette);
  GetMem(LogPal, SizeOf(TLogPalette) +
    (NumColors - 1) * SizeOf(TPaletteEntry));
  LogPal.palVersion  := $300;
  LogPal.palNumEntries := NumColors;
  GetSystemPaletteEntries(
    Canvas.Handle, 0, NumColors, LogPal.palPalEntry);
  Bitmap.Palette := CreatePalette(LogPal^);
  FreeMem(LogPal);
  { Copy a section of the screen from the   } 
  { desktop canvas to the Bitmap.           } 
  Src := BoundsRect;
  Dst := Rect(0, 0, Width, Height);
  Bitmap.Canvas.CopyRect(Dst, DtCanvas, Src);
  { Save it to disk. } 
  Bitmap.SaveToFile(`form.bmp');
  { Clean up and go home. } 
  Bitmap.Free;
  DtCanvas.Free;
end;

NOTE: This code goes the extra mile and implements a palette for the form in case the form is displaying graphics. This code was translated from the code that was originally part of an article I wrote for The Cobb Group's C++Builder Developer's Journal. Although you probably aren't interested in the C++Builder journal, you might be interested in the Delphi Developer's Journal. You can sign up for a free copy of the Delphi Developer's Journal on The Cobb Group's Web site at http://www.cobb.com/ddj.

Sample Memory Bitmap Program

Listing 12.2 contains a program called MemBmp that illustrates the use of memory bitmaps. This program scrolls a marquee across the screen when you click one of two buttons. The first button scrolls the text across the screen without using a memory bitmap (writing directly to the form's canvas). The second button uses a memory bitmap for smoother scrolling. A third button is used to stop the marquee. Figure 12.11 shows the MemBmp program running.

FIGURE 12.11. The MemBmp program running.

LISTING 12.2. MemBmpU.pas.

unit MemBmpU;
interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, 
  Controls, Forms, Dialogs, StdCtrls;
const
  DisplayText = `TurboPower Software Co.';
type
  TForm1 = class(TForm)
    Button1: TButton;
    Button2: TButton;
    Button3: TButton;
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    procedure Button3Click(Sender: TObject);
  private
    { Private declarations } 
    Done : Boolean;
  public
    { Public declarations } 
  end;
var
  Form1: TForm1;
implementation
{$R *.DFM} 
procedure TForm1.Button1Click(Sender: TObject);
var
  I : Integer;
begin
  Canvas.Font.Name := `Arial Bold';
  Canvas.Font.Size := 16;
  Canvas.Brush.Color := clSilver;
  Done := false;
  while not Done do begin
    for I := -Canvas.TextWidth(DisplayText) 
      to Pred(Width) do begin
      Sleep(1);
      Application.ProcessMessages;
      if Done then
        Break;
      Canvas.Font.Color := clGray;
      Canvas.Brush.Style := bsClear;
      Canvas.TextOut(i + 2, 12, DisplayText);
      Canvas.Font.Color := clBlack;
      Canvas.Brush.Style := bsClear;
      Canvas.TextOut(i, 10, DisplayText);
      Canvas.Font.Color := clSilver;
      Canvas.TextOut(i + 2, 12, DisplayText);
      Canvas.TextOut(i, 10, DisplayText);
    end;
  end;
end;
procedure TForm1.Button2Click(Sender: TObject);
var
  Bitmap : TBitmap;
  I : Integer;
begin
  Bitmap := TBitmap.Create;
  Bitmap.Width := Width;
  Bitmap.Height := 40;
  Bitmap.Canvas.Font.Name := `Arial Bold';
  Bitmap.Canvas.Font.Size := 16;
  Bitmap.Canvas.Brush.Color := clSilver;
  Bitmap.Canvas.FillRect(Rect(0, 0, Width, 40));
  Done := False;
  while not Done do begin
    for I := -Bitmap.Canvas.TextWidth(DisplayText) 
      to Pred(Width) do begin
      Application.ProcessMessages;
      if (Done) then
        Break;
      Sleep(1);
      Bitmap.Canvas.Font.Color := clGray;
      Bitmap.Canvas.Brush.Style := bsClear;
      Bitmap.Canvas.TextOut(2, 12, DisplayText);
      Bitmap.Canvas.Font.Color := clBlack;
      Bitmap.Canvas.Brush.Style := bsClear;
      Bitmap.Canvas.TextOut(0, 10, DisplayText);
      Canvas.Draw(I, 0, Bitmap);
    end;
  end;
  Bitmap.Free;
end;
procedure TForm1.Button3Click(Sender: TObject);
begin
  Done := True;
end;
end.

The MemBmp program is included with the book's code, along with other example programs (called BrushTst, Capture, Gradient, and Rotate) that illustrate the concepts discussed today.

Multimedia Programming

Multimedia programming is a deceivingly innocent phrase. The reason for this is that multimedia programming encompasses a wide range of possibilities. Multimedia typically includes wave audio, MIDI audio, AVI video clips, and animation. I don't want to confuse multimedia programming with game programming.


Game programming certainly involves multimedia, but it is much more involved than adding sound or video to an application. I'm going to lightly cover multimedia in this section and show you what you can do given the tools that Delphi provides right out of the box. I'm not going to talk about graphics or multimedia APIs such as OpenGL or Direct Draw. For more information on graphics programming using those libraries, get a good book that specializes in graphics programming. Delphi 4 Unleashed (ISBN: 0-672-312-859) would be a good choice.

Wave Audio with the Windows API

Normally I wouldn't talk a lot about Windows API functions because most of the time VCL offers you a better way of performing a task than does the API. In the case of playing a wave file, however, there is nothing more simple than the Win32 API PlaySound function. Playing a wave file by using this function is very easy. The first thing you need to do is add MmSystem to your unit's uses list. Then call PlaySound with the proper parameters:

PlaySound(`test.wav', 0, SND_FILENAME);

That's pretty simple, isn't it? As you can see, the first parameter to PlaySound is used to specify a sound file to play. The last parameter is used to specify flags that determine how the sound should be played. When playing a wave audio file on disk, you specify SND_FILENAME as the last parameter (you can specify other flags as well, but I'll get to that in just a bit).

The PlaySound function can also play system sounds as well as files on disk. To play a system sound, specify the sound alias in the first parameter passed to PlaySound, and SND_ALIAS in the flags parameter--for example,

PlaySound(`SystemAsterisk', 0, SND_ALIAS);

This code plays the system sound associated with the Asterisk event (assuming one has been set up on your system). To see a list of system sound events, look at the Sound applet in the Control Panel. To determine the sound alias for the event, you can look in the Windows Registration Database (the Registry). The aliases are listed under the HKEY_CURRENT_USER key.

If the requested sound cannot be found, Windows will play the default sound (the ding if you have the default sound scheme). You can prevent Windows from playing the default sound by specifying the SND_NODEFAULT flag. For example, if you want to play a system sound but don't want the default sound to play if the system sound cannot be found, you could use this code:

PlaySound(`MailBeep', 0, SND_ALIAS or SND_NODEFAULT);
Notice that the SND_ALIAS and SND_NODEFAULT flags are or'd together.


NOTE: The Win32 API MessageBeep function can also be used to play system sounds by their index number. See MessageBeep in the Win32 online help for more information.

There are two other flags that are important when dealing with the PlaySound function:

There are many other flags that can be used to control how sounds are played with PlaySound. For complete information, see the PlaySound topic in the Win32 online help.

The TMediaPlayer Component

VCL provides the MediaPlayer component for simple multimedia operations. This component, located on the System tab of the Component palette, can play wave files, MIDI files, AVI videos, and more. TMediaPlayer is easy to use, too. For just playing wave files I usually use the PlaySound function as described in the previous section. For anything more complex, though, you can use the MediaPlayer component.

The most obvious way of using TMediaPlayer is to simply drop one on a form. When you do, the media player controller is displayed. This controller has buttons for play, pause, stop, next, previous, step, back, record, and eject. Figure 12.12 shows a form with a MediaPlayer.

FIGURE 12.12. A form with a MediaPlayer component.


Using the media player in its most basic form is extremely simple. All you have to do is set the FileName property to the name of a valid multimedia file and click the Play button on the MediaPlayer. You can select any .WAV, .MID, or .AVI file. The MediaPlayer knows what to do with the file automatically and no additional intervention is required. Most of the time, you want to do something a little more interesting with the MediaPlayer, and for those times you'll have to dig a little deeper.

Although the visual control bar of the MediaPlayer is nice in some situations, you will probably use the MediaPlayer without the control bar at times. You can manipulate the MediaPlayer through code to play, start, stop, or rewind the media. If you don't want to see the MediaPlayer control bar at runtime, you'll have to set the Visible property to False.

MediaPlayer Properties, Methods, and Events

TMediaPlayer has a lot of properties. Most of them are pretty easy to understand, but some are a bit more complicated. Table 12.5 lists the primary properties of TMediaPlayer.

TABLE 12.5. PRIMARY TMediaPlayer PROPERTIES.

Property Description
AutoOpen Specifies whether the device should be opened as soon as the media player is created. Default: False
AutoRewind When True, the media position pointer will be reset to the beginning of the media after the media has played. Default: True
DeviceType The type of the multimedia device. Set to dtAutoSelect to have the device type automatically selected based on the filename extension. Default: dtAutoSelect
Display Used to set the display window to a specific component (for video devices).
DisplayRect Used to set the size and position of the playback window for video devices. The video is resized to fit this rectangle.
EnabledButtons The buttons on the MediaPlayer that should be enabled. Default: All Buttons
EndPos The media ending position. The media is played from StartPos to EndPos. If EndPos is not specified, the media plays to the end. The value you assign to EndPos depends on the type of media being played.
Error The error code of the last operation.
ErrorMessage A textual description of the last error.
Frames The number of frames to move when the Back or Next methods are called, or when the Back or Next buttons on the MediaPlayer control bar are clicked.
Length The length of the media. The value of Length depends on the type of media being played and the current value of TimeFormat.
Mode The state of the device. Can be mpNotReady, mpStopped, mpPlaying, mpRecording, mpSeeking, mpPaused, or mpOpen.
Notify When True, the OnNotify event is generated when the MediaPlayer finishes an operation.
NotifyValue The results of the last notification operation. Can be nvSuccessful, nvSuperseded, nvAborted, or nvFailure.
Position The current position in the media.
StartPos The media starting position. The media is played from StartPos to EndPos. If StartPos is not specified, the media is played from the beginning. The value you assign to StartPos depends on the type of media being played.
TimeFormat The time format to use for this device. The time format can be specified in milliseconds, frames, bytes, samples, tracks/minutes/seconds, hours/minutes/seconds, and more.
Tracks The number of tracks the media contains (for CD audio devices).
VisibleButtons Specifies the buttons that should be displayed on the MediaPlayer control bar. Default: All Buttons
Wait Specifies whether control should be returned to the calling application immediately or only after the media has finished playing.

The MediaPlayer component also has a lot of methods. Many of these methods perform the same function as the control bar buttons. Table 12.6 lists the primary methods of TMediaPlayer.

TABLE 12.6. PRIMARY TMediaPlayer METHODS.

Method Description
Back Backs up the media the number of steps specified by the Frames property.
Close Closes the device.
Eject Ejects the media, if applicable (ejects an audio CD, for example).
Next Advances the start of the next track for devices that support tracks.
Open Opens the device. (Used when AutoOpen is set to False.)
Pause Pauses playback or recording.
Play Starts playback of the device.
Previous Moves to the beginning of the previous track.
Resume Resumes the action (playing or recording) suspended with Pause.
Rewind Resets the media position to the beginning of the media.
Save Saves the media to a file specified by the FileName property.
StartRecord Starts recording of data.
Step Moves forward the number of frames specified in the Frames property.
Stop Stops the current action (playing or recording).

The MediaPlayer component has only one important event. The OnNotify event is called whenever a command has completed, but only if the Notify property is set to True. You can examine the Error and NotifyValue properties to see whether the operation was successfully carried out.


Wave Audio

Playing wave audio is one of the most basic multimedia operations. It is probably also the most common. Playing a wave file synchronously would look like this:

Player.Wait := True;
Player.FileName := `test.wav';
Player.Open;
Player.Play;

Notice that the Wait property is set to True to force the wave file to play synchronously. This is necessary if you want to play wave files back to back--for example,

Player.FileName := `Sound1.wav';
Player.Open;
Player.Wait := True;
Player.Play;
Player.FileName := `Sound2.wav';
Player.Wait := True;
Player.Play;

Notice that I set Wait to True before playing each file. The Wait property gets reset after an operation, so you must reset it each time you want to force the program execution to wait until the operation finishes.


If you don't set Wait to True, the first sound will start playing and will be immediately superseded by the second sound when it starts a few milliseconds later. If you want a sound to play in the background, set Wait to False.

To play a portion of a wave file, you can set the StartPos and EndPos before playing the file. The following example opens a wave file and plays two seconds of audio, starting at one second and ending at three seconds:

Player.FileName := `test.wav';
Player.Open;
Player.StartPos := 1000;
Player.EndPos   := 3000;
Player.Play;

The StartPos and EndPos values are specified in milliseconds, the default for wave audio devices.


NOTE: If you set either StartPos or EndPos to invalid values, the wave file won't play. Invalid values include a StartPos greater than the EndPos or an EndPos value larger than the media length.

Setting the Output Volume

Setting the volume of the wave output device is relatively simple, but you have to go to the Windows API to do it. The waveOutGetVolume and waveOutSetVolume functions can be used to get the volume and set the volume, respectively.

The volume is stored as an integer. The high word specifies the right channel volume setting, and the low word specifies the left channel volume setting. If the device doesn't have the capability of setting the right and left channel volumes independently, the low word is used to set the volume and the high word is ignored.

A value of 0 is no volume, and a value of $FFFF hexadecimal specifies full volume. Given that, the following code sets the volume of both channels to 50%:

WAVEOUTSETVOLUME(0, $80008000);

This example sets the volume to full volume:

waveOutSetVolume(0, $FFFFFFFF);

Notice that I am using 0 as the first parameter to waveOutSetVolume. This is cheating a little, because I am assuming that the wave device is device 0. That's almost always the case, so you can generally get by with this trick.

Setting the volume is as easy as that. Note that waveOutSetVolume sets only the volume for the wave output device and not the master volume. The master volume can be set only through the multimedia mixer control, a subject beyond the scope of this book. I realize that this discussion about setting the volume is a bit advanced. If you didn't understand it all, don't fret. You can always come back and review this section later if you need to.

Recording Wave Audio

Recording wave audio is not quite as straightforward as it should be. You would think that all you have to do is call StartRecording. It's not quite that simple, however, because of some problems with TMediaPlayer.

To record a wave file, you first have to open an existing wave file that has the same recording parameters as you want your new file to have. You can then record the new wave data, change the FileName property to the name of the new file, and then save the file. It's a bit cumbersome, but it does work.

For example, let's say you have a file called DUMMY.WAV that was recorded with a wave format of 22050 kHz, 8 bits per sample, and one channel (you can easily create this kind of file using Windows Sound Recorder). In this case, you could start recording wave audio on a button click like this:

procedure TForm1.StartBtnClick(Sender: TObject);
begin
  with MediaPlayer do begin
    { Set FileName to the dummy.wav file to } 
    { get the recording parameters. } 
    FileName := `dummy.wav';
    { Open the device. } 
    Open;
    { Start recording. } 
    Wait := False;
    StartRecording;
  end;
end;

At this point the recording has started and control has returned to your application. You now need to stop recording, which can be done in response to a second button click--for example,

procedure TForm1.StopBtnClick(Sender: TObject);
begin
  with MediaPlayer do begin
    { Stop recording. } 
    Stop;
    { Change the filename to the new file we want to write. } 
    FileName := `new.wav';
    { Save and close the file. } 
    Save;
    Close;
  end;
end;


NOTE: At the time of this writing, these steps are necessary to perform wave audio recording because of a bug in TMediaPlayer. It's possible that this bug will be fixed by the time Delphi 4 is released. To find out, place a MediaPlayer on a form and call the Record method. If Delphi doesn't raise an exception, the bug was fixed.

The factors that control how wave audio is recorded include the sample rate, the number of channels (mono or stereo), and the number of bits per sample (usually 8 or 16). Common waveform sampling rates are 8,000 kHz, 11,025 kHz, 22,050 kHz, and 44,100 kHz. The higher the sampling rate, the higher the quality of the recorded audio. For the most part, you probably will not use stereo recording unless you are doing game programming. Even then, you should only use stereo when needed. The number of bits per sample also affects the quality of the recorded sound. The number of bits per sample can be set at either 8 bits or 16 bits.


NOTE: The higher the quality of the sound, the more disk space a recorded waveform audio file will consume. A wave file recorded in stereo will, naturally, be twice the size of a file recorded in one channel. Likewise, using 16 bits per sample will double the size of the wave file over 8 bits per sample. A file recorded at 22,050 kHz, mono, and 8 bits per sample might be 200K, whereas its equivalent recorded at 22,050, stereo, and 16-bits per sample would be 800K. For the most part, stereo and 16 bits per sample don't provide enough benefit to warrant the additional disk space. A wave format of 22,050 kHz, mono, and 8 bits per sample offers a good compromise between sound quality and file size.

MIDI Audio

Not much needs to be said regarding MIDI audio. All you have to do is set the FileName property of the MediaPlayer to a MIDI file and call the Play method. MIDI files have either .mid or .rmi extensions.


NOTE: If you intend to go beyond simple playing of MIDI files, you will have to study the low-level midiInXXX and midiOutXXX functions. These functions are documented in the MMEDIA.HLP file. This file might be in your Delphi Help directory (it might be called MM.HLP). If not, you can probably obtain the file from Microsoft.

Most sound cards do not enable two wave files to be played at one time. Likewise, most do not enable more than one MIDI file to be played at one time. Most do, however, enable a wave file and a MIDI file to be played simultaneously. You have probably noticed this effect in games where you have a music track and sound effects being played at the same time. To play a wave file and a MIDI file at the same time, you can use two MediaPlayer components. Alternatively, you could use a MediaPlayer for the MIDI file and use PlaySound for any wave files you need to play.

A MIDI file is often used as background music for games. If you use a MIDI file in this way, you will want to restart the music when it reaches the end. TMediaPlayer doesn't have a built-in way to play a sound continuously. You can, however, make use of MediaPlayer's OnNotify event to achieve a looping effect. First you need to tell the MediaPlayer to notify you when something happens. That part is easy:

MediaPlayer.Notify := True;

After that, you need to provide an OnNotify event handler. In the event handler, you need code that restarts the MIDI file after it successfully completes. The OnNotify event would look something like this:

procedure TForm1.MediaPlayerNotify(Sender: TObject);
begin
  with MediaPlayer do
    if NotifyValue = nvSuccessful then begin
      Position := 0;
      Play;
    end;
end;

First I check to see whether the NotifyValue property contains a value of nvSuccessful. If so, I reset the file position to 0 and call the Play method to start the file playing again. That's pretty simple, but there are a couple of issues you should be aware of.


First, notice that I am setting the Position property to 0. This effectively rewinds the file to the beginning. This is not necessary if the AutoRewind property is set to True. The second issue that you should be aware of is that there are several MediaPlayer actions that could cause the OnNotify event to be called with a NotifyValue of nvSuccessful.

For example, a simple Stop command will result in a value of nvSuccessful, providing the command completed without incident. You might need to set up a state machine so that you know that the OnNotify event is being called in response to the file reaching the end, and not as a result of some other media player operation.

CD Audio

Playing CD audio is relatively simple with TMediaPlayer. To play an audio CD, you simply change the DeviceType property to dtCDAudio and click the Play button (or call the Play method).

The most difficult aspect to grasp in programming CD audio devices is the different time formats used in CD audio. You will use the TMSF (time, minutes, seconds, frames) time format to gather information about a particular track or to set the current position to a particular track. Any minute, second, or frame values will be set relative to the track number. For example, the following code formats a string to report the playing position within the current track:

var
  Time    : Integer;
  Track   : Integer;
  Minutes : Integer;
  Seconds : Integer;
  TimeStr : string;
begin
  MediaPlayer.TimeFormat := tfTMSF;
  Time    := MediaPlayer.Position;
  Track   := mci_TMSF_Track(Time);
  Minutes := mci_TMSF_Minute(Time);
  Seconds := mci_TMSF_Second(Time);
  TimeStr := Format(`Track Time: %2.2d:%2.2d', [Minutes, Seconds]);
  Label1.Caption := `Track: ` + IntToStr(Track);
  Label2.Caption := TimeStr;
end;

First, the TimeFormat is set to tfTMSF to ensure the proper time format. Next, the current position is stored in a variable called Time. Following that, the different time values (track, minutes, and seconds) are extracted using Windows' time conversion macros, mci_TMSF_Track, mci_TMSF_Minute, and mci_TMSF_Second. These macros are contained in the MMSystem unit. You will have to add that unit to your uses list if you use these macros. After the individual time values have been extracted, a time string is built to display the track time. Then the track number and track time are displayed in Label components.

You will use the MSF (minutes, seconds, frames) time format to gather information about the CD at the global level. For example, you would use the MSF time format to retrieve the current position of the CD in relation to the start of the CD. Likewise you would use the MSF time format if you want to set the current position of the CD to the 30 minute mark on the CD, regardless of the track. This code shows how to retrieve and display the current position on the CD in minutes and seconds:

var
  Time    : Integer;
  Minutes : Integer;
  Seconds : Integer;
  TimeStr : string;
begin
  MediaPlayer.TimeFormat := tfMSF;
  Time    := MediaPlayer.Position;
  Minutes := mci_MSF_Minute(Time);
  Seconds := mci_MSF_Second(Time);
  TimeStr := Format(`Total Time: %2.2d:%2.2d', [Minutes, Seconds]);
  Label3.Caption := TimeStr;
end;

The book's code contains a program called CDPlayer that illustrates how TMediaPlayer can be used to create an audio CD player.

AVI Video

To play an AVI video with TMediaPlayer, select an AVI file and call the Play method (or click the Play button). If you use the default MediaPlayer settings, a separate window will pop up and the AVI will play in that window. As an alternative, you can set the Display property to any windowed component, and the video will play in that component's client area.

For example, let's say you have a panel on your form called AVIPanel and that you want to play an AVI video in the panel. In this case, you would set the Display property like this:

MediaPlayer.Display := AVIPanel;

When the AVI plays, it will play in the panel. If the video is larger than the panel's rectangle, the video will be clipped to the size of the panel.

You can stretch or shrink a video by setting the DisplayRect property. This code will shrink or stretch a video to the size of the display panel:

MEDIAPLAYER.DISPLAYRECT := AVIPANEL.CLIENTRECT;

There are many types of AVI video formats, and not all AVIs will play on all systems. In order to play a particular video, you have to be sure that your users have the drivers installed for that type of video. If you want to play it safe, stick with the standard Microsoft AVI video types. Your users will almost certainly have the drivers for Microsoft AVIs installed because they are installed as part of the normal Windows installation.


NOTE: The TAnimate component (found on the Win32 tab of the Component palette) is used for playing small videos such as those you see used by Windows. Examples of these animations are those you see when Explorer is copying files and the animation that Explorer displays in the Find dialog when it is searching for files. The AVIs that can be played by TAnimate must be either uncompressed or run length encoded (RLE) compressed only. No other forms of compression are allowed. Also, the AVI cannot contain audio.


NOTE: The MediaPlayer component can play many types of videos and animations, provided the correct drivers are installed. For example, Autodesk Animator (AA) animations have a filename extension of either .fli or .flc. To play an AA animation, all you have to do is set the DeviceType property to dtAutoSelect and select an Animator file. As long as the AA drivers are installed, MediaPlayer will play the animation.

Summary

Graphics programming can be very interesting and very rewarding. It can also be very frustrating. VCL takes much of the frustration out of graphics programming by providing the TCanvas and TBitmap classes, along with their supporting classes such as TPen, TBrush, and TFont. These classes enable you to get on with the business of the visual aspects of graphics programming rather than worry about how to load or save bitmap files. Multimedia programming can be a lot of fun. It's rewarding to write a few lines of code and then see or hear the results a few seconds later. Multimedia can certainly add excitement to your programs, but be careful not to overdo it.

Although I can't claim that this chapter is an in-depth look at either graphics or multimedia programming in Delphi, it's a good start and introduces you to some concepts that you can carry with you for a long time.

Workshop

The Workshop contains quiz questions to help you solidify your understanding of the material covered and exercises to provide you with experience in using what you have learned. You can find answers to the quiz questions in Appendix A, "Answers to the Quiz Questions."

Q&A

Q Can the graphics concepts discussed in this chapter be used when printing as well as drawing on the screen?
A Yes. As far as Windows is concerned, a device context is a device context. It doesn't matter whether that device context is for the display screen, a memory bitmap, or a printer.

Q I see the Ellipse method, but I don't see a method for drawing perfect circles. How do I draw circles?

A Use the Ellipse method. Just be sure that the rectangle used to draw the ellipse is perfectly square and you will get a perfect circle (if that makes any sense).

Q How do I change the color of the text that DrawText produces?

A Change the Color property of the canvas font.

Q Why should I bother with a memory bitmap?

A You might not need to. However, if you ever see noticeable flicker in your drawing routines, you should think about using a memory bitmap.

Q What is the default time format for wave audio devices?

A Wave audio devices use milliseconds by default (one second is 1000 milliseconds).

Q Can I play more than one wave file at a time?

A Not with the MediaPlayer component. Microsoft's DirectX API has sound mixing capabilities that make it sound as if two or more sounds are playing at one time. If you need sound mixing, check out DirectX (free from Microsoft).

Q How can I play an AVI video directly on my form?

A Set the Display property of TMediaPlayer to the name of the form.

Quiz

1. What component can you use to draw graphics on a form?

2. Which TCanvas property controls the fill color of the canvas?

3. What does a clipping region do?

4. What function do you use to draw multiple lines of text on a canvas?

5. What TCanvas method can be used to draw a bitmap with a transparent background?

6. Which TCanvas method do you use to copy an entire bitmap to a canvas?

7. How do you save a memory bitmap to a file?

8. What component do you use to play a wave file?

9. What is the TimeFormat property of TMediaPlayer used for?

10. Can you record wave audio with the MediaPlayer component?

Exercises

1. Write a program that displays a circle on the screen when a button is clicked.
2. Write a program that draws random lines on the screen each time the form is shown (including when the form is restored after being hidden).

3. Write a program that creates a memory bitmap, draws text and shapes on the bitmap, and then displays the bitmap on the form's canvas.

4. Modify the program you wrote in exercise 3 to save the memory bitmap to disk.

5. Write a program that displays an AVI video on the application's main form.

6. Write a program that plays a wave file when the program starts.


Previous chapterNext chapterContents

© Copyright, Macmillan Computer Publishing. All rights reserved.