Shared.CommonFunc.Vcl.pas 22 KB

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