程序功能图标资源res打包处理

PNG图标是个好东西,现在系统都支持而且工具软件都能很方便生成(包括PS),要比做Icon方便很多。因此理所当然的现在项目图标PNG已经霸占了ICON的霸主地位。

项目功能比较多的时候,就会有无数的图标资源。现做的项目功能图标单16x16规格已经有200个之多~~,一个功能一个PNG图标,散落在目录中(俺们的美工喜欢做PNG图标,不太喜欢做icon)。虽然有RC资源管理着,但程序第一次加载大量功能图标时,明显会感觉有些卡的感觉。实际性能测试中也确实反映出这个情况,加载功能图标耗时严重。

问题: 图标过多的时候时候出现加载程序慢
解决: 通过合并图标一次性加载,提高效率

处理图标方案:
第一种:直接加入RC资源
第二种:把一组资源拼接成单个文件

开发环境:
XE3
Win7

第一种:直接加入RC资源

一般处理功能图标的方法,把图标分组编号。如16x16规格的功能图标分成ACTION16的类型图标组,加载时枚举所有的Action16类型组资源名称,读入ImageList中。

处理过程:
1、枚举组资源名
2、加载PNG资源 x N次
3、转换成BMP带Alpha通道 x N次
4、装入ImageList x N次

通常资源处理的方法

; 功能图标  16x16
; ------------------------
ICON01  ACTION16 .\16\NewFile.png
ICON02  ACTION16 .\16\SaveFile.png
... ...
ICONxx  ACTION16 .\16\xx.png

这种方式在资源比较少的时候没什么大问题,但资源一多时损失的效率就显现出来。主要消耗是在资源加载过程中产生的性能损失。

提示: 从资源文件读入会有一定的效率损失,过多会放大导致感觉卡顿。

第二种:把一组资源拼接成单个文件

这种方式的好处显而易见,不需要枚举名称,只有一次读资源的过程,读出来的图标直接装入ImageList,由ImageList自动切割成16规格的图标。这效率是杠杠的~。

不过这种方式还是有个制作问题。这么多资源图标通常不是一次全部完成的,除了些基本的,其他的都是有了功能才有图标。功能图标样式修改,都要重新拼接。这个人力成本很大,再说了让俺们的美工MM做这事于心不忍啊。

问题: 需要由美工MM处理成单个带透明通道的bmp文件,对美工要求比较高。

解决方案合并图标问题

介于上面这两种情况,第一种效率慢、第二种费时费力。为解决问题本人又比较懒,所以想方法就是做个预处理。结合第一种RC管理的方法是用程序自动拼接,生成单个图标组资源文件。

如上面的16x16图标的RC内容独立出来一个配置文件(icons16.lst),维护图标的资源索引(程序内部有常量对应)。

图标拼接列表配置文件(icons16.lst):

; icons16.lst
;
; 功能图标  16x16
; ------------------------
.\16\NewFile.png
.\16\SaveFile.png
... ...
.\16\xx.png

然后通过程序读取这个列表,合并图标资源并产生相应的常量定义代码。这事太美了,一箭双雕!哈~哈~哈~哈~~~

解决:资源加载慢,人工合并费时费力还不用手工维护代码常量表。

拼接合成处理过程

1、读取定义列表
2、根据图标数量生成合并后的资源图标尺寸
3、依次读入,绘制到相应偏移位置。
4、压缩合并的资源图片并保存
OK~ 完成

procedure TMergeSrv.Exec;
var
  cConvert: TConvertRes;
begin
  if FDataFile.ReadFileName then
  begin
    case FDataFile.Kind of
      dtIconMerge : cConvert := TMergeIcons.Create(FDataFile);
      dtPngPack   : cConvert := TPngPack.Create(FDataFile);
      else          cConvert := nil;
    end;

    if cConvert <> nil then
    begin
      try
        if cConvert.Exec(PrintMsg) then
          if SaveResMap(cConvert.FIconMap) then
            PrintMsg(format('Finish: %s',[ChangeFileExt(FDataFile.OutFileName, '.IconPack')]));
      finally
        cConvert.Free;
      end;
    end
    else
      PrintMsg('Err: ' + MSG_NONAMES);
  end
  else
    PrintHelp;
