Introduction to Message-Based Programming
 
Written by rdc

历史上,编程语言被分类为程序性和基于消息的。例如,QuickBasic可以被分类为过程语言,Visual Basic可以被分类为基于消息的(或事件驱动的)语言。在程序语言中,你通常从顶部开始,做一些事情,并以一些线性的方式退出。在基于消息的语言中,您通常会初始化系统,然后程序位于空闲循环中,并等待某些事情发生。当有事情发生时,您处理它,然后程序返回到空闲循环,最终在用户关闭程序时退出循环。

在程序语言中,您可以完全控制用户看到和执行的操作。在基于消息的系统中,您可以与操作系统和用户协作工作,仅处理您感兴趣的消息,并让操作系统处理其余信息。程序员从程序语言中获取基于消息的语言的真正绊脚石是仅响应消息的概念。但是,我们真的在谈论灰色阴影,而不是黑白色。在大多数语言中,包括程序性的信息在发挥重要作用。

如果您曾经使用支持子程序和函数调用的语言,那么您已经使用基于消息的编程。例如,假设您已经在QuickBasic中编写了一个游戏,该游戏位于循环中,等待其中一个箭头键被按下。如果按向上箭头键,您可以调用更新画面上精灵位置的子程序。如果按下A键,则忽略它,因为您不关心A键。这是基于消息的编程。消息是按键,精灵更新子程序是消息处理程序。

任何结构化编程语言都可以归类为基于消息的编程语言。基于消息的编程是一种概念,一种处理用户输入并对该输入做出反应的方法。它比一种语言更方法论。当操作系统从命令行演变成图形用户界面(GUI)时,它成为一些编程语言的主要特征。

在基于GUI的操作系统(如Windows)中,操作系统在内部管理所有图形元素。由于程序员没有从头构建文本编辑字段,所以他/她只是使用内置到图形外壳中的编辑字段,必须有一种方法来通知程序员用户想要更新编辑字段。最自然的方法是向程序发送一条消息,指示编辑字段已被更新。在Windows下,这种GUI元素的借用和消息的接收已经被形式化为所谓的Windows软件开发工具包,或者更常见的是Windows SDK。

Windows SDK是一组构成大多数操作系统的动态链接库(DLL)中的应用程序编程接口(API)的集合。在Windows下运行的任何基于GUI的程序都使用SDK,即使不明显。在快速应用程序开发(RAD)语言(如Delphi,Visual Basic或Real Basic)中,这些语言通过使用属性和事件来隐藏SDK的详细信息,但是它们正在使用SDK。

虽然RAD语言使程序员能够快速构建基于GUI的程序,但这也意味着SDK的更精细细节无法访问。例如,使用VB子类化控件是相当困难的,但使用SDK非常简单。然而,SDK是巨大的,API的剪切尺寸足以使许多程序员放弃SDK编程的想法。共同的想法是,它太复杂和难以使用,但相反的是。由于操作系统处理程序的所有图形元素,程序员可以专注于程序设计,用户交互的最重要方面。毕竟,GUI程序都是关于用户交互的。

FreeBasic没有用于Windows编程的RAD系统。要在FreeBasic中创建一个Windows程序,你必须使用SDK,因为这是唯一的选择。虽然SDK是巨大的,可能需要一辈子才能完全了解,对于99%的Windows程序,只需要使用一小部分SDK。实际上,Windows SDK编程并不比任何其他类型的编程更难,对于基于GUI的程序,实际上比您自己创建所有GUI元素的语言更容易。

暂时放下Windows API的所有细节,了解SDK程序中邮件的机制很重要。这是通过查看我们的老朋友Hello World节目来完成的。在FreeBasic .15b发行版的示例\\ Windows \\ gui文件夹中(这篇文章中的代码是必需的),有一个很好的Hello World程序,我要偷 - 我的意思是借用,作为这个的基础例。

Option Explicit
Option Private

#include once "windows.bi"

Declare Function        WinMain     ( ByVal hInstance As HINSTANCE, _
                                      ByVal hPrevInstance As HINSTANCE, _
                                      szCmdLine As String, _
                                      ByVal iCmdShow As Integer ) As Integer
                                  
                                  
    ''
    '' Entry point    
    ''
    End WinMain( GetModuleHandle( null ), null, Command$, SW_NORMAL )

