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

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

この文書は、Writing Anki 2.1.x Add-ons 2017-09-10 版の日本語訳です。

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

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


Anki 2.1.x アドオンの作成

他のバージョン

この文書では、まだリリースしていませんが Anki 2.1.x 用のアドオンの作成について扱います。Anki 2.0.x 用については https://apps.ankiweb.net/docs/addons.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.1.x は Qt 5.9 を使用しています。

Anki が起動すると、アドオンフォルダ内のモジュールを確認し、見つけたモジュールを一つづつ実行します。アドオンを実行すると、通常は既存のコードを変更したり、新しい機能を呼び出すメニュー項目を新たに追加します。

アドオンフォルダ

Anki のメインウィンドウのメニューから Tools>Add-ons と選ぶと、アドオンフォルダの最上位階層にアクセスできます。View Files ボタンを押すとフォルダがポップアップします。アドオンをまだ何もインストールしていない場合は、最上位のアドオンフォルダが開きます。アドオンを選択している場合は、アドオンのモジュールフォルダが開きます。これは最上位階層の一つ下の階層になります。

アドオンフォルダの名前は、"addons21" です。Anki 2.1 に対応しています。"addons" があるのは、以前 Anki 2.0.x を使っていたためです。

それぞれのアドオンは、アドオンフォルダの中の一つのフォルダを使います。Anki はそのフォルダの中にあるファイル __init__.py を探します。

addons21/my_addon/__init__.py

もし __init__.py がなければ、Anki はそのフォルダを無視します。

フォルダ名を決めるときには、a-z と 0-9 の範囲の文字から選ぶことおすすめします。これによって、Python モジュールシステムによる問題を避けることができます。

自分でフォルダを作るときはどんな名前でも使えますが、AnkiWeb からアドオンをダウンロードするときは、Anki はそのアドオンの ID をフォルダ名に使います。例えば次の通りです。

addons21/48927303923/__init__.py

Anki はさらにフォルダに meta.json ファイルを保存して、ダウンロードした時の元のアドオン名と、アドオンの利用許可を追跡ます。

ユーザーデータをアドオンフォルダに保存できません。そのようなデータは、ユーザーがアドオンをアップグレードすると、削除されるからです。

簡単なアドオンの一例

次に示す my_first_addon/__init__.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 を破壊するかもしれないからです。

アドオン独自のデータが必要な場合は、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 2.1 は、setupButtons() をもう使用していません。このコードは、モンキーパッチがどのように動作しているか、理解するのに役立ちますが、エディタにボタンと追加するには、前の項目で説明した setupEditorButtons フックを見てください。

一番簡単な方法は、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 が含んでいない標準モジュールを使う必要が ある場合には、自分のアドオンに同梱する必要があります。

この場合、pure Python モジュールは使えますが、numpy のような C 拡張を必要とするモジュールを同梱するのは非常に困難です。その理由は、Anki がサポートするオペレーティングシステム用にそれぞれコンパイルする必要があるからです。もし込み入ったことをするのであれば、代わりにユーザーに Python のスタンドアロンファイルをインストールしてもらう方が簡単です。

設定

JSON dictionary で設定を書いた config.json ファイルを入れると、ユーザーは Anki のアドオンマネージャから編集できるようになります。

簡単な例として、config.json に次のように記述します。

{"myvar": 5}

config.md は次のように記述します。

この文書はこのアドオンの設定用で、*markdown* フォーマットで書いています。

アドオンのコードには次のように記述します。

from aqt import mw
config = mw.addonManager.getConfig(__name__)
print("var is", config['myvar'])

アドオンを更新する時には、config.json を変更することができます。既存の設定と新規追加のキーを統合します。

config.json の中の既存のキーの値を変更する場合は、設定をカスタマイズしたユーザーが、"restore defaults" ボタンを押さない限り、古い値を使い続けることになります。

設定をプログラムで変更する必要がある場合は、次のように変更を保存します。

mw.addonManager.writeConfig(__name__, config)

注意: config.json が存在しない場合は、getConfig() は None を返します。たとえ、writeConfig() を呼んでいたとしてもです。

独自の GUI に管理オプションを持っているアドオンは、config ボタンを押した時にその GUI を表示できます。

mw.addonManager.setConfigAction(__name__, myOptionsFunc)

キー名の最初にアンダースコアを使うのを避けてください。Anki が将来利用するために予約しています。

ユーザーファイル

アドオンの設定に簡単なキーと値の組み合わせ以外のデータが必要な時は、"user_files" という名前の特別なフォルダをアドオンフォルダのルートに置いて使うことができます。このフォルダに置いたファイルはどれも、アドオンの更新時に保護します。アドオンフォルダのこれ以外のファイルは全て更新時に削除します。

ユーザー用の "user_files" フォルダを必ず確実に作るには、アドオンを zip ファイルにする前に README.txt や 同じようなファイルをその中に置くことで可能です。

Anki がアドオンを更新する時は、zip ファイルの中で "user_files" にすでに存在するファイルはどれも無視します。

質問解答画面での JavaScript

(2.1.0beta16 で導入予定)

