Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Интерфейс для редактирования цифровых потоков.doc
Скачиваний:
0
Добавлен:
01.07.2025
Размер:
2.93 Mб
Скачать

Конструктор

Метод с тем же именем Form1, что и класс, называется конструктором. В нем инициализируются поля класса. В данном случае инициализация происходит через вызов метода InitializeComponent. Содержание InitializeComponent редактируется целиком и полностью дизайнером среды. Если пользователь добавляет свои поля и инициализирует их в конструкторе, то это делается после вызова InitializeComponent.

Деструктор

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

Единственное поле 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-ех панелей, расположенных по периметру. На центральную панель обычно помещают компоненты, используемые для изображения основной информации (графики, таблицы, текстовые редакторы и т.п.). Боковые панели слева и справа используются для размещения таких управляющих элементов как кнопки, списки выбора и т.п. На верхней панели размещают главное меню, а на нижней - компоненты строки статуса. Обратите внимание, что окно среды организовано именно таким образом.

  1. После переноса элемента ToolStripContainer на форму щелкните по стрелке, расположенной в правой части верхней границы рамки. Появится небольшая панель ToolStripConainer Tasks, на которую выведены настройки структуры и внешнего вида компоненты ToolStripContainer. Следует выбрать команду Dock Fill In Form внизу панели. В этом случае компонента заполнит всю рабочую область окна, и ее размеры будут меняться синхронно с размерами окна.

  1. Управляющий элемент получил по умолчанию имя toolStripContainer1. В этом можно убедиться разными способами. В частности, взглянув в окно Properties. Окно Properties содержит свойства управляющих элементов, расположенных на форме и свойства самой формы. Эти свойства отображаются, если на центральной панели среды находится изображение окна Form1.h [Designer], но не изображение кода Form1.h. В строке верхней части окна Properties находится имя того элемента, который выделен в изображении (для выделения элемента по нему достаточно щелкнуть мышкой). Среди свойств любого элемента есть свойство Name, содержащее это же имя. В коде файла Form1.h в качестве нового поля формы Form1 появилась строка

private: System::Windows::Forms::ToolStripContainer^

toolStripContainer1;.

Это строка описания этого элемента. Убедитесь в этом, а так же в том, что, в частности, свойство Dock в окне Prioperties установлено в значение Fill. В теле метода InitializeComponent установка свойства Dock выглядит как this->toolStripContainer1->Dock =

System::Windows::Forms::DockStyle::Fill;.

  1. На центральную панель контейнера поместите элемент DataGridView из раздела Data окна Toolbox. Это таблица, в которую будут заноситься байты редактируемого потока.

    1. Как и в предыдущем случае, воспользуйтесь командой Dock in parent container. Таблица заполнит всю центральную панель.

    2. На той же панели DataGridView Tasks снимите флажки со свойств Enable Adding и Enable Deleting. Эти свойства по умолчанию позволяют (enable) пользователю в процессе счета выполнять добавление (adding) и стирание (deleting) строк таблицы. В приложении не предполагается возможность изменения пользователем этого параметра, так как число строк определяется объемом потока.

    3. Команда AddColumn… открывает окно Add Column, в котором указываются свойства добавляемого в таблицу столбца. Добавьте в таблицу 5 столбцов, указав в их заголовках (свойство HeaderText) числа от 1 до 5.

  1. На этой стадии можно скомпилировать проект командой prEditBinFile из меню Build и, если компиляция пройдет успешно, активировать проект командой Start Debugging (кнопка с зеленой стрелочкой). Возможна ошибка компиляции, связанная с настройкой свойств проекта по умолчанию. Чтобы обойти эту ошибку

    1. откройте окно свойств проекта через меню Project командой prEditBinFile properties….

    2. на левой панели окна откройте узел Configuration Properties, выбрав пункт General.

    3. На правой панели поменяйте значение свойства Output Directory. Для этого

      1. щелкните по стрелочке справа в строке Output Directory, выбрав <Browse…>

      2. найдите каталог Debug в каталоге prEditBinFile (но не во внешнем каталоге solWriteReadFiles) и выберите его в качестве каталога вывода.

    4. Нажмите OK

