首页 > 代码库 > 窗体皮肤实现 - 重绘窗体非客户区(三)

窗体皮肤实现 - 重绘窗体非客户区(三)

窗体边框基本的绘制和控制完成,在第二篇中主要遗留的问题。

  • 标题区域图标和按钮没绘制
  • 缩放时客户区显示有问题

 

 解决完下面的问题,皮肤处理基本完整。大致的效果GIF

 GIF中TShape的颜色表现有些问题,实际是正常的。

 

绘制标题区域内容

  1. 获取标题有效区域
  2. 绘制窗体图标
  3. 绘制按钮
  4. 绘制标题

  标题区域主要考虑窗体是否在最大化状态,最大化后实际的标题绘制区域会有变化。可以通过 IsZoomed 或 GetWindowLong(Handle, GWL_STYLE) and WS_MAXIMIZE = WS_MAXIMIZE 的方式获取。

 1 AMaxed := IsZoomed(Handle);    // 获取窗体最大化状态 2  3 function TTest.GetCaptionRect(AMaxed: Boolean): TRect; 4 var 5   rFrame: TRect; 6 begin 7   rFrame := GetFrameSize;         // 窗体上下左右的边框尺寸 8   // 最大化状态简易处理 9   if AMaxed then10     Result := Rect(8, 8, FWidth - 9 , rFrame.Top)11   else12     Result := Rect(rFrame.Left, 3, FWidth - rFrame.right, rFrame.Top);13 end;

 

绘制窗体图标稍微有些麻烦,需要获取窗体的Icon图标。窗体图标并不一定是程序图标。主要过程通过WM_GETICON 这个消息获取图标。

1   TmpHandle := THandle(SendMessage(Handle, WM_GETICON, ICON_SMALL, 0));2   if TmpHandle = 0 then3     TmpHandle := THandle(SendMessage(Handle, WM_GETICON, ICON_BIG, 0));

如果上述方法无法获得,需要通过GetClassName 和 GetClassInfoEx 这2个API获取。

 1 { Get instance } 2 GetClassName(Handle, @Buffer, SizeOf(Buffer)); 3 FillChar(Info, SizeOf(Info), 0); 4 Info.cbSize := SizeOf(Info); 5  6 if GetClassInfoEx(GetWindowLong(Handle, GWL_HINSTANCE), @Buffer, Info) then 7 begin 8   TmpHandle := Info.hIconSm; 9   if TmpHandle = 0 then10     TmpHandle := Info.HICON;11 end

  上述这2种方法还是无法获取。那~~ 就没有办法了。如果非要绘制图标可以使用Application的图标进行代替。

1 Application.Icon.Handle
 1 function TTest.GetIcon: TIcon; 2 var 3   IconX, IconY: integer; 4   TmpHandle: THandle; 5   Info: TWndClassEx; 6   Buffer: array [0 .. 255] of Char; 7 begin 8   /// 9   /// 获取当前form的图标10   /// 这个图标和App的图标是不同的11   ///12   TmpHandle := THandle(SendMessage(Handle, WM_GETICON, ICON_SMALL, 0));13   if TmpHandle = 0 then14     TmpHandle := THandle(SendMessage(Handle, WM_GETICON, ICON_BIG, 0));15 16   if TmpHandle = 0 then17   begin18     { Get instance }19     GetClassName(Handle, @Buffer, SizeOf(Buffer));20     FillChar(Info, SizeOf(Info), 0);21     Info.cbSize := SizeOf(Info);22 23     if GetClassInfoEx(GetWindowLong(Handle, GWL_HINSTANCE), @Buffer, Info) then24     begin25       TmpHandle := Info.hIconSm;26       if TmpHandle = 0 then27         TmpHandle := Info.HICON;28     end29   end;30 31   if FIcon = nil then32     FIcon := TIcon.Create;33 34   if TmpHandle <> 0 then35   begin36     IconX := GetSystemMetrics(SM_CXSMICON);37     if IconX = 0 then38       IconX := GetSystemMetrics(SM_CXSIZE);39     IconY := GetSystemMetrics(SM_CYSMICON);40     if IconY = 0 then41       IconY := GetSystemMetrics(SM_CYSIZE);42     FIcon.Handle := CopyImage(TmpHandle, IMAGE_ICON, IconX, IconY, 0);43     FIconHandle := TmpHandle;44   end;45 46   Result := FIcon;47 end;
完整获取窗体图标的方法

绘制系统最小化、最大化和关闭按钮直接使用贴图的方法。做一张PNG图片,做成资源文件加入到单元中。

注:图标是白色的没底色看不见,所以在贴的图上加了个黑底。

计算好实际位置后,直接把从资源中加载的图标绘制上去。

 1 procedure TTest.DrawButton(DC: HDC; AKind: TFormButtonKind; AState: TSkinIndicator; const R: TRect); 2 var 3   hB: HBRUSH; 4   iColor: Cardinal; 5   rSrcOff: TPoint; 6   x, y: integer; 7 begin 8   /// 绘制背景 9   case AState of10     siHover         : iColor := SKINCOLOR_BTNHOT;11     siPressed       : iColor := SKINCOLOR_BTNPRESSED;12     siSelected      : iColor := SKINCOLOR_BTNPRESSED;13     siHoverSelected : iColor := SKINCOLOR_BTNHOT;14   else                iColor := SKINCOLOR_BAKCGROUND;15   end;16   hB := CreateSolidBrush(iColor);17   FillRect(DC, R, hB);18   DeleteObject(hB);19 20   /// 绘制图标21   rSrcOff := Point(SIZE_RESICON * ord(AKind), 0);22   x := R.Left + (R.Right - R.Left - SIZE_RESICON) div 2;23   y := R.Top + (R.Bottom - R.Top - SIZE_RESICON) div 2;24   DrawTransparentBitmap(FSkinData, rSrcOff.X, rSrcOff.Y, DC, x, y, SIZE_RESICON, SIZE_RESICON);25 end;

 

最后绘制标题,设置背景SetBkMode透明,设置字体颜色SetTextColor为白色。

