Wrong window position values when program is being closed while minimized - delphi

On my Windows 7 desktop, I have the Windows Taskbar attached at the LEFT side of the screen (instead of default on the bottom) and a custom desktop toolbar ("True Launch Bar") attached at the TOP of the screen.
In a Delphi XE8 VCL project, I save the main window position values (formMain.Top and formMain.Left) in the FormClose event, and then at program start I restore the main window position with these values in the FormCreate event.
This normally works well. However, when the program is closed while MINIMIZED (formMain.WindowState = wsMinimized) then the form position values are wrong (i.e. REDUCED by the width/height of the toolbars) and so the window is restored at a wrong position at program start.
So how can I solve this problem?
EDIT: I tried the other solution mentioned by David:
var
WindowPlacement: TWindowPlacement;
R: TRect;
....
WindowPlacement.Length := SizeOf(WindowPlacement);
Win32Check(GetWindowPlacement(formMain.Handle, #WindowPlacement));
R := WindowPlacement.rcNormalPosition;
CodeSite.Send('formMainLeft by WinAPI', R.Left); // normal: 323 minimized: 323
CodeSite.Send('VCL formMain.Left', formMain.Left); // normal: 423 minimized: 323
However, this produces the same problem, as it does not take into account the space occupied by the toolbars, as it gets only the WORK AREA values.
Moreover, this is not a duplicate question as mentioned by David, but a SIMILAR question. This problem derives from wrong screen values when in Minimized state, while the other question is about overall restoring state and size.
Please also note: I do NOT want to restore the window state (e.g. Minimized) AND position, but ONLY the window position, so I cannot use SetWindowPlacement as mentioned by David in the other question.
EDIT2: I have now solved the problem with this code:
if formMain.WindowState = wsMinimized then
begin
MinimizedOffsetTop := Screen.WorkAreaTop;
MinimizedOffsetLeft := Screen.WorkAreaLeft;
end
else if formMain.WindowState = wsNormal then
begin
MinimizedOffsetTop := 0;
MinimizedOffsetLeft := 0;
end;
SettingsIni.WriteInteger('Persistence', 'Top', formMain.Top + MinimizedOffsetTop);
SettingsIni.WriteInteger('Persistence', 'Left', formMain.Left + MinimizedOffsetLeft);
(In the case the window is Maximized, I do not save and restore the window position but do only save and restore the Maximized window state).

When a window is minimized, it remembers the last normalized bounds and will restore itself there when you restore the window. These bounds are made available to you through GetWindowPlacement. This API returns work area relative coordinates. It does so because that allows for a window to be minimized and restored to the same work area relative location, even if the work area has changed in the intervening time.
Clearly the VCL calls GetWindowPlacement when you ask for Left and Top of a minimized window. How else would it get the coordinates it returns? And of course, it returns work area relative coordinates which is what is confusing you. One might consider it a bug that these properties are sometimes screen relative and other times work area relative.
Your solution is obvious though. Obtain work area relative coordinates by calling GetWindowPlacement. When you need to re-apply these coordinates, do so by calling SetWindowPlacement.
You say that you cannot use SetWindowPlacement because that forces the window to be minimized. But that's not the case. Set the showCmd member to SW_SHOWNORMAL or SW_RESTORE.
We've been using these APIs to store and restore window positions for many many years. They are known to work well.

Related

How to maximize form to desktop size?

In a Delphi 10.4.2 Win32 VCL application running on Windows 10, in a dual-monitor setup, when I set my MainForm (or any other secondary form) to start maximized by setting WindowState := wsMaximized, then the form is maximized only on the Primary Monitor.
How can I maximize the Form to the whole Desktop instead, to set the Form size to include BOTH MONITORS? Is there a built-in method of the TForm class to achieve this?
In general, this problem isn't as simple as you may think. I suppose you are imagining a desktop like this:
In this case, I assume you want the window to be placed like this:
However, what if the user has this layout:
Do you want
(entire window visible, but some screen space unused) or
(no unused space, but some parts of the window not visible)?
If you want to use the full virtual desktop space -- the last case -- it is easy though:
BoundsRect := Screen.DesktopRect;
This will do the expected thing in a simple setup, and the "no unused space, but some parts of the window might not be visible" thing in general.
Also be aware that Windows doesn't like that windows behave like this, so the user might not get a nice experience using the app.
In general, don't do this.
Please note that even a two-monitor setup, in which both monitors are landscape, can be non-trivial:
The geometry may be non-trivial even if both monitors are the same size:
Per MSDN:
Positioning Objects on Multiple Display Monitors
A window or menu that is on more than one monitor causes visual disruption for a viewer. To minimize this problem, the system displays menus and new and maximized windows on one monitor.
So, if you want the TForm window to stretch across the whole desktop, using WindowState=wsMaximize is not the way to go, as it will only work on the single monitor that the Form is being mostly displayed in.
To do what you ask, you will have to get the rectangle of the Virtual Screen from GetSystemMetrics() (or Vcl.Forms.TScreen), and then set the Form's Left/Top/Width/Height accordingly, eg:
if Screen.MonitorCount > 1 then
begin
Form.WindowState := wsNormal;
Form.Left := Screen.DesktopLeft;
Form.Top := Screen.DesktopTop;
Form.Width := Screen.DesktopWidth;
Form.Height := Screen.DesktopHeight;
// or:
Form.SetBounds(Screen.DesktopLeft, Screen.DesktopTop, Screen.DesktopWidth, Screen.DesktopHeight);
// or:
Form.BoundsRect := Screen.DesktopRect;
end else
begin
Form.WindowState := wsMaximized;
end;
This is not standard behaviour for a Windows application. Also note that as the desktop can have multiple monitors which do not need to be aligned so the desktop may not be a rectangle - which means that the bounding rectangle for the desktop may contain parts which are not visible.
If you want to do this you can use the Windows function GetDesktopWindow to get the desktop window, then get its size, and then set the size of the form to that.
procedure TMyForm.GoLarge();
var
rctDesktop: TRect;
hDT: HWND;
begin
hDT:=GetDesktopWindow();
if(hDT<>0) then
begin
GetWindowRect(hDT, rctDesktop);
Self.SetBounds(rctDesktop.Left, rctDesktop.Top, rctDesktop.Width, rctDesktop.Height);
end;
end;
This makes no allowance for the task bar or anything else which is using some of the desktop space.

Mouse.CursorPos not working

I want the user to be able to set the mouse cursor position over an item in a TDBGrid of their choice at start up.
I have a Popup menu, and CursorPoint is a global TPoint the X and Y of which are saved to an .ini file and loaded at start up. But currently it is doing nothing with the cursor.
On menu popup...
procedure TfrmMain.mnuGridPopup(Sender: TObject);
begin
Windows.GetCursorPos(CursorPoint);
end;
Then, to test the position I have a TButton
procedure TfrmMain.Button1Click(Sender: TObject);
begin
Mouse.CursorPos:=ClientToScreen(CursorPoint);
end;
That too, does not move the cursor, so, what am I doing wrong?
When you call Windows.GetCursorPos(CursorPoint); you get mouse cursor position using screen coordinates and not window specific coordinates. But later on you treat theese coordinates as if they were window-specific coordinates ClientToScreen(CursorPoint);. This of course results in you trying to move the mouse cursor to a wrong position.
So when you are saving mouse cursor position do make sure to convert those coordinates into window-specific ones prior to saving them using ScreenToClient() method.
But otherwise, as mentioned by Warren P, not all users would like for your application to move their mouse cursor. Why? Because they won't be able to find it and will eventually do erratic mouse movements to find out where the cursor is. So as soon as they do this your approach loses its purpose.

How to make a form align to the edge of the screen like the taskbar?

I'm building a "dashboard" application which is always visible along any edge of any given monitor, even when other applications are maximized. I don't necessarily need it "always on top" (although I will) but I need to make it a part of the screen as my own desktop toolbar, like the Windows Taskbar is. Even when applications are maximized, the windows are inside of this area, making this window always visible (and the desktop area smaller).
How can I make my application's main form align to the edge of a screen like this?
PS - I don't need an answer to all the extra gritty handling, such as screen resolution changes... I just need to know how to make it aligned as "part of the screen" in the first place.
You're looking for Application Desktop Toolbars, which is what the Windows task bar uses internally. It involves creating a window with specific styles, setting it up correctly, and then communicating with it using SHAppBarMessage.
It can get pretty complex, but there are some free components available with source (one at Torry, or another at DelphiPages) that have the basic shell to get you started.
An example from the AppBar.pas unit of the second link (which, according to the link's text, is freeware with source - I've used it to create an app launcher task bar, complete with buttons with application icons and descriptions read from .lnk files):
type
TAppBarMessage = (abmNew, abmRemove, abmQueryPos, abmSetPos, abmGetState,
abmGetTaskBarPos, abmActivate, abmGetAutoHideBar,
abmSetAutoHideBar, abmWindowPosChanged);
TAppBarEdge = (abeLeft, abeTop, abeRight, abeBottom, abeUnknown, abeFloat);
...
function TAppBar.AppBarMessage(abMessage: TAppBarMessage;
abEdge: TAppBarEdge; lParam: LPARAM; bRect: Boolean; var rc: TRect): UINT;
var
abd: TAppBarData;
begin
// Initialize an APPBARDATA structure
abd.cbSize := sizeof(abd);
abd.hWnd := Handle;
abd.uCallbackMessage := WM_APPBARNOTIFY;
abd.uEdge := Ord(abEdge);
if bRect then
abd.rc := rc
else
abd.rc := Rect(0, 0, 0, 0);
abd.lParam := lParam;
Result := SHAppBarMessage(Ord(abMessage), abd);
// If the caller passed a rectangle, return the updated rectangle
if bRect then
rc := abd.rc;
end;
If nothing else, you can determine this information manually. Have a look at the global Screen object in the Forms unit for information on the current resolution. (Make sure to check the MonitorCount and Monitors properties.)
Between that and a bit of basic arithmetic, it shouldn't be too hard to set up the form to align to the edge of a monitor.

What can I do about maximized, styled windows, which show their borders on adjacent monitors?

On a multi-monitor system, a "blank" VCL application maximizes fine, but the same application with styles enabled (and one chosen as default) maximizes incorrectly. What I'm seeing is the right-hand edge of the window extend onto the 2nd monitor (my main is on the left). When I started comparing to other Windows apps, I noticed that under Windows 7 (at least), maximized windows do not even have non-client borders on the left, right or bottom sides. And indeed, the standard VCL (non-styled) app behaves this same way, without non-client borders.
How do I fix this? I notice that TFormStyleHook has a handler for WMNCCalcSize, which I haven't dissected yet, but makes me wonder if VCL might be incorrectly handling this message for a maximized window.
After fiddling some time on this, my take is, this is not a vcl-styles bug at all. This indeed is related with the behavior in the article mentioned in a comment to the question by mghie.
The specific behavior is that, the size of a maximized window is larger than the work area of the monitor that the window is maximized on. Supposedly, the window manager hides the overhang borders. Apparently, it doesn't quite do so with customized frames. Note that MSDN's own custom window frame example seems to suffer from the same problem (refer to the post titled "Bug when window is Maximized " in community content). VCL's application is different than the MSDN example in that it's not based on DWM, but I still think it is the same issue.
The overhang borders have the size of the system sizing border (SM_C[X|Y]SIZEFRAME), but this is irrelevant to the workaround below as it disregards the OS suggested size/position and uses the work area.
Unfortunately I don't think this workaround is usable at all. For one, the mentioned behavior is not documented, for two, the workaround is not perfect; there's still an odd pixel out. If you snap the window exactly on the work area, the window manager decides to offset the window to where it thinks the window (with hidden frames) should be placed. (The VCL could probably be modified to do what the window manager does, and take into account overhang and do not draw them or something similar, but it would be more work and it still would be to workaround undocumented behavior..)
Anyway;
type
TForm1 = class(TForm)
..
protected
// overriding styles is not necessary since TFormStyleHook.WMGetMinMaxInfo
// first calls the default window procedure
procedure WMGetMinMaxInfo(var Message: TWMGetMinMaxInfo);
message WM_GETMINMAXINFO;
..
procedure TForm1.WMGetMinMaxInfo(var Message: TWMGetMinMaxInfo);
var
R: TRect;
begin
// always arrives with MinMaxInfo.ptMaxPosition = (-SM_CXFRAME, -SM_CYFRAME)
// and MinMaxInfo.ptMaxSize = (PrimaryMonitor.Width (?) + 2 * SM_CXFRAME, ... )
inherited;
// should test for OS, styles etc. before running the below
R := Monitor.WorkareaRect;
InflateRect(R, -1, -1); // odd pixel
OffsetRect(R, -Monitor.Left, -Monitor.Top);
Message.MinMaxInfo.ptMaxPosition := R.TopLeft;
Message.MinMaxInfo.ptMaxSize := Point(R.Width, R.Height);
end;
The only way i found is to handle WM_SIZE event and modify the window region to chop the extra border.

Start program on a second monitor?

Is there a way to specify which monitor a application appears on in Delphi or C++Builder?
I am developing a simple program for a customer, which displays kitchen orders on a secondary monitor, generated by a hospitality system. Currently they need to manually drag the window onto the second monitor after it starts.
The global Screen object (part of Forms) has the concept of Monitors. I think this was added circa Delphi 6 or 7. The following code will work:
// Put the form in the upper left corner of the 2nd monitor
// if more then one monitor is present.
if Screen.MonitorCount > 1 then
begin
Left := Screen.Monitors[1].Left;
Top := Screen.Monitors[1].Top;
end;
You could use any positive offset from that position to put it anywhere in that monitor. You can get the width and height from there too to know the dimensions.
Save the window position before program shutdown and restore them on startup. Multimonitor displays just increase the size of the desktop; other monitor surfaces just have a different section of the same X/Y plane with its origin at the top-left of the primary monitor.
This can be done automatically for you by any of several components.
BTW, the Screen variable in the Forms unit has a property called MonitorCount and another indexable property, Monitors[Index: Integer]: TMonitor. TMonitor has properties indicating the left, top, width, height etc., so all the information you need is there.
procedure TMDIChild.btnShowMonClick(Sender: TObject);
begin
if Screen.MonitorCount > 1 then
begin
FormShow.Left:=Screen.Monitors[1].Left;
FormShow.Top:=Screen.Monitors[1].Top;
FormShow.Width:=Screen.Monitors[1].Width;
FormShow.Height:=Screen.Monitors[1].Height;
end
else
begin
FormShow.Show;
end;
end;
Not really the answer your question implies, but couldn't you store the window settings (size, position, Maximized State) when ever the application is closed, and then apply them at startup?
I have done a similar thing a while ago in Delphi 5:
procedure TForm18.FormCreate(Sender: TObject);
var
Mon: TMonitor;
MonitorIdx: Integer;
begin
MonitorIdx := 1; // better read from configuration
if (MonitorIdx <> Monitor.MonitorNum) and (MonitorIdx < Screen.MonitorCount) then begin
Mon := Screen.Monitors[MonitorIdx];
Left := Left + Mon.Left - Monitor.Left;
Top := Top + Mon.Top - Monitor.Top;
end;
end;
Windows will let you specify the coordinates of a window in the CreateWindow API call. I don't know enough about Delphi or C++Builder to know if you have access to that part of the process.
You might also be able to move the window in a WM_CREATE handler.
EnumDisplayMonitors will give you the coordinates of each monitor in the system.
Evidently Delphi and C++ Builder have facilities that make this answer somewhat irrelevant. I'll leave it here in case someone comes across this question but needs it answered for a different environment.
I don't do much with windows systems, so I would suggest a hack like this.
Grab the width of the viewable desktop(both monitors combined), divide it by half and make that your starting position.
You may also look into what api tells you monitor2's dimensions.

Resources