A Developer's Life

This VB.Net version of the AutoWaitCursor class was converted from the C# version posted at: vbusers.com

I've added one overload to the Start method to take a MainWindowHandle so you can implement the class with a single method call (assuming the other default values work for your situation).

#Region " Imports..."

Imports System
Imports System.Diagnostics
Imports System.Runtime.InteropServices
Imports System.Threading
Imports System.Windows.Forms

#End Region

''' <summary>
''' This static utility class can be used to automatically show a wait cursor when the application 
''' is busy (ie not responding to user input). The class automatically monitors the application 
''' state, removing the need for manually changing the cursor.
''' </summary>
''' <example>
''' To use, simply insert the following line in your Application startup code ''' 
''' private void Form1_Load(object sender, System.EventArgs e)
''' {
'''   AutoWaitCursor.Cursor = Cursors.WaitCursor;
'''   AutoWaitCursor.Delay = new TimeSpan(0, 0, 0, 0, 25);
'''   // Set the window handle to the handle of the main form in your application 
'''   AutoWaitCursor.MainWindowHandle = this.Handle;
'''   AutoWaitCursor.Start();
''' }
''' 
''' This installs changes to cursor after 100ms of blocking work (ie. work carried out on the main application thread).
''' 
''' Note, the above code GLOBALLY replaces the following:
''' 
''' public void DoWork()
''' {
'''   try
'''   {
'''     Screen.Cursor = Cursors.Wait;
'''     GetResultsFromDatabase();
'''   }
'''     finally
'''   {
'''     Screen.Cursor = Cursors.Default;
'''   }
''' }
''' </example>
''' <remarks>
''' This class was initially converted from a C# example provided by Andrew Baker at the following 
''' URL:http://www.vbusers.com/codecsharp/codeget.asp?ThreadID=58&amp;PostID=1&amp;NumReplies=0
''' </remarks>
<DebuggerStepThrough()> _
    Public Class AutoWaitCursor

#Region " Private Attributes..."

    ''' <summary>
    ''' The default TimeSpan to wait before showing the auto wait cursor.
    ''' </summary>
    Private Shared DEFAULT_DELAY As New TimeSpan(0, 0, 0, 0, 25)

    ''' <summary>
    ''' The application state monitor class (which monitors the application busy status).
    ''' </summary>
    Private Shared _appStateMonitor As New ApplicationStateMonitor(Cursors.WaitCursor, DEFAULT_DELAY
                        )

#End Region

#Region " Constructors..."

    ''' <summary>
    ''' Initializes a new instance of the <see cref="T:AutoWaitCursor" /> class.
    ''' </summary>
    ''' <remarks>Intentionally hidden.</remarks>
    Private Sub New()
        'Do nothing
    End Sub

#End Region

