Пишем на C# программу для поиска файлов, используя ProgressBar и BackgroundWorker

Пишем на C# программу для поиска файлов, используя ProgressBar и BackgroundWorker

User Rating: 4 / 5

Всем привет.

В сегодняшней статье мы посмотрим с вами на то, как можно сочетать работу элементов управления ProgressBar и BackgroundWorker в приложении Windows Forms, и для этой цели мы напишем простую демонстрационную программу на C#, которая будет искать файлы в заданной директории на компьютере по некоторой фразе. Фразой может быть часть имени файла, его расширение или часть пути, по которому хранится файл. Прежде, чем мы начнём погружение в статью, я опишу те предусловия, при которых предполагаю, что статья будет понятна читателю и сможет принести ему какую-то пользу или как минимум дать новую информацию для размышления.

Предполагается, что читатель:

  1. уже знаком с языком C# и имеет некоторый опыт написания программ на C#, умеет создавать приложения Windows Forms в среде разработки Microsoft Visual Studio
  2. понимает, где располагается "Панель элементов" в среде разработки при создании приложения Windows Forms, а также как размещать на форме элементы управления, настраивать их свойства и создавать методы-обработчики для событий. Также есть понимание, как добавлять новые классы C# в проект.
  3. знаком со стандартными элементами управления ButtonComboBoxTextBoxListView и имеет представление, для чего они нужны в приложении Windows Forms и как используются
  4. хочет узнать/понять, как можно объединить работу двух элементов ProgressBar и BackgroundWorker в программе таким образом, чтобы прогресс бар постепенно заполнялся при выполнении некоторой реальной задачи, а сама задача выполнялась в отдельном потоке
  5. хочет узнать/понять, как можно обрабатывать события DoWorkProgressChanged и RunWorkerCompleted для элемента BackgroundWorker
  6. хочет узнать/понять, как можно работать со стандартным элементом FolderBrowserDialog для выбора одной из доступных директорий на диске
  7. хочет узнать/понять, как при помощи C# можно открывать файлы в системе с помощью стандартной программы, ассоциированной с расширением открываемого файла

Если по пунктам 1-3 оказывается, что вы пока только начинаете знакомство с языком C# и ещё не писали программ для Windows Forms, то перед этой статьей я бы порекомендовал для начала прочитать другие статьи сайта, в которых более детально описываются шаги по созданию нового проекта для Windows Forms, расположению на форме новых элементов, изменению их свойств и созданию методов-обработчиков для событий. В этих статьях также объясняется, как работать с некоторыми основными элементами управления (Button, TextBox). Ниже привожу ссылки на эти статьи:

Если вы являетесь опытным разработчиком приложений на C#, и по пунктам 4-7 оказывается, что вы уже работали ранее с элементами ProgressBar и BackgroundWorker, знакомы с элементом FolderBrowserDialog, знаете, как при помощи C# открывать файлы ассоциированной с ними программой, а также хорошо знакомы с событиями DoWork, ProgressChanged, RunWorkerCompleted, то, вероятно, большая часть этой статьи может не принести вам какого-то нового опыта/знаний, поэтому тут решайте сами, стоит ли читать дальше.

Итак, если все предусловия выше пройдены, и вы добрались до этого места, то далее я опишу тот результат, который мы получим на выходе после выполнения шагов из этой статьи.

Наше будущее приложение по поиску файлов будет выглядеть примерно следующим образом:

В выпадающем списке "Диск для поиска" будут подгружаться доступные диски на компьютере, где будет запускаться программа (в моём случае это системный диск C:\ и съемный диск E:\ с меткой "SP PHD U3"):

В поле "Путь для поиска" мы сможем выбирать директорию, в которой необходимо искать файлы.

В текстовом поле перед кнопкой "Начать поиск" мы будем вводить полное имя файла, часть имени файла или часть пути, содержащего файл.

В списке "Список найденных файлов" после поиска будут отображаться найденные файлы в заданной директории с возможностью открыть любой из них двойным кликом:

В случае, если путь для поиска содержит большое количество вложенных директорий и файлов, то перед началом самого поиска будет производиться подсчёт файлов во вложенных директориях, а прогресс бар будет иметь стиль ProgressBarStyle.Marquee, что говорит о неопределённом времени завершения процесса подсчёта файлов:

Итак, приступим к созданию этого приложения. 

1. Создание нового проекта и подготовка главной формы

Создаём в среде разработки новый проект с типом "Приложение Windows Forms (.NET Framework)" и называем его FileFinderExample. Местоположение проекта выбирайте на свой вкус, я предпочитаю оставлять все настройки по умолчанию, ровно так, как предлагает среда Microsoft Visual Studio.

Стандартную главную форму приложения после создания проекта переименовываем в FrmFileFinderMain (с переименованием всех соответствующих ссылок, т.е. так, чтобы файл исходного кода для формы назывался FrmFileFinderMain.cs).

Далее переходим к добавлению элементов управления на главную форму, они будут представлять собой интерфейс программы.

2. Установка свойств главной формы и расположение на ней элементов

Главной форме приложения установим следующие свойства:

  • FormBorderStyle: FixedSingle
  • Text: [Allineed.Ru] Пример поиска файлов в системе с помощью элементов BackgroundWorker
  • Size: 816; 562
  • StartPosition: CenterScreen
  • MaximizeBox: False
  • MinimizeBox: False

На главную форму приложения FrmFileFinderMain помещаем 6 меток (элемент Label) и устанавливаем им следующие свойства:

1-я метка:

  • Text: Диск для поиска:
  • Location: 14; 9
  • Name: LabelDrive

2-я метка:

  • Text: Путь для поиска:
  • Location: 14; 41
  • Name: LabelSearchPath

3-я метка:

  • Text: Полное имя файла, часть имени файла или часть пути, содержащего файл:
  • Location: 14; 91
  • Name: LabelFileName

4-я метка:

  • Text: Список найденных файлов:
  • Location: 14; 146
  • Name: LabelFoundFilesList

5-я метка:

  • Text: Прогресс:
  • Location: 14; 454
  • Name: LabelProgress

6-я метка:

  • Font: Microsoft Sans Serif; 8,25pt; style=Bold
  • Text: 0
  • Location: 774; 454
  • Name: LabelFilesCount

Дальше мы помещаем на форму элемент ComboBox и устанавливаем ему следующие свойства:

  • DropDownStyle: DropDownList
  • Location: 126; 6
  • Size: 662; 21
  • Name: ComboBoxDrives 

Теперь поместим на главную форму 2 текстовых поля (элемент TextBox) и установим следующие свойства:

1-е текстовое поле:

  • Text: <оставить пустым>
  • Location: 15; 57
  • Size: 632; 20
  • ReadOnly: True
  • Name: TextBoxSearchPath

