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);
}
我正在创建一个 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);
}