36

Setting a form to WindowState = wsMaximized will sometimes cause the form to be maximized but not:

enter image description here

Long-time bug: this is a question I first asked in the Borland newsgroups in 2003:

and then again in 2006:

and then again in 2008:

Someone asked it on the Embarcadero forums in 2012:

Now it's time to port the 18 year old bug to Stackoverflow. Maybe someone's finally figured out a workaround.

Steps to reproduce:

My posts contained half a dozen failure modes, but the easiest is:

  • Drop a Label and an Edit on a form:

    enter image description here

  • Add an OnEnter event for the TEdit:

    procedure TForm1.Edit1Enter(Sender: TObject);
    begin
       Label1.Font.Style := Label1.Font.Style + [fsBold];
    end;
    
  • and set the form:

    • WindowState to wsMaximized
    • AutoScroll to False

And bazinga, fails.

One of the other set of steps from the 2008 post:

  1. Create a new app and a form.
  2. Set the form to maximized (WindowState = wsMaximized) at design time.
  3. Drop a ListView control on the form
  4. During OnShow, add 20 empty items to the list view:

    procedure TForm1.FormShow(Sender: TObject);
    var
         i: Integer;
    begin
         for i := 1 to 20 do
              ListView1.Items.Add;
    
    end;
    
  5. Set the form's AutoScroll property to false (AutoScroll = False) at design time

Of course what I'm not after is "fixed in version n of RadStudio. Just use that". I'm looking for an actual fix (if there is one); which could include quoting relevant changes to the VCL source when CodeGear finally did fix it. (If it is even fixed).

Note: Changing Position from poDesigned to anything else doesn't fix it.

Workaround

A horrible, ugly, awful, disgusting, workaround I had been using was to start a timer during OnShow, and then when the timer fires, maximize the form:

procedure TForm1.tmrVclMaximizeHackTimer(Sender: TObject);
begin
   Self.WindowState := wsMaximized;
end;

I later improved this hack to post a message during OnShow; which is essentially the same as a timer message, without having to use a timer:

const
  WM_MaximizeWindow = WM_APP + $03;

procedure TForm1.FormShow(Sender: TObject);
begin
  if (Self.WindowState = wsMaximized) then
  begin
     Self.WindowState := wsNormal;
     PostMessage(Self.Handle, WM_MaximizeWindow , 0, 0);
  end;
end;

private
   procedure WMMaximizeWindow(var Message: TMessage); message WM_MaximizeWindow;

procedure TForm1.WMMaximizeWindow(var Message: TMessage);
begin
   Self.WindowState := wsMaximized;
end;

Sometimes I invent the OnAfterShow event that Delphi never did:

const
  WM_AfterShow = WM_APP + $02;

procedure TForm1.FormShow(Sender: TObject);
begin
  PostMessage(Self.Handle, WM_AfterShow, 0, 0);
  if (Self.WindowState = wsMaximized) then
  begin
     Self.WindowState := wsNormal;
     FMaximizeNeeded := True;
  end;
end;

private
   procedure WMAfterShow(var Message: TMessage); message WM_AfterShow;

procedure TForm1.WMAfterShow(var Message: TMessage);
begin
   if FMaximizeNeeded then
   begin    
      FMaximizeNeeded := False;
      Self.WindowState := wsMaximized;
   end;
end;

But no hacks are better than hacks.

Johan
  • 71,222
  • 23
  • 174
  • 298
Ian Boyd
  • 220,884
  • 228
  • 805
  • 1,125

5 Answers5

13

I Can reproduce with D7/Win7.

I don't use wsMaximized at all (similar random problems as you describe).

Workaround: use OnActivate -> ShowWindow(Handle, SW_MAXIMIZE) e.g.:

procedure TForm1.FormActivate(Sender: TObject);
begin
  // Maximize only once when the Form is first activated
  if not FMaxsimized then
  begin
    FMaxsimized := True;
    ShowWindow(Handle, SW_MAXIMIZE);
  end;
end;

This method will not work during OnShow.

Better Workaround: use ShowWindowAsync during OnShow or OnCreate e.g:

procedure TForm1.FormCreate(Sender: TObject);
begin
  ShowWindowAsync(Handle, SW_MAXIMIZE);
end;

This sets the show state of a window without waiting for the operation to complete.

