LT: Windows Spotlight + LINQPad で楽しい壁紙生活

まどすた #1 で LINQPad をネタに LT をさせて頂きました。

本稿の投稿日時が実際のものと盛大に食い違っているわけですが、既にスライド中に URI が仮置きされていて、今更変えるのも…というのが要因です。すっかり遅くなってしまいましたが、まあ、書かないよりは…という奴です。

発表資料

スライドのデザインも、標準のものを少し手直しして、アクセント カラーを LINQPad のアイコンから拝借した濃紅っぽい色と白色の組み合わせにし、従って背景もダーク カラーに変更するなど、それなりに手を入れています。

…というのもすべて、7 ページ目のネタを作るための下ごしらえな訳でして、実際のところ、他の要素は全てオマケなのかもしれません。とはいえ、オマケにしては結構前提とするコードの量も多いので、このように総括っぽい記事を書いている次第です (遅刻してますが)。

以下は、LT 中のデモに相当する内容、および詳細な解説となります。

謝辞 (と前説)

事の始まりはスライド中でも言及しましたが、

ということで、@Grabacr07 さんに Windows Spotlight の画像を取得する C# コードを LINQPad で書いてもらったのがきっかけでした。(ありがとうございます!)

具体的なコードは Gist をご参照いただくとして、要点としては、ディレクトリ:

1
%LOCALAPPDATA%\Packages\Microsoft.Windows.ContentDeliveryManager_cw5n1h2txyewy\LocalState\Assets

に転がっている画像っぽいファイルを分類の上コピー、という流れになります。

LINQPad らしいコードへ

上のコードを実行すると、コピーが行われる度にこのような感じでログが出力されます: (例示のため、意図的にウェイトを入れています)

オリジナルのログ出力

この時点でも十分に便利なのですが、実際のところ、上のコードはコンソール アプリケーションでも同じように実現できます。お手軽さという点で LINQPad の素晴らしさを実感できるものではありますが、このコードをより LINQPad らしいものとすることで、「それ ConsoleApplication123 でいいじゃん?」という声にも反論することができます。

そして、実際のところ、LINQPad らしさの源泉は、結果が単なるテキストではなく、HTML で (あるいはグリッド ビューで) 表示されることにある、といえるのです。

進捗報告

ファイルのコピーに時間が掛かる場合、進捗状況が気になってしまいます。私は画像のコピー先に OneDrive をネットワーク ドライブとしてマウントしたものを指定したのですが、これが結構 (というか、結構どころではないくらい) 遅いという事情もあり、早急に (?) 手を打つ必要がありました。

進捗状況の報告といえばプログレス バーです。LINQPad では Util.ProgressBar を使うことでプログレス バーを表示させることができるので、これを使わない手はありません。

Util.ProgressBar のサンプル

このように、Fraction プロパティに進捗率を設定してやることで、プログレス バーの値を動的に変化させることができます。

しかし、今回のようにファイルの処理状況を表示するような場合、いちいち割合を指定してやるのははっきり言ってかなり面倒です。そこで、この Util.ProgressBar クラスを拡張してみることにします:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class ProgressBar
: Util.ProgressBar
{
private int _min, _max, _value;
public int Min
{
get { return this._min; }
set { this._min = value; this.Refresh(); }
}
public int Max
{
get { return this._max; }
set { this._max = value; this.Refresh(); }
}
public int Value
{
get { return this._value; }
set { this._value = value; this.Refresh(); }
}
public ProgressBar(string caption = "", bool hideWhenCompleted = false, int min = 0, int max = 100)
: base(caption, hideWhenCompleted)
{
this.Min = min;
this.Max = max;
}
public void Refresh()
=> this.Fraction
= this.Max == 0 ? 0 : Math.Max(0, Math.Min((double)(this.Value - this.Min) / this.Max, 1));
}

このように、Min, Max, および Value プロパティを追加し、これらが変更された際に Fraction プロパティが適切に設定されるようにすることで目的を達成しています。

画像のプレビュー

さらに、せっかく画像が相手のコードなので、処理した画像をその場で表示して見てみたい、となるのが自然です。プログレス バー程度ならコマンドライン上でも実現できますし、PowerShell での進捗表示もなかなかオシャレですが、さすがに画像のインライン表示はかなり無理があるでしょうし、まさに LINQPad あってこそ、という話です。

画像の表示はまさにそのまま、Util.Image メソッドを使います。あるいは、画像を表すオブジェクト (Image とか) を .Dump() することでも同様の効果となります。

DumpContainer / Image のサンプル

サンプルなのでかなり行儀の悪いコードですが、上の例では、単に画像を表示するだけではなく、一定時間ごとに 2 つの画像を切り替えて表示しています。

この blog でも何回か取り上げている DumpContainer クラスの Content プロパティに Util.Image メソッドの返り値を設定することで画像の切り替えを行い、同じく Hyperlinq クラスのコンストラクタ引数で当該処理を記述することで、画像表示を開始するためのトリガとしています。

