C# 线程单元安全打开/保存文件对话框





5.00/5 (9投票s)
在多线程单元模式 (MTA) 的应用程序中使用 .NET OpenFileDialog 和 SaveFileDialog
下载 FileDialogsThreadAppartmentSafe_v1.zip
如果您使用 C# 进行桌面开发项目,您可能了解默认 C# 包装类对于 Win32 打开和保存文件对话框的强大功能。 它们易于使用,并且您始终会拥有正确的 Windows 样式。
但也存在一些问题,本文将讨论其中的一些问题。
背景
一旦您开发更大的应用程序,您可能会注意到,一旦您将调用线程单元状态设置为 MTA,默认的 OpenFileDialog
和 SaveFileDialog
对话框将不再起作用。 一旦您调用实例的 ShowDialog
方法,您将收到以下异常。
{System.Threading.ThreadStateException: Current thread must be set to
single thread apartment (STA) mode before OLE calls can be made.
Ensure that your Main function has STAThreadAttribute marked on it.
This exception is only raised if a debugger is attached to the process.
at System.Windows.Forms.FileDialog.RunDialog(IntPtr hWndOwner)
at System.Windows.Forms.CommonDialog.ShowDialog(IWin32Window owner)
可以通过在 STA 模式下创建一个新线程来轻松解决此问题,该线程调用打开或保存文件对话框。 但是,如果您的应用程序应该在多个显示设备上工作,那么下一个问题就会出现。 您会注意到,您无法像使用常见的 winforms 窗体实例那样设置打开文件对话框的父级。
那么,如何解决这个问题呢? 嗯,我首先想到的是使用默认的 Win32
方法,通过使用以下 PInvokes 来设置这些对话框的位置
[DllImport("User32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern bool GetWindowRect(IntPtr handle, ref RECT r);
[DllImport("user32.dll", ExactSpelling = true, CharSet = CharSet.Auto)]
public static extern IntPtr GetParent(IntPtr hWnd);
[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool SetWindowPos(IntPtr hWnd,
IntPtr hWndInsertAfter, int x, int y, int cx, int cy, uint uFlags);
可以在任何 Win32
对话框实例上设置窗口的位置。 但是,一旦您仔细查看打开/保存文件对话框的不同成员和属性,您会注意到这些对话框没有 Handle
成员,该成员是底层 Win32
对话框实例的 IntPtr
,就像常见的 System.Windows.Forms
对话框中一样。
但是使用 static PInvoke
方法并不是真正的 .NET 面向对象,对吧? 如果您的应用程序是一个 MDI 应用程序,事情会变得有点混乱,因为您需要所有这些对话框实例的对话框的 IntPtr
才能使用 Win32 PInvokes
方法。
所以我决定创建两个类,CFileOpenDlgThreadApartmentSafe
和 CFileSaveDlgThreadApartmentSafe
,使用名为 CFileDlgBase
的基类,其中包含文件对话框的通用方法和成员。
我的目标是拥有对话框类
- 具有与默认 .NET 对话框相当的属性
- 可从 STA 和 MTA 线程调用
- 具有像原始对话框一样的模态行为
- 不使用
static PInvoke
方法
这些类可以在 FileDialogsThreadAppartmentSafe
程序集中找到。
如何使用代码
在您的项目中引用程序集 FileDialogsThreadAppartmentSafe.dll 并按以下方式使用这些类
CFileOpenDlgThreadApartmentSafe dlg = new CFileOpenDlgThreadApartmentSafe();
dlg.Filter = "Text file (*.txt)|*.txt";
dlg.DefaultExt = "txt";
Point ptStartLocation = new Point(this.Location.X, this.Location.Y);
dlg.StartupLocation = ptStartLocation;
DialogResult res = dlg.ShowDialog();
if (res != System.Windows.Forms.DialogResult.OK)
return;
MessageBox.Show(string.Format("Open file {0}", dlg.FilePath));
第二个项目是示例,其中使用了两个对话框以及原始基本实现。
在 Program.cs 文件中的第 13 行,您可以看到 main 方法被标记为 [MTAThread]
。 这就是为什么当您单击标记为 Not Safe 的按钮时,您会收到上面显示的异常。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
namespace FileDialogTest
{
static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[MTAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Form1());
}
}
}
关注点
实现中最有趣的部分是两个类的 ShowDialog()
方法的调用。 此方法定义为
public virtual DialogResult ShowDialog()
在 CFileDlgBase
基类中。
这是 CFileOpenDlgThreadApartmentSafe
类中 ShowDialog
方法的实现。
public override DialogResult ShowDialog()
{
DialogResult dlgRes = DialogResult.Cancel;
Thread theThread = new Thread((ThreadStart)delegate
{
OpenFileDialog ofd = new OpenFileDialog();
ofd.Multiselect = false;
ofd.RestoreDirectory = true;
if (!string.IsNullOrEmpty(this.FilePath))
ofd.FileName = this.FilePath;
if (!string.IsNullOrEmpty(this.Filter))
ofd.Filter = this.Filter;
if (!string.IsNullOrEmpty(this.DefaultExt))
ofd.DefaultExt = this.DefaultExt;
if (!string.IsNullOrEmpty(this.Title))
ofd.Title = this.Title;
if (!string.IsNullOrEmpty(this.InitialDirectory))
ofd.InitialDirectory = this.InitialDirectory;
//Create a layout dialog instance on the current thread to align the file dialog Form
frmLayout = new Form();
if (this.StartupLocation != null)
{ //set the hidden layout form to manual form start position
frmLayout.StartPosition = FormStartPosition.Manual;
//set the location of the form
frmLayout.Location = this.StartupLocation;
frmLayout.DesktopLocation = this.StartupLocation;
}
//the layout form is not visible
frmLayout.Width = 0;
frmLayout.Height = 0;
dlgRes = ofd.ShowDialog(frmLayout);
if (dlgRes == DialogResult.OK)
this.FilePath = ofd.FileName;
});
try
{
//set STA as the Open file dialog needs it to work
theThread.TrySetApartmentState(ApartmentState.STA);
//start the thread
theThread.Start();
// Wait for thread to get started
while (!theThread.IsAlive) { Thread.Sleep(1); }
// Wait a tick more (@see: http://scn.sap.com/thread/45710)
Thread.Sleep(1);
//wait for the dialog thread to finish
theThread.Join();
DialogSuccess = true;
}
catch (Exception err)
{
DialogSuccess = false;
}
return (dlgRes);
}
该方法在单线程单元模式下启动一个新线程,并创建一个不可见的对话框实例,用作 Win32
文件对话框的父级。 这样,我们就不需要在 IntPtr
上工作,也不需要在不同线程上创建的实例上工作。 显示对话框后,该方法会等待对话框线程使用线程的 Join
方法完成。 即使在新的线程实例上创建了真正的文件对话框,调用线程的阻塞也会产生模态对话框行为。