Shared.CommonFunc.Vcl.pas 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523
  1. unit Shared.CommonFunc.Vcl;
  2. {
  3. Inno Setup
  4. Copyright (C) 1997-2025 Jordan Russell
  5. Portions by Martijn Laan
  6. For conditions of distribution and use, see LICENSE.TXT.
  7. Common VCL functions
  8. }
  9. {$B-}
  10. interface
  11. uses
  12. Windows, Messages, SysUtils, Forms, Graphics, Controls, StdCtrls, Classes;
  13. type
  14. TWindowDisabler = class
  15. private
  16. FFallbackWnd, FOwnerWnd: HWND;
  17. FPreviousActiveWnd, FPreviousFocusWnd: HWND;
  18. FWindowList: Pointer;
  19. public
  20. constructor Create;
  21. destructor Destroy; override;
  22. end;
  23. { Note: This type is also present in Compiler.ScriptFunc.pas }
  24. TMsgBoxType = (mbInformation, mbConfirmation, mbError, mbCriticalError);
  25. TMsgBoxCallbackFunc = procedure(const Flags: LongInt; const After: Boolean;
  26. const Param: LongInt);
  27. { Useful constant }
  28. const
  29. EnableColor: array[Boolean] of TColor = (clBtnFace, clWindow);
  30. function AppCreateForm(const AClass: TCustomFormClass): TCustomForm;
  31. procedure UpdateHorizontalExtent(const ListBox: TCustomListBox);
  32. function MinimizePathName(const Filename: String; const Font: TFont;
  33. MaxLen: Integer): String;
  34. function AppMessageBox(const Text, Caption: PChar; Flags: Longint): Integer;
  35. function MsgBoxP(const Text, Caption: PChar; const Typ: TMsgBoxType;
  36. const Buttons: Cardinal): Integer;
  37. function MsgBox(const Text, Caption: String; const Typ: TMsgBoxType;
  38. const Buttons: Cardinal): Integer;
  39. function MsgBoxFmt(const Text: String; const Args: array of const;
  40. const Caption: String; const Typ: TMsgBoxType; const Buttons: Cardinal): Integer;
  41. procedure ReactivateTopWindow;
  42. procedure SetMessageBoxCaption(const Typ: TMsgBoxType; const NewCaption: PChar);
  43. function GetMessageBoxCaption(const Caption: PChar; const Typ: TMsgBoxType): PChar;
  44. procedure SetMessageBoxRightToLeft(const ARightToLeft: Boolean);
  45. function GetMessageBoxRightToLeft: Boolean;
  46. procedure SetMessageBoxCallbackFunc(const AFunc: TMsgBoxCallbackFunc; const AParam: LongInt);
  47. procedure TriggerMessageBoxCallbackFunc(const Flags: LongInt; const After: Boolean);
  48. function GetOwnerWndForMessageBox: HWND;
  49. function IsWindowOnTaskbar(const Wnd: HWND): Boolean;
  50. implementation
  51. uses
  52. Consts, PathFunc, Shared.CommonFunc;
  53. var
  54. MessageBoxCaptions: array[TMsgBoxType] of PChar;
  55. MessageBoxRightToLeft: Boolean;
  56. MessageBoxCallbackFunc: TMsgBoxCallbackFunc;
  57. MessageBoxCallbackParam: LongInt;
  58. MessageBoxCallbackActive: Boolean;
  59. function AppCreateForm(const AClass: TCustomFormClass): TCustomForm;
  60. { Creates a form, making it the main form if there isn't one already.
  61. Usage: AppCreateForm(TMyForm) as TMyForm
  62. This is a wrapper around Application.CreateForm, but with these advantages:
  63. - Safety: Returns a typed value instead of writing to an untyped parameter.
  64. - Safety: When used in an assignment statement: MyForm := AppCreateForm(...)
  65. the variable isn't modified until the form is fully constructed and the
  66. function exits. Application.CreateForm writes to its parameter, making the
  67. value public, before the form's constructor is executed, which could allow
  68. code outside the form to access the form before it's fully constructed.
  69. - When the result is casted with "as", it works with type inference.
  70. - When used in the .dpr, the Delphi IDE will never touch it. }
  71. begin
  72. Application.CreateForm(AClass, Result);
  73. end;
  74. type
  75. TListBoxAccess = class(TCustomListBox);
  76. procedure UpdateHorizontalExtent(const ListBox: TCustomListBox);
  77. var
  78. I: Integer;
  79. Extent, MaxExtent: Longint;
  80. DC: HDC;
  81. Size: TSize;
  82. TextMetrics: TTextMetric;
  83. begin
  84. DC := GetDC(0);
  85. try
  86. SelectObject(DC, TListBoxAccess(ListBox).Font.Handle);
  87. //Q66370 says tmAveCharWidth should be added to extent
  88. GetTextMetrics(DC, TextMetrics);
  89. MaxExtent := 0;
  90. for I := 0 to ListBox.Items.Count-1 do begin
  91. GetTextExtentPoint32(DC, PChar(ListBox.Items[I]), Length(ListBox.Items[I]), Size);
  92. Extent := Size.cx + TextMetrics.tmAveCharWidth;
  93. if Extent > MaxExtent then
  94. MaxExtent := Extent;
  95. end;
  96. finally
  97. ReleaseDC(0, DC);
  98. end;
  99. if MaxExtent > SendMessage(ListBox.Handle, LB_GETHORIZONTALEXTENT, 0, 0) then
  100. SendMessage(ListBox.Handle, LB_SETHORIZONTALEXTENT, MaxExtent, 0);
  101. end;
  102. function MinimizePathName(const Filename: String; const Font: TFont;
  103. MaxLen: Integer): String;
  104. procedure CutFirstDirectory(var S: String);
  105. var
  106. P: Integer;
  107. begin
  108. if Copy(S, 1, 4) = '...\' then
  109. Delete(S, 1, 4);
  110. P := PathPos('\', S);
  111. if P <> 0 then
  112. begin
  113. Delete(S, 1, P);
  114. S := '...\' + S;
  115. end
  116. else
  117. S := '';
  118. end;
  119. var
  120. DC: HDC;
  121. Drive, Dir, Name: String;
  122. DriveLen: Integer;
  123. begin
  124. DC := GetDC(0);
  125. try
  126. SelectObject(DC, Font.Handle);
  127. Result := FileName;
  128. Dir := PathExtractPath(Result);
  129. Name := PathExtractName(Result);
  130. DriveLen := PathDrivePartLength(Dir);
  131. { Include any slash following drive part, or a leading slash if DriveLen=0 }
  132. if (DriveLen < Length(Dir)) and PathCharIsSlash(Dir[DriveLen+1]) then
  133. Inc(DriveLen);
  134. Drive := Copy(Dir, 1, DriveLen);
  135. Delete(Dir, 1, DriveLen);
  136. while ((Dir <> '') or (Drive <> '')) and (GetTextWidth(DC, Result, False) > MaxLen) do
  137. begin
  138. if Dir <> '' then
  139. CutFirstDirectory(Dir);
  140. { If there's no directory left, minimize the drive part.
  141. 'C:\...\filename' -> '...\filename' }
  142. if (Dir = '') and (Drive <> '') then
  143. begin
  144. Drive := '';
  145. Dir := '...\';
  146. end;
  147. Result := Drive + Dir + Name;
  148. end;
  149. finally
  150. ReleaseDC(0, DC);
  151. end;
  152. end;
  153. procedure SetMessageBoxCaption(const Typ: TMsgBoxType; const NewCaption: PChar);
  154. begin
  155. StrDispose(MessageBoxCaptions[Typ]);
  156. MessageBoxCaptions[Typ] := nil;
  157. if Assigned(NewCaption) then
  158. MessageBoxCaptions[Typ] := StrNew(NewCaption);
  159. end;
  160. function GetMessageBoxCaption(const Caption: PChar; const Typ: TMsgBoxType): PChar;
  161. const
  162. DefaultCaptions: array[TMsgBoxType] of PChar =
  163. ('Information', 'Confirm', 'Error', 'Error');
  164. begin
  165. Result := Caption;
  166. if (Result = nil) or (Result[0] = #0) then begin
  167. Result := MessageBoxCaptions[Typ];
  168. if Result = nil then
  169. Result := DefaultCaptions[Typ];
  170. end;
  171. end;
  172. procedure SetMessageBoxRightToLeft(const ARightToLeft: Boolean);
  173. begin
  174. MessageBoxRightToLeft := ARightToLeft;
  175. end;
  176. function GetMessageBoxRightToLeft: Boolean;
  177. begin
  178. Result := MessageBoxRightToLeft;
  179. end;
  180. procedure SetMessageBoxCallbackFunc(const AFunc: TMsgBoxCallbackFunc; const AParam: LongInt);
  181. begin
  182. MessageBoxCallbackFunc := AFunc;
  183. MessageBoxCallbackParam := AParam;
  184. end;
  185. procedure TriggerMessageBoxCallbackFunc(const Flags: LongInt; const After: Boolean);
  186. begin
  187. if Assigned(MessageBoxCallbackFunc) and not MessageBoxCallbackActive then begin
  188. MessageBoxCallbackActive := True;
  189. try
  190. MessageBoxCallbackFunc(Flags, After, MessageBoxCallbackParam);
  191. finally
  192. MessageBoxCallbackActive := False;
  193. end;
  194. end;
  195. end;
  196. function GetOwnerWndForMessageBox: HWND;
  197. { Returns window handle that Application.MessageBox, if called immediately
  198. after this function, would use as the owner window for the message box.
  199. Exception: If the window that would be returned is not shown on the taskbar,
  200. or is a minimized Application.Handle window, then 0 is returned instead.
  201. See comments in AppMessageBox. }
  202. begin
  203. { This is what Application.MessageBox does (Delphi 11.3) }
  204. Result := Application.ActiveFormHandle;
  205. if Result = 0 then { shouldn't be possible, but they have this check }
  206. Result := Application.Handle;
  207. { Now our overrides }
  208. if (Result = Application.Handle) and IsIconic(Result) then
  209. Exit(0);
  210. if not IsWindowOnTaskbar(Result) then
  211. Result := 0;
  212. end;
  213. function IsWindowOnTaskbar(const Wnd: HWND): Boolean;
  214. begin
  215. { Find the "root owner" window, which is what appears in the taskbar.
  216. We avoid GetAncestor(..., GA_ROOTOWNER) because it's broken in the same
  217. way as GetParent(): it stops if it reaches a top-level window that doesn't
  218. have the WS_POPUP style (i.e., a WS_OVERLAPPED window). }
  219. var RootWnd := Wnd;
  220. while True do begin
  221. { Visible WS_EX_APPWINDOW windows have their own taskbar button regardless
  222. of their root owner's visibility }
  223. if (GetWindowLong(RootWnd, GWL_EXSTYLE) and WS_EX_APPWINDOW <> 0) and
  224. (GetWindowLong(RootWnd, GWL_STYLE) and WS_VISIBLE <> 0) then
  225. Exit(True);
  226. var ParentWnd := HWND(GetWindowLongPtr(RootWnd, GWLP_HWNDPARENT));
  227. if ParentWnd = 0 then
  228. Break;
  229. RootWnd := ParentWnd;
  230. end;
  231. Result := (GetWindowLong(RootWnd, GWL_STYLE) and WS_VISIBLE <> 0) and
  232. (GetWindowLong(RootWnd, GWL_EXSTYLE) and WS_EX_TOOLWINDOW = 0);
  233. end;
  234. function AppMessageBox(const Text, Caption: PChar; Flags: Longint): Integer;
  235. var
  236. ActiveWindow: HWND;
  237. WindowList: Pointer;
  238. begin
  239. { Always restore the app first if it's minimized. This makes sense from a
  240. usability perspective (e.g., it may be unclear which app generated the
  241. message box if it's shown by itself), but it's also a VCL bug mitigation
  242. (seen on Delphi 11.3):
  243. Without this, when Application.MainFormOnTaskBar=True, showing a window
  244. like a message box causes a WM_ACTIVATEAPP message to be sent to
  245. Application.Handle, and the VCL strangely responds by setting FAppIconic
  246. to False -- even though the main form is still iconic (minimized). If we
  247. later try to call Application.Restore, nothing happens because it sees
  248. FAppIconic=False. }
  249. Application.Restore;
  250. { Always try to bring the message box to the foreground. Task dialogs appear
  251. to do that by default.
  252. Due to Windows' protections against apps stealing the foreground, the
  253. message box won't actually come to the foreground in most cases. Instead,
  254. the taskbar button will flash. That's really all we need; the user just
  255. needs to be made aware that a message box is awaiting their response.
  256. (Note: Don't run under the debugger when testing because Windows allows
  257. debugged processes to steal the foreground with no restrictions.) }
  258. Flags := Flags or MB_SETFOREGROUND;
  259. if MessageBoxRightToLeft then
  260. Flags := Flags or (MB_RTLREADING or MB_RIGHT);
  261. TriggerMessageBoxCallbackFunc(Flags, False);
  262. try
  263. { Application.MessageBox uses Application.ActiveFormHandle for the message
  264. box's owner window. If that window is Application.Handle AND it isn't
  265. currently shown on the taskbar [1], the result will be a message box
  266. with no taskbar button -- which can easily get lost behind other
  267. windows. Avoid that by calling MessageBox directly with no owner window.
  268. [1] That is the case when we're called while no forms are visible.
  269. But it can also be the case when Application.MainFormOnTaskBar=True
  270. and we're called while the application isn't in the foreground
  271. (i.e., GetActiveWindow=0). That seems like erroneous behavior on the
  272. VCL's part (it should return the same handle as when the app is in
  273. the foreground), and it causes modal TForms to get the 'wrong' owner
  274. as well. However, it can be worked around using a custom
  275. Application.OnGetActiveFormHandle handler (see IDE.MainForm).
  276. We also use the same MessageBox call when IsIconic(Application.Handle)
  277. is True to work around a separate issue:
  278. 1. Start with Application.MainFormOnTaskBar=False
  279. 2. Minimize the app
  280. 3. While the app is still minimized, call Application.MessageBox
  281. 4. Click the app's taskbar button (don't touch the message box)
  282. At this point, the form that was previously hidden when the app was
  283. minimized is shown again. But it's not disabled! You can interact with
  284. the form despite the message box not being dismissed (which can lead to
  285. reentrancy issues and undefined behavior). And the form is allowed to
  286. rise above the message box in z-order.
  287. The reason the form isn't disabled is that the VCL's DisableTaskWindows
  288. function, which is called by Application.MessageBox, ignores non-visible
  289. windows. Which seems wrong.
  290. When we call MessageBox here with no owner window, we pass the
  291. MB_TASKMODAL flag, which goes further than DisableTaskWindows and
  292. disables non-visible windows too. That prevents the user from
  293. interacting with the form. However, the form can still rise above the
  294. message box. But with separate taskbar buttons for the two windows,
  295. it's easier to get the message box back on top.
  296. (This problem doesn't occur when Application.MainFormOnTaskBar=True
  297. because the main form retains its WS_VISIBLE style while minimized.)
  298. UPDATE: Had to restrict the use of MB_TASKMODAL to only when
  299. MainFormOnTaskBar=False is set to work around *another* VCL issue.
  300. The above problem doesn't affect MainFormOnTaskBar=True so that should
  301. be fine.
  302. Details: When MainFormOnTaskBar=True and MessageBox is called with the
  303. MB_TASKMODAL flag after the main form is created but before the main
  304. form is shown, the message box appears on the screen but you can't
  305. interact with it using the keyboard; keys like Enter and Escape have no
  306. effect. The problem? The CM_ACTIVATE handler in TApplication.WndProc is
  307. calling SetFocus with a NULL window handle. This erroneous SetFocus call
  308. is only reached when the main form window is found to be disabled, which
  309. only happens when MB_TASKMODAL is used. As noted above, non-visible
  310. windows aren't disabled when only DisableTaskWindows is used.
  311. }
  312. if GetOwnerWndForMessageBox = 0 then begin
  313. ActiveWindow := GetActiveWindow;
  314. WindowList := DisableTaskWindows(0);
  315. try
  316. { Note: DisableTaskWindows doesn't disable invisible windows.
  317. MB_TASKMODAL will ensure that Application.Handle gets disabled too. }
  318. if not Application.MainFormOnTaskBar then
  319. Flags := Flags or MB_TASKMODAL;
  320. Result := MessageBox(0, Text, Caption, UINT(Flags));
  321. finally
  322. EnableTaskWindows(WindowList);
  323. SetActiveWindow(ActiveWindow);
  324. end;
  325. Exit;
  326. end;
  327. Result := Application.MessageBox(Text, Caption, Flags);
  328. finally
  329. TriggerMessageBoxCallbackFunc(Flags, True);
  330. end;
  331. end;
  332. function MsgBoxP(const Text, Caption: PChar; const Typ: TMsgBoxType;
  333. const Buttons: Cardinal): Integer;
  334. const
  335. IconFlags: array[TMsgBoxType] of Cardinal =
  336. (MB_ICONINFORMATION, MB_ICONQUESTION, MB_ICONEXCLAMATION, MB_ICONSTOP);
  337. begin
  338. Result := AppMessageBox(Text, GetMessageBoxCaption(Caption, Typ), Buttons or IconFlags[Typ]);
  339. end;
  340. function MsgBox(const Text, Caption: String; const Typ: TMsgBoxType;
  341. const Buttons: Cardinal): Integer;
  342. begin
  343. Result := MsgBoxP(PChar(Text), PChar(Caption), Typ, Buttons);
  344. end;
  345. function MsgBoxFmt(const Text: String; const Args: array of const;
  346. const Caption: String; const Typ: TMsgBoxType; const Buttons: Cardinal): Integer;
  347. begin
  348. Result := MsgBox(Format(Text, Args), Caption, Typ, Buttons);
  349. end;
  350. function ReactivateTopWindowEnumProc(Wnd: HWND; LParam: LPARAM): BOOL; stdcall;
  351. begin
  352. { Stop if we encounter the application window; don't consider it or any
  353. windows below it }
  354. if Wnd = Application.Handle then
  355. Result := False
  356. else
  357. if IsWindowVisible(Wnd) and IsWindowEnabled(Wnd) and
  358. (GetWindowLong(Wnd, GWL_EXSTYLE) and (WS_EX_TOPMOST or WS_EX_TOOLWINDOW) = 0) then begin
  359. SetActiveWindow(Wnd);
  360. Result := False;
  361. end
  362. else
  363. Result := True;
  364. end;
  365. procedure ReactivateTopWindow;
  366. { If the application window is active, reactivates the top window owned by the
  367. current thread. Tool windows and windows that are invisible, disabled, or
  368. topmost are not considered. }
  369. begin
  370. if GetActiveWindow = Application.Handle then
  371. EnumThreadWindows(GetCurrentThreadId, @ReactivateTopWindowEnumProc, 0);
  372. end;
  373. procedure FreeCaptions; far;
  374. var
  375. T: TMsgBoxType;
  376. begin
  377. for T := Low(T) to High(T) do begin
  378. StrDispose(MessageBoxCaptions[T]);
  379. MessageBoxCaptions[T] := nil;
  380. end;
  381. end;
  382. { TWindowDisabler }
  383. const
  384. WindowDisablerWndClassName = 'TWindowDisabler-Window';
  385. var
  386. WindowDisablerWndClassAtom: TAtom;
  387. function WindowDisablerWndProc(Wnd: HWND; Msg: UINT; WParam: WPARAM;
  388. LParam: LPARAM): LRESULT; stdcall;
  389. begin
  390. if Msg = WM_CLOSE then
  391. { If the fallback window becomes focused (e.g. by Alt+Tabbing onto it) and
  392. Alt+F4 is pressed, we must not pass the message to DefWindowProc because
  393. it would destroy the window }
  394. Result := 0
  395. else
  396. Result := DefWindowProc(Wnd, Msg, WParam, LParam);
  397. end;
  398. constructor TWindowDisabler.Create;
  399. const
  400. WndClass: TWndClass = (
  401. style: 0;
  402. lpfnWndProc: @WindowDisablerWndProc;
  403. cbClsExtra: 0;
  404. cbWndExtra: 0;
  405. hInstance: 0;
  406. hIcon: 0;
  407. hCursor: 0;
  408. hbrBackground: COLOR_WINDOW + 1;
  409. lpszMenuName: nil;
  410. lpszClassName: WindowDisablerWndClassName);
  411. begin
  412. inherited Create;
  413. FPreviousActiveWnd := GetActiveWindow;
  414. FPreviousFocusWnd := GetFocus;
  415. FWindowList := DisableTaskWindows(0);
  416. { Create the "fallback" window.
  417. When a child process hides its last window, Windows will try to activate
  418. the top-most enabled window on the desktop. If all of our windows were
  419. disabled, it would end up bringing some other application to the
  420. foreground. This gives Windows an enabled window to re-activate, which
  421. is invisible to the user. }
  422. if WindowDisablerWndClassAtom = 0 then
  423. WindowDisablerWndClassAtom := Windows.RegisterClass(WndClass);
  424. if WindowDisablerWndClassAtom <> 0 then begin
  425. { Create an invisible owner window for the fallback window so that it
  426. doesn't display a taskbar button. (We can't just give it the
  427. WS_EX_TOOLWINDOW style because Windows skips tool windows when searching
  428. for a new window to activate.) }
  429. FOwnerWnd := CreateWindowEx(0, WindowDisablerWndClassName, '',
  430. WS_POPUP or WS_DISABLED, 0, 0, 0, 0, HWND_DESKTOP, 0, HInstance, nil);
  431. if FOwnerWnd <> 0 then begin
  432. FFallbackWnd := CreateWindowEx(0, WindowDisablerWndClassName,
  433. PChar(Application.Title), WS_POPUP, 0, 0, 0, 0, FOwnerWnd, 0,
  434. HInstance, nil);
  435. if FFallbackWnd <> 0 then
  436. ShowWindow(FFallbackWnd, SW_SHOWNA);
  437. end;
  438. end;
  439. { Take the focus away from whatever has it. While you can't click controls
  440. inside a disabled window, keystrokes will still reach the focused control
  441. (e.g. you can press Space to re-click a focused button). }
  442. SetFocus(0);
  443. end;
  444. destructor TWindowDisabler.Destroy;
  445. begin
  446. EnableTaskWindows(FWindowList);
  447. { Re-activate the previous window. But don't do this if GetActiveWindow
  448. returns zero, because that means another application is in the foreground
  449. (possibly a child process spawned by us that is still running). }
  450. if GetActiveWindow <> 0 then begin
  451. if FPreviousActiveWnd <> 0 then
  452. SetActiveWindow(FPreviousActiveWnd);
  453. { If the active window never changed, then the above SetActiveWindow call
  454. won't have an effect. Explicitly restore the focus. }
  455. if FPreviousFocusWnd <> 0 then
  456. SetFocus(FPreviousFocusWnd);
  457. end;
  458. if FOwnerWnd <> 0 then
  459. DestroyWindow(FOwnerWnd); { will destroy FFallbackWnd too }
  460. inherited;
  461. end;
  462. initialization
  463. finalization
  464. FreeCaptions;
  465. end.