合并控件
This commit is contained in:
1 parent
233ec5d58d
commit
b842292817
11 files changed
+1400
-463
No files matched your search
+1
-1
@@ -11,7 +11,7 @@
|
||||
<ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/Basic.xaml" />
|
||||
<ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/SkinDefault.xaml" />
|
||||
<ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/Theme.xaml" />
|
||||
<ResourceDictionary Source="Themes/Generic.xaml" />
|
||||
<ResourceDictionary Source="StyledDataGrid.xaml" />
|
||||
<ResourceDictionary Source="Dictionary2.xaml" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using Prism.Mvvm;
|
||||
|
||||
namespace Main.Test;
|
||||
|
||||
public class ItemInfo : BindableBase
|
||||
{
|
||||
public int Code { get; init; }
|
||||
public string Name { get; init; } = "";
|
||||
public string Value => Price.ToString("N2", CultureInfo.CurrentCulture);
|
||||
public decimal Price { get; init; }
|
||||
public int Qty { get; init; }
|
||||
public string Sku { get; init; } = "";
|
||||
public string Shop { get; init; } = "";
|
||||
public string Buyer { get; init; } = "";
|
||||
public string Address { get; init; } = "";
|
||||
public string Remark { get; init; } = "";
|
||||
public DateTime CreatedAt { get; init; }
|
||||
public ImageSource Thumbnail { get; init; } = null!;
|
||||
public string? ThumbnailUrl { get; init; }
|
||||
private string status = "待处理";
|
||||
public string Status { get => status; set => SetProperty(ref status, value); }
|
||||
}
|
||||
|
||||
internal static class DemoData
|
||||
{
|
||||
// 冻结并共享小尺寸位图,滚动不触发网络请求或重复解码。
|
||||
private static readonly ImageSource[] Thumbnails = CreateThumbnails();
|
||||
|
||||
public static List<ItemInfo> Create(int count, int networkImageSize = 0)
|
||||
{
|
||||
string[] names = ["轻量通勤双肩包", "纯棉宽松短袖", "旅行收纳组合", "便携保温水杯", "无线办公鼠标", "桌面收纳置物盒", "运动休闲鞋", "折叠防晒遮阳伞"];
|
||||
string[] shops = ["城市生活旗舰店", "优选数码专营店", "日常好物集合店", "远行户外用品店"];
|
||||
var random = new Random(20261022);
|
||||
var session = Guid.NewGuid().ToString("N");
|
||||
var result = new List<ItemInfo>(count);
|
||||
for (var i = 0; i < count; i++)
|
||||
result.Add(new ItemInfo
|
||||
{
|
||||
Code = 100000 + i, Name = names[i % names.Length] + " · 秋季新品 / 品质款",
|
||||
Price = random.Next(1900, 69900) / 100m, Qty = random.Next(1, 100),
|
||||
Sku = $"SKU-{i:000000} / {(i % 2 == 0 ? "雾蓝" : "米白")} / 标准款",
|
||||
Shop = shops[i % shops.Length], Buyer = $"买家{i:00000}",
|
||||
Address = $"浙江省杭州市西湖区 文一路 {i % 300 + 1} 号 · 联系尾号 {i % 10000:0000}",
|
||||
Remark = i % 3 == 0 ? "请核对规格,周末可收货;外包装请加固。" : "常规发货 / 已核对收货信息",
|
||||
CreatedAt = new DateTime(2026, 10, 22, 12, 0, 0).AddMinutes(-i),
|
||||
Status = new[] { "待处理", "已确认", "待发货", "已完成" }[i % 4],
|
||||
Thumbnail = Thumbnails[i % Thumbnails.Length],
|
||||
ThumbnailUrl = networkImageSize == 0 ? null : $"https://picsum.photos/seed/{session}-{i}/{networkImageSize}/{networkImageSize}"
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
private static ImageSource[] CreateThumbnails()
|
||||
{
|
||||
var images = new ImageSource[16];
|
||||
for (var i = 0; i < images.Length; i++)
|
||||
{
|
||||
var visual = new DrawingVisual();
|
||||
using (var dc = visual.RenderOpen())
|
||||
{
|
||||
var color = Color.FromRgb((byte)(55 + i * 11), (byte)(100 + i * 6), (byte)(165 - i * 4));
|
||||
dc.DrawRoundedRectangle(new SolidColorBrush(Color.FromRgb(241, 245, 249)), null, new Rect(0, 0, 72, 72), 8, 8);
|
||||
var brush = new SolidColorBrush(color);
|
||||
var pen = new Pen(brush, 4);
|
||||
dc.DrawRoundedRectangle(null, pen, new Rect(25, 12, 22, 24), 10, 10);
|
||||
dc.DrawRoundedRectangle(brush, null, new Rect(14, 26, 44, 36), 6, 6);
|
||||
dc.DrawLine(new Pen(Brushes.White, 2), new Point(24, 44), new Point(48, 44));
|
||||
}
|
||||
var bitmap = new RenderTargetBitmap(72, 72, 96, 96, PixelFormats.Pbgra32);
|
||||
bitmap.Render(visual); bitmap.Freeze(); images[i] = bitmap;
|
||||
}
|
||||
return images;
|
||||
}
|
||||
}
|
||||
+250
-36
@@ -3,59 +3,273 @@
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:hc="https://handyorg.github.io/handycontrol"
|
||||
xmlns:local="clr-namespace:Main.Test"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:tf="Trade.Framework"
|
||||
xmlns:views="clr-namespace:Main.Test.Views"
|
||||
xmlns:vm="clr-namespace:Main.Test.ViewModels"
|
||||
Title="MainWindow"
|
||||
Width="800"
|
||||
Height="450"
|
||||
Title="复杂表格 · 运行测试"
|
||||
Width="1380"
|
||||
Height="820"
|
||||
MinWidth="900"
|
||||
MinHeight="500"
|
||||
DataContext="{tf:DISource Type=vm:MainViewModel}"
|
||||
ResizeMode="CanResizeWithGrip"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Grid Margin="0,0,0,0">
|
||||
<tf:WindowBase.Resources>
|
||||
<Style x:Key="ActionButton" TargetType="Button">
|
||||
<Setter Property="Padding" Value="9,4" />
|
||||
<Setter Property="Margin" Value="0,0,6,0" />
|
||||
<Setter Property="MinWidth" Value="48" />
|
||||
</Style>
|
||||
<ContextMenu x:Key="OrderRowMenu" DataContext="{Binding PlacementTarget.DataContext, RelativeSource={RelativeSource Self}}">
|
||||
<MenuItem
|
||||
Command="{Binding PlacementTarget.Tag.DetailCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}"
|
||||
CommandParameter="{Binding}"
|
||||
Header="查看详情" />
|
||||
<MenuItem
|
||||
Command="{Binding PlacementTarget.Tag.ConfirmCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}"
|
||||
CommandParameter="{Binding}"
|
||||
Header="确认 / 撤销确认" />
|
||||
<Separator />
|
||||
<MenuItem
|
||||
Command="{Binding PlacementTarget.Tag.CopyCodeCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}"
|
||||
CommandParameter="{Binding}"
|
||||
Header="复制编码" />
|
||||
</ContextMenu>
|
||||
</tf:WindowBase.Resources>
|
||||
<Grid Margin="16">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<DockPanel Margin="0,0,0,12">
|
||||
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal">
|
||||
<ComboBox
|
||||
x:Name="ImageMode"
|
||||
Width="155"
|
||||
Margin="0,0,8,0"
|
||||
VerticalContentAlignment="Center"
|
||||
SelectedIndex="0">
|
||||
<ComboBoxItem Content="本地图片" Tag="0" />
|
||||
<ComboBoxItem Content="Picsum 160px" Tag="160" />
|
||||
<ComboBoxItem Content="Picsum 800px" Tag="800" />
|
||||
</ComboBox>
|
||||
<ComboBox
|
||||
x:Name="DataSize"
|
||||
Width="115"
|
||||
Margin="0,0,8,0"
|
||||
VerticalContentAlignment="Center"
|
||||
SelectedIndex="1">
|
||||
<ComboBoxItem Content="1,000 行" Tag="1000" />
|
||||
<ComboBoxItem Content="10,000 行" Tag="10000" />
|
||||
<ComboBoxItem Content="50,000 行" Tag="50000" />
|
||||
</ComboBox>
|
||||
<Button
|
||||
x:Name="LoadButton"
|
||||
Click="LoadData_Click"
|
||||
Content="加载数据"
|
||||
Style="{StaticResource ActionButton}" />
|
||||
<Button
|
||||
x:Name="ImageBenchmarkButton"
|
||||
Click="ImageBenchmark_Click"
|
||||
Content="图片滚动测试"
|
||||
Style="{StaticResource ActionButton}" />
|
||||
<Button
|
||||
x:Name="BenchmarkButton"
|
||||
Click="Benchmark_Click"
|
||||
Content="运行性能测试"
|
||||
Style="{StaticResource ActionButton}" />
|
||||
</StackPanel>
|
||||
<StackPanel>
|
||||
<TextBlock
|
||||
FontSize="20"
|
||||
FontWeight="SemiBold"
|
||||
Text="订单工作台 / 复杂内容测试" />
|
||||
<StackPanel Margin="0,5,0,0" Orientation="Horizontal">
|
||||
<TextBlock Foreground="#64748B" Text="{Binding Data.Count, StringFormat=共 {0:N0} 行}" />
|
||||
<TextBlock
|
||||
Margin="12,0,0,0"
|
||||
Foreground="#64748B"
|
||||
Text="{Binding SelectedCount, StringFormat=已选 {0:N0} 行}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</DockPanel>
|
||||
<local:StyledDataGrid
|
||||
Height="Auto"
|
||||
x:Name="OrdersGrid"
|
||||
Grid.Row="1"
|
||||
d:ItemsSource="{d:SampleData ItemCount=10}"
|
||||
local:DataGridHelper.ShowBorder="False"
|
||||
GridLinesVisibility="All"
|
||||
GridLinesVisibility="Horizontal"
|
||||
IsReadOnly="True"
|
||||
ItemsSource="{Binding Data}"
|
||||
SelectedRowForeground="White"
|
||||
RowContextMenu="{StaticResource OrderRowMenu}"
|
||||
RowDetailsVisibilityMode="Collapsed"
|
||||
RowHeight="92"
|
||||
SelectRowOnRightClick="True"
|
||||
SelectedItems="{Binding SelectedItems, Mode=TwoWay}"
|
||||
SelectionMode="Extended"
|
||||
ShowCheckBox="True"
|
||||
ShowRowNumber="True">
|
||||
ShowRowNumber="True"
|
||||
ShowSelectAll="True">
|
||||
<local:StyledDataGrid.Columns>
|
||||
<DataGridTextColumn
|
||||
Width="1*"
|
||||
MinWidth="100"
|
||||
Binding="{Binding Code, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
Header="编码"
|
||||
IsReadOnly="True" />
|
||||
Width="90"
|
||||
Binding="{Binding Code}"
|
||||
Header="编码" />
|
||||
<DataGridTemplateColumn
|
||||
Width="300"
|
||||
Header="商品 / 规格"
|
||||
SortMemberPath="Name">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<Grid Margin="0,7">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="68" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Border
|
||||
Width="58"
|
||||
Height="58"
|
||||
Background="#F1F5F9"
|
||||
CornerRadius="6">
|
||||
<local:NetworkThumbnail
|
||||
Width="58"
|
||||
Height="58"
|
||||
Placeholder="{Binding Thumbnail}"
|
||||
Stretch="Uniform"
|
||||
Url="{Binding ThumbnailUrl}" />
|
||||
</Border>
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center">
|
||||
<TextBlock
|
||||
FontWeight="SemiBold"
|
||||
Text="{Binding Name}"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
ToolTip="{Binding Name}" />
|
||||
<TextBlock
|
||||
Margin="0,5,0,0"
|
||||
FontSize="11"
|
||||
Opacity="0.65"
|
||||
Text="{Binding Sku}" />
|
||||
<TextBlock
|
||||
Margin="0,4,0,0"
|
||||
FontSize="11"
|
||||
Opacity="0.65"
|
||||
Text="{Binding Shop}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
<DataGridTextColumn
|
||||
Width="1*"
|
||||
MinWidth="100"
|
||||
Binding="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
Header="名称"
|
||||
IsReadOnly="True" />
|
||||
<DataGridTextColumn
|
||||
Width="1*"
|
||||
MinWidth="100"
|
||||
Binding="{Binding Value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
Header="值"
|
||||
IsReadOnly="True" />
|
||||
<DataGridTextColumn
|
||||
Width="1*"
|
||||
MinWidth="100"
|
||||
Binding="{Binding Qty, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
Width="90"
|
||||
Binding="{Binding Price, StringFormat=N2}"
|
||||
CellStyle="{DynamicResource StyledNumberCellStyle}"
|
||||
Header="数字"
|
||||
HeaderStyle="{DynamicResource DefaultNumberCellHeaderStyle}"
|
||||
IsReadOnly="True" />
|
||||
Header="单价 / 元"
|
||||
HeaderStyle="{DynamicResource DefaultNumberCellHeaderStyle}" />
|
||||
<DataGridTextColumn
|
||||
Width="65"
|
||||
Binding="{Binding Qty}"
|
||||
CellStyle="{DynamicResource StyledNumberCellStyle}"
|
||||
Header="数量"
|
||||
HeaderStyle="{DynamicResource DefaultNumberCellHeaderStyle}" />
|
||||
<DataGridTemplateColumn
|
||||
Width="230"
|
||||
Header="买家 / 收货信息"
|
||||
SortMemberPath="Buyer">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel VerticalAlignment="Center">
|
||||
<TextBlock FontWeight="SemiBold" Text="{Binding Buyer}" />
|
||||
<TextBlock
|
||||
Margin="0,6,0,0"
|
||||
FontSize="11"
|
||||
Opacity="0.7"
|
||||
Text="{Binding Address}"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
<DataGridTemplateColumn
|
||||
Width="95"
|
||||
Header="状态"
|
||||
SortMemberPath="Status">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<Border
|
||||
Padding="8,4"
|
||||
HorizontalAlignment="Left"
|
||||
Background="#E8F4FF"
|
||||
CornerRadius="4">
|
||||
<TextBlock
|
||||
FontSize="12"
|
||||
Foreground="#1769AA"
|
||||
Text="{Binding Status}" />
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
<DataGridTextColumn
|
||||
Width="105"
|
||||
Binding="{Binding CreatedAt, StringFormat=MM-dd HH:mm}"
|
||||
Header="创建时间" />
|
||||
<DataGridTemplateColumn
|
||||
Width="165"
|
||||
Header="备注"
|
||||
SortMemberPath="Remark">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock
|
||||
MaxHeight="60"
|
||||
VerticalAlignment="Center"
|
||||
Text="{Binding Remark}"
|
||||
TextWrapping="Wrap" />
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
<DataGridTemplateColumn Width="145" Header="操作">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel VerticalAlignment="Center" Orientation="Horizontal">
|
||||
<Button
|
||||
Command="{Binding DataContext.DetailCommand, RelativeSource={RelativeSource AncestorType=local:StyledDataGrid}}"
|
||||
CommandParameter="{Binding}"
|
||||
Content="详情"
|
||||
Style="{StaticResource ActionButton}" />
|
||||
<Button
|
||||
Command="{Binding DataContext.ConfirmCommand, RelativeSource={RelativeSource AncestorType=local:StyledDataGrid}}"
|
||||
CommandParameter="{Binding}"
|
||||
Content="确认"
|
||||
Style="{StaticResource ActionButton}" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
</local:StyledDataGrid.Columns>
|
||||
</local:StyledDataGrid>
|
||||
<TextBlock
|
||||
Grid.Row="2"
|
||||
Margin="0,10,0,6"
|
||||
Text="{Binding ActionMessage}"
|
||||
TextWrapping="Wrap" />
|
||||
<Border
|
||||
Grid.Row="3"
|
||||
Padding="10,8"
|
||||
Background="#F1F5F9"
|
||||
CornerRadius="5">
|
||||
<StackPanel>
|
||||
<TextBlock
|
||||
x:Name="ImageStatisticsText"
|
||||
Margin="0,0,0,4"
|
||||
Foreground="#475569"
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock
|
||||
x:Name="PerformanceText"
|
||||
Foreground="#475569"
|
||||
Text="选择数据量后运行性能测试:连续滚动 60 次,并测量全选、取消全选及已生成行数。"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
</tf:WindowBase>
|
||||
+143
-25
@@ -1,35 +1,153 @@
|
||||
using Bogus;
|
||||
using System.Text;
|
||||
using System.Diagnostics;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Navigation;
|
||||
using System.Windows.Shapes;
|
||||
using System.Windows.Threading;
|
||||
using Main.Test.ViewModels;
|
||||
using Trade.Framework.Controls;
|
||||
|
||||
namespace Main.Test;
|
||||
/// <summary>
|
||||
/// Interaction logic for MainWindow.xaml
|
||||
/// </summary>
|
||||
|
||||
public partial class MainWindow : WindowBase
|
||||
{
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
var mainHeaderImageContent = new MainHeaderImageContent();
|
||||
//NonClientAreaHeight = mainHeaderImageContent.Height;
|
||||
//NonClientAreaContent = mainHeaderImageContent;
|
||||
}
|
||||
}
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
var timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(500) };
|
||||
timer.Tick += (_, _) => ImageStatisticsText.Text = ThumbnailLoader.Statistics;
|
||||
Loaded += (_, _) => timer.Start();
|
||||
Closed += (_, _) => timer.Stop();
|
||||
}
|
||||
|
||||
public class ItemInfo
|
||||
{
|
||||
public int Code { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Value { get; set; }
|
||||
public int Qty { get; set; }
|
||||
}
|
||||
private async void LoadData_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
SetBusy(true);
|
||||
try
|
||||
{
|
||||
var count = int.Parse((string)((ComboBoxItem)DataSize.SelectedItem).Tag);
|
||||
var watch = Stopwatch.StartNew();
|
||||
var vm = (MainViewModel)DataContext;
|
||||
vm.NetworkImageSize = int.Parse((string)((ComboBoxItem)ImageMode.SelectedItem).Tag);
|
||||
await vm.LoadAsync(count);
|
||||
OrdersGrid.ScrollIntoView(OrdersGrid.Items[0]);
|
||||
await Dispatcher.Yield(DispatcherPriority.ApplicationIdle);
|
||||
PerformanceText.Text = $"加载 {count:N0} 行(含生成与首次布局):{watch.Elapsed.TotalMilliseconds:N0} ms。可继续测试滚动和全选。";
|
||||
}
|
||||
catch (Exception ex) { PerformanceText.Text = $"加载失败:{ex.Message}"; }
|
||||
finally { SetBusy(false); }
|
||||
}
|
||||
|
||||
private async void Benchmark_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
SetBusy(true);
|
||||
try { PerformanceText.Text = await RunPerformanceTestAsync(); }
|
||||
catch (Exception ex) { PerformanceText.Text = $"测试失败:{ex.Message}"; }
|
||||
finally { SetBusy(false); }
|
||||
}
|
||||
|
||||
private async void ImageBenchmark_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
SetBusy(true);
|
||||
try { PerformanceText.Text = await RunImagePerformanceTestAsync(); }
|
||||
catch (Exception ex) { PerformanceText.Text = $"图片测试失败:{ex.Message}"; }
|
||||
finally { SetBusy(false); }
|
||||
}
|
||||
|
||||
public async Task<string> RunImagePerformanceTestAsync()
|
||||
{
|
||||
if (OrdersGrid.Items.Count == 0 || ((MainViewModel)DataContext).NetworkImageSize == 0)
|
||||
return "请先选择 Picsum 图片模式并点击加载数据,再运行图片滚动测试。";
|
||||
var viewer = FindVisual<ScrollViewer>(OrdersGrid).First();
|
||||
var offset = viewer.VerticalOffset;
|
||||
var samples = new[] { new List<double>(), new List<double>() };
|
||||
var heartbeat = Stopwatch.StartNew();
|
||||
var lastTick = heartbeat.Elapsed.TotalMilliseconds;
|
||||
var maxDelay = 0d;
|
||||
var pulse = new DispatcherTimer(DispatcherPriority.Normal) { Interval = TimeSpan.FromMilliseconds(50) };
|
||||
pulse.Tick += (_, _) =>
|
||||
{
|
||||
var now = heartbeat.Elapsed.TotalMilliseconds;
|
||||
maxDelay = Math.Max(maxDelay, Math.Max(0, now - lastTick - 50));
|
||||
lastTick = now;
|
||||
};
|
||||
pulse.Start();
|
||||
try
|
||||
{
|
||||
viewer.ScrollToHorizontalOffset(0);
|
||||
for (var pass = 0; pass < 2; pass++)
|
||||
{
|
||||
PerformanceText.Text = pass == 0 ? "首次滚动:网络图片正在加载…" : "重复访问相同位置:比较缓存与重新加载…";
|
||||
for (var i = 0; i < 40; i++)
|
||||
{
|
||||
var watch = Stopwatch.StartNew();
|
||||
viewer.ScrollToVerticalOffset(i % 24);
|
||||
await Dispatcher.Yield(DispatcherPriority.ApplicationIdle);
|
||||
samples[pass].Add(watch.Elapsed.TotalMilliseconds);
|
||||
await Task.Delay(150);
|
||||
}
|
||||
}
|
||||
// 有界等待当前可见图片,避免把尚未完成的请求当作加载成功。
|
||||
var wait = Stopwatch.StartNew();
|
||||
while (ThumbnailLoader.Pending > 0 && wait.Elapsed < TimeSpan.FromSeconds(8)) await Task.Delay(100);
|
||||
var first = samples[0].Order().ToArray(); var revisit = samples[1].Order().ToArray();
|
||||
return $"Picsum {((MainViewModel)DataContext).NetworkImageSize}px / {OrdersGrid.Items.Count:N0} 行 | 首次滚动均值 {first.Average():N1} ms / P95 {first[37]:N1} ms | 复访均值 {revisit.Average():N1} ms / P95 {revisit[37]:N1} ms | UI 心跳最大延迟 {maxDelay:N1} ms(非帧率);图片统计为本次进程累计,取消表示滚出视口。";
|
||||
}
|
||||
finally { pulse.Stop(); viewer.ScrollToVerticalOffset(offset); }
|
||||
}
|
||||
|
||||
// 测量 UI 线程滚动与布局响应,不等同于 GPU 帧率。
|
||||
public async Task<string> RunPerformanceTestAsync()
|
||||
{
|
||||
if (OrdersGrid.Items.Count == 0) return "没有可测试的数据。";
|
||||
var viewer = FindVisual<ScrollViewer>(OrdersGrid).First();
|
||||
var offset = viewer.VerticalOffset;
|
||||
var selected = ((DataGrid)OrdersGrid).SelectedItems.Cast<object>().ToArray();
|
||||
var samples = new List<double>();
|
||||
try
|
||||
{
|
||||
OrdersGrid.UnselectAll();
|
||||
OrdersGrid.ScrollIntoView(OrdersGrid.Items[0]);
|
||||
viewer.ScrollToHorizontalOffset(0);
|
||||
await Dispatcher.Yield(DispatcherPriority.ApplicationIdle);
|
||||
for (var i = 0; i < 60; i++)
|
||||
{
|
||||
var watch = Stopwatch.StartNew();
|
||||
viewer.ScrollToVerticalOffset(viewer.ScrollableHeight * ((i * 17) % 60) / 59d);
|
||||
await Dispatcher.Yield(DispatcherPriority.ApplicationIdle);
|
||||
samples.Add(watch.Elapsed.TotalMilliseconds);
|
||||
}
|
||||
var selectWatch = Stopwatch.StartNew();
|
||||
OrdersGrid.SelectAll();
|
||||
await Dispatcher.Yield(DispatcherPriority.ApplicationIdle);
|
||||
var selectMs = selectWatch.Elapsed.TotalMilliseconds;
|
||||
selectWatch.Restart(); OrdersGrid.UnselectAll();
|
||||
await Dispatcher.Yield(DispatcherPriority.ApplicationIdle);
|
||||
var clearMs = selectWatch.Elapsed.TotalMilliseconds;
|
||||
var realized = FindVisual<DataGridRow>(OrdersGrid).Count();
|
||||
samples.Sort();
|
||||
return $"{OrdersGrid.Items.Count:N0} 行 / 9 列 | 滚动与布局:均值 {samples.Average():N1} ms,P95 {samples[56]:N1} ms,最大 {samples[^1]:N1} ms | 全选 {selectMs:N0} ms / 取消 {clearMs:N0} ms | 已生成行 {realized} | 进程内存 {Process.GetCurrentProcess().WorkingSet64 / 1024 / 1024} MB(不等同帧率)";
|
||||
}
|
||||
finally
|
||||
{
|
||||
OrdersGrid.RestoreSelection(selected);
|
||||
viewer.ScrollToVerticalOffset(offset);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetBusy(bool busy)
|
||||
{
|
||||
LoadButton.IsEnabled = BenchmarkButton.IsEnabled = ImageBenchmarkButton.IsEnabled = DataSize.IsEnabled = ImageMode.IsEnabled = !busy;
|
||||
OrdersGrid.IsEnabled = !busy;
|
||||
}
|
||||
|
||||
private static IEnumerable<T> FindVisual<T>(DependencyObject parent) where T : DependencyObject
|
||||
{
|
||||
for (var i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
|
||||
{
|
||||
var child = VisualTreeHelper.GetChild(parent, i);
|
||||
if (child is T match) yield return match;
|
||||
foreach (var descendant in FindVisual<T>(child)) yield return descendant;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Threading;
|
||||
|
||||
namespace Main.Test;
|
||||
|
||||
public partial class StyledDataGrid
|
||||
{
|
||||
private DispatcherTimer? imageResumeTimer;
|
||||
private bool draggingScrollThumb;
|
||||
|
||||
private void InitializeImageLoadingPause()
|
||||
{
|
||||
imageResumeTimer = new DispatcherTimer(DispatcherPriority.Background, Dispatcher)
|
||||
{
|
||||
Interval = TimeSpan.FromMilliseconds(500)
|
||||
};
|
||||
imageResumeTimer.Tick += (_, _) =>
|
||||
{
|
||||
imageResumeTimer.Stop();
|
||||
if (!draggingScrollThumb && IsLoaded) SetImageLoadingSuspended(false);
|
||||
};
|
||||
AddHandler(Thumb.DragStartedEvent, new DragStartedEventHandler(ScrollThumbDragStarted), true);
|
||||
AddHandler(Thumb.DragCompletedEvent, new DragCompletedEventHandler(ScrollThumbDragCompleted), true);
|
||||
AddHandler(ScrollViewer.ScrollChangedEvent, new ScrollChangedEventHandler(ImageScrollChanged), true);
|
||||
Unloaded += (_, _) =>
|
||||
{
|
||||
imageResumeTimer.Stop(); draggingScrollThumb = false;
|
||||
SetImageLoadingSuspended(true);
|
||||
};
|
||||
Loaded += (_, _) => { ApplyImageLoadingOptions(); SetImageLoadingSuspended(false); };
|
||||
ApplyImageLoadingOptions();
|
||||
}
|
||||
|
||||
private void SetImageLoadingSuspended(bool suspended)
|
||||
{
|
||||
SetValue(IsImageLoadingSuspendedPropertyKey, suspended);
|
||||
NetworkThumbnail.SetIsLoadingSuspended(this, suspended);
|
||||
}
|
||||
|
||||
private void ApplyImageLoadingOptions()
|
||||
{
|
||||
NetworkThumbnail.SetCacheEnabled(this, EnableImageCache);
|
||||
if (imageResumeTimer == null) return;
|
||||
if (!PauseImageLoadingWhileDragging)
|
||||
{
|
||||
imageResumeTimer.Stop(); draggingScrollThumb = false; SetImageLoadingSuspended(false);
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsOwnScrollBar(DependencyObject? source)
|
||||
{
|
||||
var foundScrollBar = false;
|
||||
for (var parent = source; parent != null; parent = VisualTreeHelper.GetParent(parent))
|
||||
{
|
||||
if (parent is ScrollBar) foundScrollBar = true;
|
||||
if (parent is DataGrid) return ReferenceEquals(parent, this) && foundScrollBar;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void ScrollThumbDragStarted(object sender, DragStartedEventArgs e)
|
||||
{
|
||||
if (!PauseImageLoadingWhileDragging || !IsOwnScrollBar(e.OriginalSource as DependencyObject)) return;
|
||||
draggingScrollThumb = true;
|
||||
imageResumeTimer!.Stop();
|
||||
// 继承到可见图片及随后回收生成的新行,立即取消请求并清空图片。
|
||||
SetImageLoadingSuspended(true);
|
||||
}
|
||||
|
||||
private void ScrollThumbDragCompleted(object sender, DragCompletedEventArgs e)
|
||||
{
|
||||
if (!PauseImageLoadingWhileDragging || !IsOwnScrollBar(e.OriginalSource as DependencyObject)) return;
|
||||
draggingScrollThumb = false;
|
||||
RestartImageResumeTimer();
|
||||
}
|
||||
|
||||
private void ImageScrollChanged(object sender, ScrollChangedEventArgs e)
|
||||
{
|
||||
if (!NetworkThumbnail.GetIsLoadingSuspended(this) || draggingScrollThumb ||
|
||||
(e.VerticalChange == 0 && e.HorizontalChange == 0) ||
|
||||
!IsOwnScrollViewer(e.OriginalSource as DependencyObject)) return;
|
||||
RestartImageResumeTimer();
|
||||
}
|
||||
|
||||
private bool IsOwnScrollViewer(DependencyObject? source)
|
||||
{
|
||||
for (var parent = source; parent != null; parent = VisualTreeHelper.GetParent(parent))
|
||||
if (parent is DataGrid) return ReferenceEquals(parent, this);
|
||||
return false;
|
||||
}
|
||||
|
||||
private void RestartImageResumeTimer()
|
||||
{
|
||||
imageResumeTimer!.Stop();
|
||||
imageResumeTimer.Start();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Main.Test;
|
||||
|
||||
public partial class StyledDataGrid
|
||||
{
|
||||
public static readonly DependencyProperty NoDataTipsProperty = DataGridHelper.NoDataTipsProperty.AddOwner(typeof(StyledDataGrid));
|
||||
public string NoDataTips { get => (string)GetValue(NoDataTipsProperty); set => SetValue(NoDataTipsProperty, value); }
|
||||
public static readonly DependencyProperty IsActiveProperty = DataGridHelper.IsActiveProperty.AddOwner(typeof(StyledDataGrid));
|
||||
/// <summary>为 true 时抑制空数据提示;可在业务加载数据期间设置。</summary>
|
||||
public bool IsActive { get => (bool)GetValue(IsActiveProperty); set => SetValue(IsActiveProperty, value); }
|
||||
|
||||
public static readonly DependencyProperty RowContextMenuProperty = DependencyProperty.Register(nameof(RowContextMenu), typeof(ContextMenu), typeof(StyledDataGrid), new PropertyMetadata(null));
|
||||
public ContextMenu? RowContextMenu { get => (ContextMenu?)GetValue(RowContextMenuProperty); set => SetValue(RowContextMenuProperty, value); }
|
||||
|
||||
public static readonly DependencyProperty ShowSelectAllProperty = DependencyProperty.Register(nameof(ShowSelectAll), typeof(bool), typeof(StyledDataGrid), new PropertyMetadata(true));
|
||||
public static readonly DependencyProperty ToggleSelectionOnRowHeaderClickProperty = DependencyProperty.Register(nameof(ToggleSelectionOnRowHeaderClick), typeof(bool), typeof(StyledDataGrid), new PropertyMetadata(true));
|
||||
public static readonly DependencyProperty SelectRowOnRightClickProperty = DependencyProperty.Register(nameof(SelectRowOnRightClick), typeof(bool), typeof(StyledDataGrid), new PropertyMetadata(false));
|
||||
public static readonly DependencyProperty PauseImageLoadingWhileDraggingProperty = DependencyProperty.Register(nameof(PauseImageLoadingWhileDragging), typeof(bool), typeof(StyledDataGrid), new PropertyMetadata(true, ImageOptionsChanged));
|
||||
public static readonly DependencyProperty EnableImageCacheProperty = DependencyProperty.Register(nameof(EnableImageCache), typeof(bool), typeof(StyledDataGrid), new PropertyMetadata(true, ImageOptionsChanged));
|
||||
private static readonly DependencyPropertyKey IsImageLoadingSuspendedPropertyKey = DependencyProperty.RegisterReadOnly(nameof(IsImageLoadingSuspended), typeof(bool), typeof(StyledDataGrid), new PropertyMetadata(false));
|
||||
public static readonly DependencyProperty IsImageLoadingSuspendedProperty = IsImageLoadingSuspendedPropertyKey.DependencyProperty;
|
||||
|
||||
public bool ShowSelectAll { get => (bool)GetValue(ShowSelectAllProperty); set => SetValue(ShowSelectAllProperty, value); }
|
||||
public bool ToggleSelectionOnRowHeaderClick { get => (bool)GetValue(ToggleSelectionOnRowHeaderClickProperty); set => SetValue(ToggleSelectionOnRowHeaderClickProperty, value); }
|
||||
public bool SelectRowOnRightClick { get => (bool)GetValue(SelectRowOnRightClickProperty); set => SetValue(SelectRowOnRightClickProperty, value); }
|
||||
/// <summary>默认开启;拖动时暂停并清空图片,停止滚动 500ms 后恢复。</summary>
|
||||
public bool PauseImageLoadingWhileDragging { get => (bool)GetValue(PauseImageLoadingWhileDraggingProperty); set => SetValue(PauseImageLoadingWhileDraggingProperty, value); }
|
||||
/// <summary>默认开启图片缓存;设为 false 时不读取或写入缓存。</summary>
|
||||
public bool EnableImageCache { get => (bool)GetValue(EnableImageCacheProperty); set => SetValue(EnableImageCacheProperty, value); }
|
||||
public bool IsImageLoadingSuspended => (bool)GetValue(IsImageLoadingSuspendedProperty);
|
||||
|
||||
private static void ImageOptionsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
=> ((StyledDataGrid)d).ApplyImageLoadingOptions();
|
||||
|
||||
protected override void OnPreviewMouseRightButtonDown(MouseButtonEventArgs e)
|
||||
{
|
||||
if (SelectRowOnRightClick && !e.Handled && SelectionUnit != DataGridSelectionUnit.Cell && e.OriginalSource is DependencyObject source)
|
||||
{
|
||||
var row = FindRow(source);
|
||||
if (row != null && ItemsControl.ItemsControlFromItemContainer(row) == this && !row.IsSelected &&
|
||||
row.Item != System.Windows.Data.CollectionView.NewItemPlaceholder)
|
||||
{
|
||||
UnselectAll(); row.SetCurrentValue(DataGridRow.IsSelectedProperty, true);
|
||||
}
|
||||
}
|
||||
base.OnPreviewMouseRightButtonDown(e);
|
||||
}
|
||||
}
|
||||
+260
-111
@@ -1,7 +1,9 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Specialized;
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Threading;
|
||||
@@ -9,122 +11,269 @@ using System.Windows.Threading;
|
||||
namespace Main.Test;
|
||||
|
||||
/// <summary>带默认样式、行号和行选择勾选框的 DataGrid。</summary>
|
||||
public class StyledDataGrid : DataGrid
|
||||
public partial class StyledDataGrid : DataGrid
|
||||
{
|
||||
public static readonly RoutedUICommand ToggleSelectAllCommand = new("全选或取消全选", nameof(ToggleSelectAllCommand), typeof(StyledDataGrid));
|
||||
private static readonly DependencyPropertyKey AreAllRowsSelectedPropertyKey = DependencyProperty.RegisterReadOnly(
|
||||
nameof(AreAllRowsSelected), typeof(bool?), typeof(StyledDataGrid), new PropertyMetadata(false));
|
||||
public static readonly DependencyProperty AreAllRowsSelectedProperty = AreAllRowsSelectedPropertyKey.DependencyProperty;
|
||||
/// <summary>全部选中为 true,部分选中为 null,未选中为 false。</summary>
|
||||
public bool? AreAllRowsSelected => (bool?)GetValue(AreAllRowsSelectedProperty);
|
||||
public static readonly DependencyProperty SelectedRowBackgroundProperty = DependencyProperty.Register(
|
||||
nameof(SelectedRowBackground), typeof(Brush), typeof(StyledDataGrid),
|
||||
new PropertyMetadata(new SolidColorBrush(Color.FromRgb(75, 178, 255))));
|
||||
public static readonly DependencyProperty SelectedRowForegroundProperty = DependencyProperty.Register(
|
||||
nameof(SelectedRowForeground), typeof(Brush), typeof(StyledDataGrid), new PropertyMetadata(Brushes.White));
|
||||
public static readonly DependencyProperty ShowCheckBoxProperty = DependencyProperty.Register(
|
||||
nameof(ShowCheckBox), typeof(bool), typeof(StyledDataGrid), new PropertyMetadata(false, HeaderOptionsChanged));
|
||||
public static readonly DependencyProperty ShowRowNumberProperty = DependencyProperty.Register(
|
||||
nameof(ShowRowNumber), typeof(bool), typeof(StyledDataGrid), new PropertyMetadata(false, HeaderOptionsChanged));
|
||||
public static readonly DependencyProperty SelectedItemsProperty = DependencyProperty.Register(
|
||||
nameof(SelectedItems), typeof(IList), typeof(StyledDataGrid),
|
||||
new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnSelectedItemsChanged));
|
||||
|
||||
public Brush SelectedRowBackground { get => (Brush)GetValue(SelectedRowBackgroundProperty); set => SetValue(SelectedRowBackgroundProperty, value); }
|
||||
public Brush SelectedRowForeground { get => (Brush)GetValue(SelectedRowForegroundProperty); set => SetValue(SelectedRowForegroundProperty, value); }
|
||||
/// <summary>勾选状态与行 IsSelected 同步,可通过 SelectedItems 获取。</summary>
|
||||
public bool ShowCheckBox { get => (bool)GetValue(ShowCheckBoxProperty); set => SetValue(ShowCheckBoxProperty, value); }
|
||||
public bool ShowRowNumber { get => (bool)GetValue(ShowRowNumberProperty); set => SetValue(ShowRowNumberProperty, value); }
|
||||
/// <summary>可绑定的选中集合。使用 ObservableCollection 可从 VM 增删选中项;未绑定时返回原生选中集合。</summary>
|
||||
public new IList SelectedItems
|
||||
{
|
||||
get => (IList?)GetValue(SelectedItemsProperty) ?? base.SelectedItems;
|
||||
set => SetValue(SelectedItemsProperty, value);
|
||||
}
|
||||
|
||||
static StyledDataGrid()
|
||||
/// <summary>批量恢复性能测试前的选择,只发出一次选择变更。</summary>
|
||||
internal void RestoreSelection(IEnumerable<object> selected)
|
||||
{
|
||||
DefaultStyleKeyProperty.OverrideMetadata(typeof(StyledDataGrid), new FrameworkPropertyMetadata(typeof(StyledDataGrid)));
|
||||
}
|
||||
|
||||
public StyledDataGrid()
|
||||
{
|
||||
CommandBindings.Add(new CommandBinding(ToggleSelectAllCommand, (_, e) =>
|
||||
var available = Items.Cast<object>().ToHashSet();
|
||||
BeginUpdateSelectedItems();
|
||||
try
|
||||
{
|
||||
if (AreAllRowsSelected == true) UnselectAll();
|
||||
else SelectAll();
|
||||
e.Handled = true;
|
||||
}, (_, e) =>
|
||||
{
|
||||
e.CanExecute = SelectionMode == DataGridSelectionMode.Extended &&
|
||||
SelectionUnit != DataGridSelectionUnit.Cell && SelectableItemCount > 0;
|
||||
e.Handled = true;
|
||||
}));
|
||||
LoadingRow += (_, e) => UpdateRowNumber(e.Row);
|
||||
ItemContainerGenerator.ItemsChanged += (_, _) => QueueHeaderUpdate();
|
||||
Loaded += (_, _) => QueueHeaderUpdate();
|
||||
}
|
||||
|
||||
private static void HeaderOptionsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
=> ((StyledDataGrid)d).QueueHeaderUpdate();
|
||||
|
||||
protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
base.OnItemsChanged(e);
|
||||
QueueHeaderUpdate();
|
||||
UpdateSelectAllState();
|
||||
}
|
||||
|
||||
private int SelectableItemCount => Items.Count - (Items.Contains(System.Windows.Data.CollectionView.NewItemPlaceholder) ? 1 : 0);
|
||||
|
||||
protected override void OnSelectionChanged(SelectionChangedEventArgs e)
|
||||
{
|
||||
base.OnSelectionChanged(e);
|
||||
UpdateSelectAllState();
|
||||
}
|
||||
|
||||
private void UpdateSelectAllState()
|
||||
{
|
||||
SetValue(AreAllRowsSelectedPropertyKey, SelectedItems.Count == 0 ? false :
|
||||
SelectedItems.Count == SelectableItemCount ? true : (bool?)null);
|
||||
CommandManager.InvalidateRequerySuggested();
|
||||
}
|
||||
|
||||
protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
base.OnPropertyChanged(e);
|
||||
if (e.Property == FontSizeProperty || e.Property == FontFamilyProperty ||
|
||||
e.Property == FontStyleProperty || e.Property == FontWeightProperty || e.Property == FontStretchProperty)
|
||||
QueueHeaderUpdate();
|
||||
}
|
||||
|
||||
private bool headerUpdatePending;
|
||||
private void QueueHeaderUpdate()
|
||||
{
|
||||
if (headerUpdatePending) return;
|
||||
headerUpdatePending = true;
|
||||
Dispatcher.BeginInvoke(DispatcherPriority.Loaded, new Action(() =>
|
||||
{
|
||||
headerUpdatePending = false;
|
||||
// 使用最宽数字估算所有行号,避免虚拟化滚动时行头宽度跳变。
|
||||
var digits = Math.Max(1, Items.Count.ToString(CultureInfo.InvariantCulture).Length);
|
||||
var numberWidth = 0d;
|
||||
if (ShowRowNumber)
|
||||
for (var digit = '0'; digit <= '9'; digit++)
|
||||
{
|
||||
var text = new FormattedText(new string(digit, digits), CultureInfo.CurrentCulture,
|
||||
FlowDirection, new Typeface(FontFamily, FontStyle, FontWeight, FontStretch),
|
||||
FontSize, Foreground ?? Brushes.Black, VisualTreeHelper.GetDpi(this).PixelsPerDip);
|
||||
numberWidth = Math.Max(numberWidth, Math.Ceiling(text.WidthIncludingTrailingWhitespace) + 16);
|
||||
}
|
||||
SetCurrentValue(RowHeaderWidthProperty, numberWidth + (ShowCheckBox ? 32 : 0) +
|
||||
(ShowRowNumber && ShowCheckBox ? 1 : 0) + (ShowRowNumber || ShowCheckBox ? 1 : 0));
|
||||
foreach (var row in GetRealizedRows(this)) UpdateRowNumber(row);
|
||||
}));
|
||||
}
|
||||
|
||||
private void UpdateRowNumber(DataGridRow row)
|
||||
=> row.SetCurrentValue(DataGridRow.HeaderProperty, ShowRowNumber ? (row.GetIndex() + 1).ToString(CultureInfo.CurrentCulture) : null);
|
||||
|
||||
private IEnumerable<DataGridRow> GetRealizedRows(DependencyObject parent)
|
||||
{
|
||||
for (var i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
|
||||
{
|
||||
var child = VisualTreeHelper.GetChild(parent, i);
|
||||
if (child is DataGridRow row && ItemsControl.ItemsControlFromItemContainer(row) == this) yield return row;
|
||||
else if (child is not DataGrid)
|
||||
foreach (var descendant in GetRealizedRows(child)) yield return descendant;
|
||||
base.SelectedItems.Clear();
|
||||
foreach (var item in selected)
|
||||
if (available.Contains(item)) base.SelectedItems.Add(item);
|
||||
}
|
||||
finally { EndUpdateSelectedItems(); }
|
||||
}
|
||||
|
||||
private bool synchronizingSelection;
|
||||
|
||||
private static void OnSelectedItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
var grid = (StyledDataGrid)d;
|
||||
if (e.NewValue is IList { IsReadOnly: true } or IList { IsFixedSize: true })
|
||||
throw new ArgumentException("SelectedItems 必须绑定到可增删的集合,例如 ObservableCollection<T>。");
|
||||
if (e.OldValue is INotifyCollectionChanged oldCollection)
|
||||
CollectionChangedEventManager.RemoveHandler(oldCollection, grid.OnBoundSelectionChanged);
|
||||
if (e.NewValue is INotifyCollectionChanged newCollection)
|
||||
CollectionChangedEventManager.AddHandler(newCollection, grid.OnBoundSelectionChanged);
|
||||
grid.ApplyBoundSelection();
|
||||
}
|
||||
|
||||
private void OnBoundSelectionChanged(object? sender, NotifyCollectionChangedEventArgs e) => ApplyBoundSelection();
|
||||
|
||||
private void ApplyBoundSelection()
|
||||
{
|
||||
if (synchronizingSelection || GetValue(SelectedItemsProperty) is not IList boundItems ||
|
||||
ReferenceEquals(boundItems, base.SelectedItems) || SelectionUnit == DataGridSelectionUnit.Cell) return;
|
||||
var available = Items.Cast<object>().ToHashSet();
|
||||
var desired = boundItems.Cast<object>().Where(item => available.Contains(item) &&
|
||||
item != System.Windows.Data.CollectionView.NewItemPlaceholder).Distinct().ToList();
|
||||
if (SelectionMode == DataGridSelectionMode.Single) desired = desired.Take(1).ToList();
|
||||
synchronizingSelection = true;
|
||||
try
|
||||
{
|
||||
var desiredSet = desired.ToHashSet();
|
||||
var current = base.SelectedItems.Cast<object>().ToHashSet();
|
||||
if (SelectionMode == DataGridSelectionMode.Single)
|
||||
SetCurrentValue(SelectedItemProperty, desired.FirstOrDefault());
|
||||
else if (desired.Count == 0) UnselectAll();
|
||||
else
|
||||
{
|
||||
foreach (var item in current)
|
||||
if (!desiredSet.Contains(item)) base.SelectedItems.Remove(item);
|
||||
foreach (var item in desired)
|
||||
if (!current.Contains(item)) base.SelectedItems.Add(item);
|
||||
}
|
||||
}
|
||||
finally { synchronizingSelection = false; }
|
||||
UpdateSelectAllState();
|
||||
}
|
||||
|
||||
private void PublishSelection()
|
||||
{
|
||||
if (synchronizingSelection || GetValue(SelectedItemsProperty) is not IList boundItems ||
|
||||
ReferenceEquals(boundItems, base.SelectedItems)) return;
|
||||
synchronizingSelection = true;
|
||||
try
|
||||
{
|
||||
var current = base.SelectedItems.Cast<object>().ToArray();
|
||||
if (current.Length == 0) boundItems.Clear();
|
||||
else
|
||||
{
|
||||
var currentSet = current.ToHashSet();
|
||||
for (var i = boundItems.Count - 1; i >= 0; i--)
|
||||
if (!currentSet.Contains(boundItems[i]!)) boundItems.RemoveAt(i);
|
||||
var boundSet = boundItems.Cast<object>().ToHashSet();
|
||||
foreach (var item in current)
|
||||
if (boundSet.Add(item)) boundItems.Add(item);
|
||||
}
|
||||
}
|
||||
finally { synchronizingSelection = false; }
|
||||
}
|
||||
|
||||
protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)
|
||||
{
|
||||
var wasSynchronizing = synchronizingSelection;
|
||||
synchronizingSelection = true;
|
||||
try { base.OnItemsSourceChanged(oldValue, newValue); }
|
||||
finally { synchronizingSelection = wasSynchronizing; }
|
||||
ApplyBoundSelection();
|
||||
}
|
||||
private static readonly DependencyPropertyKey IsSelectionModeActivePropertyKey = DependencyProperty.RegisterReadOnly(
|
||||
nameof(IsSelectionModeActive), typeof(bool), typeof(StyledDataGrid), new PropertyMetadata(false));
|
||||
public static readonly DependencyProperty IsSelectionModeActiveProperty = IsSelectionModeActivePropertyKey.DependencyProperty;
|
||||
/// <summary>点击行头勾选框后进入批量选择,取消所有选中后退出。</summary>
|
||||
public bool IsSelectionModeActive => (bool)GetValue(IsSelectionModeActiveProperty);
|
||||
public static readonly RoutedUICommand ToggleSelectAllCommand = new("全选或取消全选", nameof(ToggleSelectAllCommand), typeof(StyledDataGrid));
|
||||
private static readonly DependencyPropertyKey AreAllRowsSelectedPropertyKey = DependencyProperty.RegisterReadOnly(
|
||||
nameof(AreAllRowsSelected), typeof(bool?), typeof(StyledDataGrid), new PropertyMetadata(false));
|
||||
public static readonly DependencyProperty AreAllRowsSelectedProperty = AreAllRowsSelectedPropertyKey.DependencyProperty;
|
||||
/// <summary>全部选中为 true,部分选中为 null,未选中为 false。</summary>
|
||||
public bool? AreAllRowsSelected => (bool?)GetValue(AreAllRowsSelectedProperty);
|
||||
public static readonly DependencyProperty SelectedRowBackgroundProperty = DependencyProperty.Register(
|
||||
nameof(SelectedRowBackground), typeof(Brush), typeof(StyledDataGrid),
|
||||
new PropertyMetadata(new SolidColorBrush(Color.FromRgb(75, 178, 255))));
|
||||
public static readonly DependencyProperty SelectedRowForegroundProperty = DependencyProperty.Register(
|
||||
nameof(SelectedRowForeground), typeof(Brush), typeof(StyledDataGrid), new PropertyMetadata(Brushes.White));
|
||||
public static readonly DependencyProperty ShowCheckBoxProperty = DependencyProperty.Register(
|
||||
nameof(ShowCheckBox), typeof(bool), typeof(StyledDataGrid), new PropertyMetadata(false, HeaderOptionsChanged));
|
||||
public static readonly DependencyProperty ShowRowNumberProperty = DependencyProperty.Register(
|
||||
nameof(ShowRowNumber), typeof(bool), typeof(StyledDataGrid), new PropertyMetadata(false, HeaderOptionsChanged));
|
||||
|
||||
public Brush SelectedRowBackground { get => (Brush)GetValue(SelectedRowBackgroundProperty); set => SetValue(SelectedRowBackgroundProperty, value); }
|
||||
public Brush SelectedRowForeground { get => (Brush)GetValue(SelectedRowForegroundProperty); set => SetValue(SelectedRowForegroundProperty, value); }
|
||||
/// <summary>勾选状态与行 IsSelected 同步,可通过 SelectedItems 获取。</summary>
|
||||
public bool ShowCheckBox { get => (bool)GetValue(ShowCheckBoxProperty); set => SetValue(ShowCheckBoxProperty, value); }
|
||||
public bool ShowRowNumber { get => (bool)GetValue(ShowRowNumberProperty); set => SetValue(ShowRowNumberProperty, value); }
|
||||
|
||||
static StyledDataGrid()
|
||||
{
|
||||
DefaultStyleKeyProperty.OverrideMetadata(typeof(StyledDataGrid), new FrameworkPropertyMetadata(typeof(StyledDataGrid)));
|
||||
}
|
||||
|
||||
public StyledDataGrid()
|
||||
{
|
||||
InitializeImageLoadingPause();
|
||||
CommandBindings.Add(new CommandBinding(ToggleSelectAllCommand, (_, e) =>
|
||||
{
|
||||
if (AreAllRowsSelected == true) UnselectAll();
|
||||
else
|
||||
{
|
||||
SelectAll();
|
||||
SetValue(IsSelectionModeActivePropertyKey, base.SelectedItems.Count > 0);
|
||||
}
|
||||
e.Handled = true;
|
||||
}, (_, e) =>
|
||||
{
|
||||
e.CanExecute = SelectionMode == DataGridSelectionMode.Extended &&
|
||||
SelectionUnit != DataGridSelectionUnit.Cell && SelectableItemCount > 0;
|
||||
e.Handled = true;
|
||||
}));
|
||||
AddHandler(ButtonBase.ClickEvent, new RoutedEventHandler(OnRowCheckBoxClick), true);
|
||||
LoadingRow += (_, e) => UpdateRowNumber(e.Row);
|
||||
ItemContainerGenerator.ItemsChanged += (_, _) => QueueHeaderUpdate();
|
||||
Loaded += (_, _) => { ApplyBoundSelection(); QueueHeaderUpdate(); };
|
||||
}
|
||||
|
||||
private void OnRowCheckBoxClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (e.OriginalSource is not CheckBox checkBox) return;
|
||||
DependencyObject? parent = checkBox;
|
||||
while (parent != null && parent != this)
|
||||
{
|
||||
if (parent is DataGridRowHeader header &&
|
||||
ItemsControl.ItemsControlFromItemContainer(FindRow(header)) == this)
|
||||
{
|
||||
SetValue(IsSelectionModeActivePropertyKey, base.SelectedItems.Count > 0);
|
||||
return;
|
||||
}
|
||||
parent = VisualTreeHelper.GetParent(parent);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnPreviewMouseLeftButtonDown(MouseButtonEventArgs e)
|
||||
{
|
||||
if (!e.Handled && ShowCheckBox && ToggleSelectionOnRowHeaderClick && SelectionUnit != DataGridSelectionUnit.Cell)
|
||||
{
|
||||
for (var source = e.OriginalSource as DependencyObject; source != null && source != this;
|
||||
source = source is FrameworkContentElement content ? content.Parent : VisualTreeHelper.GetParent(source))
|
||||
{
|
||||
// 保留行高拖动和嵌套表格自己的鼠标处理。
|
||||
if (source is Thumb || source is DataGrid) break;
|
||||
if (source is not DataGridRowHeader header) continue;
|
||||
var row = FindRow(header);
|
||||
if (row == null || ItemsControl.ItemsControlFromItemContainer(row) != this ||
|
||||
row.Item == System.Windows.Data.CollectionView.NewItemPlaceholder) break;
|
||||
|
||||
// 在 DataGridRowHeader 默认单选逻辑之前处理整个行头,
|
||||
// 包括勾选框四周的留白;阻止 CheckBox 再次切换造成双重反转。
|
||||
e.Handled = true;
|
||||
row.SetCurrentValue(DataGridRow.IsSelectedProperty, !row.IsSelected);
|
||||
SetValue(IsSelectionModeActivePropertyKey, base.SelectedItems.Count > 0);
|
||||
return;
|
||||
}
|
||||
}
|
||||
base.OnPreviewMouseLeftButtonDown(e);
|
||||
}
|
||||
|
||||
private static DataGridRow? FindRow(DependencyObject element)
|
||||
{
|
||||
for (DependencyObject? parent = element; parent != null; parent = parent is FrameworkContentElement content ? content.Parent : VisualTreeHelper.GetParent(parent))
|
||||
if (parent is DataGridRow row) return row;
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void HeaderOptionsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
=> ((StyledDataGrid)d).QueueHeaderUpdate();
|
||||
|
||||
protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
base.OnItemsChanged(e);
|
||||
QueueHeaderUpdate();
|
||||
UpdateSelectAllState();
|
||||
}
|
||||
|
||||
private int SelectableItemCount => Items.Count - (Items.Contains(System.Windows.Data.CollectionView.NewItemPlaceholder) ? 1 : 0);
|
||||
|
||||
protected override void OnSelectionChanged(SelectionChangedEventArgs e)
|
||||
{
|
||||
PublishSelection();
|
||||
base.OnSelectionChanged(e);
|
||||
UpdateSelectAllState();
|
||||
}
|
||||
|
||||
private void UpdateSelectAllState()
|
||||
{
|
||||
SetValue(AreAllRowsSelectedPropertyKey, base.SelectedItems.Count == 0 ? false :
|
||||
base.SelectedItems.Count == SelectableItemCount ? true : (bool?)null);
|
||||
if (base.SelectedItems.Count == 0) SetValue(IsSelectionModeActivePropertyKey, false);
|
||||
CommandManager.InvalidateRequerySuggested();
|
||||
}
|
||||
|
||||
protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
base.OnPropertyChanged(e);
|
||||
if (e.Property == FontSizeProperty || e.Property == FontFamilyProperty ||
|
||||
e.Property == FontStyleProperty || e.Property == FontWeightProperty || e.Property == FontStretchProperty)
|
||||
QueueHeaderUpdate();
|
||||
}
|
||||
|
||||
private bool headerUpdatePending;
|
||||
private void QueueHeaderUpdate()
|
||||
{
|
||||
if (headerUpdatePending) return;
|
||||
headerUpdatePending = true;
|
||||
Dispatcher.BeginInvoke(DispatcherPriority.Loaded, new Action(() =>
|
||||
{
|
||||
headerUpdatePending = false;
|
||||
foreach (var row in GetRealizedRows(this)) UpdateRowNumber(row);
|
||||
}));
|
||||
}
|
||||
|
||||
private void UpdateRowNumber(DataGridRow row)
|
||||
=> row.SetCurrentValue(DataGridRow.HeaderProperty, ShowRowNumber ? (row.GetIndex() + 1).ToString(CultureInfo.CurrentCulture) : null);
|
||||
|
||||
private IEnumerable<DataGridRow> GetRealizedRows(DependencyObject parent)
|
||||
{
|
||||
for (var i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
|
||||
{
|
||||
var child = VisualTreeHelper.GetChild(parent, i);
|
||||
if (child is DataGridRow row && ItemsControl.ItemsControlFromItemContainer(row) == this) yield return row;
|
||||
else if (child is not DataGrid)
|
||||
foreach (var descendant in GetRealizedRows(child)) yield return descendant;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
<ResourceDictionary
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:ext="clr-namespace:Main.Test"
|
||||
xmlns:sys="clr-namespace:System;assembly=mscorlib">
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:Main.Test"
|
||||
xmlns:ext="clr-namespace:Main.Test"
|
||||
xmlns:sys="clr-namespace:System;assembly=mscorlib">
|
||||
|
||||
|
||||
<Style x:Key="DefaultCellHeaderStyle" TargetType="DataGridColumnHeader">
|
||||
<Setter Property="UseLayoutRounding" Value="True" />
|
||||
@@ -28,7 +30,12 @@
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
UseLayoutRounding="True">
|
||||
<Grid HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<ContentPresenter
|
||||
Grid.Column="0"
|
||||
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||
RecognizesAccessKey="True"
|
||||
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
|
||||
@@ -40,7 +47,7 @@
|
||||
Margin="10,0,5,0"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
Visibility="Hidden">
|
||||
Visibility="Collapsed">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition />
|
||||
<RowDefinition />
|
||||
@@ -120,7 +127,7 @@
|
||||
<Setter TargetName="SortArrowDown" Property="Fill" Value="#61646D" />
|
||||
</Trigger>
|
||||
<Trigger Property="SortDirection" Value="{x:Null}">
|
||||
<Setter TargetName="SortArrowGrid" Property="Visibility" Value="Hidden" />
|
||||
<Setter TargetName="SortArrowGrid" Property="Visibility" Value="Collapsed" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
@@ -349,7 +356,6 @@
|
||||
<Setter Property="FocusVisualStyle" Value="{x:Null}" />
|
||||
<Setter Property="ColumnHeaderHeight" Value="36" />
|
||||
<Setter Property="MinRowHeight" Value="40" />
|
||||
<Setter Property="RowHeaderWidth" Value="0" />
|
||||
<Setter Property="CanUserAddRows" Value="False" />
|
||||
<Setter Property="AutoGenerateColumns" Value="False" />
|
||||
<Setter Property="CanUserSortColumns" Value="True" />
|
||||
@@ -405,7 +411,7 @@
|
||||
BorderThickness="1,0,1,1">
|
||||
<CheckBox
|
||||
Width="32"
|
||||
HorizontalAlignment="Center"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalContentAlignment="Center"
|
||||
AutomationProperties.Name="全选行"
|
||||
@@ -416,9 +422,13 @@
|
||||
<Style BasedOn="{StaticResource {x:Type CheckBox}}" TargetType="CheckBox">
|
||||
<Setter Property="Visibility" Value="Collapsed" />
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding ShowCheckBox, RelativeSource={RelativeSource AncestorType={x:Type ext:StyledDataGrid}}}" Value="True">
|
||||
<Setter Property="Visibility" Value="Visible" />
|
||||
</DataTrigger>
|
||||
<MultiDataTrigger>
|
||||
<MultiDataTrigger.Conditions>
|
||||
<Condition Binding="{Binding ShowCheckBox, RelativeSource={RelativeSource AncestorType={x:Type ext:StyledDataGrid}}}" Value="True" />
|
||||
<Condition Binding="{Binding ShowSelectAll, RelativeSource={RelativeSource AncestorType={x:Type ext:StyledDataGrid}}}" Value="True" />
|
||||
</MultiDataTrigger.Conditions>
|
||||
<Setter Property="Visibility" Value="Visible" />
|
||||
</MultiDataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</CheckBox.Style>
|
||||
@@ -521,4 +531,275 @@
|
||||
</MultiTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
|
||||
<FontFamily x:Key="FontFamily">fonts:GeistMono#Geist Mono, SF Mono, Menlo, Cascadia Mono, Consolas, DejaVu Sans Mono, Courier New, $Default</FontFamily>
|
||||
|
||||
<Style x:Key="StyledDefaultCellStyle" TargetType="DataGridCell">
|
||||
<Setter Property="BorderThickness" Value="0" />
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="Padding" Value="8,0" />
|
||||
<Setter Property="FontSize" Value="{DynamicResource DefaultFontSize}" />
|
||||
<Setter Property="FontFamily" Value="{DynamicResource FontFamily}" />
|
||||
<!--<Setter Property="Foreground" Value="{Binding RelativeSource={RelativeSource AncestorType=DataGrid, Mode=FindAncestor}, Path=Foreground, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />-->
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="{x:Type DataGridCell}">
|
||||
<Grid>
|
||||
<Border
|
||||
Padding="{TemplateBinding Padding}"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}">
|
||||
<ContentPresenter
|
||||
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
|
||||
</Border>
|
||||
</Grid>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="Foreground" Value="{Binding SelectedRowForeground, RelativeSource={RelativeSource AncestorType={x:Type local:StyledDataGrid}}}" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
<Style x:Key="StyledDefaultRowStyle" TargetType="{x:Type DataGridRow}">
|
||||
<Setter Property="ContextMenu" Value="{Binding RowContextMenu, RelativeSource={RelativeSource AncestorType=local:StyledDataGrid}}" />
|
||||
<Setter Property="Tag" Value="{Binding DataContext, RelativeSource={RelativeSource AncestorType=local:StyledDataGrid}}" />
|
||||
<Setter Property="BorderThickness" Value="0,0,1,0" />
|
||||
<Setter Property="Background" Value="{x:Null}" />
|
||||
<Setter Property="FontSize" Value="{DynamicResource DefaultFontSize}" />
|
||||
<Setter Property="FontFamily" Value="{DynamicResource FontFamily}" />
|
||||
<Setter Property="BorderBrush" Value="{Binding RelativeSource={RelativeSource AncestorType=DataGrid, Mode=FindAncestor}, Path=BorderBrush}" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="DataGridRow">
|
||||
<Grid>
|
||||
<Border
|
||||
x:Name="border"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}" />
|
||||
<SelectiveScrollingGrid>
|
||||
<SelectiveScrollingGrid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</SelectiveScrollingGrid.ColumnDefinitions>
|
||||
<SelectiveScrollingGrid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</SelectiveScrollingGrid.RowDefinitions>
|
||||
<DataGridCellsPresenter
|
||||
Grid.Column="1"
|
||||
ItemsPanel="{TemplateBinding ItemsPanel}"
|
||||
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
|
||||
<DataGridDetailsPresenter
|
||||
x:Name="DetailsPresenter"
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
SelectiveScrollingGrid.SelectiveScrollingOrientation="{Binding AreRowDetailsFrozen, ConverterParameter={x:Static SelectiveScrollingOrientation.Vertical}, Converter={x:Static DataGrid.RowDetailsScrollingConverter}, RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}"
|
||||
Visibility="{TemplateBinding DetailsVisibility}" />
|
||||
<DataGridRowHeader
|
||||
Grid.RowSpan="2"
|
||||
SelectiveScrollingGrid.SelectiveScrollingOrientation="Vertical"
|
||||
Visibility="{Binding HeadersVisibility, ConverterParameter={x:Static DataGridHeadersVisibility.Row}, Converter={x:Static DataGrid.HeadersVisibilityConverter}, RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}" />
|
||||
</SelectiveScrollingGrid>
|
||||
<Grid
|
||||
x:Name="fousIcon"
|
||||
IsHitTestVisible="False"
|
||||
Opacity="0"
|
||||
Visibility="Collapsed">
|
||||
<Path
|
||||
Width="10"
|
||||
Height="10"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Top"
|
||||
Data="M0,0 L0,0 50,0 50,10 10,10 10,50 0,50 0,52 z"
|
||||
Fill="#4BB2FF"
|
||||
RenderTransformOrigin="0.5,0.5"
|
||||
Stretch="Fill">
|
||||
<Path.RenderTransform>
|
||||
<TransformGroup>
|
||||
<ScaleTransform />
|
||||
<SkewTransform />
|
||||
<RotateTransform Angle="0" />
|
||||
<TranslateTransform />
|
||||
</TransformGroup>
|
||||
</Path.RenderTransform>
|
||||
</Path>
|
||||
<Path
|
||||
Width="10"
|
||||
Height="10"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Top"
|
||||
Data="M0,0 L0,0 50,0 50,10 10,10 10,50 0,50 0,52 z"
|
||||
Fill="#4BB2FF"
|
||||
RenderTransformOrigin="0.5,0.5"
|
||||
Stretch="Fill">
|
||||
<Path.RenderTransform>
|
||||
<TransformGroup>
|
||||
<ScaleTransform />
|
||||
<SkewTransform />
|
||||
<RotateTransform Angle="90" />
|
||||
<TranslateTransform />
|
||||
</TransformGroup>
|
||||
</Path.RenderTransform>
|
||||
</Path>
|
||||
<Path
|
||||
Width="10"
|
||||
Height="10"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Bottom"
|
||||
Data="M0,0 L0,0 50,0 50,10 10,10 10,50 0,50 0,52 z"
|
||||
Fill="#4BB2FF"
|
||||
RenderTransformOrigin="0.5,0.5"
|
||||
Stretch="Fill">
|
||||
<Path.RenderTransform>
|
||||
<TransformGroup>
|
||||
<ScaleTransform />
|
||||
<SkewTransform />
|
||||
<RotateTransform Angle="180" />
|
||||
<TranslateTransform />
|
||||
</TransformGroup>
|
||||
</Path.RenderTransform>
|
||||
</Path>
|
||||
<Path
|
||||
Width="10"
|
||||
Height="10"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Bottom"
|
||||
Data="M0,0 L0,0 50,0 50,10 10,10 10,50 0,50 0,52 z"
|
||||
Fill="#4BB2FF"
|
||||
RenderTransformOrigin="0.5,0.5"
|
||||
Stretch="Fill">
|
||||
<Path.RenderTransform>
|
||||
<TransformGroup>
|
||||
<ScaleTransform />
|
||||
<SkewTransform />
|
||||
<RotateTransform Angle="-90" />
|
||||
<TranslateTransform />
|
||||
</TransformGroup>
|
||||
</Path.RenderTransform>
|
||||
</Path>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter TargetName="fousIcon" Property="Opacity" Value="1" />
|
||||
<Setter Property="Foreground" Value="{Binding SelectedRowForeground, RelativeSource={RelativeSource AncestorType={x:Type local:StyledDataGrid}}}" />
|
||||
<Setter TargetName="border" Property="Background" Value="{Binding SelectedRowBackground, RelativeSource={RelativeSource AncestorType={x:Type local:StyledDataGrid}}}" />
|
||||
</Trigger>
|
||||
<Trigger Property="DetailsVisibility" Value="Visible">
|
||||
<Setter TargetName="DetailsPresenter" Property="Margin" Value="0,0,1,0" />
|
||||
</Trigger>
|
||||
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
<Style
|
||||
x:Key="StyledRowHeaderStyle"
|
||||
BasedOn="{StaticResource DefaultRowHeaderStyle}"
|
||||
TargetType="DataGridRowHeader">
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="DataGridRowHeader">
|
||||
<Border
|
||||
Background="Transparent"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="0,0,1,1">
|
||||
<Grid>
|
||||
<ContentPresenter
|
||||
x:Name="Number"
|
||||
Margin="8,0"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Content="{Binding Header, RelativeSource={RelativeSource AncestorType=DataGridRow}}"
|
||||
Visibility="Collapsed" />
|
||||
<CheckBox
|
||||
x:Name="RowCheckBox"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
AutomationProperties.Name="选择行"
|
||||
IsChecked="{Binding IsSelected, RelativeSource={RelativeSource AncestorType=DataGridRow}, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
Visibility="Collapsed" />
|
||||
</Grid>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<DataTrigger Binding="{Binding ShowRowNumber, RelativeSource={RelativeSource AncestorType=local:StyledDataGrid}}" Value="True">
|
||||
<Setter TargetName="Number" Property="Visibility" Value="Visible" />
|
||||
</DataTrigger>
|
||||
<MultiDataTrigger>
|
||||
<MultiDataTrigger.Conditions>
|
||||
<Condition Binding="{Binding ShowCheckBox, RelativeSource={RelativeSource AncestorType=local:StyledDataGrid}}" Value="True" />
|
||||
<Condition Binding="{Binding ShowRowNumber, RelativeSource={RelativeSource AncestorType=local:StyledDataGrid}}" Value="False" />
|
||||
</MultiDataTrigger.Conditions>
|
||||
<Setter TargetName="Number" Property="Visibility" Value="Collapsed" />
|
||||
<Setter TargetName="RowCheckBox" Property="Visibility" Value="Visible" />
|
||||
</MultiDataTrigger>
|
||||
<MultiDataTrigger>
|
||||
<MultiDataTrigger.Conditions>
|
||||
<Condition Binding="{Binding ShowCheckBox, RelativeSource={RelativeSource AncestorType=local:StyledDataGrid}}" Value="True" />
|
||||
<Condition Binding="{Binding IsSelectionModeActive, RelativeSource={RelativeSource AncestorType=local:StyledDataGrid}}" Value="True" />
|
||||
</MultiDataTrigger.Conditions>
|
||||
<Setter TargetName="Number" Property="Visibility" Value="Collapsed" />
|
||||
<Setter TargetName="RowCheckBox" Property="Visibility" Value="Visible" />
|
||||
</MultiDataTrigger>
|
||||
<MultiDataTrigger>
|
||||
<MultiDataTrigger.Conditions>
|
||||
<Condition Binding="{Binding ShowCheckBox, RelativeSource={RelativeSource AncestorType=local:StyledDataGrid}}" Value="True" />
|
||||
<Condition Binding="{Binding AreAllRowsSelected, RelativeSource={RelativeSource AncestorType=local:StyledDataGrid}}" Value="True" />
|
||||
</MultiDataTrigger.Conditions>
|
||||
<Setter TargetName="Number" Property="Visibility" Value="Collapsed" />
|
||||
<Setter TargetName="RowCheckBox" Property="Visibility" Value="Visible" />
|
||||
</MultiDataTrigger>
|
||||
<MultiDataTrigger>
|
||||
<MultiDataTrigger.Conditions>
|
||||
<Condition Binding="{Binding ShowCheckBox, RelativeSource={RelativeSource AncestorType=local:StyledDataGrid}}" Value="True" />
|
||||
<Condition Binding="{Binding IsSelected, RelativeSource={RelativeSource AncestorType=DataGridRow}}" Value="True" />
|
||||
</MultiDataTrigger.Conditions>
|
||||
<Setter TargetName="Number" Property="Visibility" Value="Collapsed" />
|
||||
<Setter TargetName="RowCheckBox" Property="Visibility" Value="Visible" />
|
||||
</MultiDataTrigger>
|
||||
<MultiDataTrigger>
|
||||
<MultiDataTrigger.Conditions>
|
||||
<Condition Binding="{Binding ShowCheckBox, RelativeSource={RelativeSource AncestorType=local:StyledDataGrid}}" Value="True" />
|
||||
<Condition Binding="{Binding IsMouseOver, RelativeSource={RelativeSource AncestorType=DataGridRow}}" Value="True" />
|
||||
</MultiDataTrigger.Conditions>
|
||||
<Setter TargetName="Number" Property="Visibility" Value="Collapsed" />
|
||||
<Setter TargetName="RowCheckBox" Property="Visibility" Value="Visible" />
|
||||
</MultiDataTrigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style
|
||||
x:Key="StyledNumberCellStyle"
|
||||
BasedOn="{StaticResource StyledDefaultCellStyle}"
|
||||
TargetType="DataGridCell">
|
||||
<Setter Property="HorizontalContentAlignment" Value="Right" />
|
||||
</Style>
|
||||
|
||||
<Style BasedOn="{StaticResource DefaultDataGridStyle}" TargetType="local:StyledDataGrid">
|
||||
<Setter Property="EnableRowVirtualization" Value="True" />
|
||||
<Setter Property="EnableColumnVirtualization" Value="True" />
|
||||
<Setter Property="VirtualizingPanel.IsVirtualizing" Value="True" />
|
||||
<Setter Property="VirtualizingPanel.VirtualizationMode" Value="Recycling" />
|
||||
<Setter Property="ScrollViewer.CanContentScroll" Value="True" />
|
||||
|
||||
<Setter Property="SelectionMode" Value="Extended" />
|
||||
<Setter Property="SelectionUnit" Value="FullRow" />
|
||||
<Setter Property="RowStyle" Value="{StaticResource StyledDefaultRowStyle}" />
|
||||
<Setter Property="CellStyle" Value="{StaticResource StyledDefaultCellStyle}" />
|
||||
<Setter Property="RowHeaderStyle" Value="{StaticResource StyledRowHeaderStyle}" />
|
||||
</Style>
|
||||
|
||||
</ResourceDictionary>
|
||||
@@ -0,0 +1,169 @@
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Media;
|
||||
using System.Windows;
|
||||
|
||||
namespace Main.Test;
|
||||
|
||||
/// <summary>配套图片列:自动使用所属 StyledDataGrid 的拖动暂停和图片缓存能力。</summary>
|
||||
public class StyledDataGridImageColumn : DataGridBoundColumn
|
||||
{
|
||||
private static readonly Style DefaultImageStyle = CreateDefaultStyle();
|
||||
/// <summary>可选的逐行占位图;未设置时显示空白。</summary>
|
||||
public BindingBase? PlaceholderBinding { get; set; }
|
||||
|
||||
protected override FrameworkElement GenerateElement(DataGridCell cell, object dataItem)
|
||||
{
|
||||
var image = new NetworkThumbnail { Style = ElementStyle ?? DefaultImageStyle };
|
||||
if (Binding != null) BindingOperations.SetBinding(image, NetworkThumbnail.UrlProperty, Binding);
|
||||
if (PlaceholderBinding != null)
|
||||
BindingOperations.SetBinding(image, NetworkThumbnail.PlaceholderProperty, PlaceholderBinding);
|
||||
return image;
|
||||
}
|
||||
protected override FrameworkElement GenerateEditingElement(DataGridCell cell, object dataItem) => GenerateElement(cell, dataItem);
|
||||
private static Style CreateDefaultStyle()
|
||||
{
|
||||
var style = new Style(typeof(NetworkThumbnail));
|
||||
style.Setters.Add(new Setter(FrameworkElement.WidthProperty, 58d));
|
||||
style.Setters.Add(new Setter(FrameworkElement.HeightProperty, 58d));
|
||||
style.Setters.Add(new Setter(System.Windows.Controls.Image.StretchProperty, Stretch.Uniform));
|
||||
style.Seal(); return style;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>仅为已生成的可见单元格加载图片;回收时取消,避免旧图片覆盖新行。</summary>
|
||||
public sealed class NetworkThumbnail : Image
|
||||
{
|
||||
public static readonly DependencyProperty CacheEnabledProperty = DependencyProperty.RegisterAttached("CacheEnabled", typeof(bool), typeof(NetworkThumbnail), new FrameworkPropertyMetadata(true, FrameworkPropertyMetadataOptions.Inherits, Changed));
|
||||
public static bool GetCacheEnabled(DependencyObject target) => (bool)target.GetValue(CacheEnabledProperty);
|
||||
public static void SetCacheEnabled(DependencyObject target, bool value) => target.SetValue(CacheEnabledProperty, value);
|
||||
|
||||
public static readonly DependencyProperty IsLoadingSuspendedProperty = DependencyProperty.RegisterAttached(
|
||||
"IsLoadingSuspended", typeof(bool), typeof(NetworkThumbnail),
|
||||
new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.Inherits, SuspensionChanged));
|
||||
public static bool GetIsLoadingSuspended(DependencyObject target) => (bool)target.GetValue(IsLoadingSuspendedProperty);
|
||||
public static void SetIsLoadingSuspended(DependencyObject target, bool value) => target.SetValue(IsLoadingSuspendedProperty, value);
|
||||
|
||||
private static void SuspensionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is not NetworkThumbnail image) return;
|
||||
image.CancelLoading();
|
||||
image.Source = null;
|
||||
if (!(bool)e.NewValue) image.QueueRefresh();
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty UrlProperty = DependencyProperty.Register(nameof(Url), typeof(string), typeof(NetworkThumbnail), new PropertyMetadata(null, Changed));
|
||||
public static readonly DependencyProperty PlaceholderProperty = DependencyProperty.Register(nameof(Placeholder), typeof(ImageSource), typeof(NetworkThumbnail), new PropertyMetadata(null, Changed));
|
||||
public string? Url { get => (string?)GetValue(UrlProperty); set => SetValue(UrlProperty, value); }
|
||||
public ImageSource? Placeholder { get => (ImageSource?)GetValue(PlaceholderProperty); set => SetValue(PlaceholderProperty, value); }
|
||||
private CancellationTokenSource? loading;
|
||||
private bool refreshPending;
|
||||
|
||||
public NetworkThumbnail()
|
||||
{
|
||||
Loaded += (_, _) => QueueRefresh();
|
||||
Unloaded += (_, _) => CancelLoading();
|
||||
}
|
||||
private static void Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is not NetworkThumbnail image) return;
|
||||
image.CancelLoading();
|
||||
image.Source = GetIsLoadingSuspended(image) ? null : image.Placeholder;
|
||||
// 回收行时多个绑定会依次变更,等本轮绑定完成后只加载一次新 URL。
|
||||
image.QueueRefresh();
|
||||
}
|
||||
private void QueueRefresh()
|
||||
{
|
||||
if (!IsLoaded || refreshPending) return;
|
||||
refreshPending = true;
|
||||
Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Loaded, new Action(() =>
|
||||
{
|
||||
refreshPending = false;
|
||||
if (IsLoaded) StartLoading();
|
||||
}));
|
||||
}
|
||||
private void CancelLoading()
|
||||
{
|
||||
loading?.Cancel(); loading?.Dispose(); loading = null;
|
||||
}
|
||||
private async void StartLoading()
|
||||
{
|
||||
CancelLoading(); ToolTip = null;
|
||||
if (GetIsLoadingSuspended(this)) { Source = null; return; }
|
||||
Source = Placeholder;
|
||||
if (string.IsNullOrWhiteSpace(Url)) return;
|
||||
var request = new CancellationTokenSource(); loading = request;
|
||||
var token = request.Token;
|
||||
try
|
||||
{
|
||||
var bitmap = await ThumbnailLoader.LoadAsync(Url, GetCacheEnabled(this), token);
|
||||
if (!token.IsCancellationRequested && ReferenceEquals(loading, request)) Source = bitmap;
|
||||
}
|
||||
catch (OperationCanceledException) when (token.IsCancellationRequested) { }
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (!token.IsCancellationRequested && ReferenceEquals(loading, request)) ToolTip = $"图片加载失败:{ex.Message}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static class ThumbnailLoader
|
||||
{
|
||||
private static readonly HttpClient Client = new() { Timeout = TimeSpan.FromSeconds(12) };
|
||||
private static readonly SemaphoreSlim Slots = new(4);
|
||||
private static readonly object CacheLock = new();
|
||||
private static readonly Dictionary<string, BitmapSource> Cache = new();
|
||||
private static readonly Queue<string> CacheOrder = new();
|
||||
private static long started, succeeded, failed, canceled, cacheHits, bytes, totalTicks;
|
||||
private static int active, pending;
|
||||
public static int Pending => Volatile.Read(ref pending);
|
||||
public static string Statistics => $"网络图片:请求 {Interlocked.Read(ref started)} / 成功 {Interlocked.Read(ref succeeded)} / 失败 {Interlocked.Read(ref failed)} / 取消 {Interlocked.Read(ref canceled)} / 缓存命中 {Interlocked.Read(ref cacheHits)} | 活跃 {Volatile.Read(ref active)},待完成 {Pending} | 下载 {Interlocked.Read(ref bytes) / 1024d:N0} KB | 成功平均 {AverageMilliseconds:N0} ms";
|
||||
private static double AverageMilliseconds => Interlocked.Read(ref succeeded) == 0 ? 0 : Interlocked.Read(ref totalTicks) * 1000d / Stopwatch.Frequency / Interlocked.Read(ref succeeded);
|
||||
|
||||
public static async Task<BitmapSource> LoadAsync(string url, bool cacheEnabled, CancellationToken token)
|
||||
{
|
||||
var cacheKey = url;
|
||||
lock (CacheLock)
|
||||
if (cacheEnabled && Cache.TryGetValue(cacheKey, out var cached)) { Interlocked.Increment(ref cacheHits); return cached; }
|
||||
Interlocked.Increment(ref pending);
|
||||
var acquired = false;
|
||||
try
|
||||
{
|
||||
await Slots.WaitAsync(token).ConfigureAwait(false); acquired = true;
|
||||
Interlocked.Increment(ref active); Interlocked.Increment(ref started);
|
||||
var watch = Stopwatch.StartNew();
|
||||
var data = await Client.GetByteArrayAsync(url, token).ConfigureAwait(false);
|
||||
Interlocked.Add(ref bytes, data.Length);
|
||||
var bitmap = await Task.Run(() =>
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
using var stream = new MemoryStream(data);
|
||||
var image = new BitmapImage(); image.BeginInit(); image.CacheOption = BitmapCacheOption.OnLoad;
|
||||
image.DecodePixelWidth = 72; image.StreamSource = stream; image.EndInit(); image.Freeze();
|
||||
return image;
|
||||
}, token).ConfigureAwait(false);
|
||||
token.ThrowIfCancellationRequested();
|
||||
lock (CacheLock)
|
||||
{
|
||||
if (cacheEnabled && !Cache.ContainsKey(cacheKey))
|
||||
{
|
||||
if (Cache.Count >= 128) Cache.Remove(CacheOrder.Dequeue());
|
||||
Cache.Add(cacheKey, bitmap); CacheOrder.Enqueue(cacheKey);
|
||||
}
|
||||
}
|
||||
Interlocked.Increment(ref succeeded); Interlocked.Add(ref totalTicks, watch.ElapsedTicks);
|
||||
return bitmap;
|
||||
}
|
||||
catch (OperationCanceledException) when (token.IsCancellationRequested) { Interlocked.Increment(ref canceled); throw; }
|
||||
catch { Interlocked.Increment(ref failed); throw; }
|
||||
finally
|
||||
{
|
||||
if (acquired) { Interlocked.Decrement(ref active); Slots.Release(); }
|
||||
Interlocked.Decrement(ref pending);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,249 +0,0 @@
|
||||
<ResourceDictionary
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:Main.Test">
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="/Main.Test;component/Dictionary1.xaml" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
<Style x:Key="StyledDefaultCellStyle" TargetType="DataGridCell">
|
||||
<Setter Property="BorderThickness" Value="0" />
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="Padding" Value="8,0" />
|
||||
<Setter Property="FontSize" Value="{DynamicResource DefaultFontSize}" />
|
||||
<Setter Property="FontFamily" Value="{DynamicResource FontFamily}" />
|
||||
<!--<Setter Property="Foreground" Value="{Binding RelativeSource={RelativeSource AncestorType=DataGrid, Mode=FindAncestor}, Path=Foreground, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />-->
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="{x:Type DataGridCell}">
|
||||
<Grid>
|
||||
<Border
|
||||
Padding="{TemplateBinding Padding}"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}">
|
||||
<ContentPresenter
|
||||
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
|
||||
</Border>
|
||||
</Grid>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="Foreground" Value="{Binding SelectedRowForeground, RelativeSource={RelativeSource AncestorType={x:Type local:StyledDataGrid}}}" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
<Style x:Key="StyledDefaultRowStyle" TargetType="{x:Type DataGridRow}">
|
||||
<Setter Property="BorderThickness" Value="0,0,1,0" />
|
||||
<Setter Property="Background" Value="{x:Null}" />
|
||||
<Setter Property="FontSize" Value="{DynamicResource DefaultFontSize}" />
|
||||
<Setter Property="FontFamily" Value="{DynamicResource FontFamily}" />
|
||||
<Setter Property="BorderBrush" Value="{Binding RelativeSource={RelativeSource AncestorType=DataGrid, Mode=FindAncestor}, Path=BorderBrush}" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="DataGridRow">
|
||||
<Grid>
|
||||
<Border
|
||||
x:Name="border"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}" />
|
||||
<SelectiveScrollingGrid>
|
||||
<SelectiveScrollingGrid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</SelectiveScrollingGrid.ColumnDefinitions>
|
||||
<SelectiveScrollingGrid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</SelectiveScrollingGrid.RowDefinitions>
|
||||
<DataGridCellsPresenter
|
||||
Grid.Column="1"
|
||||
ItemsPanel="{TemplateBinding ItemsPanel}"
|
||||
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
|
||||
<DataGridDetailsPresenter
|
||||
x:Name="DetailsPresenter"
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
SelectiveScrollingGrid.SelectiveScrollingOrientation="{Binding AreRowDetailsFrozen, ConverterParameter={x:Static SelectiveScrollingOrientation.Vertical}, Converter={x:Static DataGrid.RowDetailsScrollingConverter}, RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}"
|
||||
Visibility="{TemplateBinding DetailsVisibility}" />
|
||||
<DataGridRowHeader
|
||||
Grid.RowSpan="2"
|
||||
SelectiveScrollingGrid.SelectiveScrollingOrientation="Vertical"
|
||||
Visibility="{Binding HeadersVisibility, ConverterParameter={x:Static DataGridHeadersVisibility.Row}, Converter={x:Static DataGrid.HeadersVisibilityConverter}, RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}" />
|
||||
</SelectiveScrollingGrid>
|
||||
<Grid
|
||||
x:Name="fousIcon"
|
||||
IsHitTestVisible="False"
|
||||
Opacity="0"
|
||||
Visibility="Collapsed">
|
||||
<Path
|
||||
Width="10"
|
||||
Height="10"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Top"
|
||||
Data="M0,0 L0,0 50,0 50,10 10,10 10,50 0,50 0,52 z"
|
||||
Fill="#4BB2FF"
|
||||
RenderTransformOrigin="0.5,0.5"
|
||||
Stretch="Fill">
|
||||
<Path.RenderTransform>
|
||||
<TransformGroup>
|
||||
<ScaleTransform />
|
||||
<SkewTransform />
|
||||
<RotateTransform Angle="0" />
|
||||
<TranslateTransform />
|
||||
</TransformGroup>
|
||||
</Path.RenderTransform>
|
||||
</Path>
|
||||
<Path
|
||||
Width="10"
|
||||
Height="10"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Top"
|
||||
Data="M0,0 L0,0 50,0 50,10 10,10 10,50 0,50 0,52 z"
|
||||
Fill="#4BB2FF"
|
||||
RenderTransformOrigin="0.5,0.5"
|
||||
Stretch="Fill">
|
||||
<Path.RenderTransform>
|
||||
<TransformGroup>
|
||||
<ScaleTransform />
|
||||
<SkewTransform />
|
||||
<RotateTransform Angle="90" />
|
||||
<TranslateTransform />
|
||||
</TransformGroup>
|
||||
</Path.RenderTransform>
|
||||
</Path>
|
||||
<Path
|
||||
Width="10"
|
||||
Height="10"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Bottom"
|
||||
Data="M0,0 L0,0 50,0 50,10 10,10 10,50 0,50 0,52 z"
|
||||
Fill="#4BB2FF"
|
||||
RenderTransformOrigin="0.5,0.5"
|
||||
Stretch="Fill">
|
||||
<Path.RenderTransform>
|
||||
<TransformGroup>
|
||||
<ScaleTransform />
|
||||
<SkewTransform />
|
||||
<RotateTransform Angle="180" />
|
||||
<TranslateTransform />
|
||||
</TransformGroup>
|
||||
</Path.RenderTransform>
|
||||
</Path>
|
||||
<Path
|
||||
Width="10"
|
||||
Height="10"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Bottom"
|
||||
Data="M0,0 L0,0 50,0 50,10 10,10 10,50 0,50 0,52 z"
|
||||
Fill="#4BB2FF"
|
||||
RenderTransformOrigin="0.5,0.5"
|
||||
Stretch="Fill">
|
||||
<Path.RenderTransform>
|
||||
<TransformGroup>
|
||||
<ScaleTransform />
|
||||
<SkewTransform />
|
||||
<RotateTransform Angle="-90" />
|
||||
<TranslateTransform />
|
||||
</TransformGroup>
|
||||
</Path.RenderTransform>
|
||||
</Path>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter TargetName="fousIcon" Property="Opacity" Value="1" />
|
||||
<Setter Property="Foreground" Value="{Binding SelectedRowForeground, RelativeSource={RelativeSource AncestorType={x:Type local:StyledDataGrid}}}" />
|
||||
<Setter TargetName="border" Property="Background" Value="{Binding SelectedRowBackground, RelativeSource={RelativeSource AncestorType={x:Type local:StyledDataGrid}}}" />
|
||||
</Trigger>
|
||||
<Trigger Property="DetailsVisibility" Value="Visible">
|
||||
<Setter TargetName="DetailsPresenter" Property="Margin" Value="0,0,1,0" />
|
||||
</Trigger>
|
||||
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
<Style
|
||||
x:Key="StyledRowHeaderStyle"
|
||||
BasedOn="{StaticResource DefaultRowHeaderStyle}"
|
||||
TargetType="DataGridRowHeader">
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="DataGridRowHeader">
|
||||
<Border
|
||||
Background="Transparent"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="0,0,1,1">
|
||||
<DockPanel LastChildFill="True">
|
||||
<Border
|
||||
x:Name="CheckArea"
|
||||
Width="32"
|
||||
DockPanel.Dock="Right"
|
||||
Visibility="Collapsed">
|
||||
<CheckBox
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
AutomationProperties.Name="选择行"
|
||||
Focusable="True"
|
||||
IsChecked="{Binding IsSelected, RelativeSource={RelativeSource AncestorType=DataGridRow}, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
</Border>
|
||||
<Border
|
||||
x:Name="Divider"
|
||||
Width="1"
|
||||
Background="{TemplateBinding BorderBrush}"
|
||||
DockPanel.Dock="Right"
|
||||
Visibility="Collapsed" />
|
||||
<ContentPresenter
|
||||
x:Name="Number"
|
||||
Content="{Binding Header, RelativeSource={RelativeSource AncestorType=DataGridRow}}"
|
||||
Margin="8,0"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Visibility="Collapsed" />
|
||||
</DockPanel>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<DataTrigger Binding="{Binding ShowRowNumber, RelativeSource={RelativeSource AncestorType=local:StyledDataGrid}}" Value="True">
|
||||
<Setter TargetName="Number" Property="Visibility" Value="Visible" />
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding ShowCheckBox, RelativeSource={RelativeSource AncestorType=local:StyledDataGrid}}" Value="True">
|
||||
<Setter TargetName="CheckArea" Property="Visibility" Value="Visible" />
|
||||
</DataTrigger>
|
||||
<MultiDataTrigger>
|
||||
<MultiDataTrigger.Conditions>
|
||||
<Condition Binding="{Binding ShowRowNumber, RelativeSource={RelativeSource AncestorType=local:StyledDataGrid}}" Value="True" />
|
||||
<Condition Binding="{Binding ShowCheckBox, RelativeSource={RelativeSource AncestorType=local:StyledDataGrid}}" Value="True" />
|
||||
</MultiDataTrigger.Conditions>
|
||||
<Setter TargetName="Divider" Property="Visibility" Value="Visible" />
|
||||
</MultiDataTrigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style
|
||||
x:Key="StyledNumberCellStyle"
|
||||
BasedOn="{StaticResource StyledDefaultCellStyle}"
|
||||
TargetType="DataGridCell">
|
||||
<Setter Property="HorizontalContentAlignment" Value="Right" />
|
||||
</Style>
|
||||
|
||||
<Style BasedOn="{StaticResource DefaultDataGridStyle}" TargetType="local:StyledDataGrid">
|
||||
|
||||
<Setter Property="SelectionMode" Value="Extended" />
|
||||
<Setter Property="SelectionUnit" Value="FullRow" />
|
||||
<Setter Property="RowStyle" Value="{StaticResource StyledDefaultRowStyle}" />
|
||||
<Setter Property="CellStyle" Value="{StaticResource StyledDefaultCellStyle}" />
|
||||
<Setter Property="RowHeaderStyle" Value="{StaticResource StyledRowHeaderStyle}" />
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
@@ -1,38 +1,64 @@
|
||||
using Bogus;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using System.Windows;
|
||||
using MAX.Core.Logger;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Prism.Commands;
|
||||
using Prism.Mvvm;
|
||||
using Trade.Framework.Services;
|
||||
|
||||
namespace Main.Test.ViewModels;
|
||||
|
||||
internal class MainViewModel
|
||||
internal class MainViewModel : BindableBase
|
||||
{
|
||||
private readonly ILoggerFacade _logger;
|
||||
public List<ItemInfo> Data { get; set; }
|
||||
private List<ItemInfo> data = DemoData.Create(10000);
|
||||
public List<ItemInfo> Data { get => data; private set => SetProperty(ref data, value); }
|
||||
private string actionMessage = "可右键查看详情、复制编码或切换状态;默认加载 10,000 行。";
|
||||
public string ActionMessage { get => actionMessage; private set => SetProperty(ref actionMessage, value); }
|
||||
private ObservableCollection<ItemInfo> selectedItems = new();
|
||||
public ObservableCollection<ItemInfo> SelectedItems
|
||||
{
|
||||
get => selectedItems;
|
||||
set
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(value);
|
||||
if (ReferenceEquals(selectedItems, value)) return;
|
||||
selectedItems.CollectionChanged -= SelectionChanged;
|
||||
SetProperty(ref selectedItems, value);
|
||||
selectedItems.CollectionChanged += SelectionChanged;
|
||||
RaisePropertyChanged(nameof(SelectedCount));
|
||||
}
|
||||
}
|
||||
public int SelectedCount => SelectedItems.Count;
|
||||
public int NetworkImageSize { get; set; }
|
||||
public DelegateCommand<ItemInfo> DetailCommand { get; }
|
||||
public DelegateCommand<ItemInfo> ConfirmCommand { get; }
|
||||
public DelegateCommand<ItemInfo> CopyCodeCommand { get; }
|
||||
|
||||
public MainViewModel(ILoggerFacade logger, UserManager userManager)
|
||||
{
|
||||
_logger = logger;
|
||||
_logger.Info("123123");
|
||||
public MainViewModel(ILoggerFacade logger, UserManager userManager)
|
||||
{
|
||||
selectedItems.CollectionChanged += SelectionChanged;
|
||||
DetailCommand = new(item => { if (item != null) ActionMessage = $"详情:{item.Code} · {item.Name} · ¥{item.Value} · {item.Buyer} · {item.Status}"; });
|
||||
ConfirmCommand = new(item =>
|
||||
{
|
||||
if (item == null) return;
|
||||
item.Status = item.Status == "已确认" ? "待处理" : "已确认";
|
||||
ActionMessage = $"已更新 {item.Code}:{item.Status}";
|
||||
});
|
||||
CopyCodeCommand = new(item =>
|
||||
{
|
||||
if (item == null) return;
|
||||
try { Clipboard.SetText(item.Code.ToString()); ActionMessage = $"已复制编码 {item.Code}"; }
|
||||
catch (System.Runtime.InteropServices.ExternalException) { ActionMessage = "剪贴板正忙,请重试。"; }
|
||||
});
|
||||
}
|
||||
private void SelectionChanged(object? sender, NotifyCollectionChangedEventArgs e) => RaisePropertyChanged(nameof(SelectedCount));
|
||||
|
||||
|
||||
|
||||
// 创建 Faker 并配置规则
|
||||
var itemFaker = new Faker<ItemInfo>()
|
||||
.RuleFor(x => x.Code, f => f.Random.Int(1000, 9999)) // 4位随机编码
|
||||
.RuleFor(x => x.Name, f => f.Commerce.ProductName()) // 商品名称
|
||||
.RuleFor(x => x.Value, f => f.Commerce.Price().ToString()) // 商品价格
|
||||
.RuleFor(x => x.Qty, f => f.Random.Int(1, 100)); // 1-100的随机数量
|
||||
|
||||
// 生成单个对象
|
||||
var singleItem = itemFaker.Generate();
|
||||
|
||||
// 生成100个对象
|
||||
Data = itemFaker.Generate(1000);
|
||||
//Data.Clear();
|
||||
}
|
||||
public async Task LoadAsync(int count)
|
||||
{
|
||||
if (count is < 1 or > 50000) throw new ArgumentOutOfRangeException(nameof(count));
|
||||
var imageSize = NetworkImageSize;
|
||||
var rows = await Task.Run(() => DemoData.Create(count, imageSize));
|
||||
SelectedItems.Clear(); Data = rows;
|
||||
ActionMessage = $"已加载 {count:N0} 行;" + (imageSize == 0 ? "本地缩略图。" : $"Picsum {imageSize}×{imageSize} 网络随机图,可见时加载。每次重新加载使用新种子。冷 / 热测试以当前页面已加载状态为准。");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user