Why does TForm.SetBounds only work correctly when TForm.Position is set to poDefault at design time
Asked Answered
F

1

7

I have noticed something very strange. I am persisting the top, left, width, and height properties of a form when it is closing, and using this information to restore the form's last position when it is once again opened by calling SetBounds using the previously stored information. This works well, but only if the form's Position property is set to poDefault at design time. If set to something else, such as poDesigned, poScreenCenter, or poMainFormCenter, SetBounds does not restore the form's previous position and size.

Here's the strange part. What appears to matter is what the Position property is set to at design time. I can change the value of this property at runtime to poDefault and the call to SetBounds still does not work correctly. I have tried something like the following

if Self.Position <> poDefault then
  Self.Position := poDefault;

in both the form's OnCreate event handler, as well as from an overridden constructor (and have set Position to poDefault in the constructor, and called SetBounds in the OnCreate event handler). In all cases, changing the form's Position property to poDefault at runtime does not fix the problem that I've observed with SetBounds. The only consistent pattern that I have found is that SetBounds works as it should only if the form's Position property was poDefault at design time.

There are other things that I've noticed with respect to how SetBounds works when a form's Position property is not set to poDefault at design time. For example, a form whose Position property is set to poScreenCenter at design time will not necessarily appear centered on the screen if you call SetBounds. However, it does not appear in the top-left location defined by SetBounds, nor does it respect the width and height specified in the call to SetBounds. Let me repeat, however, that I am setting the Position property of the form to poDefault before calling SetBounds. I've even stuck a call to Application.ProcessMessages between the two operations, but that doesn't fix the problem.

I have tested this extensively with Delphi 10.1 Berlin running on Windows 10. I have also tested it using Delphi XE6 on Windows 7. Same results.

If you have doubts, create a VCL application with four forms. On the first form place three buttons, and add something like the following OnClick to each button:

 with TForm2.Create(nil) do
 try
   ShowModal;
 finally
   Release;
 end;

where the constructor creates TForm2, then TForm3 and TForm4.

On the OnCreate of forms 2 through 4, add the following code:

if Self.Position <> poDefault then
  Self.Position := poDefault;
Self.SetBounds(500,500,500,500);

On form2, set Position to poDefault, on form3 set Position to poScreenCenter, and on form4 leave Position set to the default, poDefaultPosOnly. Only form2 will appear at 500, 500, with a width of 500 and a height of 500.

Does anyone have a logical explanation for this result?

Flushing answered 19/8, 2016 at 9:5 Comment(2)
Try to add procedure CreateParams(var Params: TCreateParams); override; to protected section of your form definition and write ` inherited; if Self.Position <> poDefault then Self.Position := poDefault; ` in CreateParams implementation.Infinity
Miamy: Overridding CreateParams, and setting Position to poDefault worked!. Very nice! It's not the answer (since I was asking for an explanation), but it is a very nice solution to the problem. And, since all of my forms have a common ancestor, I can override this method in the ancestor and then be free to call SetBounds in the OnCreate event handler (or the overridden constructor). Thanks for the suggestion.Flushing
C
4

poDefault and friends mean "let Microsoft Windows position this form's window when the form would create and show it".

You just created Delphi object - but I wonder if it also has created/shown Windows object (HWND handle and all corresponding Windows internal structures). Especially with themed applications, not ones using standard pre-XP look and feel - they tend to ReCreateHWND when showing, because pre-loading those fancy Windows Themes is relatively expensive operation and only should be done when needed.

I think your default bounds (every property value set in the constructor might be considered a default non-tuned value, to be tuned later after object being constructed) are correctly ignored when you (or TApplication - that makes little difference for the topic) finally do FormXXX.Show.

It is during "make me a window and display it" sequence when your form looks at its properties and tells to MS Windows something like "now I want to create your internal HWND-object and position it at default coordinates/size at your discretion".

And that is absolutely correct behaviour - otherwise WHEN and HOW could TForm apply the Position property??? It just makes no sense to ask Windows for coordinates of a window that does not exists on the screen yet and maybe never would. Windows offers default coords/sizes for the this very second it being asked, looking how many other windows are there and where they are positioned ( and AMD/NVidia video drivers might also apply their correction to it).

It would make little sense, to acquire defaults now, and apply them two hours later when everything would probably be different - different amount of other windows and different positions of those, different set of monitors attached and with different resolutions, etc.

Just consider a "desktop replacement" type of notebook. It was set upon the table connected to large stationary external monitor. Then - let's imagine it - I run your application and it created the tform Delphi object and in the constructor it asked MS Windows for position - and Windows rightfully offered the position at that very secondary large monitor. But then an hour later I unplugged the notebook and walked away with it. Now an hour later I tell your application to show the form - and it will do what? display it with coordinates belonging to that now-detached external display? Outside of the viewport of the notebook's internal display that I only have at the moment? Should this form be displayed in the now "invisible" position just because when I started the application back then that spot was still visible there yet??? Way to confuse users for no gain, I think.

So the only correct behaviour would be to ask Windows for default coords this very second WHEN the form is going from hidden to visible and not a second earlier.

And that means that if you want to move your form - you should do it after it was being show. Place your Self.SetBounds(500,500,500,500); into OnShow event handler. So let the MS Windows materialize your form into default position like required by poDefault in Position property - and move your Window after that. Attempts to move the window that does not exist yet look correctly futile to me.

Either PRESET your form ( in constructing sequence) to explicitly ignore MS Windows defaults and use pre-set cords (via poDesigned value), or let the form ask Windows coordinates, but MOVE it with SetBounds after it got visible via OnShow handler.

Chinookan answered 19/8, 2016 at 9:36 Comment(5)
It seems to be enough to call HandleNeeded before SetBounds.Livvy
To begin with, my code is more complex than I implied. When restoring position, I take into account changes to monitor resolution and number of monitors. But that's beside the point. This appears to be the correct answer (except that SetBounds does not work in OnShow, but does work with OnActivate - though I try to avoid using both of these event handlers). However, given that overriding the CreateParams method (as suggested in the comment by Miamy) permits Position to be changed to poDefault, after which SetBounds works from OnCreate (or overridden constructor) this explanation makes sense.Flushing
@OndrejKelle - yes, probably. But why would you use dirty hacks, making unexpected complex calls in the constructor and potentially triggering long chains of event handlers on half-created window, when you can just shift the MOVE WINDOW action to the place VCL genuinely expects it to be?Indeclinable
@CaryJensen "When restoring position, I take into account changes to monitor resolution and number of monitors." - there are ex-RxLib componetns for it in JediVCL - TJvFormPlacement and TJvFormStorage - look at them and learn from their code ( or just use it): they seem to do just what you described and they already identified and fixed those problems you was asking about here :-)Indeclinable
@CaryJensen you could also do PostMessage from OnShow to change the window size and position. Your custom WM_xxx or standard Windows messages. However personally I'd just drop the aforementioned or similar components onto the form and avoid writing any explicit code there.Indeclinable

© 2022 - 2024 — McMap. All rights reserved.