Jump to content
Carlo Barazzetta

A BIG and very strange BUG with High-DPI, VCL Style and Form Constraints

Recommended Posts

I have encountered this problem affecting High-DPI support with VCL-Styles Enabled.
Scenario:
Main monitor: 96 dpi
Secondary monitor: 192 dpi

Per monitor V2 enabled

"Empty" application with main form with:
Constraints.MinHeight = 400
Constraints.MinWidth = 300
WindowState = wsMaximized
VCLStyle active.

1) Run (the form is full screen on the main monitor)
2) Minimize form.
3) Maximize form: BUG! appears in full screen on the main monitor but with a dpi of 192 !!!
4) Re-Minimize form.
5) Re-Maximize: appears in full screen on the main monitor but with a dpi of 96 (returns to correct scale)

 

The three factors that determine the problem are:
1) presence of MinHeight and MinWidth Constraint
2) active VCL style
3) a secondary monitor with different dpi (on which the application never runs)

 

Tested with D10.4.2 and Win10: anyone can confirm this problem or try it on other versions of windows?

 

Using the AfterMonitorDpiChanged event, on the first call after "maximize" the NewDPI value is 192, on the second call returns to 92.

 

 

the project:

program StyleTest;

uses
  Vcl.Forms,
  UstyleTest in 'UstyleTest.pas' {Form5},
  Vcl.Themes,
  Vcl.Styles;

{$R *.res}

begin
  Application.Initialize;
  Application.MainFormOnTaskbar := True;
  TStyleManager.TrySetStyle('Windows10 SlateGray');
  Application.CreateForm(TForm5, Form5);
  Application.Run;
end.

the main form dfm:

object Form5: TForm5
  Left = 0
  Top = 0
  Caption = 'Form5'
  ClientHeight = 500
  ClientWidth = 500
  Color = clBtnFace
  Constraints.MinHeight = 400
  Constraints.MinWidth = 300
  DoubleBuffered = True
  Font.Charset = DEFAULT_CHARSET
  Font.Color = clWindowText
  Font.Height = -11
  Font.Name = 'Tahoma'
  Font.Style = []
  OldCreateOrder = False
  WindowState = wsMaximized
  OnAfterMonitorDpiChanged = FormAfterMonitorDpiChanged
  PixelsPerInch = 96
  TextHeight = 13
end

the main form pas

unit UstyleTest;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;

type
  TForm5 = class(TForm)
    procedure FormAfterMonitorDpiChanged(Sender: TObject; OldDPI,
      NewDPI: Integer);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form5: TForm5;

implementation

{$R *.dfm}

procedure TForm5.FormAfterMonitorDpiChanged(Sender: TObject; OldDPI,
  NewDPI: Integer);
begin
  ;
end;

end.

 

Share this post


Link to post

Does not happen on my PC (100 % and 150 % monitors). Windows 10 Pro Czech, 20H2, build 19042.906.

Share this post


Link to post
12 minutes ago, Bill Meyer said:

I was not involved, but I know we had some issues which were resolved by the info here:

https://stackoverflow.com/questions/23551112/how-can-i-set-the-dpiaware-property-in-a-windows-application-manifest-to-per-mo#44009779

It's not a configuration problem because the application starts with correct DPI. At the first Minimize/Maximize receive a wrong DPI (the DPI of the secondary monitor), at the second Minimize/Maximize receive the correct DPI, and so on.
But this appens ONLY if I'm using a VCLStyle and I have defined min constraint for the form and the current WindowsState is wsMaximized.

Share this post


Link to post
22 minutes ago, Carlo Barazzetta said:

It's not a configuration problem because the application starts with correct DPI. At the first Minimize/Maximize receive a wrong DPI (the DPI of the secondary monitor), at the second Minimize/Maximize receive the correct DPI, and so on.
But this appens ONLY if I'm using a VCLStyle and I have defined min constraint for the form and the current WindowsState is wsMaximized.

Understood. My recollection is that the misbehavior observed here was inconsistent. Sorry I don't have better details. What I do know is that after the fix was applied to the manifest, the system behaved as expected. Worth a try, I think.

Share this post


Link to post
7 hours ago, Vincent Parrett said:

I couldn't reproduce this here, although my second monitor shows as 144 dpi (4k at 150%).

Thanks for the test, I've changed my secondo monitor to 144dpi but the problem is the same...
I've upgraded my Windows10 version to 20H2 build but the problem still persist...

 

Share this post


Link to post

Confirmed, problem appears 🙂

 

1)

Monitor 1 @ 150 %

Monitor 2 @ 100 %

Monitor 2 is my primary monitor

 

When app is on monitor 2 (primary, 100 %), it works fine.

When app is on monitor 1 (not primary, 150 %), problem appears - app alternates between 150 % and 100 % zoom on this monitor

 

