The Canvas


The Windows GDI enables us to produce graphics by using GDI objects and by calling GDI functions that can draw lines, shapes, text, or images. The TCanvas class encapsulates most of the GDI functions and objects and provides the drawing surface on which we can draw. The three graphics objects that are used to do the drawing are Pen, Brush, and Font (discussed earlier).

The Pen

The Pen is used when you're drawing lines and shapes. When you're drawing shapes, Pen is used for the outline.

To draw a line, you have to use two Canvas methods: MoveTo and LineTo. The MoveTo method is used to set the beginning drawing position. The LineTo method is used to draw the line from the drawing position set by MoveTo to the point specified by its X and Y parameters. When finished, the LineTo method also updates the drawing position.

The following example shows how to draw a triangle using the MoveTo and LineTo methods. The result is displayed in Figure 22-2.

image from book
Figure 22-2: Drawing lines .

Listing 22-2: Drawing lines with MoveTo and LineTo

image from book
procedure TMainForm.DrawButtonClick(Sender: TObject); begin   Canvas.MoveTo(100, 100);   Canvas.LineTo(200, 150);   Canvas.LineTo(100, 200);   Canvas.LineTo(100, 100); end;
image from book

The TPen class encapsulates these properties: Color, Width, and Style. The different Pen styles are displayed in Figure 22-3.

image from book
Figure 22-3: Pen styles

The StylesButtonClick method that produces the graphical output in Figure 22-3 is displayed in Listing 22-3.

Listing 22-3: Playing with styles

image from book
procedure TMainForm.StylesButtonClick(Sender: TObject); const   PEN_NAMES: array[TPenStyle] of string = ('psSolid', 'psDash', 'psDot',     'psDashDot', 'psDashDotDot', 'psClear', 'psInsideFrame'); var   i: Integer;   y: Integer; begin   for i := 0 to Ord(psInsideFrame) do   begin     y := 20 + (i * 40);     Canvas.Pen.Style := TPenStyle(i);     Canvas.TextOut(10, y, PEN_NAMES[Canvas.Pen.Style]);     Canvas.MoveTo(10, y);     Canvas.LineTo(200, y);   end; end;
image from book

The TPen class has one more property that changes the appearance of lines on the canvas: Mode. The Mode property specifies the operation that is performed on the pixels when a line is drawn on the canvas. For instance, if you set the Mode property to pmWhite, all the drawn lines will be white, regardless of the Pen's color. If you set the Mode property to pmNotCopy, the Pen's color will be inverted. To see the list of all possible Mode values, search for TPenMode in Delphi's Help.

One of the Pen modes missing from the .NET framework is the pmNotXor mode, which is most often used to create the rubber-banding effect shown in Figure 22-4.

image from book
Figure 22-4: Rubber-banding

The rubber-banding effect is pretty easily produced. The only thing you have to do is draw the same line twice. The first time it's drawn, the pixels on the canvas are inverted to make the line visible. When you draw the line again using pmNotXor, the pixels on the canvas will be restored to their original values, thus erasing the line.

The code in Listing 22-4 enables you to draw lines on the canvas just as you would in Paint or any other application that supports drawing.

Listing 22-4: Drawing lines

image from book
unit Unit1; interface uses   Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,   Dialogs; type   TForm1 = class(TForm)     procedure FormMouseMove(Sender: TObject; Shift: TShiftState;       X, Y: Integer);     procedure FormMouseUp(Sender: TObject; Button: TMouseButton;       Shift: TShiftState; X, Y: Integer);     procedure FormMouseDown(Sender: TObject; Button: TMouseButton;       Shift: TShiftState; X, Y: Integer);   private     { Private declarations }     FMouseDown: Boolean;     FStart, FEnd: TPoint;   public     { Public declarations }   end; var   Form1: TForm1; implementation {$R *.dfm} procedure TForm1.FormMouseDown(Sender: TObject; Button: TMouseButton;   Shift: TShiftState; X, Y: Integer); begin   if Button = mbLeft then   begin     FStart := Point(X, Y);     FEnd := FStart;     FMouseDown := True;   end;       // if Button end; procedure TForm1.FormMouseUp(Sender: TObject; Button: TMouseButton;   Shift: TShiftState; X, Y: Integer); begin   FMouseDown := False; end; procedure TForm1.FormMouseMove(Sender: TObject; Shift: TShiftState; X,   Y: Integer); begin   if FMouseDown then   begin     { first erase the previous line }     Canvas.Pen.Mode := pmNotXor;     Canvas.MoveTo(FStart.X, FStart.Y);     Canvas.LineTo(FEnd.X, FEnd.Y);     { draw the new line }     Canvas.MoveTo(FStart.X, FStart.Y);     Canvas.LineTo(X, Y);     { remember the new coordinates so that       we can erase them next time an OnMouseMove occurs }     FEnd := Point(X, Y);   end; end; end.
image from book

