admin 发布的文章

参考教程:
1.https://www.cnblogs.com/manupstairs/p/4890300.html
2.https://www.cnblogs.com/manupstairs/p/4909585.html
3.https://www.cnblogs.com/manupstairs/p/4928888.html
4.https://www.cnblogs.com/manupstairs/p/4948347.html
5.https://www.cnblogs.com/manupstairs/p/13661227.html

-----适合小型项目,简化版结构,不使用Locator,直接在XAML中设置DataContext
1.创建 WPF 项目
打开 Visual Studio 并创建一个新的 WPF 应用项目。

2.安装 MVVM Light
在解决方案资源管理器中,右击项目名称,选择“管理 NuGet 包”。
在 NuGet 包管理器中搜索 MvvmLightLibs 并安装该包。

3.创建 ViewModel
在项目中添加一个新的类文件,命名为 MainViewModel.cs。
让这个类继承自 ViewModelBase 类,添加一个属性用于数据绑定、添加一个Command命令:

using GalaSoft.MvvmLight;

public class MainViewModel : ViewModelBase
{
    public MainViewModel()
    {
        UpdateMessageCommand = new RelayCommand(UpdateMessage);
    }

    private string _welcomeMessage = "Hello, MVVM Light!";

    public string WelcomeMessage
    {
        get => _welcomeMessage;
        set => Set(ref _welcomeMessage, value, , nameof(WelcomeMessage));
        //在使用 MVVMLight 框架时,通常不需要直接调用 RaisePropertyChanged 方法,因为 MVVMLight 的 
          ViewModelBase 类已经为您处理了属性更改通知。您只需要在属性的 setter 方法中使用 Set 方法,并传入属性字段 
          的引用和新值,Set 方法会自动触发属性更改通知。
    }

    public RelayCommand UpdateMessageCommand { get; }
    private void UpdateMessage()
    {
        WelcomeMessage = "Message updated!";
    }
}

4.设置 DataContext
修改 MainWindow.xaml 的 XAML 代码,以将 DataContext 设置为 MainViewModel 的实例(非共享,每个独立,可以使用Locator共享MainViewModel):

<Window x:Class="YourNamespace.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:YourNamespace"
        xmlns:vm="clr-namespace:PictureStitching.ViewModel" <!-- 添加这个,如果ViewModel在子命名空间下 -->
        Title="MainWindow" Height="350" Width="525">
    <Window.DataContext>
        <vm:MainViewModel />
    </Window.DataContext>
    <Grid>
        <TextBlock Text="{Binding WelcomeMessage}" />
        <Button Content="Update Message" Command="{Binding UpdateMessageCommand}" />
    </Grid>
</Window>

在Visual Studio中设计WPF应用时,窗口大小包括边框和标题栏,这确实可能导致设计时和运行时窗口大小的差异。要解决这个问题,可以采用以下方法:
---使用SizeToContent属性:
设置Window窗口的SizeToContent属性为:SizeToContent="WidthAndHeight"
这样窗口会自动调整自己的大小以刚好容纳其内容。
这种方法确保了设计时内容区的大小和运行时一致,但可能会限制对窗口大小的手动控制。

[Parsed_movie_0 @ 000000cdff819fc0] Failed to avformat_open_input 'C'
[AVFilterGraph @ 000000cdff3c0f80] Error initializing filter 'movie' with args 'E:\image.jpg'
Error reinitializing filters!
Failed to inject frame into filter network: No such file or directory
Error while processing the decoded data for stream #0:0

路径的冒号是需要转义的,把路径的转义一下就可以了
像这样:

ffmpeg -i E:\1.mp4 -vf "movie='E\:\\icon.ico'[wm]; [in][wm]overlay=30:30[out]" E:\video\output.mp4
watermarkImage = "C:\\image\\water.png"
watermarkImage = watermarkImage.Replace("\\", @"/").Replace(":", @"\:");
// 构造 ffplay 的参数,用于预览水印效果
string ffplayArgs = $"-i \"{selectedVideo}\" -vf \"movie='{watermarkImage}' [watermark]; [in][watermark] overlay=x='if(eq(mod(floor(t),{intervalTime}), 0), clip(sin({randomLeapX}*floor(t)) * (main_w-overlay_w-1)/2 + (main_w-overlay_w)/2, 0, main_w-overlay_w), x)':y='if(eq(mod(floor(t),{intervalTime}), 0), clip(sin({randomLeapY}*floor(t)) * (main_h-overlay_h-1)/2 + (main_h-overlay_h)/2, 0, main_h-overlay_h), y)'\" -x {windowWidth} -y {windowHeight} -window_title 预览";

