情報アイランド

「情報を制する者は世界を制す」をモットーに様々な情報を提供することを目指すブログです。現在はプログラミング関連情報が多めですが、投資関連情報も取り扱っていきたいです。

WPFで動画のサムネイルを表示する(1)

下の画像のようにエクスプローラ風ツリービューとリストボックスを使って動画のサムネイル(+ファイル名)を表示するプログラムを作ってみました。

エクスプローラ部分は、CodeProjectのA Multi-Threaded WPF TreeView Explorerを使いました。

動画のサムネイルの生成は前の記事で紹介したThumnailCreaterクラスを使っています。

まずXAMLから。エクスプローラ部分の詳細は上のリンク先を参照してください。

<Window x:Class="WpfApplication1.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:WpfApplication1"
    Title="Window1" Height="600" Width="800" Loaded="Window_Loaded">
    <Grid Name="explorerGrid">
        <Grid.RowDefinitions>
            <RowDefinition Height="*"></RowDefinition>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="200"></ColumnDefinition>
            <ColumnDefinition Width="Auto"></ColumnDefinition>
            <ColumnDefinition Width="*"></ColumnDefinition>
        </Grid.ColumnDefinitions>
        <TreeView Name="treeViewFolder" Grid.Row="0" Grid.Column="0" SelectedItemChanged="treeViewFolder_SelectedItemChanged">
            <TreeView.Resources>
                <Style TargetType="{x:Type TreeViewItem}">
                    <Setter Property="HeaderTemplate">
                        <Setter.Value>
                            <DataTemplate DataType="ContentPresenter">
                                <Grid>
                                    <StackPanel Name="stackPanelImage" Orientation="Horizontal">
                                        <Image Name="imageFolder" Width="20" Height="20"  Stretch="Fill" VerticalAlignment="Center" Source="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type TreeViewItem}}, Path=(local:TreeViewItemProperties.ItemImageName)}"></Image>
                                        <TextBlock Text="{Binding}" Margin="5,0" VerticalAlignment="Center" />
                                        <Button Name="buttonReload" Height="14" VerticalAlignment="Center" Visibility="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type TreeViewItem}}, Path=(local:TreeViewItemProperties.IsCanceled), Converter={x:Static local:vc_BoolToVisiblity.Instance}}" Tag="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type TreeViewItem}}}" Click="buttonReload_Click">
                                            <TextBlock FontSize="9" Text="Reload..." TextAlignment="Center" VerticalAlignment="Center" HorizontalAlignment="Center"></TextBlock>
                                        </Button>
                                        <StackPanel Name="stackPanelLoading" Orientation="Horizontal" Visibility="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type TreeViewItem}}, Path=(local:TreeViewItemProperties.IsLoading), Converter={x:Static local:vc_BoolToVisiblity.Instance}}">
                                            <Grid Height="13"  MinWidth="50" MaxWidth="75" Margin="5,0" >
                                                <ProgressBar Name="progressbarLoading" Height="13"  MinWidth="50" MaxWidth="75" IsIndeterminate="True" HorizontalAlignment="Center" VerticalAlignment="Center"></ProgressBar>
                                                <TextBlock Name="textBlockLoading" Text="Loading..." FontSize="8.6" Margin="5,0" HorizontalAlignment="Center" VerticalAlignment="Center" />
                                            </Grid>
                                            <Button Name="buttonCancelLoad" IsEnabled="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type TreeViewItem}}, Path=(local:TreeViewItemProperties.IsCanceled), Converter={x:Static local:vc_FlipBool.Instance}}" Tag="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type TreeViewItem}}}" Height="14" Click="buttonCancelLoad_Click" VerticalAlignment="Center" >
                                                <TextBlock FontSize="9" Text="Cancel" TextAlignment="Center" VerticalAlignment="Center" HorizontalAlignment="Center"></TextBlock>
                                            </Button>
                                        </StackPanel>
                                    </StackPanel>
                                </Grid>
                            </DataTemplate>
                        </Setter.Value>
                    </Setter>
                </Style>
            </TreeView.Resources>
        </TreeView>
        <GridSplitter Grid.Row="0" Grid.Column="1" HorizontalAlignment="Center" VerticalAlignment="Stretch" ShowsPreview="True" Width="3"></GridSplitter>
        <DockPanel Grid.Row="0" Grid.Column="2">
            <DockPanel DockPanel.Dock="Top" Margin="2">
                <TextBlock DockPanel.Dock="Left" VerticalAlignment="Center" HorizontalAlignment="Center" Text="フィルタ:"></TextBlock>
                <ComboBox Name="comboBoxFilter" SelectedIndex="0" SelectionChanged="comboBoxFilter_SelectionChanged">
                    <ComboBoxItem Name="comboBoxItemAll">全てのファイル(*.*)</ComboBoxItem>
                    <ComboBoxItem Name="comboBoxItemAvi">AVIファイル(*.avi)</ComboBoxItem>
                    <ComboBoxItem Name="comboBoxItemMov">MOVファイル(*.mov;*.qt)</ComboBoxItem>
                    <ComboBoxItem Name="comboBoxItemWmv">WMVファイル(*.wmv)</ComboBoxItem>
                    <ComboBoxItem Name="comboBoxItemFlv">FLVファイル(*.flv)</ComboBoxItem>
                    <ComboBoxItem Name="comboBoxItemMpg">MPGファイル(*.mpg;*.mpeg;*.m1v;*m2v)</ComboBoxItem>
                    <ComboBoxItem Name="comboBoxItemMp4">MP4ファイル(*.mp4;*.m4v;*.m4a)</ComboBoxItem>
                    <ComboBoxItem Name="comboBoxItemMkv">MKVファイル(*.mkv)</ComboBoxItem>
                    <ComboBoxItem Name="comboBoxItemTs">TSファイル(*.ts;*.m2ts)</ComboBoxItem>
                </ComboBox>
            </DockPanel>
            <ListBox Name="listBoxFile">
                <ListBox.Resources>
                    <Style TargetType="{x:Type ListBoxItem}">
                        <Setter Property="ContentTemplate">
                            <Setter.Value>
                                <DataTemplate>
                                    <StackPanel Orientation="Vertical">
                                        <Image Source="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListBoxItem}}, Path=Tag, Converter={x:Static local:vc_PathToImageSource.Instance}}" Width="64" Height="64" VerticalAlignment="Center" HorizontalAlignment="Center"></Image>
                                        <TextBlock Text="{Binding Converter={x:Static local:vc_FullPathToFileName.Instance}}"></TextBlock>
                                    </StackPanel>
                                </DataTemplate>
                            </Setter.Value>
                        </Setter>
                    </Style>
                </ListBox.Resources>
            </ListBox>
        </DockPanel>
    </Grid>
