空談録

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

workbench.colorCustomizationsを用いた痛Visual Studio Code化

コードを書きたい、気力がない…。
創造力の減少を感じる。

かなり昔にTextMate Themeを用いてVisual Studio Codeを痛くしたものがあったと思うのですが、Visual Studio Codeが1.12になったタイミングで大きく変わったのでその対応です。
artfulplace.hatenablog.com

1.12の変更についてはリリースノートでどうぞ。
code.visualstudio.com

workbench.colorCustomizationsで設定から色を設定可能になったのはいいんですが、その結果としてまた変える対象が変わったのでぐぬぬという感じです。

注意:いつも通り壊れてるとか出たりサポート対象外になりますがそれでもよい方向けです。
公式にサポートしてほしい…。
また基本的にはlightテーマで行っています。darkなテーマでそのまま使うとおかしくなる可能性は否定できないのでご了承ください。

目指す状態

f:id:fantasticswallow:20170520215450j:plain
大体こんな感じになるようにしましょう。

cssを書き換えよう

いつも通りCSSを書き換えます。
とはいえかなり減ってきた感じです

.monaco-workbench .part.editor {
	 background: url("vsc-bg.png") no-repeat;
	 background-size: cover;
}
.monaco-editor, .monaco-editor .inputarea {
        background: none;
}
.monaco-editor.vs-dark {
        background: none;
}

まずは画像を置くのは必須です。
あとはいわゆる基本テーマの背景色を削除する形になります。
他の部分については配色テーマの置き換えで消せるのですが、基本テーマ部分(vs, vs-darkなどのuiThemeが適用される部分) については配色テーマでは操作ができないのでこの部分だけCSSで消します。

vs-darkについてはあとからCSSが指定される形になるので(light < darkという順で、vs-dark適用時もvs-lightなテーマは存在する)、無理やり消します。
ハイコントラストも同様だと思いますが探すのが面倒だった…。

残りはworkbench.colorCustomizationsで対応します。

colorCustomizationsでいじろう

というわけで画像が後ろに出てくるようにしたところで、新機能であるworkbench.colorCustomizationsをいじっていきます。
これは現在のテーマの色よりも優先される設定となります。なのでAbyssにしてるので変わらないなどなどはないわけです。

またテーマの一部だけいじりたい、という場合テーマを直接いじるのは割と大変ですしプレビューも面倒です。
そんなときにworkbench.colorCustomizationsが役立ちます。ユーザー設定保存するだけで反映されるので見た目どうかを試すのにも便利です。
微妙だったら消せばいいわけですし。

どこが設定できるかはTheme Color Referenceを見ていただくとして本題にいきましょう。
code.visualstudio.com

エディタ部分の背景色を透過させればいいので次の二つを変更します。
・editor.background
・editorGroup.background

editor.backgroundは割と範囲が広いのですが(workbench.part.editor、one-editor-silo、monaco-editor-background)、さすがにこれいじらないとつらいのでいじってしまいます。
変になる可能性も高いですが手間も増えるので難しい。

editorGroup.backgroundはworkbench.part.editor直下のcontentに該当します。これだけなので影響自体はそこまで大きくないです。

それぞれを変更したときの挙動を確認しておきます。
次の画像は左が editor.background: #FF00DD44、右が editorGroup.background: #FF00DD66 です。(それぞれもう片方は#FFFFFF00で透過させています。)

f:id:fantasticswallow:20170521114143p:plain

こうしてみると左側のほうが何層にも重なっているため右よりも濃くなっています。
なのでそのあたりを意識しつつ透明度を設定します。

workbench.colorCustomizationsでいじるときにカラーコードを8文字入れるとrgbaで反映されるのは確認済み(少なくとも今回の二つは)なので、最初の画像のような見た目であれば
・editor.background: "#EFEFEF66"
・editorGroup.background: "#FFFFFF77"
とかやると画像みたいな感じにはなります。

darkなテーマとかの場合はそれに合わせて色を変更するとよいと思います(さすがにdarkテーマでの検証まではしてない)


というわけで便利な機能が増えてましたという話でした。
issueにたびたび背景画像設定できるようにみたいなのは流れてますがどうなったのかはよくわからないのでなんとも。
しかしどういう挙動にするかもわからないので難しい…。

コードを書こうとVisual Studioを開いても何もする気がおきないので何かこうやる気を取り戻したい…。
触らない期間があるとどうしてもやる気がどっかとんで行ってしまう。ぐぬぬ

