Введение
Сообщения - нервная система 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, вы узнаете об изменении графического режима экрана.
Статья опубликована в журнале "Программист"
№5 за 2002 год