【问题标题】:Best way to access nested window from another nested window从另一个嵌套窗口访问嵌套窗口的最佳方式
【发布时间】:2011-09-21 07:30:12
【问题描述】:

GUI 应用程序具有以下窗口层次结构:

                   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

  • 所有子对话框(控制容器)都有:​​
    • 返回父对话框和
      的公共getter
    • 对其子控件执行某些操作的公共方法(因此子控件被隐藏)
  • 根窗口具有返回窗格的公共 getter

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

我在这里遇到的情况类似于this 问题中描述的情况:公共 getter 链 (GetMainWnd().GetRightPane().GetRDlg2()...) 用于访问所需的嵌套对象。 CLDlg1 知道违反Law of Demeter 的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 - 发布者/订阅者模式

在以前的解决方案中,事件源对象保留了对事件处理程序的引用,但是如果有多个事件侦听器(需要在事件上更新两个或更多类),就会出现问题。发布者/订阅者(观察者)模式解决了这个问题。我对这种模式进行了一些研究,并提出了two versions 如何实现将事件数据从源传递到处理程序。这里的代码是基于第二个的:

观察者.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 的事件驱动性质,我最初的问题正在演变为另一种形式:如何将事件从一个嵌套窗口发送到另一个嵌套窗口?

【问题讨论】:

    标签: c++ user-interface mfc gui-design


    【解决方案1】:

    也许您可以让控件公开或使用友元方法来保存 getter、setter 编写。我认为这应该在 ui 类层次结构中被接受,因为您没有将自定义控件设计为可重用,因此不应将它们封装得那么难。无论如何,它们都特定于您正在设计的单个窗口。

    所以从层次结构的顶部访问它可能会这样做(而且我认为最好将所有事件处理程序保留在一个类中的顶部):

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

    并在 CLeftPane 和 CDlg1 类中声明 GetCtrl2 友元函数

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

    或者将所有控制成员公开

    更新:我的意思是自定义对话框类有朋友功能而不是控件。

    【讨论】:

    • 我写道“所有控件都是标准 MFC 控件”。所以不能改变他们的代码并认为这不能回答问题。我的困境包括控件容器的设计——从对话框类开始。我更喜欢保护控件,因为对话框的客户端不应该知道它们。
    • 是的,我在那里犯了一个错误。容器类应该具有主窗口的友元函数,用于获取属于该容器及其子容器的每个控件
    • 好吧,您问如何从不同的地方访问容器的控件,并为此沿着链编写 getter/setter 或做其他事情。我想最好让控件受到保护,但我要说的是控件层次结构基本上是一个单一的代码单元,那么如果你想从事件处理程序访问它们,为什么要将所有东西都封装到容器类中呢?通过使用友元函数,您不必使用公共成员,但单个 getter 函数仍然可以访问容器链中所需的所有内容。
    • 关于我的问题中发布的代码,我相信您的 'GetCtrl2()' 应该返回 'm_rightPane.m_RDlg2.m_ctrl2' 并且应该是 CRDlg2 的朋友。无论如何,我认为这个解决方案不够灵活:将CCtrl2 控件替换为CCtrl3 类型的其他控件,意味着CMainWndCLDlg1 的更改。触发事件的对象不应该知道事件处理程序的类型(解决方案 1 也不符合此条件)。另一件事对我来说听起来不正确是用 getter 污染所有事件处理程序控件的 CMainWnd(解决方案 2 具有类似的“污染”)。
    【解决方案2】:

    引用OP的评论:

    另一个解决方案是使用 包含事件的接口类 特定事件源的处理程序 目的。目标对象的类 实现这个接口和事件 源和处理程序松散 耦合。这可能是要走的路吗? 这是 GUI 中的常见做法吗?

    我更喜欢这个解决方案。这在其他语言/平台(尤其是 Java)中很常见,但在 MFC 中很少见。不是因为它不好,而是因为 MFC 太老土而且面向 C。

    顺便说一句,更多面向 MFC 的解决方案将使用 Windows 消息和MFC Command Routing 机制。这可以通过 OnCmdMsg 覆盖快速轻松地完成。

    使用显式接口(特别是事件源和事件侦听器)将需要更多时间和精力,但会提供更易读和可维护的源代码。

    如果您对事件侦听器不满意,那么基于 Windows 消息的设计会比解决方案 1、2 更有希望。

    【讨论】:

    • 我的直觉告诉我这实际上是唯一正确的方法,但我不确定 MFC 中是否有捷径。在 MFC 中使用自定义消息不是类型安全的(从 WPARAM/LPARAM 转换),我更喜欢将我的事件与来自 Windows 的事件分开,最好使用通用方法 - 我作为解决方案 4 添加的一种。
    • 将您的答案标记为接受使用事件侦听器的建议,以便保持类解耦(我选择解决此问题的路线)。谢谢!
    猜你喜欢
    • 2015-01-07
    • 1970-01-01
    • 2016-08-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-05-01
    相关资源
    最近更新 更多