Лабораторная работа № 1.dpl61.narod.ru/files/TP/Lab_TP.pdf · 2017-02-19 ·...

48
Лабораторная работа № 1. Создание оконного приложения Windows. 1. Цель работы Ознакомиться со структурой оконного приложения Win32, реализацией обработки событий, простейшими способами отображения графических примитивов. 2. Описание лабораторного стенда В качестве лабораторного стенда используется IBM-совместимый персональный компьютер с установленной интегрированной средой разработки программ Dev-C++ 5.11. 3. Подготовка к выполнению работы 3.1. Повторить по конспекту лекций и литературе следующие вопросы: 3.1.1. основы программирования на языке C; 3.1.2. использование указателей; 3.1.3. основные приёмы работы в среде Dev-C++. 3.2. Подготовить электронный носитель для сохранения полученных результатов (файлов). 4. Программа работы 4.1. Создать папку для сохранения результатов работы. Создать проект Dev-C++ и сохранить его в созданную папку. 4.2. Изучить структуру основной функции приложения WinMain(). 4.3. Изучить структуру оконной функции приложения WndProc(). 4.4. Изменить заголовок основного окна. Скомпилировать и запустить созданную заготовку приложения. Исследовать поведение окна, реализуемое операционной системой. Изменить цвет фона клиентской области окна, проверить результат. 4.5. Ознакомиться с общими принципами вывода графики в клиентской области окна. Написать, отладить и выполнить программу, выводящую в окно текст. Продемонстрировать результат преподавателю. 4.6. Ознакомиться с простейшими способами отображения графических примитивов в клиентской области окна. Написать, отладить и выполнить программу, выводящую в окно различные графические примитивы. Продемонстрировать результат преподавателю. 4.7. Модифицировать имеющуюся программу таким образом, чтобы она рисовала в окне фигуру в соответствии с индивидуальным заданием (см. п. 5). Продемонстрировать результат преподавателю. 4.8. Сохранить результаты работы (папку с проектом Dev-C++) на внешнем носителе, поскольку они потребуются для выполнения следующей работы. 4.9. Оформить отчет по работе.

Transcript of Лабораторная работа № 1.dpl61.narod.ru/files/TP/Lab_TP.pdf · 2017-02-19 ·...

Page 1: Лабораторная работа № 1.dpl61.narod.ru/files/TP/Lab_TP.pdf · 2017-02-19 · – 3 – 6.2. К пункту 4.2 Основная функция приложения

Лабораторная работа № 1.

Создание оконного приложения Windows.

1. Цель работы

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

2. Описание лабораторного стенда

В качестве лабораторного стенда используется IBM-совместимый персональный компьютер с установленной интегрированной средой разработки программ Dev-C++ 5.11.

3. Подготовка к выполнению работы

3.1. Повторить по конспекту лекций и литературе следующие вопросы:

3.1.1. основы программирования на языке C;

3.1.2. использование указателей;

3.1.3. основные приёмы работы в среде Dev-C++.

3.2. Подготовить электронный носитель для сохранения полученных результатов (файлов).

4. Программа работы

4.1. Создать папку для сохранения результатов работы. Создать проект Dev-C++ и сохранить его в созданную папку.

4.2. Изучить структуру основной функции приложения WinMain().

4.3. Изучить структуру оконной функции приложения WndProc().

4.4. Изменить заголовок основного окна. Скомпилировать и запустить созданную заготовку приложения. Исследовать поведение окна, реализуемое операционной системой. Изменить цвет фона клиентской области окна, проверить результат.

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

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

4.7. Модифицировать имеющуюся программу таким образом, чтобы она рисовала в окне фигуру в соответствии с индивидуальным заданием (см. п. 5). Продемонстрировать результат преподавателю.

4.8. Сохранить результаты работы (папку с проектом Dev-C++) на внешнем носителе, поскольку они потребуются для выполнения следующей работы.

4.9. Оформить отчет по работе.

Page 2: Лабораторная работа № 1.dpl61.narod.ru/files/TP/Lab_TP.pdf · 2017-02-19 · – 3 – 6.2. К пункту 4.2 Основная функция приложения

– 2 –

5. Варианты заданий

Таблица 1.1. Варианты заданий

Вариант 1 2 3 4 5 6 7 8 9 10

Номер фигуры 1 2 3 4 5 6 1 2 3 4 Цвет 1 6 5 4 3 2 1 1 2 3 4 Цвет 2 1 2 3 4 5 6 4 3 2 1 Тип 1 1 2 3 1 2 3 2 1 3 2 Тип 2 2 3 1 2 3 2 1 3 2 1 Толщина 1 3 4 5 6 5 4 3 4 5 6 Толщина 2 6 5 4 3 4 5 6 5 4 3

Параметры «Тип 1», «Толщина 1», «Цвет 1» относятся к линиям, которыми нарисовано перекрестье, параметры «Тип 2», «Толщина 2» и «Цвет 2» - к линиям, образующим прямоугольник, окружность или эллипс.

Таблица 1.2. Виды фигур для рисования

Номер 1 2 3 4 5 6

Вид

Таблица 1.3. Номера цветов и типов линий

Номер 1 2 3 4 5 6

Цвет красный жёлтый зелёный синий чёрный серый Тип PS_SOLID

(сплошная) PS_DASH (пунктир)

PS_DOT (точки)

6. Указания к выполнению работы.

6.1. К пункту 4.1

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

Пример правильного имени папки: «D:\Work\MyProject\».

Пример недопустимого имени папки: «С:\Users\Student\Рабочий стол\MyProject\».

Для создания проекта необходимо запустить среду Dev-C++ и выбрать в меню пункт “File / New / Project…”. В появившемся окне «New Project» на вкладке «Basic» выбрать «Windows Application» и «C++ Project». В строке «Name» следует задать имя проекта, после чего нажать кнопку «Ok». В следующем окне нужно перейти в созданную ранее рабочую папку и нажать кнопку «Сохранить».

В результате Dev-C++ сформирует заготовку проекта, реализующего простейшее оконное приложение Win32, состоящую из единственного файла «main.cpp». Рекомендуется сразу нажать кнопку «Save» и сохранить этот файл в рабочую папку.

В файле «main.cpp» находятся заготовки двух функций: основной функции приложения «WinMain» и обработчика событий «WndProc». В процессе разработки эти заготовки могут дополняться необходимыми элементами.

Page 3: Лабораторная работа № 1.dpl61.narod.ru/files/TP/Lab_TP.pdf · 2017-02-19 · – 3 – 6.2. К пункту 4.2 Основная функция приложения

– 3 – 6.2. К пункту 4.2

Основная функция приложения Win32, вызываемая автоматически при запуске программы, называется не «main», как это было для консольных приложений, а «WinMain». Её подробное изучение выходит за рамки данной работы, рассмотрим лишь общий алгоритм этой функции (рис. 1.1).

Прежде всего описываются параметры нового класса окна, соответствующего основному окну создаваемого приложения. К этим параметрам, в частности, относятся вид курсора, иконка окна, цвет фона и некоторые другие. После того, как параметры заданы, предпринимается попытка зарегистрировать класс в системе (функция «RegisterClassEx»). В случае неудачи приложение завершает работу.

Далее на основе зарегистрированного класса создаётся экземпляр окна (функция «CreateWindowEx»). Поскольку ОС Windows позволяет создавать окна самого разнообразного вида и поведения, функция имеет множество параметров, отвечающих за это разнообразие. Для выполнения данной работы параметры, заданные в заготовке, изменять не следует (исключение составляют параметры width и height, задающие начальную ширину и высоту окна, а также Caption, определяющий заголовок окна, – их при желании можно изменить).

Если основное окно приложения создано удачно, функция CreateWindowEx возвращает так называемый дескриптор (handle) окна – уникальный идентификатор, позволяющий впоследствии обращаться к этому окну. Дескриптор сохраняется в переменной hwnd. Если же окно создать не удалось, функция возвращает NULL, в результате чего приложение завершает работу.

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

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

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

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

Начало

Задание параметров класса окна

Обработать сообщение

Регистрация класса в системе

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

Получить сообщение

Рис. 1.1. Алгоритм функции WinMain

Page 4: Лабораторная работа № 1.dpl61.narod.ru/files/TP/Lab_TP.pdf · 2017-02-19 · – 3 – 6.2. К пункту 4.2 Основная функция приложения

– 4 – Полученный код сообщения передаётся функции TranslateMessage(), которая выполняет дополнительную обработку сообщений, связанных с виртуальными клавишами. В нашем случае эта обработка интереса не представляет.

Наконец, функция DispatchMessage() отправляет полученное сообщение на обработку оконной функции WndProc().

6.3. К пункту 4.3

Реакция приложения на события реализуется в так называемой оконной функции WndProc(). Функция получает четыре параметра: дескриптор окна, которому предназначено сообщение (hwnd), код сообщения Message и два дополнительных параметра сообщения: wParam и lParam. Смысл дополнительных параметров зависит от конкретного кода сообщения. Например, для сообщения WM_MOUSEMOVE (перемещение мыши) wParam содержит признаки нажатых кнопок, а lParam – координаты курсора. Для сообщения WM_SIZE (изменение размера окна) wParam содержит дополнительные признаки операции, а lParam – новую ширину и высоту окна.

