Pour une vue construite à l'aide de WPF, je veux changer le curseur de la souris en un sablier lorsque l'application est occupée et ne répond pas.
Une solution consiste à ajouter
this.Cursor = Cursors.Wait;
à tous les endroits susceptibles de rendre l'interface utilisateur non réactive. Mais ce n'est évidemment pas la meilleure solution. Je me demande quel est le meilleur moyen d'y parvenir?
Est-il possible d'y parvenir en utilisant des styles ou des ressources?
Merci,
Nous avons fait une classe jetable qui change le curseur pour nous lorsque l'application va prendre longtemps, elle ressemble à ceci:
public class WaitCursor : IDisposable
{
private Cursor _previousCursor;
public WaitCursor()
{
_previousCursor = Mouse.OverrideCursor;
Mouse.OverrideCursor = Cursors.Wait;
}
#region IDisposable Members
public void Dispose()
{
Mouse.OverrideCursor = _previousCursor;
}
#endregion
}
Et nous l'utilisons comme ceci:
using(new WaitCursor())
{
// very long task
}
Ce n'est peut-être pas le meilleur design, mais ça fait l'affaire =)
J'ai utilisé les réponses ici pour construire quelque chose qui fonctionnait mieux pour moi. Le problème est que lorsque le bloc using dans la réponse de Carlo se termine, l'interface utilisateur peut en fait toujours être occupée par la liaison de données. Il se peut que des données ou des événements chargés paresseux se déclenchent à la suite de ce qui a été fait dans le bloc. Dans mon cas, il a parfois fallu plusieurs secondes pour que le curseur d'attente disparaisse jusqu'à ce que l'interface utilisateur soit réellement prête. Je l'ai résolu en créant une méthode d'assistance qui définit le curseur d'attente et prend également en charge la configuration d'une minuterie qui remettra automatiquement le curseur en arrière lorsque l'interface utilisateur est prête. Je ne peux pas être sûr que cette conception fonctionnera dans tous les cas, mais cela a fonctionné pour moi:
/// <summary>
/// Contains helper methods for UI, so far just one for showing a waitcursor
/// </summary>
public static class UiServices
{
/// <summary>
/// A value indicating whether the UI is currently busy
/// </summary>
private static bool IsBusy;
/// <summary>
/// Sets the busystate as busy.
/// </summary>
public static void SetBusyState()
{
SetBusyState(true);
}
/// <summary>
/// Sets the busystate to busy or not busy.
/// </summary>
/// <param name="busy">if set to <c>true</c> the application is now busy.</param>
private static void SetBusyState(bool busy)
{
if (busy != IsBusy)
{
IsBusy = busy;
Mouse.OverrideCursor = busy ? Cursors.Wait : null;
if (IsBusy)
{
new DispatcherTimer(TimeSpan.FromSeconds(0), DispatcherPriority.ApplicationIdle, dispatcherTimer_Tick, Application.Current.Dispatcher);
}
}
}
/// <summary>
/// Handles the Tick event of the dispatcherTimer control.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
private static void dispatcherTimer_Tick(object sender, EventArgs e)
{
var dispatcherTimer = sender as DispatcherTimer;
if (dispatcherTimer != null)
{
SetBusyState(false);
dispatcherTimer.Stop();
}
}
}
La manière la meilleure serait de ne pas faire en sorte que l'interface utilisateur ne devienne jamais réactive, en déchargeant tout le travail sur d'autres threads/tâches selon le cas.
En dehors de cela, vous êtes en quelque sorte dans un catch-22: si vous avez ajouté un moyen de détecter que l'interface utilisateur ne répond pas, il n'y a pas de bon moyen de changer le curseur, car l'endroit dont vous auriez besoin pour le faire ( le thread pair) ne répond pas ... Vous pourriez peut-être utiliser le code Win32 standard pour changer le curseur pour toute la fenêtre?
Sinon, vous devrez le faire de manière préventive, comme le suggère votre question.
Je fais simplement
Mouse.OverrideCursor = Cursors.Wait;
try {
// Long lasting stuff ...
} finally {
Mouse.OverrideCursor = null;
}
Selon la documentation de propriété Mouse.OverrideCursor
Pour effacer le curseur de remplacement, définissez OverrideCursor sur null.
L'instruction try-finally garantit que le curseur par défaut est restauré dans tous les cas, même lorsqu'une exception se produit ou que la partie d'essai reste avec return
ou break
(si à l'intérieur d'une boucle).
Soyez prudent ici car jouer avec le curseur d'attente peut causer des problèmes avec les threads STA. Assurez-vous que si vous utilisez cette chose, vous le faites dans son propre thread. J'ai posté un exemple ici Exécuter à l'intérieur d'un STA qui l'utilise pour afficher un WaitCursor pendant le démarrage du fichier généré, et ne fait pas exploser (l'application principale) AFAICT.
Je préfère personnellement ne pas voir le pointeur de la souris passer plusieurs fois du sablier à la flèche. Pour aider à empêcher ce comportement lors de l'appel de fonctions incorporées qui prennent un certain temps et essayent chacune de contrôler le pointeur de la souris, j'utilise une pile (compteur) que j'appelle LifeTrackerStack. Et ce n'est que lorsque la pile est vide (contre 0) que j'ai remis le sablier à une flèche.
J'utilise également MVVM. Je préfère également le code thread-safe.
Dans ma classe racine de modèle, je déclare mon LifeTrackerStack que je remplis dans des classes de modèle enfant ou que j'utilise directement à partir de classes de modèle enfant lorsque j'y ai accès.
Mon tracker de vie a 2 états/actions:
Ensuite, à mon avis, je me lie manuellement à mon Model.IsBusy et fais:
void ModelPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == "IsBusy")
{
if (this._modelViewAnalysis.IsBusy)
{
if (Application.Current.Dispatcher.CheckAccess())
{
this.Cursor = Cursors.Wait;
}
else
{
Application.Current.Dispatcher.Invoke(new Action(() => this.Cursor = Cursors.Wait));
}
}
else
{
Application.Current.Dispatcher.Invoke(new Action(() => this.Cursor = null));
}
}
Voici ma classe LifeTrackerStack:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
namespace HQ.Util.General
{
/// <summary>
/// Usage is to have only one event for a recursive call on many objects
/// </summary>
public class LifeTrackerStack
{
// ******************************************************************
protected readonly Action _stackCreationAction;
protected readonly Action _stackDisposeAction;
private int _refCount = 0;
private object _objLock = new object();
// ******************************************************************
public LifeTrackerStack(Action stackCreationAction = null, Action stackDisposeAction = null)
{
_stackCreationAction = stackCreationAction;
_stackDisposeAction = stackDisposeAction;
}
// ******************************************************************
/// <summary>
/// Return a new LifeTracker to be used in a 'using' block in order to ensure reliability
/// </summary>
/// <returns></returns>
public LifeTracker GetNewLifeTracker()
{
LifeTracker lifeTracker = new LifeTracker(AddRef, RemoveRef);
return lifeTracker;
}
// ******************************************************************
public int Count
{
get { return _refCount; }
}
// ******************************************************************
public void Reset()
{
lock (_objLock)
{
_refCount = 0;
if (_stackDisposeAction != null)
{
_stackDisposeAction();
}
}
}
// ******************************************************************
private void AddRef()
{
lock (_objLock)
{
if (_refCount == 0)
{
if (_stackCreationAction != null)
{
_stackCreationAction();
}
}
_refCount++;
}
}
// ******************************************************************
private void RemoveRef()
{
bool shouldDispose = false;
lock (_objLock)
{
if (_refCount > 0)
{
_refCount--;
}
if (_refCount == 0)
{
if (_stackDisposeAction != null)
{
_stackDisposeAction();
}
}
}
}
// ******************************************************************
}
}
using System;
namespace HQ.Util.General
{
public delegate void ActionDelegate();
public class LifeTracker : IDisposable
{
private readonly ActionDelegate _actionDispose;
public LifeTracker(ActionDelegate actionCreation, ActionDelegate actionDispose)
{
_actionDispose = actionDispose;
if (actionCreation != null)
actionCreation();
}
private bool _disposed = false;
public void Dispose()
{
Dispose(true);
// This object will be cleaned up by the Dispose method.
// Therefore, you should call GC.SupressFinalize to
// take this object off the finalization queue
// and prevent finalization code for this object
// from executing a second time.
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
// Check to see if Dispose has already been called.
if (!this._disposed)
{
// If disposing equals true, dispose all managed
// and unmanaged resources.
if (disposing)
{
_actionDispose();
}
// Note disposing has been done.
_disposed = true;
}
}
}
}
Et son utilisation:
_busyStackLifeTracker = new LifeTrackerStack
(
() =>
{
this.IsBusy = true;
},
() =>
{
this.IsBusy = false;
}
);
Partout où j'ai un long jogging, je fais:
using (this.BusyStackLifeTracker.GetNewLifeTracker())
{
// long job
}
Ça marche pour moi. J'espère que cela pourrait aider n'importe qui! Eric
J'ai utilisé la solution d'Olivier Jacot-Descombes, elle est très simple et fonctionne bien. Merci. mise à jour: cela fonctionne même bien sans utiliser un autre thread/travailleur en arrière-plan.
Je l'utilise avec backgroudworker, le curseur de la souris a fière allure lorsqu'il est occupé à travailler et revient à la normale lorsque le travail est terminé.
public void pressButtonToDoSomeLongTimeWork()
{
Mouse.OverrideCursor = Cursors.Wait;
// before the long time work, change mouse cursor to wait cursor
worker.DoWork += doWorkLongTimeAsync;
worker.RunWorkerCompleted += worker_RunWorkerCompleted;
worker.RunWorkerAsync(); //start doing some long long time work but GUI can update
}
private void worker_RunWorkerCompleted(object sender,
RunWorkerCompletedEventArgs e)
{
//long time work is done();
updateGuiToShowTheLongTimeWorkResult();
Mouse.OverrideCursor = null; //return mouse cursor to normal
}
La modification du curseur ne signifie pas que l'application ne répondra pas aux événements de la souris et du clavier une fois la longue tâche terminée. Pour éviter les erreurs de lecture des utilisateurs, j'utilise la classe ci-dessous qui supprime tous les messages du clavier et de la souris de la file d'attente de messages de l'application.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Windows.Input;
public class WpfHourGlass : IDisposable
{
[StructLayout(LayoutKind.Sequential)]
private struct POINTAPI
{
public int x;
public int y;
}
[StructLayout(LayoutKind.Sequential)]
private struct MSG
{
public int hwnd;
public int message;
public int wParam;
public int lParam;
public int time;
public POINTAPI pt;
}
private const short PM_REMOVE = 0x1;
private const short WM_MOUSELAST = 0x209;
private const short WM_MOUSEFIRST = 0x200;
private const short WM_KEYFIRST = 0x100;
private const short WM_KEYLAST = 0x108;
[DllImport("user32", EntryPoint = "PeekMessageA", CharSet = CharSet.Ansi, SetLastError = true, ExactSpelling = true)]
private static extern int PeekMessage([MarshalAs(UnmanagedType.Struct)]
ref MSG lpMsg, int hwnd, int wMsgFilterMin, int wMsgFilterMax, int wRemoveMsg);
public WpfHourGlass()
{
Mouse.OverrideCursor = Cursors.Wait;
bActivated = true;
}
public void Show(bool Action = true)
{
if (Action)
{
Mouse.OverrideCursor = Cursors.Wait;
}
else
{
Mouse.OverrideCursor = Cursors.Arrow;
}
bActivated = Action;
}
#region "IDisposable Support"
// To detect redundant calls
private bool disposedValue;
private bool bActivated;
// IDisposable
protected virtual void Dispose(bool disposing)
{
if (!this.disposedValue)
{
if (disposing)
{
//remove todas as mensagens de mouse
//e teclado que tenham sido produzidas
//durante o processamento e estejam
//enfileiradas
if (bActivated)
{
MSG pMSG = new MSG();
while (Convert.ToBoolean(PeekMessage(ref pMSG, 0, WM_KEYFIRST, WM_KEYLAST, PM_REMOVE)))
{
}
while (Convert.ToBoolean(PeekMessage(ref pMSG, 0, WM_MOUSEFIRST, WM_MOUSELAST, PM_REMOVE)))
{
}
Mouse.OverrideCursor = Cursors.Arrow;
}
}
// TODO: free unmanaged resources (unmanaged objects) and override Finalize() below.
// TODO: set large fields to null.
}
this.disposedValue = true;
}
public void Dispose()
{
// Do not change this code. Put cleanup code in Dispose(ByVal disposing As Boolean) above.
Dispose(true);
GC.SuppressFinalize(this);
}
#endregion
}