您的位置:首页 > 其它

透明皮肤控件设计系列(二):皮肤窗口初级篇

2013-08-12 13:23 369 查看
Windows将窗口分为客户区和非客户区,例如对于标准的Windows窗口,标题栏和边框都属于非客户区,又称为NC区。对于客户区的绘制,应用程序会收到WM_PAINT消息,而非客户区,对应的消息是WM_NCPAINT。要实现皮肤窗口,需要三个步骤:

第一步:定义非客户区的大小。

要自定义非客户区的大小,程序就要响应WM_NCCALCSIZE消息。假设我们的标题高度为60(像素,下同),边框为10,那么对应的代码应该类似这样:

const
xTitleHeight: Integer = 50; // 标题栏的高度
xFramWidth: Integer = 10; // 左、右、下边框的厚度

procedure TForm1.WMNCCALCSIZE(var Message: TWMNCCALCSIZE);
begin
with TWMNCCALCSIZE(Message).CalcSize_Params^.rgrc[0] do
begin
Inc(Left, xFramWidth);
Inc(Top, xTitleHeight);
Dec(Right, xFramWidth);
Dec(Bottom, xFramWidth);
end;
Message.Result := 0;
end;


效果如下:



可以看到,原来的标题栏还在,下面有块白色区域,其实这两者加起来就是新定义的xTitleHeight。

第二步:绘制非客户区。

我们定义一个绘制非客户区的函数:

procedure TForm1.DrawTitle;
var
DC: HDC;
C: TCanvas;
begin
DC := GetWindowDC(Handle);
C := TControlCanvas.Create;
C.Handle := DC;
try
C.Brush.Color := clRed;
C.FillRect(Rect(0, 0, Width, xTitleHeight)); // 标题区域

C.Brush.Color := clBlue;
C.FillRect(Rect(0, xTitleHeight, xFramWidth, Height - xFramWidth)); // 左边框

C.Brush.Color := clGreen;
C.FillRect(Rect(Width – xFramWidth, xTitleHeight, Width,
Height – xFramWidth)); // 右边框

C.Brush.Color := clYellow;
C.FillRect(Rect(0, Height – xFramWidth, Width, Height)); // 下边框
finally
C.Handle := 0;
C.Free;
ReleaseDC(Handle, DC);
end;
end;


然后在系统触发WM_NCPAINT消息时调用它:

procedure TForm1.WMNCPaint(var Message: TWMNCPaint);
begin
DrawTitle;
end;


现在程序运行后效果如下:



但是如果你切换到其它窗口,再切换回来,发现窗口变成了这样:



原来我们还要拦截WM_NCACTIVATE消息:

procedure TForm1.WMNCPaint(var Message: TWMNCPaint);
begin
DrawTitle;
Message.Result := 1;
end;


另外,当我们在Taskbar里点我们的窗体时,会激发active消息,在这个消息中,默认是会画非客户区的,所以也处理:

procedure TForm1.WMActivate(var Message: TWMActivate);
begin
inherited;
DrawTitle;
end;


如果我们要画自定义的标题按钮,修改DrawTitle函数即可,这个后面我们再来处理。

第三步:绘制客户区。

绘制客户区,可以重载处理WM_PAINT消息画在客户区画布,也可以重载WM_ERASEBKGND擦除背景时画在背景,也可以两者都重载。

我们定义一个函数DrawClient:

procedure TForm1.DrawClient(DC: HDC);
var
C: TCanvas;
begin
C := TControlCanvas.Create;
C.Handle := DC;
try
C.Brush.Color := clDkGray;
C.FillRect(ClientRect);
finally
C.Handle := 0;
C.Free;
end;
end;


重载处理WM_PAINT消息:

procedure TForm1.WMPaint(var Message: TWMPaint);
var
DC: HDC;
PS: TPaintStruct;
begin
DC := Message.DC;
if DC = 0 then
DC := BeginPaint(Handle, PS);
DrawClient(DC);
if DC = 0 then
EndPaint(Handle, PS);
Message.Result := 1;
end;


重载处理WM_ERASEBKGND消息:

procedure TForm1.WMEraseBkgnd(var Message: TWMEraseBkgnd);
var
DC: HDC;
begin
DC := Message.DC;
if DC <> 0 then DrawClient(DC);
Message.Result := 1;
end;


