前言

  数个月前我整理一套 windows 平台的效率工具,文章链接
  里面总结的软件操作,再经过了我几个月的运用,已经熟练地应用到我的日常工作当中了。
  只是软件的数量有点多,手动安装起来还是挺麻烦的。
  这个月初就开始筹备实现这些软件的自动化安装了,花了不少时间,也踩了一些坑。

  github 仓库地址

踩坑前奏

  最初的时候,我打算是用 bat 批处理来实现软件的自动化安装。
  因为经过我简单的网上搜索,我了解到,很多软件的安装包都有提供静默安装的选项,那么这些流程完全可以通过一个简单的 bat 批处理脚本实现自动化安装。
  但是,我发现有些软件并没有那么好弄,比如 PotPlayer 是需要解压再进行安装的,因此没有提供静默安装的功能。
  QtTabbar 1038 版本可以静默安装,但是升级包 1040 版本无法静默安装。

  考虑到上述的诸多情况,我必须要实现 自动化 操作点击才可以。
  于是乎,我打算利用 Python 来实现自动化流程。
  一开始没有想到 autohotkey 这个自动化编程工具,因为一直想着 python 的 subprocess 可以调用命令行,通过 tkinter 开发界面。
  考虑到python 自动化操作 windows ,可以用的库也有不少。
  比较知名的库是 pywinauto 和 pyautogui

  pyautogui 是跨平台的自动化包,但是大部分通过截图识别来定位鼠标位置实现自动化,因此运行效率比较低。
  具体可以参考 Stack Overflow 里面 pywinatuo 作者提供的回复 链接
  pywinatuo 的优点是调用了 windows 的 API 获取窗口的信息,在这些信息基础上实现窗体匹配。
  因此后面我就采用了 pywinatuo 来实现我的自动化安装流程。

pywinatuo 开发

  于是我开始踩坑 pywinauto 实现界面的自动化触发。
  开发的过程感受到诸多和 ahk 相同的地方。

  我首先从 QTTabBar 安装入手,QTTabBar 的启动比较坑,完成安装之后,还需要点到下拉菜单来启动。

alt

  通过敲击 alt 可以让 win10 显示出快捷键,通过快捷键的方式可以实现自动化的操作。

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

import time

from pywinauto.application import Application
from pywinauto import Desktop, keyboard

def main():

new_win = False
for win in Desktop(backend="uia").windows():
if win.class_name() == "CabinetWClass":
break
else:
new_win = True
app = Application(backend="uia").start('explorer.exe')
win = Desktop(backend="uia").window(class_name="CabinetWClass")

win.set_focus()

# NOTE 开启 QTTabBar
keyboard.send_keys("{VK_MENU}")
keyboard.send_keys("{V}")
time.sleep(0.5)
keyboard.send_keys("{Y}")
keyboard.send_keys("{DOWN}")
keyboard.send_keys("{DOWN}")
keyboard.send_keys("{DOWN}")
keyboard.send_keys("{ENTER}")

if new_win:
win.close()


if __name__ == "__main__":
main()

  通过看文档和网上查资料,总算是知道怎么在 pywinauto 里面获取到打开的 explorer 窗口。

  CabinetWClass 这个参数是通过 pywinauto 的 print_control_identifiers 获取 - 文档
  print_control_identifiers 会打印出当前窗口下所有的组件信息。
  从中我发现 explorer 这个大窗口类就是 CabinetWClass
  通过这个方法可以直接获取到 explorer 打开的窗口。

  然后通过 send_keys 发送键盘的命令实现自动化操作。


  好不容易实现 python 的自动化, Tkinter 界面也尝试弄了些 checkbox 来进行软件安装的选择。
  后续测试了一下 pyinstaller 打包程序,这一打包就直接原地炸了。
  pywinauto 打包出来的 exe 居然有 200+ M
  这个大小和 numpy 打包有得一拼了(:з」∠)

  所以我果断放弃了这个方案了,网上稍微又查了查,才发现原来还有 autohotkey 的方案。

autohotkey 开发界面

  以前用 tkinter 开发界面的时候,就有提过 tkinter 打包的界面稍微还是有点大,如果用 ahk 可以压缩得更小。Python - Tkinter Windows 磁盘映射工具
  关于 ahk 的认识,我基本上是从 capslock+ 魔改的时候认识的 github仓库
  所以我并没有真正开发过 ahk 界面。
  不过我这一次的界面开发需求其实很简单,只需要开发几个 checkbox 界面即可。

