เลือก TreeView Node เมื่อคลิกขวาก่อนแสดง ContextMenu


101

ฉันต้องการเลือก WPF TreeView Node เมื่อคลิกขวาก่อนที่จะแสดง ContextMenu

สำหรับ WinForms ฉันสามารถใช้รหัสเช่นนี้ค้นหาโหนดที่คลิกภายใต้เมนูบริบททางเลือก WPF คืออะไร?

คำตอบ:


131

ทั้งนี้ขึ้นอยู่กับวิธีการที่ต้นไม้ที่มีประชากร, ผู้ส่งและค่า e.Source อาจแตกต่างกัน

หนึ่งในวิธีแก้ไขที่เป็นไปได้คือใช้ e.OriginalSource และค้นหา TreeViewItem โดยใช้ 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;
}

เหตุการณ์นี้เป็นของ TreeView หรือ TreeViewItem?
Louis Rhys

1
มีความคิดอย่างไรในการยกเลิกการเลือกทุกอย่างหากคลิกขวาอยู่ในตำแหน่งว่าง
Louis Rhys

คำตอบเดียวที่ช่วยได้จากอีก 5 คน ... ฉันทำอะไรผิดกับประชากรต้นไม้จริงๆขอบคุณ

3
เพื่อตอบคำถามของ Louis Rhys: if (treeViewItem == null) treeView.SelectedIndex = -1หรือtreeView.SelectedItem = null. ฉันเชื่อว่าควรใช้งานได้
James M

24

หากคุณต้องการโซลูชัน XAML เท่านั้นคุณสามารถใช้ Blend Interactivity

ถือว่าTreeViewเป็นข้อมูลที่ถูกผูกไว้กับคอลเลกชันลำดับชั้นของการดูรุ่นที่มีBooleanคุณสมบัติIsSelectedและStringคุณสมบัติเช่นเดียวกับคอลเลกชันของรายการเด็กชื่อNameChildren

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

มีสองส่วนที่น่าสนใจ:

  1. TreeViewItem.IsSelectedคุณสมบัติถูกผูกไว้กับIsSelectedทรัพย์สินในมุมมองแบบ การตั้งค่าIsSelectedคุณสมบัติบน view-model เป็น true จะเลือกโหนดที่เกี่ยวข้องในทรี

  2. เมื่อPreviewMouseRightButtonDownยิงบนส่วนภาพของโหนด (ในตัวอย่างนี้ a TextBlock) IsSelectedคุณสมบัติบน view-model จะถูกตั้งค่าเป็น true กลับไปที่ 1. คุณจะเห็นว่าโหนดที่เกี่ยวข้องซึ่งถูกคลิกในโครงสร้างกลายเป็นโหนดที่เลือก

วิธีการหนึ่งที่จะได้รับการผสมผสานการโต้ตอบในโครงการของคุณคือการใช้แพคเกจ NuGet Unofficial.Blend.Interactivity


2
คำตอบที่ดีขอบคุณ! มันจะเป็นประโยชน์ที่จะแสดงสิ่งที่เป็นiและei. แก้ปัญหาการแมป namespace ไปและแม้ว่าพวกเขาซึ่งประกอบสามารถพบได้ในผมถือว่า: xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"และxmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"ซึ่งจะพบใน System.Windows.Interactivity และ Microsoft.Expression.Interactions ประกอบตามลำดับ
prlc

สิ่งนี้ไม่ได้ช่วยเนื่องจากChangePropertyActionพยายามตั้งค่าIsSelectedคุณสมบัติของวัตถุข้อมูลที่ถูกผูกไว้ซึ่งไม่ได้เป็นส่วนหนึ่งของ UI ดังนั้นจึงไม่มีIsSelectedคุณสมบัติ ฉันทำอะไรผิดหรือเปล่า?
AntonínProcházka

@ AntonínProcházka: คำตอบของฉันต้องการให้ "data object" (หรือ view model) ของคุณมีIsSelectedคุณสมบัติตามที่ระบุไว้ในย่อหน้าที่สองของคำตอบของฉัน: สมมติว่าTreeViewเป็นข้อมูลที่ผูกไว้กับคอลเลกชันมุมมองแบบลำดับชั้นที่มีคุณสมบัติบูลีนIsSelected ... (เน้นของฉัน)
Martin Liversage

