Writing Anki 2.0.x Add-ons の日本語訳。簡単なサンプルを使ったアドオン作成の概要、データベースアクセスの方法、フックを使った既存コードのカスタマイズ、デバッグ、アドオンの共有方法について説明しています。

日本語版をご覧いただく前に

この文書は、Writing Anki 2.0.x Add-ons 2017-08-26 版の日本語訳です。

Anki 2.0.x は 2017 年 8 月 27 日現在、最新バージョンは 2.0.47 です。より最新で正確な情報をご覧になりたい方は、原本をご覧ください。

また、Anki 2.1.x 用の文書の日本語訳もご覧頂けます。


Anki 2.0.x アドオンの作成

他のバージョン

この文書では、Anki 2.0.x 用のアドオンの作成について扱います。Anki 2.1.x Beta 用については https://apps.ankiweb.net/docs/addons21.html をご覧ください。

概要

Anki は、ユーザーフレンドリーなプログラミング言語 Python で作成しています。 Python についてあまり詳しくない方は、この文書の先を読む前に Python tutorial をお読みください。 (訳注: Python チュートリアル 日本語版)

Python は動的言語なため、Anki ではアドオンが極めて強力に機能します。アドオンは Anki の処理 を拡張するだけでなく、Anki の任意の側面に変更を加えることができます。例えば、スケジュール 設定の処理を変更したり、ユーザーインターフェイスを修正したりすることができます。

プラグインの開発には、特別な開発環境は必要ありません。テキストエディタがあれば十分です。 Windows や Mac をお使いの方は、このサイトで提供しているパッケージ版の Anki をお使いください。これらの プラットフォーム上でゼロから Anki を構築するための説明が入手できないためです。

メモ帳のような単純なテキストエディタでプラグインを作成できますが、シンタックスハイライト 機能 (コードの色分け) を持ったエディタを探してみると、作業がより簡単になります。

Anki を構成する2つの要素

'anki' には、「背後」で動作する全てのコードが含まれています。コレクションを開いたり、 カードを取得し、回答する処理などです。これは、Anki の GUI が使用していますが、GUI を使わず に Anki 単語帳にアクセスするコマンドラインプログラムに含めることもできます。

'aqt' には、Anki のユーザーインターフェイスの部分が含まれています。Anki のユーザー インターフェイスは、PyQt 上に構築されています。PyQt とは、クロスプラットフォーム GUI ツールキット Qt に対する Python バインディングです。PyQt は、Qt の API に密接に動作 します。Qt documentation は、特定の GUI コンポーネントの使い方を調べたい時に、非常に役立ちます。

  • Anki 2.0.x は、Qt 4.8 を使っています。

  • Anki 2.1.x は、Qt 5.9 を使っています。

Anki が起動すると、Anki は、Documents/Anki/addons フォルダの中にある .py ファイルを 探し、見つけたら一つずつ実行します。アドオンが実行されると、通常は既存のコードを修正したり、 新しい機能を提供する新しいメニュー項目を増やしたりします。

簡単なアドオンの一例

次に示す test.py ファイルを、自分のアドオンフォルダーに追加してみてください:

# aqt からメインウィンドウオブジェクト (mw) を読み込みます
from aqt import mw
# utils.py から "show info" ツールを読み込みます
from aqt.utils import showInfo
# Qt GUI ライブラリの全てを読み込みます
from aqt.qt import *

# 次のようなメニュー項目を追加してみましょう。まず最初にメニュー項目が利用可能になったら
# 呼び出す関数を作成します。

def testFunction():
    # 現在使用中のコレクションの中のカードの枚数を取得します
    # このコレクションはメインウィンドウの中に保存しています
    cardCount = mw.col.cardCount()
    # メッセージボックスを表示します
    showInfo("Card count: %d" % cardCount)

# 新しいメニュー項目 "test" を作成します。
action = QAction("test", mw)
# この項目をクリックしたら testFunction を呼び出すように設定します。
action.triggered.connect(testFunction)
# そして、この設定をツールメニューに反映します。
mw.form.menuTools.addAction(action)

Anki を再起動すると、ツールメニューの中に 'test' 項目が追加されていることに気づくでしょう。 この項目を選択して実行するとカード枚数を表示するダイアログが現れます。

プラグインの入力中に間違いがあった場合には、Anki は起動時にエラーメッセージを表示して どこに問題があるか指摘します。

コレクション

コレクションファイル上の全ての操作は、mw.col を通じてアクセスします。基本的な例で 何ができるがご紹介します。注意してほしいのは、上の例のように testFunction() の中で行ってください。 アドオンの中で直接実行することはできません。それは、Anki を起動中にアドオンが初期化し、その後にコレクションやプロファイルを 読み込むからです。

