Введение в основы программирования сообщений
 
Автор rdc

Исторически языки программирования были классифицированы как процедурные и на основе сообщений. К примеру QuickBasic может быть классифицирован как процедурный язык , а Visual Basic может быть классифицирован как язык на основе сообщения (или событийный). В процедурном языке, программа обычно начинается с верхней части, делает некоторые вещи , то есть ее работа происходит несколько линейно. В событийном языке как правило инициализируется система , а затем программа сидит в цикле простоя и ждет событий. Когда происходит событие, вы обрабатывать его , а затем программа возвращается в цикл простоя. В конечном итоге выход из цикла происходит, когда пользователь закроет программу.

В процедурном языке у вас есть полный контроль над тем, что пользователь видит и делает. В событийном языке вы работаете в сотрудничестве с операционной системой и пользователем, и обрабатываете только те сообщения, которые вас интересуют, а остальные сообщение позволяете обрабатывать операционной системе. Подлинным камнем преткновения для программистов, которые приходят к событийном языку из процедурного языка является концепция выборочного реагирования на сообщения. В большинстве языков, включая процедурные, сообщения играют важную роль.

Если вы когда-либо использовали язык, который поддерживает вызовы подпрограмм и функций, то вы использовали программирования на основе сообщений. Например у вас написана игра в QuickBasic, в которой обработка клавиш со стрелками происходит в цикле ожидания. При нажатии клавиши со стрелкой вверх, вызывается подпрограмма, которая обновляет позицию спрайта на экране. Если была нажата клавиша A, вы игнорируете ее, так как вы не заботитесь о работе этой клавиши в игре. Это и есть программирование на основе сообщений. Сообщением является клавиша , а подпрограмма обновления спрайта - это обработчик сообщений.

Любой язык структурного программирования может быть классифицирован как событийный язык программирования. Программирование на основе сообщений — это концепция или способ обработки ввода данных пользователем и реагирование на этот ввод. Это методика больше чем тип языка. Она стала доминантой для некоторых языков программирования, когда графический интерфейс пользователя (GUIs) возобладал над командной строкой в операционной системе.

В GUI операционной системы, например в Windows, все графические элементы внутренне управляются. Программисту не нужно создавать поле редактирования текста с нуля, он просто использует поле редактирования, встроенное в графическую оболочку, которая должна сообщать программисту, когда пользователь хочет обновить поле редактирования. Самый естественный метод - это направить сообщение в программу, и указать, что поле редактирования был обновлено. Под Windows , это заимствование элементов графического интерфейса пользователя и получение сообщений,  было официально оформлено в то, что сегодня называется Windows Software Development Kit, или Windows SDK.

Windows SDK — это совокупность интерфейсов прикладного программирования (API) в набор библиотек динамической компоновки (DLL), которые составляют большинство операционной системы. Любая программа на основе графического интерфейса пользователя, под управлением Windows, использует SDK, даже если это не очевидно. В быстрых разработках приложений (RAD) языках таких как Delphi, Visual Basic или Real Basic скрыты сведения о пакете SDK с помощью свойств и событий, но в реале они используют SDK.

Хотя языки RAD позволяют программисту быстро создавать графический интерфейс программы, это также означает, что мелкие детали SDK не доступны. Например весьма трудным является процесс сабклассирования элементов управления с помощью VB, но довольно прямолинейно с помощью SDK. Однако SDK огромен, и размер API является достаточно большим, чтобы многие программисты сдались использовать идею SDK программирования. Многие думают, что это слишком сложно и трудно использовать, хотя это и не так. Поскольку операционная система обрабатывает все графические элементы для программы, программист может сосредоточиться на наиболее важных аспектах разработки программы, таком как взаимодействие с пользователем. В конце концов GUI программа - это все о взаимодействии с пользователем.

