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

12条评论 2013-08-05 admin

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;

效果如下:

nc1

 

可以看到,原来的标题栏还在,下面有块白色区域,其实这两者加起来就是新定义的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;

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

nc2

 

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

nc3

 

原来我们还要拦截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过程会轮询控件,发现是没有句柄的将会通知其重绘。

程序运行效果如下:

nc4

 

 

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

一:标题的细节处理:

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

nc5

 

这是因为我们现在窗口默认的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。

待续。。。。。。

 

 

分类:界面设计

12条评论 发表评论

发表评论

(必填)

(必填), (Hidden)

XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

TrackBack URL  |  这篇文章上的评论的RSS feed


近期文章

近期评论

文章归档

分类目录