0

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.

Solution 1

  • all children dialogs (control containers) have:
    • public getters which return parent dialog and
    • public methods that perform some action on their children controls (so children controls are hidden)
  • root window has public getters which return panes

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:

Solution 2

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:

  • makes 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;
  • CMainWnd and each nested dialog have these methods repeated in their public interfaces

Which approach would be preferable? Or, is there any other solution/pattern for this problem?

Solution 3

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?

EDIT:

Solution 4 - Publisher/Subscriber Pattern

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?

Community
  • 1
  • 1
Bojan Komazec
  • 9,216
  • 2
  • 41
  • 51

2 Answers2

1

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.

9dan
  • 4,222
  • 2
  • 29
  • 44
  • My gut feeling is telling me this is actually the only right way of doing this but I wasn't sure whether there was some shortcut in MFC. Using custom messages in MFC is not type safe (casting from WPARAM/LPARAM) and I prefer separating my events from those from Windows, preferably using generic approach - one I added as Solution 4. – Bojan Komazec Jun 27 '11 at 14:10
  • Marking your answer as accepted for the suggestion of using event listeners so keeping classes decoupled (the route I picked to solve this problem). Thank you! – Bojan Komazec Jul 28 '11 at 12:06
0

Maybe you can let the controls be public or use friend methods to save the getter, setter writing. This I think should be accepted in an ui class hierarchy because you don't design your custom controls to be reusable, so they shouldn't be encasulated that hard. They are specific to that single window you are designing anyway.

So accessing it from the top of hierarchy might be done like this (and also I think it would be better to keep all the event handlers in one class at the top):

class CMainWnd
{
private:
    CCtrl1 GetCtrl1();
    CCtrl2 GetCtrl2()
    {
        return m_leftPane.m_lDlg1.m_ctrl2;
    }
}

And declare GetCtrl2 friend function in the CLeftPane and CDlg1 classes

class CDlg1
{
    friend CCtrl2 CMainWnd::GetCtrl2();
}

Or alternatively make all control members public

UPDATE: I meant the custom dialog class have the friend function not the control.

sekmet64
  • 1,635
  • 4
  • 19
  • 30
  • I wrote that "All controls are standard MFC controls." so cannot change their code and think that doesn't answer the question. My dilemma covers design of controls' containers - starting with dialog classes. I prefer keeping controls protected as dialog's clients should not know about them. – Bojan Komazec Jun 23 '11 at 15:59
  • Yeah I made an error there. The container classes should have the friend function of the main window for getting each control that belongs to that container and it's sub containers – sekmet64 Jun 23 '11 at 17:39
  • Well you asked how to access the controls of the containers from a different place and to write getters/setters along the chain for this or do something else. I guess it's better to have the controls as protected, but what I'm saying is that the control hierarchy is basically a single unit of code, so why encapsulate everything into the container classes if you want to access them from your event handlers? By using friend functions you don't have to use public members but still that single getter function can access everything it needs down the chain of containers. – sekmet64 Jun 23 '11 at 17:48
  • Regarding the code posted in my question, I believe your 'GetCtrl2()' should be returning 'm_rightPane.m_RDlg2.m_ctrl2' and should be friend of `CRDlg2`. Anyway, I think that this solution is not flexible enough: replacing `CCtrl2` control with some other, of type `CCtrl3`, means changes in `CMainWnd` and `CLDlg1`. Object that fires event should not know the type of event handler (Solution 1 doesn't meet this criteria as well). Another thing that doesn't sound right to me is polluting `CMainWnd` with getters for all event handler controls (Solution 2 features similar 'pollution'). – Bojan Komazec Jun 27 '11 at 13:56