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