Sign in Join

Live chat for Free Pascal, Lazarus, Delphi and every Pascal programmer.

Mailing lists / lazarus / 2023-06

Hi-DPI tweak of components

21 messages · 4 people · –

Reply by email Subscribe to this list Your subscription

Read-only archive. Use j and k to move between messages.

Vojtěch Čihák

Hi,
?
I finally built a hi-DPI machine (28'' 4k display, currently set to 118PPI) and I need to tweak EC-Controls (https://wiki.freepascal.org/Eye-Candy_Controls) to scale better. One problematic is TECSpinBtns because it is designed like this: it consists of (initially) nine small buttons (TPersistent) with hardcoded width=15 and its overall width is 9*15 =135 pixels or N*15 in general. Therefore, when I put this component on the form, its Width is not scaled (Height is). I tried to override?DoAutoAdjustLayout but it's not ?is TGraphicControl. ?
Thanks, V.

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.lazarus-ide.org/pipermail/lazarus/attachments/20230607/e5b7d485/attachment.htm>

Reply by email

Ondrej Pokorny
to Vojtěch Čihák
Vojt?ch ?ih?k wrote 16 lines
I finally built a hi-DPI machine (28'' 4k display, currently set to 118PPI) and I need to tweak EC-Controls (https://wiki.freepascal.org/Eye-Candy_Controls) to scale better. One problematic is TECSpinBtns because it is designed like this: it consists of (initially) nine small buttons (TPersistent) with hardcoded width=15 and its overall width is 9*15 =135 pixels or N*15 in general. Therefore, when I put this component on the form, its Width is not scaled (Height is). I tried to override?DoAutoAdjustLayout but it's not triggered at all. TECSpinBtns is TGraphicControl.

Here is information:
https://wiki.freepascal.org/High_DPI#High_DPI_in_Lazarus_1.8_and_above

IIRC, DoAutoAdjustLayout() is executed only when the DPI changes. You have to scale every hardcoded pixel value in the
constructor/loading/intitialize as well.

Ondrej
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.lazarus-ide.org/pipermail/lazarus/attachments/20230607/86213ea8/attachment.htm>

Reply by email

Vojtěch Čihák
to Ondrej Pokorny

Thanks,
?
please confirm me if I understand well the concept: 1)?when component is put on the form in designer: it?is scaled (Width & Height)?
2) when?it is put on the form later in code: it is not scaled? ?
but internals are scaled in both cases (by internals I mean for example DefaultRowHeight and DefaultColWidth of TStringGrid) ?
Or at least TStringGrid behaves as I described above. ?
Because I need to scale internals of TGraphicControl (which has no own Canvas or Handle) I decided to do scaling in SetParent();. ?
For curiosity: was tested the case when someone has two or more monitors with different DPI and moves the form (made in Lazarus) from one monitor to another? ?
V.

Reply by email

Ondrej Pokorny
to Vojtěch Čihák
Vojt?ch ?ih?k wrote 8 lines
Thanks, please confirm me if I understand well the concept: 1) when component is put on the form in designer: it is scaled (Width & Height)

Yes.

2) when it is put on the form later in code: it is not scaled

It depends on what you understand with "it is not scaled".

If you do:

MyComponent.Parent := Panel1;
MyComponent.Width := 100;
MyComponent.Height := 100;

Then obviously the width/height is not scaled because you assigned constant numbers there.

But the internals are scaled with DoAutoAdjustLayout() if the target PPI (of Panel1) is different from Screen.PixelsPerInch.

Show quoted text 5 lines
but internals are scaled in both cases (by internals I mean for example DefaultRowHeight and DefaultColWidth of TStringGrid) Or at least TStringGrid behaves as I described above.

Yes.

Because I need to scale internals of TGraphicControl (which has no own Canvas or Handle) I decided to do scaling in SetParent();.

You definitely should use DoAutoAdjustLayout().

For curiosity: was tested the case when someone has two or more monitors with different DPI and moves the form (made in Lazarus) from one monitor to another?

Yes. DoAutoAdjustLayout() takes care of it.

Ondrej
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.lazarus-ide.org/pipermail/lazarus/attachments/20230608/7049a2f1/attachment.htm>

Reply by email

Ondrej Pokorny
to Ondrej Pokorny
Ondrej Pokorny wrote 7 lines
On 08.06.2023 00:46, Vojt?ch ?ih?k via lazarus wrote: > > Because I need to scale internals of TGraphicControl (which has no > own Canvas or Handle) I decided to do scaling in SetParent();. > You definitely should use DoAutoAdjustLayout().

