Skip to content

Writing Gramplets.zh_CN

Doug Blank edited this page Oct 19, 2026 · 1 revision

🌐 English · Deutsch · Español · Français · Nederlands

编写 Gramplet

如何编写您自己的 Gramplet 并将其发布到 Gramplet 商店。只有家谱所有者或站点管理员才能创建或编辑 Gramplet(见谁可以使用 Gramplet)。

入门

Gramplet 的代码运行在一个沙箱中,其中已经提供了一组函数和对象,因此访问家谱无需任何 import。

仓库中的 gramplet_examples/ 目录是一套带编号的 API 教程,从最简单到最高级。应用内也提供同样的参考:Gramplet 编辑器中的 (i)“Writing a Gramplet”帮助按钮。

最小的实用 Gramplet

matches = people(
    "gender == Person.MALE and birth.date.sortval >= Date('Jan 1, 1900')",
    limit=25,
)

columns("Person")
for person in matches:
    row(person)

获取记录

people(where=None, what=None, order=None, limit=50) 一次调用就能以完整记录的形式获取匹配查询的人物。使用 what=["gramps_id", "primary_name"] 则只获取这些字段。

其他每种记录类型都有对应的 families() / events() / places() / repositories() / sources() / citations() / media() / notes() / tags(),签名相同。

编写查询

where= 用 GOQL 编写,语法与任何列表的搜索框相同。它可以跨越关系(birth.date.sortval),即使您筛选的字段并不是您要显示的字段。

虽然底层是真实的网络调用,但无需 await,它会被自动插入。

显示结果

  • row(*values) 向表格添加一行。传入整个 Person/Event/Place/... 对象(而不是它的某个字段),会将其渲染为可点击的链接,显示其全名或标题以及 Gramps ID。点击会打开一个弹窗,在列表、地图、关系图或时间线中查看该记录。
  • name_of(person) 以文本形式返回一个人的姓名,先名后姓(“Root Ancestor”),与 row() 的标注方式一致,适合用于标题或句子。它也接受此人的某个名字,例如 name_of(person.alternate_names[0])。
  • row()、html() 和 print() 都可以按任意顺序调用任意多次。所有内容按您添加的顺序显示。

使用记录

people()、families() 等返回的每条记录都是一个 Gramps“DataDict”:一个包含记录字段的普通 dict,并额外支持点号访问。person.primary_name.first_name 与 person["primary_name"]["first_name"] 是同一个值。当您想根据某个字段计算内容,或显示 row() 不显示的字段时,就用它。

打印记录

打印记录从不出错。print(person) 或 f"{person}" 会按 row() 的标注方式显示整条记录(“Root Ancestor [I0001]”),日期显示为日期,其他内容(filter() 的结果、person.primary_name)显示为其普通字段。

不下载家谱即可计数

total_people = db.get_number_of_people()
women = db.get_number_of("person", where="gender == Person.FEMALE")
no_birth_date = db.get_number_of("person", where="birth.date.sortval is None")

db.get_number_of_<type>() 是对整棵家谱的低成本计数。总数通过响应头返回,因此不会下载任何记录。db.get_number_of(object_type, where=...) 则是带筛选条件的同类计数。

跨越关系

在查询中

where= 条件可以跨越关系进行筛选:人物的出生事件、家庭的父亲、事件的地点。

born_and_died_same_place = people(
    "birth.place.title == death.place.title and birth.place.title is not None",
    order=[{"column": "surname", "direction": "asc"}],
    limit=50,
)

用于显示

people()、families() 等总是返回记录本身,无法直接请求关联记录的字段。要获取关联记录用于显示,需要通过 db 自己的方法另行查找,例如 db.get_event_from_handle(...),沿用 Gramps 自身数据模型所用的 handle。

集合

any(c.given_name == 'Steve' for c in children) 在任一孩子满足条件时匹配该家庭,len([c for c in children if c.gender == Person.MALE]) > 1 则只统计儿子。更多内容见 GOQL,包括当前的限制:只能深入集合一层,不能在其内容之后继续链式访问。

响应选中的记录

record = get_selected()
if record is not None:
    if isinstance(record, Person):
        ...

get_selected() 返回视图详情面板中打开的记录。没有选中任何内容,或从没有视图的编辑器预览中运行时,它为 None。它在一次运行中首次调用时发起一次网络请求,之后在该次运行的剩余时间里记住结果。