1.创建一个静态类,命名为 GlobalConfig。
2.在 GlobalConfig 类中定义静态只读属性或常量,用于存储你的配置信息(例如 ServerUrl、SoftwareVersion、VersionNumber、ActivationFolderName、ActivationFileName、UpdateUrl)。

public static class GlobalConfig
{
    public static readonly string ServerUrl = "你的服务器URL";
    public static readonly string SoftwareVersion = "软件版本";
    public static readonly int VersionNumber = 1; // 假设版本号是整数
    public static readonly string ActivationFolderName = "激活文件夹名称";
    public static readonly string ActivationFileName = "激活文件名称";
    public static readonly string UpdateUrl = "更新URL";
}
string serverUrl = GlobalConfig.ServerUrl;

添加对 System.Configuration 的引用:
1.如果你的项目是 .NET Framework 项目,System.Configuration 通常已经包含在框架中。但在一些情况下,你可能需要手动添加对它的引用。
2.对于 .NET Core 或 .NET 5/6 项目,ConfigurationManager 不再包含在核心框架中。你需要通过NuGet添加 System.Configuration.ConfigurationManager 包。

最后:using System.Configuration;

缺点:会和exe文件一起,输出外部配置文件

你提到的方法是在新项目中重复利用现有项目的包依赖,是一种有效的方法。以下是详细步骤:

1.将项目的packages.config文件拷贝到新项目中。这个文件通常位于项目根目录下,用于指定项目所依赖的NuGet包及其版本信息。
2.打开NuGet Package Manager控制台。
PixPin_2024-03-07_02-13-59.png
在Visual Studio中,你可以通过依次选择“Tools” -> “NuGet Package Manager” -> “Package Manager Console” 打开NuGet Package Manager控制台。
4.使用命令还原包,重新下载。
在NuGet Package Manager控制台中,输入以下命令:

Update-Package -ProjectName 'YourProjectName' -Reinstall

在这个命令中,将YourProjectName替换为你要操作的项目的名称。

通过这些步骤,你就可以将现有项目中的包依赖复制到新项目中,并确保它们被正确安装和引用。

<?xml version="1.0" encoding="utf-8" ?>
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
  <Costura />
</Weavers>
<?xml version="1.0" encoding="utf-8" ?>
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
    <Costura>
        <ExcludeAssemblies>
            PdfiumViewer
        </ExcludeAssemblies>
    </Costura>
</Weavers>

让 Costura 排除 PdfiumViewer.dll, 标签下应该只有程序集的名称,而不是文件路径,而且不需要文件扩展名 .dll
教程:https://blog.csdn.net/wangjiaoshoudebaba/article/details/80787677