Работа с цифровым потоком

  1. Число строк в таблице и содержимое ячеек будет определяться динамически внутри кода приложения в зависимости от объема и содержания потока. Для установки этих параметров в коде формы можно добавить описание трех полей, определяющих верхний предел объема потока _maxLength (имена полей обычно предваряются знаком подчеркивания), нижний его предел _minLength и объект _rnd класса Random, который генерирует случайные числа:

static int _maxLength=1000;

static int _minLength=100;

static Random^ _rnd=gcnew Random();

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

Можно использовать не статические поля, давая их обычное описание

int _maxLength;

int _minLength;

Random^ _rnd;

Тогда инициализацию этих полей следует проводить в теле конструктора, добавив туда строки

_maxLength=1000;

_minLength=100;

_rnd=gcnew Random();

Испытайте оба варианта.

  1. Для заполнения таблицы случайными байтами случайно выбранного объема ниже описания полей напишем метод

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

// Пишем в таблицу случайные значения байтов

dataGridView1[col,row]->Value=_rnd->Next(256);

}

}

  1. Метод Randomize() может быть вызван при загрузке формы. Для этого следует присоединить к форме обработчик события Load (загрузка). В окне Properties следует щелкнуть по иконке с изображением молнии. Это откроет панель, в которой перечислены обработчики событий формы. Одно из событий называется Load. Дважды щелкнув по полю, расположенному справа, получим в окне кода скелет метода с именем Form1_Load. Внутри (между фигурными скобками) следует написать вызов метода Randomize();

На этой стадии после компиляции проекта и его активации (команды Rebuild и Start Debugging) должна появиться таблица, заполненная байтами. Содержание таблицы можно редактировать.

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

    1. В окне Toolbox, в разделе Menus & Toolbars выбрать компоненту ToolStrip и перенести ее на левую панель контейнера;

    2. Щелкнуть по стрелочке этого компонента и выбрать из появившегося списка кнопку (Button). По умолчанию эта кнопка получит имя toolStripButton1. Можно изменить ряд ее свойств через окно Properties

      1. Имя (Name) установить в rndButton

      2. Свойство DisplayStyle изменить на Text

      3. Свойство Text заменить на Randomize.

      4. На вкладке событий (кнопка с молнией) выбрать событие Click и дважды щелкнуть по полю справа.

      5. В открывшемся скелете обработчика rndButton_Click набрать вызов метода Randomize();

  2. Скомпилировать проект (команда Rebuild) и активировать его (команда Start Debugging). Убедиться, что щелчок по кнопке действительно меняет содержимое и размеры таблицы.

  1. Внесем некоторые усовершенствования в изображение таблицы. Для этого выделим таблицу dataGridView1 и войдем в окно Properties

    1. Во-первых, заголовки строк, изображающие целые числа, плохо видны. Это можно исправить, изменив свойство RowHeadersWidthSizeMode. По умолчанию это свойство имеет значение EnabledResizing. Изменить его на AutoSizeToDisplayedHeaders. Вновь активировать приложение, проверив эффект. Заголовки строк должны изображаться полностью.

    2. Во-вторых, было бы приятней, чтобы цифры в заголовках столбцов изображались в середине ячеек. Для этого надо выбрать свойство ColumnHeadersDefaultCellStyle и в открывшемся окне изменить свойство Alignment с MiddleLeft на MiddleCenter. Сделайте и проверьте эффект.

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

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

    1. В разделе Menus & Toolbars окна Toolbox выбрать компоненту класса MenuStrip и поместить ее на верхнюю панель контейнера. Далее

      1. В появившемся окошке Type Here набрать имя меню File.

      2. В окошке ниже набрать Save

      3. В новом окошке ниже набрать Open

      4. Дважды щелкнуть по окошку Save. В окне кода должен появиться скелет обработчика saveToolStripMenuItemClick, который будет срабатывать каждый раз, когда будет выбрана команда Save меню File.

  2. Теперь следует поместить на форму компоненту диалогового окна, позволяющего назвать файл и выбрать каталог для его сохранения. Для этого откройте окно Toolbox и из раздела Dialogs перенесите на форму компоненту SaveFileDialog. Объект примет имя saveFileDialog1.

  3. Диалоговое окно должно открываться при выборе команды 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

{

// По завершении записи, либо при неудачной попытки

// поток закрывается и файл освобождается

s->Close();

}

}

