概要
WPFでGoogleカレンダー的な左上に日付があって各日付に何かしら表示できるものが欲しくなったが、標準のカレンダーそのままでは、置くスペースがないため、少しカスタムしてみました。
完成品
こんな感じです。
xamlは以下の通りです。
<Calendar x:Name="rootCalendar" HorizontalAlignment="Center" Language="ja-JP" VerticalAlignment="Center" Margin="5" >
<Calendar.Background>White</Calendar.Background>
<Calendar.CalendarDayButtonStyle>
<Style TargetType="{x:Type CalendarDayButton}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type CalendarDayButton}">
<Grid Background="White">
<Border BorderThickness="1" BorderBrush="Turquoise">
<StackPanel MinHeight="80" MinWidth="80" >
<TextBlock Text="{Binding StringFormat={}{0:dd}}" Margin="2" FontSize="16"/>
<ItemsControl >
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Margin="2" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</StackPanel>
</Border>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Calendar.CalendarDayButtonStyle>
</Calendar>
日付の下に文字列を表示させてみた
xamlのの下にを追加し、MultiBindingを追加することで画像や文字列を表示させることができます。
こんな感じです。
左に日ごとに表示されたカレンダーと右にカレンダーにバインドされているコレクションの一覧を表示しています。
ただ、今のままだと複数同じ日に表示された場合、下にもぐってしまうため、スクロールバーを付けるなどの対策が必要です。
以下に上記の画像で使用したソースを置いておきます。
<Window x:Class="CalendarCustom.MainWindow"
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:local="clr-namespace:CalendarCustom"
mc:Ignorable="d"
Title="MainWindow" Height="754" Width="1014">
<Window.DataContext>
<local:TestViewModel />
</Window.DataContext>
<Window.Resources>
<local:MultiBindingSample x:Key="MultiBindingSample" />
</Window.Resources>
<Grid x:Name="rootGrid" >
<Grid.ColumnDefinitions >
<ColumnDefinition Width="2*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Calendar x:Name="rootCalendar" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="5" >
<Calendar.Background>White</Calendar.Background>
<Calendar.CalendarDayButtonStyle>
<Style TargetType="{x:Type CalendarDayButton}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type CalendarDayButton}">
<Grid Background="White">
<Border BorderThickness="1" BorderBrush="DimGray">
<StackPanel MinHeight="80" MinWidth="80" >
<TextBlock Text="{Binding StringFormat={}{0:dd}}" Margin="2" FontSize="16"/>
<ItemsControl Background="Azure" Height="50" >
<ItemsControl.ItemsSource>
<MultiBinding Converter="{StaticResource MultiBindingSample}" UpdateSourceTrigger="PropertyChanged" Mode="OneWay">
<Binding Path="Date" UpdateSourceTrigger="PropertyChanged"/>
<Binding Path="DataContext.TestCollection" ElementName="rootGrid" />
</MultiBinding>
</ItemsControl.ItemsSource>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Margin="2" Width="70" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type local:TestModel}">
<!--<Image Source="{Binding Image, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}" Height="20" Stretch="Uniform" />-->
<TextBlock Text="{Binding DisplayItem, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}" FontSize="16" Foreground="Black" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Border>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Calendar.CalendarDayButtonStyle>
</Calendar>
<DataGrid Grid.Column="1" Margin="5,40" ItemsSource="{Binding TestCollection}" AutoGenerateColumns="False" IsReadOnly="True">
<DataGrid.Columns>
<DataGridTextColumn Header="名前" Binding="{Binding DisplayItem , UpdateSourceTrigger=PropertyChanged}" />
<DataGridTextColumn Header="日付" Binding="{Binding DisplayDate, StringFormat={}{0:yyyy/MM/dd(ddd)}}"/>
</DataGrid.Columns>
</DataGrid>
</Grid>
</Window>
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace CalendarCustom
{
public class TestViewModel : INotifyPropertyChanged
{
public ObservableCollection<TestModel> TestCollection { get; private set; }
public TestViewModel()
{
this.TestCollection = new ObservableCollection<TestModel>()
{
{new TestModel("first", new DateTime(2018, 11, 20)) },
{new TestModel("second", new DateTime(2018, 11, 20)) },
{new TestModel("third", new DateTime(2018, 11, 23)) },
{new TestModel("forth", new DateTime(2018, 11, 28)) },
{new TestModel("fifth", new DateTime(2018, 12, 20)) },
{new TestModel("sixth", new DateTime(2018, 10, 20)) },
};
this.RaisePropertyChanged(nameof(this.TestCollection));
}
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
/// <summary>
/// テストサンプル
/// </summary>
public class TestModel : INotifyPropertyChanged
{
private string _displayItem;
/// <summary>
/// 表示するもの(今回は文字列)
/// </summary>
public string DisplayItem
{
get { return _displayItem; }
private set
{
_displayItem = value;
this.RaisePropertyChanged(nameof(this.DisplayItem));
}
}
private DateTime _displayDate;
/// <summary>
/// 表示したい日付
/// </summary>
public DateTime DisplayDate
{
get { return _displayDate; }
private set
{
_displayDate = value;
this.RaisePropertyChanged(nameof(this.DisplayDate));
}
}
/// <summary>
/// ctor
/// </summary>
public TestModel(string displayItem, DateTime displayDate)
{
this.DisplayItem = displayItem;
this.DisplayDate = displayDate;
}
public TestModel()
{
}
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
using System;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
using System.Windows;
using System.Windows.Data;
namespace CalendarCustom
{
public class MultiBindingSample : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
var targetDate = (DateTime)values[0];
if (values[1] is ObservableCollection<TestModel> testCollection)
{
var chunkItem = testCollection.Where(x => x.DisplayDate.Date == targetDate.Date);
return chunkItem;
}
return DependencyProperty.UnsetValue;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
MultiBindingについて
以下がMainWindow.xamlでのMultiBindingの箇所です。
<MultiBinding Converter="{StaticResource MultiBindingSample}" UpdateSourceTrigger="PropertyChanged" Mode="OneWay">
<Binding Path="Date" UpdateSourceTrigger="PropertyChanged"/>
<Binding Path="DataContext.TestCollection" ElementName="rootGrid" />
</MultiBinding>
ここで記載している順番にobject型の配列に格納されていきます。
なので、1つ目にはDateプロパティ(カレンダーの日付)
2つ目にはDataContext.TestCollectionが格納されます。
ここで格納された配列が、MultiBindingのConverterプロパティで指定されているクラス(MultiBindingSample)のConvertメソッドのvaluesに当たるものになります。
public class MultiBindingSample : IMultiValueConverter
{
// ↓ここです
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
var targetDate = (DateTime)values[0];
// ----------------省略----------------
まとめ
カレンダーをカスタムするのは、初心者が下手に手を出すと大変だということがわかりました。
参考にさせて頂いたサイト
- ItemsControl 攻略 ~ 外観のカスタマイズ
- WPFのCalendarコントロールをカスタマイズ
- WPF の Calendar コントロールで休日を赤くする
- Calendarの日付表示をカスタマイズするには?
- MultiBindingの使い方と使いどころ
- MultiBinding Class
動作環境
Windows 10
Visual Studio 2017
.NET Framework 4.6