Лучший способ получить доступ к вложенному окну из другого вложенного окна

Приложение с графическим интерфейсом имеет следующую иерархию окон:

                   CMainWnd                      <---- main window
     CLeftPane                   CRightPane       <---- left and right panes (views)
CLDlg1       CLDlg2          CRDlg1     CRDlg2   <---- controls container windows (dialogs)
...          ...             ...        ...      <---|
CCtrl1       ...             ...        CCtrl2   <---|- controls
...          ...             ...        ...      <---|

Родительские окна над их детьми.
Каждое дочернее окно является защищенным членом родительского класса wnd.
Каждый класс дочернего окна имеет ссылку / указатель на свое родительское окно.
Панели - это пользовательские элементы управления заполнением (представления).
Все элементы управления являются стандартными элементами управления MFC.

Немного CCtrl1обработчик событий нужно изменить CCtrl2 (например, чтобы установить его текст). Каков наилучший способ достичь этого? Каков наилучший способ получить доступ к окну, вложенному в одну ветвь иерархии окон из другого окна, вложенному в другую ветвь иерархии окон?

Я публикую здесь два решения.

Решение 1

  • все дочерние диалоги (управляющие контейнеры) имеют:
    • публичные геттеры, которые возвращают родительский диалог и
    • публичные методы, которые выполняют некоторые действия с дочерними элементами управления (поэтому дочерние элементы управления скрыты)
  • Корневое окно имеет публичные геттеры, которые возвращают панели

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;       
};

Случай, который у меня здесь, похож на описанный в этом вопросе: цепочка публичных добытчиков (GetMainWnd().GetRightPane().GetRDlg2()...) используется для доступа к нужному вложенному объекту. CLDlg1 знает о CRightPane и CRDlg2, который нарушает Закон Деметры.

Этой ситуации можно избежать, переместив SetCtrl2Text(...) метод на верхний уровень в иерархии, который описан в:

Решение 2

В этом случае CMainWnd содержит все необходимые методы, которые выполняют действия в глубоко вложенных элементах управления.

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);
}

Это скрывает иерархию окон от своих клиентов, но этот подход:

  • марки CMainWnd класс переполнен открытыми методами, которые выполняют все действия со всеми вложенными элементами управления; CMainWnd служит главным распределительным щитом для всех действий клиентов;
  • CMainWnd и каждый вложенный диалог повторяют эти методы в своих открытых интерфейсах.

Какой подход будет предпочтительнее? Или есть ли другое решение / шаблон для этой проблемы?

Решение 3

Еще одним решением будет использование класса интерфейса, который содержит обработчики событий для конкретного объекта источника события. Класс целевого объекта реализует этот интерфейс, а источник и обработчик события слабо связаны. Может быть, это путь? Это обычная практика в GUI?

РЕДАКТИРОВАТЬ:

Решение 4 - Шаблон издателя / подписчика

В предыдущем решении исходный объект события сохраняет ссылку на обработчик события, но проблема возникает, если имеется несколько прослушивателей событий (необходимо обновить два или более классов по событию). Шаблон Publisher/Subscriber (Observer) решает эту проблему. Я провел небольшое исследование этого паттерна и придумал две версии, как добиться передачи данных о событиях из источника в обработчик. Код здесь основан на втором:

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());
   ...
}

Это решение разъединяет источник событий (CLDlg1) и обработчик (CRDlg2) - они не знают друг о друге.

Учитывая приведенные выше решения и управляемый событиями характер GUI, мой оригинальный вопрос переходит в другую форму: как отправить событие из одного вложенного окна в другое?

2 ответа

Решение

Цитировать комментарий ОП:

Еще одним решением будет использование класса интерфейса, который содержит обработчики событий для конкретного объекта источника события. Класс целевого объекта реализует этот интерфейс, а источник и обработчик события слабо связаны. Может быть, это путь? Это обычная практика в GUI?

Я предпочитаю это решение. Это очень распространено в других языках / платформах (особенно в Java), но редко встречается в MFC. Не потому что это плохо, а потому что MFC слишком старомоден и ориентирован на C.

Кстати, более ориентированное на MFC решение будет использовать сообщения Windows и механизм маршрутизации команд MFC. Это можно сделать очень быстро и легко с помощью переопределений OnCmdMsg.

Использование явных интерфейсов (в частности, источника событий и прослушивателей событий) потребует больше времени и усилий, но выдает более читаемый и поддерживаемый исходный код.

Если вас не устраивают прослушиватели событий, дизайн на основе сообщений Windows будет более перспективным, чем решение 1, 2.

Может быть, вы можете позволить элементам управления быть публичными или использовать методы друзей для сохранения записи метода получения и установки. Я думаю, что это должно быть принято в иерархии классов пользовательского интерфейса, потому что вы не разрабатываете свои пользовательские элементы управления для повторного использования, поэтому их не следует заключать так сложно. Они специфичны для того единственного окна, которое вы разрабатываете в любом случае.

Таким образом, доступ к нему с вершины иерархии может быть сделан следующим образом (и я также думаю, что было бы лучше сохранить все обработчики событий в одном классе вверху):

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

И объявить дружественную функцию GetCtrl2 в классах CLeftPane и CDlg1

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

Или, в качестве альтернативы, сделайте все элементы управления публичными

ОБНОВЛЕНИЕ: я имел в виду, что в пользовательском классе диалога есть функция друга, а не элемент управления.

Другие вопросы по тегам