前文的窗口如果最大化,你会发现它把任务栏也覆盖了,原因是我们窗口的 BorderStyle 设置成了 bsNone,所以要处理一下WM_GETMINMAXINFO消息:
下面开始将绘制颜色改成图片。
图片的绘制方法有很多,比如说:按照原始尺寸显示;平铺显示;拉伸显示。如果图片小于窗口尺寸,就不能按照原始尺寸显示了,因为空白的地方会很难看;如果用平铺,那么图像会严重比例失真,我们这里使用一种暂称为过度颜色处理法。
此方法的原理是,将图片等分成四个区域,假如要将图片往右下角扩展,那么除了左上角第一个区域保留不变,另外三个区域使用图片的平均颜色过渡处理,图片画到窗口后,非图片区全部用平均颜色填充,这样一来图片就比较整体平滑。
图1:左边是原图,右边只贴左上角:
图2:将剩余三个区域用平均颜色过渡处理:
图3:剩余区域全部用平均颜色贴上,最后效果图:
下面的代码是网上一个朋友写的,原来的语言是C,我将其转换成了Delphi的,另外对取平均颜色的函数进行了优化,原来的是基于像素颜色点循环取的,改用ScanLine后,速度快了50倍:
========================================================
========================================================
后来我写控件的时候,再次重写了此单元,代码精简到现在的1/6,效率也提高了更多,但原理是不变的。
现在我们只需要修改DrawTitle和DrawClient函数即可:
程序运行效果图:
最后我们画按钮。
按钮我们使用PNG格式的图片,所以程序必须添加pngimage单元。按钮一共有三个,其中最大化按钮在窗口最大化的时候,显示的是恢复按钮,所以是四类图片,又因为每个按钮都有三种状态:普通、热点(鼠标移动到上面时触发)、按下,所以实际上一共是12张PNG。
为了更新按钮的状态,我们需要处理以下消息:
1、WM_NCMOUSEMOVE:判读鼠标是否位于三个按钮上面,如果是,则重画标题,同时启动一个定时器判断鼠标是否已经离开按钮。
2、WM_NCLBUTTONDOWN、WM_NCLBUTTONUP、WM_LBUTTONUP:判断用户是否点击/松开了三个按钮的其中之一。
画按钮是在DrawTitle函数。前面我们都是直接操作画板DC,但是因为这次我们要画的内容比较多,所以先创建一个临时的BMP对象,把内容画到上面,最后才贴到DC,这样可以避免窗口闪烁,也是所谓的双缓冲区:
程序运行效果图如下:
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:左边是原图,右边只贴左上角:
图2:将剩余三个区域用平均颜色过渡处理:
图3:剩余区域全部用平均颜色贴上,最后效果图:
下面的代码是网上一个朋友写的,原来的语言是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;
程序运行效果图:
最后我们画按钮。
按钮我们使用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;
程序运行效果图如下: