65.9K
CodeProject 正在变化。 阅读更多。
Home

轻松扩展 OpenFileDialog 和 SaveFileDialog

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.82/5 (80投票s)

2007年7月11日

CPOL

10分钟阅读

viewsIcon

1561919

downloadIcon

13412

使用用户控件自定义 OpenFileDialog 和 SaveFileDialog。

Screenshot - saveas.jpg

目录

引言

如果您使用 WinForms,可能在某个时候您想要扩展 OpenFileDialogSaveFileDialog,但因为没有简单的方法而放弃了,尤其是当您想添加一些新的图形元素时。即使在 MFC 时代,这样的任务也不像 .NET 这样令人望而生畏,因为这些类是密封的,只暴露 16 个属性、5 个方法和 3 个事件用于自定义对话框。 Martin Parry 在 MSDN 上的文章可以帮助您了解如何使用 OFNHookProc 函数和 PInvoke 来自定义 OpenFileDialog。这似乎意味着我们要回到 20 世纪 90 年代初的编程方式,并添加相当多的 PInvoke 和封送处理才能做任何事情。这足以让人放弃或寻找第三方库等替代方案。如果您正在为 WPF 而不是 Windows Forms 开发,我建议您直接跳转到 我关于同一主题的 WPF 文章。然而,CastorTiu 的文章让那些选择使用 Forms 自定义这些对话框的人的生活变得更轻松。利用他在这一主题上的出色工作,我尝试更进一步,使这两个对话框的自定义更加轻松。我将只关注他对原始辛勤工作的重构和改进,因此如果您需要详细信息,请查看 CastorTiu 的文章。虽然本文仅使用 C# 代码片段,但我已将等效的 VB.NET 代码包含在可下载的 zip 文件中,供 VB 用户使用。

此控件的新增功能

为了尽可能轻松地进行扩展,我在基类控件中添加了一些额外的属性和事件,以及一些设计特性。最受欢迎的属性可能是 FileDlgType,它允许在设计时选择 OpenFileDialogSaveFileDialog。额外的属性和事件显示在下面

属性 事件

设计时的改进

最好能有一些视觉提示来表明控件的外观。我不想深入研究 设计时架构,所以我使用了一个简单的 OnPaint 重写来绘制一个红线或一个点,表示 FileDialog 接触到扩展。请注意,它仅在设计模式下绘制。

protected override void OnPaint(PaintEventArgs e)
{
    if (DesignMode)
    {
        Graphics gr = e.Graphics;
        {
            HatchBrush hb = null;
            Pen p = null;
            try
            {
                switch (this.FileDlgStartLocation)
                {
                    case AddonWindowLocation.Right:
                        hb = new System.Drawing.Drawing2D.HatchBrush
                      (HatchStyle.NarrowHorizontal, Color.Black, Color.Red);
                        p = new Pen(hb, 5);
                        gr.DrawLine(p, 0, 0, 0, this.Height);
                        break;
                    case AddonWindowLocation.Bottom:
                        hb = new System.Drawing.Drawing2D.HatchBrush
                      (HatchStyle.NarrowVertical, Color.Black, Color.Red);
                        p = new Pen(hb, 5);
                        gr.DrawLine(p, 0, 0, this.Width, 0);
                        break;
                    case AddonWindowLocation.BottomRight:
                    default:
                        hb = new System.Drawing.Drawing2D.HatchBrush
                    (HatchStyle.Sphere, Color.Black, Color.Red);
                        p = new Pen(hb, 5);
                        gr.DrawLine(p, 0, 0, 4, 4);
                        break;
                }
            }
            finally
            {
                if (p != null)
                    p.Dispose();
                if (hb != null)
                    hb.Dispose();
            }
        }
    }
    base.OnPaint(e);
}

工作原理

CastorTiu 的文章详细介绍了该控件的工作原理。我做了一些改进,但总体思路是一样的。

您需要在对话框变为模态之前获取其句柄,以便您可以将对话框与您自己的控件“粘合”在一起。

这是此复合对话框初始化的流程

  1. 使用其构造函数创建对话框。此时,仍然没有 UI,因此没有 Windows 消息可以 捕获
  2. 使用 FileDialog 的虚拟 OnPrepareMSDialog() 和控件本身的 Load 事件设置您想在运行时更改的属性
  3. 使用 DialogResult 返回值并处置控件