kobik
  • 20,439
  • 4
  • 54
  • 115
  • You wouldn't want to maximize the form during `OnActivate`, otherwise the form would try to maximize when it is activated (i.e. *every* time it is activated) – Ian Boyd Nov 02 '13 at 01:17
  • 5
    @Ian Boyd the workaround using ShowWindowAsync is fairly elegant though isn't it? Certainly better than the hack. – Tim Ebenezer Nov 02 '13 at 09:13
  • 1
    @IanBoyd, in practice you would use a Boolean flag to indicate that the form was already maximized/activated the first time. I'll make an edit. – kobik Nov 02 '13 at 10:40
  • Not that it's not a nice workaround, but basically ShowWindowAsync is the PostMessage hack in Ian's answer implemented by the system itself. With both workarounds, if you put a breakpoint to the right place, you can see that the window is shown normal before it's maximized. – Sertac Akyuz Nov 02 '13 at 13:42
10

I only tested the first reproduction case (with D7, D2007, XE2), and am able to duplicate the problem with D7 and D2007 but not with XE2.

The problem, as I see it, is that the label, having its font changed, requests its parent to re-align itself. This eventually leads to a SetWindowPos call on the form (in TWinControl.AdjustSize) with restored width/height even though the form is already maximized - which leads to the strange, behaviorally maximized but not visually maximized, form sitting on the screen.


I traced the code in D2007 and XE2 to be able to come up with what is different. The code in TWinControl.AlignControls is different between the two versions. What specifically matters is the last statement.

D2007:

procedure TWinControl.AlignControls(AControl: TControl; var Rect: TRect);

  ..
  { Apply any constraints }
  if Showing then AdjustSize;
end;

XE2:

procedure TWinControl.AlignControls(AControl: TControl; var Rect: TRect);

  ..
    // Apply any constraints
    if FAutoSize and Showing then
      DoAdjustSize;
end;

I hope this, somehow, helps you devising/deciding what workaround to use.



