C# WinForms:Form.ShowDialog() 在不同的线程中带有 IWin32Window 所有者参数

C# WinForms: Form.ShowDialog() with IWin32Window owner parameter in a different thread

我正在创建一个 C# VSTO 插件,当表单显示在辅助线程中并且所有者 window 在主线程上。

使用 VSTO 时,Excel 仅支持在主线程上更改 Excel 对象模型(它可以在单独的线程上完成,但很危险,如果 Excel 很忙)。我想在执行长时间操作时显示进度表。为了使进度表单流畅,我在单独的线程上显示表单并使用 Control.BeginInvoke() 从主线程异步更新进度。这一切都很好,但我似乎只能使用不带参数的 Form.ShowDialog() 来显示表单。如果我将 IWin32Window 或 NativeWindow 作为参数传递给 ShowDialog,表单会冻结并且不会更新进度。这可能是因为所有者 IWin32Window 参数是一个存在于主线程上的 Window 而不是显示进度表的辅助线程。

当窗体在单独的线程上时,有什么技巧可以尝试将 IWin32Window 传递给 ShowDialog 函数。从技术上讲,我不需要设置表单的所有者,但如果存在这种差异,则需要设置表单的父级。

我希望我的对话框与 Excel Window 链接,这样当 Excel 最小化或最大化时,对话框将相应地隐藏或显示。

请注意,我已经尝试走 BackgroundWorker 路线,但没有成功实现我想要完成的目标。

----更新示例代码:

以下是我正在尝试做的事情以及我正在尝试做的事情的精简版本。 MainForm 实际上并没有在我的应用程序中使用,因为我试图用它来表示 VSTO 应用程序中的 Excel Window。

Program.cs:

using System;
using System.Windows.Forms;

namespace WindowsFormsApplication1
{
    static class Program
    {
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new MainForm());
        }
    }
}

MainForm.cs:

using System;
using System.Windows.Forms;
using System.Threading;

namespace WindowsFormsApplication1
{
    public partial class MainForm : Form
    {
        public ManualResetEvent SignalEvent = new ManualResetEvent(false);
        private ProgressForm _progressForm;
        public volatile bool CancelTask;

        public MainForm()
        {
            InitializeComponent();
            this.Name = "MainForm";
            var button = new Button();
            button.Text = "Run";
            button.Click += Button_Click;
            button.Dock = DockStyle.Fill;
            this.Controls.Add(button);
        }

        private void Button_Click(object sender, EventArgs e)
        {
            CancelTask = false;
            ShowProgressFormInNewThread();
        }

        internal void ShowProgressFormInNewThread()
        {
            var thread = new Thread(new ThreadStart(ShowProgressForm));
            thread.Start();

            //The main thread will block here until the signal event is set in the ProgressForm_Load.
            //this will allow us to do the work load in the main thread (required by VSTO projects that access the Excel object model),
            SignalEvent.WaitOne();
            SignalEvent.Reset();

            ExecuteTask();
        }

        private void ExecuteTask()
        {
            for (int i = 1; i <= 100 && !CancelTask; i++)
            {
                ReportProgress(i);
                Thread.Sleep(100);
            }
        }

        private void ReportProgress(int percent)
        {
            if (CancelTask)
                return;
            _progressForm.BeginInvoke(new Action(() => _progressForm.UpdateProgress(percent)));
        }

        private void ShowProgressForm()
        {
            _progressForm = new ProgressForm(this);
            _progressForm.StartPosition = FormStartPosition.CenterParent;

            //this works, but I want to pass an owner parameter
            _progressForm.ShowDialog();

            /*
             * This gives an exception:
             * An unhandled exception of type 'System.InvalidOperationException' occurred in System.Windows.Forms.dll
             * Additional information: Cross-thread operation not valid: Control 'MainForm' accessed from a thread other than the thread it was created on.
             */
            //var window = new Win32Window(this);
            //_progressForm.ShowDialog(window);

        }

    }
}

ProgressForm.cs:

using System;
using System.Windows.Forms;

namespace WindowsFormsApplication1
{
    public partial class ProgressForm : Form
    {
        private ProgressBar _progressBar;
        private Label _progressLabel;
        private MainForm _mainForm;

        public ProgressForm(MainForm mainForm)
        {
            InitializeComponent();
            _mainForm = mainForm;
            this.Width = 300;
            this.Height = 150;
            _progressBar = new ProgressBar();
            _progressBar.Dock = DockStyle.Top;
            _progressLabel = new Label();
            _progressLabel.Dock = DockStyle.Bottom;
            this.Controls.Add(_progressBar);
            this.Controls.Add(_progressLabel);
            this.Load += ProgressForm_Load;
            this.Closed += ProgressForm_Close;
        }

        public void UpdateProgress(int percent)
        {
            if(percent >= 100)
                Close();

            _progressBar.Value = percent;
            _progressLabel.Text = percent + "%";
        }

        public void ProgressForm_Load(object sender, EventArgs e)
        {
            _mainForm.SignalEvent.Set();
        }

        public void ProgressForm_Close(object sender, EventArgs e)
        {
            _mainForm.CancelTask = true;
        }

    }
}

Win32Window.cs:

using System;
using System.Windows.Forms;

namespace WindowsFormsApplication1
{
    public class Win32Window : IWin32Window
    {
        private readonly IntPtr _handle;

        public Win32Window(IWin32Window window)
        {
            _handle = window.Handle;
        }

