惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

H
Heimdal Security Blog
A
Arctic Wolf
K
Kaspersky official blog
V
Vulnerabilities – Threatpost
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Simon Willison's Weblog
Simon Willison's Weblog
L
LINUX DO - 热门话题
MongoDB | Blog
MongoDB | Blog
T
Threat Research - Cisco Blogs
D
Docker
爱范儿
爱范儿
T
Tenable Blog
C
Check Point Blog
B
Blog
C
Cisco Blogs
Vercel News
Vercel News
The Cloudflare Blog
T
Threatpost
NISL@THU
NISL@THU
T
Tor Project blog
V2EX - 技术
V2EX - 技术
P
Palo Alto Networks Blog
Application and Cybersecurity Blog
Application and Cybersecurity Blog
T
Tailwind CSS Blog
G
GRAHAM CLULEY
P
Privacy & Cybersecurity Law Blog
SecWiki News
SecWiki News
博客园 - 司徒正美
S
Security @ Cisco Blogs
GbyAI
GbyAI
S
Secure Thoughts
Microsoft Security Blog
Microsoft Security Blog
The Register - Security
The Register - Security
Recorded Future
Recorded Future
Cloudbric
Cloudbric
Webroot Blog
Webroot Blog
N
News and Events Feed by Topic
Y
Y Combinator Blog
博客园_首页
T
Troy Hunt's Blog
The Hacker News
The Hacker News
雷峰网
雷峰网
Google DeepMind News
Google DeepMind News
U
Unit 42
AWS News Blog
AWS News Blog
PCI Perspectives
PCI Perspectives
V
Visual Studio Blog
博客园 - 聂微东
有赞技术团队
有赞技术团队
酷 壳 – CoolShell
酷 壳 – CoolShell

少数派