#Region " Public Static Properties..."

    ''' <summary>
    ''' Returns the amount of time the application has been idle.
    ''' </summary>
    Public ReadOnly Property ApplicationIdleTime() As TimeSpan
        Get
            Return _appStateMonitor.ApplicationIdleTime
        End Get
    End Property

    ''' <summary>
    ''' Returns true if the auto wait cursor has been started.
    ''' </summary>
    Public Shared ReadOnly Property IsStarted() As Boolean
        Get
            Return _appStateMonitor.IsStarted
        End Get
    End Property

    ''' <summary>
    ''' Gets or sets the Cursor to use during Application busy periods.
    ''' </summary>
    Public Shared Property Cursor() As Cursor
        Get
            Return _appStateMonitor.Cursor
        End Get
        Set(ByVal value As Cursor)
            _appStateMonitor.Cursor = value
        End Set
    End Property

    ''' <summary>
    ''' Enables or disables the auto wait cursor.
    ''' </summary>
    Public Shared Property Enabled() As Boolean
        Get
            Return _appStateMonitor.Enabled
        End Get
        Set(ByVal value As Boolean)
            _appStateMonitor.Enabled = value
        End Set
    End Property

    ''' <summary>
    ''' Gets or sets the period of Time to wait before showing the WaitCursor whilst Application is working
    ''' </summary>
    Public Shared Property Delay() As TimeSpan
        Get
            Return _appStateMonitor.Delay
        End Get
        Set(ByVal value As TimeSpan)
            _appStateMonitor.Delay = value
        End Set
    End Property

    ''' <summary>
    ''' Gets or sets the main window handle of the application (ie the handle of an MDI form).
    ''' This is the window handle monitored to detect when the application becomes busy.
    ''' </summary>
    Public Shared Property MainWindowHandle() As IntPtr
        Get
            Return _appStateMonitor.MainWindowHandle
        End Get
        Set(ByVal value As IntPtr)
            _appStateMonitor.MainWindowHandle = value
        End Set
    End Property

#End Region

#Region " Public Methods..."

    ''' <summary>
    ''' Starts the auto wait cursor monitoring the application.
    ''' </summary>
    Public Shared Sub Start()
        AutoWaitCursor.Start(AutoWaitCursor.MainWindowHandle)
    End Sub

    ''' <summary>
    ''' Starts the auto wait cursor monitoring the application.
    ''' </summary>
    ''' <param name="mainWindowHandle">Specifies the main window handle of the application 
    ''' (ie the handle of an MDI form).</param>
    Public Shared Sub Start(ByVal mainWindowHandle As IntPtr)
        AutoWaitCursor.MainWindowHandle = mainWindowHandle

        _appStateMonitor.Start()
    End Sub

    ''' <summary>
    ''' Stops the auto wait cursor monitoring the application.
    ''' </summary>
    Public Shared Sub [Stop]()
        _appStateMonitor.Stop()
    End Sub

#End Region

#Region " Private Class ApplicationStateMonitor..."

    ''' <summary>
    ''' Private class that monitors the state of the application and automatically
    ''' changes the cursor accordingly.
    ''' </summary>
    Private Class ApplicationStateMonitor : Implements IDisposable

#Region " Private Attributes..."

        ''' <summary>
        ''' The time the application became inactive.
        ''' </summary>
        Private _inactiveStart As DateTime = DateTime.Now

        ''' <summary>
        ''' If the monitor has been started.
        ''' </summary>
        Private _isStarted As Boolean = False

        ''' <summary>
        ''' Delay to wait before calling back
        ''' </summary>
        Private _delay As TimeSpan

        ''' <summary>
        ''' The windows handle to the main process window.
        ''' </summary>
        Private _mainWindowHandle As IntPtr = IntPtr.Zero

        ''' <summary>
        ''' Thread to perform the wait and callback
        ''' </summary>
        Private _callbackThread As Thread = Nothing

        ''' <summary>
        ''' Stores if the class has been disposed of.
        ''' </summary>
        Private _isDisposed As Boolean = False

        ''' <summary>
        ''' Stores if the class is enabled or not.
        ''' </summary>
        Private _enabled As Boolean = True

        ''' <summary>
        ''' GUI Thread Id .
        ''' </summary>
        Private _mainThreadId As System.UInt32 'ToDo: Unsigned Integers not supported

        ''' <summary>
        ''' Callback Thread Id.
        ''' </summary>
        Private _callbackThreadId As System.UInt32 'ToDo: Unsigned Integers not supported

        ''' <summary>
        ''' Stores the old cursor.
        ''' </summary>
        Private _oldCursor As Cursor

        ''' <summary>
        ''' Stores the new cursor.
        ''' </summary>
        Private _waitCursor As Cursor

#End Region

#Region " Constants..."

        Private Const SMTO_NORMAL As Integer = &H0
        Private Const SMTO_BLOCK As Integer = &H1
        Private Const SMTO_NOTIMEOUTIFNOTHUNG As Integer = &H8

#End Region

#Region " API (PInvoke) Declarations..."

        <DllImport("user32.dll", CharSet:=CharSet.Auto, SetLastError:=True)> _
        Private Shared Function SendMessageTimeout(ByVal hWnd As IntPtr, ByVal Msg As Integer, ByVal wParam As Integer, ByVal lParam As String, ByVal fuFlags As Integer, ByVal uTimeout As Integer, ByRef lpdwResult As Integer) As <MarshalAs(UnmanagedType.Bool)> Boolean
        End Function

        <DllImport("USER32.DLL")> _
        Private Shared Function AttachThreadInput(ByVal attachTo As System.UInt32, ByVal attachFrom As System.UInt32, ByVal attach As Boolean) As System.UInt32
        End Function

        <DllImport("KERNEL32.DLL")> _
        Private Shared Function GetCurrentThreadId() As System.UInt32 'ToDo: Unsigned Integers not supported
        End Function

#End Region

#Region " Constructors..."

        ''' <summary>
        ''' Default member initialising Constructor.
        ''' </summary>
        ''' <param name="waitCursor">The wait cursor to use.</param>
        ''' <param name="delay">The delay before setting the cursor to the wait cursor.</param>
        Public Sub New(ByVal waitCursor As Cursor, ByVal delay As TimeSpan)
            'Constructor is called from (what is treated as) the main thread
            _mainThreadId = GetCurrentThreadId()
            _delay = delay
            _waitCursor = waitCursor

            'Gracefully shuts down the state monitor
            AddHandler Application.ThreadExit, AddressOf OnApplicationThreadExit
        End Sub 'New

