目录
  1. 1. 背景
  2. 2. 软件界面
  3. 3. 源码
    1. 3.1. 窗口源码
    2. 3.2. 功能源码
  4. 4. 软件下载
媒体文件整理工具

  本软件用于整理照片和视频文件,按拍摄日期或创建日期分类到对应的子文件夹中,实现按日期整理照片,视频文件,提高文件整理效率及文件归档准确性;软件支持的文件类型包括:照片(JPG、PNG)、RAW格式照片(RAW、NEF、CR2、ARW)以及视频文件(MP4、MOV、AVI、MKV);测试文件由索尼及GoPro设备拍摄;

背景

 通过相机等设备拍摄的文件拷贝到电脑时,创建时间与实际拍摄时间会出现一定的错乱情况;本软件旨在获取媒体文件的Exif元数据,查找到媒体文件的创建媒体日期并根据改日期创建文件夹,将文件放入文件夹中;便于根据媒体实际创建日期对媒体文件进行整理;当该文件没有元数据时,使用修改日期;经对比,在没有元数据的情况下,修改日期与实际媒体文件日期最接近;测试文件由索尼a6400及GoPro 9 拍摄,其他设备需自己复验;移动文件前务必在结果预览窗口中对内容进行检查;

软件界面

界面展示

源码

 程序运行需要添加引用com包中的Microsoft Shell Controls AndAutomation,需要使用uget安装MetadataExtractor软件包;

软件截图

窗口源码

1
2
3
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
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
<Window x:Class="FileOrganizerApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="媒体文件整理工具" Height="600" Width="800" MinHeight="400" MinWidth="600">
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<!-- 标题 -->
<RowDefinition Height="Auto"/>
<!-- 功能说明 -->
<RowDefinition Height="Auto"/>
<!-- 输入区域 -->
<RowDefinition Height="Auto"/>
<!-- 按钮区域 -->
<RowDefinition Height="Auto"/>
<!-- 结果预览标题 -->
<RowDefinition Height="*"/>
<!-- 结果显示区域 -->
<RowDefinition Height="Auto"/>
<!-- 操作按钮 -->
<RowDefinition Height="Auto"/>
<!-- 版权声明 -->
</Grid.RowDefinitions>

<!-- 标题 -->
<TextBlock Grid.Row="0" Text="媒体文件整理工具" FontSize="20" FontWeight="Bold" Foreground="#333333"
HorizontalAlignment="Center" Margin="0,0,0,10"/>

<!-- 功能说明 -->
<TextBlock Grid.Row="1" Text="本软件用于整理照片和视频文件,按拍摄日期或创建日期分类到对应的子文件夹中。"
FontSize="12" Foreground="#555555" TextWrapping="Wrap" Margin="0,0,0,10"/>

<!-- 输入区域 -->
<StackPanel Grid.Row="2" Orientation="Horizontal" VerticalAlignment="Center" Margin="0,0,0,10">
<Label Content="请输入文件夹路径:" VerticalAlignment="Center" Margin="0,0,10,0"/>
<TextBox x:Name="txtFolderPath" Width="400" Height="30"/>
</StackPanel>

<!-- 按钮区域 -->
<Button Content="开始处理" Grid.Row="3" Width="150" Height="35" HorizontalAlignment="Left" Margin="0,0,0,10" Click="BtnProcess_Click"/>

<!-- 结果预览标题 -->
<TextBlock Grid.Row="4" Text="结果预览" FontSize="16" FontWeight="Bold" Foreground="#555555"
HorizontalAlignment="Left" Margin="0,10,0,5"/>

<!-- 结果显示区域 -->
<TextBox x:Name="txtMessage" Grid.Row="5"
IsReadOnly="True" TextWrapping="Wrap" VerticalScrollBarVisibility="Auto"
AcceptsReturn="True" Margin="0,10,0,10"
Background="#F9F9F9" Foreground="#333333"
FontFamily="Consolas" FontSize="14"/>

<!-- 操作按钮 -->
<StackPanel Grid.Row="6" Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,10,0,0">
<Button Content="确认移动文件" Width="120" Height="35" Margin="10,0" Click="BtnMoveFiles_Click"/>
<Button Content="关闭" Width="120" Height="35" Margin="10,0" Click="BtnClose_Click"/>
</StackPanel>

<!-- 版权声明 -->
<TextBlock Grid.Row="7" Text="© 2025 嗜血星空earth. 版权所有." FontSize="12" Foreground="#888888"
HorizontalAlignment="Right" Margin="0,10,0,0"/>
</Grid>
</Window>

