J'ai l'exemple de code suivant qui effectue un zoom chaque fois qu'un bouton est enfoncé:
XAML:
<Window x:Class="WpfApplication12.MainWindow"
xmlns="http://schemas.Microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.Microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Canvas x:Name="myCanvas">
<Canvas.LayoutTransform>
<ScaleTransform x:Name="myScaleTransform" />
</Canvas.LayoutTransform>
<Button Content="Button"
Name="myButton"
Canvas.Left="50"
Canvas.Top="50"
Click="myButton_Click" />
</Canvas>
</Window>
* .cs
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void myButton_Click(object sender, RoutedEventArgs e)
{
Console.WriteLine("scale {0}, location: {1}",
myScaleTransform.ScaleX,
myCanvas.PointToScreen(GetMyByttonLocation()));
myScaleTransform.ScaleX =
myScaleTransform.ScaleY =
myScaleTransform.ScaleX + 1;
Console.WriteLine("scale {0}, location: {1}",
myScaleTransform.ScaleX,
myCanvas.PointToScreen(GetMyByttonLocation()));
}
private Point GetMyByttonLocation()
{
return new Point(
Canvas.GetLeft(myButton),
Canvas.GetTop(myButton));
}
}
la sortie est:
scale 1, location: 296;315
scale 2, location: 296;315
scale 2, location: 346;365
scale 3, location: 346;365
scale 3, location: 396;415
scale 4, location: 396;415
comme vous pouvez le constater, il y a un problème que je pensais résoudre en utilisant Application.DoEvents();
mais ... il n'existe pas a priori dans .NET 4.
Que faire?
L'ancienne méthode Application.DoEvents () était obsolète dans WPF au lieu d'utiliser Dispatcher ou Background Worker Thread pour effectuer le traitement que vous avez décrit. Voir les liens pour quelques articles sur la façon d'utiliser les deux objets.
Si vous devez absolument utiliser Application.DoEvents (), vous pouvez simplement importer system.windows.forms.dll dans votre application et appeler la méthode. Cependant, ce n'est vraiment pas recommandé, car vous perdez tous les avantages offerts par WPF.
Essayez quelque chose comme ça
public static void DoEvents()
{
Application.Current.Dispatcher.Invoke(DispatcherPriority.Background,
new Action(delegate { }));
}
Eh bien, je viens de frapper un cas où je commence à travailler sur une méthode qui s'exécute sur le thread Dispatcher, et elle doit bloquer sans bloquer le thread d'interface utilisateur. Il s'avère que msdn explique comment implémenter un DoEvents () basé sur le Dispatcher lui-même:
public void DoEvents()
{
DispatcherFrame frame = new DispatcherFrame();
Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background,
new DispatcherOperationCallback(ExitFrame), frame);
Dispatcher.PushFrame(frame);
}
public object ExitFrame(object f)
{
((DispatcherFrame)f).Continue = false;
return null;
}
(extrait de méthode Dispatcher.PushFrame )
Si vous avez juste besoin de mettre à jour le graphique de la fenêtre, mieux utiliser comme ceci
public static void DoEvents()
{
Application.Current.Dispatcher.Invoke(DispatcherPriority.Render,
new Action(delegate { }));
}
myCanvas.UpdateLayout();
semble fonctionner aussi bien.
Un problème avec les deux approches proposées est qu’elles entraînent une utilisation ininterrompue de la CPU (jusqu’à 12% selon mon expérience). Ceci est parfois sous-optimal, par exemple lorsque le comportement de l'interface utilisateur modale est implémenté à l'aide de cette technique.
La variante suivante introduit un délai minimum entre les images à l'aide d'un temporisateur (notez qu'il est écrit ici avec Rx mais qu'il peut être atteint avec n'importe quel temporisateur normal):
var minFrameDelay = Observable.Interval(TimeSpan.FromMilliseconds(50)).Take(1).Replay();
minFrameDelay.Connect();
// synchronously add a low-priority no-op to the Dispatcher's queue
Application.Current.Dispatcher.Invoke(DispatcherPriority.Background, new Action(() => minFrameDelay.Wait()));
Depuis l'introduction de async
et await
, il est maintenant possible de renoncer au thread d'interface utilisateur à travers un bloc de code synchrone * (anciennement) * utilisant Task.Delay
, par exemple.
private async void myButton_Click(object sender, RoutedEventArgs e)
{
Console.WriteLine("scale {0}, location: {1}",
myScaleTransform.ScaleX,
myCanvas.PointToScreen(GetMyByttonLocation()));
myScaleTransform.ScaleX =
myScaleTransform.ScaleY =
myScaleTransform.ScaleX + 1;
await Task.Delay(1); // In my experiments, 0 doesn't work. Also, I have noticed
// that I need to add as much as 100ms to allow the visual tree
// to complete its arrange cycle and for properties to get their
// final values (as opposed to NaN for widths etc.)
Console.WriteLine("scale {0}, location: {1}",
myScaleTransform.ScaleX,
myCanvas.PointToScreen(GetMyByttonLocation()));
}
Soyons honnêtes, je n'ai pas essayé le code exact ci-dessus, mais je l'utilise en boucle lorsque je place de nombreux éléments dans un ItemsControl
qui comporte un modèle d'élément coûteux, en ajoutant parfois un petit délai pour donner plus de temps aux autres éléments de l'interface utilisateur.
Par exemple:
var levelOptions = new ObservableCollection<GameLevelChoiceItem>();
this.ViewModel[LevelOptionsViewModelKey] = levelOptions;
var syllabus = await this.LevelRepository.GetSyllabusAsync();
foreach (var level in syllabus.Levels)
{
foreach (var subLevel in level.SubLevels)
{
var abilities = new List<GamePlayingAbility>(100);
foreach (var g in subLevel.Games)
{
var gwa = await this.MetricsRepository.GetGamePlayingAbilityAsync(g.Value);
abilities.Add(gwa);
}
double PlayingScore = AssessmentMetricsProcessor.ComputePlayingLevelAbility(abilities);
levelOptions.Add(new GameLevelChoiceItem()
{
LevelAbilityMetric = PlayingScore,
AbilityCaption = PlayingScore.ToString(),
LevelCaption = subLevel.Name,
LevelDescriptor = level.Ordinal + "." + subLevel.Ordinal,
LevelLevels = subLevel.Games.Select(g => g.Value),
});
await Task.Delay(100);
}
}
Sur le Windows Store, lorsqu’il ya une transition de thème Nice sur la collection, l’effet est tout à fait souhaitable.
Luke