2-е текстовое поле:

  • Text: <оставить пустым>
  • Location: 15; 107
  • Size: 632; 20
  • Name: TextBoxFileName

Помещаем на форму ещё 2 кнопки (элемент Button) и установим им следующие свойства:

1-я кнопка:

  • Text: &Обзор...
  • Location: 653; 55
  • Size: 135; 23
  • Name: ButtonSelectSearchDirectory

2-я кнопка: 

  • Text: &Начать поиск
  • Location: 653; 105
  • Size: 135; 23
  • Name: ButtonStartSearch

Помещаем на форму один элемент ListView и установим ему следующие свойства:

  • FullRowSelect: True
  • GridLines: True
  • View: Details
  • Location: 15; 172
  • Size: 773; 270
  • Columns: нужно перейти в редактор столбцов и добавить два столбца со свойствами:
    • 1-й столбец:
      • Text: Имя файла
      • Width: 650
      • Name: columnHeaderFileName
    • 2-й столбец:
      • Text: Размер файла, байт
      • Width: 115
      • Name: columnHeaderFileSize
  • Groups: нужно перейти в редактор групп и добавить одну группу со свойствами:
    • Header: Найденные файлы
    • Name: listViewGroupFiles
  • MultiSelect: False
  • Name: ListViewFoundFiles

Далее размещаем на форме один элемент ProgressBar и выставляем для него следующие свойства:

  • Location: 15; 470
  • Size: 773; 23
  • MarqueeAnimationSpeed: 30
  • Style: Marquee
  • Name: ProgressBarMain 

Настало время для элементов BackgroundWorker. Помещаем на форму 2 элемента BackgroundWorker и установим для них свойства:

1-й элемент BackgroundWorker:

  • WorkerReportProgress: True
  • WorkerSupportsCancellation: True
  • Name: BackgroundWorkerEstimateSearchTime

2-й элемент BackgroundWorker:

  • WorkerReportProgress: True
  • WorkerSupportsCancellation: True
  • Name: BackgroundWorkerSearchFiles

Ещё для нашей программы нам потребуется элемент FolderBrowserDialog. Помещаем его на главную форму в одном экземпляре и устанавливаем ему свойства:

  • Name: FolderBrowserDialogSelectSearchDirectory

Последний элемент, необходимый для нашей главной формы, - это элемент ToolTip. Помещаем один элемент ToolTip на главную форму и выставим ему следующие свойства:

  • AutoPopDelay: 20000
  • BackColor: 255; 224; 192
  • ForeColor: 192; 64; 0
  • InitialDelay: 100
  • ReshowDelay: 1000
  • ToolTipIcon: Info
  • ToolTipTitle: Подсказка:
  • Name: ToolTipHints

Теперь нам осталось сделать ещё несколько действий, прежде чем мы перейдем к разработке логики программы.

Выбираем главную форму в представлении конструктора и выставляем свойство:

  • AcceptButton: ButtonStartSearch

Это позволит сделать кнопку ButtonStartSearch ( "Начать поиск" ) кнопкой по умолчанию на главной форме. И когда мы будем нажимать <Enter> после ввода части имени файла, то будет запускаться алгоритм подсчёта файлов в директории поиска или сам поиск.

Ещё мы добавим подсказки для разных элементов формы при помощи элемента ToolTip, который мы подготовили. Для этого последовательно выбираем указанные ниже элементы и выставляем им значение свойства ToolTip на ToolTipHints:

Выбрать элемент на форме со следующим значением Name Выставить элементу свойство "ToolTip на ToolTipHints" на указанное в этом столбце
ComboBoxDrives Указывает на системный диск, в котором осуществляется поиск файлов
TextBoxSearchPath Отображает путь, в котором будет производиться поиск файлов
ButtonSelectSearchDirectory Выберите директорию, в которой будет производиться поиск файлов
TextBoxFileName 1) png
найдёт все файлы, в имени которых встретится "png" или в пути до которых найдётся "png"
2) .avi
найдёт все файлы, в имени которых встретится ".avi" или в пути до которых найдётся ".avi"
это не только относится к расширению, но и может быть частью имени файла или пути до файла
ButtonStartSearch Нажмите для начала поиска или остановки поиска файлов
ListViewFoundFiles В списке будут отражены результаты поиска файлов
ProgressBarMain Отображает ход выполнения операции (оценки времени поиска файлов или непосредственно поиска файлов)

Теперь мы готовы перейти к программированию логики для нашего приложения.

3. Создаём новый класс DriveInfoItem для хранения данных о системных дисках, которые будут элементами выпадающего списка

Нам необходимо добавить в проект новый класс, давайте назовём его DriveInfoItem. Этот класс будет хранить данные о конкретном диске, который доступен в системе, на которой будет запускаться программа. Туда же будут включаться и съемные диски, т.е. наша программа сможет искать файлы также на USB-носителях ("флешки", внешние жёсткие диски и т. д.).

После добавления нового класса в проект, заменяем содержимое файла DriveInfoItem.cs следующим кодом:

using System;
using System.IO;

namespace FileFinderExample {
    /// <summary>
    /// Класс описывает элемент с информацией о диске для выпадающего списка на главной форме
    /// </summary>
    public class DriveInfoItem {
        /// <summary>
        /// хранит имя диска (например: "C:\\")
        /// </summary>
        public string DriveName { get; set; }

        /// <summary>
        /// хранит имя метки для диска, установлена в системе (например: "Мой системный диск")
        /// </summary>
        public string DriveVolumeLabel { get; set; }

        /// <summary>
        /// хранит текстовое представление формата диска (NTFS или FAT32)
        /// </summary>
        public string DriveFormat { get; set; }

        /// <summary>
        /// хранит текстовое представление типа диска.
        /// См. метод GetDriveTypeAsString() для возможных значений свойства
        /// </summary>
        public string DriveTypeString { get; set; }

        /// <summary>
        /// Хранит общее свободное пространство на диске, в Гигабайтах
        /// </summary>
        public long TotalFreeSpaceGb { get; set; }

        /// <summary>
        /// Хранит общий размер диска, в Гигабайтах
        /// </summary>
        public long TotalSizeGb { get; set; }

        /// <summary>
        /// Хранит доступное свободное пространство, в Гигабайтах
        /// </summary>
        public long AvailableFreeSpaceGb { get; set; }

