透明皮肤控件设计系列(三):皮肤窗口进阶篇

12条评论 2013-08-07 admin

前文的窗口如果最大化,你会发现它把任务栏也覆盖了,原因是我们窗口的 BorderStyle 设置成了 bsNone,所以要处理一下WM_GETMINMAXINFO消息:

procedure TForm1.WMGETMINMAXINFO(var Message: TMessage);
var
Rect: TRect;
begin
SystemParametersInfo(SPI_GETWORKAREA, 0, @Rect, 0);
with PMINMAXINFO(Message.LParam)^ do
begin
//ptReserved: TPoint;//保留不用

ptMaxSize.X := Rect.Right; //最大范围
ptMaxSize.Y := Rect.Bottom;

ptMaxPosition.X := 0; //最大的放置点
ptMaxPosition.Y := 0;

ptMinTrackSize.X := 200; //最小拖动范围
ptMinTrackSize.Y := xTitleHeight;

//ptMaxTrackSize: TPoint;//最大拖动范围
end;
end;

同样因为bsNone的缘故,窗口最大化的时候还可以改变窗口大小,所以应该修改WMNCHitTest函数,加上状态判读:

……

if PtInRect(Rect(0, 0, xHitTestWidth, xHitTestWidth), P) and (WindowState <> wsMaximized) then Message.Result := HTTOPLEFT //左上角
else if PtInRect(Rect(xHitTestWidth, 0, Width – xHitTestWidth, xHitTestWidth), P) and (WindowState <> wsMaximized) then Message.Result := HTTOP //上边
else if PtInRect(Rect(Width – xHitTestWidth, 0, xHitTestWidth, xHitTestWidth), P) and (WindowState <> wsMaximized) then Message.Result := HTTOPRIGHT //右上角
else if PtInRect(Rect(Width – xHitTestWidth, xHitTestWidth, Width, Height – xHitTestWidth), P) and (WindowState <> wsMaximized) then Message.Result := HTRIGHT //右边
else if PtInRect(Rect(Width – xHitTestWidth, Height – xHitTestWidth, Width, Height), P)and (WindowState <> wsMaximized) then Message.Result := HTBOTTOMRIGHT //右下角
else if PtInRect(Rect(xHitTestWidth, Height – xHitTestWidth, Width – xHitTestWidth, Height), P)and (WindowState <> wsMaximized) then Message.Result := HTBOTTOM //下边
else if PtInRect(Rect(0, Height – xHitTestWidth, xHitTestWidth, Height), P)and (WindowState <> wsMaximized) then Message.Result := HTBOTTOMLEFT //左下角
else if PtInRect(Rect(0, xHitTestWidth, xHitTestWidth, Height – xHitTestWidth), P)and (WindowState <> wsMaximized) then Message.Result := HTLEFT //左边
else if PtInRect(Rect(0, 0, Width, xTitleHeight), P) then Message.Result := HTCAPTION //标题栏
else inherited;

……

下面开始将绘制颜色改成图片。

图片的绘制方法有很多,比如说:按照原始尺寸显示;平铺显示;拉伸显示。如果图片小于窗口尺寸,就不能按照原始尺寸显示了,因为空白的地方会很难看;如果用平铺,那么图像会严重比例失真,我们这里使用一种暂称为过度颜色处理法。

此方法的原理是,将图片等分成四个区域,假如要将图片往右下角扩展,那么除了左上角第一个区域保留不变,另外三个区域使用图片的平均颜色过渡处理,图片画到窗口后,非图片区全部用平均颜色填充,这样一来图片就比较整体平滑。

图1:左边是原图,右边只贴左上角:

nc6

图2:将剩余三个区域用平均颜色过渡处理:

nc7

图3:剩余区域全部用平均颜色贴上,最后效果图:

nc8

 

下面的代码是网上一个朋友写的,原来的语言是C,我将其转换成了Delphi的,另外对取平均颜色的函数进行了优化,原来的是基于像素颜色点循环取的,改用ScanLine后,速度快了50倍:

 

========================================================

 

unit u_BmpUnit;

