Разбор полётовСначала мы проанализируем код основной программы. Код (ASM): invoke LoadLibrary,addr Libname or eax,eax ;.if eax!=NULL jz @f invoke FreeLibrary,eax @@: . . . Мы вызовем LoadLibrary, чтобы загрузить DLL "splash.dll". После этого выгружаем ее из памяти функцией FreeLibrary. LoadLibrary не возвратится, пока DLL не закончит свою инициализацию. Это все, что делает основная программа. Интересующая нас часть находится в DLL. Код (ASM): .if reason==DLL_PROCESS_ATTACH ; When the dll is loaded mov rax,hInst mov hInstance,rax invoke ShowBitMap После загрузки DLL в память, Windows вызывает ее входную функцию с флагом DLL_PROCESS_ATTACH. Мы пользуемся этой возможностью, чтобы отобразить сплэш-экран. Во-первых, мы сохраняем дескриптор DLL на будущее. Потом вызываем функцию ShowBitmap, которая выполняет главную работу. ShowBitmap регистрирует класс окна, создает окно и входит в цикл обработки сообщений. Следует обратить внимание на вызов CreateWindowEx: Код (ASM): INVOKE CreateWindowEx,NULL,ADDR ClassName,NULL,\ WS_POPUP,CW_USEDEFAULT,\ CW_USEDEFAULT,250,250,NULL,NULL,\ hInstance,NULL Обратите внимание, что стиль окна WS_POPUP, что делает окно без бордюра и без заголовка. Мы также ограничиваем размер окна ― 250 x 250. Теперь, когда окно создано, в обработчике WM_CREATE мы передвигаем окно в центр экрана следующим кодом. Код (ASM): invoke GetWindowRect,hWnd,addr DlgRect invoke GetDesktopWindow invoke GetWindowRect,eax,addr DesktopRect and qword ptr [rsp+28h],0 mov eax,DlgRect.bottom sub eax,DlgRect.top mov DlgHeight,eax mov [rsp+20h],rax mov r9d,DlgRect.right sub r9d,DlgRect.left mov DlgWidth,r9d mov r8d,DesktopRect.bottom sub r8d,DlgHeight shr r8d,1 mov edx,DesktopRect.right sub edx,DlgWidth shr edx,1 invoke MoveWindow,hWnd Мы получаем размеры рабочего стола и окна, а затем вычисляем координаты левого верхнего угла окна, чтобы оно было в центре. Код (ASM): invoke LoadBitmap,hInstance,addr BitmapName mov hBitMap,eax invoke SetTimer,hWnd,1,2000,NULL mov TimerID,eax Затем мы загружаем рисунок из ресурса функцией LoadBitmap и создаем таймеp, указывая в качестве его ID 1, а в качестве временного интервала 2 секунды. Таймеp будет посылать сообщения WM_TIMER окну каждый две секунды. Код (ASM): wmPAINT: invoke BeginPaint,hWnd,addr ps mov hdc,eax invoke CreateCompatibleDC,hdc mov hMemoryDC,eax invoke SelectObject,eax,hBitMap mov hOldBmp,eax invoke GetObject,hBitMap,sizeof BITMAp,addr bitmap invoke StretchBlt,hdc,0,0,250,250,\ hMemoryDC,0,0,bitmap.bmWidth,bitmap.bmHeight,SRCCOpY invoke SelectObject,hMemoryDC,hOldBmp invoke DeleteDC,hMemoryDC invoke EndPaint,hWnd,addr ps Когда окно получить сообщение WM_PAINT, она создаст DC в памяти, выберет в него рисунок, получит размер рисунка функцией GetObject, а затем поместит рисунок на окно, вызвав StretchBlt, которая действует как BitBlt, но адаптирует рисунок к желаемым размерам. В этом случае, нам нужно, чтобы рисунок поместился в окно, поэтому мы используем StrectchBlt вместо BitBlt. Мы удаляем созданный в памяти DC. Код (ASM): wmLBUTTONDOWN: invoke DestroyWindow,hWnd Пользователя бы раздражало, если бы ему пришлось бы ждать, пока сплэш-экран не исчез. Мы можем предоставить пользователю выбор. Когда он кликнет на сплэш-экране, тот исчезнет. Вот почему нам нужно обрабатывать сообщение WM_LBUTTONDOWN. Когда мы получим это сообщение, окно будет уничтожено вызовом DestroyWindow. Код (ASM): wmTIMER: invoke SendMessage,hWnd,WM_LBUTTONDOWN,NULL,NULL invoke KillTimer,hWnd,TimerID Если пользователь решит подождать, сплэш-экран исчезнет, когда пройдет заданный период времени (в нашем примере, это две секунды). Мы можем сделать это обработкой сообщения WM_TIMER. После получения этого сообщения, мы закрываем окно, послав ему сообщение WM_LBUTTONDOWN, чтобы избежать повторения кода. Таймер нам больше не нужен, поэтому мы уничтожаем его KillTimer. Когда окно будет закрыто, DLL возвращает контроль основной программе.
Глава сорок пятая. Братец Кролик и подсказки (toolip control)Мы изучим контроль tooltip. Что это такое, как его создать и как им пользоваться. Скачайте пример здесь.Теория ― МАТЬ СКЛЕРОЗАТултип ― это маленькая прямоугольное окно, которое отображается, когда курсор мыши находится над какой-то определенной областью. Окно тултипа содержит текст, заданный программистом. В этом отношении тултип играет ту же роль, что и окно статуса, но оно исчезает, когда пользователь кликает или убирает курсор мыши из заданной области. Вы, вероятно, знакомы с тултипами, ассоциированные с кнопками тулбара. Эти "тултипы" ― одно из удобств, предоставляемых тулбаром. Если вам нужны тултипы для других окон/контролов, вам необходимо создать собственный тултип контрол. Теперь, когда вы знаете, что такое тултип, давайте перейдем к тому, как мы можем создать и использовать его. Ниже pасписаны шаги: Создать тултип-контрол функцией CreateWindowEx. Определить регион, в котором он будет отслеживать передвижения мыши. Передать регион тултип-контролу. Передавать сообщения от мыши в указанном регионе тултип-контролу (этот шаг зависит от заданных флагов). Ниже мы детально проанализируем каждый шаг.Создание тултипаТултип ― это common control. Поэтому вам необходимо где-нибудь в программе вызвать функцию InitCommonControls, чтобы MASM подлинковал к выходнуму экзешнику comctl32.dll. Вы создаете тултип с помощью CreateWindowEx. Это будет выглядеть примерно так: Код (ASM): .data TooltipClassName db "Tooltips_class32",0 .code . . . . . invoke InitCommonControls invoke CreateWindowEx, NULL, addr TooltipClassName, NULL, TIS_ALWAYSTIp, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL Обратите внимание на стиль окна: TIS_ALWAYSTIP. Этот стиль указывает, что тултип будет показываться, когда курсор мыши будет находиться над заданной областью вне зависимости от статуса окна. То есть, если вы будете использовать этот флага, тултип будет появляться (когда курсор мыши будет находиться над определенной областью), даже если окно, с которым ассоциирован тултип, неактивно. Вам не нужно задавать слили WS_POPUP и WS_EX_TOOLWINDOW, потому что тултип определяет их автоматически. Вам также не нужно указывать координаты, ширину и высоту тултипа: он сам рассчитывает свои характеристики, поэтому в качестве всех четырех параметров мы указывает CW_USEDEFAULT. Оставшиеся параметры не играют роли.Определение tool'овТултип создается, но не отображается сразу. Нам нужно, чтобы он отображался только над определенной областью. Теперь пришло время задать ее. Мы называем такую область 'tool'. Tool ― это прямоугольная область клиентской части окна, в пределах которой тултип будет отслеживать передвижение мыши. Прямоугольная область может покрывать всю клиентскую часть окна или только некоторую долю от нее. Поэтому мы можем поделить 'tool' на два типа: один ― это, когда в качестве tool'а выступает целая клиентская область окна, а другой ― прямоугольная часть клиентской области окна. Оба типа находят свое применение. Например, наиболее часто тултипы первого типа используются вместе с кнопками, окнами ввода и так далее. Вам не нужно указывать координаты и размерность tool'а: предполагается, что будет задействована вся клиентская область. Tool'ы второго типа полезны, когда вы хотите поделить клиентскую часть окна на несколько регионов без использования дочерних окон. В этом случае вам будет необходимо задать координату верхнего левого угла, ширину и высоту tool'а.
Вы определяете характеристики tool'а в структуре TOOLINFO, которая имеет следующее определение: Код (ASM): TOOLINFO STRUCT cbSize DWORD ? /* размер структуры TOOLINFO. Вы должны заполнить этот параметр. Windows не будет отмечать ошибку, если это поле заполнено не правильно, но вы получите странные, непредсказуемые результаты */ uFlags DWORD ? // битовые флаги определяют характеристики tool'а в таблице hwnd QWORD ? uId QWORD ? rect RECT <> hInst QWORD ? lpszText QWORD ? TOOLINFO ENDS константаназначениеTTF_IDISHWNDЕсли вы укажете этот флаг, вы должны заполнить параметр uId дескриптором окна, который вы хотите использовать. Если вы не укажете этот флаг, это будет означать, что вы хотите использовать второй тип tool'а. В этом случае вам нужно заполнить параметр rect координатами прямоугольной областиTTF_CENTERTIPобычно окно тултипа появляется справа и ниже курсора мыши. Если вы укажете этот флаг, окно тултипа появится ниже tool'а и отцентрируется независимо от позиции мышиного курсораTTF_RTLREADINGвы можете забыть об этом флаге, если ваша программа не предназначена специально для надписей на арабским или иврите. Этот флаг отображает текст тултипа справа налево. Не работает под другими системамиTTF_SUBCLASSесли вы используете флаг, это означает, что вы указываете тултип-контролу сабклассировать окно tool'а, чтобы тултип мог интерпретировать сообщения от мыши, которые посылаются окну. Этот флаг очень удобен. Если вы не используете этот флаг, вам придется делать больше работы ― передавать сообщения от мыши тултипуhWnd ― дескриптор окна, который содержит tool. Если вы указали флаг TTF_IDISWND, это поле игнорируется, так как Windows будет использовать значение uId в качестве дескриптора окна. Вам нужно заполнить это поле, если: Вы не устанавливали флаг TTF_IDISHWND Вы указываете значение LPSTR_TEXTCALLBACK в параметре lpszText. Это значение указывает тултипу, что когда ему необходимо отобразить свое окно, он должен уведомить об этом окно, которое содержит tool. Это вид динамического обновления текста тултипа. Если вы хотите изменять динамически текст тултипа, вам следует LPSTR_TEXTCALLBACK в качестве значения LPSTR_TEXTCALLBACK. Тултип будет посылать уведомление TTN_NEEDTEXT окну, чей дескриптор содержится в поле hWnd. uId ― это поле может иметь одно из двух значений, в зависимости от того, содержит ли uFlags флаг IIF_IDISHWND. Определяемое приложением ID tool'а, если флаг TTF_IDISHWND. Так как это означает, что вы используете tool, покрывающее только часть клиентской области, то логично, что вы можете иметь несколько tool'ов на одной клиентской области (без пересечений). Тултип-контрол должен иметь какой-то путь отличать их друг от друга. В этом случае дескриптора окна hWnd не достаточно, так как все tool'ы находятся на одном и том же окне. Определяемые приложением ID служат именно для этой цели. ID может быть любым значением, главное, чтобы оно было уникально по отношению к другим ID. Дескриптор окна, чья клиентская область полностью используется в качестве tool'а, если указан флаг TTF_IDISHWND. Вы можете удивиться. почем это поле используется для хранения дескриптора окна, если есть hWnd? Ответ следующий: поле hWnd уже может быть заполнено, если в параметре lрszText указано значение LPSTR_TEXTCALLBACK. Окно, которое ответственно за предоставление текста тултипа, и окно, которое содержит tool, могут быть не одним и тем же. rect ― структура RECT, которая указывает размерность tool'а. Эта структура определяет прямоугольник относительного верхнего левого угла клиентской области окна, указанного в параметре hWnd. То есть, вы должны заполнить эту структуру, если вы хотите указать tool, который покрывает только часть клиентской области. Тултип-контрол проигнорирует это поле, если вы укажете флаг TTF_IDISHWND (вы хотите использовать в качестве tool'а целое окно). hInst ― это дескриптор процесса, содержащий ресурс строки, которая будет использована в качестве текста, если значение lpszText равно ID строкового ресурса. Это может вас несколько смутить. Прочтите сначала описание параметра lpszText, и вы поймете, для чего используется это поле. Тултип-контрол игнорирует это поле, если lpszText не содержит ID ресурса. lpszText ― это поле имеет несколько значений: Если вы укажете в этом поле значение LPSTR_TEXTCALLBACK, тултип будет посылать уведомительное сообщение TTN_NEEDTEXT окну, которое идентифицируется дескриптором поля hWnd, чтобы то предоставило тултипу текстовую строку. Это наиболее динамичный метод обновления текста тултипа: вы можете менять его каждый раз, когда отображается окно тултипа. Если вы укажете в этом поле ID строкового ресурса, тултип, когда ему потребуется отобразить текст в своем окне, будет искать строку в таблице строк процесса, заданного параметром hInst. Тултип-контрол идентифицирует ID ресурса следующим образом: так как ID ресурса ― это 16-битное значение, верхнее слово этого поля всегда будет pавно нулю. Этот метод полезен, если вы хотите портировать вашу программу на другие языки. Так как строковый ресурс определен в файле определения ресурсов, вам не нужно модифицировать исходный код. Вам только нужно изменить таблицу строк и текст тултипа изменится без риска внесения ошибок в программу. Если значение в этом поле не равно LPSTR_TEXTCALLBACK и верхнее слово не равно нулю, тултип-контрол интерпретирует значение как указатель на текстовую строку, которая будет использована в качестве текста тултипа. Этот метод самый простой, но наименее гибкий. резюме: вы должны заполнить структуру TOOLINFO и передать ее тултипу. Эта структура задаст характеристики tool'а.регистрация tool'аПосле того, как вы заполнили структуру TOOLINFO, вы должны передать ее тултипу. Тултип может обслуживать много tool'ов, поэтому обычно одно тултипа хватает на все окно. Чтобы зарегистрировать tool, вы посылаете тултипу сообщение TTM_ADDTOOL. wParam не используется, а lParam должен содержать адрес структуры TOOLINFO. Код (ASM): .data? ti TOOLINFO <> ....... .code ....... ....... invoke SendMessage, hwndTooltip, TTM_ADDTOOL, NULL, addr ti SendMessage возвратит TRUE, если tool был успешно зарегистрирован тултипом или FALSE в обратном случае. Вы можете удалить tool сообщением TTM_DELTOOL.Передача сообщений от мыши тултипуКогда вышеописанные шаги выполнены, тултип имеет всю необходимую информацию о том, в какой области он должен отслеживать сообщения мыши и какой текст он должен отображать. Единственное, что отсутсвует ― это триггер. Подумайте: область, указанная в качестве tool'а находится на клиентской части другого окна. Как может тултип перехватить сообщения от мыши для этого окна? Необходимо, чтобы он мог измерить количество времени, которое курсор мыши находится над tool'ом, чтобы вовремя отобразить окно тултипа. Есть два метода, чтобы достичь этой цели, один требует помощи со стороны окна, которое tool, а другой этого не требует. Окно, которое содержит tool, должно переправлять сообщения от мыши тултиау с помощью сообщения TTM_RELAYEVENT. lParam должен содержать адрес структуры MSG, содержащую сообщение от мыши. Тултип обрабатывает только следующие сообщения от мыши: WM_LBUTTONDOWN WM_MOUSEMOVE WM_LBUTTONUP WM_RBUTTONDOWN WM_MBUTTONDOWN WM_RBUTTONUP WM_MBUTTONUP Все другие сообщения игнорируются. Таким образом, в процедуре окна, содержащего tool, должен быть обработчик вроде следующего: Код (ASM): Wndproc proc hWnd:DWORD, uMsg:DWORD, wparam:DWORD, lparam:DWORD ....... if uMsg==WM_CREATE ............. elseif uMsg==WM_LBUTTONDOWN || uMsg==WM_MOUSEMOVE || \ uMsg==WM_LBUTTONUP || uMsg==WM_RBUTTONDOWN || \ uMsg==WM_MBUTTONDOWN || uMsg==WM_RBUTTONUP || \ uMsg==WM_MBUTTONUP invoke SendMessage, hwndTooltip, TTM_RELAYEVENT, NULL, addr msg .......... Вы можете указать флаг TTF_SUBCLASS в параметре uFlags структуры TOOLINFO. Этот флаг указывает тултипу сабклассировать окно, которое содержит tool, чтобы перехватывать сообщения от мыши без участия сабклассированного окна. Этот метод проще использовать и он требует меньше усилий, так как тултип берет всю обработку сообщений на себя. Вот и все. Теперь ваш тултип полностью функционален. Есть несколько полезных тултиповых сообщений, о которых вас следует знать. TTM_ACTIVATE ― если вы хотите включать/выключать контрол динамически, это сообщение для вас. Если wparam равен TRUE, тултип включается, если wParam равен FALSE, тултип выключается. Тултип включается автоматически, как только он был создан, поэтому вам не надо посылать сообщение, чтобы активировать его. TTM_GETTOOLINFO и TTM_SETTOOLINFO. Если вы хотите получить/изменить значения в структуре TOOLINFO после того, как она была отправлена тултипу, используйте данное сообщение. Вам потребуется указать tool, чьи характеристики вы хотите изменить, с помощью верных uId и hWnd. Если вы хотите изменить только параметр rect, используйте сообщение TTM_NEWTOOLRECT. Если вам нужно только изменить текст тултипа, используйте TTM_UPDATATIPTEXT. TTM_SETDELAYTIME. С помощью этого сообщения вы можете задать временную задержку, которую будет использовать тултип.
ПРАКТИКА ― СЕСТРА ШИЗОФРЕНИИСледующий пример ― это простое диалоговое окно с двумя кнопками. Клиентская область диалогового окна поделена на 4 области: верхняя левая, верхняя правая, нижняя левая и нижняя правая. Каждая область указана как tool с собственным текстом. Две кнопки также имеют свои собственные тексты подсказок.asm-файл Код (ASM): include win64a.inc IMAGE_BASE equ 400000h IDD_MAINDIALOG equ 101 .code WinMain proc sub esp,38h xor r8d,r8d mov [rsp+20h],r8 mov r9d,offset dialog_procedure invoke DialogBoxParam,IMAGE_BASE,IDD_MAINDIALOG invoke ExitProcess,0 invoke InitCommonControls WinMain endp dialog_procedure proc hDlg:QWORD,uMsg:QWORD,wParam:QWORD,lParam:QWORD local ti:TOOLINFO local rect:RECT mov hDlg,rcx cmp edx,WM_INITDIALOG je wmINITDIALOG cmp edx,WM_CLOSE je wmCLOSE xor eax,eax jmp wmBYE wmINITDIALOG:xor ecx,ecx mov esi,IMAGE_BASE push rcx push rsi push rcx push rcx shl esi,9 push rsi push rsi push rsi push rsi mov r9d,TTS_ALWAYSTIP xor r8d,r8d mov edx,offset ToolTipsClassName sub esp,20h call CreateWindowEx mov hwndTool,rax mov ti.cbSize,sizeof TOOLINFO mov ti.uFlags,TTF_SUBCLASS mov rcx,hDlg mov ti.hwnd,rcx lea edx,rect call GetWindowRect mov ebx,3 @@: lea ecx,rect mov r9d,ebx mov r8,handle[rbx*8] lea edx,ti call SetDlgToolArea dec ebx jns @b lea r8d,ti mov edx,offset EnumChild mov rcx,hDlg call EnumChildWindows jmp wmBYE wmCLOSE:xor edx,edx ;mov rcx,hDlg call EndDialog wmBYE: leave ret dialog_procedure endp EnumChild proc hwndChild:QWORD,lParam:QWORD local buffer[256]:BYTE push rbp mov ebp,esp sub esp,(20h+256+15)and(-16) mov lParam,rdx mov [rdx+TOOLINFO.uId],rcx;rcx=hwndChild or [rdx+TOOLINFO.uFlags],TTF_IDISHWND mov r8d,255 lea edx,buffer ;mov rcx,hwndChild invoke GetWindowText lea eax,buffer mov r9,lParam mov [r9+TOOLINFO.lpszText],rax xor r8d,r8d mov edx,TTM_ADDTOOL mov rcx,hwndTool call SendMessage leave retn EnumChild endp SetDlgToolArea proc lprect:QWORD,lpti:QWORD,lpText:QWORD,id:QWORD push rbp mov ebp,esp sub esp,20h jmp [handels+r9*8] id_0: mov [rdx+TOOLINFO.Rect.left],0 mov [rdx+TOOLINFO.Rect.top],0 mov eax,[rcx+RECT.right] sub eax,[rcx+RECT.left] shr eax,1 mov [rdx+TOOLINFO.Rect.right],eax mov eax,[rcx+RECT.bottom] sub eax,[rcx+RECT.top] shr eax,1 jmp wmBYE id_1: mov eax,[rcx+RECT.right] sub eax,[rcx+RECT.left] shr eax,1 inc eax mov [rdx+TOOLINFO.Rect.left],eax mov [rdx+TOOLINFO.Rect.top],0 mov eax,[rcx+RECT.right] sub eax,[rcx+RECT.left] jmp a1 id_2: mov [rdx+TOOLINFO.Rect.left],0 mov eax,[rcx+RECT.bottom] sub eax,[rcx+RECT.top] shr eax,1 inc eax mov [rdx+TOOLINFO.Rect.top],eax mov eax,[rcx+RECT.right] sub eax,[rcx+RECT.left] shr eax,1 jmp a1 id_3: mov eax,[rcx+RECT.right] sub eax,[rcx+RECT.left] shr eax,1 inc eax mov [rdx+TOOLINFO.Rect.left],eax mov eax,[rcx+RECT.bottom] sub eax,[rcx+RECT.top] shr eax,1 inc eax mov [rdx+TOOLINFO.Rect.top],eax mov eax,[rcx+RECT.right] sub eax,[rcx+RECT.left] a1: mov [rdx+TOOLINFO.Rect.right],eax mov eax,[rcx+RECT.bottom] sub eax,[rcx+RECT.top] wmBYE: mov [rdx+TOOLINFO.Rect.bottom],eax mov [rdx+TOOLINFO.lpszText],r8;lpText mov r9d,edx;lpti xor r8d,r8d;NULL mov edx,TTM_ADDTOOL mov rcx,hwndTool call SendMessage leave ret handels dq id_0, id_1, id_2, id_3 SetDlgToolArea endp ;--------------------------------------------------------- ToolTipsClassName db "Tooltips_class32",0 MainDialogText1 db "This is the upper left area of the dialog",0 MainDialogText2 db "This is the upper right area of the dialog",0 MainDialogText3 db "This is the lower left area of the dialog",0 MainDialogText4 db "This is the lower right area of the dialog",0 handle dq MainDialogText1,MainDialogText2,MainDialogText3,MainDialogText4 hwndTool dq ? end rc-файл Код (C): #include "resource.h" #define IDD_MAINDIALOG 101 #define IDC_OK 1000 #define IDC_EXIT 1001 IDD_MAINDIALOG DIALOG DISCARDABLE 0, 0, 229, 96 STYLE DS_MODALFRAME | DS_3DLOOK | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU | DS_CENTER CAPTION "Tooltip Example" FONT 8, "MS Sans Serif" BEGIN PUSHBUTTON "", -1,0, -4,115,51,WS_VISIBLE+BS_GROUPBOX PUSHBUTTON "", -1,114,43,115,53,WS_VISIBLE+BS_GROUPBOX PUSHBUTTON "OK",IDC_OK,55,68,50,14 PUSHBUTTON "E&xit",IDC_EXIT,123,68,50,14 END Разбор полётов После того, как создано основное диалоговое окно, мы создает тултип-контрол функцией CreateWindowsEx. Код (ASM): invoke InitCommonControls invoke CreateWindowEx,NULL,ADDR ToolTipsClassName,NULL,\ TTS_ALWAYSTIp,CW_USEDEFAULT,\ CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\ hInstance,NULL mov hwndTool,eax После этого мы переходим к определению четырех tool'ов для каждого угла диалогового окна. Код (ASM): mov id,0 ; used as the tool ID mov ti.cbSize,sizeof TOOLINFO mov ti.uFlags,TTF_SUBCLASS ; tell the tooltip control to subclass the dialog window. push hDlg pop ti.hWnd ; handle to the window that contains the tool invoke GetWindowRect,hDlg,addr rect ; получаем размерность клиентской ; области invoke SetDlgToolArea,hDlg,addr ti,addr MainDialogText1,id,addr rect Мы инициализируем члены структуры TOOLINFO. Заметьте, что мы хотим поделить клиентскую область на 4 tool'а, поэтому нам нужно знать размерность клиентской области. Это то, для чего мы вызываем GetWindowsRect. Мы не хотим передавать сообщения мыши тултипу, поэтому мы указываем флага TIF_SUBCLASS. SetDlgToolArea ― это функция, которая высчитывает координаты прямоугольной области каждого tool'а и регистрирует tool в тултипе. Функция делит клиентскую область на 4 области с одними и теми же размерами. Затем она посылает сообщение TTN_ADDTOOL тултипу, передавая адрес структуры TOOLINFO в lParam.
Код (ASM): invoke SendMessage,hwndTool,TTM_ADDTOOL,NULL,lpti После того, как все 4 tool'а зарегистрированы, мы можем перейти к кнопкам на диалоговом окне. Мы можем обрабатывать каждую кнопку с помошью ее ID, но это утомительно. Вместо этого мы используем EnumChildWindows API, чтобы перечислить все контролы на диалоговом окне и затем зарегистрировать для каждого из них подсказку. EnumChildWindows имеет следующий синтаксис: Код (C): BOOL WINAPI EnumChildWindows( _In_opt_ HWND hWndParent,// дескриптор родительского окна _In_ WNDENUMPROC lpEnumFunc, _In_ LPARAM lParam ); lpEnumFunc ― адрес функции EnumChildProc, которая будет вызываться для каждого перечисленного контрола. lParam ― заданное приложением значение, которое будет передано EnumChildProc. У этой функции следующее определение: Код (C): BOOL CALLBACK EnumChildProc( _In_ HWND hwnd, _In_ LPARAM lParam ); hwndChild ― хэндл контрола, найденного EnumChildWindows. lParam ― это тоже значение, что вы передали EnumChildWindow. В нашем примере мы вызываем EnumChildWindows следующим образом: Код (ASM): invoke EnumChildWindows,hDlg,addr EnumChild,addr ti Мы передаем адрес структуры TOOLINFO в параметре lParam, потому что мы будем регистрировать подсказки для каждого дочерний контрол в функции EnumChild. Если мы не будем использовать данный метода, нам придется объявить глобальную переменную, чтобы предотвратить баги. Когда мы вызываем EnumChildWindows, Windows перечислит дочерние конролы нашего диалогового окна и вызовет для каждого из ни функцию EnumChild. То есть, если наше диалоговое окно имеет два контрола, EnumChild будет вызван дважды. Функция EnumChild заполнит соответствующие поля структуры TOOLINFO и зарегистрирует tool. Код (ASM): EnumChild proc uses edi hwndChild:DWORD,lparam:DWORD LOCAL buffer[256]:BYTE mov edi,lParam assume edi:ptr TOOLINFO push hwndChild pop [edi].uId ; we use the whole client area of the control as the tool or [edi].uFlags,TTF_IDISHWND invoke GetWindowText,hwndChild,addr buffer,255 lea eax,buffer ; use the window text as the tooltip text mov [edi].lpszText,eax invoke SendMessage,hwndTool,TTM_ADDTOOL,NULL,edi assume edi:nothing ret EnumChild endp Заметьте, что в этом случае мы используем другой тип tool'ов, покрывающий всю клиентскую область окна. Поэтому нам нужно заполнить поле uID дескриптором окна, которое содержит tool. Также мы указываем флаг TTF_IDISHWND в параметре uFlags.
Глава сорок шестая. Братец Кролик и Контрол Listview В этой главе мы изучим как создать и использовать элемент управления listview. Скачайте пример здесь.Теория ― МАТЬ СКЛЕРОЗАListview ― это один из common control'ов, таких как treeview, richedit и так далее. Вы знакомы с ними, даже если не знаете их имен. Например, правая панель Windows Explorer'а ― это элемент управления listview. Этот элемент управления подходит для отображения элементов. В этом отношении его можно рассматривать как усовершенствованный listbox. Вы можете создать listview двумя путями. Первый метод самый простой: создайте его с помощью редактора ресурсов, главное не забудьте поместить вызов InitCommonControls. Другой метод заключается в вызове CreateWindowsEx. Вы должны указать правильное имя класса окна, то есть SysListView32. Существует четыре метода отображения элементов в listview: иконки маленькие иконки список отчет Вы можете увидеть чем отличаются виды отображения друг от друга, выбрав View[math]\to[/math]Large Icons (иконки), Small Icons (маленькие иконки), List (список) и Details (отчет) Теперь, когда мы знаем, как создать listview, мы рассмотрим, как его можно применять. Сосредоточимся на режиме "отчет", как методе отображения, который может продемонстрировать многие свойства listview. Шаги использования listview следующие: Создаем listview с помощью CreateWindowEx, указав SysListView32 как имя класса. Вы должны указать начальный тип отображения. (если предусматривается) Создаем и инициализируем списки изображений, которые будут использованы при отображении item'ов listview. Вставляем колонки в listview. Этот шаг необходим, если listview будет использовать тип отображения 'отчет'. Вставьте item'ы и подitem'ы в listview. КолонкиПри отчете в listview может быть одна или более колонок. Вы можете считать тип организации данных в этом режиме таблицей: данные организованны в ряды и колонки. В режиме отчета в listview должна быть по крайней мере одна колонка. В других режимах вам не надо вставлять колонку, так как в контроле будет одна и только одна колонка. Вы можете вставить колонку, послав сообщение LVM_INSERTCOLUMN контролу listview. LVM_INSERTCOLUMN wParam = iCol lParam = указатель на структуру LV_COLUMN iCol ― это номеp колонки, начиная с нуля. Структура LV_COLUMN содержит информацию о колонке, которая должна быть вставлена. У нее следующее определение: Код (ASM): LV_COLUMN STRUCT imask DWORD ? fmt DWORD ? lx DWORD ?,? pszText QWORD ? cchTextMax DWORD ? iSubItem DWORD ? LV_COLUMN ENDS imask ― коллекция флагов, задающие, какие члены структуры верны. Этот параметр был введен, потому что не все члены этой структуры используются одновременно. Некоторые из них используются в особых ситуациях. Эта структура используются и для ввода и для вывода, поэтому важно, чтобы вы пометили, какие параметры верны. Существуют следующие флаги: LVCF_FMTПараметр fmt веренLVCF_SUBITEMПараметр isubItem веренLVCF_TEXTПараметр pszText веренLVCF_WIDTHПараметр lx веренВы можете комбинировать вышеприведенные флаги. Например, если вы хотите указать текстовое имя колонки, вам нужно предоставить указатель на строку в параметре pszText. Также вы должны указать Windows, что параметр pszText содержит данные, указав флаг LVCF_TEXT в этом поле, иначе Windows будет игнорировать значение pszText. fmt ― указывает выравнивание элементов/подэлементов в колонке. Доступны следующие значения: LVCFMT_CENTERтекст с выравниванием по центруLVCFMT_LEFTтекст с выравниванием влевоLVCFMT_RIGHTтекст с выравниванием вправо lx ― ширина колонки в пикселях. В дальнейшем вы можете изменить ширину колонки LVM_SETCOLUMNWIDTH. pszText ― содержит указатель на имя колонки, если эта структура используется для установки свойств колонки. Если эта структура используется для получения свойств колонки, это поле содержит указатель на буфер, достаточно большой для получения имени колонки, которая будет возвращена. В этом случае вы должны указать размер буфера в поле cchTextMax. Вы должны игнорировать cchTextMax, если вы хотите установить имя колонки, потому что имя должно быть ASCIIZ-строкой, длину которой Windows сможет определить. cchTextMax ― размер в байтах буфер, указанного в поле pszText. Этот параметр используется только когда вы используете структуру для получения информации о колонке. Если вы используете эту структуру, чтобы установить свойства колонки, это поле будет игнорироваться. iSubItem ― указывает индекс подэлемента, ассоциированного с этой колонкой. Это значение используется в качестве маркера подэлемента, с которым ассоциирована эта колонка. Если хотите, вы можете указать бессмысленный номер и ваш listview будет прекрасно работать. Использование этого поля лучше всего демонстрируется, когда у вас есть номер колонки и вам нужно узнать с каким поэлементом ассоциирована эта колонка. Чтобы сделать это, вы можете послать сообщение LVM_GETCOLUMN, указав в параметре imask флаг LVCF_SUBITEM. Listview заполнит параметр iSubItem значением, которое вы укажете в этом поле, поэтому для работоспособности данного метода вам нужно указывать корректные подэлементы в этом поле. iImage и iOrder ― используется начиная с Internet Explorer 3.0. Когда listview создан, вам нужно вставить в него одну или более колонок. Если не предполагается переключение в режим отчета, то это не нужно. Чтобы вставить колонку, вам нужно создать структуру LV_COLUMN, заполнить ее необходимой информацией, указать номер колонки, а затем послать структуру listview с помощью сообщения LVM_INSERTCOLUMN. Код (ASM): LOCAL lvc:LV_COLUMN mov lvc.imask,LVCF_TEXT+LVCF_WIDTH mov lvc.pszText,offset Heading1 mov lvc.lx,150 invoke SendMessage,hList, LVM_INSERTCOLUMN,0,addr lvc Вышеприведенный кусок кода демонстрирует процесс. Он указывает текста заголовка и его ширину, а затем посылает сообщение LVM_INSETCOLUMN listview. Это просто.Item'ы и под-item'ыItem'ы ― это основные элементы listview. В режимах отображения, отличных от отчета, вы будет видеть только item'ы. Под-item'ы ― это детали item'ов. Например, если item ― это имя файла, тогда вы можете считать атрибуты файла, его размер, дату создания файла как под-item'ы. В режиме отчета самая левая колонка содержит item'ы, а остальные ― под-item'ы. Вы можете думать о item'е и его под-item'ах как о записи базы данных. Item ― это основной ключ записи и его под-item'ы ― это поля записи. Минимум, что вам нужно иметь в listview ― это item'ы, под-item'ы необязательны. Тем не менее, если вы хотите дать пользователю больше информации об элементах, вы можете ассоциировать item'ы с под-item'ами, чтобы пользователь мог видеть детали в режиме отчета. Вставить item в listview можно послав сообщение LVM_INSERTITEM. Вам также нужно передать адрес структуры LV_ITEM в lParam. LV_ITEM имеет следующее определение: Код (ASM): LV_ITEM STRUCT imask DWORD ? iItem DWORD ? iSubItem DWORD ? state DWORD ? stateMask DWORD ?,? pszText QWORD ? cchTextMax DWORD ? iImage DWORD ? lParam DWORD ? iIndent DWORD ? LV_ITEM ENDS imask ― множество флагов, которые задают, какие из параметров данной структуры будут верны. В сущности, это поле идентично параметру imask LV_COLUMN. Чтобы получить детали относительно флагов, обратитесь к вашему справочнику по API. iItem ― индекс item'а, на который ссылается эта структура. Индексы начинаются с нуля. Вы можете считать, что это поле содержит значение "ряда" таблицы. iSubItem ― индекс под-item'а, ассоциированный с item'ом, заданном в iItem. Вы можете считать, что это поле содержит "колонку" таблицы. Например, если вы хотите вставить item в только что созданный listview, значение в iItem будет равно 0 (потому что этот item первый), а значение в iSubItem также будет равно нулю (нам нужно вставить item в первую колонку). Если вы хотите указать под-item, ассоциированный с этим item'ом, iItem будет являться индексом item'а, с которым будет происходить ассоциирование (в выше приведенном примере это 0). iSubItem будет равен 1 или более, в зависимости от того, в какую колонку вы хотите вставить под-item. Например, если у вашего listview 4 колонки, первая колонка будет содержать item'ы. Остальные 3 колонки предназначаются для под-item'ов. Если вы хотите вставить под-item в 4-ую колонку, вам нужно указать в iSubItem значение 3.
state ― параметр, содержащий флаги, отражающие состояние item'а. Оно может изменяться из-за действий пользователя или другой программы. Термин 'состояние' включает в себя, находится ли item в фокусе, подсвечен ли он, выделен для операции вырезания, выбран ли он. В добавление к флагам состояния он также содержит основанный на единице индекс изображения состояния данного item'а. stateMask ― так как параметр state может содержать флаги состояния, индекс изображения, нам требуется сообщить Windows, какое значение мы хотим установить или получить. Это поле создано именно для этого. pszText ― адрес ASCIIZ-строки, которая будет использоваться в качестве названия элемента в случае, если мы хотим установить или вставить элемент. Если мы используем эту структуру для того, чтобы получить свойства элемента, этот параметр должен содержать адрес буфера, который будет заполнен названием элемента. cchTextMax ― это поле используется только тогда, когда вы используете данную структуру, чтобы получать информацию об элементе. В этом случае это поле содержит размер в байтах буфера, указанного параметром pszText. iImage ― индекс image list'а, содержащего иконки для listview. Индекс указывает на иконку, которая будет использоваться для этого элемента. lParam ― определяемое пользователем значение, которое будет использоваться, когда вы будете сортировать элементы в listview. Кратко говоря, когда вы будете указывать listview отсортировать item'ы, listview будет сравнивать item'ы попарно. Он будет посылать значение lParam обоих элементов вам, чтобы вы могли решить, какое из этих двух должно быть в списке идти раньше. Если вы пока не можете этого понять, не беспокойтесь. Вы изучите сортировку позже. Давайте кратко изложим шаги вставки элемента/подэлемента в listview. Создаем переменную типа структуры LV_ITEM. Заполняем ее необходимой информацией. Посылаем сообщение LVM_INSERTITEM listview, если вам нужно вставить элемент. Или, если вы хотите вставить подэлемент, посылаем сообщение LVM_SETITEM. Это может смущать вас, если вы не понимаете взаимоотношений между элементом и его подэлементами. Подэлементы считаются свойствами элемента. Поэтому вы можете вставить item'ы, но не под-item'ы, а также у вас не может быть подэлемента без ассоциированного с ним элемента. Вот почему вам нужно послать сообщение LVM_SETITEM, чтобы добавить подэлемент вместо LVM_INSERTITEM. Сообщения/уведомления listviewТеперь, когда вы знаете, как создавать и заполнять элементами listview, следующим шагом является общение с ним. Listview общается с родительским окном через сообщения и уведомления. Родительское окно может контролировать listview, посылая ему сообщения. Listview уведомляет родительское окно о важных/интересных сообщения через сообщение WM_NOTIFY, как и другие common control'ы.Сортировка элементов/подэлементовВы можете указать порядок сортировки контрола listview по умолчанию указав стили LVS_SORTASCENDING или LVS_SORTDESCENDING в CreateWindowEx. Эти два стиля упорядочивают элементы только по элементам. Если вы хотите отсортировать элементы другим путем, вы должны послать сообщение LVM_SORTITEMS listview. LVM_SORTITEMS wParam = lParamSort lParam = pCompareFunction lParamSort ― это определяемое пользователем значение, которое будет передаваться функции сравнения. Вы можете использовать это значение любым путем, которым хотите. pCompareFunction ― это адрес задаваемой пользователем функции, которая будет определять результат сравнения item'ов в listview. Функция имеет следующий прототип: Код (ASM): CompareFunc proto lParam1:QWORD, lParam2:QWORD, lParamSort:QWORD lParam1 или lParam2 ― это значения параметра lParam LV_ITEM, который вы указали, когда вставляли элементы в listview. lParamSort ― это значение wParam, посланное вместе с сообщением LVM_SORTITEMS. Когда listview получает сообщение LVM_SORTITEMS, она вызывает сортирующую функцию, указанную в параметре lParam, когда ей нужно узнать результат сравнения двух элементов. Кратко говоря, функция сравнения будет решать, какой из двух элементов, посланных ей, будет предшествовать другому. Правило простое: если функция возвращается отрицательное значение, тогда первый элемент (указанный в lParam1) будет предшествовать другому. Если функция возвращает положительное значение, второй элемент (заданный параметром lParam2) должен предшествовать первому. Если оба равны, тогда функция должна возвратить ноль. Что заставляет этот метод работать, так это значение lParam структуры LV_ITEM. Если вам нужно отсортировать item'ы (например, когда пользователь кликает по заголовку колонки), вам нужно подумать о схеме сортировки, в которой будет использоваться значения параметра lParam. В данном примере я помещаю это поле индекс элемента, чтобы получить другую информация о нем, послав сообщение LVM_GETITEM. Заметьте, что когда элементы перегруппированы, их индексы также меняется. Поэтому когда сортировка в моем примере выполнена, мне необходимо обновить значения в lParam, чтобы учесть новые значения индексов. Если вы хотите отсортировать элементы, когда пользователь кликает по заголовку колонки, вам нужно обработать уведомительное сообщение LVN_COLUMNCLICK в вашей оконной процедуре. LVN_COLUMNCLICK передается вашему окну через сообщение WM_NOTIFY.
ПРАКТИКА ― СЕСТРА ШИЗОФРЕНИИЭтот пример создает listview и заполняем его именами и размерами полей текущей папки. Режим отображения элементов по умолчанию поставлен в 'отчет'. В этом режиме вы можете кликать по заголовку колонок и элементы будут отсортированы согласно восходящему/нисходящему порядку. Вы можете выбрать режим отображения в меню. Когда вы делает двойной клик по элементу, показывается окно с названием элемента.Вариант #1asm-файл Код (ASM): include win64a.inc IMAGE_BASE equ 400000h IDM_MAINMENU equ 10000 IDM_ICON equ LVS_ICON IDM_SMALLICON equ LVS_SMALLICON IDM_LIST equ LVS_LIST IDM_REPORT equ LVS_REPORT .code WinMain proc local msg:MSG push rbp mov ebp,esp sub esp,sizeof MSG xor ebx,ebx call InitCommonControls mov eax,10029h mov esi,IMAGE_BASE mov edi,offset ClassName push rax ;hIconSm push rdi ;lpszClassName push IDM_MAINMENU;lpszMenuName push COLOR_WINDOW+1;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 call RegisterClassEx push rbx push rsi ;rsi=400000h shl esi,9 ;rsi=CW_USEDEFAULT push rbx push rbx push 200 push 400 push rsi push rsi mov r9d,WS_OVERLAPPEDWINDOW or WS_VISIBLE mov r8,rdi ;offset ClassName mov edx,edi ;offset ClassName mov ecx,WS_EX_CLIENTEDGE sub esp,20h call CreateWindowEx push rbx;<-- align 10h push IMAGE_BASE push rbx;NULL push rax;hWnd push 240 push 400 push rbx;0 push rbx;0 mov r9d,LVS_REPORT+WS_CHILD+WS_VISIBLE mov r8,rbx;NULL mov edx,offset ListViewClassName xor ecx,ecx;NULL sub esp,20h call CreateWindowEx mov hList,rax call InsertColumn call lv_col_info_fill 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 InsertColumn proc LOCAL lvc:LV_COLUMN push rbp mov ebp,esp sub esp,(20h+sizeof LV_COLUMN+15)and(-16) lea r9,lvc mov [r9+LV_COLUMN.imask],LVCF_TEXT+LVCF_WIDTH mov eax,offset Heading1 mov [r9+LV_COLUMN.pszText],rax mov [r9+LV_COLUMN.lx],150 mov r8,rbx;0 mov edx,LVM_INSERTCOLUMN mov rcx,hList call SendMessage lea r9,lvc mov eax,offset Heading2 mov [r9+LV_COLUMN.pszText],rax mov [r9+LV_COLUMN.lx],100 mov r8d,1 mov edx,LVM_INSERTCOLUMN mov rcx,hList call SendMessage leave retn InsertColumn endp lv_col_info_fill proc LOCAL lvItem:LV_ITEM local old_rdi:QWORD push rbp mov ebp,esp sub esp,(28h+sizeof LV_ITEM+15)and(-16) mov old_rdi,rdi xor edi,edi @@: mov lvItem.imask,LVIF_TEXT + LVIF_PARAM mov rax,[handle+rdi*8] mov lvItem.pszText,rax mov lvItem.iSubItem,0 mov lvItem.iItem,edi mov lvItem.lParam,edi lea r9,lvItem mov r8d,0 mov edx,LVM_INSERTITEM mov rcx,hList call SendMessage mov lvItem.imask,LVIF_TEXT inc lvItem.iSubItem mov rax,[handle+rdi*8+8*5] mov lvItem.pszText,rax lea r9,lvItem mov r8d,0 mov edx,LVM_SETITEM mov rcx,hList call SendMessage inc edi cmp edi,5 jb @b @0: mov rdi,old_rdi leave retn lv_col_info_fill endp ;------------------------------------------------ lv_param_update proc LOCAL lvi:LV_ITEM local old_rdi:QWORD push rbp mov ebp,esp sub esp,(28h+sizeof LV_ITEM+15)and(-16) mov old_rdi,rdi mov r9,rbx mov r8,rbx mov edx, LVM_GETITEMCOUNT mov rcx,hList call SendMessage mov edi,eax mov lvi.imask,LVIF_PARAM mov lvi.iSubItem,0 mov lvi.iItem,0 @@: lea r9,lvi mov eax,lvi.iItem mov lvi.lParam,eax mov r8,rbx;0 mov edx, LVM_SETITEM mov rcx,hList call SendMessage inc lvi.iItem dec edi jnz @b;.endw mov rdi,old_rdi leave retn lv_param_update endp ;------------------------------------------------ lv_compare proc lParam1:QWORD,lParam2:QWORD,lParamSort:QWORD local buf1[100h]:BYTE local buf2[100h]:BYTE local lvi0:LV_ITEM local old_rdi:QWORD local old_rsi:QWORD push rbp mov ebp,esp sub esp,(30h+sizeof LV_ITEM+200h+15)and(-16) mov lParam2,rdx mov lParamSort,r8 mov lvi0.imask,LVIF_TEXT lea eax,buf1 mov lvi0.pszText,rax mov lvi0.cchTextMax,100h test r8b,00000010b;[lParamSort],1;2 jnz compare_3 compare_1:mov old_rdi,rdi mov old_rsi,rsi mov lvi0.iSubItem,1 lea r9,lvi0 mov r8,rcx;[lParam1] mov edx,LVM_GETITEMTEXT mov rcx,hList call SendMessage lea esi,buf1 call convert_strhval mov edi,eax lea r9,lvi0 mov r8,lParam2 mov edx,LVM_GETITEMTEXT mov rcx,hList call SendMessage lea esi,buf1 call convert_strhval test lParamSort,1 jne @f xchg edi,eax @@: sub eax,edi compare_1_exit: mov rdi,old_rdi mov rsi,old_rsi leave retn compare_3:mov lvi0.iSubItem,0 lea r9,lvi0 mov r8,rcx;[lParam1] mov edx,LVM_GETITEMTEXT mov rcx,hList call SendMessage lea ecx,buf2 ;destination lea edx,buf1 ;source call lstrcpy lea r9,lvi0 mov r8,lParam2 mov edx,LVM_GETITEMTEXT mov rcx,hList call SendMessage lea edx,buf2 lea ecx,buf1 test lParamSort,1 jne @f xchg edx,ecx @@: call lstrcmpi leave retn lv_compare endp ;--------------------------------------- convert_strhval proc push rbp mov ebp,esp sub esp,20h mov ecx,esi call lstrlen mov ecx,eax jrcxz b xor eax,eax cdq @@: imul edx,10 lodsb lea edx,[rdx+rax-'0'] loop @b mov eax,edx b: leave ret convert_strhval endp ;---------------------------------------- WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM push rbp mov ebp,esp sub esp,30h mov hWnd,rcx mov wParam,r8 mov lParam,r9 cmp edx,WM_NOTIFY je wmNOTIFY cmp edx,WM_SIZE je wmSIZE cmp edx,WM_DESTROY je wmDESTROY wmDEFAULT:leave jmp DefWindowProc wmDESTROY:xor ecx,ecx call ExitProcess wmNOTIFY:mov rdi,r9;[lParam] mov rax,[rdi + NMHDR.hwndFrom] cmp rax,hList jne wmDEFAULT cmp [rdi + NMHDR._code],LVN_COLUMNCLICK jne wmDEFAULT wmNOTIFY_LVN_COLUMNCLICK: cmp [rdi + NM_LISTVIEW.iSubItem+4],1 jne COLUMNCLICK_FILE COLUMNCLICK_SIZE:xor lvSortSize,1 mov r8d,lvSortSize jmp @f COLUMNCLICK_FILE:xor lvSortFile,1 mov r8d,lvSortFile @@: mov r9d,lv_compare mov edx,LVM_SORTITEMS mov rcx,hList call SendMessage call lv_param_update jmp wmBYE wmSIZE: mov eax,r9d;lParam and r9,0FFFFh shr eax,16 mov qword ptr [rsp+28h],TRUE mov [rsp+20h],rax xor edx,edx mov r8,rdx mov rcx,hList call MoveWindow wmBYE: leave ret WndProc endp ;------------------------------------------------------------- ClassName db 'Win64 Iczelion''s lesson #31a: ListView Control',0 ListViewClassName db 'SysListView32',0 Heading1 db 'Filename',0 Heading2 db 'Size',0 hList dq ? hMenu dq ? lvSortSize dd 0 lvSortFile dd 2 lvI1a db 'Durian',0 lvI1b db '34',0 lvI2a db 'Banana',0 lvI2b db '54',0 lvI3a db 'Watermelon',0 lvI3b db '44',0 lvI4a db 'Apple',0 lvI4b db '55',0 lvI5a db 'Papaya',0 lvI5b db '1',0 handle dq lvI1a,lvI2a,lvI3a,lvI4a,lvI5a,lvI1b,lvI2b,lvI3b,lvI4b,lvI5b end rc-файл Код (C): #include "resource.h" #define IDM_MAINMENU 10000 #define IDM_ICON LVS_ICON #define IDM_SMALLICON LVS_SMALLICON #define IDM_LIST LVS_LIST #define IDM_REPORT LVS_REPORT IDM_MAINMENU MENU { POPUP "&View" { MENUITEM "&Icon View",IDM_ICON MENUITEM "&Small Icon View",IDM_SMALLICON MENUITEM "&List View", IDM_LIST MENUITEM "&Report View",IDM_REPORT } } Вариант #2rc-файл Код (C): #define MI_ICON 0 #define MI_REPORT 1 #define MI_SMALLICON 2 #define MI_LIST 3 #define M_MAIN 10000 M_MAIN MENU { POPUP "&View" { MENUITEM "Lar&ge Icons",MI_ICON MENUITEM "S&mall Icons",MI_SMALLICON MENUITEM "&List",MI_LIST MENUITEM "&Details",MI_REPORT } }
asm-файл Код (ASM): include win64a.inc IMAGE_BASE equ 400000h M_MAIN equ 10000 MI_ICON equ 0 MI_SMALLICON equ 2 MI_LIST equ 3 MI_REPORT equ 1 .code WinMain proc local msg:MSG local rect:RECT xor ebx,ebx invoke InitCommonControls mov edi,offset ClassName mov eax,10027h mov esi,IMAGE_BASE push rax ;hIconSm push rdi ;lpszClassName push M_MAIN ;lpszMenuName push COLOR_WINDOW+1;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 call RegisterClassEx push rbx push rsi shl esi,9 push rbx push rbx push 240 push 400 push rsi push rsi mov r9d,WS_OVERLAPPEDWINDOW+WS_VISIBLE mov r8d,edi mov edx,edi mov ecx,WS_EX_CLIENTEDGE sub esp,20h call CreateWindowEx mov hwnd,rax lea edx,rect mov ecx,eax call GetClientRect push rbx;<-- align 10h push IMAGE_BASE push rbx push hwnd mov eax,rect.bottom push rax mov eax,rect.right push rax push rbx push rbx mov r9d,LVS_REPORT + WS_CHILD + WS_VISIBLE mov r8,rbx mov edx,offset ctlClsNameLv xor ecx,ecx sub esp,20h call CreateWindowEx mov hList,rax call lv_col_insert call lv_col_info_fill mov r9d,0C9B5AFh mov r8,rbx mov edx,LVM_SETTEXTCOLOR mov rcx,hList call SendMessage mov r9d,50352Eh mov r8,rbx mov edx,LVM_SETBKCOLOR mov rcx,hList call SendMessage mov r9d,50352Eh mov r8,rbx mov edx,LVM_SETTEXTBKCOLOR mov rcx,hList call SendMessage mov rcx,hwnd call GetMenu mov menuH,rax mov qword ptr [rsp+20h],MF_CHECKED mov r9d,MI_REPORT mov r8d,MI_LIST mov edx,MI_ICON mov ecx,eax call CheckMenuRadioItem lea edi,msg window_message_loop_start: mov ecx,edi xor edx,edx mov r8,rbx mov r9,rbx call GetMessage mov ecx,edi call DispatchMessage jmp window_message_loop_start WinMain endp WndProc proc hWnd:HWND,uMsg:UINT,wParam:WPARAM,lParam:LPARAM push rbp mov ebp,esp sub esp,30h mov hWnd,rcx mov wParam,r8 mov lParam,r9 mov rdi,r9;lParam cmp edx,WM_DESTROY je wmDESTROY cmp edx,WM_SIZE je wmSIZE cmp edx,WM_NOTIFY je wmNOTIFY cmp edx,WM_COMMAND je wmCOMMAND wmDEFAULT:leave jmp DefWindowProc wmDESTROY:xor ecx,ecx call ExitProcess wmSIZE: mov eax,r9d;eax=[lParam] shr eax,16;eax=[lParam+2] and r9,0FFFFh mov qword ptr [rsp+28h],TRUE mov [rsp+20h],rax xor edx,edx mov r8,rdx mov rcx,hList call MoveWindow jmp wmBYE wmNOTIFY:mov rdi,r9;lParam mov rax,[rdi + NMHDR.hwndFrom] cmp rax,hList jne wmDEFAULT cmp [rdi + NMHDR._code],LVN_COLUMNCLICK je wmNOTIFY_LVN_COLUMNCLICK cmp [rdi + NMHDR._code],NM_DBLCLK jne wmDEFAULT wmNOTIFY_NM_DBLCLK:call lv_item_focus jmp wmBYE wmNOTIFY_LVN_COLUMNCLICK: cmp [rdi + NM_LISTVIEW.iSubItem+4],1 jne COLUMNCLICK_FILE COLUMNCLICK_SIZE:xor lvSortSize,1 mov r8d,lvSortSize jmp @f COLUMNCLICK_FILE:xor lvSortFile,1 mov r8d,lvSortFile @@: mov r9d,offset lv_compare mov edx,LVM_SORTITEMS mov rcx,hList call SendMessage call lv_param_update jmp wmBYE wmCOMMAND:or r9,r9;cmp [lParam],0 jne wmBYE mov edx,GWL_STYLE mov rcx,hList call GetWindowLongPtr and eax,not LVS_TYPEMASK movzx edi,word ptr wParam or eax,edi mov r8,rax mov edx,GWL_STYLE mov rcx,hList call SetWindowLongPtr push MF_CHECKED mov r9,rdi mov r8d,MI_LIST mov edx,MI_ICON mov rcx,menuH call CheckMenuRadioItem wmBYE: leave retn WndProc endp lv_item_focus proc local lvi4:LV_ITEM local buf4[256]:BYTE mov r9d,LVNI_FOCUSED or r8d,-1 mov edx,LVM_GETNEXTITEM mov rcx,hList call SendMessage lea r9,lvi4 mov lvi4.iItem,eax mov lvi4.iSubItem,0;ebx mov lvi4.imask,LVIF_TEXT lea eax,buf4 mov lvi4.pszText,rax mov lvi4.cchTextMax,256 xor r8d,r8d mov edx,LVM_GETITEM mov rcx,hList call SendMessage xor r9d,r9d;MB_OK mov r8d,offset ClassName lea edx,buf4 xor ecx,ecx call MessageBox leave retn lv_item_focus endp lv_param_update proc local lvi1:LV_ITEM local old_rdi:QWORD mov old_rdi,rdi xor r9d,r9d xor r8d,r8d mov edx,LVM_GETITEMCOUNT mov rcx,hList call SendMessage mov edi,eax mov lvi1.imask,LVIF_PARAM mov lvi1.iSubItem,0 mov lvi1.iItem,0 mov lvi1.lParam,0 or eax,eax je wmBYE @@: lea r9,lvi1 xor r8d,r8d mov edx,LVM_SETITEM mov rcx,hList call SendMessage inc lvi1.iItem inc lvi1.lParam dec edi jnz @b wmBYE: mov rdi,old_rdi leave retn lv_param_update endp convert_strhval proc sub esp,28h mov ecx,esi call lstrlen or eax,eax je f12 mov ecx,eax xor eax,eax cqo @@: imul edx,10 ; edx=edx*10 lodsb ; mov al,[esi]/inc esi lea edx,[rax+rdx-'0'] ; edx=edx*10+eax loop @b mov eax,edx f12: add esp,28h retn convert_strhval endp lv_compare proc lParam1:QWORD,lParam2:QWORD,lParamSort:QWORD local lvi0:LV_ITEM local buf1[255]:BYTE local buf2[255]:BYTE local old_rdi:QWORD local old_rsi:QWORD mov lParam1,rcx mov lParam2,rdx mov lParamSort,r8 mov old_rdi,rdi mov old_rsi,rsi mov lvi0.imask,LVIF_TEXT lea eax,buf1 mov lvi0.pszText,rax mov lvi0.cchTextMax,255 cmp lParamSort,1 ja compare_3 compare_1:lea r9,lvi0 mov [r9+LV_ITEM.iSubItem],1 mov r8,lParam1 mov edx,LVM_GETITEMTEXT mov rcx,hList call SendMessage lea esi,buf1 call convert_strhval mov edi,eax lea r9,lvi0 mov r8,lParam2 mov edx,LVM_GETITEMTEXT mov rcx,hList call SendMessage lea esi,buf1 call convert_strhval sub edi,eax cmp lParamSort,1 jne compare_3_exit mov eax,edi jmp compare_3_exit compare_3:lea r9,lvi0 mov [r9+LV_ITEM.iSubItem],0 mov r8,lParam1 mov edx,LVM_GETITEMTEXT mov rcx,hList call SendMessage lea ecx,buf2 ;destination lea edx,buf1 ;source call lstrcpy lea r9,lvi0 mov r8,lParam2 mov edx,LVM_GETITEMTEXT mov rcx,hList call SendMessage lea edx,buf2 lea ecx,buf1 cmp lParamSort,3 jne @f xchg edx,ecx @@: call lstrcmpi compare_3_exit:mov rdi,old_rdi mov rsi,old_rsi leave ret lv_compare endp lv_col_insert proc local lvCol:LV_COLUMN mov lvCol.imask,LVCF_TEXT + LVCF_WIDTH mov eax,offset lvTxt1 mov lvCol.pszText,rax mov lvCol.lx,150 lea r9,lvCol mov r8,rbx mov edx,LVM_INSERTCOLUMN mov rcx,hList call SendMessage or lvCol.imask,LVCF_FMT mov lvCol.fmt,LVCFMT_RIGHT mov eax,offset lvTxt2 mov lvCol.pszText,rax mov lvCol.lx,100 lea r9,lvCol mov r8d,1 mov edx,LVM_INSERTCOLUMN mov rcx,hList call SendMessage leave retn lv_col_insert endp lv_col_info_fill proc local old_rdi:QWORD push rbp mov ebp,esp sub esp,30h mov old_rdi,rdi mov edx,offset fData mov ecx,offset fTxt1 call FindFirstFile inc eax ; eax = INVALID_HANDLE_VALUE ? je fill_exit dec eax mov fFileH,rax xor edi,edi fill_start:cmp fData.dwFileAttributes,FILE_ATTRIBUTE_DIRECTORY je @f call lv_col_info_insert inc edi @@: mov edx,offset fData mov rcx,fFileH call FindNextFile or eax,eax ; eax <> 0 ? jne fill_start fill_end:mov rcx,fFileH call FindClose fill_exit:mov rdi,old_rdi leave retn lv_col_info_fill endp lv_col_info_insert proc local lvi2:LV_ITEM local buf3[20]:BYTE lea r9,lvi2 mov [r9+LV_ITEM.imask],LVIF_TEXT + LVIF_PARAM mov eax,offset fData.cFileName mov [r9+LV_ITEM.pszText],rax mov [r9+LV_ITEM.iSubItem],ebx mov [r9+LV_ITEM.iItem],edi;? mov [r9+LV_ITEM.lParam],edi mov r8,rbx mov edx,LVM_INSERTITEM mov rcx,hList call SendMessage mov lvi2.imask,LVIF_TEXT inc lvi2.iSubItem lea eax,buf3 mov lvi2.pszText,rax mov r8d,fData.nFileSizeLow mov edx,offset lvTxt3 mov ecx,eax call wsprintf lea r9,lvi2 mov r8,rbx mov edx,LVM_SETITEM mov rcx,hList call SendMessage leave ret lv_col_info_insert endp ;data hwnd dq ? ClassName db 'Win64 Iczelion''s lesson #31b: listview',0 ctlClsNameLv db 'SysListView32',0 hList dq ? lvTxt1 db 'File Name',0 lvTxt2 db 'Size',0 lvTxt3 db '%lu',0 lvSortSize dd 0 lvSortFile dd 2;0 menuH dq ? fFileH dq ? fData WIN32_FIND_DATA <> fTxt1 db '*.*',0 end Разбор полётовПервое, что должна сделать программа после того, как создано основное окно - это создать listview. Код (ASM): wmCREATE: invoke CreateWindowEx, NULL, addr ListViewClassName, NULL, \ LVS_REPORT+WS_CHILD+WS_VISIBLE, 0,0,0,0,hWnd, NULL, hInstance, NULL mov hList, eax Мы вызываем CreateWindowEx, передавая ей имя класса окна "SysListView32". Режим отображения по умолчанию задан стилем LVS_REPORT. Код (ASM): invoke InsertColumn После того, как создан listview, мы вставляем в него колонку. Код (ASM): LOCAL lvc:LV_COLUMN mov lvc.imask,LVCF_TEXT+LVCF_WIDTH mov lvc.pszText,offset Heading1 mov lvc.lx,150 invoke SendMessage,hList, LVM_INSERTCOLUMN, 0, addr lvc
Мы указываем название и ширину первой колонки, в которой будут отображаться имена файлов, в структуре LV_COLUMN, поэтому нам нужно установить в imask флаги LVCF_TEXT и LVCF_WIDTH. Мы заполняем pszText адресом названия и lx ― шириной колонки в пикселях. Когда все сделано, мы посылаем сообщение LVM_INSERTCOLUMN listview, передавая ей структуру. Код (ASM): or lvc.imask,LVCF_FMT mov lvc.fmt,LVCFMT_RIGHT После вставки первой колонки, мы вставляем следующую, в которой будут отображаться размеры файлов. Так как нам нужно, чтобы размеры файлов выравнивались по правой стороне, нам необходимо указать флаг в параметре fmt, LVCFMT_RIGHT. Мы также указываем флаг LVCF_FMT в imask, в добавление к LVCF_TEXT и LVCF_WIDTH. Код (ASM): mov lvc.pszText,offset Heading2 mov lvc.lx,100 invoke SendMessage,hList, LVM_INSERTCOLUMN, 1 ,addr lvc Оставшийся код прост. Помещаем адреса названия в pszText и ширину в lx. Затем посылаем сообщение LVM_INSERTCOLUMN listview, указывая номер колонки и адрес структуры. Когда колонки вставлены, мы можем заполнить listview элементами. Код (ASM): invoke FillFileInfo В FillFileInfo содержится следующий код. Код (ASM): FillFileInfo proc uses edi LOCAL finddata:WIN32_FIND_DATA LOCAL FHandle:DWORD invoke FindFirstFile,addr FileNamePattern,addr finddata Мы вызываем FindFirstFile, чтобы получить информацию о первом файле, который отвечает заданным условиям. У FindFirstFile следующий прототип: Код (C): HANDLE WINAPI FindFirstFile( _In_ LPCTSTR lpFileName, _Out_ LPWIN32_FIND_DATA lpFindFileData ); pFileName ― это адрес имени файла, который надо искать. Эта строка может содержать "дикие" символы. В нашем примере мы используем *.*, чтобы искать все файлы в данной папке. pWin32_Find_Data ― это адрес структуры WIN32_FIND_DATA, которая будет заполнена информацией о файле (если что-нибудь будет найдено). Эта функция возвращает INVALID_HANDLE_VALUE в rax, если не было найдено соответствующих заданным критериям файлов. Иначе она возвратит дескриптор поиска, который будет использован в последующих вызовах FindNextFile. Код (ASM): .if eax!=INVALID_HANDLE_VALUE mov FHandle,eax xor edi,edi Если файл будет найден, мы сохраним дескриптор поиска в переменную, а потом обнулим rdi, который будет использован в качестве индекса элемента (номер ряда). Код (ASM): .while eax!=0 test finddata.dwFileAttributes,FILE_ATTRIBUTE_DIRECTORY .if ZERO? В этой главе мы не будем иметь дело с папками, поэтому отфильтровываем их проверяя параметр dwFileAttributes на предмет наличия установленного флага FILE_ATTRIBUTE_DIRECTORY. Если он есть ― сразу переходим к вызову FindNextFile. Код (ASM): invoke ShowFileInfo,edi, addr finddata inc edi .endif invoke FindNextFile,FHandle,addr finddata .endw Вставляем имя и размер файла в listview вызывая функцию ShowFileInfo. Затем мы повышаем значение rdi (текущий номер столбца). И, наконец, делаем вызов FindNextFile, чтобы найти следующий файл в нашей папке, пока FindNextFile не возвратит 0, что означает то, что больше файлов найдено не было. Код (ASM): invoke FindClose,FHandle .endif ret FillFileInfo endp Когда все файлы в ткущей папке найдены, мы должны закрыть дескриптор поиска. Теперь давайте взглянем на функцию ShowFileInfo. Эта функция принимает два параметра, индекс элемента (номер ряда) и адрес структуры WIN32_FIND_DATA. Код (ASM): ShowFileInfo proc uses edi row:DWORD, lpFind:DWORD LOCAL lvi:LV_ITEM LOCAL buffer[20]:BYTE mov edi,lpFind assume edi:ptr WIN32_FIND_DATA Сохраняем адрес структуры WIN32_FIND_DATA в rdi. Код (ASM): mov lvi.imask,LVIF_TEXT+LVIF_PARAM push row pop lvi.iItem mov lvi.iSubItem,0 Мы предоставляем название элемента и значение lParam, поэтому мы помещаем флаги LVIF_TEXT и LVIF_PARAM в imask. Затем мы устанавливаем приравниваем iItem номер ряда, переданный функции и, так как это главный элемент, мы должны приравнять iSubItem нулю (колонка 0). Код (ASM): lea eax,[edi].cFileName mov lvi.pszText,eax push row pop lvi.lParam Затем мы помещаем адрес названия, в данном случая это имя файла в структуре WIN32_FIND_DATA, в pszText. Так как мы реализуем свою сортировку, мы должны заполнить lParam определенным значением. Помещаем номер ряда в это параметр, чтобы получать информацию об элементе по его индексу. Код (ASM): invoke SendMessage,hList, LVM_INSERTITEM,0, addr lvi Когда все необходимые поля в LV_ITEM заполнены, мы посылаем сообщение LVM_INSERTITEM listview, чтобы вставить в него элемент. Код (ASM): mov lvi.imask,LVIF_TEXT inc lvi.iSubItem invoke wsprintf,addr buffer, addr template,[edi].nFileSizeLow lea eax,buffer mov lvi.pszText,eax Мы установим подэлементы, ассоциированные с элементом. Подэлемент может иметь только название. Поэтому мы указываем в imask LVIF_TEXT. Затем указываем в iSubItem колонку, в которой должен находиться подэлемент. В этом случае мы устанавливаем его в 1. Названием этого элемента будет являться размер файла. Сначала нужно сконвертировать его значение в строку, вызвать wsprintf. Затем помещаем адрес строки в pszText. Код (ASM): invoke SendMessage,hList,LVM_SETITEM, 0,addr lvi assume edi:nothing ret ShowFileInfo endp Когда все требуемые поля в LV_ITEM заполнены, мы посылаем сообщение LVM_SETITEM listview, передавая ему адрес структуры LV_ITEM. Заметьте, что мы используем LVM_SETITEM, а не LVM_INSERTITEM, потому что подэлемент считается свойством элемента. Поэтому устанавливаем свойство элемента, а не вставляем новый элемент. Когда все элементы вставлены в listview, мы устанавливаем текст и цвет фона контрола listview. Код (ASM): RGB 255,255,255 invoke SendMessage,hList,LVM_SETTEXTCOLOR,0,eax RGB 0,0,0 invoke SendMessage,hList,LVM_SETBKCOLOR,0,eax RGB 0,0,0 invoke SendMessage,hList,LVM_SETTEXTBKCOLOR,0,eax Мы устанавливаем цвет текста и цвет фона с помощью сообщений LVM_SETTEXTCOLOR и LVM_SETTEXTBKCOLOR. Мы устанавливаем цвет фона listview сообщением LVM_SETBKCOLOR. Код (ASM): invoke GetMenu,hWnd mov hMenu,eax invoke CheckMenuRadioItem,hMenu,IDM_ICON,IDM_LIST, IDM_REPORT,MF_CHECKED Мы позволим пользователю выбирать режимы отображения через меню. Поэтому мы должны получить сначала дескриптор меню. Чтобы помочь пользователю переключать режимы отображения, мы помещаем в меню систему radio button'ов. Для этого нам понадобится функция CheckMenuRadioItem. Эта функция поместит radio button перед пунктом меню. Заметьте, что мы создаем окно listview с шириной и высотой равной нулю. Оно будет менять размер каждый pаз, когда будет менять размер родительское окно. В этом случае мы можем быть уверены, что размер listview всегда будет соответствовать родительскому окну. В нашем примере нам требуется, чтобы listview занимал всю клиентскую область родительского окна. Код (ASM): wmSIZE: mov eax,lParam mov edx,eax and eax,0ffffh shr edx,16 invoke MoveWindow,hList, 0, 0, eax,edx,TRUE Когда родительское окно получает сообщение WM_SIZE, нижнее слово lParam содержит новую ширину клиентской области и верхнее словно новой высоты. Тогда мы вызываем MoveWindow, чтобы изменить размер listview, чтобы тот покрывал всю клиентскую область родительского окна. Когда пользователь выберет режим отображения в меню, мы должны соответственно отреагировать. Мы устанавливаем новый стиль контрола listview функцией SetWindowLong. Код (ASM): wmCOMMAND: .if lParam==0 invoke GetWindowLong,hList,GWL_STYLE and eax,not LVS_TYPEMASK Сначала мы получаем текущие стили listview. Затем мы стираем старый стиль отображения. LVS_TYPEMASK ― это комбинированное значение всех четырех стилей отображения. Поэтому когда мы выполняем логическое умножение текущих флагов стилей со значением "not LVS_TYPEMASK", стиль текущего отображения стирается. Во время проектирования меню я немного сжульничал. Я использовал в качестве ID пунктов меню константы стилей отображения. Код (ASM): IDM_ICON equ LVS_ICON IDM_SMALLICON equ LVS_SMALLICON IDM_LIST equ LVS_LIST IDM_REPORT equ LVS_REPORT Поэтому, когда родительское окно получает сообщение WM_COMMAND, нужный стиль отображения находится в нижнем слове wParam'а (как ID пункта меню). Код (ASM): mov edx,wParam and edx,0FFFFh Мы получили стиль отображения в нижнем слове wParam. Все, что нам теперь нужно, это обнулить верхнее слово. Код (ASM): push edx or eax,edx И добавить стиль отображения к уже существующим стилям (текущий стиль отображения мы ранее оттуда убрали). Код (ASM): invoke SetWindowLong,hList,GWL_STYLE,eax И установить новые стили функцией SetWindowLong. Код (ASM): pop edx invoke CheckMenuRadioItem,hMenu,IDM_ICON,IDM_LIST, edx,MF_CHECKED .endif Нам также требуется поместить radio button перед выбранным пунктом меню. Поэтому мы вызываем CheckMenuRadioItem, передавая ей текущий стиль отображения (а также ID пункта меню). Когда пользователь кликает по заголовку колонки в режиме отчета, нам нужно отсортировать элементы в listview. Мы должны отреагировать на сообщение WM_NOTIFY. Код (ASM): wmNOTIFY: push edi mov edi,lParam assume edi:ptr NMHDR mov eax,[edi].hwndFrom .if eax==hList
Когда мы получаем сообщение WM_NOTIFY, lParam содержит указатель на структуру NMHDR. Мы можем проверить, пришло ли это сообщение от listview, сравнив параметр hwndFrom структуры NMHDR с дескриптором контрола listview. Если они совпадают, мы можем заключить, что уведомление пришло от listview. Код (ASM): .if [edi].code==LVN_COLUMNCLICK assume edi:ptr NM_LISTVIEW Если уведомление пришло от listview, мы проверяем, равен ли код LVN_COLUMNCLICK. Если это так, это означает, что пользователь кликает на заголовке колонки. В случае, что код равен LVN_COLUMNCLICK, мы считаем, что lParam содержит указатель на структуру NM_LISTVIEW, которая является супермножеством по отношению к структуре NMHDR (то есть включает ее). Затем нам нужно узнать, по какому заголовку колонки кликнул пользователь. Эту информацию мы получаем из параметра iSubItem. Его значение можно считать номером колонки (отсчет начинается с нуля). Код (ASM): .if [edi].iSubItem==1 .if SizeSortOrder==0 || SizeSortOrder==2 Если iSubItem равен 1, это означает, что пользователь кликнул по второй колонке. Мы используем глобальные переменные, чтобы сохранять текущий статус порядка сортировки. 0 означает "еще не отсортировано", 1 значит "восходящая сортировка", а 2 ― "нисходящая сортировка". Если элементы/подэлементы в колонке ранее не были отсортированы или отсортированы по нисходящей, то мы устанавливаем сортировку по восходящей. Код (ASM): invoke SendMessage,hList,LVM_SORTITEMS,1,addr CompareFunc Мы посылаем сообщение LVM_SORTITEMS listview, передавая 1 через wParam и адрес нашей сравнивающей функции через lParam. Заметьте, что значение в wParam задается пользователем, вы можете использовать его как хотите. Я использовал его в нашем примере как метод сортировки. Сначала мы взглянем на сравнивающую функцию. Код (ASM): CompareFunc proc uses edi lParam1:DWORD, lParam2:DWORD, SortType:DWORD LOCAL buffer[256]:BYTE LOCAL buffer1[256]:BYTE LOCAL lvi:LV_ITEM mov lvi.imask,LVIF_TEXT lea eax,buffer mov lvi.pszText,eax mov lvi.cchTextMax,256 В сравнивающей функции контрол listview будет передавать lParam'ы (через LV_ITEM) двух элементов, которые нужно сравнить, через lParam1 и lParam2. Вспомните, что мы помещаем индекс элемента в lParam. Таким образом мы можем получить информацию об элементах, используя эти индексы. Информация, которая нам нужна ― это названия сортирующихся элементов/подэлементов. Мы подготавливаем структуру LV_ITEM для этого, указывая в imask LVIF_TEXT и адрес буфера в pszText и размер буфера в cchTextMax. Код (ASM): .if SortType==1 mov lvi.iSubItem,1 invoke SendMessage,hList,LVM_GETITEMTEXT,lParam1,addr lvi Если значение SortType равно 1 или 2, мы знаем, что кликнута колонка размера файла. 1 означает, что необходимо отсортировать элементы в нисходящем порядке. 2 значит обратное. Таким образом мы указываем iSubItem равным 1 (чтобы задать колонку размера) и посылаем сообщение LVM_GETITEMTEXT контролу listview, чтобы получить название (строку с размером файла) подэлемента. Код (ASM): invoke String2Dword,addr buffer mov edi,eax Конвертируем строку в двойное слово с помощью функции String2Dword. Она возвращает dword-значение в eax. Мы сохраняем ее в edi для последующего сравнения. Код (ASM): invoke SendMessage,hList,LVM_GETITEMTEXT,lParam2,addr lvi invoke String2Dword,addr buffer sub edi,eax mov eax,edi Тоже самое мы делаем и с lParam2. После получения размеров обоих файлов, мы можем сравнить их. Правила, которых придерживается функция сравнения, следующие: Если первый элемент должен предшествовать другому, вы должны возвратить отрицательное значение через eax. Если второй элемента должен предшествовать первому, вы должны возвратить через eax положительное значение. Если оба элемента равны, вы должны возвратить ноль. В нашем случае нам нужно отсортировать элементы согласно их размерам в восходящем порядке. Поэтому мы просто можем вычесть размер первого элемента из второго и возвратить результат в rax. Код (ASM): .elseif SortType==3 mov lvi.iSubItem,0 invoke SendMessage,hList,LVM_GETITEMTEXT,lParam1,addr lvi invoke lstrcpy,addr buffer1,addr buffer invoke SendMessage,hList,LVM_GETITEMTEXT,lParam2,addr lvi invoke lstrcmpi,addr buffer1,addr buffer В случае, если пользователь кликнет по колонке с именем файла, мы должны сравнивать имена файлов. Мы должны получить имена файлов, а затем сравнить их с помощью функции lstrcmpi. Мы можем возвратить значение, возвращаемое этой функцией, так как оно использует те же правила сравнения. После того, как элементы отсортированы, нам нужно обновить значения lParam'ов для всех элементов, чтобы учесть изменившиеся индексы элементов, поэтому мы вызываем функцию UpdatelParam. Код (ASM): invoke UpdatelParam mov SizeSortOrder,1 Эта функция просто-напросто перечисляет все элементы в listview и обновляет значения lParam. Нам требуется это делать, иначе следующая сортировка не будет работать как ожидается, потому что мы исходим из того, что значение lParam ― это индекс элемента. Код (ASM): .elseif [edi].code==NM_DBLCLK invoke ShowCurrentFocus .endif Когда пользователь делает двойной клик на элементе, нам нужно отобразить окно с сообщение с названием элемента. Мы должны проверить, равно ли поле code в NMHDR NM_DBLCLK. Если это так, мы можем перейти к получению названия и отображению его в окне с сообщением. Код (ASM): ShowCurrentFocus proc LOCAL lvi:LV_ITEM LOCAL buffer[256]:BYTE invoke SendMessage,hList,LVM_GETNEXTITEM,-1, LVNI_FOCUSED Как мы может узнать, по какому элементу кликнули два раза? Когда элемент кликнут (одинарным или двойным нажатием), он получает фокус. Даже если выбрано несколько элементов, фокус будет только у одного. Наши задача заключается в том, чтобы найти элемент у которого находится фокус. Мы делаем это, посылая сообщение LVM_GETNEXTITEM контролу listview, указав желаемое состояние элемента в lParam. -1 в wParam означает поиск по всем элементам. Индекс элемента возвращается в eax. Код (ASM): mov lvi.iItem,eax mov lvi.iSubItem,0 mov lvi.imask,LVIF_TEXT lea eax,buffer mov lvi.pszText,eax mov lvi.cchTextMax,256 invoke SendMessage,hList,LVM_GETITEM,0,addr lvi Затем мы получаем название элемента с помощью сообщения LVM_GETITEM. Код (ASM): invoke MessageBox,0, addr buffer,addr AppName,MB_OK И наконец, мы отображаем название элемента в окне сообщения. Если вы хотите узнать, как использовать в контроле listview иконки, вы можете прочитать об этом в главе о treeview. В случае с listview надо будет сделать примерно то же самое.
Глава сорок седьмая. Братец Кролик создает MDI-приложение Скачайте пример здесь. Теория ― МАТЬ СКЛЕРОЗАMDI-интерфейс (MDI ― Multiple Document Interface ― многодокументный интерфейс), позволяет пользователю работать одновременно с несколькими документами и не обязательно одного типа. При этом однодокументные приложения (SDI ― Single Document Interface ― однодокументный интерфейс) позволяют работать только с одним документом (пример, Notepad.exe). Notepad может обрабатывать только один документ за раз. Если вы хотите открыть другой документ, вам нужно закрыть предыдущий. Как вы можете себе представить, это довольно неудобно. Сравните его с Microsoft Word: тот может держать открытыми различные документы в одно и то же время и позволяет пользователю выбирать, какой документ использовать. У MDI-приложений есть несколько характеристик, присущих только им. Вот некоторые из них: Внутри основного окна может быть несколько дочерних окон в пределах клиентской области. Когда вы сворачиваете окно, он сворачивается к нижнему левому углу клиентской области основного окна. Когда вы разворачиваете окно, его заголовок сливается с заголовком главного окна. Вы можете закрыть дочернее окно, нажав Ctrl+F4 и переключатся между дочерними окнами, нажав на Ctrl+Tab. Главное окно, которое содержит дочерние окно, называется фреймовым окном. Его клиентская область ― это место, где находятся дочерние окна, поэтому оно и называется фреймовым (frame ― рамка, кадр). Его работа чуть более сложна, чем задачи обычного окна, так как оно обеспечивает работу MDI. Чтобы контролировать дочерние окна в клиентской области, вам нужно специальное окно, которое называется клиентским окном. Вы можете считать это клиентское окно прозрачным окном, покрывающим всю клиентскую область окном фрейма. Рисунок 1. Иерархия MDI-приложенияСоздание окна фреймаТеперь мы переключим наше внимание на детали. Прежде всего вам нужно создать окно фрема. Оно создается примерно таким же образом, как и обычное окно: с помощью вызова CreateWindowEx. Есть два основных отличия от создания обычного окна. Первое различие состоит в том, что вы ДОЛЖНЫ вызывать DefFramProc вместо NtdllDefWindowProc_ для обработки Windows-сообщение вашему окну, которые вы не хотите обрабатывать самостоятельно. Это единственный способ заставить Windows делать за вас грязную работу по управлению MDI-приложение. Если вы забудете использовать DefFramProc, ваше приложение не будет иметь MDI-свойств. DefFrameProc имеет следующий синтаксис: Код (C): LRESULT WINAPI DefFrameProc( _In_ HWND hWnd, _In_ HWND hWndMDIClient, _In_ UINT uMsg, _In_ WPARAM wParam, _In_ LPARAM lParam ); Если вы сравните DefFramProc с NtdllDefWindowProc_, вы заметите, что разница между ними состоит в том, что у DefFrameProc пять параметров, в то время как у NtdllDefWindowProc_ ― четыре. Дополнительный параметр ― это дескриптор клиентского окна. Это дескриптор необходим для того, чтобы Windows могла посылать MDI-сообщения клиентскому окну. Второе различие заключается в том, что вы должны вызывать TranslateMDISysAccel в цикле обработки сообщений вашего окна фрейма. Это необходим, если вы хотите, что Windows обрабатывала нажатия на комбинации клавиш, связанных с MDI, такие как Ctrl+F4, Ctrl+Tab. У этой функции следующий прототип: Код (C): BOOL WINAPI TranslateMDISysAccel( _In_ HWND hWndClient, _In_ LPMSG lpMsg ); Первый параметр ― это дескриптор клиентского окна. Для вас не должно быть сюрпризом, что клиентское окно будет родителем окном для все дочерних MDI-окон. Второй параметр ― это адрес MSG-структуры, которую вы заполните с помощью функции GetMessage. Идея состоит в том, чтобы передать MSG-структуру клиентскому окну, что оно могло проверить, содержит ли эта структура искомые комбинации клавиш. Если это так, она обрабатывает само сообщение и возвращает ненулевое значение, или, в противном случае, FALSE. Этапы создания окно фрейма: Заполняем структуру WNDCLASSEX как обычно. Регистрируем класс окна фрейма, вызвав RegisterClassEx. Создаем окно фрейма с помощью CreateWindowEx. Внутри цикла обработки сообщений вызываем TranslateMDISysAccel. Внутри процедуры окна передаем необрабатанные сообщения DefFrameProc вместо NtdllDefWindowProc_. Создание клиентского окнаТеперь, когда у нас есть окно фрейма, мы можем создать клиентское окно. Класс клиентского окна перерегистрирован Windows. Имя этого класса ― "MDICLIENT". Вам также нужно передать адрес структуры CLIENTCREATESTRUCT функции CreateWindowEx. Эта структура имеет следующее определение: Код (ASM): CLIENTCREATESTRUCT STRUC hWindowMenu HANDLE ? idFirstChild UINT ? Reserved DWORD ? CLIENTCREATESTRUCT ENDS hWindowMenu ― это описатель подменю, к которому Windows присоединит список имен дочерних MDI-окон. Здесь требуется некоторое пояснение. Если вы когда-нибудь использовали раньше MDI-приложение вроде Microsoft Word, вы могли заметить, что у него есть подменю под названием "window", которое при активации отображает различные пункты меню, связанные с управлением дочерними окнами, а также список открытых дочерних окон. Этот список создается самими Windows: вам не нужно прилагать специальных усилий. Всего лишь передайте описатель подменю, к которому должен быть присоединен список, а Windows возьмет на себя все остальное. Обратите внимание, что подменю может быть любым: не обязательно тем, которое названо "window". Если вам не нужен список окон, просто передайте NULL в hWindowMenu. Получить дескриптор подменю можно с помощью GetSubMenu. idFirstChild ― ID первого дочернего MDI-окна. Windows увеличивает ID на 1 для каждого нового MDI-окна, которое создает приложение. Например, если вы передает 100 через это поле, первое MDI-окно будет иметь ID 100, второе - 101 и так далее. Это ID посылается фреймовому окну через WM_COMMAND, когда дочернее MDI-окно выбрано из списка окон. Обычно вы будете передавать эти "необрабатываемые" сообщения процедуре DefFrameProc. Я использую слово "необрабатываемые", потому что пункты меню списка окон не создаются вашим приложением, поэтому ваше приложение не знает их ID и не имеет обработчика для них. Поэтому для фреймового окна есть специальное правило: если у вас есть список окон, вы должны модифицировать ваш обработчик WM_COMMAND так: Код (ASM): wmCOMMAND: .if lParam==0 mov eax,wParam .if ax==IDM_CASCADE ..... .elseif ax==IDM_TILEVERT ..... .else invoke DefFrameProc, hwndFrame, hwndClient, uMsg,wParam, ret .endif Обычно вам следует игнорировать сообщения о необрабатываемых событиях, но в случае с MDI, если вы просто проигнорируете их, то когда пользователь кликнет на имени дочернего MDI-окна, это окно не станет активным. Вам следует передавать управление DefFrameProc, чтобы они были правильно обработаны. Я хочу предупредить вас относительно возможного значения idFirstChild: вам не следует использовать 0. Ваш список окон будет себя вести неправильно, то есть напротив пункта меню, обозначающего активное MDI-окно, не будет галочки. Лучше выберите какое-нибудь безопасное значение вроде 100 или выше. Заполнив структуру CLIENTCREATESTRUCT, вы можете создать клиентское окно, вызвав CreateWindowEx, указав предопределенный класс "MDICLIENT" и передав адрес структуры CLIENTCREATESTRUCT через lParam. Вы должны также указать дескриптор на окно фрейма в параметре hWndParent, чтобы Windows знала об отношениях родитель-ребенок между окном фрейма и клиентским окном. Вам следует использовать следующие стили окна: WS_CHILD, WS_VISIBLE и WS_CLIPCHILDREN. Если вы забудете указать стиль WS_VISIBLE, то не увидите дочерних MDI-окон, даже если они будут созданы успешно.Этапы создания клиентского окна следующие Получить дескриптор на подменю, к которому вы хотите присоединить список окон. Поместите значение дескриптора меню и значения, которое вы хотите использовать как ID первого дочернего MDI-окна в структуру CLIENCREATESTRUCT. Вызовите CreateWindosEx, передав имя класса "MDICLIENT" и адрес структуры CLIENTCREATESTRUCT, которую вы только что заполнили, через lParam.
Создание дочернего MDI-окнаТеперь у вас есть и окно фрейма, и клиентское окно. Теперь все готово для создания дочернего MDI-окна. Есть два пути сделать это. Вы можете послать сообщение WM_MDICREATE клиентскому окну, передав тому адрес структуры типа MDICREATESTRUCT через wParam. Это простейший и наиболее часто используемый способ создания дочерних MDI-окон. Код (ASM): .data? mdicreate MDICREATESTRUCT <> .... .code ..... ; заполняем поля структуры mdicreate ...... invoke SendMessage, hwndClient, WM_MDICREATE,addr mdicreate,0 Функция SendMessage возвратит дескриптор только что созданного дочернего MDI-окна, если все пройдет успешно. Вам не нужно сохранять дескриптор. Вы можете получить его каким-либо другим образом, если хотите. У структуры MDICREATESTRUCT следующее определение: Код (ASM): MDICREATESTRUCTA STRUC szClass LPCTSTR ? szTitle LPCTSTR ? hOwner HANDLE ? x ULONG ? y ULONG ? lx ULONG ? ly ULONG ? style DWORD ? Reserved DWORD ? lParam LPARAM ? MDICREATESTRUCTA ENDS szClass ― адрес класса окна, который вы хотите использовать в качестве шаблона для дочернего MDI-окна. szTitle ― адрес строки, которая должна появиться в заголовке дочернего окна. hOwner ― дескриптор приложения. x, y, lx, ly ― верхняя левая координата, ширина и высота дочернего окна. style ― стиль дочернего окна. Если вы создали клиентское окно со стилем MDIS_ALLCHILDSTYLES, то вы можете использовать все стили. lParam ― определяемое программистом 32-х битное значение. Используется для передачи значение между MDI-окнами. Если у вас нет подобной нужны, просто поставьте их в NULL. Вы можете вызвать CreateMDIWindow. Эта функция имеет следующий синтаксис: Код (C): HWND WINAPI CreateMDIWindow( _In_ LPCTSTR lpClassName, _In_ LPCTSTR lpWindowName, _In_ DWORD dwStyle, _In_ int X, _In_ int Y, _In_ int nWidth, _In_ int nHeight, _In_opt_ HWND hWndParent, _In_opt_ HINSTANCE hInstance, _In_ LPARAM lParam ); Если вы внимательно посмотрите на параметры, вы увидите, что они идентичны параметрам структуры MDICREATESTRUCT не считая hWndParent. Очевидно, что точно такое количество параметров вы передавали вместе с сообщением WM_MDICREATE. У структуры MDICREATESTRUCT нет поля hWndParent, потому что вы все равно должны передавать структуру клиентскому окну с помощью функции SendMessage. Сейчас вы можете спросить: какой метод я должен выбрать? Какая разница между этими двумя методами? Вот ответ: Метод WM_MDCREATE создает дочернее MDI-окно в том же треде, что и вызывающий код. Это означает, что если у приложения есть только одна ветвь, все дочерние MDI-окна будут выполняться в контексте основной ветви. Это не слишком большая проблема, если одно или более из дочерних MDI-окон не выполняет какие-либо продолжительные операции, что может стать проблемой. Подумайте об этом, иначе в какой-то момент все ваше приложение внезапно зависнет, пока операция не будет выполнена. Эта проблема как раз то, что призвана решить функция CreateMDIWindow. Она создает отдельный тред для каждого из дочерних MDI-окон, поэтому если одно из них занято, оно не приводит к зависанию всего приложения. Необходимо сказать несколько слов относительно оконной процедуры дочернего MDI-окна. Как и в случае с окном фрейма, вы не должны вызывать DefWindowProc, чтобы обработать необрабатываемые вашим приложением сообщением. Вместо этого вы должны использовать DefMDIChildProc. У этой функции точно такие же параметры, как и у DefWindowProc. Кроме WM_MDICREATE, есть еще несколько сообщений, относящихся к MDI-окнам. Их описание: WM_MDIACTIVATE ― это сообщение может быть послано приложением клиентскому окну, чтобы последнее активировало выбранное дочернее MDI-окно. Когда клиентское окно получает сообщение, оно активирует выбранное дочернее MDI-окно, а также посылает это же сообщение окну, которое было активированы или дезактивировано. Таким образом, данное сообщение имеет двойное назначение: с его помощью приложение может активировать выбранное дочернее окно, а также оно может быть использовано дочерним MDI-приложением для определения того, активировано оно или нет. Например, если каждое дочернее MDI-окно имеет различно меню, оно может использовать эту возможность для изменения меню окна фрейма при активации/дезактивации. WM_MDICASCADE, WM_MDITILE, WM_MDICONARRANGE ― эти сообщения отвечаю за расположение дочерних MDI-окон. Например, если вы хотите, чтобы дочерние MDI-окна расположились каскадом, пошлите сообщение WM_MDICASCADE клиентскому окну. WM_MDIDESTROY ― пошлите это сообщение клиентскому окну, если хотите уничтожить дочернее MDI_окно. Вам следует использовать это сообщение вместо DestroyWindow, потому что если дочернее MDI-приложение максимизировано, это сообщение восстановить заголовок окна фрейма, что не будет сделано в случае применения функции DestroyWindow. WM_MDIGETACTIVE ― используйте это сообщение, чтобы получить хэндл активного в настоящий момент дочернего MDI-окна. WM_MDIMAXIMIZE, WM_MDIRESTORE ― используйте WM_MDIMAXIZE для разворачивания дочернего MDI-окна и WM_MDIRESTORE для его восстановления. Всегда используйте эти сообщения для данных операций. Если вы используете ShowWindow с SW_MAXIMIZE, дочернее MDI-окно будет развернуто, но у вас появятся проблемы, когда вы захотите восстановить его до прежнего размера. Вы можете минимизировать окно с помощью ShowWindow без всяких проблем. WM_MDINEXT ― посылайте это сообщение клиентскому окну, чтобы активировать следующее или предыдущее дочернее MDI-окно, согласно значению wParam и lParam. WM_MDIREFRESHMENU ― это сообщение посылается клиентскому окну, чтобы обновить меню окна фрейма. Обратите внимание, что вы должны вызвать DrawMenuBar для обновления меню баp после отсылки данного сообщения. WM_MDISETMENU ― посылайте это сообщение клиентскому окну, что полностью заменить меню окна фрейма или только подменю окон. Вы должны использовать данное сообщение вместо SetMenu. После того, как вы отослали данное сообщение, вы должны вызвать DrawMenuBar. Обычно вы будете использовать это сообщение, когда у активного дочернего MDI-окна есть свое меню и вы хотите, чтобы оно заменяло меню окна фрейма, пока это дочернее MDI-окно активно. Небольшое обозрение создания MDI-приложения еще раз: Регистрируем классы окна, класса фрейма и дочернего MDI-окна. Создаем окно фрейма с помощью CreateWindowEx. Внутри цикла обработки сообщений вызываем TranslateMDISysAccel, чтобы обработать "горячие клавиши", относящиеся к MDI. Внутри оконной процедуры фреймового окна вызываем DefFramProc, чтобы обрабатывать все сообщения, необрабатываемые приложением. Создаем клиентское окно, вызвав CreateWindowEx, которой передаем имя предопределенного класса окна, "MDICLIENT", передавая адрес структуры CLIENTCREATESTRUCT через lParam. Обычно вы будете создавать клиентское окно внутри обработчика сообщения WM_CREATE окна фрейма. Вы можете создать дочернее MDI-окно, послав клиентскому окну сообщение WM_MDICREATE или вызвав функцию CreateMDIWindow. Внутри оконной процедуры дочернего MDI-ОКНА, передаем все необработанные сообщения DefMDIChildProc. Используем MDI-версии сообщений, если таковые существуют. Например, вместо WM_MDIDESTROY вместо DestroyWindow.
ПРАКТИКА ― СЕСТРА ШИЗОФРЕНИИasm-файл Код (ASM): include win64.inc IMAGE_BASE equ 400000h IDM_MENU equ 1000 IDM_FILE_EXIT equ 0 IDM_FILE_NEW equ 1 IDM_WINDOW_NEXT equ 2 IDM_WINDOW_PREVIOUS equ 3 MI_CLOSE equ 4 MI_TILEHOR equ 5 MI_TILEVER equ 6 MI_CASCADE equ 7 IDM_HELP_ABOUT equ 8 MI_CLOSE_ALL equ 9 .code WinMain proc local msg:MSG push rbp mov ebp,esp sub esp,sizeof MSG xor ebx,ebx mov edi,offset ClassName mov eax,10029h mov esi,IMAGE_BASE push rax ;hIconSm push rdi ;lpszClassName push IDM_MENU ;lpszMenuName push COLOR_APPWORKSPACE;hbrBackground push 10005h ;hCursor push rax ;hIcon push rsi ;hInstance push DLGWINDOWEXTRA;cbClsExtra & cbWndExtra lea rax,WndProc push rax ;lpfnWndProc push sizeof WNDCLASSEX;cbSize & style mov ecx,esp ;addr WNDCLASSEX mov edi,ecx sub esp,20h call RegisterClassEx mov eax,offset MdiChildProc mov [rdi+WNDCLASSEX.lpfnWndProc],rax mov [rdi+WNDCLASSEX.cbWndExtra],ebx mov [rdi+WNDCLASSEX.lpszMenuName],rbx mov eax,offset MdiChildClassName mov [rdi+WNDCLASSEX.lpszClassName],rax mov ecx,edi call RegisterClassEx push rbx push rsi ;rsi=400000h shl esi,9 ;rsi=CW_USEDEFAULT push rbx push rbx push 600 push 800 push rsi push rsi mov r9d,WS_OVERLAPPEDWINDOW or WS_VISIBLE or WS_CLIPCHILDREN mov edx,offset ClassName mov r8,rdx ;offset ClassName mov ecx,WS_EX_CLIENTEDGE sub esp,20h call CreateWindowEx mov hWnd,rax mov ecx,eax call GetMenu mov hMenu,rax mov edx,1 mov ecx,eax call GetSubMenu mov cc.hWindowMenu,rax mov edi,offset rect mov edx,edi mov rcx,hWnd call GetClientRect mov eax,offset cc push rax push rsi push rbx push hWnd mov eax,[rdi+RECT.bottom] push rax mov eax,[rdi+RECT.right] push rax push rbx push rbx mov r9d,WS_CHILD + WS_VISIBLE + WS_VSCROLL + WS_HSCROLL + WS_CLIPCHILDREN mov r8,rbx mov edx,offset MdiClientClassName mov ecx,WS_EX_CLIENTEDGE sub esp,20h call CreateWindowEx mov hClient,rax lea edi,msg @@: mov ecx,edi xor edx,edx mov r8,rbx mov r9,rbx call GetMessage mov edx,edi mov rcx,hClient call TranslateMDISysAccel test eax,eax jnz @b mov ecx,edi call DispatchMessage jmp @b WinMain endp WndProc proc hWin:HWND,uMsg:UINT,wParam:WPARAM,lParam:LPARAM push rbp mov ebp,esp sub esp,20h mov hWin,rcx mov wParam,r8 mov lParam,r9 xor ebx,ebx cmp edx,WM_DESTROY je wmDESTROY cmp edx,WM_COMMAND je wmCOMMAND cmp edx,WM_SIZE je wmSIZE leave jmp DefWindowProc wmDESTROY: xor ecx,ecx call ExitProcess wmCOMMAND:mov rax,r8;[wParam] cmp eax,MI_CLOSE_ALL;5 ja wmBYE jmp [menu_handlers+rax*8] New: mov esi,IMAGE_BASE push rbx;NULL push rsi;hInstance shl esi,9 push rbx push hClient push rsi push rsi push rsi push rsi mov r9d,MDIS_ALLCHILDSTYLES mov edx,offset MdiChildClassName mov r8,rdx mov ecx,WS_EX_MDICHILD + WS_EX_CLIENTEDGE sub esp,20h call CreateWindowEx jmp wmBYE Exit: mov rax,hWin jmp a1 Next: mov r9d,TRUE jmp a3 Prev: mov r9,rbx a3: mov r8,rbx mov edx,WM_MDINEXT a4: mov rcx,hClient jmp a2 Horiz: mov r9,rbx mov r8d,MDITILE_HORIZONTAL a5: mov edx,WM_MDITILE jmp a4 Vert: mov r9,rbx mov r8,rbx jmp a5 Cascade:mov r9,rbx mov r8d,MDITILE_SKIPDISABLED mov edx,WM_MDICASCADE jmp a4 Close: mov r9,rbx mov r8,rbx mov edx,WM_MDIGETACTIVE mov rcx,hClient call SendMessage a1: mov r9,rbx mov r8,rbx mov edx,WM_CLOSE mov rcx,rax a2: call SendMessage jmp wmBYE About: mov r9,rbx mov r8d,offset AboutMsg mov edx,offset ClassName mov rcx,hWin call ShellAbout jmp wmBYE CloseAll:mov r9,rbx mov r8,rbx mov edx,WM_MDIGETACTIVE mov rcx,hClient call SendMessage test eax,eax je wmBYE mov r9,rbx mov r8,rbx mov edx,WM_CLOSE mov ecx,eax call SendMessage jmp CloseAll wmSIZE: mov edi,offset rect mov edx,edi mov rcx,hWin call GetClientRect push TRUE mov eax,[rdi+RECT.bottom] push rax mov r9d,[rdi+RECT.right] mov r8,rbx xor edx,edx mov rcx,hClient sub esp,20h call MoveWindow wmBYE: leave retn menu_handlers dq Exit,New,Next,Prev,Close,Horiz,Vert,Cascade,About,CloseAll WndProc endp MdiChildProc proc push rbp mov ebp,esp sub esp,20h cmp edx,WM_MDIACTIVATE jz wmMDIACTIVATE leave jmp DefMDIChildProc wmMDIACTIVATE: mov rax,r9;[lParam2] cmp rax,rcx;[hChild] setne bl;MF_ENABLED=0 MF_GRAYED=1 mov r8,rbx mov edx,MI_CLOSE mov rcx,hMenu call EnableMenuItem mov r8,rbx mov edx,MI_TILEHOR mov rcx,hMenu call EnableMenuItem mov r8,rbx mov edx,MI_TILEVER mov rcx,hMenu call EnableMenuItem mov r8,rbx mov edx,MI_CASCADE mov rcx,hMenu call EnableMenuItem mov r8,rbx mov edx,IDM_WINDOW_NEXT mov rcx,hMenu call EnableMenuItem mov r8,rbx mov edx,IDM_WINDOW_PREVIOUS mov rcx,hMenu call EnableMenuItem mov r8,rbx mov edx,MI_CLOSE_ALL mov rcx,hMenu call EnableMenuItem xor ebx,ebx leave retn MdiChildProc endp ;data ClassName db 'Win64 Iczelion''s lesson #32: Multiple Document Interface (MDI)',0 MdiChildClassName db 'MDICHILD',0 MdiClientClassName db 'MDICLIENT',0 cc CLIENTCREATESTRUCT <0,2000> hWnd dq ? hClient dq ? hMenu dq ? rect RECT <> AboutMsg db 'Mikl__ 2015',0 end rc-файл Код (C): #define IDM_MENU 1000 #define IDM_FILE_EXIT 0 #define IDM_FILE_NEW 1 #define IDM_WINDOW_NEXT 2 #define IDM_WINDOW_PREVIOUS 3 #define MI_CLOSE 4 #define MI_TILEHOR 5 #define MI_TILEVER 6 #define MI_CASCADE 7 #define IDM_HELP_ABOUT 8 #define MI_CLOSE_ALL 9 IDM_MENU MENU DISCARDABLE BEGIN POPUP "&File" BEGIN MENUITEM "&New", IDM_FILE_NEW MENUITEM "&Close\tCtrl+F4",MI_CLOSE,GRAYED MENUITEM SEPARATOR MENUITEM "&Exit",IDM_FILE_EXIT END POPUP "&Window" BEGIN MENUITEM "Tile Horizontal",MI_TILEHOR,GRAYED MENUITEM "Tile Vertical",MI_TILEVER,GRAYED MENUITEM "Cascade",MI_CASCADE,GRAYED MENUITEM "&Next\tCtrl+F6",IDM_WINDOW_NEXT,GRAYED MENUITEM "&Previous\tShift+Ctrl+F6",IDM_WINDOW_PREVIOUS,GRAYED MENUITEM "&Close All",MI_CLOSE_ALL,GRAYED END POPUP "&Help" BEGIN MENUITEM "&About",IDM_HELP_ABOUT END END Разбор полётовПервое, что должна сделать программа ― это зарегистрировать классы фреймового и дочернего MDI-окна. После этого она вызывает функцию CreateWindowEx, чтобы создать окно фрейма. Внутри обработчика WM_CREATE окна фрейма создаем клиентское окно: Код (ASM): LOCAL ClientStruct:CLIENTCREATESTRUCT .if uMsg==WM_CREATE invoke GetMenu,hWnd mov hMainMenu,eax invoke GetSubMenu,hMainMenu,1 mov ClientStruct.hWindowMenu,rax mov ClientStruct.idFirstChild,100 invoke CreateWindowEx,NULL,ADDR MDIClientName,NULL,\ WS_CHILD or WS_VISIBLE or WS_CLIPCHILDREN,CW_USEDEFAU CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,hWnd,NULL,\ hInstance,addr ClientStruct mov hwndClient,rax Здесь мы вызываем GetMenu, чтобы получить дескриптор меню окна фрейма, который будем использовать в GetSubMenu. Обратите внимание, что мы передаем 1 функции GetSubMenu, потому что подменю, к которому мы будем присоединять список окон, является вторым подменю. Затем мы заполняем параметры структуры CLIENTCREATESTRUCT. Затем мы инициализируем структуру MDCLIENTSTRUCT. Обратите внимание, что мы не обязаны делать это здесь. Просто это удобнее осуществлять в обработчике WM_CREATE. Код (ASM): mov mdicreate.szClass,offset MDIChildClassName mov mdicreate.szTitle,offset MDIChildTitle push hInstance pop mdicreate.hOwner mov mdicreate.x,CW_USEDEFAULT mov mdicreate.y,CW_USEDEFAULT mov mdicreate.lx,CW_USEDEFAULT mov mdicreate.ly,CW_USEDEFAULT
После того, как окно фрейма создано (так же как клиентское окно), мы вызывает LoadMenu, чтобы загрузить меню дочернего окна из ресурса. Нам нужно получить дескриптор этого меню, чтобы мы могли заменить меню окна фрейма, когда дочернее MDI-окно становится активным. Не забудьте вызвать DestroyMenu, прежде чем приложение завершит работу. Обычно Windows сама освобождает память, занятую меню, но в данном случае этого не произойдет, так как меню дочернего окна не ассоциировано ни с каким окном, поэтому оно все еще будет занимать ценную память, хотя приложение уже прекратило свое выполнение. Код (ASM): invoke LoadMenu,hInstance, IDR_CHILDMENU mov hChildMenu,eax ........ invoke DestroyMenu, hChildMenu Внутри цикла обработки сообщений, мы вызываем TranslateMDISysAccel. Код (ASM): .while TRUE invoke GetMessage,ADDR msg,NULL,0,0 .break .if (!eax) invoke TranslateMDISysAccel,hwndClient,addr msg .if !eax invoke TranslateMessage, ADDR msg invoke DispatchMessage, ADDR msg .endif .endw Если TranslateMDISysAccel возвращает ненулевое значение, это означает, что сообщение уже было обработано Windows, поэтому вам не нужно делать что-либо с ним. Если был возвращен 0, сообщение не относится к MDI и поэтому должно обрабатываться как обычно. Код (ASM): WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM ..... .else invoke DefFrameProc,hWnd,hwndClient,uMsg,wParam,lParam ret .endif xor eax,eax ret WndProc endp Обратите внимание, что внутри оконной процедуры фреймового окна мы вызываем DefFrameProc для обработки сообщений, которые не представляют для нас интереса. Основной часть процедуры окна является обработчик сообщения WM_COMMAND. Когда пользователь выбирает в меню пункт "New", мы создает новое дочернее MDI-окно. Код (ASM): .elseif ax==IDM_NEW invoke SendMessage,hwndClient,WM_MDICREATE,0,addr mdicreate В нашем примере мы создаем дочернее MDI-окно, посылая WM_MDIREATE клиентскому окну, передавая адрес структуры MDICREATESTRUCT через lParam. Код (ASM): ChildProc proc hChild:DWORD,uMsg:DWORD,wParam:DWORD,lParam:DWORD .if uMsg==WM_MDIACTIVATE mov eax,lParam .if eax==hChild invoke GetSubMenu,hChildMenu,1 mov edx,eax invoke SendMessage,hwndClient,WM_MDISETMENU,hChildMen .else invoke GetSubMenu,hMainMenu,1 mov edx,eax invoke SendMessage,hwndClient,WM_MDISETMENU,hMainMenu .endif invoke DrawMenuBar,hwndFrame Когда создано дочернее MDI-окно, оно отслеживает сообщение WM_MDIACTIVATE, чтобы определить, является ли оно в данный момент активным. Оно делает это сравнивая значение lParam, которое содержит дескриптор активного дочернего окна со своим собственным дескриптором. Если они совпадают, значит оно является активным и следующим шагом будет замена меню окна фрейма на свое собственное. Так как изначально меню будет заменено, вам надо будет указать Windows снова в каком подменю должен появиться список окон. Поэтому мы должны снова вызвать функцию GetSubMenu, чтобы получить дескриптор подменю. Мы посылаем сообщение WM_MDISETMENU клиентскому окну, достигая, таким образом, желаемого результата. Параметр wParam сообщения WM_MDISETMENU содержит дескриптор меню, которое заменит оригинальное. lParam содержит дескриптор подменю, к которому будет присоединен список окон. Сразу после отсылки сообщения WM_MDISETMENU, мы вызываем DrawMenuBar, чтобы обновить меню, иначе произойдет большая путаница. Код (ASM): .else invoke DefMDIChildProc,hChild,uMsg,wParam,lParam ret .endif Внутри оконной процедуры дочернего MDI-окна вы должны передать все необработанные сообщения функции DefMDIChildProc. Код (ASM): .elseif ax==IDM_TILEHORZ invoke SendMessage,hwndClient,WM_MDITILE,MDITILE_HORIZONTAL,0 .elseif ax==IDM_TILEVERT invoke SendMessage,hwndClient,WM_MDITILE,MDITILE_VERTICAL,0 .elseif ax==IDM_CASCADE invoke SendMessage,hwndClient,WM_MDICASCADE,MDITILE_SKIPDISAB Когда пользователь выбирает один из пунктов меню в подменю окон, мы посылаем соответствующее сообщение клиентскому окну. Если пользователь выбирает один из методов расположения окон, мы посылаем WM_MDITILE или WM_CASCADE. Код (ASM): .elseif ax==IDM_CLOSE invoke SendMessage,hwndClient,WM_MDIGETACTIVE,0,0 invoke SendMessage,eax,WM_CLOSE,0,0 Если пользователь выбирает пункт меню "Close", мы должны получить дескриптор текущего активного MDI-окна. Код (ASM): wmCLOSE: invoke MessageBox,hChild,addr ClosePromptMessage,addr AppName .if eax==IDYES invoke SendMessage,hwndClient,WM_MDIDESTROY,hChild,0 .endif Когда процедура дочернего MDI-окна получает сообщение WM_CLOSE, его обработчик отображает окно, которое спрашивает пользователя, действительно ли он хочет закрыть окно. Если ответ - "Да", то мы посылаем клиентскому окну сообщение WM_MDIDESTROY, которое закрывает дочернее MDI-окно и восстанавливает заголовок окна фрейма.
Глава тридцать седьмая. Братец Кролик и элементы управления Мы узнаем, что такое элементы управления и как их использовать. Эта глава является не более, чем поверхностным введением в данную тему. Скачайте код примера здесь.ТЕОРИЯ ― МАТЬ СКЛЕРОЗАWindows 95 принесла несколько новых элементов пользовательского интерфейса, сделавших GUI более разнообразным. Некоторые из них широко использовались и в Windows 3.1, но программисты должны были программировать их самостоятельно. Теперь Microsoft включил их в Windows 9x и NT. Мы изучим их в этой главе. Вот список элементов управления: КатегорияЭлемент управленияОписаниеЭлементы управления, обычно используемые в главном окнеЭлементы управления главного окнаToolbar (панель инструментов)Состоит из кнопок быстрого доступаTooltip (окно подсказки)Обеспечивает пользователя быстрой подсказкой, отображая текст во всплывающем окнеStatus bar (строка состояния)Информационная строка, обычно размещаемая в нижней части окна приложения.Элементы управления для списков свойств и мастеров (wizards)Составные диалоговые элементы управленияProperty page (страница свойств)Немодальное диалоговое окно, используемое как одна страница в списке свойств или мастере.Property sheet (набор страниц свойств)Набор из множества окон страниц свойствЭлементы управления для построения приложений, похожих на Windows ExplorerЭлементы управления Windows ExplorerTree view (дерево просмотра)Отображает иерархически элементизированный список (левая панель окна программы Windows Explorer)List view (список просмотра)Отображает список элементов, идентифицируемых битовым образом и текстовыми данными (правая панель окна программы Windows Explorer)Другие элементы управленияAnimation (анимационное изображение)Проигрывает анимационную последовательность для индикации длительной операцииDrag list (список, поддерживающий операции типа drag/drop)Окно списка, поддерживающее простые операции drag/drop по отношению к себе и другим окнам типа Drag list. (Не drag/drop OLE-контейнер)Header (заголовок списка просмотра)Отображает горизонтальные заголовки для столбцов (используется совместно со списком просмотра)Hot-Key (горячая клавиша)Отображает результат операции определения клавиш активизации (горячих клавиш)Image list (список изображений)Элемент управления для хранения набора растровых изображений (битовых образов, курсоров, значков), не являющийся окномProgress bar (индикатор процесса)Отображает динамику длительной операции как процент от выполненной задачиRich edit (усовершенствованный редактор)Редактор, поддерживающий множество шрифтов и базовые возможности OLE-контейнераTabs control (набор закладок для выбора)Отображает список закладок для выбора. Tabs используются в окне набора страниц свойств для выбора страницы свойств. Панель задач (task bar) Windows — элемент управления Tabs control, использующий кнопки вместо закладокTrackbar (окно с движком для выбора значения из диапазона)Тип полосы прокрутки для выбора значения в заданном диапазонеUp-Down (полоса прокрутки, связанная с окном редактирования для увеличения или уменьшения на 1 целочисленного значения)Тип полосы прокрутки, состоящий из двух стрелок (но собственно без полосы) для увеличения или уменьшения на 1 величины, находящейся в связанном поле редактированияТак как новых элементов управления довольно много, их загрузка в память и регистрация была бы бессмысленной тратой ресурсов. Все эти элементы управления, за исключением rich edit'а, находятся в comctl32.dll, чтобы приложения могли загружать их, когда они им нужны. Rich edit находится в своей собственной dll, richedXX.dll, так как он слишком сложен и поэтому больше, чем остальные. Вы можете вызвать comctl32.dll, поместив вызов функции IntiCommonControls в вашу программу. InitCommonControls ― это функция в comctl32.dll, поэтому ее вызов в любом месте вашего кода заставит PE-загрузчик загрузить comctl32.dll, когда ваша программ запустится. Вам не нужно выполнять эту функцию, просто поместите ее где-нибудь. Эта функция ничего не делает! Ее единственной инструкцией является "ret". Ее главная цель ― это создание ссылки на comctl32.dll в секции импорта, чтобы PE-загрузчик загружал ее всегда, когда будет загружаться программа. Главным следствием будет являться то, что стартовая функция DLL зарегистрирует все классы элементов управления при загрузке dll. Элементы управления создаются на основе этих классов, как и другие дочерние элементы окон, например, окна ввода, listbox и так далее. С rich edit'ом дел обстоит совершенно по другому. Если вы хотите использовать его, вы должны вызвать LoadLibrary, чтобы загрузить его и FreeLibrary, чтобы выгрузить. Теперь давайте научимся создавать элементы управления. Вы можете использовать редактор ресурсов, чтобы внедрить их в диалоговое окно, или создать их самостоятельно. Почти все элементы управления создаются с помощью вызова CreateWindowEx или CreateWindow, путем передачи имени класса элемента управления. У некоторых элементов управления есть специальные функции для создания, хотя, на самом деле, они являются функциями-обертками вокруг CreateWindowEx, чтобы сделать создание элемента управления легче. Такие функции перечислены ниже: CreateToolbarEx CreateStatusWindow CreatePropertySheetPage PropertySheet ImageList_Create Чтобы создавать элементы управления, вы должны знать их имена. Они перечислены ниже: Категория/Элемент управленияКласс элемента управленияФункция созданияЭлементы управления главного окнаПанель инструментовToolbarWindow32CreateToolbarExОкно подсказкиtooltips_class32―Строка состоянияmsctls_statusbar32CreateStatusWindowАнимационное изображениеSysAnimate32―Индикатор процессаmsctls_progress32―Составные диалоговые элементы управленияСтраница свойств―CreatePropertySheetPageНабор страниц свойств―PropertySheetЭлементы управления Windows ExplorerДерево просмотраSysTreeView32―Список просмотраSysListView32―Список изображений―ImageList_CreateДругие элементы управленияСписок, поддерживающий операции типа drag/drop"listbox"MakeDragListЗаголовок списка просмотраSysHeader32―Горячая клавишаmsctls_hotkey32―Усовершенствованный редакторRICHEDIT―Набор закладок для выбораSysTabControl32―Окно с движком для выбора значения из диапазонаTRACKBAR_CLASS―Полоса прокрутки, связанная с окном редактирования для изменения значенияmsctls_updown32CreateUpDownControlProperty sheet'ы и Property Page'ы и список изображений имеют собственные функции создания. Drag list control ― это усовершенствованный listbox, поэтому у него нет своего собственного класса. Эти элементы управления могут использовать общие стили окна, такие как WS_CHILD и тому подобное. У них также есть специальные стили, такие как TVS_XXX для окон просмотра деревьев, LVS_XXX для list view control'а и так далее. Теперь, когда вы знаете, как создать элементы управления, можно перейти к взаимодействию элементов управления и окон, на которых эти элементы расположены. В отличие от дочерних элементов управления, элементы управления не взаимодействую с родительским окном через WM_COMMAND. Вместо этого они используют сообщение WM_NOTIFY, посылаемое родительскому окну, когда происходит какое-то событие. "Родительское окно" может контролировать "детей", посылая им определенные сообщения. Давайте посмотрим, как создать progress bar и status bar.
ПРАКТИКА ― СЕСТРА ШИЗОФРЕНИИ Код (ASM): iinclude win64a.inc IMAGE_BASE equ 400000h IDC_PROGRESS equ 1; control IDs IDC_STATUS equ 2 IDC_TIMER equ 3 btn1 equ 4 .code WinMain proc local msg:MSG invoke InitCommonControls xor ebx,ebx mov eax,10027h mov edi,offset ClassName mov esi,IMAGE_BASE push rax ;hIconSm push rdi ;lpszClassName push rbx ;lpszMenuName push COLOR_APPWORKSPACE;hbrBackground push 10005h ;hCursor push rax ;hIcon push rsi ;hInstance push rbx ;cbClsExtra & cbWndExtra pushaddr 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 400 push rsi push rsi sub esp,20h invoke CreateWindowEx,0,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 oldBrush:QWORD local ps:PAINTSTRUCT mov hWnd,rcx mov wParam,r8 mov lParam,r9 cmp edx,WM_DESTROY je wmDESTROY cmp edx,WM_PAINT je wmPAINT cmp edx,WM_CREATE je wmCREATE cmp edx,WM_COMMAND je wmCOMMAND leave jmp DefWindowProc wmPAINT:lea edx,ps invoke BeginPaint invoke SelectObject,ps.hdc,blueBrush mov oldBrush,rax finit fld angle fsincos fmul const_80 fistp tempX fmul const_80 fchs fist tempY mov eax,tempY add rax,130 push rax ;nYRadial2 mov eax,tempX add rax,190 push rax ;nXRadial2 push 80 ;nYRadial1 push 190 ;nXRadial1 push 180 ;nBottomRect sub esp,20h invoke Pie,ps.hdc,110,80,270 invoke SelectObject,ps.hdc,oldBrush lea edx,ps invoke EndPaint,hWnd jmp wmBYE wmDESTROY:mov rdx,TimerID or edx,edx jne @f invoke KillTimer;,hWnd @@: invoke ExitProcess,0 wmCREATE:mov edi,3 @@: push rbx ;lpParam push IMAGE_BASE ;hInstance push IDC_PROGRESS push hWnd ;hWndParent push nHeight[rdi*8] push nWidth[rdi*8] push y[rdi*8] push x[rdi*8] mov r9d,dwStyle[rdi*4] ;mov edx,offset progressClass;lpClassName mov ecx,dwExStyle[rdi*4] sub esp,20h invoke CreateWindowEx,,&progressClass,0;lpWindowName mov pb1H[rdi*8],rax invoke SendMessage,eax,PBM_SETSTEP,2,0 dec edi jns @b push rbx;NULL push IMAGE_BASE push btn1 push hWnd push 30 push 100 push 90 push 10 mov r8d,offset btn1Txt mov edx,offset ctlClsNameBtn sub esp,20h invoke CreateWindowEx,NULL,,,WS_CHILD + WS_VISIBLE + BS_PUSHBUTTON mov btn1H,rax invoke CreateStatusWindow,WS_CHILD + WS_VISIBLE,NULL,hWnd,IDC_STATUS mov hwndStatus,rax invoke CreateSolidBrush,0FF9933h; light blue color mov blueBrush,rax jmp wmBYE wmCOMMAND:cmp wParam,BN_CLICKED shl 16 or 4 jne wmBYE wmCOMMAND_btn1:;create a timer mov r9d,offset TimerProc invoke SetTimer,hWnd,IDC_TIMER,100 mov TimerID,rax invoke EnableWindow,btn1H,FALSE wmBYE: leave retn WndProc endp TimerProc proc hWnd:HWND local buffer[16]:BYTE mov hWnd,rcx mov edi,3 @@: invoke SendMessage,pb1H[rdi*8],PBM_STEPIT,0,0; step up the progress in invoke SendMessage,pb1H[rdi*8],PBM_GETPOS,0,0 dec edi jns @b lea ecx,buffer mov edx,offset format invoke wsprintf,,,rax lea r9,buffer invoke SendMessage,hwndStatus,SB_SETTEXT,0 invoke InvalidateRect,hWnd,0,0 finit fld delta fadd angle fst angle sub CurrentStep,2 jne wmBYE invoke KillTimer,hWnd,TimerID mov TimerID,0 mov r9d,offset Message invoke SendMessage,hwndStatus,SB_SETTEXT,0 wmBYE: leave retn TimerProc endp ;data ClassName db 'Win64 Iczelion''s lesson #18: Common Controls',0 progressClass db 'msctls_progress32',0 TimerID dq ? pb1H dq ?;hwndprogress pb2H dq ? pb3H dq ? pb4H dq ? sb1H dq ? btn1H dq ? hwndStatus dq ? ctlClsNameBtn db 'BUTTON',0 btn1Txt db 'Click To Start',0 CurrentStep dq 100 ;increase step value blueBrush dq ? const_80 dd 80.0 tempX dd ? tempY dd ? angle dd 1.57952297305486826711594014548 ;90.5*pi/180 delta dd 0.12548917321839229658081336625 ;7.19*pi/180 dwStyle dd WS_CHILD + WS_VISIBLE,WS_CHILD + WS_VISIBLE + PBS_SMOOTH,\ WS_CHILD + WS_VISIBLE + PBS_VERTICAL,WS_CHILD + WS_VISIBLE + PBS_VERTICAL + PBS_SMOOTH x dq 10,10,305,350 y dq 10,40,80,80 nHeight dq 22,22,100,100 nWidth dq 367,367,25,25 dwExStyle dd WS_EX_DLGMODALFRAME,WS_EX_DLGMODALFRAME + WS_EX_STATICEDGE,0,0 format db ' Process : %i %%',0 Message db ' 100% Completed',0 end Разбор полетов Код (ASM): invoke InitCommonControls эта функция необходима только для создания ссылки на comctl32.dll в секции импорта. Как вы можете видеть, элементы управления работают, даже если функцию InitCommonControls поместить после вызова ExitProcess. Код (ASM): wmCREATE:mov edi,3 @@: push rbx ;lpParam push IMAGE_BASE ;hInstance push IDC_PROGRESS push hWnd ;hWndParent push nHeight[rdi*8] push nWidth[rdi*8] push y[rdi*8] push x[rdi*8] mov r9d,dwStyle[rdi*4] mov edx,offset progressClass;lpClassName mov ecx,dwExStyle[rdi*4] sub esp,20h invoke CreateWindowEx,,,0;lpWindowName mov pb1H[rdi*8],rax invoke SendMessage,eax,PBM_SETSTEP,2,0 dec edi jns @b push rbx;NULL push IMAGE_BASE push btn1 push hWnd push 30 push 100 push 90 push 10 mov r8d,offset btn1Txt mov edx,offset ctlClsNameBtn sub esp,20h invoke CreateWindowEx,NULL,,,WS_CHILD + WS_VISIBLE + BS_PUSHBUTTON mov btn1H,rax invoke CreateStatusWindow,WS_CHILD + WS_VISIBLE,NULL,hWnd,IDC_STATUS mov hwndStatus,rax invoke CreateSolidBrush,0FF9933h; light blue color mov blueBrush,rax Здесь мы создаем элементы управления. Заметьте, что вызов CreateWindowEx содержит hWnd в качестве дескриптора родительского окна. Он также задает ID элемента управления, для идентификации последнего. Тем не менее, так как у нас есть дескриптор окна элемента управления, этот ID не используется. Все дочерние окна должны иметь стиль WS_CHILD. Код (ASM): mov eax,1000 mov CurrentStep,eax shl eax,16 invoke SendMessage,hwndprogress,pBM_SETRANGE,0,eax invoke SendMessage,hwndprogress,pBM_SETSTEp,10,0 После того, как создан progress bar, мы можем установить его диапазон. Диапазон по умолчанию равен от 0 до 100. Если это вас не устраивает, вы можете указать ваш собственный диапазон с помощью сообщения PBM_SETRANGE. lParam этого сообщения содержит диапазон, максимальное значение в верхнем слове и минимальное в нижнем. Вы также можете указать шаг, используя сообщение PBM_SETSTEP. Этот пример устанавливает его в 10, что означает то, что когда вы посылаете сообщение PBM_STEPIT прогресс бару, индикатор прогресса будет повышаться на 10. Вы также можете установить положение индикатора, послав сообщение PBM_SETPOS. Это сообщение дает вам полный контроль над progress bar'ом. Код (ASM): invoke CreateStatusWindow,WS_CHILD+WS_VISIBLE,NULL,hWnd,IDC_STATUS mov hwndStatus,eax invoke SetTimer,hWnd,IDC_TIMER,100,NULL ; create a timer mov TimerID,eax Затем мы создаем status bar, вызывая CreateStatusWindow. После того, как status window создан, мы создаем таймер. В этом примере мы будем обновлять progress bar каждые 100 ms, поэтому нам нужно создать таймеp. Код (C): UINT_PTR WINAPI SetTimer( _In_opt_ HWND hWnd,//дескриптор родительского окна _In_ UINT_PTR nIDEvent,/* не равный нулю идентификатор таймера. Вы можете создать свой собственный идентификатор */ _In_ UINT uElapse,/* временной интервал в миллисекундах, который должен пройти, прежде чем таймер вызовет процедуру таймер или пошлет сообщение WM_TIMER */ _In_opt_ TIMERPROC lpTimerFunc /* адрес функции таймера, которая будет вызываться при истечении временного интервала. Если параметр равен нулю, таймер вместо этого будет посылать родительскому окну сообщение WM_TIMER */ ); Если вызов прошел успешно, функция возвратит TimerID. В противном случае, будет возвращен ноль. Вот почему не желательно, чтобы идентификатор таймера был равен нулю. Когда истекает указанный временной интервал, таймер посылает сообщение WM_TIMER. Вы можете поместить здесь свой код, который будет выполнен. В данном пример, мы обновляем progress bar, а затем проверяем, было ли достигнуто максимальное значение. Если это так, мы убиваем таймеp, после чего устанавливаем текст статус-окна с помощью сообщения SB_SETTEXT. Вызывается MessageBox, и когда пользователь нажмет на OK, мы очищаем текст в status bar'е и progress bar'е.
Глава вторая. Прототип нашей IDE В качестве прототипа IDE Братец Кролик взял пакет MASM32 Стива Хатчессона (BP, BС, CBuilder, Delphi, Visual Studio организованы примерно так же). сам пакет MASM32 распространяется в виде одного исполняемого модуля, в котором в упакованном виде содержатся все его компоненты. При инсталяции выбирается один из локальных дисков, в корне которого создается каталог masm32. IDE состоит из следующих блоков: Инструментальный блок (masm32\bin). Здесь располагаются компилятор ml.exe, компоновщик link.exe, компилятор ресурсов rc.exe, редактор для написания ассемблерных программ qeditor.exe и так далее. В общем всё что Вам понадобится для превращения исходных файлов в программу. Справочный блок (masm32\help). Содержит chm- и hlp-файлы и другую документацию чтобы описать возможности пакета, собственные директивы, макросредства и так далее, и тому подобное... Блок примеров (masm32\examples, masm32\tutorial и так далее). Огромное количество примеров программирования на диалекте masm под 32-разрядную Windows. Блок описаний (masm32\include). Cодержит inc-файлы с прототипами WinAPI функций, а также константы и структуры, используемые для написания программ под Windows. Блок библиотек (masm32\lib). Cодержит lib-файлы, которые являются шлюзами для вызова WinAPI-функций, код которых расположен в системных динамических библиотеках, которые размещены в каталоге Windows\system32 Библиотека макросов (masm32\macros). Содержит всевозможные макроопределения, различные дополнения и заготовки, чтобы облегчить рутинный труд программиста. Создание inc- и lib-файлов из системных dll Для самой первой программы мы использовали файлы temphls.inc, win64.inc, user32.inc, kernel32.inc, user32.lib, kernel32lib взятые с сайта dsmhelp.narod.ru. Но проект заброшен автором, поэтому inc- и lib-файлы мы будем создавать самостоятельно Назначение inc и lib файлов inc-файлы ― это текстовые файлы содержащие описания структур данных и констант Windows, а также макроопределения. inc-файлы формируются программистом по мере расширения используемых им средств операционной системы. Аналогичны заголовочным h/hpp-файлам используемых при программировании на C/C++, иногда можно сформировать inc-файлы из h-файлов используя утилиту h2inc.exe (ее можно найти в старых пакетах MASM). Назначение lib-файлов ― предоставление программе link.exe информации о внешних ссылках на WinAPI-функции внутри системных dll-файлов. lib-файл ― это архив, который хранит набор сопоставлений "внешний символ" ― ссылка на объектный (COFF или PE) файл. Этот "символ" на стадии линковки либо добавляется в исполняемый образ (в случае с COFF ― из прекомпилированного объектного файла), либо ― прописывается в таблице импорта (в случае с PE). То есть некоторый объем внешних ссылок транслируется в ваш exe или dll. link.exe обрабатывает стандартные библиотеки COFF и библиотеки импорта COFF, которые имеют расширение LIB. Стандартные библиотеки содержат объекты и создаются с помощью утилиты lib.exe. Библиотеки импорта содержат сведения об экспорте в другие программы и создаются либо компилятором link.exe при построении программы, содержащей экспорт, либо утилитой lib.exe. Для получения содержимого системного dll-файла использую следующий bat-файл Код (DOS): @echo off ::стираем с экрана cls ::устанавливаем путь к каталогу masm64 set masm64_path=\masm64\ ::имя "препарируемой dll", начнем с user32 set FileName=user32 if exist %FileName%.inc del %FileName%.inc if exist %FileName%.def del %FileName%.def ::обрабатываем user32.dll и получаем user32.txt файл %masm64_path%bin\dumpbin.exe /EXPORTS %windir%\System32\%FileName%.dll /OUT:%FileName%.txt @echo EXPORTS >> %FileName%.def for /f "skip=16 tokens=1-4" %%a in (%FileName%.txt) do ( if "%%a"=="Summary" goto :exit if "%%d"=="" ( @echo extern __imp_%FileName%_ordinal%%a:qword >> %FileName%.inc @echo %FileName%_ordinal%%a TEXTEQU ^<__imp_%FileName%_ordinal%%a^> >> %FileName%.inc @echo %FileName%_ordinal%%a=ordinal%%a @%%a NONAME >> %FileName%.def ) else ( if not "%%d"=="(forwarded" ( @echo extern __imp_%%d:qword >> %FileName%.inc @echo %%d TEXTEQU ^<__imp_%%d^> >> %FileName%.inc @echo %%d=__imp_%%d >> %FileName%.def ))) :exit %masm64_path%bin\link -lib /DEF:%FileName%.def /OUT:%FileName%.lib /MACHINE:X64 Разбор bat-файла предварительная настройка bat-файла Код (DOS): ::стираем с экрана cls ::устанавливаем путь к каталогу masm64 set masm64_path=\masm64\ ::имя "препарируемой dll", начнем с user32 set FileName=user32 ::обрабатываем user32.dll и получаем user32.txt файл %masm64_path%bin\dumpbin.exe /EXPORTS %windir%\System32\%FileName%.dll /OUT:%FileName%.txt содержимое user32.txt Код (DOS): Dump of file C:\Windows\System32\user32.dll File Type: DLL Section contains the following exports for USER32.dll 00000000 characteristics 4CE799CD time date stamp Sat Nov 20 17:50:05 2010 0.00 version 1500 ordinal base 1003 number of functions 830 number of names ordinal hint RVA name 1502 0 000083C0 ActivateKeyboardLayout 1503 1 0002AD40 AddClipboardFormatListener 1504 2 000235B8 AdjustWindowRect 1505 3 00017CE4 AdjustWindowRectEx .... 2341 33C 0007B430 wvsprintfA 2342 33D 00020BFC wvsprintfW 1500 0002B260 [NONAME] 1501 0002AE80 [NONAME] .... Summary 2000 .data A000 .pdata 10000 .rdata 1000 .reloc 5B000 .rsrc 81000 .text после просмотра user32.txt видно, что из user32.dll импортируется 846 функции, из них 826 функций импортируются по именам, 16 ― по ординалам, а функции DefDlgProcA, DefDlgProcW, DefWindowProcA, DefWindowProcW портируются в user32.dll из системной библиотеки NTDLL.dll Код (DOS): Dump of file C:\Windows\System32\user32.dll File Type: DLL Section contains the following exports for USER32.dll 00000000 characteristics 4CE799CD time date stamp Sat Nov 20 17:50:05 2010 0.00 version 1500 ordinal base 1003 number of functions 830 number of names ordinal hint RVA name 1502 0 000083C0 ActivateKeyboardLayout <-- полезная информация начинается здесь если перед началом обработки в каталоге уже существуют файлы user32.inc, user32.def, user32.lib оставшиеся от предыдущей обработки dll-файлов ― удаляем их. Код (DOS): if exist %FileName%.inc del %FileName%.inc if exist %FileName%.def del %FileName%.def создаем файл user32.def, который должен начинаться со строки "EXPORTS" Код (DOS): @echo EXPORTS >> %FileName%.def полезная информация начинается в user32.txt с 16 строки, поэтому skip=16 означает ― пропускаем первые 16 строк в user32.txt при построчном разборе файла user32.txt используем четыре первых слова в строке, которым присвоим имена %%a, %%b, %%c, %%d Код (DOS): for /f "skip=16 tokens=1-4" %%a in (%FileName%.txt) do если первый параметр равен "Summary" ― значит обработаны все функции, входящие в dll, мы прекращаем обработку, выходим из файла user32.txt и переходим на метку :exit Код (DOS): if "%%a"=="Summary" goto :exit если четвертый параметр в файле user32.txt пустой ― перед нами импорт по ординалам Код (DOS): %%a %%b %%c %%d 1500 0002B260 [NONAME] сохраняем первое слово (ординал WinAPI-функции) в строке user32.txt в переменной %%a, обрамляем ее и помещаем в две новых строки в файл user32.inc Код (DOS): extern __imp_user32_ordinal1500:qword user32_ordinal1500 TEXTEQU <__imp_user32_ordinal1500> и user32.def Код (DOS): user32_ordinal1500=ordinal1500 @1500 NONAME если четвертый параметр непустой ― перед нами импорт по именам функций в очередной строке файла user32.txt Код (DOS): %%a %%b %%c %%d 1502 0 000083C0 ActivateKeyboardLayout четвертое слово в строке (имя WinAPI-функции), сохраняем в переменной %%d, создаем две новых строки в файле user32.inc, предваряем %%d "extern __imp_" строку завершаем ":qword", добавляем "TEXTEQU", "__imp_", экранируем управляющие символы "<" и ">" (^<__imp_%%d^>) чтобы bat-файл воспринимал их, как обычные символы. Код (DOS): extern __imp_ActivateKeyboardLayout:qword ActivateKeyboardLayout TEXTEQU <__imp_ActivateKeyboardLayout> и user32.def Код (DOS): ActivateKeyboardLayout=__imp_ActivateKeyboardLayout если четвертый параметр равен "(forwarded", значит WinAPI-функция берется из другой dll и мы пропускаем такую строку. Код (DOS): %%a %%b %%c %%d 1657 94 DefDlgProcA (forwarded to NTDLL.NtdllDialogWndProc_A) из содержимого файлов user32.def и user32.inc создаем файл user32.lib Код (DOS): :exit %masm64_path%bin\link -lib /DEF:%FileName%.def /OUT:%FileName%.lib /MACHINE:X64 того же результата можно добиться строкой Код (DOS): %masm64_path%bin\lib /DEF:%FileName%.def /OUT:%FileName%.lib /MACHINE:X64 переносим файл user32.inc в каталог masm64\include, а файл user32.lib в каталог masm64\lib удаляем программный мусор Код (DOS): if exist %FileName%.def del %FileName%.def if exist %FileName%.exp del %FileName%.exp if exist %FileName%.txt del %FileName%.txt С удивлением Братец Кролик обнаружил, что в kernel32.dll нет ExitProcess, а в user32.dll нет DefWindowProcA, обе функции портируются из ntdll.dll (RtlExitUserProcess и NtdllDefWindowProc_A соответственно) Аналогично user32.dll препарируем kernel32.dll, ntdll.dll, gdi32.dll, comctrl32.dll и далее, по мере необходимости © Mikl___ 2019
Глава четвертая. Как Братец Кролик создал универсальный bat-файл Превращение текстового asm-файла в двоичный com/exe/sys/dll-файл происходит под управлением с командной строки либо (если asm-файл единственный) в один этап (компилятору передается ключ /link) либо (если asm-файлов несколько) в два этапа с раздельной компиляцией каждого asm-файла (компилятору передается ключ /c) и (если компиляция проходит без ошибок) запускается линковка всех obj-файлов в один файл Как это сделано в FASM С целью управления форматом сгенерированного кода FASM использует директиву "format". Директива "format", за которой следует идентификатор, позволяет выбрать формат выходного файла. Директива помещается в начало asm-файла. Формат выходного файла "по-умолчанию" ― простой двоичный файл (flat binary file), его также можно выбрать с помощью директивы "format binary". За этой директивой может следовать ключевое слово "as" и строка в кавычках, указывающая расширение для выходного файла. Если имя выходного файла не было указано в командной строке, ассемблер будет использовать это расширение при создании выходного файла с именем asm-файла. Директивы "use16" и "use32" заставляют ассемблер генерировать 16-разрядный или 32-разрядный код, опуская настройку по умолчанию для выбранного выходного формата. "use64" позволяет генерировать код для long mode режима x64. Ниже описаны различные выходные форматы с указаниями, специфичными для этих форматов: Для выбора формата MZ-COM для выходного файла используйте Код (ASM): use16 org 100h Для выбора формата MZ-EXE для выходного файла используйте диррективу "format MZ". По умолчанию код собирается в 16-разрядном формате. Для выбора формата Portable Executable для выходного файла используйте диррективу "format PE", за ней могут следовать дополнительные настройки формата: Код (ASM): format PE GUI 4.0 DLL at 7000000h on 'stub.exe' сперва ключевое слово для настройки целевой подсистемы, ими могут быть: для Windows приложений: console GUI для Windows kernel mode драйверов: "native" для UEFI: "EFI" "EFIboot" "EFIruntime" Windows SubsystemValue (hex)DescriptionПримерIMAGE_SUBSYSTEM_NATIVE1Device drivers and native Windows processesformat PE nativeIMAGE_SUBSYSTEM_WINDOWS_GUI2The Windows graphical user interface subsystemformat PE GUIIMAGE_SUBSYSTEM_WINDOWS_CUI3The Windows character subsystemformat PE consoleIMAGE_SUBSYSTEM_EFI_APPLICATION0xAAn Extensible Firmware Interface applicationformat EFIIMAGE_SUBSYSTEM_EFI_BOOT_ SERVICE_DRIVER0xBAn EFI driver with boot servicesformat EFIbootIMAGE_SUBSYSTEM_EFI_RUNTIME_ DRIVER0xCAn EFI driver with run-time servicesformat EFIruntime далее могут следовать минимальная версия системы, под которую написано приложение и ее подверсия (версия и подверсией будут разделены точкой) ключевое слово "DLL" создает файл вывода как динамически подключаемую библиотеку (Dinamic Link Library) ключевое слово "WDM" создает файл вывода как Windows Driver Model (WDM-драйвер) ключевое слово "large" отмечает, что приложение может работать с адресами, размер которых свыше 2 гигабайт ключевое слово "NX" сигнализирует, что у исполняемого файла есть ограничению на исполнение кода, находящегося в неисполняемых разделах (сегменты данных, стека, ресурсах и т.п.), запрет исполнения, добавленный в таблицы страниц для предотвращения выполнения данных как кода. Используется для предотвращения уязвимости типа «переполнение буфера», позволяющей выполнять вредоносный код. Далее может следовать оператор "at" и числовое выражение, указывающее адрес базового образа PE файла Далее опционально может следовать оператор "on" со следующей за ним строкой в кавычках, содержащей имя файла, являющимся MZ-stub для PE программы (если указанный файл не в формате MZ, то он трактуется как простой двоичный исполняемый файл и конвертируется в формат MZ). По умолчанию код для этого формата 32-разрядный Для создания PE-файла для архитектуры x64 используйте ключевое слово "PE64" вместо директивы PE в декларации формата, в таком случае, по-умолчанию будет сгенерирован код для long mode. Для выбора формата Common Object File Format используйте: директиву "format COFF или "format MS COFF", зависит от того, хотите ли вы создать классический (DJGPP) или Microsoft-ский вариант файла COFF. Настройка кода по умолчанию для этого формата — 32-разрядная. Для создания файла в формате Microsoft COFF для архитектуры x64 используйте "format MS64 COFF" в таком случае по умолчанию будет сгенерирован код для long mode. Для выбора формата ELF используйте директиву "format ELF". По умолчанию в этом формате будет сгенерирован 32-разрядный код. Для создания ELF файла для архитектуры x64 используйте директиву "format ELF64", в таком случае по умолчанию будет сгенерирован код для long mode. Код (DOS): :: стираем с экрана cls :: запоминаем путь к нашей IDE set masm64_path=\masm64\ :: получаем имя обрабатываемого asm-файла set filename=%~n1 :: вызываем процедуру, которая по первому слову, составит метку, на которую передаст управление внутри bat-файла call :read_settings %filename% @echo %kind_of_file% goto %kind_of_file% :: При сборке приложения используется ключ /SUBSYSTEM:WINDOWS Создается приложение основанное на графическом интерфейсе (Grafical User Interface) Приложение не требует консоли, создает собственное окно, имеют меню, взаимодействуют с пользователем через диалоговые окна. :GUI if exist %filename%.exe del %filename%.exe if exist %filename%.obj del %filename%.obj if exist errors.txt del errors.txt %masm64_path%bin\ml64 /Cp /c /I"%masm64_path%Include" %filename%.asm >> errors.txt if errorlevel 1 exit if exist %1.rc ( %masm64_path%bin\RC /r /i"%masm64_path%\Include" %filename%.rc >> errors.txt %masm64_path%bin\link /SUBSYSTEM:WINDOWS /LIBPATH:"%masm64_path%Lib" ^ /LARGEADDRESSAWARE:NO /BASE:0x400000 /STUB:%masm64_path%bin\stubby.exe ^ /SECTION:.text,W /ALIGN:16 /entry:WinMain /MERGE:.rdata=.text ^ /fixed /nocoffgrpinfo %filename%.obj %filename%.res >> errors.txt if exist %1.res del %1.res ) else ( %masm64_path%bin\link /SUBSYSTEM:WINDOWS /LIBPATH:"%masm64_path%Lib" ^ /LARGEADDRESSAWARE:NO /BASE:0x400000 /STUB:%masm64_path%bin\stubby.exe ^ /SECTION:.text,W /ALIGN:16 /entry:WinMain /MERGE:.rdata=.text ^ /fixed /nocoffgrpinfo %filename%.obj >> errors.txt ) if errorlevel 1 exit goto exit1 :: При сборке приложения используется ключ /SUBSYSTEM:CONSOLE Приложения работают с символьным экраном, не формируют окна, не обрабатывают сообщения, не являются Windows-программами в обычном смысле. Операционная система предоставляет консоль :CONSOLE :: .... Какие-то команды для создания консольного приложения goto exit1 :: При сборке приложения используется ключ /DRIVER Создается драйвер режима ядра :DRIVER :: .... Какие-то команды для создания драйвера режима ядра goto exit1 :: При сборке приложения используется ключ /DLL Выполняет сборка DLL-библиотеки :DLL if exist %filename%.dll del %filename%.dll %masm64_path%bin\ml64 /c /Cp /I %masm64_path%include %filename%.asm >> errors.txt if errorlevel 1 exit if exist %1.rc ( %masm64_path%bin\RC /r %filename%.rc >> errors.txt if errorlevel 1 exit %masm64_path%bin\link /SUBSYSTEM:WINDOWS /LIBPATH:%masm64_path%lib ^ /ENTRY:DllMain /DLL /DLL /section:.bss,S /stub:%masm64_path%bin\stubby.exe ^ %filename%.obj %filename%.res /DEF:%filename%.def >> errors.txt if exist %1.res del %1.res ) else ( %masm64_path%bin\link /SUBSYSTEM:WINDOWS /LIBPATH:%masm64_path%lib ^ /ENTRY:DllMain /DLL /DLL /section:.bss,S /stub:%masm64_path%bin\stubby.exe ^ %filename%.obj /DEF:%filename%.def >> errors.txt ) if errorlevel 1 exit del %filename%.exp :: удаление «программного мусора» :exit1 del %filename%.obj del errors.txt :: выход из bat-файла exit :: процедура, которая разбирает первую строку asm-файла :read_settings for /f "eol=# tokens=2-3" %%A in (%filename%.asm) do ( set kind_of_file=%%A if %%B == # exit /b ) :: выход из процедуры exit /b В первой строке cls стираем с экрана в переменной masm64_path запоминаем путь к нашей IDE «set filename=» bat-файл через командную строку получает имя нашего asm-файла читаем первую строку выбранного asm-файла, которая начинается со знака «точка с запятой», которую компилятор посчитает «комментарием» и проигнорирует, но эта строка будет восприниматься командным процессором, который, в свою очередь, игнорирует символ «точка с запятой». Внутри нашего asm.bat вставим процедуру, которая по первому слову, составит метку, на которую передаст управление внутри asm.bat и там уже из нашего asm-файла сделают необходимый нам файл-результат (exe-gui, exe-consoe, dll, sys) Разделим наш asm-bat на несколько частей, допустим, в первой части asm.bat строки, которые собирают из asm-файла EXE-файлы для Windows (причем, когда нам нужен GUI ― подставляется параметр /SUBSYSTEM:WINDOWS, когда нужна консоль ― переходим во вторую часть и подставляем параметр /SUBSYSTEM:CONSOLE), когда требуется DLL-файлы ― переходим в третью часть, в четвертой части ― создаем SYS-файлы и так далее, часть строк asm-bat будет общая и служит для удаления «программного мусора» в виде obj-файлов и тому подобное. Братец Кролик нашел в книге Джеффри Рихтера «Windows. Создание эффективных Win32-приложений с учетом специфики 64-разрядных версий Windows» следующие строки Тип приложенияВходная функцияключ компоновщикаGUI- приложениеработает с ANSI-символами и строкамиWinMain/SUBSYSTEM: WINDOWSработает с Unicode-символами и строкамиwWinMainконсольное приложениеработает с ANSI-символами и строкамиmain/SUBSYSTEM: CONSOLEработает с Unicode-символами и строкамиwmain © Mikl___ 2019
Глава пятая. Братец Кролик разбирается с потрохами MessageBox MessageBox относится к диалоговому окну, специальному виду окон, позволяющих вести диалог с программой. Обмен информацией с программой и задание режимов ее работы осуществляется с помощью кнопок разного вида и с разными свойствами. Все кнопки, так же как сама диалоговая панель, являются окнами и их можно создавать в программе при помощи соответствующих функций Windows. В отличии от оконных приложений, при вызове диалогового окна значительную часть работы по созданию и поддержке таких окон берет на себя операционная система, что упрощает логику программы. Диалог, представляет собой окно со специальными свойствами, может существовать и без главного окна, образуя специфический тип "безоконного" приложения. Такие приложения, состоящие из одного диалогового окна, оказываются значительно проще программ с "главным окном". Функция MessageBox создает на экране диалоговую панель с текстом, заданным параметром lpszText и заголовком, заданным параметром lpszTitle. Код (C): int WINAPI MessageBox(HWND hwndParent, LPCSTR lpszText, LPCSTR lpszTitle, UINT fuStyle); Параметры fuStyleHexBin1MB_OK00000.0000.0000.0000.0000.00002MB_OKCANCEL10000.0000.0000.0000.0000.00013MB_ABORTRETRYIGNORE20000.0000.0000.0000.0000.00104MB_YESNOCANCEL30000.0000.0000.0000.0000.00115MB_YESNO40000.0000.0000.0000.0000.01006MB_RETRYCANCEL50000.0000.0000.0000.0000.01017MB_CANCELTRYCONTINUE60000.0000.0000.0000.0000.01108MB_TYPEMASKF0000.0000.0000.0000.0000.11119MB_NOICON000000.0000.0000.0000.0000.000010MB_ICONHAND, MB_ICONERROR, MB_ICONSTOP100000.0000.0000.0000.0001.000011MB_ICONQUESTION200000.0000.0000.0000.0010.000012MB_ICONEXCLAMATION, MB_ICONWARNING300000.0000.0000.0000.0011.000013MB_ICONASTERISK, MB_ICONINFORMATION400000.0000.0000.0000.0100.000014MB_USERICON800000.0000.0000.0000.1000.000015MB_ICONMASKF00000.0000.0000.0000.1111.000016MB_DEFBUTTON10000000.0000.0000.0000.0000.000017MB_DEFBUTTON21000000.0000.0000.0001.0000.000018MB_DEFBUTTON32000000.0000.0000.0010.0000.000019MB_DEFBUTTON43000000.0000.0000.0011.0000.000020MB_DEFMASKF000000.0000.0000.1111.0000.000021MB_APPLMODAL00000000.0000.0000.0000.0000.000022MB_SYSTEMMODAL10000000.0000.0001.0000.0000.000023MB_TASKMODAL20000000.0000.0010.0000.0000.000024MB_MODEMASK30000000.0000.0011.0000.0000.000026MB_HELP40000000.0000.0100.0000.0000.000027MB_NOFOCUS80000000.0000.1000.0000.0000.000028MB_MISCMASKC0000000.0000.1100.0000.0000.000029MB_SETFOREGROUND100000000.0001.0000.0000.0000.000030MB_DEFAULT_DESKTOP_ONLY200000000.0010.0000.0000.0000.000031MB_TOPMOST400000000.0100.0000.0000.0000.000031MB_NOTIFICATION_NT3X400000000.0100.0000.0000.0000.000032MB_RIGHT800000000.1000.0000.0000.0000.000033MB_RTLREADING1000000001.0000.0000.0000.0000.000034MB_SERVICE_NOTIFICATION2000000010.0000.0000.0000.0000.0000 Внимательно посмотрев на диапазон значений (от 0 до 200000h), мы увидим, что используется только 19 флагов (битов) из 32 возможных ― что оставляет 13 флагов, которые ещё не определены. А теперь более подробно. MessageBox не самостоятельная функция, а может быть вызвана только, чтобы передать сообщение от программы-владельца. Владелец MessageBox'а у нас идентифицируется по параметру hwndParent. Если, как в данном примере, "программы-владельца" нет, тогда hwndParent = 0, что означает "программой-владельцем" данного MessageBox является "заданный по умолчанию рабочий стол" ― первая запущенная прикладная программа, после того, как пользователь вошел в систему. Сообщение через MessageBox пользователю, который еще не ввел логин/пароль может послать служба операционной системы (например, CSRSS.EXE), но как быть, ведь пользователь еще не вошел в систему и у него нет активного рабочего стола? Для этого нужно в функцию MessageBox помимо прочих флагов передать MB_SERVICE_NOTIFICATION Во время запуска, используйте функцию GetVersionEx, чтобы проверить системную версию. Тогда при продолжении запуска Windows NT 3.x, используйте MB_SERVICE_NOTIFICATION_NT3X = 40000h, а для Windows NT 4.0, используйте MB_SERVICE_NOTIFICATION = 200000h Код (C): #ifdef _WIN32_WINNT #if (_WIN32_WINNT >= 0x0400) #define MB_SERVICE_NOTIFICATION 0x00200000L #else #define MB_SERVICE_NOTIFICATION 0x00040000L #endif #define MB_SERVICE_NOTIFICATION_NT3X 0x00040000L #endif Код (ASM): OSVERSIONINFO struct dwOSVersionInfoSize DWORD ? dwMajorVersion DWORD ? dwMinorVersion DWORD ? dwBuildNumber DWORD ? dwPlatformId DWORD ? szCSDVersion BYTE 128 dup(?) OSVERSIONINFO ends local VersionInfo:OSVERSIONINFO .... lea ecx,VersionInfo mov [rcx].OSVERSIONINFO.dwOSVersionInfoSize,sizeof OSVERSIONINFO invoke GetVersionEx mov eax,VersionInfo.dwMajorVersion;6 mov eax,VersionInfo.dwMinorVersion;1 Количество и тип кнопок Первые два бита (2-0) и 14-ый бит определяют количество кнопок и надписи на них fuStylehexbinMB_OK0000000000000000000000000MB_OKCANCEL1000000000000000000000001MB_ABORTRETRYIGNORE2000000000000000000000010MB_YESNOCANCEL3000000000000000000000011MB_YESNO4000000000000000000000100MB_RETRYCANCEL5000000000000000000000101MB_CANCELTRYCONTINUE6000000000000000000000110MB_TYPEMASK0Fh000000000000000000001111используется для программного определения количества кнопок и надписей на них. По количеству битов можно предположить, что сочетаний кнопок может быть и больше (от 0 до 0Eh) Код (C): int _CustomMessageBoxShowButtons ( HWND hwndDlg, UINT uType) { int nVisibleButtons = 0; switch(MB_TYPEMASK & uType) {case MB_OKCANCEL: ::ShowWindow(::GetDlgItem(hwndDlg, IDCANCEL), SW_SHOW); ++nVisibleButtons; // Fallsthrough case MB_OK: ::ShowWindow(::GetDlgItem(hwndDlg, IDOK), SW_SHOW); ++nVisibleButtons; break; case MB_YESNOCANCEL: ::ShowWindow(::GetDlgItem(hwndDlg, IDCANCEL), SW_SHOW); ++nVisibleButtons; // Fallsthrough case MB_YESNO: ::ShowWindow(::GetDlgItem(hwndDlg, IDYES), SW_SHOW); ::ShowWindow(::GetDlgItem(hwndDlg, IDNO), SW_SHOW); nVisibleButtons += 2; break; case MB_ABORTRETRYIGNORE: ::ShowWindow(::GetDlgItem(hwndDlg, IDABORT), SW_SHOW); ::ShowWindow(::GetDlgItem(hwndDlg, IDRETRY), SW_SHOW); ::ShowWindow(::GetDlgItem(hwndDlg, IDIGNORE), SW_SHOW); nVisibleButtons = 3; break; case MB_RETRYCANCEL: ::ShowWindow(::GetDlgItem(hwndDlg, IDRETRY), SW_SHOW); ::ShowWindow(::GetDlgItem(hwndDlg, IDCANCEL), SW_SHOW); nVisibleButtons = 2; break; #if(WINVER >= 0x0500) case MB_CANCELTRYCONTINUE: ::ShowWindow(::GetDlgItem(hwndDlg, IDCANCEL), SW_SHOW); ::ShowWindow(::GetDlgItem(hwndDlg, IDTRYAGAIN), SW_SHOW); ::ShowWindow(::GetDlgItem(hwndDlg, IDCONTINUE), SW_SHOW); nVisibleButtons = 3; break; #endif default: // Not implemented yet _ASSERTE(!"Not implemented"); break; } if (MB_HELP & uType) { ::ShowWindow(::GetDlgItem(hwndDlg, IDHELP), SW_SHOW); ++nVisibleButtons; } return nVisibleButtons; } MB_HELP4000h00000000010000000000XXXдополнительную кнопку "Справка" можно добавить к любому сочетанию клавиш Нажатие на кнопку , или нажатие клавиши ESC, или выбор кнопки возвращает значение IDCANCEL=2. Если у MessageBox стиль MB_OK, то нажатие клавиши ESC, или выбор кнопки возвращает значение IDOK=1. Если у окна сообщений нет кнопки , (стили MB_YESNO, MB_ABORTRETRYIGNORE), тогда кнопка не активизирована и нажатие на ESC не имеет никакого эффекта. Выбор кнопки или нажатие F1 генерирует событие "появления Справки". Остальные кнопки реагируют на mouse-click или на нажатие Enter если кнопка имеет фокус. Иконки 4 следующих бита (8-4) определяют иконку fuStylehexbinMB_NOICON00h000000000000000000000000MB_ICONHAND, MB_ICONERROR, MB_ICONSTOP10h000000000000000000010000MB_ICONQUESTION20h000000000000000000100000MB_ICONEXCLAMATION, MB_ICONWARNING30h000000000000000000110000MB_ICONASTERISK, MB_ICONINFORMATION40h000000000000000001000000По количеству битов можно предположить, что иконок может быть и больше (от 0 до 7), но реально значениям с 50h по 70h не соответствуют никакие иконки.MB_USERICON80h000000000000000010000000иконка, выбранная пользователем MB_ICONMASKF0h000000000000000011110000для программного определения иконкиПример программного определения иконки Код (C): void _CustomMessageBoxSetIcon( HWND hwndDlg, UINT uType) { switch(MB_ICONMASK & uType) {case MB_ICONEXCLAMATION: ::SendDlgItemMessage(hwndDlg, ID_MSGBOXICON, STM_SETICON, (WPARAM)::LoadIcon(NULL, IDI_EXCLAMATION), 0); break; case MB_ICONQUESTION: ::SendDlgItemMessage(hwndDlg, ID_MSGBOXICON, STM_SETICON, (WPARAM)::LoadIcon(NULL, IDI_QUESTION), 0); break; case MB_ICONASTERISK: ::SendDlgItemMessage(hwndDlg, ID_MSGBOXICON, STM_SETICON, (WPARAM)::LoadIcon(NULL, IDI_ASTERISK), 0); break; case MB_ICONHAND: ::SendDlgItemMessage(hwndDlg, ID_MSGBOXICON, STM_SETICON, (WPARAM)::LoadIcon(NULL, IDI_HAND), 0); break; default: // Turn off the icon ::ShowWindow(::GetDlgItem(hwndDlg, ID_MSGBOXICON), SW_HIDE); break; } }