        /// <summary>
        /// Конструктор класса, создаёт экземпляр класса по входному параметру <paramref name="driveInfo"/>
        /// </summary>
        /// <param name="driveInfo">входной экземпляр типа DriveInfo, на основе которого необходимо создать экземпляр класса DriveInfoItem</param>
        /// <exception cref="ArgumentNullException"></exception>
        public DriveInfoItem(DriveInfo driveInfo) {
            if (driveInfo == null) {
                throw new ArgumentNullException("driveInfo", "Ошибка: параметр не может быть null!");
            }

            DriveName = driveInfo.Name;
            DriveVolumeLabel = driveInfo.VolumeLabel;
            DriveFormat = driveInfo.DriveFormat;
            DriveTypeString = GetDriveTypeAsString(driveInfo.DriveType);

            TotalFreeSpaceGb = GetSizeInGigabytes(driveInfo.TotalFreeSpace);
            TotalSizeGb = GetSizeInGigabytes(driveInfo.TotalSize);
            AvailableFreeSpaceGb = GetSizeInGigabytes(driveInfo.AvailableFreeSpace);
        }

        /// <summary>
        /// Переводит размер из байт в Гигабайты
        /// </summary>
        /// <param name="size">размер, в байтах</param>
        /// <returns>целое число, размер в Гигабайтах</returns>
        private long GetSizeInGigabytes(long size) {
            return size / 1_073_741_824;
        }

        /// <summary>
        /// Возвращает часть описания диска, связанного с общим/доступным/свободным объемом дискового пространства
        /// </summary>
        /// <returns>строка, содержащая детали по занимаемому и свободному месту на диске</returns>
        private string GetVolumeSizeString() {
            return string.Format("Объём: {0}Гб, Всего свободно: {1}Гб, Доступно: {2}Гб", TotalSizeGb, TotalFreeSpaceGb, AvailableFreeSpaceGb);
        }

        /// <summary>
        /// Переопределение метода в целях отображения текста в заданном формате в выпадающем списке с дисками на главной форме
        /// </summary>
        /// <returns></returns>
        public override string ToString() {
            return GetReadableDriveName() + ": " + DriveTypeString + ", " + DriveFormat + ", " + GetVolumeSizeString();
        }

        /// <summary>
        /// Возвращает читаемое имя диска для его представления в выпадающем списке.
        /// Если метка диска не задана, вернёт строку в формате "[имя_диска]:\\", например: "[C:\\]"
        /// Если метка диска задана, вернёт строку в формате "[имя_метки_диска] имя_диска:\\", например: "[Мой системный диск] C:\\"
        /// </summary>
        /// <returns>строка, содержащая имя диска или метку и имя диска, если метка установлена</returns>
        private string GetReadableDriveName() {
            if (DriveVolumeLabel == null || DriveVolumeLabel.Length == 0) {
                return "[" + DriveName + "]";
            }
            return "[" + DriveVolumeLabel + "] " + DriveName;
        }

        /// <summary>
        /// Возвращает текстовое представление для различных типов дисков, которые могут быть в системе
        /// </summary>
        /// <param name="driveType"></param>
        /// <returns></returns>
        private string GetDriveTypeAsString(DriveType driveType) {
            switch (driveType) {
                case DriveType.Fixed:
                    return "Фиксированный диск";
                case DriveType.Network:
                    return "Сетевой диск";
                case DriveType.Removable:
                    return "Съёмный диск";
                case DriveType.Ram:
                    return "ОЗУ";
                case DriveType.NoRootDirectory:
                    return "Без корневого каталога";
                case DriveType.CDRom:
                    return "CD-ROM";
                case DriveType.Unknown:
                default:
                    return "Неизвестно";
            }
        }
    }
}

Код класса снабжён комментариями, по которым можно понять, как он устроен, поэтому я опишу лишь ключевые моменты в структуре класса:

  1. Конструктор класса принимает экземпляр стандартного класса DriveInfo, который доступен в системном пространстве имён System.IO. Из данного экземпляра конструктор нашего класса DriveInfoItem извлечёт необходимые значения свойств и инициализирует свои внутренние свойства.
  2. Однострочный метод GetSizeInGigabytes(long size) вычисляет по размеру диска в байтах его соответствующий размер в Гигабайтах. Целое число 1_073_741_824 в делителе - это по сути 10243, поскольку 1 Гб = 1024 Мб = 1024 * ( 1024 Кб) = 1024 * 1024 * (1024 байт) = 1 073 741 824. Символом подчёркивания (англ. "underscore") в программе C# можно удобно разделять разряды длинных и очень длинных чисел, что и сделано в этом примере, для лучшей читаемости.
  3. Переопределение метода ToString() позволяет получать строковое представление для экземпляра нашего класса DriveInfoItem, это строковое представление будет необходимо, когда мы будем заполнять наш выпадающий список ComboBox со всеми дисками, доступными в системе.
  4. Метод GetReadableDriveName() - возвращает строку, которая будет содержать либо название самого диска в квадратных скобках (например, "[C:\\]"), если у диска нет метки, либо строку вида "[Мой системный диск] C:\\", если "Мой системный диск" - это установленная метка для диска C:\ в системе.
  5. Метод GetDriveTypeAsString(DriveType driveType) - просто возвращает строковое описание типа диска по типу диска, который мы получаем из свойства DriveType экземпляра системного класса DriveInfo

Нетрудно догадаться, что новый класс нам потребуется при написании кода для главной формы, что мы и сделаем далее.

4. Программируем логику главной формы

4.1 Добавление необходимых классов и свойств

Переходим к самому главному и интересному - реализации логики для нашей главной формы. В этом разделе мы напишем основной код программы и позволим ей искать файлы в выбранной директории на выбранном диске, доступном в системе.

Начнём мы с создания вложенного класса, который создадим внутри класса FrmFileFinderMain нашей главной формы. Пусть этот вложенный класс называется FileSearchInfo:

        /// <summary>
        /// Класс, задающий контекст для поиска файлов. 
        /// Содержит необходимые свойства для обмена между потоками и формой
        /// </summary>
        class FileSearchInfo {
            public long FilesTotalCount { get; set; } = 0;
            public long FilesProcessedCount { get; set; } = 0;
            public string SearchDirectory { get; set; }
            public long FilesFound { get; set; } = 0;
            public string FileNameMask { get; set; } = "";
            public List<string> FoundFiles = new List<string>();
        }

Теперь в классе главной формы также создадим enum-тип с именем BackgroundWorkerMode:

        /// <summary>
        /// Текущий режим работы BackgroundWorker элементов и рекурсивного обхода директорий: 
        /// Estimate - происходит оценка времени на поиск
        /// Search - происходит непосредственно поиск файлов по заданной маске
        /// </summary>
        enum BackgroundWorkerMode {
            Estimate,
            Search
        }

Этот тип будет задавать текущий режим работы двух элементов BackgroundWorker, которые мы ранее размещали на главной форме. И по значению переменной с этим типом мы будем понимать, в каком именно режиме программа сейчас находится: режим оценки приблизительного времени на поиск файла по маске или же непосредственно сам поиска файла.

