作者: Jim Wang 公众号: 巴博萨船长

开发部领导新的要求,希望在产品安装包的向导页面做些美化。比如,显示公司的logo在向导页面的标题部分。

所谓标题即Label部分就是上图中红色方格标注的部分。 标注部分右侧是logo图标。

1. 初探,如何更换logo部分

Inno Setup开发文档里里有介绍如何更换Label中右侧的logo的方法。可以通过在**[Setup]**可以定义WizardSmallImageFile=”logo文件路径”来实现。但其要求的logo图片都是长宽相等的。长款不等的图片在显示会被拉伸,其效果惨不忍睹。该选项支持多重文件,即:你可以一次定义多个分辨率的logo文件,Inno Setup在安装的时候会根据系统当前的DPI设置,来自动选择合适分辨率的图片。多重定义的方法如下:

1
2
Example:
WizardSmallImageFile=mysmallimage.bmp,mysmallimage2.bmp

Logo文件分辨率要求如下:

1
2
3
4
5
6
7
100%	55x55
125% 64x68
150% 83x80
175% 92x97
200% 110x106
225% 119x123
250% 138x140

图片被拉伸通常发生在以下两种情况,一、显示器的设置发生了变化。二、Inno Setup向导页面被设定未可缩放。向导页面的缩放可以通过WizardSizePercenWizardResizableWizardSizePercent的合法值为从100值150。该参数的目的是允许你在不改变文字大小的前提下等比例缩放所以的安装和卸载向导页面。150意思是缩放为默认大小的150%,即增到50%。这里WizardResizable的合法值为yes和no,而当WizardStyle(其合法值为classic和modern)被设置为modern的时候,就默认为WizardResizable的值为yes,也同时默认为WizardSizePercen为120%。具体解释可以查看Inno Setup的官方文档。

2. 深入,如何得到当前的显示器的DPI

上文说到,图片的拉伸除了与向导页面有关,与系统的显示设置也相关,那如何得到当前显示的DPI呢?在Win7之前的系统中,要得到当前显示器的DPI,在Inno Setup的**[Code]**部分可以使用TGraphicsObject.PixelsPerInch 这个属性得到当前系统显示器设定的DPI。TGraphicsObject 这里有个小例子,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
procedure CheckDPI;
var
CurrentDPI, StandardDPI, MediumDPI, LargeDPI: Integer;
begin
{ Get the current DPI, 从向导页面的字体中得到当前的DPI }
CurrentDPI := WizardForm.Font.PixelsPerInch;

{ Store defaults determined from Windows DPI settings }
StandardDPI := 96; { 100% }
MediumDPI := 120; { 125% }
LargeDPI := 144; { 150% }

if (CurrentDPI >= StandardDPI) and (CurrentDPI < MediumDPI) then
begin
{ Execute some custom code for small to medium DPI }
end
else if (CurrentDPI >= MediumDPI) and (CurrentDPI < LargeDPI) then
begin
{ Execute some custom code for medium to large DPI }
end
else if (CurrentDPI >= LargeDPI) then
begin
{ Execute some custom code for large DPI or above }
end;
end;

从Inno Setup 的5.6的版本开始,在多重定义的时候,Inno Setup就会根据当前DPI自动选择图片大小。这个自动选择是如何实现的呢。 下面的代码就使用了PixelsPerInch这个属性,并尝试模拟了自动选择这部分功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
[Setup]
; Use 100% images by default
WizardImageFile=WizardImage 100.bmp
WizardSmallImageFile=WizardSmallImage 100.bmp

[Files]
; Embed all other sizes to the installer
Source: "WizardImage *.bmp"; Excludes: "* 100.bmp"; Flags: dontcopy
Source: "WizardSmallImage *.bmp"; Excludes: "* 100.bmp"; Flags: dontcopy

