Writing CDs with C# and Windows XP's ICDBurn Interface

Have you ever wondered what goes on behind the scenes when XP burn CDs? How? would of course be the fist question you would have asked. The simple answer is: the ICDBurn Interface. With this article, I will demonstrate how to write data to a CD while utilising XP's ICDBurn Interface.

So, What Is this ICDBurn Interface I'm Talking About?

The ICDBurn interface has three main functions:

  1. To determine whether hardware capable of writing to CD is present on the system.
  2. To determine the drive letter of a CD writer device.
  3. To programmatically initiate a CD writing session.

Because of these functions, it has three methods to accomplish the above-mentioned tasks:

Method Function
Burn Instructs data to be copied from the staging area to a writable CD.
GetRecorderDriveLetter Retrieves the drive letter of a CD drive that has been marked as write-enabled.
HasRecordableDrive Scans the system for a CD drive with write capability, returning TRUE if one is found.

Table 1: ICDBurn Methods

Now, dig deeper into each of these functions' purpose.

The Burn method

As mentioned in Table 1, this function instructs the data to be copied from the staging area to a writable CD. All this function needs to work is a handle of the parent window of the user interface (UI); this gets supplied through its hwnd parameter.

What is the "Staging Area?"

The staging area is the temporary burn location for files waiting to be written to disk. So, when you select all the files and folders you want to burn to disk, they are copied to this temporary area, and from there, they are written to disk. Typically, the staging area has a default location of %userprofile%\Local Settings\Application Data\Microsoft\CD Burning; one can translate this to C:\Documents and Settings\username\Local Settings\Application Data\Microsoft\CD Burning.

For the Burn method to actually work, you need to determine precisely where the user's temporary burn folder actually is. You can achieve this through the use of the following APIs:

API Name Purpose
SHGetFolderPath Takes the CSIDL of a folder and returns the pathname
SHGetSpecialFolderPath Retrieves the path of a special folder, identified by its CSIDL
SHGetFolderLocation Retrieves the path of a folder as an ITEMIDLIST structure
SHGetSpecialFolderLocation Retrieves a pointer to the ITEMIDLIST structure of a special folder
SHGetFolderPathAndSubDir Accepts the CSIDL of a folder and returns the path to that directory, appending a user-provided subdirectory path

Table 2: Staging Area APIs

CSIDL

If you (hopefully) read the Table 2, you would have noticed that I kept referring to the term CSIDL. Let me explain. CSIDL values provide a unique system-independent way to identify special folders used frequently by applications, but which may not have the same name or location on any given system. For example, the system folder may be "C:\Windows" on one system and "C:\Winnt" on another. Table 3 shows the available CSIDLs:

CSIDL Meaning
CSIDL_FLAG_CREATE Combine this CSIDL with any of the following CSIDLs to force the creation of the associated folder.
CSIDL_ADMINTOOLS The file system directory that is used to store administrative tools for an individual user. The Microsoft Management Console (MMC) will save customized consoles to this directory, and it will roam with the user.
CSIDL_ALTSTARTUP The file system directory that corresponds to the user's non localised Startup program group.
CSIDL_APPDATA The file system directory that serves as a common repository for application-specific data.
CSIDL_BITBUCKET The virtual folder containing the objects in the user's Recycle Bin.
CSIDL_CDBURN_AREA The file system directory acting as a staging area for files waiting to be written to CD.
CSIDL_COMMON_ADMINTOOLS The file system directory containing administrative tools for all users of the computer.
CSIDL_COMMON_ALTSTARTUP The file system directory that corresponds to the non-localised Startup program group for all users.
CSIDL_COMMON_APPDATA The file system directory containing application data for all users.
CSIDL_COMMON_DESKTOPDIRECTORY The file system directory that contains files and folders that appear on the desktop for all users.
CSIDL_COMMON_DOCUMENTS The file system directory that contains documents that are common to all users.
CSIDL_COMMON_FAVORITES The file system directory that serves as a common repository for favorite items common to all users.
CSIDL_COMMON_MUSIC The file system directory that serves as a repository for music files common to all users.
CSIDL_COMMON_PICTURES The file system directory that serves as a repository for image files common to all users.
CSIDL_COMMON_PROGRAMS The file system directory that contains the directories for the common program groups that appear on the Start menu for all users.
CSIDL_COMMON_STARTMENU The file system directory that contains the programs and folders that appear on the Start menu for all users.
CSIDL_COMMON_STARTUP The file system directory that contains the programs that appear in the Startup folder for all users.
CSIDL_COMMON_TEMPLATES The file system directory that contains the templates that are available to all users.
CSIDL_COMMON_VIDEO The file system directory that serves as a repository for video files common to all users.
CSIDL_CONTROLS The virtual folder containing icons for the Control Panel applications.
CSIDL_COOKIES The file system directory that serves as a common repository for Internet cookies.
CSIDL_DESKTOP The virtual folder representing the Windows desktop, the root of the namespace.
CSIDL_DESKTOPDIRECTORY The file system directory used to physically store file objects on the desktop (not to be confused with the desktop folder itself).
CSIDL_DRIVES The virtual folder representing My Computer, containing everything on the local computer: storage devices, printers, and Control Panel.
CSIDL_FAVORITES The file system directory that serves as a common repository for the user's favorite items.
CSIDL_FONTS A virtual folder containing fonts.
CSIDL_HISTORY The file system directory that serves as a common repository for Internet history items.
CSIDL_INTERNET A virtual folder representing the Internet.
CSIDL_INTERNET_CACHE The file system directory that serves as a common repository for temporary Internet files.
CSIDL_LOCAL_APPDATA The file system directory that serves as a data repository for local (no roaming) applications.
CSIDL_MYDOCUMENTS The virtual folder representing the My Documents desktop item.
CSIDL_MYMUSIC The file system directory that serves as a common repository for music files.
CSIDL_MYPICTURES The file system directory that serves as a common repository for image files.
CSIDL_MYVIDEO The file system directory that serves as a common repository for video files.
CSIDL_NETHOOD A file system directory containing the link objects that may exist in the My Network Places virtual folder.
CSIDL_NETWORK A virtual folder representing Network Neighborhood, the root of the network namespace hierarchy.
CSIDL_PERSONAL The file system directory used to physically store a user's common repository of documents.
CSIDL_PRINTERS The virtual folder containing installed printers.
CSIDL_PRINTHOOD The file system directory that contains the link objects that can exist in the Printers virtual folder.
CSIDL_PROFILE The user's profile folder.
CSIDL_PROFILES The file system directory containing user profile folders.
CSIDL_PROGRAM_FILES The Program Files folder.
CSIDL_PROGRAM_FILES_COMMON A folder for components that are shared across applications.
CSIDL_PROGRAMS The file system directory that contains the user's program groups.
CSIDL_RECENT The file system directory that contains shortcuts to the user's most recently used documents.
CSIDL_SENDTO The file system directory that contains Send To menu items.
CSIDL_STARTMENU The file system directory containing Start menu items.
CSIDL_STARTUP The file system directory that corresponds to the user's Startup program group.
CSIDL_SYSTEM The Windows System folder.
CSIDL_TEMPLATES The file system directory that serves as a common repository for document templates.
CSIDL_WINDOWS The Windows directory or SYSROOT.

