上のようなタグを複数個選択して、文字列として設定できるようなタグ入力用のコントロールを作ってみる。
ComboBox (ComboBox.ItemsPanel) WrapPanel (ComboBox.ItemContainerStyle) Content Presenter (ComboBox.ItemTemplate) CheckBox (CheckBox.Template) Border TextBlock
個々のタグとタグをまとめて管理するコレクションはそれぞれ専用のクラスを作成する。
<Window x:Class="〜〜〜"
〜中略〜
xmlns:tj="clr-namespace:tj.Controls"
>
<tj:MultiTagBox x:Name="tagBox" Height="24" Width="280"
HorizontalAlignment="Left" VerticalAlignment="Center"
DefaultText="タグを選択してください..."
/>
tagBox.ItemsSource = new MultiTagBoxItemCollection(tags);
tagBox.SelectedTags = selectedTags;
各クラスのソースを下記に示す。なお、MultiTagBox コントロール(XAML)の1行目のクラス名(x:Class="tj.Controls.MultiTagBox")にある名前空間は適宜変更すること。
/// <summary>
/// MultiTagBox のタグリストに表示されるタグ項目です。
/// </summary>
public class MultiTagBoxItem : INotifyPropertyChanged
{
/// <summary>
/// タグ名を指定して、MultiTagBoxItem のインスタンスを初期化します。
/// </summary>
/// <param name="t"></param>
public MultiTagBoxItem(string t) { Title = t; }
/// <summary>
/// タグの文字列
/// </summary>
public string Title { get; set; }
private bool _IsSelected = false;
/// <summary>
/// タグが選択されているかどうかを設定または取得します。
/// </summary>
public bool IsSelected { get { return _IsSelected; } set { _IsSelected = value; OnPropertyChanged("IsSelected"); } }
/// <summary>
/// タグ文字列を返します。
/// </summary>
/// <returns></returns>
public override string ToString()
{
return Title;
}
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string name)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(name));
}
}
#endregion
}
/// <summary>
/// MultiTagBox の項目を格納するコレクションです。
/// </summary>
public class MultiTagBoxItemCollection : ObservableCollection<MultiTagBoxItem>
{
/// <summary>
/// タグ文字列のリストを指定して、コレクションを初期化します。
/// </summary>
/// <param name="nodes"></param>
public MultiTagBoxItemCollection(IEnumerable<string> nodes)
: this(nodes.Select(t => new MultiTagBoxItem(t)))
{
}
/// <summary>
/// MultiTagBoxItem のリストを指定して、コレクションを初期化します。
/// </summary>
/// <param name="nodes"></param>
public MultiTagBoxItemCollection(IEnumerable<MultiTagBoxItem> nodes)
: base(nodes)
{
}
/// <summary>
/// 選択されているタグをカンマ区切りで返します。
/// </summary>
/// <returns></returns>
public override string ToString()
{
return ToString(", ");
}
/// <summary>
/// 選択されているタグを、指定した区切り文字列で結合して返します。
/// </summary>
/// <param name="separater"></param>
/// <returns></returns>
public string ToString(string separater)
{
var selectedItems = this.Items.Where(t => t.IsSelected).Select(t2 => t2.Title);
return string.Join(separater, selectedItems);
}
/// <summary>
/// 選択されているタグを、指定した先頭文字列と末尾文字列を付加し、結合して返します。
/// </summary>
/// <param name="head"></param>
/// <param name="tail"></param>
/// <returns></returns>
public string ToString(string head, string tail)
{
return head + ToString(tail + head) + tail;
}
/// <summary>
/// カンマ区切りのタグ文字列から、リストのタグを選択状態にします。
/// </summary>
/// <param name="str"></param>
public void Restore(string str)
{
if (string.IsNullOrEmpty(str))
{
var tags = str.Split(new string[] { "," }, StringSplitOptions.RemoveEmptyEntries);
tags = Array.ConvertAll(tags, (t) => t.Trim());
foreach (var item in this)
{
item.IsSelected = tags.Contains(item.Title);
}
}
}
}
<UserControl x:Class="tj.Controls.MultiTagBox"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Name="UserControl" mc:Ignorable="d"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Height="22" Width="120">
<ComboBox
x:Name="baseComboBox"
IsReadOnly="True"
IsEditable="True"
SnapsToDevicePixels="True"
Text="{Binding ElementName=UserControl, Path=Text, Mode=TwoWay}"
ItemsSource="{Binding ElementName=UserControl, Path=ItemsSource}"
DataContext="{Binding ElementName=UserControl, Path=DataContext}"
DropDownClosed="baseComboBox_DropDownClosed"
>
<!-- ItemsPanel -->
<ComboBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Width="{Binding ElementName=UserControl, Path=Width}" />
</ItemsPanelTemplate>
</ComboBox.ItemsPanel>
<!-- ItemContainerStyle -->
<ComboBox.ItemContainerStyle>
<Style TargetType="ComboBoxItem">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<ContentPresenter/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ComboBox.ItemContainerStyle>
<!-- ItemTemplate -->
<ComboBox.ItemTemplate>
<HierarchicalDataTemplate>
<CheckBox Content="{Binding Title}"
IsChecked="{Binding Path=IsSelected, Mode=TwoWay}"
Tag="{RelativeSource FindAncestor, AncestorType={x:Type ComboBox} }"
Click="CheckBox_Click"
Margin="0"
Padding="0"
Background="Transparent"
>
<CheckBox.Template>
<ControlTemplate>
<Border x:Name="Border" Margin="1" Background="{TemplateBinding Background}" Padding="2,0">
<TextBlock
x:Name="Presenter"
IsHitTestVisible="False"
VerticalAlignment="Center"
HorizontalAlignment="Left"
Text="{Binding Title}"
Foreground="{x:Static SystemColors.WindowTextBrush}"
TextDecorations="None"
/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="CheckBox.IsChecked" Value="true">
<Setter TargetName="Border" Property="Background" Value="SkyBlue"/>
<Setter TargetName="Presenter" Property="TextDecorations" Value="None"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="true">
<Setter TargetName="Presenter" Property="TextDecorations" Value="Underline"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</CheckBox.Template>
</CheckBox>
</HierarchicalDataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</UserControl>
/// <summary>
/// MultiTagBox.xaml の相互作用ロジック
/// </summary>
public partial class MultiTagBox : UserControl
{
public MultiTagBox()
{
InitializeComponent();
}
#region 依存関係プロパティ
/// <summary>
/// 選択候補のタグ一覧のコレクションを設定または取得します。
/// </summary>
public IEnumerable ItemsSource
{
get { return (IEnumerable)GetValue(ItemsSourceProperty); }
set
{
SetValue(ItemsSourceProperty, value);
Items.ToList().ForEach(t => { t.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler(tagBoxItem_PropertyChanged); });
SetText();
}
}
public static readonly DependencyProperty ItemsSourceProperty =
DependencyProperty.Register("ItemsSource", typeof(IEnumerable), typeof(MultiTagBox), new UIPropertyMetadata(null));
/// <summary>
/// 表示されている文字列
/// </summary>
public string Text
{
get { return (string)GetValue(TextProperty); }
private set { SetValue(TextProperty, value); }
}
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register("Text", typeof(string), typeof(MultiTagBox), new UIPropertyMetadata(string.Empty));
/// <summary>
/// デフォルトの文字列
/// </summary>
public string DefaultText
{
get { return (string)GetValue(DefaultTextProperty); }
set { SetValue(DefaultTextProperty, value); }
}
public static readonly DependencyProperty DefaultTextProperty =
DependencyProperty.Register("DefaultText", typeof(string), typeof(MultiTagBox), new UIPropertyMetadata(string.Empty));
#endregion
#region タグ操作
/// <summary>
/// ItemsSource を MultiTagBoxItemCollection として取得します。
/// </summary>
public MultiTagBoxItemCollection Items
{
get
{
if (this.ItemsSource == null) // なければ作る
{
this.ItemsSource = new MultiTagBoxItemCollection(new string[] { });
}
return (this.ItemsSource as MultiTagBoxItemCollection);
}
}
/// <summary>
/// 指定した文字列をタグ一覧に加えます。
/// </summary>
/// <param name="tag"></param>
/// <param name="isSelected"></param>
public void AddItem(string tag, bool isSelected = true)
{
var item = new MultiTagBoxItem(tag) { IsSelected = isSelected };
item.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler(tagBoxItem_PropertyChanged);
Items.Add(item);
SetText();
}
/// <summary>
/// 選択されているタグの一覧を設定または取得します。
/// </summary>
public IEnumerable<string> SelectedTags
{
get { return Items.Where(t => t.IsSelected).Select(t2 => t2.Title).ToList(); }
set
{
Items.ToList().ForEach((t) =>
{
if (value.Any((t2) => t2.Equals(t.Title)))
{
t.IsSelected = true;
}
});
SetText();
}
}
#endregion
#region タグ選択変更時の動作
/// <summary>
/// タグの選択内容をテキストボックスに反映します。
/// </summary>
private void SetText()
{
this.Text = (this.ItemsSource != null) ? this.ItemsSource.ToString() : this.DefaultText;
if (string.IsNullOrEmpty(this.Text))
{
this.Text = this.DefaultText;
}
}
private void CheckBox_Click(object sender, RoutedEventArgs e)
{
SetText();
}
private void baseComboBox_DropDownClosed(object sender, EventArgs e)
{
SetText();
}
private void tagBoxItem_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
SetText();
}
#endregion
}
Kenz Yamada(山田研二)。1984年生。大阪。ちょっとずつ好きなプログラム作ってます。
好きなものはカメラと旅行。ガジェットや身の回り、ちょっとこだわります。
詳しくは Web mixi で。