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

34 篇文章 0 订阅
5 篇文章 0 订阅
前文的窗口如果最大化,你会发现它把任务栏也覆盖了,原因是我们窗口的 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:左边是原图,右边只贴左上角:



图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;



程序运行效果图如下:



点击打开链接

 
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值