Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

Tools

Don't Be a Square


Dr. Dobb's Sourcebook January/February 1997: Don't Be a Square

Al is a consultant and author based in the Houston area. His most recent book is Developing ActiveX Web Controls (Coriolis Group, 1996). You can find Al on the Web at http://ourworld.compuserve.com/homepages/Al_Williams.


Computer programmers are frequently turning up in movies and television today. Jurassic Park, Independence Day, and The Net come to mind as recent examples. If you are like me, you find this a bit irritating. For one thing, they always show these guys doing things that seem incredible to us. I'm still searching the Web to find one of those mini satellite dishes that Jeff Goldblum used in Independence Day.

Of course, I'm used to techno-ignorance in movies. I've been an amateur (ham) radio operator for 20 years. I often see radio operators in movies slapping wildly at telegraph keys in meaningless gibberish (another scene from Independence Day, come to think about it). I don't even want to talk about Forest Whittaker's ham radio character broadcasting monologues at Diana Ross in Phenomenon. Even our beloved Star Trek has had a few blunders from time to time.

But what really irks me is how programmers in the movies are always squares, nerds, and geeks. Sure, some of us fit that description (some would say I do). But not everyone does. I don't even own a pocket protector (well, at least I don't anymore).

Of course, Windows programs are square (or, at least, rectangular). Under Windows 3.1, you would have a very difficult time trying to make a window that looks like a circle (or a star, or a logic symbol). With Win32, however, it is easy. Surprisingly, now that Win32 is the predominant operating platform, you still don't see many programs with nonrectangular windows. Maybe we are square.

In this installment, I'll show you how to free Delphi programs from their square confines. Along the way, we'll look at free-style painting and a very clever way to force Windows to perform dragging chores for you. Although the example code is in Delphi, the irregular-window technique is quite simple and applicable to most languages.

I almost chose Visual J++, Microsoft's new Java environment, as the topic for this column. I decided to wait because the release version came out just before my deadline. However, a bit later in this column, I'll give you my first impressions.

No Squares Allowed

Look at the clock in Figure 1. Notice anything unusual about it? It uses a circular window. What's more, you can drag it around by clicking on the face and moving the mouse. Now look at Figure 2. That's the same program in the Delphi design environment. Looks quite a bit different, doesn't it?

The design window doesn't have any drawing on it and has only two controls: a button and timer. The window is also obviously rectangular. Further, there is no provision in Delphi for enabling dragging with a form. Let's look at each of these problems individually.

Creating aNonrectangular Window

Creating an oddly shaped window is easy. First, you need to construct a region that has the shape you want for your window (in this case, the form). For the clock, this is a simple matter of calling CreateEllipticalRegion() during FormCreate. Then, a call to SetWindowRgn() does the rest. The call requires a window handle -- no problem, since the Handle property will supply that. It also takes the desired region. You can find this code in the FormCreate procedure in Listing One.

About Regions

Windows uses regions for many things (clipping regions, are one example). However, the regions that SetWindowRgn takes are a bit different. Normally, when you construct a region, it is relative to the client area of the window. Regions that work with SetWindowRgn, however, are relative to the top-left corner of the entire window. CreateEllipticalRegion is fine for circular windows, but what about other shapes?

Windows gives you several choices for constructing regions. CreateRectRgn is useless in this case, since the default window region is rectangular anyway. However, CreateRoundRectRgn and CreatePolygonRgn can be useful (see Figures 3 and 4). Also, you can generate complex regions by calling CombineRgn.

The polygon region for Figure 4 consists of five points. Assume that the height of the caption bar is 25 pixels (you can find this out with GetSystemMetrics). If the window were 100 pixels high by 200 pixels wide, the points would be: (0,0), (0,25), (50,200), (100,25), and (100,0). Why not just create a simple triangle?

Things to Consider

When you clip a main window, you need to think about how it will affect the standard system items on that window. For example, the clock clips its system menu and borders. That's why there is a close button in the middle of the clock. Since the borders never appear anyway, I made the clock a fixed size.

The polygon region in Figure 4 is a compromise triangle. While it has a triangular shape, it has a rectangle at the top to contain the system menu, caption bar, and system controls. You could also make room for a menu, if you wanted to have a menu bar. The rounded rectangle in Figure 3 does a nice job of leaving the system elements alone while still providing a unique visual appearance.

