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

时间:2015-03-25 18:47:02

标签: c# multithreading winforms window showdialog

我正在创建一个C#VSTO插件,当表单在辅助线程中显示且所有者窗口在主线程上时,在Form.ShowDialog()中设置所有者窗口参数时遇到问题。

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

当表单在一个单独的线程上时,我是否可以尝试将IWin32Window传递给ShowDialog函数。从技术上讲,我不需要设置表单的所有者,而是需要设置表单的父级,如果存在这样的差异。

我希望我的对话框与Excel窗口链接,以便在Excel最小化或最大化时,对话框将被隐藏或相应显示。

请注意,我已经尝试过使用BackgroundWorker路线,而且我试图完成的工作并不成功。

----更新了示例代码:

以下是我正在尝试做的以及我如何尝试这样做的精简版。 MainForm实际上并未在我的应用程序中使用,因为我试图用它来代表VSTO应用程序中的Excel窗口。

的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; }
        }
    }
}

2 个答案:

答案 0 :(得分:2)

添加另一个答案,因为虽然可以这样做,但不是推荐的方法(例如,永远不必调用Application.DoEvents())。

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

您的一些要求也没有意义。您说您希望对话框最小化并最大化Excel窗口,但您的代码锁定了UI线程,这阻止了单击Excel窗口。此外,您正在使用ShowDialog。因此,如果完成后进度对话框保持打开状态,则用户仍然无法最小化Excel窗口,因为使用了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);
}

答案 1 :(得分:0)

在非UI线程上创建winform控件很不寻常。最好在首次点击按钮时创建ProgressForm,然后您就不需要ManualResetEvent

ProgressForm实现一个简单的界面(IThreadController),允许执行任务更新进度。

ProgressForm的所有者为IntPtr handle = new IntPtr(Globals.ThisAddIn.Application.Hwnd);,这会导致ProgressForm最小化并使用Excel窗口进行恢复。

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

E.g。

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; }
}


}