В сложных приложениях реализация функции WndProc может быть очень громоздкой, хотя структура её проста: обычно она содержит единственный оператор switch, сравнивающий код полученного сообщения с кодами, на которые программа должна реагировать: LRESULT CALLBACK WndProc(HWND hwnd, UINT Message, WPARAM wParam, LPARAM lParam) { switch(Message) { case WM_MOUSEMOVE: // действия в ответ на перемещение мыши break; case WM_PAINT: // действия в ответ на перерисовку содержимого окна break; case WM_DESTROY: // действия в ответ на уничтожение окна break; // и так далее default: return DefWindowProc(hwnd, Message, wParam, lParam); } return 0; }

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

6.4. К пункту 4.4

В созданной заготовке проекта основное окно приложения имеет заголовок «Caption». В разрабатываемом приложении имеет смысл заменить его на более информативный текст, например, «Моё первое приложение Win32». Это можно сделать путём изменения фактических параметров функции CreateWindowEx (см. п. 6.2).

Компиляция и запуск приложений Win32 ничем не отличается от аналогичных действий для консольных приложений: в меню среды разработки следует выбрать пункт «Execute /

Page 5: Лабораторная работа № 1.dpl61.narod.ru/files/TP/Lab_TP.pdf · 2017-02-19 · – 3 – 6.2. К пункту 4.2 Основная функция приложения

– 5 – Compile & Run». При отсутствии синтаксических ошибок программа запустится и на экране появится основное окно приложения с заданным заголовком.

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

При этом клиентская область основного окна представляет собой пустое поле, закрашенное цветом, заданным при описании класса окна (см. п. 6.2). Содержимое этой области должно определяться приложением.

Примечание. Попробуйте задать другой цвет фона клиентской области путём замены в функции WinMain строки wc.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);

на строку wc.hbrBackground = (HBRUSH)(COLOR_MENUBAR);

Можно попробовать и другие константы цвета, определённые в файле «winuser.h» (чтобы быстро их найти, нажмите клавишу Ctrl и, не отпуская её, щелкните мышкой по тексту «COLOR_MENUBAR» или «COLOR_WINDOW»).

6.5. К пункту 4.5

6.5.1. Отрисовка содержимого клиентской области должна выполняться приложением в ответ на сообщение WM_PAINT. Это сообщение система направляет приложению при изменении размера окна, при его восстановлении из свёрнутого состояния, а также в других случаях, когда клиентская область или её часть становится видимой. Следовательно, приложение должно быть готово в любой момент перерисовать клиентскую область в ответ на сообщение WM_PAINT.

Если самому приложению необходимо в определённый момент времени перерисовать клиентскую область (например, для изменения отображаемой информации), то оно вызывает системную функцию InvalidateRect или InvalidateRgn, помечая всю клиентскую область или её часть, как требующую перерисовки. ОС, обнаружив наличие таких помеченных областей, посылает приложению сообщение WM_PAINT (это может произойти не сразу, а после выполнения системой более приоритетных задач).

Таким образом, для вывода изображений в клиентскую область в функции WndProc необходимо выполнить обработку сообщения WM_PAINT. Это можно сделать следующим образом: LRESULT CALLBACK WndProc(HWND hwnd, UINT Message, WPARAM wParam, LPARAM lParam) { switch(Message) { case WM_PAINT: ClientDraw(hwnd, wParam, lParam); break; // ........ – обработка других сообщений default: return DefWindowProc(hwnd, Message, wParam, lParam); } return 0; }

Page 6: Лабораторная работа № 1.dpl61.narod.ru/files/TP/Lab_TP.pdf · 2017-02-19 · – 3 – 6.2. К пункту 4.2 Основная функция приложения

– 6 – В приведённом примере при получении сообщения WM_PAINT вызывается функция ClientDraw, в которой и должно выполняться рисование (такой подход позволяет не загромождать функцию WndProc деталями обработки каждого сообщения).

6.5.2. Функция ClientDraw может иметь следующий вид: void ClientDraw(HWND hwnd, WPARAM wParam, LPARAM lParam) { PAINTSTRUCT ps; HDC hdc = BeginPaint(hwnd, &ps); // ......... // Операции вывода графики в клиентскую область // ......... EndPaint(hwnd, &ps); }

В качестве параметров она получает дескриптор окна, в котором будет выполняться рисование, а также параметры wParam и lParam сообщения WM_PAINT.

Для вывода графики в ОС Windows используется так называемый контекст графического устройства (DC – device context). Его можно сравнить с листом бумаги, на котором выполняется рисование. Свой контекст есть у каждого окна, и любой функции, выполняющей рисование, в качестве одного из параметров передаётся дескриптор (идентификатор) контекста.

Функция ClientDraw прежде всего вызывает системную функцию BeginPaint, которая готовит графический контекст окна к использованию. BeginPaint по окончанию своей работы возвращает дескриптор контекста hdc – его мы будем использовать в дальнейшем при вызове графических функций.

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

По окончанию рисования вызывается функция EndPaint для освобождения системных ресурсов, занятых при вызове BeginPaint.

6.5.3. Рассмотрим несложный пример вывода текста в клиентскую область окна. Для этого дополним функцию ClientDraw следующим образом (нижеприведённый текст следует вставить до функции WndProc): void ClientDraw(HWND hwnd, WPARAM wParam, LPARAM lParam) { char str[]="Ура! Мы сделали это!!!"; PAINTSTRUCT ps; HDC hdc = BeginPaint(hwnd, &ps); TextOut(hdc, 50, 20, str, strlen(str)); EndPaint(hwnd, &ps); }

Функция TextOut выводит строку из массива Str, начиная с позиции (50, 20) в локальной системе координат окна (вообще все графические функции в приведённых примерах работают с использованием локальной системы координат). Следует отметить, что ось Y локальной системы координат направлена вниз, то есть при увеличении координаты «y» точка смещается не вверх, а вниз.

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

6.6. К пункту 4.6

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

Page 7: Лабораторная работа № 1.dpl61.narod.ru/files/TP/Lab_TP.pdf · 2017-02-19 · – 3 – 6.2. К пункту 4.2 Основная функция приложения

– 7 – последующей компиляцией и выполнением программы. После включения всех примеров в указанную функцию следует продемонстрировать результат преподавателю.

6.6.1. Рисование линий

Для рисования линий можно использовать следующие функции (приведено упрощенное описание; полную документацию можно найти в [9.1]):

BOOL MoveToEx(HDC hdc, int X, int Y, LPPOINT lpPoint) – помещает текущую позицию (начальную точку рисования) в координаты (X, Y);

BOOL LineTo(HDC hdc, int nXEnd, int nYEnd) – рисует линию из текущей позиции в точку (nXEnd, nYEnd); перемещает текущую позицию в эту точку.

Пример: MoveToEx(hdc, 50,60,NULL); LineTo(hdc, 80, 80); LineTo(hdc, 40, 20); MoveToEx(hdc, 30,30,NULL); LineTo(hdc, 50, 100);

Стиль, цвет и толщина выводимых линий определяются текущим пером (Pen). По умолчанию для контекста окна уже выбрано какое-то перо, до сих пор мы им и рисовали. Чтобы изменить параметры линий, следует создать и выбрать соответствующее перо: LOGBRUSH lb; // структура, описывающая заливку линии lb.lbStyle = BS_SOLID; // стиль заливки - сплошной lb.lbColor = RGB(255,0,0); // цвет - красный lb.lbHatch = 0; // штриховки нет // создаём перо1: сплошная линия (PS_SOLID), ширина 5 HGDIOBJ hPen1 = ExtCreatePen(PS_GEOMETRIC | PS_SOLID,5,&lb,0,NULL); // создаём перо2: пунктирная линия (PS_DASH), ширина 8 lb.lbColor = RGB(0,255,0); // цвет пера 2 – зелёный HGDIOBJ hPen2 = ExtCreatePen(PS_GEOMETRIC | PS_DASH,8,&lb,0,NULL); // выбираем новое перо (1), сохраняя старое в hPenOld HGDIOBJ hPenOld = SelectObject(hdc, hPen1); // Продолжаем рисовать пером 1 LineTo(hdc, 200, 100); // выбираем перо 2 (старое можно не сохранять, мы его и так знаем) SelectObject(hdc, hPen2); // Продолжаем рисовать пером 2 LineTo(hdc, 50, 150); // выбираем перо 1 SelectObject(hdc, hPen1); // Продолжаем рисовать пером 1 LineTo(hdc, 200, 150); // восстанавливаем перо по умолчанию SelectObject(hdc, hPenOld); // Уничтожаем созданные нами перья (освобождаем ресурсы) DeleteObject(hPen1); DeleteObject(hPen2);

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

Page 8: Лабораторная работа № 1.dpl61.narod.ru/files/TP/Lab_TP.pdf · 2017-02-19 · – 3 – 6.2. К пункту 4.2 Основная функция приложения

– 8 – Если в процессе рисования ломаной линии смена пера не требуется, можно использовать функцию Polyline или PolylineTo. Они отличаются лишь тем, что первая не изменяет текущую позицию, а вторая перемещает её в конец нарисованной линии (аналогично функции LineTo). BOOL Polyline(HDC hdc, const POINT *lppt, int cPoints) BOOL PolylineTo(HDC hdc, const POINT *lppt, int cPoints)

Второй параметр – указатель на массив координат точек (x, y), через которые пройдёт ломаная линия, третий параметр – количество точек.

Дополним функцию ClientDraw следующими строками перед строкой SelectObject(hdc, hPenOld) и проверим результат работы программы: // массив координат точек (x,y) POINT Points[]={300,300, 350,300, 350,350, 200,300}; // рисуем линию через заданные четыре точки Polyline(hdc, Points, 4);

6.6.2. Рисование прямоугольников

Прямоугольник, как, впрочем, и любой другой многоугольник, можно нарисовать линиями с помощью функций LineTo, Polyline, PolylineTo, однако, для этого есть более простой способ – использование функции Rectangle: BOOL Rectangle(HDC hdc, int nLeftRect, int nTopRect, int nRightRect, int nBottomRect);

Параметры со второго по пятый задают координаты верхней левой и нижней правой точек прямоугольника. Фигура рисуется с использованием текущего пера и закрашивается текущей кистью (по умолчанию – сплошная белая).

Изобразим прямоугольник, дополнив функцию ClientDraw: SelectObject(hdc, hPen1); Rectangle(hdc, 200, 20, 300, 50);

Нарисуем ещё один прямоугольник, выбрав другое перо и кисть. При этом используем одно из созданных перьев, а кисть возьмём стандартную с помощью функции GetStockObject: SelectObject(hdc, hPen2); // выбираем перо SelectObject(hdc, GetStockObject(GRAY_BRUSH)); // выбираем кисть Rectangle(hdc, 50, 200, 100, 300);

Если закрашивать прямоугольник вообще не нужно, вместо GRAY_BRUSH следует использовать NULL_BRUSH.

6.6.3. Рисование эллипсов и окружностей BOOL Ellipse(HDC hdc, int nLeftRect, int nTopRect, int nRightRect, int nBottomRect);

Для рисования эллипса задаются те же параметры, что и для прямоугольника, однако функция выводит не прямоугольник, а вписанный в него эллипс. Если вместо прямоугольника задать квадрат, то вместо эллипса получится окружность: SelectObject(hdc, hPen1); // выбираем перо 1 // Выбираем серую кисть SelectObject(hdc, GetStockObject(GRAY_BRUSH)); // Рисуем окружность Ellipse(hdc, 50, 350, 150, 450);

Page 9: Лабораторная работа № 1.dpl61.narod.ru/files/TP/Lab_TP.pdf · 2017-02-19 · – 3 – 6.2. К пункту 4.2 Основная функция приложения

– 9 – // Выбираем «пустую» кисть (нет заливки) SelectObject(hdc, GetStockObject(NULL_BRUSH)); // Рисуем эллипс Ellipse(hdc, 200, 350, 500, 450);

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

6.7. К пункту 4.7

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

7. Содержание отчёта.

Общие положения.

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

Использование в отчёте экранных копий допускается только для окон, содержащих результат работы программы (т.е. окончательные результаты выполнения пунктов 4.6, 4.7). В текст вставляется только изображение окна разработанной программы, остальные части копии экрана должны быть удалены.

Исходные тексты программ, включенные в отчёт, должны быть отформатированы в соответствии с правилами, принятыми для языка программирования С++, аналогично тому, как это сделано в методических указаниях (отступы вложенных блоков, согласованное расположение парных открывающих и закрывающих скобок, одинаковые отступы для комментариев и т.п.). С целью упрощения форматирования и восприятия для исходных текстов следует использовать полужирный моноширинный шрифт (например «Courier New»). Фрагменты исходных текстов должны сопровождаться комментариями.

Отчёт по работе должен содержать:

Название работы. Цель работы. Номер варианта и соответствующее ему индивидуальное задание из п. 5 (с

расшифровкой обозначений цветов и типов линий). Копирование таблиц п. 5 в отчёт полностью не допускается.

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

Для отдельных пунктов программы работы в отчёте необходимо привести следующее.

Для пункта 4.2

Схему алгоритма функции WinMain(). Исходный текст функции WinMain() с пояснением основных операций в виде

комментариев.

Для пункта 4.3

Схему алгоритма функции WndProc(), учитывающую изменения, добавленные в п. 4.5. Исходный текст функции WndProc(), соответствующий приведённой схеме алгоритма,

с пояснением основных операций в виде комментариев.

Page 10: Лабораторная работа № 1.dpl61.narod.ru/files/TP/Lab_TP.pdf · 2017-02-19 · – 3 – 6.2. К пункту 4.2 Основная функция приложения

– 10 – Для пункта 4.6

Окончательный вариант исходного текста функции ClientDraw(), полученного в данном пункте, с пояснением каждого действия в виде комментариев.

Изображение основного окна разработанной программы.

Для пункта 4.7

Исходный текст функции ClientDraw(), полученный в данном пункте, с пояснением каждого действия в виде комментариев.

Изображение основного окна разработанной программы.

Заключительная часть

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

8. Контрольные вопросы.

8.1. Каково назначение основной функции приложения WinMain? Какие задачи она выполняет в разработанной программе?

8.2. Что такое «программа, управляемая событиями»? Как приложение Windows узнаёт о том или ином событии?

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

8.4. Каким образом описываются сообщения в ОС Windows?

8.5. С какой целью в приложении Windows реализуется обработка сообщений? Все ли поступающие сообщения необходимо обрабатывать явным образом?

8.6. Каково назначение оконной функции WndProc? Опишите её структуру.

8.7. В каких случаях приложение Windows должно выполнять перерисовку клиентской области окна? Как приложение узнаёт о том, что необходимо выполнить перерисовку?

8.8. С помощью какой функции выполняется вывод текста в окно? Опишите параметры этой функции.

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

8.10. Какими способами можно нарисовать в окне прямоугольник? Опишите особенности каждого из способов.

8.11. Какими способами можно нарисовать в окне ломаную линию? Опишите особенности каждого из способов.

9. Список литературы.

9.1. Каталог API (Microsoft) и справочных материалов [Электронный ресурс] URL: http://msdn.microsoft.com/library.

Page 11: Лабораторная работа № 1.dpl61.narod.ru/files/TP/Lab_TP.pdf · 2017-02-19 · – 3 – 6.2. К пункту 4.2 Основная функция приложения

– 11 –

Лабораторная работа № 2.

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

1. Цель работы

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

2. Описание лабораторного стенда

В качестве лабораторного стенда используется IBM-совместимый персональный компьютер с установленной интегрированной средой разработки программ Dev-C++ 5.11.

3. Подготовка к выполнению работы

3.1. Подготовить работоспособный проект, полученный в результате выполнения лабораторной работы №1.

3.2. Подготовить электронный носитель для сохранения полученных результатов (файлов).

4. Программа работы

4.1. На рабочем компьютере создать папку для сохранения результатов работы. Скопировать в неё проект Dev-C++ с результатами выполнения индивидуального задания ЛР №1.

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

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

4.4. Реализовать перемещение изображаемой фигуры в пределах клиентской области окна в ответ на перемещение мыши. Продемонстрировать результат преподавателю.

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

4.6. Сохранить результаты работы (папку с проектом Dev-C++) на внешнем носителе, поскольку они потребуются для выполнения следующей работы.

4.7. Оформить отчет по работе.

5. Варианты заданий

Основные параметры индивидуального задания следует взять из п. 5 ЛР №1. Дополнительно к ним в данной работе задаётся траектория перемещения фигуры. Виды

Page 12: Лабораторная работа № 1.dpl61.narod.ru/files/TP/Lab_TP.pdf · 2017-02-19 · – 3 – 6.2. К пункту 4.2 Основная функция приложения

– 12 – траекторий приведены в таблице 2.2, соответствие номера траектории варианту индивидуального задания – в таблице 2.1.

Таблица 2.1. Варианты заданий

Вариант 1 2 3 4 5 6 7 8 9 10

Траектория перемещения

1 2 3 4 5 6 5 4 3 2

Таблица 2.2. Траектории перемещения фигуры

Номер 1 2 3 4 5 6

Вид

6. Указания к выполнению работы.

6.1. К пункту 4.2

6.1.1. В предыдущей работе координаты всех графических примитивов при рисовании задавались относительно начала координат клиентской области – точки (0, 0). Это приемлемо в случае вывода статических изображений, но неудобно, если изображение необходимо перемещать в пределах окна: при таком способе задания координат для перемещения фигуры придётся изменить координаты каждой ключевой точки каждого графического примитива, составляющего фигуру (т.е. по две точки для линий эллипсов и прямоугольников). Чем сложнее фигура, тем большее количество ключевых точек она имеет.

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

6.1.2. В качестве примера рассмотрим фигуру, приведённую на рис. 2.1 и состоящую из двух пересекающихся линий. Чтобы изобразить её тем способом, который использовался в ЛР №1, в функции ClientDraw() потребуется выполнить следующее: // Первая линия // Начинаем с точки (4,1) MoveToEx(hdc, 4,1,NULL); // Линия в точку (8,5) LineTo(hdc, 8, 5); // Вторая линия // Начинаем с точки (2,5) MoveToEx(hdc, 2,5,NULL); // Линия в точку (8,2) LineTo(hdc, 8, 2);

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

1 2 3 4 5 6 7 8 9

1

2

3

4

5

6

Рис. 2.1. Рисование линий

Page 13: Лабораторная работа № 1.dpl61.narod.ru/files/TP/Lab_TP.pdf · 2017-02-19 · – 3 – 6.2. К пункту 4.2 Основная функция приложения

– 13 – 6.1.3. Теперь рассмотрим другой способ рисования фигуры. Выберем некоторую точку в качестве базовой. В общем случае выбор может быть произвольным, но обычно бывает удобно выбирать в качестве базовой какую-либо характерную точку фигуры. Пусть это будет точка пересечения линий с координатами (6, 3). Для хранения координат точки в Windows API используется специальная структура POINT, имеющая два поля: x и y. Опишем глобальную переменную для хранения координат базовой точки и присвоим ей начальное значение, соответствующее текущим координатам точки пересечения линий: POINT Base = {6,3};

Далее в функции ClientDraw() изобразим линии, задавая их координаты относительно базовой точки: // Первая линия // Начинаем с точки, отстоящей от базовой на -2 по X и -2 по Y MoveToEx(hdc, Base.x-2, Base.y-2, NULL); // Линия в точку, отстоящую от базовой на +2 по X и +2 по Y LineTo(hdc, Base.x+2, Base.y+2); // Вторая линия // Начинаем с точки, отстоящей от базовой на -4 по X и +2 по Y MoveToEx(hdc, Base.x-4, Base.y+2, NULL); // Линия в точку, отстоящую от базовой на +2 по X и -1 по Y LineTo(hdc, Base.x+2, Base.y-1);

Как видно из приведённого текста, он содержит вызовы тех же функций, что и ранее, но использует иной, на первый взгляд более сложный, способ задания координат. Однако такой подход позволит нам выполнять перемещение фигуры существенно проще: для этого потребуется всего лишь изменить координаты базовой точки и повторить без изменений основную часть программы рисования. Например, чтобы переместить фигуру на две единицы вправо и одну единицу вниз нужно скорректировать координаты базовой точки: Base.x+=2; Base.y++;

Затем вызывается функция рисования ClientDraw(), которая изобразит фигуру уже в новой позиции.

6.1.4 . Для выполнения п. 4.2 программы работы следует изменить исходный текст программы, описав глобальную переменную Base как показано выше. Затем необходимо изменить функцию ClientDraw() таким образом, чтобы она выполняла рисование фигуры из индивидуального задания способом, описанным в п. 6.1.3, т.е. относительно базовой точки.

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

6.2. К пункту 4.3

При нажатии клавиш на клавиатуре Windows посылает активному окну сообщение WM_KEYDOWN. Следовательно, чтобы реагировать на нажатия клавиш, программа должна обрабатывать это сообщение. Добавим обработку сообщения WM_KEYDOWN в оконную функцию WndProc(): case WM_KEYDOWN: ProcessKey(hwnd, wParam, lParam); break;

В ответ на сообщение вызывается функция ProcessKey(), которой передаётся дескриптор окна и параметры сообщения, содержащие код клавиши (wParam) и дополнительную информацию (lParam).

Page 14: Лабораторная работа № 1.dpl61.narod.ru/files/TP/Lab_TP.pdf · 2017-02-19 · – 3 – 6.2. К пункту 4.2 Основная функция приложения

– 14 – Функция ProcessKey() должна быть описана до WndProc() и может выглядеть следующим образом: void ProcessKey(HWND hwnd, WPARAM wParam, LPARAM lParam) { // проверяем код нажатой клавиши switch (wParam) { // VK_UP – код клавиши «Стрелка вверх» case VK_UP: Base.y-=Step.y; break; // VK_DOWN – код клавиши «Стрелка вниз» case VK_DOWN: Base.y+=Step.y; break; // ... // реакция на другие клавиши // ... } // помечаем всю клиентскую область окна, как требующую перерисовки InvalidateRect(hwnd, NULL, 1); }

В ответ на нажатие клавиш «Вверх» и «Вниз» функция изменяет координату «y» базовой точки на величину Step.y. В конце вызывается функция InvalidateRect(), требующая перерисовки клиентской области окна при появлении такой возможности.

Для нормальной работы функции ProcessKey() нужно описать глобальную переменную Step типа POINT для хранения величины шага перемещения фигуры по вертикали (Step.y) и по горизонтали (Step.х): POINT Step = {4,4};

После компиляции программы и проверки перемещения фигуры вверх и вниз функцию ProcessKey() нужно дополнить обработкой кодов клавиш VK_LEFT «Стрелка влево» и VK_RIGHT «Стрелка вправо», обеспечив соответствующее перемещение фигуры. Добавьте также обработку клавиши «Home» (код VK_HOME), обеспечив возвращение фигуры в некоторую исходную позицию. Измените значения полей переменной Step, наблюдайте и объясните результат.

6.3. К пункту 4.4

При перемещении мыши в границах клиентской области окна Windows посылает ему сообщение WM_MOUSEMOVE. Следовательно, чтобы реагировать на движение мыши, программа должна обрабатывать это сообщение. Обработка сообщения WM_MOUSEMOVE выполняется аналогично обработке WM_KEYDOWN. Дополним функцию WndProc() следующими строками: case WM_MOUSEMOVE: ProcessMouse(hwnd, wParam, lParam); break;

Параметр wParam содержит признаки кнопок мыши и клавиатуры, нажатых во время перемещения, а lParam – координаты курсора (x – младшая пара байтов, y – старшая).

Функция ProcessMouse() должна быть описана до WndProc() и может выглядеть следующим образом:

Page 15: Лабораторная работа № 1.dpl61.narod.ru/files/TP/Lab_TP.pdf · 2017-02-19 · – 3 – 6.2. К пункту 4.2 Основная функция приложения

– 15 – void ProcessMouse(HWND hwnd, WPARAM wParam, LPARAM lParam) { // Проверяем, установлен ли флаг нажатия левой кнопки мыши // Реагируем на мышь только при нажатой левой кнопке if (wParam & MK_LBUTTON) { // Получаем координаты x и y указателя мыши // и перемещаем туда базовую точку Base.x = lParam % 0x10000; Base.y = lParam / 0x10000; // Информируем ОС о необходимости перерисовки окна InvalidateRect(hwnd, NULL, 1); } }

После доработки программы и проверки её работоспособности измените функцию ProcessMouse() так, чтобы фигура двигалась за мышью:

только при нажатой правой кнопке мыши (флаг «MK_RBUTTON»); только при нажатой клавише «Shift» на клавиатуре (флаг «MK_SHIFT»); в любом случае независимо от нажатия кнопок.

6.4. К пункту 4.5

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

Для периодического формирования сообщений удобно использовать специальный объект Windows – таймер. Для создания таймера используется функция SetTimer(), описанная следующим образом: UINT_PTR SetTimer( HWND hWnd, // дескриптор окна, которому направляются сообщения UINT_PTR nIDEvent, // уникальный идентификатор таймера UINT uElapse, // период таймера, мс TIMERPROC lpTimerFunc // функция, вызываемая при срабатывании );

После создания таймер начинает работать и с периодичностью uElapse направляет заданному окну сообщение WM_TIMER с параметром wParam, равным nIDEvent. Идентификатор nIDEvent должен быть уникальным для каждого таймера (если их несколько). После того, как таймер станет не нужен, его следует уничтожить при помощи функции KillTimer().

Так как в нашей программе будет всего один таймер, в качестве nIDEvent можно указать любое число. Однако более правильно в начале программы определить константу с понятным именем и произвольным значением, а затем использовать в качестве идентификатора её: #define TIMER_ID 1234

Изменим программу, добавив в неё создание и уничтожение таймера, формирующего сообщения WM_TIMER каждые 30 мс:

Page 16: Лабораторная работа № 1.dpl61.narod.ru/files/TP/Lab_TP.pdf · 2017-02-19 · – 3 – 6.2. К пункту 4.2 Основная функция приложения

– 16 – // Перед основным циклом обработки сообщений создаём таймер SetTimer(hwnd, TIMER_ID, 30, NULL); while(GetMessage(&msg, NULL, 0, 0) > 0) { TranslateMessage(&msg); DispatchMessage(&msg); } // По окончанию работы программы уничтожаем таймер KillTimer(hwnd, TIMER_ID); return msg.wParam;

6.4.2. Для обработки сообщения WM_TIMER дополним функцию WndProc(): case WM_TIMER: if (wParam==TIMER_ID) // если сообщение от «нужного» таймера, NextMoveStep(hwnd); // то выполняем очередной шаг перемещения break;

Функция NextMoveStep() должна быть описана до WndProc() и может выглядеть следующим образом: void NextMoveStep(HWND hwnd) { // смещаем базовую точку Base.x+=Step.x; Base.y+=Step.y; // помечаем всю клиентскую область окна, как требующую перерисовки InvalidateRect(hwnd, NULL, 1); }

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

6.4.3. Рассмотренный в п. 6.4.2 обработчик сообщений от таймера обеспечивает лишь прямолинейное движение фигуры и со временем выводит её за пределы видимой клиентской области окна. Чтобы обеспечить движение фигуры по заданной траектории, необходимо контролировать её текущие координаты и своевременно изменять направление движения.

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

Пусть, например, необходимо обеспечить циклическое движение фигуры по замкнутой траектории в форме прямоугольного треугольника: сначала вниз, затем вправо, затем влево-вверх (рис. 2.2). Очевидно, что поведение фигуры имеет три состояния: движение вниз, движение вправо и движение влево-вверх. Перечисленные состояния нужно каким-либо образом обозначить, в простейшем случае пронумеровать. Но поскольку номера состояний сложно запоминать, введём несколько констант с уникальными значениями и с понятными именами, соответствующими различным состояниям: #define MS_DOWN 0 // движение вниз #define MS_RIGHT 1 // движение вправо #define MS_TOPLEFT 2 // движение влево-вверх

Ещё более правильно было бы для представления состояний определить перечислимый тип (enum), но для упрощения текста программы используем обычные целочисленные константы.

(100, 100)

(100, 300) (500, 300)

Рис. 2.2. Пример траектории движения

Page 17: Лабораторная работа № 1.dpl61.narod.ru/files/TP/Lab_TP.pdf · 2017-02-19 · – 3 – 6.2. К пункту 4.2 Основная функция приложения

– 17 – Затем необходимо описать глобальную целочисленную переменную, в которой будет храниться текущее состояние движения: int MoveState = MS_DOWN; // начальное состояние – движение вниз

Функция NextMoveStep() при каждом вызове должна проверять, в каком состоянии сейчас находится фигура, и корректировать её координаты в соответствии с этим состоянием. Кроме того, в каждом состоянии нужно проверять текущие координаты и при достижении очередной точки излома траектории переходить в следующее состояние. void NextMoveStep(HWND hwnd) { // Проверяем текущее состояние switch (MoveState) { case MS_DOWN: // движение вниз // Если координата y достигла значения 300, переходим // в состояние «движение вправо» if (Base.y>300) MoveState = MS_RIGHT; // а иначе продолжаем движение вниз else Base.y+=Step.y; break; case MS_RIGHT: // движение вправо // Если координата x достигла значения 500, переходим // в состояние «движение влево-вверх» if (Base.x>500) MoveState = MS_TOPLEFT; // а иначе продолжаем движение вправо else Base.x+=Step.x; break; case MS_TOPLEFT: // движение влево-вверх // Если координата x стала меньше 100, переходим // в исходное состояние «движение вниз» if (Base.x<100) MoveState = MS_DOWN; // а иначе продолжаем движение влево-вверх else { Base.x-=Step.x; Base.y-=Step.y/2; } break; // Если по какой-то причине переменная состояния имеет иное // значение, возвращаем её в исходное состояние default: MoveState = MS_DOWN; } // помечаем всю клиентскую область окна, как требующую перерисовки InvalidateRect(hwnd, NULL, 1); }

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

6.4.4. Для выполнения индивидуального задания необходимо по аналогии с примером, рассмотренным в п. 6.4.3, определить перечень состояний для заданной траектории движения, описать соответствующие константы состояний и модифицировать текст функции NextMoveStep() для корректного перемещения фигуры в каждом состоянии и своевременного переключения состояний. Координаты точек излома траектории можно выбирать произвольно с сохранением формы траектории (предполагается, что линии траектории направлены вертикально, горизонтально или под углом 45°).

Page 18: Лабораторная работа № 1.dpl61.narod.ru/files/TP/Lab_TP.pdf · 2017-02-19 · – 3 – 6.2. К пункту 4.2 Основная функция приложения

– 18 –

7. Содержание отчёта.

Общие положения.

См. п. 7 лабораторной работы №1.

Отчёт по работе должен содержать:

Название работы. Цель работы. Номер варианта и соответствующее ему индивидуальное задание из п. 5 (с

расшифровкой обозначений цветов и типов линий). Копирование таблиц п. 5 в отчёт полностью не допускается.

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

Для отдельных пунктов программы работы в отчёте необходимо привести следующее.

Для пункта 4.2

Исходный текст функции ClientDraw() с пояснением основных операций в виде комментариев.

Для пункта 4.3

Исходный текст функции ProcessKey() с пояснением основных операций в виде комментариев.

Для пункта 4.4

Исходные тексты всех самостоятельно полученных вариантов функции ProcessMouse() с пояснением основных операций в виде комментариев.

Для пункта 4.5

Описание констант, соответствующих состояниям движения фигуры. Исходные тексты функций NextMoveStep() и WndProc(), соответствующие

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

Заключительная часть

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

8. Контрольные вопросы.

8.1. Как ОС WIndows уведомляет приложение о нажатии клавиши? Опишите параметры соответствующего сообщения.

8.2. Как ОС WIndows уведомляет приложение о перемещении мыши? Опишите параметры соответствующего сообщения.

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

8.4. В чём преимущества задания координат графических примитивов относительно базовой точки при рисовании сложных фигур?

8.5. Почему при перемещении фигуры с помощью клавиатуры она может полностью выходить за пределы клиентской области окна, а с помощью мыши – нет?

Page 19: Лабораторная работа № 1.dpl61.narod.ru/files/TP/Lab_TP.pdf · 2017-02-19 · – 3 – 6.2. К пункту 4.2 Основная функция приложения

– 19 – 8.6. Для чего используются состояния при программировании движения по сложной

траектории?

Page 20: Лабораторная работа № 1.dpl61.narod.ru/files/TP/Lab_TP.pdf · 2017-02-19 · – 3 – 6.2. К пункту 4.2 Основная функция приложения

– 20 –

Лабораторная работа № 3.

Основы объектно-ориентированного программирования. Описание классов в языке C++. Инкапсуляция.

1. Цель работы

Ознакомиться с основами объектно-ориентированного подхода в программировании и с базовыми средствами объектно-ориентированного программирования (ООП) языка C++.

2. Описание лабораторного стенда

В качестве лабораторного стенда используется IBM-совместимый персональный компьютер с установленной интегрированной средой разработки программ Dev-C++ 5.11.

3. Подготовка к выполнению работы

3.1. Подготовить работоспособный проект, полученный в результате выполнения лабораторной работы №2.

3.2. Подготовить электронный носитель для сохранения полученных результатов (файлов).

3.3. Ознакомиться с объектно-ориентированными возможностями языка C++, изучить сведения, приведённые в п. 6 данной работы.

4. Программа работы

4.1. Создать папку для сохранения результатов работы. Создать новый проект Dev-C++ и сохранить его в подготовленную рабочую папку.

4.2. Дополнить проект описанием класса, реализующего графический объект.

4.3. Создать несколько экземпляров объекта, продемонстрировать результат преподавателю.

4.4. Дополнить описанный класс конструктором и деструктором. Отладить программу, продемонстрировать результат преподавателю.

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

4.6. Сохранить результаты работы (папку с проектом Dev-C++) на внешнем носителе, поскольку они потребуются для выполнения следующей работы.

4.7. Оформить отчет по работе.

5. Варианты заданий

Индивидуальное задание соответствует п. 5 ЛР №1.

Page 21: Лабораторная работа № 1.dpl61.narod.ru/files/TP/Lab_TP.pdf · 2017-02-19 · – 3 – 6.2. К пункту 4.2 Основная функция приложения

– 21 –

6. Указания к выполнению работы.

6.1. Общие положения

6.1.1 . Объектно-ориентированный подход в программировании предполагает представление решаемой задачи в виде взаимодействия множества объектов, каждый из которых является экземпляром определенного класса (типа), а классы образуют иерархию наследования. Объект обладает определёнными свойствами (данными) и поведением (методами), причём набор свойств и методов является общим для всех объектов одного класса. Иногда говорят, что данные (свойства) объекта определяют его внутреннее состояние, а поведение объекта (т.е. работа методов) зависит от этого состояния и может его изменять.

Разработку программ с использованием объектно-ориентированного подхода называют объектно-ориентированным программированием (ООП), а языки программирования, поддерживающие ООП – объектно-ориентированными языками.

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

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

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

6.1.2 . Во всех объектно-ориентированных языках программирования реализованы следующие основные механизмы ООП:

инкапсуляция; наследование; полиморфизм.

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

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

Page 22: Лабораторная работа № 1.dpl61.narod.ru/files/TP/Lab_TP.pdf · 2017-02-19 · – 3 – 6.2. К пункту 4.2 Основная функция приложения

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

6.2. Краткое описание объектно-ориентированных возможностей языка C++

6.2.1 . В языке C++ для реализации объектно-ориентированного подхода существуют специальные пользовательские типы – так называемые классы, объединяющие в себе данные (поля) и функции для их обработки (методы). Описание класса в самом общем виде выглядит так:

class <Имя класса> [:<Имя класса-предка>] { private: // Объявление частных членов класса protected: // Объявление защищённых членов класса public: // Объявление общедоступных членов класса };

Имя назначается классу в соответствии с теми же правилами, что и переменным, типам и т.п. Имя класса используется при описании объектов этого класса. Если класс является потомком другого класса, то после имени указывается имя класса-предка, поля и методы которого наследует описываемый класс. В некоторых системах программирования, например, в Microsoft Visual Studio, принято начинать имена классов с буквы «C» (от слова Class), чтобы отличать их от других имён (это правило является не обязательным, но весьма полезным).

Членами класса являются его поля и методы. Ключевые слова private, protected и public определяют уровень доступа к различным членам класса.

Члены, объявленные в разделе private (частные) доступны только для методов данного класса. Внешний доступ к ним, а также доступ из методов классов-потомков, запрещён.

Члены, объявленные в разделе protected, (защищённые) доступны для методов данного класса и его потомков. Внешний доступ к ним запрещён.

Члены, объявленные в разделе public, являются общедоступными (т.е. обращение к ним возможно из любого метода или функции программы).

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

Поля объявляются внутри класса подобно обычным переменным, а методы – подобно функциям, например:

class CMyClass { protected: // описания полей int number, count; // поля целого типа double Value; // поле вещественного типа public: // описания методов int GetCount(); // получить значение Count void SetCount(int data); // задать значение Count };

Page 23: Лабораторная работа № 1.dpl61.narod.ru/files/TP/Lab_TP.pdf · 2017-02-19 · – 3 – 6.2. К пункту 4.2 Основная функция приложения

– 23 – И поля, и методы можно объявлять в любом разделе: private, protected, public – это зависит лишь от того, какой уровень доступа к члену класса хочет предоставить разработчик. Обычно поля объявляют в разделах private и protected, а доступ к ним обеспечивают с помощью методов, объявленных в разделе public. Это не обязательное правило, а лишь рекомендация, позволяющая во многих случаях защитить данные от несогласованного внешнего изменения и сохранить их целостность.

6.2.2. Поля класса ни в каком дополнительном описании не нуждаются. Для методов же одного объявления недостаточно: их нужно описать (определить), т.е. задать их внутреннюю реализацию. Для простых методов это можно сделать прямо в описании класса, например:

class CMyClass { protected: int count; // поле целого типа public: // описания методов int GetCount() {return count;} // получить значение Count void SetCount(int data) {count = data;} // задать значение Count };

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

class CMyClass { protected: int count; // поле целого типа public: // описания методов int GetCount(); // Объявление метода GetCount void SetCount(int data); // Объявление метода SetCount }; ... // Описание метода GetCount int CMyClass::GetCount() { // Какие-то сложные действия по получению значения Count ... }; // Описание метода SetCount void CMyClass::SetCount(int data) { // Какие-то сложные действия по установке значения Count ... }

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

6.2.3 . После того, как класс описан, его можно использовать для описания объектов соответствующего типа:

CMyClass A,B,C; // описываем три объекта (A,B и C) типа CMyClass

Page 24: Лабораторная работа № 1.dpl61.narod.ru/files/TP/Lab_TP.pdf · 2017-02-19 · – 3 – 6.2. К пункту 4.2 Основная функция приложения

– 24 – Не следует путать описание класса с описанием объекта. Класс – это тип, а объект – это конкретный экземпляр данного типа. Класс описывает свойства (поля) и поведение (методы), присущие всем экземплярам такого типа, в то время как каждый отдельный экземпляр может иметь свои уникальные значения этих свойств и поведение, учитывающее свойства конкретного объекта. Например, автомобиль «ВАЗ 2101» – это тип (класс), а автомобиль «ВАЗ 2101» Иванова или автомобиль «ВАЗ 2101» Петрова – это экземпляры типа «ВАЗ 2101» (объекты). Все автомобили типа «ВАЗ 2101» имеют свойства «Цвет» и «Год выпуска», но у автомобиля Иванова (объект №1) цвет синий и год 1970, а у автомобиля Петрова (объект №2) цвет белый и год 1972.

Обращение к полям и методам объекта выполняется аналогично тому, как это делается для структур: сначала указывается имя объекта, а затем через точку – имя поля или метода:

CMyClass A, B; // описываем объекты типа CMyClass A.SetCount(28); // вызываем метод SetCount объекта A B.SetCount(15); // вызываем метод SetCount объекта B int c1 = A.GetCount(); // получаем значение Count объекта A (28) int c2 = B.GetCount(); // получаем значение Count объекта B (15) A.count = 10; // присваиваем полю count объекта A значение 10 // !!! объяснить, почему в данном случае так нельзя

Если имеется указатель на какой-либо объект, то для обращения к полям и методам этого объекта используют указатель на член класса «–>» (минус, угловая скобка):

CMyClass A; // описываем объект типа CMyClass CMyClass *pA; // описываем указатель pА на объект типа CMyClass pA = &A; // присваиваем указателю адрес объекта A pA->SetCount(12); int z = pa->GetCount();

6.3. К пункту 4.1

Создание проекта подробно описано в ЛР №1 п. 6.1

6.4. К пункту 4.2

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

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

Center – координаты центра; Radius1, Width1, Color1– радиус, толщина и цвет линии для окружности 1; Radius2, Width2, Color2– радиус, толщина и цвет линии для окружности 2;

Поскольку объект должен отображаться в окне, добавим ещё одно поле: дескриптор этого окна hwnd.

Далее продумаем поведение разрабатываемых объектов. Во-первых, наши объекты должны уметь рисовать себя. Контекст устройства для рисования заранее неизвестен, поэтому он будет передаваться в виде параметра при каждом вызове метода «Рисовать».

Page 25: Лабораторная работа № 1.dpl61.narod.ru/files/TP/Lab_TP.pdf · 2017-02-19 · – 3 – 6.2. К пункту 4.2 Основная функция приложения

– 25 – Во-вторых, очевидно, что изменение любого из свойств объекта приведёт к изменению его внешнего вида. В связи с этим непосредственный доступ к свойствам следует запретить, а для их изменения описать методы, которые позволят присваивать свойствам новые значения и затем автоматически перерисовывать объект.

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

6.4.2. Чтобы добавить к существующему проекту Dev C++ новый класс, выберите в меню пункт «File \ New \ Class…». В результате откроется окно, показанное на рис. 3.1. В поле «Name» введите имя нового класса, например, CGraphicObject. В полях «Source filename» и «Header filename» автоматически появятся полные имена файлов, содержащих заготовки описания и заголовка класса (проверьте, чтобы эти файлы создавались в вашей рабочей папке). Флажок «Add to current project» (добавить в текущий проект) должен быть установлен, остальные элементы окна должны быть пустыми (как на рисунке). Убедившись, что всё выполнено правильно, нажмите кнопку «Create».

После этого к проекту будут добавлены два новых файла: «CGraphicObject.h» и «CGraphicObject.cpp». Файл с расширением «.h» называется заголовочным и содержит заготовку описания класса. В файл исходного текста с расширением «.cpp» мы в процессе работы добавим описания методов класса. Обратите внимание, что в первой строке файла «CGraphicObject.cpp» задана директива #include "CGraphicObject.h"

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

Сохраните проект, выбрав в меню «File \ Save All» или нажав соответствующую кнопку на панели инструментов.

6.4.3. С учетом сведений, изложенных в п. 6.4.1, создадим описание нового класса в файле «CGraphicObject.h»:

Рис. 3.1. Создание нового класса

Page 26: Лабораторная работа № 1.dpl61.narod.ru/files/TP/Lab_TP.pdf · 2017-02-19 · – 3 – 6.2. К пункту 4.2 Основная функция приложения

– 26 – class CGraphicObject { private: // Дескриптор окна, в котором отображается объект HWND hwnd; // Координаты центра окружностей POINT Center; // Цвета окружностей unsigned Color[2]; // массив из двух элементов // Толщина линий окружностей unsigned Width[2]; // Радиусы окружностей unsigned Radius[2]; public: // Метод, отображающий объект в заданном контексте устройства void Draw(HDC hdc); // Метод, задающий координаты центра void SetCenter(int x, int y); // Метод, задающий радиус R окружности с номером Index (0 или 1) void SetRadius(unsigned Index, unsigned R); // Метод, задающий толщину W окружности с номером Index (0 или 1) void SetWidth(unsigned Index, unsigned W); // Метод, задающий цвет C окружности с номером Index (0 или 1) void SetColor(unsigned Index, unsigned C); // Метод, задающий дескриптор окна void SetHWND(HWND handle); // Метод, смещающий центр на заданную величину void MoveBy(int dx, int dy); }

Чтобы в описании класса можно было использовать типы Windows API POINT, HDC, HWND и т.п., к файлу «CGraphicObject.h» нужно подключить заголовочный файл «Windows.h»: #ifndef CGRAPHICOBJECT_H #define CGRAPHICOBJECT_H #include <windows.h> // подключаем описания Windows API class CGraphicObject { ...

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

6.4.4 . После того, как описание класса создано, опишем его методы в файле «CGraphicObject.cpp».

Метод Draw можно реализовать по аналогии с функцией ClientDraw(), рассмотренной в ЛР №1 и №2 (подробное описание приведено в соответствующих методических указаниях). Отличие состоит лишь в том, что контекст устройства hdc мы получим не в методе Draw, а в другом месте (для чего это сделано, будет показано позже). void CGraphicObject::Draw(HDC hdc) { LOGBRUSH lb; lb.lbStyle = BS_SOLID; lb.lbHatch = 0; // Цвет первой окружности lb.lbColor = Color[0];

Page 27: Лабораторная работа № 1.dpl61.narod.ru/files/TP/Lab_TP.pdf · 2017-02-19 · – 3 – 6.2. К пункту 4.2 Основная функция приложения

– 27 – // Перо для первой окружности HGDIOBJ hPen1 = ExtCreatePen( PS_GEOMETRIC|PS_SOLID, Width[0] ,&lb,0,NULL); // Цвет второй окружности lb.lbColor = Color[1]; // Перо для второй окружности HGDIOBJ hPen2 = ExtCreatePen( PS_GEOMETRIC|PS_SOLID, Width[1] ,&lb,0,NULL); // Кисть для заливки окружностей (нет заливки) SelectObject(hdc, GetStockObject(NULL_BRUSH)); // Выбираем первое перо,рисуем первую окружность (координаты вершин // описанного квадрата вычисляем через координаты центра и радиус) HGDIOBJ hPenOld = SelectObject(hdc, hPen1); Ellipse(hdc, Center.x-Radius[0], Center.y-Radius[0], Center.x+Radius[0], Center.y+Radius[0]); // Выбираем второе перо и рисуем вторую окружность SelectObject(hdc, hPen2); Ellipse(hdc, Center.x-Radius[1], Center.y-Radius[1], Center.x+Radius[1], Center.y+Radius[1]); // Восстанавливаем перо по умолчанию и удаляем ненужные перья SelectObject(hdc, hPenOld); DeleteObject(hPen1); DeleteObject(hPen2); }

Метод MoveBy должен смещать объект относительно текущего положения, т.е. изменять поля Center.x и Center.y на заданную величину. Его описание может выглядеть так: void CGraphicObject::MoveBy(int dx, int dy) { // Смещаем координаты центра Center.x += dx; Center.y += dy; // Запрашиваем перерисовку клиентской области окна InvalidateRect(hwnd, NULL, 1); }

Методы SetCenter, SetRadius, SetWidth, SetColor и SetHWND по сути мало чем отличаются друг от друга: все они должны присвоить значение параметра (или нескольких параметров) соответствующему полю (полям) объекта, после чего проинформировать Windows о необходимости перерисовать клиентскую область окна: void CGraphicObject::SetCenter(int x, int y) { // Задаём значения координат поля Center Center.x = x; Center.y = y; // Запрашиваем перерисовку клиентской области окна InvalidateRect(hwnd, NULL, 1); }

Методы, получающие в качестве одного из параметров номер окружности Index, дополнительно должны проверить его корректность (окружности всего две, поэтому допустимы только номера 0 и 1). Параметр Index имеет тип unsigned, следовательно, не может принимать отрицательные значения. Поэтому достаточно проверить, чтобы его величина была меньше двух.

Page 28: Лабораторная работа № 1.dpl61.narod.ru/files/TP/Lab_TP.pdf · 2017-02-19 · – 3 – 6.2. К пункту 4.2 Основная функция приложения

– 28 – void CGraphicObject::SetRadius(unsigned Index, unsigned R) { // Номер окружности не может быть больше 1 (только 0 или 1) if (Index<2) { // Если номер правильный, то задаём радиус соотв.окружности Radius[Index] = R; // и запрашиваем перерисовку клиентской области окна InvalidateRect(hwnd, NULL, 1); } }

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

6.5. К пункту 4.3

6.5.1. После того, как описан класс CGraphicObject и все его методы, можно создавать объекты (экземпляры) этого класса.

Опишем в начале модуля «main.cpp» глобальную переменную типа CGraphicObject: #include <windows.h> CGraphicObject A;

Если попытаться скомпилировать программу, то будет выдана ошибка: «CGraphicObject does not name a type». Это происходит из-за того, что в модуле «main.cpp» ничего неизвестно о классе CGraphicObject. Чтобы обеспечить возможность использования этого класса, к модулю «main.cpp» нужно подключить заголовочный файл «CGraphicObject.h»: #include <windows.h> #include "CGraphicObject.h" CGraphicObject A;

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

6.5.2. Несмотря на то, что объект A уже создан, значения его полей не определены. Это может вызвать серьёзные проблемы при попытке использования объекта (возвращаясь к приведённому ранее примеру с автомобилями это всё равно, что попытаться поехать на машине, у которой неизвестно что залито в бак, установлены неизвестно какие колёса и неизвестно чем и как они накачаны). C++ предоставляет средства как автоматической, так и явной инициализации создаваемых объектов, но мы рассмотрим их позже, а пока следует помнить, что новый объект типа CGraphicObject нельзя использовать до тех пор, пока не будут заданы значения всех его полей. Задайте эти значения перед основным циклом обработки сообщений, когда главное окно приложения уже создано: A.SetHWND(hwnd); // дескриптор окна A.SetCenter(150, 150); // коорднаты центра A.SetColor(0, RGB(255,0,0)); // цвет окружности 1 A.SetColor(1, RGB(0,255,0)); // цвет окружности 2 A.SetWidth(0,5); // ширина линии окружности 1 A.SetWidth(1,10); // ширина линии окружности 2 A.Radius[0] = 70; // радиус окружности 1 A.Radius[1] = 50; // радиус окружности 2 while(GetMessage(&msg, NULL, 0, 0) > 0) { ...

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

Page 29: Лабораторная работа № 1.dpl61.narod.ru/files/TP/Lab_TP.pdf · 2017-02-19 · – 3 – 6.2. К пункту 4.2 Основная функция приложения

– 29 – способ задания радиуса окружностей. Скомпилируйте программу ещё раз и убедитесь в отсутствии синтаксических ошибок.

6.5.3 . После того, как значения полей объекта заданы, его можно использовать. На первом этапе добьёмся того, чтобы объект отображался в окне. В ЛР №1 была подробно описана реализация реакции программы на сообщения WM_PAINT (см. п.п. 6.5.1, 6.5.2), в этой работе следует повторить указанные пункты, добавив в модуль «main.cpp» функцию ClientDraw и обработку сообщения WM_PAINT.

Однако реализация функции ClientDraw будет отличаться от рассмотренной ранее. Если раньше именно эта функция отвечала за формирование изображения в окне, то теперь объект умеет рисовать себя сам. Следовательно, задача функции – лишь подготовить графический контекст для использования, предоставить его объекту (или объектам), а затем освободить ресурсы: void ClientDraw(HWND hwnd, WPARAM wParam, LPARAM lParam) { PAINTSTRUCT ps; HDC hdc = BeginPaint(hwnd, &ps); // получаем дескриптор контекста A.Draw(hdc); // "просим" объект A нарисовать себя EndPaint(hwnd, &ps); // освобождаем графические ресурсы }

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

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

Использование готового класса CGraphicObject существенно упрощает задачу. Создадим ещё один экземпляр этого класса: CGraphicObject A, B;

Зададим значения его полей (после аналогичных действий для объекта A): B.SetHWND(hwnd); // дескриптор окна B.SetCenter(300, 200); // коорднаты центра B.SetColor(0, RGB(0,0,255)); // цвет окружности 1 B.SetColor(1, RGB(200,255,0)); // цвет окружности 2 ... // далее – по аналогии с объектом А

Функцию ClientDraw придётся дополнить лишь вызовом метода Draw для объекта B: void ClientDraw(HWND hwnd, WPARAM wParam, LPARAM lParam) { PAINTSTRUCT ps; HDC hdc = BeginPaint(hwnd, &ps); // получаем дескриптор контекста A.Draw(hdc); // "просим" объект A нарисовать себя B.Draw(hdc); // "просим" объект B нарисовать себя EndPaint(hwnd, &ps); // освобождаем графические ресурсы }

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

Page 30: Лабораторная работа № 1.dpl61.narod.ru/files/TP/Lab_TP.pdf · 2017-02-19 · – 3 – 6.2. К пункту 4.2 Основная функция приложения

– 30 – Из рассмотренного примера видно, что, проделав достаточно сложную подготовительную работу (описание класса CGraphicObject и разработка его методов), мы получили возможность отображать в окне новые объекты простым и понятным способом: описали объект, задали значения его полей, вызвали метод рисования Draw. Детали выполнения перечисленных действий, скрытые в описании методов класса CGraphicObject, на данном этапе нас не интересуют и на них можно не отвлекаться.

6.6. К пункту 4.4

6.6.1 . Одним из существенных преимуществ ООП, обусловленных инкапсуляцией, является обеспечение целостности данных (полей объекта). Целостность достигается за счёт того, что прямой доступ к полям блокируется, а изменение их значений осуществляется с помощью методов. Разработчик класса, зная особенности его работы, проектирует методы так, чтобы они выполняли необходимые проверки, изменения связанных полей и т.п.

Однако, как отмечено в п. 6.5.2, при создании объекта возникает ситуация, когда значения его полей не определены. Проведём аналогию с переменными: локальные переменные при создании имеют произвольные (случайные) значения. Для некоторых объектов это не играет существенной роли, но бывают ситуации, когда такое поведение недопустимо. Предположим, имеется объект «Щенок» с двумя полями: «Рост» и «Вес». Очевидно, что значения этих полей могут быть случайными, но они, во-первых, должны находиться в определённом диапазоне, а во-вторых, должны быть согласованы друг с другом (вряд ли может быть щенок ростом 300 мм с весом 100 г). Если добавить третье поле «Возраст», то взаимосвязь полей ещё более усложнится.

Простейшее решение этой проблемы показано в п. 6.5.2: мы не использовали объект до тех пор, пока не инициализировали все его поля. Но такое решение нельзя считать приемлемым. Во-первых, неопытный разработчик может просто забыть о каком-то поле, в результате чего оно останется с неопределённым значением. Во-вторых, в процессе модернизации программы автор класса может добавить новое поле, что приведёт к необходимости анализа и изменения множества модулей, использующих этот класс. В-третьих, сразу после создания и в ходе инициализации объект может находиться в недопустимом состоянии, чего следует избегать. Например, сразу после создания объект «Щенок» будет иметь возраст, рост и вес, равные нулю. Если для возраста это нормально, то для роста и веса недопустимо. Если далее мы присвоим полю «Рост» значение 50 мм, то получится, что плотность щенка равна нулю, поскольку вес остался нулевым. Если начать инициализацию с веса, то плотность какое-то время будет равна бесконечности (пока мы не зададим рост).

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

6.6.2. Конструктор класса – это метод, который автоматически вызывается при создании объекта и выделении памяти под него. От обычного метода конструктор отличается тем, что:

имя конструктора совпадает с именем класса; при описании конструктора не указывается тип возвращаемого значения.

В классе может быть несколько конструкторов. В соответствии с правилами языка С++ все они имеют одно имя, совпадающее с именем класса. Компилятор выбирает тот конструктор, который в зависимости от ситуации, в которой происходит создание объекта, соответствует ей по количеству и типам параметров. Естественным ограничением является то, что в классе не может быть двух конструкторов с одинаковым набором параметров.

Page 31: Лабораторная работа № 1.dpl61.narod.ru/files/TP/Lab_TP.pdf · 2017-02-19 · – 3 – 6.2. К пункту 4.2 Основная функция приложения

– 31 – 6.6.3. Самая простая разновидность конструктора – это конструктор без параметров, или конструктор умолчания. Он автоматически вызывается в тех случаях, когда при описании объекта после имени класса указывается только имя переменной: CGraphicObject A; // здесь для объекта А будет автоматически вызван // конструктор умолчания (при наличии)

Опишем такой конструктор для класса CGraphicObject. Для этого в файл «CGraphicObject.h» в раздел public описания класса нужно добавить объявление метода: class CGraphicObject { ... public: // Конструктор умолчания CGraphicObject(); ...

В файл «CGraphicObject.cpp» добавим описание конструктора: CGraphicObject::CGraphicObject() { hwnd = NULL; Center.x = 150; Center.y = 100; Color[0] = RGB(255,0,0); Color[1] = RGB(0,255,0); Width[0] = 5; Width[1] = 10; Radius[0] = 80; Radius[1] = 50; }

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

6.6.4 . Проверим работу конструктора умолчания. Удалите в модуле «main.cpp» все вызовы методов, задающих значения свойств объектов A и B (A.SetColor(), B.SetWidth() и т.п.), оставив только вызовы A.SetHWND() и B.SetHWND(). Параметр hwnd придётся задавать явно, поскольку на момент создания объектов его значение ещё неизвестно, а создание объектов A и B после создания окна приведёт к ненужному усложнению программы.

Скомпилируйте и запустите программу – на экране появится изображение двух окружностей, т.е. одного объекта. Закройте программу, поставьте точку останова внутри конструктора и запустите отладку. В ходе отладки можно увидеть, что конструктор автоматически вызывается два раза: один раз для объекта A, второй раз – для объекта B. Наличие в окне изображения лишь одного объекта обусловлено тем, что второй объект имеет те же размеры, что и первый, и рисуется в точности поверх него.

Измените координаты центра объекта B (после вызова метода B.SetHWND): B.SetCenter(200,200);

Скомпилируйте и запустите программу: теперь в окне видны оба объекта.

Наличие конструктора умолчания позволяет явным образом задавать для объекта лишь те значения свойств, которые должны отличаться от начальных. Попробуйте изменить в модуле «main.cpp» другие отдельные свойства объектов A и B, наблюдая результат в окне приложения.

Page 32: Лабораторная работа № 1.dpl61.narod.ru/files/TP/Lab_TP.pdf · 2017-02-19 · – 3 – 6.2. К пункту 4.2 Основная функция приложения

– 32 – 6.6.5 . Кроме конструктора умолчания в классе может быть описан конструктор с параметрами. Такие конструкторы используются, если для создания объекта требуется некая внешняя информация. Опишем для класса CGraphicObject ещё один конструктор, на этот раз с параметрами, позволяющими задать координаты центра объекта сразу при его создании. В файл «CGraphicObject.h» добавим текст: class CGraphicObject { ... public: // Конструктор умолчания CGraphicObject(); // Конструктор c параметрами CGraphicObject(int x, int y); ...

В файл «CGraphicObject.cpp» добавим описание второго конструктора: CGraphicObject::CGraphicObject(int x, int y) { hwnd = NULL; Center.x = x; Center.y = y; Color[0] = RGB(255,0,0); Color[1] = RGB(0,255,0); Width[0] = 5; Width[1] = 10; Radius[0] = 80; Radius[1] = 50; }

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

Чтобы использовать конструктор с параметрами, нужно при описании объекта задать в скобках фактические значения этих параметров. Измените описание объекта B в модуле «main.cpp»: CGraphicObject A, B(200,250);

Затем вновь удалите из текста этого модуля вызовы методов, задающих значения свойств объектов A и B, добавленные при выполнении предыдущего пункта (кроме A.SetHWND и B.SetHWND). Запустите программу и убедитесь, что объекты создаются с разными координатами центров. Поставьте точки останова в обоих конструкторах, запустите отладку и наблюдайте за тем, какие конструкторы и сколько раз вызываются автоматически. Объясните результат. Измените описание объекта A, задав для него координаты центра подобно объекту B. Вновь в режиме отладки наблюдайте вызовы конструкторов. Объясните результат. Сохраните изображение основного окна программы для отчёта. Описание и объяснение результатов отладки также необходимо привести в отчёте.

6.6.6. Кроме конструкторов для класса может быть описан ещё один метод, вызываемый автоматически – деструктор. Если задача конструкторов состоит в корректном создании объекта, то деструктор решает обратную задачу – корректное уничтожение объекта. Соответственно, деструктор автоматически вызывается при уничтожении объекта. В отличие от конструкторов, в классе может быть только один деструктор, у него нет параметров и возвращаемого значения.

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

Page 33: Лабораторная работа № 1.dpl61.narod.ru/files/TP/Lab_TP.pdf · 2017-02-19 · – 3 – 6.2. К пункту 4.2 Основная функция приложения

– 33 – занятые объектом. Кроме того, он может, например, уведомлять другие объекты о прекращении существования данного объекта.

Для объектов класса CGraphicObject, созданных в данной работе, какие-либо специальные действия при уничтожении не требуются, поскольку эти объекты не занимают дополнительных ресурсов. Тем не менее, для изучения способа описания деструктора опишем пустой деструктор, не выполняющий никаких действий. Деструктор, как и конструктор, имеет имя класса, но перед ним указывается символ «~».

Добавьте в файл «CGraphicObject.h» следующий текст: class CGraphicObject { ... public: ... // Деструктор ~CGraphicObject(); ...

В файл «CGraphicObject.cpp» добавьте описание деструктора, не выполняющего никаких действий: CGraphicObject::~CGraphicObject() { // Здесь могут быть действия по корректному уничтожению объекта // (например, освобождение занятых ресурсов) }

Наблюдать вызов деструктора в процессе отладки в данном случае не удастся, т.к. компилятор в процессе оптимизации машинного кода исключает «пустые» методы, а также методы, выполняющие ни на что не влияющие действия.

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

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

6.7. К пункту 4.5

Для выполнения индивидуального задания добавьте к проекту новый класс, задав ему какое-либо уникальное имя, например, «CMyObject» (см. п. 6.4.2).

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

После этого в созданных файлах «CMyObject.h» и «CMyObject.cpp» опишите класс CMyObject и его методы. При разработке метода Draw используйте в качестве образца функцию ClientDraw, полученную при выполнении индивидуального задания ЛР №2. Для нового класса опишите конструктор умолчания и конструктор с параметрами (набор необходимых параметров определите самостоятельно). Деструктор описывать не нужно. Скомпилируйте программу и добейтесь отсутствия ошибок.

Опишите в модуле «main.cpp» объект типа CMyObject, например, так:

Page 34: Лабораторная работа № 1.dpl61.narod.ru/files/TP/Lab_TP.pdf · 2017-02-19 · – 3 – 6.2. К пункту 4.2 Основная функция приложения

– 34 – CGraphicObject A,B(200,200); CMyObject С;

По аналогии с объектами A и B добавьте для него вызовы метода, задающего дескриптор окна (SetHWND), и метода, отображающего объект в окне (Draw). Скомпилируйте и запустите программу, убедитесь, что в окне отображаются три объекта: два типа CGraphicObject и один типа CMyObject.

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

Опишите второй объект типа CMyObject с использованием конструктора с параметрами. Проверьте работу программы. Сохраните изображение основного окна программы для отчёта.

7. Содержание отчёта.

Общие положения.

См. п. 7 лабораторной работы №1.

Отчёт по работе должен содержать:

Название работы. Цель работы. Номер варианта и соответствующее ему индивидуальное задание (с расшифровкой

обозначений цветов и типов линий). Копирование таблиц п. 5 в отчёт полностью не допускается.

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

Для отдельных пунктов программы работы в отчёте необходимо привести следующее.

Для пункта 4.2

Тексты файлов «CGraphicObject.h» и «CGraphicObject.cpp», полученные при выполнении этого пункта (без конструкторов и деструктора) с необходимыми комментариями.

Для пункта 4.3

Текст модуля «main.cpp» с пояснением основных операций в виде комментариев. Изображение основного окна разработанной программы.

Для пункта 4.4

Окончательный текст файла «CGraphicObject.h», полученный при выполнении этого пункта (с объявлением конструкторов и деструктора).

Описание (тексты) конструкторов и деструктора (полностью приводить текст файла «CGraphicObject.h» не нужно).

Описание изменений, внесённых в файл «main.cpp» в связи с использованием конструкторов (полностью приводить текст файла «main.cpp» не следует).

Изображение основного окна разработанной программы. Описание и объяснение результатов отладки программы в соответствии с п. 6.6.5.

Для пункта 4.5

Тексты файлов «CMyObject.h» и «CMyObject.cpp» с необходимыми комментариями. Окончательный текст модуля «main.cpp» с необходимыми комментариями.

Page 35: Лабораторная работа № 1.dpl61.narod.ru/files/TP/Lab_TP.pdf · 2017-02-19 · – 3 – 6.2. К пункту 4.2 Основная функция приложения

– 35 – Изображение основного окна разработанной программы, соответствующее

окончательному результату выполнения индивидуального задания.

Заключительная часть

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

8. Контрольные вопросы.

8.1. В чём суть объектно-ориентированного подхода в программировании?

8.2. Что такое наследование, инкапсуляция?

8.3. Что такое члены класса? Какие разновидности членов класса вы знаете?

8.4. Как задаётся уровень доступа к членам класса при его описании? Какие уровни доступа вам известны? Охарактеризуйте каждый из них.

8.5. Чем отличается класс от объекта?

8.6. Почему непосредственное обращение к полям класса часто является нежелательным? Как исключить возможность непосредственного обращения к полям?

8.7. В чём преимущество использования методов для изменения полей по сравнению с непосредственным обращением к полям?

8.8. Для чего нужны конструктор и деструктор? Как и когда они вызываются? Какие разновидности конструкторов вам известны?

8.9. Как при наличии нескольких конструкторов компилятор выбирает из них наиболее подходящий?

Page 36: Лабораторная работа № 1.dpl61.narod.ru/files/TP/Lab_TP.pdf · 2017-02-19 · – 3 – 6.2. К пункту 4.2 Основная функция приложения

– 36 –

Лабораторная работа № 4.

Наследование. Создание иерархии классов

1. Цель работы

Ознакомиться с понятием «наследование» в объектно-ориентированном программировании. Используя наследование, создать иерархию классов, представляющих различные графические объекты.

2. Описание лабораторного стенда

В качестве лабораторного стенда используется IBM-совместимый персональный компьютер с установленной интегрированной средой разработки программ Dev-C++ 5.11.

3. Подготовка к выполнению работы

3.1. Подготовить работоспособный проект, полученный в результате выполнения лабораторной работы №3.

3.2. Подготовить электронный носитель для сохранения полученных результатов (файлов).

4. Программа работы

4.1. На рабочем компьютере создать папку для сохранения результатов работы. Скопировать в неё проект Dev-C++ с результатами выполнения индивидуального задания ЛР №3. Скомпилировать проект, убедиться в его работоспособности.

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

4.3. На основе описанного в п. 4.2 базового класса создать класс-потомок, наследующий его свойства и методы и выводящий на экран две концентрические окружности, аналогичные рассмотренным в ЛР №3, п. 4.2. Проверить работу нового класса, продемонстрировать результат преподавателю.

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

4.5. Создать два объекта на основе классов, разработанных в п.п. 4.3, 4.4, вывести их на экран. Продемонстрировать результат преподавателю.

4.6. Сохранить результаты работы (папку с проектом Dev-C++) на внешнем носителе, поскольку они потребуются для выполнения следующей работы.

4.7. Оформить отчет по работе.

5. Варианты заданий

Параметры индивидуального задания следует взять из п. 5 ЛР №1.

Page 37: Лабораторная работа № 1.dpl61.narod.ru/files/TP/Lab_TP.pdf · 2017-02-19 · – 3 – 6.2. К пункту 4.2 Основная функция приложения

– 37 –

6. Указания к выполнению работы.

6.1. Общие сведения

6.1.1 . Для описания объектов и явлений окружающего мира очень часто используют классификацию, то есть разделение их на разновидности согласно каким-либо важным признакам. При этом выделяют некие особенности, общие для совокупности объектов, и на основании этого сходства объединяют объекты в группу (класс). Например, в класс «средства передвижения» можно объединить все объекты, предназначенные для перемещения чего-либо на значительные расстояния. Существуют свойства, имеющиеся у всех объектов этого класса, например, максимальная скорость, грузоподъёмность, запас хода. Аналогично можно выделить общее поведение всех средств передвижения: они могут перемещаться, расходовать энергию (топливо), пополнять запас энергии, принимать груз, выдавать груз.

Для более детального описания объектов классификация уточняется путём выделения внутри класса обособленных групп (подклассов). Каждому из подклассов присущи все особенности класса, но имеются некоторые дополнительные признаки, позволяющие отличить один подкласс от другого. Например, средства передвижения можно разделить на воздушные, сухопутные и водные, сухопутные – на автомобили и поезда, автомобили – на легковые и грузовые и т.д. На каждом новом уровне классификации описание объектов становится всё более точным за счёт добавления дополнительных отличительных признаков.

Использование классификации позволяет существенно упростить описание объектов. Относя объект или группу объектов к какому-либо известному классу мы тем самым наделяем их всеми особенностями этого класса. Например, утверждая, что «XYZ» – это легковой автомобиль, мы избавляемся от необходимости перечислять все его существенные свойства, позволяющие понять, что же это такое. Если известные классы не описывают объект в полной мере, всегда есть возможность дополнить классификацию ещё одним классом, который унаследует все свойства более общего класса, добавив к ним необходимые особенности. Например, в рассмотренную выше классификацию средств передвижения мы можем добавить класс «амфибии», описав его как «автомобили, способные плавать». Новый класс наследует все особенности родительского класса «автомобили» и добавляет к ним новое свойство – способность плавать.

6.1.2. Аналогичный подход используется в ООП. В процессе представления решаемой задачи в виде совокупности взаимодействующих объектов нередко возникают ситуации, когда создаваемые объекты в той или иной степени похожи друг на друга (т.е. обладают сходными свойствами и поведением). Степень сходства у различных объектов может быть разной. Совершенно одинаковые объекты (обладающие идентичным набором свойств и поведением) будут описываться одним классом, но для отличающихся объектов потребуется создать несколько классов, которые, как отмечено выше, могут быть в чём-то похожи друг на друга. Как следствие, возникает желание вынести описание схожих свойств в отдельную структуру, а затем создавать на её основе различные классы, дополняющие имеющееся общее описание лишь некоторыми специфическими особенностями.

Рассмотрим, например, элементы управления приложения Windows. Все они имеют ряд общих свойств: изображаются на экране, могут воспринимать действия пользователя (нажатия кнопок, движение мыши), в ответ на действия пользователя инициируют выполнение некоторых операций (команд). Следовательно, имеет смысл описать все перечисленные особенности в отдельном классе – назовём его CControl.

Класс CControl, описывая общие свойства всех элементов управления, не даёт ответ на вопросы: как выглядит элемент управления, как он взаимодействует с пользователем, как он должен реагировать на действия пользователя. Чтобы уточнить эти детали, разделим всю совокупность элементов управления на более мелкие группы – предположим, это будут группы «Меню» и «Кнопка». Опишем классы, соответствующие этим группам:

Page 38: Лабораторная работа № 1.dpl61.narod.ru/files/TP/Lab_TP.pdf · 2017-02-19 · – 3 – 6.2. К пункту 4.2 Основная функция приложения

– 38 – CMenu и CButton. Очевидно, что новые классы должны обладать всеми свойствами более общего класса CControl, дополняя его специфическими свойствами. Для класса CMenu это может быть, например, список пунктов меню, а для класса CButton – изображение или текст кнопки. Продолжая анализ поведения элементов управления можно сделать вывод, что класс CButton не полностью описывает все возможные варианты кнопок, поскольку они могут быть, например, с фиксацией или без фиксации. Для учёта этих особенностей создадим на основе CButton класс CCheckButton (кнопка с фиксацией), обладающий дополнительными свойствами – признаком нажатого состояния, способностью переключать этот признак в ответ на действия пользователя и способностью менять свой внешний вид в зависимости от этого признака.

В результате выполненного анализа сформировалась иерархия классов, в основе которой находится наиболее общий класс CControl. От него образованы дочерние (производные) классы CMenu и CButton, наследующие все его свойства и дополняющие их. Далее на базе CButton создан ещё один дочерний класс – CCheckButton. По отношению к CMenu и CButton класс CControl является базовым (родительским). В свою очередь, класс CButton является родительским для CCheckButton.

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

6.2. К пункту 4.2

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

Независимо от внешнего вида все эти объекты имеют общие свойства – координаты базовой точки и дескриптор окна, а также общее поведение – способность перемещаться и изображать себя в окне.

Добавим к существующему проекту новый класс CVisualObject – предок для всех графических объектов. Создание класса и добавление его к проекту подробно описано в ЛР №3 п.6.4.2. Описание класса может выглядеть следующим образом: class CVisualObject { protected: HWND hwnd; // дескриптор окна POINT base; // базовая точка public: // Конструктор по умолчанию CVisualObject(); // Метод, задающий координаты базовой точки void SetBase(int x, int y); // Метод, смещающий объект на заданную величину void MoveBy(int dx, int dy); // Метод, задающий дескриптор окна // (метод простой, описываем его здесь же) void SetHWND(HWND handle) {hwnd = handle;} };

Чтобы при описании класса CVisualObject можно было использовать типы и функции Windows API, к файлу «CVisualObject.h» следует подключить заголовочный файл «windows.h»:

Page 39: Лабораторная работа № 1.dpl61.narod.ru/files/TP/Lab_TP.pdf · 2017-02-19 · – 3 – 6.2. К пункту 4.2 Основная функция приложения

– 39 – #ifndef CGRAPHICOBJECT_H #define CGRAPHICOBJECT_H #include <windows.h> class CGraphicObject { ...

Описание конструктора в файле «CVisualObject.cpp» может выглядеть так: CVisualObject::CVisualObject() { hwnd = NULL; base.x = 200; // координаты базовой точки по умолчанию base.y = 200; }

Подробнее о конструкторах сказано в ЛР №3, п.6.6. Реализация метода SetHWND приведена непосредственно в описании класса в файле «CVisualObject.h». Методы SetBase и MoveBy аналогичны соответствующим методам класса CGraphicObject, рассмотренного в ЛР №3 (п. 6.4.4): void CVisualObject::MoveBy(int dx, int dy) { base.x += dx; base.y += dy; InvalidateRect(hwnd, NULL, 1); } void CVisualObject::SetBase(int x, int y) { base.x = x; base.y = y; InvalidateRect(hwnd, NULL, 1); }

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

6.3. К пункту 4.3

6.3.1. В среде программирования Dev C++ имеются средства для упрощённого создания классов-потомков, но с целью более полного понимания элементов описания класса мы не будем пользоваться этими средствами на данном этапе.

Создадим ещё один новый класс CTwoCircles, который станет потомком CVisualObject (создание класса описано в ЛР №3 п.6.4.2). Автоматически будет сформирован файл «CTwoCircles.h» следующего содержания: #ifndef CTWOCIRCLES_H #define CTWOCIRCLES_H

Page 40: Лабораторная работа № 1.dpl61.narod.ru/files/TP/Lab_TP.pdf · 2017-02-19 · – 3 – 6.2. К пункту 4.2 Основная функция приложения

– 40 – class CTwoCircles { public: protected: }; #endif

Далее нужно указать, что класс CTwoCircles не создаётся с нуля, а является потомком CVisualObject и, соответственно, наследует его поля и методы. Для этого дополним первую строку описания: ... class CTwoCircles : public CVisualObject { ...

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

При попытке компиляции проекта среда программирования выдаст сообщение об ошибке, смысл которого сводится к тому, что в модуле CTwoCircles ничего не известно о классе CVisualObject. Для исключения этой ошибки следует перед описанием класса CTwoCircles подключить заголовочный файл «CVisualObject.h», содержащий описание класса CVisualObject: ... #include "CVisualObject.h" class CTwoCircles : public CVisualObject

Далее опишем в новом классе поля и методы, позволяющие ему изображать в окне две концентрические окружности. При этом нужно иметь в виду, что часть полей и методов, общая для всех графических объектов, уже описана в базовом классе CVisualObject, поэтому их повторное описание в классе-потомке не требуется. class CTwoCircles : public CVisualObject { protected: // Цвета окружностей unsigned Color[2]; // массив из двух элементов // Толщина линий окружностей unsigned Width[2]; // Радиусы окружностей unsigned Radius[2]; public: // Конструктор CTwoCircles(); // Метод, отображающий объект в заданном контексте void Draw(HDC hdc); // Метод, задающий радиус R окружности с номером Index (0 или 1) void SetRadius(unsigned Index, unsigned R); // Метод, задающий толщину W окружности с номером Index (0 или 1) void SetWidth(unsigned Index, unsigned W);

Page 41: Лабораторная работа № 1.dpl61.narod.ru/files/TP/Lab_TP.pdf · 2017-02-19 · – 3 – 6.2. К пункту 4.2 Основная функция приложения

– 41 – // Метод, задающий цвет C окружности с номером Index (0 или 1) void SetColor(unsigned Index, unsigned C); };

Поскольку в соответствии с заданием класс CTwoCircles должен быть аналогичен классу CGraphicObject, созданному в ЛР №3, набор полей и методов нового класса не отличается от CGraphicObject (элементы описания, за исключением унаследованных, можно скопировать из файла «CGraphicObject.h»).

Реализация методов класса CTwoCircles, объявленных в его описании, также принципиально не отличается от методов CGraphicObject. Текст этих методов следует скопировать в файл «CTwoCircles.cpp» из файла «CGraphicObject.cpp», самостоятельно выполнив необходимые изменения.

Конструктор должен быть определён явно в классе-потомке. Его можно либо полностью написать заново, либо сначала вызвать конструктор базового класса, а затем при необходимости добавить дополнительные действия. Второй способ, как правило, является более предпочтительным, поскольку автоматически учитывает все изменения в конструкторе базового класса. Описание конструктора CTwoCircles, которое следует добавить в файл «CTwoCircles.cpp», приведено далее: CTwoCircles::CTwoCircles() : CVisualObject() { base.x = 150; base.y = 100; Color[0] = RGB(255,0,0); Color[1] = RGB(0,255,0); Width[0] = 5; Width[1] = 10; Radius[0] = 80; Radius[1] = 50; }

Текст «: CVisualObject()» в первой строке означает, что прежде всего вызывается конструктор базового класса CVisualObject, а затем выполняются операторы конструктора CTwoCircles.

6.3.2. Для проверки класса CTwoCircles в модуль «main.cpp» нужно внести следующие изменения:

Подключить модуль «CTwoCircles» (убрать строку «#include "CGraphicObject.h"» и вставить вместо неё «#include "CTwoCircles.h"»).

Удалить все описания объектов класса CGraphicObject и все ссылки на них (проверить путём компиляции проекта: пока описания и ссылки имеются, будут возникать ошибки компиляции).

В начале модуля описать глобальные переменные (объекты) TC1, TC2 типа CTwoCircles.

Дополнить функцию ClientDraw текстом: void ClientDraw(HWND hwnd, WPARAM wParam, LPARAM lParam) { PAINTSTRUCT ps; HDC hdc = BeginPaint(hwnd, &ps); TC1.Draw(hdc); TC2.Draw(hdc); EndPaint(hwnd, &ps); }

Перед основным циклом обработки сообщений «while(GetMessage(...))» добавить строки, задающие значения свойств объектов TC1, TC2:

Page 42: Лабораторная работа № 1.dpl61.narod.ru/files/TP/Lab_TP.pdf · 2017-02-19 · – 3 – 6.2. К пункту 4.2 Основная функция приложения

– 42 – TC1.SetHWND(hwnd); TC2.SetHWND(hwnd); TC1.SetBase(300,300); // Далее можно задать и другие свойства (радиус, цвет и т.п.)

Смысл перечисленных действий подробно описан в ЛР №3.

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

6.4. К пункту 4.4

6.4.1 . Класс, изображающий фигуру в соответствии с индивидуальным заданием, создаётся аналогично классу CTwoCircles. Для него нужно придумать имя (например, CMyFigure), создать файлы исходного текста «CMyFigure.h» и «CMyFigure.cpp», указать класс CVIsualObject в качестве базового, а затем добавить поля и методы, описывающие свойства и поведение фигуры, (например, размер прямоугольника или эллипса, толщина и цвет линий и т.п. – как минимум два поля и два метода по выбору студента). За основу метода Draw класса CMyFigure можно взять соответствующий метод из ЛР №3, изменив его для учёта значений свойств (полей) объекта.

6.4.2. Для проверки работы нового класса измените тип переменных TC1, TC2 в модуле «main.cpp» с CTwoCircles на CMyFigure, затем скомпилируйте и выполните программу. Если всё сделано правильно, в окне вместо концентрических окружностей будут изображены фигуры в соответствии с индивидуальным заданием.

6.5. К пункту 4.5

Для выполнения данного пункта в модуле «main.cpp» следует вместо переменных TC1 и TC2 описать две глобальные переменные, относящиеся к разным классам: одна типа CTwoCircles, вторая – типа CMyFigure. CTwoCircles TC; CMyFigure MF;

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

7. Содержание отчёта.

Общие положения.

См. п. 7 лабораторной работы №1.

Отчёт по работе должен содержать:

Название работы. Цель работы. Номер варианта и соответствующее ему индивидуальное задание из п. 5 (с

расшифровкой обозначений цветов и типов линий). Копирование таблиц п. 5 в отчёт полностью не допускается.

Для отдельных пунктов программы работы в отчёте необходимо привести следующее.

Для пункта 4.2

Полный текст заголовочного файла и файла исходного текста для базового класса с необходимыми комментариями.

Page 43: Лабораторная работа № 1.dpl61.narod.ru/files/TP/Lab_TP.pdf · 2017-02-19 · – 3 – 6.2. К пункту 4.2 Основная функция приложения

– 43 – Для пункта 4.3

Полный текст заголовочного файла и файла исходного текста для дочернего класса с необходимыми комментариями.

Изображение основного окна разработанной программы.

Для пункта 4.4

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

Полный текст модуля «main.cpp» с необходимыми комментариями. Изображение основного окна разработанной программы.

Для пункта 4.5

Полный текст модуля «main.cpp» с необходимыми комментариями. Изображение основного окна разработанной программы.

Заключительная часть

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

8. Контрольные вопросы.

8.1. Для чего в ООП используется наследование?

8.2. Что наследуется дочерним (производным) классом от родительского (базового) класса?

8.3. На что в общем случае повлияют изменения, выполненные в базовом классе:

а) только на базовый класс; б) на базовый класс и все дочерние классы; в) только на дочерние классы.

8.4. Предложите иерархию классов (2…3 уровня), описывающих какие-либо реальные объекты (за исключением примеров, приведённых в п.п. 6.1.1, 6.1.2). Обоснуйте возможный состав свойств и методов родительских и дочерних классов.

8.5. Как описывается дочерний класс на основе некоторого базового класса?

8.6. Как влияют ключевые слова private, protected, public, указанные перед именем базового класса при описании дочернего класса, на уровень доступа к членам базового класса?

8.7. На что влияют ключевые слова private, protected, public, указанные перед именем базового класса при описании дочернего класса:

а) только на уровень доступа к членам базового класса; б) только на уровень доступа к членам дочернего класса; в) на уровень доступа к членам базового и дочернего классов.

Page 44: Лабораторная работа № 1.dpl61.narod.ru/files/TP/Lab_TP.pdf · 2017-02-19 · – 3 – 6.2. К пункту 4.2 Основная функция приложения

– 44 –

Лабораторная работа № 5.

Полиморфизм. Виртуальные функции

1. Цель работы

Ознакомиться с понятием «полиморфизм» в объектно-ориентированным программировании, а также с реализацией полиморфизма при помощи виртуальных функций.

2. Описание лабораторного стенда

В качестве лабораторного стенда используется IBM-совместимый персональный компьютер с установленной интегрированной средой разработки программ Dev-C++ 5.11.

3. Подготовка к выполнению работы

3.1. Подготовить работоспособный проект, полученный в результате выполнения лабораторной работы №4.

3.2. Подготовить электронный носитель для сохранения результатов работы (файлов).

4. Программа работы

4.1. На рабочем компьютере создать папку для сохранения результатов работы. Скопировать в неё проект Dev-C++ с результатами выполнения индивидуального задания ЛР №4, убедиться в его работоспособности.

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

4.3. Изменить реализацию функции ClientDraw, использовав в ней массив указателей на графические объекты. Проверить работу программы.

4.4. Добавить в окно несколько дополнительных объектов типа CTwoCircles и CMyFigure, задав различные значения их свойств (размер, положение, цвет). Продемонстрировать результат преподавателю.

4.5. Сохранить результаты работы (папку с проектом Dev-C++) на внешнем носителе, поскольку они потребуются для выполнения следующей работы.

4.6. Оформить отчет по работе.

5. Варианты заданий

В данной работе используются только результаты выполнения индивидуального задания к ЛР №4.

Page 45: Лабораторная работа № 1.dpl61.narod.ru/files/TP/Lab_TP.pdf · 2017-02-19 · – 3 – 6.2. К пункту 4.2 Основная функция приложения

– 45 –

6. Указания к выполнению работы.

6.1. К пункту 4.2

6.1.1. В п. 4.2 ЛР №4 отмечалось, что способность рисовать себя в окне – это элемент поведения, присущий всем графическим объектам программы. Однако на тот момент было невозможно описать соответствующий метод в их общем предке – в базовом классе CVisualObject. Проблема заключалась в том, что реализация этого метода у каждого проектируемого класса графических объектов должна быть разной. Более того: на базе CVisualObject впоследствии могли описываться новые классы-потомки, о которых во время разработки базового класса вообще ничего не было известно.

Решение описанной проблемы состоит в том, чтобы объявить в базовом классе некий общий метод рисования (назовём его Draw), фактическая реализация которого будет задана в дочерних классах, причём для каждого дочернего класса эта реализация может быть своей. В языке С++, как и во многих других объектно-ориентированных языках, это достигается за счёт использования виртуальных методов (виртуальных функций).

6.1.2. Чтобы сделать метод виртуальным, в его описании следует использовать ключевое слово virtual. Дополним описание базового класса CVisualObject виртуальным методом Draw. Для этого в файл «CVisualObject.h» нужно внести следующие изменения: class CVisualObject { ... void SetHWND(HWND handle) {hwnd = handle;} // Метод, отображающий объект в заданном контексте virtual void Draw(HDC hdc); };

Скомпилируйте проект, нажав клавишу F12 (Rebuild All), и убедитесь, что компиляция приводит к ошибке «undefined reference to vtable for CVisualObject». Причина ошибки в том, что мы добавили в базовый класс лишь объявление метода Draw, а его реализация (описание) отсутствует. С другой стороны, привести осмысленное описание этого метода в базовом классе невозможно, поскольку нельзя нарисовать просто «графический объект». Рисование возможно только для потомков класса CVisualObject – окружностей, линий и т.п., – в которых метод Draw будет переопределён.

Простейшее решение проблемы заключается в том, чтобы описать в базовом классе «пустой» метод Draw, т.е. такой, который не делает ничего (фигурные скобки в конце строки обозначают пустое тело метода): ... virtual void Draw(HDC hdc) { }; };

Внесите описанные изменения, снова скомпилируйте проект (F12) и убедитесь, что ошибка исчезла.

6.1.3. Рассмотренное решение является простым и понятным, однако оно идеологически неверно: реализация метода Draw в базовом классе не должна быть пустой, она должна отсутствовать, поскольку для обобщённого графического объекта описать конкретный метод рисования в принципе невозможно.

Для объявления нереализованных виртуальных методов в языке C++ используются так называемые «чистые» (pure) виртуальные функции, признаком которых является текст «= 0» в конце объявления: ... virtual void Draw(HDC hdc) = 0; };

Page 46: Лабораторная работа № 1.dpl61.narod.ru/files/TP/Lab_TP.pdf · 2017-02-19 · – 3 – 6.2. К пункту 4.2 Основная функция приложения

– 46 – Приведённый текст означает, что в базовом классе CVisualObject виртуальная функция Draw не реализована (является «чистой»). Из этого следуют две особенности. Во-первых, мы не сможем создать экземпляр класса CVisualObject, поскольку в этом классе отсутствует реализация одного из методов (такие методы в ООП называются абстрактными методами, а классы, содержащие один или несколько абстрактных методов, называются абстрактными классами). Во-вторых, классы-потомки CVisualObject должны переопределять метод Draw (т.е. описывать его реализацию), в противном случае эти классы также будут абстрактными.

Абстрактные классы целесообразно описывать в тех случаях, когда необходимо исключить возможность создания объектов, относящихся к этим классам (т.е. когда базовый класс предназначен только для создания классов-потомков). Например, в нашем случае создание объекта типа CVisualObject бессмысленно: этот класс не содержит специфических свойств конкретных графических объектов. Сделав класс CVisualObject абстрактным, мы не позволяем создавать объекты с неполным набором свойств и методов.

Внесите приведённые изменения в текст файла «CVisualObject.h». Описание классов-потомков изменять не нужно: если в базовом классе какой-либо метод описан как виртуальный, то методы дочерних классов с таким же именем и набором параметров тоже становятся виртуальными.

Скомпилируйте проект (F12) и добейтесь отсутствия ошибок. Запустите программу и убедитесь в её работоспособности.

6.2. К пункту 4.3

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

Предположим, имеется несколько разнотипных графических объектов (в разрабатываемой программе это объекты классов CTwoCircles и CMyFigure). При необходимости обновления изображения окна вызывается функция ClientDraw, которая, в свою очередь, вызывает метод Draw каждого графического объекта. Следовательно, при добавлении или удалении объектов необходимо изменять текст функции ClientDraw, что неудобно и может привести к неочевидным ошибкам (например, добавили ещё один графический объект, но забыли вызвать его метод Draw в функции ClientDraw). Удобно было бы создать массив графических объектов и в функции ClientDraw в цикле вызывать метод рисования каждого из них независимо от их количества. Но массив может содержать только однотипные элементы, а наши объекты относятся к разным классам.

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

В результате выполнения п. 4.5 ЛР №4 описание экземпляров графических объектов в модуле «main.cpp» должно выглядеть так (имена объектов могут отличаться): #include <windows.h> #include "CTwoCircles.h" #include "CMyFigure.h" CTwoCircles TC1; CMyFigure MF1;

Дополним приведённый текст описанием массива, содержащего указатели на объекты типа CVisualObject:

Page 47: Лабораторная работа № 1.dpl61.narod.ru/files/TP/Lab_TP.pdf · 2017-02-19 · – 3 – 6.2. К пункту 4.2 Основная функция приложения

– 47 – ... CTwoCircles TC1; CMyFigure MF1; CVisualObject* VObj[]={&TC1, &MF1};

Количество элементов массива в скобках не задано и определяется количеством элементов в списке инициализации (в данном случае два). Элементу VObj[0] присваивается указатель на объект TC1, а элементу VObj[1] – указатель на объект MF1. Обратите внимание, что элементам типа «указатель на CVisualObject» присваиваются значения других типов: в первом случае типа «указатель на CTwoCircles», во втором – типа «указатель на CMyFigure». В этом заключается одно из проявлений полиморфизма: указатель на базовый класс (CVisualObject) может содержать адрес не только объекта базового класса, но и любого из его потомков (CTwoCircles, CMyFigure).

Имея такой массив указателей, функцию ClientDraw можно переписать следующим образом: void ClientDraw(HWND hwnd, WPARAM wParam, LPARAM lParam) { PAINTSTRUCT ps; HDC hdc = BeginPaint(hwnd, &ps); for (int i=0; i<COUNT_OF(VObj); i++) VObj[i]->Draw(hdc); EndPaint(hwnd, &ps); }

Примечание. В языке С++ отсутствуют стандартные средства для определения количества элементов массива. В цикле for в вышеприведённом тексте для этой цели используется макрос COUNT_OF, который нужно описать в начале модуля: #include <windows.h> #include "CTwoCircles.h" #include "CMyFigure.h" #define COUNT_OF(x) (sizeof(x)/sizeof(x[0]))

Из текста функции ClientDraw видно, что она поочерёдно вызывает метод Draw для всех объектов, указатели на которые содержатся в массиве VObj. Несмотря на то, что элементы массива имеют тип «указатель на CVisualObject», для каждого объекта благодаря полиморфизму автоматически вызывается «правильный» метод: если объект относится к классу CTwoCircles, то вызывается метод CTwoCircles::Draw, если к классу CMyFigure – то CMyFigure::Draw. Если бы метод Draw не был описан как виртуальный, то для каждого объекта независимо от его типа вызывался бы метод CVisualObject::Draw (в соответствии с типом указателя). В этом можно убедиться, убрав в описании метода CVisualObject::Draw слово virtual и заменив в конце описания текст «= 0» на пустое тело метода «{ }» (не забудьте потом вернуть всё на место).

6.3. К пункту 4.4

Благодаря рассмотренному выше подходу к реализации функции ClientDraw добавление в программу любых объектов-потомков CVisualObject не составит труда: их нужно описать и добавить указатели на них в массив VObj: ... CTwoCircles TC1, TC2; CMyFigure MF1, MF2, MF3; CVisualObject* VObj[]={&TC1, &MF1, &TC2, &MF2, &MF3};

Чтобы добавленные объекты не сливались на экране в один, их свойствам нужно задать различные значения (положение, цвет и т.п.). В разрабатываемой программе это можно сделать перед основным циклом обработки сообщений while(GetMessage(…)).

Page 48: Лабораторная работа № 1.dpl61.narod.ru/files/TP/Lab_TP.pdf · 2017-02-19 · – 3 – 6.2. К пункту 4.2 Основная функция приложения

– 48 –

7. Содержание отчёта.

Общие положения.

См. п. 7 лабораторной работы №1.

Отчёт по работе должен содержать:

Название работы. Цель работы. Номер варианта и соответствующее ему индивидуальное задание из п. 5 (с

расшифровкой обозначений цветов и типов линий). Копирование таблиц п. 5 в отчёт полностью не допускается.

Для отдельных пунктов программы работы в отчёте необходимо привести следующее.

Для пункта 4.2

Окончательный вариант текста файла «CVisualObject.h» с необходимыми комментариями.

Для пункта 4.3

Окончательный вариант текста файла «main.cpp» с необходимыми комментариями. Изображение окна разработанной программы.

Для пункта 4.4

Текст изменений, внесённых в модуль «main.cpp» с необходимыми комментариями. Изображение окна разработанной программы.

Заключительная часть

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

8. Контрольные вопросы.

8.1. В чём заключается проблема при описании метода рисования для базового класса CVisualObject?

8.2. Почему в рассмотренном примере использование абстрактного метода Draw в базовом классе является более предпочтительным по сравнению с использованием «пустого» метода (т.е. метода, который ничего не делает)?

8.3. В чём заключается особенность абстрактных классов? В каких случаях целесообразно создание абстрактных классов?

8.4. Что такое полиморфизм? Приведите пример использования полиморфизма в разработанной программе.

8.5. Каким способом в языках С/С++ можно определить число элементов массива? Поясните смысл макроса, использованного для этой цели в программе.