interface
uses
Windows,SysUtils, Classes,Graphics;
procedure MakeBmp(BmpIn: Graphics.TBitmap; var AverageColor: TColorRef);
implementation

procedure FillSolidRect(m_hDC: HDC; lpRect: PRect; clr: COLORREF); overload;
begin
Windows.SetBkColor(m_hDC, clr);
Windows.ExtTextOut(m_hDC, 0, 0, ETO_OPAQUE, lpRect, nil, 0, nil);
end;

procedure FillSolidRect(m_hDC: HDC; x, y, cx, cy: Integer; clr: COLORREF); overload;
var
r: TRect;
begin
Windows.SetBkColor(m_hDC, clr);
r := Rect(x, y, x + cx, y + cy);
Windows.ExtTextOut(m_hDC, 0, 0, ETO_OPAQUE, @r, nil, 0, nil);
end;

const
m_nOverRegio: integer = 100; //过度的大小

procedure DrawBKImageCross(dc, dcTemp: HDC; nWidth, nHeight: integer; clrCustomBK: TColorRef);
var
blend: TBlendFunction;
nStartX, nStartY: integer;
i, j: integer;
dRadiusTemp2: Double;
begin

FillChar(blend, sizeof(blend), 0);
blend.BlendOp := AC_SRC_OVER;
blend.SourceConstantAlpha := 255;

nStartX := nWidth – m_nOverRegio;
nStartY := nHeight – m_nOverRegio;

FillSolidRect(dc, nStartX, nStartY, m_nOverRegio, m_nOverRegio, clrCustomBK);
for i := 0 to m_nOverRegio – 1 do
begin
for j := 0 to m_nOverRegio – 1 do
begin
dRadiusTemp2 := sqrt((i * i + j * j));
if (dRadiusTemp2 > 99) then
begin
dRadiusTemp2 := 99;
end;
blend.SourceConstantAlpha := 255 – Round(2.55 * ((dRadiusTemp2 / m_nOverRegio) * 100));
Windows.AlphaBlend(dc, nStartX + i, nStartY + j, 1, 1, dcTemp, nStartX + i, nStartY + j, 1, 1, blend);
end;
end;
end;

function DrawVerticalTransition(dcDes, dcSrc: hdc; const rc: TRect; nBeginTransparent: integer = 0; nEndTransparent: integer = 100): integer;
var
bIsDownTransition: Boolean;
nTemp: integer;
blend: TBlendFunction;
nStartPosition, nWidth, nHeight, nMinTransition, nMaxTransition: integer;
dTransition: Double;
i: integer;
begin
bIsDownTransition := True;
if (nEndTransparent <= nBeginTransparent) then
begin
bIsDownTransition := FALSE;
nTemp := nBeginTransparent;
nBeginTransparent := nEndTransparent;
nEndTransparent := nTemp;
end;

FillChar(blend, sizeof(blend), 0);
blend.BlendOp := AC_SRC_OVER;
blend.SourceConstantAlpha := 255;

nStartPosition := rc.top;
nWidth := rc.right – rc.left;
nHeight := rc.bottom – rc.top;

nMinTransition := 255 – 255 * nBeginTransparent div 100;
nMaxTransition := 255 * (100 – nEndTransparent) div 100;
dTransition := (nMinTransition – nMaxTransition) / nHeight;
if (bIsDownTransition) then
begin
for i := 0 to nHeight – 1 do
begin
blend.SourceConstantAlpha := nMinTransition – Round(dTransition * i);
Windows.AlphaBlend(dcDes, rc.left, nStartPosition + i, nWidth, 1,
dcSrc, rc.left, nStartPosition + i, nWidth, 1, blend);
end;
end
else
begin
for i := 0 to nHeight – 1 do
begin
blend.SourceConstantAlpha := nMaxTransition + Round(dTransition * i);
Windows.AlphaBlend(dcDes, rc.left, nStartPosition + i, nWidth, 1,
dcSrc, rc.left, nStartPosition + i, nWidth, 1, blend);
end;
end;
Result := blend.SourceConstantAlpha;
end;