Table 3: CSIDL Descriptions

What a mouthful! As you can see, all of the system special folders are listed here.

ITEMIDLIST

The ITEMIDLIST structure defines an element in an item identifier list (the only member of this structure is an SHITEMID structure). An item identifier list consists of one or more consecutive ITEMIDLIST structures packed on byte boundaries, followed by a 16-bit zero value. An application can walk a list of item identifiers by examining the size specified in each SHITEMID structure and stopping when it finds a size of zero. A pointer to an item identifier list, is called a PIDL (pronounced piddle). Note, however, that it is unnecessary to use the ITEMIDLIST structure; because the PIDL is a long, it can be passed and referenced as such when implementing the APIs.

Now, move on to the GetRecorderDriveLetter method.

GetRecorderDriveLetter

As mentioned in Table 1, this method retrieves the drive letter of a CD drive that has been marked as write enabled. Obviously, this method will need a parameter supplying you with the write-enabled CD drive, but it also includes another parameter that makes sure that the "drive letter parameter" is the valid size. Based on this, it either returns an error code, or the particular drive letter.

HasRecordableDrive

Based on its descriptive name, you can see that this method determines whether you indeed have a recordable device present. If it found one, it simply returns true; else, false.

Shell Interfaces

A list of all the Windows Shell Interfaces and their functions can be found here: http://msdn.microsoft.com/en-us/library/bb774328(VS.85).aspx.

Writing CDs with C# and Windows XP's ICDBurn Interface

Your Project: SimpleBurn

Goal