派早报:Google 发布 Fitbit Air 等 - 少数派 「新人报到」確認需求,再開始 - 少数派 从 SOLO 独立开发者社区,我看到了越来越多开发者开始做自己的产品 - 少数派 我怎么管理那些"不常做,但总会忘"的生活事项 - 少数派 人形机器人量产元年,数据才是具身智能的“生死线” - 少数派 BuhoLaunchpad 高度还原 Mac 启动台:开发历程与思考 - 少数派 五年陪伴依然不舍,DIY 换壳后让罗技 MX Master 3 继续服役 - 少数派 新玩意 240|少数派的编辑们最近买了啥? - 少数派 一日一技|为什么你应该关闭 iOS 的键盘声音 - 少数派 我做了个插件和 Skills,一键提取任何网站的设计规范 Design.md - 少数派 住在三四线城市的你,该开始录播客了 - 少数派 甘南秘境,大白高国 - 少数派 AI的审美:谁让把我变成川内倫子 - 少数派 返工怎能不烦恼,打工人片单总有一部是你的「嘴替」 - 少数派 为了让「上厕所」更健康,我做了一个小工具 - 少数派 AI + Skill,能够让生成的文章去除 AI 味吗? - 少数派 新玩意|韶音OpenDots ONE 耳夹式耳机 - 少数派 《美满》| 在每一个春天的晚上相爱(362) - 少数派 新玩意|优篮子 PS01 MagSnap 磁吸支架 - 少数派 自我整合手记 | 我开始早睡了:用稳定规则,为自由托底 - 少数派 用龙虾(OpenClaw)两个多月,我最深的12个体会 - 少数派 听歌时间到,12 张你可能错过的 2025 华语乐坛好专辑 - 少数派 承诺能追吗 - 少数派 macOS 26启动台没了? 我做了个不一样的App启动器 - Keboard - 少数派 《四海为家的人》| INTJ对话INTJ(361) - 少数派 你发过的那些黑历史,是时候一次清干净了 - 少数派 新玩意:安安静静玩,越玩越专注:计客密码机 - 少数派 iPad 用户首次体验 Android 平板:vivo Pad6 Pro - 少数派 数据逻辑强 - 少数派 极北行+ | 一路向北,探访日本至北之地 | 001 - 少数派 万字剖析:千问App深度体验报告(2026) - 少数派 在2026年,如何真正防止别人抄袭你的作品 - 少数派 怎么用 50 块搭个 AI 语音助手?我踩了 3 天坑 - 少数派 YeeroAI:让 AI 对话真正成为知识管理的一部分 - 少数派 爬泰山 - 少数派 「旅图显影」 App 更新:这次,我们补上了一点「手感」 - 少数派 假期出门太折磨?我的 23 条经验帮你规划惬意旅行 - 少数派 工作流会变吗 - 少数派 Claude Opus 4.6 怎么用最省钱?我测了 5 种方案 - 少数派 GPT Image 2 让图文并茂不再稀罕 - 少数派 用户侧出发——什么是AI,我要不要学习? - 少数派 找片、转存、整理、播放一条龙!让你的付费网盘值回票价 - 少数派 欢迎试用!日课一问2.0插件 - 少数派 自己做的MDeditor,原本想购买 Typora 试了两次支付不成功,干脆自己做一个 - 少数派 vibe coding了一个 3MB 的小工具,让 ~/Downloads 彻底告别混乱 - 少数派 因为受不了 Mac 的风扇策略,我做了一个风扇控制工具 - 少数派 别只怪模型 - 少数派 Warp 终端的 AI 功能怎么用?我测了一周的体验 - 少数派 AI 写代码老是出 bug?这 5 个配置我后悔没早知道 - 少数派 「新玩意」苹果出相机可能就这样:Sigma BF + 45mm F2.8 DG Contemporary - 少数派 一个面向2030年的AI操作系统是什么样子的:浅谈cola这款有灵魂的Agent - 少数派 别只看写代码 - 少数派 每天解决10个问题,还是一口气攻坚解决400个? - 少数派 AI 交易机器人怎么搭?我用 Claude 跑了一周实盘 - 少数派 Maptoposter Online:把你爱的城市画成艺术海报 - 少数派 Function Calling 怎么用?我测了 3 个模型发现差距真大 - 少数派 Legend Talk:我做了个 AI 圆桌,让 160 位思想家围着你的问题转 - 少数派 如何找到自己的蓝方?在小县城寻找压力测试 - 少数派 语音输入与软件接口|2026年聊AI时,我们都聊些什么(上) - 少数派 混动已经卖爆,纯电又来补刀——钛7闪充版简直“不讲武德” - 少数派 本月玩什么|朋友收藏、识质存在、沙罗周期 - 少数派 为什么要每天坚持输出? - 少数派 Claude API 挂了好几个小时,你的项目有备用方案吗? - 少数派 Function Calling 没你想的复杂——我用它做了个有点用的工具 - 少数派 登录系统立即播放视频或者图片音乐的软件 - 少数派 我为什么创建 FlipHTML5 下载工具 - 少数派 残局没电?多品牌外设电量统一管理软件EasyBluetooth已支持RTSS游戏内显示以及AIDA64 - 少数派 前往通义路的路 - 少数派 太好看了,媲美Sun的个人导航页,NAS部署星云门户 - 少数派 乌黑嘴唇“一键检测”上线了 - 少数派 派早报:Claude AI 接入多个创意软件生态、FILCO 生产方接手品牌等 - 少数派 【更新】BearCLI、Claude 连接器与 MCP 服务器 - 少数派 记了上千条流水,还是看不懂财务?我做了一个让 AI 读懂账本的工作台 - 少数派 MINI R56 升级原厂 Sport 模式 - 少数派 新玩意 | 一棵柠檬树(仿真版) - 少数派 Momenta的“物理AI”野望,需迈过“含摩量”这道关 - 少数派 网页直接投屏控制手机!NAS一键部署PandaScrcpy,流畅丝滑可远程。 - 少数派 众测|邀你一同探索随身 AI 硬件入口 YoooClaw C·ONE - 少数派 2050大会:分享时间是真诚 参会记 - 少数派 iPad 赋能电影创作:国内首部宣纸手绘长片《燃比娃》的幕后故事 - 少数派 AI的审美:我用 8 个大模型给 100 张旅行照片打分 - 少数派 普通人如何破圈?去参加一个本地协会 - 少数派 把极空间的图标全换了,主题DIY全攻略打造你的专属NAS桌面 - 少数派 电子便签墙,帮你实现便签自由 - 少数派 我如何用三个 CLI 工具取代文档创建需求 - 少数派 原来真的有人可以玩一辈子 - 少数派 社区速递 139 | 派友热议三月买了啥、复古单反尼康 Df 体验 - 少数派 06 作品的赏析与评价 - 少数派 TDS REVIEW|索尼 WF-1000XM6 降噪真无线耳机体验 - 少数派 35.98万起售的第二代腾势D9,我看重的不是堆料,而是不凑合 - 少数派 鼠须管 Squirrel 皮肤配置指北 - 少数派 从watch ultra2换到redmi watch6 - 少数派 派早报:阿里巴巴发布视频生成模型 HappyHorse 1.0 等 - 少数派 别迷信1M - 少数派 家人们天塌了!网盘“大封杀”,多个渠道多条路,NAS部署PanHub - 少数派 AI与人勾心斗角!NAS一键部署AI狼人杀,假日休闲必备。 - 少数派 电商必备!Comfyui工作流批量生图插件,一次生成12张!支持Nano banana pro模型 - 少数派 Comfyui工作流配置Gpt-image-2模型教程,0.03/张 - 少数派 OpenClaw第三方APi怎么配置?可使用Gpt-image-2模型 - 少数派 会员社区话题精选 Ep. 103 - 少数派
一日一技 | 用 AutoHotkey 偷偷背单词,甚至读本书 - 少数派
2025-07-23 · via 少数派

Matrix 首页推荐 