16

ใช้ "item.Focus ();" ดูเหมือนจะไม่ทำงาน 100% โดยใช้ "item.IsSelected = true;" ทำ.


ขอบคุณสำหรับเคล็ดลับนี้ ช่วยฉันด้วย
i8abug

เคล็ดลับที่ดี ฉันเรียก Focus () ก่อนจากนั้นตั้งค่า IsSelected = true
Jim Gomes

12

ใน XAML เพิ่มตัวจัดการ PreviewMouseRightButtonDown ใน 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>

จากนั้นจัดการเหตุการณ์ดังนี้:

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

3
มันไม่ได้ผลตามที่คาดไว้ฉันมักจะได้รับองค์ประกอบรูทเป็นผู้ส่ง ฉันพบวิธีแก้ปัญหาที่คล้ายกันหนึ่งsocial.msdn.microsoft.com/Forums/en-US/wpf/thread/…ตัวจัดการเหตุการณ์ที่เพิ่มวิธีนี้ทำงานได้ตามที่คาดไว้ มีการเปลี่ยนแปลงรหัสของคุณเพื่อยอมรับหรือไม่ :-)
alex2k8

เห็นได้ชัดว่าขึ้นอยู่กับว่าคุณเติมมุมมองแบบต้นไม้อย่างไร รหัสที่ฉันโพสต์ใช้งานได้เพราะเป็นรหัสที่ฉันใช้ในเครื่องมือของฉัน
สเตฟาน

สังเกตว่าคุณกำหนดจุดดีบักที่นี่หรือไม่คุณจะเห็นว่าผู้ส่งของคุณเป็นประเภทใดซึ่งแน่นอนว่าจะแตกต่างกันไปตามวิธีการตั้งค่าต้นไม้

นี่เป็นวิธีแก้ปัญหาที่ง่ายที่สุดเมื่อใช้งานได้จริง มันได้ผลสำหรับฉัน อันที่จริงคุณควรแคสผู้ส่งเป็น TreeViewItem เพราะถ้าไม่ใช่นั่นคือบั๊ก
craftworkgames

12

ใช้แนวคิดดั้งเดิมจาก alex2k8 จัดการภาพที่ไม่ใช่ภาพอย่างถูกต้องจาก Wieser Software Ltd, XAML จาก Stefan, IsSelected จาก Erlend และการมีส่วนร่วมของฉันในการสร้างวิธีการแบบคงที่ Generic อย่างแท้จริง:

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 # ด้านหลัง:

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

แก้ไข: รหัสก่อนหน้านี้ทำงานได้ดีเสมอสำหรับสถานการณ์นี้ แต่ในอีกสถานการณ์หนึ่ง VisualTreeHelper.GetParent ส่งคืนค่า null เมื่อ LogicalTreeHelper ส่งคืนค่าดังนั้นคงที่


1
หากต้องการเพิ่มเติมคำตอบนี้นำไปใช้ในส่วนขยาย DependencyProperty: stackoverflow.com/a/18032332/84522
Terrence

7

เกือบถูกต้องแต่คุณต้องระวังภาพที่ไม่ใช่ภาพในแผนภูมิ (เช่น a Run)

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

วิธีการทั่วไปนี้ดูเหมือนจะแปลกเล็กน้อยฉันจะใช้มันได้อย่างไรเมื่อฉันเขียน TreeViewItem treeViewItem = VisualUpwardSearch <TreeViewItem> (e.OriginalSource เป็น DependencyObject); มันทำให้ฉันเกิดข้อผิดพลาดในการแปลง
Rati_Ge

TreeViewItem treeViewItem = VisualUpwardSearch <TreeViewItem> (e.OriginalSource เป็น DependencyObject) เป็น TreeViewItem;
Anthony Wieser

7

ฉันคิดว่าการลงทะเบียนตัวจัดการชั้นเรียนควรทำเคล็ดลับ เพียงลงทะเบียนตัวจัดการเหตุการณ์ที่กำหนดเส้นทางบน PreviewMouseRightButtonDownEvent ของ TreeViewItem ในไฟล์โค้ด app.xaml.cs ดังนี้:

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