この辺で

東方天空璋 体験版用のMS-IME向けテキスト辞書

いーつーもーのー
今回は動画がすぐに上がってきたからすぐに完成したぜ。

ということで東方天空璋の体験版のスペルカード名とか曲名とかキャラ名が今すぐ変換したい方向けのやつです。
最近はGoogle 日本語入力とか流行ってる気がするし、こういう辞書ファイルの需要があるのかよくわからない…。
といいつつ私はMS-IMEをずっと使っているわけですが。


ダウンロードは下のDropBoxのリンク開いて右上にあるダウンロードからどうぞ。
ネタバレ対策はないのでネタバレ怖いという方は開かないでください。
www.dropbox.com

利用方法はIMEのオプションの「ユーザー辞書ツール」から「ツール」メニュー →「テキストファイルから登録」 でどうぞ。
よくわからなければググってください。きっとわかりやすい説明がいくらでも見つかります。

ちなみに今回もgistでバージョンを管理しています。だいぶ雑に管理しています。
https://gist.github.com/fantasticswallow/e7f1c574b46a7c79ccc9f18180547a1c
ぼくのかんがえたさいきょうのじしょふぁいる とかミスを直したいとかいろいろ勝手にどうぞ。forkとかはご自由に。

利用する上での注意点は次の通りです。
・利用は自己責任でお願いします。変な変換が紙に出たとかは責任取れません。
・編集はご自由にどうぞ。ミス修正とかもご自由に
DropBoxで公開しているファイルと同じファイルの再配布は不許可。
・辞書への登録部分で何らかの変更点があるものを「配布者の名のもとに」配布するのは構いません。

以上です。
こんなん再配布するやつおらんやろ~って気もしますがまあ一応…。


そんなわけでいつものテンプレ記事でした。
製品版は買いに行きたい…でも暑そう……。

ゲームをあまりしていないので製品版に向けて東方も練習していかないとなぁと毎回変わらない感想で終わります。
いやでも音ゲー最近したらめっちゃきつかったしだいぶ衰えてしまったのでは…。

この辺で

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も同じく勝手に振られるのですが、画像のデータについてはこちら側で指定する必要があります。最も面倒な作業。

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

続きを読む

VS拡張でAdornmentLayerを下に置きたいとき

ラボ畜から解放され社畜への一歩を着々とウワーッ。

先週あたりにVisual Studio 2017のRTMがリリースされましたね。しかもRC入れてたら更新でRTMになってて驚き。
なんだかいろいろ便利機能が増えてるらしいのでちまちまと試していきたいですね。

ところで今や痛VS拡張はこれという何かが分からないので、自分で作っていれるのが主流なのか、それとも私が知らないだけでどこかでまだ作られてるのか知りませんが、最近は自分で作ったのを使っています。
最近はVSIXもかなり楽に作れるので初めてのVSIXに痛背景をおすすめしまs


しかし痛背景を追加しようと思ったとき、AdornmentLayerが前面にくると結構面倒なので後ろ側に回しましょう。

追記(3/16):@kosmosebiさんにViewportAdornemntTextViewCreationListenerのOrderで変更できるという情報をもらったので過去の文を後ろに回して書き直しました。
PredefinedAdornmentLayersが持ってる定数を基にいじれるらしいです。感謝しかない…
PredefinedAdornmentLayers の説明が不十分 | ブチザッキ


ViewportAdornmentを追加したときにすでに書かれているサンプルをもとにImageを追加するようにしたのが次のコードです。

public ViewportAdornment1(IWpfTextView view)
{
    this.view = view ?? throw new ArgumentNullException("view");

    var opacity = 0.4;
    var file = @"";

    var imgSrc = new BitmapImage(new Uri(file));
    this.image = new Image
    {
        Source = imgSrc,
        Stretch = Stretch.UniformToFill,
    };
 
    this.adornmentLayer = view.GetAdornmentLayer("ViewportAdornment1");

    image.Opacity = opacity;

    this.view.ViewportHeightChanged += this.OnSizeChanged;
    this.view.ViewportWidthChanged += this.OnSizeChanged;
    view.Closed += (o, e) => this.adornmentLayer.RemoveAdornmentsByTag("vsbg-adornmentTag");
}

OnSizeChangedは今回は関係ないので載せてません。大体サンプル通りです。


