1. Tech

Your suggestion is on its way!

An email with a link to:

http://delphi.about.com/library/bluc/text/uc052102i.htm

was emailed to:

Thanks for sharing About.com with others!

RTL referenceGlossaryTips/Tricks|FREE App/VCL|Best'O'Net|Books|Link To
 
GDI Graphics In Delphi
Page 9: API Drawings
 More of this Feature
• Page 1: GDI Jargon
• Page 2: Drawing: Lines
• Page 3: Drawing: Shapes
• Page 4: Draw vs. Paint
• Page 5: Handles and Stuff
• Page 6: Pictures: TBitmap
• Page 7: Ways To Kill Flicker
• Page 8: GDI, The Hard Way
 Join the Discussion
"Post your views, comments, questions and doubts to this article."
Discuss!
 Related Resources
• Graphics programming in Delphi
• Screen zooming
• Double buffering
• Win API in Delphi

   From the Top
After discussing all that stuff, let's have a look at doing everything the hard way with an example. I'll draw some lines.

Things You Need To Know
The first thing you'll encounter is the window class. A "window class" isn't really a class at all, but a record. Windows uses the details in this record when creating a window. What happens is that you fill out details in your "window class" and register it. Then, you use CreateWindow (or CreateWindowEx) to create the window, which causes Windows to look up information about it (saying things like: "ah, that icon" and "oh yeah, a grey background"). It gets this information from the registered "window class" details.

Next, there is a message loop. Windows works by events. When something happens, for example a mouse movement or key press, Windows sends a message to every application. If you want to respond to the event then you are responsible for handling it (though usually we ignore most messages, only handling a small subset). Remember: you program sits around waiting for messages, not being proactive. It says "is there a message yet? How about now? Still no message? Is there a message? Where's the message?" (repeat until Windows gets a message for the program to deal with). The message loop is an infinite loop. Because you don't know when your program will end you must loop indefinitely. In each loop of your message loop you use PeekMessage or GetMessage to take a message off of the event queue. Messages are put there by Windows as and when needed. You process that message using TranslateMessage and DispatchMessage. This process continues until you receive a WM_QUIT message, at which point you break out of your message loop.

Next up is the message handler. This is a special call-back function. What's a call-back function? Well, you specify a function as a field of the "window class" record. Windows takes it and calls it when necessary. That's right: you don't call the function, but let Windows do it instead, as needed. In practise, this function gets called via a complex chain after DispatchMessage in the message loop.

So what does the message handler do? Well, it handles messages (duh ;). One of its parameters is the type of message. You use a case statement on this, using pre-defined constants such as WM_PAINT, WM_DESTROY, and so on. When you are interested in a message you write code to handle it. However, you may not be interested in handling every message so at the end the DefWndProc is called, which is the default handler. Windows has default responses to messages, and by calling DefWndProc you are saying, "I don't care about this message, do with it what you want."

There are two very cryptic parameters to the message handler function: "wParam" and "lParam". What are they? Well, it depends on the message. They are both integers, and Windows uses them for whatever information is relevant to the message. For example, with WM_SIZE (called when your window gets resized) the lParam stores the new width and height information. Windows can also be cunning and pack several parts of information into a single number, which is why you might need to use LOWORD and HIWORD. These separate out the information. You'll be told in the help files when you need to use HIWORD and LOWORD, so don't worry too much.

One other final note: the code for an API program goes in your .DPR file... you know, the one that you hardly ever touch? Well, you do now. Remember to remove the form from a new project with "remove file" because we don't want any VCL (that's the whole point of this tutorial part!).

The Process
What's the process for making a stand-alone win32 application? It goes like this:

  1. Declare a TWndClass (or TWndClassEx)
  2. Fill its details out (it's a record)
  3. Register a new "window class", passing in your TWndClass
  4. Create a new window with CreateWindow (or CreateWindowEx)
  5. STORE THE HANDLE YOU GET FROM CREATEWINDOW (important!)
  6. Show the window with ShowWindow
  7. Update the window with UpdateWindow (optional)
  8. Go into an infinite loop (your message loop)
    1. Use PeekMessage (or GetMessage) to check for new messages
    2. Do anything else after that (draw your animation, whatever)
    3. Lock to a frame rate (optional)
  9. Write a window function to handle message events

Eeeeeeeeeeeek! Don't worry, we'll go through it step by step. Once you have template code it's just a copy and paste job. Luckily, I'm giving you such template code. Let's look at each bit now, because it sounds pretty scary without code (and is almost as scary with).

1) Declare a TWndClass:

var
  wc: TWndClass; // details about our window
  Msg: TMsg; // declare this too, for later
begin

