Азбука программирования в Win32 API

         

СОЗДАНИЕ ПОТОКА



Создание потока в большей степени (внешне, конечно) напоминает программу для 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 стал свободным из-за отказа от него Произошла ошибка

 

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

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

 

Как можно убедиться, все функции рассортированы в алфавитном по­рядке? В первой колонке указаны ordinals - порядковые номера функций. В более ранних версиях Windows функции могли экспортироваться не только по именам, но и но порядковым номерам. Сейчас Microsoft реко­мендует пользоваться только именами функций (именно поэтому я не описываю способ экспорта и импорта функций посредством указания порядковых номеров). Для справки - средняя колонка содержит Real Virtual Addresses - адреса функций внутри DLL.



Аналогично решается и вопрос при импорте функций. Функция объ­является как импортируемая, после чего линкер строит специальный код и таблицу импортируемых функций.

Кстати, помимо 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

 

Фон имеет повышенную интенсивность

 

 

 

 

 

или текст мигает (только в полноэкранном

 

 

 

 

 

режиме)

 

В разделе, посвященном графике, упоминалось о том, что каждый пиксель на экране состоит из трех микроточек, при этом интенсивность свечения каждой точки может изменяться от нуля до 255. В тексто­вом режиме все проще. В обычных условиях (подчеркиваю - обычных условиях!) тон символа (не пикселя - символа!) тоже определяется как состоящий из трех компонентов, однако их интенсивности могут быть 0 и 127 (флаг интенсивности не установлен), 0 и 255 (флаг интенсивности установлен). Таким образом, всего возможно 16 цветов тона символов.



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

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

Ввод из окна консоли

Для ввода из окна консоли текста с клавиатуры используется функция

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

 

Изменение фокуса

 

Обычно события меню, фокуса устройства и изменения размеров эк­рана обрабатываются системой, на долю программиста остаются события с клавиатурой и мышью. Всю информацию о событии можно получить с помощью функции ReadConsolelnputQ. По привычке приводим ее описа­ние из файла vvincon.h:

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

 

Таблица 61. События от мыши

Флаг

 

Значение

 

Эффект

 

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

Пусть читатель не судит меня строго. Я сделал все, что мог.

Теперь я должен расстаться со своим читателем и мне немного груст­но. Я не знаю, что я должен сказать: «До свидания» или «Прощайте». Надеюсь, что до свидания, мой читатель.