The workaround I could suggest (although I haven't tested it throughly) is to force show the form maximized early:
procedure TForm1.FormCreate(Sender: TObject);
var
  wplc: TWindowPlacement;
begin
  if not AutoScroll and (WindowState = wsMaximized) then begin
    wplc.length := SizeOf(wplc);
    GetWindowPlacement(Handle, @wplc);
    wplc.rcNormalPosition.Right := wplc.rcNormalPosition.Left + Width;
    wplc.rcNormalPosition.Bottom := wplc.rcNormalPosition.Top + Height;
    wplc.showCmd := SW_MAXIMIZE;
    SetWindowPlacement(Handle, @wplc);
  end;
end;

The above works because it forces to set the focus to the edit control (OnEnter event) before the VCL sets the visible flag for the form. In turn, the label's alignment request does not result with form size adjustment. Also, since, by the time VCL calls ShowWindow the form's window is already visible, it doesn't cause the form to be shown in a restored state at any stage.

However, I don't know if it would help with different reproduction scenarios.


Finally, although I can see that the behavior is corrected in newer Delphi versions, I wouldn't consider this to be a bug in the VCL. In my opinion, user code should be responsible not to cause window adjustment while window showing state is changing. The course of action I'd take for the specific scenario would be to defer to modify label's font until the VCL is done displaying the form.

Sertac Akyuz
  • 52,752
  • 4
  • 91
  • 157
  • 1
    +1! I prefer your solution with `SetWindowPlacement`, because the form is showing with a maximized state right away (as oppose to my solution where the window is shown normal before it's maximized). Also, Your method works whether `wsMaximized` is preset or not. (personally I would not use `wsMaximized`, omit the `if not AutoScroll` check, and use `SetWindowPlacement` to maximize). Also, well done for the analysis :). – kobik Nov 03 '13 at 11:04
  • @kobik - Thanks! - *".. omit the if not AutoScroll .."* - Just wanted to be least intrusive to the VCL as possible. – Sertac Akyuz Nov 03 '13 at 13:00
  • My steps to reproduce no longer reproduce with XE6. So it seems that CodeGear (or were they still Imprise back then, or had they changed their name to Embargadero already) had fixed it around the XE2 timeframe. – Ian Boyd Nov 21 '14 at 16:14
1

I don't think this is a bug in Delphi but rather a bug (or just odd behavior) in the Windows CreateWindow function. If you search for CreateWindow and WS_MAXIMIZE not working you'll find similarly very old threads and discussions from people calling CreateWindow or CreateWindowEx passing WS_MAXIMIZE in the style parameter and not seeing a maximized window when they run the application.

Excerpt from an old gamedev.net thread

the problem is that WS_MAXIMIZE apparently does not apply when using WS_OVERLAPPEDWINDOW. if you replace WS_OVERLAPPEDWINDOW with WS_POPUP you will get a maximized window. of course this may not apply to all versions of Windows, or even all versions of the Windows shell UI for that matter.

WS_OVERLAPPEDWINDOW is MS's old default window "type" and they apparently coded CreateWindow/Ex to ignore certain styles, thinking that ShowWindow would be called with SW_SHOWDEFAULT, which causes the window to be displayed according to the CreateProcess startup info parms. this ultimately gives the user the control of how an app's main window would be displayed by using the shell's shortcut settings.

The workaround is just to call ShowWindow. It should work in Delphi, too:

procedure TForm1.FormShow(Sender: TObject);
begin
   ShowWindow(Handle, SW_MAXIMIZE);
end;
Community
  • 1
  • 1
gordy
  • 8,350
  • 1
  • 27
  • 40
  • I don't think so. Switching 'autoscroll' fixing the issue suggests that the OS does not have any problem with it. If you trace the VCL code, you'll see that the scenario in the question leads to a SetWindowsPos call on a maximized window with restored width/height, which is likely part of the problem. – Sertac Akyuz Nov 01 '13 at 20:52
  • Also notice that the window *thinks* it's maximized; the border icon has it in the **maximized** state. Except it's not really. i'll even see the bug in 3rd party applications; a dead giveaway that it's a Delphi app (that and the incorrectly drawn tooltip windows) – Ian Boyd Nov 02 '13 at 01:22
0

Hope the solution i use helps others (i known window is first shown with design time size):

  • Add a timer with interval as less as just 1 (do not put 0).
  • Code for it: theTimer.Enabled:=False;WindowState:=wsMaximized;

It never fail to me.

As soon as form is shown and all pending tasks for such show are finished, the timer triggers and window is maximized.

Some time ago, i was using the Trick of sending a mouse click where maximize button was, but i discovered checking Windows OS version, plugins (on Linux), etc makes the thing so hard. That one worked exactly as if the user asks for maximize the window.

The Timer i now use does exactly the same, but avoid OS checking, etc.

Not to mention: Put WindowState to wsNormal on DesignTime (do not set it to wsMinimized, neither to wsMaximized).

0

Wow! i did not see on the post:

ShowWindowAsync(Handle,SW_MAXIMIZE);

Thanks for that, with it my problem is solved much better than with a Timer, but not perfect, it still causes a little flicker (window is shown on incomplete render, then it goes to maxized state), it is much better than the timer, less time shown in non maximized state.

And it is compatible with a prior SetBounds() on the OnShow method.

I wish: Set initial size (Width, Height) and maybe also initial position (Left, Top) of a form prior to show it, but that form must be shown maximized; such position and sizes are for when the user un-maximize the window.

That ShowWindowAsync works perfect for such objective, and no need to add an ugly timer (with interval=1 and .Enabled=False on its code as first sentence).

How could i miss it when i read the post!

So, now on i will use (just as example os initial size relative to monitor):

procedure TtheForm.FormShow(Sender: TObject);
var
   theInitialDefaultWidth,theInitialDefaultHeight:Integer;
begin
     theInitialDefaultWidth:=Round(Screen.Width*3/5);
     theInitialDefaultHeight:=Round(Screen.Height*3/5);
     WindowState:=wsNormal; // So it can still have at design time wsMaximized, this is for the SetBounds to work on a non maximized state
     SetBounds((Screen.Width-theInitialDefaultWidth)div 2,(Screen.Height-theInitialDefaultHeight)div 2,theInitialDefaultWidth,theInitialDefaultHeight); // Set default position and default size as i wish
     ShowWindowAsync(Handle,SW_MAXIMIZE); // Make the window to be shown maximized when it will be visible
     // ... // Rest of actions for the FormShow method
end;

Works perfect! I do not need to touch design time properties, i can let them as they are (WindowState=wsMaximized, Position=poScreenCenter, etc).. 100% code solution for the problem.

Thanks a lot!

P.D.: Will it work on Linux? I mean when code is compiled for Linux (in Lazarus), i must test it and see, if it does work it will be a great immprove on what i was using till now.

claudio
  • 1
  • 1