GUI application has following windows hierarchy:
CMainWnd <---- main window
CLeftPane CRightPane <---- left and right panes (views)
CLDlg1 CLDlg2 CRDlg1 CRDlg2 <---- controls container windows (dialogs)
... ... ... ... <---|
CCtrl1 ... ... CCtrl2 <---|- controls
... ... ... ... <---|
Parent windows are above their children.
Each child window is a protected member of parent wnd class.
Each child window class has a reference/pointer to its parent window.
Panes are custom control placeholders(views).
All controls are standard MFC controls.
Some CCtrl1
's event handler needs to change CCtrl2
(e.g. to set its text). What is the best way to achieve this? What is the best way to access a window nested in one branch of window hierarchy from another window, nested in another branch of window hierarchy?
I am posting here two solutions.
MainWnd.h:
#include "LeftPane.h"
#include "RightPane.h"
class CMainWnd
{
public:
CLeftPane& GetLeftPane(){return m_leftPane;}
CRightPane& GetRightPane(){return m_rightPane;}
...
protected:
CLeftPane m_leftPane;
CRightPane m_rightPane;
...
};
LeftPane.h:
#include "MainWnd.h"
#include "LDlg1.h"
#include "LDlg2.h"
class CLeftPane
{
public:
CLeftPane(CMainWnd& mainWnd) : m_mainWnd(mainWnd){};
CMainWnd& GetMainWnd() {return m_mainWnd;}
...
protected:
CMainWnd& m_mainWnd;
CLDlg1 m_LDlg1;
CLDlg2 m_LDlg2;
...
};
RightPane.h:
#include "MainWnd.h"
#include "RDlg1.h"
#include "RDlg2.h"
class CRightPane
{
public:
CRightPane(CMainWnd& mainWnd) : m_mainWnd(mainWnd){};
CMainWnd& GetMainWnd() {return m_mainWnd;}
CRDlg2& GetRDlg2() {return m_RDlg2;}
...
protected:
CMainWnd& m_mainWnd;
CRDlg1 m_RDlg1;
CRDlg2 m_RDlg2;
...
};
LDlg1.h:
#include "LeftPane.h"
#include "Ctrl1.h"
class CLDlg1
{
public:
CLDlg1(CLeftPane& leftPane) : m_leftPane(leftPane){}
protected:
CLeftPane& m_leftPane;
CCtrl1 m_ctrl1;
void OnCtrl1Event();
};
LDlg1.cpp:
#include "LDlg1.h"
#include "RDlg2.h"
void CLDlg1::OnCtrl1Event()
{
...
CString strText("test");
m_leftPane.GetMainWnd().GetRightPane().GetRDlg2().SetCtrl2Text(strText);
....
}
RDlg2.h:
#include "RightPane.h"
#include "Ctrl2.h"
class CRDlg2
{
public:
CRDlg2(CRightPane& rightPane) : m_rightPane(rightPane){}
void SetCtrl2Text(const CString& strText) {m_ctrl2.SetWindowText(strText);}
protected:
CRightPane& m_rightPane;
CCtrl2 m_ctrl2;
};
Case I have here is similar to one described in this question: a chain of public getters (GetMainWnd().GetRightPane().GetRDlg2()...
) is used to access desired nested object. CLDlg1 knows about CRightPane and CRDlg2 which violates Law of Demeter.
This situation can be avoided by moving SetCtrl2Text(...)
method to upper level in hierarchy which is described in:
In this case CMainWnd
contains all necessary methods which perform actions in deeply nested controls.
MainWnd.h:
#include "LeftPane.h"
#include "RightPane.h"
class CMainWnd
{
public:
void SetCtrl2Text(const CString& strText);
...
protected:
CLeftPane m_leftPane;
CRightPane m_rightPane;
...
};
MainWnd.cpp:
void CMainWnd::SetCtrl2Text(const CString& strText)
{
m_rightPane.SetCtrl2Text(strText);
}
RightPane.h:
#include "MainWnd.h"
#include "RDlg1.h"
#include "RDlg2.h"
class CRightPane
{
public:
CRightPane(CMainWnd& mainWnd) : m_mainWnd(mainWnd){};
CMainWnd& GetMainWnd() {return m_mainWnd;}
void SetCtrl2Text(const CString& strText);
...
protected:
CMainWnd& m_mainWnd;
CRDlg1 m_RDlg1;
CRDlg2 m_RDlg2;
...
};
RightPane.cpp:
void CRightPane::SetCtrl2Text(const CString& strText)
{
m_RDlg2.SetCtrl2Text(strText);
}
LDlg1.cpp:
#include "LDlg1.h"
void CLDlg1::OnCtrl1Event()
{
...
CString strText("test");
m_leftPane.GetMainWnd().SetCtrl2Text(strText);
....
}
RDlg2.h:
#include "RightPane.h"
#include "Ctrl2.h"
class CRDlg2
{
public:
CRDlg2(CRightPane& rightPane) : m_rightPane(rightPane){}
void SetCtrl2Text(const CString& strText);
protected:
CRightPane& m_rightPane;
CCtrl2 m_ctrl2;
};
RDlg2.cpp:
void CRDlg2::SetCtrl2Text(const CString& strText)
{
m_ctrl2.SetWindowText(strText);
}
This hides window hierarchy from its clients but this approach:
CMainWnd
class overcrowded with public methods which do all actions on all nested controls; CMainWnd
serves like a main switch board for all clients' actions;Which approach would be preferable? Or, is there any other solution/pattern for this problem?
Yet another solution would be using interface class which contains event handlers for particular event source object. Destination object's class implements this interface and event source and handler are loosely coupled. Is this maybe the way to go? Is this a common practice in GUI?
In previous solution event source object keeps a reference to event handler but the problem arises if there are multiple event listeners (two or more classes need to be updated on event). Publisher/Subscriber (Observer) Pattern solves this. I made a little research on this pattern and came up with two versions of how to achieve passing event data from source to handler. Code here is based on the second one:
Observer.h
template<class TEvent>
class CObserver
{
public:
virtual void Update(TEvent& e) = 0;
};
Notifier.h
#include "Observer.h"
#include <set>
template<class TEvent>
class CNotifier
{
std::set<CObserver<TEvent>*> m_observers;
public:
void RegisterObserver(const CObserver<TEvent>& observer)
{
m_observers.insert(const_cast<CObserver<TEvent>*>(&observer));
}
void UnregisterObserver(const CObserver<TEvent>& observer)
{
m_observers.erase(const_cast<CObserver<TEvent>*>(&observer));
}
void Notify(TEvent& e)
{
std::set<CObserver<TEvent>*>::iterator it;
for(it = m_observers.begin(); it != m_observers.end(); it++)
{
(*it)->Update(e);
}
}
};
EventTextChanged.h
class CEventTextChanged
{
CString m_strText;
public:
CEventTextChanged(const CString& strText) : m_strText(strText){}
CString& GetText(){return m_strText;}
};
LDlg1.h:
class CLDlg1
{
CNotifier<CEventTextChanged> m_notifierEventTextChanged;
public:
CNotifier<CEventTextChanged>& GetNotifierEventTextChanged()
{
return m_notifierEventTextChanged;
}
};
LDlg1.cpp:
// CEventTextChanged event source
void CLDlg1::OnCtrl1Event()
{
...
CString strNewText("test");
CEventTextChanged e(strNewText);
m_notifierEventTextChanged.Notify(e);
...
}
RDlg2.h:
class CRDlg2
{
// use inner class to avoid multiple inheritance (in case when this class wants to observe multiple events)
class CObserverEventTextChanged : public CObserver<CEventTextChanged>
{
CActualObserver& m_actualObserver;
public:
CObserverEventTextChanged(CActualObserver& actualObserver) : m_actualObserver(actualObserver){}
void Update(CEventTextChanged& e)
{
m_actualObserver.SetCtrl2Text(e.GetText());
}
} m_observerEventTextChanged;
public:
CObserverEventTextChanged& GetObserverEventTextChanged()
{
return m_observerEventTextChanged;
}
void SetCtrl2Text(const CString& strText);
};
RDlg2.cpp:
void CRDlg2::SetCtrl2Text(const CString& strText)
{
m_ctrl2.SetWindowText(strText);
}
LeftPane.h:
#include "LDlg1.h"
#include "LDlg2.h"
// forward declaration
class CMainWnd;
class CLeftPane
{
friend class CMainWnd;
...
protected:
CLDlg1 m_LDlg1;
CLDlg2 m_LDlg2;
...
};
RightPane.h:
#include "RDlg1.h"
#include "RDlg2.h"
// forward declaration
class CMainWnd;
class CRightPane
{
friend class CMainWnd;
protected:
CRDlg1 m_RDlg1;
CRDlg2 m_RDlg2;
...
};
MainWnd.h:
class CMainWnd
{
...
protected:
CLeftPane m_leftPane;
CRightPane m_rightPane;
...
void Init();
...
};
MainWnd.cpp:
// called after all child windows/dialogs had been created
void CMainWnd::Init()
{
...
// link event source and listener
m_leftPane.m_LDlg1.GetNotifierEventTextChanged().RegisterObserver(m_rightPane.m_RDlg2.GetObserverEventTextChanged());
...
}
This solution decouples event source (CLDlg1
) and handler (CRDlg2
) - they don't know about each other.
Considering solutions above and event driven nature of GUI, my original question is evolving to another form: How to send event from one nested window to another one?
Quote OP's comment:
Yet another solution would be using interface class which contains event handlers for particular event source object. Destination object's class implements this interface and event source and handler are loosely coupled. Is this maybe the way to go? Is this a common practice in GUI?
I prefer this solution. This is very common in other language/platform (especially Java), but rare in MFC. Not because its bad but because MFC is way too old fashioned and C oriented.
By the way, more MFC oriented solution would be uses Windows messages and the MFC Command Routing mechanism. This can be done very quickly and easily with OnCmdMsg overrides.
Using explicit interfaces (specifically event source and event listeners) would need more time and effort but give out more readable and maintainable source code.
If you are not comfortable with event listeners, Windows message based design would be more promising way to go than Solution 1, 2.