Ubuntu Weekly Recipe

第437回 LibreOfficeはreOfficeのライブラリではありません 〜LibreOffice Onlineを支える技術

この記事を読むのに必要な時間:およそ 17 分

Calcでキーボード操作をエミュレーションするプログラム

ここまでは元ファイルを変更しないプログラムでしたが,今度は元ファイルを変更することにします。つまり変更後に「保存」が必要になるわけです。また,LibreOfficeKitはWebブラウザーなど別のプログラムがUI部分を担当するので,UIのキーボード・マウスイベントを受け取る口が必要です。そこで今回のプログラムは,指定したセルまで移動するようキーボード入力した上で,指定した文字列をそのセルに貼り付け,保存して終了します。

lok_keyevent.cpp

#include <stdlib.h>
#include <unistd.h>
#include <iostream>
#include <mutex>
#include <condition_variable>
#define LOK_USE_UNSTABLE_API
#include <LibreOfficeKit/LibreOfficeKit.hxx>
#include <LibreOfficeKit/LibreOfficeKitEnums.h>

class LokTools {
public:
    static const std::string LO_PATH;

    LokTools(std::string path) :
        isUnoCompleted(false),
        _lo(NULL),
        _doc(NULL) {
        _lo = lok::lok_cpp_init(path.c_str());
        if (!_lo)
            throw;
    };

    ~LokTools() {
        if (_doc) delete _doc;
        if (_lo) delete _lo;
    };

    int open(std::string file) {
        _doc = _lo->documentLoad(file.c_str());
        if (!_doc) {
            std::cerr << "Error: Failed to load document: ";
            std::cerr << _lo->getError() << std::endl;
            return -1;
        }

        _doc->registerCallback(docCallback, this);
        return 0;
    };

    static void docCallback(int type, const char* payload, void* data) {
        LokTools* self = reinterpret_cast<LokTools*>(data);

        switch (type) {
        case LOK_CALLBACK_UNO_COMMAND_RESULT:
            {
                std::unique_lock<std::mutex> lock(self->unoMtx);
                self->isUnoCompleted = true;
            }
            self->unoCv.notify_one();
            break;
        default:
            ; /* do nothing */
        }
    };

    void postUnoCommand(const char *cmd, const char *args = NULL,
                        bool notify = false) {
        isUnoCompleted = false;
        _doc->postUnoCommand(cmd, args, notify);

        if (notify) {
            std::unique_lock<std::mutex> lock(unoMtx);
            unoCv.wait(lock, [this]{ return isUnoCompleted;});
        }
    };

    int inputKey(char col, char row, std::string label) {
        if (!_doc || label.empty()) return -1;

        if (_doc->getDocumentType() != LOK_DOCTYPE_SPREADSHEET) {
            std::cerr << "Error: Is not Calc file (type = ";
            std::cerr << _doc->getDocumentType() << ")" << std::endl;
            return -1;
        }

        /* GoTo the celll */
        for (int i = 0; i < col - 'A'; ++i) {
            _doc->postKeyEvent(LOK_KEYEVENT_KEYINPUT, 0, 1027);
            _doc->postKeyEvent(LOK_KEYEVENT_KEYUP, 0, 1027);
        }
        for (int i = 0; i < row - '1'; ++i) {
            _doc->postKeyEvent(LOK_KEYEVENT_KEYINPUT, 0, 1024);
            _doc->postKeyEvent(LOK_KEYEVENT_KEYUP, 0, 1024);
        }

        /* Paste label */
        if (!_doc->paste("text/plain;charset=utf-8",
                         label.c_str(), label.size())) {
            std::cerr << "Error: Failed to paste: " << label << std::endl;
        }

        /* GoTo A1 */
        gotoCell("A", "1");

        return 0;
    };

    void gotoCell(const std::string &col, const std::string &row) {
        std::string command = ".uno:GoToCell";
        std::string arguments = "{"
            "\"ToPoint\":{"
                "\"type\":\"string\","
                "\"value\":\"$" + col + "$" + row + "\""
            "}}";
        postUnoCommand(command.c_str(), arguments.c_str(), true);
    };