В FreeBasic нет RAD системы для программирования Windows. Чтобы создать программу Windows в FreeBasic, вам придется использовать SDK, так как это единственный вариант. Хотя пакет SDK является огромным и вероятно на его обучение уйдет вся жизнь, чтобы полностью понять, для 99% всех программ Windows требуется лишь небольшое подмножество пакета SDK. Реальность такова, что использовать SDK не сложнее, а даже проще , чем если бы вы самостоятельно создавали все элементы графического интерфейса Windows SDK.

Отложив в сторону все детали Windows API на данный момент, важно понять механизм сообщений в программе SDK. Это лучше сделать, глядя на нашего "старого знакомого", программы Hello World. В  папке examples\Windows\gui  FreeBasic версии 15b  (который необходим для кода в этой статье), есть хорошая программа Hello World, и я собираюсь украсть ее или точнее позаимствовать, как базу для этого примера.

Примечание от переводчика: пример программы был чуть изменен для совместимости с последними версиями freebasic.

#include once "windows.bi"

Declare Function        WinMain     ( ByVal hInstance As HINSTANCE, _
                                      ByVal hPrevInstance As HINSTANCE, _
                                      szCmdLine As String, _
                                      ByVal iCmdShow As Integer ) As Integer
                                  
                                  
    ''
    '' Точка входа    
    ''
    End WinMain( GetModuleHandle( null ), null, Command$, SW_NORMAL )

'' ::::::::
'' имя: WndProc
'' описание: обработка windows сообщений
''
'' ::::::::
Function WndProc ( ByVal hWnd As HWND, _
                   ByVal message As UINT, _
                   ByVal wParam As WPARAM, _
                   ByVal lParam As LPARAM ) As LRESULT
    
    Function = 0
    
    ''
    '' обработка сообщений
    ''
    Select Case( message )
        ''
        '' при создании окна
        ''        
        Case WM_CREATE            
            Exit Function
        
        '' пользователь кликнул по форме
        Case WM_LBUTTONUP
            MessageBox NULL, "Hello world from FreeBasic", "FB Win", MB_OK
        ''
        '' перерисовка окна
        ''
        Case WM_PAINT
            Dim rct As RECT
            Dim pnt As PAINTSTRUCT
            Dim hDC As HDC
          
            hDC = BeginPaint( hWnd, @pnt )
            GetClientRect( hWnd, @rct )
            
            DrawText( hDC, _
                      "Hello Windows from FreeBasic!", _
                      -1, _
                      @rct, _
                      DT_SINGLELINE Or DT_CENTER Or DT_VCENTER )
            
            EndPaint( hWnd, @pnt )
            
            Exit Function            
        
        ''
        '' нажатия клавиш
        ''
        Case WM_KEYDOWN
            'Close if esc key pressed
            If( LoByte( wParam ) = 27 ) Then
                PostMessage( hWnd, WM_CLOSE, 0, 0 )
            End If

        ''
        '' закрытие окна
        ''
        Case WM_DESTROY
            PostQuitMessage( 0 )
            Exit Function
    End Select
    
    ''
    '' Сообщение не касающиеся нас, отправляются в обработчик по умолчанию
    '' и возвращается результат
    ''
    Function = DefWindowProc( hWnd, message, wParam, lParam )    
    
End Function