これでAdornmentLayerを追加して、AddAdornmentとかするとわかるのですが、メソッド内で使用しているAdornmentLayerはかなり上の方の要素になっています。
(2017限定? 2015でもちょくちょくそんな現象あった気がするけど)
こうなると正直見づらいですし(画像が文字の前に来てしまう)、"定義をここに表示"みたいなコントロールを出す系の機能については追加したAdornmentLayerのコントロールにフォーカスを奪われてしまいます。痛くするのにここまでのデメリットをつけるのはつらいです。そのため、追加するAdornmentLayerを後ろの方に回しましょう。

(以下追記部分。過去の文は水平線以下)

ここでAdornmentLayerのZ-Orderを無理やり変更してたのが過去の文なわけですが、実際はViewportAdornemntを追加すると作成される"ViewportAdornmentTextViewCreationListener"のほうで変更ができます。

こんな感じのコードがデフォルトで追加されています。

/// <summary>
/// Defines the adornment layer for the scarlet adornment. This layer is ordered
/// after the selection layer in the Z-order
/// </summary>
[Export(typeof(AdornmentLayerDefinition))]
[Name("ViewportAdornment1")]
[Order(After = PredefinedAdornmentLayers.Caret)]
private AdornmentLayerDefinition editorAdornmentLayer;

(そういえばこんなの変更した記憶があるような…)

さて、このまま実行するとどこに追加されるかというと、通常のエディタ画面の場合、最前面に配置されます。
そもそも前面に置くときBeforeとAfterどっちだよという感じですが、「Before = Caretの場合、Caretのほうが前面に配置されること」を指定します。つまり背面に置きたい場合はBeforeを、前面に置きたい場合はAfterを指定します。

では今回はどうしましょう?というと最背面に置きたいので…どこだ?
ブチザッキ情報によるとDifferenceChangesを指定するとよいみたいですが、指定できるパラメータに若干知らない子がいるのでちょっと調べてみましょう。

これまたWpfTextView._baseLayer.Childrenに含まれるImplementation.AdormnemtLayer._nameを調べます。
するとこのような結果になります。

(最背面)
outlining
negativetextmarkerlayer
ViewportAdornment1 (After = DifferenceChanges)
BraceCompletion
CurrentLineHighlighter
ViewportAdornment1 (After = DifferenceSpace)
VsTextMarker
ViewportAdornment1 (After = DifferenceWordChanged)
TextMarker
SelectionAndProvisionHighlight
BlockStructure
Inter Line Adornment
RoslynLineSeparator
Squiggle
RoslynSuggestions
TextContentLayer
CaretElement
RoslynRenameDashboard
ViewportAdornment1 (After = Caret)
(最前面)

After = Caretを指定すると本当に前面に来ていることが分かります。
逆に最背面はoutliningです。どうもOutliningあたりから並びが謎い。
Difference~~を指定した場合、その時点で存在していなくても上のような位置に入ってきます。DifferenceChangesはかなり後ろに来ることが分かります。

RoslynRenameDashboardがViewportAdornmentに挟まれる理由としてはおそらくCaret, ViewportAdornment1がロードされた後にRoslynRenameDashboardがAfter Caretで読み込まれるためだと思われます。
そのためRoslynRenameDashboardを指定するのはつらそう(そもそも何のレイヤーかわからないけど)

ひとまず後ろの方に置きたい!って要望をかなえる場合これでいいかと。

/// <summary>
/// Defines the adornment layer for the scarlet adornment. This layer is ordered
/// after the selection layer in the Z-order
/// </summary>
[Export(typeof(AdornmentLayerDefinition))]
[Name("ViewportAdornment1")]
[Order(Before = PredefinedAdornmentLayers.DifferenceChanges)]
private AdornmentLayerDefinition editorAdornmentLayer;

outliningは指定したくないし…。
ちなみに結局stringで_nameを指定しているだけなので、Order(Before = "negativetextmarkerlayer")と入れても通ります。
何のレイヤーかわかりませんけど。そしてなぜかこうするとoutliningよりも背面になります。謎だ。


(ここから過去の文)

後ろに回すにあたって、まずWpfTextViewのAdornmentLayerの管理方法を確認しておきます。
WpfTextView._baseLayerがViewStackという型の値をもっており、このViewStackのChildrenに複数のAdornmentLayerが含まれています。
そしてViewStackは(たしか)Canvasを継承?親要素?してた…ような気がします。