1 /// 绘制Caption2 sData :=  GetCaption;3 SetBkMode(DC, TRANSPARENT);4 SaveColor := SetTextColor(DC, $00FFFFFF);5 Flag := DT_LEFT or DT_VCENTER or DT_SINGLELINE or DT_NOPREFIX;6 DrawTextEx(DC, PChar(sData), Length(sData), rCaptionRect, Flag, nil);7 SetTextColor(DC, SaveColor);

整个标题区域就绘制完成。

 

 标题区按钮响应鼠标消息

基本的绘制完成,鼠标滑到窗体按钮区域(最大化、最小化和关闭)和点击并不会相应。需要自己处理相应的消息。WM_NCHITTEST 消息是系统用来确定鼠标位置对应的窗体区域,可以通过这个消息实现对窗体按钮的相应。

为实现窗体按钮的响应,只要处理这个区域。其他区域消息还是交由窗体原有消息处理。

相应两种状态: 滑入时的显示样式、按下时的显示样式。

 1 procedure TTest.WMNCHitTest(var Message: TWMNCHitTest); 2 var 3   P: TPoint; 4   iHit: integer; 5 begin 6   // 需要把位置转换到实际窗口位置 7   P := NormalizePoint(Point(Message.XPos, Message.YPos)); 8  9   // 获取 位置10   // 只对监控区域处理,其他由系统处理11   iHit := HitTest(p);12   if FHotHit > HTNOWHERE then13   begin14     Message.Result := iHit;15     Handled := True;            // 处理完成,不再交由系统处理16   end;17   18   // 响应鼠标滑入监控区域后,通知非客户区重绘19   if iHit <> FHotHit then20   begin21     FHotHit := iHit;22     InvalidateNC;23   end;24 end;
 1 function TTest.HitTest(P: TPoint):integer; 2 var 3   bMaxed: Boolean; 4   r: TRect; 5   rCaptionRect: TRect; 6   rFrame: TRect; 7 begin 8   Result := HTNOWHERE; 9 10   ///11   /// 检测位置12   ///13   rFrame := GetFrameSize;14   if p.Y > rFrame.Top then15     Exit;16 17   ///18   ///  只关心窗体按钮区域19   ///20   bMaxed := IsZoomed(Handle);21   rCaptionRect := GetCaptionRect(bMaxed);22   if PtInRect(rCaptionRect, p) then23   begin24     r.Right := rCaptionRect.Right - 1;25     r.Top := 0;26     if bMaxed then27       r.Top := rCaptionRect.Top;28     r.Top := r.Top + (rFrame.Top - r.Top - SIZE_SYSBTN.cy) div 2;29     r.Left := r.Right - SIZE_SYSBTN.cx;30     r.Bottom := r.Top + SIZE_SYSBTN.cy;31 32     ///33     /// 实际绘制的按钮就三个,其他没处理34     ///35     if (P.Y >= r.Top) and (p.Y <= r.Bottom) and (p.X <= r.Right) then36     begin37       if (P.X >= r.Left) then38         Result := HTCLOSE39       else if p.X >= (r.Left - SIZE_SYSBTN.cx) then40         Result := HTMAXBUTTON41       else if p.X >= (r.Left - SIZE_SYSBTN.cx * 2) then42         Result := HTMINBUTTON;43     end;44   end;45 end;
function HitTest(P: TPoint):integer

 

上面代码获取当前鼠标所在位置,这样滑入的Hot状态信息已经获取。还个是记录按下的状态,需要使用WM_NCLBUTTONDOWN消息获得鼠标按下后的位置来实现。

 1 procedure TTest.WMNCLButtonDown(var message: TWMNCHitMessage); 2 var 3   iHit: integer; 4 begin 5   // 对监控的区域作相应 6   iHit := HTNOWHERE; 7   if (Message.HitTest = HTCLOSE) or (Message.HitTest = HTMAXBUTTON) or (Message.HitTest = HTMINBUTTON) or 8     (Message.HitTest = HTHELP) then 9   begin10     iHit := Message.HitTest;11     Message.Result := 0;12     Message.Msg := WM_NULL;13     Handled := True;           // 消息已经处理完成,不再交由系统处理14   end;15 16   // 如果按下的位置发生变化,重绘标题区17   if iHit <> FPressedHit then18   begin19     FPressedHit := iHit;20     InvalidateNC;21   end;22 end;

 

通过上述两个消息,获取到鼠标所在按钮的位置。在绘制标题区函数中直接使用。

 1 // 注意: 2 //   按钮样式枚举的顺序不要颠倒,这个和资源图标的排列顺序是一致的 3 TFormButtonKind = (fbkMin, fbkMax, fbkRestore, fbkClose, fbkHelp); 4  5 procedure TTest.PaintNC(DC: HDC); 6 const 7   HITVALUES: array [TFormButtonKind] of integer = (HTMINBUTTON, HTMAXBUTTON, HTMAXBUTTON, HTCLOSE, HTHELP); 8  9   function GetBtnState(AKind: TFormButtonKind): TSkinIndicator;10   begin11     // 按下区域 一定和 Hot区域一致,保证鼠标点击到弹起的区域是一致,才能执行12     if (FPressedHit = FHotHit) and (FPressedHit = HITVALUES[AKind]) then13       Result := siPressed14     else if FHotHit = HITVALUES[AKind] then15       Result := siHover16     else17       Result := siInactive;18   end;19 20   ... ...21 begin22     ... ...23     // 绘制 关闭按钮24     DrawButton(Dc, fbkClose, GetBtnState(fbkClose), rButton);25 26     ... ...27 end;

 