2) Fill its details out (it's a record):

with wc do
  begin
    Style := CS_VREDRAW or CS_HREDRAW; // note 1
    lpfnWndProc := @WndProc; // note 2
    cbClsExtra := 0; // extra info: almost always 0
    cbWndExtra := 0; // extra info: almost always 0
    wc.hInstance := HInstance; // note 3
    hIcon := LoadIcon(HInstance, IDI_APPLICATION); // note 4
    hCursor := LoadCursor(HInstance, IDC_ARROW); // note 5
    hbrBackground := COLOR_BTNFACE + 1; // note 6
    lpszMenuName := nil; // note 7
    lpszClassName := AppName; // note 8
  end;

The notes:

Note 1: These two mean to redraw the screen if anything happens to the width or height (movement or resize). You can also investigate CS_OWNDC.
Note 2: A call-back function. You write a function WndProc (see later) and when Windows gets a message it calls it. This lets you handle message events.
Note 3: HInstance is supplied for you by Delphi. You can replace it with GetModuleHandle(nil), though for me that had the unexpected effect of displaying the hourglass cursor when the program began until I moved the cursor elsewhere.
Note 4: The icon for our program (top-left of window and in taskbar). It's set to a default just now, but you could include an icon in a resource file and use that instead.
Note 5: The cursor to use, which I've set as the standard one here.
Note 6: Set the background to grey. Note the offset: "+ 1". There are quite a few constants like COLOR_BTNFACE. You can also look at GetStockObject(). Do so now.
Note 7: We haven't got a drop-down menu for this window
Note 8: A constant for the "window class" name. It should be the same as your first parameter to CreateWindow. In my program, it was: "const AppName = 'GDI Lines';"

Although this structure was huge, don't be scared. Most of the stuff is copy and paste.

3) Register a new "window class", passing in your TWndClass

This one's easy:

 RegisterClass(wc);

4) and 5) Create a new window with CreateWindow (or CreateWindowEx) and STORE THE HANDLE YOU GET FROM CREATEWINDOW (important!)

g_Handle := CreateWindow(AppName, 'GDI Lines',
    WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT,
    CW_USEDEFAULT, CW_USEDEFAULT, 0, 0, HInstance, nil);

This is another of those scary functions Windows does so well ;). CreateWindow allows you to create a window based on any previously registered "Window Class". That's why it was important for you to go through the nonsense above.

g_Handle here is a global variable I declared at the top of the program, of type HWND. Remember that handles are used in Windows to identify everything. Therefore, it's vital we store this because we'll be using it a lot.

The first parameter is that of your "window class" lpszClassName (that's a long pointer to a zero-terminated string, Hungarian notation fans). The next is the window caption. Then it's the screen x and y positions, and the window width and height. The others from that point on are default values... again, copy and paste without fear!

6) Show the window with ShowWindow

  ShowWindow(g_Handle, CmdShow);

This displays your window. CmdShow is supplied by Delphi. It's for the "start minimised", "start normal", "start maximised" options for programs.

7) Update the window with UpdateWindow (optional)

  UpdateWindow(g_Handle);

This sends a WM_PAINT message. Note how much we're the g_Handle already.

8) Go into an infinite loop (your message loop)

while True do
  begin
    if PeekMessage(Msg,0,0,0, PM_REMOVE) then
    begin
      if Msg.message = WM_QUIT then
        break;
      TranslateMessage(Msg);
      DispatchMessage(Msg);
    end
    else
    begin
      // Do rendering here if a real-time app
    end;
  end;

  ExitProcess(Msg.wParam); // just to be complete

The above keeps on processing messages until a WM_QUIT message happens, which is Windows telling us to get outta town.

PeekMessage checks the message queue for a message. If it finds it, it removes it from the queue (with PM_REMOVE specified, anyway). We then process it with TranslateMessage and DispatchMessage, which will call our lpfnWndProc function (our message handler). That's where the main action happens. You can use GetMessage instead of PeekMessage, although that blocks (sits there waiting for a message). If there isn't a message with PeekMessage it immediately continues. This makes it suitable for real-time effects, games, animations, etc.

9) Write a window function to handle message events

This should actually go above the previous code, but you'll be downloading the sample code and using that, so you should know that by now. Anyway...

We want to handle the Windows message WM_PAINT. This gets sent whenever Windows wants to update your window (little 'w'). You can capture it and do your drawing here. So what does that mean?

Add a case for WM_PAINT in your message handler. The message handler is a call-back function. That means you supply it to Windows when you register your "window class" and Windows calls it whenever a new message arrives (your "Window Class" had a field for this, remember?):

function WndProc(HWnd: HWND; Msg: UINT; wParam: WPARAM; 
                        lParam: LPARAM): LRESULT; stdcall;
