Situation
I have an application which calculates the constant changing position of a number of elements (usually between 10 and 20 simultaneously). The calculations are done on a dedicated thread, which then fires an event to indicate the calculations are completed. This dedicated thread is running at a stable 100 frames per second.
我有一个应用程序,它计算许多元素的恒定变化位置(通常在10到20之间同时)。计算在专用线程上完成,然后触发事件以指示计算已完成。这个专用线程以每秒100帧的速度运行。
Visualization
The visualization of the elements is done using WPF. I implemented a single canvas and have written custom code to add a visual representation of each element to the canvas. Positioning is performed using Canvas.SetLeft and Canvas.SetTop. This code is running inside an event handler on the dedicated thread, and thus does not affect performance.
使用WPF完成元素的可视化。我实现了一个画布,并编写了自定义代码,以将每个元素的可视化表示添加到画布。使用Canvas.SetLeft和Canvas.SetTop执行定位。此代码在专用线程上的事件处理程序内运行,因此不会影响性能。
Problem
Due to the fact that the elements are constantly moving, the movement appears to be stuttering. The only assumption I can make, at this time, is the fact that WPF renders at its own convience and attempts to reach a maximum of 60 frames per second. If you have additional comments about why this could happen, please, do enlighten me.
由于元素不断移动,运动似乎是口吃。我现在唯一可以假设的事实是,WPF自己动手并试图达到每秒最多60帧。如果您对此可能发生的原因有其他意见,请赐教。
Question
How can I tell WPF to render when the dedicated thread has invoked the completed calculations? If at all possible, I would like to prevent rendering of the window/canvas until the calculations are completed, at which point the frame should advance and render the new information.
当专用线程调用完成的计算时,如何告诉WPF进行渲染?如果可能的话,我想阻止渲染窗口/画布,直到计算完成,此时帧应该前进并呈现新信息。
Comment
A lot of people do not like attempts to go beyond the default 60 frames per second, and neither do I. However, it is absolutely mandatory to be able to influence when the rendering occurs, prior to going down to 60 frames per second. This is to ensure that 60 frames per seconds does not influence the stuttering problem.
很多人不喜欢超过每秒默认60帧的尝试,我也不喜欢。但是,在降低到每秒60帧之前,能够影响渲染的时间是绝对必要的。这是为了确保每秒60帧不会影响口吃问题。
Rendering (Code)
This is the involved code for the actual layout update, as per request of Dr. Andrew Burnett-Thompson. The MainViewModel contains an ObservableCollection with ActorViewModel instances, created by the dedicated worker thread. These are then translated into ActorView instances to visualize the calculated data. Upon adding and removing instances, I intentionally used Invoke, rather BeginInvoke to ensure these routines are not the cause of a performance issue.
根据Andrew Burnett-Thompson博士的要求,这是实际布局更新所涉及的代码。 MainViewModel包含一个带有ActorViewModel实例的ObservableCollection,由专用工作线程创建。然后将它们转换为ActorView实例以可视化计算的数据。在添加和删除实例时,我故意使用Invoke,而不是BeginInvoke来确保这些例程不是性能问题的原因。
Upon completion of the dedicated worker computation, UpdateLayout is invoked in MainViewModel, at which point UpdateSynchronizationCollectionLayout is invoked to update the visualization. At this point, scaling and opacity are not utilized, and the same stuttering behaviour is observed when the instance Viewbox is omitted. Unless I'm missing something, the issue should be related to the fact that I cannot check nor control the rendering time or speed.
完成专用工作器计算后,将在MainViewModel中调用UpdateLayout,此时将调用UpdateSynchronizationCollectionLayout来更新可视化。此时,不使用缩放和不透明度,并且在省略实例Viewbox时观察到相同的断续行为。除非我遗漏了某些内容,否则问题应该与我无法检查或控制渲染时间或速度这一事实有关。
/// <summary>
/// Contains the added ActorViewModel instances and the created ActorView wrapped in a ViewBox.
/// </summary>
private Dictionary<ActorViewModel, KeyValuePair<Viewbox, ActorView>> _hSynchronizationCollection = new Dictionary<ActorViewModel, KeyValuePair<Viewbox, ActorView>>();
/// <summary>
/// Update the synchronization collection with the modified data.
/// </summary>
/// <param name="sender">Contains the sender.</param>
/// <param name="e"></param>
private void _UpdateSynchronizationCollection( object sender, NotifyCollectionChangedEventArgs e )
{
// Check if the action that caused the event is an Add event.
if ( e.Action == NotifyCollectionChangedAction.Add )
{
// Invoke the following code on the UI thread.
Dispatcher.Invoke( new Action( delegate()
{
// Iterate through the ActorViewModel instances that have been added to the collection.
foreach( ActorViewModel hActorViewModel in e.NewItems )
{
// Initialize a new _hInstance of the ActorView class.
ActorView hActorView = new ActorView( hActorViewModel );
// Initialize a new _hInstance of the Viewbox class.
Viewbox hViewBox = new Viewbox { StretchDirection = StretchDirection.Both, Stretch = Stretch.Uniform };
// Add the _hInstance of the ActorView to the synchronized collection.
_hSynchronizationCollection.Add( hActorViewModel, new KeyValuePair<Viewbox, ActorView>( hViewBox, hActorView ));
// Set the child of the Viewbox to the ActorView.
hViewBox.Child = hActorView;
// Add the _hInstance of the ActorView to the canvas.
CanvasDisplay.Children.Add( hViewBox );
}
}));
}
// Check if the action that caused the event is a Remove event.
else if ( e.Action == NotifyCollectionChangedAction.Remove )
{
// Invoke the following code on the UI thread.
Dispatcher.Invoke( new Action( delegate()
{
// Iterate through the ActorViewModel instances that have been removed to the collection.
foreach( ActorViewModel hActorViewModel in e.OldItems )
{
// Check if the ActorViewModel _hInstance is contained in the synchronization collection.
if ( _hSynchronizationCollection.ContainsKey( hActorViewModel ))
{
// Remove the ActorView from the canvas.
CanvasDisplay.Children.Remove( _hSynchronizationCollection[hActorViewModel].Key );
// Remove the ActorViewModel from the collection.
_hSynchronizationCollection.Remove( hActorViewModel );
}
}
}));
}
}
/// <summary>
/// Update the synchronization collection layout with the modified data.
/// </summary>
private void _UpdateSynchronizationCollectionLayout()
{
// Invoke the following code on the UI thread.
Dispatcher.Invoke( new Action( delegate()
{
// Iterate through each ActorViewModel in the synchronization collection.
foreach( KeyValuePair<ActorViewModel, KeyValuePair<Viewbox, ActorView>> hDictionaryKeyValuePair in _hSynchronizationCollection )
{
// Retrieve the ActorViewModel.
ActorViewModel hActorViewModel = hDictionaryKeyValuePair.Key;
// Retrieve the KeyValuePair for this ActorViewModel.
KeyValuePair<Viewbox, ActorView> hKeyValuePair = hDictionaryKeyValuePair.Value;
// Sets the height of the ViewBox in which the ActorView is displayed.
hKeyValuePair.Key.Height = hKeyValuePair.Value.ActualHeight * hActorViewModel.LayoutScale;
// Sets the width of the ViewBox in which the ActorView is displayed.
hKeyValuePair.Key.Width = hKeyValuePair.Value.ActualWidth * hActorViewModel.LayoutScale;
// Set the opacity factor of the ActorView.
hKeyValuePair.Value.Opacity = hActorViewModel.LayoutOpacity;
// Sets the hValue of the Left attached property for the given dependency object.
Canvas.SetLeft( hKeyValuePair.Key, hActorViewModel.LayoutLeft - ( hActorViewModel.LayoutAlignment == MainAlignment.Center ? hKeyValuePair.Key.ActualWidth / 2 : ( hActorViewModel.LayoutAlignment == MainAlignment.Right ? hKeyValuePair.Key.ActualWidth : 0 )));
// Sets the hValue of the Top attached property for the given dependency object.
Canvas.SetTop( hKeyValuePair.Key, hActorViewModel.LayoutTop );
// Sets the hValue of the ZIndex attached property for the given dependency object.
Canvas.SetZIndex( hKeyValuePair.Key, hActorViewModel.LayoutLayerIndex );
}
}));
}
/// <summary>
/// Initialize a new _hInstance of the MainWindow class.
/// </summary>
/// <param name="hMainViewModel">Contains the object that is used as data context in MainView.</param>
internal MainView( MainViewModel hMainViewModel )
{
// Add a subscriber that occurs when an item is added, removed, changed, moved, or the entire list is refreshed.
hMainViewModel.ActorViewModel.CollectionChanged += new NotifyCollectionChangedEventHandler( _UpdateSynchronizationCollection );
// Initialize the component.
InitializeComponent();
// Set the subscriber that occurs when the layout changes.
hMainViewModel.LayoutChanged += new Action( _UpdateSynchronizationCollectionLayout );
}
1 个解决方案
#1
1
By default your application code runs on the UI thread, in other words if you write a loop that updated the location of all of your objects via their canvas coordinates, the UI will not re-render until your loop exits. You need to make your calculations 'atomic' sending updates to the UI thread, then update all of the objects in one go.
默认情况下,您的应用程序代码在UI线程上运行,换句话说,如果您编写一个循环,通过其画布坐标更新所有对象的位置,则在循环退出之前,UI将不会重新呈现。您需要使计算“原子”向UI线程发送更新,然后一次更新所有对象。
You mention that:
你提到:
Positioning is performed using Canvas.SetLeft and Canvas.SetTop. This code is running inside an event handler on the dedicated thread, and thus does not affect performance.
使用Canvas.SetLeft和Canvas.SetTop执行定位。此代码在专用线程上的事件处理程序内运行,因此不会影响性能。
I presume this is wrapped in Dispatcher.BeginInvoke to marshal it onto the UI thread?
我认为这包装在Dispatcher.BeginInvoke中以将其编组到UI线程上?
#1
1
By default your application code runs on the UI thread, in other words if you write a loop that updated the location of all of your objects via their canvas coordinates, the UI will not re-render until your loop exits. You need to make your calculations 'atomic' sending updates to the UI thread, then update all of the objects in one go.
默认情况下,您的应用程序代码在UI线程上运行,换句话说,如果您编写一个循环,通过其画布坐标更新所有对象的位置,则在循环退出之前,UI将不会重新呈现。您需要使计算“原子”向UI线程发送更新,然后一次更新所有对象。
You mention that:
你提到:
Positioning is performed using Canvas.SetLeft and Canvas.SetTop. This code is running inside an event handler on the dedicated thread, and thus does not affect performance.
使用Canvas.SetLeft和Canvas.SetTop执行定位。此代码在专用线程上的事件处理程序内运行,因此不会影响性能。
I presume this is wrapped in Dispatcher.BeginInvoke to marshal it onto the UI thread?
我认为这包装在Dispatcher.BeginInvoke中以将其编组到UI线程上?