Inno-Setup-issrc/Projects/Src/Shared.CommonFunc.Vcl.pas

524 lines
19 KiB
ObjectPascal

unit Shared.CommonFunc.Vcl;
{
Inno Setup
Copyright (C) 1997-2025 Jordan Russell
Portions by Martijn Laan
For conditions of distribution and use, see LICENSE.TXT.
Common VCL functions
}
{$B-}
interface
uses
Windows, Messages, SysUtils, Forms, Graphics, Controls, StdCtrls, Classes;
type
TWindowDisabler = class
private
FFallbackWnd, FOwnerWnd: HWND;
FPreviousActiveWnd, FPreviousFocusWnd: HWND;
FWindowList: Pointer;
public
constructor Create;
destructor Destroy; override;
end;
{ Note: This type is also present in Compiler.ScriptFunc.pas }
TMsgBoxType = (mbInformation, mbConfirmation, mbError, mbCriticalError);
TMsgBoxCallbackFunc = procedure(const Flags: LongInt; const After: Boolean;
const Param: LongInt);
{ Useful constant }
const
EnableColor: array[Boolean] of TColor = (clBtnFace, clWindow);
function AppCreateForm(const AClass: TCustomFormClass): TCustomForm;
procedure UpdateHorizontalExtent(const ListBox: TCustomListBox);
function MinimizePathName(const Filename: String; const Font: TFont;
MaxLen: Integer): String;
function AppMessageBox(const Text, Caption: PChar; Flags: Longint): Integer;
function MsgBoxP(const Text, Caption: PChar; const Typ: TMsgBoxType;
const Buttons: Cardinal): Integer;
function MsgBox(const Text, Caption: String; const Typ: TMsgBoxType;
const Buttons: Cardinal): Integer;
function MsgBoxFmt(const Text: String; const Args: array of const;
const Caption: String; const Typ: TMsgBoxType; const Buttons: Cardinal): Integer;
procedure ReactivateTopWindow;
procedure SetMessageBoxCaption(const Typ: TMsgBoxType; const NewCaption: PChar);
function GetMessageBoxCaption(const Caption: PChar; const Typ: TMsgBoxType): PChar;
procedure SetMessageBoxRightToLeft(const ARightToLeft: Boolean);
function GetMessageBoxRightToLeft: Boolean;
procedure SetMessageBoxCallbackFunc(const AFunc: TMsgBoxCallbackFunc; const AParam: LongInt);
procedure TriggerMessageBoxCallbackFunc(const Flags: LongInt; const After: Boolean);
function GetOwnerWndForMessageBox: HWND;
function IsWindowOnTaskbar(const Wnd: HWND): Boolean;
implementation
uses
Consts, PathFunc, Shared.CommonFunc;
var
MessageBoxCaptions: array[TMsgBoxType] of PChar;
MessageBoxRightToLeft: Boolean;
MessageBoxCallbackFunc: TMsgBoxCallbackFunc;
MessageBoxCallbackParam: LongInt;
MessageBoxCallbackActive: Boolean;
function AppCreateForm(const AClass: TCustomFormClass): TCustomForm;
{ Creates a form, making it the main form if there isn't one already.
Usage: AppCreateForm(TMyForm) as TMyForm
This is a wrapper around Application.CreateForm, but with these advantages:
- Safety: Returns a typed value instead of writing to an untyped parameter.
- Safety: When used in an assignment statement: MyForm := AppCreateForm(...)
the variable isn't modified until the form is fully constructed and the
function exits. Application.CreateForm writes to its parameter, making the
value public, before the form's constructor is executed, which could allow
code outside the form to access the form before it's fully constructed.
- When the result is casted with "as", it works with type inference.
- When used in the .dpr, the Delphi IDE will never touch it. }
begin
Application.CreateForm(AClass, Result);
end;
type
TListBoxAccess = class(TCustomListBox);
procedure UpdateHorizontalExtent(const ListBox: TCustomListBox);
var
I: Integer;
Extent, MaxExtent: Longint;
DC: HDC;
Size: TSize;
TextMetrics: TTextMetric;
begin
DC := GetDC(0);
try
SelectObject(DC, TListBoxAccess(ListBox).Font.Handle);
//Q66370 says tmAveCharWidth should be added to extent
GetTextMetrics(DC, TextMetrics);
MaxExtent := 0;
for I := 0 to ListBox.Items.Count-1 do begin
GetTextExtentPoint32(DC, PChar(ListBox.Items[I]), Length(ListBox.Items[I]), Size);
Extent := Size.cx + TextMetrics.tmAveCharWidth;
if Extent > MaxExtent then
MaxExtent := Extent;
end;
finally
ReleaseDC(0, DC);
end;
if MaxExtent > SendMessage(ListBox.Handle, LB_GETHORIZONTALEXTENT, 0, 0) then
SendMessage(ListBox.Handle, LB_SETHORIZONTALEXTENT, MaxExtent, 0);
end;
function MinimizePathName(const Filename: String; const Font: TFont;
MaxLen: Integer): String;
procedure CutFirstDirectory(var S: String);
var
P: Integer;
begin
if Copy(S, 1, 4) = '...\' then
Delete(S, 1, 4);
P := PathPos('\', S);
if P <> 0 then
begin
Delete(S, 1, P);
S := '...\' + S;
end
else
S := '';
end;
var
DC: HDC;
Drive, Dir, Name: String;
DriveLen: Integer;
begin
DC := GetDC(0);
try
SelectObject(DC, Font.Handle);
Result := FileName;
Dir := PathExtractPath(Result);
Name := PathExtractName(Result);
DriveLen := PathDrivePartLength(Dir);
{ Include any slash following drive part, or a leading slash if DriveLen=0 }
if (DriveLen < Length(Dir)) and PathCharIsSlash(Dir[DriveLen+1]) then
Inc(DriveLen);
Drive := Copy(Dir, 1, DriveLen);
Delete(Dir, 1, DriveLen);
while ((Dir <> '') or (Drive <> '')) and (GetTextWidth(DC, Result, False) > MaxLen) do
begin
if Dir <> '' then
CutFirstDirectory(Dir);
{ If there's no directory left, minimize the drive part.
'C:\...\filename' -> '...\filename' }
if (Dir = '') and (Drive <> '') then
begin
Drive := '';
Dir := '...\';
end;
Result := Drive + Dir + Name;
end;
finally
ReleaseDC(0, DC);
end;
end;
procedure SetMessageBoxCaption(const Typ: TMsgBoxType; const NewCaption: PChar);
begin
StrDispose(MessageBoxCaptions[Typ]);
MessageBoxCaptions[Typ] := nil;
if Assigned(NewCaption) then
MessageBoxCaptions[Typ] := StrNew(NewCaption);
end;
function GetMessageBoxCaption(const Caption: PChar; const Typ: TMsgBoxType): PChar;
const
DefaultCaptions: array[TMsgBoxType] of PChar =
('Information', 'Confirm', 'Error', 'Error');
begin
Result := Caption;
if (Result = nil) or (Result[0] = #0) then begin
Result := MessageBoxCaptions[Typ];
if Result = nil then
Result := DefaultCaptions[Typ];
end;
end;
procedure SetMessageBoxRightToLeft(const ARightToLeft: Boolean);
begin
MessageBoxRightToLeft := ARightToLeft;
end;
function GetMessageBoxRightToLeft: Boolean;
begin
Result := MessageBoxRightToLeft;
end;
procedure SetMessageBoxCallbackFunc(const AFunc: TMsgBoxCallbackFunc; const AParam: LongInt);
begin
MessageBoxCallbackFunc := AFunc;
MessageBoxCallbackParam := AParam;
end;
procedure TriggerMessageBoxCallbackFunc(const Flags: LongInt; const After: Boolean);
begin
if Assigned(MessageBoxCallbackFunc) and not MessageBoxCallbackActive then begin
MessageBoxCallbackActive := True;
try
MessageBoxCallbackFunc(Flags, After, MessageBoxCallbackParam);
finally
MessageBoxCallbackActive := False;
end;
end;
end;
function GetOwnerWndForMessageBox: HWND;
{ Returns window handle that Application.MessageBox, if called immediately
after this function, would use as the owner window for the message box.
Exception: If the window that would be returned is not shown on the taskbar,
or is a minimized Application.Handle window, then 0 is returned instead.
See comments in AppMessageBox. }
begin
{ This is what Application.MessageBox does (Delphi 11.3) }
Result := Application.ActiveFormHandle;
if Result = 0 then { shouldn't be possible, but they have this check }
Result := Application.Handle;
{ Now our overrides }
if (Result = Application.Handle) and IsIconic(Result) then
Exit(0);
if not IsWindowOnTaskbar(Result) then
Result := 0;
end;
function IsWindowOnTaskbar(const Wnd: HWND): Boolean;
begin
{ Find the "root owner" window, which is what appears in the taskbar.
We avoid GetAncestor(..., GA_ROOTOWNER) because it's broken in the same
way as GetParent(): it stops if it reaches a top-level window that doesn't
have the WS_POPUP style (i.e., a WS_OVERLAPPED window). }
var RootWnd := Wnd;
while True do begin
{ Visible WS_EX_APPWINDOW windows have their own taskbar button regardless
of their root owner's visibility }
if (GetWindowLong(RootWnd, GWL_EXSTYLE) and WS_EX_APPWINDOW <> 0) and
(GetWindowLong(RootWnd, GWL_STYLE) and WS_VISIBLE <> 0) then
Exit(True);
var ParentWnd := HWND(GetWindowLongPtr(RootWnd, GWLP_HWNDPARENT));
if ParentWnd = 0 then
Break;
RootWnd := ParentWnd;
end;
Result := (GetWindowLong(RootWnd, GWL_STYLE) and WS_VISIBLE <> 0) and
(GetWindowLong(RootWnd, GWL_EXSTYLE) and WS_EX_TOOLWINDOW = 0);
end;
function AppMessageBox(const Text, Caption: PChar; Flags: Longint): Integer;
var
ActiveWindow: HWND;
WindowList: Pointer;
begin
{ Always restore the app first if it's minimized. This makes sense from a
usability perspective (e.g., it may be unclear which app generated the
message box if it's shown by itself), but it's also a VCL bug mitigation
(seen on Delphi 11.3):
Without this, when Application.MainFormOnTaskBar=True, showing a window
like a message box causes a WM_ACTIVATEAPP message to be sent to
Application.Handle, and the VCL strangely responds by setting FAppIconic
to False -- even though the main form is still iconic (minimized). If we
later try to call Application.Restore, nothing happens because it sees
FAppIconic=False. }
Application.Restore;
{ Always try to bring the message box to the foreground. Task dialogs appear
to do that by default.
Due to Windows' protections against apps stealing the foreground, the
message box won't actually come to the foreground in most cases. Instead,
the taskbar button will flash. That's really all we need; the user just
needs to be made aware that a message box is awaiting their response.
(Note: Don't run under the debugger when testing because Windows allows
debugged processes to steal the foreground with no restrictions.) }
Flags := Flags or MB_SETFOREGROUND;
if MessageBoxRightToLeft then
Flags := Flags or (MB_RTLREADING or MB_RIGHT);
TriggerMessageBoxCallbackFunc(Flags, False);
try
{ Application.MessageBox uses Application.ActiveFormHandle for the message
box's owner window. If that window is Application.Handle AND it isn't
currently shown on the taskbar [1], the result will be a message box
with no taskbar button -- which can easily get lost behind other
windows. Avoid that by calling MessageBox directly with no owner window.
[1] That is the case when we're called while no forms are visible.
But it can also be the case when Application.MainFormOnTaskBar=True
and we're called while the application isn't in the foreground
(i.e., GetActiveWindow=0). That seems like erroneous behavior on the
VCL's part (it should return the same handle as when the app is in
the foreground), and it causes modal TForms to get the 'wrong' owner
as well. However, it can be worked around using a custom
Application.OnGetActiveFormHandle handler (see IDE.MainForm).
We also use the same MessageBox call when IsIconic(Application.Handle)
is True to work around a separate issue:
1. Start with Application.MainFormOnTaskBar=False
2. Minimize the app
3. While the app is still minimized, call Application.MessageBox
4. Click the app's taskbar button (don't touch the message box)
At this point, the form that was previously hidden when the app was
minimized is shown again. But it's not disabled! You can interact with
the form despite the message box not being dismissed (which can lead to
reentrancy issues and undefined behavior). And the form is allowed to
rise above the message box in z-order.
The reason the form isn't disabled is that the VCL's DisableTaskWindows
function, which is called by Application.MessageBox, ignores non-visible
windows. Which seems wrong.
When we call MessageBox here with no owner window, we pass the
MB_TASKMODAL flag, which goes further than DisableTaskWindows and
disables non-visible windows too. That prevents the user from
interacting with the form. However, the form can still rise above the
message box. But with separate taskbar buttons for the two windows,
it's easier to get the message box back on top.
(This problem doesn't occur when Application.MainFormOnTaskBar=True
because the main form retains its WS_VISIBLE style while minimized.)
UPDATE: Had to restrict the use of MB_TASKMODAL to only when
MainFormOnTaskBar=False is set to work around *another* VCL issue.
The above problem doesn't affect MainFormOnTaskBar=True so that should
be fine.
Details: When MainFormOnTaskBar=True and MessageBox is called with the
MB_TASKMODAL flag after the main form is created but before the main
form is shown, the message box appears on the screen but you can't
interact with it using the keyboard; keys like Enter and Escape have no
effect. The problem? The CM_ACTIVATE handler in TApplication.WndProc is
calling SetFocus with a NULL window handle. This erroneous SetFocus call
is only reached when the main form window is found to be disabled, which
only happens when MB_TASKMODAL is used. As noted above, non-visible
windows aren't disabled when only DisableTaskWindows is used.
}
if GetOwnerWndForMessageBox = 0 then begin
ActiveWindow := GetActiveWindow;
WindowList := DisableTaskWindows(0);
try
{ Note: DisableTaskWindows doesn't disable invisible windows.
MB_TASKMODAL will ensure that Application.Handle gets disabled too. }
if not Application.MainFormOnTaskBar then
Flags := Flags or MB_TASKMODAL;
Result := MessageBox(0, Text, Caption, UINT(Flags));
finally
EnableTaskWindows(WindowList);
SetActiveWindow(ActiveWindow);
end;
Exit;
end;
Result := Application.MessageBox(Text, Caption, Flags);
finally
TriggerMessageBoxCallbackFunc(Flags, True);
end;
end;
function MsgBoxP(const Text, Caption: PChar; const Typ: TMsgBoxType;
const Buttons: Cardinal): Integer;
const
IconFlags: array[TMsgBoxType] of Cardinal =
(MB_ICONINFORMATION, MB_ICONQUESTION, MB_ICONEXCLAMATION, MB_ICONSTOP);
begin
Result := AppMessageBox(Text, GetMessageBoxCaption(Caption, Typ), Buttons or IconFlags[Typ]);
end;
function MsgBox(const Text, Caption: String; const Typ: TMsgBoxType;
const Buttons: Cardinal): Integer;
begin
Result := MsgBoxP(PChar(Text), PChar(Caption), Typ, Buttons);
end;
function MsgBoxFmt(const Text: String; const Args: array of const;
const Caption: String; const Typ: TMsgBoxType; const Buttons: Cardinal): Integer;
begin
Result := MsgBox(Format(Text, Args), Caption, Typ, Buttons);
end;
function ReactivateTopWindowEnumProc(Wnd: HWND; LParam: LPARAM): BOOL; stdcall;
begin
{ Stop if we encounter the application window; don't consider it or any
windows below it }
if Wnd = Application.Handle then
Result := False
else
if IsWindowVisible(Wnd) and IsWindowEnabled(Wnd) and
(GetWindowLong(Wnd, GWL_EXSTYLE) and (WS_EX_TOPMOST or WS_EX_TOOLWINDOW) = 0) then begin
SetActiveWindow(Wnd);
Result := False;
end
else
Result := True;
end;
procedure ReactivateTopWindow;
{ If the application window is active, reactivates the top window owned by the
current thread. Tool windows and windows that are invisible, disabled, or
topmost are not considered. }
begin
if GetActiveWindow = Application.Handle then
EnumThreadWindows(GetCurrentThreadId, @ReactivateTopWindowEnumProc, 0);
end;
procedure FreeCaptions; far;
var
T: TMsgBoxType;
begin
for T := Low(T) to High(T) do begin
StrDispose(MessageBoxCaptions[T]);
MessageBoxCaptions[T] := nil;
end;
end;
{ TWindowDisabler }
const
WindowDisablerWndClassName = 'TWindowDisabler-Window';
var
WindowDisablerWndClassAtom: TAtom;
function WindowDisablerWndProc(Wnd: HWND; Msg: UINT; WParam: WPARAM;
LParam: LPARAM): LRESULT; stdcall;
begin
if Msg = WM_CLOSE then
{ If the fallback window becomes focused (e.g. by Alt+Tabbing onto it) and
Alt+F4 is pressed, we must not pass the message to DefWindowProc because
it would destroy the window }
Result := 0
else
Result := DefWindowProc(Wnd, Msg, WParam, LParam);
end;
constructor TWindowDisabler.Create;
const
WndClass: TWndClass = (
style: 0;
lpfnWndProc: @WindowDisablerWndProc;
cbClsExtra: 0;
cbWndExtra: 0;
hInstance: 0;
hIcon: 0;
hCursor: 0;
hbrBackground: COLOR_WINDOW + 1;
lpszMenuName: nil;
lpszClassName: WindowDisablerWndClassName);
begin
inherited Create;
FPreviousActiveWnd := GetActiveWindow;
FPreviousFocusWnd := GetFocus;
FWindowList := DisableTaskWindows(0);
{ Create the "fallback" window.
When a child process hides its last window, Windows will try to activate
the top-most enabled window on the desktop. If all of our windows were
disabled, it would end up bringing some other application to the
foreground. This gives Windows an enabled window to re-activate, which
is invisible to the user. }
if WindowDisablerWndClassAtom = 0 then
WindowDisablerWndClassAtom := Windows.RegisterClass(WndClass);
if WindowDisablerWndClassAtom <> 0 then begin
{ Create an invisible owner window for the fallback window so that it
doesn't display a taskbar button. (We can't just give it the
WS_EX_TOOLWINDOW style because Windows skips tool windows when searching
for a new window to activate.) }
FOwnerWnd := CreateWindowEx(0, WindowDisablerWndClassName, '',
WS_POPUP or WS_DISABLED, 0, 0, 0, 0, HWND_DESKTOP, 0, HInstance, nil);
if FOwnerWnd <> 0 then begin
FFallbackWnd := CreateWindowEx(0, WindowDisablerWndClassName,
PChar(Application.Title), WS_POPUP, 0, 0, 0, 0, FOwnerWnd, 0,
HInstance, nil);
if FFallbackWnd <> 0 then
ShowWindow(FFallbackWnd, SW_SHOWNA);
end;
end;
{ Take the focus away from whatever has it. While you can't click controls
inside a disabled window, keystrokes will still reach the focused control
(e.g. you can press Space to re-click a focused button). }
SetFocus(0);
end;
destructor TWindowDisabler.Destroy;
begin
EnableTaskWindows(FWindowList);
{ Re-activate the previous window. But don't do this if GetActiveWindow
returns zero, because that means another application is in the foreground
(possibly a child process spawned by us that is still running). }
if GetActiveWindow <> 0 then begin
if FPreviousActiveWnd <> 0 then
SetActiveWindow(FPreviousActiveWnd);
{ If the active window never changed, then the above SetActiveWindow call
won't have an effect. Explicitly restore the focus. }
if FPreviousFocusWnd <> 0 then
SetFocus(FPreviousFocusWnd);
end;
if FOwnerWnd <> 0 then
DestroyWindow(FOwnerWnd); { will destroy FFallbackWnd too }
inherited;
end;
initialization
finalization
FreeCaptions;
end.