procedure BlendBmp(BmpFrom, BmpTo: TBitmap; var Bmp: TBitmap; BlendValue: Byte);
var
I, J: Integer;
P, PFrom, PTo: PByteArray;
begin
BmpFrom.PixelFormat := pf24bit;
BmpTo.PixelFormat := pf24bit;
Bmp.PixelFormat := pf24Bit;
for J := 0 to Bmp.Height – 1 do
begin
P := Bmp.ScanLine[J];
PFrom := BmpFrom.ScanLine[J];
PTo := BmpTo.ScanLine[J];
for I := 0 to Bmp.Width * 3 – 1 do
P[I] := PFrom[I] * (255 – BlendValue) div 255 + PTo[I] * BlendValue div 255;
end;
end;

procedure MakeBmp(BmpIn: Graphics.TBitmap; var AverageColor: TColorRef);
var
BmpOut: Graphics.TBitmap;
x, y: Integer;
P: PRGBTriple;
r, g, b: Integer;
n: integer;
nStartPosition: integer;
i: integer;
blend: TBlendFunction;
rcTemp: TRect;
begin
BmpIn.PixelFormat := pf24bit;
BmpOut:=TBitmap.Create;

//计算平均颜色
r := 0; g := 0; b := 0;
with BmpIn do
begin
for y := 0 to Height – 1 do
begin
P := BmpIn.ScanLine[y];
for x := 0 to Width – 1 do
begin
r := r + P^.rgbtRed;
g := g + P^.rgbtGreen;
b := b + P^.rgbtBlue;
Inc(P); //指向下一个像素
end;
end;
end;
n := BmpIn.Width * BmpIn.Height;
AverageColor := RGB(r div n, g div n, b div n);

BmpOut.Width := BmpIn.Width;
BmpOut.Height := BmpIn.Height;
//左上
nStartPosition := BmpIn.Width – m_nOverRegio;
BitBlt(BmpOut.Canvas.Handle, 0, 0, nStartPosition, BmpIn.Height – m_nOverRegio, BmpIn.Canvas.Handle, 0, 0, SRCCOPY);

//上中
FillSolidRect(BmpOut.Canvas.Handle, nStartPosition, 0, m_nOverRegio, BmpIn.Height – m_nOverRegio, AverageColor);

//下中
nStartPosition := BmpIn.Height – m_nOverRegio;
FillSolidRect(BmpOut.Canvas.Handle, 0, nStartPosition, BmpIn.Width – m_nOverRegio, m_nOverRegio, AverageColor);

//中间
DrawBKImageCross(BmpOut.Canvas.Handle, BmpIn.Canvas.Handle, BmpIn.Width, BmpIn.Height, AverageColor);

FillChar(blend, sizeof(blend), 0);
blend.BlendOp := AC_SRC_OVER;
blend.SourceConstantAlpha := 255; // 透明度

//上中
nStartPosition := BmpIn.Width – m_nOverRegio;
for i := 0 to m_nOverRegio – 1 do
begin
blend.SourceConstantAlpha := 255 – Round(2.55 * i);
Windows.AlphaBlend(BmpOut.Canvas.Handle, nStartPosition + i, 0, 1, BmpIn.Height – m_nOverRegio,
BmpIn.Canvas.Handle, nStartPosition + i, 0, 1, BmpIn.Height – m_nOverRegio, blend);
end;

//下中
rcTemp := Rect(0, BmpIn.Height – m_nOverRegio, BmpIn.Width – m_nOverRegio, BmpIn.Height);
DrawVerticalTransition(BmpOut.Canvas.Handle, BmpIn.Canvas.Handle, rcTemp);
BmpIn.Assign(BmpOut);
BmpOut.Free;
end;

end.

========================================================

后来我写控件的时候,再次重写了此单元,代码精简到现在的1/6,效率也提高了更多,但原理是不变的。

现在我们只需要修改DrawTitle和DrawClient函数即可:

