|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Глава 14Операции с файлами и реестром В этой главе будет рассмотрено выполнение в C# задач, включающих чтение и запись в файлы и в системный реестр. В частности, будут охвачены следующие вопросы: □ Исследование структуры каталога, выяснение, какие файлы и папки присутствуют и проверка их свойств □ Перемещение, копирование и удаление файлов и папок □ Чтение и запись текста в и из файлов □ Чтение и запись в реестр Компания Microsoft предоставила очень интуитивную объектную модель, охватывающую эти области, и в ходе этой главы мы покажем, как использовать классы на основе .NET для выполнения упомянутых выше задач. Для случая операций с файловой системой соответствующие классы почти все находятся в пространстве имен System.IO, в то время как операции с реестром связаны с парой классов в пространстве имен System.Win32.
Отметим, что хотя безопасность влияет на все области, но особенно она важна при модификации файлов или записей реестра. Система безопасности рассматривается отдельно в главе 25. В этой главе мы будем предполагать, что имеются достаточные права доступа для выполнения всех примеров, которые модифицируют файлы или записи реестра, что обеспечивается, например, при использовании учетной записи с полномочиями администратора. Управление файловой системойКлассы, которые используются для просмотра файловой системы и выполнения таких операций, как перемещение, копирование и удаление файлов, показаны на следующей диаграмме. Пространство имен каждого класса показано в скобках под именем каждого класса на диаграмме: Назначение этих классов следующее: □ System.MarshalByRefObject— класс базового объекта для классов .NET, которые являются удаленными. Допускает маршализацию данных между прикладными доменами. □ FileSystemInfo— базовый класс, который представляет любой объект файловой системы. □ FileInfoи File— представляют файл в файловой системе. □ DirectoryInfoи Directory— представляют папку в файловой системе. □ Path— содержит статические члены, которые можно использовать для манипуляций именами путей доступа.
Классы .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) найти о нем информацию, используя ряд свойств, включающих:
Можно также выполнить действия на объекте файловой системы с помощью следующих методов:
Отметим, что приведенные выше таблицы показывают основные свойства и методы, и не являются исчерпывающими.
Интересно то, что время создания, время последнего доступа, и время последней записи являются изменяемыми: // 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, 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.
Основная часть метода представлена в двух вложенных циклах 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реализует большое число других методов и свойств. Таблицы представляют выборку наиболее употребительных: Свойства
Методы
Пример: 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 | Добавить материал | Нашёл ошибку | Наверх | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|