上述的绘制相应已经完成,但鼠标点击是不会有任何反应的。需要处理WM_NCLBUTTONUP消息

 1 procedure TTest.WMNCLButtonUp(var Message: TWMNCHitMessage); 2 var 3   iWasHit: Integer; 4 begin 5   iWasHit := FPressedHit; 6  7   // 处理监控区域的鼠标弹起消息 8   if iWasHit <> HTNOWHERE then 9   begin10     FPressedHit := HTNOWHERE;11     //InvalidateNC;12 13     if iWasHit = FHotHit then14     begin15       case Message.HitTest of16         HTCLOSE     : SendMessage(Handle, WM_SYSCOMMAND, SC_CLOSE, 0);17         HTMAXBUTTON : Maximize;18         HTMINBUTTON : Minimize;19         HTHELP      : SendMessage(Handle, WM_SYSCOMMAND, SC_CONTEXTHELP, 0);20       end;21 22       Message.Result := 0;23       Message.Msg := WM_NULL;24       Handled := True;           // 消息已经处理完成,不需要控件再处理25     end;26   end;27 end;
 1 procedure TTest.Maximize; 2 begin 3   if Handle <> 0 then 4   begin 5     FPressedHit := 0; 6     FHotHit := 0; 7     if IsZoomed(Handle) then 8       SendMessage(Handle, WM_SYSCOMMAND, SC_RESTORE, 0) 9     else10       SendMessage(Handle, WM_SYSCOMMAND, SC_MAXIMIZE, 0);11   end;12 end;13 14 procedure TTest.Minimize;15 begin16   if Handle <> 0 then17   begin18     FPressedHit := 0;19     FHotHit := 0;20     if IsIconic(Handle) then21       SendMessage(Handle, WM_SYSCOMMAND, SC_RESTORE, 0)22     else23       SendMessage(Handle, WM_SYSCOMMAND, SC_MINIMIZE, 0);24    end;25 end;
fun Maximize Minimize

 

整个标题区的消息基本处理完成,能正常相应标题区应有的功能。还有些细节上面需要处理一下,如修改窗体标题没有及时响应。WM_SETTEXT消息用于处理标题修改。

1 procedure TTest.WMSetText(var Message: TMessage);2 begin3   CallDefaultProc(Message);   // 优先有系统处理此消息4   InvalidateNC;               // 重绘标题区5   Handled := true;6 end;

 

绘制客户区

  还有最后一个问题。在缩放窗体时,客户区惨不忍睹。其实这个还是比较简单,处理擦除背景(WM_ERASEBKGND)和响应绘制(WM_PAINT)消息就能完成。

 擦除处理 

 1 procedure TTest.WMEraseBkgnd(var message: TWMEraseBkgnd); 2 var 3   DC: HDC; 4   SaveIndex: integer; 5 begin 6   DC := Message.DC; 7   if DC <> 0 then 8   begin 9     // 如果是容器控件,擦除一定要处理。填色也行。10     // 否则会出现因主绘制延迟,出现短暂的未刷新色块残留。特别在使用Buffer方式绘制时常出现11     SaveIndex := SaveDC(DC);12     PaintBackground(DC);13     RestoreDC(DC, SaveIndex);14   end;15 16   Handled := True;       // 消息处理完成,控件不再处理17   Message.Result := 1;   // 绘制结束,外部不用处理18 end;

 

绘制客户区,需要通知子控件刷新。

 1 procedure TTest.WMPaint(var message: TWMPaint); 2 var 3   DC, hPaintDC: HDC; 4   cBuffer: TBitmap; 5   PS: TPaintStruct; 6 begin 7   /// 8   /// 绘制客户区域 9   ///10   DC := Message.DC;11 12   hPaintDC := DC;13   if DC = 0 then14     hPaintDC := BeginPaint(Handle, PS);15 16   if DC = 0 then17   begin18     /// 缓冲模式绘制,减少闪烁19     cBuffer := TBitmap.Create;20     try21       cBuffer.SetSize(FWidth, FHeight);22       PaintBackground(cBuffer.Canvas.Handle);23       Paint(cBuffer.Canvas.Handle);24       /// 通知子控件进行绘制25       /// 主要是些图形控件的重绘制(如TShape),否则停靠在Form上的图像控件无法正常显示26       if Control is TWinControl then27         TacWinControl(Control).PaintControls(cBuffer.Canvas.Handle, nil);28       BitBlt(hPaintDC, 0, 0, FWidth, FHeight, cBuffer.Canvas.Handle, 0, 0, SRCCOPY);29     finally30       cBuffer.Free;31     end;32   end33   else34   begin35     Paint(hPaintDC);36     // 通知子控件重绘37     if Control is TWinControl then38       TacWinControl(Control).PaintControls(hPaintDC, nil);39   end;40 41   if DC = 0 then42     EndPaint(Handle, PS);43 44   Handled := True;45 end;

 

其中的Paint不需要处理任何代码。

procedure TTest.Paint(DC: HDC);begin  // 不需要处理。end;

 

基本的窗体绘制控制基本完成。

 

现在时下流行的换肤,还是比较容易实现。增加一块背景图资源,在绘制时算好位置贴上去就OK。还有一些鼠标滑入按钮的渐变效果,可以创建一个时钟记录每个按钮的背景褪色值(透明度)使用AlphaBlend 这个函数进行绘制,或是用混色的方法处理。

 1 procedure DrawTransparentBitmap(Source: TBitmap; sx, sy: Integer; Destination: HDC; 2   const dX, dY: Integer;  w, h: Integer; const Opacity: Byte = 255); overload; 3 var 4   BlendFunc: TBlendFunction; 5 begin 6   BlendFunc.BlendOp := AC_SRC_OVER; 7   BlendFunc.BlendFlags := 0; 8   BlendFunc.SourceConstantAlpha := Opacity; 9 10   if Source.PixelFormat = pf32bit then11     BlendFunc.AlphaFormat := AC_SRC_ALPHA12   else13     BlendFunc.AlphaFormat := 0;14 15   AlphaBlend(Destination, dX, dY, w, h, Source.Canvas.Handle, sx, sy, w, h, BlendFunc);16 end;
通过透明度控制背景动画效果,参考DrawTransparentBitmap

 