復習時期のカードの取得:

card = mw.col.sched.getCard()
if not card:
    # 現在の単語帳は復習済み

カードを解答する:

mw.col.sched.answerCard(card, ease)

ノートを編集する (各フィールドの最後に " new" を追加):

note = card.note()
for (name, value) in note.items():
    note[name] = value + " new"
note.flush()

ノートにタグ x を持つカードの ID を取得する:

ids = mw.col.findCards("tag:x")

指定したカード ID から質問と解答を取得する:

for id in ids:
    card = mw.col.getCard(id)
    question = card.q()
    answer = card.a()

データベースの変更後にスケジュールをリセットする。GUI も更新しなければならないので、 メインウィンドウ上で reset() を呼び出すことに注意してください:

mw.reset()

テキストファイルをコレクションに読み込む

from anki.importing import TextImporter
file = u"/path/to/text.txt"
# 単語帳を選択
did = mw.col.decks.id("ImportDeck")
mw.col.decks.select(did)
# 単語帳にノートタイプを設定
m = mw.col.models.byName("Basic")
deck = mw.col.decks.get(did)
deck['mid'] = m['id']
mw.col.decks.save(deck)
# コレクションに読み込む
ti = TextImporter(mw.col, file)
ti.initMapping()
ti.run()

ほとんど全ての GUI 処理は 'anki' 内に関連する関数を持っています。このため、Anki が利用 できるどんな処理でも、アドオンの中で同様に呼び出すことができます。

GUI の外側のコレクションにアクセスする場合は、次のようなコードを使います:

from anki import Collection
col = Collection("/path/to/collection.anki2")

Anki の外部のコレクションに何らかの修正を加えたときは、修正が済んだら col.close() を必ず呼び出さなければなりません。 これを怠ると修正点は失われます。

データベース

'anki' がサポートしていない処理を実行する必要がある場合は、データベースに直接アクセスする ことができます。Anki コレクションは、SQLite ファイル内に保存されています。詳しい情報は、 SQLite documentationをご覧ください。

Anki のデータベースオブジェクトは次のような関数をサポートしています:

execute() は、挿入と更新処理を実行します。指定した引数は ? を一緒に使います。例えば:

mw.col.db.execute("update cards set ivl = ? where id = ?", newIvl, cardId)

executemany() は、更新と挿入を一括処理します。大規模な更新にはこの関数の方が、 execute() で個別にデータを処理するよりも非常に高速に処理します。例えば:

data = [[newIvl1, cardId1], [newIvl2, cardId2]]
mw.col.db.executemany(same_sql_as_above, data)

scalar() は、単一の項目を返します:

showInfo("card count: %d" % mw.col.db.scalar("select count() from cards"))

list() は、各行の最初の列をリストで返します。次のコードの戻り値は [1, 2, 3]です:

ids = mw.col.db.list("select id from cards limit 3")

all() は、各行がリストの場合、行のリストを返します:

ids_and_ivl = mw.col.db.all("select id, ivl from cards")

execute() は、中間リストを作らずに結果の集合への処理を繰り返すのに使えます。例:

for id, ivl in mw.col.db.execute("select id, ivl from cards limit 3"):
    showInfo("card id %d has ivl %d" % (id, ivl))

アドオンが、コレクションの中のテーブルを修正することが決してないように注意してください。 このことは、Anki 将来のバージョンで変更になる場合があります。プラグイン専用のデータを保存する 必要がある時には、衝突を避けて新しいテーブルを作るか、別のファイルにデータを保存するようにして ください。小さい設定項目は、mw.col.conf の中に保存できますが、同期の度にコピーするため、 大規模なデータを保存しないでください。

フック

フックをコードのわずかな箇所に追加して、アドオンの作成がもっと簡単になるようにしました。 フックは 2 種類あります。'hooks' は引数を取り、戻り値はありませんが、'filters' 引数を取り、 (おそらく何らかの修正を加えて) 値を返します。

'hook' の簡単な例は、無駄なカード (leech) の処理の中に見つかります。スケジューラー (anki/sched.py) が、無駄なカードを見つけると、'hook' を呼び出します。

runHook("leech", card)

無駄なカードが現れた時に、特定の処理を行いたい場合、例えばそのカードを "Difficult" という名前の単語帳に移動する場合、次のようなコードで実現できます。

from anki.hooks import addHook
from aqt import mw

