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

C# 中的 Outlook 拖放

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (89投票s)

2008年7月29日

CPOL

6分钟阅读

viewsIcon

655972

downloadIcon

12511

如何将多个 Outlook 邮件或邮件附件拖放到 C# WinForm 中。

Demo application form

引言

前几天公司有个项目需要将任意数量的 Outlook 邮件或邮件附件拖放到 WinForms 应用程序中……我心想这很简单,这一定是个常见问题。我可以直接去 CodeProject 找个例子,稍微修改一下就能快速搞定。现在,既然您在这里,我相信您一定知道关于这个主题的信息非常少,所以我决定为这个小范围的知识库贡献一个完整的示例,介绍如何在不使用 Outlook 对象模型的情况下,将邮件项或附件从 Outlook 拖放到 WinForms 应用程序中。

我将跳过讲解创建窗体、允许拖放等在互联网上随处可见的内容,只专注于让 Outlook 拖放生效的代码。如果您觉得迷茫,请去找另一篇关于更基础拖放信息的文章,先实现一个功能完整的拖放应用,然后再回来修改我的代码。

Using the Code

当我开始编写代码时,我决定最简单的方法是将此功能集成到现有应用程序中,那就是创建一个新类,实现通常在拖放到 WinForm 时提供的 IDataObject 接口。新类将捕获所有对 Outlook 特定数据格式的调用,并将所有其他调用传递给原始 IDataObject。您可以在下面的 DragDrop 事件处理程序中看到使用该类的简单方法。FileGroupDescriptor 格式返回一个包含每个文件名的字符串数组,而不是您自己处理时习惯的 MemoryStream,而 FileContents 则返回一个包含每个文件二进制内容的 MemoryStream 数组。

private void Form1_DragDrop(object sender, DragEventArgs e)
{
    //wrap standard IDataObject in OutlookDataObject
    OutlookDataObject dataObject = new OutlookDataObject(e.Data);
    
    //get the names and data streams of the files dropped
    string[] filenames = (string[])dataObject.GetData("FileGroupDescriptor");
    MemoryStream[] filestreams = (MemoryStream[])dataObject.GetData("FileContents");

    for (int fileIndex = 0; fileIndex < filenames.Length; fileIndex++)
    {
        //use the fileindex to get the name and data stream
        string filename = filenames[fileIndex];
        MemoryStream filestream = filestreams[fileIndex];

        //save the file stream using its name to the application path
        FileStream outputStream = File.Create(filename);
        filestream.WriteTo(outputStream);
        outputStream.Close();
    }
}

理解代码

要理解上面的 OutlookDataObject 类是如何获取文件信息的,有两点需要注意。第一,文件名信息是从 Outlook 返回的 MemoryStream 中获取的,实际上这是 FILEGROUPDESCRIPTORAFILEGROUPDESCRIPTORW 结构的表示。第二,要获取文件内容,您需要指定一个索引来获取除第一个文件之外的任何文件,而标准的 IDataObject 并没有直接暴露这个功能。所有这些将在下面详细解释。

获取文件名

IDataObjectFileGroupDescriptorFileGroupDescriptorW 格式中返回的文件详细信息有两个版本,分别对应 FILEGROUPDESCRIPTORAFILEGROUPDESCRIPTORW 结构。在本文中,我将重点介绍 FileGroupDescriptor 格式,这是 ASCII 版本;FileGroupDescriptorW(W 代表 wide)是 Unicode 版本,当处理非 ASCII 文件名时您需要使用它,但处理方式是相同的。

//use the IDataObject to get the FileGroupDescriptor as a MemoryStream
MemoryStream fileGroupDescriptorStream = (MemoryStream)e.Data.GetData("FileGroupDescriptor");

您看到的大多数示例都涉及获取上面的 MemoryStream,并将索引 76 之后的每个非空字节转换为字符并将其附加到字符串。虽然这对于单个文件拖放来说足够了,但当拖放多个文件时,情况会变得棘手。正确的方法是获取返回的字节,并将其转换为 FILEGROUPDESCRIPTORA 结构,该结构包含一个项计数和一个 FILEDESCRIPTORA 结构数组,后者包含文件详细信息。这些结构的定义如下。

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public sealed class FILEGROUPDESCRIPTORA
{
    public uint cItems;
    public FILEDESCRIPTORA[] fgd;
}

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public sealed class FILEDESCRIPTORA
{
    public uint dwFlags;
    public Guid clsid;
    public SIZEL sizel;
    public POINTL pointl;
    public uint dwFileAttributes;
    public System.Runtime.InteropServices.ComTypes.FILETIME ftCreationTime;
    public System.Runtime.InteropServices.ComTypes.FILETIME ftLastAccessTime;
    public System.Runtime.InteropServices.ComTypes.FILETIME ftLastWriteTime;
    public uint nFileSizeHigh;
    public uint nFileSizeLow;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
    public string cFileName;
}