正直実装周りはどうでもいいのですが、WPFCanvasなりの並び順を変更するプロパティとしては Panel.ZIndex 添付プロパティ があります。
今回はCanvasなりが絡んでいて、WPFなのでこれでいけそうですね。
ZIndexは添付プロパティなのでPanel.SetZIndexで指定しましょう。

指定するのはどこでもいいのですが、GetAdornmentLayerの後ろで指定してみるとこんな感じ。

this.adornmentLayer = view.GetAdornmentLayer("ViewportAdornment1");

Panel.SetZIndex((System.Windows.UIElement)adornmentLayer, -550);

SetZIndexの第1引数がUIElementである必要があります。IAdornmentLayerは当然UIElementと関係ありませんが、実際のImplementation.AdormnemtLayerはUIElementを継承しているのでこのキャストが通ります。
今回は後ろに回したいので第2引数は適当にマイナスの数値を入れています。こんくらいあれば大体後ろになるだろう計算。
数字はお好みで。



ひさびさにかいたわりによくわからない記事となってしまった…。
日本語が壊滅している…つらい…。

ようやく春の休みなのでちょこちょこプログラミングを思い出しつつブログも書きたい。

この辺で

Slack APIのスレッドの扱いについて

進捗…ガハッ…

Slackにスレッド機能が追加されたらしいですね。

slackhq.com

www.itmedia.co.jp

見てたらdevcussionのSlackには来てたのでAPIではどう扱うのかのさわり的なところを書いておきます。
Slack APIを触っている人間がどれだけいるのかは知りませんが…

スレッドのリプライメッセージについて

まず、Slackのスレッドは親となる通常メッセージにリプライメッセージを子としてつけていくことで実現しています。
この親メッセージとリプライメッセージが一つのスレッドとなります。

実際のデータを見ながら追っていきましょう。
(ただし内容については架空のものです)

まずチャンネルに流れるメッセージは通常次のような形となっています

{
    "type": "message",
    "user": "Uxxxxxxxx",
    "text": "ねむい",
    "ts": "99999999.000001"
}

このメッセージにスレッド機能をつかってリプライをつけてみます。
つけたリプライは次のようなデータとなります。

{
    "type": "message",
    "user": "Uyyyyyyyy",
    "text": "ビャーーーッ",
    "thread_ts": "99999999.000001",
    "parent_user_id": "Uxxxxxxxx",
    "ts": "99999999.000333"
}

このとき、スレッドとなった親メッセージは次のような値に変化しています。

{
    "type": "message",
    "user": "Uxxxxxxxx",
    "text": "ねむい",
    "thread_ts": "99999999.000001",
    "reply_count": 1,
    "replies": [
        {
            "user": "Uyyyyyyyy",
            "ts": "99999999.000333"
        }
    ],
    "subscribed": false,
    "ts": "99999999.000001"
}

このように基本的にはスレッドを構成するメッセージにはthread_tsフィールドが存在します。
同じthread_tsを持つメッセージをスレッドとして管理できればスレッド機能への対応は可能です。

…え? スレッドメッセージがリプライかどうか見分ける方法って? フィールドがあるかないかじゃないですかね…

スレッドメッセージの取得方法は?

これは別に対応する必要がなくて、普通にx.history系で取れます。x.repliesで取ってくる必要はないです。
またRealTime Messagingでも普通に流れてくるはずです。

さらに言えばスレッドメッセージはthread_tsで基本的には実現しているので、クライアントが対応していなければありがたいことに普通にチャンネルに流れてきます。
つまるところ、スレッドしてリプライメッセージをまとめるか、そのまま表示するか程度の差でしかないです。公式SlackはUIでスレッドとして別にメッセージがあるように見せていますが、実際は他のメッセージと同じ扱いです。

クライアントを作るのが大変そうって? わかる

スレッドにポストする方法は?

これは普通にスレッドの親メッセージのtsを"thread_ts"パラメータに入れるだけです。
リプライしたいメッセージにthread_tsがついててもついてなくても関係なく、chat.postMessageでthread_tsのパラメータを指定すれば勝手にスレッドのリプライとしてやってくれます。

ところで一つ注意点があります。(1/19時点) thread_tsに不正な値を入れてもchat.postMessageは通ります。tsフォーマットさえ守ればおそらく通るはずです。
これの問題点はリプライ扱いなので公式クライアントでは見れなくなります。というかなんだこれ。