Здесь используются операторы try…finally. Они позволяют в блоке finally освободить поток и файл, используемые в операторах блока try, даже в том случае, если возникает ошибка при выполнении этих операторов.

  1. Проверьте работу приложения, активировав его и выбрав команду Save меню File. Посмотрите содержание получившегося бинарного файла.

  1. Теперь заполним таблицу, получая поток из файла. Для этого добавим к форме из окна Toolbox диалоговое окно OpenFileDialog, которое примет по умолчанию имя openFileDialog1.

  2. Щелкните дважды по команде меню 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 = (row + 1).ToString();

// Пишем в таблицу значения, взятые из потока s

dataGridView1[col, row]->Value =s->ReadByte();

}

}

finally

{

// Освобождаем поток и файл

s->Close();

}

}

  1. Проверьте работу приложения. Оно должно сохранять и считывать файлы.

  1. Имеет смысл на этом этапе немного оптимизировать код. Во-первых, видно, что обработчики загрузки формы Form1_Load и клика кнопки rndButton_Click выполняют один и тот же код – вызов метода Randomize(). Эти обработчики имеют одинаковое число параметров одного и того же типа Object^ sender, EventArgs^ e. Говорят, что эти два метода имеют одинаковую сигнатуру. Так как их коды совпадают, то к обработке клика кнопки можно присоединить метод загрузки формы Form1_Load, а обработчик rndButton_Click убрать. Для этой редакции необходимо

    1. Выбрать кнопку Randomize в окне дизайнера формы Form1.h [Design].

    2. Открыть окно Properties.

    3. Войти на панель событий (кнопка с молнией).

    4. В строке события Click стереть ссылку rndButton_Click.

    5. В той же строке щелкнуть по кнопке со стрелочкой, направленной вниз.

    6. Выбрать из списка обработчик Form1_Load.

    7. В редакторе кода Form1.h стереть обработчик rndButton_Click.

    8. Проверить работу программы, испытав так же кнопку Randomize.

  1. Во-вторых, код методов Randomize и openToolStripMenuItem_Click имеет много схожего. Ведь в обоих методах происходит заполнение таблицы информацией. Имеет смысл использовать этот факт, выделив схожие операторы в отдельный метод. К таким операторам относятся

    1. В методе 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;

col= i%dataGridView1->ColumnCount;

// При смене колонки указываем заголовок строки от 1 и

// далее

if (!col)

dataGridView1->Rows[row]->HeaderCell->Value=

(row+1).ToString();

// Пишем в таблицу случайные значения байтов

dataGridView1[col,row]->Value=_rnd->Next(256);

}

    1. В методе 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();

}

  1. Эти фрагменты кода можно оформить в виде отдельного метода с именем, например, ResetGrid. Но надо заметить, что в указанных фрагментах есть отличия. В методе Randomize длина потока содержится в локальной переменной streamLength и байты, помещаемые в таблицу, получаются вызовом метода _rnd->Next(256). В то же время, в методе openToolStripMenuItem_Click длина потока возвращается свойством s->Length потока, а байты возвращаются из потока методом s->ReadByte(). Что касается длины потока, то в новый метод ResetGrid можно ввести параметр типа int streamLength. И при вызове метода ResetGrid давать параметру разные значения: в случае Randomize вызывать ResetGrid(streamLength), в случае загрузки из файла ResetGrid( s->Length).

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

  3. Другой способ – добавить в метод ResetGrid "говорящий" параметр source (источник), назвав его тип Source, с двумя говорящими значениями random и fileStream. В зависимости от значения этого параметра байты будут создаваться случайным образом (random) или загружаться из файлового потока (fileStream). Тип Source надо предварительно описать внутри класса формы (вне описания методов, в любом месте) декларацией

/// <summary>

/// Тип перечисления, используемый

/// для определения источника байтов,

/// записываемых в таблицу

/// </summary>

enum class Source { random, fileStream };

  1. Теперь описание нового метода 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 =

// Это так называемое условное выражение

source == Source::fileStream ? s -> ReadByte() :

_rnd->Next(256);

}

}

  1. Компиляция командой Rebuild покажет, что объект s, используемый в условном выражении в конце метода ResetGrid не описан. Действительно, поток s описан в обработчике openToolStripMenuItem_Click в качестве локального объекта и не доступен коду метода ResetGrid. Беде можно помочь, если описать s как поле формы, добавив вне описания методов строку

/// <summary>

