СОЗДАНИЕ ПОТОКА
Создание потока в большей степени (внешне, конечно) напоминает программу для Windows, чем создание процесса. Дело в том, что для создания потока используется функция CreateThread() (аналог WinMain()), одним из аргументов которой является указатель на функцию потока (аналог оконной функции). Но давайте обо всем по порядку.
Итак, начнем по уже сложившейся традиции, с прототипа функции. Она описана в файле winbase.h:
WINBASEAPI HANDLE WINAPI CreateThrcadf
LPSECURITY_ATTRIBUTES IpThrcadAttributes, DWORD dwStackSizc,
LPTHREAD_STARTJlOUTINEIpStartAddress, LPVOID IpParameler. DWORD dwCreationFlags, LPDWORD IpThreadld);
При вызове этой функции происходит следующее:
в памяти создаются все необходимые для управления потоком структуры (назовем их объектом «поток»);
код завершения потока инициализируется значением STILL_ACTIVE;
создается структура типа CONTEXT для потока (к сожалению, я не могу описать структуру в рамках этой книги - она слишком велика, но рекомендую читателю самостоятельно разобраться с ней по заголовочным файлам и файлам системы помощи);
создается стек потока;
инициализируется регистр - указатель стека в структуре типа CONTEXT так, чтобы он указывал на верхнюю границу стека, а регистр -указатель команд - на точку входа функции потока.
Рассмотрим аргументы этой функции.
АРГУМЕНТЫ ФУНКЦИИ CREATETHREADQ
Первый аргумент - IpThreadAttnbutes - является указателем на структуру типа SECURITY_ATTRIBUTES. Так как в Windows'95 атрибуты безопасности не используются, то обычно этот аргумент равен NULL.
Второй аргумент - dwStackSize - определяет размер выделяемого потоку стека. Если в качестве этого параметра указан 0, то поток будет иметь стек такого же размера, как и у породившего его потока.
Третий аргумент этой функции - IpStartAddress - собственно и определяет поток, так как является адресом точки входа функции потока. Функ-
ция потока может иметь имя, определяемое программистом, но должна иметь следующий прототип:
DWORD WINAPI ThreadFunctionfLPVOID IpParameter);
Я не случайно дал аргументу этой функции и четвертому аргументу функции CreateThreadQ одинаковые имена. Четвертый аргумент функции CreateThreadQ - это параметр, передаваемый функции потока. Что и каким образом передается в этом параметре, совершенно неважно. Это могут быть всевозможные данные, которые функция потока может использовать для своей работы.
Если следующий аргумент - dwCreationFlags - равен нулю, то выполнение потока начнется немедленно. Если этот аргумент будет равен CREATE_SUSPENDED, то начало выполнение потока будет задержано до определенных событий, например, до вызова функции ResumeThreadQ.
И наконец, в значение, определяемое последним аргументом, IpThreadld, записывается идентификатор созданного потока. А значение, возвращаемое функцией, является хэндлом этого потока.
Раз у потока есть начало, то должно быть и
ЗАВЕРШЕНИЕ ПОТОКА
Как и процесс, поток может быть завершен двумя способами - вызовом функции ExitThreadQ и обращением к функции TenninateThread(). Отличаются они друг от друга примерно тем же, что и функции ExitProcessQ и TerminateProcessQ. Первая функция, ExitThreadQ, используется для нормального завершения потока. Естественно, что она вызывается изнутри потока. Она описана в файле winbase.h:
WINBASEAPI VOID WINAPI ExitThread(DWORD dwExitCode);
Единственным ее аргументом является двойное слово, в которое будет помещен код возврата этой функции.
Функцию TerminateProcess(), описанную в том же файле winbase.h следующим образом,
WINBASEAPI BOOL WINAPI TerminateThreadfHANDLE hThread,
DWORD dwExitCode);
следует вызывать только в крайних случаях, когда поток завис, и ни на какие действия пользователя не реагирует. Функция вызывается из какого-либо внешнего (по отношению к завершаемому) потока, а ее
аргументами являются хэндл завершаемого потока и двойное слово, в которое будет записан код завершения потока.
Осталось только узнать, что происходит при завершении потока. Во-первых, освобождаются или удаляются все занятые или созданные объекты. Это действие является стандартным и ничего особенного собой не представляет. Во-вторых, поток получает статус незанятого (signaled). В-третьих, код завершения процесса меняется со STILL ACTIVE на указанный при вызове завершающей поток функции. В-четвертых, уменьшается счетчик пользователей потока. Если пользователей потока больше не осталось, и поток является единственным потоком процесса, то завершается и процесс. Все легко, просто и логично.
СИНХРОНИЗАЦИЯ
К этому моменту читатель уже знает, что в программе один поток, главный, запускается автоматически при запуске. Следовательно, создавая новые потоки, мы тем самым делаем программу многопотоковой. Хорошо, конечно, если эти потоки работают независимо. А как быть тем потокам, которые зависят друг от друга? Например, осуществляют доступ к одному и тому же файлу или продолжение работы одного зависит от выполнения какого-то условия в другом? Для решения этих проблем в Win32 предусмотрен механизм синхронизации, который позволяет, что следует из его названия, синхронизировать работу потоков.
Обычно поток, работа которого зависит каким-то образом от другого потока, сообщает системе о том, какое событие он ожидает. После этого выполнение этого потока приостанавливается до наступления ожидаемого события. Обычно для синхронизации используются четыре типа объектов - семафоры, исключающие семафоры (объекты типа mutex), события и критические секции. Далее мы поговорим об этих объектах.
СЕМАФОРЫ
Семафор действует как обычный флажок, и используется для того, чтобы определить, свободен или нет в настоящее время требующийся потоку ресурс. Тем не менее, фактически семафор является не просто флагом. Для того чтобы создать семафор, приложение должно вызвать функцию CreateSemaphore(), описание которой находится в файле winbase.h и выглядит следующим образом:
WINBASEAPI HANDLE WINAPI CreateSemaphoreA(
LPSECURITY_ATTRIBUTESlpSemaphoreAtlributes, LONG ITnitialCount. LONG IMaximumCount, LPCSTR IpName);
WINBASEAPI HANDLE WINAPI CreateSemaphoreWf
LPSECURITY_ATTRIBUTESlpSemaphoreAttributes, LONG UnitialCount, LONG IMaximumCount. LPCWSTR IpNamc);
#ifdef UNICODE
#define CreateSemaphore CreateSemaphoreW
#else
#dcfine CreateSemaphore CreateSemaphoreA
#endif// IUNICODE
Первый аргумент, что и следует из его типа, является указателем на структуру, содержащую атрибуты доступа к семафору. Он может также принимать значение NULL в том случае, если эти атрибуты не используются, как, например, в Windows'95.
Второй аргумент - начальное значение счетчика учета ресурсов. Этот аргумент определяет, сколько потоков может получить доступ к ресурсам в момент вызова функции. К примеру, компьютер имеет три порта, к которым обращается программа. В этом случае значение счетчика учета ресурсов может быть в пределах от 0 (нет свободных портов) до трех (все порты свободны). При обращении потока к ресурсу система проверяет, свободен ли ресурс, т. е. не установлено ли максимальное значение счетчика учета ресурсов (третий аргумент функции), после чего разрешает или запрещает доступ к ресурсу. Если для потока ресурсы недоступны, то он будет ждать освобождения ресурсов.
Последний, четвертый аргумент, - это указатель на строку, содержащую имя семафора.
При успешном завершении функция возвращает хэндл созданного семафора. Возвращение NULL сигнализирует о том, что произошла ошибка.
Если два процесса используют семафор с одним и тем же именем, то в этом случае используется один и тот же семафор. Использование этого семафора и является способом синхронизации потоков.
Перед завершением потока, использующего семафор, последний должен быть освобожден. Это делается с помощью функции ReleaseSemaphoreQ, описание которой, взятое из файла winbase.h, приведено ниже:
WINBASEAPI BOOL WINAPI ReleaseSemaphore(HANDLE hSemaphore,
LONG IReleaseCount, LPLONG IpPreviousCount);
Первый аргумент - это хэндл семафора, полученный с помощью функции CreateSemaphore(). Второй аргумент определяет, какое значение
должно быть установлено в счетчике ресурсов семафора при его освобождении. В двойное слово, адрес которого определяется третьим аргументом, записывается предыдущее значение счетчика.
Таблица 55. Флаги доступа к семафору
Параметр
Описание
SEMAPHORE_ALL_ACCESS
SEMAPHORE_MODIFY_STATE
SYNCHRONIZE
Устанавливает все возможные флаги доступа к семафору
Разрешается изменение счетчика ресурсов в функции ReleaseSemaphoreQ Разрешается использование в любой из ожидающих функций сигнала об изменении состояния семафора
Поток, которому заведомо известно, что семафор уже создан, может не создавать семафор, а открыть его с помощью функции OpenSemaphoreQ. Ниже приведено описание этой функции, взятое из файла winbase.h:
WINBASEAPI HANDLE WINAPI OpenSemaphoreA(DWORD dwDesiredAccess,
BOOL blnheritHandle, LPCSTR IpName);
WINBASEAPI HANDLE WINAPI OpenSemaphoreW(DWORD dwDesiredAccess,
BOOL blnheritHandle, LPCWSTR IpName);
#ifdefUNICODE
#defme OpenSemaphore OpenSemaphoreW
#else
#defme OpenSemaphore OpenSemaphoreA
#endif// ! UNICODE
Первый аргумент определяет уровень доступа к семафору и может принимать значения, приведенные в табл. 55.
Второй аргумент определяет, наследуют ли этот семафор другие процессы, создаваемые функцией CreateProcessQ. Значение TRUE говорит о том, что семафор является наследуемым.
Главным аргументом в этой функции является третий аргумент, определяющий имя открываемого семафора. Если функция выполняется успешно, то она возвращает хэндл открытого семафора.
Созданный или открытый семафор можно использовать с помощью функции WaitForSingleObject(), описание которой, приведенное ниже, можно найти в файле winbase.h:
WINBASEAPI DWORD WINAPI WaitForSingleObject(HANDLE hHandle,
DWORD dwMilliseconds);
Первый аргумент функции очевиден - хэндл семафора. Второй аргумент определяет время ожидания наступления события в миллисекундах. Если это значение равно 0, то функция сразу же прекращает ожидание и возвращает управление. Если время ожидания определено как INFINITE, то ожидание наступления события не прекращается. Функция может вернуть значения, приведенные в табл. 56.
Алгоритм работы с семафорами выглядит следующим образом:
поток создает или открывает семафор с помощью функций CreateSemaphore() или OpenSemaphore() соответственно;
поток вызывает функцию WaitForSingleObjectQ (или WaitForMultipleObjects()) Для того, чтобы определить, свободен ли требующийся потоку ресурс. В зависимости от результата, возвращаемого этой функцией, определяются дальнейшие действия;
при завершении поток вызывает функцию ReleaseSemaphoreQ, освобождающую семафор.
СОБЫТИЯ
События являются самой примитивной разновидностью объектов синхронизации. Они используются для того, чтобы оповестить поток о том, что наступило ожидаемое событие. Эти объекты обычно используются для того, чтобы синхронизировать потоки, которые работают по принципу конвейера. К примеру, один поток опрашивает датчики и загружает считанные значения в буфер. Другой поток считывает эти данные из буфера и производит их обработку. Первый поток может сигнализировать второму о том, что событие - заполнение буфера -наступило. Второй поток может сигнализировать первому о том, что наступило другое событие - данные из буфера считаны, ожидается новая порция данных. Событие может иметь два состояния - занятое (nonsignaled) и свободное (signaled).
Таблица 56. Значения, возвращаемые функцией WaitForSingleObjectQ
Параметр |
Значение |
Описание |
WAIT OBJECT 0 WAITJTIMEOUT WAIT_ABANDONED WAIT_FAILED |
0x00000000 0x00000102 0x00000080 OxFFFFFFFF |
Объект перешел в состояние свободного Объект за указанное время не перешел в состояние свободного Объект mutex стал свободным из-за отказа от него Произошла ошибка |
WINBASEAPI HANDLE W1NAPI CreateEventA(
LPSECURITY_ATTRIBUTESlpEvc[itAttributes,
BOOL bManualResel,
BOOL blnitialState,
1.PCS1 R IpName); WINBASEAPI HANDLE WINAPI CreatcEvcntW(
LPSnCURITY_ATTRIBUTES IpEveiuAtlribules,
BOOL bManualReset,
BOOL blnitialState,
LPCWSTR IpName);
#ifdef UNICODE
#definc CreateEvcnt CreateEvcntW
«else
Adeline CreateEvent CreateEventA
#endif// IUNICODE
С первым аргументом этой функции - указателем на структуру, содержащую атрибуты доступа, мы уже знакомы.
Второй аргумент определяет тип создаваемого события. Если значение этого параметра равно TRUE, то создается объект, для сброса которого в свободное состояние необходимо использовать функцию ResetEventQ. При значении FALSE создается событие, автоматически сбрасывающееся в свободное состояние.
Третий аргумент определяет начальное состояние создаваемого события. Значение TRUE определяет, что создается событие в СВОБОДНОМ состоянии. Если поток намерен создать событие в занятом состоянии, то он должен установить этот параметр в FALSE.
И наконец, последний, четвертый аргумент, определяет имя создаваемого объекта - события.
При успешном выполнении функция возвращает хэндл созданного объекта-события. Если при выполнении функции встретилась ошибка, то возвращается значение NULL.
Для того чтобы сигнализировать о наступлении события, в потоке должна присутствовать функция SetEventQ, переводящая событие в свободное состояние, описанная в winbase.h следующим образом:
WINBASEAPI BOOL WINAPI Se(Event(HANDLE hEvent);
Единственный аргумент этой функции очевиден - хэндл события, созданного посредством CreateEventQ.
Каким образом прикладная программа может узнать о наступлении события? Да с помощью уже знакомой нам функции WaitForSingleObjectf).
Для того чтобы сбросить событие в занятое состояние, необходимо вызвать функцию ResetEvent(), описанную в том же winbase.h следующим образом:
WINBASEAPI BOOL WINAPI ResetEvent(HANDLE hEvent);
В качестве аргумента функции передается хэндл события.
Достаточно часто встречаются случаи, когда после установки события с помощью SetEventQ тут же следует вызов ResetEventQ. Для этих целей предусмотрена функция PulseEvent(), описанная так:
WINBASHAPI BOOL WINAPI PulscEvcnt(HANDLE hEvent);
Аргументом является хэндл события. После выполнения функции объект-событие остается в занятом состоянии.
Алгоритм использования объекта-события полностью аналогичен алгоритму использования семафора.
Надеюсь, что после того, что сейчас узнал читатель, разобрать критические секции и объекты типа mutex ему не составит труда.
ДИНАМИЧЕСКИ ПОДКЛЮЧАЕМЫЕ БИБЛИОТЕКИ
Думаю, что, посмотрев на размер исполняемого файла, полученного после компиляции нашей первой программы «Helloworld», многие были поражены. Как! Столько всего умеет делать программа при таком малом размере! Даже на ассемблере невозможно написать библиотеку такого размера и обладающую такими возможностями. Как же это сделано'? Ответ прост - большая часть кода, обеспечивающего возможности программы, находится вне самое программы, в библиотеках. Это естественно и понятно. Но с исполняемым файлом эти библиотеки соединяются не на стадии липковаиия, как обычные библиотеки, а НА СТАДИИ ВЫПОЛ-НР.НИц! Это одно из принципиальных положений, отличающих все версии Windows от ее главного в прошлом конкурента MS DOS.
Библиотеки динамической компоновки представляют собою одного из тех китов, на которых базировались Windows всех версий, в том числе и Windows'95. Все функции API, с которыми мы работаем, находятся в библиотеках динамической компоновки - DLL (dynamic iink libraries). Основу Windows составляют три библиотеки: kerne!32dll, user32.dll и gdi32.dll. Первая отвечает за управление памятью, процессами и потоками, вторая - за систему окон с подсистемой сообщений, третья - за графику и вывод текста (само название - GDI - является аббревиатурой выражения Graphical User Interface - графический интерфейс пользователя).
Это, так сказать, самое Windows. Многочисленные DLL, которые можно найти в директории Windows, являются ее расширениями. Но, естественно, пользователи об этом и не догадываются.
Как правило, написать DLL значительно легче, чем разработать программу, ибо DLL - это всего-навсего набор автономных функций, предназначенных для вызова из других приложений или DLL. Естественно, что при этом условии в DLL совершенно не нужен цикл обработки сообщений или функция создания окна, что, как правило, присутствует в вызывающей программе. DLL компилируются и линкуются точно так же, как и обычные программы, единственное, необходимо указать линкеру, что необходимо создать DLL, а не программу. В этом случае цинкование проходит несколько другим способом, в результате чего в конечный файл записывается другая информация, и загрузчик Windows с легкостью отличает DLL от обычной программы.
СПОСОБЫ ПРИСОЕДИНЕНИЯ DLL К ПРОГРАММЕ
Для того чтобы программа смогла выполнить код, находящийся в библиотеке, необходимо этот код сначала загрузить в память, выделенную вызывающему процессу, после чего оповестить вызывающий процесс о том, где находится загруженный код.
Решить эту задачу можно двумя способами. Первый способ - это неявное линкование с DLL. Второй способ - явная загрузка DLL.
НЕЯВНОЕ ЛИНКОВАНИЕ С DLL
При неявном линковании программа не знает о том, какую библиотеку ей необходимо присоединить. Для того чтобы неявно прилинковать библиотеку, необходимо на этапе подготовки проекта произвести некоторые действия: создать файл с расширением .lib, содержащий ссылку на DLL и перечень находящихся в ней функций. Делается это с помощью утилиты implib. Вызывается она следующим образом:
implib FileNamel.lib FileName2.dll
где FileNamel.lib - это имя создаваемого файла; а FileName2.dll - DLL. Полученный lib-файл можно прилинковать к вашей программе точно так же, как и любую другую библиотеку.
Для того чтобы проиллюстрировать процесс вызова DLL
при неявном линковании, я напишу библиотеку, в которой будет находиться всего одна функция. При обращении к этой функции будет выдаваться окно с сообщением (MessageBox) «Сейчас мы в DLL!». Наверное, трудно при-
думать что-то более простое, а? Приложение, которое будет обращаться к этой библиотеке, всего-навсего будет вызывать эту функцию, не создавая никаких окон, циклов обработки сообщений и т. д. Листинги программы и библиотеки (я назвал ее dll.dll) приведены ниже:
#ifdef_MYDLL_
#define MY API _decispec(dllexport) #e!sc
#define MYAPI _declspec(dllimport)
#endif
MYAPI void CALLBACK MyMessagc();
Листинг № 7. Файл заголовков библиотеки dll.h: ^include <windows.h> #define _MYDLL_
MYAPI void CALLBACK MyMcssageQ {
McssageBox(NULL. "Now we are in DLL!", "Hurray!" , MB_OK); i
Листинг № 8. Основной файл библиотеки dll.cpp:
LIBRARY MyMessage
DESCRIPTION 'Program'
EXETYPE WINDOWS
CODE PRELOAD MOVEABLH DISCARDABLE
DATA PRELOAD MOVEABLE SINGLE
Листинг № 9. Файл определения модуля dll.def:
//include <\vindows.h> //include "dll.h"
hit WiNAPI WmMaiufHlNSTANCE hInsiance,HINSTANCE hPrevInstance,
LPSTR Ips/CmdLinc, int nCmdShow)
i
MyMessageQ; return !;
Листинг №10. Основной файл программы, осуществляющей вызов библиотечной функции, арр.срр посредством неявной компоновки:
NAME МуЛрр
DESCRIPTION 'Program'
EXETYPE WINDOWS
CODE PRELOAD MOVEABLE DISCARDABLE
DATA PRELOAD MOVEABLE MULTIPLE
Листинг № 11. Файл определения модуля app.def.
Для того чтобы эта связка программа-DLL заработала, проделайте следующие действия:
создайте DLL;
с помощью утилиты IMPLIB создайте lib-файл;
при создании ехе-файла прилинкуйте файл, полученный с помощью implib, к вашей программе;
запустите ехе-файл.
Надеюсь, что после всех этих действий вы увидите на экране сообщение о том, что произошло обращение к DLL.
В данном случае при загрузке выполняемого файла система просматривает его для того, чтобы определить, какие DLL используются при его работе, после чего пытается загрузить требующиеся DLL. Поиск DLL осуществляется в следующих каталогах:
каталоге, содержащем ехе-файл;
текущем каталоге процесса;
системном каталоге Windows;
каталоге Windows;
каталогах, указанных в PATH.
Попробуйте изменить имя DLL-файла. Вы заметите, что если файл DLL не найден, то система выдает сообщение об этом и немедленно завершает процесс
У неявной компоновки есть свои преимущества и недостатки. По моему мнению, к преимуществам нужно отнести следующее:
программа может ничего не знать о том, что она использует DLL;
проверка доступности DLL производится еще до загрузки программы, т. е. в случае отсутствия DLL программа просто не запустится.
Недостатком такого способа можно считать то, что DLL загружается в память до программы и выгружается после окончания программы, т. е. программист не может управлять загрузкой и выгрузкой библиотек. Если программа использует несколько библиотек, то придется все библиотеки хранить в памяти от запуска до завершения программы. Наверное, иногда
неплохо было бы обратиться и к другому способу подключения библиотек к программе, который называется
ЯВНАЯ ЗАГРУЗКА DLL
В этом случае все манипуляции с DLL производит вызывающая программа. Для того чтобы библиотека загрузилась в память, должна быть вызвана одна из функций - LoadLibraryQ или LoadLibraryExQ.
В winbase.h эти функции описаны следующим образом:
WINBASEAPI HMODULE WINAPI LoadLibraryA(LPCSTR IpLibFileNamc);
WINBASEAPI HMODULE WINAPI LoadLibraryW(LPCWSTR IpLibFileName);
#ifdcf UNICODE
#define LoadLibrary LoadLibraryW
#else
#defme LoadLibrary LoadLibraryA tfendilV/ !UNICODE
WINBASEAPI HMODULE WINAPI LoadLibraryExA(LPCSTR IpLibFileNamc,
HANDLE hFile, DWORD dwFlags);
WINBASEAPI HMODULE WINAPI LoadLibraryExW(LPCWSTR IpLibFileName,
HANDLE hFile, DWORD dwFlags);
#ifdcf UNICODE
#dcflnc LoadLibraryEx LoadLibraryExW
#elsc
#define LoadLibraryEx LoadLibraryExA
#endif // (UNICODE
Аргументом первой функции является имя загружаемой DLL. Другая же при вызове должна получить три аргумента. Первый - то же имя загружаемой DLL. Второй аргумент зарезервирован и должен быть равен NULL. Третий аргумент должен представлять собой либо нуль, либо комбинацию ИЗ трех флагов: DONT_RESOLVE_DLL_REFERENCES, LOADJJBRARY_AS_DATAFILE, LOAD_WITH_ALTI:RED_SEARCH_PATH.
DONT_RESOLVE_DLL_REFERENCES
Чуть позже мы рассмотрим функцию, которая производит инициализацию и деинициализацию DLL автоматически при загрузке. Данный флаг заставляет систему не вызывать функцию инициализации. Кроме этого, при загрузке библиотеки система проверяет, не используются ли данной DLL функции из других DLL. Если используются, то загружаются и они. Если данный флаг установлен, то дополнительные библиотеки не загружаются.
LOAD LIBRARY_AS_DATAFILE
Этот флаг может использоваться в нескольких случаях. Во-первых, можно загружать библиотеку, не содержащую никакого кода и содержащую только ресурсы. Полученное значение HINSTANCE можно использовать при вызове функций, использующих ресурсы. Во-вторых, если мы загружаем ехе-файл обычным способом, то это приводит к запуску нового процесса. А как быть в том случае, если мы хотим получить доступ к ресурсам ехе-файла, не запуская его? При загрузке ехе-файла с помощью функции LoadLibraryExQ с установленным флагом LOAD_LIBRARY_AS_DATAFILE, возможно получить доступ к ресурсам ехе-файла.
LOAD_WITH__ALTERED_SEARCH_PATH
Ранее мы рассмотрели, какие каталоги и в какой последовательности просматриваются системой при загрузке DLL. Если установлен флаг LOAD_WITH_ALTERED_SEARCH_PATH, то просмотр каталогов начинается с каталога, указанного в первом аргументе функции LoadLibraryExQ. Далее просмотр продолжается в обычном порядке.
После загрузки DLL программа не может вызывать требующиеся ей функции. Дня того чтобы вызвать какую-либо функцию, ей необходимо сначала определить адрес этой функции с помощью GetProcAddressQ, a затем вызывать функцию через полученный адрес. После того, как надобность в присутствии DLL в памяти отпала, программа должна выгрузить ее с помощью функций FreeLibrary() или FreeLibraryAndExitThread(). Но сейчас разговор не об этом. Давайте попробуем рассмотреть предыдущий пример, измененный таким образом, чтобы DLL загружалась явно.
Само собой разумеется, что все, что касается DLL, никаким изменениям не подвергалось. Иначе какой смысл писать DLL, если в зависимости от потребностей программиста ее нужно было бы каждый раз переписывать? Изменился только основной файл программы, которая вызывает функцию из DLL. Итак...
^include <windows.h> #include "dll.h"
int WINAPI WinMain(HINSTANCE hlnstance. HINSTANCE hPrevInstance, LPSTR IpszCmdLine, int nCindShow)
i
HINSTANCE hDII; FARPROC MyProcAddr;
if( (hDII = LoadLibrary("dll.dll")) != NULL)
MyProcAddr - GetProcAddress(hDH, "MyMcssage"); else {
MessageBox(NULL, "Sorry, cannot find requested DLL", "Sorry", MB_OK); return 0; i
(MyProcAddr)O; FreeLibrary(liDII); return 1 ;
Листинг № 12. Основной файл программы, осуществляющей вызов библиотечной функции, арр.срр посредством явной за!рузки DLL.
С точки зрения пользователя заметить какие-либо отличия в работе программ, использующих для вызова функций из DLL неявную компоновку и явную загрузку, невозможно. С точки зрения программиста два отличия прямо-таки бросаются в глаза. Первое - если при неявной компоновке в случае отсутствия DLL программа просто не запускается, то в случае явной загрузки, возможно перехватить такую ситуацию и предпринять какие-либо действия. И второе - у программиста прибавилось головной боли. Вместо обычного обращения к функции он должен вызвать еще три вспомогательные функции, да и требующаяся функция из DLL вызывается не напрямую, а косвенно, посредством использования ее адреса. Еще раз повторю - программист должен сам решить, стоит ли овчинка выделки.
ВЫВЕРНЕМ ПРОГРАММЫ НАИЗНАНКУ
Давайте попробуем разобраться в том, что все это означает и к чему может привести.
Начнем с файла, который использует ся как библиотекой, так и приложением - файла заголовков. Чтобы ехе-файл мог вызывать функции из DLL, то, с одной стороны, библиотека должна объявить их как доступные, или, как говорят, экспортируемые. С другой стороны, сам ехе-файл должен определить эти функции как находящиеся в DLL, т. е. как импортируемые. Если мы объявим эти функции по-разному в заголовочном файле и непосредственно в тексте, мы не оберемся ошибок при компилч-ции. Следовательно, выход один - условная компиляция. Если мы посмотрим на распечатку заголовочного файла dll.li, то увидим, что я определяю макро MYAPI, которое принимает одно из двух значений (_declspec(dllexport)) или (_declspec(dllimport) ) в зависимости от факта определения другого макро, _MYDLL_ . Теперь понятно, что и в заголовочном, и в исходных файлах можно описать функцию, находящуюся в
DLL, как MYAPI, но при этом в исходном файле библиотеки мы должны определить макро _MYDLL , а в исходном файле приложения ни в коем случае это макро не определять. Что и сделано. Проверьте - работает! Посмотрите в заголовочные файлы Win32. По-моему, в них используется эта техника.
Описание функций как экспортируемых требуется линкеру для того, чтобы правильно построить таблицу экспортируемых функций в dll-файле. Каждый элемент в этой таблице содержит имя экспортируемой функции, а также ее адрес. Немаловажно, что список функций сортируется по алфавиту. Таким образом, если функция должна работать бмстро, то какое-то преимущество можно получить в том случае, если имя функции будет начинаться с первых букв алфавита. По это преимущество сработает только в момент выполнения GetProcAddressQ, а не (увы!) при обращении к функции. Для того чтобы разобраться во внутренностях DLL, воспользуемся утилитой TDUMP, поставляемой с Borland C++ 5.0 (аналогичные утилиты есть и в других системах программирования, например, в Microsoft Visual C++ подобная утилита называется DUMPBIN). Часть распечатки таблицы экспорта для kernel32.dll приведена ниже:
Turbo Dump Version 4.2.15.2 Copyright (с) 1988, 1996 Borland International Display of File KERNEL32.DLL
Exports from KERNEL32.dll
680 exported name(s), 780 export addresse(s). Ordinal base is I. Ordinal RVA Name
0049 |
0002d900 AddAtomA |
0101 |
00034c99 AddAtomW |
0102 |
0002f44b AllocConsole |
0103 |
00021b22 AllocLSCallback |
0104 |
0002 Ib55 AllocSLCallback |
0105 0106 |
0002e75b AreFileApisANSl 00034d20 BackupRead |
|
|
0774 0775 0776 0777 |
00007 Ida Istrcpyn 00007 Ida IstrcpynA 00034ccf IstrcpynW 00007251 Istrlen |
0778 |
00007251 IstrlenA |
0779 |
0002b83c IstrlenW |
Аналогично решается и вопрос при импорте функций. Функция объявляется как импортируемая, после чего линкер строит специальный код и таблицу импортируемых функций.
Кстати, помимо TDUMP можно воспользоваться утилитой IMPDEF, которая выдает список присутствующих в DLL функций. Что же касается моего личного мнения, то я рекомендую читателю изучить форматы файлов Windows и написать самостоятельно несколько утилит, которые будут использоваться для «выворачивания программ наизнанку» и показывать все те данные и в таком виде, который удобен программисту.
ИНИЦИАЛИЗАЦИЯ И ДЕИШЩИАЛЮАЦИЯ DLL
Мы разобрались, каким образом можно написать DLL и вызвать функцию из DLL. По, как правило, ьо всех нормальных (не демонстрационных) примерах существуют блоки, отвечающие за инициализацию и деннициализацию программы. Возникает вопрос: как это сделать в DLL? LcTb ли такая возможность? Да, есть! В каждой библиотеке может быть функция, которая вызывается строго в определенных обстоятельствах и обычно используется библиотекой для инициализации и деиницналнза-ции. В нашей микро-DLL эта функция не использовалась, однако, я счел бы тему рассмотренной не полностью, если бы обошел этот вопрос стороной
В Borland C++ v. 5.0 эта функция по умолчанию называется DllEntryPointO и в некотором смысле является аналогом связки LibMi/:n() + WEP() в Windows 3.x. Вызывается эта функция всего в четырех случаях и имеет следующий вид:
BOOL WINAPI DHEntryPoint(HINSTANCE hinstDll, DWORD fdwReason, LPVOID IpvReserved)
{ switch(fdwReason)
{ case DLL_PROCESS ATTACH:
/* Операторы */ case DLL_THREAD_ATTACH:
/* Операторы */ case DLL_THREAD_DETACH:
/* Операторы */ case DLL_PROCESS_DETACH:
/* Операторы */
} return(TRUE);
Лично мне именно такой способ инициализации и деинициализа-ции библиотек динамической компоновки очень импонирует. Дело в том, что он использует оператор switch - case, который применяется при обработке сообщений. Именно из-за этого вся конструкция зрительно воспринимается так же, как и оконная процедура. Мне кажется, что с такой функцией намного приятнее иметь дело, чем со связкой LibMainQ - WEP в Windows 3.x.
Первый аргумент этой функции - это хэндл библиотеки, присваиваемый системой.
Второй аргумент указывает причину вызова этой библиотеки системой.
Третий аргумент, как понятно из его названия, пока зарезервирован и обычно должен быть равным NULL.
Теперь нам необходимо до конца разобраться с причинами вызова библиотеки.
DLL_PROCESS_ATTACH
Система вызывает функцию инициализации с этим значением параметра fdwReason единственный раз при загрузке библиотеки. Другими словами, если один из потоков процесса, вызвавшего DLL, пытается вновь загрузить ее с помощью LoadLibraryQ, то обращение к DLL с параметром DLL_PROCESS_ATTACH не произойдет. Система увеличит счетчик пользователей этой DLL.
Значение, возвращаемое функцией инициализации, после обработки DLL_PROCESS_ATTACH, уведомляет пользователя, была ли инициализация успешной. В случае неуспешной инициализации функция должна возвратить FALSE, при успехе - TRUE. Это значение используется как при неявной, так и при явной загрузке DLL в память.
DLL PROCESS JDETACH
Вызов функции инициализации для обработки DLL_PROCESS_DETACH означает, что библиотека из памяти выгружается и должна произвести действия по деинициализации самое себя. Помимо того, что необходимо освободить память и другие ресурсы, хорошим тоном считается оставить систему точно в том же состоянии, в каком ее приняла библиотека (если, конечно, изменение параметров системы не является задачей одной из функций DLL). При выгрузке библиотеки есть одна тонкость, связанная с причиной завершениия процесса, обратившегося к ней. Если DLL выгружается в связи с вызовом функции ExitProcessQ (или FreeLibraryQ, хотя это и не связано с процессом), вызов функции инициализации проходит нормально. Но если процесс завершается благодаря функции TerminateProcess(), функция инициализации НЕ ВЫЗЫВАЕТСЯ! Таким образом, попутно можно сделать еще один вывод - функцией TerminateProcessQ можно и нужно пользоваться только в самых крайних случаях!
Ниже приведен чуть измененный листинг библиотеки dll.c; попробуйте загрузить ее и понаблюдать за тем, как она работает:
^include <windows.h> tfdettne _MYDLL_ #include "dll.h"
BOOL WINAPI D!IEntryPoint(HINSTANCE hinstDII, DWORD fdwReason,
LPVOID IpvReserved) {
switch(fdwReason) {
case DLL PROCESS_ATTACH: MessageBox(NULL, "We are in DLL_PROCESS_ATTACH!", "Hurray!",
MB_OK); break;
case DLL_THREAD_ATTACH: MessageBox(NULL, "We are in DLL_THREAD_ATTACH!", "Hurray!",
MB_OK); break;
case DLL_THREAD_DETACH: MessageBox(NULL, "We are in DLL_THREAD_DETACH!", "Hurray!",
MB_OK); break;
case DLL PROCESS DETACH:
MessageBox(NULL, "We are in DLL_PROCESSJ)ETACH!", "Hurray!", MBJDK);
break;
} retum(TRUE);
/
MYAPI void CALLBACK MyMessageQ
"I
MessageBox(Gc4DcsktopWindo\v().''nLI, is called!", "Hurray!" . MB_OK},
i j
Листинг № 13. Библиотека dll.c, включающая код функции инициализации.
Очередной раз - ура! Мы научились писать библиотеки динамической компоновки, добавлять в них функции инициализации и деишшиализа-ции, подключать DLL к нашим программам, используя как неявную, так и явную загрузку.
КОНСОЛИ
«Неужели для того, чтобы написать простейшую программу, которая выводит на экран несколько строк, мне необходимо городить этот огород с WinMainQ и функцией окна'.' Неужели в каждой моей программе, предназначенной для вывода текста на экран, должно присутствовать «стандартное заклинание»? Или же я вынужден вечно быть ограниченным рамками DOS?» Я предвижу такие вопросы со стороны тех, которым при разработке их программ не только ns нужен, но и мешает графический интерфейс.
Что ж, вопросы вполне понятны и закономерны. Наверное, именно эта закономерность и обусловила появление в Win32 новых функций, обеспечивающих эмуляцию текстового терминала. Эти функции называются функциями консоли,
ЧТО ТАКОЕ КОНСОЛЬ
Консоль - это интерфейс, обеспечивающий поддержку программ, работающих в текстовом режиме, т. е. программ, написанных в стиле MS DOS. Консоль состоит из буфера ввода и одного или нескольких экранных буферов. Буфер ввода включает в себя очередь, каждая запись в которой содержит информацию о вводных событиях. Под вводными событиями в данном случае подразумеваются нажатия и отжатая клавиш на клавиатуре, на мыши, движения мыши, а также действия пользователя, производимые с окном. Экранный буфер - это двумерный массив, кото-
рый содержит коды символов и цвета символов текстового экрана (аналог видеобуфера в текстовом режиме при работе в MS DOS).
Каждая программа, работающая в текстовом режиме, взаимодействует с Windows через консоль. Если одна программа запускается из консоли, принадлежащей другой программе (скажем, aidstest запускается из консоли Norton Commander'a), то запускаемая программа работает в той же консоли. Если же программа запускается самостоятельно из Windows, то ей выделяется собственная консоль (фактически ей выделяется целая виртуальная машина). Другими словами, каждая программа может получить для себя эмулятор DOS-машины и считать, что весь компьютер принадлежит только ей.
Даже из этого краткого описания видно, что консоли могут оказать программисту довольно существенную помощь, состоящую, во-первых, в том, что обработка действий пользователя с мышью и клавиатурой производится средствами Windows, а, во-вторых, разрешают доступ к некоторым функциям API. В-третьих, каждая программа может работать в своей сессии. В-четвертых, программе доступны стандартные потоки ввода-вывода DOS. Наверное, даже этого краткого перечисления достаточно для того, чтобы убедить читателя в том, что разработку программ, не имеющих графического интерфейса, имеет смысл производить с учетом новых возможностей, предоставляемых Win32.
Мы долго говорили о том, что с консолями работают программы, написанные в стиле MS DOS. Но в языке С точкой входа для ООЗ'овских программ является функция mainQ, а не WinMainQ, следовательно, и консольные программы должны точкой входа тоже иметь функцию mainQ, а не WinMainQ. Таким образом, основными отличиями консольных программ от обычных программ для Windows являются: отсутствие графического интерфейса;
использование в качестве точки входа функции mainQ, а не WinMainQ.
ТЕХНИКА РАЗРАБОТКИ КОНСОЛЬНОЙ ПРОГРАММЫ
СОЗДАНИЕ КОНСОЛИ
Как уже было сказано, консольная программа должна иметь точкой входа не WinMainQ, a mainQ. При запуске программы она должна запросить для себя консоль, используя для этого функцию AllocConsoleQ. Ее прототип находится в файле wincon.h, к которому мы будем обращаться в этом разделе:
WINBASEAPI BOOL WINAPI AllocConsole(VOID);
Эта функция, возвращающая TRUE при успешном завершении, пре доставляет вызвавшей ее программе консоль. Внешне консоль выглядит так же, как и обычное окно. У него есть заголовок, системное меню, кнопки максимизации и минимизации. Если программе необходима собственная консоль, а она работает в унаследованной, то программа может перед вызовом AlIocConsole() произвести освобождение консоли, вызвав для этого функцию FreeConsole(), описание которой практически не отличается от описания предыдущей функции:
WINBASEAPI BOOL WINAPI FreeConsole( VOID );
Если программа запускается в самостоятельной консоли, то вызов FreeConsoleQ не повлечет за собой никаких неприятных последствий.
При завершении программы самостоятельная консоль автоматически уничтожается, унаследованная же продолжает свое существование до момента завершения породившей ее программы.
ПРИСВОЕНИЕ КОНСОЛИ ИМЕНИ
Очередным шагом после создания консоли будет присвоение консоли имени. Это имя будет отображено в заголовке консоли. Делается это с помощью функции SetConsoleTitle(). Из wincon.h извлекаем прототип этой функции:
WINBASEAPI BOOL WINAPI SetConsoleTitleA(LPCSTR IpConsoleTitle ); WINBASEAPI BOOL WINAPI SetConsoleTitleW(LPCWSTR IpConsoleTitlc);
#ifdefUNICODE
#define SetConsoleTitle SetConsoIeTitleW
#else
«define SetConsoleTitle SetConsoleTitleA
tfcndif// IUNICODE
Аргумент этой функции - указатель на строку символов, содержащую текст, который будет отображен в заголовке окна консоли.
ВВОД И ВЫВОД В КОНСОЛИ
Основные функции вывода в окно консоли
Когда я изучал этот вопрос, функции ввода и вывода в консоли напомнили мне вызов прерываний DOS (наверное, так и должно быть, ведь консоль эмулирует DOS-машину). Поэтому знакомые с прерываниями DOS программисты увидят в функциях ввода - вывода много «знакомого».
В DOS для операций ввода - вывода считалось, что стандартные потоки, такие, как поток ввода, поток вывода и поток ошибок, имеют стандартные, заранее определенные хэндлы. При работе в режиме консоли стандартные потоки предопределенных хэндлов не имеют, поэтому эти хэндлы необходимо получить, обратившись к функции GetStdHandleQ. По ее описанию -
WINBASEAPI HANDLE WINAPI GetStdHandle(DWORD nStdHandle);
извлеченному в данном случае из файла winbase.h, мы видим, что для получения хэндла стандартного потока в консольной сессии мы в качестве аргумента функции должны указать номер того потока, хэндл которого нам нужен. Запоминать номера потоков не нужно, они определены в том же файле winbase.h как STD_INPUT_ HANDLE, STD_OUTPUT_HANDLE и STD_ERROR_HANDLE. При успешном завершении функция возвращает хэндл требующегося потока, в противном случае возвращаемое значение равно INVALID_HANDLE_VALUE.
Определив хэндл стандартного потока, можно попытаться вывести текст в окно консоли с помощью функции WriteConsoleQ:
WINBASEAPI BOOL WINAPI WriteConso!eA(HANDLE hConsoleOutput,
CONST VOID «IpBuffer,
DWORD nNumberOfCharsToWrite,
LPDWORD IpNumberOfCharsWritten,
LPVOID IpReserved); WINBASEAPI BOOL WINAPI WriteConsolcW(HANDLE hConsoleOutput,
CONST VOID "IpBufTer,
DWORD nNumberOfCharsToWrite,
LPDWORD IpNumberOfCharsWritten,
LPVOID IpReserved);
#ifdefUNICODE
#defme WritcConsole WriteConsoleW
#else
#defme WriteConsole WriteConsoleA
#endif// IUNICODE
Аргументами этой функции являются:
хэндл стандартного потока вывода;
указатель на выводимую строку;
длина выводимой строки в символах;
в указатель на двойное слово, в которое записывается действительное число выведенных символов;
указатель на двойное слово, зарезервированное для использования в дальнейшем, который должен быть равным NULL.
Строка выводится, начиная с текущей позиции курсора, при этом используются текущие цветовые атрибуты текста и фона. Курсор устанавливается в позицию, следующую за последним символом строки.
В случае успешного завершения функция возвращает TRUE.
Для установки позиции курсора в консоли необходимо вызвать функцию SetConsoleCursorPositionQ, прототип которой можно найти в wincon.h:
WINBASEAPI BOOL WINAPI SetConsoleCursorPosition( HANDLE hConsoleOulput, COORD dwCursorPosition);
hConsoleOutput - это хэндл стандартного вывода консоли, а структура типа COORD, содержащая координаты новой позиции курсора, определяется в wincon.h следующим образом:
typedef struct _COORD {
SHORT X;
SHORT Y; } COORD, *PCOORD;
X и Y - координаты новой позиции курсора.
Если функция завершена успешно, она возвращает ненулевое значение.
Последнее, что нам осталось сделать для того, чтобы мы могли полностью управлять выводом, - это научиться устанавливать цветовые атрибуты выводимого текста. Учиться недолго - это делается с помощью функции SetConsoleTextAttribute(). Извлечем из wincon.h ее прототип:
WINBASEAPI BOOL WINAPI SetConsolcTexlAttribute(HANDLE hConsoleOutput,
WORD wAttribute.s);
hConsoleOutput - хэндл стандартного потока вывода консоли, a wAttribues определяет цвета тона и фона текста. wAttributes должен быть комбинацией нескольких флагов. Перечень флагов приведен в табл. 57.
Таблица 57. Атрибуты цветов фона и тона окна консоли
Флаг |
Значение |
Эффект |
FOREGROUND BLUE |
0x0001 |
Тон содержит синюю составляющую |
FOREGROUND_GREEN |
0x0002 |
Тон содержит зеленую составляющую |
FOREGROUND RED |
0x0004 |
Тон содержит красную составляющую |
FOREGROUND INTENSITY |
0x0008 |
Тон имеет повышенную интенсивность |
BACKGROUNDJ3LUE |
0x0010 |
Фон имеет синюю составляющую |
BACKGROUND GREEN |
0x0020 |
Фон имеет зеленую составляющую |
BACKGROUND RED |
0x0040 |
Фон имеет красную составляющую |
BACKGROUND INTENSITY |
0x0080 |
Фон имеет повышенную интенсивность |
|
|
или текст мигает (только в полноэкранном |
|
|
режиме) |
Точно так же обстоит дело и с фоном символов. Однако в зависимости от некоторых условий (обсуждение возможностей и регистров видеоадаптера явно выходит за рамки этой книги), установленный фла! интенсивности тона обеспечивает либо повышенную интенсивность фона, либо мигание символа. Но все это касается только программ, работающих в полноэкранном режиме (не путать максимизированную консоль с работой в полноэкранном режиме!). Рекомендую читателю проверить эту установку на своем компьютере. Программы в консольном режиме, к сожалению, не могут использовать мигание фона, поэтому те символы, для которых в полноэкранном режиме установлен флаг мигания, в консольном режиме не мигают, а отображаются с повышенной интенсивностью цвета фона символов.
Мы закончили краткое обсуждение функций, обеспечивающих вывод на консоль. Давайте теперь рассмотрим функцию, обеспечивающую
Ввод из окна консоли
Для ввода из окна консоли текста с клавиатуры используется функция
ReadConsoleO, описанная следующим образом: WINRASEAPI BOOL WINAPI ReadConso!c.A(
HANDLE hConsolelnput, LPVOID IpBuffer, DWORD nNumberOfCharsToRead, I.PDWORD IpNumberOfCharsRead, LPVOID IpRcserved); WINBASEAPI BOOL WINAPI ReadConsoleW(
HANDLE hConsolelnput, LPVOID IpBuffer, DWORD nNumberOPCharsToRead, LPDWORD IpNumberOfCharsRead, LPVOID IpReserved);
#ifdefUNICODE
#defme ReadConsole ReadConsoleW
#else
#defme ReadConsole ReadConsoleA
#endif// IUNICODE
Аргументами этого файла являются: hConsolelnput - хэндл потока ввода;
IpBuffer - указатель на символьный массив, в который будет записана строка символов;
nNumberOfCharsToRead - максимальное число вводимых символов; IpNumberOfCharsRead - число фактически считанных символов; IpReserved - зарезервировано для дальнейшего использования и должно быть равно NULL.
На этом мы завершаем рассмотрение основных функций, обеспечивающих ввод-вывод в окно консоли. После демонстрационной программы рассмотрим еще несколько функций, обеспечивающих работу с клавиатурой и мышью.
Демонстрационная программа
Перед тем, как привести текст демонстрационной программы, мне бы хотелось обратить внимание читателя на одну тонкость. Программы, которые мы рассматривали до сих пор, являлись оконными программами. Я не давал никаких пояснений по поводу их компиляции. Но та программа, которую мы будем разбирать сейчас, не имеет собственного графического интерфейса, т. е. оконной программой не является, а следовательно, и компилировать ее нужно несколько иным способом. Предлагаю читателю обратиться к руководству по той системе, с которой он работает, и выяснить, каким образом можно скомпилировать консольную программу. Если читатель паче чаяния работает с Borland C++ 5.0 в IDE, то ему при создании нового проекта в TargetExpert необходимо изменить TargelMode с GU! на Console.
Сделано? Тогда листинг демонстрационной программы перед вами:
#include <windows.h>
#includc <stdio.h> mainQ {
HANDLE hStdlnputHandle, hStdOutputHandle;
COORD Coord;
char cMyString[255] = "This is our first console program! It's working !";
DWORD dwResult;
FreeConsoleQ;
AllocConsoleQ;
SetConsoleTitle("ConsoIe Demonstration program");
hStdlnputHandle = GetStdHandle(STD_lNPUT HANDLE);
hStdOutputHandle = GetStdHandle(STD_OUTPUT_HANDLE);
Coord.X = (80 - strlen(cMyString)) / 2;
Coord. Y= 12;
SetConsoleCursorPosition(hStdOutputHandle, Coord);
SetConsoleTextAttribute(hStdOutputHandle, FOREG ROUNDJIED |
BACKGROUND_RED | BACKGROUND_BLUE | BACKGROUND_GREEN FOREGROUNDJNTENSITY | BACKGROUNDJNTENSITY);
WriteConsole( hStdOutputHandle, cMyString, strlen(cMyString), &dwResult, NULL); SetConsoleTextAttribute(hStdOutputHandlc, 0); getchar();
SetConsoleCursorPosition(hStdOutputHandle, Coord); WriteConso!e(hStdOutputHandle, cMyString, strlen(cMyString),
&dwResult, NULL); Coord.X = 0; Coord.Y= 12;
SetConsolcCursorPosition(hStdOutputHandle, Coord); SetConsoleTextAttribute(hStdOutputHandle, FOREGROUND_RED [
FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_INTENSITY); WriteConsolefhStdOutputHandle, "Type some letters and press Enter, please: ",
strlen(cMyString), &dwResult, NULL); SetConsoleTextAttributefhStdOutputHandle,
FOREGROUND_RED | BACKGROUND_RED | BACKGROUND_BLUE | BACKGROUNDJ3REEN | FOREGROUNDJNTENSITY);
ReadConsolefhStdlnputHandle, cMyString, strlen(cMyString), &dwResult, NULL); return 0;
Окно, отображаемое при запуске программы, показало на рис
5 Сонм» yemomuation pcogi
Рис. 21. Окно-консоль, в которое иронзвелен выно.; счроки
Ничего особенного в этой программе не происходи1! Программа запрашивает собственную консоль и устанавливает заголовок этой консоли, после чего получаст хэндл' потоков ввода и вывода. Курсор устанавливается с таким расчетом, чтобы выводимая фраза размещалась в центре экрана. Цвет выводимых символов устанавливается как ярко-красный на ярко-белом (на рисунке показана консоль программы именно после этого момента). После вывода фразы программа ждет нажатия-Enter, после чего затирает фразу и предлагает пользователю что-нибудь набрать на экране, завершив набор нажатием клавиши Enter. Завершение ввода пользователя означает и завершение программы. Ничего сложного, но используются все функции управления консолью, которые мы уже изучили. На долю читателя я оставляю возможность самостоятельно исследовать, какие функции ввода - вывода из стандартных библиотек С и C++ могут быть использованы в консольных программах вместо функций API. Попробуйте, например, начать с исследования функции printf(). С другой стороны, приятной особенностью консольных программ является возможность вызова таких функций API, как, скажем, MessageBoxQ. Другими словами, при очень небольшой доработке большинство программ MS DOS могут быть перекомпилированы как консольные программы и могут использовать многие из тех возможностей, которые предоставляет
ОБРАБОТКА НАЖАТИЙ КЛАВИШ НА КЛАВИАТУРЕ И СОБЫТИЙ, ПРОИСШЕДШИХ С МЫШЬЮ
Мы уже знаем, что Windows - система, управляемая событиями. Консольные программы не являются исключением. По есть одна деталь, которую необходимо ел метить. События с клавиатурой и мышью записываются во входной буфе]) только в тех случаях, когда программа, во-первых, имеет клавиатурный фокус, и, во-вторых, указатель мыши находится в рабочей области консольного окна. Каждому событию, произошедшему с консолью, соответствует одна запись во входном буфере. Каждая запись о событии представляет собой заполненную структуру типа INPUT_RECORD, описание которой можно найти в файле vvincon.h:
typedcf struct _1NPUT_REC'ORD { WORD EventType; union )
KEY EVENT_RECORD KeyEvent;
MOUSE_EVENTRECORD MouseEvent;
WINDOWJ3UFFER SIZE RECORD WindowBuflcrSizcEvent;
MENU EVENT_RECORD MenuEvcnt;
FOCUS_EVENT_RECORD FocusEvent; j Event; } INPUT RECORD, *PINPUT RECORD;
Даже не особо вникая в смысл полей, видно, что консоль обрабатывает пять типов событий. Их перечень, взятый из файла wincon.h, приведен в табл. 58.
Т а б л и ц а 58. События, обрабатываемые консолью
Флаг |
Значение |
Эффект |
KEY EVENT |
0x000 1 |
Событие с клавиатурой |
MOUSE EVENT |
0x0002 |
Событие с мышью |
WINDOW_BUFFER_SIZE_EVENT |
0x0004 |
Событие по изменению размерен |
|
|
экрана |
MENU EVI-NT |
0x0008 |
Событие с меню |
FOCUS_ EVENT |
0x00 1 0 |
Изменение фокуса |
WINBASEAPI BOOL WfNAPI ReadConsoleInputA(
HANDLE hConsolelnput,
PfNPUT_RECORD IpBuffer,
DWORD nLength,
LPDWORD IpNumberOfEventsRead); WINBASEAPI BOOL WINAPI ReadConsoleInputW(
HANDLE hConsolelnput,
PINPUT_RECORD IpBuffer,
DWORD nLength,
LPDWORD IpNumberOfEventsRead);
#ifdef UNICODE
#define ReadConsoIelnput ReadConsolelnputW
#else
#define ReadConsoIelnput ReadConsolelnputA
#endif// IUNICODE
Здесь:
hConsolelnput - хэндл входного потока консоли;
IpBuffer - указатель на структуру типа INPUT_RECORD, в которую будут записаны данные о событии (или массив структур, если считываются данные более чем об одном событии);
nEength - число считываемых записей о событии;
IpNumberOfEventsRead - указатель на двойное слово, в которое записывается число реально считанных данных.
До нормальной работы осталось немного - узнать, какая информация записывается в структуру типа LNPUT_RECORD и как мы можем ее использовать. Давайте остановимся на каждом типе событий отдельно.
События с клавиатурой
События клавиатуры генерируются каждый раз при нажатии клавиши. При этом поле EventType структуры типа _INPUT_RECORD содержит значение KEY_EVENT, а в объединение Event записывается поле KeyEvent типа KEY_EVENT_RECORD. Этот тип определен в wincon.h: typedef struct _KEY_EVENT_RECORD { BOOL bKeyDown; WORD wRepeatCount; WORD wVirtuaiKeyCode; WORD wVirtualScanCode; union {
WCHAR UnicodeChar; CHAR AsciiChar; } uChar;
DWORD dwControlKeyState; } KEY_EVENT_RECORD, *PKEY_EVENT_RECORD;
Для того чтобы нормально обрабатывать события с клавиатурой, нам необходимо подробно разобрать назначение полей этой структуры. Программисты, знакомые с обработкой клавиатурных прерываний в DOS, увидят здесь множество знакомых характеристик. К сожалению, рамки этой книги не позволяют мне описать основы работы с клавиатурой. Если читатель чувствует, что у него в этой области есть пробел, рекомендую изучить этот вопрос по другим изданиям.
Если событие с клавиатурой состояло в нажатии клавиши, то поле bKeyDown принимает значение TRUE. Значение FALSE говорит о том, что произошло отжатие клавиши.
Если клавиша нажата и код клавиши начал генерироваться повторно, поле wRepeatCount является счетчиком повторов, что, кстати, следует и из его названия.
Виртуальный код нажатой клавиши записывается в поле wVirtualKeyCode, а виртуальный скан-код - в поле wVirtualScanCode.
Объединение uChar содержит ASCII или Unicode код нажатой клавиши в зависимости от того, какая версия функции ReadConsolelnputQ, ASCII или Unicode, используется.
И наконец, поле dwControlKeyState указывает на состояние управляющих клавиш. Их возможные значения приведены в табл. 59.
Т а б л и ц а 59. Флаги состояния управляющих клавиш
Флаг |
Значение |
Эффект |
RIGHT ALT PRESSED |
0x0001 |
Нажат правый Alt |
LEFT ALT PRESSED |
0x0002 |
Нажат левый Alt |
RIGHT CTRL PRESSED |
0x0004 |
Нажат правый Ctrl |
LEFT CTRL PRESSED |
0x0008 |
Нажат левый Ctrl |
SHIFT PRESSED |
0x0010 |
Нажат Shift |
NUMLOCK ON |
0x0020 |
NumLock горит |
SCROLLLOCK ON |
0x0040 |
ScrollLock горит |
CAPSLOCK ON |
0x0080 |
CapsLock горит |
ENHANCED KEY |
0x0100 |
Клавиша с двойным скан-кодом |
Демонстрационная программа
Я думаю, что даже конспективного изложения достаточно для того, чтобы можно было начать работу с клавиатурой в консольной сессии. Разве не так, уважаемый читатель? Тем не менее, я привожу демонстра-
ционную программу для того, чтобы показать, как можно обрабатывать клавиатурные события.
#include <windows.li>
#include <stdio.h>
main() {
HANDLE hStdlnputHandie, hStdOutputHandle; char cMyMessage[80] - "Do something with mouse to exit"; COORDCoord= {0,24}; DWORD dwResult; BOOL bMyFlag = TRUE; _INPUT^RECORD InputRecord; char cMyString[16]; char* cMyKeys[9] •= {" RAlt", " LAlt"," RCtrl", " LCtrl", " Shift", " NumLock",
" ScrollLock", " CapsLock"," EnhKey"); DWORD dwMyFlag;
FreeConsole();
AllocConsoleO;
SetConsoleTitle("Keyboard in console session demo program");
hStdlnputHandie = GetStdHandle(STDJNPUTJlANDLE);
hStdOutputHandle = GetStdHandle(STD_OUTPUT_HANDLE);
SctConsoleCursorPosition(hStdOutputHand!e, Coord);
SetConsoleTextAttribute(hStdOutputHandIe, FOREGROUND_RF.D |
FOREGROUND_GREEN | FOREGROUND_BLUE); WriteConsole(hStdOutputHandle, cMyMcssage, strlen(cMyMessage), &dwResult,
NULL);
while(bMyFlag) t
ReadConsoleInput(hStdInputHandlc, &InputRecord, 1, &dwResult); if(dwResult>= 1) f
ir(InputRecord.EvcntType == KEY EVENT)
t »
SetConsoleCursorPosition(hStdOutputHandlc. Coord); SctConso!eTextAttribute(hStdOutputHandle,0); WritcConsole(hStdOutputHandle, cMyMcssage, strlen(cMyMessage),
&dwResull, NULL); for( int i = 0, i < 80; i Ч
cMyMessagcfi] ~ 0; C'oord.X - 0; Coord. Y= 1;
SetConsoleCursorPositioii(hStdOutput!iandle. Co»/rd); if(lnputRecord. Event. Key Event, b Key Down)
strcat(cMyMessagc, "Pressed "); else
strcat(cMyMcssage, "Released "); strcat(strcat(cMyMessage,
itoa(InputRccord.Event KcyEvent.wVirtualKeyCode,
cMyString, 16)),""); strcat(cMyMessage,
itoa( InputRecord.Event.KeyEvent.wVirtualScanCode,
cMyString, 16));
if(InputRccord.Event.KeyEvent.dwControlKeyState != 0) ior(int i - 0; i <= 8; i+-r) {
dwMyFlag = 1;
if(InputRecord.Event.KeyEvent.dwControlKeyState & (dwMyFlag « i)) strcat(cMyMessage, cMyKeys[i]);
(
SetConsoleTcxtAttribute(hStdOutpulHandle, FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE): WriteConsolc(hStdOutpu (Handle, cMyMessage. strlen(cMyMessage),
&dwResult, NULL); }
else
ifTInputRecord.EvcntType == MOUSE_EVENT) bMyFlag = FALSE;
return 0;
;Jnc. '12. (/KHo-KOiicoiib, отогбражающес лоложслие курсора мыши и сосюяимс клавиатур!-;
На рис. 22 показан вид окна, созданного этой программой.
Эта программа запрашивает для себя отдельную консоль, после чего в нижней части экрана выдает сообщение о том, для выхода необходимо сделать что-либо с мышкой. Но суть ее не в этом. При нажатии любой клавиши в верхней части экрана появляется строка, в которой указывается, какой тип действия (нажатие или отжатие клавиши) был произведен с клавиатурой, а также перечисляются некоторые характеристики нажатой клавиши, как-то ее виртуальный и скан-коды и состояние управляющих клавиш. Я думаю, что каких-либо трудностей при разборе программы не встретится.
События с мышью
События с мышью происходят в тех случаях, когда мышь двигается (при этом курсор должен находиться поверх окна консоли), либо на ней нажимается одна или более кнопок. При возникновении события с мышью поле EventType структуры типа INPUT RECORD содержит значение MOUSE_EVENT, В объединении Event этой структуры в этом случае будет содержаться поле MouseEvent типа MOUSE_EVENT RECORD. Для того чтобы понять, какую информацию мы можем извлечь из события с мышью, рассмотрим описание типа MOUSE_EVENT_RECORD. Его мы извлечем из заголовочного файла wincon.ii:
typedef smict _MOUSn_EVENT_RECORD {
COORD dwMousePosition;
DWORD dwButtonState;
DWORD dwControlKeyState;
DWORD dwEventFlags; ! MOUSEJEVENT_RECORD, *PMOUSE_EVENT_RECORD;
В этой структуре некоторые поля нам уже знакомы. Первое поле dwMousePosition типа COORD - координаты курсора мыши во время наступления события. Если обычно координаты курсора указываются в пикселах, то в данном случае они указываются в символах, причем начало отсчета - левый верхний угол рабочей области окна консоли. Не забудьте, экран-то текстовый!
Поле dwButtonState описывает состояние кнопок мыши на момент возникновения события. Кодируется это поле достаточно замысловато. Считается, что максимум у мыши может быть четыре кнопки (лично я таких мышей не видел и не слышал о них. Может, Microsoft боится повторить ситуацию с 640 кбайтами в DOS?). При этом младший бит определяет состояние самой левой клавиши ( 1 - клавиша нажата), сле-
дующий по старшинству бит определяет состояние самой правой клавиши. Очередной бит связан со второй слева кнопкой, следующий - с третьей слева и, наконец, последний - с четвертой слева кнопкой. Для каждого из этих битов в файле wincon.h определены макросы, которые приведены в табл. 60.
Т а б л и ц а 60. Флаги, определяющие нажатую клавишу мыши
Макрос |
Значение |
FROM LEFT 1ST BUTTON PRESSED RIGHTMOST BUTTON PRESSED FROM LEFT 2ND BUTTON PRESSED FROM LEFT 3RD BUTTON PRESSED from "left Чтн "button "pressed |
0x0001 0x0002 0x0004 0x0008 0x0010 |
Флаг |
Значение |
Эффект |
MOUSE MOVED DOUBLE J7LICK |
0x0001 0x0002 |
Перемещение мыши Второй щелчок кнопки (при двойном щелчке) |
С полем dwControlKeyState мы познакомились при изучении работы с клавиатурой. Никаких изменений это поле по сравнению с аналогичным в структуре KEY_EVENT_RECORD не претерпело.
Значение последнего поля, dwEventFlags, определяет действие, которое привело к возникновению события. Если его значение равно нулю, то это означает, что была нажата или отпущена одна из кнопок мыши. Еще два возможных значения этого поля приведены в табл. 61.
Не напоминает ли это все нотификационные события при разработке оконных программ?
Демонстрационная программа
Думаю, что все дальнейшие объяснения излишни. Вспомним о принципе «Seeing is believing». Давайте разберем небольшую демонстрационную программу. При написании этой программы я, чтобы не утомлять читателя, сделал одно допущение: у мыши всего две кнопки. Надеюсь, это допущение не повлияет на восприятие программы читателем:
^include <windows,h>
mainQ
HANDLE hSldlnputHandle. liSldOtitputHandle;
COORD Coord = {0,24};
char cMyMessage[80] = "Press any key lo exit";
DWORD dwResult;
BOOL bMyFlag = TRUE;
_INPUTJ?ECORD InputRecord;
char cMyString[l6];
char* cMyButtons[4] - {" LcftButton", " RightButton".
" Mouse moved". " Double Click"}; DWORD dwMyFlag;
FrceConsoleQ;
AllocConsole();
SctConsoleTitlc("Mouse in console session demo program");
hStdlnputHandle = GctStdHandle(STD_INPUT HANDLE);
hStdOutputHandle - GetStdHandle(STD _OUTPUT_HANDLE);
SetConsoleCursorPosition(hStdOutputHandle. Coord);
SetConsoleTextAttribute(liStdOutputHandle, FOREGROUND_RED |
FOREGROUND_GREEN | FOREGROUND^BLUE);
WriteConsole(hStdOutputHand!e, cMyMessagc. strlen(cMyMessagc). &dwRcsult,NULL);
while(bMyFlag)
( i
ReadConsolclnput(hStd!nputHandle, &lnpulRecord, 1, &dwRcsult);
irflnputRccord.EventType -= MOUSE EVENT)
SelConsoleCursorPosition(hStdOtitputHandlc, Coord); SelConsolcTcxtAttribute(liS(dOulputHandle,0); WritcConsole(hSldOutput Handle. cMyMcssage, slrlen(cMyMessage),
&d\vResult, NULL); for( int i = 0; i < 80; i — )
cMyMessagcfi] = 0; Coord. X - 0; Coord. Y - I ;
SclConsolcC'ursorl'ositioiKhStdOiitpul Handle, Coord); slrcaUcMyMcssage. "P(!situ;n - "): strcatfcMy Message.
itoa( input Record. I'.vciit.MouseEvent.dwMuuscPosition.X,
cM>'String. 10)): sircat(cMyMessai:e. ". "); strcaKcMyMessagc.
itoat InputRecord. Event. Mouse Event. dwMouscPosition.Y, cMySlrin». 10));
strcat(cMyMessage, " "); l'or(int i = 0; i <= I; i-r+) {
dwMyFlag = 1; if(InputRecord.Event.MouseEvcnt.dwButtonStatc & (dwMyFlag « i))
strcaI(cMyMcssage, cMyButtons[il);
if(InputRecord.Event.MouseEvent.dwEvcntFlags & (dwMyFlag « i)) strcat(cMyMessage, cMyButtons[i+2]);
i t
SetConsoleTextAttributc(hStdOutputHamIle, FOREGROUND_RED |
FOREGROUND^GREEN FOREGROUND_BLUE); WriteConsolc(hStdOulpulHandle, cMyMessagc, strlen(cMyMcssagc),
&dwRcsull,NULL); }
else
if(lnputRecord.EvenlType =-=- KEY_EVENT) bMyFlag = FALSE;
return 0;
И, как всегда, вид окна, созданного программой (рис. 23).
1НШН1 т on»li ibcbob diB» рмшю
Рис 2~<. Омю-конс'оль, см поражающее по южсние и состояние мыши
Как и в предыдущей программе, здесь нет ничего особенного. При запуске программа сообщает пользователю, что для выхода необходимо нажать любую клавишу. При возникновении же любою события с мышью, в первой строке экрана появляется информация о событии. Все данные берутся из структуры типа _INPUT RECORD. Попробуйте подвигать мышь внутри окна и понажимать кнопки мыши, понаблюдайте за результатами.
ЗАКЛЮЧЕНИЕ
На этом книга завершена. Не знаю, для кого она оказалась более трудной - для читателя или для меня. Не знаю, какой получилась - хорошей или плохой, принесла она пользу или читатель пожалел о потерянном времени. Писал я ее с душой. Конечно, можно было бы рассказать о Windows намного больше, но, как мне кажется, цель достигнута -читатель получил первоначальные знания и знает, где искать дополнительную информацию.
Пусть читатель не судит меня строго. Я сделал все, что мог.
Теперь я должен расстаться со своим читателем и мне немного грустно. Я не знаю, что я должен сказать: «До свидания» или «Прощайте». Надеюсь, что до свидания, мой читатель.