注意:假如处理了WM_PAINT消息,那么对于窗口上放置的从TGraphicControl继承下来的无句柄控件将无法显示,因为窗口原来的WM_PAINT过程会轮询控件,发现是没有句柄的将会通知其重绘。

程序运行效果如下:



经过上面的学习,我们已经可以制作皮肤窗口了:只要在DrawTitle和DrawClient函数里面将绘制颜色的代码换成绘制图片即可。但是别着急,一个成熟的控件,很多细节是需要处理的。要是说界面编程有技巧,那么就是这些细节的地方。

一:标题的细节处理:

细心的朋友如果把鼠标移动到原来系统有按钮的地方,按下去,然后移动到其它区域再松开,会发现系统本来的按钮又出现了:



这是因为我们现在窗口默认的BorderStyle是bsSizeable,有两种方法解决:

方法1:在窗口创建的时候去掉按钮:

procedure TForm1.FormCreate(Sender: TObject);
begin
BorderIcons:=[];
end;


但是这样一来,双击标题区,窗口就不会自动最大化和恢复了,幸运的是边框还是可以改变大小。要恢复标题栏功能,方法是处理WM_NCLBUTTONDBLCLK消息:

procedure TForm1.WMNCLBUTTONDBLCLK(var Message: TWMNCLButtonDblClk);
var
P: TPoint;
Style: DWORD;
begin
inherited;
P := Point(Message.XCursor – Left, Message.YCursor – Top);
if PtInRect(Rect(0, 0, Width, xTitleHeight), P) then
begin
Style := GetWindowLong(Handle, GWL_STYLE);
if Style and WS_MAXIMIZE > 0 then
SendMessage(Handle, WM_SYSCOMMAND, SC_RESTORE, 0)
else
SendMessage(Handle, WM_SYSCOMMAND, SC_MAXIMIZE, 0);
end;
end;


方法2:直接将BorderStyle设置为bsNone:

procedure TForm1.FormCreate(Sender: TObject);
begin
BorderStyle:=bsNone;
end;


设置为bsNone后,标题区和边框功能都消失了,解决方法是处理WM_NCHITTEST消息,同时处理WM_NCLBUTTONDBLCLK:

procedure TForm1.WMNCLBUTTONDBLCLK(var Message: TWMNCLButtonDblClk);
var
P: TPoint;
Style: DWORD;
begin
inherited;
P := Point(Message.XCursor – Left, Message.YCursor – Top);
if PtInRect(Rect(0, 0, Width, xTitleHeight), P) then
begin
Style := GetWindowLong(Handle, GWL_STYLE);
if Style and WS_MAXIMIZE > 0 then
SendMessage(Handle, WM_SYSCOMMAND, SC_RESTORE, 0)
else
SendMessage(Handle, WM_SYSCOMMAND, SC_MAXIMIZE, 0);
end;
end;

procedure TForm1.WMNCHitTest(var Message: TWMNCHITTEST);
const
w = 5;
var
P: TPoint;
begin
(*
P.X := Message.XPos;
P.Y := Message.YPos + GetTitleHeight;
Windows.ScreenToClient(Self.Handle, P);
*)
P := Point(Message.XPos – Left, Message.YPos – Top);
if PtInRect(Rect(0, 0, w, w), P) then
Message.Result := HTTOPLEFT // 左上角
else if PtInRect(Rect(w, 0, Width – w, w), P) then
Message.Result := HTTOP // 上边
else if PtInRect(Rect(Width – w, 0, Width, w), P) then
Message.Result := HTTOPRIGHT // 右上角
else if PtInRect(Rect(Width – w, w, Width, Height – w), P) then
Message.Result := HTRIGHT // 右边
else if PtInRect(Rect(Width – w, Height – w, Width, Height), P) then
Message.Result := HTBOTTOMRIGHT // 右下角
else if PtInRect(Rect(w, Height – w, Width – w, Height), P) then
Message.Result := HTBOTTOM // 下边
else if PtInRect(Rect(0, Height – w, w, Height), P) then
Message.Result := HTBOTTOMLEFT // 左下角
else if PtInRect(Rect(0, w, w, Height – w), P) then
Message.Result := HTLEFT // 左边
else if PtInRect(Rect(0, 0, Width, xTitleHeight), P) then
Message.Result := HTCAPTION // 标题栏
else
inherited;
end;


本文使用的是方法2。

待续。。。。。。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: