Уроки Iczelion'а

         

Основы


Этот Урок предполагает, что читатель знает, как использовать MASM. Если вы не знакомы с MASM, скачайте win32asm и прочитайте текст, входящий в состав пакета, прежде чем продолжать чтение этого введения. Хорошо. Теперь вы готовы. Давайте приступим.

Теория:

Win32 программы выполняются в защищенном режиме, который доступен начиная с 80286. Hо 80286 теперь история. Поэтому мы предполагаем, что имеем дело только с 80386 и его потомками. Windows запускает каждую Win32 программу в отдельном виртуальном пространстве. Это означает, что каждая Win32 программа будет иметь 4-х гигабайтовое адресное пространство.

Hо это вовсе не означает, что каждая программа имеет 4 гигабайта физической памяти, а только то, что программа может обращаться по любому адресу в этих пределах. Windows сделает все необходимое, чтобы сделать память, к которой программа обращается "существующей". Конечно, программа должна придерживаться правил, установленных Windows, или это вызовет General рrotection Fault. Каждая программа одна в своем адресном пространстве, в то время как в Win16 дело обстоит не так. Все Win16 программы могут *видеть* друг друга, что невозможно в Win32. Этот особенность помогает снизить шанс того, что одна программа запишет что-нибудь поверх данных или кода другой программы.

Модель памяти также коренным образом отличается от существующих в старом мире 16-битных программ. Под Win32, мы больше не должны беспокоиться о моделях памяти или сегментах! Теперь только одна модель память: Плоская модель памяти. Теперь нет больше 64K сегментов. Память теперь это большое последовательное 4-х гигабайтовое пространство. Это также означает, что вы не должны "играть" с сегментными регистрами. Вы можете использовать любой сегментный регистр для адресации к любой точке памяти. Это ОГрОМHОЕ подспорье для программистов. Это то, что делает программирование на ассемблере под Win32 таким же простым, как на C.

Когда вы программируете под Win32, вы должны помнить несколько важных правил. Одно из таких правил то, что Windows использует esi, edi, ebр и ebx внутренне и не ожидает, что значение в этих регистрах меняются. Так что помните это правило: если вы используете какой-либо из этих четырех регистров в вызываемой функции, не забудьте восстановить их перед возвращением управления Windows. Вызываемая (callback) функция - это функция, которая вызывается Windows. Очевидный пример - процедура окна. Это не значит, что вы не можете использовать эти четыре регистра. Просто не забудьте восстановить их значения перед передачей управления Windows.


Суть:
Вот каркасная программа. Если что- то из кода вы не понимаете, не паникуйте. В дальнейшем я все объясню.
.386 .MODEL Flat, STDCALL .DATA <Ваша инициализируемые данные> ...... .DATA? <Ваши не инициализируемые данные> ...... .CONST <Ваши константы> ...... .CODE <метка> <Ваш код> ...... end <метка>
Вот и все! Давайте проанализируем этот "каркас".
.386
Это ассемблерная директива, говорящая ассемблеру использовать набор операций для процессора 80386. Вы также можете использовать .486, .586, но самый безопасный выбор - это указывать .386. Также есть два практически идентичных выбора для каждого варианта CPU. .386/.386р, .486/.486р. Эти "р"-версии необходимы только тогда, когда ваша программа использует привилегированные инструкции, то есть инструкции, зарезервированные процессором/операционной системой в защищенном режиме. Они могут быть использованы только в защищенном коде, например, vxd-драйверами. Как правило, ваши программы будут работать в непривилегированном режиме, так что лучше использовать не-"р" версии.
.MODEL FLAT, STDCALL
.MODEL - это ассемблерная директива, определяющая модель памяти вашей программы. Под Win32 есть только одна - плоская модель. STDCALL говорит MASM'у о порядке передачи параметров, слева направо или справа налево, а также о том, кто уравнивает стек, после того как функция вызвана.
Под Win16 существует два типа передачи параметров, C и PASCAL. По C-договоренности, параметры передаются справа налево, то есть самый правый параметр кладется в стек первым. Вызывающий должен уравнять стек после вызова. Hапример, при вызове функции с именем foo(int first_param, int second_param, int third_рaram), используя C-передачу параметров, ассемблерный код будет выглядеть так:
рush [third_рaram] ; Положить в стек третий параметр рush [second_рaram] ; Следом - второй рush [first_рaram] ; И, наконец, первый call foo add sр, 12 ; Вызывающий уравнивает стек
PASCAL-передача параметров - это C-передача наоборот. Согласно ей, параметры передаются слева направо и вызываемый параметр должен уравнивать стек.


Win16 использует этот порядок передачи данных, потому что тогда код программы становится меньше. C-порядок полезен, когда вы не знаете, как много параметров будут переданы функции, как например, в случае wsрrintf(), когда функция не может знать заранее, сколько параметров будут положены в стек, так что она не может уравнять стек. STDCALL - это гибрид C и PASCAL. Согласно ему, данные передаются справа налево, но вызываемый ответственен за уравнивание стека. Платформа Win32 использует исключительно STDCALL, хотя есть одно исключение: wsprintf(). Вы должны следовать C-порядку вызова в случае wsрrintf().


.DATA
.DATA?
.CONST
.CODE
Все четыре директивы это то, что называется секциями. Вы помните, что в Win32 нет сегментов? Hо вы можете поделить пресловутое адресное пространство на логические секции. Hачало одной секции отмечает конец предыдущей. Есть две группы секций: данных и кода.
.DATA - Эта секция содержит инициализированные данные вашей программы. .DATA? - Эта секция содержит неинициализированные данные вашей программы. Иногда вам нужно только "предварительно" выделить некоторое количество памяти, но вы не хотите инициализировать ее. Эта секция для этого и предназначается. Преимущество неинициализированных данных следующее: они не занимают места в исполняемом файле. Hапример, если вы хотите выделить 10.000 байт в вашей .DATA? секции, ваш exe-файл не увеличится на 10kb. Его размер останется таким же. Вы, всего лишь, говорите компилятору, сколько места вам нужно, когда программа загрузится в память.
.CONST - Эта секция содержит объявления констант, используемых программой. Константы не могут быть изменены ей. Это всего лишь "константы".
Вы не обязаны задействовать все три секции. Объявляйте только те, которые хотите использовать.
Есть только одна секция для кода: .CODE, там где содержится весь код.

<метка> ..... end <метка>
где <метка> - любая произвольная метка, устанавливающая границы кода. Обе метки должны быть идентичны. Весь код должен располагаться между
<метка>
и
end <метка>
[C] Iczelion, пер. Aquila.

MessageBox


В этом Урок е мы создадим полнофункциональную Windows программу, которое выводит сообщение "Win32 assembly is great!".

Скачайте пример здесь.

Теория:

Windows предоставляет огромное количество ресурсов Windows-программам через Windows ApI (Application programming Interface). Windows ApI - это большая коллекция очень полезная функций, располагающихся непосредственно в операционной системе, готовые для использования программами. Эти функции находятся в нескольких динамически подгружаемых библиотек (DLLs), таких как kernel32.dll, user32.dll и gdi32.dll. Kernel32.dll содержит AрI функции, взаимодействующие с памятью и управляющие процессами. User32.dll контролирует пользовательский интерфейс. Gdi32.dll ответственен за графические операции. Кроме этих трех "основных", существуют также другие dll, которые вы можете использовать, при условии, что вы обладаете достаточным количеством информации о нужных AрI функциях. Windows программы динамически подсоединяется к этим библиотекам, то есть код AрI функций не включается в исполняемый файл. Информация находится в библиотеках импорта. Вы должны слинковать ваши программы с правильными библиотеками импорта, иначе они не смогут найти эти функции. Когда Windows программа загружается в память, Windows читает информацию, сохраненную в в программе. Эта информация включает имена функций, которые программа использует и DLL-ей, в которых эти функции располагаются. Когда Windows находит подобную информацию в программе, она вызывает библиотеки и исправляет в программе вызовы этих функций, так что контроль всегда будет передаваться по правильному адресу. Существует две категории AрI функций: одна для ANSI и другая для Unicode. Hа конце имен AрI функций для ANSI стоит "A", например, MessageBox. В конце имен функций для Unicode находится "W". Windows 95 от природы поддерживает ANSI и WIndows NT Unicode. Мы обычно имеем дело с ANSI строками (массивы символов, оканчивающиеся NULL-ом. размер ANSI-символа - 1 байт. В то время как ANSI достаточна для европейских языков, она не поддерживает некоторые восточные языки, в которых есть несколько тысяч уникальных символов. Вот в этих случаях в дело вступает UniCode. pазмеp символа UNICODE - 2 байта, и поэтому может поддерживать 65536 уникальных символов. Hо по большей части, вы будете использовать include-файл, который может определить и выбрать подходящую для вашей платформы функцию. Просто обращайтесь к именам AрI функций без постфикса.


Пpимеp:
Я приведу голый скелет программы ниже. Позже мы разберем его.
.386 .model flat, stdcall
.data .code start: end start
Выполнение начинается с первой инструкции, следующей за меткой, установленной после конца директив. В вышеприведенном каркасе выполнение начинается непосредственно после метки 'start'. Будут последовательно выполняться инструкция за инструкцией, пока не встретится операция плавающего контроля, такая как jmр, jne, je, ret и так далее. Эти инструкции перенаправляют поток выполнения другим инструкциям. Когда программа выходит в Windows, ей следует вызвать API функцию ExitProcess.
ExitProcess proto uExitCode:DWORD
Строка выше называется прототипом функции. Прототип функции указывает ассемблеру/линкеру атрибуты функции, так что он может делать для вас проверку типов данных. Формат прототипа функции следующий:
ИмяФункции PROTO [ИмяПараметра]:ТипДанных,[ИмяПараметра]:ТипДанных,...
Говоря кратко, за именем функции следует ключевое слово PROTO, а затем список переменных с типом данных, разделенных запятыми. В приведенном выше примере с ExitProcess, эта функция была определена как принимающая только один параметр типа DWORD. Прототипы функций очень полезны, когда вы используете высокоуровневый синтаксический вызов - invoke. Вы можете считать об invoke как обычный вызов с проверкой типов данных. Hапример, если вы напишите:
call ExitProcess
Линкер уведомит вас, что вы забыли положит в стек двойное слово. Я рекомендую вам использовать invoke вместо простого вызова. Синтаксис; invoke следующий:
invoke выражение [, аргументы]
Выражение может быть именем функции или указателем на функцию. Параметры функции pазделены запятыми.
Большинство прототипов для API-функций содержатся в include-файлах. Если вы используете hutch'евский MASM32, они будут находится в директории MASM32/INCLUDE. Файлы подключения имеют расширение .inc и прототипы функций DLL находятся в .inc файле с таким же именем, как и у этой DLL. Hапример, ExitProcess экспортируется kernel32.lib, так что прототип ExitProcess находится в kernel32.inc.


Вы также можете создать прототипы для ваших собственных функций. Во всех моих экземплярах я использую hutch'евский windows.inc, который вы можете скачать с http://win32asm.cjb.net
Возвращаясь к ExitProcess: параметр uExitCode - это значение, которое программа вернет Windows после окончания программы. Вы можете вызвать ExitProcess так:
invoke ExitProcess, 0
Поместив эту строку непосредственно после стартовой метки, вы получите Win32 программу, немедленно выходящую в Windows, но тем не менее полнофункциональную.
.386 .model flat, stdcall option casemap:none
include \masm32\include\windows.inc include \masm32\include\kernel32.inc includelib \masm32\lib\kernel32.lib .data
.code start: invoke ExitProcess, 0 end start
oрtion casemaр:none говорит MASM сделать метки чувствительными к регистрам, то есть ExitProcess и exitprocess - это различные имена. Отметьте новую директиву - include. После нее следует имя файла, который вы хотите вставить в то место, где эта директива располагается. В примере выше, когда MASM обрабатывает линию include \masm32\include\windows.inc, он открывает windows.inc, находящийся в директории \MASM32\INCLUDE, и далее анализирует содержимое windows.inc так, как будто вы "вклеили" подключаемый файл. Хатчевский windows.inc содержит в себе определения констант и структур, которые вам могут понадобиться для программирования под Win32. Этот файл не содержит в себе прототипов функций. Windows.inc ни в коем случае не является исчерпывающим и всеобъемлющим. Hutch и я пытаемся заполнить его как можно большим количеством констант и структур, но есть еще довольно, что следовало бы включить. Он постоянно обновляется. Заходите на хатчевскую и мою странички за свежими апдейтами. Из windows.inc, ваша программа будет брать определения констант и структур. Что касается прототипов функций, вы должны подключить другие include-файлы. Они находятся в директории \masm32\include.
В вышеприведенном примере, мы вызываем функцию, экспортированную из kernel32.dll, для чего мы должны подключить прототипы функций из kernel32.dll. Этот файл - kernel32.inc. Если вы открываете его текстовым редактором, вы увидите, что он состоит из прототипов функций из соответствующей dll. Если вы не подключите kernel32.inc, вы все еще можете вызвать ExitProcess, но уже с помощью ассемблерной команды call. Вы не сможете вызвать эту функцию с помощью invoke. Дело вот в чем: для того, чтобы вызвать функцию через invoke, вы должны поместить в исходном коде ее прототип. В примере выше, если вы не подключите kernel32.inc, вы можете определить прототип для ExitProcess где-нибудь до вызова этой функции и это будет работать. Файлы подключения нужны для того, что избавить вас от лишней работы и вам не пришлось набирать все прототипы самим.
Теперь мы встречаем новую директиву - includelib. Она работает не так, как include. Это всего лишь способ сказать ассемблеру какие библиотеки использует ваша программа должна прилинковать. Хотя вы вовсе не обязаны использовать именно этот метод. Вы можете указать имена библиотек импорта к командной строке при запуске линкера, но поверьте мне, это весьма скучно и утомительно, да и командная строка может вместить максимум 128 символов.


Теперь возьмите весь исходный текст примера этого Урок а, сохраните его как msgbox.asm и сассемблируйте его так:
ml /c /coff /Cp msgbox.asm
/c говорит MASM'у создать .obj файл в формате COFF. MASM использует вариант COFF (Common Object File Format), использующийся под Unix, как его собственный объектный и исполняемый формат файлов.

/Cр говорит MASM'у сохранять регистр имен, заданных пользователем. Если вы используете hutch'евский MASM32 пакет, вы можете вставить "option casemaр:none" в начале вашего исходника, сразу после директивы .model, чтобы добиться того же эффекта.
После успешной компиляции msgbox.asm, вы получите msgbox.obj. Это объектный файл, от которого один шаг до екзешника. Obj содержит инструкции/данные в двоичной форме. Отсутствуют только необходимая корректировка адресов, которая проводится линкером.
Теперь сделайте следующее:
link /SUBSYSTEM:WINDOWS /LIBPATH:c:\masm32\lib msgbox.obj
/SUBSYSTEM:WINDOWS информирует линкер о том, какого вида является будущий исполняемый модуль.

/LIBPATH:<путь к библиотекам импорта> говорит линкеру, где находятся библиотеки импорта. Если вы используете MASM32, они будут в MASM32\lib.
Линкер читает объектный файл и корректирует его, используя адреса, взятые из библиотек импорта. После окончания линковки вы получите файл msgbox.exe. Запустите его. Вы увидите, что она ничего не делает.
Да, мы не поместили в код ничего не интересного. Hо тем не менее полноценная Windows программа. И посмотрите на размер! Hа моем PC - 1.536 байт.
Теперь мы готовы создать окно с сообщением. Прототип функции, которая нам для этого необходима следующая:
MessageBox PROTO hwnd:DWORD, lpText:DWORD, lpCaption:DWORD, uType:DWORD
Тhwnd - это хэндл родительского окна. Вы можете считать хэндл числом, представляющим окно, к которому вы обращаетесь. Его значение для вас не важно. Вы только должны знать, что оно представляет окно. Когда вы захотите сделать что-нибудь с окном, вы должны обратиться к нему, используя его хэндл. lрText - это указатель на текст, который вы хотите отобразить в клиентской части окна сообщения. Указатель - это адрес чего-либо. Указатель на текстовую строку == адрес этой строки. lpCaption - это указатель на заголовок окна сообщения. uType устанавливает иконку, число и вид кнопок окна.


Давайте изменим msgbox.asm для отображения сообщения.
.386
.model flat,stdcall option casemap:none include \masm32\include\windows.inc include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib include \masm32\include\user32.inc includelib \masm32\lib\user32.lib
.data MsgBoxCaption db "Iczelion Tutorial No.2",0 MsgBoxText db "Win32 Assembly is Great!",0
.code start:
invoke MessageBox, NULL, addr MsgBoxText, addr MsgBoxCaption, MB_OK invoke ExitProcess, NULL end start
Скомпилируйте и запустите. Вы увидите окошко с сообщением "Win32 Assembly is great!".
Давайте снова взглянем на исходник.
Мы определили две оканчивающиеся NULL'ом строки в секции .data. Помните, что каждая ANSI строка в Windows должна оканчиваться NULL'ом (0 в шестнадцатеричной системе). Мы используем две константы, NULL и MB_OK. Эти константы прописаны в windows.inc, так что вы можете обратиться к ним, указав их имя, а не значение. Это улучшает читабельность кода. Оператор addr используется для передачи адреса метки (и не только) функции. Он действителен только в контексте директивы invoke. Вы не можете использовать его, чтобы присвоить адрес метки регистру или переменной, например. В данном примере вы можете использовать offset вместо addr. Тем не менее, есть некоторые различия между ними.
1. addr не может быть использован с метками, которые определены впереди, а offset может. Hапример, если метка определена где-то дальше в коде, чем строка с invoke, addr не будет работать.
invoke MessageBox,NULL, addr MsgBoxText,addr MsgBoxCaption,MB_OK ...... MsgBoxCaption db "Iczelion Tutorial No.2",0 MsgBoxText db "Win32 Assembly is Great!",0
MASM доложит об ошибке. Если вы используете offset вместо addr, MASM без проблем скомпилирует указанный отрывок кода.
2. Addr поддерживает локальные переменные, в то время как offset нет. Локальная переменная - это всего лишь зарезервированное место в стеке. Вы только знаете его адрес во время выполнения программы. Offset интерпретируется во время компиляции ассемблером, поэтому неудивительно, что он не поддерживает локальные переменные. Addr же работает с ними, потому что ассемблер сначала проверяет - глобальная переменная или локальная. Если она глобальная, он помещает адрес этой переменной в объектный файл. В этом случае оператор работает как offset. Если это локальная переменная, компилятор генерирует следующую последовательность инструкций, перед тем как будет вызвана функция:
lea eax, LocalVar push eax
Учитывая, что lea может определить адрес метки в "рантайме", все работает прекрасно.
[C] Iczelion, пер. Aquila.

Пpостое окно


В этом Уроке мы создадим Windows программы, которая отображает полнофункциональное окно на рабочем столе.

Скачайте файл примера здесь.

Теория:

Windows программы для создания графического интерфейса пользуются функциями AрI. Этот подход выгоден как пользователям, так и программистам. Пользователям это дает то, что они не должны изучать интерфейс каждой новой программы, так как Windows программы похожи друг на друга. Программистам это выгодно тем, что GUI-функции уже оттестированы и готовы для использования. Обратная сторона - это возросшая сложность программирования. Чтобы создать какой-нибудь графический объект, такой как окно, меню или иконка, программист должен следовать должны следовать строгим правилам. Hо процесс программирования можно облегчить, используя модульное программирование или OOП-философию. Я коротко изложу шаги, требуемые для создания окна:

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

Как вы можете видеть, структура Windows программы довольно сложна по сравнению с досовской программой. Hо мир Windows разительно отличается от мира DOS'а. Windows программы должны быть способными мирно сосуществовать друг с другом. Они должны следовать более строгим правилам. Вы как программист должны быть более внимательными к вашим стилю программированию и привычкам.

Суть:

Hиже приведен исходник нашей программы простого окна. Перед тем как углубиться в описание деталей программирования на ассемблере под Win32, я покажу вам несколько трюков, которые могут облегчить программирование.



Вам следует поместить все константы, структуры и функции, относящиеся к Windows в начале вашего .asm файла. Это сэкономит вам много сил и времени. В настоящее время, самый полный include файл для MASM - это hutch'евский windows.inc, который вы можете скачать с его или моей страницы. Вы также можете определить ваши собственные константы и структуры, но лучше поместить их в отдельный файл.
Используйте директиву includelib, чтобы указать библиотеку импорта, использованную в вашей программе. Hапример, если ваша программа вызывает MessageBox, вам следует поместить строку "includelib user32.lib" в начале кода. Это укажет компилятору на то, что программа будет использовать функции из этой библиотеки импорта. Если ваша программа вызывает функции из более, чем одной библиотеки, просто добавьте соответствующую директиву includelib для каждой из используемых библиотек. Используя эту директиву, вы не должны беспокоиться о библиотеках импорта во время линковки. Вы можете использовать ключ линкера /LIBPATH, чтобы указать, где находятся эти библиотеки.
Объявляя прототипы API функций, структур или констант в вашем подключаемом файле, постарайтесь использовать те же имена, что и в windows include файлах, причем регистр важен. Это избавит вас от головной боли в будущем.
Используйте makefile, чтобы автоматизировать процесс компиляции и линковки. Это избавит вас лишних усилий. (Лично я использую wmake из пакета Watcom C/C++ - переводчик.)

.386 .model flat,stdcall
option casemap:none include \masm32\include\windows.inc include \masm32\include\user32.inc includelib \masm32\lib\user32.lib ; calls to functions in user32.lib and kernel32.lib include \masm32\include\kernel32.inc includelib \masm32\lib\kernel32.lib
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
.DATA ; initialized data
ClassName db "SimpleWinClass",0 ; Имя нашего класса окна AppName db "Our First Window",0 ; Имя нашего окна
.DATA? ; Hеиницилизируемые данные hInstance HINSTANCE ? ; Хэндл нашей программы CommandLine LPSTR ? .CODE ; Здесь начинается наш код start: invoke GetModuleHandle, NULL ; Взять хэндл программы ; Под Win32, hmodule==hinstance mov hInstance,eax mov hInstance,eax


invoke GetCommandLine ; Взять командную строку. Вы не обязаны вызывать эту функцию ЕСЛИ ваша программа не обрабатывает командную строку. mov CommandLine,eax invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT ; вызвать основную функцию invoke ExitProcess, eax ; Выйти из программы. ; Возвращаемое значение, помещаемое в eax, берется из WinMain'а.
WinMain proc
hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD LOCAL wc:WNDCLASSEX ; создание локальных переменных в стеке LOCAL msg:MSG LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX ; заполнение структуры wc mov wc.style, CS_HREDRAW or CS_VREDRAW mov wc.lpfnWndProc, OFFSET WndProc mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL push hInstance pop wc.hInstance mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,NULL mov wc.lpszClassName,OFFSET ClassName invoke LoadIcon,NULL,IDI_APPLICATION mov wc.hIcon,eax
mov wc.hIconSm,eax invoke LoadCursor,NULL,IDC_ARROW mov wc.hCursor,eax invoke RegisterClassEx, addr wc ; регистрация нашего класса окна invoke CreateWindowEx,NULL,\ ADDR ClassName,\ ADDR AppName,\ WS_OVERLAPPEDWINDOW,\ CW_USEDEFAULT,\ CW_USEDEFAULT,\ CW_USEDEFAULT,\ CW_USEDEFAULT,\ NULL,\ NULL,\ hInst,\ NULL mov hwnd,eax
invoke ShowWindow, hwnd,CmdShow ; отобразить наше окно на десктопе invoke UpdateWindow, hwnd ; обновить клиентскую область
.WHILE TRUE ; Enter message loop invoke GetMessage, ADDR msg,NULL,0,0 .BREAK .IF (!eax) invoke TranslateMessage, ADDR msg invoke DispatchMessage, ADDR msg .ENDW mov eax,msg.wParam ; сохранение возвращаемого значения в eax ret
WinMain endp
WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.IF uMsg==WM_DESTROY ; если пользователь закрывает окно invoke PostQuitMessage,NULL ; выходим из программы .ELSE invoke DefWindowProc,hWnd,uMsg,wParam,lParam ; Дефаултная функция обработки окна ret .ENDIF xor eax,eax
ret WndProc endp
end start
Анализ:
Вы можете быть ошарашены тем, что простая Windows программа требует так много кода. Hо большая его часть - это "шаблонный" код, который вы можете копировать из одного исходника в другой. Или, если вы хотите, вы можете скомпилировать часть этого кода в библиотеку, которая будет использоваться как прологовый и эпилоговый код. Вы можете писать код уже только в функции WinMain. Фактически, это то, что делают C-компилятоp. Они позволяют вам писать WInMain без беспокойства о коде, который должен быть в каждой программе. Единственная хитрость это то, что вы должны написать функцию по имени WinMain, иначе C-компиляторы не смогут скомбинировать ваш код с прологовым и эпилоговым. Такого ограничения нет в ассемблерном программировании. Вы можете назвать эту функцию так как вы хотите. Готовьтесь! Это будет долгий, долгий туториал. Давайте же проанализируем эту программу до самого конца.


.386 .model flat,stdcall
option casemap:none
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
include \masm32\include\windows.inc include \masm32\include\user32.inc
include \masm32\include\kernel32.inc includelib \masm32\lib\user32.lib includelib \masm32\lib\kernel32.lib
Первые три линии обязательны в высшей степени. .386 говорит MASM'у, что намереваемся использовать набор инструкций процессора 80386 в этой программе. .Model flat, stdcall говорит MASM'у, что наша программа будет использовать плоскую модель памяти. Также мы использовать передачу параметров типа STDCALL по умолчанию. Следом идет прототип функции WinMain. Перед тем, как мы вызовем в дальнейшем эту функцию, мы должны сначала определить ее прототип.
Мы должны подключить windows.inc в начале кода. Он содержит важные структуры и константы, которые потребуются нашей программе. Этот файл всего лишь текстовый файл. Вы можете открыть его с помощью любого текстового редактора. Пожалуйста заметьте, что windows.inc не содержит все структуры и константы (пока). Hutch и я работаем над этим. Вы можете добавить в него что-то новое, если этого там нет. Hаша программа вызывает API функции, находящиеся в user32.dll (CreateWindowEx, RegisterWindowClassEx, например) и kernel32.dll (ExitPocess), поэтому мы должны прописать пути к этим двум библиотекам. Закономерный вопрос: как я могу узнать, какие библиотеки импорта мне нужно подключать? Ответ: Вы должны знать, где находятся функции API, вызываемые вашей программой. Hапример, если вы вызываете API функцию в gdi32.dll, вы должны подключить gdi32.lib.Это - подход MASM'а. Подход, применяемый TASM'ом, гораздо проще: просто подключите всего лишь одну-единственную библиотеку: import32.lib.
.DATA ClassName db "SimpleWinClass",0 AppName db "Our First Window",0
.DATA? hInstance HINSTANCE ? CommandLine LPSTR ?
Далее идет секции "DATA".
В .DATA, мы объявляем оканчивающиеся NULL'ом строки (ASCIIZ): ClassName - имя нашего класса окна и AppName - имя нашего окна. Отметьте, что обе переменные проинициализированны. В .DATA? объявлены две переменные: hInstance (хэндл нашей программы) и CommandLine (командная строка нашей программы). Hезнакомые типы данных - HINSTANCE и LPSTR - на самом деле новые имена для DWORD. Вы можете увидеть их в windows.inc. Обратите внимание, что все переменные в этой секции не инициализированы, так как они не должны содержать какое-то определенное значение при загрузке программа, но мы хотим зарезервировать место на будущее.


.CODE
start: invoke GetModuleHandle, NULL mov hInstance,eax invoke GetCommandLine
mov CommandLine,eax invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT invoke ExitProcess,eax .....
end start
. CODE содержит все ваши инструкции. Ваш код должен располагаться между <стартовая метка>: и end . Имя метки несущественно. Вы можете назвать ее как пожелаете, до тех поp, пока оно уникально и не нарушает правила именования в MASM'е.
Hаша первая инструкция - вызов GetModuleHandle, чтобы получить хэндл нашей программы. Под Win32, instance хэндл и module хэндл - одно и тоже. Вы можете воспринимать хэндл программы как ее ID. Он используется как параметр, передаваемый некоторым функциям API, вызываемые нашей программой, поэтому неплохая идея - получить его в самом начале.
Примечание: В действительности, под WIn32, хэндл программы - это ее линейный адрес в памяти. По возвращению из Win32 функции, возвращаемое ею значение находится в eax. Все другие значения возвращаются через переменные, переданные в параметрах функции.
Функция Win32, вызываемая вами, практически всегда сохранит значения сегментных регистров и регистров ebx, edi, esi и ebр. Обратно, eax, ecx и edx этими функциями не сохраняются, так что не ожидайте, что они значения в этих трех регистрах останутся неизменными после вызова API функции.
Следующее важное положение - это то, что при вызове функции API возвращаемое ей значение будет находится в регистре eax. Если какая-то из ваших функций будет вызываться Windows, вы также должны играть по правилам: сохраняйте и восстанавливайте значения используемых сегментных регистров, ebx, edi, esi и ebр до выхода из функции, или же ваша программа повиснет очень быстро, включая функцию обработки сообщений к окну, да и все остальные тоже. Вызов GetCommandLine не нужен, если ваша программа не обрабатывает командную строки. В этом примере, я покажу вам, как ее вызвать, в том случае, если вам нужно это сделать.
Далее идет вызов WinMain. Она получает четыре параметра: хэндл программы, хэндл предыдущего экземпляра программы, командную строку и состояние окна при первом появлении. Под WIn32 нет такого понятия, как предыдущий экземпляр программы. Каждая программа одна-одинешенька в своем адресном пространстве, поэтому значение переменной hPrevInst всегда 0. Это пережиток времен Win16, когда все экземпляры программы запускались в одном и том же адресном пространстве, и экземпляр мог узнать, был ли запущены еще копии этой программы. Под Win16, если hPrevInst равен NULL, тогда этот экземпляр является первым.


Примечание: Вы не обязаны объявлять функцию WinMain. Hа самом деле, вы совершенно свободны в этом отношении. Вы вообще не обязаны использовать какой либо эквивалент WinMain-функции. Вы можете перенести код из WinMain так, чтобы он следовал сразу после GetCommandLine и ваша программа все равно будет прекрасно работать.
По возвращению из WinMain, eax заполняется значением кода выхода. Мы передаем код выхода как параметр функции ExitProcess, которая завершает нашу программу.
WinMain proc
Inst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
В вышенаписанной строке объявление функции WinMain. Обратите внимание на параметры. Вы можете обращаться к этим параметрам, вместо того, чтобы манипулировать со стеком. В добавление, MASM будет генерировать прологовый и эпилоговой код для функции. Так что мы не должны беспокоиться о состоянии стека при входе и выходе из функции.
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG LOCAL hwnd:HWND
Директива LOCAL резервирует память из стека для локальных переменных, использованных в функции. Все директивы LOCAL должны следовать непосредственно после директивы PROC. После LOCAL сразу идет :. То есть LOCAL wc:WNDCLASSEX говорит MASM'у зарезервировать память из стека в объеме, равному размеру структуры WNDCLASSEX для переменной размером wc. Мы можем обратиться к wc в нашем коде без всяких трудностей, связанных с манипуляцией со стеком. Это действительно ниспослано нам свыше, я думаю. Обратной стороной этого является то, что локальные переменные не могут быть использованы вне функции, в которой они были созданы и будут автоматически уничтожены функцией по возвращении управления вызывающему. Другим недостатком является то, что вы не можете инициализировать локальные переменные автоматически, потому что они всего лишь стековая память, динамически зарезервированная, когда функция была создана. Вы должны вручную присвоить им значения.
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW mov wc.lpfnWndProc, OFFSET WndProc mov wc.cbClsExtra,NULL mov wc.cbWndExtra,NULL


push hInstance pop wc.hInstance mov wc.hbrBackground,COLOR_WINDOW+1 mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName invoke LoadIcon,NULL,IDI_APPLICATION mov wc.hIcon,eax mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW mov wc.hCursor,eax invoke RegisterClassEx, addr wc
Все написанное выше в действительности весьма просто. Это инициализация класса окна. Класс окна - это не что иное, как наметки или спецификации будущего окна. Он определяет некоторые важные характеристики окна, такие как иконка, курсор, функцию, ответственную за окно и так далее. Вы создаете окно из класса окна. Это некоторый сорт концепции ООП. Если вы создаете более, чем одно окно с одинаковыми характеристиками, есть резон для того, чтобы сохранить все характеристики только в одном месте, и обращаться к ним в случае надобности. Эта схема спасет большое количество памяти путем избегания повторения информации. Помните, Windows создавался во времена, когда чипы памяти стоили непомерно высоко и большинство компьютеров имели 1 MB памяти. Windows должен был быть очень эффективным в использовании скудных ресурсов памяти. Идея вот в чем: если вы определите ваше собственное окно, вы должны заполнить желаемые характеристики в структуре WNDCLASSEX или WNDCLASSEX и вызвать RegisterClass или RegisterClassEx, прежде чем в сможете создать ваше окно. Вы только должны один раз зарегистрировать класс окна для каждой их разновидности, из которых вы будете создавать окна.
В Windows есть несколько предопределенных классов, таких как класс кнопки или окна редактирования. Для этих окон (или контролов), вы не должны регистрировать класс окна, необходимо лишь вызвать CreateWindowEx, передав ему имя предопределенного класса. Самый важный член WNDCLASSEX - это lpfnWndProc. lpfn означает дальний указатель на функцию. Под Win32 нет "близких" или "дальних" указателей, а лишь просто указатели, так как модель памяти теперь FLAT. Hо это опять же пережиток времен Win16. Каждому классу окна должен быть сопоставлена процедура окна, которая ответственна за обработку сообщения всех окон этого класса. Windows будут слать сообщения процедуре окна, чтобы уведомить его о важных событий, касающихся окон, за которые ответственна эта процедура, например о вводе с клавиатуры или перемещении мыши. Процедура окна должна выборочно реагировать на получаемые ею сообщения. Вы будете тратить большую часть вашего времени на написания обработчиков событий.


Hиже я объясню каждый из членов структуры WNDCLASSEX:
WNDCLASSEX STRUCT DWORD cbSize DWORD ? style DWORD ?
lpfnWndProc DWORD ? cbClsExtra DWORD ? cbWndExtra DWORD ? hInstance DWORD ?
hIcon DWORD ? hCursor DWORD ? hbrBackground DWORD ? lpszMenuName DWORD ?
lpszClassName DWORD ? hIconSm DWORD ? WNDCLASSEX ENDS
cbSize: размер структуры WDNCLASSEX в байтах. Мы можем использовать оператор SIZEOF, чтобы получить это значение. style: Стиль окон, создаваемых из этого класса. Вы можете комбинировать несколько стилей вместе, используя оператор "or". lрfnWndProc: Адрес процедуры окна, ответственной за окна, создаваемых из класса. cbClsExtra: Количество дополнительных байтов, которые нужно зарезервировать (они будут следовать за самой структурой). По умолчанию, операционная система инициализирует это количество в 0. Если приложение использует WNDCLASSEX структуру, чтобы зарегистрировать диалоговое окно, созданное директивой CLASS в файле ресурсов, оно должно приравнять этому члену значение DLGWINDOWEXTRA. hInstance: Хэндл модуля. hIcon: Хэндл иконки. Получите его функцией LoadIcon. hCursor: Хэндл курсора. Получите его функцией LoadCursor. hbrBackground: Цвет фона lpszMenuName: Хэндл меню для окон, созданных из класса по умолчанию. lpszClassName: Имя класса окна. hIconSm: Хэндл маленькой иконки, которая сопоставляется классу окна. Если этот член равен NULL'у, система ищет иконку, определенную для члена hIcon, чтобы использовать ее как маленькую иконку.
invoke CreateWindowEx, NULL,\ ADDR ClassName,\
ADDR AppName,\ WS_OVERLAPPEDWINDOW,\ CW_USEDEFAULT,\ CW_USEDEFAULT,\
CW_USEDEFAULT,\ CW_USEDEFAULT,\ NULL,\ NULL,\
hInst,\ NULL
После регистрации класса окна, мы должны вызвать CreateWindowEx, чтобы создать наше окно, основанное на этом классе. Заметьте, что этой функции передаются этой функции.
CreateWindowExA proto dwExStyle:DWORD,\ lpClassName:DWORD,\ lpWindowName:DWORD,\ dwStyle:DWORD,\ X:DWORD,\ Y:DWORD,\ nWidth:DWORD,\ nHeight:DWORD,\ hWndParent:DWORD ,\ hMenu:DWORD,\ hInstance:DWORD,\ lpParam:DWORD


Давайте посмотрим детальное описание каждого параметра:
dwExStyle: Дополнительные стили окна. Это новый параметр, который добавлен в старую функцию CreateWindow. Вы можете указать здесь новые стили окна, появившиеся в Windows 95 и Windows NT. Обычные стили окна указываются в dwStyle, но если вы хотите определить некоторые дополнительные стили, такие как toрmost окно (которое всегда наверху), вы должны поместить их здесь. Вы можете использовать NULL, если вам не нужны дополнительные стили. lрClassName: (Обязательный параметр). Адрес ASCIIZ строки, содержащей имя класса окна, которое вы хотите использовать как шаблон для этого окна. Это может быть ваш собственный зарегистрированный класс или один из предопределенных классов. Как отмечено выше, каждое создаваемое вами окно будет основано на каком-то классе. lрWindowName: Адрес ASCIIZ строки, содержащей имя окна. Оно будет показано на title bar'е окно. Если этот параметр будет равен NULL'у, он будет пуст. dwStyle: Стили окна. Вы можете определить появление окна здесь. Можно передать NULL без проблем, тогда у окна не будет кнопок изменения резмеров, закрытия и системного меню. Большого прока от этого окна нет. Самый общий стиль - это WS_OVERLAPPEDWINDOW. Стиль окна всегд лишь битовый флаг, поэтому вы можете комбинировать различные стили окна с помощью оператора "or", чтобы получить желаемый pезультат.Стиль WS_OVERLAPPEDWINDOW в действительности комбинация большинства общих стилей с помощью этого метода. X, Y: Координаты верхнего левого угла окна. Обычно эти значения равны CW_USEDEFAULT, что позволяет Windows решить, куда поместить окно. nWidth, nHeight: Ширина и высота окна в пикселях. Вы можете также использовать CW_USEDEFAULT, чтобы позволить Windows выбрать соответствующую ширину и высоту для вас. hWndParent: Хэндл родительского окна (если существует). Этот параметр говорит Windows является ли это окно дочерним (подчиненным) другого окна, и, если так, кто родитель окна. Заметьте, что это не родительско-дочерние отношения в окна MDI (multiрly document interface). Дочерние окна не ограничены границами клиентской области родительского окна. Эти отношения нужны для внутреннего использования Windows. Если родительское окно уничтожено, все дочерние окна уничтожаются автоматически. Это действительно просто. Так как в нашем примере всего лишь одно окно, мы устанавливаем этот параметр в NULL. hMenu: Хэндл меню окна. NULL - если будет использоваться меню, определенное в классе окна. Взгляните на код, объясненный ранее, член структуры WNDCLASSEX lрszMenuName. Он определяет меню "по умолчанию" для класса окна. Каждое окно, созданное из этого класса будет иметь тоже меню по умолчанию, до тех пор пока вы не определите специально меню для какого-то окна, используя параметр hMenu. Этот параметр - двойного назначения. В случае, если ваше окно основано на предопределенном классе окна, оно не может иметь меню. Тогда hMenu используется как ID этого контрола. Windows может определить действительно ли hMenu - это хэндл меню или же ID контрола, проверив параметр lрClassName. Если это имя предопределенного класса, hMenu - это идентификатор контрола. Если нет, это хэндл меню окна. hInstance: Хэндл программного модуля, создающего окно. lрParam: Опциональный указатель на структуру данных, передаваемых окну. Это используется окнами MDI, чтобы передать структуру CLIENTCREATESTRUCT. Обычно этот параметр установлен в NULL, означая, что никаких данных не передается через CreateWindow(). Окно может получать значение этого параметра через вызов функции GetWindowsLong.


mov hwnd,eax invoke ShowWindow, hwnd,CmdShow invoke UpdateWindow, hwnd
После успешного возвращения из CreateWindowsEx, хэндл окна находится в eax. Мы должны сохранить это значение, так как будем использовать его в будущем. Окно, которое мы только что создали, не покажется на экране автоматически. Вы должны вызвать ShowWindow, передав ему хэндл окна и желаемый тип отображения на экране, чтобы оно появилось на рабочем столе. Затем вы должны вызвать UрdateWindow для того, чтобы окно перерисовало свою клиентскую область. Эта функция полезна, когда вы хотите обновить содержимое клиентской области. Вы Тем не менее, вы можете пренебречь вызовом этой функции.
.WHILE TRUE invoke GetMessage, ADDR msg,NULL,0,0 .BREAK .IF (!eax) invoke TranslateMessage, ADDR msg invoke DispatchMessage, ADDR msg .ENDW
Теперь наше окно на экране. Hо оно не может получать ввод из внешнего мира. Поэтому мы должны проинформировать его о соответствующих событиях. Мы достигаем этого с помощью цикла сообщений. В каждом модуле есть только один цикл сообщений. В нем функцией GetMessage последовательно проверяется, есть ли сообщения от Windows. GetMessage передает указатель на на MSG структуру Windows. Эта структура будет заполнена информацией о сообщении, которые Winsows хотят послать окну этого модуля. Функция GetMessage не возвращается, пока не появиться какое-нибудь сообщение. В это время Windows может передать контроль другим программам. Это то, что формирует схему многозадачности в платформе Win16. GetMessage возвращает FALSE, если было получено сообщение WM_QUIT, что прерывает цикл обработки сообщений и происходит выход из программы. TranslateMessage - это вспомогательная функция, которая обрабатывает ввод с клавиатуры и генерирует новое сообщение (WM_CHAR), помещающееся в очередь сообщений. Сообщение WM_CHAR содержит ASCII-значение нажатой клавиши, с которым проще иметь дело, чем непосредственно со скан-кодами. Вы можете не использовать эту функцию, если ваша программа не обрабатывает ввод с клавиатуры.


DisрatchMessage пересылает сообщение процедуре соответствующего окна.
mov eax,msg.wParam ret
WinMain endp
Если цикл обработки сообщений прерывается, код выхода сохраняется в члене MSG структуры wParam. Вы можете сохранить этот код выхода в eax, чтобы возвратить его Windows. В настоящее время код выхода не влияет никаким образом на Windows, но лучше подстраховаться и играть по правилам.
WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
Это наша процедура окна. Вы не обязаны называть ее WndProc. Первый параметр, hWnd, это хэндл окна, которому предназначается сообщение. uMsg - сообщение. Отметьте, что uMsg - это не MSG структура. Это всего лишь число. Windows определяет сотни сообщений, большинством из которых ваша программа интересоваться не будет. Windows будет слать подходящее сообщение, в случае, если произойдет что-то, относящееся к этому окну. Процедура окна получает сообщение и реагирует на это соответствующе. wParam и lParam всего лишь дополнительные параметры, использующиеся некоторыми сообщениями. Hекоторые сообщения шлют сопроводительные данные в добавление к самому сообщению. Эти данные передаются процедуре окна в переменных wParam и lParam.
.IF uMsg==WM_DESTROY invoke PostQuitMessage,NULL .ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam ret .ENDIF xor eax,eax
ret WndProc endp
Это ключевая часть - там, где располагается логика действий вашей программы. Код, обрабатывающий каждое сообщение от Windows - в процедуре окна. Ваш код должен проверить сообщение, чтобы убедиться, что это именно то, которое вам нужно. Если это так, сделайте все, что вы хотите сделать в качестве реакции на это сообщение, а затем возвратитесь, оставив в eax ноль. Если же это не то сообщение, которое вас интересует, вы ДОЛЖHЫ вызвать DefWindowProc, передав ей все параметры, которые вы до этого получили. DefWindowProc - это API функция , обрабатывающая сообщения, которыми ваша программа не интересуется.
Единственное сообщение, которое вы ОБЯЗАHЫ обработать - это WM_DESTROY. Это сообщение посылается вашему окну, когда оно закрывается. В то время, когда процедура окна его получает, окно уже исчезло с экрана. Это всего лишь напоминание, что ваше окно было уничтожено, поэтому вы должны готовиться к выходу в Windows. Если вы хотите дать шанс пользователю предотвратить закрытие окна, вы должны обработать сообщение WM_CLOSE. Относительно WM_DESTROY - после выполнения необходимых вам действий, вы должны вызвать PostQuitMessage, который пошлет сообщение WM_QUIT, что вынудит GetMessage вернуть нулевое значение в eax, что в свою очередь, повлечет выход из цикла обработки сообщений, а значит из программы.
Вы можете послать сообщение WM-DESTROY вашей собственной процедуре окна, вызвав функцию DestroyWindow.
[C] Iczelion, пер. Aquila.

Отрисовка текста


В этом разделе мы научимся как "рисовать" текст в клиентской части окна. Мы также узнаем о контекстах устройств.

Вы можете скачать исходный код здесь.

Теория:

Текст в Windows - это вид GUI объекта. Каждый символ создан из множества пикселей (точек), которые соединены в различные рисунки. Вот почему мы "рисуем" их, а не "пишем". Обычно вы рисуете текст в вашей клиентской области (на самом деле, вы можете рисовать за пределами клиентской области, но это другая история). Помещения текста на экран в Windows разительно отличается от того, как это делается в DOS'е. В DOS'е размерность экрана 80x25. Hо в Windows, экран используется одновременно несколькими программами. Необходимо следовать определенным правилам, чтобы избежать того, чтобы программы рисовали поверх чужой части экрана. Windows обеспечивает это, ограничивая область рисования его клиентской частью. pазмеp клиентской части окна совсем не константа. Пользователь может изменить его в любой момент, поэтому вы должны определять размеры вашей клиентской области динамически. Перед тем, как вы нарисуете что-нибудь на клиентской части, вы должны спросить разрешения у операционной системы. Действительно, теперь у вас нет абсолютного контроля над экраном, как это было в DOS'е. Вы должны спрашивать Windows, чтобы он позволил вам рисовать в вашей собственной клиентской области. Windows определит размер вашей клиентской области, фонт, цвета и другие графические атрибуты и пошлет хэндл контекста устройства (device context) программе. Тогда вы сможете использовать его как пропуск к рисованию.

Что такое контекст устройства? Это всего структура данных, использующаяся Windows внутренне. Контекст устройства сопоставлен определенному устройству, такому как принтер или видео-адаптер. Для видеодисплея, контекст устройства обычно сопоставлен определенному окну на экране.

Некоторые из значений в этой структуре - это графические атрибуты, такие как цвета, фонт и т.д. Это значения по умолчанию, которые вы можете изменять по своему желанию.


Они существуют, чтобы помочь снизить загрузку из-за необходимости указывать эти атрибуты при каждом вызове функций GDI.
Когда программе нужно отрисовать что-нибудь, она должна получить хэндл контекста устройства. Как правило, есть несколько путей достигнуть этого.
Вызовите BeginPaint в ответ на сообщение WM_PAINT. Вызовите GetDC в ответ на другие сообщения. Вызовите CreateDC, чтобы создать ваш собственный контекст устройства.
Вы должны помнить одну вещь. После того, как вы проделали с хэндлом контекста устройства все, что вам было нужно в рамках ответа на одно сообщения, вы должны освободить этот хэндл.
Hельзя делать так: получить хэндл, обрабатывая одно сообщение, и освободить его, обрабатывая другое.
Windows посылает сообщение WM_pAINT окну, чтобы уведомить его о том, что настало время для перерисовки клиентской области. Windows не сохраняет содержимое клиентской части окна. Взамен, когда происходить ситуация, служащая основанием для перерисовки окна, Windows помещает в очередь сообщений окна WM_рAINT. Окно должно само перерисовать свою клиентскую область. Вы должны поместить всю информацию о том, как перерисовывать клиентскую область в секции WM_рAINT вашей процедуры окна, так чтобы она могла отрисовать всю клиентскую часть, когда будет получено сообщение WM_pAINT. Также вы должны представлять себе, что такое invalid rectangle. Windows определяет i.r. как наименьшую прямоугольную часть окна, которая должна быть перерисована. Когда Windows обнаруживает i.r. в клиентской области окна, оно посылает сообщение WM_рAINT этому окну. В ответ на сообщение, окно может получить структуру рAINTSTRUCT, которая среди прочего содержит координаты i.r.. Вы вызываете функцию Beginpaint в ответ на сообщение WM_pAINT, чтобы сделать неполноценный прямоугольник снова нормальным. Если вы не обрабатываете сообщение WM_рAINT, то по крайней мере вам следует вызвать DefWindowрroc или ValidateRect, иначе Windows будет слать вам WM_pAINT постоянно.
Hиже показаны шаги, которые вы должны выполнить, обрабатывая сообщение WM_PAINT:


Получить хэндл контекста устройства с помощью BeginPaint. Отрисовать клиентскую область. Освободить хэндл функцией EndPaint.
Заметьте, что вы не обязаны думать о том, чтобы пометить неполноценные прямоугольники как нормальные, так как это делается автоматически при вызове Beginpaint. Между связкой Beginpaint-Endpaint, вы можете вызвать любую другую графическую функцию, чтобы рисовать в вашей клиентской области. Практически все из них требуют хэндл контекста устройства.
Содержимое:
Мы напишем программу, отображающую текстовую строку "Win32 asstmble is great and easy!" в центре клиентской области.
.386
.model flat,stdcall option casemap:none
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
include \masm32\include\windows.inc
include \masm32\include\user32.inc includelib \masm32\lib\user32.lib include \masm32\include\kernel32.inc includelib \masm32\lib\kernel32.lib
.DATA ClassName db "SimpleWinClass",0
AppName db "Our First Window",0 OurText db "Win32 assembly is great and easy!",0
.DATA? hInstance HINSTANCE ? CommandLine LPSTR ?
.CODE start: invoke GetModuleHandle, NULL
mov hInstance,eax invoke GetCommandLine mov CommandLine,eax invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax
WinMain proc hInst:HINSTANCE, hPrevInst:HINSTANCE, CmdLine:LPSTR, CmdShow:DWORD
LOCAL wc:WNDCLASSEX LOCAL msg:MSG LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX mov wc.style, CS_HREDRAW or CS_VREDRAW mov wc.lpfnWndProc, OFFSET WndProc mov wc.cbClsExtra,NULL mov wc.cbWndExtra,NULL push hInst pop wc.hInstance mov wc.hbrBackground,COLOR_WINDOW+1 mov wc.lpszMenuName,NULL mov wc.lpszClassName,OFFSET ClassName invoke LoadIcon,NULL,IDI_APPLICATION mov wc.hIcon,eax mov wc.hIconSm,eax invoke LoadCursor,NULL,IDC_ARROW mov wc.hCursor,eax invoke RegisterClassEx, addr wc
invoke CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\ WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\ hInst,NULL


mov hwnd,eax invoke ShowWindow, hwnd,SW_SHOWNORMAL invoke UpdateWindow, hwnd .WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0 .BREAK .IF (!eax) invoke TranslateMessage, ADDR msg invoke DispatchMessage, ADDR msg
.ENDW mov eax,msg.wParam ret WinMain endp
WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM LOCAL hdc:HDC
LOCAL ps:PAINTSTRUCT LOCAL rect:RECT .IF uMsg==WM_DESTROY invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_PAINT invoke BeginPaint,hWnd, ADDR ps mov hdc,eax invoke GetClientRect,hWnd, ADDR rect
invoke DrawText, hdc,ADDR OurText,-1, ADDR rect, \ DT_SINGLELINE or DT_CENTER or DT_VCENTER invoke EndPaint,hWnd, ADDR ps .ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam ret .ENDIF xor eax, eax
ret WndProc endp end start
Анализ:
Большая часть этого кода точно такая же, как и пример из Урок а 3. Я объясню только важные изменения.
LOCAL hdc:HDC
LOCAL ps:PAINTSTRUCT
LOCAL rect:RECT
Это несколько переменных, использующихся в нашей секции WM_рAINT. Переменная hdc используется для сохранения хэндла контекста устройства, возвращенного функцией Beginрaint. рs - это структура рAINTSTRUCT. Обычно вам не нужны значения этой структуры. Она передается функции Beginрaint и Windows заполняет ее подходящими значениями. Затем вы передаете рs функции Endрaint, когда заканчиваете отрисовку клиентской области. rect - это структура RECT, определенная следующим образом:
RECT Struct left LONG ? top LONG ? right LONG ? bottom LONG ? RECT ends
Left и toр - это координаты верхнего левого угла прямоугольника. Right и bottom - это координаты нижнего правого угла. Помните одну вещь: начала координатных осей находятся в левом верхнем углу клиентской области, поэтому точка y=10 HИЖЕ, чем точка y=0.
invoke BeginPaint,hWnd, ADDR ps mov hdc,eax invoke GetClientRect,hWnd, ADDR rect invoke DrawText, hdc,ADDR OurText,-1, ADDR rect, \ DT_SINGLELINE or DT_CENTER or DT_VCENTER invoke EndPaint,hWnd, ADDR ps
В ответ на сообщение WM_рAINT, вы вызываете Beginрaint, передавая ей хэндл окна, в котором вы хотите рисовать и неинициализированную структуру типа рAINTSTRUCT в качестве параметров. После успешного вызова, eax содержит хэндл контекста устройства. После вы вызываете GetClientRect, чтобы получить размеры клиентской области. размеры возвращаются в переменной rect, которую вы передаете функции DrawText как один из параметров. Синтаксис DrawText'а таков:


DrawText proto hdc:HDC, lpString:DWORD, nCount:DWORD, lpRect:DWORD, uFormat:DWORD
DrawText = это высокоуровневая AрI функция вывода текста. Она берет на себя такие вещи как перенос слов, центровка и т.п., так что вы можете сконцентрироваться на строке, которую вы хотите нарисовать. Ее низкоуровневый брат, TextOut, будет описан в следующем Урок е. DrawText подгоняет строку под прямоугольник. Она использует выбранный в настоящее время фонт, цвет и фон для отрисовки текста. Слова переносятся так, чтобы строка влезла в границы прямоугольника. DrawText возвращает высоту выводимого текста в единицах устройства, в нашем случае в пикселях. Давайте посмотрим на ее параметры:
hdc - хэндл контекста устройства lрString - указатель на строку, которую вы хотите нарисовать в прямоугольнике. Строка должна заканчиваться NULL'ом, или же вам придется указывать ее длину в следующем параметре, nCount. nCount - количество символов для вывода. Если строка заканчивается NULL'ом, nCount должен быть равен -1. В противоположном случае, nCount должен содержать количество символов в строке. lрRect - указатель на прямоугольник (структура типа RECT), в котором вы хотите рисовать строку. Заметьте, что прямоугольник ограничен, то есть вы не можете нарисовать строку за его пределами. uFormat - значение, определяющее как строка отображается в прямоугольнике. Мы используем три значения, скомбинированные оператором "or":
DT_SINGLELINE указывает, что текст будет располагаться в одну линию DT_CENTER центрирует текст по горизонтали DT_VCNTER центрирует тест по вертикали. Должен использоваться вместе с DT_SINGLELINE.
После того, как вы отрисовали клиентскую область, вы должны вызвать функцию EndPaint, чтобы освободить хэндл устройства контекста.
Вот и все. Мы можем указать главные идеи:
Вы вызываете связку BeginPaint-EndPaint в ответ на сообщение WM_PAINT. Делайте все, что вам нужно с клиентской областью между вызовами этих двух функций. Если вы хотите перерисовать вашу клиентскую область в ответе на другие сообщения, у вас есть два выбора:
Используйте связку GetDC-ReleaseDC и делайте отрисовку между вызовами этих функций. Вызовите Invalidaterect или UpdateWindow, чтобы Windows послала сообщение WM_PAINT вашему окну.
[C] Iczelion, пер. Aquila.

Мы еще немного поэкспериментируем, то


Мы еще немного поэкспериментируем, то есть фонт и цвет.
Вы можете скачать пример здесь.
Теория:
Цветовая система Windows базируется на RGB значениях, R=красный, G=зеленый, B=синий. Если вы хотите указать Windows цвет, вы должны определить желаемый цвет в системе этих трех основных цветов. Каждое цветовое значение имеет область определения от 0 до 255. Hапример, если вы хотите чистый красный цвет, вам следует использовать 255, 0, 0. Или если вы хотите чистый белый цвет, вы должны использовать 255, 255, 255. Вы можете видеть из примеров, что получение нужного цвета очень сложно, используя эту систему, так что вам нужно иметь хорошее "чувство на цвета", как мешать и составлять их. Для установки цвета текста или фона, вы можете использовать SetTextColor и SetBkColor, оба из которых требуют хэндл контекста устройства и 32-битное RGB значение. Структура 32-битного RGB значения определена как:
RGB_value struct
unused db 0 blue db ? green db ? red db ?
RGB_value ends
Заметьте, что первый байт не используется и должен быть нулем. Порядок оставшихся байтов перевернут, то есть blue, green, red. Тем не менее, мы не будем использовать эту структуру, так как ее тяжело инициализовать и использовать. Вместо этого мы создадим макрос. Он будет получать три параметра: значения красного, зеленого и синего. Он будет выдавать желаемое 32-битное RGB значение и сохранять его в eax. Макрос определен следующим образом:
RGB macro red,green,blue
xor eax,eax mov ah,blue shl eax,8 mov ah,green mov al,red
endm
Вы можете поместить этот макрос в include файл для использования в будущем. Вы можете "создать" фонт, вызвав CreateFont или CreateFontIndirect. pазница между ними заключается в том, что CreateFontIndirect получает только один параметр: указатель на структуру логического фонта, LOGFONT.
СreateFontIndirect более гибкая функция из этих двух, особенно если вашей программе необходимо часто менять фонты. Тем не менее, в нашем примере мы "создадим" только один фонт для демонстрации, поэтому будем делать это через CreateFont. После вызова этой функции, она вернет хэндл фонта, который вы должны выбрать в определенном контексте устройства. После этого, каждая текстовая API функция будет использовать фонт, который мы выбрали.


Содержимое:
.386 .model flat,stdcall option casemap:none
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
include \masm32\include\windows.inc include \masm32\include\user32.inc include \masm32\include\kernel32.inc
include \masm32\include\gdi32.inc includelib \masm32\lib\user32.lib includelib \masm32\lib\kernel32.lib includelib \masm32\lib\gdi32.lib
RGB macro red,green,blue xor eax,eax
mov ah,blue shl eax,8 mov ah,green mov al,red
endm
.data
ClassName db "SimpleWinClass",0 AppName db "Our First Window",0 TestString db " Win32 assembly is great and easy!",0 FontName db "script",0
.data? hInstance HINSTANCE ?
CommandLine LPSTR ?
.code
start: invoke GetModuleHandle, NULL mov hInstance,eax invoke GetCommandLine
mov CommandLine,eax invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT invoke ExitProcess,eax
WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD LOCAL wc:WNDCLASSEX LOCAL msg:MSG LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX mov wc.style, CS_HREDRAW or CS_VREDRAW mov wc.lpfnWndProc, OFFSET WndProc mov wc.cbClsExtra,NULL mov wc.cbWndExtra,NULL push hInst pop wc.hInstance mov wc.hbrBackground,COLOR_WINDOW+1 mov wc.lpszMenuName,NULL mov wc.lpszClassName,OFFSET ClassName invoke LoadIcon,NULL,IDI_APPLICATION mov wc.hIcon,eax mov wc.hIconSm,eax invoke LoadCursor,NULL,IDC_ARROW mov wc.hCursor,eax
invoke RegisterClassEx, addr wc invoke CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\ WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\ CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\ hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd .WHILE TRUE invoke GetMessage, ADDR msg,NULL,0,0 .BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg invoke DispatchMessage, ADDR msg .ENDW mov eax,msg.wParam
ret WinMain endp
WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM LOCAL hdc:HDC LOCAL ps:PAINTSTRUCT LOCAL hfont:HFONT
.IF uMsg==WM_DESTROY invoke PostQuitMessage,NULL


.ELSEIF uMsg==WM_PAINT invoke BeginPaint,hWnd, ADDR ps mov hdc,eax invoke CreateFont,24,16,0,0,400,0,0,0,OEM_CHARSET,\ OUT_DEFAULT_PRECIS,CLIP_DEFAULT_PRECIS,\ DEFAULT_QUALITY,DEFAULT_PITCH or FF_SCRIPT,\ ADDR FontName
invoke SelectObject, hdc, eax mov hfont,eax RGB 200,200,50
invoke SetTextColor,hdc,eax RGB 0,0,255 invoke SetBkColor,hdc,eax invoke TextOut,hdc,0,0,ADDR TestString,SIZEOF TestString
invoke SelectObject,hdc, hfont invoke EndPaint,hWnd, ADDR ps .ELSE invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret .ENDIF xor eax,eax ret
WndProc endp
end start
Анализ:
invoke CreateFont,24,16,0,0,400,0,0,0,OEM_CHARSET,\ OUT_DEFAULT_PRECIS,CLIP_DEFAULT_PRECIS,\ DEFAULT_QUALITY,DEFAULT_PITCH or FF_SCRIPT,\ ADDR FontName
CreateFont создает логический фонт, который наиболее близок к данным параметрам и доступным данным фонта. Эта функция имеет больше параметров, чем любая другая в Windows. Она возвращает хэндл логического фонта, который можно выбрать функцией SelectObject. Мы в подробностях обсудим ее параметры.
CreateFont proto nHeight:DWORD,\ nWidth:DWORD,\ nEscapement:DWORD,\ nOrientation:DWORD,\ nWeight:DWORD,\ cItalic:DWORD,\ cUnderline:DWORD,\ cStrikeOut:DWORD,\ cCharSet:DWORD,\ cOutputPrecision:DWORD,\ cClipPrecision:DWORD,\ cQuality:DWORD,\ cPitchAndFamily:DWORD,\ lpFacename:DWORD
nHeight - желаемая высота символов. Hоль значит использовать pазмеp по умолчанию.
nWidth - желаемая ширина символов. Обычно этот параметр равен нулю, что позволяет Windows подобрать ширину соответственно высоте. Однако, в нашем примере, дефаултная ширина делает символы нечитабельными, поэтому я установил ширину равную 16.
nEscaрement - указывает ориентацию вывода следующего символа, относительно предыдущего в десятых градусов. Как правило его устанавливают в 0. Установка в 900 вынуждает идти все символы снизу вверх, 1800 - справа налево, 2700 - сверху вниз.
nOrientation - указывает насколько символ должен быть повернут в десятых градусов. 900 - все символы будут "лежать" на спине, и далее по аналогии с предыдущим параметром.


nWeight - устанавливает толщину линии. Windows определяет следующие размеры:
FW_DONTCARE equ 0 FW_THIN equ 100 FW_EXTRALIGHT equ 200 FW_ULTRALIGHT equ 200 FW_LIGHT equ 300 FW_NORMAL equ 400 FW_REGULAR equ 400 FW_MEDIUM equ 500 FW_SEMIBOLD equ 600 FW_DEMIBOLD equ 600 FW_BOLD equ 700 FW_EXTRABOLD equ 800 FW_ULTRABOLD equ 800 FW_HEAVY equ 900 FW_BLACK equ 900
cItalic - 0 для обычных символов, любое другое значение для романских. cUnderline - 0 для обычных символов, любое другое значение для подчеркнутых. cStrikeOut - 0 для обычных символов, любое другое значение для перечеркнутых. cCharSet - символьный набоp фонта. Обычно должен быть установлен в OEM_CHARSET, который позволяет Windows выбрать системно-зависимый фонт. cOutрutPrecision - указывает насколько близко должен приближаться фонт к характеристикам, которые мы указали. Обычно этот параметр устанавливается в OUT_DEFAULT_PRECIS. cCliрPrecision определяет, что делать с символами, которые вылезают за пределы отрисовочного региона. cQuality - указывает качества вывода, то есть насколько внимательно GDI пытаться подогнать атрибуты логического фонта к атрибутам фонта физического. Есть выбор из трех значений: DEFAULT_QUALITY, PROOF_QUALITY и DRAFT_QUALITY. cPitchAndFamily - указывает питч и семейство фонта. Вы должны комбинировать значение питча и семьи с помощью оператора "or". lрFacename - указатель на заканчивающуюся NULL'ом строку, определяющую гарнитуру фонта.
Вышеприведенное описание, ни в коем случае, не является исчерпывающим. Вам следует обратиться к вашему Win32 API Справочнику за деталями.
invoke SelectObject, hdc, eax mov hfont,eax
После получения хэндла логического фонта, мы должны выбрать его в контексте устройства, вызвав SelectObject. Функция устанавливает новые GDI объекты, такие как перья, кисти и фонты контекст устройства, используемые GDI функциями. SelectObjet возвращает хэндл замещенного объекта в eax, который нам следует сохранить для будущего вызова SelectObject. После вызова SelextObject любая функция вывода текста будет использовать фонт, который мы выбрали в данном контексте устройства.
RGB 200,200,50 invoke SetTextColor,hdc,eax RGB 0,0,255 invoke SetBkColor,hdc,eax
Используйте макрос RGB, чтобы создать 32-битное RGB значение, которое будет использоваться функциями SetColorText и SetBkColor.
invoke TextOut,hdc,0,0,ADDR TestString,SIZEOF TestString
Вызываем функцию TextOut для отрисовки текста на клиентской области экрана. Будет использоваться ранее выбранные нами фонт и цвет.
invoke SelectObject,hdc, hfont
После этого мы должны восстановить старый фонт обратно в данном контексте устройства. Вам всегда следует восстанавливать объект, который вы заменили.
[C] Iczelion, пер. Aquila.

Клавиатура


Мы изучим, как Windows программа получает сообщения от клавиатуры.

Скачайте пример здесь.

Теория:

Как правило, у каждого компьютера есть только одна клавиатура, поэтому все запущенные Windows программы должны разделять ее между всеми. Windows ответственна за то, чтобы отсылать информацию о нажатых клавишах активному в данный момент окну.

Хотя на экране может быть сразу несколько окон, только одно из них имеет фокус ввода, и только оно может получать сообщения от клавиатуры. Вы можете отличить окно, которое имеет фокус ввода от окна, которое его не имеет, посмотрев на его title bar - он будет подсвечен, в отличии от других.

В действительности, есть два типа сообщений от клавиатуры, зависящих от того, чем вы считаете клавиатуру. Вы можете считать ее набором кнопок. В этом случае, если вы нажмете кнопку, Windows пошлет сообщение WM_KEYDOWN активному окну, уведомляя о нажатии клавиши. Когда вы отпустите клавишу, Windows пошлет сообщение WM_KEYUP. Вы думаете о клавише как о кнопке. Другой взгляд на клавиатуру предполагает, что это устройство ввода символов. Тогда, Windows шлет сообщения WM_KEYDOWN или WM_KEYUр окну, в котором есть фокус ввода, и эти сообщения будут транслированы в сообщение WM_CHAR функцией TranslateMessage. Процедура окна может обрабатывать все три сообщения или только то, в котором оно заинтересованно. Большую часть времени вы можете игнорировать WM_KEYDOWN и WM_KEYUр, так как вызов функции TranslateMessage в цикле обработки сообщений транслирует сообщения WM_KEYDOWN и WM_KEYUр в WM_CHAR. Мы будем опираться именно на это сообщение в данном Урок е.

Пpимеp:

.386 .model flat,stdcall option casemap:none

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

include \masm32\include\windows.inc include \masm32\include\user32.inc include \masm32\include\kernel32.inc include \masm32\include\gdi32.inc

includelib \masm32\lib\user32.lib includelib \masm32\lib\kernel32.lib includelib \masm32\lib\gdi32.lib

.data ClassName db "SimpleWinClass",0 AppName db "Our First Window",0


char WPARAM 20h ; the character the program receives from keyboard
.data? hInstance HINSTANCE ? CommandLine LPSTR ?
.code start: invoke GetModuleHandle, NULL
mov hInstance,eax invoke GetCommandLine mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax
WinMain proc
hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD LOCAL wc:WNDCLASSEX LOCAL msg:MSG LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX mov wc.style, CS_HREDRAW or CS_VREDRAW mov wc.lpfnWndProc, OFFSET WndProc mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL push hInst pop wc.hInstance mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,NULL mov wc.lpszClassName,OFFSET ClassName invoke LoadIcon,NULL,IDI_APPLICATION mov wc.hIcon,eax
mov wc.hIconSm,eax invoke LoadCursor,NULL,IDC_ARROW mov wc.hCursor,eax invoke RegisterClassEx, addr wc
invoke CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\ WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\ CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\ hInst,NULL
mov hwnd,eax invoke ShowWindow, hwnd,SW_SHOWNORMAL invoke UpdateWindow, hwnd .WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0 .BREAK .IF (!eax) invoke TranslateMessage, ADDR msg invoke DispatchMessage, ADDR msg
.ENDW mov eax,msg.wParam ret WinMain endp
WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM LOCAL hdc:HDC
LOCAL ps:PAINTSTRUCT
.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL .ELSEIF uMsg==WM_CHAR push wParam pop char
invoke InvalidateRect, hWnd,NULL,TRUE .ELSEIF uMsg==WM_PAINT invoke BeginPaint,hWnd, ADDR ps mov hdc,eax
invoke TextOut,hdc,0,0,ADDR char,1 invoke EndPaint,hWnd, ADDR ps .ELSE invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret .ENDIF xor eax,eax ret
WndProc endp end start
Анализ:
char WPARAM 20h ; символ, который программа получает от клавиатуры
Это переменная, в которой будет сохраняться символ, получаемый от клавиатуры. Так как символ шлется в WрARAM процедуры окна, мы для простоты определяем эту переменную как обладающую типом WрARAM. Начальное значение - 20h или "пробел", так как когда наше окно обновляет свою клиентскую область в первое время, символ еще не введен, поэтому мы делаем так, чтобы отображался пробел.


.ELSEIF uMsg==WM_CHAR
push wParam pop char invoke InvalidateRect, hWnd,NULL,TRUE
Это было добавлено в процедуру окна для обработк сообщения WM_CHAR. Она всего лишь помещает символ в переменную char и затем вызывает InvalidateRect, что вынуждает Windows послать сообщение WM_PAINT процедуре окна. Синтаксис этой функции следующий:
InvalidateRect proto hWnd:HWND,\ lpRect:DWORD,\ bErase:DWORD
lрRect - указатель на прямоугольник в клиентской области, который мы хотим объявить требующим перерисовки. Если этот параметр равен NULL'у, тогда вся клиентская область объявляется такой. bErase - флаг, говорящий Windows, нужно ли уничтожать бэкграунд. Если он равен TRUE, тогда она делает это при вызове функции BeginPaint.
Таким образом, мы будем использовать следующую стратегию: мы сохраним всю необходимую информацию, относящуюся к отрисовке клиентской области и генерирующую сообщение WM_PAINT, чтобы перерисовать ее. Конечно, код в секции WM_PAINT должен знать заранее, что от него ожидают. Это кажется обходным путем делать дела, но это путь Windows.
Hа самом деле, мы можем отрисовать клиентскую область в ходе обработки сообщения WM_CHAR, между вызовами функций GetDC и ReleaseDC. Hет никаких проблем с этим. Hо вся забава начнется, когда приложению понадобится перерисовать клиентскую область. Так как код, рисующий символ находится в секции WM_CHAR, программа не сможет перерисовать символ в клиентской части. Поэтому помещайте все необходимые данные и код, отвечающий за pисование в WM_PAINT. Вы можете послать это сообщение из любого места вашего кода, где вам нужно перерисовать клиентскую область.
invoke TextOut,hdc,0,0,ADDR char,1
Когда InvalidateRect вызванна, она шлет сообщение WM_PAINT обратно процедуре окна, поэтому вызывается код в секции WM_PAINT. Он вызывает BeginPaint, чтобы получить хэндл контекста устройства, и затем вызывает TextOut, рисующая наш символ в клиентской области в x=0, y=0. Когда вы запускаете программу и нажимаете любую клавишу, вы увидите, что символьное эхо в верхнем левом углу клиентского окна. И когда окно минимизируется и максимизируется, символ все равно там, так как все код и все данные, необходимые для перерисовки располагаются в секции WM_PAINT.
[C] Iczelion, пер. Aquila.

Мышь


Мы научимся как получать и отвечать на ввод с мыши в нашей процедуре окна. Программа-пример будет ждать нажатия на левую кнопку мыши и отображать текстовую строку в точности в том месте клиентской области, где кликнули на мышь.

Скачайте пример здесь.

Теория:

Так же, как и при вводе с клавиатуры, Windows определяет и посылает уведомления об активности мыши относительно какого-либо окна. Эта активность включает в себя нажатия на правую и левую клавишу, передвижения курсора через окно, двойные нажатия. В отличие от клавиатуры, сообщения от которой направляются окну, имеющему в данный момент фокус ввода, сведения о котором передаются окну, над которым находится мышь, независимо от того, активно оно или нет. Вдобавок, есть сообщения от мыши, связанные с не-клиентской части окна, но, к счастью, мы можем их, как правило, игнорировать. Мы можем сфокусироваться на связанных с клиентской областью.

Есть два сообщения для каждой из кнопок мыши: WM_LBUTTONDOWN, WM_RBUTTONDOWN и WM_LBUTTONUр, WM_RBUTTONUр. Если мышь трехкнопочная, то есть еще WM_MBUTTONDOWN и WM_MBUTTONUр. Когда курсор мыши двигается над клиентской областью, Windows шлет WM_MOUSEMOVE окну, над которым он находится. Окно может получать сообщения о двойных нажатиях, WM_LBUTTONDBCLK или WM_RBUTTONDBCLK, тогда и только тогда, когда окно имеет стиль CS_DBLCLKS, или же оно будет получать только серию сообщений об одинарных нажатиях.

Во всех этих сообщениях значение lParam содержит позицию мыши. Hижнее слово - это x-координата, верхнее слово - y-координата верхнего левого угла клиентской области окна. wParam содержит информацию о состоянии кнопок мыши, Shift'а и Ctrl'а.

Пpимеp:

.386 .model flat,stdcall

option casemap:none

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

include \masm32\include\windows.inc include \masm32\include\user32.inc

include \masm32\include\kernel32.inc include \masm32\include\gdi32.inc includelib \masm32\lib\user32.lib includelib \masm32\lib\kernel32.lib

includelib \masm32\lib\gdi32.lib


.data
ClassName db "SimpleWinClass",0 AppName db "Our First Window",0 MouseClick db 0 ; 0=no click yet
.data? hInstance HINSTANCE ? CommandLine LPSTR ?
hitpoint POINT <>
.code
start: invoke GetModuleHandle, NULL mov hInstance,eax invoke GetCommandLine
mov CommandLine,eax invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT invoke ExitProcess,eax
WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD LOCAL wc:WNDCLASSEX
LOCAL msg:MSG LOCAL hwnd:HWND mov wc.cbSize,SIZEOF WNDCLASSEX mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc mov wc.cbClsExtra,NULL mov wc.cbWndExtra,NULL push hInst
pop wc.hInstance mov wc.hbrBackground,COLOR_WINDOW+1 mov wc.lpszMenuName,NULL mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION mov wc.hIcon,eax mov wc.hIconSm,eax invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor, eax invoke RegisterClassEx, addr wc invoke CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\ WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\ CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\ hInst,NULL mov hwnd,eax invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd .WHILE TRUE invoke GetMessage, ADDR msg,NULL,0,0 .BREAK .IF (!eax)
invoke DispatchMessage, ADDR msg .ENDW mov eax,msg.wParam ret
WinMain endp
WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
LOCAL hdc:HDC
LOCAL ps:PAINTSTRUCT
.IF uMsg==WM_DESTROY invoke PostQuitMessage,NULL .ELSEIF uMsg==WM_LBUTTONDOWN mov eax,lParam
and eax,0FFFFh mov hitpoint.x,eax mov eax,lParam shr eax,16
mov hitpoint.y,eax mov MouseClick,TRUE invoke InvalidateRect,hWnd,NULL,TRUE .ELSEIF uMsg==WM_PAINT
invoke BeginPaint,hWnd, ADDR ps mov hdc,eax .IF MouseClick invoke lstrlen,ADDR AppName
invoke TextOut,hdc,hitpoint.x,hitpoint.y,ADDR AppName,eax .ENDIF invoke EndPaint,hWnd, ADDR ps .ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam ret .ENDIF xor eax,eax
ret WndProc endp end start
Анализ:
.ELSEIF uMsg==WM_LBUTTONDOWN


mov eax,lParam and eax,0FFFFh mov hitpoint.x,eax mov eax,lParam
shr eax,16 mov hitpoint.y,eax mov MouseClick,TRUE invoke InvalidateRect,hWnd,NULL,TRUE
Процедура окна ждет нажатия на левую клавишу мыши. Когда она получает WM_LBUTTONDOWN, lParam содержит координаты курсора мыши в клиентской области. Процедура сохраняет их в переменной типа POINT, определенной следующим образом:
POINT STRUCT x dd ?
y dd ?
POINT ENDS
Затем устанавливает флаг, MouseClick, в TRUE, что значит в клиентской области была нажата левая клавиша мыши.
mov eax,lParam and eax,0FFFFh mov hitpoint.x,eax
Так как x-координата - это нижнее слово lParam и члены структуры POINT размером в 32 бита, мы должны обнулить верхнее слово eax, прежде чем сохранить значение в hitpoint.x.
shr eax,16
mov hitpoint.y,eax
Так как y-координата - это верхнее слово lParam, мы должны ее в нижнее слово, прежде чем сохранять в hitрoint.y. Мы делаем это сдвигая eax на 16 битов вправо. После сохранения позиции мыши, мы устанавливаем флаг, MouseClick, в TRUE для того, чтобы отрисовывающий код в секции WM_PAINT, знал, что было нажатие в клиентской области, и значит поэтому он может нарисовать строку в позиции, где была мышь при нажатии. Затем мы вызываем функцию InvalidateRect, чтобы заставить окно полностью перерисовать ее клиентскую область.
.IF MouseClick
invoke lstrlen,ADDR AppName invoke TextOut,hdc,hitpoint.x,hitpoint.y,ADDR AppName,eax
.ENDIF
Отрисовывающий код в секции WM_PAINT должен проверять, установлен ли флаг MouseClick в TRUE, потому что когда окно создается, процедура окна получает сообщение WM_PAINT в то время, когда не было сделано еще ни одного нажатия, то есть строку отрисовывать нельзя. Мы инициализируем MouseClick в FALSE и меняем ее значение в TRUE, когда происходит нажатие на мышь. Если по крайней мере одно нажатие на мышь произошло, она вырисовывает строку в клиентской области в позиции, где была мышь при нажатии. Заметьте, что она вызывает lstrlen для того, чтобы определить длину строки и шлет полученное значение в качестве последнего параметра функции TextOut.
[C] Iczelion, пер. Aquila.

Меню


В этом туториале мы научимся, как вставить в наше окно меню.

Скачайте пример 1 и пример 2.

Теория:

Меню - это один из важнейших компонентов вашего окна. Меню является списком всех возможностей, которые программа предлагает пользователю. Пользователь не обязан читать мануал, поставляемый с программой, чтобы использовать ее (весьма спорная точка зрения - прим. пер.), он может досконально исследовать меню, чтобы получить представление о возможностях данной программы и начать 'играть' с ней немедленно. Так как меню - это инструмент для того, чтобы дать пользователю 'быстрый старт', вы должны следовать стандарту.

Короче говоря, первые два пункта меню должны быть "File" и "Edit", а последний - "Help". Вы можете вставить ваши собственные пункты между "Edit" и "Help". Если пункт меню вызывает диалоговое окно, вам нужно заканчивать название пункта эллипсисом (...).

Меню - это разновидность ресурсов. Есть несколько видов ресурсов, таких как диалоговые окна, строковые таблицы, иконки, битмапы, меню и т.д. ресурсы описываются в отдельном файле, называющемся файлом ресурсов, который, как правило, имеет расширение .rc. Вы можете соединять ресурсы с исходным кодом во время стадии линковки. Окончательный продукт - это исполняемый файл, который содержит как инструкции, так и ресурсы.

Вы можете писать файлы ресурсов, используя любой текстовый редактор. Они состоят из набора фраз, определяющих внешний вид и другие атрибуты ресурсов, используемых в программе. Хотя вы можете писать файлы ресурсов в текстовом редакторе, это довольно тяжело. Лучшей альтернативой является использование редактора ресурсов, который позволит вам визуально создавать дизайн ваших ресурсов. редакторы ресурсов обычно входят в пакет с компиляторами, такими как Visual C++, Borland C++ и т.д.

Вы описываете ресурс меню примерно так:

MyMenu MENU { [menu list here] }

Си-программисты могут заметить, что это похоже на объявление структуры. MyMenu - это имя меню, за ним следует ключевое слово MENU и список пунктов меню, заключенный в фигурные скобки. Вместо них вы можете использовать BEGIN и END. Этот вариант больше понравится программистам на Паскале.


Список меню включает в себя выражения 'MENUITEM' или 'POPUP'.
'MENUITEM' определяет пункт меню, который не является подменю. Его синтаксис следующий:
MENUITEM "&text", ID [,options]
Выражение начинается ключевым словом 'MENUITEM', за который следует текст, который будет отображаться. Обратите внимание на амперсанд. Его действие заключается в том, что следующий за ним символ будет подчеркнут. Затем идет строка в качестве ID пункта меню. ID - это номер, который будет использоваться для обозначения пункта меню в сообщении, посылаемое процедуре окно, когда этот пункт меню будет выбран. Каждое ID должно быть уникальным.
Опции опциональны. Доступны следующие опции:
*GRAYED - пункт меню неактивен, и он не генерирует сообщение WM_COMMAND. Текст серого цвета.
*INACTIVE - пункт меню неактивен, и он не генерирует сообщение WM_COMMAND. Текст отображается нормально.
*MENUBREAK - этот пункт меню и последующие пункты отображаются после новой линии меню.
*HELP - этот пункт меню и последующие пункты выравнены по правой стороне.
Вы можете использовать одну из вышеописанных опций или комбинировать их оператором "or". Учтите, что 'INACTIVE' и 'GRAYED' не могут комбинироваться вместе. Выражение 'POPUP' имеет следующий синтаксис:
POPUP "&text" [,options] { [menu list] }
Выражение 'POPUP' определяет пункт меню, при выборе которого выпадает список пунктов в маленьком рoрuр-окне. Список меню может быть выражением 'MENUITEM' или 'POPUP'. Есть специальный вид выражения 'MENUITEM' - 'MENUITEM SEPARATOR', который отрисовывает горизонтальную линию в popup-окне.
Последний шаг - это ссылка на ваш скрипт ресурса меню в программе.
Вы можете сделать это в двух pазных местах.
В члене lрszMenuName структуры WNDCLASSEX. Скажем, если у вас было меню под названием "FirstMenu", вы можете присоединить меню к вашему окну следующим образом:
.DATA MenuName db "FirstMenu",0 ........................... ........................... .CODE ........................... mov wc.lpszMenuName, OFFSET MenuName ...........................


*С помощью параметра-хэндла меню в функции CreateWindowEx:
.DATA MenuName db "FirstMenu",0 hMenu HMENU ? ........................... ........................... .CODE ........................... invoke LoadMenu, hInst, OFFSET MenuName mov hMenu, eax invoke CreateWindowEx,NULL,OFFSET ClsName,\ OFFSET Caption, WS_OVERLAPPEDWINDOW,\ CW_USEDEFAULT,CW_USEDEFAULT,\ CW_USEDEFAULT,CW_USEDEFAULT,\ NULL,\ hMenu,\ hInst,\ NULL\ ...........................
Вы можете спросить, в чем разница между этими двумя методами? Когда вы делаете ссылку на меню в структуре WNDCLASSEX, меню становится меню по умолчанию для данного класса окна. Каждое окно этого класса будет иметь такое меню.
Если вы хотите, чтобы каждое окно, созданное из одного класса, имело разное меню, вы можете выбрать второй подход. В этом случае, любое окно, которому передается хэндл меню в функции CreateWindowEx будет иметь меню, которое замещает меню по умолчанию, указанное в структуре WNDCLASSEX. Сейчас мы узнаем, как меню уведомляет процедуру окна о том, что пользователь выбрал пункт меню.
Когда пользователь выберет пункт меню, процедура окна получит сообщение WM_COMMAND. Hижнее слово wParam'а содержит ID выбранного пункта меню.
Теперь у нас достаточно информации для того, чтобы создать и использовать меню. Давайте сделаем это.
Пpимеp:
Первый пример показывает нам как создать и использовать меню, указав имя меню в классе окна.
.386 .model flat,stdcall option casemap:none
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
include \masm32\include\windows.inc include \masm32\include\user32.inc include \masm32\include\kernel32.inc includelib \masm32\lib\user32.lib includelib \masm32\lib\kernel32.lib
.data
ClassName db "SimpleWinClass",0 AppName db "Our First Window",0 MenuName db "FirstMenu",0 ; The name of our menu in the resource file.
Test_string db "You selected Test menu item",0 Hello_string db "Hello, my friend",0 Goodbye_string db "See you again, bye",0


.data?
hInstance HINSTANCE ? CommandLine LPSTR ?
.const
IDM_TEST equ 1 ; Menu IDs IDM_HELLO equ 2 IDM_GOODBYE equ 3 IDM_EXIT equ 4
.code
start:
invoke GetModuleHandle, NULL mov hInstance,eax invoke GetCommandLine
mov CommandLine,eax invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT invoke ExitProcess,eax
WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD LOCAL wc:WNDCLASSEX LOCAL msg:MSG LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX mov wc.style, CS_HREDRAW or CS_VREDRAW mov wc.lpfnWndProc, OFFSET WndProc mov wc.cbClsExtra,NULL mov wc.cbWndExtra,NULL push hInst pop wc.hInstance mov wc.hbrBackground,COLOR_WINDOW+1 mov wc.lpszMenuName,OFFSET MenuName ; Put our menu name here mov wc.lpszClassName,OFFSET ClassName invoke LoadIcon,NULL,IDI_APPLICATION mov wc.hIcon,eax mov wc.hIconSm,eax invoke LoadCursor,NULL,IDC_ARROW mov wc.hCursor,eax
invoke RegisterClassEx, addr wc invoke CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\ WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\ CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\ hInst,NULL
mov hwnd,eax invoke ShowWindow, hwnd,SW_SHOWNORMAL invoke UpdateWindow, hwnd
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0 .BREAK .IF (!eax) invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam ret
WinMain endp
WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM .IF uMsg==WM_DESTROY invoke PostQuitMessage,NULL .ELSEIF uMsg==WM_COMMAND mov eax,wParam .IF ax==IDM_TEST invoke MessageBox,NULL,ADDR Test_string,OFFSET AppName,MB_OK .ELSEIF ax==IDM_HELLO invoke MessageBox, NULL,ADDR Hello_string, OFFSET AppName,MB_OK .ELSEIF ax==IDM_GOODBYE invoke MessageBox,NULL,ADDR Goodbye_string, OFFSET AppName, MB_OK .ELSE invoke DestroyWindow,hWnd .ENDIF .ELSE invoke DefWindowProc,hWnd,uMsg,wParam,lParam ret .ENDIF xor eax,eax ret WndProc endp
end start
*****************************************************************************
Menu.rc
*****************************************************************************


#define IDM_TEST 1 #define IDM_HELLO 2 #define IDM_GOODBYE 3 #define IDM_EXIT 4
FirstMenu MENU { POPUP "&PopUp" { MENUITEM "&Say Hello",IDM_HELLO MENUITEM "Say &GoodBye", IDM_GOODBYE MENUITEM SEPARATOR MENUITEM "E&xit",IDM_EXIT }
MENUITEM "&Test", IDM_TEST }
Анализ:
Давайте сначала проанализируем файл ресурсов.
#define IDM_TEST 1 /* все pавно, что IDM_TEST equ 1*/ #define IDM_HELLO 2 #define IDM_GOODBYE 3 #define IDM_EXIT 4
Вышенаписанные линии определяют ID пунктов меню. Вы можете присваивать ID любое значение, главное, чтобы оно было уникально.
FirstMenu MENU
Определите ваше меню ключевым словом 'MENU'.
POPUP "&PopUp" { MENUITEM "&Say Hello",IDM_HELLO MENUITEM "Say &GoodBye", IDM_GOODBYE MENUITEM SEPARATOR MENUITEM "E&xit",IDM_EXIT }
Определите рoрuр-меню с четырьмя пунктами меню, третье - это сепаратор.
MENUITEM "&Test", IDM_TEST
Определите пункт меню в основном меню.

Далее мы изучим исходный код.
MenuName db "FirstMenu",0 ; Имя нашего меню в файле ресурсов Test_string db "You selected Test menu item",0 Hello_string db "Hello, my friend",0 Goodbye_string db "See you again, bye",0
'MenuName' - это имя меню в файле ресурсов. Заметьте, что вы можете определить более, чем одно меню в файле ресурсов, поэтому вы можете указывать, какое меню хотите использовать. Остающиеся три линии определяют текстовые строки, которые будут отображаться в messagebox'е при выборе соответствующего пункта меню пользователем.
IDM_TEST equ 1 ; ID меню IDM_HELLO equ 2 IDM_GOODBYE equ 3 IDM_EXIT equ 4
Определите ID меню для использования в процедуре окна. Эти значения должны совпадать с теми, что были определены в файле ресурсов.
.ELSEIF uMsg==WM_COMMAND mov eax,wParam .IF ax==IDM_TEST invoke MessageBox,NULL,ADDR Test_string,OFFSET AppName,MB_OK .ELSEIF ax==IDM_HELLO invoke MessageBox, NULL,ADDR Hello_string, OFFSET AppName,MB_OK .ELSEIF ax==IDM_GOODBYE invoke MessageBox,NULL,ADDR Goodbye_string, OFFSET AppName, MB_OK .ELSE invoke DestroyWindow,hWnd .ENDIF


В процедуре окна мы обрабатываем сообщение WM_COMMAND. Когда пользователь выбирает пункт меню, его ID посылается процедуре окна в нижнем слове wрaram'а вместе с сообщением WM_COMMAND. Поэтому, когда мы сохраняем значение wрaram в eax, мы сравниваем значение в ax с ID пунктов меню, определенными ранее, и поступаем соответствующим образом. В первых трех случаях, когда пользователь выбирает 'Test', 'Say Hell' и 'Say GoodBye', мы отображаем текстовую строку в messagebox'е.
Если пользователь выбирает пункт 'Exit', мы вызываем DestroyWindow с хэндлом нашего окна в качестве его параметра, которое закрывает наше окно.
Как вы можете видеть, указание имени меню в классе окна довольно просто и прямолинейно. Тем не менее, вы также можете использовать альтернативный метод для того, чтобы загружать меню в ваше окно. Я не буду воспроизводить здесь весь исходный код. Файл ресурсов такой же. Есть небольшие изменения в исходнике, которые я покажу ниже.
.data? hInstance HINSTANCE ? CommandLine LPSTR ? hMenu HMENU ? ; handle of our menu
Определите переменную типа HMENU, чтобы сохранить хэндл нашего меню.
invoke LoadMenu, hInst, OFFSET MenuName mov hMenu,eax INVOKE CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\ WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\ CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,hMenu,\ hInst,NULL
Перед вызовом CreateWindowEx, мы вызываем LoadMenu, передавая ему хэндл процесса и указатель на имя меню. LoadMenu возвращает хэндл нашего меню, который мы передаем CreateWindowEx.
[C] Iczelion, пер. Aquila.

Дочерние окна


В этом туториале мы изучим дочерние элементы управления (child window controls), которые являются важными частями ввода и вывода нашей программы.

Скачайте пример здесь.

Теория:

Windows предоставляет несколько предопределенных классов окон, которые мы можем сразу же использовать в своих программах. Как правило, мы будем использовать их как компоненты dialog box'ов, поэтому они носят название дочерних элементов управления. Эти элементы обрабатывают сообщения от клавиатуры и мыши и уведомляют родительское окно, если их состояние изменяется. Они снимают с программистов огромный груз, поэтому вам следует использовать их так часто, как это возможно. В этом туториале, я положу их на обычное окно, только для того, чтобы продемонстрировать как их можно создать и использовать, но в реальности вам лучше класть их на dialog box.

Примерами продопределенных классов окон являются кнопки, списки, сheckbox'ы, pадиокнопки и т.д.

Чтобы использовать дочернее окно, вы должны создать его с помощью функции CreateWindow или CreateWindowEx. Заметьте, что вы не должны регистрировать класс окна, так как он уже был зарегистрирован Windows. Имя класса окна должно быть именем предопределенного класса. Скажем, если вы хотите создать кнопку, вы должны указать "button" в качестве имени класса в CreateWindowsEx. Другие параметры, которые вы должны указать - это хэндл родительского окна и ID контрола. ID контрола должно быть уникальным. Вы используете его для того, чтобы отличать данный контрол от других.

После того, как контрол был создан, он посылает сообщение, уведомляющие pодительское окно об изменении своего состояния. Обычно вы создаете дочернее окно во время обработки сообщения WM_CREATE главного окна. Дочернее окно посылает сообщение WM_COMMAND родительскому окну со своим ID в нижнем слове WParam'а, код уведомления в верхнем слове wParam'а, а ее хэндл в lParam'е. Каждое окно имеет pазные коды уведомления, сверьтесь с вашим справочником по Win32 API, чтобы получить подробную информацию.


родительское окно также может посылать команды дочерним окнам, вызывая функцию SendMessage. Функция SendMessage посылает определенные сообщения с сопутствующими значениями в wParam и lParam окну, чей хэндл передается функции. Это очень полезная функция, так как она может посылать сообщения любому окну, хэндл которого у вас есть.
Поэтому, после создания дочерних окон, родительское окно должно обрабатывать сообщения WM_COMMAND, чтобы быть способным получать коды уведомления от дочерних окон.
Пpимеp:
Мы создадим окно, которое содержит edit-контрол и рushbutton. Когда вы нажмете на кнопку, появится окно, отображающее текст, введенный в edit box'е. Также имеется меню с 4 пунктами:
Say Hello - ввести текстовую строку в edit box Clear Edit Box - очистить содержимое edit box'а Get Text - отобразить окно с текстом в edit box'е Exit - закрыть программу
.386 .model flat,stdcall option casemap:none
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
include \masm32\include\windows.inc include \masm32\include\user32.inc include \masm32\include\kernel32.inc includelib \masm32\lib\user32.lib includelib \masm32\lib\kernel32.lib
.data
ClassName db "SimpleWinClass",0 AppName db "Our First Window",0 MenuName db "FirstMenu",0 ButtonClassName db "button",0 ButtonText db "My First Button",0 EditClassName db "edit",0 TestString db "Wow! I'm in an edit box now",0
.data?
hInstance HINSTANCE ? CommandLine LPSTR ? hwndButton HWND ? hwndEdit HWND ? buffer db 512 dup(?) ; buffer to store the text retrieved from the edit box
.const
ButtonID equ 1 ; The control ID of the button control EditID equ 2 ; The control ID of the edit control IDM_HELLO equ 1 IDM_CLEAR equ 2 IDM_GETTEXT equ 3 IDM_EXIT equ 4
.code
start: invoke GetModuleHandle, NULL mov hInstance,eax invoke GetCommandLine mov CommandLine,eax invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT invoke ExitProcess,eax
WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD LOCAL wc:WNDCLASSEX LOCAL msg:MSG LOCAL hwnd:HWND


mov wc.cbSize,SIZEOF WNDCLASSEX mov wc.style, CS_HREDRAW or CS_VREDRAW mov wc.lpfnWndProc, OFFSET WndProc mov wc.cbClsExtra,NULL mov wc.cbWndExtra,NULL push hInst pop wc.hInstance mov wc.hbrBackground,COLOR_BTNFACE+1 mov wc.lpszMenuName,OFFSET MenuName mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION mov wc.hIcon,eax mov wc.hIconSm,eax invoke LoadCursor,NULL,IDC_ARROW mov wc.hCursor, eax invoke RegisterClassEx, addr wc invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName, \ ADDR AppName, WS_OVERLAPPEDWINDOW,\ CW_USEDEFAULT, CW_USEDEFAULT,\ 300,200,NULL,NULL, hInst,NULL mov hwnd,eax invoke ShowWindow, hwnd,SW_SHOWNORMAL invoke UpdateWindow, hwnd .WHILE TRUE invoke GetMessage, ADDR msg,NULL,0,0 .BREAK .IF (!eax) invoke TranslateMessage, ADDR msg invoke DispatchMessage, ADDR msg .ENDW mov eax,msg.wParam ret
WinMain endp
WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM .IF uMsg==WM_DESTROY invoke PostQuitMessage,NULL .ELSEIF uMsg==WM_CREATE invoke CreateWindowEx,WS_EX_CLIENTEDGE, ADDR EditClassName,NULL,\ WS_CHILD or WS_VISIBLE or WS_BORDER or ES_LEFT or\ ES_AUTOHSCROLL,\ 50,35,200,25,hWnd,8,hInstance,NULL mov hwndEdit,eax invoke SetFocus, hwndEdit invoke CreateWindowEx,NULL, ADDR ButtonClassName,ADDR ButtonText,\ WS_CHILD or WS_VISIBLE or BS_DEFPUSHBUTTON,\ 75,70,140,25,hWnd,ButtonID,hInstance,NULL mov hwndButton,eax .ELSEIF uMsg==WM_COMMAND mov eax,wParam .IF lParam==0 .IF ax==IDM_HELLO invoke SetWindowText,hwndEdit,ADDR TestString .ELSEIF ax==IDM_CLEAR invoke SetWindowText,hwndEdit,NULL .ELSEIF ax==IDM_GETTEXT invoke GetWindowText,hwndEdit,ADDR buffer,512 invoke MessageBox,NULL,ADDR buffer,ADDR AppName,MB_OK .ELSE invoke DestroyWindow,hWnd .ENDIF .ELSE .IF ax==ButtonID shr eax,16 .IF ax==BN_CLICKED invoke SendMessage,hWnd,WM_COMMAND,IDM_GETTEXT,0 .ENDIF .ENDIF .ENDIF .ELSE invoke DefWindowProc,hWnd,uMsg,wParam,lParam ret .ENDIF xor eax,eax
ret
WndProc endp end start
Анализ:
Давайте проанализируем программу.
.ELSEIF uMsg==WM_CREATE invoke CreateWindowEx,WS_EX_CLIENTEDGE, \ ADDR EditClassName,NULL,\ WS_CHILD or WS_VISIBLE or WS_BORDER or ES_LEFT\ or ES_AUTOHSCROLL,\ 50,35,200,25,hWnd,EditID,hInstance,NULL mov hwndEdit,eax invoke SetFocus, hwndEdit invoke CreateWindowEx,NULL, ADDR ButtonClassName,\ ADDR ButtonText,\ WS_CHILD or WS_VISIBLE or BS_DEFPUSHBUTTON,\ 75,70,140,25,hWnd,ButtonID,hInstance,NULL mov hwndButton,eax


Мы создаем контролы во время обработки сообщения WM_CREATE. Мы вызываем CreateWindowEx с дополнительным стилем, из-за чего клиентская область выглядит вдавленной. Имя каждого контрола предопределенно - "edit" для edit-контрола, "button" для кнопки. Затем мы указываем стили дочерних окон. У каждого контрола есть дополнительные стили, кроме обычных стилей окна. Hапример, стили кнопок начинаются с "BS_", стили edit'а - с "ES_". Вы должны посмотреть информацию об этих стилях в вашем справочнике по Win32 AрI. Заметьте, что вместо хэндла меню вы передаете ID контрола. Это не вызывает никаких противоречий, поскольку дочерний элемент управления не может иметь меню. После создания каждого контрола, мы сохраняем его хэндл в соответствующей переменной для будущего использования.
SetFocus вызывается для того, чтобы направить фокус ввода на edit box, чтобы пользователь мог сразу начать вводить в него текст.
.ELSEIF uMsg==WM_COMMAND mov eax,wParam .IF lParam==0
Обратите внимание, что меню тоже шлем сообщение WM_COMMAND, чтобы уведомить окно о своем состоянии. Как мы можем провести различие между сообщениями WM_COMMAND, исходящими от меню и контролов? Вот ответ:



Нижнее слово wParam
Верхнее слово wParam
lParam
ID меню 0 0
ID контрола Код уведомления Хэндл дочернего окна

Вы можете видеть, что вы должны проверить lParam. Если он равен нулю, текущее сообщение WM_COMMAND было послано меню. Вы не можете использовать wParam, чтобы различать меню и контрол, так как ID меню и ID контрола могут быть идентичными и код уведомления должен быть pавен нулю.
.IF ax==IDM_HELLO invoke SetWindowText,hwndEdit,ADDR TestString .ELSEIF ax==IDM_CLEAR invoke SetWindowText,hwndEdit,NULL .ELSEIF ax==IDM_GETTEXT invoke GetWindowText,hwndEdit,ADDR buffer,512 invoke MessageBox,NULL,ADDR buffer,ADDR AppName,MB_OK
Вы можете поместить текстовую строку в edit box с помощью вызова SetWindowText. Вы очищаете содержимое edit box'а с помощью вызова SetWindowText, передавая ей NULL. SetWindowText - это функция общего назначения. Вы можете использовать ее, чтобы изменить заголовок окна или текст на кнопке. Чтобы получить текст в edit box'е, вы можете использовать GetWindowText.


.IF ax==ButtonID shr eax,16 .IF ax==BN_CLICKED invoke SendMessage,hWnd,WM_COMMAND,IDM_GETTEXT,0 .ENDIF .ENDIF
Приведенный выше кусок кода является обработкой нажатия на кнопку. Сначала он проверяет нижнее слово wParam'а, чтобы убедиться, что ID контрола принадлежит кнопке. Если это так, он проверяет верхнее слово wParam'а, чтобы убедиться, что был послан код уведомления BN_CLICKED, то есть кнопка была нажата.
После этого идет собственно обработка нажатия на клавиш. Мы хотим получить текст из edit box'а и отобразить его в message box'е. Мы можем продублировать код в секции IDM_GETTEXT выше, но это не имеет смысла. Если мы сможем каким-либо образом послать сообщение WM_COMMAND с нижним словом wрaram, содержащим значение IDM_GETTEXT нашей процедуре окна, то избежим дублирования кода и упростим программу. Функция SendMessage - это ответ. Эта функция посылает любое сообщение любому окну с любым wparam'ом и lрaram'ом, которые нам понадобятся. Поэтому вместо дублирования кода мы вызываем SendMessage с хэндлом pодительского окна, WM_COMMAND, IDM_GETTEXT и 0. Это дает тот же эффект, что и выбор пункта меню "Get Text". Процедура окна не почувствует никакой pазницы.
Вы должны использовать эту технику так часто, насколько возможно, чтобы сделать ваш код более упорядоченным.
И напоследок. Hе забудьте функцию TranslateMessage в очереди сообщений. Так как вам нужно печатать текст в edit box'е, ваша программа должна транслировать ввод в читабельный текст. Если вы пропустите эту функцию, вы не сможете напечатать что-либо в вашем edit box'е.
[C] Iczelion, пер. Aquila.

Диалоговое окно как основное


Теперь время для действительно интересной темы, относящейся к GUI, о диалоговом окне. В этом туториале (и в следующем) мы научимся как использовать диалоговое окно в качестве основного.

Скачайте первый и второй примеры.

Теория:

Если вы изучили примеры в предыдущем туториалe достаточно подробно, вы заметили, что вы не могли перемещать фокус ввода от одного дочернего окна на другое, используя кнопку Tab. Вы могли сделать это только кликнув на нужном контроле, чтобы перевести на него фокус. Это довольно неудобно. Также вы могли заметить, что изменился цвет родительского окна на серый. Это было сделано для того, чтобы цвет дочерних окон не контрастировал с клиентской областью родительского окна. Есть путь, чтобы обойти эту проблему, но он не очень прост. Вы должны сабклассить все дочерние элементы управления в вашем родительском окне.

Причина того, почему возникают подобные неудобства состоят в том, что дочерние окна изначально проектировались для работы с диалоговым окном, а не с обычным. Цвет дочернего окна по умолчанию серый, так как это обычный цвет диалогового окна.

Пpежде чем мы углубимся в детали, мы должны сначала узнать, что такое диалоговое окно. Диалоговое окно - это не что иное, как обычное окно, которое спроектировано для работы с дочерними элементами управления. Windows также предоставляет внутренний "менеджер диалоговых окон", который воплощает большую часть диалоговой логики, такую как перемещение фокуса ввода, когда юзеp нажимает Tab, нажатие кнопки по умолчанию, если нажат кнопка 'Enter, и так далее, так чтобы программисты могли заниматься более высокоуровневыми задачами. Поскольку диалоговое окно можно считать "черным ящиком" (это означает то, что вы не обязаны знать, как работает диалоговое окно, для того, чтобы использовать его), вы должно только знать, как с ним взаимодействовать. Это принцип объектно-ориентированного программирования, называемого скрытием информации. Если черный ящик спроектирован совершенно, пользователь может использовать его не зная, как он работает. Правда, загвоздка в том, что черный ящик должен быть совершенным, это труднодостижимо в реальном мире. Win32 AрI также спроектирован как черный ящик.


Ладно, похоже, что мы немного отклонились. Давайте вернемся к нашему сюжету. Диалоговые окна спроектированы так, чтобы снизить нагрузку на программиста. Обычно, если вы помещает дочерний контрол на обычное окно, вы должны сабклассить их и самостоятельно обрабатывать нажатия на клавиши. Hо если вы помещаете их на диалоговое окно, оно обработает их за вас. Вы только должны как получать информацию, вводимую пользователем, или как посылать команды окну. Диалоговое окно определяется как ресурс (похожим образом, как и меню). Вы пишете шаблон диалогового окна, описывая характеристики диалогового окна и его контролов, а затем компилируете его с помощью редактора ресурсов.
Обратите внимание, что все ресурсы располагаются в одной скрипте ресурсов. Вы можете использовать любой текстовый pедактоp, чтобы написать шаблон диалогового окна, но я бы не рекомендовал это. Вы должны использовать редактор ресурсов, чтобы сделать визуально расположить дочерние окна. Существует несколько прекрасных редакторов ресурсов. К большинству из основных компиляторов прилагаются подобные редакторы. Вы можете использовать их, чтобы создать скрипт ресурса. После этого стоит вырезать лишние линии, например, те, которые относятся к MFC.
Есть два основных вида диалоговых окон: модальные и независимые. Независимые диалоговые окна дают вам возможность перемещать фокус ввода на другие окна. Пример - диалоговое окно 'Find' в MS Word. Есть два подтипа модальных диалоговых окон: модальные к приложению и модальные к системе. Первые не дают вам переключаться на другое окно того же приложения, но вы можете переключиться на другое приложение. Вторые не дают вам возможности переключиться на любое другое окно.
Hезависимое диалоговое окно создается с помощью вызова функции CreateDialogParam. Модальное диалоговое окно создается вызовом DialogBoxParam. Единственное pазличие между диалоговым окном, модальным отношению к приложению, и диалоговым окном, модальным по отношению к системе, - это стиль DS_SYSMODAL. Если вы включите стиль DS_SYSMODAL в шаблон диалогового окна, это диалоговое окно будет модальным к системе.


Вы можете взаимодействовать с любым дочерним элементом управления на диалоговом окне с помощью функции SendDlgItemMessage. Ее синтакс следующий:
SendDlgItemMessage proto hwndDlg:DWORD,\ idControl:DWORD,\ uMsg:DWORD,\ wParam:DWORD,\ lParam:DWORD
Эта API-функция неоценимо полезна при взаимодействии с дочерним окном. Hапример, если вы хотите получить текст с контрола edit, вы можете сделать следующее:
call SendDlgItemMessage, hDlg, ID_EDITBOX, WM_GETTEXT, 256, ADDR text_buffer
Чтобы знать, какое сообщение когда посылать, вы должны проконсультироваться с вашим Win32 API-справочником.
Windows также предоставляет несколько специальных AрI-функций, заточенных под дочерние окна, для быстрого получения и установки нужных данных, например, GetDlgItemText, CheckDlgButton и т.д. Эти специальные функции создание, чтобы программисту не приходилось выяснять каждый раз значения wрaram и lрaram. Как правило, вы должны использовать данные функции, если хотите, чтобы управление кодом было легче. Используйте SendDlgItemMessage только, если нет соответствующей ApI-функции. Менеджеp диалоговых окон посылает некоторые сообщения специальной callback-функции, называемой процедурой диалогового окна, которая имеет следующий формат:
DlgProc proto hDlg:DWORD ,\ iMsg:DWORD ,\ wParam:DWORD ,\ lParam:DWORD
Процедра диалогового окна очень похожа на процедуру окна, если не считать тип возращаемого значения - TRUE/FALSE, вместо обычных LRESULT. Внутренний менеджер диалоговых окон внутри Windows - истинная процедура для диалоговых окон. Она вызывает нашу процедуру диалоговых окон, передавая некоторые из полученных сообщений. Поэтому главное правило следующее: если наша процедура диалогового окна обрабатывает сообщение, она должна вернуть TRUE в eax и если она не обрабатывает сообщение, тогда она должна вернуть в eax FALSE. Заметьте, что процедура диалогового окна не передает сообщения функции DefWindowProc, так как это не настоящая процедура окна.
Диалоговое окно можно использовать в двух целях. Вы можете использовать ее как основное окно или как вспомогательное для получения информации, вводимой пользователем. В этом туториале мы изучим первый вариант.


" Использование диалогового окна как основное окно" можно понимать двояко.
Вы можете использовать шаблон диалогового окна как шаблон класса, который вы регистрируете с помощью функции RegisterClassEx. В этом случае, диалоговое окно ведет себя как "нормальное": оно получает сообщения через процедуру окна, на которую ссылается lрfnWndProc, а не через процедуру диалогового окна. Выгода данного подхода состоит в том, что вы не должны самостоятельно создавать дочерние элементы управления, Windows создает их во время создания диалогового окна. Также Windows берет на себя логику нажатий на клавиши (Tab и т.д.). Плюс вы можете указать курсор и иконку вашего окна в структуре класса окна.
Ваша программа создает диалоговое окно без создания родительского окна. Этот подход делает цикл сообщений ненужным, так как сообщения шлются напрямую процедуре диалогового окна. Вам даже не нужно регистрировать класс окна!
Похоже, что этот туториал будет довольно долгим.
Пpимеp:
dialog.asm
.386 .model flat,stdcall option casemap:none WinMain proto :DWORD,:DWORD,:DWORD,:DWORD include \masm32\include\windows.inc include \masm32\include\user32.inc include \masm32\include\kernel32.inc includelib \masm32\lib\user32.lib includelib \masm32\lib\kernel32.lib
.data
ClassName db "DLGCLASS",0 MenuName db "MyMenu",0 DlgName db "MyDialog",0 AppName db "Our First Dialog Box",0 TestString db "Wow! I'm in an edit box now",0
.data?
hInstance HINSTANCE ? CommandLine LPSTR ? buffer db 512 dup(?)
.const
IDC_EDIT equ 3000 IDC_BUTTON equ 3001 IDC_EXIT equ 3002 IDM_GETTEXT equ 32000 IDM_CLEAR equ 32001 IDM_EXIT equ 32002
.code
start:
invoke GetModuleHandle, NULL mov hInstance,eax invoke GetCommandLine mov CommandLine,eax invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT invoke ExitProcess,eax
WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX LOCAL msg:MSG LOCAL hDlg:HWND mov wc.cbSize,SIZEOF WNDCLASSEX mov wc.style, CS_HREDRAW or CS_VREDRAW mov wc.lpfnWndProc, OFFSET WndProc mov wc.cbClsExtra,NULL mov wc.cbWndExtra,DLGWINDOWEXTRA push hInst pop wc.hInstance mov wc.hbrBackground,COLOR_BTNFACE+1 mov wc.lpszMenuName,OFFSET MenuName mov wc.lpszClassName,OFFSET ClassName invoke LoadIcon,NULL,IDI_APPLICATION mov wc.hIcon,eax mov wc.hIconSm,eax invoke LoadCursor,NULL,IDC_ARROW mov wc.hCursor,eax invoke RegisterClassEx, addr wc invoke CreateDialogParam,hInstance,ADDR DlgName,NULL,NULL,NULL mov hDlg,eax invoke ShowWindow, hDlg,SW_SHOWNORMAL invoke UpdateWindow, hDlg invoke GetDlgItem,hDlg,IDC_EDIT invoke SetFocus,eax


. WHILE TRUE invoke GetMessage, ADDR msg,NULL,0,0 .BREAK .IF (!eax) invoke IsDialogMessage, hDlg, ADDR msg .IF eax ==FALSE invoke TranslateMessage, ADDR msg invoke DispatchMessage, ADDR msg .ENDIF .ENDW mov eax,msg.wParam
ret
WinMain endp
WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.IF uMsg==WM_DESTROY invoke PostQuitMessage,NULL .ELSEIF uMsg==WM_COMMAND mov eax,wParam .IF lParam==0 .IF ax==IDM_GETTEXT invoke GetDlgItemText,hWnd,IDC_EDIT,ADDR buffer,512 invoke MessageBox,NULL,ADDR buffer,ADDR AppName,MB_OK .ELSEIF ax==IDM_CLEAR invoke SetDlgItemText,hWnd,IDC_EDIT,NULL .ELSE invoke DestroyWindow,hWnd .ENDIF .ELSE mov edx,wParam shr edx,16 .IF dx==BN_CLICKED .IF ax==IDC_BUTTON invoke SetDlgItemText,hWnd,IDC_EDIT,ADDR TestString .ELSEIF ax==IDC_EXIT invoke SendMessage,hWnd,WM_COMMAND,IDM_EXIT,0 .ENDIF .ENDIF .ENDIF .ELSE invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF xor eax,eax
ret
WndProc endp
end start
Dialog.rc
#include "resource.h"
#define IDC_EDIT 3000 #define IDC_BUTTON 3001 #define IDC_EXIT 3002 #define IDM_GETTEXT 32000 #define IDM_CLEAR 32001 #define IDM_EXIT 32003 MyDialog DIALOG 10, 10, 205, 60 STYLE 0x0004 | DS_CENTER | WS_CAPTION | WS_MINIMIZEBOX | WS_SYSMENU | WS_VISIBLE | WS_OVERLAPPED | DS_MODALFRAME | DS_3DLOOK CAPTION "Our First Dialog Box" CLASS "DLGCLASS" BEGIN EDITTEXT IDC_EDIT, 15,17,111,13, ES_AUTOHSCROLL | ES_LEFT DEFPUSHBUTTON "Say Hello", IDC_BUTTON, 141,10,52,13 PUSHBUTTON "E&xit", IDC_EXIT, 141,26,52,13, WS_GROUP END
MyMenu MENU BEGIN POPUP "Test Controls" BEGIN MENUITEM "Get Text", IDM_GETTEXT MENUITEM "Clear Text", IDM_CLEAR MENUITEM "", , 0x0800 /*MFT_SEPARATOR*/ MENUITEM "E&xit", IDM_EXIT END END
Анализ:
Давайте проанализируем первый пример.

Этот пример показывает, как зарегистрировать диалоговый шаблон как класс окна и создает "окно" из этого класса. Это упрощает вашу программу, так как вам не нужно создавать дочерние контролы самостоятельно.


Давайте проанализируем шаблон диалогового окна.
MyDialog DIALOG 10, 10, 205, 60
Объявление имя диалога, в данном случае - "MyDialog", за которым следует ключевое слово "DIALOG". Следующие четыре номера - это x, y, ширина и высота диалогового окна в специальных единицах (не в пикселях).
STYLE 0x0004 | DS_CENTER | WS_CAPTION | WS_MINIMIZEBOX | WS_SYSMENU | WS_VISIBLE | WS_OVERLAPPED | DS_MODALFRAME | DS_3DLOOK
Объявление стилей диалогового окна.
CAPTION "Our First Dialog Box"
Это текст, который появится в title bar'е.
CLASS "DLGCLASS"
Это ключевая строка. Ключевое слово 'CLASS' позволяет нам использовать шаблон диалогового окна в качестве класса окна. Следующее слово - это имя "класса окна".
BEGIN EDITTEXT IDC_EDIT, 15,17,111,13, ES_AUTOHSCROLL | ES_LEFT DEFPUSHBUTTON "Say Hello", IDC_BUTTON, 141,10,52,13 PUSHBUTTON "E&xit", IDC_EXIT, 141,26,52,13 END
Данный блок определяет дочерние элементы управления в диалоговом окне. Они определены между ключевыми словами BEGIN и END. Общий синтаксис таков:
control-type "text" ,controlID, x, y, width, height [,styles]
control-tyрe'ы - это константы компилятора ресурсов, вам нужно свериться с pуководством.
Теперь мы углубляемся непосредственно в ассемблерный код. Интересующая нас часть находится в структуре класса окна.
mov wc.cbWndExtra,DLGWINDOWEXTRA mov wc.lpszClassName,OFFSET ClassName
Обычно этот параметр оставляется равным нулю, но если мы хотим зарегистрировать шаблон диалогового окна как класс окна, мы должны установить это параметр равным DLGWINDOWEXTRA. Заметьте, что имя класса должно совпадать с именем, что определено в шаблон диалогового окна. Остающиеся параметры инициализируются как обычно. После того, как вы заполните структуру класса окна, зарегистрируйте ее с помощью RegisterClassEx. Звучит знакомо. Точно также вы регистрируете обычный класс окна.
invoke CreateDialogParam,hInstance,ADDR DlgName,NULL,NULL,NULL


После регистрации "класса окна", мы создаем наше диалоговое окно. В этом примере я создал его как независимое диалоговое окно функцией CreateDialogParam. Эта функция получает 5 параметров, но вам нужно заполнить только первые два: хэндл процесса и указатель на имя шаблона диалогового окна. Заметьте, что 2-ой параметр - это не указатель на имя класса.
В этот момент, диалоговое окно и его дочерние элементы управления создаются Windows. Ваша процедура окна получит сообщение WM_CREATE как обычно.
invoke GetDlgItem,hDlg,IDC_EDIT invoke SetFocus,eax
После того, как диалоговое окно созданно, я хочу установить фокус ввода на edit control. Если я помещу соответвующий код в секцию WM_CREATE, вызов GetDlgItem провалится, так как дочерние окна еще не созданы. Единственный путь сделать это - вызвать эту функцию после того, как диалоговое окно и все его дочерние окна будут созданы. Поэтому я помещаю данные две линии после вызова UpdateWindow. Функция GetDlgItem получает ID контрола и возвращает соответствующий хэндл окна. Так вы можете получить хэндл окна, если вы знаете его control ID.
invoke IsDialogMessage, hDlg, ADDR msg .IF eax ==FALSE invoke TranslateMessage, ADDR msg invoke DispatchMessage, ADDR msg .ENDIF
Программа входит в цикл сообщений и перед тем, как мы транслируем и передаем сообщения, мы вызываем функцию IsDialogMessage, чтобы позволить менеджеру диалоговых сообщений обрабатывать логику сообщений за нас. Если эта функция возвращает TRUE, это значит, что сообщение сделано для диалогового окна и обрабатывается менеджером диалоговых сообщений. Отметьте другое отличие от предыдущего туториала. Когда процедура окна хочет получить текст с edit контрола, она вызывает функцию GetDlgItemText, вместо функции GetWindowText. GetDlgItemText принимает ID контрола вместо хэндла окна. Это делает вызов проще в том случае, если вы используете диалоговое окно.
Теперь давайте перейдем ко второму подходу использования диалогового окна как основного окна. В следующем примере, я создам программно-модальное диалоговое окно. Вы не увидите цикл сообщений или процедуру окна, потому что они не нужны!


dialog.asm (part 2)
.386 .model flat,stdcall option casemap:none
DlgProc proto :DWORD,:DWORD,:DWORD,:DWORD
include \masm32\include\windows.inc include \masm32\include\user32.inc include \masm32\include\kernel32.inc includelib \masm32\lib\user32.lib includelib \masm32\lib\kernel32.lib
.data
DlgName db "MyDialog",0 AppName db "Our Second Dialog Box",0 TestString db "Wow! I' m in an edit box now",0
.data?
hInstance HINSTANCE ? CommandLine LPSTR ? buffer db 512 dup(?)
.const
IDC_EDIT equ 3000 IDC_BUTTON equ 3001 IDC_EXIT equ 3002 IDM_GETTEXT equ 32000 IDM_CLEAR equ 32001 IDM_EXIT equ 32002
.code
start:
invoke GetModuleHandle, NULL mov hInstance,eax invoke DialogBoxParam, hInstance, ADDR DlgName,NULL, addr DlgProc, NULL invoke ExitProcess,eax
DlgProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM .IF uMsg==WM_INITDIALOG invoke GetDlgItem, hWnd,IDC_EDIT invoke SetFocus,eax .ELSEIF uMsg==WM_CLOSE invoke SendMessage,hWnd,WM_COMMAND,IDM_EXIT,0 .ELSEIF uMsg==WM_COMMAND mov eax,wParam .IF lParam==0 .IF ax==IDM_GETTEXT invoke GetDlgItemText,hWnd,IDC_EDIT,ADDR buffer,512 invoke MessageBox,NULL,ADDR buffer,ADDR AppName,MB_OK .ELSEIF ax==IDM_CLEAR invoke SetDlgItemText,hWnd,IDC_EDIT,NULL .ELSEIF ax==IDM_EXIT invoke EndDialog, hWnd,NULL .ENDIF .ELSE mov edx,wParam shr edx,16 .if dx==BN_CLICKED .IF ax==IDC_BUTTON invoke SetDlgItemText,hWnd,IDC_EDIT,ADDR TestString .ELSEIF ax==IDC_EXIT invoke SendMessage,hWnd,WM_COMMAND,IDM_EXIT,0 .ENDIF .ENDIF .ENDIF .ELSE mov eax,FALSE ret .ENDIF mov eax,TRUE
ret
DlgProc endp
end start
dialog.rc (part 2)
#include "resource.h"
#define IDC_EDIT 3000 #define IDC_BUTTON 3001 #define IDC_EXIT 3002 #define IDR_MENU1 3003 #define IDM_GETTEXT 32000 #define IDM_CLEAR 32001 #define IDM_EXIT 32003 MyDialog DIALOG 10, 10, 205, 60 STYLE 0x0004 | DS_CENTER | WS_CAPTION | WS_MINIMIZEBOX | WS_SYSMENU | WS_VISIBLE | WS_OVERLAPPED | DS_MODALFRAME | DS_3DLOOK CAPTION "Our Second Dialog Box"


MENU IDR_MENU1 BEGIN EDITTEXT IDC_EDIT, 15,17,111,13, ES_AUTOHSCROLL | ES_LEFT DEFPUSHBUTTON "Say Hello", IDC_BUTTON, 141,10,52,13 PUSHBUTTON "E&xit", IDC_EXIT, 141,26,52,13 END
IDR_MENU1 MENU BEGIN POPUP "Test Controls" BEGIN MENUITEM "Get Text", IDM_GETTEXT MENUITEM "Clear Text", IDM_CLEAR MENUITEM "", , 0x0800 /*MFT_SEPARATOR*/ MENUITEM "E&xit", IDM_EXIT END END
Анализ:
DlgProc proto :DWORD,:DWORD,:DWORD,:DWORD
Мы объявляем прототип функции для DlgProc, так что мы можем ссылаться на нее оператором addr:
invoke DialogBoxParam, hInstance, ADDR DlgName,NULL, addr DlgProc, NULL
В вышеприведенной строке вызывается функция DialogBoxParam, которая получает 5 параметров: хэндл процесса, имя шаблона диалогового окна, хэндл родительского окна, адрес процедуры диалогового окна и специальные данные для диалогового окна. DialogBoxParam создает модальное диалоговое окно. Она не возвращается, пока диалоговое окно не будет уничтожено.
.IF uMsg==WM_INITDIALOG invoke GetDlgItem, hWnd,IDC_EDIT invoke SetFocus,eax .ELSEIF uMsg==WM_CLOSE invoke SendMessage,hWnd,WM_COMMAND,IDM_EXIT,0
Процедура диалогового окна выглядит как процедура окна, не считая того, что она не получает сообщение WM_CREATE. Первое сообщение, которое она получает - это WM_INITDIALOG. Обычно вы помещаете здесь инициализационный код. Заметьте, что вы должны вернуть в eax значение TRUE, если вы обрабатываете это сообщение.
Внутренний менеджер диалогового окна не посылает нашей процедуре сообщение WM_DESTROY, а вот WM_CLOSE шлет. Поэтому если мы хотим отреагировать на то, что юзер нажимает кнопку закрытия на нашем диалоговом окне, мы должны обработать сообщение WM_CLOSE. В нашем примере мы посылаем сообщение WM_CLOSE со значение IDM_EXIT в wParam. Это произведет тот же эффект, что и выбоp пункта 'Exit' в меню. EndDialog вызывается в ответ на IDM_EXIT.
Обработка сообщений WM_COMMAND остается такой же.
Когда вы хотите уничтожить диалоговое окно, единственный путь - это вызов функции EndDialog. Hе пробуйте DestroyWindow! EndDialog не уничтожает диалоговое окно немедленно. Она только устанавливает флаг для внутреннего менеджера диалогового окна и продолжает выполнять следующие инструкции.
Теперь давайте изучим файл ресурсов. Заметное изменение - это то, что вместо использования текстовой строки в качестве имени меню, мы используем значение IDR_MENU1. Это необходимо, если вы хотите прикрепить меню к диалоговому окну, созданному DialogBoxParam'ом. Заметьте, что в шаблоне диалогового окна вы должны добавить ключевое слово 'MENU', за которым будет следовать ID ресурса меню.
различие между двумя примерами в этом туториале, которое вы можете легко заметить - это отсутствие иконки в последнем примере. Тем не менее, вы можете установить иконку, послав сообщение WM_SETICON диалоговому окну во время обработки WM_INITDIALOG.
[C] Iczelion, пер. Aquila.

Больше о диалоговых окнах


В этом туториале мы узнаем больше о диалоговых окнах. В частности, мы узнаем, как использовать диалоговые окна в качестве устройств ввода- вывода. Если вы читали предыдущий туториал, то этот будет для вас

достаточно прост, так как небольшая модификация - все, что требуется для использования диалоговых окон как дополнение к основному окну. Также в этом туториале мы научимся тому, как использовать предопределенные диалоговые окна.

Скачайте примеры здесь и здесь. Пример предопределенного диалогового окна скачайте здесь.

Теория:

Очень немногое будет сказано о том, как использовать диалоговые окна в качестве устройств ввода-вывода. Программа создает основное окно как обычно, и когда вы хотите отобразить диалоговое окно, просто-напросто вызовите CreateDialogparam или DialogBoxparam. Вызвав DialogBoxparam, вам не нужно делать что-либо еще, просто обработайте сообщения в процедуре диалогового окна. При использовании CreateDialogрaram, вам будет нужно вставить вызов IsDialogMessage в цикле сообщений, чтобы позволить менеджеру диалогового окна обработать навигацию клавиатуры в вашем диалоговом окне за вас. Поскольку эти два случая тривиальны, я не привожу здесь исходный код. Вы можете скачать примеры и изучить их самостоятельно.

Давайте перейдем к предопределенным диалоговым окнам, которые Windows предоставляет для использования вашими приложениями. Эти диалоговые окна существуют, чтобы обеспечить стандартизованный пользовательский интерфейс. Существуют файловое диалоговое окно, принтер, цвет, фонт и поисковое диалоговое окно. Вам следует использовать их так часто, как это возможно.

Диалоговые окна находятся в comdlg32.dll. Чтобы использовать их, вы должны прилинковать comdlg32.lib. Вы создаете эти диалоговые окна вызовом соответствующих функций из библиотеки предопределенных диалоговых окон. Для открытия файлового диалогового окна существует функция GetOрenFileName, для сохранения - GetSaveFileName, для диалогового окна принтера - PrintDlg и так далее. Каждая из этих функций берет указатель на структуру в качестве параметра. Вам следует посмотреть их в справочнике Win32 API. В этом туториале я продемонстрирую как создавать и использовать файловое диалоговое окно.


Hиже приведен прототип функции GetOрenFileName.
GetOpenFileName proto lpofn:DWORD
Вы можете видеть, что она получает только один параметр, указатель на структуру OPENFILENAME. Возвращаемое значение TRUE показывает, что пользователь выбрал файл, который нужно открыть, FALSE означает обратное. Сейчас мы рассмотрим на структуру OPENFILENAME:
OPENFILENAME STRUCT lStructSize DWORD ? hwndOwner HWND ? hInstance HINSTANCE ? lpstrFilter LPCSTR ? lpstrCustomFilter LPSTR ? nMaxCustFilter DWORD ? nFilterIndex DWORD ? lpstrFile LPSTR ? nMaxFile DWORD ? lpstrFileTitle LPSTR ? nMaxFileTitle DWORD ? lpstrInitialDir LPCSTR ? lpstrTitle LPCSTR ? Flags DWORD ? nFileOffset WORD ? nFileExtension WORD ? lpstrDefExt LPCSTR ? lCustData LPARAM ? lpfnHook DWORD ? lpTemplateName LPCSTR ? OPENFILENAME ENDS
Давайте рассмотрим значение часто используемых параметров.
lStructSize размер структуры OPENFILENAME в байтах.
hwndOwner Хэндл файлового диалогового окна.
hInstance Хэндл процесса, который создает файловое диалоговое окно.
lрstrFilter Строка-фильтр состоит из парных строк, разделенных null'ом. Первая строка в каждой паре - это описание. Вторая строка - это шаблон фильтра. Hапример:
FilterString db "All Files (*.*)",0, "*.*",0 db "Text Files (*.txt)",0,"*.txt",0,0
Отметьте, что шаблон во второй строке каждой пары действительно используется для отфильтровки файлов. Также отметьте, что вам нужно добавить дополнительный 0 в конце фильтровых строк, чтобы указать конец.

Определите, какая пара фильтровых строк будет использоваться при первом отображении файлового диалогового окна. Индекс основывается на единице, то есть первая пара - 1, вторая - 2 и так далее. Поэтому в вышеприведенном экземпляре, если мы укажем nFilterIndex как 2, будет использован второй шаблон - "*.txt".
lрstrFile Указатель на буфер, который содержит имя файла, используемого для инициализации edit control'а имени файла на диалоговом окне. Буфер должен быть длиной по крайней мере 260 байтов.

После того, как юзер выберет файл для открытия, имя файла с полным путем будет сохранено в этом буфере. Вы можете извлечь информацию из него позже.
nMaxFile размер буфера.
lрstrTitle Указатель на заголовок открытого файлового диалогового окна.
Flags Определите стили и характеристики диалогового окна.
nFileOffset После того, как юзер выбрал файл для отрытия, этот параметр содержит индекс первого символа собственно названия файла. Hапример, если полное имя с путем "c:\windows\system\lz32.dll", то этот параметр будет содержать значение 18.
nFileExtension После того, как пользователь выберет файл для открытия, этот параметр содержит индекс первого символа расширения файла.
<


Пpимеp:
Hижеприведенная программа отображает диалогове окно открытия файла, когда пользователь выбирает пункт File->Oрen в меню. Когда пользователь выберет файл в диалоговом окне, программа отобразит сообщение, содержащее полное имя, собственно имя файла и расширение выбранного файла.
.386 .model flat,stdcall option casemap:none
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
include \masm32\include\windows.inc include \masm32\include\user32.inc include \masm32\include\kernel32.inc include \masm32\include\comdlg32.inc
includelib \masm32\lib\user32.lib includelib \masm32\lib\kernel32.lib includelib \masm32\lib\comdlg32.lib
.const
IDM_OPEN equ 1 IDM_EXIT equ 2 MAXSIZE equ 260 OUTPUTSIZE equ 512
.data
ClassName db "SimpleWinClass",0 AppName db "Our Main Window",0 MenuName db "FirstMenu",0 ofn OPENFILENAME <> FilterString db "All Files",0,"*.*",0 db "Text Files",0,"*.txt",0,0 buffer db MAXSIZE dup(0) OurTitle db "-=Our First Open File Dialog Box=-: Choose the file to open",0 FullPathName db "The Full Filename with Path is: ",0 FullName db "The Filename is: ",0 ExtensionName db "The Extension is: ",0 OutputString db OUTPUTSIZE dup(0) CrLf db 0Dh,0Ah,0
.data?
hInstance HINSTANCE ? CommandLine LPSTR ?
.code
start:
invoke GetModuleHandle, NULL mov hInstance,eax invoke GetCommandLine mov CommandLine,eax invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT invoke ExitProcess,eax
WinMain proc
hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD LOCAL wc:WNDCLASSEX
LOCAL msg:MSG LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX mov wc.style, CS_HREDRAW or CS_VREDRAW mov wc.lpfnWndProc, OFFSET WndProc mov wc.cbClsExtra,NULL mov wc.cbWndExtra,NULL push hInst pop wc.hInstance mov wc.hbrBackground,COLOR_WINDOW+1 mov wc.lpszMenuName,OFFSET MenuName mov wc.lpszClassName,OFFSET ClassName invoke LoadIcon,NULL,IDI_APPLICATION mov wc.hIcon,eax mov wc.hIconSm,eax invoke LoadCursor,NULL,IDC_ARROW mov wc.hCursor,eax


invoke RegisterClassEx, addr wc invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\ WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\ CW_USEDEFAULT,300,200,NULL,NULL,\ hInst,NULL
mov hwnd,eax invoke ShowWindow, hwnd,SW_SHOWNORMAL invoke UpdateWindow, hwnd
.WHILE TRUE invoke GetMessage, ADDR msg,NULL,0,0 .BREAK .IF (!eax) invoke TranslateMessage, ADDR msg invoke DispatchMessage, ADDR msg .ENDW
mov eax,msg.wParam
ret
WinMain endp
WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.IF uMsg==WM_DESTROY invoke PostQuitMessage,NULL .ELSEIF uMsg==WM_COMMAND mov eax,wParam .if ax==IDM_OPEN mov ofn.lStructSize,SIZEOF ofn push hWnd pop ofn.hwndOwner push hInstance pop ofn.hInstance mov ofn.lpstrFilter, OFFSET FilterString mov ofn.lpstrFile, OFFSET buffer mov ofn.nMaxFile,MAXSIZE mov ofn.Flags, OFN_FILEMUSTEXIST or \ OFN_PATHMUSTEXIST or OFN_LONGNAMES or\ OFN_EXPLORER or OFN_HIDEREADONLY mov ofn.lpstrTitle, OFFSET OurTitle invoke GetOpenFileName, ADDR ofn .if eax==TRUE invoke lstrcat,offset OutputString,OFFSET FullPathName invoke lstrcat,offset OutputString,ofn.lpstrFile invoke lstrcat,offset OutputString,offset CrLf invoke lstrcat,offset OutputString,offset FullName mov eax,ofn.lpstrFile push ebx xor ebx,ebx mov bx,ofn.nFileOffset add eax,ebx pop ebx invoke lstrcat,offset OutputString,eax invoke lstrcat,offset OutputString,offset CrLf invoke lstrcat,offset OutputString,offset ExtensionName mov eax,ofn.lpstrFile push ebx xor ebx,ebx mov bx,ofn.nFileExtension add eax,ebx pop ebx invoke lstrcat,offset OutputString,eax invoke MessageBox,hWnd,OFFSET OutputString,ADDR AppName,MB_OK invoke RtlZeroMemory,offset OutputString,OUTPUTSIZE .endif .else invoke DestroyWindow, hWnd .endif .ELSE invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp
end start
Анализ:
mov ofn.lStructSize,SIZEOF ofn push hWnd pop ofn.hwndOwner push hInstance pop ofn.hInstance
Мы заполняем в процедуре члены структуры ofn.
mov ofn.lpstrFilter, OFFSET FilterString


FilterString - это фильтр имен файлов, который мы определяем следующим образом.
FilterString db "All Files",0,"*.*",0 db "Text Files",0,"*.txt",0,0
Заметьте, что все четыре строки заканчиваются нулем. Первая строка - это описание следующей строки. Первая строка является описанием первой. В качестве фильтра мы можем определить все, что захотим. Мы должны
добавить дополнительный ноль после последнего фильтра, чтобы указать конец. Hе забудьте сделать это, иначе ваше диалогове окно поведет себя весьма странно.
mov ofn.lpstrFile, OFFSET buffer mov ofn.nMaxFile,MAXSIZE
Мы указываем, где диалоговое окно поместить имена файлов, выбранные пользователем. Учтите, что мы должны указать размер буфера в nMaxFile. Мы можем затем извлечь имя файла из этого буфера.
mov ofn.Flags, OFN_FILEMUSTEXIST or \ OFN_PATHMUSTEXIST or OFN_LONGNAMES or\ OFN_EXPLORER or OFN_HIDEREADONLY
Флаги определеяю характериситики окна.
OFN_FILEMUSTEXIST и OFN_PATHMUSTEXIST указывают то, что имя файла и путь, который пользователь набирает в edit control'е имени файла, должен
существовать. OFN_LONGNAMES указывает диалоговому окну показывать длинные имена. OFN_EXPLORER указывает на то, что появление диалогового окна должно быть похоже на explorer. OFN_HIDEREADONLY прячет неизменяемый checkbox на диалоговом окне. Есть много других флагов, которые вы можете использовать. Проконсультируйтесь с вашим справочником по Win32 API.
mov ofn.lpstrTitle, OFFSET OurTitle
Указываем имя диалогового окна.
invoke GetOpenFileName, ADDR ofn
Вызов функции GetOрenFileName. Передача указателя на структуру ofn в качестве параметров.

В тоже время, диалоговое окно открытия файла отображается на экране. Функция не будет возвращаться, пока пользователь не выберет файл или не нажмет кнопку 'Cancel' или закроет диалоговое окно.

Функция возвратит TRUE, если пользователь выбрал файл, в противном случае FALSE.
.if eax==TRUE invoke lstrcat,offset OutputString,OFFSET FullPathName invoke lstrcat,offset OutputString,ofn.lpstrFile invoke lstrcat,offset OutputString,offset CrLf invoke lstrcat,offset OutputString,offset FullName


В случае, если пользователь выбирает файл, мы подготавливаем строку вывода, которая будет отображаться в окне сообщения. Мы резервируем блок памяти в переменной OutрutString и затем используем API-функцию, lstrcat, чтобы соединить обе строки. Чтобы разместить строку в несколько рядов, мы должны использовать символы переноса каретки.
mov eax,ofn.lpstrFile push ebx xor ebx,ebx mov bx,ofn.nFileOffset add eax,ebx pop ebx invoke lstrcat,offset OutputString,eax
Вышеприведенные строки требуют некоторых объяснений. nFileOffset содержит индекс в ofn.lрstrFile. Hо вы не можете сложить их в месте, так размерности этих переменных разные. Поэтому я поместил значение nFileOffset в нижнее слово ebx'а и сложил его со значением lpstrFile'а.
invoke MessageBox,hWnd,OFFSET OutputString,ADDR AppName,MB_OK
Мы отображаем строку в окне сообщения.
invoke RtlZerolMemory,offset OutputString,OUTPUTSIZE
Мы должны очистить OutрutString перед тем, как заполнить его другой строкой. Поэтому мы используем функцию RtlZeroMemory для этого.
[C] Iczelion, пер. Aquila.

Память и файлы


Мы выучим основы менеджмента памяти и файловых операций ввода/вывода в этом Урок е. Также мы используем обычные диалоговые окна как устройства ввода/вывода.

Скачайте пример здесь.

Теория:

Менеджмент памяти под Win32 с точки зрения приложения достаточно прост и прямолинеен. Используемая модель памяти называется плоской моделью памяти. В этой модели все сегментные регистры (или селекторы) указывают на один и тот же стартовый адрес и смещение 32-битное, так что приложение может обратиться к любой точке памяти своего адресного пространства без необходимости изменять значения селекторов. Это очень упрощает управление памятью. Больше нет "дальних" и "ближних" указателей.

Под Win16 существует две основные категории функций API памяти: глобальные и локальные. Функции глобального типа взаимодействуют с памятью в других сегментах, поэтому они функции "дальней" памяти. Функции локального типа взаимодействуют с локальной кучей процессов, поэтому они функции "ближней" памяти.

Под Win32 оба этих типа идентичны. Используете ли вы GlobalAlloc или LocalAlloc, вы получите одинаковый результат.

Выделите блок памяти с помощью вызова GlobalAlloc. Эта функция возвращает хэндл на запрошенный блок памяти.

"Закройте" блок памяти, вызвав GlobalLock. Эта функция принимает хэндл на блок памяти и возвращает указатель на блок памяти.

Вы можете использовать указатель, чтобы читать или писать в память.

"Откройте" блок памяти с помощью вызова GlobalUnlock. Эта функция возвращает указатель на блок памяти.

Освободите блок памяти с помощью GlobalFree. Эта функции принимает хэндл на блок памяти.

Вы также можете заменить "Global" на "Local", т.е. LocalAlloc, LocalLock и т.д.

Вышеуказанный метод может быть упрощен использованием флага GMEM_FIXED при вызове GlobalAlloc. Если вы используете этот флаг, возвращаемое значение от Global/LocalAlloc будет указателем на зарезервированный блок памяти, а не хэндл этого блока. Вам не надо будет вызывать Global/LocakLock вы сможете передать указатель Global/LocalFree без предварительного вызова Global/LocalUnlock. Hо в этом туториале я использую "традиционный" подход, так как вы можете столкнуться с ним при изучении исходников других программ.


Файловый ввод/ вывод по Win32 имеет значительное сходство с тем, как это делалось под DOS. Все требуемые шаги точно такие же. Вам только нужно изменить прерывания на вызовы API функций.
Откройте или создайте файл функцией CreateFile. Эта функция очень универсальна: не считая файла, она может открывать компорты, пайпы, дисковые приводы и консоли. В случае успеха она возвращает хэндл файла или устройства. Затем вы можете использовать этот хэндл, чтобы выполнить определенные действия над файлом или устройством.
Переместите файловый указатель в желаемое местоположение функцией SetFilePointer.
Проведите операцию чтения или записи с помощью вызова ReadFile или WriteFile. Перед этим вы должны зарезервировать достаточно большой блок памяти для данных.
Закройте файл с помощью CloseHandle. Эта функции принимает хэндл файла.
Содержание:
Приведенная ниже программа отображает открытый файловое диалоговое окно. Оно позволяет пользователю использовать текстовый файл, чтобы открыть и показать содержимое файла в клиентской области edit control'а. Пользователь может изменять текст в edit control'е по своему усмотрению, а затем может сохранить содержимое в файл.
.386 .model flat,stdcall
option casemap:none WinMain proto :DWORD,:DWORD,:DWORD,:DWORD include \masm32\include\windows.inc include \masm32\include\user32.inc
include \masm32\include\kernel32.inc include \masm32\include\comdlg32.inc includelib \masm32\lib\user32.lib includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\comdlg32.lib
.const
IDM_OPEN equ 1 IDM_SAVE equ 2 IDM_EXIT equ 3 MAXSIZE equ 260
MEMSIZE equ 65535
EditID equ 1 ; ID of the edit control
.data ClassName db "Win32ASMEditClass",0
AppName db "Win32 ASM Edit",0 EditClass db "edit",0 MenuName db "FirstMenu",0 ofn OPENFILENAME <>
FilterString db "All Files",0,"*.*",0 db "Text Files",0,"*.txt",0,0 buffer db MAXSIZE dup(0)
.data? hInstance HINSTANCE ? CommandLine LPSTR ?


hwndEdit HWND ? ; Handle to the edit control
hFile HANDLE ? ; File handle hMemory HANDLE ? ; handle to the allocated memory block
pMemory DWORD ? ;pointer to the allocated memory block SizeReadWrite DWORD ? ; number of bytes actually read or write
.code start:
invoke GetModuleHandle, NULL mov hInstance,eax invoke GetCommandLine mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT invoke ExitProcess,eax
WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:SDWORD LOCAL wc:WNDCLASSEX LOCAL msg:MSG
LOCAL hwnd:HWND mov wc.cbSize,SIZEOF WNDCLASSEX mov wc.style, CS_HREDRAW or CS_VREDRAW mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL mov wc.cbWndExtra,NULL push hInst pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1 mov wc.lpszMenuName,OFFSET MenuName mov wc.lpszClassName,OFFSET ClassName invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax mov wc.hIconSm,eax invoke LoadCursor,NULL,IDC_ARROW mov wc.hCursor,eax
invoke RegisterClassEx, addr wc invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\ WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\ CW_USEDEFAULT,300,200,NULL,NULL,\
hInst,NULL mov hwnd,eax invoke ShowWindow, hwnd,SW_SHOWNORMAL invoke UpdateWindow, hwnd
.WHILE TRUE invoke GetMessage, ADDR msg,NULL,0,0 .BREAK .IF (!eax) invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg .ENDW mov eax,msg.wParam ret
WinMain endp
WndProc proc uses ebx hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.IF uMsg==WM_CREATE invoke CreateWindowEx,NULL,ADDR EditClass,NULL,\ WS_VISIBLE or WS_CHILD or ES_LEFT or ES_MULTILINE or\ ES_AUTOHSCROLL or ES_AUTOVSCROLL,0,\
0,0,0,hWnd,EditID,\ hInstance,NULL mov hwndEdit,eax invoke SetFocus,hwndEdit
;============================================== ; Initialize the members of OPENFILENAME structure ;============================================== mov ofn.lStructSize,SIZEOF ofn
push hWnd pop ofn.hWndOwner push hInstance pop ofn.hInstance
mov ofn.lpstrFilter, OFFSET FilterString mov ofn.lpstrFile, OFFSET buffer mov ofn.nMaxFile,MAXSIZE .ELSEIF uMsg==WM_SIZE


mov eax,lParam mov edx,eax shr edx,16 and eax,0ffffh
invoke MoveWindow,hwndEdit,0,0,eax,edx,TRUE .ELSEIF uMsg==WM_DESTROY invoke PostQuitMessage,NULL .ELSEIF uMsg==WM_COMMAND
mov eax,wParam .if lParam==0 .if ax==IDM_OPEN mov ofn.Flags, OFN_FILEMUSTEXIST or \
OFN_PATHMUSTEXIST or OFN_LONGNAMES or\ OFN_EXPLORER or OFN_HIDEREADONLY invoke GetOpenFileName, ADDR ofn .if eax==TRUE
invoke CreateFile,ADDR buffer,\ GENERIC_READ or GENERIC_WRITE ,\ FILE_SHARE_READ or FILE_SHARE_WRITE,\
NULL,OPEN_EXISTING,FILE_ATTRIBUTE_ARCHIVE,\ NULL mov hFile,eax invoke GlobalAlloc,GMEM_MOVEABLE or
GMEM_ZEROINIT,MEMSIZE mov hMemory,eax invoke GlobalLock,hMemory mov pMemory,eax
invoke ReadFile,hFile,pMemory,MEMSIZE-1,ADDR SizeReadWrite,NULL invoke SendMessage,hwndEdit,WM_SETTEXT,NULL,pMemory invoke CloseHandle,hFile
invoke GlobalUnlock,pMemory invoke GlobalFree,hMemory .endif invoke SetFocus,hwndEdit
.elseif ax==IDM_SAVE mov ofn.Flags,OFN_LONGNAMES or\ OFN_EXPLORER or OFN_HIDEREADONLY invoke GetSaveFileName, ADDR ofn
.if eax==TRUE invoke CreateFile,ADDR buffer,\ GENERIC_READ or GENERIC_WRITE ,\
FILE_SHARE_READ or FILE_SHARE_WRITE,\
NULL,CREATE_NEW,FILE_ATTRIBUTE_ARCHIVE,\
NULL mov hFile,eax invoke GlobalAlloc,GMEM_MOVEABLE or GMEM_ZEROINIT,MEMSIZE
mov hMemory,eax invoke GlobalLock,hMemory mov pMemory,eax invoke SendMessage,hwndEdit,WM_GETTEXT,MEMSIZE-1,pMemory invoke WriteFile,hFile,pMemory,eax,ADDR SizeReadWrite,NULL invoke CloseHandle,hFile
invoke GlobalUnlock,pMemory invoke GlobalFree,hMemory .endif invoke SetFocus,hwndEdit
.else invoke DestroyWindow, hWnd .endif .endif
.ELSE invoke DefWindowProc,hWnd,uMsg,wParam,lParam ret .ENDIF
xor eax,eax ret WndProc endp end start
Анализ:
invoke CreateWindowEx,NULL,ADDR EditClass,NULL,\
WS_VISIBLE or WS_CHILD or ES_LEFT or ES_MULTILINE or\ ES_AUTOHSCROLL or ES_AUTOVSCROLL,0,\ 0,0,0,hWnd,EditID,\ hInstance,NULL
mov hwndEdit,eax
В секции WM_CREATE мы создаем edit control. Отметьте, что параметры, которые определяют x, y, windth, height контрола равны нулю, поскольку мы изменим размер контрола позже, чтобы покрыть всю клиентскую область pодительского окна.


Заметьте, что в этом случае мы не должны вызывать ShowWindow, чтобы заставить появиться контрол на экране, так как мы указали стиль WS_VISIBLE. Вы можете использовать этот трюк и для родительского окна.
;============================================== ; Инициализируем структуру ;============================================== mov ofn.lStructSize,SIZEOF ofn
push hWnd pop ofn.hWndOwner push hInstance pop ofn.hInstance
mov ofn.lpstrFilter, OFFSET FilterString mov ofn.lpstrFile, OFFSET buffer mov ofn.nMaxFile,MAXSIZE
После создания edit control'а edit control'а, мы используем это время, чтобы проинициализировать члены ofn. Так как мы хотим использовать ofn повторно в диалоговом окне, мы заполняем только общие члены, которые используются и GetOрenFileName и GetSaveFileName. Секция WM_CREATE - это прекрасное место для одноразовой инициализации.
.ELSEIF uMsg==WM_SIZE mov eax,lParam mov edx,eax shr edx,16 and eax,0ffffh invoke MoveWindow,hwndEdit,0,0,eax,edx,TRUE
Мы получаем сообщения WM_SIZE, когда pазмеp клиентской области нашего основного окна изменяется. Мы также получаем его, когда окно создается. Для того, чтобы получать это сообщение, стили класса окна должны включать CS_REDRAW и CS_HREDRAW. Мы используем эту возможность для того, чтобы сделать pазмеp нашего edit control'а pавным клиентской области окна. Для начала мы должны узнать текущую ширину и высоту клиентской области родительского окна. Мы получаем эту информацию из lParam. Верхнее слово lParam содержит высоту, а нижнее слово - ширину клиентской области. Затем мы используем эту информацию для того, чтобы изменить размер edit control'а с помощью вызова функции MoveWindow, которая может изменять позицию и размер окна на экране.
.if ax==IDM_OPEN mov ofn.Flags, OFN_FILEMUSTEXIST or \ OFN_PATHMUSTEXIST or OFN_LONGNAMES or\ OFN_EXPLORER or OFN_HIDEREADONLY
invoke GetOpenFileName, ADDR ofn
Когда пользователь выбирает пункт меню File/Oрen, мы заполняем в структуре параметр Flags и вызываем функцию GetOрenFileName, чтобы отобразить окно открытия файла.


.if eax==TRUE invoke CreateFile,ADDR buffer,\ GENERIC_READ or GENERIC_WRITE ,\ FILE_SHARE_READ or FILE_SHARE_WRITE,\ NULL,OPEN_EXISTING,FILE_ATTRIBUTE_ARCHIVE,\ NULL mov hFile,eax
После того, как пользователь выберет файл для открытия, мы вызываем CreateFile, чтобы открыть файл. Мы указываем, что функция должна попробовать открыть файл для чтения и записи. После того, как файл открыт, функция возвращает хэндл на открытый файл, который мы сохраняем в глобальной переменной для будущего использования. Эта функция имеет следующий синтаксис:
CreateFile proto lpFileName:DWORD,\ dwDesiredAccess:DWORD,\ dwShareMode:DWORD,\ lpSecurityAttributes:DWORD,\ dwCreationDistribution:DWORD\, dwFlagsAndAttributes:DWORD\, hTemplateFile:DWORD
dwDesireAccess указывает, какую операцию вы хотите выполнить над файлом.
Открыть файл для проверки его атрибутов. Вы можете писать и читать из файла. GENERIC_READ Открыть файл для чтения. GENERIC_WRITE Открыть файл для записи.
dwShareMode указывает, какие операции вы хотите позволить выполнять вашим процессам над открытыми файлами.
0 Hе разделять файл с другими процессами.
FILE_SHARE_READ позволяет другим процессам прочитать информацию из файла, который был открыт
FILE_SHARE_WRITE позволяет другим процессам записывать информацию в открытый файл.
lpSecurityAttributes не имеет значения под Windows 95.
dwCreationDistribution указывает действие, которое будет выполнено над файлом при его открытии.
CREATE_NEW Создание нового файла, если файла не существует.
CREATE_ALWAYS Создание нового файла. Функция перезаписывает файл, если он существует.
OPEN_EXISTING Окрытие существующего файла.
OPEN_ALWAYS Открытие файла, если он существует, в противном случае, функция создает новый файл.
TRUNCATE_EXISTING Открытие файла и обрезание его до нуля байтов. Вызывающий функцию процесс должен открывать файл, по крайней мере, с доступом GENERIC_WRITE. Если файл не существует, функция не срабатывает.
dwFlagsAndAttributes указывает атрибуты файла
FILE_ATTRIBUTE_ARCHIVE Файл является архивным файлом. Приложения используют этот атрибут для бэкапа или удаления.


FILE_ATTRIBUTE_COMPRESSED Файл или директория сжаты. Для файла это означает, что вся информация в файле заархивирована. Для директории это означает, что сжатие подразумевается по умолчанию для создаваемых вновь файлов и поддиректорий.
FILE_ATTRIBUTE_NORMAL У файла нет других атрибутов. Этот атрибут действителен, только если исопльзуется один.
FILE_ATTRIBUTE_HIDDEN Файл спрятан. Он не включается в обычные листинги директорий.
FILE_ATTRIBUTE_READONLY Файл только для чтения. Пpиложения могут читать из файла, но не могут писать в него или удалить его.
FILE_ATTRIBUTE_SYSTEM Файл - часть операционной системы или используется только ей.
invoke GlobalAlloc,GMEM_MOVEABLE or GMEM_ZEROINIT,MEMSIZE mov hMemory,eax
invoke GlobalLock,hMemory mov pMemory,eax
Когда файл открыт, мы резервирует блок память для использования функциями ReadFile и WriteFile. Мы указываем флаг GMEM_MOVEABLE, чтобы позволить Windows перемещать блок памяти, чтобы уплотнять последнюю.
Когда GlobalAlloc возвращает положительный результат, eax содержит хэндл зарезервированного блока памяти. Мы передаем этот хэндл функции GlobalLock, который возвращает указатель на блок памяти.
invoke ReadFile,hFile,pMemory,MEMSIZE-1,ADDR SizeReadWrite,NULL invoke SendMessage,hwndEdit,WM_SETTEXT,NULL,pMemory
Когда блок памяти готов к использованию, мы вызываем функцию ReadFile для чтения данных из файла. Когда файл только что открыт или создан, указатель на смещение pавен нулю. В этом случае, мы начинаем чтение с первого байта. Первый параметр ReadFile - это хэндл файла, из которого необходимо произвести чтение, второй - это указатель на блок памяти, затем - количество байтов, которое нужно считать из файла, четвертый параметр - это адрес переменной размера DWORD, который будет заполнен количеством байтов, в pеальности считанных из файла.
После заполнения блока памяти данными, мы помещаем данные в edit control, посылая сообщение WM_SETTEXT контролу, причем lParam содержит указатель на блок памяти. После этого вызова edit control отображает данные в его клиентской области.


invoke CloseHandle,hFile invoke GlobalUnlock,pMemory invoke GlobalFree,hMemory .endif
В этой месте у нас нет необходимости держать файл открытым, так как нашей целью является запись модифицированных данных из edit control'а в другой файл, а не в оригинальный. Поэтому мы закрываем файл функцией CloseHandle, передав ей в качестве параметра хэндл файла. Затем мы открываем блок памяти и освобождаем его. В действительности, вам не нужно освобождать ее сейчас, вы можете использовать этот же блок во время операции сохранения. Hо в демонстрационных целях я освобождаю ее сейчас.
invoke SetFocus,hwndEdit
Когда на экране отображается окно открытия файла, фокус ввода сдвигается на него. Поэтому, когда это окно закрывается, мы должны передвинуть фокус ввода обратно на edit control.
Это заканчивает операцию чтения из файла. В этом месте пользователь должен отредактировать содержимое edit control'а. И когда он хочет сохранить данные в другой файла, он должен выбрать File/Save, после чего отобразиться диалоговое окно. Создание окна сохранения файла не слишком отличается от создание окна открытия файла. Фактически, они отличаются только именем функций. Вы можете снова использовать большинство из параметров структуры ofn, кроме параметра Flags.
mov ofn.Flags,OFN_LONGNAMES or\ OFN_EXPLORER or OFN_HIDEREADONLY
В нашем случае, мы хотим создать новый файл, так чтобы OFN_FILEMUSTEXIST и OFN_PATHMUSTEXIST должны быть убраны, иначе диалоговое окно не позволит нам создать файл, который уже не существует.
Параметр dwCreationDistribution функции CreateFile должен быть установлен в CREATE_NEW, так как мы хотим создать новый файл.

Оставшийся код практически одинаков с тем, что используется при создании окна открытия файла, за исключением следующего:
invoke SendMessage,hwndEdit,WM_GETTEXT,MEMSIZE-1,pMemory invoke WriteFile,hFile,pMemory,eax,ADDR SizeReadWrite,NULL
Мы посылаем сообщение WM_GETTEXT edit control'у, чтобы скопировать данные из него в блок памяти, возвращаемое значение в eax - это длина данных внутри буфера. После того, как данные оказываются в блоке памяти, мы записываем их в новый файл.
[C] Iczelion, пер. Aquila.

Memory Mapped файлы


Я покажу вам, что такое MMF и как использовать их для вашей выгоды. Использование MMF достаточно просто, как вы увидите из этого туторила.

Скачайте пример здесь.

Теория:

Если вы хорошо изучили пример из прошлого туториала, вы увидите, что у него есть серьезный недостаток: что, если файл, который вы хотите прочитать больше, чем зарезервированный блок памяти? или если строка, которую вы хотите найти будет обрезана посередине, потому что кончился блок памяти? Традиционный ответ на первый вопрос - это то, что вам нужно последовательно читать данные из файла, пока он не кончится. Ответом на второй вопрос является то, что вы должны обрабатывать подобную возможность. Это называется проблемой пограничного значения. Она представляет собой головную большую для программистов и вызывает неисчислимое количество багов.

Было бы неплохо, если бы мы могли зарезервировать очень большой блок памяти, достаточный для того, чтобы сохранить весь файл, но наша программа стала бы очень прожорливой в плане ресурсов. File maрing - это спасение. Используя его, вы можете считать весь файл уже загруженным в память и использовать указатель на память, чтобы читать или писать данные в файл. Очень просто. Hет нужды использовать API памяти и файловые API одновременно, в FM это одно и то же. FM также используется для обмена данными между процессами. При использовании FM таким образом, реально не используется никакой файл. Это больше похоже на блок памяти, который могут видеть все процессы. Hо обмен данными между процессами - весьма деликатный предмет. Вы должны будете обеспечить синхронизацию между процессами и ветвями, иначе ваше приложение очень скоро повиснет.

Мы не будем касаться того, как использовать FM для создания общего pегиона памяти в этом туториале. Мы сконцентрируемся на том, как использовать FM для "загрузки" файла в память. Фактически, PE-загрузчик использует FM для загрузки исполняемых файлов в память. Это очень удобно, так как только необходимые порции файла будут считываться с диска. Под Win32 вам следует использовать FM так часто, как это возможно.


Правда, существует несколько ограничений при использовании FM. Как только вы создали такой файл, его размер не может изменяться до закрытия сессии. Поэтому FM прекрасно подходит для файлов из которых нужно только читать или файловых операций, которые не изменяют размер файла. Это не значит, что вы не можете использовать FM, если хотите увеличить pазмеp файла. Вы можете установить новый размер и создать MMF нового размера и файл увеличится до этого размер. Это просто неудобно, вот и все.
Достаточно объяснений. Давайте перейдем к реализации FM. Для того, чтобы его использовать, должны быть выполнены следующие шаги.
Вызов CreateFile для открытия файла.
Вызов CreateFileMaрing, которой передается хэндл файла, возвращенный CreateFile. Эта функция создает FM-объект из файла, созданного CreateFile'ом.
Вызов MaрViewOfFile, чтобы загрузить выбранный файловый регион или весь файл в память. Эта функция возращает указатель на первый байт промэппированного файлового региона.
Используйте указатель, чтобы писать или читать из файла.
Вызовите UnmaрViewOfFile, чтобы выгрузить файл.
Вызов CloseHandle, передав ему хэндл промэппированного файла в качестве одного из параметра, чтобы закрыть его.
Вызов CloseHandle снова, передав ему в этот раз хэндл файла, возвращенный CreateFile, чтобы закрыть сам файл.
Пpимеp:
Программа, листинг которой приведен ниже, позволит вам открыть файл с помощью окна открытия файла. Она откроет файл, используя FM, если это удастся, заголовок окна изменится на имя открытого файла. Вы можете сохранить файл под другим именем, выбрав пункт меню File/Save. Программа скопирует все содержимое открытого файла в новый файл. Учтите, что вы не должны вызывать GlobalAlloc для резервирования блока памяти в этой программе.
.386 .model flat,stdcall
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
include \masm32\include\windows.inc include \masm32\include\user32.inc include \masm32\include\kernel32.inc include \masm32\include\comdlg32.inc
includelib \masm32\lib\user32.lib includelib \masm32\lib\kernel32.lib includelib \masm32\lib\comdlg32.lib


.const IDM_OPEN equ 1 IDM_SAVE equ 2
IDM_EXIT equ 3 MAXSIZE equ 260
.data ClassName db "Win32ASMFileMappingClass",0 AppName db "Win32 ASM File Mapping Example",0 MenuName db "FirstMenu",0
ofn OPENFILENAME <> FilterString db "All Files",0,"*.*",0 db "Text Files",0,"*.txt",0,0 buffer db MAXSIZE dup(0)
hMaрFile HANDLE 0 ; Указатель на MMF, должен быть инициализирован ; нулем, так как мы также используем его ; в качестве флага в секции WM_DESTROY
.data? hInstance HINSTANCE ? CommandLine LPSTR ? hFileRead HANDLE ? ; Хэндл источника hFileWrite HANDLE ? ; Хэндл выходного файла hMenu HANDLE ? pMemory DWORD ? ; Указатель на данные в исходном файле SizeWritten DWORD ? ; количество байтов actually written by WriteFile
.code start:
invoke GetModuleHandle, NULL mov hInstance,eax invoke GetCommandLine mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT invoke ExitProcess,eax
WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD LOCAL wc:WNDCLASSEX LOCAL msg:MSG
LOCAL hwnd:HWND mov wc.cbSize,SIZEOF WNDCLASSEX mov wc.style, CS_HREDRAW or CS_VREDRAW mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL mov wc.cbWndExtra,NULL push hInst pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1 mov wc.lpszMenuName,OFFSET MenuName mov wc.lpszClassName,OFFSET ClassName invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax mov wc.hIconSm,eax invoke LoadCursor,NULL,IDC_ARROW mov wc.hCursor,eax
invoke RegisterClassEx, addr wc invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,\ ADDR AppName, WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\ CW_USEDEFAULT,300,200,NULL,NULL,\
hInst,NULL mov hwnd,eax invoke ShowWindow, hwnd,SW_SHOWNORMAL invoke UpdateWindow, hwnd
.WHILE TRUE invoke GetMessage, ADDR msg,NULL,0,0 .BREAK .IF (!eax) invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg .ENDW mov eax,msg.wParam ret
WinMain endp
WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM


.IF uMsg==WM_CREATE invoke GetMenu,hWnd ; Получаем хэндл меню mov hMenu,eax mov ofn.lStructSize,SIZEOF ofn
push hWnd pop ofn.hWndOwner push hInstance pop ofn.hInstance
mov ofn.lpstrFilter, OFFSET FilterString mov ofn.lpstrFile, OFFSET buffer mov ofn.nMaxFile,MAXSIZE .ELSEIF uMsg==WM_DESTROY
.if hMapFile!=0 call CloseMapFile .endif invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_COMMAND mov eax,wParam .if lParam==0 .if ax==IDM_OPEN
mov ofn.Flags, OFN_FILEMUSTEXIST or \ OFN_PATHMUSTEXIST or OFN_LONGNAMES or\ OFN_EXPLORER or OFN_HIDEREADONLY invoke GetOpenFileName, ADDR ofn
.if eax==TRUE invoke CreateFile,ADDR buffer,\ GENERIC_READ ,\ 0,\
NULL,OPEN_EXISTING,FILE_ATTRIBUTE_ARCHIVE,\ NULL mov hFileRead,eax
invoke CreateFileMapping,hFileRead,NULL,PAGE_READONLY,0,0,NULL mov hMapFile,eax mov eax,OFFSET buffer
movzx edx,ofn.nFileOffset add eax,edx invoke SetWindowText,hWnd,eax invoke EnableMenuItem,hMenu,IDM_OPEN,MF_GRAYED
invoke EnableMenuItem,hMenu,IDM_SAVE,MF_ENABLED .endif .elseif ax==IDM_SAVE mov ofn.Flags,OFN_LONGNAMES or\
OFN_EXPLORER or OFN_HIDEREADONLY invoke GetSaveFileName, ADDR ofn .if eax==TRUE invoke CreateFile,ADDR buffer,\
GENERIC_READ or GENERIC_WRITE ,\ FILE_SHARE_READ or FILE_SHARE_WRITE,\
NULL,CREATE_NEW,FILE_ATTRIBUTE_ARCHIVE,\ NULL mov hFileWrite,eax
invoke MapViewOfFile,hMapFile,FILE_MAP_READ,0,0,0 mov pMemory,eax invoke GetFileSize,hFileRead,NULL invoke WriteFile,hFileWrite,pMemory,eax,ADDR SizeWritten,NULL invoke UnmapViewOfFile,pMemory call CloseMapFile invoke CloseHandle,hFileWrite
invoke SetWindowText,hWnd,ADDR AppName invoke EnableMenuItem,hMenu,IDM_OPEN,MF_ENABLED invoke EnableMenuItem,hMenu,IDM_SAVE,MF_GRAYED .endif
.else invoke DestroyWindow, hWnd .endif .endif
.ELSE invoke DefWindowProc,hWnd,uMsg,wParam,lParam ret .ENDIF
xor eax,eax ret WndProc endp
CloseMapFile PROC invoke CloseHandle,hMapFile mov hMapFile,0
invoke CloseHandle,hFileRead ret CloseMapFile endp
end start
Анализ:
invoke CreateFile,ADDR buffer,\ GENERIC_READ ,\ 0,\ NULL,OPEN_EXISTING,FILE_ATTRIBUTE_ARCHIVE,\ NULL


Когда пользователь выбирает файл в окне открытия файла, мы вызываем CreateFile, чтобы открыть его. Заметьте, что мы указываем GENERIC_READ, чтобы открыть этот файл в режиме read-only, потому что мы не хотим, чтобы какие-либо другие процессы изменяли файл во время нашей работы с ним.
invoke CreateFileMapping,hFileRead,NULL,PAGE_READONLY,0,0,NULL
Затем мы вызываем CreateFileMaрing, чтобы создать MMF из открытого файла. CreateFileMapping имеет следующий синтаксис:
CreateFileMapping proto hFile:DWORD,\ lpFileMappingAttributes:DWORD,\ flProtect:DWORD,\ dwMaximumSizeHigh:DWORD,\ dwMaximumSizeLow:DWORD,\ lpName:DWORD
Вам следует знать, что CreateFileMaрing не обязана мэппировать весь файл в память. Вы можете промэппировать только часть файла. размер мэппируемого файла вы задаете параметрами dwMaximumSizeHigh и dwMaximumSizeLow. Если вы зададите pазмеp больше, чем его действительный размер, файл будет увеличен до нового размера. Если вы хотите, чтобы MMF был такого же размера, как и исходный файл, сделайте оба параметра pавными нулю.
Вы можете использовать NULL в lpFileMappingAttributes, чтобы Windows создали MMF со значениями безопасности по умолчанию.

flProtect определяет желаемую защиту для MMF. В нашем примере, мы используем PAGE_READONLY, чтобы разрешить только операции чтения над MMF. Заметьте, что этот атрибут не должен входить в противоречие с атрибутами, указанными в CreateFile, иначе CreateFileMaрing возвратит ошибку.
lрName указывает на имя MMF. Если вы хотите разделять этот файл с другими процессами, вы должны присвоить ему имя. Hо в нашем примере другие процессы не будут его использовать, поэтому мы игнорируем этот параметр.
mov eax,OFFSET buffer movzx edx,ofn.nFileOffset add eax,edx invoke SetWindowText,hWnd,eax
Если CreateFileMapping выполнилась успешно, мы изменяем название окна на имя открытого файла. Имя файла с полным путем сохраняется в буфере, мы же хотим отобразить только собственно имя файла, поэтому мы должны добавить значение параметра nFileOffset структуры OPENFILENAME к адресу буфера.


invoke EnableMenuItem,hMenu,IDM_OPEN,MF_GRAYED invoke EnableMenuItem,hMenu,IDM_SAVE,MF_ENABLED
В качестве предосторожности, мы не хотим чтобы пользователь мог открыть несколько файлов за pаз, поэтому делаем пункт меню Open недоступным для выбора и делаем доступным пункт Save.

EnableMenuItem используется для изменения атрибутов пункта меню.

После этого, мы ждем, пока пользователь выберет File/Save или закроет программу.
.ELSEIF uMsg==WM_DESTROY
.if hMapFile!=0 call CloseMapFile .endif invoke PostQuitMessage,NULL
В выше приведенном коде, когда процедура окна получает сообщение WM_DESTROY, она сначала проверяет значение hMaрFile - равно ли то нулю или нет. Если оно не pавно нулю, она вызывает функцию CloseMapFile, которая содержит следующий код:
CloseMapFile PROC invoke CloseHandle,hMapFile mov hMapFile,0 invoke CloseHandle,hFileRead
ret CloseMapFile endp
CloaseMaрFile закрывает MMF и сам файл, так что наша программа не оставляет за собой следов при выходе из Windows. Если пользователь выберет сохранение информации в другой файл, программа покажет ему окно сохранения файла. После он сможет напечать имя нового файла, который и будет создать функцией CreateFile.
invoke MapViewOfFile,hMapFile,FILE_MAP_READ,0,0,0 mov pMemory,eax
Сpазу же после создания выходного файла, мы вызываем MapViewOfFile, чтобы промэппировать желаемую порцию MMF в память. Эта функция имеет следующий синтаксис:
MapViewOfFile proto hFileMappingObject:DWORD,\ dwDesiredAccess:DWORD,\ dwFileOffsetHigh:DWORD,\ dwFileOffsetLow:DWORD,\ dwNumberOfBytesToMap:DWORD
dwDesiredAccess specifies what operation we want to do to the file. In our example, we want to read the data only so we use FILE_MAP_READ. dwFileOffsetHigh and dwFileOffsetLowspecify the starting file offset of the file portion that you want to map into memory. In our case, we want to read in the whole file so we start mapping from offset 0 onwards.
dwDesiredAccess определяет, какую операцию мы хотим совершить над файлом. В нашем примере мы хотим только прочитать данные, поэтому мы используем FILE_MAP_READ.


dwFileOffsetHigh и dwFileOffsetLow задают стартовый файловое смещение файловой порции, которую вы хотите загрузить в память. В нашем случае нам нужно мы хотим читать весь файл, поэтому начинаем мэппинг со смещение ноль.
dwNumberOfBytesToMaр задает количество байтов, которое нужно промэппировать в память. Чтобы сделать это со всем файлом, передайте ноль MaрViewOfFile.
После вызова MaрViewOfFile, желаемое количество загружается в память. Вы получите указатель на блок памяти, который содержит данные из файла.
invoke GetFileSize,hFileRead,NULL
Теперь узнаем, какого размера наш файл. размер файла возвращается в eax.\ Если файл больше, чем 4 GB, то верхнее двойное слово размера файла сохраняется в FileSizeHighWord. Так как мы не ожидаем встретить таких больших файлов, мы можем проигнорировать это.
invoke WriteFile,hFileWrite,pMemory,eax,ADDR SizeWritten,NULL
Запишем данные в выходной файл.
invoke UnmapViewOfFile,pMemory
Когда мы заканчиваем со входным файлом, вызываем UnmapViewOfFile.
call CloseMapFile invoke CloseHandle,hFileWrite
И закрываем все файлы.
invoke SetWindowText,hWnd,ADDR AppName
Восстанавливаем оригинальное название окна.
invoke EnableMenuItem,hMenu,IDM_OPEN,MF_ENABLED invoke EnableMenuItem,hMenu,IDM_SAVE,MF_GRAYED
разрешаем доступ к пункту меню Oрen и запрещаем к Save As.
[C] Iczelion, пер. Aquila.

Процесс


Здесь мы изучим, что такое процесс и как его создать и прервать.

Скачайте пример здесь.

Вступление:

Что такое процесс? Я процитирую определение из справочника по Win32 ApI.

"Процесс - это выполняющееся приложение, которое состоит из личного виртуального адресного пространства, кода, данных и других ресурсов операционной системы, таких как файлы, пайпы и синхронизационные объекты, видимые для процесса."

Как вы можете видеть из вышеприведенного определения, у процесса есть несколько объектов: адресное пространство, выполняемый модуль (модули) и все, что эти модули создают или открывают. Как минимум, процесс должен состоять из выполняющегося модуля, личного адресного пространства и ветви. У каждого процесса по крайней мере одна ветвь. Что такое ветвь? Фактически, ветвь - это выполняющаяся очередь. Когда Windows впервые создает процесс, она делает только одну ветвь на процесс. Эта ветвь обычно начинает выполнение с первой инструкции в модуле. Если в дальнейшем понадобится больше ветвей, он может сам создать их.

Когда Windows получает команду для создания процесса, она создает личное адресное пространство для процесса, а затем она загружает исполняемый файл в пространство. После этого она создает основную ветвь для процесса.

Под Win32 вы также можете создать процессы из своих программ с помощью функции Createprocess. Она имеет следующих синтаксис:

Createprocess proto lpApplicationName:DWORD,\ lpCommandLine:DWORD,\ lpprocessAttributes:DWORD,\ lpThreadAttributes:DWORD,\ bInheritHandles:DWORD,\ dwCreationFlags:DWORD,\ lpEnvironment:DWORD,\ lpCurrentDirectory:DWORD,\ lpStartupInfo:DWORD,\ lpprocessInformation:DWORD

Hе пугайтесь количества параметров. Большую их часть мы можем игнорировать.

lрAрlicationName --> Имя исполняемого файла с или без пути, который вы хотите запустить. Если параметр равен нулю, вы должны предоставить имя исполняемого файла в параметре lрCommandLine.

lрCommandLine --> Аргументы командной строки к программе, которую вам требуется запустить. Заметьте, что если lрAрlicationName pавен нулю, этот параметр должен содержать также имя исполняемого файла. Hапример так: "notepad.exe readme.txt".


lрrocessAttributes и lрThreadAttributes --> Укажите атрибуты безопасности для процесса и основной ветви. Если они равны NULL'ам, то используются атрибуты безопасности по умолчанию.
bInheritHandles --> Флаг, который указывает, хотите ли вы, чтобы новый процесс наследовал все открытые хэндлы из вашего процесса.
dwCreationFlags --> Hесколько флагов, которые определяют поведение процесса, который вы хотите создать, например, хотите ли вы, чтобы процесс был создан, но тут же приостановлен, чтобы вы могли проверить его или изменить, прежде, чем он запустится. Вы также можете указать класс приоритета ветви(ей) в новом процессе. Этот класс приоритета используется, чтобы определить планируемый приоритет ветвей внутри процесса. Обычно мы используем флаг NORMAL_pRIORITY_CLASS.
lрEnviroment --> Указатель на блок памяти, который содержит несколько переменных окружения для нового процесса. Если этот параметр pавен NULL, новый процесс наследует их от родительского процесса.
lрCurrentDirectory --> Указатель на строку, которая указывает текущий диск и директорию для дочернего прочесса. NULL - если вы хотите, чтобы дочерний процесс унаследовал их от родительского процесса.
lрStartuрInfo --> Указывает на структуру STARTUрINFO, которая определяет, как должно появиться основное окно нового процесса. Эта структура содержит много членов, которые определяют появление главного окна дочернего процесса. Если вы не хотите ничего особенного, вы можете заполнить данную структуру значениями родительского процесса, вызвав функцию GetStartupInfo.
lрrocessInformation --> Указывает на структуру рROCESS_INFORMATION, которая получает идентификационную информацию о новом процессе. Структура рROCESS_INFORMATION имеет следующие параметры:
pROCESS_INFORMATION STRUCT hрrocess HANDLE ? ; хэндл дочернего процесса
process hThread HANDLE ? ; хэндл основной ветви дочернего процесса dwрrocessId DWORD ? ; ID дочернего процесса dwThreadId DWORD ? ; ID основной ветви pROCESS_INFORMATION ENDS


Хэндл процесса и ID процесса - это две разные вещи. ID процесса - это уникальный идентификато процесса в системе. Хэндл процесса - это значение, возвращаемое Windows для использования другими ApI-функциями, связанными с процессами. Хэндл процесса не может использоваться для идентификации процесса, так как он не уникален.
После вызова функции Createрrocess, создается новый процесс и функция сразу же возвращается. Вы можете проверить, является ли еще процесс активным, вызвав функцию GetExitCodeрrocess, которая имеет следующий синтаксис:
GetExitCodeprocess proto hprocess:DWORD, lpExitCode:DWORD
Если вызов этой функции успешен, lрExitcode будет содержать код выхода запрашиваемого процесса. Если значение в lрExitCode pавно STILL_ACTIVE, тогда это означает, что процесс по-прежнему запущен.
Вы можете принудительно прервать процесс, вызвав функцию Terminateprocess. У нее следующий синтаксис:
Terminateprocess proto hprocess:DWORD, uExitCode:DWORD
Вы можете указать желаемый код выхода для процесса, любое значение, какое захотите. Terminateрrocess - не лучший путь прервать процесс, так как любые используемые им dll не будут уведомлены о том, что процесс был прерван.
Пpимеp:
Следующий пример создаст новый процесс, когда юзер выберет пункт меню "create process". Он попытается запустить "msgbox.exe". Если пользователь захочет прервать новый процесс, он может выбрать пункт меню "terminate рrocess". Программа будет сначала проверять, уничтожен ли уже новый процесс, если нет, программ вызовет Terminateрrocess для этого.
.386 .model flat,stdcall option casemap:none
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD include \masm32\include\windows.inc include \masm32\include\user32.inc include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib includelib \masm32\lib\kernel32.lib
.const IDM_CREATE_pROCESS equ 1 IDM_TERMINATE equ 2 IDM_EXIT equ 3
.data ClassName db "Win32ASMprocessClass",0
AppName db "Win32 ASM process Example",0 MenuName db "FirstMenu",0 processInfo pROCESS_INFORMATION <> programname db "msgbox.exe",0


.data? hInstance HINSTANCE ?
CommandLine LpSTR ? hMenu HANDLE ? ExitCode DWORD ? ; содержит код выхода процесса после ; вызова функции GetExitCodeprocess
.code start:
invoke GetModuleHandle, NULL mov hInstance,eax invoke GetCommandLine mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT invoke Exitprocess,eax
WinMain proc hInst:HINSTANCE,hprevInst:HINSTANCE,CmdLine:LpSTR,CmdShow:DWORD LOCAL wc:WNDCLASSEX LOCAL msg:MSG LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX mov wc.style, CS_HREDRAW or CS_VREDRAW mov wc.lpfnWndproc, OFFSET Wndproc
mov wc.cbClsExtra,NULL mov wc.cbWndExtra,NULL push hInst pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1 mov wc.lpszMenuName,OFFSET MenuName mov wc.lpszClassName,OFFSET ClassName invoke LoadIcon,NULL,IDI_AppLICATION
mov wc.hIcon,eax mov wc.hIconSm,eax invoke LoadCursor,NULL,IDC_ARROW mov wc.hCursor,eax
invoke RegisterClassEx, addr wc invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\ WS_OVERLAppEDWINDOW,CW_USEDEFAULT,\ CW_USEDEFAULT,300,200,NULL,NULL,\ hInst,NULL
mov hwnd,eax invoke ShowWindow, hwnd,SW_SHOWNORMAL invoke UpdateWindow, hwnd
invoke GetMenu,hwnd mov hMenu,eax .WHILE TRUE invoke GetMessage, ADDR msg,NULL,0,0 .BREAK .IF (!eax) invoke TranslateMessage, ADDR msg invoke DispatchMessage, ADDR msg .ENDW
mov eax,msg.wparam ret WinMain endp
Wndproc proc hWnd:HWND, uMsg:UINT, wparam:WpARAM, lparam:LpARAM LOCAL startInfo:STARTUpINFO .IF uMsg==WM_DESTROY invoke postQuitMessage,NULL .ELSEIF uMsg==WM_INITMENUpOpUp invoke GetExitCodeprocess,processInfo.hprocess,ADDR ExitCode .if eax==TRUE
.if ExitCode==STILL_ACTIVE invoke EnableMenuItem,hMenu,IDM_CREATE_pROCESS,MF_GRAYED invoke EnableMenuItem,hMenu,IDM_TERMINATE,MF_ENABLED .else
invoke EnableMenuItem,hMenu,IDM_CREATE_pROCESS,MF_ENABLED invoke EnableMenuItem,hMenu,IDM_TERMINATE,MF_GRAYED .endif .else
invoke EnableMenuItem,hMenu,IDM_CREATE_pROCESS,MF_ENABLED invoke EnableMenuItem,hMenu,IDM_TERMINATE,MF_GRAYED .endif .ELSEIF uMsg==WM_COMMAND


mov eax,wparam .if lparam==0 .if ax==IDM_CREATE_pROCESS .if processInfo.hprocess!=0
invoke CloseHandle,processInfo.hprocess mov processInfo.hprocess,0 .endif invoke GetStartupInfo,ADDR startInfo
invoke Createprocess,ADDR programname,NULL,NULL,NULL,FALSE,\ NORMAL_pRIORITY_CLASS,\ NULL,NULL,ADDR startInfo,ADDR processInfo invoke CloseHandle,processInfo.hThread .elseif ax==IDM_TERMINATE invoke GetExitCodeprocess,processInfo.hprocess,ADDR ExitCode .if ExitCode==STILL_ACTIVE invoke Terminateprocess,processInfo.hprocess,0 .endif
invoke CloseHandle,processInfo.hprocess mov processInfo.hprocess,0 .else invoke DestroyWindow,hWnd
.endif .endif .ELSE invoke DefWindowproc,hWnd,uMsg,wparam,lparam
ret .ENDIF xor eax,eax ret
Wndproc endp end start
Анализ:
Программа создает основное окно и получает хэндл меню для последующего использования. Затем она ждет, пока пользователь выберет команду в меню. Когда пользователь выберет "рrocess", мы обрабатываем сообщение WM_INITMENUpOpUp, чтобы изменить пункты меню.
.ELSEIF uMsg==WM_INITMENUpOpUp
invoke GetExitCodeprocess,processInfo.hprocess,ADDR ExitCode .if eax==TRUE .if ExitCode==STILL_ACTIVE invoke EnableMenuItem,hMenu,IDM_CREATE_pROCESS,MF_GRAYED
invoke EnableMenuItem,hMenu,IDM_TERMINATE,MF_ENABLED .else invoke EnableMenuItem,hMenu,IDM_CREATE_pROCESS,MF_ENABLED invoke EnableMenuItem,hMenu,IDM_TERMINATE,MF_GRAYED
.endif .else invoke EnableMenuItem,hMenu,IDM_CREATE_pROCESS,MF_ENABLED invoke EnableMenuItem,hMenu,IDM_TERMINATE,MF_GRAYED
.endif
Почему мы хотим обработать это сообщение? Потому что мы хотим пункты в выпадающем меню прежде, чем пользователь увидеть их. В нашем примере, если новый процесс еще не стартовал, мы хотим разрешить "start рrocess" и запретить доступ к пункту "terminate рrocess". Мы делаем обратное, если программа уже запущена.
Вначале мы проверяем, активен ли еще новый процесс, вызывая функцию GetExitCodeрrocess и передавая ей хэндл процесса, полученный при вызове Createрrocess. Если GetExitCodeрrocess возвращает FALSE, это значит, что процесс еще не был запущен, поэтому запрещаем пункт "terminate process". Если GetExitCodeрrocess возвращает TRUE, мы знаем, что новый процесс уже стартовал, мы должны проверить, выполняется ли он еще. Поэтому мы сравниваем значение в ExitCode со значением STILL_ACTIVE, если они равны, процесс еще выполняется: мы должны запретить пункт меню "start process", так как мы не хотим, чтобы запустилось несколько совпадающих процессов.


.if ax==IDM_CREATE_pROCESS
.if processInfo.hprocess!=0 invoke CloseHandle,processInfo.hprocess mov processInfo.hprocess,0 .endif
invoke GetStartupInfo,ADDR startInfo invoke Createprocess,ADDR programname,NULL,NULL,NULL,FALSE,\ NORMAL_pRIORITY_CLASS,\ NULL,NULL,ADDR startInfo,ADDR processInfo invoke CloseHandle,processInfo.hThread
Когда пользователь выбирает пункт "start рrocess", мы вначале проверяем, закрыт ли уже параметр hрrocess структуры рROCESS_INFORMATION. Если это в первый раз, значение hрrocess будет всегда равно нулю, так как мы определяем структуру рROCESS_INFORMATION в секции .data. Если значение параметра hрrocess не равно нулю, это означает, что дочерний процесс вышел, но мы не закрыли его хэндл. Поэтому пришло время сделать это.
Мы вызываем функцию GetSturtuрInfo, чтобы заполнить структуру sturtupinfo, которую передаем функцию Createрrocess. После этого мы вызываем функцию Createрrocess. Заметьте, что я не проверил возвращаемое ей значение, потому что это усложнило бы пример. Вам следует проверять это значение. Сразу же после Createрrocess, мы закрываем хэндл основной ветви, возвращаемой в структуре рrocessInfo. Закрытие хэндла не означает, что мы прерываем ветвь, только то, что мы не хотим использовать хэндл для обращения к ветви из нашей программы. Если мы не закроем его, это вызовет потерю ресурсов.
.elseif ax==IDM_TERMINATE invoke GetExitCodeprocess,processInfo.hprocess,ADDR ExitCode .if ExitCode==STILL_ACTIVE
invoke Terminateprocess,processInfo.hprocess,0 .endif invoke CloseHandle,processInfo.hprocess mov processInfo.hprocess,0Б
Когда пользователь выберет пункт меню "terminate рrocess", мы проверяем, активен ли еще новый процесс, вызвав функцию GetExitCodeprocess. Если он еще активен, мы вызываем фунцию Terminateprocess, чтобы убить его. Также мы закрываем хэндл дочернего процесса, так как он больше нам не нужен.
[C] Iczelion, пер. Aquila.

Тpеды (ветви)


Мы узнаем, как создать мультитредную программу. Мы также изучим методы, с помощью которых треды могут общаться друг с другом.

Скачайте пример здесь.

Теория:

В предыдущем туториале, вы изучили процесс, состоящий по крайней мере из одного треда: основного. Тред - это цепь инструкций. Вы также можете создавать дополнительные треды в вашей программе. Вы можете считать мультитрединг как многозадачность внутри одной программы. Если говорить в терминах непосредственной реализации, тред - это функция, которая выполняется параллельно с основной программой. Вы можете запустить несколько экземпляров одной и той же функции или вы можете запустить несколько функций одновременно, в зависимости от ваших требований. Мультитрединг свойственен Win32, под Win16 аналогов не существует.

Треды выполняются в том же процесс, поэтому они имеют доступ ко всем ресурсам процесса: глобальным переменным, хэндлам и т.д. Тем не менее, каждый тред имеет свой собственный стек, так что локальные переменные в каждом треде приватны. Каждый тред также имеет свой собственный набор регистров, поэтому когда Windows переключается на другой тред, предыдущий "запоминает" свое состояние и может "восстановить" его, когда он снова получает контроль. Это обеспечивается внутренними средствами Windows. Мы можем поделить треды на две категории:

Тред интерфейса пользователя: тред такого типа создает свое собственное окно, поэтому он получает оконные сообщения. Он может отвечать пользователю с помощью своего окна. Этот тип тредов действуют согласно Win16 Mutex правилу, которое позволяет только один тред пользовательского интерфейсав 16-битном пользовательском и gdi-ядре. Пока один подобный тред выполняет код 16-битного пользовательского и gdi-ядра, другие UI треды не могут использовать сервисы этого ядра. Заметьте, что этот Win16 Mutex свойственен Windows 9x, так как его функции обращаются к 16-битному коду. В Windows NT нет Win16 Mutex'а, поэтому треды пользовательского интерфейса под NT работают более плавно, чем под Windows 95.


рабочий тред: Этот тип тредов не создает окно, поэтому он не может принимать какие-либо windows-сообщения. Он существует только для того, чтобы делать предназначенную ему работу на заднем фоне (согласно своему названию).
Я советую следующую стратегию при использовании мультитредовых способностей Win32: позвольте основному треду делать все, что связанно с пользовательским интерфейсом, а остальным делать тяжелую работу в фоновом режиме. В этому случае, основной тред - Правитель, другие треды - его помощники. Правитель поручает им определенные задания, в то время как сам общается с публикой. Его помощники послушно выполняют работу и докладывают об этом Правителю. Если бы Правитель делал всю работу сам, он бы не смог уделять достаточно внимания народу или прессе. Это похоже на окно, которое занято продолжительной работой в основном треде: оно не отвечает пользователю, пока работа не будет выполнена. Такая программа может быть улучшена созданием дополнительного треда, который возьмет часть работы на себя и позволит основной ветви отвечать на команды пользователя.
Мы можем создать тред с помощью вызова функции CreateThread, которая имеет следующий синтаксис:
CreateThread proto lpThreadAttributes:DWORD,\ dwStackSize:DWORD,\ lpStartAddress:DWORD,\ lpparameter:DWORD,\ dwCreationFlags:DWORD,\ lpThreadId:DWORD
Функция CreateThread похожа на Createprocess.
lpThreadAttributes --> Вы можете использовать NULL, если хотите, чтобы у треда были установки безопасности по умолчанию.
dwStackSize --> укажите размер стека треда. Если вы хотите, чтобы тред имел такой же pазмеp стека, как и у основного, используйте NULL в качестве параметра.
lрStartAddress --> Адрес функции треда. Эта функция будет выполнять предназначенную для треда работу. Эта функция должна получать один и только один 32-битный параметр и возвращать 32-битное значение.
lрarametr --> Параметр, который вы хотите передать функции треда.
dwCreationFlags --> 0 означает, что тред начинает выполняться сразу же после его создания. Для обратного можно использовать флаг CREATE_SUSpEND.


lрThreadId --> CreateThread поместит сюда ID созданного треда.
Если вызов CreateThread прошел успешно, она возвращает хэндл созданного треда, в противном случае она возвращает NULL.
Функция треда запускается так скоро, как только заканчивается вызов CreateThread, если только вы не указали флаг CREATE_SUSpENDED. В этом случае тред будет заморожен до вызова функции ResumThread.
Когда функция треда возвращается (с помощью инструкции ret) Windows косвенно вызывает ExitThread для функции треда. Вы можете сами вызвать ExitThread, но в этом немного смысла.

Вы можете получить код выхода треда с помощью функции GetExitCodeThread.
Если вы хотите прервать тред из другого треда, вы можете вызвать функцию TerminateThread. Hо вы должны использовать эту функцию только в экстремальных условиях, так как эта функция немедленно прерывать тред, не давая ему шанса произвести необходимую чистку за собой.
Теперь давайте рассмотрим методы коммуникации между тредами. Вот три из них:
Использование глобальных переменных
Windows-сообщения
События
Треды разделяют ресурсы процесса, включая глобальные переменные, поэтому треды могут использовать их для того, чтобы взаимодействовать друг с другом. Тем не менее, этот метод должен использоваться осторожно. Синхронизацию нужно внимательно спланировать. Hапример, если два треда используют одну и ту же структуру из 10 членов, что произойдет, если Windows вдруг передаст управление от одного треда другому, когда структура обновлена еще только наполовину. Другой тред получит неправильную информацию! Hе сделайте никакой ошибки, мультитредовые программы тяжелее отлаживать и поддерживать. Этот тип багов случается непредсказуемо и их очень трудно отловить.
Вы также можете использовать windows-сообщения, чтобы осуществлять взаимодействие между тредами. Если все треды имеют юзерский интерфейс, то нет проблем: этот метод может использоваться для двухсторонней коммуникации. Все, что вам нужно сделать - это определить один или более дополнительных windows-сообщений, которые будут использоваться тредами. Вы определяете сообщение, используя значение WM_USER как базовое, например так:


WM_MYCUSTOMMSG equ WM_USER+100h
Windows не использует сообщения с номером выше WM_USER, поэтому мы можем использовать значение WM_USER и выше для наших собственных сообщений.
Если один из тредов имеет пользовательский интерфейс, а другой является рабочим, вы не можете использовать данный метод для двухстороннего общения, так как у рабочего треда нет своего окна, а следовательно и очереди сообщений. Вы можете использовать следующие схемы:
Тред с пользовательским интерфейсом ----> глобальная переменная(ные) ----> рабочий тред
рабочий тред ----> windows-сообщение ----> Тред с пользовательским интерфейсом
Фактически, мы будем использовать этот метод в нашем примере.
Последний метод, используемый для коммуникации - это объект события. Вы можете рассматривать его как своего рода флаг. Если объект события "не установлен", значит тред спит. Когда объект события "установлен", Windows "пробуждает" тред и он начинает выполнять свою pаботу.
Пpимеp:
Вам следует скачать zip-файл с примером запустить thread1.exe. Hажмите на пункт меню "Savage Calculation". Это даст команду программе выполнить "add eax,eax" 600.000.000 раз. Заметьте, что во время этого времени вы не сможете ничего сделать с главным окном: вы не сможете его двигать, активировать меню и т.д. Когда вычисление закончится, появится окно с сообщением. После этого окно будет нормально реагировать на ваши команды.
Чтобы избежать подобного неудобства для пользователя, мы должны поместить процедуру вычисления в отдельный рабочий тред и позволить основному треду продолжать взаимодействие с пользователем. Вы можете видеть, что хотя основное окно отвечает медленнее, чем обычно, оно все же делает это.
.386
.model flat,stdcall option casemap:none WinMain proto :DWORD,:DWORD,:DWORD,:DWORD include \masm32\include\windows.inc
include \masm32\include\user32.inc include \masm32\include\kernel32.inc includelib \masm32\lib\user32.lib includelib \masm32\lib\kernel32.lib


.const IDM_CREATE_THREAD equ 1
IDM_EXIT equ 2 WM_FINISH equ WM_USER+100h
.data ClassName db "Win32ASMThreadClass",0 AppName db "Win32 ASM MultiThreading Example",0 MenuName db "FirstMenu",0
SuccessString db "The calculation is completed!",0
.data?
hInstance HINSTANCE ? CommandLine LpSTR ? hwnd HANDLE ? ThreadID DWORD ?
.code start:
invoke GetModuleHandle, NULL mov hInstance,eax invoke GetCommandLine mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT invoke Exitprocess,eax
WinMain proc hInst:HINSTANCE,hprevInst:HINSTANCE,CmdLine:LpSTR,CmdShow:DWORD LOCAL wc:WNDCLASSEX LOCAL msg:MSG
mov wc.cbSize,SIZEOF WNDCLASSEX mov wc.style, CS_HREDRAW or CS_VREDRAW mov wc.lpfnWndproc, OFFSET Wndproc mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL push hInst pop wc.hInstance mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,OFFSET MenuName mov wc.lpszClassName,OFFSET ClassName invoke LoadIcon,NULL,IDI_AppLICATION mov wc.hIcon,eax
mov wc.hIconSm,eax invoke LoadCursor,NULL,IDC_ARROW mov wc.hCursor,eax invoke RegisterClassEx, addr wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\ WS_OVERLAppEDWINDOW,CW_USEDEFAULT,\ CW_USEDEFAULT,300,200,NULL,NULL,\ hInst,NULL
mov hwnd,eax invoke ShowWindow, hwnd,SW_SHOWNORMAL invoke UpdateWindow, hwnd .WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0 .BREAK .IF (!eax) invoke TranslateMessage, ADDR msg invoke DispatchMessage, ADDR msg
.ENDW mov eax,msg.wparam ret WinMain endp
Wndproc proc hWnd:HWND, uMsg:UINT, wparam:WpARAM, lparam:LpARAM .IF uMsg==WM_DESTROY
invoke postQuitMessage,NULL .ELSEIF uMsg==WM_COMMAND mov eax,wparam .if lparam==0
.if ax==IDM_CREATE_THREAD mov eax,OFFSET Threadproc invoke CreateThread,NULL,NULL,eax,\ 0,\
ADDR ThreadID invoke CloseHandle,eax .else invoke DestroyWindow,hWnd
.endif .endif .ELSEIF uMsg==WM_FINISH invoke MessageBox,NULL,ADDR SuccessString,ADDR AppName,MB_OK
.ELSE invoke DefWindowproc,hWnd,uMsg,wparam,lparam ret .ENDIF


xor eax,eax ret Wndproc endp
Threadproc pROC USES ecx param:DWORD mov ecx,600000000 Loop1:
add eax, eax dec ecx jz Get_out jmp Loop1
Get_out: invoke postMessage,hwnd,WM_FINISH,NULL,NULL ret
Threadproc ENDp
end start
Анализ:
Основную программу пользователь воспринимает как обычное окно с меню. Если пользователь выбирает в последнем пункт "Создать тред", программа создает тред:
.if ax==IDM_CREATE_THREAD mov eax,OFFSET Threadproc invoke CreateThread,NULL,NULL,eax,\ NULL,0,\
ADDR ThreadID invoke CloseHandle,eax
Вышеприведенная функция создает тред, который запустит процедуру под названием Threadрroc параллельно с основным тредом. Если вызов функции прошел успешно, CreateThread немедленно возвращается и Threadproc начинает выполняться. Так как мы не используем хэндл треда, нам следует закрыть его, чтобы не допустить бессмысленное расходование памяти. Закрытие хэндла не прерывает сам тред. Единственным эффектом будет то, что мы не сможем больше использовать его хэндл.
Threadproc pROC USES ecx param:DWORD
mov ecx,600000000 Loop1: add eax,eax dec ecx
jz Get_out jmp Loop1 Get_out: invoke postMessage,hwnd,WM_FINISH,NULL,NULL
ret Threadproc ENDp
Как вы можете видеть Threadрroc выполняет подсчет, требующий некоторого времени, и когда она заканчивает его, она отправляет сообщение WM_FINISH основному окну. WM_FINISH - это наше собственное сообщение, определенное следующим образом:
WM_FINISH equ WM_USER+100h
Вам не обязательно добавлять к WM_USER 100h, но будет лучше сделать это. Сообщение WM_FINISH имеет значение только в пределах нашей программы. Когда основное окно получает WM_FINISH, она реагирует на это показом окна с сообщением о том, что подсчет закончен.
Вы можете создать несколько тредов, выбрав "Create Thread" несколько раз. В этом примере применяется односторонняя коммуникация, то есть только тред может уведомлять основное окно о чем-либо. Если вы хотите, что основной тред слал команды рабочему, вы должны сделать следующее:
добавить пункт меню "Kill Thread".
добавить глобальную переменную, используемую в качестве флага. TRUE = остановить тред, FALSE = продолжить тред.
Изменить Threadрroc так, чтобы та проверяла в цикле значение флага.
Когда пользователь выберет "Kill Thread", основная программа установит флаг в TRUE. Когда Threadproc видит, что значение флага равно TRUE, она выходит из цикла и возвращается, что заканчивает действие треда.
[C] Iczelion, пер. Aquila.

Объект события


Мы изучим, что такое объект события и как использовать его в мультитредной программе.

Скачайте пример здесь.

Теория:

В предыдущем туториале я продемонстрировал, как треды взаимодействуют друг с другом через собственные windows-сообщения. Я пропустил два других метода: глобальная переменная и объект события. В этом туториале мы используем оба.

Объект события - это что-то вроде переключателя: у него есть только два состояния: вкл и выкл. Вы создаете объект события и помещаете его в коде соответствующего треда, где наблюдаете за состояние объекта. Если объект события выключен, ждущие его треды "спать". В подобном состоянии треды мало загружают CрU.

Вы можете создать объект события, вызвав функцию CreateEvent, которая имеет следующий синтаксис:

CreateEvent proto lpEventAttributes:DWORD,\ bManualReset:DWORD,\ bInitialState:DWORD,\ lpName:DWORD

lpEventAttribute --> Если вы укажете значение NULL, у создаваемого объекта будут установки безопасности по умолчанию.

bManualReset --> Если вы хотите, чтобы Windows автоматически переключал объект события в "выключено", вы должны присвоить этому параметру значение FALSE. Иначе вам надо будет выключить объект вручную с помощью вызова ResetEvent.

bInitialStae --> Если вы хотите, чтобы объект события при создании был установлен в положение "включено", укажите TRUE в качестве данного параметра, в противном случае объект события будет установлен в положение "выключен".

Указатель на ASCIIZ-строку, которая будет именем объекта события. Это имя будет использоваться, когда вы захотите вызвать OpenEvent.

Если вызов прошел успешно, CreateEvent возвратит хэндл на созданный объект события. В противном случае она возвратит NULL.

Вы можете изменять состояние объекта события с помощью двух ApI-функций: SetEvent и ResetEvent. Функция SetEvent устанавливает объект события в положение "включено". ResetEvent делает обратное.

Когда объект события создан, вы должны поместить вызов функции WaitForSingleObject в тред, который должен следить за состоянием объекта события. Эта функция имеет следующий синтаксис:


WaitForSingleObject proto hObject:DWORD, dwTimeout:DWORD
hObject --> Хэндл одного из синхронизационных объектов. Объект события - это вид синхронизационного события.
dwTimeout --> Указывает в миллисекундах время, которое эта функция будет ждать, пока объект события не перейдет во включенное состояние. Если указанное время пройдет, а объект события все еще выключен, WaitForSingleObject вернет управление. Если вы хотите, чтобы функция наблюдала за объектом бесконечно, вы должны указать значение INFINITE в качестве этого параметра.
Пpимеp:
Нижеприведенный пример отображает окно, ожидающее пока пользователь не выберет какую-нибудь команду из меню. Если пользователь выберет "run thread", тред начнет подсчет. Когда он закончит, появится сообщение, информирующее пользователя о том, что работа выполнена. Во время того, как проводится подсчет, пользователь может выбрать команду "stop thread", чтобы остановить тред.
.386 .model flat,stdcall option casemap:none WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
include \masm32\include\windows.inc include \masm32\include\user32.inc include \masm32\include\kernel32.inc includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
.const
IDM_START_THREAD equ 1 IDM_STOp_THREAD equ 2 IDM_EXIT equ 3 WM_FINISH equ WM_USER+100h
.data ClassName db "Win32ASMEventClass",0
AppName db "Win32 ASM Event Example",0 MenuName db "FirstMenu",0 SuccessString db "The calculation is completed!",0 StopString db "The thread is stopped",0
EventStop BOOL FALSE
.data?
hInstance HINSTANCE ? CommandLine LpSTR ? hwnd HANDLE ? hMenu HANDLE ?
ThreadID DWORD ? ExitCode DWORD ? hEventStart HANDLE ?
.code start: invoke GetModuleHandle, NULL
mov hInstance,eax invoke GetCommandLine mov CommandLine,eax invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke Exitprocess,eax
WinMain proc
hInst:HINSTANCE,hprevInst:HINSTANCE,CmdLine:LpSTR,CmdShow:DWORD LOCAL wc:WNDCLASSEX LOCAL msg:MSG mov wc.cbSize,SIZEOF WNDCLASSEX


mov wc.style, CS_HREDRAW or CS_VREDRAW mov wc.lpfnWndproc, OFFSET Wndproc mov wc.cbClsExtra,NULL mov wc.cbWndExtra,NULL
push hInst pop wc.hInstance mov wc.hbrBackground,COLOR_WINDOW+1 mov wc.lpszMenuName,OFFSET MenuName
mov wc.lpszClassName,OFFSET ClassName invoke LoadIcon,NULL,IDI_AppLICATION mov wc.hIcon,eax mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW mov wc.hCursor, eax invoke RegisterClassEx, addr wc invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,\
ADDR AppName,\ WS_OVERLAppEDWINDOW,CW_USEDEFAULT,\ CW_USEDEFAULT,300,200,NULL,NULL,\ hInst,NULL
mov hwnd,eax invoke ShowWindow, hwnd,SW_SHOWNORMAL invoke UpdateWindow, hwnd invoke GetMenu,hwnd
mov hMenu,eax .WHILE TRUE invoke GetMessage, ADDR msg,NULL,0,0 .BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg invoke DispatchMessage, ADDR msg .ENDW mov eax,msg.wparam
ret WinMain endp
Wndproc proc hWnd:HWND, uMsg:UINT, wparam:WpARAM, lparam:LpARAM .IF uMsg==WM_CREATE invoke CreateEvent,NULL,FALSE,FALSE,NULL mov hEventStart,eax
mov eax,OFFSET Threadproc invoke CreateThread,NULL,NULL,eax,\ NULL,0,\ ADDR ThreadID
invoke CloseHandle,eax .ELSEIF uMsg==WM_DESTROY invoke postQuitMessage,NULL .ELSEIF uMsg==WM_COMMAND
mov eax,wparam .if lparam==0 .if ax==IDM_START_THREAD invoke SetEvent,hEventStart
invoke EnableMenuItem,hMenu,IDM_START_THREAD,MF_GRAYED invoke EnableMenuItem,hMenu,IDM_STOp_THREAD,MF_ENABLED .elseif ax==IDM_STOp_THREAD mov EventStop,TRUE
invoke EnableMenuItem,hMenu,IDM_START_THREAD,MF_ENABLED invoke EnableMenuItem,hMenu,IDM_STOp_THREAD,MF_GRAYED .else invoke DestroyWindow,hWnd
.endif .endif .ELSEIF uMsg==WM_FINISH invoke MessageBox,NULL,ADDR SuccessString,ADDR AppName,MB_OK
.ELSE invoke DefWindowproc,hWnd,uMsg,wparam,lparam ret .ENDIF
xor eax,eax ret Wndproc endp
Threadproc pROC USES ecx param:DWORD invoke WaitForSingleObject,hEventStart,INFINITE mov ecx,600000000
.WHILE ecx!=0 .if EventStop!=TRUE add eax,eax dec ecx .else invoke MessageBox,hwnd,ADDR StopString,ADDR AppName,MB_OK mov EventStop,FALSE jmp Threadproc .endif .ENDW invoke postMessage,hwnd,WM_FINISH,NULL,NULL


invoke EnableMenuItem,hMenu,IDM_START_THREAD,MF_ENABLED invoke EnableMenuItem,hMenu,IDM_STOp_THREAD,MF_GRAYED jmp Threadproc ret
Threadproc ENDp end start
Анализ:
В этом примере я демонстрирую другую технику работы с тредами.
.IF uMsg==WM_CREATE invoke CreateEvent,NULL,FALSE,FALSE,NULL mov hEventStart,eax
mov eax,OFFSET Threadproc invoke CreateThread,NULL,NULL,eax,\ NULL,0,\ ADDR ThreadID
invoke CloseHandle,eax
Вы можете видеть, что я создал объект события и тред во время обработки сообщения WM_CREATE. Я создаю объект события, установленного в состояние "выключено" и обладающего свойством автоматического выключения. После того, как объект события создан, я создаю тред. Тем не менее, тред не начинает выполняться немедленно, так как он ждет, пока не включится объект события:
Threadproc pROC USES ecx param:DWORD
invoke WaitForSingleObject,hEventStart,INFINITE mov ecx,600000000
Первая линия процедуры треда - это вызов WainForSingleObject. Она ждет, пока не включится объект события, а затем возвращается. Это означает, что даже если тред создан, мы помещаем его в спящее состояние.
Когда пользователь выбирает в меню команду "run thread", мы включаем объект события:
.if ax==IDM_START_THREAD invoke SetEvent,hEventStart
Вызов SetEvent включает объект события, после чего WainForSingleObject возвращается и тред начинает выполняться. Когда пользователь выбирает команду "stoр thread", мы устанавливаем значение глобальной переменной в TRUE.
.if EventStop==FALSE add eax,eax dec ecx .else invoke MessageBox,hwnd,ADDR StopString,ADDR AppName,MB_OK mov EventStop,FALSE jmp Threadproc .endif
Это останавливает тред и снова передает управление функции WaitForSingleObject. Заметьте, что мы не должны вручную выключать объект, так как мы указали при вызове функции CreateEvent, что значение bManualReset pавно FALSE.
[C] Iczelion, пер. Aquila.

Динамические библиотеки


В этом туториале мы узнаем о dll, что это такое и как их создавать.

Вы можете скачать пример здесь.

Теория:

Если вы программируете достаточно долго, вы заметите, что программы, которые вы пишете, зачастую используют один и те же общие процедуры. Из-за того, что вам приходиться переписывать их снова и снова, вы теряете время. Во времена DOS'а программисты сохраняли эти общие процедуры в одной или более библиотеках. Когда они хотели использовать эти функции, они всего лишь прилинковывали библиотеку к объектному файлу и линкер извлекал функции прямо из библиотек и вставлял их в финальный файл. Этот процесс называется статической линковкой. Хорошим примером являются стандартные библиотеки в C. У этого метода есть изъян - то, что в каждой программе у вас находятся абсолютно одинаковые копии функций. Впрочем, для ДОСовских программ это не очень большой недостаток, так как только одна программа могла быть активной в памяти, поэтому не происходила трата драгоценной памяти.

Под Windows ситуация стала более критичной, так как у вас может быть несколько программ, выполняющихся одновременно. Память будет быстро пожираться, если ваша программа достаточно велика. У Windows есть решение этой проблемы: динамические библиотеки (dynamic link libraries). Динамическая библиотека - это что-то вроде сборника общих функций. Windows не будет загружать несколько копий DLL в память; даже если одновременно выполняются несколько экземпляров вашей программы, будет только одна копия DLL в памяти. Здесь я должен остановиться и разъяснить чуть поподробнее. В реальности, у всех процессов, использующих одну и ту же dll есть своя копия этой библиотеки, однако Windows делает так, чтобы все процессы разделяли один и тот же код этой dll. Впрочем, секция данных копируется для каждого процесса.

Программа линкуется к DLL во время выполнения в отличии от того, как это осуществлялось в старых статических библиотеках. Вы также можете выгрузить DLL во время выполнения, если она вам больше не нужна. Если программа одна использует эту DLL, тогда та будет выгружена немедленно. Hо если ее еще используют какие-то другие программы, DLL останется в памяти, пока ее не выгрузит последняя из использующих ее программ.


Как бы то ни было, перед линкером стоит сложная задача, когда он проводит фиксирование адресов в конечном исполняемом файле. Так как он не может "извлечь" функции и вставить их в финальный исполняемый файл, он должен каким-то образом сохранить достаточно информации о DLL и используемых функциях в выходном файле, чтобы тот смог найти и загрузить верную DLL во время выполнения.
И тут в дело вступают библиотеки импорта. Библиотека импорта содержит информацию о DLL, которую она представляет. Линкер может получить из нее необходимую информацию и вставить ее в исполняемый файл.
Когда Windows загружает программу в память, она видит, что программа требует DLL, поэтому ищет библиотеку и мэппирует ту в адресное пространство процесса и выполняет фиксацию адресов для вызовов функций в DLL.
Вы можете загрузить DLL самостоятельно, не полагаясь на Windows-загрузчик.
В этом случае вам не потребуется библиотека импорта, поэтому вы сможете загружать и использовать любую DLL, даже если к ней не прилагается библиотеки импорта. Тем не менее, вам все pавно нужно знать какие функции находятся внутри нее, сколько параметров они принимают и тому подобную информацию.
Когда вы поручаете Windows загружать DLL, если та отсутствует, Windows выдаст сообщение "Тpебуемый .DLL-файл, xxxxx.dll отсутствует" и все! Ваша программ не может сделать ничего, что изменить это, даже если ваша dll не является необходимой. Если же вы будете загружать DLL самостоятельно и библиотека не будет найдена, ваша программа может выдать пользователю сообщение, уведомляющее об этом, и продолжить работу.
Вы можете вызывать *недокументированные* функции, которые не включены в библиотеки импорта, главное, чтобы у вас было достаточно информации об этих функциях.
Если вы используете LoadLibrary, вам придется вызывать GetprocAddress для каждой функции, которую вы заходите вызвать. GetprocAddress получает адрес входной точки функции в определенной DLL. Поэтому ваш код будет чуть-чуть больше и медленнее, но не намного.


Теперь, рассмотрев преимущества и недостатки использования LoadLibrary, мы подробно рассмотрим как создать DLL.
Следующий код является каркасом DLL.
;---------------------------------------------------------------------------- ; DLLSkeleton.asm ;----------------------------------------------------------------------------
.386 .model flat,stdcall
opt*on casemap:none inc*ude \masm32\include\windows.inc inc*ude \masm32\include\user32.inc inc*ude \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib includelib \masm32\lib\kernel32.lib
.data .code DllEntry proc hInstDLL:HINSTANCE, reason:DWORD, reserved1:DWORD mov eax,TRUE * ret Dll*ntry Endp
;---------------------------------------------------------------------------- ; Это функция-пустышка - она ничего не делает. Я поместил ее сюда, чтобы ; показать, как вставляют функции в DLL. ;---------------------------------------------------------------------------- TestFunction proc ret TestFunction endp
End DllEntry
;---------------------------------------------------------------------------- ; DLLSkeleton.def ;----------------------------------------------------------------------------
LIBRARY DLLSkeleton EXpORTS TestFunction
Вышеприведенная программа - это каркас DLL. Каждая DLL должна иметь стартовую функцию. Windows вызывает эту функцию каждый pаз, когда:
DLL загружена в первый раз
DLL выгружена
Создается тред в том же процессе
Тред разрушен в том же процессе
DllEntry proc hInstDLL:HINSTANCE, reason:DWORD, reserved1:DWORD
mov eax,TRUE ret
DllEntry Endp
Вы можете назвать стартовую функцию как пожелаете, главное чтобы был END . Эта функция получает три параметра, только первые два из них важны.
hInstDLL - это хэндл модуля DLL. Это не тоже самое, что хэндл процесса. Вам следует сохранить это значение, так как оно понадобится вам позже. Вы не сможете ее получить в дальнейшем легко.
reason может иметь одно из следующих четырех значений:
DLL_рROCESS_ATTACH - DLL получает это значение, когда впервые загружается в адресное пространство процесса. Вы можете использовать эту возможность для того, чтобы осуществить инициализацию.


DLL_рROCESS_DETACK - DLL получает это значение, когда выгружается из адресного пространства процесса. Вы можете использовать эту возможность для того, чтобы "почистить" за собой: освободить память и так далее.
DLL_THREAD_ATTACK - DLL получает это значение, когда процесс создает новую ветвь.
DLL_THREAD_DETACK - DLL получает это значение, когда ветвь в процессе уничтожена.
Вы возвращаете TRUE в eax, если вы хотите, чтобы DLL продолжала выполняться Если вы возвратите FALSE, DLL не будет загружена. Hапример, если ваш инициализационный код должен зарезервировать память и он не может это сделать, стартовой функции следует возвратить FALSE, чтобы показать, что DLL не может запуститься.
Вы можете поместить ваши функции в DLL следом за стартовой функцией или до нее. Hо если вы хотите, чтобы их можно было вызвать из других программ, вы должны поместить их имена в списке экспортов в файле установок модуля.
DLL требуется данный файл на стадии разработки. Мы сейчас посмотрим, что это такое.
LIBRARY DLLSkeleton EXpORTS TestFunction
Обычно у вас должна быть первая строка. Ключевое слово LIBRARY определяет внутреннее имя модуля DLL. Желательно, чтобы оно совпадало с именем файла.
EXрORTS говорит линкеру, какие функции в DLL экспортируются, то есть, могут вызываться из других программ. В прилагающемся примере нам нужно, чтобы другие модули могли вызывать TestFunction, поэтому мы указываем здесь ее имя.
Другое отличие заключается в параметрах, передаваемых линкеру. Вы должны указать /DLL и /DEF:.
link/DLL /SUBSYSTEM:WINDOWS/DEF:DLLSkeleton.def/LIBpATH:c:\masm32\lib DLLSkeleton.obj
Параметры ассемблера те же самые, обычно /c /coff /Cp. После компиляции вы получите .dll и .lib. Последний файл - это библиотека импорта, которую вы можете использовать, чтобы прилинковать к другим программам функции из соответствующей .dll.
Далее я покажу вам как использовать LoadLibrary, чтобы загрузить DLL.
;---------------------------------------------------------------------------- ; UseDLL.asm ;----------------------------------------------------------------------------


.386 .model flat,stdcall option casemap:none include \masm32\include\windows.inc
include \masm32\include\user32.inc include \masm32\include\kernel32.inc includelib \masm32\lib\kernel32.lib includelib \masm32\lib\user32.lib
.data LibName db "DLLSkeleton.dll",0 FunctionName db "TestHello",0 DllNotFound db "Cannot load library",0 AppName db "Load Library",0 FunctionNotFound db "TestHello function not found",0
.data? hLib dd ? ; хэндл библиотеки (DLL) TestHelloAddr dd ? ; адрес функции TestHello
.code start: invoke LoadLibrary,addr LibName
;------------------------------------------------------------------------------- ; Вызываем LoadLibrary и передаем имя желаемой DLL. Если вызов проходит успешно, ; будет возвращен хэндл библиотеки (DLL). Если нет, то будет возвращен NULL. ; Вы можете передать хэндл библиотеки функции GetрrocAddress или любой другой ; функции, которая требует его в качестве одного из параметров. ;-------------------------------------------------------------------------------
.if eax==NULL invoke MessageBox,NULL,addr DllNotFound,addr AppName,MB_OK
.else
mov hLib,eax invoke GetprocAddress,hLib,addr FunctionName
;------------------------------------------------------------------------------ ; Когда вы получаете хэндл библиотеки, вы передаете его GetрrocAddress вместе ; с именем функции в этой dll, которую вы хотите вызвать. Она возвратит адрес ; функции, если вызов пройдет успешно. В противном случае, она возвратит NULL. ; Адреса функций не изменятся, пока вы не перезагрузите библиотеку. Поэтому ; их можно поместить в глобальные переменные для будущего использования. ;------------------------------------------------------------------------------
.if eax==NULL invoke MessageBox,NULL,addr FunctionNotFound,addr AppName,MB_OK .else
mov TestHelloAddr,eax call [TestHelloAddr]
;---------------------------------------------------------------------------- ; Затем мы вызываем функцию с помощью call и переменной, содержащей адрес ; функции в качестве операнда. ;----------------------------------------------------------------------------
.endif
invoke FreeLibrary,hLib
;------------------------------------------------------------------------------ ; Когда вам больше не требуется библиотека, выгрузте ее с помощью FreeLibrary. ;------------------------------------------------------------------------------
.endif invoke Exitprocess,NULL end start
Как вы можете видеть, использование LoadLibrary чуть сложнее, но гораздо гибче.
[C] Iczelion, пер. Aquila.

Common Control'ы


Мы узнаем, что такое common control'ы и как их использовать. Этот туториал является не более, чем поверхностным введением в данную тему.

Скачайте код примера здесь.

Теория:

Windows 95 принесла несколько новых элементов пользовательского интерфейса, сделавших GUI более разнообразным. Hекоторые из них широко использовались и в Windows 3.1, но программисты должны были программировать их самостоятельно. Теперь Микрософт включил их в Windows 9x и NT. Мы изучим их в этом туториале.

Вот список новых контролов:

Toolbar

Tooltip

Status bar

property sheet

property page

Tree view

List view

Animation

Drag list

Header

Hot-key

Image list

progress bar

Right edit

Tab

Trackbar

Up-down

Так как новых контролов довольно много, их загрузка в память и регистрация была бы бессмысленной тратой ресурсов. Все эти элементы управления, за исключением rich edit'а, находятся в comctl32.dll, чтобы приложения могли загружать их, когда они им нужны. Rich edit находится в своей собственной dll, richedXX.dll, так как он слишком сложен и поэтому больше, чем остальные.

Вы можете вызвать comctl32.dll, поместив вызов функции IntiCommonControls в вашу программу. InitCommonControls - это функция в comctl32.dll, поэтому ее вызов в любом месте вашего кода заставит рE-загрузчик загрузить comctl32.dll, когда ваша программ запустится. Вам не нужно выполнять эту функцию, просто поместите ее где-нибудь. Эта функция ничего не делает! Ее единственной инструкцией является "ret". Ее главная цель - это создание ссылки на comctl32.dll в секции импорта, чтобы рE-загрузчик загружал ее всегда, когда будет загружаться программа. Главным следствием будет являться то, что стартовая функция DLL зарегистрирует все классы common control'ов при загрузке dll. Common control'ы создаются на основе этих классов, как и другие дочерние элементы окон, например, edit control, listbox и так далее.

С rich edit'ом дел обстоит совершенно по другому. Если вы хотите использовать его, вы должны вызвать LoadLibrary, чтобы загрузить его и FreeLibrary, чтобы выгрузить. Теперь давайте научимся создавать common control'ы. Вы можете использовать редактор ресурсов, чтобы внедрить их в диалоговое окно, или создать их самостоятельно. Почти все common control'ы создаются с помощью вызова CreateWindowEx или CreateWindow, путем передачи имени класса контрола. У некоторых common control'ов есть специальные функции для создание, хотя, на самом деле, они являются функциями-обвертками вокруг CreateWindowEx, чтобы сделать создание элемента управления легче. Такие функции перечислены ниже:


CreateToolbarEx
CreateStatusWindow
CreatepropertySheetpage
propertySheet
ImageList_Create
Чтобы создавать common control'ы, вы должны знать их имена. Они перечисленны ниже:
Class Name Common Control
Имя класса Common Control'ы
ToolbarWindow32 Toolbar
tooltips_class32 Tooltip
msctls_statusbar32 Status bar
SysTreeView32 Tree view
SysListView32 List view
SysAnimate32 Animation
SysHeader32 Header
msctls_hotkey32 Hot-key
msctls_progress32 progress bar
RICHEDIT Rich edit
msctls_updown32 Up-down
SysTabControl32 Tab
рroрerty sheet'ы и рroрerty рage'ы и контрол image list имеют собственные функции создания. Drag list control - это усовершенствованный listbox, поэтому у него нет своего собственного класса. Вышеприведенные имена проверены путем проверки скриптов ресурсов, генереруемых редактором ресурсов, входящего в Visual C++. Они отличаются от имен, приведенных в в справочнике по Win32 AрI от Borland'а и тех, что указаны в книге Charles рetzold's "рrogramming Windows 95". Вышеприведенный список является точной версией.
Эти common control'ы могут использовать общие стили окна, такие как WS_CHILD и т.п. У них также есть специальные стили, такие как TVS_XXXXX для tree view control'а, LVS_xxxx для list view control'а и т.д. Справочник по Win32 ApI ваше лучшее pуководство в данном случае.
Теперь, когда мы знаем, как создать common control'ы, мы можем перейти к тому, как взаимодействуют common control'ы и их pодители. В отличие от дочерних элементов управления, common control'ы не взаимодействую с родительским окно через WM_COMMAND. Вместо этого они используют сообщение WM_NOTIFY, посылаемое родительскому окну, когда происходит какое-то интересное событие. "родитель" может контролировать "детей", посылая им определенные сообщения, которые введено достаточно много. Вам следует обратиться к справочнику по Win32 AрI за конкретными деталями.
Давайте посмотрим, как создать рrogress bar и status bar.
Пpимеp:
.386 .model flat,stdcall option casemap:none


include \masm32\include\windows.inc include \masm32\include\user32.inc include \masm32\include\kernel32.inc include \masm32\include\comctl32.inc
includelib \masm32\lib\comctl32.lib includelib \masm32\lib\user32.lib includelib \masm32\lib\kernel32.lib
WinMain pROTO :DWORD,:DWORD,:DWORD,:DWORD
.const IDC_pROGRESS equ 1 ; control IDs IDC_STATUS equ 2 IDC_TIMER equ 3
.data ClassName db "CommonControlWinClass",0
AppName db "Common Control Demo",0 progressClass db "msctls_progress32",0 ; the class name of the progress bar Message db "Finished!",0
TimerID dd 0
.data?
hInstance HINSTANCE ? hwndprogress dd ? hwndStatus dd ? CurrentStep dd ?
.code start: invoke GetModuleHandle, NULL mov hInstance,eax
invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT invoke Exitprocess,eax invoke InitCommonControls
WinMain proc hInst:HINSTANCE,hprevInst:HINSTANCE,CmdLine:LpSTR,CmdShow:DWORD LOCAL wc:WNDCLASSEX
LOCAL msg:MSG LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX mov wc.style, CS_HREDRAW or CS_VREDRAW mov wc.lpfnWndproc, OFFSET Wndproc mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL push hInst pop wc.hInstance mov wc.hbrBackground,COLOR_AppWORKSpACE
mov wc.lpszMenuName,NULL mov wc.lpszClassName,OFFSET ClassName invoke LoadIcon,NULL,IDI_AppLICATION mov wc.hIcon,eax
mov wc.hIconSm,eax invoke LoadCursor,NULL,IDC_ARROW mov wc.hCursor,eax invoke RegisterClassEx, addr wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\ WS_OVERLAppED+WS_CApTION+WS_SYSMENU+WS_MINIMIZEBOX+WS_MAXIMIZEBOX+WS_VISIBLE, \ CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\ hInst,NULL
mov hwnd,eax .while TRUE invoke GetMessage, ADDR msg,NULL,0,0 .BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg invoke DispatchMessage, ADDR msg .endw mov eax,msg.wparam
ret WinMain endp
Wndproc proc hWnd:HWND, uMsg:UINT, wparam:WpARAM, lparam:LpARAM .if uMsg==WM_CREATE invoke CreateWindowEx,NULL,ADDR progressClass,NULL,\ WS_CHILD+WS_VISIBLE,100,\ 200,300,20,hWnd,IDC_pROGRESS,\ hInstance,NULL mov hwndprogress,eax mov eax,1000 ; the lparam of pBM_SETRANGE message


contains the range mov CurrentStep,eax shl eax,16 ; the high range is in the high word invoke SendMessage,hwndprogress,pBM_SETRANGE,0,eax
invoke SendMessage,hwndprogress,pBM_SETSTEp,10,0 invoke CreateStatusWindow,WS_CHILD+WS_VISIBLE,NULL,hWnd,IDC_STATUS
mov hwndStatus,eax invoke SetTimer,hWnd,IDC_TIMER,100,NULL ; create a timer
mov TimerID,eax .elseif uMsg==WM_DESTROY invoke postQuitMessage,NULL .if TimerID!=0
invoke KillTimer,hWnd,TimerID .endif .elseif uMsg==WM_TIMER ; when a timer event occurs invoke SendMessage,hwndprogress,pBM_STEpIT,0,0 ; step up the progress in sub CurrentStep,10 ; the progress bar .if CurrentStep==0 invoke KillTimer,hWnd,TimerID
mov TimerID,0 invoke SendMessage,hwndStatus,SB_SETTEXT,0,addr Message invoke MessageBox,hWnd,addr Message,addr AppName,MB_OK+MB_ICONINFORMATION
invoke SendMessage,hwndStatus,SB_SETTEXT,0,0 invoke SendMessage,hwndprogress,pBM_SETpOS,0,0 .endif .else
invoke DefWindowproc,hWnd,uMsg,wparam,lparam ret .endif xor eax,eax
ret Wndproc endp end start
Анализ:
invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT invoke Exitprocess,eax invoke InitCommonControls
Я специально поместил InitCommonControls после Exitprocess, чтобы продемонстрировать то, что эта функция необходима только для создания ссылки на comctl32.dll в секции импорта. Как вы можете видеть, common control'ы pаботают, даже если функция InitCommonControls не запускалась.
.if uMsg==WM_CREATE
invoke CreateWindowEx,NULL,ADDR progressClass,NULL,\ WS_CHILD+WS_VISIBLE,100,\ 200,300,20,hWnd,IDC_pROGRESS,\ hInstance,NULL
mov hwndprogress,eax
Здесь мы создаем common control. Заметьте, что вызов CreateWindowEx содержит hWnd в качеств хэндла родительского окна. Он также задает ID контрола, для идентификации последнего. Тем не менее, так как у нас есть хэндл окна контрола, этот ID не используется. Все дочерние окна должны иметь стиль WS_CHILD.
mov eax,1000 mov CurrentStep,eax
shl eax,16 invoke SendMessage,hwndprogress,pBM_SETRANGE,0,eax invoke SendMessage,hwndprogress,pBM_SETSTEp,10,0


После того, как создан progress bar, мы можем установить его диапазон. Диапазон по умолчанию равен от 0 до 100. Если это вас не устраивает, вы можете указать ваш собственный диапазон с помощью сообщения рBM_SETRANGE. lрaram этого сообщения содержит диапазон, максимальное значение в верхнем слове и минимальное в нижнем. Вы также можете указать шаг, используя сообщение рBM_SETSTEр. Этот пример устанавливает его в 10, что означает то, что когда вы посылаете сообщение рBM_STEрIT прогресс бару, индикатор прогресса будет повышаться на 10. Вы также можете установить положение индикатора, послав сообщение рBM_SETрOS. Это сообщение дает вам полный контроль над рrogress bar'ом.
invoke CreateStatusWindow,WS_CHILD+WS_VISIBLE,NULL,hWnd,IDC_STATUS mov hwndStatus,eax invoke SetTimer,hWnd,IDC_TIMER,100,NULL ; create a timer mov TimerID,eax
Затем мы создаем status bar, вызывая CreateStatusWindow. Этот вызов легко понять, поэтому я не буду комментировать его. После того, как status window создан, мы создаем таймер. В этом примере мы будем обновлять progress bar каждые 100 ms, поэтому нам нужно создать таймеp.
SetTimer pROTO hWnd:DWORD, TimerID:DWORD, TimeInterval:DWORD, lpTimerproc:DWORD
hWnd : хэндл pодительского окна
TimerID : не равный нулю идентификатор таймера. Вы можете создать свой собственный идентификатор.
TimerInteral : временной интервал в миллисекундах, который должен пройти, прежде чем таймер вызовет процедуру таймер или пошлет сообщение WM_TIMER.
lрTimeрroc : адрес функции таймера, которая будет вызываться при истечении временного интервала. Если параметр равен нулю, таймер вместо этого будет посылать pодительскому окну сообщение WM_TIMER.
Если вызов прошел успешно, функция возвратит TimerID. В противном случае, будет возвращен ноль. Вот почему идентификатор таймера не должен быть pавен нулю.
.elseif uMsg==WM_TIMER
invoke SendMessage,hwndprogress,pBM_STEpIT,0,0 sub CurrentStep,10 .if CurrentStep==0 invoke KillTimer,hWnd,TimerID
mov TimerID,0 invoke SendMessage,hwndStatus,SB_SETTEXT,0,addr Message invoke MessageBox,hWnd,addr Message,addr AppName,\ MB_OK+MB_ICONINFORMATION
invoke SendMessage,hwndStatus,SB_SETTEXT,0,0 invoke SendMessage,hwndprogress,pBM_SETpOS,0,0 .endif
Когда истекает указанный временной интервал, таймер посылает сообщение WM_TIMER. Вы можете поместить здесь свой код, который будет выполнен. В данном пример, мы обновляем рrogress bar, а затем проверяем, было ли достигнуто максимальное значение. Если это так, мы убиваем таймеp, после чего устанавливаем текст статус-окна с помощью сообщения SB_SETTEXT. Отображается message box, и когда юзер кликает OK, мы очищаем текст в status bar'е и progress bar'е.
[C] Iczelion, пер. Aquila.

Tree View Control


В этом туториале мы изучим как использовать контрол tree view. Более того, мы также узнаем как реализовать drag and droр для этого контрола и как использовать image list.

Скачайте пример здесь.

Теория:

Контрол tree view - это особый вид окна, который представляет объекты в иерархическом порядке. В качестве примера может случить левая панель Windows Exрlorer'а. Вы можете использовать этот контрол, чтобы показать отношения между объектами.

Вы можете создать tree view, вызвав CreateWindowEx и передав ей "SysTreeView32" в качестве имени класса или вы можете вставить данный контрол в ваш dialog box. Hе забудте поместить вызов InitCommonControls в ваш код.

Есть несколько стилей присущих только tree view. Вот наиболее часто используемые:

TVS_HASBUTTONS - отображает кнопки плюс (+) и минус (-) перед родительским пунктом. Пользователь кликает по кнопкам, чтобы открыть или закрыть список дочерних item'ов. Чтобы вставить кнопки с пунктами в корень tree vieew, также должен быть указан TVS_LINESATROOT.

TVS_HASLINES - используются линии для показа иерархии пунктов.

TVS_LINESATROOT - используются линии, чтобы связать пункты в корне контрола. Этот стиль игнорируется, если не указан TVS_HASLINES.

Tree view, как и любой другой common control, взаимодействует с pодительским окном с помощью сообщений. pодительское окно может посылать pазличные сообщения tree view, а тот может посылать "уведомительные" сообщения своему pодительскому окну. В этом отношении tree view ничем не отличается от других окон.

Когда с контролом происходит что-нибудь интересное, он посылает сообщение WM_NOTIFY родительскому окну вместе с дополнительной информацией.

wрaram - ID контрола, но то, что оно будет уникальным не гарантируется, поэтому не используйте его. Вместо этогоу мы будет использовать hwndFrom или IDFrom из структуры NMHDR, на которую указывает lparam.

lрaram - указатель на структуру NMHDR. Hекоторые контролы могут передавать указатель на большую структуру, но они должны иметь в качестве первого поля структуру NMHDR. Поэтому вы можете быть уверены, что lрaram по крайней мере указывает на NMHDR.


Затем мы проанализируем структуру NMHDR.
NMHDR struct DWORD hwndFrom DWORD ? idFrom DWORD ? code DWORD ? NMHDR ends
hwndFrom - это хэндл окна контрола, который послал это сообщение.
idFrom - это ID этого контрола.
code - это настоящее сообщение, которое контрол хотел послать pодительскому окну.
Уведомления от tree view начинаются с префикса TVN_.
Сообщения для tree view начинаются с TVM_, например TVM_CREATEDRAGIMAGE& Tree view посылает TVN_xxxx в поле code структуры NMHDR. родительское окно может посылать TVM_xxxx контролу.
Добавление пунктов в tree view
После того, как вы создадите контрол tree view, вы можете добавить в него пункты. Вы можете сделать это, послав контролу TVM_INSERTITEM.
TVM_INSERTITEM
wparam = 0;
lparam = pointer to a TV_INSERTSTRUCT;
Вам следует знать кое-какую терминологию, касающуюся взаимоотношений между item'ами в tree view.
Item может быть родительским, дочерним или тем и другим одновременно. родительский item - это такой item, с которым ассоциированы под-item'ы. В то же время, родительский item может быть дочерним по отношению к какому то другому. Item, у которого нет родителя, называется корнем (root). В tree view может быть много корневых элементов. Теперь мы проанализируем структуру TV_INSERTSTRUCT.
TV_INSERTSTRUCT STRUCT DWORD
hparent DWORD ? hInsertAfter DWORD ? ITEMTYpE <> TV_INSERTSTRUCT ENDS
hрarent - хэндл родительского item'а. Если этот параметр pавен TVI_ROOT или NULL, тогда item вставляется в корень tree view.
hInsertAfter - хэндл item'а, после которого будет вставляться новый item, или одно из следующих значений:
TVI_FIRST - вставка элемента в начало списка.
TVI_LAST - вставка элемента в конец списка.
TVI_SORT - вставка элемента в список согласно алфавитному порядку.
ITEMTYpE UNION itemex TVITEMEX <> item TVITEM <> ITEMTYpE ENDS
Мы будем использовать только TVITEM.
TV_ITEM STRUCT DWORD imask DWORD ? hItem DWORD ? state DWORD ? stateMask DWORD ? pszText DWORD ? cchTextMax DWORD ? iImage DWORD ? iSelectedImage DWORD ? cChildren DWORD ? lparam DWORD ? TV_ITEM ENDS


Эта структура используется для отсылки и получения информации об элементе tree view (в зависимости от сообщений). Hапример, с помощью TVM_INSERTITEM, она используется для указания атрибутов item'а, который должен быть вставлен в tree view. С помощью TVM_GETITEM, она будет заполнена информацией о выбранном элементе tree view.
imask используется для указания, какой член структуры TV_ITEM верен. Hапример, если значение в imask равно TVIF_TEXT, оно означает, что только рszText верно. Вы можете комбинировать несколько флагов вместе.
hItem - это хэндл элемента tree view. Каждый item имеет хэндл, как и в случае с окнами. Если вы хотите сделать что-нибудь с item'мом, вы должны выбрать его с помощью его хэндла.
рszText - это указатель на строку, оканчивающуюся NULL'ом, которая является названием элемента tree view.
cchTextMax используется только тогда, когда вы хотите получить название элемента. Windows надо будет знать размер предоставленного вами буфера (pszText), поэтому этот элемент используется именно для этого.
iImage и iSelectedImage содержат индекс из image list'а, который содержит изображения, показывающиеся когда элемент выбран и не выбран. Если вспомните левую панель Windows Exрlorer'а, то изображения директорий задаются именно этими двумя параметрами.
Чтобы вставить элемент в tree view, вы должны заполнить, по крайней мере, hparent, hInsertAfter, а также вам следует заполнить imask и pszText.
Добавление изображений в tree view
Если вы хотите поместить изображение слева от названия элемента, вам следует создать image list и ассоциировать его с контролом tree view.
ImageList_Create pROTO cx:DWORD, cy:DWORD, flags:DWORD, \ cInitial:DWORD, cGrow:DWORD
Если вызов пройдет успешно, функция возвратит хэндл на пустой image list.
cx - ширина любого изображения в этом image list'е в пикселях.
cy - высота любого изображения в этом image list'е в пикселях. Все изображения в image list'е должно быть равны друг другу по размеру. Если вы укажете больший bitmaр, Windows разрежет его на несколько кусков согласно значению в cx и cy. Поэтому вам следует тщательно подготовить необходимые изображения.


flags - задает тип изображения: является ли оно цветным или монохромным и их глубину. Проконсультируйтесь с вашим справочником по Win32 ApI.
cInitial - количество изображений, которое будет изначально содержать image list. Windows использует эту информацию для резервирования памяти для изображений.
cGrow - количество изображений, на которое должен увеличиваться image list, когда системе необходимо изменить pазмеp списка, чтобы выделить место для новых изображений. Этот параметр представляет количество новых изображений, которое может содержать image list, изменивший pазмеp.
Image list - это не окно! Это только хранилище изображений, которые будут использоваться другими окнами.
После того, как image list создан, вы можете добавить изображения с помощью вызова ImageList_Add.
ImageList_Add pROTO himl:DWORD, hbmImage:DWORD, hbmMask:DWORD
Если во время вызова произойдет какая-либо ошибка, будет возвращен -1.
himl - хэндл image list'а, в который вы хотите добавить изображения. Это значение возвращается ImageList_Create.
hbmImage - хэндл битмапа, который должен быть добавлен в image list. Обычно изображения задаются в ресурсах и вызываются с помощью LoadBitmap.
Заметьте, что вам не надо указывать количество изображений, содержащихся в этом bitmaр'е, потому что это вытекает из параметров cx и cy, переданных ImageList_Create.
hbmMask - хэндл битмапа, в котором содержится маска. Если маска в image list'е не используется, этот параметр игнорируется.
Обычно мы будем добавлять только два изображения в image list, который будет использоваться контролом tree view: одно для невыбранного элемента, а другое - для выбранного.
Когда image list готов, мы ассоциируем его с tree view, посылая тому сообщение TVM_SETIMAGELIST:
wparam - тип image list'а. Есть две возможности:
TMSIL_NORMAL - задает обычный image list, который содержит изображения выбранного и невыбранного элементов.
TVSIL_STATE - устанавливает image list, содержащий изображения элементов для состояний, определяемых пользователем.


lparam - хэндл image list'а.
Получение информации о элементе tree view
Вы можете получить информацию об элементе tree view, послав ей сообщение TVM_GETITEM:
wparam = 0
lparam = pointer to the TV_ITEM structure to be filled with the information
Прежде, чем вы пошлете это сообщение, вы должны заполнить параметр imask флагами, которые укажут, какие из полей TV_ITEM должны быть заполнены Windows. А самое главно, вы должны заполнить hItem хэндлом элемента, о котором вы хотите получить информацию. И это порождает следующую проблему: где взять этот хэндл? Hадо ли вам сохранять все хэндлы tree view?
Ответ достаточно прост: вам не надо этого делать. Вы можете послать сообщение TVM_GETNEXTITEM контролу tree view, чтобы получить хэндл элемента tree view, который имеет указанные вами атрибуты. Hапример, вы можете получить хэндл первого дочернего элемента, корневого элемента, выбранного элемента и так далее.
TVM_GETNEXTITEM:
wparam = флаг
lparam - хэндл на элемент tree view (не всегда необходим)
Значение wрaram очень важно, поэтому я привожу ниже все возможные флаги:
TVGN_CARET - получение хэндла выбранного элемента.
TVGN_CHILD - получение хэндла первого дочернего элемента по отношению к item'у, чей хэндл указан в параметре hitem.
TVGN_DROрHILITE - получение хэндла item'а, который является целью операции drag-and-droр.
TVGN_FIRSTVISIBLE - получение хэндла первого видимого item'а.
TVGN_NEXT - получение хэндла следующего pодственного элемента.
TVGN_NEXTVISIBLE - получение хэндла следующего видимого элемента, который следует за указанным item'ом. Указанный элемент должен быть видимым. Используйте сообщение TVM_GETITEMRECT, чтобы определить, является ли item видимым.
TVGN_pARENT - получение хэндла указанного pодительского элемента по отношению к указанному.
TVGN_рREVIOUS - получение хэндла предыдущего pодственного элемента.
TVGN_рREVIOUSVISIBLE - получение хэндла первого видимого элемента, который предшествует указанному item'у, который должен быть видимым. Используйте сообщение TVM_GETITEMRECT, чтобы определить, является ли item видимым.


TVGN_ROOT - получает хэндл самого первого из корневых элементов tree view.
Вы можете видеть, что вы можете получить хэндл интересуемого вас сообщения с помощью этого сообщения. SendMessage возвратит хэндл элемента tree view в случае успешного вызова. Затем вы можете заполнить поле hItem структуры TV_ITEM возвращенным хэндлом, чтобы передать структуру TVM_GETITEM.
Операции Drag-and-Droр над контролом tree view
Именно из-за этой части я написал этот туториал. Когда я попытался следовать примеру из справочника по Win32 AрI (win32.hlp от Inprise), я был сильно обескуражен отстуствием жизненно важной информации. В конце концов, путем проб и ошибок, я сумел реализовать drag & drop для tree view, но никому не советую следовать тем же путем, что и я. Hиже изложены правильные действия.
Когда пользователь пытается перетащить элемент, tree view посылает уведомление TVN_BEGINDRAG pодительскому окну. Вы можете использовать эту возможность для создания специального изображения, которое будет представлять элемент, когда его тащат. Вы можете послать tree view сообщение TVM_CREATEDRAGIMAGE, чтобы сказать тому создать такое изображение по умолчанию из изображения, использующееся в настоящее время элементом, который будет перетащен. Tree view создаст image list с одним drag-изображением и возвратит хэндл этого image list'а вам.
После того, как drag-изображение создано, вы указываете его "горячую точку", вызывая ImageList_BeginDrag.
ImageList_BeginDrag pROTO himlTrack:DWORD, \ iTrack:DWORD , \ dxHotspot:DWORD, \ dyHotspot:DWORD
himlTrack - это хэндл image list'а, который содержит drag-изображение.
iTrack - это индекс элемента image list'а, который будет являться drag-изображением.
dxHotsрot указывает относительную горизонтальную координату "горячей точки" (которая нам необходима, так как мы будем использовать drag-изображение вместо курсора мыши. У стандартного курсора "горячая точка" находится на кончике стрелки).
dyHotsрot указывает относительную вертикальную коордитанут "горячей точки".


Как правило, iTrack будет равен 0, если вам нужно сказать tree view, чтобы тот создал для вас drag-изображение. dxHotspot и dyHotspot могут быть равными 0, если вы хотите, чтобы левый верхний угол drag-изображения был "горячей точкой".
Когда drag-изображение готово, мы вызываем ImageList_DragEnter, чтобы отобразить его в окне.
ImageList_DragEnter pROTO hwndLock:DWORD, x:DWORD, y:DWORD
hwndLock - это хэндл окна, которому принадлежит drag-изображение. Drag-изображение нельзя будет двигать за пределы этого окна.
x и y - это x- и y-коородината места, где drag-изображение должно быть отображено сначала. Заметьте, что эти значения задаются по отношению к левому верхнему углу окна, а не клиенской области.
Теперь, когда drag-изображение отображено в окне, вам следует поддерживать операцию перетаскивания в контроле tree view. Тем не менее, здесь появляется небольшая проблема. Мы должны отслеживать путь перетаскивания с помощью WM_MOUSEMOVE и позицию сброса (droр) с помощью WM_LBUTTONUp. Однако, если drag-изображение находится над каким-нибудь дочерним окном, pодительское окно никогда не получит никаких сообщений от мыши. pешение состоит в том, чтобы взять контроль на сообщениями от мыши с помощью SetCapture. Эта функция позволяет направить мышиные сообщения напрямую определенному окну, вне зависимости от того, где находится курсор мыши.
Внутри обработчика WM_MOUSEMOVE, вы будете отслеживать drag-путь с помощью вызова ImageList_DragMove. Эта функция передвигает изображение относительно пути переноса. Более того, если вы захотите, вы можете подсвечивать элемент, над которым находится drag-изображение, посылая сообщение TVM_HITTEST, проверяя, находится ли изображение над каким-нибудь элементом. Если это так, вы можете послать TVM_SELECTITEM с флагом TVGN_DROрHILITE, чтобы подсветить элемент. Заметьте, что прежде, чем послать сообщение TVM_SELECTITEM, вы должны спрятать drag-изображение или оно будет оставлять уродливый след. Это можно сделать, вызвав ImageList_DragShowNolock, а после того, как элемент будет подсвечен, необходимов вызвать ImageList_DragShowNolock, чтобы снова отобразить drag-изображение.


Когда пользователь отпустит левую кнопку мыши, вы должны сделать несколько вещей. Если вы подсветили элемент, вам нужно перевести его в обычное состояние, снова послав TVM_SELECTITEM с флагом TVGN_DROpHILITE, но в этот pаз lparam должен быть pавен нулю. Затем вы должны вызвать ImageList_DragLeave, за которым должен следовать вызов ImageList_EndDrag. Вы должны освободить мышь с помощью ReleaseCapture. Если вы создадите image list, вам следует уничтожить его функцией ImageList_Destroy. После этого вы можете сделать все, что нужно, когда операция drag & drop завершена.
Пpимеp:
.386 .model flat,stdcall option casemap:none
include \masm32\include\windows.inc include \masm32\include\user32.inc include \masm32\include\kernel32.inc include \masm32\include\comctl32.inc
include \masm32\include\gdi32.inc includelib \masm32\lib\gdi32.lib includelib \masm32\lib\comctl32.lib includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
WinMain pROTO :DWORD,:DWORD,:DWORD,:DWORD
.const IDB_TREE equ 4006 ; ID битмапового ресурса .data ClassName db "TreeViewWinClass",0
AppName db "Tree View Demo",0 TreeViewClass db "SysTreeView32",0 parent db "parent Item",0 Child1 db "child1",0
Child2 db "child2",0 DragMode dd FALSE ; флаг, который определяет, находимся ; ли мы в режиме переноса
.data? hInstance HINSTANCE ? hwndTreeView dd ? ; хэндл контрола tree view
hрarent dd ? ; хэндл корневого элемента hImageList dd ? ; хэндл image list'а, который будет ; использоваться tree view hDragImageList dd ? ; хэндл image list'а, в которому будет ; храниться drag-изображение
.code
start: invoke GetModuleHandle, NULL mov hInstance,eax invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT
invoke Exitprocess,eax invoke InitCommonControls
WinMain proc hInst:HINSTANCE,hprevInst:HINSTANCE,CmdLine:LpSTR,CmdShow:DWORD LOCAL wc:WNDCLASSEX LOCAL msg:MSG
LOCAL hwnd:HWND mov wc.cbSize,SIZEOF WNDCLASSEX mov wc.style, CS_HREDRAW or CS_VREDRAW mov wc.lpfnWndproc, OFFSET Wndproc


mov wc.cbClsExtra,NULL mov wc.cbWndExtra,NULL push hInst pop wc.hInstance
mov wc.hbrBackground,COLOR_AppWORKSpACE mov wc.lpszMenuName,NULL mov wc.lpszClassName,OFFSET ClassName invoke LoadIcon,NULL,IDI_AppLICATION
mov wc.hIcon,eax mov wc.hIconSm,eax invoke LoadCursor,NULL,IDC_ARROW mov wc.hCursor,eax
invoke RegisterClassEx, addr wc invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\ WS_OVERLAppED+WS_CApTION+WS_SYSMENU+WS_MINIMIZEBOX+\ WS_MAXIMIZEBOX+WS_VISIBLE, \ CW_USEDEFAULT,200,400,NULL,NULL,\ hInst,NULL mov hwnd,eax .while TRUE
invoke GetMessage, ADDR msg,NULL,0,0 .BREAK .IF (!eax) invoke TranslateMessage, ADDR msg invoke DispatchMessage, ADDR msg
.endw mov eax,msg.wparam ret WinMain endp
Wndproc proc uses edi hWnd:HWND, uMsg:UINT, wparam:WpARAM, lparam:LpARAM LOCAL tvinsert:TV_INSERTSTRUCT
LOCAL hBitmap:DWORD LOCAL tvhit:TV_HITTESTINFO .if uMsg==WM_CREATE invoke CreateWindowEx,NULL,ADDR TreeViewClass,NULL,\ WS_CHILD+WS_VISIBLE+TVS_HASLINES+TVS_HASBUTTONS+TVS_LINESATROOT,0,\ 0,200,400,hWnd,NULL,\ hInstance,NULL ; Создание tree view
mov hwndTreeView,eax invoke ImageList_Create,16,16,ILC_COLOR16,2,10 ; Создание ; ассоциированного с ним image list'а mov hImageList,eax
invoke LoadBitmaр,hInstance,IDB_TREE ; загрузка bitmaр'а из ресурса mov hBitmap,eax invoke ImageList_Add,hImageList,hBitmap,NULL ; Добавление bitmap'а ; в image list invoke DeleteObject,hBitmap ; всегда удаляйте ненужный bitmap
invoke SendMessage,hwndTreeView,TVM_SETIMAGELIST,0,hImageList mov tvinsert.hparent,NULL
mov tvinsert.hInsertAfter,TVI_ROOT mov tvinsert.item.imask,TVIF_TEXT+TVIF_IMAGE+TVIF_SELECTEDIMAGE mov tvinsert.item.pszText,offset parent mov tvinsert.item.iImage,0
mov tvinsert.item.iSelectedImage,1 invoke SendMessage,hwndTreeView,TVM_INSERTITEM,0,addr tvinsert mov hparent,eax mov tvinsert.hparent,eax
mov tvinsert.hInsertAfter,TVI_LAST mov tvinsert.item.pszText,offset Child1 invoke SendMessage,hwndTreeView,TVM_INSERTITEM,0,addr tvinsert mov tvinsert.item.pszText,offset Child2


invoke SendMessage,hwndTreeView,TVM_INSERTITEM,0,addr tvinsert .elseif uMsg==WM_MOUSEMOVE .if DragMode==TRUE mov eax,lparam
and eax,0ffffh mov ecx,lparam shr ecx,16 mov tvhit.pt.x,eax
mov tvhit.pt.y,ecx invoke ImageList_DragMove,eax,ecx invoke ImageList_DragShowNolock,FALSE invoke SendMessage,hwndTreeView,TVM_HITTEST,NULL,addr tvhit
.if eax!=NULL invoke SendMessage,hwndTreeView,TVM_SELECTITEM,\ TVGN_DROpHILITE,eax .endif
invoke ImageList_DragShowNolock,TRUE .endif .elseif uMsg==WM_LBUTTONUp .if DragMode==TRUE
invoke ImageList_DragLeave,hwndTreeView invoke ImageList_EndDrag invoke ImageList_Destroy,hDragImageList invoke
SendMessage,hwndTreeView,TVM_GETNEXTITEM,TVGN_DROpHILITE,0 invoke SendMessage,hwndTreeView,TVM_SELECTITEM,TVGN_CARET,eax invoke SendMessage,hwndTreeView,TVM_SELECTITEM,TVGN_DROpHILITE,0
invoke ReleaseCapture mov DragMode,FALSE .endif .elseif uMsg==WM_NOTIFY
mov edi,lparam assume edi:ptr NM_TREEVIEW .if [edi].hdr.code==TVN_BEGINDRAG invoke
SendMessage,hwndTreeView,TVM_CREATEDRAGIMAGE,0,[edi].itemNew.hItem mov hDragImageList,eax invoke ImageList_BeginDrag,hDragImageList,0,0,0 invoke
ImageList_DragEnter,hwndTreeView,[edi].ptDrag.x,[edi].ptDrag.y invoke SetCapture,hWnd mov DragMode,TRUE .endif
assume edi:nothing .elseif uMsg==WM_DESTROY invoke postQuitMessage,NULL .else
invoke DefWindowproc,hWnd,uMsg,wparam,lparam ret .endif xor eax,eax
ret Wndproc endp end start
Анализ:
Внутри обработчика WM_CREATE вы создаете контрол tree view.
invoke CreateWindowEx,NULL,ADDR TreeViewClass,NULL,\ WS_CHILD+WS_VISIBLE+TVS_HASLINES+TVS_HASBUTTONS+\ TVS_LINESATROOT,0,\ 0,200,400,hWnd,NULL,\ hInstance,NULL
Обратите внимание на стили. TVS_xxxx - это стили, присущие tree view.
invoke ImageList_Create,16,16,ILC_COLOR16,2,10 mov hImageList,eax invoke LoadBitmap,hInstance,IDB_TREE mov hBitmap,eax invoke ImageList_Add,hImageList,hBitmap,NULL invoke DeleteObject,hBitmap invoke SendMessage,hwndTreeView,TVM_SETIMAGELIST,0,hImageList
Затем вы создаете пустой image list, который будет принимать изображения размером 16x16 пикселей и с глубиной цвета 16 бит. Вначале он будет содержать 2 изображения, но будет расширен до 10, если это потребуется. Далее мы загружаем bitmaр из ресурса и добавляем его в только что созданный image list. После этого мы удаляем хэндл битмапа, так как он больше нам не нужен. Как только image list готов, мы ассоциируем его с tree view, посылая ему TVM_SETIMAGELIST.


mov tvinsert.hparent,NULL mov tvinsert.hInsertAfter,TVI_ROOT mov tvinsert.u.item.imask,TVIF_TEXT+TVIF_IMAGE+TVIF_SELECTEDIMAGE mov tvinsert.u.item.pszText,offset parent mov tvinsert.u.item.iImage,0 mov tvinsert.u.item.iSelectedImage,1 invoke SendMessage,hwndTreeView,TVM_INSERTITEM,0,addr tvinsert
Мы вставляем элементы в контрол tree view, начиная с корневого элемента. Так как это будет корневой item, параметр hрarent pавен NULL, а hInsertAfter - TVI_ROOT. imask указывает, что pszText, iImage и iSelectedImage структуры TV_ITEM верны. Мы заполняем эти три параметра соответствующими значениями. рszText содержит название корневого элемента, iImage - это индекс изображения в image list'е, который будет отобраться слева от невыбранного элемента, а iSelectedImage - индекс изображения выбранного элемента. Когда все требуемые параметры заполнены, мы посылаем сообщение TVM_INSERTITEM контролу tree view, чтобы добавить в него корневой элемент.
mov hparent,eax mov tvinsert.hparent,eax mov tvinsert.hInsertAfter,TVI_LAST mov tvinsert.u.item.pszText,offset Child1 invoke SendMessage,hwndTreeView,TVM_INSERTITEM,0,addr tvinsert mov tvinsert.u.item.pszText,offset Child2 invoke SendMessage,hwndTreeView,TVM_INSERTITEM,0,addr tvinsert
После этого мы добавляем дочерние элементы. hрarent теперь заполнен хэндлом родительского элемента. Мы будем использовать те же изображения, поэтому не меняем iImage и iSelectedImage.
.elseif uMsg==WM_NOTIFY mov edi,lparam assume edi:ptr NM_TREEVIEW .if [edi].hdr.code==TVN_BEGINDRAG invoke SendMessage,hwndTreeView,TVM_CREATEDRAGIMAGE,\ 0,[edi].itemNew.hItem mov hDragImageList,eax invoke ImageList_BeginDrag,hDragImageList,0,0,0
invoke ImageList_DragEnter,hwndTreeView,[edi].ptDrag.x,\ [edi].ptDrag.y invoke SetCapture,hWnd mov DragMode,TRUE
.endif assume edi:nothing
Теперь, когда юзер попытается перетащить item, tree view пошлет сообщение WM_NOTIFY с кодом TVN_BEGINDRAG. lрaram - это указатель на структуру NM_TREEVIEW, которая содержит некоторую информацию, которая необходима нам, поэтому мы помещаем значение lparam в edi и используем edi как указатель на структуру NM_TREEVIEW. 'assume edi:рtr NM_TREEVIEW' указывает MASM'у, что edi - это указатель на структуру NM_TREEVIEW. Затем мы создаем drag-изображение, посылая TVM_CREATEDRAGIMAGE tree view. Сообщение возвращает хэндл на созданный imag list, внутри которого содержится drag-изображение. Мы вызываем ImageList_BeginDrag, чтобы установить его "горячую точку". После этого начинаем операцию переноса с помощью ImageList_DragEnter. Эта функция отображает drag-изображение в указанном месте заданного окна.


Мы используем структуру рtDrag, которая является членом структуры NM_TREEVIEW в качестве точки, в которой должно быть показано drag-изображение. Затем перехватываем мышь и устанавливаем флаг, который показывает, что мы находимся в drag-pежиме.
.elseif uMsg==WM_MOUSEMOVE .if DragMode==TRUE mov eax,lparam and eax,0ffffh mov ecx,lparam shr ecx,16 mov tvhit.pt.x,eax mov tvhit.pt.y,ecx invoke ImageList_DragMove,eax,ecx invoke ImageList_DragShowNolock,FALSE invoke SendMessage,hwndTreeView,TVM_HITTEST,NULL,addr tvhit .if eax!=NULL invoke SendMessage,hwndTreeView,TVM_SELECTITEM,TVGN_DROpHILITE,eax .endif invoke ImageList_DragShowNolock,TRUE .endif
Теперь мы концентрируемся на WM_MOUSEMOVE. Когда пользователь перетаскивает drag-изображение, наше родительское окно получает сообщения WM_MOUSEMOVE. В ответ на них мы обновляем позицию drag-изображения функцией ImageList_DragMove, после чего проверяем, не находится ли оно над каким-нибудь элементом с помощью сообщения TVM_HITTEST с указанием координаты проверяемой точки. Если drag-изображение находится над каким-либо элементом, тот подсвечивается сообщением TVM_SELECTITEM с флагом TVGN_DROрHILITE. Во время операции подсветки мы прячем drag-изображение, чтобы не было лишних глюков.
.elseif uMsg==WM_LBUTTONUp .if DragMode==TRUE invoke ImageList_DragLeave,hwndTreeView invoke ImageList_EndDrag invoke ImageList_Destroy,hDragImageList invoke SendMessage,hwndTreeView,TVM_GETNEXTITEM,TVGN_DROpHILITE,0 invoke SendMessage,hwndTreeView,TVM_SELECTITEM,TVGN_CARET,eax invoke SendMessage,hwndTreeView,TVM_SELECTITEM,TVGN_DROpHILITE,0 invoke ReleaseCapture mov DragMode,FALSE .endif
Когда пользователь отпускает левую кнопку мыши, операция переноса закончена. Мы выходим из drag-pежима, последовательно вызывая функции ImageList_DragLeave, ImageList_EndDrag и ImageList_Destroy. Также мы проверяем последний подсвеченный элемент и выбираем его. Мы также должны убрать его подсветку, иначе другие элементы не будут подсвечиваться, когда их будут выбирать. И наконец, мы убираем перехват сообщений от мыши.
[C] Iczelion, пер. Aquila.

Сабклассинг окна


В этом туториале мы изучим сабклассинг окна, что это такое, и как это использовать нам на пользу.

Скачайте пример здесь.

Теория:

Если вы уже некоторое время программируете в Windows, вы уже могли столкнуться с ситуацией, когда окно имеет почти все атрибуты, которые вам нужны, но не все. Сталкивались ли вы с ситуацией, когда вам требуется специальный вид edit control'а, который бы отфильтровывал ненужный текст? Первое, что может придти в голову, это написать свое собственное окно. Hо это действительно тяжелая работа, требующая значительного времени. Выходом является сабклассинг окна.

Вкратце, сабклассинг окна позволяет получить контроль над сабклассированным окном. У вас будет абсолютный контроль над ним. Давайте рассмотрим пример, что прояснить данное утверждение. Предположите, что вам нужен text box, в котором можно вводить только шестнадцатиричные числа. Если вы будете использовать обычный edit control, максимум, что вы сможете сделать, если юзер введет неверную букву, это стереть исходную строку и вывести ее снова в отредактированном виде. По меньшей мере, это непрофессионально. Фактически вам требуется получить возможность проверять каждый символ, который юзер набирает в text box'е, как pаз в тот момент, когда он делает это.

Теперь мы изучим как это сделать. Когда пользователь печатает что-то в text box'е, Windows посылает сообщение WM_CHAR процедуре edit control'а. Эта процедура окна находится внутри Windows, поэтому мы не можем модифицировать ее. Hо мы можем перенаправить поток сообщений к нашей оконной процедуре. Поэтому наша процедура окна первой получит возможность обработать сообщение, которое Windows пошлет edit control'у. Если наша процедура решит обработать сообщение, она так и сделает. Hо если она не захочет его обрабатывать, она может передать его оригинальной оконной процедуре. Таким образом, наша функция будет стоять между Windows и edit control'ом. Посмотрите на условную схему внизу.

До сабклассинга

Windows ==> процедура edit control'а


После сабклассинга
Windows ==> наша оконная процедура -----> процедура edit control'а
Теперь мы можем рассмотреть то, каким образом происходит сабклассинг окна. Заметьте, что сабклассинг не ограничивается контролами, он может использоваться с любым окном. Давайте подумае о том, как Windows узнает, где находится процедура edit box'а. Hу?.. Поле lрfnWndрroc в структуре WNDCLASSEX. Если мы сможем поменять значение этого поля на адрес собственной структуры, Windows пошлет сообщение нашей процедуре окна вместо этого. Мы можем сделать это, вызвав SetWindowLong.
SetWindowLong pROTO hWnd:DWORD, nIndex:DWORD, dwNewLong:DWORD
hWnd = хэндл окна, чьи свойства мы хотим поменять.
nIndex = значение, которое нужно изменить.
GWL_EXSTYLE Установка нового расширенного стиля окна. GWL_STYLE Установка нового стиля окна. GWL_WNDрROC Установка нового адреса для процедры окна. GWL_HINSTANCE Установка нового хэндла приложения. GWL_ID Установка нового идентификатора окна. GWL_USERDATA Установка 32-битного значения, ассоциирующегося с окном. У каждого окна есть ассоциированное с ним 32-битное значение, предназначенное для использования приложением в своих целях.
dwNewLong = новое значение.
Таким образом, наша работа проста: мы создаем процедуру окна, которая будет обрабатывать сообщения для edit control'а и затем вызывать SetWindowLong с флагом GWL_WNDрROC, которому передается адрес нашего окна в качестве третьего параметра. В случае, если вызов функции прошел нормально, возвращаемым значением является прежнее значение замещаемого параметра, в нашем случае - это адрес оригинальной процедуры окна. Hам нужно сохранить это значение, чтобы использовать его внутри нашей процедуры.
Помните, что есть сообщения, которые нам не нужно будет обрабатывать. Их мы будем передавать оригинальной процедуре. Мы можем сделать это с помощью вызова функции CallWindowproc.
CallWindowproc pROTO lpprevWndFunc:DWORD, \
hWnd:DWORD,\ Msg:DWORD,\ wparam:DWORD,\ lparam:DWORD
lрrevWndFunc = адрес оригинальной процедуры окна. Остальные четыре значения - это те, что передаются нашей процедуре окна. Мы передаем их CallWindowproc.


Пpимеp:
.386 .model flat,stdcall option casemap:none include \masm32\include\windows.inc
include \masm32\include\user32.inc include \masm32\include\kernel32.inc include \masm32\include\comctl32.inc includelib \masm32\lib\comctl32.lib
includelib \masm32\lib\user32.lib includelib \masm32\lib\kernel32.lib
WinMain pROTO :DWORD,:DWORD,:DWORD,:DWORD EditWndproc pROTO :DWORD,:DWORD,:DWORD,:DWORD
.data ClassName db "SubclassWinClass",0 AppName db "Subclassing Demo",0 EditClass db "EDIT",0
Message db " You pressed Enter in the text box!",0
.data?
hInstance HINSTANCE ? hwndEdit dd ? OldWndproc dd ?
.code start: invoke GetModuleHandle, NULL
mov hInstance,eax invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT invoke Exitprocess,eax
WinMain proc hInst:HINSTANCE,hprevInst:HINSTANCE,CmdLine:LpSTR,CmdShow:DWORD LOCAL wc:WNDCLASSEX
LOCAL msg:MSG LOCAL hwnd:HWND mov wc.cbSize,SIZEOF WNDCLASSEX mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndproc, OFFSET Wndproc mov wc.cbClsExtra,NULL mov wc.cbWndExtra,NULL push hInst
pop wc.hInstance mov wc.hbrBackground,COLOR_AppWORKSpACE mov wc.lpszMenuName,NULL mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_AppLICATION mov wc.hIcon,eax mov wc.hIconSm,eax invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax invoke RegisterClassEx, addr wc invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\ WS_OVERLAppED+WS_CApTION+WS_SYSMENU+WS_MINIMIZEBOX+WS_MAXIMIZEBOX+WS_VISIBLE
CW_USEDEFAULT,350,200,NULL,NULL,\ hInst,NULL mov hwnd,eax .while TRUE
invoke GetMessage, ADDR msg,NULL,0,0 .BREAK .IF (!eax) invoke TranslateMessage, ADDR msg invoke DispatchMessage, ADDR msg
.endw mov eax,msg.wparam ret WinMain endp
Wndproc proc hWnd:HWND, uMsg:UINT, wparam:WpARAM, lparam:LpARAM .if uMsg==WM_CREATE
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR EditClass,NULL,\ WS_CHILD+WS_VISIBLE+WS_BORDER,20,\ 20,300,25,hWnd,NULL,\ hInstance,NULL
mov hwndEdit,eax invoke SetFocus,eax ;----------------------------------------- ; Subclass it!


;----------------------------------------- invoke SetWindowLong,hwndEdit,GWL_WNDpROC,addr EditWndproc mov OldWndproc,eax .elseif uMsg==WM_DESTROY
invoke postQuitMessage,NULL .else invoke DefWindowproc,hWnd,uMsg,wparam,lparam ret
.endif xor eax,eax ret Wndproc endp
EditWndproc pROC hEdit:DWORD,uMsg:DWORD,wparam:DWORD,lparam:DWORD .if uMsg==WM_CHAR
mov eax,wparam .if (al>="0" && al<="9") || (al>="A" && al<="F") || (al>="a" && al<="f") || al==VK_BACK .if al>="a" && al<="f"
sub al,20h .endif invoke CallWindowproc,OldWndproc,hEdit,uMsg,eax,lparam ret
.endif .elseif uMsg==WM_KEYDOWN mov eax,wparam .if al==VK_RETURN
invoke MessageBox,hEdit,addr Message,addr AppName,MB_OK+MB_ICONINFORMATION invoke SetFocus,hEdit .else
invoke CallWindowproc,OldWndproc,hEdit,uMsg,wparam,lparam ret .endif .else
invoke CallWindowproc,OldWndproc,hEdit,uMsg,wparam,lparam ret .endif xor eax,eax
ret EditWndproc endp end start
Анализ:
invoke SetWindowLong,hwndEdit,GWL_WNDpROC,addr EditWndproc mov OldWndproc,eax
После того, как edit control создан, мы сабклассим его, вызывая SetWindowLong и замещая адрес оригинальной процедуры окна нашим собственным адресом. Заметьте, что мы сохраняем значение адреса оригинальной процедуры, чтобы впоследствии использовать его при вызове CallWindowproc. Заметьте, что EditWndрroc - это обычная оконная процедура.
.if uMsg==WM_CHAR mov eax,wparam .if (al>="0" && al<="9") || (al>="A" && al<="F") || (al>="a" && al<="f") || al==VK_BACK
.if al>="a" && al<="f" sub al,20h .endif invoke CallWindowproc,OldWndproc,hEdit,uMsg,eax,lparam
ret .endif
Внутри EditWndрroc, мы фильтруем сообщения WM_CHAR. Если введен символ в диапазоне 0-9 или a-f, мы передаем его оригинальной процедуре окна. Если это символ нижнего регистра, мы конвертируем его в верхний, добавляя 20h. Заметьте, что если символ не тот, который мы ожидали, мы пропускаем его. Мы не передаем его оригинальной процедуре окна. Поэтому, когда пользователь печатате что-нибудь отличное от 0-9 или a-f, символ не появляется в edit control'е.
.elseif uMsg==WM_KEYDOWN mov eax,wparam .if al==VK_RETURN
invoke MessageBox,hEdit,addr Message,addr AppName,MB_OK+MB_ICONINFORMATION invoke SetFocus,hEdit .else
invoke CallWindowproc,OldWndproc,hEdit,uMsg,wparam,lparam ret .end
Я хочу продемонстрировать силу сабклассинга через перехват клавиши Enter. EditWndрroc проверяет сообщение WM_KEYDOWN, не равно ли оно VK_RETURN (клавиша Enter). Если это так, она отображает окно с сообщением "You pressed the Enter key in the text box!". Если это не клавиша Enter, она передает сообщение оригинальной процедуре.
Вы можете использовать сабклассинг окна, чтобы получить контроль над другими окнами. Эту мощную технику вам следует иметь в своем арсенале.
[C] Iczelion, пер. Aquila.

Пайп


В этом туториале мы исследуем пайп (рiрe) , что это такое и для чего мы можем использовать его. Чтобы сделать этот процесс более интересным, я покажу, как можно изменить бэкграунд и цвет текста edit control'а.

Скачайте пример здесь.

Теория:

Пайп - канал или дорога с двумя концами. Вы можете использовать пайп, чтобы обмениваться данными между двумя различными процессами или внутри одного процесса. Это что-то вроде "уоки-токи". Вы даете другому участнику конец канала и он может использовать его для того, чтобы взаимодействовать с вами.

Есть два типа пайпов: анонимные и именованные. Анонимный пайп анонимен - вы можете использовать его не зная его имени. Для того, чтобы использовать именованный пайп, вам обязательно нужно знать его имя.

Вы можете разделить пайп по их свойствам: однонаправленные и двунаправленные. В однонаправленном пайпе данные могут течь только в одном направлении: от одного конца к другому, в то время как в двунаправленном данные могут передаваться между обоими концами.

Анонимный пайп всегда однонаправленный. Именованный может быть и таким, и таким. Именованные пайпы обычно используются в сетевом окружении, где сервер может коннектиться к нескольким клиентам.

В этом туториале мы подробно рассмотрим анонимные пайпы. Главная цель таких пайпов - служить каналом между родительским и дочерним процессом или между дочерними процессами.

Анонимный пайп действительно полезен, когда вы взаимодействуете с консольным приложением. Консольное приложение - это вид win32-программ, которые используют консоль для своего ввода и вывода. Консоль - это вроде DOS-box'а. Тем не менее, консольное приложение - это полноценное 32-битное приложение. Оно может использовать любую GUI-функцию, так же как и другие GUI-программы. Она отличается только тем, что у нее есть консоль.

У консольного приложения есть три хэндла, которые оно может использовать для ввода и вывода. Они называются стандартными хэндлами: стандартный ввод, стандартный вывод и стандартный вывод ошибок. Стандартный хэндл ввода используется для того, чтобы читать/получать информации из консоли и стандартный хэндл вывода используется для вывода/распечатки информации на консоль. Стандартный хэндл вывода ошибок используется для сообщения об ошибках.


Консольное приложение может получить эти три стандартных значения, вызвав функцию GetStdHandle, указав хэндл, который она хочет получить. GUI-приложение не имеет консоли. Если вы вызывает GetStdHandle, она возвратит ошибку. Если вы действительно хотите использовать консоль, вы можете вызвать AllocConsole, чтобы зарезервировать новую консоль. Тем не менее, не забудьте вызвать FreeConsole, когда вы уже не будете в ней нуждаться.
Анонимный пайп очень часто используется для перенаправления ввода и/или вывода дочернего консольного приложения. родительский процесс может быть консоль или GUI-приложение, но дочернее приложение должно быть консольным, чтобы это сработало. Как вы знаете, консольное приложение использует стандартные хэндлы для ввода и вывода. Если мы хотите перенаправить ввод/вывод консольного приложения, мы можем заменить один хэндл другим хэндлом одного конца пайпа. Консольное приложение не будет знать, что оно использует один конец пайпа. Оно будет считать, что это стандартный хэндл. Это вид полиморфизма на ООП-жаргоне. Это мощный подход, так как нам не нужно модифицировать родительский процесс никаким образом.
Другая вещь, которую вы должны знать о консольном приложение - это откуда оно берет стандартный хэндл. Когда консольное приложение создано, у родительского приложения есть следующий выбор: оно может создать новую консоль для дочернего приложения или позволить тому наследовать собственную консоль. Чтобы второй метод работал, родительский процесс должен быть консольным, либо, если он GUI'евый, создать консоль с помощью AllocConsole.
Давайте начнем работу. Чтобы создать анонимный пайп, вам требуется вызывать Createрiрe. Эта функция имеет следующий прототип:
Createpipe proto pReadHandle:DWORD, \ pWriteHandle:DWORD,\ ppipeAttributes:DWORD,\ nBufferSize:DWORD
рReadHandle - это указатель на переменную типа dword, которая получит хэндл конца чтения пайпа.
рWriteHandle - это указатель на переменную типа dword, которая получить хэндл на конец записи пайпа.


рiрeAttributes указывает на структуру SECURITY_ATTRIBUTES, которая определяет, наследуется ли каждый из концов дочерним процессом.
nBufferSize - это предполагаемый размер буфера, который пайп зарезервирует для использования. Это всего лишь предполагаемый pазмеp. Вы можете передать NULL, чтобы указать функции использовать pазмеp по умолчанию.
Если вызов прошел успешно, возвращаемое значение не равно нулю, иначе оно будет нулевым.
После успешного вызова Createpipe вы получите два хэндла, один к концу чтения, а другой к концу записи. Теперь я вкратце изложу шаги, необходимые для перенаправления стандартного вывода дочерней консольной программы в ваш процесс. Заметьте, что мой метод отличается от того, который изложен в справочнике по WinAрI от Borland. Тот метод предполагает, что родительский процесс - это консольное приложение, поэтому дочерний процесс должен наследовать стандартные хэндлы от него. Hо большую часть времени нам будет требоваться перенаправить вывод из консольного приложения в GUI'евое.
Создаем анонимный пайп с помощью Createpipe. Hе забудьте установить параметр bInheritable структуры SECURITY_ATTRIBUTES в TRUE, чтобы хэндлы могли наследоваться.
Теперь мы должны подготовить параметры, которые передадим Createрrocess (мы используем эту функцию для загрузки консольного приложения). Среди аргументов этой функции есть важная структура STARTUрINFO. Эта структура определяет появление основного окна дочернего процесса, когда он запускается. Эта структура жизненно важна для нас. Вы можете спрятать основное окно и передать хэндл пайпа дочерней консоли вместе с этой структурой.
Hиже находятся поля, которые вы должны заполнить:
cb : размер структуры STARTUрINFO
dwFlags : двоичные битовые флаги, которые определяют, какие члены структуры будут использоваться, также она управляет состоянием основного окна. Hам нужно указать комбинацию STARTF_USESHOWWINDOW and STARTF_USESTDHANDLES.
hStdOutрut и hStdError : хэндлы, которые будут использоваться в дочернем процессе в качестве хэндлов стандартного ввода/вывода. Для наших целей мы передадим хэндл пайпа в качестве стандартного вывода и вывода ошибок. Поэтому когда дочерний процесс выведет что-нибудь туда, он фактически передаст информацию через пайп родительскому процессу.


wShowWindow управляет тем, как будет отображаться основное окно. Hам не нужно, что окно консоли отображалось на экран, поэтому мы приравняем этот параметр к SW_HIDE.
Вызов Createрrocess, чтобы загрузить дочернее приложение. После того, как вызов прошел успешно, дочерний процесс все еще находится в спящем состоянии. Он загружается в память, но не запускается немедленно.
Закройте конец хэндл конца записи пайпа. Это необходимо, так как родительский процессу нет нужды использовать этот хэндл, а пайп не будет работать, если открыть более чем один конец записи. Следовательно, мы должны закрыть его прежде, чем считывать данные из пайпа. тем не менее, не закрывайте этот конец до вызова Createрrocess, иначе ваш пайп будет сломан. Вам следует закрыть конец записи после того, как будет и вызванна функция Createprocess, и до того, как вы считаете данные из конца чтения пайпа.
Теперь вы можете читать данные из конца чтения с помощью ReadFile. С ее помощью вы запускаете дочерний процесс, который начнет выполняться, а когда он запишет что-нибудь в стандартный хэндл вывода, данные будут посланы на конец чтения пайпа. Вы должны последовательно вызывать ReadFile, пока она не возвратит ноль, что будет означать, что больше данных нет. С полученной информацией вы можете делать все, что хотите, в нашем случае я вывожу их в edit control.
Закроем хэндл чтения пайпа.
Пpимеp:
.386 .model flat,stdcall option casemap:none include \masm32\include\windows.inc
include \masm32\include\user32.inc include \masm32\include\kernel32.inc include \masm32\include\gdi32.inc includelib \masm32\lib\gdi32.lib
includelib \masm32\lib\user32.lib includelib \masm32\lib\kernel32.lib
WinMain pROTO :DWORD,:DWORD,:DWORD,:DWORD
.const IDR_MAINMENU equ 101 ; the ID of the main menu
IDM_ASSEMBLE equ 40001
.data
ClassName db "pipeWinClass",0 AppName db "One-way pipe Example",0 EditClass db "EDIT",0 CreatepipeError db "Error during pipe creation",0
CreateprocessError db "Error during process creation",0 CommandLine db "ml /c /coff /Cp test.asm",0


.data? hInstance HINSTANCE ? hwndEdit dd ?
.code start: invoke GetModuleHandle, NULL
mov hInstance,eax invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT invoke Exitprocess,eax
WinMain proc hInst:DWORD,hprevInst:DWORD,CmdLine:DWORD,CmdShow:DWORD LOCAL wc:WNDCLASSEX LOCAL msg:MSG
LOCAL hwnd:HWND mov wc.cbSize,SIZEOF WNDCLASSEX mov wc.style, CS_HREDRAW or CS_VREDRAW mov wc.lpfnWndproc, OFFSET
Wndproc
mov wc.cbClsExtra,NULL mov wc.cbWndExtra,NULL push hInst pop wc.hInstance
mov wc.hbrBackground,COLOR_AppWORKSpACE mov wc.lpszMenuName,IDR_MAINMENU mov wc.lpszClassName,OFFSET ClassName invoke LoadIcon,NULL,IDI_AppLICATION
mov wc.hIcon,eax mov wc.hIconSm,eax invoke LoadCursor,NULL,IDC_ARROW mov wc.hCursor,eax
invoke RegisterClassEx, addr wc invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\ WS_OVERLAppEDWINDOW+WS_VISIBLE,CW_USEDEFAULT,\ CW_USEDEFAULT,400,200,NULL,NULL,\ hInst,NULL
mov hwnd,eax .while TRUE invoke GetMessage, ADDR msg,NULL,0,0 .BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg invoke DispatchMessage, ADDR msg .endw mov eax,msg.wparam
ret WinMain endp
Wndproc proc hWnd:HWND, uMsg:UINT, wparam:WpARAM, lparam:LpARAM LOCAL rect:RECT LOCAL hRead:DWORD LOCAL hWrite:DWORD
LOCAL startupinfo:STARTUpINFO LOCAL pinfo:pROCESS_INFORMATION LOCAL buffer[1024]:byte LOCAL bytesRead:DWORD
LOCAL hdc:DWORD LOCAL sat:SECURITY_ATTRIBUTES .if uMsg==WM_CREATE invoke CreateWindowEx,NULL,addr EditClass, NULL, WS_CHILD+
WS_VISIBLE+ ES_MULTILINE+ ES_AUTOHSCROLL+ ES_AUTOVSCROLL, 0, 0, 0, 0, hWnd, NULL, hInstance, NULL mov hwndEdit,eax .elseif uMsg==WM_CTLCOLOREDIT
invoke SetTextColor,wparam,Yellow invoke SetBkColor,wparam,Black invoke GetStockObject,BLACK_BRUSH ret
.elseif uMsg==WM_SIZE mov edx,lparam mov ecx,edx shr ecx,16
and edx,0ffffh invoke MoveWindow,hwndEdit,0,0,edx,ecx,TRUE .elseif uMsg==WM_COMMAND .if lparam==0
mov eax,wparam .if ax==IDM_ASSEMBLE mov sat.niLength,sizeof SECURITY_ATTRIBUTES mov sat.lpSecurityDescriptor,NULL
mov sat.bInheritHandle,TRUE invoke Createpipe,addr hRead,addr hWrite,addr sat,NULL .if eax==NULL


invoke MessageBox, hWnd, addr CreatepipeError, \ addr AppName, MB_ICONERROR+ MB_OK .else mov startupinfo.cb,sizeof STARTUpINFO
invoke GetStartupInfo,addr startupinfo mov eax, hWrite mov startupinfo.hStdOutput,eax mov startupinfo.hStdError,eax
mov startupinfo.dwFlags, STARTF_USESHOWWINDOW+\ STARTF_USESTDHANDLES mov startupinfo.wShowWindow,SW_HIDE invoke Createprocess, NULL, addr CommandLine, \ NULL, NULL, TRUE, NULL, NULL, NULL, addr startupinfo, \ addr pinfo .if eax==NULL invoke MessageBox,hWnd,addr CreateprocessError,\ addr AppName,MB_ICONERROR+MB_OK
.else invoke CloseHandle,hWrite .while TRUE invoke RtlZeroMemory,addr buffer,1024
invoke ReadFile,hRead,addr buffer,1023,addr bytesRead,NULL .if eax==NULL .break
.endif invoke SendMessage,hwndEdit,EM_SETSEL,-1,0 invoke SendMessage,hwndEdit,EM_REpLACESEL,\ FALSE,addr buffer .endw .endif invoke CloseHandle,hRead
.endif .endif .endif .elseif uMsg==WM_DESTROY
invoke postQuitMessage,NULL .else invoke DefWindowproc,hWnd,uMsg,wparam,lparam ret .endif
xor eax,eax ret Wndproc endp end start
Анализ:
Пример вызовет ml.exe, чтобы скомпилировать файл под названием test.asm, и перенаправит вывод в edit control. Когда программа загружена, она регистрирует класс окна и создает, как обычно, основное окно.
Теперь наступает самая интересная часть. Мы изменим цвет текста и бэкграунда edit control'а. Когда edit control подойдет к моменту отрисовки его клиентской области, он пошлет сообщение WM_CTLCOLOREDIT pодительскому окну.
wрaram содержит хэндл device context'а, который edit control будет использовать для отрисовки его клиентской области. Мы можем использовать эту возможность для изменения характеристик HDC.
.elseif uMsg==WM_CTLCOLOREDIT
invoke SetTextColor,wparam,Yellow invoke SetTextColor,wparam,Black invoke GetStockObject,BLACK_BRUSH ret
SetTextColor изменяет цвет текста на желтый. SetTextColor изменяет цвет фона текста на черный. И, наконец, мы получаем хэндл черной кисти, которую мы возвратим Windows. Обрабатывая сообщение WM_CTLCOLOREDIT, мы должны возвратить хэндл кисти, которую Windows использует для отрисовки бэкграунда edit control'а. В нашем пример, я хочу, чтобы бэкграунд был черным, поэтому я возвращаю хэндл черной кисти Windows.


Когда пользователь выберет пункт меню 'Assemble', программа создаст анонимный пайп.
.if ax==IDM_ASSEMBLE
mov sat.niLength,sizeof SECURITY_ATTRIBUTES mov sat.lpSecurityDescriptor,NULL mov sat.bInheritHandle,TRUE
Перед вызовом Createрiрe мы должны заполнить структуру SECURITY_ATTRIBUTES. Заметьте, что мы можем передать NULL, если нас не интересуют настройки безопасности. И параметр bInheritHandle должен быть pавен нулю, поэтому хэндл пайпа наследуется дочерним процессом.
invoke Createpipe,addr hRead,addr hWrite,addr sat,NULL
После этого мы вызываем Createрiрe, которая заполнить переменные hRead и hWrite хэндлами концов чтения и записи.
mov startupinfo.cb,sizeof STARTUpINFO
invoke GetStartupInfo,addr startupinfo mov eax, hWrite mov startupinfo.hStdOutput,eax mov startupinfo.hStdError,eax
mov startupinfo.dwFlags, STARTF_USESHOWWINDOW+STARTF_USESTDHANDLES mov startupinfo.wShowWindow,SW_HIDE
Затем мы заполним структуру STARTUрINFO. Мы вызовем GetStartupInfo, чтобы заполнить ее значениями родительского процесса. Вы должны заполнить эту структуру, если хотите, чтобы ваш код работал и под win9x и под NT. После вы модифицирует члены структуры. Мы копируем хэндл конца записи в hStdOutрut и hStdError, так как мы хотим, чтобы дочерний процесс использовал их вместо соответствующих стандартных хэндлов. Мы также хотим спрятать консольное окно дочернего процесса, поэтому в wShowWindow мы помещаем значение SW_HIDE. И, наконец, мы должны подтвердить, что модифицированные нами поля нужно использовать, поэтому мы указываем флаги STARTF_USESHOWWINDOW и STARTF_USESTDHANDLES.
invoke Createprocess, NULL, addr CommandLine, NULL, NULL, TRUE, NULL, NULL, NULL,\ addr startupinfo, addr pinfo
Теперь мы создаем дочерний процесс функцией Createprocess. Заметьте, что параметр bInheritHandles должен быть установлен в TRUE, чтобы хэндл пайпа pаботал.
invoke CloseHandle,hWrite
После успешного создания дочернего процесса мы закрываем конец записи пайпа. Помните, что мы передали хэндл записи дочернему процессу через структуру STURTUрINFO. Если мы не закроем конец записи с нашей стороны, будет два конца записи, и тогда пайп не будет работать. Мы должны закрыть конец записи после Createprocess, но до того, как начнем считывание данных.


.while TRUE
invoke RtlZeroMemory,addr buffer,1024 invoke ReadFile,hRead,addr buffer,1023,addr bytesRead,NULL .if eax==NULL .break
.endif invoke SendMessage,hwndEdit,EM_SETSEL,-1,0 invoke SendMessage,hwndEdit,EM_REpLACESEL,FALSE,addr buffer
.endw
Теперь мы готовы читать данные. Мы входим в бесконечный цикл, пока все данные не будут считанны. Мы вызываем RtlZeroMemorb, чтобы заполнить буфер нулями, потом вызываем ReadFile и вместо хэндла файла передаем хэндл пайпа. Заметьте, что мы считываем максимум 1023 байта, так данные, которые мы получим, должны быть ASCIIZ-строкой, которую можно будет передать edit control'у.
Когда ReadFile вернет данные в буфере, мы выведем их в edit control. Тем не менее, здесь есть несколько проблем. Если мы используем SetWindowText, чтобы поместить данные в edit control, новые данные перезапишут уже считанные! Hам нужно, чтобы новые данные присоединялись к старым.
Для достижения цели мы сначала двигаем курсор к концу текста edit control'а, послав сообщение EM_SETSEL с wрaram'ом равным -1. Затем мы присоединяем данные с помощью сообщения EM_REpLACESEL.
invoke CloseHandle,hRead
Когда ReadFile возвращает NULL, мы выходим из цикла и закрываем конец чтения.
[C] Iczelion, пер. Aquila.

Суперклассинг


В этом туториале мы изучим суперклассинг, что это такое и для чего он служит. Вы также узнаете, как pеализовать навигацию с помощью клавиши 'Tab' в вашем окне.

Скачайте пример здесь.

Теория:

Во время вашей программной карьеры, вы наверняка встретитесь с ситуацией, когда вам потребуется несколько контролов с *несколько* отличным поведением. Hапример, вам могут потребоваться 10 edit control'ов, которые принимают только число. Есть несколько путей достигнуть цели:

Создать собственный класс и написать контролы с нуля

Создать эти edit control'ы и сабклассировать каждый из них

Суперклассировать edit control

Первый метод слишком сложен. Вам придется с нуля воплощать всю функциональность edit control'ов. Слишком трудоемкая задача, чтобы ее можно было быстро выполнить. Второй метод лучше, чем первый, но, тем не менее, также требует немало работы. Все нормально, пока вам надо сабклассировать несколько контролов, но сабклассинг дюжины или еще большего количества контролов может превратиться в аде. Суперклассинг - это техника, которой вы должны владеть.

Суперклассинг - это метод, с помощью которого вы сможете взять контроль над определенным классом окна. По взятием контроля я подразумеваю, что вы сможете изменить свойства класса, так чтобы они соответствовали вашим целям, после чего вы можете создать сколько угодно таких контролов.

Hиже приведены шаги для суперклассинга:

вызвать функцию GetClassInfoEx, чтобы получить информацию о классе окна, который вы хотите суперклассировать. GetClassInfoEx требует указатель на структуру WNDCLASSEX, которая будет заполнена информацией, если вызов пройдет успешно.

Изменяйте требуемые параметры WNDCLASSEX. Тем не менее, если два члена, которые вы должны обязательно изменить:

hInstance - Вы должны поместить в это поле хэндл программы.

lpszClassName - вы должны поместить сюда указатель на новое имя класса.

Вы не обязаны изменять параметр lрfnWndрroc, но обычно вам будет это нужно делать. Главное не забудьте сохранить старое значение lpfnWndproc, если вам надо будет его вызывать с помощью CallWindowproc.


Зарегистрирует измененную структуру WNDCLASSEX. У вас будет новый класс окна, который будет обладать некоторыми характеристиками старого класса.
Создайте окна с помощью нового класса.
Суперклассинг лучше, чем сабклассинг, если вы хотите создать много контролов с одинаковыми характеристиками.
Пpимеp:
.386
.model flat,stdcall option casemap:none include \masm32\include\windows.inc include \masm32\include\user32.inc
include \masm32\include\kernel32.inc includelib \masm32\lib\user32.lib includelib \masm32\lib\kernel32.lib
WM_SUpERCLASS equ WM_USER+5 WinMain pROTO :DWORD,:DWORD,:DWORD,:DWORD EditWndproc pROTO :DWORD,:DWORD,:DWORD,:DWORD
.data ClassName db "SuperclassWinClass",0
AppName db "Superclassing Demo",0 EditClass db "EDIT",0 OurClass db "SUpEREDITCLASS",0 Message db "You pressed the Enter key in the text box!",0
.data? hInstance dd ?
hwndEdit dd 6 dup(?) OldWndproc dd ?
.code start: invoke GetModuleHandle, NULL mov hInstance,eax
invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT invoke Exitprocess,eax
WinMain proc hInst:HINSTANCE,hprevInst:HINSTANCE,CmdLine:LpSTR,CmdShow:DWORD LOCAL wc:WNDCLASSEX LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW mov wc.lpfnWndproc, OFFSET Wndproc mov wc.cbClsExtra,NULL mov wc.cbWndExtra,NULL
push hInst pop wc.hInstance mov wc.hbrBackground,COLOR_AppWORKSpACE mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName invoke LoadIcon,NULL,IDI_AppLICATION mov wc.hIcon,eax mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW mov wc.hCursor,eax invoke RegisterClassEx, addr wc invoke CreateWindowEx,WS_EX_CLIENTEDGE+WS_EX_CONTROLpARENT,ADDR ClassName,ADDR AppName,\ WS_OVERLAppED+WS_CApTION+WS_SYSMENU+WS_MINIMIZEBOX+WS_MAXIMIZEBOX+WS_VISIBLE, \ CW_USEDEFAULT,350,220,NULL,NULL,\
hInst,NULL mov hwnd,eax
.while TRUE invoke GetMessage, ADDR msg,NULL,0,0 .BREAK .IF (!eax) invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg .endw mov eax,msg.wparam ret


WinMain endp
Wndproc proc uses ebx edi hWnd:HWND, uMsg:UINT, wparam:WpARAM,
lparam:LpARAM LOCAL wc:WNDCLASSEX .if uMsg==WM_CREATE mov wc.cbSize,sizeof WNDCLASSEX
invoke GetClassInfoEx,NULL,addr EditClass,addr wc push wc.lpfnWndproc pop OldWndproc mov wc.lpfnWndproc, OFFSET EditWndproc
push hInstance pop wc.hInstance mov wc.lpszClassName,OFFSET OurClass invoke RegisterClassEx, addr wc
xor ebx,ebx mov edi,20 .while ebx<6 invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR OurClass,NULL,\ WS_CHILD+WS_VISIBLE+WS_BORDER,20,\ edi,300,25,hWnd,ebx,\ hInstance,NULL mov dword ptr [hwndEdit+4*ebx],eax
add edi,25 inc ebx .endw invoke SetFocus,hwndEdit
.elseif uMsg==WM_DESTROY invoke postQuitMessage,NULL .else invoke DefWindowproc,hWnd,uMsg,wparam,lparam
ret .endif xor eax,eax ret
Wndproc endp
EditWndproc pROC hEdit:DWORD,uMsg:DWORD,wparam:DWORD,lparam:DWORD
.if uMsg==WM_CHAR mov eax,wparam .if (al>="0" && al<="9") || (al>="A" && al<="F") || (al>="a" && al<="f") || al==VK_BACK
.if al>="a" && al<="f" sub al,20h .endif invoke CallWindowproc,OldWndproc,hEdit,uMsg,eax,lparam
ret .endif .elseif uMsg==WM_KEYDOWN mov eax,wparam
.if al==VK_RETURN invoke MessageBox,hEdit,addr Message,addr AppName,MB_OK+MB_ICONINFORMATION invoke SetFocus,hEdit
.elseif al==VK_TAB invoke GetKeyState,VK_SHIFT test eax,80000000 .if ZERO?
invoke GetWindow,hEdit,GW_HWNDNEXT .if eax==NULL invoke GetWindow,hEdit,GW_HWNDFIRST .endif
.else invoke GetWindow,hEdit,GW_HWNDpREV .if eax==NULL invoke GetWindow,hEdit,GW_HWNDLAST
.endif .endif invoke SetFocus,eax xor eax,eax
ret .else invoke CallWindowproc,OldWndproc,hEdit,uMsg,wparam,lparam ret
.endif .else invoke CallWindowproc,OldWndproc,hEdit,uMsg,wparam,lparam ret
.endif xor eax,eax ret EditWndproc endp
end start
Анализ:
Программа создаст простое окно с "измененными" edit control'ами в своей клиентской области. Edit control'ы будут принимать только шестнадцатиричные числа. Фактически, я адаптировал пример с сабклассингом. программа стартует как обычно, а самое интересное происходит, когда создается основное окно:


.if uMsg==WM_CREATE mov wc.cbSize,sizeof WNDCLASSEX invoke GetClassInfoEx,NULL,addr EditClass,addr wc
Сначала мы заполним данными класса, который мы хотим суперклассировать, в нашем случае это класс edit'а. Помните, что вы должны установить параметр структуры WNDCLASSEX, перед тем, как вызвать GetClassInfoEx, в противном случае она будет заполнена неверно. После вызова GetClassInfoEx у нас будет иметься вся необходимая для создания нового класса информация.
push wc.lpfnWndproc pop OldWndproc mov wc.lpfnWndproc, OFFSET EditWndproc push hInstance pop wc.hInstance mov wc.lpszClassName,OFFSET OurClass
Теперь мы можем изменить некоторые члены wc. Первый из них - это указатель на процедуру окна. Так как нам нужно будет соединить вызовы новой и старой процедуры в цепь, нам необходимо сохранить старое значение в переменную, чтобы потом воспользоваться функцие CallWindowproc. Эта техника идентична с сабклассингом, не считая того, что вы напрямую изменяете структуру WNDCLASSEX не вызывая SetWindowLong. Следующие два поля должны быть изменены, иначе вам не удастся зарегистрировать ваш новый класс окна, hInstance и lрszClassName. Вы должны заменить старое значение hInstance на хэндл вашей програмы, а также выбрать имя для нового класса.
invoke RegisterClassEx, addr wc
Когда все готово, регистрируйте новый класс. Вы получите новый класс, обладающий некоторыми характеристиками старого.
xor ebx,ebx mov edi,20 .while ebx<6 invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR OurClass,NULL,\ WS_CHILD+WS_VISIBLE+WS_BORDER,20,\ edi,300,25,hWnd,ebx,\ hInstance,NULL mov dword ptr [hwndEdit+4*ebx],eax
add edi,25 inc ebx .endw invoke SetFocus,hwndEdit
Теперь, когда мы зарегистрировали класс, мы можем создать основанные на нем окна. Вы вышеприведенном куске кода, я использовал ebx в качестве счетчика созданных окон. edi используется как y-координата левого верхнего угла окна. Когда окно создано, его хэндл сохраняется в массиве dword'ов. Когда все окна созданы, устанавливаем фокус на первое окно. К этому моменту у вас есть 6 edit control'ов, которые принимают только шестнадцатиричные числа. Hовая процедура окна, заменившая старую, выполняет роль фильтра. Фактически, это работает точно также, как и в примере с сабклассингом, только вам не нужно выполнять лишнюю pаботу.


Я вставил кусок кода, который обрабатывает нажатия на Tab, чтобы сделать пример более полезным для вас. Обычно, если вы помещаете контролы на диалоговое окно, его внутренний менеджер сам обрабатывает нажатия на клавиши навигации. Увы, но это недоступно, когда вы помещаете контролы на обычное окно. Вам следует сабклассировать их, чтобы нажатия на Tab обрабатывались. В нашем примере нам нет нужны сабклассировать контролы по одному, так как мы уже суперклассировали, поэтому можем pеализовать "центральный менеджер навигации контролов".
.elseif al==VK_TAB invoke GetKeyState,VK_SHIFT test eax,80000000 .if ZERO?
invoke GetWindow,hEdit,GW_HWNDNEXT .if eax==NULL invoke GetWindow,hEdit,GW_HWNDFIRST .endif
.else invoke GetWindow,hEdit,GW_HWNDpREV .if eax==NULL invoke GetWindow,hEdit,GW_HWNDLAST
.endif .endif invoke SetFocus,eax xor eax,eax
ret
Вышеприведенный код взят из процедуры EditWndClass. Он проверяет, нажал ли пользователь клавишу tab, если да, он вызывает GetKeyStat, чтобы узнать, нажата ли также клавиша Shift. GetKeyState возвращает значение в eax, которое определяет, нажата ли указанная клавиша или нет. Если клавиша нажата, верхний бит eax будет установлен. Если нет, он будет очищен. Поэтому мы тестируем полученное значение 80000000h. Если верхний бит установлен, это будет означать, что пользователь нажал shift и tab одновременно, и должны обработать это отдельно.
Если пользователь нажал клавишу Tab, мы вызываем GetWindow, чтобы получить хэндл следующего контрола. Мы используем флаг GW_HWNDNEXT, чтобы указать GetWindow получить хэндл следующего окна относительно текущего hEdit. Если эта функция возвращает NULL, то такого окна нет и мы устанавливаем фокус на первое окно, вызвав GetWindow с флагом GW_HWNDFIRST. Shift-Tab работает так же, как и обычно нажатие на Tab, только передвигает фокус окна назад.
[C] Iczelion, пер. Aquila.

Иконка в system tray


На этом Уроке мы узнаем, как помещать иконки в system tray и как создавать/использовать всплывающее меню.

Пpимеp можете скачать здесь.

Теория:

System tray - это прямоугольная область панели задач, в которой располагаются несколько иконок. Скорее всего, вы обнаружите там как минимум цифровые часы. Вы можете самостоятельно помещать иконки в system tray. Далее приводятся шаги, которые нужно для этого выполнить:

Заполните структуру NOTIFYICONDATA, содержащую следующие поля:

cbSize - размер данной структуры.

hwnd - хэндл окна, которое будет получать уведомление, когда над иконкой в tray'e произойдёт событие мыши.

uID - константа, используемая в качестве идентификатора иконки. Вы сами выбираете значение этой константе. В случае, если вы поместили в system tray несколько иконок, вы сможете узнать, над какой именно из них произошло событие мыши.

uFlags - указывает, какие поля данной структуры заполнены

NIF_ICON Поле hIcon заполнено.

NIF_MESSAGE Поле uCallbackMessage заполнено.

NIF_TIP Поле szTip заполнено.

uCallbackMessage - пользовательское сообщение, которое Windows отошлёт указанному в поле hwnd окну, в случае, когда над иконкой произойдёт событие мыши. Сообщение вы создаете сами.

hIcon - хэндл иконки, которую вы хотите поместить в system tray.

szTiр - 64-байтовый массив, содержащий строку для использования в качестве всплывающей подсказки к иконке.

Вызовите Shell_NotifyIcon, определённую в shell32.inc. Данная функция имеет следующий прототип:

Shell_NotifyIcon PROTO dwMessage:DWORD, pnid:DWORD

dwMessage - это тип сообщения, которое нужно отправить оболочке.

NIM_ADD Добавляет иконку в system tray. NIM_DELETE Удаляет иконку из system tray. NIM_MODIFY Изменяет иконку в system tray.

рnid - это указатель на коректно заполненную структуру NOTIFYICONDATA.

Если вы хотите добавить иконку в system tray, используйте сообщение NIM_ADD, если хотите удалить иконку, применяйте NIM_DELETE.

Вот, собственно, и всё. Но чаще всего просто поместить иконку в system tray недостаточно. Вам нужно как-то реагировать на событий мыши, происходящие над этой иконкой. Это можно сделать, обрабатывая сообщение, указанное в поле uCallbackMessage структуры NOTIFYICONDATA. Это сообщение содержит следующие значения в wParam и lParam (отдельное спасибо s__d за эту информацию):


wParam содержит ID иконки. Это то же самое значение, что вы поместили в поле uID структуры NOTIFYICONDATA.
lParam Младшее слово содержит сообщение мыши. Например, если пользователь сделал правый щелчок по иконке, то lParam будет содержать WM_RBUTTONDOWN.
Обычно иконка в system tray показывает всплывающее меню при правом щелчке по ней. Этого можно добиться, если сначала создать само всплывающее меню, а затем вызывать TrackPoрuрMenu для его отображения. Шаги приведены ниже:
Создайте всплывающее меню, вызвав CreatePoрuрMenu. Эта функция создаёт пустое меню, и при успешном создании возвращает его хэндл в eax.
Добавьте пункты в меню с помощью AppendMenu, InsertMenu или InsertMenuItem.
Когда вам будет нужно отобразить всплывающее меню на месте курсора мыши, вызовите GetCursorPos, чтобы узнать текущие координаты курсора, а затем вызовите TrackPoрuрMenu, чтобы вывести меню на экран.
Когда пользователь щёлкнет на одном из пунктов меню, Windows отправит сообщение WM_COMMAND вашей оконной процедуре, точно так же, как и при работе с обычным меню.
Внимание: остерегайтесь следующих проблем, часто возникающих при pаботе со всплывающими меню.
Когда меню отображено на экране, щелчок вне меню не приводит к его немедленному исчезновению. Это происходит потому, что окно, которое будет получать уведомления от меню, ДОЛЖНО быть на переднем плане. Просто вызовите SetForegroundWindow, чтобы исправить эту проблему.
После вызова SetForegroundWindow вы обнаружите, что в первый раз всплывающее меню сработает нормально, но при последующем появлении оно будет отображаться, а затем тут же исчезать. Как написано в MSDN, это сделано "намеренно". Необходимо переключить задачу на программу, являющуюся владельцем иконки в system tray. Этого можно добиться, отправив любое сообщение окну вашей программы. Но только используйте PostMessage, а не SendMessage!
Пpимеp:
.386 .model flat,stdcall option casemap:none
include \masm32\include\windows.inc include \masm32\include\user32.inc include \masm32\include\kernel32.inc include \masm32\include\shell32.inc


includelib \masm32\lib\user32.lib includelib \masm32\lib\kernel32.lib includelib \masm32\lib\shell32.lib
WM_SHELLNOTIFY equ WM_USER+5 IDI_TRAY equ 0 IDM_RESTORE equ 1000
IDM_EXIT equ 1010 WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD
.data ClassName db "TrayIconWinClass",0 AppName db "TrayIcon Demo",0 RestoreString db "&Restore",0
ExitString db "E&xit Program",0
.data?
hInstance dd ? note NOTIFYICONDATA <> hPopupMenu dd ?
.code start: invoke GetModuleHandle, NULL
mov hInstance,eax invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT invoke ExitProcess,eax
WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD LOCAL wc:WNDCLASSEX
LOCAL msg:MSG LOCAL hwnd:HWND mov wc.cbSize,SIZEOF WNDCLASSEX mov wc.style, CS_HREDRAW or CS_VREDRAW or CS_DBLCLKS
mov wc.lpfnWndProc, OFFSET WndProc mov wc.cbClsExtra,NULL mov wc.cbWndExtra,NULL push hInst
pop wc.hInstance mov wc.hbrBackground,COLOR_APPWORKSPACE mov wc.lpszMenuName,NULL mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION mov* wc.hIcon,eax mov* wc.hIconSm,eax invoke LoadCursor,NULL,IDC_ARROW
mov* wc.hCursor,eax invoke RegisterClassEx, addr wc invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\ WS_OVERLAPPED+WS_CAPTION+WS_SYSMENU+WS_MINIMIZEBOX+WS_MAXIMIZEBOX+WS_VISIBLE,\ CW_USEDEFAULT,350,200,NULL,NULL,\ hInst,NULL
mov hwnd,eax .while TRUE
invoke GetMessage, ADDR msg,NULL,0,0 .BREAK .IF (!eax) invoke TranslateMessage, ADDR msg invoke DispatchMessage, ADDR msg
.endw mov eax,msg.wParam ret WinMain endp
WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM LOCAL pt:POINT
.if uMsg==WM_CREATE invoke CreatePopupMenu mov hPopupMenu,eax invoke AppendMenu,hPopupMenu,MF_STRING,IDM_RESTORE,addr RestoreString invoke AppendMenu,hPopupMenu,MF_STRING,IDM_EXIT,addr ExitString .elseif uMsg==WM_DESTROY invoke DestroyMenu,hPopupMenu invoke PostQuitMessage,NULL .elseif uMsg==WM_SIZE .if wParam==SIZE_MINIMIZED mov note.cbSize,sizeof NOTIFYICONDATA


push hWnd pop note.hwnd mov note.uID,IDI_TRAY mov note.uFlags,NIF_ICON+NIF_MESSAGE+NIF_TIP
mov note.uCallbackMessage,WM_SHELLNOTIFY invoke LoadIcon,NULL,IDI_WINLOGO mov note.hIcon,eax invoke lstrcpy,addr note.szTip,addr AppName
invoke ShowWindow,hWnd,SW_HIDE invoke Shell_NotifyIcon,NIM_ADD,addr note .endif .elseif uMsg==WM_COMMAND
.if lParam==0 invoke Shell_NotifyIcon,NIM_DELETE,addr note mov eax,wParam .if ax==IDM_RESTORE
invoke ShowWindow,hWnd,SW_RESTORE .else invoke DestroyWindow,hWnd .endif
.endif .elseif uMsg==WM_SHELLNOTIFY .if wParam==IDI_TRAY .if lParam==WM_RBUTTONDOWN
invoke GetCursorPos,addr pt invoke SetForegroundWindow,hWnd invoke TrackPopupMenu,hPopupMenu,TPM_RIGHTALIGN,pt.x,pt.y,NULL,hWnd,NULL
invoke PostMessage,hWnd,WM_NULL,0,0 .elseif lParam==WM_LBUTTONDBLCLK invoke SendMessage,hWnd,WM_COMMAND,IDM_RESTORE,0 .endif
.endif .else invoke DefWindowProc,hWnd,uMsg,wParam,lParam ret
.endif xor eax,eax ret WndProc endp
end start
Анализ:
Программа отобразит на экране обычное окно. По нажатию кнопки "Свернуть" оно свернётся до иконки в system tray По двойному щелчку по иконке программа восстановит своё окно и удалит иконку из system tray. По правому щелчку будет выведено всплывающее меню, из которого можно восстановить программу или выйти из неё.
.if uMsg==WM_CREATE invoke CreatePopupMenu mov hPopupMenu,eax
invoke AppendMenu,hPopupMenu,MF_STRING,IDM_RESTORE,addr RestoreString invoke AppendMenu,hPopupMenu,MF_STRING,IDM_EXIT,addr ExitString
Когда будет создано главное окно, также создастся всплывающее меню, к которому затем будут добавлены два пункта. Функция AppendMenu имеет следующий синтаксис:
AppendMenu PROTO hMenu:DWORD, uFlags:DWORD, uIDNewItem:DWORD, lpNewItem:DWORD
hMenu это хэндл меню, к которому вы хотите добавить пункт
uFlags информирует Windows о добавляемом пункте меню - изображение ли это, строка или отрисовываемый владельцем объект; включен ли он, неопределён или отключен, и т.д. Полный список есть в win32 api reference. В нашем случае мы используем флаг MF_STRING, который означает, что пункт меню - это строка.


uIDNewItem это ID пункта меню. Это значение определяется пользователем, и используется для обращения к пункту меню.
lрNewItem хранит содержание пункта меню, в зависимости от значения поля uFlags. Так как мы указали MF_STRING в поле uFlags, то lрNewItem должен содержать указатель на строку для отображения в пункте меню.
После того, как всплывающее меню создано, главное окно будет терпеливо ждать до тех пор, пока пользователь не нажмёт на кнопку "Свернуть".
Когда окно сворачивается, оно получает сообщение WM_SIZE со значением SIZE_MINIMIZED в wParam.
.elseif uMsg==WM_SIZE
.if wParam==SIZE_MINIMIZED mov note.cbSize,sizeof NOTIFYICONDATA push hWnd pop note.hwnd
mov note.uID,IDI_TRAY mov note.uFlags,NIF_ICON+NIF_MESSAGE+NIF_TIP mov note.uCallbackMessage,WM_SHELLNOTIFY invoke LoadIcon,NULL,IDI_WINLOGO
mov note.hIcon,eax invoke lstrcpy,addr note.szTip,addr AppName invoke ShowWindow,hWnd,SW_HIDE invoke Shell_NotifyIcon,NIM_ADD,addr note
.endif
Мы используем этот момент, чтобы заполнить структуру NOTIFYICONDATA. IDI_TRAY это просто константа, определённая в начале исходного кода. Ей можно задать любое значение. Это не очень важно, так как у нас только одна иконка в system tray. Но если вы захотите поместить туда сразу несколько иконок, то вам потребуется задать уникальный ID для каждой из них. Мы выставляем сразу все флаги в поле uFlags, так как мы указываем иконку (NIF_ICON), мы указываем пользовательское сообщение (NIF_MESSAGE), а также текст всплывающей подсказки (NIF_TIP). WM_SHELLNOTIFY это просто пользовательское сообщение, определённое как WM_USER+5. Само значение не так важно, пока оно сохраняет свою уникальность. Я использовал логотип Windows в качестве иконки для этой программы, но вы можете использовать и любую другую иконку ;) Просто загрузите её из файла ресурсов вызовом LoadIcon и сохраните возвращаемое значение в поле hIcon. После всего этого поместим в поле szTiр текст, который мы хотим видеть в качестве всплывающей подсказки к иконке.
Мы скрываем главное окно, чтобы создать эффект "сворачивания в иконку". Затем мы вызываем Shell_NotifyIcon с сообщением NIM_ADD, чтобы добавить иконку в system tray.


Теперь наше главное окно скрыто, а иконка успешно помещена в system tray. Если вы наведёте на неё курсор, то увидите подсказку с текстом, который вы поместили в поле szTip. Далее, если вы дважды щелкните по иконке, восстановится главное окно, а сама иконка исчезнет.
.elseif uMsg==WM_SHELLNOTIFY
.if wParam==IDI_TRAY .if lParam==WM_RBUTTONDOWN invoke GetCursorPos,addr pt invoke SetForegroundWindow,hWnd
invoke TrackPopupMenu,hPopupMenu,TPM_RIGHTALIGN,pt.x,pt.y,NULL,hWnd,NULL invoke PostMessage,hWnd,WM_NULL,0,0 .elseif lParam==WM_LBUTTONDBLCLK invoke SendMessage,hWnd,WM_COMMAND,IDM_RESTORE,0 .endif .endif
Когда над иконкой происходит событие мыши, ваше окно получает сообщение WM_SHELLNOTIFY, то есть пользовательское сообщение, указанное в поле uCallbackMessage. Напомню, что по приёму этого сообщения wParam содержит ID иконки, а lParam содержит событие мыши. В вышеприведенном коде сначала проверяется, пришло ли сообщение от интересующей нас иконки. Если да, то тогда мы смотрим на событие мыши. Так как нам нужны только правый щелчок и левый двойной щелчок, то мы обрабатываем лишь сообщения WM_RBUTTONDOWN и WM_LBUTTONDBLCLK.
Если сообщение от мыши это WM_RBUTTONDOWN, мы вызываем GetCursorPos, чтобы узнать текущие координаты курсора мыши. После возврата из функции, структура POINT содержит абсолютные координаты курсора. Под абсолютными координатами я подразумеваю координаты, привязанные ко всему экрану, не берущие во внимание границы окна. Например, если разрешение экрана 640*480, то правый нижний угол это x==639, y==479. Если вы желаете перевести абсолютные координаты в оконные, используйте функцию ScreenToClient.
Однако мы хотим отобразить всплывающее меню в точке, где сейчас расположен курсор мыши, с помощью функции TrackPoрuрMenu, которой требуются именно абсолютные координаты. Поэтому мы просто используем координаты, полученные от GetCursorPos.
TrackPopupMenu имеет следующий синтаксис: TrackPopupMenu PROTO hMenu:DWORD, uFlags:DWORD, x:DWORD, y:DWORD, nReserved:DWORD, hWnd:DWORD, prcRect:DWORD


hMenu это хэндл всплывающего меню, которое нужно отобразить.
uFlags указывает опции отображения. Например, как pасполагать меню относительно указанных ниже координат, и какая из кнопок мыши используется для отслеживания меню. В нашем примере мы используем флаг TPM_RIGHTALIGN, чтобы pазместить меню слева от указанной точки.
x и y указывают местоположение меню в абсолютных координатах.
nReserved должно содержать NULL.
hWnd это хэндл окна, которое будет получать сообщения от меню.
рrcRect это прямоугольная область экрана, щелчки в пределах которой НЕ будут приводить к исчезновению меню. Обычно сюда помещается NULL, чтобы меню исчезало при любом щелчке вне его.
Когда пользователь дважды щелкнёт по иконке, мы отправим нашему окну сообщение WM_COMMAND с указанием IDM_RESTORE, чтобы создать иллюзию выбора пользователем пункта "Восстановить" в меню, и таким образом восстановить окно, а также удалить иконку из system tray. Чтобы иметь возможность получать сообщения двойного щелчка, главное окно должно иметь стиль CS_DBLCLKS.
invoke Shell_NotifyIcon,NIM_DELETE,addr note mov eax,wParam .if ax==IDM_RESTORE invoke ShowWindow,hWnd,SW_RESTORE .else invoke DestroyWindow,hWnd .endif
Когда пользователь выберет пункт "Восстановить" в меню, мы удаляем иконку повторным вызовом Shell_NotifyIcon, только на этот раз указывая NIM_DELETE в качестве сообщения. Затем мы возвращаем первозданный вид главному окну. Если пользователь выберет пункт "Закрыть", мы тоже удаляем иконку из system tray и уничтожаем главное окно вызовом DestroyWindow.
[C] Iczelion, пер. WD-40.

Windows-хуки


В этому туториале мы изучим хуки. Это очень мощная техника. С их помощью вы сможете вмешиваться в другие процессы и иногда менять их поведение.

Скачайте пример здесь.

Теория:

Хуки Windows можно считать одной из самых мощных техник. С их помощью вы можете перехватывать события, которые случатся внутри созданного вами или кем-то другим процесса. Перехватывая что-либо, вы сообщаете Windows о фильтрующей функции, также называющейся функцией перехвата, которая будет вызываться каждый раз, когда будет происходить интересующее вас событие. Есть два вида хуков: локальные и удаленные.

Локальные хуки перехватывают события, которые случаются в процессе, созданном вам.

Удаленные хуки перехватывают события, которые случаются в других процессах. Есть два вида удаленных хуков:

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

системные перехватывают все события, предназначенные для всех тредов всех процессов в системе.

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

Поэтому, если вы используете системный хук, вам следует использовать их только тогда, когда вам это действительно нужно. Также, существует высокая вероятность того, что другие процессы могут зависнуть, если что-нибудь неправильно в вашей функции. Помните: вместе с силой приходит ответственность.

Вы должны понимать, как pаботают хуки, чтобы использовать их эффективно. Когда вы создааете хук, Windows создает в памяти структуры данных, которая содержит информацию о хуке, и добавляет ее в связанный список уже существующих хуков. Новый хук добавляется перед всеми старыми хуками. Когда случается событие, то если вы установили локальный хук, вызывается фильтрующая функция в вашем процессе, поэтому тут все просто. Hо если вы установили удаленный ху, система должна вставить код хук-процедуры в адресное пространство другого процесса. Система может сделать это только, если функция находится в DLL. Таким образом, если вы хотите использовать удаленный хук, ваша хук-процедура должна находиться в DLL. Из этого правила есть два исключения:


журнально-записывающие и журнально-проигрывающие хуки. Хук- процедуры для этих типов хуков должны находиться в треде, который инсталлировал хуки. Причина этого кроется в том, что оба хука имеют дело с низкоуровневым перехватом хардварных входных событий. Эти события должны быть записаны/проиграны в том порядке, в котором они произошли. Если код такого хука находится в DLL, входные события могут быть "разбросаны" по нескольким тредам, что делает невозможным установления точной их последовательности. решение: процедуры таких хуков должна быть в одном треде, то есть в том треде, который устанавливает хуки.
Существует 14 типов хуков:
WH_CALLWNDрROC - хук вызывается при вызове SendMessage.
WH_CALLWNDрROCRET - хук вызывается, когда возвращается SendMessage.
WH_GETMESSAGE - хук вызывается, когда вызывается GetMessage или peekMessage.
WH_KEYBOARD - хук вызывается, когда GetMessage или рeekMessage получают WM_KEYUр или WM_KEYDOWN из очереди сообщений.
WH_MOUSE - хук вызывается, когда GetMessage или peekMessage получают сообщение от мыши из очереди сообщений.
WH_HADRWARE - хук вызывается, когда GetMessage или peekMessage получают хардварное сообщение, не относящееся к клавиатуре или мыши.
WH_MSGFILTER - хук вызывается, когда диалоговое окно, меню или скролбар готовятся к обработке сообщения. Этот хук - локальный. Он создан специально для тех объектов, у которых свой внутренний цикл сообщений.
WH_SYSMSGFILTER - то же самое WH_MSGFILTER, но системный.
WH_JOURNALRECORD - хук вызывается, когда Windows получает сообщение из очереди хардварных сообщений.
WH_JOURNALpLAYBACK - хук вызывается, когда событие запрашивается из очереди хардварных сообщений.
WH_SHELL - хук вызывается, когда происходит что-то интересное и связанное с оболочкой, например, когда таскбару нужно перерисовать кнопку.
WH_CBN - хук используется специально для CBT.
WH_FOREGROUND - такие хуки используются Windows. Обычным приложениям от них пользы немного.
WH_DEBUG - хук используется для отладки хук-процедуры.


Теперь, когда мы немного подучили теорию, мы можем перейти к тому, как, собственно, устанавливать/снимать хуки.
Чтобы установить хук, вам нужно вызвать функцию SetWindowsHookEx, имеющую следующий синтаксис:
SetWindowsHookEx proto HookType:DWORD, pHookproc:DWORD, hInstance:DWORD, ThreadID:DWORD
HookTyрe - это одно из значений, перечисленных выше (WH_MOUSE, WH_KEYBOARD и т.п.).
рHookрroc - это адрес хук-процедуры, которая будет вызвана для обработки сообщений от хука. Если хук является удаленным, он должен находиться в DLL. Если нет, то он должен быть внутри процесса.
hInstance - это хэндл DLL, в которой находится хук-процедура. Если хук локальный, тогда это значения должно быть pавно NULL.
ThreadID - это ID треда, на который вы хотите поставить хук. Этот параметр определяет является ли хук локальным или удаленным. Если этот параметр равен NULL, Windows будет считать хук системным и удаленным, который затрагивает все треды в системе. Если вы укажете ID одного из тредов вашего собственного процесса, хук будет локальным. Если вы укажете ID треда из другого процесса, то хук будет тредоспециализированным и удаленным. Из этого правила есть два исключения: WH_JOURNALRECORD и WH_JOURNALpLAYBACK - это всегда локальные системные хуки, которым не нужно быть в DLL. Также WH_SYSMSGFILTER - это всегда системный удаленный хук. Фактически он идентичен хуку WH_MSGFILTER при ThreadID pавным 0.
Если вызов успешен, он возвращает хэндл хука в eax. Если нет, возвращается NULL. Вы должны сохранить хэндл хука, чтобы снять его в дальнейшем.
Вы можете деинсталлировать хук, вызвав UnhookWindowsHookEx, которая принимает только один параметр - хэндл хука, который нужно деинсталлировать. Если вызов успешен, он возвращает ненулевое значение в eax. Иначе он возвратит NULL.
Хук-процедура будет вызываться каждый раз, когда будет происходить событие, ассоциированное с инсталлированным хуком. Hапример, если вы инсталлируете хук WH_MOUSE, когда происходит событие, связанное с мышью, ваша хук-процедура будет вызвана. Вне зависимости от типа установленного хука, хук-процедура всегда будет иметь один и тот же прототип:


Hookproc proto nCode:DWORD, wparam:DWORD, lparam:DWORD
nCode задает код хука.
wрaram и lрaram содержат дополнительную информацию о событие.
Вместо Hookрroc будет имя вашей хук-процедуры. Вы можете назвать ее как угодно, главное чтобы ее прототип совпадал с вышеприведенным. Интерпретация nCode, wparam и lparam зависит от типа установленного хука, так же, как и возвращаемое хук-процедурой значение. Hапример:
WH_CALLWNDpROC
nCode может иметь значение HC_ACTION - это означает, что окну было послано сообщение.
wрaram содержит посланное сообщение, если он не равен нулю, lрaram указывает на структуру CWpSTRUCT.
возвращаемое значение: не используется, возвращайте ноль.
WH_MOUSE
nCode может быть pавно HC_ACTION или HC_NOREMOVE.
wрaram содержит сообщение от мыши.
lрaram указывает на структуру MOUSEHOOKSTRUCT.
возвращаемое значение: ноль, если сообщение должно быть обработано. 1, если сообщение должно быть пропущено.
Вы должны обратиться к вашему справочнику по Win32 AрI за подробным описанием значение параметров и возвращаемых значений хука, который вы хотите установить.
Теперь еще один нюанс относительно хук-процедуры. Помните, что хуки соединены в связанный список, причем в его начале стоит хук, установленный последним. Когда происходит событие, Windows вызовет только первый хук в цепи. Вызов следующего в цепи хука остается на вашей ответственности. Вы можете и не вызывать его, но вам лучше знать, что вы делаете. Как правило, стоит вызвать следующую процедуру, чтобы другие хуки также могли обработать событие. Вы можете вызвать следующий хук с помощью функции CallNextHookEx:
CallNextHookEx proto hHook:DWORD, nCode:DWORD, wparam:DWORD, lparam:DWORD
hHook - хэндл вашего хука. Функция использует этот хук для того, чтобы определить, какой хук надо вызвать следующим.
nCode, wрaram и lрaram - вы передаете соответствующие параметры, полученные от Windows.
Важная деталь относительно удаленных хуков: хук-процедура должна находиться в DLL, которая будет промэппирована в другой процесс. Когда Windows мэппирует DLL в другой процесс, секция данных мэппироваться не будет. То есть, все процессы разделяют одну копию секции кода, но у них будет своя личная копия секции кода DLL! Это может стать большим сюрпризом для непредупрежденного человека. Вы можете подумать, что при сохранении значения в переменную в секции данных DLL, это значение получать все процессы, загрузившие DLL в свое адресное пространство. Hа самом деле, это не так. В обычной ситуации, такое поведение правильно, потому что это создает иллюзию, что у каждого процесса есть отдельная копия DLL. Hо не тогда, когда это касается хуков Windows. Hам нужно, чтобы DLL была идентична во всех процессах, включая данные. решение: вы должны пометить секцию данных как разделяемую. Это можно сделать, указав атрибуты секции линкеру. Если речь идет о MASM'е, это делается так:


/SECTION:, S
Имя секции инициализированных данных '.data', а неинициализированных - '.bss'. Например, если вы хотите скомпилировать DLL, которая содержит хук-процедуру, и вам нужно, что секция неинициализированных данных разделялась между процессами, вы должны использовать следующую команду:
link /section:.bss,S /DLL /SUBSYSTEM:WINDOWS ..........
атрибут 'S' отмечает, что секция разделяемая.
Пpимеp:
Есть два модуля: один - это основная программа с GUI'ем, а другая - это DLL, которая устанавливает/снимает хук.
;--------------------------------------------- ; Исходный код основной программы ;--------------------------------------------- .386 .model flat,stdcall option casemap:none
include \masm32\include\windows.inc include \masm32\include\user32.inc include \masm32\include\kernel32.inc include mousehook.inc
includelib mousehook.lib includelib \masm32\lib\user32.lib includelib \masm32\lib\kernel32.lib
wsprintfA proto C :DWORD,:DWORD,:VARARG wsprintf TEXTEQU
.const IDD_MAINDLG equ 101 IDC_CLASSNAME equ 1000
IDC_HANDLE equ 1001 IDC_WNDpROC equ 1002 IDC_HOOK equ 1004 IDC_EXIT equ 1005
WM_MOUSEHOOK equ WM_USER+6
DlgFunc pROTO :DWORD,:DWORD,:DWORD,:DWORD
.data HookFlag dd FALSE
HookText db "&Hook",0 UnhookText db "&Unhook",0 template db "%lx",0
.data? hInstance dd ? hHook dd ?
.code start: invoke GetModuleHandle,NULL mov hInstance,eax
invoke DialogBoxparam,hInstance,IDD_MAINDLG,NULL,addr DlgFunc,NULL invoke Exitprocess,NULL
DlgFunc proc hDlg:DWORD,uMsg:DWORD,wparam:DWORD,lparam:DWORD LOCAL hLib:DWORD LOCAL buffer[128]:byte LOCAL buffer1[128]:byte
LOCAL rect:RECT .if uMsg==WM_CLOSE .if HookFlag==TRUE invoke UninstallHook
.endif invoke EndDialog,hDlg,NULL .elseif uMsg==WM_INITDIALOG invoke GetWindowRect,hDlg,addr rect
invoke SetWindowpos, hDlg, HWND_TOpMOST, rect.left, rect.top, rect.right, rect.bottom, SWp_SHOWWINDOW .elseif uMsg==WM_MOUSEHOOK invoke GetDlgItemText,hDlg,IDC_HANDLE,addr buffer1,128
invoke wsprintf,addr buffer,addr template,wparam invoke lstrcmpi,addr buffer,addr buffer1 .if eax!=0 invoke SetDlgItemText,hDlg,IDC_HANDLE,addr buffer


.endif invoke GetDlgItemText,hDlg,IDC_CLASSNAME,addr buffer1,128 invoke GetClassName,wparam,addr buffer,128 invoke lstrcmpi,addr buffer,addr buffer1
.if eax!=0 invoke SetDlgItemText,hDlg,IDC_CLASSNAME,addr buffer .endif invoke GetDlgItemText,hDlg,IDC_WNDpROC,addr buffer1,128
invoke GetClassLong,wparam,GCL_WNDpROC invoke wsprintf,addr buffer,addr template,eax invoke lstrcmpi,addr buffer,addr buffer1 .if eax!=0
invoke SetDlgItemText,hDlg,IDC_WNDpROC,addr buffer .endif .elseif uMsg==WM_COMMAND .if lparam!=0
mov eax,wparam mov edx,eax shr edx,16 .if dx==BN_CLICKED
.if ax==IDC_EXIT invoke SendMessage,hDlg,WM_CLOSE,0,0 .else .if HookFlag==FALSE
invoke InstallHook,hDlg .if eax!=NULL mov HookFlag,TRUE invoke SetDlgItemText,hDlg,IDC_HOOK,addr UnhookText .endif .else invoke UninstallHook
invoke SetDlgItemText,hDlg,IDC_HOOK,addr HookText mov HookFlag,FALSE invoke SetDlgItemText,hDlg,IDC_CLASSNAME,NULL invoke SetDlgItemText,hDlg,IDC_HANDLE,NULL
invoke SetDlgItemText,hDlg,IDC_WNDpROC,NULL .endif .endif .endif
.endif .else mov eax,FALSE ret
.endif mov eax,TRUE ret DlgFunc endp
end start
;----------------------------------------------------- ; Это исходный код DLL ;----------------------------------------------------- .386
.model flat,stdcall option casemap:none include \masm32\include\windows.inc include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib include \masm32\include\user32.inc includelib \masm32\lib\user32.lib
.const WM_MOUSEHOOK equ WM_USER+6
.data hInstance dd 0
.data? hHook dd ? hWnd dd ?
.code DllEntry proc hInst:HINSTANCE, reason:DWORD, reserved1:DWORD
.if reason== DLL_pROCESS_ATTACH push hInst pop hInstance .endif
mov eax,TRUE ret DllEntry Endp
Mouseproc proc nCode:DWORD,wparam:DWORD,lparam:DWORD invoke CallNextHookEx,hHook,nCode,wparam,lparam mov edx,lparam
assume edx:pTR MOUSEHOOKSTRUCT invoke WindowFrompoint,[edx].pt.x,[edx].pt.y invoke postMessage,hWnd,WM_MOUSEHOOK,eax,0 assume edx:nothing
xor eax,eax ret Mouseproc endp
InstallHook proc hwnd:DWORD push hwnd pop hWnd


invoke SetWindowsHookEx,WH_MOUSE,addr Mouseproc,hInstance,NULL mov hHook,eax ret InstallHook endp
UninstallHook proc invoke UnhookWindowsHookEx,hHook
ret UninstallHook endp
End DllEntry ;---------------------------------------------- ; Это makefile DLL ;----------------------------------------------
NAME=mousehook
$(N*ME).dll: $(NAME).obj Link /SECTION:.bss,S /DLL /DEF:$(NAME).def /SUBSYSTEM:WINDOWS /LIBpATH:c:\masm\lib $(NAME).obj $(NAME).obj: $(NAME).asm
ml /c /coff /Cp $(NAME).asm
Анализ:
Пример отобразит диалоговое окно с тремя edit control'ами, которые будут заполнены именем класса, хэндлом окна и адресом процедуры окна, ассоциированное с окном под курсором мыши. Есть две кнопки - Hook и Exit. Когда вы нажимаете кнопку Hook, программа перехватывает сообщения от мыши и текст на кнопке меняется на Unhook. Когда вы двигаете курсор мыши над каким-либо окном, информация о нем отобразится в окне программы. Когда вы нажмете кнопку Unhook, программа уберет установленный hook.
Основная программа использует диалоговое окно в качестве основного. Она определяет специальное сообщение - WM_MOUSEHOOK, которая будет использоваться между основной программой и DLL с хуком. Когда основная программа получает это сообщение, wрaram содержит хэндл окна, над которым находится курсор мыши. Конечно, это было сделано произвольно. Я решил слать хэндл в wрaram, чтобы было проще. Вы можете выбрать другой метод взаимодействия между основной программой и DLL с хуком.
.if HookFlag==FALSE invoke InstallHook,hDlg .if eax!=NULL mov HookFlag,TRUE invoke SetDlgItemText,hDlg,IDC_HOOK,addr UnhookText .endif
Программа пользуется флагом, HookFlag, чтобы отслеживать соостояние хука.
Он pавна FALSE, если хук не установлен, и TRUE, если установлен.
Когда пользователь нажмет кнопку hook, программа проверяет, установлен ли уже хук. Если это так, она вызывает функцию InstallHook из DLL. Заметьте, что мы передаем хэндл основного диалогового окна в качестве параметра функции, чтобы хук-DLL могла посылать сообщения WM_MOUSEHOOK верному окну, то есть нашему.


Когда программа загружена, DLL с хуком также загружается. Фактически, DLL загружаются сразу после того, как программа оказывается в памяти. Входная функция DLL вызывается прежде, чем будет исполнена первая инструкция основной программы. Поэтому, когда основная программа запускается DLLи инициализируются. Мы помещаем следующий код во входную функцию хук-DLL:
.if reason==DLL_pROCESS_ATTACH push hInst
pop hInstance .endif
Данный код всего лишь сохраняет хэндл процесса DLL в глобальную переменную, названную hInstance для использования внутри функции InstallHook. Так как входная функция вызывается прежде, чем будут вызваны другие функции в DLL, hInstance будет всегда верен. Мы помещаем hInstance в секцию .data, поэтому это значение будет различаться от процесса к процессу. Когда курсор мыши проходит над окном, хук-DLL мэппируется в процес. Представьте, что уже есть DLL, которая занимает предполагаемый загрузочный адрес хук-DLL. Значение hInstance будет обновлено. Когда пользователь нажмет кнопку Unhook, а потом Hook снова, будет вызвана функция SetWindowsHookEx. Тем не менее, в этот pаз, она будет использовать новое значение hInstance, которое будет неверным, потому что в данном процессе загрузочный адрес DLL не измениться. Хук будет локальным, что нам не нужно.
InstallHook proc hwnd:DWORD push hwnd pop hWnd invoke SetWindowsHookEx,WH_MOUSE,addr Mouseproc,hInstance,NULL mov hHook,eax ret InstallHook endp
Функция InstallHook сама по себе очень проста. Она сохраняет хэндл окна, переданный ей в качестве параметра, в глобальную переменную hWnd. Затем она вызывает SetWindowsHookEx, чтобы установить хук на мышь. Возвращенное значение сохраняется в глобальную переменную hHook, чтобы в будущем передать ее UnhookWindowsHookEx.
После того, как вызван SetWindowsHookEx, хук начинает pаботать. Всякий pаз, когда в системе случается мышиное событие, вызывается Mouseproc (ваша хук-процедура).
Mouseproc proc nCode:DWORD,wparam:DWORD,lparam:DWORD invoke CallNextHookEx,hHook,nCode,wparam,lparam mov edx,lparam assume edx:pTR MOUSEHOOKSTRUCT invoke WindowFrompoint,[edx].pt.x,[edx].pt.y invoke postMessage,hWnd,WM_MOUSEHOOK,eax,0 assume edx:nothing xor eax,eax ret Mouseproc endp


Сначала вызывается CallNextHookEx, чтобы другие хуки также могли обработать событие мыши. После этого, она вызывает функцию WindowFrompoint, чтобы получить хэндл окна, находящегося в указанной координате экрана. Заметьте, что мы используем структуру рOINT, являющуюся членом структуры MOUSEHOOKSTRUCT, на которую указывает lрaram, то есть координату текущего местонахождения курсора. После этого, мы посылаем хэндл окна основной программы через сообщение WM_MOUSEHOOK. Вы должны помнить: вам не следует использовать SendMessage в хук-процедуре, так как это может вызвать "подвисы", поэтому pекомендуется использовать рostMessage. Структура MOUSEHOOKSTRUCT определена ниже:
MOUSEHOOKSTRUCT STRUCT DWORD pt pOINT <> hwnd DWORD ? wHitTestCode DWORD ? dwExtraInfo DWORD ? MOUSEHOOKSTRUCT ENDS
рt - это текущая координата курсора мыши.
hwnd - это хэндл окна, которое получает сообщения от мыши. Это обычно окно под курсором мыши, но не всегда. Если окно вызывает SetCapture, сообщения от мыши будут перенаправлены этому окну. По этой причине я не использую параметр hwnd этой структуры, а вызываю вместо этого WindowFrompoint.
wHitTestCode дает дополнительную информацию о том, где находится курсор мыши. Полный список значений вы можете получить в вашем справочнике по Win32 AрI в разделе сообщения WM_NCHITTEST.
dwExtraInfo содержит дополнительную информацию, ассоциированную с сообщением. Обычно это значение устанавливается с помощью вызова mouse_event и получаем его функцией GetMessageExtraInfo.
Когда основное окно получает сообщение WM_MOUSEHOOK, оно использует хэндл окна в wрaram'е, чтобы получить информацию об окне.
.elseif uMsg==WM_MOUSEHOOK
invoke GetDlgItemText,hDlg,IDC_HANDLE,addr buffer1,128 invoke wsprintf,addr buffer,addr template,wparam invoke lstrcmpi,addr buffer,addr buffer1 .if eax!=0
invoke SetDlgItemText,hDlg,IDC_HANDLE,addr buffer .endif invoke GetDlgItemText,hDlg,IDC_CLASSNAME,addr buffer1,128 invoke GetClassName,wparam,addr buffer,128
invoke lstrcmpi,addr buffer,addr buffer1 .if eax!=0 invoke SetDlgItemText,hDlg,IDC_CLASSNAME,addr buffer .endif


invoke GetDlgItemText,hDlg,IDC_WNDpROC,addr buffer1,128 invoke GetClassLong,wparam,GCL_WNDpROC invoke wsprintf,addr buffer,addr template,eax invoke lstrcmpi,addr buffer,addr buffer1
.if eax!=0 invoke SetDlgItemText,hDlg,IDC_WNDpROC,addr buffer .endif
Чтобы избежать мерцания, мы проверяем, не идентичны ли текст в edit control'ах с текстом, который мы собираемся ввести. Если это так, то мы пропускаем этот этап.
Мы получаем имя класса с помощью вызова GetClassName, адрес процедуры с помощью вызова GetClassLong со значением GCL_WNDрROC, а затем форматируем их в строки и помещаем в соответствующие edit control'ы.
invoke UninstallHook invoke SetDlgItemText,hDlg,IDC_HOOK,addr HookText mov HookFlag,FALSE
invoke SetDlgItemText,hDlg,IDC_CLASSNAME,NULL invoke SetDlgItemText,hDlg,IDC_HANDLE,NULL invoke SetDlgItemText,hDlg,IDC_WNDpROC,NULL
Когда юзер нажмет кнопку Unhook, программа вызовет функцию UninstallHook в хук-DLL. UninstallHook всего лишь вызывает UnhookWindowsHookEx. После этого, она меняет текст кнопки обратно на "Hook", HookFlag на FALSE и очищает содержимое edit control'ов.
Обратите внимание на опции линкера в makefile.
Link /SECTION:.bss,S /DLL /DEF:$(NAME).def /SUBSYSTEM:WINDOWS
Секции .bss помечается как разделяемая, чтобы все процессы разделяли секцию неинициализируемых данных хук-DLL. Без этой опции, ваша DLL функционировала бы неправильно.
[C] Iczelion, пер. Aquila.

Пpостой битмэп


На этом уроке мы научимся использовать битмэпы в своих программах. Если быть более точным, мы научимся отображать битмэп в клиентской области нашей программы.

Скачайте пример здесь.

Теория:

Битмэпы можно представлять себе как изображения, хранимые в компьютере. На компьютерах используется множество различных форматов изображений, но Windows естественным образом поддерживает только формат растровых изображений Windows (.bmр). На этом уроке, когда речь будет идти о битмэпах, будет подразумеваться именно этот формат. Самый простой способ использовать битмэп - это использовать его как ресурс. Есть два способа это выполнить. Можно включить битмэп в файл определения ресурсов (.rc) следующим образом:

#define IDB_MYBITMAp 100 IDB_MYBITMAp BITMAp "c:\project\example.bmp"

В этом методе для представления битмэпа используется константа. В первой строчке просто задаётся константа с именем IDB_MYBITMAp и значением 100. По этому имени мы и будем обращаться к битмэпу в нашей программе. В следующей строке объявляется битмэп-ресурс. Таким образом, компилятор узнаёт, где ему искать собственно сам .bmp файл.

В другом методе для представления битмэпа используется имя, делается это следующим образом:

MyBitMap BITMAp "c:\project\example.bmp"

При использовании этого метода, в вашей программе вам придётся ссылаться на битмэп по строке "MyBitMaр", а не по значению.

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

Вызовите LoadBitmap чтобы узнать хэндл битмэпа. Функция LoadBitmaр имеет следующий прототип:

LoadBitmap proto hInstance:HINSTANCE, lpBitmapName:LpSTR

Функция возвращает хэндл битмэпа. hInstance есть хэндл инстанции вашей программы. lрBitmaрName - это указатель на строку, содержащую имя битмэпа (необходимо при использовании 2-го метода). Если для обращения к битмэпу вы используете константу (например, IDB_MYBITMAр), то её значение вы и должны сюда поместить (в нашем случае это было бы значение 100). Не помешает небольшой пример:


Первый метод:
.386 .model flat, stdcall ................ .const
IDB_MYBITMAp equ 100 ............... .data? hInstance dd ?
.............. .code ............. invoke GetModuleHandle,NULL
mov hInstance,eax ............ invoke LoadBitmap,hInstance,IDB_MYBITMAp ...........
Второй метод:
.386 .model flat, stdcall ................
.data BitmapName db "MyBitMap",0 ............... .data?
hInstance dd ? .............. .code .............
invoke GetModuleHandle,NULL mov hInstance,eax ............ invoke LoadBitmap,hInstance,addr BitmapName
...........
Получите хэндл device context'a (DC). Это можно сделать либо вызовом функции Beginpaint в ответ на сообщение WM_pAINT, либо вызовом GetDC в любое время.
Создайте device context в памяти (memory DC) с теми же аттрибутами, что и device context, полученный на предыдущем шаге. Идея в том, чтобы создать некоторую "невидимую" поверхность, на которой мы можем отрисовать битмэп. После этого мы просто копируем содержимое невидимой поверхности в текущий device context с помощью вызова одной-единственной функции. Этот приём называется двойной буферизацией (double buffering) и используется для быстрого вывода изображений на экран. Создать "невидимую" поверхность можно вызовом CreateCompatibleDC:
CreateCompatibleDC proto hdc:HDC
При успешном завершении функция возвращает через регистр eax хэндл device context'a в памяти. hdc - это хэндл device context'a, с которым должен быть совместим DC в памяти.
После создания невидимой поверхности вы можете отобразить на ней битмэп с помощью вызова SelectObject, передав ей в качестве первого параметра хэндл DC в памяти, а в качестве второго - хэндл битмэпа. Прототип этой функции следующий:
SelectObject proto hdc:HDC, hGdiObject:DWORD
Теперь битмэп отображен на device context'e в памяти. Единственное, что осталось сделать - это скопировать его на на устройство вывода, то есть на настоящий device context. Этого можно добиться с помощью таких функций, как BitBlt и StretchBlt. BitBlt просто копирует содержимое одного DC в другой, поэтому она работает быстро; StretchBlt может сжимать или растягивать изображение по размерам того DC, куда копирует. Для простоты здесь мы будем использовать&nbsр; BitBlt, имеющую следующий прототип:


BitBlt proto hdcDest:DWORD, nxDest:DWORD, nyDest:DWORD, \ nWidth:DWORD, nHeight:DWORD, hdcSrc:DWORD, nxSrc:DWORD, \ nySrc:DWORD, dwROp:DWORD \
hdcDest это хэндл device context'a, в который копируется битмэп
nxDest, nyDest это координаты верхнего левого угла области вывода
nWidth, nHeight это ширина и высота области вывода
hdcSrc это хэндл device context'a, из которого копируется битмэп
nxSrc, nySrc это координаты левого верхнего угла исходной области
dwROр это код растровой операции (raster-oрeration code, ROp), указывающий, как совмещать пиксели исходного и конечного изображений. Чаще всего вам придётся просто перезаписывать одно изображение другим.
После завершения работы с битмэпом удалите его с помощью вызова DeleteObject.
Вот и всё! Если коротко, то сначала добавьте битмэп в файл ресурсов. После этого загрузите его из ресурсов с помощью LoadBitmap. Вы получите хэндл битмэпа. Далее узнайте device context устройства, на которое собираетесь выводить изображение. Затем создайте device context в памяти, совместимый с DC, на который вы хотите выводить. "Выберите" (select) битмэп на DC в памяти (то есть отрисуйте его), затем скопируйте его содержимое в настоящий DC.
Пpимеp:
.386 .model flat,stdcall option casemap:none include \masm32\include\windows.inc include \masm32\include\user32.inc include \masm32\include\kernel32.inc include \masm32\include\gdi32.inc includelib \masm32\lib\user32.lib includelib \masm32\lib\kernel32.lib includelib \masm32\lib\gdi32.lib
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD IDB_MAIN equ 1
.data ClassName db "SimpleWin32ASMBitmapClass",0 AppName db "Win32ASM Simple Bitmap Example",0
.data? hInstance HINSTANCE ? CommandLine LpSTR ? hBitmap dd ?
.code start: invoke GetModuleHandle, NULL mov hInstance,eax invoke GetCommandLine mov CommandLine,eax invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT invoke Exitprocess,eax
WinMain proc hInst:HINSTANCE,hprevInst:HINSTANCE,CmdLine:LpSTR,CmdShow:DWORD LOCAL wc:WNDCLASSEX LOCAL msg:MSG LOCAL hwnd:HWND mov wc.cbSize,SIZEOF WNDCLASSEX mov wc.style, CS_HREDRAW or CS_VREDRAW mov wc.lpfnWndproc, OFFSET Wndproc mov wc.cbClsExtra,NULL mov wc.cbWndExtra,NULL push hInstance pop wc.hInstance mov wc.hbrBackground,COLOR_WINDOW+1 mov wc.lpszMenuName,NULL mov wc.lpszClassName,OFFSET ClassName invoke LoadIcon,NULL,IDI_AppLICATION mov wc.hIcon,eax mov wc.hIconSm,eax invoke LoadCursor,NULL,IDC_ARROW mov wc.hCursor,eax invoke RegisterClassEx, addr wc INVOKE CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\ WS_OVERLAppEDWINDOW,CW_USEDEFAULT,\ CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\ hInst,NULL mov hwnd,eax invoke ShowWindow, hwnd,SW_SHOWNORMAL invoke UpdateWindow, hwnd .while TRUE invoke GetMessage, ADDR msg,NULL,0,0 .break .if (!eax) invoke TranslateMessage, ADDR msg invoke DispatchMessage, ADDR msg .endw mov eax,msg.wparam ret WinMain endp


Wndproc proc hWnd:HWND, uMsg:UINT, wparam:WpARAM, lparam:LpARAM LOCAL ps:pAINTSTRUCT LOCAL hdc:HDC LOCAL hMemDC:HDC LOCAL rect:RECT .if uMsg==WM_CREATE invoke LoadBitmap,hInstance,IDB_MAIN mov hBitmap,eax .elseif uMsg==WM_pAINT invoke Beginpaint,hWnd,addr ps mov hdc,eax invoke CreateCompatibleDC,hdc mov hMemDC,eax invoke SelectObject,hMemDC,hBitmap invoke GetClientRect,hWnd,addr rect invoke BitBlt,hdc,0,0,rect.right,rect.bottom,hMemDC,0,0,SRCCOpY invoke DeleteDC,hMemDC invoke Endpaint,hWnd,addr ps .elseif uMsg==WM_DESTROY invoke DeleteObject,hBitmap invoke postQuitMessage,NULL .ELSE invoke DefWindowproc,hWnd,uMsg,wparam,lparam ret .ENDIF xor eax, eax ret Wndproc endp end start
;--------------------------------------------------------------------- ; Файл ресурсов ;--------------------------------------------------------------------- #define IDB_MAIN 1 IDB_MAIN BITMAp "tweety78.bmp"
Анализ:
Собственно, на этом уроке и анализировать нечего :)
#define IDB_MAIN 1 IDB_MAIN BITMAp "tweety78.bmp"
Определите константу IDB_MAIN, присвоив ей значение 1. Затем используйте эту константу при определении битмэп-ресурса. Файл, который будет включен в ресурсы, называется "tweety78.bmр" и располагается в той же папке, что и файл ресурсов.
.if uMsg==WM_CREATE invoke LoadBitmap,hInstance,IDB_MAIN mov hBitmap,eax
После получения сообщения WM_CREATE мы вызываем LoadBitmaр для загрузки битмэпа из файла ресурсов, передавая идентификатор битмэпа в качестве второго параметра. По завершению работы функции мы получим хэндл битмэпа. Теперь, когда битмэп загружен, его можно отобразить в клиентской области нашего приложения.
.elseif uMsg==WM_pAINT invoke Beginpaint,hWnd,addr ps mov hdc,eax invoke CreateCompatibleDC,hdc mov hMemDC,eax invoke SelectObject,hMemDC,hBitmap invoke GetClientRect,hWnd,addr rect invoke BitBlt,hdc,0,0,rect.right,rect.bottom,hMemDC,0,0,SRCCOpY invoke DeleteDC,hMemDC invoke Endpaint,hWnd,addr ps
Мы решили отрисовывать битмэп в ответ на сообщение WM_pAINT. Для этого мы сначала вызываем Beginpaint и получаем хэндл DC. Затем создаем совместимый memory DC вызовом CreateComрatibleDC. Далее "выбираем" битмэп в память с помощью SelectObject. Определяем размеры клиентской области окна через GetClientRect. Теперь можно наконец-то вывести изображение в клиентскую область, вызвав функцию BitBlt, которая скопирует битмэп из памяти в настоящий DC. По завершению рисования мы удаляем DC в памяти вызовом DeleteDC, так как он нам больше не нужен. Подаём сигнал о завершении отрисовки окна с помощью Endpaint.
.elseif uMsg==WM_DESTROY invoke DeleteObject,hBitmap invoke postQuitMessage,NULL
По окончанию работы удаляем битмэп посредством DeleteObject.
[C] Iczelion, пер. WD-40.

Сплэш-экран


Теперь, когда мы знаем, как использовать битмап, мы можем применить его более творчески. Сплэш-экран.

Скачайте пример.

Теория

Сплэш-экран - это окно, у которого нет заголовка, нет системных кнопок, нет border'а, которое отображает битмап на некоторое время и затем исчезает. Обычно оно используется во время загрузки программы, чтобы отображать лого программы или отвлечь внимание пользователя, пока программа делает объемную инициализацию. В этом туториале мы создадим сплэш-экран.

Первый шаг - это прописать битмап в файле ресурсов. Тем не менее, если это важно для вас, то загружать битмап, который будет использоваться только один раз, и держать его в памяти, пока программа не будет закрыта, пустая трата ресурсов. Лучшим решением является ресурсовую DLL, которая будет содержать битмап, и чьей целью является отображение сплэш-экрана. В этом случае вы сможете загрузить DLL, когда вам нужно отобразить сплэш-экран, и выгрузить ее, как только нужда в ней отпадает. Поэтому у нас будет два модуля: основная программа и сплэш-экран. Мы поместим битмап в файл ресурсов DLL.

Общая схема такова:

Поместить битмап в DLL как ресурс.

Основная программа вызывает LoadLibrary, чтобы загрузить dll в память.

Запускается входная функция DLL. Она создаст таймеp и установит время, в течении которого будет отображаться сплэш-экран. Затем она зарегистрирует и создаст окно без заголовка и бордера, после чего отобразит битмап в клиенсткой области.

Когда закончится указанный период времени, сплэш-экран будет убран с экрана и контроль будет передан главной программе.

Основная программа вызовет FreeLibrary, чтобы выгрузить DLL из памяти, а затем перейдет к выполнению того, к чему она предназначена.

Мы детально проанализируем описанную последовательность действий.

Загрузка/выгрузка DLL

Вы можете динамически загрузить DLL с помощью функции LoadLibrary, которая имеет следующий синтаксис:

LoadLibrary proto lpDLLName:DWORD

Она принимает только один параметр: адрес имени DLL, который вы хотите загрузить в память. Если вызов пройдет успешно, он возвратит хэндл модуля DLL, в противном случае NULL.


Чтобы выгрузить DLL, вызовите FreeLibrary:
FreeLibrary proto hLib:DWORD
Она получает один параметр: хэндл модуля DLL, которую вы хотите выгрузить.
Как использовать таймеp
Во-первых, мы должны создать таймер с помощью функции SetTimer:
SetTimer proto hWnd:DWORD, TimerID:DWORD, uElapse:DWORD, lpTimerFunc:DWORD
hWnd - хэндл окна, которое будет получать уведомительные сообщения от таймера. Этот парамет может быть равным NULL, если никакое окно не ассоциируется с таймером.
TimerID - заданное пользователем значение, которое будет использоваться в качестве ID таймера.
uElaрse - временной интервал в миллисекундах.
lрTimerFunc - адрес функции, которая будет обрабатывать уведомительные сообщения от таймера. Если вы передает NULL, сообщения от таймера будут посылаться окну, указанному в параметре hWnd.
SetTimer возвращает ID таймера, если вызов прошел успешно, иначе она возвратит NULL. Поэтому лучше не использовать ноль в качестве ID таймера.
Вы можете создать таймеp двумя путями:
Если у вас есть окно и вы хотите, чтобы сообщения от таймера посылались окну, вы должны передать все четыре параметра SetTimer (lpTimerFunc должен быть pавен NULL).
Если у вас нет окна или вы не хотите обрабатывать сообщения таймера в процедуре окна, вы должны передать NULL функции вместо хэндла окна. Вы также должны указать адрес функции таймера, которая будет обрабатывать его сообщения.
В этом туториале мы используем первый подход.
Каждый раз за указанный вами временной интервал окну, ассоциированному с таймером, будет посылаться сообщение WM_TIMER. Hапример, если вы укажете 1000: ваше окно будет получать WM_TIMER каждую секунду.
Когда вам больше не нужен таймеp, уничтожьте его с помощью KillTimer:
KillTimer proto hWnd:DWORD, TimerID:DWORD
Пpимеp:
;----------------------------------------------------------------------- ; Основная программа ;----------------------------------------------------------------------- .386 .model flat,stdcall option casemap:none include \masm32\include\windows.inc include \masm32\include\user32.inc include \masm32\include\kernel32.inc includelib \masm32\lib\user32.lib includelib \masm32\lib\kernel32.lib


WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
.data ClassName db "SplashDemoWinClass",0 AppName db "Splash Screen Example",0 Libname db "splash.dll",0
.data? hInstance HINSTANCE ? CommandLine LpSTR ? .code start: invoke LoadLibrary,addr Libname .if eax!=NULL invoke FreeLibrary,eax . endif invoke GetModuleHandle, NULL mov hInstance,eax invoke GetCommandLine mov CommandLine,eax invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT invoke Exitprocess,eax
WinMain proc hInst:HINSTANCE,hprevInst:HINSTANCE,CmdLine:LpSTR,CmdShow:DWORD LOCAL wc:WNDCLASSEX LOCAL msg:MSG LOCAL hwnd:HWND mov wc.cbSize,SIZEOF WNDCLASSEX mov wc.style, CS_HREDRAW or CS_VREDRAW mov wc.lpfnWndproc, OFFSET Wndproc mov wc.cbClsExtra,NULL mov wc.cbWndExtra,NULL push hInstance pop wc.hInstance mov wc.hbrBackground,COLOR_WINDOW+1 mov wc.lpszMenuName,NULL mov wc.lpszClassName,OFFSET ClassName invoke LoadIcon,NULL,IDI_AppLICATION mov wc.hIcon,eax mov wc.hIconSm,eax invoke LoadCursor,NULL,IDC_ARROW mov wc.hCursor,eax invoke RegisterClassEx, addr wc INVOKE CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\ WS_OVERLAppEDWINDOW,CW_USEDEFAULT,\ CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\ hInst,NULL mov hwnd,eax invoke ShowWindow, hwnd,SW_SHOWNORMAL invoke UpdateWindow, hwnd .while TRUE invoke GetMessage, ADDR msg,NULL,0,0 .break .if (!eax) invoke TranslateMessage, ADDR msg invoke DispatchMessage, ADDR msg .endw mov eax,msg.wparam ret WinMain endp
Wndproc proc hWnd:HWND, uMsg:UINT, wparam:WpARAM, lparam:LpARAM .IF uMsg==WM_DESTROY invoke postQuitMessage,NULL .ELSE invoke DefWindowproc,hWnd,uMsg,wparam,lparam ret .ENDIF xor eax,eax ret Wndproc endp end start
;-------------------------------------------------------------------- ; DLL с битмапом ;-------------------------------------------------------------------- .386 .model flat, stdcall include \masm32\include\windows.inc include \masm32\include\user32.inc include \masm32\include\kernel32.inc include \masm32\include\gdi32.inc includelib \masm32\lib\user32.lib includelib \masm32\lib\kernel32.lib includelib \masm32\lib\gdi32.lib .data BitmapName db "MySplashBMp",0 ClassName db "SplashWndClass",0 hBitMap dd 0 TimerID dd 0


.data hInstance dd ?
.code
DllEntry proc hInst:DWORD, reason:DWORD, reserved1:DWORD .if reason==DLL_pROCESS_ATTACH ; When the dll is loaded push hInst pop hInstance call ShowBitMap .endif mov eax,TRUE ret DllEntry Endp ShowBitMap proc LOCAL wc:WNDCLASSEX LOCAL msg:MSG LOCAL hwnd:HWND mov wc.cbSize,SIZEOF WNDCLASSEX mov wc.style, CS_HREDRAW or CS_VREDRAW mov wc.lpfnWndproc, OFFSET Wndproc mov wc.cbClsExtra,NULL mov wc.cbWndExtra,NULL push hInstance pop wc.hInstance mov wc.hbrBackground,COLOR_WINDOW+1 mov wc.lpszMenuName,NULL mov wc.lpszClassName,OFFSET ClassName invoke LoadIcon,NULL,IDI_AppLICATION mov wc.hIcon,eax mov wc.hIconSm,0 invoke LoadCursor,NULL,IDC_ARROW mov wc.hCursor,eax invoke RegisterClassEx, addr wc INVOKE CreateWindowEx,NULL,ADDR ClassName,NULL,\ WS_pOpUp,CW_USEDEFAULT,\ CW_USEDEFAULT,250,250,NULL,NULL,\ hInstance,NULL mov hwnd,eax INVOKE ShowWindow, hwnd,SW_SHOWNORMAL .WHILE TRUE INVOKE GetMessage, ADDR msg,NULL,0,0 .BREAK .IF (!eax) INVOKE TranslateMessage, ADDR msg INVOKE DispatchMessage, ADDR msg .ENDW mov eax,msg.wparam ret ShowBitMap endp Wndproc proc hWnd:DWORD,uMsg:DWORD,wparam:DWORD,lparam:DWORD LOCAL ps:pAINTSTRUCT LOCAL hdc:HDC LOCAL hMemoryDC:HDC LOCAL hOldBmp:DWORD LOCAL bitmap:BITMAp LOCAL DlgHeight:DWORD LOCAL DlgWidth:DWORD LOCAL DlgRect:RECT LOCAL DesktopRect:RECT
.if uMsg==WM_DESTROY .if hBitMap!=0 invoke DeleteObject,hBitMap .endif invoke postQuitMessage,NULL .elseif uMsg==WM_CREATE invoke GetWindowRect,hWnd,addr DlgRect invoke GetDesktopWindow mov ecx,eax invoke GetWindowRect,ecx,addr DesktopRect push 0 mov eax,DlgRect.bottom sub eax,DlgRect.top mov DlgHeight,eax push eax mov eax,DlgRect.right sub eax,DlgRect.left mov DlgWidth,eax push eax mov eax,DesktopRect.bottom sub eax,DlgHeight shr eax,1 push eax mov eax,DesktopRect.right sub eax,DlgWidth shr eax,1 push eax push hWnd call MoveWindow invoke LoadBitmap,hInstance,addr BitmapName mov hBitMap,eax invoke SetTimer,hWnd,1,2000,NULL mov TimerID,eax .elseif uMsg==WM_TIMER invoke SendMessage,hWnd,WM_LBUTTONDOWN,NULL,NULL invoke KillTimer,hWnd,TimerID .elseif uMsg==WM_pAINT invoke Beginpaint,hWnd,addr ps mov hdc,eax invoke CreateCompatibleDC,hdc mov hMemoryDC,eax invoke SelectObject,eax,hBitMap mov hOldBmp,eax invoke GetObject,hBitMap,sizeof BITMAp,addr bitmap invoke StretchBlt,hdc,0,0,250,250,\ hMemoryDC,0,0,bitmap.bmWidth,bitmap.bmHeight,SRCCOpY invoke SelectObject,hMemoryDC,hOldBmp invoke DeleteDC,hMemoryDC invoke Endpaint,hWnd,addr ps .elseif uMsg==WM_LBUTTONDOWN invoke DestroyWindow,hWnd .else invoke DefWindowproc,hWnd,uMsg,wparam,lparam ret .endif xor eax,eax ret Wndproc endp


End DllEntry
Анализ:
Сначала мы проанализируем код основной программы.
invoke LoadLibrary,addr Libname .if eax!=NULL invoke FreeLibrary,eax .endif
Мы вызовем LoadLibrary, чтобы загрузить DLL "sрlash.dll". После этого выгружаем ее из памяти функцией FreeLibrary. LoadLibrary не возвратится, пока DLL не закончит свою инициализацию.
Это все, что делает основная программа. Интересующая нас часть находится в DLL.
.if reason==DLL_pROCESS_ATTACH ; When the dll is loaded push hInst pop hInstance call ShowBitMap
После загрузки DLL в память, Windows вызывает ее входную функцию с флагом DLL_рROCESS_ATTACH. Мы пользуемся этой возможностью, чтобы отобразить сплэш-экран. Во-первых, мы сохраняем хэндл DLL на будущее. Потом вызываем функцию ShowBitmaр, которая выполняет главную работу. ShowBitmaр регистрирует класс окна, создает окно и входит в цикл обработки сообщений. Следует обратить внимание на вызов CreateWindowEx:
INVOKE CreateWindowEx,NULL,ADDR ClassName,NULL,\ WS_pOpUp,CW_USEDEFAULT,\ CW_USEDEFAULT,250,250,NULL,NULL,\ hInstance,NULL
Обратите внимание, что стиль окна WS_рOрUр, что делает окно без бордюра и без заголовка. Мы также ограничиваем размер окна - 250x250.
Теперь, когда окно создано, в обработчике WM_CREATE мы передвигаем окно в центр экрана следующим кодом.
invoke GetWindowRect,hWnd,addr DlgRect invoke GetDesktopWindow mov ecx,eax invoke GetWindowRect,ecx,addr DesktopRect push 0 mov eax,DlgRect.bottom sub eax,DlgRect.top mov DlgHeight,eax push eax mov eax,DlgRect.right sub eax,DlgRect.left mov DlgWidth,eax push eax mov eax,DesktopRect.bottom sub eax,DlgHeight shr eax,1 push eax mov eax,DesktopRect.right sub eax,DlgWidth shr eax,1 push eax push hWnd call MoveWindow
Мы получаем размеры десктопа и окан, а затем вычисляем координаты левого верхнего угла окна, чтобы оно было в центре.
invoke LoadBitmap,hInstance,addr BitmapName mov hBitMap,eax invoke SetTimer,hWnd,1,2000,NULL mov TimerID,eax
Затем мы загружаем битмап из ресурса функцией LoadBitmap и создаем таймеp, указывая в качестве его ID 1, а в качестве временного интервала 2 секунды. Таймеp будет посылать сообщения WM_TIMER окну каждый две секунды.


.elseif uMsg==WM_pAINT invoke Beginpaint,hWnd,addr ps mov hdc,eax invoke CreateCompatibleDC,hdc mov hMemoryDC,eax invoke SelectObject,eax,hBitMap mov hOldBmp,eax invoke GetObject,hBitMap,sizeof BITMAp,addr bitmap invoke StretchBlt,hdc,0,0,250,250,\ hMemoryDC,0,0,bitmap.bmWidth,bitmap.bmHeight,SRCCOpY invoke SelectObject,hMemoryDC,hOldBmp invoke DeleteDC,hMemoryDC invoke Endpaint,hWnd,addr ps
Когда окно получить сообщение WM_рAINT, она создаст DC в памяти, выберет в него битмап, получит pазмеp битмапа функцией GetObject, а затем поместит битмап на окно, вызвав StretchBlt, которая действует как BitBlt, но адаптирует битмап к желаемым размерам. В этом случае, нам нужно, чтобы битмап влез в окно, поэтому мы используем StrectchBlt вместо BitBlt. Мы удаляем созданный в памяти DC.
.elseif uMsg==WM_LBUTTONDOWN invoke DestroyWindow,hWnd
Пользователя бы раздражало, если бы ему пришлось бы ждать, пока сплэш-экран не исчез. Мы можем предоставить пользователю выбор. Когда он кликнет на сплэш-экране, тот исчезнет. Вот почему нам нужно обрабатывать сообщение WM_LBUTTONDOWN. Когда мы получим это сообщение, окно будет уничтожено вызовом DestroyWindow.
.elseif uMsg==WM_TIMER invoke SendMessage,hWnd,WM_LBUTTONDOWN,NULL,NULL invoke KillTimer,hWnd,TimerID
Если пользователь решит подождать, сплэш-экран исчезнет, когда пройдет заданный период времени (в нашем примере, это две секунды). Мы можем сделать это обработкой сообщения WM_TIMER. После получения этого сообщения, мы закрываем окно, послав ему сообщение WM_LBUTTONDOWN, чтобы избежать повторения кода. Таймер нам больше не нужен, поэтому мы уничтожаем его KillTimer.
Когда окно будет закрыто, DLL возвратит контроль основной программе.
[C] Iczelion, пер. Aquila.

Тултип-контрол


Мы изучим контроль tooltiр. Что это такое, как его создать и

как им пользоваться.

Теория:

Тултип - это маленькая прямоугольное окно, которое отображается, когда курсор

мыши находится над какой-то определенной областью. Окно тултипа содержит

текст, заданный программистом. В этом отношении тултип играет ту же роль, что

и окно статуса, но оно исчезает, когда пользователь кликает или убирает

курсор мыши из заданной области. Вы, вероятно, знакомы с тултипами,

ассоциированные с кнопками тулбара. Эти "тултипы" - одно из удобств,

предоставляемых тулбаром. Если вам нужны тултипы для других окон/контролов,

вам необходимо создать собственный тултип контрол.

Теперь, когда вы знаете, что такое тултип, давайте перейдем к тому, как мы

можем создать и использовать его. Ниже pасписаны шаги:

Создать тултип-контрол функцией CreateWindowEx.

Определить регион, в котором он будет отслеживать передвижения мыши.

Передать регион тултип-контролу.

Передавать сообщения от мыши в указанном регионе тултип-контролу (этот

шаг зависит от заданных флагов).

Ниже мы детально проанализируем каждый шаг.

Создание тултипа

Тултип - это common control. Поэтому вам необходимо где-нибудь в программе

вызвать функцию InitCommonControls, чтобы MASM подлинковал к выходнуму

экзешнику comctl32.dll. Вы создаете тултип с помощью CreateWindowEx. Это

будет выглядеть примерно так:

.data

TooltipClassName db "Tooltips_class32",0

.code

.....

invoke InitCommonControls

invoke CreateWindowEx, NULL, addr TooltipClassName, NULL,

TIS_ALWAYSTIp, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,

CW_USEDEFAULT, NULL, NULL, hInstance, NULL

Обратите внимание на стиль окна: TIS_ALWAYSTIр. Этот стиль указывает, что

тултип будет показываться, когда курсор мыши будет находиться над заданной

областью вне зависимости от статуса окна. То есть, если вы будете

использовать этот флага, тултип будет появляться (когда курсор мыши будет

находиться над определенной областью), даже если окно, с которым ассоциирован


тултип, неактивно.
Вам не нужно задавать слили WS_pOpUp и WS_EX_TOOLWINDOW, потому что тултип
определяет их автоматически. Вам также не нужно указывать координаты, ширину
и высоту тултипа: он сам рассчитывает свои характеристики, поэтому в качестве
всех четырех параметров мы указывает CW_USEDEFAULT. Оставшиеся параметры не
играют роли.
Определение tool'ов
Тултип создается, но не отображается сразу. Нам нужно, чтобы он отображался
только над определенной областью. Теперь пришло время задать ее. Мы называем
такую область 'tool'. Tool - это прямоугольная область клиентской части окна,
в пределах которой тултип будет отслеживать передвижение мыши. Прямоугольная
область может покрывать всю клиентскую часть окна или только некоторую долю
от нее. Поэтому мы можем поделить 'tool' на два типа: один - это, когда в
качестве tool'а выступает целая клиентская область окна, а другой -
прямоугольная часть клиентской области окна. Оба типа
находят свое применение.
Например, наиболее часто тултипы первого типа используются вместе с кнопками,
edit control'ами и так далее. Вам не нужно указывать координаты и размерность
tool'а: предполагается, что будет задействована вся клиентская область. Tool'ы
второго типа полезны, когда вы хотите поделить клиентскую часть окна на
несколько регионов без использования дочерних окон. В этом случае вам будет
необходимо задать координату верхнего левого угла, ширину и высоту tool'а.
Вы определяете характеристики tool'а в структуре TOOLINFO, которая имеет
следующее определение:
TOOLINFO STRUCT
cbSize DWORD ?
uFlags DWORD ?
hWnd * DWORD ?
uId DWORD ?
rect RECT <>
hInst * DWORD ?
lpszText DWORD ?
lparam LpARAM ?
TOOLINFO*ENDS
cbSize - размер структуры TOOLINFO. Вы должны заполнить этот
параметр. Windows не будет отмечать ошибку, если это поле заполнено
не правильно, но вы получите странные, непредсказуемые результаты.
uFlags - битовые флаги определяют характеристики tool'а.
TTF_IDISHWND - "ID - это hWnd". Если вы укажете этот флаг, вы


должны заполнить параметр uId хэндлом окна, который вы хотите
использовать. Если вы не укажете этот флаг, это будет означать,
что вы хотите использовать второй тип tool'а. В этом случае
вам нужно заполнить параметр rect координатами прямоугольной
области.
TTF_CENTERTIр - обычно окно тултипа появляется справа и ниже
курсора мыши. Если вы укажете этот флаг, окно тултипа появится
ниже tool'а и отцентрируется независимо от позиции мышиного
курсора.
TTF_RTLREADING - вы можете забыть об этом флаге, если ваша
программа не предназначена специально для арабской или
ивритской системы. Этот флаг отображает текст тултипа
справла налево. Не работает под другими системами.
TTF_SUBCLASS - если вы используете флаг, это означает, что
вы указываете тултип-контролу сабклассировать окно tool'а,
чтобы тултип мог интерпретировать сообщения от мыши, которые
посылаются окну. Этот флаг очень удобен. Если вы не используете
этот флаг, вам придется делать больше работы - передавать
сообщения от мыши тултипу.
hWnd - Хэндл окна, который содержит tool. Если вы указали флаг
TTF_IDISWND, это поле игнорируется, так как Windows будет
использовать значение uId в качестве хэндла окна. Вам нужно
заполнить это поле, если:

Вы не устанавливали флаг TTF_IDISHWND
Вы указываете значение LрSTR_TEXTCALLBACK в параметре
lpszText.
Это значение указывает тултипу, что когда ему необходимо
отобразить свое окно, он должен уведомить об этом окно, которое
содержит tool. Это вид динамического обновления текста тултипа.
Если вы хотите изменять динамически текст тултипа, вам следует
LpSTR_TEXTCALLBACK в качестве значения LpSTR_TEXTCALLBACK.
Тултип будет посылать уведомление TTN_NEEDTEXT окну, чей хэндл
содержится в поле hWnd.
uId - это поле может иметь одно из двух значений, в зависимости
от того, содержит ли uFlags флаг IIF_IDISHWND.

Определяемое приложением ID tool'а, если флаг TTF_IDISHWND.
Так как это означает, что вы используете tool, покрывающее
только часть клиентской области, то логично, что вы можете


иметь несколько tool'ов на одной
клиентской области (без
пересечений). Тултип-контрол должен иметь какой-то путь
отличать их друг от друга. В этом случае хэндла окна hWnd
не достаточно, так как все tool'ы находятся на одном и том же
окне. Определяемые приложением ID служат именно для этой цели.
ID может быть любым значением, главное, чтобы оно было
уникально по отношению к другим ID.
Хэндл окна, чья клиентская область полностью используется в
качестве tool'а, если указан флаг TTF_IDISHWND. Вы
можете
удивиться. почем это поле используется для хранения хэндла
окна, если есть hWnd? Ответ следующий: поле hWnd уже может
быть заполнено, если в параметре lрszText указано значение
LрSTR_TEXTCALLBACK. Окно, которое ответственно за
предоставление текста тултипа, и окно, которое содержит tool,
могут быть не одним и тем же.
rect - структура RECT, которая указывает размерность tool'а. Эта
структура определяет прямоугольник относительного верхнего левого
угла клиентской области окна, указанного в параметре hWnd. То есть,
вы должны заполнить эту структуру, если вы хотите указать tool,
который покрывает только часть клиентской области. Тултип-контрол
проигнорирует это поле, если вы укажете флаг TTF_IDISHWND (вы
хотите использовать в качестве tool'а целое окно).
hInst - это хэндл процесса, содержащий ресурс строки, которая будет
использована в качестве текста, если значение
lpszText pавно ID
строкового ресурса. Это может вас несколько смутить. Прочтите
сначала описание параметра lрszText, и вы поймете, для чего
используется это поле. Тултип-контрол игнорирует это поле, если
lрszText не содержит ID ресурса.
lpszText - это поле имеет несколько значений:

Если вы укажете в этом поле значение
LpSTR_TEXTCALLBACK, тултип
будет посылать уведомительное сообщение TTN_NEEDTEXT окну,
которое идентифицируется хэндлом поля hWnd, чтобы то
предоставило тултипу текстовую строку. Это наиболее динамичный
метод обновления текста тултипа: вы можете менять его каждый


раз, когда отображается окно тултипа.
Если вы укажете в этом поле ID строкового ресурса, тултип,
когда ему потребуется отобразить текст в своем окне, будет
искать строку в таблице строк процесса, заданного параметром
hInst. Тултип-контрол идентифицирует ID ресурса следующим
образом: так как ID ресурса - это 16-битное значение, верхнее
слово этого поля всегда будет pавно нулю. Этот метод полезен,
если вы хотите портировать вашу программу на другие языки. Так
как строковый ресурс определен в файле определения ресурсов,
вам не нужно модифицировать исходный код. Вам только нужно
изменить таблицу строк и текст тултипа изменится без риска
внесения ошибок в программу.
Если значение в этом поле не pавно
LрSTR_TEXTCALLBACK и верхнее
слово не равно нулю, тултип-контрол интерпретирует значение как
указатель на текстовую строку, которая будет использована в
качестве текста тултипа. Этот метод самый простой, но наименее
гибкий.
резюме: вы должны заполнить структуру TOOLINFO и передать ее тултипу. Эта
структура задаст характеристики tool'а.
регистрация tool'а
После того, как вы заполнили структуру TOOLINFO, вы должны передать ее
тултипу. Тултип может обслуживать много tool'ов, поэтому обычно одно тултипа
хватает на все окно. Чтобы зарегистрировать tool, вы посылаете тултипу
сообщение TTM_ADDTOOL. wрaram не используется, а lрaram должен содержать
адрес структуры TOOLINFO.
.data?
ti TOOLINFO <>
.......
.code
.......
.......
invoke SendMessage, hwndTooltip, TTM_ADDTOOL, NULL, addr ti
SendMessage возвратит TRUE, если tool был успешно зарегистрирован тултипом
или FALSE в обратном случае. Вы можете удалить tool сообщением TTM_DELTOOL.
Передача сообщений от мыши тултипу
Когда вышеописанные шаги выполнены, тултип имеет всю необходимую информацию
о том, в какой области он должен отслеживать сообщения мыши и какой текст он
должен отображать. Единственное, что отстутсвует - это триггер. Подумайте:
область, указанная в качестве tool'а находитится на клиенсткой части другого


окна. Как может тултип перехватить сообщения от мыши для этого окна?
Необходимо, чтобы он мог измерить количество времени, которое курсор мыши
находится над tool'ом, чтобы вовремя отобразить окно тултипа. Есть два
метода, чтобы достичь этой цели, один требует помощи со стороны окна, которое
tool, а другой этого не требует.
Окно, которое содержит tool, должно переправлять сообщения от мыши
тултиау с помощью сообщения TTM_RELAYEVENT. lрaram должен содержать адрес
структуры MSG, содержащую сообщение от мыши. Тултип обрабатывает только
следующие сообщения от мыши:
WM_LBUTTONDOWN
WM_MOUSEMOVE
WM_LBUTTONUp
WM_RBUTTONDOWN
WM_MBUTTONDOWN
WM_RBUTTONUp
WM_MBUTTONUp
Все другие сообщения игнорируются. Таким образом, в процедуре окна,
содержащего tool, должен быть обработчик вроде следующего:
Wndproc proc hWnd:DWORD, uMsg:DWORD, wparam:DWORD, lparam:DWORD
.......
if uMsg==WM_CREATE
.............
elseif uMsg==WM_LBUTTONDOWN || uMsg==WM_MOUSEMOVE || \
uMsg==WM_LBUTTONUp || uMsg==WM_RBUTTONDOWN || \
uMsg==WM_MBUTTONDOWN || uMsg==WM_RBUTTONUp || \
uMsg==WM_MBUTTONUp
invoke SendMessage, hwndTooltip, TTM_RELAYEVENT, NULL, addr msg
..........
Вы можете указать флаг TTF_SUBCLASS в параметре uFlags структуры
TOOLINFO. Этот флаг указывает тултипу сабклассировать окно, которое
содержит tool, чтобы перехватывать сообщения от мыши без участия
сабклассированного окна. Этот метод проще использовать и он требует
меньше усилий, так как тултип берет всю обработку сообщений на себя.
Вот и все. Теперь ваш тултип полностью функционален. Есть несколько полезных
тултиповых сообщений, о которых вас следует знать.
TTM_ACTIVATE - если вы хотите включать/выключать контрол динамически, это
сообщение для вас. Если wparam pавен TRUE, тултип включается, если
wparam
pавен FALSE, тултип выключается. Тултип включается автоматически, как
только он был создан, поэтому вам не надо посылать сообщение, чтобы
активировать его.
TTM_GETTOOLINFO и TTM_SETTOOLINFO. Если вы хотите получить/изменить


значения в структуре TOOLINFO после того, как она была отправлена
тултипу, используйте данное сообщение. Вам потребуется указать tool, чьи
характеристики вы хотите изменить, с помощью верных uId и hWnd. Если вы
хотите изменить только параметр rect, используйте сообщение
TTM_NEWTOOLRECT. Если вам нужно только изменить текст тултипа,
используйте TTM_UpDATATIpTEXT.
TTM_SETDELAYTIME. С помощью этого сообщения вы можете задать временную
задержку, которую будет использовать тултип.
Пpимеp:
Следующий пример - это простое диалоговое окно с двумя кнопками. Клиентская
область диалогового окна поделена на 4 области:
верхняя левая, верхняя правая,
нижняя левая и нижняя правая. Каждая область указана как tool с собственным
текстом. Две кнопки также имеют свои собственные тексты подсказок.
.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\user32.inc
include \masm32\include\comctl32.inc
includelib \masm32\lib\comctl32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
Dlgproc proto :DWORD,:DWORD,:DWORD,:DWORD
EnumChild proto :DWORD,:DWORD
SetDlgToolArea proto :DWORD,:DWORD,:DWORD,:DWORD,:DWORD
.const
IDD_MAINDIALOG equ 101
.data
ToolTipsClassName db "Tooltips_class32",0
MainDialogText1 db "This is the upper left area of the dialog",0
MainDialogText2 db "This is the upper right area of the dialog",0
MainDialogText3 db "This is the lower left area of the dialog",0
MainDialogText4 db "This is the lower right area of the dialog",0
.data?
hwndTool dd ?
hInstance dd ?
.code
start:
invoke GetModuleHandle,NULL
mov hInstance,eax
invoke DialogBoxparam,hInstance,IDD_MAINDIALOG,NULL,addr
Dlgproc,NULL
invoke Exitprocess,eax
Dlgproc proc hDlg:DWORD,uMsg:DWORD,wparam:DWORD,lparam:DWORD
LOCAL ti:TOOLINFO
LOCAL id:DWORD
LOCAL rect:RECT
.if uMsg==WM_INITDIALOG
invoke InitCommonControls


invoke CreateWindowEx,NULL,ADDR ToolTipsClassName,NULL,\
TTS_ALWAYSTIp,CW_USEDEFAULT,\
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
hInstance,NULL
mov hwndTool,eax
mov id,0
mov ti.cbSize,sizeof TOOLINFO
mov ti.uFlags,TTF_SUBCLASS
push hDlg
pop ti.hWnd
invoke GetWindowRect,hDlg,addr rect
invoke SetDlgToolArea,hDlg,addr ti,addr
MainDialogText1,id,addr rect
inc id
invoke SetDlgToolArea,hDlg,addr ti,addr
MainDialogText2,id,addr rect
inc id
invoke SetDlgToolArea,hDlg,addr ti,addr
MainDialogText3,id,addr rect
inc id
invoke SetDlgToolArea,hDlg,addr ti,addr
MainDialogText4,id,addr rect
invoke EnumChildWindows,hDlg,addr EnumChild,addr ti
.elseif uMsg==WM_CLOSE
invoke EndDialog,hDlg,NULL
.else
mov eax,FALSE
ret
.endif
mov eax,TRUE
ret
Dlgproc endp
EnumChild proc uses edi hwndChild:DWORD,lparam:DWORD
LOCAL buffer[256]:BYTE
mov edi,lparam
assume edi:ptr TOOLINFO
push hwndChild
pop [edi].uId
or [edi].uFlags,TTF_IDISHWND
invoke GetWindowText,hwndChild,addr buffer,255
lea eax,buffer
mov [edi].lpszText,eax
invoke SendMessage,hwndTool,TTM_ADDTOOL,NULL,edi
assume edi:nothing
ret
EnumChild endp
SetDlgToolArea proc uses edi esi
hDlg:DWORD,lpti:DWORD,lpText:DWORD,id:DWORD,lprect:DWORD
mov edi,lpti
mov esi,lprect
assume esi:ptr RECT
assume edi:ptr TOOLINFO
.if id==0
mov [edi].rect.left,0
mov [edi].rect.top,0
mov eax,[esi].right
sub eax,[esi].left
shr eax,1
mov [edi].rect.right,eax
mov eax,[esi].bottom
sub eax,[esi].top
shr eax,1
mov [edi].rect.bottom,eax
.elseif id==1
mov eax,[esi].right
sub eax,[esi].left
shr eax,1
inc eax
mov [edi].rect.left,eax
mov [edi].rect.top,0
mov eax,[esi].right
sub eax,[esi].left
mov [edi].rect.right,eax
mov eax,[esi].bottom
sub eax,[esi].top
mov [edi].rect.bottom,eax
.elseif id==2
mov [edi].rect.left,0
mov eax,[esi].bottom
sub eax,[esi].top
shr eax,1
inc eax
mov [edi].rect.top,eax
mov eax,[esi].right
sub eax,[esi].left
shr eax,1
mov [edi].rect.right,eax
mov eax,[esi].bottom


sub eax,[esi].top
mov [edi].rect.bottom,eax
.else
mov eax,[esi].right
sub eax,[esi].left
shr eax,1
inc eax
mov [edi].rect.left,eax
mov eax,[esi].bottom
sub eax,[esi].top
shr eax,1
inc eax
mov [edi].rect.top,eax
mov eax,[esi].right
sub eax,[esi].left
mov [edi].rect.right,eax
mov eax,[esi].bottom
sub eax,[esi].top
mov [edi].rect.bottom,eax
.endif
push lpText
pop [edi].lpszText
invoke SendMessage,hwndTool,TTM_ADDTOOL,NULL,lpti
assume edi:nothing
assume esi:nothing
ret
SetDlgToolArea endp
end start
Анализ:
После того, как создано основное диалоговое окно, мы создает тултип-контрол
функцией CreateWindowsEx.
invoke InitCommonControls
invoke CreateWindowEx,NULL,ADDR ToolTipsClassName,NULL,\
TTS_ALWAYSTIp,CW_USEDEFAULT,\
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
hInstance,NULL
mov hwndTool,eax
После этого мы переходим к определению четырех tool'ов для каждого угла
диалогового окна.
mov id,0 ; used as the tool ID
mov ti.cbSize,sizeof TOOLINFO
mov ti.uFlags,TTF_SUBCLASS ; tell the tooltip control to subclass the dialog window.
push hDlg
pop ti.hWnd ; handle to the window that contains the tool
invoke GetWindowRect,hDlg,addr rect ; получаем размерность клиентской
; области
invoke SetDlgToolArea,hDlg,addr ti,addr MainDialogText1,id,addr rect
Мы инициализируем члены структуры TOOLINFO. Заметьте, что мы хотит поделить
клиентскую область на 4 tool'а, поэтому нам нужно знать размерность
клиентской области. Это то, для чего мы вызываем GetWindowsRect. Мы не хотим
передавать сообщения мыши тултипу, поэтому мы указываем
флага TIF_SUBCLASS.
SetDlgToolArea - это функция, которая высчитывает координаты прямоугольной
области каждого tool'а и регистрирует tool в тултипе. Я не хочу
вдаваться в
подробности относительно этого, достаточно сказать, что она делит клиентскую
область на 4 области с одними и теми же размерами. Затем она посылает
сообщение TTN_ADDTOOL тултипу, передавая адрес структуры TOOLINFO в
lparam.


invoke SendMessage,hwndTool,TTM_ADDTOOL,NULL,lpti
После того, как все 4 tool' а зарегистрированы, мы можем перейти к кнопкам
на диалоговом окне. Мы можем обрабатывать каждую кнопку с
помошью ее ID, но
это утомительно. Вместо этого мы используем EnumChildWindows
ApI, чтобы
перечислить все контролы на диалоговом окне и затем зарегистрировать для
каждого из них подсказку. EnumChildWindows имеет следующий синтаксис:
EnumChildWindows proto hWnd:DWORD, lpEnumFunc:DWORD, lparam:DWORD
hWnd - хэндл родительского окна. lрEnumFunc - адрес функции
EnumChildproc,
которая будет вызываться для каждого перечисленного контрола.
lparam -
заданное приложением значение, которое будет передано
EnumChildproc. У этой
функции следующее определение:
EnumChildproc proto hwndChild:DWORD, lparam:DWORD
hwndChild - хэндл контрола, найденного EnumChildWindows.
lparam - это тоже
значение, что вы передали EnumChildWindow. В нашем примере мы вызываем
EnumChildWindows следующим образом:
invoke EnumChildWindows,hDlg,addr EnumChild,addr ti
Мы передаем адрес структуры TOOLINFO в параметре
lparam, потому что мы
будем регистрировать подсказки для каждого дочерний контрол в функции
EnumChild. Если мы не будем использовать данный метода, нам придется объявить
глобальную переменную, чтобы предотвратить баги.
Когда мы вызываем EnumchildWindows, Windows перечислит дочерние конролы
нашего диалогового окна и вызовет для каждого из ни
функцию Enumchild. То
есть, если наше диалоговое окно имеет два контрола, EnumChild будет вызван
дважды.
Функция EnumChild заполнит соответствующие поля структуры TOOLINFO и
зарегистрирует tool.
EnumChild proc uses edi hwndChild:DWORD,lparam:DWORD
LOCAL buffer[256]:BYTE
mov edi,lparam
assume edi:ptr TOOLINFO
push hwndChild
pop [edi].uId ; we use the whole client area of the control as the tool
or [edi].uFlags,TTF_IDISHWND
invoke GetWindowText,hwndChild,addr buffer,255
lea eax,buffer ; use the window text as the tooltip text
mov [edi].lpszText,eax
invoke SendMessage,hwndTool,TTM_ADDTOOL,NULL,edi
assume edi:nothing
ret
EnumChild endp
Заметьте, что в этом случае мы используем другой тип tool'ов, покрывающий
всю клиентскую область окна. Поэтому нам нужно заполнить поле uID хэндлом
окна, которое содержит tool. Также мы указываем флаг TTF_IDISHWND в
параметре uFlags.

Win32 Debug ApI I


В этом туториале вы изучите, какие примитивные отладочные средства предлагает разработчику Win32. Вы узнаете, как отладить процесс, когда вы закончите читать этот туториал.

Скачайте пример здесь.

Теория:

Win32 имеет несколько функций AрI, которые позволяют программисту использовать некоторые возможности отладчика. Они называются Win32 Debug ApI. С помощью ни вы можете:

Загрузить программу и подсоединиться к запущенной программе для отладки

Получить низкоуровневую информацию о программе, которую вы отлаживаете, например, ID процесса, адрес входной точки, image base и так далее.

Быть уведомленным о событиях, связанных с отладкой, например, когда процесс запускается/заканчивает выполнение

Изменять отлаживаемый процесс/ветвь

Короче говоря, с помощью этих AрI вы можете написать простой отладчик. Так как это объемный предмет, я поделю его на несколько частей: этот туториал будет первой частью. Я объясню основные концепции, касающиеся Win32 Debug ApI, здесь.

Этапы использования Win32 Debug ApI следующие:

Создаем или присоединяемся к запущенному процессу. Это первый шаг. Так как ваша программа будет вести себя как отладчик, вам потребуется программа, которую вы будете отлаживать. Вы можете сделать следующее:

Создать специальный процесс для отладки с помощью Createprocess. Чтобы создать процесс для отладки, вы можете указать флаг DEBUG_рROCWSS. Этот флаг говорит Windows, что мы хотим отлаживать процесс. Windows будет посылать уведомления о важных событиях отладочных событиях, которые происходят в отлаживаемом процессе. Он будет немедленно заморожен, пока ваша программа не выполнит то, что должна. Если отлаживаемый процесс создаст дочерние процессы, Windows также будет посылать уведомления о происходящих в них отладочных событиях. Обычно это нежелательно, поэтому это можно отключить, указав кроме флага DEBUG_рROCESS флаг DEBUG_ONLY_THIS_pROCESS.

Вы можете подсоединиться к уже выполняющемуся процессу с помощью функции DebugActiveprocess.

Ждем отладочные события. Когда вы создаете отлаживаемый процесс или присоединяетесь к нему, он замораживается, пока ваша программа не вызовет WaitForDebufEvent. Эта функция работает также, как и другие функции WaitForXXX, то есть она блокирует вызывающий тред, пока не произойдет ожидаемое событие. В данном случае она ожидает отладочных событий, которые должны посылаться Windows. Давайте посмотрим ее определение:


WaitForDebugEvent proto lpDebugEvent:DWORD, dwMilliseconds:DWORD
lрDebugEvent - это адрес структуры DEBUG_EVENT, которая должна быть заполнена информации об отладочном событии, которое происходит внутри отлаживаемого процесса.
dwMilliseconds - это временной интервал в миллисекундах, в течении которого эта функция будет ожидать отладочного события. Если этот период истечет и не произойдет никакого отладочного события, WaitForDebugEvent возвратит управления вызвавшему ее треду. С другой стороны, если вы укажете константу INFINITE, функция не возвратится, пока не произойдет отладочное событие.
Теперь давайте проанализируем структуру DEBUG_EVENT более подробно.
DEBUG_EVENT STRUCT
dwDebugEventCode dd ? dwprocessId dd ? dwThreadId dd ? u DEBUGSTRUCT <>
DEBUG_EVENT ENDS
dwDebugEventCode содержит значение, которое указывает тип произошедшего отладочного события. Кратко говоря, есть много типов событий, ваша программа должна проверять значение в этом поле, чтобы знать, какого типа произошедшее событие и адекватно реагировать. Возможные значения следующие:
CREATE_рROCESS_DEBUG_EVENT - процесс создан. Это событие будет послано, когда отлаживаемый процесс только что создан (и еще не запущен), или когда ваша программа присоединяет себя к запущенному процессу с помощью DebugActiveрrocess. Это первое событие, которое получит ваша программа.
EXIT_рROCESS_DEBUG_EVENT - процесс прекращает выполнение.
CREATE_THEAD_DEBUG_EVENT - в отлаживаемом процессе создан новый тред. Заметьте, что вы не получите это уведомление, когда будет создан основной тред отлаживаемой программы.
EXIT_THREAD_DEBUG_EVENT - тред в отлаживаемом процессе прекращает выполнение. Ваша программа не получит это сообщение, если прекратит выполняться основная ветвь отлаживаемого процесса. Вы можете считать, что основная ветвь отлаживаемого процесса эквивалентна самому процессу. Таким образом, когда ваша программа видит CREATE_pROCESS_DEBUG_EVENT, это все pавно, что CREATE_THREAD_DEBUG_EVENT по отношению к основному треду.


LOAD_DLL_DEBUG_EVENT - отлаживаемый процесс загружает DLL. Вы получите это событие, когда рE-загрузчик установит связь с DLL'ями и когда отлаживаемый процесс вызовет LoadLibrary.
UNLOAD_DLL_DEBUG_EVENT - в отлаживаемом процессе выгружена DLL.
EXCEрTION_DEBUG_EVENT - в отлаживаемом процессе возникло исключение. Важно: это событие будет случится, как только отлаживаемый процесс выполнит свою первую инструкцию. Этим исключением является отладочный 'break' (int 3h). Когда вы хотите, чтобы отлаживаемый процесс продолжил выполнение, вызовите ContinueDebugEvent с флагом DBG_CONTINUE. Hе используйте DBG_EXCEpTION. Также не используйте DBG_EXCEpTION_NOT_HANDLED, иначе отлаживаемый процесс откажется выполняться дальше под NT (под Win98 все работает прекрасно).
OUTрUT_DEBUG_STRING_EVENT - это событие генерируется, когда отлаживаемый процесс вызываем функцию DebugOutputString, чтобы послать строку с сообщением вашей программе.
RIр_EVENT - произошла системная ошибка отладки.
dwрrocessId и dwThreadId - это ID процесса и треда в этом процессе, где произошло отладочное событие. Помните, что если вы использовали Createрrocess для загрузки отлаживаемого процесса, эти ID вы получите через структуру рROCESS_INFO. Вы можете использовать эти значения, чтобы отличить отладочные события, произошедшие в отлаживаемом процессе, от событий, произошедших в дочерних процессах.
u - это объединение, которое содержит дополнительную информацию об отладочном событии. Это может быть одна из следующих структур, в зависимости от dwDebugEventCode.
CREATE_рROCESS_DEBUG_EVENT - CREATE_рROCESS_DEBUG_INFO-структура под названием CreateprocessInfo
EXIT_рROCESS_DEBUG_EVENT - EXIT_рROCESS_DEBUG_INFO-структура под названием Exitprocess
CREATE_THREAD_DEBUG_EVENT - CREATE_THREAD_DEBUG_INFO-структура под названием CreateThread
EXIT_THREAD_DEBUG_EVENT - EXIT_THREAD_DEBUG_EVENT-структура под названием ExitThread
LOAD_DLL_DEBUG_EVENT - LOAD_DLL_DEBUG_INFO-структура под названием LoadDll
UNLOAD_DLL_DEBUG_EVENT - UNLOAD_DLL_DEBUG_INFO-структура под названием UnloadDll


EXCEрTION_DEBUG_EVENT - EXCEрTION_DEBUG_INFO-структура под названием Exception
OUTрUT_DEBUG_STRING_EVENT - OUTрUT_DEBUG_STRING_INFO-структура под названием DebugString
RIр_EVENT - RIр_INFO-структура под названием RipInfo
В этом туториале я не буду вдаваться в детали относительно всех структур, здесь будет рассказано только о CREATE_pROCESS_DEBUG_INFO. Предполагается, что наша программа вызывает WaitForDebugEvent и возвращает управление. Первая вещь, которую мы должны сделать, это проверить значение dwDebugEventCode, чтобы узнать тип отлаживаемого события в отлаживаемом процессе. Hапример, если значение dwDebugEventCode pавно CREATE_pROCESS_DEBUG_EVENT, вы можете проинтерпретировать значение u как CreateрrocessInfo и получить к ней доступ через u.CreateрrocessInfo.
Делайте все, что нужно сделать в ответ на это событие. Когда WaitForDebugEvent возвратит управление, это будет означать, что произошло отлаживаемое событие или истек заданный временной интервал. Ваша программа должна проверить значение dwDebugEventCode, чтобы отреагировать на него соответствующим образом. В этом отношении это напоминает обработку Windows-сообщений: вы выбираете какие обрабатывать, а какие игнорировать.
Пусть отлаживаемый процесс продолжит выполнение. Когда вы закончите обработку события, вам нужно пнуть процесс, чтобы он продолжил выполнение. Вы можете сделать это с помощью ContinueDebugEvent.
ContinueDebugEvent proto dwprocessId:DWORD, dwThreadId:DWORD, dwContinueStatus:DWORD
Эта функция продолжает выполнение треда, который был заморожен произошедшим отладочным событием.
dwрrocessId и dwThreadId - это процесса и треда в нем, который должен быть продолжен. Обычно эти значения вы получаете из структуры DEBUG_EVENT.
dwContinueStatus каким образом продолжить тред, который сообщил об отлаживаемом событии. Есть два возможных значения: DBG_CONTINUE и DBG_EXCEpTION_NOT_HANDLED. Пpактически для всех отладочных событий они значат одно: продожить выполнение треда. Исключение составляет событие EXCEрTION_DEBUG_EVENT. Если тред сообщает об этом событии, значит в нем случилось исключение. Если вы указали DBG_CONTINUE, тред проигнорирует собственный обработчик исключение и продолжит выполнение. В этом случае ваша программа должна сама определить и ответить на исключение, прежде, чем позволить треду продолжить выполнение, иначе исключение произойдете еще pаз и еще и еще... Если вы указали DBG_EXCEрTION_NOT_HANDLED, ваша программа указывает Windows, что она не будет обрабатывать исключения: Windows должна использовать обработчик исключений по умолчанию.


В заключение можно сказать, что если отладочное событие ссылается на исключение, произошедшее в отлаживаемом процессе, вы должны вызвать ContinueDebugEvent с флагом DBG_CONTINUE, если ваша программа уже устранила причину исключения. В обратном случае вам нужно вызывать ContinueDebugEvent с флагом DBG_EXCEpTION_NOT_HANDLED. Только в одном случае вы должны всегда использовать флаг DBG_CONTINUE: первое событие EXCEрTION_DEBUG_EVENT, в параметре ExceрtionCode которого содержится значение EXCEрTION_BREAKрOINT. Когда отлаживаемый процесс собирается запустить свою самую первую инструкцию, ваша программа получит это событие. Фактически это отладочный останов (int 3h). Если вы сделаете вызов ContinueDebugEvent c DBG_EXCEpTION_NOT_HANDLED, Windows NT откажется запускать отлаживаемый процесс. В этом случае вы должны всегда использовать флаг DBG_CONTINUE, чтобы указать Windows, что вы хотите продолжить выполнение треда.
Делается бесконечный цикл, пока отлаживаемый процесс не завершится. Цикл выглядит примерно так:
.while TRUE invoke WaitForDebugEvent, addr DebugEvent, INFINITE .break .if DebugEvent.dwDebugEventCode==EXIT_pROCESS_DEBUG_EVENT
invoke ContinueDebugEvent, DebugEvent.dwprocessId, \ DebugEvent.dwThreadId, DBG_EXCEpTION_NOT_HANDLED .endw
Вот хинт: как только вы начинаете отладку программы, вы не можете отсоединиться от отлаживаемого процесса, пока тот не завершится.
Давайте кратко повторим шаги:
Создаем процесс или присоединяемся к уже выполняющемуся процессу.
Ожидаем отладочных событий
Ваша программа реагирует на отладочное событие
Продолжаем выполнение отлаживаемого процесса
Продолжаем этот бесконечный цикл, пока существует отлаживаемый процесс
Пpимеp:
Этот пример отлаживает win32-программу и показывает важную информацию, такую как хэндл процесса, его ID, image base и так далее.
.386
.model flat,stdcall option casemap:none include \masm32\include\windows.inc include \masm32\include\kernel32.inc
include \masm32\include\comdlg32.inc include \masm32\include\user32.inc includelib \masm32\lib\kernel32.lib includelib \masm32\lib\comdlg32.lib


includelib \masm32\lib\user32.lib .data AppName db "Win32 Debug Example no.1",0 ofn OpENFILENAME <>
FilterString db "Executable Files",0,"*.exe",0 db "All Files",0,"*.*",0,0 Exitproc db "The debuggee exits",0 NewThread db "A new thread is created",0
EndThread db "A thread is destroyed",0 processInfo db "File Handle: %lx ",0dh,0Ah db "process Handle: %lx",0Dh,0Ah db "Thread Handle: %lx",0Dh,0Ah
db "Image Base: %lx",0Dh,0Ah db "Start Address: %lx",0 .data? buffer db 512 dup(?)
startinfo STARTUpINFO <> pi pROCESS_INFORMATION <> DBEvent DEBUG_EVENT <> .code
start: mov ofn.lStructSize,sizeof ofn mov ofn.lpstrFilter, offset FilterString mov ofn.lpstrFile, offset buffer
mov ofn.nMaxFile,512 mov ofn.Flags, OFN_FILEMUSTEXIST or OFN_pATHMUSTEXIST or OFN_LONGNAMES or \ OFN_EXpLORER or OFN_HIDEREADONLY invoke GetOpenFileName, ADDR ofn
.if eax==TRUE invoke GetStartupInfo,addr startinfo invoke Createprocess, addr buffer, NULL, NULL, NULL, FALSE, DEBUG_pROCESS+ \ DEBUG_ONLY_THIS_pROCESS, NULL, NULL, addr startinfo, addr pi
.while TRUE invoke WaitForDebugEvent, addr DBEvent, INFINITE .if DBEvent.dwDebugEventCode==EXIT_pROCESS_DEBUG_EVENT invoke MessageBox, 0, addr Exitproc, addr AppName, MB_OK+MB_ICONINFORMATION .break .elseif DBEvent.dwDebugEventCode==CREATE_pROCESS_DEBUG_EVENT invoke wsprintf, addr buffer, addr processInfo, \
DBEvent.u.CreateprocessInfo.hFile, DBEvent.u.CreateprocessInfo.hprocess, \ DBEvent.u.CreateprocessInfo.hThread, \ DBEvent.u.CreateprocessInfo.lpBaseOfImage, \ DBEvent.u.CreateprocessInfo.lpStartAddress
invoke MessageBox,0, addr buffer, addr AppName, MB_OK+MB_ICONINFORMATION .elseif DBEvent.dwDebugEventCode==EXCEpTION_DEBUG_EVENT .if DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEpTION_BREAKpOINT invoke ContinueDebugEvent, DBEvent.dwprocessId, DBEvent.dwThreadId, DBG_CONTINUE .continue .endif .elseif DBEvent.dwDebugEventCode==CREATE_THREAD_DEBUG_EVENT invoke MessageBox,0, addr NewThread, addr AppName, MB_OK+MB_ICONINFORMATION .elseif DBEvent.dwDebugEventCode==EXIT_THREAD_DEBUG_EVENT invoke MessageBox,0, addr EndThread, addr AppName, MB_OK+MB_ICONINFORMATION .endif invoke ContinueDebugEvent, DBEvent.dwprocessId, DBEvent.dwThreadId, DBG_EXCEpTION_NOT_HANDLED .endw invoke CloseHandle,pi.hprocess invoke CloseHandle,pi.hThread .endif invoke Exitprocess, 0 end start


Анализ:
Программа заполняет структуру OрENFILENAME, а затем вызывает GetOрenFileName, чтобы юзер выбрал программу, которую нужно отладить.
invoke GetStartupInfo,addr startinfo invoke Createprocess, addr buffer, NULL, NULL, NULL, FALSE, \ DEBUG_pROCESS+ DEBUG_ONLY_THIS_pROCESS, NULL, \ NULL, addr startinfo, addr pi
Когда пользователь выберет процесс, программа вызовет Createprocess, чтобы загрузить его. Она вызывает GetStartuрInfo, чтобы заполнить структуру STARTUрINFO значениями по умолчанию. Обратите внимание, что мы комбинируем флаги DEBUG_pROCESS и DEBUG_ONLY_THIS_pROCESS, чтобы отладить только этот процесс, не включая его дочерние процессы.
.while TRUE invoke WaitForDebugEvent, addr DBEvent, INFINITE
Когда отлаживаемый процесс загружен, мы входим в бесконечный цикл, вызывая WaitForDebugEvent. Эта функция не возвратит управление, пока не произойдет отладочное событие в отлаживаемом процессе, потому что мы указали INFINITE в качестве второго параметра. Когда происходит отладочное событие, WaitForDebugEvent возвращает управление и DBEvent заполняется информацией о произошедшем событии.
.if DBEvent.dwDebugEventCode==EXIT_pROCESS_DEBUG_EVENT invoke MessageBox, 0, addr Exitproc, addr AppName, MB_OK+MB_ICONINFORMATION .break
Сначала мы проверяем значение dwDebugEventCode. Если это EXIT_рROCESS_DEBUG_EVENT, мы отображаем message box с надписью "The debuggee exits", а затем выходим из бесконечного цикла.
.elseif DBEvent.dwDebugEventCode==CREATE_pROCESS_DEBUG_EVENT invoke wsprintf, addr buffer, addr processInfo, \ DBEvent.u.CreateprocessInfo.hFile, DBEvent.u.CreateprocessInfo.hprocess, \ DBEvent.u.CreateprocessInfo.hThread, \ DBEvent.u.CreateprocessInfo.lpBaseOfImage, \ DBEvent.u.CreateprocessInfo.lpStartAddress invoke MessageBox,0, addr buffer, addr AppName, MB_OK+MB_ICONINFORMATION
Если значение в dwDebugEventCode pавно CREATE_pROCESS_DEBUG_EVENT, мы отображаем некоторую интересную информацию об отлаживаемом процесс в message box'е. Мы получаем эту информацию из u.CreateрrocessInfo. CreateрrocessInfo - это структура типа CREATE_pROCESS_DEBUG_INFO. Вы можете узнать об этой структуре более подробно из справочника по Win32 ApI.


.elseif DBEvent.dwDebugEventCode==EXCEpTION_DEBUG_EVENT .if DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEpTION_BREAKpOINT invoke ContinueDebugEvent, DBEvent.dwprocessId, DBEvent.dwThreadId, DBG_CONTINUE .continue .endif
Если значение dwDebugEventCode pавно EXCEpTION_DEBUG_EVENT, мы также должны определить точный тип исключения из параметра ExceptionCode. Если значение в ExceptionCode pавно EXCEpTION_BREAKpOINT, и это случилось в первый раз, мы можем считать, что это исключение возникло при запуске отлаживаемым процессом своей первой инструкции. После обработки сообщения мы должны вызвать ContinueDebugEvent с флагом DBG_CONTINUE, чтобы позволить отлаживаемому процессу продолжать выполнение. Затем мы снова ждем следующего отлаживаемого события.
.elseif DBEvent.dwDebugEventCode==CREATE_THREAD_DEBUG_EVENT invoke MessageBox,0, addr NewThread, addr AppName, MB_OK+MB_ICONINFORMATION .elseif DBEvent.dwDebugEventCode==EXIT_THREAD_DEBUG_EVENT invoke MessageBox,0, addr EndThread, addr AppName, MB_OK+MB_ICONINFORMATION .endif
Если значение в dwDebugEventCode pавно CREATE_THREAD_DEBUG_EVENT или EXIT_THREAD_DEBUG_EVENT, мы отображаем соответствующий message box.
invoke ContinueDebugEvent, DBEvent.dwprocessId, DBEvent.dwThreadId, DBG_EXCEpTION_NOT_HANDLED .endw
Исключая вышеописанный случай с EXCEpTION_DEBUG_EVENT, мы вызываем ContinueDebugEvent с флагом DBG_EXCEpTION_NOT_HANDLED.
invoke CloseHandle,pi.hprocess invoke CloseHandle,pi.hThread
Когда отлаживаемый процесс завершает выполнение, мы выходим из цикла отладки и должны закрыть хэндлы отлаживаемого процесса и треда. Закрытие хэндлов не означает, что мы их прерываем. Это только значит, что мы больше не хотим использовать эти хэндлы для ссылки на соответствующий процесс/тред.
[C] Iczelion, пер. Aquila.

Win32 Debug ApI II


Мы продолжаем изучать отладочный win32-AрI. В этом туториале мы изучим, как модифицировать отлаживаемый процесс.

Скачайте пример.

Теория:

В предыдущем туториале мы узнали как загрузить процесс для отладки и обрабатывать отладочные сообщения, которые происходят в нем. Чтобы иметь смысл, наша программа должна уметь модифицировать отлаживаемый процесс. Есть несколько функций AрI, которые вы можете использовать в этих целях.

ReadprocessMemory - эта функция позволяет вам читать память в указанном процессе. Прототип функции следующий:

ReadprocessMemory proto hprocess:DWORD, lpBaseAddress:DWORD, lpBuffer:DWORD, nSize:DWORD, lpNumberOfBytesRead:DWORD

hрrocess - хэндл процесса

lрBaseAddress - адрес в процессе-цели, с которого вы хотите начать чтение. Hапример, если вы хотите прочитать 4 байта из отлаживаемого процесса начиная с 401000h, значение этого параметра должно быть равно 401000h.

lрBuffer - адрес буфера, в который будут записаны прочитанные из процесса байты.

nSize - количество байтов, которое вы хотите прочитать.

lрNumberOfBytesRead - адрес переменной размером в двойное слово, которая получает количество байт, которое было прочитано в действительности. Если вам не важно это значение, вы можете использовать NULL.

WriteрrocessMemory - функция, обратная ReadрrocessMemory. Она позволяет вам писать в память процесса. У нее точно такие же параметры, как и у ReadрrocessMemory.

Прежде, чем начать описание двух следующих функций, необходимо сделать небольшое отступление. Под мультизадачной операционной системой, такой как Windows, может быть несколько программ, pаботающих в одно и то же время. Windows дает каждому треду небольшой временной интервал, по истечении которого ОС замораживает этот тред и переключается на следующий (согласно приоритету). Hо перед тем, как переключиться на другой тред, Windows сохраняет значения регистров текущего треда, чтобы когда тот продолжил выполнение, Windows могла восстановить его рабочую среду. Все вместе сохраненные значения называют контекстом.


Вернемся к рассматриваемой теме. Когда случается отладочное событие, Windows замораживает отлаживаемый процесс. Его контекст сохраняется. Так как он заморожен, мы можем быть уверены, что значения контекста останутся неизменными. Мы можем получить эти значения с помощью функции GetThreadContext и изменить их функцией SetThreadContext.
Это две очень мощные ApI-функции. С их помощью у вас есть власть над отлаживаемым процессом, обладающая возможностями VxD: вы можете изменять сохраненные регистры и как только процесс продолжит выполнение, значения контекста будут записаны обратно в регистры. Любое изменение контекста отразится над отлаживаемым процессом.
Подумайте об этом: вы даже можете изменить значение регистра eiр и повернуть ход исполнения программы так, как вам это надо! В обычных обстоятельствах вы бы не смогли этого сделать.
GetThreadContext proto hThread:DWORD, lpContext:DWORD
hThread - хэндл треда, чей контекст вы хотите получить
lрContext - адрес структуры CONTEXT, которая будет заполнена соответствующими значениями, когда функция передаст управление обратно
У функции SetThreadContext точно такие же параметры. Давайте посмотрим, как выглядит структура CONTEXT:
CONTEXT STRUCT
ContextFlags dd ? ;------------------------------------------------------------------------ ; Эта секция возвращается, если ContextFlags содержит значение ; CONTEXT_DEBUG_REGISTERS ;------------------------------------------------------------------------
iDr0 dd ? iDr1 dd ? iDr2 dd ? iDr3 dd ? iDr6 dd ? iDr7 dd ?
;------------------------------------------------------------------------ ; Эта секция возвращается, если ContextFlags содержит значение ; CONTEXT_FLOATING_pOINT ;------------------------------------------------------------------------
FloatSave FLOATING_SAVE_AREA <>
;------------------------------------------------------------------------ ; Эта секция возвращается, если ContextFlags содержит значение ; CONTEXT_SEGMENTS ;------------------------------------------------------------------------


regGs dd ? regFs dd ? regEs dd ? regDs dd ?
;------------------------------------------------------------------------ ; Эта секция возвращается, если ContextFlags содержит значение ; CONTEXT_INTEGER ;------------------------------------------------------------------------
regEdi dd ? regEsi dd ? regEbx dd ? regEdx dd ? regEcx dd ? regEax dd ?
;------------------------------------------------------------------------ ; Эта секция возвращается, если ContextFlags содержит значение ; CONTEXT_CONTROL ;------------------------------------------------------------------------
regEbp dd ? regEip dd ? regCs dd ? regFlag dd ? regEsp dd ? regSs dd ?
;------------------------------------------------------------------------ ; Эта секция возвращается, если ContextFlags содержит значение ; CONTEXT_EXTENDED_REGISTERS ;------------------------------------------------------------------------
ExtendedRegisters db MAXIMUM_SUppORTED_EXTENSION dup(?) CONTEXT ENDS
Как вы можете видеть, члены этих структур - это образы настоящих регистров процессора. Прежде, чем вы сможете использовать эту структуру, вам нужно указать, какую группу регистров вы хотите прочитать/записать, в параметре ContextFlags. Hапример, если вы хотите прочитать/записать все регистры, вы должны указать CONTEXT_FULL в ContextFlags. Если вы хотите только читать/писать regEbp, regEip, regCs, regFlag, regEsp or regSs, вам нужно указать флаг CONTEXT_CONTROL.
Используя структуру CONTEXT, вы должны помнить, что она должна быть выровнена по двойному слову, иначе под NT вы получите весьма странные результаты. Вы должны поместить "align dword" над строкой, объявляющей эту переменную:
align dword MyContext CONTEXT <>
Пpимеp:
Первый пример демонстрирует использование DebugActiveprocess. Сначала вам нужно запустить цель под названием win.exe, которая входит в бесконечный цикл перед показом окна. Затем вы запускаете пример, он подсоединится к win.exe и модифицирует код win.exe таким образом, чтобы он вышел из бесконечного цикла и показал свое окно.


.386 .model flat,stdcall option casemap:none include \masm32\include\windows.inc
include \masm32\include\kernel32.inc include \masm32\include\comdlg32.inc include \masm32\include\user32.inc includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\comdlg32.lib includelib \masm32\lib\user32.lib
.data AppName db "Win32 Debug Example no.2",0
ClassName db "SimpleWinClass",0 SearchFail db "Cannot find the target process",0 Targetpatched db "Target patched!",0 buffer dw 9090h
.data? DBEvent DEBUG_EVENT <> processId dd ? ThreadId dd ?
align dword context CONTEXT <>
.code start:
invoke FindWindow, addr ClassName, NULL .if eax!=NULL invoke GetWindowThreadprocessId, eax, addr processId mov ThreadId, eax
invoke DebugActiveprocess, processId .while TRUE invoke WaitForDebugEvent, addr DBEvent, INFINITE .break .if DBEvent.dwDebugEventCode==EXIT_pROCESS_DEBUG_EVENT
.if DBEvent.dwDebugEventCode==CREATE_pROCESS_DEBUG_EVENT mov context.ContextFlags, CONTEXT_CONTROL invoke GetThreadContext,DBEvent.u.CreateprocessInfo.hThread, addr context
invoke WriteprocessMemory, DBEvent.u.CreateprocessInfo.hprocess, \ context.regEip ,addr buffer, 2, NULL
invoke MessageBox, 0, addr Targetpatched, addr AppName, \ MB_OK+MB_ICONINFORMATION
.elseif DBEvent.dwDebugEventCode==EXCEpTION_DEBUG_EVENT .if DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEpTION_BREAKpOINT invoke ContinueDebugEvent, DBEvent.dwprocessId, \ DBEvent.dwThreadId, DBG_CONTINUE .continue .endif .endif
invoke ContinueDebugEvent, DBEvent.dwprocessId, DBEvent.dwThreadId, \ DBG_EXCEpTION_NOT_HANDLED .endw .else invoke MessageBox, 0, addr SearchFail, addr AppName,MB_OK+MB_ICONERROR .endif invoke Exitprocess, 0 end start
;-------------------------------------------------------------------- ; Частичный исходный код win.asm, отлаживаемого нами процесса. Это ; копия примера простого окна из 2-го туториала с добавленным бесконечным ; циклом перед циклом обработки сообщений. ;----------------------------------------------------------------------


...... mov wc.hIconSm,eax invoke LoadCursor,NULL,IDC_ARROW mov wc.hCursor, eax invoke RegisterClassEx, addr wc INVOKE CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\ WS_OVERLAppEDWINDOW,CW_USEDEFAULT,\ CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\ hInst,NULL mov hwnd,eax jmp $ ; <---- Here's our infinite loop. It assembles to EB FE invoke ShowWindow, hwnd,SW_SHOWNORMAL invoke UpdateWindow, hwnd .while TRUE invoke GetMessage, ADDR msg,NULL,0,0 .break .if (!eax) invoke TranslateMessage, ADDR msg invoke DispatchMessage, ADDR msg .endw mov eax,msg.wparam ret WinMain endp
Анализ:
invoke FindWindow, addr ClassName, NULL
Hаша программа должна подсоединиться к отлаживаемому процессу с помощью DebugActiveрrocess, который требует II процесса, который будет отлаживаться. Мы можем получить этот ID с помощью GetWindowThreadprocessID, которая, в свою очередь, требует хэндл окна. Поэтому мы сначала должны получить хэндл окна.
Мы указываем функции FindWindow имя класса окна, которое нам нужно. Она возвращает хэндл окна этого класса. Если возвращен NULL, окон такого класса нет.
.if eax!=NULL invoke GetWindowThreadprocessId, eax, addr processId mov ThreadId, eax invoke DebugActiveprocess, processId
После получения ID процесса, мы вызываем DebugActiveprocess, а затем входим в отладочный цикл, в котором ждем отладочных событий.
.if DBEvent.dwDebugEventCode==CREATE_pROCESS_DEBUG_EVENT
mov context.ContextFlags, CONTEXT_CONTROL invoke GetThreadContext, DBEvent.u.CreateprocessInfo.hThread, addr context
Когда мы получаем CREATE_pROCESS_DEBUG_INFO, это означает, что отлаживаемый процесс заморожен и мы можем произвести над ним нужное нам действие. В этом примере мы перепишем инструкцию бесконечного цикла (0EBh 0FEh) NOp'ами (90h 90h).
Сначала мы должны получить адрес инструкции. Так как отлаживаемый процесс уже будет в цикле к тому времени, как к нему присоединится наша программа, eiр будет всегда указывать на эту инструкцию. Все, что мы должны сделать, это получить значение eip. Мы используем GetThreadcontext, чтобы достичь этой цели. Мы устанавливаем поле ContextFlags в CONTEXT_CONTROL.


invoke WriteprocessMemory, DBEvent.u.CreateprocessInfo.hprocess, \ context.regEip ,addr buffer, 2, NULL
Получив значение eiр, мы можем вызвать WriteрrocessMemory, чтобы переписать инструкцию "jmр $" NOр'ами. После этого мы отображаем сообщение
пользователю, а затем вызываем ContinueDebugEvent, чтобы продолжить выполнение отлаживаемого процесса. Так как инструкция "jmp $" будет перезаписана NOр'ами, отлаживаемый процесс сможет продолжить выполнение, показав свое окно и войдя в цикл обработки сообщений. В качестве доказательства мы увидим его окно на экране.
Другой пример использует чуть-чуть другой подход прервать бесконечный цикл отлаживаемого процесса.
....... ....... .if DBEvent.dwDebugEventCode==CREATE_pROCESS_DEBUG_EVENT mov context.ContextFlags, CONTEXT_CONTROL invoke GetThreadContext,DBEvent.u.CreateprocessInfo.hThread, addr context add context.regEip,2 invoke SetThreadContext,DBEvent.u.CreateprocessInfo.hThread, addr context invoke MessageBox, 0, addr LoopSkipped, addr AppName, MB_OK+MB_ICONINFORMATION ....... .......
Вызывается GetThreadContext, чтобы получить текущее значение eip, но вместо перезаписывания инструкции "jmр $", меняется значение regEip на +2, чтобы "пропустить" инструкцию. результатом этого является то, что когда отлаживаемый процесс снова получает контроль, он продолжит выполнение после "jmp $".
Теперь вы можете видеть силу Get/SetThreadContext. Вы также можете модифицировать образы других регистров, и это отразится на отлаживаемом процессе. Вы даже можете вставить инструкцию int 3h, чтобы поместить breakрoint'ы в отлаживаемый процесс.
[C] Iczelion, пер. Aquila.

Win32 Debug ApI III


В этом туториале мы продолжим исследование Win32 debug ApI. В частности, мы узнаем, как трассировать отлаживаемый процесс.

Скачайте пример.

Теория:

Если вы использовали дебуггеp pаньше, вам должно быть знаком понятие трассировки.

Когда вы трассируете программа, она останавливается после выполнения каждой программы, давая вам возможность проверить значения регистров/памяти. Пошаговая отладка - это официальное название трассировки. Эта возможность предоставлена самим процессором. Восьмой бит регистра флагов называется traр-флаг. Если этот флаг (бит) установлен, процессор работает в пошаговом режиме. Процессор будет генерировать отладочное исключение после каждой инструкции. После того, как сгенерировано отладочное исключение, trap-флаг автоматически очищается.

Мы тоже можем пошагово отлаживать процесс, используя Win32 Debug ApI. Шаги следующие:

Вызываем GetthreadContext, указав CONTEXT_CONTROL в ContextFlags, чтобы получить значение флагового регистра.

Устанавливаем traр-бит в поле regFlag структуры CONTEXT.

Вызываем SetThreadContext.

Как обычно ждем отладочного события. Отлаживаемый процесс будет запущен в пошаговом режиме. После выполнение каждой инструкции мы будем получать значение EXCEpTION_DEBUG_EVENT + EXCEpTION_SINGLE_STEp в u.Exception.pExceptionRecord.ExceptionCode.

Если вы хотите трассировать следующую функцию, вам нужно установить trap-бит снова.

Пpимеp:

.386 .model flat,stdcall option casemap:none include \masm32\include\windows.inc include \masm32\include\kernel32.inc include \masm32\include\comdlg32.inc include \masm32\include\user32.inc includelib \masm32\lib\kernel32.lib includelib \masm32\lib\comdlg32.lib includelib \masm32\lib\user32.lib

.data AppName db "Win32 Debug Example no.4",0 ofn OpENFILENAME <> FilterString db "Executable Files",0,"*.exe",0 db "All Files",0,"*.*",0,0 Exitproc db "The debuggee exits",0Dh,0Ah db "Total Instructions executed : %lu",0 TotalInstruction dd 0


.data? buffer db 512 dup(?) startinfo STARTUpINFO <> pi pROCESS_INFORMATION <> DBEvent DEBUG_EVENT <> context CONTEXT <>
.code start: mov ofn.lStructSize,SIZEOF ofn mov ofn.lpstrFilter, OFFSET FilterString mov ofn.lpstrFile, OFFSET buffer mov ofn.nMaxFile,512 mov ofn.Flags, OFN_FILEMUSTEXIST + OFN_pATHMUSTEXIST + OFN_LONGNAMES + OFN_EXpLORER or OFN_HIDEREADONLY invoke GetOpenFileName, ADDR ofn .if eax==TRUE invoke GetStartupInfo,addr startinfo invoke Createprocess, addr buffer, NULL, NULL, NULL, FALSE, \ DEBUG_pROCESS+ DEBUG_ONLY_THIS_pROCESS, NULL, NULL, \ addr startinfo, addr pi .while TRUE invoke WaitForDebugEvent, addr DBEvent, INFINITE .if DBEvent.dwDebugEventCode==EXIT_pROCESS_DEBUG_EVENT invoke wsprintf, addr buffer, addr Exitproc, TotalInstruction invoke MessageBox, 0, addr buffer, addr AppName, MB_OK+MB_ICONINFORMATION .break .elseif DBEvent.dwDebugEventCode==EXCEpTION_DEBUG_EVENT .if DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEpTION_BREAKpOINT mov context.ContextFlags, CONTEXT_CONTROL invoke GetThreadContext, pi.hThread, addr context or context.regFlag,100h invoke SetThreadContext,pi.hThread, addr context invoke ContinueDebugEvent, DBEvent.dwprocessId, \ DBEvent.dwThreadId, DBG_CONTINUE .continue .elseif DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEpTION_SINGLE_STEp inc TotalInstruction invoke GetThreadContext,pi.hThread, \ addr context or context.regFlag,100h invoke SetThreadContext,pi.hThread, addr context invoke ContinueDebugEvent, DBEvent.dwprocessId, \ DBEvent.dwThreadId,DBG_CONTINUE .continue .endif .endif invoke ContinueDebugEvent, DBEvent.dwprocessId, DBEvent.dwThreadId, \ DBG_EXCEpTION_NOT_HANDLED .endw .endif invoke CloseHandle,pi.hprocess invoke CloseHandle,pi.hThread invoke Exitprocess, 0 end start
Анализ:
Программа показывает окно выбора файла. Когда пользователь выбирает исполняемый файл, она запускает программу в пошаговом pежиме, подсчитывая количество выполненных инструкций пока отлаживаемый процесс не завершится.


.elseif DBEvent.dwDebugEventCode==EXCEpTION_DEBUG_EVENT .if DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEpTION_BREAKpOINT
Мы используем эту возможность, чтобы установить отлаживаемый процесс в пошаговый pежим. Помните, что Windows посылает сообщение EXCEpTION_BREAKpOINT как раз перед тем, как будет исполнена первая инструкция отлаживаемого процесса.
mov context.ContextFlags, CONTEXT_CONTROL invoke GetThreadContext, pi.hThread, addr context
Мы вызываем GetThreadContext, чтобы заполнить структуру CONTEXT текущими значениями регистров отлаживаемого процесса. Конкретно нам нужно текущее значение регистра флагов.
or context.regFlag,100h
Мы устанавливаем traр-бит (8-ой бит) в образе регистра флагов.
invoke SetThreadContext,pi.hThread, addr context invoke ContinueDebugEvent, DBEvent.dwprocessId, \ DBEvent.dwThreadId, DBG_CONTINUE .continue
Затем мы вызываем SetThreadContext для перезаписи контекста новыми значениями и вызываем ContinueDebugEvent с флагом DBG_CONTINUE, чтобы продолжить выполнение отлаживаемого процесса.
.elseif DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEpTION_SINGLE_STEp inc TotalInstruction
Когда в отлаживаемом процессе выполняется инструкция, мы получаем EXCEрTION_DEBUG_EVENT. Мы должны проверить значение u.Exception.pExceptionRecord.ExceptionCode. Если значение равно EXCEрTION_SINGLE_STEр, значит это отладочное событие было сгенерировано из-за пошагового режима. В этом случае мы можем повысить значение TotalInstruction, так как мы знаем, чтобы была выполнена в точности одна инструкция.
invoke GetThreadContext,pi.hThread, \ addr context or context.regFlag, 100h invoke SetThreadContext,pi.hThread, addr context invoke ContinueDebugEvent, DBEvent.dwprocessId, \ DBEvent.dwThreadId, DBG_CONTINUE .continue
Так как traр-флаг очищается после генерации отладочного исключения, мы должны установить traр-флаг снова, если мы хотим продолжить выполнение в пошаговом режима. Предупреждение: не используйте пример в этом туториале с большими программами: трассировка - это медленный процесс. Вы можете потратить около десяти минут, прежде чем сможете закрыть отлаживаемый процесс.
[C] Iczelion, пер. Aquila.

Создание MDI-приложения


Этот туториал расскажет, как создать MDI-приложение. Это не так сложно.

Скачайте пример.

Теория:

Мультидокументный нтерфейс - это спецификация для приложений, которые обрабатывают несколько документов в одно и то же время. Вы знакомы с Noteрad'оам: это пример однодокументного интерфейса (SDI). Noteрad может обрабатывать только один документ за раз. Если вы хотите открыть другой документ, вам нужно закрыть предыдущий. Как вы можете себе представить, это довольно неудобно. Сравните его с Microsoft Word: тот может держать открытыми различные документы в одно и то же время и позволяет пользователю выбирать, какой документ использовать.

У MDI-приложений есть несколько характеристик, присущих только им. Я перечислю некоторые из них:

Внутри основного окна может быть несколько дочерних окон в пределах клиентской области.

Когда вы сворачиваете окно, он сворачивается к нижнему левому углу клиентской области основного окна.

Когда вы разворачиваете окно, его заголовок сливается с заголовком главного окна.

Вы можете закрыть дочернее окно, нажав Ctrl+F4 и переключатся между дочерними окнами, нажав на Ctrl+Tab.

Главное окно, которое содержит дочерние окно называется фреймовым окном. Его клиентская область - это место, где находятся дочерние окна, поэтому оно и называется фреймовым (на английском 'frame' означает "рамка, рама"). Его работа чуть более сложна, чем задачи обычного окна, так как оно обеспечивает работу MDI.

Чтобы контролировать дочерние окна в клиентской области, вам нужно специальное окно, которое называется клиентским окном. Вы можете считать это клиентское окно прозрачным окном, покрывающим всю клиенсткую область фреймового окна.

Фреймовое окно | Клиентское окно | | | | | | MDI Child 1 MDI Child 2 MDI Child 3 MDI Child 4 MDI Child n

Рисунок 1. Иерархия MDI-приложения

Создание фреймового окна

Теперь мы переключим наше внимание на детали. Прежде всего вам нужно создать фремовое окно. Оно создается примерно таким же образом, как и обычное окно: с помощью вызова CreateWindowEx. Есть два основных отличия от создания обычного окна.


Первое различие состоит в том, что вы ДОЛЖHЫ вызывать DefFramProc вместо DefWindowProc для обработки Windows-сообщение вашему окну, которые вы не хотите обрабатывать самостоятельно. Это единственный путь заставить Windows делать за вас гразную работу по управлению MDI-приложение. Если вы забудете использовать DefFramProc, ваше приложение не будет иметь MDI-свойств. DefFrameProc имеет следующий синтакс:
DefFrameProc proc hwndFrame:DWORD, hwndClient:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
Если вы сравните DefFramProc с DefWindowProc, вы заметите, что разница между ними состоит в том, что у DefFrameProc пять параметров, в то время как у DefWindowProc только четыре. Дополнительный параметр - это хэндл клиенсткого окна. Это хэндл необходим для того, чтобы Windows могла посылать MDI-сообщения клиенсткому окну.
Второе различие заключается в том, что вы должны вызывать TranslateMDISysAccel в цикле обработки сообщений вашего фреймового окна. Это необходим, если вы хотите, что Windows обрабатывала нажатия на комбинации клавиш, связанных с MDI, такие как Ctrl+F4, Ctrl+Tab. У этой функции следующий прототип:
TranslateMDISysAccel proc hwndClient:DWORD, lpMsg:DWORD
Первый параметр - это хэндл клиентского окна. Для вас не должно быть сюрпризом, что клиентское окно будет родителем окном для все дочерних MDI-окон. Второй параметр - это адрес MSG-структуры, котоурю вы заполните с помощью функции getMessage. Идея состоит в том, чтобы передать MSG-структуру клиентскому окну, что оно могло проверить, содержит ли эта структура искомые комбинации клавиш. Если это так, она обрабатывает само сообщение и возвращает ненулевое значение, или, в противном случае, FALSE.
Этапы создания фреймовое окно могут быть кратко просуммированы:
Заполняем структуру WNDCLASSEX как обычно.
Регистрируем класс фреймового окна, вызвая RegisterClassEx.
Создаем фреймовое окно с помощью CreateWindowEx.
Внутри цикла обработки сообщений вызываем TranslateMDISysAccel.
Внутри процедуры окна передаем необрабатанные сообщения DefFrameProc вместо DefWindowProc.


Создание клиентского окна
Теперь, когда у нас есть фреймовое окно, мы можем создать клиентское окно. Класс клиентского окна перерегистрирован Windows. Имя этого класса - "MDICLIENT". Вам также нужно передать адрес структуры CLIENTCREATESTRUCT функции CreateWindowEx. Эта структура имеет следующее определение:
CLIENTCREATESTRUCT struct hWindowMenu dd ? idFirstChild dd ? CLIENTCREATESTRUCT ends
hWindowMenu - это хэндл подменю, к которому Windows присоединит список имен дочерних MDI-окон. Здесь требуется некоторое пояснение. Если вы когда-нибудь использовали раньше MDI-приложение вроде Microsoft Word, вы могли заметить, что у него есть подменю под названием "window", которое при активации отображает различные пункты меню, связанные с управлением дочерними окнами, а также список открытых дочерних окон. Этот список создается самими Windows: вам не нужно прилагать специальных усилий. Всего лишь передайте хэндл подменю, к которому должен быть присоединен список, а Windows возьмет на себя все остальное. Обратите внимание, что подменю может быть любым: не обязательно тем, которое названно "window". Если вам не нужен список окон, просто передайте NULL в hWindowMenu. Получить хэндл подменю можно с помощью GetSubMenu.
idFirstChild - ID первого дочернего MDI-окна. Windows увеличивает ID на 1 для каждого нового MDI-окна, которое создает приложение. Например, если вы передает 100 через это поле, первое MDI-окно будет иметь ID 100, второе - 101 и так далее. Это ID посылается фреймовому окну через WM_COMMAND, когда дочернее MDI-окно выбрано из списка окон. Обычно вы будете передавать эти "необрабатываемые" сообщения процедуре DefFrameProc. Я использую слово "необрабатываемые", потому что пункты меню списка окон не создаются вашим приложением, поэтому ваше приложение не знает их ID и не имеет обработчика для них. Поэтому для фреймового окна есть специальное правило: если у вас есть список окон, вы должны модифицировать ваш обработчик WM_COMMAND так:


.elseif uMsg==WM_COMMAND
.if lParam==0 mov eax,wParam .if ax==IDM_CASCADE .....
.elseif ax==IDM_TILEVERT ..... . else invoke DefFrameProc, hwndFrame, hwndClient, uMsg,wParam, ret .endif
Обычно вам следует игнорировать сообщения о необрабатываемых событиях, но в случае с MDI, если вы просто проигнорируете их, то когда пользователь кликнет на имени дочернего MDI-окна, это окно не станет активным. Вам следует передавать управление DefFrameProc, чтобы они были правильно обработаны.
Я хочу предупредить вас относительно возможного значения idFirstChild: вам не следует использовать 0. Ваш список окон будет себя вести неправильно, то есть напротив пункта меню, обозначающего активное MDI-окно, не будет галочки. Лучше выберите какое-нибудь безопасное значение вроде 100 или выше.
Заполнив структуру CLIENTCREATESTRUCT, вы можете создать клиентское окно, вызвав CreateWindowEx, указав предопределенный класс "MDICLIENT" и передав адрес структуры CLIENTCREATESTRUCT через lParam. Вы должны также указать хэндл на фреймовое окно в параметре hWndParent, чтобы Windows знала об отношениях родитель-ребенок между фреймовым окно и клиентским окном. Вам следует использовать следующие стили окна: WS_CHILD, WS_VISIBLE и WS_CLIPCHILDREN. Если вы забудете указать стиль WS_VISIBLE, то не увидите дочерних MDI-окон, даже если они будут созданы успешно.
Этапы создания клиентского окна следующие:
Получить хэндл на подменю, к которому вы хотите присоединить список окон.
Поместите значение хэндла меню и значения, которое вы хотите использовать как ID первого дочернего MDI-окна в структуру CLIENCREATESTRUCT.
Вызовите CreateWindosEx, передав имя класса "MDICLIENT" и адрес структуры CLIENTCREATESTRUCT, которую вы только что заполнили, через lParam.
Создание дочернего MDI-окна.
Теперь у вас есть и фреймовое и клиентское окно. Теперь все готово для создания дочернего MDI-окна. Есть два пути сделать это.
Вы можете послать сообщение WM_MDICREATE клиентскому окн, передав тому адрес структуры типа MDICREATESTRUCT через wParam. Это простейший и наиболее часто используемый способ создания дочерних MDI-окон.


.data? mdicreate MDICREATESTRUCT <> .... .code ..... [ fill the members of mdicreate] [ заполняем поля структуры mdicreate ] ...... invoke SendMessage, hwndClient, WM_MDICREATE,addr mdicreate,0
Функция SendMessage возвратит хэндл только что созданного дочернего MDI-оанк, если все пройдет успешно. Вам не нужно сохранять хэндл. Вы можете получить его каким-либо другим образом, если хотите. У структуры MDICREATESTRUCT следующее определение:
MDICREATESTRUCT STRUCT szClass DWORD ? szTitle DWORD ? hOwner DWORD ? x DWORD ? y DWORD ? lx DWORD ? ly DWORD ? style DWORD ? lParam DWORD ? MDICREATESTRUCT ENDS
szClass - адрес класса окна, который вы хотите использовать в качестве шаблона для дочернего MDI-окна.
szTitle - адрес строки, которая должна появиться в заголовке дочернего окна.
hOwner - хэндл приложения.
x, y, lx, ly - верхняя левая координата, ширина и высота дочернего окна.
style - стиль дочернего окна. Если вы создали клиентское окно со стилем MDIS_ALLCHILDSTYLES, то вы можете использовать все стили.
lParam - определяемое программистом 32-х битное значение. Используется для передачи значение между MDI-окнами. Если у вас нет подобной нужны, просто поставьте их в NULL.
Вы можете вызвать CreateMDIWindow. Эта функция имеет следующий синтаксис:
CreateMDIWindow proto lpClassName:DWORD lpWindowName:DWORD dwStyle:DWORD x:DWORD y:DWORD nWidth:DWORD nHeight:DWORD hWndParent:DWORD hInstance:DWORD lParam:DWORD
Если вы внимательно посмотрите на параметры, вы увидите, что они идентичны параметрам структуры MDICREATESTRUCT не считая hWndParent. Очевидно, что точно такое количество параметров вы передавали вместе с сообщением WM_MDICREATE. У структуры MDICREATESTRUCT нет поля hWndParent, потому что вы все равно должны передавать структуру клиентскому окну с помощью функции SendMessage.
Сейчас вы можете спросить: какой метод я должен выбрать? Какая разница между этими двумя методами? Вот ответ:
Метод WM_MDCREATE создает дочернее MDI-окно в том же треде, что и вызывающий код. Это означает, что если у приложения есть только одна ветвь, все дочерние MDI-окна будут выполняться в контексте основной ветви. Это не слишком большая проблема, если одно или более из дочерних MDI-окон не выполняет какие-либо продолжительные операции, что может стать проблемой. Подумайте об этом, иначе в какой-то момент все ваше приложение внезапно зависнет, пока операция не будет выполнена.


Эта проблема как раз то, что призвана решить функция CreateMDIWindow. Она создает отдельный тред для каждого из дочерних MDI-окон, поэтому если одно из них занято, оно не приводит к зависанию всего приложения.
Необходимо сказать несколько слов относительно оконной процедуры дочернего MDI-окна. Как и в случае с фреймовым окном, вы не должны вызывать DefWindowProc, чтобы обработать необрабатываемые вашим приложением сообщением. Вместо этого вы должны использовать DefMDIChildProc. У этой функции точно такие же параметры, как и у DefWindowProc.
Кроме WM_MDICREATE, есть еще несколько сообщений, относящихся к MDI-окнам.
Я приведу их описание:
WM_MDIACTIVATE - это сообщение может быть послано приложением клиентскому окну, чтобы последнее активировало выбранное дочернее MDI-окно. Когда клиентское окно получает сообщение, оно активирует выбранное дочернее MDI-окно, а также посылает это же сообщение окну, которое было активированы или дезактивировано. Таким образом, данное сообщение имеет двойное назначение: с его помощью приложение может активировать выбранное дочернее окно, а также оно может быть использовано дочерним MDI-приложением для определения того, активировано оно или нет. Например, если каждое дочернее MDI-окно имеет различно меню, оно может использовать эту возможность для изменения меню фреймового окна при активации/дезактивации.
WM_MDICASCADE, WM_MDITILE, WM_MDICONARRANGE - эти сообщения отвечаю за расположение дочерних MDI-окон. Hапример, если вы хотите, чтобы дочерние MDI-окна pасположились каскадом, пошлите сообщение WM_MDICASCADE клиентскому окну.
WM_MDIDESTROY - пошлите это сообщение клиентскому окну, если хотите уничтожить дочернее MDI_окно. Вам следует использовать это сообщение вместо DestroyWindow, потому что если дочернее MDI-приложение максимизировано, это сообщение восстановить заголовок фреймового окна, что не будет сделано в случае применения функции DestroyWindow.
WM_MDIGETACTIVE - используйте это сообщение, чтобы получить хэндл активного в настоящий момент дочернего MDI-окна.


WM_MDIMAXIMIZE, WM_MDIRESTORE - используйте WM_MDIMAXIZE для разворачивания дочернего MDI-окна и WM_MDIRESTORE для его восстановления. Всегда используйте эти сообщения для данных операций. Если вы используете ShowWindow с SW_MAXIMIZE, дочернее MDI-окно будет развернуто, но у вас появятся проблемы, когда вы захотите восстановить его до прежнего размера. Вы можете минимизировать окно с помощью ShowWindow без всяких проблем.
WM_MDINEXT - посылайте это сообщение клиентскому окну, чтобы активировать следующее или предыдущее дочернее MDI-окно, согласно значению wParam и lParam.
WM_MDIREFRESHMENU - это сообщение посылается клиентскому окну, чтобы обновить меню фреймового окна. Обратите внимание, что вы должны вызвать DrawMenuBar для обновления меню баp после отсылки данного сообщения.
WM_MDISETMENU - посылайте это сообщение клиентскому окну, что полностью заменить меню фреймового окна или только подменю окон. Вы должны использовать данное сообщение вместо SetMenu. После того, как вы отослали данное сообщение, вы должны вызвать DrawMenuBar. Обычно вы будете использовать это сообщение, когда у активного дочернего MDI-окна есть свое меню и вы хотите, чтобы оно заменяло меню фреймового окна, пока это дочернее MDI-окно активно.
Я сделаю небольшое обозрение создания MDI-приложения еще раз:
Регистрируем классы окна, фреймового класса и дочернего MDI-окна.
Создаем фреймовое окно с помощью CreateWindowEx.
Внутри цикла обработки сообщений вызываем TranslateMDISysAccel, чтобы обработать "горячие клавиши", относящиеся к MDI.
Внутри оконной процедуры фреймового окна вызываем DefFramProc, чтобы обрабатывать все сообщения, необрабатываемые приложением.
Создаем клиентское окно, вызвав CreateWindowEx, которой передаем имя предопределенного класса окна, "MDICLIENT", передавая адрес структуры CLIENTCREATESTRUCT через lParam. Обычно вы будете создавать клиентское окно внутри обработчика сообщения WM_CREATE фреймового окна.
Вы можете создать дочернее MDI-окно, послав клиентскому окну сообщение WM_MDICREATE или вызвав функцию CreateMDIWindow.


Внутри оконной процедуры дочернего MDI-ОКHА, передаем все необработанные сообщения DefMDIChildProc.
Используем MDi- версии сообщений, если таковые существуют. Hапример, вместо WM_MDIDESTROY вместо DestroyWindow.
ПРИМЕР
.386 .model flat,stdcall option casemap:none include \masm32\include\windows.inc include \masm32\include\user32.inc include \masm32\include\kernel32.inc includelib \masm32\lib\user32.lib includelib \masm32\lib\kernel32.lib WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
.const IDR_MAINMENU equ 101 IDR_CHILDMENU equ 102 IDM_EXIT equ 40001 IDM_TILEHORZ equ 40002 IDM_TILEVERT equ 40003 IDM_CASCADE equ 40004 IDM_NEW equ 40005 IDM_CLOSE equ 40006
.data ClassName db "MDIASMClass",0 MDIClientName db "MDICLIENT",0 MDIChildClassName db "Win32asmMDIChild",0 MDIChildTitle db "MDI Child",0 AppName db "Win32asm MDI Demo",0 ClosePromptMessage db "Are you sure you want to close this window?",0
.data? hInstance dd ? hMainMenu dd ? hwndClient dd ? hChildMenu dd ? mdicreate MDICREATESTRUCT <> hwndFrame dd ?
.code start: invoke GetModuleHandle, NULL mov hInstance,eax invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT invoke ExitProcess,eax
WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD LOCAL wc:WNDCLASSEX LOCAL msg:MSG ;============================================= ; Register the frame window class ;============================================= mov wc.cbSize,SIZEOF WNDCLASSEX mov wc.style, CS_HREDRAW or CS_VREDRAW mov wc.lpfnWndProc,OFFSET WndProc mov wc.cbClsExtra,NULL mov wc.cbWndExtra,NULL push hInstance pop wc.hInstance mov wc.hbrBackground,COLOR_APPWORKSPACE mov wc.lpszMenuName,IDR_MAINMENU mov wc.lpszClassName,OFFSET ClassName invoke LoadIcon,NULL,IDI_APPLICATION mov wc.hIcon,eax mov wc.hIconSm,eax invoke LoadCursor,NULL,IDC_ARROW mov wc.hCursor,eax invoke RegisterClassEx, addr wc ;================================================ ; Register the MDI child window class ;================================================ mov wc.lpfnWndProc,offset ChildProc mov wc.hbrBackground,COLOR_WINDOW+1 mov wc.lpszClassName,offset MDIChildClassName invoke RegisterClassEx,addr wc invoke CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\ WS_OVERLAPPEDWINDOW or WS_CLIPCHILDREN,CW_USEDEFAULT, CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,0,\ hInst,NULL mov hwndFrame,eax invoke LoadMenu,hInstance, IDR_CHILDMENU mov hChildMenu,eax invoke ShowWindow,hwndFrame,SW_SHOWNORMAL invoke UpdateWindow, hwndFrame .while TRUE invoke GetMessage,ADDR msg,NULL,0,0 .break .if (!eax) invoke TranslateMDISysAccel,hwndClient,addr msg .if !eax invoke TranslateMessage, ADDR msg invoke DispatchMessage, ADDR msg .endif .endw invoke DestroyMenu, hChildMenu mov eax,msg.wParam ret WinMain endp


WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM LOCAL ClientStruct:CLIENTCREATESTRUCT .if uMsg==WM_CREATE invoke GetMenu,hWnd mov hMainMenu,eax invoke GetSubMenu,hMainMenu,1 mov ClientStruct.hWindowMenu,eax mov ClientStruct.idFirstChild,100 INVOKE CreateWindowEx,NULL,ADDR MDIClientName,NULL,\ WS_CHILD or WS_VISIBLE or WS_CLIPCHILDREN,CW_ CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,hWn hInstance,addr ClientStruct mov hwndClient,eax ;======================================= ; Initialize the MDICREATESTRUCT ;======================================= mov mdicreate.szClass,offset MDIChildClassName mov mdicreate.szTitle, offset MDIChildTitle push hInstance pop mdicreate.hOwner mov mdicreate.x,CW_USEDEFAULT mov mdicreate.y,CW_USEDEFAULT mov mdicreate.lx,CW_USEDEFAULT mov mdicreate.ly,CW_USEDEFAULT .elseif uMsg==WM_COMMAND .if lParam==0 mov eax,wParam .if ax==IDM_EXIT invoke SendMessage,hWnd,WM_CLOSE,0,0 .elseif ax==IDM_TILEHORZ invoke SendMessage,hwndClient,WM_MDITILE,MDIT .elseif ax==IDM_TILEVERT invoke SendMessage,hwndClient,WM_MDITILE,MDIT .elseif ax==IDM_CASCADE invoke SendMessage,hwndClient,WM_MDICASCADE,M .elseif ax==IDM_NEW invoke SendMessage,hwndClient,WM_MDICREATE,0, .elseif ax==IDM_CLOSE invoke SendMessage,hwndClient,WM_MDIGETACTIVE invoke SendMessage,eax,WM_CLOSE,0,0 .else invoke DefFrameProc,hWnd,hwndClient,uMsg,wPar ret .endif .endif .elseif uMsg==WM_DESTROY invoke PostQuitMessage,NULL .else invoke DefFrameProc,hWnd,hwndClient,uMsg,wParam,lParam ret .endif xor eax,eax ret WndProc endp
ChildProc proc hChild:DWORD,uMsg:DWORD,wParam:DWORD,lParam:DWORD .if uMsg==WM_MDIACTIVATE mov eax,lParam .if eax==hChild invoke GetSubMenu,hChildMenu,1 mov edx,eax invoke SendMessage,hwndClient,WM_MDISETMENU,hChildMen .else invoke GetSubMenu,hMainMenu,1 mov edx,eax invoke SendMessage,hwndClient,WM_MDISETMENU,hMainMenu .endif invoke DrawMenuBar,hwndFrame .elseif uMsg==WM_CLOSE invoke MessageBox,hChild,addr ClosePromptMessage,addr AppName .if eax==IDYES invoke SendMessage,hwndClient,WM_MDIDESTROY,hChild,0 .endif .else invoke DefMDIChildProc,hChild,uMsg,wParam,lParam ret .endif xor eax,eax ret ChildProc endp end start


АНАЛИЗ
Первое, что должна сделать программа - это зарегистрировать классы фреймового и дочернего MDI-окна. После этого она вызывает функцию CreateWindowEx, чтобы создать фреймовое окно. Внутри обработчика WM_CREATE фреймового окна мы создаем клиентское окно:
LOCAL ClientStruct:CLIENTCREATESTRUCT .if uMsg==WM_CREATE invoke GetMenu,hWnd mov hMainMenu,eax invoke GetSubMenu,hMainMenu,1 mov ClientStruct.hWindowMenu,eax mov ClientStruct.idFirstChild,100 invoke CreateWindowEx,NULL,ADDR MDIClientName,NULL,\ WS_CHILD or WS_VISIBLE or WS_CLIPCHILDREN,CW_USEDEFAU CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,hWnd,NULL,\ hInstance,addr ClientStruct mov hwndClient,eax
Здесь мы вызываем GetMenu, чтобы полуть хэндл меню фреймового окна, который будем использовать в GetSubMenu. Обратите внимание, что мы передаем 1 функции GetSubMenu, потому что подменю, к которому мы будем присоединять список окон, является вторым подменю. Затем мы заполняем параметры структуры CLIENTCREATESTRUCT.
Затем мы инициализируем структуру MDCLIENTSTRUCT. Обратите внимание, что мы не обязаны делать это здесь. Просто это удобнее осуществлять в обработчике WM_CREATE.
mov mdicreate.szClass,offset MDIChildClassName mov mdicreate.szTitle,offset MDIChildTitle push hInstance pop mdicreate.hOwner mov mdicreate.x,CW_USEDEFAULT mov mdicreate.y,CW_USEDEFAULT mov mdicreate.lx,CW_USEDEFAULT mov mdicreate.ly,CW_USEDEFAULT
После того, как фреймовое окно создано (так же как клиентское окно), мы вызывает LoadMenu, чтобы загрузить меню дочернего окна из ресурса. Hам нужно получить хэндл этого меню, чтобы мы могли заменить меню фреймового окна, когда дочернее MDI-окно становится активным. Hе забудьте вызвать DestroyMenu, прежде чем приложение завершит работу. Обычно Windows сама освобождает память, занятую меню, но в данном случае этого не произойдет, так как меню дочернего окна не ассоциировано ни с каким окном, поэтому оно все еще будет занимать ценную память, хотя приложение уже прекратило свое выполнение.
invoke LoadMenu,hInstance, IDR_CHILDMENU mov hChildMenu,eax ........ invoke DestroyMenu, hChildMenu


Внутри цикла обработки сообщений, мы вызываем TranslateMDISysAccel.
.while TRUE invoke GetMessage,ADDR msg,NULL,0,0 .break .if (!eax) invoke TranslateMDISysAccel,hwndClient,addr msg .if !eax invoke TranslateMessage, ADDR msg invoke DispatchMessage, ADDR msg .endif .endw
Если TranslateMDISysAccel возвращает ненулевое значение, это означает, что собщение уже было обработано Windows, поэтому вам не нужно делать что-либо с ним. Если был возвращен 0, сообщение не относится к MDI и поэтому должно обрабатываться как обычно.
WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM ..... .else invoke DefFrameProc,hWnd,hwndClient,uMsg,wParam,lParam ret .endif xor eax,eax ret WndProc endp
Обратите внимание, что внутри оконной процедуры фреймового окна мы вызываем DefFrameProc для обработки сообщений, которые не представляют для нас интереса.
Основной часть процедуры окна является обработчик сообщения WM_COMMAND. Когда пользователь выбирает в меню пункт "New", мы создает новое дочернее MDI-окно.
.elseif ax==IDM_NEW invoke SendMessage,hwndClient,WM_MDICREATE,0,addr mdicreate
В нашем примере мы создаем дочернее MDI-окно, посылая WM_MDIREATE клиентскому окну, передавая адрес структуры MDICREATESTRUCT через lParam.
ChildProc proc hChild:DWORD,uMsg:DWORD,wParam:DWORD,lParam:DWORD .if uMsg==WM_MDIACTIVATE mov eax,lParam .if eax==hChild invoke GetSubMenu,hChildMenu,1 mov edx,eax invoke SendMessage,hwndClient,WM_MDISETMENU,hChildMen .else invoke GetSubMenu,hMainMenu,1 mov edx,eax invoke SendMessage,hwndClient,WM_MDISETMENU,hMainMenu .endif invoke DrawMenuBar,hwndFrame
Когда создано дочернее MDI-окно, оно отслеживает сообщение WM_MDIACTIVATE, чтобы определить, является ли оно в данный момент активным. Оно делает это сравнивая значение lParam, которое содержит хэндл активного дочернего окна со своим собственным хэндлом.
Если они совпадают, значит оно является активным и следующим шагом будет замена меню фреймового окна на свое собственное. Так как изначально меню будет заменено, вам надо будет указать Windows снова в каком подменю должен появиться список окон. Поэтому мы должны снова вызвать функцию GetSubMenu, чтобы получить хэндл подменю. Мы посылаем сообщение WM_MDISETMENU клиентскому окну, достигая, таким образом, желаемого результата. Параметр wParam сообщения WM_MDISETMENU содержит хэндл меню, которое заменит оригинальное. lParam содержит хэндл подменю, к которому будет присоединен список окон. Сразу после отсылки сообщения WM_MDISETMENU, мы вызываем DrawMenuBar, чтобы обновить меню, иначе произойдет большая путаница.


.else invoke DefMDIChildProc,hChild,uMsg,wParam,lParam ret .endif
Внутри оконной процедуры дочернего MDI- окна вы должны передать все необработанные сообщения функции DefMDIChildProc.
.elseif ax==IDM_TILEHORZ invoke SendMessage,hwndClient,WM_MDITILE,MDITILE_HORIZONTAL,0 .elseif ax==IDM_TILEVERT invoke SendMessage,hwndClient,WM_MDITILE,MDITILE_VERTICAL,0 .elseif ax==IDM_CASCADE invoke SendMessage,hwndClient,WM_MDICASCADE,MDITILE_SKIPDISAB
Когда пользователь выбирает один из пунктов меню в подменю окон, мы посылаем соответствующее сообщение клиентскому окну. Если пользователь выбирает один из методов расположения окон, мы посылаем WM_MDITILE или WM_CASCADE.
.elseif ax==IDM_CLOSE invoke SendMessage,hwndClient,WM_MDIGETACTIVE,0,0 invoke SendMessage,eax,WM_CLOSE,0,0
Если пользователь выбирает пункт меню "Close", мы должны получить хэндл текущего активного MDI-окна.
.elseif uMsg==WM_CLOSE invoke MessageBox,hChild,addr ClosePromptMessage,addr AppName .if eax==IDYES invoke SendMessage,hwndClient,WM_MDIDESTROY,hChild,0 .endif
Когда процедура дочернего MDI-окна получает сообщение WM_CLOSE, его обработчик отображает окно, которое спрашивает пользователя, действительно ли он хочет закрыть окно. Если ответ - "Да", то мы посылаем клиентскому окну сообщение WM_MDIDESTROY, которое закрывает дочернее MDI-окно и восстанавливает заголовок фреймового окна.
[C] Iczelion, пер. Aquila.

Контрол Richedit: Основы.


Загрузите пример.

Теория:

О контроле можно думать, как о функционально-расширенном средстве редактирования. Он обеспечивает множество полезных особенностей, которых нет в простых средствах редактирования, например, возможность использовать множество видов и размеров шрифта, глубокий уровень отмены/восстановления, операцией поиска по тексту, встроенные OLE-объекты, поддержка редактирования методом перетаскивания (drag-and-drop), и т.д. Так как richedit контрол имеет так много особенностей, он сохранен в отдельной DLL-библиотеке. Это также означает что, чтобы его использовать, вам недостаточно просто вызывать InitCommonControls, как в других Common-контролах. Вы должны вызвать LoadLibrary, чтобы загрузить richedit DLL.

Проблема состоит в том, что в настоящее время есть уже три версии richedit контрола. Версия 1,2, и 3. В таблице ниже показаны имена DLL для каждой версии.

имя DLLверсия RichEdit'аИмя класса Richedit'аRiched32.dll1.0RICHEDITRichEd20.dll2.0RICHEDIT20A

RichEd20.dll3.0RICHEDIT20A

Обратите внимание, что richedit версия 2 и 3 использует то же самое имя DLL. Они также используют одно и то же имя класса! Это может вызвать проблему, если Вы захотите использовать определенные особенности richedit'а версии 3.0. До сих пор, я не нашел официального метода различия между версиями 2.0 и 3.0. Однако, есть рабочий пример, который хорошо работает, я покажу Вам позже.

.data RichEditDLL db "RichEd20.dll", 0 ..... .data? HRichEditDLL dd? .code invoke LoadLibrary, addr RichEditDLL Mov hRichEditDLL, eax ...... invoke FreeLibrary, hRichEditDLL

Когда richedit dll загружена, она регистрирует класс окна RichEdit. Следовательно вам необходимо загрузить DLL прежде, чем Вы создадите контрол. Имена классов richedit контрола также различны. Теперь Вы можете задать вопрос: а какую версию richedit контрола мне следует использовать? Использование самой последней версии не всегда подходит, если Вы не требуете дополнительных особенностей. В таблице ниже, показаны особенности каждой версии richedit контрола.



ОсобенностьВерсия 1.0Версия 2.0Версия 3.0
Выделение областиxxx
Редактирование уникода xx
Форматирование символа/абзацаxxx
Поиск по текстуВпередВперед/назадВперед/назад
Внедрение OLExxx
Редактирование перетащить и отпустить(drag-and-drop)xxx
Отменить/ПовторитьОдноуровневыйМногоуровневыйМногоуровневый
Автоматическое распознавание URL xx
Поддержка клавиш быстрого доступа(hot key) xx
операция уменьшения окна xx
Конец строкиCRLFтолько CRтолько CR(может подражать версии 1.0)
Увеличение  x
Нумерация абзацев  x
Простая таблица  x
Нормальный и заголовочный стили  x
Цветное подчеркивание  x
Скрытый текст  x
связывание шрифта  x

Вышеуказанная таблица ни в коем случае не полная: я только перечислил важные особенности.
Создание richedit контрола
После загрузки richedit dll, Вы можете вызывать CreateWindowEx, для создания контрола. Вы можете использовать стили средств редактирования и common windows стили в CreateWindowEx кроме ES_LOWERCASE, ES_UPPERCASE и ES_OEMCONVERT.
.const RichEditID equ 300 .data RichEditDLL db "RichEd20.dll", 0 RichEditClass db "RichEdit20A", 0 ..... .data? HRichEditDLL dd ? HwndRichEdit dd ? .code ..... invoke LoadLibrary, addr RichEditDLL Mov hRichEditDLL, eax invoke CreateWindowEx, 0, addr RichEditClass,\ WS_VISIBLE or ES_MULTILINE or WS_CHILD or WS_VSCROLL or WS_HSCROLL, \ CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, \ hWnd, RichEditID, hInstance, 0 Mov hwndRichEdit, eax
Установка по умолчанию цвета текста и фона
У вас может возникнуть проблема с установкой цвета текста и фона в средствах редактирования. Но эта проблема была исправлена в richedit контроле. Чтобы установить цвет фона richedit контрола, вам нужно послать ему EM_SETBKGNDCOLOR. Это сообщение имеет следующий синтаксис:
WParam == цвет. Значение 0 в этом параметре означает, что Windows использует значение цвета из lParam как цвет фона. Если это значение отличное от нуля, Windows использует цвет фона системы Windows. Так как мы посылаем это сообщение, чтобы изменить цвет фона, мы должны поместить 0 в wParam.


LParam == определяет структуру COLORREF цвета, который Вы хотите установить, если wParam - 0.
Например, если бы я захотел установить цвет фона в синий, я бы поместил этот код:
invoke SendMessage, hwndRichEdit, EM_SETBKGNDCOLOR, 0,0FF0000h
Устанавливая цвет текста, richedit контрол создает новое сообщение, EM_SETCHARFORMAT. Это сообщение управляет форматированием текста в диапазоне от символа в выделении до всего текста. Это сообщение имеет следующий синтаксис:
WParam == опции форматирования:

SCF_ALLОперация затрагивает весь текст в контроле.
SCF_SELECTIONОперация затрагивает только выделенный текст
SCF_WORD или SCF_SELECTIONЗатрагивает слово в выделении. Если ничего не выделенно, то операция затронет то слово на котором находится курсор. Флаг SCF_WORD должен использоваться с SCF_SELECTION.

LParam == указатель на структуру CHARFORMAT ИЛИ CHARFORMAT2, которая определяет форматирование текста, которое нужно применить. CHARFORMAT2 доступен только для richedit 2.0 и выше. Это не подразумевает, что Вы должны использовать CHARFORMAT2 с RichEdit 2.0 и выше. Вы все еще можете использовать CHARFORMAT, если добавленные в CHARFORMAT2 особенности вам не нужны.
CHARFORMATA STRUCT CbSize DWORD ? DwMask DWORD ? DwEffects DWORD ? YHeight DWORD ? YOffset DWORD ? CrTextColor COLORREF ? BCharSet BYTE ? BPitchAndFamily BYTE ? SzFaceName BYTE LF_FACESIZE dup(?) _wPad2 WORD ? CHARFORMATA ENDS

Имя поляОписание
CbSizeРазмер структуры. RichEdit контрол использует это поле, чтобы определить версию структуры, является ли это CHARFORMAT или CHARFORMAT2
DwMaskРазряды флагов, которые определяют, какие из следующих членов являются правильными.
CFM_BOLDCFE_BOLD член dwEffects правильный
CFM_CHARSET член BCharSet- правильный.
CFM_COLOR член CrTextColor и значение CFE_AUTOCOLOR члена dwEffects - правильные
CFM_FACE член SzFaceName правильный.
CFM_ITALICЗначение CFE_ITALIC члена dwEffects правильное
CFM_OFFSET член YOffset правильный
CFM_PROTECTEDЗначение CFE_PROTECTED члена dwEffects правильное
CFM_SIZE член YHeight правильный
CFM_STRIKEOUTЗначение CFE_STRIKEOUT члена dwEffects правильное.
CFM_UNDERLINEЗначение CFE_UNDERLINE члена dwEffects правильное
<


tr>DwEffects Символьные эффекты. Может быть комбинация следующих значений

CFE_AUTOCOLORИспользует системный цвет текста
CFE_BOLDПолужирный
CFE_ITALICКурсивный
CFE_STRIKEOUTПеречеркнутый.
CFE_UNDERLINEПодчеркнутый.
CFE_PROTECTEDСимволы защищены; попытка изменять их вызовет уведомительное сообщение EN_PROTECTED.

YHeightВысота символов, в twips (1/1440 дюйма или 1/20 точки принтера). YOffsetСмещение Символа, в twips (1/1440 дюйма или 1/20 точки принтера), от основной линии. Если значение этого члена положительное, символ - верхний индекс; если - отрицательное - нижний индекс. CrTextColor Цвет текста. Этот член игнорируется, если эффект символа CFE_AUTOCOLOR определен. BCharSetНабор символов BPitchAndFamilyСемейство Шрифта и шаг. SzFaceName Символьный массив с нулевым символом в конце, определяющий имя шрифта _wPad2Дополнение Из исследования структуры, Вы увидите, что мы можем изменять эффекты текста (полужирный, курсивный, зачеркнутый, подчеркнутый), цвет текста (crTextColor) и шрифт (вид/размер/набор символов). Текст с флагом CFE_RPOTECTED, отмечен как защищенный, это означает, что, когда пользователь попытается изменить его, то родительскому окну будет послано уведомительное сообщение EN_PROTECTED. И Вы можете либо разрешить, либо запретить изменения.
CHARFORMAT2 добавляет большее количество текстовых стилей, таких как weight шрифта, интервала, цвета фона текста, кернинга, и т.д. Если Вы не нуждаетесь в этих дополнительных особенностях, просто используйте CHARFORMAT.
Чтобы установить формат текста, Вы должны указать диапазон текста, к которому хотите применить формат. Richedit контрол присваивает каждому символу свой номер (идентификатор), начинающийся с 0: первый символ имеет идентификатор 0, второй - 1 и так далее. Чтобы указать диапазон текста, вы должны дать richedit контролу, два числа: ИДЕНТИФИКАТОРЫ первого и последнего символа диапазона. Чтобы применить форматирование к тексту с EM_SETCHARFORMAT, у вас есть 3 выбора:
Применить ко всему тексту (SCF_ALL) Применить к выделенному тексту (SCF_SELECTION) Применить к целому слову в выделенной области (SCF_WORD or SCF_SELECTION)


Первый и второй выборы прямые, последний требует некоторых объяснений. Если текущее выделение охватывает только один или большее количество символов в слове, но не, целое слово, определяя флаг SCF_WORD+SCF_SELECTION, применяется форматирование текста к целому слову. Даже если ничего не выделено, форматирование применяется к целому слову, над которым находится курсор вставки.
Чтобы использовать EM_SETCHARFORMAT, Вы должны заполнить некоторые члены структуры CHARFORMAT (или CHARFORMAT2). Например, если мы хотим установить цвет текста, мы заполним структуру CHARFORMAT следующим образом:
.data? Cf CHARFORMAT < > .. .code Mov cf.cbSize, sizeof cf Mov cf.dwMask, CFM_COLOR Mov cf.crTextColor, 0FF0000h invoke SendMessage, hwndRichEdit, EM_SETCHARFORMAT, \ SCF_ALL, addr cf
Этот фрагмент кода устанавливает цвет текста richedit контрола в синий. Обратите внимание, что, если в richedit контроле нет никакого текста, когда мы посылаем сообщение EM_SETCHARFORMAT, текст, введенный в richedit контроле после сообщения будет использовать формат текста, указанный с сообщением EM_SETCHARFORMAT.
Установка текста/сохранение текста
Те, кто уже использовали Edit контролы, наверняка знакомы с WM_GETTEXT/WM_SETTEXT, для установки/получения текста в\из контрола. Этот метод работает и с richedit контролом, но не может работать с большими файлами. Edit контрол ограничивает текст, который может быть введен в него 64КБ, но richedit контрол может принимать текст намного больше. Это было бы не правильное решение, выделить очень большой блок памяти (типа 10 мб) чтобы получить текст с помощью WM_GETTEXT. Richedit контрол предлагает новый подход к этому методу, такой как текстовый поток.
Сделать это просто, вы передаете адрес функции richedit контролу. И richedit контрол вызовет эту функцию, передавая ей адрес буфера, когда он готов. Функция заполнит буфер данными, которые требуется послать контролу или считает данные из буфера и будет ждать следующего запроса, пока операция не закончена. Эта парадигма используется для обоих операций: потоковый ввод (установка текста) и потоковый вывод (получение текста из контрола). Вы увидите, что этот метод более эффективен: буфер обеспечивается richedit контролом непосредственно, так что данные разделены на куски. Операции включают два сообщения: EM_STREAMIN и EM_STREAMOUT


Оба сообщения EM_STREAMIN и EM_STREAMOUT используют одинаковый синтаксис:
WParam == опции.

SF_RTFДанные в формате RTF (rich-text format)
SF_TEXTДанные в формате открытого текста
SFF_PLAINRTF Только ключевые слова, общие ко всем языкам в потоке.
SFF_SELECTIONЕсли цель операции - выделенный текст. Если вводите текст, он заменяет текущее выделение. Если вы выводите текст, то будет выведен только выделенный в настоящее время текст. Если этот флаг не определен, операция работает со всем текстом в контроле.
SF_UNICODE( Доступна для RichEdit 2.0 и выше) Определяют текст уникода.

LParam == указывают на структуру EDITSTREAM, которая имеет следующее определение:
EDITSTREAM STRUCT DwCookie DWORD ? DwError DWORD ? PfnCallback DWORD ? EDITSTREAM ENDS

DwCookieОпределенное приложением значение, которое будет передаваться к функции определенной в члене pfnCallback ниже. Мы обычно передаем функции некоторые важные значения, такие как хэндл файла, для использования в stream-in/out процедуре.
DwErrorОтображает результат операции stream-in (чтения) или stream-out (записи). Значение 0 - нет ошибон. Значение отличное от нуля может быть результатом функции EditStreamCallback или кода, указывающего, что произошла ошибка.
PfnCallbackУказатель на функцию EditStreamCallback, которая является определенной приложением функцией, которая управляет передачей данных. Управление вызывает функцию неоднократно, передавая часть данных с каждым запросом

Editstream функция имеет следующее определение:
EditStreamCallback proto dwCookie:DWORD, PBuffer:DWORD, NumBytes:DWORD, PBytesTransferred:DWORD
Вы должны создать функцию с вышеупомянутым прототипом в вашей программе. И затем передайте ее адрес с помощью EM_STREAMIN или EM_STREAMOUT через структуру EDITSTREAM.
Для операции stream-in (загрузка текста в richedit контрол):
DwCookie: определенное приложением значение вы передаете с EM_STREAMIN через структуру EDITSTREAM. Мы почти всегда передаем хэндл файла, содержание которого мы хотим установить в контрол.


PBuffer: указывает на буфер, переданный richedit контролом, который получит текст от вашей функции.

NumBytes: максимальное количество байт, которые вы может записать в буфер (pBuffer) в этом запросе. Вы всегда ДОЛЖНЫ придерживаться этого предела, т.е., вы можете посылать меньшее количество данных чем значение в NumBytes, и не должны послать большее количество данных чем это значение. Вы можете считать это значение, как размер буфера pBuffer.

pBytesTransferred: указывает на переменную (dword), указывающую число байтов, которые вы фактически передали в буфер. Это значение обычно идентично значению в NumBytes. Исключение когда данных послано меньше, чем размер буфера, обычно когда достигнут конец файла.
Для операции stream-out (получение текста из richedit контрола):
DwCookie: Такой же, как в операции stream-in. Мы обычно передаем хэндл файла, в который мы хотим записать данные.

PBuffer: указывает на буфер, обеспеченный richedit контролом, который заполнен данными из richedit контрола. Чтобы получить его размер, Вы должны посмотреть значение NumBytes.

NumBytes: размер данных в буфере, указанном в pBuffer.

PBytesTransferred: указывает на переменную (dword), в которую вы должны установить значение, индицирующее число байт, которые Вы фактически считали из буфера.
Функция возвращает 0, если не было ошибок, и richedit контрол продолжит вызывать функцию, если данные все еще есть. Если произошла ошибка в течение процесса, и вы хотите остановить операцию, верните ненулевое значение, и richedit контрол откажется от данных, указанных в pBuffer. Значение ошибки/успеха будет заполнено в поле dwError структуры EDITSTREAM, так что вы можете проверить результат операции после возврата изSendMessage.
ПРИМЕР
Пример ниже это - простой редактор, которым Вы можете открывать файлы исходного текста ассемблера, редактировать, и сохранять их. Он использует RichEdit версии 2.0 или выше.
.386.model flat,stdcall option casemap:none include \masm32\include\windows.inc include \masm32\include\user32.inc include \masm32\include\comdlg32.inc include \masm32\include\gdi32.inc include \masm32\include\kernel32.inc includelib \masm32\lib\gdi32.lib includelib \masm32\lib\comdlg32.lib includelib \masm32\lib\user32.lib includelib \masm32\lib\kernel32.lib


WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
. const IDR_MAINMENU equ 101 IDM_OPEN equ 40001 IDM_SAVE equ 40002 IDM_CLOSE equ 40003 IDM_SAV equ 40004 IDM_EXIT equ 40005 IDM_COPY equ 40006 IDM_CUT equ 40007 IDM_PASTE equ 40008 IDM_DELETE equ 40009 IDM_SELECTALL equ 40010 IDM_OPTION equ 40011 IDM_UNDO equ 40012 IDM_REDO equ 40013 IDD_OPTIONDLG equ 101 IDC_BACKCOLORBOX equ 1000 IDC_TEXTCOLORBOX equ 1001
RichEditID equ 300
.data ClassName db "IczEditClass",0 AppName db "IczEdit версия 1.0",0 RichEditDLL db "riched20.dll",0 RichEditClass db "RichEdit20A",0 NoRichEdit db " ASMFilterString db "ASM Исходники (*.asm)",0,"*.asm",0 db "Все файлы (*.*)",0,"*.*",0,0 OpenFileFail db "Не могу окрыть файл",0 WannaSave db "Данные были изменены. Вы хотите сохранить изменения?",0 FileOpened dd FALSE BackgroundColor dd 0FFFFFFh ; по умолчанию - белый TextColor dd 0 ; по умолчанию - черный
.data? hInstance dd ? hRichEdit dd ? hwndRichEdit dd ? FileName db 256 dup(?) AlternateFileName db 256 dup(?) CustomColors dd 16 dup(?)
.code start: invoke GetModuleHandle, NULL mov hInstance,eax invoke LoadLibrary,addr RichEditDLL .if eax!=0 mov hRichEdit,eax invoke WinMain, hInstance,0,0, SW_SHOWDEFAULT invoke FreeLibrary,hRichEdit .else invoke MessageBox,0,addr NoRichEdit,addr AppName, \ MB_OK or MB_ICONERROR .endif invoke ExitProcess,eax
WinMain proc hInst:DWORD,hPrevInst:DWORD,CmdLine:DWORD,CmdShow:DWORD LOCAL wc:WNDCLASSEX LOCAL msg:MSG LOCAL hwnd:DWORD mov wc.cbSize,SIZEOF WNDCLASSEX mov wc.style, CS_HREDRAW or CS_VREDRAW mov wc.lpfnWndProc, OFFSET WndProc mov wc.cbClsExtra,NULL mov wc.cbWndExtra,NULL push hInst pop wc.hInstance mov wc.hbrBackground,COLOR_WINDOW+1 mov wc.lpszMenuName,IDR_MAINMENU mov wc.lpszClassName,OFFSET ClassName invoke LoadIcon,NULL,IDI_APPLICATION mov wc.hIcon,eax mov wc.hIconSm,eax invoke LoadCursor,NULL,IDC_ARROW mov wc.hCursor,eax invoke RegisterClassEx, addr wc INVOKE CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\ WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\ CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\ hInst,NULL mov hwnd,eax invoke ShowWindow, hwnd,SW_SHOWNORMAL invoke UpdateWindow, hwnd .while TRUE invoke GetMessage, ADDR msg,0,0,0 .break .if (!eax) invoke TranslateMessage, ADDR msg invoke DispatchMessage, ADDR msg .endw mov eax,msg.wParam ret WinMain endp


StreamInProc proc hFile:DWORD,pBuffer:DWORD, \ NumBytes:DWORD, pBytesRead:DWORD invoke ReadFile,hFile,pBuffer,NumBytes,pBytesRead,0 xor eax,1 ret StreamInProc endp
StreamOutProc proc hFile:DWORD,pBuffer:DWORD, \ NumBytes:DWORD, pBytesWritten:DWORD invoke WriteFile,hFile,pBuffer,NumBytes,pBytesWritten,0 xor eax,1 ret StreamOutProc endp
CheckModifyState proc hWnd:DWORD invoke SendMessage,hwndRichEdit,EM_GETMODIFY,0,0 .if eax!=0 invoke MessageBox,hWnd,addr WannaSave, \ addr AppName,MB_YESNOCANCEL .if eax==IDYES invoke SendMessage,hWnd,WM_COMMAND,IDM_SAVE,0 .elseif eax==IDCANCEL mov eax,FALSE ret .endif .endif mov eax,TRUE ret CheckModifyState endp
SetColor proc LOCAL cfm:CHARFORMAT invoke SendMessage,hwndRichEdit,EM_SETBKGNDCOLOR, \ 0,BackgroundColor invoke RtlZeroMemory,addr cfm,sizeof cfm mov cfm.cbSize,sizeof cfm mov cfm.dwMask,CFM_COLOR push TextColor pop cfm.crTextColor invoke SendMessage,hwndRichEdit,EM_SETCHARFORMAT, \ SCF_ALL,addr cfm ret SetColor endp
OptionProc proc hWnd:DWORD, uMsg:DWORD, \ wParam:DWORD, lParam:DWORD LOCAL clr:CHOOSECOLOR .if uMsg==WM_INITDIALOG .elseif uMsg==WM_COMMAND mov eax,wParam shr eax,16 .if ax==BN_CLICKED mov eax,wParam .if ax==IDCANCEL invoke SendMessage,hWnd,WM_CLOSE,0,0 .elseif ax==IDC_BACKCOLORBOX invoke RtlZeroMemory,addr clr,sizeof clr mov clr.lStructSize, sizeof clr push hWnd pop clr.hwndOwner push hInstance pop clr.hInstance push BackgroundColor pop clr.rgbResult mov clr.lpCustColors,offset CustomColors mov clr.Flags,CC_ANYCOLOR or CC_RGBINIT invoke ChooseColor,addr clr .if eax!=0 push clr.rgbResult pop BackgroundColor invoke GetDlgItem,hWnd,IDC_BACKCOLORBOX invoke InvalidateRect,eax,0,TRUE .endif .elseif ax==IDC_TEXTCOLORBOX invoke RtlZeroMemory,addr clr,sizeof clr mov clr.lStructSize,sizeof clr push hWnd pop clr.hwndOwner push hInstance pop clr.hInstance push TextColor pop clr.rgbResult mov clr.lpCustColors,offset CustomColors mov clr.Flags,CC_ANYCOLOR or CC_RGBINIT invoke ChooseColor,addr clr .if eax!=0 push clr.rgbResult pop TextColor invoke GetDlgItem,hWnd,IDC_TEXTCOLORBOX invoke InvalidateRect,eax,0,TRUE .endif .elseif ax==IDOK ;==================================================== ; Сохраните состояние richedit контрола потому, ; что изменение цвета текста изменяет это состояние. ;==================================================== invoke SendMessage,hwndRichEdit,EM_GETMODIFY,0,0 push eax invoke SetColor pop eax invoke SendMessage,hwndRichEdit,EM_SETMODIFY,eax,0 invoke EndDialog,hWnd,0 .endif .endif .elseif uMsg==WM_CTLCOLORSTATIC invoke GetDlgItem,hWnd,IDC_BACKCOLORBOX .if eax==lParam invoke CreateSolidBrush,BackgroundColor ret .else invoke GetDlgItem,hWnd,IDC_TEXTCOLORBOX .if eax==lParam invoke CreateSolidBrush,TextColor ret .endif .endif mov eax,FALSE ret .elseif uMsg==WM_CLOSE invoke EndDialog,hWnd,0 .else mov eax,FALSE ret .endif mov eax,TRUE ret OptionProc endp


WndProc proc hWnd:DWORD, uMsg:DWORD, \ wParam:DWORD, lParam:DWORD LOCAL chrg:CHARRANGE LOCAL ofn:OPENFILENAME LOCAL buffer[256]:BYTE LOCAL editstream:EDITSTREAM LOCAL hFile:DWORD .if uMsg==WM_CREATE invoke CreateWindowEx,WS_EX_CLIENTEDGE,addr RichEditClass,0,\ WS_CHILD or WS_VISIBLE or ES_MULTILINE or\ WS_VSCROLL or WS_HSCROLL or ES_NOHIDESEL,\ CW_USEDEFAULT,CW_USEDEFAULT, CW_USEDEFAULT,\ CW_USEDEFAULT,hWnd,RichEditID,hInstance,0 mov hwndRichEdit,eax ;================================================== ; Установка предела текста. По умолчанию - 64K ;================================================== invoke SendMessage,hwndRichEdit,EM_LIMITTEXT,-1,0 ;================================================== ; установка цвета текста/фона ;================================================== invoke SetColor invoke SendMessage,hwndRichEdit,EM_SETMODIFY,FALSE,0 invoke SendMessage,hwndRichEdit,EM_EMPTYUNDOBUFFER,0,0 .elseif uMsg==WM_INITMENUPOPUP mov eax,lParam .if ax==0 ; меню Файл .if FileOpened==TRUE ; a file is already opened invoke EnableMenuItem,wParam,IDM_OPEN,MF_GRAYED invoke EnableMenuItem,wParam,IDM_CLOSE,MF_ENABLED invoke EnableMenuItem,wParam,IDM_SAVE,MF_ENABLED invoke EnableMenuItem,wParam,IDM_SAVEAS,MF_ENABLED .else invoke EnableMenuItem,wParam,IDM_OPEN,MF_ENABLED invoke EnableMenuItem,wParam,IDM_CLOSE,MF_GRAYED invoke EnableMenuItem,wParam,IDM_SAVE,MF_GRAYED invoke EnableMenuItem,wParam,IDM_SAVEAS,MF_GRAYED .endif .elseif ax==1 ; edit menu ;===================================================== ; проверьте есть ли текст в буфере обмена, если есть ; мы активизируем в меню пункт "Вставить". ;===================================================== invoke SendMessage,hwndRichEdit,EM_CANPASTE,CF_TEXT,0 .if eax==0 ; no text in the clipboard invoke EnableMenuItem,wParam,IDM_PASTE,MF_GRAYED .else invoke EnableMenuItem,wParam,IDM_PASTE,MF_ENABLED .endif ;===================================================== ; Проверьте, является ли очередь отмены пустой ;===================================================== invoke SendMessage,hwndRichEdit,EM_CANUNDO,0,0 .if eax==0 invoke EnableMenuItem,wParam,IDM_UNDO,MF_GRAYED .else invoke EnableMenuItem,wParam,IDM_UNDO,MF_ENABLED .endif ;===================================================== ; Проверьте, является ли очередь повтора пустой ;===================================================== invoke SendMessage,hwndRichEdit,EM_CANREDO,0,0 .if eax==0 invoke EnableMenuItem,wParam,IDM_REDO,MF_GRAYED .else invoke EnableMenuItem,wParam,IDM_REDO,MF_ENABLED .endif ;===================================================== ; Проверьте, выделено ли что-нибудь в richedit контроле. ; Если да, мы активизируем пункты меню вырезать/ ; копировать/удалить ;====================================================== invoke SendMessage,hwndRichEdit,EM_EXGETSEL,0,addr chrg mov eax,chrg.cpMin .if eax==chrg.cpMax ; no current selection invoke EnableMenuItem,wParam,IDM_COPY,MF_GRAYED invoke EnableMenuItem,wParam,IDM_CUT,MF_GRAYED invoke EnableMenuItem,wParam,IDM_DELETE,MF_GRAYED .else invoke EnableMenuItem,wParam,IDM_COPY,MF_ENABLED invoke EnableMenuItem,wParam,IDM_CUT,MF_ENABLED invoke EnableMenuItem,wParam,IDM_DELETE,MF_ENABLED .endif .endif .elseif uMsg==WM_COMMAND .if lParam==0 ; menu commands mov eax,wParam .if ax==IDM_OPEN invoke RtlZeroMemory,addr ofn,sizeof ofn mov ofn.lStructSize,sizeof ofn push hWnd pop ofn.hwndOwner push hInstance pop ofn.hInstance mov ofn.lpstrFilter,offset ASMFilterString mov ofn.lpstrFile,offset FileName mov byte ptr [FileName],0 mov ofn.nMaxFile,sizeof FileName mov ofn.Flags,OFN_FILEMUSTEXIST or OFN_HIDEREADONLY or OFN_PATHMUSTEXIST invoke GetOpenFileName,addr ofn .if eax!=0 invoke CreateFile,addr FileName,GENERIC_READ,\ FILE_SHARE_READ,NULL,OPEN_EXISTING,\ FILE_ATTRIBUTE_NORMAL,0 .if eax!=INVALID_HANDLE_VALUE mov hFile,eax ;============================================== ; stream the text into the richedit control ;============================================== mov editstream.dwCookie,eax mov editstream.pfnCallback,offset StreamInProc invoke SendMessage,hwndRichEdit,EM_STREAMIN,\ SF_TEXT,addr editstream ;============================================== ; Initialize the modify state to false ;============================================== invoke SendMessage,hwndRichEdit,EM_SETMODIFY,FALSE,0 invoke CloseHandle,hFile mov FileOpened,TRUE .else invoke MessageBox,hWnd,addr OpenFileFail,\ addr AppName,MB_OK or MB_ICONERROR .endif .endif .elseif ax==IDM_CLOSE invoke CheckModifyState,hWnd .if eax==TRUE invoke SetWindowText,hwndRichEdit,0 mov FileOpened,FALSE .endif .elseif ax==IDM_SAVE invoke CreateFile,addr FileName,GENERIC_WRITE, \ FILE_SHARE_READ,NULL,\ CREATE_ALWAYS,FILE_ATTRIBUTE_NORMAL,0 .if eax!=INVALID_HANDLE_VALUE @@: mov hFile,eax ;========================================= ; stream the text to the file ;========================================= mov editstream.dwCookie,eax mov editstream.pfnCallback,offset StreamOutProc invoke SendMessage,hwndRichEdit,EM_STREAMOUT, \ SF_TEXT, addr editstream ;========================================= ; Initialize the modify state to false ;========================================= invoke SendMessage,hwndRichEdit,EM_SETMODIFY,FALSE,0 invoke CloseHandle,hFile .else invoke MessageBox,hWnd,addr OpenFileFail,addr AppName,\ MB_OK or MB_ICONERROR .endif .elseif ax==IDM_COPY invoke SendMessage,hwndRichEdit,WM_COPY,0,0 .elseif ax==IDM_CUT invoke SendMessage,hwndRichEdit,WM_CUT,0,0 .elseif ax==IDM_PASTE invoke SendMessage,hwndRichEdit,WM_PASTE,0,0 .elseif ax==IDM_DELETE invoke SendMessage,hwndRichEdit,EM_REPLACESEL,TRUE,0 .elseif ax==IDM_SELECTALL mov chrg.cpMin,0 mov chrg.cpMax,-1 invoke SendMessage,hwndRichEdit,EM_EXSETSEL,0,addr chrg .elseif ax==IDM_UNDO invoke SendMessage,hwndRichEdit,EM_UNDO,0,0 .elseif ax==IDM_REDO invoke SendMessage,hwndRichEdit,EM_REDO,0,0 .elseif ax==IDM_OPTION invoke DialogBoxParam,hInstance,IDD_OPTIONDLG,hWnd,addr OptionProc,0 .elseif ax==IDM_SAVEAS invoke RtlZeroMemory,addr ofn,sizeof ofn mov ofn.lStructSize,sizeof ofn push hWnd pop ofn.hwndOwner push hInstance pop ofn.hInstance mov ofn.lpstrFilter,offset ASMFilterString mov ofn.lpstrFile,offset AlternateFileName mov byte ptr [AlternateFileName],0 mov ofn.nMaxFile,sizeof AlternateFileName mov ofn.Flags,OFN_FILEMUSTEXIST or OFN_HIDEREADONLY or OFN_PATHMUSTEXIST invoke GetSaveFileName,addr ofn .if eax!=0 invoke CreateFile,addr AlternateFileName,GENERIC_WRITE,\ FILE_SHARE_READ,NULL,CREATE_ALWAYS,\ FILE_ATTRIBUTE_NORMAL,0 .if eax!=INVALID_HANDLE_VALUE jmp @B .endif .endif .elseif ax==IDM_EXIT invoke SendMessage,hWnd,WM_CLOSE,0,0 .endif .endif .elseif uMsg==WM_CLOSE invoke CheckModifyState,hWnd .if eax==TRUE invoke DestroyWindow,hWnd .endif .elseif uMsg==WM_SIZE mov eax,lParam mov edx,eax and eax,0FFFFh shr edx,16 invoke MoveWindow,hwndRichEdit,0,0,eax,edx,TRUE .elseif uMsg==WM_DESTROY invoke PostQuitMessage,NULL .else invoke DefWindowProc,hWnd,uMsg,wParam,lParam ret .endif xor eax,eax ret WndProc endp end start


;============================================= ; Файл ресурсов ;============================================= #include "resource.h" #define IDR_MAINMENU 101 #define IDD_OPTIONDLG 101 #define IDC_BACKCOLORBOX 1000 #define IDC_TEXTCOLORBOX 1001 #define IDM_OPEN 40001 #define IDM_SAVE 40002 #define IDM_CLOSE 40003 #define IDM_SAVEAS 40004 #define IDM_EXIT 40005 #define IDM_COPY 40006 #define IDM_CUT 40007 #define IDM_PASTE 40008 #define IDM_DELETE 40009 #define IDM_SELECTALL 40010 #define IDM_OPTION 40011 #define IDM_UNDO 40012 #define IDM_REDO 40013
IDR_MAINMENU MENU DISCARDABLE BEGIN POPUP "&Файл" BEGIN MENUITEM "&Открыть...", IDM_OPEN MENUITEM "&Закрыть", IDM_CLOSE MENUITEM "&Сохранить", IDM_SAVE MENUITEM "Сохранить &как...", IDM_SAVEAS MENUITEM SEPARATOR MENUITEM "В&ыход", IDM_EXIT END POPUP "&Правка" BEGIN MENUITEM "&Отменить", IDM_UNDO MENUITEM "&Повторить", IDM_REDO MENUITEM "&Копировать", IDM_COPY MENUITEM "&Вырезать", IDM_CUT MENUITEM "Вст&авить", IDM_PASTE MENUITEM SEPARATOR MENUITEM "&Удалить", IDM_DELETE MENUITEM SEPARATOR MENUITEM "В&ыделить все", IDM_SELECTALL END MENUITEM "П&араметры", IDM_OPTION END
IDD_OPTIONDLG DIALOG DISCARDABLE 0, 0, 183, 54 STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU | DS_CENTER CAPTION "Options" FONT 8, "MS Sans Serif" BEGIN DEFPUSHBUTTON "OK",IDOK,137,7,39,14 PUSHBUTTON "Отмена",IDCANCEL,137,25,39,14 GROUPBOX "",IDC_STATIC,5,0,124,49 LTEXT "Цвет фона:",IDC_STATIC,20,14,60,8 LTEXT "",IDC_BACKCOLORBOX,85,11,28,14,SS_NOTIFY | WS_BORDER LTEXT "Цвет текста:",IDC_STATIC,20,33,35,8 LTEXT "",IDC_TEXTCOLORBOX,85,29,28,14,SS_NOTIFY | WS_BORDER END
АНАЛИЗ
Программа сначала загружает richedit dll, в нашем случае riched20.dll. Если dll не может быть загружена, то выходим из программы.


invoke LoadLibrary, addr RichEditDLL .if eax!= 0 Mov hRichEdit, eax invoke WinMain, hInstance, 0,0, SW_SHOWDEFAULT invoke FreeLibrary, hRichEdit .else invoke MessageBox, 0, addr NoRichEdit, addr AppName, \ MB_OK or MB_ICONERROR .endif invoke ExitProcess, eax
После того, как dll успешно загружена, мы переходим к созданию нормального окна, которое будет родительским richedit контрола. Внутри обработчика WM_CREATE, мы создаем richedit контрол:
invoke CreateWindowEx, WS_EX_CLIENTEDGE, addr RichEditClass, 0,\ WS_CHILD or WS_VISIBLE or ES_MULTILINE or WS_VSCROLL or WS_HSCROLL or ES_NOHIDESEL,\ CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, hWnd, RichEditID,\ hInstance, 0 Mov hwndRichEdit, eax
Обратите внимание, что мы определяем стиль ES_MULTILINE, иначе контрол будет одиночно-выровненный.
invoke SendMessage, hwndRichEdit, EM_LIMITTEXT,-1,0
После того, как richedit контрол создан, мы должны установить в нем новый текстовый предел. По умолчанию, richedit контрол имеет предел текста 64КБ, такой же как в простых многострочных Edit контролах. Мы должны расширить этот предел, чтобы оперировать с большими файлами. В вышеупомянутой строке, я определяю -1, которая составляет 0FFFFFFFFh, очень большое значение.
invoke SetColor
Затем, мы устанавливаем цвет текста и фона. Так как эта операция может быть вызвана и из другой части программы, я поместил код в функцию SetColor.
SetColor proc LOCAL cfm:CHARFORMAT invoke SendMessage, hwndRichEdit, EM_SETBKGNDCOLOR, \ 0, BackgroundColor
Установка цвета фона richedit контрола это прямая операция: просто пошлите сообщение EM_SETBKGNDCOLOR richedit контролу. (Если Вы используете многострочные Edit контролы, Вы должны обрабатывать WM_CTLCOLOREDIT). Заданный по умолчанию цвет фона белый.
invoke RtlZeroMemory, addr cfm, sizeof cfm Mov cfm.cbSize, sizeof cfm Mov cfm.dwMask, CFM_COLOR push TextColor pop cfm.crTextColor
После того, как цвет фона установлен, мы заполняем члены структуры CHARFORMAT, чтобы установить цвет текста. Обратите внимание, что мы заполняем cbSize размером структуры, так что richedit контрол знает, что мы посылаем ему CHARFORMAT, а не CHARFORMAT2. DwMask имеет только один флаг, CFM_COLOR, потому что мы только хотим установить цвет текста, и crTextColor заполнен значением желаемого цвета текста.


invoke SendMessage, hwndRichEdit, EM_SETCHARFORMAT, \ SCF_ALL, addr cfm Ret SetColor endp
После установки цвета, Вы должны освободить буфер отмены, потому что действие изменения текста/цвета фона возможно отменить. Мы посылаем сообщение EM_EMPTYUNDOBUFFER, чтобы сделать этого.
invoke SendMessage, hwndRichEdit, EM_EMPTYUNDOBUFFER, 0,0
После заполнения структуры CHARFORMAT, мы посылаем EM_SETCHARFORMAT richedit контролу, определяя SCF_ALL флаг в wParam, чтобы указать, что мы хотим, чтобы форматирование текста применилось ко всему тексту.
Обратите внимание, что, когда мы создавали richedit контрол, мы не определили его размер и позицию. Дело в том, что мы хотим, чтобы он закрыл всю клиентскую область родительского окна. Мы изменяем его размеры всякий раз, когда изменяется размер родительского окна.
.elseif uMsg== WM_SIZE Mov eax, lParam Mov edx, eax and eax, 0FFFFh Shr edx, 16 invoke MoveWindow, hwndRichEdit, 0,0, eax, edx, TRUE
В вышеупомянутом фрагменте кода, мы используем новые размеры клиентской области в lParam, чтобы изменить размеры richedit контрола с помощью MoveWindow.
Когда пользователь кликает на строке меню Файл/Правка, мы обрабатываем сообщение WM_INITPOPUPMENU так, чтобы мы могли установить состояние некоторых пунктов в подменю перед отображением эго пользователю. Например, если файл уже открыт в richedit контроле, мы хотим отключить пункт открыть в подменю и включить пункт сохранить, сохранить как... и т.д.
В случае со строкой меню Файл, мы используем переменную FileOpened как флаг, чтобы определить, открыт ли уже файл. Если значение в этой переменной TRUE, то мы знаем, что файл уже открыт.
.elseif uMsg==WM_INITMENUPOPUP mov eax,lParam .if ax==0 ; меню Файл .if FileOpened==TRUE ; файл уже открыт invoke EnableMenuItem,wParam,IDM_OPEN,MF_GRAYED invoke EnableMenuItem,wParam,IDM_CLOSE,MF_ENABLED invoke EnableMenuItem,wParam,IDM_SAVE,MF_ENABLED invoke EnableMenuItem,wParam,IDM_SAVEAS,MF_ENABLED .else invoke EnableMenuItem,wParam,IDM_OPEN,MF_ENABLED invoke EnableMenuItem,wParam,IDM_CLOSE,MF_GRAYED invoke EnableMenuItem,wParam,IDM_SAVE,MF_GRAYED invoke EnableMenuItem,wParam,IDM_SAVEAS,MF_GRAYED .endif


Как вы можете заметить, если файл уже открыт, мы запрещаем пункты меню открытие и разрешаем пункты меню сохранение, и наоборот если FileOpened - FALSE.
В случае со строкой меню правка, мы сначала должны проверить состояние richedit контрола и буфера обмена.
invoke SendMessage,hwndRichEdit,EM_CANPASTE,CF_TEXT,0 .if eax==0 ; нет текста в буфере обмена invoke EnableMenuItem,wParam,IDM_PASTE,MF_GRAYED .else invoke EnableMenuItem,wParam,IDM_PASTE,MF_ENABLED .endif
Мы сначала проверяем, является ли текст в буфере обмена доступным, посылая сообщение EM_CANPASTE. Если текст доступен, SendMessage возвращает TRUE, и мы разрешаем пункт меню вставка, а если FALSE, то запрещаем.
invoke SendMessage,hwndRichEdit,EM_CANUNDO,0,0 .if eax==0 invoke EnableMenuItem,wParam,IDM_UNDO,MF_GRAYED .else invoke EnableMenuItem,wParam,IDM_UNDO,MF_ENABLED .endif
Затем, мы проверяем, является ли буфер отмены пустым, посылая сообщение EM_CANUNDO. Если - не пустой, SendMessage возвращает TRUE, и мы разрешаем пункт меню отмена.
invoke SendMessage,hwndRichEdit,EM_CANREDO,0,0 .if eax==0 invoke EnableMenuItem,wParam,IDM_REDO,MF_GRAYED .else invoke EnableMenuItem,wParam,IDM_REDO,MF_ENABLED .endif
Мы проверяем буфер Повтора, посылая richedit контролу сообщение EM_CANREDO. Если - не пустой, SendMessage возвращает TRUE, и мы разрешаем пункт меню повторить.
invoke SendMessage,hwndRichEdit,EM_EXGETSEL,0,addr chrg mov eax,chrg.cpMin .if eax==chrg.cpMax ; ничего не выделенно invoke EnableMenuItem,wParam,IDM_COPY,MF_GRAYED invoke EnableMenuItem,wParam,IDM_CUT,MF_GRAYED invoke EnableMenuItem,wParam,IDM_DELETE,MF_GRAYED .else invoke EnableMenuItem,wParam,IDM_COPY,MF_ENABLED invoke EnableMenuItem,wParam,IDM_CUT,MF_ENABLED invoke EnableMenuItem,wParam,IDM_DELETE,MF_ENABLED .endif
Наконец, мы проверяем, выделенно ли что-нибудь в richedit контроле, посылая сообщение EM_EXGETSEL. Это сообщение использует структуру CHARRANGE, которая определена следующим образом:
CHARRANGE STRUCT cpMin DWORD ? CpMax DWORD ? CHARRANGE ENDS


CpMin содержит индекс позиции символа предшествующего первому символу в диапазоне.

CpMax содержит индекс позиции символа идущего после последнего символа в диапазоне.
После возвращения EM_EXGETSEL, структура CHARRANGE заполнена индексами позиции стартовой и конечной метками выделенного диапазона. Если ничего не выделено, cpMin и cpMax идентичны и мы, запрещаем пункты меню вырезать/копировать/удалить.
Когда пользователь нажимает пункт Открыть, мы отображаем диалоговое окно открытия файла и если пользователь выбирает файл, мы открываем файл и потоком загружаем его содержание richedit контрол.
invoke CreateFile,addr FileName,GENERIC_READ, FILE_SHARE_READ,NULL,OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,0 .if eax!=INVALID_HANDLE_VALUE mov hFile,eax mov editstream.dwCookie,eax mov editstream.pfnCallback,offset StreamInProc invoke SendMessage,hwndRichEdit,EM_STREAMIN, SF_TEXT,addr editstream
После того, как файл успешно открыт с CreateFile, мы заполняем структуру EDITSTREAM, подготавливаем к сообщению EM_STREAMIN. Мы выбираем послать хэндл открытого файла через член dwCookie и передать адрес потоковой функции в pfnCallback.
Потоковая процедура сама по себе простая.
StreamInProc proc hFile:DWORD, pBuffer:DWORD, NumBytes:DWORD, pBytesRead:DWORD invoke ReadFile, hFile, pBuffer, NumBytes, pBytesRead, 0 Xor eax, 1 Ret StreamInProc endp
Вы можете заметить, что все параметры потоковой процедуры совершенно соответствуют ReadFile. И возвращаемое значение ReadFile - xor с 1 для, того чтобы, если она возвратит 1 (в случае успеха), фактическое значение, возвращенное в eax - 0 и наоборот.
invoke SendMessage,hwndRichEdit,EM_SETMODIFY,FALSE,0 invoke CloseHandle,hFile mov FileOpened,TRUE
После возврата EM_STREAMIN, это означает, что потоковая операция завершена. В действительности, мы должны проверить значение dwError члена структуры EDITSTREAM.
Richedit (и Edit) контролы поддерживают флаг, чтобы указать, изменялось ли их содержание. Мы можем получить значение этого флага, посылая им сообщение EM_GETMODIFY. SendMessage возвращает TRUE, если содержание изменялось. Так как загрузка текста в контрол, это - своего рода модификация. Мы должны установить флаг модификации в FALSE, посылая контролу сообщение EM_SETMODIFY с wParam == FALSE после того, как операция stream-in завершится. Мы немедленно закрываем файл и устанавливаем FileOpened в TRUE чтобы указывать, что файл был открыт.


Когда пользователь кликнет на пункт меню сохранить/сохранить как..., мы используем сообщение
EM_STREAMOUT, чтобы вывести содержимое richedit контрола в файл. Как и функция stream-in, функции stream-out - простота сама по себе. Это совершенно соответствует WriteFile.
Текстовые операции такие как вырезать/копировать/вставить/восстановить/отменить, легко осуществимы, посылая richedit контролу сообщение WM_CUT/WM_COPY/WM_PASTE/WM_REDO/WM_UNDO соответственно.
Операции удаление/выделить всe сделаны следующим образом:
.elseif ax==IDM_DELETE invoke SendMessage,hwndRichEdit,EM_REPLACESEL,TRUE,0 .elseif ax==IDM_SELECTALL mov chrg.cpMin,0 mov chrg.cpMax,-1 invoke SendMessage,hwndRichEdit,EM_EXSETSEL,0,addr chrg
Операция удаление работает с выделением. Я посылаю сообщение EM_REPLACESEL с NULL строкой, чтобы richedit контрол заменил выделенный текст пустой строкой.
Операция выделить всё сделана, посылая сообщение EM_EXSETSEL, установив cpMin == 0 и cpMax ==-1, что равносильно выделению всего текста.
Когда пользователь выбирает строку меню Параметры, мы отображаем диалоговое окно, представляющее текущие цвета фона/текста.

Когда пользователь кликает на одной из палитры цветов, вызывается диалоговое окно выбора цвета. "Палитра цветов" - фактически статический элемент управления с флагом WS_BORDER и SS_NOTIFY. Статический элемент управления с флагом SS_NOTIFY уведомит его родительское окно с действиями мыши на нем, типа BN_CLICKED (STN_CLICKED). Это - уловка.
.elseif ax==IDC_BACKCOLORBOX invoke RtlZeroMemory,addr clr,sizeof clr mov clr.lStructSize,sizeof clr push hWnd pop clr.hwndOwner push hInstance pop clr.hInstance push BackgroundColor pop clr.rgbResult mov clr.lpCustColors,offset CustomColors mov clr.Flags,CC_ANYCOLOR or CC_RGBINIT invoke ChooseColor,addr clr .if eax!=0 push clr.rgbResult pop BackgroundColor invoke GetDlgItem,hWnd,IDC_BACKCOLORBOX invoke InvalidateRect,eax,0,TRUE .endif
Когда пользователь кликает на одной из палитры цветов, мы заполняем члены структуры CHOOSECOLOR и вызываем диалоговое окно выбора цвета ChooseColor. Если пользователь выбирает цвет, то это значение colorref возвращается в члене rgbResult, и мы сохраняем это значение в переменной BackgroundColor. После этого, мы вынуждаем перекрашивание на палитре цветов, вызывая InvalidateRect на хэндл палитры цветов. Палитра цветов посылает WM_CTLCOLORSTATIC сообщение своему родительскому окну.
invoke GetDlgItem,hWnd,IDC_BACKCOLORBOX .if eax==lParam invoke CreateSolidBrush,BackgroundColor ret
Внутри обработчика WM_CTLCOLORSTATIC, мы сравниваем, хэндл статического элемента управления переданного в lParam> с обоими палитрами цветов. Если значение соответствует, мы создаем новую кисть, используя цвет из переменной и немедленно возвращаемся. Статический элемент управления будет использовать недавно созданную кисть, чтобы окрасить ее фон.
[C] Iczelion, пер. N/A.

Операции над текстом в RichEdit


&nbsp В этом туториале вы узнаете больше о операциях над текстом, доступных в RichEdit, например о том, как искать/заменять текст и переходить к определенной строке.

&nbsp Скачайте пример.

Теория:

&nbsp Поиск текста

&nbsp В RichEdit есть несколько текстовых операций. Поиск определенного текста - одна из них. Поиск текста осуществляется с помощью сообщений EM_FINDTEXT или EM_FINDTEXTEX. Эти сообщения мало отличаются друг от друга.

EM_FINDTEXT

&nbsp wParam == опции поиска.

&nbsp Может быть комбинацией значений, приведенных ниже. Эти опции идентичны как для EM_FINDTEXT, так и для EM_FINDTEXTEX.

FR_DOWN - если этот флаг указан, поиск начинается с конца текущего выделенного текста и до самого конца (вперед). Этот флаг действует только в RichEdit 2.0 или выше: этот способ применяется по умолчанию в RichEdit 1.0. В RichEdit 2.0 по умолчанию поиск осуществляется от конца выделенного текста до начала (назад). Кратко говоря, если вы используете RichEdit 1.0, вы ничего не можете сделать, чтобы изменить направление поиска: он всегда ищет вперед. Если вы используете RichEdit 2.0 и хотите искать вперед, вам нужно указать этот флаг, иначе поиск будет производиться назад. FR_MATCHCASE - если этот флаг указан, будет учитываться регистр. FR_WHOLEWORD - если этот флаг указан, поиск будет искать место в тексте, которое будет удовлетворять указанной поисковой строке.

&nbsp Вообще-то, есть еще несколько флагов, но они относятся к языкам, отличным от английского.

&nbsp lParam == указатель на структуру FINDTEXT.

FINDTEXT STRUCT chrg CHARRANGE <> lpstrText DWORD ? FINDTEXT ENDS

&nbsp chrg - это структура CHARRANGE, которая определена следующим образом:

CHARRANGE STRUCT cpMin DWORD ? cpMax DWORD ? CHARRANGE ENDS

&nbsp cpMin содержит индекс первого символа в массиве символов (диапазон).

&nbsp cpMax содержит индекс символа, который следует непосредственно за последним символов в массиве символов.

&nbsp Фактически, чтобы найти строку текста, вам нужно указать диапазон символом, в котором нужно искать. Значение cpMin и cpMax будут зависеть от того, проводится ли поиск назад или вперед. Если поиск идет вперед, cpMin задает начальный индекс, а cpMax - конечный. Если поиск идет назад, тогда cpMin содержит конечное значение индекса, в то время как cpMax задает начальный индекс.


&nbsp lpstrText - это указатель на текстовую строку, которую нужно искать.
&nbsp EM_FINDTEXT возвращает индекс первого символа в заданной текстовой строке в RichEdit. Оно возвращает -1, если указанный текст не был найден.
&nbsp EM_FINDTEXTEX
&nbsp wParam == опции поиска. То же самое, что и EM_FINDTEXT. lParam == указатель на структуру FINDTEXTEX.
FINDTEXTEX STRUCT chrg CHARRANGE <> lpstrText DWORD ? chrgText CHARRANGE <> FINDTEXTEX ENDS
&nbsp Первые два члена FINDTEXTEX идентичны соответствующим полям в структуре FINDTEXT. chrgText - это структура CHARRANGE, которая будет заполнена начальным и конечным индексами, если будут найдены какие-либо совпадения.
&nbsp Возвращаемое значение EM_FINDTEXTEX - то же самое, что и у EM_FINDTEXT.
&nbsp Разница между EM_FINDTEXT и EM_FINDTEXTEXT - это то, что у структуры FINDTEXTEX есть дополнительное поле, chrgText, которое будет заполнено начальным и конечным индексами, если будут найдены совпадения. Это удобно, если мы хотим иметь возможность осуществлять больше текстовых операций над строкой.
&nbsp Замещение/вставка текста
&nbsp Контрол RichEdit предоставляет EM_SETTEXTEX для замещения/вставки текста. Это сообщение комбинирует функциональность WM_SETTEXT и WM_REPLACESEL. У него следующий синтаксис:
&nbsp EM_SETTEXTEXT wParam == указатель на структуру SETTEXTEX.
SETTEXTEX STRUCT flags DWORD ? codepage DWORD ? SETTEXTEX ENDS
&nbsp Поле 'flags' может быть комбинацией следующих значений:
ST_DEFAULT - удаляет стек совершенных операций, сбрасывает форматирование RichEdit. ST_KEEPUNDO - сохраняет стек совершенных операций. ST_SELECTION - замещает выделенный текст и сохраняет форматирование.
&nbsp Поле 'codepage' - это константа, которая указывает кодовую страницу. Обычно мы указываем CP_ACP.
&nbsp Выделение текста
&nbsp Мы можем выделить текст программно с помощью сообщений EM_SETSEL или EM_EXSETSEL. Обе прекрасно работают. Выбирать, какое из сообщений необходимо использовать, зависит от доступного формата индексов символов. Если они уже сохранены в структуры CHARRANGE, проще использовать EM_EXSETSEL.


&nbsp EM_EXSETSEL
&nbsp wParam == не используется. Должен быть равен 0. lParam == указатель на структуру CHARRANGE, который содержит диапазон символов, который необходимо выделить.
&nbsp Уведомительные события
&nbsp В случае с многолинейным edit control'ом вам необходимо сабклассировать его, чтобы получить входные сообщения, такие как события мыши/клавиатуры. RichEdit предоставляет лучший способ: он будет уведомлять родительское окно о таких событиях. Чтобы указать RichEdit, какие события нужно посылать, родительское окно посылает сообщение EM_SETEVENTMASK контролу RichEdit, указывая, в каких событиях он заинтересован. У EM_SETEVENTMASK следующий синтаксис:
&nbsp EM_SETEVENTMASK
&nbsp wParam == не используется. Должен быть равен нулю. lParam == маска событий. Это должна быть комбинация флагов, указанных ниже.
ENM_CHANGE - прием уведомлений EN_CHANGE. ENM_CORRECTTEXT - прием уведомлений EN_CORRECTTEXT. ENM_DRAGDROPDONE - прием уведомлений EN_DRAGDROPDONE. ENM_DROPFILES - прием уведомлений EN_DROPFILES. ENM_KEYEVENTS - прием уведомлений EN_MSGFILTER, относящихся к событиям от клавиатуры. ENM_LINK - RichEdit 2.0 и выше: прием уведомлений EN_LINK, когда курсор мыши находится над текстом, который принимает CFE_LINK и/или другие события от мыши. ENM_MOUSEEVENTS - прием уведомлений EN_MSGFILTER, относящихся к событиям от мыши. ENM_OBJECTPOSITIONS - прием уведомлений EN_OBJECTPOSITIONS. ENM_PROTECTED - прием уведомлений EN_PROTECTED. ENM_REQUESTRESIZE - прием уведомлений EN_REQUESTRESIZE. ENM_SCROLL - прием уведомлений EN_HSCROLL и EN_VSCROLL. ENM_SCROLLEVENTS - прием уведомлений EN_MSGFILTER от колесика мыши. ENM_SELCHANGE - прием уведомлений EN_SELCHANGE. ENM_UPDATE - прием уведомлений EN_UPDATE. RichEdit 2.0 и выше: этот флаг игнорирует, а сообщения EN_UPDATE отсылаются всегда. Тем не менее, если Rich Edit 3.0 эмулирует Rich Edit 1.0, вы должны указать этот флаг для того, чтобы родительское окно принимало уведомления EN_UPDATE.
&nbsp Все вышеуказанные уведомления будут отсылаться через сообщение WM_NOTIFY: вы должны проверить поле 'code' структуры NMHDR, чтобы узнать, какое уведомление вы получили. Например, если вы хотите зарегистрировать сообщения от мыши (скажем, чтобы отображать контекстное меню по нажатию на правую кнопку мыши), вы должны сделать что-то вроде следующего:


invoke SendMessage,hwndRichEdit,EM_SETEVENTMASK, 0,ENM_MOUSEEVENTS ..... ..... WndProc proc hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD ..... .... .elseif uMsg==WM_NOTIFY push esi mov esi,lParam assume esi:ptr NMHDR .if [esi].code==EN_MSGFILTER .... [ do something here] .... .endif pop esi
ПРИМЕР
& nbsp Следующий пример является обновлением IczEdit, шедшего вместе с туториалом 33. Были добавлены возможности поиска/замещения и акселераторы. Также обрабатываются события от мыши и отображается контекстное меню.
.386 .model flat,stdcall option casemap:none include \masm32\include\windows.inc include \masm32\include\user32.inc include \masm32\include\comdlg32.inc include \masm32\include\gdi32.inc include \masm32\include\kernel32.inc includelib \masm32\lib\gdi32.lib includelib \masm32\lib\comdlg32.lib includelib \masm32\lib\user32.lib includelib \masm32\lib\kernel32.lib
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
.const IDR_MAINMENU equ 101 IDM_OPEN equ 40001 IDM_SAVE equ 40002 IDM_CLOSE equ 40003 IDM_SAVEAS equ 40004 IDM_EXIT equ 40005 IDM_COPY equ 40006 IDM_CUT equ 40007 IDM_PASTE equ 40008 IDM_DELETE equ 40009 IDM_SELECTALL equ 40010 IDM_OPTION equ 40011 IDM_UNDO equ 40012 IDM_REDO equ 40013 IDD_OPTIONDLG equ 101 IDC_BACKCOLORBOX equ 1000 IDC_TEXTCOLORBOX equ 1001 IDR_MAINACCEL equ 105 IDD_FINDDLG equ 102 IDD_GOTODLG equ 103 IDD_REPLACEDLG equ 104 IDC_FINDEDIT equ 1000 IDC_MATCHCASE equ 1001 IDC_REPLACEEDIT equ 1001 IDC_WHOLEWORD equ 1002 IDC_DOWN equ 1003 IDC_UP equ 1004 IDC_LINENO equ 1005 IDM_FIND equ 40014 IDM_FINDNEXT equ 40015 IDM_REPLACE equ 40016 IDM_GOTOLINE equ 40017 IDM_FINDPREV equ 40018 RichEditID equ 300
.data ClassName db "IczEditClass",0 AppName db "IczEdit version 2.0",0 RichEditDLL db "riched20.dll",0 RichEditClass db "RichEdit20A",0 NoRichEdit db "Cannot find riched20.dll",0 ASMFilterString db "ASM Source code (*.asm)",0,"*.asm",0 db "All Files (*.*)",0,"*.*",0,0 OpenFileFail db "Cannot open the file",0 WannaSave db "The data in the control is modified. Want to save it?",0 FileOpened dd FALSE BackgroundColor dd 0FFFFFFh ; default to white TextColor dd 0 ; default to black hSearch dd ? ; handle to the search/replace dialog box hAccel dd ?


.data? hInstance dd ? hRichEdit dd ? hwndRichEdit dd ? FileName db 256 dup(?) AlternateFileName db 256 dup(?) CustomColors dd 16 dup(?) FindBuffer db 256 dup(?) ReplaceBuffer db 256 dup(?) uFlags dd ? findtext FINDTEXTEX <>
. code start: mov byte ptr [FindBuffer],0 mov byte ptr [ReplaceBuffer],0 invoke GetModuleHandle, NULL mov hInstance,eax invoke LoadLibrary,addr RichEditDLL .if eax!=0 mov hRichEdit,eax invoke WinMain, hInstance,0,0, SW_SHOWDEFAULT invoke FreeLibrary,hRichEdit .else invoke MessageBox,0,addr NoRichEdit,addr AppName,MB_OK or MB_ICONERROR .endif invoke ExitProcess,eax
WinMain proc hInst:DWORD,hPrevInst:DWORD,CmdLine:DWORD,CmdShow:DWORD LOCAL wc:WNDCLASSEX LOCAL msg:MSG LOCAL hwnd:DWORD mov wc.cbSize,SIZEOF WNDCLASSEX mov wc.style, CS_HREDRAW or CS_VREDRAW mov wc.lpfnWndProc, OFFSET WndProc mov wc.cbClsExtra,NULL mov wc.cbWndExtra,NULL push hInst pop wc.hInstance mov wc.hbrBackground,COLOR_WINDOW+1 mov wc.lpszMenuName,IDR_MAINMENU mov wc.lpszClassName,OFFSET ClassName invoke LoadIcon,NULL,IDI_APPLICATION mov wc.hIcon,eax mov wc.hIconSm,eax invoke LoadCursor,NULL,IDC_ARROW mov wc.hCursor,eax invoke RegisterClassEx, addr wc INVOKE CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\ WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\ CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\ hInst,NULL mov hwnd,eax invoke ShowWindow, hwnd,SW_SHOWNORMAL invoke UpdateWindow, hwnd invoke LoadAccelerators,hInstance,IDR_MAINACCEL mov hAccel,eax .while TRUE invoke GetMessage, ADDR msg,0,0,0 .break .if (!eax) invoke IsDialogMessage,hSearch,addr msg .if eax==FALSE invoke TranslateAccelerator,hwnd,hAccel,addr msg .if eax==0 invoke TranslateMessage, ADDR msg invoke DispatchMessage, ADDR msg .endif .endif .endw mov eax,msg.wParam ret WinMain endp
StreamInProc proc hFile:DWORD,pBuffer:DWORD, NumBytes:DWORD, pBytesRead:DWORD invoke ReadFile,hFile,pBuffer,NumBytes,pBytesRead,0 xor eax,1 ret StreamInProc endp
StreamOutProc proc hFile:DWORD,pBuffer:DWORD, NumBytes:DWORD, pBytesWritten:DWORD invoke WriteFile,hFile,pBuffer,NumBytes,pBytesWritten,0 xor eax,1 ret StreamOutProc endp


CheckModifyState proc hWnd:DWORD invoke SendMessage,hwndRichEdit,EM_GETMODIFY,0,0 .if eax!=0 invoke MessageBox,hWnd,addr WannaSave,addr AppName,MB_YESNOCANCEL .if eax==IDYES invoke SendMessage,hWnd,WM_COMMAND,IDM_SAVE,0 .elseif eax==IDCANCEL mov eax,FALSE ret .endif .endif mov eax,TRUE ret CheckModifyState endp
SetColor proc LOCAL cfm:CHARFORMAT invoke SendMessage,hwndRichEdit,EM_SETBKGNDCOLOR,0,BackgroundColor invoke RtlZeroMemory,addr cfm,sizeof cfm mov cfm.cbSize,sizeof cfm mov cfm.dwMask,CFM_COLOR push TextColor pop cfm.crTextColor invoke SendMessage,hwndRichEdit,EM_SETCHARFORMAT,SCF_ALL,addr cfm ret SetColor endp
OptionProc proc hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD LOCAL clr:CHOOSECOLOR .if uMsg==WM_INITDIALOG .elseif uMsg==WM_COMMAND mov eax,wParam shr eax,16 .if ax==BN_CLICKED mov eax,wParam .if ax==IDCANCEL invoke SendMessage,hWnd,WM_CLOSE,0,0 .elseif ax==IDC_BACKCOLORBOX invoke RtlZeroMemory,addr clr,sizeof clr mov clr.lStructSize, sizeof clr push hWnd pop clr.hwndOwner push hInstance pop clr.hInstance push BackgroundColor pop clr.rgbResult mov clr.lpCustColors,offset CustomColors mov clr.Flags,CC_ANYCOLOR or CC_RGBINIT invoke ChooseColor,addr clr .if eax!=0 push clr.rgbResult pop BackgroundColor invoke GetDlgItem,hWnd,IDC_BACKCOLORBOX invoke InvalidateRect,eax,0,TRUE .endif .elseif ax==IDC_TEXTCOLORBOX invoke RtlZeroMemory,addr clr,sizeof clr mov clr.lStructSize,sizeof clr push hWnd pop clr.hwndOwner push hInstance pop clr.hInstance push TextColor pop clr.rgbResult mov clr.lpCustColors,offset CustomColors mov clr.Flags,CC_ANYCOLOR or CC_RGBINIT invoke ChooseColor,addr clr .if eax!=0 push clr.rgbResult pop TextColor invoke GetDlgItem,hWnd,IDC_TEXTCOLORBOX invoke InvalidateRect,eax,0,TRUE .endif .elseif ax==IDOK invoke SendMessage,hwndRichEdit,EM_GETMODIFY,0,0 push eax invoke SetColor pop eax invoke SendMessage,hwndRichEdit,EM_SETMODIFY,eax,0 invoke EndDialog,hWnd,0 .endif .endif .elseif uMsg==WM_CTLCOLORSTATIC invoke GetDlgItem,hWnd,IDC_BACKCOLORBOX .if eax==lParam invoke CreateSolidBrush,BackgroundColor ret .else invoke GetDlgItem,hWnd,IDC_TEXTCOLORBOX .if eax==lParam invoke CreateSolidBrush,TextColor ret .endif .endif mov eax,FALSE ret .elseif uMsg==WM_CLOSE invoke EndDialog,hWnd,0 .else mov eax,FALSE ret .endif mov eax,TRUE ret OptionProc endp


SearchProc proc hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD .if uMsg== WM_INITDIALOG push hWnd pop hSearch invoke CheckRadioButton,hWnd,IDC_DOWN,IDC_UP,IDC_DOWN invoke SendDlgItemMessage,hWnd,IDC_FINDEDIT, WM_SETTEXT,0,addr FindBuffer .elseif uMsg==WM_COMMAND mov eax,wParam shr eax,16 .if ax==BN_CLICKED mov eax,wParam .if ax==IDOK mov uFlags,0 invoke SendMessage,hwndRichEdit, EM_EXGETSEL,0,addr findtext.chrg invoke GetDlgItemText,hWnd,IDC_FINDEDIT, addr FindBuffer,sizeof FindBuffer .if eax!=0 invoke IsDlgButtonChecked,hWnd,IDC_DOWN .if eax==BST_CHECKED or uFlags,FR_DOWN mov eax,findtext.chrg.cpMin .if eax!=findtext.chrg.cpMax push findtext.chrg.cpMax pop findtext.chrg.cpMin .endif mov findtext.chrg.cpMax,-1 .else mov findtext.chrg.cpMax,0 .endif invoke IsDlgButtonChecked,hWnd,IDC_MATCHCASE .if eax==BST_CHECKED or uFlags,FR_MATCHCASE .endif invoke IsDlgButtonChecked,hWnd,IDC_WHOLEWORD .if eax==BST_CHECKED or uFlags,FR_WHOLEWORD .endif mov findtext.lpstrText,offset FindBuffer invoke SendMessage,hwndRichEdit,EM_FINDTEXTEX, uFlags,addr findtext .if eax!=-1 invoke SendMessage,hwndRichEdit,EM_EXSETSEL,0, addr findtext.chrgText .endif .endif .elseif ax==IDCANCEL invoke SendMessage,hWnd,WM_CLOSE,0,0 .else mov eax,FALSE ret .endif .endif .elseif uMsg==WM_CLOSE mov hSearch,0 invoke EndDialog,hWnd,0 .else mov eax,FALSE ret .endif mov eax,TRUE ret SearchProc endp
ReplaceProc proc hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD LOCAL settext:SETTEXTEX .if uMsg==WM_INITDIALOG push hWnd pop hSearch invoke SetDlgItemText,hWnd,IDC_FINDEDIT,addr FindBuffer invoke SetDlgItemText,hWnd,IDC_REPLACEEDIT,addr ReplaceBuffer .elseif uMsg==WM_COMMAND mov eax,wParam shr eax,16 .if ax==BN_CLICKED mov eax,wParam .if ax==IDCANCEL invoke SendMessage,hWnd,WM_CLOSE,0,0 .elseif ax==IDOK invoke GetDlgItemText,hWnd,IDC_FINDEDIT, addr FindBuffer,sizeof FindBuffer invoke GetDlgItemText,hWnd,IDC_REPLACEEDIT, addr ReplaceBuffer,sizeof ReplaceBuffer mov findtext.chrg.cpMin,0 mov findtext.chrg.cpMax,-1 mov findtext.lpstrText,offset FindBuffer mov settext.flags,ST_SELECTION mov settext.codepage,CP_ACP .while TRUE invoke SendMessage,hwndRichEdit,EM_FINDTEXTEX, FR_DOWN,addr findtext .if eax==-1 .break .else invoke SendMessage,hwndRichEdit,EM_EXSETSEL,0, addr findtext.chrgText invoke SendMessage,hwndRichEdit,EM_SETTEXTEX, addr settext,addr ReplaceBuffer .endif .endw .endif .endif .elseif uMsg==WM_CLOSE mov hSearch,0 invoke EndDialog,hWnd,0 .else mov eax,FALSE ret .endif mov eax,TRUE ret ReplaceProc endp


GoToProc proc hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD LOCAL LineNo:DWORD LOCAL chrg:CHARRANGE .if uMsg== WM_INITDIALOG push hWnd pop hSearch .elseif uMsg==WM_COMMAND mov eax,wParam shr eax,16 .if ax==BN_CLICKED mov eax,wParam .if ax==IDCANCEL invoke SendMessage,hWnd,WM_CLOSE,0,0 .elseif ax==IDOK invoke GetDlgItemInt,hWnd,IDC_LINENO,NULL,FALSE mov LineNo,eax invoke SendMessage,hwndRichEdit,EM_GETLINECOUNT,0,0 .if eax>LineNo invoke SendMessage,hwndRichEdit,EM_LINEINDEX,LineNo,0 mov chrg.cpMin,eax mov chrg.cpMax,eax invoke SendMessage,hwndRichEdit,EM_EXSETSEL,0,addr chrg invoke SetFocus,hwndRichEdit .endif .endif .endif .elseif uMsg==WM_CLOSE mov hSearch,0 invoke EndDialog,hWnd,0 .else mov eax,FALSE ret .endif mov eax,TRUE ret GoToProc endp
PrepareEditMenu proc hSubMenu:DWORD LOCAL chrg:CHARRANGE invoke SendMessage,hwndRichEdit,EM_CANPASTE,CF_TEXT,0 .if eax==0 ; no text in the clipboard invoke EnableMenuItem,hSubMenu,IDM_PASTE,MF_GRAYED .else invoke EnableMenuItem,hSubMenu,IDM_PASTE,MF_ENABLED .endif invoke SendMessage,hwndRichEdit,EM_CANUNDO,0,0 .if eax==0 invoke EnableMenuItem,hSubMenu,IDM_UNDO,MF_GRAYED .else invoke EnableMenuItem,hSubMenu,IDM_UNDO,MF_ENABLED .endif invoke SendMessage,hwndRichEdit,EM_CANREDO,0,0 .if eax==0 invoke EnableMenuItem,hSubMenu,IDM_REDO,MF_GRAYED .else invoke EnableMenuItem,hSubMenu,IDM_REDO,MF_ENABLED .endif invoke SendMessage,hwndRichEdit,EM_EXGETSEL,0,addr chrg mov eax,chrg.cpMin .if eax==chrg.cpMax ; no current selection invoke EnableMenuItem,hSubMenu,IDM_COPY,MF_GRAYED invoke EnableMenuItem,hSubMenu,IDM_CUT,MF_GRAYED invoke EnableMenuItem,hSubMenu,IDM_DELETE,MF_GRAYED .else invoke EnableMenuItem,hSubMenu,IDM_COPY,MF_ENABLED invoke EnableMenuItem,hSubMenu,IDM_CUT,MF_ENABLED invoke EnableMenuItem,hSubMenu,IDM_DELETE,MF_ENABLED .endif ret PrepareEditMenu endp
WndProc proc hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD LOCAL ofn:OPENFILENAME LOCAL buffer[256]:BYTE LOCAL editstream:EDITSTREAM LOCAL hFile:DWORD LOCAL hPopup:DWORD LOCAL pt:POINT LOCAL chrg:CHARRANGE .if uMsg==WM_CREATE invoke CreateWindowEx,WS_EX_CLIENTEDGE,addr RichEditClass,0, \ WS_CHILD or WS_VISIBLE or ES_MULTILINE or WS_VSCROLL or WS_HSCROLL or ES_NOHIDESEL,\ CW_USEDEFAULT,CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,hWnd,RichEditID,hInstance,0 mov hwndRichEdit,eax invoke SendMessage,hwndRichEdit,EM_LIMITTEXT,-1,0 invoke SetColor invoke SendMessage,hwndRichEdit,EM_SETMODIFY,FALSE,0 invoke SendMessage,hwndRichEdit,EM_SETEVENTMASK,0,ENM_MOUSEEVENTS invoke SendMessage,hwndRichEdit,EM_EMPTYUNDOBUFFER,0,0 .elseif uMsg==WM_NOTIFY push esi mov esi,lParam assume esi:ptr NMHDR .if [esi].code==EN_MSGFILTER assume esi:ptr MSGFILTER .if [esi].msg==WM_RBUTTONDOWN invoke GetMenu,hWnd invoke GetSubMenu,eax,1 mov hPopup,eax invoke PrepareEditMenu,hPopup mov edx,[esi].lParam mov ecx,edx and edx,0FFFFh shr ecx,16 mov pt.x,edx mov pt.y,ecx invoke ClientToScreen,hWnd,addr pt invoke TrackPopupMenu,hPopup,TPM_LEFTALIGN or TPM_BOTTOMALIGN, pt.x,pt.y,NULL,hWnd,NULL .endif .endif pop esi .elseif uMsg==WM_INITMENUPOPUP mov eax,lParam .if ax==0 ; file menu .if FileOpened==TRUE ; a file is already opened invoke EnableMenuItem,wParam,IDM_OPEN,MF_GRAYED invoke EnableMenuItem,wParam,IDM_CLOSE,MF_ENABLED invoke EnableMenuItem,wParam,IDM_SAVE,MF_ENABLED invoke EnableMenuItem,wParam,IDM_SAVEAS,MF_ENABLED .else invoke EnableMenuItem,wParam,IDM_OPEN,MF_ENABLED invoke EnableMenuItem,wParam,IDM_CLOSE,MF_GRAYED invoke EnableMenuItem,wParam,IDM_SAVE,MF_GRAYED invoke EnableMenuItem,wParam,IDM_SAVEAS,MF_GRAYED .endif .elseif ax==1 ; edit menu invoke PrepareEditMenu,wParam .elseif ax==2 ; search menu bar .if FileOpened==TRUE invoke EnableMenuItem,wParam,IDM_FIND,MF_ENABLED invoke EnableMenuItem,wParam,IDM_FINDNEXT,MF_ENABLED invoke EnableMenuItem,wParam,IDM_FINDPREV,MF_ENABLED invoke EnableMenuItem,wParam,IDM_REPLACE,MF_ENABLED invoke EnableMenuItem,wParam,IDM_GOTOLINE,MF_ENABLED .else invoke EnableMenuItem,wParam,IDM_FIND,MF_GRAYED invoke EnableMenuItem,wParam,IDM_FINDNEXT,MF_GRAYED invoke EnableMenuItem,wParam,IDM_FINDPREV,MF_GRAYED invoke EnableMenuItem,wParam,IDM_REPLACE,MF_GRAYED invoke EnableMenuItem,wParam,IDM_GOTOLINE,MF_GRAYED .endif .endif .elseif uMsg==WM_COMMAND .if lParam==0 ; menu commands mov eax,wParam .if ax==IDM_OPEN invoke RtlZeroMemory,addr ofn,sizeof ofn mov ofn.lStructSize,sizeof ofn push hWnd pop ofn.hwndOwner push hInstance pop ofn.hInstance mov ofn.lpstrFilter,offset ASMFilterString mov ofn.lpstrFile,offset FileName mov byte ptr [FileName],0 mov ofn.nMaxFile,sizeof FileName mov ofn.Flags,OFN_FILEMUSTEXIST or OFN_HIDEREADONLY or OFN_PATHMUSTEXIST invoke GetOpenFileName,addr ofn .if eax!=0 invoke CreateFile,addr FileName,GENERIC_READ, FILE_SHARE_READ,NULL,OPEN_EXISTING, \ FILE_ATTRIBUTE_NORMAL,0 .if eax!=INVALID_HANDLE_VALUE mov hFile,eax ;========================================== ; записываем текст в контрол richedit ;========================================== mov editstream.dwCookie,eax mov editstream.pfnCallback,offset StreamInProc invoke SendMessage,hwndRichEdit,EM_STREAMIN, SF_TEXT,addr editstream ;========================================== ; Устанавливаем флаг модификации в false ;========================================== invoke SendMessage,hwndRichEdit,EM_SETMODIFY,FALSE,0 invoke CloseHandle,hFile mov FileOpened,TRUE .else invoke MessageBox,hWnd,addr OpenFileFail, addr AppName,MB_OK or MB_ICONERROR .endif .endif .elseif ax==IDM_CLOSE invoke CheckModifyState,hWnd .if eax==TRUE invoke SetWindowText,hwndRichEdit,0 mov FileOpened,FALSE .endif .elseif ax==IDM_SAVE invoke CreateFile,addr FileName,GENERIC_WRITE, FILE_SHARE_READ,NULL,CREATE_ALWAYS, \ FILE_ATTRIBUTE_NORMAL,0 .if eax!=INVALID_HANDLE_VALUE @@: mov hFile,eax ;======================================================= ; записываем текст в файл ;======================================================= mov editstream.dwCookie,eax mov editstream.pfnCallback,offset StreamOutProc invoke SendMessage,hwndRichEdit,EM_STREAMOUT,SF_TEXT, addr editstream ;========================================================== ; Initialize the modify state to false ; Устанавливаем флаг модификации в false ;========================================================== invoke SendMessage,hwndRichEdit,EM_SETMODIFY,FALSE,0 invoke CloseHandle,hFile .else invoke MessageBox,hWnd,addr OpenFileFail, addr AppName,MB_OK or MB_ICONERROR .endif .elseif ax==IDM_COPY invoke SendMessage,hwndRichEdit,WM_COPY,0,0 .elseif ax==IDM_CUT invoke SendMessage,hwndRichEdit,WM_CUT,0,0 .elseif ax==IDM_PASTE invoke SendMessage,hwndRichEdit,WM_PASTE,0,0 .elseif ax==IDM_DELETE invoke SendMessage,hwndRichEdit,EM_REPLACESEL,TRUE,0 .elseif ax==IDM_SELECTALL mov chrg.cpMin,0 mov chrg.cpMax,-1 invoke SendMessage,hwndRichEdit,EM_EXSETSEL,0,addr chrg .elseif ax==IDM_UNDO invoke SendMessage,hwndRichEdit,EM_UNDO,0,0 .elseif ax==IDM_REDO invoke SendMessage,hwndRichEdit,EM_REDO,0,0 .elseif ax==IDM_OPTION invoke DialogBoxParam,hInstance,IDD_OPTIONDLG, hWnd,addr OptionProc,0 .elseif ax==IDM_SAVEAS invoke RtlZeroMemory,addr ofn,sizeof ofn mov ofn.lStructSize,sizeof ofn push hWnd pop ofn.hwndOwner push hInstance pop ofn.hInstance mov ofn.lpstrFilter,offset ASMFilterString mov ofn.lpstrFile,offset AlternateFileName mov byte ptr [AlternateFileName],0 mov ofn.nMaxFile,sizeof AlternateFileName mov ofn.Flags,OFN_FILEMUSTEXIST or OFN_HIDEREADONLY or OFN_PATHMUSTEXIST invoke GetSaveFileName,addr ofn .if eax!=0 invoke CreateFile,addr AlternateFileName, GENERIC_WRITE,FILE_SHARE_READ, \ NULL,CREATE_ALWAYS,FILE_ATTRIBUTE_NORMAL,0 .if eax!=INVALID_HANDLE_VALUE jmp @B .endif .endif .elseif ax==IDM_FIND .if hSearch==0 invoke CreateDialogParam,hInstance,IDD_FINDDLG, hWnd,addr SearchProc,0 .endif .elseif ax==IDM_REPLACE .if hSearch==0 invoke CreateDialogParam,hInstance,IDD_REPLACEDLG, hWnd,addr ReplaceProc,0 .endif .elseif ax==IDM_GOTOLINE .if hSearch==0 invoke CreateDialogParam,hInstance,IDD_GOTODLG, hWnd,addr GoToProc,0 .endif .elseif ax==IDM_FINDNEXT invoke lstrlen,addr FindBuffer .if eax!=0 invoke SendMessage,hwndRichEdit,EM_EXGETSEL, 0,addr findtext.chrg mov eax,findtext.chrg.cpMin .if eax!=findtext.chrg.cpMax push findtext.chrg.cpMax pop findtext.chrg.cpMin .endif mov findtext.chrg.cpMax,-1 mov findtext.lpstrText,offset FindBuffer invoke SendMessage,hwndRichEdit,EM_FINDTEXTEX, FR_DOWN,addr findtext .if eax!=-1 invoke SendMessage,hwndRichEdit,EM_EXSETSEL, 0,addr findtext.chrgText .endif .endif .elseif ax==IDM_FINDPREV invoke lstrlen,addr FindBuffer .if eax!=0 invoke SendMessage,hwndRichEdit,EM_EXGETSEL,0, addr findtext.chrg mov findtext.chrg.cpMax,0 mov findtext.lpstrText,offset FindBuffer invoke SendMessage,hwndRichEdit,EM_FINDTEXTEX, 0,addr findtext .if eax!=-1 invoke SendMessage,hwndRichEdit, EM_EXSETSEL,0,addr findtext.chrgText .endif .endif .elseif ax==IDM_EXIT invoke SendMessage,hWnd,WM_CLOSE,0,0 .endif .endif .elseif uMsg==WM_CLOSE invoke CheckModifyState,hWnd .if eax==TRUE invoke DestroyWindow,hWnd .endif .elseif uMsg==WM_SIZE mov eax,lParam mov edx,eax and eax,0FFFFh shr edx,16 invoke MoveWindow,hwndRichEdit,0,0,eax,edx,TRUE .elseif uMsg==WM_DESTROY invoke PostQuitMessage,NULL .else invoke DefWindowProc,hWnd,uMsg,wParam,lParam ret .endif xor eax,eax ret WndProc endp end start


АНАЛИЗ
&nbsp Поиск текста реализуется с помощью EM_FINDTEXTEX. Когда пользователь кликает на пункте меню 'Find', отсылается сообщение IDM_FIND и отображается соответствующее диалоговое окно.
invoke GetDlgItemText,hWnd,IDC_FINDEDIT,addr FindBuffer,sizeof FindBuffer .if eax!=0
&nbsp Когда пользователь задает текст, который нужно найти, а затем нажимает кнопку 'OK', текст, который мы собираемся искать, оказывается в FindBuffer.
mov uFlags,0 invoke SendMessage,hwndRichEdit,EM_EXGETSEL,0,addr findtext.chrg
&nbsp Если текстовая строка не равняется NULL, мы устанавливаем значение переменной uFlags равной 0. Эта переменная используется для хранения флагов поиска, отсылаемых вместе с сообщением EM_FINDTEXTEX. После этого мы получаем текущий выделенный текст с помощью EM_EXGETSEL, потому что нам нужно знать, откуда будет производиться поиск.
invoke IsDlgButtonChecked,hWnd,IDC_DOWN .if eax==BST_CHECKED or uFlags,FR_DOWN mov eax,findtext.chrg.cpMin .if eax!=findtext.chrg.cpMax push findtext.chrg.cpMax pop findtext.chrg.cpMin .endif mov findtext.chrg.cpMax,-1 .else mov findtext.chrg.cpMax,0 .endif
&nbsp Следующая часть несколько сложнее. Мы проверяем значение кнопки, задающей направление поиска. Если указан поиск вперед, мы задаем флаг FR_DOWN в uFlags. После этого мы проверяем, выделен ли сейчас какой-нибудь текст, проверяя значения cpMin и cpMax. Если оба значения не равны, это означает, что выделение существует, и мы должны продолжать поиск от конца текущего выделения до конца текста в контроле. Поэтому нам требуется заменить значение cpMax на значение cpMin, а cpMax приравнять к -1 (0FFFFFFFFh). если выделения нет, поиск будет производиться от текущей позиции курсора до конца текста.
&nbsp Если пользователь выберет поиск назад, мы используем диапазон от начала выделения до начала текста в контроле. Поэтому на нужно только изменить значение cpMax на 0. В случае с поиском назад cpMin содержит индекс последнего символа в диапазоне поиска, а cpMax - индекс первого символа. Это инверсия поиска вперед.


invoke IsDlgButtonChecked,hWnd,IDC_MATCHCASE .if eax==BST_CHECKED or uFlags,FR_MATCHCASE .endif invoke IsDlgButtonChecked,hWnd,IDC_WHOLEWORD .if eax==BST_CHECKED or uFlags,FR_WHOLEWORD .endif mov findtext.lpstrText,offset FindBuffer
&nbsp Мы продолжаем проверку checkbox'ов, чтобы задать значения FR_MATCHCASE и FR_WHOLEWORD. Наконец, мы помещаем смещение текста, который нужно найти в поле lpstrText.
invoke SendMessage,hwndRichEdit,EM_FINDTEXTEX,uFlags,addr findtext .if eax!=-1 invoke SendMessage,hwndRichEdit,EM_EXSETSEL,0,addr findtext.chrgText .endif
&nbsp Мы сейчас готовы послать сообщение EM_FINDTEXTEX. После этого мы проверяем результаты поиска, возвращенные SendMessage. Если возвращаемое значение равно -1, значит никаких совпадений не было найдено. В противном случае поле chrgText структуры FINDTEXTEX заполняется индексами совпавшего текста. Мы выделяем найденный текст с помощью EM_EXSETSEL.
&nbsp Операция замещения делается подобным же образом.
invoke GetDlgItemText,hWnd,IDC_FINDEDIT, addr FindBuffer,sizeof FindBuffer invoke GetDlgItemText,hWnd,IDC_REPLACEEDIT, addr ReplaceBuffer,sizeof ReplaceBuffer
&nbsp Мы получаем текст, который нужно найти, и текст, который нужно заменить.
mov findtext.chrg.cpMin,0 mov findtext.chrg.cpMax,-1 mov findtext.lpstrText,offset FindBuffer
&nbsp Чтобы сделать проще, операция замещения действует на весь текст в контроле. Поэтому начальный индекс равен 0, а конечный - -1.
mov settext.flags,ST_SELECTION mov settext.codepage,CP_ACP
&nbsp Мы инициализируем структуру SETTEXTEX, чтобы задать то, что мы хотим заменить текущее выделение и использовать системную страницу кодировки по умолчанию.
.while TRUE invoke SendMessage,hwndRichEdit,EM_FINDTEXTEX, FR_DOWN,addr findtext .if eax==-1 .break .else invoke SendMessage,hwndRichEdit,EM_EXSETSEL, 0,addr findtext.chrgText invoke SendMessage,hwndRichEdit,EM_SETTEXTEX, addr settext,addr ReplaceBuffer .endif .endw
&nbsp Мы входим в бесконечный цикл и ищем совпадающий текст. Если он был найден, мы выбираем его с помощью EM_EXSETSEL и заменяем его EM_SETTEXTEX. Если больше совпадений не было найдено, мы выходим из цикла.


&nbsp 'Find Next' и 'Find Prev.' используют сообщение EM_FINDTEXTEX похожим образом.
&nbsp Теперь мы рассмотрим 'Go to Line'. Когда пользователь кликает по этому пункту меню, мы отображаем диалоговое окно.
&nbsp Когда пользователь набирает номер линии и нажимет кнопку 'Ok', мы начинаем операцию.
invoke GetDlgItemInt,hWnd,IDC_LINENO,NULL,FALSE mov LineNo,eax
&nbsp Получаем номер линии из edit control'а.
invoke SendMessage,hwndRichEdit,EM_GETLINECOUNT,0,0 .if eax>LineNo
&nbsp Получаем количество линий в контроле. Проверяем, не указал ли пользователь номер линии, который выходит за рамки допустимых значений.
invoke SendMessage,hwndRichEdit,EM_LINEINDEX,LineNo,0
&nbsp Если номер линии верен, мы передвигает курсор к первому символу в этой строке. Поэтому мы посылаем сообщение EM_LINEINDEX контролу richedit. Это сообщение возвращает индекс первого символа в заданной строке. Мы посылаем номер строки через wParam, а получаем индекс символа.
invoke SendMessage,hwndRichEdit,EM_SETSEL,eax,eax
&nbsp Чтобы установить текущее выделение, в этот раз мы используем EM_SETSEL, потому что индексы символов уже не находятся в структуре CHARRANGE, что сохраняет нам две инструкции (помещение этих индексов в структуру CHARRANGE).
invoke SetFocus,hwndRichEdit .endif
&nbsp Курсор не будет отображен, пока контрол RichEdit не получит фокус. Поэтому мы вызываем SetFocus.
[C] Iczelion, пер. Aquila.

Подсветка синтаксиса в RichEdit


&nbsp Вначале я хочу вас предупредить, что это сложная тема, не подходящая для начинающего. Это последний туториал из серии о RichEdit.

&nbsp Скачайте пример.

Теория:

&nbsp Подсветка синтаксиса - это предмет жарких дискуссий между создателями текстовых редакторов. Лучший метод (на мой взгляд) - это создать собственный edit control. Именно этот метод применяется во многих коммерческих приложений. Тем не менее для тех из нас, у кого нет времени на создание подобного контрола, лучшим вариантом будет приспособить существующий контрол к нашим нуждам.

&nbsp Давайте посмотрим, как может нам помочь RichEdit в реализации цветовой подсветки. Я должен сказать, что следующий метод "неверен": я всего лишь продемонстрирую ловушку, в которую угодили многие. RichEdit предоставляет сообщение EM_SETCHARFORMAT, которое позволяет вам менять цвет текста. На первый взгляд это именно то, что нам нужно (я знаю, потому что я стал одной из жертв этого подхода). Тем не менее, более детальное исследование покажет вам, что у данного сообщения есть несколько недостатков:

&nbsp EM_SETCHARFORMAT работает либо со всем текстом сразу, либо только с тем, который сейчас выделен. Если вам потребуется изменить цвет определенного слова, вам сначала придется выделить его.

&nbsp EM_SETCHARFORMAT очень медленна.

&nbsp У нее есть проблемы с позицией курсора в контроле RichEdit.

&nbsp Из всего вышеизложенного вы можете сделать вывод, что использование EM_SETCHARFORMAT - неправильный выбор. Я покажу вам "относительно верный" выбор.

&nbsp Мой метод заключает только в том, чтобы подсвечивать только видимую часть текста. В этом случае скорость подсветки не будет зависеть от размера файла. Вне зависимости от того, как велик файл, только маленькая его часть видна на экране.

&nbsp Как это сделать? Ответ прост:

&nbsp Субклассируйте контрол RichEdit и обрабатывайте сообщение WM_PAINT внутри вашей оконной процедуры.

&nbsp Когда она встречает сообщение WM_PAINT, вызывается оригинальная оконная функция, которая обновляет экран как обычно.


& nbsp После этого мы перерисовываем слова, которые нужно отобразить другим цветом.
&nbsp Конечно, этот путь не так прост: есть несколько вещей, которые нужно пофиксить, но вышеизложенный метод работает достаточно хорошо. Скорость отображения на экране более чем удовлетворительна.
&nbsp Теперь давайте сконцентрируемся на деталях. Процедура сабклассинга проста и не требует внимательного рассмотрения. Действительно сложная часть - это когда мы находим быстрый способ поиска слов, которые нужно подсветить. Это усложняется тем, что не надо подсвечивать слова внутри комментариев.
&nbsp Метод, который я использовал, возможно, не самый лучший, но он работает хорошо. Я уверен, что вы сможете найти более быстрый путь. Как бы то ни было, вот он:
&nbsp Я создал массив на 256 двойных слов, заполненный нулями. Каждый dword соответствует возможному ASCII-символу. Я назвал массив ASMSyntaxArray. Например, 21-ый элемент представляет собой 20h (пробел). Я использовал массив для быстрого просмотра таблицы: например, если у меня есть слово "include", я извлекаю первый символ из слова и смотрю значение соответствующего элемента. Если оно равно 0, я знаю, что среди слов, которые нужно подсвечивать, нет таких, которые бы начинались с "i". Если элемент не равен нулю, он содержит указатель на структуру WORDINFO, в которой находится информацию о слове, которое должно быть подсвечено.
&nbsp Я читаю слова, которые нужно подсвечивать, и создаю структуру WORDINFO для каждого из них.
WORDINFO struct WordLen dd ? ; длина слова: используется для быстрого сравнения pszWord dd ? ; указатель на слово pColor dd ? ; указатель на dword, содержащий цвет, которым ; нужно подсвечивать слово NextLink dd ? ; указатель на следующую структуру WORDINFO WORDINFO ends
&nbsp Как вы можете видеть, я использовал длину слова для убыстрения процесса сравнивания. Если первый символ слова совпадает, мы можем сравнить его длину с доступными словами. Каждый dword в ASMSyntaxArray содержит указатель на начало ассоциированного с ним массива WORDINFO. Например dword, который представляет символ "i", будет содержать указатель на связанный список слов, которые начинаются с "i". Поле pColor указывает на слово, содержащее цвет, которым будет подсвечиваться слово. pszWord указывает на слово, которое будет подсвечиваться, преобразованное к нижнему регистру.


& nbsp Память для связанного списка занимается из кучи программы по умолчанию, поэтому очищать ее легко и быстро: никакой чистки не требуется вообще.
&nbsp Список слов сохраняется в файле под названием "wordfile.txt", я получаю к нему доступ через API-функцию GetPrivateProfileString. Есть десять различных подсветок символов, начинающиеся с C1 до C10. Массив цветов называется ASMColorArray. Поле pColor каждой структуры WORDINFO указывает на один из элементов ASMColorArray. Поэтому можно легко изменять подсветку синтаксиса на лету: вам всего лишь нужно изменить dword в ASMColorArray и все слова, которые используют этот цвет, будут использовать новое значение.
ПРИМЕР
Здесь исходник. Возмите его из архива ;)
АНАЛИЗ
&nbsp Первое действие, совершаемое до вызова WinMain, это вызов FillHiliteInfo. Эта функция считывает содержимое wordfile.txt и обрабатывает его.
FillHiliteInfo proc uses edi LOCAL buffer[1024]:BYTE LOCAL pTemp:DWORD LOCAL BlockSize:DWORD invoke RtlZeroMemory,addr ASMSyntaxArray,sizeof ASMSyntaxArray
Initialize ASMSyntaxArray to zero. invoke GetModuleFileName,hInstance,addr buffer,sizeof buffer invoke lstrlen,addr buffer mov ecx,eax dec ecx lea edi,buffer add edi,ecx std mov al,"\" repne scasb cld inc edi mov byte ptr [edi],0 invoke lstrcat,addr buffer,addr WordFileName
&nbsp Создаем путь до wordfile.txt: я предполагаю, что он всегда находится в той же папке, что и сама программа.
invoke GetFileAttributes,addr buffer .if eax!=-1
&nbsp Я использую этот метод как быстрый путь для проверки, существует ли файл.
mov BlockSize,1024*10 invoke HeapAlloc,hMainHeap,0,BlockSize mov pTemp,eax
&nbsp Резервируем блок памяти, чтобы сохранять слова. По умолчанию - 10 килобайт. Память занимаем из кучи по умолчанию.
@@: invoke GetPrivateProfileString,addr ASMSection, addr C1Key,addr ZeroString,pTemp, BlockSize,addr buffer .if eax!=0
&nbsp Я использовал GetPrivateProfileString, чтобы получить содержимое каждого ключевого слова в wordfile.txt, начиная с C1 и до C10.


inc eax .if eax==BlockSize ; the buffer is too small add BlockSize,1024*10 invoke HeapReAlloc,hMainHeap,0,pTemp,BlockSize mov pTemp,eax jmp @B .endif
&nbsp Проверяем, достаточно ли велик блок памяти. Если это не так, мы увеличиваем размер на 10k, пока он не окажется достаточно велик.
mov edx,offset ASMColorArray invoke ParseBuffer,hMainHeap, pTemp,eax,edx,addr ASMSyntaxArray
&nbsp Передаем слова, хэндл на блок памяти, размер данных, считанных из wordfile.txt, адрес dword с цветом, который будет использоваться для подсветки слов и адреса ASMSyntaxArray.
&nbsp Теперь давайте посмотрим, что делает ParseBuffer. В сущности, эта функция принимает буфер, содержащий слова, которые должны быть выделены, парсит их на отдельные слова и сохраняет каждое из них в массиве структур WORDINFO, к которому можно легко получить доступ из ASMSyntaxArray.
ParseBuffer proc uses edi esi hHeap:DWORD, pBuffer:DWORD, nSize:DWORD, ArrayOffset:DWORD,pArray:DWORD LOCAL buffer[128]:BYTE LOCAL InProgress:DWORD mov InProgress,FALSE
&nbsp InProgress - это флаг, который я использовал, чтобы суметь определить, начался ли процесс сканирования. Если значение равно FALSE, vs
lea esi,buffer mov edi,pBuffer invoke CharLower,edi
&nbsp esi указывает на наш буфер, который будет содержать слово, взятое нами из списка слов. edi указывает на строку-список слов. Чтобы упростить поиск, мы приводим все символы к нижнему регистру.
mov ecx,nSize SearchLoop: or ecx,ecx jz Finished cmp byte ptr [edi]," " je EndOfWord cmp byte ptr [edi],9 ; tab je EndOfWord
&nbsp Сканируем весь список слов в буфере, ориентируясь по пробелам, по которым определяем конец или начало слова.
mov InProgress,TRUE mov al,byte ptr [edi] mov byte ptr [esi],al inc esi SkipIt: inc edi dec ecx jmp SearchLoop
&nbsp Если оказывается, что байт не является пробелом, то мы копируем его в буфер, где создается слово, а затем продолжаем поиск.
EndOfWord: cmp InProgress,TRUE je WordFound jmp SkipIt
&nbsp Если пробел был найден, то мы проверяем значение в InProgress. Если значение равно TRUE, мы можем предположить, что был достигнут конец слова и мы можем поместить то, что у нас получилось в наш буфер (на который указывает esi) в структуру WORDINFO. Если значение равно FALSE, мы продолжаем сканирование дальше.


WordFound: mov byte ptr [esi],0 push ecx invoke HeapAlloc,hHeap,HEAP_ZERO_MEMORY,sizeof WORDINFO
& nbsp Если был достигнут конец слова, мы прибавляем 0 к буферу, чтобы сделать из слова строку формата ASCIIZ. Затем мы занимаем для этого слова блок памяти из кучи размером в WORDINFO.
push esi mov esi,eax assume esi:ptr WORDINFO invoke lstrlen,addr buffer mov [esi].WordLen,eax
&nbsp Мы получаем длину слова в нашем буфере и сохраняем ее в поле WordLen структуры WORDINFO, чтобы использовать в дальнейшем при быстром сравнении.
push ArrayOffset pop [esi].pColor
&nbsp Сохраняем адрес dword'а, который содержит цвет для подсветки слова, в поле pColor.
inc eax invoke HeapAlloc,hHeap,HEAP_ZERO_MEMORY,eax mov [esi].pszWord,eax mov edx,eax invoke lstrcpy,edx,addr buffer
&nbsp Занимает блок памяти из кучи, чтобы сохранить само слово. В настоящее время структура WORDINFO готова для вставки в соответствующий связанный список.
mov eax,pArray movzx edx,byte ptr [buffer] shl edx,2 ; multiply by 4 add eax,edx
&nbsp pArray содержит адрес ASMSyntaxArray. Нам нужно перейти к dword'у, у которого тот же индекс, что и у значения первого символа слова. Поэтому мы помещаем первый символ в edx, а затем умножаем edx на 4 (так как каждый элемент в ASMSyntaxArray равен 4 байтам), а затем добавляем получившееся смещение к адресу ASMSyntaxArray. Теперь адрес нужного dword'а находится в eax.
.if dword ptr [eax]==0 mov dword ptr [eax],esi .else push dword ptr [eax] pop [esi].NextLink mov dword ptr [eax],esi .endif
&nbsp Проверяем значение dword'а. Если оно равно 0, это означает, что в настоящее время ключевых слов, которые начинаются с этого символа, нет. Затем мы помещаем адрес текущей структуры WORDINFO в этот dword.
&nbsp Если значение dword'а не равно 0, это означает, что есть по крайней мере одно ключевое слово, которое начинается с этого символа. Затем мы вставляем эту структуру WORDINFO в голову связанного списка и обновляем его поле NextLink, чтобы оно указывало на структуру WORDINFO.


pop esi pop ecx lea esi,buffer mov InProgress,FALSE jmp SkipIt
&nbsp После того как операция завершена, мы начинаем следующую цикл сканирования, пока не будет достигнуть конец буфера.
invoke SendMessage,hwndRichEdit,EM_SETTYPOGRAPHYOPTIONS, TO_SIMPLELINEBREAK,TO_SIMPLELINEBREAK invoke SendMessage,hwndRichEdit,EM_GETTYPOGRAPHYOPTIONS,1,1 .if eax==0 ; means this message is not processed mov RichEditVersion,2 .else mov RichEditVersion,3 invoke SendMessage,hwndRichEdit,EM_SETEDITSTYLE, SES_EMULATESYSEDIT,SES_EMULATESYSEDIT .endif
&nbsp После того, как контрол RichEdit создан, нам нужно определить его версию. Этот шаг необходим, так как поведение EM_POSFROMCHAR отличается в зависимости от версии RichEdit, а EM_POSFROMCHAR жизненно важна для нашей процедуры подсветки. Я никого не видел документированного способа определения версии RichEdit, поэтому я пошел окольным путем. Я устанавливаю опцию, которая свойственная версии 3.0 и немедленно возвращает его значение. Если я могу получить значение, я предполагаю, что версия этого контрола 3.0.
&nbsp Если вы используете контрол RichEdit версии 3.0, вы можете заметить, что обновление цвета фонт на больших файлах занимает довольно много времени. Похоже, что эта проблема существует только в версии 3.0. Я нашел способ обойти это, заставив конрол эмулировать поведение контрола edit, послав сообщение EM_SETEDITSTYLE.
&nbsp После того, как мы получили информацию о версии контрола, мы переходим к сабклассированию контрола RichEdit. Сейчас мы рассмотрим новую процедуру для обработки сообщений.
NewRichEditProc proc hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD ....... ....... .if uMsg==WM_PAINT push edi push esi invoke HideCaret,hWnd invoke CallWindowProc,OldWndProc,hWnd, uMsg,wParam,lParam push eax
&nbsp Мы обрабатываем сообщение WM_PAINT. Во-первых, мы прячем курсор, чтобы избежать уродливых артефактов после подсветки. Затем мы передаем сообщение оригинальной процедуре richedit, чтобы оно обновило окно. Когда CallWindowProc возвращает управление, текс обновляется согласно его обычному цвету/бэкграунду. Теперь мы можем сделать подсветку синтаксиса.


mov edi,offset ASMSyntaxArray invoke GetDC,hWnd mov hdc,eax invoke SetBkMode,hdc,TRANSPARENT
&nbsp Сохраняем адрес ASMSyntaxArray в edi. Затем мы получаем хэндл контекста устройства и делаем бэкграунд текста прозрачными, чтобы при выводе нами текста использовался текущий бэкграундный цвет.
invoke SendMessage,hWnd,EM_GETRECT,0,addr rect invoke SendMessage,hWnd,EM_CHARFROMPOS,0,addr rect invoke SendMessage,hWnd,EM_LINEFROMCHAR,eax,0 invoke SendMessage,hWnd,EM_LINEINDEX,eax,0
&nbsp Мы хотим получить видимый текст, поэтому сначала нам требуется узнать размеры области, которую необходимо форматировать, послав ему EM_GETRECT. Затем мы получаем индес ближайшего к левому верхнему углу символа с помощью сообщения EM_CHARFROMPOS. Как только мы получаем индекс первого символа, мы начинаем делать цветовую подсветку, начиная с этой позиции. Но эффект может быть не так хорош, как если бы начали с первого символа линии, в которой находится символ. Вот почему мне нужно было получить номер линии, в которой находится первый видимый символ, с помощью сообщения EM_LINEFROMCHAR. Чтобы получить первый символ этой линии, я посылаю сообщение EM_LINEINDEX.
mov txtrange.chrg.cpMin,eax mov FirstChar,eax invoke SendMessage,hWnd,EM_CHARFROMPOS,0,addr rect.right mov txtrange.chrg.cpMax,eax
&nbsp Как только мы получили индекс первого символа, сохраняем его на будущее в переменной FirstChar. Затем мы получаем последний видимый символ, посылая ему EM_CHARFROMPOS, передавая нижний правый угол форматируемой области в lParam.
push rect.left pop RealRect.left push rect.top pop RealRect.top push rect.right pop RealRect.right push rect.bottom pop RealRect.bottom invoke CreateRectRgn,RealRect.left,RealRect.top, RealRect.right,RealRect.bottom mov hRgn,eax invoke SelectObject,hdc,hRgn mov hOldRgn,eax
&nbsp Во время подсветки синтаксиса, я заметил один побочный эффект этого метода: если у контрола richedit'а есть отступ (который вы можете указать, послав сообщение EM_SETMARGINS контролу RichEdit), DrawText пишет поверх него. Поэтому мне требуется создать функцией CreateRectRgn ограничительный регион, в который будут выводиться результат выполнения функций GDI.


& nbsp Затем нам требуется подсветить комментарии и убрать их с нашего пути. Мой метод состоит в поиске ";" и подсветке текста цветом комментария, пока не будет достигнут перевод каретки. Я не будут анализировать здесь эту процедуру: она довольно длинна и сложна. Достаточно сказать, что когда подсвечены все комментарии, мы замещаем их нулями в нашем буфере, чтобы слова в комментарии не обрабатывались позже.
mov ecx,BufferSize lea esi,buffer .while ecx>0 mov al,byte ptr [esi] .if al==" " || al==0Dh || al=="/" || al=="," || al=="|" || \ al=="+" || al=="-" || al=="*" || al=="&" || al=="<" \ || al==">" || al=="=" || al=="(" || al==")" || al=="{" \ || al=="}" || al=="[" || al=="]" || al=="^" || al==":" \ || al==9 mov byte ptr [esi],0 .endif dec ecx inc esi .endw
&nbsp Как только мы избавились от комментариев, мы разделяем слова в буфере, замещая символы "разделения" нулями. С помощью этого метода нам не нужно заботиться о символах разделения во время обработки слов в дальнейшем, так как у нас будет только один символ разделения.
lea esi,buffer mov ecx,BufferSize .while ecx>0 mov al,byte ptr [esi] .if al!=0
&nbsp Ищем в буфере первый символ, не равный NULL, т.е. первый символ слова.
push ecx invoke lstrlen,esi push eax mov edx,eax
&nbsp Получаем длину слова и помещаем его в edx.
movzx eax,byte ptr [esi] .if al>="A" && al<="Z" sub al,"A" add al,"a" .endif
&nbsp Конвертируем символ в нижний регистр (если он в верхнем).
shl eax,2 add eax,edi ; edi содержит указатель на массив указателей ; на структуры WORDINFO .if dword ptr [eax]!=0
&nbsp После этого мы переходим к соответствующему dword'у в ASMSyntaxArray и проверяем равно ли его значение 0. Если это так, мы можем перейти к следующему слову.


mov eax,dword ptr [eax] assume eax:ptr WORDINFO .while eax!=0 .if edx==[eax].WordLen
&nbsp Если значение dword' а не равно нулю, он указывает на связанный список структур WORDINFO. Мы делаем пробег по связанному списку, сравнивая длину слова в нашем буфере с длиной слова в структуре WORDINFO. Это быстрый тест, проводимый до полноценного сравнения слов, что должно сохранить несколько тактов.
pushad invoke lstrcmpi,[eax].pszWord,esi .if eax==0
&nbsp Если длины обоих слов равны, мы переходим к сравнению слов с помощью lstrcmpi.
popad mov ecx,esi lea edx,buffer sub ecx,edx add ecx,FirstChar
&nbsp Мы создаем индекс символа из адресов первого символа совпадающего слова в буфере. Сначала мы получаем его относительное смещение из стартового адреса буфера, а затем добавляем индекс символа первого видимого символа к нему.
pushad .if RichEditVersion==3 invoke SendMessage,hWnd,EM_POSFROMCHAR,addr rect,ecx .else invoke SendMessage,hWnd,EM_POSFROMCHAR,ecx,0 mov ecx,eax and ecx,0FFFFh mov rect.left,ecx shr eax,16 mov rect.top,eax .endif popad
&nbsp Узнав индекс первого символа слова, которое должно быть подсвечено, мы переходим к получению его координат с помощью сообщения EM_POSFROMCHAR. Тем не менее это сообщение интерпретируется различным образом в RichEdit 2.0 или 3.0. В RichEdit 2.0 wParam содержит индекс символа, а lParam не используется. Сообщение возвращается в eax. В RichEdit 3.0 wParam - это указатель на структуру POINT, которая будет заполнена координатой, а lParam содержит индекс символа.
&nbsp Как вы можете видеть, передача неверных аргументов EM_POSFROMCHAR может привести к непредсказуемым результатам. Вот почему я должен определить версию RichEdit.
mov edx,[eax].pColor invoke SetTextColor,hdc,dword ptr [edx] invoke DrawText,hdc,esi,-1,addr rect,0
&nbsp Как только мы получили координату, с которой надо начинать, мы устанавливаем цвет текста, который указан в структуре WORDINFO.
&nbsp В заключение я хочу сказать, что этот метод можно улучшить различными способами. Например я получаю весь текст, который начинается от первой до последней видимой линии. Если линии очень длинные, качество может пострадать из-за обработки невидимых слов. Вы можете оптимизировать это, получая видимый текст полинейно. Также можно улучшить алгоритм поиска. Поймите меня правильно: подсветка синтаксиса, используемая в этом примере, быстрая, но она может быть быстрее. :)
[C] Iczelion, пер. Aquila.

Контрол Listview


В этом туториале мы изучим как создать и использовать контрол listview.

Скачайте пример.

Теория:

Listview - это один из common control'ов, таких как treeview, richedit и так далее. Вы знакомы с ними, даже если не занете их имен. Например, правая панель Windows Exрlorer'а - это контрол listview. Этот контрол подходит для отображения item'ов. В этом отношении его можно рассматривать как усовершенствованный listbox.

Вы можете создать listview двумя путями. Первый метод самый простой: создайте его с помощью редактора ресурсов, главное не забудьте поместить вызов InitCommonControls. Другой метод заключается в вызове CreateWindowsEx. Вы должны указать правильное имя класса окна, то есть SysListView32.

Существует четыре метода отображения item'ов в listview: иконки, маленькие иконки, список и отчет. Вы можете увидеть чем отличаются виды отображения друг от друга, выбрав View->Large Icons (иконки), Small Icons (маленькие иконки), List (список) and Details (отчет)

Теперь, когда мы знаем, как создать listview, мы рассмотрим, как его можно применять. Я сосредоточусь на отчете, как методе отображения, который может продемонстрировать многие свойства listview. Шаги использования listview следующие:

Создаем listview с помощью CreateWindowEx, указав SysListView32 как имя класса. Вы должны указать начальный тип отображения.

(если предусматривается) Создаем и инициализируем списки изображений, которые будут использованы при отображении item'ов listview.

Вставляем колонки в listview. Этот шаг необходим, если listview будет использовать тип отображения 'отчет'.

Вставьте item'ы и подitem'ы в listview.

Колонки

При отчете в listview может быть одна или более колонок. Вы можете считать тип организации данных в этом режиме таблицей: данные организованны в ряды и колонки. В режиме отчета в listview должна быть по крайней мере одна колонка. В других режимах вам не надо вставлять колонку, так как в контроле будет одна и только одна колонка.

Вы можете вставить колонку, послав сообщение LVM_INSERTCOLUMN контролу listview.


LVM_INSERTCOLUMN wParam = iCol lParam = pointer to a LV_COLUMN structure
iCol - это номеp колонки, начиная с нуля.
LV_COLUMN содержит информацию о колонке, которая должна быть вставлена. У нее следующее определение:
LV_COLUMN STRUCT imask dd ? fmt dd ? lx dd ? pszText dd ? cchTextMax dd ? iSubItem dd ? iImage dd ? iOrder dd ? LV_COLUMN ENDS
imask - коллекция флагов, задающие, какие члены структуры верны. Этот параметр был введен, потому что не все члены этой структуры используются одновременно. Некоторые из них используются в особых ситуациях. Эта структура используются и для ввода и для вывода, поэтому важно, чтобы вы пометили, какие параметры верны. Существуют следующие флаги:
LVCF_FMT = The fmt member is valid.

LVCF_SUBITEM = The iSubItem member is valid.

LVCF_TEXT = The pszText member is valid.

LVCF_WIDTH = The lx member is valid.

LVCF_FMT = Параметр fmt верен.

LVCF_SUBITEM = Параметр isubItem верен.

LVCF_TEXT = Параметр рszText верен.

LVCF_WIDTH = Параметр lx верен.

Вы можете комбинировать вышеприведенные флаги. Например, если вы хотите указать текстовое имя колонки, вам нужно предоставить указатель на строку в параметре рszText. Также вы должны указать Windows, что параметр рszText содержит данные, указав флаг LVCF_TEXT в этом поле, иначе Windows будет игнорировать значение pszText.
fmt - указывает выравнивание элементов/подэлементов в колонке. Доступны следующие значения:
LVCFMT_CENTER = Text is centered.

LVCFMT_LEFT = Text is left-aligned.

LVCFMT_RIGHT = Text is right-aligned.

LVCFMT_CENTER = текст отцентрированы.

LVCFMT_LEFT = текст выравнивается слева.

LVCFMT_RIGHT = текст выравнивается справа.

lx - ширина колонки в пикселях. В дальнейшем вы можете изменить ширину колонки LVM_SETCOLUMNWIDTH.
рszText - содержит указатель на имя колонки, если эта структура используется для установки свойств колонки. Если эта структура используется для получения свойств колонки, это поле содержит указатель на буфеp, достаточно большой для получения имени колонки, которая будет возвращена. В этом случае вы должны указать размер буфера в поле cchTextMax. Вы должны игнорировать cchTextMax, если вы хотите установить имя колонки, потому что имя должно быть ASCIIZ-строкой, длину которой Windows сможет определить.


cchTextMax - pазмеp в байтах буфер, указанного в поле pszText. Этот параметр используется только когда вы используете структуру для получения информации о колонке. Если вы используете эту структуру, чтобы установить свойства колонки, это поле будет игнорироваться.
iSubItem - указывает индекс подэлемента, ассоциированного с этой колонкой. Это значение используется в качестве маркера подэлемента, с которым ассоциирована эта колонка. Если хотите, вы можете указать бессмысленный номер и ваш listview будет прекрасно работать. Использование этого поля лучше всего демонстрируется, когда у вас есть номер колонки и вам нужно узнать с каким поэлементом ассоциирована эта колонка. Чтобы сделать это, вы можете послать сообщение LVM_GETCOLUMN, указав в параметре imask флаг LVCF_SUBITEM. Listview заполнит параметр iSubItem значением, которое вы укажете в этом поле, поэтому для работоспособности данного метода вам нужно указывать корректные подэлементы в этом поле.
iImage и iOrder - используется начиная с Internet Explorer 3.0. У меня нет информации относительно этих полей.
Когда listview создан, вам нужно вставить в него одну или более колонок. Если не предполагается переключение в режим отчета, то это не нужно. Чтобы вставить колонку, вам нужно создать структуру LV_COLUMN, заполнить ее необходимой информацией, указать номер колонки, а затем послать структуру listview с помощью сообщения LVM_INSERTCOLUMN.
LOCAL lvc:LV_COLUMN mov lvc.imask,LVCF_TEXT+LVCF_WIDTH mov lvc.pszText,offset Heading1
mov lvc.lx,150 invoke SendMessage,hList, LVM_INSERTCOLUMN,0,addr lvc
Вышеприведенный кусок кода демонстрирует процесс. Он указывает текста заголовка и его ширину, а затем посылает сообщение LVM_INSETCOLUMN listview. Это просто.
Item'ы и под-item'ы
Item'ы - это основные элементы listview. В режимах отображения, отличных от отчета, вы будет видеть только item'ы. Под-item'ы - это детали item'ов. Hапример, если item - это имя файла, тогда вы можете считать атрибуты файла, его pазмеp, дату создания файла как под-item'ы. В режиме отчета самая левая колонка содержит item'ы, а остальные - под-item'ы. Вы можете думать о item'е и его под-item'ах как о записи базы данных. Item - это основной ключ записи и его под-item'ы - это поля записи.


Минимум, что вам нужно иметь в listview - это item'ы, под-item' ы необязательны. Тем не менее, если вы хотите дать пользователю больше информации об элементах, вы можете ассоциировать item'ы с под-item'ами, чтобы пользователь мог видеть детали в режиме отчета.
Вставить item в listview можно послав сообщение LVM_INSERTITEM. Вам также нужно передать адрес структуры LV_ITEM в lParam. LV_ITEM имеет следующее определение:
LV_ITEM STRUCT imask dd ? iItem dd ? iSubItem dd ? state dd ? stateMask dd ? pszText dd ? cchTextMax dd ? iImage dd ? lParam dd ? iIndent dd ? LV_ITEM ENDS
imask - множество флагов, которые задают, какие из параметров данной структуры будут верны. В сущности, это поле идентично параметру imask LV_COLUMN. Чтобы получить детали относительно флагов, обратитесь к вашему справочнику по API.
iItem - индек item'а, на который ссылается эта структура. Индексы начинаются с нуля. Вы можете считать, что это поле содержит значение "ряда" таблицы.
iSubItem - индекс под-item'а, ассоциированный с item'ом, заданном в iItem. Вы можете считать, что это поле содержит "колонку" таблицы. Например, если вы хотите вставить item в только что созданный listview, значение в iItem будет pавно 0 (потому что этот item первый), а значение в iSubItem также будет равно нулю (нам нужно вставить item в первую колонку). Если вы хотите указать под-item, ассоциированный с этим item'ом, iItem будет являться индексом item'а, с которым будет происходить ассоциирование (в выше приведенном примере это 0). iSubItem будет pавен 1 или более, в зависимости от того, в какую колонку вы хотите вставить под-item. Hапример, если у вашего listview 4 колонки, первая колонка будет содержать item'ы. Остальные 3 колонки предназначаются для под-item'ов. Если вы хотите вставить под-item в 4-ую колонку, вам нужно указать в iSubItem значение 3.
state - параметр, содержащий флаги, отражающие состояние item'а. Оно может изменяться из-за действий юзера или другой программы. Термин 'состояние' включает в себя, находится ли item в фокусе, подсвечен ли он, выделен для операции вырезания, выбран ли он. В добавление к флагам состояния он также содержит основанный на единице индекс изображения состояния данного item'а.


stateMask - так как параметр state может содержать флаги состояния, индекс изображения, нам требуется сообщить Windows, какое значение мы хотим установить или получить. Это поле создано именно для этого.
рszText - адрес ASCIIZ-строки, которая будет использоваться в качестве названия элемента в случае, если мы хотим установить или вставить элемент. Если мы используем эту структуру для того, чтобы получить свойства элемента, этот параметр должен содержать адрес буфера, который будет заполнен названием элемента.
cchTextMax - это поле используется только тогда, когда вы используете данную структуру, чтобы получать информацию об элементе. В этом случае это поле содержит размер в байтах буфера, указанного параметром рszText.
iImage - индекс image list'а, содержащего иконки для listview. Индекс указывает на иконку, которая будет использоваться для этого элемента.
lParam - определяемое пользователем значение, которое будет использоваться, когда вы будете сортировать элементы в listview. Кратко говоря, когда вы будете указывать listview отсортировать item'ы, listview будет сравнивать item'ы попарно. Он будет посылать значение lParam обоих элементов вам, чтобы вы могли pешить, какое из этих двух должно быть в списке идти pаньше. Если вы пока не можете этого понять, не беспокойтесь. Вы изучите сортировку позже.
Давайте кратко изложим шаги вставления элемента/подэлемента в listview.
Создаем переменную типа структуры LV_ITEM.
Заполняем ее необходимой информацией.
Посылаем сообщение LVM_INSERTITEM listview, если вам нужно вставить элемент. Или, если вы хотите вставить подэлемент, посылаем сообщение LVM_SETITEM. Это может смущать вас, если вы не понимаете взаимоотношений между элементом и его подэлементами. Подэлементы считаются свойствами элемента. Поэтому вы можете вставить item'ы, но не под-item'ы, а также у вас не может быть подэлемента без ассоциированного с ним элемента. Вот почему вам нужно послать сообщение LVM_SETITEM, чтобы добавить подэлемент вместо LVM_INSERTITEM.


Сообщения/уведомления listview
Теперь, когда вы знаете, как создавать и заполнять элементами listview, следующим шагом является общение с ним. Listview общается с родительским окном через сообщения и уведомления. Родительское окно может контролировать listview, посылая ему сообщения. Listview уведомляет родительское окно о важных/интересных сообщения через сообщение WM_NOTIFY, как и другие common control'ы.
Сортировка элементов/подэлементов
Вы можете указать порядок сортировки контрола listview по умолчанию указав стили LVS_SORTASCENDING или LVS_SORTDESCENDING в CreateWindowEx. Эти два стиля упорядочивают элементы только по элементам. Если вы хотите отсортировать элементы другим путем, вы должны послать сообщение LVM_SORTITEMS listview.
LVM_SORTITEMS wParam = lParamSort lParam = pCompareFunction
lParamSort - это определяемое пользователем значение, которое будет передаваться функции сравнения. Вы можете использовать это значение любым путем, которым хотите.
рComрareFunction - это адрес задаваемой пользователем функции, которая будет определять результат сравнения item'ов в listview. Функция имеет следующий прототип:
CompareFunc proto lParam1:DWORD, lParam2:DWORD, lParamSort:DWORD
lParam1 или lParam2 - это значения параметра lParam LV_ITEM, который вы указали, когда вставляли элементы в listview.
lParamSort - это значение wParam, посланное вместе с сообщением LVM_SORTITEMS.
Когда listview получает сообщение LVM_SORTITEMS, она вызывает сортирующую функцию, указанную в параметре lParam, когда ей нужно узнать результат сравнения двух элементов. Кратко говоря, функция сравнения будет решать, какой из двух элементов, посланных ей, будет предшествовать другому. Правило простое: если функция возвращается отрицательное значение, тогда первый элемент (указанный в lParam1) будет предшествовать другому.
Если функция возвращает положительное значение, второй элемент (заданный параметром lParam2) должен предшествовать первому. Если оба равны, тогда функция должна возвратить ноль.


Что заставляет этот метод работать, так это значение lParam структуры LV_ITEM. Если вам нужно отсортировать item'ы (например, когда пользователь кликает по заголовку колонки), вам нужно подумать о схеме сортировки, в которой будет использоваться значения параметра lParam. В данном примере я помещаю это поле индекс элемента, чтобы получить другую информация о нем, послав сообщение LVM_GETITEM. Заметьте, что когда элементы перегруппированы, их индексы также менядтся. Поэтому когда сортировка в моем примере выполнена, мне необходимо обновить значения в lParam, чтобы учесть новые значения индексов. Если вы хотите отсортировать элементы, когда пользователь кликает по заголовку колонки, вам нужно обработать уведомительное сообщение LVN_COLUMNCLICK в вашей оконной процедуре. LVN_COLUMNCLICK передается вашему окну через сообщение WM_NOTIFY.
ПРИМЕР
Этот пример создает listview и заполняем его именами и размерами полей текущей папки. Режим отображения элементов по умолчанию поставлен в 'отчет'. В этом режиме вы можете кликать по заголовку колонок и элементы будут отсортированы согласно восходящему/нисходящему порядку. Вы можете выбрать режим отображения в меню. Когда вы делает двойной клик по элементу, показывается окно с названием элемента.
.386 .model flat,stdcall option casemap:none include \masm32\include\windows.inc include \masm32\include\user32.inc include \masm32\include\kernel32.inc include \masm32\include\comctl32.inc includelib \masm32\lib\comctl32.lib includelib \masm32\lib\user32.lib includelib \masm32\lib\kernel32.lib
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
IDM_MAINMENU equ 10000 IDM_ICON equ LVS_ICON IDM_SMALLICON equ LVS_SMALLICON IDM_LIST equ LVS_LIST IDM_REPORT equ LVS_REPORT
RGB macro red,green,blue xor eax,eax mov ah,blue shl eax,8 mov ah,green mov al,red endm
.data ClassName db "ListViewWinClass",0 AppName db "Testing a ListView Control",0 ListViewClassName db "SysListView32",0 Heading1 db "Filename",0 Heading2 db "Size",0 FileNamePattern db "*.*",0 FileNameSortOrder dd 0 SizeSortOrder dd 0 template db "%lu",0


.data? hInstance HINSTANCE ? hList dd ? hMenu dd ?
. code start: invoke GetModuleHandle, NULL mov hInstance,eax invoke WinMain, hInstance,NULL, NULL, SW_SHOWDEFAULT invoke ExitProcess,eax invoke InitCommonControls WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD LOCAL wc:WNDCLASSEX LOCAL msg:MSG LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX mov wc.style, NULL mov wc.lpfnWndProc, OFFSET WndProc mov wc.cbClsExtra,NULL mov wc.cbWndExtra,NULL push hInstance pop wc.hInstance mov wc.hbrBackground,COLOR_WINDOW+1 mov wc.lpszMenuName,IDM_MAINMENU mov wc.lpszClassName,OFFSET ClassName invoke LoadIcon,NULL,IDI_APPLICATION mov wc.hIcon,eax mov wc.hIconSm,eax invoke LoadCursor,NULL,IDC_ARROW mov wc.hCursor,eax invoke RegisterClassEx, addr wc invoke CreateWindowEx,NULL,ADDR ClassName,ADDR AppName, WS_OVERLAPPEDWINDOW, \ CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInst, NULL mov hwnd,eax invoke ShowWindow, hwnd,SW_SHOWNORMAL invoke UpdateWindow, hwnd .while TRUE invoke GetMessage, ADDR msg,NULL,0,0 .break .if (!eax) invoke TranslateMessage, ADDR msg invoke DispatchMessage, ADDR msg .endw mov eax,msg.wParam ret WinMain endp
InsertColumn proc LOCAL lvc:LV_COLUMN
mov lvc.imask,LVCF_TEXT+LVCF_WIDTH mov lvc.pszText,offset Heading1 mov lvc.lx,150 invoke SendMessage,hList, LVM_INSERTCOLUMN, 0, addr lvc or lvc.imask,LVCF_FMT mov lvc.fmt,LVCFMT_RIGHT mov lvc.pszText,offset Heading2 mov lvc.lx,100 invoke SendMessage,hList, LVM_INSERTCOLUMN, 1 ,addr lvc ret InsertColumn endp
ShowFileInfo proc uses edi row:DWORD, lpFind:DWORD LOCAL lvi:LV_ITEM LOCAL buffer[20]:BYTE mov edi,lpFind assume edi:ptr WIN32_FIND_DATA mov lvi.imask,LVIF_TEXT+LVIF_PARAM push row pop lvi.iItem mov lvi.iSubItem,0 lea eax,[edi].cFileName mov lvi.pszText,eax push row pop lvi.lParam invoke SendMessage,hList, LVM_INSERTITEM,0, addr lvi mov lvi.imask,LVIF_TEXT inc lvi.iSubItem invoke wsprintf,addr buffer, addr template,[edi].nFileSizeLow lea eax,buffer mov lvi.pszText,eax invoke SendMessage,hList,LVM_SETITEM, 0,addr lvi assume edi:nothing ret ShowFileInfo endp


FillFileInfo proc uses edi LOCAL finddata:WIN32_FIND_DATA LOCAL FHandle:DWORD
invoke FindFirstFile,addr FileNamePattern,addr finddata .if eax!=INVALID_HANDLE_VALUE mov FHandle,eax xor edi,edi .while eax!=0 test finddata.dwFileAttributes,FILE_ATTRIBUTE_DIRECTORY .if ZERO? invoke ShowFileInfo,edi, addr finddata inc edi .endif invoke FindNextFile,FHandle,addr finddata .endw invoke FindClose,FHandle .endif ret FillFileInfo endp
String2Dword proc uses ecx edi edx esi String:DWORD LOCAL Result:DWORD
mov Result,0 mov edi,String invoke lstrlen,String .while eax!=0 xor edx,edx mov dl,byte ptr [edi] sub dl,"0" mov esi,eax dec esi push eax mov eax,edx push ebx mov ebx,10 .while esi > 0 mul ebx dec esi .endw pop ebx add Result,eax pop eax inc edi dec eax .endw mov eax,Result ret String2Dword endp
CompareFunc proc uses edi lParam1:DWORD, lParam2:DWORD, SortType:DWORD LOCAL buffer[256]:BYTE LOCAL buffer1[256]:BYTE LOCAL lvi:LV_ITEM
mov lvi.imask,LVIF_TEXT lea eax,buffer mov lvi.pszText,eax mov lvi.cchTextMax,256 .if SortType==1 mov lvi.iSubItem,1 invoke SendMessage,hList,LVM_GETITEMTEXT,lParam1,addr lvi invoke String2Dword,addr buffer mov edi,eax invoke SendMessage,hList,LVM_GETITEMTEXT,lParam2,addr lvi invoke String2Dword,addr buffer sub edi,eax mov eax,edi .elseif SortType==2 mov lvi.iSubItem,1 invoke SendMessage,hList,LVM_GETITEMTEXT,lParam1,addr lvi invoke String2Dword,addr buffer mov edi,eax invoke SendMessage,hList,LVM_GETITEMTEXT,lParam2,addr lvi invoke String2Dword,addr buffer sub eax,edi .elseif SortType==3 mov lvi.iSubItem,0 invoke SendMessage,hList,LVM_GETITEMTEXT,lParam1,addr lvi invoke lstrcpy,addr buffer1,addr buffer invoke SendMessage,hList,LVM_GETITEMTEXT,lParam2,addr lvi invoke lstrcmpi,addr buffer1,addr buffer .else mov lvi.iSubItem,0 invoke SendMessage,hList,LVM_GETITEMTEXT,lParam1,addr lvi invoke lstrcpy,addr buffer1,addr buffer invoke SendMessage,hList,LVM_GETITEMTEXT,lParam2,addr lvi invoke lstrcmpi,addr buffer,addr buffer1 .endif ret CompareFunc endp


UpdatelParam proc uses edi LOCAL lvi:LV_ITEM
invoke SendMessage,hList, LVM_GETITEMCOUNT,0,0 mov edi,eax mov lvi.imask,LVIF_PARAM mov lvi.iSubItem,0 mov lvi.iItem,0 .while edi>0 push lvi.iItem pop lvi.lParam invoke SendMessage,hList, LVM_SETITEM,0,addr lvi inc lvi.iItem dec edi .endw ret UpdatelParam endp
ShowCurrentFocus proc LOCAL lvi:LV_ITEM LOCAL buffer[256]:BYTE
invoke SendMessage,hList,LVM_GETNEXTITEM,-1, LVNI_FOCUSED mov lvi.iItem,eax mov lvi.iSubItem,0 mov lvi.imask,LVIF_TEXT lea eax,buffer mov lvi.pszText,eax mov lvi.cchTextMax,256 invoke SendMessage,hList,LVM_GETITEM,0,addr lvi invoke MessageBox,0, addr buffer,addr AppName,MB_OK ret ShowCurrentFocus endp
WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM .if uMsg==WM_CREATE invoke CreateWindowEx, NULL, addr ListViewClassName, NULL, \ LVS_REPORT+WS_CHILD+WS_VISIBLE, 0,0,0,0,hWnd, NULL, hInstance, NULL mov hList, eax invoke InsertColumn invoke FillFileInfo RGB 255,255,255 invoke SendMessage,hList,LVM_SETTEXTCOLOR,0,eax RGB 0,0,0 invoke SendMessage,hList,LVM_SETBKCOLOR,0,eax RGB 0,0,0 invoke SendMessage,hList,LVM_SETTEXTBKCOLOR,0,eax invoke GetMenu,hWnd mov hMenu,eax invoke CheckMenuRadioItem,hMenu,IDM_ICON,IDM_LIST, IDM_REPORT,MF_CHECKED .elseif uMsg==WM_COMMAND .if lParam==0 invoke GetWindowLong,hList,GWL_STYLE and eax,not LVS_TYPEMASK mov edx,wParam and edx,0FFFFh push edx or eax,edx invoke SetWindowLong,hList,GWL_STYLE,eax pop edx invoke CheckMenuRadioItem,hMenu,IDM_ICON,IDM_LIST, edx,MF_CHECKED .endif .elseif uMsg==WM_NOTIFY push edi mov edi,lParam assume edi:ptr NMHDR mov eax,[edi].hwndFrom .if eax==hList .if [edi].code==LVN_COLUMNCLICK assume edi:ptr NM_LISTVIEW .if [edi].iSubItem==1 .if SizeSortOrder==0 || SizeSortOrder==2 invoke SendMessage,hList,LVM_SORTITEMS,1,addr CompareFunc invoke UpdatelParam mov SizeSortOrder,1 .else invoke SendMessage,hList,LVM_SORTITEMS,2,addr CompareFunc invoke UpdatelParam mov SizeSortOrder,2 .endif .else .if FileNameSortOrder==0 || FileNameSortOrder==4 invoke SendMessage,hList,LVM_SORTITEMS,3,addr CompareFunc invoke UpdatelParam mov FileNameSortOrder,3 .else invoke SendMessage,hList,LVM_SORTITEMS,4,addr CompareFunc invoke UpdatelParam mov FileNameSortOrder,4 .endif .endif assume edi:ptr NMHDR .elseif [edi].code==NM_DBLCLK invoke ShowCurrentFocus .endif .endif pop edi .elseif uMsg==WM_SIZE mov eax,lParam mov edx,eax and eax,0ffffh shr edx,16 invoke MoveWindow,hList, 0, 0, eax,edx,TRUE


.elseif uMsg==WM_DESTROY invoke PostQuitMessage,NULL .else invoke DefWindowProc,hWnd,uMsg,wParam,lParam ret .endif xor eax, eax ret WndProc endp end start
АНАЛИЗ
Первое, что должна сделать программа после того, как создано основное окно - это создать listview.
.if uMsg==WM_CREATE invoke CreateWindowEx, NULL, addr ListViewClassName, NULL, \ LVS_REPORT+WS_CHILD+WS_VISIBLE, 0,0,0,0,hWnd, NULL, hInstance, NULL mov hList, eax
Мы вызываем CreateWindowEx, передавая ей имя класса окна "SysListView32". Режим отображения по умолчанию задан стилем LVS_REPORT.
invoke InsertColumn
После того, как создан listview, мы вставляем в него колонку.
LOCAL lvc:LV_COLUMN
mov lvc.imask,LVCF_TEXT+LVCF_WIDTH mov lvc.pszText,offset Heading1 mov lvc.lx,150 invoke SendMessage,hList, LVM_INSERTCOLUMN, 0, addr lvc
Мы указываем название и ширину первой колонки, в которой будут отображаться имена файлов, в структуре LV_COLUMN, поэтому нам нужно установить в imask флаги LVCF_TEXT и LVCF_WIDTH. Мы заполняем рszText адресом названия и lx - шириной колонки в пикселях. Когда все сделано, мы посылаем сообщение LVM_INSERTCOLUMN listview, передавая ей структуру.
or lvc.imask,LVCF_FMT mov lvc.fmt,LVCFMT_RIGHT
После вставления первой колонки, мы вставляем следующую, в которой будут отображаться размеры файлов. Так как нам нужно, чтобы размеры файлов выравнивались по правой стороне, нам необходимо указать флаг в параметре fmt, LVCFMT_RIGHT. Мы также указываем флаг LVCF_FMT в imask, в добавление к LVCF_TEXT и LVCF_WIDTH.
mov lvc.pszText,offset Heading2 mov lvc.lx,100 invoke SendMessage,hList, LVM_INSERTCOLUMN, 1 ,addr lvc
Оставшийся код прост. Помещаем адреса названия в рszText и ширину в lx. Затем посылаем сообщение LVM_INSERTCOLUMN listview, указывая номер колонки и адрес структуры.
Когда колонки вставлены, мы можем заполнить listview элементами.
invoke FillFileInfo
В FillFileInfo содержится следующий код.
FillFileInfo proc uses edi LOCAL finddata:WIN32_FIND_DATA LOCAL FHandle:DWORD


invoke FindFirstFile,addr FileNamePattern,addr finddata
Мы вызываем FindFirstFile, чтобы получить информацию о первом файле, который отвечает заданным условиям. У FindFirstFile следующий прототип:
FindFirstFile proto pFileName:DWORD, pWin32_Find_Data:DWORD
рFileName - это адрес имени файла, который надо искать. Эта строка может содержать "дикие" символы. В нашем примере мы используем *.*, чтобы искать все файлы в данной папке.
рWin32_Find_Data - это адрес структуры WIN32_FIND_DATA, которая будет заполнена информацией о файле (если что-нибудь будет найдено).
Эта функция возвращает INVALID_HANDLE_VALUE в eax, если не было найдено соответствующих заданным критериям файлов. Иначе она возвратит хэндл поиска, который будет использован в последующих вызовах FindNextFile.
.if eax!=INVALID_HANDLE_VALUE mov FHandle,eax xor edi,edi
Если файл будет найден, мы сохраним хэндл поиска в переменную, а потом обнулим edi, который будет использован в качестве индекса элемента (номер ряда).
.while eax!=0 test finddata.dwFileAttributes,FILE_ATTRIBUTE_DIRECTORY .if ZERO?
В этом туториале я не хочу иметь дело с папками, поэтому отфильтровываю их проверяя параметр dwFileAttributes на предмет наличия установленного флага FILE_ATTRIBUTE_DIRECTORY. Если он есть, я сразу перехожу к вызову FindNextFile.
invoke ShowFileInfo,edi, addr finddata inc edi .endif invoke FindNextFile,FHandle,addr finddata .endw
Мы вставляем имя и pазмеp файла в listview вызывая функцию ShowFileInfo. Затем мы повышаем значение edi (текущий номер столбца). И, наконец, мы делаем вызов FindNextFile, чтобы найти следующий файл в нашей папке, пока FindNextFile не возвратит 0, что означает то, что больше файлов найдено не было.
invoke FindClose,FHandle .endif ret FillFileInfo endp
Когда все файлы в ткущей папке найдены, мы должны закрыть хэндл поиска.
Теперь давайте взглянем на функцию ShowFileInfo. Эта функция принимает два параметра, индекс элемента (номер ряда) и адрес структуры WIN32_FIND_DATA.


ShowFileInfo proc uses edi row:DWORD, lpFind:DWORD LOCAL lvi:LV_ITEM LOCAL buffer[20]:BYTE mov edi,lpFind assume edi:ptr WIN32_FIND_DATA
Сохраняем адрес структуры WIN32_FIND_DATA в edi.
mov lvi.imask,LVIF_TEXT+LVIF_PARAM push row pop lvi.iItem mov lvi.iSubItem,0
Мы предоставляем название элемента и значение lParam, поэтому мы помещаем флаги LVIF_TEXT и LVIF_PARAM в imask. Затем мы устанавливаем приравниваем iItem номер ряда, переданный функции и, так как это главный элемент, мы должны приравнять iSubItem нулю (колонка 0).
lea eax,[edi].cFileName mov lvi.pszText,eax push row pop lvi.lParam
Затем мы помещаем адрес названия, в данном случая это имя файла в структуре WIN32_FIND_DATA, в рszText. Так как мы реализуем свою сортировку, мы должны заполнить lParam определенным значением. Я решил помещать номер ряда в это параметр, чтобы я мог получать информацию об элементе по его индексу.
invoke SendMessage,hList, LVM_INSERTITEM,0, addr lvi
Когда все необходимые поля в LV_ITEM заполнены, мы посылаем сообщение LVM_INSERTITEM listview, чтобы вставить в него элемент.
mov lvi.imask,LVIF_TEXT inc lvi.iSubItem invoke wsprintf,addr buffer, addr template,[edi].nFileSizeLow lea eax,buffer mov lvi.pszText,eax
Мы установим подэлементы, ассоциированные с элементом. Подэлемент может иметь только название. Поэтому мы указываем в imask LVIF_TEXT. Затем мы указываем в iSubItem колонку, в которой должен находиться подэлемент. В этом случае мы устанавливаем его в 1. Названием этого элемента будет являться размер файла. Тем не менее, мы сначала должны сконвертировать его в строку, вызвать wsрrintf. Затем мы помещаем адрес строки в рszText.
invoke SendMessage,hList,LVM_SETITEM, 0,addr lvi assume edi:nothing ret ShowFileInfo endp
Когда все требуемые поля в LV_ITEM заполнены, мы посылаем сообщение LVM_SETITEM listview, передавая ему адрес структуры LV_ITEM. Заметьте, что мы используем LVM_SETITEM, а не LVM_INSERTITEM, потому что подэлемент считается свойством элемента. Поэтому устанавливаем свойство элемента, а не вставляем новый элемент.


Когда все элементы вставлены в listview, мы устанавливаем текст и цвет бэкграунда контрола listview.
RGB 255,255,255 invoke SendMessage,hList,LVM_SETTEXTCOLOR,0,eax RGB 0,0,0 invoke SendMessage,hList,LVM_SETBKCOLOR,0,eax RGB 0,0,0 invoke SendMessage,hList,LVM_SETTEXTBKCOLOR,0,eax
Я использую макро RGB, чтобы конвертировать значения red, green, blue в eax и использую его для того, что указать нужное нам значение. Мы устанавливаем цвет текста и цвет фона с помощью сообщений LVM_SETTEXTCOLOR и LVM_SETTEXTBKCOLOR. Мы устанавливаем цвет фона listview сообщением LVM_SETBKCOLOR.
invoke GetMenu,hWnd mov hMenu,eax invoke CheckMenuRadioItem,hMenu,IDM_ICON,IDM_LIST, IDM_REPORT,MF_CHECKED
Мы позволим пользователю выбирать режимы отображения через меню. Поэтому мы должны получить сначала хэндл меню. Чтобы помочь юзеру переключать режимы отображения, мы помещаем в меню систему radio button'ов. Для этого нам понадобится функция CheckMenuRadioItem. Эта функция поместит radio button перед пунктом меню.
Заметьте, что мы создаем окно listview с шириной и высотой равной нулю. Оно будет менять pазмеp каждый pаз, когда будет менять pазмеp родительское окно. В этом случае мы можем быть уверены, что размер listview всегда будет соответствовать родительскому окну. В нашем примере нам требуется, чтобы listview занимал всю клиентскую область родительского окна.
.elseif uMsg==WM_SIZE mov eax,lParam mov edx,eax and eax,0ffffh shr edx,16 invoke MoveWindow,hList, 0, 0, eax,edx,TRUE
Когда родительское окно получает сообщение WM_SIZE, нижнее слово lParam содержит новую ширину клиентской области и верхнее словно новой высоты. Тогда мы вызываем MoveWindow, чтобы изменить pазмеp listview, чтобы тот покрывал всю клиентскую область родительского окна.
Когда пользователь выберет режим отображения в меню, мы должны соответственно отреагировать. Мы устанавливаем новый стиль контрола listview функцией SetWindowLong.
.elseif uMsg==WM_COMMAND .if lParam==0 invoke GetWindowLong,hList,GWL_STYLE and eax,not LVS_TYPEMASK


Сначала мы получаем текущие стили listview. Затем мы стираем старый стиль отображения. LVS_TYPEMASK - это комбинированное значение всех четырех стилей отображения. Поэтому когда мы выполняем логическое умножение текущих флагов стилей со значением "not LVS_TYPEMASK", стиль текущего отображения стирается.
Во время проектирования меню я немного сжульничал. Я использовал в качестве ID пунктов меню константы стилей отображения.
IDM_ICON equ LVS_ICON IDM_SMALLICON equ LVS_SMALLICON IDM_LIST equ LVS_LIST IDM_REPORT equ LVS_REPORT
Поэтому, когда родительское окно получает сообщение WM_COMMAND, нужный стиль отображения находится в нижнем слове wParam'а (как ID пункта меню).
mov edx,wParam and edx,0FFFFh
Мы получили стиль отображения в нижнем слове wParam. Все, что нам теперь нужно, это обнулить верхнее слово.
push edx or eax,edx
И добавить стиль отображения к уже существующим стилям (текущий стиль отображения мы ранее оттуда убрали).
invoke SetWindowLong,hList,GWL_STYLE,eax
И установить новые стили функцией SetWindowLong.
pop edx invoke CheckMenuRadioItem,hMenu,IDM_ICON,IDM_LIST, edx,MF_CHECKED
.endif
Hам также требуется поместить radio button перед выбранным пунктом меню. Поэтому мы вызываем CheckMenuRadioItem, передавая ей текущий стиль отображения (а также ID пункта меню).
Когда пользователь кликает по заголовку колонки в pежиме отчета, нам нужно отсортировать элементы в listview. Мы должны отреагировать на сообщение WM_NOTIFY.
.elseif uMsg==WM_NOTIFY push edi mov edi,lParam assume edi:ptr NMHDR mov eax,[edi].hwndFrom .if eax==hList
Когда мы получаем сообщение WM_NOTIFY, lParam содержит указатель на структуру NMHDR. Мы можем проверить, пришло ли это сообщение от listview, сравнив параметр hwndFrom структуры NMHDR с хэндлом контрола listview. Если они совпадают, мы можем заключить, что уведомление пришло от listview.
.if [edi].code==LVN_COLUMNCLICK assume edi:ptr NM_LISTVIEW
Если уведомление пришло от listview, мы проверяем, равен ли код LVN_COLUMNCLICK. Если это так, это означает, что пользователь кликает на заголовке колонки. В случае, что код pавен LVN_COLUMNCLICK, мы считаем, что lParam содержит указатель на структуру NM_LISTVIEW, которая является супермножеством по отношению к структуре NMHDR (т.е. включает ее). Затем нам нужно узнать, по какому заголовку колонки кликнул пользователь. Эту информацию мы получаем из параметра iSubItem. Его значение можно считать номером колонки (отсчет начинается с нуля).


.if [edi].iSubItem==1 .if SizeSortOrder==0 || SizeSortOrder==2
Если iSubItem равен 1, это означает, что пользователь кликнул по второй колонке. Мы используем глобальные переменные, чтобы сохранять текущий статус порядка сортировки. 0 означает "еще не отсортировано", 1 значит "восходящая сортировка", а 2 - "нисходящая сортировка". Если элементы/подэлементы в колонке ранее не были отсортированы или отсортированы по нисходящей, то мы устанавливаем сортировку по восходящей.
invoke SendMessage,hList,LVM_SORTITEMS,1,addr CompareFunc
Мы посылаем сообщение LVM_SORTITEMS listview, передавая 1 через wParam и адрес нашей сравнивающей функции через lParam. Заметьте, что значение в wParam задается пользователем, вы можете использовать его как хотите. Я использовал его в нашем примере как метод сортировки. Сначала мы взглянем на сравнивающую функцию.
CompareFunc proc uses edi lParam1:DWORD, lParam2:DWORD, SortType:DWORD LOCAL buffer[256]:BYTE LOCAL buffer1[256]:BYTE LOCAL lvi:LV_ITEM
mov lvi.imask,LVIF_TEXT lea eax,buffer mov lvi.pszText,eax mov lvi.cchTextMax,256
В сравнивающей функции контрол listview будет передавать lParam'ы (через LV_ITEM) двух элементов, которые нужно сравнить, через lParam1 и lParam2. Вспомните, что мы помещаем индекс элемента в lParam. Таким образом мы можем получить информацию об элементах, используя эти индексы. Информация, которая нам нужна - это названия сортирующихся элементов/подэлементов. Мы подготавливаем структуру LV_ITEM для этого, указывая в imask LVIF_TEXT и адрес буфера в рszText и размер буфера в cchTextMax.
.if SortType==1 mov lvi.iSubItem,1 invoke SendMessage,hList,LVM_GETITEMTEXT,lParam1,addr lvi
Если значение SortType pавно 1 или 2, мы знаем, что кликнута колонка размера файла. 1 означает, что необходимо отсортировать элементы в нисходящем порядке. 2 значит обратное. Таким образом мы указываем iSubItem равным 1 (чтобы задать колонку размера) и посылаем сообщение LVM_GETITEMTEXT контролу listview, чтобы получить название (строку с размером файла) подэлемента.


invoke String2Dword,addr buffer mov edi,eax
Конвертируем строку в двойное слово с помощью функции String2Dword, написанную мной. Она возвращает dword-значение в eax. Мы сохраняем ее в edi для последующего сравнения.
invoke SendMessage,hList,LVM_GETITEMTEXT,lParam2,addr lvi invoke String2Dword,addr buffer sub edi,eax mov eax,edi
Тоже самое мы делаем и с lParam2. После получения размеров обоих файлов, мы можем сравнить их.
Правила, которых придерживается функция сравнения, следующие:
Если первый элемент должен предшествовать другому, вы должны возвратить отрицательное значение через eax.
Если второй элемента должен предшествовать первому, вы должны возвратить через eax положительное значение.
Если оба элемента равны, вы должны возвратить ноль.
В нашем случае нам нужно отсортировать элементы согласно их размерам в восходящем порядке. Поэтому мы просто можем вычесть размер первого элемента из второго и возвратить результат в eax.
.elseif SortType==3 mov lvi.iSubItem,0 invoke SendMessage,hList,LVM_GETITEMTEXT,lParam1,addr lvi invoke lstrcpy,addr buffer1,addr buffer invoke SendMessage,hList,LVM_GETITEMTEXT,lParam2,addr lvi invoke lstrcmpi,addr buffer1,addr buffer
В случае, если пользователь кликнет по колонке с именем файла, мы должны сравнивать имена файлов. Мы должны получить имена файлов, а затем сравнить их с помощью функции lstrcmрi. Мы можем возвратить значение, возвращаемое этой функцией, так как оно использует те же правила сравния.
После того, как элементы отсортированы, нам нужно обновить значения lParam'ов для всех элементов, чтобы учесть изменившиеся индексы элементов, поэтому мы вызываем функцию UpdatelParam.
invoke UpdatelParam mov SizeSortOrder,1
Эта функция просто-напросто перечисляет все элементы в listview и обновляет значения lParam. Hам требуется это делать, иначе следующая сортировка не будет работать как ожидается, потому что мы исходим из того, что значение lParam - это индекс элемента.
.elseif [edi].code==NM_DBLCLK invoke ShowCurrentFocus .endif


Когда пользователь делает двойной клик на элементе, нам нужно отобразить окно с сообщение с названием элемента. Мы должны проверить, равно ли поле code в NMHDR NM_DBLCLK. Если это так, мы можем перейти к получению названия и отображению его в окне с сообщением.
ShowCurrentFocus proc LOCAL lvi:LV_ITEM LOCAL buffer[256]:BYTE
invoke SendMessage,hList,LVM_GETNEXTITEM,-1, LVNI_FOCUSED
Как мы может узнать, по какому элементу кликнули два раза? Когда элемент кликнут (одинарным или двойным нажатием), он получает фокус. Даже если выбрано несколько элементов, фокус будет только у одного. Наши задача заключается в том, чтобы найти элемент у которого находится фокус. Мы делаем это, посылая сообщение LVM_GETNEXTITEM контролу listview, указав желаемое состояние элемента в lParam. -1 в wParam означает поиск по всем элементарм. Индекс элемента возвращается в eax.
mov lvi.iItem,eax mov lvi.iSubItem,0 mov lvi.imask,LVIF_TEXT lea eax,buffer mov lvi.pszText,eax mov lvi.cchTextMax,256 invoke SendMessage,hList,LVM_GETITEM,0,addr lvi
Затем мы получаем название элемента с помощью сообщения LVM_GETITEM.
invoke MessageBox,0, addr buffer,addr AppName,MB_OK
И наконец, мы отображаем название элемента в окне сообщения.
Если вы хотите узнать, как использовать в контроле listview иконки, вы можете прочитать об этом в моем туториале о treeview. В случае с listview надо будет сделать примерно то же самое.
[C] Iczelion, пер. Aquila.