[Code]
function GetScalingFactor: Integer;
begin
if WizardForm.Font.PixelsPerInch >= 192 then Result := 200
else
if WizardForm.Font.PixelsPerInch >= 144 then Result := 150
else
if WizardForm.Font.PixelsPerInch >= 120 then Result := 125
else Result := 100;
end;

procedure LoadEmbededScaledBitmap(Image: TBitmapImage; NameBase: string);
var
Name: String;
FileName: String;
begin
Name := Format('%s %d.bmp', [NameBase, GetScalingFactor]);
ExtractTemporaryFile(Name);
FileName := ExpandConstant('{tmp}\' + Name);
Image.Bitmap.LoadFromFile(FileName);
DeleteFile(FileName);
end;

procedure InitializeWizard;
begin
{ If using larger scaling, load the correct size of images }
if GetScalingFactor > 100 then
begin
LoadEmbededScaledBitmap(WizardForm.WizardBitmapImage, 'WizardImage');
LoadEmbededScaledBitmap(WizardForm.WizardBitmapImage2, 'WizardImage');
LoadEmbededScaledBitmap(WizardForm.WizardSmallBitmapImage, 'WizardSmallImage');
end;
end;

那么有没有在不用重复定义,仅用一张图片然后根据当前的DPI来进行人为的缩放,也能达到良好的显示效果呢,当然有,方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var
CustomPage: TWizardPage;
BtnImage: TBitmapImage;

procedure InitializeWizard;
begin
CustomPage := CreateCustomPage(wpLicense, 'Heading', 'Sub heading.');
ExtractTemporaryFile('image.bmp');
BtnImage := TBitmapImage.Create(WizardForm); //在当前页面即wpLicense页面创建一个Image按钮。
with BtnImage do
begin
Parent := CustomPage.Surface;
Bitmap.LoadFromFile(ExpandConstant('{tmp}')+'\image.bmp');
AutoSize := True;
AutoSize := False; // 缩放之前 关系BtnImage的自由大小属性
Height := ScaleX(Height); // 然后根据当前BtnImage的大小进行缩放
Width := ScaleY(Width);
Stretch := True;
Left := ScaleX(90);
Top := WizardForm.SelectTasksPage.Top + WizardForm.SelectTasksPage.Height -
Height - ScaleY(8);
Cursor := crHand;
OnClick := @ImageOnClick;
end;
end;

3. 进阶,Win8之后新系统得到当前的显示器的DPI

由于Windows 8.1之后的系统中,用户可以调整每个监视器的DPI缩放比例,并且Font.PixelsPerInch始终为主监视器返回DPI值,因此第2节中的方法就不在适用新的系统了。如果在多显示多DPI的复杂环境中,第2节中的方法表现的也不尽如人意。某些时候,可以监听WM_DPICHANGED事件,然后手动并使窗口适应变化的DPI(但此更改可以只是将窗口移动到具有不同DPI设置的监视器)。而 要获取某个窗口的DPI,就需要考虑别的解决方案了。我在网上找到了相关的介绍,但是我当前的项目中不需要这些,也就没对代码进行测试。感兴趣的朋友可以自己尝试一下,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
[Code]
const
S_OK = $00000000;
LOGPIXELSY = 90;
MONITOR_DEFAULTTONULL = $00000000;
MONITOR_DEFAULTTOPRIMARY = $00000001;
MONITOR_DEFAULTTONEAREST = $00000002;

type
HDC = THandle;
HMONITOR = THandle;
MONITOR_DPI_TYPE = (
MDT_EFFECTIVE_DPI,
MDT_ANGULAR_DPI,
MDT_RAW_DPI
);
const
MDT_DEFAULT = MDT_EFFECTIVE_DPI;

function GetDC(hWnd: HWND): HDC;
external 'GetDC@user32.dll stdcall';
function ReleaseDC(hWnd: HWND; hDC: HDC): Integer;
external 'ReleaseDC@user32.dll stdcall';
function GetDeviceCaps(hdc: HDC; index: Integer): Integer;
external 'GetDeviceCaps@gdi32.dll stdcall';
function MonitorFromWindow(hwnd: HWND; dwFlags: DWORD): HMONITOR;
external 'MonitorFromWindow@user32.dll stdcall';
function GetDpiForMonitor(hmonitor: HMONITOR; dpiType: MONITOR_DPI_TYPE;
out dpiX: UINT; out dpiY: UINT): HRESULT;
external 'GetDpiForMonitor@shcore.dll stdcall delayload';

function IsWindows81OrLater: Boolean;
begin
Result := GetWindowsVersion >= $06030000;
end;

function GetPrimaryMonitorDPI: Integer;
var
DC: HDC;
begin
// no need for try..finally block here; just get the desktop DC handle,
// get the DPI and release the obtained handle
DC := GetDC(0);
Result := GetDeviceCaps(DC, LOGPIXELSY);
ReleaseDC(0, DC);
end;

function GetWindowMonitorDPI(Handle: HWND): Integer;
var
HorzDPI: UINT;
VertDPI: UINT;
Monitor: HMONITOR;
begin
// if we're not on at least Windows 8.1, then return the primary monitor
// DPI since earlier systems did not allow per monitor DPI settings
if not IsWindows81OrLater then
Result := GetPrimaryMonitorDPI
else
// otherwise determine which monitor the given window intersects and try
// to get DPI settings of the found monitor (if any; check MSDN docs for
// the explanation and other options how to find the nearest monitor)
begin
// try to get the monitor which the window intersects
Monitor := MonitorFromWindow(Handle, MONITOR_DEFAULTTONULL);
// if there's any, then...
if Monitor <> 0 then
begin
// try to get DPI for that monitor; if it succeeds, return the value,
// raise an exception otherwise (for details check the MSDN docs)
if GetDpiForMonitor(Monitor, MDT_DEFAULT, HorzDPI, VertDPI) = S_OK then
Result := VertDPI
else
RaiseException('GetDpiForMonitor function call failed!');
end
else
RaiseException('The given window does not intersect any monitor!');
end;
end;

这是Windows SDK中介绍的方法,整个设计相对来说比较复杂。简单来说,将上述代码加入你的项目中,当你需要显示器的DPI的时候就调用GetWindowMonitorDPI函数,然后根据DPI结合第2节中的方法来缩放或者选择你要显示的图片。

4. 回到项目,更换向导页面标题部分的背景

上述的内容,也是我再寻找解决新需求的过程中,搜集和整理的部分内容,记录下来也是为了以后需要的时候知道应该向那个方向努力。我告诉开发主管的是,我个人不建议将我们安装应用程序的向导页面设置为可缩放,因为这样需要做出的改变特别多,而且当前的大小也能满足使用需求。但美化工作不能少,第一步的任务核心就是页面简介背景部分,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
[Files]
Source: "Logo-header.bmp"; Flags: dontcopy

[Code]

procedure InitializeWizard();
var
BitmapImage: TBitmapImage;
begin
//
ExtractTemporaryFile('Logo-header.bmp');
BitmapImage := TBitmapImage.Create(WizardForm);
BitmapImage.Parent := WizardForm.MainPanel;
BitmapImage.Width := WizardForm.MainPanel.Width;
BitmapImage.Height := WizardForm.MainPanel.Height;
BitmapImage.Stretch := True;
BitmapImage.AutoSize := False;
BitmapImage.Bitmap.LoadFromFile(ExpandConstant('{tmp}\Logo-header.bmp'));

// 禁用 原始页面的小图标
WizardForm.WizardSmallBitmapImage.Visible := False;
// 启用 页面简介标题
WizardForm.PageDescriptionLabel.Visible := True;
// 启用 页面名称标题
WizardForm.PageNameLabel.Visible := True;
// 限定页面简介标题宽度
WizardForm.PageDescriptionLabel.Width :=
WizardForm.PageDescriptionLabel.Width - ScaleX(120);
// 限定页面名称标题宽度
WizardForm.PageNameLabel.Width :=
WizardForm.PageNameLabel.Width - ScaleX(120)
end;

上述代码的核心思想就是,准备一张图片,该图片并不会被拷贝在目标文件夹中,在页面初始的时候,设定页面标题简述部分的背景,由于原始页面小logo被禁用,就需要缩进标题名称和简介文本的宽度,防止其覆盖图片的部分内容。

image-20200220170839898

任务第二部分内容,就是更改欢迎页面的背景图片,即上图中红色部分框选的地方。首先在**[Setup]中定义属性为DisableWelcomePage=no,意指启用欢迎页面。 然后在[Setup]部分定义WizardBitmapImage属性即可。结束页面(下图)与开始页面相似,只需在[Setup]**部分定义WizardBitmapImage2 属性即可。

image-20200220170935862

需要说明的是,如果属性WizardBitmapImage2为被定义,而属性WizardBitmapImage被定义,则结束页面会选择WizardBitmapImage中定义的图片作为上图中红色部分圈选的背景图片。

5. 扩展,欢迎页面和结束页面整页照片

我们在第4节部分,了解了如何在使用Inno Setup的属性设定,更改欢迎和结束页面的左侧背景图片,那么有没有一种可能,即省去这两个页面的文字(通常需要考虑翻译的问题),直接使用一张图片作为整个页面的背景呢?有,本节我们就探索一下。中心思想为四部:

  • 在各自的父页面上拉伸“ WizardBitmapImage”(欢迎)和“ WizardBitmapImage2”(完成),因为这两个页面都有图片部分,就不需要再初始化TBitmapImage了。
  • 隐藏其它内容,例如 标题和段落.
  • 确保安装程序永远不需要重新启动计算机,否则会在结束图片上会显示上重新启动提示。
  • 请确保在**[Run]**部分中没有任何postinstall条目。否则结束页面上也会显示提示信息。

image-20200220171021313

实现这部分的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[Setup]
DisableWelcomePage=no
WizardImageFile=Demo.bmp // 如果WizardImageFile2 没定义则结束页面也使用该图片

[Code]

procedure InitializeWizard();
begin
// 欢迎页面
// 隐藏 部分内容
WizardForm.WelcomeLabel1.Visible := False;
WizardForm.WelcomeLabel2.Visible := False;
// 拉伸页面宽度
WizardForm.WizardBitmapImage.Width := WizardForm.WizardBitmapImage.Parent.Width;

// 结束页面
// 隐藏 部分内容
WizardForm.FinishedLabel.Visible := False;
WizardForm.FinishedHeadingLabel.Visible := False;
// 拉伸页面宽度
WizardForm.WizardBitmapImage2.Width := WizardForm.WizardBitmapImage2.Parent.Width;
end;

6. 扩展2,在向导页脚左侧显示版本号

需要的显示结果如下图所示:

image-20200221093833941

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[Code]

procedure InitializeWizard();
var
VersionLabel: TNewStaticText;
begin
// 定义一个静态文本实例
VersionLabel := TNewStaticText.Create(WizardForm);
// 从Setup部分的设定中获取版本号
VersionLabel.Caption := Format('Version: %s', ['{#SetupSetting("AppVersion")}']);
// 所有页面有效
VersionLabel.Parent := WizardForm;
// 左侧位置
VersionLabel.Left := ScaleX(16);
// 保证 文本与按钮的 在水平方向 中心对齐
VersionLabel.Top :=
WizardForm.BackButton.Top +
(WizardForm.BackButton.Height div 2) -
(VersionLabel.Height div 2)
end;

对齐的方式,以按钮的左上位置为基点,加上按钮高度与文本高度之差即可。


版权声明:
文章首发于 Jim Wang's blog , 转载文章请务必以超链接形式标明文章出处,作者信息及本版权声明。