NVDA 插件开发实践第三篇: 敢想敢做——写出你的第一行代码,控制台不可怕

时隔几个月,插件开发实践的第三篇来了!

这是 NVDA 插件开发实践系列文章的第三篇。你可以通过下面的“传送门”查看之前的章节:

前言

在上一篇,我找到了一个实际需求——在 Visual Studio Code 中读出编辑器中当前光标所在行号。作为一个懒人,我给你的第一建议不是说干就干,,而是确定需求,检索已有方案,最终思考是否适合自己。这个过程中,相信你我收获的不只是源于最初问题的那一个一个答案。

想法可以有,路要一步一步走

在分析了已有方案后,我并不满意,原因如下:

  1. 状态栏上的信息很多,对于读出行号这一需求而言,其他的内容显然是冗余的。
  2. Ctrl+G 足够快捷但改变了系统焦点位置,实际上,想读出行号我需要两步: Ctrl+G 后按 ESC 返回编辑器。

如果第二个方案可以勉强接受,那么能不能改进一下,解决我提到的槽点——获取行号需要执行两步的问题。当然可以,对于 NVDA 来说,插件的能量是巨大的。这种需求自然不在话下。

那么,从一个最符合直觉的角度来考虑,我们的插件可以是怎样的呢?我的初步构想大概如下:

  1. 提供一个快捷键用于在需要的时候读出行号。
  2. 最好能跟随 NVDA “文档格式”面板中“读出行号”的选项,如果开启该选项则支持自动读出行号。

先考虑实现第一个功能,至少需要解决两个小问题,一是如何以编程方式获取到当前光标所在的行号,可能是一个 string;二是如何让 NVDA 将获取到的行号信息读出来。

不忘重申: 在“预备篇”我也说过,这个系列不是面向初学者的入门指南,你至少应该熟练掌握甚至精通 NVDA 的使用,其次,你至少应该对 python 有所了解,能够用 python 编写一些处理日常任务的脚本应该是最基本的要求。
从第二篇你也能看到,能够熟练应用浏览器检索/分析资料也会让这一切变得更简单。

写代码从学会 Copy 开始

没错,我就是要先教你怎么学会复制粘贴。这里的复制粘贴不完全等价于 Ctrl+C、Ctrl+V,实际上我们要知道的是去哪里复制;复制哪些内容;粘贴到哪里。

我们常说照葫芦画瓢,是有道理的,写代码当然要先看看别人怎么写的。

既然是开发 NVDA 插件,我更推荐你从 NVDA Core 中去阅读代码,原因有三:一是更具体,更有针对性,有助于你了解 NVDA 的核心 API 及其实现原理,长远来看,这就是在学习从哪里复制的一个过程。二是严谨性,要知道,能合并入 NVDA 核心的代码是经过了社区开发者的多轮 Review 的,代码质量有一定的保障;三是学习这个项目的一些 Best Practice 有助于未来做核心贡献,NVDA 的 code base 目前采用了 Ruff 进行 Lintting Check,即便不是每个学习 NVDA 插件开发的人都会成为未来的 code contributor 仅从培养新手的 coding style 的角度来看,阅读高质量的代码也是益处多多。

所以,建议先安装一个 GitNVDA 的 GitHub 仓库 clone 到本地,每个合格的 Plagiarist 人手一份哦。注意,不是 Download ZIP 哈!具体的步骤我就不写了,相信聪明的你可以利用搜索引擎学到这部分内容。

去哪里复制

思考一下,我们要实现的功能类似于哪个已有功能?去看看那部分代码,会不会从中得到一些启发呢?

