
在默认情况下,Qt IFW 不支持离线升级。如果将一个程序的新版本安装到其旧版本所在目录,会提示以下错误:

这样一来,要升级程序就只能先手动卸载旧版本,然后才能安装新版本。显然这是一件很麻烦的事情,为了简化这个过程,可以使用覆盖安装。
1
自动卸载
覆盖安装实现起来并不困难,它只不过是将上述操作“自动化”了而已。即在安装新版本之前,先利用脚本对旧版本进行卸载,而这一步无需用户干预。
至于真正的卸载,我们可以使用 maintenancetool,它是 Qt 中的维护工具,用于添加/更新/删除组件。有关该工具支持的选项,可以通过 -h 来查看:

对于我们来说,比较有用的是 --script 选项,可以利用它来控制指定的脚本(例如:uninstallscript.qs)以完成卸载。
关于卸载脚本的编写,相对而言也比较容易(参考:https://doc.qt.io/qtinstallerframework/noninteractive.html),我们可以通过模拟用户单击与 UI 交互,来实现最终的卸载功能:
// 卸载脚本:如果程序已安装,则会调用 maintenance 工具,自动进行卸载。
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); }
现在,用之前的安装程序测试一下,执行 maintenancetool.exe --script=uninstallscript.qs 命令:

O(∩_∩)O哈哈~,卸载成功。
2
覆盖安装
有了这些基本知识,现在对上一节《Qt IFW 创建安装程序》中的内容进行完善,加上覆盖安装功能,最终的效果如下:

自定义 UI
既然是覆盖安装,必然少不了对安装位置的检测,一旦发现程序已安装,往往需要加一些友好性的提示信息(例如:上图中显示的“检测到程序已安装,继续将会被覆盖。”)。
要完成这一步,则需要为安装程序添加自定义 UI。首先,要在 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>description</string> </property> </widget> </item> <item> <layout class="QHBoxLayout" name="horizontalLayout"> <property name="spacing"> <number>6</number> </property> <item> <widget class="QLineEdit" name="targetDirectory"> <property name="readOnly"> <bool>true</bool> </property> </widget> </item> <item> <widget class="QPushButton" name="targetChooser"> <property name="text"> <string>...</string> </property> </widget> </item> </layout> </item> <item> <widget class="QLabel" name="warning"> <property name="enabled"> <bool>true</bool> </property> <property name="text"> <string>warning</string> </property> </widget> </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>
然后,还需要在 package.xml 文件中用 <UserInterfaces> 元素标记它:
<?xml version="1.0" encoding="UTF-8"?> <Package> <DisplayName>MyApp</DisplayName> <Description>The first ifw installer.</Description> <Version>1.0.0-1</Version> <ReleaseDate>2019-10-12</ReleaseDate> <Default>true</Default> <Script>installscript.qs</Script> <UserInterfaces> <UserInterface>targetwidget.ui</UserInterface> </UserInterfaces> </Package>
指定卸载脚本
上面已经介绍了卸载脚本 uninstallscript.qs 的编写,现在需要做的是将该脚本放入 data 目录中 (例如:data/script/uninstallscript.qs),最终由 Qt IFW 打包进安装程序:

这样的话,卸载脚本就会被安装至最终目录。当需要进行覆盖安装时,maintenancetool 工具就可以很容易的找到它了。
修改安装脚本
经过上面几步,我们已经添加了自定义 UI 以及卸载要用到的脚本,现在是时候将交互部分添加到安装脚本 installscript.qs 中了:
var targetDirectoryPage = null;
// 构造函数 function Component() { installer.gainAdminRights(); component.loaded.connect(this, this.installerLoaded); }
// 实用函数,类似于 QString QDir::toNativeSeparators() var Dir = new function () { this.toNativeSparator = function (path) { if (installer.value("os") == "win") return path.replace(/\//g, '\\'); return path; } };
// 添加桌面和开始菜单快捷方式 Component.prototype.createOperations = function() { component.createOperations(); component.addOperation("CreateShortcut", "@TargetDir@/bin/MyApp.exe", "@DesktopDir@/MyApp.lnk", "workingDirectory=@TargetDir@");
component.addOperation("CreateShortcut", "@TargetDir@/bin/MyApp.exe", "@StartMenuDir@/MyApp.lnk", "workingDirectory=@TargetDir@"); }
// 加载组件后立即调用 Component.prototype.installerLoaded = function() { installer.setDefaultPageVisible(QInstaller.TargetDirectory, false); installer.addWizardPage(component, "TargetWidget", QInstaller.TargetDirectory);
targetDirectoryPage = gui.pageWidgetByObjectName("DynamicTargetWidget"); targetDirectoryPage.windowTitle = "选择安装目录"; targetDirectoryPage.description.setText("请选择程序的安装位置:"); targetDirectoryPage.targetDirectory.textChanged.connect(this, this.targetDirectoryChanged); targetDirectoryPage.targetDirectory.setText(Dir.toNativeSparator(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); if (dir != "") { targetDirectoryPage.targetDirectory.setText(Dir.toNativeSparator(dir)); } }
// 当安装位置发生改变时调用 Component.prototype.targetDirectoryChanged = function() { var dir = targetDirectoryPage.targetDirectory.text; if (installer.fileExists(dir) && installer.fileExists(dir + "/bin/MyApp.exe")) { targetDirectoryPage.warning.setText("<p style=\"color: red\">检测到程序已安装,继续将会被覆盖。</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 + "/script/uninstallscript.qs"); } }
基本的思路就是这样,如果有什么细节需要修改,直接完善以上脚本即可。
·END·
高效程序员 谈天 · 说地 · 侃代码 · 开车

长按识别二维码,解锁更多精彩内容 ---------------------------------------------------------------------------------------------------------------------- 我们尊重原创,也注重分享,文章来源于微信公众号:高效程序员,建议关注公众号查看原文。如若侵权请联系qter@qter.org。 ----------------------------------------------------------------------------------------------------------------------
|