使用 DataTrigger 可以使你在 XAML 中根据数据绑定的值来动态地改变控件的外观或行为。这在创建交互式用户界面时非常有用,例如根据用户的输入来修改控件的样式或显示不同的内容。

  1. 创建Model(模型)
    首先定义一个LoginModel模型来表示登录信息。

    namespace Login.Model
    {
     public class LoginModel
     {
         public string Username { get; set; }
         public string Password { get; set; }
     }
    }
  2. 创建ViewModel(视图模型)
    ViewModel作为View和Model之间的桥梁,包含用户输入的数据和命令绑定。

    using System;
    using System.ComponentModel;
    using System.Windows.Input;
    using Login.Command;
    using Login.Model;
    
    namespace Login.ViewModel
    {
     public class LoginViewModel : INotifyPropertyChanged
     {
    
         private LoginModel loginModel;
    
         public String Username
         {
             get { return loginModel.Username; }
             set
             { 
                 this.loginModel.Username = value;
                 OnPropertyChanged(nameof(Username));
             }
         }
    
         public String Password
         { 
             get { return loginModel.Password; }
             set
             { 
                 this.loginModel.Password = value;
                 OnPropertyChanged("Password");
             }
         }
    
         public ICommand LoginCommand { get; private set; }
    
         public LoginViewModel()
         {
             loginModel = new LoginModel();
             LoginCommand = new RelayCommand(Login, CanLogin);
         }
    
         private void Login(object parameter)
         {
             // 在这里添加登入逻辑
             if (CanLogin(null))
             {
                 Console.WriteLine($"登录成功:用户名={Username}, 密码={Password}");
             }
         }
    
         private bool CanLogin(object parameter)
         {
             // 在这里添加验证逻辑
             return !string.IsNullOrEmpty(Username) && !string.IsNullOrEmpty(Password);
         }
    
         public event PropertyChangedEventHandler PropertyChanged;
         protected virtual void OnPropertyChanged(string propertyName)
         {
             PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
         }
     }
    }
  3. 创建View(视图)
    接下来创建XAML界面用于用户输入和触发登录命令。

    <Window x:Class="Login.MainWindow"
         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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
         xmlns:local="clr-namespace:Login"
         mc:Ignorable="d"
         Title="MainWindow" Height="450" Width="800">
     <Grid>
         <StackPanel Margin="20">
             <Label Content="Username:"/>
             <TextBox Text="{Binding Username, UpdateSourceTrigger=PropertyChanged}" Margin="0,5"/>
             <Label Content="Password:"/>
             <TextBox Text="{Binding Password, UpdateSourceTrigger=PropertyChanged}" Margin="0,5"/>
             <Button Content="Login" Command="{Binding LoginCommand}" Width="100" Height="30" Margin="0,15"/>
         </StackPanel>
     </Grid>
    </Window>

    4.创建RelayCommand
    RelayCommand是一个实现了ICommand接口的类,用于绑定View中的命令到ViewModel的命令处理方法

    using System;
    using System.Windows.Input;
    
    namespace Login.Command
    {
     public class RelayCommand : ICommand
     {
         private readonly Action<object> _execute;
         private readonly Func<object, bool> _canExecute;
    
         public RelayCommand(Action<object> execute, Func<object, bool> canExecute = null)
         {
             _execute = execute ?? throw new ArgumentNullException(nameof(execute));
             _canExecute = canExecute;
         }
    
         public event EventHandler CanExecuteChanged
         {
             add { CommandManager.RequerySuggested += value; }
             remove { CommandManager.RequerySuggested -= value; }
         }
    
         public bool CanExecute(object parameter)
         {
             return _canExecute == null || _canExecute(parameter);
         }
    
         public void Execute(object parameter)
         {
             _execute(parameter);
         }
     }
    }

    5.绑定DataContext
    建立视图(UI)与视图模型(ViewModel)之间的数据绑定关系

    using System.Windows.Data;
    using Login.ViewModel;
    
    namespace Login
    {
     /// <summary>
     /// MainWindow.xaml 的交互逻辑
     /// </summary>
     public partial class MainWindow : Window
     {
         public MainWindow()
         {
             InitializeComponent();
             DataContext = new LoginViewModel();
         }
     }
    }

  1. 安装AutoUpdater.NET:打开你的WPF项目,然后使用NuGet包管理器来安装AutoUpdater.NET。
  2. 配置更新过程
    a. 在你的WPF应用程序中找到主窗体的代码文件。
    b. 引入AutoUpdater.NET命名空间:

    using AutoUpdaterDotNET;

    c. 在主窗体的Loaded事件中配置AutoUpdater:

    • 以下是在主窗体的Loaded事件中启动自动更新的示例:

      /**
          * 配置 AutoUpdater.NET 检查更新参数
          */
         private async void MainWindow_Loaded(object sender, RoutedEventArgs e)
         {
             try
             {
                 await Task.Delay(300);
      
                 if (GlobalConfig.SoftwareVersionId <= 0)
                 {
                     return;
                 }
      
                 /** 设置图标,将 System.Drawing.Icon 对象转换为 System.Drawing.Bitmap 对象 */
                 AutoUpdater.UpdateFormSize = new System.Drawing.Size(800, 500);
                 try
                 {
                     AutoUpdater.Icon = Properties.Resources.Icon.ToBitmap();
                 }
                 catch
                 {
                     /** 资源缺失不致命,保持默认图标即可,不阻断更新检查 */
                 }
      
                 /** 从配置文件读取更新URL */
                 string updateUrl = $"{GlobalConfig.ServerUrl}/api/client/update/xml/{GlobalConfig.SoftwareVersionId}";
                 AutoUpdater.Start(updateUrl);
             }
             catch
             {
                 /** async void 顶层兜底:避免 AutoUpdater / 资源加载异常被 Dispatcher.UnhandledException 上抛终结进程 */
             }
         }
    • 确保将URL替换为指向你的更新XML文件的真实URL。
  3. 服务器配置 XML 文件

    <?xml version="1.0" encoding="UTF-8"?>
    <item>
      <version>1.1.0.0</version>
      <url>https://update.codespool.com/VideoWatermark/AutoUpdaterTest.zip</url>
      <changelog>https://update.codespool.com/VideoWatermark/release.html</changelog>
      <mandatory>false</mandatory>
    </item>
  4. 服务器配置 changelog文件

    <!DOCTYPE html>
    <html lang="en">
    <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>软件更新日志</title>
     <style>
         body {
             font-family: Arial, sans-serif;
             line-height: 1.6;
             padding: 20px;
         }
         h1 {
             text-align: center;
         }
         h2 {
             margin-top: 30px;
         }
         ul {
             margin-left: 20px;
         }
     </style>
    </head>
    <body>
     <h1>软件更新日志</h1>
     
     <h2>版本 1.0.0</h2>
     <ul>
         <li>新增功能:用户登录</li>
         <li>新增功能:创建和编辑个人资料</li>
         <li>新增功能:查看其他用户资料</li>
         <li>修复了一些已知的 bug</li>
     </ul>
    </body>
    </html>