Matrix 是少数派的写作社区,我们主张分享真实的产品体验,有实用价值的经验与思考。我们会不定期挑选 Matrix 最优质的文章,展示来自用户的最真实的体验和观点。 

文章代表作者个人观点,少数派仅对标题和排版略作修改。


同许多人一样,我在人生的每个阶段里,都多次启动过背单词的计划,当然,也每次都没有坚持下去。我认为,这其中一个原因在于,无论是小时候背单词书,还是现在使用手机上的单词软件,都过于强调学习计划。然而,生活中的变化和诱惑实在太多,在没有考试等因素驱动的情况下,只要有一天没有按照计划行事,那么遗忘和放弃就是自然而然的结果。

总而言之,作为工作中实际上不是特别需要使用英文的打工人,我没有很强的动力去每天背上十个或者更多单词。我只想要在打工的间隙,悄无声息地偶尔学习一下,无须低头看手机,不强求每天背单词的数量,不考虑科学的记忆曲线,背到了算赚到,没记住那拜拜下次再见。

出于这个目的,我以「桌面」和「背单词」为关键字进行了检索,发现市面上的此类软件不少,应当也有很多优秀的产品。可惜的是,这些软件对我来说都不够轻量,显得太正式,界面和丰富,功能太复杂了。其中看起来最符合我要求的一款,叫 ToastFish,据说是让用户可以在通知栏背单词,然而,它在我这台破工作电脑上并没有运行成功。另外,从介绍上看,这款软件也逃脱不了「本次背多少个单词」这一让我感觉有些焦虑的设定。

于是,我很自然地又想起了那个已经在我电脑后台默默运行的 AutoHotkey 脚本,自从我因为工作而开始使用 Windows 办公后,AutoHotkey 给予我很大的帮助,具体可参见这篇文章。上一次我使用 AutoHotkey 编写脚本,是为了写划词翻译和截图 OCR,具体可以参见这篇文章。结合我的目标,很容易联想到 AutoHotkey 自带的 ToolTip 函数,其效果是显示一个工具提示窗口。窗口非常小,用来不着痕迹地背单词可以说是刚刚好。

AutoHotkey 的语法足够简单,配合上现在的 AI 工具,经过一点尝试,我就完成了一个基本可用的背单词小脚本。想到工具提示窗口小巧、不易察觉的特性,我又顺手把它改成了一个阅读文章的小脚本,相比起网上使用 Word 文档或者记事本的摸鱼技巧,利用工具提示窗口更加轻便和隐蔽。

即便算上空行,两个脚本加起来也不过 200 行左右。不需要额外下载软件(当然单词本和书籍是需要另外准备的),就拥有了两个不同类型软件的核心功能,我认为是足够经济实惠的。因此,我也认为值得在这里分享。

下面,我先将脚本实现的全部功能展示如下。诸位如果对此感兴趣,可以继续看后文,我会解说其中的核心功能部分,并在最后列举一些额外功能的实现思路,以方便大家能够按需修改。现有功能的完整的代码我还是会放在文末。

按下设定的热键进入或退出背单词模式(所有热键均可自定义,下同):

  • 按下热键,在鼠标所在的位置会随机显示一个单词,再点击热键可以查看单词的音标和释义;
  • 在此基础上,可以通过热键调用其他词典(比如 GoldenDict)以进一步查看单词的其他释义、例句(下图未展示),或者收藏单词以便下次复习,又或者彻底删除太简单的单词;
  • 可以随时切换单词表为已收藏的单词,进入「复习模式」开始复习;
  • 也可以随时切换以进入「测试模式」,此时按下热键首先显示的不是单词本身,而是对应的音标和释义,方便检查自己是否学会了单词的拼写;
  • 如果有一段时间未学习单词,会自动退出单词模式(防止误触热键)。
单词模式:从开始到 abandon

按下设定的另一组热键进入或退出看书模式:

  • 按下热键开关显示一行文字(推荐使用鼠标中键);
  • 按下热键上下换行(推荐使用鼠标滚轮);
  • 如果有一段时间没有看书,会自动退出看书模式(防止误触热键);
  • 下次看书时,自动从上次退出时的一行开始。
看书模式:波澜不惊,心旷神怡

第一项功能:背个单词

首先,我们需要一张词汇表。这里必须感谢具有分享精神的每个人,如今要取得一份 TXT 格式的单词本并不困难,比如我自己使用的单词本来自这里

总体来说,满足如下条件的文档就非常符合我的要求:

  1. 每行一个单词;
  2. 单词后跟着音标、释义等,并以空格隔开。
像这样的单词表就很合适

话虽如此,第一个条件不满足也可以,只不过在读取单词表的时候要多写一些判断条件,不以按「行」区分单词,而是用分割线或其他符号来标记。

第二个条件不满足也没有任何关系,因为我们完全可以设计成只调用其他词典来显示释义。