def onLeech(card):
    # スケジューラーが修正する際には、 .flush() を使わずに修正できます。
    card.did = mw.col.decks.id("Difficult")
    # カードがフィルター単語帳の中にある場合は、復習時期を元に戻して取得元の単語帳に
    # 戻さなければなりません
    card.odid = 0
    if card.odue:
        card.due = card.odue
        card.odue = 0

addHook("leech", onLeech)

aqt/editor.py の中に 'filter' の例があります。エディターは、入力欄からフォーカスが外れる と "editFocusLost" filter を呼び出します。そして、アドオンはノートに変更を加えます。

if runFilter(
    "editFocusLost", False, self.note, self.currentField):
    # ノートを更新して、スケジュールを再度読み込む
    def onUpdate():
        self.loadNote()
        self.checkValid()
    self.mw.progress.timer(100, onUpdate, False)

このサンプルでは、それぞれの filter は 3 つの引数を受け取ります。修正フラグ、ノート、現在のフィールドです。 filter が変更を加えない場合は、修正フラグは受け取った値と同じ値を返します。 変更を加えた場合は、True を返します。このようにして、どんなアドオンでも変更を加えると ユーザーインターフェイスは、ノートを読み込み直して、更新内容を表示します。

Japanese Support アドオンは、このフックを使って別のフィールドからフィールドを自動的に生成します。 単純化したものを次に示します。

def onFocusLost(flag, n, fidx):
    from aqt import mw
    # japanese model か?
    if "japanese" not in n.model()['name'].lower():
        return flag
    # src フィールドと dst フィールドがあるか?
    for c, name in enumerate(mw.col.models.fieldNames(n.model())):
        for f in srcFields:
            if name == f:
                src = f
                srcIdx = c
        for f in dstFields:
            if name == f:
                dst = f
    if not src or not dst:
        return flag
    # dst フィールドは入力済みか?
    if n[dst]:
        return flag
    # イベントは src フィールドで発生したか?
    if fidx != srcIdx:
        return flag
    # ソーステキストを取得
    srcTxt = mw.col.media.strip(n[src])
    if not srcTxt:
        return flag
    # 欄を更新
    try:
        n[dst] = mecab.reading(srcTxt)
    except Exception, e:
        mecab = None
        raise
    return True

addHook('editFocusLost', onFocusLost)

filter の第一引数は、必ず返される引数です。このフォーカスを失った時の filter の中では、 引数はフラグですが、別のオブジェクトになる場合もあります。例えば、anki/collection.py の中では、_renderQA() は、カードの表面と裏面用に生成した HTML を収容する "mungeQA" filter を呼び出します。latex.py は、この filter を LaTeX タグの中のテキストを画像に変換する のに使っています。

Anki 2.1 では、エディタにボタンを追加するフックを追加しました。次のように使います。

from aqt.utils import showInfo
from anki.hooks import addHook

# cross out the currently selected text
def onStrike(editor):
    editor.web.eval("wrap('<del>', '</del>');")

def addMyButton(buttons, editor):
    editor._links['strike'] = onStrike
    return buttons + [editor._addButton(
        "iconname", # "/full/path/to/icon.png",
        "strike", # link name
        "tooltip")]

addHook("setupEditorButtons", addMyButton)

モンキーパッチとメソッドの隠蔽

フックを持っていない関数を修正したい場合には、カスタム版の関数で上書きすることが可能です。 このことを、「モンキーパッチ」を呼ぶことがあります

aqt/editor.py には、setupButtons() という関数があり、エディターの中にある太字ボタン、 斜字体ボタンのようなボタンを生成します。自分のアドオンに違ったボタンを追加することを考えて みましょう。

一番簡単な方法は、Anki のソースコードからその関数をコピーペーストして、自分のテキストを ボタンに追加します。そして、元の関数を上書きします。次の通りです。

from aqt.editor import Editor

def mySetupButtons(self):
    <オリジナルからコピーペーストしたコード>
    <カスタムアドオンのコード>

Editor.setupButtons = mySetupButtons

この方法は、将来の Anki のバージョンで元のコードが更新されるような場合に、自分のアドオンも 更新する必要になる問題をはらんでいます。もっと良い方法は、オリジナルの関数を保存しておいて 自分のカスタムバージョンの中で呼び出すことです。

from aqt.editor import Editor

def mySetupButtons(self):
    origSetupButtons(self)
    <カスタムアドオンのコード>

origSetupButtons = Editor.setupButtons
Editor.setupButtons = mySetupButtons

これはよく行われる処理なので、Anki では wrap() という関数を提供して、もう少し使いやすく しています。実際の例をご紹介します。

from anki.hooks import wrap
from aqt.editor import Editor
from aqt.utils import showInfo

