【问题标题】:Image handling in WPF not thread safe?WPF 中的图像处理不是线程安全的?
【发布时间】:2015-01-16 19:25:43
【问题描述】:

我正在另一个标记为 STA 的线程中创建一个窗口,此窗口有一些控件和图像。

我要关闭这个窗口,然后在主 UI 线程中打开另一个窗口,我有一个打印对话框,并使用以下代码获取 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 进行了一些调用,我考虑尝试删除图像并将就地图像控件的 Source 设置为 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 个窗口、1 个文件夹和 1 个图像。 在我的情况下 主窗口.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>

重现步骤:

  1. 在主窗口中单击“打开窗口 1”按钮。
  2. 将在第二个 UI 线程中弹出一个窗口,关闭此窗口。
  3. 点击“打开窗口2”按钮
  4. 主 UI 线程中会弹出一个窗口
  5. 点击“打印我”按钮,应用程序应该崩溃

希望现在可以更轻松地帮助我。

编辑2:

添加了缺少的代码部分,对此错误表示抱歉。

另一个可能有助于解决问题的信息是,当您以相反的顺序单击按钮时——首先是窗口 2,然后是窗口 1——然后尝试打印不会触发任何异常,所以我仍然相信它的一些图像缓存问题,当图像第一次加载到主 UI 线程打印时,如果不是,它将失败。

【问题讨论】:

  • 您可能首先尝试冻结原始图像。与其从 Uri 加载它们,不如从 Web 手动检索它们(例如通过 WebClient),并从包含下载缓冲区的 MemoryStream 初始化 BitmapImage。然后,您可以在创建 BitmapImage 后立即冻结它们。当然,您也可以在另一个线程中执行此操作。
  • 如何冻结原始图像?我唯一要做的就是用 在 XAML 中指定图像,我不使用任何 WebClient 或以异步方式加载图像,我只是有一个显示图像的单独线程中的窗口,一旦我关闭该窗口并返回主 UI 线程并尝试打印相同的图像,它就会崩溃。我是否删除带有 {x:Null} 的图像一切正常。
  • @Clemens 忘记了我上次评论中的 [at],请查看我关于此问题的消息
  • 此处快速检查显示从本地文件 Uris 加载的图像已被冻结。
  • @Clemens 请看看我的编辑。

标签: c# wpf multithreading image


【解决方案1】:

您正在使用由 Window1Window2 上创建的 UI 对象。

基本上,ControlTemplate 的一部分在线程之间共享,这不应该发生(即BitmapImage,正如我从你的调用堆栈中读到的那样)。

你可以明确地说没有分享:

<ControlTemplate x:Key="PlusToggleButton"
                    TargetType="{x:Type ToggleButton}"
                    x:Shared="False">

【讨论】:

  • 请告诉我这是一个谎言,请不要说我需要做的只是设置一个布尔值,一切都很好......而且我一直用头撞桌子将近一个星期...很快就会测试它...
  • 工作正常,如果我有其他问题我会回来阅读这个主题。目前我想知道如果我只是随机将 x:Shared="false" 放在我拥有的每个控件上?!?
  • 我现在阅读了msdn.microsoft.com/en-us/library/aa970778(v=vs.110).aspx,但我真的不明白为什么在打开 Window 2 时没有出现异常?!?如果我理解正确,它应该会发生,因为 Window 2 也访问相同的资源并构建按钮/图像,那么为什么只有打印会触发异常?!?你有什么解释吗?
  • @RandRandom:据我所知,出于性能原因,BitmapImage 默认情况下会被缓存。在我看来,Window2显示 BitmapImage 时不会点击VerifyAccess(),但只有在它想要实际打印它时才点击。
  • @ChrisEelmaa 您应该将您的最后一条评论添加到答案中。我也发现了。
【解决方案2】:

该异常表明您正在尝试访问创建它的线程之外的控件(本质上是从DispatcherObject 派生的类)!

很难根据您对线程的解释提出代码修复建议。但一个简单的规则是确保您在 UI 线程中创建一个与 UI 线程无关的控件,并且在访问此类控件的属性时也这样做。

查看代码

this.DocumentPaginator

这个属性访问器似乎违反了线程访问(这意味着,这个属性正在被一个没有创建它的线程访问)。

您可以使用以下代码在 UI 线程上运行属性访问器(您还需要确保在 UI 线程上创建此类对象)

Application.Current.Dispatcher.Invoke(
  new Action(() => {
//Your code/method name here
}
));

如果这个概念对您来说是新概念,值得一读MSDN 页面

这是 VerifyAccess 的 MSDN 参考

【讨论】:

  • 感谢您的“信息”。我不敢告诉你,但你所说的都是我已经知道的。这也是我尝试将 print 方法包装在 Dispatcher Invoke 中的第一件事。对于我可以在 VisualTree 中以递归方式找到的所有元素,我还检查了 CheckAccess 返回的内容,并且它始终返回 true。在我使用 {x:Null} 删除图像的源后,一切正常,所以我的提示是 WPF 在第二个 STA 线程和主 UI 线程中加载图像,而不是尝试从缓存中加载它并在那里出现异常。这就是我尝试上述方法的原因。
  • 而且我没有在代码中访问任何图像,或者以任何方式处理它们,唯一发生的事情是我在 UI 中显示它们或打印它们,我所做的只是在我的 XAML ,正如我曾经提到的,我将其更改为 一切都很好。
【解决方案3】:

在当前的编辑中,这个问题肯定是因为“打印”、点击处理程序、打印我按钮。我在一个新项目中尝试了此代码,但无法使其崩溃,因此该错误很可能在打印功能中。

【讨论】:

  • 你不能让它崩溃,因为我忘记添加打印事件,现在包含它,请再看看,不胜感激。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-01-28
  • 1970-01-01
相关资源
最近更新 更多