Scaling a variable by percentage forth and back

Hello guys, even though this is about windows programing, my problem is actually math in relation to DPI aware program, so I'm posting here rather than in windows programming sub forum.

Currently my window class stores a variable with DIP dimensions which is updated each time the window resizes, moves or when it is initially created ex:
1
2
// Holds window dimensions in DIP's (device independent pixels)
RECT DipRect;


In addition my class also stores a variable which is a scale representing system setting in windows settings:
1
2
// Holds scale (1.0, 1.25, 1.5 or 1.75)
float DpiScale;


DpiScale variable is computed on the fly and it thus either, 1, 1.25, 1.50 or 1.75, which depends on setting in windows.

When I get WM_DPICHANGED message I use these variables to compute new size and position of a window by multiplying DipRect and DpiScale, resulting in new size and position of a window.

While this works just fine, I'm not happy with this method because I need to update DIP dimension everywhere in code, but I want to be able to compute DIP size of a window without managing those variables by simply calling just one function to get original window size.

So here is a math problem to achieve this.

Let variable dim represents current arbitrary window DPI dimension:
int dim = 100;

And let variable scale represents current scale:
float scale = 1.0;

You get WM_DPICHANGED and compute new scale to be 1.25 which means window dimension needs to be increased by 25%, therefore:
1
2
3
4
auto new_scale = 1.25;
auto diff = new_scale - scale; // 0.25
dim += (dim * diff) // 100 + 100 * 0.25 = 125
scale += new_scale; // 1.0 + 0.25 = 1.25 

And we get new dimension of 125 and new scale of 1.25 which is correct.

Now assume you get a new WM_DPICHANGED event and new scale is back to 1.0 therefore variable dim needs to be scaled down to original value of 100

However, multiplying this same variable by 0.75 will not get us the original dimension of 100:
dim *= 0.75 // 93.75 (but 100 is expected)

Which is incorrect because original value was 100.
1
2
3
4
5
auto new_scale = 1.00;
auto diff = new_scale - scale; // 1.0 - 1.25 = -0.25
dim = ? // how do we calculate new dimension here
dim -= (dim * diff) // 125 - 125 * -0.25 = 125 - 31.25 = 93.75 INCORRECT!
scale += new_scale; // 1.25 + (-0.25) = 1.0 


Computing the scale goes fine, however computing dim variable to get a dimension at scale is an issue.
So the first question is how to mathematically get original dimension back again?

Note that the formula should be able to give always same result for given scale.
The Window might resize to what ever dimension and we should get a value for given scale regardless of current dimension.
Last edited on
malibor wrote:
scale += new_scale;

Shouldn't this be
 
scale += diff;
or
 
scale = new_scale;
?

malibor wrote:
question is how to mathematically get original dimension back again?

First note that
 
dim += (dim * diff)
is the same as
 
dim *= new_scale;

This only works when the old scale is 1.0, so what you might want to do is to first undo the old scale (to get back to 1.0) before applying the new scale. You can do this by dividing by the old scale.

1
2
dim /= scale;
dim *= new_scale;

If you want to combine these in the same statement it would like like this:

 
dim = (dim / scale) * new_scale;


Floating-point calculations are not always exact, and here it's made worse by conversions back and forth between float and int. You might want to round before assigning to the integer variable dim to avoid that something like 124.99999999... gets truncated to 124 instead of 125.

 
dim = std::round((dim / scale) * new_scale);

I don't know if this is enough. Small errors could easily accumulate if you are not careful. Another perhaps more robust solution would be to store the original dim value and always recalculate the new dim from that instead.

 
dim = std::round(original_dim * new_scale);

This would prevent the accumulation of errors over time.
Last edited on
Shouldn't this be
scale += diff;

Ah yes my mistake, it should be scale += diff to change scale to new value.

But scale = new_scale does the same as well, it's only important that diff be calculated before assigning scale to new value, that's why I separated this.