procedure TForm1.FormCreate(Sender: TObject);
begin
BorderStyle := bsNone;
// BorderIcons := [];
m_BackBMP := TBitmap.Create;
m_BackBMP.LoadFromFile(ExtractFilePath(Application.ExeName) + ‘Back.bmp’);
u_BmpUnit.MakeBmp(m_BackBMP, m_BackColor);
end;

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)); //下边框
*)
if Assigned(m_BackBMP) then
begin
C.Brush.Color := m_BackColor;

C.FillRect(Rect(0, 0, Width, xTitleHeight)); //标题区域
BitBlt(DC, 0, 0, Width, xTitleHeight, m_BackBMP.Canvas.Handle, 0, 0, SRCCOPY);

C.FillRect(Rect(0, xTitleHeight, xFramWidth, Height – xFramWidth)); //左边框
BitBlt(DC, 0, xTitleHeight, xFramWidth, Height – xFramWidth, m_BackBMP.Canvas.Handle, 0, xTitleHeight, SRCCOPY);

C.FillRect(Rect(Width – xFramWidth, xTitleHeight, Width, Height – xFramWidth)); //右边框
BitBlt(DC, Width – xFramWidth, xTitleHeight, Width, Height – xFramWidth, m_BackBMP.Canvas.Handle, Width – xFramWidth, xTitleHeight, SRCCOPY);

C.FillRect(Rect(0, Height – xFramWidth, Width, Height)); //下边框
BitBlt(DC, 0, Height – xFramWidth, Width, Height, m_BackBMP.Canvas.Handle, 0, Height – xFramWidth, SRCCOPY);
end;

finally

C.Handle := 0;
C.Free;
ReleaseDC(Handle, DC);
end;
end;

 

procedure TForm1.DrawClient(DC: HDC);
var
C: TCanvas;
begin
C := TControlCanvas.Create;
C.Handle := DC;
try
(*
C.Brush.Color := clDkGray;
C.FillRect(ClientRect);
*)
if Assigned(m_BackBMP) then
begin
C.Brush.Color := m_BackColor;
C.FillRect(ClientRect);
BitBlt(C.Handle, 0, 0, ClientWidth, ClientHeight, m_BackBMP.Canvas.Handle, xFramWidth, xTitleHeight, SRCCOPY);
end;
finally
C.Handle := 0;
C.Free;
end;
end;

程序运行效果图:

nc9

 

最后我们画按钮。

按钮我们使用PNG格式的图片,所以程序必须添加pngimage单元。按钮一共有三个,其中最大化按钮在窗口最大化的时候,显示的是恢复按钮,所以是四类图片,又因为每个按钮都有三种状态:普通、热点(鼠标移动到上面时触发)、按下,所以实际上一共是12张PNG。

为了更新按钮的状态,我们需要处理以下消息:

1、WM_NCMOUSEMOVE:判读鼠标是否位于三个按钮上面,如果是,则重画标题,同时启动一个定时器判断鼠标是否已经离开按钮。

2、WM_NCLBUTTONDOWN、WM_NCLBUTTONUP、WM_LBUTTONUP:判断用户是否点击/松开了三个按钮的其中之一。

画按钮是在DrawTitle函数。前面我们都是直接操作画板DC,但是因为这次我们要画的内容比较多,所以先创建一个临时的BMP对象,把内容画到上面,最后才贴到DC,这样可以避免窗口闪烁,也是所谓的双缓冲区:

procedure TForm1.DrawTitle;
var
TitleBmp: TBitmap;
DC: HDC;
C: TCanvas;
var
R: TRect;
Style: DWORD;
begin
TitleBmp := TBitmap.Create;
TitleBmp.Width := Width;
TitleBmp.Height := xTitleHeight;

TitleBmp.Canvas.Brush.Color := m_BackColor;
TitleBmp.Canvas.FillRect(Rect(0, 0, Width, xTitleHeight));//先用平均颜色填充整个标题区

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)); //下边框
*)
if Assigned(m_BackBMP) then
begin
C.Brush.Color := m_BackColor;