The Brush

The Brush is used by methods that draw shapes to fill the interior of the drawn shape. Usually, the Brush only specifies the color of the shape, but it can also specify a pattern or a bitmap image that can be used as a pattern. Figure 22-5 displays the various Brush styles.

image from book
Figure 22-5: Brush styles

The following listing shows the code that displays the available Brush styles.

Listing 22-5: Working with Brush styles

image from book
procedure TMainForm.DrawButtonClick(Sender: TObject); const   RECT_SIZE = 50;   BRUSH_NAMES: array[TBrushStyle] of string = ('bsSolid',     'bsClear', 'bsHorizontal', 'bsVertical', 'bsFDiagonal',     'bsBDiagonal', 'bsCross', 'bsDiagCross'); var   y: Integer;   style: TBrushStyle; begin   { erase the entire Canvas }   Canvas.Brush.Style := bsSolid;   Canvas.Brush.Color := clWhite;   Canvas.FillRect(ClientRect);   { draw rectangles }   y := 10;   for style := bsSolid to bsDiagCross do   begin     Canvas.Brush.Style := style;     { select a random color }     Canvas.Brush.Color := Random(High(TColor));     Canvas.Rectangle(10, y, 10 + RECT_SIZE, y + RECT_SIZE);     { temporarily change brush style to bsClear to       draw text without a background color }     Canvas.Brush.Style := bsClear;     Canvas.TextOut(70, y + (RECT_SIZE div 2), BRUSH_NAMES[style]);     Inc(y, RECT_SIZE + 10);   end;       // for end;
image from book

Drawing Text

The most straightforward method for drawing text on a canvas is the TextOut method. As you've already seen, the TextOut method accepts three parameters. The first two parameters are X and Y coordinates and the last is the string that is to be drawn on the canvas.

The TextOut method uses both the Brush and Font properties of the canvas when drawing the string. The Font property specifies the general characteristics of the text (font family and attributes) and the Brush property specifies the background color. If you want to draw text with a colored background, set Brush.Style to bsSolid. To draw the text without the colored background, set Brush.Style to bsClear.

Instead of using the Canvas's Brush property, you can also use the GDI API functions SetBkMode and SetBkColor to set the background color and mode (TRANSPARENT or OPAQUE):

function SetBkMode(DC: HDC; BkMode: Integer): Integer; stdcall;  function SetBkColor(DC: HDC; Color: COLORREF): COLORREF; stdcall;

Notice the first parameter in both functions. The first parameter accepts an HDC variable — a handle to a certain device context. At the API level, device contexts (data structures that contain screen or printer information) represent the drawing surface. The TCanvas class encapsulates the device context and the Handle property of the canvas is actually the handle of the GDI device context required by all GDI functions. So, when you need to call a GDI function directly, you can pass the Canvas's Handle property as the DC parameter (see Figure 22-6).

image from book
Figure 22-6: Drawing text

The following listing shows the code that produces the graphical output displayed in Figure 22-6.

Listing 22-6: Drawing text

image from book
procedure TMainForm.DrawButtonClick(Sender: TObject); begin   Canvas.Font.Name := 'Verdana';   Canvas.Font.Size := 14;   { VCL }   Canvas.Brush.Color := clBlack;   Canvas.Font.Color := clLime;   Canvas.TextOut(10, 10, 'Brush.Style := bsSolid; (opaque background)');   Canvas.Brush.Style := bsClear;   Canvas.Font.Color := clBlue;   Canvas.TextOut(10, 40, 'Brush.Style := bsClear; (transparent background)');   { GDI API + VCL}   SetBkMode(Canvas.Handle, OPAQUE);   SetBkColor(Canvas.Handle, clWhite);   SetTextColor(Canvas.Handle, clBlack);   Canvas.TextOut(10, 70, 'SetBkMode(Canvas.Handle, OPAQUE);');   SetBkMode(Canvas.Handle, TRANSPARENT);   Canvas.TextOut(10, 100, 'SetBkMode(Canvas.Handle, TRANSPARENT);'); end;
image from book