isinstance(record, Person)(等)告诉您拿到的是哪种记录,这对附加到“全部”视图的 Gramplet 很重要。

选择变化时更新

要让 Gramplet 在您点选列表时实时更新,请在其编辑器中开启“Re-run automatically when the selected record changes”。关闭时(默认),get_selected() 仍然可用,但只反映 Gramplet 上次运行时选中的内容。

响应当前筛选器

Gramplet 可以在视图当前应用的筛选器之上叠加自己的查询,无论该筛选器是在搜索框中输入的,还是在“筛选器”中选择的(已保存的筛选器或自定义规则)。把列表筛选到家谱的某一支后,Gramplet 的输出也会以同样方式缩小范围:

rows = filter(
    "person",
    where=and_filters(get_filter(), "birth.date.sortval is not None and death.date.sortval is not None"),
)

这需要两项清单设置:

  • views: [...],因为 get_filter() 只返回与视图自身记录类型匹配的 where_expr;
  • listensToFilter: true,以便筛选器变化时 Gramplet 重新运行。

图表

pygal、matplotlib 和 plotly 均已内置,因此普通的 import(对 plotly 则是 from plotly.subplots import make_subplots)可以离线使用,无需 %pip install。

对 plotly 的 Figure、pygal 生成的 SVG 字符串或 matplotlib 图形执行 print(fig),会被自动识别并绘制为图表,无需手动 to_html() 或嵌入。

对于图表数据,如果只需要一两个字段,请传入 what=[...]。单独调用 people() 会下载每条记录的每个字段,对于只绘制(比如)去世年龄的图表来说很浪费。

并排布局

col1, col2 = st.columns([2, 1])
with col1:
    row("Wider column")
with col2:
    row("Narrower column")

st.columns(spec) 把区域并排排列:传入整数表示该数量的等宽列,传入权重列表(st.columns([2, 1]))则按比例分配宽度。它为每一列返回一个区域。

在 with col: 内写入的任何内容(row()、html()、print() 或其他 st.* 调用,包括嵌套的 st.columns())都会进入该列。col.write(x) 无需 with 块即可达到同样效果。

在独立窗口中运行的 Gramplet

Gramplets → 新建 Gramplet… 会打开为这种类型准备好的 Gramplet 编辑器。对于已有的 Gramplet,把 类型 设为 Gramplet — opens in a window from the Gramplets menu。

视图 Gramplet 的设置位置上会改为显示一个 类别 字段,用于在 Gramplets 菜单中将其与其他 Gramplet 归为一组(例如 chart)。留空则归入 Other。

请求输入

窗口一打开代码就会运行。要先请求输入,请用 st.* 控件绘制输入项,把实际工作放在 st.button 之后。这些控件遵循 Streamlit 的名称和参数,在视图 Gramplet 中用法相同:

  • st.button、st.text_input、st.number_input、st.checkbox、st.selectbox、st.radio
  • st.columns、st.progress
  • 消息框 st.info / st.success / st.warning / st.error
  • st.session_state

显示进度

对于长时间运行的 Gramplet,可以像 Streamlit 一样用 st.progress(value, text) 显示进度条。value 为 0–100(或 0.0–1.0)。返回的进度条的 .progress(...) 会原地更新它,.empty() 会移除它,开销低到每行调用一次也没问题。

people(..., limit=None)(以及其他列表函数)会返回所有匹配项,而不是前 50 个:

surname = st.text_input("Surname", "Smith")
if st.button("Run"):
    everyone = people(f"primary_name.surname_list[0].surname == '{surname}'", limit=None)
    bar = st.progress(0, text="Checking people...")
    for i, person in enumerate(everyone):
        bar.progress((i + 1) / len(everyone))
        ...
    bar.empty()

长时间运行

Gramplet 可以运行任意长的时间,打开了几个小时的页面也仍能运行它们。Gramplet 发出的每个请求都使用您当前的登录状态,并像应用的其他部分一样在后台续期。只有当登录本身已结束时,运行才会停止,并显示 Your session has expired. Sign in again, then rerun this Gramplet.

安装第三方包

%pip install unidecode
from unidecode import unidecode

%pip install 是 Jupyter 风格的语法,在代码运行前会被改写为真正的 await micropip.install([...]) 调用。它必须单独占一行并位于顶部,就像在 notebook 中一样;例如写在 if 里面就不起作用。