/// Поток ввода информации из файла

/// </summary>

Stream^ s;

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

Новая редакция методов 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

{

// По завершении записи, либо при неудачной попытке

// поток закрывается и файл освобождается

s->Close();

}

}

  1. Можно написать иную версию метода ResetGrid, выбрав другой тип второго параметра source. Это немного упростит код, не требуя описания поля потока s в форме и описания типа Source. Поток ввода s останется локальным в методе openToolStripMenuItem_Click, как это было в первой редакции. Для этого требуется новая редакция кода, отличная от проведенной в пунктах 23-27. Но не спешите ее делать, стирая только что внесенные изменения. Можно сделать новую редакцию, не теряя прежней!

  2. Новая редакция предполагает следующие изменения, которые пока не следует проводить явно

    1. Убрать описания типа Source и поля s в форме.

    2. Параметр source метода ResetGrid описать типом Object^, а не Source, как было в последней редакции.

    3. Заменить условное выражение в конце кода метода 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), как указано выше, то компиляция пройдет успешно.

    1. В методе Randomize() следует изменить вызов метода ResetGrid с ResetGrid(streamLength, Source::random); на ResetGrid(streamLength, _rnd);

    2. В методе-обработчике openToolStripMenuItem_Click следует заменить фрагмент кода

// Создаем файловый поток байтов

s = File::OpenRead(openFileDialog1->FileName);

// Делается попытка записи информации из таблицы в файл

try

{

// Заполняем байтами таблицу

ResetGrid(s->Length,Source::fileStream);

}

на другой

// Создаем файловый поток байтов

Stream^ s = File::OpenRead(openFileDialog1->FileName);

// Делается попытка записи информации из таблицы в файл

try

{

// Заполняем байтами таблицу

ResetGrid((int)s->Length,s);

}

  1. Описанные изменения можно внести, не убирая прежний код, если использовать инструмент так называемой условной компиляции. Для этого в начале файла формы Form1.h, перед строкой namespace prEditBinFile наберите строки

// Использование параметров условной компиляции

#define version1

#undef version1

Первая строка #define version1 активирует некоторый параметр version1, который может быть произвольным идентификатором и который используется в дальнейшем. Вторая строка #undef version1 отменяет эту активацию. Две такие строки эквивалентны отсутствию какой-либо редакции кода. Однако их наличие позволит сохранить обе версии кода, описанные выше, и манипулировать ими по желанию программиста.

  1. В коде следует провести следующую редакцию

    1. В методе Randomize()

// Заносим в таблицу случайные байты

#ifdef version1

ResetGrid(streamLength, Source::random);

#else

ResetGrid(streamLength, _rnd);

#endif

Другими словами, если параметр version1 описан, то будет компилироваться первая версия, стоящая между #ifdef version1 и #else. В противном случае – вторая версия, стоящая между #else и #endif.

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

#ifdef version1

/// <summary>

/// Тип перечисления, используемый

/// для определения источника байтов,

/// записываемых в таблицу

/// </summary>

enum class Source { random, fileStream };

/// <summary>

/// Поток ввода информации из файла

/// </summary>

Stream^ s;

#endif

    1. В описании метода 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

    1. В описании метода 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

  1. В этой редакции компилироваться будет только код, который не удовлетворяет условию #ifdef version1, а также код, стоящий вне условных операторов. Компилируемый и не компилируемый код должны существенно различаться цветом. Скомпилируйте программу и проверьте ее работу. Для возврата к предыдущей версии кода достаточно закомментировать строку #undef version1, поставив перед ней двойной слэш //. Убедитесь, что выделился код, построенный в предыдущей версии. Вновь скомпилируйте и проверьте работу приложения.

  1. Что произойдет, если при редактировании ячейки таблицы будет введена строка, которая не может быть байтом – либо содержит нецифровые символы, либо выходит за диапазон [0; 255], и после этой редакции попытаться сохранить содержимое таблицы в файле? Испытайте.

  2. Для того, чтобы избежать остановки приложения по ошибке при неверном наборе строки, следует обработать два события таблицы – 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;

}

  1. В конец метода ResetGrid (после закрывающей скобки } цикла по i) следует добавить код, делающий не редактируемыми пустые ячейки последней строки таблицы

// Пустые ячейки последней строки должны быть "только для чтения" (не редактируемые).

