Jump to content
LarsJ

Implementing a Windows message loop in a non-AutoIt GUI

Recommended Posts

Posted (edited)

GUIs of interest here are non-AutoIt GUIs not created with GUICreate(). They are created with Windows API functions eg. _WinAPI_CreateWindowEx().

(2020-04-26) Is it possible in such a non-AutoIt GUI to implement a Windows message loop? Which functionality and techniques do and do not work in such non-AutoIt GUIs? Which internal functions can be used? Can AutoIt and non-AutoIt GUIs exist side by side in the same script? Can existing UDFs be used?

This example started last week with inspiration from this thread. After several tests during the week, I've decided to make a little more of the example. This is an update of first post. Over the coming weeks I'll be adding more code in new posts. If it all works well and stable, the ideas in the project may be seen in a wider perspective.

Some information has been deleted during this update. The information will be added again later.
 

Basic functionality
 

What is a Windows message loop
In Microsoft documentation, a message loop is usually coded like this:

while GetMessage(&msg, 0, 0, 0) { // Retrieve a message from the message queue
  // Processes accelerator keystrokes
  if (!TranslateAccelerator(
        hwndMain,      // handle to receiving window
        haccel,        // handle to active accelerator table
        &msg))         // message data
  {
    TranslateMessage(&msg); // Translate a virtual-key message into a character message
    DispatchMessage(&msg);  // Send a message to the window procedure
  }
}

And the window procedure is defined like this:

LRESULT CALLBACK WinProc(
  HWND hwnd,        // handle to window
  UINT uMsg,        // message identifier
  WPARAM wParam,    // first message parameter
  LPARAM lParam)    // second message parameter
{
  switch (uMsg) {
    case WM_CREATE:
      // Initialize the window.
      return 0;

    case WM_PAINT:
      // Paint the window's client area.
      return 0;

    case WM_SIZE:
      // Set the size and position of the window.
      return 0;

    case WM_DESTROY:
      // Clean up window-specific data objects.
      return 0;

    //
    // Process other messages.
    //

    default:
      return DefWindowProc(hwnd, uMsg, wParam, lParam);
  }
  return 0;
}

Message loop
The purpose of a message loop is first and foremost to keep the program running and prevent it from ending immediately. It's exactly the same in AutoIt. But why so complicated code as shown above? It's necessary for advanced message handling eg. to use keyboard accelerators that are local to the individual program and not global as hot keys.

Window procedure
The window procedure handles the messages sent from the DispatchMessage() function in the message loop. Even in a simpler message loop, messages are sent to the window procedure. This is done through deault code in the Windows API functions and the operating system. If a message isn't handled by the window procedure, it's forwarded to the default window procedure DefWindowProc().
 

0) _WinAPI_RegisterClassEx.au3
This is the example of _WinAPI_RegisterClassEx() in the help file. _WinAPI_CreateWindowEx() is used to create the GUI window. The example here is a slightly modified version:

#AutoIt3Wrapper_Au3Check_Parameters=-d -w 1 -w 2 -w 3 -w 4 -w 5 -w 6 -w 7

#AutoIt3Wrapper_UseX64=y

Opt( "MustDeclareVars", 1 )

#include <WinAPIRes.au3>
#include <WinAPISys.au3>
#include <WindowsConstants.au3>

Global $bExit = False

Example()

