一行代码查找和显示 Win32/COM 错误字符串






4.96/5 (40投票s)
2005 年 12 月 31 日
10分钟阅读

152189

1572
使用编译器 COM 支持(即使在非 COM 应用程序中)也能在一行代码中获取 Win32 错误代码或 HRESULT 的消息。
图 1. 本文的 SimpleErrors 示例应用程序。
引言
在以前的项目中,您可能调用过 Win32 或 COM 函数。本文不讨论调用这些函数的具体细节,而是揭示一种处理这些函数错误的非常简单的方法。大多数函数本身只返回一个难以解析的 `HRESULT`(来自 COM)或一个无效的 `HANDLE` 值(例如 `INVALID_HANDLE_VALUE` 或其他类似值)。但是,要获取实际发生的错误以便显示给用户并不容易。
Code Project 上的其他文章,例如 Ajit Jadhav 的文章“不要调用 GetLastError()!”以及其他文章,提倡一种简单、面向对象的处理这种糟糕情况的方法。事实证明,还有一种更简单的方法,它可以在一行代码中查找错误值、解析它,并显示相应的字符串(如果找到的话)在一个消息框中。这与编译器 COM 支持有关,而——事实证明——即使您的应用程序中根本不使用 COM,您也可以使用它。
根据您的应用程序需求进行操作
本文下面的几位发帖者提出了非常有见地的评论,我欢迎对这种方法的所有反馈。使用这种方法,尤其是 `_com_error`,的一些(不限于)注意事项列在下面的消息板帖子中。
本文无意提倡这种方法而排斥其他所有方法,也并非说没有更好的方法。但我只是在这里提出这个方法,特别是对于那些不想每次调用 Win32 函数时都编写大型 `switch` / `case` 或 `if` / `else` 块,而是想做一些更高级处理的人来说。
基本上,要传达的信息是:这是 VC++ 的另一个可能有用的功能;但最终,最重要的是根据您特定应用程序的实际情况进行最合理的处理。
_com_error 类
编译器 COM 支持及其提供的类的详细信息超出了本文的范围。不用说,有一个名为 `_com_error` 的类,您可以使用它来替代其他一些糟糕的方法,例如使用 `FormatMessage()`。Jadhav 试图通过提供一个 `CWin32Error` 类来解决这个问题,该类甚至可以作为异常对象抛出,它据称完成了编译器 COM 支持实际上已经提供给您的功能!
我将留给您查阅文档中的 `_com_error`,但不用说,该类在其构造函数中有一个构造函数,该构造函数接受 **either**:一个 `HRESULT` **or** Win32 函数 `GetLastError()` 的输出。然后,您可以调用成员函数 `_com_error::ErrorMessage()` 来自然地获取相应的系统错误消息。**另外,** `_com_error` 类本身就是一个异常类,并且与编译器内置的异常处理语法一起工作。您可以随心所欲地使用 `_com_error` 来进行 `throw`、`catch` 等块。不幸的是,异常处理不是我的专长,因此本文只关注如何使用 `_com_error::ErrorMessage()` 成员函数。
从 Jadhav 文章中的消息板帖子可以清楚地看出,人们意识到了这一点,但实际上使用它非常简单,并且它经过了可靠的验证,正如包含的演示项目和源代码所说明的那样。让我带您轻松地使用 `_com_error` 类。使用 `_com_error` 原生函数的另一个好处是,系统错误消息会自动本地化,以便以计算机区域设置使用的语言显示。
但是,我是否必须使用 COM 才能使用编译器 COM 支持?
简短的回答:否。如果该类不依赖于此类,则不需要。幸运的是,`_com_error` 不会。
在应用程序中使用 _com_error
为了说明如何在应用程序中使用 `_com_error` 类,我使用 MFC AppWizard 创建了一个小型、基于对话框的示例程序,名为 SimpleErrors。例如,**列表 1** 显示了用于根据 Windows Win32 SDK 系统错误(由 `GetLastError()` 返回)显示消息框的唯一一行代码。
AfxMessageBox(_com_error(GetLastError()).ErrorMessage(),MB_ICONSTOP);
**列表 1**:使用消息框向用户发出 Win32 错误的警报。
**列表 2**:显示用于从 `FAILED HRESULT` 获取错误消息的单行代码。
AfxMessageBox(_com_error(hResult).ErrorMessage(),MB_ICONSTOP);
**列表 2**:使用消息框向用户发出 COM 错误的警报。
这一切都是 Visual C++ “免费”提供的!以下是步骤。
步骤 1:向 STDAFX.H 添加代码
首先,我们需要确保将适当的包含文件添加到 *STDAFX.H*。如果您没有编写 MFC 应用程序,请将下面这行添加到所有源文件中都包含的文件中。无论如何,要添加的行如下面**列表 3** 所示。
#include <comdef.h> // Compiler COM support
**列表 3**:包含编译器 COM 支持和 `_com_error` 的正确头文件。
步骤 2:向应用程序添加 _com_error 调用
在 SimpleErrors 示例应用程序中,我添加了两个按钮:一个按钮执行一个伪造的 Win32 `CreateFile()` 调用;另一个按钮在不存在的接口(自引用接口 `IAmDumb`)上执行伪造的 `CoCreateInstance()` 调用。让我们来了解一下每个按钮的消息处理程序。**列表 4** 显示了我如何设置伪造的 Win32 调用。
void CSimpleErrorsDlg::OnBnClickedCallFunction() { // Do something bogus here, like try to open a file // which doesn't exist. if (CreateFile(_T("./testing.TXT"), 0L,FILE_SHARE_READ,NULL,OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,NULL)==INVALID_HANDLE_VALUE) { AfxMessageBox( _com_error(GetLastError()).ErrorMessage(),MB_ICONSTOP); } }
**列表 4**:使用 `_com_error::ErrorMessage()` 处理文件打开错误。
我上面的方法只是调用 `CreateFile()` 来打开不存在的文件 *testing.TXT*。请注意错误处理是如何在一行内完成的。以调用 `AfxMessageBox()` 开头的行在单击**图 1**中的 **调用 Win32 函数并获取错误** 按钮时,会显示以下消息框,如下面的**图 2** 所示。
**图 2**:**列表 4** 中的 `_com_error::ErrorMessage()` 调用产生了此消息框。
调用 COM 函数时,情况也类似。现在,对于 COM,我们调用 `CoCreateInstance()`。有关 COM 的更多信息以及如何在程序中使用 COM,我将仅参考 Michael Dunn 的精彩介绍。
- Michael Dunn:COM 简介:它是什么以及如何使用它
- Brian Hart:DCOM D-Mystified:DCOM 教程 - 好吧,好吧,我是在吹嘘自己。
- 更多精彩 COM 文章(点击此处)
总之——抛开所有介绍——这就是处理 COM 函数失败的方法。现在,对于 Win32,我们可以简单地向函数传递一个不存在的文件名,这样我们就完成了。但是在这里,我们创建了一个伪造的接口,接口有标签或 GUID(全局唯一 ID),它们告诉 COM 您想要哪个接口。我们在这里传递给 `CoCreateInstance()` 的是所谓的 CLSID 和 IID。不要在意这些是什么;同样,超出了本文的范围。我准备调用 `CoCreateInstance()` 的第一步是使用 Visual C++ 附带的 Create GUID 工具来创建两个 GUID,一个用于 CLSID,一个用于我的伪造接口的 IID。这里是我生成的 GUID,显示在**列表 5** 中。
// {9346460E-F860-450c-B8C6-80D705644FF0} static const GUID CLSID_IAmDumb = { 0x9346460e, 0xf860, 0x450c, { 0xb8, 0xc6, 0x80, 0xd7, 0x5, 0x64, 0x4f, 0xf0 } }; // {5E0E7ED9-83FF-4c31-AD12-46021DE03884} static const GUID IID_IAmDumb = { 0x5e0e7ed9, 0x83ff, 0x4c31, { 0xad, 0x12, 0x46, 0x2, 0x1d, 0xe0, 0x38, 0x84 } };
**列表 5**:我为伪造的、虚假的 `IAmDumb` 接口创建的 GUID。
如果愿意,您可以直接从上面的**列表 5** 中复制 GUID 到您的项目中(如果您正在跟随)。将这些 GUID 声明放在我要调用 `CoCreateInstance()` 的同一源文件中的最佳位置。
**注意**:这 **不是** 通常访问 COM 接口的方式!有关更多详细信息,请参阅 Dunn 的文章(或其他文章)。
再次提醒大家我们的目的。请记住,我们想了解如何轻松捕获错误,特别是来自 `HRESULT` 的错误。因此,我创建了一个 **伪造的** 接口 `IAmDumb`,然后我将调用此伪造的、未注册接口上的 `CoCreateInstance()`,以便获得一个错误来显示。请参阅下面的**列表 6**,了解我如何做到这一点。
void CSimpleErrorsDlg::OnBnClickedCallCom() { // Call a random COM function, // say, CoCreateInstance(), with // bogus parameters so that it // gives us a FAILURE HRESULT IUnknown* pUnk=NULL; // Always must be called before // using COM functions CoInitialize(NULL); HRESULT hResult = CoCreateInstance(CLSID_IAmDumb,NULL, CLSCTX_LOCAL_SERVER,IID_IAmDumb,(void**)&pUnk); if (FAILED(hResult)) { AfxMessageBox(_com_error(hResult).ErrorMessage(),MB_ICONSTOP); } CoUninitialize(); // Call so Windows can clean up }
**列表 6**。调用 `CoCreateInstance()` 来尝试获取一个伪造的接口,并处理由此产生的错误。
同样,我在这里不是为了解释 COM。如果您需要有关我所做事情的帮助,请参考上面的 Dunn。注意 `FAILED()` 宏的使用。它可以用于任何 `HRESULT` 值,并且通常是判断 COM 调用是否不成功的良好方法。如果您想了解更多信息,请查阅文档中的 `FAILED`。上面的 `AfxMessageBox()` 调用会产生如下面的**图 3** 所示的对话框。
**图 3**:在我们的伪造接口 `IAmDumb` 上调用 `CoCreateInstance()` 的结果。
要跟随我并在示例应用程序中亲自查看这一点,请单击 **调用 COM 函数并获取错误** 按钮。同样,由于 `_com_error::ErrorMessage()` 为您提供的错误消息会自动本地化,因此您看到的将是该消息在您区域设置语言中的翻译(我想)。
讨论
值得讨论的是这种特定的错误处理方法,以及在某些情况下不适合使用它的场景。最重要的是,您应该使用您认为最适合您特定应用程序的方法。一位消息板发帖者 Doug Scmidt,也发帖评论了本文,他警告不要有试图在一行内完成所有事情的诱惑。确实,可能存在不适合此方法的应用程序类型。有关更多信息,请参见下面的 Schmidt 的帖子。
正如另一位读者提到的,`_com_error` 提供的系统错误消息非常简洁,而且确实如此。讽刺的是,以《Windows 用户界面指南》等书籍而闻名的微软——它严厉告诫读者要使错误消息有帮助,并且不要责怪用户——却在其 Windows 系统错误消息数据库中放入了这些极其简洁的错误。
克服简洁性问题的一种可能方法是向错误消息添加额外文本;即,也许创建一个 `GetErrorMessage()` 函数,该函数会以一种对用户更有帮助的方式格式化消息。我不会假设我知道该函数的实现方式,因为您应用程序的目的的实现可能会有所不同。然而,一个包含正在操作的文件名或接口名(或其他内容)、系统错误消息以及用户解决问题的方法的错误消息可能会很受欢迎。
同样值得讨论的是:在 Jadhav 的文章中,一些消息板发帖者声称 `_com_error::_com_error()` 总是需要一个 `HRESULT`,因此您必须如此将 `GetLastError()` 的输出传递给它
AfxMessageBox( _com_error(HRESULT_FROM_WIN32(GetLastError())).ErrorMessage(), MB_ICONSTOP);
**列表 7**:将 `HRESULT_FROM_WIN32` 宏应用于 `GetLastError()` 的所有输出。
对我个人而言,即使早在 VC5 时,这种情况也从未发生过,并且 `_com_error::ErrorMessage()` 似乎都可以正常工作,无论您是否使用 `HRESULT_FROM_WIN32` 宏。
我可以继续讨论这种方法的缺点,但这并不是我来这里的目的。相反,我很高兴如果读者能使用下面的文章消息板分享您对如何处理 Win32 和 COM 错误报告的看法和技巧。读者也被鼓励将帖子写给本文的普通读者,而不是写给我。让下面的消息板成为关于此主题的“论坛”。
结论
好了,就这些了。我们已经了解了如何使用 VC++ 编译器随附的“内置”类 `_com_error` 类,来从 COM 和 Win32 调用生成人类可读的错误。
祝您错误处理愉快(新年快乐 2006 - 我在 2005 年除夕写这篇文章!)!