Notice that the triangular-shaped window allows you to resize it by grabbing the apex of the triangle. This means that during OnResize events, you'll need to calculate a different region and call SetWindowRgn again.

Don't forget that the region is relative to the top of the window, not the client area. Unlike a normal window rectangle, however, the top left of the region is point (0,0). If you need to know the bounding rectangle for the window, you'll need to use code like that shown in Example 1.

Windows without Controls

Delphi has a split personality. On the outside, it presents an interface that looks like Visual Basic. You have a form and components. You drop components on the form to create a program. However, inside Delphi is a complete windowing class library similar in some ways to MFC for C++.

For many programs, dropping components on a form works well, but sometimes you want more control. Take, for example, the clock face. How can you easily create that with components?

Luckily, Delphi allows you to use controls and draw on windows at the same time. The key is to override the OnPaint handler for the form. Delphi will call OnPaint every time Windows requests a redraw.

The key to drawing inside a form is the form's canvas. If you have a traditional Windows background, you can think of a canvas as a device context for most purposes. The canvas serves two purposes: It is a place to draw and it is a set of tools to use while drawing. For example, consider drawing a line. Given a canvas named Canvas, you can draw a 45 degree line using Example 2(a).

So what does the line look like? Well, that depends on the canvas' Pen property (see Table 1 for other interesting canvas properties). The Pen property allows you to set the line width, style, and color. Example 2(b) is from the clock example that draws the hour hand. Don't worry about all the trigonometry, but do notice the use of the Pascal with statement to avoid having to write Form1.Canvas so many times. The code sets the width of the pen to 22 pixels and the color to red. Unlike a regular device context, the canvas will retain these settings until you change them.

Other items you can set in a canvas include the brush (to fill interiors) and the font (for drawing characters). Some things that are missing include the text background color and the text background mode. However, you can still use the Windows API to change these if necessary. Simply use the canvas' Handle property to call the Windows API; see Example 2(c).

Of course, just because you draw on the form doesn't mean you can't use controls, too. The clock, for example, uses a perfectly ordinary button and an invisible timer control. Drawing on windows is very useful if you are developing your own custom controls, too.

There is one fine point to drawing that is worth mentioning. You shouldn't draw outside the OnPaint handler except under very unusual circumstances. Remember, Windows may ask you to draw any portion of your window at any time. Suppose you draw the clock face in response to the timer event. Then a user covers the clock with another program. When the covering program disappears, Windows will ask you to redraw the clock by sending a WM_PAINT message (that triggers an OnPaint event). Granted, you could duplicate your drawing code in each place, but that is wasteful and difficult to maintain. A better answer is to place all your drawing code in OnPaint and then use Invalidate to trigger a repaint when you want to redraw for your own purposes.

When should you not draw during OnPaint? The one situation where that makes sense is when you are drawing in response to a mouse event (for example, dragging a rubber-band rectangle around with the mouse). You need these drawings to happen at once. There is no assurance that an Invalidate will trigger an OnPaint immediately if other messages (like mouse messages) are pending.

Dragging a Form

Usually, users move forms by grabbing the caption bar and dragging the window around. The clock doesn't have a caption bar. Even if it did, you wouldn't be able to see all of it. Besides, it would look odd to have a caption bar roosting above the 12:00 position.

Delphi programmers have it easy when it comes to creating drag-and-drop Windows. Practically every control has a DragMode property. If you set the property to dmAutomatic, Delphi takes care of everything for you.

Forms, however, don't have a DragMode property. Delphi won't move them for you. Well, you could do it yourself. First, you'd detect the mouse-down event and set a flag. Then, you'd need to watch for mouse-move events. If the flag is set, you'd move the window to match the mouse coordinates. Finally, when the mouse button went up, you'd clear the flag. Actually, it would be even more difficult, since you'd need to track the offset of the window versus the mouse pointer. But you get the idea.

Since Delphi won't do the work, how about Windows? Windows would do what we want if the window had a caption bar. Can we cheat and pretend the window has a caption bar? You bet we can!

Windows really doesn't know exactly what the mouse pointer is over at any given time. It only knows that it is over a window. To find out more, Windows sends a WM_NCHITTEST message (that stands for nonclient hit test). The window processes this message by figuring out what portion of the window the mouse is over and returning a special code (see Table 2). For example, if the mouse is over the client area, the window returns HTCLIENT. It returns HTCAPTION for the caption bar.

