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

使用 C# 和 IMAPI2 刻录和擦除 CD/DVD/蓝光介质

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (196投票s)

2008年3月22日

CPOL

10分钟阅读

viewsIcon

1951594

downloadIcon

32782

在 C# 中使用 Image Mastering API。

引言

Windows 在发布 Vista 操作系统时引入了新的 IMAPIv2.0,相比原始 IMAPI 有了很大的改进。原始 IMAPI 对 CDROM 非常好,但存在一些巨大限制,例如无法写入 DVD 介质。我相信这个限制是因为在 2001 年 Windows XP 发布时,几乎没有人拥有 DVD 刻录机。IMAPIv2 允许您写入 CD、DVD 甚至蓝光介质,以及读取和写入 ISO 文件。IMAPIv2.0 存在一个问题,因为它仅随 Windows Vista 提供。但在 2007 年 6 月,微软发布了适用于 Windows XP 和 Windows 2003 的更新程序包。您可以在 此处 下载更新。

要使用 Windows 存储功能包 1.0 中添加的功能,您需要在此处 下载更新。Windows Vista Service Pack 2 已包含此更新。

我撰写本文是作为我 C++ 文章《使用 Image Mastering API 版本 2.0 刻录 CD/DVD 介质》的续篇。大多数 IMAPI2 示例似乎都是用脚本语言编写的。我找到的唯一 C# 示例是 Windows Vista SDK 随附的 **IBurn** 控制台项目,以及最近 dmihailescu 的文章:《如何使用 IMAPIv2.0 创建光盘映像文件》,该文章展示了如何创建 ISO 文件。

这篇文章比我想象的要困难。通常,.NET 应用程序应该更容易,但我需要弄清楚一些问题才能使其正常工作。如果您不想听我抱怨,可以跳过下一节。

问题

IMAPI2 是通过两个独立的 COM DLL 实现的:imapi2.dllimapi2fs.dllimapi2.dll 处理大多数设备和录制 API,而 imapi2fs.dll 处理所有文件系统和 IStream API。这可能看起来不是什么大问题,尤其是在使用 C++ 时。但对于 .NET 来说,这会成为一个巨大的问题,因为您需要将 IMAPI2FS 创建的 IStream 用于 IMAPI2 以写入介质。您最终会收到一个类似以下的错误消息:

Unable to cast object of type 'IMAPI2FS.FsiStreamClass' to type 'IMAPI2.IStream'

微软意识到了这个问题,并创建了一个名为 **IBurn** 的项目,并在 Windows Vista SDK 中发布了它。他们创建了一个 Interop 命名空间,将 IMAPI2 和 IMAPI2FS 的许多类、枚举和接口合并到一个命名空间中,位于一个名为 Interop.cs 的文件中。这解决了将 IMAPI2FS 的 IStream 无法强制转换为 IMAPI2 的问题。

不幸的是,他们的实现并没有完全解决我的问题。我遇到了其他 COM 问题,例如当我尝试使用返回 Array 的任何方法时,应用程序都会抛出异常。后来我发现,通过将 Array 更改为 object[] 可以解决此问题。当我尝试刻录多个 CD 时,我还遇到了空引用异常。微软承认 Interop.cs 并不是一个完整的解决方案,仅用于演示如何使用 C# 来使用 IMAPI2。

于是,我踏上了创建完整解决方案的征程。

我使用 **Microsoft Type Library to Assembly Converter**(tlbimp.exe)从 COM DLL 创建了两个 .NET Interop 程序集。我使用了以下命令创建了这两个程序集:

tlbimp imapi2.dll /out:imapi2int.dll
tlbimp imapi2fs.dll /out:imapi2fsint.dll

获得 .NET 程序集后,我使用 Reflector 反编译了互操作程序集,并创建了 C# 源代码。我合并了这两个文件,并对接口和辅助类进行了许多修改,并增加了对 IMAPI2 所有接口的支持。这听起来比实际操作简单得多。