def buttonPressed(self):
    showInfo("pressed " + `self`)

def mySetupButtons(self):
    # - size=False は、小さいボタンは使わない
    # - lambda は、予め設定されているメソッドの代わりに関数の中で
    #    エディタインスタンスをコールバックに渡す時に必要
    self._addButton("mybutton", lambda s=self: buttonPressed(self),
                    text="PressMe", size=False)

Editor.setupButtons = wrap(Editor.setupButtons, mySetupButtons)

既定では、wrap() は元のコードの後にカスタムコードを実行します。第3引数 "before" を渡すと これを逆転できます。元のバージョンの前と後の両方で実行する必要がある場合は、次のようにします。

from anki.hooks import wrap
from aqt.editor import Editor

def mySetupButtons(self, _old):
    <オリジナルの前で実行するコード>
    ret = _old(self)
    <オリジナルの後で実行するコード>
    return ret

Editor.setupButtons = wrap(Editor.setupButtons, mySetupButtons, "around")

関数の前後でコードを実行するのではなく、関数の中を修正する必要がある場合には、元のコードの 中の対象とする関数にフックを追加するのが良い方法かも知れません。このような場合には、 追加するフックについての質問をフォーラムに投稿してください。

Qt

概要で話したとおり、Qt documentation は 色々な GUI ウィジェットを表示する方法を学ぶのに非常に貴重な文書です。

一つ覚えておいてほしいことは、Python ではオブジェクトはガベージコレクションされます。 次のように記述するとどうなるでしょうか。

def myfunc():
    widget = QWidget()
    widget.show()

すると、この関数を終了するとすぐにウェジットは消えてしまいます。これを避けるには、 トップレベルのウェジットに既存のオブジェクトを割り当てます。次の通りです。

def myfunc():
    mw.myWidget = widget = QWidget()
    widget.show()

Qt オブジェクトを作って、既存のオブジェクトを親とするときには、このことはあまり必要としません。それは、親オブジェクトが新規オブジェクトを参照し続けるからです。

標準モジュール

Anki は、このプログラムの実行に必要な標準モジュールだけを含めて提供しています。Python の完全な複製を含んではいません。このために、Anki が含んでいない標準モジュールを使う必要が ある場合には、自分のアドオンに同梱する必要があります。

デバッグ

自分のコードから例外が発生した時には、Anki の標準例外ハンドラー (標準エラー出力に書き出さ れるものは何でも) が補足します。デバッグ目的のために、情報を出力する必要がある場合は、 aqt.utils.showInfo を使うか、sys.stderr.write("text\n") で標準エラー出力に書き出す 必要があります。

Anki には、REPL が含まれています。プログラムの中から shortcut key を押すと ウィンドウが立ち上がります。上の欄に式や文を入力し、ctrl+return/command+return を押すと 評価します。セッション例を次に挙げます。

>>> mw
<no output>

>>> print mw
<aqt.main.AnkiQt object at 0x10c0ddc20>

>>> invalidName
Traceback (most recent call last):
  File "/Users/dae/Lib/anki/qt/aqt/main.py", line 933, in onDebugRet
    exec text
  File "<string>", line 1, in <module>
NameError: name 'invalidName' is not defined

>>> a = [a for a in dir(mw.form) if a.startswith("action")]
... print(a)
... print()
... pp(a)
['actionAbout', 'actionCheckMediaDatabase', ...]

['actionAbout',
 'actionCheckMediaDatabase',
 'actionDocumentation',
 'actionDonate',
 ...]

>>> pp(mw.reviewer.card)
<anki.cards.Card object at 0x112181150>

>>> pp(card()) # mw.reviewer.card.__dict__ へのショートカット
{'_note': <anki.notes.Note object at 0x11221da90>,
 '_qa': [...]
 'col': <anki.collection._Collection object at 0x1122415d0>,
 'data': u'',
 'did': 1,
 'due': -1,
 'factor': 2350,
 'flags': 0,
 'id': 1307820012852L,
 [...]
}

>>> pp(bcard()) # ブラウザで選択したカードへのショートカット
<as above>

何が評価されたか知るためには、式を明示的に出力する必要があることに注意してください。Anki では pp() (pretty print) がスコープの中でオブジェクトの詳細を素早くダンプすることが簡単に できるようになっています。ショートカット ctrl+shift+return は上の欄中の現在のテキストを pp() で囲んで実行し結果を表示します。

Linux を使っているかソースコードから Anki を実行している場合は、自分のスクリプトを pdb を 使ってデバッグすることも可能です。次の行を自分のコードのどこかに置けば、Anki がその場所に 達するとターミナルにデバッガーが立ち上がります。

