Burn CDs in C++ and C# With ICDBurn - Burn Files to CDs (
Page 4 of 4 )
Burn Files to CDs
In contrast to IMAPI's low-level COM interfaces, ICDBurn doesn't appear to be useful beyond the Windows shell. To prove that
this isn't the case, I've created a GUI-based file-backup utility, named CDBackup, for backing up a list of
files to one or more CDs. ICDBurn is a very important part of this utility.
CDBackup's sparse user interface (see Figure 5) consists of a list (with names of files to backup -- names are
dragged to and dropped on the list), a status label (displaying the name of each file copied to the staging area), and two
buttons for burning the named files and removing the highlighted name from the list.
ADVERTISEMENT
Figure 5: The Burn and Remove buttons are disabled if no filenames appear in the list.
To use CDBackup, drag and drop filenames onto the list, and click Burn. Insert a blank writable CD into the
default burner device and respond to the wizard. Once the wizard finishes, the CD is ejected. You might need to repeatedly
insert blank CDs and work with the wizard until the backup is done.
I've organized CDBackup's source code into three files. Listing 4 presents the main source file,
cdbackup.c. (Although I could have written this source code in C++ or C#, I chose to use C because
CDBackup isn't a big utility, and because I've always enjoyed working with the C language.)
Listing 4:cdbackup.c
// cdbackup.c
// Backup files to one or more CDs.
//#define _UNICODE
#include <process.h>
#include <shlobj.h>
#include <stdio.h>
#include <tchar.h>
#include "cdbackup.h"
#pragma resource "cdbackup.res"
#define APP_NAME _T ("CD Backup")
#define CSIDL_CDBURN_AREA 0x3b
#define INI_FILE _T ("cdbackup.ini")
typedef struct
{
const struct ICDBurnVtbl *lpVtbl;
}
ICDBurn;
typedef struct ICDBurnVtbl
{
HRESULT (__stdcall *QueryInterface) (ICDBurn *this, const IID *riid,
void **ppd);
ULONG (__stdcall *AddRef) (ICDBurn *this);
ULONG (__stdcall *Release) (ICDBurn *this);
HRESULT (__stdcall *GetRecorderDriveLetter) (ICDBurn *this, LPWSTR pszDrive,
UINT cch);
HRESULT (__stdcall *Burn) (ICDBurn *this, HWND hwnd);
HRESULT (__stdcall *HasRecordableDrive) (ICDBurn *this, BOOL *pfHasRecorder);
}
ICDBurnVtbl;
CLSID g_CLSID_CDBurn =
{
0xfbeb8a05, 0xbeee, 0x4442, { 0x80, 0x4e, 0x40, 0x9d, 0x6c, 0x45, 0x15,
0xe9 }
};
HANDLE g_hThread;
ICDBurn *g_pICDBurn;
IID g_IID_ICDBurn =
{
0x3d73a659, 0xe5d0, 0x4d42, {0xaf, 0xc0, 0x51, 0x21, 0xba, 0x42, 0x5c,
0x8d }
};
TCHAR g_szPathStagingArea [_MAX_PATH];
BOOL CALLBACK CDBDlgProc (HWND, UINT, WPARAM, LPARAM);
void BackupFiles (HWND);
void BackupThread (void *);
void DeleteStagingAreaContent (void);
BOOL LoadFilenames (HWND);
void SaveFilenames (HWND);
#pragma argsused
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hInstancePrev,
LPSTR lpszCmdLine, int iCmdShow)
{
BOOL hasRecorder = FALSE;
if (FAILED (SHGetFolderPath (NULL, CSIDL_CDBURN_AREA, NULL,
SHGFP_TYPE_CURRENT, g_szPathStagingArea)))
{
MessageBox (HWND_DESKTOP, _T ("Unable to obtain staging area location!"),
APP_NAME, MB_OK);
goto exit1;
}
if (FAILED (CoInitialize (NULL)))
{
MessageBox (HWND_DESKTOP, _T ("Unable to initialize COM!"), APP_NAME,
MB_OK);
goto exit1;
}
if (FAILED (CoCreateInstance (&g_CLSID_CDBurn, NULL, CLSCTX_INPROC_SERVER,
&g_IID_ICDBurn, (LPVOID *) &g_pICDBurn)))
{
MessageBox (HWND_DESKTOP, _T ("Unable to instantiate shell class!"),
APP_NAME, MB_OK);
goto exit2;
}
if (FAILED (g_pICDBurn->lpVtbl->HasRecordableDrive (g_pICDBurn,
&hasRecorder)))
{
MessageBox (HWND_DESKTOP,
_T ("Unable to determine if CD burner present!"), APP_NAME,
MB_OK);
goto exit3;
}
if (!hasRecorder)
{
MessageBox (HWND_DESKTOP, _T ("CD burner not present!"), APP_NAME,
MB_OK);
goto exit3;
}
DialogBox (hInstance, _T ("CDBDlgBox"), HWND_DESKTOP, CDBDlgProc);
exit3:
g_pICDBurn->lpVtbl->Release (g_pICDBurn);
exit2:
CoUninitialize ();
exit1:
return 0;
}
#pragma argsused
BOOL CALLBACK CDBDlgProc (HWND hWnd, UINT iMsg, WPARAM wParam, LPARAM lParam)
{
int i, nFiles;
DWORD dwExitCode;
TCHAR szPathFileToBackup [_MAX_PATH];
switch (iMsg)
{
case WM_COMMAND:
switch (LOWORD (wParam))
{
case IDB_BURN:
EnableWindow (GetDlgItem (hWnd, IDB_BURN), FALSE);
EnableWindow (GetDlgItem (hWnd, IDB_REMOVE), FALSE);
// Create a background thread for performing backup. A
// background thread is used because the backup task can be
// lengthy, which (if run on the main thread) would screw
// up GUI painting.
g_hThread = (HANDLE) _beginthread (BackupThread, 100000,
(void *) hWnd);
return TRUE;
case IDB_REMOVE:
i = SendDlgItemMessage (hWnd, IDL_FILES, LB_GETCURSEL, 0, 0);
// Delete the selected filename entry. Disable buttons and
// remove horizontal scrollbar if no more entries in list.
if (!SendDlgItemMessage (hWnd, IDL_FILES, LB_DELETESTRING,
i, 0))
{
EnableWindow (GetDlgItem (hWnd, IDB_BURN), FALSE);
EnableWindow (GetDlgItem (hWnd, IDB_REMOVE), FALSE);
// Remove the horizontal scrollbar.
SendDlgItemMessage (hWnd, IDL_FILES,
LB_SETHORIZONTALEXTENT, 0, 0);
}
else // If more entries, however, ...
{
// If no entry occupies the entry just deleted, select
// the first entry.
if (SendDlgItemMessage (hWnd, IDL_FILES, LB_SETCURSEL,
i, 0) == LB_ERR)
SendDlgItemMessage (hWnd, IDL_FILES, LB_SETCURSEL,
0, 0);
}
return TRUE;
case IDCANCEL:
// Prevent user from closing GUI while background thread is
// still alive. However, the GUI will close, even if the
// background thread is running, should GetExitCodeThread()
// fail -- g_hThread must have the THREAD_QUERY_INFORMATION
// access right.
if (GetExitCodeThread (g_hThread, &dwExitCode))
if (dwExitCode == STILL_ACTIVE)
{
MessageBox (hWnd, _T ("Backup to CD in progress!"),
APP_NAME, MB_OK);
return TRUE;
}
SaveFilenames (hWnd);
DragAcceptFiles (hWnd, FALSE);
EndDialog (hWnd, 0);
return TRUE;
}
break;
case WM_DROPFILES:
// Prepare to receive pathnames of files and/or directories to drop.
nFiles = DragQueryFile ((HANDLE) wParam, 0xFFFFFFFF, NULL, 0);
for (i = 0; i < nFiles; i++)
{
// Obtain next file or directory pathname to drop.
DragQueryFile ((HANDLE) wParam, i, szPathFileToBackup,
sizeof (szPathFileToBackup));
// Only file pathnames are accepted in this version of CDBackup.
if (!(GetFileAttributes (szPathFileToBackup) &
FILE_ATTRIBUTE_DIRECTORY))
SendDlgItemMessage (hWnd, IDL_FILES, LB_ADDSTRING, 0,
(LPARAM) szPathFileToBackup);
}
if (nFiles)
{
HDC hDC;
TEXTMETRIC tm;
// Set the horizontal scrolling extent to _MAX_PATH characters
// of average character width.
hDC = GetDC (hWnd);
GetTextMetrics (hDC, &tm);
ReleaseDC (hWnd, hDC);
SendDlgItemMessage (hWnd, IDL_FILES, LB_SETHORIZONTALEXTENT,
_MAX_PATH*tm.tmAveCharWidth, 0);
EnableWindow (GetDlgItem (hWnd, IDB_BURN), TRUE);
EnableWindow (GetDlgItem (hWnd, IDB_REMOVE), TRUE);
// Select the first entry in the list.
SendDlgItemMessage (hWnd, IDL_FILES, LB_SETCURSEL, 0, 0);
}
DragFinish ((HANDLE) wParam);
return FALSE;
case WM_INITDIALOG:
DragAcceptFiles (hWnd, TRUE);
// If no filenames were loaded, disable the user interface.
if (!LoadFilenames (hWnd))
{
EnableWindow (GetDlgItem (hWnd, IDB_BURN), FALSE);
EnableWindow (GetDlgItem (hWnd, IDB_REMOVE), FALSE);
}
else
{
HDC hDC;
TEXTMETRIC tm;
// Set the horizontal scrolling extent to _MAX_PATH characters
// of average character width.
hDC = GetDC (hWnd);
GetTextMetrics (hDC, &tm);
ReleaseDC (hWnd, hDC);
SendDlgItemMessage (hWnd, IDL_FILES, LB_SETHORIZONTALEXTENT,
_MAX_PATH*tm.tmAveCharWidth, 0);
}
return TRUE;
}
return FALSE;
}
void BackupFiles (HWND hWnd)
{
HRESULT hr;
int count, i;
__int64 limit = 645000000, tally = 0;
/*
If the staging area contains more content than can fit onto the writable
CD inserted into the burner device, ICDBurn's wizard will complain. In
an attempt to prevent this from happening, this function tries to keep
track of the amount of content currently in the staging area, and only
copy a file if its size plus the existing content size doesn't exceed
the limit imposed by the limit variable.
For this to work, you should only insert blank CDs into the drive --
don't insert a nonfinalized CD which already has some content on it.
Also, make sure that the staging area is empty of content before running
this program.
To accommodate 650Mb and 700Mb CDs, I've initialized limit's value to
645,000,000 bytes. You might want to adjust this value as necessary.
WARNING: An infinite loop and repeated wizard activation will occur if
any file has a size that exceeds limit's value! For example, you attempt
to copy a 1Gb file.
*/
count = SendDlgItemMessage (hWnd, IDL_FILES, LB_GETCOUNT, 0, 0);
for (i = 0; i < count; i++)
{
HANDLE handle;
LARGE_INTEGER fileSize;
TCHAR szErrMsg [_MAX_PATH*2], szPathFileToCopy [_MAX_PATH],
szPathStagingAreaFile [_MAX_PATH*2];
// Obtain the pathname of the next file to copy.
SendDlgItemMessage (hWnd, IDL_FILES, LB_GETTEXT, i,
(LPARAM) szPathFileToCopy);
// Obtain the file's size.
handle = CreateFile (szPathFileToCopy, GENERIC_READ, FILE_SHARE_READ,
NULL, OPEN_EXISTING, 0, NULL);
if (handle == INVALID_HANDLE_VALUE)
{
_tcscpy (szErrMsg, _T ("Unable to obtain size for "));
_tcscat (szErrMsg, szPathFileToCopy);
_tcscat (szErrMsg, _T (" ... ignoring file!"));
MessageBox (hWnd, szErrMsg, APP_NAME, MB_OK);
continue;
}
GetFileSizeEx (handle, &fileSize);
CloseHandle (handle);
// Will the file fit onto the CD?
if (fileSize.QuadPart+tally <= limit)
{
TCHAR drive [_MAX_DRIVE], dir [_MAX_DIR], fname [_MAX_FNAME],
ext [_MAX_EXT], szStatus [_MAX_FNAME+8];
_tsplitpath (szPathFileToCopy, drive, dir, fname, ext);
_tcscpy (szPathStagingAreaFile, g_szPathStagingArea);
_tcscat (szPathStagingAreaFile, _T ("\\"));
_tcscat (szPathStagingAreaFile, fname);
// Update the status label to inform user of file being copied.
_tcscpy (szStatus, _T ("Copying "));
_tcscat (szStatus, fname);
SetWindowText (GetDlgItem (hWnd, IDT_STATUS), szStatus);
// Attempt to copy the file, overwriting a same-named file if
// present.
if (!CopyFile (szPathFileToCopy, szPathStagingAreaFile, FALSE))
{
_tcscpy (szErrMsg, _T ("Unable to copy "));
_tcscat (szErrMsg, szPathFileToCopy);
_tcscat (szErrMsg, _T (" to staging area ... ignoring file!"));
MessageBox (hWnd, szErrMsg, APP_NAME, MB_OK);
continue;
}
tally += fileSize.QuadPart;
continue;
}
tally = 0;
SetWindowText (GetDlgItem (hWnd, IDT_STATUS), _T ("Burning"));
hr = g_pICDBurn->lpVtbl->Burn (g_pICDBurn, NULL);
if (hr == ERROR_INVALID_FUNCTION)
MessageBox (hWnd, _T ("Wizard terminated by user!"), APP_NAME,
MB_OK);
if (FAILED (hr))
MessageBox (hWnd, _T ("Burning failure!"), APP_NAME, MB_OK);
if (hr == ERROR_INVALID_FUNCTION || FAILED (hr))
{
DeleteStagingAreaContent ();
break;
}
// The most recent file could not be copied to the staging area
// because this area's limit was reached. As a result, the tally was
// reset and the staging area's content was burned (and then deleted,
// automatically if burn succeeded, or via DeleteStagingAreaContent()
// otherwise). We must re-attempt to copy the file, by decrementing
// the index so that the next loop iteration will still point to the
// file.
i--;
}
// The tally variable will contain a nonzero value if the loop ends while
// fileSize.QuadPart+tally <= limit is still true -- there are files in the
// staging area waiting to be burned.
if (tally != 0)
{
SetWindowText (GetDlgItem (hWnd, IDT_STATUS), _T ("Burning"));
hr = g_pICDBurn->lpVtbl->Burn (g_pICDBurn, NULL);
if (hr == ERROR_INVALID_FUNCTION)
MessageBox (hWnd, _T ("Wizard terminated by user!"), APP_NAME,
MB_OK);
if (FAILED (hr))
MessageBox (hWnd, _T ("Burning failure!"), APP_NAME, MB_OK);
if (hr == ERROR_INVALID_FUNCTION || FAILED (hr))
DeleteStagingAreaContent ();
}
if (SendDlgItemMessage (hWnd, IDL_FILES, LB_GETCOUNT, 0, 0))
{
EnableWindow (GetDlgItem (hWnd, IDB_BURN), TRUE);
EnableWindow (GetDlgItem (hWnd, IDB_REMOVE), TRUE);
}
SetWindowText (GetDlgItem (hWnd, IDT_STATUS), _T (" "));
}
void BackupThread (void *hWnd)
{
BackupFiles ((HWND) hWnd);
_endthread ();
}
void DeleteStagingAreaContent (void)
{
HANDLE handle;
WIN32_FIND_DATA w32fd;
TCHAR szPathFilesToDelete [_MAX_PATH*2];
_tcscpy (szPathFilesToDelete, g_szPathStagingArea);
_tcscat (szPathFilesToDelete, _T ("\\*.*"));
handle = FindFirstFile (szPathFilesToDelete, &w32fd);
if (handle == INVALID_HANDLE_VALUE)
return;
do
{
_tcscpy (szPathFilesToDelete, g_szPathStagingArea);
_tcscat (szPathFilesToDelete, _T ("\\"));
_tcscat (szPathFilesToDelete, w32fd.cFileName);
DeleteFile (szPathFilesToDelete);
}
while (FindNextFile (handle, &w32fd));
FindClose (handle);
}
BOOL LoadFilenames (HWND hWnd)
{
int count, i;
TCHAR key [14], value [_MAX_PATH];
GetPrivateProfileString (_T ("filenames"), _T ("count"), _T (""), value, 6,
INI_FILE);
count = _ttoi (value);
for (i = 0; i < count; i++)
{
_stprintf (key, _T ("filename%d"), i);
GetPrivateProfileString (_T ("filenames"), key, _T (""), value,
_MAX_PATH, INI_FILE);
SendDlgItemMessage (hWnd, IDL_FILES, LB_ADDSTRING, 0, (LPARAM) value);
}
if (count)
SendDlgItemMessage (hWnd, IDL_FILES, LB_SETCURSEL, 0, 0);
return (count) ? TRUE : FALSE; // Return FALSE if no filenames loaded.
}
void SaveFilenames (HWND hWnd)
{
int count, i;
TCHAR key [14], value [_MAX_PATH];
count = SendDlgItemMessage (hWnd, IDL_FILES, LB_GETCOUNT, 0, 0);
_stprintf (value, _T ("%d"), count);
WritePrivateProfileString (_T ("filenames"), _T ("count"), value, INI_FILE);
for (i = 0; i < count; i++)
{
_stprintf (key, _T ("filename%d"), i);
SendDlgItemMessage (hWnd, IDL_FILES, LB_GETTEXT, i, (LPARAM) value);
WritePrivateProfileString (_T ("filenames"), key, value, INI_FILE);
}
}
Although Listing 4 is lengthy, this source code should be fairly easy to follow. The WinMain() function takes
care of acquiring the staging area's location, working with COM to access ICDBurn's functions, and verifying the presence of
a CD burning device. It also initiates the dialog-based GUI via a call to the DialogBox() function, passing the
address of a CDBDlgProc callback procedure.
Note
Some developers get upset when they see goto statements (for transferring execution to labeled code located elsewhere) in
source code. After all, this statement has been badly abused in the past, leading to spaghetti code (code whose
logical flow is so tangled that it's difficult to follow). Regardless, I chose to insert a few goto statements in
WinMain(), to transfer execution to cleanup code when something goes wrong, which makes it possible to avoid
duplicating cleanup code. I did this because, unlike C++ and C#, C doesn't incorporate an exception-handling mechanism.
The first message sent to CDBDlgProc is WM_INITDIALOG. This callback procedure responds by telling
Windows that filenames can be dragged and dropped anywhere on the dialog's GUI, by trying to load filenames previously saved
to a cdbackup.ini file stored in the Windows directory, and by either disabling the buttons (no filenames) or
adjusting the length of the list's horizontal scrollbar.
The WM_DROPFILES message is sent to CDBDlgProc in response to the user dragging one or more
filenames to and dropping them on the dialog. Assuming that a filename doesn't identify a directory (which is ignored), this
name is added to the dialog's list control. If at least one name is dropped, the list's horizontal scrollbar is sized, the
two buttons are enabled, and the first list item is selected.
In response to an attempt to end the program (by clicking the X button on the titlebar), or by clicking one of the dialog's
two buttons, Windows sends a WM_COMMAND message to the callback procedure. The code for the end attempt,
indicated by IDCANCEL, prevents the program from ending if a background thread is running, saves the list of
filenames to cdbackup.ini, and performs other cleanup.
When Burn is clicked, WM_COMMAND's IDB_BURN code disables the GUI's buttons and starts a background
thread to handle the backup task. A background thread is used so that any GUI updates (such as a repaint) occur promptly,
rather than being delayed. When Remove is clicked, the IDB_REMOVE code deletes the selected entry, appropriately
updating the GUI.
The background thread invokes a function named BackupFiles() to perform the backup task. This function
repeatedly copies files to the staging area, and activates the wizard to initiate burning once no more files can be copied
(due to a limit being reached). The wizard is also activated after this loop if there is any content remaining in the staging
area.
As indicated in the comments, there's a potential problem with BackupFiles()'s looping behavior. Suppose you
attempt to copy a file whose size is 1Gb to the staging area. Because the limit is less than 1Gb, the file will never be
copied, the wizard will constantly be activated, and the loop will continue to try and copy the file to the staging area.
Caution
Never use writable CDs that have content previously burned onto them and aren't finalized with CDBackup. Because
they hold less content than assumed by this program, the wizard will report an error when it discovers more information in
the staging area than can fit on the CD. This will screw up the looping behavior in BackupFiles(), and probably
result in wasted CDs.
Listing 5's cdbackup.rc resource file describes the resources used by CDBackup.
I used Borland C++ 5.5.1's brc32.exe resource compiler to compile cdbackup.rc to
cdbackup.res, followed by bcc32.exe to compile cdbackup.c and (via this source file's
#pragma resource "cdbackup.res" directive) bundle cdbackup.res's content into the resulting
cdbackup.exe executable. Assuming that c:\borland\bcc55\projects\cdbackup is current, the commands
below perform these tasks:
The -WM command-line option causes bcc32.exe to generate a 32-bit multithreaded target program.
This option is necessary because cdbackup.c creates a background thread (via a call to
_beginthread()) for copying files to the staging area and activating ICDBurn's wizard for burning CDs.
Because I simplified CDBackup, you have several opportunities to improve this program. For example, you could
make CDBackup more robust by having it delete any initial content in the staging area (which could screw up the
tally) prior to copying the first set of files, and also by having this program remove the potential for an infinite loop
(discussed earlier).
Providing the capability to copy directories to CDs is a more significant enhancement. However, this task isn't as easy to
code. For example, copying a single directory to the staging area may involve a deeply nested hierarchy of subdirectories.
Not only do you need to figure out the total size of and copy the hierarchy to the staging area, you need to deal with the
situation where the entire hierarchy doesn't fit onto one CD.
There's another improvement to think about: Rather than assume an arbitrary limit on the amount of content that can be copied
to the staging area, it would be useful to determine the amount of free space on the CD currently inserted into the burning
device. This information can be obtained by working with low-level IMAPI COM interfaces and ICDBurn's
GetRecorderDriveLetter() function.
Regarding GetRecorderDriveLetter(), CDBackup doesn't invoke this function to obtain the drive
letter of the default burning device, and then subsequently display the letter. Because this omission might be confusing if
you've more than one CD burner, consider modifying CDBackup to obtain the drive letter, and then present a
suitable message (that includes this letter) somewhere on the GUI.
Tip
Check out Microsoft's HOW TO: Disable Notification Area Balloon Tips in
Windows XP document if you're tired of seeing the "You have files waiting to be written to the CD" notification area
balloon tip when cdbackup copies files to the staging area.
Conclusion
Microsoft has developed an infrastructure for improving ICDBurn's wizard via CD writing extensions. Although it's beyond the
scope of this article to delve into this infrastructure, I've found an interesting item that you should check out if you're
interested in improving the wizard: Blind Computing's
Shell Programmer's
Guide.