WFPのListBoxでカードUIを作成する

WPFでListBoxを使ってカードUIを作成する方法です。

作成したものは以下のような外観になります。

f:id:Takachan:20171129001551p:plain

リサイズすると横幅に合わせて列数が変わります。

f:id:Takachan:20171129001557p:plain

f:id:Takachan:20171129001603p:plain

よこに広げると以下のように並び変えられます。

f:id:Takachan:20171129001613p:plain

カードの構造

XAMLを貼る前に構造はこんな感じになっています。色がついてる部分はStackPanel.BackgroundをViewModelのBarColor:Colorでバインドして塗りつぶしています。

f:id:Takachan:20171204235306p:plain

XAML

全体のコードは以下の通りです。

<ListBox Margin="10"
                 Padding="10"
                 Background="Transparent"
                 ScrollViewer.HorizontalScrollBarVisibility="Disabled"
                 BorderThickness="0"
                 ItemsSource="{Binding ItemsSource}">
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <StackPanel Margin="5 6 7 7" Width="180" Background="White">
                <StackPanel.Effect>
                    <DropShadowEffect Color="#D7D7D7" BlurRadius="2" ShadowDepth="3" Direction="315"/>
                </StackPanel.Effect>
                <TextBlock Padding="20" Foreground="#808080" Height="175" Text="{Binding Message}"/>
                <StackPanel Height="5" Background="{Binding BarColor}"/>
            </StackPanel>
        </DataTemplate>
    </ItemsControl.ItemTemplate>

    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <WrapPanel Orientation="Horizontal" IsItemsHost="True"/>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>

    <ItemsControl.ItemContainerStyle>
        <Style TargetType="ListBoxItem">
            <Setter Property="OverridesDefaultStyle" Value="True" />
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type ContentControl}">
                        <Border Background="{TemplateBinding Background}">
                            <ContentPresenter />
                        </Border>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
            <Setter Property="Focusable" Value="False"/>
        </Style>
    </ItemsControl.ItemContainerStyle>
</ListBox>

横幅に合わせてカードが回り込むようにListBoxのプロパティに以下の属性を指定しています。これを指定しないと右に無限に伸びてしまい回り込みが発生しません。

ScrollViewer.HorizontalScrollBarVisibility="Disabled"

で、上記を指定したうえで、ItemPanelTemplateにWrapパネルを指定します。

<ItemsControl.ItemsPanel>
    <ItemsPanelTemplate>
        <WrapPanel Orientation="Horizontal" IsItemsHost="True"/>
    </ItemsPanelTemplate>
</ItemsControl.ItemsPanel>

カード自身の見た目はDataTemplateで指定しています。ここでは一番外側のStackPanelに影をつけるためにEffectにDropShadowEffectをつけています。これをつけるとだいぶカードっぽくなります。

<DataTemplate>
    <StackPanel Margin="5 6 7 7" Width="180" Background="White">
        <StackPanel.Effect>
            <DropShadowEffect Color="#D7D7D7" BlurRadius="2" ShadowDepth="3" Direction="315"/>
        </StackPanel.Effect>
        <TextBlock Padding="20" Foreground="#808080" Height="175" Text="{Binding Message}"/>
        <StackPanel Height="5" Background="{Binding BarColor}"/>
    </StackPanel>
</DataTemplate>

ItemsControl.ItemContainerStyleの部分は、Windows10で要素にMouseOverエフェクトが発生して、外観が変になるので、自分で描画するようにするおまじないです。OverridesDefaultStyleとTemplateを両方指定しないと要素が表示されないなど発生するので何も考えずコピペでも大丈夫です。

ViewModel

参考までに上記コードにバインドするViewModelを以下に張り付けておきます。