我遇到的一个最大的问题,花了好长时间才弄明白,就是 getset 属性的顺序颠倒。Reflector 自动生成的代码总是将 get 放在 set 之前。IMAPI2 中的许多属性要求属性定义时 set 必须在 get 之前。如果顺序不正确,应用程序就会崩溃并立即退出。它不会抛出异常来提供任何关于问题的线索。它只会终止应用程序。所以,在我终于弄清楚问题所在后,我使用 OLE/COM 对象查看器打开了 COM DLL,并检查了每个属性的实际 TypeLib,确保它们的顺序正确。

我还选择不实现 Interop.cs 中的 AStream 接口。我直接使用 System.Runtime.InteropServices.ComTypes.IStream 接口。

最后,为了接收所有事件的通知,我不得不查阅 SDK,找到所有事件的 Dispatch ID。没有这些值,事件处理程序就无法接收通知。

新的 IMAPI2 互操作 - Imapi2interop.cs

我用来替换 Interop.cs 的是 Imapi2Interop.cs,包含在源代码中。它定义了以下类和接口:

  • IBootOptions - 指定要添加到光盘的启动映像
  • IEnumFsiItems - 枚举 FsiDirectoryItem 对象下的子目录和文件项
  • IEnumProgressItems - 枚举进度项集合
  • IFileSystemImageResult - 获取有关刻录映像、映像数据流和进度信息
  • IFsiDirectoryItem - 将项添加到文件系统映像或从中移除项
  • IFsiFileItem - 标识文件内容的文件大小和数据流
  • IDiscFormat2Data - 将数据流写入光盘
  • IDiscFormat2DataEventArgs - 检索有关当前写入操作的信息
  • IDiscFormat2Erase - 从光盘擦除数据
  • IDiscFormat2RawCD - 使用 Disc At Once (DAO) 模式将原始映像写入光盘设备
  • IDiscFormat2RawCDEventArgs - 检索有关当前写入操作的信息
  • IDiscFormat2TrackAtOnce - 以 Track-At-Once 模式将音频写入空白 CD-R 或 CD-RW 介质
  • IDiscFormat2TrackAtOnceEventArgs - 检索有关当前写入操作的信息
  • IDiscMaster2 - 枚举计算机上安装的 CD 和 DVD 设备
  • IDiscRecorder2 - 表示物理设备
  • IDiscRecorder2Ex - 检索 IDiscRecorder2 接口无法提供的信息
  • IProgressItem - 检索结果映像某个段的块信息
  • IProgressItems - 枚举结果映像中的进度项
  • IWriteEngine2 - 将数据流写入设备
  • IWriteEngine2EventArgs - 检索有关当前写入操作的信息
  • IWriteSpeedDescriptor - 检索刻录机和当前介质支持的详细写入配置

Windows 存储功能包 1.0 还添加了以下接口:

  • IBurnVerification - 设置刻录操作的验证级别
  • IFileSystemImage - 构建、导入和导出文件系统映像
  • IFileSystemImage2 - 通过写入多个启动条目或 EFI/UEFI 支持所需的启动映像来扩展 IFileSystemImage 接口
  • IFileSystemImage3 - 通过设置或检查 UDF 文件系统(2.50 及更高版本)的元数据和元数据镜像文件来确定冗余,从而扩展 IFileSystemImage2 接口
  • IFsiNamedStreams - 枚举文件系统映像中与文件关联的命名流
  • IIsoImageManager - 验证现有 ISO 文件是否包含可供刻录的有效映像
  • IMultiSession - 包含派生多会话接口的通用属性的基接口。
  • IMultiSessionSequential - 通过检索顺序录制的介质上先前导入会话的信息来扩展 IMultiSession 接口
  • IRawCDImageCreator - 创建原始 CD 映像以在 Disc-at-once 模式下写入。
  • IRawCDImageTrackInfo - 跟踪应用于 CD 介质的每轨属性

