设计丢来一个系统升级界面UI图,是个异形窗口,即有窗体圆角又有个火箭头凸出来,挺带感的。旧貌换新颜,这事值一做。
一. 预处理
首先想到的实现方式是填个底色,然后设置窗体的TransparentColor为该底色即可,毕竟软件启动时的splash屏就是这么实现的。
在PhotoShop里填充上黄色底色:
导出为WEB格式时,选择PNG8格式,并仔细裁剪掉边缘杂色。最后存储为png图像文件。
二. 通过TransparentColor属性实现异形窗体
设置窗体TransparentColor=true、TransparentColorValue=黄色底色,补上点击和窗体移动代码,运行测试效果。
显示效果倒是实现了,但有一个问题:点击窗体上半部分会穿透到下层窗口,下半部分却能正常响应。反复测试发现该现象与图片的宽高比例关系不大。
三. 通过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图片来运算性能更佳。
首先,要处理下图片,将底色转为透明。
然后重写代码:
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;
测试运行,一次到位,效果相当不错。
下次再尝试做个阴影(投影)效果的异形窗口。