if (streamLength % dataGridView1->ColumnCount != 0)

for (col=streamLength % dataGridView1->ColumnCount;

col<dataGridView1->ColumnCount;col++)

dataGridView1[col,dataGridView1->RowCount-1]

->ReadOnly=true;

  1. Целью следующей редакции приложения будет добавление возможности представлять изображение байтов в таблице не только в десятичной (decimal), но и в двоичной (binary) и шестнадцатеричной (hexadecimal) системе счисления. Для этого добавим к свойству Items меню menuStrip1 управляющий элемент класса ComboBox:

    1. Войдите в окно дизайнера формы Form1.h [Design] и выделите в нем главное меню menuStrip1.

    2. Откройте окно Properties и в правой колонке свойства Items с надписью Collection щелкните по кнопке с многоточием.

    3. В открывшемся окне Items Collection Editor, в списке строки "Select item…" выберите элемент ComboBox и нажмите кнопку Add.

    4. На правой панели измените свойства новой компоненты класса ToolStripComboBox

      1. Свойство Name, по умолчанию равное toolStripComboBox1, замените на scaleBox (система счисления на английском scale of notation)

      2. Свойство DropDownStyle с DropDown - на DropDownList. Это значение свойства DropDownStyle запретит пользователю вводить свои значения в список элементов, а использовать только уже внесенные

      3. Кнопкой с многоточием у свойства Items (Collection) откройте окно "String Collection Editor" и наберите в нем три строки decimal, binary и hexadecimal. Эти три строки и только они будут появляться в списке элемента scaleBox. Выбором одного из значений, мы планируем регулировать представление байтов в таблице.

      4. Закройте на этом окно свойств и вернитесь к изображению окна с кодом формы.

  2. Мы планируем, что при выборе одного из элементов scaleBox изображение байтов в таблице будет даваться в соответствующей системе счисления. Опишем в форме перечислимый, "говорящий" тип переменных, который назовем Scale, состоящий из элементов decimal, binary и hexadecimal. Подобный тип Source уже вводился ранее, в пункте 23. Внесите в тело класса Form1 (только не внутри какого-либо метода!) описание перечислимого типа Scale в виде

/// <summary>

/// Тип, определяющий возможные системы счисления

/// для представления чисел в таблице

/// </summary>

enum class Scale { decimal, binary, hexadecimal };

  1. Добавим внутрь класса формы (после описания типа Scale) поле типа Scale с именем _scale

/// <summary>

/// Поле, хранящее текущее представление чисел

/// </summary>

Scale _scale;

Внутрь конструктора класса (метод Form1.h в верхней части кода) добавим инициализацию поля _scale в виде оператора

_scale =Scale::decimal;

  1. Изменение системы счисления должно приводить к изменению значения поля _scale нашей формы и, параллельно, к изменению изображения чисел в таблице. Для этого напишем код метода SetScale (после описания типа Scale и поля _scale), устанавливающего новое значение поля _scale

/// <summary>

/// Устанавливает представление чисел в таблице в новой системе

/// счисления

/// </summary>

/// <param name="scale">

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

/// </param>

void SetScale(Scale scale)

