PiedSoftware 5 Posted August 20 (edited) Hi I want to be able to expand a small modal dialog that offers the user just a combo box to the minimum size able to show all values in the dropdown list of a TComboBox. I wrote the following code to calculate the size required by the combobox: function MaxTextWidth(canvas: TCanvas; list: TStrings): integer; var s: string; begin result := 0; for s in list do result := Max(result, canvas.TextWidth(s)); end; procedure SetComboWidth(Combo: TComboBox); var width: integer; begin width := MaxTextWidth(Combo.Canvas, Combo.Items) + 26; // Include arrow button if Combo.Style = csDropDown then inc(width, 8); Combo.Width := width; end{ SetComboWidth}; It had the desired effect on my machine, a NUC box still on good ole' Windows 10. The users however on Windows 11 find that the calculated with is not enough. The longest values were clipped. I don't know if the OS is the problem. My machine doesn't let me update. Does this problem look familiar to anyone? -- Mark Edited August 20 by PiedSoftware Share this post Link to post
PeaShooter_OMO 42 Posted August 20 It is always a good idea to not hard code any widths related to the visual apsects of controls. They might change when the OS changes version. You have to get the width of the non-client area of a ComboBox so that you can add it your calculated MaxTextWidth to get a proper control width. But... ComboBox does not play nicely and getting its ClientWidth is not possible with functions like GetClientRect thus ComboBox does not have a properly calculated ClientWidth for it so you will have to make another plan to get it. There is a Win32 function called GetComboBoxInfo. It returns a record with details about the combobox like size of the control, the size and position of the button and size of position of the edit control in it. You will use the width of the edit control and deduct that from the width of the ComboBox and there you have the non-client area width. Just add that to the MaxTextWidth and you have the size the ComboBox should be. Remember to do these calculations and setting of the ComboBox size again when the Windows theme or settings change or the font changes. 2 Share this post Link to post
MarkShark 27 Posted August 20 Another possible issue is that when you use a control's canvas outside the paint event, the font of that canvas may not be set properly. It's good practice to set it explicitly with something like: Combo.Canvas.Font := Combo.Font; before calling TextWidth. Also note that you may have to scale any hard coded pixel values based on dpi. Share this post Link to post
PiedSoftware 5 Posted August 22 On 8/20/2025 at 8:24 PM, MarkShark said: Another possible issue is that when you use a control's canvas outside the paint event, the font of that canvas may not be set properly. It's good practice to set it explicitly with something like: Combo.Canvas.Font := Combo.Font; before calling TextWidth. Also note that you may have to scale any hard coded pixel values based on dpi. I tried this, which is similar to what you said, and it seemed to make no difference: procedure SetComboWidth(Combo: TComboBox); var width: integer; begin Combo.Canvas.Font := Combo.Font; //<<<<< width := MaxTextWidth(Combo.Canvas, Combo.Items) + 26; // Include arrow button if Combo.Style = csDropDown then inc(width, 8); Combo.Width := width; end{ SetComboWidth}; Share this post Link to post
David Heffernan 2472 Posted August 23 You didn't account for any font scaling. So if the user has a high dpi screen and a font scaling larger than 100% the your 26 and 8 won't be right. Then again, because you didn't provide full code and details we are all guessing a bit. 1 Share this post Link to post
HeartWare 9 Posted Monday at 09:31 AM For scaling pixel values, I use this helper function: {$IFNDEF CPUX86 } {$IFNDEF CPUX64 } {$DEFINE PUREPASCAL } {$ENDIF } {$ENDIF } CONST DefDPI = WinAPI.Windows.USER_DEFAULT_SCREEN_DPI; {$IFDEF PUREPASCAL } FUNCTION Scale(Pixels,OldDPI,NewDPI : NativeInt) : NativeInt; BEGIN IF OldDPI<>NewDPI THEN BEGIN IF OldDPI<>DefDPI THEN Pixels:=MulDiv(Pixels,DefDPI,OldDPI); IF NewDPI<>DefDPI THEN Pixels:=MulDiv(Pixels,NewDPI,DefDPI) END; Result:=Pixels END; {$ELSEIF DEFINED(CPUX64) } FUNCTION Scale(Pixels{RCX},OldDPI{RDX},NewDPI{R8} : NativeInt) : NativeInt; ASSEMBLER; ASM MOV RAX,Pixels CMP OldDPI,NewDPI JE @OUT MOV ECX,DefDPI // Also clears upper 32 bits of RCX MOV R9,OldDPI CMP OldDPI,RCX JE @SKIP1 IMUL RAX,RCX IDIV R9 @SKIP1: CMP NewDPI,DefDPI JE @OUT IMUL NewDPI IDIV RCX @OUT: END; {$ELSEIF DEFINED(CPUX86) } FUNCTION Scale(Pixels{EAX},OldDPI{EDX},NewDPI{ECX} : NativeInt) : NativeInt; ASSEMBLER; ASM CMP OldDPI,NewDPI JE @OUT CMP OldDPI,DefDPI JE @SKIP1 PUSH NewDPI MOV ECX,OldDPI IMUL EAX,DefDPI IDIV ECX POP NewDPI @SKIP1: CMP ECX,DefDPI JE @OUT IMUL NewDPI MOV ECX,DefDPI IDIV ECX @OUT: END; {$ENDIF } And these CLASS HELPERs: CLASS FUNCTION TMonitorHelper.Scale(Pixels,OldDPI,NewDPI : NativeInt) : NativeInt; BEGIN Result:=HeartWare.VCL.Funcs.Scale(Pixels,OldDPI,NewDPI) // The base function from previous code block // END; FUNCTION TMonitorHelper.Scale(Value : Integer) : Integer; BEGIN Result:=Scale(Value,Screen.DefaultPixelsPerInch,PixelsPerInch) END; FUNCTION TFormHelper.Scale(Pixels,OldDPI,NewDPI : NativeInt) : NativeInt; BEGIN Result:=Monitor.Scale(Pixels,OldDPI,NewDPI) END; FUNCTION TFormHelper.Scale(Value : Integer) : Integer; BEGIN Result:=Monitor.Scale(Value) END; Then I just use Form1.Scale(8) to get a 96dpi 8-pixel scaled value at whatever scale factor the form (or rather, the monitor upon which the form is located) currently is at. Share this post Link to post
Anders Melander 2096 Posted Monday at 12:13 PM 2 hours ago, HeartWare said: I use this helper function: Without having timed this my guess is that your assembler version would be faster if you simply got rid of all the "optimizations" and just did the IMUL+IDIV. Also, newer versions of Delphi already have all this stuff in TControl. Share this post Link to post
David Heffernan 2472 Posted Monday at 02:20 PM 4 hours ago, HeartWare said: Then I just use Form1.Scale(8) to get a 96dpi 8-pixel scaled value at whatever scale factor the form (or rather, the monitor upon which the form is located) currently is at. You definitely don't want to be doing this when the VCL provides exactly this functionality. Share this post Link to post
HeartWare 9 Posted Tuesday at 09:49 AM 19 hours ago, David Heffernan said: You definitely don't want to be doing this when the VCL provides exactly this functionality. Okay - then I'll just do it the way you described... Oh, Wait! You didn't! Guess that means I'll continue doing it the way I've always done it... Share this post Link to post
HeartWare 9 Posted Tuesday at 10:04 AM (edited) 21 hours ago, Anders Melander said: Also, newer versions of Delphi already have all this stuff in TControl. Didn't know of the "ScaleValue" but it doesn't say from the description that it always scales the value as given from 96dpi to the current scale. It seems like it has the usual weakness of being an accumulated (floating point) scaling factor, which means that it'll slowly go out of sync if you move the form back and forth between various DPI settings. I prefer my solution (or David's if he ever gives it and it is different from this (IMO faulty) implementation). From what I can deduce from the source, it also scales from the control's original scaling factor being 1, so if the .DFM file is saved in 150% it scales out from that size (144dpi) and not from 100% (96dpi). I may be wrong on this, but that's what it seems to me from the source code. Edited Tuesday at 10:10 AM by HeartWare Share this post Link to post
HeartWare 9 Posted Tuesday at 10:09 AM 21 hours ago, Anders Melander said: Without having timed this my guess is that your assembler version would be faster if you simply got rid of all the "optimizations" and just did the IMUL+IDIV. What "optimizations" are you referring to? My test for unnecessary DIV/MUL when calculating to/from 96dpi? I always go via 96dpi (in the generalized function) as this will ensure that the scaled value is predictable and always the same, regardless of what the current DPI is (I get a more precise and consistent value if I go 150->96->200 than if I go from 150->200 directly - assuming that the 150 was a calculated value from 96 originally. Maybe not with 150/200 exactly, but other combinations can lead to rounding errors) Share this post Link to post
Anders Melander 2096 Posted Tuesday at 10:13 AM 4 minutes ago, HeartWare said: My test for unnecessary DIV/MUL when calculating to/from 96dpi? Yes Share this post Link to post
HeartWare 9 Posted Tuesday at 10:17 AM I'm not disagreeing (don't have the knowledge to do that), but do you really think that a MUL/DIV sequence takes less time than a JMP? I know that the newer processors have all this read-ahead and stuff, but (and I honestly don't know) does it really slow it down that much to flush the read-ahead cache and the pipeline? I'm not contradicting you in any way - just surprised (but my assembly knowledge is primarily back from the Pentium age, so I'm not fluent in all the new optimizations done on-chip in modern CPUs). Share this post Link to post
David Heffernan 2472 Posted Tuesday at 08:11 PM 10 hours ago, HeartWare said: Didn't know of the "ScaleValue" but it doesn't say from the description that it always scales the value as given from 96dpi to the current scale. Yes, ScaleValue is it. Share this post Link to post
Anders Melander 2096 Posted Wednesday at 01:26 AM 14 hours ago, HeartWare said: but do you really think that a MUL/DIV sequence takes less time than a JMP? As I said, I haven't timed it - and it depends. MUL is dirt cheap. DIV is a little more expensive but not nearly as bad as it once was. Branches are almost always bad but if OldDPI=NewDPI most of the time then the jump out is obviously worth it. I honestly wouldn't bother doing this in asm in the first place. Unless you are using it to scale graphics in real-time, or something like that, then it's a completely wasted effort. Also remember that pascal code can be inlined (avoiding the call overhead), while asm functions can't. I would replace the MulDiv with a simple expression; You likely don't need the 64-bit and overflow handling baked into MulDiv (which is a Windows API function, btw - not cheap). Share this post Link to post
PiedSoftware 5 Posted Wednesday at 02:22 AM (edited) On 9/8/2025 at 10:13 PM, Anders Melander said: Also, newer versions of Delphi already have all this stuff in TControl. Which version introduced that? It sounds useful. I am on 10.4, but I don't think we are likely to upgrade. The product is going to be replaced by a web app in C# Edited Wednesday at 02:23 AM by PiedSoftware Share this post Link to post
Anders Melander 2096 Posted Wednesday at 09:02 AM 6 hours ago, PiedSoftware said: Which version introduced that? Delphi 11 Share this post Link to post
David Heffernan 2472 Posted Wednesday at 11:36 AM 10 hours ago, Anders Melander said: Unless you are using it to scale graphics in real-time, or something like that, then it's a completely wasted effort. Also remember that pascal code can be inlined (avoiding the call overhead), while asm functions can't. I would replace the MulDiv with a simple expression; You likely don't need the 64-bit and overflow handling baked into MulDiv (which is a Windows API function, btw - not cheap). There's absolutely no scenario where the performance of these scaling functions could be important. It's just blind asm for the sake of it, premature optimisation 101 Share this post Link to post