Selecione TreeView Node com o botão direito antes de exibir ContextMenu


Respostas:


130

Dependendo da forma como a árvore foi preenchida, os valores do remetente e da e.Source podem variar .

Uma das soluções possíveis é usar e.OriginalSource e encontrar TreeViewItem usando VisualTreeHelper:

private void OnPreviewMouseRightButtonDown(object sender, MouseButtonEventArgs e)
{
    TreeViewItem treeViewItem = VisualUpwardSearch(e.OriginalSource as DependencyObject);

    if (treeViewItem != null)
    {
        treeViewItem.Focus();
        e.Handled = true;
    }
}

static TreeViewItem VisualUpwardSearch(DependencyObject source)
{
    while (source != null && !(source is TreeViewItem))
        source = VisualTreeHelper.GetParent(source);

    return source as TreeViewItem;
}

este evento é para TreeView ou TreeViewItem?
Louis Rhys de

1
an Alguma ideia de como desmarcar tudo se o botão direito estiver em um local vazio?
Louis Rhys

A única resposta que ajudou entre 5 outras ... Estou realmente fazendo algo errado com a população de visualização em árvore, obrigado.

3
Em resposta à pergunta de Louis Rhys: if (treeViewItem == null) treeView.SelectedIndex = -1ou treeView.SelectedItem = null. Eu acredito que qualquer um deve funcionar.
James M

24

Se você deseja uma solução apenas XAML, pode usar o Blend Interactivity.

Suponha que os TreeViewdados sejam vinculados a uma coleção hierárquica de modelos de visualização tendo uma Booleanpropriedade IsSelectede uma Stringpropriedade Name, bem como uma coleção de itens filhos nomeados Children.

<TreeView ItemsSource="{Binding Items}">
  <TreeView.ItemContainerStyle>
    <Style TargetType="TreeViewItem">
      <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}"/>
    </Style>
  </TreeView.ItemContainerStyle>
  <TreeView.ItemTemplate>
    <HierarchicalDataTemplate ItemsSource="{Binding Children}">
      <TextBlock Text="{Binding Name}">
        <i:Interaction.Triggers>
          <i:EventTrigger EventName="PreviewMouseRightButtonDown">
            <ei:ChangePropertyAction PropertyName="IsSelected" Value="true" TargetObject="{Binding}"/>
          </i:EventTrigger>
        </i:Interaction.Triggers>
      </TextBlock>
    </HierarchicalDataTemplate>
  </TreeView.ItemTemplate>
</TreeView>

Existem duas partes interessantes:

  1. A TreeViewItem.IsSelectedpropriedade está ligada à IsSelectedpropriedade no modelo de exibição. Definir a IsSelectedpropriedade no modelo de visualização como true selecionará o nó correspondente na árvore.

  2. Quando PreviewMouseRightButtonDowndispara na parte visual do nó (neste exemplo a TextBlock), a IsSelectedpropriedade no modelo de visualização é definida como verdadeira. Voltando a 1. você pode ver que o nó correspondente que foi clicado na árvore se torna o nó selecionado.

Uma maneira de obter o Blend Interactivity em seu projeto é usar o pacote NuGet Unofficial.Blend.Interactivity .


2
Ótima resposta, obrigado! Seria útil mostrar como os mapeamentos ie einamespace resolvem embora e em quais assemblies eles podem ser encontrados. Suponho que: xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"e xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions", que são encontrados nos assemblies System.Windows.Interactivity e Microsoft.Expression.Interactions respectivamente.
prlc

Isso não ajudou, pois o ChangePropertyActionestá tentando definir uma IsSelectedpropriedade do objeto de dados vinculado, que não faz parte da interface do usuário, portanto, não tem IsSelectedpropriedade. Estou fazendo algo errado?
Antonín Procházka

@ AntonínProcházka: Minha resposta requer que seu "objeto de dados" (ou modelo de visualização) tenha uma IsSelectedpropriedade conforme declarado no segundo parágrafo da minha resposta: Suponha que os TreeViewdados sejam vinculados a uma coleção hierárquica de modelos de visualização com uma propriedade booleanaIsSelected ... (ênfase minha).
Martin Liversage

