Automatically showing a wait cursor to indicate a long operation is occurring


/ Published in: C#
Save to your folder(s)

Great way to show the user that something is happening without too much tedious work. As simple to use as putting the following code in Form_Load:

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();

(From the site)
It can be extremely tiresome to have to continually remember to set and unset the wait cursor for an application. If an exception occurs you have to remember to add a try finally block to restore the cursor, or if you popup a message box you must remember to change the cursor first otherwise the user will just sit there thinking the application is busy! These are just a few of the many irritations that I have tried to solve with the class below.

The class below automatically monitors the state of an application and sets and restores the cursor according to whether the application is busy or not. All that’s required are a few lines of setup code and your done (see the example in the class header). If you have a multithreaded application, it won't change the cursor unless the main input thread is blocked. Infact you can remove all of your cursor setting code everywhere!


Copy this code and paste it in your HTML
  1. using System;
  2. using System.Diagnostics;
  3. using System.Runtime.InteropServices;
  4. using System.Threading;
  5. using System.Windows.Forms;
  6.  
  7. namespace VBusers.Windows.Forms.Utils
  8. {
  9. /// <summary>
  10. /// This static utility class can be used to automatically show a wait cursor when the application
  11. /// is busy (ie not responding to user input). The class automatically monitors the application
  12. /// state, removing the need for manually changing the cursor.
  13. /// </summary>
  14. /// <example>
  15. /// To use, simply insert the following line in your Application startup code
  16. ///
  17. /// private void Form1_Load(object sender, System.EventArgs e)
  18. /// {
  19. /// AutoWaitCursor.Cursor = Cursors.WaitCursor;
  20. /// AutoWaitCursor.Delay = new TimeSpan(0, 0, 0, 0, 25);
  21. /// // Set the window handle to the handle of the main form in your application
  22. /// AutoWaitCursor.MainWindowHandle = this.Handle;
  23. /// AutoWaitCursor.Start();
  24. /// }
  25. ///
  26. /// This installs changes to cursor after 100ms of blocking work (ie. work carried out on the main application thread).
  27. ///
  28. /// Note, the above code GLOBALLY replaces the following:
  29. ///
  30. /// public void DoWork()
  31. /// {
  32. /// try
  33. /// {
  34. /// Screen.Cursor = Cursors.Wait;
  35. /// GetResultsFromDatabase();
  36. /// }
  37. /// finally
  38. /// {
  39. /// Screen.Cursor = Cursors.Default;
  40. /// }
  41. /// }
  42. /// </example>
  43. [DebuggerStepThrough()]
  44. public class AutoWaitCursor
  45. {
  46. #region Member Variables
  47.  
  48. private static readonly TimeSpan DEFAULT_DELAY = new TimeSpan(0, 0, 0, 0, 25);
  49. /// <summary>
  50. /// The application state monitor class (which monitors the application busy status).
  51. /// </summary>
  52. private static ApplicationStateMonitor _appStateMonitor = new ApplicationStateMonitor( Cursors.WaitCursor, DEFAULT_DELAY );
  53.  
  54. #endregion
  55.  
  56. #region Constructors
  57.  
  58. /// <summary>
  59. /// Default Constructor.
  60. /// </summary>
  61. private AutoWaitCursor()
  62. {
  63. // Intentionally blank
  64. }
  65.  
  66. #endregion
  67.  
  68. #region Public Static Properties
  69.  
  70. /// <summary>
  71. /// Returns the amount of time the application has been idle.
  72. /// </summary>
  73. public TimeSpan ApplicationIdleTime
  74. {
  75. get{ return _appStateMonitor.ApplicationIdleTime; }
  76. }
  77.  
  78. /// <summary>
  79. /// Returns true if the auto wait cursor has been started.
  80. /// </summary>
  81. public static bool IsStarted
  82. {
  83. get{ return _appStateMonitor.IsStarted; }
  84. }
  85.  
  86. /// <summary>
  87. /// Gets or sets the Cursor to use during Application busy periods.
  88. /// </summary>
  89. public static Cursor Cursor
  90. {
  91. get { return _appStateMonitor.Cursor; }
  92. set
  93. {
  94. _appStateMonitor.Cursor = value;
  95. }
  96. }
  97.  
  98. /// <summary>
  99. /// Enables or disables the auto wait cursor.
  100. /// </summary>
  101. public static bool Enabled
  102. {
  103. get { return _appStateMonitor.Enabled; }
  104. set
  105. {
  106. _appStateMonitor.Enabled = value;
  107. }
  108. }
  109.  
  110. /// <summary>
  111. /// Gets or sets the period of Time to wait before showing the WaitCursor whilst Application is working
  112. /// </summary>
  113. public static TimeSpan Delay
  114. {
  115. get { return _appStateMonitor.Delay; }
  116. set { _appStateMonitor.Delay = value; }
  117. }
  118.  
  119. /// <summary>
  120. /// Gets or sets the main window handle of the application (ie the handle of an MDI form).
  121. /// This is the window handle monitored to detect when the application becomes busy.
  122. /// </summary>
  123. public static IntPtr MainWindowHandle
  124. {
  125. get { return _appStateMonitor.MainWindowHandle; }
  126. set { _appStateMonitor.MainWindowHandle = value; }
  127. }
  128.  
  129. #endregion
  130.  
  131. #region Public Methods
  132.  
  133. /// <summary>
  134. /// Starts the auto wait cursor monitoring the application.
  135. /// </summary>
  136. public static void Start()
  137. {
  138. _appStateMonitor.Start();
  139. }
  140.  
  141. /// <summary>
  142. /// Stops the auto wait cursor monitoring the application.
  143. /// </summary>
  144. public static void Stop()
  145. {
  146. _appStateMonitor.Stop();
  147. }
  148.  
  149. #endregion
  150.  
  151. #region Private Class ApplicationStateMonitor
  152.  
  153. /// <summary>
  154. /// Private class that monitors the state of the application and automatically
  155. /// changes the cursor accordingly.
  156. /// </summary>
  157. private class ApplicationStateMonitor : IDisposable
  158. {
  159. #region Member Variables
  160.  
  161. /// <summary>
  162. /// The time the application became inactive.
  163. /// </summary>
  164. private DateTime _inactiveStart = DateTime.Now;
  165. /// <summary>
  166. /// If the monitor has been started.
  167. /// </summary>
  168. private bool _isStarted = false;
  169. /// <summary>
  170. /// Delay to wait before calling back
  171. /// </summary>
  172. private TimeSpan _delay;
  173. /// <summary>
  174. /// The windows handle to the main process window.
  175. /// </summary>
  176. private IntPtr _mainWindowHandle = IntPtr.Zero;
  177. /// <summary>
  178. /// Thread to perform the wait and callback
  179. /// </summary>
  180. private Thread _callbackThread = null;
  181. /// <summary>
  182. /// Stores if the class has been disposed of.
  183. /// </summary>
  184. private bool _isDisposed = false;
  185. /// <summary>
  186. /// Stores if the class is enabled or not.
  187. /// </summary>
  188. private bool _enabled = true;
  189. /// <summary>
  190. /// GUI Thread Id .
  191. /// </summary>
  192. private uint _mainThreadId;
  193. /// <summary>
  194. /// Callback Thread Id.
  195. /// </summary>
  196. private uint _callbackThreadId;
  197. /// <summary>
  198. /// Stores the old cursor.
  199. /// </summary>
  200. private Cursor _oldCursor;
  201. /// <summary>
  202. /// Stores the new cursor.
  203. /// </summary>
  204. private Cursor _waitCursor;
  205.  
  206. #endregion
  207.  
  208. #region PInvokes
  209.  
  210. [DllImport("user32.dll", CharSet=CharSet.Auto, SetLastError=true)]
  211. [return:MarshalAs(UnmanagedType.Bool)]
  212. private static extern bool SendMessageTimeout(IntPtr hWnd, int Msg, int wParam, string lParam, int fuFlags, int uTimeout, out int lpdwResult);
  213.  
  214. [DllImport("USER32.DLL")]
  215. private static extern uint AttachThreadInput(uint attachTo, uint attachFrom, bool attach);
  216.  
  217. [DllImport("KERNEL32.DLL")]
  218. private static extern uint GetCurrentThreadId();
  219.  
  220. private const int SMTO_NORMAL = 0x0000;
  221. private const int SMTO_BLOCK = 0x0001;
  222. private const int SMTO_NOTIMEOUTIFNOTHUNG = 0x0008;
  223.  
  224. #endregion
  225.  
  226. #region Constructors
  227.  
  228. /// <summary>
  229. /// Default member initialising Constructor
  230. /// </summary>
  231. /// <param name="waitCursor">The wait cursor to use.</param>
  232. /// <param name="delay">The delay before setting the cursor to the wait cursor.</param>
  233. public ApplicationStateMonitor(Cursor waitCursor, TimeSpan delay)
  234. {
  235. // Constructor is called from (what is treated as) the main thread
  236. _mainThreadId = GetCurrentThreadId();
  237. _delay = delay;
  238. _waitCursor = waitCursor;
  239. // Gracefully shuts down the state monitor
  240. Application.ThreadExit +=new EventHandler(_OnApplicationThreadExit);
  241. }
  242.  
  243. #endregion
  244.  
  245. #region IDisposable
  246.  
  247. /// <summary>
  248. /// On Disposal terminates the Thread, calls Finish (on thread) if Start has been called
  249. /// </summary>
  250. public void Dispose()
  251. {
  252. if (_isDisposed)
  253. {
  254. return;
  255. }
  256. // Kills the Thread loop
  257. _isDisposed = true;
  258. }
  259.  
  260. #endregion IDisposable
  261.  
  262. #region Public Methods
  263.  
  264. /// <summary>
  265. /// Starts the application state monitor.
  266. /// </summary>
  267. public void Start()
  268. {
  269. if ( !_isStarted )
  270. {
  271. _isStarted = true;
  272. this._CreateMonitorThread();
  273. }
  274. }
  275.  
  276. /// <summary>
  277. /// Stops the application state monitor.
  278. /// </summary>
  279. public void Stop()
  280. {
  281. if ( _isStarted )
  282. {
  283. _isStarted = false;
  284. }
  285. }
  286.  
  287. /// <summary>
  288. /// Set the Cursor to wait.
  289. /// </summary>
  290. public void SetWaitCursor()
  291. {
  292. // Start is called in a new Thread, grab the new Thread Id so we can attach to Main thread's input
  293. _callbackThreadId = GetCurrentThreadId();
  294.  
  295. // Have to call this before calling Cursor.Current
  296. AttachThreadInput(_callbackThreadId, _mainThreadId, true);
  297.  
  298. _oldCursor = Cursor.Current;
  299. Cursor.Current = _waitCursor;
  300. }
  301.  
  302. /// <summary>
  303. /// Finish showing the Cursor (switch back to previous Cursor)
  304. /// </summary>
  305. public void RestoreCursor()
  306. {
  307. // Restore the cursor
  308. Cursor.Current = _oldCursor;
  309. // Detach from Main thread input
  310. AttachThreadInput(_callbackThreadId, _mainThreadId, false);
  311. }
  312.  
  313. /// <summary>
  314. /// Enable/Disable the call to Start (note, once Start is called it *always* calls the paired Finish)
  315. /// </summary>
  316. public bool Enabled
  317. {
  318. get { return _enabled; }
  319. set
  320. {
  321. _enabled = value;
  322. }
  323. }
  324.  
  325. /// <summary>
  326. /// Gets or sets the period of Time to wait before calling the Start method
  327. /// </summary>
  328. public TimeSpan Delay
  329. {
  330. get { return _delay; }
  331. set { _delay = value; }
  332. }
  333.  
  334. #endregion
  335.  
  336. #region Public Properties
  337.  
  338. /// <summary>
  339. /// Returns true if the auto wait cursor has been started.
  340. /// </summary>
  341. public bool IsStarted
  342. {
  343. get{ return _isStarted; }
  344. }
  345.  
  346. /// <summary>
  347. /// Gets or sets the main window handle of the application (ie the handle of an MDI form).
  348. /// This is the window handle monitored to detect when the application becomes busy.
  349. /// </summary>
  350. public IntPtr MainWindowHandle
  351. {
  352. get { return _mainWindowHandle; }
  353. set { _mainWindowHandle = value; }
  354. }
  355.  
  356.  
  357. /// <summary>
  358. /// Gets or sets the Cursor to show
  359. /// </summary>
  360. public Cursor Cursor
  361. {
  362. get { return _waitCursor; }
  363. set { _waitCursor = value; }
  364. }
  365.  
  366. /// <summary>
  367. /// Returns the amount of time the application has been idle.
  368. /// </summary>
  369. public TimeSpan ApplicationIdleTime
  370. {
  371. get{ return DateTime.Now.Subtract( _inactiveStart ) ; }
  372. }
  373.  
  374. #endregion
  375.  
  376. #region Private Methods
  377.  
  378. /// <summary>
  379. /// Prepares the class creating a Thread that monitors the main application state.
  380. /// </summary>
  381. private void _CreateMonitorThread()
  382. {
  383. // Create the monitor thread
  384. _callbackThread = new Thread(new ThreadStart(_ThreadCallbackLoop));
  385. _callbackThread.Name = "AutoWaitCursorCallback";
  386. _callbackThread.IsBackground = true;
  387. // Start the thread
  388. _callbackThread.Start();
  389. }
  390.  
  391. /// <summary>
  392. /// Thread callback method.
  393. /// Loops calling SetWaitCursor and RestoreCursor until Disposed.
  394. /// </summary>
  395. private void _ThreadCallbackLoop()
  396. {
  397. try
  398. {
  399. do
  400. {
  401. if ( !_enabled || _mainWindowHandle == IntPtr.Zero )
  402. {
  403. // Just sleep
  404. Thread.Sleep( _delay );
  405. }
  406. else
  407. {
  408. // Wait for start
  409. if ( _IsApplicationBusy( _delay, _mainWindowHandle ) )
  410. {
  411. try
  412. {
  413. this.SetWaitCursor();
  414. _WaitForIdle();
  415. }
  416. finally
  417. {
  418. // Always calls Finish (even if we are Disabled)
  419. this.RestoreCursor();
  420. // Store the time the application became inactive
  421. _inactiveStart = DateTime.Now;
  422. }
  423. }
  424. else
  425. {
  426. // Wait before checking again
  427. Thread.Sleep( 25 );
  428. }
  429. }
  430. } while (!_isDisposed && _isStarted);
  431. }
  432. catch ( ThreadAbortException )
  433. {
  434. // The thread is being aborted, just reset the abort and exit gracefully
  435. Thread.ResetAbort();
  436. }
  437. }
  438.  
  439. /// <summary>
  440. /// Blocks until the application responds to a test message.
  441. /// If the application doesn't respond with the timespan, will return false,
  442. /// else returns true.
  443. /// </summary>
  444. private bool _IsApplicationBusy( TimeSpan delay, IntPtr windowHandle)
  445. {
  446. const int INFINITE = Int32.MaxValue;
  447. const int WM_NULL = 0;
  448. int result = 0;
  449. bool success;
  450.  
  451. // See if the application is responding
  452. if ( delay == TimeSpan.MaxValue )
  453. {
  454. success = SendMessageTimeout( windowHandle, WM_NULL,0, null,
  455. SMTO_BLOCK, INFINITE , out result);
  456. }
  457. else
  458. {
  459. success = SendMessageTimeout( windowHandle, WM_NULL,0, null,
  460. SMTO_BLOCK, System.Convert.ToInt32( delay.TotalMilliseconds ) , out result);
  461. }
  462.  
  463. if ( result != 0 )
  464. {
  465. return true;
  466. }
  467. return false;
  468. }
  469.  
  470. /// <summary>
  471. /// Waits for the ResetEvent (set by Dispose and Reset),
  472. /// since Start has been called we *have* to call RestoreCursor once the thread is idle again.
  473. /// </summary>
  474. private void _WaitForIdle()
  475. {
  476. // Wait indefinately until the application is idle
  477. _IsApplicationBusy( TimeSpan.MaxValue, _mainWindowHandle );
  478. }
  479.  
  480. /// <summary>
  481. /// The application is closing, shut the state monitor down.
  482. /// </summary>
  483. /// <param name="sender"></param>
  484. /// <param name="e"></param>
  485. private void _OnApplicationThreadExit(object sender, EventArgs e)
  486. {
  487. this.Dispose();
  488. }
  489.  
  490. #endregion
  491. }
  492.  
  493. #endregion
  494. }
  495. }

URL: http://www.vbusers.com/codecsharp/codeget.asp?ThreadID=58&PostID=1&NumReplies=0

Report this snippet


Comments

RSS Icon Subscribe to comments

You need to login to post a comment.