windows - tag - ¿Cómo cargo iconos desde un recurso sin sufrir aliasing?
tag folders (1)
Tengo una aplicación de GUI que incluye una serie de iconos utilizados para botones de la barra de herramientas, glifos de menú, iconos de notificación, etc. Estos iconos están vinculados a la aplicación como recursos y están disponibles una variedad de tamaños diferentes. Normalmente, para las imágenes del botón de la barra de herramientas, tengo disponibles versiones de 16px, 24px y 32px. Mis íconos son 32bpp con transparencia parcial.
La aplicación tiene un alto reconocimiento de DPI y ajusta el tamaño de todos los elementos visuales de acuerdo con la escala de la fuente predominante. Por lo tanto, por ejemplo, al escalar 100% de fuente, 96 ppp, el tamaño del icono de la barra de herramientas es 16px. Con una escala de 125%, 120 ppp, el tamaño del icono de la barra de herramientas es 20px. Necesito poder cargar un icono de tamaño 20px sin ningún efecto de aliasing. ¿Cómo puedo hacer esto? Tenga en cuenta que me gustaría apoyar Windows 2000 y versiones posteriores.
En Vista y hasta se agregaron una serie de funciones nuevas que hacen que esta tarea sea trivial. La función más adecuada aquí es LoadIconWithScaleDown
.
Esta función buscará primero en el archivo de icono un ícono que tenga exactamente el mismo tamaño. Si no se encuentra una coincidencia, a menos que tanto cx como cy coincidan con uno de los tamaños de icono estándar: 16, 32, 48 o 256 píxeles, se selecciona el siguiente ícono más grande y luego se reduce al tamaño deseado. Por ejemplo, si la aplicación Callign solicita un icono con una dimensión x de 40 píxeles, el icono de 48 píxeles se usa y se reduce a 40 píxeles. Por el contrario, la función LoadImage selecciona el icono de 32 píxeles y lo escala hasta 40 píxeles.
Si la función no puede ubicar un ícono más grande, tiene el comportamiento estándar de buscar el siguiente ícono más pequeño y escalarlo al tamaño deseado.
En mi experiencia, esta función hace un excelente trabajo de escalado y los resultados no muestran signos de aliasing.
Para las versiones anteriores de Windows, a mi leal saber y entender, no hay ninguna función que pueda realizar esta tarea adecuadamente. Los resultados obtenidos de LoadImage
son de muy mala calidad. En cambio, el mejor enfoque que he encontrado es el siguiente:
- Examine las imágenes disponibles en el recurso para encontrar la imagen con el tamaño más grande que es menor que el tamaño del icono deseado.
- Cree un nuevo ícono del tamaño deseado e inicialícelo para que sea completamente transparente.
- Coloque el ícono más pequeño del recurso en el centro del ícono nuevo (más grande).
Esto significa que habrá un pequeño borde transparente alrededor del ícono, pero generalmente esto es lo suficientemente pequeño para ser insignificante. La opción ideal sería utilizar un código que pudiera reducirse al igual que LoadIconWithScaleDown
, pero no es trivial de escribir.
Entonces, sin más preámbulos aquí está el código que uso.
unit uLoadIconResource;
interface
uses
SysUtils, Math, Classes, Windows, Graphics, CommCtrl;
function LoadIconResourceSize(const ResourceName: string; IconSize: Integer): HICON;//will not throw an exception
function LoadIconResourceMetric(const ResourceName: string; IconMetric: Integer): HICON;
implementation
function IconSizeFromMetric(IconMetric: Integer): Integer;
begin
case IconMetric of
ICON_SMALL:
Result := GetSystemMetrics(SM_CXSMICON);
ICON_BIG:
Result := GetSystemMetrics(SM_CXICON);
else
raise EAssertionFailed.Create(''Invalid IconMetric'');
end;
end;
procedure GetDIBheaderAndBits(bmp: HBITMAP; out bih: BITMAPINFOHEADER; out bits: Pointer);
var
pbih: ^BITMAPINFOHEADER;
bihSize, bitsSize: DWORD;
begin
bits := nil;
GetDIBSizes(bmp, bihSize, bitsSize);
pbih := AllocMem(bihSize);
Try
bits := AllocMem(bitsSize);
GetDIB(bmp, 0, pbih^, bits^);
if pbih.biSize<SizeOf(bih) then begin
FreeMem(bits);
bits := nil;
exit;
end;
bih := pbih^;
Finally
FreeMem(pbih);
End;
end;
function CreateIconFromSmallerIcon(IconSize: Integer; SmallerIcon: HICON): HICON;
procedure InitialiseBitmapInfoHeader(var bih: BITMAPINFOHEADER);
begin
bih.biSize := SizeOf(BITMAPINFOHEADER);
bih.biWidth := IconSize;
bih.biHeight := 2*IconSize;//height of xor bitmap plus height of and bitmap
bih.biPlanes := 1;
bih.biBitCount := 32;
bih.biCompression := BI_RGB;
end;
procedure CreateXORbitmap(const sbih, dbih: BITMAPINFOHEADER; sptr, dptr: PDWORD);
var
line, xOffset, yOffset: Integer;
begin
xOffset := (IconSize-sbih.biWidth) div 2;
yOffset := (IconSize-sbih.biHeight) div 2;
inc(dptr, xOffset + IconSize*yOffset);
for line := 0 to sbih.biHeight-1 do begin
Move(sptr^, dptr^, sbih.biWidth*SizeOf(DWORD));
inc(dptr, IconSize);//relies on the fact that no padding is needed for RGBA scanlines
inc(sptr, sbih.biWidth);//likewise
end;
end;
var
SmallerIconInfo: TIconInfo;
sBits, xorBits: PDWORD;
xorScanSize, andScanSize: Integer;
xorBitsSize, andBitsSize: Integer;
sbih: BITMAPINFOHEADER;
dbih: ^BITMAPINFOHEADER;
resbitsSize: DWORD;
resbits: Pointer;
begin
Result := 0;
Try
if not GetIconInfo(SmallerIcon, SmallerIconInfo) then begin
exit;
end;
Try
GetDIBheaderAndBits(SmallerIconInfo.hbmColor, sbih, Pointer(sBits));
if Assigned(sBits) then begin
Try
if (sbih.biWidth>IconSize) or (sbih.biHeight>IconSize) or (sbih.biPlanes<>1) or (sbih.biBitCount<>32) then begin
exit;
end;
xorScanSize := BytesPerScanline(IconSize, 32, 32);
Assert(xorScanSize=SizeOf(DWORD)*IconSize);
andScanSize := BytesPerScanline(IconSize, 1, 32);
xorBitsSize := IconSize*xorScanSize;
andBitsSize := IconSize*andScanSize;
resbitsSize := SizeOf(BITMAPINFOHEADER) + xorBitsSize + andBitsSize;
resbits := AllocMem(resbitsSize);//AllocMem zeroises the memory
Try
dbih := resbits;
InitialiseBitmapInfoHeader(dbih^);
xorBits := resbits;
inc(PByte(xorBits), SizeOf(BITMAPINFOHEADER));
CreateXORbitmap(sbih, dbih^, sBits, xorBits);
//don''t need to fill in the mask bitmap when using RGBA
Result := CreateIconFromResourceEx(resbits, resbitsSize, True, $00030000, IconSize, IconSize, LR_DEFAULTCOLOR);
Finally
FreeMem(resbits);
End;
Finally
FreeMem(sBits);
End;
end;
Finally
if SmallerIconInfo.hbmMask<>0 then begin
DeleteObject(SmallerIconInfo.hbmMask);
end;
if SmallerIconInfo.hbmColor<>0 then begin
DeleteObject(SmallerIconInfo.hbmColor);
end;
End;
Finally
DestroyIcon(SmallerIcon);
End;
end;
function LoadIconResourceSize(const ResourceName: string; IconSize: Integer): HICON;//will not throw an exception
function LoadImage(IconSize: Integer): HICON;
begin
Result := Windows.LoadImage(HInstance, PChar(ResourceName), IMAGE_ICON, IconSize, IconSize, LR_DEFAULTCOLOR);
end;
type
TGrpIconDir = packed record
idReserved: Word;
idType: Word;
idCount: Word;
end;
TGrpIconDirEntry = packed record
bWidth: Byte;
bHeight: Byte;
bColorCount: Byte;
bReserved: Byte;
wPlanes: Word;
wBitCount: Word;
dwBytesInRes: DWORD;
wID: WORD;
end;
var
i, BestAvailableIconSize, ThisSize: Integer;
ResourceNameWide: WideString;
Stream: TResourceStream;
IconDir: TGrpIconDir;
IconDirEntry: TGrpIconDirEntry;
begin
//LoadIconWithScaleDown does high quality scaling and so we simply use it if it''s available
ResourceNameWide := ResourceName;
if Succeeded(LoadIconWithScaleDown(HInstance, PWideChar(ResourceNameWide), IconSize, IconSize, Result)) then begin
exit;
end;
//XP: find the closest sized smaller icon and draw without stretching onto the centre of a canvas of the right size
Try
Stream := TResourceStream.Create(HInstance, ResourceName, RT_GROUP_ICON);
Try
Stream.Read(IconDir, SizeOf(IconDir));
Assert(IconDir.idCount>0);
BestAvailableIconSize := high(BestAvailableIconSize);
for i := 0 to IconDir.idCount-1 do begin
Stream.Read(IconDirEntry, SizeOf(IconDirEntry));
Assert(IconDirEntry.bWidth=IconDirEntry.bHeight);
ThisSize := IconDirEntry.bHeight;
if ThisSize=0 then begin//indicates a 256px icon
continue;
end;
if ThisSize=IconSize then begin
//a perfect match, no need to continue
Result := LoadImage(IconSize);
exit;
end else if ThisSize<IconSize then begin
//we''re looking for the closest sized smaller icon
if BestAvailableIconSize<IconSize then begin
//we''ve already found one smaller
BestAvailableIconSize := Max(ThisSize, BestAvailableIconSize);
end else begin
//this is the first one that is smaller
BestAvailableIconSize := ThisSize;
end;
end;
end;
if BestAvailableIconSize<IconSize then begin
Result := CreateIconFromSmallerIcon(IconSize, LoadImage(BestAvailableIconSize));
if Result<>0 then begin
exit;
end;
end;
Finally
FreeAndNil(Stream);
End;
Except
;//swallow because this routine is contracted not to throw exceptions
End;
//final fallback: make do without
Result := 0;
end;
function LoadIconResourceMetric(const ResourceName: string; IconMetric: Integer): HICON;
begin
Result := LoadIconResourceSize(ResourceName, IconSizeFromMetric(IconMetric));
end;
end.
Usar estas funciones es bastante obvio. Suponen que el recurso se encuentra en el mismo módulo que el código. El código podría generalizarse fácilmente para recibir un HMODULE
en caso de que necesite soporte para ese nivel de generalidad.
Llame a LoadIconResourceMetric
si desea cargar iconos de tamaño igual al icono pequeño del sistema o al icono grande del sistema. El parámetro IconMetric
debe ser ICON_SMALL
o ICON_BIG
. Para barras de herramientas, menús e iconos de notificación, se debe usar ICON_SMALL
.
Si desea especificar el tamaño del icono en términos absolutos, use LoadIconResourceSize
.
Estas funciones devuelven un HICON
. Por supuesto, puede asignar esto a la propiedad Handle
de una instancia de TIcon
. Es más probable que desee agregar a una lista de imágenes. La forma más sencilla de hacerlo es llamar a ImageList_AddIcon
pasando el Handle
de la instancia TImageList
.
Nota 1: las versiones anteriores de Delphi no tienen definido CommCtrl
en CommCtrl
. Para tales versiones de Delphi necesita llamar a GetProcAddress
para cargarlo. Tenga en cuenta que esta es una API única de Unicode, por lo que debe enviarle un PWideChar
para el nombre del recurso. De esta manera: LoadIconWithScaleDown(..., PWideChar(WideString(ResourceName)),...)
.
Nota 2: La definición de LoadIconWithScaleDown
es defectuosa. Si lo llama después de que se haya inicializado la biblioteca de controles comunes, entonces no tendrá problemas. Sin embargo, si llama a la función desde el principio en la vida de su proceso, entonces LoadIconWithScaleDown
puede fallar. Acabo de presentar QC#101000 para informar este problema. Nuevamente, si está afligido por esto, entonces tiene que llamar a GetProcAddress
usted mismo.