Resizing TImageList Bitmaps to Fit High-DPI Scaling Size (for Menus, Toolbars, Trees, etc.)

high dpi menu bitmap rescaling
So, you’ve made your Delphi application high-DPI aware and after a few manual fixes the UI looks more or less usable on 4K displays having logical DPI values set to more than 100% (96 DPI). However, you open up the application’s main menu (or any popup menu) set to display images from an image list – and your fancy images appear super small (or are not drawn at all when you move your mouse over items)? The same small images appear on toolbars? You then note buttons having their Glyph property set to display some 16×16 pixels graphics – caption font is ok, but the glyph is also barely visible. Now what? How to have those images at the correct size for the applied DPI scaling?

Say you run your application on a 15’’ laptop with resolution set to 3480×2160 and scaling set to 250% (240 DPI) – the ideal glyph size for this is 40×40 pixels as 250% of 16 is 40. Do you have all the images/bitmaps you use in your application in 40×40 px size? Do you have them in 20×20 (125%) and 24×24 (150%) and 32×32 (200%) and 48×48 (300%) and so on .. ? I surely do not.

delphi button glyphs I’ve started developing this particular application of mine some 10+ years ago. At that time I’ve used bitmaps shipped with Delphi and those are 16 x 16 pixels (actually 32×16 as the image has the mask in it). I’ve used those for menus, toolbars, buttons and all around my UI. I’m no graphics guru – have no plans (read: time) to redraw all those glyphs to have them in various pixel perfect sizes. I guess most of you are in the same position.

Now, and again, ideally you would want to display pixel perfect 40×40 bitmap when on 250% DPI. When I say pixel perfect I mean you would want to have a glyph that is not upsized or downsized by resizing – as stretching a bitmap will suffer from aliasing effect. If you have your glyphs in all needed sizes you can accomplish this by storing all the images you use in your UI directly inside your executable as a resource, then depending on the DPI scaling load the needed size and apply to UI.

As said, I do not have those glyphs in various needed sizes – I’m stuck with 16×16 pixels. BUT, I still want my application to look usable even on 250% DPI scaling. The only solution I’ve come up is to programmatically resize (or more precisely upsize) depending on the logical DPI setting. Yes, this suffers from aliasing – but I can live with it – it’s good enough for me so I hope it’s good enough for you. <aside>Anybody hearing Dodgy’s song or is it only me :)</aside>

For me, the whole “my icons/glyphs/images appear small“ issue boils down to resizing the bitmaps in image lists (TImageList) and those set for the Glyph property for TBitBtn and TSpeedButton.

Resizing TImageList Bitmaps to Fit High DPI scaling

So, without further ado, here’s what I did with TImageList components: I’m upsizing images to fit the needed size depending on the logical DPI.

The thing is that, yes, aliasing will happen, and your 16×16 px bitmaps when stretched to 40×40 would not look their best. But on 15 inch 4K display when on 250% DPI – 40 pixels look (“eye testing”) as small/big as 16 pixels on 24 inch Full HD 100% DPI (96).

Actually, I’m up-sizing image list bitmaps when logical DPI > 150%, when <= 150% I’m simply centering the 16x16 image into the needed actual size. For example, when on 150% logical DPI, the 16x16 should be 24x24 – I’m simply centering my original 16x16 into 24x24. For me, after testing on various devices (read: screen resolutions) and various logical DPI settings, this works okish. Luckily, all my TImageList components are inside a TDataModule. I’ve added one more empty image list, called it “highDPIImageListContainer”. Here’s the ResizeImageListImagesforHighDPI procedure code:

procedure ResizeImageListImagesforHighDPI(const imgList: TImageList);
const
  DevImgSIZE = 16;
var
  ii : integer;
  mb, ib, sib, smb : TBitmap;