Anki は、復習画面やプレビューダイアログ、カードレイアウト画面に質問や解答を表示する前に HTML を変更するフックを提供します。 このフックはカードに JavaScript を追加するのに役立ちます。

例:

from anki.hooks import addHook
def prepare(html, card, context):
    return html + """
<script>
document.body.style.background = "blue";
</script>"""
addHook('prepareQA', prepare)

このフックは三つの引数を取ります。質問または解答の HTML、現在のカードオブジェクト (これによって、例えば特定のノートタイプにアドオンを限定することができます)、フックを実行するコンテキストを示す文字列です。

変更した HTML を必ず戻すようにしてください。

コンテキストは次の中から一つ選びます。"reviewQuestion", "reviewAnswer", "clayoutQuestion", "clayoutAnswer", "previewQuestion", "previewAnswer"

注意: カードレイアウト面での解答のプレビューや、"show both sides (両面表示)" を設定したプレビュー画面は、"Answer" コンテキストだけ使えます。これは カードの裏面に追加した JavaScript は、表面だけに追加した JavaScript に依存すべきではないことを意味します。

Anki は新しいテキストを表示する前に、前のテキストをフェードアウトするため、JavaScript のフックは、適切なタイミングでスクロールするようにアクションを実行する必要があります。次のように行います。

from anki.hooks import addHook
def prepare(html, card, context):
    return html + """
<script>
onUpdateHook.push(function () {
    window.scrollTo(0, 2000);
})
</script>"""
addHook('prepareQA', prepare)
  • onUpdateHook は新しいカードを DOM に配置した後に発生しますが、このカードを表示する前です。

  • onShownHook はこのカードがフェードインした後に発生します。

このフックは、質問や解答を表示するたびにリセットします。

デバッグ

自分のコードから例外が発生した時には、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 のソースコードは 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 をブラウズすることが簡単にできます。

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

アドオンの共有

AnkiWeb は、アドオンモジュールを収録するには zip ファイルを要求します。フォルダ名は必要しとません。例えば、次のようなモジュールを持っているとします。

addons21/myaddon/__init__.py
addons21/myaddon/my.data

この場合、zip ファイルの内容を次のようにしてください。

__init__.py
my.data

フォルダ名を含めた次のような zip ファイルは、AnkiWeb は受け付けません。

myaddon/__init__.py
myaddon/my.data

zip ファイルの名前は自由に付けることができます。

Python は、実行するときに __pycache__ フォルダを自動的に作ります。zip を作る前に必ずこのフォルダを削除してください。AnkiWeb は、__pycache__ フォルダを含んだ zip ファイルを受け付けないからです。

Zip ファイルを作ったら https://ankiweb.net/shared/addons/ にアップロードできます。

Anki 2.0 アドオンの移植

Python 3

Anki 2.1 は Python 3.6 以降が必須です。Python 3 を自分のマシンにインストールしたら、2to3 ツールを使って、自動的に既存のスクリプトを Python 3 のコードにフォルダ単位で変換できます。 次の通りです。

2to3-3.6 --output-dir=aqt3 -W -n aqt
mv aqt aqt-old
mv aqt3 aqt

ほとんどの単純なコードは自動的に変換できますが、手作業で変更の必要がある箇所が残るかもしれません。

Qt5 / PyQt5

PyQt5 でシグナルとスロットをつなぐ構文が変わりました。最近の PyQt4 バージョンではこの新しい構文を同じようにサポートしていますので、Anki 2.0 と 2.1 の両方のアドオンで同じ構文を使えます。

さらに詳しい情報は次のリンクをご覧ください。 http://pyqt.sourceforge.net/Docs/PyQt4/new_style_signals_slots.html

あるアドオン作者が次のツールがコードを自動的に変換するのに役立ったと報告してくれました。 https://github.com/rferrazz/pyqt4topyqt5

Qt モジュールは、'PyQt4' の代わりに 'PyQt5' の中にあります。条件分岐で読み込むこともできますが、さらに簡単な方法は aqt.qt から読み込むことです。例えば次のようにします。

from aqt.qt import *

これは、特定の Qt のバージョンを指定することなく、QDialog のような全ての Qt オブジェクトを読み込みます。

単一の .py アドオンにも独自のフォルダが必要

それぞれのアドオンは、独自のフォルダに保存することになりました。以前 demo.py という名前をつけていたアドオンの場合、demo というフォルダと、__init__.py を一緒に作る必要あります。

2.0 との互換性を気にしないなら、名前を demo.pydemo/__init__.py に変更するだけで済みます。

同じファイルで 2.0 をサポートする計画の場合は、元のファイルをフォルダにコピーして (demo.pydemo/demo.py)、さらに次のような demo/__init__.py を追加して、相対的にアドオンを読み込みます。

from . import demo

AnkiWeb にアップロードする時にはフォルダを Zip ファイルに収める必要があります。さらに詳しい情報は アドオンの共有 をご覧ください。

アップグレードでフォルダを削除

