Visual Basic, .NET, ASP, VBScript
 

   
 

Родился я 07 ноября 1974 года. Как и все нормальные люди закончил среднюю школу, а потом (уже не как все) пошел учиться в институт. С выбором профессии я явно ошибся ;-) и поступил учиться в сельскохозяйственный институт на технолога сельскохозяйственного производства. Примерно на втором курсе я понял, что жестоко ошибся, но бросать учебу не стал и закончил учебу с дипломом с отличием. Не много думая сразу пошел на второе высшее образование в академию Государственной службы на факультет экономики и, через 3 года, стал дипломированным экономистом со специализацией финансовый менеджмент. Так у меня стало два диплома.
А как же программирование, спросите Вы. Это мое хобби, программирую на VB, пишу сайты на HTML и ASP (посмотрите внимательнее на этот сайт). Могу делать базы данных Access, да и вообще на все руки мастер ;-)).

 
     
   
 

Введение

Сообщения - нервная система 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 год

 
     

   
   
     
  VBNet рекомендует