Ассемблер под Windows №27

Сабклассинг, суперклассинг и работа с памятью


Доброго времени суток, уважаемые подписчики. Сегодня мы немного отступим от нашего проекта и вернёмся к главной теме рассылки, к программированию под Windows на ассемблере. К тому же это наверняка пригодиться нам в нашем проекте.


Думаю, многие из вас уже сталкивались с тем, что иногда контролы ведут не так, как вам бы этого хотелось. Причём проблема не в контроле (он работает без ошибок), а в том, что контрол всегда работает одинаково, а вам нужно что-то особенное. Именно для таких случаев существует сабклассинг (subclassing).
Создавая окно Windows создаёт структуру окна на основе заданного класса и, в последующих операциях с окном, использует эту структуру. Информацию из этой структуры можно получить, используя функцию GetWindowLong. Функции передаётся два параметра, handle окна и индекс получаемого значения в структуре окна. Причём нулевой индекс указывает на первый байт дополнительной информации окна (она указывается при создании класса, параметр cbWndExtra структуры WNDCLASSEX), а основная информация окна имеет негативные индексы, вот они:
имя индексаиндексВозвращаемое значение
GWL_WNDPROC-4указатель на процедуру окна
GWL_HINSTANCE-6handle программы
GWL_HWNDPARENT-8handle окна-родителя
GWL_ID-12индекс окна
GWL_STYLE-16стиль окна
GWL_EXSTYLE-20расширенный стиль окна
GWL_USERDATA-21информация пользователя
Помимо того, что эта информация может быть прочитана, она также может быть изменена так, как это нужно программисту. Для этого служит функция SetWindowLong. Это даёт нам возможность перехватывать сообщения, посылаемые окну. Теперь нам просто нужно создать процедуру, которая служила бы фильтром к сообщениям окна, и выставить её как процедуру окна при помощи функции SetWindowLong. Главное, не забыть, после фильтрации сообщений, передавать их старой процедуре обработки. Сделать это можно функцией CallWindowProc. Рассмотрим это на небольшом примере:
subclass.rc:
#define WS_VISIBLE    0x10000000L
#define WS_CAPTION    0x00C00000L
#define WS_CHILD      0x40000000L
#define WS_OVERLAPPED 0x00000000L
#define WS_TABSTOP    0x00010000L
#define WS_BORDER     0x00800000L
#define WS_SYSMENU    0x00080000L
#define DIALOG_1      1

1 DIALOG 159, 122, 161, 127
STYLE WS_OVERLAPPED | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
CAPTION "subclass"
FONT 8, "MS Sans Serif"
{
  EDITTEXT 100, 15, 24, 60, 42
}
subclass.asm:
include macros.inc
include user32.inc
include kernel32.inc
include def32.inc
  .386
  .model flat
  .data?
OldWndProc dd ?
  .code
_start:
  null ebx
  run GetModuleHandle, ebx
  run DialogBoxParam, eax, 1, ebx, offset WndProc, ebx
  run ExitProcess, ebx

WndProc proc
  push ebp
  mov ebp,esp
  pusha
  mov edi,[ebp+08h]
  null ebx
  mov eax,[ebp+0Ch]
  cmp eax,WM_INITDIALOG
  je init_proc
  cmp eax,WM_CLOSE
  je close_proc
  popa
  pop ebp
  null eax
  ret 16
init_proc:
  run GetDlgItem, edi, 100
  run SetWindowLong, eax, GWL_WNDPROC, offset NewWndProc
  mov OldWndProc, eax
EndWndProc:
  popa
  pop ebp
  null eax
  ret 16
close_proc:
  run EndDialog, edi, ebx
  jmp short EndWndProc
WndProc endp

NewWndProc proc
  push ebp
  mov ebp,esp
  mov eax,[ebp+0Ch]
  cmp eax,WM_CHAR
  jne Pass_message
  cmp dword ptr [ebp+10h],'s'
  jne Pass_message
  mov dword ptr [ebp+10h],'$'
