Wednesday 7 July, 2010

AUTOMATICALLY SHOWING A WAIT CURSOR TO INDICATE A LONG OPERATION IS OCCURRING

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

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!

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

No comments:

Post a Comment