在不同的UI线程中打印DocumentViewer的内容

5
在我的WPF应用程序中,有一个特定的窗口,其中包含了一个DocumentViewer和其他控件。当这个窗口被打开并加载时,它会动态地构建一个带有进度指示器的FixedDocument,然后在DocumentViewer中显示它。为了改善用户体验,我将该窗口运行在它自己的线程中,以便在构建文档时主应用程序窗口仍然响应。根据此网页上的提示,我像这样在新线程中打开我的窗口:
public void ShowDocumentViewerWindow(params object[] data) {
    var thread = new Thread(() => {
        var window = new MyDocumentViewerWindow(new MyObject(data));
        window.Closed += (s, a) => window.Dispatcher.InvokeShutdown();
        window.Show();
        System.Windows.Threading.Dispatcher.Run();
    });
    thread.SetApartmentState(ApartmentState.STA);
    thread.Start();
}

到目前为止,我对这个设置非常满意,但是我遇到了一个问题。

MyDocumentViewerWindow 包含一个打印按钮,该按钮引用内置的打印命令,目标是文档查看器:

<Button Command="Print" CommandTarget="{Binding ElementName=MyDocumentViewer}">Print</Button>

在将窗口放到自己的线程之前,这段代码可以正常工作。但是现在当我点击它时,应用程序会崩溃。Visual Studio 2010 将上面代码中的以下行标记为崩溃位置,并显示 'The calling thread cannot access this object because a different thread owns it.' 的信息:

System.Windows.Threading.Dispatcher.Run();

堆栈跟踪从这里开始:
at System.Windows.Threading.Dispatcher.VerifyAccess()
at MS.Internal.Printing.Win32PrintDialog.ShowDialog()
at System.Windows.Controls.PrintDialog.ShowDialog()
at System.Printing.PrintQueue.GatherDataFromPrintDialog(PrintDialog printDialog, XpsDocumentWriter&amp;amp; writer, PrintTicket&amp;amp; partialTrustPrintTicket, PrintQueue&amp;amp; partialTrustPrintQueue, Double&amp;amp; width, Double&amp;amp; height, String jobDescription)
at System.Printing.PrintQueue.CreateXpsDocumentWriter(String jobDescription, PrintDocumentImageableArea&amp;amp; documentImageableArea)
at System.Windows.Controls.Primitives.DocumentViewerBase.OnPrintCommand()
at System.Windows.Controls.Primitives.DocumentViewerBase.ExecutedRoutedEventHandler(Object target, ExecutedRoutedEventArgs args)
...

我猜测打印对话框是在主UI线程打开的,并且试图访问由我的线程创建和拥有的文档,因此导致了崩溃。

有什么想法可以解决这个问题吗?我想保持窗口在其自己的线程中。

2个回答

7

通过一些谷歌搜索,我偶然发现了下面这个帖子,似乎正是我正在遇到的问题。

PrintDialog和次要UI线程的严重问题

在那个帖子中,那个人最终使用了一个自定义的PrintDialog类(源代码可以在这里找到),它与内置的PrintDialog基本相同,但对一些跨线程错误进行了一些调整(它还覆盖了XPS文档编写器,后者显然进一步将其绑定到应用程序的主UI线程中)

我复制并粘贴了该自定义PrintDialog的代码(并将类重命名为ThreadSafePrintDialog),删除了我的打印按钮的CommandTarget,并改为使用自己的Print方法:

private void Print_Executed(object sender, ExecutedRoutedEventArgs args) {
    var printDialog = new ThreadSafePrintDialog();
    if (!printDialog.ShowDialog(this)) return;

    printDialog.PrintDocument(DocumentViewer.Document.DocumentPaginator, "My Document");
}

完美运作。


你能否把这个类放在这里,因为链接已经失效了。我正在遇到这个问题,而且我认为我们恰好遇到了同样的问题。 - Hakan Fıstık
我是说,您能否请提供“ThreadSafePrintDialog”类的源代码? - Hakan Fıstık

1

你的直觉是正确的。当对象被另一个线程创建时,你不能在UI线程上访问它。

我相信你有几个选择:

  1. 您可以在UI线程上创建此文档,可能在后台线程中收集所需的信息,然后在UI线程上实际构建对象。这取决于您的文档创建内容。您可以执行以下操作:

    public void CreateDocument(T inputDataForDocumentCreation) {
      var uiDispatcher = Dispatcher.CurrentDispatcher;
      ThreadPool.QueueUserWorkItem(_ => {
        // Load and create document components with yourDataForDocumentCreation
    
         dispatcher.BeginInvoke(DispatcherPriority.Normal, () => {
         //Actually create the document (this will happen on the UI thread, so it may be accessed from the UI thread)
      });
      });
    }
    
  2. 您可以将此命令发送到创建其他文档的线程吗?保留此线程并执行thread.Invoke(printMethod)

  3. 您可以查看Freezable Objects。请参阅此页面底部,标题为“创建自己的Freezable类”。这将使您的文档能够安全地从与创建它的线程不同的线程访问。


谢谢。创建我的文档涉及实例化FixedDocument,添加FixedPage对象,填充它们的控件等。因为FixedDocument是DispatcherObject,所以我不能在后台线程中创建它,然后将其设置为DocumentViewer的源,因为这样也会创建跨线程冲突。我发现我必须在与我的DocumentViewer相同的线程上创建我的文档 - 即在UI线程上 :-( 但我刚刚找到了解决我的问题的方法 - 我现在会发布它。 - Ross

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