1.保持原有视频的参数(如编码器、比特率等)不变
在这个命令中:
-c:v libx264 指定使用 H.264 视频编码器。
-crf 18 设置常量速率因子(CRF),其中较低的值表示较高的质量。CRF 18-20 通常被认为是视觉上无损的高质量编码。
-preset slow 提高编码效率和质量,但会花费更多的时间。

ffmpeg -i \"{videoFile}\" -i \"{watermarkImage}\" -filter_complex \"overlay=x='mod(main_h/0.8*ceil(t),main_w)':y='mod(main_w/3*ceil(t/2),main_h)'\" -c:v libx264 -crf 18 -preset slow \"{outputVideo}\"

2.

string ffmpegArgs = $"-i \"{videoFile}\" -i \"{watermarkImage}\" -filter_complex \"overlay=x='mod(main_h/0.8*ceil(t),main_w)':y='mod(main_w/3*ceil(t/2),main_h)'\" -b:v 36729k -c:a copy \"{outputVideo}\"";

3.

string ffmpegArgs = $"-i \"{videoFile}\" -i \"{watermarkImage}\" -filter_complex \"overlay=x='mod(main_h/0.8*ceil(t),main_w)':y='mod(main_w/3*ceil(t/2),main_h)'\" -c:v libx264 -crf 18 -preset slow -c:a libfdk_aac -vbr 4 \"{outputVideo}\"";

问题:在使用cmd窗口执行以上命令时(cmd中参数前面要加 ffmpeg 注意文件位置),可以成功处理,但在运行WPF测试的时候,发现只有一个大小为0kb的新文件生成,但迟迟不见处理。给人一种假死的现象。而当关掉调试的WPF程序时,过几秒钟,貌似ffmpeg.exe 又起作用了,文件处理成功了。这个不得其解.
原因:process.WaitForExit(); 这句执行会造成程序一直处于等待状态。于是,抱着试试看的态度,注释了这一句。当然,程序不再等待外部进程完成执行,proces.Close(); 这一句也要注释一下,测试结果成功!

<Application x:Class="VideoWatermark.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:VideoWatermark"
             StartupUri="MainWindow.xaml">
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/SkinDefault.xaml"/>
                <ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/Theme.xaml"/>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>