</Window>

構造的にはツリービュー部分とリストボックス+コンボボックス部分に二分されているだけなので分かりにくくはないと思います。

ただ、バインディング周りが少々複雑なので簡単に解説をします。

サムネイルとファイル名を表示するリストボックスはContentTemplateを変更して独自の表示にしています。

その部分を抜き出すと、

<DataTemplate>
    <StackPanel Orientation="Vertical">
        <Image Source="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListBoxItem}}, Path=Tag, Converter={x:Static local:vc_PathToImageSource.Instance}}" Width="64" Height="64" VerticalAlignment="Center" HorizontalAlignment="Center"></Image>
        <TextBlock Text="{Binding Converter={x:Static local:vc_FullPathToFileName.Instance}}"></TextBlock>
    </StackPanel>
</DataTemplate>

垂直方向に要素を配置するスタックパネルの中にImage要素とTextBlock要素を格納しています。

Image要素は複雑なのでTextBlock要素から説明しますと、Textプロパティにバインディングを指定しています。

Text="{Binding Converter={x:Static local:vc_FullPathToFileName.Instance}}"

各ファイルを表すリストボックス項目のコンテンツにはそのファイルのフルパスが格納されていますので、TextBlock要素に必要なのはTextBlock.TextプロパティとListBoxItem.Contentを結び付けることです。この場合ですと、Text={Binding}とだけ書けばTextBlock.Textにコンテンツを渡すことができます。しかし、TextBlock要素に表示したいのはフルパスではなくファイル名だけなので、コンバータを使ってフルパスからファイル名に変換します。コンバータは、vc_FullPathToFileName.Instanceです。vc_FullPathToFileNameクラスの定義を以下に示します。

[ValueConversion(typeof(string), typeof(string))]
public class vc_FullPathToFileName : IValueConverter
{
    public static vc_FullPathToFileName Instance = new vc_FullPathToFileName();

    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return Path.GetFileName((string)value);
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotSupportedException("Cannot convert back");
    }
}

次は、Image要素です。

次回でコードを示しますが、Image要素に表示する動画のサムネイルはコードビハインドで生成されてビットマップファイルとしてプログラムが存在するフォルダの配下に保存されます(一時的に)。そして、最終的に保存先のファイル名がリストボックス項目のTagプロパティに格納されます。

なので、Image要素にサムネイルを表示するために必要なことは、Image.Sourceプロパティとその親のListBoxItem.Tagプロパティを結び付けることです。それは、Image.Sourceプロパティに指定するバインディング式を以下のようにすることで可能です。

Source="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListBoxItem}}, Path=Tag, Converter={x:Static local:vc_PathToImageSource.Instance}}"

RelativeSourceModeFindAncestorにすることでコントロールの階層構造上でルートに向かって要素を探せます。AncestorTypeListBoxItemにすれば、ルートに向かって検索した時に最初に見つかったListBoxItemが返ってきます。これをバインディングのソースにして、パスをTagプロパティにし、コンバータにvc_PathToImageSource.Instance.Instanceを指定しています。このコンバータは渡されたファイル名から適切なファイルを開き画像データを読み込んでそれを返すコンバータで下のようになります。特にピクセルフォーマットをBgra32にすることに注意してください。

[ValueConversion(typeof(string), typeof(ImageSource))]
public class vc_PathToImageSource : IValueConverter
{
    public static vc_PathToImageSource Instance = new vc_PathToImageSource();

    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        string fileName = (string)value;

        if (!File.Exists(fileName))
            return null;

        Bitmap bitmap = (Bitmap)Bitmap.FromFile(fileName);
        BitmapData bitmapData = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height), ImageLockMode.ReadWrite, bitmap.PixelFormat);
        byte[] buffer = new byte[4 * bitmapData.Width * bitmapData.Height];
        Marshal.Copy(bitmapData.Scan0, buffer, 0, buffer.Length);
        BitmapSource bitmapSource = BitmapSource.Create(bitmapData.Width, bitmapData.Height, 96, 96, PixelFormats.Bgra32, null, buffer, 4 * bitmapData.Width);
        bitmap.UnlockBits(bitmapData);
        bitmap.Dispose();

        return bitmapSource;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotSupportedException("Cannot convert back");
    }
}

これで、XAMLとコンバータ部分は終わりです。次回はイベントハンドラの実装です。

(続く...)

pizyumi
プログラミング歴19年のベテランプログラマー。業務システム全般何でも作れます。現在はWeb系の技術を勉強中。
スポンサーリンク

-C#, wpf