这是幕后发生的事情:您首先使用正确的 FileDialog 类型作为泛型参数创建 DialogWrapper 帮助器类

public DialogResult ShowDialog(IWin32Window owner)
{
    DialogResult returnDialogResult = DialogResult.Cancel;
    if (this.IsDisposed)
        return returnDialogResult;
    if (owner == null || owner.Handle == IntPtr.Zero)
    {
        WindowWrapper wr = new WindowWrapper
	(System.Diagnostics.Process.GetCurrentProcess().MainWindowHandle);
        owner = wr;
    }
    OriginalCtrlSize = this.Size;
    _MSdialog = (FileDlgType == 
	FileDialogType.OpenFileDlg)?new OpenFileDialog()as 
	FileDialog:new SaveFileDialog() as FileDialog;
    _dlgWrapper = new WholeDialogWrapper(this);
    OnPrepareMSDialog();
    if (!_hasRunInitMSDialog)
        InitMSDialog();
    try
    {
        System.Reflection.PropertyInfo AutoUpgradeInfo = 
		MSDialog.GetType().GetProperty("AutoUpgradeEnabled");
        if (AutoUpgradeInfo != null)
            AutoUpgradeInfo.SetValue(MSDialog, false, null);
        returnDialogResult = _MSdialog.ShowDialog(owner);
    }
    catch (ObjectDisposedException)
    {
    }
    catch (Exception ex)
    {
        MessageBox.Show("unable to get the modal dialog handle", ex.Message);
    }
    return returnDialogResult;
}

当您调用控件的 ShowDialog public 方法时,消息只有在您调用 .NET 的 Open(Save)FileDialog.ShowDialog() 后才会开始流动。您必须监视 WM_ACTIVATE,由于应用程序过滤器无法捕获它,因此您必须依赖父窗口的 WndProc。我发现使用一个消息循环窗口比使用一个虚拟表单效果更好,开销也更小。我在 WholeDialogWrapper 的构造函数中调用 AssignDummyWindow() 来创建它。

private void AssignDummyWindow()
{
    _hDummyWnd = NativeMethods.CreateWindowEx(0, "Message",
        null, WS_VISIBLE, 0, 0, 0, 0,HWND_MESSAGE, NULL, NULL, NULL);
    if (_hDummyWnd == NULL || !NativeMethods.IsWindow(_hDummyWnd))
        throw new ApplicationException("Unable to create a dummy window");
   AssignHandle(_hDummyWnd);
}

由于我们有这个窗口可以监听其子窗口,我们将如下捕获 WM_ACTIVATE

protected override void WndProc(ref Message m)
{
  switch ((Msg)m.Msg)
  {//.... code omitted
   case Msg.WM_ACTIVATE:
   if (_WatchForActivate && !mIsClosing && m.Msg == (int)Msg.WM_ACTIVATE)
                        //WM_NCACTIVATE works too
   {  //Now the Open/Save Dialog is visible and about to enter the modal loop
      _WatchForActivate = false;
      //Now we save the real dialog window handle
      _FileDialogHandle = m.LParam;
      ReleaseHandle();//release the dummy window
      AssignHandle(_FileDialogHandle);//assign the native open file handle
                                // to grab the messages
      NativeMethods.GetWindowRect(_FileDialogHandle,
                    ref _CustomControl._DialogWindowRect);
      _CustomControl._FileDialogHandle = _FileDialogHandle;
    }
    break;
   //.... code omitted
  }
  base.WndProc(ref m);
}

一旦我们获得了真实的对话框句柄 _FileDialogHandle,我们就可以忘记虚拟窗口并开始监听真正重要的事情。注意我如何释放虚拟窗口句柄并分配新的句柄。当同一个 WndProc 捕获 WM_SHOWWINDOW 消息时,我们终于可以排列我们的控件并设置父窗口了

