站内搜索

用Delphi来DIY一个软件变脸功能

 

 < 想法 >

   软件的“换肤”技术早已不是什么新鲜事了,但细心的朋友一定已经发现了,现在正悄悄地流行了一种新的改善视觉效果的方法――这里我斗胆定义为“换肤色”技术吧!用过Winamp 5、Windows MediaPlay 9、MSN Messenger 6、QQ2004这些新版本软件了吧,呵呵,全都采用了所谓换汤不换药的“换肤色”技术。挺有意思是吧,下面我们就“自己动手,丰衣足食”。

   < 准备 >

  首先我用eXeScrope打开了WMP9和MSN6的相关可执行文件和动态链接库,没找到有关界面的资源,晚辈才疏学浅,猜想可能它们的界面是实时计算出来的吧。QQ2004和Winamp5就比较直观了,一个是直接用BMP文件的,另一个采用的是PNG格式。

  BMP文件没什么好说的,关于PNG格式我这里略说两句。PNG(Portable Network Graphics)是为了适应网络数据传输而设计的一种图像格式,用于取代格式较为简单、专利限制严格的GIF图像文件格式。PNG格式大致具有以下优点:高压缩率、支持Alpha通道(全透明、全不透明、可变透明)、提供Gamma(图像亮点)校正机制、提供二维交叉存取机制、支持真彩/灰度/颜色索引的图像。

  分析了一下Winamp5的图形界面布局,他许多漂亮的阴影、渐变效果可不是BMP通过指定颜色透明能做到的;另外考虑到一个程序使用图片皮肤的话文件都会比较多,BMP的话一般都至少有几百K的总大小;所以我觉得PNG图片更适合来做绚丽的界面皮肤。

  Delphi默认是不支持PNG格式的图片的,只能去下载第三方控件了。到DFW论坛里去搜了很多终于让我找到了PNGImage这么个好东东,带源代码、帮助文件,无需安装,支持PNG透明。呵呵,这样我们就可以开工了!

  < 动手 >

  我先看了一下PNGImage的帮助文件,里面的《Example 3: Drawing png over other formats》是一个将一幅指定的PNG图片读入后覆盖到一幅JPG图片上的示例,我尝试了一下能很好的支持带透明的PNG文件。因为是要拿这些PNG文件来作程序界面的,所以我首先打算要把这个PNG图片画到窗体上去:


 
  uses
   ..., pngimage;  // 加上这个

  procedure TForm1.FormPaint(Sender: TObject);
   var
   Png: TPngObject;
   Rect: TRect;
   begin
   Png := TPngObject.Create;
   Png.LoadFromFile('1.png');

  Rect.Left := 0;
   Rect.Top := 0;
   Rect.Right := Rect.Left + Png.Width;
   Rect.Bottom := Rect.Top + Png.Height;

  Png.Draw(Canvas, Rect);

  Png.Free;
   end;

  以上代码实现了将1.png文件读入后画到窗体上去,这张图片是Winamp5的默认主界面,其中右下角有一块凹入的是透明部分,怎么样,效果出来了吧(如图1)。

 接下来我打算把PNG图片放到TImage控件里来做成模拟的按钮。这个比较简单,经过几下尝试,发现只要“Image1.Picture.Assign(Png);”这一句就可以了,同样很好的显示了渐变透明的效果。(注:不能使用“Image1.Picture.Bitmap.Assign(Png);”,虽然这句代码能画出图形,但对于透明是无可奈何的,全部变成黑色;另外不可使用“Image1.Assign(Png);”或“Image1.Picture.Bitmap.Canvas.Assign(Png);”,否则产生运行时类型转换错误,因为TPngObject根本不能转换为TImage或者TBitmapCanvas类型。)另外对于TImage控件中已经有图片的情况,想要将PNG图片盖上去,可以使用TPngObject对象的Draw方法:


 
  Rect.Left := 0;
   Rect.Top := 0;
   Rect.Right := Rect.Left + Png.Width;
   Rect.Bottom := Rect.Top + Png.Height;

  Png.Draw(Image1.Canvas, Rect);
   Image1.Refresh;

  注意:此处不能使用TImage的方法,不然原图就没了;而且还需要调用TImage.Refresh后才能显示更改后的图片(如图2,左边是显示单PNG图片,右边是将PNG图片盖到已有的位图上去)。


   说了这么多,现在该考虑我们的重点内容“换肤色”了。我考虑的基本原理是:先将所有界面相关的图片都做成灰阶PNG图片,可以做出颜色渐变、立体等各种效果;然后用指定的色彩“蒙”到灰阶图片上去。想起来简单,可实际动起手来发现还是碰到了好多问题。因为对RGB颜色和位图只了解一点,一开始便胡乱猜想是不是拿灰阶图片中的某一点像素的RGB值去和指定颜色的RGB值做逻辑与运算(呵呵,让人笑话了),编了点代码试了试,对于几种颜色(黑、白、红、绿、蓝、黄、桃红)的确能“蒙”出正确颜色来(通过和做图软件中得出的效果进行比较),可其他的比如渐变色、非常规色等,就拿刚才前面用到的Winamp5主界面的图片,转出来后变成了大花脸。。。唉,别偷懒,还是好好分析一下吧。

  < 动脑 >

  以“浅色-深色”渐变图片为例,假设我们要将所有含“深色”色的像素转成指定色彩,也就是要转成类似白-红、浅黄-深黄渐变的效果。我们知道TColor其实是用一定范围的十六进制数值来表示的,从低位到高位每个字节分别保存红、绿、蓝的值。对于灰阶色来说,每一种“灰色”其R、G、B的三值是相等的,从黑(RGB(0,0,0))到白(RGB(255,255,255))。经过一段时间的琢磨,我发现对某一像素点的色彩转换大致的思路应该是:

  该点目标色离白色的“距离”(之间的值差,姑且这样称呼吧)/指定彩色离白色的“距离” = 该点灰色离白色的“距离”/最深色离白色的“距离”

  这里的“距离”其实分别是该种颜色的R、G、B三值和255的差的绝对值。有点昏了是吧?呵呵,其实应该是比较好理解的,直观一点的原始公式(分别计算R、G、B三值)是:

  (255 - 目标色R值) / (255 - 指定色R值) = (255 - 灰色R值) / (255 - 最深色R值)

  移项后可得解:

  目标色R值 = 255 - (255 - 灰色R值) * (255 - 指定色R值) / (255 - 最深色R值)

  同理可得目标色的G、B值。现在你可以拿一个指定色(一种浅红)(RGB(153,0,0))和一种灰(RGB(204,204,204))算一下,分别四舍五入后得出的结果RGB(235,204,204),拿到做图软件里去对比一下吧,和做图软件里产生的彩色渐变出来的效果基本看不出区别了!
   既然算法已经找到了,转成代码就再轻松不过了:


 
  procedure TForm1.Button2Click(Sender: TObject);
   var
   i, j: Integer;
   R, G, B, RGBTemp: Cardinal;
   t1, t2: Cardinal;
   Png: TPngObject;
   Rect: TRect;
   begin
   Png := TPngObject.Create;
   Png.LoadFromFile('2.png');

  t1 := GetTickCount;

  for i := 0 to Png.Width - 1 do
   begin
   for j := 0 to Png.Height - 1 do
   begin
   RGBTemp := Png.Pixels[i, j];
   R := GetRValue(RGBTemp);
   G := GetGValue(RGBTemp);
   B := GetBValue(RGBTemp);


   { 计算公式:目标色R/G/B值 = 255 - (255 - 灰色R/G/B值) * (255 - 指定色R/G/B值) / (255 - 最深色R/G/B值) }
 


 
  Png.Pixels[i, j] := RGB(255 - (255 - R) * (255 - 153) div 255, // 按公式计算当前像素的 R 的值
   255 - (255 - G) * (255 - 0) div 255,   // 计算 G 值
   255 - (255 - B) * (255 - 0) div 255);  // 计算 B 值
   end;
   end;

  Rect.Left := 448;
   Rect.Top := 152;
   Rect.Right := Rect.Left + Png.Width;
   Rect.Bottom := Rect.Top + Png.Height;

  Png.Draw(Canvas, Rect);

  t2 := GetTickCount - t1;
   ShowMessage(IntToStr(t2));

  Png.Free;
   end;

  颜色转换后的效果如图3。

  目标看似达到了,不过看看这粗糙的算法吧,二重循环遍历每个象素一定是很慢的,测试了一下转换这张200*200(象素)的图片在P4 2.4的CPU下耗时平均94ms(上面我用了一个RGBTemp临时变量来保存当前像素的RGB值,要不然在计算R、G、B时分别去直接读PNG.Pixels[i,j]的话时间基本要再翻倍)。天!这个耗时很可观哪!后来我把代码改成把图片的ScanLine属性复制到一个指针数组,大大提高了运算速度:

  { 定义指针数组类型 }
 


 
  const
   MaxPixelCount = 65536;

  type
   PRGBArray = ^TRGBArray;
   TRGBArray = array [0..MaxPixelCount - 1] of TRGBTriple;

  procedure TForm1.Button3Click(Sender: TObject);
   var
   i, j: Integer;
   Row: PRGBArray;
   Png: TPngObject;
   Rect: TRect;
   begin
   Png := TPngObject.Create;
   Png.LoadFromFile('2.png');

  for i := 0 to Png.Height - 1 do
   begin
   Row := Png.Scanline[i];  // 复制ScanLine属性到Row指针数组
   for j := 0 to Png.Width - 1 do
   begin
   Row[j].rgbtRed := 255 - (255 - Row[j].rgbtRed) * (255 - 153) div 255;
   Row[j].rgbtGreen := 255 - (255 - Row[j].rgbtGreen) * (255 - 0) div 255;
   Row[j].rgbtBlue := 255 - (255 - Row[j].rgbtBlue) * (255 - 0) div 255;
   end;
   end;
   { ... }
   { 后面的画图片代码相同 }

  经过这个算法优化,运行时间缩短到几乎为0ms了(偶尔出现16ms)!

  < 收工 >

  总算写完了^^。以上算法是我自己琢磨出来的,网上也没找到什么相关资料,哪位朋友如果有更好的方法,请多多指点,也希望能和我联系(islet8@yahoo.com.cn)。希望这篇文章能给各位朋友起到抛砖引玉的作用!

   以上代码在WinXP + Delphi7下调试通过。

  • 上一篇:教你在DELPHI中如何调用系统对话框
  • 下一篇:用简单的Tracer类来为应用写入跟踪