You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

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

火山引擎 最新活动