To draw text on a canvas, you can also use the TextRect procedure, which writes a string inside a rectangle and clips the sections of the string that don't fit in the specified rectangle, as shown in Figure 22-7.

image from book
Figure 22-7: The TextRect method

Listing 22-7: The TextRect method

image from book
procedure TMainForm.DrawButtonClick(Sender: TObject); var   rc: TRect; begin   rc := Rect(10, 10, 100, 40);   Canvas.Brush.Color := clWhite;   Canvas.Rectangle(rc);   Canvas.TextRect(rc, 10, 10, 'TextRect displays text in a rectangle.'); end;
image from book

The GDI API has yet another, really powerful text drawing function that isn't encapsulated in the TCanvas class and is often used by component developers: DrawText. The DrawText function can be used to display formatted text. It allows you to specify the rectangle that will be used for the formatting, the number of characters to draw, and the formatting options. Here's the declaration of the DrawText function:

function DrawText(hDC: HDC; lpString: PChar; nCount: Integer;    var lpRect: TRect; uFormat: UINT): Integer; stdcall;

When you call the DrawText function, you have to do the following:

  • Pass the Canvas's handle as the hDC parameter.

  • Pass a string value as the lpString parameter. (If you're passing a string variable or a string property, you'll have to typecast it to PChar.)

  • Pass the length of the string as the nCount parameter. (If you pass –1, the DrawText function will display the entire string.)

  • Pass the rectangle in which the text is to be drawn as the lpRect parameter.

  • Pass one or more formatting constants as the uFormat parameter. (If you want to use several formatting styles, you have to combine them with the or operator.)

The most often used formatting values are listed in Table 22-1.

Table 22-1: Several text formatting values

Constant

Meaning

DT_SINGLELINE

Draw text on a single line.

DT_LEFT

Align text to the left.

DT_CENTER

Center text horizontally.

DT_RIGHT

Align text to the right.

DT_VCENTER

Align text vertically.

DT_WORD_ELLIPSIS

Truncate words that don’t fit in the specified rectangle and display ellipses

DT_WORDBREAK

Break words into new lines if they don’t fit in the specified rectangle

DT_CALCRECT

Use this value to calculate how big the rectangle has to be to accommodate the entire string. (If you use this value, the DrawText function will perform the calculation but won’t display the string.)

The following figure shows several strings displayed with the DrawText function.

The following listing contains the code that generates the graphical output displayed in Figure 22-8.

image from book
Figure 22-8: The DrawText function

Listing 22-8: Using the DrawText function

image from book
unit Unit1; interface uses   Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,   Dialogs, XPMan, StdCtrls; type   TMainForm = class(TForm)     DrawButton: TButton;     XPManifest: TXPManifest;     procedure DrawButtonClick(Sender: TObject);   private     { Private declarations }   public     { Public declarations }   end; var   MainForm: TMainForm; implementation {$R *.dfm} procedure ClearCanvas(ACanvas: TCanvas; AColor: TColor); begin   with ACanvas do   begin     Brush.Style := bsSolid;     Brush.Color := AColor;     { ClipRect identifies the section of the canvas that       needs to be repainted. }     FillRect(ClipRect);   end; end; procedure TMainForm.DrawButtonClick(Sender: TObject); var   rc: TRect;   msg: string; begin   { clear canvas }   ClearCanvas(Canvas, clWhite);   Canvas.Font.Name := 'Times New Roman';   Canvas.Font.Size := 20;   Canvas.Brush.Style := bsClear;   { left, centered, and right text }   rc := Rect(10, 10, 420, 10 + Canvas.TextHeight('W'));   Canvas.Rectangle(rc);   DrawText(Canvas.Handle, 'Left', -1, rc, DT_SINGLELINE or DT_LEFT);   DrawText(Canvas.Handle, 'Centered', -1, rc, DT_SINGLELINE or DT_CENTER);   DrawText(Canvas.Handle, 'Right', -1, rc, DT_SINGLELINE or DT_RIGHT);   { center vertically and horizontally }   rc := Rect(10, rc.Bottom + 10, 420, rc.Bottom + 150);   Canvas.Rectangle(rc);   DrawText(Canvas.Handle, 'Horizontally && Vertically Centered', -1,     rc, DT_SINGLELINE or DT_VCENTER or DT_CENTER);   { truncate with ellipses }   msg := 'This line is too long and will be truncated.';   rc := Rect(10, rc.Bottom + 10, 220, rc.Bottom + 10 + Canvas.TextHeight('W'));   Canvas.Rectangle(rc);   DrawText(Canvas.Handle, PChar(msg), -1, rc, DT_WORD_ELLIPSIS);   { draw multiline text }   msg := 'The DrawText function determined the appropriate ' +     'rectangle for this string. DrawText calculates the ' +     'rectangle size when you pass DT_CALCRECT as the uFormat parameter.';   rc := Rect(10, rc.Bottom + 10, 500, rc.Bottom + 20);   { calculate the appropriate rectangle size }   DrawText(Canvas.Handle, PChar(msg), -1, rc, DT_CALCRECT or DT_WORDBREAK);   Canvas.Rectangle(rc);   DrawText(Canvas.Handle, PChar(msg), -1, rc, DT_WORDBREAK); end; end.
image from book