ちなみにAPIレスポンスではちゃんと拾えています。
f:id:fantasticswallow:20170119203621p:plain

im.historyによるレスポンスと公式クライアントの見え方です。少なくともuuuuというメッセージは見えません。
そして世界のどこのSlackにもなさそうな"1484824875.990999"とかいうふざけたthread_tsですが通っています。
ウーーン

reply_broadcastとかいうやつ

殺意しか湧かないですね!

Slackのスレッド機能にはもう一つ機能があります。リプライメッセージをチャンネルにも通知するという機能です。
"Also send to {チャンネル名}"とかそういうチェックがリプライの所にあると思います。それですそれ。

さっきの画像の"Also send as direct message"もそうです。
これらはAPIではreply_broadcastとして扱われます。

この機能を使う、つまりチェックを入れて投稿するにはchat.postMessageの"reply_broadcast"パラメータをtrueにするだけです。
問題はやってくるレスポンスデータですよええ…

こんなんが来ます。

{
    "channel_id": "00000000",
    "channel_type": "D",
    "timestamp": "1484826106000008",
    "is_multiteam": false,
    "attachments": [
        {
            "from_url": "https:\/\/devcussion.slack.com\/archives",
            "fallback": "[January 19th, 2017 3:21 AM] fantasticswallow: \u3042\u3042\u3042\u3042",
            "ts": "1484824875.000002",
            "author_subname": "fantasticswallow",
            "channel_id": "D1A25G6E8",
            "channel_name": "Direct Message",
            "is_msg_unfurl": true,
            "text": "\u3042\u3042\u3042\u3042",
            "author_link": "https:\/\/devcussion.slack.com\/team\/fantasticswallow",
            "author_icon": "https:\/\/avatars.slack-edge.com\/2016-01-16\/18649887269_b1c2847c8a63b9a64a8c_48.png",
            "mrkdwn_in": [
                "text"
            ],
            "id": 1,
            "footer": "2 replies"
        },
        {
            "fallback": "Foo\uff01",
            "author_subname": "fantasticswallow",
            "text": "Foo\uff01",
            "mrkdwn_in": [
                "text"
            ],
            "author_link": "https:\/\/devcussion.slack.com\/team\/fantasticswallow",
            "from_url": "https:\/\/devcussion.slack.com\/archives",
            "ts": "1484826106.000008",
            "author_icon": "https:\/\/avatars.slack-edge.com\/2016-01-16\/18649887269_b1c2847c8a63b9a64a8c_48.png",
            "id": 2
        }
    ],
    "text": "",
    "type": "message",
    "subtype": "reply_broadcast",
    "user": "U0JK7N9D5",
    "ts": "1484826106.000010"
}

ウウウーーーーン……なんだこれ…
なんかもう頑張って対応…って無理じゃね…

RealTime Messagingにおけるスレッドイベント

最後にRTMでもらえるスレッド周りのイベントを紹介しましょう。
リプライメッセージそのものは普通にメッセージと同じでやってくるので特別に用意されていません。

まずmessage_repliedっていうのがあります。
https://api.slack.com/events/message/message_replied
これは簡単で、「すでにあるメッセージがスレッドになったぜ!」って通知をしてくれます。それだけです。

次にreply_broadcastがあります
https://api.slack.com/events/message/reply_broadcast
上のreply_broadcastがポストされたら飛んできます。



というわけでSlack APIのスレッド周りのお話でした。
reply_broadcastに殺意が湧くのとUIどうしろと感が半端ないこと以外は平和でしたね!

基本的にスレッドも普通のメッセージと同じフィールドを持っているのでスレッドガン無視も一つの手かなぁとは思います。
それはそれでどうなのよという感じですが、かといって対応しようにもそう簡単に実装できるものでもない気が…

この辺で

2016年を振り返るらしい

どちらかというと明日が冬コミ3日目という情報の方が信じられない…

毎年目標もなく気が向いたらブログを書くだけの人間ですが、振り返りは書いておきましょう。

プヨグラミング

思った以上に何も進歩がなかった…
最近こそNereidでひさびさにおうちプログラミングの機会は増えましたが、7月から11月とかマジでVisual Studioを起動した記憶がない…

それもこれもラボ畜がわるい。わたしはらぼちくにくるうけもの…。

