DWinLib 6:漂亮的 WinAPI 集成






4.94/5 (27投票s)
我的 DWinLib Windows API 包装器与 Francisco Campos 的 Pretty WinAPI 框架相结合
上次修订摘要
2021年1月16日:DWinLib
6.04:在我的MIDI排序程序中添加简单的双耳节拍创建机制时进行了大量更改。清理了代码,在回调机制中添加了lambda函数,改进了菜单,大大简化了错误字符串单元,添加了可以自定义颜色的滚动条(不包含在DWinLib目录中,但可以在fractalBrowser示例中找到),并对dwl::ControlWin
控件进行了大修,使其都使用相同的基类winProc
,这大大简化了它们,并可以轻松添加事件。 (例如,请参阅EditBoxBase::wKillFocus
中如何使用onKillFocusC
。)将DC
等swc
项放入全局命名空间,以减少函数签名中的按键次数。
目录
- 引言
- 关于项目构建和布局的说明
- DWinLib与SWC的方法差异
- 错误处理、全局变量和设计
- SWC的改进
- GDI对象
- 趣味内容
- 学到的Visual Studio技巧
- 待办事项
- 使用技巧
- 结束语
- DWinLib替代方案
- 历史
引言
在过去的八个月里,我很有幸能够重新审视DWinLib
,这是我多年前开始开发的Windows包装库。与其他包装库一样,DWinLib
使编写独立的Windows程序比通过纯粹的Window API调用来创建它们更加愉快。
现在有许多可用的包装库,即使这个主题现在有点老派,我也认为我会更新这篇文章和我其他的DWinLib
文章,使它们成为一个连贯的整体,供任何对深入研究这种类型的东西感到好奇的人参考。此外,DWinLib
的一些方面可能也会让你感兴趣。
为了提供一些背景信息,我很久以前就开始认真使用Borland Builder 4.0进行编码,当我的项目崩溃时,我花了许多天(几周?)来弄清楚为什么它会遭受残酷的死亡。即使没有这个症状,BCB也经常在不同的情况下崩溃,这让我非常困惑。最终,我知道我必须改变,由于当时Visual Studio Express是免费的,所以我选择了它。我没有尝试学习MFC或其他框架,而是决定学习基础知识,以确保我不再需要猜测是我自己做错了什么,还是根本问题出在别人的代码上。
在此过程中,我创建了一些我喜欢的东西。例如,我在DWinLib
中非常喜欢的一点是,永远不必考虑控件ID——它们被封装起来,看不见,甚至不需要用于头文件常量声明。
我另一个感到满意的方面是窗口调整大小时的抖动大大减少。由于DWinLib
包含了一个停靠框架,与其他人都有经验的其他方法相比,这是一个很大的进步。(例如,尝试下面的示例项目中的DwlDockWork
项目,从左侧调整窗口大小,并注意带有停靠器(在左侧或右侧)的窗口调整大小是多么平滑。然后与Visual Studio进行比较,它非常不稳定。即使是我更苛刻的项目也超级平滑。)
但我的原始停靠框架有一个问题:在Windows 7中,当停靠器未停靠并被拖动时,它无法很好地绘制。
在浏览CodeProject时,我偶然发现了Francisco Campos的Pretty WinAPI Class文章。它有一个我喜欢的停靠框架,所以我决定看看是否可以将其以及其他最好的功能集成到DWinLib
中。之前的下载就是这样,本文的其余部分将描述DWinLib
的现状。
总结您可能对将DWinLib
用于项目感兴趣的原因
DWinLib
可免费用于非GPL程序。- 它是一个非常轻量级的包装库,可能会让您熟悉比其他一些包装库更多的Windows API的方面。一旦您掌握了它,这种熟悉感会很有趣。
- 它比许多其他包装库更容易处理控件。
- 在
DWinLib
中,笔、位图、字体和画笔更容易使用。 - 它设计干净,正确使用命名空间,并且不使用匈牙利命名法。
- 代码库包含了主菜单系统、最近使用的文件以及MDI和SDI应用程序所需的一切。它甚至包含了最小内存占用的撤销系统的框架。
- 包含两个不同的停靠框架,由于使用的命名空间方法,添加更多框架很简单。
dwl
停靠框架比我体验过的其他框架更流畅。
下载次数
(SwcRebarTest
项目(标题为“DWinLib Test
”)包含在内,供任何希望挑战自己的人使用。6.03代码版本运行正常,尽管代码中有一个我从未有时间修复的错误,因为我自己的工作中从未用过rebar。当前代码可以编译,但存在一个我从未深入研究过的新错误。如果您需要rebar控件,也许代码可以帮助您开始,但rebar类可以从头开始重写。)
除了这些示例之外,我还重构了Francisco的许多代码,您可能会发现它很有用
关于项目构建和布局的说明
示例zip文件使用的目录结构比我发现的任何其他方法都能更有效地减少库和包含文件的搜索时间。我的“Programs”子目录包含编写代码所需的一切,并且布置如下面的Explorer截图所示。它还包含一个“MyProgs”目录,“OthersProgs”目录和一个“TheoryAndExamples”目录,以保持事物易于理解的系统。如果您是编程新手,我希望这种安排能帮助您更快地专注于编码,而花费更少的时间来组织一切。
使用库
所有示例程序都已设置为使用库,在大多数情况下可以加快编译速度。已创建两个库:MDI和SDI。它们位于LibsAndUtils/DWinLibLibrary目录中,如果您打开主项目(不在该目录中,而是位于DWinLibExamples的子目录中),则由于使用了项目依赖项并指定了相对路径,库应该会自动编译。(zip文件中的库项目不包含已编译的库,因为每个库大约7到13 MB,并且没有必要进行如此大的下载,因为Visual Studio会创建它们。)
所有项目都已针对Unicode编译,因为Multi-Byte已被Microsoft弃用,但您可以根据需要创建Multi-Byte库,并且DWinLib
可以正常工作。只需遵循现有库的示例,然后将“属性 - >配置属性 - >通用 - >字符集”更改为“使用多字节字符集”。当然,这也意味着您需要更改链接到该库的主项目以使用相同的字符集。
我应该提到,将项目链接到库会使得修改项目以使用其他设置变得更加困难。逐步执行一个场景将为您提供所有需要内容的想法。
目前,SdiNoDocks
项目是唯一使用SDI界面的项目。要将其更改为MDI,请执行以下步骤:
- 从解决方案中删除
DWL_SDI_Unicode
项目。 - 通过选择“文件 -> 打开 -> 项目/解决方案…”将
MDI_Unicode
项目添加到解决方案中,并确保在“打开项目”对话框中选择了“添加到解决方案”单选按钮。 - 在Visual Studio中配置
SdiNoDocks
项目时,将配置属性 - >C/C++ - >预处理器中的“预处理器定义”从“DWL_SDI_APP”更改为“DWL_MDI_APP”。对Dwl_SDI_Unicode项目也执行相同的操作。 - 在解决方案资源管理器中右键单击“SdiNoDocks”项目,然后选择“属性”。然后选择“配置属性 - >链接器 - >通用”,并将Debug附加库目录更改为“..\..\..\LibsAndUtils\DWinLibLibrary\MDI_Unicode\Debug;%(AdditionalLibraryDirectories)”,并将Release版本更改为“..\..\..\LibsAndUtils\DWinLibLibrary\MDI_Unicode\Release;%(AdditionalLibraryDirectories)”。
- 在解决方案资源管理器中右键单击顶部的“解决方案‘SdiNoDocks’(2个项目)”行,然后通过选择“公共属性 - >项目依赖项”并单击“依赖项:”部分中的复选框,使
SdiNoDocks
依赖于DWL_MDI_Unicode
库项目。 - 在SdiNoDocks的“配置属性 - >链接器 - >输入”属性页中,将所有配置的附加依赖项更改为“DWL_MDI_Unicode.lib”(因此该行应为“
DWL_MDI_Unicode.lib;%(AdditionalDependencies)
”。
如果您执行程序,一切都会重新构建,完成后,程序将作为MDI应用程序打开。它不是世界上最简单的例程,但一旦您掌握了相关的过程,Visual Studio的另一个级别的实用性就会向您开放。
关于构建DWinLib
项目的最后一件事。我无法在新的默认Visual Studio“Conformance mode”设置(截至2017年)(属性 - >配置属性 - >C/C++ - >语言 - >Conformance mode)中编译任何项目。主要原因是DWinLib
允许您使用MCBS
或Unicode
,并且有许多传递TCHAR*
的实例。在这些位置,Visual Studio会抱怨无法转换为wchar_t*
,尤其是在动态传递string
作为数组时。因此,Conformance mode已改回“No”。
方法差异
在开始之前,如果您想熟悉DWinLib
的核心设计,它已在此处概述。本文将重点介绍DWinLib
和SWC的修订版如何协同工作,这并非易事。(“SWC
”是Campos为其库起的名称,尽管他在其他地方也提到了“PWC
”。)Francisco曾说:“我不保证它写得很好”,我对此没有异议。他的框架似乎与早期Windows编程有着悠久的历史,并且命名风格(以及许多地方的缺失)——以及代码中到处使用魔术数字和其他项——造成了一些困惑。但尽管存在这些问题,他的成就的范围是惊人的。我花了两个月的时间,可能更长,才完成了上述结果。我怀疑他本人花费了超过一年,我非常感谢他的毅力以及让他公开分享的意愿。
如果您想深入研究SWC本身,并避免一些困难,我在上面的zip文件中包含了Francisco的PwcStudio
的重构版本。我发现最好重命名类和变量,以便在查找错误时可以在两个框架中逐步执行等效代码,这就是结果。(我还将许多实现移到了正确的.cpp文件中——Franciscos的代码的这方面非常令人讨厌。)我的记忆可能不准确,但感觉我花了一周时间重构,使类名和变量名更好地描述了它们的意图。这还不包括SWC中的所有内容——只是我需要理解才能完成工作的内容!
当我最终理解Campos的方法背后的机制时,我震惊地发现,似乎所有窗口消息都通过SwcBaseWin::WndProc
(或重构前的CWin::WinProc
)路由。我的意思是,用户创建的窗口和通用控件都在那里处理,尽管它们以我从未深入研究过的方式调用了原始过程。
我保留了我现有的方法,而不是采用Francisco的方法。通用控件是从一个基类派生的,该基类有自己的窗口过程,与主应用程序窗口过程无关。这种分离在逻辑上反映了Microsoft自己处理这些窗口过程的方式,因此如果您遵循类关系,应该会更容易理解。(这可能也解释了我的WindowProc
与他的相比很简短,如下面所示。)
为了说明这带来的编码差异,这里是Francisco的窗口过程的复制粘贴。我不知道所有分支是否真的需要,而且反向工程其逻辑和原因不是我乐于做的。
static LRESULT CALLBACK WndProc(HWND hWnd, UINT uID, WPARAM wParam, LPARAM lParam)
{
CWin* pWnd=NULL;
BOOL bClose=FALSE;
BOOL bResult=FALSE;
LRESULT lResult=0;
if( uID == WM_INITDIALOG ) //is a dialog window
{
if (pWnd== NULL)
{
pWnd =reinterpret_cast<CWin*>(lParam);
::SetWindowLong(hWnd,GWL_USERDATA,reinterpret_cast<long> (pWnd));
pWnd->SethWnd(hWnd);
}
}else if( uID == WM_NCCREATE) //is a normal windows
{
pWnd =reinterpret_cast<CWin*> ((long)((LPCREATESTRUCT)lParam)->lpCreateParams);
BOOL res=pWnd->IsMDI();
if (res == 0)
{
pWnd =reinterpret_cast<CWin*> ((long)((LPCREATESTRUCT)lParam)->lpCreateParams);
}
else
{
LPMDICREATESTRUCT pmcs = ( LPMDICREATESTRUCT )(( LPCREATESTRUCT )lParam )->
lpCreateParams;
pWnd =reinterpret_cast<CWin*>(pmcs->lParam);
pWnd->SethWnd(hWnd);
}
::SetWindowLong(hWnd,GWL_USERDATA,reinterpret_cast<long>(pWnd));
pWnd->SethWnd(hWnd);
}
pWnd= reinterpret_cast<CWin*>(::GetWindowLong(hWnd,GWL_USERDATA));
if (pWnd!=NULL)
pWnd->SaveMsg(hWnd,uID, wParam,lParam); //save the actually message, the idea is if
//you need to call the default message
//[Default()]
if (HIWORD(pWnd))
{
if(uID == WM_COMMAND)
{
CWin* pChild=reinterpret_cast<CWin*>((HWND)
::GetWindowLong(pWnd->GetDlgItem( LOWORD(wParam)),
GWL_USERDATA) );
if (HIWORD(pChild))
{
int x=HIWORD(wParam);
if (x == CBN_EDITCHANGE )
pChild->OnCbnEditChange();
if (x == CBN_KILLFOCUS )
pChild->OnCbnKillFocus();
if (x == CBN_EDITUPDATE )
pChild->OnCbnEditUpdate();
if (x == CBN_CLOSEUP )
pChild->OnCbnCloseUp();
if (x == CBN_SELENDOK )
pChild->OnCbnSelendOk();
if (x == CBN_SELENDCANCEL )
pChild->OnCbnSelendCandel();
if (x == CBN_SELCHANGE )
pChild->OnCbnSelChange();
if (x == CBN_SETFOCUS )
pChild->OnCbnSetFocus();
if (x == CBN_DROPDOWN )
pChild->OnCbnDropDown();
}
else
pWnd->OnCommand(wParam,lParam);
}
else if( uID == WM_DESTROY)
{
if(IsWindow(pWnd->GetSafeHwnd()) )
pWnd->OnDestroy();
return 0;
}
else if (uID == WM_NCDESTROY)
return 0;
else if(uID == WM_CLOSE)
{
bClose=TRUE;
lResult=pWnd->OnClose();
}
else if(uID == WM_COMPAREITEM )
{
if(wParam != 0)
{
CWin* pChild=reinterpret_cast<CWin*>((HWND)
::GetWindowLong(pWnd->GetDlgItem( ((( LPDRAWITEMSTRUCT )lParam)->CtlID) ),
GWL_USERDATA) );
bResult=pChild->OnCompareItem((LPCOMPAREITEMSTRUCT) lParam );
if(bResult && pWnd->IsDialog())
return ::SetWindowLong(pWnd->GetSafeHwnd(), DWL_MSGRESULT, ( LONG )bResult);
}
}
else if( uID == WM_MEASUREITEM)
{
if(wParam != 0)
{
CWin* pChild=reinterpret_cast<CWin*>((HWND)
::GetWindowLong(pWnd->GetDlgItem((((LPMEASUREITEMSTRUCT)lParam)->CtlID)),
GWL_USERDATA) );
bResult=pChild->OnMeasureItem((LPMEASUREITEMSTRUCT) lParam );
if(bResult && pWnd->IsDialog())
::SetWindowLong(pWnd->GetSafeHwnd(), DWL_MSGRESULT, ( LONG )bResult);
}
}
else if (uID == WM_DRAWITEM)
{
//el problema con estos mensajes es que nunca llegan al control directamente,
//inicialmente el mensaje se envia al propietario del control,luego es labor
//nuestra enrutarlo desde aqui a quien debe manejarlo.
//
if(wParam != 0)
{
CWin* pChild=reinterpret_cast<CWin*>((HWND)
::GetWindowLong(pWnd->GetDlgItem( ((( LPDRAWITEMSTRUCT )lParam)->CtlID) ),
GWL_USERDATA) );
bResult=pChild->OnDrawItem((LPDRAWITEMSTRUCT) lParam );
if(bResult && pWnd->IsDialog())
::SetWindowLong(pWnd->GetSafeHwnd(), DWL_MSGRESULT, ( LONG )bResult);
}
}
else if(uID == WM_NOTIFY)
{
LPNMHDR pNMHDR = ( LPNMHDR )lParam;
CWin* pChild=reinterpret_cast<CWin*>((HWND)
::GetWindowLong(pNMHDR->hwndFrom,
GWL_USERDATA) );
if ( pChild )
{
BOOL bNotify=TRUE;
bResult = pChild->ReflectChildNotify( pNMHDR, bNotify);
if ( pWnd->IsDialog())
::SetWindowLong(pWnd->GetSafeHwnd(), DWL_MSGRESULT, ( LONG )bResult);
if(bNotify)
pWnd->OnNotify(wParam,pNMHDR);
if (bResult != 0)
return bResult;
}
}
if( pWnd->IsDialog())
{
if (bClose)
return 0;
bResult=pWnd->NewMsgProc(hWnd,uID,wParam,lParam,lResult);
if(!bResult)
return pWnd->DefWindowProc(pWnd->GetSafeHwnd(),uID,wParam,lParam);
return 0;
}
else
{
if(bClose )
{
if(lResult)
return pWnd->DefWindowProc(pWnd->GetSafeHwnd(),uID,wParam,lParam);
return 0;
}
bResult=pWnd->NewMsgProc(hWnd,uID,wParam,lParam,lResult);
if(!bResult)
return pWnd->DefWindowProc(hWnd,uID,wParam,lParam);
if (pWnd->IsMDI() )
return pWnd->DefWindowProc(hWnd,uID,wParam,lParam);
}
}
return lResult;
}
与下面的代码进行比较
LRESULT CALLBACK dwl::Application::winProc(HWND window, UINT msg, WPARAM wParam,
LPARAM lParam) {
try {
BaseWin * win(nullptr);
{ //Scope the iterator to eliminate the possibility that the windowproc
//deletes the window and causes the iterator to become invalid,
//which will crash the program
auto it = gDwlApp->windowsC.find(window);
if (it != gDwlApp->windowsC.end()) win = it->second;
}
if (win) return win->winProc(window, msg, wParam, lParam);
else {
BaseWin * tempWin = static_cast<BaseWin*>(TlsGetValue(gDwlApp->tlsIndexC));
gDwlGlobals->dwlApp->windowsC.insert(std::make_pair(window, tempWin));
//#ifdef DWL_DO_LOGGING
// wStringStream str;
// str << _T(" Adding window to map. Window HWND: ") << window <<
// _T(" BaseWin*: ") << tempWin<< _T(" MSG: ") << msg;
// gLogger->log(str);
// #endif
return tempWin->winProc(window, msg, wParam, lParam);
}
}
catch (Exception & e) {
wString str = dwl::strings::msgProgramming() + e.strC;
str += dwl::strings::msgPleaseReport();
if (e.continuableC == Continuable::True)
str += dwl::strings::msgWillAttemptContinue();
else str += dwl::strings::msgProgramMustExit();
MessageBox(gDwlGlobals->dwlMainWin->hwnd(), str.c_str(),
dwl::strings::msgError().c_str(), MB_OK);
if (e.continuableC == Continuable::False) exit(EXIT_FAILURE);
}
catch (std::exception & e) {
wString str = dwl::strings::msgPleaseReport();
str += _T("\r\n");
str += dwl::strings::stdException();
str += _T("\r\nError: ");
str += utils::strings::convertToApiString(e.what());
str += _T("\r\n");
str += dwl::strings::stdExceptionAbortQuery();
int wish = MessageBox(gDwlGlobals->dwlMainWin->hwnd(),
str.c_str(), dwl::strings::msgError().c_str(), MB_YESNO);
if (wish == IDYES) exit(EXIT_FAILURE);
}
catch (...) {
wString str = dwl::strings::msgUnknownException();
str += dwl::lastSysError();
str += dwl::strings::msgUnknownExceptionAbortQuery();
int wish = MessageBox(gDwlGlobals->dwlMainWin->hwnd(), str.c_str(),
dwl::strings::msgError().c_str(), MB_YESNO);
if (wish == IDYES) exit(EXIT_FAILURE);
} return -1;
}
如您所见,DWinLib
的方法不需要任何逻辑检查——只需在应用程序映射中找到与HWND
对应的窗口,并将消息发送到接收器。如果不存在,则将其添加到映射中。即使它包含SWC中不包含的用于异常处理的代码,该函数仍然比Campos的短。
(在6.04概述中,我提到错误字符串单元已被大大简化。以前,异常wString
被保存在一个使用map
和enum
作为基础的类中。用纯static
函数替换它们是一次值得进行的重构。)
另一个值得一提的是,使用线程本地存储来临时存储窗口类比SWC使用的GetWindowLong/SetWindowLong
方法更安全。DWinLib
曾经使用GetWindowLong/SetWindowLong
,我必须感谢David Nash,感谢他在很久以前在DWinLib
以及其他方面的贡献。
错误处理、全局变量和设计
说到异常处理,我想提一个在6.03版本中我学到了很多并想提一下的主题。由于Campos的代码和我注意到DWinLib
早期版本的一个问题,DWinLib
实现了一个重大更改。即使有异常处理,在程序崩溃期间,处理器也从未被正确触发。
原因是DWinLib
在构造函数中创建了所有内容。我几乎忘记了问题的逐步原因,但我很确定初始异常在堆栈展开过程中导致了更多异常的抛出,而额外的错误破坏了我的方法。
SWC似乎是基于MFC的,并采用两步方式进行窗口创建。在其中,构造函数做的很少,如果有什么可能抛出异常的话。它们主要用于变量初始化。窗口创建在构造函数完成任务之后处理。换句话说,创建看起来像这样:
Window * win = new Window(/*args*/);
win->instantiate(/*other args*/);
有了这个,而不是所有东西都在new
中完成,异常处理器就能正确工作。您可以看到我的方法会向用户显示异常文本(如果未在代码中处理),但您可以轻松地将其修改为日志记录系统。
关于异常,值得注意的是,在主窗口完全构造之前,所有异常都是不可继续的。这是因为应用程序的run()
循环直到该点才进入,而且我找不到简单的方法将MainAppWin::instantiate
绑定到该例程。
这引出了一个问题,即使用WM_CREATE
(在DWinLib
中由wCreate
处理)。在纯API程序中,窗口的子窗口通常在WM_CREATE
处理程序中创建。然后检查LRESULT
是否有错误,并以这种方式处理。
DWinLib
在内部广泛使用instantiate
方法。但是,如果您愿意,您可以使用wCreate
处理程序,并以更“直接API”的方式进行操作。请记住,DWinLib
程序的run()
过程在主窗口完全构造之前是无效的。
有时,有必要将一些代码放在您通常会与instantiate
方法关联的wCreate
处理程序中。如果创建的窗口或资源依赖于创建窗口的HWND
,并且有依赖于该HWND
的wPaint
或其他处理程序,则需要在wCreate
例程中构造它,以强制其在paint处理程序被调用之前发生。
我不认为任何示例都显示了这种设计约束。在它们中,所有子窗口都在instantiate
方法中创建,并通过异常处理错误。由于异常不应该发生,并且主程序不应该启动失败,这在逻辑上是合理的,即使它从错误处理的角度来看似乎是违反直觉的。您可以将所有内容改回wCreate
和错误代码而不是异常,因为zip文件包含所有源代码,但这将是一项非常繁重的工作,收益甚微,甚至没有。
在上面,我在6.04版本的错误string
注释中括号括出了关于存储那些string
s的丑陋方法已被消除的内容。在此注释的基础上,我认为最初的想法是使用enum
和相关的蠢事,以便将来可以轻松地将所有错误字符串存储在一个DLL中,从而简化国际化。将它们转换为DLL仍然很容易——只需将DwlStrings
单元转换为DLL,并通过在该单元中查找/替换来更改函数签名。为了让您了解涉及的工作量,这是DwlStrings.h文件的一个小片段:
namespace dwl {
namespace strings {
static wString buttonCreationFailure() { return _T("Button: Creation failure"); }
static wString checkBoxCreationFailure() { return _T("CheckBox: Creation failure"); }
...
...
(如果您想扩展此方法并使您的可执行文件更难被反向工程,您可能还对Michael Haephrati的String Obfuscation System感兴趣。)
我读过一些关于异常和断言的意见,并得出结论,异常是应该很少发生的事情。我见过的代码,包括SWC,到处都 liberal 地使用断言,除此之外别无他物。(SWC的断言在展开时存在一个错误,因此它们不像假定那样工作,我认为这很有趣。)
因此,当在生产模式下禁用断言时,堆栈转储是调试时仅有的工具。基于这个想法,我用throw
替换了断言,以提供更快的线索来确定根本问题的根源。当我调试一个例程时,我有时会使用断言来将VS带到正确的行,但这似乎是它们唯一有用的时间。(正确的Visual Studio断言是_ASSERT
,它不需要包含头文件。)
为了更好地与Windows交互,我的Exception
类包含一个std::wstring
或std::string
,具体取决于UNICODE
宏。它还包含一个continuableC
enum
变量,用于指示在发生较小错误时程序是否可以继续运行。(当然,是在实例化完成后。在此之前,您可以指定Continuable::True
,但程序仍会终止。)
至于全局变量本身,我并不像有些人那样反对它们,但我会谨慎使用它们。DWinLib
本身包含一个全局单元,其中包含指向MainWin
和Application
的指针,并且还负责创建和销毁Windows对话框的包装器。我开发的MIDI程序在另一个命名空间中有一个自己的全局单元,其处理方式如我在“Two-thirds of a pimpl and a grin”中所述。
(如果您是编程新手,避免这种全局变量的一个原因是因为当您的代码库变大时,对全局变量的调用很可能会导致缓存未命中,并花费更多时间。此外,一旦您通过了特定数量的全局变量,您的代码就会变得非常丑陋,并且容易出现全局实例化顺序问题。在我的String
例子中,一旦遇到异常,时间就不是最担心的问题了。我在前面提到的Two-thirds of a pimpl and a grin文章中讨论了如何克服实例化问题,而缓存未命中是全局变量的组织性功能需要付出的微小代价。只是要注意,您可能不想在可能成为瓶颈的高调用例程中使用全局变量。)
我还保留了DWinLib
的其余大部分内容,因为SWC的方法对我来说并不直观。例如,CMsg
是SWC中CWin
使用的基类。CMsg
封装了窗口过程。“CWin
派生自CMsg
”的说法听起来不合逻辑。(并且考虑到CWin
内部有一个WndProc
,这会带来一些困惑!)
在DWinLib
中,应用程序“拥有”一个消息过程;它不“派生”自消息过程。该过程将消息路由到窗口本身的相应处理程序。没有任何东西“派生”自窗口过程!而Campos的窗口过程是一个长的宏,在MinGW上明显不起作用。我相信(但尚未测试)DWinLib
使用的虚拟路由在该平台上会正常工作。(虚拟方法在我之前的文章:DWinLib - The Guts中已概述。)
另一个区别是,我稍微重构了DWinLib
的类层次结构。它不再包含以前的DwlMdiFrame
。当在PrecompiledHeader.h文件中定义了DWL_MDI_APP
时,BaseWin
会自动成为MDI容器。如示例所示,主窗口中的某些项需要根据您的程序是MDI还是SDI进行更改。
最后一点是DWinLib
现在在代码中使用了命名空间,以简化事物,并允许您更自由地使用名称。您想拥有一个名为“Object
”的类吗?您可以在全局命名空间级别或我的巧妙命名的dwl
命名空间之外的任何命名空间中这样做。我还将Francisco的许多代码放入了swc
命名空间,以提醒他曾做出贡献。我不能保证他的某些代码不在dwl
中,因为我将那些部分重新适配到了现有的DWinLib
类中,但他的大部分工作都有了一个带有标签的家。在使用一段时间后,他的一些创造物被放入了全局命名空间,因为每次都输入swc::DC
太累了。Christopher Diggin的“any”类也被放在了cd
命名空间中,以提醒他曾做出贡献。(抱歉,Christopher——除非我必须,否则我不喜欢为命名空间输入三个字母的缩写。)他的any
类是一个非常方便的模板,我在几个场合都用过它!
SWC的改进
并非Francisco的所有库都已集成到DWinLib
中,如果您比较示例与他的工作,您会看到。但重要的核心已集成,因此其余部分的编码将比已完成的部分容易得多。我只是没有时间和精力。我的编码优先级一直是我的项目,现在DWinLib
已经足够健壮,可以承载它以新形式运行,我怀疑我是否会去处理“Pretty WinAPI
Class”页面上显示的那些其他窗口。如果您愿意,可以自由地这样做,并通知我或将代码发布到CodeProject。
抵消这一负面影响的是,我在重构SWC的过程中做了一些改进,并且修复了许多重要的bug。
- Francisco在他的文档中曾猜测是否可以将顶部停靠器添加到框架中。我已经完成了。
- 他还请求了一个
CString
的替代品。尽管我没有添加一个(如果您愿意,可以轻松使用Joe O'Leary的CStdString),但我已经集成了我旧的wString
类型,它会根据UNICODE
宏扩展到std::string
或std::wstring
。(我最初通过一个扩展std::basic_string
的typedef来实现这一点,但后来又对其进行了修改,以使用PJ Arends的作品。)您会在DWinLib
的许多函数签名和返回定义中找到wString
。 - 我将删除所有匈牙利命名法视为一项改进,尽管有些人可能对此持不同意见。正如在之前的文章中所述,我会在全局变量前加上“
g
”,并在类变量后加上“C”。我还开始更频繁地使用单元局部变量,并在它们后面加上“U”。Windows项,如鼠标回调,通常在前面加上“w”,例如“wMouseDown
”。对于我在DWinLib
中覆盖的任何命名约定,我都不道歉——我的目标是创建一个在整个项目中简单且一致的东西,如果我必须触及别人的代码,我通常会使其看起来像我的其他代码,以加快未来审查的理解速度,并最大程度地减少代码库中的样式数量。 - 在我看来,另一项改进是
DWinLib
中注册的Window
类数量减少了。Francisco为SWC创建的每个非原生控件窗口都创建了一个新的窗口类。例如,所有停靠窗口都是不同的类,尽管它们具有与其他窗口完全相同的属性。我对此进行了修改,使停靠窗口从一个WNDCLASSEX
(在dwl::DockWindow
单元中)派生,其他窗口从其他类派生。因此,每个不同的类型将只注册一个窗口类。(要更好地看到这一点,请在SWC重构项目中的SwcBaseWin::RegisterDefaultClass
中设置一个断点。) - 此外,我将所有内容重构为使用标准库。这使得一些代码更容易阅读。例如,Francisco在他的代码库中到处使用
swc::Array
(在我重构的版本中),它让人想起老式的C用法:SwcTabCtrl* tabControl = (SwcTabCtrl*) tabsC[selectedTabC];
与此相比:
SwcTabCtrl * tabControl = tabsC[selectedTabC];
消除了第一种方法中必要强制转换的冗余,使得代码更容易理解,因为强制转换总是打断我的思路,并让我以怀疑的眼光审视它们。它们很危险,需要注意。
- 我消除了所有
if (HIWORD(someWindowClassPointer) == NULL)
测试(DWinLib
从未有过)。例如,SwcTabbedWindowContainer::GetNumWnd
包含这两行:SwcBaseWin* pw=((SwcTabCtrl*) tabCtrlPtrArrayC [tabNumber])->parentC; if (HIWORD(pw)== NULL) return NULL;
SWC中也使用了类似的测试,我不明白为什么他不直接使用
if (!pw) return NULL;
代替。(也许这与系统保留的LOWORD
内存有关?但即使在这种情况下,HIWORD
检查也是不必要的,因为据我所知,Windows永远不应该发送会导致您在这些区域测试系统逻辑的消息。) - 两个其他重大改进已在zip文件下方提及:
- 所有示例都可以编译为
UNICODE
或Multi-Byte
程序。SWC只能编译为Multi-Byte
程序。 - 如果您想要MDI程序而不是SDI程序,请执行使用库中给出的操作。
- 所有示例都可以编译为
- 我认为bug修复并没有太多地体现在SWC重构示例中。例如,鼠标可以拖动停靠器到主窗口右侧很远的地方,窗口仍然会尝试停靠,如主窗口上的窗口轮廓所示。实际上,释放鼠标时它就会停靠。我已经修改了它,以便只有当鼠标靠近窗口边缘时才会发生停靠。
- 第二个,更隐蔽的bug是,如果您使用“X”按钮删除窗口一侧的所有停靠器。然后将另一个停靠器移动到该边缘:直到程序重新启动,该侧将不再有任何停靠动作。我已经在我的示例中修复了这个问题。
- 您应该能够轻松地在
DWinLib
中找到任何函数体。作为原始SWC代码库的一个实验,尝试在不使用Class Explorer或全局搜索的情况下确定CMiniDock::OnLButtonUp
的实现位置。没那么容易!而且这只是许多定义不在您期望位置的例子之一。 - 最后一个bug修复是所有停靠器在程序终止时都会泄漏。从未调用过析构函数。这并不是一个关键的bug,因为Windows会在此时回收内存,但这并不是一个好习惯。我还解决了额外的资源泄漏,尽管我不能保证我解决了所有问题。(例如,
CReBarCtrlEx::OnPaint
的构造方式使得CPaintDC
构造函数中的BeginPaint
调用永远不会终止。)
说到泄漏的停靠器,有一个有趣的结构被用来克服这个问题。
在重写过程中,我采用了SWC的方法,即程序中有一个“空闲”循环。当然,这样的努力从来都不是完全直接的(尽管也不算太难)。这是代码的相关部分:
//DWinLib:
#ifdef DWL_MDI_APP
HWND mdiClient = gDwlMainWin->mdiClientHwnd();
HWND hwnd = gDwlMainWin->hwnd();
//Per Microsoft, all exceptions must be handled before returning control back to
//this message pump. An old link for this statement is dead.
//See DwlBaseApp::WndProc for the exception handler implemented for DWinLib.
accelTableC = accelC.table(); //The app can change this while running by calling
while ((var = GetMessage(&msg, NULL, 0, 0)) != 0) { //changeAccelTable(...)
if (var == -1) return var;
if (!TranslateMDISysAccel(mdiClient, &msg) &&
!TranslateAccelerator(hwnd, accelTableC, &msg)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
if (!::PeekMessage(&msg, NULL, NULL, NULL, PM_NOREMOVE)) {
idleC = true;
//Do the idle processing:
while (idleC) idleC = wIdle();
}
}
}
#else
while ((var = GetMessage(&msg, NULL, 0, 0)) != 0) {
if (var == -1) return var;
TranslateMessage(&msg);
DispatchMessage(&msg);
//Now check if we should go into idle processing:
if (!::PeekMessage(&msg, NULL, NULL, NULL, PM_NOREMOVE)) {
idleC = true;
//Do the idle processing:
while (idleC) idleC = wIdle();
}
}
#endif
//...
bool Application::wIdle() {
bool result = false;
iteratorInvalidatedC = false; //Set this up so only changes made in 'wIdle' affect it.
auto it = windowsC.begin();
while (it != windowsC.end()) {
if (it->second->wIdle() == true) result = true;
if (iteratorInvalidatedC == true) {
iteratorInvalidatedC = false;
it = gApplication->windowsC.begin();
if (it == windowsC.end()) return result;
//The code will now go through all the windows again (except the first window)
//and redo the 'wIdle' processing, but that is better than blowing up!
}
++it;
}
return result;
}
//SWC:
MSG msg;
BOOL bresult;
BOOL bPeekMsg=TRUE;
while (bPeekMsg || GetMessage(&msg, NULL, 0, 0))
{
if (bPeekMsg)
{
if(!PeekMessage(&msg,NULL,0,0,PM_REMOVE))
bPeekMsg=mainWinC->OnIdle();
continue;
}
if (bMDI)
{
bresult=(
(!TranslateMDISysAccel (mainWinC->GetSafeClientHwnd(), &msg))
&& (!TranslateAccelerator (msg.hwnd,hAccelTable, &msg)));
}
else
bresult=(!TranslateAccelerator (msg.hwnd, hAccelTable, &msg));
CWin* pActive= reinterpret_cast<CWin*>((HWND)::GetWindowLong(msg.hwnd,GWL_USERDATA));
//CWin::GetUserPointerWindow(msg.hwnd);
BOOL bPre=TRUE;
//if (pActive)
// bPre=pActive->PreTranslateMessage(&msg);
if (bresult && bPre)
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
return msg.wParam;
}
//SWC only queries the main window for idle conditions, unlike my rewrite:
virtual BOOL OnIdle() //Main window
{
return FALSE;
}
您会注意到,当DWinLib
程序获得许多窗口时,如果知道其中一些窗口永远不会进行空闲处理,那么查询所有窗口的空闲循环可能有点过头了。如果需要,您可以修改它。我在6.04重写中通过向Application
添加Idler
单元对此进行了修改:
void Idler::addToIdleList(dwl::BaseWin * win) {
auto it = std::find(windowsToIdleC.begin(), windowsToIdleC.end(), win);
if (it != windowsToIdleC.end()) return; //Already in list, no need to insert again
windowsToIdleC.push_back(win);
}
void Idler::wIdle() {
bool idleProcessingNeeded = true;
auto it = windowsToIdleC.begin();
std::list<std::list<dwl::BaseWin*>::iterator> itsToRemove;
while (!windowsToIdleC.empty()) {
while (it != windowsToIdleC.end()) {
bool itHasMoreIdleProcessingToDo = (*it)->wIdle();
if (!itHasMoreIdleProcessingToDo) itsToRemove.push_back(it);
++it;
}
while (!itsToRemove.empty()) {
windowsToIdleC.erase(itsToRemove.back());
itsToRemove.pop_back();
}
}
}
您还会注意到,在“BOOL bPre=TRUE;
”之后的两行被注释掉了,如果您深入挖掘Campos的原始源代码。在我的机器上,示例在PreTranslateMessage
部分失败,注释掉可以避免这个问题,尽管coolbar不再显示。
进入有趣的情况,我实际上使用wIdle
来销毁浮动的停靠窗口。
过去,我曾遇到过WM_NCDESTROY
不是窗口收到的最后一个消息的情况。我相信涉及WM_UAHDESTROYWINDOW
消息,但我不记得更多了。由于这些经验,我避免假设WM_NCDESTROY
是一个有用的工具。当寻找在适当的时候销毁浮动窗口的方法时,我能看到的唯一方法就是使用最后一个窗口消息。但为了使其更安全,我使用了以下结构:
LRESULT swc::FloatingWindow::wNcDestroy() {
if (!beingDestroyed()) {
needToDestroyC = true;
}
return 0;
}
bool swc::FloatingWindow::wIdle() {
if (needToDestroyC) {
delete this;
}
return false; //If you return true, wIdle will continually reprocess
//and take up 100% of processor.
}
它奏效了!尽管我不知道这是否是过度设计,还是不是。当WM_NCDESTROY
后面跟着另一个消息时,第二个消息会立即排队吗?我从未测试过。如果您遇到这种情况,并且这种方法失败了,您就可以嘲笑我设计了一个无效的解决方案!
GDI对象
Windows编程中一个令人沮丧的方面是跟踪图形设备接口(GDI)对象。这些是笔、画笔、位图和字体。当您在设备上下文(DC)中选择一个来使用时,通常必须记住在处理完毕后将之前的对象重新选择到DC中,并执行DeleteObject
。
DWinLib
的早期版本有一个机制,其中DwlDC
(我的Windows HDC
的包装器)包含一个画笔、字体、位图和钢笔指针,因此当DwlDC
超出范围时,就会处理适当的进程,并且“记忆的繁琐”减少了。
SWC没有任何等效的东西,在DWinLib
6.00中,重点是让它工作,而不是担心不便。但实际上使用这些方法使旧的沮丧感浮现出来,并促使我再一次修改DWinLib
。
新的框架不是我早期技术的直接翻译,因为我对某些方面不满意,而且SWC是不同的范例。所以我回到了起点。
在DWinLib
6.01中,早期的swc::Gdi
已被重命名为swc::DC
(现在自6.04起仅为DC
),因为这个缩写更好地描述了该类包装的内容。DC
现在有四个std::unique_ptr
,每个GDI对象一个。代码的相关部分将为您提供该如何使用的良好概念,但我将在以下代码片段之后更具体地说明:
class DC {
private:
enum Type { UseBeginPaint, UseGetDC, UseCreateCompatibleDC, Unspecified };
Type typeC = Unspecified;
HWND hwndC; //Must make hwndC be initialized before dcC for BeginPaint to work.
PAINTSTRUCT * psC; //Must also be before dcC.
HDC dcC;
std::unique_ptr<swc::Bitmap> bitmapC;
std::unique_ptr<swc::Pen> penC;
std::unique_ptr<swc::Font> fontC;
std::unique_ptr<swc::Brush> brushC;
public:
DC(HDC dc=NULL) : dcC(dc), typeC(Unspecified) {
}
DC::DC(HDC dc, HWND hwnd) : dcC(dc), hwndC(hwnd), typeC(UseGetDC) {
}
DC::DC(HWND hwnd, PAINTSTRUCT * ps) : hwndC(hwnd), psC(ps),
dcC(BeginPaint(hwndC, psC)), typeC(UseBeginPaint) {
}
DC::DC(HWND hwnd) : hwndC(hwnd), dcC(GetDC(hwnd)), typeC(UseGetDC) {
}
DC::DC(DC & dc) : dcC(CreateCompatibleDC(dc.dcC)), typeC(UseCreateCompatibleDC) {
}
~DC() {
if (bitmapC.get()) {
SelectObject(dcC, bitmapC->oldBitmapC);
}
if (fontC.get()) {
SelectObject(dcC, fontC->oldFontC);
}
if (penC.get()) {
SelectObject(dcC, penC->oldPenC);
}
if (brushC.get()) {
SelectObject(dcC, brushC->oldBrushC);
}
if (typeC == UseBeginPaint) EndPaint(hwndC, psC);
else if (typeC == UseGetDC) ReleaseDC(hwndC, dcC);
else if (typeC == UseCreateCompatibleDC) DeleteDC(dcC);
else if (typeC == Unspecified) {
//Do nothing, and let the caller manage the DC
}
}
HDC operator()() { return dcC; }
HFONT setFont(HFONT font, DeleteAction deleteAction) {
if (fontC.get()) {
//First, remove the font from the dc:
HFONT oldFont = (HFONT) SelectObject(dcC, fontC->oldFontC);
}
//The following will DeleteObject on the HFONT if 'deleteActionC == DoDelete'
fontC.reset(new swc::Font(font, deleteAction));
fontC->oldFontC = (HFONT) SelectObject(dcC, fontC->fontC);
return fontC->oldFontC;
}
HPEN setPen(HPEN pen, DeleteAction deleteAction) {
if (penC.get()) {
//First, remove the pen from the dc:
HPEN oldPen = (HPEN) SelectObject(dcC, penC->oldPenC);
}
//The following will DeleteObject on the HPEN if 'deleteActionC == DoDelete'
penC.reset(new swc::Pen(pen, deleteAction));
//And finally select the pen:
penC->oldPenC = (HPEN) SelectObject(dcC, penC->penC);
return penC->oldPenC;
}
HBRUSH setBrush(HBRUSH brush, DeleteAction deleteAction) {
if (brushC.get()) {
//First, remove the brush from the dc:
HBRUSH oldBrush = (HBRUSH) SelectObject(dcC, brushC->oldBrushC);
}
//The following will DeleteObject on the HBRUSH if 'deleteActionC == DoDelete'
brushC.reset(new swc::Brush(brush, deleteAction));
//And finally select the brush:
brushC->oldBrushC = (HBRUSH) SelectObject(dcC, brushC->brushC);
return brushC->oldBrushC;
}
HBITMAP setBitmap(HBITMAP bitmap, DeleteAction deleteAction) {
if (bitmapC.get()) {
//First, remove the bitmap from the dc:
HBITMAP oldBitmap = (HBITMAP) SelectObject(dcC, bitmapC->oldBitmapC);
}
//The following will DeleteObject on the HBITMAP if 'deleteActionC == DoDelete'
bitmapC.reset(new swc::Bitmap(bitmap, deleteAction));
//And finally select the bitmap:
bitmapC->oldBitmapC = (HBITMAP) SelectObject(dcC, bitmapC->bitmapC);
return bitmapC->oldBitmapC;
}
//...
如您所见,根据使用的DC
构造函数,在DC
销毁时将采取适当的操作。您不再需要记住在HDC
和HWND
传递到DC
时调用ReleaseDC
。
而且,如果您通过“setXXX”调用传递了HPEN
、HBRUSH
或其他GDI对象,则必须指定该对象在周期结束时是否应被DeleteObject
ed。这个额外的指定步骤可能看起来不方便,但它让我回想起我想要对象的使用方式,并允许我将字体或其他对象作为类成员,而不必在DC超出范围时销毁。
一个例子将帮助我们开始,并阐明细节。
Francisco的SWC代码有几个资源问题,这些问题归结为管理。据我记忆,这不是其中一个,但它确实显示了方法的不同,以及我的修订带来的简化。为什么Francisco在原始代码中使用new
和delete
是未知的。我认为这并非必需,但我只认真查看了足以确定我不需要这样做。
//DWinLib version of Dock Manager Window painting:
LRESULT swc::DockManagerWindow::wPaint(DC & dc) {
Brush brush(CreateSolidBrush(dwl::colors::windowFace()), DoDelete);
//The following is lazy coding, because the 'getClientRect()' routine makes a copy,
//whereas if I'd "Rect r; getClientRect(r);", no copy would have been made.
//Lazy, lazy, lazy! But I never claimed not to be, even though it required
//this long comment to point out the extent of my laziness.
Rect clientRect = getClientRect();
DC memDC(dc);
Bitmap memDcBitmap(CreateCompatibleBitmap(dc(), clientRect.width(), clientRect.height()),
DoDelete);
//Note that the underlying HBITMAP is being passed in the following call, NOT a
//swc::Bitmap:
memDC.setBitmap(memDcBitmap(), DontDelete);
memDC.fillRect(&clientRect, &brush);
dc.bitBlt(0, 0, clientRect.width(), clientRect.height(), memDC(), clientRect.left,
clientRect.top, SRCCOPY);
return TRUE;
}
//Francisco's original code:
BOOL DockManager::OnPaint(HDC hDC) {
CRect rcClient;
CPaintDC dc(GetSafeHwnd()); // device context for painting
CBrush cbr;
CRect m_rectDraw;
cbr.CreateSolidBrush(CDrawLayer::GetRGBColorFace());
GetClientRect(rcClient);
CGDI MemDC;
CBitmap m_BitmapMemDC;
MemDC.CreateCompatibleDC(dc.m_hDC);
m_BitmapMemDC.CreateCompatibleBitmap(dc.m_hDC,rcClient.Width(),rcClient.Height());
CBitmap *m_bitmapOld=new CBitmap(MemDC.SelectObject(&m_BitmapMemDC));
MemDC.FillRect(&rcClient,&cbr);
//paint routines
dc.BitBlt(0,0,rcClient.Width(),rcClient.Height(),MemDC.m_hDC,
rcClient.left,rcClient.top,SRCCOPY);
MemDC.SelectObject(m_bitmapOld);
m_BitmapMemDC.DeleteObject();
MemDC.DeleteDC();
cbr.DeleteObject();
m_bitmapOld->DeleteObject();
delete m_bitmapOld;
return TRUE;
}
请注意,不再需要DeleteObject
s,尽管如果您确实想在DWinLib
中使用旧方法,也可以这样做。换句话说,您可以通过SelectObject
和DeleteObject
自己进行所有资源管理,但为什么要这样做呢?
另请注意,在我的重写中,位图被编码为在超出范围时被DeleteObject
ed,而不是在内存DC销毁时。这允许位图保留更长时间,如果您需要它用于其他目的。
GDI和DC对象是交互的,为了更好地理解这种交互,这里粘贴了swc::Font
对象相关部分的代码:
//Forward declare the DC class:
class DC;
namespace swc {
enum DeleteAction { DoDelete, DontDelete };
//-------------------------------
//Font
//-------------------------------
class Font {
friend class DC;
private:
HFONT fontC;
//Housekeeping items for swc::DC to use if needed:
private:
HFONT oldFontC;
DeleteAction deleteActionC;
public:
Font(HFONT font, DeleteAction deleteAction) :
fontC(font), oldFontC(NULL), deleteActionC(deleteAction) {
}
~Font() {
if (deleteActionC==DoDelete && fontC!=NULL && fontC!=oldFontC)
DeleteObject(fontC);
}
//...
我认为最好将oldFontC
成员放入字体本身,而不是用这些细节污染DC
。即使DC
在逻辑上负责跟踪旧字体、笔等,但将所有这些项放在该类中会使代码更加混乱。如果您有兴趣了解我为什么这样说,以下是DWinLib
先前版本的代码。将DwlDC
构造函数和析构函数与前面的代码进行比较。
//Used when painting from a WM_PAINT MESSAGE
DwlDC::DwlDC(HWND hwnd, PAINTSTRUCT * ps) : hwndC(hwnd), typeC(UseBeginPaint), psC(ps),
penC(NULL), brushC(NULL), bmpC(NULL), fontC(NULL),
deletePenWhenDoneC(false), deleteBrushWhenDoneC(false),
deleteBmpWhenDoneC(false), deleteFontWhenDoneC(false) {
dcC = BeginPaint(hwndC, psC);
initObjects();
}
//Used when painting from a non-WM_PAINT message
DwlDC::DwlDC(HWND hwnd) : hwndC(hwnd), typeC(UseGetDC),
penC(NULL), brushC(NULL), bmpC(NULL), fontC(NULL),
deletePenWhenDoneC(false), deleteBrushWhenDoneC(false),
deleteBmpWhenDoneC(false), deleteFontWhenDoneC(false) {
dcC = GetDC(hwnd);
initObjects();
}
//Used when creating a compatible DC from another dc
DwlDC::DwlDC(DwlDC & wdc) : hwndC(NULL), typeC(UseCreateCompatibleDC),
penC(NULL), brushC(NULL), bmpC(NULL), fontC(NULL),
deletePenWhenDoneC(false), deleteBrushWhenDoneC(false),
deleteBmpWhenDoneC(false), deleteFontWhenDoneC(false) {
dcC = CreateCompatibleDC(wdc());
initObjects();
}
DwlDC::~DwlDC() {
SelectObject(dcC, origBrushC);
SelectObject(dcC, origPenC);
SelectObject(dcC, origBmpC);
SelectObject(dcC, origFontC);
if (penC && penC != origPenC && deletePenWhenDoneC) DeleteObject(penC);
if (brushC && brushC != origBrushC && deleteBrushWhenDoneC) DeleteObject(brushC);
if (bmpC && bmpC != origBmpC && deleteBmpWhenDoneC) DeleteObject(bmpC);
if (fontC && fontC != origFontC && deleteFontWhenDoneC) DeleteObject(fontC);
if (typeC == UseBeginPaint) EndPaint(hwndC, psC);
else if (typeC == UseGetDC) ReleaseDC(hwndC, dcC);
else if (typeC == UseCreateCompatibleDC) DeleteDC(dcC);
}
void DwlDC::initObjects() {
origPenC = (HPEN) GetCurrentObject(dcC, OBJ_PEN);
origBrushC = (HBRUSH) GetCurrentObject(dcC, OBJ_BRUSH);
origFontC = (HFONT) GetCurrentObject(dcC, OBJ_FONT);
origBmpC = (HBITMAP) GetCurrentObject(dcC, OBJ_BITMAP);
}
void DwlDC::font(HFONT newFont, bool deleteFontWhenDone) {
//This will return the original font if the user wants to do something with it
HFONT oldFont = (HFONT)SelectObject(dcC, newFont);
if (deleteFontWhenDoneC && oldFont != fontC) DeleteObject(oldFont);
fontC = newFont;
deleteFontWhenDoneC = deleteFontWhenDone;
}
//...
在我的重新设计中,我也可以删除friend
声明,并为fontC
和doDeleteC
成员添加getter和setter(尽管doDeleteC
可能永远不会被DC
直接更改),但我认为friend
关系是一个更干净的解决方案。这可能是我第三次在任何生产代码中使用friend
,这说明了我发现这种构造有多么少用。
关于这方面的最后一点是,我使用operator()
来返回字体、笔、DC等中的项。浏览Francisco的代码,我发现了一个我以前从未见过的隐式转换运算符。对于将实例隐式转换为其HPEN
成员的情况,它看起来像这样:
operator HPEN() { return penC; }
尽管它提供了便利,但我回避任何类型的隐式转换。它们以前曾让我吃过亏。我想通过查看屏幕来了解函数何时被调用。
HPEN pen = theSwcPen();
//rather than:
HPEN pen = theSwcPen;
我也厌恶在各地输入getPen
,因为通过operator()
的使用,转换非常明显。重复前面代码中的评论,我懒惰!我试图使我的代码尽可能简单地满足我的懒惰,同时又足够具有描述性以保持高度的可理解性!我希望上面的内容能在未来激发一些伟大的、懒惰的创作,如果您有任何改进建议,请在下方发布!
趣味内容
SWC是一个有趣的框架,尽管它的编码实践有时让我嘀咕(实际上是很多)。例如,它并不是真正“即插即用”的。rebar和停靠窗口是在SwcBaseSdiMdiFrame
单元(在我重构的版本中)中设计和实现的。主窗口是PwcStudioMainWin
,它派生自SwcBaseSdiMdiFrame
。如果您想插入另一个停靠框架,或删除rebar,您必须重构这两个对象,而不仅仅是一个。
由于这类问题,我重新设计了DWinLib
,使其对即插即用更加友好。在示例中,停靠器被“插入”到主窗口中。它们不包含在基类中。
通过将此与命名空间结合,您可以更轻松地使用另一个停靠框架——只需将其插入主窗口(MainAppWin
)即可。为了测试它,我复制了所有SWC停靠框架文件,给它们起了另一个命名空间,并通过包含新文件并修改MainAppWin.cpp和MainAppWin.h中的命名空间来更改程序以使用副本。实际修改程序以使用新文件的最后一部分工作量不到一分钟,结果符合我的预期。
在重构SWC时,我遇到了一些我从未想过会做的事情。在DWinLib
和重构的文件中,有一个SwcPrimitives.h。在其中,Size
派生自SIZE
(并且Rect
派生自RECT
等…),通过这样做,它捕获了所有Windows SIZE
的特性。这种范例从未进入我的脑海,我认为这相当巧妙!
合并后的DWinLib
/SWC代码库包含一些您可能会觉得有用的项目。Campos的GDI单元(swc::Gdi
)有一个渐变类,我认为它相当不错。它似乎目前没有针对宽度(或高度)小于256像素的位图进行优化,因为它在绘制位图时总是会迭代256步,但有时间的话可以对此进行更改。
您可能会发现Utilities子目录中的一些项目很有用。例如,如果您需要使用Windows的方法枚举窗口的子窗口,dwl::ChildEnumerator
可以处理这项任务的繁重工作。(我还没有需要公开DWinLib
的子窗口,所以目前还没有将其编码到DWinLib
中,尽管这样做会很简单。只需枚举dwl::Control::childControlsC
向量即可。)
学到的Visual Studio技巧
在执行这些修改时,我的调查不知何故引导到了“工具 - >代码片段管理器”菜单项。如果您从未玩过这个工具,我建议您这样做,因为它是一个很好的时间节省器。
我总是会忘记输入_T("some string")
。按键总是感觉很别扭,因为需要频繁使用'Shift'键。使用代码片段,我现在只需按“T”(当然,要大写,使用'Shift'),然后按Tab键,就会出现一个通用的字符串,我可以覆盖它,然后按“Enter”键跳到“)”的末尾。超级棒!以下是该操作的代码:
<!-- T.snippet -->
<?xml version="1.0" encoding="utf-8" ?>
<CodeSnippets xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
<CodeSnippet Format="1.0.0">
<Header>
<Title>_T</Title>
<Shortcut>T</Shortcut>
<Description>Code snippet for _T statement</Description>
<Author>Me, Myself, and I</Author>
<SnippetTypes>
<SnippetType>Expansion</SnippetType>
<SnippetType>SurroundsWith</SnippetType>
</SnippetTypes>
</Header>
<Snippet>
<Declarations>
<Literal>
<ID>expression</ID>
<ToolTip>Expression to evaluate</ToolTip>
<Default>string</Default>
</Literal>
</Declarations>
<Code Language="cpp"><![CDATA[_T("$expression$")$end$]]>
</Code>
</Snippet>
</CodeSnippet>
</CodeSnippets>
如果您需要这个,您只需将其复制到一个专用目录的文本文件中,然后通过“工具 - >代码片段管理器”菜单项将其指向VS。
我还编写了“td
”来填充通用的//TODO: string
,“throw
”扩展为一个带有通用_T
宏的dwl::Exception
,以及“sup
”开始定义一个std::unique_ptr
。从上面的例子中,您很可能可以找出如何将它们添加到您的系统中。
花了一分钟才意识到片段中的“$end$
”是VS在突出显示的片段区域内按“Enter”键时跳转到的位置。有时,这是摆脱这种突出显示的唯一方法。
另一个小麻烦是,使用片段有时会弄乱您的缩进,如果您不使用Visual Studio强制的标准样式。罢了。
尽管存在这些问题,我以前从未接触过代码片段的使用,所以我想在这里提到它。如果您还没有接触过,希望这能为您节省一些时间。
还有最后一个我将在下面记录的技巧,因为我在修订过程中不得不谷歌搜索了三次。为了使类视图转到当前选择的类,在工具 - >选项 - >键盘中,“显示包含命令:”框中输入“SynchronizeClassView
”,然后在“按快捷键”框中输入您想要的快捷键,最后按“分配”。我使用Ctrl + F1,这是一种快速查看类中其他项的便捷方法。
待办事项
我知道示例程序中有四个问题。其中三个在6.03的rebar测试中存在,所以我怀疑它们也影响6.04版本,尽管正如我前面所说,rebar示例不起作用,并且被保留下来作为任何愿意尝试的人的练习。
第一,如果您在release模式下编译rebar项目,rebar的高度将是大约150像素而不是20像素。这在Francisco的代码中就已经存在,我无法快速找出原因。
第二,在release和debug模式下,停靠代码和rebar大小调整之间存在奇怪的交互。如果您不断按“Ctrl + N”来创建新窗口,MDI工作区会闪烁到rebar空间。
我没有深入研究这两个问题,因为我没有计划使用rebar,但值得注意的是。如果您尝试并找到问题,请在下方发布。
此外,rebar不使用Campos的渐变绘制方法,因为我从未在ControlWin::classWinProc
中重写wPaint
例程。不知何故,Francisco在上面长篇复制粘贴的代码中的代码调用了使用渐变的paint过程,但我的重构最终调用了Windows提供的默认渐变。当我覆盖WM_PAINT
时,似乎有什么东西遮盖了rebar,并且我无法让rebar绘制,因为HREGION
无效,尽管HDC有效!这是一个我没有弄清楚的困惑。
最后一个问题是在DwlPwcStudio
(以及Francisco的原始版本)中。在停靠窗口的分割线内部,在各种情况下计算和绘制不正确。我认为这与非客户区域被错误地计算有关,但我没有深入研究。这在我的MEdit工作中不是优先事项。如果您解决了这个问题,请发布解决方案,我将将其集成到DWinLib
中。
关于最后一项(分割线绘制不正确):距离写完上一段已经过去了一周左右,我现在非常确定这个怪癖是由于Windows的一个怪异之处,即在客户端和窗口坐标之间发生了偏移。dwl::DockWindow
单元引起了我的注意,因为我试图让分割线在那里正常工作。我没有将此更改移植到SWC代码中,但必要的修订可能涉及类似这样的内容:
Rect mainWin;
gDwlMainWin->getWindowRect(mainWin);
Rect clientRect;
getWindowRect(clientRect);
blitRect.offsetRect(Point(clientRect.left-mainWin.left, clientRect.top-mainWin.top));
请参阅dwl::DockWindow::drawWindowResizeBar
以了解dwl
版本。
这里还可以再加一点。现在DWinLib
已经稳定下来,它可以被做成一个头文件库。我认为如果您想抛弃库并将所有内容的编译绑定到一个周期中,这将进一步提高编译时间。
但是我的#include
记忆告诉我,可能会有循环依赖,如果走这条路,将很难修复。而且我不喜欢在头文件库的安排中查找东西。这会变得非常痛苦。所以我会保持现状。如果缩短编译时间是优先事项,那么编译库的方法是首选。
在我完成6.01 MEdit重构时,我意识到菜单回调可以改进,所以在结束之前应该提到这一点,因为我现在不打算立即处理它。
目前,MEdit
在主窗口中有大量的dwl::CallbackItems
来处理诸如打开和关闭文件等操作。菜单逻辑需要将委托传递给它,这通过菜单创建逻辑中的CreateDelegate
宏实例化来完成。
所有菜单只需要一个整数id,以便传回WM_COMMAND
处理程序。dwl::CallbackItems
拥有必要的id,可以通过id()
访问,但是更改菜单逻辑以获取该id并使用它而不是委托不是一件简单的事情。(逻辑的制定只是为了让它工作,而且菜单逻辑是微软给我们的一个复杂而扭曲的野兽,如果您查看DwlMenu
文件,就会发现。实施必要的更改需要一天或更长时间(对我来说,事情总是如此,可能会更长),目前我只能凑合着做,因为它确实能工作,尽管我创建包含自己的回调的工具栏和菜单包装类的做法会创建具有新编号的委托,这些编号处理与现有编号相同的项。)
使用技巧
我的主要目标一直是我的MIDI程序MEdit,它比我所知道的其他音序器能提供更精细地控制MIDI事件。DWinLib
的出现是因为Borland Builder的一个bug,而环境让我重新发明轮子来修复那个bug。正如您可能从GDI对象部分和本文的其余部分推断出的那样,我的目标是让DWinLib
简单而强大,同时又非常接近API底层。与David Nash的方法不同,您不必考虑控件ID。而且我认为他的,或其他包装库,不像DWinLib
那样容易使用GDI对象(但我没有详细研究过,所以如果我错了,请告诉我)。
另一件事是DWinLib
没有DialogProc
导向的窗口,以及所有后台工作。但是,它包含一个相当简单的机制来创建一个窗口并将其用作对话框。窗口创建过程的基本原理是相同的,因此您不需要记住两种不同的方法。唯一不同的是当选定的窗口变为模态时父窗口如何处理,以及我用于返回值(如链接文章中所述)的机制。
有些人可能会认为这是个缺点,因为DWinLib
不倾向于通过资源创建表单。这样做需要更多的工作来隐藏控件ID,而我的项目不需要。换句话说,对于一个复杂的金融应用程序布局一个输入表单不是我会在DWinLib
中乐于做的事情。但是对于一个免费的包装库来说,它确实有很多功能。(您可以通过修改特定窗口的wCommand
例程来处理该窗口特有的控件ID,因此如果您想走这条路,添加该功能可能并不难。)
(再玩了一会儿,通过Visual Studio的对话框编辑器创建一个对话框并在代码中调用它,并不是一件非常困难的事情。对话框窗口有自己的窗口过程,所以它们不依赖于DWinLib
的内部机制。它们仍然很麻烦,因为您必须做很多工作来在头文件中正确设置ID。前面提到的“对话框”文章有代码展示了如何实现这一点。)
在我重构MEdit
时,一些有用的知识点浮现出来:
MainAppWin
是在堆栈上创建的。几乎所有其他窗口都是通过new
创建的,并且DWinLib
负责删除子窗口。(很久以前,我发现有些窗口,如模态对话框,可以安全地在堆栈上创建。现在可能仍然可能,但我已经有一段时间没有尝试过了。)- 如果您担心每个窗口的虚拟函数数量,其中一些您可能永远不需要,您可以为每个窗口类型覆盖虚拟
winProc
,并且只定义该类型想要拥有的虚拟方法。我认为没有必要,特别是考虑到当今计算机的功能,但如果您愿意,可以这样做。 - 关于最后一点,通过覆盖相同的
winProc
并添加其他您希望响应的消息的处理程序,最后调用原始winProc
进行原始消息处理,也可以轻松地扩展一个类。例如,这是
MEdit
的MainAppWin::winProc
的一个修改版本:LRESULT MainAppWin::winProc(HWND window, UINT msg, WPARAM wParam, LPARAM lParam) { //This is a small aspect of the single-instance logic: if (msg == UWM_YOU_ARE_ME) return UWM_YOU_ARE_ME; //And continue on to other stuff: else if (msg == WM_COPYDATA) { COPYDATASTRUCT *cds = (COPYDATASTRUCT*)lParam; if(cds->dwData == UWM_YOU_ARE_ME) { //Constrained to items //coming from this program openMultiFiles(wString((TCHAR*)cds->lpData)); return 0; } } //handle other stuff... //and finally call the original proc: return BaseWin::winProc(window, msg, wParam, lParam); }
- 在某些情况下,如
dwl::ModalBase
逻辑,对于MDI构建,需要使窗口过程返回DefWindowProc
结果而不是DefFrameProc
。如果您创建一个需要前者(后者)的类,只需在适当的构造函数中添加一行wUseDefProcInMdiC = true;
。 - 在
DWinLib
的当前状态下,有两个窗口过程值得注意,因为我没有保证它们都有相同的函数已定义。我在处理MEdit
的滚动代码时(重新)发现了这一点。停靠窗口中的滚动条工作正常,但主窗口中的滚动条却不行。问题是MDI子窗口有自己的窗口过程
dwl::MdiBase::winProc
,因为它们经常需要调用和返回DefMDIChildProc
而不是DefWindowProc
。我的问题是我没有在winProc
中启用WM_HSCROLL
和WM_VSCROLL
,但取消注释其中的旧代码解决了问题。(我不需要在头文件中定义过程,因为dwl::MdiBase
继承自dwl::BaseWin
。但是这些过程不会被启用,因为dwl::MdiBase::winProc
完全覆盖了dwl::BaseWin::winProc
。)根据关于哪些窗口需要MDI处理的问题,停靠器不属于此类别,因为它们连接到主窗口的逻辑,而不是MDI客户端窗口。rebar和状态栏等项也免于MDI处理。
- 除非我忽略了什么重大问题,否则菜单皮肤无法用于系统菜单。在我的测试中,我无法使用Francisco代码派生的
CMenuSpawn
方法来控制该菜单。(据我所知,菜单代码最初是由Iuri Apollonio创建的。1998年的一个早期文章可以在CodeGuru上找到。如果我从根本上来源上错了,或者您找到了Apollonio作品的更好链接,请留言。)
顺便说一句,为了让弹出菜单被皮肤化,在响应鼠标按下消息处理程序调用
popupAtMouse
之前,调用changeToOwnerDrawn
。 - 您可以安全地使用多重继承与
DWinLib
窗口,只要这种方法有意义。例如,您不想创建一个有两个winProc
s的窗口,因为这会破坏DWinLib
本身的内部结构。但是创建一个带有Undo
功能的窗口是可以按预期工作的。
日志记录
如果您需要排除故障,并且想在不进行断点调试的情况下捕获大量内容,DWinLib
有一个简陋的日志记录系统。要使用它,
- 取消注释PrecompiledHeaders.h文件中库和主项目中的
#define DWL_DO_LOGGING
行。(未来,这可以修改为只影响主程序,但根据当前dwl::Application
中的配置,记录器已被实例化,并且这允许在DWinLib
本身内部进行日志记录,如果您需要跟踪其中的内容。) - 在您希望捕获内容的.cpp文件顶部的以下位置添加:
#if defined (DWL_DO_LOGGING) #include "DwlLogger.h" extern dwl::Logger * gLogger; #endif
- 在需要日志记录的点,放置类似以下的代码:
#ifdef DWL_DO_LOGGING std::tstringstream str; wString space = _T(" "); str << _T("Msg: ") << msg; gLogger->padStream(str, 10); str << space << gLogger->crack(msg); gLogger->padStream(str, 34); str << _T("hwnd: ") << (win) << _T(", wParam: ") << wParam; gLogger->padStream(str, 70); str << _T("lParam: ") << lParam; gLogger->log(str); #endif
正如可以猜到的,前述内容将记录发送到
winProc
的所有消息。(padStream
只是为了对齐列,因为参差不齐的文本很难用肉眼解析。)搜索示例项目中的#ifdef DWL_DO_LOGGING
将揭示DWinLib
中已注释掉的区域,其中已注释掉了其他示例用法。如果您认为它们最初是为我自己的bug查找而创建的,那么您是正确的。 - 在DwlApplication.cpp的
dwl::Application::Application
中更改日志文件的位置/文件名。它可能是这样的行:gLogger = new Logger(_T("C:\\Users\\David\\Documents\\Programs\\MyProgs\\curLog.txt"));
您也可以在运行时更改日志位置,方法是在
MainAppWin
中,在调用dwl::MainWin::instantiate
方法之后,使用以下代码:#if defined DWL_DO_LOGGING if (!gDialogs->openDialog(wString(_T("Log File:\0*.txt\0\0"), 18), _T("txt"), OFN_HIDEREADONLY)) return; gLogger->changeFile(gDialogs->fileName()); #endif
- 然后运行程序并重现您要测试的情况。如果您运行多次并在每次运行后查看,我推荐Notepad++,因为它会提示您文件已更改。这节省了很多重新加载的麻烦。而且,在我看来,Notepad++是一个您应该知道的不可或缺的工具。
在我大部分编程工作中,我使用日志记录来弄清楚许多事情。最终,我意识到OutputDebugString
几乎总是更快的一种调试技术。这是一个可以减少其调用繁琐的片段:
<?xml version="1.0" encoding="utf-8" ?> <CodeSnippets xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet"> <CodeSnippet Format="1.0.0"> <Header> <Title>DebugOut</Title> <Shortcut>debugout</Shortcut> <Description>Send something to OutputDebugString</Description> <Author>Me, Myself, and I</Author> <SnippetTypes> <SnippetType>Expansion</SnippetType> <SnippetType>SurroundsWith</SnippetType> </SnippetTypes> </Header> <Snippet> <Declarations> <Literal> <ID>expression</ID> <ToolTip>Item to output</ToolTip> <Default></Default> </Literal> </Declarations> <Code Language="cpp"> <![CDATA[std::wstringstream str; str << "$expression$" << std::endl; OutputDebugString(str.str().c_str());$end$ ]]> </Code> </Snippet> </CodeSnippet> </CodeSnippets>;
以上是我工作中所触发的内容。如果您尝试DWinLib
,希望这些能缩短您的学习时间。如果您有其他可以在此处解决的问题,请告诉我,我会添加指针。
结束语
除上述内容外,没有立即想到的事情,所以我将在此结束。我刚刚才想起PWC Studio示例中的停靠器是带标签的,我之前没说过,所以如果您将一个未停靠的拖到一个已停靠的上面,您可以从底部选择您想要的标签。这让我想起,当将一个未停靠的窗口拖到一个停靠器上时,当鼠标在停靠器的非客户端区域上方时,没有提供任何反馈。Campos的代码也没有这个功能,所以如果我以后使用SWC停靠器,我可能会解决这个问题。
哦,是的。创建窗口时,请通过 gDwlGlobals->dwlApp->createWindow(BaseWin * winBeingCreated, const CREATESTRUCT & cs)
进行最终创建。示例代码也使用了此技术,您可以参考它们来理解我的意思。这样可以确保正确设置线程局部存储。我曾使用过一个两步流程,需要先向应用程序预注册正在创建的窗口,然后再创建它,结果我经常忘记执行其中一个步骤。新的流程解决了我的健忘问题。
好了,关于 DWinLib
就先说到这里!
希望以上内容能传达出从事此类项目时的一些沮丧和喜悦。我会再做一遍吗?是的,如果必须从头开始。但是,如果微软完成了他们的 C# 原生代码编译器 工作,我会选择用 C# 来完成这项任务(前提是编译器包含在 Express 版本中)。拥有这样一个库触手可及,使用该框架将变得轻而易举。但事已至此,我对此感到满意。至于扁平化的 Metro 界面,我确定如果需要,可以为其进行改造,但我一直不太喜欢它们的扁平化。也许有一天我会改变主意,但如果您决定添加此功能,请务必发布您的修改!
祝您编码愉快!
DWinLib 替代方案
Windows 特定产品
- .NET - 微软的杰作,用它编写的程序在运行时会被解释执行。这需要安装 .NET。据我所知,要获得最高的生产力,您需要 Visual Studio,尽管 Borland 曾经有一个围绕它的编程环境,后来似乎被 Embarcadero Technologies 继承了。 Mono 是创建和执行 .NET 代码的另一个选择。
- MFC - 几乎所有框架的“祖母”。要在实际应用程序中使用 MFC,需要购买(已停产?)300 美元的 VS 版本才能获得资源编辑器。如果您想静态链接 MFC,则需要 800 美元的 VS 版本。(我对这些价格不太确定,因为 Visual Studio 的定价结构在 2010 版之后发生了变化,标准版似乎不再提供。)
- Visual Class Library (VCL) - 曾经,您可以花 50 美元购买 Borland 的 Personal Edition,但现在不行了。最低入门价似乎是 $199。Borland 曾给我留下了糟糕的印象(这也是
DWinLib
诞生的原因)。我不会让这种情况再次发生。(Embarcadero 现在似乎非常重视他们的产品,所以如果您对这条路线感兴趣,可以忽略我的牢骚。C++ Builder 编程起来非常有趣,我很高兴它是我的第一个 C++ 工具。)
Windows 特定框架
- Visual Component Framework (VCF) - Jim Crafton 和他的团队在这个项目上付出了巨大的努力。它包含了线程库、图形库以及大量的其他东西。甚至还有快速应用程序开发框架的苗头。它也兼容跨平台。但我找不到关于其内部工作原理的全面概述。
- Relisoft 的通用 Windows 包装器 - 我早期学习创建 Windows API 包装器的地方之一。
- Oluseyi Sonaiya 的窗口包装器 - 另一个资源。
- David Nash 的 SDI 框架 - Nash 在这个领域的贡献令人印象深刻,因为他的创建现在支持 MDI!另外,David 是个很棒的人,我不是因为名字熟悉才这么说的!(他帮助我理解了 Windows
LRESULT
数据类型的复杂性,并极大地指出了使DWinLib
跨编译器兼容所需要的条件。即,他完成了从 Borland C++ Builder 到 Visual Studio 的初始移植,对此我深表感谢。) - Win32 通用包装器 - 但该网站似乎并没有展示它是如何实现的。此外,要实现它的大部分技巧,还需要高级模板魔法。
- WTL - 适合那些对模板欲罢不能的人。它的支持者们赞不绝口。
(我相信上面列出的所有 Windows 特定框架都要求您自己分配和处理控件 ID,如果您想处理子控件。否则,您需要开发自己的解决方案来更轻松地完成这项工作。DWinLib
没有这个要求。关于 WTL,我可能错了。)
跨平台
- GTK - 自我初次审阅以来,它已发生巨大变化,因为我曾经说过 他们的示例在面向对象的意义上并非“面向对象”。我现在不能再这么说了。
- wxWidgets - 我比大多数都更喜欢它,因为 其设计似乎相当简洁。不幸的是,我很久以前运行过一些基于它的程序,其中一些“控件”并没有按我预期的那样工作。当我联系作者时,他告诉我问题出在 wxWidgets 上,无法克服。我不知道这些怪癖是否已修复,但考虑到时间已经过去,我猜它们应该已经修复了。)
- Qt - 据我所知,这是最简洁的替代方案之一,但您必须链接 Qt DLL 才能使用 LGPL 许可证。所有其他许可选项都很昂贵,除非您将程序完全开源。
历史记录
对于任何对 changelog
感兴趣的人,以下是修改的比较全面的列表
6.04 - 2021 年 1 月 16 日
- 在我的 MIDI 排序程序中添加了一个简单的双耳节拍创建机制,进行了大量更改。代码清理,向回调机制添加 lambda 函数,改进了菜单,大大简化了错误字符串单元,添加了可以自定义颜色的滚动条(未包含在 DWinLib 目录中,但可以在 fractalBrowser 示例 中找到),并重新设计了
dwl::ControlWin
控件,使其全部使用相同的基本winProc
,极大地简化了它们,并使其易于添加事件(如果需要)。(例如,请参阅EditBoxBase::wKillFocus
中如何使用onKillFocusC
。)将DC
等一些swc
项目放入全局命名空间,以减少函数签名中的击键次数。
6.03 - 2017 年 12 月 5 日
- 小错误更新。在更新
MEdit
时没有保留DWinLib
的更改日志,但我知道消除了几个小错误。所有示例程序都可以重新编译而无需任何更改。
6.02 - 2014 年 12 月 9 日
- 如前所述,已创建 MDI 和 SDI 库,可以轻松地在项目中进行使用,以减少编译时间。
- 从现在开始,“6.02”和任何将来的版本号将不再反映在工作项目的子目录结构中,仅供参考。
- 将
WinMainO
单元更改为MainAppWin
,因为该名称更具描述性,并且没有只有我才知道的背景故事。gWinMain
已更改为gMainWin
以支持此修改。 - 添加了“项目构建和布局注意事项”部分。
- 将一个旧类“
Timer
”(现在称为“Timer_Cpp
”)放入Timer
文件中,以避免与我创建的Windows
定时器类发生冲突,使其拥有一个合适的家。它位于utils
命名空间中。 - 将前面提到的
Timer
类更改为Timer_Win
,以便Timer_Cpp
与Timer_Win
相比成为一个合乎逻辑的名称。 - 向
FloatWindow
构造函数和DockWindow::instantiate
添加了helpTopic
int 参数。还在DockWindow
构造函数中添加了wUseDefProcInMdiC = true
,并结合其他一些修改,停靠窗口现在可以正确响应 F1 键。
6.01 - 2014 年 10 月 27 日
主要
- GDI 对象现在可以更轻松地管理,如GDI 对象部分所述。
- 将
dwl::DockWindows
从 SWC 停靠窗口的副本更改为使用非标签式方法,每个窗口都是自己的容器。(这与MEdit
的行为方式相同,并且现在继续保持此行为,并进行了一些改进!)
次要
如果您曾大量使用早期版本的 DWinLib
,以下内容可能会引起您的兴趣。我不敢保证我记录了所有内容,但这个列表无疑能让您了解使事情正常工作所需的努力,以及良好的重构可能需要的细微之处。
- 修复了一些资源管理项,例如
swc::FloatingWindow::drawFrame
出于某种原因多了一个delete brush
行。 - 从
dwl::Strings
中删除了一个临时的stringEnumC
。 - 添加了早期版本中存在的旧类。许多类被放入了
dwl
命名空间dwl::WinDialogs
dwl::Scrollbar
dwl::IniFile
dwl::ModalBase
dwl::Button
dwl::ComboBox
dwl::EditBoxBase
dwl::EditIntBox
dwl::TextBox
dwl::RadioButton
dwl::CallbackForwarder
dwl::CallbackWin
dwl::ProgressBar
dwl::WinCriticalSection
dwl::RegistryManipulator
dwl::ToolTip
- 此类包含用于工具提示处理的整数命令和字符串。dwl::ToolTips
- 此类包含一个整数和字符串的map
,用于处理工具提示通知。以上两类一起用于处理应用程序的工具提示。在
MEdit
中,gDwlMainWin
有一个ToolTips
对象,工具栏有一个ToolTip
(没有“s”)unique_ptr
的vector
,用于填充ToolTips
对象。这样,当工具提示超出范围时(例如,通过销毁工具栏),ID 就会从处理中移除。以下是用于示例的简短代码//In the toolbar definition: class Toolbar : public dwl::ToolbarControl { private: //... std::vector<std::unique_ptr<dwl::ToolTip>> tooltipsC; //... //In the corresponding cpp file: tooltipsC.push_back(make_unique<tooltip>(gWinMain->newPerfCB.id(), _T("Create a new composition")));</tooltip>
通过查看代码,您可以了解它如何与
dwl::BaseWin::wNotify
和dwl::Application::tooltipsC
交互。
dwl::Undoer (原名为 'DwlUndo')
- 值得注意的是,它已配置为使用您必须自己定义的::Application
单元,而不是dwl::Application
。(MEdit 中所有核心的非 UI 逻辑都包含在该单元中,我认为在其他应用程序中使用相同的方法是有意义的,但我并不强迫您这样做。)utils::Timer
utils::DllWrap
- 将所有名为
WindowProc
和winProc
的过程更改为winProc
以保持一致性。 - 重命名了以下内容,以便用户代码可以使用原始名称
gGlobals
到gDwlGlobals
gApplication
到gDwlApp
- 将
MdiWindow
更改为AppWindow
。这是一个非 DWL 核心文件。换句话说,它是特定于 MDI 程序的应用程序。唯一受影响的示例程序是SwcRebarTest
,因为其他 MDI 示例直接使用了MdiBaseWin
类,而不是从中派生AppWindow
。 - 在
MainAppWin
/dwl::MainWin
中消除了updateWindow
静态函数方法。这是通过DWinLib
主窗口而不是MainAppWin
进行停靠窗口的遗留物。取而代之的是,在dwl::MainWin
中添加了一个虚拟update
函数。尽管在设计中停靠窗口与主窗口相关联,但就静态库而言,它们是DWinLib
库的一部分。为了避免MainAppWin
成为库依赖项,必须使用虚拟函数。 - 修改了框架,以便将停靠容器传递指向停靠管理器(dock manager)的指针,这样
dwl::MainWin
就无需包含cd::any dockManagerC
成员。dwl::MainWin
、TabbedWindowContainer
和FloatingWindow
是唯一受此影响的单元。 - 将
WinMain
更改为_tWinMain
,并相应地更改了LPSTR
/LPTSTR
。(这是早期版本中的一个疏忽。)我认为这使得DWinLib
在MinW
下无法按原样编译,但我不确定。在这种环境中修改为使用TCHAR
应该不难? - 将示例项目的入口点文件名更改为反映项目,而不是与
DwlPwcStudio
相同的文件(它们是纯副本)。 - 修复了杂项项目,例如消除了之前编译时从未出现过的“强制 X 为 bool”的性能警告。
- 对
swc::DC
进行了额外的次要修订- 将
dcC
设置为private
,并通过operator()
访问。 swc::DC
过去包含一个静态的halfGrayBrush
。我将其删除,并创建了一个接受Brush::Pattern
枚举的Brush
构造函数来实现此目的。原因在于,使用旧的static
方法需要记住在使用创建的画笔时使用delete
。
- 将
- 在
ImageControl
中添加了通过string
而不是通过数字或资源插入和检索图像的方法。这些不应用于最初没有string
s 设置的imagelist
,尽管在非常小心的情况下也可以使用。这些例程是void ImageControl::addImage(wString str)
和int ImageControl::imagePos(wString str)
。 - 修改了程序入口的
try/catch
块,使其按预期工作。从逻辑上讲,此时无法重新进入app->run()
例程,并且初始示例并未反映这一点。此外,测试表明abort()
会意外挂起程序,但exit(EXIT_FAILURE);
会产生预期的中止结果。 - 将标签式窗口容器的构造函数更改为接受 id 向量,而不是在
instantiate
中分配。这涉及到该类的一些更改以及至少一个示例:DwlPwcStudio
。 - 整理了示例中的图标。
- 使项目负责命名所有资源。现在必须将资源 ID 传递给 DWL 类,而不是 DWL 类包含
#include 'DwlResources.h
。希望这最终能使使用它们不那么令人困惑,因为函数签名表明了类所需要的东西。 - 充分利用了命名空间,一旦我弄清楚它们如何消除了在某些地方使用不方便名称的需要。最好的例子是
MainMenu
代码:有一个dwl::MainMenu
,应用程序的MainMenu
从它派生,即ui::MainMenu
。(这可能有点过头,因为没有必要保持全局命名空间的纯净,但这只是一个例子。不过,我已经采用了这种方法,因为它使我在类视图中更容易找到东西。) - 此外,关于命名空间,我已将 DwlUtilities 文件中的内容移至其自己的逻辑文件中(位于
utils
命名空间中,例如所有string
函数,现在位于 StringUtils.h 中,并放置在utils::strings
命名空间中。) ChildrenEnumerator
类已重命名为ChildEnumerator
。- 将
createWindow
调用(现在称为instantiate
)的返回类型从bool
改为void
,因为据我所见,所有问题都会导致某种形式的异常,并显示一个MessageBox
来显示错误。 DWinLib
最初在dwl::Control
中有height()
和width()
函数,但我将它们修改为winHeight()
和winWidth()
,并添加了clientHeight()
和clientWidth()
以更加明确。我怀疑在许多情况下,对于无边框窗口,它们是等效的,但阅读一些代码让我挠头,然后我进行了更改,希望我不会再挠头了。关于最后一点,在处理
MEdit
中的滚动条时,我发现在某些情况下最简单的方法是处理通过WM_WINDOWPOSCHANGED
传递到窗口过程的WINDOWPOS
尺寸,因此我向dwl::BaseWin
添加了一些变量来存储它们。它们是wpTopC
、wpLeftC
、wpWidthC
和wpHeightC
。它们可以通过wpTop()
、wpLeft()
等访问,尽管它们也是protected
成员,所以您也可以这样使用它们。在其他情况下,它们也很有用,例如工具提示的大小调整,但可能还有其他方法可以实现我在MEdit
中所做的事情。dwl::ControlWin
进行了轻微的改动,因为在阅读和尝试修复我的dwl::MinWin
被合并以使MEdit
工作时出现的错误时产生了混淆。我相信这是试图更好地合并 SWC 和DWinLib
在控件窗口中的方法的结果,但我忘记了细节,只记得那花了几个痛苦的几个小时。- 将绝大多数
createWindow
例程重命名为instantiate
,因为这似乎更能描述情况,并且更适用于那些情况。换句话说,在重构MEdit
时,我修改了程序,使得构造函数中很少有东西可能抛出异常,如主文章所述,这导致我在这些情况之后调用instantiate
,即使它们不是窗口。因此,instantiate
成为我词汇表中描述构造函数调用后发生情况的标准函数。(dwl::Application
仍然有一个createWindow
例程,它实际上负责调用CreateWindow
。) - 在 SWC 重构项目中,我将
SwcTabbedContainer
类重命名为swc::TabbedMainWindowContainer
。让SwcTabbedContainer
从SwcTabbedWindowContainer
派生是没有意义的,而这个更改更好地描述了情况。这减少了混乱,这总是好的。该类被添加到DWinLib
,并且 DwlPwcStudio 示例被修改为正确使用它来处理 SDI 应用程序。(由于时间原因,我改用了一个空白窗口。) - 我将 SWC 继承的
DockEnum
enum
分拆开,并将其重构为具有非styleC
名称的逻辑枚举。我的第一次尝试包括以下内容,我认为我成功了enum DrawGripperWhen : uint32_t { Docked = 1, Floating = 2, }; enum DrawBorder : uint32_t { //The following must be set to the same as BF_TOP, BF_LEFT, BF_BOTTOM, & BF_RIGHT //in order to call ::DrawEdge successfully: OnLeft = 1, OnTop = 2, OnRight = 4, OnBottom = 8, }; //In DwlDockWindow.cpp, constructor: //... drawBorderC(s_cast<DrawBorder>(OnTop | OnBottom)), drawGripperWhenC(s_cast<DrawGripperWhen>(Docked | Floating)), //...
但逻辑 OR 与
static_cast
结合使用在位操作方面出了问题。所以目前的解决方案是以下结构//In 'DwlSwcEnums.h': class EnumBitFieldBase { //It is assumed that derived classes will have a public enum in them, that //user code can use. For an example, see the following 'DrawGripperWhen' //and 'DrawBorder' derivations. protected: DWORD bitFieldC; public: EnumBitFieldBase() : bitFieldC(0) { } EnumBitFieldBase(DWORD value) : bitFieldC(value) { } void value(DWORD val) { bitFieldC = val; } DWORD value() { return bitFieldC; } void operator=(DWORD val) { bitFieldC = val; } bool operator|(DWORD val) { return (bitFieldC | val) ? true : false; } bool operator&(DWORD val) { return (bitFieldC & val) ? true : false; } DWORD operator()() { return bitFieldC; } //If the following is uncommented, //it will clash with 'bool operator&(DWORD val)' //Therefore use the 'operator()()' to compare bit fields, like //"drawBorderC() | BF_ADJUST" to send a bit pattern to a function, //or "if (drawBorderC & DrawBorder::OnTop)" to do a comparison. //operator bool() { // return bitFieldC ? true : false; // } }; class DrawGripperWhen : public EnumBitFieldBase { public: enum { Docked = 0x1, Floating = 0x2, Force32 = 0x7FFFFFFF }; DrawGripperWhen(DWORD val) : EnumBitFieldBase(val) { } }; class DrawBorder : public EnumBitFieldBase { public: enum { //The following must be set to the same as BF_TOP, BF_LEFT, //BF_BOTTOM, & BF_RIGHT //in order to call ::DrawEdge correctly: OnLeft = 1, OnTop = 2, OnRight = 4, OnBottom = 8, Force32 = 0x7FFFFFFF }; DrawBorder(DWORD val) : EnumBitFieldBase(val) { } }; //And use it like: DrawBorder drawBorder(DrawBorder::OnTop | DrawBorder::OnBottom); ... if (drawBorder & DrawBorder::OnTop) doSomething();
使用
OnTop
、OnBottom
、Docked
和Floating
有点繁琐,但它有效。 - 修复了几个关于 rebar 的第五和第六个问题,我在文本中没有提及。实际上,问题出在图像列表的绘制上,它没有正确响应以反映鼠标焦点和禁用按钮。我可能在重新创建 SWC 的原始逻辑时引入了它们 - 我不知道。但它们似乎已修复!
- 在重新开发
DWinLib
的后期,我注意到 SWC 重构示例中存在一个 bug:如果将帮助和资源容器组合在一起,然后浮动(使它们都在一个窗口中),然后重新停靠到左侧(或任何其他侧面,可能),程序最终会崩溃。简要研究一下,问题与 SWC 工具提示有关。不知何故
origProcC
变得损坏,并且ControlWin::winProc
在尝试调用它时失败:return CallWindowProc(win->origProcC, hwnd, msg, wParam, lParam);
目前,我只是注释掉了实例化工具提示的三行。它们位于
swc::TabbedWindowContainer::wCreate
中。DWinLib
在纳入 SWC 之前就已经有了一个工具提示机制,我正在使用它。dwl::Application::tooltipsC
、Tooltip
和Tooltips
类以及BaseWin::wNotify
例程将让您了解其工作原理,但基本上,整个程序在Application
单元中有一个Tooltips
的vector
,它保存必要的string
s,当 Windows 发送一个WM_NOTIFY
消息请求工具提示文本时,BaseWin::wNotify
例程会启动返回string
的过程。子项,例如工具栏,包含它们自己的
Tooltip
(末尾没有“s”)的vector
,并且这些工具提示项负责在创建时将文本注册到Tooltips
,并在销毁时取消注册。例如,这是处理MEdit
中“新建文件”按钮的行tooltipsC.push_back(make_unique<ToolTip>(gWinMain->newPerfCB.id(), _T("Create a new composition")));
当我有更多时间时,我可能会更深入地研究原始问题。
- 一切就绪后,并非如此。
MEdit
中的所有内容在调试模式下都能正常工作,并且我早期的发布模式测试也如此,但在完成最终调整后,MEdit
在发布模式下(但在调试模式下不会)执行了部分(并非全部)菜单回调后触发了一个断点。这些不是我设置的断点;它们显然与 Windows 本身有关。经过几个小时的调试,发现问题在于我在处理
WM_COMMAND
后返回了DefWindowProc
(或DefFrameProc
)调用。将返回值更改为 '0
' 解决了问题,正如微软所记录的那样。为什么它从未在调试模式下出现令人费解,但我将解释留给“魔法咒语”。