Introduction
In a recent
project I needed to be able to dynamically load
CFormViews
into the
MainFrame
of my application. I searched the developer sites
and found a couple
articles that explained how to load
document/views into MDI applications but
unfortunately, I needed to use the SDI approach. I
searched MSDN and found even more articles
discussing the technique under MDI's. I eventually
decided that it shouldn't be too difficult and
here is the result.
I started out by
referencing an article on Codeguru.com entitled "Replacing
a view in a doc-view application" by
Jorge Lodos Vigil. That article gave me the basics
for switching view
in an SDI application.
The next problem to solve was how to export the
CFormView
(or
CView
derived class)
from a DLL and what kind of DLL? In the MFC
AppWizard(dll) you have three types of DLLs that
you can create, they are:
- Regular DLL
with MFC statically
linked
- Regular DLL
using shared MFC DLL
-
MFC Extension DLL
(using shared MFC DLL)
After some research
and the usual trial and error, I discovered that
the third choice MFC
Extension DLL was the proper DLL type
for my requirements. An
MFC Extension DLL will only work from
an MFC application and in this case that's exactly
what was needed. There are other distinctions
between the types of DLLs and I urge you to
discover them on your own.
The DLL
implementation
After creating an
MFC Extension DLL project go to the ResourceView
in the Workspace and create a new Dialog Resource
of type
IDD_FORMVIEW
.
Next select 'Insert|New Class... ' from the main
menu and create a new
CFormView
derived class using the new dialog as the Dialog
ID. Add the controls and functionality as you
normaly would and then add this code under the
DllMain()
,
ensuring that you replace the variables as
explained in the code.
extern "C" AFX_EXT_API UINT Init(CRuntimeClass** view)
{
new CDynLinkLibrary(YourDLLName);
*view = RUNTIME_CLASS(CYourClass);
return(0);
}
That's pretty
much all you need to do for the DLL side of
things. Looks pretty simple, right? Let's move
onto the main application or the 'host'.
The 'Host'
application
There are endless
ways that we can set up the ‘host’ application to
load up the DLLs and import the views. In this
example I’m not going to do anything fancy, as I
want you to understand how to accomplish the tucancode.net.
I will explain the method I have chosen, based on
my requirements, at the end of the article so stay
tuned.
Create an MFC
executable project using the Single Document (SDI)
option, you can modify all the options in the
Appwizard but on the last page select
CView
or a CView
derived class as the Base class.
Select the
ResourceView tab in the Workspace, open the Menu
treeitem then double-click on the
IDR_MAINFRAME
and add a menuitem under the View menu or create a
new menu - it makes no difference. Then select
your new menuitem and bring up the Class Wizard
and add command routing for it, here is the code
that will load up the DLL, grab the exported
function and retrieve the
CRuntimeClass
.
void CMainFrame::OnViewLoadviewfromdll()
{
typedef UINT ( * LPDLLFUNC)(CRuntimeClass**);
LPDLLFUNC lpfnDllFunc = NULL;
HINSTANCE hDLL = NULL;
hDLL = LoadLibrary("InitialContact.dll");
if(hDLL)
{
lpfnDllFunc = (LPDLLFUNC)::GetProcAddress(hDLL,"Init_");
if (!lpfnDllFunc)
{
AfxMessageBox("Function not found in DLL");
FreeLibrary(hDLL);
return;
}
CRuntimeClass* pNewViewClass;
lpfnDllFunc(&pNewViewClass);
ASSERT(pNewViewClass);
CSdiDllFramesDoc* pDoc = (CSdiDllFramesDoc*) GetActiveDocument();
ASSERT(pDoc); pDoc->SwitchToView(pNewViewClass);
}
else
{
AfxMessageBox("Dll not found!");
}
}
For clarity, I
present the code from Jorge Lodos Vigil's article
referenced above (comments removed):
BOOL CYourDoc::SwitchToView(CRuntimeClass* pNewViewClass)
{
CFrameWnd* pMainWnd = (CFrameWnd*)AfxGetMainWnd();
CView* pOldActiveView = pMainWnd->GetActiveView();
if (pOldActiveView->IsKindOf(pNewViewClass))
return TRUE;
::SetWindowLong(pOldActiveView->m_hWnd, GWL_ID, 0);
CCreateContext context;
context.m_pNewViewClass = pNewViewClass;
context.m_pCurrentDoc = this;
CView* pNewView = STATIC_DOWNCAST(CView, pMainWnd->CreateView(&context));
if (pNewView != NULL)
{
pNewView->ShowWindow(SW_SHOW);
pNewView->OnInitialUpdate();
pMainWnd->SetActiveView(pNewView);
pMainWnd->RecalcLayout();
pOldActiveView->DestroyWindow();
return TRUE;
}
return FALSE;
}
There really
isn't anything new here. We have
typedef
'ed
a function pointer and created a variable of that
type. We then loaded the library, retrieved the
address of our exported function and loaded that
into our function pointer. Then, using a
CRuntimeClass
we passed that into our exported function via the
function pointer. We then grab a pointer to our
CDocument
class and call the member
SwitchToView()
passing in the
CRuntimeClass
that we retrieved from the DLL.
All in all it was
much easier than I thought and I say that with
about three failed attempts, having started with
the first DLL type and working my way down the
list.
An Advanced
Example
The requirements
in my implementation were that the menuitems were
to be read from a database. The table in the
database held the following information:
- Menu Caption
- Menu ID
- DLL Name
- Function Name
With that I
decided upon this class framework to hold the
information read in from the database. I also
provided the class the ability to
LoadPlugin()
and UnLoadPlugin()
the DLLs. Next, I created a
CMap
to hold each instance of the
HSMenuItem
class and set the menu id up to be the key. This
seemed to be a logical choice and after you see
how I handle the menu selections you'll see the
ease of use and extensibility that the 'host'
application provides. Here's the
HSMenuItem
class:
Collapse
class HSMenuItem
{
public:
HSMenuItem(CString _MenuCaption, DWORD _MenuID,
CString _LibraryName, CString _FuncName) :
m_MenuCaption(_MenuCaption), m_ButtonID(_ButtonID),
m_LibraryName(_LibraryName), m_FuncName(_FuncName),
m_bLoaded(FALSE)
{
m_lpfnDllFunc = NULL;
m_hDLL = NULL;
}
~HSMenuItem()
{
if(m_bLoaded)
{
UnLoadPlugin();
}
}
BOOL LoadPlugin()
{
m_hDLL = LoadLibrary(m_LibraryName);
m_lpfnDllFunc = (LPDLLFUNC)::GetProcAddress(m_hDLL,m_FuncName);
if (!m_lpfnDllFunc)
{
FreeLibrary(m_hDLL);
return(FALSE);
}
m_bLoaded = TRUE;
return(TRUE);
}
BOOL UnLoadPlugin()
{
if(m_hDLL)
{
m_lpfnDllFunc = NULL;
FreeLibrary(m_hDLL);
}
return(FALSE);
}
typedef UINT ( * LPDLLFUNC)(CRuntimeClass**);
LPDLLFUNC m_lpfnDllFunc;
CString m_MenuCaption; DWORD m_MenuID; CString m_LibraryName; CString m_FuncName;
private:
BOOL m_bLoaded;
HINSTANCE m_hDLL;
};
typedef CMap <DWORD,DWORD,HSMenuItem*,HSMenuItem*> mapMenuItems;
Loading the Menus
up from the database and dynamically creating them
is an exercise left to the reader. I will show you
how easy it was to handle the menu options. First
I hooked up my menus to go through the message map
using
ON_COMMAND_RANGE
routing that to my
HandleMenu(UINT menuid)
function that looks like this:
BOOL CMainFrame::HandleMenu(UINT menuid)
{
if (menuid != 0)
{
HSMenuItem* mi = NULL;
m_MenuItems.Lookup(menuid,mi);
if(mi != NULL)
{
CRuntimeClass* runtimeclass;
mi->m_lpfnDllFunc(&runtimeclass);
CAlarmAssistantDoc* pDoc = (CAlarmAssistantDoc*) GetActiveDocument();
pDoc->SwitchToView(runtimeclass);
}
}
}
As you can see
this is very flexible and will load up any
CView
derived class that you have exported from a DLL.
I've found this approach very useful in projects
where you can deliver optional functionality in
stages. Even better, bug fixes do not require a
full recompile of the application - just the DLL.
My thanks go out
to Mr. Vigil for his
SwitchToView()
as well as all the other contributors to
CodeProject.com.