来年はもう少し書きたい、せめてNereidは完成させたい。せめてSlackアプリはつくりおえ…ウッ

ゲーム

気が付いたらSteamが入っていた。

地球防衛して大復活の5面で焼かれていました。大復活難しい…。
地球防衛については割と頑張りましたが武器コンプリートをする前に心が折れてしまった悲しみ。マジで後半は天の兵団と竜の宴ループでしかなかったしなぁ。グングニルが早めに出たのでまだなんとかなったのですがとはいえやはり作業量がつらい。
なおフェンサーは最後まで使いこなすことはできませんでした。エアレイダーのリムペットガンで戦ってる方が戦力になる程度には才能が足りない。

そさげ

ぱどどらは今年は耐えきりましたが、ティターニアの8コンボ教室が突破できなくて残っていたモチベーション?使命感?みたいなのは死にました。
もう無理…転生ウズメでたら起こしてください…。いや起こさなくていいや…。
転生ミネルヴァでやるのは割と楽しかったのですが、なんか完全に時代に置いていかれた感じがあるのでもう無理かなぁという。

ゴ魔乙はギルドバトルで急速にやる気が墜落した気がします。
ギルドへの参加をやめて適当にプレイしてた頃のモチベーションを取り戻した方がいい気がする。
それはそうとしてソードが楽しいですね。使いこなせないけど。

10月くらいから新しくシャドウバーーーースを始めました。
私ふぁぼツバメ!しゃどうばーす知ってる? 本格アグロバトルがスマホでこんなに簡単に以下略。
違うんだ、私は宴冥府ルナちゃんとかノノパメラとかで遊びたいだけなんだ…なんだこの超越とかいうゴリラは…
宴冥府はその辺の鷹が飛んできて死ぬしパメラちゃんはテミス撃たれるしどっちも大体超越の前には無力だし……
とりあえず対戦する超越の8割が4ターン目までに0コス導きを使うの反則だと思いますううーーーー。

いやでもニンジンとかネクロアサシンとかでめっちゃ楽しい動きができるようになったのでルナちゃんは楽しい。勝てるとは言ってない。
なんだかんだで楽しんではいます。でもえいらは5コスでいいし運命は手札消滅でいいし超越はPP10以降じゃないと撃てなくなってほしい。
超越のセラフよりもどうにもならない感じはなんなんだ。

というか土にもふぇありーーさーくるみたいに1コススペルで土の印2枚とかひとつで3回くらい使える土の印とかください…。

お花

闇の組織の陰謀によりFlower Knight Girlを始めてしまった感じです。
巫女アネモネかわいい。

f:id:fantasticswallow:20161230104923j:plain

イベントはそこそこ楽です。ガチャ?知らんなぁ

f:id:fantasticswallow:20161230104809j:plain

パーティはそこそこそろってきたのですがまだ足りない気はします。
とりあえず虹が欲しい……。
総合力の割には善戦してくれることが多いのですがさすがにパーティ1に全振りしすぎて他のパーティでボスいくと簡単に全滅するのがつらい。

スノードロップほしいなぁと思いながら続けていますが果たしてどうなることやら…

大学その他もろもろ

無事にけんきゅーーーしつとかいうところでらぼちくになりました。
研究というのが大変につらいことだけわかりました。というか私は研究が向いてないのではという感じがしました。
普段から大変適当な人間なのでしっかりしたあれそれができないんだなぁという。
(もしかして:仕事も向いてない)

発想力もなければ論文執筆能力もなくすぐれたプレゼン能力もないのでもうだめです。

そんなこんなで来年からしゅーしょくです。内定という何かをもらえたのでお仕事にはありつけそうです。
問題は何か月もつんでしょうかというあれ。
お金をもらいつつ無心に働ければいいんですけどねぇ…。


というわけでなんかうつうつしてきたところで終わりです。
なんかだいがく関係でめっちゃ余裕のない一年だったなぁという気分です。来年からもそうなのかなぁ…せめてTLは追えるくらいの余裕がほしいなぁ…。
まず帰ってきて何もやる気が出ないの時点でダメさを感じる。

来年は死んだ目で日々を過ごさないようにだけ気を付けたいなぁと思います。

この辺で

追記:あれげは?と聞かれましたがばるどはーとしか買ってなかったので書くことがなかったというあれです。
来年の予定は未定です。オトメドメインは買いたいかなぁ程度。

ObservableCollectionでAddRangeする

