用AHK脚本实现滴答清单每天任务自动推迟

技术路线:使用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

}