空談録

http://artfulplace.net/blogs/ からひっこしつつ

OneNoteのページをC#のコードでコピーする

私は知っている…誰も書いてない = 需要がないことをなぁ…!

というわけで、OneNoteのページを移動させるのがめんどくさいのでなんかアプリケーション作ってコピーしようと思ったのですが、OneNoteのApplicationClassにはCopyPageContentみたいな気の利くメソッドがありません。どういうことだ。

ApplicationClassをオブジェクトブラウザで見てるとこんなメソッドがあることはわかります。
・GetPageContent(string pageId)
・UpdatePageContent(string pageXml)
・DeletePageContent(string pageId, string objectID)
…なんかDeletePageContentは違う気がする(DeleteHierarchyの気がしてきた)。

まあ確かに世の中取得と追加ができればコピーはできるでしょう。コピーして元のページを消せば移動もOK!
なんとありがたい!!今すぐ悔い改めてほしい!!!

とりあえずページのコピーができれば移動も達成できるのでコピーの仕方について書いていきます。

コピーしたXMLのdiffを確認しよう

Office界では同じ処理がすでにある場合、既存の処理によって得られる結果を確認するというのは定石ですね。どうかしてるぜ!
Visual Studio CodeのCompareで比較してみます。

f:id:fantasticswallow:20170326111707p:plain

まあ雰囲気だけ感じ取ってもらえばいいのですが、変化しているのは
・Page.@ID
・OE.@objectID
・CallbackID.@callbackID
の3つのIDです。

PageのIDは存在するページのIDを指定する必要があります。このIDを基にUpdatePageContentはページの書き換えを行います。そのためコピー先のページIDに書き換えます。ここはReplace(元のID, 新しいページのID)で終わるので楽です。
OEのobjectIDはOE要素を追加した際に自動で振られるIDです。競合してはいけないため、IDを残してしまうとUpdatePageContentに失敗します。しかし特にこちら側で割り当てる必要もないので(勝手に追加される)、すべてのobjectIDを消せばこちらはOKです。
CallbackIDも同じく勝手に振られるのですが、画像のデータについてはこちら側で指定する必要があります。最も面倒な作業。

まあ実際やっていきましょう。

テキストのみのページをコピーしよう

流れとしてはこうです。

・新しいページを作成してIDを取得
・コピー元のページコンテンツを取得
・IDを書き換える+objectIDを削除
・UpdatePageContentで新しいページにコピー元のページのコンテンツを書き込む

というわけでザクザクと書いていきましょう。
めんどくさいのでいくつかのメソッドを先に定義します。

// need set instance
internal ApplicationClass app { get; set; }

public string GetPageContent(string id)
{
    app.GetPageContent(id, out var res);
    return res;
}

public string CreateNewPage(string sectionId)
{
    app.CreateNewPage(sectionId, out var pId, NewPageStyle.npsBlankPageNoTitle);
    return pId;
}

public void UpdatePageContent(string str)
{
    app.UpdatePageContent(str);
}

public string GetBinaryPageContent(string pageid, string callbackId)
{
    app.GetBinaryPageContent(pid, callbackId, out var res);
    return res;
}

全部outで定義してくれるInterop.OneNoteとout varの相性が大変良くていい…。

今回はCreateNewPageのPageStyleを指定してますが、コピー先のページの情報は一切使わないので何でもいいです。

では実際にコピーするメソッドを作成しましょう。

public void CopyPageContent(string targetPageId, string copyToSectionId)
{
    var currentPage = GetPageContent(targetPageId);
    var newPageId = CreateNewPage(copyToSectionId);

    var idIndex = currentPage.IndexOf(targetPageId);
    currentPage = currentPage.Remove(idIndex, targetPageId.Length);
    currentPage = currentPage.Insert(idIndex, newPageId);
    currentPage = Regex.Replace(currentPage, "objectID=\"[0-9A-F\\-\\{\\}]+\"", "");

    UpdatePageContent(currentPage);
}

ちょっとどころかだいぶ強引なobjectIdの消し方してます。だってselectedとか入るしめんどくさいし…
XElementに展開して消せばまだましなんでしょうがめんどくさい…。
何かいい手段を思いついたら書き直します。

これでひとまず画像を含まないページはコピーできます。しかし画像を含むと画像データが読めずに死にます。使いづらいのでこれを拡張していきます。

CallbackIDをDataに置き換えよう!

さて、画像要素であるImageやらInkDrawingはCallbackIDしか持っていません。
皆様one:Imageがchoiceでelementに
・File (type:FilePath)
・Data (type:base64binary)
・CallbackID (type:CallbackID)
を持てることは知っていると思いますが(???)、実はcallbackIDを指定しても画像を読み込めません。

というのをxsdのコメントから察することができます。

<xsd:documentation xml:lang="en">
Definition of an image.
The data is either referenced in a file or is included inlined, Base64 encoded.
</xsd:documentation>

ファイルかBase64エンコードしたインライン要素で指定できるよ!って言ってますね。CallbackIDとは一言も言ってません。

するとどうするかというと、やはりどちかで指定するしかないのですが、Fileに吐き出すのは何か違う気がするので、インラインで追加するしかありません。
それもこれもOneNote.ApplicationClassがCopyさせてくれないのがわるい…。

とはいえ画像のbase64binaryはGetBinaryPageContentすれば手に入るのでCallbackIDを拾って対応する画像のDataに置き換えていくだけです。
こんな感じで対応できます。

public void CopyPageContent(string targetPageId, string copyToSectionId)
{
    var currentPage = GetPageContent(targetPageId);
    var newPageId = CreateNewPage(copyToSectionId);

    var idIndex = currentPage.IndexOf(targetPageId);
    currentPage = currentPage.Remove(idIndex, targetPageId.Length);
    currentPage = currentPage.Insert(idIndex, newPageId);
    currentPage = Regex.Replace(currentPage, "objectID=\"[0-9A-F\\-\\{\\}]+\"", "");

    while (currentPage.Contains("<one:CallbackID"))
    {
        var cIdIndex = currentPage.IndexOf("<one:CallbackID");
        var cLastIndex = currentPage.IndexOf("/>", cIdIndex) + 2;
        var cidData = currentPage.Substring(cIdIndex, cLastIndex - cIdIndex);
        currentPage = currentPage.Remove(cIdIndex, cLastIndex - cIdIndex);
        var cId = cidData.Remove(0, cidData.IndexOf('"') + 1);
        cId = cId.Remove(cId.IndexOf('"'));

        var imageBinary = GetBinaryPageContent(targetPageId, cId);

        var dataContent = $"<one:Data>{imageBinary}</one:Data>";
        currentPage = currentPage.Insert(cIdIndex, dataContent);
    }

    UpdatePageContent(currentPage);
}

なんだこの泥臭い感じ…という感じですが一括変換ができない時点でお察し。
こんな感じでobjectIDを爆破すればいい気がするんだけどやはりめんd

ひとまず先頭からCallbackID要素を回収して、CallbackIDを取得して、CallbackIDの位置にData要素を追加という流れをCallbackIDがなくなるまでやるだけです。ウーーン

これで動くしいいかな…いいよね…


というわけでOneNoteのページをコピーする話でした。
最近プヨグラミングしてなかったしこんくらいのらいとな話題がい…い…?

3月の終わりが近づいてきてつらい。

この辺で