かずきのBlog@hatena

日本マイクロソフトに勤めています。XAML + C#の組み合わせをメインに、たまにASP.NETやJavaなどの.NET系以外のことも書いています。掲載内容は個人の見解であり、所属する企業を代表するものではありません。

Silverlight 4で少し変った画像ビューワ?を作ってみた

時間があったので作ってみました。
MVVMとBlendのBehaviorあたりを使っています。

完成予想図

起動すると真っ白の画面が表示されます。

ここに画像をドラッグアンドドロップすると表示されます。

複数の画像をドロップすることで複数表示させることが出来ます。

表示された画像は、ドラッグして移動させることが出来ます。

画像を右クリックすると画像を消せます。

作ってみよう

ということで作ってみます。プロジェクト名はImageViewerにしてSilverlight Applicationを作成します。
まずは、MVVMパターンで作るのでViewModelBase型を作ります。こいつは、INotifyPropertyChangedインターフェースを実装してるだけなので、説明は要らないと思います。

using System.ComponentModel;

namespace ImageViewer
{
    public class ViewModelBase : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        protected virtual void OnPropertyChanged(string name)
        {
            var h = PropertyChanged;
            if (h != null)
            {
                h(this, new PropertyChangedEventArgs(name));
            }
        }
    }
}

続けて、画像1つを表すImageViewModelを作成します。こいつは、画像データと画面上の場所をとりあえずデータとして持たせることにします。今回は、コレクションへの追加や削除や移動は全てImageViewModel内で完結するようにします。なので、コンストラクタで自分が所属するコレクションを受け取り、自分自身を追加して、Removeメソッドでコレクションから自分自身を削除するようにしています。移動は、移動量を受け取り、そのぶんXとYに加算するMoveメソッドを作ります。

using System.Windows.Media;
using System.Collections.ObjectModel;

namespace ImageViewer
{
    public class ImageViewModel : ViewModelBase
    {
        #region 親のコレクション
        public ObservableCollection<ImageViewModel> Parent { get; private set; }
        #endregion

        // 親のコレクションに登録する
        public ImageViewModel(ObservableCollection<ImageViewModel> parent)
        {
            this.Parent = parent;
            this.Parent.Add(this);
        }

        #region Xプロパティ
        private int _x;
        public int X
        {
            get { return _x; }
            set
            {
                _x = value;
                OnPropertyChanged("X");
            }
        }
        #endregion

        #region Yプロパティ
        private int _y;
        public int Y
        {
            get { return _y; }
            set
            {
                _y = value;
                OnPropertyChanged("Y");
            }
        }
        #endregion

        #region イメージデータ
        private ImageSource _image;
        public ImageSource Image
        {
            get { return _image; }
            set
            {
                _image = value;
                OnPropertyChanged("Image");
            }
        }
        #endregion

        #region public void Move(int x, int y)
        // 移動
        public void Move(int x, int y)
        {
            this.X += x;
            this.Y += y;
        }
        #endregion

        #region public void Remove()
        // コレクションから自分自身を削除する
        public void Remove()
        {
            this.Parent.Remove(this);
            this.Parent = null;
        }
        #endregion
    }
}

次にMainPageのViewModelを作成します。こいつはImageViewModelのコレクションを持つようにします。

using System.Windows;
using System.IO;
using System.Collections.ObjectModel;
using System.Windows.Media.Imaging;

namespace ImageViewer
{
    public class MainPageViewModel : ViewModelBase
    {
        #region 画面に表示する画像
        private ObservableCollection<ImageViewModel> _images = new ObservableCollection<ImageViewModel>();
        public ObservableCollection<ImageViewModel> Images { get { return _images; } }
        #endregion
    }
}

画面にドラッグアンドドロップされたらImageViewModelを作成してImagesコレクションに追加するようにしたいので、FileInfo[]とドロップされた場所を受け取る感じのメソッドをMainPageViewModelに追加します。

using System.Windows;
using System.IO;
using System.Collections.ObjectModel;
using System.Windows.Media.Imaging;

