123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523 |
- 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.
|