alt

  不过开发 ahk 界面果然还是非常不习惯。
  这个东西和 tkinter 一样也有同样的问题,没有 Qt 的 Layout 的概念。
  所以界面的长宽位置大小都是固定的,窗口也无法进行缩放。

  不过,考虑到 ahk 超级小的大小,还是太香了,委曲求全可还行。

注 : 也有可能我对 ahk 还了解不深(:з」∠)


  后面在 ahk 中文网 上了解到了 中文最强 IDE , 河许人出品。 链接
  这个工具可以实现类似 QtDesigner 的界面拖拽的效果。
  不过界面修改起来很麻烦,人工修改代码需要断开界面的链接 ε=(´ο`*)))
  用起来有诸多不爽的地方。

  界面开发其实倒还好说,官方文档也非常全面,和 mel 文档有得一拼,每个命令都有个小案例,学习起来非常便利。
  最大的难点就是如何获取 checkbox 的状态。
  在组件的长宽后面加上 v 开发的变量名,可以绑定一个状态变量进去。
  后续触发 Gui, Submit ,NoHide 之后就可以让这些变量获取到 checkbox 的状态。

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
Gui Add, GroupBox, x10 y6 w120 h205, 效率软件
Gui Add, CheckBox, x20 y23 w100 h25 +Checked vListary, Listary
Gui Add, CheckBox, y+5 w100 h25 +Checked vQTTabBar, QTTabBar
Gui Add, CheckBox, y+5 w100 h25 +Checked vDitto, Ditto
Gui Add, CheckBox, y+5 w100 h25 vWGesture, WGesture
Gui Add, CheckBox, y+5 w100 h25 vQuicker, Quicker
Gui Add, CheckBox, y+5 w100 h25 vTencentDesktop, 腾讯桌面管理
Gui Add, GroupBox, x135 y6 w120 h49, 代码编辑器
Gui Add, CheckBox, x143 y24 w102 h23 +Checked vVScode, VScode
Gui Add, GroupBox, x135 y66 w120 h80, 截取软件
Gui Add, CheckBox, x143 y86 w107 h23 +Checked vSnipaste, Snipaste
Gui Add, CheckBox, x143 y115 w107 h23 +Checked vScreenToGif, ScreenToGif
Gui Add, GroupBox, x135 y153 w120 h58, 播放器
Gui Add, CheckBox, x143 y176 w102 h23 +Checked vPotPlayer, Pot Player
Gui Add, Button, x8 y222 w248 h23, 自动安装

Gui +AlwaysOnTop
Gui Show , w260 h252 , 自动安装 - 界面
Return

GuiEscape:
GuiClose:
ExitApp


Button自动安装:
Gui, Submit ,NoHide
SetTitleMatchMode, 1
; 软件安装流程 ...
ExitApp

  +Checked 添加标记可以让 checkbox 保持勾选的状态。

autohotkey 自动化安装流程

  大部分的安装的可以静默安装来完成。
  需要特殊处理有的 QtTabbar 和 PotPlayer , 还有 msi 同时安装会导致冲突。

QtTabbar 自动化安装

  这个工具安装可谓是花费了我好多时间。
  因为插件安装完成之后并没有进行启动,而这个插件启动的方式又比较隐秘,参考上面的截图。
  启动需要点击下拉菜单实现。
  如果不将启动流程自动化,反而会比较让人困惑。


  后面又在网上找到了中文语言的配置文件,于是又想实现自动加载语言配置的功能。
  通过查 QtTabbar 的官方网站提供的文档
  我发现 QtTabbar 的架构还是非常灵活的。
  最重要的是还支持 Scripting ,可以通过 windows 的 Active Scripting 来调用 QtTabbar 的 API。 文档
  支持 windows 平台的 VBScript 和 JScript。
  官方提供的的案例都是 JScript 写的。
  我查了一下微软官方关于 JScript 的描述,直接惊呆了。
  JScript 是根据 ECAMscript 3.0 的标准做的脚本语言(其实就是 JavaScript , 只不过版本过于过时)
  感觉回到10多年前的 IE 时代,那个时候 JScript 可以通过调用 ActiveXObject 实现对 windows API 的调用。

  至于如何通过 JScript 来操作 QtTabbar ,官网提供了很详细的文档 API文档
  还包括一些简单的调用案例
  可以通过脚本来制作一些 QtTabbar 的命令按钮,实现文件操作的自动化。

  通过这个脚本操作可以实现很多细微操作,因为命令行调用 explorer 只能开启一个新的窗口,如果要新建一个 Tab 还是得用脚本实现。
  JScript 的执行可以调用 windows 自带的 cscript 来执行 百度百科

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
C:\Users\timmyliang>where cscript
C:\Windows\System32\cscript.exe

C:\Users\timmyliang>cscript
Microsoft (R) Windows Script Host Version 5.812
版权所有(C) Microsoft Corporation。保留所有权利。

用法:CScript scriptname.extension [option...] [arguments...]

选项:
//B 批模式:不显示脚本错误及提示信息
//D 启用 Active Debugging
//E:engine 使用执行脚本的引擎
//H:CScript 将默认的脚本宿主改为 CScript.exe
//H:WScript 将默认的脚本宿主改为 WScript.exe (默认)
//I 交互模式(默认,与 //B 相对)
//Job:xxxx 执行一个 WSF 工作
//Logo 显示徽标(默认)
//Nologo 不显示徽标:执行时不显示标志
//S 为该用户保存当前命令行选项
//T:nn 超时设定秒:允许脚本运行的最长时间
//X 在调试器中执行脚本
//UUnicode 表示来自控制台的重定向 I/O

  cscript 是 System32 目录下的一个 exe ,可以用来执行 JScript 和 VBScript
  我还是比较喜欢使用 JScript , js 毕竟也写过一段时间了。
  只可惜基于 ES3 标准,很多 ES6 的写法都不支持。

  原本打算通过脚本后台来实现 QtTabbar 的自启动的。
  为此还专门用 procmon 来监听查看 QtTabbar 的文件输入输出流。
  尽管发现了 QtTabbar 进行了很多 注册表的 写入写出 操作。
  但是单纯修改注册表并不能实现 QtTabbar 多标签的自启动。
  后来还是看了 QtTabbar 的官方文档,找到了 ShowToolbar 的命令可以实现这个效果。
  我在自己的电脑上测试得很完美。
  然而当我走安装流程的时候,发现脚本并不起作用。
  ShowToolbar 命令需要有 Tab 窗口开启才可以实现调用。
  但是如果还没有开启多标签页面的话,无法获取到打开的标签的实例,根本就无法调用这个方法。

  最后无奈,只能还是采用之前 python 自动化弄过的 快捷键操作实现 开启。
  自动化开启有可能因为用户操作等问题,导致开启不成功。这个自动操作的体验也不是太友好,但是没有更好的方法了。


  后来找到汉化语言 xml 配置,于是想将 汉化 添加上。
  只要通过快捷键开启 QTTabBar 之后,就可以愉快地使用 JScript API 了。
  于是我又查了文档,并没有直接加载 语言设置 的功能,但是可以通过加载 全局设置 功能间接加载 语言设置。
  全局设置可以通过 QTTabBar 的设置界面进行导出。

alt

  API 文档 通过 Import 命令可以加载 xml 配置。
  但是 xml 的配置只能是个绝对路径。
  因此我需要实现 JScript 的文件拷贝和读写功能,实现对路径的替换。

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
var shell = new ActiveXObject("wscript.shell");
var qs = new ActiveXObject("QTTabBarLib.Scripting");
var fso = new ActiveXObject("Scripting.FileSystemObject");

var DIR = fso.GetAbsolutePathName(".")

var APPDATA = shell.ExpandEnvironmentStrings("%APPDATA%")
var xml = fso.BuildPath(DIR, "*.xml")
fso.CopyFile(xml, APPDATA)

var config = fso.BuildPath(DIR, "config.xml")

// NOTE https://language-and-engineering.hatenablog.jp/entry/20090203/p1
var stream = new ActiveXObject("ADODB.Stream");
stream.CharSet = "utf-8";
stream.Type = 2;
stream.Open();
stream.LoadFromFile(config)
var config_xml = stream.ReadText(-1)
stream.Close()

var target_config = fso.BuildPath(APPDATA, "config.xml")

var stream = new ActiveXObject("ADODB.Stream");
stream.Type = 2;
stream.charset = "utf-8";
stream.Open();
stream.WriteText(config_xml.replace("{{QTTabBar}}",APPDATA),1);
stream.SaveToFile( target_config, 2 );
stream.Close();

qs.CloseAllWindows()
var SYSTEMDRIVE = shell.ExpandEnvironmentStrings("%SYSTEMDRIVE%")
wnd = qs.open(SYSTEMDRIVE)
// NOTE 导入中文设定
wnd.InvokeCommand("Import",target_config)

  通过 FileSystemObject 无法处理 utf-8 的文件。
  因此需要用 ADODB.Stream 来读写 utf-8 的 xml 配置
  这里通过 JScript 直接将文件拷贝到 APPDATA 的目录下,这个目录一般不需要管理员权限。

alt

  安装流程见上图,缺点就是敲快捷键的时候,如果遇到用户操作或者过于卡顿会导致启动失败。

PotPlayer 安装

  QTTabbar 是最为艰难的了,后续这些问题都不算太大的问题。
  PotPlayer 的安装包没有提供静默安装的方案,安装包需要经过解压才可以执行安装。
  这个基本上利用 AHK 的按键自动化点击即可。

  其中的难点就是要实现窗口等待。

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
; PotPlayer 安装操作
install_PotPlayer(){
install_path := A_ScriptDir "\software\PotPlayerSetup64-200512-ads.exe"

; 判断安装包是否存在
if !FileExist(install_path){
MsgBox, "PotPlayerSetup64-200512-ads.exe not exists"
return
}
ToolTip, 解压 PotPlayer 安装包

install_cmd := install_path
Run, %install_cmd%
WinWait, Installer Language, , 3
SendInput,{Enter}
; ControlClick, OK , Installer Language
Sleep, 2000
WinWait, PotPlayer-64 bit 安装
SendInput,{Enter}
SendInput,{Enter}
SendInput,{Enter}
Loop, 10
{
Sleep, 200
ControlClick, 安装 , PotPlayer-64 bit 安装
}

; ; 等待 关闭 按钮激活
; Sleep, 2000
while (WinExist(PotPlayer-64 bit 安装)) {
ToolTip, 等待 PotPlayer 安装
Sleep, 500
; ControlGet, button_enable, Visible ,, ClassNN Button2
ControlClick, 关闭 , PotPlayer-64 bit 安装
}

}

  点击安装按钮有时候会卡主,所以弄了个循环10次进行点击。
  后续进入安装流程,定期点击 关闭 按钮直到窗口关闭为止。

Snipaste 安装

  Snipaste 只提供了压缩包的便捷安装版本。
  因此自动安装需要用过 ahk 实现压缩包的解压
  还好 ahk官方论坛上有提供解决方案。
  也是通过调用 windows 的 API 实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Unz(sZip, sUnz)
{
fso := ComObjCreate("Scripting.FileSystemObject")
If Not fso.FolderExists(sUnz) ;http://www.autohotkey.com/forum/viewtopic.php?p=402574
fso.CreateFolder(sUnz)
psh := ComObjCreate("Shell.Application")
zippedItems := psh.Namespace( sZip ).items().count
psh.Namespace( sUnz ).CopyHere( psh.Namespace( sZip ).items, 4|16 )
Loop {
sleep 50
unzippedItems := psh.Namespace( sUnz ).items().count
ToolTip Unzipping in progress..
IfEqual,zippedItems,%unzippedItems%
break
}
ToolTip
}

  解压之后再执行 Snipaste.exe 启动

msi 处理

  msi安装如果同时启动 /passive 安装会跳过只保留一个。
  因此加入死循环来检测是否 msiexec.exe 在运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
install_Quicker(){
install_path := A_ScriptDir "\software\Quicker.x64.1.8.0.0.msi"

; 判断安装包是否存在
if !FileExist(install_path){
MsgBox, "Quicker.x64.1.8.0.0.msi not exists"
return
}

install_cmd := install_path " /passive"
Run, %install_cmd%
Loop {
ToolTip, 安装 Quicker
sleep 500
result := WinExist("ahk_exe msiexec.exe")
if (!result){
break
}
}
}

总结

  这次尝试了 ahk 的图形化编程,打包之后的程序大小只有 1M+ ,真的非常小巧。
  自动化安装流程有一定的安装失败几率,这个可能因电脑而异。