找回密码
 立即注册
Qt开源社区 门户 查看内容

Qt IFW 覆盖安装

2019-10-27 07:15| 发布者: admin| 查看: 4272| 评论: 0

摘要: 在默认情况下,Qt IFW 不支持离线升级。如果将一个程序的新版本安装到其旧版本所在目录,会提示以下错误:这样一来,要升级程序就只能先手动卸载旧版本,然后才能安装新版本。显然这是一件很麻烦的事情,为了简化这 ...


在默认情况下,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。
----------------------------------------------------------------------------------------------------------------------

鲜花

握手

雷人

路过

鸡蛋

公告
可以关注我们的微信公众号yafeilinux_friends获取最新动态,或者加入QQ会员群进行交流:190741849、186601429(已满) 我知道了