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 编程变得更容易。
事后看来,我现在意识到这个方法是错误的。在学习 MFC 之前,我应该花时间更深入地研究 Windows API。一次只学习一个主题比同时学习两者要容易得多(也快得多)。从某种意义上说,我应该先学会走路再尝试跑步。编写 Win32 应用程序时我面临的两个主要挑战是:
- 为 Win32 程序带来面向对象的方法
- 使用专业的 UI 来构建 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);
我们可能会想将 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 map 中。具体方法如下:
步骤 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 map 中。
// 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 map 中提取指针,并使用它将消息处理重定向到适当的 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()
下一个代码片段处理窗口过程,它首先接收消息。我们从 map 中提取 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);
}
这里描述的方法使用一个全局 map 将窗口句柄 (HWND
) 与 CWnd
对象关联起来。此 map 使用线程局部存储 (TLS) 来确保窗口创建是线程安全的。如果没有使用 TLS,在不同线程中同时创建多个窗口可能会失败。使用 map 将窗口句柄 (HWND
) 与 CWnd
对象关联起来,还可以处理窗口的所有消息。例如,在使用此方法时,无需在 WM_NCCREATE
之前丢弃窗口消息。
历史
- 2005 年 3 月:版本 1.0
- 首次发布
- 2005 年 4 月:版本 2.0
- 添加了
CDialog
- 添加了
CWinApp
- 添加了
- 2005 年 12 月:版本 3.0
- 添加了跟踪功能
- 添加了对不同线程中的 Windows 的支持
- 2006 年 4 月:版本 4.0
- 使用 map 而不是窗口的用户数据来存储窗口的
CWnd
指针。这允许在创建窗口时将lpParam
参数用于用户数据。
- 使用 map 而不是窗口的用户数据来存储窗口的
- 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
可以从 SourceForge 的这里下载最新版本的 Win32++。
- 添加了
有什么新内容
Win32++ 使用 CDocker 支持停靠和分割窗口。在停靠时,未停靠的 docker 会被拖到另一个 docker 上。各种视觉提示,例如停靠目标(小的箭头状图像)和停靠提示(目标窗口的一部分变为蓝色),都会提供有关 docker 将停靠在哪里的线索。为了方便取消停靠,被停靠窗口的标题栏会被拖放。
几乎任何子窗口都可以用作 docker 的视图窗口。CContainer
类提供了一个专门用于 docker 的视图,它具有标签,并且还有一个可选的工具栏。
CTabbedMDI
类增加了对标签式 MDI 的支持。它们的功能与传统的多个文档界面 (Multiple Document Interface) 非常相似,但使用标签控件来管理各种 MDI 子窗口。
有关详细的修订更改列表,请参阅此处。
参考资料
- Charles Petzold 著《Programming Windows》
- Reliable Software 著《Reliable Software Win32 Tutorial》
- Oleg Pudeyev 著《Window Procedures as Class Member Functions》
- Microsoft Systems Journal 中 Paul DiLascia 关于菜单栏的文章
- Catch22 上的停靠技术描述:Catch22
- Dev-C++:BloodshedSoftware 提供的免费 C++ 编译器,可从此处下载
- Visual C++ 2008 Express:Microsoft 提供的免费编译器和 IDE,可在此处获取
- Win32 和 Win64 平台软件开发工具包,可在此处获取
- Code::Blocks:可从此处下载的免费 C++ IDE
- Win32++ 主页