2016年の霊圧がどんどん小さくなっていく…

XAMLな世界でItemsSourceといえばObservableCollection!という感じですが3回に1回くらいAddRangeがほしくなります。
foreachでAddしてるとなんか残念な気分になるというかこちらでいちいちforeachしたくないという。

// collection = IEnumerable<T>
// observableCollection = ObservableCollection<T>

foreach (var item in collection)
{
    observableCollection.Add(item);
}

別に4行だしいいじゃんといわれればそんな気分にもなりますが何度も書くのは嫌になります。
というわけで拡張メソッドでAddRangeを作ってみましょう。
もちろん上のやつを拡張メソッドにしてもうれしいかもしれませんがうれしくないのでそこそこパフォーマンスの向上を狙ってみます。

完成したソースコード

解説しても9割は不要な情報でしかないので今回のコードを先に貼っておきます

public static class ObservableCollectionExtensions
{
    public static void AddRange<T>(this ObservableCollection<T> source, IEnumerable<T> collection)
    {
        if (ValidateCollectionCount(source, collection))
        {
            return;
        }

        var itProperty = typeof(ObservableCollection<T>).GetProperty("Items", BindingFlags.NonPublic | BindingFlags.Instance);
        var colResetMethod = typeof(ObservableCollection<T>).GetMethod("OnCollectionReset", BindingFlags.NonPublic | BindingFlags.Instance);

        var list = itProperty.GetValue(source) as List<T>;
        if (list != null)
        {
            list.AddRange(collection);
            colResetMethod.Invoke(source, null);
        }
    }

    private const int switchForeachThresold = 2;
    private static bool ValidateCollectionCount<T>(ObservableCollection<T> source, IEnumerable<T> collection)
    {
        var count = collection.Count();
        if (count <= switchForeachThresold)
        {
            foreach (var item in collection)
            {
                source.Add(item);
            }
            return true;
        }

        return false;
    }
}

パフォーマンスについては後で書きますがforeachで追加するよりは10 ~ 20倍くらい早くはなります。
ただし素数1のコレクションをAddRangeで追加する場合、foreachの2倍ほど遅くなります。
collectionの要素数見てもいいかもしれませんが閾値までは調べてないので対応はしてません。
とはいえ要素数1のコレクションを100万回AddRangeで追加しての情報なので基本は問題ないとはおもいます? 気になるなら対応するとよいとおもいます。

追記(12/30):↑の問題にとりあえず対応してみました。ぶっちゃけ閾値1でいい気はします。(大幅なパフォーマンス改善が行えるのは1のみ)
閾値の変更についてはswitchForeachThresoldとかいう定数をいじってください。
そもそもforeachは絶対に使いたくない場合はif (ValidateCollectionCount)のブロックを削除してください。

ObservableCollectionの内部コレクションについて

AddRange処理には次の手順が必要になります。
・ObservableCollectionの内部コレクションに対して要素の追加
・NotifyCollectionChangedの実行

ところでObservableCollectionの内部コレクションの型はなんでしょうか? という疑問が出ます。
これについてはReferenceSourceから拾ってきましょう。ObservableCollectionはCollection<T>を継承しているためCollection<T>の内部コレクションがそのままObservableCollectionの内部コレクションになります。

Collectionのコンストラクタは次の通り。
referencesource/collection.cs at master · Microsoft/referencesource · GitHub

public class Collection<T>: IList<T>, IList, IReadOnlyList<T>
{
    IList<T> items;

    public Collection() {
        items = new List<T>();
    }

    // ...
}

まあわかりやすい。List<T>だそうです。
List<T>ということはList<T>.AddRangeで内部コレクションへの追加が行えます。知らない型だったらそれこそAddRangeの内部ソースについても調べる必要が出そうでしたがこれなら問題ないですね。

ObservableCollectionから先ほどのitemsにアクセスするには Collection(T).Items プロパティ (System.Collections.ObjectModel) が使えます。
しかしこの子はprotectedです。ぐぬぬ

ついでにNotifyCollectionChangedも見ておきましょう。

ObservableCollection.NotifyCollectionChangedの呼び出し

これはMSDNに載ってるのでReferenceSourceを見なくても余裕です。
ObservableCollection(T).OnCollectionChanged メソッド (NotifyCollectionChangedEventArgs) (System.Collections.ObjectModel)

アッアッ…これもprotected…