接下来是脚本,我给单词模式添加了开关功能,这就像是打开了一个软件。这么做的好处是,我不需要把各种功能刻意放在一些特别复杂或不常用的热键上。

在 AutoHotkey 实现「开关」并不困难,只需要将一个变量设置为单词模式开关的标记。然后,在后续的所有操作上,一律使用 #HotIf,表示满足特定条件下时热键才生效。我自己使用的是 Ctrl 加上鼠标的一个侧键(XButton1)来开关单词模式。

words_flag:= false ; 标记是否进入了单词模式
^XButton1::
{
global words_flag
words_flag:= !words_flag ; 每次按下热键,改变标记的状态
; ……
}

#HotIf words_flag ; 仅在 word_flag 为 true 的时候,后续的热键才生效
; ……
#HotIf

在打开单词模式的同时,有必要预读取一次单词本,防止我的破电脑在后续显示单词时因反复读取而产生任何卡顿。

一开始我在获取单词表时,中文和音标总是显示为乱码。由于我对文字编码问题从来是搞不明白的,所以主要的解决思路就是尝试。最终,我在这台电脑上的解决方法是:

  • 将下载下来的 TXT 单词本手动保存为带有 BOM 的 UTF-8 编码文件;
  • 在使用 AutoHotkey 写入时指定使用「UTF-8」编码;
  • 在使用 AutoHotkey 读取时指定使用「UTF-8-RAW」编码。
记得选择编码

AutoHotkey 的常用的编码还包括「UTF-8」和「CP936」(GBK/GB2312),如果出现乱码,不妨一试。

global words_list
Loop Read, words_path . "words.txt", "UTF-8-RAW" ; 读取单词本内容
words_list.Push(A_LoopReadLine)

接下来,我们需要按下热键显示一个单词。我使用的热键是同样是鼠标侧键,如果使用的鼠标没有侧键,我建议使用一个相对不容易误操作的组合键作为热键。产生单词的部分,最简单的写法是生成一个随机数,然后显示单词表里对应行数的内容。

XButton1::
{
word_index:= Random(1, words_list.Length) ; 生成 1 到单词表长度之间的随机数
ToolTip(words_list[word_index])
}

为了区别于单词壁纸,让背单词的过程更接近于「背」而不是「看」,我们可以分两步显示词汇表的内容。一开始只显示单词本身,先想想自己是否认识这个单词,再单击鼠标左键后才显示对应的音标和释义。或者反过来,一开始只显示音标和释义,先猜猜这个单词如何拼写,再显示单词原文,这就相当于是各种背单词软件中的「测试模式」。

exam_flag:= false
^MButton:: ; Ctrl + 鼠标中键开启或关闭“测试模式”
{
global exam_flag
exam_flag:= !exam_flag
if exam_flag
ToolTip("测试模式")
else
ToolTip("普通模式")
SetTimer () => ToolTip(), -800
}

XButton1::
{
global words_list, word_index
word1:= StrSplit(words_list[word_index], " ", , 2)[1] ; StrSplit 分割空格前后的内容
word2:= StrSplit(words_list[word_index], " ", , 2)[2]
if exam_flag
ToolTip(word2)
else
ToolTip(word1)
KeyWait("LButton", "D") ; 不管一开始显示什么,单击鼠标左键,都会显示完整的单词+注释等内容
ToolTip(word1 . "`n" . word2)
}

其实到这一步为止,已经完成了我最初设想的背单词功能,如果不需要更多花里胡哨的功能,只需要在上面的内容里再加上两行单击关闭工具提示框的代码即可:

KeyWait("LButton", "D") 
ToolTip()

不过我还是为它适当添加了一些功能,包括查看词典,收藏和删掉单词。为此,我们需要再增加一个标记,仅在单词显示后这些功能才生效。

dict_flag:= false ; 标记现在是不是显示了单词内容
XButton1::
{
; ……
global dict_flag
dict_flag:= true
}

首先是调用其他词典软件查看释义,我这里设置设置的热键是键盘上的 D 键。

设置快捷键算是 AutoHotkey 本职工作,不存在难度。由于我下载过 GoldenDict,因此这里就使用这个软件。它可以读取剪贴板内的单词,并通过浮窗显示释义。虽然,在这台电脑上的反应不算快,但偶尔查一次,仍不失轻便。AutoHotkey 也提供了存放和恢复剪贴板内容的方法(当然某些特殊格式可能无法被识别),可以尽可能地减少对日常工作的影响。

GoldenDict 设置
d::
{
; ……
tmp:= ClipboardAll() ; 将剪贴板内容暂存下来
A_Clipboard:= StrSplit(words_list[word_index], " ", 2)[1] ; 复制单词到剪贴板
Send "^+c" ; 这是我设置的 GoldenDict 查单词快捷键
A_Clipboard:= tmp ; 恢复剪贴板
}

其次是收藏单词,我设置的热键是键盘上的 S 键。