ทำงานให้ฉัน! และเรียบง่ายด้วย
dvallejo

2
สวัสดีนาธาน. ดูเหมือนว่าโค้ดจะใช้งานได้ทั่วโลกและจะมีผลกับ TreeView ทุกตัว จะไม่ดีกว่าเหรอหากมีวิธีแก้ปัญหาเฉพาะในท้องถิ่น มันสามารถสร้างผลข้างเคียง?
Eric Ouellet

รหัสนี้เป็นรหัสสากลสำหรับแอปพลิเคชัน WPF ทั้งหมด ในกรณีของฉันนี่เป็นพฤติกรรมที่จำเป็นดังนั้นจึงสอดคล้องกับ Treeview ทั้งหมดที่ใช้ภายในแอปพลิเคชัน อย่างไรก็ตามคุณสามารถลงทะเบียนเหตุการณ์นี้บนอินสแตนซ์ Treeview ได้เองดังนั้นจึงใช้ได้กับ Treeview นั้นเท่านั้น
Nathan Swannet

2

อีกวิธีหนึ่งในการแก้ปัญหาโดยใช้ MVVM คือคำสั่ง bind สำหรับคลิกขวาที่โมเดลมุมมองของคุณ มีคุณสามารถระบุตรรกะอื่น ๆ source.IsSelected = trueรวมทั้ง ใช้เฉพาะxmlns:i="http://schemas.microsoft.com/expression/2010/intera‌​ctivity"จากSystem.Windows.Interactivity.

XAML เพื่อดู:

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

ดูโมเดล:

    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

ฉันมีปัญหากับการเลือกเด็กด้วยเมธอด HierarchicalDataTemplate ถ้าฉันเลือกลูกของโหนดมันจะเลือกรูทพาเรนต์ของลูกนั้น ฉันพบว่าเหตุการณ์ MouseRightButtonDown จะถูกเรียกใช้สำหรับทุกระดับที่เด็กเป็น ตัวอย่างเช่นถ้าคุณมีต้นไม้อะไรทำนองนี้:

Item 1
   - Child 1
   - Child 2
      - Subitem1
      - Subitem2

ถ้าฉันเลือก Subitem2 เหตุการณ์จะเริ่มทำงานสามครั้งและจะเลือกรายการ 1 ฉันแก้ไขสิ่งนี้ด้วยบูลีนและการโทรแบบอะซิงโครนัส

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

รู้สึกอึดอัดเล็กน้อย แต่โดยพื้นฐานแล้วฉันตั้งค่าบูลีนเป็นจริงในการส่งผ่านครั้งแรกและรีเซ็ตบนเธรดอื่นในไม่กี่วินาที (3 ในกรณีนี้) ซึ่งหมายความว่าครั้งต่อไปจะผ่านจุดที่พยายามจะเลื่อนขึ้นต้นไม้จะถูกข้ามไปโดยที่คุณเลือกโหนดที่ถูกต้อง ดูเหมือนว่าจะใช้งานได้ :-)


คำตอบคือการตั้งค่าไปMouseButtonEventArgs.Handled trueตั้งแต่เด็กเป็นคนแรกที่ถูกเรียก การตั้งค่าคุณสมบัตินี้เป็นจริงจะปิดใช้งานการโทรอื่น ๆ ไปยังผู้ปกครอง
Basit Anwer

0

คุณสามารถเลือกได้โดยใช้เหตุการณ์เมื่อวางเมาส์ลง ซึ่งจะทริกเกอร์การเลือกก่อนที่เมนูบริบทจะเริ่มขึ้น


0

หากคุณต้องการอยู่ในรูปแบบ MVVM คุณสามารถทำสิ่งต่อไปนี้:

ดู:

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

รหัสหลัง:

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

ดู

private YourTreeElementClass _clickedTreeElement;

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

ตอนนี้คุณสามารถตอบสนองต่อการเปลี่ยนแปลงคุณสมบัติ ClickedTreeElement หรือคุณสามารถใช้คำสั่งที่ทำงานภายในกับ ClickedTreeElement

มุมมองเพิ่มเติม:

<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>
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.