hItem ― это дескриптор окна просмотра деревьев. Каждый элемент имеет дескриптор, как и в случае с окнами. Если вы хотите сделать что-нибудь с элементом, вы должны выбрать его с помощью его дескриптора. pszText ― это указатель на строку, оканчивающуюся NULL, которая является названием окна просмотра деревьев. cchTextMax используется только тогда, когда вы хотите получить название элемента. Windows надо будет знать размер предоставленного вами буфера (pszText), поэтому этот элемент используется именно для этого. iImage и iSelectedImage содержат индекс из списка изображений, который содержит изображения, показывающиеся когда элемент выбран и не выбран. Если вспомните левую панель Windows Explorer'а, то изображения директорий задаются именно этими двумя параметрами. Чтобы вставить элемент в окно просмотра деревьев, вы должны заполнить, по крайней мере, hParent, hInsertAfter, а также вам следует заполнить imask и pszText. поле stateMask определяет, какое состояние элемента должно быть установлено или получено. Оно может принимать одно из значений или комбинацию значений из таблицы описанной в поле state поле state содержит информацию о состоянии элемента окна просмотра деревьев. Некоторые наиболее часто используемые значения Содержание поляназначениеTVIS_DISABLEDэлемент запрещенTVIS_DROPHILITEDэлемент выбран как объект операции перетаскивания (drag-and-drop)TVIS_EXPANDEDветвь, порожденная элементом, полностью развернута (применимо только к порождающим узлам)TVIS_EXPANDEDDONCEветвь, порожденная элементом, развернута на один уровень или более (применимо только к порождающим узлам)TVIS_FOCUSEDэлемент имеет фокус вводаTVIS_SELECTEDэлемент выбран Когда элемент вставляется в древовидную структуру, поле pszText должно содержать указатель на строку, которая будет отображаться рядом с этим элементом. Если информация об элементе записывается в структуру из окна просмотра деревьев, поле pszText должно содержать указатель на символьный массив, имеющий достаточно большой размер для того, чтобы записать в него текст. Поле cchTextMax будет содержать количество символов, записываемых в буфер pszText.Списки изображенийКаждая запись в окно просмотра деревьев может иметь две ассоциированных с ней пиктограммы. Первая пиктограмма выводится на экран, когда запись развернута, вторая ― когда запись свернута (например, пиктограммы, изображающие открытый и закрытый каталог). Кроме того, приложение может задать список изображений для обозначения состояний записи. В этом случае окно просмотра деревьев резервирует место слева от записи для размещения данного изображения. Для работы с изображением предназначены функции GetImageList и SetImageList Код (C): GetImageList ( UINT nImage ); Функция GetImageList возвращает указатель на список изображений, связанных с окно просмотра деревьев или 0 ― в случае неудачи. Параметр nImage и может принимать одно из следующих значений: TVSIL_NORMAL ― предписывает возвращать список обычных изображений, который содержит изображение для выбранных и невыбранных записей; TVSIL_STATE ― предписывает возвращать список изображений, который содержит изображение для определяемых пользователем состояний. Код (C): SetImageList ( CImageList* pImageList, int nImageListType ); Функция SetImageList устанавливает новый список изображений для просмотра дерева, тип списка задается параметром nImageListTypeДобавление изображений в окно просмотра деревьевЕсли вы хотите поместить изображение слева от названия элемента, вам следует создать список изображений и ассоциировать его с окном просмотра деревьев. Код (C): HIMAGELIST ImageList_Create( int cx,//ширина любого изображения в этом списке изображений в пикселях int cy,/* высота любого изображения в этом списке изображений в пикселях. Все изображения в списке изображений должно быть равны друг другу по размеру. Если вы укажете большой рисунок, Windows разрежет его на несколько кусков согласно значению в cx и cy. Поэтому вам следует тщательно подготовить необходимые изображения */ UINT flags,/* задает тип изображения: является ли оно цветным или монохромным и их глубину */ int cInitial, /* количество изображений, которое будет изначально содержать список изображений. Windows использует эту информацию для резервирования памяти для изображений*/ int cGrow /* количество изображений, на которое должен увеличиваться список изображений, когда системе необходимо изменить размер списка, чтобы выделить место для новых изображений. Этот параметр представляет количество новых изображений, которое может содержать список изображений, изменивший размер*/ ); Если вызов пройдет успешно, функция возвратит дескриптор на пустой список изображений. Список изображений ― это не окно! Это только хранилище изображений, которые будут использоваться другими окнами. После того, как список изображений создан, вы можете добавить изображения с помощью вызова ImageList_Add. Код (C): int ImageList_Add( _In_ HIMAGELIST himl, /* дескриптор списка изображений, в который вы хотите добавить изображения. Это значение возвращается ImageList_Create*/ _In_ HBITMAP hbmImage, /* дескриптор рисунка, который должен быть добавлен в список изображений. Обычно изображения задаются в ресурсах и вызываются с помощью LoadBitmap. Заметьте, что вам не надо указывать количество изображений, содержащихся в этом рисунке, потому что это вытекает из параметров cx и cy, переданных ImageList_Create */ _In_opt_ HBITMAP hbmMask /* дескриптор рисунка, в котором содержится маска. Если маска в списке изображений не используется, этот параметр игнорируется */ ); Если во время вызова произойдет какая-либо ошибка, будет возвращен -1. Обычно мы будем добавлять только два изображения в список изображений, который будет использоваться окном просмотра деревьев: одно для невыбранного элемента, а другое ― для выбранного. Когда список изображений готов, мы ассоциируем его с окном просмотра деревьев, посылая тому сообщение TVM_SETIMAGELIST: wParam ― тип списка изображений. Есть две возможности: TMSIL_NORMAL ― задает обычный список изображений, который содержит изображения выбранного и невыбранного элементов. TVSIL_STATE ― устанавливает список изображений, содержащий изображения элементов для состояний, определяемых пользователем. lParam ― дескриптор списка изображений. Функции для работы с Tree view в целомКогда ветка дочерних записей развернута, то они изображаются с некоторым сдвигом относительно родительской записи. Для определения и изменения сдвига дочерней записи относительно родительской предназначены функции: GetIndent() и SetIndent (UINT nIndent) GetIndent() ― возвращает сдвиг в пикселях дочерней записи относительно родительской. SetIndent (UINT nIndent) ― задает сдвиг в пикселях дочерней записи относительно родительской. Если задается значение меньшее, чем определяемое системой, то функция игнорируется. Изменение содержимого дереваФункции предназначенные для работы с конкретными записями, а также для добавления, изменения и удаления записей. GetCount ― возвращает число записей, вставленных в окно просмотра деревьев GetVisibleCount ― возвращает текущее количество видимых записей в окно просмотра деревьев DeleteAllItems ― удаление всех записей из окна просмотра деревьев InsertItem ― функция осуществляет вставку новых записей и определяет начальное положение записи при ее добавлении в окно просмотра деревьев. При вызове этой функции задаются дескриптор родительской записи и дескриптор записи, после которой должна быть вставлена новая запись. Второй дескриптор должен быть дескриптором дочерней записи или принимать одно из следующих значений: TVI_FIRST, TVI_LAST или TVI_SORT. Значения TVI_FIRST и TVI_LAST определяют, что запись должна быть помещена первой или последней в списке дочерних записей. Если задано значение TVI_SORT, то после вставки записи дочерние записи сортируются по алфавиту. Получение информации об окне просмотра деревьевВы можете получить информацию об окне просмотра деревьев, послав ей сообщение TVM_GETITEM: wParam=0 lParam=указатель на структуру TV_ITEM заполненную информацией Прежде, чем вы пошлете это сообщение, вы должны заполнить параметр imask флагами, которые укажут, какие из полей TV_ITEM должны быть заполнены Windows. А самое главное, вы должны заполнить hItem дескриптором элемента, о котором вы хотите получить информацию. И это порождает следующую проблему: где взять этот дескриптор? Надо ли вам сохранять все дескрипторы окон просмотра деревьев? Ответ достаточно прост: вам не надо этого делать. Вы можете послать сообщение TVM_GETNEXTITEM окну просмотра деревьев, чтобы получить дескриптор окна просмотра деревьев, который имеет указанные вами атрибуты. Например, вы можете получить дескриптор первого дочернего элемента, корневого элемента, выбранного элемента и так далее.
TVM_GETNEXTITEM: wParam=флаг lParam ― дескриптор окна просмотра деревьев (не всегда необходим) Значение wParam очень важно, поэтому ниже приведены все возможные флаги: TVGN_CARET ― получение дескриптора выбранного элемента. TVGN_CHILD ― получение дескриптора первого дочернего элемента по отношению к элементу, чей дескриптор указан в параметре hitem. TVGN_DROPHILITE ― получение дескриптора элемента, который является целью операции drag-and-drop. TVGN_FIRSTVISIBLE ― получение дескриптора первого видимого элемента. TVGN_NEXT ― получение дескриптора следующего родственного элемента. TVGN_NEXTVISIBLE ― получение дескриптора следующего видимого элемента, который следует за указанным элементом. Указанный элемент должен быть видимым. Используйте сообщение TVM_GETITEMRECT, чтобы определить, является ли элемент видимым. TVGN_PARENT ― получение дескриптора указанного родительского элемента по отношению к указанному. TVGN_PREVIOUS ― получение дескриптора предыдущего родственного элемента. TVGN_PREVIOUSVISIBLE ― получение дескриптора первого видимого элемента, который предшествует указанному элементу, который должен быть видимым. Используйте сообщение TVM_GETITEMRECT, чтобы определить, является ли элемент видимым. TVGN_ROOT ― получает дескриптор самого первого из корневых элементов окна просмотра деревьев. Вы можете видеть, что вы можете получить дескриптор интересующего вас сообщения с помощью этого сообщения. SendMessage возвратит дескриптор окна просмотра деревьев в случае успешного вызова. Затем вы можете заполнить поле hItem структуры TV_ITEM возвращенным дескриптором, чтобы передать структуру TVM_GETITEM.Операции Drag-and-Drop над окном просмотра деревьевКогда пользователь пытается перетащить элемент, окно просмотра деревьев посылает уведомление TVN_BEGINDRAG родительскому окну. Вы можете использовать эту возможность для создания специального изображения, которое будет представлять элемент, когда его тащат. Вы можете послать окну просмотра деревьев сообщение TVM_CREATEDRAGIMAGE, чтобы сказать тому создать такое изображение по умолчанию из изображения, использующееся в настоящее время элементом, который будет перетащен. Окно просмотра деревьев создаст список изображений с одним drag-изображением и возвратит дескриптор этого списка изображений. После того, как drag-изображение создано, вы указываете его "горячую точку", вызывая ImageList_BeginDrag. Код (C): BOOL ImageList_BeginDrag( HIMAGELIST himlTrack, // это дескриптор списка изображений, который содержит drag-изображение int iTrack, // индекс элемента списка изображений, который будет являться drag-изображением int dxHotspot, /* относительная горизонтальная координата "горячей точки" (которая нам необходима, так как мы будем использовать drag-изображение вместо курсора мыши. У стандартного курсора "горячая точка" находится на кончике стрелки). */ int dyHotspot // относительная вертикальная координата "горячей точки" ); Как правило, iTrack будет равен 0, если вам нужно сказать окну просмотра деревьев, чтобы то создало для вас drag-изображение. dxHotspot и dyHotspot могут быть равными 0, если вы хотите, чтобы левый верхний угол drag-изображения был "горячей точкой". Когда drag-изображение готово, мы вызываем ImageList_DragEnter, чтобы отобразить его в окне. Код (C): BOOL ImageList_DragEnter( HWND hwndLock,/* дескриптор окна, которому принадлежит drag-изображение. Drag-изображение нельзя будет двигать за пределы этого окна */ int x, /* координаты места, где drag-изображение должно быть отображено сначала. Заметьте, что эти значения задаются по отношению к левому верхнему углу окна, а не клиентской области. */ int y ); Теперь, когда drag-изображение отображено в окне, вам следует поддерживать операцию перетаскивания в окно просмотра деревьев. Тем не менее, здесь появляется небольшая проблема. Мы должны отслеживать путь перетаскивания с помощью WM_MOUSEMOVE и позицию сброса (drop) с помощью WM_LBUTTONUP. Однако, если drag-изображение находится над каким-нибудь дочерним окном, родительское окно никогда не получит никаких сообщений от мыши. Решение состоит в том, чтобы взять контроль на сообщениями от мыши с помощью SetCapture. Эта функция позволяет направить мышиные сообщения напрямую определенному окну, вне зависимости от того, где находится курсор мыши. Внутри обработчика WM_MOUSEMOVE, вы будете отслеживать drag-путь с помощью вызова ImageList_DragMove. Эта функция передвигает изображение относительно пути переноса. Более того, если вы захотите, вы можете подсвечивать элемент, над которым находится drag-изображение, посылая сообщение TVM_HITTEST, проверяя, находится ли изображение над каким-нибудь элементом. Если это так, вы можете послать TVM_SELECTITEM с флагом TVGN_DROPHILITE, чтобы подсветить элемент. Заметьте, что прежде, чем послать сообщение TVM_SELECTITEM, вы должны спрятать drag-изображение или оно будет оставлять уродливый след. Это можно сделать, вызвав ImageList_DragShowNolock, а после того, как элемент будет подсвечен, необходимо вызвать ImageList_DragShowNolock, чтобы снова отобразить drag-изображение. Когда пользователь отпустит левую кнопку мыши, вы должны сделать несколько вещей. Если вы подсветили элемент, вам нужно перевести его в обычное состояние, снова послав TVM_SELECTITEM с флагом TVGN_DROPHILITE, но в этот pаз lparam должен быть равен нулю. Затем вы должны вызвать ImageList_DragLeave, за которым должен следовать вызов ImageList_EndDrag. Вы должны освободить мышь с помощью ReleaseCapture. Если вы создадите список изображений, вам следует уничтожить его функцией ImageList_Destroy. После этого вы можете сделать все, что нужно, когда операция drag-and-drop завершена.Нотификационные сообщения окну просмотра деревьевПри воздействии на окно просмотра деревьев генерируется сообщение WM_NOTIFY. В этом сообщении может содержаться несколько нотификационных кодов. нотификационный кодназначениеTVN_DELETEITEMэлемент удаленTVN_ITEMEXPANDINGэлемент должен быть развернут или свернутTVN_ITEMEXPANDEDэлемент развернут или свернутTVN_SELCHANGINGэлемент должен быть выбранTVN_SELCHANGEDэлемент выбранПРАКТИКА ― СЕСТРА ШИЗОФРЕНИИrc-файл Код (C): #define IDB_TREE 4006 #define IDM_EXPANDALL 0 #define IDM_COLLAPSEALL 1 #define IDM_EXIT 2 #define ZZZ_Menu 30 IDB_TREE BITMAP "Images\\list.bmp" ZZZ_Menu MENU { POPUP "Treeview" { MENUITEM "Expand all",IDM_EXPANDALL MENUITEM "Collapse all",IDM_COLLAPSEALL MENUITEM SEPARATOR MENUITEM "&Exit",IDM_EXIT } }
asm-файл Код (ASM): include win64a.inc include gdi32.inc includelib gdi32.lib IMAGE_BASE equ 400000h IDB_TREE equ 4006 ZZZ_Menu equ 30 IDM_EXPANDALL equ 0 IDM_COLLAPSEALL equ 1 IDM_EXIT equ 2 ID_TREEVIEW equ 44 .code WinMain proc local msg:MSG xor ebx,ebx invoke InitCommonControls mov eax,10027h mov edi,offset ClassName mov esi,IMAGE_BASE push rax ;hIconSm push rdi ;lpszClassName push ZZZ_Menu ;lpszMenuName push COLOR_APPWORKSPACE;hbrBackground push 10005h ;hCursor push rax ;hIcon push rsi ;hInstance push rbx ;cbClsExtra & cbWndExtra db 68h dd WndProc;lpfnWndProc push sizeof WNDCLASSEX;cbSize & style invoke RegisterClassEx,esp ;addr WNDCLASSEX push rbx push rsi ;rsi=400000h shl esi,9 ;rsi=CW_USEDEFAULT push rbx push rbx push 260;rsi push 385;rsi push rsi push rsi sub esp,20h invoke CreateWindowEx,WS_EX_CLIENTEDGE,edi,edi,\ WS_OVERLAPPEDWINDOW+WS_VISIBLE lea edi,msg @@:invoke GetMessage,edi,0,0,0 invoke DispatchMessage,edi jmp @b WinMain endp WndProc proc hWnd:HWND,uMsg:UINT,wParam:WPARAM,lParam:LPARAM local tvis:TV_INSERTSTRUCT local old_rdi:QWORD local ps:PAINTSTRUCT local tvhit:TV_HITTESTINFO mov hWnd,rcx mov lParam,r9 mov old_rdi,rdi cmp edx,WM_DESTROY je wmDESTROY cmp edx,WM_NOTIFY je wmNOTIFY cmp edx,WM_LBUTTONUP je wmLBUTTONUP cmp edx,WM_CREATE je wmCREATE cmp edx,WM_COMMAND je wmCOMMAND cmp edx,WM_SIZE je wmSIZE cmp edx,WM_MOUSEMOVE je wmMOUSEMOVE leave jmp DefWindowProc wmMOUSEMOVE:cmp DragMode,TRUE jnz wmBYE movzx ecx,r9w;word ptr lParam movzx edx,word ptr lParam+2 mov tvhit.pt.x,ecx ; eax is the horizontal position of the drag image mov tvhit.pt.y,edx ; ecx is the vertical position invoke ImageList_DragMove invoke ImageList_DragShowNolock,FALSE lea r9,tvhit ; check if an item is hit invoke SendMessage,hWndTree,TVM_HITTEST,NULL or eax,eax;.if eax!=NULL jz @f invoke SendMessage,hWndTree,TVM_SELECTITEM,TVGN_DROPHILITE,rax @@: invoke ImageList_DragShowNolock,TRUE jmp wmBYE wmCOMMAND:cmp r8d,IDM_EXIT ja wmBYE jmp handle[r8*8] handle dq wmEXPANDALL,wmCOLLAPSEALL,wmDESTROY wmEXPANDALL:invoke SendMessage,hWndTree,TVM_EXPAND,TVE_EXPAND,hParent0 invoke SendMessage,hWndTree,TVM_EXPAND,TVE_EXPAND,hParent1 invoke SendMessage,hWndTree,TVM_EXPAND,TVE_EXPAND,hParent2 invoke SendMessage,hWndTree,TVM_EXPAND,TVE_EXPAND,hChild3 jmp wmBYE wmCOLLAPSEALL:invoke SendMessage,hWndTree,TVM_EXPAND,TVE_COLLAPSE,hParent0 jmp wmBYE wmDESTROY:invoke ExitProcess,0 wmSIZE: lea edx,ps invoke BeginPaint mov r8d,GreenBrush lea edx,ps.rcPaint invoke FillRect,eax lea edx,ps invoke EndPaint,hWnd jmp wmBYE wmCREATE:push rbx;NULL push IMAGE_BASE;hInstance push ID_TREEVIEW push rcx;hWnd push 200 push 200 push rbx push rbx mov edx,offset TreeViewClass sub esp,20h invoke CreateWindowEx,0,,NULL,WS_CHILD or WS_VISIBLE or TVS_HASLINES or \ TVS_HASBUTTONS or TVS_LINESATROOT or WS_CLIPSIBLINGS mov hWndTree,rax invoke SendMessage,hWndTree,TVM_SETBKCOLOR,0,0FF00h ;---------- [Get the Imagelist] ---------- mov qword ptr [rsp+20h],10 invoke ImageList_Create,16,16,ILC_COLOR16,5 mov hImageList,rax invoke LoadBitmap,IMAGE_BASE,IDB_TREE mov edi,eax;mov hBitmap,rax invoke ImageList_Add,hImageList,eax,NULL invoke DeleteObject,edi;hBitmap invoke SendMessage,hWndTree,TVM_SETIMAGELIST,0,hImageList ;---------- [Fill the tree] ---------- invoke SendMessage,hWndTree,TVM_DELETEITEM,0,TVI_ROOT mov tvis.hParent,rbx;NULL mov tvis.hInsertAfter,TVI_ROOT mov tvis.item.imask,TVIF_TEXT or TVIF_IMAGE or TVIF_SELECTEDIMAGE mov eax,offset Parent0 mov tvis.item.pszText,rax mov tvis.item.iImage,ebx;0 mov tvis.item.iSelectedImage,1 lea r9,tvis invoke SendMessage,hWndTree,TVM_INSERTITEM,0 mov hParent0,rax mov tvis.hParent,rax mov eax,offset Parent1 mov tvis.item.pszText,rax lea r9,tvis invoke SendMessage,hWndTree,TVM_INSERTITEM,0 mov hParent1,rax mov tvis.hParent,rax mov tvis.hInsertAfter,TVI_LAST mov eax,offset Child1 mov tvis.item.pszText,rax lea r9,tvis invoke SendMessage,hWndTree,TVM_INSERTITEM,0 mov hChild1,rax mov eax,offset Child2 mov tvis.item.pszText,rax lea r9,tvis invoke SendMessage,hWndTree,TVM_INSERTITEM,0 mov hChild2,rax mov eax,offset Child3 mov tvis.item.pszText,rax lea r9,tvis invoke SendMessage,hWndTree,TVM_INSERTITEM,0 mov hChild3,rax mov tvis.hParent,rax mov eax,offset Child4 mov tvis.item.pszText,rax lea r9,tvis invoke SendMessage,hWndTree,TVM_INSERTITEM,0 mov hChild4,rax mov eax,offset Child5 mov tvis.item.pszText,rax lea r9,tvis invoke SendMessage,hWndTree,TVM_INSERTITEM,0 mov hChild5,rax mov rax,hParent1 mov tvis.hParent,rax mov eax,offset Child6 mov tvis.item.pszText,rax lea r9,tvis invoke SendMessage,hWndTree,TVM_INSERTITEM,0 mov hChild6,rax mov rax,hParent0 mov tvis.hParent,rax mov tvis.hInsertAfter,TVI_LAST mov tvis.item.imask,TVIF_TEXT or TVIF_IMAGE or TVIF_SELECTEDIMAGE mov eax,offset Parent2 mov tvis.item.pszText,rax lea r9,tvis invoke SendMessage,hWndTree,TVM_INSERTITEM,0 mov hParent2,rax mov tvis.hParent,rax mov eax,offset Child7 mov tvis.item.pszText,rax lea r9,tvis invoke SendMessage,hWndTree,TVM_INSERTITEM,0 mov hChild7,rax invoke CreateSolidBrush,000FF00h;green=00FF00h mov GreenBrush,eax jmp wmBYE wmLBUTTONUP:cmp DragMode,TRUE jnz wmBYE invoke ImageList_DragLeave,hWndTree invoke ImageList_EndDrag invoke ImageList_Destroy,hDragImageList invoke SendMessage,hWndTree,TVM_GETNEXTITEM,TVGN_DROPHILITE,0; Get the currently hilited item invoke SendMessage,hWndTree,TVM_SELECTITEM,TVGN_CARET,rax invoke SendMessage,hWndTree,TVM_SELECTITEM,TVGN_DROPHILITE,0 invoke ReleaseCapture mov DragMode,FALSE jmp wmBYE wmNOTIFY:cmp r8d,ID_TREEVIEW ; Treeview? jne wmBYE cmp [r9+NM_TREEVIEW.hdr._code],TVN_ITEMEXPANDING jne @f cmp [r9+NM_TREEVIEW.action],TVE_COLLAPSE jne wmBYE invoke SendMessage,hWndTree,TVM_SELECTITEM,TVGN_CARET,0 jmp wmBYE @@: cmp [r9+NM_TREEVIEW.hdr._code],TVN_BEGINDRAG jnz wmBYE mov rdi,r9;lParam mov r9,[rdi+NM_TREEVIEW.itemNew.hItem] invoke SendMessage,hWndTree,TVM_CREATEDRAGIMAGE,0 mov hDragImageList,rax invoke ImageList_BeginDrag,hDragImageList,0,0,0 mov r8d,[rdi+NM_TREEVIEW.ptDrag.y] mov edx,[rdi+NM_TREEVIEW.ptDrag.x] invoke ImageList_DragEnter,hWndTree invoke SetCapture,hWnd mov DragMode,TRUE wmBYE: mov rdi,old_rdi leave xor eax,eax retn WndProc endp ;data ClassName db 'Win64 Iczelion''s lesson #19: Tree View Demo',0 TreeViewClass db "SysTreeView32",0 Parent0 db 'Zero',0 Parent1 db 'One',0 Parent2 db 'Two',0 Child1 db 'Eight',0 Child2 db 'Four',0 Child3 db 'Five',0 Child4 db 'Six',0 Child5 db 'Seven',0 Child6 db 'Three',0 Child7 db 'Nine',0 DragMode db FALSE GreenBrush dd ? hWndTree dq ? hParent0 dq ? hParent1 dq ? hParent2 dq ? hChild1 dq ? hChild2 dq ? hChild3 dq ? hChild4 dq ? hChild5 dq ? hChild6 dq ? hChild7 dq ? hImageList dq ? hDragImageList dq ? end Разбор полетовВнутри обработчика WM_CREATE вы создаете окно просмотра деревьев. Код (ASM): 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 ― это стили, присущие окну просмотра деревьев. Код (ASM): 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 Затем вы создаете пустой список изображений, который будет принимать изображения размером 16x16 пикселей и с глубиной цвета 16 бит. Вначале он будет содержать 2 изображения, но будет расширен до 10, если это потребуется. Далее мы загружаем рисунок из ресурса и добавляем его в только что созданный список изображений. После этого мы удаляем дескриптор рисунка, так как он больше нам не нужен. Как только список изображений готов, мы ассоциируем его с окном просмотра деревьев, посылая ему TVM_SETIMAGELIST. Код (ASM): 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
Мы вставляем элементы в окно просмотра деревьев, начиная с корневого элемента. Так как это будет корневой элемент, параметр hParent равен NULL, а hInsertAfter ― TVI_ROOT. imask указывает, что pszText, iImage и iSelectedImage структуры TV_ITEM верны. Мы заполняем эти три параметра соответствующими значениями. pszText содержит название корневого элемента, iImage ― это индекс изображения в списке изображений, который будет отображаться слева от невыбранного элемента, а iSelectedImage ― индекс изображения выбранного элемента. Когда все требуемые параметры заполнены, мы посылаем сообщение TVM_INSERTITEM окну просмотра деревьев, чтобы добавить в него корневой элемент. Код (ASM): 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 После этого мы добавляем дочерние элементы. hParent теперь заполнен дескриптором родительского элемента. Мы будем использовать те же изображения, поэтому не меняем iImage и iSelectedImage. Код (ASM): wmNOTIFY: mov rdi,lParam assume rdi:ptr NM_TREEVIEW .if [rdi].hdr.code==TVN_BEGINDRAG invoke SendMessage,hwndTreeView,TVM_CREATEDRAGIMAGE,\ 0,[edi].itemNew.hItem mov hDragImageList,rax invoke ImageList_BeginDrag,hDragImageList,0,0,0 invoke ImageList_DragEnter,hwndTreeView,[rdi].ptDrag.x,\ [rdi].ptDrag.y invoke SetCapture,hWnd mov DragMode,TRUE .endif assume rdi:nothing Теперь, когда пользователь попытается перетащить элемент, окно просмотра деревьев пошлет сообщение WM_NOTIFY с кодом TVN_BEGINDRAG. lParam ― это указатель на структуру NM_TREEVIEW, которая содержит некоторую информацию, которая необходима нам, поэтому мы помещаем значение lParam в rdi и используем rdi как указатель на структуру NM_TREEVIEW. 'assume edi:ptr NM_TREEVIEW' указывает MASM'у, что rdi ― это указатель на структуру NM_TREEVIEW. Затем мы создаем drag-изображение, посылая [TVM_CREATEDRAGIMAGE окну просмотра деревьев. Сообщение возвращает дескриптор на созданный список изображений, внутри которого содержится drag-изображение. Мы вызываем ImageList_BeginDrag, чтобы установить его "горячую точку". После этого начинаем операцию переноса с помощью ImageList_DragEnter. Эта функция отображает drag-изображение в указанном месте заданного окна. Мы используем структуру ptDrag, которая является полем структуры NM_TREEVIEW в качестве точки, в которой должно быть показано drag-изображение. Затем перехватываем мышь и устанавливаем флаг, который показывает, что мы находимся в drag-режиме. Код (ASM): wmMOUSEMOVE: .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 Теперь мы концентрируемся на WM_MOUSEMOVE. Когда пользователь перетаскивает drag-изображение, наше родительское окно получает сообщения WM_MOUSEMOVE. В ответ на них мы обновляем позицию drag-изображения функцией ImageList_DragMove, после чего проверяем, не находится ли оно над каким-нибудь элементом с помощью сообщения TVM_HITTEST с указанием координаты проверяемой точки. Если drag-изображение находится над каким-либо элементом, тот подсвечивается сообщением TVM_SELECTITEM с флагом TVGN_DROPHILITE. Во время операции подсветки мы прячем drag-изображение, чтобы не было лишних глюков. Код (ASM): wmLBUTTONUP: .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-режима, последовательно вызывая функции ImageList_DragLeave, ImageList_EndDrag и ImageList_Destroy. Также мы проверяем последний подсвеченный элемент и выбираем его. Мы также должны убрать его подсветку, иначе другие элементы не будут подсвечиваться, когда их будут выбирать. И наконец, мы убираем перехват сообщений от мыши.
Глава тридцать девятая. Братец Кролик изучает сабклассинг окна В этой главе мы изучим сабклассинг окна, что это такое, и как его использовать. Скачайте пример здесь.Теория ― МАТЬ СКЛЕРОЗАЕсли вы уже некоторое время программируете в Windows, вы уже могли столкнуться с ситуацией, когда окно имеет почти все атрибуты, которые вам нужны, но не все. Сталкивались ли вы с ситуацией, когда вам требуется специальный вид окна ввода, который бы отфильтровывал ненужный текст? Первое, что может придти в голову, это написать свое собственное окно. Hо это действительно тяжелая работа, требующая значительного времени. Выходом является сабклассинг окна. Вкратце, сабклассинг окна позволяет получить контроль над сабклассированным окном. У вас будет абсолютный контроль над ним. Давайте рассмотрим пример, что прояснить данное утверждение. Предположите, что вам нужен text box, в котором можно вводить только шестнадцатиричные числа. Если вы будете использовать обычное окне ввода, максимум, что вы сможете сделать, если пользователь введет неверную букву, это стереть исходную строку и вывести ее снова в отредактированном виде. По меньшей мере, это непрофессионально. Фактически вам требуется получить возможность проверять каждый символ, который пользователь набирает в text box'е, как pаз в тот момент, когда он делает это. Теперь мы изучим как это сделать. Когда пользователь печатает что-то в text box'е, Windows посылает сообщение WM_CHAR процедуре окна ввода. Эта процедура окна находится внутри Windows, поэтому мы не можем модифицировать ее. Hо мы можем перенаправить поток сообщений к нашей оконной процедуре. Поэтому наша процедура окна первой получит возможность обработать сообщение, которое Windows пошлет окну ввода. Если наша процедура решит обработать сообщение, она так и сделает. Hо если она не захочет его обрабатывать, она может передать его оригинальной оконной процедуре. Таким образом, наша функция будет стоять между Windows и окном ввода. Посмотрите на условную схему внизу. До сабклассинга: Windows [math]\to[/math] процедура окна ввода После сабклассинга: Windows [math]\to[/math] наша оконная процедура [math]\to[/math] процедура окна ввода Теперь мы можем рассмотреть то, каким образом происходит сабклассинг окна. Заметьте, что сабклассинг не ограничивается элементами управления, он может использоваться с любым окном. Давайте подумаем о том, как Windows узнает, где находится процедура edit box'а. Hу?.. Поле lpfnWndProc в структуре WNDCLASSEX. Если мы сможем поменять значение этого поля на адрес собственной структуры, Windows пошлет сообщение нашей процедуре окна вместо этого. Мы можем сделать это, вызвав SetWindowLong. Код (C): LONG WINAPI SetWindowLong( _In_ HWND hWnd, //дескриптор окна, чьи свойства мы хотим поменять _In_ int nIndex, // значение, которое нужно изменить _In_ LONG dwNewLong ); dwNewLong ― новое значение. У каждого окна есть ассоциированное с ним 32-битное значение, предназначенное для использования приложением в своих целях. GWL_EXSTYLEУстановка нового расширенного стиля окна.GWL_STYLEУстановка нового стиля окна.GWL_WNDPROCУстановка нового адреса для процедуры окна.GWL_HINSTANCEУстановка нового дескриптора приложения.GWL_IDУстановка нового идентификатора окна.GWL_USERDATAУстановка 32-битного значения, ассоциирующегося с окном.Таким образом, наша работа проста: мы создаем процедуру окна, которая будет обрабатывать сообщения для окна ввода и затем вызывать SetWindowLong с флагом GWL_WNDPROC, которому передается адрес нашего окна в качестве третьего параметра. В случае, если вызов функции прошел нормально, возвращаемым значением является прежнее значение замещаемого параметра, в нашем случае ― это адрес оригинальной процедуры окна. Нам нужно сохранить это значение, чтобы использовать его внутри нашей процедуры. Помните, что есть сообщения, которые нам не нужно будет обрабатывать. Их мы будем передавать оригинальной процедуре. Мы можем сделать это с помощью вызова функции CallWindowProc. Код (C): LRESULT WINAPI CallWindowProc( _In_ WNDPROC lpPrevWndFunc,// адрес оригинальной процедуры окна _In_ HWND hWnd, _In_ UINT Msg, _In_ WPARAM wParam, _In_ LPARAM lParam ); Остальные четыре значения ― это те, что передаются нашей процедуре окна. Мы передаем их CallWindowProc. ПРАКТИКА ― СЕСТРА ШИЗОФРЕНИИ Код (ASM): include win64a.inc IMAGE_BASE equ 400000h .code WinMain proc local msg:MSG xor ebx,ebx mov eax,10029h mov esi,IMAGE_BASE push rax ;hIconSm mov edi,offset ClassName push rdi ;lpszClassName push rbx ;lpszMenuName push COLOR_WINDOW;hbrBackground push 10005h ;hCursor push rax ;hIcon push rsi ;hInstance push rbx ;cbClsExtra & cbWndExtra db 68h dd WndProc;lpfnWndProc push sizeof WNDCLASSEX;cbSize & style invoke RegisterClassEx,esp ;addr WNDCLASSEX push rbx push rsi ;rsi=400000h shl esi,9 ;rsi=CW_USEDEFAULT push rbx push rbx push 240 push 410 push rsi push rsi sub esp,20h invoke CreateWindowEx,WS_EX_CLIENTEDGE,edi,edi,\ WS_OVERLAPPEDWINDOW or WS_VISIBLE push rbx push IMAGE_BASE push rbx push rax;hWnd push 24 push 300 push 20 push 20 mov edx,offset ctlClsNameEdit sub esp,20h invoke CreateWindowEx,WS_EX_CLIENTEDGE,,0,\ WS_CHILD + WS_VISIBLE + WS_BORDER mov edit1H,rax invoke SetFocus,eax mov r8d,offset edit1_procedure invoke SetWindowLongPtr,edit1H,GWL_WNDPROC mov wndProcAddr,rax lea edi,msg @@: invoke GetMessage,edi,0,0,0 invoke TranslateMessage,edi invoke DispatchMessage,edi jmp @b WinMain endp WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM cmp edx,WM_DESTROY je wmDESTROY wmDEFAULT:jmp DefWindowProc wmDESTROY:invoke ExitProcess,0 WndProc endp edit1_procedure proc hWnd:HWND, uMsg:QWORD, wParam:WPARAM, lParam:LPARAM push rbp mov ebp,esp sub esp,30h mov hWnd,rcx mov uMsg,rdx mov wParam,r8 mov lParam,r9 cmp edx,WM_KEYDOWN je edit1_wmKEYDOWN cmp edx,WM_CHAR jne a1 edit1_wmCHAR: cmp r8b,VK_BACK ;compare with virtual key BACKSPACE e a1 cmp r8b,30h ;compare with ascii 0 jb edit1_wmBYE cmp r8b,39h ;compare with ascii 9 jbe a1 and r8b,-33 ;so our DL become big letter cmp r8b,41h ;compare with ascii A jb edit1_wmBYE cmp r8b,46h ;compare with ascii F ja edit1_wmBYE ;something else a1: mov [esp+20h],r9;lParam mov r9,r8;wParam mov r8,rdx;uMsg mov rdx,rcx;hWnd invoke CallWindowProc,wndProcAddr jmp edit1_wmBYE edit1_wmKEYDOWN: cmp r8b,VK_RETURN ;compare with virtual key RETURN jne a1 mov r8d,offset ClassName mov edx,offset edit1Txt1 invoke MessageBox,,,,MB_OK invoke SetFocus,hWnd edit1_wmBYE: leave ret edit1_procedure endp ClassName db 'Win64 Iczelion''s lesson #20: Window Subclassing',0 ctlClsNameEdit db 'EDIT',0 edit1Txt1 db 'A simple HEX edit control!',0 wndProcAddr dq ? edit1H dq ? end Разбор полетов Код (ASM): invoke SetWindowLong, hwndEdit, GWL_WNDPROC, addr EditWndproc mov OldWndproc,eax После того, как окно ввода создано, мы сабклассим его, вызывая SetWindowLong и замещая адрес оригинальной процедуры окна нашим собственным адресом. Заметьте, что мы сохраняем значение адреса оригинальной процедуры, чтобы впоследствии использовать его при вызове CallWindowProc. Заметьте, что EditWndProc ― это обычная оконная процедура. Код (ASM): wmCHAR: mov rax,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 Внутри EditWndProc, мы фильтруем сообщения WM_CHAR. Если введен символ в диапазоне 0-9 или a-f, мы передаем его оригинальной процедуре окна. Если это символ нижнего регистра, мы конвертируем его в верхний, добавляя 20h. Заметьте, что если символ не тот, который мы ожидали, мы пропускаем его. Мы не передаем его оригинальной процедуре окна. Поэтому, когда пользователь набирает что-нибудь отличное от 0-9 или A-F, символ не появляется в окне ввода. Код (ASM): wmKEYDOWN: mov rax,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. EditWndProc проверяет сообщение WM_KEYDOWN, не равно ли оно VK_RETURN (клавиша Enter). Если это так, она отображает окно с сообщением "You pressed the Enter key in the text box!". Если это не клавиша Enter, она передает сообщение оригинальной процедуре. Вы можете использовать сабклассинг окна, чтобы получить контроль над другими окнами. Эту мощную технику вам следует иметь в своем арсенале.
Глава сороковая. Братец Кролик и пайп В этой главе мы исследуем пайп (pipe ―труба, трубопровод), что это такое и для чего мы можем использовать его. Чтобы сделать этот процесс более интересным, покажем, как можно изменить фон и цвет текста у окна ввода. Скачайте пример здесь.Теория ― МАТЬ СКЛЕРОЗАПайп ― канал или дорога с двумя концами. Вы можете использовать пайп, чтобы обмениваться данными между двумя различными процессами или внутри одного процесса. Это что-то вроде переговорного устройства. Вы даете другому участнику конец канала и он может использовать его для того, чтобы взаимодействовать с вами. Есть два типа пайпов: анонимные и именованные. Анонимный пайп анонимен ― вы можете использовать его не зная его имени. Для того, чтобы использовать именованный пайп, вам обязательно нужно знать его имя. Вы можете разделить пайп по их свойствам: однонаправленные и двунаправленные. В однонаправленном пайпе данные могут течь только в одном направлении: от одного конца к другому, в то время как в двунаправленном данные могут передаваться между обоими концами. Анонимный пайп всегда однонаправленный. Именованный может быть и таким, и таким. Именованные пайпы обычно используются в сетевом окружении, где сервер может коннектиться к нескольким клиентам. В этой главе мы подробно рассмотрим анонимные пайпы. Главная цель таких пайпов ― служить каналом между родительским и дочерним процессом или между дочерними процессами. Анонимный пайп действительно полезен, когда вы взаимодействуете с консольным приложением. Консольное приложение ― это вид windows-программ, которые используют консоль для своего ввода и вывода. Консоль ― это вроде DOS-box'а. Тем не менее, консольное приложение ― это полноценное 64-битное приложение. Оно может использовать любую GUI-функцию, так же как и другие GUI-программы. Она отличается только тем, что у нее есть консоль. У консольного приложения есть три дескриптора, которые оно может использовать для ввода и вывода. Они называются стандартными дескрипторами: стандартный ввод, стандартный вывод и стандартный вывод ошибок. Стандартный дескриптор ввода используется для того, чтобы читать/получать информации из консоли и стандартный дескриптор вывода используется для вывода/распечатки информации на консоль. Стандартный дескриптор вывода ошибок используется для сообщения об ошибках. Консольное приложение может получить эти три стандартных значения, вызвав функцию GetStdHandle, указав дескриптор, который она хочет получить. GUI-приложение не имеет консоли. Если вы вызывает GetStdHandle, она возвратит ошибку. Если вы действительно хотите использовать консоль, вы можете вызвать AllocConsole, чтобы зарезервировать новую консоль. Тем не менее, не забудьте вызвать FreeConsole, когда вы уже не будете в ней нуждаться. Анонимный пайп очень часто используется для перенаправления ввода и/или вывода дочернего консольного приложения. родительский процесс может быть консоль или GUI-приложение, но дочернее приложение должно быть консольным, чтобы это сработало. Как вы знаете, консольное приложение использует стандартные дескрипторы для ввода и вывода. Если мы хотите перенаправить ввод/вывод консольного приложения, мы можем заменить один дескриптор другим дескриптором одного конца пайпа. Консольное приложение не будет знать, что оно использует один конец пайпа. Оно будет считать, что это стандартный дескриптор. Это вид полиморфизма на ООП-жаргоне. Это мощный подход, так как нам не нужно модифицировать родительский процесс никаким образом. Другая вещь, которую вы должны знать о консольном приложение ― это откуда оно берет стандартный дескриптор. Когда консольное приложение создано, у родительского приложения есть следующий выбор: оно может создать новую консоль для дочернего приложения или позволить тому наследовать собственную консоль. Чтобы второй метод работал, родительский процесс должен быть консольным, либо, если он GUI'евый, создать консоль с помощью AllocConsole. Давайте начнем работу. Чтобы создать анонимный пайп, вам требуется вызывать CreatePipe. Эта функция имеет следующий прототип: Код (C): BOOL WINAPI CreatePipe( _Out_ PHANDLE hReadPipe,/* указатель на переменную типа qword, которая получит дескриптор конца чтения пайпа*/ _Out_ PHANDLE hWritePipe,/*указатель на переменную типа qword, которая получить дескриптор на конец записи пайпа*/ _In_opt_ LPSECURITY_ATTRIBUTES lpPipeAttributes,/*указывает на структуру SECURITY_ATTRIBUTES, которая определяет, наследуется ли каждый из концов дочерним процессом */ _In_ DWORD nSize /* предполагаемый размер буфера, который пайп зарезервирует для использования. Это всего лишь предполагаемый размер. Вы можете передать NULL, чтобы указать функции использовать размер по умолчанию */ ); Если вызов прошел успешно, возвращаемое значение не равно нулю, иначе оно будет нулевым. После успешного вызова CreatePipe вы получите два дескриптора, один к концу чтения, а другой к концу записи. Теперь изложим шаги, необходимые для перенаправления стандартного вывода дочерней консольной программы в ваш процесс. Создаем анонимный пайп с помощью CreatePipe. Не забудьте установить параметр bInheritable структуры SECURITY_ATTRIBUTES в TRUE, чтобы дескрипторы могли наследоваться. Теперь мы должны подготовить параметры, которые передадим CreateProcess (мы используем эту функцию для загрузки консольного приложения). Среди аргументов этой функции есть важная структура STARTUPINFO. Эта структура определяет появление основного окна дочернего процесса, когда он запускается. Эта структура жизненно важна для нас. Вы можете спрятать основное окно и передать дескриптор пайпа дочерней консоли вместе с этой структурой. Hиже находятся поля, которые вы должны заполнить: cb : размер структуры STARTUPINFO dwFlags : двоичные битовые флаги, которые определяют, какие члены структуры будут использоваться, также она управляет состоянием основного окна. Нам нужно указать комбинацию STARTF_USESHOWWINDOW and STARTF_USESTDHANDLES. hStdOutput и hStdError : дескрипторы, которые будут использоваться в дочернем процессе в качестве дескрипторов стандартного ввода/вывода. Для наших целей мы передадим дескриптор пайпа в качестве стандартного вывода и вывода ошибок. Поэтому когда дочерний процесс выведет что-нибудь туда, он фактически передаст информацию через пайп родительскому процессу. wShowWindow управляет тем, как будет отображаться основное окно. Нам не нужно, что окно консоли отображалось на экран, поэтому мы приравняем этот параметр к SW_HIDE. Вызов CreateProcess, чтобы загрузить дочернее приложение. После того, как вызов прошел успешно, дочерний процесс все еще находится в спящем состоянии. Он загружается в память, но не запускается немедленно. Закройте конец дескриптор конца записи пайпа. Это необходимо, так как родительский процессу нет нужды использовать этот дескриптор, а пайп не будет работать, если открыть более чем один конец записи. Следовательно, мы должны закрыть его прежде, чем считывать данные из пайпа. тем не менее, не закрывайте этот конец до вызова CreateProcess, иначе ваш пайп будет сломан. Вам следует закрыть конец записи после того, как будет и вызвана функция CreateProcess, и до того, как вы считаете данные из конца чтения пайпа. Теперь вы можете читать данные из конца чтения с помощью ReadFile. С ее помощью вы запускаете дочерний процесс, который начнет выполняться, а когда он запишет что-нибудь в стандартный дескриптор вывода, данные будут посланы на конец чтения пайпа. Вы должны последовательно вызывать ReadFile, пока она не возвратит ноль, что будет означать, что больше данных нет. С полученной информацией вы можете делать все, что хотите, в нашем случае я вывожу их в окно ввода. Закроем дескриптор чтения пайпа.
ПРАКТИКА ― СЕСТРА ШИЗОФРЕНИИrc-файл Код (C): #define MI_CSCRIPT 100 #define IDC_MENU 30 IDC_MENU MENU { POPUP "&Action" { MENUITEM "&Assemble",MI_CSCRIPT } } asm-файл Код (ASM): include win64a.inc IMAGE_BASE equ 400000h MI_CSCRIPT equ 100 IDC_MENU equ 30 .code WinMain proc local msg:MSG xor ebx,ebx mov eax,10027h mov esi,IMAGE_BASE mov edi,offset ClassName push rax ;hIconSm push rdi ;lpszClassName push IDC_MENU ;lpszMenuName push COLOR_WINDOW;hbrBackground push 10005h ;hCursor push rax ;hIcon push rsi ;hInstance push rbx ;cbClsExtra & cbWndExtra db 68h dd WndProc ;lpfnWndProc push sizeof WNDCLASSEX;cbSize & style invoke RegisterClassEx,esp ;addr WNDCLASSEX push rbx push rsi ;rsi=400000h shl esi,9 ;rsi=CW_USEDEFAULT push rbx push rbx push 350 push 600 push rsi push rsi sub rsp,20h invoke CreateWindowEx,WS_EX_CLIENTEDGE,edi,edi,\ WS_OVERLAPPEDWINDOW or WS_VISIBLE push rbx push IMAGE_BASE push rbx push rax push 280 push 575 push rbx push rbx mov edx,offset ctlClsNameEdit sub rsp,20h invoke CreateWindowEx,WS_EX_CLIENTEDGE,,0,\ WS_CHILD + WS_VISIBLE + WS_HSCROLL + WS_VSCROLL +\ ES_MULTILINE + ES_AUTOHSCROLL + ES_AUTOVSCROLL mov edit1H,rax lea edi,msg @@:invoke GetMessage,edi,0,0,0 invoke TranslateMessage,edi invoke DispatchMessage,edi jmp @b WinMain endp ;---------------------------------------- WndProc proc hWnd:HWND,uMsg:UINT,wParam:WPARAM,lParam:LPARAM local pipeReadH:QWORD local pipeWriteH:QWORD local pro1StartInfo:STARTUPINFO local pro1Info:PROCESS_INFORMATION local pipeRead:QWORD local pipeBuffer[400h]:BYTE mov hWnd,rcx mov wParam,r8 mov lParam,r9 cmp edx,WM_DESTROY je wmDESTROY cmp edx,WM_COMMAND je wmCOMMAND cmp edx,WM_SIZE je wmSIZE cmp edx,WM_CTLCOLOREDIT je wmCTLCOLOREDIT leave jmp DefWindowProc wmDESTROY: invoke ExitProcess,0 wmSIZE: mov qword ptr [esp+28h],TRUE mov rax,r9 movzx r9,ax shr eax,16 mov [esp+20h],rax invoke MoveWindow,edit1H,0,0 jmp wmBYE wmCTLCOLOREDIT:invoke SetTextColor,r8,0C9B5AFh invoke SetBkColor,wParam,0 invoke GetStockObject,BLACK_BRUSH jmp wmBYE wmCOMMAND:cmp r8,0FFFFh and MI_CSCRIPT jne wmBYE wmCOMMAND_MI_CSCRIPT:mov edi,offset pipeSecAttr mov [rdi+SECURITY_ATTRIBUTES.nLength], sizeof SECURITY_ATTRIBUTES mov [edi+SECURITY_ATTRIBUTES.lpSecurityDescriptor],rbx mov [edi+SECURITY_ATTRIBUTES.bInheritHandle],TRUE lea edx,pipeWriteH lea ecx,pipeReadH invoke CreatePipe,,,rdi,0 or eax,eax;NULL je MI_CSCRIPT_error_pipe lea edi,pro1StartInfo mov [edi+STARTUPINFO.cb],sizeof STARTUPINFO invoke GetStartupInfo,edi mov rax,pipeWriteH mov [rdi+STARTUPINFO.hStdOutput],rax mov [rdi+STARTUPINFO.hStdError],rax mov [rdi+STARTUPINFO.dwFlags],STARTF_USESHOWWINDOW + STARTF_USESTDHANDLES mov [rdi+STARTUPINFO.wShowWindow],SW_HIDE ;+----------------+ ;| create process | ;+----------------+ lea eax,pro1Info mov [rsp+48h],rax mov [rsp+40h],rdi mov [rsp+38h],rbx mov [rsp+30h],rbx mov [rsp+28h],rbx mov qword ptr [rsp+20h],TRUE mov edx,offset pro1CmdLine invoke CreateProcess,0,,0,0 or eax,eax;cmp eax,NULL je MI_CSCRIPT_error_process @@: invoke CloseHandle,pipeWriteH cmp eax,TRUE jne @b lea ecx,pipeBuffer invoke RtlZeroMemory,,400h mov [rsp+20h],rbx lea r9,pipeRead lea edx,pipeBuffer invoke ReadFile,pipeReadH,,1023 or eax,eax;cmp eax,NULL je @f invoke SendMessage,edit1H,EM_SETSEL,-1,0 lea r9,pipeBuffer invoke SendMessage,edit1H,EM_REPLACESEL,0 jmp @f MI_CSCRIPT_error_pipe:mov r8d,offset ClassName mov edx,offset errStr1 invoke MessageBox,hWnd,,,,MB_OK jmp wmBYE MI_CSCRIPT_error_process:mov r8d,offset ClassName mov edx,offset errStr2 invoke MessageBox,hWnd,,,MB_OK @@: invoke CloseHandle,pipeReadH invoke CloseHandle,pro1Info.hProcess invoke CloseHandle,pro1Info.hThread wmBYE: leave retn WndProc endp ;----------------------------------------- ClassName db 'Win64 Iczelion''s lesson #21: Pipe',0 ctlClsNameEdit db 'EDIT',0 edit1H dq ? pipeSecAttr SECURITY_ATTRIBUTES <> errStr1 db 'Error - Pipe Creation Failed!',0 errStr2 db 'Error - Process Creation Failed!',0 pro1CmdLine db 'ml64 /Cp msgbox.asm /link /subsystem:windows /entry:WinMain',0 end
Разбор полетовПример вызовет ml.exe, чтобы скомпилировать файл под названием test.asm, и перенаправит вывод в окно ввода. Когда программа загружена, она регистрирует класс окна и создает, как обычно, основное окно. Теперь наступает самая интересная часть. Мы изменим цвет текста и фон окна ввода. Когда окно ввода подойдет к моменту отрисовки его клиентской области, он пошлет сообщение WM_CTLCOLOREDIT родительскому окну. wParam содержит дескриптор device context'а, который окно ввода будет использовать для отрисовки его клиентской области. Мы можем использовать эту возможность для изменения характеристик HDC. Код (ASM): wmCTLCOLOREDIT:invoke SetTextColor, wParam, Yellow invoke SetTextColor,wParam,Black invoke GetStockObject,BLACK_BRUSH ret SetTextColor изменяет цвет текста на желтый. SetTextColor изменяет цвет фона текста на черный. И, наконец, мы получаем дескриптор черной кисти, которую мы возвратим Windows. Обрабатывая сообщение WM_CTLCOLOREDIT, мы должны возвратить дескриптор кисти, которую Windows использует для отрисовки фона окна ввода. В нашем пример, я хочу, чтобы фон был черным, поэтому я возвращаю дескриптор черной кисти Windows. Когда пользователь выберет пункт меню 'Assemble', программа создаст анонимный пайп. Код (ASM): .if ax==IDM_ASSEMBLE mov sat.niLength,sizeof SECURITY_ATTRIBUTES mov sat.lpSecurityDescriptor,NULL mov sat.bInheritHandle,TRUE Перед вызовом CreatePipe мы должны заполнить структуру SECURITY_ATTRIBUTES. Заметьте, что мы можем передать NULL, если нас не интересуют настройки безопасности. И параметр bInheritHandle должен быть равен нулю, поэтому дескриптор пайпа наследуется дочерним процессом. Код (ASM): invoke CreatePipe,addr hRead,addr hWrite,addr sat,NULL После этого мы вызываем CreatePipe, которая заполнить переменные hRead и hWrite дескрипторами концов чтения и записи. Код (ASM): 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 Затем мы заполним структуру STARTUPINFO. Мы вызовем GetStartupInfo, чтобы заполнить ее значениями родительского процесса. После вы модифицирует члены структуры. Мы копируем дескриптор конца записи в hStdOutput и hStdError, так как мы хотим, чтобы дочерний процесс использовал их вместо соответствующих стандартных дескрипторов. Мы также хотим спрятать консольное окно дочернего процесса, поэтому в wShowWindow мы помещаем значение SW_HIDE. И, наконец, мы должны подтвердить, что модифицированные нами поля нужно использовать, поэтому мы указываем флаги STARTF_USESHOWWINDOW и STARTF_USESTDHANDLES. Код (ASM): invoke CreateProcess, NULL, addr CommandLine, NULL, NULL, TRUE, NULL, NULL, NULL,\ addr startupinfo, addr pinfo Теперь мы создаем дочерний процесс функцией CreateProcess. Заметьте, что параметр bInheritHandles должен быть установлен в TRUE, чтобы дескриптор пайпа работал. Код (ASM): invoke CloseHandle,hWrite После успешного создания дочернего процесса мы закрываем конец записи пайпа. Помните, что мы передали дескриптор записи дочернему процессу через структуру STURTUPINFO. Если мы не закроем конец записи с нашей стороны, будет два конца записи, и тогда пайп не будет работать. Мы должны закрыть конец записи после Createprocess, но до того, как начнем считывание данных. Код (ASM): .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 Теперь мы готовы читать данные. Мы входим в бесконечный цикл, пока все данные не будут считаны. Мы вызываем RtlZeroMemory, чтобы заполнить буфер нулями, потом вызываем ReadFile и вместо дескриптора файла передаем дескриптор пайпа. Заметьте, что мы считываем максимум 1023 байта, так данные, которые мы получим, должны быть ASCIIZ-строкой, которую можно будет передать окну ввода. Когда ReadFile вернет данные в буфере, мы выведем их в окно ввода. Тем не менее, здесь есть несколько проблем. Если мы используем SetWindowText, чтобы поместить данные в окно ввода, новые данные перезапишут уже считанные! Нам нужно, чтобы новые данные присоединялись к старым. Для достижения цели мы сначала двигаем курсор к концу текста окна ввода, послав сообщение EM_SETSEL с wParam'ом равным -1. Затем мы присоединяем данные с помощью сообщения EM_REPLACESEL. Код (ASM): invoke CloseHandle, hRead Когда ReadFile возвращает NULL, мы выходим из цикла и закрываем конец чтения.
Глава сорок первая. Братец Кролик и суперклассинг В этой главе мы изучим суперклассинг, что это такое и для чего он служит. Вы также узнаете, как pеализовать навигацию с помощью клавиши 'Tab' в вашем окне. Скачайте пример здесь.Теория ― МАТЬ СКЛЕРОЗАВо время вашей программной карьеры, вы наверняка встретитесь с ситуацией, когда вам потребуется несколько контролов с *несколько* отличным поведением. Например, вам могут потребоваться 10 окон ввода, которые принимают только число. Есть несколько путей достигнуть цели: Создать собственный класс и написать контролы с нуля Создать эти edit control'ы и сабклассировать каждый из них Суперклассировать окно ввода Первый метод слишком сложен. Вам придется с нуля воплощать всю функциональность окна ввода. Слишком трудоемкая задача, чтобы ее можно было быстро выполнить. Второй метод лучше, чем первый, но, тем не менее, также требует немало работы. Все нормально, пока вам надо сабклассировать несколько контролов, но сабклассинг дюжины или еще большего количества контролов может превратиться в ад. Суперклассинг ― это техника, которой вы должны владеть. Суперклассинг ― это метод, с помощью которого вы сможете взять контроль над определенным классом окна. По взятием контроля я подразумеваю, что вы сможете изменить свойства класса, так чтобы они соответствовали вашим целям, после чего вы можете создать сколько угодно таких контролов. Hиже приведены шаги для суперклассинга: вызвать функцию GetClassInfoEx, чтобы получить информацию о классе окна, который вы хотите суперклассировать. GetClassInfoEx требует указатель на структуру WNDCLASSEX, которая будет заполнена информацией, если вызов пройдет успешно. Изменяйте требуемые параметры WNDCLASSEX. Тем не менее, если два члена, которые вы должны обязательно изменить: hInstance ― Вы должны поместить в это поле дескриптор программы. lpszClassName ― вы должны поместить сюда указатель на новое имя класса. Вы не обязаны изменять параметр lpfnWndProc, но обычно вам будет это нужно делать. Главное не забудьте сохранить старое значение lpfnWndproc, если вам надо будет его вызывать с помощью CallWindowProc. Зарегистрирует измененную структуру WNDCLASSEX. У вас будет новый класс окна, который будет обладать некоторыми характеристиками старого класса. Создайте окна с помощью нового класса. Суперклассинг лучше, чем сабклассинг, если вы хотите создать много контролов с одинаковыми характеристиками.ПРАКТИКА ― СЕСТРА ШИЗОФРЕНИИ Код (ASM): include win64a.inc IMAGE_BASE equ 400000h .code WinMain proc local msg:MSG xor ebx,ebx mov eax,10029h mov esi,IMAGE_BASE push rax ;hIconSm mov edi,offset ClassName push rdi ;lpszClassName push rbx ;lpszMenuName push COLOR_WINDOW;hbrBackground push 10005h ;hCursor push rax ;hIcon push rsi ;hInstance push rbx ;cbClsExtra & cbWndExtra db 68h dd WndProc;lpfnWndProc push sizeof WNDCLASSEX;cbSize & style invoke RegisterClassEx,esp ;addr WNDCLASSEX push rbx push rsi ;rsi=400000h shl esi,9 ;rsi=CW_USEDEFAULT push rbx push rbx push 240 push 410 push rsi push rsi sub esp,20h invoke CreateWindowEx,WS_EX_CLIENTEDGE,edi,edi,\ WS_OVERLAPPEDWINDOW or WS_VISIBLE lea edi,msg @@: invoke GetMessage,edi,0,0,0 invoke TranslateMessage,edi invoke DispatchMessage,edi jmp @b WinMain endp WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM local editCls:WNDCLASSEX mov hWnd,rcx cmp edx,WM_CREATE je wmCREATE cmp edx,WM_DESTROY je wmDESTROY wmDEFAULT:leave jmp DefWindowProc wmDESTROY:invoke ExitProcess,0 wmCREATE:lea edi,editCls mov dword ptr [rdi+WNDCLASSEX.cbSize],sizeof WNDCLASSEX mov esi,offset ctlClsNameEdit+3;ClsName="EDIT" invoke GetClassInfoEx,0,esi,rdi sub esi,3 mov rax,[rdi+WNDCLASSEX.lpfnWndProc] mov wndProcAddr,rax mov dword ptr [rdi+WNDCLASSEX.lpfnWndProc],offset edit1_procedure mov dword ptr [rdi+WNDCLASSEX.hInstance],IMAGE_BASE mov [rdi+WNDCLASSEX.lpszClassName],rsi;OFFSET ctlClsNameEdit ClsName="HEXEDIT" invoke RegisterClassEx,edi mov ebx,5 mov edi,20 @@: push 0;rbx push IMAGE_BASE push rbx push hWnd push 24 push 300 push rdi push 20 sub esp,20h invoke CreateWindowEx,WS_EX_CLIENTEDGE,esi,0,\ WS_CHILD + WS_VISIBLE + WS_BORDER mov [edit1H+8*rbx],rax invoke SendMessage,eax,EM_LIMITTEXT,15,0 ;limit to 15 chars add edi,30 dec ebx jns @b inc ebx invoke SetFocus,[edit1H+8*5] wmBYE: leave retn WndProc endp edit1_procedure proc hWnd:HWND, uMsg:QWORD, wParam:WPARAM, lParam:LPARAM push rbp mov ebp,esp sub esp,30h mov hWnd,rcx mov uMsg,rdx mov wParam,r8 mov lParam,r9 cmp edx,WM_KEYDOWN je edit1_wmKEYDOWN cmp edx,WM_CHAR jne @f edit1_wmCHAR: cmp r8b,VK_BACK ;compare with virtual key BACKSPACE je @f cmp r8b,'0' ;compare with ascii 0 jb edit1_wmBYE cmp r8b,'9' ;compare with ascii 9 jbe @f and r8b,-33 ;so our DL become big letter cmp r8b,'A' ;compare with ascii A jb edit1_wmBYE cmp r8b,'F' ;compare with ascii F ja edit1_wmBYE ;something else @@: mov [esp+20h],r9;lParam mov r9,r8;wParam mov r8,rdx;uMsg mov rdx,rcx;hWnd invoke CallWindowProc,wndProcAddr jmp edit1_wmBYE edit1_wmKEYDOWN: cmp r8b,VK_RETURN ;compare with virtual key RETURN je wmKEYDOWN_VK_RETURN cmp r8b,VK_TAB jne @b ;----------------------------------------------------- wmKEYDOWN_VK_TAB:invoke GetKeyState,VK_SHIFT or eax,eax;test eax,0x80000000 js VK_TAB_PREV;jne VK_TAB_PREV VK_TAB_NEXT:invoke GetWindow,hWnd,GW_HWNDNEXT;=2 or eax,eax jne VK_TAB_BYE xor edx,edx;GW_HWNDFIRST=0 jmp @f VK_TAB_PREV:invoke GetWindow,hWnd,GW_HWNDPREV;=3 test eax,eax jne VK_TAB_BYE mov edx,GW_HWNDLAST;=1 @@: invoke GetWindow,hWnd VK_TAB_BYE:mov ecx,eax jmp @f wmKEYDOWN_VK_RETURN:mov r8d,offset ClassName mov edx,offset edit1Txt1 invoke MessageBox,,,,MB_OK mov rcx,hWnd @@: invoke SetFocus edit1_wmBYE:leave ret edit1_procedure endp ClassName db 'Win64 Iczelion''s lesson #22: Superclassing Demo',0 ctlClsNameEdit db 'HEXEDIT',0 edit1Txt1 db 'A simple HEX edit control!',0 wndProcAddr dq ? edit1H dq 6 dup(?) end
Разбор полетовПрограмма создаст простое окно с "измененными" окнами ввода в своей клиентской области. Окна ввода будут принимать только шестнадцатиричные числа. Фактически, это адаптированный пример с сабклассингом. Программа стартует как обычно, а самое интересное происходит, когда создается основное окно: Код (ASM): wmCREATE: mov wc.cbSize,sizeof WNDCLASSEX invoke GetClassInfoEx,NULL,addr EditClass,addr wc Сначала мы заполним данными класса, который мы хотим суперклассировать, в нашем случае это класс edit'а. Помните, что вы должны установить параметр структуры WNDCLASSEX, перед тем, как вызвать GetClassInfoEx, в противном случае она будет заполнена неверно. После вызова GetClassInfoEx у нас будет иметься вся необходимая для создания нового класса информация. Код (ASM): 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 и lpszClassName. Вы должны заменить старое значение hInstance на дескриптор вашей программы, а также выбрать имя для нового класса. Код (ASM): invoke RegisterClassEx, addr wc Когда все готово, регистрируйте новый класс. Вы получите новый класс, обладающий некоторыми характеристиками старого. Код (ASM): 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овая процедура окна, заменившая старую, выполняет роль фильтра. Фактически, это работает точно также, как и в примере с сабклассингом, только вам не нужно выполнять лишнюю работу. Вставлен кусок кода, который обрабатывает нажатия на Tab, чтобы сделать пример более полезным для вас. Обычно, если вы помещаете контролы на диалоговое окно, его внутренний менеджер сам обрабатывает нажатия на клавиши навигации. Увы, но это недоступно, когда вы помещаете контролы на обычное окно. Вам следует сабклассировать их, чтобы нажатия на Tab обрабатывались. В нашем примере нам нет нужны сабклассировать контролы по одному, так как мы уже суперклассировали, поэтому можем реализовать "центральный менеджер навигации контролов". Код (ASM): .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, только передвигает фокус окна назад.
Глава сорок вторая. Братец Кролик и иконки в system tray В этой главе мы узнаем, как помещать иконки в system tray и как создавать/использовать всплывающее меню. Скачайте пример здесь.Теория ― МАТЬ СКЛЕРОЗАSystem tray ― это прямоугольная область панели задач, в которой располагаются несколько иконок. Скорее всего, вы обнаружите там как минимум цифровые часы. Вы можете самостоятельно помещать иконки в system tray. Далее приводятся шаги, которые нужно для этого выполнить: Заполните структуру NOTIFYICONDATA, Код (ASM): NOTIFYICONDATA STRUCT cbSize DWORD ?,? hWnd QWORD ? uID DWORD ? uFlags DWORD ? uCallbackMessage DWORD ?,? hIcon QWORD ? szTip BYTE 64 dup (?) NOTIFYICONDATA ENDS содержащую следующие поля: cbSize ― размер данной структуры. hwnd ― дескриптор окна, которое будет получать уведомление, когда над иконкой в tray'e произойдёт событие мыши. uID ― константа, используемая в качестве идентификатора иконки. Вы сами выбираете значение этой константе. В случае, если вы поместили в system tray несколько иконок, вы сможете узнать, над какой именно из них произошло событие мыши. uFlags ― указывает, какие поля данной структуры заполнены NIF_ICON Поле hIcon заполнено. NIF_MESSAGE Поле uCallbackMessage заполнено. NIF_TIP Поле szTip заполнено. uCallbackMessage ― пользовательское сообщение, которое Windows отошлёт указанному в поле hwnd окну, в случае, когда над иконкой произойдёт событие мыши. Сообщение вы создаете сами. hIcon ― дескриптор иконки, которую вы хотите поместить в system tray. szTip ― 64-байтовый массив, содержащий строку для использования в качестве всплывающей подсказки к иконке. Вызовите Shell_NotifyIcon, определённую в shell32.inc. Данная функция имеет следующий прототип: Код (C): BOOL Shell_NotifyIcon( _In_ DWORD dwMessage, _In_ PNOTIFYICONDATA lpdata ); dwMessage ― это тип сообщения, которое нужно отправить оболочке. NIM_ADD Добавляет иконку в system tray. NIM_DELETE Удаляет иконку из system tray. NIM_MODIFY Изменяет иконку в system tray. lpdata ― это указатель на корректно заполненную структуру NOTIFYICONDATA. Если вы хотите добавить иконку в system tray, используйте сообщение NIM_ADD, если хотите удалить иконку, применяйте NIM_DELETE. Вот, собственно, и всё. Но чаще всего просто поместить иконку в system tray недостаточно. Вам нужно как-то реагировать на событий мыши, происходящие над этой иконкой. Это можно сделать, обрабатывая сообщение, указанное в поле uCallbackMessage структуры NOTIFYICONDATA. Это сообщение содержит следующие значения в wParam и lParam: wParam содержит ID иконки. Это то же самое значение, что вы поместили в поле uID структуры NOTIFYICONDATA. lParam Младшее слово содержит сообщение мыши. Например, если пользователь сделал правый щелчок по иконке, то lParam будет содержать WM_RBUTTONDOWN. Обычно иконка в system tray показывает всплывающее меню при правом щелчке по ней. Этого можно добиться, если сначала создать само всплывающее меню, а затем вызывать TrackPopupMenu для его отображения. Шаги приведены ниже: Создайте всплывающее меню, вызвав CreatePopupMenu. Эта функция создаёт пустое меню, и при успешном создании возвращает его дескриптор в eax. Добавьте пункты в меню с помощью AppendMenu, InsertMenu или InsertMenuItem. Когда вам будет нужно отобразить всплывающее меню на месте курсора мыши, вызовите GetCursorPos, чтобы узнать текущие координаты курсора, а затем вызовите TrackPopupMenu, чтобы вывести меню на экран. Когда пользователь щёлкнет на одном из пунктов меню, Windows отправит сообщение WM_COMMAND вашей оконной процедуре, точно так же, как и при работе с обычным меню. Внимание: остерегайтесь следующих проблем, часто возникающих при работе со всплывающими меню. Когда меню отображено на экране, щелчок вне меню не приводит к его немедленному исчезновению. Это происходит потому, что окно, которое будет получать уведомления от меню, ДОЛЖНО быть на переднем плане. Просто вызовите SetForegroundWindow, чтобы исправить эту проблему. После вызова SetForegroundWindow вы обнаружите, что в первый раз всплывающее меню сработает нормально, но при последующем появлении оно будет отображаться, а затем тут же исчезать. Как написано в MSDN, это сделано "намеренно". Необходимо переключить задачу на программу, являющуюся владельцем иконки в system tray. Этого можно добиться, отправив любое сообщение окну вашей программы. Но только используйте PostMessage, а не SendMessage!
ПРАКТИКА ― СЕСТРА ШИЗОФРЕНИИВариант #1 Код (ASM): include win64a.inc include shell32.inc includelib shell32.lib IMAGE_BASE equ 400000h WM_SHELLNOTIFY equ WM_USER+5 IDI_TRAY equ 0 IDM_RESTORE equ 1000 IDM_EXIT equ 1010 .code WinMain proc local msg:MSG push rbp mov ebp,esp sub esp,sizeof MSG xor ebx,ebx mov esi,IMAGE_BASE mov ecx,offset FileName call LoadCursorFromFile mov note.hIcon,rax mov edi,offset ClassName push rax ;hIconSm push rdi ;lpszClassName push rbx ;lpszMenuName push COLOR_WINDOW;hbrBackground push 10005h ;hCursor push rax ;hIcon push rsi ;hInstance push rbx ;cbClsExtra & cbWndExtra db 68h dd WndProc ;lpfnWndProc push sizeof WNDCLASSEX;cbSize & style mov ecx,esp ;addr WNDCLASSEX call RegisterClassEx push rbx push rsi ;rsi=400000h shl esi,9 ;rsi=CW_USEDEFAULT push rbx push rbx push 200 push 350 push rsi push rsi mov r9d,WS_OVERLAPPEDWINDOW or WS_VISIBLE mov r8,rdi ;offset ClassName mov edx,edi ;offset ClassName xor ecx,ecx sub esp,20h call CreateWindowEx lea edi,msg @@: mov ecx,edi xor edx,edx xor r8d,r8d xor r9d,r9d call GetMessage mov ecx,edi call DispatchMessage jmp @b WinMain endp WndProc proc hWnd:QWORD, uMsg:QWORD, wParam:QWORD, lParam:QWORD local pt:POINT push rbp mov ebp,esp sub esp,(40h+sizeof POINT+15)and(-16) mov hWnd,rcx mov wParam,r8 cmp edx,WM_DESTROY je wmDESTROY cmp edx,WM_CREATE je wmCREATE cmp edx,WM_SHELLNOTIFY je wmSHELLNOTIFY cmp edx,WM_COMMAND je wmCOMMAND cmp edx,WM_SIZE je wmSIZE leave jmp DefWindowProc wmDESTROY:mov rcx,hPopupMenu call DestroyMenu xor ecx,ecx call ExitProcess wmCREATE:mov note.hWnd,rcx;rcx=hWnd call CreatePopupMenu mov hPopupMenu,rax mov r9d,offset RestoreString mov r8d,IDM_RESTORE xor edx,edx;mov edx,MF_STRING mov ecx,eax;hPopupMenu call AppendMenu mov r9d,offset ExitString mov r8d,IDM_EXIT xor edx,edx;mov edx,MF_STRING mov rcx,hPopupMenu call AppendMenu jmp wmBYE wmSHELLNOTIFY:or r8,r8;.if wParam==IDI_TRAY jne wmBYE cmp r9,WM_RBUTTONDOWN jne @f wmSHELLNOTIFY_WM_RBUTTONDOWN: lea ecx,pt call GetCursorPos mov rcx,hWnd call SetForegroundWindow mov [rsp+30h],rbx;0 mov rax,hWnd mov [rsp+28h],rax mov [rsp+20h],rbx;0 mov r9d,pt.y mov r8d,pt.x mov edx,TPM_RIGHTALIGN mov rcx,hPopupMenu call TrackPopupMenu xor r9d,r9d xor r8d,r8d mov edx,WM_NULL mov rcx,hWnd call PostMessage jmp wmBYE @@: cmp r9,WM_LBUTTONDBLCLK jne wmBYE wmSHELLNOTIFY_WM_LBUTTONDBLCLK: xor r9d,r9d;0 mov r8d,IDM_RESTORE mov edx,WM_COMMAND mov rcx,hWnd call SendMessage jmp wmBYE wmCOMMAND:or r9,r9;.if lParam==0 message is not from control jnz wmBYE mov edx,offset note mov ecx,NIM_DELETE call Shell_NotifyIcon mov rcx,hWnd cmp word ptr wParam,IDM_RESTORE jne @f wmCOMMAND_IDM_RESTORE: mov edx,SW_RESTORE call ShowWindow jmp wmBYE @@: call DestroyWindow jmp wmBYE wmSIZE: cmp r8d,SIZE_MINIMIZED;.if wParam==SIZE_MINIMIZED jnz wmBYE xor edx,edx;mov edx,SW_HIDE call ShowWindow mov edx,offset note xor ecx,ecx;mov ecx,NIM_ADD call Shell_NotifyIcon wmBYE: leave retn WndProc endp ;--------------------------------------- RestoreString db "&Restore",0 ExitString db "E&xit Program",0 FileName db "..\Images\Cursor.cur",0 note label NOTIFYICONDATA dd sizeof NOTIFYICONDATA,0 dq ? dd IDI_TRAY dd NIF_ICON+NIF_MESSAGE+NIF_TIP dd WM_SHELLNOTIFY,0 dq ? ClassName db 'Win64 Iczelion''s lesson #23-1: TrayIcon Demo' db 64-(lengthof ClassName) dup(0) hPopupMenu dq ? end Вариант #2 Код (ASM): include win64a.inc include shell32.inc includelib shell32.lib IMAGE_BASE equ 400000h WM_SHELLNOTIFY equ WM_USER+5 IDI_TRAY equ 0 IDM_SHOWHIDE equ 100 IDM_EXIT equ 101 .code WinMain proc local msg:MSG push rbp mov ebp,esp sub esp,sizeof MSG xor ebx,ebx mov esi,IMAGE_BASE mov ecx,offset FileName call LoadCursorFromFile mov note.hIcon,rax mov edi,offset ClassName push rax ;hIconSm push rdi ;lpszClassName push rbx ;lpszMenuName push COLOR_WINDOW;hbrBackground push 10005h ;hCursor push rax ;hIcon push rsi ;hInstance push rbx ;cbClsExtra & cbWndExtra db 68h dd WndProc ;lpfnWndProc push sizeof WNDCLASSEX;cbSize & style mov ecx,esp ;addr WNDCLASSEX call RegisterClassEx push rbx push rsi ;rsi=400000h shl esi,9 ;rsi=CW_USEDEFAULT push rbx push rbx push 200 push 350 push rsi push rsi mov r9d,WS_OVERLAPPEDWINDOW or WS_VISIBLE mov r8,rdi ;offset ClassName mov edx,edi ;offset ClassName xor ecx,ecx sub esp,20h call CreateWindowEx lea edi,msg @@: mov ecx,edi xor edx,edx xor r8d,r8d xor r9d,r9d call GetMessage mov ecx,edi call DispatchMessage jmp @b WinMain endp WndProc proc hWnd:QWORD, uMsg:QWORD, wParam:QWORD, lParam:QWORD local pt:POINT push rbp mov ebp,esp sub esp,(40h+sizeof POINT+15)and(-16) mov hWnd,rcx mov wParam,r8 cmp edx,WM_DESTROY je wmDESTROY cmp edx,WM_CREATE je wmCREATE cmp edx,WM_SHELLNOTIFY je wmSHELLNOTIFY cmp edx,WM_COMMAND je wmCOMMAND cmp edx,WM_SYSCOMMAND je wmSYSCOMMAND defwndproc:leave jmp DefWindowProc wmDESTROY:mov edx,offset note mov ecx,NIM_DELETE call Shell_NotifyIcon mov rcx,hPopupMenu call DestroyMenu xor ecx,ecx call ExitProcess wmCOMMAND:or r9,r9;.if lParam==0 message is not from control jnz wmBYE cmp word ptr wParam,IDM_SHOWHIDE je showhide cmp word ptr wParam,IDM_EXIT jne wmBYE @@: call DestroyWindow jmp wmBYE wmSYSCOMMAND:cmp r8,SC_MINIMIZE; when user presses "minimize" button, main window jne defwndproc; should be hidden showhide:xor showflag,1 mov edx,showflag lea edx,[rdx+rdx*4] call ShowWindow jmp wmBYE wmSHELLNOTIFY:or r8,r8;.if wParam==IDI_TRAY WM_SHELLNOTIFY handler - here we handle actions jne wmBYE; like clicking on our icon cmp r9,WM_LBUTTONDOWN je showhide cmp r9,WM_RBUTTONDOWN jne wmBYE wmSHELLNOTIFY_WM_RBUTTONDOWN: lea ecx,pt call GetCursorPos mov rcx,hWnd call SetForegroundWindow mov [rsp+30h],rbx;0 mov rax,hWnd mov [rsp+28h],rax mov [rsp+20h],rbx;0 mov r9d,pt.y mov r8d,pt.x mov edx,TPM_RIGHTALIGN mov rcx,hPopupMenu call TrackPopupMenu xor r9d,r9d xor r8d,r8d mov edx,WM_NULL mov rcx,hWnd call PostMessage jmp wmBYE wmCREATE:mov note.hWnd,rcx;rcx=hWnd mov edx,offset note xor ecx,ecx;NIM_ADD=0 call Shell_NotifyIcon call CreatePopupMenu mov hPopupMenu,rax mov r9d,offset szShowHide mov r8d,IDM_SHOWHIDE xor edx,edx;MF_STRING=0 mov ecx,eax;hPopupMenu call AppendMenu mov r9d,offset szExit mov r8d,IDM_EXIT xor edx,edx;MF_STRING=0 mov rcx,hPopupMenu call AppendMenu wmBYE: leave retn WndProc endp ;--------------------------------------- szShowHide db "&Show/Hide",0 szExit db "E&xit",0 FileName db "..\Images\Cursor.cur",0 showflag dd 1 ;if main window is visible showflag=1 note label NOTIFYICONDATA dd sizeof NOTIFYICONDATA,0 dq ? dd IDI_TRAY dd NIF_ICON+NIF_MESSAGE+NIF_TIP dd WM_SHELLNOTIFY,0 dq ? ClassName db 'Win64 Iczelion''s lesson #23-2: TrayIcon Demo' db 64-(lengthof ClassName) dup(0) hPopupMenu dq ? end Вариант #3. Анимация в трееrc-файл Код (C): #define icon1 1 #define icon2 2 #define icon3 3 #define icon4 4 #define icon5 5 #define icon6 6 #define icon7 7 #define icon8 8 #define icon9 9 #define icon10 10 #define icon11 11 icon1 ICON "..\\Images\\01.ico" icon2 ICON "..\\Images\\02.ico" icon3 ICON "..\\Images\\03.ico" icon4 ICON "..\\Images\\04.ico" icon5 ICON "..\\Images\\05.ico" icon6 ICON "..\\Images\\06.ico" icon7 ICON "..\\Images\\07.ico" icon8 ICON "..\\Images\\08.ico" icon9 ICON "..\\Images\\09.ico" icon10 ICON "..\\Images\\10.ico" icon11 ICON "..\\Images\\11.ico"
asm-файл Код (ASM): include win64a.inc include shell32.inc includelib shell32.lib IMAGE_BASE equ 400000h WM_SHELLNOTIFY equ WM_USER+5 IDI_TRAY equ 0 IDM_SHOWHIDE equ 100 IDM_EXIT equ 101 .code WinMain proc local msg:MSG push rbp mov ebp,esp sub esp,sizeof MSG xor ebx,ebx mov esi,IMAGE_BASE mov ecx,offset FileName call LoadCursorFromFile mov note.hIcon,rax mov edi,offset ClassName push rax ;hIconSm push rdi ;lpszClassName push rbx ;lpszMenuName push COLOR_WINDOW;hbrBackground push 10005h ;hCursor push rax ;hIcon push rsi ;hInstance push rbx ;cbClsExtra & cbWndExtra db 68h dd WndProc ;lpfnWndProc push sizeof WNDCLASSEX;cbSize & style mov ecx,esp ;addr WNDCLASSEX call RegisterClassEx push rbx push rsi ;rsi=400000h shl esi,9 ;rsi=CW_USEDEFAULT push rbx push rbx push 200 push 408 push rsi push rsi mov r9d,WS_OVERLAPPEDWINDOW or WS_VISIBLE mov r8,rdi ;offset ClassName mov edx,edi ;offset ClassName xor ecx,ecx sub esp,20h call CreateWindowEx lea edi,msg @@: mov ecx,edi xor edx,edx xor r8d,r8d xor r9d,r9d call GetMessage mov ecx,edi call DispatchMessage jmp @b WinMain endp WndProc proc hWnd:QWORD, uMsg:QWORD, wParam:QWORD, lParam:QWORD local ps:PAINTSTRUCT push rbp mov ebp,esp sub esp,(40h+sizeof PAINTSTRUCT+20h+15)and(-16) mov hWnd,rcx cmp edx,WM_DESTROY je wmDESTROY cmp edx,WM_CREATE je wmCREATE cmp edx,WM_TIMER je wmTIMER cmp edx,WM_PAINT je wmPAINT defwndproc:leave jmp DefWindowProc wmDESTROY:xor edx,edx call KillTimer mov edx,offset note mov ecx,NIM_DELETE call Shell_NotifyIcon xor ecx,ecx call ExitProcess wmPAINT:lea edx,ps call BeginPaint mov r9,note.hIcon mov r8d,100 mov edx,330 mov ecx,eax call DrawIcon lea edx,ps mov rcx,hWnd call EndPaint jmp wmBYE wmTIMER:mov rdx,index inc edx cmp dl,11 jb @f xor edx,edx @@: mov index,rdx mov rdx,[hIcon2+rdx*8] mov note.hIcon,rdx mov edx,offset note mov ecx,NIM_MODIFY call Shell_NotifyIcon mov r8d,TRUE xor edx,edx mov rcx,hWnd call InvalidateRect;send WM_PAINT jmp wmBYE wmCREATE:mov note.hWnd,rcx;rcx=hWnd mov edx,offset note xor ecx,ecx;NIM_ADD=0 call Shell_NotifyIcon mov ebx,11 @@: mov qword ptr [rsp+28h],0 mov r9d,16 mov [rsp+20h],r9 mov r8d,IMAGE_ICON mov edx,ebx mov ecx,IMAGE_BASE call LoadImage mov hIcon2[rbx*8-8],rax dec ebx jnz @b xor r9d,r9d mov r8d,500 xor edx,edx mov rcx,hWnd call SetTimer wmBYE: leave retn WndProc endp ;--------------------------------------- FileName db "..\Images\Cursor.cur",0 note label NOTIFYICONDATA dd sizeof NOTIFYICONDATA,0 dq ? dd IDI_TRAY dd NIF_ICON+NIF_MESSAGE+NIF_TIP dd WM_SHELLNOTIFY,0 dq ? ClassName db 'Win64 Iczelion''s lesson #23-2: TrayIcon Animation' db 64-(lengthof ClassName) dup(0) index dq 0 hIcon2 dq 12 dup(?) end
Разбор полётовПрограмма отобразит на экране обычное окно. По нажатию кнопки "Свернуть" оно свернётся до иконки в system tray. По двойному щелчку по иконке программа восстановит своё окно и удалит иконку из system tray. По правому щелчку будет выведено всплывающее меню, из которого можно восстановить программу или выйти из неё. Код (ASM): wmCREATE: invoke CreatePopupMenu mov hPopupMenu,rax invoke AppendMenu,hPopupMenu,MF_STRING,IDM_RESTORE,addr RestoreString invoke AppendMenu,hPopupMenu,MF_STRING,IDM_EXIT,addr ExitString Когда будет создано главное окно, также создастся всплывающее меню, к которому затем будут добавлены два пункта. Функция AppendMenu имеет следующий синтаксис: Код (C): BOOL WINAPI AppendMenu( _In_ HMENU hMenu,//дескриптор меню, к которому вы хотите добавить пункт _In_ UINT uFlags,/* информирует Windows о добавляемом пункте меню - изображение ли это, строка или отрисовываемый владельцем объект; включен ли он, неопределён или отключен, и так далее. В нашем случае мы используем флаг MF_STRING, который означает, что пункт меню - это строка */ _In_ UINT_PTR uIDNewItem,/* ID пункта меню. Это значение определяется пользователем, и используется для обращения к пункту меню */ _In_opt_ LPCTSTR lpNewItem /* хранит содержание пункта меню, в зависимости от значения поля uFlags. Так как мы указали MF_STRING в поле uFlags, то lpNewItem должен содержать указатель на строку для отображения в пункте меню */ ); После того, как всплывающее меню создано, главное окно будет терпеливо ждать до тех пор, пока пользователь не нажмет на кнопку "Свернуть". Когда окно сворачивается, оно получает сообщение WM_SIZE со значением SIZE_MINIMIZED в wParam. Код (ASM): wmSIZE: .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. После всего этого поместим в поле szTip текст, который мы хотим видеть в качестве всплывающей подсказки к иконке. Мы скрываем главное окно, чтобы создать эффект "сворачивания в иконку". Затем мы вызываем Shell_NotifyIcon с сообщением NIM_ADD, чтобы добавить иконку в system tray. Теперь наше главное окно скрыто, а иконка успешно помещена в system tray. Если вы наведёте на неё курсор, то увидите подсказку с текстом, который вы поместили в поле szTip. Далее, если вы дважды щелкните по иконке, восстановится главное окно, а сама иконка исчезнет. Код (ASM): wmSHELLNOTIFY: .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. Однако мы хотим отобразить всплывающее меню в точке, где сейчас расположен курсор мыши, с помощью функции TrackPopupMenu, которой требуются именно абсолютные координаты. Поэтому мы просто используем координаты, полученные от GetCursorPos. TrackPopupMenu имеет следующий синтаксис: Код (C): BOOL WINAPI TrackPopupMenu( _In_ HMENU hMenu, // дескриптор всплывающего меню, которое нужно отобразить _In_ UINT uFlags, /* указывает опции отображения. Например, как располагать меню относительно указанных ниже координат, и какая из кнопок мыши используется для отслеживания меню. В нашем примере мы используем флаг TPM_RIGHTALIGN, чтобы разместить меню слева от указанной точки */ _In_ int x, // указывают местоположение меню в абсолютных координатах _In_ int y, _In_ int nReserved, // должно содержать NULL _In_ HWND hWnd, // дескриптор окна, которое будет получать сообщения от меню _In_opt_ const RECT *prcRect /* прямоугольная область экрана, щелчки в пределах которой НЕ будут приводить к исчезновению меню. Обычно сюда помещается NULL, чтобы меню исчезало при любом щелчке вне его */ ); Когда пользователь дважды щёлкнет по иконке, мы отправим нашему окну сообщение WM_COMMAND с указанием IDM_RESTORE, чтобы создать иллюзию выбора пользователем пункта "Восстановить" в меню, и таким образом восстановить окно, а также удалить иконку из system tray. Чтобы иметь возможность получать сообщения двойного щелчка, главное окно должно иметь стиль CS_DBLCLKS. Код (ASM): 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
Глава сорок третья. Братец Кролик и Windows-хуки В этой главе мы изучим хуки. Это очень мощная техника. С их помощью вы сможете вмешиваться в другие процессы и иногда менять их поведение. Скачайте пример здесь.Теория ― МАТЬ СКЛЕРОЗАХуки Windows можно считать одной из самых мощных техник. С их помощью вы можете перехватывать события, которые случатся внутри созданного вами или кем-то другим процесса. Перехватывая что-либо, вы сообщаете Windows о фильтрующей функции, также называющейся функцией перехвата, которая будет вызываться каждый раз, когда будет происходить интересующее вас событие. Есть два вида хуков: локальные и удаленные. Локальные хуки перехватывают события, которые случаются в процессе, созданном вам. Удаленные хуки перехватывают события, которые случаются в других процессах. Есть два вида удаленных хуков: тредоспециализированные перехватывают события, которые случатся в определенном треде другого процесса. То есть, такой хук нужен вам, когда необходимо наблюдать за процессами, происходящими в определенном треде какого-то процесса. системные перехватывают все события, предназначенные для всех тредов всех процессов в системе. При установке хуков, помните, что они оказывают отрицательное воздействие на быстродействие системы. Особенно в этом отличаются системные. Так как все требуемые события будут проходить через вашу функцию, ваша система может значительно потерять в быстродействии. Поэтому, если вы используете системный хук, вам следует использовать их только тогда, когда вам это действительно нужно. Также, существует высокая вероятность того, что другие процессы могут зависнуть, если что-нибудь неправильно в вашей функции. Помните: вместе с силой приходит ответственность. Вы должны понимать, как работают хуки, чтобы использовать их эффективно. Когда вы создаете хук, Windows создает в памяти структуры данных, которая содержит информацию о хуке, и добавляет ее в связанный список уже существующих хуков. Новый хук добавляется перед всеми старыми хуками. Когда случается событие, то если вы установили локальный хук, вызывается фильтрующая функция в вашем процессе, поэтому тут все просто. Hо если вы установили удаленный ху, система должна вставить код хук-процедуры в адресное пространство другого процесса. Система может сделать это только, если функция находится в DLL. Таким образом, если вы хотите использовать удаленный хук, ваша хук-процедура должна находиться в DLL. Из этого правила есть два исключения: журнально-записывающие и журнально-проигрывающие хуки. Хук-процедуры для этих типов хуков должны находиться в треде, который инсталлировал хуки. Причина этого кроется в том, что оба хука имеют дело с низкоуровневым перехватом хардварных входных событий. Эти события должны быть записаны/проиграны в том порядке, в котором они произошли. Если код такого хука находится в DLL, входные события могут быть "разбросаны" по нескольким тредам, что делает невозможным установления точной их последовательности. решение: процедуры таких хуков должна быть в одном треде, то есть в том треде, который устанавливает хуки. Существует 14 типов хуков: WH_CALLWNDPROC ― хук вызывается при вызове SendMessage. WH_CALLWNDPROCRET ― хук вызывается, когда возвращается SendMessage. WH_GETMESSAGE ― хук вызывается, когда вызывается GetMessage или PeekMessage. WH_KEYBOARD ― хук вызывается, когда GetMessage или PeekMessage получают WM_KEYUP или 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, имеющую следующий синтаксис: Код (C): HHOOK WINAPI SetWindowsHookEx( _In_ int idHook, /* одно из значений, перечисленных выше (WH_MOUSE, WH_KEYBOARD и тому подобное) */ _In_ HOOKPROC lpfn, /* адрес хук-процедуры, которая будет вызвана для обработки сообщений от хука. Если хук является удаленным, он должен находиться в DLL. Если нет, то он должен быть внутри процесса */ _In_ HINSTANCE hMod, /* дескриптор DLL, в которой находится хук-процедура. Если хук локальный, тогда это значения должно быть равно NULL */ _In_ DWORD dwThreadId /* ID треда, на который вы хотите поставить хук. Этот параметр определяет является ли хук локальным или удаленным. Если этот параметр равен NULL, Windows будет считать хук системным и удаленным, который затрагивает все треды в системе. Если вы укажете ID одного из тредов вашего собственного процесса, хук будет локальным. Если вы укажете ID треда из другого процесса, то хук будет тредоспециализированным и удаленным. Из этого правила есть два исключения: WH_JOURNALRECORD и WH_JOURNALPLAYBACK ― это всегда локальные системные хуки, которым не нужно быть в DLL. Также WH_SYSMSGFILTER ― это всегда системный удаленный хук. Фактически он идентичен хуку WH_MSGFILTER при ThreadID равным 0 */ );
Если вызов успешен, он возвращает хэндл хука в eax. Если нет, возвращается NULL. Вы должны сохранить хэндл хука, чтобы снять его в дальнейшем. Вы можете деинсталлировать хук, вызвав UnhookWindowsHookEx, которая принимает только один параметр ― дескриптор хука, который нужно деинсталлировать. Если вызов успешен, он возвращает ненулевое значение в rax. Иначе он возвратит NULL. Хук-процедура будет вызываться каждый раз, когда будет происходить событие, ассоциированное с инсталлированным хуком. Например, если вы инсталлируете хук WH_MOUSE, когда происходит событие, связанное с мышью, ваша хук-процедура будет вызвана. Вне зависимости от типа установленного хука, хук-процедура всегда будет иметь один и тот же прототип: Код (ASM): Hookproc proto nCode:DWORD, wparam:DWORD, lparam:DWORD nCode задает код хука. wParam и lParam содержат дополнительную информацию о событие. Вместо HookProc будет имя вашей хук-процедуры. Вы можете назвать ее как угодно, главное чтобы ее прототип совпадал с вышеприведенным. Интерпретация nCode, wParam и lParam зависит от типа установленного хука, так же, как и возвращаемое хук-процедурой значение. Например: WH_CALLWNDPROC nCode может иметь значение HC_ACTION ― это означает, что окну было послано сообщение. wParam содержит посланное сообщение, если он не равен нулю, lрaram указывает на структуру CWPSTRUCT. возвращаемое значение: не используется, возвращайте ноль. WH_MOUSE nCode может быть равно HC_ACTION или HC_NOREMOVE. wParam содержит сообщение от мыши. lParam указывает на структуру MOUSEHOOKSTRUCT. возвращаемое значение: ноль, если сообщение должно быть обработано. 1, если сообщение должно быть пропущено. Вы должны обратиться к вашему справочнику по Win32 API за подробным описанием значение параметров и возвращаемых значений хука, который вы хотите установить. Теперь еще один нюанс относительно хук-процедуры. Помните, что хуки соединены в связанный список, причем в его начале стоит хук, установленный последним. Когда происходит событие, Windows вызовет только первый хук в цепи. Вызов следующего в цепи хука остается на вашей ответственности. Вы можете и не вызывать его, но вам лучше знать, что вы делаете. Как правило, стоит вызвать следующую процедуру, чтобы другие хуки также могли обработать событие. Вы можете вызвать следующий хук с помощью функции CallNextHookEx: Код (C): LRESULT WINAPI CallNextHookEx( _In_opt_ HHOOK hhk, /* дескриптор вашего хука. Функция использует этот хук для того, чтобы определить, какой хук надо вызвать следующим */ _In_ int nCode, _In_ WPARAM wParam, _In_ LPARAM lParam ); nCode, wParam и lParam - передают соответствующие параметры, полученные от Windows. Важная деталь относительно удаленных хуков: хук-процедура должна находиться в DLL, которая будет промэппирована в другой процесс. Когда Windows мэппирует DLL в другой процесс, секция данных мэппироваться не будет. То есть, все процессы разделяют одну копию секции кода, но у них будет своя личная копия секции кода DLL! Это может стать большим сюрпризом для непредупрежденного человека. Вы можете подумать, что при сохранении значения в переменную в секции данных DLL, это значение получать все процессы, загрузившие DLL в свое адресное пространство. Hа самом деле, это не так. В обычной ситуации, такое поведение правильно, потому что это создает иллюзию, что у каждого процесса есть отдельная копия DLL. Hо не тогда, когда это касается хуков Windows. Нам нужно, чтобы DLL была идентична во всех процессах, включая данные. решение: вы должны пометить секцию данных как разделяемую. Это можно сделать, указав атрибуты секции линкеру. Если речь идет о MASM'е, это делается так: Код (Text): /SECTION:, S Имя секции инициализированных данных '.data', а неинициализированных ― '.bss'. Например, если вы хотите скомпилировать DLL, которая содержит хук-процедуру, и вам нужно, что секция неинициализированных данных разделялась между процессами, вы должны использовать следующую команду: Код (Text): link /section:.bss,S /DLL /SUBSYSTEM:WINDOWS атрибут 'S' отмечает, что секция разделяемая.ПРАКТИКА ― СЕСТРА ШИЗОФРЕНИИЕсть два модуля: один ― это основная программа с GUI'ем, а другая ― это DLL, которая устанавливает/снимает хук
файл makeDLL64.bat Код (Text): cls set masm64_path=\masm64\ set filename=tut_24a if exist %filename%.dll del %filename%.dll %masm64_path%bin\ml64.exe /c /Cp /I %masm64_path%include %filename%.asm || exit %masm64_path%bin\Link.exe %filename%.obj /LIBPATH:%masm64_path%lib ^ /SUBSYSTEM:WINDOWS /ENTRY:DllEntry /DLL /section:.bss,S ^ /stub:%masm64_path%bin\stubby.exe /DEF:%filename%.def || exit del %filename%.obj del %filename%.exp tut_24a.asm Код (ASM): include win64a.inc WM_MOUSEHOOK equ WM_USER+6 MOUSEHOOKSTRUCT STRUCT pt POINT <> hwnd QWORD ? wHitTestCode QWORD ? dwExtraInfo QWORD ? MOUSEHOOKSTRUCT ENDS .data? hInstance dq ? hHook dq ? hWnd dq ? .code DllEntry proc hInstDLL:QWORD, reason:QWORD, unused:QWORD mov hInstance,rcx mov eax,TRUE ret DllEntry Endp MouseProc proc nCode:QWORD,wParam:QWORD,lParam:QWORD push rbp mov ebp,esp sub rsp,20h mov lParam,r8 mov r9,r8;lParam mov r8,rdx;wParam mov edx,ecx;nCode mov rcx,hHook call CallNextHookEx mov rax,lParam mov rcx,[rax] call WindowFromPoint mov rcx,hWnd mov edx,WM_MOUSEHOOK mov r8,rax mov r9d,0 call PostMessage;,hWnd,WM_MOUSEHOOK,eax,0 xor eax,eax leave ret MouseProc endp InstallHook proc hwnd:QWORD push rbp mov ebp,esp sub esp,20h mov hWnd,rcx mov ecx,WH_MOUSE mov rdx,offset MouseProc mov r8,hInstance mov r9d,0 call SetWindowsHookEx;,WH_MOUSE,addr MouseProc,hInstance,NULL mov hHook,rax leave ret InstallHook endp UninstallHook proc push rbp mov ebp,esp sub esp,20h mov rcx,hHook call UnhookWindowsHookEx leave ret UninstallHook endp end tut_24a.def Код (Text): LIBRARY tut_24a EXPORTS MouseProc InstallHook UninstallHook asm2.bat Код (Text): cls set masm64_path=\masm64\ set filename=tut_24b if exist %filename%.exe del %filename%.exe %masm64_path%bin\RC /r %filename%.rc || exit %masm64_path%bin\ml64 /Cp /c /I"%masm64_path%Include" %filename%.asm || exit %masm64_path%bin\link /SUBSYSTEM:WINDOWS /LIBPATH:"%masm64_path%Lib" ^ /entry:WinMain %filename%.obj %filename%.res /LARGEADDRESSAWARE:NO ^ /ALIGN:16 /SECTION:.text,W ^ /BASE:0x400000 /STUB:%masm64_path%bin\stubby.exe || exit del %filename%.res del %filename%.obj tut_24b.asm Код (ASM): include win64a.inc include mymacros.asm includelib tut_24a.lib 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 IMAGE_BASE equ 400000h InstallHook proto :QWORD UninstallHook proto MouseProc proto :QWORD,:QWORD,:QWORD DlgFunc proto :QWORD,:QWORD,:QWORD,:QWORD .data HookFlag dq FALSE HookText db "&Hook",0 UnhookText db "&Unhook",0 template db "%lx",0 hHook dq ? .code WinMain proc sub esp,38h xor ebx,ebx mov [rsp+20h],rbx mov r9d,offset DlgFunc mov r8,rbx mov edx,IDD_MAINDLG mov ecx,IMAGE_BASE call DialogBoxParam;,hInstance,IDD_MAINDLG,NULL,addr DlgFunc,NULL xor ecx,ecx call ExitProcess WinMain endp DlgFunc proc hDlg:QWORD,uMsg:QWORD,wParam:QWORD,lParam:QWORD LOCAL hLib:QWORD LOCAL buffer[128]:byte LOCAL buffer1[128]:byte LOCAL rect:RECT push rbp mov ebp,esp sub esp,(38h+8+2*128+sizeof RECT+15)and(-16) mov hDlg,rcx mov wParam,r8 mov lParam,r9 cmp edx,WM_INITDIALOG je wmINITDIALOG cmp edx,WM_MOUSEHOOK je wmMOUSEHOOK cmp edx,WM_COMMAND je wmCOMMAND cmp edx,WM_COMMAND je wmCOMMAND cmp edx,WM_CLOSE je wmCLOSE xor eax,eax;FALSE leave retn wmCLOSE:cmp HookFlag,TRUE jnz @f call UninstallHook @@: xor edx,edx mov rcx,hDlg call EndDialog jmp wmBYE wmINITDIALOG:lea edi,rect mov edx,edi mov rcx,hDlg call GetWindowRect mov qword ptr [rsp+30h],SWP_SHOWWINDOW mov eax,[rdi+RECT.bottom] mov [rsp+28h],rax mov eax,[rdi+RECT.right] mov [rsp+20h],rax mov r9d,[rdi+RECT.top] mov r8d,[rdi+RECT.left] or edx,HWND_TOPMOST mov rcx,hDlg call SetWindowPos jmp wmBYE wmMOUSEHOOK:mov r9d,128 lea r8,buffer1 mov edx,IDC_HANDLE ;mov rcx,hDlg call GetDlgItemText mov r8,wParam mov edx,offset template lea ecx,buffer call wsprintf lea edx,buffer1 lea ecx,buffer call lstrcmpi or eax,eax;.if eax!=0 jz @f lea r8,buffer mov edx,IDC_HANDLE mov rcx,hDlg call SetDlgItemText @@: mov r9d,128 lea r8,buffer1 mov edx,IDC_CLASSNAME mov rcx,hDlg call GetDlgItemText mov r8d,128 lea edx,buffer mov rcx,wParam call GetClassName lea edx,buffer1 lea ecx,buffer call lstrcmpi or eax,eax;.if eax!=0 jz @f lea r8,buffer mov edx,IDC_CLASSNAME mov rcx,hDlg call SetDlgItemText @@: mov r9d,128 lea r8,buffer1 mov edx,IDC_WNDPROC mov rcx,hDlg call GetDlgItemText mov edx,GCL_WNDPROC mov rcx,wParam call GetClassLong mov r8,rax mov edx,offset template lea ecx,buffer call wsprintf lea edx,buffer1 lea ecx,buffer call lstrcmpi or eax,eax;.if eax!=0 jz wmBYE lea r8,buffer mov edx,IDC_WNDPROC mov rcx,hDlg call SetDlgItemText jmp wmBYE wmCOMMAND:cmp r9,rbx;cmp lParam,rbx;.if lParam!=0 jz wmBYE mov rax,r8;wParam mov edx,eax shr edx,16 or edx,edx;.if dx==BN_CLICKED jnz wmBYE and eax,0FFFFh cmp eax,IDC_EXIT jnz @f mov r9,rbx mov r8,rbx mov edx,WM_CLOSE mov rcx,hDlg call SendMessage jmp wmBYE @@: cmp HookFlag,rbx;FALSE jnz @f mov rcx,hDlg call InstallHook or eax,eax; .if eax!=NULL jz wmBYE mov HookFlag,TRUE mov r8d,offset UnhookText mov edx,IDC_HOOK mov rcx,hDlg call SetDlgItemText jmp wmBYE @@: call UninstallHook mov r8d,offset HookText mov edx,IDC_HOOK mov rcx,hDlg call SetDlgItemText mov HookFlag,rbx;FALSE mov r8,rbx mov edx,IDC_CLASSNAME mov rcx,hDlg call SetDlgItemText mov r8,rbx mov edx,IDC_HANDLE mov rcx,hDlg call SetDlgItemText mov r8,rbx mov edx,IDC_WNDPROC mov rcx,hDlg call SetDlgItemText wmBYE: leave mov eax,TRUE retn DlgFunc endp end tut_24b.rc Код (C): #define IDD_MAINDLG 101 #define IDC_CLASSNAME 1000 #define IDC_HANDLE 1001 #define IDC_WNDPROC 1002 #define IDC_HOOK 1004 #define IDC_EXIT 1005 #define IDC_STATIC -1 #define DS_MODALFRAME 0x80 #define WS_POPUP 0x80000000 #define WS_CAPTION 0xC00000 #define WS_SYSMENU 0x80000 #define ES_AUTOHSCROLL 0x80 #define ES_READONLY 0x800 IDD_MAINDLG DIALOG DISCARDABLE 0, 0, 229, 85 STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU CAPTION "Iczelion Tutorial #24: Mouse Hook Demo" FONT 8, "MS Sans Serif" BEGIN GROUPBOX "Window Information",IDC_STATIC,7,7,214,67 LTEXT "Class name:",IDC_STATIC,21,22,39,8 EDITTEXT IDC_CLASSNAME,69,20,139,12,ES_AUTOHSCROLL | ES_READONLY LTEXT "Handle:",IDC_STATIC,33,37,26,8 EDITTEXT IDC_HANDLE,69,36,77,12,ES_AUTOHSCROLL | ES_READONLY LTEXT "Window Proc:",IDC_STATIC,13,52,46,8 EDITTEXT IDC_WNDPROC,69,51,77,12,ES_AUTOHSCROLL | ES_READONLY DEFPUSHBUTTON "&Hook",IDC_HOOK,159,35,50,14 PUSHBUTTON "E&xit",IDC_EXIT,159,50,50,14 END
Разбор полётовПример отобразит диалоговое окно с тремя окнами ввода, которые будут заполнены именем класса, дескриптором окна и адресом процедуры окна, ассоциированное с окном под курсором мыши. Есть две кнопки - Hook и Exit. Когда вы нажимаете кнопку Hook, программа перехватывает сообщения от мыши и текст на кнопке меняется на Unhook. Когда вы двигаете курсор мыши над каким-либо окном, информация о нем отобразится в окне программы. Когда вы нажмете кнопку Unhook, программа уберет установленный hook. Основная программа использует диалоговое окно в качестве основного. Она определяет специальное сообщение - WM_MOUSEHOOK, которая будет использоваться между основной программой и DLL с хуком. Когда основная программа получает это сообщение, wParam содержит дескриптор окна, над которым находится курсор мыши. Конечно, это было сделано произвольно. Я решил слать дескриптор в wParam, чтобы было проще. Вы можете выбрать другой метод взаимодействия между основной программой и DLL с хуком. Код (ASM): .if HookFlag==FALSE invoke InstallHook,hDlg .if eax!=NULL mov HookFlag,TRUE invoke SetDlgItemText,hDlg,IDC_HOOK,addr UnhookText .endif Программа пользуется флагом, HookFlag, чтобы отслеживать состояние хука. Он равен FALSE, если хук не установлен, и TRUE, если установлен. Когда пользователь нажмет кнопку hook, программа проверяет, установлен ли уже хук. Если это так, она вызывает функцию InstallHook из DLL. Заметьте, что мы передаем дескриптор основного диалогового окна в качестве параметра функции, чтобы хук-DLL могла посылать сообщения WM_MOUSEHOOK верному окну, то есть нашему. Когда программа загружена, DLL с хуком также загружается. Фактически, DLL загружаются сразу после того, как программа оказывается в памяти. Входная функция DLL вызывается прежде, чем будет исполнена первая инструкция основной программы. Поэтому, когда основная программа запускается DLL и инициализируются. Помещаем следующий код во входную функцию хук-DLL: Код (ASM): .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 не измениться. Хук будет локальным, что нам не нужно. Код (ASM): 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аз, когда в системе случается мышиное событие, вызывается MouseProc (ваша хук-процедура). Код (ASM): 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, чтобы получить дескриптор окна, находящегося в указанной координате экрана. Заметьте, что мы используем структуру POINT, являющуюся членом структуры MOUSEHOOKSTRUCT, на которую указывает lParam, то есть координату текущего местонахождения курсора. После этого, мы посылаем дескриптор окна основной программы через сообщение WM_MOUSEHOOK. Вы должны помнить: вам не следует использовать SendMessage в хук-процедуре, так как это может вызвать зависание программы, поэтому рекомендуется использовать PostMessage. Структура MOUSEHOOKSTRUCT определена ниже: Код (ASM): MOUSEHOOKSTRUCT STRUCT DWORD pt pOINT <> //текущая координата курсора мыши hwnd DWORD ? /* дескриптор окна, которое получает сообщения от мыши. Это обычно окно под курсором мыши, но не всегда. Если окно вызывает SetCapture, сообщения от мыши будут перенаправлены этому окну. По этой причине не используют параметр hwnd этой структуры, а вызывают WindowFromPoint*/ wHitTestCode DWORD ? /* дополнительная информация о том, где находится курсор мыши */ dwExtraInfo DWORD ? /* дополнительная информация, ассоциированная с сообщением. Обычно это значение устанавливается с помощью вызова mouse_event и получаем его функцией GetMessageExtraInfo */ MOUSEHOOKSTRUCT ENDS Когда основное окно получает сообщение WM_MOUSEHOOK, оно использует дескриптор окна в wParam, чтобы получить информацию об окне. Код (ASM): wmMOUSEHOOK: 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 Чтобы избежать мерцания, мы проверяем, не идентичны ли текст в окнах ввода с текстом, который мы собираемся ввести. Если это так, то мы пропускаем этот этап. Мы получаем имя класса с помощью вызова GetClassName, адрес процедуры с помощью вызова GetClassLong со значением GCL_WNDPROC, а затем форматируем их в строки и помещаем в соответствующие окна ввода. Код (ASM): 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 и очищает содержимое окон ввода. Обратите внимание на опции линкера в makefile. Код (Text): Link /SECTION:.bss,S /DLL /DEF:$(NAME).def /SUBSYSTEM:WINDOWS Секции .bss помечается как разделяемая, чтобы все процессы разделяли секцию неинициализируемых данных хук-DLL. Без этой опции, ваша DLL функционировала бы неправильно.
Глава сорок четвертая. Братец Кролик и сплэш-экран Теперь, когда мы знаем, как использовать рисунок, мы можем применить его более творчески. Скачайте пример здесь.Теория ― МАТЬ СКЛЕРОЗАСплэш-экран ― это окно, у которого нет заголовка, нет системных кнопок, нет рамки, которое отображает рисунок на некоторое время и затем исчезает. Обычно сплэш-экран используется во время загрузки программы, чтобы отображать логотип программы или отвлечь внимание пользователя, пока программа делает длительную инициализацию. В этой главе мы создадим сплэш-экран. Первый шаг ― это прописать рисунок в файле ресурсов. Тем не менее, если это важно для вас, то загружать рисунок , который будет использоваться только один раз, и держать его в памяти, пока программа не будет закрыта, пустая трата ресурсов. Лучшим решением является ресурсовую DLL, которая будет содержать рисунок, и чьей целью является отображение сплэш-экрана. В этом случае вы сможете загрузить DLL, когда вам нужно отобразить сплэш-экран, и выгрузить ее, как только нужда в ней отпадает. Поэтому у нас будет два модуля: основная программа и сплэш-экран. Мы поместим рисунок в файл ресурсов DLL. Общая схема такова: Поместить рисунок в DLL как ресурс. Основная программа вызывает LoadLibrary, чтобы загрузить dll в память. Запускается входная функция DLL. Она создаст таймеp и установит время, в течении которого будет отображаться сплэш-экран. Затем она зарегистрирует и создаст окно без заголовка и бордера, после чего отобразит битмап в клиенсткой области. Когда закончится указанный период времени, сплэш-экран будет убран с экрана и контроль будет передан главной программе. Основная программа вызовет FreeLibrary, чтобы выгрузить DLL из памяти, а затем перейдет к выполнению того, к чему она предназначена. Мы детально проанализируем описанную последовательность действий.Загрузка/выгрузка DLLВы можете динамически загрузить DLL с помощью функции LoadLibrary, которая имеет следующий синтаксис: Код (C): HMODULE WINAPI LoadLibrary( _In_ LPCTSTR lpFileName ); Она принимает только один параметр: адрес имени DLL, который вы хотите загрузить в память. Если вызов пройдет успешно, он возвратит хэндл модуля DLL, в противном случае NULL. Чтобы выгрузить DLL, вызовите FreeLibrary: Код (C): BOOL WINAPI FreeLibrary( _In_ HMODULE hModule ); Она получает один параметр: дескриптор модуля DLL, которую вы хотите выгрузить.Как использовать таймеpВо-первых, мы должны создать таймер с помощью функции SetTimer: Код (C): UINT_PTR WINAPI SetTimer( _In_opt_ HWND hWnd, /* дескриптор окна, которое будет получать уведомительные сообщения от таймера. Этот параметр может быть равным NULL, если никакое окно не ассоциируется с таймером */ _In_ UINT_PTR nIDEvent, /* заданное пользователем значение, которое будет использоваться в качестве ID таймера */ _In_ UINT uElapse, // временной интервал в миллисекундах _In_opt_ TIMERPROC lpTimerFunc /* адрес функции, которая будет обрабатывать уведомительные сообщения от таймера. Если вы передает NULL, сообщения от таймера будут посылаться окну, указанному в параметре hWnd */ ); SetTimer возвращает ID таймера, если вызов прошел успешно, иначе она возвратит NULL. Поэтому лучше не использовать ноль в качестве ID таймера. Вы можете создать таймеp двумя путями: Если у вас есть окно и вы хотите, чтобы сообщения от таймера посылались окну, вы должны передать все четыре параметра SetTimer (lpTimerFunc должен быть равен NULL). Если у вас нет окна или вы не хотите обрабатывать сообщения таймера в процедуре окна, вы должны передать NULL функции вместо дескриптора окна. Вы также должны указать адрес функции таймера, которая будет обрабатывать его сообщения. В этой главе мы используем первый подход. Каждый раз за указанный вами временной интервал окну, ассоциированному с таймером, будет посылаться сообщение WM_TIMER. Например, если вы укажете 1000: ваше окно будет получать WM_TIMER каждую секунду. Когда вам больше не нужен таймеp, уничтожьте его с помощью KillTimer: Код (C): BOOL WINAPI KillTimer( _In_opt_ HWND hWnd, _In_ UINT_PTR uIDEvent );
ПРАКТИКА ― СЕСТРА ШИЗОФРЕНИИmakedll64.bat Код (Text): cls set masm64_path=\masm64\ set filename=tut_26a if exist %filename%.dll del %filename%.dll %masm64_path%bin\RC /r %filename%.rc || exit %masm64_path%bin\ml64.exe /c /Cp /I %masm64_path%include %filename%.asm || exit %masm64_path%bin\Link.exe %filename%.obj %filename%.res /LIBPATH:%masm64_path%lib ^ /SUBSYSTEM:WINDOWS /ENTRY:DllEntry /DLL /ALIGN:16 /section:.bss,S ^ /stub:%masm64_path%bin\stubby.exe /DEF:%filename%.def || exit del %filename%.obj del %filename%.exp del %filename%.res tut_26a.asm Код (ASM): OPTION DOTNAME option casemap:none include temphls.inc include win64.inc include kernel32.inc includelib kernel32.lib include user32.inc includelib user32.lib include gdi32.inc includelib gdi32.lib OPTION PROLOGUE:none OPTION EPILOGUE:none IMAGE_BASE equ 400000h .data BitmapName db "MySplashBMP",0 ClassName db "SplashWndClass",0 hBitMap dq ? TimerID dq ? hInstance dq ? .code DllEntry proc hInst:QWORD, reason:QWORD, reserved1:QWORD sub esp,28h cmp edx,DLL_PROCESS_ATTACH ; When the dll is loaded jnz @f mov hInstance,rcx call ShowBitMap @@: add esp,28h mov eax,TRUE retn DllEntry Endp ShowBitMap proc LOCAL msg:MSG push rbp mov ebp,esp sub esp,sizeof MSG xor ebx,ebx push 10029h ;hIconSm lea rdi,ClassName push rdi ;lpszClassName push rbx ;lpszMenuName push COLOR_WINDOW+1;hbrBackground push 10005h ;hCursor push 10029h ;hIcon push hInstance push rbx ;cbClsExtra & cbWndExtra lea rax,WndProc push rax ;lpfnWndProc push sizeof WNDCLASSEX;cbSize & style mov rcx,rsp ;addr WNDCLASSEX sub esp,20h call RegisterClassEx push rbx push hInstance push rbx push rbx push 250 push 250 push CW_USEDEFAULT push CW_USEDEFAULT mov r9d,WS_POPUP or WS_VISIBLE mov r8,rbx mov edx,edi xor ecx,ecx sub esp,20h call CreateWindowEx lea edi,msg @@: mov ecx,edi xor edx,edx mov r8,rbx mov r9,rbx call GetMessage or eax,eax jz @f mov ecx,edi call DispatchMessage jmp @b @@: leave retn ShowBitMap endp WndProc proc hWnd:QWORD,uMsg:QWORD,wParam:QWORD,lParam:QWORD LOCAL ps:PAINTSTRUCT LOCAL hMemoryDC:HDC LOCAL hOldBmp:QWORD LOCAL bitmap:BITMAP LOCAL DlgHeight:QWORD LOCAL DlgWidth:QWORD LOCAL DlgRect:RECT LOCAL DesktopRect:RECT push rbp mov ebp,esp sub esp,(20h+sizeof PAINTSTRUCT+sizeof BITMAP+2*(sizeof RECT)+4*32+15)and(-16) mov hWnd,rcx mov wParam,r8 mov lParam,r9 cmp edx,WM_DESTROY je wmDESTROY cmp edx,WM_CREATE je wmCREATE cmp edx,WM_TIMER je wmTIMER cmp edx,WM_PAINT je wmPAINT cmp edx,WM_LBUTTONDOWN je wmLBUTTONDOWN leave jmp DefWindowProc wmDESTROY:mov rcx,hBitMap jrcxz @f;.if hBitMap!=0 call DeleteObject @@: xor ecx,ecx call PostQuitMessage jmp wmBYE wmCREATE:lea rdx,DlgRect call GetWindowRect call GetDesktopWindow mov ecx,eax lea rdx,DesktopRect call GetWindowRect push rbx ;Part of the later call to MoveWindow (no repaint) mov eax,DlgRect.bottom ;Get the bottom of our dialogs window sub eax,DlgRect.top ;subtract the y value at the top of our window mov DlgHeight,rax ;And store it as the dialog's height push rax ;Push it for the call to MoveWindow mov r9d,DlgRect.right ;The X coordinate of the right side of our dialog sub r9d,DlgRect.left ;minus that of the left side mov DlgWidth,r9 ;gives us the width mov r8d,DesktopRect.bottom ;Get the bottom of the desktop window sub r8,DlgHeight ;Subtract the height of our dialog shr r8,1 ;and divide by 2...this gives the middle of the screen mov edx,DesktopRect.right ;Get the right side of the desktop sub rdx,DlgWidth ;Minus the width of our dialog shr edx,1 ;Divide by 2 mov rcx,hWnd ;Push the window handle sub esp,20h call MoveWindow ;Move the window lea edx,BitmapName mov rcx,hInstance call LoadBitmap mov hBitMap,rax mov r9,rbx mov r8d,2000 mov edx,1 mov rcx,hWnd call SetTimer mov TimerID,rax jmp wmBYE wmTIMER:mov r9,rbx mov r8,rbx mov edx,WM_LBUTTONDOWN mov rcx,hWnd call SendMessage mov rdx,TimerID mov rcx,hWnd call KillTimer jmp wmBYE wmPAINT:lea edx,ps call BeginPaint mov ecx,eax call CreateCompatibleDC mov hMemoryDC,rax mov rdx,hBitMap mov ecx,eax call SelectObject mov hOldBmp,rax lea r8,bitmap mov edx,sizeof BITMAP mov rcx,hBitMap call GetObject push SRCCOPY mov eax,bitmap.bmHeight push rax mov eax,bitmap.bmWidth push rax push rbx push rbx push hMemoryDC push 250 mov r9d,250 mov r8,rbx xor edx,edx mov rcx,ps.hdc sub esp,20h call StretchBlt mov rdx,hOldBmp mov rcx,hMemoryDC call SelectObject mov rcx,hMemoryDC call DeleteDC lea edx,ps mov rcx,hWnd call EndPaint jmp wmBYE wmLBUTTONDOWN:call DestroyWindow wmBYE: leave retn WndProc endp end tut_26a.def Код (Text): LIBRARY tut_26a.dll tut_26a.rc Код (C): MySplashBMP BITMAP "Images\\JourneyStart.bmp" tut_26b.asm Код (ASM): OPTION DOTNAME include temphls.inc include win64.inc include kernel32.inc includelib kernel32.lib include user32.inc includelib user32.lib OPTION PROLOGUE:none OPTION EPILOGUE:none IMAGE_BASE equ 400000h .code WinMain proc local msg:MSG push rbp mov ecx,offset libName call LoadLibrary or eax,eax je @f mov ecx,eax call FreeLibrary @@: mov ebp,esp sub esp,sizeof MSG xor ebx,ebx mov eax,10029h mov edi,offset ClassName mov esi,IMAGE_BASE push rax ;hIconSm push rdi ;lpszClassName push rbx ;lpszMenuName push COLOR_WINDOW;hbrBackground push 10005h ;hCursor push rax ;hIcon push rsi ;hInstance push rbx ;cbClsExtra & cbWndExtra db 68h dd WndProc ;lpfnWndProc push sizeof WNDCLASSEX;cbSize & style mov rcx,rsp ;addr WNDCLASSEX sub esp,20h call RegisterClassEx push rbx push rsi ;rsi=400000h shl esi,9 ;rsi=CW_USEDEFAULT push rbx push rbx push 240 push 400 push rsi push rsi mov r9d,WS_OVERLAPPEDWINDOW or WS_VISIBLE mov r8,rdi ;offset ClassName mov edx,edi ;offset ClassName xor ecx,ecx sub esp,20h call CreateWindowEx lea edi,msg @@: mov ecx,edi xor edx,edx mov r8,rbx mov r9,rbx call GetMessage mov ecx,edi call DispatchMessage jmp @b WinMain endp WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM cmp edx,WM_DESTROY je wmDESTROY jmp DefWindowProc wmDESTROY: xor ecx,ecx call ExitProcess WndProc endp ;--------------------------------------- ClassName db 'Win64 Iczelion''s lesson #26: Splash Screen',0 libName db 'tut_26A.dll',0 end