Теперь добавим к коду главной формы следующие свойства:

        /// <summary>
        /// Запущен или нет сейчас поиск
        /// </summary>
        public bool IsSearchRunning { get; set; } = false;

        /// <summary>
        /// Контекстный объект, содержащий необходимые свойства для обмена данными между формой и потоками для объектов BackgroundWorker        
        /// </summary>
        private FileSearchInfo FileSearchInfoHolder { get; set; } = new FileSearchInfo();

Как видите, первое свойство по сути является флагом, говорящим нашей программе "запущен ли сейчас поиск?", и по умолчанию его значение false, т.е. никакого поиска не производится.

Второе свойство с именем FileSearchInfoHolder имеет тип нашего вложенного класса, который мы определили выше - FileSearchInfo. И по умолчанию мы инициализируем это свойство новым объектом с типом FileSearchInfo. Этот объект будет является "контекстом", который будет необходим для обмена данными между потоком основной программы и потоками, отвечающими за оценку времени на поиск файла и поиск файла (потоки, создаваемые элементами BackgroundWorker).

4.2 Добавление вспомогательных ("служебных") методов

Далее добавим к коду главной формы следующий метод SetIsSearchRunningAndUpdateButtonStartState с одним входным параметром isRunning типа bool:

        /// <summary>
        /// Переключает свойство IsSearchRunning в значение <paramref name="isRunning"/>, а также
        /// обновляет текст кнопки ButtonStart и её состояние
        /// </summary>
        /// <param name="isRunning">значение, которым необходимо обновить свойство IsSearchRunning</param>
        private void SetIsSearchRunningAndUpdateButtonStartState(bool isRunning) {
            if (isRunning) {
                ButtonStartSearch.Text = "&Прервать";
            } else {
                ButtonStartSearch.Text = "&Начать поиск";
                ButtonStartSearch.Enabled = true;
            }
            IsSearchRunning = isRunning;
        }

Этот метод устанавливает текст кнопки ButtonStartSearch в одно из значений: "&Прервать", если поиск был уже запущен или "&Начать поиск", если поиск пока не был запущен. Также помимо установки текста для кнопки он устанавливает и само значение для свойства IsSearchRunning главной формы.

Теперь добавим в код главной формы следующий метод StartSelectedFileUsingShellExecute, который примет на вход строку, содержащую полный путь к файлу и запустит его штатными средствами, т.е. через инициализацию экземпляра системного класса ProcessStartInfo требуемыми параметрами и передачу этого экземпляра в статический метод Start системного класса Process:

        /// <summary>
        /// Запускает выбранный файл с помощью стандартной программы и через штатные механизмы оболочки
        /// операционной системы
        /// </summary>
        /// <param name="pathToFile">путь к файлу для запуска</param>
        private void StartSelectedFileUsingShellExecute(string pathToFile) {
            ProcessStartInfo processStartInfo = new ProcessStartInfo();
            processStartInfo.FileName = pathToFile;
            processStartInfo.UseShellExecute = true;
            Process.Start(processStartInfo);
        }

Обратите внимание, что мы выставляем свойство UseShellExecute в значение true. Это означает, что для запуска процесса будет использоваться оболочка операционной системы.

Далее добавим в код для главной формы метод UpdateSearchPathReadonlyTextBox(string searchPath):

        private void UpdateSearchPathReadonlyTextBox(string searchPath) {
            if (!searchPath.Equals(TextBoxSearchPath.Text) && FileSearchInfoHolder.FilesTotalCount > 0) {
                FileSearchInfoHolder.FilesTotalCount = 0;
            }
            TextBoxSearchPath.Text = searchPath;
        }

Как можно понять из названия метода, он обновляет указанной строкой searchPath поле только для чтения, в котором хранится директория поиска файла. В этом же методе мы делаем проверку: если путь поиска был изменен, и общее количество посчитанных файлов больше 0, то мы сбросим общее количество файлов снова в 0. Почему это необходимо: как только мы изменяем директорию поиска, то это означает, что мы теряем понимание, сколько в новой директории всего вложенных файлов, а значит наш прогресс бар не сможет корректно вычислять шаг прогресса при поиске файлов в этой новой директории. Поэтому мы обнуляем счётчик FilesTotalCount в нашем контекстном объекте FileSearchInfoHolder, и это будет являться признаком, что при старте поиска нам сперва необходимо будет оценить приблизительное время поиска - через подсчёт всех вложенных файлов в выбранном новом каталоге.