基础知识讲完后,让我们开始编写代码,将返回的 MemoryStream 实际转换为可用的东西,例如文件名字符串数组。这涉及到将原始字节放入非托管内存,并使用 Marshal.PtrToStructure 将其转换回结构。代码中有一些额外的封送处理,因为 FILEGROUPDESCRIPTORA 结构的 fgd 数组不会被填充,因为 PtrToStructure 方法无法处理可变长度数组。

//use IDataObject to get FileGroupDescriptor
//as a MemoryStream and copy into a byte array
MemoryStream fgdStream = 
  (MemoryStream)e.Data.GetData("FileGroupDescriptor");
byte[] fgdBytes = new byte[fgdStream.Length];
fgdStream.Read(fgdBytes, 0, fgdBytes.Length);
fgdStream.Close();

//copy the file group descriptor into unmanaged memory
IntPtr fgdaPtr = Marshal.AllocHGlobal(fgdBytes.Length);
Marshal.Copy(fgdBytes, 0, fgdaPtr, fgdBytes.Length);

//marshal the unmanaged memory to a FILEGROUPDESCRIPTORA struct
object fgdObj = Marshal.PtrToStructure(fgdaPtr, 
                        typeof(NativeMethods.FILEGROUPDESCRIPTORA));
NativeMethods.FILEGROUPDESCRIPTORA fgd = 
             (NativeMethods.FILEGROUPDESCRIPTORA)fgdObj;

//create a array to store file names in
string[] fileNames = new string[fgd.cItems];

//get the pointer to the first file descriptor
IntPtr fdPtr = (IntPtr)((int)fgdaPointer + Marshal.SizeOf(fgdaPointer));

//loop for the number of files acording to the file group descriptor
for(int fdIndex = 0;fdIndex < fgd.cItems;fdIndex++)
{
    //marshal the pointer to the file descriptor as a FILEDESCRIPTORA struct
    object fdObj = Marshal.PtrToStructure(fdPtr, 
                          typeof(NativeMethods.FILEDESCRIPTORA));
    NativeMethods.FILEDESCRIPTORA fd = (NativeMethods.FILEDESCRIPTORA)fdObj;
    
    //get file name of file descriptor and put in array
    fileNames[fdIndex] = fd.cFileName;

    //move the file descriptor pointer to the next file descriptor
    fdPtr = (IntPtr)((int)fdPtr + Marshal.SizeOf(fd));
}

此时,我们已经将 MemoryStream 转换为一个字符串数组,其中包含每个拖放文件的名称,这比之前容易处理得多。Outlook 邮件会以其主题名加上 ".msg" 后缀的形式显示,而对于 Outlook 邮件附件,则显示附件的文件名。

获取文件内容

文件内容位于 FileContents 格式之后。如果您拖放单个附件,则默认的 IDataObject 会按预期工作,并返回一个包含该文件数据的 MemoryStream。由于多种原因,当拖放多个附件或 Outlook 电子邮件消息时,情况会变得更复杂。多个附件会造成问题,因为操作系统为拖放数据调用的允许指定索引,但 C# 对 IDataObject 的实现并未直接暴露此功能。邮件消息也是一个问题,因为操作系统调用返回一个 IStorage,它是一种复合文件类型,同样,C# 对 IDataObject 的实现也无法处理这种类型的返回,因此您会得到一个 null

指定索引

要访问多个拖放文件的内容,需要指定一个索引来指示需要哪个文件的内容。默认的 IDataObject 不允许这样做,但可以将其转换为 COM IDataObject,它将接受一个 FORMATETC 结构,该结构有一个索引属性,可以设置为指定所需的文件内容。

//cast the default IDataObject to a com IDataObject
System.Runtime.InteropServices.ComTypes.IDataObject comDataObject;
comDataObject = (System.Runtime.InteropServices.ComTypes.IDataObject)e.Data;

//create a FORMATETC struct to request the data with from the com IDataObject
FORMATETC formatetc = new FORMATETC();
formatetc.cfFormat = (short)DataFormats.GetFormat(format).Id;
formatetc.dwAspect = DVASPECT.DVASPECT_CONTENT;
formatetc.lindex = 0; //zero based index to retrieve
formatetc.ptd = new IntPtr(0);
formatetc.tymed = TYMED.TYMED_ISTREAM | TYMED.TYMED_ISTORAGE | TYMED.TYMED_HGLOBAL;

//create STGMEDIUM to output request results into
STGMEDIUM medium = new STGMEDIUM();

//using the com IDataObject interface get the data using the defined FORMATETC
comDataObject.GetData(ref formatetc, out medium);

如上例所示,通过更改 FORMATETC 结构中的 lindex 属性的值,我们可以更改要检索的文件内容的索引。调用的结果存储在 STGMEDIUM 结构中;它在 unionmember 属性中包含指向实际结果的指针,并在 tymed 属性中包含结果的类型。STGMEDIUM 提供三种返回类型,每种类型在下面都有解释。