'' ::::::::
'' имя: WinMain
'' описание: Точка входа в программу gui win2
''
'' ::::::::
Function WinMain ( ByVal hInstance As HINSTANCE, _
                   ByVal hPrevInstance As HINSTANCE, _
                   szCmdLine As String, _
                   ByVal iCmdShow As Integer ) As Integer    
     
    Dim wMsg As MSG
    Dim wcls As WNDCLASS     
    Dim szAppName As String
    Dim hWnd As HWND
     
    Function = 0
     
    ''
    '' установка класса окна
    ''
    szAppName = "HelloWin"
     
    With wcls
        .style         = CS_HREDRAW Or CS_VREDRAW
        .lpfnWndProc   = @WndProc
        .cbClsExtra    = 0
        .cbWndExtra    = 0
        .hInstance     = hInstance
        .hIcon         = LoadIcon( NULL, IDI_APPLICATION )
        .hCursor       = LoadCursor( NULL, IDC_ARROW )
        .hbrBackground = GetStockObject( WHITE_BRUSH )
        .lpszMenuName  = NULL
        .lpszClassName = StrPtr( szAppName )
    End With
          
    ''
    '' регистрация класса окна     
    ''     
    If( RegisterClass( @wcls ) = FALSE ) Then
       MessageBox( null, "Failed to register wcls!", szAppName, MB_ICONERROR )
       Exit Function
    End If
    
    ''
    '' создаем окно и показываем
    ''
    hWnd = CreateWindowEx( 0, _
                            szAppName, _
                           "The Hello Program", _
                           WS_OVERLAPPEDWINDOW, _
                           CW_USEDEFAULT, _
                           CW_USEDEFAULT, _
                           CW_USEDEFAULT, _
                           CW_USEDEFAULT, _
                           NULL, _
                           NULL, _
                           hInstance, _
                           NULL )
                          

    ShowWindow( hWnd, iCmdShow )
    UpdateWindow( hWnd )
     
    ''
    '' обработка windows сообщений
    ''
    While( GetMessage( @wMsg, NULL, 0, 0 ) <> FALSE )    
        TranslateMessage( @wMsg )
        DispatchMessage( @wMsg )
    Wend
    
    
    ''
    '' Программа закончилась
    ''
    Function = wMsg.wParam

End Function



Если вы успешно скомпилировали и запустили программу, вы увидите стандартное окно с сообщением, напечатанное на форме. Если щелкнуть форму, то будет отображаться окно сообщения, и если вы нажмете клавишу Escape, программа закроется.

Найдите время, чтобы изучить окна. Вы увидите, что форма имеет кнопки свертывания, развертывания, меню системы и это может быть изменено. Теперь посмотрим на код выше. Любой код, необходимый для обработки свойств указанного окна, дескрипторы ОС - все это для вас не существует. Есть также одна строка кода для отображения messagebox, которая сама по себе, представляет собой довольно сложный объект. Отношение результата к количеству кода, является весьма примечательным. Если вы были, чтобы попытались воссоздать эту простуя программу, с помощью стандартных графических команд на FreeBasic, программа стала бы в сто раз больше.

Первое, что вы должны заметить в коде - это формат. Это основной формат любой Windows программы FreeBasic. Каждая программа Windows, независимо от того, простая или сложная, будет следовать этому базовому формату. Двумя ключевыми элементами этой программы являются функции WinMain и WinProc.

WinMain процедура не является процедурой, которую вызывает Windows при запуске программы. Это точка входа программы Windows. В WinMain строится и регистрируется главное окно программы , а затем программа заходит в цикл обработки сообщений для обработки сообщений. После того, как программа входит в цикл сообщений, она начнет обработку сообщений в процедуре WinProc. Поскольку эта статья — о модели сообщений в программе Windows, мы только обсудим цикл обработки сообщений в процедурах WinMain и WinProc.

Когда программа работает под управлением операционной системы Windows, сообщения создаются постоянно. Когда запущена программа Windows, ОС будет отправлять сообщения в программу, когда что-то случается, и ОС думает, что программа должны знать об этих сообщениях. Некоторые из этих конкретных сообщений программы отправляются непосредственно в WinProc (или аналогичную) процедуру, а другие, главным образом сообщения, созданные пользователем, помещаются в очередь сообщений. Поскольку большая часть программы является взаимодействие с пользователем, важно понять очередь сообщений.

Очередь — это структура данных, где данные в добавляются в «конец» очереди , а удаляются с «начала». Это называется "первым вошел - первым вышел" или стек FIFO. Если вы когда-нибудь стоял в очереди, чтобы купить билеты в кино, вы знаете, что такое очередь.

