Inno-Setup-issrc/Projects/Src/Setup.SetupForm.pas
Martijn Laan 3723ae64a5
Cleanup.
2025-01-22 16:37:15 +01:00

543 lines
18 KiB
ObjectPascal

unit Setup.SetupForm;
{
Inno Setup
Copyright (C) 1997-2025 Jordan Russell
Portions by Martijn Laan
For conditions of distribution and use, see LICENSE.TXT.
TSetupForm
}
interface
uses
Windows, SysUtils, Messages, Classes, Graphics, Controls, Forms, Dialogs,
UIStateForm;
type
TSetupForm = class(TUIStateForm)
private
FBaseUnitX, FBaseUnitY: Integer;
FRightToLeft: Boolean;
FFlipControlsOnShow: Boolean;
FSizeAndCenterOnShow: Boolean;
FControlsFlipped: Boolean;
FKeepSizeY: Boolean;
procedure WMQueryEndSession(var Message: TWMQueryEndSession); message WM_QUERYENDSESSION;
protected
procedure Center;
procedure CenterInsideControl(const Ctl: TWinControl;
const InsideClientArea: Boolean);
procedure CenterInsideRect(const InsideRect: TRect);
procedure CreateParams(var Params: TCreateParams); override;
procedure CreateWnd; override;
procedure FlipControlsIfNeeded;
procedure SizeAndCenterIfNeeded(const ACenterInsideControl: Boolean;
const CenterInsideControlCtl: TWinControl;
const CenterInsideControlInsideClientArea: Boolean);
procedure VisibleChanging; override;
procedure WndProc(var Message: TMessage); override;
public
constructor Create(AOwner: TComponent); override;
constructor CreateNew(AOwner: TComponent; Dummy: Integer = 0); override;
function CalculateButtonWidth(const ButtonCaptions: array of String): Integer;
procedure InitializeFont;
function ScalePixelsX(const N: Integer): Integer;
function ScalePixelsY(const N: Integer): Integer;
function ShouldSizeX: Boolean;
function ShouldSizeY: Boolean;
function ShowModal: Integer; override;
procedure FlipSizeAndCenterIfNeeded(const ACenterInsideControl: Boolean = False;
const CenterInsideControlCtl: TWinControl = nil;
const CenterInsideControlInsideClientArea: Boolean = False); virtual;
property BaseUnitX: Integer read FBaseUnitX;
property BaseUnitY: Integer read FBaseUnitY;
published
property ControlsFlipped: Boolean read FControlsFlipped;
property FlipControlsOnShow: Boolean read FFlipControlsOnShow write FFlipControlsOnShow;
property KeepSizeY: Boolean read FKeepSizeY write FKeepSizeY;
property RightToLeft: Boolean read FRightToLeft;
property SizeAndCenterOnShow: Boolean read FSizeAndCenterOnShow write FSizeAndCenterOnShow;
end;
procedure CalculateBaseUnitsFromFont(const Font: TFont; var X, Y: Integer);
function GetRectOfPrimaryMonitor(const WorkArea: Boolean): TRect;
function SetFontNameSize(const AFont: TFont; const AName: String;
const ASize: Integer; const AFallbackName: String;
const AFallbackSize: Integer): Boolean;
const
OrigBaseUnitX = 6;
OrigBaseUnitY = 13;
implementation
uses
Generics.Collections, UITypes,
Shared.CommonFunc, Shared.CommonFunc.Vcl, Setup.MainFunc, BidiUtils;
var
WM_QueryCancelAutoPlay: UINT;
function GetRectOfPrimaryMonitor(const WorkArea: Boolean): TRect;
begin
if not WorkArea or
not SystemParametersInfo(SPI_GETWORKAREA, 0, @Result, 0) then
Result := Rect(0, 0, GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN));
end;
function GetRectOfMonitorContainingRect(const R: TRect): TRect;
{ Returns bounding rectangle of monitor containing or nearest to R }
type
HMONITOR = type THandle;
TMonitorInfo = record
cbSize: DWORD;
rcMonitor: TRect;
rcWork: TRect;
dwFlags: DWORD;
end;
const
MONITOR_DEFAULTTONEAREST = $00000002;
var
Module: HMODULE;
MonitorFromRect: function(const lprc: TRect; dwFlags: DWORD): HMONITOR; stdcall;
GetMonitorInfo: function(hMonitor: HMONITOR; var lpmi: TMonitorInfo): BOOL; stdcall;
M: HMONITOR;
Info: TMonitorInfo;
begin
Module := GetModuleHandle(user32);
MonitorFromRect := GetProcAddress(Module, 'MonitorFromRect');
GetMonitorInfo := GetProcAddress(Module, 'GetMonitorInfoA');
if Assigned(MonitorFromRect) and Assigned(GetMonitorInfo) then begin
M := MonitorFromRect(R, MONITOR_DEFAULTTONEAREST);
Info.cbSize := SizeOf(Info);
if GetMonitorInfo(M, Info) then begin
Result := Info.rcWork;
Exit;
end;
end;
Result := GetRectOfPrimaryMonitor(True);
end;
function SetFontNameSize(const AFont: TFont; const AName: String;
const ASize: Integer; const AFallbackName: String;
const AFallbackSize: Integer): Boolean;
{ Returns True if AName <> '' and it used AName as the font name,
False otherwise. }
function SizeToHeight(const S: Integer): Integer;
begin
Result := MulDiv(-S, Screen.PixelsPerInch, 72);
end;
begin
Result := False;
if AName <> '' then begin
if FontExists(AName) then begin
AFont.Name := AName;
AFont.Height := SizeToHeight(ASize);
Result := True;
Exit;
end;
{ Note: AFallbackName is not used if the user specified an empty string for
AName because in that case they want the default font used always }
if (AFallbackName <> '') and FontExists(AFallbackName) then begin
AFont.Name := AFallbackName;
AFont.Height := SizeToHeight(AFallbackSize);
Exit;
end;
end;
AFont.Name := GetPreferredUIFont;
AFont.Height := SizeToHeight(AFallbackSize);
end;
procedure CalculateBaseUnitsFromFont(const Font: TFont; var X, Y: Integer);
var
DC: HDC;
Size: TSize;
TM: TTextMetric;
begin
DC := GetDC(0);
try
SelectObject(DC, Font.Handle);
{ Based on code from Q145994: }
GetTextExtentPoint(DC,
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz', 52, Size);
X := (Size.cx div 26 + 1) div 2;
GetTextMetrics(DC, TM);
Y := TM.tmHeight;
finally
ReleaseDC(0, DC);
end;
end;
procedure NewChangeScale(const Ctl: TControl; const XM, XD, YM, YD: Integer);
var
X, Y, W, H: Integer;
begin
X := MulDiv(Ctl.Left, XM, XD);
Y := MulDiv(Ctl.Top, YM, YD);
if not(csFixedWidth in Ctl.ControlStyle) then
W := MulDiv(Ctl.Width, XM, XD)
else
W := Ctl.Width;
if not(csFixedHeight in Ctl.ControlStyle) then
H := MulDiv(Ctl.Height, YM, YD)
else
H := Ctl.Height;
Ctl.SetBounds(X, Y, W, H);
end;
procedure NewScaleControls(const Ctl: TWinControl; const XM, XD, YM, YD: Integer);
{ This is like TControl.ScaleControls, except it allows the width and height
to be scaled independently }
var
I: Integer;
C: TControl;
begin
for I := 0 to Ctl.ControlCount-1 do begin
C := Ctl.Controls[I];
if C is TWinControl then begin
TWinControl(C).DisableAlign;
try
NewScaleControls(TWinControl(C), XM, XD, YM, YD);
NewChangeScale(C, XM, XD, YM, YD);
finally
TWinControl(C).EnableAlign;
end;
end
else
NewChangeScale(C, XM, XD, YM, YD);
end;
end;
function GetParentSetupForm(AControl: TControl): TSetupForm;
begin
{ Note: Unlike GetParentForm, this checks all levels, not just the top }
repeat
if AControl is TSetupForm then begin
Result := TSetupForm(AControl);
Exit;
end;
AControl := AControl.Parent;
until AControl = nil;
Result := nil;
end;
function IsParentSetupFormFlipped(AControl: TControl): Boolean;
var
ParentForm: TSetupForm;
begin
ParentForm := GetParentSetupForm(AControl);
if Assigned(ParentForm) then
Result := ParentForm.ControlsFlipped
else
Result := False;
end;
function IsParentSetupFormRightToLeft(AControl: TControl): Boolean;
var
ParentForm: TSetupForm;
begin
ParentForm := GetParentSetupForm(AControl);
if Assigned(ParentForm) then
Result := ParentForm.RightToLeft
else
Result := False;
end;
type
TControlAnchorsList = TDictionary<TControl, TAnchors>;
TControlAccess = class(TControl);
procedure StripAndStoreCustomAnchors(const Ctl: TControl; const AnchorsList: TControlAnchorsList);
var
I: Integer;
begin
if Ctl.Anchors <> [akLeft, akTop] then begin
AnchorsList.Add(Ctl, Ctl.Anchors);
{ Before we can set Anchors to [akLeft, akTop] (which has a special
'no anchors' meaning to VCL), we first need to update the Explicit*
properties so the control doesn't get moved back to an old position. }
TControlAccess(Ctl).UpdateExplicitBounds;
Ctl.Anchors := [akLeft, akTop];
end;
if Ctl is TWinControl then
for I := 0 to TWinControl(Ctl).ControlCount-1 do
StripAndStoreCustomAnchors(TWinControl(Ctl).Controls[I], AnchorsList);
end;
procedure RestoreAnchors(const Ctl: TControl; const AnchorsList: TControlAnchorsList);
begin
{ The order in which we restore the anchors shouldn't matter, so just
enumerate the list. }
for var Item in AnchorsList do
Item.Key.Anchors := Item.Value;
end;
{ TSetupForm }
constructor TSetupForm.Create(AOwner: TComponent);
begin
{ Must initialize FRightToLeft here in addition to CreateNew because
CreateNew isn't virtual on Delphi 2 and 3 }
FRightToLeft := LangOptions.RightToLeft;
FFlipControlsOnShow := FRightToLeft;
FSizeAndCenterOnShow := True;
inherited;
{ In Delphi 2005 and later, Position defaults to poDefaultPosOnly, but we
don't want the form to be changing positions whenever its handle is
recreated, so change it to the D7 and earlier default of poDesigned. }
if Position = poDefaultPosOnly then
Position := poDesigned;
end;
constructor TSetupForm.CreateNew(AOwner: TComponent; Dummy: Integer = 0);
begin
{ Note: On Delphi 2 and 3, CreateNew isn't virtual, so this is only reached
when TSetupForm.CreateNew is called explicitly }
FRightToLeft := LangOptions.RightToLeft;
FFlipControlsOnShow := FRightToLeft;
FSizeAndCenterOnShow := True;
inherited;
end;
function TSetupForm.CalculateButtonWidth(const ButtonCaptions: array of String): Integer;
var
DC: HDC;
I, W: Integer;
begin
Result := ScalePixelsX(75);
{ Increase the button size if there are unusually long button captions }
DC := GetDC(0);
try
SelectObject(DC, Font.Handle);
for I := Low(ButtonCaptions) to High(ButtonCaptions) do begin
W := GetTextWidth(DC, ButtonCaptions[I], True) + ScalePixelsX(20);
if Result < W then
Result := W;
end;
finally
ReleaseDC(0, DC);
end;
end;
procedure TSetupForm.CenterInsideControl(const Ctl: TWinControl;
const InsideClientArea: Boolean);
var
R: TRect;
begin
const CtlForm = GetParentForm(Ctl);
if (CtlForm = nil) or not IsWindowVisible(CtlForm.Handle) or
IsIconic(CtlForm.Handle) then begin
Center;
Exit;
end;
if not InsideClientArea then begin
if GetWindowRect(Ctl.Handle, R) then
CenterInsideRect(R);
end
else begin
R := Ctl.ClientRect;
MapWindowPoints(Ctl.Handle, 0, R, 2);
CenterInsideRect(R);
end;
end;
procedure TSetupForm.CenterInsideRect(const InsideRect: TRect);
var
R, MR: TRect;
begin
R := Bounds(InsideRect.Left + ((InsideRect.Right - InsideRect.Left) - Width) div 2,
InsideRect.Top + ((InsideRect.Bottom - InsideRect.Top) - Height) div 2,
Width, Height);
{ Clip to nearest monitor }
MR := GetRectOfMonitorContainingRect(R);
if R.Right > MR.Right then
OffsetRect(R, MR.Right - R.Right, 0);
if R.Bottom > MR.Bottom then
OffsetRect(R, 0, MR.Bottom - R.Bottom);
if R.Left < MR.Left then
OffsetRect(R, MR.Left - R.Left, 0);
if R.Top < MR.Top then
OffsetRect(R, 0, MR.Top - R.Top);
BoundsRect := R;
end;
procedure TSetupForm.Center;
begin
CenterInsideRect(GetRectOfPrimaryMonitor(True));
end;
procedure TSetupForm.CreateParams(var Params: TCreateParams);
begin
inherited;
{ With Application.MainFormOnTaskBar=True, by default, a form won't get a
taskbar button if the main form hasn't been created yet (due to the owner
being an invisible Application.Handle), or if the main form exists but
isn't visible (e.g., because it's a silent install). Force it to have a
taskbar button in those cases by specifying no owner for the window.
(Another method is to set WS_EX_APPWINDOW and leave WndParent set to
Application.Handle, but it doesn't quite work correctly: if the form
displays a message box, and you activate another app's window, clicking on
the form's taskbar button activates the message box again, but the taskbar
button doesn't change to a "selected" state.) }
if (Params.WndParent <> 0) and
(Application.MainFormOnTaskBar or (Params.WndParent <> Application.Handle)) and
not IsWindowOnTaskbar(Params.WndParent) then
Params.WndParent := 0;
if FRightToLeft then
Params.ExStyle := Params.ExStyle or (WS_EX_RTLREADING or WS_EX_LEFTSCROLLBAR or WS_EX_RIGHT);
end;
procedure TSetupForm.CreateWnd;
begin
inherited;
if WM_QueryCancelAutoPlay <> 0 then
AddToWindowMessageFilterEx(Handle, WM_QueryCancelAutoPlay);
end;
procedure TSetupForm.FlipControlsIfNeeded;
begin
if FFlipControlsOnShow then begin
FFlipControlsOnShow := False;
FControlsFlipped := not FControlsFlipped;
FlipControls(Self);
end;
end;
procedure TSetupForm.SizeAndCenterIfNeeded(const ACenterInsideControl: Boolean; const CenterInsideControlCtl: TWinControl; const CenterInsideControlInsideClientArea: Boolean);
begin
if FSizeAndCenterOnShow then begin
FSizeAndCenterOnShow := False;
{ Apply custom initial size from script - depends on Anchors being set on all the controls }
if ShouldSizeX then
ClientWidth := MulDiv(ClientWidth, SetupHeader.WizardSizePercentX, 100);
if ShouldSizeY then
ClientHeight := MulDiv(ClientHeight, SetupHeader.WizardSizePercentY, 100);
{ Center }
if ACenterInsideControl then
CenterInsideControl(CenterInsideControlCtl, CenterInsideControlInsideClientArea)
else
Center;
end;
end;
function TSetupForm.ShouldSizeX: Boolean;
begin
Result := SetupHeader.WizardSizePercentX > 100;
end;
function TSetupForm.ShouldSizeY: Boolean;
begin
Result := not FKeepSizeY and (SetupHeader.WizardSizePercentY > 100);
end;
procedure TSetupForm.FlipSizeAndCenterIfNeeded(const ACenterInsideControl: Boolean;
const CenterInsideControlCtl: TWinControl; const CenterInsideControlInsideClientArea: Boolean);
begin
{ Flipping must be done first because when flipping after sizing the flipping might get old info for anchors that didn't do their work yet. }
FlipControlsIfNeeded;
SizeAndCenterIfNeeded(ACenterInsideControl, CenterInsideControlCtl, CenterInsideControlInsideClientArea);
end;
procedure TSetupForm.InitializeFont;
var
ControlAnchorsList: TControlAnchorsList;
W, H: Integer;
R: TRect;
begin
{ Note: Must keep the following lines in synch with Setup.ScriptFunc.pas's
InitializeScaleBaseUnits }
SetFontNameSize(Font, LangOptions.DialogFontName, LangOptions.DialogFontSize,
'', 8);
CalculateBaseUnitsFromFont(Font, FBaseUnitX, FBaseUnitY);
if (FBaseUnitX <> OrigBaseUnitX) or (FBaseUnitY <> OrigBaseUnitY) then begin
ControlAnchorsList := TControlAnchorsList.Create;
try
{ Custom anchors interfere with our scaling code, so strip them and restore
afterwards. }
StripAndStoreCustomAnchors(Self, ControlAnchorsList);
{ Loosely based on scaling code from TForm.ReadState: }
NewScaleControls(Self, BaseUnitX, OrigBaseUnitX, BaseUnitY, OrigBaseUnitY);
R := ClientRect;
W := MulDiv(R.Right, FBaseUnitX, OrigBaseUnitX);
H := MulDiv(R.Bottom, FBaseUnitY, OrigBaseUnitY);
SetBounds(Left, Top, W + (Width - R.Right), H + (Height - R.Bottom));
finally
RestoreAnchors(Self, ControlAnchorsList);
end;
end;
end;
function TSetupForm.ScalePixelsX(const N: Integer): Integer;
begin
Result := MulDiv(N, BaseUnitX, OrigBaseUnitX);
end;
function TSetupForm.ScalePixelsY(const N: Integer): Integer;
begin
Result := MulDiv(N, BaseUnitY, OrigBaseUnitY);
end;
function TSetupForm.ShowModal: Integer;
begin
{ Work around VCL issue (Delphi 11.3): ShowModal calls DisableTaskWindows
without ensuring the form's handle has been created first. If the handle
is created after DisableTaskWindows, PopupMode=pmAuto breaks;
TCustomForm.CreateParams finds that the active window is disabled, and
doesn't use it as the owner. It then falls back to pmNone behavior, which
is to use the main form or application window as the owner. }
HandleNeeded;
Result := inherited;
end;
procedure TSetupForm.VisibleChanging;
begin
inherited;
{ Note: Unlike DoShow, any exceptions raised in VisibleChanging will be
propagated out, which is what we want }
if not Visible then
FlipSizeAndCenterIfNeeded;
end;
procedure TSetupForm.WMQueryEndSession(var Message: TWMQueryEndSession);
begin
{ TDummyClass.AntiShutdownHook in Setup.dpr already denies shutdown attempts
but we also need to catch WM_QUERYENDSESSION here to suppress the VCL's
default handling which calls CloseQuery. We do not want to let TMainForm &
TNewDiskForm display any 'Exit Setup?' message boxes since we're already
denying shutdown attempts, and also we can't allow them to potentially be
displayed on top of another dialog box that's already displayed. }
{ Return zero, except if RestartInitiatedByThisProcess is set (which means
we called RestartComputer previously) }
if RestartInitiatedByThisProcess then
Message.Result := 1;
end;
procedure TSetupForm.WndProc(var Message: TMessage);
begin
{ When we receive a 'QueryCancelAutoPlay' message as a result of a new CD
being inserted, return 1 to prevent it from being 'autoplayed'.
Note: According to the docs, this message is only sent on Shell version
4.70 and later. }
if (WM_QueryCancelAutoPlay <> 0) and (Message.Msg = WM_QueryCancelAutoPlay) then
Message.Result := 1
else
inherited;
end;
initialization
BidiUtils.IsParentFlippedFunc := IsParentSetupFormFlipped;
BidiUtils.IsParentRightToLeftFunc := IsParentSetupFormRightToLeft;
WM_QueryCancelAutoPlay := RegisterWindowMessage('QueryCancelAutoPlay');
end.