它还定义了以下事件:

  • DDiscFormat2DataEvents
    • DiscFormat2Data_EventHandler
  • DDiscFormat2EraseEvents
    • DiscFormat2Erase_EventHandler
  • DDiscFormat2RawCDEvents
    • DiscFormat2RawCD_UpdateEventHandler
  • DDiscFormat2TrackAtOnceEvents
    • DiscFormat2TrackAtOnce_EventHandler
  • DDiscMaster2Events
    • DiscMaster2_NotifyDeviceAddedEventHandler
    • DiscMaster2_NotifyDeviceRemovedEventHandler
  • DFileSystemImageEvents
    • DFileSystemImage_EventHandler
  • DWriteEngine2Events
    • DWriteEngine2_EventHandler

Using the Code

请确保 XP 和 2003 已安装本文开头提到的 IMAPI2 更新。

不要将 imapi2.dllimapi2fs.dll COM DLL 添加到您的项目中。这会导致上述问题。

imapi2interop.cs 文件添加到您的项目中,并在您的应用程序中定义命名空间。

using IMAPI2.Interop;

为了接收 COM 发送给事件处理程序的通知,您需要打开 AssemblyInfo.cs 文件并将 ComVisible 属性更改为 true

[assembly: ComVisible(true)]

确定介质类型

要确定介质类型和硬盘上的可用空间,请创建一个 MsftDiscFormat2Data 对象,并在 Recorder 属性中设置当前刻录机。然后,您可以从 IDiscFormat2Data CurrentPhysicalMediaType 属性获取介质类型。

获得介质类型后,创建一个 MsftFileSystemImage 对象,并使用介质类型调用 ChooseImageDefaultsForMediaType 方法。

要确定介质上是否已记录了任何会话,请检查 IDiscFormatData2 MediaHeuristicallyBlank 属性。

如果为 false,则表示已记录其他会话,您需要将 MsftFileSystemImageMultisessionInterfaces 属性设置为 IDiscFormat2Data MultisessionInterfaces 属性,然后调用 IDiscFormat2Data ImportFileSystem() 方法。

然后,通过将 MsftFileSystemImageFreeMediaBlocks 与扇区大小(2048)相乘来获取可用的介质块。如果介质上记录了以前的会话,则该空间将从介质的总大小中扣除。

private void buttonDetectMedia_Click(object sender, EventArgs e)
{
    if (devicesComboBox.SelectedIndex == -1)
    {
        return;
    }

    var discRecorder =
        (IDiscRecorder2)devicesComboBox.Items[devicesComboBox.SelectedIndex];

    MsftFileSystemImage fileSystemImage = null;
    MsftDiscFormat2Data discFormatData = null;

    try
    {
        //
        // Create and initialize the IDiscFormat2Data
        //
        discFormatData = new MsftDiscFormat2Data();
        if (!discFormatData.IsCurrentMediaSupported(discRecorder))
        {
            labelMediaType.Text = "Media not supported!";
            _totalDiscSize = 0;
            return;
        }
        else
        {
            //
            // Get the media type in the recorder
            //
            discFormatData.Recorder = discRecorder;
            IMAPI_MEDIA_PHYSICAL_TYPE mediaType = discFormatData.CurrentPhysicalMediaType;
            labelMediaType.Text = GetMediaTypeString(mediaType);

            //
            // Create a file system and select the media type
            //
            fileSystemImage = new MsftFileSystemImage();
            fileSystemImage.ChooseImageDefaultsForMediaType(mediaType);

            //
            // See if there are other recorded sessions on the disc
            //
            if (!discFormatData.MediaHeuristicallyBlank)
            {
                fileSystemImage.MultisessionInterfaces = 
				discFormatData.MultisessionInterfaces;
                fileSystemImage.ImportFileSystem();
            }

            Int64 freeMediaBlocks = fileSystemImage.FreeMediaBlocks;
            _totalDiscSize = 2048 * freeMediaBlocks;
        }
    }
    catch (COMException exception)
    {
        MessageBox.Show(this, exception.Message, "Detect Media Error",
            MessageBoxButtons.OK, MessageBoxIcon.Error);
    }
    finally
    {
        if (discFormatData != null)
        {
            Marshal.ReleaseComObject(discFormatData);
        }

        if (fileSystemImage != null)
        {
            Marshal.ReleaseComObject(fileSystemImage);
        }
    }

    UpdateCapacity();
}

