2. Логические шрифты

advertisement
ФЕДЕРАЛЬНОЕ АГЕНСТВО ПО ОБРАЗОВАНИЮ
МОСКОВСКИЙ ИНЖЕНЕРНО-ФИЗИЧЕСКИЙ ИНСТИТУТ
(ГОСУДАРСТВЕННЫЙ УНИВЕРСИТЕТ)
К.Г. Финогенов
Лабораторный практикум
«Основы разработки приложений
Windows»
Книга 2
Москва 2005
УДК 32.973.1
ББК 681.3
Ф59
Финогенов К.Г. Лабораторный практикум «Основы разработки
приложений Windows». Книга 2. Уч. пособие.
М.:МИФИ, 2005. 108 с.
Пособие предназначено для широкого круга читателей, приступающих к
освоению программирования на языке С++ в операционной системе Windows. В
первой части описано использование в прикладных программах таких средств
системы Windows, как шрифты, таймеры, дочерние окна, растровые изображения
и др. Уделено внимание специфическим возможностям 32-разрядных приложений
– особенностям обслуживания файлов и организации параллельных вычислений.
Вторая часть пособия – описание лабораторного практикума по изучению
основ разработки приложений Windows.
Предназначено для обучения студентов кафедры компьютерных медицинских
систем факультета автоматики и электроники МИФИ по курсам “Информатика”,
“Компьютерный практикум” и “Языки программирования и операционные
системы”. Пособие может быть также полезно студентам, аспирантам и
преподавателям, имеющим представление о языке С++ и желающим
самостоятельно освоить принципы разработки прикладных программ,
предназначенных для работы в системе Windows.
Рецензенты: С. М. Зайцев и В. В. Комаров
Рекомендовано редсоветом МИФИ в качестве учебного пособия
© Московский инженерно-физический институт
(государственный университет), 2005
Оглавление
Часть 1. Теоретические сведения ..................................... 4
1. Основы архитектуры защищенного режима .............. 4
Регистры процессора..................................................................... 4
Адресация памяти ......................................................................... 9
2. Логические шрифты.............................................................. 13
Создание логических шрифтов .................................................. 13
Вывод на экран текстовых строк ............................................... 17
3. Таймеры Windows................................................................... 19
Организация и обслуживание таймеров.................................... 19
Мультимедийные таймеры ......................................................... 22
Измерение интервалов времени ......................................... 22
Организация периодического процесса ............................. 23
Задание однократного интервала времени ...................... 25
4. Дочерние окна........................................................................... 26
Создание и использование дочерних окон ............................... 26
Окна предопределенных классов в главном окне .................... 33
5. Вывод растровых изображений ....................................... 35
Процедура вывода растрового изображения ............................ 35
Компоновка составных изображений ........................................ 42
6. Обслуживание файлов в 32-разрядных
приложениях Windows .............................................................. 44
Базовые операции с файлами ..................................................... 45
Открытие и создание файла ............................................. 45
Запись и чтение файла ....................................................... 47
Файлы, проецируемые в память................................................. 50
7. Процессы и потоки ................................................................. 54
Создание дочернего процесса .................................................... 54
Создание дочернего потока ........................................................ 55
Синхронизация потоков.............................................................. 61
Общие характеристики объектов Windows .................... 61
Синхронизация с помощью состояний потока ................ 63
Синхронизация с помощью событий ................................. 65
Критические секции и защита данных ............................. 68
8. Библиотеки динамической компоновки ..................... 70
Часть 2. Лабораторный практикум .............................. 73
Работы лабораторного практикума ........................................... 73
Индивидуальные задания лабораторного практикума ............ 88
Список литературы .................................................................. 107
3
Часть 1
Теоретические сведения
1. ОСНОВЫ АРХИТЕКТУРЫ ЗАЩИЩЕННОГО
РЕЖИМА
Регистры процессора
Процесс выполнения любой программы заключается в том, что
процессор считывает из памяти команду за командой, выполняет
их, а результаты записывает снова в память или, возможно,
оставляет в своих внутренних регистрах для использования в
следующих командах. Таким образом, для программиста
важнейшими элементами архитектуры процессора и компьютера в
целом является оперативная память и средства обращения к ней, а
также
регистры
процессора,
в
которых
сохраняются
промежуточные результаты выполняемых операций.
Работая на языке высокого уровня, например на С++,
программист лишь в редких случаях непосредственно использует в
своих программах обозначение регистров и практически никогда
не обращается по абсолютным адресам памяти. Однако при
отладке программ приходится иметь дело и с тем, и с другим, так
как отладчик выводит на экран текст программы в виде команд
процессора, почти в каждой из которых фигурирует обозначение
того или иного регистра, а также адреса ячеек памяти.
Количество и назначение регистров процессора, как и механизм
адресации памяти, зависит от типа процессора. Мы будем
рассматривать 32-разрядные процессоры фирмы Intel (например
Pentium) и совместимые с ними процессоры других фирм.
В процессоре имеется около трех десятков программно
адресуемых регистров, из которых важнейшими для программиста
являются регистры общего назначения и сегментные. Кроме того,
4
необходимо иметь представление о назначении указателя команд и
регистра флагов.
Процессор содержит 8 регистров общего назначения (рис. 1.1),
носящих мнемонические обозначения EAX, EBX, ECX и т. д. Все
они имеют размер по четыре байта, т. е. могут хранить по одному
целому числу типа int или unsigned int.
Рис. 1.1. Регистры общего назначения
Для того чтобы программа могла работать с более короткими
переменными (например, short – 2 байта или char – 1 байт),
младшие половины регистров EAX, EBX, ECX и EDX имеют
самостоятельные обозначения – AX, BX, CX и DX соответственно;
при этом они, в свою очередь, еще разделены пополам с
образованием регистров длиной всего 1 байт. Старшая половина
(старший байт) регистра AX носит название AH (от high –
5
старший), младший байт – AL (от low – младший). Аналогично
построены имена остальных байтовых регистров.
Компилятор, преобразуя текст программы в машинные коды,
активно использует регистры процессора для временного хранения
операндов команд и результатов выполняемых операций. На рис.
1.2 показано, как может выглядеть простой фрагмент программы
на языке С++ после компиляции. Видно, что компилятор выделил
для хранения переменных x и y регистры EAX и EDX, и в тот же
регистр EDX поместил результат сложения. В дальнейшем по ходу
программы этот результат может быть перенесен в память, а
регистр EDX использован для выполнения других операций.
int x=0x12345678;
int y=0x11111111;
int z=x+y;
mov EAX,12345678h
mov EDX,11111111h
add EDX,EAX
Рис. 1.2. Фрагмент программы и результат его компиляции
Особое значение имеют сегментные регистры процессора (рис.
1.3). Свое название они получили потому, что любая программа
располагается в связных участках памяти, называемых сегментами.
Рис. 1.3. Сегментные регистры
Как правило, для программы выделяются три сегмента: команд,
данных и стека (рис. 1.4). В сегменте команд хранятся
программные строки, в сегменте данных располагаются
глобальные данные, которые видны из любой точки программы, а
6
сегмент стека служит для временного хранения локальных данных,
место под которые выделяется лишь на время выполнения той
функции, в которой они объявлены.
Рис. 1.4. Сегменты программы и роль сегментных регистров
В сегментных регистрах хранится информация, позволяющая
процессору определить, в каком месте памяти располагается тот
или иной программный сегмент. Поскольку в программу обычно
входят три сегмента, для обращения к ним используются три
сегментных регистра. Регистр CS всегда служит для адресации
сегмента команд, в регистре DS обычно хранится адрес сегмента
данных, а регистр SS используется исключительно для работы со
стеком. Остальные сегментные регистры используются по мере
надобности для обращения, например, к системным полям памяти.
По содержимому сегментного регистра процессор может
определить, каков адрес начала сегмента в памяти (этот адрес
называется базовым). Однако ему еще нужно добраться до
конкретного данного, хранящегося в сегменте. Для этого служит
смещение, которое представляет собой номер начального байта
конкретного данного от начала сегмента. Таким образом, адрес
любого байта памяти состоит из двух частей: содержимого
сегментного регистра, по которому процессор определяет базовый
адрес сегмента, и смещения, которое следует прибавить к базовому
адресу, чтобы получить истинный адрес адресуемой ячейки
памяти.
Смещение очередной команды, которую надлежит выполнять
процессору, всегда находится в регистре-указателе команд EIP. Он
не входит в состав регистров общего назначения (и,
соответственно, не был показан на рис. 1.1), потому что не является
7
программно адресуемым регистром, т. е. к нему нельзя напрямую
обратиться из программы и изменить его значение. Содержимым
регистра EIP управляет исключительно сам процессор: прочитав из
памяти очередной байт команды, процессор увеличивает
содержимое EIP на 1, чем и обеспечивается строго
последовательное чтение байтов программы, находящейся в
памяти. Поскольку при обращении к сегменту команд процессор
использует сегментный регистр CS, содержимое пары регистров
CS и EIP, которое обычно обозначается, как CS:EIP, в каждый
момент времени характеризует то место, до которого дошла
программа по ходу своего выполнения.
Текущее смещение в стеке всегда находится в регистре-указателе стека ESP. Поскольку адрес стека процессор определяет с
помощью сегментного регистра SS, содержимое пары регистров
SS:ESP характеризует то место стека, в котором находится
последнее отправленное туда данное.
К сегменту данных процессор обращается обычно с помощью
регистра DS; смещение же конкретной ячейки может находиться в
этом случае в любом регистре общего назначения: EAX, EBX и
других (за исключением, разумеется, ESP). Смещение может также
входить непосредственно в код команды, т. е. храниться не в
регистре, а в составе кода команды в памяти.
Еще один регистр процессора, о котором следует иметь
представление – это регистр флагов. В отличие от остальных
регистров, в каждом из которых хранится единое данное, регистр
флагов представляет собой, в основном, набор однобитовых
флагов. Некоторые из этих флагов устанавливаются и
сбрасываются программистом по мере необходимости (например,
флаг IF управления аппаратными прерываниями процессора), хотя
следует отметить, что в защищенном режиме обращение к этим
флагам осуществляется, как правило, не непосредственно из
прикладной программы, а из специальных программ, работающих
на высшем уровне привилегий – так называемых драйверов.
Другими флагами управляет сам процессор, устанавливая или
сбрасывая их по результатам выполнения каждой команды. Если,
например, в результате выполнения арифметической операции
образовалось отрицательное число, устанавливается флаг знака SF;
если получился нулевой результат, устанавливается флаг нуля ZF и
8
т. д. Анализ флагов позволяет процессору выполнять операции
сравнения и условных переходов. Содержимое регистра флагов
отображается в отладчике, и программист имеет возможность
определить состояние флагов в любой точке программы.
Адресация памяти
Как уже отмечалось, адрес любого байта памяти состоит из двух
компонентов – содержимого сегментного регистра (оно называется
селектором) и смещения. Получение из этих двух компонентов
истинного
физического
адреса
памяти
осуществляется
процессором
посредством
довольно
сложной
цепочки
преобразований (рис. 1.5).
Рис. 1.5. Цепочка преобразования адресов в защищенном режиме
При расшифровке кодов команд процессор имеет дело с
содержимым сегментных регистров (селекторами) и смещениями,
которые могут храниться в тех или иных регистрах или входить в
коды команд в виде операндов. Их же наблюдает программист,
отлаживая программу с помощью отладчика. Комбинацию
селектора и смещения иногда называют виртуальным адресом.
9
Получив из программы виртуальный адрес, процессор
преобразует его в линейный. Делается это следующим образом.
В состав селекторов входят номера (индексы) ячеек
специальной таблицы, содержащей дескрипторы сегментов
программы. Эта таблица заранее строится операционной системой
и размещается в системных полях памяти. Каждый дескриптор
таблицы дескрипторов имеет размер 8 байт, и в нем хранятся все
характеристики, необходимые процессору для обслуживания этого
сегмента: базовый линейный адрес сегмента, его граница, которая
представляет собой номер последнего байта сегмента, а также
атрибуты сегмента, определяющие его свойства (рис. 1.6). Таким
образом, селекторы в конечном счете характеризуют сегменты
памяти.
Рис. 1.6. Преобразование виртуального адреса в линейный
Процессор с помощью селектора определяет индекс
дескриптора адресуемого сегмента, извлекает из него базовый
линейный 32-разрядный адрес сегмента и, прибавив к нему 32разрядное же смещение, образует адрес адресуемой ячейки памяти.
Этот адрес называется линейным, потому что в отличие от
виртуального адреса, состоящего из двух частей, он представляет
10
собой одно 32-разрядное число, которое может принимать, в
принципе, любое значение от 0 до 0xFFFFFFFF, т. е. до 4 Г – 1.
Существенной характеристикой 32-разрядных приложений
Windows является то, что они работают в так называемой плоской
модели памяти (иногда для обозначения плоской памяти
используют англоязычный термин “FLAT”, являющийся, кстати,
ключевым словом некоторых языков программирования). Базовым
адресам всех сегментов, хранящимся в таблице дескрипторов,
присваивается значение 0, а границам сегментов – значение 4 Г – 1.
Таким образом, все сегменты в плоской модели памяти имеют
размер 4 Гбайт и накладываются друг на друга. Однако различным
участкам программ (как прикладных, так и системных)
назначаются различные смещения. Поскольку базовые адреса всех
сегментов равны 0, смещения, действующие в программе,
совпадают с линейными адресами. Это дает основание говорить,
что 32-разрядные программы работают в линейном пространстве
адресов.
Из всего пространства линейных адресов система Windows
выделяет первые два гигабайта для прикладных программ, а
вторые два гигабайта – для системных (рис. 1.7).
Рис. 1.7.Распределение линейного адресного пространства
Прикладные программы размещаются начиная с 5-го мегабайта
линейного пространства, т. е. с адреса 0x400000 (первые 4
мегабайта зарезервированы для системного использования),
поэтому функция WinMain может начинаться, например, с адреса
11
0x40107C, глобальные данные размещаться по адресам,
начинающимся с 0x402074, а стек начинаться с адреса 0x68FE30.
Реально размер прикладных программ редко превышает
несколько единиц или десятков мегабайт (часто гораздо меньше),
поэтому “залезть” во второй гигабайт прикладная программа
может лишь в том случае, если она обрабатывает очень большие
массивы данных, например массив из 250 миллионов целых чисел,
который займет в памяти, действительно, около одного гигабайта.
Большая часть системных программ располагается в 4-м
гигабайте. Поэтому линейные адреса (и смещения) системных
программ обычно имеют значения, начинающиеся с 0xC
(например, 0xC0141A90).
Получив линейный адрес адресуемого байта, процессор с
помощью таблиц страниц (их еще называют таблицами страничной
трансляции) преобразует его в 32-разрядный физический адрес
того байта памяти, где находится или, точнее, с которого
начинается конкретная команда или конкретное данное. Этот адрес
зависит от объема памяти, установленной на компьютере, и может
располагаться в любом месте физической памяти (кроме 1-го
мегабайта, где размещаются программы MS-DOS, видеопамять и
ПЗУ BIOS).
Страничная трансляция представляет собой довольно сложный
механизм, в котором принимают участие аппаратные средства
процессора и находящиеся в памяти таблицы страничной
трансляции. Упрощенно процедуру страничной трансляции можно
представить следующим образом.
Все пространство линейных адресов так же, как и пространство
физических адресов реально установленной на компьютере памяти,
делится на страницы объемом по 4 Кбайт. Каждой странице
линейных адресов соответствует своя ячейка, или запись в таблице
страниц. В эту ячейку (она имеет размер всего 4 байта) система
заносит физический адрес той страницы памяти, в которую в
настоящий момент загружена часть программы, соответствующая
данной странице линейных адресов, чем и осуществляется
отображение смещений программы на физические адреса памяти
(рис. 1.8).
Если в процессе поддержки многозадачного режима система
выгрузит из памяти текущую программу (или ее часть), в
12
соответствующих ячейках таблицы страниц делается отметка “не
загружена”, и связь между линейными и физическими адресами
для данной программы разрывается. В дальнейшем, при повторной
загрузке той же программы, она может попасть уже в другие
участки физической памяти, и система соответствующим образом
модифицирует таблицу страниц. В результате физические адреса,
по которым загружаются части программы, в процессе работы
системы динамически изменяются (и, разумеется, на разных
компьютерах будут разными), в то время, как линейные адреса
(смещения) конкретной программы, назначаемые ей в процессе
компиляции, жестко фиксированы.
Рис. 1.8. Страничная трансляция адресов
Любопытным следствием из рассмотренного механизма
преобразования адресов является тот факт, что все программы в
процессе компиляции получают одни и те же линейные адреса.
Однако для каждой программы система образует свою таблицу
страниц, переключая их при передаче управления от одной
программы к другой. Поэтому, хотя все программы выполняются в
одном и том же линейном адресном пространстве, их коды
находятся в разных местах физической памяти и не затирают друг
друга.
13
2. ЛОГИЧЕСКИЕ ШРИФТЫ
Создание логических шрифтов
Разрабатывая изобразительные детали приложения, естественно
воспользоваться богатыми возможностями, предоставляемыми
системой Windows в части шрифтового оформления документов.
Для того чтобы создать красивый и наглядный документ или
экранный кадр, приходится использовать шрифты с различным
начертанием, оформлением (курсив, жирный, подчеркнутый),
размером и пространственной ориентацией. В зависимости от
принципа хранения в памяти компьютера формы символов
различают растровые (точечные) и масштабируемые шрифты,
которые еще называют шрифтами TrueType. Достоинство шрифтов
TrueType заключается в том, что они позволяют изменять в
широких пределах размер и другие характеристики символов
(например, ширину букв) без снижения качества изображения, что
и обусловило их широкое применение.
Операционная система Windows поставляется с базовым
набором растровых и масштабируемых шрифтов с разнообразными
начертаниями и характеристиками. Среди них имеются как
шрифты с равной шириной всех символов (они традиционно
используются, например, в исходных текстах программ), так и
более приятные для глаза пропорциональные шрифты, у которых
ширина символа зависит от его формы (буква “Ш”, например, шире
символа “1”). Многие прикладные программы при их установке
расширяют базовый набор Windows, добавляя свои шрифты;
наконец, можно приобрести и установить в системе наборы
шрифтов независимых разработчиков.
Работая с какой-либо коммерческой прикладной программой,
использующей шрифты, например с текстовым редактором Microsoft Word или графическим редактором CorelDraw, пользователь
может выбирать для оформления документа любые шрифты из
установленных в системе, назначая им требуемые характеристики
(размер, интервал между символами и строками т. д.) с помощью
средств используемого редактора. Сложнее обстоит дело при
разработке собственного приложения Windows. На экран можно
вывести только тот шрифт, дескриптор которого загружен в
14
контекст устройства; при необходимости изменить характеристики
шрифта надо сначала создать новый шрифт, хотя под этим
обманчивым термином понимается не разработка собственного
шрифта, а выбор одного из шрифтов, установленных в системе, и
придание ему требуемых характеристик.
Правда, в системе имеется несколько готовых шрифтов,
дескрипторы которых можно получить со “склада” Windows с
помощью макроса SelectFont() или обобщенной функции SelectObject(). Дескриптор одного из них по умолчанию
загружается в контекст устройства при его создании. Однако
возможности таких шрифтов ограничены, так как они не
допускают изменения своих характеристик (кроме цвета).
Процедура создания нового шрифта довольно проста. Для этого
нужно объявить в программе структурную переменную типа
LOGFONT, заполнить ее поля требуемыми значениями и вызвать
функцию Windows CreateFontIndirect(). Эта функция
вернет дескриптор нового шрифта; после выбора полученного
дескриптора в контекст устройства любая функция вывода на экран
текста будет использовать именно этот шрифт. Если в приложении
желательно использовать разные шрифты, их можно создать
заранее и выбирать в контекст устройства по мере необходимости.
Функция CreateFontIndirect() использует в качестве
исходного материала физический шрифт, хранящийся на диске в
виде файла; результатом работы этой функции будет логический
шрифт, дескриптор которого и загружается в контекст устройства.
Вывести на экран текст непосредственно физическим шрифтом
нельзя, так как функции GDI работают только с логическими
шрифтами. Даже если мы хотим иметь шрифт с характеристиками,
в точности соответствующими физическому шрифту, все равно из
него сначала надо образовать логический шрифт (обнулив все
члены структуры LOGFONT, кроме имени шрифта) и лишь затем им
пользоваться.
Структура LOGFONT содержит много членов, однако обычно
можно ограничиться заданием лишь небольшой их части. Следует
только иметь в виду, что неправильная установка того или иного
члена этой структуры может привести к весьма неприятным
последствиям, так как Windows, не сумев создать в точности
заказанный вами шрифт, будет пытаться подобрать наиболее
15
подходящий; часто в этом случае подбирается шрифт, весьма
далекий от заказанного.
Структуру LOGFONT целесообразно использовать для задания
характеристик масштабируемых (TrueType) шрифтов; только в
этом случае будут действовать такие, например, характеристики
шрифта, как угол наклона или размер (растровые шрифты
допускают изменение размера, но лишь в обусловленных пределах
или при низком качестве увеличения).
Структура LOGFONT имеет следующий состав членов:
typedef
int
int
int
int
int
struct tagLOGFONT
lfHeight;//Высота
lfWidth;//Средняя ширина; если=0, то по умолчанию
lfEscapement;//Угол наклона в единицах 1/10 градуса
lfOrientation;//Не используется
lfWeight;//Насыщенность: FW_NORMAL (нормальная),
FW_BOLD (полужирный шрифт)
lfItalic;//Если=1, то курсив
lfUnderline;//Если=1, то подчеркивание
lfStrikeOut;//Если=1, то перечеркивание
lfCharSet;//Набор символов; обычно=0
lfOutPrecision;//Точность соответствия; обычно=0
lfClipPrecision;//Способ вырезки части символа;
//обычно=0
BYTE lfQuality;//Качество; обычно=0
BYTE lfPitchAndFamily;//Шаг и семейство; обычно=0
BYTE lfFaceName[LF_FACESIZE];//Имя начертания шрифта
} LOGFONT;
BYTE
BYTE
BYTE
BYTE
BYTE
BYTE
Наиболее важным является последний член описанной выше
структуры, символьный массив lfFaceName. В него надо
скопировать полное имя шрифта из числа шрифтов, установленных
в Windows, например Times New Roman Cyr, Arial Cyr,
Courier New Cyr и т. д. Если в имени шрифта будет допущена
хотя бы незначительная ошибка, Windows, скорее всего, создаст
шрифт с совершенно другим начертанием. Обратите также
внимание на то, что член lfFaceName описан в структуре LOGFONT, как символьный массив длиной LF_FACESIZE символов. В
эту переменную следует занести не адрес имени шрифта, а саму
символьную строку с именем шрифта. Поэтому этот член нельзя
16
инициализировать
оператором
присваивания;
придется
воспользоваться функцией C++ strcpy() для копирования строк.
После того, как новые шрифты использованы для вывода на
экран текстовых строк и необходимость в них отпала, их следует
удалить, чтобы освободить занимаемую ими память. Это можно
сделать и перед завершением приложения в функции OnDestroy() обработки сообщения WM_DESTROY с помощью макроса
DeleteFont() или обобщенной функции DeleteObject().
Практически при завершении приложения Windows удаляет все
созданные в нем объекты.
Вывод на экран текстовых строк
Как и в случае любых других графических инструментов, вывод
текста в окно приложения осуществляется тем шрифтом,
дескриптор которого выбран в настоящий момент в контекст
устройства. Поэтому общая процедура использования шрифта
выглядит следующим образом:
 в функции OnCreate() обработки сообщения WM_CREATE
