You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

WPF TreeView类型及共享上下文菜单实现技术咨询

WPF标签页控件共享ContextMenu+TreeView层级实现方案

我来给你梳理下这个场景的具体实现思路和代码示例,结合你提到的TemplateFamily(父项)、TemplateParameter(子项)的结构,还有三个标签页共用同一个ContextMenu的需求,咱们一步步来:

一、界面布局(XAML)

首先是三个标签页的布局,把ContextMenu定义在窗口资源里,这样TreeView和两个ListBox都能直接引用:

<Window.Resources>
    <!-- 共享的ContextMenu -->
    <ContextMenu x:Key="SharedContextMenu">
        <MenuItem Header="新建" Click="NewMenuItem_Click"/>
        <MenuItem Header="编辑" Click="EditMenuItem_Click"/>
        <MenuItem Header="删除" Click="DeleteMenuItem_Click"/>
    </ContextMenu>
</Window.Resources>

<TabControl>
    <!-- 带TreeView的标签页 -->
    <TabItem Header="模板结构">
        <TreeView x:Name="TemplateTreeView" ContextMenu="{StaticResource SharedContextMenu}">
            <TreeView.ItemTemplate>
                <HierarchicalDataTemplate ItemsSource="{Binding Parameters}">
                    <!-- 父项(TemplateFamily)显示模板 -->
                    <TextBlock Text="{Binding FamilyName}"/>
                    <!-- 子项(TemplateParameter)显示模板 -->
                    <HierarchicalDataTemplate.ItemTemplate>
                        <DataTemplate>
                            <TextBlock Text="{Binding ParameterName}"/>
                        </DataTemplate>
                    </HierarchicalDataTemplate.ItemTemplate>
                </HierarchicalDataTemplate>
            </TreeView.ItemTemplate>
        </TreeView>
    </TabItem>

    <!-- 第一个ListBox标签页 -->
    <TabItem Header="参数列表1">
        <ListBox x:Name="ParamListBox1" ContextMenu="{StaticResource SharedContextMenu}">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding ParameterName}"/>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </TabItem>

    <!-- 第二个ListBox标签页 -->
    <TabItem Header="参数列表2">
        <ListBox x:Name="ParamListBox2" ContextMenu="{StaticResource SharedContextMenu}">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding ParameterName}"/>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </TabItem>
</TabControl>

