技术路线:使用AHK脚本,通过图片搜索功能实现,使用了AHK的函数。
你是不是也有这么个习惯?不停地把新任务加到 todo list 中,只来得及完成紧急的,重要的任务,凡是不能半小时完成的,就越推越久直到清单变得特别长,长到甚至不想去看一眼。
我也经常这样。有不少同学问过我,为什么看起来我这么激情洋溢、乐观向上。因为我在朋友圈和博客里只展示激情洋溢、乐观向上的一面。推迟任务是常有的事,因为时间或者精力不够。任务推迟很久的,我就取消掉!说明即使我不完成天下也不会大乱。一般地,文章我不当时看,而是加到列表里。列里很长,闲了的时候就找出一篇来看。偶尔有还没来得及看的文章已经失去了链接、作者(被迫?)主动删除的,那就算了,也不特别地惋惜。每天,如果计划清单太长,看起来不可能完成那么多,我就删除最古老的那些,把计划看的文章移到爱好阅读或专业阅读列表里。诸如此类,眼不见心不烦。
每天,我还都会做一个操作——在“滴答清单”PC版里,昨天未完成的任务,已经被系统标注为过期的,我手动把它们标注为计划今天完成。这个动作已经持续了不止一年,这两天终于觉得DRY原则应该贯彻一下。AHK的语法忘得光光,边查边写,花1.5小时写了脚本,第二天又花半小时优化了一下代码,完成以后共72行。
过程如下。
1. 计划
干活之前,我先坐下来列个计划。
技术路线确定 用AHK的图片搜索功能。以前我用过这个技术,还写过一小段函数(有时好使,有时不)。滴答清单,操作时按的几个按钮,用AHK带的工具WindowSpy.ahk探测以后,都没有找到窗口句柄。没有稳定的句柄类,我了解的技术方案就只剩了图片搜索。这会导致限制屏幕的分辨率。不过,脚本只在我的机器上跑,基本不切换分辨率。对分辨率的限制可以忍受。
计划的功能,就是我平时的操作过程——由按列表排序改为按日期排序,然后点击 推迟,最后再改回按列表排序。操作涉及的按钮如下图所示。
列计划时预计得干一两个小时。如果实施中时间超过太多,那就得分割成几天完成了。
实际写代码计时,花了1.5小时,包括挣扎过想用函数,但是语法不熟悉而失败所花费的时间。
2. 运行的预置条件,检查 或 假设
程序/算法的特征之一是 可重现性。之所以有些程序不符合重现,十有八九是因为外部环境不同。所以,程序在运行之前,要保证环境(以及输入)是相同的。
我把AHK脚本中对预置条件的检查放在了初始化部分。
包括 分辨率,滴答清单进程的窗口激活/放在最上层、窗口最大化,在滴答清单中跳转到今日任务。
分辨率,粗糙地判断分辨率符合2560*1440,否则退出脚本。如果想增加通用性,并不需要修改这里,但是对每种分辨率都要单独截出所有按钮图标的图片。对分辨率的限制是由AHK搜索图片的机制决定的。
激活窗口并最大化,是为后面的鼠标操作创造条件。更严格地做法,应该在此之前禁用鼠标和键盘,避免用户误操作。由于我是唯一用户,主动运行而不是由时钟等外界因素触发,所以我可以做到不操作,不干扰脚本运行。更通用的做法,还可以包括把滴答清单的进程所对应的路径写成变量;更通用,还可以通过配置文件或GUI配置界面由用户修改。
在滴答清单中跳转到今日任务的方法,是发送快捷键。这不是滴答清单的默认配置,而是我的机器设置的。这里,假设AHK脚本运行在我的机器上。
以上,要么检测环境,要么粗暴地假设。粗暴的假设最省时间,检测、容错、适应各种环境越多越细致,工作量越大。所有这些检测都不是核心代码,而是核心代码工作的必要条件。有时,核心代码工作很好,有时,莫名其妙就崩了。并非核心代码有异,而是环境不同,或者如葛老师常说的,能运行的时候,还没出错,那不是技术,只是运气。工程和产品,运行别人的手里,不像原型只工作在受控的实验室环境下,需要巨大的工作量消耗在更多的非核心代码上。
往往,需要很多无聊换得些微快意,真是无耐。
3. 关键技术 语法
AHK的语法不熟悉,有点像我的母语basic,又不完全像。基本上每条语句都要查语法手册,包括内置函数调用、字符串、变量赋值这些基本操作,真是举步维艰。
用完就忘。
更糟糕地是,在网上查到的语法,即使是相当大的网站,仍然可能是错的。我猜测可能由于AHK的大小版本更迭,作者们对语法保持兼容性也不太执着吧。用bing搜索到的,比如(https://wyagd001.github.io/v2/docs/Functions.htm) 这种内容和目录看起来非常权威的,也是错的,或者与我的AHK版本不兼容。
更更糟糕的是,写代码语法出错时,AHK的编译器或解释器并不报错。它猜测我的意思就跑下去了,我也无从知道它到底怎么想的。反正结果出来了,不是我想要的。
最权威的,目前最好使的语法手册,是AHK自带的help文件名为AutoHotkey.chm。所以,如果你也写AHK脚本,别去搜索手册了,用本地的。有时候解决方案需要搜索网上的,也别全信,复制过来十有八九不能跑,版本不同,而且不告诉你。
更更更糟糕的是,我在这1.5小时代码中,一直想用函数。由于搜索图片若干次,操作类似,所以我想把这几步操作封装成函数。但是!不好使。我要调用,没有调用;我要定义,跑到了函数里面;没有报错,还是其他更明显的语法报错中给出一些AHK解释器执行的代码,我才知道它的理解和我的意图相差甚远。回忆起来,隐约有印象,每次我想用函数都不顺畅。极其偶尔成功过,我也并不知道为什么就成功了。
回顾本节标题,语法居然能成为关键技术,真是程序员之耻。
4. 关键技术 找图片
涉及到几张图片,我分别先用微信alt+a快捷键截图,保存成png无损图片,在AHK脚本中定义如下。
图片看起来如下。
之所以需要把变量提取出来,是因为类似 clock 这样的图标需要使用不止一次。根据DRY原则,根据经验,如果不抽象,那么在后续的修改中可能经常会需要修改不同的几个地方,稍一疏忽就漏掉一处——接着就是无穷无尽的不知道哪里出来的毛病。想起YP同学问过我,如果换成我,如果解决她遇到的问题。我当时说:由于咱俩的工作习惯不同,所以你遇到的问题,我不会遇到的。就像地上有个坑,你问我怎么跳出来。我不会跳进去的,离老远就看到,早早绕开。
搜索图片,在AHK中用法大致如下。
使用内置函数 ImageSearch,注意第一个逗号这种稀奇(我见识少)的语法。第1个参数X和第2个参数Y,用于输出,获取找到的图片的横纵坐标;X1,Y1,X2,Y2这4个参数是搜索的范围;最后一个参数%icon%中的百分号是必须的,一言难尽(我理解不深),是要搜索的图片,比如list.png。
但是!不好使呢。我在这句后面调用鼠标点击,鼠标不动。
此时,应该贯彻原则,不在项目代码中做技术原型实验。但是我自恃用过,觉得不会错,就在项目代码中做了实验。折腾了一圈,打印不出来图片的横纵坐标,空的;手动传参调用鼠标移动,好使。诸如此类。我终于想起来,在 ImageSearch 之后加了一条,
判断 ImageSearch 是否执行出错——包括有没有找到。没有,没找到图片。然而,我亲眼看到了图片在GUI界面上,又反复检查了截的图片文件 list.png。
查手册,AHK帮助说,用PrtSc截屏,粘贴到画笔中。
我突然想起来,这事儿,以前也发生过啊,不止一次。想起来当年关同学说:没有QQ,我怎么截屏啊。还得是基本操作。微信在截屏的过程中,压缩了吧,而AHK非常可能逐个像素判断的。也正因为这个,所以图片搜索依赖分辨率。如果支持多分辨率,就需要每个分辨率都截图(也包括滴答清单的不同皮肤,包括夜晚和白天不同),根据读到分辨率选择匹配的搜索图片。无他,遍历是最有效的方法。要么,就像我这次写的代码,粗暴地只针对我机器的分辨率。
5. 关键技术 函数
完成以上工作以后的代码相当丑陋,搜索6次按钮图标,每个点击一下,每段的代码看起来都是下面这样的。
因为我不会写函数,只能先这样对付着,6段非常相似的代码。能跑了。
第二天,我不甘心,又去搜索AHK的函数到底怎么写。搜索到的结果都表明,我写的对。但是运行起来的结果并不对。同时,我也不知道以前对的时候为什么就对了。错的时候倒是似乎好一点,虽然不知道为什么,反正大多数时候结果都是错的。
苍天不负苦心人,终于找到原因。从此以后不会再错了,也不会再对得不明不白了。
AHK 2.X版本手册中在程序员自定义函数中提到
https://www.autohotkey.com/docs/v2/Functions.htm,
"When a function is defined, its parameters are listed in parentheses next to its name (there must be no spaces between its name and the open-parenthesis)"
函数的名字 和 容纳函数参数列表的括号对儿 之间,不能有空格。
得这样写 foo(a,b)
不能写成 foo (a,b)
上述两写法的差别就只在 foo 的后面有没有空格。没有空格的才能正确运行。我不由得感叹:这什么烂语法,太不符合常规了。
蓦然一惊,这个,我是不是知道过,又忘了啊。
把6大段改成6次对同一函数的调用。
代码的逻辑清晰多了。运行前,
第1步.假设已按列表排序,所以点击列表图标;
第2步.改为按时间排序;
//以上是设置操作的环境
//以下是操作
第3步. AHK脚本点击延迟图标,滴答清单弹出对话框,
AHK脚本点击对话框中的延迟按钮;
//以上是操作
//以下是恢复操作的环境为原始状态
第4步.按列表排序
//以上是恢复操作的环境为原始状态
每两次鼠标点击之间 sleep 1秒钟,为了滴答清单有充分时间响应,并且我也能看到并享受自动操作的快乐。因为每两次操作都要sleep,所以这段代码也放到了ImageSearch_and_MouseClick函数之中。
6. 附录 代码
6.1 文件列表
ahk postpone.ahk
by_date.png
by_list.png
clock.png
list.png
postpone.png
postpone_button.png
6.2 代码
;;------------------
; icons
list=list.png
clock=clock.png
by_date=by_date.png
by_list=by_list.png
postpone=postpone.png
postpone_button=postpone_button.png
; init-------------
;; resolustion assumed
X1:=0
Y1:=0
X2:=2560
Y2:=1440
if (! (A_ScreenWidth == X2) || (ScreenHeight == Y2))
{
msgbox ,,, 需要显示分辨率为 2560*1440 才能正常工作。
exit
}
;; active window
Run, C:\Program Files (x86)\滴答清单\TickTick.exe
;; maximize window
Sleep 500
WinActivate, ahk_class HwndWrapper[TickTick.exe;;52329611-5419-4919-bf17-3295f24271e2]
WinMaximize , A
;; goto today-------------
SendInput ^h
sleep 500
;--- action seq---------------
;;-- in menu, find icon of list
;; *假设* default 排序图标是 list AKA "sorted by list"
ImageSearch_and_MouseClick(list)
;; sort "by date"
ImageSearch_and_MouseClick(by_date)
;; click "postpone"
if (ImageSearch_and_MouseClick(postpone) != 0)
{
ImageSearch_and_MouseClick(clock)
ImageSearch_and_MouseClick(by_list) ; 如果没有需要postpone的工作,此处恢复为 by_list
Exit
}
;; click "postpone_button"
ImageSearch_and_MouseClick(postpone_button)
;;-- in menu, find icon of clock AKA "sorted by date"
ImageSearch_and_MouseClick(clock)
;; sort "by_list"
ImageSearch_and_MouseClick(by_list)
Exit
ImageSearch_and_MouseClick(icon){ ; https://www.autohotkey.com/docs/v2/Functions.htm, "When a function is defined, its parameters are listed in parentheses next to its name (there must be no spaces between its name and the open-parenthesis)"
X1:=0
Y1:=0
X2:=2560
Y2:=1440
ImageSearch, X, Y, X1, Y1, X2, Y2, %icon%
if( ErrorLevel!=0 ) {
msgbox ,,, %icon% not found
return 555
}
MouseClick,% left, X+10, Y+10
Sleep 1000
return 0
}