WPF中如何访问Material Design Card内控件?含PDF拖拽场景需求
嘿,我来帮你搞定这个PDF拖拽展示的需求!咱们一步步拆解问题,从访问Card内部控件、实现单个删除,到更优雅的实现方案都给你安排明白。
一、先完善你的Card模板(添加必要控件与命名)
首先,你的原始Card里缺少文件名和文件大小的展示控件,而且没有给需要赋值的控件命名,这会导致后续无法准确访问它们。咱们先修改XAML,给关键控件加上x:Name,并补充文件名、大小的TextBlock:
<materialDesign:Card x:Key="PDFThumbnail" Margin="3,3,0,0" Width="200"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="140" /> <RowDefinition Height="*" /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> <!-- 给Image命名,方便后续赋值缩略图 --> <Image x:Name="PdfThumbnailImage" Height="140" Width="196" Stretch="UniformToFill" /> <!-- 添加文件名显示控件 --> <TextBlock x:Name="FileNameText" Grid.Row="1" Margin="8,4" TextTrimming="CharacterEllipsis" FontSize="14"/> <!-- 添加文件大小显示控件 --> <TextBlock x:Name="FileSizeText" Grid.Row="1" Margin="8,28,8,4" FontSize="12" Foreground="Gray"/> <StackPanel HorizontalAlignment="Right" Grid.Row="2" Orientation="Horizontal" Margin="8"> <!-- 给删除按钮命名,绑定点击事件 --> <Button x:Name="DeleteCardBtn" Style="{StaticResource MaterialDesignToolButton}" Width="30" Padding="2 0 2 0" materialDesign:RippleAssist.IsCentered="True"> <materialDesign:PackIcon Kind="Delete" /> </Button> </StackPanel> </Grid> </materialDesign:Card>
二、拖拽文件时实例化Card并赋值内容
接下来,给你的WrapPanel开启拖拽支持(设置AllowDrop="True"),然后处理Drop事件,在里面完成Card的实例化、控件查找与数据赋值:
private void WrapPanel_Drop(object sender, DragEventArgs e) { if (e.Data.GetDataPresent(DataFormats.FileDrop)) { string[] files = (string[])e.Data.GetData(DataFormats.FileDrop); var wrapPanel = sender as WrapPanel; foreach (string filePath in files) { // 只处理PDF文件 if (Path.GetExtension(filePath).Equals(".pdf", StringComparison.OrdinalIgnoreCase)) { // 从资源中获取Card模板并实例化 var pdfCard = (Card)FindResource("PDFThumbnail"); // 查找Card内部的控件 var thumbnailImage = pdfCard.FindName("PdfThumbnailImage") as Image; var fileNameText = pdfCard.FindName("FileNameText") as TextBlock; var fileSizeText = pdfCard.FindName("FileSizeText") as TextBlock; var deleteBtn = pdfCard.FindName("DeleteCardBtn") as Button; // 赋值文件名与文件大小 FileInfo fileInfo = new FileInfo(filePath); fileNameText.Text = fileInfo.Name; fileSizeText.Text = $"{FormatFileSize(fileInfo.Length)}"; // 生成并赋值PDF缩略图(这里需要你引入PDF处理库,比如PdfiumViewer) thumbnailImage.Source = GetPdfThumbnail(filePath); // 其他逻辑后面加... } } } } // 辅助方法:格式化文件大小为易读格式 private string FormatFileSize(long bytes) { if (bytes < 1024) return $"{bytes} B"; if (bytes < 1048576) return $"{bytes / 1024:F1} KB"; if (bytes < 1073741824) return $"{bytes / 1048576:F1} MB"; return $"{bytes / 1073741824:F1} GB"; } // 示例:生成PDF第一页缩略图的方法(需安装NuGet包PdfiumViewer) private BitmapImage GetPdfThumbnail(string filePath) { using (var document = PdfiumViewer.PdfDocument.Load(filePath)) { using (var image = document.Render(0, 196, 140, true)) { var bitmapImage = new BitmapImage(); using (var memoryStream = new MemoryStream()) { image.Save(memoryStream, ImageFormat.Png); memoryStream.Position = 0; bitmapImage.BeginInit(); bitmapImage.CacheOption = BitmapCacheOption.OnLoad; bitmapImage.StreamSource = memoryStream; bitmapImage.EndInit(); } return bitmapImage; } } }
三、实现单个Card的删除功能
在刚才的Drop事件里,给删除按钮绑定Click事件,点击时把当前Card从WrapPanel中移除:
// 接上面的Drop事件代码,在找到deleteBtn后添加: deleteBtn.Click += (s, args) => { wrapPanel.Children.Remove(pdfCard); }; // 最后把实例化好的Card添加到WrapPanel wrapPanel.Children.Add(pdfCard);
四、选中Card与批量删除(可选)
如果你需要先选中Card再删除,咱们可以给Card添加选中状态的逻辑:
// 接上面的Drop事件代码,添加选中逻辑: pdfCard.MouseLeftButtonDown += (s, args) => { // 清除所有Card的选中状态 foreach (var child in wrapPanel.Children) { if (child is Card card) { card.BorderThickness = new Thickness(0); card.Background = Brushes.White; // 恢复默认背景色 } } // 设置当前Card为选中状态 pdfCard.BorderThickness = new Thickness(2); pdfCard.BorderBrush = Brushes.DodgerBlue; pdfCard.Background = Brushes.AliceBlue; };
然后添加一个全局删除按钮,点击删除选中的Card:
private void DeleteSelectedCardBtn_Click(object sender, RoutedEventArgs e) { Card selectedCard = null; foreach (var child in MyWrapPanel.Children) { if (child is Card card && card.BorderThickness.Left == 2) { selectedCard = card; break; } } if (selectedCard != null) { MyWrapPanel.Children.Remove(selectedCard); } }
五、更优雅的替代方案:ItemsControl + MVVM绑定
如果你的项目规模较大,或者想让代码更易维护,推荐用ItemsControl替代直接操作WrapPanel的Children,结合MVVM模式实现数据绑定,这样就不需要手动查找控件了:
1. 定义ViewModel类
public class PdfFileViewModel : INotifyPropertyChanged { private string _fileName; public string FileName { get => _fileName; set { _fileName = value; OnPropertyChanged(); } } private string _fileSize; public string FileSize { get => _fileSize; set { _fileSize = value; OnPropertyChanged(); } } private BitmapImage _thumbnail; public BitmapImage Thumbnail { get => _thumbnail; set { _thumbnail = value; OnPropertyChanged(); } } public ICommand DeleteCommand { get; set; } public event PropertyChangedEventHandler PropertyChanged; protected void OnPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } } public class MainViewModel : INotifyPropertyChanged { public ObservableCollection<PdfFileViewModel> PdfFiles { get; set; } = new ObservableCollection<PdfFileViewModel>(); public ICommand DeleteSelectedCommand { get; set; } private PdfFileViewModel _selectedPdf; public PdfFileViewModel SelectedPdf { get => _selectedPdf; set { _selectedPdf = value; OnPropertyChanged(); } } public MainViewModel() { DeleteSelectedCommand = new RelayCommand(() => { if (SelectedPdf != null) { PdfFiles.Remove(SelectedPdf); } }); } public event PropertyChangedEventHandler PropertyChanged; protected void OnPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } } // 简单的RelayCommand实现 public class RelayCommand : ICommand { private readonly Action _execute; private readonly Func<bool> _canExecute; public event EventHandler CanExecuteChanged; public RelayCommand(Action execute, Func<bool> canExecute = null) { _execute = execute ?? throw new ArgumentNullException(nameof(execute)); _canExecute = canExecute; } public bool CanExecute(object parameter) => _canExecute == null || _canExecute(); public void Execute(object parameter) => _execute(); public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty); }
2. 修改XAML使用ItemsControl
<Window.DataContext> <local:MainViewModel /> </Window.DataContext> <Grid> <Grid.RowDefinitions> <RowDefinition Height="*" /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> <!-- 用ItemsControl绑定PDF文件集合,内部用WrapPanel布局 --> <ItemsControl ItemsSource="{Binding PdfFiles}" AllowDrop="True" PreviewDrop="ItemsControl_Drop"> <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <WrapPanel /> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> <ItemsControl.ItemTemplate> <DataTemplate> <materialDesign:Card Margin="3,3,0,0" Width="200" MouseLeftButtonDown="Card_MouseLeftButtonDown"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="140" /> <RowDefinition Height="*" /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> <Image Source="{Binding Thumbnail}" Height="140" Width="196" Stretch="UniformToFill" /> <TextBlock Text="{Binding FileName}" Grid.Row="1" Margin="8,4" TextTrimming="CharacterEllipsis" FontSize="14"/> <TextBlock Text="{Binding FileSize}" Grid.Row="1" Margin="8,28,8,4" FontSize="12" Foreground="Gray"/> <StackPanel HorizontalAlignment="Right" Grid.Row="2" Orientation="Horizontal" Margin="8"> <Button Style="{StaticResource MaterialDesignToolButton}" Width="30" Padding="2 0 2 0" Command="{Binding DeleteCommand}"> <materialDesign:PackIcon Kind="Delete" /> </Button> </StackPanel> </Grid> </materialDesign:Card> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl> <Button Grid.Row="1" Content="删除选中文件" Command="{Binding DeleteSelectedCommand}" Margin="8"/> </Grid>
3. 处理拖拽事件
private void ItemsControl_Drop(object sender, DragEventArgs e) { if (e.Data.GetDataPresent(DataFormats.FileDrop)) { string[] files = (string[])e.Data.GetData(DataFormats.FileDrop); var viewModel = DataContext as MainViewModel; foreach (string filePath in files) { if (Path.GetExtension(filePath).Equals(".pdf", StringComparison.OrdinalIgnoreCase)) { FileInfo fileInfo = new FileInfo(filePath); var pdfVm = new PdfFileViewModel { FileName = fileInfo.Name, FileSize = FormatFileSize(fileInfo.Length), Thumbnail = GetPdfThumbnail(filePath) }; // 绑定删除命令 pdfVm.DeleteCommand = new RelayCommand(() => { viewModel.PdfFiles.Remove(pdfVm); }); viewModel.PdfFiles.Add(pdfVm); } } } } private void Card_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { var card = sender as Card; var pdfVm = card.DataContext as PdfFileViewModel; var mainVm = DataContext as MainViewModel; mainVm.SelectedPdf = pdfVm; }
这样一来,所有的数据操作都通过ViewModel完成,不需要再手动查找控件,代码更清晰,也更容易扩展功能~
内容的提问来源于stack exchange,提问作者Emre Kadan