Идём дальше и добавим в код главной формы следующий метод:

        /// <summary>
        /// Метод выбирает один из доступных в системе дисков в выпадающем списке по заданному пути поиска файлов.
        /// Находит в начале пути поиска <paramref name="searchPath"/> букву диска и выберет его в выпадающем списке.  
        /// </summary>
        /// <param name="searchPath">путь поиска файлов</param>
        private void SelectDriveBySearchPath(string searchPath) {
            int commaSlashPosition = searchPath.IndexOf(":\\");
            if (commaSlashPosition >= 0) {
                string driveLetterFromPath = searchPath.Substring(0, commaSlashPosition + 2);
                
                foreach (var item in ComboBoxDrives.Items) {
                    if (item is DriveInfoItem driveInfoItem) {
                        if (driveInfoItem.DriveName.Equals(driveLetterFromPath)) {
                            ComboBoxDrives.SelectedItem = item;
                            break;
                        }
                    }
                }
            } else {
                MessageBox.Show("Ошибка: невозможно найти диск, соответствующий выбранному пути", "Ошибка", MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
        }

Данный метод сделает вот что: в выпадающем списке с дисками он выберет тот диск, который соответствует выбранной директории поиска файлов. Он пытается "достать" из пути поиска начальную букву диска, а дальше перебирает все элементы выпадающего списка (т.е. диски) и, найдя нужный диск, выставит его в качестве выбранного элемента. Т.е. этот метод нужен по сути для синхронизации пути поиска и того системного диска, который этому пути соответствует.

Теперь добавим в код главной формы следующие три метода:

        private void UpdateSearchDirectoryFromSelectedDrive() {
            FileSearchInfoHolder.SearchDirectory = (ComboBoxDrives.SelectedItem as DriveInfoItem).DriveName;
            UpdateSearchPathReadonlyTextBox(FileSearchInfoHolder.SearchDirectory);
        }

        /// <summary>
        /// Загружает в выпадающий список все доступные в системе диски с краткой информацией о них
        /// </summary>
        private void LoadAvailableDrivesInfo() {
            DriveInfo[] driveInfos = DriveInfo.GetDrives();
            foreach (var driveInfo in driveInfos) {
                ComboBoxDrives.Items.Add(new DriveInfoItem(driveInfo));                
            }
            ComboBoxDrives.SelectedIndex = 0;
        }

        /// <summary>
        /// Метод запускает поиск файлов по имени файла (маске файла)
        /// </summary>
        private void StartSearchFilesByFileName() {
            LabelProgress.Text = "Поиск файла по маске *" + FileSearchInfoHolder.FileNameMask + "* в каталоге '" + FileSearchInfoHolder.SearchDirectory + "'...";
            LabelFilesCount.Visible = false;            
            ProgressBarMain.Style = ProgressBarStyle.Continuous;
            FileSearchInfoHolder.FilesFound = 0;
            FileSearchInfoHolder.FilesProcessedCount = 0;
            SetIsSearchRunningAndUpdateButtonStartState(true);
            BackgroundWorkerSearchFiles.RunWorkerAsync(FileSearchInfoHolder);
        }

Кратко их назначение:

  • UpdateSearchDirectoryFromSelectedDrive() - установит директорию поиска контекстного объекта (свойство FileSearchInfoHolder.SearchDirectory) в значение, соответствующее текущему выбранному диску из доступных в выпадающем списке. Метод также обновит текстовое поле с текущей директорией поиска
  • LoadAvailableDrivesInfo() - будет вызываться при запуске программы. Он получает информацию обо всех доступных дисках в системе и добавляет их в выпадающий список. Обратите внимание, что в метод Add мы передаем в цикле новый экземпляр нашего класса DriveInfoItem, и при создании этого экземпляра в его конструктор мы передаем сам инстанс системного класса DriveInfo
  • StartSearchFilesByFileName() - как следует из его названия, начинает поиск файлов по имени файла. Он также сбрасывает счётчики ранее найденных файлов и обработанных файлов, меняет стиль прогресс бара на непрерывный, вызывает ранее добавленный метод, включающий режим поиска файлов, а также запускает асинхронно работу того BackgroundWorker элемента, который производит непосредственно поиск файлов.
4.3. Добавление основного метода для рекурсивного обхода директорий и оценки времени поиска / непосредственно поиска файлов

Теперь, когда различные вспомогательные методы добавлены в код главной формы, настало время реализовать ключевой для логики поиска метод, который будет выполнять рекурсивный обход директорий системы (начиная с заданной "родительской" директории).

Ниже представлен код этого метода, который мы назовём CalculateFilesCountRecursively:

        /// <summary>
        /// Выполняет рекурсивный обход директорий, начиная с родительской директории <paramref name="parentDirectory"/>.
        /// Может работать в двух режимах:
        /// 1) подсчёт общего количества вложенных файлов внутри родительской директории <paramref name="parentDirectory"/>,
        /// 2) поиск в родительской директории <paramref name="parentDirectory"/> файла по заданной маске
        /// </summary>
        /// <param name="parentDirectory">родительская директория, с которой необходимо начать рекурсивный обход вложенных директорий и файлов</param>
        /// <param name="workerMode">режим работы объектов BackgroundWorker: Estimate - оценка времени поиска в каталоге, Search - сам поиск</param>
        /// <param name="fileInfoHolder">контекстный объект, содержащий необходимые свойства для обеспечения оценки поиска и самого поиска</param>
        private void CalculateFilesCountRecursively(string parentDirectory, BackgroundWorkerMode workerMode, FileSearchInfo fileInfoHolder) {
            try {
                IEnumerable<string> subdirectories = Directory.EnumerateDirectories(parentDirectory, "*", SearchOption.TopDirectoryOnly);
                IEnumerable<string> files = Directory.EnumerateFiles(parentDirectory);

                if (workerMode == BackgroundWorkerMode.Estimate) {
                    // если было запрошено прерывание операции оценки времени поиска - выходим из рекурсии
                    if (BackgroundWorkerEstimateSearchTime.CancellationPending) {
                        return;
                    }

                    fileInfoHolder.FilesTotalCount += files.LongCount();
                    BackgroundWorkerEstimateSearchTime.ReportProgress(10);                                        
                } else if (workerMode == BackgroundWorkerMode.Search) {
                    
                    // если было запрошено прерывание операции поиска - выходим из рекурсии
                    if (BackgroundWorkerSearchFiles.CancellationPending) {
                        return;
                    }

                    foreach (string file in files) {
                        if (file.Contains(fileInfoHolder.FileNameMask)) {
                            fileInfoHolder.FoundFiles.Add(file);
                            FileSearchInfoHolder.FilesFound++;
                        }
                    }

                    List<string> foundFiles = new List<string>(fileInfoHolder.FoundFiles);

                    fileInfoHolder.FilesProcessedCount += files.LongCount();
                    int progress = (int)(fileInfoHolder.FilesProcessedCount * 100 / fileInfoHolder.FilesTotalCount);                    
                    BackgroundWorkerSearchFiles.ReportProgress(progress, foundFiles);
                    fileInfoHolder.FoundFiles.Clear();
                }

                if (subdirectories.LongCount() > 0) {
                    foreach (string subdirectory in subdirectories) {
                        CalculateFilesCountRecursively(subdirectory, workerMode, fileInfoHolder);
                    }
                }
            } catch (UnauthorizedAccessException unauthorizedAccessException) {
                // TODO: обработать исключение при необходимости...
            } catch (DirectoryNotFoundException directoryNotFoundException) {
                // TODO: обработать исключение при необходимости...
            } catch (Exception otherException) {
                // TODO: обработать исключение при необходимости...
            }
        }

Разберём логику этого метода.

Во-первых, он сразу начинается с конструкции try ... catch - она позволяет обработать ошибки, которые могут возникать в процессе рекурсивного просмотра директорий и работы с файлами внутри этих директорий, а конкретно в нашем случае мы "ловим" исключения типов UnauthorizedAccessException, DirectoryNotFoundException и Exception. Ошибки могут возникать, например, из-за попытки неавторизованного доступа к ресурсу (файлу/директории). Какие-то каталоги могут оказаться системными, и доступ к ним будет запрещён, либо могут возникнуть какие-то ошибки ввода-вывода, связанные с безопасностью. Кроме этого, какой-то путь может оказаться слишком длинным для файловой системы.

Чтобы наш поиск и наша программа не "развалились" при описанных ситуациях и исключениях, необходимо обработать исключения и отреагировать на них должным образом. У нас демонстрационное приложение, поэтому в секциях catch я оставил везде комментарии TODO. При желании можно реализовать логику в этих блоках, но это выходит за рамки текущей статьи.

В самом блоке try мы запрашиваем перечисление директорий и файлов, находящихся внутри текущей родительской директории parentDirectory, которая в процессе рекурсивных вызовов будет меняться, уходя "вглубь" каталогов файловой системы. На старте работы нашего алгоритма parentDirectory равна той самой директории, которую мы и выберем в интерфейсе программы, и именно там мы будем искать файлы.

Следом идёт условный оператор if (...) else if (...), где мы проверяем текущий режим работы элементов BackgroundWorker и рекурсивного просмотра директорий на одно из возможных значений (это и есть блоки кода для веток if/else if):

  • Режим Estimate - означает, что мы не выполняем сам поиск файлов, а лишь подсчитываем общее количество всех вложенных файлов внутри родительской директории parentDirectory. Само собой, включая также и вложенные каталоги, находящиеся внутри parentDirectory. Обратите внимание, что мы проверяем специальный флаг CancellationPending в соответствующем объекте BackgroundWorker, который нужен для оценки времени поиска. Если флаг выставлен, то мы тут же возвращаемся из метода, завершая на этом рекурсивный просмотр вложенных каталогов. Если же флаг прерывания работы не был выставлен, то мы прибавляем количество найденных файлов к счётчику всех файлов (FilesTotalCount) и оповещаем основной поток программы о ходе прогресса, вызывая метод ReportProgress объекта BackgroundWorker со значением 10. Почему 10? Это просто произвольное значение "из головы", которое всегда больше 0, но меньше 100. Поскольку в режиме Estimate у нас прогресс бар работает в режиме Marquee (т.е. он всегда показывает неопределённый "вечный" индикатор прогресса), то нам достаточно самого факта, что произошёл какой-то прогресс, и в данном случае несущественно, что у нас это будет всегда значение 10.
  • Режим Search - означает, что мы ранее уже оценили общее количество файлов внутри родительской директории, и теперь нам нужно просто перебирать все файлы внутри родительской директории, а в имени и пути каждого файла искать заданную в интерфейсе программы подстроку (имя файла, часть имени файла или часть пути к файлу). Что мы и делаем в цикле foreach по найденным файлам. Внутри цикла как раз условие if, где мы проверяем, содержит ли полный путь и имя текущего файла заданную на главной форме маску поиска. В случае, если файл нам подходит, и он должен попасть в результаты поиска, то мы его добавляем в список FoundFiles нашего контекстного объекта, а также увеличиваем счётчик найденных файлов FilesFound на единицу. Обратите внимание, что после цикла по файлам мы создаём отдельный новый список foundFiles и помещаем в него все файлы из списка FoundFiles контекстного объекта. Почему нужен новый список? Потому, что мы хотим передать промежуточный результат с найденными на текущем шаге файлами через вызов метода ReportProgress объекта BackgroundWorker, который отвечает за поиск файлов. Если мы передадим просто список FoundFiles текущего контекстного объекта, то мы получим исключение во время работы программы, поскольку со списком FoundFiles будет работать одновременно 2 потока: главный поток программы и поток объекта BackgroundWorker, в результате чего между ними возникнет конфликт одновременного доступа к списку и его модификации. Далее мы наращиваем счётчик FilesProcessedCount - на то количество файлов, которое было найдено в текущей родительской директории parentDirectory. Следующей инструкцией мы вычисляем progress - процент прогресса для нашего прогресс бара. Для этого используем несложную формулу, по сути  это обычная пропорция: <% всей выполненной работы, т.е. поиска файлов> = <количество уже обработанных файлов> * 100 / <общее количество всех файлов в директории поиска, вычисленное на этапе оценки, когда отработал соответствующий BackgroundWorker>. Когда процент вычислен, остаётся вызвать метод ReportProgress для объекта BackgroundWorker, отвечающего за поиск файлов, передав ему 2 аргумента: progress (само значение прогресса всей выполненной "работы" по поиску) и foundFiles (список всех найденных файлов на текущем шаге). Оперируя двумя этими параметрами, основной поток программы сможет сделать две основные вещи: увеличить процент заполнения прогресс бара, а также добавить найденные файлы сразу в элемент ListView для их последующего просмотра.

После if (...) else if (...) оператора идёт проверка - есть ли вложенные директории внутри текущей родительской (parentDirectory)? Если да, то здесь и возникает рекурсия (т.е. рекурсивный вызов метода): мы должны "бежать" по всем вложенным директориям и вызвать "себя", т.е. тот же метод CalculateFilesCountRecursively, в котором мы сейчас находимся. Только на этот раз родительской директорией станет текущая вложенная директория, а вот режим workerMode и контекстный объект fileInfoHolder у нас не меняются на протяжении всей рекурсии:

                if (subdirectories.LongCount() > 0) {
                    foreach (string subdirectory in subdirectories) {
                        CalculateFilesCountRecursively(subdirectory, workerMode, fileInfoHolder);
                    }
                }
4.4. Реализация обработки событий для элементов управления главной формы

На данном этапе у нас реализованы все необходимые методы, которые не являются методами-обработчиками событий от элементов интерфейса главной формы.

Теперь перейдем к обработке событий для формы.

Начнём с события загрузки главной формы - реализуем событие Load для формы:

        private void FrmFileFinderMain_Load(object sender, EventArgs e) {
            LabelProgress.Visible = false;
            LabelFilesCount.Visible = false;
            ProgressBarMain.Visible = false;
            this.DoubleBuffered = true;

            LoadAvailableDrivesInfo();
            UpdateSearchDirectoryFromSelectedDrive();
        }

Как видим, по умолчанию при загрузке главной формы мы спрячем метку с ходом выполнения прогресса (LabelProgress), метку количества найденных файлов (LabelFilesCount), сам прогресс бар, а также установим двойную буферизацию для главной формы для более плавной её прорисовки, исключающей не очень приятный эффект "мерцания" при частых операциях перерисовки главного окна. Также мы загружаем доступные системные диски и устанавливаем директорию поиска по умолчанию.

Следующий шаг - реализуем событие DoWork для обоих элементов BackgroundWorker. Как помним, один у нас отвечает за примерную оценку времени поиска (т.е. подсчёт общего количества вложенных файлов внутри директории поиска), а второй - за сам поиск.

Начнём с элемента BackgroundWorkerEstimateSearchTime и реализуем обработку его события DoWork:

        /// <summary>
        /// Метод для выполнения основной работы для элемента BackgroundWorker, отвечающего за оценку времени поиска в заданной директории.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void BackgroundWorkerEstimateSearchTime_DoWork(object sender, DoWorkEventArgs e) {
            if (e.Argument is FileSearchInfo fileInfo) {
                if (BackgroundWorkerEstimateSearchTime.CancellationPending) {
                    e.Cancel = true;                    
                } else {
                    CalculateFilesCountRecursively(FileSearchInfoHolder.SearchDirectory, BackgroundWorkerMode.Estimate, fileInfo);
                    if (BackgroundWorkerEstimateSearchTime.CancellationPending) {
                        e.Cancel = true;
                    }
                }                
            }            
        }

И также реализуем для второго элемента BackgroundWorkerSearchFiles:

        private void BackgroundWorkerSearchFiles_DoWork(object sender, DoWorkEventArgs e) {
            if (e.Argument is FileSearchInfo fileInfo) {
                if (BackgroundWorkerSearchFiles.CancellationPending) {
                    e.Cancel = true;
                } else {
                    CalculateFilesCountRecursively(FileSearchInfoHolder.SearchDirectory, BackgroundWorkerMode.Search, fileInfo);
                    if (BackgroundWorkerSearchFiles.CancellationPending) {
                        e.Cancel = true;
                    }
                }                
            }
        }

По сути обработка события DoWork представляет собой "метод выполнения некоторой полезной работы", т.е. некоторый программный код, который и выполняется объектом BackgroundWorker. Но поскольку в нашей программе-примере у нас рекурсивный алгоритм поиска файлов в заданной директории, то мы дополнительно перейдем из основного рабочего метода DoWork в наш рекурсивный метод CalculateFilesCountRecursively.

У метода-обработчика события DoWork во втором параметре e есть свойство Argument. Оно используется для передачи в рабочий метод DoWork некоторого дополнительного параметра, который будет использоваться в логике работы рабочего метода DoWork.

В нашем случае это главный контекстный объект формы с типом FileSearchInfo. Также заметьте, что на каждом шаге мы проверяем флаг отмены работы (CancellationPending) для каждого из объектов BackgroundWorker. В случае, если этот флаг отмены был установлен, то мы отменяем события (или нашу "работу по поиску файлов"), устанавливая e.Cancel = true. Что является у нас признаком отмены работы? Верно, нажатие на кнопку "Прервать", т.к. она как раз предназначена для отмены подсчёта файлов (если мы на этапе оценки) или для отмены самого поиска файлов.

Теперь реализуем для обоих объектов BackgroundWorker обработку события RunWorkerCompleted. Событие вызывается, когда "вся работа завершена", т.е. когда BackgroundWorker успешно завершил исполнение всех инструкций в DoWork и отмены работы не было, либо когда "работа была отменена", т.е. в процессе выполнения работы была произведена отмена. Признак отмены работы - установка флага e.Cancelled, и мы его будем проверять при обработке события.

Для объекта BackgroundWorkerEstimateSearchTime, который отвечает за оценку времени поиска, обработка события выглядит следующим образом:

        private void BackgroundWorkerEstimateSearchTime_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) {
            if (e.Cancelled) {
                // Оценка времени поиска завершилась с прерыванием. Выводим сообщение об этом, сбрасываем счётчики,
                // скрываем метку с количеством файлов и делаем невидимым прогресс бар
                SetIsSearchRunningAndUpdateButtonStartState(false);

                LabelProgress.Text = "Оценка времени поиска была прервана.";
                FileSearchInfoHolder.FilesTotalCount = 0;
                FileSearchInfoHolder.FilesFound = 0;
                ProgressBarMain.Visible = false;
                LabelFilesCount.Text = "0";
                LabelFilesCount.Visible = false;
            } else {
                // Оценка времени поиска завершилась без прерывания. Значит, запускаем непосредственно поиск файлов по маске
                StartSearchFilesByFileName();
            }            
        }

