65.9K
CodeProject 正在变化。 阅读更多。
Home

Windows 和 XML 注册表的简单堆栈包装器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.19/5 (13投票s)

2003年2月3日

9分钟阅读

viewsIcon

75592

downloadIcon

796

由我自己决定,我倾向于泄露句柄,让键开着太久,关闭并重新打开键太频繁,并总体上把事情搞得一团糟。坦率地说,你们大多数人也是如此(无意冒犯)。

动机

我发现使用 Windows 注册表容易出错。我倾向于泄露句柄,让键开着太久,关闭并重新打开键太频繁,并总体上把事情搞得一团糟。坦率地说,你们大多数人也是如此(无意冒犯)。除此之外,我喜欢给用户提供完全避免注册表并以 XML 形式存储配置信息的选项。当配置信息在多台机器之间共享时(例如,在共享驱动器上或通过 URL 访问),以及当代码在没有注册表的平台上运行时,这尤其有用。

为了我自己的方便,我为注册表创建了一个简单(甚至过于简单)的封装器,并强迫自己使用它。这并不容易。但是,最终,我发现这些小类是瑰宝,并认为在这里分享它们是合适的。

请注意,我最初创建此实用程序是为了只封装 Windows 注册表,后来才添加了 XML 支持。因此,当本文中出现注册表一词时,请记住我指的是注册表的概念,它可能存储在实际的 Windows 注册表或某些其他存储中(例如 XML 或数据库)。

基础:RegKey 和 RegValue,以及 RegDDX

使用此代码时,您将最常看到两个基本类

  1. RegKey 是入口点类。要实例化 RegKey,请调用与所需存储对应的静态成员函数(Windows 注册表为 RegKey::FromHKEY(),XML 为 RegKey::FromFile())。RegKey 是其他 RegKeyRegValue 对象的容器,可以使用其 Key()Value() 方法访问它们。
  2. RegValue 是值持有类,它公开了获取和设置值的接口。如果内部类型与请求的类型不同,RegValue 将尝试进行合理的转换(例如,通过 itoaint 转换为字符串)。RegValue 对象只能通过有效的 RegKey 访问。

除了上面两个基本类之外,文件 RegistryDX.h 包含两个类,用于帮助编写对话框:RegDDXRegDDXMethodCall。在运行时,这些类通过两步过程将控件中的值与注册表中的值同步

  • 当对话框启动时
    1. 首先,必须先将值从注册表读入中间值;
    2. 其次,可以使用 DDX 将中间值应用于控件。
  • 当对话框以肯定的方式关闭时(用户点击“确定”),操作以相反的顺序进行
    1. 首先,将控件读入中间值;
    2. 其次,将中间值发送到注册表。

RegDDXMethodCall 使用 C++ 中使用堆栈构造/析构反转的旧技巧,根据 CDataExchange::m_bSaveAndValidate 的值交换调用顺序。

数据验证应该在数据移入和移出控件时发生,而不是在数据移入和移出注册表时发生。尽管如此,也应该优雅地处理来自注册表的意外值(例如,当用户手动编辑某些内容时)。

用法详情

RegKey 隐藏了创建、打开和关闭键的复杂性,并在必要时管理 Win32 句柄。同样,RegValue 封装了注册表值的概念,并管理创建、写入和读取值。为简单起见,这两个类都设计为在堆栈上使用。基于堆栈的对象使内存管理不那么令人担忧,而且我通常发现输入“.”比输入“->”更容易。是的,我就是这么懒。

要使用这些封装器,您需要做三(或四)件事

  1. 使用 RegKey::FromHKEY()RegKey::FromFile() 创建一个 RegKey 实例。
  2. 通过访问适当命名的子键来选择要操作的注册表部分。
  3. 访问 RegValue 实例,通过名称读取或写入感兴趣的值。
  4. 如果注册表存储在 XML 文件中,您需要调用 Commit() 来写入更改。使用 Windows 注册表时,此步骤不是必需的,因为所有更改都会立即进行

或者,用代码表示

// Step 1: Open the key. If the returned RegKey
// is invalid it must not be used
RegKey hkcu = RegKey::FromHKEY(HKEY_CURRENT_USER);
if (!hkcu.IsValid()) return;

// Step 2: Select the registry sub-section to work in
RegKey myKey = hkcu.Key("Software\My Company\My Product");