Для программы очередь сообщений будет содержать одно или больше сообщений, выстроенных как люди в кино. Ожидающий цикл программы Windows сидит и ожидает поступления сообщений , а затем перенаправляет сообщения в программу. Этот цикл обработки сообщений содержится в следующем фрагменте кода из WinMain.

    ''
    '' обработка windows сообщений
    ''
    While( GetMessage( @wMsg, NULL, 0, 0 ) <> FALSE )    
        TranslateMessage( @wMsg )
        DispatchMessage( @wMsg )
    Wend



Процедура GetMessage процедура получает сообщение из очереди через параметр wMsg. Параметр wMsg является типом MSG, который содержит необходимую информацию, относящуюся к конкретному сообщению. Вот определение типа MSG.

Type MSG
hwnd As HWND
    message As UINT
    wParam As WPARAM
    lParam As LPARAM
    Time As DWORD
    pt As Point
End Type



hwnd представляет собой дескриптор окна, для которого необходимо обработать сообщение. Это сообщение будет обработано процедурой этого окна в WinProc.

Message это идентификатор сообщения. Он может быть, например, WM_CREATE, который сигнализирует, что окно было создано, но еще не показано

wParam и lParam указывают дополнительную информацию, основанную на типе сообщения. Например при нажатии клавиши, можно получить код ключа с помощью lobyte из wParam.

time указывает время, в которое было размещено сообщение , а pt является структурой, содержащей позицию курсора, когда было сгенерировано сообщение.

TranslateMessage преобразует виртуальную клавишу сообщения в символьную клавишу сообщения, а затем кладет их обратно в очереди так, чтобы клавиша могла обрабатываться при желании. Эта процедура будет требоваться любой программе, использующей клавиатуру. DispatchMessage отправляет сообщение в процедуру WinProc (или аналогичную), связанную с окном, идентифицированным параметром hWnd.

Подводя итог действий, пользовательские сообщения будут помещены в "конец" очереди сообщений. GetMessage извлекает первое сообщение из очереди, передает его в TranslateMessage,  которая преобразует сообщение в случае необходимости, и кладет его обратно в очередь. Затем сообщение передается в DispatchMessage, которая рассматривает сообщение, чтобы определить, какое окно должно получить сообщение, а затем передает сообщение в процедуры обработчика окна, который в нашем примере, является WinProc.

Прежде чем мы обсудим процедуру WinProc , мы должны задать вопрос: Что произойдет, если имеется более одного окна в программе? Как WinMain узнает, какую процедуру использовать? Ответ содержится в структуре WNDCLASS. В нашем примере, wcls определяется как WNDCLASS в процедуре WinMain.

    With wcls
        .style         = CS_HREDRAW Or CS_VREDRAW
        .lpfnWndProc   = @WndProc
        .cbClsExtra    = 0
        .cbWndExtra    = 0
        .hInstance     = hInstance
        .hIcon         = LoadIcon( NULL, IDI_APPLICATION )
        .hCursor       = LoadCursor( NULL, IDC_ARROW )
        .hbrBackground = GetStockObject( WHITE_BRUSH )
        .lpszMenuName  = NULL
        .lpszClassName = StrPtr( szAppName )
    End With



Как вы можете видеть, структура WNDCLASS содержит всю информацию, касающуюся конкретного окна. В отношении сообщений, важным пунктом является поле lpfnWndProc.. Это поле содержит адрес нашего обработчика сообщений для данного окна. Оператор @ в FreeBASIC возвращает адрес объекта, в данном случае адрес нашей процедуры WinProc. Как только это окно зарегистрируется с помощью метода RegisterClass, Windows будет знать, какую процедуру использовать для обработки сообщений.

Как вы можете видеть, нет особое значение в имя WinProc. WinProc можно назвать MyWinProc или WinHandler. Фактическое имя SDK является WindowProc, но название может быть любым. Важно понимать только то, что обработчик сообщений должен иметь те же параметры, что мы определили в нашей WinProc, а также адрес этой процедуры должен быть сохранен в .lpfnWndProc.