Func Example()
  Local Const $sClass = "MyWindowClass"
  Local Const $sName = "_WinAPI_RegisterClassEx"

  ; Get module handle for the current process
  Local $hInstance = _WinAPI_GetModuleHandle( 0 )

  ; Create a class cursor
  Local $hCursor = _WinAPI_LoadCursor( 0, 32512 ) ; IDC_ARROW

  ; Create a class icons (large and small)
  Local $tIcons = DllStructCreate( "ptr;ptr" )
  _WinAPI_ExtractIconEx( @SystemDir & "\shell32.dll", 130, DllStructGetPtr( $tIcons, 1 ), DllStructGetPtr( $tIcons, 2 ), 1 )
  Local $hIcon = DllStructGetData( $tIcons, 1 )
  Local $hIconSm = DllStructGetData( $tIcons, 2 )

  ; Create DLL callback function (window procedure)
  Local $pWinProc = DllCallbackGetPtr( DllCallbackRegister( "WinProc", "lresult", "hwnd;uint;wparam;lparam" ) )

  ; Create and fill $tagWNDCLASSEX structure
  Local $tWCEX = DllStructCreate( $tagWNDCLASSEX & ";wchar szClassName[" & ( StringLen( $sClass ) + 1 ) & "]" )
  DllStructSetData( $tWCEX, "Size", DllStructGetPtr( $tWCEX, "szClassName" ) - DllStructGetPtr( $tWCEX ) )
  DllStructSetData( $tWCEX, "Style", 0 )
  DllStructSetData( $tWCEX, "hWndProc", $pWinProc )
  DllStructSetData( $tWCEX, "ClsExtra", 0 )
  DllStructSetData( $tWCEX, "WndExtra", 0 )
  DllStructSetData( $tWCEX, "hInstance", $hInstance )
  DllStructSetData( $tWCEX, "hIcon", $hIcon )
  DllStructSetData( $tWCEX, "hCursor", $hCursor )
  DllStructSetData( $tWCEX, "hBackground", _WinAPI_CreateSolidBrush( _WinAPI_GetSysColor( $COLOR_3DFACE ) ) )
  DllStructSetData( $tWCEX, "MenuName", 0 )
  DllStructSetData( $tWCEX, "ClassName", DllStructGetPtr( $tWCEX, "szClassName" ) )
  DllStructSetData( $tWCEX, "hIconSm", $hIconSm )
  DllStructSetData( $tWCEX, "szClassName", $sClass )

  ; Register a window class
  _WinAPI_RegisterClassEx( $tWCEX )

  ; Create a window
  _WinAPI_CreateWindowEx( 0, $sClass, $sName, BitOR( $WS_CAPTION, $WS_POPUPWINDOW, $WS_VISIBLE ), ( @DesktopWidth - 826 ) / 2, ( @DesktopHeight - 584 ) / 2, 826, 584, 0 )

  ; Main msg loop
  While Sleep(10)
    If $bExit Then ExitLoop
  WEnd

  ; Unregister window class and release resources
  _WinAPI_UnregisterClass( $sClass, $hInstance )
  _WinAPI_DestroyCursor( $hCursor )
  _WinAPI_DestroyIcon( $hIcon )
  _WinAPI_DestroyIcon( $hIconSm )
EndFunc

; Window procedure
Func WinProc( $hWnd, $iMsg, $wParam, $lParam )
  Switch $iMsg
    Case $WM_CLOSE
      $bExit = True
  EndSwitch
  Return _WinAPI_DefWindowProcW( $hWnd, $iMsg, $wParam, $lParam )
EndFunc

Note that the Esc key, which can normally be used to close an AutoIt window, doesn't work. But you can use Alt+F4 to close the window. Alt+F4 is a hot key.

In the example, the main message loop is coded this way:

; Main msg loop
While Sleep(10)
  If $bExit Then ExitLoop
WEnd

And the window procedure that handles messages:

Func WinProc( $hWnd, $iMsg, $wParam, $lParam )
  Switch $iMsg
    Case $WM_CLOSE
      $bExit = True
  EndSwitch
  Return _WinAPI_DefWindowProcW( $hWnd, $iMsg, $wParam, $lParam )
EndFunc

Esc key
In AutoIt, the Esc key to exit the program is implemented as an accelerator key. But this simple message loop is unable to handle keyboard accelerators.
 