Measuring Text

The TCanvas class has three methods that allow you to determine the width and height of a string: TextExtent, TextHeight, and TextWidth. While TextHeight and TextWidth only return the height or width of the string, the TextExtent function returns both width and height in a tagSize (TSize) record:

tagSIZE = record   cx: Longint; { width }   cy: Longint; { height } end;

The following figure shows an example application that draws each character in a string with a different font. This application uses the TextWidth function to determine where it should draw each character. You can see the code in Listing 22-9.

image from book
Figure 22-9: Using TextWidth to determine character width

Listing 22-9: Using the TextWidth function

image from book
 procedure TMainForm.DrawButtonClick(Sender: TObject); const   s = 'Borland Delphi'; var   c: Char;   x: Integer; begin   Canvas.Brush.Color := clWhite;   Canvas.FillRect(ClientRect);   x := 25;   for c in s do   begin     Canvas.Font.Name := Screen.Fonts[Random(Screen.Fonts.Count)];     Canvas.Font.Size := Random(60) + 12;     Canvas.Font.Color := Random(High(TColor));     Canvas.TextOut(x, 100, c);     Inc(x, Canvas.TextWidth(c));   end; end;
image from book

Using API Functions to Retrieve a Drawing Surface

Although it's best to use the Canvas to draw both on screen and to the printer, there are situations where you'll need to (or want to) do things the API way. To retrieve a device context, you can use the GetDC API function. The GetDC function accepts a window handle and returns a device context handle that enables you to draw in the client area of the specified window:

function GetDC(hWnd: HWND): HDC; stdcall;

When you use the GetDC function to retrieve a device context handle, you must release the acquired handle when you no longer need it. To release a device context, call the ReleaseDC function. This function requires you to pass both the device context handle and the handle of the window whose device context you're releasing:

function ReleaseDC(hWnd: HWND; hDC: HDC): Integer; stdcall;

When you draw using API functions, you'll notice that you have to do much more than when you're using TCanvas methods. For instance, if you want to draw a simple string, you can use the TextOut function, but you have to pass five parameters to the function, not three. Along with the X and Y coordinates and the string, the GDI TextOut function requires two more parameters: the device context handle and the length of the string. The following listing illustrates how to use GDI API functions to display a simple text message on the form.

Listing 22-10: Using API functions to draw on the form

