Dennis Crain
Microsoft Developer Network Technology Group
Happy Birthday Annie!
May 1995
Click to open or copy the files in the METAVIEW sample application for this technical article.
This article describes the experiences of a technical writer in the Microsoft Developer Network as he finally bites the bullet and writes his first application based on the document/view framework provided by the Microsoft® Foundation Class Library (MFC). Not really understanding why he would ever want to use MFC, his heart slowly softens to the prospect of never writing an application in C—ever again. The document used in the sample application is the Win32® enhanced metafile.
I have always enjoyed programming. That is, programming in C. Who can argue that C can't give you everything you need to get the job done. It seemed to me that this C++ and MFC (Microsoft® Foundation Class Library) hype was nothing more than marketing weasels trying to justify their salaries. After all, I could write procedures for Windows® in my sleep. I loved the graceful, flowing indents of massive switch statements that could handle everything, plus more, that a Windows-based application could ever think of. I was bound and determined not to get sucked into the vortex. The black hole of abstraction wasn't going to get me. No way. No siree.
But I began to feel as though I was alone. I tried to start an interest group for C programmers, and I was the only one who showed up! My colleagues were all wearing C++ suits with MFC ties to work. I began hearing voices as I was programming: "MFC would make that easier." But I was not interested in "easy." I was interested in "control." The C++/MFC combination only took me further away from an already abstracted Windows world a la C. I became depressed. I began to question my worth. (Is this getting deep enough?)
It was then that my friend Nigel intervened. He challenged me to write an MFC application that used the document/view architecture. Reluctantly, I said okay. Mind you, I quickly qualified this response with, "But why do I want to do this boring thing?"
What follows is an account of this process. More accurately, it is a discussion of those areas where I had to do some research to get the functionality that I needed in the application. Consider it a testimony to the power of MFC to free programmers from the mundane and repetitive garbage that we have had to deal with since Windows version 1.0. Am I coming around late? Probably. But I am confident that some of you are late-comers as well.
As I noted above, Nigel challenged me to write an application using the document/view architecture of MFC. The purpose of this challenge was to get me fully involved and immersed into MFC. As a base specification, the Win32-based application was to use enhanced metafiles as the document and display multiple views of the document as either the rendered picture or the metafile header displayed as text. Furthermore, the application would handle printing along with print preview. So with this in mind, I took the challenge. As you will see, there were a few detours that evolved along the way. Although they were not essential to the application, they helped the functionality. Consider these as useful tips.
No rocket science here. I simply used the Microsoft Visual C++™ version 2.0 AppWizard. I selected the multiple-document interface (MDI), toolbar, status bar, and print preview options. The resultant project created the document and view classes. But wait, did I say view versus views? Why didn't AppWizard create the second view? As you recall, the application must provide two different views of the same document. However, AppWizard creates only a single view class. "Bummer," I thought. I would need to add the second view myself. Oh well, the print preview was free. More on that second view later.
So just where do you open a document in an architecture such as this? I incorrectly assumed that it would be in response to the user selecting the Open command from the File menu. Silly me. That was too obvious. I went down this path for a while and soon realized that I was doing all the work. I thought that MFC was supposed to give me a hand! "No matter," I thought. "I will just take a trip down to Nigel's office and ask him." Sure enough, I needed to open the document in the Serialize function provided by MFC in the document class (METAVDOC.CPP). The following code illustrates my first attempt at this.
void CMetavw1Doc::Serialize(CArchive& ar)
{
if (ar.IsStoring())
{
}
else
{
UINT uiSig;
ar.Read(&uiSize, sizeof(UINT));
if (uiSiz == EMR_HEADER) {
m_hemf = GetEnhMetaFile(m_szPathName);
}
}
}
The code of interest is in the else block (shown in bold, above). This code simply reads the first sizeof(UINT) bytes to see if they are the signature for an enhanced metafile. If they are, the metafile bits are retrieved by way of GetEnhMetaFile. Because I do not save any documents, there is no code in response to IsStoring.
Now, wouldn't it be nice if I were to simply call something like Load(m_szPathName) to do this? Aha! The first little detour! I decided to write a class to handle the enhanced metafile "things" such as loading and playback (more on this soon). Use of this class reduced the above code to the following:
void CMetavw1Doc::Serialize(CArchive& ar)
{
if (ar.IsStoring())
{
}
else
{
cemf.Load(m_szPathName);
}
}
Notice that in both code fragments I used the m_szPathName member variable as an argument while calling the GetEnhMetaFile and Load functions. Initially, I thought that I could get the fully qualified filename from the CArchive parameter in the Serialize function. After all, CArchive contains the m_pDocument member variable that points to the CDocument object being serialized. Great! CDocument has a convenient member variable that sounded just like what I wanted, m_strPathName. Unfortunately, m_pDocument->strPathName is initialized to NULL when loading a file. So, I decided to grab the filename and path by overriding the OnOpenDocument function. The path is passed to OnOpenDocument, so I simply made a copy of it within the CMetavw1Doc class as shown below. Note that this copy of the path is stored in the CString m_szPathName member variable, which, by no coincidence, is the same variable passed to GetEnhMetaFile and Load in the code above.
BOOL CMetavw1Doc::OnOpenDocument(LPCTSTR lpszPathName)
{
m_szPathName = lpszPathName;
if (!CDocument::OnOpenDocument(lpszPathName))
return FALSE;
return TRUE;
}
So, what did I get for free? That's an easy one to answer. Everything in the list below (plus more that I am sure I don't have an appreciation for yet) is provided by the MFC framework:
After filling my view class (METAVVW.CPP) with code specific to successfully render an enhanced metafile, it became clear that I was reverting to some of my unorganized tendencies when coding in C. So I decided to get this code out of the view class by writing a class to handle the loading and playback of the metafiles.
For the purpose of this small application, Load and Draw were the most important member functions of the class. In the spirit of C++, I provided some additional functions to access various attributes of the metafile, such as the handle, the description string, and a pointer to the metafile header. The following code (taken from CEMF.H) gives you a good idea where I am going with this class. Note that I derived the class from CObject versus CDC or CMetaFileDC. CDC includes the PlayMetaFile and AddMetaFileComment metafile member functions. In retrospect, it might have been more convenient to derive the class from CDC. Deriving the class from CMetaFileDC just didn't seem right, because I was not creating a metafile but rather simply viewing existing metafiles. However, a sensible, fully functional metafile class could be derived from CMetaFileDC. Yes, there are many ways to skin a cat (sorry to all of you cat lovers!).
class CEMF : public CObject
{
// Operations
public:
CEMF();
~CEMF();
BOOL Load(const char *szFileName);
BOOL Draw(CDC* pDC, RECT* pRect);
LPENHMETAHEADER GetEMFHeader(){return ((m_pEMFHdr) ? m_pEMFHdr : NULL);};
LPTSTR GetEMFDescString(){return ((m_pDescStr) ? m_pDescStr : NULL);};
HENHMETAFILE GetEMFHandle(){return ((m_hemf) ? m_hemf : NULL);};
protected:
BOOL GetEMFCoolStuff();
BOOL LoadPalette();
// Data members
protected:
CString m_szPathName;
HENHMETAFILE m_hemf;
LPENHMETAHEADER m_pEMFHdr;
LPTSTR m_pDescStr;
LPPALETTEENTRY m_pPal;
UINT m_palNumEntries;
LPLOGPALETTE m_pLogPal;
LOGPALETTE m_LogPal;
HPALETTE m_hPal;
};
The Load function looks suspiciously like my first go at opening the file in the first Serialize code fragment above. However, this time there is no CArchive object to take advantage of. No problem. Using a CFile object permits reading of the file signature. The GetEMFCoolStuff and LoadPalette functions are taken directly from a previous sample of mine (see the EMFDCODE sample, available in the MSDN Library). They obtain copies of the metafile header, description string, and palette embedded within the metafile. Of course they are now in the CEMF class.
BOOL CEMF::Load(const char *szFileName)
{
UINT uiSig;
// Save the filename.
m_szPathName = szFileName;
// Check the file signature to see if this is an enhanced metafile.
CFile cfEMF;
cfEMF.Open(m_szPathName, CFile::modeRead | CFile::shareDenyWrite);
cfEMF.Read(&uiSig, sizeof(UINT));
cfEMF.Close();
// If this is an EMF, then obtain a handle to it.
if (uiSig == EMR_HEADER)
{
m_hemf = GetEnhMetaFile(m_szPathName);
GetEMFCoolStuff();
LoadPalette();
}
else
m_hemf = NULL;
// Return success.
return ((m_hemf) ? TRUE : FALSE);
}
The Draw function is called by the OnDraw function in the view class (METAVVW.CPP). There is not much going on here. If there is a palette, indicated by a non-NULL value in the m_hPal member variable, the palette is selected and realized into the device context (DC). I was a little bummed when I realized that CDC::SelectPalette required a pointer to a CPalette object. But I was equally thrilled to find the CPalette::FromHandle function. I was able to easily convert a handle to a palette to a CPalette object. From here it was just a simple matter of playing the metafile a la CDC::PlayMetaFile.
BOOL CEMF::Draw(CDC *pdc, RECT *pRect)
{
ASSERT(m_hemf);
BOOL fRet = FALSE;
CRect crect;
CPalette cpalOld = NULL;
if (m_hemf)
{
if (m_hPal)
{
CPalette cpal;
if ((cpalOld = pdc->SelectPalette(cpal.FromHandle(m_hPal), FALSE)))
pdc->RealizePalette();
}
fRet = pdc->PlayMetaFile(m_hemf, pRect);
if (cpalOld)
pdc->SelectPalette(cpalOld, FALSE);
}
return (fRet);
}
So what else is in the CEMF class? As I mentioned above, there are two private member functions that handle metafile palette and header issues. I won't discuss those in this article. Of course, you will probably want to take a peek at CEMF.CPP and CEMF.H in the sample application included with this article! If you intend to do something serious with this class, I suggest that you add additional functionality to the class. This would include the ability to enumerate the metafile and to handle metafiles contained within clipboard files. Again, these issues are addressed in the above-mentioned articles.
If I heard Nigel correctly, the challenge was to show the document in three different ways. The document was to be viewed as a rendered picture in a child window, as text describing the metafile header in a child window, and as a rendered picture in print preview. In addition, the two views in child windows were to be built around the MDI architecture provided by the MFC framework. Let's take a look at each of these individually.
No problem here. Just a matter of calling the Draw function in the CEMF class. Take a closer look at the OnDraw function in METAVVW.CPP.
void CMetavw1View::OnDraw(CDC* pDC)
{
CMetavw1Doc* pDoc = GetDocument();
// Flag to prevent drawing in response to full-drag sizing.
// See OnSize() and FullDragOn() in this module.
if (m_fDraw)
{
// If either printing or print previewing, the rect is
// provided by CPrintInfo in OnPreparePrinting.
if (pDC->IsPrinting())
{
pDoc->m_cemf.Draw(pDC, &m_rectDraw);
}
else
{
GetClientRect(&m_rectDraw);
pDoc->m_cemf.Draw(pDC, &m_rectDraw);
}
}
}
This code is organized around two conditional statements that are worth noting. The first condition is an "all-or-nothing" test. If m_fDraw is false, no attempt is made to draw. So what is m_fDraw all about? Well, this is detour #2, and I will address it shortly. The second condition tests if the drawing is taking place on the printer (or print preview) or within the view (child) window. The IsPrinting member function of the CDC class is an inline function that returns the CDC::m_bPrinting public member variable. Prior to using this function, I was testing m_bPrinting directly. Upon finding the IsPrinting function, I became somewhat puzzled. After all, m_bPrinting was simply returned by IsPrinting. The IsPrinting function seems more in the spirit of C++. If they were to change the spelling of m_bPrinting, my code would not work. But this still disturbs me a bit. After all, I was creative enough to debug the application, watch a few variables, and then figure out a way to getting what I wanted. This leads me to my first (and probably last) hypothesis: encapsulation and data hiding can exact revenge against enthusiasm.
So much for hypothesizing. Back to the code. As we are discussing the rendering of the document within the view child window, the client area is obtained by way of GetClientRect and placed in m_rectDraw.
if (pDC->IsPrinting())
{
pDoc->m_cemf.Draw(pDC, &m_rectDraw);
}
else
{
GetClientRect(&m_rectDraw);
pDoc->m_cemf.Draw(pDC, &m_rectDraw);
}
The Draw function is then called and poof! The picture appears.
Yes I cheated. I am addressing two methods here, print preview and rendering on a printer. But I am only doing this because the MFC framework makes very little distinction between the two.
If IsPrinting returns True, the Draw function is called with what initially appears to be an uninitialized m_rectDraw.
if (pDC->IsPrinting())
{
pDoc->m_cemf.Draw(pDC, &m_rectDraw);
}
else
{
GetClientRect(&m_rectDraw);
pDoc->m_cemf.Draw(pDC, &m_rectDraw);
}
Fortunately, this is not the case. The framework comes to the rescue again. When printing or print previewing, several functions are called prior to OnDraw. These functions are overrideable. In this case I overrode the OnPrint function (which ultimately calls OnDraw). A pointer to a CPrintInfo object is passed to the OnPrint function. One data member of this class is a CRect object defining the usable page area. This rectangle is merely copied into m_rectDraw prior to calling OnDraw.
void CMetavw1View::OnPrint(CDC* pDC, CPrintInfo* pInfo)
{
m_rectDraw = pInfo->m_rectDraw;
OnDraw(pDC);
}
The rest is history. Call Draw and you are done!
One additional note on print preview. I spent a bit of time trying to get the dimensions of the print preview "page." I kept scratching my head trying to figure out how I was going to grab the origin and extents of the preview page centered in the preview window. I even went so far as to make a copy of the CPreviewDC object (private to AFX) just to grab the origins. But I was still faced with obtaining the extents. Thank goodness I remembered my hypothesis-now-turned-axiom: encapsulation and data hiding can exact revenge against enthusiasm. So after grunging around a bit in MFC, I realized that the print preview code was going to do all of the scaling automatically. After watching CPrintInfo::m_rectDraw while zooming, I realized that it was always the same. Just what I wanted! Chalk another one up to MFC.
I don't know about you, but my painting code is generally fairly extensive. I am really impressed with handling print preview, normal drawing, and printing in 19 lines of code (including comments). True, I did write the CEMF class. But hey, I get to use it whenever I want in future projects! True, MFC handled all of the print preview and printing. But guess what? Microsoft gets to maintain that code for me!
So what was that m_fDraw flag all about in the OnDraw function? Recall that it was an "all-or-nothing" test. If m_fDraw is False, no attempt is made to draw. I darted down this detour when I began playing large metafiles in the client area of a view window in response to resizing the window. In the Windows NT™ operating system, there is an option (set by the user in the Desktop applet in Control Panel) that permits drawing as a window is resized. This option is called full-drag. In Windows 3.1 and Windows 95 this option does not exist (yeah!). I am here to tell you that if you are rendering a large metafile that does lots of complex things, you will absolutely detest this full-drag option. So how do you turn it off? You don't! Remember, the user set this mode. You don't really want to reset it. You might say, "This never stopped me in the past!" Well, there really isn't a convenient way of resetting it. I chose to use a one-shot timer to deal with the problem.
So what is a one-shot timer? Simply put, it is a timer that is used once and then destroyed. The basic implementation, in the case of resizing the window, is to start a timer when WM_SIZE messages are encountered. If a timer already exists (as in the case of consecutive WM_SIZE messages), kill it and restart another one. When a WM_TIMER message finally sneaks through the message queue, kill the timer. Remember, you will not get a WM_TIMER message until you stop resizing the window. WM_TIMER messages have very low priority. The following two functions, OnSize and OnTimer, illustrate how I dealt with the timer in this application. In addition to setting and killing timers, these functions also set the value of the m_fDraw data member.
void CMetavw1View::OnSize(UINT nType, int cx, int cy)
{
CView::OnSize(nType, cx, cy);
// Only do this if full-drag is enabled.
if (m_fFullDragOn)
{
if (!m_uiTimer)
KillTimer(1);
m_uiTimer = SetTimer(1, 100, NULL);
m_fDraw = FALSE;
}
}
When the WM_TIMER message is finally dealt with, the OnTimer function sets m_fDraw to zero, kills the timer, and repaints the client area of the view window.
void CMetavw1View::OnTimer(UINT nIDEvent)
{
m_fDraw = TRUE;
m_uiTimer = 0;
KillTimer(1);
InvalidateRect(NULL);
}
The m_fFullDragOn data member referred to in the OnSize function is set by a call to the FullDragOn function in the constructor for the view. The function simply queries the registration database to determine the value of DragFullWindows in the Control Panel\Desktop subkey of HKEY_CURRENT_USER. If the value of DragFullWindows is 1, the function returns True; otherwise it returns False.
BOOL CMetavw1View::FullDragOn()
{
HKEY hkey = NULL;
DWORD dwType;
long lResult;
LPSTR lpszDataBuf;
DWORD cbData = 0;
lResult = RegOpenKeyEx(HKEY_CURRENT_USER, "Control Panel\\Desktop", 0,
KEY_READ, &hkey);
if (hkey)
{
// Obtain size of key.
lResult = RegQueryValueEx(hkey, "DragFullWindows", NULL,
&dwType, NULL, &cbData);
// Alloc sufficient memory for key value.
lpszDataBuf = (LPSTR)malloc(cbData * sizeof(char));
// Get the key value.
lResult = RegQueryValueEx(hkey, "DragFullWindows", NULL, &dwType,
(LPBYTE)lpszDataBuf, &cbData);
return (*lpszDataBuf == '1');
}
return FALSE;
}
The net result of all this is that when the window is resized, nothing is redrawn until the resizing stops.
You may recall that Nigel's challenge specified that the application would display multiple views of the document as either the rendered picture or the metafile header displayed as text. No problem; there must be a wizard for this. To my shock, there was none! So I took a look at Dale Rogerson's article, "Multiple Views for a Single Document." (MSDN Library Archive, Technical Articles) This was most helpful. However, until you do this two or three times, you can just plain get lost! Believe me, I was lost for a few hours as I bounced back and forth between adding the second view and writing the CEMF class. I would suggest that you focus on nothing but the second view until it is up and going. Nigel added a second view to one of his sample applications ("VIEWDIB: Views Multiple DIBs Simultaneously" Editor's note: We're sorry to say that this sample is no longer available on the MSDN Library) based on Dale's article. He derived the following list based on his experience. Between Dale's article and Nigel's list, I was able to painlessly add a second view. If I can do it, you can as well!
\nType\n\n\n\nFileType\nFile Type
extern C???App NEAR theApp; (replace ??? with your application name)
m_pBasicViewTemplate = new CMultiDocTemplate(...);
AddDocTemplate(m_pBasicViewTemplate);
CreateOrActivateFrame(theApp.m_p????ViewTemplate, RUNTIME_CLASS(C???View));
You will need to add the view header files to MAINFRM.CPP.
Okay, so this is another one of those unadulterated testimonials to MFC. What can I say. But I really was a skeptic before I began. If you haven't made the plunge, I suggest that you give it a try. I remember an acquaintance who once said, "I will never touch a Microsoft product, as they represent everything evil about the software industry." He now works at Microsoft! The point is, never say never. This is the attitude I had taken with C++ and MFC. And say, did you hear that the next version of Visual C++ is going to be called Visual Cobol++? I just might have to retire then!
Rogerson, Dale. "Multiple Views for a Single Document." August 1993. (MSDN Library Archive, Technical Articles)
Thompson, Nigel. "VIEWDIB: Views Multiple DIBs Simultaneously." (MSDN Library, Sample Code for Articles and Books)