from aqt.qt import debug; debug()

別の方法としては、export DEBUG=1 と自分のシェルで実行すれば、補足していない例外個所で デバッガーが立ち上がります。

もっと詳しく学びたい場合には

anki と aqt の両方が http://github.com/dae/ で入手できます。コレクション オブジェクトは、anki の collection.py の中で定義されています。他に調べる価値のある ファイルは、cards.py、notes.py、sched.py、models.py や decks.py です。

aqt のソースコード見ることも、特定の処理のための anki の呼び出し方や GUI の詳細 を理解するのに役立ちます。

多くの GUI は、designer ファイルの中で定義されてます。Qt Designer というプログラムを 使えば .ui ファイルを開いて、GUI をブラウズすることが簡単にできます。

最後になりますが、他のアドオンが何かを実現している方法を見ることも、非常に役立ちます。

Anki 1.2 プラグインからの移植

注意すべき主な変更点:

  • テーブルの変更: facts→notes、reviewHistory→revlog

  • フィールドは、現在 notes テーブルに 'flds' という単体のテキストフィールドに保存している。 各フィールドは \x1f で区切られている。

  • cardTags テーブルを廃止しました。以前と同様の方法で検索するには col.findCards("tag:x note:y card:z") をお使いください。

  • スケジュールのコードは全て sched.py にあります。単語帳のコードは collection.py です。

  • notes テーブルを一括更新する場合は、findReplace() を使わないでください。 必ず col.updateFieldCache() を呼び出してください。

  • Q/A キャッシュを廃止しました。このため質問か解答を生成していないカードの中を テキスト検索することはできません。

  • 変更の前にコレクションを保存するには、古いアンドゥ (元に戻す) システムの代わりに mw.checkpoint("Undo Name") を呼び出してください。ユーザーが操作をやり直す場合、 保存済みの状態に戻ります。

  • 変更の同期を確実にするには、ノートやカードをデータベース内で修正した場合に、mod の更新と usn が col.usn() に設定を必ず行ってください。

  • 同様に、モデルや単語帳を修正した場合は、適切なマネージャで必ず save() を呼んでください。

  • タイマーを設定する場合は、mw.progress.timer() を使って、データベース処理の最中に タイマーが起動することが決して起らないようにしてください。

  • stats テーブルを廃止しました。同期中のマージはできなくなりました。統計は revlog テーブルから引き出す必要があります。

アドオンの共有

単純な一つのファイルからなるアドオンは、その .py をアップロードできます。複数のファイルの アドオンは、Python パッケージとして動作するようにサブホルダーを作って、パッケージを読み込む 小さな .py ファイルを作ってください。Japanese support アドオンを使って説明しますと 次のような構造になります。

japanese/file1.py
japanese/file2.py
japanese/__init__.py # 空も可能。このフォルダーがパッケージであることを示す
japanese/<バイナリーのサポートファイル>
jp.py

複数ファイルのアドオンをアップロードするには、フォルダーとローダー .py ファイルを zip ファイルにして、その zip ファイルをアップロードしてください。

https://ankiweb.net/shared/addons/ にアドオンをアップロードしてください。


日本語版訳注

Anki 2 の変更点を知るには、Anki 2の変更点 が役立ちます。

更に、アプリケーションの個々の機能や処理を詳しく理解するには Anki User Manual をご覧ください。

Anki 2.0.47 現在、Anki に同梱している Python のバージョンは 2.7.6 です。

アドオン開発の簡単なチュートリアルとして フックを使った Anki アドオンのつくり方を用意しました。この記事を補足して独自フックの追加してアドオンを作成する方法や、作成したアドオンを AnkiWeb のアドオン一覧へ登録する方法について説明しています。

日本語版更新履歴

  • 2012/09/16 Anki 2 Release Candidate 4 準拠 (2012/09/08版) 初出

  • 2013/02/03 Anki 2.0.5 準拠 (2012/12/20版)

  • 2013/05/07 Anki 2.0.8 準拠 (2013/05/07版)

  • 2013/06/07 Anki 2.0.8 準拠 (2013/05/13版)

  • 2014/02/15 Anki 2.0.22 準拠 (2014/02/14版)

  • 2016/05/01 Anki 2.0.33 準拠 (2016/03/04版)

  • 2017/07/28 Anki 2.1 Beta 3 準拠 (2017/07/14版)

  • 2017/08/15 Anki 2.1 Beta 11 準拠 (2017/08/15版)

  • 2017/08/27 Anki 2.0.47 準拠 (2017/08/26版)