Keyboard accelerators
The Microsoft documentation for keyboard accelerators can be found here. To use keyboard accelerators in a program, the following code steps must be implemented:

  • Create a keyboard accelerator struct to store information
  • Fill in the accelerator key structure with information
  • Create an accelerator table with CreateAcceleratorTable()
  • Include TranslateAccelerator() in the message loop
  • Include a WM_COMMAND message handler in the window procedure
     

Includes\WinMsgLoop.au3  (2020-04-26)
WinMsgLoop.au3 implements the functions to use keyboard accelerators and to create a Windows message loop:

#include-once

; Message structure
Global Const $tagMSG = "hwnd hwnd;uint message;wparam wParam;lparam lParam;dword time;int X;int Y"

; Keyboard accelerator structure
Global Const $tagACCEL = "byte fVirt;word key;word cmd;"
; Values of the fVirt field
Global Const $FVIRTKEY  = TRUE
Global Const $FNOINVERT = 0x02
Global Const $FSHIFT    = 0x04
Global Const $FCONTROL  = 0x08
Global Const $FALT      = 0x10

#cs
; One accelerator key
Local $tAccel = DllStructCreate( $tagACCEL )
DllStructSetData( $tAccel, "fVirt", $FVIRTKEY )
DllStructSetData( $tAccel, "key", $VK_ESCAPE )
DllStructSetData( $tAccel, "cmd", $VK_ESCAPE ) ; cmd = key to keep it simple

; Two accelerator keys
Local $tAccel = DllStructCreate( $tagACCEL & $tagACCEL )
DllStructSetData( $tAccel, 1, $FVIRTKEY )
DllStructSetData( $tAccel, 2, $VK_KEY1 )
DllStructSetData( $tAccel, 3, $VK_KEY1 )
DllStructSetData( $tAccel, 4, $FVIRTKEY )
DllStructSetData( $tAccel, 5, $VK_KEY2 )
DllStructSetData( $tAccel, 6, $VK_KEY2 )
#ce

; Error handling
;   @error =   0: No errors
;              1: Parameter error

; Create a keyboard accelerator table
Func WinMsgLoop_CreateAcceleratorTable( $tAccel )
  Local $iSize = DllStructGetSize( $tAccel )
  If Mod( $iSize, 6 ) Then Return SetError(1,0,0) ; SetError ( code [, extended = 0 [, return value]] )
  Return DllCall( "User32.dll", "handle", "CreateAcceleratorTableW", "struct*", $tAccel, "int", $iSize/6 )[0]
EndFunc

; Retrieve a message from the message queue
Func WinMsgLoop_GetMessage( ByRef $tMsg )
  Return DllCall( "User32.dll", "bool", "GetMessageW", "struct*", $tMsg, "hwnd", 0, "uint", 0, "uint", 0 )[0]
EndFunc

; Processes accelerator keystrokes
Func WinMsgLoop_TranslateAccelerator( $hWnd, $hAccel, ByRef $tMsg )
  Return DllCall( "User32.dll", "int", "TranslateAcceleratorW", "hwnd", $hWnd, "handle", $hAccel, "struct*", $tMsg )[0]
EndFunc

; Processes dialog box messages
Func WinMsgLoop_IsDialogMessage( $hWnd, ByRef $tMsg )
  Return DllCall( "User32.dll", "bool", "IsDialogMessageW", "hwnd", $hWnd, "struct*", $tMsg )[0]
EndFunc

; Translate a virtual-key message into a character message
Func WinMsgLoop_TranslateMessage( ByRef $tMsg )
  DllCall( "User32.dll", "bool", "TranslateMessage", "struct*", $tMsg )
EndFunc

; Send a message to the window procedure
Func WinMsgLoop_DispatchMessage( ByRef $tMsg )
  DllCall( "User32.dll", "lresult", "DispatchMessageW", "struct*", $tMsg )
EndFunc

; Destroy a keyboard accelerator table
Func WinMsgLoop_DestroyAcceleratorTable( $hAccel )
  DllCall( "User32.dll", "bool", "DestroyAcceleratorTable", "handle", $hAccel )