</Application>
<Window x:Class="VideoWatermark.MainWindow"
        xmlns:hc="https://handyorg.github.io/handycontrol"
        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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:VideoWatermark"
        Icon="ico.ico"
        mc:Ignorable="d"
        ResizeMode="CanMinimize"
        Title="VideoWatermark" Height="500" Width="815">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="50"/>
            <RowDefinition Height="50"/>
            <RowDefinition Height="50"/>
            <RowDefinition Height="300"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="800"/>
        </Grid.ColumnDefinitions>

        <StackPanel Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center">
            <TextBox x:Name="VideosPathTextBox" Style="{StaticResource TextBoxExtend}" hc:InfoElement.TitlePlacement="Left" hc:InfoElement.Title="视频文件:" Width="600" VerticalAlignment="Center" IsEnabled="False"/>
            <Button Style="{StaticResource ButtonPrimary}" Width="85" Content="选择文件" Click="SelectVideoButton_Click"/>
        </StackPanel>
        <StackPanel Grid.Row="1" Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center">
            <TextBox x:Name="TxtWatermarkPath" Style="{StaticResource TextBoxExtend}" hc:InfoElement.TitlePlacement="Left" hc:InfoElement.Title="图片水印:" Width="600" VerticalAlignment="Center" IsEnabled="False"/>
            <Button  Style="{StaticResource ButtonPrimary}" Width="85"  Content="选择水印" Click="Button_WatermarkClick"/>
        </StackPanel>
        <StackPanel Grid.Row="2" Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center">
            <Button Style="{StaticResource ButtonPrimary}" Width="335"  Content="开始" Click="Button_AddWatermarkButton_Click" Margin="0,0,10,0"/>
            <Button Style="{StaticResource ButtonPrimary}" Width="335"  Content="清空" Click="Button_ClearList_Click"/>
        </StackPanel>

        <StackPanel Grid.Row="3" Grid.Column="0" Orientation="Vertical" VerticalAlignment="Top" HorizontalAlignment="Center">
            <ListView x:Name="VideosListView" ItemsSource="{Binding DataList}" >
                <ListView.View>
                    <GridView>
                        <GridViewColumn Width="80" Header="序号" DisplayMemberBinding="{Binding Index}"/>
                        <GridViewColumn Width="500" Header="名称" DisplayMemberBinding="{Binding Name}"/>
                        <GridViewColumn Width="80" Header="状态" DisplayMemberBinding="{Binding Remark}"/>
                    </GridView>
                </ListView.View>
            </ListView>
            <Border x:Name="NoDataBorder" Style="{StaticResource BorderRegion}" Visibility="Visible">
                <TextBlock Text="没有数据" Height="200" HorizontalAlignment="Center" Foreground="#000000"/>
            </Border>
        </StackPanel>
    </Grid>
</Window>
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.IO;
using Microsoft.Win32;
using VideoWatermark.Models;
using System.Diagnostics;


namespace VideoWatermark
{
    /// <summary>
    /// MainWindow.xaml 的交互逻辑
    /// </summary>
    public partial class MainWindow : Window
    {
        public ObservableCollection<VideoItem> DataList { get; set; } = new ObservableCollection<VideoItem>();
        public MainWindow()
        {
            InitializeComponent();
            DataList = new ObservableCollection<VideoItem>(); // 初始化DataList
            DataContext = this;
        }

        private void SelectVideoButton_Click(object sender, RoutedEventArgs e)
        {
            OpenFileDialog openFileDialog = new OpenFileDialog
            {
                Multiselect = true,
                Filter = "Video files (*.mp4;*.avi;*.wmv)|*.mp4;*.avi|All files (*.*)|*.*"
            };

            if (openFileDialog.ShowDialog() == true)
            {
                // 获取已在DataList中的文件名
                var existingFiles = DataList.Select(v => v.Name).ToList();

                // 将选中的文件路径添加到文本框,同时避免重复
                List<string> newFiles = new List<string>();
                int index = DataList.Count + 1; // 计算新的起始序号

                foreach (string fileName in openFileDialog.FileNames)
                {
                    string justFileName = Path.GetFileName(fileName);
                    if (!existingFiles.Contains(justFileName))
                    {
                        newFiles.Add(fileName);
                        // 只有当文件不在列表中时,才添加到DataList
                        DataList.Add(new VideoItem
                        {
                            Index = index++,
                            Name = justFileName,
                            Remark = "待处理"
                        });
                    }
                }

                // 更新文本框
                if (newFiles.Any())
                {
                    string existingText = VideosPathTextBox.Text;
                    if (!string.IsNullOrEmpty(existingText))
                    {
                        VideosPathTextBox.Text += ";";
                    }
                    VideosPathTextBox.Text += string.Join(";", newFiles);
                }
                UpdateNoDataVisibility();

                // 更新ListView
                VideosListView.ItemsSource = null;
                VideosListView.ItemsSource = DataList;
            }
        }