功能源码

1
2
3
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
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
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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
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
251
252
253
254
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Windows;
using MetadataExtractor;
using MetadataExtractor.Formats.Exif;
using Shell32;

// 为 System.IO.Directory 设置别名,解决命名冲突
using SystemDirectory = System.IO.Directory;

namespace FileOrganizerApp
{
public partial class MainWindow : Window
{
private static readonly string LogFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "FileDateLog.txt");
private Dictionary<string, DateTime> fileDateMapping = new Dictionary<string, DateTime>();
private string currentFolderPath;

public MainWindow()
{
InitializeComponent();
}

private void BtnProcess_Click(object sender, RoutedEventArgs e)
{
currentFolderPath = txtFolderPath.Text;

if (!SystemDirectory.Exists(currentFolderPath))
{
MessageBox.Show("文件夹不存在,请检查路径!", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
return;
}

File.WriteAllText(LogFilePath, "文件处理日志:\n");
var files = SystemDirectory.GetFiles(currentFolderPath);
StringBuilder resultMessage = new StringBuilder();
fileDateMapping.Clear();

foreach (var file in files)
{
var extension = Path.GetExtension(file).ToLower();

try
{
if (extension == ".jpg" || extension == ".jpeg" || extension == ".png")
{
resultMessage.AppendLine(ReadPhotoDateTime(file));
}
else if (extension == ".raw" || extension == ".nef" || extension == ".cr2" || extension == ".arw")
{
resultMessage.AppendLine(ReadRawPhotoDateTime(file));
}
else if (extension == ".mp4" || extension == ".mov" || extension == ".avi" || extension == ".mkv")
{
resultMessage.AppendLine(ReadVideoDateTime(file));
}
else
{
resultMessage.AppendLine($"跳过不支持的文件格式:{file}");
Log($"跳过不支持的文件格式:{file}");
}
}
catch (Exception ex)
{
resultMessage.AppendLine($"文件处理失败:{file},错误信息:{ex.Message}");
Log($"文件处理失败:{file},错误信息:{ex.Message}");
}
}

txtMessage.Text = resultMessage.ToString();
}

private string ReadPhotoDateTime(string file)
{
try
{
var directories = ImageMetadataReader.ReadMetadata(file);
var subIfdDirectory = directories.OfType<ExifSubIfdDirectory>().FirstOrDefault();
var dateTime = subIfdDirectory?.GetDescription(ExifDirectoryBase.TagDateTimeOriginal);

if (!string.IsNullOrEmpty(dateTime) && TryParseDate(dateTime, out DateTime parsedDateTime))
{
Log($"照片文件:{file},拍摄日期:{parsedDateTime:yyyy-MM-dd}");
fileDateMapping[file] = parsedDateTime;
return $"照片文件:{file}\n拍摄日期:{parsedDateTime:yyyy-MM-dd}";
}
else
{
var modificationTime = File.GetLastWriteTime(file);
Log($"照片文件:{file},修改日期:{modificationTime:yyyy-MM-dd}");
fileDateMapping[file] = modificationTime;
return $"照片文件:{file}\n修改日期:{modificationTime:yyyy-MM-dd}";
}
}
catch (Exception ex)
{
throw new Exception($"读取照片元数据失败:{ex.Message}");
}
}

private string ReadRawPhotoDateTime(string file)
{
try
{
var directories = ImageMetadataReader.ReadMetadata(file);
var subIfdDirectory = directories.OfType<ExifSubIfdDirectory>().FirstOrDefault();
var dateTime = subIfdDirectory?.GetDescription(ExifDirectoryBase.TagDateTimeOriginal);

if (!string.IsNullOrEmpty(dateTime) && TryParseDate(dateTime, out DateTime parsedDateTime))
{
Log($"RAW照片文件:{file},拍摄日期:{parsedDateTime:yyyy-MM-dd}");
fileDateMapping[file] = parsedDateTime;
return $"RAW照片文件:{file}\n拍摄日期:{parsedDateTime:yyyy-MM-dd}";
}
else
{
var modificationTime = File.GetLastWriteTime(file);
Log($"RAW照片文件:{file},修改日期:{modificationTime:yyyy-MM-dd}");
fileDateMapping[file] = modificationTime;
return $"RAW照片文件:{file}\n修改日期:{modificationTime:yyyy-MM-dd}";
}
}
catch (Exception ex)
{
throw new Exception($"读取RAW照片元数据失败:{ex.Message}");
}
}

private string ReadVideoDateTime(string file)
{
try
{
Shell shell = new Shell();
Folder folder = shell.NameSpace(Path.GetDirectoryName(file));
FolderItem folderItem = folder.ParseName(Path.GetFileName(file));

int mediaDateColumnIndex = -1;
for (int i = 0; i < 256; i++)
{
string columnHeader = folder.GetDetailsOf(null, i);
if (columnHeader.Contains("创建媒体日期") || columnHeader.Contains("Media created"))
{
mediaDateColumnIndex = i;
break;
}
}

if (mediaDateColumnIndex == -1)
{
throw new Exception("无法找到媒体日期列,请检查系统语言设置或文件属性。");
}

string dateTimeStr = folder.GetDetailsOf(folderItem, mediaDateColumnIndex);

if (!string.IsNullOrEmpty(dateTimeStr))
{
dateTimeStr = CleanDateString(dateTimeStr);
DateTime parsedDate = ExtractDateFromVideo(dateTimeStr);
Log($"视频文件:{file},创建日期:{parsedDate:yyyy-MM-dd}");
fileDateMapping[file] = parsedDate;
return $"视频文件:{file}\n创建日期:{parsedDate:yyyy-MM-dd}";
}
else
{
var creationTime = File.GetCreationTime(file);
Log($"视频文件:{file},文件创建日期:{creationTime:yyyy-MM-dd}");
fileDateMapping[file] = creationTime;
return $"视频文件:{file}\n文件创建日期:{creationTime:yyyy-MM-dd}";
}
}
catch (Exception ex)
{
throw new Exception($"读取视频元数据失败:{ex.Message}");
}
}

private static string CleanDateString(string dateTimeStr)
{
return Regex.Replace(dateTimeStr, @"[\u200E\u200F]", "").Trim();
}

private static DateTime ExtractDateFromVideo(string dateTimeStr)
{
try
{
var parts = dateTimeStr.Split(' ');
var datePart = parts[0];

if (DateTime.TryParse(datePart, out DateTime parsedDate))
{
return parsedDate.Date;
}
else
{
throw new FormatException($"无法解析日期部分:{datePart}");
}
}
catch (Exception ex)
{
throw new FormatException($"无法从日期字符串中提取日期:{dateTimeStr}. 错误信息: {ex.Message}");
}
}

private static bool TryParseDate(string dateString, out DateTime parsedDate)
{
string[] formats = { "yyyy/M/d '星期'ddddtt HH:mm", "yyyy/M/d '星期'ddddtt H:mm", "yyyy-MM-dd'T'HH:mm:ss" };
var culture = new CultureInfo("zh-CN") { DateTimeFormat = { AMDesignator = "上午", PMDesignator = "下午" } };
return DateTime.TryParseExact(dateString, formats, culture, DateTimeStyles.None, out parsedDate);
}

private static void Log(string message)
{
File.AppendAllText(LogFilePath, message + Environment.NewLine, Encoding.UTF8);
}

private void BtnMoveFiles_Click(object sender, RoutedEventArgs e)
{
if (fileDateMapping.Count == 0)
{
MessageBox.Show("没有可移动的文件,请先处理文件夹!", "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}

foreach (var kvp in fileDateMapping)
{
string file = kvp.Key;
DateTime date = kvp.Value;
string dateFolder = Path.Combine(currentFolderPath, date.ToString("yyyy-MM-dd"));

if (!SystemDirectory.Exists(dateFolder))
{
SystemDirectory.CreateDirectory(dateFolder);
}

string destinationPath = Path.Combine(dateFolder, Path.GetFileName(file));
File.Move(file, destinationPath);
Log($"文件移动成功:{file} -> {destinationPath}");
}

MessageBox.Show("文件已成功移动到对应日期文件夹!", "操作完成", MessageBoxButton.OK, MessageBoxImage.Information);
txtMessage.Text += "\n文件移动操作已完成。";
}

private void BtnClose_Click(object sender, RoutedEventArgs e)
{
Close();
}
}
}

软件下载

点击下载媒体文件管理工具

文章作者: 嗜血星空earth
文章链接: http://sxxkearth.github.io/2025/04/16/%E5%AA%92%E4%BD%93%E6%96%87%E4%BB%B6%E6%95%B4%E7%90%86%E5%B7%A5%E5%85%B7/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请附以署名及出处!

评论