создаются необходимые шрифты, а их дескрипторы
запоминаются в глобальных переменных;
 в функции OnPaint() обработки сообщений WM_PAINT с
помощью макроса SelectFont() в контекст устройства выбирается дескриптор требуемого шрифта;
 при необходимости в контексте устройства устанавливаются
требуемые характеристики шрифта – цвет и прозрачность
фона;
 с помощью функций TextOut() или DrawText() в окно
приложения выводятся требуемые строки.
Функция TextOut() позволяет вывести в окно приложения
строку текста, начиная с точно заданной точки, координаты
которой в виде номеров пикселов указываются в параметрах
функции. Координаты относятся к верхнему левому углу
знакоместа. Поскольку шрифты в Windows могут иметь различный
размер, высота знакоместа обычно заранее неизвестна, и для того
чтобы последовательные строки не накладывались друг на друга,
необходимо с помощью структуры типа TEXTMETRIC и функции
17
GetTextMetrics() определить высоту символов и, исходя из
этой величины, задавать координаты следующих строк. Эта
процедура была описана в пособии [1].
Функция DrawText() позиционирует текст по-другому. Из ее
прототипа, взятого из интерактивного справочника
int DrawText(HDC hDC, LPCTSTR lpString, int nCount,
LPRECT lpRect, UINT uFormat);
видно, что в качестве ее параметров выступают: дескриптор
контекста устройства hDC, адрес выводимой строки lpString,
длина выводимой строки nCount, адрес lpRect структурной
переменной типа RECT с координатами прямоугольной области
экрана, в которую выводится текст, а также константа
форматирования текста uFormat.
Функция DrawText() выводит заданный текст в указанную
прямоугольную область (которая может быть и гораздо больше
выводимой строки по размеру), причем расположение текста
внутри области определяется последним параметром функции.
Использованная в приводимом ниже примере символическая
константа DT_CENTER позиционирует текст по горизонтали по
центру прямоугольника (рис. 2.1).
Рис. 2.1. Расположение текстовой строки в главном окне приложения
при использовании функции DrawText()
Текст может быть позиционирован также по левому краю
заданной прямоугольной области (константа DT_LEFT) или по
правому краю (константа DT_RIGHT).
18
Если текст состоит из одной строки, а высота области
превышает высоту текста, то строку можно позиционировать и по
вертикали, для чего используются константы DT_VCENTER,
DT_BOTTOM или DT_TOP. Позиционирование по вертикали
требует явного объявления текста однострочным с помощью
константы DT_SINGLELINE. Все константы позиционирования
можно комбинировать с помощью побитовой операции ИЛИ
(символ | языка С++).
Вывод текстовых строк с помощью функции DrawText()
очень удобен в плане задания аккуратного расположения строк,
хотя и оказывается более сложным, чем при использовании
функции TextOut(). Ниже приведен текст функции OnPaint(),
использованной при приложении, вывод которого приведен на рис.
2.1:
void OnPaint(HWND hwnd){
PAINTSTRUCT ps;
RECT r;//Прямоугольная область для вывода строки текста
char s[]="Строка текста";//Сама строка
HDC hdc=BeginPaint(hwnd,&ps);//Получим контекст устройства
r.left=5;//Задаем координаты прямоугольника
r.top=30;//в пределах главного окна
r.right=ps.rcPaint.right-5;//Ради симметрии по горизонтали
r.bottom=80;//Высота прямоугольника почти произвольна
SelectFont(hdc,hFont);//Выбираем крупный шрифт
//(созданный заранее)
SetBkMode(hdc,TRANSPARENT);//Назначаем прозрачность фона
DrawText(hdc,s,strlen(s),&r,//Собственно вывод строки
DT_CENTER|DT_VCENTER|DT_SINGLELINE);//Центрируем
EndPaint(hwnd,&ps);
}
Ширина окна здесь получена из структурной переменной ps,
где она входит в состав элемента rcPaint типа RECT.
3. ТАЙМЕРЫ WINDOWS
Организация и обслуживание таймеров
В прикладных задачах часто возникает необходимость временнόй синхронизации тех или иных процессов. Представим себе,
19
например, что компьютер используется для управления
экспериментальной или производственной установкой. Тогда
одной из функций управляющей программы может быть
периодическое переключение текущего режима работы установки
– значений действующих в ней электрических или магнитных
полей, давлений, температур и т. д. В других случаях может
использоваться “однократная” временная синхронизация, когда
установка включается, отрабатывает заданное время и
выключается по истечении установленного временного интервала.
Если интервал времени необходимо выдержать с высокой
точностью, то для его задания приходится использовать
специальные аппаратные средства (автономные или связанные с
компьютером) – измерители временных интервалов; если же
высокой точности не требуется, то вполне можно воспользоваться
машинным таймером. Непосредственный доступ к физическому
машинному таймеру, как и к другим аппаратным средствам
компьютера, в Windows запрещен, однако Windows предоставляет
прикладному программисту функции, позволяющие установить в
приложении требуемое количество программных таймеров, с
помощью которых приложение может обеспечить временную
синхронизацию и задание временных интервалов.
Приложение устанавливает, или активизирует таймер, вызывая
функцию SetTimer(). Эта функция имеет следующий прототип:
UINT SetTimer(
HWND hwnd,//Дескриптор окна, с которым связан этот таймер
UINT idTimer,//Идентификатор таймера
UINT uTimeout,//Период срабатывания таймера в миллисекундах
TIMERPROC tmprc//Прикладная функция обслуживания таймера
);
Через параметр hwnd системе Windows передается дескриптор
окна, для которого устанавливается данный таймер. Сама функция
SetTimer() может быть вызвана в любом месте программы,
однако, указав дескриптор hwnd, мы связываем таймер с
конкретным окном, в оконную функцию которого будут поступать
сообщения WM_TIMER.
Параметр idTimer определяет номер, который мы
присваиваем данному таймеру. При установке единственного
20
таймера этот номер не имеет значения и на его месте можно
указать 0, но при наличии нескольких таймеров (из которых один,
например, генерирует периодические сигналы, а другой задает
время измерения) номер, присвоенный таймеру, позволяет в
дальнейшем определить, от какого именно таймера пришло данное
сообщение WM_TIMER.
Параметр uTimeout задает период срабатывания данного
таймера. Таймер после установки начинает периодически с
интервалом uTimeout генерировать сообщения WM_TIMER,
поступающие в окно hwnd. Этот временнόй интервал задается в
миллисекундах, что, строго говоря, не имеет смысла, так как
период отсчета времени программным таймером имеет величину
около 50 мс.
Последний параметр, tmprc, дает возможность организовать
обслуживание таймера несколько иначе. Параметр представляет
собой имя CALLBACK-функции, которая должна быть определена в
программе и содержать процедуру прикладной обработки
прерываний от таймера. Если этот параметр указан, то при каждом
срабатывании таймера система Windows будет непосредственно, в
обход оконной функции, вызывать функцию tmprc(). Такой
способ установки таймера можно использовать, например, в тех
случаях, когда в приложении отсутствует главное окно, что,
впрочем, не типично для приложений Windows. Если сообщения от
таймера предполагается обрабатывать обычным образом,
посредством оконной функции, то на месте последнего параметра
функции SetTimer() указывается NULL.
Функция SetTimer() в случае своего успешного выполнения
(нормальной установки таймера) возвращает номер данного
таймера, т. е. фактически значение второго параметра.
Как уже отмечалось, таймер после своей установки начинает
периодически генерировать сообщения с заданным интервалом;
если
таймер
надо
остановить,
используется
функция
KillTimer() с дескриптором окна и номером таймера в качестве
параметров. В частности, можно остановить таймер в прикладной
функции обработки его сообщения. В этом случае мы получим
режим задания однократного временнόго интервала.
21
Приведем в качестве примера скелетную схему программы, в
которой требуется в течение заданного времени периодически
выводить на экран некоторую информацию. Нас будут
интересовать три прикладные функции: OnCreate(), в которой
устанавливается таймер, OnTimer(), где периодически
выполняются некоторые содержательные действия, например,
чтение из установки текущего измеряемого значения, и
OnPaint(), где это значение выводится в главное окно
приложения. Поскольку эти функции будут вызываться в ответ на
сообщения WM_CREATE, WM_TIMER и WM_PAINT, в составе
оконной функции главного окна должны в этом случае
присутствовать макросы HANDLE_MSG, соответствующие этим
сообщениям.
//Глобальные переменные
int timeout=60;//60 замеров по 2с, всего 2 мин
int data//Переменная для получения измерительных данных
BOOL OnCreate(HWND hwnd, LPCREATESTRUCT){
SetTimer(hwnd,1,2000,NULL)//Таймер №1, период 2с
}
void OnTimer (HWND hwnd,UINT){
... //Формирование очередного данного в переменной data
InvalidateRect(hwnd,NULL,TRUE);//Инициирование WM_PAINT
timeout--;//Отсчитываем число замеров
if(!timeout) //Если timeout = 0
KillTimer(hwnd,1)//уничтожим таймер №1
}
void OnPaint(HWND hwnd){
... //Вывод в главное окно данного
}
data
Мультимедийные таймеры
Основным недостатком обычных таймеров Windows является
невысокая предельная частота (18,2 Гц) и, как следствие этого,
низкое временное разрешение (55 мс). Если воспользоваться таким
таймером для плавного перемещения изображения какого-либо
объекта по экрану, то при разрешении экрана 800600 пикселов
объект затратит на прохождение всего экрана по горизонтали более
22
40 с, т. е. движение его будет чрезвычайно медленным. Для
получения более высоких скоростей перемещения и для отсчета
интервалов времени с более высокой точностью можно
использовать мультимедийные таймеры, предельное разрешение
которых составляет 1 мс, что соответствует частоте 1 кГц.
Для работы с мультимедийными таймерами предусмотрена
небольшая группа специальных функций, имена которых
начинаются со слова time (начинающегося со строчной буквы, что
нетипично для имен функций Windows).
Рассмотрим несколько вариантов установки и использования
мультимедийного таймера.
Измерение интервалов времени
В процессе оптимизации программ может возникнуть
необходимость измерить время выполнения того или иного
фрагмента программы. Для этого можно использовать
мультимедийную функцию timeGetTime(), которая возвращает
время в миллисекундах, истекшее от последней загрузки
операционной системы:
DWORD t1,t2,t3;//Переменные для записи времени
t1=timeGetTime();
...//Контролируемый фрагмент программы
t2=timeGetTime();
t3=t2-t1;//Время выполнения контролируемого фрагмента в мс
Организация периодического процесса
Установка и использование мультимедийного таймера требует
выполнения целого ряда действий. Прежде всего с помощью
функции timeBeginPeriod() задается требуемое временное
разрешение устанавливаемого таймера в миллисекундах. Хотя
минимальное значение параметра этой функции составляет 1 мс,
однако следует иметь в виду, что установленный таймер активно
использует ресурсы операционной системы и при малом значении
времени разрешения или при установке нескольких таймеров
системе может не хватить ресурсов, что приведет к ее аварийной
остановке.
Следующим этапом является установка временного события,
которая выполняется с помощью функции timeSetEvent(). В
23
качестве параметров этой функции указывается, в частности,
временной интервал срабатывания таймера, а также адрес той
прикладной функции обратного вызова, которая будет
активизироваться при каждом его срабатывании.
Уничтожение мультимедийного таймера требует вызова двух
функций: timeEndPeriod(), отменяющей установленное ранее
разрешение таймера, и timeKillEvent(), которая прекращает
действие всех системных процессов, связанных с работой
мультимедийного таймера.
Фрагмент
программы,
в
которой
устанавливается
мультимедийный таймер, может выглядеть таким образом:
timeBeginPeriod(1);//Установим максимальное разрешение
MMRESULT mmr=timeSetEvent(5,1,TimeProc,0,TIME_PERIODIC);
...//Продолжение программы
В
качестве
параметров
функции
timeSetEvent()
указывается период его срабатывания (5 мс в примере), значение
установленного ранее разрешения (1 мс), имя прикладной функции
обработки прерываний от таймера (например, TimeProc),
произвольное данное пользователя, которое будет передано в эту
функцию (у нас 0), а также символическая константа, задающая
режим работы таймера. Функция установки таймера возвращает (в
переменную типа MMRESULT) его номер, назначаемый системой и
используемый затем нами при уничтожении данного таймера.
Прикладная функция TimeProc(), вызываемая в данном
примере каждые 5 мс, предназначена для выполнения требуемых
периодических действий. Сложность, однако, заключается в том,
что в этой функции запрещен вызов каких-либо функций Windows,
кроме мультимедийных, а также функции PostQuitMessage().
В результате типичный текст функции TimeProc() выглядит следующим образом:
void CALLBACK TimeProc(UINT,UINT,DWORD,DWORD,DWORD){
PostMessage(hwnd,WM_USER,(WPARAM)parm1,(LPARAM)parm2);
}
Вызов функции PostMessage() приводит к посылке в окно
hwnd нашего приложения сообщения пользователя с кодом
WM_USER, в состав которого входят два произвольных параметра
24
parm1 и parm2. Для Windows код WM_USER (он равен 0x400), при
использовании его в рамках окон прикладных классов, ничего не
значит, так как стандартные сообщения Windows имеют коды от 0
до WM_USER–1, однако мы можем обрабатывать сообщение
WM_USER в нашей оконной функции наравне с остальными
(системными) сообщениями:
LRESULT CALLBACK WndProc(HWND hwnd,UINT msg,
WPARAM wParam,LPARAM lParam){
switch(msg){
HANDLE_MSG(hwnd,WM_CREATE,OnCreate);
HANDLE_MSG(hwnd,WM_PAINT,OnPaint);
HANDLE_MSG(hwnd,WM_DESTROY,OnDestroy);
... //Обработка других сообщений Windows
default:
return(DefWindowProc(hwnd,msg,wParam,lParam));
case WM_USER:
OnUser(wParam,lParam);//Вызов прикладной функции
//обработки сообщений от мультимедийного таймера
}
}
На выполнение прикладной функции OnUser(), которой через
ее аргументы wParam и lParam передаются (при необходимости)
наши параметры parm1 и parm2, уже не накладывается никаких
ограничений; в ней можно выполнять любые действия и, в
частности, вызывать любые функции Windows. Следует только
иметь в виду, что функция OnUser() не вызывается
непосредственно прерыванием от таймера; сообщение WM_USER
поступает в очередь сообщений приложения, а функция OnUser()
будет вызвана, лишь когда дойдет очередь до обработки этого
сообщения. Описанный здесь механизм иногда называют
отсроченной или отложенной обработкой.
После того, как необходимость в периодических действиях
отпала, мультимедийный таймер необходимо уничтожить:
timeEndPeriod(1);//Указывается заданное ранее разрешение
timeKillEvent(mmr);//Указывается номер данного таймера
Задание однократного интервала времени
При необходимости отработки однократного интервала времени
необходимо выполнить все описанные выше действия, только в
25
качестве режима установки временного события указывается
константа TIME_ONESHOT:
timeBeginPeriod(1);//Установим максимальное разрешение
mmr=timeSetEvent(1000,1,TimeProc,0,TIME_ONESHOT);
...//Продолжение программы
void CALLBACK TimeProc(UINT,UINT,DWORD,DWORD,DWORD){
PostMessage(hwnd,WM_USER,(WPARAM)parm1,(LPARAM)parm2);
time EndPeriod(1);//Отмена установленного ранее разрешения
..timeKillEvent(mmr);//В предположении, что mmr – глобальная
}
// переменная
В данном варианте сообщение WM_USER посылается лишь один
раз по истечении точно 1с (с погрешностью в 1 мс). В прикладной
функции TimeProc обработки этого сообщения, помимо
активизации содержательных действий (посредством посылки
сообщения WM_USER), необходимо отменить установленное ранее
временное разрешение, как это и показано в приведенных выше
строках.
4. ДОЧЕРНИЕ ОКНА
Создание и использование дочерних окон
Любое реальное приложение Windows содержит большое
количество окон. Хотя все графические элементы, составляющие
экранный кадр приложения (текстовые строки, геометрические
фигуры, растровые изображения) можно вывести непосредственно
в главное окно, такой способ формирования экранного кадра
обычно оказывается неудобным, главным образом из-за
невозможности раздельного взаимодействия (с помощью мыши
или клавиатуры) с этими элементами. Организация же системы
вложенных, или порожденных, окон позволяет для каждого такого
окна или группы окон иметь, например, свою форму курсора или
свой цвет фона; главное же достоинство порожденных окон
заключается
в
возможности
придания
каждому
окну
индивидуальной оконной функции. Это позволяет отдельно и поразному обрабатывать для каждого окна поступающие в него
26
сообщения Windows (или, наоборот, посылать в окна программно
сформированные сообщения).
Так, на рис. 4.1 приведен для примера возможный вид главного
окна программы, управляющей некоторой измерительной
установкой. В окне имеется поле для ввода экспозиции,
поясняющая надпись “Введите Экспозицию:”, кнопка пуска, а
также большое дочернее окно для динамического вывода значения
времени, оставшегося до конца сеанса измерения.
Рис. 4.1. Дочернее окно в главном окне приложения
Реально почти все изображения, которые мы видим на экране, –
разнообразные кнопки, полосы прокрутки, ползунки, поля для
ввода текста, изображения различных приборов и индикаторов –
представляют собой окна с теми или иными характеристиками.
Обычно эти окна образуют иерархическую систему. Например,
любой диалог прежде всего представляет собой окно; однако в нем
обычно имеются еще и внутренние окна – кнопки, списки, полосы
прокрутки и т. д.
Модальные диалоги, широко используемые в приложениях
Windows в качестве средства управления работой программы,
относятся к числу всплывающих окон, что определяется указанием
при описании их стиля константы WS_POPUP. Модальные
диалоговые окна могут быть только всплывающими. Вообще же
вложенные окна, в частности немодальные диалоги, могут быть как
всплывающими, так и дочерними; в последнем случае в описании
их стиля присутствует константа WS_CHILD. Всплывающие и
дочерние окна характеризуются следующими различиями:
 если в главном окне приложения одновременно образованы и
всплывающие, и дочерние окна, то всплывающие окна будут
27
изображаться поверх дочерних, “всплывать” над ними, что и
определило их название;
 дочерние окна могут перемещаться только в пределах
родительского окна, в то время как положение всплывающих
окон не ограничено какими-либо границами; всплывающее
окно можно вытащить за пределы главного окна приложения
и поместить в любом месте экрана (для чего необходимо,
чтобы у всплывающего окна, кроме рамки, была еще и полоса
заголовка);
 координаты дочерних окон задаются относительно границ
рабочей
области
родительского
окна;
координаты
всплывающих окон задаются относительно границ экрана;
 всплывающее окно может содержать собственное меню; в
дочерних окнах меню не бывает.
Указанные различия делают всплывающие окна более удобными
для организации активного диалога с пользователем, в процессе
которого он работает с органами управления окном, вводит
входные данные в соответствующие поля, выбирает требуемые
режимы и т. д. Дочерние окна часто используются как
информационные, а также для придания отдельным областям
главного окна специфических свойств. Выделив, например, в
тексте справки некоторые ключевые слова в отдельные окна,
можно с классом этих окон связать курсор характерной формы
(обычную стрелку заменить на изображение ладони), задать в
контексте устройства другой цвет символов и вдобавок определить
функцию обработки сообщений WM_LBUTTONDOWN, чтобы при
щелчке мышью по выделенному слову вызывался поясняющий это
слово текст.
Диалоговые окна относятся к окнам предопределенных классов,
описанных в Windows. Это ограничивает их возможности, но
упрощает взаимодействие с элементами управления, входящими в
состав диалоговых окон, так как в Windows для этого
предусмотрены стандартные средства.
В тех случаях, когда вложенное окно должно обеспечивать
более широкие возможности, целесообразно создать в программе
не диалоговое, а обычное порожденное окно и наделить его
требуемыми характеристиками. При этом, если форма диалоговых
окон описывается в файле ресурсов, а создание осуществляется
28
функциями
DialogBox()
или
CreateDialog(),
то
порожденные (как дочерние, так и всплывающие) окна
описываются непосредственно в программе, а создаются
универсальными
функциями
CreateWindow()
или
CreateWindowEx(). Для определенности мы далее будем
говорить о дочерних окнах, хотя фактически весь этот материал в
равной степени относится и к окнам всплывающим.
Процедура создания и обслуживания дочернего, как и любого
другого, окна состоит из трех этапов:
 регистрации класса дочернего окна, в процессе которой за
окном закрепляется его оконная функция;
 создания окна функцией Windows CreateWindow() с
указанием стиля окна и его местоположения;
 обработки сообщений, поступающих в дочернее окно.