Normally, you don't process this message. The default window procedure (built into Windows) does it for you. However, you may process it if you wish. For example, windows that have skinny caption bars must override default processing.

This still sounds like a lot of work, right? It would appear that you have to determine when the mouse is over the right area and respond to the message. But this is not exactly the case. The clock only shows its client area, so it would be perfectly acceptable to simply return HTCAPTION at all times. This wouldn't work if the window had a menu or system icons. A more general solution is to call the inherited version of the message handler. If it returns HTCLIENT, change the value to HTCAPTION. In all other cases, allow the return value to remain unchanged.

Handling messages in Delphi is straightforward. Simply define a procedure with the message attribute; see Example 3(a). If you like, you can use a variation of TMessage that parses the normal wParam and lParam parameters for you. In this case, you simply don't care. But wait. How can a procedure return HTCAPTION or any other value? There is a Result field in TMessage (and related record types). To return a value from message processing, you fill in the Result field and return; see Example 3(b).

The Inherited keyword causes Delphi to call the base-class version of the message handler. Then it is a simple matter to convert return codes of HTCLIENT to HTCAPTION. With this code in place, you can click anywhere on the client area of the window and drag it around the screen.

The Clock Strikes One

Well, if you ever want a round-face clock, you now know exactly what to do. There are other things you can do with irregular windows, too. How about a radar screen for a game? Or gauges? What about windows shaped like flowchart symbols or logic gates? There are many possibilities. Be sure to drop me a note and tell me what you've done with irregular windows.

A First Look at Visual J++

Unless you've been hiding under a rock, you must have noticed the Internet explosion. One interesting byproduct of this explosion is Java. Java is a language like C++ in some ways and very unlike C++ in others. Of course, languages are a dime a dozen and everyone wants to build their own. Three things make Java different.

  • Sun Microsystems developed it and is actively promoting it for Web development.
  • It runs on many different platforms.
  • Many Java environments deliberately can't do certain operations that would help people write web viruses.

Java lets you write applets -- window-based programs that run inside web browsers. However, to write an applet in plain Java takes considerable knowledge about Java classes and the Abstract Window Toolkit that Java uses to handle windowing.

