又有新瓶装旧酒——使用Delphi实现图片界面换肤色 [原创] (2004-8)[1]

[入库:2005年8月18日] [更新:2007年3月24日]

本文简介:选择自 islet8 的 blog

< 想法 >

    软件的“换肤”技术早已不是什么新鲜事了,但细心的朋友一定已经发现了,现在正悄悄地流行了一种新的改善视觉效果的方法——这里我斗胆定义为“换肤色”技术吧!用过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)。
图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图片盖到已有的位图上去)。
图2

    说了这么多,现在该考虑我们的重点内容“换肤色”了。我考虑的基本原理是:先将所有界面相关的图片都做成灰阶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 的值

本文关键:又有新瓶装旧酒——使用Delphi实现图片界面换肤色 [原创] (2004-8)
  相关方案
Google
 

本站最佳浏览方式为 分辨率 1024x768 IE 6.0(或更高版本的 IE浏览器)

go top