[Lazarus] Hi-DPI tweak of components
Ondrej Pokorny
lazarus at kluug.net
Thu Jun 8 18:40:09 CEST 2023
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.
More information about the lazarus
mailing list