Although Microsoft is touting ActiveX as a replacement for Java, it is hedging its bets by providing a Java development environment built on the same environment their C++ compiler uses (the Microsoft Developer's Studio). As Java environments go, Visual J++ isn't better (or worse) than any other Java environment. However, it makes it much easier to get started. You use a Wizard to start your application. Visual J++ writes all the code you need to get a bare-bones applet (or full application) started. Then, you just flesh out the code. If you are familiar with Visual C++, there isn't anything like Class Wizard (not yet, anyway). Even without Class Wizard, the environment is still a big improvement over coding by hand.

Java doesn't support the idea of resources in the same way that Windows does. Instead of storing dialog templates and menus as resources, Java forces you to write code to represent them. This has certain advantages, but it does mean more work. With Microsoft's Visual J++, you can continue to use the familiar resource editor to create dialogs and menus. When you are ready, the Resource Wizard will convert them to Java source files you can include in your project. The beta copy of VJ++ that I used didn't always do the conversion perfectly (text fields were sometimes shorter than their contents, for example). However, the results were good and you don't have to know much about Java to get it to work -- a few minor tweaks and you're up and running.

The other strong point to VJ++ is the debugger. You get the usual debugging environment with watches, breakpoints, and all the other things you expect from a C++ debugger. If you are writing serious Java code, you must have a debugger. Of course, VJ++ isn't the only game in town. See Steve Yalovitser's article, "Visual Development Tools for Java," (Dr. Dobb's Sourcebook, July/August 1996) if you want to read about similar environments from Autodesk, Rouge Wave, and Symantec.

Do you want to hear more about Visual J++ in future columns? Are you tired of hearing about the Internet in general and Java in particular? Let me know. Visit my web site (http://ourworld.compuserve.com/homepages/Al_Williams/) and drop me a note or send me a letter in care of the magazine. Remember: If you don't vote, you can't complain.

DDJ

Listing One

{ Odd shaped clock by Al Williams }
unit clock;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  StdCtrls, ExtCtrls;
type
  TForm1 = class(TForm)
    Button1: TButton;
    Timer1: TTimer;
    procedure FormCreate(Sender: TObject);
    procedure FormPaint(Sender: TObject);
    procedure Button1Click(Sender: TObject);
    procedure Timer1Timer(Sender: TObject);
  private
    { Private declarations }
    { This message handler allows you to drag the clock }
  procedure WMNCHitTest(var Msg: TMessage); message WM_NCHITTEST;
  public
    { Public declarations }
  end;
var
  Form1: TForm1;
implementation

{$R *.DFM}
procedure TForm1.FormCreate(Sender: TObject);
var
   rgn : HRGN;
   r : TRect;
begin
   { Region is relative to window area }
   GetWindowRect(handle,r);
   { normalize so that 0,0 is top left }
   r.right:=r.right-r.left;
   r.bottom:=r.bottom-r.top;
   r.left:=0;
   r.top:=0;
   { Create ellipse region }
   rgn:=CreateEllipticRgn(0,0,r.right,r.bottom);
   { Force clock to be circular }
   SetWindowRgn(handle,rgn,False);
end;
procedure TForm1.FormPaint(Sender: TObject);
var
  r : TRect;
  radius : Integer;
  pt : TPoint;
  now : TDateTime;
  hour, min, sec, msec : Word;
  tm : TTextMetric;
  hi : Integer;
  angle : Extended;
  x,y : Integer;
begin
  now:=time;                     { get current time}
  DecodeTime(now,hour,min,sec,msec);
  if hour >= 12 then hour:=hour-12; { adjust for 12 hour clock }
  r:=GetClientRect;  { find center of square window }
  pt.x:=r.right div 2;
  pt.y:=r.bottom div 2;
  if r.right<r.bottom then  { use shortest dimension as radius }
    radius:=r.right div 2
  else
    radius:=r.bottom div 2;
  SetBkMode(Form1.Canvas.Handle,TRANSPARENT);
  SetTextAlign(Form1.Canvas.Handle,TA_CENTER);
{ use with to avoid writing Form1.Canvas all the time }
  with Form1.Canvas do
    begin
{ Set font for numerals }
    Font.name:='Arial';
    Font.size:=-40;
{ find height of text }
    GetTextMetrics(Handle,tm);
    hi:=tm.tmHeight+tm.tmExternalLeading;
    Pen.Width:=GetSystemMetrics(SM_CXBORDER);
    Pen.Color:=clBlack; { reset for 2nd time around }
{ Draw border }
    Ellipse(5,5,(r.right-Pen.Width)-5,(r.bottom-Pen.Width)-5);
{ Draw inner circle }
    Pen.Color:=clGray;
    Ellipse(30,30,r.right-30,r.bottom-30);
{ Draw numerals }
    TextOut(pt.x,pt.y-radius,'12');
    TextOut(pt.x+radius,pt.y-hi div 2,'3');
    TextOut(pt.x,pt.y+radius-hi,'6');
    TextOut(pt.x-radius,pt.y-hi div 2,'9');
{ Get ready to draw hands }
    Pen.Width:=22;
    Pen.Color:=clRed;
    angle:=((hour+min/60)*30-90)*0.0175; { degrees->radians }
    x:=Round(2*(radius/3)*cos(angle))+pt.x;
    y:=Round(2*(radius/3)*sin(angle))+pt.y;
    MoveTo(pt.x,pt.y);
    LineTo(x,y);
    angle:=(min*6-90)*0.0175;
    x:=Round(9*(radius/10)*cos(angle))+pt.x;
    y:=Round(9*(radius/10)*sin(angle))+pt.y;
    MoveTo(pt.x,pt.y);
    LineTo(x,y);
    end;
end;
{ This message handler allows you to drag the clock }
procedure TForm1.WMNCHitTest(var Msg: TMessage);
begin
  inherited;
  { if we are over the client area, lie! }
  if Msg.Result = HTCLIENT then Msg.Result:=HTCAPTION;
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
  Form1.Close
end;
{ Resist the urge to redraw clock hands here! Instead, just
  invalidate the form. }
procedure TForm1.Timer1Timer(Sender: TObject);
begin
Invalidate;
end;

end.

Back to Article


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.