MFC拆分窗口与视图间数据交换:CSplitterWnd完整实践指南

发布时间:2026/9/3 21:25:08
MFC拆分窗口与视图间数据交换:CSplitterWnd完整实践指南 简介面向MFC开发者的拆分窗口专题资源围绕CSplitterWnd类讲解如何在单个窗口内创建多个子视图并解决视图之间的数据同步问题适用于构建编辑器、数据库管理面板等多区域桌面程序。资源包共29个文件压缩前约68KB内容涵盖8个头文件、7个CPP源文件、1份Word说明文档、图标与RC资源脚本等完整源码工程与配套文字讲解齐全便于对照学习。已有885人学习下载。示例工程SplitterWndDemo演示了静态与动态拆分窗口的创建流程从继承CSplitterWnd、重写OnCreate到组装视图和消息映射均有体现文档部分则系统梳理了成员变量共享、自定义消息传递、ON_NOTIFY事件通知、COleDataObject剪贴板、文档视图架构以及观察者模式等六种数据交换策略可帮助开发者根据项目场景选择合适方案。 做MFC界面开发拆分窗口Splitter Window是个绕不开的需求。不管你是要做左侧树形菜单右侧详情面板还是做类似于调试工具的上位机界面CSplitterWnd几乎是最常用的方案。但很多新手在独立实现拆分窗口时普遍会遇到两个问题一是不知道动态拆分和静态拆分怎么选二是一旦把窗口拆成两个独立视图视图之间的数据交换就成了麻烦事。这篇文章不做原理堆砌只讲怎么做、为什么这么做以及数据交换的五种实用方案和我在实际项目中踩过的坑。1. 拆分窗口的选型动态拆分还是静态拆分1.1 动态拆分的实现原理动态拆分是指在运行时用户可以通过拖动拆分条自由调整各窗格大小甚至可以多次拆分。这种模式适用于窗格数量和排列方式不确定的场景比如编辑器的分屏功能。在MFC中动态拆分核心是CSplitterWnd的Create方法// 在主框架的OnCreateClient中 BOOL CMainFrame::OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext* pContext) { if (!m_wndSplitter.Create(this, 2, 2, CSize(100, 100), pContext)) { TRACE(拆分窗口创建失败\n); return FALSE; } return TRUE; }这里2行2列代表初始布局是2x2的网格最大窗格数也是由Create的第二个和第三个参数决定的。动态拆分有个特点所有窗格共享同一个视图类。也就是说你不能在一个窗格里放树形控件、另一个窗格里放列表控件除非你重写视图的创建逻辑。这就是很多人在实际开发中发现“动态拆分只能拆分相同视图”的痛点。1.2 静态拆分的关键差异静态拆分CreateStatic则完全不一样。它允许每个窗格使用不同的视图类这一点在实际项目里太重要了。BOOL CMainFrame::OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext* pContext) { if (!m_wndSplitter.CreateStatic(this, 1, 2)) { return FALSE; } // 为左窗格创建树形视图 if (!m_wndSplitter.CreateView(0, 0, RUNTIME_CLASS(CLeftTreeView), CSize(200, 100), pContext)) { return FALSE; } // 为右窗格创建列表视图 if (!m_wndSplitter.CreateView(0, 1, RUNTIME_CLASS(CRightListView), CSize(600, 100), pContext)) { return FALSE; } return TRUE; }静态拆分的核心逻辑是“先划分区域再绑定视图类”。CreateStatic只负责把客户区分成指定的行列具体的视图创建通过CreateView逐格完成。这样左窗格用CTreeView右窗格用CListView互不干涉。选择建议如果你的界面布局固定、左右两边的内容不一样直接用静态拆分省心省力。如果你的功能是类似多标签文档编辑希望用户能自由拆分和合并再考虑动态拆分。2. 左右窗格数据交换的四种主流方案2.1 方案一通过主框架类直接访问子视图拆分窗口常驻于主框架CMainFrame中因此最直接的方案就是通过主框架获取子视图指针然后调用子视图的公有方法。// 从右侧视图获取左侧视图指针 CLeftTreeView* pTreeView (CLeftTreeView*)((CMainFrame*)AfxGetMainWnd()) -m_wndSplitter.GetPane(0, 0); // 调用左侧视图的公有方法 pTreeView-AddTreeNode(新的节点);这个方案的优点是简单直接适合跨视图调用方法很频繁的场景。但缺点也很明显——视图类之间形成硬耦合左侧视图改了类名或方法签名右侧视图就要跟着修改。项目小的时候可以这么干项目大了维护成本会比较高。2.2 方案二自定义消息实现单向通知自定义消息是MFC中很经典的做法。它把视图之间的直接调用转换成消息发送调用方不需要知道接收方是谁接收方只需在自己的消息映射中处理。先定义消息 ID#define WM_LEFT_DATA_CHANGED (WM_APP 100) #define WM_RIGHT_DATA_UPDATED (WM_APP 101)在接收方视图类的头文件中声明消息处理函数class CLeftTreeView : public CTreeView { public: afx_msg LRESULT OnLeftDataChanged(WPARAM wParam, LPARAM lParam); DECLARE_MESSAGE_MAP() };在实现文件中添加消息映射和处理逻辑BEGIN_MESSAGE_MAP(CLeftTreeView, CTreeView) ON_MESSAGE(WM_LEFT_DATA_CHANGED, CLeftTreeView::OnLeftDataChanged) END_MESSAGE_MAP() LRESULT CLeftTreeView::OnLeftDataChanged(WPARAM wParam, LPARAM lParam) { // wParam可以传指针lParam可以传长度或额外信息 // 例如CString* pStr (CString*)wParam; // TRACE(收到右侧数据变更通知%s\n, *pStr); return 0; }发送方调用时只需要获取接收方窗口句柄并发送消息CWnd* pLeftWnd GetDlgItem(ID_LEFT_TREE_VIEW); // 或用其他方式获取 // 如果已经拿到视图指针直接 pLeftWnd-SendMessage(...) ::SendMessage(pLeftWnd-GetSafeHwnd(), WM_LEFT_DATA_CHANGED, (WPARAM)strData, 0);自定义消息方案将耦合度从“类级别”降到了“消息级别”。接收方的类实现变化不影响发送方只要消息契约不变就行。这个方案在单文档界面SDI和拆分窗口中都很常用。2.3 方案三共享数据类或单例对象如果你的左右视图操作的是同一份业务数据比如同一个设备参数配置那么更合理的做法是抽出独立的共享数据类。视图不直接通信而是都去读写一个公共对象再通过消息或回调通知对端刷新界面。// 共享数据类全局唯一或由主框架持有 class CSharedConfig { public: static CSharedConfig Instance() { static CSharedConfig instance; return instance; } void SetDeviceName(const CString name) { m_strDeviceName name; } CString GetDeviceName() const { return m_strDeviceName; } private: CString m_strDeviceName; };使用时左侧视图更新数据CSharedConfig::Instance().SetDeviceName(_T(PLC-01)); // 通知右侧视图刷新 ::SendMessage(pRightWnd-GetSafeHwnd(), WM_RIGHT_DATA_UPDATED, 0, 0);右侧视图收到消息后从共享数据类中读取最新数据刷新界面LRESULT CRightListView::OnRightDataUpdated(WPARAM wParam, LPARAM lParam) { CString strName CSharedConfig::Instance().GetDeviceName(); // 刷新列表或编辑框 m_editName.SetWindowText(strName); return 0; }这种做法的精髓在于数据与界面分离。视图只负责展示和用户交互数据变更汇聚在共享类中排查问题时只需要看共享类的读写点不用在视图之间来回追。2.4 方案四文档-视图架构的UpdateAllViews如果项目使用了文档-视图架构比如CFormView配合CDocument那么MFC已经为你提供了现成的数据交换机制——UpdateAllViews。// 在文档类中修改数据后 m_strSharedData _T(新的数据); UpdateAllViews(NULL, 0, m_strSharedData); // 在视图类中重写OnUpdate void CLeftTreeView::OnUpdate(CView* pSender, LPARAM lHint, CObject* pHint) { if (pHint ! NULL) { CString* pStr (CString*)pHint; // 根据提示更新界面 } }文档-视图方案的优点是框架帮你处理了视图的遍历和通知数据变更的源头明确。缺点是拆分窗口配合文档视图时如果你没有用到Doc/View架构强行引入CDocument反而增加复杂度。2.5 方案对比速查表方案耦合度复杂度适用场景主框架直接访问高低小型项目、原型验证自定义消息中中大多数拆分窗口场景共享数据类低中数据模型复杂的应用UpdateAllViews低高标准Doc/View架构3. 实操一个“设备参数面板实时数据列表”的完整实现3.1 窗口布局与视图类设计假设我们要做一个上位机工具左侧是设备参数编辑面板CFormView右侧是实时数据日志列表CListView。功能要求左侧修改参数后右侧日志窗口实时输出变更记录右侧清空日志时左侧状态栏同步更新记录数。窗口布局用静态拆分1行2列左窗格宽度220像素右窗格填充剩余空间。先定义两个视图类// CParamPanelView : public CFormView // 包含若干编辑框、下拉框和一个“应用参数”按钮 // CDataLogView : public CListView // 包含一个列表控件四列时间、参数名、旧值、新值3.2 创建CSplitterWnd的具体步骤在主框架类中添加成员变量class CMainFrame : public CFrameWnd { private: CSplitterWnd m_wndSplitter; };在OnCreateClient中创建静态拆分和两个视图BOOL CMainFrame::OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext* pContext) { // 创建1行2列的静态拆分窗口 if (!m_wndSplitter.CreateStatic(this, 1, 2)) { TRACE(CreateStatic failed\n); return FALSE; } // 创建左侧参数面板视图 if (!m_wndSplitter.CreateView(0, 0, RUNTIME_CLASS(CParamPanelView), CSize(220, 300), pContext)) { TRACE(CreateView CParamPanelView failed\n); return FALSE; } // 创建右侧数据日志视图 if (!m_wndSplitter.CreateView(0, 1, RUNTIME_CLASS(CDataLogView), CSize(600, 300), pContext)) { TRACE(CreateView CDataLogView failed\n); return FALSE; } // 设置最小窗格宽度 m_wndSplitter.SetColumnInfo(0, 220, 150); m_wndSplitter.SetColumnInfo(1, 600, 300); return TRUE; }这里有几个细节要注意。CreateStatic的1行2列决定了窗格结构CreateView的第三个参数必须是视图类的CRuntimeClass可以由RUNTIME_CLASS宏获取。视图类的构造函数必须是默认构造无参否则运行时创建会报错。SetColumnInfo的第二个参数是初始宽度第三个参数是最小宽度。如果不设置用户拖动拆分条时可能把某个窗格拖到几乎不可见体验很差。3.3 定义消息协议与数据传递结构这一节是数据交换的核心。我们要在两个视图之间建立清晰的消息契约。先定义消息ID和数据类型。为了传递参数名和值使用一个简单的结构体// DataProtocol.h #pragma once #define WM_PARAM_CHANGED (WM_APP 200) #define WM_LOG_CLEARED (WM_APP 201) #define WM_LOG_COUNT_UPDATED (WM_APP 202) struct ParamChangeInfo { CString strParamName; CString strOldValue; CString strNewValue; };自定义消息的WPARAM和LPARAM本质上只是整数类型可以传指针。我们把ParamChangeInfo的指针放在WPARAM中传递。这里有一个重要的内存管理问题如果使用SendMessage它是同步执行的接收方处理完消息后才会返回因此传栈上对象的指针是完全安全的。如果使用PostMessage它是异步投递的发送方会立即返回此时如果传的是局部变量的指针函数结束后对象就被销毁了接收方收到的是悬空指针。在拆分窗口的数据交换中尽量用SendMessage避免用PostMessage。3.4 参数面板到日志列表的单向通信在CParamPanelView中点击“应用参数”按钮时收集界面数据并发送到右侧void CParamPanelView::OnBnClickedApply() { CString strParamName, strValue, strOldValue; GetDlgItemText(IDC_EDIT_PARAM_NAME, strParamName); GetDlgItemText(IDC_EDIT_PARAM_VALUE, strValue); // 记录旧值这里简单处理实际可以从共享数据类中获取 strOldValue CSharedConfig::Instance().GetParamValue(strParamName); // 构造变更信息 ParamChangeInfo info; info.strParamName strParamName; info.strOldValue strOldValue; info.strNewValue strValue; // 获取右侧视图指针并发送消息 CMainFrame* pMainFrame (CMainFrame*)AfxGetMainWnd(); if (pMainFrame ! NULL) { CWnd* pRightWnd pMainFrame-m_wndSplitter.GetPane(0, 1); if (pRightWnd ! NULL) { pRightWnd-SendMessage(WM_PARAM_CHANGED, (WPARAM)info, 0); } } // 同步更新共享数据 CSharedConfig::Instance().SetParamValue(strParamName, strValue); }在CDataLogView中处理这条消息BEGIN_MESSAGE_MAP(CDataLogView, CListView) ON_MESSAGE(WM_PARAM_CHANGED, CDataLogView::OnParamChanged) END_MESSAGE_MAP() LRESULT CDataLogView::OnParamChanged(WPARAM wParam, LPARAM lParam) { ParamChangeInfo* pInfo (ParamChangeInfo*)wParam; if (pInfo NULL) return 0; CListCtrl refList GetListCtrl(); // 在列表顶部插入一行 int nIndex refList.InsertItem(0, CTime::GetCurrentTime().Format(_T(%H:%M:%S))); refList.SetItemText(nIndex, 1, pInfo-strParamName); refList.SetItemText(nIndex, 2, pInfo-strOldValue); refList.SetItemText(nIndex, 3, pInfo-strNewValue); return 0; }这里调用GetPane(0, 1)获取右侧窗格的CWnd指针再直接调用SendMessage。由于拆分窗口是主框架的成员变量主框架和视图类之间需要互相暴露一些接口这在同项目的类之间是正常的。3.5 日志列表到参数面板的反向同步右侧清空日志后应该通知左侧更新状态显示。在CDataLogView的“清空日志”按钮中void CDataLogView::OnBnClickedClearLog() { GetListCtrl().DeleteAllItems(); CMainFrame* pMainFrame (CMainFrame*)AfxGetMainWnd(); if (pMainFrame ! NULL) { CWnd* pLeftWnd pMainFrame-m_wndSplitter.GetPane(0, 0); if (pLeftWnd ! NULL) { pLeftWnd-SendMessage(WM_LOG_CLEARED, 0, 0); } } }在CParamPanelView中处理这条消息更新状态栏文本LRESULT CParamPanelView::OnLogCleared(WPARAM wParam, LPARAM lParam) { SetDlgItemText(IDC_STATIC_LOG_COUNT, _T(日志已清空)); return 0; }这里要注意的是WM_APP200之后的消息号是私有的不会与系统消息和控件通知冲突。多个视图之间使用不同的消息ID可以避免互相干扰。3.6 数据同步的实际效果与边界情况当用户修改参数后右侧日志能立刻收到通知并插入一条记录。当用户清空日志后左侧面板的提示文字同步更新。这套机制的核心价值在于视图之间的协作是“按事件驱动”的而不是“轮询查看状态”。边界情况处理同样重要。比如左侧视图连续修改参数10次右侧会收到10条消息因为SendMessage是同步的实际执行顺序是先更新日志再更新界面所以用户看到的日志总是最新的。如果改成PostMessage消息会在窗口消息队列中排队用户快速操作时日志顺序有保证因为消息队列是FIFO的但需要注意指针的生命周期。还有一个容易忽略的点拆分窗口的所有视图都是在OnCreateClient中创建的此时窗口还没显示出来不能在这时候发送消息。等窗口创建完成、视图的OnInitialUpdate执行完毕后通信才安全。我见过有人把SendMessage放在OnCreateClient里结果接收方视图还没完成初始化导致崩溃。4. 常见问题与排查技巧实录4.1 拆分窗口创建失败创建一个拆分窗口时最常遇到的就是CreateStatic或CreateView返回FALSE。通常原因有几种一是视图类没有DECLARE_DYNCREATE和IMPLEMENT_DYNCREATE宏CSplitterWnd不能动态创建这种类二是视图类没有默认构造函数三是资源ID冲突。排查方法很简单在OnCreateClient中逐行检查返回值用TRACE输出错误信息BOOL CMainFrame::OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext* pContext) { if (!m_wndSplitter.CreateStatic(this, 1, 2)) { TRACE(CreateStatic failed: %d\n, GetLastError()); return FALSE; } // 这里注意CreateView失败时最好也检查一下 if (!m_wndSplitter.CreateView(0, 0, RUNTIME_CLASS(CParamPanelView), CSize(220, 300), pContext)) { TRACE(CreateView failed, class: %s\n, RUNTIME_CLASS(CParamPanelView)-m_lpszClassName); return FALSE; } // ... }视图类的声明中必须包含DECLARE_DYNCREATE(CParamPanelView)实现文件中必须包含IMPLEMENT_DYNCREATE(CParamPanelView, CFormView)。这是最容易被遗漏的一点。另外CFormView的子类在创建前需要关联对话框模板资源如果ID不正确CreateView也会失败。4.2 拆分条拖动后视图显示错乱静态拆分窗口中如果视图大小在WM_SIZE中处理不当拖动拆分条后会出现内容显示不全或空白区域的问题。CFormView本身会对控件进行布局但如果你手动计算了控件位置就需要在OnSize中重新布局。对于CListView刷新列表控件的列宽即可void CDataLogView::OnSize(UINT nType, int cx, int cy) { CListView::OnSize(nType, cx, cy); // 调整列表列宽让列自适应窗口 CListCtrl refList GetListCtrl(); if (refList.GetSafeHwnd() ! NULL) { int nTotalWidth cx; refList.SetColumnWidth(0, nTotalWidth * 0.15); refList.SetColumnWidth(1, nTotalWidth * 0.25); refList.SetColumnWidth(2, nTotalWidth * 0.25); refList.SetColumnWidth(3, nTotalWidth * 0.35); } }这里还有一个容易犯的错误在CListView的OnInitialUpdate中设置了列的初始宽度但窗口创建初期OnSize会被触发多次如果在这个时机还没有创建列表控件判断GetSafeHwnd是否为空就非常重要。4.3 ListCtrl选中后失去焦点变灰这是很经典的问题。在拆分窗口中右侧CListView的列表控件选中一行后如果用户点击左侧窗格右侧选中行会变成灰色视觉上不太友好。解决办法是设置CListCtrl的扩展风格// 在CDataLogView的OnInitialUpdate中 CListCtrl refList GetListCtrl(); refList.SetExtendedStyle(refList.GetExtendedStyle() | LVS_EX_FULLROWSELECT | LVS_EX_GRIDLINES);但这样还不够窗口失去焦点后选中项仍然会变色。解决办法是处理视图的WM_ACTIVATE和WM_KILLFOCUS消息在这种状态下把选中项颜色强制恢复可以在自定义绘制中处理也可以简单地在失去焦点时设置一个标志在OnPaint或NM_CUSTOMDRAW中根据标志决定绘制颜色。个人经验是使用NM_CUSTOMDRAW消息重绘选中行void CDataLogView::OnNMCustomdraw(NMHDR* pNMHDR, LRESULT* pResult) { NMLVCUSTOMDRAW* pLVCD (NMLVCUSTOMDRAW*)pNMHDR; if (pLVCD-nmcd.dwDrawStage CDDS_PREPAINT) { *pResult CDRF_NOTIFYITEMDRAW; return; } if (pLVCD-nmcd.dwDrawStage CDDS_ITEMPREPAINT) { if (pLVCD-nmcd.uItemState CDIS_SELECTED) { // 自定义选中颜色不随焦点变化 pLVCD-clrText RGB(0, 0, 0); pLVCD-clrTextBk RGB(200, 220, 240); } *pResult CDRF_NEWFONT; return; } *pResult CDRF_DODEFAULT; }这种方式比OnActivate状态判断更直接颜色统一不变用户视觉上不会有焦点的突兀切换。4.4 数据交换时消息丢失或收不到消息使用SendMessage发送自定义消息到子视图时偶尔会看到消息收不到的情况。排查思路是先确认接收方窗口句柄非空再确认消息ID没有冲突。// 排查消息是否发送成功 CWnd* pRightWnd pMainFrame-m_wndSplitter.GetPane(0, 1); if (pRightWnd NULL || pRightWnd-GetSafeHwnd() NULL) { AfxMessageBox(_T(右侧视图未创建)); return; } BOOL bResult pRightWnd-SendMessage(WM_PARAM_CHANGED, (WPARAM)info, 0);SendMessage的返回值就是消息处理函数的返回值如果你在OnParamChanged中正确return了值bResult应该有预期值。如果bResult始终为0且界面没有变化检查消息映射是否写错。还有一个细节因为使用ON_MESSAGE映射自定义消息消息处理函数必须是afx_msg LRESULT类型WPARAM和LPARAM参数顺序不能反否则数据错乱。4.5 关闭窗口时崩溃拆分窗口的销毁顺序也有讲究。如果你的视图类在析构函数中释放了某个资源而该资源被另一个视图通过指针引用关闭窗口时可能因为销毁顺序产生悬空指针。一般情况下CSplitterWnd会负责销毁所有子视图顺序是先创建的后销毁。如果两个视图互相持有对方的指针在视图的OnDestroy中把对外暴露的指针置空不要等析构函数再处理。这是一个安全有效的防御性写法。5. 实际开发中的经验补齐5.1 拆分条的最小尺寸限制默认情况下拆分窗口允许用户把某个窗格拖到宽度为0这在实际体验中很糟糕。用SetColumnInfo设置最小尺寸是必要操作m_wndSplitter.SetColumnInfo(0, 220, 180); // 左窗格最小180 m_wndSplitter.SetColumnInfo(1, 600, 300); // 右窗格最小300这里的第一个参数是列索引第二个是当前宽度第三个是最小宽度。如果不设置系统默认最小宽度可能是0或一个很小的值。5.2 嵌套拆分超过两层的复杂布局如果你的界面需要三栏布局比如左侧树、中间列表、右侧详情可以在某个窗格内再创建一个CSplitterWnd这就是嵌套拆分。// 先拆分1行2列 m_wndSplitter.CreateStatic(this, 1, 2); m_wndSplitter.CreateView(0, 0, RUNTIME_CLASS(CLeftTreeView), CSize(200, 100), pContext); // 再在右侧窗格创建子拆分 m_wndSubSplitter.CreateStatic(m_wndSplitter, 1, 2, WS_CHILD | WS_VISIBLE, m_wndSplitter.IdFromRowCol(0, 1)); m_wndSubSplitter.CreateView(0, 0, RUNTIME_CLASS(CMiddleListView), CSize(400, 100), pContext); m_wndSubSplitter.CreateView(0, 1, RUNTIME_CLASS(CRightDetailView), CSize(200, 100), pContext);嵌套拆分的关键在于CSplitterWnd::CreateStatic的最后一个参数要传入父窗格的子窗口ID。父窗格的ID用IdFromRowCol(row, col)获取比如IdFromRowCol(0, 1)就是右侧窗格的ID。这样子拆分窗口才能正确嵌入到父窗格中。嵌套拆分在数据交换上需要更清晰的消息路由。因为跨层访问路径变长建议子拆分窗口的视图不要直接访问主框架而是通过父视图或共享数据类中转否则代码耦合度会急剧上升。5.3 自绘控件与拆分窗口的配合在实际工控或上位机项目中很多人喜欢用自绘按钮和美化表格。拆分窗口本身不限制内容但要注意自绘控件刷新和视图刷新的协调。例如左侧面板中有一个自绘的“启动”按钮按钮状态改变了需要同步到右侧的日志列表。自绘控件一般在OnPaint中绘制你只需要在发送消息后调用Invalidate让控件或视图重绘。// 左侧视图更新按钮状态 m_btnStart.SetState(TRUE); // 通知右侧 pRightWnd-SendMessage(WM_PARAM_CHANGED, ...); // 强制左侧重绘 Invalidate(FALSE);注意如果在SendMessage中同步执行了右侧视图的重绘而右侧视图此时刚好有某种联动关系可能会导致重绘嵌套。这时候可以把右侧的刷新操作通过PostMessage延迟到消息队列尾部执行避免递归重绘。5.4 使用TCHAR和CString时注意字符集拆分窗口中的数据交换离不开字符串处理。如果你的项目使用的是Unicode字符集CString的底层是wchar_t在消息中传递CString指针或者const wchar_t*指针时接收方强转类型必须匹配。推荐在消息结构中统一使用CString避免混用CStringA和CStringW。在项目的字符集设置上保持统一不要在一个项目中混合使用多字节和Unicode否则字符串长度和内存布局都会出问题。6. 最后再分享一点个人心得拆分窗口和视图间数据交换这两件事单独看都不难难的是组合在一起时理不清依赖关系。我个人的建议是在新项目里优先采用“共享数据层自定义消息”的组合方案界面上各自独立数据上共享一份变更时通过消息通知对方刷新。这样即使以后要改布局、替换视图类也不会牵一发而动全身。如果你只是写一个一次性使用的内部小工具直接在主框架里调用视图方法就够了不用过度设计。设计模式的选择应该匹配项目规模和维护周期这是我在踩过几次坑之后总结出来的原则。希望这篇内容对你能有点帮助。本文还有配套的精品资源点击获取