WPF 中的图像处理不是线程安全的?
Image handling in WPF not thread safe?
我正在另一个标记为 STA 的线程中创建 Window 这个 window 有一些控件和图像。
我会关闭这个 window 并在主 UI 线程中打开另一个 window 在这个我有一个打印对话框并使用下面的代码得到一个 FixedDocumentSequence
:
var tempFileName = System.IO.Path.GetTempFileName();
File.Delete(tempFileName);
using (var xpsDocument = new XpsDocument(tempFileName, FileAccess.ReadWrite, CompressionOption.NotCompressed))
{
var writer = XpsDocument.CreateXpsDocumentWriter(xpsDocument);
writer.Write(this.DocumentPaginator);
}
using (var xpsDocument = new XpsDocument(tempFileName, FileAccess.Read, CompressionOption.NotCompressed))
{
var xpsDoc = xpsDocument.GetFixedDocumentSequence();
return xpsDoc;
}
在线:
writer.Write(this.DocumentPaginator);
我从对 VerifyAccess 的内部调用中得到一个 InvalidOperationException,这是 StackTrace:
bei System.Windows.Threading.Dispatcher.VerifyAccess()
bei System.Windows.Threading.DispatcherObject.VerifyAccess()
bei System.Windows.Media.Imaging.BitmapDecoder.get_IsDownloading()
bei System.Windows.Media.Imaging.BitmapFrameDecode.get_IsDownloading()
bei System.Windows.Media.Imaging.BitmapSource.FreezeCore(Boolean isChecking)
bei System.Windows.Freezable.Freeze(Boolean isChecking)
bei System.Windows.PropertyMetadata.DefaultFreezeValueCallback(DependencyObject d, DependencyProperty dp, EntryIndex entryIndex, PropertyMetadata metadata, Boolean isChecking)
bei System.Windows.Freezable.FreezeCore(Boolean isChecking)
bei System.Windows.Media.Animation.Animatable.FreezeCore(Boolean isChecking)
bei System.Windows.Freezable.Freeze()
bei System.Windows.Media.DrawingDrawingContext.DrawImage(ImageSource imageSource, Rect rectangle, AnimationClock rectangleAnimations)
bei System.Windows.Media.DrawingDrawingContext.DrawImage(ImageSource imageSource, Rect rectangle)
bei System.Windows.Media.DrawingContextDrawingContextWalker.DrawImage(ImageSource imageSource, Rect rectangle)
bei System.Windows.Media.RenderData.BaseValueDrawingContextWalk(DrawingContextWalker ctx)
bei System.Windows.Media.DrawingServices.DrawingGroupFromRenderData(RenderData renderData)
bei System.Windows.UIElement.GetDrawing()
bei System.Windows.Media.VisualTreeHelper.GetDrawing(Visual reference)
bei System.Windows.Xps.Serialization.VisualTreeFlattener.StartVisual(Visual visual)
bei System.Windows.Xps.Serialization.ReachVisualSerializer.SerializeTree(Visual visual, XmlWriter resWriter, XmlWriter bodyWriter)
bei System.Windows.Xps.Serialization.ReachVisualSerializer.SerializeObject(Object serializedObject)
bei System.Windows.Xps.Serialization.DocumentPageSerializer.SerializeChild(Visual child, SerializableObjectContext parentContext)
bei System.Windows.Xps.Serialization.DocumentPageSerializer.PersistObjectData(SerializableObjectContext serializableObjectContext)
bei System.Windows.Xps.Serialization.ReachSerializer.SerializeObject(Object serializedObject)
bei System.Windows.Xps.Serialization.DocumentPageSerializer.SerializeObject(Object serializedObject)
bei System.Windows.Xps.Serialization.DocumentPaginatorSerializer.PersistObjectData(SerializableObjectContext serializableObjectContext)
bei System.Windows.Xps.Serialization.DocumentPaginatorSerializer.SerializeObject(Object serializedObject)
bei System.Windows.Xps.Serialization.XpsSerializationManager.SaveAsXaml(Object serializedObject)
bei System.Windows.Xps.XpsDocumentWriter.SaveAsXaml(Object serializedObject, Boolean isSync)
bei System.Windows.Xps.XpsDocumentWriter.Write(DocumentPaginator documentPaginator)
由于 StackTrace 对 BitmapSource/BitmapDecoder
进行了一些调用,我考虑尝试删除图像并将就地图像控件的源设置为 null
<Image Source={x:Null} />
在我对所有图像执行此操作后,我的代码 运行 很顺利,没有更多异常被触发。
我尝试通过以下方法制作自定义图像来解决此问题:
public class CustomImage : Image
{
public CustomImage()
{
this.Loaded += CustomImage_Loaded;
this.SourceUpdated += CustomImage_SourceUpdated;
}
private void CustomImage_SourceUpdated(object sender, System.Windows.Data.DataTransferEventArgs e)
{
FreezeSource();
}
private void CustomImage_Loaded(object sender, System.Windows.RoutedEventArgs e)
{
FreezeSource();
}
private void FreezeSource()
{
if (this.Source == null)
return;
var freeze = this.Source as Freezable;
if (freeze != null && freeze.CanFreeze && !freeze.IsFrozen)
freeze.Freeze();
}
}
但我仍然收到错误。
我正在寻找适用于我的 WPF 应用程序中所有图像的解决方案。
希望我说清楚了,因为用 2 个线程来解释这很奇怪,而且有时会出现随机异常。
编辑:
经过一些进一步的测试后,我现在可以向您展示一个可重现的应用程序,并解决手头的问题,希望它更清楚。
您需要 3 个 windows、1 个文件夹和 1 张图片。
在我的情况下
MainWindow.xaml
Window1.xaml
Window2.xaml
Images 是文件夹的名字,里面有一个叫"plus.png"的图片。
MainWindow.xaml:
<StackPanel Orientation="Vertical">
<StackPanel.Resources>
<Style TargetType="{x:Type Button}">
<Setter Property="Margin"
Value="0,0,0,5"></Setter>
</Style>
</StackPanel.Resources>
<Button Content="Open Window 1" Click="OpenWindowInNewThread" />
<Button Content="Open Window 2" Click="OpenWindowInSameThread" />
</StackPanel>
MainWindow.xaml.cs:
private void OpenWindowInNewThread(object sender, RoutedEventArgs e)
{
var th = new Thread(() =>
{
SynchronizationContext.SetSynchronizationContext(new DispatcherSynchronizationContext(Dispatcher.CurrentDispatcher));
var x = new Window1();
x.Closed += (s, ec) => Dispatcher.CurrentDispatcher.BeginInvokeShutdown(DispatcherPriority.Background);
x.Show();
System.Windows.Threading.Dispatcher.Run();
});
th.SetApartmentState(ApartmentState.STA);
th.IsBackground = true;
th.Start();
}
private void OpenWindowInSameThread(object sender, RoutedEventArgs e)
{
var x = new Window2();
x.Show();
}
Window1.xaml:
<StackPanel Orientation="Horizontal">
<ToggleButton Template="{StaticResource PlusToggleButton}" />
</StackPanel>
Window1.xaml.cs:
没有代码只有构造函数...
Window2.xaml:
<StackPanel Orientation="Horizontal">
<ToggleButton Template="{StaticResource PlusToggleButton}" />
<Button Content="Print Me" Click="Print"></Button>
</StackPanel>
Window2.xaml.cs:
public void Print(object sender, RoutedEventArgs e)
{
PrintDialog pd = new PrintDialog();
pd.PrintVisual(this, "HelloWorld");
}
App.xaml:
<ControlTemplate x:Key="PlusToggleButton"
TargetType="{x:Type ToggleButton}">
<Image Name="Image"
Source="/WpfApplication1;component/Images/plus.png"
Stretch="None" />
</ControlTemplate>
重现步骤:
- 在 MainWindow 中点击 "Open Window 1".
按钮
- 一个window将在第二个UI-线程中弹出,关闭这个Window。
- 单击显示 "Open Window 2"
的按钮
- 一个window会在主UI-Thread
中弹出
- 点击按钮说 "Print Me",应用程序应该崩溃
希望现在可以更轻松地帮助我了。
编辑2:
添加了缺失的代码部分,对这个错误感到抱歉。
另一个可能有助于解决问题的信息是,当您以相反的顺序单击按钮时 - 首先是 Window 2 然后是 Window 1 - 然后尝试打印不会触发异常,所以我仍然相信它有一些图像缓存问题,当图像第一次加载到主程序时 UI-线程打印工作,否则它将失败。
该异常表明您正在尝试访问创建它的线程之外的控件(本质上是从 DispatcherObject 派生的 class)!
很难根据您对线程的解释提出代码修复建议。但一个简单的规则是确保您在 UI 线程中创建一个 UI 线程不可知控件,并且在访问此类控件的属性时也执行相同的操作。
看代码
this.DocumentPaginator
这个 属性 访问器似乎违反了线程访问(这意味着,这个 属性 正在被一个没有创建它的线程访问)。
您可以使用以下代码在 UI 线程上 运行 属性 访问器(并且您还需要确保在 UI 线程上创建此类对象)
Application.Current.Dispatcher.Invoke(
new Action(() => {
//Your code/method name here
}
));
如果这个概念对您来说是新的,值得阅读此 MSDN 页
这是 VerifyAccess
的 MSDN 参考
根据当前的编辑,这个问题很可能是因为 "Print"、点击处理程序、打印我按钮。我在一个新项目中尝试了这段代码,但我无法让它崩溃,所以错误很可能在 Print 函数中。
您正在使用 Window1
在 Window2
上创建的 UI 对象。
本质上,ControlTemplate
的部分在线程之间共享,这不应该发生(即 BitmapImage
,正如我从您的调用堆栈中读到的那样)。
你可以明确地说没有共享:
<ControlTemplate x:Key="PlusToggleButton"
TargetType="{x:Type ToggleButton}"
x:Shared="False">
我正在另一个标记为 STA 的线程中创建 Window 这个 window 有一些控件和图像。
我会关闭这个 window 并在主 UI 线程中打开另一个 window 在这个我有一个打印对话框并使用下面的代码得到一个 FixedDocumentSequence
:
var tempFileName = System.IO.Path.GetTempFileName();
File.Delete(tempFileName);
using (var xpsDocument = new XpsDocument(tempFileName, FileAccess.ReadWrite, CompressionOption.NotCompressed))
{
var writer = XpsDocument.CreateXpsDocumentWriter(xpsDocument);
writer.Write(this.DocumentPaginator);
}
using (var xpsDocument = new XpsDocument(tempFileName, FileAccess.Read, CompressionOption.NotCompressed))
{
var xpsDoc = xpsDocument.GetFixedDocumentSequence();
return xpsDoc;
}
在线:
writer.Write(this.DocumentPaginator);
我从对 VerifyAccess 的内部调用中得到一个 InvalidOperationException,这是 StackTrace:
bei System.Windows.Threading.Dispatcher.VerifyAccess()
bei System.Windows.Threading.DispatcherObject.VerifyAccess()
bei System.Windows.Media.Imaging.BitmapDecoder.get_IsDownloading()
bei System.Windows.Media.Imaging.BitmapFrameDecode.get_IsDownloading()
bei System.Windows.Media.Imaging.BitmapSource.FreezeCore(Boolean isChecking)
bei System.Windows.Freezable.Freeze(Boolean isChecking)
bei System.Windows.PropertyMetadata.DefaultFreezeValueCallback(DependencyObject d, DependencyProperty dp, EntryIndex entryIndex, PropertyMetadata metadata, Boolean isChecking)
bei System.Windows.Freezable.FreezeCore(Boolean isChecking)
bei System.Windows.Media.Animation.Animatable.FreezeCore(Boolean isChecking)
bei System.Windows.Freezable.Freeze()
bei System.Windows.Media.DrawingDrawingContext.DrawImage(ImageSource imageSource, Rect rectangle, AnimationClock rectangleAnimations)
bei System.Windows.Media.DrawingDrawingContext.DrawImage(ImageSource imageSource, Rect rectangle)
bei System.Windows.Media.DrawingContextDrawingContextWalker.DrawImage(ImageSource imageSource, Rect rectangle)
bei System.Windows.Media.RenderData.BaseValueDrawingContextWalk(DrawingContextWalker ctx)
bei System.Windows.Media.DrawingServices.DrawingGroupFromRenderData(RenderData renderData)
bei System.Windows.UIElement.GetDrawing()
bei System.Windows.Media.VisualTreeHelper.GetDrawing(Visual reference)
bei System.Windows.Xps.Serialization.VisualTreeFlattener.StartVisual(Visual visual)
bei System.Windows.Xps.Serialization.ReachVisualSerializer.SerializeTree(Visual visual, XmlWriter resWriter, XmlWriter bodyWriter)
bei System.Windows.Xps.Serialization.ReachVisualSerializer.SerializeObject(Object serializedObject)
bei System.Windows.Xps.Serialization.DocumentPageSerializer.SerializeChild(Visual child, SerializableObjectContext parentContext)
bei System.Windows.Xps.Serialization.DocumentPageSerializer.PersistObjectData(SerializableObjectContext serializableObjectContext)
bei System.Windows.Xps.Serialization.ReachSerializer.SerializeObject(Object serializedObject)
bei System.Windows.Xps.Serialization.DocumentPageSerializer.SerializeObject(Object serializedObject)
bei System.Windows.Xps.Serialization.DocumentPaginatorSerializer.PersistObjectData(SerializableObjectContext serializableObjectContext)
bei System.Windows.Xps.Serialization.DocumentPaginatorSerializer.SerializeObject(Object serializedObject)
bei System.Windows.Xps.Serialization.XpsSerializationManager.SaveAsXaml(Object serializedObject)
bei System.Windows.Xps.XpsDocumentWriter.SaveAsXaml(Object serializedObject, Boolean isSync)
bei System.Windows.Xps.XpsDocumentWriter.Write(DocumentPaginator documentPaginator)
由于 StackTrace 对 BitmapSource/BitmapDecoder
进行了一些调用,我考虑尝试删除图像并将就地图像控件的源设置为 null
<Image Source={x:Null} />
在我对所有图像执行此操作后,我的代码 运行 很顺利,没有更多异常被触发。
我尝试通过以下方法制作自定义图像来解决此问题:
public class CustomImage : Image
{
public CustomImage()
{
this.Loaded += CustomImage_Loaded;
this.SourceUpdated += CustomImage_SourceUpdated;
}
private void CustomImage_SourceUpdated(object sender, System.Windows.Data.DataTransferEventArgs e)
{
FreezeSource();
}
private void CustomImage_Loaded(object sender, System.Windows.RoutedEventArgs e)
{
FreezeSource();
}
private void FreezeSource()
{
if (this.Source == null)
return;
var freeze = this.Source as Freezable;
if (freeze != null && freeze.CanFreeze && !freeze.IsFrozen)
freeze.Freeze();
}
}
但我仍然收到错误。 我正在寻找适用于我的 WPF 应用程序中所有图像的解决方案。
希望我说清楚了,因为用 2 个线程来解释这很奇怪,而且有时会出现随机异常。
编辑: 经过一些进一步的测试后,我现在可以向您展示一个可重现的应用程序,并解决手头的问题,希望它更清楚。
您需要 3 个 windows、1 个文件夹和 1 张图片。 在我的情况下 MainWindow.xaml Window1.xaml Window2.xaml
Images 是文件夹的名字,里面有一个叫"plus.png"的图片。
MainWindow.xaml:
<StackPanel Orientation="Vertical">
<StackPanel.Resources>
<Style TargetType="{x:Type Button}">
<Setter Property="Margin"
Value="0,0,0,5"></Setter>
</Style>
</StackPanel.Resources>
<Button Content="Open Window 1" Click="OpenWindowInNewThread" />
<Button Content="Open Window 2" Click="OpenWindowInSameThread" />
</StackPanel>
MainWindow.xaml.cs:
private void OpenWindowInNewThread(object sender, RoutedEventArgs e)
{
var th = new Thread(() =>
{
SynchronizationContext.SetSynchronizationContext(new DispatcherSynchronizationContext(Dispatcher.CurrentDispatcher));
var x = new Window1();
x.Closed += (s, ec) => Dispatcher.CurrentDispatcher.BeginInvokeShutdown(DispatcherPriority.Background);
x.Show();
System.Windows.Threading.Dispatcher.Run();
});
th.SetApartmentState(ApartmentState.STA);
th.IsBackground = true;
th.Start();
}
private void OpenWindowInSameThread(object sender, RoutedEventArgs e)
{
var x = new Window2();
x.Show();
}
Window1.xaml:
<StackPanel Orientation="Horizontal">
<ToggleButton Template="{StaticResource PlusToggleButton}" />
</StackPanel>
Window1.xaml.cs: 没有代码只有构造函数...
Window2.xaml:
<StackPanel Orientation="Horizontal">
<ToggleButton Template="{StaticResource PlusToggleButton}" />
<Button Content="Print Me" Click="Print"></Button>
</StackPanel>
Window2.xaml.cs:
public void Print(object sender, RoutedEventArgs e)
{
PrintDialog pd = new PrintDialog();
pd.PrintVisual(this, "HelloWorld");
}
App.xaml:
<ControlTemplate x:Key="PlusToggleButton"
TargetType="{x:Type ToggleButton}">
<Image Name="Image"
Source="/WpfApplication1;component/Images/plus.png"
Stretch="None" />
</ControlTemplate>
重现步骤:
- 在 MainWindow 中点击 "Open Window 1". 按钮
- 一个window将在第二个UI-线程中弹出,关闭这个Window。
- 单击显示 "Open Window 2" 的按钮
- 一个window会在主UI-Thread 中弹出
- 点击按钮说 "Print Me",应用程序应该崩溃
希望现在可以更轻松地帮助我了。
编辑2:
添加了缺失的代码部分,对这个错误感到抱歉。
另一个可能有助于解决问题的信息是,当您以相反的顺序单击按钮时 - 首先是 Window 2 然后是 Window 1 - 然后尝试打印不会触发异常,所以我仍然相信它有一些图像缓存问题,当图像第一次加载到主程序时 UI-线程打印工作,否则它将失败。
该异常表明您正在尝试访问创建它的线程之外的控件(本质上是从 DispatcherObject 派生的 class)!
很难根据您对线程的解释提出代码修复建议。但一个简单的规则是确保您在 UI 线程中创建一个 UI 线程不可知控件,并且在访问此类控件的属性时也执行相同的操作。
看代码
this.DocumentPaginator
这个 属性 访问器似乎违反了线程访问(这意味着,这个 属性 正在被一个没有创建它的线程访问)。
您可以使用以下代码在 UI 线程上 运行 属性 访问器(并且您还需要确保在 UI 线程上创建此类对象)
Application.Current.Dispatcher.Invoke(
new Action(() => {
//Your code/method name here
}
));
如果这个概念对您来说是新的,值得阅读此 MSDN 页
这是 VerifyAccess
的 MSDN 参考根据当前的编辑,这个问题很可能是因为 "Print"、点击处理程序、打印我按钮。我在一个新项目中尝试了这段代码,但我无法让它崩溃,所以错误很可能在 Print 函数中。
您正在使用 Window1
在 Window2
上创建的 UI 对象。
本质上,ControlTemplate
的部分在线程之间共享,这不应该发生(即 BitmapImage
,正如我从您的调用堆栈中读到的那样)。
你可以明确地说没有共享:
<ControlTemplate x:Key="PlusToggleButton"
TargetType="{x:Type ToggleButton}"
x:Shared="False">