Table of content
Introduction
This article is a follow on to the article
An MFC extension library to enable DLL plug-in technology for your application using MESSAGE_MAPs. If you
are not familiar with the library itself I would recommend that you look at the main article first.
This plug-in demonstrates a replacement of the standard MFC print preview window with an enhanced version that supports:
Multipage display : 1, 2, 3, 4, 6 or 9 pages at a timeSwitching the page layout between Portrait and LandscapePrint setup available in preview modeThe mouse wheel will work for scrolling pagesOnce again I will review how I went about designing the plug-in and implementing its features. Along the
way I will also cover problems encountered and how they were solved.
When I look at the plug-ins I can write, it seems to me that I will be able to take most of
my existing article series and convert each as I go. It has been a good starting point for upgrading
this library. If users of the class wish to see a specific plug-in created, they should visit my
blog page and post a suggesstion there.
Other articles in the series
Developing the plug-in
As a starting point, I created an MFC extension DLL as described in the main article. From there
I got hold of the source from my MultiPage PrintPreview enhancements for MFC Doc/View applications article
and started combining the two into a working project.
The first step in this procedure was to replace the existing CView::OnFilePrintPreview()
function
with our own. So, I created a plug-in to handle this:
I added a message map entry to process the existing ID_FILE_PRINT_PREVIEW
message.
BEGIN_MESSAGE_MAP(CEnhancedPrintPreview, CPlugInMap)
ON_COMMAND(ID_FILE_PRINT_PREVIEW, OnFilePrintPreview)
END_MESSAGE_MAP()
With the V1.2 upgrade to the library, I was able to return true
for all CView
derived classes
so that we can replace their existing print preview functionality.
bool CEnhancedPrintPreview::IsPlugInFor(CRuntimeClass *pClass)
{
if (pClass->IsDerivedFrom(RUNTIME_CLASS(CView)))
{
return true;
}
return false;
}
For the Pre call for the message we suppress the standard implementation of it using SuppressThisMessage()
,
as we do not want multiple preview windows being displayed.
void CEnhancedPrintPreview::OnFilePrintPreview()
{
if (IsPreCall())
{
SuppressThisMessage();
CPrintPreviewState* pState = new CPrintPreviewState;
CView *pView = static_cast<CView*>(m_pPlugInFor);
if (!pView->DoPrintPreview(IDD_PREVIEW, pView,
RUNTIME_CLASS(CMultiPagePreviewView), pState))
{
TRACE0("Error: DoPrintPreview failed.\n");
AfxMessageBox(AFX_IDP_COMMAND_FAILURE);
delete pState;
pState = NULL;
}
}
}
Well, once I had this and the standard code from the enhanced print preview code merged sufficiently to get this far,
I ran some tests to see how well it worked.
And it failed very badly. :( In fact I could not immediately see why it was failing, so I placed judicious TRACE
statements throughout the code. One of the symptoms of the failure was I was getting to see the standard MFC print preview
dialog bar:
Now during the print preview initialisation, this standard dialog bar is being destroyed and replaced with our own, which
enabled the mouse wheel messages, so why is it being shown? This was the first clue to my problem. It seems that even though
I was trying to suppress the ID_FILE_PRINT_PREVIEW
message in the standard MFC code, I was failing to, and two
preview views were being created, mine and MFCs. This was the main source of the problem. So how do I fix it?
Message suppression in the library
In the MFC extension library that handles the plug-ins, each plug-in had a bool m_bSuppressThisMessage
variable
which when we needed to suppress the message was being set to true
, so I checked the state of the flag in the
ProcessCommandMessageMaps()
function after my plug-in message had just been processed.
And it was false
!
After some thought it occurred to me that when we create the preview view and we go about displaying it, other messages may be being
processed by the library in the background, probably due to a message pump being done due to a SendMessage()
call, and
as we only have a single flag for all passes through the library, any additional messages that do get processed before we return from
our plug-in function will reset the suppressed message flag. This is not what we want to happen.
Now that we have identified the flaw in the library we need to fix it.
Fixing the problem
Well the problem we have here is in the library, and not our plug-in, so I went back to the library source and decided that we needed
a stack of suppression flags, one gets added for every message being processed, and is removed when that message gets completed. This led
to some changes in the CPlugInMap
class as follows:
std::vector<bool> m_bSuppressThisMessage;
void CPlugInMap::SetPreCall()
{
m_bPreCall = true;
m_bPostCall = false;
m_bSuppressThisMessage.push_back(false);
}
void CPlugInMap::SetPostCall()
{
m_bPreCall = false;
m_bPostCall = true;
m_bSuppressThisMessage.push_back(false);
}
bool CPlugInMap::IsSuppressed()
{
bool bSuppressed = m_bSuppressThisMessage[m_bSuppressThisMessage.size() - 1];
m_bSuppressThisMessage.pop_back();
return bSuppressed;
}
bool CPlugInMap::GetSuppressedState() const
{
bool bSuppressed = m_bSuppressThisMessage[m_bSuppressThisMessage.size() - 1];
return bSuppressed;
}
Well this also made me think about a problem that was originally experienced by early users of the plug-in library, that is
that when WM_NCDESTROY
messages were processed, some objects called delete this
on themselves, causing
Post message handler to fail with a GPF when trying to access the deleted map objects. I did not want to re-introduce the
same problem, so had to also change the ProcessCommandMessageMaps()
and ProcessWindowMessageMaps()
functions to check the CPIState
object related to the plug-in object.
BOOL CPlugInApp::ProcessWindowMessageMaps(
CPIState& state,
bool pre,
bool* pbSuppress,
CPlugInMap** pMaps,
int count,
UINT message,
WPARAM wParam,
LPARAM lParam,
LRESULT* pResult)
{
BOOL ret = FALSE;
BOOL local_ret = FALSE;
*pbSuppress = false;
for (int i = 0; i < count; ++i)
{
if (pre)
{
pMaps[i]->SetPreCall();
}
else
{
pMaps[i]->SetPostCall();
}
local_ret = CallWindowMessageMap(pMaps[i],
message, wParam, lParam, pResult);
if (local_ret)
{
ret = TRUE;
}
if (!state.IsDestroyed() && pMaps[i]->IsSuppressed())
{
*pbSuppress = true;
}
}
return ret;
}
Once all this had been done, I rebuilt the project and the problem had been fixed.
This new version of the plug-in library is now V1.3 - It seems every time I create a new plug-in, I
discover another flaw in it. Hopefully there will be less and less of these in the future.
Making the preview view plug-in enabled
I also wanted the preview view to make use of any other plug-ins that may enhance its
appearance. One such is the owner-drawn menu plug-in.
So I added versions of the OnCmdMsg
and OnWndMsg
functions along with the additional
support varaibles:
virtual BOOL OnCmdMsg(UINT nID, int nCode, void* pExtra,
AFX_CMDHANDLERINFO* pHandlerInfo);
virtual BOOL OnWndMsg(UINT message, WPARAM wParam,
LPARAM lParam, LRESULT* pResult);
private:
CPlugInMap** m_pMaps;
int m_MapCount;
bool m_bSuppressThisMessage;
CPIState m_state;
CMultiPagePreviewView::CMultiPagePreviewView()
{
m_pPageInfo = m_pageInfoArray2;
m_Across = 2;
m_Down = 1;
m_nPages = 2;
m_pMaps = NULL;
m_MapCount = 0;
CPlugInApp *pApp = static_cast<CPlugInApp*>(AfxGetApp());
m_pMaps = pApp->GetMessageMaps(this, m_MapCount);
}
CMultiPagePreviewView::~CMultiPagePreviewView()
{
for (int i = 0; i < m_MapCount; ++i)
{
delete m_pMaps[i];
m_pMaps[i] = NULL;
}
delete []m_pMaps;
m_pMaps = NULL;
m_state.SetDestroyed();
}
BOOL CMultiPagePreviewView::OnCmdMsg(
UINT nID,
int nCode,
void* pExtra,
AFX_CMDHANDLERINFO* pHandlerInfo)
{
CPlugInApp *pApp = static_cast<CPlugInApp*>(AfxGetApp());
CPIState stateCopy(m_state);
ASSERT(pApp);
BOOL ret_pre = FALSE;
BOOL ret_app = FALSE;
BOOL ret_post = FALSE;
m_bSuppressThisMessage = false;
ret_pre = pApp->ProcessCommandMessageMaps(
stateCopy,
true,
&m_bSuppressThisMessage,
m_pMaps,
m_MapCount,
nID,
nCode,
pExtra,
pHandlerInfo);
if (!m_bSuppressThisMessage)
{
ret_app = CPreviewView::OnCmdMsg(nID, nCode, pExtra, pHandlerInfo);
}
if (!stateCopy.IsDestroyed())
{
ret_post = pApp->ProcessCommandMessageMaps(
stateCopy,
false,
&m_bSuppressThisMessage,
m_pMaps,
m_MapCount,
nID,
nCode,
pExtra,
pHandlerInfo);
}
if (ret_pre || ret_app || ret_post)
return TRUE;
return FALSE;
}
BOOL CMultiPagePreviewView::OnWndMsg(
UINT message,
WPARAM wParam,
LPARAM lParam,
LRESULT* pResult)
{
LRESULT lResult;
if (message == WM_COMMAND)
{
if (OnCommand(wParam, lParam))
{
lResult = 1;
if (pResult != NULL)
*pResult = lResult;
return TRUE;
}
return FALSE;
}
if (message == WM_NOTIFY)
{
NMHDR* pNMHDR = (NMHDR*)lParam;
if (pNMHDR->hwndFrom != NULL && OnNotify(wParam, lParam, &lResult))
{
if (pResult != NULL)
*pResult = lResult;
return TRUE;
}
return FALSE;
}
CPlugInApp *pApp = static_cast<CPlugInApp*>(AfxGetApp());
ASSERT(pApp);
CPIState stateCopy(m_state);
BOOL ret_pre = FALSE;
BOOL ret_app = FALSE;
BOOL ret_post = FALSE;
m_bSuppressThisMessage = false;
ret_pre = pApp->ProcessWindowMessageMaps(
stateCopy,
true,
&m_bSuppressThisMessage,
m_pMaps,
m_MapCount,
message,
wParam,
lParam,
pResult);
if (!m_bSuppressThisMessage)
{
ret_app = CPreviewView::OnWndMsg(message, wParam, lParam, pResult);
}
if (!stateCopy.IsDestroyed())
{
ret_pre = pApp->ProcessWindowMessageMaps(
stateCopy,
false,
&m_bSuppressThisMessage,
m_pMaps,
m_MapCount,
message,
wParam,
lParam,
pResult);
}
if (ret_pre || ret_app || ret_post)
return TRUE;
return FALSE;
}
This made the preview view plug-in enabled, so it makes use of the owner-drawn menu plug-in for the popup menu
used to select how many pages the user wishes to preview at a time. A toolbar resource, which is loaded by the
owner-drawn menu plug-in was added with the correct images.
So how did it look?
At this point I had all the code that changes the print orientation and printer disabled, so
what we had was this:
I Now needed to enable the printer selection and the paper orientation features.
Paper orientation and printer selection
In the original aritcle, I had a class which enumerates all the printers installed on the local system. Along
with this class, the user had to add some functions to their CWinApp
derived class to allow the
page orientation and the printer to be changed. But we cannot do this in our plug-in. We have to find a different
method of doing this because we cannot modify the CWinApp
or CPlugInApp
derived classes.
Changing the paper orientation
If you look in the original article, you will see that the paper orientation change is done like this:
class CMultiPagePrintPreviewApp : public CWinApp
{
CEnumPrinters m_Printers;
void SetPrintOrientation(int mode);
}
void CMultiPagePrintPreviewApp::SetPrintOrientation(int mode)
{
m_Printers.SetPrintOrientation(m_hDevMode, mode);
}
bool CEnumPrinters::SetPrintOrientation(HANDLE &hDevMode, int mode)
{
if (hDevMode == INVALID_HANDLE_VALUE)
return false;
switch (mode)
{
case DMORIENT_PORTRAIT :
{
LPDEVMODE pDevMode = (LPDEVMODE)::GlobalLock(hDevMode);
pDevMode->dmOrientation = DMORIENT_PORTRAIT;
::GlobalUnlock(hDevMode);
}
break;
case DMORIENT_LANDSCAPE :
{
LPDEVMODE pDevMode = (LPDEVMODE)::GlobalLock(hDevMode);
pDevMode->dmOrientation = DMORIENT_LANDSCAPE;
::GlobalUnlock(hDevMode);
}
break;
default :
ASSERT(FALSE);
return false;
}
return true;
}
In this version we had to call the relevant function of the CWinApp
derived class as
we needed access to the CWinApp::m_hDevMode
global handle, which manages the currently
selected printer. We need to find a different way of doing this.
After looking through the MSDN, I discovered that calling the function CWinApp::GetPrinterDeviceDefaults()
returns the required handles in a PRINTDLG
structure. So we can extract the function from the CEnumPrinters
class and rewrite it as follows:
bool CMultiPagePreviewView::SetPrintOrientation(int mode) const
{
PRINTDLG pd;
pd.lStructSize = (DWORD)sizeof(PRINTDLG);
BOOL bRet = AfxGetApp()->GetPrinterDeviceDefaults(&pd);
if (bRet)
{
switch (mode)
{
case DMORIENT_PORTRAIT :
{
LPDEVMODE pDevMode = (LPDEVMODE)::GlobalLock(pd.hDevMode);
pDevMode->dmOrientation = DMORIENT_PORTRAIT;
::GlobalUnlock(pd.hDevMode);
}
break;
case DMORIENT_LANDSCAPE :
{
LPDEVMODE pDevMode = (LPDEVMODE)::GlobalLock(pd.hDevMode);
pDevMode->dmOrientation = DMORIENT_LANDSCAPE;
::GlobalUnlock(pd.hDevMode);
}
break;
default :
ASSERT(FALSE);
return false;
}
return true;
}
return false;
}
So we can hook up the paper orientation, but how do we switch printers?
Switching printers
Well again we need to avoid using functions added to the CWinApp
derived class so we need
to make use of existing MFC code. The most obvious of which was CWinApp::SelectPrinter()
, but
after playing with this for a while, I could still only get GPFs!
So I went back to my original article and took a look at the comments added by users of the code, and found the
following entry by asconaga:
So, following his advice, I removed the printer combo box, and added a print setup button and mapped the relevant code.
And hey presto, it works without any problems.
Conclusion
Another article in the series and another flaw in the library exposed and killed, hopefully the last of nay present.
This plug-in demonstrates the replacement and enhancement of a standard MFC feature, again in a modular fashion, building
on my previous article series to produce a single MFC upgrade path.
I hope you have enjoyed reading (and using) this article.
References
Here are a list of related articles used in the making of this plug-in
Version history
- V1.0 21st May 2004 - Initial release
A research and development programmer working for a pharmaceutical instrument company for the past 17 years.
I am one of those lucky people who enjoys his work and spends more time than he should either doing work or reseaching new stuff. I can also be found on playing DDO on the Cannith server (Send a tell to "Maetrim" who is my current main)
I am also a keep fit fanatic, doing cross country running and am seriously into [url]http://www.ryushinkan.co.uk/[/url] Karate at this time of my life, training from 4-6 times a week and recently achieved my 1st Dan after 6 years.