Комментарии о том, что происходит в методе, есть в коде, поэтому детальнее останавливаться здесь не буду.

Для второго объекта BackgroundWorkerSearchFiles, который отвечает за поиск файлов, обработка события следующая:

        private void BackgroundWorkerSearchFiles_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) {
            SetIsSearchRunningAndUpdateButtonStartState(false);

            if (e.Cancelled) {
                // Операция поиска файлов была прервана
                LabelProgress.Text = "Операция поиска прервана.";
                ProgressBarMain.Visible = false;
                LabelFilesCount.Text = "0";
                LabelFilesCount.Visible = false;
            } else {
                // Поиск завершился штатно, без прерывания
                LabelProgress.Text = "Поиск по маске *" + FileSearchInfoHolder.FileNameMask + "* в каталоге '" + FileSearchInfoHolder.SearchDirectory + "' завершён. Найдено файлов: ";
                LabelFilesCount.Left = LabelProgress.Right + 10;
                LabelFilesCount.Text = FileSearchInfoHolder.FilesFound.ToString();
                LabelFilesCount.Visible = true;
            }            
        }

Теперь реализуем обработку события ProgressChanged для обоих объектов BackgroundWorker. Событие вызывается, когда изменился прогресс по выполняемой работе объектом BackgroundWorker. Я приведу оба метода-обработчика для наших BackgroundWorker объектов в одном сниппете кода ниже:

        private void BackgroundWorkerEstimateSearchTime_ProgressChanged(object sender, ProgressChangedEventArgs e) {
            LabelFilesCount.Text = FileSearchInfoHolder.FilesTotalCount.ToString();
        }

        /// <summary>
        /// Событие обработки изменения прогресса для элемента BackgroundWorker, отвечающего за поиск файлов.
        /// Обновляет значение прогресс бара и добавляет в элемент ListViewFoundFiles все найденные файлы.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void BackgroundWorkerSearchFiles_ProgressChanged(object sender, ProgressChangedEventArgs e) {
            ProgressBarMain.Value = e.ProgressPercentage;
            List<string> foundFiles = (List<string>)e.UserState;

            ListViewGroup group = ListViewFoundFiles.Groups["listViewGroupFiles"];

            foreach (string fileName in foundFiles) {
                long fileSizeInBytes = -1;
                try {
                    FileInfo fileInfo = new FileInfo(fileName);
                    fileSizeInBytes = fileInfo.Length;
                } catch (FileNotFoundException fileNotFoundException) {
                    //TODO: обработать исключение при необходимости...
                }
                
                ListViewItem value = new ListViewItem(new string[] { fileName, fileSizeInBytes.ToString() }, 0, group);
                ListViewFoundFiles.Items.Add(value);
            }

            FileSearchInfoHolder.FoundFiles.Clear();            
        }