end;

1、读取参数定义列表

使用那个图标组索引定义,输出到哪里。主要有一个2个比较细节的地方。

第一个:输出路径需要转换成完整路径(如: .\action16.pack)。
第二个资源定义文件位置需要设成当前路径。

这两个处理主要是为了简化PNG图标文件的读取。

function TParams.ReadFileName: Boolean;
var
  sFileName: string;
  sPath: string;
begin
  Result := False;
  FileName := '';

  // 从参数读取资源图标维护列表
  sFileName := ChangeFileExt(ParamStr(0), '.lst');
  if ParamCount >= 1 then
    sFileName := Trim(ParamStr(1));
  if FileExists(sFileName) then
    FileName := sFileName;

  // 从第二个参数中读取需要输出的资源包名称
  // 情景:1、没有第二个参数,默认使用配置文件名
  //       2、第二个参数是个路径,作为输出路径,文件名同配置名。
  //       3、有明确输出文件名,直接使用。
  OutFileName := ChangeFileExt(FileName, '.bmp');
  if ParamCount >= 2 then
  begin
    sFileName := Trim(ParamStr(2));
    if (sFileName <> '') then
    begin
      if (sFileName[Length(sFileName)] = '\') then 
        OutFileName := Format('%s%s',[sFileName, ExtractFileName(OutFileName)])
      else
      begin
        OutFileName := sFileName;
        if not DirectoryExists(ExtractFilePath(sFileName)) then
          if not CreateDir(ExtractFilePath(sFileName)) then
            OutFileName := '';
      end;
    end;
  end;

  // 把输出文件变成完整路径,为简化后续PNG资源的加载
  if OutFileName <> '' then
    OutFileName := ExpandFileName(OutFileName);

  /// 设置当前处理目录,为简化后续图标资源的加载
  if FileName <> '' then
  begin
    sPath := ExtractFilePath(FileName);
    SetCurrentDir(sPath);
    FileName := ExtractFileName(FileName);
  end;

  // 
  if SameText(ExtractFileExt(FileName), '.lst') then
    Kind := dtIconMerge
  else
    Kind := dtPngPack;

  Result := (FileName <> '') and (OutFileName <> '');
end;

读入参数设置的配置文件

function TMergeIcons.LoadImageNames: Boolean;
var
  I: Integer;
  sVal: string;
begin
  // 读取配置文件
  //   清除空白行和注释行
  FFiles := TStringList.Create;
  FFiles.LoadFromFile(SourceFile);
  for I := FFiles.Count - 1 downto 0 do
  begin
    sVal := Trim(FFiles[i]);
    if (sVal = '') or (sVal[1] = ';') or (sVal[1] = '/') then
      FFiles.Delete(i)
    else
      FFiles[i] := sVal;
  end;

  Result := FFiles.Count > 0;
end;

2、生成拼接图片的规格

procedure TMergeIcons.BuildResMap;
var
  bExists: Boolean;
  I: Integer;
begin
  // 预读图标文件尺寸
  FIcon := TPngImage.Create;
  bExists := False;
  for I := 0 to Count - 1 do
  begin
    bExists := LoadIcon(0);
    if bExists then
      Break;
  end;

  if not bExists then
    Exit;

  // 设置图标拼接行列数
  FColCnt := 10;
  FRowCnt := Count div FColCnt;
  if Count mod FColCnt > 0 then
    inc(FRowCnt);

  FWidth := FIcon.Width;
  FHeight:= FIcon.Height;

  BuildMap(FWidth * FColCnt, FHeight * FRowCnt);
end;

生成带有Alpha通道的透明Bitmap

procedure TConvertRes.BuildMap(w, h:Integer);
begin
  FIconMap := TBitmap.Create;
  FIconMap.PixelFormat := pf32bit;
  FIconMap.alphaFormat := afIgnored;
  FIconMap.SetSize(w, h);
  // Alpha 透明化
  FIconMap.Canvas.Brush.Color := clBlack;
  FIconMap.Canvas.FillRect(Rect(0, 0, FIconMap.Width, FIconMap.Height));
end;

3、并入资源图标

for I := 0 to Count - 1 do
 begin
   if LoadIcon(i) then
   begin
     MergeIcon(i);
     PrintMsg(format('ok:并入资源(%d)%s', [i, FileNames[i]]));
   end
   else
     PrintMsg(format('Err: 无法加载 (%d)%s 文件', [i, FileNames[i]]));
 end;
function TMergeIcons.LoadIcon(AIndex: Integer): Boolean;
begin
  try
    Result := False;
    if FileExists(FileNames[AIndex]) then
    begin
      FIcon.LoadFromFile(FileNames[AIndex]);
      Result := not FIcon.Empty;
    end;
  except
    Result := False;
  end;
end;

MergeIcon 根据索引进行偏移并入

function TMergeIcons.MergeIcon(AIndex: Integer): Boolean;
var
  iCol: Integer;
  iRow: Integer;
begin
  Result := True;
  // 按照索引进行偏移并入
  iRow := AIndex div FColCnt;
  iCol := AIndex mod FColCnt;
  FIconMap.Canvas.Draw(FWidth * iCol, FHeight * iRow, FIcon);
end;

4、压缩资源并输出

function TMergeSrv.SaveResMap(ASource: TBitmap): Boolean;
var
  cData: TMemoryStream;
  cPack: TZCompressionStream;
begin
  Result := False;
  if ASource = nil then
    Exit;
  if not DirectoryExists(ExtractFilePath(FDataFile.OutFileName)) then
    if not CreateDir(ExtractFilePath(FDataFile.OutFileName)) then
      Exit;

  // 把资源压缩到内存流中
  cData := TMemoryStream.Create;
  try
    // 生成一份对照Bitmap文件,用户检测合并文件是否有问题。
    ASource.SaveToStream(cData);
    cData.SaveToFile(FDataFile.OutFileName);
    cData.Clear;

    // 生成资源使用的压缩包文件
    cPack := TZCompressionStream.Create(clMax, cData);
    try
      ASource.SaveToStream(cPack);
    finally
      cPack.free;
    end;
    cData.SaveToFile(ChangeFileExt(FDataFile.OutFileName, '.IconPack'));

  finally
    cData.Free;
  end;
  Result := True;
end;

最终产生的效果

下面是个测试目录,16文件夹是存放所有16x16规格的图标。Icons16.ist文件用于维护功能图标组文件。

执行完处理产生的合并文件

完成的目标文件

最终资源文件会产生一个具有Alpha通道的Bitmap文件,Alpha通道就是一个Mark文件。程序中产生同Icon相应的透明效果。

加载资源时不需要任何处理,直接加入到ImageList。完美解决主程序的加载资源消耗时间过长问题。

还种更懒的方法,列表配置文件都不需要,直接读取目录内所有文件进行拼接。这种方式只要在开发时约定图标资源的使用方式就没问题。如按照 前缀_<文件名> 方式引用。不管代码还是外部配置,代码就是定义的常量,外部配置就是个字符串,加载时转换到常量定义值。这样内部资源顺序不管怎么变都使用都不会受影响。

最终的RC文件的内容

; 功能图标  16x16 
;   所有同类规格Action16图标变成一个资源, 
;   读取一次,在动态加载时可以一次装载到ImageList 
; ----------------------------------------
ACTION16 RCDATA .\Icons16.IconPack

通过这个方法保证了动态加载资源时的效率。

结束

完整工程代码: https://github.com/cmacro/simple/tree/master/MergeIconsRes

这种方案不仅对图标有效,对其他批量从资源读取内容的情况一样有效。

这个文档很久前些的,顺便搬过来,方便查找。