        private void Button_WatermarkClick(object sender, RoutedEventArgs e)
        {
            OpenFileDialog openFileDialog = new OpenFileDialog();
            openFileDialog.Filter = "图片文件|*.png;*.jpg;*.jpeg|所有文件|*.*";

            if (openFileDialog.ShowDialog() == true)
            {
                TxtWatermarkPath.Text = openFileDialog.FileName;
            }
        }

        private void Button_ClearList_Click(object sender, RoutedEventArgs e)
        {
            DataList.Clear();
            VideosPathTextBox.Text = string.Empty;
            VideosListView.ItemsSource = null;
            VideosListView.ItemsSource = DataList;
            UpdateNoDataVisibility();
            HandyControl.Controls.MessageBox.Show(this, "列表已清空", "提示");
        }


        private async void Button_AddWatermarkButton_Click(object sender, RoutedEventArgs e)
        {
            string watermarkImage = TxtWatermarkPath.Text;
            string[] videoFiles = VideosPathTextBox.Text.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries);

            // 检查是否导入了视频文件
            if (videoFiles.Length == 0)
            {
                HandyControl.Controls.MessageBox.Show(this, "请先导入视频文件!", "缺少视频");
                return;
            }

            // 检查是否导入了水印图片
            if (string.IsNullOrWhiteSpace(watermarkImage) || !File.Exists(watermarkImage))
            {
                HandyControl.Controls.MessageBox.Show(this, "请先导入水印图片!", "缺少水印");
                return;
            }


            foreach (string videoFile in videoFiles)
            {
                // 检查视频文件是否存在
                if (!File.Exists(videoFile))
                {
                    UpdateVideoItemStatus(Path.GetFileName(videoFile), "文件不存在");
                    continue; // 跳过不存在的文件
                }

                UpdateVideoItemStatus(Path.GetFileName(videoFile), "正在处理");

                await Task.Run(() => RunFFmpegProcess(videoFile, watermarkImage));

                // 更新视频状态为 "已完成"
                UpdateVideoItemStatus(Path.GetFileName(videoFile), "已完成");
            }

            HandyControl.Controls.MessageBox.Show(this, "所有添加水印操作完成!", "提示");
        }


        private void RunFFmpegProcess(string videoFile, string watermarkImage)
        {
            string outputVideo = Path.Combine(Path.GetDirectoryName(videoFile), "watermarked_" + Path.GetFileName(videoFile));
            // 在这里编写使用 FFmpeg 添加水印的代码
            string ffmpegArgs = $"-i \"{videoFile}\" -i \"{watermarkImage}\" -filter_complex \"overlay=x='mod(main_h/0.8*ceil(t),main_w)':y='mod(main_w/3*ceil(t/2),main_h)'\" \"{outputVideo}\"";
            // 请确保你已经正确配置 FFmpeg.AutoGen,并且有 FFmpeg 的可执行文件
            //string ffmpegPath = @"C:\Users\az102\ Visual Studio Source\repos\VideoWatermark\VideoWatermark\FFmpeg\ffmpeg.exe"; // 请根据实际路径配置 FFmpeg 可执行文件路径
            string ffmpegPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "FFmpeg", "ffmpeg.exe");

            Process process = new Process();
            process.StartInfo.FileName = ffmpegPath;
            process.StartInfo.Arguments = $"-i \"{videoFile}\" -i \"{watermarkImage}\" -filter_complex \"overlay=x='mod(main_h/0.8*ceil(t),main_w)':y='mod(main_w/3*ceil(t/2),main_h)'\" \"{outputVideo}\"";
            process.StartInfo.UseShellExecute = false;
            process.StartInfo.RedirectStandardOutput = true;
            process.StartInfo.CreateNoWindow = true;

            process.Start();
            process.WaitForExit();
            process.Close();

        }

        private void UpdateVideoItemStatus(string fileName, string status)
        {
            var videoItem = DataList.FirstOrDefault(v => v.Name == fileName);
            if (videoItem != null)
            {
                Application.Current.Dispatcher.Invoke(() =>
                {
                    videoItem.Remark = status;
                });
            }
        }

        private void UpdateNoDataVisibility()
        {
            if (DataList.Count > 0)
            {
                NoDataBorder.Visibility = Visibility.Collapsed;
            }
            else
            {
                NoDataBorder.Visibility = Visibility.Visible;
            }
        }

    }
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.ComponentModel;

namespace VideoWatermark.Models
{
    public class VideoItem : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        public int Index { get; set; }