EndFunc

; Posts a WM_QUIT message to the message queue
; WinMsgLoop_GetMessage() returns 0 on WM_QUIT and the message loop terminates
Func WinMsgLoop_PostQuitMessage( $iExitCode = 0 )
  Return DllCall( "User32.dll", "none", "PostQuitMessage", "int", $iExitCode )[0]
EndFunc

 

1) Windows message loop.au3
In this example, the Esc and End keys can be used to exit the program. It contains a complete Windows message loop. The script contains a number of ConsoleWrites so you can see what's going on in SciTE console.

Keyboard accelerators:

#cs
; Esc accelerator key to Exit
; Create keyboard accelerator structure
Local $tAccel = DllStructCreate( $tagACCEL )
DllStructSetData( $tAccel, "fVirt", $FVIRTKEY )
DllStructSetData( $tAccel, "key", $VK_ESCAPE )
DllStructSetData( $tAccel, "cmd", $VK_ESCAPE ) ; cmd = key to keep it simple
#ce
; Esc/End accelerator keys to Exit
; Create keyboard accelerator structure
Local $tAccel = DllStructCreate( $tagACCEL & $tagACCEL )
DllStructSetData( $tAccel, 1, $FVIRTKEY )
DllStructSetData( $tAccel, 2, $VK_ESCAPE )
DllStructSetData( $tAccel, 3, $VK_ESCAPE ) ; cmd = key to keep it simple
DllStructSetData( $tAccel, 4, $FVIRTKEY )
DllStructSetData( $tAccel, 5, $VK_END )
DllStructSetData( $tAccel, 6, $VK_END )
; Create a keyboard accelerator table
Local $hAccel = WinMsgLoop_CreateAcceleratorTable( $tAccel )
ConsoleWrite( "$hAccel = " & $hAccel & @CRLF & @CRLF )

Using dialog box keys (2020-04-26)
To use dialog box keys, the message loop must contain the IsDialogMessage() function. The function identifies and processes the keys.

Windows message loop: (2020-04-26)

; Windows message loop
Local $tMsg = DllStructCreate( $tagMSG )
While WinMsgLoop_GetMessage( $tMsg ) ; Retrieve a message from the message queue
  ConsoleWrite( "0x" & Hex( DllStructGetData( $tMsg, "message" ), 4 ) & @CRLF )
  If Not WinMsgLoop_TranslateAccelerator( $hWnd, $hAccel, $tMsg ) And _ ; Processes accelerator keystrokes
     Not WinMsgLoop_IsDialogMessage( $hWnd, $tMsg ) Then                ; Processes dialog box messages
         WinMsgLoop_TranslateMessage( $tMsg ) ; Translate a virtual-key message into a character message
         WinMsgLoop_DispatchMessage( $tMsg )  ; Send a message to the window procedure
  EndIf
  If $bExit Then ExitLoop
WEnd

WM_COMMAND message handler: (2020-04-26)

Case $WM_COMMAND
  ConsoleWrite( @CRLF & "$WM_COMMAND" & @CRLF )
  Switch BitShift( $wParam, 16 ) ; HiWord
    Case 1 ; Accelerator key
      ConsoleWrite( "Accelerator key" & @CRLF )
      Switch BitAND( $wParam, 0xFFFF ) ; LoWord
        Case $VK_ESCAPE
          ConsoleWrite( "$VK_ESCAPE" & @CRLF )
          _WinAPI_DestroyWindow( $hWnd )
          Return 0 ; Don't call DefWindowProc()
        Case $VK_END
          ConsoleWrite( "$VK_END" & @CRLF )
          _WinAPI_DestroyWindow( $hWnd )
          Return 0
      EndSwitch
  EndSwitch

Program termination code: (2020-04-26)