    int save() {
        postUnoCommand(".uno:Save", NULL, true);
        return 0;
    };

    std::mutex unoMtx;
    std::condition_variable unoCv;
    bool isUnoCompleted;

private:
    lok::Office *_lo;
    lok::Document *_doc;
};
const std::string LokTools::LO_PATH = "/usr/lib/libreoffice/program";

void usage(const char *name)
{
    std::cerr << "Usage: " << name;
    std::cerr << " [-p path-of-libreoffice] [-r row] [-c column]";
    std::cerr << " CalcFile Label" << std::endl;
}

int main(int argc, char **argv)
{
    int opt;
    std::string lo_path = LokTools::LO_PATH;
    char column = 'A';
    char row = '1';
    while ((opt = getopt(argc, argv, "p:c:r:")) != -1) {
        switch (opt) {
        case 'p':
            lo_path = std::string(optarg);
            break;
        case 'c':
            if (isalpha(*optarg))
                column = toupper(*optarg);
            break;
        case 'r':
            if (isdigit(*optarg))
                row = *optarg;
            break;
        default:
            usage(argv[0]);
            exit(EXIT_FAILURE);
        }
    }

    if (argc - optind < 2) {
        usage(argv[0]);
        exit(EXIT_FAILURE);
    }
    const char *calc_file = argv[optind++];
    const char *label = argv[optind];


    try {
        LokTools lok(lo_path);

        if (lok.open(calc_file)) {
            std::cerr << "Error: Failed to open" << std::endl;
            exit(EXIT_FAILURE);
        }

        if (lok.inputKey(column, row, label)) {
            std::cerr << "Error: Failed to input keys" << std::endl;
            exit(EXIT_FAILURE);
        }
        if (lok.save()) {
            std::cerr << "Error: Failed to save document" << std::endl;
            exit(EXIT_FAILURE);
        }

    } catch (const std::exception & e) {
        std::cerr << "Error: " << e.what() << std::endl;
        exit(EXIT_FAILURE);
    }

    return 0;
}

ビルド方法と使い方は次のとおりです。以下の例だと「F5」のセルに「日本語ラベル」を記述します。ちなみにサンプルの都合上,-cオプションと-rオプションともに,1文字しか受け付けないようにしています。きちんと作りなおせば,もっと広範囲のセルを指定が可能です。

$ g++ lok_keyevent.cpp -Wall -Werror -std=c++11 -ldl -o lok_keyevent
$ ./lok_keyevent -c F -r 5 sample.ods "日本語ラベル"

コマンドには表計算ソフトウェアであるCalcのファイルを渡してください。A1セルにカーソルがある空のファイルを想定しています。LibreOffice Calcで新規作成して保存すれば,そのようなファイルを生成できます。libreofficeコマンドを使ってヘッドレスに作成する方法はわかりませんでした。

コードは前と異なり,LokToolsクラスを作成しその中に各種メソッドを追加しています。しかしながら初期化やファイルを開いている部分でやろうとしていることは同じです。

キーボード入力部分は,inputKey()メソッドが該当します。

/* GoTo the celll */
for (int i = 0; i < col - 'A'; ++i) {
    _doc->postKeyEvent(LOK_KEYEVENT_KEYINPUT, 0, 1027);
    _doc->postKeyEvent(LOK_KEYEVENT_KEYUP, 0, 1027);
}
for (int i = 0; i < row - '1'; ++i) {
    _doc->postKeyEvent(LOK_KEYEVENT_KEYINPUT, 0, 1024);
    _doc->postKeyEvent(LOK_KEYEVENT_KEYUP, 0, 1024);
}

lok::DocumentクラスのpostKeyEvent()を使ってキーボードイベントをLibreOffice本体に伝えます。第一引数はイベントの種類,第二引数がキーイベントよって入力される文字のUnicodeで,第三引数はキーイベントの値です。今回はカーソルキーなので第二引数は0になっています。第三引数の値の具体的な値については,LibreOfficeのIDLファイルAPIドキュメントを参照してください。ここでは1027が右カーソルキーで,1024が下カーソルキーです。つまり列の数だけ右に移動したあとに,行の数だけ下に移動しているわけです。

