Сообщения Windows в Visual Basic Введение Сообщения - нервная система Windows. Каждую секунду Windows рассылает десятки сообщений работающим программам, информируя их об изменениях среды и действиях пользователя. Но многие VB-программисты ничего не знают об этом, так как Visual Basic заботливо оградил их от этого, создав свою, более простую для понимания, систему. Однако, за простоту и доступность приходится платить некоторыми ограничениями, которые иногда оказываются слишком серьезными. Сообщения и события Когда создавался Visual Basic, его разработчики выбрали схему работы программ, основанную на событиях. Ее главная особенность состоит в том, что программисту предлагают уже готовый набор стандартных событий, порождаемых соответствующими Windows-сообщениями (например, событие MouseMove в Visual Basic порождается сообщением WM_MOUSEMOVE). Все остальные Windows-сообщения, для которых разработчики Visual Basic не создали соответствующего события, либо скрыты от программиста, либо игнорируются. Вторая особенность Visual Basic в том, что многие стандартные элементы управления (Form, ListBox, ComboBox и другие) уже умеют реагировать на некоторые основные Windows-сообщения. Так, например, Form можно перемещать по экрану мышью, изменять ее размеры, сворачивать, разворачивать, она имеет стандартное системное меню, которое реагирует на все основные команды и так далее. "Ну и хорошо, чего же еще надо?" - скажете вы. В большинстве случаев я, пожалуй, соглашусь с вами. Но представьте себе, что в вашей программе понадобилось сделать что-то нестандартное, например, добавить новый пункт в системное меню. Программист, знакомый с API, сможет сделать это за пару минут, и нужный пункт, не сомневаюсь, там появится, но вот что будет, если пользователь программы его выберет? С точки зрения Windows все будет в порядке: сообщение о том, что пользователем был выбран этот пункт, отправится владельцу этого меню. Но что произойдет с точки зрения Visual Basic? Итак, ваша форма получила сообщение о выборе системного меню. Это сообщение ей знакомо, и меню откроется. Пользователь выбирает новый пункт, Windows посылает соответствующее сообщение, а в ответ... тишина. А чего вы ждали в ответ? Ваша форма, не имеющая ни малейшего представления о полученном сообщении, просто его проигнорирует! Можно тысячу раз выбирать этот пункт меню, и Windows тысячу раз сообщит об этом форме, но все бесполезно. От вас потребуются огромные усилия, чтобы заставить программу обрабатывать сообщения, а в некоторых случаях вы, поверьте мне, будете бессильны. Посылка сообщений Отправить сообщение другому окну - дело нехитрое. Конечно, Visual Basic не сможет нам в этом помочь, но и не помешает. В Win32 API имеется функция SendMessage: LRESULT SendMessage( HWND hWnd, // описатель окна назначения UINT Msg, // посылаемое сообщение WPARAM wParam, // первый параметр сообщения LPARAM lParam // второй параметр сообщения ); hWnd - описатель окна, которому посылается сообщение. В Visual Basic получить описатель окна можно с помощью одноименного свойства. Например, для получения описателя TextBox можно написать так: Text1.hWnd. Msg - номер посылаемого сообщения. Лучше всего пользоваться соответствующими константами, предварительно объявив их. Например, константа WM_SYSCOMMAND равна &H112. wParam и lParam специфичны для каждого конкретного сообщения. Более того, lParam - нетипизированный параметр. Что это означает для VB-программиста? Во-первых, то, что его нужно объявить как параметр типа Any, т.е. отключить проверку типов со стороны Visual Basic. Почему? Потому что lParam может использоваться для передачи не только чисел, но и указателей на строки или UDT, а объявив его как Long, вы не сможете передать в функцию строку String, чего требуют некоторые сообщения. Функция SendMessage возвращает свое значение для каждого конкретного сообщения. В большинстве случаев его можно игнорировать (например, при посылке сообщения EM_UNDO), но в некоторых случаях это значение очень даже нужно (например, EM_CANUNDO). Лучше его объявить как тип Long. Итак, функцию нужно объявить примерно так (обратите внимание, что мы воспользуемся ее ANSI -псевдонимом):Public Declare Function SendMessage Lib "user32" Alias "SendMessageA" (ByVal_ hwnd As Long, ByVal wMsg As Long, ByVal wParam As Long, lParam As Any) As Long Теперь давайте разберемся, что можно сделать с помощью этой функции. В качестве примера я расскажу, как можно реализовать простейшую функцию Undo (отмена последней операции) для TextBox. TextBox в Visual Basic не имеет такого метода, но у стандартного текстового окна в Windows, есть сообщение EM_UNDO, отменяющее последнюю операцию, а так как TextBox в Visual Basic основан на стандартном текстовом окне, то он также должен уметь реагировать на это сообщение. Более того, попробуйте использовать в своих программах комбинацию Ctrl+Z и Вы поймете, что TextBox уже умеет выполнять отмену последней операции! Раз все так прекрасно, остается только послать TextBox, нужное сообщение. Давайте посмотрим, как это можно сделать:Call SendMessage(Text1.hWnd, EM_UNDO, 0&, 0&) Обратите внимание, что это сообщение не нуждается в дополнительных параметрах, кроме того, возвращаемое значение можно игнорировать. Важно: Кроме EM_UNDO в Windows API есть еще одно полезное сообщение на эту же тему - EM_CANUNDO, с помощью которого можно узнать, возможна ли отмена в данный момент:Dim CanUndo as Boolean CanUndo = SendMessage(Text1.hWnd, EM_CANUNDO, 0&, 0&) В этом примере переменная CanUndo получит значение True в случае, если отмена для Text1 возможна, и False в противном случае. Кстати, если послать сообщение EM_UNDO окну, у которого пуст буфер отмен, ошибки не возникнет - просто ничего не произойдет. Я показал, как использовать SendMessage для "своих" окон, но вам, вероятно, понадобится посылать сообщения и "чужим" окнам. Это делается точно так же, единственная проблема состоит в том, чтобы найти hWnd нужного окна. Для этого нужно построить список существующих окон и выбрать из них нужное. Составлять список можно по-разному, я рекомендую следующий способ: найти hWnd окна верхнего уровня Windows (это рабочий стол) и затем перебрать все дочерние окна, определяя, есть ли у них свои дочерние окна и заголовок:'может быть ошибка, если при построении списка изменится количество окон в системе On Error Resume Next 'Построение списка программ i = 0 msStart = timeGetTime 'запомним время начала задачи 'Первое окно Desktop hWnd = GetWindow(GetDesktopWindow, GW_CHILD) Do While hWnd <> hNull 'Запросить заголовок найденного окна sWindowText = WindowTextFromWnd(hWnd) If sWindowText <> Empty Then 'Нужное нам окно видимое, с названием и без потомков If IsVisibleTopWnd(hWnd, False, False, False) Then 'Заполняем массив, содержащий название окна и его hWnd i = i + 1 'увеличиваем счетчик ReDim Preserve AppRun(i) AppRun(i).hWnd = hWnd AppRun(i).Name = sWindowText End If End If 'Следующее окно этого же уровня hWnd = GetWindow(hWnd, GW_HWNDNEXT) msStop = timeGetTime 'время окончания задачи 'если "повисли", то выйти из цикла If msStop - msStart > 250 Then Exit Do Loop 'функция используется для получения заголовка окна по его hWnd Function WindowTextFromWnd(ByVal hWnd As Long) As String Dim c As Integer 'вполне достаточно Integer Dim s As String c = GetWindowTextLength(hWnd) 'получаем длину заголовка If c <= 0 Then Exit Function 'если длина 0, то выход s = String$(c, 0) 'создаем строку-приемник для заголовка c = GetWindowText(hWnd, s, c + 1) 'получаем заголовок WindowTextFromWnd = s 'возвращаем его End Function Важно: При построении списка всех окон в системе можно войти в бесконечный цикл. Это может случиться, если при проходе по всем окнам, изменится их количество в системе. Для того чтобы избежать этого, я использую проверку времени выполнения цикла, и если цикл выполняется более 250 мс, то предполагаю, что мы "повисли", и завершаю цикл принудительно. Этот метод, конечно, отнимает дополнительные ресурсы у системы, но зато надежен. Существует другой способ построения списка всех окон - через функцию EnumWindows, но мне кажется, что GetWindow проще, хотя при использовании EnumWindows бесконечного цикла не получится. Определив hWnd и заголовки всех окон, вы без особого труда найдете нужное вам, и сможете отправить ему сообщение. Прием сообщений Прием сообщений в версиях до VB6 был вовсе невозможен с помощью стандартных средств языка. Дело в том, что для этого нужна API-функция SetWindowLong, являющаяся callback-функцией (т.е. она требует передачи ей в качестве одного из параметров указателя на другую функцию), а в Visual Basic невозможно было получить указатель на функцию. С приходом новой версии Visual Basic 6 все изменилось. Теперь VB имеет в своем арсенале оператор AddressOf для получения указателей, но с большими ограничениями: можно получить указатель на функцию расположенную только в стандартном модуле, Вы никогда не сможете получить указатель на объект и т.д. Меня, да и многих других программистов, этот оператор не вполне устраивает, ввиду этих ограничений, а также проблем, связанных с отладкой программы, но лучше такой оператор, чем ничего. Итак, давайте рассмотрим прием сообщений с теоретической точки зрения. У каждого окна в Windows должна быть некая процедура обработки сообщений. Создавая новое окно, Visual Basic пишет за нас такую процедуру, включая в нее обработку сообщений, которые считает нужными, и превращая некоторые из них в соответствующие события (которые мы можем обрабатывать), а некоторые обрабатывает сам. Как же перехватывать сообщения? Для этого используется такая методика, как создание подкласса окна. Вы просто подменяете уже существующую оконную процедуру, заботливо созданную VB, на свою собственную с помощью функции WinAPI SetWindowLong, которая позволяет изменять некоторые атрибуты окна, в том числе и оконную процедуру. Важно: Создание подкласса окна только на первый взгляд безопасное и простое занятие. Дело в том, что создавая подкласс окна Вы оказываетесь впереди Visual Basic, принимая все на себя со всеми вытекающими отсюда последствиями. Основная проблема это отсутствие контроля типов данных, т.е. передавая какие-либо данные всегда проверяйте что вы передаете и, например, если передадите в CallWindowProc другие типы данных или перепутаете порядок следования переменных, то Widows может рухнуть... и никто вас не предупредит об этом. Еще одна большая неприятность заключается в том, что вы не сможете запросто использовать все средства отладки, имеющиеся в распоряжении Visual Basic, например, любая остановка на Breakpoint в коде подкласса может привести к фатальным последствиям, а окна Watch и Locals обычно не работают. Так что мой вам совет - пишите код один раз и без ошибок. Объявить эту функцию нужно так:Declare Function SetWindowLong Lib "user32" Alias "SetWindowLongA" (ByVal_ hwnd As Long, ByVal nIndex As Long, ByVal dwNewLong As Long) As Long hwnd - дескриптор окна, для которого выполняется действие; nIndex - смещение в памяти для доступа к изменяемому атрибуту окна; для оконной процедуры смещение обозначается константой GWL_WNDPROC = -4; dwNewLong - новое значение изменяемого параметра (атрибута), в нашем случае это указатель на новую процедуру. Эта функция возвращает старое значение измененного параметра, в нашем случае - указатель на замененную функцию. Такую замену можно производить в событии Form_Load, а восстановление стандартной оконной процедуры - в событии Form_Unload. Если кто-то думает, что восстанавливать старую оконную процедуру в "правах" не обязательно, то он глубоко заблуждается. Если этого не сделать и завершить программу (это можно сделать, например, нажав кнопку End на панели инструментов VB), то среда разработки будет закрыта операционной системой Windows с очень интуитивно понятным сообщением типа "Программа выполнила недопустимую операцию и будет закрыта". Естественно, что после нескольких таких сообщений Вам придется перезагрузить компьютер, особенно в системах Windows 9x. Так что мой Вам совет: забудьте про кнопку End. Теперь давайте посмотрим на код, отвечающий за подмену оконной процедуры:Public OldWndProc as Long 'переменная для хранения указателя на старую оконную процедуру Public Const GWL_WNDPROC = (-4) Private Sub Form_Load() gWH = Me.hWnd 'дескриптор нашего окна OldWndProc = SetWindowLong(gWH, GWL_WNDPROC, AddressOf WindowProc) End Sub Private Sub Form_Unload(Cancel As Integer) SetWindowLong gWH, GWL_WNDPROC, OldWndProc End Sub Собственная же процедура WindowProc может, в общем случае, выглядеть так (поместите этот код в стандартный модуль!):Private Const WM_MENUSELECT = &H11F Function WindowProc(ByVal hwnd As Long, ByVal Msg As Long, ByVal_ wParam As Long, ByVal lParam As Long) As Long Dim lReturn As Long 'вначале позволим произвести обработку стандартной оконной процедуре процедуре, а затем сами lReturn = CallWindowProc(OldWndProc, hwnd, Msg, wParam, lParam) Select Case Msg 'проверяем сообщения Case WM_MENUSELECT 'если нужное нам, то выполняем некоторые действия (вместо 'WM_MENUSELECT может быть любое другое сообщение, константа) ... ... End Select WindowProc = lReturn 'вернем значение функции End Function С теоретической точки зрения созданная функция WindowProc и будет подклассом окна, hWnd которого передается в первом параметре этой функции. Это означает, что теперь все сообщения, адресованные этому окну, будут направляться в функцию WindowProc и вы сможете их обработать. Обрабатывать сообщения внутри собственной оконной процедуры можно по-разному: обработать сообщение, а затем передать его стандартной оконной процедуре; вначале дать стандартной оконной процедуре обработать сообщение, а затем дополнительно обработать его самостоятельно; обработать сообщение и не передавать его в стандартную оконную процедуру. Важно: Создав свою оконную процедуру и подключив ее к нужному окну, помните, что с этого момента она и только она одна будет получать все сообщения, адресованные этому окну. Отсюда следует, что если вы игнорируете какие-либо сообщения и не передаете их в стандартную оконную процедуру, отвечаете за последствия только вы. Например, если вы собираетесь обрабатывать сообщение WM_SYSCOMMAND, то не стоит отрезать его от системы, иначе можно многое потерять. С другой стороны, если вы проводите обработку своего сообщения, то вполне можно ограничиться и собственным кодом. Но представьте себе, что какой-нибудь программист создал из своей программы подкласс для Вашего окна и "следит" за Вашими сообщениями? Получит он их в этом случае? Ответ один - нет. Ну и что он о Вас подумает? Отладка подклассов Отладка кода с подклассами отличается от отладки обычного кода. Самое важное в подклассах это восстановить указатель на стандартную процедуру обработки сообщений перед выходом из программы, либо при прерывании ее с помощью кнопки Break. Если остановить программу, нажав End на панели инструментов, то Visual Basic будет закрыт с сообщением о выполнении недопустимой операции. Принимая это во внимание, при создании подклассов, нужно забыть об этой кнопке и всегда завершать программу правильно. Есть еще одна проблема. Создайте подкласс окна и установите в процедуре обработки событий точку прерывания. Теперь запустите программу и дождитесь, когда программа прервется в этой точке при обработке одного из событий, которых может быть сотни в секунду! Куда денутся все остальные сообщения? А никуда! Они просто пропадут, так как чтобы перейти к обработке следующего сообщения, нужно выйти из процедуры. Но вы не можете выйти из процедуры, поскольку кнопки на панели инструментов и меню среды VB не работают. Получается, что среда VB как бы зависла... Впрочем, нажав F5, вы перейдете к следующему сообщению, но проблема в том, что Windows посылает очень много сообщений, и следующее событие наступает так быстро, что вы не успеете понять, в чем дело, и VB снова в "коме". Так что вам не удастся воспользоваться ни окном Watch, ни Locals... Где же выход? Выход есть. Он заключается в выносе критичного кода подкласса в отдельную DLL-библиотеку, в которой и нужно проводить обработку всех событий. Передавать же в клиентскую программу следует только те события, которые вам действительно нужны. Но создание таких DLL дело непростое, а, я бы сказал, очень даже сложное и доступно оно не каждому. Если попробовать это описать, то, думаю, получится статья побольше той, которую вы сейчас читаете. Может быть я возьмусь ее написать в скором времени. Заключение В этой статье я попытался рассказать о том, как преодолеть некоторые ограничения Visual Basic, обрабатывая те сообщения Windows, о которых Visual Basic ничего не знает. Освоив технику создания подклассов для приема сообщений, вы сможете значительно расширить свои возможности. Вот некоторые из наиболее интересных направлений работы в этой области: обрабатывая сообщение WM_ENTERIDLE, вы сможете поймать момент для выполнения фоновых операций; с помощью сообщения WM_GETMINMAXINFO, можно задать для окна его максимальные и минимальные размеры; используя сообщение WM_MENUSELECT, легко реализовать вывод подсказок в строке состояния при выборе пунктов меню; сообщение WM_FONTCHANGE позволяет узнавать об изменениях шрифтов в системе, а обработав WM_DISPLAYCHANGE, вы узнаете об изменении графического режима экрана.