Case $WM_CLOSE
  ConsoleWrite( @CRLF & "$WM_CLOSE" & @CRLF )
  If MsgBox( $MB_OKCANCEL, "Really close?", "My application", 0, $hWnd ) = $IDOK Then
    _WinAPI_DestroyWindow( $hWnd )
  Else
    ConsoleWrite( @CRLF )
  EndIf
  Return 0 ; Don't call DefWindowProc()

Case $WM_DESTROY
  ConsoleWrite( @CRLF & "$WM_DESTROY" & @CRLF )
  $bExit = True
  Return 0

 

2) Windows message loop 2.au3 (2020-04-26)
Optimized version of the message loop because DllCall() is used directly instead of more time-consuming functions in WinMsgLoop.au3 UDF:

; Windows message loop
Local $tMsg = DllStructCreate( $tagMSG )
While DllCall( "User32.dll", "bool", "GetMessageW", "struct*", $tMsg, "hwnd", 0, "uint", 0, "uint", 0 )[0] ; Retrieve a message from the message queue
  ConsoleWrite( "0x" & Hex( DllStructGetData( $tMsg, "message" ), 4 ) & @CRLF )
  If Not DllCall( "User32.dll", "int", "TranslateAcceleratorW", "hwnd", $hWnd, "handle", $hAccel, "struct*", $tMsg )[0] And _ ; Processes accelerator keystrokes
     Not DllCall( "User32.dll", "bool", "IsDialogMessageW", "hwnd", $hWnd, "struct*", $tMsg )[0] Then                         ; Processes dialog box messages
         DllCall( "User32.dll", "bool", "TranslateMessage", "struct*", $tMsg )    ; Translate a virtual-key message into a character message
         DllCall( "User32.dll", "lresult", "DispatchMessageW", "struct*", $tMsg ) ; Send a message to the window procedure
  EndIf
  If $bExit Then ExitLoop
WEnd

 

3) Navigating with Tab key.au3 (2020-04-26)
When IsDialogMessage() is added to the code in the message loop, dialog box keys work immediately. Eg. Tab and Shift+Tab. Demonstrated in the example with three buttons.
 

4) Adding virtual ListView.au3
A virtual listview with cell background colors is created in the window. As it's a virtual list view, a WM_NOTIFY message handler is needed to handle LVN_GETDISPINFO notifications. Background colors are drawn through NM_CUSTOMDRAW notifications. A virtual and custom drawn listview is very message intensive and therefore interesting to test.

; Create ListView
$hListView = _GUICtrlListView_Create( $hWnd, "", 10, 10, 800, 538, $LVS_DEFAULT+$LVS_OWNERDATA-$LVS_SINGLESEL, $WS_EX_CLIENTEDGE )
_GUICtrlListView_SetExtendedListViewStyle( $hListView, $LVS_EX_DOUBLEBUFFER+$LVS_EX_FULLROWSELECT )

; Add columns
For $i = 0 To $iCols - 1
  _GUICtrlListView_AddColumn( $hListView, "Col " & $i, 96, 2 ) ; 2 = Centered text
Next

; ListView items
For $i = 0 To $iRows - 1
  For $j = 0 To $iCols - 1
    $aItems[$i][$j] = $i & "/" & $j
  Next
Next

; ListView colors
Local $aLVColors = [ 0xCCCCFF, 0xCCFFFF, 0xCCFFCC, 0xFFFFCC, 0xFFCCCC, 0xFFCCFF ] ; BGR
For $i = 0 To $iRows - 1
  For $j = 0 To $iCols - 1
    $aColors[$i][$j] = $aLVColors[Random( 0,5,1 )]
  Next
Next

