@kurosawa0626 さんが Photoshop のナビゲーター的なものを作りたい、という話をしていて、過去に似たようなものを作ったことがあったので共有してみます。
記事内のコードは WPF で書いたものですが、WinRT でも一部を除いてほぼ同じようなコードで書けるはずです。
2015/04/12 追記:
@okazuki さんが、WinRT でやる場合の補足記事を書いてくださいました。
http://okazuki.hatenablog.com/entry/2015/04/11/195941
目標
今回は、Thumb コントロールを活用し、Photoshop のナビゲーターと同等のものを作ります。
ScrollViewer に表示されているコンテンツについて、以下を実現しましょう。
- サムネイルの表示
- Viewport (赤枠部分) の表示
- 赤枠のドラッグで Viewport の位置変更
↓ みたいなやつ
最終的に完成したコードは、gist で公開しています。
WPF アプリケーションを作成し、MainWindow.xaml と MainWindow.xaml.cs を組み込めば実行できます。
ScrollViewer とか
大きなコンテンツを画面内に収めたいときは、ScrollViewer コントロールを使用しましょう。
表示領域よりもコンテンツのサイズの方が大きいとき、ScrollViewer はスクロール バーを表示して、可視領域を移動できるようになります。
このとき、実際に見えている領域のことを Viewport と呼びます。
それに対し、ScrollViewer 内の全域 (= コンテンツのサイズ) は Extent です。
それぞれ、ViewportWidth、ViewportHeight、ExtentWidth、ExtentHeight プロパティでサイズを取得できます。
余談ですが、ListBox などの ItemsControl 派生型は、内部で ScrollViewer を使用していることが多いですね。
ひとまず、下記のように画像 (line 28-34) とサムネイル (line 19-21)、そして Viewport を示す Border (line 22-25) 配置する XAML コードを用意しました。
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 35 36 |
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Width="1200" Height="600"> |
サムネイルと Viewport の表示
Bitmap
コンテンツ部分の Visual 要素をビットマップに書き込み、画像として表示する方法です。
WPF と WinRT でやり方が微妙に異なりますが、考え方は同じです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
using System; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Media; using System.Windows.Media.Imaging; namespace ThumbsScrollViewer { public partial class MainWindow { public MainWindow() { this.InitializeComponent(); // Bitmap をダウンロードして var image = new BitmapImage(new Uri("http://www.hitachinoki.net/download/wp/2015_ta.jpg")); // Image コントロールの Source に設定 (サムネイルはコントロール自体のサイズを小さくすればよい) this.Image.Source = this.Thumbnail.Source = image; } } } |
VisualBrush (WPF のみ)
別の方法もあります。
Brush 派生型の VisualBrush を使用し、コンテンツ部分の Visual 要素でサムネイル領域を塗りつぶすことで、縮小されたサムネイルを表現できます。
残念ながら WinRT には VisualBrush がないため、WPF 限定です。
Viewport
サムネイル上に ScrollViewer が表示している範囲 (Viewport) を示すためには、…手計算です! (説明省略
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 |
private void UpdateThumbnailViewport(object sender, ScrollChangedEventArgs e) { // ExtentWidth/Height が ScrollViewer 内の広さ // ViewportWidth/Height が ScrollViewer で実際に表示されているサイズ var xfactor = this.Thumbnail.ActualWidth / e.ExtentWidth; var yfactor = this.Thumbnail.ActualHeight / e.ExtentHeight; var left = e.HorizontalOffset * xfactor; var top = e.VerticalOffset * yfactor; var width = e.ViewportWidth * xfactor; if (width > this.Thumbnail.ActualWidth) width = this.Thumbnail.ActualWidth; var height = e.ViewportHeight * yfactor; if (height > this.Thumbnail.ActualHeight) height = this.Thumbnail.ActualHeight; // Canvas (親パネル) 上での Viewport の位置を、Left/Top 添付プロパティで設定 // (XAML で言う Canvas.SetLeft(this.Viewport, left); Canvas.SetTop(this.Viewport, top); this.Viewport.Width = width; this.Viewport.Height = height; } |
この時点で、ScrollViewer 内に画像を表示し、画像のサムネイルを表示し、更に ScrollViewer で実際に見えている範囲を赤枠で表示することができました。
CombinedGeometry
これは、WPF 限定です。
サムネイルと同じサイズの図形と、Viewport (赤枠) 部分と同じサイズの図形を、それぞれ RectangleGeometry で表現し、それを CombinedGeometry で結合させます。
その際、GeometryCombineMode=”Xor” を指定することによって、「Viewport 以外の部分のみを塗りつぶす」ことが可能です。
方法 : 結合したジオメトリを作成する
https://msdn.microsoft.com/ja-jp/library/ms746682(v=vs.110).aspx
1 2 3 4 5 6 7 8 9 10 |
GeometryCombineMode="Xor" /> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public MainWindow() { this.InitializeComponent(); var image = new BitmapImage(new Uri("http://www.hitachinoki.net/download/wp/2015_ta.jpg")); image.DownloadCompleted += (sender, e) => { // サムネイルを表示する Image コントロールのサイズで RectangleGeometry を作成し、 // CombinedGeometry の 1 番目のジオメトリに設定 var rect = new Rect(0, 0, this.Thumbnail.ActualWidth, this.Thumbnail.ActualHeight); this.CombinedGeometry.Geometry1 = new RectangleGeometry(rect); }; this.Image.Source = this.Thumbnail.Source = image; } |
1 2 3 4 5 6 7 8 |
private void UpdateThumbnailViewport(object sender, ScrollChangedEventArgs e) { // 中略... // Viewport を計算したとき、そのサイズで RectangleGeometry を作成し、 // CombinedGeometry の 2 番目のジオメトリに設定 this.CombinedGeometry.Geometry2 = new RectangleGeometry(new Rect(left, top, width, height)); } |
Thumb コントロール
今回のミソである Viewport (赤枠) を、サムネイル上で動かせるようにしましょう。
もっとも簡単な方法は、Thumb コントロールを使用することです。
System.Windows.Controls.Primitives.Thumb (WPF)
Windows.UI.Xaml.Controls.Primitives.Thumb (WinRT)
Thumb コントロールは、ユーザーがドラッグによってコントロールを動かす機能を簡単に実現するためのものです。
これは、ScrollBar や Slider でユーザーがドラッグする部分 (“つまみ” の部分) で使われているものです。
DragStarted、DragCompleted、および DragDelta イベントを使用し、それぞれドラッグの開始と終了、ドラッグの移動量を検知することができます。
今回のサンプルでは、赤枠部分を Thumb コントロールに置き換え、DragDelta イベントを使用し移動量を ScrollViewer に反映します。
Thumb コントロールは他のコントロールと同様に ControlTemplate を使用し外観をカスタマイズできるため、赤枠は Thumb の外観として表現します。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
DragDelta="OnDragDelta"> BorderThickness="2" Background="Transparent" /> |
1 2 3 4 5 6 7 8 |
private void OnDragDelta(object sender, DragDeltaEventArgs e) { this.ScrollViewer.ScrollToHorizontalOffset( this.ScrollViewer.HorizontalOffset + (e.HorizontalChange * this.ScrollViewer.ExtentWidth / this.Thumbnail.ActualWidth)); this.ScrollViewer.ScrollToVerticalOffset( this.ScrollViewer.VerticalOffset + (e.VerticalChange * this.ScrollViewer.ExtentHeight / this.Thumbnail.ActualHeight)); } |
これで完成です!
まとめ
実は、1 年前に「Primitive コントロール探訪」という形で Controls.Primitives 名前空間以下の便利コントロールを紹介しようと書きかけていた記事の一つだったりします (忙しくて頓挫してますが!)。
今回は、@kurosawa0626 さんのリクエストで復活しました。
ありがとうございます (?)。
前述のとおり、Thumb コントロールは、ScrollBar や Slider の “つまみ” の部分で利用されているものです。
うまく活用すれば、今回のようにドラッグでコントロールを動かしたい場合や、リサイズ処理等に応用できます。
使ってみてください!
今回のサンプルは、gist で公開しています。
WPF アプリケーションを作成し、MainWindow.xaml と MainWindow.xaml.cs を組み込めば実行できます。
https://gist.github.com/Grabacr07/988bc04fb7f16aaa4fdc