'' ::::::::
'' name: WndProc
'' desc: Processes windows messages
''
'' ::::::::
Function WndProc ( ByVal hWnd As HWND, _
                   ByVal message As UINT, _
                   ByVal wParam As WPARAM, _
                   ByVal lParam As LPARAM ) As LRESULT
    
    Function = 0
    
    ''
    '' Process messages
    ''
    Select Case( message )
        ''
        '' Window was created
        ''        
        Case WM_CREATE            
            Exit Function
        
        '' User clicked the form
        Case WM_LBUTTONUP
            MessageBox NULL, "Hello world from FreeBasic", "FB Win", MB_OK
        ''
        '' Windows is being repainted
        ''
        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            
        
        ''
        '' Key pressed
        ''
        Case WM_KEYDOWN
            'Close if esc key pressed
            If( LoByte( wParam ) = 27 ) Then
                PostMessage( hWnd, WM_CLOSE, 0, 0 )
            End If

        ''
        '' Window was closed
        ''
        Case WM_DESTROY
            PostQuitMessage( 0 )
            Exit Function
    End Select
    
    ''
    '' Message doesn't concern us, send it to the default handler
    '' and get result
    ''
    Function = DefWindowProc( hWnd, message, wParam, lParam )    
    
End Function

'' ::::::::
'' name: WinMain
'' desc: A win2 gui program entry point
''
'' ::::::::
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
     
    ''
    '' Setup window class
    ''
    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
          
    ''
    '' Register the window class     
    ''     
    If( RegisterClass( @wcls ) = FALSE ) Then
       MessageBox( null, "Failed to register wcls!", szAppName, MB_ICONERROR )
       Exit Function
    End If
    
    ''
    '' Create the window and show it
    ''
    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 )
     
    ''
    '' Process windows messages
    ''
    While( GetMessage( @wMsg, NULL, 0, 0 ) <> FALSE )    
        TranslateMessage( @wMsg )
        DispatchMessage( @wMsg )
    Wend
    
    
    ''
    '' Program has ended
    ''
    Function = wMsg.wParam

End Function



如果您已成功编译并运行该程序,您将看到一个标准窗口,并在表单上打印出一条消息。如果您单击表单,将显示一个消息框,如果按退出键,程序将关闭。

花一点时间来检查窗户。您将看到该表单具有最大,最小和还原按钮,系统菜单并可以调整大小。现在看看上面的代码。处理所提到的窗口属性没有任何代码,操作系统会为您处理所有这些。它也只需要一行代码来显示消息框,它本身就是一个相当复杂的对象。结果与代码量的比例是非常显着的。如果要使用FreeBasic的标准图形命令尝试重新创建这个简单的程序,程序将会是一百倍。

上面列出的代码首先要注意的是格式。这是FreeBasic Windows程序的基本格式。每个Windows程序,无论多么简单或复杂,都将遵循相同的基本格式。这个程序的两个关键因素是WinMainWinProc程序。

WinMain过程是程序启动时Windows调用的过程。它是Windows程序的切入点。在WinMain中,您构建并注册主程序窗口,然后进入消息循环以处理消息。一旦程序进入消息循环,它将使用WinProc过程开始处理消息。由于本文是关于Windows程序中的消息模型,我们将仅讨论WinMain和WinProc过程中的消息循环。

当Windows操作系统运行时,始终会生成消息。当Windows程序运行时,操作系统将向程序发送消息,当操作系统认为程序应该知道的情况发生。这些程序特定消息中的一些直接发送到WinProc(或类似的)过程,而其他程序特定的消息主要由用户生成的消息发送到消息队列中。由于大多数程序涉及用户交互,因此了解消息队列很重要。

队列是一种数据结构,其中数据被添加到队列的“后退”并从“前端”中删除。这被称为先进先出或FIFO堆栈。如果你曾经站在一起买电影票,你就经历了一个排队。

对于程序,消息队列将保存一个或多个消息,像电影票证行中的人一样排列。Windows程序的空闲循环位于等待消息到达,然后将消息转换和分发到程序。此消息循环包含在WinMain的以下代码摘录中。

    ''
    '' Process windows messages
    ''
    While( GetMessage( @wMsg, NULL, 0, 0 ) <> FALSE )    
        TranslateMessage( @wMsg )
        DispatchMessage( @wMsg )
    Wend



GetMessage程序通过WMSG参数从队列中检索消息。wMsg参数是包含与特定消息相关的必要信息的MSG类型def。以下是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过程处理。