2)

Monitor 1 @ 100 %

Monitor 2 @ 150 %

Monitor 2 is my primary monitor

 

When app is on monitor 2 (primary, 150 %), it works fine.

When app is on monitor 1 (not primary, 100 %), problem appears - app alternates between 100 % and 150 % zoom on this monitor

Share this post


Link to post
1 hour ago, Vandrovnik said:

Confirmed, problem appears 🙂

 

1)

Monitor 1 @ 150 %

Monitor 2 @ 100 %

Monitor 2 is my primary monitor

 

When app is on monitor 2 (primary, 100 %), it works fine.

When app is on monitor 1 (not primary, 150 %), problem appears - app alternates between 150 % and 100 % zoom on this monitor

 

2)

Monitor 1 @ 100 %

Monitor 2 @ 150 %

Monitor 2 is my primary monitor

 

When app is on monitor 2 (primary, 150 %), it works fine.

When app is on monitor 1 (not primary, 100 %), problem appears - app alternates between 100 % and 150 % zoom on this monitor

But in my configuration the primary monitor is monitor 1 with 96 DPI, the application is on monitor 1 (the primary) and the bug is present also on primary monitor.

Edited by Carlo Barazzetta

Share this post


Link to post

At the moment I've resolved rewriting WMGetMinMaxInfo handler using other variables (MinFormWidth, MinFormHeight, MaxFormWidth, MaxFormHeight: Integer;) into my base form of the application.

 

procedure TBaseForm.WMGetMinMaxInfo(var Message: TWMGetMinMaxInfo);
var
  LMinMaxInfo: PMinMaxInfo;
begin
  if not (csReading in ComponentState) then
  begin
    LMinMaxInfo := Message.MinMaxInfo;
    with LMinMaxInfo^ do
    begin
      with ptMinTrackSize do
      begin
        if MinFormWidth > 0 then X := MinFormWidth;
        if MinFormHeight > 0 then Y := MinFormHeight;
      end;
      with ptMaxTrackSize do
      begin
        if MaxFormWidth > 0 then X := MaxFormWidth;
        if MaxFormHeight > 0 then Y := MaxFormHeight;
      end;
      ConstrainedResize(ptMinTrackSize.X, ptMinTrackSize.Y, ptMaxTrackSize.X,
        ptMaxTrackSize.Y);
    end;
  end;
  inherited;
end;

 

Share this post


Link to post

I can confirm the issue with the configuration similar to yours:

  • Monitor 1 - 100% (primary)
  • Monitor 2 - 125%
  • App's main form is shown on the primary monitor (1).

 

The bug happens if monitors are related like this (monitor 2 has negative X coordinates in its area) 

 

image.thumb.png.fdb848ae62b8713e56816c8780bbfc44.png

 

The bug does not happen if monitors are related like this:

 

image.thumb.png.ddcd30c14b7e1c93cabbfe3fe96679c8.png

 

 

Edited by balabuev

Share this post


Link to post

Ok I can now reproduce this here after reading the above, when I tried it before I didn't move it to monitor 1 (2 is my primary).  Even worse, when minimizing it the second or third time, it jumps over to the other monitors task bar, but restores to the correct monitor. Something is very broken with monitor detection. 

 

I'm seeing a similar issue with menus and vcl styles when moving between monitors - depending on which monitor a menu is first used on it will either be displayed too small or too large - it seems like it's finding the wrong monitor to get the dpi. I've been trying to debug this for days but it's very difficult to debug since that seems to impact on the behaviour.    

Share this post


Link to post

So I added some debugging info to see what is going on, iterating the screen.monitors collection

procedure TForm3.ListMonitors;
var
  i : integer;
  monitor : TMonitor;
begin
  Memo1.Lines.Clear;
  for i := 0 to Screen.MonitorCount -1 do
  begin
    monitor := Screen.Monitors[i];
    Memo1.Lines.Add('Monitor idx : ' + IntTostr(i));
    Memo1.Lines.Add('Monitor num : ' + IntTostr(monitor.MonitorNum));
    Memo1.Lines.Add('Monitor primary : ' + BoolToStr(monitor.Primary,true));
    Memo1.Lines.Add('Monitor ppi : ' + IntToStr(monitor.PixelsPerInch));
    Memo1.Lines.Add('Monitor size (Top,Left,Width,Height) : ' + IntToStr(monitor.Top) + ',' + IntToStr(monitor.Left) + ',' + IntToStr(monitor.Width) + ',' + IntToStr(monitor.Height));
  end;
  Memo1.Lines.Add('');
  monitor := Screen.MonitorFromWindow(Self.Handle);
  Memo1.Lines.Add('Current Monitor : ' + IntToStr(monitor.MonitorNum));
end;

 

