WPF中的图像处理不安全?

6
我正在创建一个在不同线程中标记为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进行了一些调用,我考虑尝试删除图像并将原地图像控件的源设置为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应用程序中所有图像的解决方案。
希望我表达清楚了,因为用两个线程和某些异常来解释这个问题相当奇怪。
编辑:经过进一步测试,现在我能够向您展示一个具有手头问题的可重现应用程序,希望这样更清晰。
您需要3个窗口,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>

重现步骤:

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

希望现在更容易帮助我了解问题。

编辑2:

添加了丢失的代码部分,对此错误感到抱歉。

另外一个可能有助于解决问题的信息是,当您按相反的顺序点击按钮 - 先是窗口2,然后是窗口1 - 然后尝试打印时,不会触发任何异常,因此我仍然认为这是一些图像缓存问题,当图像首次加载到主UI线程时,如果不行,就会失败。


你可以尝试首先冻结原始图像。不要从Uri加载它们,而是手动从Web(例如通过WebClient)检索它们,并从包含下载缓冲区的MemoryStream初始化BitmapImage。然后在创建BitmapImage后立即将其冻结。当然,你也可以在另一个线程中执行此操作。 - Clemens
我该如何冻结原始图像?我所做的唯一一件事就是在XAML中使用<Image Source="..\Images\foo.jpg" />指定图像,我没有使用任何WebClient或异步方式加载图像,我只是在单独的线程中拥有一个窗口来显示图像,一旦我关闭该窗口并返回到主UI线程并尝试打印相同的图像,它就会崩溃。如果我使用{x:Null}删除图像,则一切正常。 - Rand Random
@Clemens 在我上一条评论中忘记了 @ 符号,请查看我在这个问题上的留言。 - Rand Random
@RandRandom,Print Me按钮没有Click处理程序Print。 - Kcvin
@RandRandom 有更新吗? - Kcvin
显示剩余5条评论
3个回答

4

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

基本上,ControlTemplate 的某些部分在线程之间共享,这是不应该发生的(尤其是 BitmapImage,根据您的调用堆栈)。

您可以明确告诉它不要共享:

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

请告诉我这是谎言,不要说我只需要设置一个布尔值就可以了...我已经猛烈敲打桌子将近一周了...马上就要测试它了... - Rand Random
工作正常,如果有进一步的问题,我会深入研究这个主题并回来。目前我想知道,如果我随意在每个控件上都加上 x:Shared="false",是否会影响性能?!? - Rand Random
我现在阅读 https://msdn.microsoft.com/en-us/library/aa970778(v=vs.110).aspx,但我真的不明白为什么打开 Window 2 时没有出现异常?如果我理解正确的话,应该会发生异常,因为 Window 2 也访问了相同的资源并构建了按钮/图像,那么为什么只有打印触发了异常呢?你有什么解释吗? - Rand Random
1
据我所知,BitmapImage出于性能考虑默认被缓存。对我来说,似乎Window2显示BitmapImage时没有触发VerifyAccess(),而只有在想要实际打印它时才会触发。 - Erti-Chris Eelmaa
@ChrisEelmaa,你应该将你最后的评论添加到答案中。我也发现了同样的问题。 - Kcvin

3
该异常提示您正在尝试在创建它的线程之外访问控件(实际上是从DispatcherObject派生的类)!根据您对线程的解释,很难建议一个代码修复方案。但一个简单的规则是确保在UI线程中创建一个UI线程不可知的控件,并在访问此类控件的属性时也这样做。看看代码。
this.DocumentPaginator

该属性访问器似乎违反了线程访问规则(也就是说,该属性被一个非创建它的线程访问)。
您可以使用以下代码在UI线程上运行属性访问器(并且还需要确保这样的对象在UI线程上创建)。
Application.Current.Dispatcher.Invoke(
  new Action(() => {
//Your code/method name here
}
));

如果这个概念对您来说是新的,建议阅读这篇 MSDN 页面

这里有关于VerifyAccess的MSDN参考文献


谢谢你的“信息”。很抱歉告诉你,你所说的一切我都已经知道了。这也是我尝试将打印方法包装在Dispatcher Invoke中的第一件事。我还检查了CheckAccess返回的内容,并且它总是返回true,对于我可以在VisualTree中以递归方式找到的所有元素。在我使用{x:Null}删除图像源之后,一切都正常工作了,因此我的提示是WPF在第二个STA线程中加载图像,然后主UI线程尝试从缓存中加载它,因此出现异常。这就是为什么我尝试了上面的方法。 - Rand Random
我在代码中没有访问任何图像,也没有以任何方式处理它们,我只是在界面上显示它们或打印它们,所有我做的都是在我的 XAML 中使用 <Image Source="..\Images\foo.jpg" />,正如我之前提到的,一旦我将其更改为 <Image Source="{x:Null}" />,一切都很好。 - Rand Random

-2
通过当前的编辑,这个问题肯定是因为"Print"、点击处理程序和打印按钮。我在一个新项目中尝试了这段代码,但无法使其崩溃,所以错误很可能出现在打印函数中。

你无法让它崩溃,因为我忘记添加打印事件了,现在已经包含了,请再看一下,感激不尽。 - Rand Random

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接