• Управление файловой системой 
  • Классы .NET, представляющие файлы и папки
  • Класс Path
  • Пример: файловый браузер
  • Перемещение, копирование и удаление файлов
  • Пример FilePropertiesAndMovement
  • Чтение и запись файлов
  • Потоки 
  • Чтение и запись двоичных файлов
  • Класс FileStream
  • Пример: объект чтения двоичного файла
  • Чтение и запись текстовых файлов
  • Класс StreamReader
  • Класс StreamWriter
  • Пример: ReadWriteText
  • Чтение и запись в реестр
  • Реестр
  • Классы реестра в .NET
  • Пример: SelfPlacingWindow
  • Заключение
  • Глава 14

    Операции с файлами и реестром

    В этой главе будет рассмотрено выполнение в C# задач, включающих чтение и запись в файлы и в системный реестр. В частности, будут охвачены следующие вопросы:

    □ Исследование структуры каталога, выяснение, какие файлы и папки присутствуют и проверка их свойств

    □ Перемещение, копирование и удаление файлов и папок

    □ Чтение и запись текста в и из файлов

    □ Чтение и запись в реестр

    Компания Microsoft предоставила очень интуитивную объектную модель, охватывающую эти области, и в ходе этой главы мы покажем, как использовать классы на основе .NET для выполнения упомянутых выше задач. Для случая операций с файловой системой соответствующие классы почти все находятся в пространстве имен System.IO, в то время как операции с реестром связаны с парой классов в пространстве имен System.Win32.

    Классы на основе .NET включают также ряд классов и интерфейсов в пространстве имен

    System.Runtime.Serilization
    , которые связаны с сериализацией, то есть процессом преобразования некоторых данных (например, содержимого документа) в поток байтов для хранения в каком либо месте. Мы не будем рассматривать эти классы в данной главе, так как сосредоточимся на классах, которые предназначены для прямого доступа к файлам.

    Отметим, что хотя безопасность влияет на все области, но особенно она важна при модификации файлов или записей реестра. Система безопасности рассматривается отдельно в главе 25. В этой главе мы будем предполагать, что имеются достаточные права доступа для выполнения всех примеров, которые модифицируют файлы или записи реестра, что обеспечивается, например, при использовании учетной записи с полномочиями администратора.

    Управление файловой системой 

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

    Назначение этих классов следующее:

    □ 

    System.MarshalByRefObject
    — класс базового объекта для классов .NET, которые являются удаленными. Допускает маршализацию данных между прикладными доменами.

    FileSystemInfo
    — базовый класс, который представляет любой объект файловой системы.

    FileInfo
    и
    File
    — представляют файл в файловой системе.

    DirectoryInfo
    и
    Directory
    — представляют папку в файловой системе.

    Path
    — содержит статические члены, которые можно использовать для манипуляций именами путей доступа.

    Отметим, что в Windows объекты, которые содержат файлы и используются для организации файловой системы, называются папками. Например, в пути доступа

    C:\My Documents\ReadMe.txt
    файлом является
    ReadMe.txt
    , а
    My Documents
     — папкой. Папка (folder) является специфическим для Windows терминам: практически во всех других операционных системах вместо папки используется термин каталог (directory). В соответствии с желанием Microsoft, чтобы .NET была максимально независимой от операционной системы, соответствующие базовые классы .NET называются
    Directory
    и
    DirectoryInfo
    . Однако в связи с возможной путаницей с каталогами LDAP (обсуждаемыми в главе 15), и в связи с тем, что эта книга посвящена Windows, здесь используется термин папка.

    Классы .NET, представляющие файлы и папки

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

    □ 

    Directory
    и
    File
    содержат только статические методы и никогда не создают экземпляров. Эти классы применяются с помощью предоставления пути доступа к соответствующему объекту файловой системы, когда вызывается метод-член. Если нужно выполнить только одну операцию на папке или файле, то эти классы более эффективны, так как не требуют создания экземпляра класса .NET.

    □ 

    DirectoryInfo
    и
    FileInfo
    реализуют приблизительно те же открытые методы, что и
    Directory
    и
    File
    , а также некоторые открытые свойства и конструкторы, но они имеют состояние и члены этих классов не являются статическими. На самом деле необходимо создать экземпляры этих классов, и при этом каждый экземпляр ассоциируется с определенной папкой или файлом. Это означает, что эти классы являются более эффективными, если выполняется несколько операций с помощью одного и того же объекта, так как они будут считывать аутентификационные и другие данные при создании соответствующего объекта файловой системы, а затем им не понадобиться больше считывать эту информацию, независимо от того, сколько методов вы будете вызывать на каждом объекте (экземпляре класса). Для сравнения, соответствующим классам без состояния понадобиться снова проверять данные файла или папки для каждого вызываемого метода.

    В этом разделе мы будем в основном использовать классы

    FileInfo
    и
    DirectoryInfo
    , но оказывается, что многие (но не все) вызываемые методы реализуются также классами
    File
    и
    Directory
    (хотя в этих случаях эти методы требуют дополнительный параметр, имя пути доступа объекта файловой системы и пара методов имеют немного отличные имена). Например:

    FileInfo MyFile = new FileInfo(@"C:\Program Files\My Program\ReadMe.txt");

    MyFile.CopyTo(@"D:\Copies\ReadMe.txt");

    Имеет тот же результат, что и:

    File.Copy(@"C:\Program Files\My Program\ReadMe.txt", @"D:\Copies\ReadMe.txt");

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

    FileInfo
    MyFile
    , но он оставляет
    MyFile
    готовым для выполнения дальнейших действий на том же файле.

    Для создания экземпляра класса

    FileInfo
    или
    DirectoryInfo
    в конструктор передается строка, содержащая путь доступа к соответствующей файловой системе. Мы только что проиллюстрировали процесс для файла. Для папки код выглядит аналогично:

    DirectoryInfo MyFolder = new DirectoryInfo(@"C:\Program Files");

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

    FileInfo Test = new FileInfo(@"C:\Windows");

    Console.WriteLine(Test.Exists.ToString());

    Console.WriteLine(Test.CreationTime.ToString());

    Отметим, что для того, чтобы это свойство возвращало

    true
    , соответствующий объект файловой системы должен быть соответствующего типа. Другими словами, если создается экземпляр объекта
    FileInfo
    , содержащий путь доступа папки или, если создается объект
    DirectoryInfo
    , задающий путь доступа файла,
    Exists
    будет иметь значение
    false
    . С другой стороны, большинство свойств и методов этих объектов будут возвращать значение, если вообще это возможно. Но они не обязательно порождают исключение из-за того, что был вызван неправильный тип объекта, а только в том случае, если требовалось выполнить что-то реально невозможное. Например, приведенный выше фрагмент кода сначала выведет
    false
    (так как
    C:\Windows
    является папкой), но затем все равно правильно покажет время создания папки, так как в папке имеется эта информация. С другой стороны, если затем попробовать открыть папку, как если бы это был файл, с помощью метода
    FileInfo.Open()
    , то будет порождено исключение.

    После того, как определено, что соответствующий объект файловой системы существует, можно (если используется класс

    FileInfo
    или
    DirectoryInfo
    ) найти о нем информацию, используя ряд свойств, включающих:

    Имя Назначение
    CreationTime
    Время создания файла или папки.
    DirectoryName(FileInfo)
    ,
    Parent(DirectoryInfo)
    Полный путь доступа содержащей папки.
    Exists
    Существует ли файл или папка.
    Extension
    Расширение файла. Возвращается пустым для папок.
    FullName
    Полное имя пути доступа файла или папки.
    LastAccessTime
    Время последнего доступа к файлу или папке.
    LastWriteTime
    Время последней модификации файла или папки.
    Name
    Имя файла или папки.
    Root
    (Только
    DirectoryInfo
    .) Корневая часть пути доступа.
    Length
    (Только
    FileInfo
    .) Возвращает размер файла в байтах.

    Можно также выполнить действия на объекте файловой системы с помощью следующих методов:

    Имя Назначение
    Create()
    Создает папку или пустой файл с заданным именем. Для
    FileInfo
    он возвращает также объект потока, чтобы позволить записать в файл. Потоки будут рассмотрены позже.
    Delete()
    Удаляет файл или папку. Для папок существует вариант рекурсивного метода
    Delete
    .
    MoveTo()
    Перемещает и/или переименовывает файл или папку.
    CopyTo()
    (Только
    FileInfo
    .) Копирует файл. Отметим, что не существует метода копирования для папок. Если копируются все деревья каталогов, то необходимо индивидуально скопировать каждый файл и создать новые папки, соответствующие старым папкам.
    GetDirectories()
    (Только
    DirectoryInfo
    .) Возвращает массив объектов
    DirectoryInfo
    , представляющих все папки, содержащиеся в этой папке.
    GetFiles()
    (Только
    DirectoryInfo
    .) Возвращает массив объектов
    FileInfo
    , представляющих все папки, содержащиеся в этой папке.
    GetFileSystemObjects()
    (Только
    DirectoryInfo
    .) Возвращает объекты
    FileInfo
    и
    DirectoryInfo
    , представляющие все объекты, содержащиеся в этой папке, как массив ссылок
    FileSystemInfo
    .

    Отметим, что приведенные выше таблицы показывают основные свойства и методы, и не являются исчерпывающими.

    В приведенных выше таблицах не перечислены большинство свойств или методов, которые позволяют записывать или читать данные в файлах. Это в действительности делается с помощью потоковых объектов, которые будут рассмотрены позже.

    FileInfo
    реализует также ряд методов (
    Open()
    ,
    OpenRead()
    ,
    OpenText()
    ,
    OpenWrite()
    ,
    Create()
    ,
    CreateText()
    , которые возвращают объекты потоков для этой цели).

    Интересно то, что время создания, время последнего доступа, и время последней записи являются изменяемыми:

    // Test является FileInfo или DirectoryInfo. Задать время создания

    // как 1 Jan 2001, 7.30 am

    Test.CreationTime = new DateTime(2001, 1, 1, 7, 30, 0);

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

    Класс Path

    Класс

    Path
    не является классом, экземпляры которого будут создаваться. Скорее он предоставляет некоторые статические методы, которые облегчают работу с путями доступа. Например, предположим, что необходимо вывести имя полного пути доступа для файла
    ReadMe.txt
    в папке
    C:\My Documents
    . Путь доступа к файлу можно найти с помощью следующей операции:

    Console.WriteLine(Path.Combine(@"C:\My Documents", "ReadMe.txt"));

    Использовать класс Path значительно проще, чем пытаться справиться с символами-разделителями вручную, потому что класс

    Path
    знает различные форматы имен путей доступа в различных операционных системах. Во время написания книги Windows являлась единственной операционной системой, поддерживаемой .NET, но если, например, .NET будет в дальнейшем перенесена на Unix, то
    Path
    сможет справиться с путями доступа Unix, где в качестве разделителя в именах путей доступа используется /, а не \.
    Path.Combine
    является методом этого класса, который будет вероятно использоваться чаще всего, но
    Path
    реализует также другие методы, которые предоставляют информацию о пути доступа или требуемом для него формате.

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

    Пример: файловый браузер

    В этом разделе представлен пример приложения C#, называемого

    FileProperties
    .
    FileProperties
    представляет простой интерфейс пользователя, который позволяет перемещаться по файловой системе и видеть время создания время последнего доступа, время последней записи и размер файлов.

    Приложение

    FileProperties
    выглядит следующим образом. Пользователь вводит имя папки или файла в основное текстовое поле в верхней части окна и нажимает кнопку
    Display
    . Если вводится путь доступа к папке, ее содержимое выводится в окне списка. Если ввести путь доступа к файлу, то данные о нем появятся в текстовых полях в нижней части формы, а содержимое папки — в окне списка. На экране показано приложение
    FileProperties
    , используемое для просмотра папки:

    Пользователь может очень легко перемещаться по файловой системе, щелкая мышью на любой из папок в правом окне списка для перехода в эту папку или на кнопке Up для перемещения в родительскую папку. На приведенном выше экране в основном текстовом поле введено C:\4990, чтобы получить содержимое этого раздела, который использует окно списка для дальнейшего перемещения. Пользователь может также выбрать файл, щелкая на его имени в окне списка, в этом случае его свойства выводятся в текстовых полях:

    Обратите внимание, что при желании можно также вывести время создания, время последнего доступа и время последнего изменения для папок —

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

    Мы создаем проект как стандартное приложение C# Windows в Visual Studio.NET и добавляем различные текстовые поля и окно списка из области Windows Forms в панели инструментов. Затем элементы управления переименовываются в более понятные имена

    txtBoxInput
    ,
    txtBoxFolder
    ,
    buttonDisplay
    ,
    buttonUp
    ,
    listBoxFiles
    ,
    listBoxFolders
    ,
    textBoxFileName
    ,
    textBoxCreationTime
    ,
    txtBoxLastAccessTime
    ,
    txtBoxLasrWriteTime
    и
    textBoxFileSize
    .

    После чего добавляется следующий код:

    using System;

    using System.Drawing;

    using System.Collections;

    using System.ComponentModel;

    using System.Windows.Forms;

    using System.IO;

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

    public class Form1 : System.Windows.Forms.Form {

    #region Member Fields

     private string currentFolderPath;

    currentFolderPath
    будет содержать путь доступа папки, содержимое которой будет выводится в данный момент в окне списка.

    Теперь нам нужно добавить обработку генерируемых пользователем событий. При этом возможны следующие способы вывода:

    □ Пользователь щелкает на кнопке Display. В этом случае необходимо определить, что введенный пользователем текст в основном текстовом поле является путем доступа к файлу или папке. Если это папка, мы перечисляем файлы и вложенные папки этой папки в окне списка. Если это файл, мы делаем это для папки, содержащей этот файл, но также выводим свойства файла в нижних текстовых полях.

    □ Пользователь щелкает на имени файла в окне списка Files. В этом случае мы выводим свойства этого файла в нижних текстовых полях.

    □ Пользователь щелкает на имени папки в окне списка Folders. В этом случае мы очищаем все элементы управления и затем выводим содержимое этой вложенной папки в окне списка.

    □ Пользователь щелкает на кнопке Up. В этом случае мы очищаем все элементы управления и затем выводим содержимое родительской папки в окне списка.

    Прежде чем показать код обработчиков событий, приведем код методов. Для начала необходимо очистить содержимое всех элементов управления. Этот метод вполне очевиден:

    protected void ClearAllFields() {

     listBoxFolders.Items.Clear();

     listBoxFiles.Items.Clear();

     txtBoxFolder.Text = "";

     txtBoxFileName.Text = "";

     txtBoxCreationTime.Text = "";

     txtBoxLastAccessTime.Text = "";

     txtBoxLastWriteTime.Text = "";

     txtBoxSize.Text ="";

    }

    Затем определяется метод

    DisplayFileInfo()
    , который обрабатывает процесс вывода информации для заданного файла в текстовых полях. Этот метод получает один параметр, полное имя пути доступа файла и работает, создавая объект
    FileInfo
    на основе этого пути доступа:

    protected void DisplayFileInfo(string fileFullName) {

     txtBoxFileName.Text = TheFile.Name;

     txtBoxCreationTime.Text = TheFile.CreationTime.ToLongTimeString();

     txtBoxLastAccessTime.Text = TheFile.LastAccessTime.ToLongDateString();

     txtBoxLastWriteTime.Text = TheFile.LastWriteTime.ToLongDateString();

     txtBoxFileSize.Text = TheFile.Length.ToString() + " bytes";

    }

    Отметим, что здесь мы воздержались от порождения исключения в случае каких-либо проблем с местоположением файла. Исключение будет обрабатываться в вызывающей процедуре (в одном из обработчиков событий). Наконец, мы определяем метод

    DisplayFolderList()
    , который выводит содержимое данной папки в двух окнах списка. Полное имя пути доступа папки передается в этот метод в качестве параметра:

    protected void DisplayFolderList(string folderFullName) {

     DirectoryInfo TheFolder = new DirectoryInfo(folderFullName);

     if (TheFolder.Exists)

    throw new DirectoryNotFoundException("Folder not found: " + folderFullName);

     ClearAllFields();

     txtBoxFolder.Text = TheFolder.FullName;

     currentFolderPath = TheFolder.FullName;

     // перечисляем все папки, вложенные в папку

     foreach(DirectoryInfo NextFolder in TheFolder.GetDirectories())

      listBoxFolders.Items.Add(NextFolder.Name);

     // перечисляем все файлы в папке

     foreach (FileInfo NextFile in TheFolder.GetFiles())

      listBoxFiles.Items.Add(NextFile.Name);

    }

    Теперь мы проверим программы обработки событий. Обработчик события нажатия на кнопку

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

    protected void onDisplayButtonClick(object sender, EventArgs e) {

     try {

      string FolderPath = txtBoxInput.Text;

      DirectoryInfo TheFolder = new DirectoryInfo(FolderPath);

      if (TheFolder.Exists) {

       DisplayFolderList(TheFolder.FullName);

       return;

      }

      FileInfo TheFile = new FileInfo(FolderPath);

      if (TheFile.Exists) {

       DisplayFolderList(TheFile.Directory.FullName);

       int Index = listBoxFiles.Items.IndexOf(TheFile.Name);

       listBoxFiles.SetSelected(Index, true);

       return;

      }

      throws

       new FileNotFoundException("There is no file or folder with " + "this name: " + txtBoxInput.Text);

     } catch (Exception ex) {

      MessageBox.Show(ex.Message);

     }

    }

    В приведенном выше коде мы определяем, что поставляемый текст представляет папку или файл, создавая по очереди экземпляры

    DirectoryInfo
    и
    FileInfo
    и проверяя свойство
    Exists
    каждого объекта. Если ни один из них не существует, то порождается исключение. Если это папка, вызывается метод
    DisplayFolderList
    , чтобы заполнить окна списков. Если это файл, то необходимо заполнить окна списков и текстовые поля, которые выводят свойства файла. Мы обрабатываем этот случай, заполняя сначала окна списков. Затем программным путем выбирается соответствующее имя файла в окне списка файлов. Это имеет в точности тот же результат, как если бы пользователь выбрал этот элемент, порождается событие выбора элемента. Затем можно просто выйти из текущего обработчика событий, зная, что обработчик событий выбранного элемента будет немедленно вызван для вывода свойств файла.

    Следующий код является обработчиком событий, который вызывается, когда в окне списка выбирается элемент либо пользователем, либо, как указано выше, программным путем. Он создает полное имя пути доступа выбранного файла и передает его в метод

    DisplayFileInfo()
    , который был показан ранее:

    protected void OnListBoxFilesSelected(object sender, EventArgs e) {

     try {

      string SelectedString = listBoxFiles.SelectedItem.ToString();

      string FullFileName = Path.Combine(currentFolderPath, SelectedString);

      DisplayFileInfo(FullFileName);

     } catch (Exception ex) {

      MessageBox(ex.Message);

     }

    }

    Обработчик событий выбора папки в окне списка папок реализуется похожим образом, за исключением того, что в этом случае вызывается

    DisplayFolderList()
    для обновления содержимого окон списков:

    protected void OnListBoxFoldersSeleeted(object sender, EventArgs e) {

     try {

      string SelectedString = listBoxFolders.SelectedItem.ToString();

      string FullPathName = Path.Combine(currentFolderPath, SelectedString);

      Display FolderList(FullPathName);

     } catch (Exception ex) {

      MessageBox.Show(ex.Message);

     }

    }

    Наконец, когда происходит щелчок на кнопке Up, должен также вызываться метод

    DisplayFolderList()
    , за исключением того, что в этот раз необходимо получить путь доступа предка выводимой в данный момент папки. Это достигается с помощью свойства
    FileInfo.DirectoryName
    , которое возвращает путь доступа родительской папки

    protected void OnUpButtonClick(object sender, EventArgs e) {

     try {

      string FolderPath = new FileInfo(currentFolderPath).DirectoryName;

      DisplayFolderList(FolderPath);

     } catch (Exception ex) {

      MessageBox(ex.Message);

     }

    }

    Отметим, что для этого проекта мы не показали код, который добавляет методы обработки событий к соответствующим событиям элементов управления. Нам не нужно добавлять этот код вручную, так как согласно замечанию в главе 7 можно использовать окно Properties в Visual Studio для связывания каждого метода обработки событий с событием.

    Перемещение, копирование и удаление файлов

    Мы уже упоминали, что перемещение и удаление файлов или папок делается методами

    MoveTo()
    и
    Delete()
    классов
    FileInfo
    и
    DirectoryInfo
    . Эквивалентными методами в классах File и Directory являются Move() и Delete(). Классы FileInfo и File также соответственно реализуют методы
    CopyTo()
    и
    Copy()
    . Однако не существует методов для копирования полных папок, необходимо сделать это, копируя каждый файл в папке.

    Использование всех этих методов является вполне понятным, все детали можно найти в MSDN. В этом разделе мы проиллюстрируем их использование для определенных случаев вызова статических методов

    Move()
    ,
    Copy()
    и
    Delete()
    на классе
    File
    . Чтобы сделать это, мы преобразуем предыдущий пример
    FileProperties
    в новый пример
    FilePropertiesAndMovement
    . Этот пример будет иметь дополнительное свойство, позволяющее при выводе свойств файла выполнить удаление этого файла или перемещение или копирование его в другое место.

    Пример FilePropertiesAndMovement

    Пример выглядит следующим образом:

    На этом экране можно видеть, что он похож внешне на пример

    FileProperties
    , за исключением того, что существует дополнительная группа из трех кнопок и текстового поля в нижней части окна. Эти элементы управления будут доступны только в том случае, когда пример действительно выводит свойства файла, в других случаях они будут отключены. Мы поджали существующие элементы управления немного вверх, чтобы основная форма не становилась слишком большой. Когда выводятся свойства файла, пример автоматически помещает полное имя пути доступа этого файла в нижнем текстовом поле для редактирования пользователю. Пользователь может затем щелкнуть по любой из кнопок, чтобы выполнить соответствующую операцию. Когда это делается, появляется окно с сообщением, подтверждающем действие. В приведенном выше случае, если пользователь щелкает на CopyTo, мы увидим следующее сообщение:

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

    Чтобы закодировать это, необходимо добавить подходящие элементы управления, а также их методы обработки событий к коду примера

    FileProperties
    . Мы задаем для новых элементов управления имена
    buttonDelete
    ,
    buttonCopyTo
    ,
    buttonMoveTo
    , и
    txtBoxNewPath
    .

    Посмотрим сначала на метод обработки событий, который вызывается, когда пользователь нажимает кнопку Delete:

    protected void OnDeleteButtonClick(object sender, EventArgs e) {

     try {

      string FilePath = Path.Combine(currentFolderPath, txtBoxFileName.Text);

      string Query = "Really delete the file\n" + FilePath + "?";

      if (MessageBox.Show(Query, "Delete File?", MessageBoxButtons.YesNo) == DialogResult.Yes) {

       File.Delete(FilePath); DisplayFolderList(currentFolderPath);

      }

     } catch (Exception ex) {

      MessageBox.Show("Unable to delete file. The following exception" + " occured:\n" + ex.Message, "Failed");

     }

    }

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

    CurrentParentPath
    , которое будет содержать путь доступа текущей папки, и формируем текст в поле
    textBoxFileName
    , которое будет содержать имя файла.

    Методы для перемещения и копирования файла структурированы похожим образом:

    protected void OnMoveButtonClickfobject sender, EventArgs e) {

     try {

      string FilePath = Path.Combine(currentFolderPath, txtBoxFileName.Text);

      string Query =

       "Really move the file\n" + FilePath + "\nto " + txtBoxNewPath.Text + "?";

      if (MessageBox.Show(Query, "Move File?", MessageBoxButtons.YesNo) == DialogResult.Yes) {

       File.Move(FilePath, txtBoxNewPath.Text);

       DisplayFolderList(currentFolderPath);

      }

     } catch(Exception ex) {

      MessageBox.Show("Unable to move file. The following exception" + " occured: \n" + ex.Message, "Failed");

     }

    }


    protected void OnCopyButtonClick(object sender, EventArgs e) {

     try {

      string FilePath = Path.Combine(currentFolderPath, txt.BoxFileName.Text);

      string Query = "Really copy the file\n" + FilePath + "\nto " + txtBoxNewPath.Text + "?";

      if (MessageBox.Show(Query, "Copy File?", MessageBoxButtons.YesNo) == DialogResult.Yes) {

       File.Copy(FilePath, txtBoxNewPath.Text);

       DisplayFolderList(currentFolderPath);

      }

     } catch (Exception ex) {

      MessageBox.Show("Unable to copy file. The following exception" + " occured:\n" + ex.Message, "Failed");

     }

    }

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

    DisplayFileInfo()
    :

    protected void DisplayFileInfo(string fileFullName) {

     FileInfo TheFile = new FileInfo(fileFullName);

     if (!TheFile.Exists) throw new FileNotFoundException("File not found: " + fileFullName);

     txtBoxFileName.Text = TheFile.Name;

     txtBoxCreationTime.Text = TheFile.CreationTime.ToLongTimeString();

     txtBoxLastAccessTime.Text = TheFile.LastAccessTime.ToLongDateString();

     txtBoxLastWriteTime.Text = TheFile.LastWriteTime.ToLongDateString();

     txtBoxFileSize.Text = TheFile.Length.ToString() + " bytes";

     // включает кнопки перемещения, копирования и удаления

     txtBoxNewPath.Text = TheFile.FullName;

     txtBoxNewPath.Enabled = true;

     buttonCopyTo.Enabled = true;

     buttonDelete.Enabled = true;

     buttonMoveTo.Enabled = true;

    }

    Нам нужно также сделать одно изменение в

    DisplayFolderInfo
    :

    protected void DisplayFolderList(string folderFullName) {

     DirectoryInfo TheFolder = new DirectoryInfo(folderFullName);

     if (!TheFolder.Exists)

    throw new DirectoryNotFoundException("Folder not found: " + folderFullName);

     ClearAllFields();

     DisableMoveFeatures();

     txtBoxFolder.Text = TheFolder.FullName;

     currentFolderPath = TheFolder.FullName;

     // перечислить все папки, вложенные в папку

     foreach(DirectoryInfo NextFolder in TheFolder.GetDirectories())

      listBoxFolders.Items.Add(NextFolder.Name);

     // перечислить все файлы в папке

     foreach (FileInfo NextFile in TheFolder.GetFiles())

      listBoxFiles.Items.Add(NextFile.Name);

    }

    DisableMoveFeatures
    является небольшой служебной функцией, которая отключает новые элементы управления:

    void DisableMoveFeatures() {

     txtBoxNewPath.Text = "";

     txtBoxNewPath.Enabled = false;

     buttonCopyTo.Enabled = false;

     buttonDelete.Enabled = false;

     buttonMoveTo.Enabled = false;

    }

    Нам также понадобится добавить код в

    ClearAllFields()
    , чтобы очистить дополнительное текстовое поле:

    protected void ClearAllFields() {

     listBoxFolders.Items.Clear();

     listBoxFiles.Items.Clear();

     txtBoxFolder.Text = "";

     txtBoxFileName.Text = "";

     txtBoxCreationTime.Text = "";

     txtBoxLastAccessTime.Text = "";

     txtBoxLastWriteTime.Text = "";

     txtBoxFileSize.Text = "";

     txtBoxNewPath.Text = "";

    }

    После этого код закончен.

    Чтение и запись файлов

    Чтение и запись файлов является в принципе очень простым процессом, но делается это не с помощью объектов

    DirectoryInfo
    или
    FileInfo
    , которые только что были рассмотрены. Вместо этого используется ряд классов, которые представляют общую концепцию, называемую потоком.

    Потоки 

    Идея потока существует уже очень давно. Поток является объектом, используемым для пересылки данных. Данные могут передаваться в одном или в двух направлениях:

    □ Если данные передаются в программу из некоторого внешнего источника, то речь идет о чтении из потока.

    □ Если данные передаются из программы в некоторый внешний источник, то речь идет о записи в поток.

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

    □ Чтение или запись данных в сети с помощью некоторого сетевого протокола, куда посылают данные или получают с другого компьютера.

    □ Чтение или запись через именованный канал.

    □ Чтение или запись данных в области памяти.

    Для таких примеров Microsoft поставляет базовый класс .NET для записи в память и чтения из памяти

    System.IO.MemoryStream
    , в то время как
    System.Net.Sockets.Networkstream
    обрабатывает сетевые данные. Не существует базовых классов потока для записи в каналы или чтения из каналов, но существует базовый класс потока,
    System.IO.Stream
    , из которого можно создать, если понадобиться, производный класс. Поток не делает никаких предположений о природе внешнего источника данных.

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

    sprintf()
    , а в C# два базовых класса .NET,
    StringReader
    и
    StringWriter
    , могут использоваться в таком контексте.

    Преимущество применения отдельного объекта для передачи данных, вместо классов

    FileInfo
    и
    DirectoryInfo
    , состоит в том, что разделение концепции передачи данных и определенного источника данных облегчает замену источников данных. Сами объекты потоков содержат большой объем базового кода, имеющего отношение к переносу данных между внешними источниками и переменными в коде приложения, и сохраняя этот код отдельно от любой концепции определенного источника данных, мы облегчаем повторное применения этого кода (через наследование) в различных обстоятельствах. Например, упомянутые выше классы
    StringReader
    и
    StringWriter
    являются частью того же дерева наследования, что и два класса, используемых для чтения и записи текстовых файлов, —
    StreamReader
    и
    StreamWriter
    . Классы почти наверняка неявно задействуют значительный объем общего кода.

    Реальная иерархия связанных с потоком классов в пространстве имен

    System.IO
    выглядит следующим образом:

    Что касается чтения из файлов или записи в файлы, то мы будем связаны в основном со следующими классами:

    □ 

    FileStream
    . Этот класс предназначен для чтения и записи двоичных данных в произвольный двоичный файл, однако при желании можно использовать его для чтения и записи в любой файл.

    StreamReader
    и
    StreamWriter
    . Эти классы специально предназначены для чтения и записи в текстовые файлы.

    Упомянем также другие классы, которые могут оказаться полезными, хотя они и не будут представлены в приводимых примерах. Если вы хотите использовать эти классы, обратитесь к документации MSDN, чтобы получить подробности об их работе.

    BinaryReader
    и
    BinaryWriter
    . Эти классы в действительности сами не реализуют потоки, но они могут обеспечить оболочки вокруг других потоковых объектов.
    BinaryReader
    и
    BinaryWriter
    поддерживают дополнительное форматирование двоичных данных, что позволяет напрямую читать или записывать содержимое переменных C# в соответствующий поток. Проще всего считать, что
    BinaryReader
    и
    BinaryWriter
    находятся между потоком и кодом приложения, обеспечивая дополнительное форматирование:

    Различие между использованием этих классов и непосредственным использованием описанных ниже потоковых объектов состоит в том, что базовый поток работает с байтами. Например, пусть часть процесса сохранения некоторого документа состоит в записи содержимого переменной типа

    long
    в двоичный файл. Каждая переменная типа
    long
    занимает 8 байтов, если используется плоский обыкновенный двоичный поток, необходимо будет явно записывать каждые эти 8 байтов памяти. В коде C# это будет означать, что необходимо явно выполнять некоторые битовые операции для извлечения каждых 8 байтов из значения
    long
    . Используя экземпляр
    BinaryWriter
    , можно инкапсулировать всю операцию в перегруженный метод
    BinaryWriter.Write()
    , который получает
    long
    в качестве параметра и который будет помещать эти 8 байтов в поток (и следовательно, если поток направлен в файл, то в файл). Соответствующий метод
    BinaryReader.Read()
    будет извлекать 8 байтов из потока и восстанавливать значение
    long
    .

    BufferedStream
    . По соображениям производительности при чтении из файла или при записи в файл вывод буферизуется. Это означает, что если программа запрашивает следующие 2 байта файлового потока и поток передает запрос Windows, то Windows не станет соединяться с файловой системой и затем искать и считывать файл с диска, для того чтобы получить 2 байта. Вместо этого произойдет извлечение большого блока файла за один раз и сохранение этого блока в области памяти, называемой буфером. Последующие запросы данных из потока будут удовлетворяться из буфера, пока он не будет исчерпан, и тогда Windows извлечет другой блок данных из файла. Запись в файлы работает таким же образом. Для файлов это делается автоматически операционной системой, но возможен случай, когда придется написать потоковый класс для чтения из некоторого другого устройства, которое не буферизуется. В таком случае можно вывести этот класс из
    BufferedStream
    , который сам реализует буфер (
    BufferedStream
    не создан, однако, для ситуаций, когда приложение часто чередует операции чтения и записи данных).

    Чтение и запись двоичных файлов

    Чтение и запись двоичных файлов делается обычно с помощью класса

    FileStream
    .

    Класс FileStream

    Экземпляр

    FileStream
    используется для чтения или записи данных файла. Чтобы создать
    FileStream
    , необходимо иметь данные четырех видов:

    Файл для доступа.

    □ Режим, который указывает, как необходимо открыть файл. Например, собираетесь ли вы создать новый файл или открыть существующий файл, и, если открывается существующий файл, должна ли какая-либо операция записи интерпретироваться как перезапись содержимого файла или как добавление к файлу.

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

    □ Общий доступ. Другими словами, будет ли осуществляться исключительный доступ к файлу, или желательно, чтобы другие потоки могли одновременно получать доступ к этому файлу. Если так, то должны ли другие потоки иметь доступ для чтения файла, записи в него или для того и другого.

    Первый из этих видов данных представлен обычно строкой, которая содержит полное имя пути доступа файла, и в этой главе будут рассматриваться только те конструкторы, которые требуют строку. Помимо этих конструкторов, существуют и некоторые другие, которые получают дескриптор файла Windows в стиле старого Windows API. Остальные три вида данных представлены тремя перечислениями .NET, называемыми соответственно

    FileMode
    ,
    FileAccess
    и
    FileShare
    . Значения этих перечислений должны быть понятны из названий.

    Перечисление Значения
    FileMode
    (режим файла)
    Append
    (добавить),
    Create
    (создать),
    CreateNew
    (создать новый),
    Open
    (открыть),
    OpenOrCreate
    (открыть или создать),
    Truncate
    (обрезать)
    FileAccess
    (доступ к файлу)
    Read
    (чтение),
    ReadWrite
    (чтение-запись),
    Write
    (запись)
    FileShare
    (общий доступ к файлу)
    None
    (нет),
    Read
    (чтение),
    ReadWrite
    (чтение-запись),
    Write
    (запись)

    Отметим, что в случае

    FileMode
    могут порождаться исключения, если запросить режим, который несогласован с существующим статусом файла.
    Append
    ,
    Open
    и
    Truncate
    будут порождать исключение, если файл еще не существует, a
    CreateNew
    будет порождать исключение, если он существует.
    Create
    и
    OpenOrCreate
    будут удовлетворять любому сценарию, но
    Create
    будет удалять любой существующий файл для замены его новым, вначале пустым файлом.

    Существует большое число конструкторов для

    FileSream
    . Три простейшие из них работают следующим образом:

    // создает файл с доступом read-write и предоставляет другим потокам

    // доступ на чтение

    FileStream fs = new FileStream(@"C:\C# Projects\Projects.doc", FileMode.Create);

    // как и выше, но мы получаем доступ к файлу только на запись

    FileStream fs2 = new FileStream(@"C:\C# Projects\Projects2.doc", FileMode.Create, FileAccess.Write);

    // как и выше, но другие потоки не имеют никакого доступа к файлу,

    // пока fs3 открыт

    FileStream fs3 = new FileStream(@"C:\C# Projects\Projects3.doc", FileMode.Create, FileAccess.Read, FileShare.None);

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

    FileAcces.ReadWrite
    и
    FileShare.Read
    для третьего и четвертого параметров. Также можно создать файловый поток из экземпляра
    FileInfo
    :

    FileInfo MyFile4 = new FileInfo(@"C:\C# Projects\Projects4.doc");

    FileStream fs4 = MyFile4.OpenRead();

    FileInfo MyFile5 = new FileInfo(@"C:\C# Projects\Projects5.doc");

    FileStream fs5 = MyFile5.OpenWrite();

    FileInfo MyFile6 = new FileInfo(@"C:\C# Projects\Projects6.doc");

    FileStream fs6 = MyFile6.Open(FileMode.Append, FileAccess.Read, FileShare.None);

    FileInfo MyNewFile = new FileInfo(@"C:\C# Projects\ProjectsNew.doc");

    FileStream fs7 = MyNewFile.Create();

    FileInfo.OpenRead()
    поставляет поток, предоставляющий доступ только для чтения к существующему файлу, в то время как FileInfo.OpenWrite() предоставляет доступ для чтения-записи.
    FileInfo.Open()
    позволяет явно определить параметры режима, доступа и общего доступа.

    Не забудьте, что по окончании работы поток необходимо закрыть:

    fs.Close();

    Закрытие потока освобождает связанные с ним ресурсы и позволяет другим приложениям настроить потоки на тот же самый файл. Для чтения и записи данных в поток

    FileStream
    реализует ряд методов.

    Метод

    ReadByte()
    является простейшим способом чтения данных: он захватывает один байт из потока и преобразовывает результат в int, имеющее значение между 0 и 255. По достижении конца потока возвращается -1:

    int NextByte = fs.ReadByte();

    Если желательно прочитать несколько байтов за один раз, можно вызвать метод

    Read()
    , который читает указанное число байтов в массиве.
    Read()
    возвращает реально прочитанное число байтов, если это значение равно нулю, то это говорит о конце потока:

    // считать 100 байтов int nBytes = 100;

    byte [] ByteArray = new byte[nBytes];

    int nBytesRead = fs.Read(ByteArray, 0, nBytes);

    Второй параметр в методе

    Read()
    — смещение, которое указывает операции
    Read
    начать заполнение массива с элемента, отличного от первого.

    Если требуется записать данные в файл, то существует два параллельных метода

    WriteByte()
    и
    Write()
    .
    WriteByte()
    записывает один байт в поток:

    byte Next Byte = 100; fs.WriteByte(NextByte);

    Write()
    , с другой стороны, записывает массив байтов:

    // чтобы записать 100 байтов

    int nBytes = 100;

    byte [] ByteArray = new byte[nBytes];

    // здесь массив заполняется значениями, которые надо записать

    fs.Write(BуteArray, 0, nBytes);

    Как и для метода

    Read(),
    второй параметр позволяет начать записывать с некоторого места, отличного от начала массива. Оба метода
    WriteByte()
    и
    Write()
    возвращают
    void
    .

    Помимо этих методов

    FileStream
    реализует также другие методы и свойства для выполнения задач учета, типа определения числа байтов в потоке, блокирования потока или очистки буфера. Эти методы обычно не требуются для базового чтения и записи, а если они потребуются, все детали находятся в документации MSDN.

    Пример: объект чтения двоичного файла

    Для иллюстрации использования класса

    FileStream
    напишем пример
    BinaryFileReader
    , который считывает и выводит любой файл. Он создается в Visual Studio.NET как оконное приложение. Добавляем один пункт меню, который выводит стандартный диалог
    OpenFileDialog
    , запрашивающий файл для чтения, а затем выводит файл. Так как мы читаем двоичные файлы, нам необходимо иметь возможность выводить непечатные символы. Делаем это, выводя каждый байт файла отдельно, по 16 байтов в каждой строке многострочного текстового поля. Если байт представляет печатный символ ASCII, выводится этот символ, иначе выводится значение байта в шестнадцатеричном формате. В любом случае, мы дополняем выводимый текст пробелами, так что каждый выводимый 'байт' занимает четыре столбца, чтобы байты аккуратно выровнялись друг под другом.

    Вот как выглядит работа

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

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

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

    Итак, запишем код. Вначале добавим дополнительные инструкции

    using
    , так как помимо
    System.IO
    этот пример будет использовать класс
    StringBuilder
    из пространства имен
    System.Text
    для создания строк текстового поля:

    using System.IO;

    using System.Text;

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

    public class Form1 : System.Windows.Forms.Form {

     OpenFileDialog ChooseOpenFileDialog = new OpenFileDialog();

     string ChosenFile;

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

     public Form1() {

      InitializeComponent();

      menuFileOpen.Click += new EventHandler(OnFileOpen);

      ChooseOpenFileDialog.FileOk += new CancelEventHandler(OnOpenFileDialogOK);

     }


     void OnFileOpen(object Sender, EventArgs e) {

      ChooseOpenFileDialog.ShowDialog();

     }


     void OnOpenFileDialogOK(object Sender, CancelEventArgs e) {

      ChosenFile = ChooseOpenFileDialog.FileName;

      this.Text = ChosenFile;

      DisplayFile();

     }

    Из этого кода мы видим, что, когда пользователь нажимает OK, чтобы выбрать файл в файловом диалоге, вызывается метод

    DisplayFile()
    , который реально выполняет работу считывания файла:

     void DisplayFile() {

      int nCols = 16;

      FileStream InStream = new FileStream(ChosenFile, FileMode.Open, FileAccess.Read);

      long nBytesToRead = InStream.Length; if (nBytesToRead > 65536/4)

      nBytesToRead = 65536/4;

      int nLines = (int)(nBytesToRead/nCols) + 1;

      String [] Lines = new string[nLines];

      int nBytesRead = 0;

      for (int i=0; i<nLines; i++) {

       StringBuilder NextLine = new StringBuilder();

       NextLine.Capacity = 4*nCols;

       for (int j = 0; j<nCols; j++) {

        int NextByte = InStream.ReadByte();

        nBytesRead++;

        if (NextByte <0 || nBytesRead > 65536) break;

        char NextChar = (char)NextByte;

        if (NextChar < 16)

    NextLine.Append(" x0" + string.Format("{0,1:X}", (int(NextChar));

        else if (char.IsLetterOrDigit(NextChar) || char.IsPunctuation(NextChar))

        (NextLine.Append(" " + NextChar + " ");

        else NextLine.Append(" x" + string.Format("{0,2:X}", (int)NextChar));

       }

       Lines[i] = NextLine.ToString();

      }

      InStream.Close();

      this.textBoxContents.Lines = Lines;

     }

    Разберем данный метод подробнее. Мы создаем экземпляр

    FileStream
    для выбранного файла и хотим открыть существующий файл для чтения. Затем мы определяем, сколько существует байтов для чтения и сколько строк должно выводиться. Число байтов обычно равно числу байтов в файле. Однако текстовые поля могут выводить максимум только 65536 символов, и для выбранного формата выводится 4 символа для каждого байта в файле, поэтому необходимо сделать ограничение числа показываемых байтов, если файл длиннее 65536/4 = 16384.

    В случае выведения более длинных файлов в таком рабочем окружении можно рассмотреть класс

    RichTextBox
    в пространстве имен
    System.Windows.Forms
    .
    RichTextBox
    аналогичен текстовому полю, но имеет значительно более развитые средства форматирования и не имеет ограничения на объем выводимого текста. Мы используем здесь
    TextBox
    , чтобы сохранить тривиальность примера и сосредоточиться на процессе чтения файла.

    Основная часть метода представлена в двух вложенных циклах for, которые создают каждую строку текста для вывода. Мы используем класс

    StringBuilder
    для создания каждой строки по соображениям производительности. Мы будем добавлять подходящий текст для каждого байта к строке, которая представляет каждую линию 16 раз. Если каждый раз мы будем выделять новую строку и делать копию полусозданной линии, мы не только затратим много времени на выделение строк, но истратим много памяти из кучи динамической памяти. Отметим, что наше определение 'печатных' символов соответствует буквам, цифрам и символам пунктуации, как указано соответствующими статическими методами
    System.Char
    . Мы исключили, однако, все символы со значением меньше 16 из списка печатных символов, а значит, будем перехватывать возврат каретки (13) и перевод строки (10) как двоичные символы (многострочное текстовое поле не может правильно вывести эти символы, если они встречаются отдельно внутри строки).

    В конце мы закрываем поток и задаем содержимое текстового поля как массив строк, который был создан.

    Чтение и запись текстовых файлов

    Теоретически вполне возможно использование класса

    FileStream
    для чтения и вывода текстовых файлов. В конце концов, мы уже продемонстрировали, как это делается. Формат, в котором выводился выше файл
    CivNegotiations.txt
    , был не очень удобен для пользователя, но это было связано не с какой-либо внутренней проблемой класса
    FileStream
    , а со способом, который был выбран для вывода результатов в текстовом поле.

    Если известно, что определенный файл содержит текст, то будет более удобно прочитать его с помощью классов StreamReader и

    StreamWriter
    . Это связано с тем, что эти классы работают на чуть более высоком уровне, и предназначены специально для чтения текста. Методы, которые они реализуют, способны автоматически определять, где находятся удобные точки для остановки чтения, основываясь на содержимом потока. В частности:

    □ Эти классы реализуют методы для чтения или записи одной строки текста за раз (

    StreamReader.ReadLine()
    и
    StreamWriter.WriteLine()
    ). В случае чтения, это значит, что поток будет автоматически определять, где находится следующий перевод каретки, и остановит чтение в этой точке. В случае записи это означает, что поток будет автоматически добавлять комбинацию возврата каретки-перевода строки в записываемый текст.

    □ При использовании классов

    StreamReader
    и
    StreamWriter
    не нужно беспокоиться об использованном в файле кодировании. Кодирование означает формат, который имел текст в содержащем его файле. Возможное кодирование включает ASCII (1 байт для каждого символа), или любой из форматов на основе Unicode, UTF7 и UTF8. Текстовые файлы в системах Windows 9х всегда являются ASCII, так как Windows 9х не поддерживает Unicode, и поэтому текстовые файлы могут теоретически содержать данные Unicode, UTF7 или UTF8 вместо данных ASCII. Соглашение состоит в том, что если файл имеет формат ASCII, значит он содержит текст. Если же любой из форматов Unicode, то он будет указан первыми двумя или тремя байтами файла, которые содержат определенную комбинацию значений для указания формата. Эти байты называются маркерами кода байтов. Когда файл открывается с помощью любого стандартного оконного приложения, такого как Notepad или WordPad, то не нужно беспокоиться об этом, так как эти приложения знакомы с различными методами кодирования и автоматически правильно считывают файл. Это также случай для класса
    StreamReader
    , который будет правильно считывать файл в любом из этих форматов, в то время как класс
    StreamWriter
    способен форматировать текст, который он записывает, с помощью любой запрошенной техники кодирования. С другой стороны, если требуется прочитать и вывести текстовый файл с помощью класса
    FileStream
    , придется все это выполнять самостоятельно.

    Класс StreamReader

    Класс

    StreamReader
    используется для чтения текстовых файлов. Создать
    StreamReader
    проще, чем экземпляр
    FileStream
    , так как некоторые из параметров
    FileStream
    не требуются. В частности, режим и тип доступа несущественны, поскольку с помощью
    StreamReader
    можно только читать. Также не существует непосредственного параметра для определения полномочий общего доступа. Но с другой стороны, существуют и новые параметры:

    □ Необходимо определить, что делать с различными методами кодирования. Можно дать инструкцию

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

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

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

    StreamReader
    работает на относительно высоком уровне, может оказаться полезным в ситуации, когда имеется другой поток, который читает данные из некоторого другого источника, использовать средства, предоставляемые
    StreamReader
    для обработки этого потока, как если бы он содержал текст. Это можно сделать, передавая вывод из этого потока в
    StreamReader
    . Таким образом,
    StreamReader
    может использоваться для чтения и обработки данных из любого источника данных, а не только файлов. Это по сути ситуация, которая рассматривалась ранее в отношении класса
    BinaryReader
    . Однако в этой книге мы будем использовать
    StreamReader
    только для прямого соединения с файлами.

    В силу указанных возможностей

    StreamReader
    имеет большое число конструкторов. Помимо этого существует пара методов
    FileInfo
    , которые также возвращают ссылки на
    StreamReader
    :
    OpenText()
    и
    CreateText()
    . Здесь мы проиллюстрируем некоторые из конструкторов.

    Простейший конструктор получает только имя файла. Этот

    StreamReader
    будет проверять маркеры кода байтов, чтобы определить кодирование:

    StreamReader sr = new StreamReader(@"C:\My Documents\ReadMe.txt");

    И наоборот, если желательно определить, предполагается ли кодирование UTF8:

    StreamReader sr = new StreamReader(@"C:\My Documents\ReadMe.txt", Encoding.UTF8Encoding);

    Мы определяем кодирование, используя одно из нескольких свойств класса,

    System.Text.Encoding
    . Этот класс является абстрактным базовым классом, из которого определяется ряд классов, которые реализуют методы, реально выполняющие кодирование текста. Каждое свойство возвращает экземпляр соответствующего класса. Здесь можно использовать следующие свойства:

    ASCII

    Unicode

    UTF7

    UTF8

    BigEndianUnicode

    Следующий пример показывает сцепление

    StreamReader
    с
    FileStream
    . Преимущество этого состоит в том, что можно явно определить, создавать ли файл и полномочия совместного доступа, что невозможно сделать, если напрямую соединять StreamReader с файлом:

    FileStream fs =

     new FileStream(@"C:\My Documents\ReadMe.txt", FileMode.Open,

     FileAccess.Read, FileShare.None);

    StreamReader sr = new StreamReader(fs);

    Для этого примера мы уточняем., что

    StreamReader
    будет искать маркеры кода байтов, чтобы определись используемый метод кодирования, так он будет делать и в следующих примерах, где
    StreamReader
    получают из экземпляра
    FileInfo
    :

    FileInfo MyFile = new FileInfo(@"C:\My Documents\ReadMe.txt");

    StreamReader sr = MyFile.OpenText();

    Также как с

    FileStream
    , необходимо всегда закрывать
    StreamReader
    после использования. Невыполнение этого приведет к файлу, остающемуся заблокированным для других процессов (если только для создания
    StreamReader
    не использовался
    FileStream
    со спецификацией
    FileShare.ShareReadWrite
    ).

    sr.Close();

    Теперь мы перешли к проблеме создания экземпляра

    StreamReader
    . Так же, как с классом
    FileStream
    , мы просто указываем различные способы чтения данных и оставляем другие, менее часто используемые, методы
    StreamReader
    для документации MSDN.

    Возможно, простейшим в использовании является метод

    ReadLine()
    , который продолжает чтение, пока не доходит до конца строки. Он не включает комбинацию возврат каретки-перевода строки, которая отмечает конец строки в возвращаемой строке:

    string NextLine = sr.ReadLine();

    Альтернатива — захватить весь остаток файла (или строго говоря, остаток потока) в одной строке:

    string RestOfStream = sr.ReadToEnd();

    Можно также прочитать один символ:

    int NextChar = sr.Read();

    Эта конструкция с

    Read()
    преобразует возвращаемый символ в
    int
    . Это делается так, потому что имеется возможность альтернативного возврата -1, если будет достигнут конец потока.

    Наконец, можно прочитать заданное число символов в массив с использованием смещения:

    // прочитать 100 символов

    int nChars = 100;

    chr [] CharArray = new char[nChars];

    int nCharsRead = sr.Read(CharArray, 0, nChars);

    nCharsRead
    будет меньше
    nChars
    , если запрос чтения потребует больше символов, чем осталось в файле.

    Класс StreamWriter

    Он работает практически таким же образом, как и

    StreamReader
    , за исключением того только, что
    StreamWriter
    используется для записи в файл (или в другой поток). Возможности создания
    StreamWriter
    включают в себя:

    StreamWriter sw = new StreamWriter(@"C:\My Documents\ReadMe.txt");

    Приведенный выше код будет использовать кодирование UTF8, которое рассматривается в .NET как метод кодирования по умолчанию. Если желательно определить альтернативное кодирование:

    StreamWriter sw = new StreamWriter(@"C:\My Docurnents\ReadMe.txt", true, Encoding.ASCII);

    В этом конструкторе вторым параметром является Boolean, который указывает, должен ли файл быть открыт для добавления. Странно, но не существует конструктора, который получает только имя файла и класс кодирования.

    Конечно, можно соединить

    StreamWriter
    с файловым потоком, чтобы предоставить больший контроль над параметрами открытия файла:

    FileStream fs = new FileStream(@"C:\Му Documents\ReadMe.txt",

    FileMode.CreateNew, FileAcces.Write, FileShare.ShareRead);

    StreamWriter sw = new StreamWriter(fs);

    FileInfo
    не реализует никаких методов, которые возвращают
    StreamWriter
    . Альтернативно, если вы захотите создать новый файл и начать записывать в него данные, то может оказаться полезной такая последовательность действий:

    FileInfo MyFile = new FileInfo(@"C:\My Documents\NewFile.txt");

    StreamWriter sw = MyFile.CreateText();

    Так же, как и со всеми другими потоковыми классами, важно закрыть

    StreamWriter
    , когда работа закончена:

    sw.Close();

    Запись в поток делается с помощью любого из четырех перегружаемых методов

    StreamWriter.Write()
    . Простейший из них записывает строку и дополняет ее комбинацией возврат каретки — перевод строки:

    string NextLine = "Groovy Line";

    sw.Write(NextLine);

    Можно также записать один символ:

    char NextChar = 'а';

    sw.Write(NextChar);

    Массив символов также возможен:

    char [] Char Array = new char[100];

    // инициализация этого массива символов

    sw.Write(CharArray);

    Можно даже записать часть массива символов:

    int nCharsToWrite = 50;

    int StartAtLocation = 25;

    char [] CharArray = new char[100];

    // инициализируем эти символы

    sw.Write(CharArray, StartAtLocation, nCharsToWrite);

    Пример: ReadWriteText

    Пример

    ReadWriteText
    выводит результат использования классов
    StreamReader
    и
    StreamWriter
    . Он похож на предыдущий пример
    ReadBinaryFile
    , но предполагает, что считываемый файл является текстовым файлом, и выводит его в этом виде. Он может сохранить файл (со всеми изменениями, которые будут сделаны в тексте в текстовом поле). Он будет сохранять любой файл в формате Unicode.

    Снимок показывает использование

    ReadWriteText
    для вывода того же файла
    CivNegotiations
    , который использовался раньше. В этот раз, однако, мы сможем прочитать содержимое гораздо легче.

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

    BinaryFileReader
    . Как и для данного примера, открытие нового файла приводит к вызову метода
    DisplayFile()
    . Единственное реальное различие между этим примером и предыдущим состоит в реализации
    DisplayFile
    , а также в том факте, что теперь имеется возможность сохранить файл. Она представлена другим пунктом меню — save. Обработчик для этого пункта вызывает другой добавленный в код метод —
    SaveFile()
    . (Отметим, что этот файл всегда перезаписывает первоначальный файл, этот пример не имеет возможности записи в другой файл.)

    Посмотрим сначала на

    SaveFile
    , так как это простейшая функция. Мы просто записываем каждую строку текстового поля по очереди в поток
    StreamWriter
    , полагаясь на метод
    StreamReader.WriteLine()
    для добавления комбинации возврата каретки-перевода строки в конце каждой строки:

    void SaveFile() {

     StreamWriter sw = new StreamWriter(ChosenFile, false, Encoding.Unicode);

     foreach (string Line in textBoxContents.Lines) sw.WriteLine(Line);

     sw.Close();

    }

    ChosenFile
    является строковым полем основной формы, которое содержит имя прочитанного файла (как и в предыдущем примере). Отметим, что при открытии потока определяется кодирование Unicode. Если желательно записывать файлы в другом формате, необходимо изменить значение этого параметра. Второй параметр в этом конструкторе будет задан как true, если мы хотим добавлять к файлу, но в данном случае мы этого не делаем. Кодирование должно задаваться во время создания объекта записи потока. В дальнейшем оно доступно только для чтения как свойство
    Encoding
    .

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

    (char)13 - (char)10
    находится в файле). Решим эту проблему, считывая сначала файл в экземпляр класса
    StringCollection
    , который находится в пространстве имен
    System.Collections.Specialized
    . Этот класс создан для хранения множества строк, которые могут динамически увеличиваться. Он реализует два интересующих нас метода:
    Add()
    , добавляющий строку в коллекцию, и
    CopyTo()
    , который копирует коллекцию строк в обычный массив (экземпляр
    System.Array
    ). Каждый элемент объекта
    StringCollection
    будет содержать одну строку файла.

    Метод

    DisplayFile()
    вызывает другой метод
    ReadFileIntoStringCollection()
    , который реально считывает файл. После этого мы знаем, сколько имеется строк, поэтому можно скопировать
    StringCollection
    в обычный массив фиксированного размера и передать этот массив в текстовое поле. Так как при создании копии копируются только ссылки на строки, а не сами строки, процесс будет достаточно эффективным:

    void DisplayFile() {

     StringCollection LinesCollection = ReadFileIntoStringCollection();

     string [] LinesArray = new string[LinesCollection.Count];

     LinesCollection.CopyTo(LinesArray, 0);

     this.textBoxContents.Lines = LinesArray;

    }

    Второй параметр

    StringCollection.CopyTo()
    указывает индекс в массиве назначения, где мы хотим начать размещение коллекции.

    Теперь рассмотрим метод

    ReadFileIntoStringCollection()
    . Мы используем
    StreamReader
    для считывания каждой строки. Основной трудностью является необходимость подсчитывать считанные символы, чтобы не превысить емкость текстового поля:

    ArrayList ReadFileIntoStringCollection() {

     const int MaxBytes = 65536;

     StreamReader sr = new StreamReader(ChosenFile);

     StringCollection Result = new StringCollection();

     int nBytesRead = 0;

     string NextLine;

     while ((NextLine = sr.ReadLine()) != null) {

      nBytesRead += NextLine.Length;

      if (nBytesRead > MaxBytes) break;

      Result.Add(NextLine);

     }

     sr.Close();

     return Result;

    }

    Код завершен.

    Если выполнить

    ReadWriteText
    — считать файл
    CivNegotiations
    и затем сохранить его, то файл будет иметь формат Unicode. Это невозможно для любого из обычных оконных приложений: Notepad, Wordpad и даже наш собственный пример
    ReadWriteText
    будут по-прежнему считывать файл и выводить его правильно в Windows NT/2000/XP, хотя, так как Windows 9х не поддерживает Unicode, приложения типа Notepad не смогут понять файл Unicode на этих платформах (Если загрузить пример с web-сайта издательства Wrox Press, то можно попробовать это сделать.) Однако, если попробовать вывести файл снова с помощью предыдущего примера
    ReadBinaryFile
    , то разница будет заметна немедленно, как на следующем экране. Два начальных файла указывают, что файл имеет формат Unicode, и поэтому мы видим, что каждый символ представлен двумя байтами. Этот последний факт вполне очевиден, поскольку старший байт каждого символа в данном конкретном файле равен нулю, поэтому каждый второй байт в этом файле выводится теперь как x00:

    Чтение и запись в реестр

    Во всех версиях Windows, начиная с Windows 95, реестр был центральным репозиторием всех конфигурационных данных, связанных с настройкой Windows, предпочтениями пользователя и установленным программным обеспечением и устройствами. Почти все коммерческое программное обеспечение сегодня использует реестр для хранения информации о себе, и компоненты COM должны помещать информацию о себе в реестр, чтобы клиент смог их вызвать. Платформа .NET и сопровождающая ее концепция о нулевом влиянии установки слегка уменьшили значение реестра для приложений в том смысле, что сборки являются полностью самодостаточными, поэтому никакая информация об определенных сборках не должна помещаться в реестр для сборок общего использования.

    Тот факт, что приложения могут теперь устанавливаться с помощью программы установки Windows также освобождает разработчика от некоторых прямых манипуляций с реестром, которые обычно были связаны с установкой приложений. Однако, несмотря на это, при распространении любого законченного приложения вполне вероятно, что это приложение будет использовать реестр для хранения информации о своей конфигурации. Если приложение появляется в диалоговом окне Add/Remove Programs в панели управления, то создается соответствующая запись в реестре.

    Как и можно было ожидать от такой всеобъемлющей библиотеки, как библиотека .NET, она содержит классы, которые предоставляют доступ к реестру. Имеются два класса, связанных с реестром, и оба находятся в пространстве имен

    Microsoft.Win32
    . Это классы
    Registry
    и
    RegistryKey
    . Прежде чем рассматривать их, кратко разберем структуру самого реестра.

    Реестр

    Реестр имеет иерархическую структуру, очень похожую на файловую систему. Обычно просмотр или изменение содержимого реестра выполняется с помощью одной из двух утилит —

    regedit
    или
    regedt32
    .
    regedit
    стандартно поставляется со всеми версиями Windows, начиная с Windows 95. Утилита
    regedt32
    поставляется с Windows NT и Windows 2000 — она менее дружественна пользователю, чем
    regedit
    , но разрешает доступ к данным безопасности, которые
    regedit
    не может видеть. Здесь будет использоваться
    regedit
    , которую можно запустить, вводя
    regedit
    в диалоговом окне Run… или командной строке.

    При первом запуске

    regedit
    появится примерно следующее изображение:

    Regedit
    имеет интерфейс пользователя в стиле представлений дерева/списка, аналогичный Проводнику Windows, который соответствует иерархической структуре самого реестра. Однако, как мы скоро увидим, существуют некоторые различия.

    В файловой системе узлами самого верхнего уровня можно считать разделы диска C:\, D:\ и т.д. В реестре эквивалентом разделу является улей реестра. Невозможно изменить существующие ульи, они являются фиксированными, и всего существует семь ульев (хотя с помощью

    regedit
    можно увидеть только пять):

    □ HKEY_CLASSES_ROOT (HKCR) содержит данные о типах файлов в системе (

    .txt
    ,
    .doc
    и т.д.) и приложениях, которые могут открывать файлы каждого типа. Включает также информацию о регистрации для всех компонентов COM (эта область является обычно наибольшей областью реестра, так как Windows сегодня поставляется с огромным числом компонентов COM).

    □ HKEY_CURRENT_USER (HKCU) содержит данные о предпочтениях пользователя, который в данный момент зарегистрирован на машине.

    □ HKEY_LOCAL_MACHINE (HKLM) является огромным ульем, который содержит данные обо всем программном обеспечении и оборудовании, установленном на машине. Он также содержит улей HKCR: HKCR в действительности не является независимым ульем со своими собственными правами, а является просто удобным отображением на ключ реестра

    HKLM/SOFTWARE/Classes
    .

    □ HKEY_USERS (HKUSR) содержит данные о пользовательских предпочтениях всех пользователей. Как можно догадаться, он содержит также улей HKCU, который является отображением на один из ключей в HKEY_USERS).

    □ HKEY_CURRENT_CONFIG (HKCF) содержит данные об оборудовании компьютера.

    Оставшиеся два ключа содержат информацию, которая имеет временный характер и которая часто изменяется:

    □ HKEY_DYN_DATA является общим контейнером для любых изменчивых данных, которые необходимо хранить где-то в реестре.

    □ HKEY_PERFORMANCE_DATA содержит данные, связанные с производительностью выполняющихся приложений.

    Внутри ульев находится древовидная структура ключей реестра. Каждый ключ является во многом аналогом папки или файла в файловой системе. Однако существует одно очень важное различие: файловая система различает файлы (которые предназначены для хранения данных) и папки (которые предназначены прежде всего для хранения других файлов или папок), но в реестре существуют только ключи. Ключ может содержать как данные, так и другие ключи.

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

    Эту структуру можно увидеть, используя

    regedit
    для проверки ключей реестра. На экране показано содержимое ключа HKCU/Control Panel/Appearance, в котором находятся данные выбранной цветовой схемы зарегистрированного в данный момент пользователя. regedit показывает, какой ключ проверяется, выводя его с изображением открытой папки в древовидном представлении:

    Ключ HKCU/Control Panel/Appearance имеет три заданных именованных значения, хотя значение по умолчанию не содержит никаких данных. Столбец на экране, помеченный Type, указывает тип данных каждого значения. Записи в реестре можно форматировать как один из трех типов данных. Этими типами являются REG_SZ (что грубо соответствует экземпляру строки .NET — соответствие неточное, так как типы данных реестра не являются типами данных .NET), REG_DWORD (грубо соответствует

    uint
    ), и REG_BINARY (массив байтов).

    Приложение, которое хочет сохранить данные в реестре, будет делать это создавая ряд ключей реестра, обычно с ключом HKLM/Software/<ИмяКомпании>. Отметим, что эти ключи не обязательно должны содержать какие-либо данные. Иногда сам факт существования ключа предоставляет достаточно информации для приложения.

    Классы реестра в .NET

    Доступ к реестру осуществляется с помощью двух классов в пространстве имен

    Microsoft.Win32
    Registry
    и
    RegistryKey
    . Экземпляр
    RegistryKey
    представляет ключ реестра. Этот класс реализует методы для доступа к ключам-потомкам, для создания новых ключей или для чтения или изменения значений ключа. Другими словами, чтобы делать все необходимое с ключом реестра (за исключением задания уровней безопасности для ключа).
    RegistryKey
    является классом, который будет использоваться практически для любой работы с реестром.
    Registry
    , напротив, является классом, экземпляры которого никогда не создаются. Его роль состоит в предоставлении экземпляров
    RegistryKey
    , которые являются ключами верхнего уровня, различными ульями, чтобы начать перемещение по реестру.
    Registry
    предоставляет эти экземпляры через семь статических свойств, называемых соответственно
    ClassesRoot
    ,
    CurrentConfig
    ,
    CurrentUser
    ,
    DynData
    ,
    LocalMachine
    ,
    PerformanceData
    и
    Users
    .

    Поэтому, например, чтобы получить экземпляр

    RegistryKey
    , который представляет ключ HKLM, необходимо написать:

    RegistryKey Hklm = Registry.LocalMachine;

    Процесс получения ссылки на объект

    RegistryKey
    называют открытием ключа.

    Можно было бы ожидать, что методы объекта

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

    Наиболее очевидное различие состоит в том, как открывают ключ реестра в заданном месте в реестре. Класс

    Registry
    не имеет никаких открытых конструкторов, которые можно использовать, он не имеет также никаких методов, которые позволят перейти прямо к ключу, задавая его имя. Вместо этого ожидается, что вы спуститесь вниз к этому ключу с вершины соответствующего улья. Если желательно создать экземпляр объекта
    RegistryKey
    , то единственный способ — начать с соответствующего статического свойства
    Registry
    и двигаться оттуда вниз. Поэтому, например, если нужно прочитать некоторые данные в ключе
    HKLM/Software/Microsoft
    , то ссылку на него можно получить следующим образом:

    RegistryKey Hklm = Registry.LocalMachine;

    RegistryKey HkSoftware = Hklm.OpenSubKey("Software");

    RegistryKey HkMicrosoft = HkSoftware.OpenSubKey("Microsoft");

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

    OpenSubKey
    , которая получает второй параметр типа
    bool
    , указывающий, требуется ли иметь доступ к ключу для чтения-записи. Поэтому, например, при желании модифицировать ключ
    Microsoft
    (предполагая, что вы являетесь системным администратором с полномочиями на это), необходимо написать следующее:

    RegistryKey Hklm = Registry.LocalMachine;

    RegistryKey HkSoftware = Hklm.OpenSubKey("Software");

    RegistryKey HkMicrosoft = HkSoftware.OpenSubKey("Microsoft", true);

    В связи с этим, так как этот ключ содержит информацию, используемую приложениями Microsoft, в большинстве случаев нежелательно модифицировать этот конкретный ключ.

    Метод

    OpenSubKey()
    будет вызываться, если ожидается, что ключ уже присутствует. Если ключ отсутствует, то он возвращает ссылку null. Если желательно создать ключ, то необходимо использовать метод
    CreateSubKey()
    (который автоматически предоставляет доступ к ключу для чтения-записи через возвращаемую ссылку):

    RegistryKey Hklm = Registry.LocalMachine;

    RegistryKey HkSoftware = Hklm.OpenSubKey("Software");

    RegistryKey HkMine = HkSoftware.CreateSubKey("MyOwnSoftware");

    Способ работы

    CreateSubKey()
    достаточно интересен: он будет создавать ключ, если тот еще не существует, а если уже существует, то будет возвращать экземпляр
    RegistryKey
    , который представляет существующий ключ. Причина такого поведения метода состоит в способе использования реестра. Реестр в целом содержит долговременные данные, например, конфигурационную информацию для Windows и различных приложений. Поэтому не очень часто возникает ситуация, когда необходимо явно создать ключ.

    Более распространена ситуация, когда приложению необходимо убедиться, что некоторые данные присутствуют в реестре, другими словами, создать соответствующие ключи, если они еще не существуют, но ничего не делать, если они существуют. Метод

    CreateSubKey()
    прекрасно с этим справляется. В отличие, например, от ситуации с
    FileInfo.Open()
    у
    CreateSubKey()
    нет возможности случайно удалить какие-либо данные. Если целью действительно является удаление ключей реестра, то необходимо явно вызвать метод
    RegistryKey.Delete()
    . Это имеет смысл с учетом важности реестра для Windows. Меньше всего хотелось бы полностью разрушить Windows, удалив пару важных ключей во время отладки обращений к реестру в C#.

    Когда ключ реестра для чтения или модификации найден, можно использовать методы

    SetValue()
    или
    GetValue()
    для задания или получения из них данных. Оба эти метода получают в качестве параметра строку, задающую имя значения, a
    SetValue()
    требует дополнительно ссылку на объект, содержащий детали значения. Так как параметр определяется как объектная ссылка, то он может действительности быть ссылкой на любой класс по желанию.
    SetValue()
    будет определять по типу реально предоставленного класса, как задать значение
    REG_SZ
    ,
    REG_DWORD
    или
    REG_BINARY
    . Например:

    RegistryKey HkMine = HkSoftware.CreateSubKey("MyOwnSoftware");

    HkMine.SetValue("MyStringValue", "Hello World");

    HkMine.SetValue(MyIntValue", 20);

    Этот код задает для ключа два значения:

    MyStringValue
    будет иметь тип
    REG_SZ
    , а
    MyIntValue
    — тип
    REG_DWORD
    . В последующем примере будут рассмотрены только эти два типа.

    RegistryKey.GetValue()
    работает по большей части таким же образом. Он определен для возврата объектной ссылки, а значит, он может на самом деле вернуть ссылку на
    string
    , если обнаружит значение типа
    REG_SZ
    , и
    int
    , если это значение имеет тип
    REG_DWORD
    :

    string StringValue = (string)HkMine.GetValue("MyStringValue");

    int IntValue = (int)HkMine.Get.Value("MyIntValue");

    И наконец, по окончании чтения или модификации данных ключ необходимо закрыть:

    HkMine.Close();

    Помимо этих методов,

    RegistryKey
    реализует большое число других методов и свойств. Таблицы представляют выборку наиболее употребительных:

    Свойства

    Имя свойства Назначение
    Name
    Имя ключа (только для чтения)
    SubKeyCount
    Число потомков этого ключа
    ValueCount
    Сколько значений содержит ключ

    Методы

    Имя Назначение
    Close()
    Закрывает ключ
    CreateSubKey()
    Создает подключ с заданным именем (или открывает его, если он уже существует)
    DeleteSubKey()
    Удаляет заданный подключ
    DeleteSubKeyTree()
    Рекурсивно удаляет подключ и всех его потомков
    DeleteValue()
    Удаляет именованное значение из ключа
    GetSubKeyNames()
    Возвращает массив строк, содержащих имена подключей
    GetValue()
    Возвращает именованное значение
    GetValueNames()
    Возвраает массив строк, содержащих имена всех значений ключа
    OpenSubKey()
    Возвращает ссылку на экземпляр
    RegistryKey
    , который представляет заданный подключ
    SetValue()
    Задает именованное значение

    Пример: SelfPlacingWindow

    Проиллюстрируем использование классов реестра с помощью приложения, которое называется

    SelfPlacingWindow
    . Этот пример является простым приложением Windows на C#, которое в действительности почти не имеет свойств. Единственное, что можно сделать в этом приложении, это щелкнуть по кнопке, что приведет к появлению стандартного диалогового окна выбора цветов в Window (представляемому классом
    System.Windows.Forms.ColorDialog
    ), чтобы можно было выбрать цвет который станет фоновым цветом формы

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

    Местом, где

    SelfPlacingWindow
    хранил свою информацию в реестре, является ключ
    HKLM/Software/WroxPress/SelfPlacingWindow
    . HKLM является обычным местом для хранения информации о конфигурации приложений, но отметим, что оно не является специфическим для пользователя. Вероятно, вам понадобится скопировать эту информацию в улей HK_Users, чтобы каждый пользователь имел свой собственный профиль.

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

    Если теперь изменить цвет фона и переместить или изменить размер окна приложения

    SelfPlacingWindow
    , оно создаст перед завершением ключ
    HKLM/Software/WroxPress/SelfPlacingWindow
    и запишет в него свою новую конфигурационную информацию. Можно проверить эту информацию с помощью
    regedit
    :

    На этом экране можно видеть, что

    SelfPlacingWindow
    помещает ряд значений в ключ реестра. 

    Значения

    Red
    ,
    Green
    и
    Blue
    задают компоненты цветов, которые формируют выбранный цвет фона. О компонентах цвета подробно рассказывается в главе 21. Любой изображаемый цвет в системе может быть полностью описан этими тремя компонентами, каждый из которых представляется числом между 0 и 255 (или
    0x00
    и
    0xff
    в шестнадцатеричном представлении). Указанные здесь значения задают ярко-зеленый цвет. Существуют также четыре дополнительных значения
    REG_DWORD
    , которые представляют положение и размер окна:
    X
    и
    Y
    являются координатами верхнего левого угла окна на рабочем столе.
    Width
    и
    Height
    задают размер окна
    WindowsState
    является единственным значением, для которого мы использовали строковый тип данных (
    REG_SZ
    ), и оно может содержать одну из строк
    normal
    ,
    maximized
    или
    minimized
    , в зависимости от конечного состояния окна при выходе из приложения.

    Если теперь запустить

    SelfPlacingWindow
    снова, то оно будет считывать этот ключ реестра и автоматически позиционировать себя соответственно:

    В этот раз, когда происходит выход из

    SelfPlacingWindow
    , приложение будет перезаписывать предыдущие настройки в реестре новыми значениями, существующими во время выхода из приложения. Чтобы создать код примера, мы создаем проект Windows Forms в Visual Studio.NET и добавляем окно списка и кнопку, используя набор инструментов среды разработчика. Мы изменим имена этих элементов управления соответственно на
    listBoxMessages
    и
    buttonChooseColor
    . Также необходимо обеспечить использование пространства имен
    Microsoft.Win32
    .

    using System;

    using System.Drawing;

    using System.Collections;

    using System.ComponentModel;

    using System.Windows.Forms;

    using System.Data;

    using Microsoft.Win32;

    Нам необходимо добавить одно поле к основному классу

    Form1
    , который будет представлять окно диалога выбора цвета.

    public class Form1 : System.Windows.Forms.Form {

     /// <summary>

     /// Обязательная переменная проектировщика.

     /// </summary>

     private System.СomponentModel.Container components;

     private System.Windows.Forms.ListBox ListBoxMessages;

     private system.Windows.Forms.Button buttonChooseColor;

     ColorDialog ChooseColorDialog = new ColorDialog();

    Довольно много действий происходит в конструкторе Form1:

    public Form1() {

     InitializeComponent();

     buttonChooseColor.Click += new EventHandler(OnClickChooseColor);

     try {

      if (ReadSettings() == false)

    listBoxMessages.Items.Add("No information in registry");

      else

       listBoxMessages.Items.Add("Information read in from registry");

      StartPosition = FormStartPosition.Manual;

     } catch (Exception e) {

      listBoxMessages.Items.Add("A problem occured reading in data from registry:");

      listBoxMessages.Items.Add(e.Message);

     }

    }

    В этом конструкторе мы начинаем с создания метода обработки события нажатия пользователем кнопки. Обработчиком является метод с именем

    OnClickChooseColor
    (см. ниже). Считывание конфигурационной информации делается с помощью другого метода —
    ReadSettings()
    .
    ReadSettings()
    возвращает
    true
    , если находит информацию в реестре, и
    false
    , если не находит (что будет, по-видимому, иметь место, так как приложение выполняется первый раз). Мы помещаем эту часть конструктора в блок
    try
    на случай возникновения каких-либо исключений при считывании значений реестра (это может произойти, если вмешался некоторый пользователь и сделал какие-то изменения с помощью
    regedit
    ).

    Инструкция

    StartPosition = FormStartPosition.Manual;
    говорит форме взять свою начальную позицию из свойства
    DeskTopLocation
    вместо используемого по умолчанию положения в Window (поведение по умолчанию). Возможные значения берутся из перечисления
    FormStartPosition
    .

    SelfPlacingWindow
    также является одним из немногих приложений в этой книге, для которого существенно используется добавление кода в метод
    Dispose()
    . Напомним, что
    Dispose()
    вызывается, когда приложение завершается нормально, так что это идеальное место для сохранения конфигурационной информации в реестре. Это делается с помощью другого метода, который будет написан,—
    SaveSettings()
    :

    /// <summary>

    /// Очистить все использованные ресурсы

    /// </summary>

    public override void Dispose() {

     SaveSettings();

     base.Dispose();

     if(components != null) components.Dispose();

    }

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

    void OnClickChooseColor(object Sender, EventArgs e) {

     if (ChooseColorDialog.ShowDialog() == DialogResult.OK)

      BackColor = ChooseColorDialog.Color;

    }

    Теперь посмотрим, как сохраняются настройки:

    void SaveSettings() {

     RegistryKey SoftwareKey = Registry.LocalMachine.OpenSubKey("Software", true);

     RegistryKey WroxKey = SoftwareKey.CreateSubKey("WroxPress");

     RegistryKey SelfPlacingWindowKey = WroxKey.CreateSubKey("SelfPlacingWindowKey");

     SelfPlacingWindowKey.SetValue("BackColor", (object)BackColor.ToKnownColor());

     SelfPlacingWindowKey.SetValue("Red", (object)(int) BackColor.R);

     SelfPlacingWindowKey.SetValue("Green", (object)(int)BackColor.G);

     SelfPlacingWindowKey.SetValue("Blue", (object)(int)Backcolor.В);

     SelfPlacingWindowKey.SetValue("Width", (object)Width);

     SelfPlacingWindowKey.SetValue("Height", (object)Height);

     SelfPlacingWindowKey.SetValue("X", (object)DesktopLocation.X);

     SelfPlacingWindowKey.SetValue("Y", (object)DesktopLocation.Y);

     SelfPlacingWindowKey.SetValue("WindowState", (object)WindowState.ToString());

    }

    Мы начали с перемещения в реестре, чтобы получить ключ реестра

    HKLM/Software/WroxPress/SelfPlacingWindow
    с помощью продемонстрированной выше техники, начиная со статического свойства
    Registry.LocalMachine
    , которое представляет улей
    HKLM
    :

    RegistryKey SoftwareKey = Registry.LocalMachine.OpenSubKey("Software" , true);

    RegistryKey WroxKey = SoftwareKey.CreateSubKey("WroxPress");

    RegistryKey SelfPlacingWindowKey = WroxKey.CreateSubKey("SelfPlacingWindowKey");

    Мы используем метод

    RegistryKey.OpenSubKey()
    , а не
    RegistryKey.CreateSubKey()
    , позволявший добраться до ключа
    HKLM/Software
    . Так происходит вследствие уверенности, что этот ключ уже существует, в противном случае имеется серьезная проблема с компьютером, так как этот ключ содержит настройки для большого объема системного программного обеспечения. Мы также указываем, что нам требуется доступ для записи в этот ключ. Это вызвано тем, что если ключ
    WroxPress
    еще не существует, нам нужно будет его создать, что включает запись в родительский ключ.

    Следующий ключ для перехода —

    HKLM/Software/WroxPress
    , но так как мы не уверены, что ключ уже существует, то используем
    CreateSubKey()
    для его автоматического создания, если он не существует. Отметим, что
    CreateSubKey()
    автоматически предоставляет доступ для записи к рассматриваемому ключу. Когда мы достигнем
    HKLM/Software/Wrox
    .
    Press/SelfPlacingWindow
    , то останется просто вызвать метод
    RegistryKey.SetValue()
    несколько раз, чтобы создать или задать соответствующие значения. Существуют, однако, некоторые осложнения.

    Первое. Можно заметить что мы задействуем пару классов, которые раньше не встречались: свойство

    DeskTopPosition
    класса
    Form
    указывает позицию верхнего левого угла экрана и имеет тип
    Point
    . Рассмотрим структуру
    Point
    в главе GDI+. Здесь необходимо знать только, что она содержит два целых числа —
    X
    и
    Y
    , которые представляют горизонтальную и вертикальную позиции на экране. Мы также используем три свойства члена класса
    Color
    :
    R
    ,
    G
    и
    B
    .
    Color
    представляет цвет, а его свойства задают красный, зеленый и синий компоненты, которые составляют цвет и имеют тип
    byte
    . Также применяется свойство
    Form
    .
    WindowState
    , содержащее перечисление, которое задает текущее состояние окна —
    minimized
    ,
    maximized
    или
    restored
    .

    Отметим, что при преобразовании типов

    SetValue()
    получает два параметра: строку, которая задает имя ключа, и экземпляр
    System.Object
    , содержащий значение.
    SetValue
    имеет возможность выбора формата для хранения значения, он может сохранить его как
    REG_SZ
    ,
    REG_BINARY
    или
    REG_DWORD
    , и он в действительности делает правильный выбор в зависимости от заданного типа данных. Поэтому для
    WindowsState
    передается строка и
    SetValue()
    определяет, что она должна быть преобразована в
    REG_SZ
    . Аналогично для различных позиций и размеров, которые мы передаем, целые значения будут преобразованы в
    REG_DWORD
    . Однако компоненты цвета являются более сложными, но мы хотим, чтобы они также хранились как
    REG_DWORD
    , потому что они имеют числовые типы. Однако если метод
    SetValue()
    видит, что данные имеют тип
    byte
    , он будет сохранять их гак строку
    REG_SZ
    в реестре. Чтобы избежать этого, преобразуем компоненты цвета в
    int
    .

    Мы также явно преобразуем все значения в тип

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

    Метод

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

    bool ReadSettings() {

     RegistryKey SoftwareKey = Registry.LocalMachine.OpenSubKey("Software"));

     RegistryKey WroxKey = SoftwareKey.OpenSubKey("WroxPress");

     if (WroxKey == null) return false;

     RegistryKey SelfPlacingWindowKey = WroxKey.OpenSubKey("SelfPlacingWindow");

     if (SelfPlacingWindowKey == null) return false;

     else

    listBoxMessages.Items.Add("Successfully opened key " + SelfPlacingWindowKey.ToString());

     int RedComponent = (int)SelfPlacingWindowKey.GetValue("Red");

     int GreenComponent = (int)SelfPlacingWindowKey.GetValue("Green");

     int BlueComponent = (int)SelfPlacingWindowKey.GetValue("Blue");

     BackColor = Color.FromArgb(RedComponent, GreenComponent, BlueComponent);

     listBoxMessages.Items.Add("Background color: " + BackColor.Name);

     int X = (int(SelfPlacingWindowKey.GetValue("X");

     int Y = (int)SelfPlacingWindowKey.GetValue("Y");

     DesktopLocation = new Point(X, Y);

     listBoxMessages.Items.Add("Desktop location: " + DesktopLocation.ToString());

     Height = (int)SelfPlacingWindowKey.GetValue("Height");

     Width = (int)SelfPlacingWindowKey.GetValue("Width");

     listBoxMessages.Items.Add("Size: " + new Size(Width, Height).ToString());

     string InitialWindowState = (string)SelfPlacingWindowKey.GetValue("WindowState");

     listBoxMessages.Items.Add("Window State: " + InitialWindowState);

     WindowState =

      (FormWindowState)FormWindowState.Parse(WindowState.GetType() , InitialWindowState)

     return true;

    }

    В

    ReadSettings()
    мы должны сначала перейти в ключ реестра
    HKLM/Software/WroxPress/SelfPlacingWindow
    . При этом, однако, мы надеемся найти ключ, чтобы его можно было прочитать. Если его нет, то, вероятно, пример выполняется в первый раз. В этом случае мы хотим прервать чтение ключей, и, конечно, не желаем создавать какие-либо ключи. Теперь мы все время используем метод
    RegistryKey.OpenSubkey()
    . Если на каком-то этапе
    OpenSubkey()
    возвращает ссылку null, то мы знаем, что ключ реестра отсутствует, и можем вернуть значение
    false
    в вызывающий код.

    В реальности для считывания ключей используется метод

    RegistryKey.GetValue()
    , который определен как возвращающий объектную ссылку, это означает, что такой метод может на самом деле вернуть экземпляр практически любого класса, который он выберет подобно
    SetValue()
    , он возвращает класс объекта, соответствующий типу данных, которые он найдет в ключе. Поэтому можно предполагать, что ключ
    REG_SZ
    будет выдан как строка, а другие ключи — как
    int
    . Мы также преобразуем соответственно возвращаемую из
    SetValue()
    ссылку. При возникновении исключения, если кто-то делал какие-то манипуляции с реестром и исказил типы данных, наше преобразование породит исключение, которое будет перехватываться обработчиком в конструкторе
    Form1
    .

    Остальная часть кода использует еще один тип данных, структуру

    Size
    , выглядящую пока незнакомой, потому что она будет рассматриваться только в главе GDI+. Структура
    Size
    аналогична
    Point
    , но используется для представления размеров, а не координат. Она имеет два свойства члена —
    Width
    и
    Height
    , и мы используем структуру
    Size
    в данном случае просто как удобный способ представления размера формы для вывода в поле списка.

    Заключение

    В этой главе было рассмотрено использование базовых классов .NET для доступа к реестру и файловой системе из кода C#. Мы видели, что базовые классы предоставляют достаточно мощные и при этом доступные объектные модели для создания, модификации или чтения ключей в случае реестра, а в случае файловой системы — для копирования файлов, перемещения, создания и удаления файлов и папок, а так же для чтения и записи текстовых или двоичных файлов.

    В этой главе предполагалось, что код должен выполняться с помощью учетной записи, которая имеет необходимые полномочия доступа для выполнения действий, требуемых кодом. Очевидно, что вопрос безопасности является важным вопросом там, где дело касается доступа к файлам, и эта область будет рассмотрена в главе 25. 







     

    Главная | В избранное | Наш E-MAIL | Добавить материал | Нашёл ошибку | Наверх