├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.yml │ └── feature-request.yml └── workflows │ └── release.yml ├── .gitignore ├── FindDuplicateFiles.sln ├── LICENSE ├── README.md ├── docs └── resources │ └── images │ ├── demo1.png │ ├── demo2.png │ ├── icon.png │ └── logo.png └── src ├── AboutWindow.xaml ├── AboutWindow.xaml.cs ├── App.xaml ├── App.xaml.cs ├── AssemblyInfo.cs ├── Common └── GlobalArgs.cs ├── Filters ├── IFileSearchFilter.cs ├── IgnoreEmptyFileFilter.cs ├── IgnoreExtensionFilter.cs ├── IgnoreHiddenFileFilter.cs ├── IgnoreSmallFileFilter.cs ├── OnlyExtensionFilter.cs └── OnlyFileNameFilter.cs ├── FindDuplicateFiles.csproj ├── Images ├── ThemeDark.png ├── ThemeGreen.png ├── Themes │ ├── Dark │ │ ├── about.png │ │ ├── available_updates.png │ │ ├── c_sharp_logo.png │ │ ├── close.png │ │ ├── delete.png │ │ ├── delete_bin.png │ │ ├── download.png │ │ ├── filter.png │ │ ├── github.png │ │ ├── home_page.png │ │ ├── horizontal_line.png │ │ ├── loader.png │ │ ├── maximize.png │ │ ├── multiple_choice.png │ │ ├── package_search.png │ │ ├── reset.png │ │ ├── restore.png │ │ ├── search.png │ │ └── stop.png │ └── Green │ │ ├── about.png │ │ ├── available_updates.png │ │ ├── c_sharp_logo.png │ │ ├── close.png │ │ ├── delete.png │ │ ├── delete_bin.png │ │ ├── download.png │ │ ├── filter.png │ │ ├── github.png │ │ ├── home_page.png │ │ ├── horizontal_line.png │ │ ├── loader.png │ │ ├── maximize.png │ │ ├── multiple_choice.png │ │ ├── package_search.png │ │ ├── reset.png │ │ ├── restore.png │ │ ├── search.png │ │ └── stop.png ├── icon.ico └── icon.png ├── MainWindow.xaml ├── MainWindow.xaml.cs ├── Model ├── AppConfigInfo.cs ├── SearchConfigs.cs ├── SimpleFileInfo.cs ├── UpdateAppInfo.cs └── UpdateConfigInfo.cs ├── Resource.Designer.cs ├── Resource.resx ├── SearchFile ├── CheckDuplicateQueue.cs └── SearchFilesJob.cs ├── Styles ├── AboutWindowStyle.xaml └── MainWindowStyle.xaml ├── Themes ├── ThemeDark.xaml └── ThemeGreen.xaml ├── ViewModel ├── AboutWindowViewModel.cs ├── DuplicateFileModel.cs ├── MainWindowViewModel.cs ├── SearchDirectoryModel.cs └── ViewModelBase.cs ├── icon.ico └── 首次运行请看这里!!!.txt /.github/ISSUE_TEMPLATE/bug-report.yml: -------------------------------------------------------------------------------- 1 | name: 报告错误 2 | description: 报告一个错误 3 | labels: [错误] 4 | assignees: [JiuLing-zhang] 5 | body: 6 | - type: textarea 7 | id: description 8 | attributes: 9 | label: 问题描述 10 | description: 请简短描述下你所遇到的错误。 11 | placeholder: 问题描述 12 | validations: 13 | required: true 14 | - type: textarea 15 | id: repro-steps 16 | attributes: 17 | label: 重现步骤 18 | description: 请描述问题出现的操作步骤。 19 | placeholder: | 20 | 1. 点击"按钮A" 21 | 2. 点击"按钮B" 22 | 3. 出现错误 23 | validations: 24 | required: true 25 | - type: textarea 26 | id: remark 27 | attributes: 28 | label: 问题补充 29 | description: 如果可以,请补充提供下错误截图等。 30 | placeholder: 问题补充 31 | - type: input 32 | id: os 33 | attributes: 34 | label: 操作系统 35 | description: 请提供错误出现的操作系统。 36 | placeholder: 例如 Windows 10、Windows 11 37 | validations: 38 | required: true 39 | - type: dropdown 40 | id: version 41 | attributes: 42 | label: 程序版本 43 | description: 错误出现在哪个版本中。 44 | options: 45 | - 1.7.5 46 | - 1.7.4 47 | - 1.7.3 48 | - 1.7.2 49 | - 更早 50 | validations: 51 | required: true 52 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.yml: -------------------------------------------------------------------------------- 1 | name: 功能建议 2 | description: 提议一个好的想法,帮我们完善程序 3 | labels: [功能建议] 4 | assignees: [JiuLing-zhang] 5 | body: 6 | - type: textarea 7 | id: description 8 | attributes: 9 | label: 功能描述 10 | description: 用简短的话描述下功能建议。 11 | placeholder: 功能描述 12 | validations: 13 | required: true 14 | - type: textarea 15 | id: solution 16 | attributes: 17 | label: 解决方案 18 | description: 请描述下你理想的解决方案是什么。 19 | placeholder: 解决方案 20 | validations: 21 | required: true 22 | - type: textarea 23 | id: remark 24 | attributes: 25 | label: 补充说明 26 | description: 如果还有其它需要补充的,请说明。 27 | placeholder: 补充说明 28 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release-job 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*.*.*" 7 | 8 | jobs: 9 | build: 10 | runs-on: windows-latest 11 | 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v2 15 | 16 | - name: Setup .NET 17 | uses: actions/setup-dotnet@v1 18 | with: 19 | dotnet-version: 7.0.x 20 | 21 | - name: Restore dependencies 22 | run: dotnet restore 23 | 24 | - name: Build 25 | run: dotnet build --no-restore -c release 26 | 27 | - name: Get version 28 | uses: olegtarasov/get-tag@v2.1 29 | id: tagName 30 | 31 | - name: Create zip 32 | shell: pwsh 33 | # 配置【编译后的文件地址】 34 | run: Compress-Archive -Path ${{github.workspace}}\src\bin\Release\net7.0-windows\* -DestinationPath ${{github.workspace}}\src\bin\Release\FindDuplicateFiles_${{ steps.tagName.outputs.tag }}.zip 35 | 36 | - name: Release 37 | uses: softprops/action-gh-release@v0.1.13 38 | if: startsWith(github.ref, 'refs/tags/') 39 | with: 40 | # 配置【README地址】 41 | body: | 42 | Release为自动生成,具体内容请查看提交日志,或直接查看[README](https://github.com/JiuLing-zhang/FindDuplicateFiles#readme) 43 | # 配置【zip文件地址】 44 | files: ${{github.workspace}}\src\bin\Release\FindDuplicateFiles_${{ steps.tagName.outputs.tag }}.zip 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build and Object Folders 2 | bin/ 3 | obj/ 4 | 5 | # Nuget packages directory 6 | packages/ 7 | 8 | ## Ignore Visual Studio temporary files, build results, and 9 | ## files generated by popular Visual Studio add-ons. 10 | 11 | # User-specific files 12 | *.suo 13 | *.user 14 | *.sln.docstates 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Rr]elease/ 19 | x64/ 20 | *_i.c 21 | *_p.c 22 | *.ilk 23 | *.meta 24 | *.obj 25 | *.pch 26 | *.pdb 27 | *.pgc 28 | *.pgd 29 | *.rsp 30 | *.sbr 31 | *.tlb 32 | *.tli 33 | *.tlh 34 | *.tmp 35 | *.log 36 | *.vspscc 37 | *.vssscc 38 | .builds 39 | 40 | # Visual C++ cache files 41 | ipch/ 42 | *.aps 43 | *.ncb 44 | *.opensdf 45 | *.sdf 46 | 47 | # Visual Studio profiler 48 | *.psess 49 | *.vsp 50 | *.vspx 51 | 52 | # Guidance Automation Toolkit 53 | *.gpState 54 | 55 | # ReSharper is a .NET coding add-in 56 | _ReSharper* 57 | 58 | # NCrunch 59 | *.ncrunch* 60 | .*crunch*.local.xml 61 | 62 | # Installshield output folder 63 | [Ee]xpress 64 | 65 | # DocProject is a documentation generator add-in 66 | DocProject/buildhelp/ 67 | DocProject/Help 68 | UpgradeLog*.XML 69 | 70 | # Lightswitch 71 | _Pvt_Extensions 72 | GeneratedArtifacts 73 | *.xap 74 | ModelManifest.xml 75 | 76 | #Backup file 77 | *.bak 78 | .vs 79 | -------------------------------------------------------------------------------- /FindDuplicateFiles.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.31410.223 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FindDuplicateFiles", "src\FindDuplicateFiles.csproj", "{22729FF4-61AB-418C-BBD3-FD005204270B}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {22729FF4-61AB-418C-BBD3-FD005204270B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {22729FF4-61AB-418C-BBD3-FD005204270B}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {22729FF4-61AB-418C-BBD3-FD005204270B}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {22729FF4-61AB-418C-BBD3-FD005204270B}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {8B3B1DD1-C688-46AB-8C27-867B5845453D} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 九零 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 |

解放你的硬盘空间,红盘用户的福音