按下某个按键,读出一些内容。 NVDA 里类似的功能很多,比如 NVDA+F12 读出时间。让我们去 NVDA 的代码库中找找这部分代码:

  1. 打开 Clone 回来的 NVDA 代码库,探索下目录结构,发现这个目录里包含了一些 Build NVDA 所需的文件;
  2. 看到了 source 这个目录(猜想,这就是存放源代码的地方喽);
  3. source 目录里的内容,从名字上看大致也就知道其作用了,所以,懂一点英文可能还是有用的,别听他们忽悠你了——英文无用论。
  • appModules 应用程序模块,打开看看,存放了一些对特定程序的特殊支持代码;
  • brailleDisplayDrivers 显然,这是盲文点显器驱动;
  • brailleViewer 这是盲文查看器;
  • synthDrivers 这是语音合成器驱动;
  • globalCommands.py 从名字上看,这是全局命令的意思。

经过粗略的探索,我们猜想, NVDA+F12 这个功能最有可能在 globalCommands.py 里面,打开看看,文件的顶部是这个文件的版权声名:遵循的开源协议、贡献者等信息,不得不说,贡献者很多,大呼开源大法好。

尝试在 globalCommands.py 中搜索 NVDA+F12 这个字符串,你已经找到了。

复制哪些内容

学过 Python 的你,应该能很轻松看懂 script_dateTime 方法的执行逻辑,此处省略其他的代码,看 script_dateTime 的最后一行,

ui.message(text)

似乎朗读文本的功能就是这一行,能不能验证一下呢?此时,我们应该想到 NVDA 的 Python 控制台。

在 NVDA 菜单的“工具”子菜单下,或者按 NVDA+Ctrl+z 打开 NVDA Python 控制台,键入以下代码:

import ui
ui.message("Hello World!")

当你键入 ui.message("Hello World!") 这一行并按下回车的时候, NVDA 果然朗读了 "Hello World!",好了,这行代码可以被我们复制。

我们知道了怎么让 NVDA 朗读一段文本,那么行列信息怎么获取呢?举一反三一下。

已知这个信息在状态栏上,那么 NVDA是怎么获取状态栏信息的呢?我们知道有个朗读状态栏的快捷键,对于笔记本布局来说是 NVDA+Shift+End 一样,我们去 globalCommands.py 里再搜一下。

是不是很容易就找到了 script_reportStatusLine 这个方法,稍微读一下,实际上第一步是有效性检查,在执行后续逻辑之前,Check 一下有没有状态栏文本。

那么我们再读一下 _getStatusBarText 这个私有方法,很好,我们发现它是调用了 api.getStatusBar 进而通过 api.getStatusBarText 获得了状态栏的文本,似乎有了眉目,我们一样用 NVDA Python 控制台来看看这两个方法具体是怎么用的。

在控制台中键入以下代码:

import api
help(api.getStatusBar)

输出了以下内容:

>>> help(api.getStatusBar)
Help on function getStatusBar in module api:

getStatusBar() -> Optional[NVDAObjects.NVDAObject]
    Obtain the status bar for the current foreground object.
    @return: The status bar object or C{None} if no status bar was found.

getStatusBar 函数的功能是获取当前前台对象的状态栏对象。
返回值:状态栏对象或如果没有找到状态栏则返回 None 。状态栏对象的类型是 Optional[NVDAObjects.NVDAObject],也就是说,它可以是 NVDAObjects.NVDAObject 类型的对象,或者是 None

NVDA Python 控制台

想必你已经发现了,NVDA Python 控制台(下称“控制台”)是我们开发插件不可缺少的工具。这并不属于 Python 基础,它有哪些奇技淫巧我可能有必要在进行下一步之前跟你啰嗦一下。

实际上这个工具大多数用法跟普通的 Python 控制台并无二致,既然叫 NVDA Python 控制台自然有一些 NVDA 特定的用法。比如可以导入 NVDA 自有的 module,可以动态执行代码,对 NVDA Object 进行检查。

控制台支持的基本操作

  1. F6: 在输入区域和输出区域之间切换。
  2. 在输出区域:
  • Alt+上箭头:上一个输出块。
  • Alt+下箭头:下一个输出块。
  1. 在输入区域:
  • Tab:缩进和补全代码。
  • 上/下箭头:切换输入历史。
  • 回车执行代码/代码块。

