Draw in the caption bar
I've seen some programs that add text or buttons on the title bar of a
form. How can I do this in Delphi?
I got my first clue into solving this problem when I wrote a previous tip that
covered rolling up the client area of forms so that only the caption bar showed.
In my research for that tip, I came across the WMSetText message that is used
for drawing on a form's canvas. I wrote a little sample application to test
drawing in the caption area. The only problem with my original code was that
the button would disappear when I resized or moved the form.
I turned to well-known Delphi/Pascal guru, Neil Rubenking, for help. He pointed
me in the direction of his book, "Delphi Programming Problem Solver," which
had an example of doing this exact thing. The code you'll see below is an
adaptation of the example in his book. The most fundamental difference between
our examples is that I wanted to make a speedbutton with a bitmap glyph, and
Neil actually drew a shape directly on the canvas.
Neil also placed the button created in 16-bit Delphi on the left-hand side of
the frame, and Win32 button placement was on the right. I wanted my buttons
to be placed on the right for both versions, so I wrote appropriate code to
handle that. The deficiency in my code was the lack of handlers for activation
and painting in the non-client area of the form.
One thing that I'm continually discovering is that there is a very definitive
structure in Windows - a definite hierarchy of functions. I've realized that
the thing that makes Windows programming at the API level difficult is the sheer
number of functions in the API set.
For those who are reluctant to dive into the WinAPI, think in terms of
categories first, then narrow your search. You'll find that doing it this
way will make your life much easier.
What makes all of this work is Windows messages. The messages that we are
interested in here are not the usual Windows messages handled by vanilla
Windows apps, but are specific to an area of a window called the non-client
area. The client area of a window is the part inside the border which is where
most applications present information.
The non-client area of a window consists of its borders, caption bar,
system menu, and sizing buttons. The Windows messages that pertain to this
area have the naming convention of WM_NCMessageType. Taking the name apart,
'WM' stands for Windows Message, 'NC' stands for Non-client area, and
MessageType is the message type being trapped.
For example, WM_NCPaint is the paint message for the non-client area. Taking
into account the hierarchical and categorical nature of the Windows API,
nomenclature is a very big part of it; especially with Windows messages.
If you look in the help file under messages, peruse through the list of
messages and you will see that the order that is followed.
Let's look at a list of things that we need to consider to add a button to
the title bar of a form:
1.We need to have a function to draw the button.
2.We'll have to trap drawing and painting events so that our button stays
visible when the form activates, resizes, or moves.
3.Since we're dropping a button on the title bar, we have to have some way
of trapping for a mouse click on the button.
I'll now discuss these topics, in the above order.
Drawing a TRect as a Button
As I mentioned above, you can't drop VCL objects onto a
non-client area of a window, but you can draw on it and essentially
simulate the appearance of a button. In order to perform drawing in
the title bar of a window, you have to do three very important things
in order:
1.You have to get the current measurements of the window and
the size of the frame bitmaps so you know what area to draw
in and how big to draw the rectangle.
2.Then, you have to define a TRect structure with the proper
size and position within the title bar.
3.Finally, you have to draw the TRect to appear as a button,
then add any glyphs or text you might want to draw to the
buttonface.
All this is accomplished in a single call. For this program we make a
call to a procedure called DrawTitleButton, which is listed below:
| Pascal |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
| procedure TTitleBtnForm.DrawTitleButton;
var
bmap : TBitmap; {Bitmap to be drawn - 16 X 16 : 16 Colors}
XFrame, {X and Y size of Sizeable area of Frame}
YFrame,
XTtlBit, {X and Y size of Bitmaps in caption}
YTtlBit : Integer;
begin
{Get size of form frame and bitmaps in title bar}
XFrame := GetSystemMetrics(SM_CXFRAME);
YFrame := GetSystemMetrics(SM_CYFRAME);
XTtlBit := GetSystemMetrics(SM_CXSIZE);
YTtlBit := GetSystemMetrics(SM_CYSIZE);
{$IFNDEF WIN32}
TitleButton := Bounds(Width - (3 * XTtlBit) - ((XTtlBit div 2) - 2),
YFrame - 1,
XTtlBit + 2,
YTtlBit + 2);
{$ELSE} {Delphi 2.0 positioning}
if (GetVerInfo = VER_PLATFORM_WIN32_NT) then
TitleButton := Bounds(Width - (3 * XTtlBit) - ((XTtlBit div 2) - 2),
YFrame - 1,
XTtlBit + 2,
YTtlBit + 2)
else
TitleButton := Bounds(Width - XFrame - 4*XTtlBit + 2,
XFrame + 2,
XTtlBit + 2,
YTtlBit + 2);
{$ENDIF}
Canvas.Handle := GetWindowDC(Self.Handle); {Get Device context for drawing}
try
{Draw a button face on the TRect}
DrawButtonFace(Canvas, TitleButton, 1, bsAutoDetect, False, False, False);
bmap := TBitmap.Create;
bmap.LoadFromFile('help.bmp');
with TitleButton do
{$IFNDEF WIN32}
Canvas.Draw(Left + 2, Top + 2, bmap);
{$ELSE}
if (GetVerInfo = VER_PLATFORM_WIN32_NT) then
Canvas.Draw(Left + 2, Top + 2, bmap)
else
Canvas.StretchDraw(TitleButton, bmap);
{$ENDIF}
finally
ReleaseDC(Self.Handle, Canvas.Handle);
bmap.Free;
Canvas.Handle := 0;
end;
end;
Step 1 above is accomplished by making four calls to the WinAPI
function, GetSystemMetrics, asking the system for the width and
height of the window that can be sized (SM_CXFRAME and
SM_CYFRAME), and the size of the bitmaps contained on the title
bar (SM_CXSIZE and SM_CYSIZE).
Step 2 is performed with the Bounds function which returns a
TRect defined by the size and position parameters which are
supplied to it. Notice that I used some conditional compiler
directives here. This is because the size of the title bar buttons in
Windows 95 and Windows 3.1 are different, so they have to be
sized differently.
And since I wanted to be able to compile this in either version of
Windows, I used a test for the predefined symbol, WIN32, to
see what version of Windows the program is compiled under.
However, since the Windows NT UI is the same as Windows 3.1,
it's necessary to grab further version information under
the Win32 conditional to see if the Windows version is Windows NT.
If it is, then we define the TRect to be just like the Windows 3.1
TRect.
To perform Step 3, we make a call to the Buttons unit's
DrawButtonFace to draw button features within the TRect that we
defined. As added treat, I included code to draw a bitmap in the
button. Again, you'll see that I used a conditional compiler directive
to draw the bitmap under different versions of Windows.
I did this purely for personal reasons because the bitmap that I used
was 16 X 16 pixels in dimension, which might be too big for Win95
buttons. So I used StretchDraw under Win32 to stretch the bitmap to
the size of the button.
Trapping the Drawing and Painting Events
You have to make sure that the button will stay visible every time the
form repaints itself. Painting occurs in response to activation and
resizing, which fire off paint and text setting messages that will
redraw the form. If you don't have a facility to redraw your button,
you'll lose it every time a repaint occurs.
So what we have to do is write event handlers which will perform their
default actions, but also redraw our button when they fire off. The
following four procedures handle the paint triggering and painting
events:
{Paint triggering events}
procedure TForm1.WMNCActivate(var Msg : TWMNCActivate);
begin
Inherited;
DrawTitleButton;
end;
procedure TForm1.FormResize(Sender: TObject);
begin
Perform(WM_NCACTIVATE, Word(Active), 0);
end;
{Painting events}
procedure TForm1.WMNCPaint(var Msg : TWMNCPaint);
begin
Inherited;
DrawTitleButton;
end;
procedure TForm1.WMSetText(var Msg : TWMSetText);
begin
Inherited;
DrawTitleButton;
end;
Every time one of these events fires off, it makes a call to the
DrawTitleButton procedure. This will ensure that our button is
always visible on the title bar. Notice that we use the default handler
OnResize on the form to force it to perform a
WM_NCACTIVATE.
Handling Mouse Clicks
Now that we've got code that draws our button and ensures that it's
always visible, we have to handle mouse-clicks on the button. The
way we do this is with two procedures. The first procedure tests to
see if the mouse-click was in the area of our button, then the second
procedure actually performs the code execution associated with our
button. Let's look at the code below:
{Mouse-related procedures}
procedure TForm1.WMNCHitTest(var Msg : TWMNCHitTest);
begin
Inherited;
{Check to see if the mouse was clicked in the area of the button}
with Msg do
if PtInRect(TitleButton, Point(XPos - Left, YPos - Top)) then
Result := htTitleBtn;
end;
procedure TForm1.WMNCLButtonDown(var Msg : TWMNCLButtonDown);
begin
inherited;
if (Msg.HitTest = htTitleBtn) then
ShowMessage('You pressed the new button');
end;
The first procedure WMNCHitTest(var Msg :
TWMNCHitTest) is a hit tester message to determine where the
mouse was clicked in the non-client area. In this procedure we test if
the point defined by the message was within the bounds of our
TRect by using the PtInRect function. If the mouse click was
performed in the TRect, then the result of our message is set to
htTitleBtn, which is a constant that was declared as htSizeLast +
1. htSizeLast is a hit test constant generated by hit test events to test
where the last hit occurred.
The second procedure is a custom handler for a left mouse-click on
a button in the non-client area. Here we test if the hit test result was
equal to htTitleBtn. If it is, we show a message. This was purely
for simplicity's sake, but you can make any call you choose to at this
point.
Putting it All Together
Let's look at the entire code in the form to see how it all works together:
unit Capbtn;
interface
uses
SysUtils, WinTypes, WinProcs, Messages, Classes, Graphics, Controls,
Forms, Dialogs, Buttons;
type
TTitleBtnForm = class(TForm)
procedure FormResize(Sender: TObject);
private
TitleButton : TRect;
procedure DrawTitleButton;
{Paint-related messages}
procedure WMSetText(var Msg : TWMSetText); message WM_SETTEXT;
procedure WMNCPaint(var Msg : TWMNCPaint); message WM_NCPAINT;
procedure WMNCActivate(var Msg : TWMNCActivate); message WM_NCACTIVATE;
{Mouse down-related messages}
procedure WMNCHitTest(var Msg : TWMNCHitTest); message WM_NCHITTEST;
procedure WMNCLButtonDown(var Msg : TWMNCLButtonDown);
message WM_NCLBUTTONDOWN;
function GetVerInfo : DWORD;
end;
var
TitleBtnForm: TTitleBtnForm;
const
htTitleBtn = htSizeLast + 1;
implementation
{$R *.DFM}
procedure TTitleBtnForm.DrawTitleButton;
var
bmap : TBitmap; {Bitmap to be drawn - 16 X 16 : 16 Colors}
XFrame, {X and Y size of Sizeable area of Frame}
YFrame,
XTtlBit, {X and Y size of Bitmaps in caption}
YTtlBit : Integer;
begin
{Get size of form frame and bitmaps in title bar}
XFrame := GetSystemMetrics(SM_CXFRAME);
YFrame := GetSystemMetrics(SM_CYFRAME);
XTtlBit := GetSystemMetrics(SM_CXSIZE);
YTtlBit := GetSystemMetrics(SM_CYSIZE);
{$IFNDEF WIN32}
TitleButton := Bounds(Width - (3 * XTtlBit) - ((XTtlBit div 2) - 2),
YFrame - 1,
XTtlBit + 2,
YTtlBit + 2);
{$ELSE} {Delphi 2.0 positioning}
if (GetVerInfo = VER_PLATFORM_WIN32_NT) then
TitleButton := Bounds(Width - (3 * XTtlBit) - ((XTtlBit div 2) - 2),
YFrame - 1,
XTtlBit + 2,
YTtlBit + 2)
else
TitleButton := Bounds(Width - XFrame - 4*XTtlBit + 2,
XFrame + 2,
XTtlBit + 2,
YTtlBit + 2);
{$ENDIF}
Canvas.Handle := GetWindowDC(Self.Handle); {Get Device context for drawing}
try
{Draw a button face on the TRect}
DrawButtonFace(Canvas, TitleButton, 1, bsAutoDetect, False, False, False);
bmap := TBitmap.Create;
bmap.LoadFromFile('help.bmp');
with TitleButton do
{$IFNDEF WIN32}
Canvas.Draw(Left + 2, Top + 2, bmap);
{$ELSE}
if (GetVerInfo = VER_PLATFORM_WIN32_NT) then
Canvas.Draw(Left + 2, Top + 2, bmap)
else
Canvas.StretchDraw(TitleButton, bmap);
{$ENDIF}
finally
ReleaseDC(Self.Handle, Canvas.Handle);
bmap.Free;
Canvas.Handle := 0;
end;
end;
{Paint triggering events}
procedure TTitleBtnForm.WMNCActivate(var Msg : TWMNCActivate);
begin
Inherited;
DrawTitleButton;
end;
procedure TTitleBtnForm.FormResize(Sender: TObject);
begin
Perform(WM_NCACTIVATE, Word(Active), 0);
end;
{Painting events}
procedure TTitleBtnForm.WMNCPaint(var Msg : TWMNCPaint);
begin
Inherited;
DrawTitleButton;
end;
procedure TTitleBtnForm.WMSetText(var Msg : TWMSetText);
begin
Inherited;
DrawTitleButton;
end;
{Mouse-related procedures}
procedure TTitleBtnForm.WMNCHitTest(var Msg : TWMNCHitTest);
begin
Inherited;
{Check to see if the mouse was clicked in the area of the button}
with Msg do
if PtInRect(TitleButton, Point(XPos - Left, YPos - Top)) then
Result := htTitleBtn;
end;
procedure TTitleBtnForm.WMNCLButtonDown(var Msg : TWMNCLButtonDown);
begin
inherited;
if (Msg.HitTest = htTitleBtn) then
ShowMessage('You pressed the new button');
end;
function TTitleBtnForm.GetVerInfo : DWORD;
var
verInfo : TOSVERSIONINFO;
begin
verInfo.dwOSVersionInfoSize := SizeOf(TOSVersionInfo);
if GetVersionEx(verInfo) then
Result := verInfo.dwPlatformID;
{Returns:
VER_PLATFORM_WIN32s Win32s on Windows 3.1
VER_PLATFORM_WIN32_WINDOWS Win32 on Windows 95
VER_PLATFORM_WIN32_NT Windows NT }
end;
end. |
|
Some Suggestions on Exploring What We've Discussed
You might want to play around with this code a bit to customize it to your own
needs. For instance, if you want to add a bigger button, add pixels to the
XTtlBit var. You might also want to mess around with creating a floating
toolbar that is purely on the title bar. Also, now that you have a means of
interrogating what's going on in the non-client area of the form, you might
want to play around with the default actions taken with the other buttons
like the System Menu button to perhaps display your own custom menu.
Take heed though, playing around with Windows messages can be dangerous. Save
your work constantly, and be prepared for some system crashes while you mess
around with them. In any case, though, have fun!
I got it at
www.undu.com
Regards,
Eduardo Tavares
www.tavareswebsite.cjb.net