У меня есть что-то вроде этого в TreeView:
<DataTemplate x:Key="myTemplate">
<StackPanel MouseDown="OnItemMouseDown">
...
</StackPanel>
</DataTemplate>
Используя это, я получаю события нажатия мыши, если нажимаю на элементы на панели стека. Однако ... кажется, что за панелью стека есть еще один элемент - TreeViewItem - по нему очень сложно попасть, но не невозможно, и именно тогда начинаются проблемы.
Я попытался обработать PreviewMouseDown для TreeViewItem, однако для этого требуется
e.Handled = false
в противном случае стандартное поведение в виде дерева перестает работать.
Хорошо, вот исходный код ...
MainWindow.xaml
<Window x:Class="WPFMultiSelectTree.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WPFMultiSelectTree"
Title="Multiple Selection Tree" Height="300" Width="300">
<Window.Resources>
<!-- Declare the classes that convert bool to Visibility -->
<local:VisibilityConverter x:Key="visibilityConverter"/>
<local:VisibilityInverter x:Key="visibilityInverter"/>
<!-- Set the style for any tree view item -->
<Style TargetType="TreeViewItem">
<Style.Triggers>
<DataTrigger Binding="{Binding Selected}" Value="True">
<Setter Property="Background" Value="DarkBlue"/>
<Setter Property="Foreground" Value="White"/>
</DataTrigger>
</Style.Triggers>
<EventSetter Event="PreviewMouseDown" Handler="OnTreePreviewMouseDown"/>
</Style>
<!-- Declare a hierarchical data template for the tree view items -->
<HierarchicalDataTemplate
x:Key="RecursiveTemplate"
ItemsSource="{Binding Children}">
<StackPanel Margin="2" Orientation="Horizontal" MouseDown="OnTreeMouseDown">
<Ellipse Width="12" Height="12" Fill="Green"/>
<TextBlock
Margin="2"
Text="{Binding Name}"
Visibility="{Binding Editing, Converter={StaticResource visibilityInverter}}"/>
<TextBox
Margin="2"
Text="{Binding Name}"
KeyDown="OnTextBoxKeyDown"
IsVisibleChanged="OnTextBoxIsVisibleChanged"
Visibility="{Binding Editing, Converter={StaticResource visibilityConverter}}"/>
<TextBlock
Margin="2"
Text="{Binding Index, StringFormat=({0})}"/>
</StackPanel>
</HierarchicalDataTemplate>
<!-- Declare a simple template for a list box -->
<DataTemplate x:Key="ListTemplate">
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</Window.Resources>
<Grid>
<!-- Declare the rows in this grid -->
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<!-- The first header -->
<TextBlock Grid.Row="0" Margin="5" Background="PowderBlue">Multiple selection tree view</TextBlock>
<!-- The tree view -->
<TreeView
Name="m_tree"
Margin="2"
Grid.Row="1"
ItemsSource="{Binding Children}"
ItemTemplate="{StaticResource RecursiveTemplate}"/>
<!-- The second header -->
<TextBlock Grid.Row="2" Margin="5" Background="PowderBlue">The currently selected items in the tree</TextBlock>
<!-- The list box -->
<ListBox
Name="m_list"
Margin="2"
Grid.Row="3"
ItemsSource="{Binding .}"
ItemTemplate="{StaticResource ListTemplate}"/>
</Grid>
</Window>
MainWindow.xaml.cs
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private Container m_root;
private Container m_first;
private ObservableCollection<Container> m_selection;
private string m_current;
/// <summary>
/// Constructor
/// </summary>
public MainWindow()
{
InitializeComponent();
m_selection = new ObservableCollection<Container>();
m_root = new Container("root");
for (int parents = 0; parents < 50; parents++)
{
Container parent = new Container(String.Format("parent{0}", parents + 1));
for (int children = 0; children < 1000; children++)
{
parent.Add(new Container(String.Format("child{0}", children + 1)));
}
m_root.Add(parent);
}
m_tree.DataContext = m_root;
m_list.DataContext = m_selection;
m_first = null;
}
/// <summary>
/// Has the shift key been pressed?
/// </summary>
private bool ShiftPressed
{
get
{
return Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift);
}
}
/// <summary>
/// Has the control key been pressed?
/// </summary>
private bool CtrlPressed
{
get
{
return Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl);
}
}
/// <summary>
/// Clear down the selection list
/// </summary>
private void DeselectAndClear()
{
foreach(Container container in m_selection)
{
container.Selected = false;
}
m_selection.Clear();
}
/// <summary>
/// Add the container to the list (if not already present),
/// mark as selected
/// </summary>
/// <param name="container"></param>
private void AddToSelection(Container container)
{
if (container == null)
{
return;
}
foreach (Container child in m_selection)
{
if (child == container)
{
return;
}
}
container.Selected = true;
m_selection.Add(container);
}
/// <summary>
/// Remove container from list, mark as not selected
/// </summary>
/// <param name="container"></param>
private void RemoveFromSelection(Container container)
{
m_selection.Remove(container);
container.Selected = false;
}
/// <summary>
/// Process single click on a tree item
///
/// Normally just select an item
///
/// SHIFT-Click extends selection
/// CTRL-Click toggles a selection
/// </summary>
/// <param name="sender"></param>
private void OnTreeSingleClick(object sender)
{
FrameworkElement element = sender as FrameworkElement;
if (element != null)
{
Container container = element.DataContext as Container;
if (container != null)
{
if (CtrlPressed)
{
if (container.Selected)
{
RemoveFromSelection(container);
}
else
{
AddToSelection(container);
}
}
else if (ShiftPressed)
{
if (container.Parent == m_first.Parent)
{
if (container.Index < m_first.Index)
{
Container item = container;
for (int i = container.Index; i < m_first.Index; i++)
{
AddToSelection(item);
item = item.Next;
if (item == null)
{
break;
}
}
}
else if (container.Index > m_first.Index)
{
Container item = m_first;
for (int i = m_first.Index; i <= container.Index; i++)
{
AddToSelection(item);
item = item.Next;
if (item == null)
{
break;
}
}
}
}
}
else
{
DeselectAndClear();
m_first = container;
AddToSelection(container);
}
}
}
}
/// <summary>
/// Process double click on tree item
/// </summary>
/// <param name="sender"></param>
private void OnTreeDoubleClick(object sender)
{
FrameworkElement element = sender as FrameworkElement;
if (element != null)
{
Container container = element.DataContext as Container;
if (container != null)
{
container.Editing = true;
m_current = container.Name;
}
}
}
/// <summary>
/// Clicked on the stack panel in the tree view
///
/// Double left click:
///
/// Switch to editing mode (flips visibility of textblock and textbox)
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnTreeMouseDown(object sender, MouseButtonEventArgs e)
{
Debug.WriteLine("StackPanel mouse down");
switch(e.ChangedButton)
{
case MouseButton.Left:
switch (e.ClickCount)
{
case 2:
OnTreeDoubleClick(sender);
e.Handled = true;
break;
}
break;
}
}
/// <summary>
/// Clicked on tree view item in tree
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnTreePreviewMouseDown(object sender, MouseButtonEventArgs e)
{
Debug.WriteLine("TreeViewItem preview mouse down");
switch (e.ChangedButton)
{
case MouseButton.Left:
switch (e.ClickCount)
{
case 1:
{
// We've had a single click on a tree view item
// Unfortunately this is the WHOLE tree item, including the +/-
// symbol to the left. The tree doesn't do a selection, so we
// have to filter this out...
MouseDevice device = e.Device as MouseDevice;
Debug.WriteLine(String.Format("Tree item clicked on: {0}", device.DirectlyOver.GetType().ToString()));
// This is bad. The whole point of WPF is for the code
// not to know what the UI has - yet here we are testing for
// it as a workaround. Sigh...
if (device.DirectlyOver.GetType() != typeof(Path))
{
OnTreeSingleClick(sender);
}
// Cannot say handled - if we do it stops the tree working!
//e.Handled = true;
}
break;
}
break;
}
}
/// <summary>
/// Key press in text box
///
/// Return key finishes editing
/// Escape key finishes editing, restores original value (this doesn't work!)
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnTextBoxKeyDown(object sender, KeyEventArgs e)
{
switch(e.Key)
{
case Key.Return:
{
TextBox box = sender as TextBox;
if (box != null)
{
Container container = box.DataContext as Container;
if (container != null)
{
container.Editing = false;
e.Handled = true;
}
}
}
break;
case Key.Escape:
{
TextBox box = sender as TextBox;
if (box != null)
{
Container container = box.DataContext as Container;
if (container != null)
{
container.Editing = false;
container.Name = m_current;
e.Handled = true;
}
}
}
break;
}
}
/// <summary>
/// When text box becomes visible, grab focus and select all text in it.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnTextBoxIsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
{
bool visible = (bool)e.NewValue;
if (visible)
{
TextBox box = sender as TextBox;
if (box != null)
{
box.Focus();
box.SelectAll();
}
}
}
}
Вот класс Контейнер
public class Container : INotifyPropertyChanged
{
private string m_name;
private ObservableCollection<Container> m_children;
private Container m_parent;
private bool m_selected;
private bool m_editing;
/// <summary>
/// Constructor
/// </summary>
/// <param name="name">name of object</param>
public Container(string name)
{
m_name = name;
m_children = new ObservableCollection<Container>();
m_parent = null;
m_selected = false;
m_editing = false;
}
/// <summary>
/// Name of object
/// </summary>
public string Name
{
get
{
return m_name;
}
set
{
if (m_name != value)
{
m_name = value;
OnPropertyChanged("Name");
}
}
}
/// <summary>
/// Index of object in parent's children
///
/// If there's no parent, the index is -1
/// </summary>
public int Index
{
get
{
if (m_parent != null)
{
return m_parent.Children.IndexOf(this);
}
return -1;
}
}
/// <summary>
/// Get the next item, assuming this is parented
///
/// Returns null if end of list reached, or no parent
/// </summary>
public Container Next
{
get
{
if (m_parent != null)
{
int index = Index + 1;
if (index < m_parent.Children.Count)
{
return m_parent.Children[index];
}
}
return null;
}
}
/// <summary>
/// List of children
/// </summary>
public ObservableCollection<Container> Children
{
get
{
return m_children;
}
}
/// <summary>
/// Selected status
/// </summary>
public bool Selected
{
get
{
return m_selected;
}
set
{
if (m_selected != value)
{
m_selected = value;
OnPropertyChanged("Selected");
}
}
}
/// <summary>
/// Editing status
/// </summary>
public bool Editing
{
get
{
return m_editing;
}
set
{
if (m_editing != value)
{
m_editing = value;
OnPropertyChanged("Editing");
}
}
}
/// <summary>
/// Parent of this object
/// </summary>
public Container Parent
{
get
{
return m_parent;
}
set
{
m_parent = value;
}
}
/// <summary>
/// WPF Property Changed event
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Handler to inform WPF that a property has changed
/// </summary>
/// <param name="name"></param>
private void OnPropertyChanged(string name)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
/// <summary>
/// Add a child to this container
/// </summary>
/// <param name="child"></param>
public void Add(Container child)
{
m_children.Add(child);
child.m_parent = this;
}
/// <summary>
/// Remove a child from this container
/// </summary>
/// <param name="child"></param>
public void Remove(Container child)
{
m_children.Remove(child);
child.m_parent = null;
}
}
Два класса VisibilityConverter и VisibilityInverter являются реализациями IValueConverter, который переводит bool в Visibility. Они гарантируют, что TextBlock отображается, когда он не редактируется, и TextBox отображается при редактировании.