控制台实践演练

NVDA Python 控制台对于新手也是非常友好的,它支持 NVDA 的标准语法,你不仅可以用来开发调试 NVDA 代码,你也可以把它当做一个 playground 用来熟悉 Python 语法,简直就是随身携带的 Python 执行环境。

下面我会给你几个好玩的任务让你彻底熟悉该控制台的用法。

蜂鸣音

在控制台执行以下代码:

import tones
tones.beep(640,500)

获取帮助

上面我们用简单的两行代码就可以播放出0.5秒的 beep 声音。

想知道 beep 这个函数的帮助信息吗?这当然难不倒聪明的你:

help(tones.beep)

跟上面一样, help 可以将 beep 的 docstring 打印出来。

如果想知道 tones 的更多信息呢? help(tones) 自然是可以的。还记得 python 的 dir 函数吗,不妨试一下 dir(tones)

想一下,假如你把 beep 写成了 beap 必然会出错,可是死活想不起 beep 是怎么拼的,怎么办呢?使用 dir 当然可以,但一般没人会这么做!

然而,别忘了控制台的自动补全功能,输入 tones.b 案两次 Tab 你会发现,控制台在一个菜单里列出了 tones 内,以 b 开头的匹配项,回车即可完成输入。

如果你把范围进一步缩小,先输入 tones.be(多输入个 e) 再案两次 Tab,你会发现 tones.beep 已经被自动补全了。

在控制台里输入代码块

题目:打印10以内(包含10)的所有偶数

要求:

  1. 10以内(包含10)指从1到10的所有整数。
  2. 偶数是指能够被2整除的整数。
  3. 输出每个偶数,每个数占一行。

在控制台执行以下代码:

for num in range(1, 11):
    if num % 2 == 0:
        print(num)

提示: 输入完 for num in range(1, 11):案回车后案 Tab 即可插入制表符增加一层缩进,输入完代码块的最后一行案两次回车即可执行代码块。

控制台的隐式导入

实际上上面用到的 api这个 Module 在控制台里已经被隐式导入了,这是为了方便开发者调试,我写出来只是为了提醒你,在后续写代码的时候千万不要忘记。

还有一些常用的 Module 或 NVDAObject 也已经在控制台里被隐士导入/定义了,如下:

  • api: 在NVDA 里非常常用的 Module。
  • focus:当前系统焦点
  • fg:前台对象,(foreground object)注意,这是打开控制台之前的前台对象。
  • nav: 当前导航对象(navigator object)。

举一反三

我已经把强大的控制台工具介绍给你了,你可以大胆探索一下,下面几个函数/技巧可能会帮到你。

  • type()
  • help
  • dir
  • module.__file__

举例:

  • type(fg)
  • fg.name
  • focus.name
  • focus.role
  • focus == nav
  • nav.devInfo

上面的内容似乎有一点神秘,在开始下一篇之前,我希望你已经进行了一些探索,相信我,动手探索会有收获的。

写在最后

工具、搜索、阅读,探索,英文和英文无用论,结语我只能想到这么几个关键词。

好啦,这一篇也就写到这里吧!

最后,我希望听到你的赞美和批评,请通过任意一种你喜欢的方式给我一些反馈。

标签: 开发实践, NVDA

已有 3 条评论

  1. bingling

    写的非常通俗易懂,身为一个小白表示已经看明白了。感谢大佬的教学,非常非常非常期待下一篇。

  2. 伶牙俐齿

    作者好,刚看完你的分享,觉得非常好。但在我这里的NVDA控制台中,你说的那些,比如fg、focus等都提示未找到,用import导入也不行,不知我这边是什么情况。

  3. Jiajun

    文档可查,经验无价。支持作者。

添加新评论