        IntPtr IWin32Window.Handle
        {
            get { return _handle; }
        }
    }
}

在非 UI 线程上创建 winform 控件是不常见的。最好在第一次单击按钮时创建 ProgressForm,这样就不需要 ManualResetEvent.

ProgressForm 实现一个简单的接口 (IThreadController),使您正在执行的任务能够更新进度。

ProgressForm 的所有者是 IntPtr handle = new IntPtr(Globals.ThisAddIn.Application.Hwnd);,这导致 ProgressForm 最小化并用 Excel window 恢复。

我认为您不需要使用 ShowDialog,因为它会阻塞 UI 线程。您可以改用 Show

例如

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Threading;
using System.Runtime.InteropServices;

namespace ExcelAddIn1 {
public partial class UserControl1 : UserControl {

    public UserControl1() {
        InitializeComponent();
    }

    private void button1_Click(object sender, EventArgs e)
    {
        button1.Enabled = false;

        var pf = new ProgressForm();
        IntPtr handle = new IntPtr(Globals.ThisAddIn.Application.Hwnd);
        pf.Show(new SimpleWindow { Handle = handle });

        Thread t = new Thread(o => {
            ExecuteTask((IThreadController) o);
        });
        t.IsBackground = true;
        t.Start(pf);

        pf.FormClosed += delegate {
            button1.Enabled = true;
        };
    }

    private void ExecuteTask(IThreadController tc)
    {
        for (int i = 1; i <= 100 && !tc.IsStopRequested; i++)
        {
            Thread.Sleep(100);
            tc.SetProgress(i, 100);
        }
    }

    class SimpleWindow : IWin32Window {
        public IntPtr Handle { get; set; }
    }
}

interface IThreadController {
    bool IsStopRequested { get; set; }
    void SetProgress(int value, int max);
}

public partial class ProgressForm : Form, IThreadController {
    private ProgressBar _progressBar;
    private Label _progressLabel;

    public ProgressForm() {
        //InitializeComponent();
        this.Width = 300;
        this.Height = 150;
        _progressBar = new ProgressBar();
        _progressBar.Dock = DockStyle.Top;
        _progressLabel = new Label();
        _progressLabel.Dock = DockStyle.Bottom;
        this.Controls.Add(_progressBar);
        this.Controls.Add(_progressLabel);
    }

    public void UpdateProgress(int percent) {
        if (percent >= 100)
            Close();

        _progressBar.Value = percent;
        _progressLabel.Text = percent + "%";
    }

    protected override void OnClosed(EventArgs e) {
        base.OnClosed(e);
        IsStopRequested = true;

    }

    public void SetProgress(int value, int max) {
        int percent = (int) Math.Round(100.0 * value / max);

        if (InvokeRequired) {
            BeginInvoke((Action) delegate {
                UpdateProgress(percent);
            });
        }
        else
            UpdateProgress(percent);
    }

    public bool IsStopRequested { get; set; }
}


}

添加另一个答案,因为虽然可以通过这种方式完成,但这不是推荐的方式(例如永远不必调用 Application.DoEvents())。

使用 pinvoke SetWindowLong 设置所有者,但是这样做会导致需要 DoEvents

您的一些要求也没有意义。您说您希望对话框通过 Excel window 最小化和最大化,但是您的代码锁定了 UI 线程,这会阻止单击 Excel window.另外,您正在使用 ShowDialog。因此,如果进度对话框在完成后保持打开状态,用户仍然无法最小化 Excel window,因为使用了 ShowDialog

public partial class MainForm : UserControl
{
    public ManualResetEvent SignalEvent = new ManualResetEvent(false);
    private ProgressForm2 _progressForm;
    public volatile bool CancelTask;

    public MainForm()
    {
        InitializeComponent();
        this.Name = "MainForm";
        var button = new Button();
        button.Text = "Run";
        //button.Click += button1_Click;
        button.Dock = DockStyle.Fill;
        this.Controls.Add(button);
    }

    private void button1_Click(object sender, EventArgs e)
    {
        CancelTask = false;
        ShowProgressFormInNewThread();
    }

    internal void ShowProgressFormInNewThread()
    {
        var thread = new Thread(new ParameterizedThreadStart(ShowProgressForm));
        thread.Start(Globals.ThisAddIn.Application.Hwnd);

        //The main thread will block here until the signal event is set in the ProgressForm_Load.
        //this will allow us to do the work load in the main thread (required by VSTO projects that access the Excel object model),
        SignalEvent.WaitOne();
        SignalEvent.Reset();

        ExecuteTask();
    }

    private void ExecuteTask()
    {
        for (int i = 1; i <= 100 && !CancelTask; i++)
        {
            ReportProgress(i);
            Thread.Sleep(100);

            // as soon as the Excel window becomes the owner of the progress dialog
            // then DoEvents() is required for the progress bar to update
            Application.DoEvents();
        }
    }

    private void ReportProgress(int percent)
    {
        if (CancelTask)
            return;
        _progressForm.BeginInvoke(new Action(() => _progressForm.UpdateProgress(percent)));
    }

    private void ShowProgressForm(Object o)
    {
        _progressForm = new ProgressForm2(this);
        _progressForm.StartPosition = FormStartPosition.CenterParent;

        SetWindowLong(_progressForm.Handle, -8, (int) o); // <-- set owner
        _progressForm.ShowDialog();
    }

    [DllImport("user32.dll")]
    static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
}