というわけでAddRangeするためには
・Reflectionでごり押し
・ObservableCollectionを継承したクラスを作る
のどちらかで対応する必要があります。グヌヌ

実際にAddRangeを追加しよう

というわけでReflectionでごり押ししたのが上の完成したなんたらです。
えっ?OnCollectionChanged呼び出してないじゃんって? これはこれでまた理由があります。ありますがいったん放置。

継承パターンについてはこんな感じになります。すっきりしてます。

public class ExtendObservableCollection<T> : ObservableCollection<T>
{
    public void AddRange(IEnumerable<T> collection)
    {
        ((List<T>)this.Items).AddRange(collection);
        OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
    }
}

問題は通常のObservableCollectionが使えないという点ですがそれを許容できるなら…?
(ただしパフォーマンスはReflectionと大差ない)

ところでNotifyCollectionChangedEventArgsではIListを用いて複数アイテムの変更を通知できます。
じゃあAddRangeでも使えばいいじゃん!っていうと確かに使えますが今度はListCollectionViewの制約にひっかかります。
どういうことかというと、"ListBoxにバインドした状態でAddRangeすると落ちる"ようになります。

この辺については
ListCollectionView/CollectionView doesn't support NotifyCollectionChanged with multiple items
とかで対応する方法とかは書いてあるようです。ObservableCollectionでAddRangeしたいがためにListCollectionViewとかいじるのは嫌なのでResetで対応しています。

(もちろんResetなのでUIをRefreshするのとほぼ同義の処理です。そのため1件とかでAddRangeしまくるとパフォーマンスが強烈に低下します。)

パフォーマンスを見てみる

とまあだらだら書いてきましたが、これでforeachと大差なかったらforeachでよくね!って感じなので見てみます。

比較用のAddRangeTestは次のようなものを用意。

public static void AddRangeTest<T>(this ObservableCollection<T> source, IEnumerable<T> collection)
{
    foreach (var item in collection)
    {
        source.Add(item);
    }
}

普通のforeachですね。ふつう。

計測ですがWindow.LoadedでObservableCollectionに追加して測定します。このときObservableCollectionはListBoxにバインドしておきます。

こんな感じ

private void Window_Loaded(object sender, RoutedEventArgs e)
{
    var sw = new Stopwatch();
    sw.Start();
    // Test Method #1
    col.AddRange(Enumerable.Range(0, 1000000).Select(x => x.ToString()).ToArray());
    col.AddRange(Enumerable.Range(1000000, 1000000).Select(x => x.ToString()).ToArray());

    // Test Method #2
    for (var i = 0; i < 100; i++)
    {
        col.AddRange(Enumerable.Range(10000 *i, 10000).Select(x => x.ToString()).ToArray());
    }

    sw.Stop();
    Debug.WriteLine(sw.ElapsedMilliseconds);
}

LINQ使ってるあたりでなんか遅い気はしますが全部同じ条件だし問題ないでしょう。
5回測定した平均が下の通りです。値はミリ秒。
AddTestがforeach、AddRngがReflection、ExtAddが継承ベースのAddRangeです。

AddTest AddRng ExtAdd
test#1 12589.4 833.8 839.4
test#2 6076.8 424.0 383.8

基本的にforeachの場合、件数に対して一定の時間だけかかります。私の環境で100万件だとほぼ常に6000msですね。
逆に追加したReflectionなAddRangeだとcollectionの件数が少ないほど遅いですが大体3 ~ 5個要素があるとforeachより早くなります。
また200個くらいからそこまで実行時間が変化しなくなります。
AddRangeに対して、コレクションの要素数を変化させつつ100万件の要素を追加したときの時間を下に書いておきます。こちらもミリ秒。
(elementが2の場合、2個の要素を持つコレクションをAddRangeで50万回追加した。)

element time
1 12967
2 6707
4 4118
5 3040
10 2063
20 1078
50 715
100 582
200 484
1000 380
10000 389

もちろん常に一定数追加することはないと思いますので理論値として見といてほしいのですが、まあそこそこなパフォーマンスかなぁという感想です。
collection.Count < 3とかのときはAddで追加とかにしてもいいのですが単純に面倒だった…。


というわけでObservableCollectionにAddRangeする拡張メソッドの話でした。
これで楽していきたい…

そろそろ今年も終わるしまた今年の感想的な記事を書いて今年のブログは終わりかなぁ。

この辺で