var
  ps: TPaintStruct;
  DrawDC: HDC;
begin
  case Msg of
    WM_DESTROY:
      begin
        PostQuitMessage(0); // stop message loop
        Result := 0;
        Exit;
      end;
    WM_PAINT:
      begin
// note: g_Handle is the handle to our window, got from CreateWindow
// tell Windows we're painting the window
         DrawDC := BeginPaint(g_Handle, ps); 
         DrawOurStuff(DrawDC);
         EndPaint(g_Handle, ps); // we've stopped painting now
         Result := 0;
         Exit;
       end;
    WM_SIZE: // track window size changes
      begin
        WindowWidth := LOWORD(lParam);
        WindowHeight := HIWORD(lParam);
      end;
    // handle other messages, e.g. WM_KEYDOWN
  end;

  // We let any other, unhandled, messages be dealt with by the default handler
  Result := DefWindowProc(HWnd, Msg, wParam, lParam); // default message handler
end;

The above code gets called in response to Windows receiving a message. Messages handlers check the "Msg" parameter to see what the message type is. If it's one we care about, we add a line or two in our case to handle the message (like here, with WM_PAINT). If we haven't handled the message we call the default message handler, DefWindowProc with the same parameters. This lets Windows do its standard handling, and means no messages are skipped.

First of all, remember that WM_DESTROY handler. Without it, your program will hang around in the background after you close it. That is, if you click the close button on the window top-right and press CTRL-ALT-DEL, you'll still see it in the task manager list. Bleh!

The second part to note is that we call BeginPaint. This function lets Windows do some preparation so we can draw in the window. The first parameter is a handle to the window. This lets Windows identify what needs drawn, whether the background needs erased, etc. The second parameter is a TPaintStruct. This contains information that's quite useful, but that we don't need here.

Having called BeginPaint, we need a HDC (handle to device context, remember) to draw with. This is the return value of BeginPaint, so we store it.

After that, it's a case of drawing our stuff with the newly acquired HDC and then finishing up. Once we've done our drawing, we call EndPaint. This is pretty obvious: we let Windows know we've finished and it says, "thanks!"

Well, that's us got our HDC, which fills in the blank from the very first example: "This code, of course, assumes you have already nabbed a DC".

Let's add in an example function that draws lots of lines, for the sheer hell of it:

const
  NUM_SHAPES = 4000;

procedure DrawOurStuff(DrawDC: HDC);
var
  i: Integer;
  OldPen, DrawPen: HPEN;
begin
  // Create our pen to draw with. It will be a solid line style, of width 1 and
  // a completely random colour
  DrawPen := CreatePen(PS_SOLID, 1, RGB(Random(256), Random(256), Random(256)));

  // Select our new pen into the DC, so lines will be drawn using it. Store
  // the old pen too
  OldPen := SelectObject(DrawDC, DrawPen);

  // Draw our lines
  for i := 0 to NUM_SHAPES - 1 do
    LineTo(DrawDC, Random(WindowWidth), Random(WindowHeight));

  // Select the old pen back again so Windows doesn't complain
  SelectObject(DrawDC, OldPen);

  // and kill the pen we created
  DeleteObject(DrawPen);
end;

Why Did We Do All That?
Compile an empty program (start a new application). See how huge the exe is (for me, it's 351Kb with Delphi 6). Now, let's compare... the above code compiles (with Delphi 6) to... 11Kb!!! (!!) That's just slightly smaller. (11/351) * 100 = approx 3.1%. That's right, the above application is 3.1% of the size of our empty VCL program. Ahem! Suddenly you begin to resent the bloatware downloads other people use...

Remember that you don't need to memorise the above code. Just create a template API project (with a .dpr and .res file only! - don't include anything else like .dofs or .cfgs, which will mess up things if you copy and paste). Et voila! You've got a one-way ticket to smaller exes.

That's all folks!

   Question, Suggestions...
If you have any questions or comments to this (huge) article, please post them on the Delphi Programming Forum. Discuss!

First page > GDI Jargon > Page 1, 2, 3, 4, 5, 6, 7, 8, 9

All graphics (if any) in this feature created by Zarko Gajic.

 More Delphi
· Learn another routine every day - RTL Quick Reference.
· Download free source code applications and components.
· Talk about Delphi Programming, real time.
· Link to the Delphi Programming site from your Web pages.
· Tutorials, articles, tech. tips by date: 2001|2000|1999|1998 or by TOPIC.
· NEXT ARTICLE: Articles.
More Delphi articles
 Stay informed with all new and interesting things about Delphi (for free).
Subscribe to the Newsletter
Name
Email

 Got some code to share? Got a question? Need some help?

©2014 About.com. All rights reserved.