The purpose of SimpleBurn (if you haven't figured it out yet) would be to create a program to write CDs with.

Now, create your project. Follow these steps:

  1. Launch Microsoft Visual Studio 2005 or Visual C# 2005 Express.
  2. In the Project types box, select Visual C# Projects.
  3. In the Project Templates box, select Windows Application.
  4. Name the project SimpleBurn.

Design

With SimpleBurn, I have decided to create a skinnable design, as well as use Pictures as buttons, instead of real buttons. In the spirit of burning, I have given all the pictures a "Burning" look. I have included all the Pictures with this article; feel free to create your own as well. Now, start with the design. Modify the Form displayed's properties as follows:

Property Value
Name frmSimpleBurn
BackColor White
BackgroundImage Smokey.gif
FormBorderStyle None
Width 786
Height 556

Table 4: Form Properties

Add 8 (eight) PictureBoxes to your form and set their respective properties as follows:

Control Property Value
PictureBox1 Name picStage
  Image Stage.png
  SizeMode AutoSize
PictureBox2 Name picFiles
  Image Files.png
  SizeMode AutoSize
PictureBox3 Name picBurner
  Image BurnerPres.png
  SizeMode AutoSize
PictureBox4 Name picDrive
  Image Drive.png
  SizeMode AutoSize
PictureBox5 Name picAddFiles
  Image AddFiles.png
  SizeMode AutoSize
PictureBox6 Name picAddFolders
  Image AddFolders.png
  SizeMode AutoSize
PictureBox7 Name picBurn
  ImageBurn.png
  SizeMode AutoSize
PictureBox8 Name picExit
  Image Exit.png
  SizeMode AutoSize

Table 5: All the PictureBox Properties

Some of these PictureBoxes will be used as Labels, and some will be used as Buttons, so you have to be careful where you place them.

Add 1 (one) TextBox to the Form, and assign the following Properties:

Property Value
Name txtStagingArea
BackColor Yellow
Location You should place this TextBox next to/underneath the picStage PictureBox

Table 6: TextBox Properties

This TextBox will be used to indicate the Staging Area path.

Add 1 (one) ListBox to the Form and set the Following Properties:

Property Value
Name lbFiles
BackColor Yellow
Location You should place this ListBox next to/underneath the picFiles PictureBox

Table 7: ListBox Properties

lbFiles will be use to show which Files and folders you have added.

Add 2 (two) Labels to your Form, and set the following Properties:

Control Property Value
Label1 Name lblDriveLetter
  BackColor Yellow
  AutoSize True
  Location You should place this Label on top of/next to the picDrive PictureBox
Label2 Name lblHasBurner
  BackColor Yellow
  AutoSize True
  Location You should place this Label on top of/next to the picBurner PictureBox

Table 8: Label Properties

lblDriveLetter will show the Recordable device's drive letter. lblHasBurner will show either True or False, depending on whether you have a recordable drive present.

Your Form should now look similar to Figure 1.

[FormDesign.jpg]

Figure 1: The Form's scary design

Writing CDs with C# and Windows XP's ICDBurn Interface

Coding

Incorporating the ICDBurn Interface

This will enable you to use Windows XP's ICDBurn interface. To add an Interface to your project, simply select Project (from the Main menu), Add New Item, from the Project menu, and select Interface from the Templates box. Name it ICDBurnInterface.cs.

Ensure that you have the following Namespaces:

using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;

For simplicity sake, create a new Namespace for which all your project files will be part. Just above the Interface declaration, add the following:

namespace SimpleBurn
{

Remember to add the closing curly brace at the bottom of your code file as well.

You may now be wondering, how will you get access to the system's ICDBurn interface, is it a built-in type into the C# language already, what must you do?

Answering these questions is quite easy. Let me explain. The COM Interop provides access to an existing COM component but does not require that you modify the original component. To reference COM objects and interfaces in Visual Studio .NET or in Visual Studio 2005, you must include a .NET definition for the COM component. In Visual C# .NET, you can declare COM definitions manually if you use the ComImport attribute. Add the following just above the Interface declaration:

[ComImport]
//System's ICDBurn Interface
[Guid("3d73a659-e5d0-4d42-afc0-5121ba425c8d")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]

Don't worry; the above looks complicated, but really isn't. What I did here was to first add the ComImport attribute because of the reasons I explained in the previous paragraph. Secondly, I got access to the system's ICDBurn interface by using its Globally Unique Identifier. This CLSID is stored in the Registry (along with all other Interfaces) under HKEY_LOCAL_MACHINE\SOFTWARE\Classes\Interface\{3d73a659-e5d0-4d42-afc0-5121ba425c8d}. For more information on CLSID and its various uses, feel free to look at this article: http://www.codeguru.com/vb/gen/vb_system/win32/article.php/c13987/. Lastly, because all COM Interfaces are inherited directly or indirectly from IUnknown, you must implement IUnknown. The IUnknown Interface lets clients get pointers to other interfaces on a given object through the QueryInterface method, and manage the existence of the object through the IUnknown::AddRef and IUnknown::Release methods. All other COM interfaces are inherited, directly or indirectly, from IUnknown. Therefore, the three methods in IUnknown are the first entries in the VTable for every interface. The IUnknown Interface contains these three methods:

Method Description
QueryInterface Returns pointers to supported interfaces
AddRef Increments Reference Count
Release Decrements Reference Count

Table 9: IUnknown Methods

All you still need to do in the ICDBurn Interface code file is to add the physical Interface declaration:

internal interface ICDBurn
{
   uint GetRecorderDriveLetter(
      [MarshalAs(UnmanagedType.LPWStr,SizeParamIndex =1)]
      string pszDrive,
      uint cch);    //Get Drive Letter of Write-Enabled CD Drive

   // Copy From Staging Area To Writable CD.
   int SBBurn(IntPtr hWnd);

   //Determines Presence Of Recordable Drive
   int SBRecordableDrivePresent(ref int iHasRecorder);
}

All you did here was to add the ICDBurn Interface, with all its various methods, as explained in Table 1.

Burn baby, burn!

Not quite yet, but the next class you are going to add will allow you to do just that. Add a new class named SBBurn.

First, you need to ensure that you have all the correct Namespaces ready. You know the drill:

using System;
using System.Runtime.InteropServices;    // For APIs
using System.Windows.Forms;

And, as you did with the previous code file, you added your own Namespace declaration, so do that now:

namespace SimpleBurn
{

Remember to also add the closing curly brace at the bottom of the code file as well. The reason you added it is just so that your code is more organised, and you know which objects apply to which files.

The APIs

If you can recall from Table 2, I explained which APIs you will need to get the staging area folder. Declare them now, as well as their constants. Add the following APIs to SBBurn:

//Converts An Item Identifier List To A File System Path
[DllImport("Shell32.dll")]
public static extern bool
   SHGetPathFromIDList(IntPtr ilPtr, string sPath);

//Retrieves A Pointer To The ITEMIDLIST Structure Of A Special Folder.
[DllImport("shell32.dll", CharSet = CharSet.Auto)]
private static extern uint SHGetSpecialFolderLocation(IntPtr hWnd,
   CSIDL nFolder, out IntPtr Pidl);

private const int CSIDL_CDBURN_AREA = 59;    //Burn Area API Constant

Add the CSIDL enumeration (as explained in Table 3):

/// <summary>
/// Provide A Unique System-Independent Way To Identify Special
/// Folders Used Frequently  By Applications, But Which May Not
/// Have The Same Name Or Location On Any Given System.
/// </summary>
public enum CSIDL
{
   Desktop = 0x0000,        // Desktop
   Internet = 0x0001,       // Internet Explorer (Icon On Desktop)
   Programs = 0x0002,       // Start Menu\Programs
   Controls = 0x0003,       // My Computer\Control Panel
   Printers = 0x0004,       // My Computer\Printers
   Personal = 0x0005,       // My Documents
   Favorites = 0x0006,      // User Name\Favorites
   Startup = 0x0007,        // Start Menu\Programs\Startup
   Recent = 0x0008,         // User Name\Recent
   SendTo = 0x0009,         // User Name\SendTo
   BitBucket = 0x000a,      // Desktop\Recycle Bin
   StartMenu = 0x000b,      // User Name\Start Menu
   MyDocuments = 0x000c,    // Logical "My Documents" Desktop Icon
   MyMusic = 0x000d,        // "My Music" Folder
   MyVideo = 0x000e,        // "My Videos" Folder
   DesktopDirectory = 0x0010,    // User Name\Desktop
   Drives = 0x0011,         // My Computer
   Network = 0x0012,        // Network Neighborhood (My Network Places)
   Nethood = 0x0013,        // User Name\NetHood
   Fonts = 0x0014,          // Windows\Fonts
   Templates = 0x0015,
   CommonStartMenu = 0x0016,           // All Users\Start Menu
   CommonPrograms = 0x0017,            // All Users\Start Menu\Programs
   CommonStartup = 0x0018,             // All Users\Startup
   CommonDesktopDirectory = 0x0019,    // All Users\Desktop
   AppData = 0x001a,                   // User Name\Application Data
   PrintHood = 0x001b,                 // User Name\PrintHood
   // User Name\Local Settings\Applicaiton Data ( Non Roaming )
   LocalAppData = 0x001c,
   AltStartup = 0x001d,                // Non Localized Startup
   CommonAltStartup = 0x001e,          // Non Localized Common Startup
   CommonFavorites = 0x001f,
   InternetCache = 0x0020,
   Cookies = 0x0021,
   History = 0x0022,
   CommonAppdata = 0x0023,            // All Users\Application Data
   Windows = 0x0024,                  // GetWindowsDirectory()
   System = 0x0025,                   // GetSystemDirectory()
   ProgramFiles = 0x0026,             // C:\Program Files
   MyPictures = 0x0027,               // C:\Program Files\My Pictures
   Profile = 0x0028,                  // USERPROFILE
   SystemX86 = 0x0029,                // X86 System Directory On RISC
   ProgramFilesX86 = 0x002a,          // X86 C:\Program Files On RISC
   ProgramFilesCommon = 0x002b,       // C:\Program Files\Common
   ProgramFilesCommonx86 = 0x002c,    // X86 Program Files\Common On RISC
   CommonTemplates = 0x002d,          // All Users\Templates
   CommonDocuments = 0x002e,          // All Users\Documents
   // All Users\Start Menu\Programs\Administrative Tools
   CommonAdminTools = 0x002f,
   // User Name\Start Menu\Programs\Administrative Tools
   AdminTools = 0x0030,
   Connections = 0x0031,           // Network And Dial-up Connections
   CommonMusic = 0x0035,           // All Users\My Music
   CommonPictures = 0x0036,        // All Users\My Pictures
   CommonVideo = 0x0037,           // All Users\My Video
   Resources = 0x0038,             // Resource Direcotry
   ResourcesLocalized = 0x0039,    // Localized Resource Directory
   // Links To All Users OEM specific apps
   CommonOemLinks = 0x003a,
   // USERPROFILE\Local Settings\Application Data\Microsoft\CD Burning
   CdBurnArea = 0x003b,
   // Computers Near Me (Computers From Workgroup Membership)
   ComputersNearMe = 0x003d,
   // Combine With CSIDL_ Value To Force Folder Creation In
   // SHGetFolderPath()
   FlagCreate = 0x8000,
   // Combine With CSIDL_ Value To Return An Unverified Folder Path
   FlagDontVerify = 0x4000,
   // Combine With CSIDL_ Value To Insure Non - Alias Versions Of
   // The PIDL
   FlagNoAlias = 0x1000,
   // Combine With CSIDL_ Value To Indicate Per - User Init
   // (eg. Upgrade)
   FlagPerUserInit = 0x0800,
   FlagMask = 0xFF00,         // Mask For All Possible Flag Values
}

Hey, what about me?

Now that you have created most of the needed objects, you still haven't created your Interface object to to refer to the ICDBurn Interface. Because of that, you still haven't created variables to store the staging area's path, to determine whether you have a recordable drive present, or to determine the recordable drive's drive letter. Create them now:

private ICDBurn SBICDBurn;             //ICDBurn Object

private bool SBDisposedVal = false;    //Detect Redundant Calls
private bool SBRecordableDrive;        //Recording Drive Present

private string SBriveLetter;           //Recorder's Drive Letter
//Folder Holding Files To Be Burnt
private string SBStageFolder;

Writing CDs with C# and Windows XP's ICDBurn Interface

What if something goes wrong?

If you have ever burnt information to a CD, you would know that sometimes problems do occur. There may not be enough space available on the disk, the disk may be damaged, the writer may be damaged—anything might have happened. To determine what has happened, the Image Mastering Application Programming Interface (IMAPI) provides a complete list of possible errors. This API allows you to burn audio and data CD-R and CD-RW discs. IMAPI includes the following Interfaces:

Interface Name Description
IDiscMaster The IDiscMaster interface allows an application to reserve an image mastering API, enumerate disc mastering formats and disc recorders supported by an image mastering object, and start a simulated or actual burn of a disc.
IDiscMasterProgressEvents The IDiscMasterProgressEvents interface provides a single interface for all callbacks that can be made from IMAPI to an application.
IDiscRecorder The IDiscRecorder interface enables access to a single disc recorder device, labeled the active disc recorder.
IJolietDiscMaster The IJolietDiscMaster interface enables the staging of a CD data disc.
IRedbookDiscMaster The IRedbookDiscMaster interface enables the staging of an audio CD image.

Table 10: IMAPI Interfaces

So, as you now can realise, IMAPI and the ICDBurn Interface work hand-in hand when it comes to the actual burning of the disk. Now, in your program, you need to provide a way to determine the CD burning errors. Add the following enumeration just above your class constructor:

//Some Of The Returned Errors By ICDBurn Interface
public enum ImapiErrorValues : uint
{
   Ok = 0,    //Everything Fine
   False = 1,
   //Ignored Unknown Property Passed
   PropertiesIgnored = 2147746304,
   BufferTooSmall = 2147746305,     //Buffer Too Small
   //DiscMaster Has Not Been Opened Yet
   NotOpened = 2147746315,
   //Recorder Object Not Initialized
   NotInitialized = 2147746316,
   UserAbort = 2147746317,            //Cancelled By The USer
   Generic = 2147746318,              //Generic Error
   MediumNotPresent = 2147746319,     //No Disk In Drive
   MediumInvalidType = 2147746320,    //Disk Not Correct Type
   //Recorder Doesn't Support Properties
   DeviceNoProperties = 2147746321,
   //Device Cannot Be Used / Already In Use
   DeviceNotAccessible = 2147746322,
   DeviceNotPresent = 2147746323,     //Device Not Present
   DeviceInvalidType = 2147746324,
   //Drive Interface Cannot Be Initialized
   InitializeWrite = 2147746325,
   //Drive Interface Cannot Be Initialised For Closing
   InitializeEndWrite = 2147746326,
   FileSystem = 2147746327,        //File System Error
   FileAccess = 2147746328,        //Error While Writing Image File
   DiskInfo = 2147746329,          //Error While Reading Disk Data
   TrackNotOpen = 2147746330,      //Audio Track Not Open For Writing
   //Audio Track Already Staged    // Opened For Writing
   TrackOpen = 2147746331,
   DiskFull = 2147746332,          //Disk Is Full
   BadJolietName = 2147746333,     //Badly named Element
   //Image Is Corrupted / Damaged  // Cleared
   InvalidImage = 2147746334,
   NoActiveFormat = 2147746335,    //Recording Format Not Selected Yet
   NoActiveRecorder = 2147746336,  //Recorder Not Selected Yet
   WrongFormat = 2147746337,
   AlreadyOpen = 2147746338,       //DiscMaster Already Open
   //Disk Was Removed From Active Recorder
   WrongDisk = 2147746339,
   FileExists = 2147746340,        //File Already Exists
   //Stash File Already In Use By Different Application
   StashInUse = 2147746341,
   //A Different Application IS Already Using The Device
   DeviceStillInUse = 2147746342,
   LossOfStreaming = 2147746343,      //Content Streaming Was Lost
   CompressedStash = 2147746344,      //Stash On Compressed Volume
   EncryptedStash = 2147746345,       //Stash On Encrypted Volume
   //Not Enough Free Space to Create Stash File
   NotEnoughDiskForStash = 2147746346,
   RemovableStash = 2147746347,        //Stash On Removable Volume
   CannotWriteToMedia = 2147746348,    //Cannot Write To Media
   TrackNotBigEnough = 2147746349,     //Track Not Big Enough
   //Attempted To Create Boot Image On Non - Blank Disk
   BootImageAndNonBlankDisk = 2147746350
}

Starting the fire: The Constructor

In a manner of speaking. Now, you need to initialise the startup values for your class and physically create the Interface object. Set all the Upfront properties in your Class Constructor as follows:

/// <summary>
/// Set Upfront Properties
/// </summary>
public SBBurn()
{
   if (!CreateInterface())    //Try To Create Interface
      //Cannot
      throw new Exception("Cannot Create ICDBurn Interface");

   //Is Recorder Available?
   SBRecordableDrive = SBRecorderAvailable();

   if (SBRecordableDrive)    //If Available

      //Store Drive Letter Of Recordable Device
      SBriveLetter = SBGetDriveLetter();

   SBStageFolder = GetBurnStagingAreaFolder();    //Set Stage Folder
}

Create the Interface:

/// <summary>
/// Create ICD_Burn Interface
/// </summary>
private bool CreateInterface()
{

   //Shell Object
   Guid SBShellCDBurnStr =
      new Guid("FBEB8A05-BEEE-4442-804e-409d6c4515e9");
   //Interface IID
   Guid SBInterfaceID_ICDBurn =
      new Guid("3d73a659-e5d0-4d42-afc0-5121ba425c8d");

   //Create COM Object Through Its GUID
   Type ComType = Type.GetTypeFromCLSID(SBShellCDBurnStr);

   if (ComType == null)    //If Empty
   {
      throw new Exception("Operating System Not Supported");
   }

   object SBICDBurnObj = null;    //Object To "Hold" The Burn Object

   SBICDBurnObj = Activator.CreateInstance(ComType);    //Create It

   SBICDBurn = (ICDBurn)SBICDBurnObj;    //Set Value

   SBICDBurnObj = null;    //Release

   if (SBICDBurn != null)
   {
      return true;    //Interface Exists
   }
   else
   {
      //Interface Doesn't Exist
      MessageBox.Show("Cannot Create Interface");

      return false;
   }

}

Adding Gasoline: The Properties

All you still need to do with the SBBurn class is to add some properties to it, to let the form object know what the "status" of your Interface object is. I mean, you need a way to export the drive letter of the recordable device, the staging area path, as well as the fact that you have a recordable device—or not. Now, add Properties to your SBBurn class.

/// <summary>
/// Do We Have A Recordable Device?
/// >/summary>
public bool SBHasRecordableDrive
{
   get
   {
      return SBRecordableDrive;    //Return Value - True Or False
   }
}

/// <summary>
/// Get Recorder's Drive Letter
/// </summary>
public string SBRecorderDrivePath
{
   get
   {
      return SBriveLetter;    //Return Value
   }
}

/// <summary>
/// Get Temporary Burn Folder
/// </summary>
public string SbStageAreaFolderPath
{
   get
   {
      return SBStageFolder;    //Return Value
   }
}

Writing CDs with C# and Windows XP's ICDBurn Interface

Working with the Properties: The Methods

Add the following methods to your class:

/// <summary>
/// Method To Determine The SBRecordableDrivePresent Property
/// And To Set The Result = To The Drive
/// </summary>
private bool SBRecorderAvailable()
{
   if (SBICDBurn == null)    //If No Recorder
      return false; 

   int SBDrive = 0;          //Initialize Drive
   int SBResult;

   //Get Drive
   SBResult = SBICDBurn.SBRecordableDrivePresent(ref SBDrive);

   return Convert.ToBoolean(SBDrive);    //Convert To True / False
}

/// <summary>
/// Get Drive Letter
/// </summary>
private string SBGetDriveLetter()
{
   // Only one drive on the system can have
   // "allow cd burning on this drive" set on its properties.

   if (SBHasRecordableDrive == false)    //If No Drive Found
      return "No Available Drive";

   if (SBICDBurn == null)    //If We Cannot Create ICDBurn Interface
      throw new Exception("ICDBurn is nothing!");

   string SBDrive = " ";    //Initialize Drive Letter Variable

   //Get Drive Letter
   uint SBResult = (uint)SBICDBurn.GetRecorderDriveLetter(SBDrive, 4);

   //If Everything Not OK
   if (SBResult != (uint)ImapiErrorValues.Ok)
   {
      //Get Returned Error Code
      ImapiErrorValues SBErrorCode = (ImapiErrorValues)SBResult;
         throw new Exception("Error Returned By IMAPI " + SBErrorCode);
   }

   //Make Returned Drive Letter String More Readable
   SBDrive = SBDrive.TrimEnd(new char[] { char.MinValue });

   return SBDrive;    //Return Value
}

/// <summary>
/// Get The Temporary burn Folder On The System
/// </summary>
public static string GetBurnStagingAreaFolder()
{
   IntPtr pidl = Marshal.AllocHGlobal(1024);    //Make Space

   //Get The Temporary Burn Folder
   SHGetSpecialFolderLocation(pidl, CSIDL.CdBurnArea, out pidl);

   //Create Enough Space In String
   string path = new string(char.MinValue, 260);

   bool result = SHGetPathFromIDList(pidl, path);    //See If Possible

   if (result == false)
   {
      //No Staging Area Specified
      throw new Exception("No Staging Area Specified");
   }
   Marshal.FreeHGlobal(pidl);    //Free The Space

   //Format String Appropriately
   path = path.TrimEnd(char.MinValue);

   return path;    //Return Value
}

The Burn method

/// <summary>
/// Copy From Stage To CD
/// </summary>
public ImapiErrorValues Burn(IntPtr h)
{
   return (ImapiErrorValues)SBICDBurn.SBBurn(h);    //Burn

}

As you can see, this method returns an ImapiErrorValues object. If the result were OK, it would burn; otherwise, it will not burn, but it will inform you about the error that occurred.

Disposal

/// <summary>
/// Disposal Of Burner
/// </summary>
/// <param name="disposing"></param>
protected void Dispose(bool disposing)
{
   if (!this.SBDisposedVal)
   {
      if (disposing)
      {
         if (SBICDBurn != null)
         {
            //Dispose Object, To Be Able To Burn Again
            Marshal.FinalReleaseComObject(SBICDBurn);

            SBICDBurn = null;
         }
      }
   }
   this.SBDisposedVal = true;
}

Inviting the form to the barbeque

Surprise, surprise, I'm not going to start off mentioning the Namespaces, because all should be included already, for now. You still need to add the SimpleBurn namespace, though:

namespace SimpleBurn
{
   partial class frmSimpleBurn : System.Windows.Forms.Form
   {

While you're at it, make sure that your class declaration looks like mine in the above segment.

Creating the interface objects

Declare the following:

private SBBurn SBBurner;    //Burner Object

private string SBStage;     //Stage

Here, you create the Burn object, all of the SBBurn class' functionalities are inside SBBurner, so, from within your code, whenever you want to get access to those methods, you just need to refer to SBBurner. You also created the SBStage object that will be used to store the system's temporary Burn folder's location.

The Constructor

There's nothing really overly complicated here; you just need to instantiate the Burner object, obtain the staging area path, and display this information on the form. You also need to display either a True or False when a recordable drive is present. Edit your Constructor to reflect mine:

public frmSimpleBurn()
{
   // This call is required by the Windows Form Designer.
   InitializeComponent();

   SBBurner = new SBBurn();       //Create SBBurn Object

   //Store CSIDL CdBurnArea Location
   SBStage = SBBurner.SbStageAreaFolderPath;

   if(SBStage == string.Empty)    //If Not Found

      //Hard Code Default Location
      SBStage = "C:\\Documents and Settings\\Administrator\\
         Local Settings\\Application Data\\Microsoft\\CD Burning";

   //Recordable Device = True / False?
   this.lblHasBurner.Text = this.lblHasBurner.Text +
      SBBurner.SBHasRecordableDrive;

   //Recorder Drive Letter
   this.lblDriveLetter.Text = SBBurner.SBRecorderDrivePath;

   this.txtStagingArea.Text = SBStage;    //Display Stage Path
}

Writing CDs with C# and Windows XP's ICDBurn Interface

Adding Folders

Add the following method to your form:

/// <summary>
/// Add Complete Folders To Be Burnt To Stage
/// </summary>
private void AddFolder(string SBMainDir,
   System.IO.DirectoryInfo SBCurrDir, bool SBRecurse)
{
   Application.DoEvents();
   this.lbFiles.Refresh();

   this.lbFiles.Items.Add(SBCurrDir.FullName);    //Add Directory

   string SBStagePath;

   if (SBMainDir == SBCurrDir.FullName)
   {
      SBStagePath = SBStage;    //Stage Path
   }
   else
   {
      string SBRelPath =
         SBCurrDir.FullName.Substring(SBMainDir.Length + 1);

      SBStagePath = System.IO.Path.Combine(SBStage, SBRelPath);

      if (System.IO.Directory.Exists(SBStagePath) == false)
      {
         //Create Directory
         System.IO.Directory.CreateDirectory(SBStagePath);
      }
   }

   //Copy Files Found
   foreach (System.IO.FileInfo file in SBCurrDir.GetFiles())
   {
      System.IO.File.Copy(file.FullName,
         System.IO.Path.Combine(SBStagePath, file.Name));

      this.lbFiles.Items.Add(file.FullName);    //Add To List
   }

   if (SBRecurse)    //Loop Through Sub Directories
   {
      foreach (System.IO.DirectoryInfo dir in
         SBCurrDir.GetDirectories())
      {
         AddFolder(SBMainDir, dir, true);    //Add Folders
      }
   }

}

The whole logic behind this procedure is to copy the files and subfolders in the folders you will select to the staging area. It is actually quite simple because, as you loop through the folders that need to be created, you create them on the staging area, and then recurse through the files so that the complete folders are copied. Now, call this method from your picAddFolders_Click method:

/// <summary>
/// Add Folders
/// </summary>
private void picAddFolders_Click(object sender, EventArgs e)
{
   FolderBrowserDialog SBOFD = new FolderBrowserDialog();

   //My Computer
   SBOFD.RootFolder = Environment.SpecialFolder.MyComputer;

   SBOFD.ShowNewFolderButton = false;    //Cannot Create New Folders

   System.Windows.Forms.DialogResult SBResult = SBOFD.ShowDialog();

   //If Not File Selected / Cancelled
   if (SBResult != System.Windows.Forms.DialogResult.OK)
      return;

   //Recursing?
   System.Windows.Forms.DialogResult SBResult2 =
      MessageBox.Show("Include Subdirectories?",
      "Include SubDirectories?", MessageBoxButtons.YesNoCancel);

   if (SBResult2 == DialogResult.Cancel)    //No
      return;

   bool SBRecurse = (SBResult2 == DialogResult.Yes);    //Yes

   //Loop
   AddFolder(SBOFD.SelectedPath,
      new System.IO.DirectoryInfo(SBOFD.SelectedPath), SBRecurse);
}

This enables you to select all the folders you want to burn via the use of the FolderBrowserDialog object. If you decided to include all subdirectories as well, the AddFolder method you created earlier will do just that.

Adding files

Adding certain files from various folders will be done in the picAddFiles_Click event:

/// <summary>
/// Add Files
/// </summary>
private void picAddFiles_Click(object sender, EventArgs e)
{
   //New Open File Dialog
   OpenFileDialog SBOFD = new OpenFileDialog();

   //GUID Of My Computer
   string SBMyComputerGUID = "::
      {20D04FE0-3AEA-1069-A2D8-08002B30309D}";

   SBOFD.InitialDirectory = SBMyComputerGUID;    //Initial Directory

   SBOFD.Multiselect = true;    //Can Select More Than One File

   System.Windows.Forms.DialogResult SBResult = SBOFD.ShowDialog();

   //If Not File Selected / Cancelled
   if (SBResult != System.Windows.Forms.DialogResult.OK)
      return;

   foreach (string SBFile in SBOFD.FileNames)    //Get All File Names
   {
      if (System.IO.File.Exists(SBFile))
      {
         //Copy Selected Files To Stage Folder
         System.IO.File.Copy(SBFile, SBStage + "\\" +
            System.IO.Path.GetFileName(SBFile));
      }
      this.lbFiles.Items.Add(SBFile);    //Add To List
   }
}

With the code segment above, you created a new OpenFileDialog and set its InitialDirectory to the GUID of My Computer. I chose to do this just so that you can see that you can use the GUIDs (as explained earlier) with this Dialog as well. You set the MultiSelect property to true, so that you can select more than one file at a time, and then lastly, you just copy the selected files over to the staging area. At run time, as soon as a file or folder gets added to the Staging Area (see Figure 2), a notification, similar to what is shown in Figure 3, is shown.

[FilesAndFoldersAdded.jpg]

Figure 2: Adding Files and Folders during run time

[YouHaveFilesWaiting.png]

Figure 3: You have files waiting to be burnt

If you click the displayed Notification, you can see which files are waiting to be written onto the CD, as displayed in Figure 4.

[FilesReady.png]

Figure 4: Files waiting To Be Burnt

Deleting folders

/// <summary>
/// Delete All When Done
/// </summary>
/// <param name="directory"></param>
private void DeleteFolder(System.IO.DirectoryInfo SBCurrDir)
{
   //Find Files On Stage
   foreach (System.IO.FileInfo file in SBCurrDir.GetFiles())
   {
      System.IO.File.Delete(file.FullName);    //Delete
   }

    //Loop Through All Sub Dirs
   foreach (System.IO.DirectoryInfo folder in
            SBCurrDir.GetDirectories())
   {
      DeleteFolder(folder);    //Delete
   }
   if (SBCurrDir.FullName != SBStage)

      System.IO.Directory.Delete(SBCurrDir.FullName);
}

Obviously, here you just loop through the folder you have selected, delete its contents, and then delete the folder itself from the staging area.

Writing CDs with C# and Windows XP's ICDBurn Interface

Light the fire: The picBurn_Click event

private void picBurn_Click(object sender, EventArgs e)
{
   SBBurner.Burn(this.Handle);    //Burn!

   //Delete Files On Stage Afterwards
   DeleteFolder(new System.IO.DirectoryInfo(SBStage));

   this.lbFiles.Items.Clear();    //Clear The ListBox
   this.lbFiles.Refresh();
}

Call the Burn method; then afterwards, just clear the staging area folder and the Files listbox.

Disposal

/// <summary>
/// Remove All Folders When Done
/// </summary>
private void frmSimpleBurn_FormClosing(object sender,
   System.Windows.Forms.FormClosingEventArgs e)
{
   if (SBBurner != null)
      DeleteFolder(new System.IO.DirectoryInfo(SBStage));

}

private void picExit_Click(object sender, EventArgs e)
{
   this.Close();    //Exit
}

Just in case there are still folders in the staging area, you just remove them when the form closes. If you were to Build and Run your project now, you would be able to Burn a CD, as described in the following sequence of pictures.

[CDWritingWizard.png]

Figure 5: the CD Writing Wizard starts

[Writing.png]

Figure 6: Writing Your Files

[FinishedWriting.png]

Figure 7: Finished the Writing process

[WrittenCDInsertedAgain.png]

Figure 8: Completed CD inserted again

[FilesOnCD.png]

Figure 9: Files now on the CD

Special effects

If you look at the Form's scary Background picture again, you will see that it is actually a picture of Flames. What you may not know is that I designed the picture so that it can be used as a skin for your form. That means that the outside parts of the picture will be made transparent. Currently, if you run the program, the Form is still a Form. You can fix this now. Add the following lines to the Form's Paint event:

/// <summary>
/// Create Background Skin
/// </summary>
private void frmSimpleBurn_Paint(object sender, PaintEventArgs e)
{
   Bitmap bmpSBBack;    //Create Bitmap Object

   bmpSBBack = (Bitmap)this.BackgroundImage;    //Obtain Form Image

   //Make All White Pixels Transparent
   bmpSBBack.MakeTransparent(Color.White);
}

A new Bitmap object gets created, and then set to the Form's BackgroundImage property. The last line is the actual trick. What it does is to set all White pixels to Transparent; this means that the outside areas of the picture that were white, are now transparent. Wait, before you run! You still need to make sure that the Drawing Namespace is included. If necessary, add it at the top of your code file:

using System.Drawing;

If you were to run your project now, you will see that it resembles a screen similar to the following:

[RunTime.jpg]

Figure 10: Your skinned Form

Moving the Form around

Because there is no Titlebar present, you cannot move the Form around the screen. What you will do now is to enable you to drag the Form around, by using any part of the Form.

Add the following variable, just above the Class declaration:

private Point SbMouseOffset;    //Mouse Starting Location

Edit the Form's MouseMove event to look like the following:

//Enable Form Dragging
private void frmSimpleBurn_MouseMove(object sender, MouseEventArgs e)
{
   if (e.Button == MouseButtons.Left)    //Left Button Pressed
   {
      Point SBMousePos = Control.MousePosition;    //Get Location

      SBMousePos.Offset(SbMouseOffset.X, SbMouseOffset.Y);

      Location = SBMousePos;
   }

}

Edit the Form's MouseDown event to look like the following:

/// <summary>
/// Move Form, While Moving Mouse
/// </summary>
private void frmSimpleButton_MouseDown(object sender,
   MouseEventArgs e)
{
   SbMouseOffset = new Point(-e.X, -e.Y);    //Move Form

}

That is it! Now you are able to move the Form around. You can click on any empty part of the Form, and start dragging. You can, of course, add similar logic to all the other controls on the form as well, to be able to click and drag anywhere on the Form.

Conclusion

I sincerely hope you have enjoyed this article as much as I did in explaining the ICDBurn Interface to you. Until next time, happy burning!



About the Author

Hannes du Preez

Hannes du Preez is a Microsoft MVP for Visual Basic. He is a trainer at a South African-based company. He is the co-founder of hmsmp.co.za, a community for South African developers.

Downloads

Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • On-demand Event Event Date: September 10, 2014 Modern mobile applications connect systems-of-engagement (mobile apps) with systems-of-record (traditional IT) to deliver new and innovative business value. But the lifecycle for development of mobile apps is also new and different. Emerging trends in mobile development call for faster delivery of incremental features, coupled with feedback from the users of the app "in the wild." This loop of continuous delivery and continuous feedback is how the best mobile …

  • Java developers know that testing code changes can be a huge pain, and waiting for an application to redeploy after a code fix can take an eternity. Wouldn't it be great if you could see your code changes immediately, fine-tune, debug, explore and deploy code without waiting for ages? In this white paper, find out how that's possible with a Java plugin that drastically changes the way you develop, test and run Java applications. Discover the advantages of this plugin, and the changes you can expect to see …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds