【问题标题】:In WPF, how to set a Window Owner of a Window build on another thread (another Dispatcher)在 WPF 中,如何在另一个线程(另一个 Dispatcher)上设置 Window 的 Window Owner
【发布时间】:2013-01-31 12:16:14
【问题描述】:

我遇到了以下异常: InvalidOperationException : 调用线程无法访问此对象,因为另一个线程拥有它。

当我尝试设置一个窗口的所有者时,该窗口是在所有者之外的另一个线程上构建的。

我知道我只能从正确的线程更新 UI 对象,但是如果它来自另一个线程,为什么我不能只设置所有者?我可以用另一种方式吗?我想让进度窗口成为唯一可以输入条目的窗口。

这是出现错误的代码部分:

    public partial class DlgProgress : Window
{
    // ******************************************************************
    private readonly DlgProgressModel _dlgProgressModel;

    // ******************************************************************
    public static DlgProgress CreateProgressBar(Window owner, DlgProgressModel dlgProgressModel)
    {
        DlgProgress dlgProgressWithProgressStatus = null;
        var listDlgProgressWithProgressStatus = new List<DlgProgress>();
        var manualResetEvent = new ManualResetEvent(false);
        var workerThread = new ThreadEx(() => StartDlgProgress(owner, dlgProgressModel, manualResetEvent, listDlgProgressWithProgressStatus));
        workerThread.Thread.SetApartmentState(ApartmentState.STA);
        workerThread.Start();
        manualResetEvent.WaitOne(10000);
        if (listDlgProgressWithProgressStatus.Count > 0)
        {
            dlgProgressWithProgressStatus = listDlgProgressWithProgressStatus[0];
        }

        return dlgProgressWithProgressStatus;
    }

    // ******************************************************************
    private static void StartDlgProgress(Window owner, DlgProgressModel progressModel, ManualResetEvent manualResetEvent, List<DlgProgress> listDlgProgressWithProgressStatus)
    {
        DlgProgress dlgProgress = new DlgProgress(owner, progressModel);
        listDlgProgressWithProgressStatus.Add(dlgProgress);
        dlgProgress.ShowDialog();
        manualResetEvent.Set();
    }