namespace ImageViewer
{
    public class MainPageViewModel : ViewModelBase
    {
        #region 画面に表示する画像
        private ObservableCollection<ImageViewModel> _images = new ObservableCollection<ImageViewModel>();
        public ObservableCollection<ImageViewModel> Images { get { return _images; } }
        #endregion

        // 引数で渡されたファイルを読み込んで、pointの場所に設定する
        public void AddImages(FileInfo[] infos, Point point)
        {
            var currentPosition = point;
            foreach (var file in infos)
            {
                AddImage(file, currentPosition);
                // 複数ドロップされたときは、ちょっとずつずらして画像を追加する
                currentPosition = new Point(currentPosition.X + 10, currentPosition.Y + 10);
            }
        }

        public void AddImage(FileInfo file, Point point)
        {
            // ファイルを読み込んで
            using (var stream = file.OpenRead())
            {
                // 画像をImagesに追加する
                var bitmap = new WriteableBitmap(0, 0);
                bitmap.SetSource(stream);
                var model = new ImageViewModel(Images) // ImageViewModel内でコレクションにAddする
                {
                    // 場所と画像のデータをセットする
                    X = (int)point.X,
                    Y = (int)point.Y,
                    Image = bitmap
                };
            }
        }

    }
}

次にXAMLを作りこんでいきます。まずは、ドラッグアンドドロップを受け入れるためGridにAllowDropとDropイベントを登録します。

<UserControl 
    x:Class="ImageViewer.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    d:DesignHeight="300" d:DesignWidth="400">
    
    <Grid x:Name="LayoutRoot" Background="White" AllowDrop="True" Drop="Image_Drop" >
    </Grid>
</UserControl>

Image_Dropメソッドは、先ほどMainPageViewModelに実装したAddImagesに渡すために必要な情報を揃えて、AddImagesを呼ぶだけです。

using System.IO;
using System.Windows;
using System.Windows.Controls;

namespace ImageViewer
{
    public partial class MainPage : UserControl
    {
        public MainPage()
        {
            InitializeComponent();
            // DataContextにViewModelを設定する
            DataContext = new MainPageViewModel();
        }

        // ViewModelを取得する
        public MainPageViewModel Model
        {
            get { return DataContext as MainPageViewModel; }
        }

        private void Image_Drop(object sender, DragEventArgs e)
        {
            // ドロップされた場所を取得して
            var pos = e.GetPosition(e.OriginalSource as UIElement);
            // ドロップされたファイルを取得して
            var files = e.Data.GetData(DataFormats.FileDrop) as FileInfo[];
            if (files == null) return;

            // ViewModelに追加する
            Model.AddImages(files, pos);
            e.Handled = true;
        }

    }
}

次に、画像を表示するようにします。Imagesコレクションにあるデータを画面に表示するのでItemsControlをGridに配置します。表示場所はXとYで指定するのでItemsControlのItemsPanelをCanvasにします。表示するデータは、画像なのでItemTemplateをImageに設定してRenderTransformのXとYにImageViewModleのXとYをバインドする形で表示場所をViewModelと同期させています。

<UserControl 
    x:Class="ImageViewer.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    d:DesignHeight="300" d:DesignWidth="400">
    
    <Grid x:Name="LayoutRoot" Background="White" AllowDrop="True" Drop="Image_Drop" >
        <ItemsControl ItemsSource="{Binding Images}">
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <Canvas />
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <Image Source="{Binding Image}">
                        <Image.RenderTransform>
                            <TranslateTransform
                                X="{Binding X}"
                                Y="{Binding Y}" />
                        </Image.RenderTransform>
                    </Image>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
    </Grid>
</UserControl>

この時点で実行すると、画像の移動や削除は出来ませんが、ドロップした画像がドロップした場所に表示されると思います。次に、画像の移動と削除をしてくれるBehaviorを作っていきます。BehaviorはBlendで提供されている基本クラスを使って作るので、Expression Blend 3のSDKをインストールする必要があります。

デフォルトでインストールすると、C:\Program Files\Microsoft SDKs\Expression\Blend 3\Interactivity\Libraries\SilverlightにSystem.Windows.Interactivity.dllがあるので、これをプロジェクトの参照に追加します。
ビヘイビアを作るには、Behavior型を使うのでさっくりと作ってしまいます。基本的にOnAttachedで必要なマウス関係のイベントを登録して、ドラッグされたときに移動距離を算出して移動させているだけになります。右クリック時にはImageViewModelのRemoveメソッドを呼び出して削除処理をしています。