{

// Если текущее представление совпадает с устанавливаемым,

// выход из метода

if (scale == _scale) return;

// В цикле по всем ячейкам таблицы

for (int row = 0; row < dataGridView1->RowCount; row++)

for (int col = 0; col < dataGridView1->ColumnCount; col++)

{

// если в ячейке пусто, выход из цикла

if (!dataGridView1[col, row]->Value) break;

// Строка текущей ячейки

String^ cellValue = dataGridView1[col, row]->Value

->ToString();

// В зависимости от выбора нового представления

switch (scale)

{

case Scale::decimal: // Decimal

// В зависимости от прежнего представления,

// хранящегося в поле _scale, происходит

// преобразование строки в десятичное выражение байта

dataGridView1[col, row]->Value =

Convert::ToByte(cellValue, _scale ==

Scale::binary ? 2 : 16);

break;

case Scale::binary: // Binary

// В зависимости от прежнего представления,

// хранящегося в поле _scale, происходит

// преобразование байта в двоичную строку

dataGridView1[col, row]->Value =

Convert::ToString(_scale == Scale::hexadecimal ?

Convert::ToByte(cellValue, 16) :

Byte::Parse(cellValue), 2);

break;

case Scale::hexadecimal: //Hex

// В зависимости от прежнего представления,

// хранящегося в поле _scale, происходит

// преобразование байта в 16-ричную строку

dataGridView1[col, row]->Value =

Convert::ToString(_scale == Scale::binary ?

Convert::ToByte(cellValue, 2) :

Byte::Parse(cellValue), 16);

break;

}

}

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

_scale = scale;

}

  1. Теперь вернемся к scaleBox, управляющему представлением в зависимости от выбора в нем той или иной строки. В дальнейшем для нас важно, что строки, входящие в список scaleBox, расположены в том же порядке, в котором описаны элементы типа Scale. При изменении элемента scaleBox в меню наступает событие SelectedIndexChanged (изменен выбранный индекс). Следует добавить обработчик этого события в нашу форму, для чего

    1. Войдите в окно дизайнера Form1.h [Design] и выделите scaleBox.

    2. Войдите в окно свойств Properties и на вкладке Events (кнопка с молнией) найдите событие SelectedIndexChanged.

    3. Щелкните дважды по полю справа. В окне кода должен появиться скелет метода – обработчика события.

  2. Внутри метода-обработчика scaleBox_SelectedIndexChanged наберите оператор вызова метода SetScale так, чтобы обработчик имел вид

private: System::Void scaleBox_SelectedIndexChanged

(System::Object^ sender, System::EventArgs^ e)

{

// Устанавливаем новое представление чисел в таблице

SetScale((Scale)scaleBox->SelectedIndex);

}

При вызове SetScale в качестве параметра стоит выражение (Scale)scaleBox->SelectedIndex. Оно означает, что новое, выбранное значение scaleBox->SelectedIndex индекса элемента scaleBox преобразуется к элементу типа Scale и передается в качестве параметра методу SetScale. Элементы scaleBox индексированы от нуля и т.д. Так же "индексированы" элементы типа Scale. Поэтому указанное преобразование вполне определено и взаимно однозначно.

  1. Следует так же инициализировать значение индекса scaleBox, который по умолчанию отрицательный. Для этого в конструкторе формы (метод Form1 в верхней части файла) следует набрать строку

scaleBox->SelectedIndex=0;

  1. Теперь можно активировать проект, убедившись, что изменение представления байтов происходит нормально при выборе различных элементов в списке scaleBox из меню. Но, если нажать кнопку Randomize, или попытаться сохранить файл, находясь в представлении, отличном от десятичного, или, тем более, редактировать ячейки, результат будет неудовлетворительным. Поэтому нам необходимо изменить коды

    1. в уже готовом методе ResetGrid, помещающем числа в таблицу,

    2. в обработчике saveToolStripMenuItem_Click, сохраняющем содержание таблицы в файле и

    3. в обработчике dataGridView1_CellValidating, определяющем условия верной редакции ячеек.

  2. Начнем с метода ResetGrid. В нем есть операторы, передающие в ячейку значение байта из потока или из последовательности случайных чисел, которые имеют вид

#ifdef version1

source == Source::fileStream ? s->ReadByte() : _rnd->Next(256);

#else

source == _rnd ? _rnd->Next(256):((Stream^)source)

->ReadByte() ;

#endif

  1. Изменим эти операторы. Но, прежде, перед циклом по потоку внутри этого же метода (цикл по i) опишем локальную переменную, которая будет хранить текущий байт потока

// Хранит текущий байт

Byte curByte;

  1. Указанные выше операторы заменим на следующие

curByte =

#if version1

source == Source->fileStream ? s->ReadByte() : _rnd

->Next(256);

#else

source == _rnd ? _rnd->Next(256) : ((Stream^)source)

->ReadByte();

#endif

switch (_scale)

{

case Scale::decimal:

dataGridView1[col, row]->Value = curByte;

break;

case Scale::binary:

dataGridView1[col, row]->Value =

Convert::ToString(curByte, 2);

break;

case Scale::hexadecimal:

dataGridView1[col, row]->Value =

Convert::ToString(curByte, 16);

break;

}

Здесь мы вначале передаем в curByte байт из потока или из _rnd->Next, не помещая его в ячейку таблицы. Затем, в зависимости от установленной системы счисления (поле _scale) заполняем текущую ячейку таблицы соответствующим значением полученного байта в десятичной, двоичной или 16-ричной системе счисления.

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

  2. Можно сократить код, заменив оператор switch более коротким условным выражением вида

