.NET中文件对话框的线程安全使用

在.NET开发中,经常使用Win32的OpenFileDialog和SaveFileDialog来实现文件的打开和保存功能。这些对话框简单易用,并且能够自动适应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.

这个异常只会在调试器附加到进程时抛出。

解决方案

一个简单的解决方案是创建一个新的线程,将其公寓状态设置为STA,然后调用打开或保存文件对话框。但是,如果应用程序需要在多个显示设备上工作,会发现无法像使用WinForms窗体实例那样设置打开文件对话框的父窗口。

可以使用Win32API来设置这些对话框的位置。以下是一些使用PInvoke的方法:

[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对话框实例的位置。但是,如果仔细查看OpenFileDialog和SaveFileDialog的成员和属性,会发现这些对话框没有Handle成员,这是指向底层Win32对话框实例的IntPtr,就像在System.Windows.Forms对话框中一样。

为了解决这个问题,创建了两个类:CFileOpenDlgThreadApartmentSafe和CFileSaveDlgThreadApartmentSafe,它们都使用一个名为CFileDlgBase的基类,该基类包含文件对话框的公共方法和成员。

目标是创建具有以下特性的对话框类:

  • 与默认.NET对话框相似的属性
  • 可以从STA和MTA线程调用者调用
  • 具有原始对话框的模态行为
  • 不使用静态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的按钮时会收到上面显示的异常。

实现要点

实现中最有趣的部分是对ShowDialog方法的调用。这个方法在CFileDlgBase基类中定义为:

public virtual DialogResult 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 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); }
沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485