using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Interactivity;

namespace ImageViewer
{
    public class ImageMoveBahavior : Behavior<Image>
    {
        // マウスが押されているかどうかのフラグ
        private bool _mouseDown;
        // 直前のマウスの座標
        private Point _prevPosition;

        protected override void OnAttached()
        {
            base.OnAttached();
            // 各種イベント登録
            base.AssociatedObject.MouseLeftButtonDown += AssociatedObject_MouseLeftButtonDown;
            base.AssociatedObject.MouseMove += AssociatedObject_MouseMove;
            base.AssociatedObject.MouseLeftButtonUp += AssociatedObject_MouseLeftButtonUp;
            base.AssociatedObject.MouseRightButtonDown += AssociatedObject_MouseRightButtonDown;
        }

        protected override void OnDetaching()
        {
            base.OnDetaching();
            // 登録されたイベントを削除
            base.AssociatedObject.MouseLeftButtonDown -= AssociatedObject_MouseLeftButtonDown;
            base.AssociatedObject.MouseMove -= AssociatedObject_MouseMove;
            base.AssociatedObject.MouseLeftButtonUp -= AssociatedObject_MouseLeftButtonUp;
            base.AssociatedObject.MouseRightButtonDown -= AssociatedObject_MouseRightButtonDown;
        }

        // ViewModelを取得する
        private ImageViewModel GetModel()
        {
            return base.AssociatedObject.DataContext as ImageViewModel;
        }

        private void AssociatedObject_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
        {
            // 右クリックされたのでViewModelをRemoveする
            e.Handled = true;
            GetModel().Remove();
        }

        private void AssociatedObject_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            // マウスの左ボタンが押されたのでマウスのキャプチャを開始
            base.AssociatedObject.CaptureMouse();
            // フラグをON
            _mouseDown = true;
            // 現在のマウスの位置を取得する
            _prevPosition = e.GetPosition(null);
        }

        private void AssociatedObject_MouseMove(object sender, MouseEventArgs e)
        {
            // マウスが押されてなかったら何もしない
            if (!_mouseDown) return;

            // マウスの現在地を取得して
            var pos = e.GetPosition(null);
            // 前回との差分を取得してViewModleのMoveメソッドを呼ぶ
            GetModel().Move(
                (int)(pos.X - _prevPosition.X),
                (int)(pos.Y - _prevPosition.Y));
            _prevPosition = pos;
        }

        private void AssociatedObject_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            // マウスの左ボタンが離されたのでマウスのキャプチャを終了
            base.AssociatedObject.ReleaseMouseCapture();
            // フラグもOFFにする
            _mouseDown = false;
        }

    }
}

ビヘイビアが出来たので、Imageコントロールに登録します。

<UserControl 
    x:Class="ImageViewer.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
    xmlns:l="clr-namespace:ImageViewer"
    mc:Ignorable="d"
    d:DesignHeight="300" d:DesignWidth="400">
    
    <Grid x:Name="LayoutRoot" Background="White" AllowDrop="True" Drop="Image_Drop" >
        <ItemsControl ItemsSource="{Binding Images}">
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <Canvas />
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <Image Source="{Binding Image}">
                        <!-- ビヘイビアを登録 -->
                        <i:Interaction.Behaviors>
                            <l:ImageMoveBahavior />
                        </i:Interaction.Behaviors>
                        <Image.RenderTransform>
                            <TranslateTransform
                                X="{Binding X}"
                                Y="{Binding Y}" />
                        </Image.RenderTransform>
                    </Image>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
    </Grid>
</UserControl>

実行すると、最初に示した動きになっていると思います。
完成系は、以下の場所からダウンロードできます(VWD2010 EE英語版 + SL4 Tools RC2で作成してます)
http://cid-c0989b857f2f850c.skydrive.live.com/self.aspx/%e5%85%ac%e9%96%8b/samples/ImageViewer.zip