begin
  if Screen.PixelsPerInch = 96 then Exit;

  //clear images
  highDPIImageListContainer.Clear;

  //add from source image list
  for ii := 0 to -1 + imgList.Count do
    highDPIImageListContainer.AddImage(imgList, ii);

  //set size to match DPI size (like 250% of 16px = 40px)
  imgList.SetSize(MulDiv(DevImgSIZE, Screen.PixelsPerInch, 96), MulDiv(DevImgSIZE, Screen.PixelsPerInch, 96));

  //add images back to original ImageList stretched (if DPI scaling > 150%) or centered (if DPI scaling <= 150%)
  for ii := 0 to -1 + highDPIImageListContainer.Count do
  begin
    sib := TBitmap.Create; //stretched (or centered) image
    smb := TBitmap.Create; //stretched (or centered) mask
    try
      sib.Width := imgList.Width;
      sib.Height := imgList.Height;
      sib.Canvas.FillRect(sib.Canvas.ClipRect);
      smb.Width := imgList.Width;
      smb.Height := imgList.Height;
      smb.Canvas.FillRect(smb.Canvas.ClipRect);

      ib := TBitmap.Create;
      mb := TBitmap.Create;
      try
        ib.Width := DevImgSIZE;
        ib.Height := DevImgSIZE;
        ib.Canvas.FillRect(ib.Canvas.ClipRect);

        mb.Width := DevImgSIZE;
        mb.Height := DevImgSIZE;
        mb.Canvas.FillRect(mb.Canvas.ClipRect);

        ImageList_DrawEx(highDPIImageListContainer.Handle, ii, ib.Canvas.Handle, 0, 0, ib.Width, ib.Height, CLR_NONE, CLR_NONE, ILD_NORMAL);
        ImageList_DrawEx(highDPIImageListContainer.Handle, ii, mb.Canvas.Handle, 0, 0, mb.Width, mb.Height, CLR_NONE, CLR_NONE, ILD_MASK);

        if Screen.PixelsPerInch * 100 / 96 <= 150 then //center if <= 150%
        begin
          sib.Canvas.Draw((sib.Width - ib.Width) DIV 2, (sib.Height - ib.Height) DIV 2, ib);
          smb.Canvas.Draw((smb.Width - mb.Width) DIV 2, (smb.Height - mb.Height) DIV 2, mb);
        end
        else //stretch if > 150%
        begin
          sib.Canvas.StretchDraw(Rect(0, 0, sib.Width, sib.Width), ib);
          smb.Canvas.StretchDraw(Rect(0, 0, smb.Width, smb.Width), mb);
        end;
      finally
        ib.Free;
        mb.Free;
      end;

      imgList.Add(sib, smb);
    finally
      sib.Free;
      smb.Free;
    end;
  end;
end;

What I’m doing here: I’m copying all images from a 16×16 image list to the highDPIImageListContainer. The Size (actually Width/Height) property of the original image list is set to match the needed size per the logical DPI setting (note: SetSize clears the image list). Finally, resizing (/upsizing) or centering all the images and adding them back to the original image list.

When the application starts I simply check the Screen.PixelsPerInch value and act if needed, by calling:

  //”dm” is the name of the TDataModule instance
  for i := 0 to -1 + dm.ComponentCount do
    if dm.Components[i] is TImageList then
      ResizeImageListImagesforHighDPI(TImageList(dm.Components[i]));

That’s it, no magic here. As I said, not ideal, but works for me.

The final result is in the image on top. The left side is when images are displayed as 16×16 and the right side is when they are upsized to 40×40 (250% logical DPI). Also, images in the previous post display how this looks in a sample application.

Menu Drawing

Now, the glyphs in menu items look ok – they are not too small. Not too sharp either, but usable. However, when you move your mouse over menu items you’ll notice that images would disappear. That’s actually Delphi not handling menu drawing how it should be done. Nothing to do here, except wait for Embarcadero to fix.

“Fixing” TBitBtn.Glyph / TSpeedButton.Glyph and alike

Here goes: Making the Glyph Property High DPI Aware for TBitBtn and TSpeedButton.

3 thoughts on “Resizing TImageList Bitmaps to Fit High-DPI Scaling Size (for Menus, Toolbars, Trees, etc.)

  1. Silver Warior

    Why are you stretching each individual ImageList image separately? That can be pretty slow.

    Are you aware that TImageList stores all its images in two large bitmaps (one for images and one for image masks) and that you can access these bitmaps by getting their handles using GetImageBitmap and GetMaskBitmap methods.

    So with that in mind you could achieve same results following these steps:
    1. Create two bitmaps. One for storing copy of ImageList ImageBitmap and another for storing copy of ImageList MaskBitmap
    2. Copy contents of ImageList ImageBitmap to previously created bitmap for storing ImageBitmap copy. You can access the ImageList ImageBitmap by getting its handle with GetImageBitmap method.
    3. Do same for ImageList MaskBitmap.
    4. Change the size of ImageList images using SetSize method
    5. Resize the ImageList ImageBitmap to fit the stretched images and StretchDraw the contents from ImageBitmap copy that you make previously.
    6. Do same with MaskBitmap

    This approach would allow you to resize all ImageList images at once and would thus be much faster than your approach. But it can’t be used for when you are just centering (repositioning) images.

    BTW: When copying all images from one image list to another why don’t you use AddImages method instead of calling AddImage multiple times in a loop?

    Reply
    1. zarkogajic Post author

      Hi Silver.
      I’m not sure about your GetImageBitmap/GetMaskBitmap idea. In step 6, how do you assign the stretched image back to the image list (there’s no SetImageBitmap or alike I know of)?

      I’m not using AddImages as this will copy images in their original size and not stretched / centered.

      Reply
  2. Pingback: Learn How To Resize TImageList Bitmaps to Fit High-DPI Scaling Size (for Menus, … – HengeDK

Leave a Reply

Your email address will not be published. Required fields are marked *