Все сообщения, будь то генерируемые пользователем или системой, проходят через обработчик сообщений, который в нашем примере, является WinProc. Глядя на список параметров WinProc мы видим, что у нас есть большинство компонентов структуры сообщения,  извлекаемые с помощью GetMessage. HWND является хендлом окна, message является идентификатором сообщения , а WPARAM и LPARAM  дополнительной информацией к сообщению. После того, как сообщение было передано в WinProc, мы должны решить, заинтересованы ли мы в обработке этого сообщения.

Обработка обычно делается в избирательной форме, при рассмотрении параметра message. Например, если message будет равно WM_PAINT, то мы можем поставить наш код рисования под сообщением WM_PAINT так, чтобы наше окно обновлялось каждый раз, когда все окно или часть окна перерисовывается. Если мы не заботимся о сообщении, то мы просто игнорируем сообщение, передавая его процедуре DefWindowProc, которая поместит его в обработчик сообщений по умолчанию.

Действие здесь довольно простое. WinMain или Windows, посылает нам сообщения, а мы реагируем на эти сообщения, представляющие интерес. Когда сообщения поступают в очередь сообщений, они обрабатываются и отправляются в WinProc, где они могут быть дополнительно обработаны, а затем передается вместе с DefWindowProc для обработки операционной системой. Этот цикл продолжается в течение жизни программы, пока сообщение WM_QUIT не получено, и в этот момент окно разрушается и программа завершается.

В нашем примере программы, мы заинтересованы только в трех сообщениях: WM_LBUTTONUP, WM_PAINT и WM_KEYDOWN. Сообщения WM_CREATE и WM_DESTROY по сути шаблонные, и их вы найдете практически в любой оконной программе. Поскольку нас интересует только эти три сообщения, то нам нужно написать код для обработки этих трех сообщений. Остальные сообщения нас не касаются и мы даже не будем о них задумываться.

В событийном языке, вы пишете код для обработки события, которое произошло. Мы знаем, что событие произошло, потому что мы получили сообщение, описывающее событие. Если мы заинтересованы в нем, то мы пишем код, который связан с ним. Вместо того чтобы писать огромное количество кода, чтобы справиться с любым аспектом программы, как это делается в процедурном языке, нам нужно только написать код для определенных событий, и пусть операционная система обрабатывать все остальное.

Теперь, конечно, вы должны написать код для создания окна и элементов управления, но наш пример - это главным образом стереотипный тип кода. Вы просто должны следовать API и передавать соответствующие параметры функции CreateWindow. После того, как вы понимаете шаблон, вам просто подключить дополнительный код в вашу программу. Реальные действия происходят в функции WinProc, когда вы взаимодействуете с элементами управления или окном.

Программирование на основе сообщений требует привычки. При написании эффективных программ с графическим интерфейсом, программист должен уступить контроль над программой и работать в сотрудничестве с операционной системой и пользователем.

При разработке программы GUI, вы должны спросить себя: "Как я хочу, чтобы моя программа отвечала пользователю?" Например, когда приложение свернуто, программа должна игнорировать событие, или же она должна висеть в системном трее? В этом заключается суть событийного программирования. Определение того, что события имеют важное значение, а затем записать индивидуальные процедуры, которые обрабатывают каждое событие. Программа на основе сообщений - это просто набор конкретных подпрограмм, написанных в ответ на конкретные сообщения.

Несмотря на объем SDK, основная концепция программирования на основе сообщений довольно проста. Вы пишете коллекцию подпрограмм для обработки сообщений. Это основная задача. Все другие вещи, как создание окна, или обновление окна делается операционной системой. Это сфера применения SDK подходит для большинства людей. Лучший способ освоить SDK, это просто понять концепцию событийного программирования и узнать шаблонный код. Как только это будет сделано, создание изощренных программ Windows, окажется не таким уж и сложным.