microsoft example controles wpf controls

example - ¿Cómo puedo crear un control de etiquetado similar a evernote en wpf?



wpf layout (2)

Aquí es cómo me gustaría crear este control.

Descripción general de alto nivel:

Este control contendrá los siguientes componentes principales: (1) un AutoCompleteTextBox o AutoCompleteComboBox (2) el control de "botón" que describe (3) Una colección de UI para contener etiquetas aplicadas. AutoCompleteTextBox y la colección para contener etiquetas aplicadas se ubicarán por adelantado en un contenedor de diseño de su elección.

Primero, podemos aprovechar un AutoCompleteTextBox o AutoCompleteComboBox para darnos las opciones de estilo Intellisense según los tipos de usuarios. A continuación, "escuchamos" a los usuarios que seleccionan una etiqueta presentada de la lista desplegable y creamos dinámicamente un nuevo control de "botón" (podríamos crear un UserControl / CustomControl para él por adelantado, pero necesitaríamos "nuevo" uno por lo menos). El "botón" contendrá como texto el texto de AutoCompleteTextBox . Finalmente, insertamos el nuevo "botón" en un ListBox (u otro tipo de colección de UI correspondiente) que contiene todas las etiquetas aplicadas actualmente.

Detalles:

Hay algunos controles de AutoCompleteTextBox por ahí, pero voy a describir cómo podría usar este CodeProject . Este proyecto de ejemplo muestra cómo puede usar un TextBox regular, un AutoCompleteComboBox o un AutoCompleteTextBox para lograr las opciones de estilo intellisense. Las piezas centrales de este proyecto son realmente AutoCompleteManager y un DataProvider, del tipo IAutoCompleteDataProvider , junto con el IAutoAppendDataProvider .

Antes de describir más detalles, aquí hay algunas capturas de pantalla de este control AutoCompleteTextBox en acción (tenga en cuenta que estoy usando un estilo diferente para los ListBoxItems que los suministros originales del autor). La propiedad AutoAppend de este control es un buen toque (está activada para este ejemplo, por lo que después de comenzar a escribir la coincidencia actual automáticamente "termina" mi palabra para mí). Después de escribir sólo una "I":

Después de pasar el mouse sobre "Indiana":

Después de hacer clic en "Indiana":

Dado que el código de este proyecto maneja las opciones desplegables para nosotros, ahora debemos "escuchar" cuando el usuario selecciona un elemento de la lista desplegable y crear el nuevo control de "botón" en consecuencia. Hay dos casos principales en los que estoy pensando que debemos manejar.

El primer caso es cuando el usuario selecciona un elemento de la lista con su mouse. Para manejar esto, podríamos insertar el código para crear el nuevo control de "botón" en el controlador MouseLeftButtonUp en el AutoCompleteManager.cs , que está alrededor de la línea 451:

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. } }

El segundo caso es cuando el usuario selecciona un elemento de la lista presionando la tecla Intro. Para manejar esto, podríamos insertar un nuevo código similar en el controlador TextBox_PreviewKeyDown en AutoCompleteManager.cs , alrededor de la línea 291:

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. } // ... }

Detalles de impresión fina

Si decide utilizar AutoCompleteTextBox del CodeProject que mencioné, es posible que deba aplicar un par de correcciones. Me encontré con dos cosas pequeñas después de haber importado todo en un proyecto más grande (no ocurrieron cuando se ejecutó el proyecto de muestra incluido). El primero fue este error de enlace:

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'')

Otros han experimentado este problema con ListBoxes, descrito here . Como sugiere una de las respuestas a esa publicación, pude incluir una configuración de estilo explícita para HorizontalContentAligment y VerticalContentAlignment en mi estilo para resolver este problema.

El segundo problema ocurrió después de que AutoCompleteTextBox estaba en una aplicación que incluía pestañas. Cuando tenga controles en un control de pestañas, los controles anidados obtendrán su evento Loaded elevado con bastante frecuencia, al menos una vez por cada vez que se haga clic en la pestaña que contiene el control. Esto causó llamadas adicionales inesperadas al método AutoCompleteManager''s AttachTextBox() , lo que provocó que debug.Assert() fallara y ocurrieran excepciones (el autor original asumió que el evento Loaded solo se generaría una vez). Hasta ahora, la única solución que necesitaba hacer para manejar eso ha sido en AutoCompeteTextBox.cs . Acabo de agregar una _isInitialized para asegurarme de que AttachTextBox solo se llame una vez:

void AutoCompleteTextBox_Loaded(object sender, RoutedEventArgs e) { if (! _isInitialized) { _acm.AttachTextBox(this); _isInitialized = true; } }

El uso de este enfoque debería permitirle crear un control que se comporte como lo que usted describe.

Me gusta el control de etiquetado en Evernote (versión de Windows) y me preguntaba si hay algo similar por ahí. Solo he podido encontrar controles de nube de etiquetas.

Específicamente, me gusta el formato libre de escritura, como en un cuadro de texto que busca y presenta al estilo Intellisense las etiquetas que coinciden con las que he escrito. Cuando selecciono una etiqueta, el texto se reemplaza con un botón que representa la etiqueta y el texto del botón es el texto de la etiqueta.

Actualización : añadiendo capturas de pantalla

Añadiendo una nueva etiqueta

viendo las etiquetas existentes y haga clic en ''x'' para eliminar la etiqueta


Esto me pareció un ejercicio realmente bueno, así que traté de construir este control. No lo probé a fondo, avíseme si desea trabajar con él y necesita más ayuda.

Ejemplo de uso:

<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>

ViewModel:

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)); } } }

EvernoteTagControl:

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; } } }

EvernoteTagItem:

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://.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; } } }

Temas / 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>