// Более короткий код, выполняющий те же функции, что оператор switch

dataGridView1[col, row]->Value = _scale == Scale::decimal ?

curByte.ToString() : Convert::ToString(curByte, _scale

== Scale::binary ? 2 : 16);

Возьмите в комментаторские скобки /* */ весь оператор switch, набрав затем приведенный выше код. Проверьте работу приложения в этой версии. Ошибок быть не должно.

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

// Байты записываются из ячеек в поток

s->WriteByte(Convert::ToByte( dataGridView1[col,row]

->Value));

запишем

// Байты записываются из ячеек в поток

s->WriteByte(Convert::ToByte( (String^)dataGridView1[col, row]->Value, _scale == Scale::hexadecimal ? 16 : _scale

== Scale::binary ? 2 : 10));

Здесь используется метод ToByte класса Convert, который преобразует строку, предполагающую выражение байта, записанного в одной из систем счисления с основаниями 2, 10 или 16, в значение байта.

  1. После этой редакции приложение должно работать и в режиме сохранения байтов из любого представления. Проверьте.

  1. Для того, чтобы ячейки можно было редактировать не только в десятичном виде, изменим код обработчика dataGridView1_CellValidating. Вот так выглядит прежний код в этом обработчике

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

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]!";

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

// Если ячейка только для чтения, то проверка редакции не производится

if (dataGridView1[e->ColumnIndex,e->RowIndex]->ReadOnly) return;

// Оператор try предваряет код, который может вызвать ошибку

// (выйти на исключительную ситуацию)

try

{

Convert::ToByte(e->FormattedValue->ToString(), _scale

== Scale::hexadecimal ? 16 : _scale == Scale::binary ? 2 : 10);

}

catch (...)

{

// Если исключительная ситуация возникает,

// управление передается блоку catch

// Если вводимая величина не является байтом, ввод отменяется

e->Cancel = true;

// Сообщение, которое появится, если введенная строка байтом не

// является

dataGridView1->Rows[e->RowIndex]->ErrorText =

"Набранная строка не является байтом в "

+ (_scale == Scale::binary ? "двоичной " :

_scale == Scale::decimal ? "десятичной " :

"шестнадцатеричной ") + "системе счисления!\n" +

"Нажмите esc, или наберите число в интервале [0;" +

(_scale == Scale::decimal ? "255" :

_scale == Scale::binary ? "11111111" : "ff") + "]";

}

  1. Этот вариант приложения должен работать в любой системе счисления, реагируя в том числе и на неверный ввод. Проверьте.

  2. Осталось немного оптимизировать код. Заметим, что условное выражение, возвращающее основание системы счисления

_scale == Scale::hexadecimal ? 16 : _scale == Scale::binary ? 2 : 10

встречается дважды – в обработчиках saveToolStripMenuItem_Click и dataGridView1_CellValidating. Можно заменить это выражение описанием одного свойства формы с именем Radix (так переводится на английский язык "основание системы счисления"). Для этого где-нибудь в теле класса формы (но не внутри какого-либо метода) наберите код

/// <summary>

/// Возвращает основание системы счисления

/// </summary>

property int Radix

{

int get()

{

return _scale == Scale::hexadecimal ? 16 : _scale

== Scale::binary ? 2 : 10;

}

}

Этот код знакомит нас с таким членом класса как "свойство". В данном случае свойство с именем Radix возвращает с помощью метода get значение основания системы счисления (число типа int) в зависимости от текущего значения поля _scale формы.

  1. Теперь в тех местах, где встречается выражение _scale == Scale::hexadecimal ? 16 : _scale == Scale::binary ? 2 : 10, его можно заменить словом Radix. Проверьте работу приложения.

  2. В методе ResetGrid был записан код вида

// Более короткий код, выполняющий те же функции, что оператор switch

dataGridView1[col, row]->Value = _scale == Scale::decimal ? curByte.ToString() : Convert::ToString(curByte, _scale == Scale::binary ? 2 : 16);

С помощью свойства Radix его можно сократить еще больше, записав

// Более короткий код, выполняющий те же функции, что оператор switch

dataGridView1[col, row]->Value =

Convert::ToString(curByte, Radix);

63