信息是消息标识符。这可能是例如WM_CREATE,它表示窗口已被创建,但尚未显示。

wParam中lParam的都基于消息类型指定附加信息。例如,当按下一个键时,可以使用wParam的lobyte来检索密钥代码。

时间指定消息发布的时间,pt是包含消息发布时光标位置的结构。

TranslateMessage将虚拟键消息转换为字符消息,然后将其放回队列,以便可以处理密钥。任何使用键盘的程序都需要这个程序。DispatchMethod然后向与的hWnd参数标识的窗口关联的Windows WinProc(或类似)过程发送一条消息。

为了总结这里的操作,用户生成的消息将被放置到消息队列的“返回”中。GetMessage检索第一个等待的消息,将其传递给TranslateMessage,如果需要,将其转换为消息,并将其放回队列。然后将消息传递给DispatchMessage,DispatchMessage检查消息以查看哪个窗口应该获取消息,然后将消息传递到Windows处理程序过程,在我们的示例中是WinProc。

在讨论WinProc程序之前,我们需要提出一个问题:程序中有多个窗口会发生什么? WinMain如何知道使用什么程序?答案包含在WNDCLASS结构中。在我们的示例中,wcls在WinMain过程中被定义为WNDCLASS。

    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是windows句柄,消息是消息id,wParam和lParam保存附加的消息信息。一旦一条消息传递给WinProc,我们就必须决定是否对该消息感兴趣。

这通常用select case来完成,我们检查消息参数以查看我们是否要处理消息。例如,如果消息是WM_PAINT,那么我们将把我们的绘图代码放在WM_PAINT消息下,以便每次重新绘制窗口的全部或部分时,我们的窗口将被更新。如果我们不在乎一个消息,那么我们只需将消息传递给DefWindowProc消息,由默认消息处理程序处理。

这里的行动是非常简单的。WinMain或Windows向我们发送消息,我们回应那些感兴趣的消息。当消息进入消息队列时,它们被处理并发送到WinProc,在那里可以进一步处理它们,然后传递给DefWindowProc以由操作系统处理。该循环在程序的生命周期中持续,直到接收到WM_QUIT消息,此时窗口被销毁并且程序被终止。

在我们的例子中,程序我们只关心三个消息WM_LBUTTONUP,WM_PAINT和WM_KEYDOWN。WM_CREATE和WM_DESTROY基本上是样板,你会发现任何Windows程序。由于我们只对这三条消息感兴趣,我们只需要为这三条消息编写代码。我们可能收到的其他消息不涉及我们,所以我们甚至不打扰他们。

在基于消息的语言中,您正在编写代码来处理已发生的事件。我们知道事件已经发生,因为我们收到了描述事件的消息。如果我们对该事件感兴趣,那么我们编写代码来响应它。而不是像程序语言那样编写大量的代码来处理程序的每个方面,我们只需要为某些事件编写代码,我们让操作系统处理所有其他事情。

现在当然,你必须编写代码来创建一个窗口和控件,但这主要是样板类型的代码。您只需要遵循API并将适当的参数传递给CreateWindow函数。一旦了解了样板,只需在需要时将代码插入程序即可。当您与窗口或控件交互时,WinProc功能中会发生真正的动作。

基于消息的编程需要与程序编程不同的思维方式。用程序语言,用户必须响应程序;程序员负责。在GUI程序中,程序必须响应用户,用户负责。要编写有效的GUI程序,程序员必须放弃对程序的控制,并与操作系统和用户合作。

当您设计GUI程序时,您必须问问自己:“我如何让程序对用户做出回应?”例如,当应用程序最小化时,程序是否忽略该事件,还是应该将其放在系统托盘中?这是基于消息的编程的本质。定义什么事件很重要,然后编写处理每个事件的各个例程。基于消息的程序只是响应特定消息编写的特定例程的集合。

尽管SDK的声誉,消息编程的基本概念非常简单。您正在编写一系列例程来处理消息。这是核心任务。所有其他的东西,如创建一个窗口,或什么时候重绘窗口都是由操作系统完成的。大多数人都可以使用SDK的范围。那里有很多东西但是,像cliché说,吃大象的最好方法是一口咬一口。掌握SDK的最好办法是简单了解基于消息的编程概念,并学习样板代码。一旦完成,创建复杂的Windows程序并不是那么难。