这只适用于 纯 Python 包,即不含编译代码或 C 扩展的包。PyPI 上大多数小型纯 Python 工具都可以这样顺利安装。

从代码中修改数据

在 Gramplet 编辑器中开启 允许修改数据 后,Gramplet(视图 Gramplet 永远不修改数据)就可以修改和删除记录。其 API 与 Gramps 桌面版上 Gram.py Script 使用的相同:像修改任何 Python 对象一样修改记录。

for person in people("len(citation_list) == 0", limit=None):
    person.private = True

商店中的示例

  • Mark Unsourced People Private 先用 filter() 低成本地找出人物,然后只获取并修改需要修改的那些,全部放在一个命名的 begin_changes() 组中。
  • Tag People 演示了向列表添加内容(person.tag_list.append(tag.handle);记录以 handle 而非名称存储标签),并使用一个必须先做出选择才会修改任何内容的 st.selectbox()。

修改、删除与分组

  • record.field = value 修改一个字段,嵌套字段也可以(person.primary_name.first_name = "Chris")。列表操作(.append()、.pop() 等)和任何 set_*() 方法也一样,例如 person.set_privacy(True)。
  • delete(record) 删除一条记录。它不会移除其他记录对它的引用。
  • begin_changes("message") / end_changes() 把两者之间的修改归入以该消息命名的组。它们是可选的:不使用时,整次运行是一个组,以 Gramplet 的名称命名。

哪些记录可以修改

来自 people() 及其他复数形式函数、get_raw_object()、get_object()、get_selected() 和 db.* 的记录都可以修改。filter() 的结果不行,因为它们只包含记录的部分字段。

在一次运行中两次请求同一条记录,会得到同一个对象,其中包含您的修改。

开发时的试运行

在 Gramplet 编辑器中,每次运行都是试运行。每条被修改的记录都会先按其 Gramps 模式进行校验,与服务器所做的校验相同;无效的值会使运行停止并指明是哪个字段。

试运行与真实运行有一点可能不同:搜索(filter()、count()、where=)在服务器上执行,因此它们看到的仍是本次运行修改之前的家谱。

修改何时保存

在真实运行中,运行 会在代码关闭每个组时保存该组,即在 end_changes() 处或运行结束时。用户看到的内容(包括撤销)见修改数据。

输出中的控件(st.button("Apply") 等)会以当前结果所来自的模式重新运行代码。因此,Gramplet 可以在试运行中展示它将修改的内容,并在“运行”之后应用这些修改。

发布到商店

文件夹结构

在 gramplet-store/ 中,每个 Gramplet 一个文件夹,以其 id 命名。文件夹名称必须与清单自身的 id 字段一致。

gramplet-store/
  <slug>/
    manifest.json   # required
    code.py         # required -- the Gramplet's Python source
    icon.png        # optional (png/jpg/jpeg/svg/webp)

manifest.json

字段 必需 含义
id 是 必须与文件夹名一致。
name 是 作为该条目在商店中的标题显示。
description 是 显示在商店中名称的下方。
version 是 普通的 semver,例如 "1.0.0"——每当 code.py 或清单发生变化时都要递增,以便已安装的副本能检测到有可用更新。
author 是 在商店中显示。
category 是 一个简短标签("example"、"detail"、"utility"、"chart"……),用于对条目分组/筛选。
views 否 该 Gramplet 可以添加到哪些对象类型视图("person"、"family"……)。省略表示“所有类型”。
listensToSelection 否 所选记录变化时,该 Gramplet 是否应重新运行。
listensToFilter 否 当前筛选器(搜索框或筛选器选择器)变化时,该 Gramplet 是否应重新运行。
kind 否 "view"(默认)表示视图 Gramplet;"window" 表示从 Gramplets 菜单运行的 Gramplet。views/listensTo* 仅适用于视图 Gramplet。
allowChanges 否 设为 true 则在安装时开启允许修改数据(商店会显示一个会修改您的数据徽章)。仅允许与 "kind": "window" 一起使用。

重新生成目录

添加或编辑条目后,重新生成 catalog.json,这是应用唯一获取的文件:

npm --prefix app run build:gramplet-catalog

它会校验每个条目(必填字段齐全、id 与文件夹一致、version 符合 semver、code.py 非空、kind 有效),并在遇到第一个问题时明确报错,而不是发布一个损坏的目录。

Clone this wiki locally