使用 Qt Installer Framework 制作安装包与程序自动升级
这将是你看到的全网最详细的使用 Qt Installer Framework 制作 Qt 应用程序安装包和程序自动升级中文教程,没有之一。
Qt Installer Framework 提供了一组工具和实用工具来创建安装程序,并在不重写源代码的情况下将它们部署到所有受支持的桌面 Qt 平台上,它可以运行在 Linux、Microsoft Windows、and OS X 平台上。
你只需要提供需要安装的源程序、程序名、安装包、授权文件等信息,Qt Installer Framework 就可以创建具有安装、更新、删除等一系列页面的安装程序。你可以通过向预定义的页面添加小部件或添加整个页面来为用户提供额外的选项,从而定制安装程序;你也可以通过创建脚本来向安装程序添加额外的操作。
软件下载
你可在 Qt 官方网站上下载到 Qt Installer Framework 的安装文件;首先选择版本目录,再根据目标平台下载相应的安装文件,点击安装即可。
安装完成后,查看安装目录:
- 在
bin
目录中包含程序集,这里主要用到的是用于生成安装程序binarycreator.exe
和生成升级仓库的repogen.exe
,可以将bin
目录添加到环境变量中; doc
目录包含 Qt Installer Framework 的使用帮助,可以将其添加到Qt Assisant
中;依次点击Qt Assisant
菜单上的“编辑” -> “首选项”,选择“文档选项卡”,点击“添加”按钮,选择doc
目录中的ifw.qch
文件即可;example
目录包含使用示例。
创建安装程序
这节教程将描述如何为一个小项目创建一个简单的安装程序,大致会执行如下过程:
- 创建一个 package directory,这里将包含所有配置文件和可安装软件包。
- 创建一个包含如何构建安装程序二进制文件和在线存储库的信息的 configuration file。
- 创建一个包含关于可安装组件的信息的 package information file。
- 创建安装程序的内容,并将它复制到 package directory。
- 使用 binarycreator 工具创建安装程序。
创建 Package 目录
在本例中,我们创建一个 Package
目录,在目录中包含 config
和 packages
两个子目录。
在 packages
可以放置不同的程序组件,目录名一般以 domain-like identifier 方式命名,例如:com.vendor.root
。在组件根目录下,创建名为 data
和 meta
的子目录。
packages
目录结构可以如下所示 :
-packages
- com.vendor.root
- data
- meta
- com.vendor.root.component1
- data
- meta
- com.vendor.root.component1.subcomponent1
- data
- meta
- com.vendor.root.component2
- data
- meta
Meta 目录
meta
目录包含指定部署和安装过程的文件,这些文件不会由安装程序提取安装,meta
目录中必须包含至少一个包信息文件(package.xml )和你在包信息文件中引用的所有文件,例如:脚本、用户界面文件和翻译等。
有关这个文件的详细信息将在下面的章节介绍。
Data 目录
将安装程序需要安装的文件(你的exe程序及其依赖等)放到 data
目录中。包含安装过程中安装程序提取的安装内容。
创建 Configuration File
在 config
目录中,创建具有以下内容的名为 config.xm
的文件:
<?xml version="1.0" encoding="UTF-8"?>
<Installer>
<Name>Your application</Name>
<Version>1.0.0</Version>
<Title>Your application Installer</Title>
<Publisher>Your vendor</Publisher>
<StartMenuDir>Super App</StartMenuDir>
<TargetDir>@HomeDir@/InstallationDirectory</TargetDir>
</Installer>
该配置文件指定了在安装页面上显示的以下信息:
<Version>
元素指定了应用程序版本号 。<Publisher>
元素指定了软件的发布者(该信息可以在 Windows 控制面板中显示)。<StartMenuDir>
元素在 Windows 开始菜单中为产品指定了默认程序组的名称。<TargetDir>
元素指定应用程序的默认安装目录。有关更多信息,请参见 “Qt Installer Framework 帮助” 的 “Component Scripting” 章节中的 “Predefined Variables” 小节。
有关配置文件格式和可用元素的更多信息,请参 “Qt Installer Framework 帮助” 的 “Configuration File” 章节。
创建 Package Information File
package.xml
文件是组件的主要信息文件,我们在 com.vendor.root
的 meta
目录中创建一个具有以下内容并名为 package.xml
的文件:
<?xml version="1.0" encoding="UTF-8"?>
<Package>
<DisplayName>The root component</DisplayName>
<Description>Install this example.</Description>
<Version>0.1.0-1</Version>
<ReleaseDate>2010-09-21</ReleaseDate>
<Licenses>
<License name="Beer Public License Agreement" file="license.txt" />
</Licenses>
<Default>script</Default>
<Script>installscript.qs</Script>
<UserInterfaces>
<UserInterface>page.ui</UserInterface>
</UserInterfaces>
</Package>
包信息文件元素的描述信息可以在 “Qt Installer Framework 帮助” 的 “Package Directory” 章节查看。
创建 Installer Content
上面已经介绍过了,将安装程序需要安装的文件(你的exe程序及其依赖等)放到 data
目录中。在本例中,我们将程序可执行文件放到 packages/com.vendor.root/data
目录中。
创建程序安装包
一切准备就绪后,就可以创建程序安装包了。在命令行工具中切换到 Package
目录,执行下列命令即可:
binarycreator.exe -c config/config.xml -p packages YourInstaller.exe
定制安装程序
在上一章讲述了如何创建一个安装程序,在这一章将讲述如何使用脚本定制一个安装程序,例如:
- 在脚本中添加可以被安装程序执行的 operations;
- 添加新页面;
- 通过将定制的用户界面元素插入到单个小部件中来修改现有的页面。
- 添加语言变体
你可以使用 component scripts 和 control script 来定制安装程序,组件脚本(component scripts)在组件的 package.xml
文件的 Script
元素中指定。关于组件脚本的详细描述可以在 “Qt Installer Framework 帮助” 的 “Component Scripting” 章节查看。
控制脚本(control script )与整个安装程序相关联,你可以在 control.xml
文件的 ControlScript
元素中指定它。 控制脚本可以是安装程序资源的一部分,也可以在命令行上传递。它们可以用来修改在加载组件之前呈现给用户的安装程序页面。 另外,您可以使用它们来修改卸载程序中的页面。关于控制脚本的详细描述可以在 “Qt Installer Framework 帮助” 的 “Controller Scripting” 章节查看。
有关可以在组件和控制脚本中使用的全局 JavaScript 对象的更多信息,请参阅 “Qt Installer Framework 帮助” 的 “Scripting API” 章节。
添加 Operations
您可以使用组件脚本在安装过程中执行 Qt Installer Framework 操作,可以用来移动、复制或修补文件等。另外,您可以通过派生 KDUpdater::UpdateOperation
来实现在安装程序中注册定制安装操作的方法 。
关于可用操作的摘要,请参阅 “Qt Installer Framework 帮助” 的 “Operations” 章节。
添加页面
组件可以包含一个或多个用户界面文件,这些文件由组件或控制脚本放置到安装程序中。 安装程序会自动加载 package.xml
文件中的 userinterface
元素中列出的所有用户界面文件。
使用组件脚本来添加页面
向安装程序添加一个新页面,请使用 installer::addWizardPage()
方法并指定新页面的位置。 例如,下面的代码“准备安装”页面之前添加了一个 MyPage
实例:
installer.addWizardPage( component, "MyPage", QInstaller.ReadyForInstallation );
您可以使用组件脚本的 component::userInterface
方法来访问加载的小部件,如下代码片段所示:
component.userInterface( "MyPage" ).checkbox.checked = true;
使用控制脚本添加页面
可以使用 installer::addWizardPage()
方法和 UI 文件中设置的对象名称注册一个自定义页面(例如 "MyPage"
),然后调用 Dynamic${ObjectName}Callback()
方法(例如 DynamicMyPageCallback()
):
function Controller()
{
installer.addWizardPage(component, "MyPage", QInstaller.TargetDirectory)
}
Controller.prototype.DynamicMyPageCallback()
{
var page = gui.pageWidgetByObjectName("DynamicMyPage");
page.myButton.click,
page.myWidget.subWidget.setText("hello")
}
您可以通过使用在 UI 文件中设置的对象名称来访问部件。例如,myButton
和 myWidget
是上面代码中的小部件对象名称。
添加小部件
你可以使用组件或控制脚本来将定制的用户界面元素插入安装程序中,作为单个小部件(比如复选框等)。 要插入单个小部件,可以使用 installer::addWizardPageItem()
方法。例如,下面的代码片段将 MyWidget
的一个实例从脚本中添加到组件选择页面:
installer.addWizardPageItem( component, "MyWidget", QInstaller.ComponentSelection );
与安装程序交互功能
您可以使用控制脚本在测试中自动执行安装程序功能。下面的代码片段说明了如何在目标目录选择页面上自动单击 “Next” 按钮:
Controller.prototype.TargetDirectoryPageCallback = function()
{
gui.clickButton(buttons.NextButton);
}
翻译页面
安装程序使用 Qt 翻译系统来将用户可读( user-readable)的输出转换为其他语言。 为了向终端用户提供组件脚本和用户界面中包含的字符串的本地化版本,需要先创建安装系统随组件一起加载的 QTranslator 文件。 安装程序会加载与当前系统语言环境相匹配的翻译文件。 例如,如果系统语言环境是德语,则加载 de.qm
文件。此外,还有一个本地化 license_de.txt
会显示加载,而不是默认的 license.txt
文件。
需要将翻译添加 package.xml
文件中以为组件激活翻译文件:
<Translations>
<Translation>de.qm</Translation>
</Translations>
在脚本中使用 qsTr()
函数来表示文字文本。另外,你可以在脚本中添加 Component.prototype.retranslateUi
方法,当安装程序的语言发生变化时,它会被调用,并加载翻译文件。
在翻译用户界面时,使用 qsTr
或 UI 文件的类名时,用于翻译的上下文是脚本文件的 basename。 例如,如果脚本文件名为 installscript.qs
,上下文将是 installscript
。
注意:翻译系统也可以用来定制 UI。使用如 en.ts
文件用一个定制的英语版本替换安装程序中的任何文本。
覆盖安装
默认情况下,Qt Installer Framework 不支持离线升级,如果将一个新的版本安装到旧版本的目录,会提示下列错误:
The directory you selected already exists and contains an installation...
如果想要升级,只能先手动卸载旧版程序,再安装新版本的程序。
你也可以使用上面讲述的定义安装程序的方法重新定制安装程序,来强制覆盖安装新版本程序;或先自动卸载旧版本程序,再安装新版程序。
你需要下面三件事:
- 创建一个组件脚本
- “target directory” 页面自定义 UI
- 一个控制器脚本,它会自动地单击卸载程序。
下面将介绍详细的修改过程,首先需要修改 config.xml
文件:
<?xml version="1.0" encoding="UTF-8"?>
<Installer>
<Name>Atlas4500 Tuner</Name>
<Version>1.0.0</Version>
<Title>Atlas4500 Tuner Installer</Title>
<Publisher>EF Johnson Technologies</Publisher>
<StartMenuDir>EF Johnson</StartMenuDir>
<TargetDir>C:\Program Files (x86)\EF Johnson\Atlas4500 Tuner</TargetDir>
</Installer>
接着是 <Component>/meta/package.xml
文件:
<?xml version="1.0" encoding="UTF-8"?>
<Package>
<DisplayName>Atlas4500Tuner</DisplayName>
<Description>Install the Atlas4500 Tuner</Description>
<Version>1.0.0</Version>
<ReleaseDate></ReleaseDate>
<Default>true</Default>
<Required>true</Required>
<Script>installscript.qs</Script>
<UserInterfaces>
<UserInterface>targetwidget.ui</UserInterface>
</UserInterfaces>
</Package>
自定义组件的脚本 <Component>/meta/installscript.qs
文件:
var targetDirectoryPage = null;
function Component()
{
installer.gainAdminRights();
component.loaded.connect(this, this.installerLoaded);
}
Component.prototype.createOperations = function()
{
// Add the desktop and start menu shortcuts.
component.createOperations();
component.addOperation("CreateShortcut",
"@TargetDir@/Atlas4500Tuner.exe",
"@DesktopDir@/Atlas4500 Tuner.lnk",
"workingDirectory=@TargetDir@");
component.addOperation("CreateShortcut",
"@TargetDir@/Atlas4500Tuner.exe",
"@StartMenuDir@/Atlas4500 Tuner.lnk",
"workingDirectory=@TargetDir@");
}
Component.prototype.installerLoaded = function()
{
installer.setDefaultPageVisible(QInstaller.TargetDirectory, false);
installer.addWizardPage(component, "TargetWidget", QInstaller.TargetDirectory);
targetDirectoryPage = gui.pageWidgetByObjectName("DynamicTargetWidget");
targetDirectoryPage.windowTitle = "Choose Installation Directory";
targetDirectoryPage.description.setText("Please select where the Atlas4500 Tuner will be installed:");
targetDirectoryPage.targetDirectory.textChanged.connect(this, this.targetDirectoryChanged);
targetDirectoryPage.targetDirectory.setText(installer.value("TargetDir"));
targetDirectoryPage.targetChooser.released.connect(this, this.targetChooserClicked);
gui.pageById(QInstaller.ComponentSelection).entered.connect(this, this.componentSelectionPageEntered);
}
Component.prototype.targetChooserClicked = function()
{
var dir = QFileDialog.getExistingDirectory("", targetDirectoryPage.targetDirectory.text);
targetDirectoryPage.targetDirectory.setText(dir);
}
Component.prototype.targetDirectoryChanged = function()
{
var dir = targetDirectoryPage.targetDirectory.text;
if (installer.fileExists(dir) && installer.fileExists(dir + "/maintenancetool.exe")) {
targetDirectoryPage.warning.setText("<p style=\"color: red\">Existing installation detected and will be overwritten.</p>");
}
else if (installer.fileExists(dir)) {
targetDirectoryPage.warning.setText("<p style=\"color: red\">Installing in existing directory. It will be wiped on uninstallation.</p>");
}
else {
targetDirectoryPage.warning.setText("");
}
installer.setValue("TargetDir", dir);
}
Component.prototype.componentSelectionPageEntered = function()
{
var dir = installer.value("TargetDir");
if (installer.fileExists(dir) && installer.fileExists(dir + "/maintenancetool.exe")) {
installer.execute(dir + "/maintenancetool.exe", "--script=" + dir + "/scripts/auto_uninstall.qs");
}
}
定义目标目录页面 <Component>/meta/targetwidget.ui
文件:
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>TargetWidget</class>
<widget class="QWidget" name="TargetWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>491</width>
<height>190</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>491</width>
<height>190</height>
</size>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="description">
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLineEdit" name="targetDirectory">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="targetChooser">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>...</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="topMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="warning">
<property name="enabled">
<bool>true</bool>
</property>
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>122</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>
<Component>/data/scripts/auto_uninstall.qs
文件:
// Controller script to pass to the uninstaller to get it to run automatically.
// It's passed to the maintenance tool during installation if there is already an
// installation present with: <target dir>/maintenancetool.exe --script=<target dir>/scripts/auto_uninstall.qs.
// This is required so that the user doesn't have to see/deal with the uninstaller in the middle of
// an installation.
function Controller()
{
gui.clickButton(buttons.NextButton);
gui.clickButton(buttons.NextButton);
installer.uninstallationFinished.connect(this, this.uninstallationFinished);
}
Controller.prototype.uninstallationFinished = function()
{
gui.clickButton(buttons.NextButton);
}
Controller.prototype.FinishedPageCallback = function()
{
gui.clickButton(buttons.FinishButton);
}
这里的思想是检测当前目录是否有安装,如果有的话,在该目录中运行维护工具(执行卸载方法),并使用一个控制器脚本模拟点击 下一步
按钮。
注意,我把控制器脚本放在一个脚本目录中,该目录是实际组件数据的一部分。
你可以自己复制这些文件,然后调整相应的字符串以使其正常工作。
创建在线安装程序
创建在线安装应用程序,只需创建一个存储库并将其上传到 web 服务器,然后在 config.xml
文件中指定存储库的位置即可。
创建存储库
使用 repogen
工具创建一个包或多个包的在线存储库 :
repogen.exe -p <package_directory> <repository_directory>
例如,要创建一个只包含 org.qt-project.sdk.qt 和 org.qt-project.sdk.qtcreator 包的存储库,只需输入以下命令:
repogen.exe -p packages -i org.qt-project.sdk.qt,org.qt-project.sdk.qtcreator repository
当存储库被创建时,将其上传到 web 服务器即可。
注意:你必须在安装程序配置文件中指定存储库的位置。
配置存储库
安装程序配置文件(config.xml)中的 <RemoteRepositories>
元素可以包含多个存储库的列表,每一个储存库都可以有以下参数:
<Url>
,组件的连接地址<Enabled>
,0 表示禁用这个存储库<Username>
,用户名<Password>
,密码<DisplayName>
,显示名
URL 需要指定 Updates.xml 文件的位置,例如:
<RemoteRepositories>
<Repository>
<Url>http://www.example.com/packages</Url>
<Enabled>1</Enabled>
<Username>user</Username>
<Password>password</Password>
<DisplayName>Example repository</DisplayName>
</Repository>
</RemoteRepositories>
程序自动更新
如果要实现 Qt 程序自动更新的功能,需要依赖一个在线仓库。在程序启动时或点击手动更新按钮时,运行管理工具(检测更新模式),如果检测到新版本,则更新程序,示例如下:
QProcess process;
process.start("maintenancetool --checkupdates");
// Wait until the update tool is finished
process.waitForFinished();
if(process.error() != QProcess::UnknownError)
{
qDebug() << "Error checking for updates";
return false;
}
// Read the output
QByteArray data = process.readAllStandardOutput();
// No output means no updates available
// Note that the exit code will also be 1, but we don't use that
// Also note that we should parse the output instead of just checking if it is empty if we want specific update info
if(data.isEmpty())
{
qDebug() << "No updates available";
return false;
}
// Call the maintenance tool binary
// Note: we start it detached because this application need to close for the update
QStringList args("--updater");
bool success = QProcess::startDetached("maintenancetool", args);
// Close the application
qApp->closeAllWindows();
当然,你也可以使用开源项目 Skycoder42/QtAutoUpdater 来实现应用程序的自动和手动更新。
参考文章:
Qt Installer Framework Manual
Workaround for Qt Installer Framework not overwriting existing installation
Qt Installer Framework: Auto Update
您好,想請問一下我可以在打包時將auto_uninstall.qs放進打包檔嗎?
因之前已經有安裝目錄,但後來才增加auto_uninstall.qs,使用者使用舊版的狀況下是沒有auto_uninstall.qs檔案的,所以在執行installer.execute時會因找不到/scripts/auto_uninstall.qs檔案而移除失敗。
想請問是否有方法能將auto_uninstall.qs檔案直接放在執行檔能找到的地方,讓舊版使用者能正常移除安裝程式??
我測試將installer.execute(dir + "/maintenancetool.exe", "--script=" + dir + "/scripts/auto_uninstall.qs");的寫法改成installer.execute(dir + "/maintenancetool.exe", "--script=auto_uninstall.qs"); 這個方法好像是無效的
感謝您
可以“放在執行檔能找到的地方”,但是需要指定脚本的路径。你仔细对比一下你installer.execute(dir + "/maintenancetool.exe", "--script=auto_uninstall.qs"); 这句话只指定了一个相对的路径,需要指定绝对路径的。