16

Usando "item.Focus ();" não parece funcionar 100%, usando "item.IsSelected = true;" faz.


Obrigado por esta dica. Me ajudou.
18abug

Boa dica. Eu chamo Focus () primeiro e, em seguida, defino IsSelected = true.
Jim Gomes

12

Em XAML, adicione um manipulador PreviewMouseRightButtonDown em XAML:

    <TreeView.ItemContainerStyle>
        <Style TargetType="{x:Type TreeViewItem}">
            <!-- We have to select the item which is right-clicked on -->
            <EventSetter Event="TreeViewItem.PreviewMouseRightButtonDown" Handler="TreeViewItem_PreviewMouseRightButtonDown"/>
        </Style>
    </TreeView.ItemContainerStyle>

Em seguida, trate o evento assim:

    private void TreeViewItem_PreviewMouseRightButtonDown( object sender, MouseEventArgs e )
    {
        TreeViewItem item = sender as TreeViewItem;
        if ( item != null )
        {
            item.Focus( );
            e.Handled = true;
        }
    }

2
Não funciona como esperado, sempre recebo o elemento raiz como remetente. Eu encontrei uma solução semelhante um social.msdn.microsoft.com/Forums/en-US/wpf/thread/… Manipuladores de eventos adicionados dessa forma funcionam conforme o esperado. Alguma alteração em seu código para aceitá-lo? :-)
alex2k8

Aparentemente, depende de como você preenche a visualização em árvore. O código que postei funciona, porque é o código exato que uso em uma de minhas ferramentas.
Stefan

Observe que, se você definir um ponto de depuração aqui, poderá ver que tipo é o seu remetente, o que irá diferir com base em como você configurou a árvore

Esta parece ser a solução mais simples quando funciona. Funcionou para mim Na verdade, você deve apenas converter o remetente como um TreeViewItem, porque se não for, isso é um bug.
craftworkgames de

12

Usando a ideia original de alex2k8, manipulando corretamente os elementos não visuais da Wieser Software Ltd, o XAML de Stefan, o IsSelected de Erlend e minha contribuição de realmente tornar o método estático Genérico:

XAML:

<TreeView.ItemContainerStyle> 
    <Style TargetType="{x:Type TreeViewItem}"> 
        <!-- We have to select the item which is right-clicked on --> 
        <EventSetter Event="TreeViewItem.PreviewMouseRightButtonDown"
                     Handler="TreeViewItem_PreviewMouseRightButtonDown"/> 
    </Style> 
</TreeView.ItemContainerStyle>

Código C # por trás:

void TreeViewItem_PreviewMouseRightButtonDown(object sender, MouseButtonEventArgs e)
{
    TreeViewItem treeViewItem = 
              VisualUpwardSearch<TreeViewItem>(e.OriginalSource as DependencyObject);

    if(treeViewItem != null)
    {
        treeViewItem.IsSelected = true;
        e.Handled = true;
    }
}

static T VisualUpwardSearch<T>(DependencyObject source) where T : DependencyObject
{
    DependencyObject returnVal = source;

    while(returnVal != null && !(returnVal is T))
    {
        DependencyObject tempReturnVal = null;
        if(returnVal is Visual || returnVal is Visual3D)
        {
            tempReturnVal = VisualTreeHelper.GetParent(returnVal);
        }
        if(tempReturnVal == null)
        {
            returnVal = LogicalTreeHelper.GetParent(returnVal);
        }
        else returnVal = tempReturnVal;
    }

    return returnVal as T;
}

Edit: O código anterior sempre funcionou bem para este cenário, mas em outro cenário VisualTreeHelper.GetParent retornou null quando LogicalTreeHelper retornou um valor, então corrigiu isso.


1
Para promover isso, esta resposta implementa isso em uma extensão DependencyProperty: stackoverflow.com/a/18032332/84522
Terrence

7