#End Region

#Region " IDisposable Interface Methods..."

        ''' <summary>
        ''' On Disposal terminates the Thread, calls Finish (on thread) if Start has been called
        ''' </summary>
        Public Sub Dispose() Implements System.IDisposable.Dispose
            If (_isDisposed) Then
                Return
            End If

            'Kills the Thread loop
            _isDisposed = True
        End Sub 'Dispose

#End Region

#Region " Public Properties..."

        ''' <summary>
        ''' Returns true if the auto wait cursor has been started.
        ''' </summary>
        Public ReadOnly Property IsStarted() As Boolean
            Get
                Return _isStarted
            End Get
        End Property

        ''' <summary>
        ''' Gets or sets the main window handle of the application (ie the handle of an MDI form).
        ''' This is the window handle monitored to detect when the application becomes busy.
        ''' </summary>
        Public Property MainWindowHandle() As IntPtr
            Get
                Return _mainWindowHandle
            End Get
            Set(ByVal value As IntPtr)
                _mainWindowHandle = value
            End Set
        End Property


        ''' <summary>
        ''' Gets or sets the Cursor to show.
        ''' </summary>
        Public Property Cursor() As Cursor
            Get
                Return _waitCursor
            End Get
            Set(ByVal value As Cursor)
                _waitCursor = value
            End Set
        End Property

        ''' <summary>
        ''' Returns the amount of time the application has been idle.
        ''' </summary>
        Public ReadOnly Property ApplicationIdleTime() As TimeSpan
            Get
                Return DateTime.Now.Subtract(_inactiveStart)
            End Get
        End Property

#End Region

#Region " Public Methods..."

        ''' <summary>
        ''' Starts the application state monitor.
        ''' </summary>
        Public Sub Start()
            If (Not _isStarted) Then
                _isStarted = True

                Me.CreateMonitorThread()
            End If
        End Sub

        ''' <summary>
        ''' Stops the application state monitor.
        ''' </summary>
        Public Sub [Stop]()
            If (_isStarted) Then
                _isStarted = False
            End If
        End Sub

        ''' <summary>
        ''' Set the Cursor to wait.
        ''' </summary>
        Public Sub SetWaitCursor()
            'Start is called in a new Thread, grab the new
                Thread Id so we can attach to Main thread's input
            _callbackThreadId = GetCurrentThreadId()

            'Have to call this before calling Cursor.Current
            AttachThreadInput(_callbackThreadId, _mainThreadId, True)

            _oldCursor = Windows.Forms.Cursor.Current
            Windows.Forms.Cursor.Current = _waitCursor
        End Sub

        ''' <summary>
        ''' Finish showing the Cursor (switch back to previous Cursor).
        ''' </summary>
        Public Sub RestoreCursor()
            'Restore the cursor
            Windows.Forms.Cursor.Current = _oldCursor

            'Detach from Main thread input
            AttachThreadInput(_callbackThreadId, _mainThreadId, False)
        End Sub

        ''' <summary>
        ''' Enable/Disable the call to Start (note, once Start is called it *always* calls the paired Finish).
        ''' </summary>
        Public Property Enabled() As Boolean
            Get
                Return _enabled
            End Get
            Set(ByVal value As Boolean)
                _enabled = value
            End Set
        End Property

        ''' <summary>
        ''' Gets or sets the period of Time to wait before calling the Start method.
        ''' </summary>
        Public Property Delay() As TimeSpan
            Get
                Return _delay
            End Get
            Set(ByVal value As TimeSpan)
                _delay = value
            End Set
        End Property

