在 WPF 中显示带有大量项目的 ListBox 中的边框图块
Display bordered tiles in ListBox with large number of items in WPF
我正在尝试构建一个类似于第一个屏幕截图中的 GUI:
基本上它是一个图块视图,显示在带边框的网格中,随着 window 调整大小时动态更改列数。项目是可选择的。这些可能看起来像图片,但它们只是 Unicode 字符。
我制作了一个自定义对象列表,并成功地将列表绑定到一个ListBox控件。自然地,这给了我一个垂直堆叠的字符列表。
所以我用谷歌搜索了一下,发现我可以为列表框设置一个自定义的 ItemsPanelTemplate:
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Margin="0"/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
结果如下:
为了设置图块的边框和大小,我还添加了一个 ItemTemplate:
<ListBox Margin="0,0,0,0" Padding="0,0,0,0" Grid.Row="0" Grid.Column="0" x:Name="tw"
SelectionChanged="tw_SelectionChanged"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ScrollViewer.VerticalScrollBarVisibility="Visible">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Margin="0,0,0,0"/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<Label
Padding="0,0,0,0"
Margin="0,0,0,0"
Width="50"
Height="50"
FontSize="20"
Background="White"
HorizontalAlignment="Left"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
BorderThickness="1"
BorderBrush="Black"
Content="{Binding Path=LiteralString}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
结果如下:
据我所知。
第一个问题是,如您所见,我无法摆脱的瓷砖之间有 spaces。您会在标记中注意到,我已尝试尽可能手动将 Margin 和 Padding 设置为 0。 space 还在。
第二个问题是,您无法真正看到选择了哪个项目。从下数第 3 个字符和从右数第 2 个字符被选中,但蓝色被标签覆盖。您只能在字符左侧的 space 中看到它。我不知道如何解决这个问题。
另一个潜在的问题是性能。该视图最终将具有过滤器,但是当所有过滤器都关闭时,将显示大约 6500 个项目。当我只有一个基本的 ListBox 时,应用程序在 运行 时占用了大约 28MB 的 RAM。当我添加带有 WrapPanel 的 ItemsPanelTemplate 时,内存使用量跃升至大约 136MB。当我添加带有标签的 ItemTemplate 时,内存使用量跃升至 183MB,更糟糕的是,window 现在需要更长的时间来加载。
我试过用 TextBlocks 替换 Labels,因为我读到它们更轻巧。事情变快了,内存使用量下降到 130MB 左右,但 TextBlocks 并不好。我不能将文本居中放置,也不能给它们加边框。正如您在屏幕截图中所见,所选项目变得一团糟:
诚然,我的电脑速度很慢,183MB 也不算死,但还是有点低效。我打算在这个应用程序中加入更多的功能,如果这一件事占用了这么多的 RAM,那么最终的应用程序很可能是一个无法使用的资源吸血鬼。另外,我相信我正在模仿的应用程序也是用 .NET 编写的(虽然我不确定它是否是 WPF)并且那个应用程序在显示视图时只使用 17.5MB 的 RAM。
是否有另一种方法可以做到这一点,一种既更高效又更容易定制的方法?
谢谢。
编辑:
我找到了稍微好一点的方法。我完全删除了 ItemTemplate,而是使用 ItemContainerStyle:
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Style.Setters>
<Setter Property="BorderBrush" Value="Black"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Width" Value="50"/>
<Setter Property="Height" Value="50"/>
<Setter Property="FontSize" Value="20"/>
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
</Style.Setters>
<Style.Triggers>
<Trigger Property="IsSelected" Value="True" >
<Setter Property="FontWeight" Value="Bold" />
<Setter Property="Background" Value="SteelBlue" />
</Trigger>
</Style.Triggers>
</Style>
</ListBox.ItemContainerStyle>
这会产生更好的结果。瓷砖之间没有可见的 spaces,我可以自定义选定和取消选择的瓷砖的外观。
我唯一不知道该怎么做的是有一条粗细为 1 像素的边框线。我得到两条粗细为 1 的线,导致粗细为 2。我尝试将边距和填充设置为 0 和 -1。没有运气。奇怪的是,将 BorderBrush 设置为 Black 并将 BorderThickness 设置为 0.5 不会生成粗细为 1 的组合黑线,而是粗细为 2 的灰线。
与我使用标签时相比,内存使用量下降了约 150MB,但加载时间仍然很糟糕,所以我仍在寻找更好的解决方案。
编辑 2:
我玩了一会儿边距,终于搞定了。通过将 ListBoxItem 的边距设置为 -0.5,我得到了正确的边框厚度,将瓷砖稍微压在一起。我想保留这个问题,希望有人能提出更有效的解决方案。 UI 一旦变为 运行 就会响应,但加载时间很糟糕。在我的应用程序的使用过程中,随着过滤器的调整和查询的进行,列表框将被反复重新填充。我希望这能尽快发生。另外,我认为内存使用率过高。
由于我没有足够的代表来 post 发表评论,我将尝试回答,或至少指出正确的方向。
1) 你得到 Thickness of 2 的原因是相邻的边界加入了它们各自的厚度。您使用的 -0.5 边距使它们相交,使合成厚度为 1。相反,您可以尝试设置右侧和底部的边框厚度,例如:
<Setter Property="BorderThickness" Value="0,0,1,1"/>
这意味着 LeftSide=0,Top=0,RightSide=1,Bottom=1 厚。所以最左边的项目不会有左边框,最上面的项目不会有顶部边框。因此相邻的边界不会合并,厚度加倍。
2) 以及内存使用过多和整体性能低下。当您将 WrapPanel 用于 ItemsPanel 时,您将放弃 UI 虚拟化。因为通常非 UI 计算不会花费太多时间,所以首先要考虑的是 UI 性能。尝试加载您的数据并在没有 UI 的情况下进行计算,我认为差异会很明显。
我建议您对 ListBox ItemsPanel 使用某种 VirtualizingWrapPanel。我个人使用这个,并根据更有经验的编码员的建议进行更正:
First correction
Second correction
我正在尝试构建一个类似于第一个屏幕截图中的 GUI:
基本上它是一个图块视图,显示在带边框的网格中,随着 window 调整大小时动态更改列数。项目是可选择的。这些可能看起来像图片,但它们只是 Unicode 字符。
我制作了一个自定义对象列表,并成功地将列表绑定到一个ListBox控件。自然地,这给了我一个垂直堆叠的字符列表。
所以我用谷歌搜索了一下,发现我可以为列表框设置一个自定义的 ItemsPanelTemplate:
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Margin="0"/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
结果如下:
为了设置图块的边框和大小,我还添加了一个 ItemTemplate:
<ListBox Margin="0,0,0,0" Padding="0,0,0,0" Grid.Row="0" Grid.Column="0" x:Name="tw"
SelectionChanged="tw_SelectionChanged"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ScrollViewer.VerticalScrollBarVisibility="Visible">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Margin="0,0,0,0"/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<Label
Padding="0,0,0,0"
Margin="0,0,0,0"
Width="50"
Height="50"
FontSize="20"
Background="White"
HorizontalAlignment="Left"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
BorderThickness="1"
BorderBrush="Black"
Content="{Binding Path=LiteralString}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
结果如下:
第一个问题是,如您所见,我无法摆脱的瓷砖之间有 spaces。您会在标记中注意到,我已尝试尽可能手动将 Margin 和 Padding 设置为 0。 space 还在。
第二个问题是,您无法真正看到选择了哪个项目。从下数第 3 个字符和从右数第 2 个字符被选中,但蓝色被标签覆盖。您只能在字符左侧的 space 中看到它。我不知道如何解决这个问题。
另一个潜在的问题是性能。该视图最终将具有过滤器,但是当所有过滤器都关闭时,将显示大约 6500 个项目。当我只有一个基本的 ListBox 时,应用程序在 运行 时占用了大约 28MB 的 RAM。当我添加带有 WrapPanel 的 ItemsPanelTemplate 时,内存使用量跃升至大约 136MB。当我添加带有标签的 ItemTemplate 时,内存使用量跃升至 183MB,更糟糕的是,window 现在需要更长的时间来加载。
我试过用 TextBlocks 替换 Labels,因为我读到它们更轻巧。事情变快了,内存使用量下降到 130MB 左右,但 TextBlocks 并不好。我不能将文本居中放置,也不能给它们加边框。正如您在屏幕截图中所见,所选项目变得一团糟:
诚然,我的电脑速度很慢,183MB 也不算死,但还是有点低效。我打算在这个应用程序中加入更多的功能,如果这一件事占用了这么多的 RAM,那么最终的应用程序很可能是一个无法使用的资源吸血鬼。另外,我相信我正在模仿的应用程序也是用 .NET 编写的(虽然我不确定它是否是 WPF)并且那个应用程序在显示视图时只使用 17.5MB 的 RAM。
是否有另一种方法可以做到这一点,一种既更高效又更容易定制的方法?
谢谢。
编辑:
我找到了稍微好一点的方法。我完全删除了 ItemTemplate,而是使用 ItemContainerStyle:
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Style.Setters>
<Setter Property="BorderBrush" Value="Black"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Width" Value="50"/>
<Setter Property="Height" Value="50"/>
<Setter Property="FontSize" Value="20"/>
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
</Style.Setters>
<Style.Triggers>
<Trigger Property="IsSelected" Value="True" >
<Setter Property="FontWeight" Value="Bold" />
<Setter Property="Background" Value="SteelBlue" />
</Trigger>
</Style.Triggers>
</Style>
</ListBox.ItemContainerStyle>
这会产生更好的结果。瓷砖之间没有可见的 spaces,我可以自定义选定和取消选择的瓷砖的外观。
我唯一不知道该怎么做的是有一条粗细为 1 像素的边框线。我得到两条粗细为 1 的线,导致粗细为 2。我尝试将边距和填充设置为 0 和 -1。没有运气。奇怪的是,将 BorderBrush 设置为 Black 并将 BorderThickness 设置为 0.5 不会生成粗细为 1 的组合黑线,而是粗细为 2 的灰线。
与我使用标签时相比,内存使用量下降了约 150MB,但加载时间仍然很糟糕,所以我仍在寻找更好的解决方案。
编辑 2:
我玩了一会儿边距,终于搞定了。通过将 ListBoxItem 的边距设置为 -0.5,我得到了正确的边框厚度,将瓷砖稍微压在一起。我想保留这个问题,希望有人能提出更有效的解决方案。 UI 一旦变为 运行 就会响应,但加载时间很糟糕。在我的应用程序的使用过程中,随着过滤器的调整和查询的进行,列表框将被反复重新填充。我希望这能尽快发生。另外,我认为内存使用率过高。
由于我没有足够的代表来 post 发表评论,我将尝试回答,或至少指出正确的方向。 1) 你得到 Thickness of 2 的原因是相邻的边界加入了它们各自的厚度。您使用的 -0.5 边距使它们相交,使合成厚度为 1。相反,您可以尝试设置右侧和底部的边框厚度,例如:
<Setter Property="BorderThickness" Value="0,0,1,1"/>
这意味着 LeftSide=0,Top=0,RightSide=1,Bottom=1 厚。所以最左边的项目不会有左边框,最上面的项目不会有顶部边框。因此相邻的边界不会合并,厚度加倍。
2) 以及内存使用过多和整体性能低下。当您将 WrapPanel 用于 ItemsPanel 时,您将放弃 UI 虚拟化。因为通常非 UI 计算不会花费太多时间,所以首先要考虑的是 UI 性能。尝试加载您的数据并在没有 UI 的情况下进行计算,我认为差异会很明显。 我建议您对 ListBox ItemsPanel 使用某种 VirtualizingWrapPanel。我个人使用这个,并根据更有经验的编码员的建议进行更正:
First correction
Second correction