さて、ここで DumpContainerHyperlinq を取り上げたのは、「ログをクリックすると画像がプレビュー表示される」機能を実現したいからです。

確かにこれら 2 つのクラスを使うだけで、「クリックすると当該画像を表示する」機能は実現可能に見えます。しかし、今回プレビュー表示する Windows Spotlight の画像は縦長 (Portlait) 画像と横長 (Landscape) 画像の 2 種類が存在するため、高さが大きく異なります。ログを分かりやすく表示しつつ、プレビュー画像も綺麗に表示するためには、画像を常に出力の一番下に表示してやる必要があるでしょう。そして、画像を一番下に固定させるということは即ち、ログ出力部分がちょうど中間に入る―ログ出力は単に .Dump() すればよいわけではなくなる、ということに繋がります。

すぐに思いつくのは、ログ出力全体を DumpContainer に入れてしまい、出力の度に Content を再設定する…という手法です。しかし、これは全体の再描画を要するため、例えば Dump されたオブジェクト出力の折り畳み状態などが出力の度にリセットされてしまいますし、また、Hyperlinq の再出力に伴う結果が予期できないなど、不便かつ危険といえます。

そこで、今回、DumpContainer の入れ子による連続出力を実装してみることにしました。これは簡単に言ってしまえば (出力要素, 新しい DumpContainer) という形の単方向連結リストを作り、それを表示させてやることで、既に表示された部分を再描画して破壊することなく、データを連続的に表示させられるようにしたものです。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class AppendableDumpContainer : DumpContainer
{
private DumpContainer _head, _current;
public AppendableDumpContainer()
{
this._head = this._current = new DumpContainer();
}
public void Append(object obj)
{
var dc = new DumpContainer();
this._current.Content = Util.VerticalRun(obj, dc);
this._current = dc;
this.Content = this._head;
}
}

Util.ProgressBar の例と同様、今回も DumpContainer クラスを継承します。Append メソッドを呼び出すことで引数のオブジェクトを表示し、_current フィールドの DumpContainer を新しく生成、設定することで現在位置を次に移動させます。上の例では Util.VerticalRun メソッドを使用することで垂直方向に出力されていきますが、Util.HorizontalRun メソッドを使うように変更すれば水平方向にも対応可能です。

AppendableDumpContainer のサンプル

このように、既に表示されている部分に影響を与えることなく、更にオブジェクトを表示させてゆくことができました。表示すると末尾に 1 行分の空白が出来てしまったり、コード的には、Content プロパティをオーバーライドすることができないので全体内容の書き換えに対して防御できず、従って Append メソッドの最後で常に最初の DumpContainer を表示させるようにすることで対応していたりと、多少不恰好になってしまってはいますが、目的は達成できているといえるでしょう。

LINQPad らしい出力の完成

これらを組み合わせて、最初のコードに手を入れていきます。コード全体は最後にリンクを紹介することとして、ここでは重要な変更点のみ、掻い摘んで説明します。

1
2
3
4
var bar = new ProgressBar("Copying:", max: files.Length).Dump();
var dcLog = new AppendableDumpContainer().Dump();
var dcImg = new DumpContainer();
Util.WithStyle(dcImg, "display: block; zoom: 0.5").Dump();

最初に、プレースホルダの表示部分です。

  • プログレス バー
  • ログ出力 (下に延びる)
  • プレビュー画像 (高さ可変)

の順に並ぶように出力します。また、プレビュー画像は、オリジナル (フル HD) の 1/2 の大きさで表示されるようにするため、Util.WithStyle メソッドで DumpContainer 全体を囲っています。

そして、ログ出力のコードは以下のようなものとなります:

1
2
3
dcLog.Append(new Hyperlinq(
() => dcImg.Content = Util.Image(path),
"log message goes here..."));

クリックすると、プレビュー画像を表示するプレースホルダの内容を書き換えるだけですが、先述の通り DumpContainer 自体に対してスタイルが適用されているため、1/2 で表示されます。

その他必要な変更を行った結果を以下に示します:

LINQPad らしいログ出力

当初のものと比べ、より LINQPad らしい出力となっています。

まとめ

この blog でも LINQPad を使い倒す記事がそれなりの数揃ってきて、割と自分の LINQPad 芸風というものが浮かび上がってきたように思います。

つまるところ、それは DumpContainer であり、或いは Hyperlinq であり、そして Util クラスを使って形を作る、というような感じです。単体でも十分に面白いとはいえ単純な機能ですが、組み合わせ次第で如何様にも活用できます。まるで LINQ のようであり、さすが LINQPad という名を冠するソフトの機能なだけあります。

LINQPad とより仲良くなれる、これらの機能の面白さ、奥深さ、活用し甲斐について、少しでも皆様に伝わりましたら幸いです。

ダウンロード

本稿で紹介したコードを含んだ LINQPad クエリ ファイルは GitHub で公開しています