// Step 3: Manipulate keys
myKey.Value("Foo").Set(true);

很简单,是吧?

现在,让我们谈谈键何时关闭,这可能有点复杂。一个键只有在其下的所有键都已关闭后才能关闭。一个键必须保持打开状态,直到所有下游句柄都可以关闭,然后必须从“底部”向上关闭。为了支持这一点,RegKeyRegValue 背后的实现被实现为双向链式引用计数树(详情如下)。

考虑在这个例子中发生的情况,其中子键(或值)从一个函数按值返回到另一个函数。这个例子说明了为什么保持父键活动直到所有对子键的引用都已删除很重要

void StartHere()
{
    RegValue value = MyGetRegValueFunction("Software\My Company\My Product");
    value.Set(true);
}

 
RegValue MyGetRegValueFunction(const char* path)
{
    RegKey hkcu=  RegKey::FromHKEY(HKEY_CURRENT_USER);
    if (!hkcu.IsValid()) throw;
    return hkcu.Key(path).Value("Foo"); 
    // returning a sub-RegValue by value, what happens to hkcu?
}

如果我们在 MyGetRegValueFunction 中当 hkcu 超出范围时关闭了根存储,那么 StartHere 将无法使用返回的子键。当 StartHere 返回并且 value 析构时,对存储根 (hkcu) 的所有引用都已消失,它可以安全地关闭以避免资源泄漏。表面上很简单,但背后却很复杂。

处理句柄

存储是我用来描述注册表键和值持久化位置的词语。这可能是 Windows 注册表、XML 文件,或者将来某个时候可能是 ODBC 连接。调用 RegKey::From*() 总是会打开或创建请求的存储根(根键、文件或连接),如果失败则会失败。失败表示为返回一个无效的 RegKey(使用 key.IsValid() 进行测试)。使用无效的 RegKey 通常会导致异常。

RegKey::Key() 方法永远不会打开或创建键存储。相反,该方法只是提供了一个到指定路径上的键的接口。如果键必须存在(例如,当键中的值被“设置”时),如果存储不存在,它将自动创建。

同样,RegKey::Value() 不会创建或打开值,但提供了一个可用于在该位置操作键的接口。只有当需要键的存储时,它才会实际创建。

我知道这很奇怪。但这段代码的主要思想是让开发人员不必担心键或值是否存在以及何时打开和关闭它。在我看来,系统应该在需要时自动处理这些操作。当你决定设置键的值时,它需要存在。但是,当你获取值时,它应该回退到默认值。我曾想在 RegKeyRegValue 中添加一个协议,用于测试存储中注册表键的存在,但我成功地抵抗住了。基本上,我从未遇到过一个我需要知道的合法情况。

重载方法 RegValue::Get() 将尝试打开该值以及导致其在指定路径的键链,但如果它不存在,则不会创建它。如果键确实存在,则读取并返回该值。我喜欢这种方法,因为它不需要向注册表添加大量默认键。由于我必须通过引用向 Get() 提供一个对象,因此我已经决定了该值的默认值,因此无需创建该键。

另一方面,重载方法 RegValue::Set() 需要一个实际的条目才能完成其工作。因此,Set 将尝试打开(或在必要时创建)导致该值的键链以及该值本身。这是使用这些封装器创建键和值的主要方式——通过设置内容。

看一些代码可能会更清楚,所以这里是另一个例子

// To get started, we open a root key on HKEY_CURRENT_USER
RegKey hkcu = RegKey::FromHKEY(HKEY_CURRENT_USER);
if (!hkcu.IsValid()) throw;

// We can get to sub-keys using Key-to-Key calling or using a delimted path
RegKey key1 = hkcu.Key("Software\My Company\My Product");  // delimited path
RegKey key2 = 
    hkcu.Key("Software").Key("My Company").Key("My  Product"); // key-to-key

ASSERT(key1 == key2);  // the keys are identical
 
// Similarly, addressing values can be done from the root or sub-key 
RegValue value1 = hkcu.Value("Software\My Company\My Product\Value 1");
// a single  delimited path from root

RegValue value2 = key1.Value("Value 1"); // from a  sub-key instance
ASSERT(value1 == value2); // either way, the value is the  same

// Up to this point the storage hasn't been modified and only the root key 
// is open.  The next line of code will cause the keys "Software", 
// "My Company", and "My Product" as well as the value "Value 1" be created.
// In addition, "Value 1" will be set to true (1).
value1.Set(true);

