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