Классы всех дочерних окон удобно регистрировать в функции
WinMain() вслед за регистрацией класса главного окна;
создаются же дочерние окна обычно в функции OnCreate(),
вызываемой Windows в процессе создания главного окна.
Сообщения, поступающие в главное окно, обрабатываются, как
известно, оконной функцией главного окна; для обработки
сообщений, поступающих в дочерние окна, должна быть
предусмотрена отдельная оконная функция. Если при этом
дочерних окон несколько, и все они обладают существенно
разными характеристиками, для них предусматривают отдельные
классы и, соответственно, отдельные оконные функции. Если же
дочерние окна более однородны, все они могут принадлежать
одному классу, и тогда оконная функция для всех дочерних окон
будет одна, а различать отдельные окна придется по их
дескрипторам.
На рис. 4.2 приведен вид главного окна приложения, назначение
которого – вывод на экран информации из базы данных пациента.
29
Рис. 4.2. Главное окно приложения с несколькими дочерними окнами
В главном окне образовано одно большое дочернее окно (с
рамкой и полосой заголовка) для вывода данных в графической
форме и пять “управляющих” окон меньшего размера с названиями
отображаемых данных. Управляющие окна не имеют ни заголовка,
ни рамки, и поэтому никак не выделяются на общем фоне главного
окна. Их размеры соответствуют размерам выводимых в них строк
текста. Все они принадлежат одному классу, для которого задана
специфическая форма курсора – изображение кисти руки. В
единую оконную функцию этих окон включена обработка
сообщения WM_LBUTTONDOWN. При щелчке мышью по той или
иной строке с названием данного в большое дочернее окно
выводится соответствующая информация в виде графика.
Таким образом, в данном приложении зарегистрированы классы
трех окон – главного окна, большого окна для вывода графика и
пяти управляющих окон с названиями данных. Соответственно, в
приложении имеются и три оконных функции. Легко сообразить,
что оконная функция главного окна почти пуста – в ней может
обрабатываться одно единственное сообщение WM_DESTROY о
завершении приложения. Оконная функция окна графика содержит
обработку сообщений WM_PAINT, а окна с названиями данных, как
уже отмечалось, должны обрабатывать сообщения WM_LBUTTONDOWN.
30
Приведем теперь в качестве образца слегка сокращенный текст
простой программы, в которой, кроме главного окна,
предусмотрено еще одно дочернее окно для вывода графика (рис.
4.3).
Рис. 4.3. Главное окно приложения с одним дочерним окном
#include <windows.h>
#include <windowsx.h>
/*Глобальные переменные*/
HINSTANCE hI;
char szClassName[]="MainWindow";
char szDataClassName[]="DataWindow";
char szTitle[]="Дочернее окно в главном";
/*Главная функция приложения*/
int WINAPI WinMain(HINSTANCE hInst,HINSTANCE,LPSTR,int){
hI=hInst;
MSG msg;
WNDCLASS wc;
ZeroMemory(&wc,sizeof(wc));
/*Регистрируем класс главного окна*/
wc.lpszClassName=szClassName;
wc.hInstance=hInst;
wc.lpfnWndProc=WndProc;
wc.hCursor=LoadCursor(NULL,IDC_ARROW);
wc.hIcon=LoadIcon(NULL,IDI_APPLICATION);
wc.hbrBackground=GetStockBrush(WHITE_BRUSH);
RegisterClass(&wc);
/*Регистрируем класс дочернего окна*/
31
wc.lpszClassName=szDataClassName;//Другой класс
wc.lpfnWndProc=DataWndProc;//Другая оконная функция
wc.hbrBackground=GetStockBrush(LTGRAY_BRUSH);//Другой цвет
RegisterClass(&wc);//Регистрируем
/*Создаем главное окно обычным образом*/
HWND hwnd = CreateWindow(szClassName,szTitle,
WS_OVERLAPPEDWINDOW,10,10,228,280,HWND_DESKTOP,
NULL,hInst,NULL);
ShowWindow(hwnd,SW_SHOWNORMAL);//Делаем главное окно видимым
while(GetMessage(&msg,NULL,0,0))//Цикл обработки сообщений
DispatchMessage(&msg);
return 0;
}
/*Оконная процедура главного окна*/
LRESULT CALLBACK WndProc(HWND hwnd,UINT msg,
WPARAM wParam,LPARAM lParam){
switch(msg){
HANDLE_MSG(hwnd,WM_CREATE,OnCreate);
HANDLE_MSG(hwnd,WM_PAINT,OnPaint);
HANDLE_MSG(hwnd,WM_DESTROY,OnDestroy);
default:
return(DefWindowProc(hwnd,msg,wParam,lParam));
}
}
/*Функция обработки сообщения WM_CREATE главного окна*/
BOOL OnCreate(HWND hwnd,LPCREATESTRUCT){
CreateWindow(szDataClassName, NULL,//Создадим дочернее окно
WS_CHILD | WS_DLGFRAME | //Окно дочернее, с толстой рамкой,
WS_VISIBLE,//видимое (не нужна функция ShowWindow())
10,10,200,200,//Положение дочернего окна в главном
hwnd, NULL, hI, NULL);
return TRUE;
}
/*Функция обработки сообщения WM_PAINT главного окна*/
void OnPaint(HWND hwnd){
PAINTSTRUCT ps;
HDC hdc=BeginPaint(hwnd,&ps);
/*Выведем поясняющий текст*/
char str[]="График Декартова листа";
TextOut(hdc,20,220,str,strlen(str));
EndPaint(hwnd,&ps);
}
/*Оконная процедура дочернего окна */
LRESULT CALLBACK DataWndProc(HWND hwnd, UINT msg,
WPARAM wParam,LPARAM lParam){
32
switch(msg){
HANDLE_MSG(hwnd,WM_PAINT,DataOnPaint);
default:
return(DefWindowProc(hwnd,msg,wParam,lParam));
}
}
/*Функция обработки сообщения WM_PAINT дочернего окна*/
void DataOnPaint(HWND hwnd){
PAINTSTRUCT ps;
float x,y,t;
int a=-5;
HDC hdc=BeginPaint(hwnd,&ps);
/*Построим график функции*/
for(t=-50;t<50;t+=0.01){
x=a*t/(1+t*t*t)*30;
y=a*t*t/(1+t*t*t)*30;
Rectangle(hdc,x+100,y+100,x+102,y+102);
}
EndPaint(hwnd,&ps);
}
Как видно из текста программы, оконная функция дочернего
окна и объявляется, и выглядит так же, как и для главного окна. В
ней удобно использовать макрос HANDLE_MSG; единственным
обрабатываемым здесь сообщением является WM_PAINT
(поскольку дочернее окно мы отдельно закрывать не намерены, в
оконной
функции
отсутствует
обработка
сообщения
WM_DESTROY). Разумеется, при необходимости можно было
обрабатывать и другие сообщения для этого окна.
Главное окно мы обычно сначала создаем функцией CreateWindow(), а затем делаем видимым вызовом функции ShowWindow(). Дочернее окно можно сразу вывести на экран, указав в
его стиле, среди прочих, константу WS_VISIBLE. В этом случае
отпадает необходимость в вызове для него функции
ShowWindow() (впрочем, таким же приемом можно было
воспользоваться и при создании главного окна). Константа
WS_DLGFRAME делает дочернее окно выпуклым, что характерно
для диалоговых окон. При использовании, например, константы
WS_BORDER мы получили бы менее красивое “плоское” дочернее
окно в тонкой черной рамке.
33
Окна предопределенных классов в главном окне
До сих пор мы сталкивались с такими формами приложений
Windows:
 приложение с главным окном;
 приложение без главного окна на основе модального диалога;
 приложение с главным окном и вложенным диалоговым
окном (модальным или немодальным);
 приложение с главным окном и вложенными дочерними