使用斜杠分隔格式的资源字符串 (IDS_...) 指定路径以获取感兴趣的键很方便。如果您想编写以 RegKey 作为参数并在指示的注册表部分上进行“千篇一律”操作的例程,则递归调用 RegKeyKeyValue 方法会很方便。

关于使用 RegKeyRegValue 的介绍就差不多了。它们是简化注册表使用的简单类(至少对我来说)。示例应用程序展示了它们的一些更多用法示例。现在,深入核心......

设计与实现

RegKeyRegValue 遵循 pImpl 和 Handle 模式。查看 Registry.h,您不会看到除了 KeyImplValueImpl 的裸露声明之外的实现细节。这降低了类的表面复杂性,并使客户端免受实现更改的影响。此外,RegKeyRegValue 只包含一个指针 (m_pImpl) 并且没有虚函数,这使它们非常轻量级。

执行实际工作的实现类可以在 RegistryImpl.hRegistryHKEY.cppRegistryXML.cpp 中找到。RegKeyKeyImpl(键及其存储的实现)之间的关系是多对一的。也就是说,可能有许多 RegKey 对象引用同一个实现实例。RegValueRegValueImpl 遵循相同的模式,如下图图 1 所示

Class Relationship Diagram

图 1:接口对象(虚线上方)和实现对象(虚线下方)之间的关系。

在对话框中使用 RegDDX

RegDemo 是一个小型基于对话框的应用程序,演示了 RegKeyRegValue 的基本用法,以及如何非常快速简单地实现一个将设置持久化到注册表中的对话框。示例对话框由三个简单控件组成

RegDemo Screenshot

这些控件的值在注册表中的表示方式如下

Screenshot of RegEdit

糟糕,我是在不同时间截的图,所以值不匹配。我们假装在对话框图中我还没有点击“确定”......

使对话框支持注册表的非常简单的步骤如下。如果您已经实现了对话框,请跳过步骤 1 和 2,也许还有步骤 3

  1. 在资源编辑器中布局对话框。
  2. 创建 MFC 类来实现对话框。
  3. 使用 CStringintbool 等为每个控件创建“值”成员变量。
  4. 向类添加两个空方法
    DoRegistryDataExchange(CDataExchange *pDX)
    DoControlsDataExchange(CDataExchange* pDX)
  5. 将原始 DoDataExchange 的内容移动到 DoControlsDataExchange
  6. DoDataExchange 现在获得了以下两行实现,“YourDlgClass”替换为您的对话框实现类的名称
    RegDDXMethodCall<YourDlgClass> ddx1(pDX, this, DoRegistryDataExchange);
    RegDDXMethodCall<YourDlgClass> ddx2(pDX, this, DoControlsDataExchange);
  7. 根据需要实现 DoRegistryDataExchange 方法,以将数据移入/移出注册表。以下样板代码可能很有用
    RegKey base = RegKey::FromHKEY(HKEY_CURRENT_USER);
    if (base.IsValid()) {
        RegKey base = hkcu.Key(CString((LPCTSTR)IDS_REG_ROOT)); 
        RegDDX regDX(pDX, base);
    
        // use the Set* methods on RegDDX to move date between the registry
        // and your objects.  Depending on the value of pDX->m_bSaveAndValidate
        // the direction of information flow with be automatically determined.
        // regDX.SetString(, ); 
        // regDX.SetInt(, ); 
        //  
    }

    完成上述操作后,对话框中指定的值应在单击“确定”时自动保存,在单击“取消”时丢弃,并在重新调用时恢复。

显然,这些类使实现设置对话框比单独依赖 MFC 和 Win32 的功能容易得多。此外,您从注册表获得了一个抽象级别,允许您根据需要切换使用 Windows 注册表或 XML 文件——或者两者都使用。

结论

有一次,我通过 BoundsChecker 运行了这段代码,它结果很干净。此后我修改了它,它可能会有内存泄漏。如果是这样,请在下面添加评论描述泄漏,我会处理它。

历史

  • 2004年8月7日更新,增加了对 XML 存储以及 Windows 注册表存储的支持。API RegKey::FromHKEY() 的签名已更改,省略了引用传递的 bool;请改用 IsValid()
  • 原始版本发布于 2003年2月3日
© . All rights reserved.