My primary monitor is 1 (96dpi) which is to the left of 0 (144 dpi)  - this is after moving the window to monitor 0 (the one on the right).

Monitor idx : 0
Monitor num : 0
Monitor primary : False
Monitor ppi : 144
Monitor size (Top,Left,Width,Height) : 0,2560,3840,2160
Monitor idx : 1
Monitor num : 1
Monitor primary : True
Monitor ppi : 96
Monitor size (Top,Left,Width,Height) : 0,0,2560,1440

Current Monitor : 0

After minimizing and restoring (so still on monitor 0)

 

Monitor idx : 0
Monitor num : 0
Monitor primary : False
Monitor ppi : 144
Monitor size (Top,Left,Width,Height) : 0,2560,3840,2160
Monitor idx : 1
Monitor num : 1
Monitor primary : True
Monitor ppi : 96
Monitor size (Top,Left,Width,Height) : 0,0,2560,1440

Current Monitor : 1

 

Clearly Screen.MonitorFromHandle is returning the incorrect monitor - I suspect this is the cause of the issues I'm seeing with menus too. Looking at the code behind MonitorFromHandle I can't see what the problem is, I suspect this may be a windows bug?

 

 

Share this post


Link to post

More info

 

If I change my debug output to do this 

 

  monitor := Screen.MonitorFromPoint(Self.BoundsRect.CenterPoint);// MonitorFromWindow(Self.Handle);
  Memo1.Lines.Add('Current Monitor : ' + IntToStr(monitor.MonitorNum));

It always give the correct monitor. 

Current Monitor : 1
Windows Bounds (Top,Left,Bottom,Right) : -8,-8,1408,2568

Current Monitor : 0
Windows Bounds (Top,Left,Bottom,Right) : -11,2549,2111,6411

 

Those top/left values on monitor 0 looked odd, so I turned off themes

 

Current Monitor : 1
Windows Bounds (Top,Left,Bottom,Right) : -8,-8,1408,2568

Current Monitor : 0
Windows Bounds (Top,Left,Bottom,Right) : -11,2549,2111,6411

So that's not it, some googling found this is explained here - https://devblogs.microsoft.com/oldnewthing/20120326-00/?p=8003

 

So I'm no closer to figuring this out.

 

  • Like 1

Share this post


Link to post

I'm getting nowhere with this - something is causing windows to send WM_DPICHANGED when restoring the window - according to the documentation for WM_DPICHANGED the message is sent when

  1. The window is moved to a new monitor that has a different DPI.
  2. The DPI of the monitor hosting the window changes.

We are definitely not changing the dpi of any monitors,  so that suggests that  windows thinks the window has changed monitors. I've been looking though the vcl source but cannot see where that is happening. Since this only happens with vcl themes enabled I think it's safe to say this is probably not a windows bug. 

 

@Carlo Barazzetta did you report this yet? If so post the number here so we can vote for it. Hopefully we can find a workaround for this because I guarantee I will have customers reporting this as an issue with my software otherwise. 

Share this post


Link to post

Last one before I give up on this for today.

 

It seems to be caused by TFormStyleHook.WMWindowPosChanging calling Form.Constraints := FRestoringConstraints; - this causes a flurry of calls to SetBounds - proving difficult to debug, but at some point the incorrect bounds are set and windows then fires off the dpi changed message -while debugging I saw the window jump and then the dpi change message fired. 

Share this post


Link to post

The BDS IDE itself also goes catatonic for a long time when changing DPI.

The style management code in VCL SUCKS!

Share this post


Link to post
2 hours ago, Vincent Parrett said:

I'm getting nowhere with this - something is causing windows to send WM_DPICHANGED when restoring the window - according to the documentation for WM_DPICHANGED the message is sent when

  1. The window is moved to a new monitor that has a different DPI.
  2. The DPI of the monitor hosting the window changes.

We are definitely not changing the dpi of any monitors,  so that suggests that  windows thinks the window has changed monitors. I've been looking though the vcl source but cannot see where that is happening. Since this only happens with vcl themes enabled I think it's safe to say this is probably not a windows bug. 

 

@Carlo Barazzetta did you report this yet? If so post the number here so we can vote for it. Hopefully we can find a workaround for this because I guarantee I will have customers reporting this as an issue with my software otherwise. 

No, I haven't reported the BUG yet, I was hoping to be able to find the problem and also provide a possible solution ...

Share this post


Link to post

Handling of maximized form state is bugfull even without multi-monitor setup, and even without VCL Styles. Use the following code with a simple memo on a form:

 

procedure TForm1.FormResize(Sender: TObject);
begin
  Memo1.Lines.Add(Width.ToString + ', ' + Height.ToString + ', ' +
                  IsIconic(Handle).ToString);
end;
  • Run the project and maximize the form.
  • Minimize the form using taskbar app button.
  • Maximize it back using taskbar app button.