Usually the best way to do "internals" scaling is to scale them when they are needed and not to store the scaled value.

Take a look at this example:
* TToolButton.Paint:
? dist := FToolbar.Scale96ToFont(cHorIconTextDist);

The following examples show you how to handle a scaled internal value with a default value
* TCustomColorBox.ColorRectWidth
* TCustomGrid.DefaultColWidth
* TCustomTreeView.ExpandSignSize

---

If you have values that need to be scaled you do:

1.) Initialize the sizes for Screen.PixelsPerInch in constructor:
MySize := Scale96ToScreen(cMyValueAt96DPI);
2.) Use DoAutoAdjustLayout() to scale them when PPI changes:
MySize := Round(MySize * AXProportion);

Simple as that.

Check TCustomColorBox how it handles FColorRectWidth as an example (it uses a default value and if the user overwrites it, it gets scaled in DoAutoAdjustLayout().

Ondrej
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.lazarus-ide.org/pipermail/lazarus/attachments/20230608/54420892/attachment-0001.htm>

Reply by email

Giuliano Colla
to Ondrej Pokorny

Il 08/06/2023 08:08, Ondrej Pokorny via lazarus ha scritto:

Check TCustomColorBox how it handles FColorRectWidth as an example (it uses a default value and if the user overwrites it, it gets scaled in DoAutoAdjustLayout().

I believe that scaling for different DPI has been implemented with a shortsighted approach.
IMHO the general approach should have been (or should be if someone is willing to face it) to have *two* parameters for width and height: a pixel value and a linear value (in mm, inches, whatever). If the designer or user sets the pixel value, this value is taken and used, but the linear value is? calculated.
If the designer or user sets the linear value, the pixel value is calculated using the current DPI, and is used to paint the control. A new DPI will cause all pixel values to be updated from the linear values. I don't know if there are some cases where the pixel value should be retained even for a significant DPI change. A FixedPixel boolean or a zero value in the linear value might override the resizing.

I understand that this would require a revision of all TControl descendants, by exposing the new properties, but I believe it to be the only way to make the DPI handling user friendly and mainly transparent to the users.

Giuliano

Do not do to others as you would have them do to you.They might have different tastes.

Reply by email

Giuliano Colla
to Giuliano Colla

Il 08/06/2023 11:34, Giuliano Colla via lazarus ha scritto:

Show quoted text 28 lines
Il 08/06/2023 08:08, Ondrej Pokorny via lazarus ha scritto: > Check TCustomColorBox how it handles FColorRectWidth as an example > (it uses a default value and if the user overwrites it, it gets > scaled in DoAutoAdjustLayout(). I believe that scaling for different DPI has been implemented with a shortsighted approach. IMHO the general approach should have been (or should be if someone is willing to face it) to have *two* parameters for width and height: a pixel value and a linear value (in mm, inches, whatever). If the designer or user sets the pixel value, this value is taken and used, but the linear value is? calculated. If the designer or user sets the linear value, the pixel value is calculated using the current DPI, and is used to paint the control. A new DPI will cause all pixel values to be updated from the linear values. I don't know if there are some cases where the pixel value should be retained even for a significant DPI change. A FixedPixel boolean or a zero value in the linear value might override the resizing. I understand that this would require a revision of all TControl descendants, by exposing the new properties, but I believe it to be the only way to make the DPI handling user friendly and mainly transparent to the users. Giuliano

An afterthought. A simpler way (or a first step) could be to keep the linear value hidden. This wouldn't require a deep LCL revision, and would make fully transparent the DPI handling. The designer sets the desired width/height to the current screen, but the linear value is stored and used whenever the DPI changes.

Giuliano

Do not do to others as you would have them do to you.They might have different tastes.

Reply by email

Ondrej Pokorny
to Giuliano Colla: Il 08/06/2023 08:08, Ondrej Pokorny via lazarus ha scritto: I believe that scaling for…
Giuliano Colla wrote 25 lines
Il 08/06/2023 08:08, Ondrej Pokorny via lazarus ha scritto: > Check TCustomColorBox how it handles FColorRectWidth as an example > (it uses a default value and if the user overwrites it, it gets > scaled in DoAutoAdjustLayout(). I believe that scaling for different DPI has been implemented with a shortsighted approach. IMHO the general approach should have been (or should be if someone is willing to face it) to have *two* parameters for width and height: a pixel value and a linear value (in mm, inches, whatever). If the designer or user sets the pixel value, this value is taken and used, but the linear value is? calculated. If the designer or user sets the linear value, the pixel value is calculated using the current DPI, and is used to paint the control. A new DPI will cause all pixel values to be updated from the linear values. I don't know if there are some cases where the pixel value should be retained even for a significant DPI change. A FixedPixel boolean or a zero value in the linear value might override the resizing. I understand that this would require a revision of all TControl descendants, by exposing the new properties, but I believe it to be the only way to make the DPI handling user friendly and mainly transparent to the users.

LOL. What advantage do you get from this approach? You get rid of rounding errors when scaling occurs? So you get rid of problems within 1px margins? And what do you have to pay for it? To double-up all current pixel (integer precision) values with other double-precision units and do conversions between them all the time? No way it is a better approach.

Well, you can create your own component library from scratch that will use double-precision units for positions&sizes (Delphi FireMonkey is one example). VCL and LCL have been designed when scaling was not an issue, so you have to live with the preconditions.

Ondrej

Reply by email

Ondrej Pokorny
to Giuliano Colla: Il 08/06/2023 11:34, Giuliano Colla via lazarus ha scritto: An afterthought. A simpler…
Giuliano Colla wrote 36 lines
Il 08/06/2023 11:34, Giuliano Colla via lazarus ha scritto: > Il 08/06/2023 08:08, Ondrej Pokorny via lazarus ha scritto: > >> Check TCustomColorBox how it handles FColorRectWidth as an example >> (it uses a default value and if the user overwrites it, it gets >> scaled in DoAutoAdjustLayout(). > > I believe that scaling for different DPI has been implemented with a > shortsighted approach. > IMHO the general approach should have been (or should be if someone > is willing to face it) to have *two* parameters for width and height: > a pixel value and a linear value (in mm, inches, whatever). > If the designer or user sets the pixel value, this value is taken and > used, but the linear value is? calculated. > If the designer or user sets the linear value, the pixel value is > calculated using the current DPI, and is used to paint the control. > A new DPI will cause all pixel values to be updated from the linear > values. > I don't know if there are some cases where the pixel value should be > retained even for a significant DPI change. A FixedPixel boolean or a > zero value in the linear value might override the resizing. > > I understand that this would require a revision of all TControl > descendants, by exposing the new properties, but I believe it to be > the only way to make the DPI handling user friendly and mainly > transparent to the users. > > Giuliano > An afterthought. A simpler way (or a first step) could be to keep the linear value hidden. This wouldn't require a deep LCL revision, and would make fully transparent the DPI handling. The designer sets the desired width/height to the current screen, but the linear value is stored and used whenever the DPI changes.

Well, this still could be implemented.

But do not forget that the LCL must stay backwards-compatible (old code must work in new versions) and to some extend also forwards-compatible (LFM from new Lazarus versions must open in older Lazarus versions - that is valid for at least 2 latest stable versions). Especially, the LFM compatibility is an issue.

All in all, an over-complicated approach with little gain.

Ondrej

Reply by email

Giuliano Colla
to Ondrej Pokorny

Il 08/06/23 11:58, Ondrej Pokorny via lazarus ha scritto:

All in all, an over-complicated approach with little gain.

The gain would be that you do not add up rounding errors. We can't have fractional pixels, of course, but we may have the exact actual size at design/creation time, and for each different DPI the best approximation to the actual size. If you switch back and forth between two monitors with different DPI the rounding errors remain constant, they don't add up.

Giuliano

Reply by email

Jean SUZINEAU
to Giuliano Colla: Il 08/06/2023 08:08, Ondrej Pokorny via lazarus ha scritto: I believe that scaling for…

Le 08/06/2023 ? 11:34, Giuliano Colla via lazarus a ?crit?:

I believe that scaling for different DPI has been implemented with a shortsighted approach.

It would have been nice at the time of Delphi 1 in 1995 ... ;-)

A few weeks ago I got some havoc trying to port some code from LCL to Delphi 10.2 / FireMonkey when coordinates switch from Integer to Single, TPoint to TPointF,? array of TPoint to TPolygon... https://github.com/jsuzineau/pascal_o_r_mapping/blob/b54853062432d7921b17a01c7b951688590a292a/delphi/pascal_o_r_mapping/03_Data/uBatpro_Element.pas#L1478

I think you can't use integer value for mm or inches.

Reply by email

Giuliano Colla
to Jean SUZINEAU

Il 08/06/23 12:39, Jean SUZINEAU via lazarus ha scritto:

I think you can't use integer value for mm or inches.

Of course you can't. But you can use integer values for hundredth of mm (that's the resolution of high quality photo-composition) or for thousandth of inch (slightly worse but still acceptable), which in a 64bit long integer would give enough space to fit a screen much wider than the Solar system!

Giuliano

Reply by email

Ondrej Pokorny
to Giuliano Colla: Il 08/06/23 11:58, Ondrej Pokorny via lazarus ha scritto: The gain would be that you do…
Giuliano Colla wrote 10 lines
Il 08/06/23 11:58, Ondrej Pokorny via lazarus ha scritto: > All in all, an over-complicated approach with little gain. The gain would be that you do not add up rounding errors. We can't have fractional pixels, of course, but we may have the exact actual size at design/creation time, and for each different DPI the best approximation to the actual size. If you switch back and forth between two monitors with different DPI the rounding errors remain constant, they don't add up.

You have to consider that for monitor DPI scaling, incrementing series of multiplications like
A * 1.5 * 1.25 * 1.75 * 2.00 * etc * etc
never appear.

You always have a limited count of different resolutions (usually 2) and you always switch between up- and down-scaling. Statistically the Round() function rounds up and down equal sets of float values. So, statistically, the errors cannot add up, but they fix themselves up with the count of up- and down-scaling operations.

Here is my proof for switching between 2 different resolutions from start with 100%:

program Project1;
uses Math;
const
 ? Resolutions: array[0..3] of Double = (1.25, 1.50, 1.75, 2.00);
var
 ? R: Double;
 ? I, F: Integer;
begin
 ? for R in Resolutions do
 ??? for I := 0 to 1000 do
 ??? begin
 ????? F := I;
 ????? // scale first up and then down
 ????? F := Round(F * R);
 ????? F := Round(F / R);
 ????? if not SameValue(F, I) then
 ??????? Writeln(I, ': ', F);
 ??? end;
 ? ReadLn;
end.

Run the program, and you will se that there is no starting value at 100% that would differ if you scale it up and down.

-------

If you want to test scaling down and up, then there of course there will be a discrepancy between starting and ending value after 1 down- and up-scaling cycle because you lose resolution with the first division. But if you do the same up/down scaling again, you will see that every starting value ended at a well defined pair of values. The values definitely do not grow or shrink with the count of scaling operations.

program Project1;
uses Math;
const
 ? Resolutions: array[0..3] of Double = (1.25, 1.50, 1.75, 2.00);
var
 ? R: Double;
 ? I, F, ErrorCountInMiddleValue, ErrorCountAfterSecondScaling,
MiddleValue: Integer;
begin
 ? ErrorCountInMiddleValue := 0;
 ? ErrorCountAfterSecondScaling := 0;
 ? for R in Resolutions do
 ??? for I := 0 to 1000 do
 ??? begin
 ????? F := I;
 ????? // first scale down, then up
 ????? F := Round(F / R);
 ????? F := Round(F * R);
 ????? MiddleValue := F;
 ????? if not SameValue(F, I) then
 ????? begin
 ??????? Inc(ErrorCountInMiddleValue);
 ??????? // scale first down and then up
 ??????? F := Round(F / R);
 ??????? F := Round(F * R);
 ??????? if not SameValue(F, MiddleValue) then
 ??????? begin
 ????????? Writeln('Error after second scaling: ', I, ': ', F);
 ????????? Inc(ErrorCountAfterSecondScaling);
 ??????? end;
 ????? end;
 ??? end;
 ? Writeln('ErrorCountInMiddleValue: ', ErrorCountInMiddleValue);
 ? Writeln('ErrorCountAfterSecondScaling: ', ErrorCountAfterSecondScaling);
 ? ReadLn;
end.

As you can see, there is no starting value that would cause a constantly incrementing or decrementing series of values. Every starting value settles down on a well-defined pair of values after the second scaling.

I consider my arguments as proven. Your suggestion to have a better precision units for sizes doesn't

And even if you found some very rare combination of scaling A -> B -> C -> A of some value X that increments by 1px every time after the whole scaling cycle is performed: who cares that after so many scaling operations the window/column/... width is bigger/smaller by 1px? As I said in the beginning, the default values should not be scaled with this approach, they should be scaled always from a constant value at 100%/96 PPI. That means for them the rounding error problem does not apply. It would apply only for user-defined sizes and there it does not matter because if the size doesn't do it for the user, he can always resize the window/column/ whatever.

Ondrej

Reply by email

Ondrej Pokorny
to Giuliano Colla: Il 08/06/23 11:58, Ondrej Pokorny via lazarus ha scritto: The gain would be that you do…
Giuliano Colla wrote 10 lines
Il 08/06/23 11:58, Ondrej Pokorny via lazarus ha scritto: > All in all, an over-complicated approach with little gain. The gain would be that you do not add up rounding errors. We can't have fractional pixels, of course, but we may have the exact actual size at design/creation time, and for each different DPI the best approximation to the actual size. If you switch back and forth between two monitors with different DPI the rounding errors remain constant, they don't add up.

As I showed in my last email, switching back and forth between two monitors with different DPI the rounding errors remain constant even with the current integer-precision pixel values. The rounding errors never add up.

So the problem you want to solve does not exist in the first place.

Ondrej

Reply by email

Ondrej Pokorny
to Ondrej Pokorny: You have to consider that for monitor DPI scaling, incrementing series of multiplications…

Here is a more general proof for every possible combination of resolutions between 100% and 400% and for values [0..10000]:

program Project1;
uses Math;
const
 ? Resolutions: array[0..8] of Double = (1.00, 1.25, 1.50, 1.75, 2.00,
2.50, 3.00, 3.50, 4.00);
var
 ? SourceResultion, TargetResolution: Double;
 ? I, F, ErrorCountInMiddleValue, ErrorCountAfterSecondScaling,
MiddleValue: Integer;
begin
 ? ErrorCountInMiddleValue := 0;
 ? ErrorCountAfterSecondScaling := 0;
 ? for SourceResultion in Resolutions do
 ??? for TargetResolution in Resolutions do
 ????? if not SameValue(SourceResultion, TargetResolution) then
 ????? begin
 ??????? for I := 0 to 10000 do
 ??????? begin
 ????????? F := I;
 ????????? // scale from source to target and back
 ????????? F := Round(F / SourceResultion * TargetResolution);
 ????????? F := Round(F * SourceResultion / TargetResolution);
 ????????? MiddleValue := F;
 ????????? if not SameValue(F, I) then
 ????????? begin
 ??????????? Inc(ErrorCountInMiddleValue);
 ?? ???????? // scale from source to target and back again
 ??????????? F := Round(F / SourceResultion * TargetResolution);
 ??????????? F := Round(F * SourceResultion / TargetResolution);
 ??????????? if not SameValue(F, MiddleValue) then
 ??????????? begin
 ????????????? Writeln('Error after second scaling: ', I, ': ', F, '
SourceResultion:TargetResolution (', SourceResultion, ':',
TargetResolution, ')');
 ????????????? Inc(ErrorCountAfterSecondScaling);
 ??????????? end;
 ????????? end;
 ??????? end;
 ????? end;
 ? Writeln('ErrorCountInMiddleValue: ', ErrorCountInMiddleValue);
 ? Writeln('ErrorCountAfterSecondScaling: ', ErrorCountAfterSecondScaling);
 ? ReadLn;
end.

Ondrej

Reply by email

Ondrej Pokorny
to Ondrej Pokorny

And because it is so much fun, I wrote a completely general program to test it.

You can define how many resolution combinations you want to go through and what maximum value you want to start with and it goes through all possible combinations and gives you results after how many cycles a start-end pair has settled down and what was the maximum difference.

E.g. for the most typical use-case of 2 different resolutions and max scaling of 200% and max starting value of 2000, the results are:

-- Input --
Possible resolutions: 100%, 125%, 150%, 175%, 200% UniqueCombinations: True
ResolutionSteps: 2
MaxValue: 2000
-- Results --
Count: 40020
Error count after the cycle #1: 5615 (14,03%)
No errors after cycle #2
Maximal error value (difference between start and end value): 1 (5,88% from value 17 to 18 for combination 10)

So not really bad, after the 1st cycle the value always settles down and the maximal error is 1px, which is ~6% of the start value.

---------

For 3 different resolutions (100%, 125%, 200%), the maximum error value is 2px after max 10 resolution changes:

-- Input --
Possible resolutions: 100%, 125%, 200%
UniqueCombinations: False
ResolutionSteps: 10
MaxValue: 500
-- Results --
Count: 29583048
Error count after the cycle #1: 6805918 (23,01%)
No errors after cycle #2
Maximal error value (difference between start and end value): 2 (9,52% from value 21 to 19 for combination 2000000001)

----------

But even after 8 resolution steps (using 5 different resolutions), the error is not that bad:

-- Input --
Possible resolutions: 100%, 125%, 150%, 175%, 200% UniqueCombinations: False
ResolutionSteps: 8
MaxValue: 500
-- Results --
Count: 195702624
Error count after the cycle #1: 82854224 (42,34%)
Error count after the cycle #2: 9545914 (4,88%)
Error count after the cycle #3: 184165 (0,09%)
No errors after cycle #4
Maximal error value (difference between start and end value): 5 (26,32% from value 19 to 14 for combination 32401201)

---------

To sum it up, for 3 and less monitor DPI resolutions, the maximum error is 2px, which is marginal and it is there only for the very small value of 19px.

When the number of different resolutions increases (in the above example 5 different resolutions), the error gets higher, but still is acceptable and the higher the starting value, the lower the rounding error. And honestly, who has 5 monitors and every one with a different resolution and moves a window between all of them, so that it bothers him?

My conclusion is that it really is not worth it to introduce a floating-point precision sizes within the LCL. Yes, there would be some gain in scaling precision but only for a lot of different DPI values (more than 3) and the gain would not be striking. On the other hand, the effort needed would be very high, especially due to forwards compatibility of the LFM format.

Ondrej

Program code:

program TestScaling;
uses Math, SysUtils;
const
 ? //DefResolutions: array[0..2] of Double = (1.00, 1.25, 2.00);
 ? DefResolutions: array[0..4] of Double = (1.00, 1.25, 1.50, 1.75, 2.00);
 ? //DefResolutions: array[0..8] of Double = (1.00, 1.25, 1.50, 1.75,
2.00, 2.50, 3.00, 3.50, 4.00); // test even more resolutions - up to 400%
 ? MaxDiffFromValue = 16; // do not test values smaller than this for
MaxDiff*
function Scale(const aValue: Integer; const aFromResolution,
aToResolution: Double): Integer;
begin
 ? Result := Round(aValue / aFromResolution * aToResolution);
end;
function ScaleCycle(const V: Integer; R: TArray<Integer>): Integer;
var
 ? I: Integer;
begin
 ? Result := V;
 ? for I := 0 to High(R)-1 do
 ??? Result := Scale(Result, DefResolutions[R[I]], DefResolutions[R[I+1]]);
 ? Result := Scale(Result, DefResolutions[R[High(R)]],
DefResolutions[R[0]]);
end;
function NextCombination(var R: TArray<Integer>): Boolean;
var
 ? I: Integer;
begin
 ? for I := High(R) downto 0 do
 ? begin
 ??? if R[I]<High(DefResolutions) then
 ??? begin
 ????? Inc(R[I]);
 ????? Exit(True);
 ??? end;
 ??? R[I] := 0;
 ? end;
 ? Result := False;
end;
function CombinationIsUnique(const R: TArray<Integer>): Boolean;
var
 ? I, L: Integer;
begin
 ? for I := 0 to High(R) do
 ??? for L := I+1 to High(R) do
 ????? if R[I]=R[L] then
 ??????? Exit(False);
 ? Result := True;
end;
function WriteCombination(const R: TArray<Integer>): string;
var
 ? I: Integer;
begin
 ? Result := '';
 ? for I := 0 to High(R) do
 ??? Result := Result + IntToStr(R[I]);
end;
function WriteResolutions(const R: array of Double): string;
var
 ? I: Integer;
begin
 ? Result := '';
 ? for I := 0 to High(R) do
 ? begin
 ??? if Result'' then
 ????? Result := Result + ', ';
 ??? Result := Result + IntToStr(Round(R[I]*100))+'%';
 ? end;
end;
var
 ? Resolutions: array of Integer;
 ? I, F, RefValue, Cycle, ResolutionSteps, MaxValue, StartValue,
MaxDiff, MaxDiffStartValue, MaxDiffEndValue: Integer;
 ? Count: Int64;
 ? MaxDiffRatio: Double;
 ? Errors: array of Int64;
 ? MaxDiffCombination: string;
 ? UniqueCombinationsC: Char;
 ? UniqueCombinations: Boolean;
begin
 ? try
 ??? Write('Do you want to test only unique combinations? (y/n): ');
 ??? ReadLn(UniqueCombinationsC);
 ??? UniqueCombinations := LowerCase(UniqueCombinationsC)='y';
 ??? if UniqueCombinations then
 ????? Write(Format('Input how many resolutions do you want to cycle
through (min = 2, max = %d): ', [Length(DefResolutions)]))
 ??? else
 ????? Write('Input how many resolutions do you want to cycle through
(min = 2): ');
 ??? ReadLn(ResolutionSteps);
 ??? if ResolutionSteps<2 then
 ????? raise EInOutError.Create('Invalid resolution count');
 ??? if UniqueCombinations and (ResolutionSteps>Length(DefResolutions)) then
 ????? raise EInOutError.CreateFmt('Invalid resolution count (in case of
unique combintations, the resolution count must be max "%d"',
[Length(DefResolutions)]);
 ??? Write('Input the maximum value you want to test (e.g. 1000): ');
 ??? ReadLn(MaxValue);
 ??? if MaxValue<1 then
 ????? raise EInOutError.Create('Invalid maximum value');
 ??? Resolutions := nil;
 ??? SetLength(Resolutions, ResolutionSteps);
 ??? for I := 0 to High(Resolutions) do
 ????? Resolutions[I] := 0;
 ??? Count := 0;
 ??? MaxDiff := 0;
 ??? MaxDiffRatio := 0;
 ??? MaxDiffStartValue := 0;
 ??? MaxDiffEndValue := 0;
 ??? MaxDiffCombination := '';
 ??? Errors := nil;
 ??? while NextCombination(Resolutions) do
 ??? begin
 ????? if UniqueCombinations and not CombinationIsUnique(Resolutions) then
 ??????? continue;
 ????? Write('Combination: ');
 ????? Writeln(WriteCombination(Resolutions));
 ????? for I := 0 to MaxValue do
 ????? begin
 ??????? Inc(Count);
 ??????? StartValue := I;
 ??????? RefValue := I;
 ??????? F := ScaleCycle(RefValue, Resolutions);
 ??????? Cycle := 0;
 ??????? while not SameValue(F, RefValue) do
 ??????? begin
 ????????? // compute MaxDiff - but only for values above MaxDiffFromValue
 ????????? if (StartValue>=MaxDiffFromValue)
 ????????? and (MaxDiff<Abs(F-StartValue)) then
 ????????? begin
 ??????????? MaxDiff := Abs(F-StartValue);
 ??????????? MaxDiffRatio := Abs(F-StartValue) / StartValue;
 ??????????? MaxDiffStartValue := StartValue;
 ??????????? MaxDiffEndValue := F;
 ??????????? MaxDiffCombination := WriteCombination(Resolutions);
 ????????? end;
 ????????? RefValue := F;
 ????????? if High(Errors)<Cycle then
 ????????? begin
 ??????????? SetLength(Errors, Cycle+1);
 ??????????? Errors[Cycle] := 0;
 ????????? end;
 ????????? Inc(Errors[Cycle]);
 ????????? Inc(Cycle);
 ????????? F := ScaleCycle(F, Resolutions);
 ??????? end;
 ????? end;
 ??? end;
 ??? Writeln('end.');
 ??? Writeln;
 ??? Writeln('-- Input --');
 ??? Writeln('Possible resolutions: ', WriteResolutions(DefResolutions));
 ??? Writeln('UniqueCombinations: ', BoolToStr(UniqueCombinations, True));
 ??? Writeln('ResolutionSteps: ', ResolutionSteps);
 ??? Writeln('MaxValue: ', MaxValue);
 ??? Writeln('-- Results --');
 ??? Writeln('Count: ', Count);
 ??? for Cycle := 0 to High(Errors) do
 ????? Writeln(Format('Error count after the cycle #%d: %d (%.2f%%)',
[Cycle+1, Errors[Cycle], Errors[Cycle] / Count * 100]));
 ??? Writeln(Format('No errors after cycle #%d', [Length(Errors)+1]));
 ??? Writeln(Format('Maximal error value (difference between start and
end value): %d (%.2f%% from value %d to %d for combination %s) ',
[MaxDiff, MaxDiffRatio*100, MaxDiffStartValue, MaxDiffEndValue,
MaxDiffCombination]));
 ? except
 ??? on E: Exception do
 ????? Writeln(E.ClassName, ': ', E.Message);
 ? end;
 ? ReadLn;
end.

Reply by email

Giuliano Colla
to Ondrej Pokorny

Il 08/06/23 18:40, Ondrej Pokorny via lazarus ha scritto:

And because it is so much fun, I wrote a completely general program to test it.

Well you've shown that it isn't so bad. But there's another detail to consider, when resizing: font size. Font size can (and should be) specified in typographic points, in order to ensure that text is both readable and not too large. i.e. in units which are 1/72". Resizing when text size is specified in inches, i.e. in its actual, system independent, size, while width and height are specified in pixels i.e. in system dependent units is the best way to create the mess which is there since the early times of Delphi.

Giuliano

Reply by email

Ondrej Pokorny
to Giuliano Colla
Giuliano Colla wrote 13 lines
Il 08/06/23 18:40, Ondrej Pokorny via lazarus ha scritto: > And because it is so much fun, I wrote a completely general program > to test it. Well you've shown that it isn't so bad. But there's another detail to consider, when resizing: font size. Font size can (and should be) specified in typographic points, in order to ensure that text is both readable and not too large. i.e. in units which are 1/72". Resizing when text size is specified in inches, i.e. in its actual, system independent, size, while width and height are specified in pixels i.e. in system dependent units is the best way to create the mess which is there since the early times of Delphi.

Font size/height are not scaled. The TFont.PixelsPerInch property is directly assigned to the target value, so there cannot be a scaling problem there.

See TControl.ScaleFontsPPI and TControl.DoScaleFontPPI.

Ondrej

Reply by email

Vojtěch Čihák
to Ondrej Pokorny: Usually the best way to do "internals" scaling is to scale them when they are needed and…

Hi,

I have one more question to?DoAutoAdjustLayout();. I use TECSpinBtns (TGraphicControl) in two variants: The first as a stand-alone component and the second as a part of TECSpinEdit (where TEdit is Owner and TECSpinBtns is subcomponent anchored to it). When I put them on the form,?DoAutoAdjustLayout()?is triggered only in the first case. Is it that Owner is responsible to call?DoAutoAdjustLayout()?of owned controls? ?
V.

______________________________________________________________

Show quoted text 5 lines
Od: "Ondrej Pokorny via lazarus" Komu: Datum: 08.06.2023 08:08 P?edm?t: Re: [Lazarus] Hi-DPI tweak of components

---If you have values that need to be scaled you do: 1.) Initialize the sizes for Screen.PixelsPerInch in constructor: MySize := Scale96ToScreen(cMyValueAt96DPI);
2.) Use DoAutoAdjustLayout() to scale them when PPI changes: MySize := Round(MySize * AXProportion);
Simple as that.
Check TCustomColorBox how it handles FColorRectWidth as an example (it uses a default value and if the user overwrites it, it gets scaled in DoAutoAdjustLayout(). Ondrej

----------

Reply by email

Ondrej Pokorny
to Vojtěch Čihák
Vojt?ch ?ih?k wrote 18 lines
Hi, I have one more question to DoAutoAdjustLayout();. I use TECSpinBtns (TGraphicControl) in two variants: The first as a stand-alone component and the second as a part of TECSpinEdit (where TEdit is Owner and TECSpinBtns is subcomponent anchored to it). When I put them on the form, DoAutoAdjustLayout()?is triggered only in the first case. Is it that Owner is responsible to call DoAutoAdjustLayout()?of owned controls?

This is strange. All controls should be adjusted regardless of the Owner. TLabeledEdit works fine (edit that has an attached label).

Ondrej
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.lazarus-ide.org/pipermail/lazarus/attachments/20230615/80ea71e4/attachment-0001.htm>

Reply by email

Vojtěch Čihák
to Ondrej Pokorny
TLabeledEdit is probably not a good example since both TEdit and TLabel are native widgets (maybe they scale by widgetset) and there is some Spacing between them hardcoded to 3px. I cannot see any scaling related code in this simple component.
?
Anyway, I know what's going on:
?
procedure TWinControl.AutoAdjustLayout(AMode: TLayoutAdjustmentPolicy;
? const AFromPPI, AToPPI, AOldFormWidth, ANewFormWidth: Integer);
var
? i: Integer;
begin
? DisableAutoSizing{$IFDEF DebugDisableAutoSizing}('TWinControl.AutoAdjustLayout'){$ENDIF};
? try
? ? for i:=0 to ControlCount-1 do
? ? ? Controls[i].AutoAdjustLayout(AMode, AFromPPI, AToPPI, AOldFormWidth, ANewFormWidth);

? ? inherited;
? finally
? ? EnableAutoSizing{$IFDEF DebugDisableAutoSizing}('TWinControl.AutoAdjustLayout'){$ENDIF}; ? end;
end;
?
i.e. form loops all its children and scale them. But when component is dropped on the form, scaling is done before Parent is set. Therefore only main component's DoAutoAdjustLayout is called, subcomponent is ommited. ?
I noticed that it is opposite when loading from *.lfm. There scaling done after the Parent is set, so both component's and subcomponent's?DoAutoAdjustLayout is called. ?
It's fine since there are simplier ways how to do initial scaling (when component is created and put on the form). ?
Thanks, I only needed to understand better how it works. ?

Reply by email