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.
Related
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.
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.
I'd like to display a form off of a systray icon event, which just shows some information next to the taskbar and disappears itself after some time. The main issue I'm running into is positioning the form in a way that it is both in the correct position and visible. I found a couple of ways to determine where the icon is, but in testing I found them inconsistent based on OS (I attempted this in another app and ended up giving up and using a centered modal form). For example:
procedure GetWorkAreaRect(var outrect: TRect);
// returns the dimensions of the work area.
begin
Systemparametersinfo(SPI_GETWORKAREA, 0, #outrect, 0);
end;
The problem when that works is determining from there where to put the form (above, below, left, right). Any suggestions?
Edit: The problem is in positioning the form in relationship to the system tray icon, not necessarily determining where the system tray icon is. I made another attempt and got it working as long as some conditions are met. Most notably, it doesn't work if the taskbar is set to auto-hide, because the assumption is made that the click is made outside of the work area. This is not true when the form is set to auto-hide.
function PositionForm(X, Y, Width, Height: Integer): TPoint;
// receives mouse-click position in X and Y, form width and height in width and height
// returns Left and Top in TPoint.X and TPoint.Y.
var
workrect: TRect;
resrect: TPoint;
begin
GetWorkAreaRect(workrect);
if Y > WorkRect.Bottom then // taskbar is on bottom
begin
resRect.X := WorkRect.Right - Width;
resrect.Y := WorkRect.Bottom - Height;
end
else
if X > WorkRect.Right then // taskbar is on right
begin
resrect.X := WorkRect.Right - Width;
resrect.Y := WorkRect.Bottom - Height;
end
else
if X < WorkRect.Left then // taskbar is on left
begin
resrect.X := WorkRect.Left;
resrect.Y := WorkRect.Bottom - Height;
end
else
if Y < WorkRect.Top then // taskbar is on top
begin
resrect.X := WorkRect.Right - Width;
resrect.Y := WorkRect.Top;
end;
Result := ResRect;
end;
So on the surface, it seems the issue is to find an independent way to determine where the taskbar resides...or could the logic be extended above to take care of this?
When your notification icon receives the message corresponding to an action, you can query at that point to find out an associated point on the screen.
For example if you are handling WM_RBUTTONUP, WM_CONTEXTMENU etc. in your icon's message procedure you can call GetMessagePos to find out the position on the icon associated with the message.
I wrap this up with the following function so that I can decode the message into a TPoint:
function MessagePoint: TPoint;
begin
Result := TSmallPoint(GetMessagePos());
end;
So what you can do is, in your icon's message procedure, make a note of this point. When you need to show the form, use this point to determine where your icon lives. Since the point can be in the taskbar, you'll need to clip it into the work area.
After your question update it seems you want to know how to find out the location of the taskbar. Do that by calling SHAppBarMessage passing ABM_GETTASKBARPOS.
Windows does not expose a native way to query where system tray icons are positioned, or even if they are visible at all. But you can do it manually with some lower level API trickery, as demonstrated in the following article:
CTrayIconPosition - where is my tray icon?
That works up to XP, at least, maybe even Vista. Windows 7 drastically redesigned the way the system tray acts, so I do not know if these techniques still work anymore.
You can use TJvDesktopAlert to display simple notifications, if you have JCL and JVCL.
procedure TForm1.ShowDesktopAlert(const AHeader, AMessage: string);
begin
with TJvDesktopAlert.Create(nil) do
begin
StyleOptions.DisplayDuration := 5000;
HeaderText := AHeader;
MessageText := AMessage;
AutoFree := True;
Execute;
end;
end;
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.
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.