; Set number of rows in virtual ListView
DllCall( "user32.dll", "lresult", "SendMessageW", "hwnd", $hListView, "uint", $LVM_SETITEMCOUNT, "wparam", $iRows, "lparam", 0 )
Case $WM_NOTIFY
  Switch DllStructGetData( DllStructCreate( $tagNMHDR, $lParam ), "Code" )
    Case $LVN_GETDISPINFOW
      ; Fill virtual listview
      Local Static $tText = DllStructCreate( "wchar[100]" ), $pText = DllStructGetPtr( $tText )
      Local $tNMLVDISPINFO = DllStructCreate( $tagNMLVDISPINFO, $lParam )
      If Not BitAND( DllStructGetData( $tNMLVDISPINFO, "Mask" ), $LVIF_TEXT ) Then Return
      Local $sItem = $aItems[DllStructGetData($tNMLVDISPINFO,"Item")][DllStructGetData($tNMLVDISPINFO,"SubItem")]
      DllStructSetData( $tText, 1, $sItem )
      DllStructSetData( $tNMLVDISPINFO, "TextMax", StringLen( $sItem ) )
      DllStructSetData( $tNMLVDISPINFO, "Text", $pText )
      Return 0 ; Don't call DefWindowProc()

    Case $NM_CUSTOMDRAW
      ; Draw back colors
      Local $tNMLVCUSTOMDRAW = DllStructCreate( $tagNMLVCUSTOMDRAW, $lParam )
      Local $dwDrawStage = DllStructGetData( $tNMLVCUSTOMDRAW, "dwDrawStage" ), $iItem
      Switch $dwDrawStage                        ; Holds a value that specifies the drawing stage
        Case $CDDS_PREPAINT                      ; Before the paint cycle begins
          Return $CDRF_NOTIFYITEMDRAW            ; Notify the parent window of any item-related drawing operations
        Case $CDDS_ITEMPREPAINT                  ; Before painting an item
          Return $CDRF_NOTIFYSUBITEMDRAW         ; Notify the parent window of any subitem-related drawing operations
        Case $CDDS_ITEMPREPAINT + $CDDS_SUBITEM  ; Before painting a subitem
          $iItem = DllStructGetData( $tNMLVCUSTOMDRAW, "dwItemSpec" )
          DllStructSetData( $tNMLVCUSTOMDRAW, "ClrTextBk", $aColors[$iItem][DllStructGetData($tNMLVCUSTOMDRAW,"iSubItem")] )
          Return $CDRF_NEWFONT                   ; $CDRF_NEWFONT must be returned after changing font or colors
      EndSwitch
  EndSwitch

 

Usage of the code
I don't think there's such a great need for code like this. At least the code shows that translating Windows API functions into AutoIt works pretty well. Implementing the code in the WinMsgLoop.au3 UDF was straightforward.

There is a question about using keyboard accelerators in non-AutoIt GUIs in this post.

If you use AutoIt for prototyping, you might get some ideas here. Because the examples do not contain internal AutoIt functions but only Windows API functions, they are easy to translate into C/C++ and other languages. When translating back and forth between AutoIt and other languages, the internal functions are always the problem. How to translate these functions?

Recent performance issue
Recently, a problem was raised in this post regarding a multiple 100% performance degradation on Windows 10 versions later than 1803 as soon as a GUI element became visible in a script. The problem persisted throughout the rest of the code, even though the GUI element was deleted. I've tested the issue under Windows 10 1809 with all the examples here. All of the examples suffer from the problem. How much of the code needs to be translated into C/C++ before the problem disappears? Only the line which contains the _WinAPI_CreateWindowEx() function that creates and displays the window? Or does the problem still exist when all code is translated? That could be interesting. I've already done examples of translating AutoIt code into C/C++ eg. in this example and this example so it's not that hard.
 

7z-file
The 7z-file contains source code for the UDFs and examples.

You need AutoIt 3.3.12 or later. Tested on Windows 7 and Windows 10.

Comments are welcome. Let me know if there are any issues.

WinMsgLoop.7z

Edited by LarsJ
Updates and new 7z-file

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

  • Recently Browsing   0 members

    No registered users viewing this page.

×
×
  • Create New...