Quase certo , mas você precisa estar atento para não visuais na árvore, (como um Run, por exemplo).

static DependencyObject VisualUpwardSearch<T>(DependencyObject source) 
{
    while (source != null && source.GetType() != typeof(T))
    {
        if (source is Visual || source is Visual3D)
        {
            source = VisualTreeHelper.GetParent(source);
        }
        else
        {
            source = LogicalTreeHelper.GetParent(source);
        }
    }
    return source; 
}

este método genérico parece um pouco estranho, como posso usá-lo ao escrever TreeViewItem treeViewItem = VisualUpwardSearch <TreeViewItem> (e.OriginalSource as DependencyObject); dá-me um erro de conversão
Rati_Ge

TreeViewItem treeViewItem = VisualUpwardSearch <TreeViewItem> (e.OriginalSource como DependencyObject) como TreeViewItem;
Anthony Wieser

6

Acho que registrar um manipulador de classe deve resolver o problema. Basta registrar um manipulador de eventos roteados no PreviewMouseRightButtonDownEvent do TreeViewItem em seu arquivo de código app.xaml.cs como este:

/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
    protected override void OnStartup(StartupEventArgs e)
    {
        EventManager.RegisterClassHandler(typeof(TreeViewItem), TreeViewItem.PreviewMouseRightButtonDownEvent, new RoutedEventHandler(TreeViewItem_PreviewMouseRightButtonDownEvent));

        base.OnStartup(e);
    }

    private void TreeViewItem_PreviewMouseRightButtonDownEvent(object sender, RoutedEventArgs e)
    {
        (sender as TreeViewItem).IsSelected = true;
    }
}

Funcionou para mim! E simples também.
dvallejo

2
Olá Nathan. Parece que o código é global e afetará cada TreeView. Não seria melhor ter uma solução apenas local? Isso pode criar efeitos colaterais?
Eric Ouellet

Este código é realmente global para todo o aplicativo WPF. No meu caso, esse era o comportamento necessário, portanto, era consistente para todas as visualizações de árvore usadas no aplicativo. No entanto, você pode registrar este evento em uma instância de treeview, portanto, só é aplicável para essa treeview.
Nathan Swannet

2

Outra maneira de resolver isso usando MVVM é o comando de ligação para clicar com o botão direito em seu modelo de visualização. Lá você também pode especificar outra lógica source.IsSelected = true. Isso usa apenas xmlns:i="http://schemas.microsoft.com/expression/2010/intera‌​ctivity"de System.Windows.Interactivity.

XAML para visualização:

<TreeView ItemsSource="{Binding Items}">
  <TreeView.ItemContainerStyle>
    <Style TargetType="TreeViewItem">
      <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}"/>
    </Style>
  </TreeView.ItemContainerStyle>
  <TreeView.ItemTemplate>
    <HierarchicalDataTemplate ItemsSource="{Binding Children}">
      <TextBlock Text="{Binding Name}">
        <i:Interaction.Triggers>
          <i:EventTrigger EventName="PreviewMouseRightButtonDown">
            <i:InvokeCommandAction Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=DataContext.TreeViewItemRigthClickCommand}" CommandParameter="{Binding}" />
          </i:EventTrigger>
        </i:Interaction.Triggers>
      </TextBlock>
    </HierarchicalDataTemplate>
  </TreeView.ItemTemplate>
</TreeView>

Ver modelo:

    public ICommand TreeViewItemRigthClickCommand
    {
        get
        {
            if (_treeViewItemRigthClickCommand == null)
            {
                _treeViewItemRigthClickCommand = new RelayCommand<object>(TreeViewItemRigthClick);
            }
            return _treeViewItemRigthClickCommand;
        }
    }
    private RelayCommand<object> _treeViewItemRigthClickCommand;

    private void TreeViewItemRigthClick(object sourceItem)
    {
        if (sourceItem is Item)
        {
            (sourceItem as Item).IsSelected = true;
        }
    }

1