流结果 (TYMED_ISTREAM)

如果 STGMEDIUMtymed 属性是 TYMED_ISTREAM,则结果是一个流。这通常由默认的 IDataObject 处理,但在使用 COM IDataObject 时,需要重新编写处理代码。

//marshal the returned pointer to a IStream object
IStream iStream = (IStream)Marshal.GetObjectForIUnknown(medium.unionmember);
Marshal.Release(medium.unionmember);

//get the STATSTG of the IStream to determine how many bytes are in it
iStreamStat = new System.Runtime.InteropServices.ComTypes.STATSTG();
iStream.Stat(out iStreamStat, 0);
int iStreamSize = (int)iStreamStat.cbSize;

//read the data from the IStream into a managed byte array
byte[] iStreamContent = new byte[iStreamSize];
iStream.Read(iStreamContent, iStreamContent.Length, IntPtr.Zero);

//wrapped the managed byte array into a memory stream
Stream filestream = new MemoryStream(iStreamContent);

存储结果 (TYMED_ISTORAGE)

如果 STGMEDIUMtymed 属性是 TYMED_ISTORAGE,则结果是一个存储,它是一种复合文件类型。这比流更复杂一些,因为它需要复制到一个内存后备的 IStorage 中,然后才能从后备内存存储中读取其数据。

NativeMethods.IStorage iStorage = null;
NativeMethods.IStorage iStorage2 = null;
NativeMethods.ILockBytes iLockBytes = null;
System.Runtime.InteropServices.ComTypes.STATSTG iLockBytesStat;
try
{
    //marshal the returned pointer to a IStorage object
    iStorage = (NativeMethods.IStorage)
       Marshal.GetObjectForIUnknown(medium.unionmember);
    Marshal.Release(medium.unionmember);

    //create a ILockBytes (unmanaged byte array)
    iLockBytes = NativeMethods.CreateILockBytesOnHGlobal(IntPtr.Zero, true);
    
    //create a IStorage using the ILockBytes 
    //(unmanaged byte array) as a backing store
    iStorage2 = NativeMethods.StgCreateDocfileOnILockBytes(iLockBytes, 
                                                       0x00001012, 0);

    //copy the returned IStorage into the new memory backed IStorage
    iStorage.CopyTo(0, null, IntPtr.Zero, iStorage2);
    iLockBytes.Flush();
    iStorage2.Commit(0);

    //get the STATSTG of the ILockBytes to determine 
    //how many bytes were written to it
    iLockBytesStat = new System.Runtime.InteropServices.ComTypes.STATSTG();
    iLockBytes.Stat(out iLockBytesStat, 1);
    int iLockBytesSize = (int)iLockBytesStat.cbSize;

    //read the data from the ILockBytes 
    //(unmanaged byte array) into a managed byte array
    byte[] iLockBytesContent = new byte[iLockBytesSize];
    iLockBytes.ReadAt(0, iLockBytesContent, iLockBytesContent.Length, null);

    //wrapped the managed byte array into a memory stream
    Stream filestream = new MemoryStream(iStreamContent);
}
finally
{
    //release all unmanaged objects
    Marshal.ReleaseComObject(iStorage2);
    Marshal.ReleaseComObject(iLockBytes);
    Marshal.ReleaseComObject(iStorage);
}

HGlobal 结果 (TYMED_HGLOBAL)

如果 STGMEDIUMtymed 属性是 TYMED_HGLOBAL,则结果存储在 HGlobal 中。对于 Outlook 拖放而言,这种类型不应返回,但为了完整性起见,我使用了一些反射来让原始 IDataObject 类处理它。

//get the internal ole dataobject and its GetDataFromHGLOBLAL method
BindingFlags bindingFlags = BindingFlags.NonPublic | BindingFlags.Instance;
FieldInfo innerDataField = 
  e.Data.GetType().GetField("innerData", bindingFlags);
IDataObject oleDataObject = 
  (System.Windows.Forms.IDataObject)innerDataField.GetValue(e.Data);
MethodInfo getDataFromHGLOBLALMethod = 
  oleDataObject.GetType().GetMethod("GetDataFromHGLOBLAL", bindingFlags);

getDataFromHGLOBLALMethod.Invoke(oleDataObject, 
  new object[] { format, medium.unionmember });

结论

好了,希望这些内容能帮到一些人。我还有一些其他的 Outlook 小技巧,我将写成文章介绍;一个是如何在不使用对象模型的情况下提取和保存邮件附件,另一个是如何使用本文中的代码实现 Outlook 邮件和附件到 IE 的拖放(需要适当的安全措施,所以只适用于内网)。

历史

  • 2008年7月1日
    • 原始文章。
© . All rights reserved.