文字列も究極的にはこのpostKeyEvent()を用いて入力可能です。ただ単純に指定した文字列をセルを書き込むだけであれば,paste()メソッドを使えます。paste()メソッドは第一引数にMIMEを,第二引数に貼り付けるデータを,第三引数にそのサイズを指定します。MIMEを指定できることからもわかるように,別に文字列に限らず,LibreOfficeで貼り付けられるデータであればなんでもかまいません。今回のコマンドでは,指定した文字列をそのまま移動したセルの上に貼り付けています。

最後にgotoCell()を呼び出して,カーソルの位置をA1に戻しています。このgotoCell()では,カーソルキーとは異なりlok::DocumentクラスのpostUnoCommand()を使ってカーソルの移動を行っています。このpostUnoCommand()は,ドキュメントの操作を行えるUNOインターフェースのコマンドを送るメソッドです。第一引数にコマンド名を,第二引数にコマンドのオプションをJSON形式で渡します。

UNO APIはマクロでも使われているAPIです。つまり,UNOコマンドを使えば,大抵の操作はできることになります。もし現時点でLibreOfficeKitを使うとなると,このpostUnoCommand()経由でさまざまな操作を行うことになるでしょう※4⁠。サンプルコードではpostUnoCommand()自体がいくつかの場所にわかれています。以下はそれらのうち説明に必要な部分をまとめた擬似的なコードです。

※4
利用できるUNOコマンドのリストなどがあればよかったのですが,めぼしいドキュメントを見つけられませんでした。もし何かUNO APIを使った操作を考えているのであれば,一度LibreOffice上でその操作をマクロとして記録し,そこからプログラムに書き起こした方が簡単かもしれません。マクロの記録については,まず最初にメニューの「ツール」から「オプション」を開いて,⁠LibreOffice→詳細」「マクロの記録を有効にする」にチェックを入れます。再び「ツール」から「マクロ」にある「マクロの記録」を選ぶと,そこからの操作をマクロとして記録するようになりますので,適宜再現したい操作を行ってください。記録を終了させた上で,⁠マクロ」「マクロの管理」から「LibreOffice Basic」を選ぶと,先ほど記録したマクロを編集できます。その内容を読めば,おおよそどのコマンドを使えばいいかわかるでしょう。
std::string command = ".uno:GoToCell";
std::string arguments = "{"
    "\"ToPoint\":{"
        "\"type\":\"string\","
        "\"value\":\"$" + col + "$" + row + "\""
    "}}";
_doc->postUnoCommand(command.c_str(), arguments.c_str(), true);

std::unique_lock<std::mutex> lock(unoMtx);
unoCv.wait(lock, [this]{ return isUnoCompleted;});

.uno:GoToCellがUNO APIのコマンドです。GoToCellはToPointプロパティで,移動先のセル名を指定します。F5セルだったら$F$5となります。postUnoCommand()は第三引数で,コマンド完了の通知を行うかどうかのフラグを設定出来ます。

完了時に呼び出されるのはlok::DocumentクラスのregisterCallback()で登録したコールバック関数です。このコールバックはLibreOfficeKitEnums.hのLibreOfficeKitCallbackTypeにリストアップされているイベント時に呼び出され,コマンド完了時以外にもタイル領域の更新時や検索完了時にも呼び出されます。そこで本プログラムではstd::condition_variableを用いて,postUnoCommand()呼出し後にコールバックが呼ばれるまで待つようにしています※5⁠。

※5
UNOコマンドによっては同期的に動いているようにも見えるので,必ずしも待つ必要があるわけではなさそうです。

ファイルを保存する場合も同様に.uno:Saveを用いて保存しています。保存の場合は,引数を与える必要は特にありません。ただしコマンドの終了イベントをちゃんと待たずにプログラムを終了すると,当然のことながら保存されませんので注意してください。

著者プロフィール

柴田充也(しばたみつや)

Ubuntu Japanese Team Member株式会社 創夢所属。数年前にLaunchpad上でStellariumの翻訳をしたことがきっかけで,Ubuntuの翻訳にも関わるようになりました。