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.
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.
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?
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.
Pingback: Learn How To Resize TImageList Bitmaps to Fit High-DPI Scaling Size (for Menus, … – HengeDK
Thanks, this looks great!
But for some reason it’s not working for me.
I have 36X36 images, but after running the stretch or centre, I’m just getting a few weird pale shapes.
It almost looks as if the mask and image are getting placed into one squashed image as the left hand side has some pale ghostly shapes, and the right hand side nothing.
After a lot of fiddling around (eventually ran at 96 DPI design time and run time, just copying the list out and back again) the best I could get was the top left half of the logo showing.
Not sure what is different on my bitmaps to yours (can’t see anywhere I could upload my TImageList?) Or a sample pic.
Sorry about not being able to provide you with anything concrete, but any ideas? Just debugging is a nightmare, as you can’t see which if the 3 stages are losing the images.
Using Delphi 10.2 Berlin
Solved?
Yes, thanks, I was creating the highDPIImageListContainer at runtime. Not sure what size it was defaulting to, but it made everything go haywire. Came right when I set it to the same size as the image list I was trying to resize.
Obvious when you spot it, but had me scratching my head for a bit!
Thanks, this looks great!
Maybe just make a note to make sure the size of the HighDPIImageListContainer matches the original size of the imgList (or call SetSize() in your code) else really strange things happen to your images.
Yes, thanks for noticing.
I was suffering exactly the same problem as you have described. Your technique works a treat. Elegant and simple. Thank you.
ImageList_DrawEx
where do I find that procedure?
Kind regards
Lasse 🙂
Hi,
unit Winapi.CommCtrl
-žarko