Eu estava tendo problemas ao selecionar filhos com um método HierarchicalDataTemplate. Se eu selecionasse o filho de um nó, de alguma forma, ele selecionaria o pai raiz desse filho. Eu descobri que o evento MouseRightButtonDown seria chamado para todos os níveis em que a criança estava. Por exemplo, se você tiver uma árvore parecida com esta:

Item 1
   - Criança 1
   - Criança 2
      - Subitem1
      - Subitem2

Se eu selecionasse o Subitem2, o evento seria disparado três vezes e o item 1 seria selecionado. Resolvi isso com uma chamada booleana e assíncrona.

private bool isFirstTime = false;
    protected void TaskTreeView_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
    {
        var item = sender as TreeViewItem;
        if (item != null && isFirstTime == false)
        {
            item.Focus();
            isFirstTime = true;
            ResetRightClickAsync();
        }
    }

    private async void ResetRightClickAsync()
    {
        isFirstTime = await SetFirstTimeToFalse();
    }

    private async Task<bool> SetFirstTimeToFalse()
    {
        return await Task.Factory.StartNew(() => { Thread.Sleep(3000); return false; });
    }

Parece um pouco confuso, mas basicamente eu defini o booleano como verdadeiro na primeira passagem e o redefini em outro encadeamento em alguns segundos (3 neste caso). Isso significa que a próxima passagem por onde tentaria subir na árvore será ignorada, deixando você com o nó correto selecionado. Parece que funciona até agora :-)


A resposta é definir MouseButtonEventArgs.Handledcomo true. Pois a criança é a primeira a ser chamada. Configurar esta propriedade como true irá desativar outras chamadas para o pai.
Basit Anwer

0

Você pode selecioná-lo com o evento ao pressionar o mouse. Isso irá acionar o select antes de o menu de contexto entrar em ação.


0

Se você deseja permanecer dentro do padrão MVVM, você pode fazer o seguinte:

Visão:

<TreeView x:Name="trvName" ItemsSource="{Binding RootElementListView}" Tag="{Binding ClickedTreeElement, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
    <TreeView.ItemTemplate>
        <HierarchicalDataTemplate DataType="{x:Type models:YourTreeElementClass}" ItemsSource="{Binding Path=Subreports}">
            <TextBlock Text="{Binding YourTreeElementDisplayProperty}" PreviewMouseRightButtonDown="TreeView_PreviewMouseRightButtonDown"/>
        </HierarchicalDataTemplate>
    </TreeView.ItemTemplate>
</TreeView>

Código por trás:

private void TreeView_PreviewMouseRightButtonDown(object sender, MouseButtonEventArgs e)
{
    if (sender is TextBlock tb && tb.DataContext is YourTreeElementClass te)
    {
        trvName.Tag = te;
    }
}

ViewModel:

private YourTreeElementClass _clickedTreeElement;

public YourTreeElementClass ClickedTreeElement
{
    get => _clickedTreeElement;
    set => SetProperty(ref _clickedTreeElement, value);
}

Agora você pode reagir à alteração da propriedade ClickedTreeElement ou pode usar um comando que funcione internamente com ClickedTreeElement.

Visão estendida:

<UserControl ...
             xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity">
    <TreeView x:Name="trvName" ItemsSource="{Binding RootElementListView}" Tag="{Binding ClickedTreeElement, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
        <i:Interaction.Triggers>
            <i:EventTrigger EventName="MouseRightButtonUp">
                <i:InvokeCommandAction Command="{Binding HandleRightClickCommand}"/>
            </i:EventTrigger>
        </i:Interaction.Triggers>
        <TreeView.ItemTemplate>
            <HierarchicalDataTemplate DataType="{x:Type models:YourTreeElementClass}" ItemsSource="{Binding Path=Subreports}">
                <TextBlock Text="{Binding YourTreeElementDisplayProperty}" PreviewMouseRightButtonDown="TreeView_PreviewMouseRightButtonDown"/>
            </HierarchicalDataTemplate>
        </TreeView.ItemTemplate>
    </TreeView>
</UserControl>
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.