But this only works when the old scale is 1.0, so what you might want to do is to first undo the old scale (to get back to 1.0) before applying the new scale.

1
2
dim /= scale;
dim *= new_scale;


Is it as simple as that?
diving by old scale and multiplying by new one, huh how did I miss that.
Thank you a lot!

I don't know if this is enough. Small errors could easily accumulate if you are not careful. Another perhaps more robust solution would be to store the original dim value and always recalculate the new dim from that instead.


Yes, this is my current implementation but downside is that every window and all controls must maintain their own original dimensions which need to be updated on every window resize.
And since window resize messages are in DPI's then every update of original values needs to be scaled to DIP's which is extra computation.

Not sure, but this likely also means duplicate memory usage because windows internally already stores dimensions of each window and those dimensions are DPI scaled, not originals.

So basically I want to reuse those values by re-computing them and saving some memory, code bloat and complexity as the end result.

I didn't think of accumulation of floating point precision loss, so it seems like a solution with new problem heh.

Thanks a lot for help, I'm now not really sure which method is better.
you are worried about 2 numbers ... at most, 2 doubles or 2 ints at 8 bytes each ... 16 bytes. My computer has 68719476736 of those, so its wasting 0.000000000232831 of its memory. Store the originals if you expect the user or the code to change the dimensions repeatedly and rapidly.

BUT.....
its going to take a lot of accumulated error before you have a user observable error even if there is some error. I can't see 1 pixel on a modern screen: you would probably need to be off by 5+ before it was observable to the most observant users and 10+ before flagging it as a true bug, unless you are doing something very precise far beyond the usual slop of a window dimensions. It may be fine to ignore this 'problem' until you can prove its worth doing anything about at all? Your errors are probably like down in the 1.0 e-10 or so pixel space.
Last edited on
You're right, I gave up from scaling and will continue to use variables to store original dimensions.

However there is also another issue with implementing a function for this, which is that for such function to work there must be a variable to store previous scale to be able to rescale dimensions to new scale, thus by removing one variable for dimensions another one is introduced for old scale and therefore not saving much at all.

Yet another even bigger problem is that API's such as GetWindowRect and GetClientRect return DPI adjusted values but one can't know if those values are based on old scale or new scale, that is whether the API was called prior or after DPI handler.

The only way to be sure is to call these API's within DPI handlers, which is then a limitation on when the function should be used.

Here is my experimental function to get DIP version of window RECT

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// Window position and size in DIP's
bool BaseWindow::GetDipWindowPos(RECT& rect) const noexcept
{
	if (GetWindowRect(mhWnd, &rect) != FALSE)
	{
		// Actual width and height including non client area
		rect.right -= rect.left;
		rect.bottom -= rect.top;
  
                // This is where I stuck due to incorrect result
		if (mDpiChange.x < 0)
		{
			rect.right /= (mDpiScale.x + std::abs(mDpiChange.x));
			rect.bottom /= (mDpiScale.y + std::abs(mDpiChange.y));
		}

                // If this is child map screen to client coordinates
		HWND hParent = GetParent(mhWnd);
		if (hParent != NULL)
		{
			// Screen position coordinates relative to parent client? coordinates
			if (MapWindowPoints(HWND_DESKTOP, hParent, reinterpret_cast<LPPOINT>(&rect), 1) == 0)
			{
			      return false;
			}
		}

		// DIP Position relative to parent
		rect.left = ToDipsX(rect.left);
		rect.top = ToDipsY(rect.top);

		return true;
	}

	return false;
}


It works just fine except a problem is that it's impossible to know whether GetWindowRect returns DPI values prior DPI change or after, that is it's impossible to know when this function is called.

If it's prior DPI change then the returned RECT is useless.

Another problem is that parent window may actually be owner window, which if true is again useless function because owned windows get WM_SIZE message after parents, at least so was my observation, thus useless to position a window relative to owner window.

I was struggling with this for hours with different results and it seems it's best to give up and just store original values in every window instance.
Topic archived. No new replies allowed.