本例效果图:
WM_NCHITTEST 消息返回后, 消息的 Result 参数表示了鼠标所在窗口的部位.
窗体设计步骤:
新建工程后, 随便添加一个菜单; 设置窗体的 AutoScroll 属性为 True, 并添加一个
Panel 放在合适的位置, 以让窗口出现滚动条.
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, ExtCtrls, StdCtrls, Menus;
type
TForm1 = class(TForm)
Panel1: TPanel;
MainMenu1: TMainMenu;
mnuFile: TMenuItem;
private
procedure MyMsg(var msg: TWMNCHitTest); message WM_NCHITTEST;
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
const
arr: array[-2..21] of string = (
'HTERROR',
'HTTRANSPARENT',
'HTNOWHERE',
'HTCLIENT - 客户区',
'HTCAPTION - 标题',
'HTSYSMENU - 系统菜单',
'HTGROWBOX',
'HTMENU - 菜单',
'HTHSCROLL - 水平滚动条',
'HTVSCROLL - 垂直滚动条',
'HTMINBUTTON - 最小化按钮',
'HTMAXBUTTON - 最大化按钮',
'HTLEFT - 左边界',
'HTRIG - 右边界',
'HTTOP - 上边界',
'HTTOPLEFT - 左上角',
'HTTOPRIG - 右上角',
'HTBOTTOM - 下边界',
'HTBOTTOMLEFT - 左下角',
'HTBOTTOMRIG - 右下角',
'HTBORDER',
'HTOBJECT',
'HTCLOSE - 关闭按钮',
'HTHELP');
procedure TForm1.MyMsg(var msg: TWMNCHitTest);
var
i: Integer;
begin
Inherited;
i := msg.Result;
Text := Format('%d: %s', [i, arr[i]]);
end;
end.
//另附 WM_NCHITTEST 消息中 Result 参数的所有可能值的列表:
HTERROR = -2;
HTTRANSPARENT = -1;
HTNOWHERE = 0;
HTCLIENT = 1;
HTCAPTION = 2;
HTSYSMENU = 3;
HTGROWBOX = 4;
HTSIZE = HTGROWBOX;
HTMENU = 5;
HTHSCROLL = 6;
HTVSCROLL = 7;
HTMINBUTTON = 8;
HTMAXBUTTON = 9;
HTLEFT = 10;
HTRIGHT = 11;
HTTOP = 12;
HTTOPLEFT = 13;
HTTOPRIGHT = 14;
HTBOTTOM = 15;
HTBOTTOMLEFT = $10;
HTBOTTOMRIGHT = 17;
HTBORDER = 18;
HTREDUCE = HTMINBUTTON;
HTZOOM = HTMAXBUTTON;
HTSIZEFIRST = HTLEFT;
HTSIZELAST = HTBOTTOMRIGHT;
HTOBJECT = 19;
HTCLOSE = 20;
HTHELP = 21;
通常,我们拖动对话框窗口的标题 栏来移动窗口,但有时候,我们想通过鼠标在客户区上拖动来移动窗口。
一个容易想到的方案是,处理鼠标 消息WM_LBUTTONDOWN和WM_LBUTTONUP。 在OnLButtonUp函数中计算鼠标位置的变化,调用MoveWindow实 现窗口的移动。
注意,拖动标题栏移动窗口的时 候,会出现一个矩形框,它提示了窗口移动的当前位置。当鼠标左键放开的时候,窗口就移动到矩形框所在位置。而我们的实现方案中没有这个功能。
要实现此功能,我们必须自己来画 这些矩形。
事实上,我们没有必要自己来做这 件事情,因为Windows已经给我们做好了。
试想,如果我能够欺骗Windows, 告诉它现在鼠标正在拖动的是标题栏而不是客户区,那么窗口移动操作就由Windows来代劳了。
要欺骗Windows并 不像想像中的困难,甚至非常简单。
我们利用一个消息:WM_NCHITTEST。
MSDN对 它的解释是:
The WM_NCHITTEST message is sent to a window when the cursor moves, or when a mouse button is pressed or released. If the mouse is not captured, the message is sent to the window beneath the cursor. Otherwise, the message is sent to the window that has captured the mouse.
这个消息是当鼠标移动或者有鼠标 键按下时候发出的。
Windows用 这个消息来做什么? “HITTEST”就是“命 中测试”的意思,WM_NCHITTEST消 息用来获取鼠标当前命中的位置。
WM_NCHITTEST的 消息响应函数会根据鼠标当前的坐标来判断鼠标命中了窗口的哪个部位,消息响应函数的返回值指出了部位,例如它可能会返回HTCAPTION, 或者HTCLIENT等。(其返回值有很多,请查阅MSDN)。
为了便于理解,我先描述一下Windows对 鼠标键按下的响应流程:
1. 确定鼠标键 点击的是哪个窗口。Windows会用表记录当前屏幕上各个窗口的区域坐标,当鼠标驱动程序通知Windows鼠 标键按下了,Windows根据鼠标的坐标确定它点击的是哪个窗口。
2. 确定鼠标键 点击的是窗口的哪个部位。Windows会向鼠标键点击的窗口发送WM_NCHITTEST消 息,来询问鼠标键点击的是窗口的哪个部位。(WM_NCHITTEST的消息响应函数的返回值会通 知Windows)。通常来说,WM_NCHITTEST消 息是系统来处理的,用户一般不会主动去处理它(也就是说,WM_NCHITTEST的消息响应函数 通常采用的是Windows默认的处理函数)。
3. 根据鼠标键 点击的部位给窗口发送相应的消息。例如:如果WM_NCHITTEST的消息响应函数的返回值是HTCLIENT, 表示鼠标点击的是客户区,则Windows会向窗口发送WM_LBUTTONDOWN消 息;如果WM_NCHITTEST的消息响应函数的返回值不是HTCLIENT(可 能是HTCAPTION、HTCLOSE、HTMAXBUTTON等), 即鼠标点击的是非客户区,Windows就会向窗口发送WM_NCLBUTTONDOWN消 息。
我们有必要详细讨论一下:如果WM_NCHITTEST的 消息响应函数的返回值是HTCAPTION,即指示了鼠标点击了标题栏,接下去Windows的 处理是怎样的?
上面已经提到,接下来,Windows会 向窗口发送WM_NCLBUTTONDOWN消息。
MSDN对WM_NCLBUTTONDOWN消 息描述如下:
WM_NCLBUTTONDOWN
nHittest = (INT) wParam; // hit-test value
pts = MAKEPOINTS(lParam); // position of cursor
WM_NCLBUTTONDOWN的wParam指 示了鼠标点击的窗口部位,lParam指示了当前鼠标的坐标。
如果应用程序没有对该消息响应, 则由系统默认处理。
系统默认处理又是怎样的呢?系统 发现wParam指示了鼠标点击的是标题栏,就会标识当前窗口处于“拖 拽状态”(Windows内部记录了每个窗 口的状态信息)。由于标识了“拖拽状态”, 则从此刻起到鼠标键放开之前,你的鼠标移动状况完全由Windows跟踪。它根据鼠标的移动,使得 窗口作“同步”移动。
注意,这个过程中,窗口不会收到WM_NCMOUSEMOVE消 息,因为窗口和鼠标是“同步”移动的,你的鼠标相对于窗口是静止的。(这些细节你可以自己写个示例来测试并分析得出,事实上我也是这么做的。如果我的观点 有错误,欢迎指正)。
class TForm1 : public TForm
{
__published: // IDE-managed Components
void __fastcall FormResize(TObject *Sender);
private: // User declarations
TRect m_rctCaptionBtn;
bool m_bCaptionMouseDown;
void __fastcall CrnDrawCaptionButton(bool bDown = false);
void __fastcall WMNCPaint(TWMNCPaint &Msg);
void __fastcall WMNCActivate(TWMNCActivate &Msg);
void __fastcall WMSetText(TWMSetText &Msg);
void __fastcall WMNCHitTest(TWMNCHitTest &Msg);
void __fastcall WMNCLButtonDown(TWMNCLButtonDown &Msg);
void __fastcall WMNCLButtonUp(TWMNCLButtonUp &Msg);
BEGIN_MESSAGE_MAP
VCL_MESSAGE_HANDLER(WM_NCPAINT, TWMNCPaint, WMNCPaint)
VCL_MESSAGE_HANDLER(WM_NCACTIVATE, TWMNCActivate, WMNCActivate)
VCL_MESSAGE_HANDLER(WM_SETTEXT, TWMSetText, WMSetText)
VCL_MESSAGE_HANDLER(WM_NCHITTEST, TWMNCHitTest, WMNCHitTest)
VCL_MESSAGE_HANDLER(WM_NCLBUTTONDOWN, TWMNCLButtonDown, WMNCLButtonDown)
VCL_MESSAGE_HANDLER(WM_NCLBUTTONUP, TWMNCLButtonUp, WMNCLButtonUp)
END_MESSAGE_MAP(TForm)
public: // User declarations
__fastcall TForm1(TComponent* Owner);
};
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
// 当改变窗体大小时,重新绘制按钮,否则有错位
OnResize = FormResize;
m_bCaptionMouseDown = false;
}
#include <Buttons.hpp>
const htCaptionBtn = HTSIZELAST + 1;
//---------------------------------------------------------------------------
void __fastcall TForm1::CrnDrawCaptionButton(bool bDown)
{
int xFrame, yFrame, xSize, ySize;
TRect r;
// Dimensions of Sizeable Frame
xFrame = ::GetSystemMetrics(SM_CXFRAME);
yFrame = ::GetSystemMetrics(SM_CYFRAME);
// Dimensions of Caption Buttons
xSize = ::GetSystemMetrics(SM_CXSIZE);
ySize = ::GetSystemMetrics(SM_CYSIZE);
// Define the placement of the new caption button
m_rctCaptionBtn = Bounds(Width - xFrame - 4 * xSize + 2,
yFrame + 2, xSize - 2, ySize - 4);
// Get the handle to canvas using Form's device context
Canvas->Handle = ::GetWindowDC(Handle);
Canvas->Font->Name = "Marlett";
Canvas->Font->Color = clBlue;
Canvas->Font->Style = TFontStyles() << fsBold;
try
{
// 绘制按钮,注意第6个参数指明了是否为按下状态
DrawButtonFace(Canvas, m_rctCaptionBtn, 1, bsAutoDetect, False, bDown, False);
// 定义绘制按钮上文字的区域
r = Bounds(Width - xFrame - 4 * xSize + 3,
yFrame + 3, xSize - 5, ySize - 7);
// 用Marlett的字体画一个小圆点,对应的字符是i,可根据需要更改
::DrawText(Canvas->Handle, "i", 1, &r, DT_CENTER | DT_VCENTER);
}
__finally
{
::ReleaseDC(Handle, Canvas->Handle);
Canvas->Handle = 0;
}
}
//---------------------------------------------------------------------------
void __fastcall TForm1::WMNCPaint(TWMNCPaint &Msg)
{
TForm::Dispatch(&Msg);
CrnDrawCaptionButton();
}
//---------------------------------------------------------------------------
void __fastcall TForm1::WMNCActivate(TWMNCActivate &Msg)
{
TForm::Dispatch(&Msg);
CrnDrawCaptionButton();
}
//---------------------------------------------------------------------------
void __fastcall TForm1::WMSetText(TWMSetText &Msg)
{
TForm::Dispatch(&Msg);
CrnDrawCaptionButton();
}
//---------------------------------------------------------------------------
void __fastcall TForm1::WMNCHitTest(TWMNCHitTest &Msg)
{
TForm::Dispatch(&Msg);
if (PtInRect(m_rctCaptionBtn, TPoint(Msg.XPos - Left, Msg.YPos - Top)))
Msg.Result = htCaptionBtn;
else if (m_bCaptionMouseDown)
{
m_bCaptionMouseDown = false;
CrnDrawCaptionButton();
}
}
//---------------------------------------------------------------------------
void __fastcall TForm1::WMNCLButtonDown(TWMNCLButtonDown &Msg)
{
TForm::Dispatch(&Msg);
// 63 63 72 75 6E 2E 63 6F 6D
if (Msg.HitTest == htCaptionBtn)
{
m_bCaptionMouseDown = true;
CrnDrawCaptionButton(true);
}
}
//---------------------------------------------------------------------------
// 在标题栏按钮的鼠标弹出事件中加入相应的处理代码
void __fastcall TForm1::WMNCLButtonUp(TWMNCLButtonUp &Msg)
{
TForm::Dispatch(&Msg);
if (Msg.HitTest == htCaptionBtn)
{
// 先恢复按钮的弹出状态
CrnDrawCaptionButton();
// 加入自己的处理代码
ShowMessage("哈哈,我就是标题栏新添加的按钮");
}
}
//---------------------------------------------------------------------------
//
void __fastcall TForm1::FormResize(TObject *Sender)
{
Perform(WM_NCACTIVATE, Active, 0);
}