Pass_message:
  run CallWindowProc, OldWndProc, dword ptr [ebp+08h], eax, dword ptr [ebp+10h], dword ptr [ebp+14h]
  pop ebp
  ret 16
NewWndProc endp
  end _start
Здесь мы создаём обычный диалог, с одним edit box-ом. При инициализации мы получаем handle edit box-а и изменяем его процедуру обработки сообщений на нашу. SetWindowLong возвращает предидущее значение устанавливаемой переменной, поэтому возвращённое значение ни что иноё, как адрес старой процедуры edit box-а. Особое внимание хочу обратить на процедуру NewWndProc. Она проверяет, не WM_CHAR ли сообщение, пришедшее edit box-у, и если это оно и нажатый символ равен "s", то она заменяет символ на "$". Преимущество сабклассинга в том, что настоящая процедура даже не знает, что была нажата "s", для неё такая клавиша никогда и не была нажата.
Этот способ достаточно хорош, но если нужно создать десятки или даже сотни контролов, то сабклассинг может стать достаточно не эффективным решением. Проблема в том, что сабклассинг окна можно произвести лишь после его создания мы вынуждены будем вызывать одни и те же функции десятки или сотни раз, создавая нужные нам контролы. То есть для избежания проблем нам надо заранее изменить процедуру окна контрола. Можно конечно изменить процедуру окна класса функциями SetClassLong и GetClassLong, но тогда мы теряем возможность использовать обычный контрол и решаем этой возможности все работающие приложения разом. Выходом из положения является так называемый суперклассинг (superclassing). Суперклассинг подразумевает собой создание класса, отличающегося от уже существующего лишь заранее установленной процедурой фильтра сообщений (как NewWndProc). Для этого мы должны получить информацию об уже существующем классе, используем функцию GetClassInfo, передав ей три параметра, handle приложения, создавшего класс, имя класса и адрес структуры WNDCLASSEX для заполнения. Далее мы изменяем процедуру обработки сообщений класса, не забыв предварительно сохранить адрес старой, и имя класса. Регистрируем его и используем.
Для получения результата нам нужно лишь немного изменить программу subclass:
superclass.asm:
#define WS_VISIBLE    0x10000000L
#define WS_CAPTION    0x00C00000L
#define WS_CHILD      0x40000000L
#define WS_OVERLAPPED 0x00000000L
#define WS_TABSTOP    0x00010000L
#define WS_BORDER     0x00800000L
#define WS_SYSMENU    0x00080000L
#define DIALOG_1      1

1 DIALOG 159, 122, 161, 127
STYLE WS_OVERLAPPED | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
CAPTION "superclass"
FONT 8, "MS Sans Serif"
{
  CONTROL "", IDC_1, "EditX", 0 | WS_CHILD | WS_VISIBLE, 11, 11, 142, 14
  CONTROL "", IDC_2, "EditX", 0 | WS_CHILD | WS_VISIBLE, 11, 33, 142, 16
}
superclass.asm:
include macros.inc
include user32.inc
include kernel32.inc
include def32.inc
  .386
  .model flat
  .data?
OldWndProc dd ?
wcex       WNDCLASSEX <?>
ClassName  db 8 dup(?)
  .code
_start:
  null ebx
  run SuperClassIt
  run GetModuleHandle, ebx
  run DialogBoxParam, eax, 1, ebx, offset WndProc, ebx
  run ExitProcess, ebx

WndProc proc
  push ebp
  mov ebp,esp
  pusha
  mov edi,[ebp+08h]
  null ebx
  mov eax,[ebp+0Ch]
  cmp eax,WM_INITDIALOG
  je init_proc
  cmp eax,WM_CLOSE
  je close_proc
  popa
  pop ebp
  null eax
  ret 16
init_proc:
EndWndProc:
  popa
  pop ebp
  null eax
  ret 16