5 |
6 | 7 | ![](https://img.shields.io/badge/build-passing-brightgreen) 8 | ![](https://img.shields.io/github/license/JiuLing-zhang/FindDuplicateFiles) 9 | ![](https://img.shields.io/github/v/release/JiuLing-zhang/FindDuplicateFiles) 10 | 11 |
12 | 13 | ![demo1.png](https://github.com/JiuLing-zhang/FindDuplicateFiles/raw/main/docs/resources/images/demo1.png) 14 | 15 | ## 使用说明 16 | * 选项 - 启用图片文件预览 17 | 启用时,在搜索结果中点击图片文件,右下角会出现图片预览结果。 18 | 19 | * 选项 - 删除时移除空目录 20 | 删除文件的时候,如果文件夹中只剩余当前文件,删除的时候会顺便删除文件夹。 21 | 22 | * 结果区域 - 一键选中重复文件 23 | 自动选中所有重复文件(每个文件只保留一份)。 24 | 25 | * 结果区域 - 自定义选择重复文件 26 | 弹出筛选窗口,用户可以根据查找路径选择本次要删除的目标文件夹。 27 | 28 | * 其它 29 | 其它选项都比较通俗易懂,还请同学自行摸索。 30 | 31 | ![demo2.png](https://github.com/JiuLing-zhang/FindDuplicateFiles/raw/main/docs/resources/images/demo2.png) 32 | 33 | ## License 34 | MIT License -------------------------------------------------------------------------------- /docs/resources/images/demo1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiuLing-zhang/FindDuplicateFiles/510fd287ee0538f2bf3f6c25d215ac1abc119dcf/docs/resources/images/demo1.png -------------------------------------------------------------------------------- /docs/resources/images/demo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiuLing-zhang/FindDuplicateFiles/510fd287ee0538f2bf3f6c25d215ac1abc119dcf/docs/resources/images/demo2.png -------------------------------------------------------------------------------- /docs/resources/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiuLing-zhang/FindDuplicateFiles/510fd287ee0538f2bf3f6c25d215ac1abc119dcf/docs/resources/images/icon.png -------------------------------------------------------------------------------- /docs/resources/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiuLing-zhang/FindDuplicateFiles/510fd287ee0538f2bf3f6c25d215ac1abc119dcf/docs/resources/images/logo.png -------------------------------------------------------------------------------- /src/AboutWindow.xaml: -------------------------------------------------------------------------------- 1 |  17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 重复文件查找 38 | 39 | 42 | 43 | 44 | 45 | 46 | 47 | 作者:九零 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /src/AboutWindow.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Windows; 4 | using System.Windows.Navigation; 5 | using FindDuplicateFiles.Common; 6 | using FindDuplicateFiles.ViewModel; 7 | 8 | namespace FindDuplicateFiles 9 | { 10 | /// 11 | /// AboutWindow.xaml 的交互逻辑 12 | /// 13 | public partial class AboutWindow : Window 14 | { 15 | private readonly AboutWindowViewModel _myModel = new(); 16 | public AboutWindow() 17 | { 18 | InitializeComponent(); 19 | DataContext = _myModel; 20 | _myModel.Version = $"版本:{GlobalArgs.AppVersion}"; 21 | _myModel.DownloadUrl = "https://github.com/JiuLing-zhang"; 22 | } 23 | private void Window_MouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e) 24 | { 25 | this.DragMove(); 26 | } 27 | 28 | private void GoWebsite_RequestNavigate(object sender, RequestNavigateEventArgs e) 29 | { 30 | OpenUrl(e.Uri.AbsoluteUri); 31 | e.Handled = true; 32 | } 33 | 34 | private void BtnCheckUpdate_Click(object sender, RoutedEventArgs e) 35 | { 36 | var app = JiuLing.AutoUpgrade.Shell.AutoUpgradeFactory.Create(); 37 | app.SetUpgrade(x => 38 | { 39 | x.IsCheckSign = true; 40 | }); 41 | app.UseHttpMode(Resource.AutoUpgradePath).Run(); 42 | } 43 | private static void OpenUrl(string url) 44 | { 45 | using var compiler = new Process(); 46 | compiler.StartInfo.FileName = url; 47 | compiler.StartInfo.UseShellExecute = true; 48 | compiler.Start(); 49 | } 50 | private void BtnClose_Click(object sender, RoutedEventArgs e) 51 | { 52 | this.Close(); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/App.xaml: -------------------------------------------------------------------------------- 1 |  6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/App.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | 3 | namespace FindDuplicateFiles 4 | { 5 | /// 6 | /// Interaction logic for App.xaml 7 | /// 8 | public partial class App : Application 9 | { 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | 3 | [assembly: ThemeInfo( 4 | ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located 5 | //(used if a resource is not found in the page, 6 | // or application resource dictionaries) 7 | ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located 8 | //(used if a resource is not found in the page, 9 | // app, or any theme specific resource dictionaries) 10 | )] 11 | -------------------------------------------------------------------------------- /src/Common/GlobalArgs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using FindDuplicateFiles.Model; 4 | 5 | namespace FindDuplicateFiles.Common 6 | { 7 | public class GlobalArgs 8 | { 9 | /// 10 | /// App名称 11 | /// 12 | public static string AppName { get; set; } = AppDomain.CurrentDomain.FriendlyName; 13 | 14 | /// 15 | /// App Data文件夹路径 16 | /// 17 | private static readonly string AppDataPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); 18 | /// 19 | /// 配置文件路径 20 | /// 21 | public static string AppConfigPath { get; set; } = Path.Combine(AppDataPath, AppName, "config.json"); 22 | 23 | public static AppConfigInfo AppConfig { get; set; } 24 | public static string AppVersion { get; set; } = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version?.ToString(); 25 | 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Filters/IFileSearchFilter.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | 4 | namespace FindDuplicateFiles.Filters 5 | { 6 | /// 7 | /// 文件过滤器 8 | /// 9 | interface IFileSearchFilter 10 | { 11 | /// 12 | /// 根据条件过滤 13 | /// 14 | /// 文件列表 15 | /// 符合条件的文件列表 16 | public List FilterByCondition(List files); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Filters/IgnoreEmptyFileFilter.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Linq; 4 | 5 | namespace FindDuplicateFiles.Filters 6 | { 7 | /// 8 | /// 空文件过滤器 9 | /// 10 | public class IgnoreEmptyFileFilter : IFileSearchFilter 11 | { 12 | public List FilterByCondition(List files) 13 | { 14 | return files.Where(x => x.Length != 0).ToList(); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Filters/IgnoreExtensionFilter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace FindDuplicateFiles.Filters 9 | { 10 | /// 11 | /// 忽略扩展名过滤器 12 | /// 13 | public class IgnoreExtensionFilter : IFileSearchFilter 14 | { 15 | private readonly List _extensions; 16 | public IgnoreExtensionFilter(List extensions) 17 | { 18 | _extensions = extensions; 19 | } 20 | public List FilterByCondition(List files) 21 | { 22 | return files.Where(x => _extensions.Contains(x.Extension.ToLower()) == false).ToList(); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Filters/IgnoreHiddenFileFilter.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Linq; 4 | 5 | namespace FindDuplicateFiles.Filters 6 | { 7 | /// 8 | /// 隐藏文件过滤器 9 | /// 10 | public class IgnoreHiddenFileFilter : IFileSearchFilter 11 | { 12 | public List FilterByCondition(List files) 13 | { 14 | return files.Where(x => x.Attributes != FileAttributes.Hidden).ToList(); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Filters/IgnoreSmallFileFilter.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Linq; 4 | 5 | namespace FindDuplicateFiles.Filters 6 | { 7 | /// 8 | /// 小文件过滤器 9 | /// 10 | public class IgnoreSmallFileFilter : IFileSearchFilter 11 | { 12 | private readonly decimal _minLength; 13 | /// 14 | /// 初始化过滤器 15 | /// 16 | /// 最小的文件大小(KB) 17 | public IgnoreSmallFileFilter(decimal minLength) 18 | { 19 | _minLength = minLength; 20 | } 21 | public List FilterByCondition(List files) 22 | { 23 | return files.Where(x => x.Length / 1024m >= _minLength).ToList(); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Filters/OnlyExtensionFilter.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Linq; 4 | 5 | namespace FindDuplicateFiles.Filters 6 | { 7 | /// 8 | /// 指定扩展名过滤器 9 | /// 10 | public class OnlyExtensionFilter : IFileSearchFilter 11 | { 12 | private readonly List _extensions; 13 | /// 14 | /// 初始化过滤器 15 | /// 16 | /// 指定的扩展名列表 17 | public OnlyExtensionFilter(List extensions) 18 | { 19 | _extensions = extensions; 20 | } 21 | public List FilterByCondition(List files) 22 | { 23 | return files.Where(x => _extensions.Contains(x.Extension.ToLower())).ToList(); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Filters/OnlyFileNameFilter.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Linq; 4 | 5 | namespace FindDuplicateFiles.Filters 6 | { 7 | /// 8 | /// 指定文件名过滤器 9 | /// 10 | internal class OnlyFileNameFilter : IFileSearchFilter 11 | { 12 | private readonly List _fileNames; 13 | /// 14 | /// 初始化过滤器 15 | /// 16 | /// 指定的文件名列表 17 | public OnlyFileNameFilter(List fileNames) 18 | { 19 | _fileNames = fileNames; 20 | } 21 | public List FilterByCondition(List files) 22 | { 23 | return files.Where(x => _fileNames.Contains(x.Name.ToLower())).ToList(); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/FindDuplicateFiles.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | WinExe 5 | net7.0-windows 6 | true 7 | true 8 | icon.ico 9 | 10 | 九零 11 | 重复文件查找 12 | 1.7.9.4 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | PreserveNewest 80 | 81 | 82 | PreserveNewest 83 | 84 | 85 | PreserveNewest 86 | 87 | 88 | PreserveNewest 89 | 90 | 91 | PreserveNewest 92 | 93 | 94 | PreserveNewest 95 | 96 | 97 | PreserveNewest 98 | 99 | 100 | PreserveNewest 101 | 102 | 103 | PreserveNewest 104 | 105 | 106 | PreserveNewest 107 | 108 | 109 | PreserveNewest 110 | 111 | 112 | PreserveNewest 113 | 114 | 115 | PreserveNewest 116 | 117 | 118 | PreserveNewest 119 | 120 | 121 | PreserveNewest 122 | 123 | 124 | PreserveNewest 125 | 126 | 127 | PreserveNewest 128 | 129 | 130 | PreserveNewest 131 | 132 | 133 | PreserveNewest 134 | 135 | 136 | PreserveNewest 137 | 138 | 139 | PreserveNewest 140 | 141 | 142 | PreserveNewest 143 | 144 | 145 | PreserveNewest 146 | 147 | 148 | PreserveNewest 149 | 150 | 151 | PreserveNewest 152 | 153 | 154 | PreserveNewest 155 | 156 | 157 | PreserveNewest 158 | 159 | 160 | PreserveNewest 161 | 162 | 163 | PreserveNewest 164 | 165 | 166 | PreserveNewest 167 | 168 | 169 | PreserveNewest 170 | 171 | 172 | PreserveNewest 173 | 174 | 175 | PreserveNewest 176 | 177 | 178 | PreserveNewest 179 | 180 | 181 | PreserveNewest 182 | 183 | 184 | PreserveNewest 185 | 186 | 187 | PreserveNewest 188 | 189 | 190 | PreserveNewest 191 | 192 | 193 | PreserveNewest 194 | 195 | 196 | PreserveNewest 197 | 198 | 199 | PreserveNewest 200 | 201 | 202 | PreserveNewest 203 | 204 | 205 | 206 | 207 | 208 | PreserveNewest 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | True 220 | True 221 | Resource.resx 222 | 223 | 224 | 225 | 226 | 227 | ResXFileCodeGenerator 228 | Resource.Designer.cs 229 | 230 | 231 | 232 | 233 | -------------------------------------------------------------------------------- /src/Images/ThemeDark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiuLing-zhang/FindDuplicateFiles/510fd287ee0538f2bf3f6c25d215ac1abc119dcf/src/Images/ThemeDark.png -------------------------------------------------------------------------------- /src/Images/ThemeGreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiuLing-zhang/FindDuplicateFiles/510fd287ee0538f2bf3f6c25d215ac1abc119dcf/src/Images/ThemeGreen.png -------------------------------------------------------------------------------- /src/Images/Themes/Dark/about.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiuLing-zhang/FindDuplicateFiles/510fd287ee0538f2bf3f6c25d215ac1abc119dcf/src/Images/Themes/Dark/about.png -------------------------------------------------------------------------------- /src/Images/Themes/Dark/available_updates.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiuLing-zhang/FindDuplicateFiles/510fd287ee0538f2bf3f6c25d215ac1abc119dcf/src/Images/Themes/Dark/available_updates.png -------------------------------------------------------------------------------- /src/Images/Themes/Dark/c_sharp_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiuLing-zhang/FindDuplicateFiles/510fd287ee0538f2bf3f6c25d215ac1abc119dcf/src/Images/Themes/Dark/c_sharp_logo.png -------------------------------------------------------------------------------- /src/Images/Themes/Dark/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiuLing-zhang/FindDuplicateFiles/510fd287ee0538f2bf3f6c25d215ac1abc119dcf/src/Images/Themes/Dark/close.png -------------------------------------------------------------------------------- /src/Images/Themes/Dark/delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiuLing-zhang/FindDuplicateFiles/510fd287ee0538f2bf3f6c25d215ac1abc119dcf/src/Images/Themes/Dark/delete.png -------------------------------------------------------------------------------- /src/Images/Themes/Dark/delete_bin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiuLing-zhang/FindDuplicateFiles/510fd287ee0538f2bf3f6c25d215ac1abc119dcf/src/Images/Themes/Dark/delete_bin.png -------------------------------------------------------------------------------- /src/Images/Themes/Dark/download.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiuLing-zhang/FindDuplicateFiles/510fd287ee0538f2bf3f6c25d215ac1abc119dcf/src/Images/Themes/Dark/download.png -------------------------------------------------------------------------------- /src/Images/Themes/Dark/filter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiuLing-zhang/FindDuplicateFiles/510fd287ee0538f2bf3f6c25d215ac1abc119dcf/src/Images/Themes/Dark/filter.png -------------------------------------------------------------------------------- /src/Images/Themes/Dark/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiuLing-zhang/FindDuplicateFiles/510fd287ee0538f2bf3f6c25d215ac1abc119dcf/src/Images/Themes/Dark/github.png -------------------------------------------------------------------------------- /src/Images/Themes/Dark/home_page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiuLing-zhang/FindDuplicateFiles/510fd287ee0538f2bf3f6c25d215ac1abc119dcf/src/Images/Themes/Dark/home_page.png -------------------------------------------------------------------------------- /src/Images/Themes/Dark/horizontal_line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiuLing-zhang/FindDuplicateFiles/510fd287ee0538f2bf3f6c25d215ac1abc119dcf/src/Images/Themes/Dark/horizontal_line.png -------------------------------------------------------------------------------- /src/Images/Themes/Dark/loader.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiuLing-zhang/FindDuplicateFiles/510fd287ee0538f2bf3f6c25d215ac1abc119dcf/src/Images/Themes/Dark/loader.png -------------------------------------------------------------------------------- /src/Images/Themes/Dark/maximize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiuLing-zhang/FindDuplicateFiles/510fd287ee0538f2bf3f6c25d215ac1abc119dcf/src/Images/Themes/Dark/maximize.png -------------------------------------------------------------------------------- /src/Images/Themes/Dark/multiple_choice.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiuLing-zhang/FindDuplicateFiles/510fd287ee0538f2bf3f6c25d215ac1abc119dcf/src/Images/Themes/Dark/multiple_choice.png -------------------------------------------------------------------------------- /src/Images/Themes/Dark/package_search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiuLing-zhang/FindDuplicateFiles/510fd287ee0538f2bf3f6c25d215ac1abc119dcf/src/Images/Themes/Dark/package_search.png -------------------------------------------------------------------------------- /src/Images/Themes/Dark/reset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiuLing-zhang/FindDuplicateFiles/510fd287ee0538f2bf3f6c25d215ac1abc119dcf/src/Images/Themes/Dark/reset.png -------------------------------------------------------------------------------- /src/Images/Themes/Dark/restore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiuLing-zhang/FindDuplicateFiles/510fd287ee0538f2bf3f6c25d215ac1abc119dcf/src/Images/Themes/Dark/restore.png -------------------------------------------------------------------------------- /src/Images/Themes/Dark/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiuLing-zhang/FindDuplicateFiles/510fd287ee0538f2bf3f6c25d215ac1abc119dcf/src/Images/Themes/Dark/search.png -------------------------------------------------------------------------------- /src/Images/Themes/Dark/stop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiuLing-zhang/FindDuplicateFiles/510fd287ee0538f2bf3f6c25d215ac1abc119dcf/src/Images/Themes/Dark/stop.png -------------------------------------------------------------------------------- /src/Images/Themes/Green/about.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiuLing-zhang/FindDuplicateFiles/510fd287ee0538f2bf3f6c25d215ac1abc119dcf/src/Images/Themes/Green/about.png -------------------------------------------------------------------------------- /src/Images/Themes/Green/available_updates.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiuLing-zhang/FindDuplicateFiles/510fd287ee0538f2bf3f6c25d215ac1abc119dcf/src/Images/Themes/Green/available_updates.png -------------------------------------------------------------------------------- /src/Images/Themes/Green/c_sharp_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiuLing-zhang/FindDuplicateFiles/510fd287ee0538f2bf3f6c25d215ac1abc119dcf/src/Images/Themes/Green/c_sharp_logo.png -------------------------------------------------------------------------------- /src/Images/Themes/Green/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiuLing-zhang/FindDuplicateFiles/510fd287ee0538f2bf3f6c25d215ac1abc119dcf/src/Images/Themes/Green/close.png -------------------------------------------------------------------------------- /src/Images/Themes/Green/delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiuLing-zhang/FindDuplicateFiles/510fd287ee0538f2bf3f6c25d215ac1abc119dcf/src/Images/Themes/Green/delete.png -------------------------------------------------------------------------------- /src/Images/Themes/Green/delete_bin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiuLing-zhang/FindDuplicateFiles/510fd287ee0538f2bf3f6c25d215ac1abc119dcf/src/Images/Themes/Green/delete_bin.png -------------------------------------------------------------------------------- /src/Images/Themes/Green/download.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiuLing-zhang/FindDuplicateFiles/510fd287ee0538f2bf3f6c25d215ac1abc119dcf/src/Images/Themes/Green/download.png -------------------------------------------------------------------------------- /src/Images/Themes/Green/filter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiuLing-zhang/FindDuplicateFiles/510fd287ee0538f2bf3f6c25d215ac1abc119dcf/src/Images/Themes/Green/filter.png -------------------------------------------------------------------------------- /src/Images/Themes/Green/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiuLing-zhang/FindDuplicateFiles/510fd287ee0538f2bf3f6c25d215ac1abc119dcf/src/Images/Themes/Green/github.png -------------------------------------------------------------------------------- /src/Images/Themes/Green/home_page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiuLing-zhang/FindDuplicateFiles/510fd287ee0538f2bf3f6c25d215ac1abc119dcf/src/Images/Themes/Green/home_page.png -------------------------------------------------------------------------------- /src/Images/Themes/Green/horizontal_line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiuLing-zhang/FindDuplicateFiles/510fd287ee0538f2bf3f6c25d215ac1abc119dcf/src/Images/Themes/Green/horizontal_line.png -------------------------------------------------------------------------------- /src/Images/Themes/Green/loader.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiuLing-zhang/FindDuplicateFiles/510fd287ee0538f2bf3f6c25d215ac1abc119dcf/src/Images/Themes/Green/loader.png -------------------------------------------------------------------------------- /src/Images/Themes/Green/maximize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiuLing-zhang/FindDuplicateFiles/510fd287ee0538f2bf3f6c25d215ac1abc119dcf/src/Images/Themes/Green/maximize.png -------------------------------------------------------------------------------- /src/Images/Themes/Green/multiple_choice.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiuLing-zhang/FindDuplicateFiles/510fd287ee0538f2bf3f6c25d215ac1abc119dcf/src/Images/Themes/Green/multiple_choice.png -------------------------------------------------------------------------------- /src/Images/Themes/Green/package_search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiuLing-zhang/FindDuplicateFiles/510fd287ee0538f2bf3f6c25d215ac1abc119dcf/src/Images/Themes/Green/package_search.png -------------------------------------------------------------------------------- /src/Images/Themes/Green/reset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiuLing-zhang/FindDuplicateFiles/510fd287ee0538f2bf3f6c25d215ac1abc119dcf/src/Images/Themes/Green/reset.png -------------------------------------------------------------------------------- /src/Images/Themes/Green/restore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiuLing-zhang/FindDuplicateFiles/510fd287ee0538f2bf3f6c25d215ac1abc119dcf/src/Images/Themes/Green/restore.png -------------------------------------------------------------------------------- /src/Images/Themes/Green/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiuLing-zhang/FindDuplicateFiles/510fd287ee0538f2bf3f6c25d215ac1abc119dcf/src/Images/Themes/Green/search.png -------------------------------------------------------------------------------- /src/Images/Themes/Green/stop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiuLing-zhang/FindDuplicateFiles/510fd287ee0538f2bf3f6c25d215ac1abc119dcf/src/Images/Themes/Green/stop.png -------------------------------------------------------------------------------- /src/Images/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiuLing-zhang/FindDuplicateFiles/510fd287ee0538f2bf3f6c25d215ac1abc119dcf/src/Images/icon.ico -------------------------------------------------------------------------------- /src/Images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiuLing-zhang/FindDuplicateFiles/510fd287ee0538f2bf3f6c25d215ac1abc119dcf/src/Images/icon.png -------------------------------------------------------------------------------- /src/MainWindow.xaml: -------------------------------------------------------------------------------- 1 |  15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 57 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 69 | 70 | 71 | 72 | 73 | 74 | 77 | 80 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 匹配方式 110 | 文件名 111 | 文件大小 112 | 文件修改时间 113 | MD5(速度慢,准确性高) 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 选项 122 | 仅查找以下文件名(多个用|隔开) 129 | 134 | 忽略空文件 135 | 忽略隐藏文件 136 | 忽略系统文件 137 | 忽略小文件(小于1024KB) 138 | 查找所有文件 139 | 仅查找图片文件 140 | 仅查找媒体文件 141 | 仅查找文档文件 142 | 启用图片文件预览 143 | 删除时移除空目录 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 查找路径(这里也支持拖拽哦~~) 152 | 161 | 162 | 163 | 164 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 204 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 253 | 256 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 从下方选择要删除的文件所在路径 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 293 | 294 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | -------------------------------------------------------------------------------- /src/MainWindow.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Windows; 6 | using System.Windows.Controls; 7 | using System.Windows.Data; 8 | using System.Windows.Input; 9 | using System.Windows.Media; 10 | using System.Windows.Media.Animation; 11 | using System.Windows.Media.Imaging; 12 | using FindDuplicateFiles.Common; 13 | using FindDuplicateFiles.Model; 14 | using FindDuplicateFiles.SearchFile; 15 | using FindDuplicateFiles.ViewModel; 16 | using JiuLing.CommonLibs.ExtensionMethods; 17 | 18 | namespace FindDuplicateFiles 19 | { 20 | /// 21 | /// Interaction logic for MainWindow.xaml 22 | /// 23 | public partial class MainWindow : Window 24 | { 25 | /// 26 | /// 是否正在进行搜索 27 | /// 28 | private bool _isSearching; 29 | 30 | private readonly SearchFilesJob _searchFilesJob = new(); 31 | private readonly MainWindowViewModel _myModel = new(); 32 | public MainWindow() 33 | { 34 | InitializeComponent(); 35 | 36 | LoadingAppConfig(); 37 | LoadingTheme(GlobalArgs.AppConfig.Theme); 38 | 39 | BindingItemsSource(); 40 | BindingSearchEvent(); 41 | 42 | InitializeSearchCondition(); 43 | InitializeLoadingBlock(); 44 | } 45 | 46 | private void LoadingAppConfig() 47 | { 48 | if (!File.Exists(GlobalArgs.AppConfigPath)) 49 | { 50 | GlobalArgs.AppConfig = new AppConfigInfo(); 51 | } 52 | else 53 | { 54 | string json = File.ReadAllText(GlobalArgs.AppConfigPath); 55 | GlobalArgs.AppConfig = System.Text.Json.JsonSerializer.Deserialize(json); 56 | } 57 | //TODO 这里可以为配置文件加入版本号,有更新时才覆盖保存 58 | SaveAppConfig(); 59 | 60 | // string updateConfigPath = $"{GlobalArgs.AppPath}{GlobalArgs.UpdateConfigPath}"; 61 | // string updateConfigString = File.ReadAllText(updateConfigPath); 62 | // GlobalArgs.UpdateConfig = System.Text.Json.JsonSerializer.Deserialize(updateConfigString); 63 | } 64 | 65 | private void InitializeSearchCondition() 66 | { 67 | //匹配方式 68 | ChkFileName.IsChecked = true; 69 | ChkFileSize.IsChecked = true; 70 | ChkFileLastWriteTimeUtc.IsChecked = true; 71 | ChkMD5.IsChecked = false; 72 | 73 | //选项 74 | ChkOnlyFileName.IsChecked = false; 75 | ChkIgnoreEmptyFile.IsChecked = true; 76 | ChkIgnoreHiddenFile.IsChecked = true; 77 | ChkIgnoreSystemFile.IsChecked = true; 78 | ChkIgnoreSmallFile.IsChecked = false; 79 | RdoAllFile.IsChecked = true; 80 | ChkPreviewImage.IsChecked = false; 81 | ChkDeleteEmptyDirectory.IsChecked = false; 82 | 83 | _myModel.SearchDirectory.Clear(); 84 | } 85 | 86 | 87 | /// 88 | /// UI数据绑定 89 | /// 90 | private void BindingItemsSource() 91 | { 92 | ListBoxSearchFolders.ItemsSource = _myModel.SearchDirectory; 93 | ListBoxDirectoryFilter.ItemsSource = _myModel.SearchDirectory; 94 | ListViewDuplicateFile.ItemsSource = _myModel.DuplicateFiles; 95 | 96 | CollectionView view = (CollectionView)CollectionViewSource.GetDefaultView(ListViewDuplicateFile.ItemsSource); 97 | var groupDescription = new PropertyGroupDescription("Key"); 98 | view.GroupDescriptions?.Add(groupDescription); 99 | 100 | RdoOnlyImageFile.ToolTip = $"包括:{GlobalArgs.AppConfig.ImageExtension}文件"; 101 | RdoOnlyMediaFile.ToolTip = $"包括:{GlobalArgs.AppConfig.MediaExtension}文件"; 102 | RdoOnlyDocumentFile.ToolTip = $"包括:{GlobalArgs.AppConfig.DocumentExtension}文件"; 103 | ChkIgnoreSystemFile.ToolTip = $"包括:{GlobalArgs.AppConfig.SystemExtension}文件"; 104 | DataContext = _myModel; 105 | } 106 | 107 | /// 108 | /// 绑定查找任务的内部事件 109 | /// 110 | private void BindingSearchEvent() 111 | { 112 | _searchFilesJob.EventMessage = ExecutedMessage; 113 | _searchFilesJob.EventDuplicateFound = DuplicateFilesFound; 114 | _searchFilesJob.EventSearchFinished = SearchFinished; 115 | } 116 | private void InitializeLoadingBlock() 117 | { 118 | var da = new DoubleAnimation() 119 | { 120 | From = 0, 121 | To = 360, 122 | Duration = new Duration(TimeSpan.FromSeconds(3)), 123 | RepeatBehavior = RepeatBehavior.Forever 124 | }; 125 | var rt = new RotateTransform(); 126 | ImgLoading.RenderTransform = rt; 127 | rt.BeginAnimation(RotateTransform.AngleProperty, da); 128 | } 129 | 130 | private void ExecutedMessage(string message) 131 | { 132 | _myModel.JobMessage = message; 133 | } 134 | private void Window_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) 135 | { 136 | this.DragMove(); 137 | 138 | if (GridImage.Visibility == Visibility) 139 | { 140 | ImgPreview.Source = null; 141 | GridImage.Visibility = Visibility.Collapsed; 142 | } 143 | } 144 | 145 | private void BtnMinimize_Click(object sender, RoutedEventArgs e) 146 | { 147 | this.WindowState = WindowState.Minimized; 148 | } 149 | 150 | private void BtnMaximize_Click(object sender, RoutedEventArgs e) 151 | { 152 | if (this.WindowState == WindowState.Maximized) 153 | { 154 | this.WindowState = WindowState.Normal; 155 | this.ImgMaximize.Source = new BitmapImage(new Uri($"pack://application:,,,/Images/Themes/{GlobalArgs.AppConfig.Theme}/maximize.png")); 156 | } 157 | else 158 | { 159 | this.WindowState = WindowState.Maximized; 160 | this.ImgMaximize.Source = new BitmapImage(new Uri($"pack://application:,,,/Images/Themes/{GlobalArgs.AppConfig.Theme}/restore.png")); 161 | } 162 | } 163 | 164 | private void BtnClose_Click(object sender, RoutedEventArgs e) 165 | { 166 | this.Close(); 167 | } 168 | 169 | private void BtnAddSearchFolder_Click(object sender, RoutedEventArgs e) 170 | { 171 | var fbd = new System.Windows.Forms.FolderBrowserDialog(); 172 | fbd.ShowDialog(); 173 | var path = fbd.SelectedPath; 174 | if (path.IsEmpty()) 175 | { 176 | return; 177 | } 178 | 179 | AddSearchFolderOnly(path); 180 | } 181 | 182 | private void AddSearchFolderOnly(string path) 183 | { 184 | if (_myModel.SearchDirectory.Any(x => x.DirectoryName == path)) 185 | { 186 | return; 187 | } 188 | 189 | if (_myModel.SearchDirectory.Any(x => x.DirectoryName.IndexOf(path) == 0 || path.IndexOf(x.DirectoryName) == 0)) 190 | { 191 | MessageBox.Show("添加失败:查找路径不能相互包含", "重复文件查找", MessageBoxButton.OK, MessageBoxImage.Error); 192 | return; 193 | } 194 | 195 | _myModel.SearchDirectory.Add(new SearchDirectoryModel() 196 | { 197 | DirectoryName = path 198 | }); 199 | } 200 | 201 | private void BtnRemoveSearchFolder_Click(object sender, RoutedEventArgs e) 202 | { 203 | var tag = (sender as Button)?.Tag; 204 | if (tag == null) 205 | { 206 | return; 207 | } 208 | 209 | string path = tag.ToString(); 210 | var obj = _myModel.SearchDirectory.First(x => x.DirectoryName == path); 211 | _myModel.SearchDirectory.Remove(obj); 212 | } 213 | 214 | private void BtnSearch_Click(object sender, RoutedEventArgs e) 215 | { 216 | if (_isSearching) 217 | { 218 | if (MessageBox.Show("确定要停止搜索吗?", "重复文件查找", MessageBoxButton.OKCancel, MessageBoxImage.Warning) == MessageBoxResult.Cancel) 219 | { 220 | return; 221 | } 222 | EndSearch(); 223 | } 224 | else 225 | { 226 | BeginSearch(); 227 | } 228 | } 229 | 230 | private void BeginSearch() 231 | { 232 | if (_myModel.SearchDirectory.Count == 0) 233 | { 234 | MessageBox.Show("请选择要查找的文件夹", "重复文件查找", MessageBoxButton.OK, MessageBoxImage.Stop); 235 | return; 236 | } 237 | 238 | //匹配方式校验 239 | SearchMatchEnum searchMatch = 0; 240 | if (ChkFileName.IsChecked == true) 241 | { 242 | searchMatch |= SearchMatchEnum.Name; 243 | } 244 | if (ChkFileSize.IsChecked == true) 245 | { 246 | searchMatch |= SearchMatchEnum.Size; 247 | } 248 | if (ChkFileLastWriteTimeUtc.IsChecked == true) 249 | { 250 | searchMatch |= SearchMatchEnum.LastWriteTime; 251 | } 252 | if (ChkMD5.IsChecked == true) 253 | { 254 | searchMatch |= SearchMatchEnum.MD5; 255 | } 256 | if (searchMatch == 0) 257 | { 258 | MessageBox.Show("请选择匹配方式", "重复文件查找", MessageBoxButton.OK, MessageBoxImage.Stop); 259 | return; 260 | } 261 | 262 | //选项 263 | SearchOptionEnum searchOption = 0; 264 | var searchOptionData = new SearchOptionData(); 265 | if (ChkIgnoreEmptyFile.IsChecked == true) 266 | { 267 | searchOption |= SearchOptionEnum.IgnoreEmptyFile; 268 | } 269 | if (ChkIgnoreHiddenFile.IsChecked == true) 270 | { 271 | searchOption |= SearchOptionEnum.IgnoreHiddenFile; 272 | } 273 | if (ChkIgnoreSystemFile.IsChecked == true) 274 | { 275 | searchOption |= SearchOptionEnum.IgnoreSystemFile; 276 | } 277 | if (ChkIgnoreSmallFile.IsChecked == true) 278 | { 279 | searchOption |= SearchOptionEnum.IgnoreSmallFile; 280 | } 281 | if (RdoOnlyImageFile.IsChecked == true) 282 | { 283 | searchOption |= SearchOptionEnum.OnlyImageFile; 284 | } 285 | if (RdoOnlyMediaFile.IsChecked == true) 286 | { 287 | searchOption |= SearchOptionEnum.OnlyMediaFile; 288 | } 289 | if (RdoOnlyDocumentFile.IsChecked == true) 290 | { 291 | searchOption |= SearchOptionEnum.OnlyDocumentFile; 292 | } 293 | if (ChkOnlyFileName.IsChecked == true) 294 | { 295 | var onlyFileNames = TxtOnlyFileNames.Text.Trim(); 296 | if (onlyFileNames.IsEmpty()) 297 | { 298 | MessageBox.Show("请输入要查找的文件名", "重复文件查找", MessageBoxButton.OK, MessageBoxImage.Stop); 299 | TxtOnlyFileNames.Focus(); 300 | return; 301 | } 302 | searchOptionData.OnlyFileNames = new List(); 303 | searchOption |= SearchOptionEnum.OnlyFileName; 304 | foreach (var onlyFileName in onlyFileNames.Split('|')) 305 | { 306 | searchOptionData.OnlyFileNames.Add(onlyFileName.Trim().ToLower()); 307 | } 308 | } 309 | SetBeginSearchStyle(); 310 | _myModel.DuplicateFiles.Clear(); 311 | var config = new SearchConfigs() 312 | { 313 | Folders = new List(_myModel.SearchDirectory.Select(x => x.DirectoryName).ToList()), 314 | SearchMatch = searchMatch, 315 | SearchOption = searchOption, 316 | SearchOptionData = searchOptionData 317 | }; 318 | _searchFilesJob.Start(config); 319 | } 320 | 321 | /// 322 | /// 开始搜索 323 | /// 324 | private void SetBeginSearchStyle() 325 | { 326 | TxtSearch.Text = "停止"; 327 | ImgSearch.Source = new BitmapImage(new Uri($"pack://application:,,,/Images/Themes/{GlobalArgs.AppConfig.Theme}/stop.png")); 328 | _isSearching = true; 329 | _myModel.IsShowLoading = _isSearching; 330 | } 331 | 332 | private void EndSearch() 333 | { 334 | SetEndSearchStyle(); 335 | _searchFilesJob.Stop(); 336 | } 337 | 338 | /// 339 | /// 结束搜索 340 | /// 341 | private void SetEndSearchStyle() 342 | { 343 | TxtSearch.Text = "查找"; 344 | ImgSearch.Source = new BitmapImage(new Uri($"pack://application:,,,/Images/Themes/{GlobalArgs.AppConfig.Theme}/search.png")); 345 | _isSearching = false; 346 | _myModel.IsShowLoading = _isSearching; 347 | } 348 | 349 | private void BtnReset_Click(object sender, RoutedEventArgs e) 350 | { 351 | if (_isSearching) 352 | { 353 | MessageBox.Show("任务执行中,禁止重置", "重复文件查找", MessageBoxButton.OK, MessageBoxImage.Stop); 354 | return; 355 | } 356 | InitializeSearchCondition(); 357 | } 358 | 359 | private void ChangeTheme_Click(object sender, RoutedEventArgs e) 360 | { 361 | if (_isSearching) 362 | { 363 | MessageBox.Show("任务执行中不允许换肤", "重复文件查找", MessageBoxButton.OK, MessageBoxImage.Stop); 364 | return; 365 | } 366 | 367 | var tag = (sender as Button)?.Tag; 368 | if (tag == null) 369 | { 370 | MessageBox.Show("修改主题失败:系统错误", "重复文件查找", MessageBoxButton.OK, MessageBoxImage.Stop); 371 | return; 372 | } 373 | 374 | string theme = tag.ToString(); 375 | GlobalArgs.AppConfig.Theme = theme; 376 | 377 | SaveAppConfig(); 378 | 379 | LoadingTheme(theme); 380 | } 381 | 382 | private void LoadingTheme(string themeName) 383 | { 384 | try 385 | { 386 | var mResourceSkin = new ResourceDictionary() 387 | { 388 | Source = new Uri($"/Themes/Theme{themeName}.xaml", UriKind.RelativeOrAbsolute) 389 | }; 390 | Application.Current.Resources.MergedDictionaries[0] = mResourceSkin; 391 | 392 | ImgSearch.Source = new BitmapImage(new Uri($"pack://application:,,,/Images/Themes/{GlobalArgs.AppConfig.Theme}/search.png")); 393 | ImgReset.Source = new BitmapImage(new Uri($"pack://application:,,,/Images/Themes/{GlobalArgs.AppConfig.Theme}/reset.png")); 394 | ImgFilter.Source = new BitmapImage(new Uri($"pack://application:,,,/Images/Themes/{GlobalArgs.AppConfig.Theme}/filter.png")); 395 | ImgMultipleChoice.Source = new BitmapImage(new Uri($"pack://application:,,,/Images/Themes/{GlobalArgs.AppConfig.Theme}/multiple_choice.png")); 396 | ImgDeleteBin.Source = new BitmapImage(new Uri($"pack://application:,,,/Images/Themes/{GlobalArgs.AppConfig.Theme}/delete_bin.png")); 397 | ImgLoading.Source = new BitmapImage(new Uri($"pack://application:,,,/Images/Themes/{GlobalArgs.AppConfig.Theme}/loader.png")); 398 | } 399 | catch (IOException ex) 400 | { 401 | MessageBox.Show($"主题加载失败,{ex.Message}", "重复文件查找", MessageBoxButton.OK, MessageBoxImage.Error); 402 | } 403 | } 404 | 405 | private void DuplicateFilesFound(string key, SimpleFileInfo simpleFile) 406 | { 407 | Application.Current.Dispatcher.Invoke(() => 408 | { 409 | _myModel.DuplicateFiles.Add(new DuplicateFileModel() 410 | { 411 | Key = key, 412 | Name = simpleFile.Name, 413 | Path = simpleFile.Path, 414 | Size = Math.Ceiling(simpleFile.Size / 1024), 415 | LastWriteTime = simpleFile.LastWriteTime, 416 | Extension = simpleFile.Extension, 417 | IsCheckedFile = false 418 | }); 419 | }); 420 | } 421 | private void SearchFinished() 422 | { 423 | Application.Current.Dispatcher.Invoke(() => 424 | { 425 | SetEndSearchStyle(); 426 | MessageBox.Show("查找完成", "重复文件查找", MessageBoxButton.OK, MessageBoxImage.Information); 427 | }); 428 | } 429 | 430 | private void ListViewDuplicateFile_SelectionChanged(object sender, SelectionChangedEventArgs e) 431 | { 432 | if (ChkPreviewImage.IsChecked == false) 433 | { 434 | return; 435 | } 436 | 437 | var lv = sender as ListView; 438 | if (lv?.SelectedItem is not DuplicateFileModel selectFile) 439 | { 440 | return; 441 | } 442 | if (selectFile.Extension.IsNotEmpty() && GlobalArgs.AppConfig.ImageExtension.Contains(selectFile.Extension)) 443 | { 444 | ImgPreview.Source = new BitmapImage(new Uri(selectFile.Path, UriKind.Absolute)); 445 | GridImage.Visibility = Visibility.Visible; 446 | } 447 | } 448 | 449 | private void BtnAbout_Click(object sender, RoutedEventArgs e) 450 | { 451 | var about = new AboutWindow 452 | { 453 | Owner = this 454 | }; 455 | about.ShowDialog(); 456 | } 457 | private void BtnChooseFile_Click(object sender, RoutedEventArgs e) 458 | { 459 | if (_myModel.DuplicateFiles.Count == 0) 460 | { 461 | MessageBox.Show("没有可用数据", "重复文件查找", MessageBoxButton.OK, MessageBoxImage.Information); 462 | return; 463 | } 464 | if (_myModel.DuplicateFiles.Any(x => x.IsCheckedFile)) 465 | { 466 | RemoveChooseAllFiles(); 467 | } 468 | else 469 | { 470 | ChooseAllFiles(); 471 | } 472 | 473 | } 474 | 475 | /// 476 | /// 是否在全选操作 477 | /// 478 | private bool _isSelectingAll = false; 479 | private void ChooseAllFiles() 480 | { 481 | _isSelectingAll = true; 482 | foreach (var duplicateFile in _myModel.DuplicateFiles) 483 | { 484 | duplicateFile.IsCheckedFile = true; 485 | } 486 | var keyList = _myModel.DuplicateFiles.GroupBy(x => x.Key); 487 | foreach (var keyItem in keyList) 488 | { 489 | _myModel.DuplicateFiles.First(x => x.Key == keyItem.Key).IsCheckedFile = false; 490 | } 491 | _isSelectingAll = false; 492 | } 493 | 494 | 495 | private void RemoveChooseAllFiles() 496 | { 497 | foreach (var duplicateFile in _myModel.DuplicateFiles) 498 | { 499 | duplicateFile.IsCheckedFile = false; 500 | } 501 | } 502 | private void BtnDeleteFile_Click(object sender, RoutedEventArgs e) 503 | { 504 | try 505 | { 506 | if (_myModel.DuplicateFiles.Count == 0) 507 | { 508 | MessageBox.Show("没有可用数据", "重复文件查找", MessageBoxButton.OK, MessageBoxImage.Information); 509 | return; 510 | } 511 | 512 | var selectCount = _myModel.DuplicateFiles.Count(x => x.IsCheckedFile); 513 | if (MessageBox.Show($"确认要删除选中文件吗?文件删除后不可恢复!{System.Environment.NewLine}待删除的文件总数:{selectCount}", "重复文件查找", MessageBoxButton.YesNo, MessageBoxImage.Question) != MessageBoxResult.Yes) 514 | { 515 | return; 516 | } 517 | 518 | //删除文件 519 | for (int i = _myModel.DuplicateFiles.Count - 1; i >= 0; i--) 520 | { 521 | if (_myModel.DuplicateFiles[i].IsCheckedFile == false) 522 | { 523 | continue; 524 | } 525 | 526 | string fileFullName = _myModel.DuplicateFiles[i].Path; 527 | string directory = Path.GetDirectoryName(fileFullName); 528 | if (File.Exists(fileFullName)) 529 | { 530 | File.Delete(fileFullName); 531 | } 532 | if (ChkDeleteEmptyDirectory.IsChecked == true && Directory.GetDirectories(directory).Length == 0 && Directory.GetFiles(directory).Length == 0) 533 | { 534 | Directory.Delete(directory); 535 | } 536 | 537 | _myModel.DuplicateFiles.RemoveAt(i); 538 | } 539 | 540 | //文件删除后,清除不再重复的文件 541 | var singleKeys = _myModel.DuplicateFiles 542 | .GroupBy(x => x.Key).Select(x => new { Key = x.Key, Count = x.Count() }) 543 | .Where(x => x.Count < 2) 544 | .Select(x => x.Key) 545 | .ToList(); 546 | for (int i = _myModel.DuplicateFiles.Count - 1; i >= 0; i--) 547 | { 548 | if (!singleKeys.Contains(_myModel.DuplicateFiles[i].Key)) 549 | { 550 | continue; 551 | } 552 | _myModel.DuplicateFiles.RemoveAt(i); 553 | } 554 | MessageBox.Show("删除完成", "重复文件查找", MessageBoxButton.OK, MessageBoxImage.Information); 555 | } 556 | catch (Exception ex) 557 | { 558 | MessageBox.Show($"删除失败:{ex.Message}", "重复文件查找", MessageBoxButton.OK, MessageBoxImage.Error); 559 | } 560 | } 561 | 562 | private void ChkIsChecked_Checked(object sender, RoutedEventArgs e) 563 | { 564 | if (_isSelectingAll) 565 | { 566 | //全选操作时,不处理事件 567 | return; 568 | } 569 | 570 | var chk = (sender as CheckBox); 571 | if (chk == null) 572 | { 573 | MessageBox.Show("选择失败,系统内部错误", "重复文件查找", MessageBoxButton.OK, MessageBoxImage.Error); 574 | return; 575 | } 576 | 577 | if (chk.DataContext is not DuplicateFileModel checkedFile) 578 | { 579 | MessageBox.Show("选择失败,系统内部错误", "重复文件查找", MessageBoxButton.OK, MessageBoxImage.Error); 580 | return; 581 | } 582 | 583 | if (!_myModel.DuplicateFiles.Any(x => x.Key == checkedFile.Key && x.IsCheckedFile == false)) 584 | { 585 | MessageBox.Show("必须至少保留重复文件中的一个", "重复文件查找", MessageBoxButton.OK, MessageBoxImage.Stop); 586 | chk.IsChecked = false; 587 | return; 588 | } 589 | } 590 | 591 | private void SaveAppConfig() 592 | { 593 | var directory = Path.GetDirectoryName(GlobalArgs.AppConfigPath) ?? throw new ArgumentException("配置文件路径异常"); 594 | if (!Directory.Exists(directory)) 595 | { 596 | Directory.CreateDirectory(directory); 597 | } 598 | string appConfigString = System.Text.Json.JsonSerializer.Serialize(GlobalArgs.AppConfig); 599 | File.WriteAllText(GlobalArgs.AppConfigPath, appConfigString); 600 | } 601 | 602 | private void ListBoxSearchFolders_Drop(object sender, DragEventArgs e) 603 | { 604 | var directories = (string[])e.Data.GetData(DataFormats.FileDrop); 605 | if (directories == null) 606 | { 607 | return; 608 | } 609 | 610 | foreach (var directory in directories) 611 | { 612 | if (!Directory.Exists(directory)) 613 | { 614 | continue; 615 | } 616 | AddSearchFolderOnly(directory); 617 | } 618 | } 619 | 620 | private void BtnFilter_Click(object sender, RoutedEventArgs e) 621 | { 622 | if (grid1.RowDefinitions[1].Height.GridUnitType == GridUnitType.Pixel) 623 | { 624 | grid1.RowDefinitions[1].Height = new GridLength(0, GridUnitType.Auto); 625 | } 626 | else 627 | { 628 | grid1.RowDefinitions[1].Height = new GridLength(0, GridUnitType.Pixel); 629 | } 630 | } 631 | 632 | private void RadioButtonFilter_Checked(object sender, RoutedEventArgs e) 633 | { 634 | var directory = _myModel.SearchDirectory.FirstOrDefault(x => x.IsSelected)?.DirectoryName; 635 | if (directory.IsEmpty()) 636 | { 637 | MessageBox.Show("出错了:获取目录失败!", "重复文件查找", MessageBoxButton.OK, MessageBoxImage.Error); 638 | return; 639 | } 640 | 641 | //清除上次选中的遗留状态 642 | foreach (var duplicateFile in _myModel.DuplicateFiles) 643 | { 644 | duplicateFile.IsCheckedFile = false; 645 | } 646 | 647 | //查找所选文件夹下所有重复文件的 key 648 | var filterFilesKeys = _myModel.DuplicateFiles.Where(x => x.Path.IndexOf(directory) == 0).Select(x => x.Key).ToList(); 649 | foreach (var duplicateFile in _myModel.DuplicateFiles) 650 | { 651 | if (duplicateFile.Path.IndexOf(directory) != 0) 652 | { 653 | continue; 654 | } 655 | 656 | if (!filterFilesKeys.Contains(duplicateFile.Key)) 657 | { 658 | continue; 659 | } 660 | duplicateFile.IsCheckedFile = true; 661 | } 662 | } 663 | 664 | private void ChkOnlyFileName_Checked(object sender, RoutedEventArgs e) 665 | { 666 | TxtOnlyFileNames.IsEnabled = true; 667 | } 668 | 669 | private void ChkOnlyFileName_Unchecked(object sender, RoutedEventArgs e) 670 | { 671 | TxtOnlyFileNames.Text = ""; 672 | TxtOnlyFileNames.IsEnabled = false; 673 | } 674 | 675 | } 676 | } 677 | -------------------------------------------------------------------------------- /src/Model/AppConfigInfo.cs: -------------------------------------------------------------------------------- 1 | namespace FindDuplicateFiles.Model 2 | { 3 | public class AppConfigInfo 4 | { 5 | public string Theme { get; set; } = "Dark"; 6 | public string ImageExtension { get; set; } = ".gif;.jpg;.jpeg;.png;.bmp"; 7 | public string MediaExtension { get; set; } = ".mp3;.wma;.wav;.mpeg;.avi;.mov;.wmv;.rm;.rmvb;.flv"; 8 | public string SystemExtension { get; set; } = ".dll"; 9 | public string DocumentExtension { get; set; } = ".txt;.pdf;.doc;.docx;.xls;.xlsx;.ppt;.pptx"; 10 | 11 | /// 12 | /// 每次查询允许的最大重复文件数量 13 | /// 14 | public int MaxAllowDuplicateFileCount { get; set; } = 500; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Model/SearchConfigs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace FindDuplicateFiles.Model 5 | { 6 | public class SearchConfigs 7 | { 8 | public List Folders { get; set; } 9 | public SearchMatchEnum SearchMatch { get; set; } 10 | public SearchOptionEnum SearchOption { get; set; } 11 | public SearchOptionData SearchOptionData { get; set; } 12 | } 13 | 14 | /// 15 | /// 匹配方式 16 | /// 17 | [Flags] 18 | public enum SearchMatchEnum 19 | { 20 | Name = 1, 21 | Size = 2, 22 | LastWriteTime = 4, 23 | MD5 = 8, 24 | } 25 | /// 26 | /// 查找选项 27 | /// 28 | [Flags] 29 | public enum SearchOptionEnum 30 | { 31 | IgnoreEmptyFile = 1, 32 | IgnoreHiddenFile = 2, 33 | IgnoreSmallFile = 4, 34 | OnlyImageFile = 8, 35 | OnlyMediaFile = 16, 36 | OnlyDocumentFile = 32, 37 | IgnoreSystemFile = 64, 38 | OnlyFileName = 128 39 | } 40 | 41 | /// 42 | /// 查找选项的数据 43 | /// 44 | public class SearchOptionData 45 | { 46 | public List OnlyFileNames { get; set; } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Model/SimpleFileInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FindDuplicateFiles.Model 4 | { 5 | public class SimpleFileInfo 6 | { 7 | public string Name { get; set; } 8 | public string Path { get; set; } 9 | public decimal Size { get; set; } 10 | public DateTime LastWriteTime { get; set; } 11 | /// 12 | /// 文件后缀名 13 | /// 14 | public string Extension { get; set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Model/UpdateAppInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace FindDuplicateFiles.Model 8 | { 9 | public class UpdateAppInfo 10 | { 11 | public int code { get; set; } 12 | public string message { get; set; } 13 | public Data data { get; set; } 14 | } 15 | 16 | public class Data 17 | { 18 | public int versionCode { get; set; } 19 | public string versionName { get; set; } 20 | public string minVersionName { get; set; } 21 | public string downloadUrl { get; set; } 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/Model/UpdateConfigInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace FindDuplicateFiles.Model 8 | { 9 | public class UpdateConfigInfo 10 | { 11 | public string Url { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Resource.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // 此代码由工具生成。 4 | // 运行时版本:4.0.30319.42000 5 | // 6 | // 对此文件的更改可能会导致不正确的行为,并且如果 7 | // 重新生成代码,这些更改将会丢失。 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace FindDuplicateFiles { 12 | using System; 13 | 14 | 15 | /// 16 | /// 一个强类型的资源类,用于查找本地化的字符串等。 17 | /// 18 | // 此类是由 StronglyTypedResourceBuilder 19 | // 类通过类似于 ResGen 或 Visual Studio 的工具自动生成的。 20 | // 若要添加或移除成员,请编辑 .ResX 文件,然后重新运行 ResGen 21 | // (以 /str 作为命令选项),或重新生成 VS 项目。 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Resource { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Resource() { 33 | } 34 | 35 | /// 36 | /// 返回此类使用的缓存的 ResourceManager 实例。 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("FindDuplicateFiles.Resource", typeof(Resource).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// 重写当前线程的 CurrentUICulture 属性,对 51 | /// 使用此强类型资源类的所有资源查找执行重写。 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | 63 | /// 64 | /// 查找类似 http://api.jiuling.cc/app/check-update/581671619f5131a82ea6b813329be6f2/windows 的本地化字符串。 65 | /// 66 | internal static string AutoUpgradePath { 67 | get { 68 | return ResourceManager.GetString("AutoUpgradePath", resourceCulture); 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Resource.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | http://api.jiuling.cc/app/check-update/581671619f5131a82ea6b813329be6f2/windows 122 | 123 | -------------------------------------------------------------------------------- /src/SearchFile/CheckDuplicateQueue.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using FindDuplicateFiles.Model; 7 | 8 | namespace FindDuplicateFiles.SearchFile 9 | { 10 | public class CheckDuplicateQueue 11 | { 12 | /// 13 | /// 是否停止 14 | /// 15 | private bool _isStop; 16 | /// 17 | /// 用于处理文件的队列 18 | /// 19 | private readonly ConcurrentQueue _fileProcessing = new(); 20 | /// 21 | /// 任务是否添加完成 22 | /// 23 | private bool _isFinished; 24 | /// 25 | /// 匹配方式 26 | /// 27 | private SearchMatchEnum _searchMatch; 28 | 29 | private static readonly SemaphoreSlim MySemaphoreSlim = new(1, 2); 30 | private readonly ConcurrentDictionary> _duplicateFiles = new(); 31 | 32 | /// 33 | /// 执行任务的消息 34 | /// 35 | public Action EventMessage; 36 | /// 37 | /// 发现重复文件 38 | /// 39 | public Action EventDuplicateFound; 40 | /// 41 | /// 搜索完成 42 | /// 43 | public Action EventSearchFinished; 44 | 45 | /// 46 | /// 正在执行的任务总数 47 | /// 48 | private int _searchingCount; 49 | public async void Start(SearchMatchEnum searchMatch) 50 | { 51 | _isStop = false; 52 | _isFinished = false; 53 | EventMessage?.Invoke("准备查找...."); 54 | await Task.Run(() => 55 | { 56 | _searchMatch = searchMatch; 57 | while (!_isStop) 58 | { 59 | if (!_fileProcessing.TryDequeue(out var fileInfo)) 60 | { 61 | if (_isFinished) 62 | { 63 | return; 64 | } 65 | continue; 66 | } 67 | 68 | Task.Run(() => 69 | { 70 | SearchDuplicate(fileInfo); 71 | }); 72 | } 73 | }); 74 | } 75 | /// 76 | /// 查找重复文件的具体逻辑 77 | /// 78 | /// 79 | private async void SearchDuplicate(SimpleFileInfo fileInfo) 80 | { 81 | Interlocked.Increment(ref _searchingCount); 82 | await MySemaphoreSlim.WaitAsync(); 83 | try 84 | { 85 | if (_isStop) 86 | { 87 | return; 88 | } 89 | EventMessage?.Invoke($"检查文件:{fileInfo.Path}"); 90 | string fileKey = ""; 91 | if ((_searchMatch & SearchMatchEnum.Name) == SearchMatchEnum.Name) 92 | { 93 | fileKey = fileInfo.Name; 94 | } 95 | 96 | if ((_searchMatch & SearchMatchEnum.Size) == SearchMatchEnum.Size) 97 | { 98 | fileKey = $"{fileKey}${fileInfo.Size}"; 99 | } 100 | 101 | if ((_searchMatch & SearchMatchEnum.LastWriteTime) == SearchMatchEnum.LastWriteTime) 102 | { 103 | fileKey = $"{fileKey}${fileInfo.LastWriteTime:yyyy-MM-dd HH:mm:ss}"; 104 | } 105 | 106 | if ((_searchMatch & SearchMatchEnum.MD5) == SearchMatchEnum.MD5) 107 | { 108 | string md5 = JiuLing.CommonLibs.Security.MD5Utils.GetFileValueToLower(fileInfo.Path); 109 | fileKey = $"{fileKey}${md5}"; 110 | } 111 | 112 | var newFile = new List { fileInfo }; 113 | var resultFile = _duplicateFiles.AddOrUpdate(fileKey, newFile, (_, oldValue) => 114 | { 115 | oldValue.Add(fileInfo); 116 | return oldValue; 117 | }); 118 | if (resultFile.Count > 1) 119 | { 120 | //有重复文件 121 | if (resultFile.Count == 2) 122 | { 123 | //如果是第一次发现重复,则需要连同之前一次的文件信息一并通知 124 | EventDuplicateFound?.Invoke(fileKey, resultFile[0]); 125 | } 126 | 127 | EventDuplicateFound?.Invoke(fileKey, fileInfo); 128 | } 129 | } 130 | finally 131 | { 132 | MySemaphoreSlim.Release(); 133 | if (Interlocked.Decrement(ref _searchingCount) == 0 && _isFinished) 134 | { 135 | EventSearchFinished.Invoke(); 136 | } 137 | } 138 | } 139 | 140 | /// 141 | /// 任务完成 142 | /// 143 | public void Finished() 144 | { 145 | _isFinished = true; 146 | } 147 | 148 | public void AddOneFileToQueue(SimpleFileInfo file) 149 | { 150 | _fileProcessing.Enqueue(file); 151 | } 152 | 153 | public void AddMultipleFilesToQueue(List files) 154 | { 155 | foreach (var file in files) 156 | { 157 | _fileProcessing.Enqueue(file); 158 | } 159 | } 160 | 161 | public void Stop() 162 | { 163 | _isStop = true; 164 | EventMessage?.Invoke("任务被终止"); 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/SearchFile/SearchFilesJob.cs: -------------------------------------------------------------------------------- 1 | using FindDuplicateFiles.Common; 2 | using FindDuplicateFiles.Filters; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Threading.Tasks; 8 | using FindDuplicateFiles.Model; 9 | 10 | namespace FindDuplicateFiles.SearchFile 11 | { 12 | public class SearchFilesJob 13 | { 14 | /// 15 | /// 是否停止 16 | /// 17 | private bool _isStop; 18 | 19 | /// 20 | /// 执行任务的消息 21 | /// 22 | public Action EventMessage; 23 | 24 | /// 25 | /// 文件查找任务队列 26 | /// 27 | private CheckDuplicateQueue _checkDuplicateQueue; 28 | 29 | /// 30 | /// 发现重复文件 31 | /// 32 | public Action EventDuplicateFound; 33 | /// 34 | /// 搜索完成 35 | /// 36 | public Action EventSearchFinished; 37 | 38 | private SearchConfigs _searchConfig; 39 | public async void Start(SearchConfigs config) 40 | { 41 | _isStop = false; 42 | _searchConfig = config; 43 | await Task.Run(() => 44 | { 45 | _checkDuplicateQueue = new CheckDuplicateQueue 46 | { 47 | EventDuplicateFound = EventDuplicateFound, 48 | EventMessage = EventMessage, 49 | EventSearchFinished = EventSearchFinished 50 | }; 51 | 52 | _checkDuplicateQueue.Start(_searchConfig.SearchMatch); 53 | foreach (string folderPath in _searchConfig.Folders) 54 | { 55 | EachDirectory(folderPath, paths => 56 | { 57 | CalcFilesInfo(paths); 58 | }); 59 | } 60 | 61 | _checkDuplicateQueue.Finished(); 62 | }); 63 | } 64 | 65 | private void EachDirectory(string folderPath, Action> callbackFilePaths) 66 | { 67 | try 68 | { 69 | if (_isStop) 70 | { 71 | return; 72 | } 73 | 74 | if (!Directory.Exists(folderPath)) 75 | { 76 | return; 77 | } 78 | Directory.GetDirectories(folderPath).ToList().ForEach(path => 79 | { 80 | //继续遍历文件夹内容 81 | EachDirectory(path, callbackFilePaths); 82 | }); 83 | 84 | callbackFilePaths.Invoke(Directory.GetFiles(folderPath).ToList()); 85 | } 86 | catch (UnauthorizedAccessException) 87 | { 88 | //todo 没有权限时记录错误 89 | } 90 | } 91 | 92 | private void CalcFilesInfo(List paths) 93 | { 94 | EventMessage?.Invoke($"读取文件:{string.Join(",", paths)}"); 95 | //根据路径加载文件信息 96 | var files = paths.Select(path => new FileInfo(path)).ToList(); 97 | 98 | //条件过滤器 99 | if ((_searchConfig.SearchOption & SearchOptionEnum.IgnoreEmptyFile) == SearchOptionEnum.IgnoreEmptyFile) 100 | { 101 | IFileSearchFilter filter = new IgnoreEmptyFileFilter(); 102 | files = filter.FilterByCondition(files); 103 | } 104 | if ((_searchConfig.SearchOption & SearchOptionEnum.IgnoreHiddenFile) == SearchOptionEnum.IgnoreHiddenFile) 105 | { 106 | IFileSearchFilter filter = new IgnoreHiddenFileFilter(); 107 | files = filter.FilterByCondition(files); 108 | } 109 | if ((_searchConfig.SearchOption & SearchOptionEnum.IgnoreSmallFile) == SearchOptionEnum.IgnoreSmallFile) 110 | { 111 | IFileSearchFilter filter = new IgnoreSmallFileFilter(1024); 112 | files = filter.FilterByCondition(files); 113 | } 114 | if ((_searchConfig.SearchOption & SearchOptionEnum.IgnoreSystemFile) == SearchOptionEnum.IgnoreSystemFile) 115 | { 116 | IFileSearchFilter filter = new IgnoreExtensionFilter(GlobalArgs.AppConfig.SystemExtension.Split(';').ToList()); 117 | files = filter.FilterByCondition(files); 118 | } 119 | if ((_searchConfig.SearchOption & SearchOptionEnum.OnlyDocumentFile) == SearchOptionEnum.OnlyDocumentFile) 120 | { 121 | IFileSearchFilter filter = new OnlyExtensionFilter(GlobalArgs.AppConfig.DocumentExtension.Split(';').ToList()); 122 | files = filter.FilterByCondition(files); 123 | } 124 | if ((_searchConfig.SearchOption & SearchOptionEnum.OnlyImageFile) == SearchOptionEnum.OnlyImageFile) 125 | { 126 | IFileSearchFilter filter = new OnlyExtensionFilter(GlobalArgs.AppConfig.ImageExtension.Split(';').ToList()); 127 | files = filter.FilterByCondition(files); 128 | } 129 | if ((_searchConfig.SearchOption & SearchOptionEnum.OnlyMediaFile) == SearchOptionEnum.OnlyMediaFile) 130 | { 131 | IFileSearchFilter filter = new OnlyExtensionFilter(GlobalArgs.AppConfig.MediaExtension.Split(';').ToList()); 132 | files = filter.FilterByCondition(files); 133 | } 134 | if ((_searchConfig.SearchOption & SearchOptionEnum.OnlyFileName) == SearchOptionEnum.OnlyFileName) 135 | { 136 | IFileSearchFilter filter = new OnlyFileNameFilter(_searchConfig.SearchOptionData.OnlyFileNames); 137 | files = filter.FilterByCondition(files); 138 | } 139 | files.ForEach(file => 140 | { 141 | //符合条件的文件写入队列 142 | var simpleInfo = new SimpleFileInfo() 143 | { 144 | Name = file.Name, 145 | Path = file.FullName, 146 | Size = file.Length, 147 | LastWriteTime = file.LastWriteTime, 148 | Extension = file.Extension.ToLower() 149 | }; 150 | _checkDuplicateQueue.AddOneFileToQueue(simpleInfo); 151 | }); 152 | } 153 | public void Stop() 154 | { 155 | _isStop = true; 156 | _checkDuplicateQueue.Stop(); 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/Styles/AboutWindowStyle.xaml: -------------------------------------------------------------------------------- 1 |  3 | 4 | 7 | 8 | 24 | 25 | 33 | 34 | 42 | 43 | 49 | 50 | 57 | 58 | 66 | 67 | 72 | 73 | 76 | 77 | 82 | -------------------------------------------------------------------------------- /src/Styles/MainWindowStyle.xaml: -------------------------------------------------------------------------------- 1 |  3 | 4 | 33 | 34 | 35 | 49 | 65 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 87 | 92 | 97 | 102 | 105 | 108 | 111 | 112 | 115 | 116 | 120 | 121 | 124 | 125 | 130 | 131 | 136 | 137 | 140 | 141 | 144 | 145 | 148 | 149 | 153 | 154 | 161 | 162 | 166 | 167 | -------------------------------------------------------------------------------- /src/Themes/ThemeDark.xaml: -------------------------------------------------------------------------------- 1 |  4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/Themes/ThemeGreen.xaml: -------------------------------------------------------------------------------- 1 |  4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/ViewModel/AboutWindowViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace FindDuplicateFiles.ViewModel 2 | { 3 | public class AboutWindowViewModel : ViewModelBase 4 | { 5 | private string _downloadUrl; 6 | /// 7 | /// 下载地址 8 | /// 9 | public string DownloadUrl 10 | { 11 | get => _downloadUrl; 12 | set 13 | { 14 | _downloadUrl = value; 15 | OnPropertyChanged(); 16 | } 17 | } 18 | 19 | private string _version; 20 | /// 21 | /// 版本 22 | /// 23 | public string Version 24 | { 25 | get => _version; 26 | set 27 | { 28 | _version = value; 29 | OnPropertyChanged(); 30 | } 31 | } 32 | 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/ViewModel/DuplicateFileModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FindDuplicateFiles.ViewModel 4 | { 5 | public class DuplicateFileModel : ViewModelBase 6 | { 7 | private string _key; 8 | public string Key 9 | { 10 | get => _key; 11 | set 12 | { 13 | _key = value; 14 | OnPropertyChanged(); 15 | } 16 | } 17 | 18 | private bool _isCheckedFile; 19 | public bool IsCheckedFile 20 | { 21 | get => _isCheckedFile; 22 | set 23 | { 24 | _isCheckedFile = value; 25 | OnPropertyChanged(); 26 | } 27 | } 28 | 29 | private string _name; 30 | public string Name 31 | { 32 | get => _name; 33 | set 34 | { 35 | _name = value; 36 | OnPropertyChanged(); 37 | } 38 | } 39 | 40 | private string _path; 41 | public string Path 42 | { 43 | get => _path; 44 | set 45 | { 46 | _path = value; 47 | OnPropertyChanged(); 48 | } 49 | } 50 | 51 | private decimal _size; 52 | public decimal Size 53 | { 54 | get => _size; 55 | set 56 | { 57 | _size = value; 58 | OnPropertyChanged(); 59 | } 60 | } 61 | 62 | private DateTime _lastWriteTime; 63 | public DateTime LastWriteTime 64 | { 65 | get => _lastWriteTime; 66 | set 67 | { 68 | _lastWriteTime = value; 69 | OnPropertyChanged(); 70 | } 71 | } 72 | 73 | 74 | private string _extension; 75 | public string Extension 76 | { 77 | get => _extension; 78 | set 79 | { 80 | _extension = value; 81 | OnPropertyChanged(); 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/ViewModel/MainWindowViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | 3 | namespace FindDuplicateFiles.ViewModel 4 | { 5 | public class MainWindowViewModel : ViewModelBase 6 | { 7 | public MainWindowViewModel() 8 | { 9 | SearchDirectory = new ObservableCollection(); 10 | DuplicateFiles = new ObservableCollection(); 11 | } 12 | 13 | private ObservableCollection _searchDirectory; 14 | /// 15 | /// 要搜索的文件夹集合 16 | /// 17 | public ObservableCollection SearchDirectory 18 | { 19 | get => _searchDirectory; 20 | set 21 | { 22 | _searchDirectory = value; 23 | OnPropertyChanged(); 24 | } 25 | } 26 | 27 | 28 | private ObservableCollection _duplicateFiles; 29 | /// 30 | /// 重复文件集合 31 | /// 32 | public ObservableCollection DuplicateFiles 33 | { 34 | get => _duplicateFiles; 35 | set 36 | { 37 | _duplicateFiles = value; 38 | OnPropertyChanged(); 39 | } 40 | } 41 | 42 | private bool _isShowLoading; 43 | /// 44 | /// 是否显示loading窗口 45 | /// 46 | public bool IsShowLoading 47 | { 48 | get => _isShowLoading; 49 | set 50 | { 51 | _isShowLoading = value; 52 | OnPropertyChanged(); 53 | } 54 | } 55 | 56 | private string _jobMessage; 57 | /// 58 | /// 任务消息 59 | /// 60 | public string JobMessage 61 | { 62 | get => _jobMessage; 63 | set 64 | { 65 | _jobMessage = value; 66 | OnPropertyChanged(); 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/ViewModel/SearchDirectoryModel.cs: -------------------------------------------------------------------------------- 1 | namespace FindDuplicateFiles.ViewModel 2 | { 3 | public class SearchDirectoryModel : ViewModelBase 4 | { 5 | private string _directoryName; 6 | /// 7 | /// 已选取的要搜索的文件夹 8 | /// 9 | public string DirectoryName 10 | { 11 | get => _directoryName; 12 | set 13 | { 14 | _directoryName = value; 15 | OnPropertyChanged(); 16 | } 17 | } 18 | 19 | private bool _isSelected; 20 | /// 21 | /// 是否选中 22 | /// 23 | public bool IsSelected 24 | { 25 | get => _isSelected; 26 | set 27 | { 28 | _isSelected = value; 29 | OnPropertyChanged(); 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/ViewModel/ViewModelBase.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using System.Runtime.CompilerServices; 3 | 4 | namespace FindDuplicateFiles.ViewModel 5 | { 6 | public class ViewModelBase : INotifyPropertyChanged 7 | { 8 | public event PropertyChangedEventHandler PropertyChanged; 9 | 10 | public void OnPropertyChanged([CallerMemberName] string caller = null) 11 | { 12 | var handler = PropertyChanged; 13 | if (handler != null) 14 | { 15 | handler(this, new PropertyChangedEventArgs(caller)); 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiuLing-zhang/FindDuplicateFiles/510fd287ee0538f2bf3f6c25d215ac1abc119dcf/src/icon.ico -------------------------------------------------------------------------------- /src/首次运行请看这里!!!.txt: -------------------------------------------------------------------------------- 1 | 首次运行时需要安装.Net7运行时,下载地址: 2 | https://aka.ms/dotnet/7.0/windowsdesktop-runtime-win-x64.exe --------------------------------------------------------------------------------