private void InitControls()
{
    mInitializated = true;

    // Lets get information about the current open dialog
    NativeMethods.GetClientRect(new HandleRef(this,_hFileDialogHandle), 
				ref _DialogClientRect);
    NativeMethods.GetWindowRect(new HandleRef(this,_hFileDialogHandle), 
				ref _DialogWindowRect);

    // Lets borrow the Handles from the open dialog control
    PopulateWindowsHandlers();

    switch (_CustomControl.FileDlgStartLocation)
    {
        case AddonWindowLocation.Right:
            // Now we transfer the control to the open dialog
            _CustomControl.Location = new Point((int)
		(_DialogClientRect.Width - _CustomControl.Width), 0);
            break;
        case AddonWindowLocation.Bottom:
            // Now we transfer the control to the open dialog
            _CustomControl.Location = new Point(0, 
		(int)(_DialogClientRect.Height - _CustomControl.Height));
            break;
        case AddonWindowLocation.BottomRight:
            // We don't have to do too much in this case, just the default thing
            _CustomControl.Location = 
		new Point((int)(_DialogClientRect.Width - _CustomControl.Width), 
		(int)(_DialogClientRect.Height - _CustomControl.Height));
            break;
    }
    // Everything is ready, now lets change the parent
    NativeMethods.SetParent(new HandleRef(_CustomControl,_CustomControl.Handle), 
		new HandleRef(this,_hFileDialogHandle));

    // Send the control to the back
    // NativeMethods.SetWindowPos(_CustomControl.Handle, 
	(IntPtr)ZOrderPos.HWND_BOTTOM, 0, 0, 0, 0, UFLAGSZORDER);
    _CustomControl.MSDialog.Disposed += new EventHandler(DialogWrappper_Disposed);
}

事件如何连接

您可能认为此代码在属性和事件方面是详尽的。嗯……不完全是!我在原始工作的基础上添加了几个属性和事件,但您可能仍然想添加更多。

在上面的代码片段中,您可以看到如何连接 Open(Save)FileDialog.Dispose 中的事件,但这很简单。我将基于我添加的示例,描述如何将新事件添加到 FileDialogControlBase
还有一个名为 MSFileDialogWrapper 的辅助类,它通过 WndProc 监视 Open(Save)FileDialog 对象,如下所示

protected override void WndProc(ref Message m)
{
    switch ((Msg)m.Msg)
    {
        case Msg.WM_NOTIFY:
            OFNOTIFY ofNotify = (OFNOTIFY)Marshal.PtrToStructure
                    (m.LParam, typeof(OFNOTIFY));
            switch (ofNotify.hdr.code)
            {
                //.... code omitted
                case (uint)DialogChangeStatus.CDN_TYPECHANGE:
                    {
                        OPENFILENAME ofn =
            (OPENFILENAME)Marshal.PtrToStructure
            (ofNotify.OpenFileName, typeof(OPENFILENAME));
                        int i = ofn.nFilterIndex;
                        if (_CustomCtrl != null && _filterIndex != i)
                        {
                            _filterIndex = i;
                            _CustomCtrl.OnFilterChanged
                    (this as IWin32Window, i);
                        }
                    }
                    break;
            }
        //.... code omitted
    }
    base.WndProc(ref m);
}

一旦我们将内部指针封送回正确的结构,我们就调用 FileDialogControBase 对象的 OnFilterChanged 方法,在本例中是 _CustomCtrl。如果深入研究此方法,我们会发现以下内容

internal void OnFilterChanged(IWin32Window sender, int index)
{
    if (EventFilterChanged != null)
        EventFilterChanged(sender, index);
}

快速查看 EventFilterChanged 定义会发现它是一个事件。

public delegate void PathChangedEventHandler(IWin32Window sender,
                                            string filePath);
public delegate void FilterChangedEventHandler(IWin32Window sender, int index);
public event PathChangedEventHandler EventFileNameChanged;
public event PathChangedEventHandler EventFolderNameChanged;
public event FilterChangedEventHandler EventFilterChanged;
public event CancelEventHandler EventClosingDialog;

这使得添加和删除它们非常容易,就像处理常规 WinForms 事件一样。

如何添加新属性

要设置 Open(Save)FileDialog 对象的属性,您必须重写 OnPrepareMSDialog(),如果设计时设置不正确。但是,更改 Open(Save)FileDialog 的外观更复杂。例如,我将展示如何更改“确定”按钮上的文本 - 即“保存”或“打开”按钮。我们首先从 FileDialogControlBase 暴露该属性,然后使用 PInvoke 设置文本,如下所示

