Win32++:MFC 的简单替代方案






4.92/5 (186投票s)
一个简单的Windows框架,初学者和中级程序员可以将其作为MFC的替代方案。它使得学习Windows编程更加容易。
引言
Win32++是一个简单易懂的库,用于创建Windows应用程序。它可以在常用的免费编译器上运行,使其成为MFC的免费替代方案。
Win32++的设计旨在让那些学习直接使用C++和Windows API进行编程的人们的生活变得更轻松一些。Win32++不试图隐藏Windows API。相反,它暴露了Windows API,使其更容易学习和理解。对于那些寻求简单、健壮且高效的框架的专业程序员来说,Win32++也是一个不错的选择。
该代码旨在运行在各种C++编译器上,包括Microsoft、Borland以及GNU的免费MinGW编译器。Win32++支持所有Windows操作系统,从Windows 95到Windows 7。它可以用于创建32位和64位应用程序。
Win32++还直接支持Windows CE操作系统。Windows CE是运行在各种Pocket PC、智能手机以及工业设备和嵌入式系统上的操作系统。Windows CE API是Windows API的一个子集。它还包含一些为运行它的较小型计算机和设备量身定制的新通用控件。
Win32++为直接使用Windows API编程带来了面向对象的方法。每个创建的窗口都是一个C++类对象,能够拥有自己的窗口过程来路由消息。
背景
当我第一次尝试自学使用C++进行Windows编程时,我简单地看了一些网上的Win32教程,然后直接开始使用MFC。我曾希望使用MFC能使学习Windows编程的任务变得更容易。
事后看来,我认识到这种方法是错误的。我应该花时间更深入地学习Windows API,然后再转向MFC。一步一步地学习这两个主题会比同时学习两者要容易得多(也快得多)。从某种意义上说,我应该先学会走路再尝试跑步。我在编写Win32应用程序时面临的两个主要挑战是:
- 为Win32程序带来面向对象的方法
- 使用专业外观的用户界面构建Windows API程序
考虑到这一点,我决定重新审视我的Windows API编程,并为我的应用程序开发一个通用的框架,可以作为MFC的替代方案。我的目标是生产一个健壮、面向对象且能产生专业外观结果的框架。
框架概述
下图展示了Win32++中使用的类
定义框架本身的类包含在Win32xx
命名空间中。这些类如下:
CBitmapInfoPt
:一个类,用于简化BITMAPINFO
结构体的创建和使用,以便与GDI图形一起使用。CCmdbar
:一个在Windows CE上用于提供CommandBar
的类。它由Windows CE上的CFrame
使用。CContainer
:一个用于停靠器的专用视图窗口。它有标签和一个可选的工具栏。CCriticalSection
:此类为多线程应用程序提供线程同步。CDC
:一个代表设备上下文的类。它简化了使用Windows GDI的工作。CDialog
:负责创建模态和非模态对话框的类。它由CFrame
使用,也可以用于创建对话框应用程序。CDocker
:一个提供停靠和分割窗口支持的类。CFrame
:此类创建一个框架窗口,该窗口具有重叠条、菜单栏、工具栏和状态栏。框架窗口的客户区应由一个单独的CWnd
对象占用。CListView
:一个用于创建列表视图控件的类。CMDIApp
:此类继承自CWinApp
。你应该继承此类来启动MDI框架应用程序。CMDIChild
:这是用于MDI子窗口的类。每个MDI子窗口都应继承此类。CMDIClient
:这是Win32++内部用作MDI框架的视图窗口的类。CMDIFrame
:此类负责创建MDI框架窗口。它继承自CFrame
。CMenubar
:此类负责创建菜单栏。菜单栏是包含在重叠条控件中的菜单。CPoint
:此类可用于代替POINT结构。CPropertyPage
:此类为Win32++增加了属性页支持。属性页有一个或多个属性工作表。CPropertySheet
:此类代表一个属性页。它由CPropertySheet
使用。CRebar
:此类负责创建重叠条。它由CFrame
使用。CRect
:此类可用于代替RECT结构。CSize
:此类可用于代替SIZE结构。CSocket
:此类为Win32++增加了网络支持。CStatusBar
:负责创建状态栏的类。它由CFrame
使用。CTab
:一个用于创建选项卡控件的类。CToolBar
:负责创建工具栏的类。它由CFrame
使用。CTreeView
:一个用于创建树视图控件的类。CWceFrame
:一个为Pocket PC提供简单框架的类。它利用Commandbar
来显示菜单和工具栏按钮。CWinApp
:负责初始化框架的类,并提供我们的消息循环。你应该继承此类来启动框架。CWinException
:一个处理异常的类。CWnd
:负责窗口对象的类。它是更专业化的窗口对象(如CDialog
、CFrame
、CToolbar
等)的基类。
关于文件下载
从Sourceforge下载的文件包括以下内容:
- Win32++库本身
- 库的帮助文档
- 一系列教程
- 一系列示例应用程序
示例应用程序包括:
Browser
- 基于ActiveX控件的Internet浏览器应用程序。Dialog
- 一个简单的对话框应用程序示例。DialogDemo
- 一个演示滑块控件和进度条的交互式对话框应用程序。DialogTab
- 一个带有选项卡控件的对话框应用程序。DirectX
- 一个简单的DirectX应用程序。Dock
- 一个简单的停靠应用程序示例。DockContainer
- 一个包含容器的停靠应用程序示例。DockTabbedMDI
- 一个带有容器和选项卡式MDI的停靠应用程序示例。Explorer
- 一个类似Windows Explorer的应用程序。FastGDI
- 一个演示直接操作位图颜色的应用程序。FormDemo
- 框架内一个非模态对话框的示例。MDIFrame
- 一个简单的MDI框架应用程序。MDIFrameDemo
- 演示MDI框架的一些附加功能。Networking
- 演示网络的使用。Notepad
- 一个带有打印功能的简单文本编辑器。Performance
- 测量Win32++的消息处理速度。Picture
- 一个简单的图片渲染应用程序。PropertySheets
- 属性页的演示。Scribble
- 一个简单的绘图应用程序。Simple
- 创建一个简单的窗口。Splitter
-CSplitter
类的一个演示。StaticLibrary
- 将Win32++框架构建成一个静态库。TabDemo
- 演示在框架中使用CTab
控件。Themes
- 演示如何自定义重叠条和工具栏控件的颜色。Threads
- 演示多线程Windows。WinCE samples
- 一小部分Windows CE的示例。
使用框架
构成框架基础的代码位于Win32++目录中。您不需要修改这些文件,而是应该继承Win32++并添加任何额外的代码到您的派生类中。例如,要使用框架创建SDI框架窗口,您通常会从CFrame
派生自己的类,并将对标准框架的任何修改放在那里。您可以重写WndProc
成员函数来处理您希望处理的任何额外消息。
一个单独的视图窗口放置在框架窗口的客户区上方。通常,这个视图窗口是通过从CWnd
继承一个类来创建的。CFrame::SetView
函数用于将视图窗口分配给框架。然而,对于MDI框架,CMDIFrame
已经使用CMDIClient
作为视图窗口,您将使用CMDIFrame::AddMDIChild
来创建MDI子窗口的新实例。
直接使用Windows API编程的一个重要优点是生成的代码是可移植的,也就是说,它可以在不同的编译器上编译。本框架中的代码已经过兼容性检查,兼容Visual C++ 6.0、Visual Studio .NET 2003、Visual C++ 2008 Express Edition以及Dev-C++ 4.9.9.2版本。Dev-C++是一款免费的C++编译器和集成开发环境,可从此处下载。该框架还兼容Visual C++ Toolkit 2003(Microsoft的免费编译器)和Borland的免费Turbo C++ 2006。
一个提供使用框架的分步指南的教程包含在框架中。
面向对象的方法
将面向对象的方法引入直接使用Windows API编程的关键是拥有一个可以创建窗口的C++类,并且该类包含自己的窗口过程作为成员函数。一旦有了这个类,我们就可以继承它并重写窗口过程成员函数,以根据我们的需要为每个派生窗口类型处理消息。
创建这样一个类并非易事,我怀疑这也是MFC最初被创造出来的原因之一。问题源于在创建窗口之前需要注册“窗口类”的方式。这里的“类”指的是Windows API的“窗口类”,这与C++类不同。下面的代码片段展示了如何使用API注册一个窗口类:
WNDCLASSEX wc = {0};
wc.cbSize = sizeof(WNDCLASSEX);
wc.lpfnWndProc = WindowProc; //Window procedure function
wc.hInstance = hInstance;
wc.lpszClassName = "TEST";
wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
//Register the window class
::RegisterClassEx(&wc);
请注意,我们需要提供窗口过程的函数名。窗口过程是我们控制何时收到窗口消息时要执行的操作的地方。此函数必须严格符合Windows API规定的预定义标准。回调函数的典型声明如下:
LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg,
WPARAM wParam, LPARAM lParam);
我们可能会 tempted 将WindowProc
函数设置为类的成员。不幸的是,每个类成员函数都包含一个隐式的this
指针作为其参数之一,因此不能用作窗口的回调函数。如果我们这样做,我们的WindowProc
函数将不再符合预定义的标准,程序将无法编译。
我们可以将WindowProc
函数设置为类的static
成员函数。static
函数中没有隐式的this
,这样可以正确编译。然而,static
成员函数无法访问类对象(即它没有this
指针),也无法访问类的其他成员。正是这一点阻止了static
成员函数以面向对象的方式使用。下面的代码演示了static
成员函数方法的局限性:
class TestStatic
{
public:
int member;
void NormalFunction()
{
//We can access member variables in a normal
//member function
member = 5;
//The following line is equivalent to the one above
this->member = 5;
}
void static StaticFunction()
{
//We cannot access member variables
//in a static member function
//The following line will give a compile error
member = 5;
//This will give an error too
this->member = 5;
}
};
如果我们可以得到指向窗口类对象(我们的this
指针)的指针,那么用于窗口过程的static
成员函数将很有用。在窗口创建过程中,有多种技术可以用来获取我们指针的访问权限。我选择的方法利用线程局部存储(Thread Local Storage)来存储我们的指针,该指针随后会被插入到STL映射中。具体方法如下:
步骤 1:设置线程局部存储以存储我们的this
指针。这在CWinApp
类中完成。
CWinApp::CWinApp(HINSTANCE hInstance) : m_hInstance(hInstance)
{
if (GetApp() == 0)
{
st_dwTlsIndex = ::TlsAlloc();
//snip
}
}
步骤 2:当我们使用CreateEx
创建窗口时,将我们的this
指针存储在线程局部存储中。
// Ensure this thread has the TLS index set
TLSData* pTLSData = GetApp()->SetTlsIndex();
// Store the CWnd pointer in thread local storage
pTLSData->pCWnd = this;
步骤 3:在窗口的初始创建过程中,从线程局部存储中提取指针并将其添加到STL映射中。
// Retrieve the pointer to the TLS Data
TLSData* pTLSData = (TLSData*)TlsGetValue(GetApp()->GetTlsIndex());
// Retrieve pointer to CWnd object from Thread Local Storage TLS
w = pTLSData->pCWnd;
// Store the CWnd pointer in the HWND map
GetApp()->AddToMap(hWnd, w);
return w->WndProc(hWnd, uMsg, wParam, lParam);
步骤 4:对于每个后续的窗口消息,我们从STL映射中提取指针,并使用它将消息处理重定向到相应的WndProc
函数。
CWnd* w = GetApp()->GetCWndFromMap(hWnd);
return w->WndProc(hWnd, uMsg, wParam, lParam);
窗口创建详解
现在我们已经了解了线程局部存储和窗口过程,是时候看看它们如何结合起来创建窗口了。这是创建窗口的代码:
HWND CWnd::CreateEx(DWORD dwExStyle, LPCTSTR lpszClassName, LPCTSTR lpszWindowName,
DWORD dwStyle, int x, int y, int nWidth, int nHeight, HWND hParent,
HMENU hMenu, LPVOID lpParam /*= NULL*/)
{
try
{
// Test if Win32++ has been started
if (0 == GetApp())
throw CWinException(_T("Win32++ has not been initialised properly.\n
Start the Win32++ by inheriting from CWinApp."));
// Only one window per CWnd instance allowed
if (::IsWindow(m_hWnd))
throw CWinException(_T("CWnd::CreateEx ... Window already exists"));
// Ensure a window class is registered
TCHAR ClassName[MAX_STRING_SIZE] = _T("");
if (0 == lstrlen(lpszClassName) )
lstrcpyn (ClassName, _T("Win32++ Window"), MAX_STRING_SIZE);
else
// Create our own local copy of szClassName.
lstrcpyn(ClassName, lpszClassName, MAX_STRING_SIZE);
WNDCLASS wc = {0};
wc.lpszClassName = ClassName;
wc.hbrBackground = (HBRUSH)::GetStockObject(WHITE_BRUSH);
wc.hCursor = ::LoadCursor(NULL, IDC_ARROW);
if (!RegisterClass(wc)) // Register the window class (if not already registered)
throw CWinException(_T("CWnd::CreateEx Failed to register window class"));
// Ensure this thread has the TLS index set
TLSData* pTLSData = GetApp()->SetTlsIndex();
// Store the CWnd pointer in thread local storage
pTLSData->pCWnd = this;
// Create window
m_hWnd = ::CreateWindowEx
(dwExStyle, ClassName, lpszWindowName, dwStyle, x, y, nWidth,
nHeight, hParent, hMenu, GetApp()->GetInstanceHandle(), lpParam);
// Now handle window creation failure
if (!m_hWnd)
throw CWinException(_T("CWnd::CreateEx ... Failed to Create Window"));
m_hWndParent = hParent;
// Automatically subclass predefined window class types
::GetClassInfo(GetApp()->GetInstanceHandle(), lpszClassName, &wc);
if (wc.lpfnWndProc != st_pfnWndProc)
{
Subclass();
// Send a message to force the HWND to be added to the map
::SendMessage(m_hWnd, WM_NULL, 0, 0);
OnCreate(); // We missed the WM_CREATE message, so call OnCreate now
}
// Clear the CWnd pointer from TLS
pTLSData->pCWnd = NULL;
// Window creation is complete. Now call OnInitialUpdate
OnInitialUpdate();
}
catch (const CWinException &e)
{
e.MessageBox();
}
return m_hWnd;
} // HWND CWnd::CreateEx()
下一个代码段处理窗口过程,该过程首先接收消息。我们从映射中提取CWnd
对象的指针,并使用它将窗口消息的处理重定向到相应的WndProc
函数。
LRESULT CALLBACK CWnd::StaticWindowProc
(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
try
{
CWnd* w = GetApp()->GetCWndFromMap(hWnd);
if (0 != w)
{
// CWnd pointer found, so call the CWnd's WndProc
return w->WndProc(hWnd, uMsg, wParam, lParam);
}
else
{
// The CWnd pointer wasn't found in the map, so add it now
// Retrieve the pointer to the TLS Data
TLSData* pTLSData = (TLSData*)TlsGetValue(GetApp()->GetTlsIndex());
if (NULL == pTLSData)
throw CWinException(_T("CWnd::StaticCBTProc ... Unable to get TLS"));
// Retrieve pointer to CWnd object from Thread Local Storage TLS
w = pTLSData->pCWnd;
if (NULL == w)
throw CWinException(_T("CWnd::StaticWindowProc .. Failed to route message"));
pTLSData->pCWnd = NULL;
// Store the CWnd pointer in the HWND map
GetApp()->AddToMap(hWnd, w);
// Store the HWND in the CWnd object early
w->m_hWnd = hWnd;
return w->WndProc(hWnd, uMsg, wParam, lParam);
}
}
//snip
最后,下一个代码段显示了由StaticWindowProc
调用的函数。通常,当我们从CWnd
派生一个新类时,我们会重写此函数来控制各种窗口消息的处理方式。
LRESULT CWnd::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
// Override this function in your class derived from CWnd to handle
// window messages. A typical function might look like this:
// switch (uMsg)
// {
// case MESSAGE1: // Some Windows API message
// OnMessage1(); // A user defined function
// break; // Also do default processing
// case MESSAGE2:
// OnMessage2();
// return x; // Don't do default processing, but instead return
// // a value recommended by the Windows API documentation
// }
// Always pass unhandled messages on to WndProcDefault
return WndProcDefault(hWnd, uMsg, wParam, lParam);
}
这里描述的方法使用一个全局映射来关联窗口句柄(HWND
)和CWnd
对象。这个映射使用线程局部存储(TLS)来确保窗口的创建是线程安全的。如果未使用TLS,尝试在不同线程中同时创建多个窗口可能会失败。使用映射将窗口句柄(HWND
)与CWnd
对象关联起来,还可以确保处理窗口的所有消息。例如,使用此方法时,无需在WM_NCCREATE
之前丢弃窗口消息。
历史
- 2005年3月:版本 1.0
- 首次发布
- 2005年4月:版本 2.0
- 添加了
CDialog
- 添加了
CWinApp
- 添加了
- 2005年12月:版本 3.0
- 添加了跟踪功能
- 添加了对不同线程中的Windows的支持
- 2006年4月:版本 4.0
- 使用映射代替窗口的用户数据来存储窗口的
CWnd
指针。这允许在创建窗口时将lpParam
参数用于用户数据。
- 使用映射代替窗口的用户数据来存储窗口的
- 2006年12月:版本 5.0
- 添加了
CRebar
、CMenubar
、CSplitter
- 添加了MDI框架支持
- 添加了属性工作表支持
- 添加了消息反射
- 添加了自动子类化
- 添加了多语言支持
- 为菜单图标添加了图标
- 为重叠条、工具栏和菜单添加了皮肤
- 添加了
- 2008年3月:版本 6.0
- 添加了Windows CE支持
- 添加了
CSocket
以支持网络 - 添加了
CDC
以简化使用Windows图形设备接口(GDI) - 增强了教程。现在还演示了文件操作和打印。
- 2009年11月:版本 6.7
- 添加了
CPoint
、CRect
和CSize
- 添加了
CListView
和CTreeView
- 框架应用程序现在将设置保存在注册表中
- 添加了带容器的停靠支持
- 添加了
CTab
和CTabbedMDI
最新版本的Win32++可以从SourceForge的此处下载。
- 添加了
有什么新内容
Win32++使用CDocker来支持停靠和分割窗口。在停靠时,未停靠的docker会被拖到另一个docker上方。各种视觉提示,例如停靠目标(小的箭头状图像)和停靠提示(目标窗口的一部分变成蓝色),会提示docker将停靠在哪里。为了方便取消停靠,被停靠窗口的标题栏会被拖动和放下。
几乎任何子窗口都可以用作docker的视图窗口。CContainer
类为docker提供了专用视图,该视图带有标签,并且还有一个可选的工具栏。
CTabbedMDI
类增加了对选项卡式MDI的支持。这些功能非常类似于传统的多个文档界面,但使用选项卡控件来管理各种MDI子窗口。
有关详细的修订更改列表,请参阅此处。
参考资料
- “编程Windows” 作者:Charles Petzold
- “可靠软件Win32教程” 作者:Reliable Software
- “窗口过程作为类成员函数” 作者:Oleg Pudeyev
- Paul DiLascia在Microsoft Systems Journal上关于菜单栏的文章
- Catch22网站上关于停靠技术描述
- Dev-C++:来自BloodshedSoftware的免费C++编译器,可从此处下载
- Visual C++ 2008 Express:来自Microsoft的免费编译器和IDE,可从此处获取
- Win32和Win64平台软件开发工具包,可从此处获取
- Code::Blocks:一个免费的C++ IDE,可从此处下载
- Win32++主页