アドオンを更新する時には、アドオンフォルダのすべてのファイルを削除します。例外は特別な user_files フォルダ だけです。アドオンが単純なキー/値の組み合わせ以外の設定データが必要な場合は、必ず関連するファイルを "user_files" に保存して、更新時に失われるのを避けてください。

2.0 と 2.1 を一つのコードベースでサポート

ほとんどの Python 3 のコードは、Python 2 でも動作します。このため、Anki 2.0 と 2.1 の両方で動作するようにアドオンを更新することが可能です。このようにする価値があるかどうかは、必要のある変更内容によります。

scheduler に手を加えているほとんどのアドオンは、わずかな変更だけで 2.1 で動作するでしょう。reviewer、browser、editor の動作を変更するアドオンはさらに多くの作業を必要とします。

最も困難な箇所は、サポートを停止した QtWebKit から QtWebEngine への変更です。WebView を使って単純ではない操作をしている場合は、Anki 2.1 へのコードの移植は、ある程度の作業が必要になり、一つのコードベースで両方のバージョンの Anki をサポートするのは難しいと考えるかもしれません。

修正なしにアドオンが動作する場合、あるいはわずかな変更が必要な場合には、if 文をコードに追加して、同じファイルで 2.0.x と 2.1.x の両方をサポートするファイルをアップロードするのが一番簡単かもしれません。

もっと大きい変更が必要な場合は、2.0.x に対する更新を停止し、あるいは別のファイルで二つのバージョンをサポートすることを維持するのがより簡単かもしれません。

Webview の変更点

Qt 5 は、WebKit の代わりに Chromium ベースの WebEngine を採用しました。このため、Anki の WebView には、WebEngine を現在使用しています。そのためのノートです。

  • 外部の Chrome インスタンスを使って WebView をデバッグできるようになりました。Anki を起動する前に環境変数 QTWEBENGINE_REMOTE_DEBUGGING を 8080 に設定して、Chrome で localhost:8080 にアクセスします。

  • WebEngine は Python との通信に別の方法を使います。 AnkiWebView() は、WebView 用のラッパーで pycmd(str) 関数を提供します。この関数は Javascript の中で ankiwebview の onBridgeCmd(str) メドッドを呼び出します。 Anki の UI の reviewer.py や deckbrowser.py といった様々な場所で、これを使うために変更しなければなりませんでした。

  • Javascript を非同期的に評価します。このため、JS の式の結果が必要な場合は ankiwebview の evalWithCallback() を使うことができます。

  • この非同期の動作の結果、editor.saveNow() はコールバックが必要になりました。アドオンがブラウザ内でアクションを実行する場合、editor.saveNow() を最初に呼んでから、コールバックの中のコードの残りを実行する必要がおそらくあるでしょう。 .onSearch() を呼ぶには、.search()/.onSearchActivated() も変更する必要があります。例えば、ブラウザの .deleteNotes() をご覧ください。

  • setScrollPosition() のような WebKit でサポートした様々な操作は、JavaScript で実装する必要があります。

  • mw.web.triggerPageAction(QWebEnginePage.Copy) のようなページの動作も非同期で、JavaScript や遅延を使って書き直す必要があります。

  • WebEngine には、WebKit のような keyPressEvent() を提供していません。このため、メニューやボタンに割り当ててないショートカットを捕捉するコードは変更しなければなりませんでした。setStateShortcuts() は、指定した状態のショートカットを調節するのに使えるフックを呼び出します。

Reviewer の変更点

Anki は次のカードをフェードインする前に、前のカードをフェードアウトするようになりました。このため showQuestion フックが発生した時には、DOM の中の次のカードが表示できません。適切な時に Javascript を実行するのに使える新しいフックがあります。詳しくは、こちら をご覧ください。

アドオンの設定

多くの小さな 2.0 用のアドオンは、ユーザーがソースコードを編集してカスタマイズすることを必要としていました。2.1 では、これはもう良いアイデアではありません。ユーザーの変更が、更新の確認やダウンロードで上書きされるからです。2.1 では 設定 システムを導入して、このような場合に対応するようになりました。2.0 も同様にサポートする必要がある場合には、次のようなコードが使えるでしょう。

if getattr(mw.addonsManager, "getConfig", None):
    config = mw.addonManager.getConfig(__name__)
else:
    config = dict(optionA=123, optionB=456)

日本語版訳注

Anki 2.1 Beta の更新履歴を知るには、Anki 2.1 Beta が役立ちます。

Anki 2.1 Beta 16 現在、Anki に同梱している Python のバージョンは 3.6.1 です。

日本語版更新履歴

  • 2017/08/27 Anki 2.1 Beta 13 準拠 (2017/08/26版) 初出

  • 2017/08/29 Anki 2.1 Beta 14 準拠 (2017/08/28版)

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

  • 2017/09/03 Anki 2.1 Beta 15 準拠 (2017/09/02版)

  • 2017/09/06 Anki 2.1 Beta 16 準拠 (2017/09/06版)

  • 2017/09/10 Anki 2.1 Beta 16 準拠 (2017/09/10版)

  • 2017/10/29 Anki 2.1 Beta 17 準拠 (2017/10/03版)