/>Первое знакомство
Данный разделназывается “первое знакомство”. Здесь вы действительно познакомитесь с первымприложением для Windows. Но не думайте, что это знакомство только с простейшимприложением. Здесь вы познакомитесь с некоторыми основными идеями, заложеннымив системы типа Windows, а также с их влиянием на работу (и написание)приложений. Простейшее приложение оказывается лишь поводом для серьезногознакомства с целым классом систем.
Не ищитездесь подробных обсуждений тех или иных вопросов — здесь будут встречатьсятолько обзоры, а более полные сведения вы сможете найти в последующих разделах.Такая структура принята потому, что программирование в Windows требует использования функций и инструментов из самыхразных подсистем, так что последовательное рассмотрение Windows API практически невозможно.
Так,например, даже в простейшем приложении надо осуществлять вывод данных в окно.Для этого необходимо изучить графический интерфейс устройств (GDI) и контекст устройства. Но для этогонадо уже быть знакомым с диспетчером памяти, с описанием ресурсов приложения, сиспользованием стандартных диалогов, с обработкой сообщений и многим другим.При рассмотрении любого из перечисленных пунктов пришлось бы ссылаться надругие разделы, в том числе на тот же графический интерфейс./> Такие разные операционные системы
Сразу оговоримся— в Windows возможно запускать приложения(application) двухразных типов — приложения Windows и приложения MS–DOS. Методы разделенияресурсов, применяемые этими приложениями существенно различаются, какразличаются и методы доступа к ресурсам. В этой ситуации мы будем, говоря оприложении вообще, подразумевать приложение Windows. Если разговор зайдет оприложениях MS–DOS, то это будет оговорено отдельно.
Рассматриваяработу приложения в среде Windows надо отталкиваться от того факта, что Windowsявляется многозадачной средой. В этом случае в системе может выполнятьсяодновременно[1] несколько разныхприложений. Каждое приложение для своей работы требует некоторых ресурсовсистемы — дискового пространства, оперативной памяти, времени процессора,устройств ввода и вывода информации и пр. Соответственно Windows долженвыполнять функции арбитра, осуществляющего разделение ресурсов междуприложениями и контролирующего корректность работы приложений с выделенными имресурсами.
С этой точкизрения можно рассмотреть развитие операционных систем, начиная от простейших(типа MS–DOS) и заканчивая достаточно сложными (как Windows NT, Unix, OpenVMS),для того что бы лучше понять возможности и ограничения разных реализацийWindows.
Простейшиеоднопользовательские однозадачные операционные системы являются, одновременно,примитивными арбитрами. Операционная система предоставляет набор средств длявыполнения тех или иных операций по работе с ресурсами и организует запускприложений. При этом приложению передается управление и система ожидает, покаоно завершит работу. Во время работы это приложение может обращаться к системедля получения ресурсов, а может это делать и в обход системы. Корректностьработы приложения никак не контролируется — система только лишь предоставляетпримитивный, часто неоптимальный и необязательный метод доступа к ресурсу.Приложение пользуется всеми ресурсами практически без ограничений (заисключением тех, которые заняты самой системой) и имеет непосредственный доступк аппаратуре компьютера.
Такой подходтипичен для операционных систем небольших компьютеров: сравнительно слабаяподдержка периферийных устройств, простая файловая система и уникальнаяоткрытость, почти вседозволенность для приложений — так как конфликтовать им нес кем. Яркий пример — MS DOS первых версий.
Практическитакие операционные среды мало применимы и, если система оказалась удачной, тоона начинает развиваться в сторону поддержки многозадачного режима. Более того— обработка прерываний в однозадачной системе — неизбежный “многозадачныйминимум”. Резидентная программа в однозадачной среде — еще одна попыткаприближения к многозадачности. Те, кто пытался создать сколько–нибудь сложнуюрезидентную программу обязательно сталкивался с трудностями разделенияресурсов, так как эффективных механизмов для этого однозадачные ОС несодержат. А благодаря своей открытости такие системы оказались благодатнойпочвой для развития вирусов.
Наиболеемощными представляются многозадачные, многопользовательские ОС, особенно вмультипроцессорных системах. Работа такой системы основана на параллельномисполнении нескольких приложений. Приложение может выполняется либо наотдельном процессоре, либо в режиме разделения времени — в определенныевременные интервалы.
Приложение незнает, и практически не наблюдает присутствия и выполнения других приложений.Все проблемы бесконфликтного разделения ресурсов между разными задачами лежатна системе. Приложение не знает, в какой момент времени ее выполнение будетпрервано, не знает когда будет возобновлено и вообще не регистрирует фактапрерывания. Для одной задачи процесс выполнения представляется непрерывным.
Многопользовательскиемногозадачные системы обычно встречаются на более производительных компьютерах,при этом они содержат мощные средства взаимодействия с периферией, практическиисключающие необходимость доступа приложений непосредственно к аппаратуре. Кроме того операционная система имеет очень гибкую и эффективную системураспределения ресурсов, привилегий и пределов доступа ко всем ресурсамкомпьютера, а также содержит средства защиты от несанкционированного доступа кним. В качестве примеров можно привести OpenVMS, Unix, в несколько меньшей мереWindows NT (реализована для персонального компьютера).
Однако такиеудобные и мощные ОС часто не имеет смысл реализовывать на персональномкомпьютере для которого (по идее его использования) не нужнамногопользовательская система, и поэтому его открытость далеко не всегдаявляется уязвимостью, но часто даже повышает эффективность его использования.Понятно также, что с ростом мощности персонального компьютера, появляетсянеобходимость создания простых многозадачных ОС, ориентированных на работуодного пользователя.
Такая системазанимает промежуточное положение между однозадачной и многопользовательскоймногозадачной системами. Очевидно, что ее ядро будет обладать большейоткрытостью, большей возможностью доступа к аппаратуре и относительно простымиметодами разделения ресурсов, чем в сложных системах.
В качествеиллюстраций к этому можно привести Windows 3.x, разделение ресурсов в которой взначительной мере основано на методах MS–DOS, а также Windows–95, котораязанимает промежуточное положение между Windows NT и Windows 3.x, предоставляясущественно упрощенные методы доступа к ресурсам, чем Windows NT и, в то жевремя, обеспечивая качественно лучшую защиту, чем Windows 3.x./> Краткие сведения оразделении ресурсов в Windows
Для того, чтобы Windows мог успешно разделять ресурсы, на разрабатываемые программынакладывают ряд требований[2]. Понятно, что первымтаким требованием является требование не использования аппаратурынепосредственно. Вы можете использовать только средства Windows для выполнениявсех своих задач, но не осуществлять доступ к аппаратуре непосредственно.
Например,приложение не должно обращаться к видеопамяти, средствам BIOS и т.д. Еслиприложение должно вывести на дисплей какое–либо изображение оно обязановоспользоваться существующими функциями Windows.
Конечно,ограничение на доступ к аппаратуре — не самое приятное ограничение, но оноимеет и свою оборотную, причем приятную, сторону: пользователь (и разработчик)может не думать о том, с чем конкретно он имеет дело — об этом заботитьсяWindows. То есть Windows сам обеспечивает выполнение всех требуемых операций наимеющемся оборудовании, причем используя практически все возможности этогооборудования.
Интереснообзорно рассмотреть методы разделения основных ресурсов компьютера междузадачами, принятыми в Windows, и влияние этих методов на правила написанияпрограмм. Причем для первого знакомства отталкиваться мы будем от наиболеепростой системы — Windows 3.x.
К основнымразделяемым ресурсам несомненно относятся дисплей, клавиатура, мышь,оперативная память, процессор и диск. Стоит коротко отметить методы разделениякаждого из этих ресурсов./> Дисплей
Дляразделения дисплея между разными задачами в Windows используются окна (window). Каждой задаче назначено, поменьшей мере, одно окно, и осуществлять вывод приложение может (точнее должно)только в это окно.
Приложениеможет обладать несколькими окнами. В этом случае, обычно, одно окно является родительским (parent), а другиеявляются дочерними(child) окнами поотношению к родительскому окну. Как правило, приложение имеет только одно окно,не имеющее родителей — это так называемое главное окно приложения (main window). Всеостальные окна приложения являются дочерними по отношению к этому окну.
Окна могутперемещаться по экрану, перекрывая полностью или частично другие окна. Окнамогут находиться в максимизированном(“распахнутом” на весь экран, maximized,zoomed), нормальном или минимизированном (minimized, iconed) состоянии. Вминимизированном состоянии окно заменяется на специальную небольшую картинку,называемую пиктограммой(иконой, icon), либо помещается вспециальный список окон (taskbarили systray дляWindows–95 и Windows NT 4.0).
Основнаялогика использования перекрывающихся окон реализована в Windows, хотяпрограммистам остается достаточно много работы — приложение вынужденоперерисовывать окно каждый раз, когда возникает в этом необходимость (например,при увеличении размеров окна, если ранее скрытое окно становится видимым и вдругих случаях).
То, что невся работа по перерисовке перекрывающихся окон выполняется системой, связано сиспользованием графического режима отображения окон. Для полной автоматизациинеобходимо было бы “виртуализовать” всю работу с окнами — то есть в обычнойоперативной памяти должна находиться копия изображения окна. Тогда Windows могбы полностью или частично восстанавливать изображение при появлении ранееневидимой части окна по этой копии. Однако общий размер нескольких копий (длякаждого окна своя копия) может быть сопоставим с объемом всей оперативнойпамяти компьютера. Скажем для режима 1280x1024,16 бит/пиксель (это далеко не самый хороший)битмап экрана занимает примерно 2.5MB.Кроме того, размер окна может быть больше экрана и таких окон может бытьнесколько. Таким образом Windows практически не может использовать виртуальныеокна — ресурсов компьютера для этого явно не хватает (их еще надо разделять свыполняемыми приложениями и с компонентами самой системы).
Строго говоря,окно в Windows является тем самым объектом, для которого частично реализован объектно–ориентированныйподход. Интересно, что в документации Windows термин “объект” никогда неприменяется к окну, а то, что называется “объектами”, ни в коей мере не являетсяобъектами ООП./> Клавиатура и мышь
Этиустройства относятся к устройствам ввода данных. Разделение этих устройствмежду разными задачами легко можно разбить на два круга вопросов:
· определение, ккакой задаче относятся данные, полученные от устройства ввода.
· передачаполученных от устройства данных требуемой задаче.
Для мышиопределение, к какой задаче относятся данные, вполне очевидно: так как каждаязадача имеет по крайней мере одно окно, то информация от мыши должнаиспользоваться окном, на фоне которого находится курсор мыши.
Дляклавиатуры дело обстоит несколько сложнее: нам придется ввести понятие активное окно (active window). В данныймомент времени обязательно существует только одно активное окно, это окновыделяется цветом заголовка, рамки или подписи (если окно минимизировано).Активное окно является пользователем клавиатуры в данный момент времени. Длятого, что бы другая задача могла получать данные, необходимо сделать активным окно,принадлежащее этой задаче.
Передачаполученных от устройства данных требуемой задаче. Хотя интуитивно кажется, чтоздесь не должно быть особых сложностей, но именно здесь очень ярко проявляютсяособенности организации Windows. Мы к этому вернемся, рассматривая методразделения процессора./> Диск
Дляразделения дискового пространства используется файловая система. Здесь Windows3.x просто пользуется уже имеющимся — файловой системой DOS; Windows–95 используетслегка модернизированную файловую систему DOS (поддерживаются имена файловдлиной до 256 символов и возможно использование такназываемого FAT32 вместо FAT16 или FAT12). И только Windows NT предоставляетсобственную файловую систему — NTFS, хотя может работать и с FAT. NTFSотличается от FAT существенно более сложной организацией, позволяющей создаватьединые тома из нескольких дисков, организовывать зеркальные тома или тома сизбыточностью для хранения важных данных, а также задавать права доступа к отдельнымфайлам конкретным пользователям. Естественно, более сложная система оказываетсяболее чувствительной к сбоям (несмотря на специально принятые меры) и менеепроизводительной (несмотря на специальную оптимизацию).
Для доступа кфайлам Windows предоставляет свои собственные функции. В случае Windows 3.x этифункции в основном соответствуют функциям DOS для доступа к файлам и разделениядоступа. Для нормальной работы Windows надо устанавливать программу SHARE.EXEдо запуска Windows 3.1, либо, в случае Windows 3.11, будет использованспециальный компонент Windows — VSHARE.386. Более того, по версию Windows 3.0включительно, имел место любопытный нюанс: Windows имел собственную функцию дляоткрытия файлов (OpenFile), но совершенно не предоставлялсредств для чтения/записи — они были просто не декларированы, хотя внутрисамого Windows содержались. Программисту рекомендовалось либо применятьфункции Run–Time библиотеки принятого языка (что можно было делать лишьограниченно), либо написать свои процедуры на ассемблере. Либо, что делалосьгораздо чаще, использовать не декларированные функции Windows для работы сфайлами. С тех пор Microsoft просто декларировал эти функции.
Дляприложений, работающих в Win32 про функции DOS надо просто забыть — Win32предоставляет более богатый набор операций над файлами, поддерживает работу сразными файловыми системами[3] а, кроме того, исключаетвозможность применения прерываний DOS./>
Память
Реализацияметодов разделения памяти в Windows API и Win32 API качественно различаются.Для этого придется рассмотреть историю развития диспетчера памяти, что будетсделано позже. Сейчас надо обратить внимание только на некоторые общие идеиразделения памяти.
В обоих APIпамять делится на отдельные блоки. Однако деление осуществляется совершенноразными методами.Windows API
Коротко можноотметить, что вся доступная для Windows память называется глобальной (иногда глобальный хип,глобальная куча, global heap).Эта глобальная память делится на отдельные блоки, которые могут бытьперемещаемыми в памяти. В виде блоков глобальной памяти в Windowsпредставляются даже программы — в этом случае каждому программному сегментусоответствует один блок глобальной памяти.
Сегментданных программы, представленный в виде блока глобальной памяти, можетсодержать свою локальнуюкучу (локальный хип, local heap).Эта память также может делиться на блоки, называемыми локальными. Термин локальный применяется кпамяти, если она принадлежит сегменту данных программы.
Windowsпредоставляет программные средства для манипуляции с блоками обоих видов — иглобальными, и локальными. Каждый блок может быть фиксированным, перемещаемымили удаляемым в/из памяти. Это обеспечивает возможность как выделения большихнепрерывных блоков данных (за счет перемещения других блоков), так ивозможность удаления части блоков при недостатке памяти.Win32 API
В Windows–95и в Windows NT используется так называемая виртуальная память. Для каждогозапущенного приложения выделяется собственное адресное пространство, размером 4Г, которым приложение владеет монопольно. Вэтом пространстве не находится никаких данных или кода других приложений. Такимобразом приложения Win32 изолированы друг от друга. Необходимо учесть, что“адресное пространство” не соответствует реально выделяемой памяти — это тотдиапазон адресов, в котором можетразмещаться память, реально выделенная приложению. Очевидно, что из возможных 4Г адресного пространства используются обычнотолько несколько мегабайт, занимаемые кодом и данными приложения и необходимымикомпонентами системы.
Адресноепространство приложения делится на отдельные фрагменты, содержащие код, данные,служебную информацию и пр., необходимые для этого приложения. Однако такоеделение статично — перемещение фрагментов в адресном пространстве непроисходит. Оптимизация доступа к памяти осуществляется не с помощьюперемещения блоков или их удаления, а с помощью механизма отображения виртуальногоадресного пространства на физически доступную память компьютера (упрощенноможно считать, что виртуальное адресное пространство приложения — этоспециальный файл подкачки страниц; оперативная память при этом выполняет ролькэша, в котором находятся только активно используемые код и данные).
Помимо этогов адресном пространстве приложения могут выделяться одна или несколько куч(хипов), разделяемых на отдельные блоки. Вот эти–то блоки могут перемещатьсявнутри своей кучи и даже удаляться из памяти. Сама куча в адресном пространствеприложения перемещаться не будет. Для каждого приложения выделяется по меньшеймере одна куча, называемая стандартной(default heap).Все функции Windows API, работающие с глобальной или локальной кучамиперенесены в Win32 API так, что они работают именно с этой стандартной кучей.При этом нет никакой разницы между глобальной и локальной кучами./> Процессор
Выше, прирассмотрении разных типов операционных систем, было выделено два “чистых” типасистем: однопользовательские однозадачные и многопользовательскиемногозадачные. Windows во всех его версиях занимает некоторые промежуточныеположения между двумя этими крайними типами. Так версии Windows 3.xприближаются к простейшему типу однопользовательских однозадачных систем (сочень ограниченной реализацией некоторых возможностей как многопользовательскойработы, так и многозадачного режима), а наиболее сложная Windows NT являетсяистинно многозадачной системой с развитыми средствами разделения доступапользователей.Windows API и объектно–ориентированноепрограммирование
Методыразделения процессора, применяемые разными реализациями Windows, интереснорассмотреть в их развитии — от простого к сложному. Так в идеальной однозадачнойсреде, приложение, раз начавшись, выполняется без перерывов до полногозавершения. В истинно многозадачной среде приложение выполняется за многошагов, причем само приложение не знает, когда его прервут для обработки другихприложений — этим ведает только система[4].
Промежуточнымрешением является среда, получившая название псевдомногозадачной (невытесняющаямногозадачность, non–preemptivemultitasking). В такой среде, подобно однозадачной, система непрерывает выполнения приложения. Однако само приложение должно быть разделенона небольшие, быстро выполняемые фрагменты. После выполнения такого фрагментасистема может перейти к выполнению другого приложения. При этом приложение самоуведомляет систему, где ее можно прервать для выполнения других задач.
В Windows 3.xэто может быть реализовано двумя разными методами:
· обычно приложениеразбивается на набор небольших, быстро выполняемых функций. В этом случаесистема просто вызывает нужные функции для выполнения требуемых задач. Послезавершения обработки одной функции система может вызвать другую функцию другогоприложения, осуществляя таким образом выполнение нескольких приложений как–быодновременно.
· можновоспользоваться специальной функцией, передающей управление системе, и возвращающейего назад приложению после обработки других приложений. Таких функций в Windows3.x две — Yield и DirectYield. Однако этот путь используется в оченьспециальных случаях, например при разработке отладчиков, из–за довольно жесткихограничений на применение этих функций.
При написаниинормальных приложений для Windows 3.x разбиение программы на отдельные функциипроизводится не механически, скажем через 100 строк, а функционально — каждая функция выполняетопределенные действия. При этом система, вызывая соответствующую функцию,передает ей некоторые данные, которые указывают, что надо сделать.
Это оченьважный момент.
До сих пор все программы состояли из алгоритма,управляющего данными. На практике это означало, что алгоритм, описывающий программу, предусматривалкогда, где и в какой мере возможно получение данных и управляющих воздействий,и как и куда направлять вывод результатов.
Например, принеобходимости ввода данных с клавиатуры, программа включала в себя вызов коперационной системе (или BIOS, на худой конец), который и возвращал требуемыеданные.
Еще раз:обычная программа генерирует вызовы к операционной среде для получения и выводаданных: алгоритм управляетданными
Врассмотренном нами случае получается совершенно иная ситуация: поступающие от системы данные управляют поведениемпрограммы. Часто такими данными являются управляющие воздействияпользователя (например, изменение размеров окна, вызов меню и др.). Этивоздействия, вообще говоря, не синхронны с работой вашей программы, то естьполучается, что данные управляюталгоритмом — один из основных принципов объектно–ориентированногопрограммирования (ООП).
Введем новыепонятия:
· данные,передаваемые от системы к соответствующей функции называются сообщением (message).
· процесс обращенияк требуемой функции называется посылкой(post) или передачей (send) сообщения.
· функция,обрабатывающая сообщения, называется процедурой обработки сообщений (message handler).
Такимобразом, когда вы создаете программу, работающую в псевдомногозадачной среде(здесь: Windows 3.x), вы должны написать требуемые процедуры обработкисообщений. Далее Windows будет передавать вашим процедурам сообщения для ихобработки.
С точкизрения ООП все объекты должны обладать 3мя свойствами:
инкапсуляция — объединение в единое целое алгоритмов и необходимыхданных;
наследование — возможность порождения новых объектов, основываясь насуществующих, наследуя их свойства;
полиморфизм — разность реакций на одинаковые воздействия; наследникиодного объекта могут отличаться своими свойствами друг от друга и от предка.
С точкизрения этих свойств объект, определенный процедурой обработки сообщений,удовлетворяет всем этим требованиям. Процедура обработки сообщений можетпользоваться специфичными, сгруппированными в каких–либо структурах, данными(инкапсуляция). Мы можем создавать новый объект со своей процедурой обработкисообщений, которая может ссылаться на процедуру ранее описанного объекта(наследование), а также выполнять обработку дополнительных сообщений или иначеобрабатывать прежние сообщения (полиморфизм).
Обычноговорят, что процедура обработки сообщений определяет свойства объекта, так какзадает реакцию этого объекта на воздействия (сообщения). Именно с такойтрактовкой объекта возникли первые языки ООП.
В Windowsобъектом ООП является окно. Соответственно говорят, что сообщения направлены непроцедуре, а окну. Процедура обработки сообщений определяет окно с конкретнымисвойствами, даже больше — одна процедура может обслуживать несколько разныхокон, но тогда эти окна будут иметь одинаковую реакцию на одинаковыевоздействия. То есть процедура обработки сообщений определяет не одно окно, ацелый класс (class) окон.
Сообщения,которые Windows направляет окну, отражают то, что происходит с этим окном.Например, есть сообщения, информирующие об изменении размеров окна, или оперемещении мыши, или нажатии на клавишу и др. Передача сообщений являетсямеханизмом разделения многих ресурсов, не только процессора. Так, с помощьюодних сообщений, реализовано разделение мыши или клавиатуры между задачами,другие сообщения, получаемые окном, помогают осуществить разделение дисплея ит.д.
Таким образомпсевдомногозадачный метод разделения процессора оказался основой для построенияобъектно–ориентированной среды и попутно перевернул всю привычную нам философиюнаписания программ — мы теперь создаем не управляющий алгоритм, а наборпроцедур, обеспечивающий реакцию нашего окна (то есть нашей программы) навнешние события.
Обработкасообщений является очень распространенным способом организации ООП–библиотекили ООП–языков. Существенноеотличие (причем не в лучшую сторону) Windows 3.x заключается в том, чтообработка сообщений является методом разделения процессора впсевдомногозадачной среде. Так как система не прерываетвыполнение приложения в процессе обработки сообщения, то его обработка недолжна занимать много времени
Это сильнозатрудняет применение Windows 3.x для расчетных задач — либо мы должны ихвыполнить быстро, либо разбить на быстро выполняемые части, и понемногуобрабатывать по мере получения сообщений. Понятно, что обычно приходитсяразбивать на части, а это существенно замедляет вычисления. Вообще говоря, приобработке сообщения лучше укладываться в интервал менее 1 секунды, что бы задержка в реакции Windows на управляющиевоздействия не была очень большой; критичной является задержка порядка 1–2 минуты — при этом Windows 3.x можетпросто дать сбой или зависнуть (что очень сильно зависит от наличия другихработающих приложений).Win32 API
В болеесложном Win32 API применяется так называемая истинная многозадачность (вытесняющая, preemptive multitasking).В этом случае разделение процессора осуществляется по определенным временныминтервалам (квантам времени). Обработка сообщений перестала быть методомразделения процессора, и в процессе обработки сообщения система можетпередавать управление другим приложениям. Сама же идея примененияобъектно–ориентированного подхода к окнам осталась неизменной.
Однако надоотметить, что реализация истинной многозадачности оказалась неполной. В рамкахWin32 API могут работать как настоящие Win32 приложения, так и их 16ти разрядныесобратья, написанные для Windows API. При запуске таких 16ти разрядных приложений подWin32 для них запускается специальная виртуальная 16ти разрядная Windows–машина, причемв Windows–95 для всех 16ти разрядных приложений используется одна общаявиртуальная машина. Это значит, что истинная многозадачность реализована толькомежду Win32 приложениями, в то время как 16ти разрядные приложения между собой используют обработкусообщений для разделения отведенного им процессорного времени. В случае WindowsNT для каждого 16ти разрядногоприложения запускается собственная Windows–машина, что позволяет им разделятьпроцессор общим способом с приложениями Win32.
Истиннаямногозадачность в Win32 позволила реализовать так называемые многопотоковые приложения (multithread application).При этом выделяют два новых понятия — процесс (proccess) и поток(thread). Процессыв Win32 API примерно эквивалентны приложениям в Windows API. Для каждогопроцесса выделяются определенные системные ресурсы — адресное пространство,приоритеты и права доступа к разделяемым ресурсам и прочее, но не процессорноевремя. Процесс только лишь описывает запущенную задачу, как она есть, безнепосредственных вычислений. Для разделения процессора используются непроцессы, а потоки, которым и выделяется процессорное время. В рамках каждогопроцесса выделяется свой поток, называемый первичным (primary thread), создаваемый поумолчанию при создании процесса. При необходимости в пределах одного процессаможет быть создано много потоков, конкурирующих между собой (и с потокамидругих процессов) за процессорное время, но не за адресное пространство./> Как написать приложение для Windows
Пока мырассмотрели только основную идею использования сообщений для реализацииобъектно–ориентированной операционной среды. Сейчас надо перейти к особенностяморганизации приложения, работающего в такой среде.
Каждоеприложение открывает по меньшей мере одно окно (в принципе могут существоватьприложения вообще без окон, но как небольшие специализированные процедуры, нетребующие никакого управления). Свойства окна определяются процедурой обработкисообщений этого окна. Таким образом, что бы определить свойства нужного окна,надо написать процедуру обработки сообщений, посылаемых этому окну (оконную процедуру или оконную функцию — window procedure, она жепроцедура обработки сообщений,message handler).
Однапроцедура может обслуживать сообщения, посылаемые разным окнам с одинаковымисвойствами. Говорят, что окна, имеющие одну и ту же оконную функцию,принадлежат к одному классу окон. Вы должны эту процедуру зарегистрировать —это называется регистрацией класса окон.
Далеенеобходимо предусмотреть средства для создания и отображения окназарегистрированного класса. С таким окном пользователь будет работать —передвигать его по экрану, изменять размеры, вводить текст и т.д. Вамнеобходимо обеспечить реакцию этого окна (то есть вашего приложения) надействия пользователя. Фактически вы должны запустить механизм, обеспечивающийдоставку сообщений, адресованных вашему окну, до получателя — оконнойпроцедуры. Этот механизм должен работать, пока работает ваше приложение. Такоймеханизм называется цикломобработки сообщений (messageloop).
Таким образомвы должны выполнить несколько шагов для создания собственного приложения:
· написать оконнуюфункцию;
· зарегистрироватьэту функцию (класс) в Windows, присвоив классу уникальное имя;
· создать окно,принадлежащее данному классу;
· обеспечить работуприложения, организовав цикл обработки сообщений.
Чутьподробнее рассмотрим, что происходит с приложением за время его “жизни” — отзапуска до завершения — перед тем, как перейдем к рассмотрению конкретногопримера.
Когда вызапускаете приложения для Windows, система сначала находит исполняемый файл изагружает его в память. После этого приложение осуществляет инициализациюнеобходимых объектов, регистрирует необходимые ему оконные классы, создаетнужные окна. можно считать, что, начиная с этого момента, приложение способнонормально взаимодействовать с пользователем и необходимым образом реагироватьна его действия. В это время должен работать цикл обработки сообщений, которыйбудет распределять поступающие сообщения конкретным окнам.
Сообщения,которые будет получать окно, информируют приложение о всех действиях, которыепредпринимает пользователь при работе с данным окном. Так, существуютсообщения, информирующие о создании окна, изменении его положения, размеров,вводе текста, перемещении курсора мыши через область окна, выборе пунктов меню,закрытии окна и т.д. Для удобства работы системы все сообщения имеют уникальныеномера, по которым определяется назначение этого сообщения; а для удобстваразработки приложений для всех сообщений определяются символические названия.Например:
#define WM_MOVE 0x0003
#define WM_SIZE 0x0005
В большинствеслучаев названия сообщений начинаются на WM_,однако названия некоторых сообщений имеют префиксы BM_, EM_, LBM_, CBM_и другие.
Для началавыделим четыре сообщения, с которыми мы будем знакомится первыми. Это сообщенияприменяются при создании окна (WM_CREATE),при закрытии[5] (WM_DESTROY и WM_QUIT)и при его перерисовывании (WM_PAINT).
В тот момент,когда приложение создает новое окно, оконная процедура получает специальноесообщение WM_CREATE, информирующее окно о его создании.При этом окно создается с помощью вызова специальной функции (CreateWindow, CreateWindowExи некоторые другие), которая выполняет все необходимые действия; сообщение приэтом имеет лишь “информационный” характер — оно информирует окно о том, что егосоздают. Однако реальное создание происходит не в обработчике этого сообщения,а в той функции, которую вызвали для создания окна.
На сообщенииперерисовки окна WM_PAINT надо остановиться чуть подробнее.Дело в том, что какая–либо часть окна может быть скрыта от пользователя(например, перекрыта другим окном). Далее в процессе работы эта часть можетстать видимой, например вследствие перемещения других окон. Сама система приэтом не знает, что должно быть нарисовано в этой, ранее невидимой части окна. Вэтой ситуации приложение вынуждено позаботиться о перерисовке нужной части окнасамостоятельно, для чего ему и посылается это сообщение каждый раз, как видимаяобласть окна изменяется.
Когда окнозакрывается, оно получает сообщение WM_DESTROY,информирующее о закрытии окна. Как и в случае создания, сообщениео закрытии является информационным; реальное закрытие осуществляетсяспециальной функцией (обычно DestroyWindow),которая, среди прочего, и известит окно о его уничтожении.
Все время,пока пользователь работает с приложением, работает цикл обработки сообщенийэтого приложения, обеспечивающий доставку сообщений окнам. В конце работыприложения этот цикл, очевидно, должен завершиться. В принципе, можно сделатьтак, что бы в цикле проверялось наличие окон у приложения. При закрытии всехокон цикл тоже должен завершить свою работу. Однако можно несколько упроститьзадачу — и в Windows именно так и сделано — вместо проверки наличия окон можнопредусмотреть специальный метод завершения цикла при получении последним окном(обычно это главное окно приложения) сообщения о его уничтожении (WM_DESTROY). Для этого применяетсяспециальное сообщение WM_QUIT, котороепосылается не какому–либо окну, а всему приложению в целом. При извлеченииэтого сообщения из очереди цикл обработки сообщений завершается. Для посылкитакого сообщения предусмотрена специальная функция — PostQuitMessage.
Послезавершения цикла обработки сообщений приложение уничтожает оставшиеся ненужныеобъекты и возвращает управление операционной системе.
Сейчас вкачестве примера мы рассмотрим простейшее приложение для Windows, традиционнуюпрограмму “Hello, world!”. После этого подробнее рассмотрим, как это приложениеустроено. Здесь же можно заметить, что при создании практически любых,написанных на “C”, приложений для Windows этот текст может использоваться вкачестве шаблона./>Пример 1A —первое приложениеФайл 1a.cpp
#define STRICT
#include
#define UNUSED_ARG(arg) (arg)=(arg)
static char szWndClass[] = «test window»;
LRESULT WINAPI_export WinProc( HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam )
{
UNUSED_ARG( wParam );
UNUSED_ARG( lParam );
PAINTSTRUCT ps;
switch ( uMsg ) {
case WM_CREATE:
return 0L;
case WM_PAINT:
BeginPaint( hWnd, &ps );
TextOut( ps.hdc, 0, 0, «Hello, world!», 13 );
EndPaint( hWnd, &ps );
return 0L;
case WM_DESTROY:
PostQuitMessage( 0 );
return 0L;
default:
break;
}
return DefWindowProc( hWnd, uMsg, wParam, lParam );
}
static BOOL init_instance(HINSTANCE hInstance )
{
WNDCLASS wc;
wc.style = 0L;
wc.lpfnWndProc = WinProc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = hInstance;
wc.hIcon = LoadIcon( NULL, IDI_APPLICATION );
wc.hCursor = LoadCursor( NULL, IDC_ARROW );
wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
wc.lpszMenuName = NULL;
wc.lpszClassName = szWndClass;
return RegisterClass( &wc ) == NULL? FALSE: TRUE;
}
int PASCAL WinMain(HINSTANCE hInst, HINSTANCE hPrevInst, LPSTR lpszCmdLine, int nCmdShow )
{
UNUSED_ARG( lpszCmdLine );
MSG msg;
HWND hWnd;
if ( !hPrevInst ) {
if ( !init_instance( hInst ) ) return 1;
}
hWnd= CreateWindow(
szWndClass, «window header», WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
NULL, NULL, hInst, NULL
);
if ( !hWnd ) return1;
ShowWindow( hWnd,nCmdShow );
UpdateWindow( hWnd );
while ( GetMessage(&msg, NULL, NULL, NULL ) ) {
TranslateMessage( &msg );
DispatchMessage( &msg );
}
return msg.wParam;
}
/>
Рисунок 1. Приложение 1a.cpp в среде Windows 3.x или Windows NT 3.x (слева) или в среде Windows–95 или Windows NT 4.0 (справа).
В зависимостиот платформы, на которой запускается это приложение, внешний вид окна можетнесколько изменяться. Это связано с изменившимся интерфейсом пользователя припереходе от Windows 3.x и Windows NT 3.x к Windows–95 и Windows NT 4.0.
Далее мы рассмотрим исходный текст более подробно. При первомвзгляде на него обращают на себя внимание сразу несколько необычных (посравнению с программами для DOS) вещей:
· новые типы данных
· странные именапеременных
· обилиеиспользуемых функций и передаваемых им параметров
Примерно втаком порядке мы и рассмотрим эти вопросы./> Новые типы данных
Итак, еще разрассмотрим первое Windows–приложение (1a.cpp).
Обычно вначале “С”–программы помещается директива препроцессора #include для включения файла, содержащего основныеопределения и прототипы функций. При написании Windows–приложений вы должнывключить файл WINDOWS.H. Этот файл содержит определениятипов, констант и функций, используемых в Windows[6].
В приложенииперед включением WINDOWS.H определяется специальный символ STRICT:
#define STRICT
#include
Он указывает,что необходимо осуществлять строгую проверку типов. То есть использованиевместо переменной одного типа переменной какого–либо другого, даже сходного,типа будет рассматриваться компилятором как ошибка.
Для большейчасти обычных типов Windows предлагает свои собственные определения — чтообъясняется возможностью реализации на разных вычислительных платформах и,соответственно, Windows–приложения должны быть переносимыми хотя бы на уровнеисходного текста.
Для 16ти и 32х разрядныхплатформ существенно различаются режимы адресации. Например, для 32х разрядныхмашин практически не применяются near и far модификаторы адреса (Win32 требует, чтобы приложения разрабатывались в 32х разрядной flat–моделипамяти, где на все про все отводится один 32х разрядный сегмент, размеромдо 4Г). Кроме того, стандартом Cпредполагается, что тип данных intимеет длину одно слово. То есть для 16ти разрядных машин он совпадает с типом short int, а для 32х разрядных с типом long int.Это приводит к частичной непереносимости С–программ с одной платформы надругую.
Из большогоколичества определяемых типов выделим несколько, с которыми нам придетсястолкнуться в самом начале. Те, которые мы будем вводить позже, будутобъясняться по мере поступления.Новое название Значение для Windows API Значение для Win32 API
Символы (#define)
FAR
NEAR
PASCAL
LONG
VOID
NULL
WINAPI
CALLBACK
far
near
pascal
long
void
0
pascal far
pascal far
pascal
long
void
0
pascal
pascal
Типы (typedef)
BOOL
BYTE
WORD
DWORD
UINT
NPSTR
PSTR
LPSTR
LPCSTR
WPARAM
LPARAM
LRESULT
FARPROC
HANDLE
HFILE
HWND
HINSTANCE
HDC
int
unsigned char
unsigned short int
unsigned long int
unsigned int
char near*
char *
char far*
const char far*
UINT
LONG
LONG
(far pascal *)(void)
unsigned int
HANDLE
HANDLE
HANDLE
HANDLE
int
unsigned char
unsigned short int
unsigned long int
unsigned int
char *
char *
char *
const char *
UINT
LONG
LONG
(pascal *)( void )
unsigned int
HANDLE
HANDLE
HANDLE
HANDLE
Практическидля всех определенных типов существуют типы “указатель на...”. Ближниеуказатели строятся с помощью префикса NP,а дальние — LP, указатели, соответствующие принятоймодели памяти, строятся с помощью префикса P. Например, BYTE —тип, представляющий отдельный байт, LPBYTE— дальний указатель на байт, а NPBYTE —ближний указатель. Исключение — тип VOID,он имеет только дальний указатель LPVOID.
Внимательнееразберемся с типом HANDLE (и со всеми“производными” от него): Дело в том, что Windows создает специальные структурыданных, описывающих требуемые объекты (например окно). Эта структура данныхзачастую принадлежит не вашему приложению, а самой системе. Для того, что быэтот объект можно было идентифицировать, вводится специальное понятие хендл (дескриптор, handle). Хендл в Windows —это просто целое число, иногда номер, присвоенный данному объекту, причемзначение NULL указывает на несуществующий объект.Единственное исключение — HFILE,для которого определено специальное значение — HFILE_ERROR, равное -1(это связано с тем, что хендл файла первоначально был заимствован у DOS, гдехендл 0 обозначает стандартное устройство вывода stdout). Понятие хендла в Windowsиспользуется очень широко, а для облегчения контроля типов используется большоеколичество производных от хендла типов.Win32
Здесь же надоеще раз отметить, что для Win32 API всегда применяется 32х разрядная flat–модель памяти. В этом случае модификаторы far и nearне применяются. Кроме того хендл, соответствующий типу unsigned int, становится 32х разрядным. Это на самом делеприводит к изрядным сложностям при переходе с платформы на платформу. Дело втом, что в Windows API хендл часто объединяется с какими–либо дополнительнымиданными и размещается в одном двойном слове, передаваемом в качестве параметрафункции или сообщения, а в Win32 такое уже не получится — хендл сам занимаетвсе двойное слово.
Кроме того, вWin32 API для работы с файлами используется опять–таки хендл,но уже не типа HFILE, а HANDLE. При этом нулевое значение по–прежнему являетсядопустимым и обозначает стандартное устройство вывода, а значение -1 — неверный хендл. Для обозначенияневерного хендла файла определен символ INVALID_HANDLE_VALUE,равный -1. Для других хендлов, кроме хендлов файлов,этот символ не применяется, так как для индикации ошибки применяется значение 0. При этом тип HFILE и символ HFILE_ERRORопределены также, как и в 16ти разрядных Windows — в виде 16ти разрядного целого числа. В принципе допустимо простоеприведение типов, однако в будущих реализациях Windows API ситуация может измениться, так как тип HANDLE соответствует 32х разрядному числу./>/>Венгерская нотация
При чтениитекстов C—программ и документации Вы обратите внимание на несколько странноенаписание имен переменных и функций. Например:
lpszFileName, wNameLength
РазработчикиWindows рекомендуют применять специфичные правила описания имен переменных,которые получили название “венгерская нотация” по национальности программистаCharles Simonyi из Microsoft, предложившего ее. Применение венгерской нотацииулучшает читаемость программ и уменьшает вероятность ошибки. Хотя, конечно, этодается ценой увеличения длины имен переменных.
Хорошимпрограммистским правилом является использование мнемонических имен переменных. Венгерская нотация предполагает не только применение мнемоники для определениясмысла переменной (как, например, FileSize),но и включение ее типа в имя. Например lpszFileNameобозначает дальний указатель на ASCIIZ[7]строку символов, содержащую имя файла.
Как видно изпримера, перед мнемоническим именем переменной пишется небольшой префикс,указывающий ее тип. Каким образом строится префикс? Из небольшой таблицы можнополучить представление об обозначении основных типов данных:обозначающий символ название обозначаемого типа пояснение c char символ by BYTE байт n int целое число x short координата или размер y short координата или размер i int целое число f, b BOOL логическая величина w WORD слово без знака h HANDLE хендл l LONG длинное целое со знаком dw DWORD длинное целое без знака e FLOAT число с плавающей запятой *fn функция s строка sz строка, оканчивающаяся '\0' (ASCIIZ) p * указатель на ... lp far* дальний указатель на ... np near* ближний указатель на ...
Зная этутаблицу легко самим понять или составить имена переменных в венгерской нотации.Даже если Вы не будете сами применять венгерскую нотацию при написаниипрограмм, то знать ее все равно надо, так как она принята во всей документации,сопровождающий Windows. К сожалению даже здесь разработчики оказались не совсемпоследовательны и Вам придется столкнуться в документации с некорректным (сточки зрения приведенной таблицы) применением венгерской нотации.
Так, вкачестве примера можно привести название поля cbWndExtra в структуре WNDCLASS. В данном случае префикс cb расшифровывается как Count of Bytes./>
Структура приложенияWindows
Итак, еще разпосмотрим на приложение 1a.cpp и вспомним, что надо сделать для написанияприложения:
· написать оконнуюфункцию;
· зарегистрироватьэту функцию в Windows;
· создать окно,принадлежащее данному классу;
· обеспечить работуприложения, обрабатывая поступающие окну сообщения.
Врассматриваемом нами примере выполняются все эти действия. Исходный кодсодержит следующие функции: WinProc,init_instance и WinMain. Здесь WinProcявляется оконной процедурой, init_instanceрегистрирует класс окон (оконную процедуру), а WinMain создает окно и обеспечивает работу всего приложения.
Можно короткорассмотреть основную последовательность действий, происходящих в системе призапуске приложения. Пока, что бы не вникать в сложности, ограничимся обзоромработы 16ти разрядногоприложения.
1. Операционная система загружаетисполняемый файл в память и выделяет требуемые для первоначальной загрузкиресурсы.
2. Управление передается специальнонаписанному разработчиками компиляторов startup–коду,который инициализирует приложение, получает необходимую информацию (как,например, командная строка, хендл копии приложения и пр.), запускаетконструкторы статических объектов.
3. startup–код вызывает функцию WinMain,передавая ей полученные от операционной системы данные. Функция WinMainразрабатывается для каждого приложения.
4. WinMain обычно осуществляет регистрацию класса окон.
5. Далее WinMain создает и отображает главное окно приложения.
6. WinMain обеспечивает функционирование приложения, организуя циклобработки сообщений. В этом цикле приложение извлекает поступающие к немусообщения, выполняет их предварительную обработку и затем передает ихокну–получателю (то есть вызывает необходимую оконную процедуру).
7. Оконная процедура выполняет обработкусообщений, направленных окну, обеспечивая реакцию задачи на действияпользователя.
8. Приложение работает, покапользователь не закроет главное окно этого приложения. В момент закрытия этогоокна оконная процедура принимает специальные меры для завершения циклаобработки сообщений, организованного WinMain.
9. Когда цикл обработки сообщений завершается,WinMain продолжает исполнение своего кода,выполняя, при необходимости, деинициализацию и уничтожение созданных объектов,после чего завершает работу.
10.Возвратуправления из функции WinMain происходитопять–же в промежуточный exit–код,созданный разработчиками компиляторов. Он запускает деструкторы статическихобъектов, деинициализирует приложение и возвращает управление в систему.
11.Системаосвобождает оставшиеся занятыми ресурсы, закрепленные за этим приложением.
Теперь, передтем как перейти к рассмотрению непосредственно кода приложения, надо сделатьпоследнее замечание. Windows API разрабатывался тогда, когда систем виртуальнойпамяти на персональном компьютере еще не существовало. Первые версии Windowsмогли работать на XT с 640K оперативнойпамяти. Из–за очень ограниченного объема памяти приходилось идти на различныеухищрения. Так один из способов экономии памяти связан с тем, что кодприложений обычно не изменяется. Если запустить две копии одного и того–жеприложения (скажем, два Notepad’а для редактирования двух разных файлов), токод этих приложений будет одинаковым. В этом случае его можно загружать толькоодин раз, но для необходимости как–то различать разные копии приложения ивозникло понятие хендл копииприложения (instancehandle, HINSTANCE). /> Функция WinMain
Обычнаяпрограмма на C (C++) содержит так называемую главную процедуру main. При создании программ для Windows тоже необходимоописать такую процедуру, правда она называется WinMain и имеет другие аргументы:
int PASCAL WinMain( HANDLEhInstance, HANDLE hPrevInstance, LPSTR lpszCmdLine, int nCmdShow )
{
// ...
}
Описаниеглавной функции ключевым словом PASCALуказывает на применение соглашений языка Pascal при передаче аргументов ивызове функции (так делается в большинстве функций Windows, потому что вызовpascal–декларированной функции осуществляется чуть быстрее и занимает меньшеместа, чем C–декларированной).
Рассмотрим ееаргументы:
HANDLE hInstance — этот параметр является хендлом,указывающим конкретную копию приложения. Знание этого хендла потребуется длясвязи тех или иных данных с конкретной копией.
HANDLEhPrevInstance —описывает хендл предыдущей копии приложения. Если данная копия является первой,то эта переменная содержит NULL.Использование этой информации несколько специфично:
Во–первых, Windows связывает некоторые данные с конкретной копиейприложения (например: экспортированные функции, окна и пр.). При связываниинеобходимо указывать хендл копии приложения.
Внимание: при использовании C++ иногда удобно описать статическийобъект. Однако в этом случае может потребоваться информация о hInstance для конструктора статического объекта. По страннойпричине мы ее не можем узнать до вызова WinMain— эта информация известна с самого начала (еще до вызова каких–либоконструкторов): startup–код в самых первых инструкцияхобращается к процедуре INITTASK, котораявозвращает системную информацию, в том числе hInstance. После этого hInstance копируется в статическую переменную, используемую startup– и exit– кодом, однако эта переменная является локальной (?!) инедоступна для остальных модулей приложения. Причина такого поступка со стороныразработчиков компиляторов остается непонятной.
Во–вторых, некоторые операции должны выполняться только при запускепервой копии приложения, а все остальные копии должны игнорировать эти операцииили выполнять вместо них другие. Для пояснения этого обратим внимание наоконную функцию. Когда мы создаем приложение, то мы описываем специфичныеоконные функции и регистрируем классы окон. После запуска первой копии (покаона активна) эти классы окон известны Windows. Значит при запуске последующихкопий нам не надо их регистрировать.
В–третьих, иногда нам надо получить данные от предыдущей копииприложения (например, если наши приложения организуют обмен данными междусобой). С помощью hPrevInstance мыможем сделать это (только в Windows API, в Win32 API это не получится)[8].
В–четвертых, 32х битовые приложения Win32 API всегда предполагают, чтозапущена только одна копия приложения (так как в виртуальном адресномпространстве приложения кроме нее других приложений, в том числе копий, ненаходится). При этом hInstance указываетначальный адрес загрузки модуля (и для большинства приложений он совпадает), а hPrevInstance всегда равен NULL.
LPSTRlpszCmdLine — каки обычная C–программа, приложение Windows может получать командную строку.Параметр lpszCmdLine является указателем на этустроку.
int nCmdShow — этот параметр указывает, в какомвиде должно быть изображено окно приложения. Для описания значений этойпеременной существует целый набор #define’ов,начинающихся с префикса SW_.Например, значение nCmdShow равное SW_SHOWMINNOACTIVE указывает на то, что окнодолжно быть отображено в минимизированном состоянии, а значение SW_SHOWNORMAL указывает на необходимость отображения окна внормальном состоянии. Пользователь может указать, в каком виде показыватьглавное окно приложения, настраивая характеристики ярлыка (shortcut)./>/> Регистрация классаокон
Послеаргументов функции WinMain мы начнемрассматривать непосредственно тело этой процедуры. В самом начале мы должнызарегистрировать класс нашего окна, убедившись в том, что это первая копияприложения. Для этого мы должны заполнить структуру WNDCLASS и передать ее функции RegisterClass:
WNDCLASS WC;
if ( !hPrevInstance ) {
WC.style= NULL;
WC.lpfnWndProc= WinProc;
WC.cbClsExtra= NULL;
WC.cbWndExtra= NULL;
WC.hInstance= hInstance;
WC.hIcon= LoadIcon( NULL, IDI_APPLICATION );
WC.hCursor= LoadCursor( NULL, IDC_ARROW );
WC.hbrBackground= GetStockObject( WHITE_BRUSH );
WC.lpszMenuName= NULL;
WC.lpszClassName= «Hello application»;
if ( !RegisterClass( &WC ) ) return NULL;
}
Эта операцияобычно выполняется в отдельной процедуре (init_instance в 1a.cpp) так как структура WNDCLASS используется однократно, только для вызова функции RegisterClass, после чего ее можно не держатьв стеке.
Структура WNDCLASS содержит следующие поля:
Поле style содержит комбинацию CS_xxx констант, задающих некоторые характеристики класса.Часто этот параметр равен NULL.Для более подробного знакомства с этим полем рекомендуется посмотреть описаниефункции RegisterClass и возможные стили класса.
Например,стиль CS_HREDRAW говорит о том, что окно должноперерисовываться целиком при изменении его горизонтального размера, CS_VREDRAW — при изменении вертикального размера,CS_DBLCLK — окно будет реагировать на двойныенажатия на клавишу мыши. Особенно надо отметить стиль CS_GLOBALCLASS. Обычно зарегистрированный класс используетсятолько лишь данным приложением (и всеми его копиями), но недоступен для другихприложений. Стиль CS_GLOBALCLASSразрешает использование класса другими приложениями.
Если Вызахотите объединить несколько стилей класса, используйте оператор ПОБИТОВОЕ ИЛИ, например: CS_HREDRAW|CS_VREDRAW|CS_DBLCLK.
lpfnWndProc является указателем на оконнуюфункцию. Вы не можете указать его 0, этафункция должна быть написана Вами.
cbClsExtra, cbWndExtra. При регистрации класса окна Windows создаетспециальный блок, содержащий информацию о классе; В некоторых случаях бываетудобно включить в эту структуру свои данные (их смогут использовать все окна,принадлежащие этому классу — даже окна разных приложений).
Для этогоWindows может зарезервировать специальное добавочное пространство в этом блоке,размер этого пространства указывается параметром cbClsExtra. Это пространство может быть использовано вамипо своему усмотрению, поэтому вы должны задать его размер. Если Вы несобираетесь использовать это пространство, укажите его размер 0 (NULL).
Позже, присоздании окна, Windows создаст другой блок, описывающий окно, и в нем выделитдополнительное пространство размером cbWndExtraбайт. Оно также предназначено для использования Вами. Если оно Вам нетребуется, укажите размер 0 (NULL).В таких полях удобно хранить данные, уникальные для каждого окна — скажем,хендл редактируемого файла, если это окно редактора.
/>
Рисунок 2. Структуры данных, используемые Windows для описания окон.
Вы можете использоватьпри желании двенадцать функций для чтения/записи данных, находящихся вструктурах описания окна и класса:
UINT GetWindowWord( hWnd, nIndex );
UINT SetWindowWord( hWnd, nIndex, wNewValue );
LONG GetWindowLong( hWnd, nIndex );
LONG SetWindowLong( hWnd, nIndex, dwNewValue );
int GetWindowText( hWnd, lpWinName, nMaxCount);
int GetWindowTextLength( hWnd );
void SetWindowText( hWnd, lpszWinName );
“Текст окна”(...WindowText...) является обычно заголовкомокна, а для окон специального вида (например, кнопки) — текстом, написанном вэтом окне.
UINT GetClassWord( hWnd, nIndex );
UINT SetClassWord( hWnd, nIndex, wNewValue );
LONG GetClassLong( hWnd, nIndex );
LONG SetClasslong( hWnd, nIndex, dwNewValue );
int GetClassName( hWnd, lpClassName, nMaxCount );
Поле hInstance, конечно, содержит хендл копииприложения, регистрирующего данный класс окон. Он будет использоваться Windowsдля связи оконной функции с копией приложения, содержащего оконную функцию.
Следующие триполя (hIcon, hCursor,hbrBackground) пояснить будет посложнее — дляэтого надо будет разобраться с GDI (hbrBackground)и ресурсами (hIcon, hCursor),что будет сделано в соответствующих разделах. Поэтому пока что мы сделаемнесколько замечаний и рассмотрим простейший случай.
hIcon — это хендл пиктограммы (иконки),которая будет выводиться вместо окна в минимизированном состоянии. Функция LoadIcon находит соответствующую пиктограммуи возвращает ее хендл. В нашем примере LoadIconиспользует стандартную пиктограмму (первый параметр равен NULL), которая используется по умолчанию для представленияприложения (второй параметр равен IDI_APPLICATION).
hCursor является, аналогично hIcon, хендлом курсора. Это курсор мыши, который будетотображаться при перемещении мыши через окно. Требуемый курсор загружаетсяфункцией LoadCursor, применение которой похоже нафункцию LoadIcon. Для стандартных курсоров существуютследующие имена: IDC_ARROW, IDC_CROSS, IDC_IBEAM, IDC_ICON,IDC_SIZE, IDC_SIZENESW, IDC_SIZES,IDC_SIZENWSE, IDC_SIZEWE, IDC_UPARROWи IDC_WAIT. Наиболее часто употребляемый курсорIDC_ARROW.
Третий хендл— хендл кисти — hbrBackground. Применение кисти существенноотличается от пиктограммы и курсора, поэтому она задается иным способом.Функция GetStockObject возвращает хендл заранеесозданного Windows стандартного объекта. Мы используем GetStockObject для получения хендла “белой кисти” WHITE_BRUSH. Помимо хендлов кистей этафункция может возвращать хендлы других объектов. В примере 1a.cpp функция GetStockObject не применялась — вместо хендлакисти разрешено указывать специальную константу, обозначающую системный цвет (впримере — COLOR_WINDOW). Для того, что бы система моглаотличить хендл от цвета, требуется, что бы к нему была прибавлена 1 (0означает неверный хендл, являясь в то же время корректным номером цвета).
Следуетотметить, что кисть WHITE_BRUSH вовсене обязательно имеет белый цвет. Просто эта кисть обычно используется длязакраски фона окна, часто это действительно белый цвет, но он может бытьизменен при настройке цветов Windows.
Указатель настроку lpszMenuName. Если Ваше окно имеет связанноес ним меню, то Вы можете указать имя этого меню для его автоматическогоиспользования. Как это делается мы рассмотрим несколько позже. В данном примерепредполагается, что меню нет — lpszMenuNameравен NULL.
Последнийпараметр lpszClassName является именем регистрируемогокласса окна. Рекомендуется включать в это имя название задачи, так как этоуменьшает вероятность совпадения имени данного класса с уже зарегистрированнымклассом другого приложения.
ВНИМАНИЕ! Если Вы применяете русские символы в именах меню, класса идр., то Вам надо быть очень осторожными — стандартные шрифты нелокализованныхдля России версий Windows 3.x не содержат русских символов, и, помимо этого,компиляторы могут не воспринимать русский текст, особенно некоторые его буквы(как, например, букву “я”, код которой равен 255). Дополнительно могут встретиться осложнения из–за того,что среда программирования в Windows может использовать собственные нерусифицированныешрифты, или из–за того, что DOS–программы используют иную кодировку русскихсимволов, чем Windows–приложения (если Вы используете какие–либо DOS–средствадля разработки программ, либо Ваше приложение будет использоватьDOS–программы).
Если функция RegisterClass вернула не 0, то класс успешно зарегистрирован. Этот зарегистрированныйкласс будет существовать до тех пор, пока приложение активно, или пока Вы невызовете функцию
UnregisterClass( lpszClassName,hInstance )
дляуничтожения данного класса. Если вы собираетесь вызвать функцию UnregisterClass, то надо убедиться что нет ни одного окна,принадлежащего данному классу. Эта функция обычно применяется для глобальныхклассов (со стилем CS_GLOBALCLASS)./> Создание и отображениеокна
Послерегистрации класса мы можем создавать окна. Делается это с помощью процедуры CreateWindow. Эта функция создает окно ивозвращает его хендл. Если создание окна почему–либо невозможно, товозвращается значение NULL. Рассмотримпараметры этой функции:
HWND CreateWindow(
lpszClassName, lpszWindowName, dwStyle,
nX, nY, nWidth, nHeight,
hWndParent, hMenu, hInstance,lpParam
);
lpszClassName задает имя класса окна. По этомуимени Windows находит описание класса окон и, соответственно, оконную функцию,которая будет вызываться для обработки сообщений, посылаемых окну. Такоймеханизм связывания окна с функцией (через имя класса) позволяет определятьоконную функцию в одном приложении, а использовать в другом.
lpszWindowName задает заголовок окна. Этотзаголовок размещается в верхней части окна. В некоторых случаях этот текстиспользуется иначе — например, если окно — «кнопка», то это текст накнопке, или это может быть текст под пиктограммой, если окно минимизировано.
Параметр dwStyle содержит битовые флаги, задающиенекоторые характеристики окна, как–то тип рамки, наличие заголовка, кнопоксистемного меню, минимизации, максимизации и т.д. Признаки окна называютсястилями и описаны в windows.h спрефиксом WS_. Вы должны объединять отдельные стили спомощью операции ПОБИТОВОЕ ИЛИ: WS_CAPTION|WS_SYSMENU.
Возможныхстилей окна очень много, мы их будем рассматривать их по мере надобности. Длясправок надо обратиться к описанию функции CreateWindow. Некоторые комбинации признаков, описывающиечасто применяемый стиль окна, заранее определены как отдельные константы. Так,например, стиль WS_OVERLAPPEDWINDOW описывает«обычное» перекрывающееся окно, имеющее кнопку системного меню, двекнопки максимизации и минимизации, заголовок окна и «широкую» рамку —для возможности изменять размер окна.
/>
Рисунок 3. Основные элементы окна
nX, nY эти параметры задают позициюверхнего левого угла окна относительно экрана или, для дочернего окна,относительно верхнего левого угла внутренней области родительского окна.Позиция задается в пикселях. Вместо конкретных чисел можно подставить CW_USEDEFAULT — в этом случае Windows будетопределять позицию сам.
nWidth, nHeight — ширина и высота окна в пикселях.Вместо этих величин Вы можете указать CW_USEDEFAULT,для того, что бы Windows самостоятельно определил их.
hWndParent этот параметр является хендломродительского окна. Если Вы указали NULL,то данное окно не является дочерним (используемым)[9],а если Вы задали хендл другого окна, то вновь создаваемое окно будет дочерним(используемым) по отношению к указанному.
hMenu обычно задает хендл меню,используемого окном. Однако здесь есть несколько тонкостей:
· если меню описанопри регистрации класса, и этот параметр равен NULL, то будет использоваться меню, описанное в классе.
· если и прирегистрации класса меню не задано, и данный параметр равен NULL, то окно будет создано без меню, но Вы все равносможете подключить меню к данному окну позже.
· если данное окноявляется дочерним, то этот параметр задает не хендл меню, а индекс дочернегоокна. Вы должны назначить всем дочерним окнам одного родительского разныеиндексы.
hInstance, как и раньше, является хендломкопии приложения, использующего данное окно. Обратите внимание на то, что такойже хендл при регистрации класса указывает на копию приложения, содержащуюоконную функцию, а здесь хендл указывает на копию приложения, использующегоданное окно (и, в Windows API, может отличаться).
lpParam. Как правило, этот параметр равен NULL. Он используется сравнительно редко,обычно если окно является окном MDI–интерфейса (Multiple Document Interface) — о MDI смотри в разделе,посвященном оконным системам. Этот указатель передается оконной функции приобработке сообщения, информирующего о создании окна, и используетсянепосредственно самой оконной функцией. Так Вы можете передать оконной функциитребуемые для создания окна данные через этот параметр.
В некоторыхслучаях при описании стиля окна необходимо задавать дополнительныехарактеристики, которые не входят в состав параметра dwStyle. Так, например, Вы можете создать окно, котороевсегда находится поверх всех остальных окон (always on top, topmost).Для этого используется дополнительное двойное слово стиля (extended style) и, соответственно, другая функциядля создания окна:
HWND CreateWindowEx(
dwExStyle, lpszClassName, lpszWindowName, dwStyle,
nX, nY, nWidth, nHeight,
hWndParent, hMenu, hInstance, lpParam
);
Эта функцияотличается только наличием еще одного параметра dwExStyle, задающего дополнительные стили окна. Для заданияэтих стилей используются мнемонические имена с префиксом WS_EX_..., например WS_EX_TOPMOSTили WS_EX_TRANSPARENT.
Обычно послесоздания окна оно остается невидимым, однако мы можем указать на необходимостьотображения окна во время работы функции CreateWindowс помощью стиля окна WS_VISIBLE.
Однако в WinMain не рекомендуется создавать сразувидимое окно. Это связано с тем, что приложение может быть запущено в начальноминимизированном состоянии. Как надо отобразить окно мы можем узнать черезаргумент nCmdShow функции WinMain, но использовать его функцией CreateWindow сложно. Поэтому окно создается невидимым изатем отображается в требуемом нам виде:
ShowWindow( hWnd, nCmdShow );
UpdateWindow( hWnd );
Функция ShowWindow указывает окну, в каком виде онодолжно быть отображено на экране. В функции WinMain параметр nCmdShowберется из переданных аргументов — он определяет, каким образом пользовательхочет запустить это приложение. Во время выполнения функции ShowWindow на экране отображается рамка и заголовок окна внужном месте и требуемого размера.
Во времявыполнения функции UpdateWindowотображается внутренняя область окна. Определимся, что внутренняя область окна иногданазывается клиентной(client), а рамка, заголовок, или заменяющая все пиктограмманазываются внешней областью,обрамлением (frame)или неклиентной (non–client) областью окна. Для отображениявнутренней области окна приложение получает специальное сообщение, обработчиккоторого выполняет требуемое рисование./>/> Цикл обработкисообщений
Сейчас нампридется обзорно рассмотреть механизм передачи сообщений в Windows 3.x (реальноон гораздо сложнее, особенно в Windows–95 или Windows NT). Для начала следуетвыделить некоторый источниксообщений. Им может являться какое–либо устройство ввода(например, клавиатура, мышь), специальные устройства (типа таймера), либокакое–либо работающее приложение. Сообщения, полученные от источника сообщений,накапливаются в очередисообщений (message queue). Далее сообщения будут извлекаться из очереди ипередаваться для обработки конкретной оконной процедуре.
Дляизвлечения из очереди используется специальный цикл обработки сообщений (message loop),разрабатываемый для каждого приложения. В этом цикле сообщения извлекаются изочереди, определяется, какое окно (какая процедура обработки сообщений) должноэто сообщение получить, при необходимости выполняются специальные действия, итолько затем сообщение передается конкретной оконной процедуре. И такой процессработает все время, пока работает приложение.
Процесспомещения сообщения в очередь и его извлечения из нее никак жестко не увязан повремени. Более того, может случиться так, что посланное сообщение вообще небудет обработано, например, если окно–получатель будет уничтожено прежде, чемуспеет его обработать.
/>
Рисунок 4. Обработка посланных сообщений в Windows
Например,когда вы нажимаете на клавишу, генерируется аппаратное прерывание. Клавиатурныйдрайвер Windows обрабатывает это прерывание и помещает соответствующеесообщение в очередь сообщений. При этом указывается, какое окно должно получитьданное сообщение.
Этот процессназывается посылкой(post) сообщений,так как посылка сообщения напоминает посылку письма: посылающий сообщениеуказывает адресата, отправляет сообщение и больше о нем не беспокоится.Отправитель не знает, когда точно его сообщение получит адресат. Такой способобработки сообщений часто называется асинхронным.
Извлечениесообщений из очереди приложения и направление их соответствующим окнамосуществляет цикл обработки сообщений, обычно входящий в функцию WinMain. Этот процесс выполняется в несколькоприемов:
· сообщениевыбирается из очереди с помощью функции GetMessageили PeekMessage
· затем сообщениетранслируется с помощью функции TranslateMessage[10](одно сообщение может порождать последовательность других или заменяться, как,например, происходит для сообщений клавиатуры WM_KEYDOWN). Часто трансляция состоит из вызова более чемодной функции, сюда могут добавляться специальные средства трансляцииакселераторов и немодальных диалогов (об этом позже).
· и только послеэтого оно направляется окну с помощью функции DispatchMessage (это называется диспетчеризацией)
Эти функцииобразуют цикл обработки сообщений, так как после завершения обработки одногосообщения приложение должно приготовиться к обработке следующего. Циклзаканчивается только при завершении работы приложения.
MSG msg;
while ( GetMessage(&msg, NULL, NULL, NULL ) ) {
TranslateMessage( &msg );
DispatchMessage( &msg );
}
Это самыйпростой вид цикла обработки сообщений. В реальных приложениях он более сложный.Все три функции, вызываемые здесь, принадлежат Windows. Назначение их должнобыть понятно. Требуется добавить несколько замечаний о функции GetMessage. Эта функция имеет следующие аргументы:
BOOL GetMessage( lpMsg, hWnd, uMsgFilterMin, uMsgFilterMax);
lpMsg указывает на структуру MSG, в которую будет записано полученноесообщение. Если очередь сообщений пуста, то GetMessage передает управление оболочке, так что та можетначать обработку сообщений другого приложения.
Какие жеданные передаются одним сообщением?
typedef struct tagMSG {
HWND hwnd; // хендл окна-получателя
UINT message; // номер сообщения WM_...
WPARAM wParam; // параметр сообщения
LPARAM lParam; // параметр сообщения
DWORD time; // время поступления сообщения
POINT pt; // координаты сообщения (для сообщениймыши)
} MSG;
Поле message структуры MSG задает номер сообщения, посланного системой.Интерпретация параметров сообщения wParamи lParam зависит от самого сообщения. Для этогонадо смотреть описание конкретного сообщения и обрабатывать параметрысоответствующим образом. Так как в системе определено огромное количестворазных сообщений, то для простоты использования применяются символические именасообщений, задаваемыми с помощью #defineв заголовочном файле. В качестве примера можно привести сообщения WM_CREATE, WM_PAINT, WM_QUIT.
hWnd указывает хендл окна, сообщения длякоторого будут выбираться из очереди. Если hWnd равен NULL,то будут выбираться сообщения для всех окон данного приложения, а если hWndуказывает реальное окно, то из очереди будут выбираться все сообщения,направленные этому окну или его потомкам (дочерним или используемым окнами, илиих потомкам, в том числе отдаленным).
uMsgFilterMin и uMsgFilterMax обычно установлены в NULL. Вообще они задают фильтр для сообщений. GetMessage выбирает из очереди сообщения,номера (имена) которых лежат в интервале от uMsgFilterMin до uMsgFilterMax.Нулевые значения исключают фильтрацию.
Функция GetMessage возвращает во всех случаях, кромеодного, ненулевое значение, указывающее, что цикл надо продолжать. Только водном случае эта функция возвратит 0— если она извлечет из очереди сообщение WM_QUIT.Это сообщение посылается только при окончании работы приложения.
Послезавершения цикла надо сделать совсем немногое — освободить память от техобъектов, которые создавались во время работы приложения (если они ещесуществуют). Некоторые объекты, которые уничтожаются автоматически, можно неосвобождать — это сделает Windows. Таков, например, зарегистрированный намикласс окон.
И остаетсяеще одно дело: так как WinMain возвращаетрезультат, то мы должны вернуть какое–либо значение. В Windows принято, чтовозвращаемое значение является параметром wParam сообщения WM_QUIT,завершившего цикл обработки сообщений. Таким образом мы пишем:
return msg.wParam;/>Оконная процедура
Пока мырассмотрели только функцию WinMain,причем она будет без существенных изменений сохраняться и в последующихпримерах. Теперь мы должны написать оконную функцию. Строго говоря, описать еелучше перед WinMain — тогда не надо описывать еепрототип.
И еще однозамечание: после вызова функции RegisterClass,регистрирующей данную оконную процедуру, вы не должны вызывать ее напрямую —это приведет к ошибке. Вызывать эту функцию может только Windows. Позже мыузнаем, почему это так и как можно ее вызвать самим.
Оконнаяфункция должна быть декларирована следующим образом (в случае Win32 API ключевое слово _exportможет быть пропущено, подробнее об описании оконных функций см. в разделе,посвященном диспетчеру памяти):
LRESULT WINAPI _export proc( HWNDhWnd, UINT uMsg, WPARAM wParam, LPARAM lParam )
{
// ...
}
В качествеаргументов мы получаем параметры переданного сообщения. Обычно оконные функцииоформляются примерно по такой схеме:
LRESULT WINAPI _export proc( HWND hWnd, UINT uMsg,WPARAM wParam, LPARAM lParam )
{
// описание внутренних переменных
switch ( uMsg ) {
case WM_...:
// обработка нужного сообщения
break;
// обработка другихсообщений...
default:
return DefWindowProc( hWnd, uMsg, wParam, lParam );
}
return 0L;
}
Главнымэлементом является конструкция switch,которая позволяет написать обработку каждого отдельного сообщения, полученногоокном. В объектных библиотеках эти функции берет на себя базовый интерактивныйобъект, позволяющий связать определенные методы класса с получаемымисообщениями.
Для упрощениянаписания оконной функции Windows предлагает специальную функцию
LRESULT DefWindowProc( hWnd, uMsg, wParam, lParam );
Эта функцияреализует стандартную обработку сообщений, что позволяет описать окно, имеющеезаданные нами стили и свойства и чистую внутреннюю область. Поэтому вы должныопределять обработку только тех сообщений, которые вы хотите обрабатыватьнестандартным образом, а все остальные передавать этой процедуре.
Сообщения,которые получает окно, информируют его о самых разных событиях, происходящих сэтим окном, с приложением в целом, с самой системой и так далее. Сейчас мырассмотрим четыре сообщения, которые применяются в рассмотренном примере, и ихобработку./>Сообщение WM_CREATE
Самым первыммы рассмотрим сообщение WM_CREATE.Это сообщение посылается окну в тот момент, когда оно создается. Реальнымсозданием окна ведает функция CreateWindow,а не обработчик сообщения WM_CREATE.Вы в этот момент должны инициализировать свои переменные, выполнить необходимыенастройки, создать требуемые объекты и прочее. При создании окно еще невидимо —поэтому Вы можете менять его размеры, устанавливать в нужное положение, менятьцвета не опасаясь мелькания на экране. Часто здесь создаются необходимыеструктуры и выделяются требуемые окном ресурсы.
Стандартнаяобработка этого сообщения необязательна — функция DefWindowProc просто возвращает 0 в ответ на это сообщение.
Параметр wParam не используется, а параметр lParam содержит указатель[11]на структуру CREATESTRUCT. В этой структуре передаетсяосновная информация о создаваемом окне.
typedef struct tagCREATESTRUCT{
void FAR* lpCreateParams; // указатель на дополнительные данные,
// переданный как параметр lpParamв вызове
// функции CreateWindow илиCreateWindowEx
HINSTANCE hInstance; // хендл копии приложения, создавшей окно
HMENU hMenu; // хендл меню (или NULL, если нет)
HWND hwndParent; // хендл родительского окна (или NULL)
int cy, cx; // размеры окна
int y, x; // положение окна
LONG style; // стили окна
LPCSTR lpszName; // заголовок окна
LPCSTR lpszClass; // имя класса, к которому принадлежит окно
DWORD dwExStyle; // расширенные стили окна (см.CreateWindowEx)
} CREATESTRUCT;
Поля x, y,cx и cyв момент обработки сообщения WM_CREATEмогут быть еще не определены. При необходимости получить информацию о размереили положении окна надо пользоваться функциями GetWindowRect или GetClientRect,которые возвращают корректный результат
Возвращаемоеобработчиком значение:
не 0 — возникла ошибка, окно надо уничтожить(далее, при уничтожении, будет получено сообщение WM_DESTROY), функция CreateWindowили CreateWindowEx вернет NULL.
0 — окно успешно создано; функция CreateWindow или CreateWindowEx вернет хендл окна.
/>Сообщения WM_DESTROY и WM_QUIT
Еще односообщение, интересующее нас — WM_DESTROY.Это сообщение посылается окну в момент его уничтожения. Это одно из последнихсообщений, которое получает окно — можно считать, что после этого оно уже несуществует, причем в момент получения этого сообщения окно уже невидимо. В этотмомент вы можете выполнить необходимые операции по освобождению выделенных присоздании окна ресурсов. Оба параметра сообщения WM_DESTROY не используются.
Как и WM_CREATE сообщение WM_DESTROY является информационным — реальное уничтожениеокна осуществляется функцией DestroyWindow,а не обработчиком сообщения WM_DESTROY.Независимо от способа обработки этого сообщения окно будет уничтожено.
Если вашаоконная функция обслуживает главное окно приложения, то в момент уничтоженияокна вы должны принять меры для завершения работы всего приложения в целом. Дляэтого вы должны послать сообщение WM_QUIT,при извлечении которого из очереди закончится цикл обработки сообщений. Еслиэтого сообщения не послать, то цикл обработки сообщений продолжит работу дальшепосле закрытия всех имеющихся окон. На практике это означает что приложениевообще перестанет получать сообщения и “зависнет” на функции GetMessage, которая будет ждать, пока не придет новоесообщение. В случае Windows 3.xесть единственный способ удалить такое приложение — перезапуск всего Windows (вWindows–95 или Windows NT такое приложение можно снять самому с помощьюменеджера задач). Сообщение WM_QUITпосылается с помощью функции:
void PostQuitMessage( nExitCode);
Параметр nExitCode будет передан как wParam сообщения WM_QUITи позже возвращен функцией WinMainв качестве параметра завершения.
Обработчиковдля сообщения WM_QUIT писать не надо, так как:
Во–первых, функция GetMessage,получив это сообщение просто вернет FALSE,так что цикл обработки сообщений будет закончен без трансляции идиспетчеризации этого сообщения.
Во–вторых, это сообщение адресовано не окну, а приложению. То есть,даже если воспользоваться функцией PeekMessageдля извлечения сообщения WM_QUITиз очереди (в отличие от GetMessageэто получится), оно не будет отправлено никакому окну, так как хендлокна–получателя равен NULL./> Сообщение WM_PAINT
Последнеерассматриваемое нами сообщение — WM_PAINT.Оба параметра сообщения WM_PAINTне используются. С этим сообщением нам придется разбираться подробнее. Раньше,когда мы обсуждали разделение экрана между разными задачами, говорилось о том,что в Windows невозможно полностью виртуализовать всю работу с экраном — тоесть содержимое невидимой в данной момент части окна для Windows остаетсянеизвестным.
Представимсебе, что невидимая часть окна стала видимой (например, вследствие перемещениядругого окна) — тогда возникает необходимость восстановить ее образ. Windows незнает, что там должно быть отображено, следовательно отображением можетзаниматься только само приложение.
Для этоговводится специальное сообщение WM_PAINT,которое посылается окну тогда, когда часть окна (или все окно) нуждается вперерисовке. Часто перерисовка окна занимает значительное время, а обработку сообщенийрекомендуется выполнять как можно быстрее; поэтому Windows особым образомобрабатывает WM_PAINT, что бы задержки в обработке этогосообщения не оказывали такого влияния на работу системы.
Получив этосообщение, процедура обработки сообщений должна узнать, какую часть окна надоперерисовать, выполнить перерисовку, и сообщить Windows, что данная часть окнатеперь корректна. Вы можете перерисовывать не только эту часть окна, а все окносразу — Windows проследит, что бы реальные изменения происходили только в тойчасти, которая нуждается в перерисовке.
Приложениедолжно быть построено таким образом, что бы, получив сообщение WM_PAINT, оно могло перерисовать требуемую часть окна. Тоесть приложение должно знать, что, где и как должно быть нарисовано в окне, таккак в любой момент может потребоваться многократная перерисовка какой–либочасти окна./> Основы рисования в окне
Еще развспомним, каким образом появляется сообщение WM_PAINT: это происходит когда все окно, или только егочасть становятся видимыми. Та часть окна, которая нуждается в перерисовке,является неверной — ее содержимое не соответствует требуемому. Эта частьполучила название неверныйпрямоугольник (invalid rectangle).
Соответственно,после перерисовки она перестает быть неверной — то есть становится верным прямоугольником (valid rectangle).
Теперь стоитвыяснить при каких условиях возникают неверные прямоугольники и как онииспользуются. Для этого посмотрим на те ситуации, когда неверный прямоугольникможет возникнуть, а когда нет:
Неверный прямоугольник возникает, если:
· скрытая областьокна (например, закрытая другим окном) становиться видимой.
· область окна“прокручивается” (с помощью функций ScrollWindowили ScrollDC). В этом случае та часть окна, вкоторой должен появиться новый, невидимый ранее, текст, объявляется невернымпрямоугольником.
· окно изменяетразмер. Обычно это приводит к появлению неверных прямоугольников только приувеличении размера окна и только на дополнительно появившейся поверхности.Однако, если при регистрации класса окна Вы указали стиль CS_HREDRAW или CS_VREDRAW,то все окно целиком будет рассматриваться как неверное.
· Вами вызвана однаиз функций:
·
void InvalidateRect( hWnd, lpRect, fErase );
void InvalidateRgn( hWnd, hRgn, fErase );
Параметрыэтих функций: hWnd — хендл окна, содержащего прямоугольник(или регион), нуждающийся в перерисовке. lpRect— указатель на структуру типа RECT,описывающую прямоугольник, или hRgn —хендл нужного региона. fErase — логическая(TRUE,FALSE) величина, указывающая, нужно ливосстанавливать фон неверной области, или нет.
Неверный прямоугольник не появляется при перемещении курсора мыши илипиктограмм через область окна — Windows самостоятельно сохраняет и восстанавливаетокно под курсором (или пиктограммой).
В некоторыхслучаях неверные прямоугольники могут создаваться, а могут и не создаваться.Это может быть, если часть окна становиться видимой после использованиядиалогов, выпадающих меню или после вызова функции MessageBox, являющейся частным случаем стандартного диалога.В этих случаях Windows может сохранять текст под появляющимся окном ивосстанавливать его позже. Однако это делается не всегда и зависит от разныхфакторов — размера диалога или меню, режима его создания и других факторов./>Еще о сообщении WM_PAINT
Допустим, чтов результате какого–либо события появился неверный прямоугольник. Что с нимпроисходит дальше?
Windows,обнаружив неверный прямоугольник, принадлежащий какому–либо окну, ставит вочередь приложения сообщение WM_PAINT,адресованное этому окну. Оконная процедура, получив это сообщение,перерисовывает требуемую часть окна (или большую), после чего сообщает Windowsо том, что неверный прямоугольник исправлен.
Если этого несообщить Windows, то будет считаться, что прямоугольник остался неверным, иснова будут генерироваться сообщения WM_PAINTэтому окну. Поэтому очень важно не оставлять после WM_PAINT неверных прямоугольников.
Таким образомосуществляется поддержание окна в требуемом состоянии. Однако перерисовкаявляется медленным процессом (к тому же многие приложения перерисовываютбольшую часть окна, чем надо), а неверные прямоугольники могут появляться внескольких экземплярах, иногда перекрывающихся, до того, как приложение начнетобрабатывать WM_PAINT.
Поэтому вWindows сообщение WM_PAINT трактуетсяне как сообщение, а скорее как состояние: если в очереди приложения естьсообщение WM_PAINT, то окно надо перерисовать. А если вэто время появляются новые неверные прямоугольники, то новых сообщений WM_PAINT в очередь не попадает, а простоновый неверный прямоугольник объединяется со старым. Обычно в результатеобъединения возникает некоторая неверная область сложной формы[12].
Сообщение WM_PAINT является низкоприоритетным. Чем отличаетсянизкоприоритетное сообщение от нормального? Тем, что, если в очереди приложенияесть только сообщения с низким приоритетом, то Windows может передатьуправление другому приложению, имеющему в очереди сообщения нормальногоприоритета (в Windows только два сообщения являются низкоприоритетными: WM_PAINT и WM_TIMER).
Эти двеособенности позволяют Windows сравнительно легко переносить продолжительноерисование окна, хотя при этом перерисовываемая поверхность зачастуюприближается к размерам всего экрана. Специфичная обработка сообщения WM_PAINT является причиной того, что вWindows рекомендуется сосредоточивать все операции по выводу в окно вобработчике сообщения WM_PAINT.
Если Вам надосамим осуществить какой–либо вывод в окно (понятно, что такая необходимостьвозникает и при обработке других сообщений), то настоятельно рекомендуетсяследующий метод:
· все операции повыводу сосредотачиваются в обработчике сообщения WM_PAINT.
· когда у Васвозникает необходимость осуществить вывод в окно, Вы вызываете функцию InvalidateRect, которая маркирует нужнуючасть окна как неверную, и, следовательно, в очередь приложения попадаетсообщение WM_PAINT.
· при этомсообщение только лишь оказывается в очереди, но оно не попадает на обработкунемедленно, реальный вывод произойдет несколько позже. Если Вам надо именносейчас осуществить вывод в окно, то добавьте вызов функции UpdateWindow. Эта функция немедленно передаст оконнойпроцедуре сообщение WM_PAINT для егообработки.
Конечно,осуществление вывода при обработке других сообщений не является ошибкой, хотяделать этого не стоит, особенно если рисование сколько–нибудь сложное. Ксожалению, всегда следовать этому правилу почти невозможно, однако нужны вескиепричины для его нарушения.
При обработкесообщения WM_PAINT стоит придерживаться несколькихправил, специфичных для этого сообщения:
Первое: Никогда не передавайте сообщение WM_PAINT непосредственно окну. Для этого существует функция UpdateWindow, которая генерирует этосообщение, если неверный прямоугольник существует.
Второе: Рекомендуется начинать обработку сообщения WM_PAINT с функции:
HDC BeginPaint( hWnd,lpPaintstruct );
изаканчивать функцией
void EndPaint( hWnd,lpPaintstruct );
где lpPaintstruct — указатель на структуру типа PAINTSTRUCT. Эти функции выполняют нескольконужных дополнительных действий для обработки сообщения WM_PAINT, в том числе объявляют внутреннюю область окнакорректной.
Наконец: Если все же Вы не используете BeginPaint...EndPaint, то обязательно объявляйтеперерисованную область верной с помощью функции ValidateRect или ValidateRgn.
void ValidateRect( hWnd, lpRect );
void ValidateRgn( hWnd, hRgn );
Параметрыэтих функций: hWnd — хендл окна, содержащего прямоугольник(или регион), нуждающийся в перерисовке. lpRect— указатель на структуру типа RECT,описывающую прямоугольник, или hRgn —хендл нужного региона./>Контекст устройства
Рассматриваясообщение WM_PAINT и неверные прямоугольники мыобсудили основные правила осуществления вывода в Windows. Теперь обзорнопознакомимся с правилами осуществления вывода графической информации в окно.
В первом женаписанном приложении 1a.cpp мы увидели, что функция вывода текста TextOut использовала не хендл окна, а такназываемый хендл контекстаустройства (HDC, device context). Зачем он понадобился? и почему быне использовать вместо него хендл окна?
Дело в том,что все средства вывода в Windows относятся не к менеджеру окон, а к графическому интерфейсу устройств(GDI). GDIпредставляет собой библиотеку функций для выполнения графического вывода наразличных устройствах, не только на дисплее. При создании GDI стремилисьсделать работу с устройствами независимой от самих устройств. Так, одни и те жефункции, осуществляющие вывод текста, рисование линий и т.д. могут применятьсядля работы с дисплеем, принтером, плоттером и другими устройствами.
Для этогопришлось ввести дополнительное понятие — контекст устройства, идентифицируемыйего хендлом. Все функции вывода взаимодействуют с этим контекстом. Часто дажеговорят, что функции GDI рисуютна контексте, а не непосредственно на устройстве. Контекстустройства описывает так называемые атрибуты контекста и непосредственно характеристики устройства, на которомосуществляется вывод.
Атрибуты контекста устройства независимы от самого устройства. Онихарактеризуют то изображение, которое будет рисоваться. В число атрибутоввходят кисти, перья, цвет текста, цвет фона и многое другое. Так, назначивконтексту устройства текущее перо вы определяете толщину, цвет и стиль(сплошная или прерывистая) тех линий, которые будут отображаться последующимивызовами функций, рисующих эти линии. При необходимости нарисовать линиюдругого стиля вы должны поменять текущее перо. Аналогично определяются кисть,используемая для закраски фона фигур, шрифт, применяемый при выводе текста, имного других атрибутов контекста устройства.
Информация об устройстве описывает непосредственновозможности самого графического устройства. Функции GDI взаимодействуют сустройством опосредованно — через контекст и через драйвер этого устройства.Благодаря наличию информации об устройстве одна и та же функция GDI способна осуществлять вывод на любоеустройство, для которого существует необходимый драйвер. Так, вы можетевыбирать перо или кисть любого цвета, а GDI примет меры, что бы необходимое изображение былополучено как на цветном дисплее, так и на черно–белом принтере или плоттере.
Как правиловы можете не заботиться о характеристиках устройств, на которых реально будетработать приложение. Однако, при разработке сложных приложений, которые могутшироко распространяться, вы должны все–таки позаботиться о некоторых вопросахсовместимости — например, при назначении цветов стоит их подбирать так, что быпри переходе на черно–белое оборудование изображение осталось бы различимым.Часто лучшим решением является возможность легкой настройки программыпользователем под его конкретную аппаратуру.
Обычно надопредусматривать следующие варианты:
· если приложениеосуществляет вывод только в окно, то надо учитывать возможность работы:
· с разнымразрешением — от 640x400, 640x480 и до часто встречающихся 1024x768, 1280x1024.Следует подчеркнуть, что в некоторых случаях возможны режимы работы мониторатолько с 400 строками развертки, а не с 480, как считают обычно. Было бы оченьжелательно, что бы даже в таком режиме все диалоги и окна умещались на экране.
· с разным числомцветов — от 8 и до более чем 16 миллионов цветов (16 777 216).Чисто монохроматические дисплеи (черный и белый) уже практически невстречаются, а вот дисплеи дешевых переносных компьютеров часто дают только 8–16 градаций серого; причем различимостьцветов может быть невелика.
· с разныминастройками системной палитры; включая контрастные и энергосберегающие режимы(иногда применяются для переносных компьютеров).
· если приложениеспособно выводить на принтер, то надо иметь в виду, что вместо принтера можетоказаться плоттер, который хорошо рисует линии, но совершенно не может выводитьрастровых изображений, либо АЦПУ, которое способно только печатать текст. Передвыводом рисунков следует проверять возможности данного устройства[13]./>Работа с контекстом устройства
Так как длярисования на каком–либо устройстве необходимо получить хендл контекста этогоустройства, то естественно надо рассмотреть основные правила работы сконтекстом и средства для его получения. Здесь будут обзорно рассмотреныосновные правила работы с контекстом, для получения более подробной информацииследует обратиться к разделу, посвященному работе с графикой.
Собственносуществует две группы методов получения контекста устройства — создание и получение контекста устройства. Разницасвязана с тем, что создание и, позже, уничтожение контекста устройства занимаетнекоторое время. Если вы собираетесь осуществлять вывод на принтер, то этизатраты времени ничтожно малы по сравнению со всем временем печати. Однако,если вы собираетесь только осуществлять рисование в окне (которое можетобновляться очень часто), то даже сравнительно быстрая операция созданияконтекста, повторенная многократно, займет значительное время. Поэтому вWindows существует несколько заранее созданных контекстов, соответствующихдисплею. При выводе в окно контекст создавать не надо, надо воспользоватьсяодной из функций, возвращающих такой заранее заготовленный контекст устройства.
Более того, вWindows методы, создающие контекст, предназначены для работы с устройством целиком,а методы, возвращающие уже существующий — с окном. Разница заключается вприменении системы координат, связанной с контекстом. В первом случае системакоординат связана с верхним левым углом устройства, а во втором случае — сверхним левым углом внутренней области окна[14].
Сейчас мысосредоточимся только на двух способах получения контекста устройства и нанекоторых общих правилах применения этого контекста. С первым способом мы ужепознакомились — он основан на функциях BeginPaintи EndPaint, а второй на функциях GetDC и ReleaseDC:
HDC GetDC(hWnd );
void ReleaseDC( hWnd, hDC );
Оба способавозвращают заранее заготовленный контекст устройства, однако делают это поразному. Функции BeginPaint и EndPaint предназначены для обработки сообщения WM_PAINT. В других случаях пользоваться этимифункциями не рекомендуется. Это связано с тем, что:
· эти функцииобъявляют окно корректным
· возвращаемыйконтекст устройства соответствует даже не внутренней области окна, а тольконеверной области. То есть система координат контекста будет связана с верхнимлевым углом окна, а вот область рисования будет соответствовать только невернойобласти. При попытке рисовать на таком контексте все, что не попадает вневерную область, не будет рисоваться. Это сделано для некоторого ускоренияперерисовки окна.
· функция BeginPaint дополнительно принимает меры кзакраске фона той кисточкой, которая была задана при регистрации класса окна.Это позволяет при разработке обработчика сообщения WM_PAINT не заботиться о закраске фона окна.
Втораярассматриваемая пара функций (GetDC, ReleaseDC) этих операций не делает, но затоона возвращает контекст для всей внутренней области окна, а не только дляневерной области. При необходимости использовать именно эти функции вобработчике сообщения WM_PAINT необходимосамостоятельно принять меры к закраске фона и к объявлению окна корректным.
Всерассматриваемые нами функции для получения контекста устройства приводились впаре — функция для получения и функция для освобождения. Это связано с тем, чтоприменение полученного контекста устройства должно быть ограничено обработкой только текущего сообщения.Оставлять такой контекст занятым нельзя, так как в системе зарезервированотолько 8 таких контекстов; если контекст неосвободить, то несколько одновременно отображаемых окон (а в Windows почтивсегда одновременно работает несколько приложений), могут занять все контекстыи при попытке что–то нарисовать в следующем окне возникнет ошибка.
В процессерисования вы будете постоянно изменять атрибуты контекста — выбирать новыекисти, перья, изменять цвета и режимы рисования и так далее. Все эти изменениядействуют только в то время, пока контекст существует. Как только контекстосвобождается (или уничтожается, если он был создан), то все изменения,сделанные в его атрибутах, пропадают. Контекст, который вы получаете,практически всегда[15] настроен стандартнымобразом.Системыкоординат Windows
При рисованиина контексте устройства вам придется задавать координаты выводимого текста иизображаемых фигур в определенной системе координат. Система координат,связанная с контекстом устройства — система координат GDI, определяет правила преобразованиякоординат x и yвсеми функциями GDI. Вы можетесами определять различные масштабные коэффициенты по обеим осям, задаватьположение начала отсчета этой системы координат, либо использовать одну изстандартных систем координат GDI. Вглаве, посвященной выводу графических изображений на контексте устройства, мыподробнее познакомимся с этими системами координат.
Сейчас женадо выделить несколько основных систем координат, применяемых Windows, и уточнить области применения этихсистем координат.
Перваярассматриваемая система координат — система координат менеджера окон. В этой системе ось x направлена по горизонтали направо, ось y — по вертикали вниз. Начало отсчета (0,0) связана либо с верхним левым угломдисплея, либо с верхним левым углом внутренней области родительского окна. Ценаодной единицы этой системы координат равна одной единице устройства (пикселю).Для пересчета координат из системы отсчета, связанной с внутренней областьюокна в систему отсчета, связанную с экраном (и наоборот) используются функции ClientToScreen и ScreenToClient.
Очевидно, чтотакая система отсчета удобна далеко не во всех случаях. Например, если окносодержит текст, либо отдельные элементы с текстом, то размеры текста (илиэлементов) будут зависеть от используемого шрифта. В такой ситуации было быудобно для задания размеров и положения текста применять не единицы устройства,а величины, производные от размера символов. Пример таких окон — панелидиалога. На таких панелях обычно располагается значительное количество кнопок,флажков, списков, просто статического текста и пр., а, так как одно и то жеприложение и одними и теми же панелями диалогов может работать на самых разныхкомпьютерах с различными видеоадаптерами, мониторами, системными шрифтами иверсиями Windows, то и размеры панелей должныопределяться пропорционально размерам шрифта, используемого этими панелями.
При описаниипанелей диалогов используется своя собственная система координат — система координат панели диалога.В этом случае начало отсчета помещается в верхний левый угол внутренней областипанели диалога[16], ориентация осейкоординат сохраняется прежней, а в качестве единиц отсчета применяют по оси x — одну четвертую от средней ширины символашрифта, а по оси y — одну восьмую от высоты шрифта. Обычно этивеличины примерно соответствуют одному пикселю. Дополнительную информацию можнополучить, например, из описания функции MapDialogUnits.
Сложность вприменении этой системы координат связана с понятием средней ширины символа. Дело в том, чтоподавляющее большинство шрифтов является пропорциональными — то есть каждыйсимвол шрифта имеет свою ширину. Для вычисления «средней ширины» применяютширины символов алфавита, взвешенные с учетом частотности встречи символов вобщелитературном тексте. Как правило — английском. Все это может привести кнекоторым ошибкам в задании положения и размеров при использовании иного языка,чем английский, особенно если при этом используются нестандартные шрифты. В Windows–95 легко наблюдать этот эффект,изменяя с помощью панелиуправления (control panel) размеры используемых шрифтов и наблюдая заотображением стандартных диалогов./> Как построить приложение
Послеразработки исходного текста приложения возникает необходимость егоскомпилировать и построить файлобраза задачи (выполняемыйфайл, exe–файл).В книге не будет даваться подробное описание конкретных методов построенияприложений — эту информацию стоит почерпнуть из руководств по вашемукомпилятору. В зависимости от применяемой среды разработки приложений и даже отспособа ее применения процесс построения приложений будет изменяться.
В данномразделе будут рассмотрены только основные шаги, из которых состоит построениеприложения для Windows и исходные файлы, которые могут для этого потребоваться.Все среды построения приложений будут так или иначе реализовывать эти шаги иразработчику надо будет ориентироваться в тех исходных файлах, которые будутприменяться или создаваться в процессе генерации выполняемого файла./> Как строилиWindows–приложения в самом начале
Предварительноначнем с истории, времен ранних версий Windows (до 3.x). В те времена, когдаWindows только начинал развиваться, компиляторы не содержали практическиникаких средств поддержки процесса создания приложений для Windows. В этомслучае практически вся работа сваливалась на программистов, которые должны былиразрабатывать дополнительные текстовые файлы, необходимые для построенияприложений.
Основные сложностибыли связаны с двумя особенностями приложений для Windows:
· приложениеподдерживало динамическую загрузку библиотек. В случае DOS все необходимое дляработы приложения находилось в файле образа задачи, а в случае Windows большоеколичество функций, необходимых приложению, содержится в динамическизагружаемых библиотеках (часто это компоненты операционной системы). В такихслучаях говорят, что приложение импортирует(import) функции.В то же время приложение должно предоставлять часть своих функций для того, чтобы их вызывала операционная система (как, скажем, оконная процедура). В этихслучаях говорят, что приложение экспортирует(export) функции.
· приложениесодержало дополнительные данные, называемые ресурсами приложения (не путать сресурсами компьютера или операционной системы). В виде ресурсов приложениямогли выступать пиктограммы, курсоры, меню, диалоги и пр., использовавшиесяприложением. Эти ресурсы включались в специальном формате в файл образа задачи.
ОбычнаяDOS–задача не могла делать таких вещей. Поэтому в Windows был принят новыйформат выполняемого файла, а для нормального построения образа задачи пришлосьизменить стандартный компоновщик(linker) так,чтобы он мог использовать информацию об экспортируемых и импортируемых функцияхи собирать выполняемые файлы в новом формате. Соответственно, такой компоновщикнуждался в информации об экспортируемых и импортируемых функциях, а также онекоторых дополнительных параметрах Windows–приложения. Чтобы предоставить этуинформацию компоновщику надо было написать специальный текстовой файл — файл описания модуля (def–файл).
В файлеописания модуля перечислялись имена экспортируемых функций, имена импортируемыхи библиотеки, содержащие функции, подлежащие импорту, задавался размер стека,давалось короткое описание приложения и пр. Это было не слишком удобно, так какпри вызове какой–либо новой функции из Windows API (а их более 1000), необходимо было добавлять ее имя в файл описаниямодуля.
В техслучаях, когда приложение нуждалось в собственных ресурсах, необходимо былоотдельно описать эти ресурсы в специальном текстовом файле описания ресурсов (rc–файле). Вместе скомпиляторами до сих поставляется специальный компилятор ресурсов RC.EXE,который воспринимает файл описания ресурсов, компилирует его и создает файл ресурсов приложения(res–файл). Затемтот–же компилятор ресурсов может объединить уже построенный выполняемый файл сфайлом ресурсов приложения.
Таким образомпостроение приложения состояло из следующих этапов:
· разработкаисходных текстов — .c, .cpp, .h, .hpp, .rcи .def файлы;
· компиляцияисходного текста программы — из .c и .cpp получаем .obj (иногда .lib);
· компиляцияресурсов приложения — из .rcполучаем .res;
· компоновкаприложения — из .obj и .libполучаем .exe;
· встраиваниересурсов приложения в выполняемый файл — из .exe и .resполучаем рабочий .exe.
Конечно, еслисобственных ресурсов приложение не имело, то построение задачи несколькоупрощалось. Но и при этом необходимость перечислять все экспортируемые иимпортируемые функции в файле описания приложения была крайне неудобной./>Следующий этап
Очевидно, чтонеобходимость перечислять кучу имен функций в файле описания приложения никогоне приводила в восторг. Поэтому на следующем этапе была включена поддержкаWindows–приложений непосредственно в компиляторы[17].Для этого было добавлено ключевое слово _export(иногда __export), которое применяется при описанииэкспортируемых функций непосредственно в тексте C–программы. Для таких функцийкомпилятор включает в объектный файл специальную информацию, так чтокомпоновщик может правильно собрать выполняемый файл. Так, например, былосделано в первом примере для оконной процедуры:
LRESULT WINAPI _export proc( ...
Помимо экспортированных функций, в файлеописания приложения было необходимо перечислять все импортированные функции. Аэтот список был самым большим (Windows экспортирует более 1000 функций, которые могут импортироваться приложениями).Для решения этой задачи был изменен формат библиотек объектных файлов, так чтопоявилась возможность описать имена функций, экспортированных другими модулями.Так компоновщик задачи может определить, какие функции импортируются и из какихмодулей.
В этом случаесоставление файла описания модуля стало тривиальным — длинные списки именэкспортированных и импортированных функций исчезли, а остальная информациязаполнялась, как правило, однообразно для разных приложений. Компоновщики задачполучили возможность обходиться вообще без файлов описания модуля — при егоотсутствии подставлялись стандартные значения. С этого момента простейшееприложение можно было создать, написав только один файл с исходным текстомпрограммы.
Библиотеки сосписками функций, которые можно импортировать из Windows (то естьэкспортированных компонентами Windows) входят в состав всех компиляторов.Иногда может возникнуть необходимость использования нестандартного компонента(или собственной динамически загружаемой библиотеки), для которыхсоответствующей библиотеки с ссылками нет. В таком случае можно воспользоватьсяспециальным приложением — implib.exe — которое входит в состав большинствакомпиляторов (если его нет в составе компилятора, то значит его возможностиреализованы в каком–либо другом инструменте, как, например, wlib.exe в WatcomC/C++). Implib позволяет по имеющемуся файлу динамически загружаемой библиотеки(.dll) или файлу описания проекта модуля библиотеки (.def), содержащему списокэкспортированных функций, построить библиотечный файл (.lib), с ссылками нафункции библиотеки.
Первоначально,стремясь максимально уменьшить время загрузки модулей в память, приэкспортировании функций им присваивались уникальные номера (назначаютсяразработчиком, либо просто по порядку). Конкретная функция однозначноопределяется именем экспортирующей библиотеки динамической загрузки иидентификатором функции. Для задания идентификаторов экспортируемых функцийиспользуется файл описания модуля. Однако использование номеров вместо именявляется не слишком удобным для человека, поэтому в Windows используются обаметода — функции доступны как по их идентификаторам, так и по их именам. ДляWindows API общепринятым методом считается использование идентификаторов —Microsoft следит за тем, что бы все документированные функции сохраняли своиидентификаторы. А в Win32 API предполагается использование имен; более того,Microsoft не гарантирует, что идентификаторы документированных функций не будутизменяться.
Построениеприложения сохранило в себе все шаги, однако создание файла описания приложениястало необязательным. При разработке приложений для Win32 API с файламиописания модуля практически не приходится иметь дело, он используется в оченьредких случаях для построения динамически загружаемых библиотек. В настоящеевремя и в случае Windows API, и в случае Win32 API этот файл создается толькоесли необходимо обеспечить нестандартную компоновку модуля, имеющего, например,разделяемые сегменты данных.
Современныекомпиляторы и системы поддержки проектов фактически остались в рамкахрассмотренного порядка построения приложения. Небольшие изменения реализованы вразных компиляторах независимо друг от друга. Так, иногда включение файларесурсов приложения в выполняемый модуль выполняется не компилятором ресурсов,а непосредственно компоновщиком; в других случаях специальные редакторыресурсов позволяют обойтись без построения файла описания ресурсов (.rc), асоздать непосредственно файл ресурсов приложения (.res). Особенно часто это делаетсяв системах визуального программирования.
Если выиспользуете какую–либо систему поддержки проектов (Watcom IDE, MicrosoftDeveloper Studio, Borland IDE, Symantec IDE и пр.) — а скорее всего это именнотак — то вы должны только проследить за тем, что бы необходимые файлы быливключены в проект. Система сама отследит, как и когда должен использоваться тотили иной исходный файл.
Обычно впроект включаются следующие файлы:
· исходные текстына C и C++ — файлы с расширениями .c, .cpp, .c++;
· файл, содержащийресурсы приложения, как правило только один, либо .rc либо .res;
· файлы библиотек .lib, содержащие либо статически компонуемыемодули, либо ссылки на функции динамически загружаемых библиотек (частоотсутствуют). Стандартные библиотеки обычно присоединяются компоновщиком поумолчанию, поэтому их перечислять здесь не надо;
· дополнительныеобъектные файлы .obj — отдельно включаются очень редко;
· файл описаниямодуля .def, только один, часто только при желанииописать нестандартные параметры компоновки (см. ниже, в описании этого файла)./> Файл описания модуля(.def)
Файл описаниямодуля (приложения) содержит обычный текст, который можно написать любымтекстовым редактором. В качестве примера рассмотрим один из файлов описаниямодуля, использованный для построения приложения testapp.exe:
NAME TESTAPP
DESCRIPTION 'TESTAPP — test application'
EXETYPE WINDOWS
PROTMODE
STUB 'WINSTUB.EXE'
CODE LOADONCALL MOVEABLE
DATA MOVEABLE MULTIPLE
STACKSIZE 8192
HEAPSIZE 4096
В этом файлеотсутствуют списки экспортированных и импортированных функций, так какиспользовался компилятор для Windows 3.1, осуществляющий связывание симпортированными функциями с помощью библиотек. Сейчас мы рассмотриминформацию, которая может размещаться в этом файле.
Большинствокомпиляторов могут использовать собственные директивы, а также собственныерасширения для параметров, задаваемых в директивах, не описанные в общихруководствах (как, к примеру, директива PROTMODEв приведенном примере). Кроме того список возможных директив в файлах описаниямодулей для Windows API и для Win32 API различается.Windows API
Первая строкаобычно задает имя модуля. Если вы строите приложение, то первой должна стоятьдиректива NAME, а если динамически загружаемуюбиблиотеку — LIBRARY. Указание обоих директив считаетсянекорректным. Имя должно соответствовать имени файла, так для testapp.exe этастрока будет такой: NAME TESTAPP, а дляmydll.dll — LIBRARY MYDLL.
LIBRARY dllname
NAME exename
Обычноследующая строка — описание данного модуля. Она начинается с директивы DESCRIPTION, за которой следует произвольныйтекст, заключенный в апострофы:
DESCRIPTION ‘description text for module’
Следующая директива, если и присутствует, товсегда определяет, что данный модуль предназначен для работы в среде Windows(аналогичные файлы описания модулей могут применяться для OS/2).
EXETYPE WINDOWS
Еще дведирективы предназначены для задания размеров стека и кучи. Задание размерастека менее 5K приводит к тому, что Windows самувеличивает его до 5K. Задание размеракучи вообще абстрактно — главное, что бы не 0, так как Windows будет увеличивать кучу при необходимости(вплоть до наибольшего размера сегмента данных приложения — 64K). Размер кучи 0говорит о том, что она просто не используется..
HEAPSIZE size
STACKSIZE size
Оченьлюбопытная директива — STUB. О ней надорассказать чуть подробнее. Ранее было отмечено, что для Windows–приложений былразработан собственный формат выполняемого модуля. Очевидно, что попытказапустить такой модуль на старой версии DOS, который не рассчитан на такуювозможность, приведет к грубой ошибке, вплоть до зависания компьютера или порчиданных. Чтобы этого избежать, сделали так — Windows–приложение состоит из двухчастей:
· Первая частьпредставляет собой небольшое приложение MS–DOS, называемую заглушкой (stub). Обычно это приложение простопишет на экране фразу типа “This programmust be run under Microsoft Windows.”. Заголовок этой заглушки чуть изменен, чтобыWindows мог отличить DOS–приложение от Windows–приложения, но это изменениенаходится в неиспользуемой MS–DOS’ом части заголовка.
STUB ‘stubexe.exe’
Здесь stubexe.exe — имя приложения–заглушки(возможно полное имя, вместе с путем)
· Вторая часть —собственно код и данные Windows–приложения с собственным заголовком.
Разрабатываясобственную заглушку можно делать интересные приложения, работающие в DOS и вWindows. Скажем, собрать в один выполняемый файл DOS–версию приложения иWindows–версию.
Еще три директивыиспользуются для описания параметров сегментов кода и данных. Директива CODE задает характеристики сегментов кода, DATA — сегментов данных, а SEGMENTS позволяет описать характеристики для конкретногосегмента (в квадратные скобки [] заключены необязательные параметры, знак |разделяет возможные варианты; запись [FIXED|MOVEABLE]означает, что может быть указано либо FIXED,либо MOVEABLE, либо ничего). Более подробно охарактеристиках сегментов приложения см. в описании диспетчера памяти Windows3.x.
CODE [FIXED|MOVEABLE] [DISCARDABLE] [PRELOAD|LOADONCALL]
DATA [NONE|SINGLE|MULTIPLE][FIXED|MOVEABLE]
SEGMENTS
segname [CLASS ‘clsname’] [minalloc] [FIXED|MOVEABLE] [DISCARDABLE][SHARED|NONSHARED] [PRELOAD|LOADONCALL]
...
Могут бытьуказаны следующие параметры: FIXEDили MOVEABLE — сегмент фиксирован в памяти илиперемещаемый; если сегмент перемещаемый, то он может быть теряемым (DISCARDABLE). Параметры PRELOAD и LOADONCALLуказывают, когда сегмент должен быть загружен в оперативную память — при загрузкеприложения (PRELOAD) или при непосредственном обращении кнему (LOADONCALL). Параметры NONE, SINGLEили MULTIPLE применяются только для сегментовданных. NONE или SINGLE применяется для динамически загружаемых библиотек; NONE — библиотека не имеет сегмента данныхвообще, SINGLE — сегмент данных присутствует в памятив единственном экземпляре (динамические библиотеки загружаются только один раз,других копий не существует, все приложения ссылаются на одну библиотеку сединым сегментом данных). MULTIPLE— сегмент данных загружается для каждой копии приложения, применяется толькодля приложений.
Директива SEGMENTS описывает характеристики конкретныхсегментов; segname — имя сегмента, clsname — имя класса сегмента, minalloc — минимальный размер сегмента. SHARED или NONSHAREDсообщает, является ли сегмент разделяемым разными копиями приложения, либо нет.После одной директивы SEGMENTS можетразмещаться описание нескольких сегментов, по одному на каждой строке.
Еще две частоиспользуемых директивы — EXPORTSи IMPORTS. Они задают списки экспортированных иимпортированных имен функций, описание каждой функции находится на своейстроке. В обоих случаях надо учитывать следующую особенность — в Windowsразличают внешниеи внутренниеимена. Внутренние имена — это имена, использованные при разработке исходноготекста программы, те которые содержаться в тексте C–программы (или в объектномфайле). Внешние имена — имена используемые при экспорте/импорте, доступныедругим модулям. Внешнее и внутреннее имя могут не совпадать, поэтомупредусмотрена возможность задавать оба имени.
EXPORTS
exportname [=internalname] [@id] [RESIDENTNAME] [NODATA] [argsize]
...
В разделе EXPORTS перечисляются имена функций,экспортируемых данным модулем. Параметр exportnameзадает внешнее имя функции, под которым она будет доступна другим модулям,параметр internalname используется, если внешнее ивнутреннее имена различаются, @idзадает идентификатор функции, argsize— если указан, сообщает сколько слов в стеке занимает список аргументовфункции. Параметры RESIDENTNAME и NODATA используются крайне редко; RESIDENTNAME говорит о том, что имя функциидолжно размещаться в так называемом резидентном списке имен (который находитьсяв памяти постоянно после загрузки модуля), обычно имена размещаются внерезидентном списке, который загружается при необходимости. NODATA говорит о том, что функция использует сегмент данныхвызывающего модуля, а не экспортирующего (подробнее — при разговоре одиспетчере памяти).
IMPORTS
[internalname=] modulename.id
[internalname=] modulename.importname
Раздел IMPORTS задает соответствие внутренних именимпортируемых функций (internalname)функциям, экспортированным другими модулями. Возможно два метода связыванияимен — по идентификатору (первый пример), modulename — имя экспортирующего модуля, id — идентификатор; либо по именам (второй пример), importname — внешнее имя функции, под которымона была экспортирована другим модулем.Win32 API
Файл описаниямодуля в Win32 API используется для задания нестандартных характеристикдинамически загружаемых библиотек. Полное описание файлов описания модуля дляWin32 API надо смотреть в документации, сопровождающей применяемый компилятор.
Так как файлописания модуля используется для создания DLL, то первая директива — LIBRARY. Часто применяется также директива EXPORTS для задания идентификаторовэкспортируемых функций (обе — см. в описании файла описания модуля для WindowsAPI).
Для заданиянестандартных параметров отдельных сегментов используется директива SECTIONS, заменяющая прежнюю директиву SEGMENTS. Синтаксис директивы SECTIONS несколько иной, хотя допускается замена слова SECTIONS на SEGMENTS:
SECTIONS
secname [CLASS ‘classname’] [EXECUTE] [READ] [WRITE] [SHARED]
Послеуказания имени секции (сегмента) следует необязательное указание имени класса иатрибутов этой секции — разрешение на выполнение (EXECUTE), на чтение (READ),на запись (WRITE) и объявление секции разделяемой (SHARED) между разными копиями модуля(загруженными в разных адресных пространствах разных приложений!)./>/>/>
Дополнительныеразделы
В этомразделе будет рассказано о малоизвестном заголовочном файле — windowsx.h. В некоторых случаях разработчики его, конечно,используют, но редко больше чем на 5%от его возможностей. Этот заголовочный файл был разработан специально дляоблегчения контроля исходного текста программы. К сожалению, в большей частидокументации, сопровождающей компиляторы, этот файл вообще не описан, хотя существуетуже очень давно. Пожалуй впервые он описан в документации, сопровождающейMicrosoft Visual C++ 4.0 (Microsoft Developer Studio, раздел “SDKs|Win32SDK|Guides|Programming Techniques|Handling Messages with portable macros”).Однако даже там описаны только принципы его применения, но не дано подробноеописание всех его макросов. Как результат — при применении windowsx.h приходится постоянно обращаться к его исходномутексту./>Заголовочный файл WINDOWSX.H
В Windowsчасто бывает так, что одна и та же функция может работать с объектами разныхтипов. В таких случаях бывает трудно проследить корректность ее применения. Дляэтого в windowsx.h включено много макросов, которыепереопределяют вызовы таких функций, что бы текст получился более читаемым.Именно эту возможность windowsx.h иногдаприменяют на практике.
Напримерфункция DeleteObject может применяться для удалениямногих объектов GDI(Graphics Devices Interface)— карандашей, кистей, регионов и пр. По названию функции понять, какой–именнообъект она удаляет нельзя, поэтому при чтении исходного кода приходитсясосредотачиваться на параметрах этой функции. В windowsx.h определены макросы:
#define DeletePen(hpen) DeleteObject((HGDIOBJ)(HPEN)(hpen))
#define DeleteBrush(hbr) DeleteObject((HGDIOBJ)(HBRUSH)(hbr))
#define DeleteRgn(hrgn) DeleteObject((HGDIOBJ)(HRGN)(hrgn))
прииспользовании которых текст становится более читаемым и легче находятся ошибки.
При включениифайла windowsx.h в ваше приложение это надо делатьпосле включения основного файла windows.h:
#define STRICT
#include
#include
Для того, чтобы получить представление о возможностях windowsx.hрекомендуется посмотреть его исходный текст. В нем присутствуют следующиеразделы:
· макросы дляработы с функциями ядра (несколько макросов для работы с глобальной памятью)
· макросы дляработы с объектами GDI
· макросы дляработы с окнами (вызовы стандартных функций)
· распаковщикисообщений (самая большая часть)
· макросы дляработы с окнами стандартных классов (кнопки, списки и пр.)
· некоторые макросыдля оптимизации стандартной библиотеки времени выполнения
В процессерассмотрения соответствующих разделов мы часто будем обращаться к этомузаголовочному файлу. Здесь же мы детально познакомимся с использованиемраспаковщиков сообщений./> Распаковщикисообщений
Большая частьфайла windowsx.h предназначена для описания распаковщиков сообщений(message crackers).Так как в книге преимущественно будут приводятся фрагменты кода сиспользованием распаковщиков, то в этом месте мы с ними и познакомимся. При ихиспользовании придется постоянно заглядывать в исходный текст windowsx.h, так как в обычной документации распаковщики неописаны. По счастью этот файл хорошо структурирован и снабжен достаточнымикомментариями.
Для того, чтобы понять его назначение, вернемся к оконной процедуре. Основная ее часть —конструкция switch, которая нужна для обработки конкретныхсообщений. Обычно (кроме простейших примеров) этот switch разрастается до невообразимых размеров, так что егокрайне трудно прочитать (и не дай боже искать там ошибку). Вторым побочнымэффектом является то, что огромное количество переменных, используемых дляобработки разных сообщений, сосредотачиваются в одной процедуре, бесполезнозанимая место.
Вполнеочевиден выход — разнести обработку сообщений по отдельным функциям, которыебудут вызываться из процедуры обработки сообщений. Однако для каждого сообщенияпередаются свои данные, упакованные в двух параметрах — wParam и lParam.Иногда они не используются, иногда содержат какие–либо значения, иногда —указатели. Естественно, было бы удобным передавать в вызываемую функцию ужераспакованные параметры. Затруднение здесь вызывает то, что для Windows API идля Win32 API одни и те же данные одного и того же сообщения могут бытьупакованы по разному[18].
Приразработке windowsx.h это все было учтено (для WindowsAPI и для Win32 API распаковщики определяются по разному). Так, для каждогосообщения WM_xxx определен свой макрос с именем HANDLE_WM_xxx. Например, для сообщения WM_CREATE определен макрос:
HANDLE_WM_CREATE(hwnd, wParam,lParam, fn)
Параметрывсех макросов одинаковые, что позволяет передавать им непосредственно параметрысообщения (окно–адресат hwnd,параметры wParam и lParam), а также имя функции–обработчика fn. Этот макрос должен использоваться внутри конструкции switch для вызова нужной функции и передачией распакованных параметров. Например фрагмент следующего вида:
switch ( uMsg ) {
case WM_CREATE: return HANDLE_WM_CREATE(hWnd,wParam,lParam,fnOnCreate);
// ...
}
будет превращенкомпилятором в следующий фрагмент (подробнее см. исходный текст windowsx.h):
switch ( uMsg ) {
case WM_CREATE:
return ((fnOnCreate)((hWnd),(LPCREATESTRUCT)(lParam)) ?
0L: (LRESULT)-1L);
// ...
}
То есть прираскрытии макроса HANDLE_WM_xxxосуществляется распаковка параметров, вызов функции и анализ возвращаемогорезультата. Здесь, кстати, скрыта одна ловушка (по счастью крайне редкая):результат, возвращаемой функцией–обработчиком не всегда будет совпадать срезультатом, описанным в справочнике для данного сообщения. Случай с WM_CREATE именно такой — согласно описаниюобработчик WM_CREATE должен вернуть 0L, если все в порядке. А, как мы видим в приведенномфрагменте, функция, вызываемая распаковщиком, должна вернуть TRUE, то есть не 0,если все в порядке (распаковщик сам заменит TRUE на 0L).
Прирассмотрении этого примера возникает вопрос — а как должна быть описанафункция–обработчик, что бы распаковщик ее правильно вызывал? Ответ прост — всамом файле windowsx.h перед определением соответствующегомакроса приводится прототип этой функции. То есть нам надо сделать следующее:открыть windowsx.h, найти в нем строку, гдеопределяется распаковщик для WM_CREATE(это легко делается поиском) и посмотреть на приведенный там текст:
/* BOOL Cls_OnCreate(HWND hwnd, LPCREATESTRUCTlpCreateStruct) */
#define HANDLE_WM_CREATE(hwnd, wParam, lParam, fn) \
((fn)((hwnd), (LPCREATESTRUCT)(lParam))? 0L: (LRESULT)-1L)
#define FORWARD_WM_CREATE(hwnd, lpCreateStruct, fn) \
(BOOL)(DWORD)(fn) ((hwnd), WM_CREATE, 0L, \
(LPARAM)(LPCREATESTRUCT)(lpCreateStruct))
Описаниефункции Cls_OnCreate мы и ищем. Далее нам надо егопросто скопировать в наше приложение и исправить при желании имя функции.Единственное, что остается не слишком удобным — так это вызовмакроса–распаковщика — уж очень длинная строка получается. Для этого в windowsx.h содержится отдельный небольшоймакрос:
HANDLE_MSG( hWnd, uMsg, fn )
Используетсяон таким способом:
switch ( uMsg ) {
HANDLE_MSG( hWnd, WM_CREATE, Cls_OnCreate );
// ...
}
При этом онсам вставляет “case WM_xxx: return ...” и прочее. Важно следить, что бы в описании оконнойпроцедуры параметры wParam и lParam назывались именно так и не иначе. Делов том, что HANDLE_MSG при обращении к макросу HANDLE_WM_xxx указывает ему именно эти имена.
Чтобызакончить разговор о распаковщиках сообщений надо ответить только на двавопроса — зачем нужны макросы FORWARD_WM_xxx,определенные в том–же windowsx.h и какможно добавить распаковщики для каких–либо сообщений, там не определенных(например, нестандартных).
Рассмотренныепока макросы–распаковщики позволяли нам вызвать функцию–обработчик сообщения,имея для нее данные, упакованные в wParamи lParam. Однако иногда возникает другая необходимость— имея параметры функции передать какое–либо сообщение. Для этого предназначенымакросы FORWARD_WM_xxx. Для использования этихмакросов, необходимо, что–бы функция, получающая параметры сообщения, имеласледующий вид:
LRESULT proc( HWND hwnd, UINT uMsg, WPARAM wParam, LPARAMlParam );
Макросы FORWARD_WM_xxx получают в качестве параметровраспакованные данные (как и функция–обработчик), упаковывают их в параметрысообщения и вызывают указанную функцию. По счастью практически все функции,которые придется вызывать с помощью макросов FORWARD_WM_xxx (SendMessage,PostMessage, DefWindowProc и пр.) соответствуют приведенному описанию.
Например,сообщение WM_SETFONT посылается окну (стандартногокласса) для того, что бы назначить ему нужный шрифт. Параметры этого сообщенияследующие: wParam содержит хендл шрифта, а младшее словоlParam указывает, надо ли перерисовывать окно сразу после смены шрифта. Предположим,что ваше окно имеет дочернее окно, и вам хочется сделать так, чтобы при сменешрифта в вашем окне одновременно менялся шрифт в дочернем. Соответственно выдолжны включить в оконную процедуру обработку сообщения WM_SETFONT и в его обработчике передать такое–же сообщениедочернему окну.
void Cls_OnSetFont( HWND hwnd, HFONT hfont, BOOL fRedraw );
LRESULT WINAPI_export proc( HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam )
{
switch ( uMsg ) {
// ...
HANDLE_MSG( hWnd, WM_SETFONT, Cls_OnSetFont );
// ...
}
}
// ...
void Cls_OnSetFont(HWND hwnd, HFONT hfont, BOOL fRedraw )
{
HWND hwndChild = ...; // определение хендла дочернего окна
FORWARD_WM_SETFONT( hwndChild, hfont, fRedraw, SendMessage );
}
Здесь,кстати, можно было бы воспользоваться макросом SetWindowFont из того же windowsx.h. Этот макрос обращается к FORWARD_WM_SETFONT, как в рассмотренном примере, однакотекст при этом становится более читаемым:
void Cls_OnSetFont( HWND hwnd,HFONT hfont, BOOL fRedraw )
{
HWND hwndChild = ...; // определение хендла дочернего окна
SetWindowFont( hwndChild, hfont, fRedraw );
}
Добавлениесобственных распаковщиков не должно вызвать больших затруднений — достаточнотолько разработать реализации макросов HANDLE_WM_xxxи FORWARD_WM_xxx аналогично уже сделанному в windowsx.h./>Пример 1B —использование распаковщиков сообщений
Этот примериллюстрирует применение распаковщиков сообщений на примере простейшегоприложения. Фактически он соответствует слегка измененному примеру 1A, вкотором оконная процедура переписана для использования распаковщиков сообщений.Функция WinMain в этом примере осталась безизменений.
#define STRICT
#include
#include
#define UNUSED_ARG(arg) (arg)=(arg)
static char szWndClass[]= «test window»;
BOOL Cls_OnCreate(HWND hwnd, LPCREATESTRUCT lpCreateStruct )
{
UNUSED_ARG( hwnd );
UNUSED_ARG( lpCreateStruct );
return TRUE;
}
void Cls_OnPaint(HWND hwnd )
{
PAINTSTRUCT ps;
BeginPaint( hwnd,&ps );
TextOut( ps.hdc, 0, 0, «Hello, world!», 13 );
EndPaint( hwnd, &ps );
}
void Cls_OnDestroy(HWND hwnd )
{
UNUSED_ARG( hwnd );
PostQuitMessage( 0);
}
LRESULT WINAPI_export WinProc( HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam )
{
switch ( uMsg ) {
HANDLE_MSG( hWnd, WM_CREATE, Cls_OnCreate );
HANDLE_MSG( hWnd, WM_PAINT, Cls_OnPaint );
HANDLE_MSG( hWnd, WM_DESTROY, Cls_OnDestroy );
default: break;
}
return DefWindowProc( hWnd, uMsg, wParam, lParam );
}
static BOOLinit_instance( HINSTANCE hInstance )
{
WNDCLASS wc;
wc.style = 0;
wc.lpfnWndProc = WinProc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = hInstance;
wc.hIcon = LoadIcon( NULL, IDI_APPLICATION );
wc.hCursor = LoadCursor( NULL, IDC_ARROW );
wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
wc.lpszMenuName = NULL;
wc.lpszClassName = szWndClass;
return RegisterClass( &wc ) == NULL? FALSE: TRUE;
}
int PASCAL WinMain( HINSTANCEhInst, HINSTANCE hPrevInst, LPSTR lpszCmdLine, int nCmdShow )
{
UNUSED_ARG( lpszCmdLine );
MSG msg;
HWND hWnd;
if ( !hPrevInst ) {
if ( !init_instance( hInst ) ) return 1;
}
hWnd= CreateWindow(
szWndClass, // class name
«window header», // window name
WS_OVERLAPPEDWINDOW, // window style
CW_USEDEFAULT,CW_USEDEFAULT, // window position
CW_USEDEFAULT,CW_USEDEFAULT, // window size
NULL, // parent window
NULL, // menu
hInst, // current instance
NULL // user-definedparameters
);
if ( !hWnd ) return1;
ShowWindow( hWnd,nCmdShow );
UpdateWindow( hWnd );
while ( GetMessage(&msg, NULL, NULL, NULL ) ) {
TranslateMessage( &msg );
DispatchMessage( &msg );
}
return msg.wParam;
}/> Немного об объектах
Здесь мырассмотрим некоторые основные особенности реализации объектно–ориентированногопрограммирования в Windows. В последнее время получили огромное распространениебиблиотеки объектов для создания приложений в среде Windows. Особенно широкоони стали распространяться с развитием систем визуального программирования.Наибольшее распространение получили библиотеки объектов компаний
· Borland — Object Windows Library (OWL), поддерживается компиляторамиBorland C++ (рассматривается версия v2.5, сопровождающая компилятор BorlandC/C++ v4.5).
· Microsoft — Microsoft Foundation Classes (MFC), поддерживаетсянаибольшим количеством компиляторов, среди которых Microsoft Visual C++, WatcomC++, Symantec C++ и другие (рассматривается версия v4.0, сопровождающая VisualC/C++ v4.0).
Такиебиблиотеки достаточно многофункциональны и громоздки, размер исполняемогофайла, созданного с их помощью редко бывает меньше 300–400K. Конечно, при разработке больших систем,поддерживающих такие инструменты как OLE, DAO или WOSE, регистрирующих своисобственные типы файлов и т.д., использование этих библиотек может существенносократить время, необходимое для разработки приложения.
Этибиблиотеки объектов, хотя и имеют огромное количество различий, неизбежно имеюти много общего, что определяется платформой, на которой они работают —Windows. Для обеспечения эффективной работы приложений эти библиотеки вынужденыпредусматривать простой механизм доступа посредством методов объектов кфункциям API, что их неизбежно сближает между собой. Кроме того реализация ииерархия объектов в Windows неизбежно приводит к появлению чем–то сходнойиерархии классов в библиотеках ООП.
В этомразделе мы рассмотрим простейшее приложение в среде Windows, построенноесредствами ООП, причем все классы будут оригинальными — ни MFC, ни OWL неприменяется. Это сделано для того, что бы “извлечь” на поверхность некоторыеаспекты разработки классов для Windows–приложений. Здесь будут использоватьсясущественно упрощенные методы реализации объектов, по сравнению с “большими”библиотеками.
Возможно, чтов некоторых частных случаях использование такого подхода может оказаться иболее продуктивным, чем применение MFC или OWL. Особенно, если ваше приложениепохоже на простейшее “Hello, world!” (в этом случае, правда, еще удобнееможет быть обойтись совсем без классов)./> Особенности ООП вWindows
На самом делеWindows не является настоящей объектно–ориентированной средой. Хотя окно иможет быть названо объектом ООП, но лишь с достаточной натяжкой. Самоесущественное отличие окна в Windows от объекта ООП заключается в том, чтосообщение, обрабатываемое оконной функцией, во многих случаях не выполняетдействий, а является “информационным”, указывая на то, что над окномвыполняется та или иная операция какой–либо внешней функцией.
Поясним этона примере создания окна. В случае ООП для уничтожения объекта он долженполучить сообщение “destroy”, обработка которого приведет к его уничтожению. ВWindows сообщение WM_DESTROY невыполняет никаких функций по уничтожению окна. Оно только информирует окно отом, что в это время окно уничтожается средствами обычной функциональнойбиблиотеки, например посредством функции DestroyWindow.Вы можете вообще игнорировать это сообщение, возвращать любое значение,вызывать или не вызывать функцию обработки по умолчанию — окно все равно будетуничтожено.
С точкизрения реализации объектов это приводит к тому, что большая часть методовпредставлена в двух вариантах — один вызывает соответствующую функцию API, адругой вызывается при обработке соответствующего сообщения. Так в случаефункции DestroyWindow и сообщения WM_DESTROY для класса, представляющего окно, будетсуществовать два метода: метод Destroyи метод OnDestroy (названия методов условны, вреальных библиотеках они могут отличаться). Метод Destroy будет соответствовать вызову функции DestroyWindow, а метод OnDestroy — обработчику сообщения WM_DESTROY.
На этом, ксожалению, сложности не кончаются. Допустим, что вы хотите сделать так, что быокно не уничтожалось. На первый взгляд надо переопределить метод Destroy, что бы он не вызывал функцию DestroyWindow; при этом вызов метода Destroy не будет уничтожать окно. Однако всегораздо сложнее: окно по–прежнему может быть уничтожено прямым обращением кфункции API — DestroyWindow. Более того, стандартныеобработчики сообщений (то есть принадлежащие Windows, а не библиотеке классов)так и делают. Так стандартная обработка сообщения WM_CLOSE приводит к вызову функции DestroyWindow (а не метода Destroy). То есть для решения подобной задачи надо переопределитьвсе методы объекта и все обработчики сообщений, которые ссылаются насоответствующую функцию API. Задача фактически не решаемая — особенно с учетомнедостаточно подробной и точной документации.
Все этоприводит к тому, что для написания надежных приложений с использованиембиблиотек классов приходится очень хорошо представлять себе как работаетWindows, каким функциям или сообщениям API соответствуют те или иные методы, а,кроме того, как это реализовано в конкретной библиотеке./> Базовые классыприложения
Когдаразрабатывается обычное приложение на C или C++ без использования классов, тонадо разработать как функцию WinMain,определяющую работу приложения в целом, так и оконную процедуру, определяющуюреакцию окна на внешние воздействия. При применении объектов эти функциивозлагаются на методы классов. Естественно, что и в MFC, и в OWL существуютклассы, предназначенные как для описания приложения в целом, так и для описанияокон. Эти классы должны использоваться в качестве классов–предков, от которыхпорождаются классы, описывающие ваше приложение и его главное окно.
Классы,описывающие приложение, в конечном итоге берут на себя работу, выполняемуюфункцией WinMain, — они регистрируют специальный классокон в Windows, организуют создание и отображение главного окна приложения ицикл обработки сообщений, организуют инициализацию и создание необходимыхобъектов при запуске приложения и их уничтожение при завершении. Причем, таккак заранее не известно, как будут называться классы, разработанныепользователем, и сколько и каких окон надо будет создавать, то библиотекипредлагают только лишь базовые классы, на основе которых надо разработать своисобственные классы для описания приложения и окна.MFC
В библиотекеMicrosoft Foundation Classes для описания приложения используются следующиеклассы:
/>
Рисунок 5. Классы MFC, описывающие окно и приложение.
Данная версияMFC рассчитана на разработку многопотоковых приложений для Win32. Это наложилосвой отпечаток на иерархию классов — в качестве одного из базовых выступаеткласс CWinThread, описывающий отдельный поток. Итолько на его основе построен класс CWinApp,описывающий приложение (в Win32 существует понятие основного потока, который обслуживаетфункцию WinMain, именно он и будет представленобъектом класса CWinThread, на основе которого порождаетсяобъект класса CWinApp).OWL
В этойбиблиотеке иерархия классов несколько иная — библиотека рассчитана наразработку 16ти разрядныхприложений Windows 3.x, не поддерживающую потоков.
/>
Рисунок 6. Классы OWL, описывающие окно и приложение.Окна ООП и окна Windows
Прииспользовании библиотек классов надо как–то связывать экземпляры объектов,описывающих окна в приложении, с описанием того–же окна в Windows. Здесь надоучитывать, что, с одной стороны:
· существуют методыклассов, соответствующие вызову функций API;
· существуют методыклассов, соответствующие обработчикам сообщений;
а, с другойстороны:
· существуют окна,созданные с помощью классов ООП;
· существуют окна,созданные другими приложениями, модулями а также стандартными средствамиWindows, не имеющими отношения к применяемой библиотеке.
Так,например, диалог “Открыть Файл” является стандартным диалогом Windows. Онсоздается и выполняется посредством вызова одной функции API — FileOpen. Эта функция сама, независимо от приложения и егобиблиотеки классов, создает необходимые окна и работает с ними. Однако упрограммиста может возникнуть необходимость как–то взаимодействовать с этимдиалогом в процессе его работы.
Можновыделить четыре возможных ситуации, с которыми придется столкнуться во времяработы приложения:
1. должна быть вызвана функция API дляокна, реализованного как объект класса;
2. должна быть вызвана функция API дляокна, не являющегося объектом класса;
3. окно, реализованное как объекткласса, получает сообщение — то есть надо вызвать соответствующийметод–обработчик этого сообщения;
4. окно, не являющееся объектом класса,получает сообщение.
Случаи 1 и 2 решаются сравнительно просто — среди членов–данных классадолжен присутствовать член, задающий хендл окна в Windows. В таком случае вызовфункций API, нуждающихся в хендле, происходи элементарно. Небольшой нюанссвязан с окнами, не являющимися объектами класса. Например, диалоги, включаястандартные и их элементы управления — кнопки, флажки и прочее, часто создаютсякак окна, принадлежащие Windows. То есть первоначально, в момент их создания,не существует объектов приложения, соответствующих этим окнам. Для этого вбиблиотеку вводятся средства создания объектов по хендлу. Эти средства могутнесколько различаться в разных библиотеках.
Напримерметод CWnd* CWnd::FromHandle( HWND ), существующийв MFC, создает специальный объект, описывающий окно, связывает его с указаннымхендлом и возвращает указатель на него. Этот объект считается “временным” —спустя некоторое время MFC сама его уничтожит. В OWL аналогичного эффекта можнодобиться используя специальную форму конструктора объекта TWindow.
Случай 3 существенно более сложный. Когда окно получает сообщение,Windows вызывает зарегистрированную оконную процедуру, причем для этойпроцедуры передается только хендл окна и параметры сообщения. Указатель наобъект приложения, описывающего это окно, остается в момент получения сообщениянеизвестным!
Обычно вбиблиотеках классов используют следующий способ: при запуске приложения вWindows регистрируется оконная процедура, определяющая специальный класс окон,используемый всей иерархией классов данной библиотеки. Эта оконная процедураполучает сообщение и выполняет две операции:
· находит связанноес данным хендлом окно — для этого библиотеки классов поддерживают специальные таблицы соответствия хендлов оконописаниям этих окон в приложении
· распределяетпришедшее сообщение соответствующему (обычно виртуальному) методу–обработчикуэтого сообщения.
Для заданияметодов–обработчиков конкретных сообщений вводятся специальные таблицы отклика или таблицы трансляции сообщений (response table, message map table).Когда вы разрабатываете новый класс окон, вы для него должны разработать такуютаблицу, в которой должны быть указаны соответствия приходящих сообщений ивызываемых методов (конечно, если это не сделано в классе–предке).
Случай 4 вообще самый сложный — в нормальных условиях библиотеки егоне обрабатывают, так как для получения сообщений окна, не являющегося объектомкласса, необходимо подменить процедуру обработки сообщений.
Этонакладывает ограничения на применение методов–обработчиков сообщений — дляокон, не созданных как объекты класса, эти методы вызываться не будут. В случаеMFC названия таких методов обычно начинаются на On..., например OnDestroy;а в случае OWL — на Ev..., например EvDestroy. Часто можно так организоватьприложение, что переопределять эти методы просто не потребуется, однако это невсегда удобно и возможно.
Принеобходимости как–либо изменить реакцию окна на внешние события (переопределитьпринятую обработку сообщений) надо, во–первых, создать соответствующий объекткласса (как в случае 2). Во–вторых обычное окно, создаваемое Windows (например,какой–либо элемент управления диалогом — кнопка, флажок и пр.) или другимприложением, использует собственную оконную процедуру. Эта процедура,естественно, никак не связана с библиотекой ООП, применяемой вашим приложением.Таким образом, при получении окном сообщений, вызывается только лишь егособственная оконная процедура, не обращающаяся к методам класса. То естьнеобходимо осуществить подмену оконной процедуры (в Windows это называется порождением подкласса окон— subclass) спомощью специальных методов библиотек, выполняющих эту операцию: SubclassWindowFunction в OWL или SubclassWindow в MFC. После этого новаяоконная функция будет обращаться к методам класса для обработки сообщений, а вкачестве стандартной обработки будет использоваться та оконная функция, котораяиспользовалась окном до ее подмены.
Однако прииспользовании этого приема необходимо учитывать следующие нюансы:
· при созданииобъекта класса лучше использовать один из базовых классов (CWnd или TWindow),так как все порожденные от них классы переопределяют значительно большее числометодов, предполагая стандартную обработку сообщений, реализованную в DefWindowProc, а не в той процедуре, которуювы подменили. Это может привести к конфликтам между новой обработкой событий ипрежней оконной процедурой. Особенно опасна ошибка в назначении класса —библиотека классов и компилятор никак не смогут проверить вас и предупредить,если вы, скажем, для кнопки, создадите объект класса “список” (LISTBOX). При такой ошибке конфликт практически неизбежен. Влюбом случае надо хорошо представлять себе, для какой стандартной оконнойпроцедуры реализован какой класс библиотеки ООП и обработку каких сообщений онпереопределяет, прежде чем решиться на подмену оконной процедуры.
· в случае Win32для окон, созданных другим приложением, оконные процедуры (используемая окном иназначаемая вами) размещается в различных адресных пространствах разныхпроцессов. Обращение из другого процесса по новому адресу функции приведет,скорее всего, к ошибке — так как этот адрес задан в адресном пространствевашего приложения, а что находится в адресном пространстве другого процесса поэтому адресу вам неизвестно. Решить эту проблему можно, выделяя описаниеобъекта класса и его процедуры в отдельную DLL, а затем внедряя ее в адресноепространство процесса, создавшего окно. Однако этот прием существенно сложнее./>Пример 1C — использованиесобственных классов
В этомпримере используется несколько упрощенный метод реализации объектов. Главноеограничение — невозможность назначения обработчиков сообщений для окон, несозданных в качестве объектов класса. В остальном этот вариант сохраняет всенеобходимые функции, причем делает это более компактным и быстрым способом.Такой способ часто применяется в приложениях–примерах, сопровождающихкомпиляторы.
Короткорассмотрим реализацию этого способа: вместо ведения таблиц соответствия хендловобъектам приложения можно хранить необходимые данные непосредственно вструктуре описания окна в Windows (см. “Регистрация класса окон”). Так какдоступ к этим данным осуществляется только с помощью функций, то размещать тамвсе описание окна нецелесообразно, зато в этой структуре можно разместитьуказатель на связанный объект. Отсюда следует ограничение — этот метод будетработать только с теми окнами, в структуре описания которых в Windowsзарезервировано специальное поле для указателя. Это могут быть только окна, созданныенами.
/>
Рисунок 7. Поиск метода–обработчика сообщения в примере.
Помимо этогоиспользуется еще один прием — вместо таблиц функций–обработчиков сообщений длякаждого класса окон формируется специальная виртуальная функция–диспетчер,которая осуществляет вызовы нужных методов. Если в случае MFC или OWL надовести таблицы отклика, то в рассматриваемом примере надо разрабатыватьсоответствующую функцию.
Кроме того,для упрощения в примере остались некоторые следы обычного программирования —осталась, хотя и сильно измененная, функция WinMain, в которой создается объект “приложение”.
Рассматриваемыйпример состоит из 3х файлов: 1c.h — общий заголовочный файл, содержащийописания базовых классов; 1c_cls.cpp — методы и статические данные базовыхклассов; 1c_main.cpp — собственно само приложение: описание собственных классови их методов, а также функция WinMain.Файл 1c.h
#define STRICT
#include
#define UNUSED_ARG(arg) (arg)=(arg)
class Win0 {
protected:
HWND hwnd;
virtual LRESULT dispatch(UINT, WPARAM, LPARAM );
virtual BOOL OnCreate( LPCREATESTRUCT );
virtual void OnDestroy( void ) = 0;
virtual void OnPaint( HDC hdc ) = 0;
public:
Win0( void );
~Win0( void );
BOOL create( char* );
void destroy( void );
void update( void ) { UpdateWindow( hwnd ); }
void show( int nCmdShow ) { ShowWindow( hwnd, nCmdShow );}
friend LONG WINAPI_export Win0proc( HWND, UINT, WPARAM, LPARAM );
};
class App0 {
public:
static HINSTANCE hInstance;
static HINSTANCE hPrevInstance;
static LPSTR lpszCmdLine;
static int nCmdShow;
App0( HINSTANCE,HINSTANCE, LPSTR, int );
~App0( void );
BOOL init(void );
int run( void );
void release( void );
};Файл 1c_cls.cpp
#include«1c.h»
HINSTANCE App0::hInstance;
HINSTANCE App0::hPrevInstance;
LPSTR App0::lpszCmdLine;
int App0::nCmdShow;
static char szWndClass[]=«test window class»;
static Win0* on_create_ptr;
Win0::Win0( void )
{
hwnd = NULL;
}
Win0::~Win0( void )
{
destroy();
}
LRESULT WINAPI_export Win0proc( HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam )
{
Win0* pwin;
pwin =(Win0*)GetWindowLong( hWnd, 0 );
if ( !pwin ) {
SetWindowLong( hWnd, 0, (LONG)(Win0 FAR*)(pwin = on_create_ptr) );
pwin->hwnd = hWnd;
}
return pwin->dispatch( uMsg, wParam, lParam );
}
LRESULTWin0::dispatch( UINT uMsg, WPARAM wParam, LPARAM lParam )
{
PAINTSTRUCT ps;
switch ( uMsg ) {
case WM_CREATE: return OnCreate( (LPCREATESTRUCT)lParam )? 0L: -1L;
case WM_PAINT: OnPaint( BeginPaint( hwnd, &ps ) ); EndPaint(hwnd, &ps ); return 0L;
case WM_DESTROY: OnDestroy(); return 0L;
default: break;
}
return DefWindowProc( hwnd, uMsg, wParam, lParam );
}
void Win0::destroy(void )
{
if ( IsWindow( hwnd ) ) DestroyWindow( hwnd );
hwnd = (HWND)NULL;
}
BOOL Win0::create(char* title )
{
on_create_ptr = this;
CreateWindow(
szWndClass, // class name
title, // window name
WS_OVERLAPPEDWINDOW, // window style
CW_USEDEFAULT,CW_USEDEFAULT, // window position
CW_USEDEFAULT,CW_USEDEFAULT, // window size
NULL, // parent window
NULL, // menu
hInstance, // current instance
NULL // user-definedparameters
);
on_create_ptr = (Win0*)NULL;
return IsWindow( hwnd );
}
BOOL Win0::OnCreate(LPCREATESTRUCT lpCreateStruct )
{
UNUSED_ARG( lpCreateStruct );
return TRUE;
}
App0::App0( HINSTANCEhInst, HINSTANCE hPrev, LPSTR lpszCmd, int nShow )
{
hInstance = hInst;
hPrevInstance = hPrev;
lpszCmdLine = lpszCmd;
nCmdShow = nShow;
}
App0::~App0( void )
{
}
BOOL App0::init( void)
{
static BOOL done;
WNDCLASS wc;
if ( !done&& !hPrevInstance ) {
wc.style = 0;
wc.lpfnWndProc = Win0proc;
wc.cbClsExtra = 0;
wc.cbWndExtra = sizeof(LONG);
wc.hInstance = hInstance;
wc.hIcon = LoadIcon( NULL, IDI_APPLICATION );
wc.hCursor = LoadCursor( NULL, IDC_ARROW );
wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
wc.lpszMenuName = NULL;
wc.lpszClassName = szWndClass;
done = RegisterClass( &wc )? TRUE: FALSE;
}
return done;
}
int App0::run( void )
{
MSG msg;
while ( GetMessage(&msg, NULL, NULL, NULL ) ) {
TranslateMessage( &msg );
DispatchMessage( &msg );
}
return msg.wParam;
}
void App0::release(void )
{
}Файл 1c_main.cpp
#include«1c.h»
class MainWindow:public Win0 {
protected:
virtual void OnDestroy( void );
virtual void OnPaint( HDC hdc );
public:
MainWindow( void );
~MainWindow( void );
};
class MyApp: publicApp0 {
protected:
MainWindow wnd;
public:
MyApp( HINSTANCE hInst, HINSTANCE hPrevInst, LPSTR lpszCmdLine, int nCmdShow);
~MyApp( void );
BOOL init( void );
};
MainWindow::MainWindow(void ): Win0()
{
}
MainWindow::~MainWindow(void )
{
}
voidMainWindow::OnDestroy( void )
{
PostQuitMessage( 0 );
}
voidMainWindow::OnPaint( HDC hdc )
{
TextOut( hdc, 0, 0, «Hello, world!», 13 );
}
MyApp::MyApp( HINSTANCEhInst, HINSTANCE hPrevInst, LPSTR lpszCmdLine, int nCmdShow )
: App0( hInst,hPrevInst, lpszCmdLine, nCmdShow )
{
}
MyApp::~MyApp( void )
{
}
BOOL MyApp::init(void )
{
if ( App0::init() ) {
if ( wnd.create( «window header» ) ) {
wnd.show( nCmdShow );
wnd.update();
return TRUE;
}
}
return FALSE;
}
int PASCAL WinMain( HINSTANCEhInst, HINSTANCE hPrevInst, LPSTR lpszCmdLine, int nCmdShow )
{
int a;
MyApp app( hInst, hPrevInst, lpszCmdLine, nCmdShow );
if ( app.init() ) {
a = app.run();
} else a = -1;
app.release();
return a;
}Обзор примера 1C
Примерсодержит два базовых класса: App0 —описывает приложение и Win0 — описываетокно.
Класс App0 содержит 4 члена–данных: hInstance,hPrevInstance, lpszCmdLine и nCmdShow,которые являются аргументами функции WinMain.Интереснее разобраться с методами, описанными в этом классе. Конструктор простоинициализирует члены–данные для использования в последующем; деструктор вообщеничего не делает. Пара методов init и release предназначена для переопределения вдальнейшем — метод init долженвыполнять специфичную инициализацию приложения, а метод release — операции при завершении. В классе App0 метод initосуществляет регистрацию оконной процедуры (в терминологии Windows — класса),которая будет применяться данным приложением. Метод run выполняет циклобработки сообщений.
Класс Win0 содержит только один член–данные hwnd — хендл окна. Конструктор устанавливаетзначение хендла окна равным NULL(окно не создано), деструктор проверяет существование окна и, принеобходимости, закрывает его. Методы create,destroy, update и showсоответствуют функциям API: CreateWindow,DestroyWindow, UpdateWindow и ShowWindow.Методы OnCreate, OnDestroy и OnPaintсоответствуют обработчикам сообщений WM_CREATE,WM_DESTROY и WM_PAINT. Метод dispatchявляется диспетчером, который распределяет пришедшие сообщения посоответствующим методам–обработчикам.
В том–жеклассе декларирована дружественная функция Win0proc, которая является собственно оконной процедурой.
Короткорассмотрим, как создается окно в этом примере. Для создания окна необходимовызвать метод create, который, в свою очередь, вызоветфункцию CreateWindow из Windows. Во время созданияокна его оконная процедура начнет получать сообщения (в том числе и WM_CREATE, хотя, на самом деле, это будет непервое полученное сообщение). Эта процедура для нормальной работы требует, чтобы в структуре описания окна в Windows был сохранен указатель на объект,описывающий окно в приложении. Но в момент первого вызова обработчика сообщенийэтот указатель там не находиться — все происходит еще только во время работыфункции CreateWindow. Соответственно мы используемнекоторую статическую переменную (on_create_ptr),которая перед вызовом CreateWindowинициализируется указателем на объект. Тогда обработчик сообщений может бытьпостроен по следующей схеме:
LONG WINAPI _export Win0proc( HWND hWnd, UINT uMsg,WPARAM wParam, LPARAM lParam )
{
Win0* pwin;
pwin =(Win0*)GetWindowLong( hWnd, 0 ); // получаем указатель на объект
if ( !pwin ) { // указатель равен NULL — объект толькосоздается
// инициализируем объект и указатель на него
SetWindowLong( hWnd, 0, (LONG)(Win0 FAR*)(pwin =on_create_ptr) );
pwin->hwnd = hWnd;
}
// вызываем виртуальную функцию-диспетчер
return pwin->dispatch( uMsg, wParam, lParam );
}
Принормальной работе первый вызов функции GetWindowLongвернет указатель на объект, так что следующий шаг — вызов функции–диспетчера.Таким образом дополнительные затраты ресурсов на реализацию ООП таким способомоказываются минимальными. В случае разработки классов–наследников от Win0 надо разработать собственнуюфункцию–диспетчер, которая будет вместо процедуры DefWindowProc вызывать диспетчер класса–предка.
В примерах,сопровождающих компиляторы инициализация объекта и указателя на объект в структуреописания окна выполняется при обработке WM_CREATE.Это решение не является наилучшим — сообщение WM_CREATE далеко не самое первое из обрабатываемыхсообщений, хотя, предусмотрев обработку сообщений с помощью DefWindowProc при неопределенном указателе, можноосуществлять инициализацию и при обработке WM_CREATE.
Конечно, этотпример крайне упрощен. Вообще, даже в простейших случаях, надо проводитьконтроль корректности данных, убедиться, что окно еще не существует передвызовом CreateWindow в методе create, проверить on_create_ptrперед использованием и многое другое. Данный пример специально лишен всегоэтого, что бы в максимально открытом виде продемонстрировать простейшую схему./>Основы работы с памятью
Дополнительнонадо разобраться с несколькими терминами Windows API, которые постоянноприменяются, но очень плохо описаны в документации. Речь идет о хендлах копииприложения (HINSTANCE), модуля (HMODULE) и задачи (HTASK).Все эти хендлы используются разными функциями, причем разница между ними никакне поясняется. Помимо этого в Win32 API появилась пара дополнительных хендлов —хендл процесса и хендл потока, а также идентификаторы процесса и потока. Приэтом осталась все прежние понятия, часто изменившие смысл, но в документациипо–прежнему не описанные (или описанные плохо).Хендл задачи (HTASK), хендлы иидентификаторы процесса и потока
В Windows 3.xпод задачей подразумевается конкретный запущенный процесс, для которогоопределены командная строка, текущая выполняемая инструкция, указатель на стек,переменные окружения, PDB (эквивалентпрефикса задачи (PSP) в среде DOS) и пр. Хендл задачи можнополучить с помощью функции
HTASK GetCurrentTask( void);
В Win32 хендлзадачи не применяется, а вместо него надо пользоваться хендлами и/илиидентификаторами процесса и потока. Их можно получить с помощьюфункций:
HANDLE GetCurrentProcess(void );
HANDLE OpenProcess( fdwAccess, fInherit, dwIDProccess );
DWORD GetCurrentProcessId( void );
HANDLE GetCurrentThread( void );
DWORD GetCurrentThreadId( void );
Функции GetCurrentProcess и GetCurrentThread возвращают так называемый псевдодескриптор[19]процесса (потока). Псевдодескриптор — это некоторая величина, рассматриваемая вкачестве дескриптора текущего процесса (потока). То есть эта величина,применяемая в контексте другого процесса (потока), будет описывать его, а неданный поток. Для получения “настоящего” хендла из псевдодескриптора надовоспользоваться функцией:
BOOL DuplicateHandle(
hSourceProcess, hSourceHandle, hTargetProcess, lphTargetHandle,
fdwAccess, fInherit, fdwOptions
);Хендл копии приложения (HINSTANCE)
В Windows 3.xэтот хендл указывает на сегмент данных приложения, который содержит стек илокальную кучу. Для каждого запущенного приложения создается свой собственныйсегмент данных, что позволяет однозначно определить конкретную копию приложенияпо его сегменту данных или организовать обмен данными между двумя копиямиодного приложения. Так функция GetInstanceDataпозволяет скопировать данные, принадлежащие сегменту данных другой копии, вто–же самое место текущей копии.
int GetInstanceData( hInstance, pByte, cbData );
В функцию WinMain передается как хендл текущей копииприложения, так и хендл предыдущей копии, что позволяет организоватьвзаимодействие двух копий между собой, предотвратить запуск других копий, еслиэто может привести к ошибке, или для выполнения действий, необходимых только впервой копии приложения.
В Win32 длякаждого запущенного приложения (т.е. процесса) выделяется виртуальное адресноепространство в 4Г в едином сегменте. Поэтому данный хендлсоответствует не сегменту данных (который описывает весь 4Г сегмент), а адресу в виртуальном пространстве, с которогобыл загружен данный модуль. В адресном пространстве одного процесса никакихдругих приложений не существует, поэтому этот хендл не может применяться дляобнаружения других копий приложения и тем более для обмена данными междуразными копиями приложений. В приложениях Win32 hPrevInstance всегда равен NULL, а хендл текущей копии приложения в большинстве случаевсовпадает. При необходимости обнаружения других копий приложения надо использоватькакие–либо иные методы, например функцию:
HWND FindWindow(lpszClassName, lpszWindowTitle );
Хендл окна вWin32 является уникальным и может идентифицировать конкретное окно в любомприложении.
Для обменаданными между приложениями (процессами) приходится передавать данные изадресного пространства одного процесса в адресное пространство другого. Длявыполнения этих операций предусмотрено сообщение WM_COPYDATA. Когда Вы посылаете это сообщение окну,созданному другим процессом, указанные Вами данные копируются в адресноепространство другого процесса и могут быть прочитаны оконной процедуройокна–получателя. Этот механизм может применяться и для обмена данными между 16ти и 32х битовымиприложениями, однако для этого необходимо определить номер сообщения WM_COPYDATA и специальную структуру COPYDATASTRUCT для 16ти битовой платформы — так какфайл windows.h не содержит этих определений:
#define WM_COPYDATA 0x004A
typedef structtagCOPYDATASTRUCT {
DWORD dwData;
DWORD cbData;
LPVOID lpData;
} COPYDATASTRUCT, FAR* PCOPYDATASTRUCT;Хендл модуля (HMODULE)
В Windows 3.xпод модулем понимается отдельный выполняемый файл или библиотека динамическойкомпоновки. Для описания модуля создается специальный сегмент описания модуля,содержащий информацию о всех сегментах данного модуля и их атрибутах. Хендлмодуля идентифицирует этот сегмент. Для получения хендла модуля Вы можетевоспользоваться функциями:
HMODULE GetModuleHandle( lpszFileName );
int GetModuleFileName( hInstance, lpsBuffer, cbMaxSize );
В Windows 3.xхендл модуля часто может быть заменен на хендл копии приложения. В Win32 хендлмодуля вообще является синонимом хендла копии приложения. В документации ещевстречаются оба термина, как они перекочевали из 16ти битовых Windows, хотя теперьони тождественны./> Подробнее о приложении (2)
Итак, впредыдущих разделах мы рассмотрели основы организации приложения в средеWindows. Это было первое знакомство с простейшим приложением, которое было лишьповодом для разговора об основах работы оконных систем.
Приложение всреде Windows, как и в среде DOS, содержит так называемую главную функцию — WinMain, вызываемую при запуске приложения. Приложениезавершается при окончании работы этой функции.
Обычно, хотяэто и не обязательно, функция WinMainреализует следующую схему:
· выполняютсятребуемые инициализационные действия
· создается главноеокно приложения, для чего часто регистрируется новый класс окон (оконнаяфункция);
· организуется циклобработки сообщений приложения. Обычно цикл завершается при закрытии главногоокна приложения;
· после завершенияцикла обработки сообщений выполняется освобождение занятых ресурсов, после чегофункция WinMain заканчивается.
Замечание 1. Если приложение содержит непродолжительные (порядка 1 сек.) операции, не требующие взаимодействияс пользователем (например, только файл–ориентированный ввод–вывод или настройкадругого приложения), то эти действия могут быть выполнены непосредственнофункцией WinMain без создания окон и без организации цикла обработки сообщений.
Замечание 2. В некоторых случаях приложение может обойтись безрегистрации класса окон и организации цикла обработки сообщений, применяя вкачестве главного окна модальный диалог.
Замечание 3. В момент вызова функции WinMain ей, через аргументы, передается несколькопараметров, например хендл копии приложения hInstance. До вызова WinMainприложение “не знает” этих данных. Поэтому могут возникать сложности сиспользованием статических конструкторов объектно–ориентированных языков (C++).
!!! Фокусввода!!!!!!
В Windowsсуществует определенная путаница терминов. Попробуем разобраться с некоторымииз них. Как известно, окно может находиться в нескольких состояниях:
Максимизированном, то есть быть “распахнутым” на весь экран — при этомвнутренняя область окна занимает весь экран, кроме небольших полос сверху — гдеразмещается заголовок и меню, снизу — горизонтальная полоса прокрутки и справа— вертикальная полоса прокрутки; рамка окна находится за пределами экрана, мыее не видим, перемещение окна невозможно.
Длямаксимизации окна мы можем воспользоваться функцией ShowWindow со следующими возможными параметрами:
ShowWindow( hWnd, SHOW_FULLSCREEN );
ShowWindow( hWnd, SW_SHOWMAXIMIZED );
ShowWindow( hWnd, SW_MAXIMIZE );
максимизированноеокно всегда активно и имеет фокус ввода. Когда какое–либо окно максимизируется,все остальные верхние окна получают сообщение WM_SIZE, информирующее о том, что они “закрыты” сверхумаксимизированным окном.
Мы можемузнать, является ли наше окно максимизированным с помощью функции
BOOL IsZoomed( hWnd );
Прииспользовании системного меню операции максимизации окна соответствует пункт Maximize, выбор которогопорождает системную команду SC_MAXIMIZE(или синоним SC_ZOOM). (см. сообщение WM_SYSCOMMAND)
Здесь вместотермина maximizeможет использоваться zoom.
Минимизированным, то есть представленным в виде пиктограммы. Для того,что бы превратить окно в пиктограмму, мы должны воспользоваться одним изспособов:
ShowWindow( hWnd, SHOW_ICONWINDOW );
ShowWindow( hWnd, SW_SHOWMINIMIZED );
ShowWindow( hWnd, SW_SHOWMINNOACTIVE );
ShowWindow( hWnd, SW_MINIMIZE );
CloseWindow( hWnd );
Разныеспособы, использующие ShowWindow,отличаются только правилами активации окна. SW_SHOWMINIMIZED и SHOW_ICONWINDOWотображает окно в виде пиктограммы, делая его активным; SW_SHOWMINNOACTIVE не изменяет текущего активного окна; SW_MINIMIZE (как и функция CloseWindow) делает активным следующее окно в спискеWindows. Последний способ эффективен при минимизации главного окна приложения— так как минимизированное главное окно обычно обозначает передачу активностидругому приложению.
Проверитьсостояние окна можно с помощью функции
BOOL IsIconic( hWnd );
Прииспользовании системного меню превращению окна в иконку соответствует пункт Minimize, порождающийсистемную команду SC_MINIMIZE (илисиноним SC_ICON). (см. сообщение WM_SYSCOMMAND)
В этом случаеиспользуется сразу три разных термина для обозначения одного и того–же: minimize, close и iconic. При этом функция CloseWindow является единственной,интерпретирующей термин closeтаким способом; в остальных случаях close означает действительно закрытие (иногдауничтожение) окна. Здесь же надо, что термин open, применяемый к минимизированномуокну обозначает его максимизацию или восстановление нормальных размеров.
Нормальным, то есть мы видим (или можем увидеть) его рамку, мы можемперемещать окно по экрану. Когда окно находится в нормальном состоянии, то длянего определены максимально и минимально допустимый размеры. Эти размеры нельзяпутать с максимизированным и минимизированным состояниями. Максимальный размернормального окна может даже превышать размер окна в максимизированномсостоянии, минимальный размер это обычно такой размер, при котором окно ещеможет быть корректно представлено в виде окна.
Для переходаиз минимизированного состояния к нормальному можно воспользоваться функцией
OpenIcon( hWnd );
или, как изминимизированного, так и из максимизированного состояния можно пользоватьсяфункцией ShowWindow с параметрами:
ShowWindow( hWnd, SHOW_OPENWINDOW );
ShowWindow( hWnd, SW_SHOWNORMAL );
ShowWindow( hWnd, SW_RESTORE );
ShowWindow( hWnd, SW_SHOWNOACTIVATE );
Вдокументации (SDK Help)указано, что SW_RESTORE и SW_SHOWNORMAL эквивалентны, но это далеко не так — SW_RESTORE восстанавливает предыдущеесостояние, а не нормальное. То есть, если Вы минимизировали окно измаксимизированного, то SW_RESTORE вернетВас к максимизированному окну, а SW_SHOWNORMAL— к нормальному. SW_SHOWNORMAL имеет синоним SHOW_OPENWINDOW.
Если окновосстанавливается или максимизируется из минимизированного состояния, то Вашеокно получит сообщение WM_QUERYOPEN —обрабатывая которое Вы можете разрешить или запретить дальнейшие действия. ЕслиВы возвращаете TRUE, то окно будет раскрыто, а если Вывернете FALSE, то окно останется минимизированным.
Несколькозамечаний: На самом деле Windows не является настоящей объектно–ориентированнойсредой. Хотя окно и может быть названо объектом ООП, но лишь с достаточнойнатяжкой. Самое существенное отличие окна в Windows от объекта ООП заключаетсяв том, что сообщение, обрабатываемое оконной функцией, во многих случаях невыполняет действий, а является “информационным”, указывая на то, что над окномвыполняется та или иная операция какой–либо внешней функцией.
Поясним этона примере создания окна. В случае ООП для уничтожения объекта он долженполучить сообщение “destroy”, обработка которого приведет к его уничтожению. ВWindows сообщение WM_DESTROY невыполняет никаких функций по уничтожению окна. Оно только информирует окно отом, что в это время окно уничтожается средствами обычной функциональнойбиблиотеки, например посредством функции DestroyWindow.Вы можете вообще игнорировать это сообщение, возвращать любое значение,вызывать или не вызывать функцию обработки по умолчанию — окно все равно будетуничтожено.
Если бы всесообщения, получаемые окном были только информационными, то к этому легко можнобыло бы приспособиться. Однако для некоторых сообщений должна выполнятьсяобработка по умолчанию, если Вы ее не выполнили сами, а для других такаяобработка должна выполняться обязательно, даже если Вы уже обработали этосообщение. Все это заметно усложняет написание приложений в среде Windows.
Ситуациядополнительно усугубляется тем, что в документации, как правило, ничего несообщается о том, какая обработка сообщения выполняется по умолчанию и, крометого, по некоторым сообщениям приводятся некорректные (или неполные) сведенияоб их параметрах, выполняемым функциям, условиям возникновения и возвращаемомрезультате.
Для окон,использующих в качестве процедуры обработки сообщений по умолчанию не DefWindowProc, а иную функцию (например, DefMDIChildProc), можно уточнить списоксообщений обязательно подлежащих обработке по умолчанию. Однако это уточнениекасается только тех сообщений, обработку которых для DefWindowProc можно игнорировать, а для иных функций нельзя,список же того, что можно игнорировать для DefWindowProc, а что нельзя остается неизвестным./> Настройка приложений
Поднастройкой (иногда «профилированием») понимается заданиехарактеристик приложения и их сохранение для использования при следующемзапуске.
Обычно такиезадачи решаются с помощью создания настроечных файлов. Однако конфигурацияописывается каждой задачей по–своему, что не всегда удобно. Windows предлагаетобщий для всех приложений механизм описания их характеристик, с использованиемфайлов настройки.
Такие файлы(обычно имеющие расширение .INI)являются обычными ASCII–файлами, разделенными на секции, начинающиеся с именисекции, заключенного в квадратные скобки. Далее следует список параметров ввиде параметр=значение, каждый параметрразмещается в отдельной строке. В этот файл можно вставлять комментарии —строки начинающиеся с ‘;’.
Пример взятиз файла WORKSHOP.INI:
[User Controls]
BorShade=E:\BORLANDC\WORKSHOP\BWCC.DLL
[RWS_Bitmap]
PercentLeft=50
ZoomLeft=1
ZoomRight=1
bVert=0
[RWS_Font]
PercentLeft=50
ZoomLeft=4
ZoomRight=1
bVert=1
Для работы стакими файлами Windows предоставляет набор функций, осуществляющих запись ичтение параметров:
int GetProfileInt(lpszSection, lpszEntry, nDefault);
int GetProfileString(
lpszSection, lpszEntry, lpszDefault, lpsBuffer, nMaxBuffer
);
BOOL WriteProfileString(lpszSection, lpszEntry, lpszString);
Параметр lpszSection задает имя секции (квадратныхскобок в имени указывать не надо), lpszEntry— имя параметра. Если мы получаем значение параметра, то можем указать значениепо умолчанию, которое возвращается, если данный параметр не найден.
С помощьюфункции GetProfileString можно получить список именвсех параметров в секции, указав lpszEntryравным NULL. При этом имена параметров секции будутскопированы в буфер последовательно друг за другом, каждое имя будетзаканчиваться байтом '\0' и послепоследнего имени будут стоять два байта '\0'.
Функция WriteProfileString позволяет не толькозаписывать параметры, но и удалять, для чего надо указать lpszString равным NULL.Можно удалить целиком всю секцию, указав для этого lpszEntry равным NULL.
Все трирассмотренных функции используют файл WIN.INI.При этом имя секции часто ассоциируется с именем приложения, поэтому вдокументации имя секции часто называется именем приложения.
Конечно,часто бывает неудобно использовать общий файл настроек для всех существующихприложений (при этом, в частности, трудно организовать удаление приложений).Windows предоставляет возможность использовать собственный файл настройки. Дляработы с собственными файлами настройки предусмотрены еще три функции:
int GetPrivateProfileInt( lpszSection, lpszEntry, nDefault,lpszIniFile );
int GetPrivateProfileString(
lpszSection, lpszEntry, lpszDefault, lpsBuffer, nMaxBuffer,lpszIniFile
);
BOOL WritePrivateProfileString( lpszSection,lpszEntry, lpszString, lpszIniFile );
Последнийпараметр этих функций lpszIniFile задаетимя файла настройки. Если вы не указываете путь к файлу, то он размещается вкаталоге Windows.Реестр Windows
RegOpenKey Opens a specified key
RegCreateKey Creates a specified key
RegCloseKey Closes a key and releasesthe key's handle
RegQueryValue Retrieves the text stringfor a specified key
RegSetValue Associates a text stringwith a specified key
RegDeleteKey Deletes a specified key
RegEnumKey Enumerates the subkeys ofa specified key
#includeshellapi.h
LONGRegOpenKey(hkey, lpszSubKey, lphkResult);
HKEY hkey; /* handle of an open key */
LPCSTR lpszSubKey; /* address of string for subkey to open */
HKEY FAR* lphkResult; /* address of handle of open key */
TheRegOpenKey function opens the specified key.
Parameter Description
hkey Identifiesan open key (which can be HKEY_CLASSES_ROOT). The key opened by the RegOpenKeyfunction is a subkey of the key identified by this parameter. This value shouldnot be NULL.
lpszSubKey Pointsto a null-terminated string specifying the name of the subkey to open.
lphkResult Pointsto the handle of the key that is opened.
Returns
Thereturn value is ERROR_SUCCESS if the function is successful. Otherwise, it isan error value.
Comments
Unlikethe RegCreateKey function, the RegOpenKey function does not create thespecified key if the key does not exist in the database.
Example
char szBuff[80];
LONG cb;
HKEY hkStdFileEditing;
if (
RegOpenKey(
HKEY_CLASSES_ROOT,
«NewAppDocument\\protocol\\StdFileEditing»,
&hkStdFileEditing
) == ERROR_SUCCESS
) {
cb =sizeof(szBuff);
if (
RegQueryValue(
hkStdFileEditing,
«handler»,
szBuff,
&cb
) == ERROR_SUCCESS
&& lstrcmpi(«nwappobj.dll», szBuff) == 0
) RegDeleteKey(hkStdFileEditing, «handler»);
RegCloseKey(hkStdFileEditing);
}