收藏功能依靠保存至同目录下的「favourites.txt」文档来实现,你可以事先在文件夹内手动创建好文件,也可以像我一样在进入单词模式或首次收藏功能的时候创建这个文件。为了背收藏的单词,我们需要增加一个切换单词本的操作,我自己设置的快捷键是 Shift + 鼠标侧键。

; 创建 favourites.txt 的操作
if !FileExist(words_path . "favourites.txt")
FileOpen(words_path . "favourites.txt", "w", "UTF-8").Write(Chr(0xFEFF) "")

; 将单词收藏到 favourites.txt 的操作
s::
{
; ……
; 下面这个 if 的目的是判断单词在收藏里有没有出现过
if !Instr(FileRead(words_path . "favourites.txt"), words_list[word_index])
FileOpen(words_path . "favourites.txt", "a", "utf-8").Write(words_list[word_index] . "`n")
}

; 切换单词表为 favourites.txt 的操作
favourite_flag:= false
+XButton1::
{
global favourite_flag, words_list
words_list:= [] ; 清空单词表,切换为已收藏的单词表或原始单词本
favourite_flag:= !favourite_flag
if favourite_flag
{
Loop Read, words_path . "favourites.txt", "UTF-8-RAW"
words_list.Push(A_LoopReadLine)
ToolTip Format("复习模式")
}
else
{
Loop Read, words_path . "words.txt", "UTF-8-RAW"
words_list.Push(A_LoopReadLine)
ToolTip Format("结束复习")
}
SetTimer () => ToolTip(), -800
}

还有一种情况是这单词实在太简单了,我们不想要再次看到它。

那么实际操作就是从单词本里彻底删掉这一行,需要修改单词列表和单词文件。如果我们选择的单词表是相对全面或者基础的,那么删掉单词会是一个很常用的功能,因此我这里使用的热键是鼠标的右键(RButton)。也就是说,对于还想要再看到的单词,左键关闭显示,对于不想再见到的单词,右键删除。

RButton::
{
; ……
words_list.RemoveAt(word_index) ; 删除词汇表的这一行
new_list:= ""
for i in words_list
new_list.= i "`n"
; 覆盖 TXT 单词本内容,这里有可能是 favourite.txt,也可能是 words.txt,取决于在背哪个词汇表
FileOpen(相应的文件路径, "w", "utf-8").Write(new_list) 
}

在什么都不做只关闭工具提示框,以及收藏、删单词的场合,都应该关掉工具提示框,避免单词一直显示在桌面上,同时应将 dict_flag 设置为 false,避免反复触发以上操作。

global dict_flag
dict_flag:= false
ToolTip()

最后,由于这个背单词工具没有任何界面,全靠热键操作,如果忘记退出背单词模式,有可能出现一些尴尬场景。比如说,当在给其他人演示的时候,不小心点到热键,屏幕上突然蹦出一个单词。

因此,我们还是加一个自动关闭的设定为好。具体而言,就是设置一个计时器,超过一定时间没有使用单词模式下的功能,就自动退出单词模式。

words_last_press:= A_TickCount ; 记录最近一次打开或使用背单词功能的时间

words_check() {
if A_TickCount - words_last_press >= 600 * 1000 ; 检查上次使用时间距离现在是否超过了 600 秒
words_mode ; 改变单词模式的开关状态
}

