Создание Windows-приложения на C++, редактирующего бинарный файл Введение 1. Вначале создаем пустое решение (solution) с именем solWriteReadFiles. Для чего a. В меню File командой New->Projects… открываем окно New Project (новый проект). b. На левой панели окна Project types: (типы проектов) в разделе Other Project Types (другие типы проектов) выбираем команду Visual Studio Solutions (решения). c. На правой панели Templates: (шаблоны) выбираем Blank Solution (пустое решение). d. В нижней части окна в строке Location (положение) с помощью кнопки Browse… (просмотр) выбираем каталог …Documents\Visual Studio 2008\Projects. В этот каталог среда поместит каталог решения. e. В строке Name: (имя) набираем имя решения solWriteReadFiles. 2. В решение solWriteReadFiles добавляем проект консольного приложения. Для чего a. Из меню View открываем окно Solution Explorer (обозреватель решения). b. Щелкаем правой кнопкой над именем решения solWriteReadFiles, открывая контекстное меню. c. Выбираем команду Add->New Project… (добавь->новый проект). Это открое окно New Project. d. На левой панели Project Types: выбираем раздел Visual C++ и в нем команду CLR (Common Language Runtime, или общий язык времени выполнения). e. На правой панели Templates: выбираем шаблон CLR Console Application (консольное приложение). f. В строке Name: набираем имя prWriteReadFiles. Так будет называться консольное приложение. 3. Заменяем заголовок и содержание метода main текстом, составляющим код приложения по созданию, записи и чтения файлов: using namespace System::IO; using namespace System::Data; using namespace System::Xml; void main() { // Описывается и создается в памяти объект rnd типа Random // Он позволяет генерировать псевдослучайные числа Random^ rnd = gcnew Random(); // Описывается переменная типа Double Double d; // Переменной типа double присваивается случайное значение в интервале [0;1) d = rnd->NextDouble(); // На консоль выводится значение переменной d Console::WriteLine("Число {0} получено из генератора случайных чисел", d); 1 // Работа с бинарным файлом // Описывается массив байтов array<Byte>^ bytes; // Запись в бинарный файл Console::WriteLine("Запись в бинарный файл числа с плавающей запятой"); // Для хранения битового представления переменной типа double // в массив bytes засылаются байты, из которых состоит переменная d bytes = BitConverter::GetBytes(d); // Создается объект s типа Stream, ассоциированный с бинарным файлом Test. // Файл создается в текущей директории - там, где находится exe-файл процесса Stream^ s = File::Create("Test"); // В файл записывается массив с бинарным содержимым переменной типа double s->Write(bytes, 0, bytes->Length); // Закрывается объект s (поток) и файл, ассоциированный с ним, освобождается s->Close(); Console::WriteLine("Число {0} записано в бинарный файл Test",d); Console::ReadLine(); // Инициализация элементов массива. Не обязательная процедура. // В данном слуае происходит обнуление всех байтов для "чистоты эксперимента". bytes->Initialize(); // Считывание из бинарного файла Console::WriteLine("Считывание из бинарного файла числа с плавающей запятой"); // Создается объект s типа Stream, // ассоциированный с существующим бинарным файлом Test. s = File::OpenRead("Test"); // В массив считывается содержимое файла s->Read(bytes, 0, bytes->Length); // Закрывается объект s (поток) и файл, ассоциированный с ним, освобождается s->Close(); // На консоль выводится содержимое массива в формате переменной типа double Console::WriteLine("Число {0} прочитано из бинарного файла Test", BitConverter::ToDouble(bytes, 0)); Console::ReadLine(); // Запись в текстовой файл Console::WriteLine("Запись в текстовой файл числа с плавающей запятой"); // Создается объект tw типа TextWriter, // которому ставится в соответствие файл для записи текста TextWriter^ tw = File::CreateText("Test.txt"); 2 // В файл записывается число типа double и сносится строка tw->WriteLine(d); // Закрывается объект tw (поток) и файл, ассоциированный с ним, освобождается tw->Close(); Console::WriteLine("Число {0} записано в текстовой файл Test.txt",d); Console::ReadLine(); // Чтение из текстового файла // Создается объект tr типа TextReader, ассоциированный с текстовым файлом, TextReader^ tr = File::OpenText("Test.txt"); // В строку str считывается строка текста из потока tr String^ str = tr->ReadLine(); // Закрывается объект tr (поток) и файл, ассоциированный с ним, освобождается tr->Close(); // Строка str выводится на консоль Console::WriteLine(str); Console::ReadLine(); //Запись в xml-формате Console::WriteLine("Запись в xml-файл числа с плавающей запятой"); // Создается объект xmlw типа XmlWriter, ассоциированный с xml-файлом XmlWriter^ xmlw = XmlWriter::Create("Test.xml"); // Записывается узел типа элемент xmlw->WriteStartElement("aDouble"); // Записывается содержание узла aDouble xmlw->WriteValue(d); // файл освобождается и объект xmlw закрывается xmlw->Close(); // Чтение из xml-файла // Создается объект xmlr типа XmlReader, ассоциированный с xml-файлом XmlReader^ xmlr = XmlReader::Create("Test.xml"); // Считываются все узлы до конца файла while (xmlr->Read()) if (xmlr->NodeType == XmlNodeType::Text) // Если узел текстовой Console::WriteLine(xmlr->Value);// Значение узла распечатывается Console::ReadLine(); // файл освобождается и объект xmlr закрывается xmlr->Close(); // Запись в xml-файл кэшированных данных Console::WriteLine("Запись в xml-файл кэшированной базы данных"); // Создается объект ds типа DataSet, // кэширующий данные в памяти, с именем aDataSet 3 DataSet^ ds = gcnew DataSet("aDataSet"); // К объекту ds добавляется таблица с именем aTable ds->Tables->Add(gcnew DataTable("aTable")); // К таблице добавляется колонка с именем aColumn ds->Tables["aTable"]->Columns->Add(gcnew DataColumn("aColumn")); // К таблице добавляется строка ds->Tables[0]->Rows->Add(ds->Tables[0]->NewRow()); // В элемент 0,0 таблицы заносится число d ds->Tables[0]->Rows[0][0] = d; // Данные объекта ds записываются во вновь создаваемый xml-файл ds->WriteXml("Test.ds.xml");//Console.Out // Считывание xml-файла в память // Создается объект dsr класса DataSet без имени DataSet^ dsr = gcnew DataSet(); // В объект считывается информация из xml-файла dsr->ReadXml("Test.ds.xml"); // Содержимое 0,0-элемента таблицы aTable выводится на консоль Console::WriteLine(dsr->Tables["aTable"]->Rows[0]["aColumn"]); Console::ReadLine(); } 4. Из меню Build (построить) компилируем приложение командой Rebuild prWriteReadFiles. Если возникают ошибки, проверяем внимательно, верно ли выполнено копирование кода. 5. Если ошибок нет, активируем приложение командой Start Debugging (зеленая стрелочка). Приложение должно выполняться до конца без ошибок. При этом последовательно, в нужных местах (посмотрите, в каких) следует нажимать клавишу Enter. 6. Попытайтесь отредактировать приложение, записывая в файл a. случайное число типа int, b. массив случайной длины, состоящий из случайных чисел Windows-приложение на C++, редактирующее бинарный файл Целью следующего приложения является возможность визуального редактирования содержимого байтового потока, связанного с бинарным файлом. 1. Открыть решение solWriteReadFiles. 2. В окне Solution Explorer над заголовком решения solWriteReadFiles в контекстном меню (правая кнопка мышки) выбрать команду Add->New Project… 3. В открывшемся окне Add New Project a. на левой панели Project Types: выбрать узел Other Languages->Visual C++->CLR, b. на правой панели Templates: выбрать шаблон Windows Forms Application c. в строке Name: набрать имя проекта prEditBinFile d. щелкнуть OK 4. Должно появиться окно дизайнера проекта с именем Form1.h [Design] с изображением пустого окна приложения серого цвета с заголовком Form1. 4 В этом окне визуально редактируются свойства окна приложения, изображенного серым цветом. 1. Раздвинем серое окно примерно на половину поля внешнего окна, захватив курсором мышки небольшие квадраты, расположенные на правой и нижней границе окна, и потянув мышку вправо и вниз соответственно, либо потянув за квадрат в нижнем правом углу по диагонали вправо и вниз. 2. Изменим другие свойства окна приложения, используя окно Properties (если окно Properties не открыто, это можно сделать через меню View среды) a. В окне Properties найдем свойство Text, содержащее в правой колонке текст Form1. Это заголовок окна по умолчанию. Изменим значение этого свойства на Edit Binary File. После нажатия Enter это изменение должно появиться в заголовке. b. Значение другого свойства Start Position (начальное положение окна на экране после запуска приложения), по умолчанию равного WindowsDefaultLocation (положение по умолчанию), изменим на CenterScreen. Теперь после запуска приложения окно будет изображаться в центре экрана. 3. В окне Solution Explorer вызовем контекстное меню над заголовком проекта prEditBinFile и выберем команду Set As Startup Project. Теперь при активации решения кнопкой с зеленой стрелкой (команда Start Debugging) активироваться будет именно последний проект prEditBinFile. 4. Активируем приложение, чтобы увидеть результат. Пока пустое окно с заголовком Edit Binary File должно появиться в центре экрана. Файловый состав приложения Созданное приложение состоит из ряда файлов, содержание которых может быть просмотрено через окно Solution Explorer. Некоторые файлы относятся к так называемым «заголовочным файлам», или «хэдерам». Ссылки на хэдеры расположены в узле “Header Files”и имеют расширение .h. Другие файлы относятся к файлам-ресурсам и ссылки на них находятся в узле “Resource Files”. Наконец, третий тип файлов – «файлы-исходники» имеют расширения .cpp и ссылки в узле “Source Files”. При создании оконного приложения основной файл, который подвергается непосредственному редактированию программистом – заголовочный файл Form1.h (по умолчанию), содержащий код формы приложения. Редакция остальных файлов происходит опосредовано инструментами среды при изменении свойств проекта. Команда Code из меню View открывает панель с кодом файла Form1.h. Часть заголовочного файла Form1.h редактируется дизайнером среды. Эта часть расположена в разделе кода, ограниченного строками: #pragma region Windows Form Designer generated code … #pragma endregion Щелчок по знаку минус на левом поле первой строки позволяет сжать раздел до одной строки с заголовком Windows Form Designer generated code. Таким образом, любую часть кода можно выделять в отдельный раздел. Файл Form1.h содержит описание класса с именем Form1. 5 Пространство имен Описание помещено в так называемое пространство имен prEditBinFile, открывающееся кодом namespace prEditBinFile { using namespace System; using namespace System::ComponentModel; using namespace System::Collections; using namespace System::Windows::Forms; using namespace System::Data; using namespace System::Drawing; … } Здесь перечислены ссылки к тем пространствам имен, к классам которых обращается код, приведенный в файле. Класс Form1 Далее следует собственно описание класса Form1, состоящее из заголовка public ref class Form1 : public System::Windows::Forms::Form { // Здесь описываются члены класса } и описания членов класса – полей, свойств, методов. Среда предоставляет по умолчанию три метода и одно поле, которые выглядят следующим образом public: Form1(void) { InitializeComponent(); // //TODO: Add the constructor code here // } protected: /// <summary> /// Clean up any resources being used. /// </summary> ~Form1() { if (components) { delete components; } } 6 private: /// <summary> /// Required designer variable. /// </summary> System::ComponentModel::Container ^components; #pragma region Windows Form Designer generated code /// <summary> /// Required method for Designer support - do not modify /// the contents of this method with the code editor. /// </summary> void InitializeComponent(void) { this->SuspendLayout(); // // Form1 // this->AutoScaleDimensions = System::Drawing::SizeF(6, 13); this->AutoScaleMode = System::Windows::Forms::AutoScaleMode::Font; this->ClientSize = System::Drawing::Size(748, 482); this->Name = L"Form1"; this->StartPosition = System::Windows::Forms::FormStartPosition::CenterScreen; this->Text = L"Edit Binary File"; this->ResumeLayout(false); } #pragma endregion Конструктор Метод с тем же именем Form1, что и класс, называется конструктором. В нем инициализируются поля класса. В данном случае инициализация происходит через вызов метода InitializeComponent. Содержание InitializeComponent редактируется целиком и полностью дизайнером среды. Если пользователь добавляет свои поля и инициализирует их в конструкторе, то это делается после вызова InitializeComponent. Деструктор Метод с именем ~Form1 называется деструктором. Он вызывается приложением после закрытия формы. Редактировать его содержание имеет смысл лишь при наличии достаточного опыта программирования, если требуется освобождение дополнительных ресурсов, занятых формой. 7 Единственное поле components, которое присутствует в форме по умолчанию, используется исключительно дизайнером. Модификаторы доступа Описания членов класса Form1 снабжены так называемыми модификаторами доступа public, protected, private. Модификаторы указывают степень доступа: public – доступен любому внешнему пользователю, protected – доступен только классам-наследникам Form1, private – доступен только членам класса формы Form1. Из контекста видно, что конструктор формы Form1 доступен любому клиенту, деструктор – только наследникам, а поле и метод InitializeComponent - только членам класса. Общее правило построения класса – члены класса должны иметь минимально возможный уровень доступа. Так, в большинстве случаев поля класса имеют модификатор private. То же относится и к методам, используемым исключительно для внутренних нужд класса. Управляющие элементы и их свойства Дальнейшие действия в построении приложения будут сводиться к редактированию файла Form1.h. При этом визуальные изменения на форме и изменения, проводимые через окно Properties, будут отображаться дизайнером в коде файла Form1.h. Для выбора управляющих элементов и компонент, размещаемых на форме приложения, используется окно Toolbox. Если это окно отсутствует, откройте его через меню View. 1. В разделе Menus & Toolbars окна Toolbox находится компонента ToolStripContainer. Щелкните по этой компоненте, затем по полю формы. Эти действия помещают на форму компоненту из Toolbox. Перенесенный на форму управляющий элемент является объектом класса ToolStripContainer. Он представляет собой контейнер, в котором будут располагаться другие управляющие элементы. Контейнер ToolStripContainer состоит из пяти панелей – центральной панели и 4-ех панелей, расположенных по периметру. На центральную панель обычно помещают компоненты, используемые для изображения основной информации (графики, таблицы, текстовые редакторы и т.п.). Боковые панели слева и справа используются для размещения таких управляющих элементов как кнопки, списки выбора и т.п. На верхней панели размещают главное меню, а на нижней - компоненты строки статуса. Обратите внимание, что окно среды организовано именно таким образом. 2. После переноса элемента ToolStripContainer на форму щелкните по стрелке, расположенной в правой части верхней границы рамки. Появится небольшая панель ToolStripConainer Tasks, на которую выведены настройки структуры и внешнего вида компоненты ToolStripContainer. Следует выбрать команду Dock Fill In Form внизу панели. В этом случае компонента заполнит всю рабочую область окна, и ее размеры будут меняться синхронно с размерами окна. 3. Управляющий элемент получил по умолчанию имя toolStripContainer1. В этом можно убедиться разными способами. В частности, взглянув в окно Properties. Окно Properties содержит свойства управляющих элементов, расположенных на форме и свойства самой формы. Эти свойства отображаются, если на центральной панели среды находится изображение окна Form1.h [Designer], но не изображение кода Form1.h. В строке верхней части окна Properties находится имя того элемента, который выделен в 8 изображении (для выделения элемента по нему достаточно щелкнуть мышкой). Среди свойств любого элемента есть свойство Name, содержащее это же имя. В коде файла Form1.h в качестве нового поля формы Form1 появилась строка private: System::Windows::Forms::ToolStripContainer^ toolStripContainer1;. Это строка описания этого элемента. Убедитесь в этом, а так же в том, что, в частности, свойство Dock в окне Prioperties установлено в значение Fill. В теле метода InitializeComponent установка свойства Dock выглядит как this->toolStripContainer1->Dock = System::Windows::Forms::DockStyle::Fill;. 4. На центральную панель контейнера поместите элемент DataGridView из раздела Data окна Toolbox. Это таблица, в которую будут заноситься байты редактируемого потока. a. Как и в предыдущем случае, воспользуйтесь командой Dock in parent container. Таблица заполнит всю центральную панель. b. На той же панели DataGridView Tasks снимите флажки со свойств Enable Adding и Enable Deleting. Эти свойства по умолчанию позволяют (enable) пользователю в процессе счета выполнять добавление (adding) и стирание (deleting) строк таблицы. В приложении не предполагается возможность изменения пользователем этого параметра, так как число строк определяется объемом потока. c. Команда AddColumn… открывает окно Add Column, в котором указываются свойства добавляемого в таблицу столбца. Добавьте в таблицу 5 столбцов, указав в их заголовках (свойство HeaderText) числа от 1 до 5. 5. На этой стадии можно скомпилировать проект командой prEditBinFile из меню Build и, если компиляция пройдет успешно, активировать проект командой Start Debugging (кнопка с зеленой стрелочкой). Возможна ошибка компиляции, связанная с настройкой свойств проекта по умолчанию. Чтобы обойти эту ошибку a. откройте окно свойств проекта через меню Project командой prEditBinFile properties…. b. на левой панели окна откройте узел Configuration Properties, выбрав пункт General. c. На правой панели поменяйте значение свойства Output Directory. Для этого i. щелкните по стрелочке справа в строке Output Directory, выбрав <Browse…> ii. найдите каталог Debug в каталоге prEditBinFile (но не во внешнем каталоге solWriteReadFiles) и выберите его в качестве каталога вывода. d. Нажмите OK 6. Число строк в таблице и содержимое ячеек будет определяться динамически внутри кода приложения в зависимости от объема и содержания потока. Для установки этих параметров в коде формы можно добавить описание трех полей, определяющих верхний предел объема потока _maxLength (имена полей обычно предваряются знаком подчеркивания), нижний его предел _minLength и объект _rnd класса Random, который генерирует случайные числа: static int _maxLength=1000; static int _minLength=100; static Random^ _rnd=gcnew Random(); 9 Описание можно поместить в любом месте внутри скобок, ограничивающих тело класса Form1, но не внутри какого-либо метода. Но удобнее описать эти поля в самом конце кода после окончания раздела дизайнера (строка #pragma endregion). Описание этих полей сопровождает модификатор static, который требуется, если (как в данном случае) поля инициализированы конкретными значениями непосредственно в коде. Можно использовать не статические поля, давая их обычное описание int _maxLength; int _minLength; Random^ _rnd; Тогда инициализацию этих полей следует проводить в теле конструктора, добавив туда строки _maxLength=1000; _minLength=100; _rnd=gcnew Random(); Испытайте оба варианта. 7. Для заполнения таблицы случайными байтами случайно выбранного объема ниже описания полей напишем метод /// <summary> /// Устанавливает случайный объем байтов и /// заполняет таблицу случайными байтами. /// </summary> void Randomize() { // Задаем случайную длину потока в интервале [_minLength;_maxLength) int streamLength=_rnd->Next(_maxLength-_minLength)+_minLength; // Определяем число строк в таблице в зависимости от длины потока dataGridView1->RowCount=streamLength /dataGridView1->ColumnCount; // Если длина потока не делится нацело на число столбцов таблицы, // то добавляем еще одну строку if (streamLength % dataGridView1->ColumnCount!=0) dataGridView1->RowCount++; // Текущие номера строк и колонок в таблице int row,col; // В цикле по всем элементам потока заполняем ячейки таблицы // слева направо и сверху вниз for (int i=0;i<streamLength;i++) { row=i/dataGridView1->ColumnCount; col= i%dataGridView1->ColumnCount; // При смене колонки указываем заголовок строки от 1 и далее if (!col) dataGridView1->Rows[row]->HeaderCell->Value= (row+1).ToString(); // Пишем в таблицу случайные значения байтов 10 dataGridView1[col,row]->Value=_rnd->Next(256); } 8. 9. 10. 11. } Метод Randomize() может быть вызван при загрузке формы. Для этого следует присоединить к форме обработчик события Load (загрузка). В окне Properties следует щелкнуть по иконке с изображением молнии. Это откроет панель, в которой перечислены обработчики событий формы. Одно из событий называется Load. Дважды щелкнув по полю, расположенному справа, получим в окне кода скелет метода с именем Form1_Load. Внутри (между фигурными скобками) следует написать вызов метода Randomize(); На этой стадии после компиляции проекта и его активации (команды Rebuild и Start Debugging) должна появиться таблица, заполненная байтами. Содержание таблицы можно редактировать. Если необходимо, чтобы метод Randomize мог быть вызван пользователем в любое время, следует разместить на форме кнопку, щелчок по которой вызывает метод Randomize(). С этой целью a. В окне Toolbox, в разделе Menus & Toolbars выбрать компоненту ToolStrip и перенести ее на левую панель контейнера; b. Щелкнуть по стрелочке этого компонента и выбрать из появившегося списка кнопку (Button). По умолчанию эта кнопка получит имя toolStripButton1. Можно изменить ряд ее свойств через окно Properties i. Имя (Name) установить в rndButton ii. Свойство DisplayStyle изменить на Text iii. Свойство Text заменить на Randomize. iv. На вкладке событий (кнопка с молнией) выбрать событие Click и дважды щелкнуть по полю справа. v. В открывшемся скелете обработчика rndButton_Click набрать вызов метода Randomize(); Скомпилировать проект (команда Rebuild) и активировать его (команда Start Debugging). Убедиться, что щелчок по кнопке действительно меняет содержимое и размеры таблицы. Внесем некоторые усовершенствования в изображение таблицы. Для этого выделим таблицу dataGridView1 и войдем в окно Properties a. Во-первых, заголовки строк, изображающие целые числа, плохо видны. Это можно исправить, изменив свойство RowHeadersWidthSizeMode. По умолчанию это свойство имеет значение EnabledResizing. Изменить его на AutoSizeToDisplayedHeaders. Вновь активировать приложение, проверив эффект. Заголовки строк должны изображаться полностью. b. Во-вторых, было бы приятней, чтобы цифры в заголовках столбцов изображались в середине ячеек. Для этого надо выбрать свойство ColumnHeadersDefaultCellStyle и в открывшемся окне изменить свойство Alignment с MiddleLeft на MiddleCenter. Сделайте и проверьте эффект. c. Такую же процедуру проделайте со свойством DefaultCellStyle. Убедитесь, что после нее все ячейки таблицы заполняются числами, изображаемыми в центре. 11 12. Для сохранения содержания таблицы в файле и, возможно, чтения из файлового потока следует поместить на форму меню с соответствующими командами: a. В разделе Menus & Toolbars окна Toolbox выбрать компоненту класса MenuStrip и поместить ее на верхнюю панель контейнера. Далее i. В появившемся окошке Type Here набрать имя меню File. ii. В окошке ниже набрать Save iii. В новом окошке ниже набрать Open iv. Дважды щелкнуть по окошку Save. В окне кода должен появиться скелет обработчика saveToolStripMenuItemClick, который будет срабатывать каждый раз, когда будет выбрана команда Save меню File. 13. Теперь следует поместить на форму компоненту диалогового окна, позволяющего назвать файл и выбрать каталог для его сохранения. Для этого откройте окно Toolbox и из раздела Dialogs перенесите на форму компоненту SaveFileDialog. Объект примет имя saveFileDialog1. 14. Диалоговое окно должно открываться при выборе команды Save меню File. После чего в файл должно записываться содержание таблицы. Все это обеспечит код, который следует поместить внутрь (между фигурными скобками) обработчика saveToolStripMenuItemClick private: System::Void saveToolStripMenuItem_Click(System::Object^ sender, System::EventArgs^ e) { // Если выбор файла не был сделан, метод прекратит свою работу if (saveFileDialog1->ShowDialog() != System::Windows::Forms::DialogResult::OK) return; // Создается поток s для записи в файл, // ассоциированный с файлом, открытом в диалоговом окне Stream^ s=saveFileDialog1->OpenFile(); // Делается попытка записи информации из таблицы в файл try { // в цикле по всем строкам и столбцам таблицы for (int row=0;row<dataGridView1->RowCount;row++) for (int col=0;col<dataGridView1->ColumnCount;col++) { // Если ячейка таблицы пустая, то цикл завершается. // Ожидается, что это возможно только в том случае, // когда последняя строка заполнена не до конца if (!dataGridView1[col,row]->Value) break; // Байты записываются из ячеек в поток s->WriteByte(Convert::ToByte( dataGridView1[col,row]->Value)); } } finally { // По завершении записи, либо при неудачной попытки 12 // поток закрывается и файл освобождается s->Close(); } } Здесь используются операторы try…finally. Они позволяют в блоке finally освободить поток и файл, используемые в операторах блока try, даже в том случае, если возникает ошибка при выполнении этих операторов. 15. Проверьте работу приложения, активировав его и выбрав команду Save меню File. Посмотрите содержание получившегося бинарного файла. 16. Теперь заполним таблицу, получая поток из файла. Для этого добавим к форме из окна Toolbox диалоговое окно OpenFileDialog, которое примет по умолчанию имя openFileDialog1. 17. Щелкните дважды по команде меню Open. Откроется обработчик openToolStripMenuItem_Click, внутрь которого поместите код, считывающий поток из файла и помещающий байты потока в таблицу dataGridview1 private: System::Void openToolStripMenuItem_Click(System::Object^ sender, System::EventArgs^ e) { // Если выбор файла не был сделан, метод прекратит свою работу if (openFileDialog1->ShowDialog() != System::Windows::Forms::DialogResult::OK) return; // Создаем файловый поток байтов Stream^ s = File::OpenRead(openFileDialog1->FileName); try { // Заполняем байтами таблицу // Определяем число строк в таблице в зависимости от длины потока dataGridView1->RowCount = (int)s->Length / dataGridView1->ColumnCount; // Если длина потока не делится нацело на число столбцов, // добавляем одну строку if (s->Length % dataGridView1->ColumnCount != 0) dataGridView1->RowCount++; // Текущие номера строк и колонок в таблице int row, col; // В цикле по всем элементам потока //заполняем ячейки таблицы слева направо и сверху вниз for (int i = 0; i < s->Length; i++) { // Определяем текущий номер строки столбца row = i / dataGridView1->ColumnCount; col = i % dataGridView1->ColumnCount; // Устанавливаем заголовок строки от 1 и далее при смене строки if (col == 0) dataGridView1->Rows[row]->HeaderCell->Value = 13 (row + 1).ToString(); // Пишем в таблицу значения, взятые из потока s dataGridView1[col, row]->Value =s->ReadByte(); } } finally { // Освобождаем поток и файл s->Close(); } } 18. Проверьте работу приложения. Оно должно сохранять и считывать файлы. 19. Имеет смысл на этом этапе немного оптимизировать код. Во-первых, видно, что обработчики загрузки формы Form1_Load и клика кнопки rndButton_Click выполняют один и тот же код – вызов метода Randomize(). Эти обработчики имеют одинаковое число параметров одного и того же типа Object^ sender, EventArgs^ e. Говорят, что эти два метода имеют одинаковую сигнатуру. Так как их коды совпадают, то к обработке клика кнопки можно присоединить метод загрузки формы Form1_Load, а обработчик rndButton_Click убрать. Для этой редакции необходимо a. Выбрать кнопку Randomize в окне дизайнера формы Form1.h [Design]. b. Открыть окно Properties. c. Войти на панель событий (кнопка с молнией). d. В строке события Click стереть ссылку rndButton_Click. e. В той же строке щелкнуть по кнопке со стрелочкой, направленной вниз. f. Выбрать из списка обработчик Form1_Load. g. В редакторе кода Form1.h стереть обработчик rndButton_Click. h. Проверить работу программы, испытав так же кнопку Randomize. 20. Во-вторых, код методов Randomize и openToolStripMenuItem_Click имеет много схожего. Ведь в обоих методах происходит заполнение таблицы информацией. Имеет смысл использовать этот факт, выделив схожие операторы в отдельный метод. К таким операторам относятся a. В методе Randomize // Определяем число строк в таблице в зависимости от длины потока dataGridView1->RowCount=streamLength /dataGridView1->ColumnCount; // Если длина потока не делится нацело на число столбцов таблицы, // то добавляем еще одну строку if (streamLength % dataGridView1->ColumnCount!=0) dataGridView1->RowCount++; // Текущие номера строк и колонок в таблице int row,col; // В цикле по всем элементам потока // заполняем ячейки таблицы слева направо и сверху вниз for (int i=0;i<streamLength;i++) { row=i/dataGridView1->ColumnCount; 14 col= i%dataGridView1->ColumnCount; // При смене колонки указываем заголовок строки от 1 и далее if (!col) dataGridView1->Rows[row]->HeaderCell->Value= (row+1).ToString(); // Пишем в таблицу случайные значения байтов dataGridView1[col,row]->Value=_rnd->Next(256); } b. В методе openToolStripMenuItem_Click // Заполняем байтами таблицу // Определяем число строк в таблице в зависимости от длины потока dataGridView1->RowCount = (int)s->Length / dataGridView1->ColumnCount; // Если длина потока не делится нацело на число столбцов, // добавляем одну строку if (s->Length % dataGridView1->ColumnCount != 0) dataGridView1->RowCount++; // Текущие номера строк и колонок в таблице int row, col; // В цикле по всем элементам потока // заполняем ячейки таблицы слева направо и сверху вниз for (int i = 0; i < s->Length; i++) { // Определяем текущие номера строки и столбца row = i / dataGridView1->ColumnCount; col = i % dataGridView1->ColumnCount; // Устанавливаем заголовок строки от 1 и далее при смене строки if (col == 0) dataGridView1->Rows[row]->HeaderCell->Value = (row + 1).ToString(); // Пишем в таблицу значения, взятые из потока s dataGridView1[col, row]->Value =s->ReadByte(); } 21. Эти фрагменты кода можно оформить в виде отдельного метода с именем, например, ResetGrid. Но надо заметить, что в указанных фрагментах есть отличия. В методе Randomize длина потока содержится в локальной переменной streamLength и байты, помещаемые в таблицу, получаются вызовом метода _rnd->Next(256). В то же время, в методе openToolStripMenuItem_Click длина потока возвращается свойством s->Length потока, а байты возвращаются из потока методом s->ReadByte(). Что касается длины потока, то в новый метод ResetGrid можно ввести параметр типа int streamLength. И при вызове метода ResetGrid давать параметру разные значения: в случае Randomize вызывать ResetGrid(streamLength), в случае загрузки из файла ResetGrid( s->Length). 22. Второе отличие, касающееся самих байтов, обойти немного сложнее. Не желательно копировать получаемые байты в отдельный массив памяти для временного хранения, а затем передавать массив методу ResetGrid. Это расточительно при больших массивах. 15 23. Другой способ – добавить в метод ResetGrid "говорящий" параметр source (источник), назвав его тип Source, с двумя говорящими значениями random и fileStream. В зависимости от значения этого параметра байты будут создаваться случайным образом (random) или загружаться из файлового потока (fileStream). Тип Source надо предварительно описать внутри класса формы (вне описания методов, в любом месте) декларацией /// <summary> /// Тип перечисления, используемый /// для определения источника байтов, /// записываемых в таблицу /// </summary> enum class Source { random, fileStream }; 24. Теперь описание нового метода ResetGrid можно оформить в виде /// <summary> /// Заполняет таблицу байтами, полученными от источника /// </summary> /// <param name="streamLength"> /// Длина потока (число байтов) /// </param> /// <param name="source"> /// Источник потока /// </param> void ResetGrid(int streamLength, Source source) { // Определяем число строк в таблице в зависимости от длины потока dataGridView1->RowCount = streamLength / dataGridView1->ColumnCount; // Если длина потока не делится нацело на число столбцов, // добавляем одну строку if (streamLength % dataGridView1->ColumnCount != 0) dataGridView1->RowCount++; // Текущие номера строк и колонок в таблице int row, col; // В цикле по всем элементам потока заполняем ячейки таблицы слева направо // и сверху вниз for (int i = 0; i < streamLength; i++) { // Определяем текущий номер строки столбца row = i / dataGridView1->ColumnCount; col = i % dataGridView1->ColumnCount; // Устанавливаем заголовок строки от 1 и далее при смене строки if (col == 0) dataGridView1->Rows[row]->HeaderCell->Value = (row + 1).ToString(); // Пишем в таблицу либо случайные значения байтов, // либо значения, взятые из потока s, в зависимости от значения // параметра source dataGridView1[col, row]->Value = 16 // Это так называемое условное выражение source == Source::fileStream ? s->ReadByte() : _rnd->Next(256); } } 25. Компиляция командой Rebuild покажет, что объект s, используемый в условном выражении в конце метода ResetGrid не описан. Действительно, поток s описан в обработчике openToolStripMenuItem_Click в качестве локального объекта и не доступен коду метода ResetGrid. Беде можно помочь, если описать s как поле формы, добавив вне описания методов строку /// <summary> /// Поток ввода информации из файла /// </summary> Stream^ s; 26. Ошибка компиляции исчезнет. Теперь следует изменить содержание методов Randomize и openToolStripMenuItem_Click, убрав из них код, обобщенный методом ResetGrid, и внеся в них вызов этого метода с определенными параметрами. 27. Новая редакция методов Randomize и openToolStripMenuItem_Click будет выглядеть следующим образом /// <summary> /// Устанавливает случайный объем байтов и заполняет таблицу случайными байтами. /// </summary> void Randomize() { // Задаем случайную длину потока в интервале [_minLength;_maxLength) int streamLength=_rnd->Next(_maxLength-_minLength)+_minLength; // Заносим в таблицу случайные байты ResetGrid(streamLength, Source::random); } private: System::Void openToolStripMenuItem_Click(System::Object^ sender, System::EventArgs^ e) { // Если выбор файла не был сделан, метод прекратит свою работу if (openFileDialog1->ShowDialog() != System::Windows::Forms::DialogResult::OK) return; // Создаем файловый поток байтов s = File::OpenRead(openFileDialog1->FileName); // Делается попытка записи информации из файла в таблицу try { // Заполняем байтами таблицу ResetGrid(s->Length,Source::fileStream); } finally { 17 // По завершении записи, либо при неудачной попытке // поток закрывается и файл освобождается s->Close(); } } 28. Можно написать иную версию метода ResetGrid, выбрав другой тип второго параметра source. Это немного упростит код, не требуя описания поля потока s в форме и описания типа Source. Поток ввода s останется локальным в методе openToolStripMenuItem_Click, как это было в первой редакции. Для этого требуется новая редакция кода, отличная от проведенной в пунктах 23-27. Но не спешите ее делать, стирая только что внесенные изменения. Можно сделать новую редакцию, не теряя прежней! 29. Новая редакция предполагает следующие изменения, которые пока не следует проводить явно a. Убрать описания типа Source и поля s в форме. b. Параметр source метода ResetGrid описать типом Object^, а не Source, как было в последней редакции. c. Заменить условное выражение в конце кода метода ResetGrid, где используется параметр source source == Source::fileStream ? _fileStreamIn->ReadByte() : _rnd->Next(256); новой версией source == _rnd ? _rnd->Next(256) : ((Stream^) source)->ReadByte(); В этом новом выражении source является объектом класса Object – общим предком классов Random и Stream. Теперь, если вместо параметра source при вызове ResetGrid подставить объект _rnd, что должно быть сделано внутри метода Randomize, то байт будет получен из метода Next(256), как указано в условном выражении. В противном случае источником будет объект потока, т.е. класса Stream. Такой вызов ResetGrid должен быть сделан из методаобработчика openToolStripMenuItem_Click. Однако, т.к. source по описанию имеет тип Object, в котором нет метода ReadByte, то написать просто source>ReadByte() нельзя. Компилятор не пропустит такой код. Если же использовать приведение типа ((Stream^) source), как указано выше, то компиляция пройдет успешно. d. В методе Randomize() следует изменить вызов метода ResetGrid с ResetGrid(streamLength, Source::random); на ResetGrid(streamLength, _rnd); e. В методе-обработчике openToolStripMenuItem_Click следует заменить фрагмент кода // Создаем файловый поток байтов s = File::OpenRead(openFileDialog1->FileName); // Делается попытка записи информации из таблицы в файл try { // Заполняем байтами таблицу 18 ResetGrid(s->Length,Source::fileStream); } на другой // Создаем файловый поток байтов Stream^ s = File::OpenRead(openFileDialog1->FileName); // Делается попытка записи информации из таблицы в файл try { // Заполняем байтами таблицу ResetGrid((int)s->Length,s); } 30. Описанные изменения можно внести, не убирая прежний код, если использовать инструмент так называемой условной компиляции. Для этого в начале файла формы Form1.h, перед строкой namespace prEditBinFile наберите строки // Использование параметров условной компиляции #define version1 #undef version1 Первая строка #define version1 активирует некоторый параметр version1, который может быть произвольным идентификатором и который используется в дальнейшем. Вторая строка #undef version1 отменяет эту активацию. Две такие строки эквивалентны отсутствию какой-либо редакции кода. Однако их наличие позволит сохранить обе версии кода, описанные выше, и манипулировать ими по желанию программиста. 31. В коде следует провести следующую редакцию a. В методе Randomize() // Заносим в таблицу случайные байты #ifdef version1 ResetGrid(streamLength, Source::random); #else ResetGrid(streamLength, _rnd); #endif Другими словами, если параметр version1 описан, то будет компилироваться первая версия, стоящая между #ifdef version1 и #else. В противном случае – вторая версия, стоящая между #else и #endif. b. Описание типа Source и поля s следует окружить условием так, чтобы эти описания имели вид #ifdef version1 /// <summary> /// Тип перечисления, используемый /// для определения источника байтов, /// записываемых в таблицу /// </summary> enum class Source { random, fileStream }; /// <summary> /// Поток ввода информации из файла 19 /// </summary> Stream^ s; #endif c. В описании метода ResetGrid следует сделать условным тип второго параметра void ResetGrid(int streamLength, #ifdef version1 Source #else Object^ #endif source) и изменить код в условном выражении #ifdef version1 source == Source::fileStream ? s->ReadByte() : _rnd->Next(256); #else source == _rnd ? _rnd->Next(256) : ((Stream^) source)->ReadByte(); #endif d. В описании метода openToolStripMenuItem_Click изменение имеет вид #ifdef version1 s = File::OpenRead(openFileDialog1->FileName); try { // Заполняем байтами таблицу ResetGrid((int) s->Length, Source::fileStream); } #else // Создаем файловый поток байтов Stream^ s = File::OpenRead(openFileDialog1->FileName); // Делается попытка записи информации из файла в таблицу try { // Заполняем байтами таблицу ResetGrid((int)s->Length,s); } #endif 32. В этой редакции компилироваться будет только код, который не удовлетворяет условию #ifdef version1, а также код, стоящий вне условных операторов. Компилируемый и не компилируемый код должны существенно различаться цветом. Скомпилируйте программу и проверьте ее работу. Для возврата к предыдущей версии кода достаточно закомментировать строку #undef version1, поставив перед ней двойной слэш //. Убедитесь, что выделился код, построенный в предыдущей версии. Вновь скомпилируйте и проверьте работу приложения. 33. Что произойдет, если при редактировании ячейки таблицы будет введена строка, которая не может быть байтом – либо содержит нецифровые символы, либо выходит 20 за диапазон [0; 255], и после этой редакции попытаться сохранить содержимое таблицы в файле? Испытайте. 34. Для того, чтобы избежать останова приложения по ошибке при неверном наборе строки, следует обработать два события таблицы – CellValidating и CellEndEdit. Для этого перейдите в окно дизайнера Form1.h [Design], выделите таблицу dataGridView1 и в окне Properties на вкладке событий (кнопка с молнией) найдите эти два события. Дважды щелкните по полю справа для первого и второго событий. Затем в полученные обработчики добавьте код так, чтобы результат выглядел следующим образом private: System::Void dataGridView1_CellValidating(System::Object^ sender, System::Windows::Forms::DataGridViewCellValidatingEventArgs^ e) { // Описывает переменную, которая будет содержать значение введенного байта Byte newByte; // Если все нормально, то выход из метода if (Byte::TryParse(e->FormattedValue->ToString(), newByte) // В последней строке могут быть пустые ячейки. // На пустой ячейке заканчивается поток. // Пустые ячейки должны иметь запрет на редактирование, // который устанавливается в методе ResetGrid || dataGridView1[e->ColumnIndex,e->RowIndex]->ReadOnly) return; // Если вводимая величина не является байтом, ввод отменяется e->Cancel = true; // Сообщение, которое появится, если введенная строка байтом не является dataGridView1->Rows[e->RowIndex]->ErrorText = "Набранная строка не является байтом.\n" + "Нажмите esc, или наберите число в интервале [0;255]!"; } private: System::Void dataGridView1_CellEndEdit( System::Object^ sender, System::Windows::Forms::DataGridViewCellEventArgs^ e) { // Очистка строки ErrorText в конце редактирования ячейки // обеспечит исчезновение объекта класса ErrorProvider (восклицательный знак) dataGridView1->Rows[e->RowIndex]->ErrorText = String::Empty; } 35. В конец метода ResetGrid (после закрывающей скобки } цикла по i) следует добавить код, делающий не редактируемыми пустые ячейки последней строки таблицы // Пустые ячейки последней строки должны быть "только для чтения" (не редактируемые). if (streamLength % dataGridView1->ColumnCount != 0) for (col=streamLength % dataGridView1->ColumnCount; col<dataGridView1->ColumnCount;col++) dataGridView1[col,dataGridView1->RowCount-1]->ReadOnly=true; 21 Задания для самостоятельной работы 1. Настоящий проект предполагает, что число байтов, взятых с файла, остается неизменным. Могут меняться лишь их значения. Измените код программы и свойства таблицы так, чтобы можно было менять число редактируемых байтов, добавляя строки к таблице и стирая их. 2. Добавьте к проекту возможность представлять байты в таблице не только в десятичном виде, но и в двоичном и шестнадцатеричном, а также редактировать их. (Воспользуйтесь методами ToString и ToByte класса Convert). Форматирование текста. Добавьте к решению solWriteReadFiles консольное приложение. Перенесите в него код, записанный ниже. Изучите работу этого приложения. Сделайте самостоятельно следующее: добавьте в таблицу массы частиц и вычисление положения центра масс с выводом на экран и в файл. // prWriteFormattedText.cpp : main project file. #include "stdafx.h" using namespace System; using namespace System::IO; void main() { double d = Math::PI; // Формат изображения строк Console::WriteLine("{0,-20};{0,20};", d); Console::WriteLine("f:{0,20:f};g:{0,20:g};\ne:{0,20:e};r:{0,20:r}", d); Console::WriteLine("f5:{0,20:f5};g1:{0,20:g1};\ne9:{0,20:e9};r3:{0,20:r3}", d); Console::ReadLine(); // Опишем массив координат частиц array<double,2>^ q; // Введем число частиц Console::Write("Введите число частиц n = "); int n = int::Parse(Console::ReadLine()); // Определим массив координат q = gcnew array<double,2>(3, n); // Заполним массив случайными числами, лежащими в интервале [-1;1) Random^ rnd = gcnew Random(); for (int i = 0; i < 3; i++) for (int a = 0; a < n; a++) q[i, a] = 2 * rnd->NextDouble() - 1; // Занесем массив в текстовой файл, форматируя размещение символов TextWriter^ tw = File::CreateText("Positions.txt"); 22 try { // Заголовки столбцов tw->WriteLine("\t|{0,-8}|{1,-8}|{2,-8}|", " X", " Y", " Z"); // Строка разделения for (int i = 0; i < 36; i++) tw->Write("-"); tw->WriteLine(); // Строки чисел-координат for (int a = 0; a < n; a++) { // Заголовок текущей строки tw->Write("{0,5} |", a + 1); // Содержание текущей строки for (int i = 0; i < 3; i++) tw->Write(q[i, a] >= 0 ? " {0,-7:f4}|" : "{0,-8:f4}|", q[i, a]); tw->WriteLine(); } } finally { tw->Close(); } } 23