制作可点击的异形窗口(1)

设计丢来一个系统升级界面UI图,是个异形窗口,即有窗体圆角又有个火箭头凸出来,挺带感的。旧貌换新颜,这事值一做。

截图1

一. 预处理

首先想到的实现方式是填个底色,然后设置窗体的TransparentColor为该底色即可,毕竟软件启动时的splash屏就是这么实现的。
在PhotoShop里填充上黄色底色:
截图2
导出为WEB格式时,选择PNG8格式,并仔细裁剪掉边缘杂色。最后存储为png图像文件。
截图3

二. 通过TransparentColor属性实现异形窗体

设置窗体TransparentColor=true、TransparentColorValue=黄色底色,补上点击和窗体移动代码,运行测试效果。
截图4
显示效果倒是实现了,但有一个问题:点击窗体上半部分会穿透到下层窗口,下半部分却能正常响应。反复测试发现该现象与图片的宽高比例关系不大。
截图5

三. 通过RGN实现异形窗体

既然点击不正常,而delphi窗体自带的的实现方法不便修改,那就换种实现方式:直接读取像素信息,把底色对应的区域从窗体 RGN 中剔除。
快速撸了一段实现代码:

procedure TfrmUpdate.FormCreate(Sender: TObject);
var
  bmp: TBitmap;
  FullRgn, LineRgn: HRGN;
  x, y, StartX: Integer;
  Row: PRGBTriple; //从ScanLine读取像素
  IsTransparent: Boolean;
  TargetB, TargetG, TargetR: Byte;
begin
  ClientWidth := Image1.Width;
  ClientHeight := Image1.Height;
  //底色的RGB分量
  TargetB := $00;
  TargetG := $D8;
  TargetR := $FF;
  bmp := TBitmap.Create;
  try
    bmp.Assign(Image1.Picture.Graphic);
    //24位色深度,让PRGBTriple准确对位
    bmp.PixelFormat := pf24bit;
    //初始化一个空RGN
    FullRgn := CreateRectRgn(0, 0, 0, 0);
    //水平扫描线像素
    for y := 0 to bmp.Height - 1 do
    begin
      //依当前图片特性优化计算,跳过没有透明色的区域。其他图片注释掉这两行
      if (y > 55) and (y < bmp.Height - 8) then
        Continue;
      Row := bmp.ScanLine[y]; //获取当前行的内存指针
      x := 0;
      while x < bmp.Width do
      begin
        //判断是否为底色(透明色)
        IsTransparent := (Row.rgbtBlue = TargetB) and
                         (Row.rgbtGreen = TargetG) and
                         (Row.rgbtRed = TargetR);
        //如果是透明色,一直往右找直到找到不透明的像素
        while (x < bmp.Width) and IsTransparent do
        begin
          Inc(x);
          Inc(Row);
          if x < bmp.Width then
            IsTransparent := (Row.rgbtBlue = TargetB) and
                             (Row.rgbtGreen = TargetG) and
                             (Row.rgbtRed = TargetR);
        end;
        //记录这段不透明区域的起点X坐标
        StartX := x; 
        //从不透明像素起往右找,直到再次遇到透明色或到达边界
        while (x < bmp.Width) and (not IsTransparent) do
        begin
          Inc(x);
          Inc(Row);
          if x < bmp.Width then
            IsTransparent := (Row.rgbtBlue = TargetB) and
                             (Row.rgbtGreen = TargetG) and
                             (Row.rgbtRed = TargetR);
        end;
        //将这段不透明的水平线段(StartX 到 x)加到RGN
        if StartX < x then
        begin
          //创建一个1像素高的矩形区域 (左闭右开:y到y+1)
          LineRgn := CreateRectRgn(StartX, y, x, y + 1);
          //将这个细长条合并到总区域中 (RGN_OR并集)
          CombineRgn(FullRgn, FullRgn, LineRgn, RGN_OR);
          DeleteObject(LineRgn);
        end;
      end;
    end;
    //依当前图片特性优化计算,将没有透明色的区域加入RGN
    LineRgn := CreateRectRgn(0, 55, bmp.Width, bmp.Height - 8);
    CombineRgn(FullRgn, FullRgn, LineRgn, RGN_OR);
    DeleteObject(LineRgn);
    //计算完毕将RGN应用到窗体
    SetWindowRgn(Handle, FullRgn, True);
    DeleteObject(FullRgn);
  finally
    bmp.Free;
  end;
end;

运行后功能正常,达到要求的效果。

四. 针对PNG8位色优化

既然用的PNG图片,没必须再转一次24位色的bmp来计算,直接基于8位索引色的PNG图片来运算性能更佳。
首先,要处理下图片,将底色转为透明。
截图6
然后重写代码:

procedure TfrmUpdate.FormCreate(Sender: TObject);
var
var
  png: TPNGImage;
  FullRgn, LineRgn: HRGN;
  x, y, StartX: Integer;
  Row: PByteArray;      //PNG8的ScanLine
  IsTransparent: Boolean;
  TransparentIdx: Byte; //透明色在调色板中的索引
  i: Integer;
begin
  ClientWidth := Image1.Width;
  ClientHeight := Image1.Height;
  Label2.Caption := FUpdateThread.ReleaseNote;
  FUpdateThread.FormHandle := Handle;

  //直接使用PNG对象,不用转换
  png := TPNGImage(Image1.Picture.Graphic);

  //在第一行像素中查找透明色索引值
  TransparentIdx := 0;
  Row := png.ScanLine[0];
  for x := 0 to png.Width - 1 do
  begin
    if png.Pixels[x, 0] = png.TransparentColor then
    begin
      TransparentIdx := Row[x];
      Break;
    end;
  end;

  //初始化一个空RGN
  FullRgn := CreateRectRgn(0, 0, 0, 0);

  //逐行扫描,直接读取PNG8的索引数据
  for y := 0 to png.Height - 1 do
  begin
    //依当前图片特性优化计算,跳过没有透明色的区域。其他图片注释掉这两行
    if (y > 55) and (y < png.Height - 8) then
      Continue;

    //ScanLine返回索引值数组,每个字节是调色板索引
    Row := png.ScanLine[y];
    x := 0;

    while x < png.Width do
    begin
      //直接比较索引值,无需查找调色板
      IsTransparent := Row[x] = TransparentIdx;

      //跳过透明像素
      while (x < png.Width) and (Row[x] = TransparentIdx) do
        Inc(x);

      //记录本行不透明区域的起始X坐标
      StartX := x;

      //从不透明像素起往右找,直到再次遇到透明像素或到达边界
      while (x < png.Width) and (Row[x] <> TransparentIdx) do
        Inc(x);

      //将这段不透明的水平线段(StartX 到 x)加到RGN
      if StartX < x then
      begin
        LineRgn := CreateRectRgn(StartX, y, x, y + 1);
        CombineRgn(FullRgn, FullRgn, LineRgn, RGN_OR);
        DeleteObject(LineRgn);
      end;
    end;
  end;

  //依当前图片特性优化计算,将没有透明色的大片区域加入RGN
  LineRgn := CreateRectRgn(0, 55, png.Width, png.Height - 8);
  CombineRgn(FullRgn, FullRgn, LineRgn, RGN_OR);
  DeleteObject(LineRgn);

  //计算完毕将RGN应用到窗体
  SetWindowRgn(Handle, FullRgn, True);
  DeleteObject(FullRgn);
end;

测试运行,一次到位,效果相当不错。
截图7

下次再尝试做个阴影(投影)效果的异形窗口。

posted on 2026-07-01 23:24  IT老彭  阅读(0)  评论(0)    收藏  举报