[DefaultValue("&Open")]
public string FileDlgOkCaption
{
    get { return _OKCaption; }
    set { _OKCaption = value; }
//........................

protected override void OnLoad(EventArgs e)
{
    base.OnLoad(e);
    if (!DesignMode)
    {
        if (MSDialog != null)
        {
            MSDialog.FileOk += new CancelEventHandler
                    (FileDialogControlBase_ClosingDialog);
            MSDialog.Disposed += new EventHandler
                    (FileDialogControlBase_DialogDisposed);
            MSDialog.HelpRequest += new EventHandler
                    (FileDialogControlBase_HelpRequest);
            NativeMethods.SetWindowText(_dlgWrapper.Handle, _Caption);
            //will work only for open dialog, save dialog will not update
            NativeMethods.SetWindowText(_hOKButton, _OKCaption);
        }
    }
}

正如您在上面的代码注释中看到的,我们意识到我们正在处理一个黑匣子,有时我们无法知道更新的正确方法。这就是我们可以在 OpenFileDialog 上更改但不能在 SaveFileDialog 上更改的情况之一。如果您想知道我们如何获得 _hOKButton_dlgWrapper.Handle,您需要查看 FileDialogEnumWindowCallBack,当 WM_SHOWWINDOWDialogWrapperWndProc 中捕获时,它会被调用。

private bool FileDialogEnumWindowCallBack(IntPtr hwnd, int lParam)
{
    StringBuilder className = new StringBuilder(256);
    NativeMethods.GetClassName(new HandleRef(this,hwnd), className, className.Capacity);
    int controlID = NativeMethods.GetDlgCtrlID(hwnd);
    WINDOWINFO windowInfo;
    NativeMethods.GetWindowInfo(new HandleRef(this,hwnd), out windowInfo);
    // Dialog Window
    if (className.ToString().StartsWith("#32770"))
    {
        _BaseDialogNative = new MSFileDialogWrapper(_CustomControl);
        _BaseDialogNative.AssignHandle(hwnd);
        return true;
    }
    switch ((ControlsId)controlID)
    {
    //.....code omitted
        case ControlsId.ButtonOk:
            _OKButton = hwnd;
            _OKButtonInfo = windowInfo;
            _CustomControl._hOKButton = hwnd;
            break;
    //.....code omitted
    }
}

如何在 Windows 2000 和 XP 上为 OpenFileDialog 和 SaveFiledialog 添加“常用位置”栏

在 .NET 3.5 为 Windows Vista 及更高版本引入 FileDialogCustomPlacesCollection 之前,据我所知,没有 API 允许您修改对话框上的“常用位置”栏。在 Windows 2000 和 XP 上有一个方法可以做到这一点,正如 Dino Esposito 在他的 MSDN 文章中所述。这需要修改注册表,并且只要用户登录,就会影响所有这些对话框的实例。由于这更像是一个本机对话框功能,我使用了应用于基类 FileDialog扩展方法。下面显示了 static 类,它公开了两个 public 方法来设置常用位置和恢复注册表。

public static class FileDialogPlaces
{
    private static readonly string TempKeyName = 
		"TempPredefKey_" + Guid.NewGuid().ToString();
    private const string Key_PlacesBar = 
	@"Software\Microsoft\Windows\CurrentVersion\Policies\ComDlg32\PlacesBar";
    private static RegistryKey _fakeKey;
    private static IntPtr _overriddenKey;
    private static object[] m_places;

    public static void SetPlaces(this FileDialog fd, object[] places)
    {
        if (fd == null)
            return;
        if (m_places == null)
            m_places = new object[5];
        else
            m_places.Initialize();

        if (places != null)
        {
            for (int i = 0; i < m_places.GetLength(0); i++)
            {
                m_places[i] = places[i];
            }
        }
        if (_fakeKey != null)
            ResetPlaces(fd);
        SetupFakeRegistryTree();
        if (fd != null)
            fd.Disposed += (object sender, EventArgs e) => 
		{ if (m_places != null && fd != null) ResetPlaces(fd); };
    }

    static public void ResetPlaces(this FileDialog fd)
    {
        if (_overriddenKey != IntPtr.Zero)
        {
            ResetRegistry(_overriddenKey);
            _overriddenKey = IntPtr.Zero;
        }
        if (_fakeKey != null)
        {
            _fakeKey.Close();
            _fakeKey = null;
        }
        //delete the key tree
        Registry.CurrentUser.DeleteSubKeyTree(TempKeyName);
        m_places = null;
    }

    private static void SetupFakeRegistryTree()
    {
        _fakeKey = Registry.CurrentUser.CreateSubKey(TempKeyName);
        _overriddenKey = InitializeRegistry();
        // write dynamic places here reading from Places
        RegistryKey reg = Registry.CurrentUser.CreateSubKey(Key_PlacesBar);
        for (int i = 0; i < m_places.GetLength(0); i++)
        {
            if (m_places[i] != null)
            {
                reg.SetValue("Place" + i.ToString(), m_places[i]);
            }
        }
    }

    static readonly UIntPtr HKEY_CURRENT_USER = new UIntPtr(0x80000001u);
    private static IntPtr InitializeRegistry()
    {
        IntPtr hkMyCU;
        NativeMethods.RegCreateKeyW(HKEY_CURRENT_USER, TempKeyName, out hkMyCU);
        NativeMethods.RegOverridePredefKey(HKEY_CURRENT_USER, hkMyCU);
        return hkMyCU;
    }

    static void ResetRegistry(IntPtr hkMyCU)
    {
        NativeMethods.RegOverridePredefKey(HKEY_CURRENT_USER, IntPtr.Zero);
        NativeMethods.RegCloseKey(hkMyCU);
        return;
    }
}

SetPlaces 接受一个最多包含五个对象的数组作为参数,这些对象可以是数字(特殊文件夹)或字符串(常规文件夹)。设置在 FileDialog 上的 Disposed 事件会自动调用 ResetPlaces 来恢复注册表。为了方便起见,我包含了 Places 辅助枚举,用于预定义的特殊文件夹。请注意,这在 Vista 或更高版本的 Windows OS 上可能会因 UAC 而失败。
如果您在 Vista 或更高版本上运行您的应用程序,请忽略此类,而是使用 Microsoft 的 FileDialogCustomPlacesCollection 类,或者更好的是,使用一些逻辑根据 操作系统版本 来选择方法。

使用控件

现在,让我们开始使用它。要开始使用,您可以将代码放入您的项目中,或者只是将对 FileDlgExtenders.dll 程序集的引用添加到 FileDlgExtenders 项目中。如果您选择后者,请先构建解决方案,因为在设计时需要基类。为了尽可能简单,选择“将用户控件添加到项目”,然后选择“继承的用户控件”,最后从列表中选择 FileDialogControlBase。例如,我添加了一个名为 MySaveDialogControl 的控件,它只是将图像转换为所需尺寸、方向和文件格式的缩略图。接下来,您可能会在设计模式下设置属性和事件。有两种方法可以显示该控件。

通过调用常规方法 ShowDialog 显示

要在运行时设置 FileDialog 的数据,请在您的子类中重写 virtual 方法 OnPrepareMSDialog()。您应该首先调用 base.OnPrepareMSDialog(),这样您自己的更改就不会被清除。下面是一个示例,说明我如何在派生控件中更改运行时 FileDialog 的属性。

protected override void OnPrepareMSDialog()
{
    base.FileDlgInitialDirectory = Environment.GetFolderPath
				(Environment.SpecialFolder.MyPictures);
    if (Environment.OSVersion.Version.Major < 6)
    	MSDialog.SetPlaces( new object[] { @"c:\", 
	(int)Places.MyComputer, (int)Places.Favorites, (int)Places.Printers, 
	(int)Places.Fonts, });
    base.OnPrepareMSDialog();
}

如果您选择重写 OnLoad,也需要采取类似的预防措施。始终调用 base.OnLoad,否则 Load 事件将不会被调用。提示:如果 Visual Studio 无法呈现新控件,请清理解决方案并重新生成。如果仍然不起作用,您可能需要手动清除 bin 文件夹,并检查您在设计时初始化的对象。
最后,这是调用者显示的示例

using (MySaveDialogControl saveDialog = new MySaveDialogControl(lblFilePath.Text, this))
{
    if (saveDialog.ShowDialog(this) == DialogResult.OK)
    {
        lblFilePath.Text = saveDialog.MSDialog.FileName;
    }
}

通过调用 Extensions 类的扩展方法 ShowDialog 显示

有些人可能更喜欢扩展方法 ShowDialog 提供的语法糖

public static class Extensions
{
    public static DialogResult ShowDialog
	(this FileDialog fdlg, FileDialogControlBase ctrl, IWin32Window owner)
   {
        ctrl.FileDlgType =(fdlg is SaveFileDialog)?
		FileDialogType.SaveFileDlg: FileDialogType.OpenFileDlg;
        if (ctrl.ShowDialogExt(fdlg, owner) == DialogResult.OK)
            return DialogResult.OK;
        else
            return DialogResult.Ignore;
    }
}

您将不再需要关心重写 OnPrepareMSDialog(),但您必须在调用者代码中以编程方式在运行时设置 FileDialog 成员。

using (MyOpenFileDialogControl openDialogCtrl = new MyOpenFileDialogControl())
{
    openDialogCtrl.FileDlgInitialDirectory = 
	Environment.GetFolderPath(Environment.SpecialFolder.MyPictures);
    OpenFileDialog openDialog = new OpenFileDialog();
    openDialog.InitialDirectory = Environment.GetFolderPath
		(Environment.SpecialFolder.MyPictures);
    openDialog.AddExtension = true;
    openDialog.Filter = "Image Files(*.bmp)|*.bmp 
	|Image Files(*.JPG)|*.JPG|Image Files(*.jpeg)|*.jpeg
	|Image Files(*.GIF)|*.GIF|Image Files(*.emf)|*emf.|
	Image Files(*.ico)|*.ico|Image Files(*.png)|*.png|
	Image Files(*.tif)|*.tif|Image Files(*.wmf)|*.wmf|Image Files(*.exif)|*.exif";
    openDialog.FilterIndex = 2;
    openDialog.CheckFileExists = true;
    openDialog.DefaultExt = "jpg";
    openDialog.FileName = "Select Picture";
    openDialog.DereferenceLinks = true;
    if (Environment.OSVersion.Version.Major < 6)
    	openDialog.SetPlaces(new object[] { @"c:\", 
	(int)Places.MyComputer, (int)Places.Favorites, 
	(int)Places.Printers, (int)Places.Fonts, });
    if (openDialog.ShowDialog(openDialogCtrl, this) == DialogResult.OK)
    {
        lblFilePath.Text = openDialog.FileName;
    }
}

历史

  • 如果忽略 其所基于的工作,这是 1.0 版本。对我来说,它在 32 位 Windows XP SP3 上运行良好。希望您会喜欢。
  • 版本 1.1 修复了与文件列表销毁相关的错误,并保持句柄和视图模式最新。我添加了一个新的设计时属性来启用/禁用 **OK** 按钮,称为 'FileDlgEnableOkBtn'。
    下面是一个根据转换结果使用此属性的示例
    private void MySaveDialogControl_FilterChanged
                    (IWin32Window sender, int index)
    {
        FileDlgEnableOkBtn = GetFormatFromIndex(index);
    }
  • 版本 1.2 根据查看者的反馈添加了多项修复。
    我认为最重要的更改如下
    • John Horigan:将 Vista/7 的 'AutoUpgradeEnabled' 属性设置为 false
    • LETRESTE Bruno:改进了显示对话框时的 autosize
    • 解决方案已转换为 Visual Studio 2008。VS 2005 的旧源代码可在 customFileDialog_old.zip 中找到
    • 如果您仍在使用 VS 2005,只需将新的 *.cs 文件复制到旧文件之上并打开解决方案/项目。应该可以工作。
    • 我还使用了一个自动化工具为同一解决方案创建了 VB 版本。
    • VB 源代码也包含在内,并且似乎可以正常工作。
  • 版本 1.3 修复了更多问题,感谢 beautyod 等观众
  • 我还为 Windows 2000 和 XP 添加了对“常用位置”栏的支持,这是一项新功能。
  • 为 VB 和 C# 提供了 VS 2008(.NET 3.5)和 2010(.NET 4.0)的解决方案文件。以前的源代码可在嵌入的 CustomFileDialog_src_old.zip 存档中找到。
  • 版本 1.4 结合了 Phil Atkin、John Simmons / outlaw programmer、neyerMat、kore_sar 等人提供的评论,对 Windows 7、64 位等进行了更多修复。
  • 版本 1.5 更改为 Visual Studio 2013 项目风格,并在 codeplex 上提供最新的源代码。
© . All rights reserved.