        private string name;
        public string Name
        {
            get { return name; }
            set
            {
                name = value;
                OnPropertyChanged("Name");
            }
        }

        private string remark;
        public string Remark
        {
            get { return remark; }
            set
            {
                remark = value;
                OnPropertyChanged("Remark");
            }
        }

        protected void OnPropertyChanged(string propertyName)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

1.Grid:是 WPF 中用于创建灵活布局的强大控件。它允许您通过行和列来安排子元素。
基本语法:

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="*"/>
        <RowDefinition Height="2*"/>
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto"/>
        <ColumnDefinition Width="*"/>
    </Grid.ColumnDefinitions>

    <!-- 子元素 -->
    <Button Content="Button 1" Grid.Row="0" Grid.Column="0"/>
    <Button Content="Button 2" Grid.Row="0" Grid.Column="1"/>
    <TextBox Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2"/>
</Grid>

主要属性:
▪RowDefinitions 和 ColumnDefinitions: 定义网格的行和列。每个 RowDefinition 和 ColumnDefinition 可以设置其宽度和高度。
▪Height 和 Width: 可以设置为固定值、Auto(自动调整大小以适应内容)或星号表示法(如 *),星号表示法用于分配剩余空间。
▪Grid.Row 和 Grid.Column: 指定子元素位于哪一行和哪一列。行列编号从 0 开始。
▪Grid.RowSpan 和 Grid.ColumnSpan: 指定子元素横跨的行数或列数。
创建具有两行两列的网格,其中第一行高度自适应,第二行高度是第一行的两倍;两列宽度均分配:

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="2*"/>
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*"/>
        <ColumnDefinition Width="*"/>
    </Grid.ColumnDefinitions>

    <TextBlock Text="Row 0, Column 0" Grid.Row="0" Grid.Column="0"/>
    <TextBlock Text="Row 0, Column 1" Grid.Row="0" Grid.Column="1"/>
    <TextBlock Text="Row 1, Column 0" Grid.Row="1" Grid.Column="0"/>
    <TextBlock Text="Row 1, Column 1" Grid.Row="1" Grid.Column="1"/>
</Grid>

2.StackPanel:是 WPF 中的一种布局控件,用于按顺序(水平或垂直)排列子元素。
基本语法:

<StackPanel Orientation="Horizontal" VerticalAlignment="Top" HorizontalAlignment="Left">
    <!-- 子元素 -->
    <Button Content="Button 1"/>
    <Button Content="Button 2"/>
    <Button Content="Button 3"/>
</StackPanel>

主要属性:
▪Orientation: 决定子元素是水平(Horizontal)排列还是垂直(Vertical)排列。默认值是 Vertical。
▪VerticalAlignment 和 HorizontalAlignment: 决定 StackPanel 如何在其父容器中对齐。可以设置为 Top、Bottom、Left、Right 或 Center。
▪Margin: 设置 StackPanel 边缘与其周围元素之间的距离。
▪Background: 设置 StackPanel 的背景色。
垂直排列的 StackPanel:

<StackPanel Orientation="Vertical">
    <Button Content="Button 1"/>
    <Button Content="Button 2"/>
    <TextBox Text="Enter Text Here"/>
</StackPanel>

水平排列的 StackPanel:

<StackPanel Orientation="Horizontal">
    <Button Content="Button 1"/>
    <Button Content="Button 2"/>
    <TextBox Text="Enter Text Here"/>
</StackPanel>

3.WrapPanel:与 StackPanel 类似,但当空间不足以容纳更多元素时,它会将元素包裹到下一行或列。
4.DockPanel:允许子元素停靠到父容器的顶部、底部、左侧或右侧。
5.Canvas:提供了一个绝对定位的空间,你可以在其中明确地设置子元素的精确位置和大小。
6.UniformGrid:类似于 Grid,但所有单元格的大小都是一致的。
7.TabControl:允许通过选项卡来组织内容,每个选项卡对应一个页面。
8.Expander:提供了一个可以展开和折叠的区域,用于显示隐藏的内容。
9.ScrollViewer:当内容超出可视区域时,提供滚动功能。
10.Border:虽然主要用于装饰,但也可以用作布局控件,为其子元素提供边框。
11、Viewbox:自动缩放其内容以适应分配给它的空间。
12.StackPanel 和 DockPanel 的组合:这些控件经常结合使用,以实现更复杂的布局。