Адресация памяти в реальном режиме
Для адресации байта памяти в реальном режиме работы используются две 16-разрядные компоненты адреса - сегмент и смещение. Физический адрес , который попадает на шину адреса системной платы компьютера, складывается (в буквальном смысле этого слова) из сдвинутой влево на четыре бита и дополненной справа четырьмя нулевыми битами сегментной компоненты и компоненты смещения. Перед сложением компонента смещения расширяется до 20 бит так, что в старшие четыре бита записываются нули (рис. 2.1).
Рис. 2.1. Получение физического адреса в реальном режиме
Задавая произвольные значения для сегмента и смещения мы можем сконструировать физический адрес для обращения к памяти размером 1 Мбайт плюс 64 Кбайт (и минус 16 байт).
Адрес, состоящий из сегмента и селектора, мы будем называть логическим адресом реального режима . Диапазон логических адресов от 0000h:0000h до FFFFh:000Fh соответствует диапазону физических адресов от 00000h до FFFFFh. Этот диапазон адресов соответствует первому мегабайту оперативной памяти.
Диапазон логических адресов от FFFFh:0010h до FFFFh:FFFFh соответствует так называемой области старшей памяти (High Memory Area). Размер области старшей памяти равен 64 Кбайта без 16 байт, и эта память доступна в реальном режиме для процессора модели 80286 и более старших моделей. Если вы работаете с операционной системой MS-DOS версии 5.0 или 6.2, имеет смысл загрузить ядро MS-DOS в область старших адресов, указав в файле config.sys команду:
DOS=HIGH
Недостатки реального режима работы процессора очевидны. Вы не можете использовать расширенную память, расположенную в адресном пространстве выше области старшей памяти. Если в вашем компьютере установлено 16 Мбайт оперативной памяти, процессор не сможет непосредственно адресовать из них целых 15 Мбайт (рис. 2.2).
Рис. 2.2. Адресация памяти в MS-DOS
На заре развития персональных компьютеров оперативная память размером в 1 Мбайт считалась достаточно большой для решения любых мыслимых задач. Однако с появлением Windows и внедрением графического пользовательского интерфейса критерии оценки объема памяти резко изменились.
Теперь минимальный объем памяти для нормальной работы приложений Windows составляет 4 Мбайта, а для некоторых приложений (например, для системы разработки Borland C++ for Windows версии 4.0 или Microsoft Visual C++) требуется 8 Мбайт или даже 16 Мбайт. Схема адресации реального режима непригодна для работы с такими большими объемами памяти, так как в этой схеме для физического адреса предусмотрено всего 20 разрядов.
Вторым крупным недостатком схемы адресации реального режима является то, что программы, работающие в реальном режиме, имеют полный доступ ко всей адресуемой памяти. Несмотря на то, что в MS-DOS имеются функции управления памятью, с помощью которых программы могут получить в свое распоряжение блоки памяти, ничто не помешает программе выполнить запись за пределами полученного блока или даже в системную область памяти, разрушив MS-DOS.
Если в мультизадачной среде одна задача может писать данные в область памяти, отведенной другой задаче, она может разрушить и эту задачу, и ядро операционной системы. Поэтому в мультизадачных операционных системах, разработанных для процессоров серии Intel 80xxx или Pentium, применяется только защищенный режим работы процессора.
Адресация памяти в защищенном режиме
В защищенном режиме, как и в реальном, логический адрес состоит из двух компонент. Однако эти компоненты называются не сегмент и смещение, а селектор и смещение . Для вычисления физического адреса в процессоре 80286 используются также две таблицы дескрипторов - глобальная таблица дескрипторов GDT (Global Descriptor Table ) и локальная таблица дескрипторов LDT (Local Descroptor Table ). Селектор используется для адресации ячейки одной из таблиц дескрипторов, содержащей помимо прочей информации базовый 24-разрядный адрес сегментов. Для получения физического адреса базовый адрес складывается со смещением, расширенным до 24 разрядов (рис. 2.3).
Рис. 2.3. получение физического адреса в процессоре 80286
Согласно этой схеме адресации памяти, селектор содержит номер ячейки таблицы дескрипторов, но не компоненту физического адреса. Программа может задавать не любые значения селекторов, а только те, которые соответствуют существующим ячейкам таблицы дескрипторов. Разумеется, программа может загрузить в сегментный регистр любое значение, однако при попытке обратиться к сегменту памяти с использованием неправильного селектора работа программы будет прервана.
Таким образом, несмотря на то, что компоненты адреса остались, как и в реальном режиме, 16-разрядными, новая схема адресации защищенного режима процессора 80286 позволяет адресовать до 16 Мбайт памяти, так как в результате преобразования получается 24-разрядный физический адрес.
Кроме индекса, используемого для выбора ячейки дескрипторной таблицы при формировании физического адреса, селектор содержит еще два поля (рис. 2.4).
Рис. 2.4. Формат селектора
Поле TI (Table Indicator ) используется для выбора таблицы дескрипторов. Как мы уже говорили, существуют таблицы дескрипторов двух типов. В любой момент времени может использоваться одна глобальная таблица дескрипторов и одна локальная таблица дескрипторов. Если бит TI равен 0, для выборки базового адреса используется глобальная таблица дескрипторов GDT, если 1 - локальная LDT.
Зачем нужно использовать дескрипторные таблицы двух типов?
В мультизадачной операционной системе можно использовать одну глобальную таблицу дескрипторов для описания областей памяти, принадлежащей операционной системе и несколько локальных таблиц дескрипторов для каждой задачи. В этом случае при соответствующей настройке базовых адресов можно изолировать адресные пространства операционной системы и отдельных задач. Если сделать так, что каждая задача будет пользоваться только своей таблицей дескрипторов, любая задача сможет адресоваться только к своим сегментам памяти, описанным в соответствующей таблице, и к сегментам памяти, описанным в глобальной таблице дескрипторов. В системе может существовать только одна глобальная таблица дескрипторов.
Поле RPL (Requested Privilege Level ) селектора содержит уровень привилегий, запрошенный программой при обращении к сегменту. Программа может обращаться только к таким сегментам, которые имеют соответствующий уровень привилегий. Поэтому программа не может, например, воспользоваться глобальной таблицей дескрипторов для получения доступа к описанным в ней системным сегментам, если она не обладает достаточным уровнем привилегий. На этом основана защита системных данных от разрушения (преднамеренного или в результате программной ошибки) со стороны прикладных программ.
Таблица дескрипторов содержит, помимо базового адреса сегмента, другую информацию, описывающую сегмент (рис. 2.5). Точный формат дескриптора, а также других структур данных и системных регистров, имеющих отношение к работе в защищенном режиме, вы можете найти в 6 томе "Библиотеки системного программиста".
Рис. 2.5. Формат дескриптора сегмента процессора 80286
В частности, дескриптор содержит размер сегмента (предел). При вычислении физического адреса процессор следит за тем, чтобы физический адрес не выходил за пределы, указанные в дескрипторе сегмента. Программа не может обратиться к памяти, лежащей вне пределов, указанных в дескрипторе. Если же она попробует это сделать, ее работа будет прервана.
Поэтому, создав, например, сегмент для хранения массива размером 100 байт, вы не сможете записать в него 101 байт или 10 Кбайт данных.
Поле доступа содержит уровень привилегий сегмента и информацию о типе сегмента. Существуют сегменты кода, сегменты данных и системные сегменты. Кроме того, для сегмента кода можно запретить операцию чтения, а для сегмента данных - операцию записи. Поэтому операционная система может создать сегменты кода, которые можно выполнять, но нельзя читать, и сегменты данных, защищенные от записи.
Что же касается привилегий, в процессорах 80ххх и Pentium существуют четыре уровня привилегий - от 0 до 3, причем наибольшие привилегии соответствуют уровню 0. Уровни привилегий часто называют также кольцами защиты (рис. 2.6).
Рис. 2.6. Кольца защиты
В кольце 0 обычно работает ядро операционной системы. Кольцо 1 соответствует уровню привилегий драйверов, кольцо 2 - системам, таким как системы управления базами данных. В наименее привилегированном кольце 3 располагаются прикладные программы, запускаемые пользователем.
Описанная выше схема распределения привилегий может изменяться от одной операционной системы к другой. В операционной системе Windows 3.1 в нулевом кольце располагаются только виртуальные драйверы, все остальные модули Windows, а также приложения, работают в кольце 3.
Процессоры 80386, 80486 и Pentium используют более сложную схему адресации памяти, которая, однако, остается прозрачной для программиста.
Преобразование адреса в этих процессорах является многоступенчатым. Программы адресуют память с помощью логического адреса, состоящего из 16-разрядного селектора и 32-разрядного смещения. Так же, как и в процессоре 80286, селектор используется для выборки базового адреса сегмента из глобальной или локальной таблицы дескрипторов. Отличие заключается в том, что во-первых, используются 32-разрядные базовый адрес и смещение, а во-вторых, результат сложения называется линейным адресом и используется не для непосредственной адресации памяти, а для дальнейших преобразований (рис. 2.7).
Рис. 2.7. Преобразование логического адреса в линейный
Старшие десять бит линейного адреса используются как индекс в каталоге таблиц страниц (рис. 2.8).
Рис. 2.8. Преобразование линейного адреса в физический
В каталоге таблиц страниц хранятся дескрипторы, содержащие помимо другой информации, физические адреса таблиц страниц.
Следующие десять бит линейного адреса используются для выбора из таблицы страниц, адрес которой определяется старшими десятью битами линейного адреса.
Таблица страниц может описывать до 1024 страниц размером 4096 байт.
Младшие двенадцать бит линейного адреса содержат смещение адресуемого байта внутри страницы.
Отметим, что преобразование линейного адреса в физический выполняется процессором с помощью каталога таблиц страниц и таблиц страниц , подготовленных операционной системой. Программист, создающий приложения для Windows, никогда не работает с таблицами страниц или каталогом таблиц страниц. Он пользуется логическим адресом в формате <селектор:смещение>, поэтому схема преобразования логического адреса в физический остается для него прозрачной.
Операционная система Microsoft Windows версии 3.1 может работать, как вы знаете, в стандартном и расширенном режиме . В первом случае используется схема адресации процессора 80286, даже если в компьютере установлен процессор 80386. Если Windows работает на процессоре 80386, 80486 или Pentium, при наличии достаточного объема оперативной памяти (больше 2 Мбайт) по умолчанию используется расширенный режим работы и, соответственно, схема преобразования адресов процессора 80386.
Основное преимущество системы управления памятью расширенного режима работы Windows заключается в использовании виртуальной памяти. Виртуальная память работает на уровне страниц (описанных в каталогах страниц) и совершенно прозрачна для программиста. Операционная система Windows полностью управляет виртуальной памятью. Если программа пытается обратиться к странице, отсутствующей в памяти и выгруженной на диск, происходит прерывание работы программы, страница подгружается с диска, вслед за чем работа программы продолжается.
Программа может заказывать для себя блоки памяти огромного размера, адресуясь к ним непосредственно, при этом возникает полная иллюзия работы с оперативной памятью большого размера.
Описанная выше схема адресации в защищенном режиме накладывает ограничения на операции, которое приложение Windows может выполнять над селекторами.
Приложение Windows не должно выполнять над селекторами арифметические операции и операции сравнения
Программируя для реального режима операционной системы MS-DOS, вы, возможно, при адресации блока памяти большого размера (больше 64 Кбайт) изменяли содержимое сегментных регистров. В защищенном режиме вы не можете делать никаких предположений относительно базового адреса следующего или предыдущего дескриптора в локальной или глобальной таблице дескрипторов.
Сказанное не означает, что приложения Windows не могут работать с блоками памяти, занимающими несколько сегментов. В этом случае для адресации вам нужно использовать специальные методы. Однако, если вы составляете приложение на языке программирования С или С++, при определении указателей на блоки памяти размером больше, чем 64 Кбайт, можно воспользоваться ключевым словом huge. Для таких указателей при необходимости будет автоматически выполняться переключение на нужные селекторы.
Работа приложения Windows не должна зависеть от уровня привилегий, предоставленного приложению операционной системой, так как в новых версиях Windows этот уровень может измениться.
Приведем исходный текст приложения SELECTOR, с помощью которого вы сможете проанализировать структуру селектора сегмента кода и сегмента данных, взятых из регистров CS и DS (листинг 2.1).
Акселераторы
Для ускорения доступа к строкам меню при помощи клавиатуры (а также для назначения тех или иных функций, не связанных с меню, комбинациям клавиш), используется так называемая таблица акселераторов (accelerator table ).
Таблица акселераторов находится в ресурсах приложения и определяет соответствие между комбинациями клавиш и значением параметра wParam сообщения WM_COMMAND, попадающего в функцию окна, когда вы нажимаете эти комбинации клавиш.
Например, вы можете определить, что комбинации клавиш <Control+Insert> соответствует значение wParam, равное CM_EDITCUT. В этом случае если нажать указанную выше комбинацию клавиш, в функцию окна попадет сообщение WM_COMMAND с параметром wParam, равным CM_EDITCUT.
Обычно комбинации клавиш, используемые для ускоренного выбора (или просто акселераторы) обозначаются в правом столбце меню (рис. 1.17).
Рис. 1.17. Акселераторы в меню "Edit"
Однако такое обозначение, сделанное при помощи символа \t в шаблоне меню не распознается Windows, а служит лишь для удобства пользователя. Для того чтобы комбинация клавиш стала работать как акселератор, она должна быть описана в таблице акселераторов. Кроме этого, приложение должно загрузить таблицу акселераторов из ресурсов приложения и изменить цикл обработки сообщений.
Активизация и блокирование строк меню
Для изменения состояния элемента меню удобно использовать функцию EnableMenuItem :
BOOL WINAPI EnableMenuItem(HMENU hmenu, UINT idItem, UINT uEnable);
Параметр hmenu указывает идентификатор меню, над элементом которого будет выполняться операция активизации или блокирования.
Параметр idItem определяет элемент меню, над которым выполняется операция. Интерпретация этого параметра зависит от значения параметра uEnable.
Параметр uEnable может принимать значения MF_DISABLED, MF_ENABLED или MF_GRAYED в комбинации с одним из значений: MF_BYCOMMAND или MF_BYPOSITION.
Для блокирования элемента меню необходимо использовать значение MF_DISABLED. Если заблокированный элемент меню нужно изобразить серым цветом, вместо MF_DISABLED используйте значение MF_GRAYED.
Для активизации заблокированного ранее элемента меню укажите значение MF_ENABLED.
Если в параметре fuFlags указан флаг MF_BYCOMMAND, параметр idItem определяет идентификатор элемента меню, состояние которого будет изменено. Если указан флаг MF_BYPOSITION, параметр idItem определяет порядковый номер элемента меню, состояние которого будет изменено.
Как и после выполнения других операций по изменению меню, после изменения состояния элемента меню необходимо вызвать функцию DrawMenuBar, которая отобразит внесенные изменения на экране.
Анализ DLL-библиотек при помощи утилиты tdump.exe
В комплекте системы разработки Borland Turbo C++ for Windows входит утилита tdump.exe , предназначенная для работы в среде MS-DOS. С помощью этой утилиты вы сможете проанализировать содержимое любой DLL-библиотеки, определив имена экспортируемых функций, их порядковые номера, имена DLL-библиотек и номера функций, импортируемых из этих библиотек и т. д.
В системном каталоге Windows имеется DLL-библиотека toolhelp.dll, предназначенная для создания отладочных средств и работы с внутренними структурами данных Windows. Мы выбрали эту библиотеку для наших исследований в основном из-за ее небольшого размера. Описание самой библиотеки выходит за рамки данного тома "Библиотеки системного программиста".
Итак, скопируйте библиотеку в любой временный каталог и введите в среде MS-DOS (или в среде виртуальной машины MS-DOS) следующую команду:
tdump toolhelp.dll toolhelp.map
В результате в файл toolhelp.map будет записано подробное описание библиотеки toolhelp.dll. Мы приведем полученный листинг с нашими комментариями.
Turbo Dump Version 3.1 Copyright (c) 1988, 1992 Borland International Display of File TOOLHELP.DLL
В начале файла DLL-библиотеки находится заголовок в формате MS-DOS. Он нас не интересует.
Old Executable Header
DOS File Size 3730h ( 14128. ) Load Image Size 359h ( 857. ) Relocation Table entry count 0000h ( 0. ) Relocation Table address 0040h ( 64. ) Size of header record (in paragraphs) 0004h ( 4. ) Minimum Memory Requirement (in paragraphs) 0000h ( 0. ) Maximum Memory Requirement (in paragraphs) FFFFh ( 65535. ) File load checksum 0000h ( 0. ) Overlay Number 0000h ( 0. )
Initial Stack Segment (SS:SP) 0000:00B8 Program Entry Point (CS:IP) 0000:0000
Далее в листинге приводится информация о заголовке загрузочного файла в формате Windows. Это так называемый заголовок нового исполняемого формата.
New Executable header Operating system Windows File Load CRC 0AABAA86Bh Program Entry Point (CS:IP) 0001:016A Initial Stack Pointer (SS:SP) 0000:0000 Auto Data Segment Index 0002h ( 2. ) Initial Local Heap Size 0200h ( 512. ) Initial Stack Size 0000h ( 0. ) Segment count 0002h ( 2. ) Module reference count 0002h ( 2. ) Moveable Entry Point Count 0000h ( 0. ) File alignment unit size 0010h ( 16. ) DOS File Size 3730h ( 14128. ) Linker Version 5.20
Обратите внимание, что в заголовке присутствует информация об адресе точки входа (Program entry Point). Начальное содержимое указателя стека (Initial Stack Pointer) равно нулю, так же как и начальный размер стека (Initial Stack Size). Это понятно, так как DLL-библиотека не имеет собственного стека.
В то же время начальный размер локальной области данных отличен от нуля и равен 512 байт (Initial Local Heap Size). Из заголовка можно также определить, что модуль DLL-библиотеки состоит из двух сегментов (Segment Count).
Далее в листинге приведены различные флаги. В частности, видно, что сегмент данных DGROUP имеет атрибуты single и может использоваться совместно различными приложениями (shared). Данный модуль может работать только в защищенном режиме (Protected mode only) и является ни чем иным, как DLL-библиотекой (Module type - Dynamic link Library(DLL)).
Program Flags DGROUP : single (shared) Global initializaton : No Protected mode only : Yes Application type : Uses windowing API Self Loading : No Errors in image : No Module type : Dynamic link Library (DLL) Other EXE Flags 2.X protected mode : No 2.X proportional font : No Gangload area : Yes Start of Gangload Area 03E0h Length of Gangload Area 3160h Miminum code swap area size 0 Expected Windows Version 3.00
Затем в листинге перечисляются различные таблицы с указанием их смещения и размера:
Segment Table Offset: 00C0h Length: 0010h Resource Table Offset: 00D0h Length: 0018h Resident Names Table Offset: 00E8h Length: 0012h Module Reference Table Offset: 00FAh Length: 0004h Imported Names Table Offset: 00FEh Length: 000Dh Entry Table Offset: 010Bh Length: 0070h Nonresident Names Table Offset: 017Bh Length: 0236h
Это таблица сегментов. В ней описаны два сегмента. Первый сегмент является сегментом кода, второй - сегментом данных.
Segment Table offset: 00C0h
Segment Number: 01h Segment Type: CODE Alloc Size : 2EEEh Sector Offset: 0040h File length: 2EEEh Attributes: Preloaded Relocations
Segment Number: 02h Segment Type: DATA Alloc Size : 0120h Sector Offset: 033Eh File length: 0120h Attributes: Sharable Preloaded
Далее приводится информация о ресурсах DLL-библиотеки. Таблица ресурсов описывает единственный ресурс типа Version info, описывающий версию модуля.
Resource Table offset: 00D0h
Sector size: 0010h
type: Version info Identifier: 1 offset: 03540h length: 01F0h Attributes: Moveable Shareable
После таблицы ресурсов описывается таблица резидентных имен. В ней перечисляются имена функций, которые загружаются в память вместе с библиотекой и находятся там до момента выгрузки библиотеки из памяти. В нашем случае в таблице резидентных имен описана единственная функция WEP.
Resident Name Table offset: 00E8h Module Name: 'TOOLHELP' Name: WEP Entry: 0001
Таблица ссылок на модули содержит имена модулей (DLL-библиотек), на которые ссылаются функции, расположенные в исследуемой библиотеке. Как видно из листинга, функции библиотеки toolhelp.dll ссылаются на модули KERNEL и USER. Эти модули являются основными компонентами операционной системы Windows и расположены, соответственно, в файлах krnl386.exe, krnl286.exe и user.exe.
Module Reference Table offset: 00FAh Module 1: KERNEL Module 2: USER
Imported Names Table offset: 00FEh name offset KERNEL 0001h USER 0008h
Таблица входов (Entry Table) описывает экспортируемые функции. Для каждой функции приводится ее порядковый номер и смещение.
Entry Table offset: 010Bh Fixed Segment Records ( 1 Entries) Segment: 0001h Entry 1: Offset: 018Ah Exported Single data
Null Entries: 48
Fixed Segment Records ( 34 Entries) Segment: 0001h Entry 50: Offset: 057Bh Exported Single data Entry 51: Offset: 0318h Exported Single data Entry 52: Offset: 0399h Exported Single data Entry 53: Offset: 02A2h Exported Single data Entry 54: Offset: 0417h Exported Single data Entry 55: Offset: 04A9h Exported Single data Entry 56: Offset: 090Eh Exported Single data Entry 57: Offset: 095Eh Exported Single data Entry 58: Offset: 09E9h Exported Single data Entry 59: Offset: 0A90h Exported Single data Entry 60: Offset: 0AD9h Exported Single data Entry 61: Offset: 0B15h Exported Single data Entry 62: Offset: 0B8Ch Exported Single data Entry 63: Offset: 0CAAh Exported Single data Entry 64: Offset: 0CEDh Exported Single data Entry 65: Offset: 0D2Eh Exported Single data Entry 66: Offset: 0F1Ch Exported Single data Entry 67: Offset: 0F67h Exported Single data Entry 68: Offset: 0FCAh Exported Single data Entry 69: Offset: 28B0h Exported Single data Entry 70: Offset: 2925h Exported Single data Entry 71: Offset: 11CEh Exported Single data Entry 72: Offset: 13F4h Exported Single data Entry 73: Offset: 1B72h Exported Single data Entry 74: Offset: 1C29h Exported Single data Entry 75: Offset: 2060h Exported Single data Entry 76: Offset: 2111h Exported Single data Entry 77: Offset: 26EAh Exported Single data Entry 78: Offset: 29C4h Exported Single data Entry 79: Offset: 2B6Ch Exported Single data Entry 80: Offset: 2DAEh Exported Single data Entry 81: Offset: 0D68h Exported Single data Entry 82: Offset: 0D97h Exported Single data Entry 83: Offset: 0DC0h Exported Single data
Имена и порядковые номера экспортируемых функций приведены в таблице нерезидентных имен (Non-Resident Name Table):
Non-Resident Name Table offset: 017Bh Module Description: 'TOOLHELP - Debug/Tool Helper library' Name: TASKSETCSIP Entry: 81 Name: MEMMANINFO Entry: 72 Name: STACKTRACEFIRST Entry: 66 Name: MEMORYWRITE Entry: 79 Name: GLOBALINFO Entry: 53 Name: TASKNEXT Entry: 64 Name: CLASSNEXT Entry: 70 Name: GLOBALENTRYHANDLE Entry: 54 Name: GLOBALHANDLETOSEL Entry: 50 Name: INTERRUPTREGISTER Entry: 75 Name: STACKTRACECSIPFIRST Entry: 67 Name: LOCALNEXT Entry: 58 Name: INTERRUPTUNREGISTER Entry: 76 Name: MODULENEXT Entry: 60 Name: LOCALINFO Entry: 56 Name: TASKFINDHANDLE Entry: 65 Name: TASKSWITCH Entry: 83 Name: MEMORYREAD Entry: 78 Name: NOTIFYREGISTER Entry: 73 Name: GLOBALNEXT Entry: 52 Name: TIMERCOUNT Entry: 80 Name: MODULEFINDHANDLE Entry: 62 Name: MODULEFIRST Entry: 59 Name: GLOBALENTRYMODULE Entry: 55 Name: STACKTRACENEXT Entry: 68 Name: GLOBALFIRST Entry: 51 Name: SYSTEMHEAPINFO Entry: 71 Name: TERMINATEAPP Entry: 77 Name: TASKFIRST Entry: 63 Name: NOTIFYUNREGISTER Entry: 74 Name: TASKGETCSIP Entry: 82 Name: CLASSFIRST Entry: 69 Name: MODULEFINDNAME Entry: 61 Name: LOCALFIRST Entry: 57
Далее в листинге описываются ссылки на импортируемые модули. Каждая такая ссылка состоит из имени модуля (в нашем случае это KERNEL или USER) и порядкового номера импортируемой функции. Сделав дамп файла krnl386.exe при помощи утилиты tdump.exe, вы сможете определить, что ссылке KERNEL.3 соответствует функция GetVersion, ссылке KERNEL.4 - функция LocalInit, а ссылке KERNEL.5 - функция LocalAlloc.
Segment Relocation Records
Segment 0001h relocations
type offset target BASE 2BBBh 0001h:0000h BASE 2E93h 0002h:0000h PTR 020Fh KERNEL.3 PTR 0177h KERNEL.4 PTR 27D9h KERNEL.5 PTR 28A5h KERNEL.7 PTR 2761h KERNEL.137 OFFS 2D47h KERNEL.114 BASE 0214h KERNEL.18 PTR 2E66h USER.13 PTR 0E2Bh KERNEL.150 PTR 01EAh KERNEL.28 PTR 27B0h KERNEL.36 PTR 23E5h KERNEL.170 PTR 223Dh KERNEL.47 PTR 23FDh KERNEL.176 PTR 224Fh KERNEL.50 PTR 0B66h USER.430 PTR 28F6h USER.179 PTR 29A5h KERNEL.72 OFFS 2EC1h KERNEL.178 PTR 1D79h KERNEL.202 OFFS 27EFh 0001h:16C5h LOBYTE 2ACEh KERNEL.114 additive LOBYTE 2CA4h KERNEL.114 additive Relocations: 25
Автоматическая память
Автоматические данные располагаются, как и статические, в автоматическом сегменте данных (рис. 2.10), назначаемым приложению при его загрузке в память. К автоматическим данным относится стек, параметры функций и локальные переменные.
Как мы уже говорили, размер стека определяется оператором STACKSIZE в файле определения модуля и не увеличивается автоматически во время работы приложения.
Библиотека импорта
Для того чтобы редактор связей мог создать ссылку, в файл проекта приложения вы должны включить так называемую библиотеку импорта (import library ), созданную приложением Import Lib , входящим в состав системы разработки Borland Turbo C++ for Windows. Соответствующее средство имеется в SDK, а также в любой другой системе разработки приложений Windows.
Библиотека импорта создается либо на основе dll-файла библиотеки, либо на основе файла определения модуля, используемого для создания DLL-библиотеки.
В любом случае вам надо запустить приложение Import Lib, и из меню "File" выбрать строку "File Select...". При этом на экране появится диалоговая панель "Select File", с помощью которой вы можете выбрать нужный вам dll- или def-файл (рис. 3.6).
Рис. 3.6. Работа с приложением Import Library
После выбора файла приложение Import Lib создаст библиотеку импорта в виде lib-файла, расположенного в том же каталоге, что и исходный dll- или def-файл. Этот файл необходимо включить в проект создаваемого вами приложения, пользующегося функциями DLL-библиотеки.
Следует заметить, что стандартные библиотеки систем разработки приложений Windows содержат как обычные объектные модули, предназначенные для статической компоновки, так и ссылки на различные стандартные DLL-библиотеки, экспортирующие функции программного интерфейса операционной системы Windows.
В состав системы разработки Borland C++ for Windows версии 4.0 и 4.01 входит DLL-библиотека, содержащая функции стандартной библиотеки компилятора. Эта библиотека расположена в файле с именем bc40rtl.dll. В проекте приложения вы можете определить, что для стандартных функций следует использовать динамическую компоновку. В этом случае размер полученного загрузочного модуля заметно сократится, однако для работы приложения будет нужен файл bc40rtl.dll.
Библиотеки динамической компоновки
3.1.
3.2.
3.3.
3.4.
3.5.
3.6.
3.7.
В этой главе мы расскажем вам о важнейшем механизме, лежащем в основе операционной системы Windows - механизме библиотек динамической компоновки DLL. Если вы уже заметили, в Windows имеется много важнейших механизмов и систем, например, только что рассмотренная система управления памятью, интерфейс графических устройств GDI, система динамического обмена данными DDE, система управления шрифтами, интерфейсы для мультимедиа, система динамической вставки и привязки объектов OLE, и так далее, и так почти до бесконечности.
Однако будем терпеливыми, и постараемся сосредоточиться, так как вся операционная система Windows и все ее драйверы (кроме виртуальных), а также другие расширения в некотором смысле есть ни что иное, как набор библиотек динамической компоновки. Редкое крупное приложение Windows обходится без собственных библиотек динамической компоновки, и ни одно приложение не может обойтись без вызова функций, расположенных в таких библиотеках. В частности, все функции программного интерфейса Windows находятся именно в библиотеках динамической компоновки DLL (Dynamic-Link Libraries ).
Что же это за библиотеки и почему они имеют такое большое значение?
Чтение и запись
Для выполнения операций чтения и записи в программном интерфейсе операционной системы Windows версии 3.1 предусмотрены четыре функции: _lread, _hread, _lwrite, _hwrite.
Функция _lread предназначена для чтения из открытого файла:
UINT WINAPI _lread( HFILE hf, // идентификатор файла void _huge* hpvBuffer, // адрес буфера UINT cbBuffer); // размер буфера
Функция возвращает количество байт данных, прочитанных из файла. Возвращенное значение может быть меньше затребованного в параметре cbBuffer, если в процессе чтения был достигнут конец файла. При ошибке функция возвращает значение HFILE_ERROR.
Через параметр hf функции следует передать идентификатор файла, для которого необходимо выполнить операцию чтения.
Прочитанные данные будут записаны в буфер hpvBuffer, имеющий размер cbBuffer байт. Этот буфер можно получить динамически, вызывав, например, функцию GlobalAlloc или LocalAlloc. Размер буфера не должен превышать 65534 байт.
В программном интерфейсе операционной системы Windows версии 3.1 появилась функция _hread , с помощью которой можно выполнять чтение из файла блоков практически любого размера:
long WINAPI _hread( HFILE hf, // идентификатор файла void _huge* hpvBuffer, // адрес буфера long cbBuffer); // размер буфера
Так же как и функция _lread, функция _hread возвращает количество байт данных, прочитанных из файла. Возвращенное значение может быть меньше затребованного в параметре cbBuffer, если в процессе чтения был достигнут конец файла. При ошибке функция возвращает значение HFILE_ERROR.
Вы можете с помощью функции GlobalAlloc заказать для функции _hread буфер размером, большим 64 Кбайт.
С помощью функции _lwrite вы можете выполнить запись данных в файл:
UINT WINAPI _lwrite( HFILE hf, // идентификатор файла void _huge* hpvBuffer, // адрес буфера UINT cbBuffer); // размер буфера
Назначение параметров этой функции аналогично назначению параметров функции _lread. Перед вызовом функции _lwrite буфер должен содержать записываемые в файл данные.
Функция возвращает количество байт данных, записанных в файл, или значение HFILE_ERROR при ошибке.
Если вам надо писать в файл блоки, имеющие размер больше 64 Кбайт, воспользуйтесь функцией _hwrite , которая впервые появилась в программном интерфейсе Windows версии 3.1:
long WINAPI _hwrite( HFILE hf, // идентификатор файла void _huge* hpvBuffer, // адрес буфера long cbBuffer); // размер буфера
Назначение параметров функции аналогично назначению параметров функции _lwrite. Функция возвращает количество байт данных, записанных в файл, или значение HFILE_ERROR при ошибке.
Дефрагментация локального блока памяти
Функция LocalCompact выполняет дефрагментацию свободного пространства в локальной области данных:
UINT WINAPI LocalCompact(UINT uMinFree);
Функция возвращает размер самого большого доступного непрерывного блока в локальной области памяти, причем, если параметр на равен 0 или -1, выполняется дефрагментация памяти и удаление блоков, отмеченных как удаляемые. Если параметр функции указан как 0, функция не выполняет дефрагментацию памяти, но возвращает правильное значение с учетом возможного выполнения дефрагментации.
Дефрагментация памяти
Если вызвать функцию GlobalCompact , операционная система Windows выполнит объединение всех свободных блоков в один. В качестве параметра этой функции необходимо указать требуемый размер непрерывного блока свободной памяти:
DWORD WINAPI GlobalCompact(DWORD dwMinFree);
Функция возвращает размер самого большого доступного непрерывного блока памяти, причем, если параметр на равен 0 или -1, выполняется дефрагментация памяти и удаление блоков, отмеченных как удаляемые. Если параметр функции указан как 0 или -1, функция не выполняет дефрагментацию памяти, но возвращает правильное значение с учетом возможного выполнения дефрагментации.
Динамический импорт функций во время выполнения приложения
В некоторых случаях невозможно выполнить динамическую компоновку на этапе редактирования. Вы можете, например, создать приложение, которое состоит из основного модуля и дополнительных, реализованных в виде DLL-библиотек. Состав этих дополнительных модулей и имена файлов, содержащих DLL-библиотеки, может изменяться, при этом в приложение могут добавляться новые возможности.
Если вы, например, разрабатываете систему распознавания речи, то можете сделать ее в виде основного приложения и набора DLL-библиотек, по одной библиотеке для каждого национального языка. В продажу система может поступить в комплекте с одной или двумя библиотеками, но в дальнейшем пользователь сможет купить дополнительные библиотеки и, просто переписав новые библиотеки на диск, получить возможность работы с другими языками. При этом основной модуль приложения не может "знать" заранее имена файлов дополнительных DLL-библиотек, поэтому динамическая компоновка с использованием библиотеки импорта или оператора IMPORTS файла определения модуля невозможна.
Однако приложение может в любой момент времени загрузить любую DLL-библиотеку, вызвав специально предназначенную для этого функцию программного интерфейса Windows с именем LoadLibrary . Приведем ее прототип:
HINSTANCE WINAPI LoadLibrary(LPCSTR lpszLibFileName);
Параметр функции является указателем на текстовую строку, закрытую двоичным нулем. В эту строку перед вызовом функции следует записать путь к файлу DLL-библиотеки или имя этого файла. Если путь к файлу не указан, при поиске выполняется последовательный просмотр следующих каталогов:
текущий каталога;
каталог, в котором находится операционная система Windows;
системный каталог Windows;
каталог, в котором находится загрузочный файл приложения, загружающего DLL-библиотеку;
каталоги, перечисленные в операторе описания среды PATH, расположенном в файле autoexec.bat;
каталоги, расположенные в сети, если для них определены пути поиска.
Если файл DLL-библиотеки найден, функция LoadLibrary возвращает идентификатор модуля библиотеки.
В противном случае возвращается код ошибки, который по своему значению меньше величины HINSTANCE_ERROR, определенной в файле windows.h. Возможны следующие коды ошибок:
Код ошибки | Описание |
0 | Мало памяти, неправильный формат загружаемого файла |
2 | Файл не найден |
3 | Путь к файлу не существует |
5 | Была предпринята попытка динамически загрузить приложение или произошла ошибка при совместном использовании файлов. Также возможно, что была предпринята попытка доступа к файлу в сети пользователем, не имеющим на это достаточных прав |
6 | Данная библиотека требует отдельный сегмент данных для каждой задачи |
8 | Мало памяти для запуска приложения |
10 | Неправильная версия операционной системы Windows |
11 | Неправильный формат загрузочного файла приложения Windows |
12 | Данное приложение разработано для операционной системы, отличной от Microsoft Windows |
13 | Данное приложение разработано для MS-DOS версии 4.0 |
14 | Неизвестный тип исполняемого файла |
15 | Была предпринята попытка загрузить приложение, разработанное для реального режима работы процессора в среде ранних версий операционной системы Windows |
16 | Была предпринята попытка загрузить вторую копию исполняемого файла, содержащего сегменты данных, отмеченные как multiple, но не защищенные от записи |
19 | Попытка загрузки компрессованного выполнимого файла |
20 | Файл, содержащий DLL-библиотеку, имеет неправильный формат |
21 | Данное приложение работает только в среде 32-битового расширения Windows |
В качестве примера приведем фрагмент исходного текста приложения, загружающего DLL-библиотеку из файла srcdll.dll:
HINSTANCE hLib; hLib = LoadLibrary("srcdll.dll"); if(hLib >= HINSTANCE_ERROR) { // Работа с DLL-библиотекой } FreeLibrary(hLib);
Если DLL- библиотека больше не нужна, ее следует освободить с помощью функции FreeLibrary :
void WINAPI FreeLibrary(HINSTANCE hLibrary);
В качестве параметра этой функции следует передать идентификатор освобождаемой библиотеки.
Если счетчик использования DLL-библиотеки становится равным нулю (что происходит, когда все приложения, работавшие с библиотекой, освободили ее или завершили свою работу), DLL-библиотека выгружается из памяти. При этом вызывается функция WEP, выполняющая все необходимые завершающие действия.
Однако прежде чем выгружать библиотеку из памяти, приложение, вероятно, захочет вызвать из нее хотя бы одну функцию, поэтому теперь мы расскажем о том, как вызвать функцию из загруженной DLL-библиотеки.
Для того чтобы вызвать функцию из библиотеки, зная ее идентификатор, необходимо получить значение дальнего указателя на эту функцию, вызвав функцию GetProcAddress :
FARPROC WINAPI GetProcAddress(HINSTANCE hLibrary, LPCSTR lpszProcName);
Через параметр hLibrary вы должны передать функции идентификатор DLL-библиотеки, полученный ранее от функции LoadLibrary.
Параметр lpszProcName является дальним указателем на строку, содержащую имя функции или ее порядковый номер, преобразованный макрокомандой MAKEINTRESOURCE.
Приведем фрагмент кода, в котором определяются адреса двух функций. В первом случае используется имя функции, а во втором - ее порядковый номер:
FARPROC lpMsg; FARPROC lpTellMe; lpMsg = GetProcAddress(hLib, "Msg"); lpTellMe = GetProcAddress(hLib, MAKEINTRESOURCE(8));
Перед тем как передать управление функции по полученному адресу, следует убедиться в том, что этот адрес не равен NULL:
if(lpMsg != (FARPROC)NULL) { (*lpMsg)((LPSTR)"My message"); }
Для того чтобы включить механизм проверки типов передаваемых параметров, вы можете определить свой тип - указатель на функцию, и затем использовать его для преобразования типа адреса, полученного от функции GetProcAddress:
typedef int (FAR PASCAL *LPGETZ)(int x, int y); LPGETZ lpGetZ; lpGetZ = (LPGETZ)GetProcAddress(hLib, "GetZ");
А что произойдет, если приложение при помощи функции LoadLibrary попытается загрузить DLL-библиотеку, которой нет на диске?
В этом случае операционная система Windows выведет на экран диалоговую панель с сообщением о том, что она не может найти нужную DLL-библиотеку. В некоторых случаях появление такого сообщения нежелательно, так как либо вас не устраивает внешний вид этой диалоговой панели, либо по логике работы вашего приложения описанная ситуация является нормальной.
Для того чтобы отключить режим вывода диалоговой панели с сообщением о невозможности загрузки DLL-библиотеки, вы можете использовать функцию SetErrorMode , передав ей в качестве параметра значение SEM_NOOPENFILEERRORBOX .
Приведем прототип функции SetErrorMode:
UINT WINAPI SetErrorMode(UINT fuErrorMode);
Эта функция позволяет отключать встроенный в Windows обработчик прерывания MS-DOS INT24h (критическая ошибка). В качестве параметра этой функции можно указывать комбинацию следующих значений:
Значение | Описание |
SEM_FAILCRITICALERRORS | Операционная система Windows не выводит на экран сообщение обработчика критических ошибок, возвращая приложению соответствующий код ошибки |
SEM_NOGPFAULTERRORBOX | На экран не выводится сообщение об ошибке защиты памяти. Этот флаг может использоваться только при отладке приложений, если они имеют собственный обработчик такой ошибки |
SEM_NOOPENFILEERRORBOX | Если Windows не может открыть файл, на экран не выводится диалоговая панель с сообщением об ошибке |
DLL-библиотеки в операционной системе Windows
Формат DLL-библиотеки почти идентичен формату загрузочного модуля приложения Windows, однако вы не можете "запустить" DLL-библиотеку на выполнение, как обычное приложение. И это понятно, так как назначение DLL-библиотек другое - они служат хранилищем функций, вызываемых приложениями во время работы. Функции, расположенные в DLL-библиотеках, могут вызывать функции, которые находятся в других библиотеках (рис. 3.3).
Рис. 3.3. Вызов функций из DLL-библиотек
Для того чтобы вам были понятны отличия между приложением и DLL-библиотекой, уточним такие понятия, как задача (task ), копия приложения (instance ) и модуль (module ).
Когда вы запускаете несколько копий одного приложения, Windows загружает в оперативную память только одну копию кода и ресурсов. Для каждой копии приложения создается отдельный сегмент данных, стек и очередь сообщений (рис. 3.4).
Рис. 3.4. Копии приложения и модуль приложения
Задачу (task) можно рассматривать как совокупность виртуального процессора и ресурсов, таких как стек, регистры процессора, глобальная память, очередь сообщений и т. д. В операционной системе Windows имеется база данных запущенных задач, в которой хранится вся относящаяся к задаче информация, например, идентификатор приложения, идентификатор модуля приложения и т. д.
Копия приложения (instance) является контекстом, в котором выполняется модуль приложения. Идентификатор копии приложения, который вы получаете через параметр hInstance функции WinMain, является идентификатором сегмента данных DGROUP, используемого при выполнении программного модуля.
Модуль приложения (module) состоит из исполнимого кода и ресурсов. Внутри Windows есть база данных модулей, в которой хранится такая информация, как ссылки на экспортируемые функции, расположение сегментов кода и сегментов, содержащих ресурсы. Так как модуль приложения не изменяется (сегменты кода и ресурсов не изменяются), все запущенные копии одного приложения могут им пользоваться.
DLL-библиотека также является модулем.
Она находится в памяти в единственном экземпляре, содержит сегменты кода и ресурсы, а так же один сегмент данных (рис. 3.5). Можно сказать, что для DLL-библиотеки создается одна копия (instance), состоящая только из сегмента данных, и один модуль, состоящий из кода и ресурсов.
Рис. 3.5. Структура DLL-библиотеки в памяти
DLL-библиотека, в отличие от приложения, не имеет стека и очереди сообщения.
Функции, расположенные в модуле DLL-библиотеки, выполняются в контексте вызвавшей их задачи. При этом они пользуются стеком копии приложения, так как собственного стека в DLL-библиотеке не предусмотрено.
Тем не менее, функции, расположенные в DLL-библиотеке, пользуются сегментом данных, принадлежащей этой библиотеке, а не копии приложения. При этом возникают трудности, связанные с тем, что сегментный регистр SS, с помощью которого адресуется стек, указывает на стек копии приложения, а регистр DS - на сегмент данных DLL-библиотеки. В обычных приложениях Windows регистры SS и DS адресуют один и тот же автоматический сегмент данных. Позже мы займемся этим вопросом вплотную.
Для чего используются DLL-библиотеки ?
Прежде всего для экономии памяти, так как все запущенные приложения могут использовать один модуль DLL-библиотеки, не включая стандартные функции в состав своих модулей.
С помощью DLL-библиотек можно организовать коллективное использование ресурсов или данных, расположенных в сегменте данных библиотеки. Более того, вы можете создать DLL-библиотеки, состоящие только из одних ресурсов, например, из пиктограмм или изображений bitmap. В состав Windows входит DLL-библиотека moricons.dll, состоящая из одних пиктограмм. Файлы с расширением fon представляют собой ни что иное, как DLL-библиотеки, содержащие шрифты в виде ресурса.
Функции, входящие в состав DLL-библиотеки, могут заказывать блоки памяти с атрибутом GMEM_SHARE. Такой блок памяти не принадлежит ни одному приложению и поэтому не освобождается автоматически при завершении работы приложения. Так как в Windows версии 3.1 все приложения используют общую глобальную память, блоки памяти с атрибутом GMEM_SHARE можно использовать для обмена данными между приложениями.
Управлять таким обменом могут, например, функции, расположенные в соответствующей DLL-библиотеке. Однако в следующих версиях Windows каждое приложение будет работать в собственном адресном пространстве, поэтому для организации обмена данных между приложениями следует использовать специальный механизм динамического обмена данными DDE, который мы рассмотрим позже в отдельной главе.
Использование DLL-библиотек повышает модульность приложений и самой операционной системы Windows. С точки зрения приложения DLL-библиотека является не более чем набором функций с тем или иным интерфейсом, а также, возможно, набором ресурсов. Внутреннее "устройство" и алгоритмы работы функций, а также используемые функциями структуры данных полностью скрыты от приложения. Поэтому при внесении изменений или усовершенствований в DLL-библиотеки нет необходимости выполнять повторную сборку приложений (если не изменился интерфейс или набор функций, входящих в библиотеку).
Область применения DLL-библиотек достаточно широка. Помимо предоставления приложениям функций организации пользовательского интерфейса и реализации различных расширений Windows типа мультимедиа или систем управления базами данных, DLL-библиотеки необходимы для обеспечения ряда системных операций.
Например, приложение может организовать "перехват" системных сообщений или функций, при этом соответствующие модули "перехватчика" необходимо располагать в фиксированном сегменте кода DLL-библиотеки. Для удаляемых (discardable) блоков памяти можно, вызвав функцию GlobalNotify, определить функцию, которой будет передаваться управление при попытке удалить блок из памяти. Такая функция должна находиться в фиксированном сегменте кода в DLL-библиотеке. Если ваше приложение обрабатывает аппаратные прерывания или само вызывает программные прерывания, ему также не обойтись без DLL-библиотек (единственный способ обработки прерываний в реальном времени - создание виртуального драйвера). Наконец, все обычные драйвера устройств в операционной системе Windows реализованы с помощью DLL-библиотек.
Даже если вы не собираетесь обрабатывать или вызывать прерывания и не разрабатываете собственный драйвер, отдельные подсистемы большого приложения имеет смысл оформлять в виде DLL-библиотек из соображений модульности и доступности библиотек для других приложений. Например, в приложении SMARTPAD мы создали орган управления Toolbar с использованием разработанного нами класса С++ Toolbar. Однако если бы мы сосредоточили все функции этого класса в DLL-библиотеке, нашим органом управления могли бы воспользоваться и другие созданные нами приложения.
Разумеется, у динамической компоновки есть и свои недостатки.
Во-первых, DLL-библиотеки сложнее в разработке по сравнению с обычными библиотеками статической компоновки. Приходится принимать во внимание неравенство содержимого регистров DS и SS во время выполнения функций, расположенных в DLL-библиотеке, а также то, что DLL-библиотека имеет единственный сегмент данных, общий для всех приложений, вызывающих функции из библиотеки.
Во-вторых, в дополнение к exe-файлу вместе с приложением необходимо устанавливать один или несколько dll-файлов, что в некоторой степени усложняет процесс установки и сопровождения приложения. Может также возникнуть ситуация, при которой приложение не находит свою DLL-библиотеку, несмотря на то, что нужная библиотека есть на диске.
В-третьих, без тщательного планирования состава функций, включаемых в DLL-библиотеку, экономии памяти может и не получиться. В частности, возможна такая ситуация, когда и приложение, и DLL-библиотека пользуются одними и теми же функциями стандартной библиотеки компилятора в варианте статической компоновки.
Добавление строк
Для добавления строк в созданные функциями CreateMenu и CreatePopupMenu пустые меню можно воспользоваться функцией AppendMenu :
BOOL WINAPI AppendMenu(HMENU hmenu, UINT fuFlags, UINT idNewItem, LPCSTR lpszNewItem);
Параметр hmenu указывает идентификатор меню, к которому добавляется строка или временное меню. Вы должны использовать значение, полученное от функций CreateMenu и CreatePopupMenu.
Параметр fuFlags определяет атрибуты создаваемого элемента меню. Можно указывать следующие значения (соответствующие символические константы описаны в файле windows.h):
Константа | Описание |
MF_BITMAP | Для изображения строки меню используется графическое изображение bitmap. Если указан этот параметр, младшее слово параметра lpszNewItem содержит идентификатор изображения |
MF_CHECKED | При выводе меню на экран строка меню отмечается галочкой"" |
MF_DISABLED | Строка меню отображается в нормальном виде (не серым цветом), но находится в неактивном состоянии |
MF_ENABLED | Строка меню разблокирована и отображается в нормальном виде |
MF_GRAYED | Строка меню отображается серым цветом и находится в неактивном состоянии. Такую строку нельзя выбрать |
MF_MENUBREAK | Если описывается меню верхнего уровня, элемент меню выводится с новой строки. Если описывается временное меню, элемент меню выводится в новом столбце |
MF_MENUBARBREAK | Аналогично MF_MENUBREAK, но дополнительно новый столбец отделяется вертикальной линией (используется при создании временных меню) |
MF_OWNERDRAW | Строка меню рисуется окном, создавшем меню. Когда меню отображается в первый раз, функция этого окна получает сообщение WM_MEASUREITEM, в ответ на которое функция окна должна сообщить размеры области, занимаемой изображением строки меню. Рисовать изображение строки надо тогда, когда в функцию окна придет сообщение WM_DRAWITEM. Флаг MF_OWNERDRAW можно указывать только для временных меню |
MF_POPUP | С данным элементом меню связывается временное меню. Если используется этот флаг, параметр idNewItem должен содержать идентификатор временного меню, связанного с данным элементом |
MF_SEPARATOR | Используется для создания горизонтальной разделительной линии во временных меню. Если указан этот флаг, параметры lpszNewItem и idNewItem не используются |
MF_STRING | Элемент меню является строкой символов. Параметр lpszNewItem должен указывать на строку символов, закрытую двоичным нулем |
MF_UNCHECKED | При выводе меню на экран строка не отмечается галочкой "" |
Вы можете указывать сразу несколько флагов, объединив их операцией логического ИЛИ, однако следует иметь в виду, что существует четыре группы взаимно несовместимых флагов:
MF_DISABLED, MF_ENABLED, MF_GRAYED |
MF_BITMAP, MF_OWNERDRAW, MF_STRING |
MF_MENUBREAK, MF_MENUBARBREAK |
MF_CHECKED, MF_UNCHECKED |
Назначение параметра lpszNewItem также зависит от параметра fuFlags. Если этот параметр равен MF_STRING, параметр lpszNewItem должен указывать на строку символов, закрытую двоичным нулем, если MF_BITMAP - младшее слово параметра lpszNewItem содержит идентификатор изображения, а если параметр fuFlags равен MF_OWNERDRAW, приложение должно передать через параметр lpszNewItem 32-битовое значение, идентифицирующее строку меню.
Еще одна функция, предназначенная для добавления элементов в меню, называется InsertMenu . Эта функция может добавить элемент в середину меню, сдвинув вниз уже существующие элементы.
Приведем прототип функции InsertMenu:
BOOL WINAPI InsertMenu(HMENU hmenu, UINT idItem, UINT fuFlags, UINT idNewItem, LPCSTR lpszNewItem);
Параметры этой функции аналогичны параметрам функции AppendMenu, за исключением параметров idItem и fuFlags.
Параметр idItem определяет элемент меню, перед которым должен быть вставлен новый элемент. Интерпретация этого параметра зависит от значения параметра fuFlags.
В дополнение к возможным значениям параметра fuFlags, описанным нами для функции AppendMenu, вместе с функцией InsertMenu вы можете использовать еще два - MF_BYCOMMAND и MF_BYPOSITION.
Если указан флаг MF_BYCOMMAND, параметр idItem определяет идентификатор элемента меню, перед которым будет вставлен новый элемент.
Если указан флаг MF_BYPOSITION, параметр idItem определяет порядковый номер элемента меню, перед которым будет вставлен новый элемент. Для того чтобы добавить элемент в конец меню, для параметра idItem можно указать значение -1.
После внесения всех изменений в меню приложение должно вызвать функцию DrawMenuBar :
void WINAPI DrawMenuBar(HWND hwnd);
Эта функция перерисовывает полосу меню для указанного параметром hwnd окна. В качестве параметра функции следует передать идентификатор окна, создавшего меню.
Дополнительная память в структуре класса окна
При регистрации класса окна вы можете в поле cbClsExtra структуры WNDCLASS указать размер дополнительной области памяти, которая будет зарезервирована в структуре данных, описывающей класс окна. В эту область можно записать данные, предназначенные для использования всеми окнами, создаваемыми на базе класса.
Для работы с этой дополнительной памятью предназначены функции SetClassWord, SetClassLong, GetClassWord, GetClassLong.
Функция SetClassWord устанавливает в структуре, описывающей класс для окна hwnd, новое значение wNewWord, при этом смещение устанавливаемого слова определяется параметром offset:
WORD WINAPI SetClassWord(HWND hwnd, int offset, WORD wNewWord);
Для параметра offset вы должны использовать значения от нуля до указанного в поле cbClsExtra минус 2 или следующие значения:
Значение | Описание |
GCW_HBRBACKGROUND | Идентификатор кисти для закрашивания фона окна |
GCW_HCURSOR | Идентификатор курсора |
GCW_HICON | Идентификатор пиктограммы |
GCW_STYLE | Стили окна |
С помощью перечисленных выше четырех значений вы можете изменить характеристики класса окна. Эти изменения будут учитываться только для вновь создаваемых окон.
В случае ошибки функция SetClassWord возвращает нулевое значение.
Функция GetClassWord позволяет вам прочитать содержимое слова дополнительной области памяти со смещением offset:
WORD WINAPI GetClassWord(HWND hwnd, int offset);
Для этой функции вы можете использовать те же значения, что и для предыдущей, плюс еще два:
Значение | Описание |
GCW_CBCSLEXTRA | Размер дополнительной области памяти в структуре класса окна |
GCW_CBWNDEXTRA | Размер дополнительной области памяти в структуре окна |
GCW_HBRBACKGROUND | Идентификатор кисти для закрашивания фона окна |
GCW_HCURSOR | Идентификатор курсора |
GCW_HICON | Идентификатор пиктограммы |
GCW_STYLE | Стили окна |
Функция GetClassWord возвращает значение указанного слова из структуры класса окна или нулевое значение при ошибке.
Функция SetClassLong аналогична функции SetClassWord, но работает с двойными словами:
LONG WINAPI SetClassLong(HWND hwnd, int offset, LONG nVal);
Для параметра offset дополнительно можно указать значение GCL_WNDPROC, при этом функция заменит адрес функции окна. Мы пользовались этим приемом в приложении SMARTPAD, перехватывая управление у стандартной функции окна органа управления класса "edit".
В случае ошибки функция SetClassLong возвращает нулевое значение.
С помощью функции GetClassLong вы можете получить из структуры класса окна значение двойного слова, расположенного со смещением offset:
LONG WINAPI GetClassLong(HWND hwnd, int offset);
Для этой функции можно указать положительное смещение или одну из двух констант - GCL_WNDPROC и GCL_MENUNAME. В первом случае функция GetClassLong возвратит адрес функции окна для данного класса, во втором - указатель на строку имени меню, указанного при регистрации класса.
Дополнительная память в структуре окна
При регистрации класса окна функцией RegisterClass вы можете в поле cbWndExtra структуры WNDCLASS указать размер дополнительной области памяти, которая будет зарезервирована в структуре, описывающей каждое окно, создаваемое на базе данного класса.
Для работы с этой дополнительной памятью предназначены функции SetWindowWord, SetWindowLong, GetWindowWord, GetWindowLong.
Функция SetWindowWord устанавливает в структуре, описывающей окно hwnd, новое значение wNewWord, при этом смещение устанавливаемого слова определяется параметром offset:
WORD WINAPI SetWindowWord(HWND hwnd, int offset, WORD wNewWord);
Для параметра offset вы должны использовать значения от нуля до указанного в поле cbClsExtra минус 2 или следующие значения:
Значение | Описание |
GWW_HINSTANCE | Идентификатор приложения, владеющего данным окном |
GWW_ID | Идентификатор дочернего окна |
В случае ошибки функция SetWindowWord возвращает нулевое значение.
Функция GetWindowWord позволяет вам прочитать содержимое слова дополнительной области памяти в структуре окна со смещением offset:
WORD WINAPI GetWIndowWord(HWND hwnd, int offset);
Для этой функции вы можете использовать следующие значения:
Значение | Описание |
GWW_HINSTANCE | Идентификатор приложения, владеющего данным окном |
GWW_HWNDPARENT | Идентификатор родительского окна |
GWW_ID | Идентификатор дочернего окна |
Функция GetWindowWord возвращает значение указанного слова из структуры класса окна или нулевое значение при ошибке.
Функция SetWindowLong аналогична функции SetWindowWord, но работает с двойными словами:
LONG WINAPI SetWindowLong(HWND hwnd, int offset, LONG nVal);
Для параметра offset дополнительно можно указать следующие значения:
Значение | Описание |
GWL_EXSTYLE | Расширенный стиль окна |
GWL_STYLE | Стиль окна |
GWL_WNDPROC | Указатель на функцию окна |
Если параметр hwnd содержит идентификатор диалоговой панели, вы можете использовать еще несколько значений:
Значение | Описание |
DWL_MSGRESULT | Значение, возвращенное при обработке сообщения в функции диалоговой панели |
DWL_USER | Дополнительная информация, имеющая отношение к приложению, такая как идентификаторы или указатели |
DWL_DLGPROC | Указатель на функцию диалоговой панели |
В случае ошибки функция SetWindowLong возвращает нулевое значение.
С помощью функции GetWindowLong вы можете получить из структуры окна значение двойного слова, расположенного со смещением offset:
LONG WINAPI GetWindowLong(HWND hwnd, int offset);
Для этой функции можно указать положительное смещение или одну из констант, описанных выше для функции SetWindowLong .
Файл определения модуля для DLL-библиотеки
Файл определения модуля для DLL-библиотеки отличается от соответствующего файла обычного приложения Windows. В качестве примера приведем образец такого файла:
LIBRARY DLLNAME DESCRIPTION 'DLL-библиотека DLLNAME' EXETYPE windows CODE moveable discardable DATA moveable single HEAPSIZE 1024 EXPORTS DrawBitmap @4 ShowAll HideAll GetMyPool @8 FreeMyPool @9
В файле определения модуля DLL-библиотеки вместо оператора NAME должен находиться оператор LIBRARY , определяющий имя модуля DLL-библиотеки, под которым она будет известна Windows.
Остальные операторы те же, что и для обычного приложения, за исключением того что в файле определения модуля DLL-библиотеки не должно быть оператора STACKSIZE (так как у DLL-библиотеки нет стека).
Оператор CODE используется для определения атрибутов сегмента кода DLL-библиотеки.
Особенностью оператора DATA является использование параметра SINGLE. Так как DLL-библиотека находится в памяти в единственном экземпляре и может иметь только один сегмент данных, для описания сегмента данных библиотеки требуется параметр SINGLE.
Оператор HEAPSIZE определяет начальное значение локальной области памяти, которая будет выделена для DLL-библиотеки функцией LibEntry на этапе инициализации. Если DLL-библиотека не должна иметь локальную область данных, в качестве параметра следует указать нулевое значение:
HEAPSIZE 0
И, наконец, оператор EXPORTS предназначен для перечисления имен и порядковых номеров функций, экспортируемых DLL-библиотекой.
Файлы и обработка сообщений
Так как работа приложений Windows (и самой операционной системы Windows) основана на передаче и обработке сообщений, вся файловая активность приложения также начинается во время обработки какого-либо сообщения. Например, редактор текста загружает редактируемый файл, когда приходит сообщение от строки "Open..." меню "File".
С файлами можно работать по-разному.
Первый способ (используемый программами MS-DOS) заключается в том, чтобы открыть все нужные им файлы в начале работы программы и закрыть их перед завершением работы программы. Если, однако, программа работает с большим количеством файлов, ей может не хватить управляющих блоков DFCB, используемых MS-DOS для каждого открытого файла (см. 1 том "Библиотеки системного программиста", раздел 2.4 "Таблица файлов MS-DOS"). Количество таких блоков определяется оператором "FILES=" в файле config.sys.
Для более экономного использования управляющих блоков DFCB программы могут открывать и закрывать файлы по мере необходимости, сокращая таким образом количество файлов, открытых одновременно. Это второй способ.
Приложения Windows не должны держать файлы открытыми на протяжении всего времени своей работы. Более того, вся обработка файла должна выполнятся во время обработки одного сообщения. То есть обработчик сообщения должен открыть файл, выполнить над ним все нужные операции ввода/вывода, а затем закрыть файл. Есть и еще одно ограничение - необходимо закрыть все файлы перед выводом на экран немодальной или модальной диалоговой панели (в том числе и перед вызовом функции MessageBox).
Смысл этих ограничений заключается в том, что они не позволяют пользователю переключиться на другое приложение, если текущее приложение не закрыло все свои файлы.
Почему нельзя переключаться на другое приложение, не закрыв предварительно все файлы?
Представьте себе, что ваше приложение открыло файл, расположенный на дискете. После этого пользователь переключился на другое приложение, которое также работает с дискетами. Для него потребовалось установить другую дискету и пользователь это сделал. Затем он снова переключился на первое приложение, которое продолжило, например, прерванную операцию записи. Но теперь эта операция будет выполняться с другой дискетой, в результате чего обе дискеты скорее всего придется форматировать.
Поэтому приложения Windows должны придерживаться следующего правила:
Все операции с файлом, такие как открытие файла, чтение/запись и закрытие, должны выполнятся во время обработки одного сообщения. Нельзя открывать файл в обработчике одного сообщения и закрывать его в обработчике другого сообщения.
Фиксирование и расфиксирование блока памяти
Для получения доступа к полученному блоку памяти его необходимо зафиксировать, вызвав функцию GlobalLock :
void FAR* WINAPI GlobalLock(HGLOBAL hglb);
Функция GlobalLock фиксирует блок памяти, идентификатор которого передается ей через параметр hglb и возвращает логический адрес зафиксированного блока или NULL, если указанный блок удален или произошла ошибка.
В защищенном режиме работы Windows продолжает перемещать блоки памяти, зафиксированные функцией GlobalLock, изменяя базовый адрес в локальной таблице дескрипторов. Даже для фиксированных блоков памяти вызов этой функции все же необходим, потому что нужно получить логический адрес блока памяти.
Для каждого блока памяти Windows поддерживает счетчик фиксирований. Этот счетчик увеличивается при вызове функции GlobalLock и уменьшается при расфиксировании блока функцией GlobalUnlock :
BOOL WINAPI GlobalUnlock(HGLOBAL hglb);
Если содержимое счетчика уменьшилось до нуля, функция возвращает значение FALSE. В противном случае возвращается TRUE.
Файл windowsx.h содержит макрокоманды, облегчающие работу с глобальными блоками памяти. Например, макрокоманда GlobalAllocPtr получает блок памяти и фиксирует его:
#define GlobalAllocPtr(flags, cb) \ (GlobalLock(GlobalAlloc((flags), (cb))))
В данном случае идентификатор полученного блока памяти не сохраняется, так как он не нужен. Вы можете освободить блок памяти, зная только его логический адрес (см. следующий раздел).
Фиксирование линейного адреса блока памяти
В некоторых случаях вам необходимо запретить изменение линейного адреса блока памяти, которое выполняется в процессе перемещения. Такое изменение выполняется заменой базового адреса в соответствующем дескрипторе локальной таблицы дескрипторов. В результате при перемещении блока логический адрес остается постоянным (так как при перемещении блока ему не присваивается новый дескриптор, а логический адрес состоит из ссылки на дескриптор, номера запрошенного кольца защиты и смещения), в то время как линейный может изменяться.
Если драйвер какого-либо устройства ввода/вывода работает с линейным адресом буфера, память, отведенная для такого буфера в некоторых случаях должна быть зафиксирована функцией GlobalFix :
void WINAPI GlobalFix(HGLOBAL hglb);
Параметр функции указывает идентификатор фиксируемого блока памяти.
Как только отпадет необходимость в фиксировании блока памяти, его следует расфиксировать, вызвав функцию GlobalUnfix :
void WINAPI GlobalUnfix(HGLOBAL hglb);
Единственный параметр этой функции должен содержать идентификатор блока памяти, который будет расфиксирован.
Фиксирование страниц блока памяти
В расширенном режиме работы операционной системы Windows версии 3.1 используется виртуальная память. Как мы уже говорили, при использовании виртуальной памяти вся глобальная область памяти делится на страницы размером 4 Кбайт. Эти страницы могут располагаться в физической оперативной памяти или на диске в специальном файле виртуальной памяти. Если приложение обращается к странице, которая отсутствует в физической оперативной памяти, она загружается туда из файла виртуальной памяти.
Однако на загрузку страницы памяти из файла требуется значительное время. В некоторых случаях необходимо обеспечить постоянное присутствие блока памяти в физической оперативной памяти.
Фиксирование блока памяти функцией GlobalFix не предотвращает сброс страниц памяти, распределенных блоку, в файл виртуальной памяти, а всего лишь запрещает перемещение блока памяти в линейном адресном пространстве.
Для исключения страниц памяти, принадлежащих указанному блоку памяти, из процесса страничного обмена необходимо использовать функцию GlobalPageLock :
UINT WINAPI GlobalPageLock(HGLOBAL hglb);
Идентификатор блока, для которого необходимо запретить страничный обмен, указывается через параметр hglb.
Операционная система Windows поддерживает счетчик блокирования страничного обмена. Содержимое этого счетчика увеличивается на единицу при каждом вызове функции GlobalPageLock.
Функция GlobalPageLock возвращает новое значение счетчика или ноль при ошибке.
Как только надобность в блокировке страничного обмена отпадает, следует вызвать функцию GlobalPageUnlock :
UINT WINAPI GlobalPageUnlock(HGLOBAL hglb);
Эта функция разрешает страничный обмен для блока памяти, заданного параметром hglb. Функция возвращает текущее значение счетчика или ноль при ошибке.
Фильтр WH_CALLWNDPROC
Приведем прототип функции фильтра типа WH_CALLWNDPROC (для функции можно использовать любое имя):
LRESULT CALLBACK CallWndProc( int code, // код действия WPARAM wParam, // флаг текущей задачи LPARAM lParam) // адрес структуры с сообщением
Функция CallWndProc обязательно должна находиться DLL-библиотеке. Она вызывается операционной системой Windows после вызова функции SendMessage, но перед тем, как сообщение попадет в функцию окна, для которой оно предназначено. Фильтр может изменить любые параметры сообщения или его код.
Если код действия (параметр code) меньше нуля, функция фильтра должна вызвать функцию CallNextHookEx, не изменяя перехваченное сообщение.
Если сообщение послано текущей задачей, значение параметра wParam отлично от нуля, в противном случае оно равно NULL.
Параметр lParam содержит указатель на структуру, которая описывает перехваченное сообщение (эта структура на описана в файле windows.h):
struct _Msg { LPARAM lParam; // параметр lParam сообщения WPARAM wParam; // параметр wParam сообщения UINT uMsg; // код сообщения HWND hWnd; // идентификатор окна, для которого }; // предназначено сообщение
Функция фильтра должна всегда возвращать нулевое значение.
Заметим, что после установки фильтра WH_CALLWNDPROC производительность операционной системы Windows заметно уменьшится, поэтому использование такого фильтра оправдано только при отладке приложения или для создания приложений, специально предназначенных для отладки других приложений Windows.
Фильтр WH_CBT
Большинство приложений, созданных фирмой Microsoft для своей операционной системы Windows, имеют встроенные обучающие системы, предназначенные для того, чтобы пользователь мог быстрее освоить работу с приложением. Например, текстовый процессор Microsoft Word for Windows версий 2.0 и 6.0 имеет очень удобную и легкую в использовании обучающую систему. Эта обучающая система не только рассказывает пользователю о том, как надо работать с текстовым процессором, но и, что очень важно, дает ему возможность попробовать выполнить те или иные действия самостоятельно. Когда пользователь пытается работать самостоятельно, все выглядит так, как будто он имеет дело с настоящим текстовым процессором, но при этом его действия ограничиваются обучающей системой.
Для облегчения создания подобных обучающих программ приложения могут установить фильтр WH_CBT, специально предназначенный для организации контроля за действиями пользователя со стороны обучающих программ. Мы не беремся утверждать, что обучающая программа для текстового процессора Microsoft Word for Windows построена с использованием этого фильтра, однако, вполне вероятно, что это так и есть.
Приведем прототип функции фильтра WH_CBT:
LRESULT CALLBACK CBTProc( int code, // код действия WPARAM wParam, // назначение зависит от кода действия LPARAM lParam); // назначение зависит от кода действия
Данная функция должна располагаться в DLL-библиотеке. Она вызывается операционной системой перед выполнением таких действий, как создание, активизация, удаление, перемещение и изменение размеров окна, перед удалением сообщений мыши или клавиатурных сообщений из системной очереди сообщений, перед передачей фокуса ввода и т. д.
Фильтр может разрешить или запретить выполнение перечисленных выше операций, возвращая соответствующее значение.
В зависимости от параметра code меняется назначение параметров wParam и lParam. Перечислим возможные значения для параметра code и кратко опишем назначение остальных параметров функции фильтра WH_CBT.
Значение параметра code | Описание | |
HCBT_ACTIVATE | Фильтр вызывается перед активизацией любого окна. Если функция возвращает FALSE, окно активизируется, если TRUE - нет. Параметр wParam содержит идентификатор активизирующегося окна. Параметр lParam содержит указатель на структуру CBTACTIVATESTRUCT :
typedef struct tagCBTACTIVATESTRUCT { BOOL fMouse; HWND hWndActive; } CBTACTIVATESTRUCT; Флаг fMouse содержит TRUE, если окно активизируется в результате щелчка мыши, поле hWndActive содержит идентификатор активного окна | |
HCBT_CREATEWND | Фильтр вызывается перед созданием окна. Если функция фильтра возвращает FALSE, создание окна разрешается, если TRUE - нет. Параметр wParam содержит идентификатор создаваемого окна. Параметр lParam содержит дальний указатель LPCREATESTRUCT на структуру CREATESTRUCT :
typedef struct tagCREATESTRUCT { void FAR* lpCreateParams; HINSTANCE hInstance; HMENU hMenu; HWND hwndParent; int cy; int cx; int y; int x; LONG style; LPCSTR lpszName; LPCSTR lpszClass; DWORD dwExStyle; } CREATESTRUCT; | |
HCBT_DESTROYWND | Фильтр вызывается перед уничтожением окна. Если функция возвращает TRUE, уничтожение окна отменяется. Параметр wParam содержит идентификатор уничтожаемого окна. Параметр lParam содержит 0 | |
HCBT_MINMAX | Фильтр вызывается перед выполнением минимизации или максимизации окна. Если функция фильтра возвращает TRUE, перечисленные операции не выполняются. Параметр wParam содержит идентификатор окна, для которого выполняется минимизация или максимизация. Старшее слово параметра lParam равно нулю, младшее содержит одну из констант, описанных в файле windows.h с префиксом SW_, такие как SW_HIDE, SW_SHOWNORMAL, SW_SHOWMINIMIZED, SW_SHOWNOACTIVATE, SW_SHOW, SW_MINIMIZE, SW_SHOWMINNOACTIVE, SW_SHOWNA, SW_RESTORE | |
HCBT_MOVESIZE | Фильтр вызывается перед перемещением или изменением размера окна. Если функция фильтра возвращает TRUE, выполнение операции отменяется. Параметр wParam содержит идентификатор окна, для которого выполняется перемещение или изменение размера. Параметр lParam содержит дальний указатель на структуру RECT, описывающую прямоугольную область | |
HCBT_SYSCOMMAND | Фильтр вызывается при обработке системной команды. Вызов функции фильтра выполняется из функции DefWindowProc. Если функция фильтра возвращает TRUE, выполнение системной команды отменяется. Параметр wParam содержит код системной команды, такой, как SC_CLOSE, SC_HSCROLL, SC_MINIMIZE и т. д.Если параметр wParam содержит код команды SC_HOTKEY (активизация окна, связанного с клавишей ускоренного выбора, назначенной приложением), младшее слово параметра lParam содержит идентификатор окна, для которого назначена клавиша ускоренного выбора. Для остальных команд значение этого параметра не определено | |
HCBT_CLICKSKIPPED | Фильтр вызывается при удалении сообщения мыши из системной очереди сообщений, при условии, что дополнительно определен фильтр WH_MOUSE. Параметр wParam содержит код сообщения мыши. Параметр lParam содержит дальний указатель на структуру MOUSEHOOKSTRUCT :
typedef struct tagMOUSEHOOKSTRUCT { POINT pt; HWND hwnd; UINT wHitTestCode; DWORD dwExtraInfo; } MOUSEHOOKSTRUCT; | |
HCBT_KEYSKIPPED | Фильтр вызывается при удалении клавиатурного сообщения из системной очереди сообщений, при условии, что дополнительно определен фильтр WH_KEYBOARD. Параметр wParam содержит виртуальный код клавиши.содержимое параметра lParam аналогично содержимому соответствующего параметра клавиатурных сообщений WM_KEYDOWN и WM_KEYUP | |
HCBT_SETFOCUS | Фильтр вызывается при установке фокуса ввода. Если функция фильтра возвращает значение TRUE, фокус не устанавливается. Параметр wParam содержит идентификатор окна, получающего фокус ввода. Младшее слово парамера lParam содержит идентификатор окна, теряющего фокус ввода. Старшее слово парамера lParam всегда содержит NULL | |
HCBT_QS | Фильтр вызывается при удалении из системной очереди сообщения WM_QUEUESYNC, предназначенного для использования обучающим приложением. Это сообщение позволяет ему определить момент завершения того или иного события в главном приложении. Тех, кого интересуют подробности, мы отсылаем к документации, поставляющейся вместе с SDK |
Фильтр WH_DEBUG
Приведем прототип функции фильтра типа WH_DEBUG :
LRESULT CALLBACK DebugProc( int code, // код действия WPARAM wParam, // идентификатор задачи LPARAM lParam); // адрес структуры DEBUGHOOKINFO
Фильтр WH_DEBUG предназначен для отладчиков и должен находиться в DLL-библиотеке. Он вызывается перед вызовом других фильтров, установленных функцией SetWindowsHookEx.
Параметр wParam содержит идентификатор задачи, которая установила фильтр.
Параметр lParam содержит дальний указатель на структуру DEBUGHOOKINFO :
typedef struct tagDEBUGHOOKINFO { HMODULE hModuleHook; LPARAM reserved; LPARAM lParam; WPARAM wParam; int code; } DEBUGHOOKINFO;
В этой структуре в поле hModuleHook находится идентификатор модуля, содержащего функцию фильтра, поля lParam, wParam, code содержат параметры, передаваемые функции фильтра. Поле reserved не используется.
Функция фильтра типа WH_DEBUG может предотвратить вызов другого фильтра, для чего она должна возвратить значение TRUE. Если она вернет FALSE, управление будет передано соответствующему фильтру.
Фильтр WH_GETMESSAGE
Фильтр WH_GETMESSAGE получает управление, когда функция GetMessage или PeekMessage возвращают выбранное из очереди сообщение. Функция фильтра должна находиться в DLL-библиотеке.
Приведем прототип функции фильтра типа WH_GETMESSAGE:
LRESULT CALLBACK GetMsgProc( int code, // код действия WPARAM wParam, // не определено LPARAM lParam); // адрес структуры MSG
Параметр lParam содержит указатель на структуру MSG , содержащую перехваченное сообщение:
typedef struct tagMSG { HWND hwnd; UINT message; WPARAM wParam; LPARAM lParam; DWORD time; POINT pt; } MSG;
Функция фильтра может изменить любой параметр сообщения и даже его код. В последнем случае произойдет замена одного сообщения на другое.
Возвращаемое функцией фильтра значение должно всегда равняться нулю.
Фильтр WH_HARDWARE
Фильтр WH_HARDWARE предназначен для перехвата сообщений, поступающих от нестандартных устройств ввода, таких, как устройства перьевого ввода (клавиатура и мышь - это стандартные устройства ввода). Функция фильтра должна находиться в DLL-библиотеке.
Приведем прототип функции фильтра типа WH_HARDWARE:
LRESULT CALLBACK HardwareProc( int code, // код действия WPARAM wParam, // не определено LPARAM lParam); // адрес структуры HARDWAREHOOKSTRUCT
Структура HARDWAREHOOKSTRUCT описана в файле windows.h:
typedef struct tagHARDWAREHOOKSTRUCT { HWND hWnd; UINT wMessage; WPARAM wParam; LPARAM lParam; } HARDWAREHOOKSTRUCT;
В этой структуре поле hWnd содержит идентификатор окна, которому предназначено сообщение, поле wMessage содержит код сообщения, поля wParam и lParam содержат дополнительную информацию, зависящую от кода сообщения.
Фильтр WH_JOURNALPLAYBACK
Фильтр WH_JOURNALPLAYBACK используется для "проигрывания" сообщений, записанных фильтром WH_JOURNALRECORD. Если установлен этот фильтр, обычный ввод при помощи мыши или клавиатуры отключается. Функция фильтра должна находиться в DLL-библиотеке.
Приведем прототип функции фильтра типа WH_JOURNALPLAYBACK:
LRESULT CALLBACK JournalPlaybackProc( int code, // код действия WPARAM wParam, // содержит NULL LPARAM lParam); // адрес структуры EVENTMSG
Перед возвратом управления функция фильтра WH_JOURNALPLAYBACK должна записать по адресу, переданному ей через параметр lParam, структуру данных, записанную ранее функцией фильтра WH_JOURNALRECORD.
Функция фильтра должна вернуть интервал времени ожидания (в машинных тиках) перед тем как Windows начнет обработку сообщения. Если вернуть нулевое значение, ожидание выполняться не будет.
Фильтр WH_JOURNALRECORD
Фильтр WH_JOURNALRECORD вызывается, когда Windows удаляет сообщения из системной очереди. Функция фильтра должна находиться в DLL-библиотеке.
Приведем прототип функции фильтра типа WH_JOURNALRECORD:
LRESULT CALLBACK JournalRecordProc( int code, // код действия WPARAM wParam, // содержит NULL LPARAM lParam); // адрес структуры EVENTMSG
Данный фильтр предназначен для записи перехваченных сообщений в память или файл. Он не может изменять или удалять сообщения из системной очереди.
Параметр code может принимать одно из трех значений:
Значение параметра code | Описание |
HC_ACTION | Windows извлекает сообщение из системной очереди |
HC_SYSMODALON | Windows выводит на экран системную модальную диалоговую панель. Начиная с этого момента приложение должно остановить запись сообщений |
HC_SYSMODALOFF | Windows удаляет системную модальную диалоговую панель, так что теперь можно продолжить запись сообщений |
Структура EVENTMSG описана в файле windows.h:
typedef struct tagEVENTMSG { UINT message; UINT paramL; UINT paramH; DWORD time; } EVENTMSG;
Фильтр WH_KEYBOARD
Фильтр WH_KEYBOARD получает управление, когда функции GetMessage или PeekMessage возвращают сообщения WM_KEYDOWN, WM_KEYUP, WM_SYSKEYDOWN, WM_SYSKEYUP или WM_CHAR. Функция фильтра должна находиться в DLL-библиотеке.
Приведем прототип функции фильтра типа WH_KEYBOARD:
LRESULT CALLBACK KeyboardProc( int code, // код действия WPARAM wParam, // код виртуальной клавиши LPARAM lParam); // дополнительная информация
Параметр code может принимать значения HC_ACTION и HC_NOREMOVE. В первом случае перехваченное сообщение после обработки будет удалено из системной очереди сообщений, во втором - останется в этой очереди (т. к. оно было выбрано при помощи функции PeekMessage с параметром PM_NOREMOVE). Если сообщение останется в очереди, таблица состояния клавиатуры, которую можно получить при помощи функции GetKeyboardState, может не отражать состояние клавиатуры на момент выборки сообщения.
Параметры wParam и lParam содержат ту же самую информацию, что и соответствующие параметры клавиатурных сообщений WM_KEYDOWN, WM_KEYUP, WM_SYSKEYDOWN, WM_SYSKEYUP, WM_CHAR.
Параметр wParam содержит код виртуальной клавиши, соответствующей нажатой физической клавише. Именно этот параметр используется приложениями для идентификации нажатой клавиши.
Приведем описание отдельных бит парамера lParam, через который передается дополнительная информация о клавиатурном сообщении .
Бит | Описание |
0-15 | Счетчик повторов. Если нажать клавишу и держать ее в нажатом состоянии, несколько сообщений WM_KEYDOWN и WM_SYSKEYDOWN будут слиты в одно. Количество объединенных таким образом сообщений |
16-23 | OEM скан-код клавиши. Изготовители аппаратуры (OEM - Original Equipment Manufacturer) могут заложить в своей клавиатуре различное соответствие скан-кодов и обозначений клавиш. Скан-код генерируется клавиатурным контроллером. Это тот самый код, который получают в регистре AH программы MS-DOS, вызывая прерывание INT16h |
24 | Флаг расширенной клавиатуры. Этот бит установлен в 1, если сообщение соответствует клавише, имеющейся только на расширенной 101- или 102-клавишной клавиатуре. Это может быть одна из следующих клавиш: <Home>, <End>, <PgUp>, <PgDn>, <Insert>, <Delete>, клавиши дополнительной клавиатуры. |
25-26 | Не используются |
27-28 | Зарезервированы для использования Windows |
29 | Код контекста. Этот бит равен 1, если сообщение соответствует комбинации клавиши <Alt> с любой другой, и 0 в противном случае |
30 | Предыдущее состояние клавиши. Если перед приходом сообщения клавиша, соответствующая сообщению, была в нажатом состоянии, этот бит равен 1. В противном случае бит равен 0 |
31 | Флаг изменения состояния клавиши (transition state). Если клавиша была нажата, бит равен 0, если отпущена - 1 |
При помощи этого фильтра приложение может удалить клавиатурное сообщение. Для удаления сообщения функция фильтра должна вернуть значение 1. Если же возвращенное значение будет равно 0, сообщение будет обработано системой.
Фильтр WH_MOUSE
Фильтр WH_MOUSE получает управление, когда функции GetMessage или PeekMessage возвращают сообщения мыши. Функция фильтра должна находиться в DLL-библиотеке.
Приведем прототип функции фильтра типа WH_MOUSE:
LRESULT CALLBACK MouseProc( int code, // код действия WPARAM wParam, // код сообщения LPARAM lParam); // указатель на структуру MOUSEHOOKSTRUCT
Так же, как и для предыдущего фильтра, параметр code может принимать значения HC_ACTION и HC_NOREMOVE.
Параметр wParam содержит код сообщения, поступившего от мыши.
Через параметр lParam передается указатель на структуру MOUSEHOOKSTRUCT :
typedef struct tagMOUSEHOOKSTRUCT { POINT pt; HWND hwnd; UINT wHitTestCode; DWORD dwExtraInfo; } MOUSEHOOKSTRUCT;
Эта структура содержит дополнительную информацию, имеющую отношение к сообщению.
Поле pt является структурой типа POINT, в которой находятся экранные x- и y-координаты курсора мыши. Поле hwnd содержит идентификатор окна, в функцию которого будет направлено сообщение. В поле wHitTestCode находится код тестирования, определяющий область окна, соответствующую расположению курсора мыши на момент генерации сообщения. Поле dwExtraInfo содержит дополнительную информацию, которую можно получить с помощью функции GetMessageExtraInfo :
LPARAM WINAPI GetMessageExtraInfo(void);
Назначение отдельных бит возвращаемого этой функцией значения зависит от аппаратного обеспечения.
Фильтр WH_MSGFILTER
Фильтр WH_MSGFILTER получает управление, когда диалоговая панель или меню выбирает сообщение. Функции фильтра разрешается изменить или обработать перехваченное сообщение. Фильтр WH_MSGFILTER может находиться в приложении или в DLL-библиотеке, его можно определить как для отдельной задачи, так и для всей системы в целом.
Приведем прототип функции фильтра типа WH_MSGFILTER:
LRESULT CALLBACK MessageProc( int code, // код действия WPARAM wParam, // не определено LPARAM lParam); // указатель на структуру MSG
Параметр code может принимать значения MSGF_DIALOGBOX (ввод в диалоговой панели), MSGF_SCROLLBAR (ввод в области полосы просмотра), MSGF_MENU (ввод в меню) или MSGF_NEXTWINDOW (пользователь активизирует следующее окно, нажимая комбинацию клавиш <Alt + Tab> или <Alt + Esc>).
Если фильтр обрабатывает сообщение, функция фильтра должна вернуть ненулевое значение, если нет - нулевое.
Фильтр WH_SHELL
Фильтр WH_SHELL предназначен для приложений-оболочек (shell application) и позволяет получать необходимые извещения от операционной системы Windows.
Приведем прототип функции фильтра типа WH_SHELL:
LRESULT CALLBACK ShellProc( int code, // код действия WPARAM wParam, // флаг текущей задачи LPARAM lParam); // не определено
Параметр code может принимать одно из следующих значений:
Значение параметра code | Описание |
HSHELL_ACTIVATESHELLWINDOW | Оболочка должна активизировать свое главное окно |
HSHELL_WINDOWCREATED | Создано окно верхнего уровня, которое не принадлежит ни одному приложению. Это окно будет существовать во время работы функции фильтра. Идентификатор созданного окна передается через параметр wParam |
HSHELL_WINDOWDESTROYED | Описанное выше окно верхнего уровня будет уничтожено после завершения работы функции фильтра. Идентификатор уничтожаемого окна передается через параметр wParam |
Функция фильтра должна вернуть нулевое значение.
Фильтр WH_SYSMSGFILTER
Фильтр WH_SYSMSGFILTER получает управление, когда диалоговая панель или меню выбирает сообщение. Функции фильтра разрешается изменить или обработать перехваченное сообщение. Фильтр WH_SYSMSGFILTER может находиться только DLL-библиотеке. Назначение этого фильтра аналогично назначению фильтра WH_MSGFILTER, но он может быть установлен только для всей системы в целом.
Приведем прототип функции фильтра типа WH_SYSMSGFILTER:
LRESULT CALLBACK SysMsgProc( int code, // код действия WPARAM wParam, // не определено LPARAM lParam); // указатель на структуру MSG
Если фильтр обрабатывает сообщение, функция фильтра должна вернуть ненулевое значение, если нет - нулевое.
Приложение может установить одновременно фильтры WH_SYSMSGFILTER и WH_MSGFILTER, в этом случае вначале вызывается фильтр WH_SYSMSGFILTER, а затем - фильтр WH_MSGFILTER. Если же функция фильтра WH_SYSMSGFILTER возвращает ненулевое значение, фильтр WH_MSGFILTER не вызывается.
Фильтры
В операционной системе Windows, в основе которой лежит механизм обмена сообщениями, имеется очень мощное средство, позволяющее организовать фильтрацию или перехват любых сообщений. Причем можно фильтровать сообщения, предназначенные как для некоторых, так и для всех приложений. Вы можете создать свой фильтр, получающий управление до того, как то или иное сообщение достигнет очереди сообщений или перед вызовом соответствующей функции окна. Фильтр может оставить перехваченное сообщение без изменения, изменить его или даже изъять.
Можно встроить несколько однотипных фильтров. При этом организуется цепочка фильтров. Первым получит управление тот фильтр, который был встроен в последнюю очередь.
Для чего можно использовать фильтры?
С помощью фильтров вы можете перехватить сообщения, поступающие в любые диалоговые панели, полосы просмотра, меню и т. п., причем как для отдельного приложения, так и для всех приложений сразу. Можно "отобрать" сообщения у функций GetMessage, PeekMessage, SendMessage. Можно записывать и затем проигрывать события, связанные с перемещением мыши и использованием клавиатуры. Фильтры облегчают создание обучающих приложений, которые работают совместно с обычными приложениями Windows в режиме обучения. Обучающие приложения может контролировать действия пользователя, когда он работает с приложением.
В качестве примера в разделе "Приложение WINHOOK" мы расскажем вам о том, как можно с помощью фильтров переключать раскладку клавиатуры. Мы приведем исходные тексты несложного руссификатора клавиатуры, позволяющего использовать в операционной системе Windows символы кириллицы.
Флаги состояния элемента меню
Функция GetMenuState возвращает флаги состояния для заданного элемента меню:
UINT WINAPI GetMenuState(HMENU hmenu, UINT idItem, UINT fuFlags);
Параметр hmenu определяет меню, для которого будет выполняться операция.
Параметр idItem определяет элемент меню, для которого будут получены флаги состояния. Интерпретация этого параметра зависит от значения параметра fuFlags.
Если в параметре fuFlags указан флаг MF_BYCOMMAND, параметр idItem определяет идентификатор строки меню, для которого выполняется операция. Если указан флаг MF_BYPOSITION, параметр idItem определяет порядковый номер этой строки.
Для временного меню старший байт возвращаемого функцией значения содержит количество элементов во временном меню, а младший - набор флагов, описывающих временное меню. Для меню верхнего уровня возвращаемое значение является набором флагов, описывающих указанный элемент меню:
Флаг | Описание |
MF_BITMAP | Для изображения строки меню используется графическое изображение bitmap |
MF_CHECKED | Строка меню отмечена галочкой"" |
MF_DISABLED | Строка меню находится в неактивном состоянии |
MF_ENABLED | Строка меню разблокирована и отображается в нормальном виде. Этому состоянию соответствует возвращаемое функцией GetMenuState значение, равное нулю |
MF_GRAYED | Строка меню отображается серым цветом и находится в неактивном состоянии. Такую строку нельзя выбрать |
MF_MENUBREAK | Для меню верхнего уровня элемент меню выводится с новой строки. Для временного меню элемент выводится в новом столбце |
MF_MENUBARBREAK | Аналогично MF_MENUBREAK, но дополнительно столбец отделен вертикальной линией |
MF_SEPARATOR | Строка является горизонтальной разделительной линией во временных меню |
MF_UNCHECKED | Строка не отмечена галочкой "" |
Если указанный элемент меню не существует, функция GetMenuState возвращает значение -1.
Flags
Поле Flags позволяет задать различные режимы выбора файла, влияющие на внешний вид диалоговой панели. Приведем список флагов, комбинации которых можно использовать для заполнения этого поля.
Флаг | Описание |
OFN_ALLOWMULTISELECT | Разрешается выбор нескольких файлов одновременно. Если указан этот флаг, после выбора поле lpstrFile будет указывать на буфер, заполненный именами выбранных файлов (или путями к выбранным файлам), разделенными пробелом |
OFN_CREATEPROMPT | При использовании этого флага если указанный файл не существует, создается диалоговая панель, в которой предлагается создать файл. Этот флаг устанавливается автоматически при использовании флагов OFN_PATHMUSTEXIST и OFN_FILEMUSTEXIST |
OFN_ENABLEHOOK | Разрешается использовать функцию фильтра, адрес которой указан в поле lpfnHook |
OFN_ENABLETEMPLATE | Если указан этот флаг, для создания диалоговой панели Windows будет использовать шаблон, определяемый содержимым полей hInstance и lpTemplateName |
OFN_ENABLETEMPLATEHANDLE | При использовании этого флага поле hInstance используется для идентификации блока памяти, содержащий предварительно загруженный шаблон диалоговой панели. В этом случае содержимое поля lpTemplateName игнорируется |
OFN_EXTENSIONDIFFERENT | Устанавливается после возвращения из функции и указывает, что расширение возвращенного имени файла отличается от заданного в поле lpstrDefExt. Этот флаг не устанавливается, если перед вызовом функции в поле lpstrDefExt было записано значение NULL, или если файл не имеет расширения имени |
OFN_FILEMUSTEXIST | Можно выбирать только имена тех файлов, которые существуют. Если в поле "File Name" диалоговой панели набрать имя несуществующего файла, на экране появится диалоговая панель с предупреждающим сообщением |
OFN_HIDEREADONLY | Убрать переключатель "Read Only" |
OFN_NOCHANGEDIR | Для выбора используется каталог, который был текущим при вызове функции |
OFN_NOREADONLYRETURN | Выбранные файлы не могут иметь атрибут "только чтение" или располагаться в защищенном от записи каталоге |
OFN_NOTESTFILECREATE | Перед завершением работы диалоговой панели создание файла не выполняется. Не выполняются и проверки на переполнение диска, защиту записи или наличие доступа в сети |
OFN_NOVALIDATE | В возвращаемом имени файла могут присутствовать неразрешенные символы |
OFN_OVERWRITEPROMPT | Используется для диалоговой панели "Save As...". Если выбранный файл существует, на экран выводится диалоговая панель с предупреждением |
OFN_PATHMUSTEXIST | Можно вводить только существующие пути к файлам |
OFN_READONLY | После вызова функции переключатель "Read Only" будет находиться во включенном состоянии |
OFN_SHAREWARE | Флаг устанавливается после возвращения из функции и указывает, что при вызове функции OpenFile произошла ошибка при совместном доступе к файлу в сети |
OFN_SHOWHELP | Если указан этот флаг, в диалоговой панели будет создана кнопка "Help". Если указан этот флаг, поле hwndOwner не должно содержать значение NULL |
Функции для работы с меню
В программном интерфейсе операционной системы Windows есть функции, специально предназначенные для работы с меню. С помощью этих функций приложение может создавать меню (даже не имея его шаблона), добавлять или удалять строки или временные меню, активизировать или блокировать отдельные строки меню, изменять состояние строк (отмеченное или не отмеченное) и т. д.
В этом разделе мы расскажем вам о том, как пользоваться такими функциями.
Функции фильтра
Функция фильтра должна иметь следующий прототип:
LRESULT CALLBACK HookProc( int code, WPARAM wParam, LPARAM lParam)
Имя функции фильтра может быть любым, так как при установке фильтра вы должны указать функции SetWindowsHookEx указатель на функцию фильтра.
Параметр code может определять действия, выполняемые функцией фильтра. В операционной системе Windows версии 3.0 этот параметр мог принимать отрицательные значения. В этом случае функция фильтра должна была передавать управление следующему в цепочке фильтру при помощи функции DefHookProc. Операционная система Windows версии 3.1 никогда не вызывает функцию фильтра с отрицательным значением параметра code.
Назначение параметров wParam и lParam зависит от типа фильтра, задаваемого при помощи параметра idHook функции SetWindowsHookEx. Мы не будем подробно описывать все возможные типы фильтров, так как это займет очень много времени. При необходимости вы сможете найти полное описание в документации, поставляющейся вместе с SDK. Рассмотрим в деталях только самые интересные, на наш взгляд, типы фильтров.
Функции Windows для работы с файлами
С помощью функций, входящих в состав программного интерфейса операционной системы Windows вы можете выполнять над файлами те же операции, что и в среде MS-DOS. Это открытие, закрытие, чтение, запись, позиционирование, удаление и т. п.
Функция LibEntry
До тех пор, пока ни одно из приложений не затребовало функцию из DLL-библиотеки, сама библиотека находится в файле на диске. В оперативной памяти ее нет. Однако как только приложение затребует функцию из DLL-библиотеки или загрузит библиотеку в память явным образом, начнется процесс инициализации библиотеки. Этот процесс выполняется только один раз, так как в памяти может находиться только одна копия DLL-библиотеки.
В процессе инициализации после загрузки библиотеки в память Windows вызывает функцию LibEntry , которая должна быть определена в каждой DLL-библиотеке. Можно считать, что функция LibEntry является точкой входа библиотеки, получающей управление при загрузке библиотеки в память.
Задачей функции LibEntry является инициализация локальной области памяти, если она определена для DLL-библиотеки.
Функция LibEntry должна быть дальней функцией, составленной на языке ассемблера, так как она получает параметры через регистры процессора. Перечислим и опишем параметры, передаваемые функции LibEntry при загрузке DLL-библиотеки в память.
Регистры | Описание |
DS | Селектор, указывающий на сегмент данных DLL-библиотеки. Отметим, что хотя сразу после загрузки DLL-библиотека имеет собственный сегмент данных, в нем не определена область локальных данных. Для определения области локальных данных функция LibEntry должна вызвать функцию LocalInit |
DI | Идентификатор DLL-библиотеки, присвоенный ей операционной системой Windows после загрузки. По своему назначению аналогичен идентификатору приложения hInstance, передаваемому обычному приложению через соответствующий параметр функции WinMain. Идентификатор DLL-библиотеки используется функциями библиотеки при создании таких, например, объектов, как окна (если окно создается функцией, расположенной в DLL-библиотеке, при его создании следует использовать не идентификатор приложения hInstance, вызвавшего функцию, а идентификатор DLL-библиотеки) |
CX | Требуемый размер локальной области данных, указанный в операторе HEAPSIZE файла определения модуля DLL-библиотеки. При вызове функции инициализации локальной области памяти LocalInit в качестве значения параметра uStartAddr следует использовать нуль, а в качестве значения параметра uEndAddr - содержимое регистра CX |
ES:SI | Дальний указатель на строку параметров, которая может быть указана при явной загрузке DLL-библиотеки (позже мы рассмотрим различные способы загрузки DLL-библиотеки). Обычно этот параметр не используется |
Вам не надо определять функцию LibEntry самостоятельно, так как при создании файла DLL-библиотеки редактор связей, входящий в систему разработки, включит уже имеющийся в стандартной библиотеке модуль. Этот стандартный модуль выполняет всю необходимую работу по инициализации локальной области памяти DLL-библиотеки (с помощью функции LocalInit) и затем вызывает функцию LibMain, которая будет описана в следующем разделе.
Иногда для DLL-библиотеки может потребоваться нестандартная инициализация. Только в этом случае вам придется разработать функцию LibEntry самостоятельно. За основу вы можете взять файл libentry.asm из SDK, который содержит исходный текст упомянутой выше функции.
Функция LibMain
Функция LibMain должна присутствовать в каждой стандартной DLL-библиотеке. Эту функцию вам надо определить самостоятельно, причем вы можете воспользоваться языком программирования С.
По своему назначению функция LibMain напоминает функцию WinMain обычного приложения Windows. Функция WinMain получает управление при запуске приложения, а функция LibMain - при загрузке DLL-библиотеки в память. Так же как и функция WinMain, функция LibMain имеет параметры, которые можно использовать для инициализации библиотеки.
Приведем прототип функции LibMain :
int FAR PASCAL LibMain(HINSTANCE hInstance, WORD wDataSegment, WORD wHeapSize, LPSTR lpszCmdLine);
Параметр hInstance при вызове функции содержит идентификатор DLL-библиотеки. Это ни что иное, как содержимое регистра DI перед вызовом функции LibEntry.
Через параметр wDataSegment передается селектор, соответствующий сегменту данных DLL-библиотеки. Если DLL-библиотека не имеет сегмента данных, этот параметр содержит идентификатор DLL-библиотеки (точнее, идентификатор модуля DLL-библиотеки). Значение параметра wDataSegment соответствует содержимому регистра DS перед вызовом функции LibEntry.
Параметр wHeapSize содержит размер в байтах локальной области данных DLL-библиотеки. Если DLL-библиотека не имеет сегмента данных, в этом параметре находится нулевое значение.
И, наконец, через параметр lpszCmdLine передается указатель на командную строку, которую можно передать DLL-библиотеке при ее явной загрузке в память.
Если инициализация DLL-библиотеки выполнена успешно, функция LibMain должна возвратить ненулевое значение. Если в процессе инициализации произошла ошибка, следует возвратить нуль. В этом случае функция LibEntry также возвратит нуль. Это приведет к тому, что Windows выгрузит библиотеку из памяти.
Разрабатывая процедуру инициализации DLL-библиотеки, учтите, что функция LibMain вызывается только один раз, во время загрузки библиотеки в память. Не следует думать, что для каждого приложения, использующего одну и ту же DLL-библиотеку, будет вызвана функция LibMain.
При попытке повторной загрузки уже загруженной ранее DLL-библиотеки будет увеличено содержимое счетчика использования библиотеки, но и только.
Если для каждого приложения в локальной области данных DLL-библиотеки необходимо выделять отдельные структуры данных, вам придется разработать собственную процедуру регистрации приложения.
Например, можно создать специальную функцию регистрации LibRegister, которая возвращает приложению указатель на выделенную для него в локальной области структуру данных. Вызывая функции из DLL-библиотеки, приложение передает им в качестве, например, первого параметра, этот указатель.
Перед завершением работы приложение должно вызвать созданную вами функцию, освобождающую выделенную структуру данных. Она может называться, например, LibUnregister.
Если функция LibMain заказывает блоки памяти из глобальной области данных, следует использовать флаг GMEM_SHARE. Такие блоки памяти будут принадлежать создавшей их DLL-библиотеке. Освобождение заказанных глобальных блоков памяти можно выполнить в функции WEP, получающей управление при выгрузке DLL-библиотеки из памяти.
Приведем пример простейшего варианта функции LibMain:
int FAR PASCAL LibMain(HINSTANCE hInstance, WORD wDataSegment, WORD wHeapSize, LPSTR lpszCmdLine) { if(wHeapSize != 0) UnlockData(0); return 1; }
Эта функция проверяет размер локальной области данных, и если эта область данных существует, она расфиксируется при помощи макрокоманды UnlockData. После этого возвращается признак успешной инициализации.
Функция malloc и farmalloc
Для получения блока памяти приложения Windows могут использовать функцию malloc.
void *malloc(size_t size);
Выделенный блок памяти будет автоматически зафиксирован.
С помощью этой функции вы, однако, не сможете получить блок памяти размером большим, чем 64 Кбайт, так как тип size_t отображается на 16-разрядное целое.
Блоки памяти, полученные с помощью функции malloc, необходимо освобождать функцией free , указывая ей адрес освобождаемого блока памяти:
void free(void *block);
Если вам надо получить большой блок памяти, можно воспользоваться функцией farmalloc , которая входит в состав стандартной библиотеки Borland C++:
void far *farmalloc(unsigned long nbytes);
Так как параметр этой функции, определяющий размер заказываемого блока памяти, имеет тип unsigned long, вы можете запрашивать блоки памяти очень большого размера. Возвращаемое этой функцией значение следует преобразовывать в указатель типа huge. В этом случае с использованием такого указателя вы сможете напрямую адресовать любой участок большого блока памяти.
Освобождение блока памяти, полученного при помощи функции farmalloc, должно выполняться функцией farfree :
void farfree(void far *block);
В некоторых случаях использование функции farmalloc может оказаться предпочтительней, чем использование функции GlobalAlloc.
Как вы знаете, локальная таблица дескрипторов может содержать не более чем 8192 дескриптора. Значительная часть локальной таблицы дескрипторов, которая в Windows версии 3.1 одна на все приложения, может быть занята самой операционной системой или другими приложениями. Если ваше приложение заказывает большое количество глобальных блоков памяти, используя функцию GlobalAlloc, таблица дескрипторов может быстро переполниться, так как для каждого блока памяти в локальной дескрипторной таблице создается отдельный дескриптор.
Для функции farmalloc используется другой метод. В локальной таблице дескрипторов создается один дескриптор, который адресует область памяти, используемую одновременно для выделения нескольких блоков памяти. Например, если вы заказали блок памяти размером 100 Кбайт, а затем из них освободили 50 Кбайт, при повторном запросе памяти она будет выделена из заказанного ранее блока без создания нового дескриптора. Таким образом, функция farmalloc позволяет экономить свободное пространство в локальной таблице дескрипторов.
Функция WEP
DLL-библиотека в любой момент времени может быть выгружена из памяти. В этом случае Windows перед выгрузкой вызывает функцию WEP . Эта функция, как и функция LibMain, вызывается только один раз. Она может быть использована для уничтожения структур данных и освобождения блоков памяти, заказанных при инициализации DLL-библиотеки.
Приведем прототип функции WEP (при использовании системы разработки Borland Turbo C++ for Windows):
int FAR PASCAL WEP(int bSystemExit);
Параметр bSystemExit может принимать значения WEP_FREE_DLL и WEP_SYSTEM_EXIT . Этот параметр указывает причину выгрузки DLL-библиотеки из памяти. В первом случае выгрузка выполняется потому, что либо функциями библиотеки не пользуется ни одно приложение, либо одно из приложений выдало запрос на выгрузку. Если же параметр имеет значение WEP_SYSTEM_EXIT, выгрузка библиотеки происходит из-за завершения работы операционной системы Windows.
Функция WEP должна всегда возвращать значение 1.
Вам не обязательно самостоятельно определять функцию WEP. Так же как и функция LibEntry, функция WEP добавляется в DLL-библиотеку транслятором. Вы, однако, можете определить собственную функцию WEP, если перед выгрузкой DLL-библиотеки требуется освободить заказанные ранее блоки памяти.
Приведем пример простейшей функции WEP:
int FAR PASCAL WEP(int bSystemExit) { return 1; }
В операционной системе Windows версии 3.0 на использование функции WEP накладывались серьезные ограничения, которые делали ее практически бесполезной. Например, размер стека, используемого для работы этой функции, составлял всего 256 байт, чего совершенно недостаточно для вызова функций программного интерфейса Windows. Есть и другие проблемы, однако мы не будем их затрагивать из-за того, что версия 3.0 уже сильно устарела, а в версии 3.1 все эти ограничения сняты. Поэтому вы можете использовать функцию WEP для любых процедур, в частности, для освобождения памяти, полученной при инициализации в функции LibMain.
Глобальная динамическая память
В Windows версии 3.1 область глобальной памяти общая для всех приложений Windows. Теоретически одно приложение может заказать для себя блок памяти из глобальной области и передать его идентификатор другому приложению, однако такая практика не приветствуется, так как в следующих версиях Windows адресные пространства приложений могут быть разделены (для передачи данных между приложениями необходимо использовать механизм динамической передачи данных DDE, который будет описан в одном из следующих томов "Библиотеки системного программиста").
Глобальная и локальная область памяти
Свободное пространство в области стандартной памяти и расширенная память используются операционной системой Windows. Она как бы объединяет всю свободную память в одну глобальную область памяти (global heap ) и использует эту область для себя и для запуска приложений Windows.
Помимо глобальной области памяти, для каждого приложения Windows выделяется собственная локальная область памяти (local heap ). Размер этой области ограничен величиной 64 Кбайт. Если для приложения требуются блоки памяти большего размера, оно может их получить из глобальной области памяти
На практике глобальная память используется всегда, когда приложению требуется блок памяти размером, большим чем несколько Кбайт.
Графика в меню
До сих пор наши меню состояли только из текстовых строк и разделительных линий, однако вы можете сделать меню из произвольных графических изображений. Если вам не нравится стандартная отметка строк меню при помощи галочки, ее можно заменить на любое графическое изображение (небольшого размера).
Для того чтобы вместо строк в меню расположить графические изображения bitmap, эти изображения надо загрузить из ресурсов или создать другим способом, а затем идентификатор изображения указать в качестве последнего параметра функций AppendMenu или InsertMenu. Необходимо также использовать флаг MF_BITMAP :
AppendMenu(hmenuLineStyle, MF_ENABLED | MF_BITMAP, CM_LINE1, (LPCSTR)(DWORD)hbmpLine1);
Если вы загрузили bitmap при помощи функции LoadBitmap, не забудьте перед завершением приложения (или после удаления соответствующего меню) удалить bitmap функцией DeleteObject.
Для замены стандартного символа отметки строки меню (галочки) на другой предназначена функция SetMenuItemBitmaps:
BOOL WINAPI SetMenuItemBitmaps(HMENU hmenu, UINT idItem, UINT fuFlags, HBITMAP hbmUnckecked, HBITMAP hbmChecked);
Эта функция выполняет замену символа отметки для строки idItem меню hmenu.
Для параметра fuFlags можно использовать два значения - MF_BYCOMMAND и MF_BYPOSITION . Если указан флаг MF_BYCOMMAND, параметр idItem определяет идентификатор элемента меню, для которого выполняется замена символа отметки. Если указан флаг MF_BYPOSITION, параметр idItem определяет порядковый номер элемента меню.
Параметр hbmUnckecked представляет собой идентификатор изображения, которое будет расположено слева от неотмеченной строки меню, параметр hbmChecked - идентификатор изображения символа отметки.
Любой из последних параметров или оба можно указывать как NULL. В этом случае будет использовано изображение по умолчанию (т. е. слева от неотмеченной строки не будет никакого изображения, слева от отмеченной - галочка).
Однако есть небольшая тонкость. Вы не можете использовать для отметки строк меню изображения bitmap любого размера.
Нужные размеры изображения необходимо определить при помощи функции GetMenuCheckMarkDimensions :
DWORD WINAPI GetMenuCheckMarkDimensions(void);
Младшее слово возвращаемого значения содержит ширину изображения, старшее - высоту:
DWORD dwMark; WORD wWidth, wHeight; dwMark = GetMenuCheckMarkDimensions(); wWidth = LOWORD(dwMark); wHeight = HIWORD(dwMark);
Тонкость заключается в том, что на момент трансляции исходного текста приложения вы не можете знать требуемые размеры изображения. Так как на этапе сборки загрузочного модуля приложения размеры изображения неизвестны, вы (строго говоря) не можете просто загрузить соответствующие изображения bitmap из ресурсов приложения.
Выход заключается в том, чтобы в процессе инициализации приложения определить требуемые размеры изображения, вызвав функцию GetMenuCheckMarkDimensions, а затем подготовить нужные изображения bitmap в памяти. Однако, так как мы еще не рассказывали вам подробно об изображениях bitmap, в примере GMENU, приведенном в следующем разделе, мы для простоты (данная глава посвящена меню, а не изображениям bitmap) все-таки загрузили изображения размером 10 х 10 пикселов из ресурсов.
При создании строки меню вы можете указать константу MF_OWNERDRAW. В этом случае функция окна, работающая с данным меню, должна будет сама нарисовать строку меню. Можно нарисовать любое изображение.
Перед тем как отобразить меню, содержащее строки со стилем MF_OWNERDRAW , операционная система Windows посылает в функцию окна сообщение WM_MEASUREITEM . В ответ на это сообщение функция должна сообщить Windows размеры окна, необходимые для изображения строки меню.
Когда надо отобразить строку меню, Windows посылает в родительское окно сообщение WM_DRAWITEM . Вместе с этим сообщением передается вся информация, необходимая родительскому окну для того чтобы нарисовать строку меню.
Параметр lParam сообщения WM_MEASUREITEM содержит указатель на структуру MEASUREITEMSTRUCT , описанную в файле windows.h:
typedef struct tagMEASUREITEMSTRUCT { UINT CtlType; UINT CtlID; UINT itemID; UINT itemWidth; UINT itemHeight; DWORD itemData; } MEASUREITEMSTRUCT;
В этом же файле описаны ближний и дальний указатели на эту структуру:
typedef MEASUREITEMSTRUCT NEAR* PMEASUREITEMSTRUCT; typedef MEASUREITEMSTRUCT FAR* LPMEASUREITEMSTRUCT;
Когда функция окна получает сообщение WM_MEASUREITEM, поле CtlType содержит значение ODT_MENU, в поле itemID находится идентификатор строки меню, а в поле itemData - 32-разрядное значение, переданное через параметр lpNewItem функций AppendMenu, InsetMenu, ModifyMenu. Поле CtlID не используется.
Получив сообщение WM_MEASUREITEM, функция окна должна, пользуясь значением указателя из lParam, записать в поле itemWidth ширину строки меню, а в поле itemHeight - высоту строки меню.
Параметр lParam сообщения WM_DRAWITEM содержит указатель на структуру DRAWITEMSTRUCT . Эта структура и соответствующие указатели описаны в файле windows.h следующим образом:
typedef struct tagDRAWITEMSTRUCT { UINT CtlType; UINT CtlID; UINT itemID; UINT itemAction; UINT itemState; HWND hwndItem; HDC hDC; RECT rcItem; DWORD itemData; } DRAWITEMSTRUCT; typedef DRAWITEMSTRUCT NEAR* PDRAWITEMSTRUCT; typedef DRAWITEMSTRUCT FAR* LPDRAWITEMSTRUCT;
Приведем назначение отдельных полей структуры DRAWITEMSTRUCT при ее использовании для меню.
Имя поля | Описание |
CtlType | Тип органа управления. Для меню принимает значение ODT_MENU |
CtlID | Идентификатор органа управления. Для меню не используется |
itemID | Идентификатор строки меню |
itemAction | Действия, которые необходимо выполнить при изображении строки меню. Определен в виде отдельных битовых флагов: ODA_DRAWENTIRE - требуется перерисовать всю строку; ODA_FOCUS - этот бит устанавливается в 1, если строка меню получила или потеряла фокус ввода, новое состояние строки можно узнать, проанализировав содержимое поля itemState; ODA_SELECT - изменилось состояние строки меню, для уточнения состояния необходимо использовать поле itemState |
itemState | Вид, в котором необходимо изобразить строку меню. Определен в виде отдельных битовых флагов: ODS_CHECKED - выбрана строка меню; ODS_DISABLED - строка неактивна; ODS_FOCUS - строка получила фокус ввода; ODS_GRAYED - строка меню должна быть изображена серым цветом; ODS_SELECTED - строка выбрана |
hwndItem | Идентификатор меню |
hDC | Контекст устройства, который необходимо использовать для рисования строки меню |
rcItem | Прямоугольные границы, внутри которых необходимо нарисовать строку |
itemData | Содержит 32-битовое значение, полученное через параметр lpNewItem функций AppendMenu, InsetMenu, ModifyMenu |
HInstance
Поле hInstance используется перед вызовом функции для идентификации блока памяти, содержащего шаблон диалоговой панели. Содержимое поля игнорируется в том случае, если не указаны флаги OFN_ENABLETEMPLATE или OFN_ENABLETEMPLATEHANDLE.
HwndOwner
Поле hwndOwner должно содержать идентификатор окна, создавшего диалоговую панель. Можно указать значение NULL, при этом диалоговая панель не будет иметь окно-владельца. В этом случае нельзя использовать флаг OFN_SHOWHELP.
Идентификатор элемента меню
Для получения идентификатора элемента меню, расположенного в указанной позиции, вы можете воспользоваться функцией GetMenuItemID :
UINT WINAPI GetMenuItemID(HMENU hmenu, int nPos);
Параметр hmenu задает меню, идентификатор элемента которого требуется определить. Порядковый номер элемента определяется параметром nPos, причем первому элементу соответствует нулевое значение. В случае ошибки (если параметр hmenu указан как NULL или указанный элемент является временным меню) функция GetMenuItemID возвращает значение -1. Если вы попытаетесь определить идентификатор горизонтальной разделительной линии (сепаратора), функция вернет нулевое значение.
Идентификатор меню
С помощью функции GetMenu вы можете определить идентификатор меню, связанного с окном:
HMENU WINAPI GetMenu(HWND hwnd);
Идентификатор окна задается при помощи параметра hwnd.
Функция возвращает идентификатор меню или NULL, если окно не имеет меню. Дочернее окно не может иметь меню, однако в документации к SDK говорится, что если вы вызовете данную функцию для дочернего окна, возвращенное значение будет неопределено.
Идентификатор временного меню
Для определения идентификатора временного меню следует вызвать функцию GetSubMenu :
HMENU WINAPI GetSubMenu(HMENU hmenu, int nPos);
Эта функция для меню верхнего уровня с идентификатором hmenu возвращает идентификатор временного меню, порядковый номер которого задается параметром nPos. Первому временному меню соответствует нулевое значение параметра nPos.
Если функция GetSubMenu вернула значение NULL, то меню верхнего уровня не содержит в указанной позиции временное меню.