将文件和目录添加到列表框

我创建了一个名为 IMediaItem 的通用接口。IMediaItem 包含三个属性和一个方法:

interface IMediaItem
{
    /// <summary>
    /// Returns the full path of the file or directory
    /// </summary>
    string Path { get; }

    /// <summary>
    /// Returns the size of the file or directory to the next largest sector
    /// </summary>
    Int64 SizeOnDisc { get; }

    /// <summary>
    /// Returns the Icon of the file or directory
    /// </summary>
    System.Drawing.Image FileIconImage { get; }

    // Adds the file or directory to the directory item, usually the root.
    bool AddToFileSystem(IFsiDirectoryItem rootItem);
}

对于文件项,我创建了 FileItem 类。此类通过 PInvoke 调用 SHCreateStreamOnFile Windows API 来创建一个 IStream,并将其添加到 IFsiDirectoryItem

对于目录项,我创建了 DirectoryItem 类。此类使用一种更简单的方法,通过调用 IFsiDirectoryItem.AddTree 方法将目录及其所有子目录添加到 IStream

这些类和接口位于 IMediaItem.cs 文件中。

创建映像

我在 MainForm 类中使用 CreateMediaFileSystem 方法来创建要写入介质的 IStream 映像。我遍历添加到文件 listbox 的文件和目录。

private bool CreateMediaFileSystem(IDiscRecorder2 discRecorder, out IStream dataStream)
{
    MsftFileSystemImage fileSystemImage = null;
    try
    {
        fileSystemImage = new MsftFileSystemImage();
        fileSystemImage.ChooseImageDefaults(discRecorder);
        fileSystemImage.FileSystemsToCreate =
            FsiFileSystems.FsiFileSystemJoliet | FsiFileSystems.FsiFileSystemISO9660;
        fileSystemImage.VolumeName = textBoxLabel.Text;

        fileSystemImage.Update +=
        new DFileSystemImage_EventHandler(fileSystemImage_Update);

        //
        // If multisessions, then import previous sessions
        //
        if (multisessionInterfaces != null)
        {
            fileSystemImage.MultisessionInterfaces = multisessionInterfaces;
            fileSystemImage.ImportFileSystem();
        }

        //
        // Get the image root
        //
        IFsiDirectoryItem rootItem = fileSystemImage.Root;

        //
        // Add Files and Directories to File System Image
        //
        foreach (IMediaItem mediaItem in listBoxFiles.Items)
        {
            //
            // Check if we've cancelled
            //
            if (backgroundBurnWorker.CancellationPending)
            {
                break;
            }

            //
            // Add to File System
            //
            mediaItem.AddToFileSystem(rootItem);
        }

        fileSystemImage.Update -=
        new DFileSystemImage_EventHandler(fileSystemImage_Update);

        //
        // did we cancel?
        //
        if (backgroundBurnWorker.CancellationPending)
        {
            dataStream = null;
            return false;
        }

        dataStream = fileSystemImage.CreateResultImage().ImageStream;
    }
    catch (COMException exception)
    {
        MessageBox.Show(this, exception.Message,
            "Create File System Error",
            MessageBoxButtons.OK, MessageBoxIcon.Error);
        dataStream = null;
        return false;
    }
    finally
    {
        if (fileSystemImage != null)
        {
            Marshal.ReleaseComObject(fileSystemImage);
        }
    }

    return true;
}

多线程

刻录或格式化介质可能需要一些时间,因此我们不希望在主 UI 线程上执行这些操作。我使用 BackgroundWorker 类来处理这些耗时任务的多线程。BackgroundWorker 类允许您在线程内设置值,然后调用 ReportProgress 方法,该方法会在调用线程中触发 ProgressChanged 事件。当您的工作线程完成后,它会触发 RunWorkerCompleted 事件,通知调用线程已完成。我不会详细介绍整个线程过程,因为这不是本文的主要内容。

