空談録

世界で5人くらいに役立ちたい

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引数は適当にマイナスの数値を入れています。こんくらいあれば大体後ろになるだろう計算。
数字はお好みで。



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

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

この辺で