image from book
procedure TMainForm.GetDCButtonClick(Sender: TObject); var   context: HDC;   msg: string; begin   context := GetDC(Handle);   try     msg := 'Using GetDC & TextOut API functions.';     TextOut(context, 20, 20, PChar(msg), Length(msg));   finally     { release the device context when you're done }     ReleaseDC(Handle, context);   end; end;
image from book

You can see the result of the GetDCButtonClick method in Figure 22-10.

image from book
Figure 22-10: The result of all that code in Listing 22-10

The GetWindowDC function is another function that allows you to retrieve a device context. Unlike the TCanvas class and the GetDC function that enable you to draw only in the client area of the window, GetWindowDC returns the device context for the entire window including the title bar, menus, and window borders.

The following example shows how to paint in both the client and non-client areas of the window (see Listing 22-11 and Figure 22-11).

image from book
Figure 22-11: Painting in the non-client area of the window

Listing 22-11: Using the GetWindowDC function

image from book
procedure TMainForm.GetWindowDCButtonClick(Sender: TObject); var   winContext: HDC; begin   winContext := GetWindowDC(Handle);   try     { erase the entire window, including borders & the title bar }     Canvas.Brush.Color := clWebPaleGoldenrod;     FillRect(winContext, Rect(0, 0, Width, Height),       Canvas.Brush.Handle);   finally     ReleaseDC(Handle, winContext);   end; end;
image from book

The OnPaint Event

You might have noticed that the graphics displayed in the previous examples look fine as long as you don't move the window, minimize it, or cover it with another window. In fact, if you do almost anything that is either directly or indirectly related to the window, the graphics will either disappear entirely or get messed up (see Figure 22-12).

image from book
Figure 22-12: A messed-up gradient

To ensure that your graphics remain unscathed by other activities, you have to write your painting code in the OnPaint event handler because the OnPaint event occurs every time the operating system determines that either the entire window or only a portion of the window has to be repainted. You can also manually request a window repaint by calling Invalidate.

Listing 22-12 shows how to draw a simple gradient in the OnPaint event handler.

The easiest way to draw a gradient is to:

  1. Draw a black-to-blue, black-to-red, or black-to-green gradient.

  2. Draw the gradient in 256 steps, regardless of the width or height of the destination window.

  3. Calculate the height or width of the rectangle that needs to be drawn for each color (if the form is 1,000 pixels high, you have to draw a 4-pixel high rectangle for each color).

Listing 22-12: Drawing a simple gradient

image from book
{ draw a black-to-blue gradient } procedure TMainForm.FormPaint(Sender: TObject); var   rowHeight: Integer;   i: Integer; begin   { calculate the height of each row }   rowHeight := Succ(ClientHeight div 256);   { draw 256 differently colored rectangles - the gradient}   for i := 0 to 255 do   begin     Canvas.Brush.Color := RGB(0, 0, i);     Canvas.FillRect(Rect(0, i * rowHeight, ClientWidth, Succ(i) * rowHeight));   end;       // for end;
image from book

The gradient drawn by the code in Listing 22-12 is displayed in Figure 22-13.

image from book
Figure 22-13: A simple gradient

The gradient in Figure 22-13 will be correctly displayed as long as you don't resize the window. If you want to display the gradient correctly while the user is resizing the window, call Invalidate in the form's OnResize event handler to repaint the entire form:

procedure TMainForm.FormResize(Sender: TObject); begin   Invalidate; end;

When you run this code, you'll notice the flickering introduced by the Invalidate method when you try to resize the window. Some developers try to eliminate the flicker by calling Paint in the OnResize event handler or by assigning the same event handler to both OnPaint and OnResize. You should never do this, especially if your graphics are computationally expensive, because the OnPaint handler will be called twice.

The flickering occurs because Windows erases the background of the window before repainting. So, to avoid the flickering, you simply have to tell Windows to stop erasing the background of the window. To do that, you have to handle the WM_ERASEBKGND message and assign a nonzero value (usu- ally 1) to the message's result. Listing 22-13 shows how to eliminate the flickering problem.

Listing 22-13: Handling WM_ERASEBKGND to eliminate flicker

image from book
type   TMainForm = class(TForm)     procedure FormResize(Sender: TObject);     procedure FormPaint(Sender: TObject);   private     { Private declarations }   public     { Public declarations }     procedure EraseBackground(var Message: TWMEraseBkgnd);         message WM_ERASEBKGND;   end; procedure TMainForm.EraseBackground(var Message: TWMEraseBkgnd); begin   Message.Result := 1; end; procedure TMainForm.FormResize(Sender: TObject); begin   Invalidate; end;
image from book

Now that you know how to draw a simple flicker-free gradient, we can create a better one. This "better" way is similar to the last one but uses floating-point values for better quality. It's also much faster because the MoveTo and LineTo procedures draw lines faster than FillRect.

Listing 22-14: Another way to draw gradients

image from book
type   TMainForm = class(TForm)     procedure FormResize(Sender: TObject);     procedure FormPaint(Sender: TObject);   private     { Private declarations }   public     { Public declarations }     procedure EraseBackground(var Message: TWMEraseBkgnd);         message WM_ERASEBKGND;   end; var   MainForm: TMainForm; implementation {$R *.dfm} procedure TMainForm.FormPaint(Sender: TObject); var   colorHeight: Double;   i: Integer; begin   if ClientHeight = 0 then Exit;   { determine how much a single color should cover }   colorHeight := 256 / ClientHeight;   for i := 0 to ClientHeight do   begin     { draw a black-to-red gradient }     Canvas.Pen.Color := RGB(Round(i * colorHeight), 0, 0);     Canvas.MoveTo(0, i);     Canvas.LineTo(ClientWidth, i);   end;       // for i end; procedure TMainForm.FormResize(Sender: TObject); begin   Invalidate; end; procedure TMainForm.EraseBackground(var Message: TWMEraseBkgnd); begin   Message.Result := 1; end; end.
image from book

The gradient drawn by the code in Listing 22-14 is displayed in Figure 22-14.

image from book
Figure 22-14: Another gradient

Finally, let's create a real gradient that can use custom start and end colors.

The most important thing that you have to do if you want to draw a gradient that uses custom colors is determine how much red, green, and blue to add to the start color at each step. Listing 22-15 contains the source code of the application displayed in Figure 22-15.

image from book
Figure 22-15: Drawing a real gradient with custom colors

Listing 22-15: Drawing gradients that support custom colors

image from book
unit Unit1; interface uses   Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,   Dialogs, ExtCtrls, XPMan, Menus; type   TMainForm = class(TForm)   private     { Private declarations }   public     { Public declarations }     procedure EraseBackground(var Message: TWMEraseBkgnd);         message WM_ERASEBKGND;   end; var   MainForm: TMainForm; implementation {$R *.dfm} procedure TMainForm.FormPaint(Sender: TObject); var   startColor: TColor;   endColor: TColor;   redStart, blueStart, greenStart: Integer;   redStep, blueStep, greenStep: Double;   i: Integer;   rc: TRect; begin   if ClientHeight = 0 then Exit;   { use colors from the two TColorDialogs }   startColor := StartColorDialog.Color;   endColor := EndColorDialog.Color;   { extract R, G, and B value from the start color }   redStart := GetRValue(startColor);   greenStart := GetGValue(startColor);   blueStart := GetBValue(startColor);   { determine how much endColor you have to add to startColor each step }   redStep := (GetRValue(endColor) - redStart) / ClientHeight;   greenStep := (GetGValue(endColor) - greenStart) / ClientHeight;   blueStep := (GetBValue(endColor) - blueStart) / ClientHeight;   for i := 0 to ClientHeight do   begin     Canvas.Pen.Color := RGB(redStart + Round(i * redStep),       greenStart + Round(i * greenStep), blueStart + Round(i * blueStep));     Canvas.MoveTo(0, i);     Canvas.LineTo(ClientWidth, i);   end;   { draw the Caption }   rc := ClientRect;   Canvas.Brush.Style := bsClear;   Canvas.Font := FontDialog.Font;   DrawText(Canvas.Handle, PChar(Caption), -1, rc,     DT_SINGLELINE or DT_VCENTER or DT_CENTER); end; procedure TMainForm.StartColorItemClick(Sender: TObject); begin   if StartColorDialog.Execute then Invalidate; end; procedure TMainForm.EndColorItemClick(Sender: TObject); begin   if EndColorDialog.Execute then Invalidate; end; procedure TMainForm.SelectFontItemClick(Sender: TObject); begin   if FontDialog.Execute then Invalidate; end; procedure TMainForm.ExitItemClick(Sender: TObject); begin   Close; end; procedure TMainForm.FormResize(Sender: TObject); begin   Invalidate; end; procedure TMainForm.EraseBackground(var Message: TWMEraseBkgnd); begin   Message.Result := 1; end; end.
image from book



Inside Delphi 2006
Inside Delphi 2006 (Wordware Delphi Developers Library)
ISBN: 1598220039
EAN: 2147483647
Year: 2004
Pages: 212
Authors: Ivan Hladni

Similar book on Amazon

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