Видим, что обработка события для элемента BackgroundWorkerEstimateSearchTime - в одну строку. Мы просто обновляем текст метки со счётчиком всех файлов внутри директории поиска.

Обработка события для элемента BackgroundWorkerSearchFiles заключается, прежде всего, в обновлении текущего значения прогресс бара. Также мы должны добавить в список ListViewFoundFiles все найденные файлы за текущую итерацию прогресса. Обратите внимание, что список найденных файлов передаётся в свойстве e.UserState (тип object), который мы явно приводим к типу List<string>. Дальше остаётся пробежаться циклом по найденным файлам, для каждого вычислить его размер в байтах (с помощью стандартного класса FileInfo) и добавить очередной найденный файл в список.

В завершение мы должны очистить список файлов FoundFiles в контекстном объекте.

Теперь обработаем событие TextChanged для текстового поля с именем искомого файла:

        /// <summary>
        /// Обработка изменения события изменения текста в текстовом поле TextBoxFileName.
        /// Необходимо делать доступной кнопку поиска, если маска поиска для файлов задана и недоступной, 
        /// если поле пустое или содержит лишь пробелы.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void TextBoxFileName_TextChanged(object sender, EventArgs e) {
            ButtonStartSearch.Enabled = !"".Equals(TextBoxFileName.Text.Trim());
        }