将数据写入介质

我在 MainFormbackgroundBurnWorker_DoWork 中写入数据,这是 BackgroundWorker backgroundBurnWorkerDoWork 事件。

private void backgroundBurnWorker_DoWork(object sender, DoWorkEventArgs e)
{
    MsftDiscRecorder2 discRecorder = null;
    MsftDiscFormat2Data discFormatData = null;

    try
    {
        //
        // Create and initialize the IDiscRecorder2 object
        //
        discRecorder = new MsftDiscRecorder2();
        var burnData = (BurnData)e.Argument;
        discRecorder.InitializeDiscRecorder(burnData.uniqueRecorderId);

        //
        // Create and initialize the IDiscFormat2Data
        //
        discFormatData = new MsftDiscFormat2Data
            {
                Recorder = discRecorder,
                ClientName = ClientName,
                ForceMediaToBeClosed = _closeMedia
            };

        //
        // Set the verification level
        //
        var burnVerification = (IBurnVerification)discFormatData;
        burnVerification.BurnVerificationLevel = _verificationLevel;

        //
        // Check if media is blank, (for RW media)
        //
        object[] multisessionInterfaces = null;
        if (!discFormatData.MediaHeuristicallyBlank)
        {
            multisessionInterfaces = discFormatData.MultisessionInterfaces;
        }

        //
        // Create the file system
        //
        IStream fileSystem;
        if (!CreateMediaFileSystem(discRecorder, multisessionInterfaces, out fileSystem))
        {
            e.Result = -1;
            return;
        }

        //
        // add the Update event handler
        //
        discFormatData.Update += discFormatData_Update;

        //
        // Write the data here
        //
        try
        {
            discFormatData.Write(fileSystem);
            e.Result = 0;
        }
        catch (COMException ex)
        {
            e.Result = ex.ErrorCode;
            MessageBox.Show(ex.Message, "IDiscFormat2Data.Write failed",
                MessageBoxButtons.OK, MessageBoxIcon.Stop);
        }
        finally
        {
            if (fileSystem != null)
            {
                Marshal.FinalReleaseComObject(fileSystem);
            }
        }

        //
        // remove the Update event handler
        //
        discFormatData.Update -= discFormatData_Update;

        if (_ejectMedia)
        {
            discRecorder.EjectMedia();
        }
    }
    catch (COMException exception)
    {
        //
        // If anything happens during the format, show the message
        //
        MessageBox.Show(exception.Message);
        e.Result = exception.ErrorCode;
    }
    finally
    {
        if (discRecorder != null)
        {
            Marshal.ReleaseComObject(discRecorder);
        }

        if (discFormatData != null)
        {
            Marshal.ReleaseComObject(discFormatData);
        }
    }
}

进度更新事件

IDiscFormat2Data 支持通过 CancelWrite 方法取消操作。当我收到 IDiscFormatData2Update 事件时,我检查用户是否按下了“取消”按钮。如果用户已取消,则 BackgroundWorkerCancellationPending 属性将为 true,我将取消写入操作并立即返回。否则,我收集 IDiscFormat2DataEventArgs 对象中的数据,然后调用 backgroundBurnWorker.ReportProgress,以便 UI 线程可以更新数据和进度条。