    // ******************************************************************
    private DlgProgress(Window owner, DlgProgressModel dlgProgressModel)
    {
        if (owner == null)
        {
            throw new ArgumentNullException("Owner cannot be null");
        }

        InitializeComponent();
        this.Owner = owner; // Can't another threads owns it exception

【问题讨论】:

  • 你不能。你不能把属于不同Dispatchers的DispatcherObjects放在一起
  • 您真的不想创建多线程 UI 线程,这似乎是您正在做的事情。如果可能,您真的希望为您的应用程序保留一个 UI 线程。
  • 为什么不在你所有者的线程中创建你的dlgProgress,然后将它传递给你的workerThread,而不是传递owner 参数?
  • 技术上这是 Windows 支持的,你必须 pinvoke SetParent()。这将在多大程度上破坏 WPF 管道是您绝对必须担心的事情,它肯定不是为了支持它而编写的。编写这样的代码是没有意义的,让 UI 线程显示对话框,在工作线程中进行实际的繁重工作。 BackgroundWorker 对此总是有好处的。
  • 我希望将 ProgressWindow 放入不同的 messageLoop,因为我的 UI 更新非常密集(OCX STA)并且需要一段时间。我希望能够取消并让取消按钮响应。

标签: c# wpf multithreading dispatcher


【解决方案1】:

上面的答案是正确的。但我会试着总结一下:

     [DllImport("user32.dll")]
     static extern int SetWindowLong(IntPtr hwnd, int index, int newStyle);

  public static void SetOwnerWindowMultithread(IntPtr windowHandleOwned, IntPtr intPtrOwner)
  {
            if (windowHandleOwned != IntPtr.Zero && intPtrOwner != IntPtr.Zero)
            {
                SetWindowLong(windowHandleOwned, GWL_HWNDPARENT, intPtrOwner.ToInt32());
            }
 }

获取 WPF 处理程序的代码:

 public static IntPtr GetHandler(Window window)
        {
            var interop = new WindowInteropHelper(window);
            return interop.Handle;
        }

请注意,在设置所有者调用之前应初始化窗口! (可以在 window.Loaded 或 window.SourceInitialized 事件中设置)

 var handler = User32.GetHandler(ownerForm);

        var thread = new Thread(() =>
        {
                var window = new DialogHost();
                popupKeyboardForm.Show();
                SetOwnerWindowMultithread(GetHandler(popupKeyboardForm), handler);
                Dispatcher.Run();
        });

        thread.IsBackground = true;
        thread.Start();

也可以使用 SetParent。比你不需要转换处理程序:

[DllImport("user32.dll", SetLastError = true)]
static extern IntPtr SetParent(IntPtr hWndChild, IntPtr hWndNewParent);

请注意,父级和所有者具有不同的含义。 Win32 window Owner vs window Parent?

【讨论】:

  • 非常感谢您提供的信息。我真的很高兴你能澄清它。如果我需要它,我会更容易做到。
【解决方案2】:

我主要是根据 Hans Passant 的建议做出的。 重要的是,我怀疑这段代码应该只适用于 32 位,因为我在 IntPtr 上使用了“ToInt32”。

这是代码:

WindowHelper 函数:

        // ******************************************************************
    private const int GWL_HWNDPARENT = -8; // Owner --> not the parent

    [DllImport("user32.dll")]
    static extern int SetWindowLong(IntPtr hwnd, int index, int newStyle);

    // ******************************************************************
    public static void SetOwnerWindow(Window owned, IntPtr intPtrOwner)
    {
        try
        {
            IntPtr windowHandleOwned = new WindowInteropHelper(owned).Handle;
            if (windowHandleOwned != IntPtr.Zero && intPtrOwner != IntPtr.Zero)
            {
                SetWindowLong(windowHandleOwned, GWL_HWNDPARENT, intPtrOwner.ToInt32());
            }
        }
        catch (Exception ex)
        {
            Debug.Print(ex.Message);
        }
    }

    // ******************************************************************

调用函数:

  using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Threading;
using HQ.Util.General.Threading;
using HQ.Util.Unmanaged;

namespace HQ.Wpf.Util.Dialog
{
    /// <summary>
    /// Interaction logic for DlgProgressWithProgressStatus.xaml
    /// </summary>
    public partial class DlgProgress : Window
    {
        // ******************************************************************
        private readonly DlgProgressModel _dlgProgressModel;

        // ******************************************************************
        public static DlgProgress CreateProgressBar(Window owner, DlgProgressModel dlgProgressModel)
        {
            DlgProgress dlgProgressWithProgressStatus = null;
            var listDlgProgressWithProgressStatus = new List<DlgProgress>();
            var resetEvent = new ManualResetEvent(false);

            IntPtr windowHandleOwner = new WindowInteropHelper(owner).Handle;
            dlgProgressModel.Owner = owner;
            dlgProgressModel.IntPtrOwner = windowHandleOwner;

            var workerThread = new ThreadEx(() => StartDlgProgress(dlgProgressModel, resetEvent, listDlgProgressWithProgressStatus));
            workerThread.Thread.SetApartmentState(ApartmentState.STA);
            workerThread.Start();
            resetEvent.WaitOne(10000);
            if (listDlgProgressWithProgressStatus.Count > 0)
            {
                dlgProgressWithProgressStatus = listDlgProgressWithProgressStatus[0];
            }

            return dlgProgressWithProgressStatus;
        }

        // ******************************************************************
        private static void StartDlgProgress(DlgProgressModel progressModel, ManualResetEvent resetEvent, List<DlgProgress> listDlgProgressWithProgressStatus)
        {
            DlgProgress dlgProgress = new DlgProgress(progressModel);
            listDlgProgressWithProgressStatus.Add(dlgProgress);
            resetEvent.Set();
            dlgProgress.ShowDialog();
        }

        // ******************************************************************
        private DlgProgress(DlgProgressModel dlgProgressModel)
        {
            if (dlgProgressModel.Owner == null)
            {
                throw new ArgumentNullException("Owner cannot be null");
            }

            InitializeComponent();
            // this.Owner = owner; // Can't another threads owns it exception

            if (dlgProgressModel == null)
            {
                throw new ArgumentNullException("dlgProgressModel");
            }

            _dlgProgressModel = dlgProgressModel;
            _dlgProgressModel.Dispatcher = this.Dispatcher;
            _dlgProgressModel.PropertyChanged += _dlgProgressModel_PropertyChanged;
            DataContext = _dlgProgressModel;
        }

        // ******************************************************************
        // Should be call as a modal dialog
        private new void Show()
        {
            throw new Exception("Should only be used as modal dialog");
        }

        // ******************************************************************
        void _dlgProgressModel_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
        {

            //          if (e.PropertyName == "IsJobCanceled" || e.PropertyName == "IsJobCompleted" || e.PropertyName == "IsProgressCompleted")
            // Faster if we don't check strings and check condition directly 
            {
                if (_dlgProgressModel.HaveConditionToClose())
                {
                    if (_dlgProgressModel.IsJobCanceled == true)
                    {
                        SetDialogResult(false);
                    }
                    else
                    {
                        SetDialogResult(true);
                    }
                }
            }
        }

        // ******************************************************************
        private void SetDialogResult(bool result)
        {
            this._dlgProgressModel.Dispatcher.BeginInvoke(new Action(() =>
                {
                    this.DialogResult = result;
                }), DispatcherPriority.Background);
        }

        // ******************************************************************
        private bool _isFirstTimeLoaded = true;

        private Timer _timer = null;
        // ******************************************************************
        private void WindowLoaded(object sender, RoutedEventArgs e)
        {
            if (_isFirstTimeLoaded)
            {
                WindowHelper.SetOwnerWindow(this, _dlgProgressModel.IntPtrOwner);
                Dispatcher.BeginInvoke(new Action(ExecuteDelayedAfterWindowDisplayed), DispatcherPriority.Background);
                _isFirstTimeLoaded = false;

                if (_dlgProgressModel.FuncGetProgressPercentageValue != null)
                {
                    TimerCallback(null);
                    _timer = new Timer(TimerCallback, null, _dlgProgressModel.MilliSecDelayBetweenCall, _dlgProgressModel.MilliSecDelayBetweenCall);
                }
            }
        }

        // ******************************************************************
        private void TimerCallback(Object state)
        {
            Dispatcher.BeginInvoke(new Action(() =>
                {
                    _dlgProgressModel.ValueCurrent = _dlgProgressModel.FuncGetProgressPercentageValue();
                }));
        }

        // ******************************************************************
        private void ExecuteDelayedAfterWindowDisplayed()
        {
            if (_dlgProgressModel._actionStarted == false)
            {
                _dlgProgressModel._actionStarted = true;
                Task.Factory.StartNew(ExecuteAction);
            }
        }

        // ******************************************************************
        private void ExecuteAction()
        {
            _dlgProgressModel.ExecuteAction();
            _dlgProgressModel._actionTerminated = true;
            _dlgProgressModel.IsJobCompleted = true;
        }

        // ******************************************************************
        private void CmdCancel_Click(object sender, RoutedEventArgs e)
        {
            this._dlgProgressModel.IsJobCanceled = true;
        }

        // ******************************************************************
        private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
        {
            if (! _dlgProgressModel.HaveConditionToClose())
            {
                e.Cancel = true;
                return;
            }

            WindowHelper.SetOwnerWindow(this, 0);

            this.CmdCancel.IsEnabled = false;
            this.CmdCancel.Content = "Canceling...";
            this._dlgProgressModel.Dispose();
        }

        // ******************************************************************
    }
}

【讨论】:

    【解决方案3】:
    internal class WindowHelp
    {
        private const int GWL_HWNDPARENT = -8;
        [DllImport("user32.dll")]
        private static extern IntPtr SetWindowLong(IntPtr hwnd, int index, int newStyle);
        public static void SetOwnerWindow(IntPtr hwndOwned, IntPtr intPtrOwner)
        {
            try
            {
                if (hwndOwned != IntPtr.Zero && intPtrOwner != IntPtr.Zero)
                {
                    SetWindowLong(hwndOwned, GWL_HWNDPARENT, intPtrOwner.ToInt32());
                }
            }
            catch { }
        }
    }
    WindowInteropHelper helper = new WindowInteropHelper(owner);
    _messageBox.Loaded += (sender, e) =>
    {
        IntPtr windowHandleOwned = new WindowInteropHelper(_messageBox).Handle;
        owner.Dispatcher.Invoke(new Action(() =>
        {
            WindowHelp.SetOwnerWindow(windowHandleOwned, helper.Handle);
        }));
    };
    

    这有一个问题是,当应用程序关闭并且拥有的窗口仍然打开时,它会尝试执行一些可能失败的事情,我的猜测是它正在尝试关闭所有拥有的窗口。

    System.ComponentModel.Win32Exception
      HResult=0x80004005
      Message=Invalid window handle
      Source=WindowsBase
      StackTrace:
       at MS.Win32.ManagedWndProcTracker.HookUpDefWindowProc(IntPtr hwnd)
       at MS.Win32.ManagedWndProcTracker.OnAppDomainProcessExit()
       at MS.Win32.ManagedWndProcTracker.ManagedWndProcTrackerShutDownListener.OnShutDown(Object target, Object sender, EventArgs e)
       at MS.Internal.ShutDownListener.HandleShutDown(Object sender, EventArgs e)
    

    给它自己的线程的一个缺点是你必须跟踪子窗口并在应用程序到达关闭的后期阶段之前关闭主窗口时关闭它:

    private void View_Closing(object sender, CancelEventArgs e)
    {
        UIGlobal.SelfThreadedDialogs.ForEach(k =>
        {
            try
            {
                if (k != null && !k.Dispatcher.HasShutdownStarted)
                {
                    k.Dispatcher.InvokeShutdown();
                    //k.Dispatcher.Invoke(new Action(() => { k.Close(); }));
                }
            }
            catch { }
        });
    }
    

    拥有这种“多线程和相关”行为的代价。

    有时不需要跟踪和/或所有者的 View_Closing 代码。有时您只需要跟踪以保留对拥有的窗口的引用,以便在应用程序关闭之前它们不会被垃圾收集。这取决于。看看什么适合你的情况。

    【讨论】:

      【解决方案4】:

      这不是设置所有者。如果您想从另一个线程操作 WPF 中的控件,您需要创建一个委托并将其传递给控件的调度程序。

      if(Control.Dispatcher.CheckAccess())
      {
          //The control can be accessed without using the dispatcher.
          Control.DoSomething();
      }
      else{
           //The dispatcher of the control needs to be informed
           MyDelegate md = new MyDelegate( delegate() { Control.DoSomething(); });
           Control.Dispatcher.Invoke(md, null);
      }
      

      请参阅this 帖子。

      【讨论】:

      • 谢谢 bash,我知道我必须使用 Dispatcher 才能这样做。我的问题是我希望与另一个模式窗口具有相同的行为。但是由于在 2 个不同的线程上有 2 个窗口,这让我很头疼。
      • 当您尝试从其他线程设置所有者时,此选项不起作用。将抛出异常。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-04-02
      • 2018-08-04
      • 1970-01-01
      • 2014-11-20
      • 2020-10-15
      • 1970-01-01
      相关资源
      最近更新 更多