Cdialog的销毁
2025年11月28日:
这是我写的第一篇总结性文档,就是从这里开始,我养成了归纳梳理、总结文档的习惯,并一直保持到现在。到目前为止,我总结了大大小小上百篇文档,其中有一些甚至花费了数月的精力,成为我可以拿出手与别人分享的 “作品”。
回想这篇文档的诞生,还是在史工的督促下开始、由史工几次校稿直至最终完成。最终完成后,也在事业部做了一次报告并分享给了大家,得到了同事们的一些夸赞。
此刻,距离这篇文档诞生已经过去了 3 年半,这几年发生了很多事情,我也已不在 PKPM 工作。但正如前面所说,从这篇文档开始,我养成了一个好习惯,直至现在,甚至以后都会受益终生。
在时间的大跨度下,我看到了一些当年书写这篇文档时都想象不到的独特的意义。或许以后再看到这篇文章、再看到这段文字,就又会感悟出新的想法。
可能这就是总结的魅力,当我把此刻的想法落于纸面(程序员的纸面——md文档),就如同此刻的人生快照被永远存放在了这里。而后不同时刻的我再回头看这些东西的时候,都将是不同的我穿梭回了那一时刻,与那时的我发生碰撞,然后,又会有新的东西迸发出来。
总结并写一些东西,绝对是一件值得付出精力的事。
从项目实例分析对话框的销毁流程
1 单一对话框的销毁
新建简单对话框,添加虚函数OnCancel()、Destroywindow()、PosetNcDestroy(),增加消息响应函数OnClose()、OnDestroy()、OnNcDestroy(),上述六个函数都是与对话框销毁相关,现通过单击对话框右上角的叉号,跟随调试,观察对话框的销毁流程。
1.1 模态对话框
模态对话框即为通过DoModal()等方法创建的对话框。
模态对话框需要在对话框关闭后才能返回,返回值是EndDialog(关于该函数的介绍见本文5.2)中的dwResult参数。1
通过DoModal()创建的模态对话框,直到对话框被销毁,程序才会继续往下执行。从用户的角度来看,模态对话框垄断了用户的输入,当模态对话框打开时,用户只能与该对话框进行交互,而其他用户界面对象接收不到输入信息。因此当模态对话框没关闭之前,是无法操作下方的对话框的,类似于警告窗口,不关闭是无法继续操作的。
单击模态对话框右上角的叉号,首先会通过消息映射,进入OnClose();然后会进入OnCancel(),在该函数中调用CDialog::OnCancel()后,对话框会消失;然后进入Destroywindow(),该函数会通过发送WM_DESTROY和WM_NCDESTROY两个消息,依次进入OnDestroy()和OnNcDestroy()两个函数中2 3,在OnNcDestroy()中,又会调用PosetNcDestroy(),最后返回到Destroywindow()中。
模态对话框的销毁流程如下图所示。
1.2 非模态对话框
非模态对话框需要通过new、create()、ShowWindow(SW_SHOW)等一系列方法,才可以创建。
非模式对话框在建立对话框窗口后直接返回,返回值是对话框的句柄。1
从用户的角度来看,非模态对话框不会垄断用户的输入,当非模态对话框创建完成并显示时,允许用户执行程序中其他任务,而不用关闭这个对话框。(模态和非模态对话框的特点4 5 6)
非模态对话框点击右上角的叉号,只会依次进入OnClose()和OnCancel(),不会像模态对话框一样继续往下销毁。这时窗口已经消失,但其实指针并没有被销毁,内存中还保留着对话框的所有信息。如果还能正确拿到指针的话,通过指针调用ShowWindow(SW_SHOW),对话框还能继续显示。
因此在不做其他处理下,非模态对话框点击右上角叉号或是“取消”,对话框只是不可见且没有被破坏。如果在非模态对话框中实现对话框的销毁,则必须重写OnCancel()成员函数并在其中调用DestroyWindow() 7,无需重新DestroyWindow()。
非模态对话框完整的销毁流程只会在父对话框销毁时才会走完(具体内容见本文2、3节),但这样销毁又是有问题的,因为非模态对话框是new出来的,所以需要有delete与之对应,不处理的话会有野指针存在。8 9
需要特殊说明的是,在微软的CDialog::OnCancel帮助文档中明确写出:非模态对话框在OnCancel()中不要调用基类成员函数7。经过项目实测,在OnCancel()中调用基类的CDialog::OnCancel也并非不可,但是需要放在DestroyWindow()之前,若是CDialog::OnCancel放在DestroyWindow()之后,代码便会报错。从项目实测的效果上来看,在OnCancel中调用基类的CDialog::OnCancel也是多余的,位置不对还会报错,因此本文强烈不建议非模态对话框中调用CDialog::OnCancel。
综上所述,若要非模态对话框点击右上角叉号就会正常销毁,需要在OnCancel()中调用虚函数DestroyWindow(),并且同样需要在PosetNcDestroy()中添加delete this。
一般delete之后,还需要把该指针置为nullptr,但是this是不能直接赋值的,因此本文在这里建议把句柄置为空,以便外部通过GetSafeHwnd()检测DLG窗口存在使用10。而且需要注意的是,是要在delete之后将该指针对应的句柄置为空。
如要在类外销毁对话框的话,需要拿到正确的指针,通过指针调用DestroyWindow(),同样也可以正确销毁对话框。
下方为创建与销毁一个非模态对话框的代码实例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 在主程序中创建非模态对话框:
Pop* pop = new Pop;
pop->Create(IDD_DLG_POP,this); // 父对话框就是当前对话框,在这里会进OnInit
pop->ShowWindow(SW_SHOW); // 显示对话框
// 非模态对话框中:
// .h 中:
class PopDlg : public CDialog
{
protected:
virtual void OnCancel(); // 需要重写的虚函数
virtual void PostNcDestroy(); // 需要重写的虚函数
};
// .cpp 中:
void PopDlg::OnCancel()
{
// TODO: 在此添加专用代码和/或调用基类
//CDialog::OnCancel(); // 根据微软的函数说明,这里不再调用基类的OnCancel()
DestroyWindow(); // 增加
}
void PopDlg::PostNcDestroy()
{
//TODO: 在此添加专用代码和/或调用基类
delete this; // 增加
this->m_hWnd = NULL; // 把句柄置为空
CDialog::PostNcDestroy();
}
如下图所示,是已在 OnCancel()中添加了DestroyWindow()、在PostNcDestroy()中添加了delete this后跟随调试,监测到的非模态对话框销毁流程。可以发现,在代码完整的情况下,非模态对话框的销毁流程基本和模态对话框是相同的。
2 父对话框与弹出式子对话框的销毁
弹出式子对话框,指的是在父对话框面板中单击某一按钮,会另外弹出的一个对话框。如下图所示。为达到弹出的效果,子对话框的样式需设为 “Popup”11 。
若子对话框为非模态的,在create()时,第二个参数要赋值this,即指明父子对话框的关系:Create(IDD, this),若第二个参数为 NULL 的话,则不认为是父子对话框的关系。
这里要探讨的是,当子对话框打开时,直接销毁父对话框销毁,子对话框能否被自动销毁、是怎样销毁的。
2.1 父对话框为模态对话框
(1)子对话框为模态对话框
当父子对话框都为模态对话框时,此时无法通过点击父对话框右上角的叉号关闭父对话框;父对话框也不存在指针,无法通过类外调用关闭父对话框。
(2)子对话框为非模态对话框
当子对话框打开时,通过点击父对话框右上角的叉号关闭父对话框,父对话框是可以走完完整的销毁流程的,并且在父对话框的DestroyWindow()中调用CDialog::DestroyWindow() 时,会依次进入子对话框的OnDestroy()和OnNcDestroy()中,并且在OnNcDestroy()中调用了PostNcDestroy(),然后返回到父对话框的DestroyWindow()中,父对话框会继续把剩下的销毁流程走完。
若子对话框在PostNcDestroy()有delete this的操作,则也会进入子对话框的析构。此时对话框的销毁流程完整,认为子对话框也是正确销毁了。
即:如果非模态对话框已在类内增加了销毁相关的内容,通过点击父对话框右上角的叉号,是可以完整销毁父对话框和子对话框的。
销毁流程如下图所示。
2.2 父对话框为非模态对话框
(1)子对话框为模态对话框
当子对话框为模态对话框时,无法点击父对话框右上角的叉号。因此通过在子对话框中调用父对话框的DestroyWindow()的方法,尝试关闭父对话框,观察销毁流程。销毁流程如下图所示,可以发现,程序总是会从父对话框的DestroyWindow()返回时报错。
分析可知,当子对话框调用DestroyWindow()关闭父对话框时,父对话框也关闭了子对话框,在返回时已经找不到子对话框的对象,所以会报错。
因此,当父对话框为非模态、子对话框为模态时,通过调用DestroyWindow()关闭父对话框的操作是非法的。
(2)子对话框为非模态对话框
根据上面的分析,为了能够正确销毁两个非模态对话框,在父对话框和子对话框的OnCancel()中调用了DestroyWindow(),并在PostNcDestroy()中添加了delete this。
通过点击父对话框右上角的叉号关闭父对话框,观察关闭流程。销毁流程如下图所示,可认为父对话框和子对话框都是销毁完成的。
3 父对话框与嵌入式子对话框的销毁
嵌入式子对话框,指的是嵌入在父对话框中的子对话框。嵌入式子对话框只能是非模态的,否则没有意义。首先子对话框的样式需设为 “Child” 而非 “Popup”,然后在Create的时候,第二个参数需赋值this,即指明父对话框为当前。最后还要通过SetWindowPos()或是MoveWindow()函数将对话框移动到父对话框的固定位置,像控件一样在父对话框中展示。如下图所示。
创建嵌入式子对话框的代码示例如下:
1
2
3
4
5
6
7
8
9
10
// 如何创建一个嵌入式子对话框:
ChildInsideDlg* ptrDlg = new ChildInsideDlg;
ptrDlg->Create(IDD_DLG_CHILD_INSIDE_DLG, this);
CRect rect; // 获取控件的CRect信息
m_PlaceHolder.GetWindowRect(rect);
//m_PlaceHolder为CStatic类,对应父对话框中的Picture Control控件,属性设为Rectangle
::ScreenToClient(rect); // 把坐标改成局部坐标系
// 调整位置:
ptrDlg->SetWindowPos(NULL, rect.left, rect.top, rect.Width(), rect.Height(), NULL);
ptrDlg->ShowWindow(SW_SHOW); // 显示对话框
根据本文上面讨论过的内容,当对话框为非模态的时候,会在OnCancel()中调用DestroyWindow()、在PostNcDestroy()中添加delete this。
下面分别观察父对话框为模态和非模态时,对话框的销毁流程。
3.1 父对话框为模态对话框
点击父对话框右上角的叉号,可发现父对话框可以完整走完整个销毁流程,销毁流程如下图所示。与弹出式子对话框的销毁流程基本相同。唯一不同的是,嵌入式子对话框的销毁会在OnDestroy()之后调用,而非OnDestroy()之前。
3.2 父对话框为非模态对话框
父对话框为非模态对话框时的销毁流程如下图所示,同样与弹出式子对话框的销毁流程基本相同。不同的地方也是:子对话框的销毁会在OnDestroy()之后调用,而非OnDestroy()之前。
4 小结
不管子对话框为弹出式还是嵌入式,只要包含必要的销毁代码,通过合法的操作,关闭父对话框的同时,子对话框也能被正确销毁。
其中,“必要的销毁代码”指的是,无论是父对话框还是子对话框,当为非模态时,需要在OnCancel()中调用DestroyWindow()、在PostNcDestroy()中添加delete this。
“合法的操作”指的是,只关闭当前界面允许点击的对话框,即只通过点击对话框右上角叉号进行关闭操作。如果程序不允许点击父对话框的叉号(子对话框为模态时),再通过调用方法关闭父对话框的话,即为“不合法的操作”。
5 其他的一些问题
5.1 OnBnClickedCancel()和OnCancel()
在项目测试的时候,发现如果同时添加了OnBnClickedCancel()和OnCancel()两个函数,程序会先进入OnBnClickedCancel(),在OnBnClickedCancel()中进入CDialog::OnCancel()中,从而跳过了OnCancel()。换句话说,当有OnBnClickedCancel()的时候,该消息响应函数就会完全代替OnCancel(),不论是位置还是作用。
同样,OnBnClickedOk()和OnOK()也是一样的代替关系。
下面展示的是通过类向导添加的OnBnClickedCancel()和OnCancel()的相关代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Pop : public CDialog
{
protected:
DECLARE_MESSAGE_MAP()
protected:
afx_msg void OnBnClickedCancel();
virtual void OnCancel();
};
BEGIN_MESSAGE_MAP(Pop, CDialog)
ON_BN_CLICKED(IDCANCEL, &Pop::OnBnClickedCancel)
END_MESSAGE_MAP()
// 点击按钮“取消”
void Pop::OnBnClickedCancel()
{
// TODO: 在此添加控件通知处理程序代码
CDialog::OnCancel();
}
void Pop::OnCancel()
{
// TODO: 在此添加专用代码和/或调用基类
CDialog::OnCancel();
}
5.2 EndDialog()
在模式对话框下,要用到CDialog::OnOK()或CDialog::OnCancel()以及CDialog::OnClose()关闭对话框,一定要注意之前的CDialog::(当然,也可以重载自己对话框的这个虚函数,但是,后面必须调用基类的CDialog::)。
CDialog::OnOK()或CDialog::OnCancel()都会默认调用CDialog::EndDialog(),但是会传参不同,对应的函数调用也会不同,总结如下图:12
可以发现,CDialog::EndDialog(IDOK)与CDialog::EndDialog(IDCANCEL)的区别就是少了一个CDialog::UpdateData(),在此之后,都会调用DestroyWindow()来销毁对话框,后面的流程在本文的上半部分都介绍的很清楚了。也就是说话,CDialog::EndDialog(IDOK)在关闭对话框后,会把对话框上控件关联的变量的值更新为用户最后输入的值;而CDialog::EndDialog(IDCANCEL)就不会更新这个值。
为什么非模态对话框不能使用EndDialog()关闭窗口对话框呢,因为DoModal()函数,模态对话框是通过DoModal()函数创建的,而EndDialog()是用来结束DoModal()循环使用的,DoModal()内部结束后,会调用DestroyWindow()来销毁对话框。13 14
说的再详细一些,OnOK()和OnCancel()消息响应函数并没有调用DestroyWindow(),它们只是调用了EndDialog()跳出循环,并没有销毁窗库。
对于模态对话框,DoModal()函数自动调用DestroyWindow(),而对非模态对话框,我们若要使用OK或者Cancel按钮结束对话框,必须重写对应的虚函数或是消息响应函数以使其调用DestroyWindow()来销毁窗口。
6 PKPM程序调用Panel时的销毁
6.1 从施工图模块看Panel对话框的销毁:
在现在的 PKPM 程序中,会使用侧边停靠窗口(Panel),该窗口通过调用接口函数:RELOAD_MAIN_SUB(Itype, Nint, Ipara, Nfloat, Rpara, Nstr, Lstr, StrPara, n1),传入对话框名(CString 型)、窗口 ID(UINT 型)、对话框指针(int 型)等一系列参数,当Itype = -18时,为与 cfg 窗口等高的 Panel,Itype = -23时,为面积较小的miniPanel。
手动销毁 Panel 同样可以调用接口RELOAD_MAIN_SUB,与创建不同的是IPara[0]的值。当IPara[0]=1时,表示创建对话框;当IPara[0]=0时,表示要销毁对话框。
点击 Panel 右上角叉号的销毁方式,在创建伊始就已经决定了:
当RPara[3]不赋值时,点击 Panel 右上角的叉号,不会进入到子对话框的任何销毁函数中,指针也依然存在,内存中还保留着对话框的完整信息。此时认为对话框只是被隐藏。
当RPara[3]=1时,点击 Panel 右上角的叉号,跟随施工图模块中的调试进程,子对话框会先进到SetDialogFunc()中,把对话框的指针设为 NULL,然后依次调用了OnDestroy()和OnNcDestroy(),并在OnNcDestroy()调用了PostNcDestroy(),若在PostNcDestroy()添加了delete this 的话,也会进入析构函数。此时销毁流程完整,如下图所示。
分析此时的销毁流程,与上文中在父对话框调用DestroyWindow()进行销毁的流程完全一致。可以猜测是主框架调用了DestroyWindow()对主对话框进行销毁,销毁主对话框的同时,也对子对话框进行了销毁。
手动调用RELOAD_MAIN_SUB销毁 miniPanel 时的销毁流程与上面一致,同样猜测是主框架调用了DestroyWindow()对模块内的子对话框进行销毁。
6.2 从 PKPMMain 看 Panel 对话框的销毁:
查阅PKPMMain的代码,发现不管是 Panel 还是 miniPanel 都是继承于 CDockablePane类,查阅资料,该类没有直接提供销毁的函数15。但是可以通过给窗口发送 WM_CLOSE 的消息来关闭对话框16。PKPMMain 中 Panel 和miniPanel 的相关代码,发现确实是通过发送了 WM_CLOSE 来关闭对话框的,如下面两张截图所示。
miniPanel 中:
Panel 中:
跟随进程调试,发现发送WM_CLOSE的消息之后,会进入基类的OnClose中(但是我并没有在微软CDockablePane类说明中发现这个函数的相关内容),如下图所示:
在黄区跟随调试的时候,DestroyWindow()可以跟进去,如下图所示。但是我在外网的测试工程下,不管是F12 还是跟随调试的时候 F11,都会报错,进不去DestroyWindow(),猜测可能是因为外网的VS环境配置不全。
可以发现,DestroyWindow()其实是 CWnd 下的虚函数。
6.3 对比 CDockablePane 类和 CDialogEx 类的销毁
跟随普通对话框的销毁流程,这里以施工图-梁模块中的悬停 Tips 对话框为例,按照本文第一节的描述,已在OnCancel中添加了DestroyWindow,如下图所示:
调试时 F11 进入,同样也会进入到CWnd::DestroyWindow()中。
CDockablePane 类的继承关系为:CDockablePane : CPane : CBasePan : CWnd。
CDialogEx类的继承关系为:CDialogEx : CDialog : CWnd。
可以发现,这两个类能追溯至一个共同的基类:CWnd。
上面跟随CDockablePane和CDialogEx两个类的销毁流程同样也可以发现,不管是CDockablePane还是CDialogEx的销毁,其实都是调用的CWnd::DestroyWindow()。可以理解为DestroyWindow()就是CWnd下专门负责销毁的函数。换而言之,只要是进入到了CWnd::DestroyWindow()中,那么在此之后的销毁流程就都是一样的,也就验证了 6.1 中的推测,总结为:在 Panel 或 miniPanel 中直接点击右上角的叉号,发送WM_CLOSE的消息关闭对话框,子对话框中只要销毁流程完善,父对话框和子对话框就都会被合理销毁掉。
7 对话框销毁相关的实际问题汇总
对话框重复销毁
总结这个问题的时候,才发现其实我碰到过的三个问题其实都是因为对话框重复销毁造成的,场景不同但是问题原因都是这一个。也证明了理清对话框的销毁流程是十分有意义的。
(1)调用 PKPMMain 创建的 Panel 在销毁时报错
这个问题其实就是促使这篇文章产生的诱因。
在做研发任务的时候,调整了侧边停靠 Panel 的关闭逻辑,不仅修改了创建时调用RELOAD_MAIN_SUB传入的参数RPara[3]为 1,还新增了消息CMT_CLOSE_DOCKDLG_CAP的映射函数Cmd_ClosePanel,该函数可以接收到点击 Panel 右上角叉号的消息,分析该消息内容,判断对话框名称是否符合,关闭所需的对话框。
但是根据 6.1 可知,当RPara[3]=1时,Panel 对话框通过点击右上角的叉号就是有完整的销毁流程的,此时若再通过消息映射函数Cmd_ClosePanel再关闭一次对话框,便会报错,事实也确实报错了。
解决方法是去掉了消息映射函数Cmd_ClosePanel中关闭对话框相关的代码,保证对话框自身的销毁流程完整就可以了。
(2)鼠标悬停 Tips 改为通过发送 WM_CLOSE 后销毁报错
鼠标悬停 Tips 就是指的鼠标悬停在梁跨上的时候,弹出来的包含配筋信息的那个 Tips,该 Tips 都是非模态对话框,一开始都是通过外部调用DestroyWindow直接销毁的,后面交流之后认为用PostMessage发送WM_CLOSE消息来销毁更好。(使鼠标钩子函数继续往下走,与业务代码相关)
于是增加了消息映射函数OnClose和虚函数OnCancel,并在OnCancel中增加了DestroyWindow,如下图所示:
程序执行到上图的红色剪头处会报错,猜测是DestroyWindow中已经完整的销毁了对话框的资源和窗口,所以当CDialogEx::OnCancel中试图销毁对话框的资源或窗口时,便会报错,具体可参考本文 1.2。
修改方法是去掉CDialogEx::OnCancel:
(3)鼠标悬停 Tips 有两个及以上时销毁报错
在完成鼠标悬停 Tips 的时候,曾经遇到过这个问题,就是有一个 Tip 的时候,不会报错,但是有两个及以上 Tips 的时候,就会在销毁的时候报错,所示情况如下图:
报错位置为向对话框 2 发送DestroyWindow消息处,跟随调试发现,当试图向对话框 2 发送DestroyWindow时,该对话框的指针已经为 NULL了,因此报错。找到了对话框 2 的实际销毁位置,发现是在对话框 1 销毁时的DestroyWindow和OnDestroy之间,依次进入到了对话框 2 的OnDestroy和OnNcDestroy中,根据本文 3.2 总结的流程可以发现,这是父子对话框的销毁流程,推测对话框 2 为对话框 1 的子对话框,因此在销毁 1 的时候,已经把 2 完全销毁了,这时若再想通过DestroyWindow来销毁 2,就会重复销毁,并报错。
Tips 在 Create 的时候并没有传父对话框的指针,推测可能是当没有传父对话框指针式,就默认上一个对话框为父对话框,所以才会有这样奇怪的嵌套关系。
修改方法,Tips 在 Creat 的时候,取 PKPMMain 的窗口指针作为父对话框指针传进去,便不会再出现上述的问题了。
8 有待完善的问题
(1)PKPMMain中侧边停靠是用的CDockablePane创建的对话框,该对话框网上资料很少,深入了解的话还需要多查阅一些资料。
(2)鼠标钩子如果用全局钩子可以完成更多好玩的事情,比如说鼠标/键盘宏,对话框销毁的内容整理完之后就开始着手写一个全局钩子,并整理钩子相关的内容。
(3)OnOK中也需要添加DestroyWindow() ?!
参考文献1718
-
MiTu_-_. CSDN. 让你清楚模式(DialogBoxParam)与非模式(CreateDialogParam)对话框的区别[DB/OL]. (2020-04-25). https://blog.csdn.net/Martin0316/article/details/105753803 ↩︎ ↩︎2
-
woswod. CSDN. MFC中的DestroyWindow详解[DB/OL]. (2017-02-14) https://blog.csdn.net/woswod/article/details/63253315 ↩︎
-
Microsoft. Microsoft. DestroyWindow function (winuser.h) [DB/OL]. (2022-02-24). https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-destroywindow ↩︎
-
hideforever. CSDN. 模态与非模态对话框在实现原理上的区别 [DB/OL]. (2016-05-06) https://blog.csdn.net/hideforever/article/details/51328717 ↩︎
-
Strive–顾. CSDN. MFC——模态对话框和非模态对话框区别 [DB/OL]. (2017-09-09) https://blog.csdn.net/perfectguyipeng/article/details/77916091 ↩︎
-
jinlking. CSDN. MFC下的模态与非模态对话框[DB/OL]. (2008-12-30). https://blog.csdn.net/jinlking/article/details/3657246 ↩︎
-
Microsoft. Microsoft. CDialog::OnCancel function [DB/OL]. (2006-07-12). https://docs.microsoft.com/en-us/previous-versions/visualstudio/visual-studio-6.0/aa279051(v=vs.60)?redirectedfrom=MSDN ↩︎ ↩︎2
-
齐鲁至滇. CSDN.非模态对话框的创建与删除[DB/OL]. (2010-02-06). https://blog.csdn.net/xin_yu_xin/article/details/5295028 ↩︎
-
Microsoft. Microsoft. CWnd::PostNcDestroy function (winuser.h) [DB/OL]. (2007-12-31). https://docs.microsoft.com/zh-cn/previous-versions/visualstudio/visual-studio-2008/sk933a19(v=vs.90) ↩︎
-
360doc. 安全窗口GetSafeHwnd详细解说 [DB/OL]. (2017-08-23). http://www.360doc.com/content/17/0823/10/1411057_681440477.shtml ↩︎
-
迪迦·奥特曼. CSDN. MFC窗口style为overlapped popup child三者的区别[DB/OL]. (2020-02-21). https://blog.csdn.net/qwq1503/article/details/104433436 ↩︎
-
OnTheWay_Boy. CSDN帖子. 关于模态对话框的销毁 [DB/OL]. (2016-11-14). https://bbs.csdn.net/topics/392047903?sharesource=qq_34649825&spm=1001.2014.3001.5506 ↩︎
-
米乐-miller. CSDN. 窗口关闭过程,非模态对话框为什么不能使用enddialog关闭窗口 [DB/OL]. (2013-01-15). https://blog.csdn.net/hanyujianke/article/details/8507064 ↩︎
-
Microsoft. Microsoft. EndDialog function (winuser.h) [DB/OL]. (2021-10-13). https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-enddialog ↩︎
-
Microsoft. Microsoft. CDockablePane选件类 [DB/OL]. (2013-03-01). https://docs.microsoft.com/zh-cn/previous-versions/visualstudio/visual-studio-2012/bb984433(v=vs.110) ↩︎
-
dxy408460910. CSDN. VS2010 关于CDockablePane 关闭事件 [DB/OL]. (2013-08-14). https://blog.csdn.net/dxy408460910/article/details/9965561 ↩︎
-
菜鸟开车. 博客园. 句柄的意义和作用以及句柄和指针的区别[DB/OL]. (2017-02-14) https://www.cnblogs.com/luckylihuizhou/p/6398784.html ↩︎
-
Microsoft. Microsoft. CWnd::OnNcDestroy function (winuser.h) [DB/OL]. (2007-12-13). https://docs.microsoft.com/zh-cn/previous-versions/visualstudio/visual-studio-2008/2zw820e5(v=vs.90) ↩︎




















