如何从调度线程访问由单独线程生成的WPF UI元素?

5
我需要使用wpf UI元素如FixedDocument、FlowDocument、PageContent、BlockUIContainer等生成一个长的打印预览。为了保持我的UI响应,我正在使用一个单独的Thread类线程完成这部分工作(BackgroundWorker不起作用,因为我需要一个STA线程)。到目前为止一切都很好。
但是,在显示打印预览后,现在我需要打印,点击生成的预览上的打印图标会抛出臭名昭著的“调用线程无法访问此对象,因为不同线程拥有它”异常。那么,有什么解决办法吗?

编辑(代码):

Dispatcher.CurrentDispatcher.Invoke(new Action(() =>  
    {  
        Thread thread = new Thread(() =>  
            {  
                FixedDocument document = renderFlowDocumentTemplate(report);  
                PrintPreview preview = new PrintPreview();  
                preview.WindowState = WindowState.Normal;  
                preview.documentViewer.Document = document;  
                preview.ShowDialog();  
            });  
        thread.SetApartmentState(ApartmentState.STA);  
        thread.Start();  
    }));`

好的,RenderFlowDocumentTemplate()方法生成打印预览(包含UI元素),并使用报表数据填充它们。PrintPreview是一个自定义窗口,包含一个DocumentViewer元素,实际上持有和显示预览,并包含打印图标,点击该图标后我应该会得到打印对话框。

编辑(XAML):

<cw:CustomWindow x:Class="MyApp.Reports.PrintPreview"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:cw="clr-namespace:MyApp.UI.CustomWindows;assembly=MyApp.UI.CustomWindows">    
    <DocumentViewer Margin="0,30,0,0" Name="documentViewer"></DocumentViewer>
</cw:CustomWindow>`

请分享您的代码以便了解问题。 - pchajer
6个回答

1

还有另一个“技巧”...

我经常遇到相同的问题。例如,我尝试将FrameworkElement绑定到ContentPresenter。我的解决方案是,我使用ItemsControl代替ContentPresenter,并通过ObservableCollection<FrameworkElement>将我的单个FrameworkElement绑定到只有一个项目的集合上。之后,就没有问题了。


1

我之前尝试过这个方法 - 我认为问题在于打印预览对话框需要在主线程上运行。


在我看来是这样的。我可以从其他线程将一些非 UI 代码调度到主线程,但如何对 UI 元素执行相同的操作呢? - atiyar
我觉得你必须在该线程上创建它们 - 除非在渲染时设置了线程引用,但我认为它已经在构建中了。如果你的renderFlowDocumentTemplate正在创建一些不带对其他线程的引用的UI元素,那么你可以直接调用它,然后它就在正确的线程上了 - 如果你没有获得任何输出,你可以在UI元素上调用测量和排列来强制重绘。 - Rune Andersen

1
发现另一个人有完全相同的问题 - 在不同的UI线程中打印DocumentViewer的内容。我只是按照相同的路径进行操作。这里的代码在这里真的是我的救星。
现在我不再尝试从Dispatcher线程访问生成的次要线程UI元素,而是将其余的打印过程在次要线程上执行。没有跨线程的UI元素"VerifyAccess",一切都很顺利。 :)

1

最简单的方法是。

Action a = () =>
{
    //Code from another thread.
};
Dispatcher.BeginInvoke(a);

请问您能否详细说明一下?仍然出现“调用线程无法访问此对象,因为不同的线程拥有它”的问题。 - atiyar
定位抛出异常的确切行,并将其放入“Action”方法中。在需要时使用UI中的“Dispatcher”。否则,您可以发布一些代码,以便我们更好地理解问题? - Silvermind

1

我写了这个简单的代码片段,虽然我没有相关经验,但我测试了几个方面,看起来它运行得很好。

/// <summary>
/// Creates UI element on a seperate thread and transfers it to
/// main UI thread. 
/// 
/// Usage; if you have complex UI operation that takes a lot of time, such as XPS object creation.
/// </summary>
/// <param name="constructObject">Function that creates the necessary UIElement - will be executed on new thread</param>
/// <param name="constructionCompleted">Callback to the function that receives the constructed object.</param>
public void CreateElementOnSeperateThread(Func<UIElement> constructObject, Action<UIElement> constructionCompleted)
{
    VerifyAccess();

    // save dispatchers for future usage.
    // we create new element on a seperate STA thread
    // and then basically swap UIELEMENT's Dispatcher.
    Dispatcher threadDispatcher = null;
    var currentDispatcher = Dispatcher.CurrentDispatcher;

    var ev = new AutoResetEvent(false);
    var thread = new Thread(() =>
        {
            threadDispatcher = Dispatcher.CurrentDispatcher;
            ev.Set();

            Dispatcher.Run();
        });

    thread.SetApartmentState(ApartmentState.STA);
    thread.IsBackground = true;
    thread.Start();

    ev.WaitOne();

    threadDispatcher.BeginInvoke(new Action(() =>
        {
            var constructedObject = constructObject();
            currentDispatcher.BeginInvoke(new Action(() =>
                {
                    var fieldinfo = typeof (DispatcherObject).GetField("_dispatcher",
                                                                       BindingFlags.NonPublic |
                                                                       BindingFlags.Instance);
                    if (fieldinfo != null)
                        fieldinfo.SetValue(constructedObject, currentDispatcher);

                    constructionCompleted(constructedObject);
                    threadDispatcher.BeginInvokeShutdown(DispatcherPriority.Normal);
                }), DispatcherPriority.Normal);
        }), DispatcherPriority.Normal);
}

这里是用法:

 CreateElementOnSeperateThread(() =>
        {
            // running on new temp dispatcher.
            var loadsOfItems = new List<int>();
            for(var i = 0; i < 100000; i++)
                loadsOfItems.Add(i+12);


            var dataGrid = new DataGrid {ItemsSource = loadsOfItems, Width = 500, Height = 500};

            dataGrid.Measure(new Size(500, 500));
            dataGrid.Arrange(new Rect(0, 0, 500, 500));

            return dataGrid;
        }, result => SampleGrid.Children.Add(result));

我不是完全确定,但你为什么问呢?据我所知,冻结的对象本质上已经是跨线程的。 - Erti-Chris Eelmaa
我计划在一个线程中进行一些渲染(渲染目标),然后在主线程中使用它们。因此,我想知道图像源的上下文更改是否存在问题。 - Meirion Hughes

0
在这种情况下,使用调度程序类。Dispatcher类具有invoke和beginInvoke方法。它们允许使用调度过程将请求发送到当前线程。 你需要做的是使用委托创建以下调用:
  App.Current.Dispatcher.BeginInvoke(DispatcherPriority.Background, new Action(() =>
{
//you code goes here.
 }));

BeginInvoke 调用过程会异步地处理您的调用。


另一种方法是创建具有特定打印功能的委托和函数,并从您的代码中调用该委托。这将起作用。如果不起作用,最好分享您的代码。 - JSJ

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