✨简述
这里提到Treeview控件,有时它在wpf中的展示效果并不是太能满足我们的需求,而且重写控件也很麻烦,毕竟这个框架已经成型,在它的基础上修改,可施展空间很少;所以,这里主要介绍一种动态创建控件的方式来实现我们想要的效果。特别是像我这样不是以编程为主的开发人员来说,帮助还是挺大的。
Treeview常规效果
要实现的效果
那么,为了实现下面这种看着略微有点立体感的效果,我借助了大模型,同时也做了一番比较。
在代码开发方面,我常用DeepSeek、文心一言、通义以及VisualStudio自带的GitHub Copilot,其中GitHub Copilot对整个开发思路的支持还是有限的,我多用它来矫正代码问题以及一些方法的生成。
🎆IDE介绍
开集成开发环境(IDE)中,配合AI实现代码开发,已成为当前主流,能够节省很多开发时间,但前提是开发人员需要有一定基础并且懂得业务需求,不然单凭借大模型的自动生成,是无法满足UI需求的,很多效果都是基于已有模式提供,并不一定是你想要的方式。
网上很多介绍大模型能力的,其实大部分让AI自动生成的效果,并不是自己已经设计好的UI,而更多的是让大模型去发挥自己的设计能力,生成一个比较贴近提示词效果的UI。不过,提示词写的约详细明确,生成效果越好,这也不可否认。
- 当我要设计一个点击事件时,在注释中输入“点击”俩字,后面自动引出“空白处,取消选中”,这就是GitHub Copilot的基础能力,它会理解我这个.cs代码页,知道缺少哪些功能,再根据我的提示词“点击”,联想生成后面的内容。
- 那么,我们就按照这个思路继续生成,看看它的实现效果
- 其实,当我们Tab生成这个方法之后,代码是报错的,因为它少了一个“}”。当然这是小问题,补全即可。先不管这个方法能否实现我们想要的效果,至少也提供了一个比较符合的答案,特别是在代码格式和生成的最优效果方面,还是蛮不错的,有些代码生成之后,自己都感叹没有想到这种简洁的代码段。
🎟️过程说明
🏆AI模型的支持
DeepSeek
在使用AI模型支持开发的时候,我首先想到用DeepSeek,这也是25年开始到现在讨论最火的模型。不过,在AI发展的阶段,每天每时每刻都可能有超越和被超越的可能,任何单一、封闭的事物都不可能有长足的发展。
那么,DeepSeek的问题在哪呢? 这个平台最大的问题就是上下文支持,同一个对话中,当反复问答几次之后,它的反应都是一如既往的一致:
在这个过程,我也是用字节新推的TRAE来辅助编程设计,可明显觉得它在wpf(C#)上开发的理解和生成能力上,略显不足,对于新手开发windows应用来说,难度还是挺大额。
文心一言
接下来就转战到文心一言进行提问,它的帮助是明显的,而且还能提供至少三种实现方式供选择,同时它还是一个知错能改的小帮手,但也不是全面的,可能我的提示词过于简单,在基本上重复的问答过后,有些问题就是反复的犯错,无法解决。但很多常规性的代码还是可以帮助实现
通义
然后就是通义,它的优点在于有个模块叫“代码模式”
这个分屏展示的代码模式,其实就是一个变种,一个专门针对代码编程的新模式。在wpf开发中,虽然不能如web展示方式直接实现,但生成代码效果和问题处理上,还是相比其他要专业一些。
GitHub Copilot
前面IDE有提到,这个帮助也是不错的。
🥏编码过程
- 首先,我们需要创建一个wpf窗体界面,工程右键添加即可
- 然后在 .xaml中添加一个ScrollViewer容器,为了更好的使用它的滚轮效果,毕竟如果数据超出范围了,也方便查看
<Grid x:Name="MainGrid"
Background="LightGray">
<ScrollViewer
VerticalScrollBarVisibility="Hidden"
Background="LightSlateGray"
Margin="5,5,0,0"
Height="400"
Width="780"
HorizontalAlignment="Left"
VerticalAlignment="Top">
<Grid
x:Name="DetailsGrid"
Margin="5,5,5,5">
<!-- 初始时不生成任何行,通过代码动态生成 -->
</Grid>
</ScrollViewer>
</Grid>
数据将会在中间青灰色部分展示出来
- 接下来就是在容器“DetailsGrid”中动态生成控件了,在此之前需要准备数据。定义一个树节点的类,包含id、label和child,以及一个节点类
/// This class represents a tree node with an ID, label, and a collection of child nodes.
public class Node
{
[JsonProperty("id")]
public Int64 Id { get; set; }
[JsonProperty("label")]
public string Label { get; set; }
[JsonProperty("children")]
public List<Node> Children { get; set; }
}
/// 节点类
public class RootData
{
public List<Node> Data { get; set; }
}
- 设计一个方法用于获取所有根节点
public static class JsonHelper
{
// 方法:获取所有根节点
public static List<Node> GetRootNodes(string jsonData)
{
var data = JsonConvert.DeserializeObject<RootData>(jsonData);
return data.Data;
}
}
- 开始对数据进行处理,这里我使用了根节点和子节点分开来动态生成数据的方式实现。
private int currentRowIndex = 0; // 全局行索引
private int newLeft = 50; // 全局左边距
/// 数据处理
///
private void DataDealing()
{
// 处理数据
var jsonData = @"
{
""data"": [{
""id"": 1,
""label"": ""根节点"",
""children"": [{
""id"": 2,
""label"": ""子节点1"",
""children"": [{
""id"": 3,
""label"": ""子节点1-1""
}, {
""id"": 4,
""label"": ""子节点1-2""
}]
}, {
""id"": 5,
""label"": ""子节点2"",
""children"": [{
""id"": 6,
""label"": ""子节点2-1"",
""children"": [{
""id"": 8,
""label"": ""子节点2-1-1"" ,
""children"": [{
""id"": 12,
""label"": ""子节点2-1-1-1""
}, {
""id"": 13,
""label"": ""子节点2-1-1-2""
}]
}, {
""id"": 9,
""label"": ""子节点2-1-2""
}]
}, {
""id"": 7,
""label"": ""子节点2-2"",
""children"": [{
""id"": 10,
""label"": ""子节点2-2-1""
}, {
""id"": 11,
""label"": ""子节点2-2-2""
}]
}]
}]
}]
}";
// 获取所有根节点
var rootNodes = JsonHelper.GetRootNodes(jsonData);
foreach (var rootNode in rootNodes)
{
GenerateRootControls(rootNode.Label, DetailsGrid);
newLeft += 30; // 增加左边距以缩进子节点
currentRowIndex++; // 增加行索引以准备下一个子节点或子节点的子节点
foreach (var childNode in rootNode.Children)
{
GenerateChildControls(childNode, DetailsGrid, 0);
currentRowIndex++; // 增加行索引以准备下一个子节点或子节点的子节点
}
}
}
- 根节点生成方法
/// 生成首节点控件
private void GenerateRootControls(string label, Grid grid)
{
RowDefinition rowDef = new RowDefinition
{
Height = new GridLength(45, GridUnitType.Pixel)
};
grid.RowDefinitions.Add(rowDef);
// 生成Rectangle控件
Rectangle rectangle = new Rectangle
{
Width = 700,
Height = 40,
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Center,
Fill = new SolidColorBrush(Colors.White),
Stroke = new SolidColorBrush(Colors.White),
RadiusX = 2,
RadiusY = 2,
Margin = new Thickness(5, 0, 0, 0)
};
// 创建一个 DropShadowEffect 对象
DropShadowEffect shadowEffect = new DropShadowEffect
{
Color = Colors.Black,
BlurRadius = 5,
ShadowDepth = 2,
Opacity = 0.5
};
// 将阴影效果应用到 Rectangle 上
rectangle.Effect = shadowEffect;
Grid.SetRow(rectangle, currentRowIndex);
Grid.SetColumn(rectangle, 0);
grid.Children.Add(rectangle);
Image image = new Image
{
Height = 32,
Width = 32,
VerticalAlignment = VerticalAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Left,
Source = new BitmapImage(new Uri("/Images/rootNode.png", UriKind.Relative)),
Margin = new Thickness(5, 5, 0, 0)
};
Grid.SetRow(image, currentRowIndex);
Grid.SetColumn(image, 0);
grid.Children.Add(image);
// 生成TextBlock控件: 名称
TextBlock textBlockBarcode = new TextBlock
{
Text = label,
VerticalAlignment = VerticalAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Left,
Foreground = new SolidColorBrush(Colors.Gray),
FontSize = 25,
Width = 300,
Margin = new Thickness(newLeft, 0, 0, 0)
};
Grid.SetRow(textBlockBarcode, currentRowIndex);
Grid.SetColumn(textBlockBarcode, 0);
grid.Children.Add(textBlockBarcode);
Image image1 = new Image
{
Height = 32,
Width = 32,
VerticalAlignment = VerticalAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Left,
Source = new BitmapImage(new Uri("/Images/down1.png", UriKind.Relative)),
Margin = new Thickness(650, 0, 0, 0)
};
image1.MouseUp += DynamicImageDown_MouseUp;
Grid.SetRow(image1, currentRowIndex);
Grid.SetColumn(image1, 0);
grid.Children.Add(image1);
}
- 子节点生成方法,子节点需要判断其是否存在子节点,如果没有子节点,则不生成图片及相关操作。
/// 生成子节点控件
private void GenerateChildControls(Node childNode, Grid grid, int level)
{
RowDefinition rowDef = new RowDefinition
{
Height = new GridLength(45, GridUnitType.Pixel)
};
grid.RowDefinitions.Add(rowDef);
// 根据层级调整缩进或其他样式
double indent = newLeft + 30 * level; // 每一层增加30像素的缩进
// 生成Rectangle控件
Rectangle rectangle = new Rectangle
{
Width = 700,
Height = 40,
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Center,
Fill = new SolidColorBrush(Colors.White),
Stroke = new SolidColorBrush(Colors.White),
RadiusX = 2,
RadiusY = 2,
Margin = new Thickness(5, 0, 0, 0)
};
// 创建一个 DropShadowEffect 对象
DropShadowEffect shadowEffect = new DropShadowEffect
{
Color = Colors.Black,
BlurRadius = 5,
ShadowDepth = 2,
Opacity = 0.5
};
// 将阴影效果应用到 Rectangle 上
rectangle.Effect = shadowEffect;
Grid.SetRow(rectangle, currentRowIndex);
Grid.SetColumn(rectangle, 0);
grid.Children.Add(rectangle);
if (childNode.Children != null)
{
Image image1 = new Image
{
Height = 32,
Width = 32,
VerticalAlignment = VerticalAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Left,
Source = new BitmapImage(new Uri("/Images/down1.png", UriKind.Relative)),
Margin = new Thickness(650, 0, 0, 0)
};
image1.MouseUp += DynamicImageDown_MouseUp;
Grid.SetRow(image1, currentRowIndex);
Grid.SetColumn(image1, 0);
grid.Children.Add(image1);
Image image = new Image
{
Height = 32,
Width = 32,
VerticalAlignment = VerticalAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Left,
Source = new BitmapImage(new Uri("/Images/rootNode.png", UriKind.Relative)),
Margin = new Thickness(indent - 32, 5, 0, 0)
};
Grid.SetRow(image, currentRowIndex);
Grid.SetColumn(image, 0);
grid.Children.Add(image);
// 生成TextBlock控件: 名称
TextBlock textBlockBarcode = new TextBlock
{
Text = childNode.Label,
VerticalAlignment = VerticalAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Left,
Foreground = new SolidColorBrush(Colors.Gray),
FontSize = 25,
Width = 300,
Margin = new Thickness(indent, 0, 0, 0)
};
Grid.SetRow(textBlockBarcode, currentRowIndex);
Grid.SetColumn(textBlockBarcode, 0);
grid.Children.Add(textBlockBarcode);
// 递归调用以生成子节点控件
foreach (var child1Node in childNode.Children)
{
currentRowIndex++; // 增加行索引以准备下一个子节点或子节点的子节点
GenerateChildControls(child1Node, DetailsGrid, level + 1);
}
}
else
{
// 绑定事件
rectangle.MouseDown += DynamicRectStockOutCode_MouseDown;
// 生成TextBlock控件: 名称
TextBlock textBlockBarcode = new TextBlock
{
Text = childNode.Label,
VerticalAlignment = VerticalAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Left,
Foreground = new SolidColorBrush(Colors.Black),
FontSize = 25,
Width = 300,
Margin = new Thickness(indent, 0, 0, 0)
};
Grid.SetRow(textBlockBarcode, currentRowIndex);
Grid.SetColumn(textBlockBarcode, 0);
grid.Children.Add(textBlockBarcode);
}
}
- 点击事件方法,这两个方法都是大模型GtiHub Copilot生成的,比较经典的两处分别是
bool isFirstChildVisible = childControls[0].Visibility == Visibility.Visible;
- 和
// 获取当前行的子节点控件 var childControls = DetailsGrid.Children.OfType<UIElement>() .Where(c => Grid.GetRow(c) > currentRow) .ToList();
/// <summary>
/// 动态下拉图片点击事件
private void DynamicImageDown_MouseUp(object sender, MouseButtonEventArgs e)
{
// 获取当前行索引
int currentRow = Grid.GetRow((UIElement)sender);
// 获取当前行的子节点控件
var childControls = DetailsGrid.Children.OfType<UIElement>()
.Where(c => Grid.GetRow(c) > currentRow)
.ToList();
// 隐藏或显示子节点控件,如果当前行的子节点控件是可见的,则隐藏它们,否则显示它们;
if (childControls.Count == 0)
{
return; // 如果没有子节点控件,直接返回
}
// 确定比当前行大的第一个元素是否可见
bool isFirstChildVisible = childControls[0].Visibility == Visibility.Visible;
// 隐藏或显示子节点控件
foreach (var control in childControls)
{
if (isFirstChildVisible)
{
control.Visibility = Visibility.Collapsed;
}
else
{
control.Visibility = Visibility.Visible;
}
}
}
/// <summary>
/// 最里层节点允许点击变色
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void DynamicRectStockOutCode_MouseDown(object sender, MouseButtonEventArgs e)
{
// 获取当前行索引
int currentRow = Grid.GetRow((UIElement)sender);
// 获取当前行的子节点控件
var childControls = DetailsGrid.Children.OfType<UIElement>()
.Where(c => Grid.GetRow(c) == currentRow)
.ToList();
// 改变当前节点的颜色#1A76FF,如果点击了其他节点,则将之前的节点颜色改为白色
foreach (var control in DetailsGrid.Children.OfType<UIElement>())
{
if (Grid.GetRow(control) == currentRow)
{
if (control is Rectangle rectangle)
{
rectangle.Fill = new SolidColorBrush(Colors.Blue);
rectangle.Stroke = new SolidColorBrush(Colors.Blue);
}
}
else
{
if (control is Rectangle rectangle)
{
rectangle.Fill = new SolidColorBrush(Colors.White);
rectangle.Stroke = new SolidColorBrush(Colors.White);
}
}
}
}
🎯展示
至此,就实现了类似Treeview那种点击展开和选取节点的方法,感觉上效果要比重写控件资源简单一些。
欢迎大家批评指正。