Как загружать значки из ресурса без наложения псевдонимов?

У меня есть приложение GUI, которое включает в себя несколько значков, используемых для кнопок панели инструментов, глифов меню, значков уведомлений и т.д. Эти значки привязаны к приложению как ресурсы и доступны различные размеры. Как правило, для изображений кнопок панели инструментов я имею 16px, 24px и 32px версии. Мои значки 32bpp с частичной прозрачностью.

Приложение имеет высокий уровень распознавания DPI и регулирует размер всех визуальных элементов в соответствии с преобладающим масштабированием шрифтов. Так, например, при 100% масштабировании шрифта, 96 точек на дюйм, размер значка панели инструментов составляет 16 пикселей. При 125% масштабировании, 120 точек на дюйм, размер значка на панели инструментов составляет 20 пикселей. Мне нужно иметь возможность загружать иконку размером 20 пикселей без каких-либо эффектов псевдонимов. Как я могу это сделать? Обратите внимание, что я хотел бы поддерживать Windows 2000 и более поздние версии.

Ответ 1

В Vista и добавлен ряд новых функций, которые делают эту задачу тривиальной. Функция, которая наиболее подходит здесь, LoadIconWithScaleDown.

Эта функция сначала начнет поиск файла значка для значка, имеющего точно такой же размер. Если совпадение не найдено, то, если оба cx и cy не совпадают с одним из стандартных размеров значков - 16, 32, 48 или 256 пикселей - выбирается следующий самый большой значок, а затем уменьшается до нужного размера. Например, если в приложении callign запрошен значок с размером x 40 пикселей, используется 48-пиксельный значок и уменьшен до 40 пикселей. Напротив, функция LoadImage выбирает 32-пиксельный значок и масштабирует его до 40 пикселей.

Если функция не может найти более крупный значок, по умолчанию используется стандартное поведение поиска следующего наименьшего значка и масштабирования его до нужного размера.

По моему опыту эта функция отлично справляется с масштабированием, и результаты не показывают признаков сглаживания.

Для более ранних версий Windows есть, насколько мне известно, ни одна функция, которая могла бы выполнить эту задачу адекватно. Результаты, полученные из LoadImage, имеют очень низкое качество. Вместо этого лучший подход, который я нашел, выглядит следующим образом:

  • Изучите доступные изображения в ресурсе, чтобы найти изображение с наибольшим размером, который меньше желаемого размера значка.
  • Создайте новый значок нужного размера и инициализируйте его полностью прозрачным.
  • Поместите значок меньшего размера из ресурса в центр нового (большего) значка.

Это означает, что вокруг значка будет небольшая прозрачная рамка, но обычно это достаточно мало, чтобы быть незначительным. Идеальным вариантом было бы использовать код, который мог бы уменьшаться так же, как и LoadIconWithScaleDown, но это нетривиально для записи.

Итак, без дальнейшего использования здесь используется код, который я использую.

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 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.

Использование этих функций совершенно очевидно. Они предполагают, что ресурс находится в том же модуле, что и код. Код может быть легко обобщен для получения HMODULE, если вам нужна поддержка этого уровня общности.

Вызовите LoadIconResourceMetric, если вы хотите загрузить значки размером, равным значку системы или значку системы. Параметр IconMetric должен быть либо ICON_SMALL, либо ICON_BIG. Для панелей инструментов следует использовать меню и значки уведомлений ICON_SMALL.

Если вы хотите указать размер значка в абсолютных терминах, используйте LoadIconResourceSize.

Эти функции возвращают HICON. Конечно, вы можете назначить это свойство Handle экземпляра TIcon. Скорее всего, вы захотите добавить в список изображений. Самый простой способ сделать это - вызвать ImageList_AddIcon передачу Handle экземпляра TImageList.

Примечание 1: В старых версиях Delphi нет LoadIconWithScaleDown, определенных в CommCtrl. Для таких версий Delphi вам необходимо вызвать GetProcAddress, чтобы загрузить его. Обратите внимание, что это API только для Unicode, поэтому вы должны отправить ему PWideChar для имени ресурса. Пример: LoadIconWithScaleDown(..., PWideChar(WideString(ResourceName)),...).

Примечание 2: Определение LoadIconWithScaleDown является ошибочным. Если вы вызываете его после инициализации библиотеки общих элементов управления, у вас не будет проблем. Однако, если вы вызовете функцию на ранней стадии жизни, тогда LoadIconWithScaleDown может выйти из строя. Я только что представил QС# 101000, чтобы сообщить об этой проблеме. Опять же, если вы страдаете от этого, вы должны сами вызвать GetProcAddress.