感觉XE3有些伤不起,Release版本的exe竟然要2.42M。哎~。看来要搞个C版的。

 

  1 unit ufrmCaptionToolbar;  2   3 interface  4   5 uses  6   Messages, SysUtils, Variants, Types, Controls, Forms, Dialogs, StdCtrls,  7   ExtCtrls,  8   ComCtrls,  9   Windows,  // 这个单元放在 ComCtrls 的后面,HITTEST 的定义重名。大小写不敏感真的很不方便 10   Classes, Graphics, 11   pngimage, Actions, ActnList, ToolWin, Vcl.ImgList, Vcl.Buttons; 12  13 type 14   TFormButtonKind = (fbkMin, fbkMax, fbkRestore, fbkClose, fbkHelp); 15   TSkinIndicator = (siInactive, siHover, siPressed, siSelected, siHoverSelected); 16  17   TTest = class 18   strict private 19   const 20     WM_NCUAHDRAWCAPTION = $00AE; 21   private 22     FCallDefaultProc: Boolean; 23     FChangeSizeCalled: Boolean; 24     FControl: TWinControl; 25     FHandled: Boolean; 26  27     FRegion: HRGN; 28     FLeft: integer; 29     FTop: integer; 30     FWidth: integer; 31     FHeight: integer; 32  33     /// 窗体图标 34     FIcon: TIcon; 35     FIconHandle: HICON; 36  37     // 38     FPressedHit: Integer;     // 实际按下的位置, (只处理关心的位置,其他有交由系统处理) 39     FHotHit: integer;         // 记录上次的测试位置 (只处理关心的位置,其他有交由系统处理) 40  41     // skin 42     //  这个内容应独立出来,作为单独一份配置应用于所有窗体。 43     FSkinData: TBitmap; 44     procedure DrawButton(DC: HDC; AKind: TFormButtonKind; AState: TSkinIndicator; const R: TRect); 45  46     function GetHandle: HWND; inline; 47     function GetForm: TCustomForm; inline; 48     function GetFrameSize: TRect; 49     function GetCaptionRect(AMaxed: Boolean): TRect; inline; 50     function GetCaption: string; 51     function GetIcon: TIcon; 52     function GetIconFast: TIcon; 53  54     procedure ChangeSize; 55     function  NormalizePoint(P: TPoint): TPoint; 56     function  HitTest(P: TPoint):integer; 57     procedure Maximize; 58     procedure Minimize; 59  60     // 第一组 实现绘制基础 61     procedure WMNCPaint(var message: TWMNCPaint); message WM_NCPAINT; 62     procedure WMNCActivate(var message: TMessage); message WM_NCACTIVATE; 63     procedure WMNCLButtonDown(var message: TWMNCHitMessage); message WM_NCLBUTTONDOWN; 64     procedure WMNCUAHDrawCaption(var message: TMessage); message WM_NCUAHDRAWCAPTION; 65  66     // 第二组 控制窗体样式 67     procedure WMNCCalcSize(var message: TWMNCCalcSize); message WM_NCCALCSIZE; 68     procedure WMWindowPosChanging(var message: TWMWindowPosChanging); message WM_WINDOWPOSCHANGING; 69  70     // 第三组 绘制背景和内部控件 71     procedure WMEraseBkgnd(var message: TWMEraseBkgnd); message WM_ERASEBKGND; 72     procedure WMPaint(var message: TWMPaint); message WM_PAINT; 73  74     // 第四组 控制按钮状态 75     procedure WMNCHitTest(var Message: TWMNCHitTest); message WM_NCHITTEST; 76     procedure WMNCLButtonUp(var Message: TWMNCHitMessage); message WM_NCLBUTTONUP; 77     procedure WMNCMouseMove(var Message: TWMNCMouseMove); message WM_NCMOUSEMOVE; 78  79     procedure WMSetText(var Message: TMessage); message WM_SETTEXT; 80  81     procedure WndProc(var message: TMessage); 82     procedure CallDefaultProc(var message: TMessage); 83  84   protected 85     property  Handle: HWND read GetHandle; 86     procedure InvalidateNC; 87     procedure PaintNC(DC: HDC); 88     procedure PaintBackground(DC: HDC); 89     procedure Paint(DC: HDC); 90  91   public 92     constructor Create(AOwner: TWinControl); 93     destructor Destroy; override; 94  95     property Handled: Boolean read FHandled write FHandled; 96     property Control: TWinControl read FControl; 97     property Form: TCustomForm read GetForm; 98  99   end;100 101   TForm11 = class(TForm)102     Button1: TButton;103     Shape1: TShape;104     Edit1: TEdit;105     Edit2: TEdit;106     Edit3: TEdit;107     Edit4: TEdit;108     ToolBar1: TToolBar;109     ToolButton1: TToolButton;110     ToolButton2: TToolButton;111     ToolButton3: TToolButton;112     ActionList1: TActionList;113     Action1: TAction;114     Action2: TAction;115     Action3: TAction;116     ImageList1: TImageList;117     procedure Action1Execute(Sender: TObject);118     procedure Action2Execute(Sender: TObject);119     procedure SpeedButton1Click(Sender: TObject);120   private121     FTest: TTest;122   protected123     function DoHandleMessage(var message: TMessage): Boolean;124     procedure WndProc(var message: TMessage); override;125   public126     constructor Create(AOwner: TComponent); override;127     destructor Destroy; override;128   end;129 130   Res = class131     class procedure LoadGraphic(const AName: string; AGraphic: TGraphic);132     class procedure LoadBitmap(const AName: string; AGraphic: TBitmap);133   end;134 135 var136   Form11: TForm11;137 138 implementation139 140 const141   SKINCOLOR_BAKCGROUND  = $00BF7B18;  // 背景色142   SKINCOLOR_BTNHOT      = $00F2D5C2;  // Hot 激活状态143   SKINCOLOR_BTNPRESSED  = $00E3BDA3;  // 按下状态144   SIZE_SYSBTN: TSize    = (cx: 29; cy: 18);145   SIZE_FRAME: TRect     = (Left: 4; Top: 28; Right: 5; Bottom: 5); // 窗体边框的尺寸146   SPACE_AREA            = 3;          // 功能区域之间间隔147   SIZE_RESICON          = 16;         // 资源中图标默认尺寸148 149 150 {$R *.dfm}151 {$R MySkin.RES}152 153 type154   TacWinControl = class(TWinControl);155 156 function BuildRect(L, T, W, H: Integer): TRect; inline;157 begin158   Result := Rect(L, T, L + W, T + H);159 end;160 161 procedure DrawTransparentBitmap(Source: TBitmap; sx, sy: Integer; Destination: HDC;162   const dX, dY: Integer;  w, h: Integer; const Opacity: Byte = 255); overload;163 var164   BlendFunc: TBlendFunction;165 begin166   BlendFunc.BlendOp := AC_SRC_OVER;167   BlendFunc.BlendFlags := 0;168   BlendFunc.SourceConstantAlpha := Opacity;169 170   if Source.PixelFormat = pf32bit then171     BlendFunc.AlphaFormat := AC_SRC_ALPHA172   else173     BlendFunc.AlphaFormat := 0;174 175   AlphaBlend(Destination, dX, dY, w, h, Source.Canvas.Handle, sx, sy, w, h, BlendFunc);176 end;177 178 class procedure Res.LoadBitmap(const AName: string; AGraphic: TBitmap);179 var180   cPic: TPngImage;181   cBmp: TBitmap;182 begin183   cBmp := AGraphic;184   cPic := TPngImage.Create;185   try186     cBmp.PixelFormat := pf32bit;187     cBmp.alphaFormat := afIgnored;188     try189       LoadGraphic(AName, cPic);190       cBmp.SetSize(cPic.Width, cPic.Height);191       cBmp.Canvas.Brush.Color := clBlack;192       cBmp.Canvas.FillRect(Rect(0, 0, cBmp.Width, cBmp.Height));193       cBmp.Canvas.Draw(0, 0, cPic);194     except195       // 不处理空图片196     end;197   finally198     cPic.Free;199   end;200 end;201 202 class procedure Res.LoadGraphic(const AName: string; AGraphic: TGraphic);203 var204   cStream: TResourceStream;205   h: THandle;206 begin207   ///208   /// 加载图片资源209   h := HInstance;210   cStream := TResourceStream.Create(h, AName, RT_RCDATA);211   try212     AGraphic.LoadFromStream(cStream);213   finally214     cStream.Free;215   end;216 end;217 218 { TForm11 }219 220 constructor TForm11.Create(AOwner: TComponent);221 begin222   FTest := TTest.Create(Self);223   inherited;224 end;225 226 destructor TForm11.Destroy;227 begin228   inherited;229   FreeAndNil(FTest);230 end;231 232 procedure TForm11.Action1Execute(Sender: TObject);233 begin234   Tag := Tag + 1;235   Caption := format(test %d, [Tag]);236 end;237 238 procedure TForm11.Action2Execute(Sender: TObject);239 begin240   if Shape1.Shape <> High(TShapeType) then241     Shape1.Shape := Succ(Shape1.Shape)242   else243     Shape1.Shape := low(TShapeType);244 end;245 246 function TForm11.DoHandleMessage(var message: TMessage): Boolean;247 begin248   Result := False;249   if not FTest.FCallDefaultProc then250   begin251     FTest.WndProc(message);252     Result := FTest.Handled;253   end;254 end;255 256 procedure TForm11.SpeedButton1Click(Sender: TObject);257 begin258   Caption := format(test %d, [1]);259 end;260 261 procedure TForm11.WndProc(var message: TMessage);262 begin263   if not DoHandleMessage(Message) then264     inherited;265 end;266 267 procedure TTest.CallDefaultProc(var message: TMessage);268 begin269   if FCallDefaultProc then270     FControl.WindowProc(message)271   else272   begin273     FCallDefaultProc := True;274     FControl.WindowProc(message);275     FCallDefaultProc := False;276   end;277 end;278 279 procedure TTest.ChangeSize;280 var281   hTmp: HRGN;282 begin283   /// 设置窗体外框样式284   FChangeSizeCalled := True;285   try286     hTmp := FRegion;287     try288       /// 创建矩形外框,3的倒角289       FRegion := CreateRoundRectRgn(0, 0, FWidth, FHeight, 3, 3);290       SetWindowRgn(Handle, FRegion, True);291     finally292       if hTmp <> 0 then293         DeleteObject(hTmp);294     end;295   finally296     FChangeSizeCalled := False;297   end;298 end;299 300 function TTest.NormalizePoint(P: TPoint): TPoint;301 var302   rWindowPos, rClientPos: TPoint;303 begin304   rWindowPos := Point(FLeft, FTop);305   rClientPos := Point(0, 0);306   ClientToScreen(Handle, rClientPos);307   Result := P;308   ScreenToClient(Handle, Result);309   Inc(Result.X, rClientPos.X - rWindowPos.X);310   Inc(Result.Y, rClientPos.Y - rWindowPos.Y);311 end;312 313 function TTest.HitTest(P: TPoint):integer;314 var315   bMaxed: Boolean;316   r: TRect;317   rCaptionRect: TRect;318   rFrame: TRect;319 begin320   Result := HTNOWHERE;321 322   ///323   /// 检测位置324   ///325   rFrame := GetFrameSize;326   if p.Y > rFrame.Top then327     Exit;328 329   ///330   ///  只关心窗体按钮区域331   ///332   bMaxed := IsZoomed(Handle);333   rCaptionRect := GetCaptionRect(bMaxed);334   if PtInRect(rCaptionRect, p) then335   begin336     r.Right := rCaptionRect.Right - 1;337     r.Top := 0;338     if bMaxed then339       r.Top := rCaptionRect.Top;340     r.Top := r.Top + (rFrame.Top - r.Top - SIZE_SYSBTN.cy) div 2;341     r.Left := r.Right - SIZE_SYSBTN.cx;342     r.Bottom := r.Top + SIZE_SYSBTN.cy;343 344     ///345     /// 实际绘制的按钮就三个,其他没处理346     ///347     if (P.Y >= r.Top) and (p.Y <= r.Bottom) and (p.X <= r.Right) then348     begin349       if (P.X >= r.Left) then350         Result := HTCLOSE351       else if p.X >= (r.Left - SIZE_SYSBTN.cx) then352         Result := HTMAXBUTTON353       else if p.X >= (r.Left - SIZE_SYSBTN.cx * 2) then354         Result := HTMINBUTTON;355     end;356   end;357 end;358 359 constructor TTest.Create(AOwner: TWinControl);360 begin361   FControl := AOwner;362   FRegion := 0;363   FChangeSizeCalled := False;364   FCallDefaultProc := False;365 366   FWidth := FControl.Width;367   FHeight := FControl.Height;368   FIcon := nil;369   FIconHandle := 0;370 371   // 加载资源372   FSkinData := TBitmap.Create;373   Res.LoadBitmap(MySkin, FSkinData);374 end;375 376 destructor TTest.Destroy;377 begin378   FIconHandle := 0;379   if FSkinData <> nil then380     FreeAndNil(FSkinData);381   if FIcon <> nil then382     FreeAndNil(FIcon);383   if FRegion <> 0 then384     DeleteObject(FRegion);385   inherited;386 end;387 388 procedure TTest.DrawButton(DC: HDC; AKind: TFormButtonKind; AState: TSkinIndicator; const R: TRect);389 var390   hB: HBRUSH;391   iColor: Cardinal;392   rSrcOff: TPoint;393   x, y: integer;394 begin395   /// 绘制背景396   case AState of397     siHover         : iColor := SKINCOLOR_BTNHOT;398     siPressed       : iColor := SKINCOLOR_BTNPRESSED;399     siSelected      : iColor := SKINCOLOR_BTNPRESSED;400     siHoverSelected : iColor := SKINCOLOR_BTNHOT;401   else                iColor := SKINCOLOR_BAKCGROUND;402   end;403   hB := CreateSolidBrush(iColor);404   FillRect(DC, R, hB);405   DeleteObject(hB);406 407   /// 绘制图标408   rSrcOff := Point(SIZE_RESICON * ord(AKind), 0);409   x := R.Left + (R.Right - R.Left - SIZE_RESICON) div 2;410   y := R.Top + (R.Bottom - R.Top - SIZE_RESICON) div 2;411   DrawTransparentBitmap(FSkinData, rSrcOff.X, rSrcOff.Y, DC, x, y, SIZE_RESICON, SIZE_RESICON);412 end;413 414 function TTest.GetFrameSize: TRect;415 begin416   Result := SIZE_FRAME;417 end;418 419 function TTest.GetCaptionRect(AMaxed: Boolean): TRect;420 var421   rFrame: TRect;422 begin423   rFrame := GetFrameSize;424   // 最大化状态简易处理425   if AMaxed then426     Result := Rect(8, 8, FWidth - 9 , rFrame.Top)427   else428     Result := Rect(rFrame.Left, 3, FWidth - rFrame.right, rFrame.Top);429 end;430 431 function TTest.GetCaption: string;432 var433   Buffer: array [0..255] of Char;434   iLen: integer;435 begin436   if Handle <> 0 then437   begin438     iLen := GetWindowText(Handle, Buffer, Length(Buffer));439     SetString(Result, Buffer, iLen);440   end441   else442     Result := ‘‘;443 end;444 445 function TTest.GetForm: TCustomForm;446 begin447   Result := TCustomForm(Control);448 end;449 450 function TTest.GetHandle: HWND;451 begin452   if FControl.HandleAllocated then453     Result := FControl.Handle454   else455     Result := 0;456 end;457 458 function TTest.GetIcon: TIcon;459 var460   IconX, IconY: integer;461   TmpHandle: THandle;462   Info: TWndClassEx;463   Buffer: array [0 .. 255] of Char;464 begin465   ///466   /// 获取当前form的图标467   /// 这个图标和App的图标是不同的468   ///469   TmpHandle := THandle(SendMessage(Handle, WM_GETICON, ICON_SMALL, 0));470   if TmpHandle = 0 then471     TmpHandle := THandle(SendMessage(Handle, WM_GETICON, ICON_BIG, 0));472 473   if TmpHandle = 0 then474   begin475     { Get instance }476     GetClassName(Handle, @Buffer, SizeOf(Buffer));477     FillChar(Info, SizeOf(Info), 0);478     Info.cbSize := SizeOf(Info);479 480     if GetClassInfoEx(GetWindowLong(Handle, GWL_HINSTANCE), @Buffer, Info) then481     begin482       TmpHandle := Info.hIconSm;483       if TmpHandle = 0 then484         TmpHandle := Info.HICON;485     end486   end;487 488   if FIcon = nil then489     FIcon := TIcon.Create;490 491   if TmpHandle <> 0 then492   begin493     IconX := GetSystemMetrics(SM_CXSMICON);494     if IconX = 0 then495       IconX := GetSystemMetrics(SM_CXSIZE);496     IconY := GetSystemMetrics(SM_CYSMICON);497     if IconY = 0 then498       IconY := GetSystemMetrics(SM_CYSIZE);499     FIcon.Handle := CopyImage(TmpHandle, IMAGE_ICON, IconX, IconY, 0);500     FIconHandle := TmpHandle;501   end;502 503   Result := FIcon;504 end;505 506 function TTest.GetIconFast: TIcon;507 begin508   if (FIcon = nil) or (FIconHandle = 0) then509     Result := GetIcon510   else511     Result := FIcon;512 end;513 514 procedure TTest.InvalidateNC;515 begin516   if FControl.HandleAllocated then517     SendMessage(Handle, WM_NCPAINT, 1, 0);518 end;519 520 procedure TTest.Maximize;521 begin522   if Handle <> 0 then523   begin524     FPressedHit := 0;525     FHotHit := 0;526     if IsZoomed(Handle) then527       SendMessage(Handle, WM_SYSCOMMAND, SC_RESTORE, 0)528     else529       SendMessage(Handle, WM_SYSCOMMAND, SC_MAXIMIZE, 0);530   end;531 end;532 533 procedure TTest.Minimize;534 begin535   if Handle <> 0 then536   begin537     FPressedHit := 0;538     FHotHit := 0;539     if IsIconic(Handle) then540       SendMessage(Handle, WM_SYSCOMMAND, SC_RESTORE, 0)541     else542       SendMessage(Handle, WM_SYSCOMMAND, SC_MINIMIZE, 0);543    end;544 end;545 546 procedure TTest.PaintNC(DC: HDC);547 const548   HITVALUES: array [TFormButtonKind] of integer = (HTMINBUTTON, HTMAXBUTTON, HTMAXBUTTON, HTCLOSE, HTHELP);549 550   function GetBtnState(AKind: TFormButtonKind): TSkinIndicator;551   begin552     if (FPressedHit = FHotHit) and (FPressedHit = HITVALUES[AKind]) then553       Result := siPressed554     else if FHotHit = HITVALUES[AKind] then555       Result := siHover556     else557       Result := siInactive;558   end;559 560 var561   hB: HBRUSH;562   rFrame: TRect;563   rButton: TRect;564   SaveIndex: integer;565   bMaxed: Boolean;566   rCaptionRect : TRect;567   sData: string;568   Flag: Cardinal;569   SaveColor: cardinal;570 begin571   SaveIndex := SaveDC(DC);572   try573     bMaxed := IsZoomed(Handle);574 575     // 扣除客户区域576     rFrame := GetFrameSize;577     ExcludeClipRect(DC, rFrame.Left, rFrame.Top, FWidth - rFrame.Right, FHeight - rFrame.Bottom);578 579     ///580     ///  标题区域581     ///582     rCaptionRect := GetCaptionRect(bMaxed);583 584     // 填充整个窗体背景585     hB := CreateSolidBrush(SKINCOLOR_BAKCGROUND);586     FillRect(DC, Rect(0, 0, FWidth, FHeight), hB);587     DeleteObject(hB);588 589     /// 绘制窗体图标590     rButton := BuildRect(rCaptionRect.Left + 2, rCaptionRect.Top, GetSystemMetrics(SM_CXSMICON), GetSystemMetrics(SM_CYSMICON));591     rButton.Top := rButton.Top + (rFrame.Top - rButton.Bottom) div 2;592     DrawIconEx(DC, rButton.Left, rButton.Top, GetIconFast.Handle, 0, 0, 0, 0, DI_NORMAL);593 594     rCaptionRect.Left := rButton.Right + 5; // 前部留白595 596     /// 绘制窗体按钮区域597     rButton.Right := rCaptionRect.Right - 1;598     rButton.Top := 0;599     if bMaxed then600       rButton.Top := rCaptionRect.Top;601     rButton.Top := rButton.Top + (rFrame.Top - rButton.Top - SIZE_SYSBTN.cy) div 2;602     rButton.Left := rButton.Right - SIZE_SYSBTN.cx;603     rButton.Bottom := rButton.Top + SIZE_SYSBTN.cy;604     DrawButton(Dc, fbkClose, GetBtnState(fbkClose), rButton);605 606     OffsetRect(rButton, - SIZE_SYSBTN.cx, 0);607     if bMaxed then608       DrawButton(Dc, fbkRestore, GetBtnState(fbkRestore), rButton)609     else610       DrawButton(Dc, fbkMax, GetBtnState(fbkMax), rButton);611 612     OffsetRect(rButton, - SIZE_SYSBTN.cx, 0);613     DrawButton(Dc, fbkMin, GetBtnState(fbkMin), rButton);614 615     rCaptionRect.Right := rButton.Left - 3; // 后部空出616 617     /// 绘制Caption618     sData :=  GetCaption;619     SetBkMode(DC, TRANSPARENT);620     SaveColor := SetTextColor(DC, $00FFFFFF);621 622     Flag := DT_LEFT or DT_VCENTER or DT_SINGLELINE or DT_NOPREFIX;623     DrawTextEx(DC, PChar(sData), Length(sData), rCaptionRect, Flag, nil);624     SetTextColor(DC, SaveColor);625   finally626     RestoreDC(DC, SaveIndex);627   end;628 end;629 630 procedure TTest.PaintBackground(DC: HDC);631 var632   hB: HBRUSH;633   R: TRect;634 begin635   GetClientRect(Handle, R);636   hB := CreateSolidBrush($00F0F0F0);637   FillRect(DC, R, hB);638   DeleteObject(hB);639 end;640 641 procedure TTest.Paint(DC: HDC);642 begin643   // PaintBackground(DC);644   // TODO -cMM: TTest.Paint default body inserted645 end;646 647 procedure TTest.WMEraseBkgnd(var message: TWMEraseBkgnd);648 var649   DC: HDC;650   SaveIndex: integer;651 begin652   DC := Message.DC;653   if DC <> 0 then654   begin655     SaveIndex := SaveDC(DC);656     PaintBackground(DC);657     RestoreDC(DC, SaveIndex);658   end;659 660   Handled := True;661   Message.Result := 1;662 end;663 664 procedure TTest.WMNCActivate(var message: TMessage);665 begin666   // FFormActive := Message.WParam > 0;667   Message.Result := 1;668   InvalidateNC;669   Handled := True;670 end;671 672 procedure TTest.WMNCCalcSize(var message: TWMNCCalcSize);673 var674   R: TRect;675 begin676   // 改变边框尺寸677   R := GetFrameSize;678   with TWMNCCalcSize(Message).CalcSize_Params^.rgrc[0] do679   begin680     Inc(Left, R.Left);681     Inc(Top, R.Top);682     Dec(Right, R.Right);683     Dec(Bottom, R.Bottom);684   end;685   Message.Result := 0;686   Handled := True;687 end;688 689 procedure TTest.WMNCHitTest(var Message: TWMNCHitTest);690 var691   P: TPoint;692   iHit: integer;693 begin694   // 需要把位置转换到实际窗口位置695   P := NormalizePoint(Point(Message.XPos, Message.YPos));696 697   // 获取 位置698   iHit := HitTest(p);699   if FHotHit > HTNOWHERE then700   begin701     Message.Result := iHit;702     Handled := True;703   end;704 705   if iHit <> FHotHit then706   begin707     FHotHit := iHit;708     InvalidateNC;709   end;710 711 end;712 713 procedure TTest.WMWindowPosChanging(var message: TWMWindowPosChanging);714 var715   bChanged: Boolean;716 begin717   CallDefaultProc(TMessage(Message));718 719   Handled := True;720   bChanged := False;721 722   /// 防止嵌套723   if FChangeSizeCalled then724     Exit;725 726   if (Message.WindowPos^.flags and SWP_NOSIZE = 0) or (Message.WindowPos^.flags and SWP_NOMOVE = 0) then727   begin728     if (Message.WindowPos^.flags and SWP_NOMOVE = 0) then729     begin730       FLeft := Message.WindowPos^.x;731       FTop := Message.WindowPos^.y;732     end;733     if (Message.WindowPos^.flags and SWP_NOSIZE = 0) then734     begin735       bChanged := ((Message.WindowPos^.cx <> FWidth) or (Message.WindowPos^.cy <> FHeight)) and736         (Message.WindowPos^.flags and SWP_NOSIZE = 0);737       FWidth := Message.WindowPos^.cx;738       FHeight := Message.WindowPos^.cy;739     end;740   end;741 742   if (Message.WindowPos^.flags and SWP_FRAMECHANGED <> 0) then743     bChanged := True;744 745   if bChanged then746   begin747     ChangeSize;748     InvalidateNC;749   end;750 end;751 752 procedure TTest.WMNCLButtonDown(var message: TWMNCHitMessage);753 var754   iHit: integer;755 begin756   inherited;757 758   iHit := HTNOWHERE;759   if (Message.HitTest = HTCLOSE) or (Message.HitTest = HTMAXBUTTON) or (Message.HitTest = HTMINBUTTON) or760     (Message.HitTest = HTHELP) then761   begin762     iHit := Message.HitTest;763 764     Message.Result := 0;765     Message.Msg := WM_NULL;766     Handled := True;767   end;768 769   if iHit <> FPressedHit then770   begin771     FPressedHit := iHit;772     InvalidateNC;773   end;774 end;775 776 procedure TTest.WMNCLButtonUp(var Message: TWMNCHitMessage);777 var778   iWasHit: Integer;779 begin780   iWasHit := FPressedHit;781   if iWasHit <> HTNOWHERE then782   begin783     FPressedHit := HTNOWHERE;784     //InvalidateNC;785 786     if iWasHit = FHotHit then787     begin788       case Message.HitTest of789         HTCLOSE     : SendMessage(Handle, WM_SYSCOMMAND, SC_CLOSE, 0);790         HTMAXBUTTON : Maximize;791         HTMINBUTTON : Minimize;792         HTHELP      : SendMessage(Handle, WM_SYSCOMMAND, SC_CONTEXTHELP, 0);793       end;794 795       Message.Result := 0;796       Message.Msg := WM_NULL;797       Handled := True;798     end;799   end;800 end;801 802 procedure TTest.WMNCMouseMove(var Message: TWMNCMouseMove);803 begin804   if (FPressedHit <> HTNOWHERE) and (FPressedHit <> Message.HitTest) then805     FPressedHit := HTNOWHERE;806 end;807 808 procedure TTest.WMSetText(var Message: TMessage);809 begin810   CallDefaultProc(Message);811   InvalidateNC;812   Handled := true;813 end;814 815 procedure TTest.WMNCPaint(var message: TWMNCPaint);816 var817   DC: HDC;818 begin819   DC := GetWindowDC(Control.Handle);820   PaintNC(DC);821   ReleaseDC(Handle, DC);822   Handled := True;823 end;824 825 procedure TTest.WMNCUAHDrawCaption(var message: TMessage);826 begin827   /// 这个消息会在winxp下产生,是内部Bug处理,直接丢弃此消息828   Handled := True;829 end;830 831 procedure TTest.WMPaint(var message: TWMPaint);832 var833   DC, hPaintDC: HDC;834   cBuffer: TBitmap;835   PS: TPaintStruct;836 begin837   ///838   /// 绘制客户区域839   ///840   DC := Message.DC;841 842   hPaintDC := DC;843   if DC = 0 then844     hPaintDC := BeginPaint(Handle, PS);845 846   if DC = 0 then847   begin848     /// 缓冲模式绘制,减少闪烁849     cBuffer := TBitmap.Create;850     try851       cBuffer.SetSize(FWidth, FHeight);852       PaintBackground(cBuffer.Canvas.Handle);853       Paint(cBuffer.Canvas.Handle);854       /// 通知子控件进行绘制855       /// 主要是些图形控件的重绘制(如TShape),否则停靠在Form上的图像控件无法正常显示856       if Control is TWinControl then857         TacWinControl(Control).PaintControls(cBuffer.Canvas.Handle, nil);858       BitBlt(hPaintDC, 0, 0, FWidth, FHeight, cBuffer.Canvas.Handle, 0, 0, SRCCOPY);859     finally860       cBuffer.Free;861     end;862   end863   else864   begin865     Paint(hPaintDC);866     // 通知子控件重绘867     if Control is TWinControl then868       TacWinControl(Control).PaintControls(hPaintDC, nil);869   end;870 871   if DC = 0 then872     EndPaint(Handle, PS);873 874   Handled := True;875 end;876 877 procedure TTest.WndProc(var message: TMessage);878 begin879   FHandled := False;880   Dispatch(message);881 end;882 883 end.
完整测试单元代码

 

相关API和消息

  • IsZoomed                             --- 窗体是否最大化
  • GetClassInfoEx                      --- 获取窗体图标
  • WM_GETICON                       --- 获取窗体图标
  • DrawTransparentBitmap         --- 绘制透明图片
  • GetWindowLong                     --- 获取窗体信息 
  • DrawIconEx                           --- 绘制ICON
  • SetBkMode                            --- 设置字体绘制背景 
  • SetTextColor                          --- 设置字体绘制颜色

 

开发环境:

  • XE3
  • win7

源代码:

    https://github.com/cmacro/simple/tree/master/TestCaptionToolbar_v0.3

 

窗体皮肤实现 - 重绘窗体非客户区(三)