Recentemente, um projeto legado na empresa exigiu a adição de funcionalidade para exportar dados para CDs. Inicialmente, resisti a essa solicitação, considerando o CD um meio de armazenamento obsoleto. Assim como walkmans, MP3s e MP4s, os CDs estão prestes a sair de cena. No entanto, seguindo as instruções da gestão, precisei implementar essa funcionalidade.
Após pesquisa, descobri que a documentação disponível sobre gravação de CDs em C# é bastante antiga e os métodos existentes não são particularmente convenientes. Embora tenham encontrado exemplos úteis, como o do CodePlex Archive e o projeto BurnCdDemo, enfrentamos desafios durante a implementação, especialmente ao lidar com permissões de administrador e problemas de compatibilidade.
Neste artigo, apresentaremos uma abordagem simplificada para implementar funcionalidades básicas de gravação de CDs: listagem de unidades ópticas, obtenção de informações do disco, adição de arquivos, processo de gravação e notificação de progresso.
**Ferramentas Necessárias**
Como meu computador de trabalho não possui unidade de CD, a testagem se tornou difícil. A solução foi instalar uma unidade óptica virtual. Recomendo o PhantomBurner, uma ferramenta de virtualização confiável. Após a instalação e ativação, crie um disco virtual usando DVD+RW para facilitar os testes.
**Implementação**
**1. Listagem de Dispositivos**
Primeiro, criamos uma classe OpticalDeviceHelper com um método estático para obter a lista de unidades ópticas disponíveis.
/// <summary>
/// Obtém a lista de unidades ópticas disponíveis
/// </summary>
/// <returns>Lista de unidades ópticas</returns>
public static List<OpticalDevice> GetOpticalDeviceList()
{
List<OpticalDevice> deviceList = new List<OpticalDevice>();
// Cria um objeto DiscMaster2 para conectar às unidades ópticas
MsftDiscMaster2 discMaster = new MsftDiscMaster2();
for (int i = 0; i < discMaster.Count; i++)
{
if (discMaster[i] != null)
{
OpticalDevice device = new OpticalDevice(discMaster[i]);
deviceList.Add(device);
}
}
return deviceList;
}
A classe OpticalDevice representa uma unidade óptica com as seguintes propriedades principais:
MsftDiscRecorder2 deviceController = null; // Controlador da unidade
/// <summary>
/// Nome da unidade
/// </summary>
public string DeviceName { get; private set; }
/// <summary>
/// Suporte do dispositivo para gravação
/// </summary>
public bool IsDeviceSupported { get; private set; }
/// <summary>
/// Suporte do meio atual
/// </summary>
public bool IsCurrentMediaSupported { get; private set; }
/// <summary>
/// Espaço livre no disco
/// </summary>
public long FreeDiskSpace { get; private set; }
/// <summary>
/// Tamanho total do disco
/// </summary>
public long TotalDiskSpace { get; private set; }
/// <summary>
/// Estado do meio atual
/// </summary>
public IMAPI_FORMAT2_DATA_MEDIA_STATE CurrentMediaState { get; private set; }
/// <summary>
/// Nome do estado do meio
/// </summary>
public string CurrentMediaStateName { get; private set; }
/// <summary>
/// Tipo do meio físico
/// </summary>
public IMAPI_MEDIA_PHYSICAL_TYPE CurrentMediaType { get; private set; }
/// <summary>
/// Nome do tipo do meio
/// </summary>
public string CurrentMediaTypeName { get; private set; }
/// <summary>
/// Indica se é possível gravar
/// </summary>
public bool CanWrite {get;private set;}
O método de inicialização da classe OpticalDevice é responsável por configurar todas as propriedades:
/// <summary>
/// Construtor da OpticalDevice
/// </summary>
/// <param name="uniqueId"></param>Identificador único
public OpticalDevice(string uniqueId)
{
this.uniqueId = uniqueId;
deviceController = new MsftDiscRecorder2();
deviceController.InitializeDiscRecorder(uniqueId);
InitializeDevice();
this.FilesToBurn = new List<BurnFile>();
this.TotalFilesSize = 0;
}
/// <summary>
/// Inicializa a unidade óptica
/// Atualiza informações da unidade, pode ser chamado após inserir um novo disco
/// </summary>
public void InitializeDevice()
{
try
{
if (deviceController.VolumePathNames != null && deviceController.VolumePathNames.Length > 0)
{
foreach (object mountPoint in deviceController.VolumePathNames)
{ // Usa o primeiro ponto de montagem encontrado
DeviceName = mountPoint.ToString();
break;
}
}
// Define o novo formato do disco e define o dispositivo
MsftDiscFormat2Data dataWriter = new MsftDiscFormat2Data();
dataWriter.Recorder = deviceController;
if (!dataWriter.IsRecorderSupported(deviceController))
{
return;
}
if (!dataWriter.IsCurrentMediaSupported(deviceController))
{
return;
}
if (dataWriter.FreeSectorsOnMedia >= 0)
{ // Espaço disponível
FreeDiskSpace = dataWriter.FreeSectorsOnMedia * 2048L;
}
if (dataWriter.TotalSectorsOnMedia >= 0)
{ // Tamanho total
TotalDiskSpace = dataWriter.TotalSectorsOnMedia * 2048L;
}
CurrentMediaState = dataWriter.CurrentMediaStatus; // Estado do meio
CurrentMediaStateName = OpticalDeviceHelper.GetMediaStateName(CurrentMediaState);
CurrentMediaType = dataWriter.CurrentPhysicalMediaType; // Tipo do meio
CurrentMediaTypeName = OpticalDeviceHelper.GetMediaTypeName(CurrentMediaType);
CanWrite = OpticalDeviceHelper.GetMediaBurnable(CurrentMediaState); // Verifica se é possível gravar
}
catch (COMException ex)
{
string errMsg = ex.Message.Replace("\r\n", ""); // Remove caracteres de nova linha da mensagem de erro
this.CurrentMediaStateName = $"Exceção COM:{errMsg}";
}
catch (Exception ex)
{
this.CurrentMediaStateName = $"{ex.Message}";
}
}
É importante notar que operações em unidades ópticas requerem permissões de administrador. Para configurar um aplicativo C# para ser executado com privilégios de administrador, consulte a documentação apropriada.
**2. Adicionando Arquivos para Gravação**
Adicionamos à classe OpticalDevice uma lista de arquivos para gravar e o tamanho total desses arquivos:
/// <summary>
/// Lista de arquivos para gravar
/// </summary>
public List<BurnFile> FilesToBurn {get;set;}
/// <summary>
/// Tamanho total dos arquivos para gravar
/// </summary>
public long TotalFilesSize { get; set; }
A classe BurnFile é definida como:
/// <summary>
/// Representa um arquivo ou pasta para gravação
/// </summary>
public class BurnFile
{
/// <summary>
/// Caminho do arquivo/pasta
/// </summary>
public string FilePath { get; set; }
/// <summary>
/// Nome do arquivo/pasta
/// </summary>
public string FileName { get; set; }
/// <summary>
/// Indica se é uma pasta
/// </summary>
public bool IsDirectory { get; set; }
/// <summary>
/// Tamanho do arquivo/pasta
/// </summary>
public long Size { get; set; }
}
O método para adicionar arquivos à lista de gravação é:
/// <summary>
/// Adiciona um arquivo ou pasta à lista de gravação
/// </summary>
public BurnFile AddFileToBurn(string path)
{
BurnFile file = null;
if(string.IsNullOrEmpty(path))
{
throw new Exception("O caminho do arquivo não pode ser vazio.");
}
if(!CanWrite)
{
throw new Exception("O estado atual do disco não permite gravação.");
}
file = new BurnFile();
long fileSize = 0;
if (Directory.Exists(path))
{
DirectoryInfo dirInfo = new DirectoryInfo(path);
fileSize = GetDirectorySize(path);
file.FileName = dirInfo.Name;
file.FilePath = dirInfo.FullName;
file.Size = fileSize;
file.IsDirectory = true;
}
else if (File.Exists(path))
{
FileInfo fileInfo = new FileInfo(path);
fileSize = fileInfo.Length;
file.FileName = fileInfo.Name;
file.FilePath = fileInfo.FullName;
file.Size = fileSize;
file.IsDirectory = false;
}
else
{
throw new Exception("Arquivo ou pasta não existe");
}
if (TotalFilesSize + fileSize >= FreeDiskSpace)
{
throw new Exception("Espaço insuficiente no disco");
}
if (FilesToBurn.Any(f => f.FileName.ToLower() == file.FileName.ToLower()))
{
throw new Exception($"Já existe um arquivo com nome {file.FileName}");
}
FilesToBurn.Add(file);
TotalFilesSize += fileSize;
return file;
}
**3. Processo de Gravação com Notificação de Progresso**
Para implementar a notifciação de progresso durante a gravação, primeiro definimos um delegate:
/// <summary>
/// Delegate para notificação de progresso da gravação
/// </summary>
public delegate void BurnProgressChanged(BurnStatus burnStatus);
A classe BurnStatus contém informações sobre o progresso da gravação:
/// <summary>
/// Informações sobre o progresso da gravação
/// </summary>
public class BurnStatus
{
/// <summary>
/// Ação atual em progresso
/// Correspondente ao enum IMAPI2.IMAPI_FORMAT2_DATA_WRITE_ACTION
/// 4 - Escrevendo dados 5 - Concluído a escrita de dados 6 - Gravação concluída
/// </summary>
public int CurrentAction { get; set; }
/// <summary>
/// Nome da ação atual
/// </summary>
public string CurrentActionName { get; set; }
/// <summary>
/// Tempo decorrido em segundos
/// </summary>
public int ElapsedTime { get; set; }
/// <summary>
/// Tempo estimado total em segundos
/// </summary>
public int EstimatedTime { get; set; }
/// <summary>
/// Progresso da escrita de dados (0-1)
/// </summary>
public decimal Progress { get; set; }
/// <summary>
/// Progresso da escrita de dados em formato de porcentagem
/// </summary>
public string ProgressPercentage { get { return Progress.ToString("0.00%"); } }
}
Adicionamos uma propriedade delegate à classe OpticalDevice:
/// <summary>
/// Notificação de mudança de progresso da gravação
/// </summary>
public BurnProgressChanged OnProgressChanged { get; set; }
O método principal de gravação é implementado da seguinte forma:
/// <summary>
/// Inicia o processo de gravação
/// </summary>
public void Burn(string diskLabel = "Dados")
{
if(!CanWrite)
{
throw new Exception("O estado atual do disco não permite gravação");
}
if (string.IsNullOrEmpty(diskLabel))
{
throw new Exception("O rótulo do disco não pode ser vazio");
}
if (FilesToBurn.Count <= 0)
{
throw new Exception("A lista de arquivos para gravar está vazia");
}
if(TotalFilesSize<=0)
{
throw new Exception("O tamanho total dos arquivos é zero");
}
try
{
// Cria um fluxo de imagem para um diretório específico
dynamic fileSystemImage = new IMAPI2FS.MsftFileSystemImage();
IMAPI2FS.IFsiDirectoryItem rootDir = fileSystemImage.Root;
dynamic dataWriter = new MsftDiscFormat2Data();
dataWriter.Recorder = deviceController;
dataWriter.ClientName = "GravadorCD";
// Define as configurações padrão da imagem
fileSystemImage.ChooseImageDefaults(deviceController);
// Define informações do disco
fileSystemImage.VolumeName = diskLabel;
for (int i = 0; i < FilesToBurn.Count; i++)
{
rootDir.AddTree(FilesToBurn[i].FilePath, true);
}
// Cria uma imagem do sistema de arquivos
IStream stream = fileSystemImage.CreateResultImage().ImageStream;
try
{
dataWriter.Update += new DDiscFormat2DataEvents_UpdateEventHandler(ProgressHandler);
dataWriter.Write(stream); // Grava o fluxo no disco
}
catch (System.Exception ex)
{
throw ex;
}
finally
{
if (stream != null)
{
Marshal.FinalReleaseComObject(stream);
}
}
}
catch (Exception ex)
{
Console.WriteLine($"Falha na gravação:{ex.Message}");
}
}
/// <summary>
/// Manipulador de eventos para notificação de progresso
/// </summary>
/// <param name="sender"></param>Origem do evento
/// <param name="progress"></param>Informações de progresso
void ProgressHandler(dynamic sender, dynamic progress)
{
BurnStatus status = new BurnStatus();
try
{
status.ElapsedTime = progress.ElapsedTime;
status.EstimatedTime = progress.TotalTime;
status.CurrentAction = progress.CurrentAction;
if (status.ElapsedTime > status.EstimatedTime)
{ // Se o tempo decorrido exceder o tempo estimado, ajusta o tempo estimado
status.EstimatedTime = status.ElapsedTime;
}
int currentAction = progress.CurrentAction;
switch (currentAction)
{
case (int)IMAPI2.IMAPI_FORMAT2_DATA_WRITE_ACTION.IMAPI_FORMAT2_DATA_WRITE_ACTION_CALIBRATING_POWER:
status.CurrentActionName = "Calibrando Potência (OPC)";
break;
case (int)IMAPI2.IMAPI_FORMAT2_DATA_WRITE_ACTION.IMAPI_FORMAT2_DATA_WRITE_ACTION_COMPLETED:
status.CurrentActionName = "Gravação Concluída";
status.Progress = 1;
status.EstimatedTime = status.ElapsedTime; // Ajusta tempo estimado ao tempo real
break;
case (int)IMAPI2.IMAPI_FORMAT2_DATA_WRITE_ACTION.IMAPI_FORMAT2_DATA_WRITE_ACTION_FINALIZATION:
status.CurrentActionName = "Finalizando a Gravação";
status.Progress = 1;
status.EstimatedTime = status.ElapsedTime; // Ajusta tempo estimado ao tempo real
break;
case (int)IMAPI2.IMAPI_FORMAT2_DATA_WRITE_ACTION.IMAPI_FORMAT2_DATA_WRITE_ACTION_FORMATTING_MEDIA:
status.CurrentActionName = "Formatando o Disco";
break;
case (int)IMAPI2.IMAPI_FORMAT2_DATA_WRITE_ACTION.IMAPI_FORMAT2_DATA_WRITE_ACTION_INITIALIZING_HARDWARE:
status.CurrentActionName = "Inicializando Hardware";
break;
case (int)IMAPI2.IMAPI_FORMAT2_DATA_WRITE_ACTION.IMAPI_FORMAT2_DATA_WRITE_ACTION_VALIDATING_MEDIA:
status.CurrentActionName = "Validando o Disco";
break;
case (int)IMAPI2.IMAPI_FORMAT2_DATA_WRITE_ACTION.IMAPI_FORMAT2_DATA_WRITE_ACTION_VERIFYING:
status.CurrentActionName = "Verificando os Dados";
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;
status.CurrentActionName = "Escrevendo Dados";
status.Progress = Convert.ToDecimal(writtenSectors)/ Convert.ToDecimal(totalSectors);
break;
default:
status.CurrentActionName = "Ação Desconhecida";
break;
}
}
catch (Exception ex)
{
status.CurrentActionName = ex.Message;
}
if (OnProgressChanged != null)
{
OnProgressChanged(status);
}
}
**Correção de Bug no Cálculo de Tamanho do Disco**
Durante o desenvolvimento, enfrentamos um problema onde o tamanho do disco era retornado como negativo. A causa era um overflow no tipo de dado int ao calcular o tamanho total e o espaço livre. A solução foi alterar o fator de multiplicação de 2048 para 2048L para usar um tipo de dado long e evitar o overflow.
No código original:
// Espaço disponível
m_nUseableSize = msFormat.FreeSectorsOnMedia * 2048;
// Tamanho total
m_nDiskSize = msFormat.TotalSectorsOnMedia * 2048;
Corrigido para:
// Espaço disponível
m_nUseableSize = msFormat.FreeSectorsOnMedia * 2048L;
// Tamanho total
m_nDiskSize = msFormat.TotalSectorsOnMedia * 2048L;
**Conclusão**
Este artigo demonstrou uma implementação simplificada para funcionalidades básicas de gravação de CDs em C#. Abordamos a listagem de unidades ópticas, obtenção de informações do disco, adição de arquivos para gravação, o processo de gravação com notificação de progresso, e uma correção de bug comum.
A implementação utiliza a API IMAPI2 do Windows, que é a interface padrão para operações de gravação de discos. A abordagem apresentada mantém a simplicidade enquanto fornece funcionalidades essenciais para aplicações que necessitem de recursos de gravação de CDs.