close_proc:
  run EndDialog, edi, ebx
  jmp short EndWndProc
WndProc endp

SuperClassIt proc
  pusha
  mov wcex.cbSize,48
  mov dword ptr [ClassName],'tidE'
  run GetClassInfoEx, ebx, offset ClassName, offset wcex
  mov eax,wcex.lpfnWndProc
  mov OldWndProc,eax
  mov byte ptr [ClassName+4],'X'
  mov wcex.lpfnWndProc,offset NewWndProc
  mov wcex.lpszClassName, offset ClassName
  run RegisterClassEx, offset wcex
  popa
  ret
SuperClassIt endp

NewWndProc proc
  push ebp
  mov ebp,esp
  mov eax,[ebp+0Ch]
  cmp eax,WM_CHAR
  jne Pass_message
  cmp dword ptr [ebp+10h],'s'
  jne Pass_message
  mov dword ptr [ebp+10h],'$'
Pass_message:
  run CallWindowProc, OldWndProc, dword ptr [ebp+08h], eax, dword ptr [ebp+10h], dword ptr [ebp+14h]
  pop ebp
  ret 16
NewWndProc endp
  end _start
Как вы видите изменений достаточно мало. Просто в ресурсах мы изменяем класс edit на наш editX, а потом в программе создаём его используя суперклассинг edit.


Ещё один вопрос, который я хотел бы разобрать, это работа с памятью в среде Windows. Эта тема уже была поднята в уроке 10, но способ выделения большого глобального куска памяти не всегда оправдан. Очень часто нужно выделять больше кусков памяти с меньшим размером, а так как GlobalAlloc всегда выделяет достаточно большой кусок памяти (обычно 64 Кб), даже если реально требуется меньше, память становится очень фрагментированной. В случае, если работа происходит с огромным количеством мизерных строк, то оправдано создание менеджера памяти, но если достаточно 10-16 достаточно больших кусков, то почти идеальным выбором могут стать функции работы с так называемой кучей (heap)! У каждого процесса при создании уже имеется одна "куча", но она часто используется для нужд системы и её лучше не трогать, но мы можем создать новую кучу функцией HeapCreate, которой необходимо указать флаги создания, начальный размер и максимальный размер. Большим плюсом является создание "куч", которые могут рости (growable heap). Плюс заключается в том, что размер выделеных кусков ограничен лишь памятью вашего компьютера, а создать такую "кучу" можно указав максимальный размер равным нулю.
  null ebx
  run HeapCreate, ebx, 1024, ebx
  mov hHeap,eax
Например, этот фрагмент создаёт "кучу", размер которой равен 1Кб, но попытка выделить 30 Кб из "кучи" увенчается успехом, так как третий параметр, максимальный размер, равен нулю.
Следующем шагом будет резервирование памяти.
  run HeapAlloc, hHeap, HEAP_ZERO_MEMORY, 4096
  mov oBuffer,eax
Функция HeapAlloc возвращает указатель на зарезервированную память в случае успеха. Получает следующие параметры: handle "кучи", флаги резервирования и размер участка памяти. Освобождение памяти происходит следующим путём:
  run HeapFree, hHeap, ebx, oBuffer
Параметры следующие: handle "кучи", флаги и указатель на блок памяти. Функция HeapSize определяет точный размер куска памяти выделеного из "кучи" и получает те же параметры, что и HeapFree. После использования "куча" удаляется функцией HeapDestroy.
  run HeapDestroy, hHeap
Вот, вроде и все основные функции работы с "кучами...


Вы можете скачать следующие дополнения к выпуску:
0027asm - набранный вариант примеров, приведённых в выпуске
0027inc - файлы дополнения kernel32.inc, user32.inc и def32.inc
0027exe - скомпилированные примеры выпуска


На сегодня это всё. Если есть какие вопросы, пишите. Пишите на Dark_Lord@RusFAQ.ru или Dark_Lord@land.ru.

Сайт управляется системой uCoz