Следующим шагом обработаем событие Click - нажатие на кнопку ButtonStartSearch ( "Начать поиск" / "Прервать" ):

        /// <summary>
        /// Обработка нажатия на кнопку "Начать поиск" / "Прервать"
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void ButtonStartSearch_Click(object sender, EventArgs e) {
            if (IsSearchRunning) {
                // Поиск уже запущен - прервать
                
                // Запретить повторные нажатия на кнопку "Прервать"
                ButtonStartSearch.Enabled = false;

                // Асинхронная отмена работы BackgroundWorker-ов
                if (BackgroundWorkerEstimateSearchTime.IsBusy) {
                    BackgroundWorkerEstimateSearchTime.CancelAsync();
                }
                if (BackgroundWorkerSearchFiles.IsBusy) {
                    BackgroundWorkerSearchFiles.CancelAsync();
                }
            } else {
                // Поиск не запущен - запустить оценку времени поиска или сам поиск
                FileSearchInfoHolder.FileNameMask = TextBoxFileName.Text;
                ProgressBarMain.Value = 0;
                ListViewFoundFiles.Groups.Clear();
                ListViewFoundFiles.Groups.Add(new ListViewGroup("listViewGroupFiles", "Найденные файлы"));

                FileSearchInfoHolder.FoundFiles.Clear();

                ProgressBarMain.Visible = true;

                if (FileSearchInfoHolder.FilesTotalCount == 0) {
                    ProgressBarMain.Style = ProgressBarStyle.Marquee;
                    LabelProgress.Text = "Подсчёт количества файлов в системе и оценка примерного времени... Найдено файлов:";
                    LabelProgress.Visible = true;
                    LabelFilesCount.Visible = true;
                    LabelFilesCount.Left = LabelProgress.Right + 10;                    
                    SetIsSearchRunningAndUpdateButtonStartState(true);
                    BackgroundWorkerEstimateSearchTime.RunWorkerAsync(FileSearchInfoHolder);
                } else {                    
                    StartSearchFilesByFileName();
                }
            }
        }

По сути основной смысл обработки - это проверить запущен или нет поиск. Если уже запущен, значит мы нажали "Прервать" для его остановки. Если не был запущен, то мы должны очистить результаты предыдущего поиска - т.е. почистить список ранее найденных файлов, почистить свойство FoundFiles контекстного объекта, сделать видимым прогресс бар и далее, в зависимости от того, чему равно значение FilesTotalCount, мы либо запускаем сразу сам поиск (если оно больше 0, значит, мы уже ранее оценивали общее количество файлов в каталоге), либо запускаем оценку времени поиска.

Теперь напишем обработчик нажатия на кнопку "Обзор...", которая должна показать стандартный диалог для выбора директории:

        /// <summary>
        /// Обработка нажатия на кнопку "Обзор..." - для выбора директории, в которой необходимо производить поиск файлов
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void ButtonSelectSearchDirectory_Click(object sender, EventArgs e) {
            DialogResult dialogResult = FolderBrowserDialogSelectSearchDirectory.ShowDialog();
            if (dialogResult == DialogResult.OK) {
                string selectedPath = FolderBrowserDialogSelectSearchDirectory.SelectedPath;
                if (!selectedPath.EndsWith("\\")) {
                    selectedPath += "\\";
                }                
                FileSearchInfoHolder.SearchDirectory = selectedPath;
                UpdateSearchPathReadonlyTextBox(selectedPath);
                SelectDriveBySearchPath(selectedPath);                               
            }
        }

Метод проверяет - был ли диалог выбора директории закрыт нажатием на кнопку "ОК", и если да, значит это подтверждение выбора каталога, поэтому мы в переменной selectedPath при необходимости добавляем завершающий символ , после чего устанавливаем выбранный путь в качестве целевого пути поиска, а также меняем в выпадающем списке на форме диск, содержащий этот путь поиска.

Реализуем обработчик DoubleClick двойного клика на списке найденных файлов. Мы хотим, чтобы по двойному клику у нас файл открылся ассоциированной с расширением файла программой. Поэтому мы получаем в selectedItem выбранный в списке элемент и передаём его в ранее написанный метод StartSelectedFileUsingShellExecute:

        /// <summary>
        /// Обработка двойного клика по одному из найденных файлов - для запуска стандартной программы, 
        /// которая сможет открыть файл
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void ListViewFoundFiles_DoubleClick(object sender, EventArgs e) {
            var selectedItems = ListViewFoundFiles.SelectedItems;
            if (selectedItems.Count > 0) {
                var selectedItem = selectedItems[0];
                StartSelectedFileUsingShellExecute(selectedItem.Text);
            }
        }

Последний обработчик, который нам осталось реализовать, - это обработка события SelectedIndexChanged для элемента ComboBoxDrives. Оно будет происходить при смене диска в выпадающем списке, и в этом случае мы просто изменим целевую директорию для поиска файлов на ту, что соответствует корневому каталогу выбранного диска:

        /// <summary>
        /// Изменение пути поиска при перевыборе диска в выпадающем списке
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void ComboBoxDrives_SelectedIndexChanged(object sender, EventArgs e) {
            var selectedItem = ComboBoxDrives.SelectedItem;
            if (selectedItem is DriveInfoItem driveInfoItem) {
                string selectedPath = driveInfoItem.DriveName;
                FileSearchInfoHolder.SearchDirectory = selectedPath;
                UpdateSearchPathReadonlyTextBox(selectedPath);
            }
        }

На этом этапе весь код программы для поиска файлов полностью готов, и можно проверить программу в работе. Можете её запустить, выбрать какой-то каталог и ввести, например, часть имени файла или расширение файла. Нажмите кнопку "Начать поиск" и посмотрите, как программа будет себя вести.

Спасибо за внимание, пишите свои вопросы, если они вдруг возникли, в комментариях к данной статье.

Ссылка на готовый архив с программой, разработанной в рамках данной статьи:

https://allineed.ru/our-products/download/4-allineed-ru-examples/24-file-finder-example

 

 

Яндекс.Метрика