You will see that during minimize/maximize steps two OnResize events are fired with different Width/Height values. This was not the case in early Delphi versions, like Delphi 7; this was introduced later along with the Application.MainFormOnTaskbar feature.

 

I'm sure that all such issues are related, and the problem is that they forget to check for IsIconic() whenever appropriate.

 

 

Share this post


Link to post

So today I took a copy of the TFormStyleHook (a bit of work due to use of protected methods on TCustomForm) and registered for my form class and started adding some outputdebug strings. (BTW, it's staggering how much code there is (with many corner cases) in the stylehook! 

 

This is what happens when restoring after minimizing on monitor 0 (the non primary monitor)

TVSoftFormStyleHook.WMWindowPosChanging x : 0 y 0 w : 0 h : 0 flags[ SWP_NOMOVE + SWP_NOSIZE ]
   Form Bounds: Left: 3803 Top: 499 Width: 1302 Height: 675
   
TVSoftFormStyleHook.WMWindowPosChanging x : 2549 y -11 w : 3862 h : 2182 flags[ SWP_NOCOPYBITS + SWP_FRAMECHANGED ]
   Form Bounds: Left: 3803 Top: 499 Width: 1302 Height: 675
   
TVSoftFormStyleHook.WMWindowPosChanging x : 0 y 0 w : 0 h : 0 flags[ SWP_FRAMECHANGED + SWP_NOACTIVATE + SWP_NOZORDER + SWP_NOMOVE + SWP_NOSIZE ]
   Form Bounds: Left: 3803 Top: 499 Width: 1302 Height: 675 
TVSoftFormStyleHook.WMWindowPosChanging : Applying FRestoringContraints

TVSoftFormStyleHook.WMWindowPosChanging x : 0 y 0 w : 1302 h : 675 flags[ SWP_NOACTIVATE + SWP_NOZORDER + SWP_NOMOVE ]
   Form Bounds: Left: 3803 Top: 499 Width: 1302 Height: 675
  
SetBounds: Left: -32000 Top: -32000 Width: 1302 Height: 675

TVSoftFormStyleHook.WMWindowPosChanging x : -29440 y -32000 w : 0 h : 0 flags[ SWP_NOACTIVATE + SWP_NOZORDER + SWP_NOSIZE ]
   Form Bounds: Left: -32000 Top: -32000 Width: 1302 Height: 675
  
FormAfterMonitorDpiChanged : OldDpi : 144 NewDPI : 96

TVSoftFormStyleHook.WMWindowPosChanging x : 0 y 0 w : 0 h : 0 flags[ SWP_FRAMECHANGED + SWP_NOACTIVATE + SWP_NOZORDER + SWP_NOMOVE + SWP_NOSIZE ]
   Form Bounds: Left: -29440 Top: -32000 Width: 1302 Height: 675
   
TVSoftFormStyleHook.WMWindowPosChanging x : 0 y 0 w : 0 h : 0 flags[ SWP_NOMOVE + SWP_NOSIZE ]
   Form Bounds: Left: 2552 Top: -8 Width: 3856 Height: 2116

As you can see it is called at least once with co-ordinates that are on the other monitor (monitor 1) - which is why the errant WM_DPICHANGED is fired!! I tried to decode the flags as well 

 

I commented out this line in TVSoftFormStyleHook.WMWindowPosChanging and that seems to solve it, however we then no longer have any constraints.  

Form.Constraints := FRestoringConstraints;

So while I can see what is happening, I haven't quite figured out why - it seems like the constraints are somehow lost when minimizing,  however resetting them in WMWindowPosChanging is causing windows to believe the window has changed monitors. What a mess. 

Share this post


Link to post
9 hours ago, Vincent Parrett said:

SetBounds: Left: -32000 Top: -32000 Width: 1302 Height: 675

This is the key line. Now let's think, where this strange "-32000" numbers may araise at all. I'm sure 99.9% that the numbers are received from a Windows API function call, such as GetWindowRect (or similar) in minimized window state (IsIconic(Handle) = True).

 

Simple experiment proves it:

procedure TForm1.Timer1Timer(Sender: TObject);
var
  wr: TRect;
begin
  if WindowState = TWindowState.wsMinimized then
  begin
    GetWindowRect(Handle, wr);
    OutputDebugString(PChar(wr.Left.ToString + ', ' + wr.Top.ToString));
  end;
end;

image.png.11425ef07c92213cd50943ccf45c64f3.png

 

 

So:

9 hours ago, Vincent Parrett said:

As you can see it is called at least once with co-ordinates that are on the other monitor (monitor 1)

With "-32000" left and top values the window is not, of course, within the area of your monitor 1, but this monitor becomes the closest one to the window.

 

 

Edited by balabuev
  • Like 1

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×