/// <summary>
/// MainWindow.xaml の相互作用ロジック
/// </summary>
public partial class MainWindow : Window
{
    public MainWindow()
    {
        this.InitializeComponent();

        var vm = new MainViewModel();
        vm.ItemsSource.Add(new CardItem() { 
            Message = "aaaaaaaa", BarColor = new SolidColorBrush(Color.FromRgb(0x45, 0x6D, 0xEE)) });
        vm.ItemsSource.Add(new CardItem() { 
            Message = "bbbbbbbb", BarColor = new SolidColorBrush(Color.FromRgb(0x3E, 0xB3, 0x47)) });
        vm.ItemsSource.Add(new CardItem() { 
            Message = "cccccccc", BarColor = new SolidColorBrush(Color.FromRgb(0xF8, 0xBF, 0x4C)) });
        vm.ItemsSource.Add(new CardItem() { 
            Message = "dddddddd", BarColor = new SolidColorBrush(Color.FromRgb(0xE2, 0x68, 0x5D)) });

        this.DataContext = vm;
    }
}

public class MainViewModel : ViewModelBase
{
    public ObservableCollection<CardItem> ItemsSource { get; set; } = new ObservableCollection<CardItem>();
}

/// <summary>
/// カードのデータを表します。
/// </summary>
public class CardItem : Bindable
{
    /// <summary>
    /// カードのメッセージを設定または取得します。
    /// </summary>
    public string Message { get; set; }

    /// <summary>
    /// カードの色を設定または取得します。
    /// </summary>
    public SolidColorBrush BarColor { get; set; }
}

基本的にここから少し改変すれば大抵の表示はできるようになるので必要に応じて書き換えてください。

おまけ

もう1つだけカードデザイン例を置いておきます。有名なThe Ultimate Web Code Generatorさんのサイトデザインをリスペクトしてみました。

f:id:Takachan:20171205235559p:plain

前述の内容と同じくDetaTemplateを以下のように指定しています。

<DataTemplate>
    <StackPanel Margin="16 12" Width="270" Background="White">
        <StackPanel.Effect>
            <DropShadowEffect Color="#000000" Opacity="0.2" BlurRadius="8" ShadowDepth="6" Direction="315"/>
        </StackPanel.Effect>
        <StackPanel Height="164" Background="{Binding BarColor}">
            <TextBlock Margin="20 110 0 0" FontSize="24" Text="{Binding Title}" Foreground="White"></TextBlock>
        </StackPanel>
        <TextBlock Margin="20 20 20 0" Height="98" TextWrapping="WrapWithOverflow" FontSize="14" LineHeight="18" Text="{Binding Message}"/>
        <Border BorderThickness="0 1 0 0" BorderBrush="#E5E5E5">
            <TextBlock Margin="20 18 0 0" Foreground="{Binding BarColor}" Height="42" FontSize="14" Text="{Binding Footer}"/>
        </Border>
    </StackPanel>
</DataTemplate>

またバインドしているデータですが、1枚のカードを表すViewModelを以下のように定義し

/// <summary>
/// カードのデータを表す
/// </summary>
public class CardItem : Bindable
{
    /// <summary>
    /// カードのタイトル
    /// </summary>
    public string Title { get; set; }

    /// <summary>
    /// カードのメッセージ
    /// </summary>
    public string Message { get; set; }

    /// <summary>
    /// カードの色
    /// </summary>
    public SolidColorBrush BarColor { get; set; }

    /// <summary>
    /// フッターメッセージ
    /// </summary>
    public string Footer { get; set; }
}

MainWindowのコンストラクターで以下のようにデータを設定しています。

public partial class MainWindow : Window
{
    public MainWindow()
    {
        this.InitializeComponent();

        var vm = new MainViewModel();

        vm.ItemsSource.Add(new CardItem()
        {
            Title = "CSS(3)",
            Message = "Generate the most commonaly used and most required CSS for your website.",
            Footer = "CSS GENERATOR",
            BarColor = new SolidColorBrush(Color.FromRgb(0x95, 0x75, 0xCD))
        });

        vm.ItemsSource.Add(new CardItem()
        {
            Title = "HTML(5)",
            Message = "Generate the most useful HTML elements for your website.",
            Footer = "HTML GENERATOR",
            BarColor = new SolidColorBrush(Color.FromRgb(0x4F, 0xC3, 0xF7))
        });
        // 以下略...