void discFormatData_Update([In, MarshalAs(UnmanagedType.IDispatch)] object sender,
                           [In, MarshalAs(UnmanagedType.IDispatch)] objectprogress)
{
    //
    // Check if we've cancelled
    //
    if (backgroundBurnWorker.CancellationPending)
    {
        var format2Data = (IDiscFormat2Data)sender;
        format2Data.CancelWrite();
        return;
    }

    var eventArgs = (IDiscFormat2DataEventArgs)progress;

    _burnData.task = BURN_MEDIA_TASK.BURN_MEDIA_TASK_WRITING;

    // IDiscFormat2DataEventArgs Interface
    _burnData.elapsedTime = eventArgs.ElapsedTime;
    _burnData.remainingTime = eventArgs.RemainingTime;
    _burnData.totalTime = eventArgs.TotalTime;

    // IWriteEngine2EventArgs Interface
    _burnData.currentAction = eventArgs.CurrentAction;
    _burnData.startLba = eventArgs.StartLba;
    _burnData.sectorCount = eventArgs.SectorCount;
    _burnData.lastReadLba = eventArgs.LastReadLba;
    _burnData.lastWrittenLba = eventArgs.LastWrittenLba;
    _burnData.totalSystemBuffer = eventArgs.TotalSystemBuffer;
    _burnData.usedSystemBuffer = eventArgs.UsedSystemBuffer;
    _burnData.freeSystemBuffer = eventArgs.FreeSystemBuffer;

    //
    // Report back to the UI
    //
    backgroundBurnWorker.ReportProgress(0, _burnData);
}

格式化/擦除 RW 光盘

burnmedia_format.png

我在 MainFormbackgroundFormatWorker_DoWork 中格式化光盘,这是 BackgroundWorker backgroundFormatWorkerDoWork 事件。

private void backgroundFormatWorker_DoWork(object sender, DoWorkEventArgs e)
{
    MsftDiscRecorder2 discRecorder = null;
    MsftDiscFormat2Erase discFormatErase = null;

    try
    {
        //
        // Create and initialize the IDiscRecorder2
        //
        discRecorder = new MsftDiscRecorder2();
        var activeDiscRecorder = (string)e.Argument;
        discRecorder.InitializeDiscRecorder(activeDiscRecorder);

        //
        // Create the IDiscFormat2Erase and set properties
        //
        discFormatErase = new MsftDiscFormat2Erase
            {
                Recorder = discRecorder,
                ClientName = ClientName,
                FullErase = !checkBoxQuickFormat.Checked
            };

        //
        // Setup the Update progress event handler
        //
        discFormatErase.Update += discFormatErase_Update;

        //
        // Erase the media here
        //
        try
        {
            discFormatErase.EraseMedia();
            e.Result = 0;
        }
        catch (COMException ex)
        {
            e.Result = ex.ErrorCode;
            MessageBox.Show(ex.Message, "IDiscFormat2.EraseMedia failed",
                MessageBoxButtons.OK, MessageBoxIcon.Stop);
        }

        //
        // Remove the Update progress event handler
        //
        discFormatErase.Update -= discFormatErase_Update;

        //
        // Eject the media
        //
        if (checkBoxEjectFormat.Checked)
        {
            discRecorder.EjectMedia();
        }

    }
    catch (COMException exception)
    {
        //
        // If anything happens during the format, show the message
        //
        MessageBox.Show(exception.Message);
    }
    finally
    {
        if (discRecorder != null)
        {
            Marshal.ReleaseComObject(discRecorder);
        }

        if (discFormatErase != null)
        {
            Marshal.ReleaseComObject(discFormatErase);
        }
    }
}

历史

  • 2008年3月21日
    • 首次发布
  • 2008年3月25日 
    • 添加了 Visual Studio 2005 项目
  • 2008年3月29日 
    • 检测介质类型和大小,支持多会话
  • 2009年5月2日 
    • 增加了对 Windows 存储功能包的支持,并进行了错误修复
  • 2009年12月13日 
    • 移除了对 AcquireExclusiveAccessReleaseExclusiveAccess 方法的调用,因为它们不再需要
    • 调用 IDiscRecorder2.SupportedProfiles 来枚举支持的光盘类型
  • 2010年3月2日
    • 修复了 IDiscRecorder2Ex 类中一些不正确的参数
    • 修复了潜在的线程问题
    • 添加了图标
  • 2010年3月22日
    • 修复了 Leroe 发现的一个非常严重的 Windows XP 错误,即 MsftFileSystemImageClass 总是无必要地使用 IFileSystemImage3 接口,而使用 IFileSystemImage 接口就足够了。还进行了一些代码重构。
© . All rights reserved.