多文件重命名器 - 用于批量重命名文件的 Windows Forms 应用






4.63/5 (8投票s)
一个简单、强大且快速的 Windows Forms 应用程序,用于批量重命名文件
引言
这是一个简单、强大且快速的 Windows Forms 应用程序,用于批量重命名文件。批量重命名意味着您可以根据特定标准一次性编辑选定文件夹中的所有文件。
其理念是允许用户在拥有大量具有相似名称的文件时,能够一次性编辑所有文件名。例如,假设您有以下文件:
File_1#somestring#228511.txt
File_2#somestring#876007.txt
File_3#somestring#231223.txt
File_4#somestring#401709.txt
File_5#somestring#663775.txt
File_6#somestring#151348.txt
File_7#somestring#880078.txt
File_8#somestring#392233.txt
File_9#somestring#906363.txt
File_10#somestring#327665.txt
现在,假设您想将“somestring”替换为其他内容。或者您想删除末尾的随机数字。或者您想更改文件扩展名。或者在某个索引处插入一些字符串。此应用程序允许您轻松地将所有这些更改一次性应用于所有文件。
使用应用程序
应用程序的初始界面如下所示:
加载文件名
首先,使用“浏览文件夹”按钮浏览一个文件夹。选择文件夹后,文件名将显示在 richtextbox
控件中。文件名将仅显示文件名(不含路径),逐行显示(每行一个文件)。
private void BrowseFolder()
{
if (folderBrowserDialog1.ShowDialog() == DialogResult.Cancel)
return;
folder = folderBrowserDialog1.SelectedPath;
richTextBox1.Text = "";
if (filenames != null)
filenames.Clear();
FillRTB(folder);
selections.Clear();
for (int i = 0; i < filenames.Count; i++)
selections.Add(i, new List<Tuple<int, int>>());
deque.ClearAll();
deque.Add(new ArrayList(filenames));
}
高亮显示
在实际编辑文件名之前,我们需要先高亮显示所有需要执行特定操作的子字符串。
按索引标记
此功能将根据当前选定字符串的起始索引和任何文件名的长度,高亮显示每个文件名中的选定内容。
也就是说,如果您在第 2 行(File_10#somestring#327665.txt)选择了“#somestring#”,此操作将高亮显示所有行中索引 7 到 19 之间的子字符串,而不管选定值如何。
private void HighlightTextIndex(int startIndex, int length)
{
/*
* Marks a substring of specified length on startIndex in each filename.
*/
int i = 0;
for(int row = 0; row < filenames.Count; row++)
{
if (((string)filenames[row]).Length < startIndex + length) // skip if this filename
// is too short
{
i += ((string)filenames[row]).Length + 1;
continue;
}
richTextBox1.Select(i + startIndex, length);
richTextBox1.SelectionBackColor = Color.Yellow;
selections[row].Add(new Tuple<int, int>(startIndex, length));
i += ((string)filenames[row]).Length + 1;
}
richTextBox1.Select(0, 0);
}
按值和索引标记字符串
此功能与“按索引标记”功能相同,只是它还会尝试匹配选定值——如果值不匹配,则不会高亮显示。
private void HighlightTextStringIndex(int startIndex, string searchText)
{
/*
* Marks searchText on given index in each filename
* (if the selection on the index does not match searchText, don't highlight).
*/
int i = 0;
for (int row = 0; row < filenames.Count; row++)
{
if (((string)filenames[row]).Length < startIndex + searchText.Length) // skip if
// this filename is too short
{
i += ((string)filenames[row]).Length + 1;
continue;
}
richTextBox1.Select(i + startIndex, searchText.Length);
if (richTextBox1.SelectedText == searchText)
{
richTextBox1.SelectionBackColor = Color.Yellow;
selections[row].Add(new Tuple<int, int>(startIndex, searchText.Length));
}
i += ((string)filenames[row]).Length + 1;
}
richTextBox1.Select(0, 0);
}
标记所有字符串
此函数将遍历所有行,并标记所有与当前选定值匹配的字符串。
private void HighlightTextAll(string searchText)
{
/*
* Searches for and marks all instances of searchText inside the richTextBox.
*/
Regex regex = new Regex(Regex.Escape(searchText));
int i = 0;
for (int row = 0; row < filenames.Count; row++)
{
MatchCollection matches = regex.Matches((string)filenames[row]);
foreach (Match match in matches)
{
richTextBox1.Select(i + match.Index, match.Length);
richTextBox1.SelectionBackColor = Color.Yellow;
selections[row].Add(new Tuple<int, int>(match.Index, match.Length));
}
i += ((string)filenames[row]).Length + 1;
}
richTextBox1.Select(0, 0);
}
清除选择
清除所有高亮显示。
private void HighlightClear()
{
/*
* Clears all highlight color.
*/
richTextBox1.SelectionChanged -= RichTextBox1_SelectionChanged;
richTextBox1.SelectAll();
richTextBox1.SelectionChanged += RichTextBox1_SelectionChanged;
richTextBox1.SelectionBackColor = RichTextBox.DefaultBackColor;
richTextBox1.Select(0, 0);
foreach(int n in selections.Keys)
selections[n].Clear();
}
应用更改
ReplaceSelection
将用输入框中的文本替换所有行中所有高亮显示的内容。InsertBeforeSelection
将在所有行中所有高亮显示文本的前面插入输入的文本。DeleteSelection
删除所有行中所有高亮显示的内容。
所有方法都使用一个名为 ReplaceInsert
的子方法,该方法根据其第二个参数在 Replace
/Insert
之间切换。DeleteSelection
使用 Replace
功能,并将替换字符串设置为空。
private void ReplaceInsert(string newString, bool replace)
{
// check if selections are clear - allowed for insert
int startIndex;
if (selections.Where(x => x.Value.Count > 0).Count() == 0)
{
startIndex = FindSelectionIndex();
foreach (int n in selections.Keys)
selections[n].Add(new Tuple<int, int>(startIndex, 0));
}
int offset;
foreach (int n in selections.Keys)
{
for(int i = 0; i < selections[n].Count; i++)
{
offset = 0;
if (replace)
{
filenames[n] = ((string)filenames[n]).Remove
(selections[n][i].Item1, selections[n][i].Item2);
offset -= selections[n][i].Item2;
}
filenames[n] = ((string)filenames[n]).Insert(selections[n][i].Item1, newString);
offset += newString.Length;
ApplyOffset(n, i, offset);
}
}
FillRTB();
}
private void ApplyOffset(int rowkey, int start, int offset)
{
// adjust starting index for all selections that are on higher index
// (to the right) than the start index
for(int i = start + 1; i < selections[rowkey].Count; i++)
{
selections[rowkey][i] = new Tuple<int, int>(selections[rowkey][i].Item1 +
offset, selections[rowkey][i].Item2);
}
}
MyInputBox
为了输入替换/插入字符串,我设计了一个特殊的输入框(独立的类),它基本上只包含一个空的文本框(没有周围的窗体)。MyInputBox
通过调用 static
方法 ShowDialog
来显示,类似于 MessageBox
。它始终显示在当前鼠标位置。
通过按 **ENTER** 键确认输入的字符串(**确定**),通过按 **ESCAPE** 键拒绝(**取消**)。
using System.Drawing;
using System.Windows.Forms;
namespace FileRenamerAdvanced
{
public static class MyInputBox
{
internal class MyInputForm : Form
{
TextBox tbx;
public MyInputForm()
{
this.FormBorderStyle = FormBorderStyle.None;
this.ShowInTaskbar = false;
tbx = new TextBox();
tbx.Width = 200;
tbx.Location = new System.Drawing.Point(0, 0);
tbx.KeyUp += Tbx_KeyUp;
this.Controls.Add(tbx);
this.Size = tbx.Size;
this.BackColor = Color.Magenta;
this.TransparencyKey = Color.Magenta;
}
private void Tbx_KeyUp(object sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.Enter)
{
this.DialogResult = DialogResult.OK;
}
else if (e.KeyCode == Keys.Escape)
{
this.DialogResult = DialogResult.Cancel;
}
}
public new string ShowDialog()
{
base.ShowDialog();
if (this.DialogResult == DialogResult.OK)
return tbx.Text;
return null;
}
}
public static string ShowDialog()
{
MyInputForm form = new MyInputForm();
form.StartPosition = FormStartPosition.Manual;
form.Location = Control.MousePosition;
string retValue = form.ShowDialog();
form.Close();
return retValue;
}
}
}
选择字典
高亮显示的内容会被编辑,但为了以编程方式遍历这些高亮显示的选定内容,代码必须能够识别具体哪些文本被高亮显示了。
为了实现这一点,我引入了一个名为 selections
的 Dictionary
类型变量。
private Dictionary<int, List<Tuple<int, int>>> selections =
new Dictionary<int, List<Tuple<int, int>>>();
此字典包含 List
对象,每个对象又包含 Tuple
对象,其中包含一对 int
变量——这些整数代表起始 index
(tuple Item1
)和 length
(tuple Item2
)。此字典的键是整数,代表 richtextbox
中行的序号(row=filename
)——因此,字典会根据 richTextBox
中的文件名数量进行初始化,每个文件名对应一个空的列表。
其思路是将每行文本(每个文件名)中的所有选定内容存储为一个元组列表,其中包含选定的起始索引和长度,例如:
对于上面的选定内容,变量 selections 将包含:
0:
(6,1)
(17,1)
1:
(7,1)
(18,1)
2:
(6,1)
(17,1)
3:
(6,1)
(17,1)
4:
(6,1)
(17,1)
5:
(6,1)
(17,1)
6:
(6,1)
(17,1)
7:
(6,1)
(17,1)
8:
(6,1)
(17,1)
9:
(6,1)
(17,1)
手动编辑
还有一种可能性是解锁 richtextbox
并直接对其进行任何手动更改。当然,这仍然需要逐个文件进行更改——但这仍然比在 Windows Explorer 中逐个文件手动操作要好。
这种编辑类型的唯一限制是,在退出手动编辑模式时,行数必须保持不变——因为行与实际文件相关联,所以必须维护行数/文件数。
private void checkBox1_CheckedChanged(object sender, EventArgs e)
{
if (filenames == null)
return;
if (checkBox1.Checked)
{
HighlightClear();
richTextBox1.ReadOnly = false;
}
else
{
string[] tmp = richTextBox1.Text.Split('\n');
if (tmp.Length == filenames.Count)
{
filenames = new ArrayList(tmp);
deque.Add(new ArrayList(filenames));
richTextBox1.ReadOnly = true;
}
else
{
MessageBox.Show("Expected number of filenames is " +
filenames.Count.ToString(), "Invalid number of filenames",
MessageBoxButtons.OK, MessageBoxIcon.Error);
checkBox1.Checked = true;
}
}
}
Deque - 撤销/重做
该应用程序还支持撤销/重做功能。(撤销/重做是指所有“应用更改”操作。)
为了使撤销/重做功能正常工作,我们需要一种双向栈结构(可以从列表的两端推入和拉出元素的结构)。这种结构称为“deque”,在 .NET Framework 中(目前)还没有内置类,所以我需要为此目的自己构建一个。
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
namespace FileRenamerAdvanced
{
class Deque<T>
{
private LinkedList<T> _list;
private int _index;
public int Capacity { get; set; }
public Deque(int capacity)
{
Capacity = capacity;
_list = new LinkedList<T>();
_index = -1;
}
public void Add(T elem)
{
// if capacity is full and index is at the last element, remove first
if (_list.Count == Capacity && _index == Capacity - 1)
{
_list.RemoveFirst();
}
// if index is somewhere in between, delete all the values on higher indexes
else if(_index < Capacity - 1)
{
ClearAllNext();
}
// add last and raise index
_list.AddLast(elem);
_index = Math.Min(_index + 1, Capacity - 1);
}
public T GetPreviousElement()
{
_index = Math.Max(_index - 1, 0);
return _list.ElementAt<T>(_index);
}
public T GetNextElement()
{
_index = Math.Min(_index + 1, _list.Count - 1);
return _list.ElementAt<T>(_index);
}
public void ClearAll()
{
_list.Clear();
_index = -1;
}
public void ClearAllNext()
{
while (_list.Count - 1 > _index && _list.Count != 0)
{
_list.RemoveLast();
}
}
public void ClearAllPrevious()
{
while (_index >= 0)
{
_list.RemoveFirst();
--_index;
}
}
}
}
Deque
对象的核心是一个 LinkedList
。该类使用泛型变量类型 T
进行初始化,并接受所需的 Deque
capacity
作为参数——这意味着它能够记住多少个撤销对象。
元素可以被添加到(推入)结构中,也可以被从中取出(拉出)。结构会始终记住当前索引——从该点我们可以向后(撤销 - GetPreviousElement
)或向前(重做 - GetNextElement
)移动。GetPreviousElement
会一直获取元素,直到到达列表的开头,而 GetNextElement
也是如此,只是方向相反。
添加新元素时,它们总是添加到当前索引(活动元素)之后——如果列表已满且元素被添加到列表末尾,则会删除列表的第一个元素。如果新元素被添加到列表的中间(意味着我们通过撤销回退了几次,然后进行了其他更改),那么所有之前存在的重做元素(右侧的)都会被删除——之后将无法重做。
保存更改
对文件名的更改仅在 richtextbox
和代码中使用的其他内存结构中进行。实际的文件名(存储在系统上的文件)仅在您单击“保存更改”时才会被更改。完成此操作后,richtextbox
中的内容将应用于实际文件。
在实际更改文件名之前,richtextbox
中的所有新文件名都会被检查是否存在非法字符。如果其中任何一个包含非法字符,它们将在保存前被标记为红色。当处于手动模式时,“保存更改”将被禁用。
private void SaveChanges()
{
if (MessageBox.Show("Rename " + filenames.Count.ToString() + " files.
Are you sure?", "Save changes", MessageBoxButtons.YesNo,
MessageBoxIcon.Question) == DialogResult.No)
return;
HighlightClear();
// check manual edit - must be off
if (checkBox1.Checked)
{
MessageBox.Show("Manual mode is on! Please turn it off before saving.",
"Save failed", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
// check if filenames are valid
int i = 0;
int cnt = 0;
foreach (string f in filenames)
{
if (f.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0)
{
// highlight red
richTextBox1.Select(i, f.Length);
richTextBox1.SelectionBackColor = Color.Red;
cnt++;
}
i += f.Length + 1;
}
if (cnt > 0)
{
MessageBox.Show("Filenames marked red contain invalid characters!",
"Save failed", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
try
{
for (i = 0; i < filenames.Count; i++)
File.Move(folder + "\\" + filenames_orig[i], folder + "\\" + filenames[i]);
}
catch (Exception ex)
{
MessageBox.Show("Error:\n" + ex.Message, "Save failed",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
FillRTB(folder);
}
快捷键组合
应用程序中的每个功能都可以通过键盘快捷键访问——一旦您习惯了这些组合键,这将使该应用程序的使用速度非常快。
高亮显示通过使用 Left ctrl+Left shift+字母 来完成,而应用更改通过使用 Left alt+字母 来完成。
CTRL+B = BROWSE
CTRL+Z = UNDO
CTRL+SHIFT+Z = REDO
CTRL+SHIFT+I = MARK ON INDEX
CTRL+SHIFT+S = MARK STRING ON INDEX
CTRL+SHIFT+A = MARK ALL STRINGS
CTRL+SHIFT+C = CLEAR HIGHLIGHT
ALT+R = REPLACE
ALT+I = INSERT
ALT+D = DELETE
ALT+S = SAVE
private void RichTextBox1_KeyDown(object sender, System.Windows.Forms.KeyEventArgs e)
{
if(Keyboard.IsKeyDown(Key.LeftCtrl) && Keyboard.IsKeyDown(Key.B)) // CTRL+B = BROWSE FOLDER
{
BrowseFolder();
return;
}
if (filenames == null)
return;
if (Keyboard.IsKeyDown(Key.LeftCtrl) && Keyboard.IsKeyDown(Key.LeftShift) &&
Keyboard.IsKeyDown(Key.Z)) // CTRL+SHIFT+Z = REDO
{
Redo();
return;
}
else if(Keyboard.IsKeyDown(Key.LeftCtrl) && Keyboard.IsKeyDown(Key.Z)) // CTRL+Z = UNDO
{
Undo();
return;
}
else if (Keyboard.IsKeyDown(Key.LeftCtrl) &&
Keyboard.IsKeyDown(Key.LeftShift)) // LEFT CTRL + LEFT SHIFT
{
string selection = richTextBox1.SelectedText;
int index = FindSelectionIndex();
if (selection != "")
{
if (Keyboard.IsKeyDown(Key.I)) // I = INDEX
{
HighlightClear();
HighlightTextIndex(index, selection.Length);
return;
}
else if (Keyboard.IsKeyDown(Key.S)) // S = STRING
{
HighlightClear();
HighlightTextStringIndex(index, selection);
return;
}
else if (Keyboard.IsKeyDown(Key.A)) // A = ALL
{
HighlightClear();
HighlightTextAll(selection);
return;
}
}
else if (Keyboard.IsKeyDown(Key.C)) // C = CLEAR
{
HighlightClear();
return;
}
}
else if(Keyboard.IsKeyDown(Key.LeftAlt))
{
if (Keyboard.IsKeyDown(Key.R)) // R = REPLACE
{
ReplaceSelection();
deque.Add(new ArrayList(filenames));
return;
}
else if (Keyboard.IsKeyDown(Key.D)) // D = DELETE
{
DeleteSelection();
deque.Add(new ArrayList(filenames));
return;
}
else if (Keyboard.IsKeyDown(Key.I)) // I = INSERT
{
InsertBeforeSelection();
deque.Add(new ArrayList(filenames));
return;
}
else if (Keyboard.IsKeyDown(Key.S)) // S = SAVE
{
SaveChanges();
return;
}
}
}
历史
- 2020年7月26日:初始版本