Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Multiple File Renamer - Windows Forms App for Batch Renaming Files

0.00/5 (No votes)
26 Jul 2020 1  
A simple, yet powerful and fast Windows Forms app for batch renaming files
This article goes through an overview of how the app works, including: loading filenames, highlighting a selection in each filename, mark string on index (where it will also try to match the selection value), mark all strings (which will mark all of the strings that match the current selection's value), clearing all highlighting, and the selections dictionary (where highlighted text gets edited).

Introduction

This is a simple, yet powerful and fast Windows Forms app for batch renaming files. Batch renaming here means that you can edit all the files from a chosen folder at the same time based on certain criteria.

The idea is to allow the users whenever they have a large number of files with similar names to be able to edit the filenames all at once. For example, let's say you have files with these names:

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

Now let's say you want to replace „somestring“ with something else. Or you want to delete the random numbers in the end. Or you want to change the extension. Or insert some string at an index. This app allows you to apply each of these changes easily to all the files at once.

Using the App

This is how the app initially looks like:

Image 1

Loading Filenames

First, browse for a folder by using the „Browse folder“ button. After choosing a folder, filenames will appear in the richtextbox control. The filenames will be shown without the path (just the filenames), above each other (one file per line).

Image 2

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));
}

Highlighting

Before any actual editing of the filenames, first we need to highlight all the substrings we want a specific action done upon.

Mark on Index

This functionality will highlight a selection in each filename based on currently selected string's starting index and length of any of the filenames.

i.e., If you select „#somestring#“ in line no. 2 (File_10#somestring#327665.txt), this action will highlight all the substrings of all the lines between indexes 7 and 19, regardless of the selection value:

Image 3Image 4

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);
}

Mark String on Index

This functionality will do the same thing as Mark on index, except it will also try to match the selection value – if the value does not match, it will not highlight it.

Image 5Image 6

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);
}

Mark All Strings

This function will simply go through all the lines and mark all of the strings that match the current selection's value.

Image 7Image 8

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);
}

Clear Selection

Clears all highlighting.

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();
}

Apply Changes

  • ReplaceSelection replaces all the highlighted text in all the lines with text entered in an input box.
  • InsertBeforeSelection inserts entered text in front of all the highlighted text in all the lines.
  • DeleteSelection deletes all the highlighted text in all the lines.

All methods are using a sub-method called ReplaceInsert, which switches between Replace/Insert based on its second argument. DeleteSelection uses the Replace functionality with an empty string.

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

For the purpose of entering the replacement/insertion string, I have designed a special input box (separate class), which basically consists of only an empty textbox (no surrounding form). The MyInputBox is shown by calling the static method ShowDialog, similar to MessageBox. It is always shown on current mouse position.

The entered string is confirmed (OK) by pressing ENTER, and it is rejected (Cancel) by pressing 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;
        }
    }
}

Image 9

Selections Dictionary

Highlighted text gets edited, but in order to programatically loop through these highlighted selections, the code must be able to recognize which text exactly is highlighted.

To achieve this, I introduced a Dictionary type variable called selections:

private Dictionary<int, List<Tuple<int, int>>> selections = 
                              new Dictionary<int, List<Tuple<int, int>>>();

This dictionary consists of List objects each consisting of Tuple objects containing pairs of int variables – these integers represent start index (tuple Item1) and length (tuple Item2). Keys of this dictionary are integers that represent the ordinal numbers of rows in the richtextbox (row=filename) – so the dictionary is initialized with as many empty lists as there are filenames in richTextBox.

The idea was to store all the selections in each line of text (each filename) as a list of tuples containing first index and length of selection, i.e.,

Image 10

For the upper selection, the variable selections will contain:

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)

Manual Edit

There is also the possibility to unlock the richtextbox and do any kind of manual changes directly to the filenames in there. Of course, this would require then to do the change to each file individually – which is still better than to do it manually file by file in Windows Explorer.

The only limitation to this type of edit is that the number of lines has to be kept the same when closing the manual edit mode – because the lines are mapped to actual files, so the number of lines/files has to be maintained.

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 - Undo / Redo

The app also supports undo / redo functionality. (The undo / redo refers to all the „apply changes“ actions.)

For the undo / redo functionality to work, we need a sort-of double-sided stack structure (something where we can push and pull elements from each side of the list). Such a structure is called a „deque“ and it does not (still) exist as a built-in class in .NET Framework, so I needed to build one myself for this purpose.

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;
            }
        }
    }
}

The backbone of a Deque object is a LinkedList. The class is initialized with a generic variable type T, and it takes as parameter the desired capacity of the Deque – meaning how many undo objects will it be able to remember.

Elements can be added (pushed), and taken (pulled) from the structure. The structure always remembers the current index – from which point we can go backwards (undo – GetPreviousElement) or forward (redo – GetNextElement). GetPreviousElement will take elements as long as it does not reach the beginning of the list, while the same applies to GetNextElement, only in the opposite direction.

When adding new elements, they are always added after the current index (active element) – if the list is full and the element is added to the end of the list, then the first element of the list is removed. If the new element is added somewhere in the middle of the list (meaning we backed up a couple of times with undo, then did some other change), all of the redo elements (to the right) that existed before are deleted – redo after that is not possible.

Save Changes

Changes to the filenames are always done only in the richtextbox and other memory structures used within the code. The actual filenames (files stored on the system) are only changed when you hit the Save changes. When this is done, then whatever is at the moment inside the richtextbox is applied to the actual files.

Before actually changing the filenames, all of the new filenames in the richtextbox are checked for illegal characters. If any of them contain illegal characters, they will be marked red before saving. Save changes is disabled if the manual mode is on.

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);
}

Shortcut Key Combinations

Each functionaility in the app is also available through a keyboard shortcut – this is making this app very fast to use, once you get used to the key combinations.

Highlighting is done by using Left ctrl+Left shift+letter, while apply changes is done by using Left alt+letter.

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;
        }
    }
}

History

  • 26th July, 2020: Initial version

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here