二、数据类型定义(C#)

按照你说的父项TemplateFamily和子项TemplateParameter,咱们定义对应的实体类,实现INotifyPropertyChanged方便数据绑定:

using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;

public class TemplateFamily : INotifyPropertyChanged
{
    private string _familyName;
    public string FamilyName
    {
        get => _familyName;
        set { _familyName = value; OnPropertyChanged(); }
    }

    public ObservableCollection<TemplateParameter> Parameters { get; set; } = new ObservableCollection<TemplateParameter>();

    public event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

public class TemplateParameter : INotifyPropertyChanged
{
    private string _parameterName;
    public string ParameterName
    {
        get => _parameterName;
        set { _parameterName = value; OnPropertyChanged(); }
    }

    // 可根据需求添加其他属性,比如参数类型、默认值等

    public event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

三、ContextMenu共享逻辑处理(C#后台代码)

因为三个控件共用同一个ContextMenu,所以在菜单点击事件里需要先判断当前触发菜单的源对象类型,再执行对应的新建/编辑/删除逻辑:

using System.Windows;
using System.Windows.Controls;
using System.Linq;

private void NewMenuItem_Click(object sender, RoutedEventArgs e)
{
    var menuItem = sender as MenuItem;
    var contextMenu = menuItem?.Parent as ContextMenu;
    var sourceElement = contextMenu?.PlacementTarget as FrameworkElement;

    // 判断触发源是TreeView还是ListBox,以及TreeView选中项的类型
    if (sourceElement is TreeView treeView)
    {
        var selectedItem = treeView.SelectedItem;
        if (selectedItem is TemplateFamily family)
        {
            // 给选中的父项添加新子参数
            family.Parameters.Add(new TemplateParameter { ParameterName = "新参数" });
        }
        else if (selectedItem is TemplateParameter param)
        {
            // 给当前参数的父家族添加同级新参数
            var parentFamily = treeView.Items.Cast<TemplateFamily>().First(f => f.Parameters.Contains(param));
            parentFamily.Parameters.Add(new TemplateParameter { ParameterName = "新参数" });
        }
    }
    else if (sourceElement is ListBox listBox)
    {
        // 给ListBox添加新参数
        if (listBox.ItemsSource is ObservableCollection<TemplateParameter> paramList)
        {
            paramList.Add(new TemplateParameter { ParameterName = "新参数" });
        }
    }
}

private void EditMenuItem_Click(object sender, RoutedEventArgs e)
{
    var menuItem = sender as MenuItem;
    var contextMenu = menuItem?.Parent as ContextMenu;
    var sourceElement = contextMenu?.PlacementTarget as FrameworkElement;

    if (sourceElement is TreeView treeView)
    {
        var selectedItem = treeView.SelectedItem;
        if (selectedItem is TemplateFamily family)
        {
            // 这里可以替换为弹窗交互修改名称
            family.FamilyName = "修改后的模板家族名称";
        }
        else if (selectedItem is TemplateParameter param)
        {
            param.ParameterName = "修改后的参数名称";
        }
    }
    else if (sourceElement is ListBox listBox)
    {
        var selectedParam = listBox.SelectedItem as TemplateParameter;
        if (selectedParam != null)
        {
            selectedParam.ParameterName = "修改后的参数名称";
        }
    }
}

private void DeleteMenuItem_Click(object sender, RoutedEventArgs e)
{
    var menuItem = sender as MenuItem;
    var contextMenu = menuItem?.Parent as ContextMenu;
    var sourceElement = contextMenu?.PlacementTarget as FrameworkElement;

    if (sourceElement is TreeView treeView)
    {
        var selectedItem = treeView.SelectedItem;
        if (selectedItem is TemplateFamily family)
        {
            // 删除选中的父项
            var familyList = treeView.ItemsSource as ObservableCollection<TemplateFamily>;
            familyList?.Remove(family);
        }
        else if (selectedItem is TemplateParameter param)
        {
            // 删除选中的子参数
            var parentFamily = treeView.Items.Cast<TemplateFamily>().First(f => f.Parameters.Contains(param));
            parentFamily.Parameters.Remove(param);
        }
    }
    else if (sourceElement is ListBox listBox)
    {
        var selectedParam = listBox.SelectedItem as TemplateParameter;
        if (selectedParam != null && listBox.ItemsSource is ObservableCollection<TemplateParameter> paramList)
        {
            paramList.Remove(selectedParam);
        }
    }
}

四、初始化绑定数据(C#)

在窗口的构造函数里给TreeView和ListBox绑定测试数据:

public MainWindow()
{
    InitializeComponent();

    // 初始化TreeView的模板家族数据
    var familyList = new ObservableCollection<TemplateFamily>
    {
        new TemplateFamily
        {
            FamilyName = "基础模板家族",
            Parameters = { new TemplateParameter { ParameterName = "宽度" }, new TemplateParameter { ParameterName = "高度" } }
        },
        new TemplateFamily
        {
            FamilyName = "高级模板家族",
            Parameters = { new TemplateParameter { ParameterName = "分辨率" }, new TemplateParameter { ParameterName = "帧率" } }
        }
    };
    TemplateTreeView.ItemsSource = familyList;

    // 初始化两个ListBox的参数数据
    var paramList = new ObservableCollection<TemplateParameter>
    {
        new TemplateParameter { ParameterName = "颜色" },
        new TemplateParameter { ParameterName = "透明度" }
    };
    ParamListBox1.ItemsSource = paramList;
    ParamListBox2.ItemsSource = new ObservableCollection<TemplateParameter>(paramList);
}

这样就完整实现了你需要的功能:TreeView的两层结构展示、三个标签页共享ContextMenu,点击不同控件的项时菜单能执行对应逻辑。

内容的提问来源于stack exchange,提问作者Igavshne

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.26 08:22:59