#End Region

#Region " Private Methods..."

        ''' <summary>
        ''' Prepares the class creating a Thread that monitors the main application state.
        ''' </summary>
        Private Sub CreateMonitorThread()
            'Create the monitor thread
            _callbackThread = New Thread(New ThreadStart(AddressOf ThreadCallbackLoop))
            _callbackThread.Name = "AutoWaitCursorCallback"
            _callbackThread.IsBackground = True

            'Start the thread
            _callbackThread.Start()
        End Sub

        ''' <summary>
        ''' Thread callback method. 
        ''' Loops calling SetWaitCursor and RestoreCursor until Disposed.
        ''' </summary>
        Private Sub ThreadCallbackLoop()
            Try
                Do
                    If Not _enabled OrElse _mainWindowHandle = IntPtr.Zero Then
                        'Just sleep
                        Thread.Sleep(_delay)
                    Else
                        'Wait for start
                        If IsApplicationBusy(_delay, _mainWindowHandle) Then
                            Try
                                Me.SetWaitCursor()
                                WaitForIdle()
                            Finally
                                ' Always calls Finish (even if we are Disabled)
                                Me.RestoreCursor()
                                ' Store the time the application became inactive
                                _inactiveStart = DateTime.Now
                            End Try
                        Else
                            ' Wait before checking again
                            Thread.Sleep(25)
                        End If
                    End If
                Loop While Not _isDisposed AndAlso _isStarted
            Catch
                'The thread is being aborted, just reset the abort
                    and exit gracefully
                Thread.ResetAbort()
            End Try
        End Sub

        ''' <summary>
        ''' Blocks until the application responds to a test message.
        ''' If the application doesn't respond with the timespan, will return false,
        ''' else returns true.
        ''' </summary>
        Private Function IsApplicationBusy(ByVal delay As TimeSpan, ByVal windowHandle As IntPtr) As Boolean
            Const INFINITE As Integer = Int32.MaxValue
            Const WM_NULL As Integer = 0

            Dim result As Integer = 0
            Dim success As Boolean

            'See if the application is responding
            If (delay = TimeSpan.MaxValue) Then
                success = SendMessageTimeout(windowHandle, WM_NULL, 0, Nothing, SMTO_BLOCK, INFINITE, result)
            Else
                success = SendMessageTimeout(windowHandle, WM_NULL, 0, Nothing, SMTO_BLOCK, System.Convert.ToInt32(delay.TotalMilliseconds), result)
            End If

            If result <> 0 Then
                Return True
            End If
            Return False
        End Function

        ''' <summary>
        ''' Waits for the ResetEvent (set by Dispose and Reset), 
        ''' since Start has been called we *have* to call RestoreCursor once the thread is idle again.
        ''' </summary>
        Private Sub WaitForIdle()
            'Wait indefinately until the application is idle
            IsApplicationBusy(TimeSpan.MaxValue, _mainWindowHandle)
        End Sub

        ''' <summary>
        ''' The application is closing, shut the state monitor down.
        ''' </summary>
        ''' <param name="sender">The sender.</param>
        ''' <param name="e">The <see cref="T:System.EventArgs"/> instance containing the event data.</param>
        Private Sub OnApplicationThreadExit(ByVal sender As Object, ByVal e As EventArgs)
            Me.Dispose()
        End Sub

#End Region

    End Class

#End Region

End Class

Return to A Developer's Life