Introduction
This article presents a class that is a hack around a shortcoming in the free CutePDF Writer. The shortcoming is that the "Save As" dialog always pops up so it is impossible to print to a PDF file without user interaction. This class makes it possible to use the free CutePDF Writer without user interaction.
Background
I have a small app that is scheduled to run every morning at 5:00 AM. It's job is to print out a summary report of the previous days activities. I used to have it print out on paper, but I found that to be a waste of paper as I would usually only read it once and then toss it. So I decided to it would be better to simply print to a PDF file that I could make a hard copy of if I needed one. I already had the CutePDF Writer installed on my system, so it was only natural that I use it.
My first attempt, which worked fairly well, was to simply find the "Save As" dialog using the FindWindow
API, and simulating a click on the Save button by posting a BN_CLICKED
command to the dialog. But this approach had several problems including the unreliability of FindWindow
, and what would happen if another application also happened to have a Save As dialog open at the same time.
The main problem I had though was specifing the folder that the CutePDF Writer was going to save the PDF file to. All was well as long as no other app also used the CutePDF Writer to print a PDF file as it always defaulted the folder in the Save As dialog to the last folder used by any app. I then discovered that the last folder used was saved in the Windows registry, and I realized that by changing that registry value to point to the folder where I wanted to save my PDF file the Save As dialog would use the folder I wanted, not the last one used by some other random application.
I also tried to fix the problem I had with actually finding the proper "Save As" dialog. The dialog is not a child of the calling application, but instead is a child of the CPWSave.exe application. So I used the EnumWindows
API to list all the top level Windows on the system. In the callback function, I first check if the top level window is a dialog by checking if the class name is "#32770
" which is the class name for dialogs. If it is, then I check if the dialog is owned by the CPWSave.exe program. I could probably add a few more checks, but at this point I can be fairly certain that the window found is the proper Save As dialog.
BOOL CALLBACK CCutePDFWriter::GetSaveAsDialogProc(HWND hWnd, LPARAM lp)
{
BOOL result = TRUE;
TCHAR Buffer[MAX_PATH + 1] = {0};
GetClassName(hWnd, Buffer, _countof(Buffer));
if (_tcsicmp(Buffer, DIALOG_CLASS_NAME) == 0)
{
DWORD ProcessID = 0;
GetWindowThreadProcessId(hWnd, &ProcessID);
HANDLE hProcess = OpenProcess (PROCESS_QUERY_INFORMATION
| PROCESS_VM_READ
, FALSE
, ProcessID);
if (NULL != hProcess)
{
GetModuleBaseName(hProcess, NULL, Buffer, _countof(Buffer));
if (_tcsicmp(Buffer, EXE_FILE_NAME) == 0)
{
datastruct *dsp = (datastruct*)lp;
dsp->hWnd = hWnd;
dsp->ProcessID = ProcessID;
result = FALSE;
}
CloseHandle(hProcess);
}
}
return result;
}
I also wanted to address what would happen if another application was already using the CutePDF Writer and was waiting for a user to click on the Save button of an active dialog. My solution was to look for an active dialog in the class constructor, and if one was found simply wait for it to close. This is not the ideal solution as it will make the application that is using this class appear to hang.
CCutePDFWriter::CCutePDFWriter(void)
: SavedDC(0)
, CPWProcessID(0)
{
HWND hWnd = GetSaveAsDialog();
if (NULL != hWnd)
{
HANDLE ProcessHandle = OpenProcess(SYNCHRONIZE, FALSE, CPWProcessID);
WaitForSingleObject(ProcessHandle, INFINITE);
CloseHandle(ProcessHandle);
Using the Code
To use the code, simply declare a CCutePDFWriter
class object, call its GetDC(LPCTSTR Folder)
method to get the PDF printer device context. You specify the complete path to the folder that the PDF file will be saved in the GetDC
call. If the folder does not exist, it will be created via a call to SHCreateDirectoryEx
. If GetDC
returns NULL
, you can find out why by calling GetLastError()
. The actual name of the file is specified when you call the StartDoc()
function. When you are finished printing the PDF file, you call the ReleaseDC()
method or let the CCutePDFWriter
object go out of scope.
CCutePDFWriter PDF_Printer;
CDC *pDC = PDF_Printer.GetDC(_T("C:\\My_PDF_Folder\\"));
pDC->StartDoc(_T("My_PDF_File"));
pDC->StartPage();
pDC->Ellipse(100, 200, 300, 400);
pDC->EndPage();
pDC->EndDoc();
PDF_Printer.ReleaseDC();
After running the demo code, you will have a PDF file containing a circle drawn on the top right corner of the page. The file will be C:\My_PDF_Folder\My_PDF_File.pdf.
Shortcomings
This class does have a couple of shortcomings. Among them are the fact that the Save As dialog does still pop up for a brief while, but usually too short of a time for it to bother anyone other than the fact that it grabs the input focus for a bit. The other is my use of the Sleep
function to try and avoid race conditions as I try to let the CutePDF writer change registry settings before this code does, rather than after.
History
- December 7, 2009 - Posted to CodeProject