我喜欢 Evernote(Windows 版)中的标记控件,想知道那里是否有类似的东西?我只能找到标签云控件。
具体来说,我喜欢自由格式的输入,就像在文本框中查找并显示与我输入的内容匹配的标签的 Intellisense 样式一样。当我选择一个标签时,文本被一个代表标签的按钮替换,按钮的文本是标签文本。
更新:添加截图
添加新标签
查看现有标签并单击“x”删除标签
这似乎是一个非常好的练习,所以我尝试构建这个控件。我没有彻底测试它,如果你想使用它并需要进一步的帮助,请告诉我。
示例用法:
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication1"
xmlns:s="clr-namespace:System;assembly=mscorlib"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<local:ViewModel />
</Window.DataContext>
<Grid>
<!-- todo: implement ICommand properties on EvernoteTagControl to allow easy binding to the viewmodel. Alternatively, the user could use a behavior to handle TagClick, and if necessary TagAdded/TagRemoved -->
<local:EvernoteTagControl ItemsSource="{Binding SelectedTags}" TagClick="TagControl_TagClick" >
<local:EvernoteTagControl.AllTags>
<s:String>read</s:String>
<s:String>receipt</s:String>
<s:String>recipe</s:String>
<s:String>research</s:String>
<s:String>restaurants</s:String>
</local:EvernoteTagControl.AllTags>
</local:EvernoteTagControl>
</Grid>
</Window>
视图模型:
using System.Collections.Generic;
using System.ComponentModel;
namespace WpfApplication1
{
public class ViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private List<EvernoteTagItem> _selectedTags = new List<EvernoteTagItem>();
public List<EvernoteTagItem> SelectedTags
{
get { return _selectedTags; }
set
{
_selectedTags = value;
if (_selectedTags != value)
OnPropertyChanged("SelectedTags");
}
}
public ViewModel()
{
this.SelectedTags = new List<EvernoteTagItem>() { new EvernoteTagItem("news"), new EvernoteTagItem("priority") };
}
private void OnPropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
印象笔记标签控件:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
namespace WpfApplication1
{
[TemplatePart(Name = "PART_CreateTagButton", Type = typeof(Button))]
public class EvernoteTagControl : ListBox
{
public event EventHandler<EvernoteTagEventArgs> TagClick;
public event EventHandler<EvernoteTagEventArgs> TagAdded;
public event EventHandler<EvernoteTagEventArgs> TagRemoved;
static EvernoteTagControl()
{
// lookless control, get default style from generic.xaml
DefaultStyleKeyProperty.OverrideMetadata(typeof(EvernoteTagControl), new FrameworkPropertyMetadata(typeof(EvernoteTagControl)));
}
public EvernoteTagControl()
{
//// some dummy data, this needs to be provided by user
//this.ItemsSource = new List<EvernoteTagItem>() { new EvernoteTagItem("receipt"), new EvernoteTagItem("restaurant") };
//this.AllTags = new List<string>() { "recipe", "red" };
}
// AllTags
public List<string> AllTags { get { return (List<string>)GetValue(AllTagsProperty); } set { SetValue(AllTagsProperty, value); } }
public static readonly DependencyProperty AllTagsProperty = DependencyProperty.Register("AllTags", typeof(List<string>), typeof(EvernoteTagControl), new PropertyMetadata(new List<string>()));
// IsEditing, readonly
public bool IsEditing { get { return (bool)GetValue(IsEditingProperty); } internal set { SetValue(IsEditingPropertyKey, value); } }
private static readonly DependencyPropertyKey IsEditingPropertyKey = DependencyProperty.RegisterReadOnly("IsEditing", typeof(bool), typeof(EvernoteTagControl), new FrameworkPropertyMetadata(false));
public static readonly DependencyProperty IsEditingProperty = IsEditingPropertyKey.DependencyProperty;
public override void OnApplyTemplate()
{
Button createBtn = this.GetTemplateChild("PART_CreateTagButton") as Button;
if (createBtn != null)
createBtn.Click += createBtn_Click;
base.OnApplyTemplate();
}
/// <summary>
/// Executed when create new tag button is clicked.
/// Adds an EvernoteTagItem to the collection and puts it in edit mode.
/// </summary>
void createBtn_Click(object sender, RoutedEventArgs e)
{
var newItem = new EvernoteTagItem() { IsEditing = true };
AddTag(newItem);
this.SelectedItem = newItem;
this.IsEditing = true;
}
/// <summary>
/// Adds a tag to the collection
/// </summary>
internal void AddTag(EvernoteTagItem tag)
{
if (this.ItemsSource == null)
this.ItemsSource = new List<EvernoteTagItem>();
((IList)this.ItemsSource).Add(tag); // assume IList for convenience
this.Items.Refresh();
if (TagAdded != null)
TagAdded(this, new EvernoteTagEventArgs(tag));
}
/// <summary>
/// Removes a tag from the collection
/// </summary>
internal void RemoveTag(EvernoteTagItem tag, bool cancelEvent = false)
{
if (this.ItemsSource != null)
{
((IList)this.ItemsSource).Remove(tag); // assume IList for convenience
this.Items.Refresh();
if (TagRemoved != null && !cancelEvent)
TagRemoved(this, new EvernoteTagEventArgs(tag));
}
}
/// <summary>
/// Raises the TagClick event
/// </summary>
internal void RaiseTagClick(EvernoteTagItem tag)
{
if (this.TagClick != null)
TagClick(this, new EvernoteTagEventArgs(tag));
}
}
public class EvernoteTagEventArgs : EventArgs
{
public EvernoteTagItem Item { get; set; }
public EvernoteTagEventArgs(EvernoteTagItem item)
{
this.Item = item;
}
}
}
印象笔记标签项:
using System.Collections;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
namespace WpfApplication1
{
[TemplatePart(Name = "PART_InputBox", Type = typeof(AutoCompleteBox))]
[TemplatePart(Name = "PART_DeleteTagButton", Type = typeof(Button))]
[TemplatePart(Name = "PART_TagButton", Type = typeof(Button))]
public class EvernoteTagItem : Control
{
static EvernoteTagItem()
{
// lookless control, get default style from generic.xaml
DefaultStyleKeyProperty.OverrideMetadata(typeof(EvernoteTagItem), new FrameworkPropertyMetadata(typeof(EvernoteTagItem)));
}
public EvernoteTagItem() { }
public EvernoteTagItem(string text)
: this()
{
this.Text = text;
}
// Text
public string Text { get { return (string)GetValue(TextProperty); } set { SetValue(TextProperty, value); } }
public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(EvernoteTagItem), new PropertyMetadata(null));
// IsEditing, readonly
public bool IsEditing { get { return (bool)GetValue(IsEditingProperty); } internal set { SetValue(IsEditingPropertyKey, value); } }
private static readonly DependencyPropertyKey IsEditingPropertyKey = DependencyProperty.RegisterReadOnly("IsEditing", typeof(bool), typeof(EvernoteTagItem), new FrameworkPropertyMetadata(false));
public static readonly DependencyProperty IsEditingProperty = IsEditingPropertyKey.DependencyProperty;
/// <summary>
/// Wires up delete button click and focus lost
/// </summary>
public override void OnApplyTemplate()
{
AutoCompleteBox inputBox = this.GetTemplateChild("PART_InputBox") as AutoCompleteBox;
if (inputBox != null)
{
inputBox.LostFocus += inputBox_LostFocus;
inputBox.Loaded += inputBox_Loaded;
}
Button btn = this.GetTemplateChild("PART_TagButton") as Button;
if (btn != null)
{
btn.Loaded += (s, e) =>
{
Button b = s as Button;
var btnDelete = b.Template.FindName("PART_DeleteTagButton", b) as Button; // will only be found once button is loaded
if (btnDelete != null)
{
btnDelete.Click -= btnDelete_Click; // make sure the handler is applied just once
btnDelete.Click += btnDelete_Click;
}
};
btn.Click += (s, e) =>
{
var parent = GetParent();
if (parent != null)
parent.RaiseTagClick(this); // raise the TagClick event of the EvernoteTagControl
};
}
base.OnApplyTemplate();
}
/// <summary>
/// Handles the click on the delete glyph of the tag button.
/// Removes the tag from the collection.
/// </summary>
void btnDelete_Click(object sender, RoutedEventArgs e)
{
var item = FindUpVisualTree<EvernoteTagItem>(sender as FrameworkElement);
var parent = GetParent();
if (item != null && parent != null)
parent.RemoveTag(item);
e.Handled = true; // bubbling would raise the tag click event
}
/// <summary>
/// When an AutoCompleteBox is created, set the focus to the textbox.
/// Wire PreviewKeyDown event to handle Escape/Enter keys
/// </summary>
/// <remarks>AutoCompleteBox.Focus() is broken: http://stackoverflow.com/questions/3572299/autocompletebox-focus-in-wpf</remarks>
void inputBox_Loaded(object sender, RoutedEventArgs e)
{
AutoCompleteBox acb = sender as AutoCompleteBox;
if (acb != null)
{
var tb = acb.Template.FindName("Text", acb) as TextBox;
if (tb != null)
tb.Focus();
// PreviewKeyDown, because KeyDown does not bubble up for Enter
acb.PreviewKeyDown += (s, e1) =>
{
var parent = GetParent();
if (parent != null)
{
switch (e1.Key)
{
case (Key.Enter): // accept tag
parent.Focus();
break;
case (Key.Escape): // reject tag
parent.Focus();
parent.RemoveTag(this, true); // do not raise RemoveTag event
break;
}
}
};
}
}
/// <summary>
/// Set IsEditing to false when the AutoCompleteBox loses keyboard focus.
/// This will change the template, displaying the tag as a button.
/// </summary>
void inputBox_LostFocus(object sender, RoutedEventArgs e)
{
this.IsEditing = false;
var parent = GetParent();
if (parent != null)
parent.IsEditing = false;
}
private EvernoteTagControl GetParent()
{
return FindUpVisualTree<EvernoteTagControl>(this);
}
/// <summary>
/// Walks up the visual tree to find object of type T, starting from initial object
/// http://www.codeproject.com/Tips/75816/Walk-up-the-Visual-Tree
/// </summary>
private static T FindUpVisualTree<T>(DependencyObject initial) where T : DependencyObject
{
DependencyObject current = initial;
while (current != null && current.GetType() != typeof(T))
{
current = VisualTreeHelper.GetParent(current);
}
return current as T;
}
}
}
主题/generic.xaml:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication1"
xmlns:tkInput="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Input.Toolkit">
<SolidColorBrush x:Key="HighlightBrush" Color="DodgerBlue" />
<!-- EvernoteTagControl default style -->
<Style TargetType="{x:Type local:EvernoteTagControl}">
<Style.Resources>
<SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}" Color="White"/>
<SolidColorBrush x:Key="{x:Static SystemColors.ControlBrushKey}" Color="White" />
<LinearGradientBrush x:Key="IconBrush" EndPoint="0,1">
<GradientStop Color="#5890f0" Offset="0" />
<GradientStop Color="#0351d7" Offset="1" />
</LinearGradientBrush>
</Style.Resources>
<Setter Property="FocusVisualStyle" Value="{x:Null}" />
<Setter Property="VerticalAlignment" Value="Top" />
<Setter Property="Margin" Value="5" />
<Setter Property="MinHeight" Value="25" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="SnapsToDevicePixels" Value="True" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:EvernoteTagControl}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Path Grid.Column="0" Margin="2" Fill="{StaticResource IconBrush}" Height="19" Stretch="Uniform" Data="M 50.535714,0.44196425 0.00446427,34.754464 l 0,106.906246 100.71874573,0 0,-107.124996 L 50.535714,0.44196425 z m 0.1875,21.21874975 c 6.311826,0 11.40625,5.094424 11.40625,11.40625 0,6.311826 -5.094424,11.4375 -11.40625,11.4375 -6.311826,0 -11.4375,-5.125674 -11.4375,-11.4375 0,-6.311826 5.125674,-11.40625 11.4375,-11.40625 z" />
<ItemsPresenter Grid.Column="1" />
<Button Margin="5,0,0,0" Grid.Column="2" Content="Click to add tag..." x:Name="PART_CreateTagButton">
<Button.Template>
<ControlTemplate TargetType="Button">
<ContentPresenter TextElement.Foreground="#FF555555" VerticalAlignment="Center" />
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Cursor" Value="Hand" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Button.Template>
</Button>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsEditing" Value="True">
<Setter TargetName="PART_CreateTagButton" Property="Visibility" Value="Collapsed" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
<Setter Property="ItemContainerStyle">
<Setter.Value>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="FocusVisualStyle" Value="{x:Null}" />
</Style>
</Setter.Value>
</Setter>
<Setter Property="ItemsPanel" >
<Setter.Value>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- EvernoteTagItem default style -->
<Style TargetType="{x:Type local:EvernoteTagItem}">
<Setter Property="FocusVisualStyle" Value="{x:Null}" />
<Setter Property="MinWidth" Value="50" />
<Setter Property="Margin" Value="0,0,2,0" />
<Setter Property="Padding" Value="5,2,0,2" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:EvernoteTagItem}">
<Button x:Name="PART_TagButton" Content="{TemplateBinding Text}" Margin="{TemplateBinding Margin}" Padding="{TemplateBinding Padding}">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border Margin="{TemplateBinding Margin}" Padding="{TemplateBinding Padding}" BorderBrush="Gray" BorderThickness="1" CornerRadius="2" Background="#01FFFFFF">
<Grid >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ContentPresenter VerticalAlignment="Center" HorizontalAlignment="Left" Margin="0,0,0,2" />
<Button x:Name="PART_DeleteTagButton" Grid.Column="1" Margin="3,0" VerticalAlignment="Center" HorizontalAlignment="Right" Visibility="Hidden" >
<Button.Template>
<ControlTemplate>
<Grid Height="10" Width="10" Background="#01FFFFFF" >
<Path Stretch="Uniform" ClipToBounds="True" Stroke="{StaticResource HighlightBrush}" StrokeThickness="2" Data="M 85.364473,6.9977109 6.0640998,86.29808 6.5333398,85.76586 M 6.9926698,7.4977169 86.293043,86.79809 85.760823,86.32885" />
</Grid>
</ControlTemplate>
</Button.Template>
</Button>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Foreground" Value="{StaticResource HighlightBrush}" />
<Setter TargetName="PART_DeleteTagButton" Property="Visibility" Value="Visible" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Button.Template>
</Button>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsEditing" Value="True">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:EvernoteTagItem}">
<tkInput:AutoCompleteBox x:Name="PART_InputBox"
Text="{Binding Text, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
ItemsSource="{Binding AllTags, RelativeSource={RelativeSource AncestorType={x:Type local:EvernoteTagControl}}}"
IsTextCompletionEnabled="True"
/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Trigger>
</Style.Triggers>
</Style>
</ResourceDictionary>
以下是我将如何创建此控件。
此控件将包含以下主要组件:(1)AutoCompleteTextBox
或AutoCompleteComboBox
(2) 您描述的“按钮”控件 (3) 用于保存应用标签的 UI 集合。保存应用标签的AutoCompleteTextBox
集合将提前放置在您选择的布局容器中。
首先,我们可以利用AutoCompleteTextBox
orAutoCompleteComboBox
在用户键入时为我们提供 Intellisense 样式的选项。接下来,我们“监听”用户从下拉列表中选择呈现的标签并动态创建一个新的“按钮”控件(我们可以提前为其创建一个 UserControl/CustomControl,但我们需要“新建”至少一个)。“按钮”的文本将包含AutoCompleteTextBox
. 最后,我们将新的“按钮”插入到包含所有当前应用标签的 ListBox(或其他相关的 UI 集合类型)中。
那里有一些AutoCompleteTextBox
控件,但我将描述如何使用这个 CodeProject one。此示例项目展示了如何使用常规TextBox
、anAutoCompleteComboBox
或AutoCompleteTextBox
来实现智能感知样式选项。该项目的核心部分实际上是AutoCompleteManager
和一个 DataProvider,类型为IAutoCompleteDataProvider
,以及IAutoAppendDataProvider
.
在描述更多细节之前,这里是这个控件的一些截图AutoCompleteTextBox
(注意我使用的 ListBoxItems 的样式与原始作者提供的样式不同)。这个控件的AutoAppend
属性是一个很好的接触(在这个例子中它是打开的,所以在我开始输入当前匹配后自动为我“完成”我的话)。只输入一个“I”后:
将鼠标悬停在“印第安纳州”上后:
点击“印第安纳州”后:
由于该项目中的代码为我们处理下拉选项,因此我们现在需要“监听”用户何时从下拉列表中选择项目并相应地创建新的“按钮”控件。我正在考虑我们需要处理 的两个主要案例。
第一种情况是用户用鼠标从列表中选择一个项目。为了处理这个问题,我们可以在451 行的MouseLeftButtonUp
处理程序中插入代码来创建新的“按钮”控件:AutoCompleteManager.cs
private void ListBox_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
ListBoxItem item = null;
var d = e.OriginalSource as DependencyObject;
while (d != null)
{
if (d is ListBoxItem)
{
item = d as ListBoxItem;
break;
}
d = VisualTreeHelper.GetParent(d);
}
if (item != null)
{
_popup.IsOpen = false;
UpdateText(item.Content as string, true);
// User has selected an item with the mouse...
// ** Add your new code HERE... something like:
//
// TagButton tagButton = new TagButton(_textBox.Text); // _textBox is the TextBox to which the AutoCompleteManager has been applied
// _autoCompleteTagControl.TagContainer.Add(tagButton); // _autoCompleteTagControl would be the control that we're making... it contains out other controls - I'm assuming we've passed it in or made it available.
}
}
第二种情况是用户通过按回车键从列表中选择一个项目。为了处理这个问题,我们可以在291 行附近的TextBox_PreviewKeyDown
处理程序中插入类似的新代码:AutoCompleteManager.cs
private void TextBox_PreviewKeyDown(object sender, KeyEventArgs e)
{
_supressAutoAppend = e.Key == Key.Delete || e.Key == Key.Back;
if (!_popup.IsOpen)
{
return;
}
if (e.Key == Key.Enter)
{
_popup.IsOpen = false;
_textBox.SelectAll();
// User has selected an item by hitting the enter key...
// ** Add your new code HERE to create new TagButton, etc.
}
// ...
}
如果您决定使用AutoCompleteTextBox
我提到的 CodeProject 中的,您可能需要应用几个修复程序。在将所有内容导入一个更大的项目后,我遇到了两件小事(仅在运行包含的示例项目时没有发生)。第一个是这个绑定错误:
System.Windows.Data Error: 4 : Cannot find source for binding with reference 'RelativeSource FindAncestor, AncestorType='System.Windows.Controls.ItemsControl', AncestorLevel='1''. BindingExpression:Path=HorizontalContentAlignment; DataItem=null; target element is 'ListBoxItem' (Name=''); target property is 'HorizontalContentAlignment' (type 'HorizontalAlignment')
其他人在使用 ListBoxes 时遇到过这个问题,在此处进行了描述。正如该帖子的答案之一所暗示的那样,我能够在我的 Style 中包含 HorizontalContentAligment 和 VerticalContentAlignment 的显式样式设置来解决这个问题。
第二个问题发生在AutoCompleteTextBox
包含标签的应用程序中。当您在 TabControl 中有控件时,嵌套控件将经常引发其 Loaded 事件 - 每次单击包含该控件的选项卡时至少触发一次。这导致对该方法的意外额外调用AutoCompleteManager's AttachTextBox()
,导致debug.Assert()
失败并发生异常(原作者假设 Loaded 事件只会引发一次)。到目前为止,我需要做的唯一修复就是在AutoCompeteTextBox.cs
. 我刚刚添加了一个_isInitialized
标志以确保AttachTextBox
只被调用一次:
void AutoCompleteTextBox_Loaded(object sender, RoutedEventArgs e)
{
if (! _isInitialized)
{
_acm.AttachTextBox(this);
_isInitialized = true;
}
}
使用这种方法应该允许您创建一个行为类似于您所描述的控件。