一.背景介绍

最近公司一个老项目需要添加导出数据到光盘的功能.开始对这功能添加有点抵触的.光盘?都啥年代了.

光盘一种即将淘汰的存储媒介.就像当年的随身听,Mp3,Mp4一样,即将退出历史舞台.领导让加,发牢骚归发牢骚,活还是得干哈.

从网上找了一些资料,都是特别古老的文档了.很多方法用起来并不是特别的方便.

1.CodePlex Archive https://archive.codeplex.com/?p=csimapi2samples 其中的例子功能不全,参数都使用dynamic类型,刚开始不了解那些基础概念时,理解起来比较难.可以参考学习.

2.博客:https://blog.csdn.net/huanghunjiuba/article/details/12967463 中的例子BurnCdDemo.

里面的BurnCdDemo对ImApi2做了封装.其实例子里面的封装做的挺不错的.注释也特别全.

在做测试的时候发现刻录功能在本机运行的非常好.在公司的测试机上却获取不到光盘大小,取到的大小是负数.对着Demo的源代码检查了半天,没找到原因.╮( ̄▽ ̄")╭,脑袋好大的.后来找到原因了,在下面的过程中会描述下.

C#光盘刻录-LMLPHP

PS:个人不喜欢啥都做抽象.抽象,意味着要多写代码,查找问题也不方便.抽象,依赖注入,控制反转是个好东西,就是要多写许多代码.

个人觉得,很多技术还是要看实际项目情况而使用,没有必要去为了追求使用而滥用.看起来高大上,实际多写了代码维护也不方便.

So,自己搞个简单点的.因为就那么几个功能:获取光驱设备列表,获取光盘信息,添加媒体文件,刻录,进度通知.

二.工欲善其事必先利其器

因工作电脑没有光驱,测试非常不方便.那我们就安装一个虚拟光驱.

推荐一个比较好用的虚拟光驱软件PhantomBurner,带注册码.下载地址:https://download.csdn.net/download/zjhaag/10909339

安装过程就不说了.选下安装路径,一路Next.安装完成后提示重启.其实不重启也可以用.

C#光盘刻录-LMLPHP

安装完成后界面如下.在About中输入注册码激活.点击Create,选择Dvd+RW创建Virtual Disc Image.在资源管理器中就可以看到我们创建的虚拟光驱了.

C#光盘刻录-LMLPHP

 三.开工

1.设备列表获取

(1)首先添加一个RecorderHelper.在RecorderHelper类中添加一个静态方法GetRecorderList用于获取光驱设备列表.

         /// <summary>
/// 获取光驱设备列表
/// </summary>
/// <returns></returns>
public static List<Recorder> GetRecorderList()
{
List<Recorder> recordList = new List<Recorder>(); // Create a DiscMaster2 object to connect to optical drives.
MsftDiscMaster2 discMaster = new MsftDiscMaster2();
for (int i = ; i < discMaster.Count; i++)
{
if (discMaster[i] != null)
{
Recorder recorder = new Recorder(discMaster[i]);
recordList.Add(recorder);
}
}
return recordList;
}

此时,需要Recorder光驱对象主要属性如下

         MsftDiscRecorder2 msRecorder = null;    //Recorder

         /// <summary>
/// 当前磁盘标签
/// </summary>
public string RecorderName { get; private set; } /// <summary>
/// 是否支持当前刻录机
/// </summary>
public bool IsRecorderSupported { get; private set; } /// <summary>
/// 是否支持当前磁盘媒体
/// </summary>
public bool IsCurrentMediaSupported { get; private set; } /// <summary>
/// 当前磁盘可用大小
/// </summary>
public long FreeDiskSize { get; private set; } /// <summary>
/// 当前磁盘总大小
/// </summary>
public long TotalDiskSize { get; private set; } /// <summary>
/// 当前媒体状态
/// </summary>
public IMAPI_FORMAT2_DATA_MEDIA_STATE CurMediaState { get; private set; } /// <summary>
/// 当前媒体状态
/// </summary>
public string CurMediaStateName { get; private set; } /// <summary>
/// 当前媒体类型
/// </summary>
public IMAPI_MEDIA_PHYSICAL_TYPE CurMediaType { get; private set; } /// <summary>
/// 当前媒体类型
/// </summary>
public string CurMediaTypeName { get; private set; } /// <summary>
/// 是否可以刻录
/// </summary>
public bool CanBurn {get;private set;}

(2)对Recorder各属性进行初始化,赋值.主要方法如下

         /// <summary>
/// Recorder Ctor
/// </summary>
/// <param name="uniqueId">标识Id</param>
public Recorder(string uniqueId)
{
this.uniqueId = uniqueId;
msRecorder = new MsftDiscRecorder2();
msRecorder.InitializeDiscRecorder(uniqueId);
InitRecorder(); this.BurnMediaList = new List<BurnMedia>();
this.BurnMediaFileSize = ;
} /// <summary>
/// 初始化Recorder
/// 更新Recorder信息,更新光盘后可重试.
/// </summary>
public void InitRecorder()
{
try
{
if (msRecorder.VolumePathNames != null && msRecorder.VolumePathNames.Length > )
{
foreach (object mountPoint in msRecorder.VolumePathNames)
{ //挂载点 取其中一个
RecorderName = mountPoint.ToString();
break;
}
}
// Define the new disc format and set the recorder
MsftDiscFormat2Data dataWriter = new MsftDiscFormat2Data();
dataWriter.Recorder = msRecorder; if (!dataWriter.IsRecorderSupported(msRecorder))
{
return;
}
if (!dataWriter.IsCurrentMediaSupported(msRecorder))
{
return;
}
if (dataWriter.FreeSectorsOnMedia >= )
{ //可用大小
FreeDiskSize = dataWriter.FreeSectorsOnMedia * 2048L;
} if (dataWriter.TotalSectorsOnMedia >= )
{ //总大小
TotalDiskSize = dataWriter.TotalSectorsOnMedia * 2048L;
}
CurMediaState = dataWriter.CurrentMediaStatus; //媒体状态
CurMediaStateName = RecorderHelper.GetMediaStateName(CurMediaState);
CurMediaType = dataWriter.CurrentPhysicalMediaType; //媒介类型
CurMediaTypeName = RecorderHelper.GetMediaTypeName(CurMediaType);
CanBurn = RecorderHelper.GetMediaBurnAble(CurMediaState); //判断是否可刻录
}
catch (COMException ex)
{
string errMsg = ex.Message.Replace("\r\n", ""); //去掉异常信息里的\r\n
this.CurMediaStateName = $"COM Exception:{errMsg}";
}
catch (Exception ex)
{
this.CurMediaStateName = $"{ex.Message}";
}
}

(3)测试代码如下

                     #region 查找光驱设备
Console.Clear();
Console.WriteLine("正在查找光驱设备..");
List<Recorder> recorderList = RecorderHelper.GetRecorderList();
if (recorderList.Count <= )
{
Console.WriteLine("没有可以使用的光驱,请检查.");
Console.WriteLine("请连接光驱后,按任意键重试...");
Console.ReadKey();
continue;
}
for (int i = ; i < recorderList.Count; i++)
{
Recorder tempRecorder = recorderList[i];
Console.WriteLine($"发现光驱设备:[{i+1}] {tempRecorder.RecorderName}");
Console.WriteLine($"媒体类型:{tempRecorder.CurMediaTypeName}");
Console.WriteLine($"媒体状态:{tempRecorder.CurMediaStateName}");
Console.WriteLine("支持刻录:" + (tempRecorder.CanBurn ? "√" : "×"));
Console.WriteLine($"可用大小:{FormatFileSize(tempRecorder.FreeDiskSize)}");
Console.WriteLine($"总大小:{FormatFileSize(tempRecorder.TotalDiskSize)}");
}
if (!recorderList.Any(r=>r.CanBurn))
{
Console.WriteLine("没有可以用于刻录的光驱设备,请检查后,按任意键重试.");
Console.ReadKey();
continue;
}
#endregion

测试结果:

C#光盘刻录-LMLPHP

至此,我们完成了对于光驱设备列表的获取及Recorder对象各属性的初始化工作.

说明下,对光驱设备操作需要有管理员权限,没有管理员权限会导致获取失败.对于C#应用程序设置以管理员身份启动请参照https://www.cnblogs.com/babycool/p/3569183.html

2.添加刻录文件

(1)我们为Recorder对象添加待刻录媒体对象列表和待刻录媒体文件大小两个属性

         /// <summary>
/// 待刻录媒体对象List
/// </summary>
public List<BurnMedia> BurnMediaList {get;set;} /// <summary>
/// 待刻录媒体文件大小
/// </summary>
public long BurnMediaFileSize { get; set; }

待刻录媒体对象定义如下

     /// <summary>
/// 刻录媒体
/// </summary>
public class BurnMedia
{
/// <summary>
/// 路径
/// </summary>
public string MediaPath { get; set; } /// <summary>
/// 名称
/// </summary>
public string MediaName { get; set; } /// <summary>
/// 是否是文件夹
/// </summary>
public bool IsDirectory { get; set; } /// <summary>
/// 大小
/// </summary>
public long Size { get; set; }
}

(2)为Recorder对象添加AddBurnMedia方法

         /// <summary>
/// 添加刻录媒体对象
/// </summary>
public BurnMedia AddBurnMedia(string path)
{
BurnMedia media = null;
if(string.IsNullOrEmpty(path))
{
throw new Exception("文件路径不能为空.");
}
if(!CanBurn)
{
throw new Exception("当前磁盘状态不支持刻录.");
}
media = new BurnMedia();
long fileSize = ;
if (Directory.Exists(path))
{
DirectoryInfo dirInfo = new DirectoryInfo(path);
fileSize = GetDirectorySize(path);
media.MediaName = dirInfo.Name;
media.MediaPath = dirInfo.FullName;
media.Size = fileSize;
media.IsDirectory = true;
}
else if (File.Exists(path))
{
FileInfo fileInfo = new FileInfo(path);
fileSize = fileInfo.Length;
media.MediaName = fileInfo.Name;
media.MediaPath = fileInfo.FullName;
media.Size = fileSize;
media.IsDirectory = false;
}
else
{
throw new Exception("文件不存在");
}
if (BurnMediaFileSize + fileSize >= FreeDiskSize)
{
throw new Exception("剩余空间不足");
}
if (BurnMediaList.Any(m => m.MediaName.ToLower() == media.MediaName.ToLower()))
{
throw new Exception($"已存在媒体名称为{media.MediaName}的对象");
}
BurnMediaList.Add(media);
BurnMediaFileSize += fileSize;
return media;
}

(3)测试代码如下

                     while (true)
{
Console.WriteLine("添加文件:请输入待刻录文件或文件夹路径. 0完成 1查看已添加文件");
string filePath = Console.ReadLine();
if (string.IsNullOrEmpty(filePath))
{
continue;
}
else if (filePath == "")
{
break;
}
else if (filePath == "")
{
ShowBurnMediaListInfo(recorder);
}
else
{
try
{
BurnMedia media = recorder.AddBurnMedia(filePath);
Console.WriteLine($"添加成功:{filePath}");
Console.WriteLine("文件大小:" + FormatFileSize(media.Size));
Console.WriteLine("已添加文件总大小:" + FormatFileSize(recorder.BurnMediaFileSize));
}
catch (Exception ex)
{
Console.WriteLine($"添加失败:{ex.Message}");
}
}
}

运行结果:

C#光盘刻录-LMLPHP

3.刻录及刻录进度通知

为Recorder对象添加Burn方法.同时,添加刻录进度通知.

(1)添加BurnProgressChanged委托用于通知刻录进度

     /// <summary>
/// 刻录进度delegate
/// </summary>
public delegate void BurnProgressChanged(BurnProgress burnProgress);

刻录进度通知对象定义如下(其属性为根据需求添加):

     /// <summary>
/// 刻录进度对象
/// </summary>
public class BurnProgress
{
/// <summary>
/// 当前操作
/// 对应IMAPI2.IMAPI_FORMAT2_DATA_WRITE_ACTION枚举
/// 4 正在写入数据 5完成数据写入 6 刻录完成
/// </summary>
public int CurrentAction { get; set; } /// <summary>
/// 当前操作Name
/// </summary>
public string CurrentActionName { get; set; } /// <summary>
/// 已用时间单位S
/// </summary>
public int ElapsedTime { get; set; } /// <summary>
/// 预计总时间单位S
/// </summary>
public int TotalTime { get; set; } /// <summary>
/// 数据写入进度
/// </summary>
public decimal Percent { get; set; } /// <summary>
/// 数据写入进度%
/// </summary>
public string PercentStr { get { return Percent.ToString("0.00%"); } }
}

(2)为Recorder对象添加委托属性

         /// <summary>
/// 刻录进度变化通知
/// </summary>
public BurnProgressChanged OnBurnProgressChanged { get; set; }

(3)为Recorder添加Burn方法

         /// <summary>
/// 刻录
/// </summary>
public void Burn(string diskName = "SinoUnion")
{
if(!CanBurn)
{
throw new Exception("当前磁盘状态不支持刻录");
}
if (string.IsNullOrEmpty(diskName))
{
throw new Exception("DiskName不能为空");
}
if (BurnMediaList.Count <= )
{
throw new Exception("待刻录文件列表不能为空");
}
if(BurnMediaFileSize<=)
{
throw new Exception("待刻录文件大小为0");
} try
{ //说明
//1.fsi.ChooseImageDefaults用的是IMAPI2FS的,我们定义的msRecorder是IMAPI2的.所以必须用动态类型
//2.dataWriter也要使用动态类型,要不然Update事件会出异常.
// Create an image stream for a specified directory.
dynamic fsi = new IMAPI2FS.MsftFileSystemImage(); // Disc file system
IMAPI2FS.IFsiDirectoryItem dir = fsi.Root; // Root directory of the disc file system
dynamic dataWriter = new MsftDiscFormat2Data(); //Create the new disc format and set the recorder dataWriter.Recorder = msRecorder;
dataWriter.ClientName = "SinoGram";
//不知道这方法不用行不行.用的参数是IMAPI2FS的.
//所以学官网的例子,把fsi改成了动态的.使用msRecorder作为参数
fsi.ChooseImageDefaults(msRecorder); //设置相关信息
fsi.VolumeName = diskName; //刻录磁盘名称
for (int i = ; i < BurnMediaList.Count; i++)
{
dir.AddTree(BurnMediaList[i].MediaPath, true);
}
// Create an image from the file system
IStream stream = fsi.CreateResultImage().ImageStream;
try
{
dataWriter.Update += new DDiscFormat2DataEvents_UpdateEventHandler(BurnProgressChanged);
dataWriter.Write(stream);// Write stream to disc
}
catch (System.Exception ex)
{
throw ex;
}
finally
{
if (stream != null)
{
Marshal.FinalReleaseComObject(stream);
}
}
}
catch (Exception ex)
{
Console.WriteLine($"刻录失败:{ex.Message}");
}
} /// <summary>
/// 刻录进度通知
/// </summary>
/// <param name="object"></param>
/// <param name="progress"></param>
void BurnProgressChanged(dynamic @object, dynamic progress)
{
BurnProgress burnProgress = new BurnProgress();
try
{
burnProgress.ElapsedTime = progress.ElapsedTime;
burnProgress.TotalTime = progress.TotalTime;
burnProgress.CurrentAction = progress.CurrentAction;
if (burnProgress.ElapsedTime > burnProgress.TotalTime)
{ //如果已用时间已超过预估总时间.则将预估总时间设置为已用时间
burnProgress.TotalTime = burnProgress.ElapsedTime;
}
string strTimeStatus;
strTimeStatus = "Time: " + progress.ElapsedTime + " / " + progress.TotalTime;
int currentAction = progress.CurrentAction;
switch (currentAction)
{
case (int)IMAPI2.IMAPI_FORMAT2_DATA_WRITE_ACTION.IMAPI_FORMAT2_DATA_WRITE_ACTION_CALIBRATING_POWER:
burnProgress.CurrentActionName = "Calibrating Power (OPC)";
break;
case (int)IMAPI2.IMAPI_FORMAT2_DATA_WRITE_ACTION.IMAPI_FORMAT2_DATA_WRITE_ACTION_COMPLETED:
burnProgress.CurrentActionName = "Completed the burn";
burnProgress.Percent = ;
burnProgress.TotalTime = burnProgress.ElapsedTime; //刻录完成,将预估用时,修正为已用时间
break;
case (int)IMAPI2.IMAPI_FORMAT2_DATA_WRITE_ACTION.IMAPI_FORMAT2_DATA_WRITE_ACTION_FINALIZATION:
burnProgress.CurrentActionName = "Finishing the writing";
burnProgress.Percent = ;
burnProgress.TotalTime = burnProgress.ElapsedTime; //写入完成,将预估用时,修正为已用时间
break;
case (int)IMAPI2.IMAPI_FORMAT2_DATA_WRITE_ACTION.IMAPI_FORMAT2_DATA_WRITE_ACTION_FORMATTING_MEDIA:
burnProgress.CurrentActionName = "Formatting media";
break;
case (int)IMAPI2.IMAPI_FORMAT2_DATA_WRITE_ACTION.IMAPI_FORMAT2_DATA_WRITE_ACTION_INITIALIZING_HARDWARE:
burnProgress.CurrentActionName = "Initializing Hardware";
break;
case (int)IMAPI2.IMAPI_FORMAT2_DATA_WRITE_ACTION.IMAPI_FORMAT2_DATA_WRITE_ACTION_VALIDATING_MEDIA:
burnProgress.CurrentActionName = "Validating media";
break;
case (int)IMAPI2.IMAPI_FORMAT2_DATA_WRITE_ACTION.IMAPI_FORMAT2_DATA_WRITE_ACTION_VERIFYING:
burnProgress.CurrentActionName = "Verifying the data";
break;
case (int)IMAPI2.IMAPI_FORMAT2_DATA_WRITE_ACTION.IMAPI_FORMAT2_DATA_WRITE_ACTION_WRITING_DATA:
dynamic totalSectors;
dynamic writtenSectors;
dynamic startLba;
dynamic lastWrittenLba;
totalSectors = progress.SectorCount;
startLba = progress.StartLba;
lastWrittenLba = progress.LastWrittenLba;
writtenSectors = lastWrittenLba - startLba;
burnProgress.CurrentActionName = "Writing data";
burnProgress.Percent = Convert.ToDecimal(writtenSectors)/ Convert.ToDecimal(totalSectors);
break;
default:
burnProgress.CurrentActionName = "Unknown action";
break;
}
}
catch (Exception ex)
{
burnProgress.CurrentActionName = ex.Message;
}
if (OnBurnProgressChanged != null)
{
OnBurnProgressChanged(burnProgress);
}
}

(4)测试代码如下

                     if (recorder.BurnMediaList.Count <= )
{
Console.WriteLine($"未添加任何刻录文件.已退出刻录过程.");
}
else
{
#region 刻录前确认
bool confirmBurn = false;
Console.Clear();
ShowBurnMediaListInfo(recorder);
while (true)
{
Console.WriteLine();
Console.ForegroundColor = ConsoleColor.DarkGreen;//设置颜色.
Console.WriteLine($"刻录过程一旦开始,终止可能会造成磁盘损坏.确认要开始刻录(y/n)?");
Console.ForegroundColor = colorFore;//还原颜色.
string confirmStr = Console.ReadLine();
if (confirmStr.ToLower() == "n")
{
break;
}
else if (confirmStr.ToLower() == "y")
{
confirmBurn = true;
break;
}
}
if (!confirmBurn)
{
Console.WriteLine($"本次刻录过程已退出");
continue;
}
#endregion
Console.CursorVisible = false; //隐藏光标
ShowBurnProgressChanged(recorder);
recorder.Burn(); //刻录
Console.WriteLine();
}

(5)输出刻录进度变化,添加ShowBurnProgressChanged方法

在查找如何输出进度数据的时候,看到了控制台输出进度条的功能.带颜色的还挺漂亮的,看到好的功能就想加到我们的项目里.

         /// <summary>
/// 输出刻录进度通知
/// </summary>
/// <param name="recorder"></param>
static void ShowBurnProgressChanged(Recorder recorder)
{
Console.Clear(); #region 搭建输出显示框架
Console.WriteLine();
Console.WriteLine($"**********************刻录中,请稍候**********************");
Console.WriteLine();
Console.WriteLine(" 当前操作:"); //第4行当前操作
Console.WriteLine(); // 第6行绘制进度条背景
Console.Write(" ");
Console.BackgroundColor = ConsoleColor.DarkCyan;
for (int i = ; i <= ; i++)
{ //设置50*1的为总进度
Console.Write(" ");
} Console.WriteLine();
Console.BackgroundColor = colorBack; Console.WriteLine(); //第7行输出空行
Console.WriteLine(); //第8行输出进度
Console.WriteLine($"*********************************************************"); //第9行
Console.WriteLine(); //第10行输出空行
#endregion //进度变化通知时,更新相关行数据即可.
recorder.OnBurnProgressChanged += (burnProgress) => {
if (burnProgress.CurrentAction == )
{ //刻录完成
Console.SetCursorPosition(, );
Console.WriteLine($"*************************刻录完成************************");
}
//第4行 当前操作
Console.SetCursorPosition(, );
Console.Write($" 当前操作:{burnProgress.CurrentActionName}");
Console.Write(" "); //填充空白区域
Console.ForegroundColor = colorFore; // 第6行 绘制进度条进度(进度条前预留2空格)
Console.BackgroundColor = ConsoleColor.Yellow; // 设置进度条颜色
Console.SetCursorPosition(, ); // 设置光标位置,参数为第几列和第几行
for (int i = ; i <burnProgress.Percent*; i++)
{ //每个整数写入1个空格
Console.Write(" "); // 移动进度条
}
Console.BackgroundColor = colorBack; // 恢复输出颜色 //第8行 已用时间,总时间
Console.ForegroundColor = ConsoleColor.Green;// 更新进度百分比,原理同上.
Console.SetCursorPosition(, );
Console.Write($" 进度:{burnProgress.PercentStr} " +
$"已用时间:{FormatTime(0, 0, burnProgress.ElapsedTime)} " +
$"剩余时间:{FormatTime(0, 0, burnProgress.TotalTime - burnProgress.ElapsedTime)}");
Console.Write(" "); //填充空白区域
Console.ForegroundColor = colorFore; Console.SetCursorPosition(, ); //光标 定位到第10行
};
}

运行结果如下,剩余时间为IMAPI2提供.并不一定准确.

C#光盘刻录-LMLPHP

四.BurnCdDemo获取磁盘大小为负数bug

在开始的时候,我们说了使用BurnCdDemo获取光盘大小为负数.在实际开发的过程中,也碰到了.因为我们买的光盘太大了?达到8G,实际模拟刻录时,没那么大.所以没啥问题.

我们看BurnCdDemo里Recorder对象的代码:

 private long m_nDiskSize = ;
private long m_nUseableSize = ; //可用的
m_nUseableSize = msFormat.FreeSectorsOnMedia * ;
//总大小
m_nDiskSize = msFormat.TotalSectorsOnMedia * ;

msFormat.FreeSectorsOnMedia和msFormat.TotalSectorsOnMedia类型为int.大家想到原因了吧.int溢出了.所以把后面的2048改为2048L就可以了.看似正常的代码,以后在碰到类型转换时要多注意些.

五.总结

总结写点啥呢.当然是最重要的哈.上源码:https://github.com/279328316/RecorderHelper

如有问题,欢迎留言.

05-14 20:21