使用 Image Mastering API 2.0 (IMAPI2) 刻录 CD/DVD 介质






4.85/5 (75投票s)
使用 IMAPI2 的 CD/DVD 刻录应用程序示例。
引言
如果您正在寻找 C#/.NET 的示例,请查看我的文章:“使用 C# 和 IMAPI2 刻录和擦除 CD/DVD 介质”。
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 发布了更新包。您可以 在此处 下载更新。
您还需要下载并安装 Microsoft Windows 软件开发工具包 (SDK),以获取编译应用程序所需的头文件。您可以 在此处 下载 SDK。
下载并安装 SDK 后,您需要确保 SDK 的 include 目录在 Visual Studio 的 *Include* 路径中。在使用 Visual Studio 2005 时,您还必须将 SDK 的 *LIB* 路径作为 *LIB* 目录列表的第一个条目。
任何想要开发完整应用程序的人还应该阅读“Joliet 录制规范”。
Using the Code
我已经创建了几个围绕 IMAPIv2 接口的包装器,以协助管理接口实例
CDiscMaster
包装了 IDiscMaster2
接口,该接口允许您确定计算机是否安装了任何光驱,如果安装了,则允许您枚举计算机上安装的 CD 和 DVD 驱动器。
CDiscRecorder
包装了 IDiscRecorder2
接口,该接口代表每个物理驱动器。您可以使用此接口检索有关驱动器的信息,包括制造商信息、逻辑驱动器和支持的介质。
CDiscFormatData
包装了 IDiscFormat2Data
接口,该接口用于将数据写入介质。
CDiscFormatDataEvent
包装了 DDiscFormat2DataEvents
通知,用于接收 IDiscFormat2Data
写入函数的状态。
对于文件系统,我创建了一个基类 CBaseObject
,它具有三个函数:GetPath()
、GetName()
和 GetSizeOnDisc()
。GetPath
返回文件或目录在计算机上的完整路径。GetName
仅返回文件名或目录名,用于显示目的,也用作录制镜像根目录中文件或目录的名称。GetSizeOnDisc
是一个纯虚函数,它由派生自 CBaseObject
的两个类 CFileObject
和 CDirObject
实现。CFileObject
返回文件在介质上占用的空间。CDirObject
返回目录中所有文件和子目录的总大小。
我将在这里介绍几个要点,但您应该下载源代码来查看所有代码。
您需要在应用程序中包含 *imapi2* 头文件
#include <imapi2.h>
#include <imapi2error.h>
#include <imapi2fs.h>
#include <imapi2fserror.h>
需要 imapi2.h 和 imapi2error.h 来进行 imapi2
接口。需要 imapi2fs.h 和 imapi2fserror.h 来进行 imapi2
文件系统接口。
然后,我创建一个 CDiscMaster
类的实例,初始化它,并获取每个设备的唯一 ID。唯一 ID 用于初始化 CDiscRecord
对象,我使用该对象获取组合框条目的显示信息并存储项数据的指针。
CDiscMaster discMaster;
.
.
discMaster.Initialize();
.
.
long totalDevices = discMaster.GetTotalDevices();
for (long deviceIndex = 0; deviceIndex < totalDevices; deviceIndex++)
{
CString recorderUniqueID = discMaster.GetDeviceUniqueID(deviceIndex);
CDiscRecorder* pDiscRecorder = new CDiscRecorder();
pDiscRecorder->Initialize(recorderUniqueID);
//
// Get the volume path(s). usually just 1
//
CString volumeList;
ULONG totalVolumePaths = pDiscRecorder->GetTotalVolumePaths();
for (ULONG volIndex = 0; volIndex < totalVolumePaths; volIndex++)
{
if (volIndex)
volumeList += _T(",");
volumeList += pDiscRecorder->GetVolumePath(volIndex);
}
//
// Add Drive to combo and IDiscRecorder as data
//
CString productId = pDiscRecorder->GetProductID();
CString strName;
strName.Format(_T("%s [%s]"), (LPCTSTR)volumeList, (LPCTSTR)productId);
int comboBoxIndex = m_deviceComboBox.AddString(strName);
m_deviceComboBox.SetItemDataPtr(comboBoxIndex, pDiscRecorder);
}
当在设备组合框中选择一个项时,我获取所选设备的 CDiscRecorder
对象,该对象存储在项的数据中。然后,我使用 CDiscRecorder
对象获取支持的介质类型。支持的介质类型返回一个在 enum IMAPI_MEDIA_PHYSICAL_TYPE
类型中定义的整数。由于我得到的值如 IMAPI_MEDIA_TYPE_DVDPLUSR
和 IMAPI_MEDIA_TYPE_DVDDASHR
,我设置了三个成员变量(m_isCdromSupported
、m_isDvdSupported
和 m_isDualLayerDvdSupported
),如果任何一个系列介质受支持,则将它们设置为 true
。然后,我将这些介质类型添加到介质类型组合框中,让用户选择他们将使用的介质类型。然后,我使用一个非常“粗略”的估计来确定用户已填充了介质的多少。
void CBurnCDDlg::OnCbnSelchangeDeviceCombo()
{
m_isCdromSupported = false;
m_isDvdSupported = false;
m_isDualLayerDvdSupported = false;
m_mediaTypeCombo.ResetContent();
int selectedIndex = m_deviceComboBox.GetCurSel();
ASSERT(selectedIndex >= 0);
if (selectedIndex < 0)
{
return;
}
CDiscRecorder* discRecorder =
(CDiscRecorder*)m_deviceComboBox.GetItemDataPtr(selectedIndex);
if (discRecorder != NULL)
{
CDiscFormatData discFormatData;
if (!discFormatData.Initialize(discRecorder, CLIENT_NAME))
{
return;
}
//
// Display Supported Media Types
//
CString supportedMediaTypes;
ULONG totalMediaTypes = discFormatData.GetTotalSupportedMediaTypes();
for (ULONG volIndex = 0; volIndex < totalMediaTypes; volIndex++)
{
int mediaType = discFormatData.GetSupportedMediaType(volIndex);
if (volIndex > 0)
supportedMediaTypes += _T(", ");
supportedMediaTypes += GetMediaTypeString(mediaType);
}
m_supportedMediaTypes.SetWindowText(supportedMediaTypes);
//
// Add Media Selection
//
if (m_isCdromSupported)
{
int stringIndex = m_mediaTypeCombo.AddString(_T("700MB CD Media"));
m_mediaTypeCombo.SetItemData(stringIndex, CD_MEDIA);
}
if (m_isDvdSupported)
{
int stringIndex = m_mediaTypeCombo.AddString(_T("4.7GB DVD Media"));
m_mediaTypeCombo.SetItemData(stringIndex, DVD_MEDIA);
}
if (m_isDualLayerDvdSupported)
{
int stringIndex = m_mediaTypeCombo.AddString(_T("8.5GB Dual-Layer DVD"));
m_mediaTypeCombo.SetItemData(stringIndex, DL_DVD_MEDIA);
}
m_mediaTypeCombo.SetCurSel(0);
OnCbnSelchangeMediaTypeCombo();
}
}
当用户将文件添加到列表时,我创建一个 CFileObject
并将其添加到文件列表框中。然后,我调用 UpdateCapacity
函数,该函数计算文件列表框中所有项所需的总存储空间,并更新容量进度条。
void CBurnCDDlg::OnBnClickedAddFilesButton()
{
CFileDialog fileDialog(TRUE, NULL, NULL, OFN_FILEMUSTEXIST, _T
("All Files (*.*)|*.*||"), NULL, 0);
if (fileDialog.DoModal() == IDOK)
{
CFileObject* pFileObject = new CFileObject(fileDialog.GetPathName());
int addIndex = m_fileListbox.AddString(pFileObject->GetName());
m_fileListbox.SetItemDataPtr(addIndex, pFileObject);
UpdateCapacity();
EnableBurnButton();
}
}
当用户将文件夹添加到列表时,我创建一个 CDirObject
并将其添加到文件列表框中。与文件对象一样,我调用 UpdateCapacity
函数来计算文件列表框中所有项所需的总存储空间,并更新容量进度条。
void CBurnCDDlg::OnBnClickedAddFolderButton()
{
BROWSEINFO bi = {0};
bi.hwndOwner = m_hWnd;
bi.ulFlags = BIF_RETURNONLYFSDIRS|BIF_USENEWUI;
LPITEMIDLIST lpidl = SHBrowseForFolder(&bi);
if (!lpidl)
return;
TCHAR selectedPath[_MAX_PATH] = {0};
if (SHGetPathFromIDList(lpidl, selectedPath))
{
CDirObject* pDirObject = new CDirObject(selectedPath);
int addIndex = m_fileListbox.AddString(pDirObject->GetName());
m_fileListbox.SetItemDataPtr(addIndex, pDirObject);
UpdateCapacity();
EnableBurnButton();
}
}
当用户按下“刻录”按钮时,我禁用用户界面并启动另一个线程 BurnThread
来执行刻录。这将使 UI 在刻录过程中保持响应。
void CBurnCDDlg::OnBnClickedBurnButton()
{
if (m_isBurning)
{
SetCancelBurning(true);
}
else
{
SetCancelBurning(false);
m_isBurning = true;
UpdateData();
EnableUI(false);
AfxBeginThread(BurnThread, this, THREAD_PRIORITY_NORMAL);
}
}
UINT CBurnCDDlg::BurnThread(LPVOID pParam)
{
IStream* dataStream = NULL;
CBurnCDDlg* pThis = (CBurnCDDlg*)pParam;
if (!CreateMediaFileSystem(pThis, &dataStream))
{ // CreateMediaFileSystem reported error to UI
return false;
}
//
// Get the selected recording device from the combobox
//
int selectedIndex = pThis->m_deviceComboBox.GetCurSel();
ASSERT(selectedIndex >= 0);
if (selectedIndex < 0)
{
pThis->SendMessage(WM_BURN_FINISHED, 0,
(LPARAM)_T("Error: No Device Selected"));
return 0;
}
CDiscRecorder* pOrigDiscRecorder =
(CDiscRecorder*)pThis->m_deviceComboBox.GetItemDataPtr(selectedIndex);
if (pOrigDiscRecorder == NULL)
{
//
// This should never happen
//
pThis->SendMessage(WM_BURN_FINISHED, 0,
(LPARAM)_T("Error: No Data for selected device"));
return 0;
}
//
// Did user cancel?
//
if (pThis->GetCancelBurning())
{
pThis->SendMessage(WM_BURN_FINISHED, 0, (LPARAM)_T("User Canceled!"));
return 0;
}
pThis->SendMessage(WM_BURN_STATUS_MESSAGE, 0,
(LPARAM)_T("Initializing Disc Recorder..."));
//
// Create another disc recorder because we're in a different thread
//
CDiscRecorder discRecorder;
CString errorMessage;
if (discRecorder.Initialize(pOrigDiscRecorder->GetUniqueId()))
{
//
//
//
if (discRecorder.AcquireExclusiveAccess(true, CLIENT_NAME))
{
CDiscFormatData discFormatData;
if (discFormatData.Initialize(&discRecorder, CLIENT_NAME))
{
discFormatData.SetCloseMedia(pThis->m_closeMedia ? true : false);
//////////////////////////////
//
// Burn the media here
//
discFormatData.Burn(pThis->m_hWnd, dataStream);
//
// Release the IStream after burning
//
dataStream->Release();
//
// Eject Media if they chose
//
if (pThis->m_ejectWhenFinished)
{
discRecorder.EjectMedia();
}
}
discRecorder.ReleaseExclusiveAccess();
//
// Finished Burning, GetHresult will determine if it was successful or not
//
pThis->SendMessage(WM_BURN_FINISHED, discFormatData.GetHresult(),
(LPARAM)(LPCTSTR)discFormatData.GetErrorMessage());
}
else
{
errorMessage.Format(_T("Failed: %s is exclusive owner"),
(LPCTSTR)discRecorder.ExclusiveAccessOwner());
pThis->SendMessage(WM_BURN_FINISHED, discRecorder.GetHresult(),
(LPARAM)(LPCTSTR)errorMessage);
}
}
else
{
errorMessage.Format(_T("Failed to initialize recorder - Unique ID:%s"),
(LPCTSTR)pOrigDiscRecorder->GetUniqueId());
pThis->SendMessage(WM_BURN_FINISHED, discRecorder.GetHresult(),
(LPARAM)(LPCTSTR)errorMessage);
}
return 0;
}
UI 通知
工作线程通过 SendMessage
命令与 UI 通信。我从工作线程向 UI 发送状态消息 (WM_BURN_STATUS_MESSAGE
) 和刻录完成 (WM_BURN_FINISHED
) 消息。
我通过 CDiscFormatDataEvent::Update
函数中的 WM_IMAPI_UPDATE
消息将事件通知发送到 UI。我在 CDiscFormatData::Burn
函数中创建一个 CDiscFormatDataEvent
类的实例,该类实现了 DDiscFormat2DataEvents
接口。当它收到事件时,它会获取数据并将其发送到 UI,以便 UI 可以更新状态。
历史
-
v1 - 2007 年 12 月 29 日
- 初始发布。
-
v2 - 2008 年 1 月 8 日
- 将项目从 Visual Studio 2008 转换为 Visual Studio 2005。
- 从
CDiscFormatDataEvent
类中移除了 ATL。 - 示例应用程序静态链接 MFC DLL。
- 错误修复 - 未将镜像设置为介质大小。
- 支持超大文件。感谢 Dale Stewart。
- 当
InitializeDiscRecorder
因虚拟 CDROM 等原因失败时,不会断言。 -
v4 - 2009 年 12 月 12 日
- 更新了指向最新 Windows SDK 的链接。
- 修复了一个
IStream
未释放的泄漏。