; 进入单词模式的时候,启动计时器并更新最后使用时间
^XButton1:: words_mode() 
words_mode()
{
global words_last_press, words_flag
words_last_press:= A_TickCount
words_flag:= !words_flag

if words_flag
{
; ……
SetTimer words_check, 120 * 1000 ; 打开单词模式的同时打开计时器,每 120 秒检查一次
}
else
{
; ……
SetTimer words_check, 0 ; 关闭单词模式的同时,关闭计时器
}

; 显示单词的时候,更新最后使用时间
XButton1::
{
global words_last_press
words_last_press:= A_TickCount
; ……
}

第二项功能:读本书

写完背单词模式脚本,读书模式大部分内容都可以照搬。首先当然也需要准备好想要阅读的 TXT 文档,进入阅读模式后,脚本也需要逐行读取文件,定时关闭功能显得更为重要。不同的是,因为很多书籍存在大量空行,读取文件时要将空行排除在外。

if Trim(A_LoopReadLine) != "" ; 排除空行
book.Push(A_LoopReadLine) 

另外,由于书本内容一行可以非常长,ToolTip 显示无法设置宽度,默认宽度为一整个屏幕,这会阻碍我们阅读,最好还是把过长的内容手动分一下行。

这么多内容如果都放在一行的话就太长了
split_string(s, len:= 25) ; 处理文字,按照默认 25 个字换一次行
{
result:= ""
Loop Parse s
{
result.= A_LoopField
if (Mod(A_Index, len) = 0)
result.= "`n"
}
return result
}

看书和翻书可以都放在鼠标中键(滚轮)解决,这样就可以不动键盘实现翻书。

; 用鼠标中键打开或关闭显示书本内容
MButton::
{
; ……
global reading_flag ; 标记是否正在显示内容
reading_flag:= !reading_flag
if reading_flag
{
ToolTip(split_string(book[book_index])) ; 显示一行内容,其中 book_index 指示当前阅读到了哪一行
}
; ……
}

; 用鼠标滚轮上下翻页(行)
#HotIf reading_flag
WheelUp::
{
global book_index
book_index:= Max(1, book_index - 1) ; 这是为了避免在上翻时出现不存在的行号,比如第 0 行(AutoHotkey 的数组从 1 开始),下同
ToolTip(split_string(book[book_index]))
}
WheelDown::
{
global book_index
book_index:= Min(book.length, book_index + 1)
ToolTip(split_string(book[book_index]))
}
#HotIf

读书模式与背单词模式最大的区别在于,背单词是随机出现一个单词,每次背的东西可以是不一样的,但读书是要接着上文的,总不可能每次都从头开始。因此,我们就需要一个方法来记录上次读到哪里了。关于此,茴香豆有不止四种写法,我自己试了其中的三种。

第一种是阅后即焚,在关闭书籍显示或阅读结束的时候,读到哪里删到哪里,将书越读越薄的理念贯彻到底。好处是不用记录读到哪里了,每次都从第一行开始看,缺点是被删掉了的内容,是无法在下次点开的时候回看的。

book_index:= book_index - 1 ; 往上一行开始删
while book_index > 0
{
book.RemoveAt(book_index)
book_index -= 1
}
book_index:= 1
new:= ""
for i in book
new.= i "`n"
FileOpen(book_file, "w", "utf-8").Write(new) ; 删完了写回原 TXT 文档

第二种是在关闭书籍显示或阅读结束时,把行号加入读取的文本里最后一行存起来。为了避免书的最后一行本身是数字,需要在行号的前面加一些特殊符号(我使用的是「&&」)来避免误会。

Loop Read, book_file, "UTF-8" ; 读取书籍内容到 book 变量
{
if Trim(A_LoopReadLine) != ""
book.Push(A_LoopReadLine)
}

if SubStr(book[book.length], 1, 2) != "&&" ; 如果当前文档最后一行不是以“&&”开头,那么说明是首次打开,
{
FileOpen(book_file, "a", "utf-8-RAW").Write("&& 1") ; 在文档最后一行添加 && + 行号
book.Push("&& 1")
}
book_index:= StrSplit(book[book.length], " ")[-1] ; 分离出行号

; 下面是关闭书籍显示或结束阅读时加行号的操作
FileOpen(book_file, "a", "utf-8").Write(" " . book_index)

由于上面两种方法都会删改书本内容,我总觉得有点别扭。因此,目前我使用的是第三种方法——把行号放在 TXT 文档的文件名里。

; TXT 文档的地址和原始文件名 book.txt
book_path:= "D:\ahk"
book_file:= "D:\ahk\book.txt"

; 在文档所在位置匹配文件名带 book 的文档
Loop Files, book_path . "book*.txt", "R"
book_file:= A_LoopFileFullPath
if StrSplit(book_file, "")[-1] = "book.txt" ; 如果文件名没有包含数字,那么说明是首次打开
{
FileMove(book_file, book_path . "book1.txt") ; 将文档重命名为 book + 行号
book_file:= book_path . "book1.txt"
}
book_index:= SubStr(StrSplit(StrSplit(book_file, "")[-1], ".")[1], 5) ; 截取文件名中的数字,即行号

; 下面是关闭书籍显示或结束阅读时加行号的操作
FileMove(book_file, book_path . "book" . book_index . ".txt")

其他方法包括在同路径下新建一个文本,类似单词模式下的 favourites.txt,专门用来存放读到哪里,同样也可以避免反复删改原文档。唯一需要记得的是,在换书的时候,应该删掉或重置这个只记录行号的文件。

未来可能的扩展:画个饼

作为纯粹的业余人士,我愿意稍微花点时间(其实也花不了多少时间),来折腾一些小工具,提高自己的工作或学习效率,是因为比起别人设定好的软件,只有我才真正知晓自己的需求。

我目前写好的脚本,在实现背单词和看书的基本功能的同时,在界面整洁(根本没界面)、保护颈椎(减少低头看手机的时间)、防止焦虑(没有设置学习任务)、稳定情绪(不需要在同事凑过来的时候匆忙关闭软件或文档)等方面,还有着独特的优势。

而且,如果哪天我想要使用更多功能,不需要等待开发者,只要自己动手就可以将它改造为「威力增强版」,下面举几个我想到的栗子和实现方式。

对于背单词模式:

第一,我可以在测试模式里设置难度,或者说从更多维度考查自己记忆单词的程度。

例如,在困难模式下,将音标和释义进一步分离,每次只随机显示一项;而在简单模式下,显示单词的随机几个字母拼写,比如将「苹果」对应的单词显示为「a _ _ l _」。

这个操作的实现并不复杂,只需要结合使用随机数生成(Random)+ 截取字符串(StrSplit、SubStr)就可以实现,这样测试单词的效果就很接近于现实中的单词软件。

第二,我可以更集中地学习一批单词,有点类似于设置学习任务。

如果觉得单词表里的词汇太多,不利于集中学习怎么办。我们没有必要手动地去复制出 20个单词到原来的 words.txt 文档内,而是可以写脚本从单词表中随机挑选 20 个,存放到一个新文档中,然后将学习的词汇表切换至这个文档进行反复学习,并通过测试模式检验拼写。等到这 20 个单词学得差不多了,再自动或点击热键来补充到 20 个。

关于这部分内容的实现,可以参考上面提到的随机数生成,以及加入到收藏和删除单词的写法。

第三,我可以记录每个单词的学习次数,或单词最近一次出现的时间,随机但优先显示学习次数较少、上次出现时间较久远的词汇。

这一升级最好是结合上面的学习任务进行。我们需要相应记录词汇出现的频率表,用某种数据结构或算法来调整接下来出现的单词。比如,每次需要显示单词的时候,根据每个词汇出现次数和上次出现时间计算出一个权重,仅在权重前 50% 的单词中随机显示单词。显示单词的同时,需要相应更新该词汇的出现次数(+1),并记下它最后出现时间。

AutoHotkey 中提供了排序算法(Sort),也能够很精确地记录时间(年、月、日、时、分、秒,分别对应 A_YYYY、A_MM、A_DD、A_Hour、A_Min、A_Sec 这个内置变量),其他诸如写入和读取文档的方法,在上文中也已经出现过,实现起来确实稍复杂但也不算困难。主要难度可能在于权重计算公式的合理性,要简化的话,可以考虑只考虑出现的次数或最后出现时间的一项。

第四,计算权重完全也可以不依赖学习的次数,而是自己的主观感受。

我们可以把删除单词和收藏单词改造为「太简单」和「记不清」两个热键。给每个单词一个初始分数 0 分,如果单词「太简单」,则扣除一定分数,「记不清」则增加一定的分数,如果普通地过掉单词,则分数不增不减。根据单词分数的高低,决定其出现的概率,低分不出现,高分多出现。

第五,我们还可以做得再像背单词软件一点,对我们的学习成果做一个记录和分析,比如今天学会多少个单词,近一周学了多少个单词……

对于读书模式:

第一,如果想要同时读多本书,可以考虑添加一个书架

具体的操作上,也就是增加一个记录书籍文件名称的文档,并设置热键切换在读书籍。至于上面提到的行号记录,就可以放在这个文档中。

这个「书架」文档可以手动维护,也利用脚本自动维护,方法是读取目录下的全部 TXT 文档,添加至「书架」文档中。如何读取目录下名字符合一定条件的全部文档,以及如何维护文档内容,在本文中亦均有提及。

第二,如果读书模式读的是外文书籍,可以增加一个翻译热键。

利用各大词典或翻译工具、AI 工具的接口,翻译当前显示行内容为中文。这里同样可以利用剪贴板 + 其他软件的快捷键来使用。如果没有其他软件,那么可以参考我的上一篇文章,让 AutoHotkey 再辛苦兼职一下翻译功能。

不过,对我而言,现有的功能就已经十分足够。加上这些东西不是不好,也不是实现不了,可我总觉得,随着软件变得复杂,学习目标变得明确,可能会让我太过执着于「学到」点什么,失去了在工作间隙无痛地摄入零碎知识的初心。也许,这样一来,我将会逐渐地不想要使用这些功能了。

最后,是背单词和读书模式的完整代码:

#Requires AutoHotkey v2.0
; 文档的默认地址为“D:\ahk\”
; 单词本和书本的默认名字分别为“words.txt”和“book.txt”

; 背单词模式
words_list:= []
word_index:= 1
words_path:= "D:\ahk\"
words_flag:= false
dict_flag:= false
favourite_flag:= false
exam_flag:= false
words_last_press:= A_TickCount

^XButton1:: words_mode()
words_mode()
{
global
words_last_press:= A_TickCount
words_flag:= !words_flag
words_list:= []
if words_flag
{
if !FileExist(words_path . "favourites.txt")
FileOpen(words_path . "favourites.txt", "w", "UTF-8").Write(Chr(0xFEFF) "")
Loop Read, words_path . "words.txt", "UTF-8-RAW"
words_list.Push(A_LoopReadLine)
SetTimer words_check, 120 * 1000
ToolTip Format("开始")
SetTimer () => ToolTip(), -1200
}
else
{
dict_flag:= false
SetTimer words_check, 0
ToolTip ("结束")
SetTimer () => ToolTip(), -800
}
}

words_check() {
if A_TickCount - words_last_press >= 600 * 1000
words_mode
}

#HotIf words_flag
^MButton::
{
global exam_flag
exam_flag:= !exam_flag
if exam_flag
ToolTip("测试模式")
else
ToolTip("普通模式")
SetTimer () => ToolTip(), -800
}

+XButton1::
{
global favourite_flag, words_list
words_list:= []
favourite_flag:= !favourite_flag
if favourite_flag
{
Loop Read, words_path . "favourites.txt", "UTF-8-RAW"
words_list.Push(A_LoopReadLine)
ToolTip Format("复习模式")
}
else
{
Loop Read, words_path . "words.txt", "UTF-8-RAW"
words_list.Push(A_LoopReadLine)
ToolTip Format("结束复习")
}
SetTimer () => ToolTip(), -800
}

XButton1::
{
global words_list, dict_flag, word_index, words_last_press
words_last_press:= A_TickCount
word_index:= Random(1, words_list.Length)
try
{
word1:= StrSplit(words_list[word_index], " ", , 2)[1]
word2:= StrSplit(words_list[word_index], " ", , 2)[2]
if exam_flag
ToolTip(word2)
else
ToolTip(word1)
KeyWait("LButton", "D")
ToolTip(word1 . "`n" . word2)
dict_flag:= true
}
}
#HotIf

#HotIf dict_flag
LButton::
{
global dict_flag
dict_flag:= false
ToolTip()
}

d::
{
tmp:= ClipboardAll()
A_Clipboard:= StrSplit(words_list[word_index], " ", 2)[1]
Send "^+c"
A_Clipboard:= tmp
}

s::
{
global dict_flag
if !Instr(FileRead(words_path . "favourites.txt"), words_list[word_index])
FileOpen(words_path . "favourites.txt", "a", "utf-8").Write(words_list[word_index] . "`n") 
dict_flag:= false
ToolTip Format("加入收藏:{}", StrSplit(words_list[word_index], " ", 2)[1])
SetTimer () => ToolTip(), -1200
}

RButton::
{
global words_list, dict_flag
words_list.RemoveAt(word_index)
new_list:= ""
for i in words_list
new_list.= i "`n"
if favourite_flag
FileOpen(words_path . "favourites.txt", "w", "utf-8").Write(new_list)
else
FileOpen(words_path . "words.txt", "w", "utf-8").Write(new_list)
dict_flag:= false
ToolTip()
}
#HotIf

; 读书模式
book_index:= 1
book_path:= "D:\ahk\"
book_file:= "D:\ahk\book.txt"
book:= []
book_flag:= false
reading_flag:= false
book_last_press:= A_TickCount

^XButton2:: book_mode()
book_mode()
{
global book_flag, book, book_index, book_file, book_last_press, reading_flag
book_flag:= !book_flag
book_last_press:= A_TickCount
if book_flag
{
Loop Files, book_path . "book*.txt", "R"
book_file:= A_LoopFileFullPath
if StrSplit(book_file, "\")[-1] = "book.txt"
{
FileMove(book_file, book_path . "book1.txt")
book_file:= book_path . "book1.txt"
}
book_index:= SubStr(StrSplit(StrSplit(book_file, "\")[-1], ".")[1], 5)
Loop Read, book_file, "UTF-8"
{
if Trim(A_LoopReadLine) != ""
book.Push(A_LoopReadLine) 
}
SetTimer book_check, 120 * 1000
ToolTip("开始")
SetTimer () => ToolTip(), -800
}
else
{
SetTimer book_check, 0
reading_flag:= false
FileMove(book_file, book_path . "book" . book_index . ".txt")
ToolTip("结束")
SetTimer () => ToolTip(), -800
}
}

book_check() 
{
if A_TickCount - book_last_press >= 600 * 1000
book_mode
}

split_string(s, len:= 25)
{
result:= ""
Loop Parse s
{
result.= A_LoopField
if (Mod(A_Index, len) = 0)
result.= "`n"
}
return result
}

#HotIf book_flag
MButton::
{
global reading_flag, book_file, book_last_press
book_last_press:= A_TickCount
reading_flag:= !reading_flag
if reading_flag
{
try
ToolTip(split_string(book[book_index]))
}
else
{
FileMove(book_file, book_path . "book" . book_index . ".txt")
book_file:= book_path . "book" . book_index . ".txt"
ToolTip()
}
}
#HotIf

#HotIf reading_flag
WheelDown::
{
global book_index, book_last_press
book_last_press:= A_TickCount
book_index:= Min(book.length, book_index + 1)
ToolTip(split_string(book[book_index]))
}
WheelUp::
{
global book_index, book_last_press
book_last_press:= A_TickCount
book_index:= Max(1, book_index - 1)
ToolTip(split_string(book[book_index]))
}
#HotIf

> 关注 少数派公众号,解锁全新阅读体验 📰

> 实用、好用的 正版软件,少数派为你呈现 🚀