#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&PostID=1&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
|