BitBlt(TitleBmp.Canvas.Handle, 0, 0, Width, xTitleHeight, m_BackBMP.Canvas.Handle, 0, 0, SRCCOPY);
DrawIconEx(TitleBmp.Canvas.Handle, 6, 6, Application.Icon.Handle, 16, 16, 0, 0, DI_NORMAL);
TitleBmp.Canvas.Font.Assign(Font);
TitleBmp.Canvas.Brush.Style := bsClear;
ExtTextOut(TitleBmp.Canvas.Handle, 26, 6, TitleBmp.Canvas.TextFlags, nil, PChar(Caption), Length(Caption), nil);
R := GetRectMiniButton;
if m_MiniButtonDown then
begin
TitleBmp.Canvas.Draw(R.Left, R.Top, btn_min_down)
end
else if m_MiniButtonHover then
begin
TitleBmp.Canvas.Draw(R.Left, R.Top – 1, btn_min_highlight);
end
else
TitleBmp.Canvas.Draw(R.Left, R.Top, btn_min_normal);

R := GetRectMaxButton;

Style := GetWindowLong(Handle, GWL_STYLE);
if Style and WS_MAXIMIZE > 0 then
begin
if m_MaxButtonDown then
begin
TitleBmp.Canvas.Draw(R.Left, R.Top, btn_Restore_down)
end
else if m_MaxButtonHover then
begin
TitleBmp.Canvas.Draw(R.Left, R.Top – 1, btn_Restore_highlight);
end
else
TitleBmp.Canvas.Draw(R.Left, R.Top, btn_Restore_normal);
end
else
begin
if m_MaxButtonDown then
begin
TitleBmp.Canvas.Draw(R.Left, R.Top, btn_max_down)
end
else if m_MaxButtonHover then
begin
TitleBmp.Canvas.Draw(R.Left, R.Top – 1, btn_max_highlight);
end
else
TitleBmp.Canvas.Draw(R.Left, R.Top, btn_max_normal);
end;
R := GetRectCloseButton;
if m_CloseButtonDown then
begin
TitleBmp.Canvas.Draw(R.Left, R.Top, btn_close_down);
end
else if m_CloseButtonHover then
begin
TitleBmp.Canvas.Draw(R.Left, R.Top – 1, btn_close_highlight);
end
else
TitleBmp.Canvas.Draw(R.Left, R.Top, btn_close_normal);
//C.FillRect(Rect(0, 0, Width, xTitleHeight)); //标题区域
BitBlt(DC, 0, 0, Width, xTitleHeight, TitleBmp.Canvas.Handle, 0, 0, SRCCOPY);

C.FillRect(Rect(0, xTitleHeight, xFramWidth, Height – xFramWidth)); //左边框
BitBlt(DC, 0, xTitleHeight, xFramWidth, Height – xFramWidth, m_BackBMP.Canvas.Handle, 0, xTitleHeight, SRCCOPY);

C.FillRect(Rect(Width – xFramWidth, xTitleHeight, Width, Height – xFramWidth)); //右边框
BitBlt(DC, Width – xFramWidth, xTitleHeight, Width, Height – xFramWidth, m_BackBMP.Canvas.Handle, Width – xFramWidth, xTitleHeight, SRCCOPY);

C.FillRect(Rect(0, Height – xFramWidth, Width, Height)); //下边框
BitBlt(DC, 0, Height – xFramWidth, Width, Height, m_BackBMP.Canvas.Handle, 0, Height – xFramWidth, SRCCOPY);
end;

finally
C.Handle := 0;
C.Free;
ReleaseDC(Handle, DC);
TitleBmp.Free;
end;
end;

另外,默认程序是正方形的,我们可以修改为圆角窗口:

procedure TForm1.WMSize(var Message: TWMSize);
var
Rgn: HRGN;
begin
inherited;
DrawTitle;
Rgn := CreateRoundRectRgn(0, 0, Width, Height, 5, 5);
SetWindowRgn(Handle, Rgn, True);
DeleteObject(Rgn);
end;

程序运行效果图如下:

nc10

 

 

本文源代码点击这里下载。

分类:界面设计

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


近期文章

近期评论

文章归档

分类目录