Button
in Caption
|
|
This was found
else where but this place needs it Q & A - How can I put a
button on a form's 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 insight 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 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 Delphi/Pascal guru Neil Rubenking for help. He pointed me
in the direction of his book, Delphi Programming Problem Solver,
which contains an example for doing this exact thing. The code 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. He 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 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 we're interested in here are not the
usual Windows messages handled by plain-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 where most
applications present information. The non-client area consists of
the window's 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 type of message 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 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: We need to
have a function to draw the button. We'll have to trap drawing
and painting events so that our button stays visible when the form
activates, resizes or moves. We're dropping a button on the
title bar, so we have to have a 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 You can't drop VCL objects
onto a non-client area of a window, but you can draw on it and
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:
You must 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. Then you have to
define a TRect structure with the proper size and position within
the title bar. 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 of this is accomplished in a single call. For
this program we make a call to the DrawTitleButtonprocedure, which
is listed below:
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 that 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 which 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 so, 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. You'll see that I used a conditional
compiler directive to draw the bitmap under different versions of
Windows. I did this because the bitmap I used was 16x16 pixels,
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 must 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 and 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:
{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. 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.
Suggestions for
Exploring 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 can also 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 experiment.
Written by Brendan
Delumpa on 3/19/97
|
| |