Первое знакомство
Данный раздел называется “первое знакомство”. Здесь вы действительно познакомитесь с первым приложением для 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. Мы к этому вернемся, рассматривая метод разделения процессора.
Диск
Для разделения дискового пространства используется файловая система. Здесь Windows 3.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–preemptive multitasking). В такой среде, подобно однозадачной, система не прерывает выполнения приложения. Однако само приложение должно быть разделено на небольшие, быстро выполняемые фрагменты. После выполнения такого фрагмента система может перейти к выполнению другого приложения. При этом приложение само уведомляет систему, где ее можно прервать для выполнения других задач.
В Windows 3.x это может быть реализовано двумя разными методами:
обычно приложение разбивается на набор небольших, быстро выполняемых функций. В этом случае система просто вызывает нужные функции для выполнения требуемых задач. После завершения обработки одной функции система может вызвать другую функцию другого приложения, осуществляя таким образом выполнение нескольких приложений как–бы одновременно.
можно воспользоваться специальной функцией, передающей управление системе, и возвращающей его назад приложению после обработки других приложений. Таких функций в Windows 3.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ти разрядные приложения между собой используют обработку сообщений для разделения отведенного им процессорного времени. В случае Windows NT для каждого 16ти разрядного приложения запускается собственная Windows–машина, что позволяет им разделять процессор общим способом с приложениями Win32.
Истинная многозадачность в Win32 позволила реализовать так называемые многопотоковые приложения (multithread application). При этом выделяют два новых понятия — процесс (proccess) и поток (thread). Процессы в Win32 API примерно эквивалентны приложениям в Windows API. Для каждого процесса выделяются определенные системные ресурсы — адресное пространство, приоритеты и права доступа к разделяемым ресурсам и прочее, но не процессорное время. Процесс только лишь описывает запущенную задачу, как она есть, без непосредственных вычислений. Для разделения процессора используются не процессы, а потоки, которым и выделяется процессорное время. В рамках каждого процесса выделяется свой поток, называемый первичным (primary thread), создаваемый по умолчанию при создании процесса. При необходимости в пределах одного процесса может быть создано много потоков, конкурирующих между собой (и с потоками других процессов) за процессорное время, но не за адресное пространство.
Как написать приложение для Windows
Пока мы рассмотрели только основную идею использования сообщений для реализации объектно–ориентированной операционной среды. Сейчас надо перейти к особенностям организации приложения, работающего в такой среде.
Каждое приложение открывает по меньшей мере одно окно (в принципе могут существовать приложения вообще без окон, но как небольшие специализированные процедуры, не требующие никакого управления). Свойства окна определяются процедурой обработки сообщений этого окна. Таким образом, что бы определить свойства нужного окна, надо написать процедуру обработки сообщений, посылаемых этому окну (оконную процедуру или оконную функцию — window procedure, она же процедура обработки сообщений, message handler).
Одна процедура может обслуживать сообщения, посылаемые разным окнам с одинаковыми свойствами. Говорят, что окна, имеющие одну и ту же оконную функцию, принадлежат к одному классу окон. Вы должны эту процедуру зарегистрировать — это называется регистрацией класса окон.
Далее необходимо предусмотреть средства для создания и отображения окна зарегистрированного класса. С таким окном пользователь будет работать — передвигать его по экрану, изменять размеры, вводить текст и т.д. Вам необходимо обеспечить реакцию этого окна (то есть вашего приложения) на действия пользователя. Фактически вы должны запустить механизм, обеспечивающий доставку сообщений, адресованных вашему окну, до получателя — оконной процедуры. Этот механизм должен работать, пока работает ваше приложение. Такой механизм называется циклом обработки сообщений (message loop).
Таким образом вы должны выполнить несколько шагов для создания собственного приложения:
написать оконную функцию;
зарегистрировать эту функцию (класс) в 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 <windows.h>
#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 ) return 1;
ShowWindow( hWnd,
nCmdShow );
UpdateWindow( hWnd );
while ( GetMessage(
&msg, NULL, NULL, NULL ) ) {
TranslateMessage( &msg
);
DispatchMessage( &msg );
}
return
msg.wParam;
}
Рисунок 7. Приложение 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. Этот файл содержит определения типов, констант и функций, используемых в Windows6.
В приложении перед включением WINDOWS.H определяется специальный символ STRICT:
#define
STRICT
#include <windows.h>
Он указывает, что необходимо осуществлять строгую проверку типов. То есть использование вместо переменной одного типа переменной какого–либо другого, даже сходного, типа будет рассматриваться компилятором как ошибка.
Для большей части обычных типов 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) |
|
|
Типы
(typedef) |
|
|
Практически для всех определенных типов существуют типы “указатель на...”. Ближние указатели строятся с помощью префикса 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 обозначает дальний указатель на ASCIIZ7 строку символов, содержащую имя файла.
Как видно из примера, перед мнемоническим именем переменной пишется небольшой префикс, указывающий ее тип. Каким образом строится префикс? Из небольшой таблицы можно получить представление об обозначении основных типов данных:
обозначающий символ | название обозначаемого типа | пояснение |
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ти разрядного приложения.
Операционная система загружает исполняемый файл в память и выделяет требуемые для первоначальной загрузки ресурсы.
Управление передается специально написанному разработчиками компиляторов startup–коду, который инициализирует приложение, получает необходимую информацию (как, например, командная строка, хендл копии приложения и пр.), запускает конструкторы статических объектов.
startup–код вызывает функцию WinMain, передавая ей полученные от операционной системы данные. Функция WinMain разрабатывается для каждого приложения.
WinMain обычно осуществляет регистрацию класса окон.
Далее WinMain создает и отображает главное окно приложения.
WinMain обеспечивает функционирование приложения, организуя цикл обработки сообщений. В этом цикле приложение извлекает поступающие к нему сообщения, выполняет их предварительную обработку и затем передает их окну–получателю (то есть вызывает необходимую оконную процедуру).
Оконная процедура выполняет обработку сообщений, направленных окну, обеспечивая реакцию задачи на действия пользователя.
Приложение работает, пока пользователь не закроет главное окно этого приложения. В момент закрытия этого окна оконная процедура принимает специальные меры для завершения цикла обработки сообщений, организованного WinMain.
Когда цикл обработки сообщений завершается, WinMain продолжает исполнение своего кода, выполняя, при необходимости, деинициализацию и уничтожение созданных объектов, после чего завершает работу.
Возврат управления из функции WinMain происходит опять–же в промежуточный exit–код, созданный разработчиками компиляторов. Он запускает деструкторы статических объектов, деинициализирует приложение и возвращает управление в систему.
Система освобождает оставшиеся занятыми ресурсы, закрепленные за этим приложением.
Теперь, перед тем как перейти к рассмотрению непосредственно кода приложения, надо сделать последнее замечание. Windows API разрабатывался тогда, когда систем виртуальной памяти на персональном компьютере еще не существовало. Первые версии Windows могли работать на XT с 640K оперативной памяти. Из–за очень ограниченного объема памяти приходилось идти на различные ухищрения. Так один из способов экономии памяти связан с тем, что код приложений обычно не изменяется. Если запустить две копии одного и того–же приложения (скажем, два Notepad’а для редактирования двух разных файлов), то код этих приложений будет одинаковым. В этом случае его можно загружать только один раз, но для необходимости как–то различать разные копии приложения и возникло понятие хендл копии приложения (instance handle, HINSTANCE).
Функция WinMain
Обычная программа на C (C++) содержит так называемую главную процедуру main. При создании программ для Windows тоже необходимо описать такую процедуру, правда она называется WinMain и имеет другие аргументы:
int PASCAL WinMain(
HANDLE hInstance, HANDLE hPrevInstance, LPSTR lpszCmdLine, int
nCmdShow )
{
//
...
}
Описание главной функции ключевым словом PASCAL указывает на применение соглашений языка Pascal при передаче аргументов и вызове функции (так делается в большинстве функций Windows, потому что вызов pascal–декларированной функции осуществляется чуть быстрее и занимает меньше места, чем C–декларированной).
Рассмотрим ее аргументы:
HANDLE hInstance — этот параметр является хендлом, указывающим конкретную копию приложения. Знание этого хендла потребуется для связи тех или иных данных с конкретной копией.
HANDLE hPrevInstance — описывает хендл предыдущей копии приложения. Если данная копия является первой, то эта переменная содержит NULL. Использование этой информации несколько специфично:
Во–первых, Windows связывает некоторые данные с конкретной копией приложения (например: экспортированные функции, окна и пр.). При связывании необходимо указывать хендл копии приложения.
Внимание: при использовании C++ иногда удобно описать статический объект. Однако в этом случае может потребоваться информация о hInstance для конструктора статического объекта. По странной причине мы ее не можем узнать до вызова WinMain — эта информация известна с самого начала (еще до вызова каких–либо конструкторов): startup–код в самых первых инструкциях обращается к процедуре INITTASK, которая возвращает системную информацию, в том числе hInstance. После этого hInstance копируется в статическую переменную, используемую startup– и exit– кодом, однако эта переменная является локальной (?!) и недоступна для остальных модулей приложения. Причина такого поступка со стороны разработчиков компиляторов остается непонятной.
Во–вторых, некоторые операции должны выполняться только при запуске первой копии приложения, а все остальные копии должны игнорировать эти операции или выполнять вместо них другие. Для пояснения этого обратим внимание на оконную функцию. Когда мы создаем приложение, то мы описываем специфичные оконные функции и регистрируем классы окон. После запуска первой копии (пока она активна) эти классы окон известны Windows. Значит при запуске последующих копий нам не надо их регистрировать.
В–третьих, иногда нам надо получить данные от предыдущей копии приложения (например, если наши приложения организуют обмен данными между собой). С помощью hPrevInstance мы можем сделать это (только в Windows API, в Win32 API это не получится)8.
В–четвертых, 32х битовые приложения Win32 API всегда предполагают, что запущена только одна копия приложения (так как в виртуальном адресном пространстве приложения кроме нее других приложений, в том числе копий, не находится). При этом hInstance указывает начальный адрес загрузки модуля (и для большинства приложений он совпадает), а hPrevInstance всегда равен NULL.
LPSTR lpszCmdLine — как и обычная 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). В таких полях удобно хранить данные, уникальные для каждого окна — скажем, хендл редактируемого файла, если это окно редактора.
Рисунок 7. Структуры данных, используемые 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 описывает "обычное" перекрывающееся окно, имеющее кнопку системного меню, две кнопки максимизации и минимизации, заголовок окна и "широкую" рамку — для возможности изменять размер окна.
Рисунок 7. Основные элементы окна
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), разрабатываемый для каждого приложения. В этом цикле сообщения извлекаются из очереди, определяется, какое окно (какая процедура обработки сообщений) должно это сообщение получить, при необходимости выполняются специальные действия, и только затем сообщение передается конкретной оконной процедуре. И такой процесс работает все время, пока работает приложение.
Процесс помещения сообщения в очередь и его извлечения из нее никак жестко не увязан по времени. Более того, может случиться так, что посланное сообщение вообще не будет обработано, например, если окно–получатель будет уничтожено прежде, чем успеет его обработать.
Рисунок 7. Обработка посланных сообщений в Windows
Например, когда вы нажимаете на клавишу, генерируется аппаратное прерывание. Клавиатурный драйвер Windows обрабатывает это прерывание и помещает соответствующее сообщение в очередь сообщений. При этом указывается, какое окно должно получить данное сообщение.
Этот процесс называется посылкой (post) сообщений, так как посылка сообщения напоминает посылку письма: посылающий сообщение указывает адресата, отправляет сообщение и больше о нем не беспокоится. Отправитель не знает, когда точно его сообщение получит адресат. Такой способ обработки сообщений часто называется асинхронным.
Извлечение сообщений из очереди приложения и направление их соответствующим окнам осуществляет цикл обработки сообщений, обычно входящий в функцию WinMain. Этот процесс выполняется в несколько приемов:
сообщение выбирается из очереди с помощью функции GetMessage или PeekMessage
затем сообщение транслируется с помощью функции TranslateMessage10 (одно сообщение может порождать последовательность других или заменяться, как, например, происходит для сообщений клавиатуры 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( HWND hWnd, 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 в Watcom C/C++). Implib позволяет по имеющемуся файлу динамически загружаемой библиотеки (.dll) или файлу описания проекта модуля библиотеки (.def), содержащему список экспортированных функций, построить библиотечный файл (.lib), с ссылками на функции библиотеки.
Первоначально, стремясь максимально уменьшить время загрузки модулей в память, при экспортировании функций им присваивались уникальные номера (назначаются разработчиком, либо просто по порядку). Конкретная функция однозначно определяется именем экспортирующей библиотеки динамической загрузки и идентификатором функции. Для задания идентификаторов экспортируемых функций используется файл описания модуля. Однако использование номеров вместо имен является не слишком удобным для человека, поэтому в Windows используются оба метода — функции доступны как по их идентификаторам, так и по их именам. Для Windows API общепринятым методом считается использование идентификаторов — Microsoft следит за тем, что бы все документированные функции сохраняли свои идентификаторы. А в Win32 API предполагается использование имен; более того, Microsoft не гарантирует, что идентификаторы документированных функций не будут изменяться.
Построение приложения сохранило в себе все шаги, однако создание файла описания приложения стало необязательным. При разработке приложений для Win32 API с файлами описания модуля практически не приходится иметь дело, он используется в очень редких случаях для построения динамически загружаемых библиотек. В настоящее время и в случае Windows API, и в случае Win32 API этот файл создается только если необходимо обеспечить нестандартную компоновку модуля, имеющего, например, разделяемые сегменты данных.
Современные компиляторы и системы поддержки проектов фактически остались в рамках рассмотренного порядка построения приложения. Небольшие изменения реализованы в разных компиляторах независимо друг от друга. Так, иногда включение файла ресурсов приложения в выполняемый модуль выполняется не компилятором ресурсов, а непосредственно компоновщиком; в других случаях специальные редакторы ресурсов позволяют обойтись без построения файла описания ресурсов (.rc), а создать непосредственно файл ресурсов приложения (.res). Особенно часто это делается в системах визуального программирования.
Если вы используете какую–либо систему поддержки проектов (Watcom IDE, Microsoft Developer 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 program must 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, либо ничего). Более подробно о характеристиках сегментов приложения см. в описании диспетчера памяти Windows 3.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 для задания идентификаторов экспортируемых функций (обе — см. в описании файла описания модуля для Windows API).
Для задания нестандартных параметров отдельных сегментов используется директива 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|Win32 SDK|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 <windows.h>
#include <windowsx.h>
Для того, что бы получить представление о возможностях windowsx.h рекомендуется посмотреть его исходный текст. В нем присутствуют следующие разделы:
макросы для работы с функциями ядра (несколько макросов для работы с глобальной памятью)
макросы для работы с объектами GDI
макросы для работы с окнами (вызовы стандартных функций)
распаковщики сообщений (самая большая часть)
макросы для работы с окнами стандартных классов (кнопки, списки и пр.)
некоторые макросы для оптимизации стандартной библиотеки времени выполнения
В процессе рассмотрения соответствующих разделов мы часто будем обращаться к этому заголовочному файлу. Здесь же мы детально познакомимся с использованием распаковщиков сообщений.
Распаковщики сообщений
Большая часть файла windowsx.h предназначена для описания распаковщиков сообщений (message crackers). Так как в книге преимущественно будут приводятся фрагменты кода с использованием распаковщиков, то в этом месте мы с ними и познакомимся. При их использовании придется постоянно заглядывать в исходный текст windowsx.h, так как в обычной документации распаковщики не описаны. По счастью этот файл хорошо структурирован и снабжен достаточными комментариями.
Для того, что бы понять его назначение, вернемся к оконной процедуре. Основная ее часть — конструкция switch, которая нужна для обработки конкретных сообщений. Обычно (кроме простейших примеров) этот switch разрастается до невообразимых размеров, так что его крайне трудно прочитать (и не дай боже искать там ошибку). Вторым побочным эффектом является то, что огромное количество переменных, используемых для обработки разных сообщений, сосредотачиваются в одной процедуре, бесполезно занимая место.
Вполне очевиден выход — разнести обработку сообщений по отдельным функциям, которые будут вызываться из процедуры обработки сообщений. Однако для каждого сообщения передаются свои данные, упакованные в двух параметрах — wParam и lParam. Иногда они не используются, иногда содержат какие–либо значения, иногда — указатели. Естественно, было бы удобным передавать в вызываемую функцию уже распакованные параметры. Затруднение здесь вызывает то, что для Windows API и для Win32 API одни и те же данные одного и того же сообщения могут быть упакованы по разному18.
При разработке windowsx.h это все было учтено (для Windows API и для 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, LPCREATESTRUCT lpCreateStruct) */
#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, LPARAM lParam );
Макросы 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 <windows.h>
#include <windowsx.h>
#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 BOOL
init_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(
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, // 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-defined parameters
);
if ( !hWnd ) return 1;
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, сопровождающая компилятор Borland C/C++ v4.5).
Microsoft — Microsoft Foundation Classes (MFC), поддерживается наибольшим количеством компиляторов, среди которых Microsoft Visual C++, Watcom C++, Symantec C++ и другие (рассматривается версия v4.0, сопровождающая Visual C/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 для описания приложения используются следующие классы:
Рисунок 7. Классы MFC, описывающие окно и приложение.
Данная версия MFC рассчитана на разработку многопотоковых приложений для Win32. Это наложило свой отпечаток на иерархию классов — в качестве одного из базовых выступает класс CWinThread, описывающий отдельный поток. И только на его основе построен класс CWinApp, описывающий приложение (в Win32 существует понятие основного потока, который обслуживает функцию WinMain, именно он и будет представлен объектом класса CWinThread, на основе которого порождается объект класса CWinApp).
OWL
В этой библиотеке иерархия классов несколько иная — библиотека рассчитана на разработку 16ти разрядных приложений Windows 3.x, не поддерживающую потоков.
Рисунок 7. Классы OWL, описывающие окно и приложение.
Окна ООП и окна Windows
При использовании библиотек классов надо как–то связывать экземпляры объектов, описывающих окна в приложении, с описанием того–же окна в Windows. Здесь надо учитывать, что, с одной стороны:
существуют методы классов, соответствующие вызову функций API;
существуют методы классов, соответствующие обработчикам сообщений;
а, с другой стороны:
существуют окна, созданные с помощью классов ООП;
существуют окна, созданные другими приложениями, модулями а также стандартными средствами Windows, не имеющими отношения к применяемой библиотеке.
Так, например, диалог “Открыть Файл” является стандартным диалогом Windows. Он создается и выполняется посредством вызова одной функции API — FileOpen. Эта функция сама, независимо от приложения и его библиотеки классов, создает необходимые окна и работает с ними. Однако у программиста может возникнуть необходимость как–то взаимодействовать с этим диалогом в процессе его работы.
Можно выделить четыре возможных ситуации, с которыми придется столкнуться во время работы приложения:
должна быть вызвана функция API для окна, реализованного как объект класса;
должна быть вызвана функция API для окна, не являющегося объектом класса;
окно, реализованное как объект класса, получает сообщение — то есть надо вызвать соответствующий метод–обработчик этого сообщения;
окно, не являющееся объектом класса, получает сообщение.
Случаи 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 <windows.h>
#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 );
}
LRESULT
Win0::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-defined parameters
);
on_create_ptr
= (Win0*)NULL;
return IsWindow( hwnd );
}
BOOL Win0::OnCreate(
LPCREATESTRUCT lpCreateStruct )
{
UNUSED_ARG(
lpCreateStruct );
return TRUE;
}
App0::App0(
HINSTANCE hInst, 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 : public
App0 {
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 )
{
}
void
MainWindow::OnDestroy( void )
{
PostQuitMessage(
0 );
}
void
MainWindow::OnPaint( HDC hdc )
{
TextOut(
hdc, 0, 0, "Hello, world!", 13 );
}
MyApp::MyApp(
HINSTANCE hInst, 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(
HINSTANCE hInst, 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 struct
tagCOPYDATASTRUCT {
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 releases the key's handle
RegQueryValue Retrieves the text string for a specified key
RegSetValue Associates a text string with a specified key
RegDeleteKey Deletes a specified key
RegEnumKey Enumerates the subkeys of a specified key
#include shellapi.h
LONG RegOpenKey(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 */
The RegOpenKey function opens the specified key.
Parameter Description
hkey Identifies an open key (which can be HKEY_CLASSES_ROOT). The key opened by the RegOpenKey function is a subkey of the key identified by this parameter. This value should not be NULL.
lpszSubKey Points to a null-terminated string specifying the name of the subkey to open.
lphkResult Points to the handle of the key that is opened.
Returns
The return value is ERROR_SUCCESS if the function is successful. Otherwise, it is an error value.
Comments
Unlike the RegCreateKey function, the RegOpenKey function does not create the specified 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);
}
1 Так как процессор, обычно, только один, то в данный момент времени будет работать только одно приложение. Однако, так как переключение между приложениями осуществляется достаточно быстро, то возникает впечатление одновременной работы нескольких приложений. Эта оговорка не влияет на последующие рассуждения.
2 В принципе требования могут и нарушаться. Так приложения под Windows 3.x сравнительно легко могут получить доступ к аппаратуре, хотя делать это не рекомендуется. Приложениям Win32 уже значительно сложнее получить доступ — система лучше защищена от таких попыток, особенно Windows NT.
3 Строго говоря, обычный DOS тоже может работать с различными файловыми системами — для CD ROM дисков специально спроектирована своя собственная файловая система (CDFS). При этом надо установить драйвер CD–ROM, обеспечивающий физический доступ к диску, и программу MSCDEX, которая осуществляет работу с дисками в формате CDFS.
4 Кроме некоторых особых случаев, связанных с обработкой критичных по времени процессов, скажем некоторых операций ввода/вывода, взаимодействия с периферией и пр. Однако обычно такие задачи решаются драйверами устройств, так что приложение об этом опять–же не ведает.
5 Точнее при уничтожении. Термин “закрытие” в Windows часто имеет другой смысл, в том числе — сворачивание окна в пиктограмму.
6 В Win32 API заголовочный файл windows.h просто включает в себя набор директив #include для включения необходимых заголовочных файлов и директив условной компиляции.
7 Строка ASCII — строка символов таблицы ASCII, то есть обычный текст. Однако при программировании часто используются строки либо со специальным завершающим символом (в C это байт с кодом 0 — ASCIIZ), либо с указанием длины строки в виде лидирующего байта (ASCIIB) или слова (ASCIIW).
8 При необходимости выяснить наличие других копий приложения в системе можно попробовать найти другие окна, принадлежащие тому–же классу, что и главное окно нашего приложения. Это легко делается с помощью функции FindWindow. Этот метод работает как в Windows API, так и в Win32 API.
9 При задании хендла окна–родителя окна могут находиться либо в отношениях родительское/дочернее (parent/child), либо в отношениях владелец/используемое (owner/owned), в зависимости от наличия стиля WS_CHILD у порожденного окна.
10 В некоторых руководствах в простейших примерах обходятся без трансляции вообще. Однако это является не совсем корректным, так как функция TranslateMessage распознает комбинацию клавиш Alt+Space как команду нажатия на кнопку системного меню. Конечно без нее приложение будет работать, но не в полной мере реализует стандартный клавиатурный интерфейс.
11 В Windows все указатели, которые получает или передает Вам система являются 32х разрядными. В случае Windows API это будут дальние (far) указатели, а в случае Win32 это ближние (near), так как они уже являются 32х разрядными.
12 В первых версиях Windows в результате объединения формировался новый неверный прямоугольник. В современных версиях вместо неверного прямоугольника реально формируется неверный регион, который может иметь сложную форму.
13 Для проверки возможностей аппаратуры следует использовать функцию GetDeviceCaps.
14 В частном случае — с верхним левым углом самого окна, включая его обрамление (см. описание функции GetWindowDC).
15 Кроме случаев применения сохраняемых контекстов. См. стили класса окон CS_OWNDC и CS_CLASSDC.
16 Панель диалога является родительским окном для всех элементов управления, размещенных на этой панели, включая статические — рамки, текст и прочее.
17 Ориентировочно, начиная с компиляторов для Windows 3.0
18 Это связано с тем, что параметр wParam имеет тип UINT — то есть он в Windows API представляет собой 16ти разрядное слово, а в Win32 API — 32х разрядное. Кроме того, в младшем или старшем слове lParam часто размещается хендл, который в Windows API 16ти разрядный. К сожалению в Win32 API хендлы 32х разрядные, так что уместить его на прежнем месте не представляется возможным.
19 Это просто некоторая константа, используемая для обозначения «текущего потока» или «текущего процесса».