окнами.
В действительности в рамках главного окна приложения можно
иметь не только вложенные дочерние или всплывающие окна,
созданные в программе, но и любые окна предопределенных в
Windows классов – кнопки, списки, статические элементы с
текстом и др. Таким образом, рассмотренные нами ранее элементы
управления необязательно должны быть элементами диалога; с
таким же успехом их можно использовать в составе главного окна
приложения или любых вложенных окон. На рис. 4.1 были
показаны два окна такого рода – окно для ввода текста, в данном
случае числа, задающего время экспозиции, и обычная кнопка
“Пуск”.
Для создания окон предопределенных классов так же, как и
любых других, используется функция CreateWindow(). Так,
окна, показанные на рис. 4.1, созданы следующим образом:
HWND hwndEdit=CreateWindow("EDIT",NULL,
WS_CHILD|WS_VISIBLE|WS_BORDER|ES_NUMBER,
162,10,45,20, hwnd, NULL, hInst, NULL);
HWND hwndBtn= CreateWindow("BUTTON","Пуск",
WS_CHILD|WS_VISIBLE|BS_PUSHBUTTON,
162,40,45,35, (HMENU)ID_GO, NULL, hInst, NULL);
В качестве класса окна указывается один из предопределенных
классов (“EDIT”, “BUTTON”, “LISTBOX” и т. д.). В составную
константу, определяющую стиль окна, могут входить как
константы общего назначения (WS_CHILD, WS_VISIBLE,
WS_BORDER), так и характерные только для окон данного класса.
Так, константа ES_NUMBER разрешает ввод в окно редактирования
только десятичных цифр, а константа BS_PUSHBUTTON
34
определяет внешний вид и функциональное назначение кнопки
(“нажимаемая” кнопка).
Обычно элементам управления (кнопкам, спискам и проч.)
присваивают идентификаторы – произвольные символические
константы. Идентификаторы удобны тем, что при поступлении в
окно приложения сообщения WM_COMMAND, возникшего в
результате воздействия пользователя на тот или иной элемент
управления, среди параметров этого сообщения передается и
идентификатор активизированного элемента. Выполняя с помощью
конструкции switch-case анализ этого параметра, программа
определяет, на какой конкретно элемент управления воздействовал
пользователь. Если, например, в диалоговом окне имеются две
кнопки с идентификаторами BTN_1 и BTN_2, то функция OnCommand() будет выглядеть так:
void OnCommand(HWND hwnd,int id,HWND,UINT){
switch(id){
case BTN_1:
...//Действия при нажатии на кнопку с идентификатором
break;
case BTN_2:
...//Действия при нажатии на кнопку с идентификатором
break;
...//И т. д.
BTN_1
BTN_2
Если элементы управления входят в состав диалогового окна, то
идентификаторы назначаются им в предложениях файла ресурсов:
CONTROL "Пуск", BTN_1, "BUTTON", BS_PUSHBUTTON, 27,32,43,12
Если, однако, элемент управления создается непосредственно в
программе, как дочернее окно предопределенного класса,
идентификатор должен быть назначен в процессе его создания. В
этом случае идентификатор указывается в качестве восьмого по
счету параметра функции CreateWindow() (см. приведенное
выше предложение создания кнопки, которой назначается
идентификатор ID_GO). Однако согласно прототипу функции
CreateWindow() восьмой параметр должен иметь типа HMENU.
Поэтому при подстановке в качестве этого параметра
символического обозначения идентификатора его следует явным
35
образом преобразовать в тип HMENU, что и сделано в приведенном
выше фрагменте.
5. ВЫВОД РАСТРОВЫХ ИЗОБРАЖЕНИЙ
Важнейшим
изобразительным
средством
графической
операционной
системы
являются
цветные
изображения
разнообразных объектов. Рисунки используются для повышения
наглядности курсоров, кнопок, пиктограмм, заставок и других
элементов интерфейса; служат иллюстративным материалом для
интерактивных учебников и справочников; входят органической
частью в состав приложений, предназначенных для управления
производственными
процессами
или
измерительными
установками.
Поскольку
светящаяся
поверхность
видеомонитора
представляет собой набор точек (точечный растр), любое
изображение в конечном счете является точечным. Поэтому
приобретают особую важность принципы создания, хранения и
вывода на экран точечных, или растровых, изображений. В
простейшем случае, для монохромной видеосистемы, каждая точка
экрана описывается одним битом посылаемых на экран данных:
если бит установлен, точка светится, если сброшен – погашена. Для
современных цветных видеосистем это уже не так, каждая точка
описывается обычно тремя байтами, характеризующими ее цвет,
однако старое название растровых изображений – битовые
матрицы или, иногда, битовые карты (от англ. bitmap) осталось в
обиходе.
Процедура вывода растрового изображения
Имеется несколько возможностей хранения исходных
растровых изображений:
 в файлах с расширением .BMP, создаваемых обычно с
помощью того или иного графического редактора, например
пакета CorelDraw от Corel Corporation, встроенной в Windows
программы Paint или одного из универсальных редакторов
ресурсов, входящих в пакеты сред программирования Borland
C++, Visual C++ и др.; файл можно также получить путем
36
сканирования фотографии с переводом результата в формат
.BMP;
 в файле ресурсов в виде таблиц чисел, которые можно
получить с помощью того же редактора ресурсов;
 непосредственно в полях данных приложения в виде
массивов целых чисел.
Обычно используется первый вариант. Если исходное
изображение хранится в файле .BMP, то загрузить его в память
можно двумя способами. Первый требует использования файла
ресурсов, в котором должно быть предложение, описывающее
файл с изображением:
pict BITMAP "apple.bmp"
На имя этого ресурса (pict в данном примере) программа
будет ссылаться при загрузке изображения в память. Как известно,
при компиляции проекта, в который входит файл ресурсов,
фактически выполняются две операции компиляции: исходного
текста программы с образованием объектного файла с
расширением .OBJ, и файла ресурсов с образованием второго
промежуточного файла с расширением .RES. Этот файл содержит
изображение практически в том же виде, что и исходный файл
.BMP. Далее файлы .OBJ и .RES обрабатываются компоновщиком,
который объединяет файлы .OBJ и .RES и создает загрузочный
файл .EXE, готовый к выполнению. Ресурсы (не только
изображения, но и описание диалогов, меню, текстовых строк и
проч.) сохраняются в файле .EXE в специальном формате,
понятном
редактору
ресурсов.
Это
дает
возможность
редактировать ресурсы в файле .EXE, не имея исходного текста
программы.
Для того чтобы изображение можно было вывести на экран, оно
должно находиться не в файле, а в памяти. Загрузка изображенияресурса из файла .EXE в память осуществляется в программе
вызовом функции LoadImage(). После этого программа может
приступить к процедуре вывода изображения в окно. Отметим, что
в этом случае для выполнения программы файл .BMP уже не
нужен; изображение хранится в загрузочном файле, который в силу
этого может иметь значительный размер.
37
Другой способ загрузки изображения в память не требует
наличия файла ресурсов; загрузка изображения осуществляется той
же функцией LoadImage()непосредственно из файла .BMP.
Однако в этом случае приложение может выполняться, только если
ему доступны все необходимые файлы с рисунками, поскольку в
загрузочном файле рисунков нет. Легко сообразить, что именно
такая методика используется в программах, позволяющих
просматривать рисунки, хранящиеся на дисках. Оба описанных
способа схематически изображены на рис. 5.1.
Рис. 5.1. Загрузка растрового изображения в память:
а – с помощью файла ресурсов, б –непосредственно из файла .BMP
Область памяти, куда загружается изображение, должна иметь
особую организацию, совместимую с представлением на экране
окна приложения. Такая память называется совместимой (compatible), и функция LoadImage(), загружая изображение из файла,
38
одновременно
создает
область
совместимой
памяти
соответствующего изображению размера. После этого нам остается
только скопировать изображение из совместимой памяти в окно
приложения, для чего предусмотрена функция BitBlt() (от Bit
block transfer, блочная передача битов). Эта функция отличается
высокой эффективностью, обеспечивая быстрый вывод на экран
даже больших изображений. Как правило, копирование
изображения из совместимой памяти в окно осуществляется в ответ
на сообщение WM_PAINT, поскольку только в этом случае
обеспечивается правильное восстановление содержимого окна
после его затирания.
Как известно, при выводе в окно приложения любых
графических образов (строк текста, геометрических фигур,
отдельных точек) необходимо использовать контекст устройства
(фактически – контекст окна). В контексте хранятся дескрипторы
всех используемых графических инструментов (перьев, кистей и
шрифтов). Для работы с совместимой памятью для нее также
необходимо создать контекст устройства, который в этом случае
называется совместимым контекстом. В совместимый контекст
должен быть загружен дескриптор совместимой памяти, т. е.
фактически дескриптор выводимого в окно растрового
изображения.
Таким образом, для вывода на экран (точнее, в окно приложения)
растрового изображения в памяти должны иметься три объекта:
 совместимая память, в которой находится изображение;
 обычный контекст устройства, посредством которого мы
обращаемся к окну и выводим в него те или иные
графические элементы;
 совместимый контекст, связанный с совместимой памятью,
посредством которого мы обращаемся к этой памяти и
забираем из нее находящееся в ней изображение.
Все эти объекты схематически изображены на рис. 5.2 вместе с
функциями Windows, служащими для создания этих объектов или
работы с ними.
Как уже отмечалось, для загрузки изображения в память
предусмотрена функция LoadImage(), которая используется в
двух вариантах. Для загрузки изображения из выполнимого файла
39
.EXE (т. е. посредством файла ресурсов) вызов функции выглядит
следующим образом:
HBITMAP hBitmap = (HBITMAP)LoagImage
(hInstance, "pict", IMAGE_BITMAP, 0,0,0);
Здесь hInstance – дескриптор экземпляра приложения,
передаваемый в программы через первый параметр функции WinMain(), а pict – имя нашего ресурса в файле ресурсов.
Последний параметр функции предназначен для указания флагов
загрузки, которые в данном случае не нужны, а два параметра
перед ним позволяют задать размеры создаваемой совместимой
памяти. При нулевых значениях этих двух параметров выделяется
столько памяти, сколько требуется для хранения изображения.
40
Рис. 5.2. Процедура вывода растрового изображения
Функция LoadImage() позволяет загружать не только
растровые изображения, но также курсоры и пиктограммы.
Поскольку дескрипторы этих графических объектов принадлежат к
разным типам данных (HBITMAP, HCURSOR и HICON),
возвращаемое функцией значение следует преобразовать в тип
загружаемого объекта. В нашем случае это тип HBITMAP.
41
Во втором варианте, когда файл ресурсов отсутствует, и
изображение загружается непосредственно из файла .BMP,
функция LoadImage() используется в следующем формате:
HBITMAP hBitmap = (HBITMAP)LoagImage
(NULL,"apple.bmp", IMAGE_BITMAP, 0,0, LR_LOADFROMFILE);
Здесь вместо дескриптора экземпляра приложения указывается
NULL, а вместо имени ресурса – имя самого файла с
изображением. Кроме того,
необходимо указать флаг
LR_LOADFROMFILE, определяющий загрузку из файла. Как и
ранее, возвращаемое значение следует явно преобразовать в тип
HBITMAP.
Для работы с совместимой памятью необходимо иметь
совместимый контекст устройства. Он создается с помощью
функции CreateCompatibleDC():
HDC hdcMem = CreateCompatibleDC(hdc):
Обычно совместимый контекст создается в функции OnCreate(); необходимый для этого контекст окна hdc в этом случае
может быть получен с помощью функции GetDC().
Совместимый контекст схож с обычным контекстом окна в том
отношении, что в нем хранится та же информация о текущих
графических инструментах (дескрипторы пера, кисти и шрифта,
цвет шрифта и т. д.). Однако кроме этого в совместимом контексте
имеется дескриптор растра. При создании совместимого контекста
в него по умолчанию помещается дескриптор изображения,
представляющего собой один единственный черный пиксел.
Выбрав в совместимый контекст дескриптор нашего растра
(совместимой памяти), мы получим возможность оперировать с
этой памятью, в частности, копировать ее в окно приложения.
Выбор дескриптора совместимой памяти в совместимый
контекст осуществляется вызовом макроса SelectBitmap()
(или обобщенной функции SelectObject()):
SelectBitmap(hdcMem,hBitmap);
Собственно вывод изображения в окно приложения
осуществляется, как уже отмечалось, функцией BiBlt(), вызов
которой в общем виде можно записать следующим образом:
42
BitBlt(hdc,x,y,w,h,hdcMem,x0,y0,ROP);
Параметры hdc и hdcMem представляют собой дескрипторы
контекстов приемника и источника копируемого изображения. В
данном случае мы копируем из совместимой памяти (дескриптор
hdcMem) в окно приложения (дескриптор hdc).
Параметры x и y задают положение копии в окне-приемнике (от
начала рабочей области окна), а параметры w и h – размер
копируемой части изображения. Если w или h меньше размеров
изображения, в окно скопируется только его часть. Параметры x0
и y0 определяют координаты начальной точки копирования в
изображении-источнике.
Наиболее
естественно
положить
x0=y0=0, чтобы скопировать изображение целиком от самого его
начала, но можно и вырезать из исходного изображения любую
прямоугольную область.
Функция BitBlt() позволяет не только выводить в окна
приложения нарисованные заранее растровые изображения, но и
закрашивать область копирования, а также выполнять над битами
изображения и битами области копирования ряд логических
операций (НЕ, ИЛИ, И и ряд более сложных). С их помощью
можно, в частности, стирать выведенное ранее изображение. Вид
операции, выполняемой над изображением (вид растровой
операции), определяется значением последнего параметра ROP
функции BitBlt(). При выводе в окно статических изображений
используется операция простого копирования и параметр ROP
имеет значение SRCCOPY (source copy, копирование источника).
При выводе движущихся изображений, когда перед выводом
изображения на новое место надо сначала стереть старое
изображение, используются и другие растровые операции.
Функция BitBlt() требует указания размеров изображения.
Для получения характеристик графических объектов Windows
предусмотрена функция GetObject(), которая возвращает
информацию о запрошенном объекте в соответствующую этому
объекту структуру. Для объекта-растра используется структура
типа BITMAP. Элемент bmWidth этой структуры принимает
значение ширины изображения, а элемент bmHeight – его
высоты. Эти значения можно затем использовать при вызове
функции BitBlt():
43
BITMAP bm;//Переменная для характеристик объекта
HBITMAP hBitmap=(HBITMAP)LoadImage(...);//Загрузка растра
GetObject(hBitmap,sizeof(BITMAP),&bm);//Получение
//характеристик объекта
BitBlt(hdc,10,20,bm.bmWidth,bm.bmHeight,hdcMem,0,0,SRCCOPY);
В приведенном примере все изображение копируется в окно со
сдвигом от начала окна на 10 пикселов по горизонтали и 20 по
вертикали.
Компоновка составных изображений
Выше уже отмечалось, что в совместимом контексте, как и в
любом контексте устройства, имеются дескрипторы графических
инструментов вместе с некоторыми их характеристиками
(например, цветом шрифта или фона под ним). Это дает
возможность рисовать с помощью этих инструментов в
совместимой памяти, как в обычном окне. Выбрав в совместимый
контекст дескриптор изображения, загруженного из файла, мы
можем далее, например, заключить часть изображения в цветную
рамку или вывести поверх него некоторую надпись:
SelectBitmap(hdcMem,hBitmap);//Выбираем в совместимый
//контекст дескриптор совместимой памяти
SetTextColor(hdcMem,RGB(0,0,200);//Изменяем цвет шрифта в
//совместимом контексте
SetTextMode(HdcMem,TRANSPARENT); //Делаем в нем фон прозрачным
char str[]="Образец";//Строка для вывода поверх изображения
TextOut(hdcMem,100,100,str,strlen(str));//Выводим строку
Разумеется, выведенный в совместимую память текст не
появится в окне приложение до тех пор, пока мы не скопируем
совместимую память в окно функцией BitBlt().
Часто возникает необходимость размещения в окне нескольких
изображений либо вывод небольшого рисунка поверх фонового
изображения, которое само хранится в файле .BMP и должно быть
сначала скопировано в окно. В таких случаях создается несколько
областей совместимой памяти и, соответственно, несколько
совместимых контекстов. В каждую область совместимой памяти
загружается свое изображение, а затем эти области либо по
отдельности копируются в окно приложения, либо (что лучше)
44
сначала копируются в отдельную область совместимой памяти
большого размера, а та уже копируется в окно (рис. 5.3).
Рис. 5.3. Объединение нескольких изображений в совместимой памяти и
их копирование в окно приложения
Однако для выполнения такой процедуры необходимо иметь
пустую совместимую память требуемого размера. Для такой
памяти предусмотрена функция CreateCompatibleBitmap():
45
HBITMAP hBitMap=CreateCompatibleBitmap(hdc,800,600);
В этом примере создается совместимая память размером во весь
экран (800600 пикселов). Заметим, что для создания совместимой
памяти таким способом так же, как и при создании совместимого
контекста, требуется иметь контекст окна hdc. Поскольку пустая
совместимая память обычно создается заранее, в функции OnCreate(), контекст hdc получают вызовом функции GetDC().
При создании пустой совместимой памяти с помощью функции
CreateCompatibleBitmap()следует иметь в виду, что реально
эта память отнюдь не пуста, а может быть заполнена любым
“мусором”. Если затем эта память заполняется копируемым в нее
изображением целиком, то “мусор”, разумеется, затирается. Если,
однако, графические объекты выводятся в отдельные участки
большого блока совместимой памяти, ее следует предварительно
очистить. Проще всего это сделать с помощью функции
FillRect(), которая позволяет залить указанную в ее
параметрах прямоугольную область любой кистью (не обязательно
белой):
RECT rect;//Структура,описывающая прямоугольник
rect.left=rect.top=0;//Зальем всю совместимую память
rect.right=800;//размером 800600 пикселов
rect.bottom=600;
HBRUSH hBrush=CreateSolidBrush(RGB(180,250,250));//Кисть
FillRect(hdcMem,&rect,hBrush);//Заливаем
В этом примере вся совместимая память закрашивается бледнобирюзовой кистью.
6. ОБСЛУЖИВАНИЕ ФАЙЛОВ В 32-РАЗРЯДНЫХ
ПРИЛОЖЕНИЯХ WINDOWS
Те или иные операции с файлами используются почти в любом
приложении Windows. Действительно, трудно представить себе
программу, которая вводит информацию только с клавиатуры и выводит ее только на экран. В этом случае объемы обрабатываемых
данных будут невелики, а результаты работы программы
недолговечны. Чаще и исходные для программы данные, и данные,
полученные в результате ее работы, хранятся на дисках в виде
46
файлов; многие программы по ходу своей работы создают еще и
временные файлы для хранения промежуточной информации. При
завершении программы эти временные файлы уничтожаются.
Для
32-разрядных
приложений
системой
Windows
предоставляются как традиционные средства работы с файловой
системой (создание и удаление файлов и каталогов, чтение и
запись, поиск, изменение характеристик и др.), так и
специфические средства, не поддерживаемые в 16-разрядных
приложениях Windows или в MS-DOS. К последним прежде всего
следует отнести проецирование файлов в память, асинхронные
операции с файлами и расширенное толкование понятия
физической памяти как суммы оперативной памяти и страничных
файлов (файлов подкачки).
Базовые операции с файлами
Открытие и создание файла
Перед тем как начать работать с файлом, программа должна
либо его создать заново, либо открыть, если он был создан ранее. И
та и другая операция выполняются в 32-разрядных приложениях
Windows с помощью одной универсальной функции – CreateFile(). При успешном выполнении эта функция возвращает 32разрядный дескриптор файла типа HANDLE, через который потом и
выполняются все операции с данным файлом. Если файл открыть
не удалось, функция CreateFile() возвращает 0xFFFFFFFF.
Функция CreateFile() имеет следующий прототип:
HANDLE CreateFile(
LPCTSTR lpFileName,//Адрес спецификации файла
DWORD dwDesiredAccess,//Режим доступа
DWORD dwShareMode,//Режим разделения
LPSECURITY_ATTRIBUTES lpSecurityAttributes,//Адрес
//структуры с атрибутами защиты
DWORD dwCreationDistribution,//Режим открытия
DWORD dwFlagsAndAttributes,//Атрибуты файла
HANDLE hTemplateFile //Дескриптор файла-шаблона
);
47
Параметр lpFileName описывает спецификацию файла по
обычным правилам, например "Myfile.001", если файл
находится в текущем каталоге, или "C:\\Dir\\1.1", если файл
находится в конкретном каталоге конкретного диска. Обратите
внимание на обозначение пути к файлу. Поскольку в языке С++
символ “\” является управляющим, то для того чтобы ввести этот
символ в символьную строку, например, в качестве разделителя
каталогов в составном пути, следует указывать два таких символа.
Допустимо также использование для разделения элементов пути
одного знака деления: "C:/Dir/1.1"
Параметр dwDesiredAccess при работе с файлами чаще
всего указывается в виде GENERIC_READ | GENERIC_WRITE,
что обеспечивает доступ к файлу как для чтения, так и для записи.
Параметр dwShareMode имеет значение в тех случаях, когда к
файлу могут одновременно обращаться несколько приложений;
если файл будет использоваться в монопольном режиме, на месте
этого параметра указывается 0.
Параметр
lpSecurityAttributes
указывает
на
структурную переменную типа SECURITY_ATTRIBUTES, которая
позволяет задать атрибуты защиты для данного объекта. Этот
параметр имеет смысл, главным образом, в сетевых приложениях;
создавая обычную прикладную программу, которая будет
выполняться на автономной или локальной машине, на месте этого
параметра можно указать NULL.
Параметр dwCreationDistribution может принимать
следующие значения:
CREATE_NEW – создается новый файл; если файл уже
существует, функция CreateFile() возвращает ошибку;
CREATE_ALWAYS – создается новый файл; если файл с
указанным именем уже существует, он затирается вновь
создаваемым;
OPEN_EXISTING – открывается существующий файл; если
такого файла нет, функция CreateFile() возвращает ошибку;
OPEN_ALWAYS – открывается существующий файл; если такого
файла нет, функция CreateFile() создает новый файл с
указанным именем.
48
Параметр dwFlagsAndAttributes позволяет установить
атрибуты и некоторые другие специальные характеристики файла.
В большинстве случаев эти характеристики значения не имеют, и в
качестве этого параметра можно указать 0.
Параметр hTemplateFile может содержать дескриптор
открытого файла-шаблона, атрибуты которого применяются и для
вновь создаваемого файла. Обычно в качестве значения этого
параметра используется NULL.
Таким образом, типичным для прикладной программы будут
следующие формы вызова функции CreateFile():
/*При создании нового файла*/
HANDLE hFile=CreateFile("001.dat",
GENERIC_READ|GENERIC_WRITE,
0, NULL, CREATE_ALWAYS, 0, NULL);
/*При открытии существующего файла*/
HANDLE hFile=CreateFile("001.dat",
GENERIC_READ | GENERIC_WRITE,
0, NULL, OPEN_EXISTING, 0, NULL);
После завершения работы с файлом или перед завершением
программы все открытые в ней файлы следует закрыть. Для этого
используется функция CloseHandle() c указанием в качестве ее
единственного параметра дескриптора закрываемого файла:
CloseHandle(hFile);
Впрочем, в процессе завершения приложения Windows
закрывает все созданные им объекты, в частности файлы, так что
во многих случаях заботу о закрытии файлов можно возложить на
Windows.
Запись и чтение файла
При работе с данными, содержащимися в файле, чаще других
используются три функции: записи в файл WriteFile(), чтения
из файла ReadFile() и установки файлового указателя SetFilePointer().
Функции чтения и записи имеют практически одинаковые
прототипы:
BOOL ReadFile(HANDLE hFile,
LPVOID lpBuffer,
49
DWORD nNumberOfBytesToRead,
LPDWORD lpNumberOfBytesRead,
LPOVERLAPPED lpOverlapped);
BOOL WriteFile(HANDLE hFile,
LPCVOID lpBuffer,
DWORD nNumberOfBytesToWrite,
LPDWORD lpNumberOfBytesWritten,
LPOVERLAPPED lpOverlapped);
Параметр hFile обозначает полученный от функции CreateFile() дескриптор созданного или открытого файла.
Параметр lpBuffer представляет собой адрес программного
буфера, предназначенного для хранения данных, записываемых в
файл или считываемых из файла. Обобщенный тип этого буфера –
LPVOID или LPCVOID – позволяет обмениваться с файлом
данными любой организации – отдельными байтами или
скалярными переменными большей длины, а также массивами и
структурами.
Третий параметр рассматриваемых функций определяет
заказываемый объем пересылаемых данных (в байтах).
Четвертый параметр является адресом программной переменной
типа DWORD, в которую после завершения передачи данных будет
записано истинное число переданных байтов. Этой переменной
можно воспользоваться для контроля правильности выполненной
операции записи или чтения.
Последний параметр, lpOverlapped, используется в тех
случаях, когда операции над файлом предполагается осуществлять
в асинхронном режиме (его еще называют режимом с
перекрытием). Обычно программа, поставив запрос на чтение
данных из файла или запись данных в файл, останавливается до
завершения этой операции, хотя в течение всего этого времени
процессор почти свободен и мог бы выполнять полезную работу.
Такой режим выполнения программы называется синхронным.
Некоторые операционные системы, в частности, Windows NT и
Windows 2000, позволяют выполнять асинхронные операции над
файлами. Суть асинхронного режима заключается в том, что
система, запустив операцию ввода или вывода, возвращает
управление в программу, которая может таким образом
продолжить выполнение параллельно с дисковой операцией ввода-
50
вывода. В случае обычных операций чтения или записи значение
параметра lpOverlapped должно быть равно NULL.
Функция SetFilePointer() установки файлового указателя
служит для перемещения указателя в файле на заданный байт,
чтобы последующая операция чтения или записи выполнила
пересылку данных не с начала файла, а от положения указателя.
Прототип этой функции имеет следующий вид:
DWORD SetFilePointer(HANDLE hFile,LONG lDistanceToMove,
PLONG lpDistanceToMoveHigh,DWORD dwMoveMethod);
Параметр hFile обозначает полученный от функции CreateFile() дескриптор созданного или открытого файла.
Параметр lDistanceToMove задает 32-битовое смещение в
файле до точки установки указателя.
Параметр
lpDistanceToMoveHigh
является
адресом
старшей половины смещения указателя, если смещение
характеризуется 64-битовым значением. Обычно здесь указывается
NULL.
Последний параметр dwMoveMethod определяет способ
установки указателя. Этот параметр может принимать три
значения:
FILE_BEGIN – указатель устанавливается от начала файла и,
следовательно,
рассматривается
как
число
без
знака.
Максимальное смещение в файле (если не использовать параметр
lpDistanceToMoveHigh) достигает 232 - 2 = 4 Гбайт - 2;
FILE_CURRENT – указатель устанавливается от текущей
позиции указателя и, следовательно, рассматривается как число со
знаком, что позволяет смещать указатель как вперед, так и назад от
текущего положения в пределах до 2 Гбайт;
FILE_END – указатель устанавливается от конца файла, причем
смещение может быть как положительным, так и отрицательным.
Рассмотрим содержательную часть простой программы,
создающей новый файл, записывающей в него некоторую
информацию, а затем выполняющей, с целью контроля, чтение
небольшой порции этой информации:
struct GOODS{//Структура, описывающая изделия на складе
int Nmb;//Номер изделия
int Price;//Цена изделия
51
};
const int N=1000000;//Пусть на складе миллион образцов изделий
GOODS Stock[N];//Создадим в памяти массив для всех изделий
GOODS Unit1;//Структурная переменная для одного изделия
DWORD dwCount;//Для функций чтения/записи
... //Заполним массив структур данными
/*Создадим новый файл*/
HANDLE hFile=CreateFile("stock.dat",
GENERIC_READ|GENERIC_WRITE,
0,NULL,CREATE_ALWAYS,0,NULL);
/*Запишем в файл весь массив структурных переменных*/
WriteFile(hFile,Stock,sizeof(Stock),&dwCount,NULL);
/*Сместим указатель на последнюю запись и прочитаем ее */
SetFilePointer(hFile,sizeof(GOODS)*(-1),NULL,FILE_END);
ReadFile(hFile,&Unit1,sizeof(GOODS),&dwCount,NULL);
CloseHandle(hFile);//Закроем файл (нет необходимости)
С таким же успехом и даже более наглядно было бы в
операциях записи, установки указатели и чтения указывать
количество записываемых байтов в виде числа. Поскольку каждое
число типа int требует для своего хранения четыре байта, то
каждая структурная переменная типа GOODS занимает в памяти 8
байт, а весь массив – 8000000 байт. Поэтому запись в файл можно
выполнить таким образом:
WriteFile(hFile,Stock,8000000,&dwCount,NULL);
а, например, установку указателя так:
SetFilePointer(hFile, -8,NULL,FILE_END);
или так:
SetFilePointer(hFile, 7999992,NULL,FILE_BEGIN);
Следует заметить, что при работе с большими массивами
(превышающими по объему 1 Мбайт) надо специально подумать об
их размещении в памяти. Если массив объявлен как локальная
переменная, то для него должно быть выделено место в стеке, а
размер стека по умолчанию ограничивается системой
программирования. Впрочем, изменив настройки системы
программирования, можно увеличить стек до нужного размера.
52
Файлы, проецируемые в память
Как известно, процессор не может работать с содержимым
файла, находящегося на диске, – для обращения к какому-либо
участку файла этот участок необходимо сначала прочитать в
память. Если же требуется не просто обработать данные из файла, а
модифицировать его содержимое, то к операции чтения файла
добавляется еще и операция его записи. Поэтому стандартная для
16-разрядных приложений процедура работы с дисковым файлом
(в наиболее полном виде, когда требуется модификация файла)
включает по меньшей мере четыре этапа:
 открытие файла на диске;
 чтение требуемого участка файла в созданную заранее
программную переменную;
 модификация этой программной переменной по заданному
алгоритму;
 запись модифицированной переменной на то же место в
файле на диске.
Обычно работа с файлом требует обращения не к одному его
участку, а к нескольким, и тогда перечисленные выше операции
приходится выполнять повторно. Во многих случаях, особенно при
обслуживании обширных баз данных, программа по ходу своего
выполнения запрашивает различные участки файла сотни и тысячи
раз, и если эти запросы происходят не упорядоченно, а вразбивку,
как это обычно и бывает, то каждое обращение к файлу требует
выполнения операций физического чтения или записи, что
существенно замедляет скорость выполнения программы,
поскольку скорость работы диска в тысячи раз меньше скорости
процессора.
Значительно более эффективным было бы чтение всего файла в
оперативную память. В этом случае потребуется только одна
операция физического чтения в начале сеанса работы и одна
операция физической записи в конце сеанса. Однако такая
методика возможна только при работе с файлами относительно
небольшого размера.
Windows предоставляет для 32-разрядных приложений
чрезвычайно удобный механизм работы с файлом, вообще
исключающий (с точки зрения программиста) операции
53
физического чтения или записи. Сущность этого механизма,
называемого проецированием файла в память, заключается в том,
что конкретный файл включается в состав физической памяти, а
затем в адресном пространстве приложения резервируется область
достаточного размера и часть физической памяти, включающая в
себя файл, отображается на эту область. Дальнейшие операции с
файлом заменяются операциями с его отображением (проекцией) в
адресном пространстве приложения.
Процедура создания и использования проекции файла (рис. 6.1)
распадается на несколько этапов:
 открытие (или, возможно, создание) файла обычным образом
с помощью функции CreateFile();
 создание в физической памяти объекта “проекция файла” с
помощью функции CreateFileMapping();
 выделение в линейном пространстве области адресов для
проекции файла и отображение этой области на проекцию
файла в физической памяти с помощью функции
MapViewOfFile(), которая возвращает указатель на
выделенную область;
 обращение (чтение или запись) к выделенной области
линейных адресов (фактически смещений) посредством
указателя, так же, как это выполняется для любых массивных
переменных.
54
Рис. 6.1. Процесс проецирования файла в память
Рассмотрим
содержательную
часть
программы,
осуществляющей проецирование файла в память и работу с его
проекцией. Будем считать, что файл с именем 1.dat состоит из
1000 чисел типа int (и занимает, следовательно, на диске
пространство 4000 байт).
/*Операция открытия существующего файла*/
hFile=CreateFile("1.dat",GENERIC_READ,
0,NULL,OPEN_EXISTING,0,NULL);
/*Создание в памяти объекта "проекция файла"*/
HANDLE hMem = CreateFileMapping
(hFile,NULL,PAGE_READONLY,0,0,NULL);
/*Отображение линейных адресов на проекцию файла */
int* ptr = (int*)MapViewOfFile(hMem,FILE_MAP_READ,0,0,0);
/*Чтение участков файла*/
int x=ptr[0];//Чтение первого данного из файла
int y=ptr[999];//Чтение последнего данного из файла
В приведенном примере файл был открыт только для чтения
(параметр
GENERIC_READ
функции
CreateFile()).
Соответственно, такой же атрибут получила проекция файла в
физической памяти (параметр PAGE_READONLY), а также и
55
выделенная
область
линейных
адресов
(параметр
FILE_MAP_READ). Если в проецируемый файл предполагается
выполнять запись новых данных, тогда для соответствующих
параметров
вызываемых
функций
следует указать значения GENERIC_READ|GENERIC_WRITE,
PAGE_READWRITE и FILE_MAP_ALL_ACCESS.
Файл с данными может состоять из данных любого типа.
Поэтому функция MapViewOfFile(), которая должна выполнять
свою работу для любых типов данных, возвращает в соответствии
со своим прототипом обобщенный указатель типа LPVOID. При
использовании возвращаемого этой функцией значения его
необходимо преобразовать в указатель на конкретный тип данных,
содержащихся в файле. В нашем случае файл заполнен числами
типа int, и возвращаемое значение явным образом преобразуется
в тип int*. Если бы файл был заполнен, например, байтовыми
переменными типа char или BYTE, то и указатель на проекцию
файла следовало получить такого же типа:
BYTE* ptr = (BYTE*)MapViewOfFile(...);
Четвертый и пятый параметры функции CreateFileMapping() позволяют задать размер проекции файла. Указание в
качестве параметров нулевых значений заставляет Windows
спроецировать весь файл.
Функция MapViewOfFile() позволяет при необходимости
получить отображение на линейные адреса любой части файла, для
чего следует указать смещение отображения от начала файла
(третий и четвертый параметр функции), а также размер
отображения (пятый параметр). Если требуется спроецировать весь
файл (как в нашем примере), на месте всех этих трех параметров
указываются нули.
7. ПРОЦЕССЫ И ПОТОКИ
Процессом (process) называют экземпляр выполняемой
программы – не ход выполнения программы, а именно саму
программу вместе с ее ресурсами – выделенным ей адресным
пространством, динамически подключаемыми библиотеками и др.
Таким образом, процесс в Win32, в противоречии со смыслом этого
56
слова, не является динамическим объектом – это скорее оболочка,
набор ресурсов, которые нужны для выполнения программы.
Сам же ход выполнения программы называют потоком (thread).
Поток выполняется в рамках владеющего им процесса или, как
говорят, в контексте процесса. Именно потокам операционная
система выделяет кванты процессорного времени. Когда
операционная система запускает приложение, она создает
первичный поток процесса, составляющего это приложение. Таким
образом, любой процесс имеет по крайней мере один поток
выполнения. Первичный поток может запустить дополнительные
потоки, которые в программе оформляются как более или менее
самостоятельные функции. Эти потоки будут выполняться
параллельно, конкурируя за процессорное время. Кроме того,
первичный поток может запустить один или несколько процессов
(программ); эти программы, точнее их потоки, также будут
выполняться параллельно с породившим их родительским потоком.
Создание дочернего процесса
В качестве порожденного, или дочернего процесса может быть
запущено любое приложение Windows, например, Калькулятор или
Блокнот. Разумеется, для прикладных программных комплексов
более полезным является запуск взаимосвязанных прикладных
процессов, между которыми разумным образом разделены
функции всего программного комплекса.
Дочерний процесс создается вызовом функции CreateProcess(). Эта функция требует указания 10 параметров,
большинством из которых в простых программах можно
пренебречь. Однако для запуска дочернего процесса в программе
придется создать и настроить должным образом некоторые
служебные поля данных. Рассмотрим простейшую форму запуска
дочернего процесса, в качестве которого мы используем, например,
Калькулятор CALC.EXE (это не очень интересно по существу, но
зато весьма наглядно).
STARTUPINFO si;//Служебная структурная переменная
PROCESS_INFORMATION pi;//Служебная структурная переменная
ZeroMemory(&si,sizeof(si));//Очистим переменную si
si.cb=sizeof(si);//Заносим в член si.cb размер переменной si
57
CreateProcess("e:\\win98\\calc.exe",NULL,NULL,NULL,
FALSE,0,NULL,NULL,&si,&pi);
Создание дочернего потока
Как уже отмечалось, система, запуская процесс (приложение),
организует в нем первичный поток выполнения. Этот поток может
с помощью функции CreateThread() создать еще один или
несколько потоков, которые будут выполняться параллельно в
контексте владеющего ими процесса (запущенной программы). С
формальной точки зрения каждый поток представляет собой одну
из функций, входящих в состав всего процесса; в отличие от
остальных эту функцию можно назвать функцией потока или
рабочей функцией. Рабочая функция может иметь любое
содержимое, однако ее прототип жестко определен:
DWORD WINAPI Thread(LPVOID);
Как видно из прототипа, функция потока может принять в
качестве параметра из вызывающего потока данное любого типа
(число, адрес массива или структуры и др.) и использовать его
далее по своему усмотрению.
Функция создания (запуска) потока CreateThread() требует
указания шести параметров:
HANDLE CreateThread(LPSECURITY_ATTRIBUTES lpThreadAttributes,
DWORD dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId
);
Параметр lpThreadAttributes задает адрес структуры с
атрибутами защиты потока. В простой прикладной программе эта
структура не нужна, и на месте этого атрибута указан NULL.
Параметр dwStackSize позволяет указать размер стека для
данного потока; если этот параметр равен нулю, размер стека
определяется по умолчанию.
С помощью третьего параметра задается имя рабочей функции
потока. Разумеется, фактическое имя рабочей функции может быть
58
каким угодно, важно только, чтобы ее прототип соответствовал
приведенному выше.
Четвертый параметр служит для передачи в рабочую функцию
произвольных данных из порождающей поток программы. Как уже
отмечалось выше, его тип – LPVOID – позволяет передавать любой
объем данных любого типа.
Параметр dwCreationFlags определяет режим создания
потока. Если для него указать значение CREATE_SUSPENDED,
поток будет создан в спящем состоянии и фактически начнет
выполняться лишь после вызова функции ResumeThread(). При
нулевом значении этого параметра создаваемый поток начнет
выполняться сразу после создания.
Наконец, в качестве последнего параметра указывается адрес
переменной типа DWORD, в которую функция создания потока
вернет его идентификатор.
Создав поток, функция CreateThread() возвращает его
дескриптор типа HANDLE, который затем можно использовать при
выполнении различных операций над потоком, например,
приостановке выполнения потока с помощью функции SuspendThread().
Скелетная схема программы, в которой запускается вторичный
поток, выглядит следующим образом:
DWORD WINAPI MyThread(LPVOID);//Прототип рабочей функции
DWORD dwId;//Переменная для идентификатора потока
HANDLE hThread;//Переменная для дескриптора потока
hThread=CreateThread(NULL,0,MyThread,NULL,0,&dwId);
...//Продолжение программы порождающего потока
/*Рабочая функция потока*/
DWORD WINAPI MyThread(LPVOID){
//Любые действия, предназначенные для выполнения
//параллельно с порождающим потоком
return 0;
}
В рамках учебного пособия затруднительно привести
содержательный пример использования потоков, поскольку
реальные многопоточные программы (для моделирования
процессов, обработки заявок, параллельной обработки данных и
59
др.) оказываются слишком сложными. Однако технику создания и
обслуживания потоков можно продемонстрировать на простом
примере “управляющего” потока, который, независимо от
основной программы, периодически выполняет в ней некоторые
переключения (смену цвета, формы или положения некоторого
объекта).
Рассмотрим приложение, в которой выбором одного пункта
меню дочерний поток создается, а выбором другого –
уничтожается. Будучи запущен, вторичный поток начинает
периодически инициировать вывод в главное окно приложения
текущего времени (минуты и секунды). Приводимый пример
позволит нам проиллюстрировать программное решение
некоторых смежных вопросов: запуск и уничтожение потока по
командам меню, циклическое безостановочное выполнение
рабочей функции потока, взаимодействие функции потока с
главным окном приложения и др. На рис. 7.1 приведен вид
главного окна приложения с открытым меню и выведенным
текущим временем.
Рис. 7.1. Окно приложения с порожденным потоком
/*Заголовочный файл Thread.h */
#define ID_1 101
#define ID_2 102
/*Файл ресурсов*/
#include "Thread.h"
Main MENU{
POPUP "Потоки"{
MENUITEM "ON", ID_1
MENUITEM "OFF", ID_2
}
}
/*Исходный текст программы (выборочно)*/
#include <windows.h>
#include <windowsx.h>
60
#include "Thread.h"
HWND hWnd;//Дескриптор главного окна
HANDLE hThread1;//Дескриптор создаваемого потока
BOOL bFin=true;//Булева переменная для закрытия потока
/*Главная функция приложения*/
int WINAPI WinMain(HINSTANCE hInst,HINSTANCE,LPSTR,int){
... //Регистрация класса окна
hWnd=CreateWindow(szClassName, //Создадим главное окно
"Threads",WS_OVERLAPPEDWINDOW,10,10,120,80,
HWND_DESKTOP,NULL,hInst,NULL);
ShowWindow(hWnd,SW_SHOWNORMAL);//Покажем главное окно
while(GetMessage(&msg,NULL,0,0))//Цикл обработки
DispatchMessage(&msg);
//сообщений
return 0;
}
/*Оконная функция*/
LRESULT CALLBACK WndProc(HWND hwnd,UINT msg,
WPARAM wParam,LPARAM lParam){
switch(msg){
HANDLE_MSG(hwnd,WM_COMMAND,OnCommand);
HANDLE_MSG(hwnd,WM_PAINT,OnPaint);
HANDLE_MSG(hwnd,WM_DESTROY,OnDestroy);
default:
return(DefWindowProc(hwnd,msg,wParam,lParam));
}
}
/*Оконная функция*/
void OnCommand(HWND,int id,HWND,UINT){
DWORD dwIDThread;//Идентификатор потока
switch(id){
case ID_1://Выбрали ON
bFin=true;//Разрешим потоку выполняться
hThread1=CreateThread(NULL,0,Thread1,//Создание потока
NULL,0,&dwIDThread);
break;
case ID_2://Выбрали OFF
bFin=false;//Завершим поток
CloseHandle(hThread1);//Освободим дескриптор потока
RECT rect;//Для координат главного окна
HDC hdc=GetDC(hWnd);//Получим контекст устройства
GetClientRect(hWnd,&rect);//Получим координаты окна
FillRect(hdc,&rect,GetStockBrush//Закрасим окно белым
(WHITE_BRUSH));
61
ReleaseDC(hWnd,hdc);//Освободим контекст устройства
break;
}
}
/*Функция обработки сообщения WM_PAINT*/
void OnPaint(HWND hwnd){
char szT[10];
PAINTSTRUCT ps;
SYSTEMTIME st;
HDC hdc=BeginPaint(hwnd,&ps);
if(hThread1){//Только если есть поток
GetLocalTime(&st);//Получим текущее время
wsprintf(szT,"%d:%d",st.wMinute,st.wSecond);
TextOut(hdc,80,5,szT,strlen(szT));//Выведем в окно
}
EndPaint(hwnd,&ps);
}
/*Функция обработки сообщения WM_DESTROY*/
void OnDestroy(HWND){
bFin=FALSE;//Завершим поток при завершении приложения
PostQuitMessage(0);
}
/*Рабочая функция потока */
DWORD WINAPI Thread1(LPVOID){
while(bFin){//Пока выполнение потока разрешено
InvalidateRect(hWnd,NULL,TRUE);//Инициируем WM_PAINT
Sleep(1000);//Усыпим поток на 1 с
}
return 0;Завершим функцию потока (поток уничтожается)
}
Программа построена обычным образом и состоит из главной
функции WinMain() и оконной функции, в которой обрабатываются сообщения WM_COMMAND, WM_PAINT и WM_DESTROY. При
выборе пункта меню "ON" выполняются два действия:
устанавливается в состояние true вспомогательная булева
переменная bFin, которая управляет циклическим выполнением
рабочей функции потока, и вызовом функции CreateThread()
создается вторичный поток. Дескриптор потока сохраняется в
переменной hThread1. Начиная с этого момента в контексте
приложения параллельно выполняются два потока: первичный,
который “крутится” в цикле обработки сообщений, и вторичный,
62
который инициирует периодический вывод в окно текущего
времени.
Выбор пункта меню “OFF” приводит к выполнению более
сложного алгоритма. Переменная bFin сбрасывается в состояние
false, что приводит к разрыву цикла выполнения рабочей
функции потока и к ее завершению. Далее вызовом функции
CloseHandle() освобождается ненужный более дескриптор
потока. Если этого не сделать, то при повторных запусках потока
выбором пункта меню “ON”, будут создаваться все новые и новые
дескрипторы, что вряд ли разумно.
Последнее действие, выполняемое на этом участке программы,
состоит в очистке главного окна приложения. Если окно не
очистить, то последнее выведенное значение времени так и
останется в окне, путая пользователя. Окно очищается вызовом
функции Fillrect(), которая требует в качестве параметров
дескриптор контекста устройства, адрес структуры типа RECT с
координатами очищаемой области и дескриптор кисти, которой
закрашивается окно. Для получения контекста устройства
используется функция GetDC(), координаты рабочей области
окна определяются с помощью функции GetClientRect(), а
белая кисть для закрашивания берется со “склада” Windows.
Рабочая функция потока очень проста. В ней организован цикл
while до сброса переменной bFin. В цикл включена функция
Sleep(), которая усыпляет поток на указанный интервал времени
(1000 мс). При переводе переменной bFin в состояние false
цикл while разрывается и выполнением предложения return
рабочая функция потока завершает свою работу. Завершение
рабочей функции автоматически приводит к уничтожению самого
потока и освобождению всех занимаемых им ресурсов.
В цикл while включено единственное содержательное
предложение – вызов функции InvalidateRect(), которая
инициирует посылку в главное окно приложения сообщения
WM_PAINT и, соответственно, перерисовку окна.
В функции OnPaint() обработки сообщения WM_PAINT
определяется текущее время, значения минут и секунд
преобразуются в символьную форму, и сформированная строка
текста выводится в окно. Этот алгоритм выполняется только при
63
ненулевом значении дескриптора потока hThread1, что
предохраняет от вывода в окно приложения текущего времени при
запуске программы, когда в окно посылается первое сообщение
WM_PAINT.
Синхронизация потоков
Общие характеристики объектов Windows
Организация программного комплекса, состоящего из нескольких
процессов или потоков, почти неминуемо приводит к необходимости
синхронизации
выполняемых
фрагментов
программы.
Синхронизация может иметь различные аспекты: сообщение потоку о
завершении другого потока или его фрагмента, запрет потоку
обращаться к некоторым данным, пока с ними работает другой поток,
наложение ограничений на количество потоков, использующих одни
и те же данные, и др.
Синхронизация процессов и входящих в них потоков
осуществляется с помощью различных объектов Windows, к
которым относятся как сами процессы и потоки, так и
программные средства, созданные специально для целей
синхронизации: критические секции, мьютексы, семафоры,
события и некоторые другие. Мы рассмотрим только
синхронизацию потоков с помощью самих же потоков, а также
событий.
Для объектов, которые могут служить целям синхронизации, в
системе Windows предусматриваются два состояния – свободное,
или сигнальное (signaled), и занятое, или несигнальное (nonsignaled). Занятое состояние объекта используется для запрета тех или
иных программных действий; свободное состояние, наоборот,
снимает запрет и разрешает эти действия.
Имеются различные способы как изменения состояния
синхронизирующего объекта, так и анализа его состояния. Для
некоторых объектов имеются функции, позволяющие переводить
объект в требуемое (свободное или занятое) состояние. Например,
для объекта “событие” (event) функция SetEvent()
устанавливает объект в свободное (сигнальное) состояние, а
64
функция ResetEvent(), наоборот, сбрасывает его в занятое
состояние. В соответствии с именами этих функций свободное
состояние объекта называют еще установленным (set), а занятое –
сброшенным (reset).
Анализ состояния объекта осуществляется с помощью двух
основных функций синхронизации – WaitForSingleObject()
и WaitForMultipleObjects(). Суть их одинаковая – они
останавливают выполнение потока, “усыпляя” его, если
анализируемый объект занят, и разрешают потоку дальнейшее
выполнение (“будят” поток), как только объект освобождается.
При этом первая из двух упомянутых выше функций позволяет
анализировать состояние только одного объекта Windows, а вторая
функция может анализировать состояние группы объектов.
Усыпление потока является высокоэффективной системной
операцией, которая почти не требует процессорного времени.
Поэтому в то время как один или несколько потоков спят в
ожидании некоторого события, остальные действующие в системе
потоки забирают себе практически все процессорное время. Как
только ожидаемое событие произойдет, спящий поток мгновенно
пробуждается и продолжает свое выполнение.
Функция WaitForSingleObject() имеет следующий
прототип:
DWORD WaitForSingleObject(
HANDLE hObject,//Дескриптор синхронизирующего объекта
DWORD dwTimeout//Лимит времени ожидания в миллисекундах
);
Если нам нужно, чтобы функция ожидала событие
неограниченное время, в качестве второго параметра следует
задать константу INFINITE.
Мы видим, что функция ожидания освобождения объекта
требует указания дескриптора этого объекта. Вообще для объектов
Windows характерно обладание тремя описателями: дескриптором,
именем и состоянием. Как мы уже видели, дескриптор объекта
возвращается функциями, которые этот объект либо создают, либо
открывают; обычно дескриптор используется для обращения к
объекту внутри процесса (хотя, возможно, и из другого потока).
65
Имя объекта, если оно необходимо, также задается объекту на
этапе его создания; оно является общесистемной характеристикой
и служит для обращения к объекту из другого процесса. Что
касается состояния, то для объектов, которые будут
рассматриваться в этом разделе, характерны следующие состояния:
 Поток – состояние сбрасывается при создании потока и
устанавливается при его завершении.
 Событие – требуемое состояние события задается при его
создании функцией CreateEvent(). Кроме того, событие
может быть установлено в процессе выполнения потока
явным образом функцией SetEvent() или сброшено
функцией ResetEvent(). В некоторых случаях, когда
события используются в асинхронных операциях, состояние
события задается системой.
Синхронизация с помощью состояний потока
Представим себе электронную таблицу, в которой хранятся
взаимосвязанные данные, т. е. исходные данные вместе с
результатами их обработки. Ввод новых данных в таблицу или
изменение старых потребует пересчета всей или части таблицы.
Если таблица велика, а формулы, связывающие данные, сложны,
этот пересчет может занять весьма значительное время, и в течение
этого времени программа будет “висеть”, не позволяя
пользователю вводить новые данные. Для повышения
эффективности программы можно алгоритм пересчета данных
выделить в отдельный поток, который будет выполняться
параллельно с основным. Тогда при вводе данных, например, с
клавиатуры, когда процессор практически свободен, процессорное
время будет отдаваться потоку пересчета, не замедляя ввод новых
данных. Если еще воспользоваться встроенной в Windows
системой приоритетов и назначить потоку пересчета низкий
приоритет, то эффективность программы еще более повысится, так
как поток пересчета не будет претендовать на время процессора,
пока выполняются какие-либо более приоритетные потоки. Однако
возникновение любой паузы в выполнении приоритетных потоков
приведет к активизации потока пересчета.
66
Однако в описываемой задаче может возникнуть ситуация,
когда первичный поток, дойдя до некоторой точки, не может
продолжить выполнение, пока не будут пересчитаны все данные.
Если считать, что вторичный поток завершается, закончив пересчет
данных, а при вводе новых данных запускается снова, то в
рассматриваемом случае первичный поток должен остановиться в
“критической точке” (когда ему понадобились обновленные
данные) и ждать завершения вторичного. Взаимодействие таких
потоков вместе с функциями, которые его организуют, показано на
рис. 7.2.
Рис. 7.2. Синхронизация с помощью объекта “поток”
В некотором месте приложение (строго говоря, его первичный
поток) создает вторичный поток с рабочей функцией Calc(), и
начиная с этого момента в системе параллельно выполняются оба
потока, и первичный, и вторичный. В той точке, где приложению
требуются результаты выполнения потока Calc(), вызывается
функция ожидания одного объекта WaitForSingleObject(),
которая останавливает дальнейшее выполнение первичного потока
в ожидании установки состояния вторичного. Однако пока
67
вторичный поток выполняется, его состояние сброшено, и
первичный
поток,
“застряв”
на
функции
WaitForSingleObject(), будет находиться в спящем
состоянии. Завершение рабочей функции Calc() приводит к
завершению вторичного потока, установке его состояния и снятию
блокировки первичного потока, который может теперь
воспользоваться результатами вычислений, выполненных во
вторичном потоке.
Синхронизация с помощью событий
Объект “событие” относится к числу весьма эффективных
средств синхронизации потоков и процессов. С помощью этого
объекта поток может уведомить другой поток об окончании любой
операции или вообще о возникновении любой оговоренной заранее
ситуации, могущей повлиять на выполнение другого потока.
Другими словами, события не привязаны к определенным
действиям, например, завершению процесса, окончанию операции
ввода-вывода и др. Программист имеет возможность устанавливать
и сбрасывать события в любой точке программы, организуя тем
самым взаимосвязь потоков нужным ему образом. Заметим, что,
поскольку при создании события ему можно дать произвольное
имя и затем открыть это событие в другом процессе, события
можно использовать для синхронизации не только потоков одного
процесса, но и самостоятельных процессов, точнее, потоков,
работающих в контексте самостоятельных процессов.
Перед тем, как использовать событие в целях синхронизации,
его надо создать. Событие создается с помощью функции CreateEvent() со следующим прототипом:
HANDLE CreateEvent(
LPSECURITY_ATTRIBUTES
lpEventAttributes,//Адрес атрибутов защиты
BOOL bManualReset,//Флаг ручного сброса события
BOOL bInitialState,//Флаг начального состояния события
LPCTSTR lpName//Имя события
);
Существует два типа событий: со сбросом вручную и с
автоматическим сбросом (автосбросом). События со сбросом
вручную требуют явной установки их в свободное состояние
68
функцией SetEvent() и столь же явного переключения в занятое
состояние функцией ResetEvent(); очевидно, что эти
переключения можно выполнять в произвольных точках
программы. События с автосбросом точно так же устанавливаются
явным образом функцией SetEvent(), однако их сброс
происходит автоматически, как только в управляемом потоке
функция ожидания события обнаружила, что событие установлено
и, соответственно, разбудила поток. Событиями с автосбросом
удобно пользоваться в циклических фрагментах программ, так как
такое событие, автоматически сброшенное функцией WaitForSingleObject() в начале разблокированного ею же
фрагмента, предотвращает повторное его выполнение до новой
установки этого события в управляющем потоке.
Если в функции CreateEvent() параметр bManualReset
равен TRUE, создается событие со сбросом вручную; если этот
параметр равен FALSE, создается событие с автосбросом.
Параметр bInitialState задает начальное состояние
создаваемого события. Если он равен TRUE, создается
установленное событие; если этот параметр равен FALSE,
созданное событие будет сброшено.
Функция CreateEvent(), как и другие функции создания
объектов Windows, возвращает дескриптор созданного события
типа HANDLE, который затем используется в функциях
переустановки и ожидания события.
Для установки события в свободное (сигнальное) состояние
используется функция SetEvent(), в качестве параметра которой
указывается дескриптор устанавливаемого события. Если событие
удалось установить, функция возвращает значение TRUE.
Для сброса события в занятое (несигнальное) состояние
используется функция ResetEvent(), в качестве параметра
которой указывается дескриптор сбрасываемого события. Если
событие удалось сбросить, функция возвращает значение TRUE.
На рис. 7.3 показан пример взаимодействия потоков,
синхронизация которых осуществляется с помощью двух событий.
Первичный поток создает два события в сброшенном состоянии,
сохраняя их дескрипторы hEvent1 и hEvent2, после чего
запускает вторичный поток, из которого должен получить
69
последовательно два результата. Созданные события пока никак не
влияют на ход приложения. Оба потока выполняются параллельно
до тех пор, пока первичному потоку не потребуется первый
результат из вторичного. В этой точке первичный поток
останавливается вызовом функции WaitForSingleObject(), в
качестве первого параметра которой указывается дескриптор
первого (пока сброшенного) события hEvent1. Как только
вторичный поток получил первый результат, он вызовом функции
SetEvent(hEvent1) устанавливает первое событие, что
приводит к “побудке” первичного потока. Такая же процедура
(только с использованием второго события) повторяется, когда
первичный поток доходит до точки, в которой ему нужен второй
результат из вторичного потока.
Рис. 7.3. Синхронизация потоков с помощью событий
70
Критические секции и защита данных
Критической секцией называется произвольный фрагмент
программы, который должен обладать монопольным доступом к
некоторым данным любого содержания и объема. Критические
секции можно использовать только для синхронизации потоков,
входящих в единый процесс; если требуется обеспечить
поочередный доступ к общим данным потоков, входящих в разные
процессы, то в этом случае надо вместо критических секций
использовать другие объекты Windows – мьютексы.
Для использования в программе критических секций следует
прежде всего объявить среди глобальных данных структурную
переменную типа CRITICAL_SECTION:
CRITICAL_SECTION CritSect;
Переменная CritSect внешне никак не связана ни с
защищаемыми данными, ни с тем фрагментом программы, который
к этим данным обращается; она, можно сказать, носит
организационный характер: с ее помощью Windows определяет,
можно ли в данный момент предоставить доступ к общим данным
тому или иному потоку.
В первичном потоке программы, еще до создания дочерних
потоков, следует выполнить инициализацию объекта Windows
“критическая секция”. Эта инициализация выполняется вызовом
функции InitializeCriticalSection() с указанием в
качестве ее параметра адреса созданной ранее глобальной
структурной переменной типа CRITICAL_SECTION.
Фрагменты потоков, в которых осуществляется обращение к
общим данным и которые, собственно, и называются критическими
секциями, защищаются функциональными скобками
EnterCriticalSection(&CritSect)
...
LeaveCriticalSection(&CritSect)
с указанием в качестве параметра адреса той же структурной
переменной типа CRITICAL_SECTION.
Какие изменения в ход выполнения потоков вносит такая
организация? Обычно каждому потоку система по очереди
выделяет кванты времени, в течение которых потоки и
выполняются. "Отнять" время процессора у потока система может
в любой момент, на любой его команде. При наличии в потоке
71
критической секции алгоритм квантования времени усложняется.
Если первый поток вошел в свою критическую секцию, система попрежнему может в какой-то момент времени приостановить его
выполнение и передать время процессора другому потоку, в том
числе и второму потоку, связанному с той же переменной типа
CRITICAL_SECTION. Однако как только второй поток подойдет к
своей критической секции и вызовет функцию EnterCriticalSection(), система прервет его выполнение и отдаст время
процессора либо снова первому потоку, либо другим потокам,
действующим в системе. Пока первый поток не выйдет из своей
критической секции (вызовом функции LeaveCriticalSection()), второй поток войти в свою критическую секцию не
может. В результате, пока первый поток не закончит работу с
общими данными, они будут недоступны второму потоку.
Разумеется, в процессе может существовать любое количество
потоков, работающих с общими данными. В любом случае
обращаться к общим данным они смогут только поочередно.
В каких случаях надо защищать данные от другого потока? Если
общие для нескольких потоков данные только читаются, то к таким
данным может обращаться любое число потоков. Проблемы
возникают в том случае, если потоки как-то видоизменяют данные
– модифицируют отдельные элементы, уничтожают их или
добавляют. Пусть общие данные олицетворяют собой счет в банке,
доступный нескольким клиентам – мужу и жене, например, или
нескольким агентам по закупке некоторой продукции. Клиенты
снимают деньги в банке с помощью программы, в которой
каждому клиенту отведен отдельный поток. Получив запрос
клиента на снятие некоторой суммы, программа проверяет наличие
такой суммы на счете, оформляет необходимые документы, выдает
клиенту деньги и уменьшает величину счета на снятую сумму.
Представим теперь, что где-либо между действиями по
проверке суммы на счете и ее уменьшением система передала
управление потоку второго клиента, который тоже поставил запрос
на снятие денег со счета. Поскольку на счете пока еще имеется
прежняя сумма, второй поток ее проанализирует и, обнаружив, что
она достаточно велика, уменьшит в соответствии с запросом
второго клиента. После того как управление вернется в первый
поток, он продолжит анализ старой суммы и тоже снимет деньги
со счета, хотя их там, возможно, недостаточно или вообще нет.
72
Защита с помощью критической секции всего блока действий по
анализу суммы счета, выдаче денег клиенту и коррекции счета
приведет к тому, что запросы всех клиентов на снятие денег будут
выполняться по очереди и не смогут прерывать друг друга, чем и
будет обеспечен правильный алгоритм анализа остатка средств на
счете.
С целью экономии системных ресурсов созданные объекты
критических секций следует удалять после того, как отпала
необходимость в их использовании. Удаление критической секции
осуществляется вызовом предусмотренной для этого функции
Windows с указанием адреса соответствующей структурной
переменной:
DeleteCriticalSection(CritSect);
Критические секции являются одним из наиболее используемых
механизмов защиты данных общего пользования в многопоточных
системах. Еще раз укажем, что защищать следует лишь те данные,
которые модифицируются потоками; если потоки только читают
данные, не изменяя их, то обращение к ним многих потоков
осуществляется безболезненно.
8. БИБЛИОТЕКИ ДИНАМИЧЕСКОЙ
КОМПОНОВКИ
Библиотеки динамической компоновки, или динамически
подключаемые библиотеки, которые обычно сокращенно называют
DLL-библиотеками или просто DLL (от Dynamic Link Library)
можно смело назвать краеугольным камнем операционной системы
Windows. Все функции обслуживания 32-разрядных приложений
содержатся в таких библиотеках; прежде всего это файлы
KERNEL32.EXE (управление памятью, потоками и процессами),
USER32.DLL (поддержка интерфейса пользователя) и GDI32.EXE
(графика и вывод текста).
DLL-библиотеки содержат программы общего пользования, т. е.
фактически подпрограммы, используемые многими (или всеми)
загружаемыми приложениями. При обычной, не динамической
компоновке все подпрограммы, в которых нуждается данное
приложение, подсоединяются к основной программе и входят в
состав ее выполнимого файла. Если некоторая подпрограмма
73
нужна многим приложениям, то ее коды размножаются, повторяясь
во всех скомпонованных приложениях. Такая система приводит к
увеличению
объемов
загрузочных
файлов
и
крайне
неэффективному использованию памяти компьютера.
Суть использования DLL-библиотек заключается в следующем.
Все функции DLL-библиотек находятся в памяти в одном
экземпляре. При компоновке приложения в его загрузочный файл
включаются не коды требуемых функций, а лишь их имена вместе
с именами содержащих их DLL-библиотек. В процессе загрузки
приложения в память система проецирует коды DLL на адресное
пространство выполняемого процесса. В этом случае связь
приложения с DLL-библиотекой сохраняется на все время
выполнения приложения; такая процедура носит название неявного
или статического подключения.
В целях оптимизации используемых ресурсов приложение
может пойти дальше, организовав загрузку библиотеки
(фактически проецирование DLL на свое адресное пространство) в
нужный момент по ходу своего выполнения. Когда необходимость
в функциях DLL отпадет, приложение может выгрузить
библиотеку, освободив свое адресное пространство. Такая
процедура
носит
название
динамического
подключения
(динамической загрузки).
Необходимость разработки прикладных DLL-библиотек
возникает в тех случаях, когда в сложном программном комплексе,
включающем в себя много отдельных программ (например,
приема, обработки и сохранения данных, их графического
отображения в разных видах и т. д.) какие-то его элементы имеют
общий
характер
и
должны
использоваться
многими
составляющими комплекса. Оформление этих элементов в виде
функций DLL-библиотек позволяет сократить объем программного
обеспечения и повысить эффективность его функционирования.
Создание собственной DLL-библиотеки не составляет особого
труда. Современные интегрированные среды разработки (Borland
C++, Microsoft Visual Studio и др.) имеют в своем составе средства
подготовки как обычных выполнимых программ, так и библиотек
динамической компоновки. Фактически оформление некоторой
прикладной функции в виде элемента DLL-библиотеки сводится к
74
установке соответствующих флажков при создании проекта, в
котором данная функция будет компилироваться.
Статическое подключение DLL-библиотеки к приложению
осуществляется также на этапе создание проекта, в котором
компилируется и отлаживается данное приложение. Динамическое
же подключение требует включения в текст приложения функций
загрузки и выгрузки DLL-библиотеки.
Техника создания и использования прикладных DLL-библиотек
описана в соответствующих лабораторных работах данного
пособия.
75
Часть 2
Лабораторный практикум
Работы лабораторного практикума
Работа 1. Создание логических шрифтов
a. Создайте приложение с главным окном большого размера,
покрасьте его каким-либо бледным цветом и сформируйте часть
титульного листа некоторого отчета, содержащую следующие три
отдельные строки:
Тема: название работы
Студент: ваша фамилия
Группа: ваша учебная группа
Строки оформите разными шрифтами. Для этого в функциии
OnCreate() обработки сообщения WM_CREATE создайте три
логических шрифта, различающихся гарнитурами, размером и
характеристиками:
 для первой строки – шрифт Times New Roman Cyr
высотой 50 пикселов;
 для второй строки – шрифт Courier New Cyr высотой 40
пикселов, повышенной жирности;
 для третьей строки – шрифт Arial Cyr высотой 40
пикселов, курсивный.
В функции OnPaint() обработки сообщения WM_PAINT
последовательно выбирайте в контекст устройства дескрипторы
созданных шрифтов и выводите заданные строки. Покрасьте
первую строку в красный цвет, а две другие – в синий. Все строки
разместите по горизонтали по центру окна, для чего вместо
функции вывода текста TextOut() воспользуйтесь функцией
DrawText() с указанием соответствующих флагов и координат.
b. Создайте четвертый шрифт с именем Wingdings, Wingdings 2 или Webdings размером 60 – 70 пикселов. С помощью
таблицы символов Windows и таблицы кодировки символов (см.,
76
например, [2], стр. 15) определите коды нескольких символов этих
шрифтов и выведите их в главное окно, сформировав в программе
строку этих символов в виде последовательности кодов. Не
забудьте завершить строку нулем.
Работа 2. Таймеры Windows (индивидуальное задание А)
Включите в оконную функцию обработку сообщения WM_TIMER. В функции OnCreate() обработки сообщения WM_CREATE установите таймер с частотой, указанной в задании. В цветное
главное окно приложения выведите фигуру, указанную в
индивидуальном задании, и в каждом такте таймера изменяйте ее
указанным в задании образом. Одновременно отсчитывайте в
глобальной переменной число прошедших тактов и по истечении
заданного числа тактов уничтожьте таймер, остановив изменение
изображения.
Работа 3. Дочернее окно в главном окне приложения
Модифицируйте программу из предыдущей работы. После
регистрации класса главного окна зарегистрируйте класс дочернего
окна, назначив ему свою оконную процедуру и другой цвет фона.
Создайте это окно в функции OnCreate(), назначив ему стиль
WS_CHILD|WS_DLGFRAME|WS_VISIBLE. В оконную процедуру
дочернего окна включите обработку единственного сообщения
WM_PAINT, в которой выводите в это окно надпись "Осталось
... тактов" с указанием текущего числа оставшихся тактов.
Для этого в каждом такте таймера преобразуйте оставшееся число
тактов (хранящееся в глобальной переменной) в строку символов с
помощью функции itoa(), а затем с помощью функций
strcpy() и strcat() образуйте итоговую строку, выводимую в
дочернее окно. Не забудьте, что в каждом такте таймера теперь
надо инициировать посылку сообщения WM_PAINT в оба окна – в
главное для изменения конфигурации фигуры и в дочернее для
обновления выводимого сообщения.
Работа 4. Вывод растровых изображений с использованием
совместимой памяти
a. Создайте приложение с главным окном, в которое выводится
нарисованный предварительно в графическом редакторе Paint
77
цветной рисунок произвольного содержания размером, например,
200  200 пикселов. Содержательный алгоритм программы
заключен в функциях обработки сообщений WM_CREATE и
WM_PAINT.
Состав функции OnCreate():
 получение контекста окна (функция GetDC());
 создание совместимой памяти с одновременной загрузкой в
него изображения (функция LoadImage());
 создание совместимого с окном контекста совместимой
памяти (функция CreateCompatibleDC());
 выбор совместимой памяти с изображением в совместимый
контекст (макрос SelectBitmap());
 получение характеристик объекта совместимой памяти
(функция GetObject());
 освобождение контекста окна (функция ReleaseDC()).
Состав функции OnPaint():
 получение контекста окна (функция BeginPaint());
 копирование совместимой памяти в окно (функция
BitBlt();
 освобождение контекста окна (функция EndPaint()).
b. Модифицируйте предыдущую программу, выведя в
совместимую память, помимо загруженного в нее рисунка,
дополнительные графические образы: заключите рисунок в рамку,
нарисованную толстым цветным пером, и поместите в поле
рисунка свою фамилию в качестве подписи. Поскольку
совместимая память вместе с совместимым контекстом создается в
функции OnCreate(), все эти манипуляции можно выполнить
там же. Для того чтобы рамка рисунка выглядела аккуратно, при
создании пера используйте константу PS_INSIDEFRAME. Для
определения размеров изображения воспользуйтесь функцией
GetObject и структурной переменной типа BITMAP.
Выполните пункт b задания в двух вариантах: с загрузкой
изображения посредством файла ресурсов и непосредственно из
файла .BMP.
78
Работа 5. Измерение временных характеристик программы с
помощью мультимедийного таймера
Включите в предыдущую программу измерение времени
выполнения одного из ее фрагментов, например, функции OnCreate(). С этой целью вызовите функцию timeGetTime()
дважды, в самом начале изучаемого фрагмента и перед его
завершением, сохранив в глобальной переменной разность
значений, возвращаемых этой функцией. Выведите эту разность в
виде текста в главное окно приложения, дополнив его
обозначением единиц времени (миллисекунд). Выполните
многократный (5 – 10 раз) прогон программы, сравните и
проанализируйте полученные результаты.
Работа 6. Вывод движущихся изображений с синхронизацией
от системного таймера (индивидуальное задание B)
Разработайте программу, в которой в главном окне черного
цвета размером 256256 пикселов по траектории, указанной в
задании, непрерывно перемещается залитая ярким цветом фигура
(например, круг диаметром 50 – 60 пикселов).
В функции OnCreate() создайте таймер с минимально
возможным периодом (50 мс), а также яркую цветную кисть для
заливки движущейся фигуры.
В функции OnTimer() вычисляйте текущие координаты
фигуры, изменяя их в соответствии с заданием; после этого
вызовом функции InvalidateRect() инициируйте посылку в
главное окно сообщения WM_PAINT.
В функции OnPaint() выводите в окно фигуру, используя
текущие значения координат.
Убедитесь в невысокой скорости перемещения изображения и в
наличии у него характерного мерцания. Сохраните отлаженную
программу, чтобы иметь возможность сравнить ее со следующей.
Работа 7. Повышение качества движущихся изображений с
помощью совместимой памяти
Выполните модификацию предыдущей программы так, чтобы
изменение изображения выполнялось не непосредственно в
главном окне, а в совместимой памяти с последующим
копированием этой памяти в главное окно. Содержательный
79
алгоритм программы заключен в функциях обработки сообщений
WM_CREATE, WM_TIMER и WM_PAINT.
Состав функции OnCreate():
 получение контекста окна (функция GetDC());
 создание пустой совместимой памяти объемом 256256
пикселов (функция CreateCompatibleBitmap());
 создание совместимого с окном контекста совместимой
памяти (функция CreateCompatibleDC());
 выбор совместимой памяти в совместимый контекст (макрос
SelectBitmap());
 выбор в контекст совместимой памяти прозрачного пера и
яркой кисти для рисования изображения в этой памяти;
 образование
структурной
переменной
типа
RECT,
описывающей размер совместимой памяти, и ее
инициализация;
 установка таймера с периодом 50 мс;
 освобождение контекста окна (функция ReleaseDC()).
Состав функции OnTimer():
 вычисление текущих координат фигуры в соответствии с
заданием;
 закрашивание совместимой памяти черным цветом (функция
FillRect());
 рисование в совместимой памяти фигуры с использованием
текущих значений координат;
 вызов функции InvalidateRect(), инициирующей
посылку в главное окно сообщения WM_PAINT без
перерисовки фона (последний параметр функции InvalidateRect() должен быть равен FALSE).
Состав функции OnPaint():
 получение контекста окна (функция BeginPaint());
 копирование совместимой памяти в окно (функция
BitBlt();
 освобождение контекста окна (функция EndPaint()).
80
Убедитесь в повышении качества изображения (исчезло его
мерцание), хотя скорость перемещения изображения осталась,
естественно, низкой.
Работа 8. Движение изображения по фоновому рисунку
Найдите в каталогах Windows какой-либо рисунок размером
256256 пикселов (например, Лес.bmp или Чешуя.bmp) и
скопируйте его в свой рабочий каталог. Увеличьте размер главного
окна на 8 пикселов по горизонтали и на 26 пикселов по вертикали
для компенсации толщины рамок и строки заголовка (чтобы размер
собственно рабочей области окна точно соответствовал размеру
фонового рисунка).
В функции OnCreate() вместо создания одной пустой
области совместимой памяти размером 256256 пикселов, как это
делалось в работе 7, образуйте две таких области, одновременно
загрузив и в ту, и в другую рисунок из файла .bmp. Одна из этих
областей будет служить источником фонового рисунка, а другая
использоваться как рабочая; в ней будет формироваться
движущееся изображение. Для каждой из областей совместимой
памяти предусмотрите свой совместимый контекст и выберите в
эти контексты дескрипторы соответствующих областей. Далее, как
и в предыдущей работе, выберите в контекст рабочей совместимой
памяти дескрипторы прозрачного пера и цветной яркой кисти,
освободите контекст окна и установите таймер.
В функции OnTimer() вместо закрашивания рабочей
совместимой памяти черным цветом, как это делалось в
предыдущей работе, скопируйте в нее функцией BitBlt()
фоновый рисунок из фоновой совместимой памяти, после чего
нарисуйте новое (сдвинутое) изображение и инициируйте посылку
в главное окно сообщения WM_PAINT.
Функция OnPaint() не претерпевает никаких изменений; в
ней осуществляется копирование рабочей совместимой памяти
(фон плюс наша фигура) в окно приложения.
Работа 9. Работа с файлами (индивидуальное задание C)
Составьте программу, которая с помощью 32-разрядных
функций работы с файлами CreateFile(), WriteFile(),
SetFilePointer() и ReadFile() выполняет базовые
81
операции с файлами, именно: создание нового файла, запись в него
данных и их контрольное чтение. Зарезервируйте в памяти массив
и заполните его в цикле данными, указанными в индивидуальном
задании. Создайте файл с произвольным именем и расширением.
Выведите созданный массив в файл. С помощью функции SetFilePointer() установите указатель на начало файла и
прочитайте созданный файл в другой, пустой, массив такого же
размера. Выведите для контроля в окно сообщения два-три
элемента этого массива. Программа состоит из одной функции
WinMain() без каких-либо окон или элементов управления
(кроме окна вывода сообщения).
С помощью программы NortonCommander изучите физическое
содержимое файла на диске и удостоверьтесь, что файл имеет
правильную длину и содержит данные в соответствии с заданием.
Работа 10. Стандартные диалоги Windows
для работы с файлами
Составьте программу, которая вызовом стандартного диалога
Windows “Открытие файла” позволяет найти на диске нужный
файл, после чего открывает этот файл и читает из него данные в
предусмотренный в программе пустой массив. Выведите для
контроля в окно сообщения несколько элементов этого массива.
Прочитайте созданный в предыдущей работе файл и убедитесь в
правильном функционировании программы.
Стандартный диалог Windows “Открытие файла” создается
вызовом функции GetOpenFileName(), в качестве параметра
которой указывается адрес созданной в программе структурной
переменной типа OPENFILENAME. Эту структуру необходимо
предварительно обнулить, а затем заполнить в ней по меньшей
мере следующие элементы:
lStructSize
lpstrFilter
lpstrFile
nMaxFile
В элемент lpstrFilter помещается адрес строки,
содержащей фильтр и имеющей, например, такой вид (если
открываемый файл имеет расширение .DAT):
char str[]="Файлы данных (*.DAT)", "*.DAT",
82
"Все файлы (*.*)","*.*", "";
В каждой паре строк сначала определяется комментарий,
который появится в соответствующем поле стандартного диалога, а
затем соответствующий этому комментарию шаблон, по которому
функция GetOpenFileName() будет отбирать файлы для вывода
в создаваемый ею диалог.
Работа 11. Проецирование файла в память
Составьте
программу,
выполняющую
проецирование
созданного в работе 9 файла в память. Предусмотрите и в проекции
файла, и в его отображении доступ для чтения и записи. Не
забудьте преобразовать полученный вами указатель в тип,
соответствующий тем данным, из которых составлен файл (int,
short, BYTE и т. д.). Получив указатель на проекцию файла в
памяти, модифицируйте последний элемент в файле, записав в него
какое-либо легко идентифицируемое число (например, 0x12345678
для элементов типа int или 0xABCD для элементов типа short).
С помощью программы NortonCommander убедитесь, что файл на
диске модифицирован должным образом.
Работа 12. Потоки (индивидуальное задание D)
Создайте обычное приложение Windows с главным окном,
покрашенным в какой-либо цвет. В оконную функцию включите
обработку сообщений WM_CREATE и WM_DESTROY. В функции
обработки сообщения WM_CREATE создайте с помощью функции
CreateThread() два вторичных потока с индивидуальными
рабочими функциями. В функции CreateThread() достаточно
указать два параметра – имя рабочей функции потока и адрес
переменной для возврата идентификатора потока; остальные
параметры можно принять равными 0.
В каждой рабочей функции потока с помощью оператора
while организуйте бесконечный цикл до перевода в состояние
FALSE глобальной булевой переменной, которую установите в
значение TRUE перед созданием потока, и в значение FALSE – в
функции OnDestroy(). Эти действия обеспечат завершение
обоих потоков при завершения всего приложения. Включите в
цикл while вызов функции Sleep() для усыпления потока на
некоторое время (0,5 – 1,5 с). В результате действия, составляющие
83
содержание рабочих функций потоков, будут выполняться
периодически и независимо с заданной для каждого потока
частотой.
В рабочих функциях потоков в определенные участки главного
окна приложения выводятся, в соответствии с индивидуальным
заданием, некоторые символьные или графические данные. Для
получения необходимого для этого контекста устройства
воспользуйтесь функцией GetDC(); не забудьте, завершив вывод в
окно, освободить контекст устройства вызовом функции ReleaseDC(). Вывод в окно в этом случае не сопровождается
затиранием предыдущего изображения, поэтому перед выводом
нового изображения старое необходимо явным образом затирать
вызовом функции FillRect(). Поскольку каждый из двух
потоков выводит изображение в свою область окна, то и затирать
он должен только эту область. Обязательно предусмотрите
закрашивание половин окна разными цветами, чтобы они наглядно
различались. Координаты всей рабочей области окна можно
получить посредством функции GetClientRect(); из этих
данных нетрудно вычислить координаты требуемой половины окна
(верхней, нижней, правой или левой).
Работа 13. Синхронизация потоков с помощью событий
Модифицируйте программу из предыдущей работы. Включите в
проект файл ресурсов с описанием меню, состоящего из четырех
пунктов для включения и выключения каждого из двух потоков.
В функции OnCreate(), наряду с двумя потоками, создайте
два объекта событий, сохранив в глобальных переменных их
дескрипторы. Оба события создаются со сбросом вручную, их
начальное состояние лучше задать сброшенным, чтобы при запуске
программы выполнение потоков было заблокировано.
В функции OnCommand() обработки сообщений WM_COMMAND
от пунктов меню с помощью конструкции switch-case
анализируйте код выбранного пункта и вызовом функций
SetEvent() и ResetEvent() устанавливайте или сбрасывайте
соответствующие события.
В начале фрагмента циклического выполнения каждого потока
поместите функции WaitForSingleObject() ожидания
84
“своего” события. Теперь выбором пунктов меню можно
независимо запускать или останавливать оба ваших потока.
Работа 14. Защита данных с помощью критической секции
Составьте
программу,
наглядно
иллюстрирующую
использование критических секций для защиты данных общего
пользования. Пусть такими данными является банковский счет, с
которого два потока будут периодически и параллельно снимать
суммы, равные одной денежной единице. Программа будет
состоять из функции WinMain(), реализующей первичный поток
приложения, а также двух вторичных потоков, имитирующих
действия клиентов банка. Результаты работы программы
достаточно выводить в окно сообщения, поэтому можно обойтись
без главного окна.
Состав глобальных переменных:
 структурная
переменная
CritSect
типа
CRITICAL_SECTION;
 целочисленная переменная nBalance для остатка денег на
счете;
 булева переменная bGo, управляющая выполнением потоков;
 две целочисленные переменные nDeals1 и nDeals2 для
отсчета
числа
выполненных
банковских
операций
(транзакций) первым и вторым потоками;
 два дескриптора создаваемых в программе файлов;
 переменная типа DWORD, требуемая для операций записи в
файл.
Локальные переменные в главной функции WinMain():
 массив из двух переменных hThreads[2] типа HANDLE для
хранения дескрипторов создаваемых потоков;
 две переменные типа DWORD для хранения идентификаторов
создаваемых потоков;
 символьный массив достаточной длины для формирования
итогового сообщения.
Состав главной функции WinMain():
 инициализация критической секции вызовом функции InitializeCriticalSection();
85
 создание двух файлов с произвольными именами для записи в
них информации о произведенных транзакциях;
 создание двух вторичных потоков с рабочими функциями
Thread1 и Thread2;
 установка переменной bGo, разрешающей выполнение обоим
потокам;
 “пустой” цикл while(nBalance>=1), останавливающий
выполнение первичного потока (функции WinMain()) до
полного истощения денег на счете;
 после выхода из цикла while() сброс переменной bGo;
 ожидание завершения обоих потоков с помощью функции
WaitForMultipleObjects();
 формирование в символьном массиве (с помощью функции
wsprintf())
итогового
сообщения,
включающего
исходную сумму счета, количество транзакций первого и
второго потоков, остаток денег на счете, а также сумму,
снятую со счета обоими потоками вместе;
 вывод сформированной символьной строки в окно сообщения
и завершение программы;
 закрытие обоих потоков и удаление критической секции.
Рабочие функции потоков ничем не отличаются друг от друга за
исключением того, что первый поток, выполнив транзакцию,
инкрементирует переменную nDeals1, а второй –
переменную nDeals2.
Состав рабочих функций потоков:
 цикл while(bGo), в который включается весь текст рабочей
функции (за исключением последнего предложения return
0);
 вход в критическую секцию (функция EnterCriticalSection());
 чтение в локальную переменную потока nLocalBalance
текущего содержимого глобальной переменной nBalance
(суммы денег на счете);
 проверка с помощью предложения if(nLocalBalance
>=1) наличия денег на счете (все дальнейшие операции
86
выполняются в блоке if, т. е. лишь при положительном
результате этого сравнения);
 уменьшение значения переменной nLocalBalance на
единицу (снятие со счета);
 инкремент переменной nDeals или nDeals2;
 формирование символьной строки с текущей суммой на счете
(значение переменной nLocalBalance);
 запись этой строки в файл, соответствующий данному потоку
(эта операция введена для увеличения времени работы
функции потока и повышения вероятности ее прерывания с
передачей управления другому потоку);
 в качестве последней операции перенос значения переменной
nLocalBalance в глобальную переменную nBalance;
 после закрытия блока if выход из критической секции
вызовом функции LeaveCriticalSection()
 после закрытия блока while завершающий оператор return 0 и закрытие рабочей функции потока.
Программу необходимо сначала отладить при выключенных
операциях с критической секцией. Возможный результат работы
программы показан на приведенном ниже рисунке:
Видно, что при начальной сумме 100000 единиц со счета было
снято в общей сложности 189580 единиц, причем на счете еще
осталась некоторая сумма.
После того, как программа будет отлажена, ее следует запустить
при включенных операциях с критической секцией. В этом случае
сумма сделок должна в точности соответствовать сумме вклада, а
остаток вклада должен быть равен 0.
87
Работа 15. Библиотеки динамической компоновки
a. Неявное (статическое) подключение. В этом случае DLLбиблиотека автоматически проецируется на адресное пространство
вызывающей программы еще в процессе ее запуска. Создайте
проект с именем, например, DLL.IDE для подготовки выполнимого
DLL-модуля. В окне Target Type укажите тип приложения Dynamic
Library (.dll). Исключите из проекта файлы .RC и .DEF.
В исходный текст программы DLL-библиотеки включите две
прикладные функции-подпрограммы с произвольными именами
(например, Func1 и Func2), которые вызовом функции MessageBox() выводят в окно сообщения различающуюся
информацию. Исходный текст библиотеки должен состоять только
из этих функций (без функции WinMain()). В начале программы,
как обычно, должны быть описаны прототипы этих функций, в
которых перед типом возвращаемого значения следует указать
описатель __declspec (dllexport) (заметьте, что этот
описатель начинается с двух символов подчеркивания). Включите в
текст программы директиву #include для подсоединения
заголовочного файла Windows.h.
В результате компиляции и компоновки должны создаться
файлы DLL.DLL и DLL.LIB. Удостоверьтесь в этом. Не запускайте
полученный модуль DLL.DLL; он предназначен не для
самостоятельной работы, а для вызова из программы .EXE.
Создайте второй проект с именем, например, CPP.IDE для
подготовки выполнимого модуля вызывающей программы. В окне
Target Type этого проекта укажите, как обычно, тип приложения
Application (.exe).В состав проекта включите, помимо исходного
файла CPP.CPP, еще и файл библиотеки импорта DLL.LIB, с
помощью которой осуществляется поиск и привязка к
выполнимому модулю вызываемых в нем DLL-библиотек.
В функцию WinMain() вызывающей программы включите
сначала вывод в окно сообщения какой-либо информационной
строки, чтобы при запуске программы удостовериться, что она
начала работать, а затем – вызовы первой и второй функций DLLбиблиотеки. Перед функцией WinMain() опишите прототипы
вызываемых функций с описателем __declspec (dllimport).
88
Откомпилируйте программу CPP.EXE. Запустив программу на
выполнение, убедитесь, что она вызывает обе функции созданной
вами DLL-библиотеки. Просмотрите с помощью какого-либо
текстового редактора содержимое файла библиотеки импорта
DLL.LIB и найдите в нем “декорированные” компоновщиком
имена ваших функций. Запишите эти имена, чтобы
воспользоваться ими в следующем задании. Для определения
“декорированных” имен можно также вызвать утилиту TDUMP в
таком формате:
TDUMP имя_dll.DLL
имя_dll.MAP.
b. Явное (динамическое) подключение. В этом случае загрузка
(а также и выгрузка) DLL-библиотеки осуществляется программой
по мере необходимости. Воспользуйтесь созданной в предыдущем
задании динамической библиотекой DLL.DLL. Создайте новый
проект для подготовки вызывающей программы с именем,
например, CPP2.IDE. Не включайте в него библиотеку импорта. Не
включайте в него прототипы вызываемых функций. Алгоритм
программы CPP2.CPP должен содержать следующие элементы:
 вывод в окно сообщения информации о запуске программы;
 загрузка динамической библиотеки DLL.DLL вызовом
функции Windows LoadLibrary() с сохранением
полученного дескриптора;
 получение адресов вызываемых из DLL-библиотеки функций
с помощью функции Windows GetProcAddress(). В
качестве первого параметра этой функции следует указывать
полученный ранее дескриптор загруженной библиотеки, а в
качестве второго параметра – найденные в предыдущем
задании “декорированные” имена вызываемых функций;
 последовательный вызов обеих функций из DLL-библиотеки
посредством полученных на предыдущем этапе адресов;
 выгрузка библиотеки вызовом функции FreeLibrary().
Откомпилируйте программу CPP.EXE. Запустив программу на
выполнение, убедитесь, что внешне она работает так же, как и
предыдущая, хотя, возможно, вам удастся заметить задержку при
ее выполнении на этапе поиска и загрузки DLL-библиотеки.
89
c. Динамическая загрузка с исходными именами
вызываемых функций. Поскольку в этом задании DLLбиблиотеку следует создавать не так, как в предыдущих заданиях,
откройте новый проект для создания приложения типа Dynamic Library (.dll). Пусть этот проект носит имя DLL3.IDE. Включите в
него имеющийся уже у вас исходный файл DLL3.CPP и, кроме
того, файл определения модуля с именем DLL3.DEF. Этот файл
будет состоять из трех строк (при наличии в DLL-библиотеке двух
функций):
EXPORTS
имя_функции1_в_программе = “декорированное”_имя_функции
имя_функции2_в_программе = “декорированное”_имя_функции
Заново скомпилируйте DLL-библиотеку.
Создайте новый проект для подготовки вызывающей
программы (CPP3.IDE). Скопируйте файл CPP2.CPP под именем
CPP3.CPP и исправьте в нем имя загружаемой DLL-библиотеки на
новое. Исправьте также “декорированные” имена вызываемых
функций на те, которые использовались в исходном файле DLLбиблиотеки. Получите загрузочный модуль основной программы и
убедитесь в правильности его работы.
Работа 16. Передача параметров в функции DLL-библиотек
Воспользуйтесь заданием D (работа 8) из практикума
предыдущего семестра, где в главное окно приложения выводился
некоторый график. Оформите алгоритм рисования графика в виде
программы DLL-библиотеки. Включите в библиотеку две функции
рисования одного и того же графика, различающиеся, например,
масштабом изображения или его положением относительно осей
координат. Поскольку для вывода любого изображения в окно
приложения необходимо использовать дескриптор контекста
устройства, его значение придется передавать в функцию DLLбиблиотеки в качестве параметра. Другим параметром может быть,
например, цвет графика или дескриптор пера. Создайте
соответствующую DLL-библиотеку и подключите ее к
приложению любым из рассмотренных в предыдущей работе
способом. В соответствующем месте основной программы
(именно, в функции OnPaint()) осуществите последовательный
90
вызов функций DLL-библиотеки. Запустите программу и убедитесь
в правильном отображении графиков.
91
Индивидуальные задания лабораторного практикума
Задание A1. В середину главного окна голубого цвета выведите
произвольный символ красного цвета. В каждом такте таймера,
работающего с частотой 20 Гц, перемещайте символ на небольшой
угол по образующей улитки Паскаля, уравнение которой в
параметрической форме имеет вид x = a cos2 t + cos t, y = a cos t sin t
+
+ sin t. Выберите значение a в пределах 1,5 – 2,0, эффективный
радиус кривой 40 – 50 пикселов, шаг изменения параметра t 0,05 и
такое число шагов, чтобы получить один полный период графика.
Выполните задание в двух вариантах – с затиранием фона окна при
его перерисовке и без затирания фона.
Задание B1. Фигура плавно перемещается по горизонтали от
левого края окна до правого, скачком возвращается назад и
повторяет плавное движение вправо. В каждом шаге x-координату
фигуры изменяйте на 1 пиксел.
Задание C1. Массив записываемых в файл данных должен
представлять собой последовательный ряд из 5000 целых чисел в
убывающем порядке.
Задание D1. Первый поток выводит в левую половину окна
цветными символами текущее время (часы, минуты и секунды),
получаемое с помощью функции GetLocalTime().
Второй поток выводит в правую половину окна изображение
круга, заливаемого попеременно то одним, то другим цветом.
Задание A2. У левого края достаточно широкого главного окна
желтого цвета выведите горизонтальную зеленую ленточку длиной
10 пикселов. В каждом такте таймера, работающего с частотой 20
Гц, изменяйте ее длину на 4 – 5 пикселов так, чтобы она росла
вправо. Остановите таймер, когда ленточка достигнет правого края
экрана.
Задание B2. Фигура плавно перемещается по окружности,
уравнение которой в параметрической форме имеет вид x = sin t, y
=
= cos t. Шаг изменения параметра t задайте равным 0,02, значения x
и y умножайте на 80.
92
Задание C2. Массив записываемых в файл данных должен
представлять собой последовательный ряд из 2000 целых четных
чисел.
Задание D2. Первый поток выводит в верхнюю половину окна
цветными символами текущие экранные координаты курсора
мыши, получаемые с помощью функции GetCursorPos().
Второй поток выводит в нижнюю половину окна изображение
попеременно то левой, то правой половины цветного круга.
Перемещая по экрану курсор мыши, проанализируйте работу
программы.
Задание A3. В середину главного окна серого цвета выведите
толстую синюю линию длиной 50 – 100 пикселов под углом к
вертикали. В каждом такте таймера, работающего с частотой 1 Гц,
изменяйте ее положение так, чтобы она качалась вправо-влево
вокруг нижней точки наподобие маятника метронома. Остановите
таймер после 10 периодов качания.
Задание B3. Фигура плавно перемещается по вертикали от
нижнего края окна до верхнего, скачком возвращается назад и
повторяет движение вверх. В каждом шаге y-координату фигуры
изменяйте на 1 пиксел.
Задание C3. Массив записываемых в файл данных должен
представлять собой последовательный ряд из 1000 коротких (тип
short) целых чисел.
Задание D3. Первый поток выводит в левую половину окна
круг, залитый каким-либо цветом, и попеременно изменяет цвет
фона под ней, т. е. цвет этой половины окна.
Второй поток выводит в правую половину окна цветными
символами число миллисекунд, истекших от момента загрузки
операционной системы, которое можно получить с помощью
функции GetTickCount(). Оцените, когда была в последний раз
загружена операционная система на вашем компьютере?
Задание A4. У левого края достаточно широкого главного окна
желтого цвета выведите один произвольный символ синего цвета.
В каждом такте таймера, работающего с частотой 10 Гц,
перемещайте символ вправо на 4 – 5 пикселов. Остановите
перемещение, когда символ дойдет до правого края окна.
93
Выполните задание в двух вариантах – с затиранием фона окна при
его перерисовке и без затирания фона.
Задание B4. Фигура плавно перемещается по эллипсу,
уравнение которого в параметрической форме имеет вид x = sin (t +
1,0),
y = cos t. Шаг изменения параметра t задайте 0,02, значения x и y
умножайте на 80.
Задание C4. Массив записываемых в файл данных должен
представлять собой последовательный ряд из 1000 целых четных
(тип unsigned int) чисел в убывающем порядке.
Задание D4. Первый поток выводит в верхнюю половину окна
изображение цветного круга попеременно то большего, то
меньшего диаметра.
Второй поток выводит в нижнюю половину окна цветными
символами текущее время по Гринвичу (часы, минуты и секунды),
получаемое с помощью функции GetSystemTime(). На сколько
время по Гринвичу отличается от московского?
Задание A5. В нижней части достаточно высокого главного
окна желтого цвета нарисуйте горизонтально лежащий коричневый
брусок. В каждом такте таймера, работающего с частотой 20 Гц,
увеличивайте высоту бруска на 4 – 5 пикселов так, чтобы он рос
вверх. Остановите таймер, когда брусок достигнет верха окна.
Задание B5. Фигура плавно перемещается по синусоиде y = sin t
вдоль горизонтали от левого края окна до правого, скачком
возвращается назад и повторяет движение вправо. В каждом шаге
x-координату фигуры изменяйте на 1 пиксел, параметр t на 0,05;
значения y умножайте на 80.
Задание C5. Массив записываемых в файл данных должен
представлять собой последовательный ряд из 5000 коротких (тип
short) целых нечетных чисел.
Задание D5. Первый поток выводит в случайные места левой
половины окна небольшие цветные кружки. Для повышения
наглядности не затирайте изображение в каждом такте вывода,
накапливая выводимые фигуры на экране.
Второй поток выводит в правую половину окна изображение
толстой цветной линии, которая попеременно отклоняется от
вертикали то вправо, то влево (наподобие часового маятника).
94
Задание A6. С левой стороны достаточно широкого главного
окна зеленого цвета выведите желтый круг диаметром 50 – 60
пикселов. В каждом такте таймера, работающего с частотой 20 Гц,
перемещайте круг вправо на 4 – 5 пикселов. Остановите таймер,
когда круг достигнет правого края окна. Выполните задание в двух
вариантах – с затиранием фона окна при его перерисовке и без
затирания фона.
Задание B6. Фигура плавно перемещается по образующей
фигуры Лиссажу, уравнение которой в параметрической форме
имеет вид x = sin t, y = cos(2t + π/2). В каждом шаге параметр t
изменяйте на 0,02, значения x и y умножайте на 80. Для константы
π/2 в языке С++ существует обозначение M_PI_2.
Задание C6. Массив записываемых в файл данных должен
представлять собой последовательный ряд из 2000 целых четных
чисел в убывающем порядке.
Задание D6. Первый поток выводит в верхнюю половину окна
попеременно изображения то круга, то квадрата с общим центром.
Фигуры должны быть залиты каким-либо цветом.
Второй поток выводит в нижнюю половину окна цветными
символами время выполнения предыдущего такта этого потока
(вместе с интервалом ожидания, задаваемым функцией Sleep()).
Для
определения
длительности
фрагмента
программы
воспользуйтесь функцией мультимедийного таймера timeGetTime().
Задание A7. В середину достаточно большого главного окна
голубого цвета выведите небольшой розовый квадрат. В каждом
такте таймера, работающего с частотой 10 Гц, увеличивайте размер
квадрата на 5 – 10 пикселов, оставляя неизменным положение его
центра. Остановите таймер, когда квадрат займет все окно.
Задание B7. Фигура плавно перемещается по диагонали от
левого верхнего края окна до правого нижнего, скачком
возвращается назад и повторяет движение по диагонали. В каждом
шаге обе координаты фигуры изменяйте на 1 пиксел.
Задание C7. Массив записываемых в файл данных должен
представлять собой последовательный ряд из 4000 целых (тип
UINT) чисел, кратных 5.
95
Задание D7. Первый поток выводит в левую половину окна
изображение желтого полумесяца, у которого попеременно то
увеличивается, то уменьшается толщина.
Второй поток выводит в правую половину окна цветными
символами полный объем физической памяти и объем свободной
физической памяти. Вывод лучше организовать в две строки.
Требуемую информацию можно получить с помощью функции
GlobalMemoryStatus(). Запуская вместе с испытываемой
программой другие приложения Windows (ваши или системные),
пронаблюдайте за использованием физической памяти.
Задание A8. В середину достаточно большого главного окна
голубого цвета выведите один произвольный символ темнозеленого цвета. В каждом такте таймера, работающего с частотой
20 Гц, перемещайте символ на небольшой угол по образующей
кардиоиды, уравнение которой в параметрической форме имеет
вид x = cos t (1 + cos t), y = sin t (1 + cos t). Задайте эффективный
радиус кривой 40 – 50 пикселов, шаг изменения параметра t 0,1 и
такое число шагов, чтобы получить один полный период графика.
Выполните задание в двух вариантах – с затиранием фона окна при
его перерисовке и без затирания фона.
Задание B8. Фигура плавно перемещается по вертикали от
верхнего края окна до нижнего, скачком возвращается назад и
повторяет движение вниз. В каждом шаге y-координату фигуры
изменяйте на 1 пиксел.
Задание C8. Массив записываемых в файл данных должен
представлять собой последовательный ряд из 256 чисел типа BYTE
в убывающем порядке.
Задание D8. Первый поток выводит в верхнюю половину окна
изображение цветного треугольника, поворачивающийся вокруг
своего основания попеременно вершиной то вверх, то вниз.
Треугольник рисуется функцией Polygon().
Второй поток выводит в нижнюю половину окна цветными
символами время в миллисекундах, истекшее от момента загрузки
операционной системы, которое можно получить с помощью
функции GetCurrentTime(). Оцените, когда в последний раз
была загружена операционная система на вашем компьютере?
96
Задание A9. В середину главного окна темно-синего цвета
выведите изображение полной желтой Луны. Надвигайте на нее с
правой стороны тень Земли, изображая затмение Луны. В каждом
такте таймера, работающего с частотой 10 Гц, перемещайте тень
Земли влево на 4 – 5 пикселов до тех пор, пока Луна не закроется
полностью.
Задание B9. Фигура плавно перемещается по образующей
вырожденной
фигуры
Лиссажу,
уравнение
которой
в
параметрической форме имеет вид x = sin t, y = cos(t – π/2). В
каждом шаге параметр t изменяйте на 0,02, значения x и y
умножайте на 80. Для константы π/2 в языке С++ существует
обозначение M_PI_2.
Задание C9. Массив записываемых в файл данных должен
представлять собой последовательный ряд из 1000 коротких (тип
short) целых четных чисел в убывающем порядке.
Задание D9. Первый поток выводит в левую половину окна
цветными символами состояние клавиш Shift, которое можно
получить с помощью функции GetAsyncKeyState().
Состояние анализируемых клавиш индицируйте, выводя на экран
соответствующие надписи.
Второй поток выводит в правую половину окна две толстые
цветные линии, расходящиеся из одной точки под некоторым
углом к горизонтали, и периодически то увеличивает, то
уменьшает угол между ними.
Задание A10. В левый нижний угол квадратного главного окна
светло-зеленого цвета выведите один произвольный фиолетовый
символ. В каждом такте таймера, работающего с частотой 10 Гц,
перемещайте символ по диагонали вправо и вверх на 4 – 5
пикселов. Выполните задание в двух вариантах – с затиранием
фона окна при его перерисовке и без затирания фона. Остановите
таймер, когда изображение символа подойдет к правому верхнему
углу окна.
Задание B10. Фигура плавно перемещается по дуге, уравнение
которой в параметрической форме имеет вид x = sin2 t, y = cos t.
Шаг изменения параметра t задайте равным 0,02, значения x и y
умножайте на 80.
97
Задание C10. Массив записываемых в файл данных должен
представлять собой последовательный ряд из 10000 целых чисел,
кратных 4.
Задание D10. Первый поток выводит в верхнюю половину окна
попеременно изображения то левой, то правой половины
вытянутого цветного эллипса.
Второй поток выводит в нижнюю половину окна цветными
символами ширину и высоту главного окна приложения, которые
можно вычислить из данных, предоставляемых функцией
GetWindowRect(). Изменяя размер главного окна приложения,
проанализируйте работу программы.
Задание A11. В середине достаточно широкого главного окна
светло-серого цвета нарисуйте длинный горизонтальный брусок, у
которого небольшая часть слева закрашена красным цветом, а
остальная поверхность – синим. В каждом такте таймера,
работающего с частотой 5 Гц, перемещайте границу смены цвета
на 4 – 5 пикселов вправо. Остановите таймер, когда весь брусок
станет красным.
Задание B11. Фигура плавно перемещается по синусоиде
x = sin t вдоль вертикали от верхнего края окна до нижнего,
скачком возвращается назад и повторяет движение вниз. В каждом
шаге y-координату фигуры изменяйте на 1 пиксел, параметр t на
0,05; значения x умножайте на 80.
Задание C11. Массив записываемых в файл данных должен
представлять собой последовательный ряд из 2000 коротких (тип
short) нечетных целых чисел в убывающем порядке.
Задание D11. Первый поток выводит в левую половину окна
цветными символами число целых секунд, истекших с момента
загрузки операционной системы. Значение этого времени (в других
единицах) можно получить с помощью функции GetCurrentTime().
Второй поток выводит в правую половину окна треугольник,
заливаемый попеременно то синим, то зеленым цветом.
Треугольник рисуется функцией Polygon().
Задание A12. В середину главного окна черного цвета выведите
закрашенный белой кистью круг диаметром 150 – 200 пикселов. В
98
каждом такте таймера, работающего с максимальной частотой (20
Гц), уменьшайте яркость зеленого компонента цвета кисти на 2 – 3
единицы интенсивности и наблюдайте изменение цвета круга.
Остановите таймер после завершения двух периодов смены цвета.
Задание B12. Фигура плавно перемещается по дуге, уравнение
которой в параметрической форме имеет вид x = sin t, y = cos2 t.
Шаг изменения параметра t задайте равным 0,02, значения x и y
умножайте на 80.
Задание C12. Массив записываемых в файл данных должен
представлять собой последовательный ряд из 512 чисел типа BYTE.
Задание D12. Первый поток выводит в верхнюю половину окна
цветными символами номер текущего такта данного потока.
Второй поток выводит в нижнюю половину окна толстую
цветную линию, качающуюся вокруг своей середины наподобие
детских качелей.
Задание A13. В середину главного окна синего цвета выведите
прописную букву “А” красного цвета. В каждом такте таймера,
работающего с частотой 10 Гц, превращайте ее в следующий по
алфавиту символ. Остановите таймер после вывода всех символов
символьной таблицы.
Задание
B13.
Фигура
плавно
перемещается
по
развертывающейся спирали, уравнение которой в параметрической
форме имеет вид x = t sin t, y = t cos t. Параметр t изменяйте от 2 до
20 с шагом 0,05, значения x и y умножайте на 8.
Задание C13. Массив записываемых в файл данных должен
представлять собой последовательный ряд из 500 коротких (тип
short) целых чисел.
Задание D13. Первый поток выводит в левую половину окна
квадрат, нарисованный толстым цветным пером, и попеременно
изменяет цвет этой половины окна, в том числе и цвет внутри
квадрата.
Второй поток выводит в правую половину состояние левой
клавиши мыши. Состояние клавиш мыши или клавиатуры можно
получить с помощью функции GetAsyncKeyState().Состояние
анализируемой клавиши индицируйте, выводя на экран
соответствующие надписи.
99
Задание A14. В середину главного окна зеленого цвета
выведите светло-фиолетовый круг диаметром 100 – 120 пикселов, в
правой части которого вырезан узкий сектор. В последовательных
тактах таймера, работающего с частотой 2 Гц, сжимайте и
раскрывайте этот сектор, получая стилизованное изображение
закрывающегося и открывающегося рта. Остановите таймер после
10 периодов смены изображения. Круг с вырезанным сектором
рисуется функцией Pie().
Задание B14. Фигура плавно перемещается по диагонали от
левого нижнего края окна до правого верхнего, скачком
возвращается назад и повторяет движение по диагонали. В каждом
шаге обе координаты фигуры изменяйте на 1 пиксел.
Задание C14. Массив записываемых в файл данных должен
представлять собой последовательный ряд из 10000 целых четных
(тип unsigned int) чисел в убывающем порядке.
Задание D14. Первый поток выводит в верхнюю половину окна
попеременно то два, то три концентрических круга.
Второй поток выводит в нижнюю половину окна цветными
символами координаты рабочей области окна.
Изменяя размер окна приложения, проанализируйте работу
программы.
Задание A15. В середину главного окна темно-синего цвета
выведите толстую светло-голубую линию длиной 50 – 100
пикселов под углом к вертикали. В каждом такте таймера,
работающего с частотой 5 Гц, изменяйте ее положение так, чтобы
она качалась вправо-влево вокруг верхней точки наподобие
маятника. Остановите таймер после 10 периодов качания.
Задание B15. Фигура плавно перемещается по дуге, уравнение
которой в параметрической форме имеет вид x = – sin t, y = cos2 t.
Шаг изменения параметра t задайте равным 0,02, значения x и y
умножайте на 80.
Задание C15. Массив записываемых в файл данных должен
представлять собой последовательный ряд из 4000 коротких (тип
USHORT) целых нечетных чисел.
Задание D15. Первый поток выводит в левую половину окна
черный квадрат и изменяет попеременно цвет фона под ним, т. е.
цвет левой половины окна.
100
Второй поток выводит в правую половину окна произвольный
цветной символ со случайными координатами.
Задание A16. В середине главного окна синего цвета нарисуйте
тонкий серп старой луны желтого цвета. В каждом такте таймера,
работающего с частотой 2 Гц, уменьшайте на 1 – 2 пиксела
толщину полумесяца, чтобы он, пройдя через новолуние,
превратился в изображение молодой луны, после чего остановите
таймер.
Задание B16. Фигура плавно перемещается по горизонтали от
правого края окна до левого, скачком возвращается назад и
повторяет плавное движение влево. В каждом шаге x-координату
фигуры изменяйте на 1 пиксел.
Задание C16. Массив записываемых в файл данных должен
представлять собой последовательный ряд из 256 чисел типа char.
Задание D16. Первый поток выводит в верхнюю половину окна
попеременно то левую четверть, то правые три четверти цветного
круга.
Второй поток выводит точно в центр рабочей области нижней
половины окна цветной символ.
Изменяя размер окна приложения, проанализируйте работу
программы.
Задание A17. В середину главного окна бледно-желтого цвета
выведите залитый коричневым цветом круг диаметром 50 – 100
пикселов. В каждом такте таймера, работающего с частотой 10 Гц,
увеличивайте горизонтальный диаметр круга на 2 – 3 пиксела,
превращая его тем самым во все более вытянутый эллипс.
Положение центра эллипса оставляйте неизменным. Остановите
таймер, когда изображение эллипса займет весь экран по ширине.
Задание B17. Фигура плавно качается по вертикали в пределах
окна. Такое движение можно получить, если при неизменной
x-координате фигуры ее y-координату определять по формуле
y = sin t. Шаг изменения параметра t задайте равным 0,03, значения
y умножайте на 90.
Задание C17. Массив записываемых в файл данных должен
представлять собой последовательный ряд из 1000 целых чисел,
начинающихся с 4096.
101
Задание D17. Первый поток выводит в левую половину окна
цветной квадрат и периодически изменяет фон этой половины
окна, закрашивая ее штриховой кистью с разной ориентацией
штрихов.
Второй поток выводит в правую половину окна цветными
символами координаты курсора мыши в рабочей области окна.
Экранные координаты курсора мыши можно получить с помощью
функции GetCursorPos(); для преобразования их в координаты
рабочей области используется функция ScreenToClient().
Задание A18. В середине главного окна темно-синего цвета
нарисуйте тонкий белый лунный серп рогами вниз (чего никогда не
бывает в наших широтах), слегка наклоненный в одну сторону. В
каждом такте таймера, работающего с частотой 5 Гц, покачивайте
серп, наклоняя его то в одну, то в другую сторону. Остановите
таймер после 10 периодов качания.
Задание B18. Фигура плавно перемещается по очень узкому
эллипсу, лежащему горизонтально, уравнение которого в
параметрической форме имеет вид x =90 sin t, y =10 cos t. Шаг
изменения параметра t задайте равным 0,03.
Задание C18. Массив записываемых в файл данных должен
представлять собой последовательный ряд из 1000 целых нечетных
чисел в убывающем порядке.
Задание D18. Первый поток выводит в верхнюю половину окна
цветной символ с случайными координатами (в пределах этой
половины окна). Для повышения наглядности не затирайте
изображение в каждом такте вывода, накапливая выводимые
символы на экране.
Второй поток выводит в нижнюю половину окна попеременно
то правую, то левую половины окружности, рисуемые толстыми
перьями разных цветов. Часть окружности рисуется функцией
Arc().
Задание A19. В середину главного окна желтого цвета выведите
один произвольный символ ярко-синего цвета. В каждом такте
таймера, работающего с частотой 50 Гц, перемещайте символ на
небольшой угол по кругу радиусом 50 – 80 пикселов. Уравнение
круга в параметрической форме имеет вид x = sin t, y = cos t. Шаг
102
изменения параметра t задайте 0,05, а число шагов таким, чтобы
получить один полный период графика. Выполните задание в двух
вариантах – с затиранием фона окна при его перерисовке и без
затирания фона.
Задание B19. Фигура плавно перемещается по образующей
вырожденной
фигуры
Лиссажу,
уравнение
которой
в
параметрической форме имеет вид x = sin t, y = cos(t + π/2). В
каждом шаге параметр t изменяйте на 0,02, значения x и y
умножайте на 80. Для константы π/2 в языке С++ существует
обозначение M_PI_2.
Задание C19. Массив записываемых в файл данных должен
представлять собой последовательный ряд из 500 коротких (тип
short) целых четных чисел в убывающем порядке.
Задание D19. Первый поток рисует в левой половине толстым
цветным пером изображение прозрачного квадрата попеременно то
большего,
то
меньшего
размера
с
концентрическим
расположением.
Второй поток выводит в правую половину окна круг достаточно
большого размера, закрашенный первоначально черным цветом.
Значение цвета кисти, которой закрашивается круг, периодически
увеличивается с шагом 3 – 5 единиц.
Задание A20. В нижнюю часть достаточно высокого главного
окна светло-зеленого цвета выведите короткую строку текста
темно-зелеными буквами. В каждом такте таймера, работающего с
частотой 10 Гц, смещайте строку вверх на 5 – 6 пикселов.
Остановите таймер, когда строка дойдет до верхнего края окна.
Задание B20. Фигура плавно перемещается по свертывающейся
спирали, уравнение которой в параметрической форме имеет вид
x = sin t / 0.1t, y = t cos t / 0.1t. Параметр t изменяйте от 5 до 30 с
шагом 0,05, значения x и y умножайте на 80.
Задание C20. Массив записываемых в файл данных должен
представлять собой последовательный ряд из 256 чисел типа
UCHAR в убывающем порядке.
Задание D20. Первый поток выводит в верхнюю половину
окрашенного окна большой круг, кисть для заливки которого
выбирается случайным образом из трех созданных заранее кистей
103
разных цветов. Для того чтобы наглядно фиксировать моменты
смены кисти, в центр круга выводится номер такта этого потока.
Первый поток выводит в нижнюю половину окна цветными
символами текущее время (часы, минуты и секунды), получаемое с
помощью функции GetLocalTime().
Задание A21. В середину главного окна фиолетового цвета,
квадратной формы и достаточно большого размера выведите
залитый красным цветом круг небольшого диаметра. В каждом
такте таймера, работающего с частотой 10 Гц, увеличивайте
диаметр круга на 3 – 4 пиксела, оставляя неизменным положение
его центра. Остановите таймер, когда круг займет все окно.
Задание B21. Фигура периодически в пределах окна плавно
перемещается по экспоненциальной кривой, описываемой
уравнением y = 1 – e –2 t. Шаг изменения параметра t задайте
равным 0,03, значения y умножайте на 180. В каждом шаге xкоординату фигуры изменяйте на 1 пиксел.
Задание C21. Массив записываемых в файл данных должен
представлять собой последовательный ряд из 1000 коротких (тип
unsigned short) нечетных целых чисел в убывающем порядке.
Задание D21. Первый поток выводит в левую половину
окрашенного окна треугольник, заливаемый попеременно то
синим, то зеленым цветом. Треугольник рисуется с помощью
функции Polygon().
Второй поток выводит в верхнюю часть правой половины
окрашенного окна небольшой цветной круг, который в каждом
такте вывода смещается на 1 – 2 пиксела вниз.
Задание A22. В середину главного окна светло-зеленого цвета
выведите залитую темно-зеленым цветом половину круга
диаметром 50 – 100 пикселов. В каждом такте таймера,
работающего с частотой 5 Гц, изменяйте изображение так, чтобы
выводилась то правая, то левая половина круга. Остановите таймер
после 10 периодов смены изображения.
Задание B22. Фигура плавно перемещается по дуге, уравнение
которой в параметрической форме имеет вид x = – sin2 t, y = cos t.
Шаг изменения параметра t задайте равным 0,02, значения x и y
умножайте на 80.
104
Задание C22. Массив записываемых в файл данных должен
представлять собой последовательный ряд из 2000 целых чисел,
начинающихся с числа 256.
Задание D22. Первый поток выводит в верхнюю половину окна
квадрат, попеременно окрашивая его в синий, зеленый и красный
цвета.
Второй поток выводит в нижнюю половину окна цветными
символами объем свободной физической памяти, который можно
получить с помощью функции GlobalMemoryStatus().
Запуская вместе с испытываемой программой другие приложения
Windows (ваши или системные), пронаблюдайте за использованием
физической памяти.
Задание A23. В середину главного окна серого цвета выведите
достаточно большой синий квадрат, образовав его с помощью
функции RoundRect(). Начальные значения диаметров
скругляющих эллипсов примите равными 0. В каждом такте
таймера, работающего с частотой 20 Гц, увеличивайте ширину
обоих скругляющих эллипсов на 2 – 3 пиксела, и остановите
таймер, когда ширина эллипсов достигнет диаметра круга.
Задание B23. Фигура периодически в пределах окна плавно
перемещается по кривой Гаусса, описываемой уравнением y =
= e – (t – 20) (t – 20) / 20. Шаг изменения параметра t задайте равным
0,2, значения y умножайте на 180. В каждом шаге x-координату
фигуры изменяйте на 1 пиксел.
Задание C23. Массив записываемых в файл данных должен
представлять собой последовательный ряд из 1000 целых чисел в
убывающем порядке.
Задание D23. Первый поток выводит в левую половину
окрашенного окна нижнюю часть окружности (дугу) с раствором
135°, нарисованную толстым цветным пером. Дуга покачивается в
каждом такте потока вокруг центра окружности на угол 45°. Дуга
окружности рисуется функцией Arc().
Второй поток выводит в правую половину окна текущую
позицию курсора мыши в рабочей области окна. Экранные
координаты курсора мыши можно получить с помощью функции
105
GetCursorPos(); для преобразования их в координаты рабочей
области используется функция ScreenToClient().
Задание A24. В середину главного окна зеленого цвета
выведите небольшой бледно-желтый квадрат. В каждом такте
таймера, работающего с частотой 2 Гц, увеличивайте
горизонтальный размер фигуры на 4 – 5 пикселов, преобразуя его
во все более вытянутый прямоугольник и оставляя неизменным
положение его центра. Остановите таймер, когда фигура займет все
окно по горизонтали.
Задание B24. Фигура плавно перемещается по диагонали от
правого нижнего края окна до левого верхнего, скачком
возвращается назад и повторяет движение по диагонали. В каждом
шаге обе координаты фигуры изменяйте на 1 пиксел.
Задание C24. Массив записываемых в файл данных должен
представлять собой последовательный ряд из 256 чисел типа BYTE.
Задание D24. Первый поток периодически изменяет фон
верхней половины окна, задавая ему три разных оттенка желтого
цвета.
Второй поток выводит в нижнюю половину окна цветными
символами текущие координаты главного окна приложения на
рабочем столе, которые можно получить с помощью функции
GetWindowRect(). Перемещая окно приложения по рабочему
столу Windows, убедитесь в том, что программа работает
правильно.
Задание A25. В середину главного окна темно-синего цвета
выведите символ цифры ноль красного цвета. В каждом такте
таймера, работающего с частотой 5 Гц, увеличивайте значение
выводимой цифру, изменяя ее от 0 до 9. По достижении цифры 9
изменяйте ее на ноль. Остановите таймер после прохождения трех
полных периодов смены цифр.
Задание B25. Фигура периодически в пределах окна плавно
перемещается по кривой Гаусса, описываемой уравнением y =
= – e – (t – 25) (t – 25) / 100. Шаг изменения параметра t задайте равным
0,2, значения y умножайте на 180. В каждом шаге x-координату
фигуры изменяйте на 1 пиксел.
106
Задание C25. Массив записываемых в файл данных должен
представлять собой последовательный ряд из 1000 целых чисел,
кратных 5.
Задание D25. Первый поток выводит в левую половину окна
цветную окружность, нарисованную толстым цветным пером.
Окружность то заливается цветом своей образующей, то кажется
прозрачной.
Второй поток выводит в правую половину окна цветными
символами текущее время (часы, минуты и секунды), получаемое с
помощью функции GetLocalTime().
Задание A26. В середину главного окна зеленого цвета
выведите произвольный красный символ. В каждом такте таймера,
работающего с частотой 20 Гц, перемещайте символ на небольшой
угол по образующей фигуры Лиссажу, уравнение которой в
параметрической форме имеет вид x = sin t, y = cos (a t + ).
Выберите значение a = 1,5,  – от 0,05 до 0,5. Эффективный радиус
кривой увеличьте до 40 – 50 пикселов, а шаг изменения параметра t
задайте 0,1. Подберите такое число шагов, чтобы получить один
полный период графика. Выполните задание в двух вариантах – с
затиранием фона окна при его перерисовке и без затирания фона.
Задание B26. Фигура плавно перемещается по прямой линии,
подчиняясь зависимости y = x / 4 + 15 от левого края окна до
правого, скачком возвращается назад и повторяет движение
вправо. В каждом шаге x-координату фигуры изменяйте на 1
пиксел.
Задание C26. Массив записываемых в файл данных должен
представлять собой последовательный ряд из 256 чисел типа char,
начинающихся с –127.
Задание D26. Первый поток выводит в левую половину окна
попеременно изображения то круга, то эллипса с общим центром.
Фигуры должны быть залиты каким-либо цветом.
Второй поток выводит в правую половину окна цветными
символами время выполнения предыдущего такта этого потока
(вместе с интервалом ожидания, задаваемым функцией Sleep()).
Задание A27. В середину главного окна черного цвета выведите
закрашенный кистью яркого красного цвета круг диаметром 150 –
107
200 пикселов. В каждом такте таймера, работающего с
максимальной частотой (20 Гц), увеличивайте яркость зеленого
компонента цвета кисти на 2 – 3 единицы интенсивности и
наблюдайте изменение цвета круга. Остановите таймер после
завершения двух периодов смены цвета. Выполните задание в двух
вариантах – с затиранием фона окна при его перерисовке и без
затирания фона.
Задание B27. Фигура плавно перемещается по косинусоиде x =
= sin t вдоль вертикали от нижнего края окна до верхнего, скачком
возвращается назад и повторяет движение вверх. В каждом шаге
y-координату фигуры изменяйте на 1 пиксел, параметр t на 0,05;
значения x умножайте на 80.
Задание C27. Массив записываемых в файл данных должен
представлять собой последовательный ряд из 1024 чисел типа
BYTE.
Задание D27. Первый поток выводит в верхнюю половину окна
изображение левой половины цветного круга попеременно то
большего, то меньшего диаметра.
Второй поток выводит в нижнюю половину окна цветными
символами текущую секунду, получаемую с помощью функции
GetLocalTime().
Задание A28. В верхнем левом углу главного окна зеленого
цвета нарисуйте залитый синим цветом небольшой треугольник с
вершиной, расположенной на диагонали окна. В каждом такте
таймера, работающего с частотой 10 Гц, изменяйте координату
вершины так, чтоб она оставалась на диагонали, но перемещалась в
сторону нижнего правого угла окна. Остановите таймер после того,
как треугольник, удлиняясь, достигнет угла окна. Выполните
задание в двух вариантах – с затиранием фона окна при его
перерисовке и без затирания фона. Треугольник рисуется с
помощью функции Polygon().
Задание B28. Фигура плавно перемещается по кривой y = sin 3 t
вдоль горизонтали от левого края окна до правого, скачком
возвращается назад и повторяет движение вправо. В каждом шаге
y-координату фигуры изменяйте на 1 пиксел, параметр t на 0,05;
значения y умножайте на 80.
108
Задание C28. Массив записываемых в файл данных должен
представлять собой последовательный ряд из 64 К целых чисел
типа WORD.
Задание D28. Первый поток выводит в нижнюю половину окна
цветной треугольник с вертикальным основанием и обращенный
вершиной то вправо, то влево. Треугольник рисуется с помощью
функции Polygon().
Второй поток выводит в верхнюю половину окна
последовательно наращиваемое число, изображаемое цветными
цифрами.
Задание A29. В каждом такте таймера, работающего с частотой
10 Гц, выводите в окно темно-синего цвета размером 400  400
пикселов ярко-желтый символ звездочки (*) в позицию со
случайными координатами. Остановите таймер после вывода
заданного числа (50 – 100) символов. Выполните задание в двух
вариантах – с затиранием фона окна при его перерисовке и без
затирания фона.
Задание B29. Фигура плавно перемещается по кривой y = | sin t |
вдоль горизонтали от левого края окна до правого, скачком
возвращается назад и повторяет движение вправо. В каждом шаге
y-координату фигуры изменяйте на 1 пиксел, параметр t на 0,05;
значения y умножайте на 80.
Задание C29. Массив записываемых в файл данных должен
представлять собой ряд из 16 четных чисел типа int.
Задание D29. Первый поток выводит в левую половину окна то
верхнюю, то нижнюю половину цветного круга.
Второй поток выводит в правую половину окна
последовательные случайные числа в пределах от 0 до 99.
Задание A30. В середину главного окна черного цвета выведите
закрашенный белой кистью квадрат с размером сторон 150 – 200
пикселов. В каждом такте таймера, работающего с максимальной
частотой (20 Гц), уменьшайте яркость всех трех компонентов цвета
кисти на 2 – 3 единицы интенсивности и наблюдайте изменение
цвета фигуры. Остановите таймер после завершения двух периодов
смены цвета.
109
Задание B30. Фигура плавно перемещается по горизонтали,
касаясь нижнего края окна, от левого края окна до правого.
Коснувшись правого края, фигура изменяет направление движения
и начинает перемещаться влево. Коснувшись левого края окна,
фигура опять изменяет направление движения и т. д. В каждом
шаге
x-координату фигуры изменяйте на 1 пиксел.
Задание C30. Массив записываемых в файл данных должен
представлять собой последовательный ряд уменьшающихся чисел
типа char от 255 до 0.
Задание D30. Первый поток выводит в правую половину окна
попеременно цветные буквы “A” или “Z”.
Второй поток выводит в случайные места левой половины окна
небольшие цветные кружки. Для повышения наглядности не
затирайте изображение в каждом такте вывода, накапливая
выводимые фигуры на экране.
Задание A31. В каждом такте таймера, работающего с
максимальной частотой (20 Гц), выводите в окно светло-голубого
цвета размером 400  400 пикселов небольшой (диаметром 20 – 30
пикселов) ярко-желтый круг в позицию со случайными
координатами. Диапазон координат подберите так, чтобы все
выводимые фигуры не выходили за пределы окна. Остановите
таймер после вывода заданного числа (100 – 200) фигур.
Выполните задание в двух вариантах – с затиранием фона окна при
его перерисовке и без затирания фона.
Задание B31. Фигура плавно перемещается по образующей
фигуры Лиссажу, уравнение которой в параметрической форме
имеет вид x = sin t, y = cos(2t + 2). В каждом шаге параметр t
изменяйте на 0,02, значения x и y умножайте на 80. Для константы
π/2 в языке С++ существует обозначение M_PI_2.
Задание C31. Массив записываемых в файл данных должен
представлять собой последовательный ряд из 1 миллиона целых
чисел типа int.
Задание D31. Первый поток выводит в верхнюю половину окна
то зеленый прямоугольник, то вписанный в те же координаты
синий эллипс.
110
Второй поток выводит в нижнюю половину окна цветными
символами число секунд, истекших от момента загрузки
операционной системы, которое можно получить с помощью
функции GetTickCount(). Оцените, когда была в последний раз
загружена операционная система на вашем компьютере?
Задание A32. В середину главного окна белого цвета выведите
закрашенный кистью черного цвета треугольник с размером сторон
150 – 200 пикселов. В каждом такте таймера, работающего с
максимальной частотой (20 Гц), увеличивайте яркость всех трех
компонентов цвета кисти на 2 – 3 единицы интенсивности и
наблюдайте изменение цвета фигуры. Остановите таймер после
завершения двух периодов смены цвета. Выполните задание в двух
вариантах – с затиранием фона окна при его перерисовке и без
затирания фона. Треугольник рисуется с помощью функции Polygon().
Задание B32. Фигура плавно перемещается по вертикали,
касаясь левого края окна, от верхнего края окна до нижнего.
Коснувшись нижнего края, фигура изменяет направление движения
и начинает перемещаться вверх. Коснувшись верхнего края окна,
фигура опять изменяет направление движения и т. д. В каждом
шаге y-координату фигуры изменяйте на 1 пиксел.
Задание C32. Массив записываемых в файл данных должен
представлять собой последовательный ряд из 64 K чисел типа
WORD.
Задание D32. Первый поток выводит в левую половину окна
изображение правой половины цветного круга попеременно то
большего, то меньшего диаметра.
Второй поток выводит в правую половину окна цветными
символами время выполнения предыдущего такта первого потока
(вместе с интервалом ожидания, задаваемым функцией Sleep()).
Для
определения
длительности
фрагмента
программы
воспользуйтесь функцией мультимедийного таймера timeGetTime().
111
Список литературы
1. Финогенов К. Г. Лабораторный практикум “Основы
разработки приложений Windows”. Книга 1. М.: МИФИ, 2004.
2. Финогенов К. Г. Лабораторный практикум “Основы
программирования на языке С++”. М.: МИФИ, 2004.
3. Финогенов К. Г. Win32. Основы программирования. М.:
ДИАЛОГ-МИФИ, 2002.
112
К. Г. Финогенов
Лабораторный практикум
«Основы разработки приложений
Windows»
Книга 2
Редактор Н.В. Шумакова
Формат 60  84 1/16
Тираж 150 экз.
Заказ
Подписано в печать 00.00.2005 г.
Печ. л. 6,75
Уч.-изд. л. 6,75
Изд. № 019-1
Московский инженерно-физический институт (государственный университет)
Типография МИФИ. 115409, Москва, Каширское шоссе, 31
113
Download