Recent

Author Topic: How to compute TComboBox.Width to fit for a certain text?  (Read 4885 times)

Hartmut

  • Hero Member
  • *****
  • Posts: 1131
I want to set the Width of a TComboBox automatically, so that it's Width fits for the longest text which I want to display there.

Suppose I know the longest text and compute it's length via TextWidth() and set this value to TComboBox.Width then the ComboBox is to small. I found out, that I must add an "offset" (I think for the Dropdown-Button), but it's value depends of the used Font and the Font's size.

E.g. on Windows with Font 'Times New Roman' this offset is 24 for Height=12 or is 27 for Height=22.
E.g. on Linux with Font 'DejaVu Sans Mono' this offset is 31 for Height=12 or is 42 for Height=22.
So we have other values for other Fonts and the difference is bigger.

Is it possible to compute this offset, if I know the Font and 'Height'? I want this for Windows and Linux.

Versions: Lazarus 2.0.6 / FPC 3.0.4 on Windows 7 (32-bit) and Ubuntu 18.04 (64-bit).
 
I attached a little demo, if you want to play with it:     

Code: Pascal  [Select][+][-]
  1. unit Unit1;
  2.  
  3. {$mode objfpc}{$H+}
  4.  
  5. interface
  6.  
  7. uses Classes, SysUtils, Forms, Controls, Dialogs, StdCtrls;
  8.  
  9. type
  10.  TForm1 = class(TForm)
  11.   CB: TComboBox;
  12.   Button_Close: TButton;
  13.   procedure FormActivate(Sender: TObject);
  14.   procedure Button_CloseClick(Sender: TObject);
  15.  private
  16.  public
  17.  end;
  18.  
  19. var Form1: TForm1;
  20.  
  21. implementation
  22.  
  23. {$R *.lfm}
  24.  
  25. procedure TForm1.FormActivate(Sender: TObject);
  26.    var s: string;
  27.        b,ofs: integer;
  28.    begin
  29.    CB.Font.Name:='Times New Roman';        // select a Font and a size:
  30. // CB.Font.Name:='Courier New';
  31. // CB.Font.Name:='DejaVu Sans Mono'; {on Linux only}
  32.    CB.Font.Height:=22; {try e.g. 12 and 22}
  33.    writeln('Name=', CB.Font.Name);
  34.    writeln('Height=', CB.Font.Height);
  35.  
  36.    s:='99.000.000.000';                    // compute the length for text 's':
  37.    CB.Canvas.Font.Assign(CB.Font); {without this TextWidth() is wrong}
  38.    b:=CB.Canvas.TextWidth(s);
  39.    writeln('b=', b);
  40.  
  41.    ofs:=27; {an offset is needed, depending of the Font and it's size}
  42.    writeln('ofs=', ofs);
  43.    CB.Width:=b+ofs;                        // set TComboBox.Width
  44.    CB.Text:=s;
  45.    end;
  46.  
  47. procedure TForm1.Button_CloseClick(Sender: TObject);
  48.    begin
  49.    Close;
  50.    end;
  51.  
  52. end.      

Thanks in advance.

wp

  • Hero Member
  • *****
  • Posts: 13491
Re: How to compute TComboBox.Width to fit for a certain text?
« Reply #1 on: July 21, 2020, 07:33:51 pm »
This works for me (Windows, but should be cross-platform):

Code: Pascal  [Select][+][-]
  1. uses
  2.   Math, LCLIntf, LCLType;
  3.  
  4. function Combobox_Autowidth(AComboBox: TComboBox): Integer;
  5. const
  6.   SPACING = 8;  // some pixels as margin
  7. var
  8.   i: Integer;
  9.   w: Integer;
  10. begin
  11.   w := 0;
  12.   AComboBox.Canvas.Font.Assign(AComboBox.Font);
  13.   for i := 0 to AComboBox.Items.Count-1 do
  14.     w := Max(AComboBox.Canvas.TextWidth(AComboBox.Items[i]), w);
  15.   Result := w + GetSystemMetrics(SM_CXVSCROLL) + SPACING;
  16. end;
  17.  
  18. { TForm1 }
  19.  
  20. procedure TForm1.FormShow(Sender: TObject);
  21. begin
  22.   Combobox1.Width := Combobox_AutoWidth(Combobox1);
  23. end;
« Last Edit: July 21, 2020, 07:35:41 pm by wp »

Hartmut

  • Hero Member
  • *****
  • Posts: 1131
Re: How to compute TComboBox.Width to fit for a certain text?
« Reply #2 on: July 21, 2020, 08:04:22 pm »
Hello wp,
thanks a lot for this function. A quick test on Linux with Lazarus 2.0.6 shows, that the computed width is too small. And the amount, how much it is too small, differs with Font.Height.
I must stop for today. Tomorrow I will test on Windows and report more detailed.

jamie

  • Hero Member
  • *****
  • Posts: 7663
Re: How to compute TComboBox.Width to fit for a certain text?
« Reply #3 on: July 22, 2020, 12:06:32 am »
Your pixel Per Inch settings is part of calculating the FONT height so if the two aren't the same between Linux and Windows you may get a difference.

 The SIZE = -Height * 72/PixelPerInch

  The Size are in POINTS. To get height you need to work that math backwards.
The only true wisdom is knowing you know nothing

Hartmut

  • Hero Member
  • *****
  • Posts: 1131
Re: How to compute TComboBox.Width to fit for a certain text?
« Reply #4 on: July 22, 2020, 10:06:54 am »
Hello wp, now I made some tests on Windows and here the computed width is too small, if Font Height grows (see attached screenshots which I made on Windows and Linux). 

On Windows you see, that Height=12 is perfect, but for Height=22 and 30 the last char is not complete. The width of the Dropdown-Button seems to be constant. But we see, that before the 1st char there is a "space", which grows with 'Height' and maybe this is the reason, that the end is not complete?

On Linux you see, that Width is always too small and the amount, how much it is too small, grows with Height. Here we don't have a growing "space" before the 1st char as on Windows. But here the width of the Dropdown-Button grows with Height and is Font-dependent (Default Font with Height=30 has some pixels more width than Font 'DejaVu Sans Mono' with same Height).

I modified function Combobox_Autowidth() to see the result of GetSystemMetrics():
Code: Pascal  [Select][+][-]
  1. var m: integer;
  2. ...
  3. m:=GetSystemMetrics(SM_CXVSCROLL);
  4. writeln('m=', m);
  5. Result := w + m + SPACING;

I saw that 'm' is always 16 (on Windows and Linux). But we don't need a constant correction offset, we need one, which depends on the selected Font and Font Height.
Can somebody help?

Here is my current FormShow() procedure:
Code: Pascal  [Select][+][-]
  1. procedure TForm1.FormShow(Sender: TObject);
  2.    begin
  3.    CB.Font.Name:='Times New Roman';
  4. // CB.Font.Name:='Courier New';
  5. // CB.Font.Name:='DejaVu Sans Mono'; {on Linux only}
  6.  
  7.    CB.Font.Height:=22; {try e.g. 12 / 22 / 30}
  8.    writeln('Font.Name=', CB.Font.Name);
  9.    writeln('Height=', CB.Font.Height);
  10.  
  11.    CB.Width:=Combobox_AutoWidth(CB);
  12.    writeln('CB.Width=', CB.Width);
  13.    writeln('PixelsPerInch=', Screen.PixelsPerInch); {is ALWAYS 96}
  14.    end;

I attached my updated demo, if you want to play with it. ComboBox 'CB' has 3 items of different length, which I added via Object Inspector.

Hartmut

  • Hero Member
  • *****
  • Posts: 1131
Re: How to compute TComboBox.Width to fit for a certain text?
« Reply #5 on: July 22, 2020, 10:11:14 am »
Your pixel Per Inch settings is part of calculating the FONT height so if the two aren't the same between Linux and Windows you may get a difference.
    The SIZE = -Height * 72/PixelPerInch
The Size are in POINTS. To get height you need to work that math backwards.

Thanks jamie for that suggestion. Did you mean 'Screen.PixelsPerInch'? It's always 96 (both on Windows and Linux).

howardpc

  • Hero Member
  • *****
  • Posts: 4144
Re: How to compute TComboBox.Width to fit for a certain text?
« Reply #6 on: July 22, 2020, 10:24:21 am »
If I understand correctly, Hartmut is wanting the un-dropped-down combo overall width to accommodate the largest item that its Text could show.
Accordingly, the width of a vertical scrollbar that might be needed in the dropdown (if DropDownCount is less than the item Count) is not relevant.
The following, slightly hackish routine works for me on Linux for a variety of font sizes. Perhaps it also gives correct results on Windows.
Code: Pascal  [Select][+][-]
  1. uses Themes;
  2. function Combobox_Autowidth(AComboBox: TComboBox): Integer;
  3. const
  4.   MARGIN = '               ';
  5. var
  6.   i, w: Integer;
  7. begin
  8.   Result := 0;
  9.   AComboBox.Canvas.Font.Assign(AComboBox.Font);
  10.   for i := 0 to AComboBox.Items.Count-1 do
  11.     begin
  12.       w := AComboBox.Canvas.TextWidth(MARGIN + AComboBox.Items[i]);
  13.       if w > Result then
  14.         Result := w;
  15.     end;
  16.   Inc(Result, ThemeServices.GetDetailSize(ThemeServices.GetElementDetails(tcDropDownButtonNormal)).cx);
  17. end;

wp

  • Hero Member
  • *****
  • Posts: 13491
Re: How to compute TComboBox.Width to fit for a certain text?
« Reply #7 on: July 22, 2020, 11:22:15 am »
now I made some tests on Windows and here the computed width is too small, if Font Height grows (see attached screenshots which I made on Windows and Linux). 

I tested on "real" Windows (you probably are on Wine?), and it works correctly, even with different font sizes.

I also ran Manjaro Linux in a VM (gtk2), and I confirm that my code does not work there. It does not seem to yield the correct result for GetSystemMetrics. Maybe another element of the long list of SM_*** constants is more appropriate, but I did not come to a quick solution. When I increases the font size the results became particularly weird because the font size in the control itself did increase while the font size in the dropdown window did not.

Yes, Howard's way of employing the theme services looks very promising; but I see problems here on Cocoa where my sparse experience warns that themeservices do not seem to work at all on macOS.

Hartmut

  • Hero Member
  • *****
  • Posts: 1131
Re: How to compute TComboBox.Width to fit for a certain text?
« Reply #8 on: July 22, 2020, 01:40:37 pm »
Thanks a lot to howardpc for this suggestion, but unfortunately it does not work on my Ubuntu 18.04 (64-bit) with Lazarus 2.0.6 / FPC 3.0.4. The width is always too big (see attached screenshot for Default-Font and 'DejaVu Sans Mono' both with Height=22).

I played with length of const 'MARGIN' to check, if another length would fit, but:
 - MARGIN-length = 8 would fit for Default-Font (Height=22) while
 - MARGIN-length = 4 would fit for 'DejaVu Sans Mono' or 'Courier New' (Height=22)
So we would need different MARGIN-lengths for differen Fonts... 

Then I modified your function Combobox_Autowidth() to display the result of ThemeServices.GetDetailSize() which always returns -1, which might mean an error. I tried to find infos about this function, but Lazarus-Online-Help (F1) showed nothing usable and with google in a reasonable time the same, only 2 fixed errors in https://bugs.freepascal.org/view.php?id=0027381 and https://bugs.freepascal.org/view.php?id=32437

Then I tried on Windows 7 (32-bit) - real Windows, no VM or WINE - with Lazarus 2.0.6 / FPC 3.0.4. Again the width was always too big with original MARGIN-length of 15. Again I played with it's length to check, if another length would fit, but:
 - MARGIN-length = 5 would fit for Default-Font (Height=22) while
 - MARGIN-length = 6 would fit for 'Times New Roman' (Height=22) while
 - MARGIN-length = 2 would fit for 'Courier New' (Height=22).

On Windows too ThemeServices.GetDetailSize() returns -1. I tried also Lazarus 2.1.0 rev=62449 / FPC 3.3.1 rev=43796 but it's the same.
Do you have an idea to avoid this?
 
I tested on "real" Windows (you probably are on Wine?), and it works correctly, even with different font sizes.
I too used "real" Windows 7 (32-bit).
With 'Courier New' it works correctly, but e.g. with 'Times New Roman' not (please see screenshot CB_Windows.jpg in reply #4).

Quote
I also ran Manjaro Linux in a VM (gtk2), and I confirm that my code does not work there. It does not seem to yield the correct result for GetSystemMetrics. Maybe another element of the long list of SM_*** constants is more appropriate, but I did not come to a quick solution.
From my understanding, GetSystemMetrics() does not know the selected Combobox Font and it's size? So I fear, even with another SM_*** constant it cannot work, because we need a correction offset, which depends on the selected Font and Font Height...

Quote
Yes, Howard's way of employing the theme services looks very promising; but I see problems here on Cocoa where my sparse experience warns that themeservices do not seem to work at all on macOS.
This wouldn't be a problem, I don't have macOS.

Thanks to both of you for your help.

howardpc

  • Hero Member
  • *****
  • Posts: 4144
Re: How to compute TComboBox.Width to fit for a certain text?
« Reply #9 on: July 22, 2020, 02:48:50 pm »
Well, Hartmut, you are indeed correct that ThemeServices returns -1 (meaning "unknown dimension") for the dropdown-arrow width of a combobox.
I measured it (in pixels) on my monitor under gtk2 and came up with a value of 41.
Thus the following adjusted routine works far better:
Code: Pascal  [Select][+][-]
  1. uses  LCLType;
  2. function Combobox_Autowidth(AComboBox: TComboBox): Integer;
  3. const
  4.   MARGIN = '    ';
  5. var
  6.   i, w: Integer;
  7. begin
  8.   Result := 0;
  9.   AComboBox.Canvas.Font.Assign(AComboBox.Font);
  10.   for i := 0 to AComboBox.Items.Count-1 do
  11.     begin
  12.       w := AComboBox.Canvas.TextWidth(MARGIN + AComboBox.Items[i]);
  13.       if w > Result then
  14.         Result := w;
  15.     end;
  16.   Inc(Result, MulDiv(41, ScreenInfo.PixelsPerInchX, 96));
  17. end;
Perhaps this also gives better results on Windows (where the value of 41 probably needs adjusting to the win32 widget value, which is unlikely to be identical to gtk2's value).
 

Hartmut

  • Hero Member
  • *****
  • Posts: 1131
Re: How to compute TComboBox.Width to fit for a certain text?
« Reply #10 on: July 22, 2020, 06:27:27 pm »
Thanks a lot again, howardpc for this new idea, but unfortunately it does not give satisfying results...

On Ubuntu 18.04 (64-bit):
With original length for MARGIN of 4 the resulting Width was always too big.
I modified your function Combobox_Autowidth() to display the result of ScreenInfo.PixelsPerInchX (which was always 96) and the result of MulDiv(), which was always 41. Then I replaced the complete MulDiv() with a const 'MD' and played with it:

For original MARGIN-length=4 I got correct results with this values for 'MD':
 - 'DejaVu Sans Mono': Height=12 => MD=2, Height=22 => MD=-11, Height=30 => MD=-23
That means, MARGIN is too long, because growing Height needs decreasing MD's.

I played with MARGIN-length and found the best possible results for this Font with MARGIN-length=2:
 - 'DejaVu Sans Mono': Height=12 => MD=16, Height=22 => MD=15, Height=30 => MD=13
Here a value for 'MD' of 16 would have been "good enough" for me, but with the next Font (with same MARGIN-length=2) I only got correct results with this values for 'MD':
 - 'Default Font': Height=12 => MD=26, Height=22 => MD=33, Height=30 => MD=40
So here we would need complete other values for 'MD' than for the 1st Font and they are not "in a near range" as before :-(

And while for 'DejaVu Sans Mono' MARGIN-length=2 is a little too long, for 'Default Font' the same MARGIN-length=2 is too short, which is a contradiction. So from my understanding this approach cannot solve this problem (?)

On Windows 7 (32-bit):
ScreenInfo.PixelsPerInchX was again always 96 and the result of MulDiv() again was always 41. And with original length for MARGIN of 4 the resulting Width was also always too big.

I played with MARGIN-length and 'MD' and found perfect values for one Font with MARGIN-length=1:
 - 'Times New Roman': Height=12 => MD=22, Height=22 => MD=22, Height=30 => MD=22
But with the next Font (with same MARGIN-length=1) I needed:
 - 'Courier New': Height=12 => MD=17, Height=22 => MD=11, Height=30 => MD=8
 
So we have on Windows the same general problem as on Linux...
Do you have another idea? Thanks a lot for your help.

howardpc

  • Hero Member
  • *****
  • Posts: 4144
Re: How to compute TComboBox.Width to fit for a certain text?
« Reply #11 on: July 22, 2020, 07:11:56 pm »
Short answer to your last question: I don't think there is a reliable cross-platform solution that covers all possible variations of font name and font size.
I suspect that Canvas.TextWidth is not giving accurate results for unusual fonts and outlying font size values, merely producing results that may be good enough in common cases. How often do you actually need to change the combobox font from its Parent's value? I would just go for a routine that gives reliable results for a chosen font specification, and then leave the font unchanged.

The point of the MulDiv line was to adapt the code for any possible monitor that might run the code. For any one given monitor (e. g. your monitor), obviously its PixelsPerInch value is a constant, and in your case the calculated "width" does not need scaling, as it happens.

wp

  • Hero Member
  • *****
  • Posts: 13491
Re: How to compute TComboBox.Width to fit for a certain text?
« Reply #12 on: July 22, 2020, 08:06:44 pm »
I played a bit with Win10 and 7, and with gtk2 and qt on Linux.

Windows seems to work when a size-dependent margin is added (Combobox.Canvas.TextWidth('M')) in addition to a small constant offset (2 pixels, which of course must be scaled to the screen resolution: ScaleX(2, 96) where 96 is the unscaled resolution ).

Linux qt is almost perfect, a constant margin of 8 pixels (to be scaled) seems to be sufficient

Linux gkt2 is the worst...

But I also guess that when the user changes themes in qt or gtk2 there will be new values again. Canvas.TextWidth is always correct (I tested this many times by drawing the boundary rectangle), the problems are the margins and the button width.

So, optimizing this is wasted time... Add ample of margin, and focus on something else...

sstvmaster

  • Sr. Member
  • ****
  • Posts: 306
Re: How to compute TComboBox.Width to fit for a certain text?
« Reply #13 on: July 23, 2020, 09:21:38 am »
And something like this, see attachment next post!!!
« Last Edit: July 23, 2020, 10:21:38 am by sstvmaster »
greetings Maik

Windows 10,
- Lazarus 4.4 (stable) + fpc 3.2.2 (stable)
- Lazarus 4.99 (trunk) + fpc 3.3.1 (main/trunk)

sstvmaster

  • Sr. Member
  • ****
  • Posts: 306
Re: How to compute TComboBox.Width to fit for a certain text?
« Reply #14 on: July 23, 2020, 10:05:00 am »
Improved version with height selection.
greetings Maik

Windows 10,
- Lazarus 4.4 (stable) + fpc 3.2.2 (stable)
- Lazarus 4.99 (trunk) + fpc 3.3.1 (main/trunk)

 

TinyPortal © 2005-2018