Author: Lou Adler
Is it possible to create forms with shapes other than the standard rectangular
shape in Windows?
Answer:
Sometimes it's just not enough to write applications that have the same boring
rectangular forms over and over again. Sometimes you need a change. How about an
elliptical form? Or maybe even a triangular form? Sound intriguing? It's not that
hard to do.
New in Win32 is something called a region. The Win32 API Programmer's Reference
defines a region as follows:
...a rectangle, polygon or ellipse (or a combination of two or more of these
shapes) that can be filled, painted, inverted, framed and used to perform hit
testing (testing for the cursor location).
From the definition, the most notable thing about a region is that it can be
manipulated in a variety of ways. For our purposes we want to define a region to
create a specific shape.
I should point out that a region can be defined for just about any TWinControl
descendant (not just forms), meaning you can apply a region to a TPanel or even a
TEdit (though I strongly recommend against it). But to alter the shape of a
TWinControl descendant, all you need to provide is a handle and employ some
handy-dandy shape change functions.
To get a control to change its shape, follow this two-step process:
Define the boundaries of the region that represent a particular shape.
Apply the boundaries you've defined to a window.
This is pretty simple. However, it's very important to refer to the help file, and
to have the source at hand. I wouldn't be able to accomplish many of my projects,
let alone write many of the articles I write here, without those two resources at
my disposal. Especially with the Windows API calls, having access to the Window.PAS
file is essential so I know what to pass into the functions. Remember, the WinAPI
calls are really wrapper calls into the appropriate Windows DLLs, and of course,
the help file is essential to getting background information on the topic you're
interested in.
With respect to this article, look up the SetWindowRgn topic in Win32 Developer's
Help, and have it handy while you're putting together your program. Pay particular
attention to the Group hyperlink because it will give you a run-down of all the
procedures related to the region topic. Let's move on!
Defining a Region's Boundary
The first step to creating a form of a different shape is to define the shape
itself. For our discussion, we'll use three WinAPI calls:
CreateEllipticRgn
This function will create an elliptically-shaped region.
CreateRoundRectRgn
This will create a rectangular region with rounded corners.
CreatePolygonRgn
This will create just about any multi-sided shape, as long as the lines form a
closed solid.
These functions return a HRGN type, which will then be used by a function called
SetWindowRgn whose sole purpose in life it is to set the parameters defined by a
particular region variable. I've encapsulated these functions in methods that are
part of a demonstration form. The functions are coded as follows:
1
2 {===========================================================================
3 Notice that all the functions are used in an assignment
4 operation to a variable called rgn. This is a
5 private var that I declared for the form. The private var is
6 accessible to all functions; I did this so that I could change the shape of
7 the form or a control on the form, and use the same region.
8 ===========================================================================}
9
10 procedure TForm1.DrawEllipticRegion(wnd: HWND; rect: TRect);
11 begin
12 rgn := CreateEllipticRgn(rect.left, rect.top, rect.right, rect.bottom);
13 SetWindowRgn(wnd, rgn, TRUE);
14 end;
15
16 procedure TForm1.DrawRndRectRegion(wnd: HWND; rect: TRect);
17 begin
18 rgn := CreateRoundRectRgn(rect.left, rect.top, rect.right, rect.bottom, 30, 30);
19 SetWindowRgn(wnd, rgn, TRUE);
20 end;
21
22 procedure TForm1.DrawPolygonRegion(wnd: HWND; rect: TRect; NumPoints: Integer;
23 DoStarShape: Boolean);
24 const
25 RadConvert = PI / 180;
26 Degrees = 360;
27 MaxLines = 100;
28 var
29 x, y,
30 xCenter,
31 yCenter,
32 radius,
33 pts,
34 I: Integer;
35 angle,
36 rotation: Extended;
37 arPts: array[0..MaxLines] of TPoint;
38 begin
39
40 xCenter := (rect.Right - rect.Left) div 2;
41 yCenter := (rect.Bottom - rect.Top) div 2;
42 if DoStarShape then
43 begin
44 rotation := Degrees / (2 * NumPoints);
45 pts := 2 * NumPoints;
46 end
47 else
48 begin
49 rotation := Degrees / NumPoints; //get number of degrees to turn per point
50 pts := NumPoints
51 end;
52 radius := yCenter;
53
54 {This loop defines the Cartesian points of the shape. Notice
55 I've added 90 degrees to the rotation angle. This is so that shapes will
56 stand up; otherwise they'll lie on their sides. I had to
57 brush up on my trigonometry to accomplish this (forgot all those sin and cos
58 thingies. Many thanks to Terry Smithwick and David Ullrich for their
59 assistance on CompuServe!}
60 for I := 0 to pts - 1 do
61 begin
62 if DoStarShape then
63 if (I mod 2) = 0 then //which means that
64 radius := Round(radius / 2)
65 else
66 radius := yCenter;
67
68 angle := ((I * rotation) + 90) * RadConvert;
69 x := xCenter + Round(cos(angle) * radius);
70 y := yCenter - Round(sin(angle) * radius);
71 arPts[I].X := x;
72 arPts[I].Y := y;
73 end;
74
75 rgn := CreatePolygonRgn(arPts, pts, WINDING);
76 SetWindowRgn(wnd, rgn, TRUE);
77 end;
The first two functions are pretty simple, just two-liners. All that's needed to
create the appropriate shapes is a handle and a TRect structure. For forms, that
structure would be taken from the ClientRect property; for other controls, use the
BoundsRect property.
The DrawPolygonRegion method, however, is much more complex. This is due in part to
the fact that CreatePolygonRgn requires the vertices of the corners of the polygon
to be passed as an array of TPoints, and partly because I wanted to draw
equilateral polygons based off points rotated around a common center point. For
that I had to use some trigonometry.
I wanted to not only draw polygon regions, but stars as well. Using rotational trig
allowed me to do it. The way the function works if the DrawStarShape parameter is
set to True is that for every even value of I in the loop, the radius of the circle
is set to half its length, and to maintain the number of points of the polygon I
want to draw, I double the number of points to accomodate the contraction of the
radius.
At the very end of each function is a call to SetWindowRgn. This function takes as
parameters a window handle, a rgn var, and a Boolean value that specifies whether
the window should be re-drawn. In all cases, if you want to see the shape you've
made, this must be always be set to True.
Below is the listing for the entire source code of my test form. On the form I've
dropped four TButtons (one for each of the shapes: ellipse, round rectangle,
polygon and star); a TPanel to demonstrate the ability to set regions for
TWinControl descendants other than TForm; and a SpinEdit used in conjunction with
the Polygon and Star region buttons to define the number of points that'll be
defining the shape. Here's the code:
78 unit regmain;
79
80 interface
81
82 uses
83 Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
84 ExtCtrls, StdCtrls, Spin;
85
86 type
87 TForm1 = class(TForm)
88 Button1: TButton;
89 Button2: TButton;
90 Button3: TButton;
91 SpinEdit1: TSpinEdit;
92 Button4: TButton;
93 Panel1: TPanel;
94 Edit1: TEdit;
95 procedure DrawRndRectRegion(wnd: HWND; rect: TRect);
96 procedure DrawEllipticRegion(wnd: HWND; rect: TRect);
97 procedure DrawPolygonRegion(wnd: HWND; rect: TRect; NumPoints: Integer;
98 DoStarShape: Boolean);
99 procedure Button1Click(Sender: TObject);
100 procedure Button2Click(Sender: TObject);
101 procedure Button3Click(Sender: TObject);
102 procedure Button4Click(Sender: TObject);
103 private
104 { Private declarations }
105 rgn: HRGN;
106 rect: TRect;
107 public
108 { Public declarations }
109 end;
110
111 var
112 Form1: TForm1;
113
114 implementation
115
116 {$R *.DFM}
117
118 procedure TForm1.DrawRndRectRegion(wnd: HWND; rect: TRect);
119 begin
120 rgn := CreateRoundRectRgn(rect.left, rect.top, rect.right, rect.bottom, 30, 30);
121 SetWindowRgn(wnd, rgn, TRUE);
122 end;
123
124 procedure TForm1.DrawEllipticRegion(wnd: HWND; rect: TRect);
125 begin
126 rgn := CreateEllipticRgn(rect.left, rect.top, rect.right, rect.bottom);
127 SetWindowRgn(wnd, rgn, TRUE);
128 end;
129
130 procedure TForm1.DrawPolygonRegion(wnd: HWND; rect: TRect; NumPoints: Integer;
131 DoStarShape: Boolean);
132 const
133 RadConvert = PI / 180;
134 Degrees = 360;
135 MaxLines = 100;
136 var
137 x, y,
138 xCenter,
139 yCenter,
140 radius,
141 pts,
142 I: Integer;
143 angle,
144 rotation: Extended;
145 arPts: array[0..MaxLines] of TPoint;
146 begin
147
148 xCenter := (rect.Right - rect.Left) div 2;
149 yCenter := (rect.Bottom - rect.Top) div 2;
150 if DoStarShape then
151 begin
152 rotation := Degrees / (2 * NumPoints);
153 pts := 2 * NumPoints;
154 end
155 else
156 begin
157 rotation := Degrees / NumPoints; //get number of degrees to turn per point
158 pts := NumPoints
159 end;
160 radius := yCenter;
161
162 {This loop defines the Cartesian points of the shape. Again,
163 I've added 90 degrees to the rotation angle so the shapes will
164 stand up rather than lie on their sides. Thanks again to Terry Smithwick and
165 David Ullrich for their trig help on CompuServe.}
166 for I := 0 to pts - 1 do
167 begin
168 if DoStarShape then
169 if (I mod 2) = 0 then //which means that
170 radius := Round(radius / 2)
171 else
172 radius := yCenter;
173
174 angle := ((I * rotation) + 90) * RadConvert;
175 x := xCenter + Round(cos(angle) * radius);
176 y := yCenter - Round(sin(angle) * radius);
177 arPts[I].X := x;
178 arPts[I].Y := y;
179 end;
180
181 rgn := CreatePolygonRgn(arPts, pts, WINDING);
182 SetWindowRgn(wnd, rgn, TRUE);
183 end;
184
185 procedure TForm1.Button1Click(Sender: TObject);
186 begin
187 DrawEllipticRegion(Form1.Handle, Form1.ClientRect);
188 end;
189
190 procedure TForm1.Button2Click(Sender: TObject);
191 begin
192 DrawPolygonRegion(Panel1.Handle, Panel1.BoundsRect, SpinEdit1.Value, False);
193 end;
194
195 procedure TForm1.Button3Click(Sender: TObject);
196 begin
197 DrawRndRectRegion(Form1.Handle, Form1.ClientRect);
198 end;
199
200 procedure TForm1.Button4Click(Sender: TObject);
201 begin
202 DrawPolygonRegion(Panel1.Handle, Panel1.BoundsRect, SpinEdit1.Value, True);
203 end;
204
205 end.
As you can see, defining and setting regions is pretty easy. Look in the help file for in-depth discussions. If you belong to the MS Developer's Network, the library CDs discuss this topic comprehensively.
|