当前位置: 代码网 > it编程>编程语言>C# > C#使用RegNotifyChangeKeyValue监听注册表更改的几种方式

C#使用RegNotifyChangeKeyValue监听注册表更改的几种方式

2024年06月02日 C# 我要评论
养成一个好习惯,调用 Windows API 之前一定要先看文档 RegNotifyChangeKeyValue 函数 (winreg.h) - Win32 apps | Microsoft Learn 同步阻塞模式 RegNotifyChangeKeyValue的最后一个参数传递false,表示以 ...

养成一个好习惯,调用 windows api 之前一定要先看文档

regnotifychangekeyvalue 函数 (winreg.h) - win32 apps | microsoft learn

同步阻塞模式

regnotifychangekeyvalue的最后一个参数传递false,表示以同步的方式监听。
同步模式会阻塞调用线程,直到监听的目标发生更改才会返回,如果在ui线程上调用,则会导致界面卡死,因此我们一般不会直接在主线程上同步监听,往往是创建一个新的线程来监听。

示例代码因为是控制台程序,因此没有创建新的线程。

registrykey hkey = registry.currentuser.createsubkey("software\\1-regmonitor");
string changebefore = hkey.getvalue("testvalue").tostring();
console.writeline($"testvalue的当前值是:{changebefore}, 时间:{datetime.now:hh:mm:ss}");

//此处创建一个任务,5s之后修改testvalue的值为一个新的guid
task.delay(5000).continuewith(t =>
{
    string newvalue = guid.newguid().tostring();
    console.writeline($"testvalue的值即将被改为:{newvalue}, 时间:{datetime.now:hh:mm:ss}");
    hkey.setvalue("testvalue", newvalue);
});
int ret = regnotifychangekeyvalue(hkey.handle, false, regnotifyfilter.changelastset, new safewaithandle(intptr.zero, true), false);
if(ret != 0)
{
    console.writeline($"出错了:{ret}");
    return;
}
string currentvalue = hkey.getvalue("testvalue").tostring();
console.writeline($"testvalue的最新值是:{currentvalue}, 时间:{datetime.now:hh:mm:ss}");
hkey.close();
console.readline();

运行结果:
image

异步模式

regnotifychangekeyvalue的最后一个参数传递true,表示以异步的方式监听。
异步模式的关键点是需要创建一个事件,然后regnotifychangekeyvalue会立即返回,不会阻塞调用线程,然后需要在其他的线程中等待事件的触发。
当然也可以在regnotifychangekeyvalue返回之后立即等待事件,这样跟同步阻塞没有什么区别,如果不是出于演示目的,则没什么意义。

出于演示目的毫无意义的异步模式示例:

registrykey hkey = registry.currentuser.createsubkey("software\\1-regmonitor");
string changebefore = hkey.getvalue("testvalue").tostring();
console.writeline($"testvalue的当前值是:{changebefore}, 时间:{datetime.now:hh:mm:ss}");

//此处创建一个任务,5s之后修改testvalue的值为一个新的guid
task.delay(5000).continuewith(t =>
{
    string newvalue = guid.newguid().tostring();
    console.writeline($"testvalue的值即将被改为:{newvalue}, 时间:{datetime.now:hh:mm:ss}");
    hkey.setvalue("testvalue", newvalue);
});
manualresetevent manualresetevent = new manualresetevent(false);
int ret = regnotifychangekeyvalue(hkey.handle, false, regnotifyfilter.changelastset, manualresetevent.safewaithandle, true);
if(ret != 0)
{
    console.writeline($"出错了:{ret}");
    return;
}
console.writeline($"regnotifychangekeyvalue立即返回,时间:{datetime.now:hh:mm:ss}");
manualresetevent.waitone();
string currentvalue = hkey.getvalue("testvalue").tostring();
console.writeline($"testvalue的最新值是:{currentvalue}, 时间:{datetime.now:hh:mm:ss}");
hkey.close();
manualresetevent.close();
console.writeline("收工");
console.readline();

运行结果:
image

正经的代码大概应该这么写:

演示代码请忽略参数未判空,异常未处理等场景

class registrymonitor
{
    private thread m_thread = null;
    private string m_keyname;
    private regnotifyfilter m_notifyfilter = regnotifyfilter.changelastset;

    public event eventhandler registrychanged;

    public registrymonitor(string keyname, regnotifyfilter notifyfilter)
    {
        this.m_keyname = keyname;
        this.m_notifyfilter = notifyfilter;
        this.m_thread = new thread(threadaction);
        this.m_thread.isbackground = true;
    }
    public void start()
    {
        this.m_thread.start();
    }
    private void threadaction()
    {
        using(registrykey hkey = registry.currentuser.createsubkey(this.m_keyname))
        {
            using(manualresetevent waithandle = new manualresetevent(false))
            {
                int ret = regnotifychangekeyvalue(hkey.handle, false, this.m_notifyfilter, waithandle.safewaithandle, true);
                waithandle.waitone();
                this.registrychanged?.invoke(this, eventargs.empty);
            }
        }
    }
}

static void main(string[] args)
{
    string keyname = "software\\1-regmonitor";
    registrykey hkey = registry.currentuser.createsubkey(keyname);
    string changebefore = hkey.getvalue("testvalue").tostring();
    console.writeline($"testvalue的当前值是:{changebefore}, 时间:{datetime.now:hh:mm:ss}");

    //此处创建一个任务,5s之后修改testvalue的值为一个新的guid
    task.delay(5000).continuewith(t =>
    {
        string newvalue = guid.newguid().tostring();
        console.writeline($"testvalue的值即将被改为:{newvalue}, 时间:{datetime.now:hh:mm:ss}");
        hkey.setvalue("testvalue", newvalue);
    });

    registrymonitor monitor = new registrymonitor(keyname, regnotifyfilter.changelastset);
    monitor.registrychanged += (sender, e) =>
    {
        console.writeline($"{keyname}的值发生了改变");
        string currentvalue = hkey.getvalue("testvalue").tostring();
        console.writeline($"testvalue的最新值是:{currentvalue}, 时间:{datetime.now:hh:mm:ss}");
        hkey.close();
    };
    monitor.start();

    console.writeline("收工");
    console.readline();
}

运行结果:
image

那么问题来了:

  1. 上面监听一个路径就需要创建一个线程,如果要监听多个路径,就需要创建多个线程,且他们什么事都不干,就在那等,这不太科学。
  2. 经常写c#的都知道,一般不建议代码中直接创建thread
  3. 改成线程池或者task行不行?如果在线程池或者task里面调用waitone进行阻塞,那也是不行的。

接下来 ,我们尝试改造一下

基于线程池的异步模式

调用线程池的registerwaitforsingleobject,给一个事件注册一个回调,当事件触发时,则执行指定的回调函数,参考threadpool.registerwaitforsingleobject 方法 (system.threading) | microsoft learn

代码实例如下:

class registrymonitor
{
    private string m_keyname;
    private regnotifyfilter m_notifyfilter = regnotifyfilter.changelastset;
    private registeredwaithandle m_registered = null;
    private registrykey m_key = null;
    private manualresetevent m_waithandle = null;

    public event eventhandler registrychanged;

    public registrymonitor(string keyname, regnotifyfilter notifyfilter)
    {
        this.m_keyname = keyname;
        this.m_notifyfilter = notifyfilter;
    }
    public void start()
    {
        this.m_key = registry.currentuser.createsubkey(this.m_keyname);
        this.m_waithandle = new manualresetevent(false);
        int ret = regnotifychangekeyvalue(this.m_key.handle, false, this.m_notifyfilter | regnotifyfilter.threadagnostic, this.m_waithandle.safewaithandle, true);
        this.m_registered = threadpool.registerwaitforsingleobject(this.m_waithandle, callback, null, timeout.infinite, true);

    }
    private void callback(object state, bool timedout)
    {
        this.m_registered.unregister(this.m_waithandle);
        this.m_waithandle.close();
        this.m_key.close();
        this.registrychanged?.invoke(this, eventargs.empty);
    }
}

static void main(string[] args)
{
    for(int i = 1; i <= 50; i++)
    {
        string keyname = $"software\\1-regmonitor\\{i}";

        registrykey hkey = registry.currentuser.createsubkey(keyname);
        hkey.setvalue("testvalue", guid.newguid().tostring());
        string changebefore = hkey.getvalue("testvalue").tostring();
        console.writeline($"{keyname} testvalue的当前值是:{changebefore}, 时间:{datetime.now:hh:mm:ss}");

        registrymonitor monitor = new registrymonitor(keyname, regnotifyfilter.changelastset);
        monitor.registrychanged += (sender, e) =>
        {
            console.writeline($"{keyname}的值发生了改变");
            string currentvalue = hkey.getvalue("testvalue").tostring();
            console.writeline($"{keyname} testvalue的最新值是:{currentvalue}, 时间:{datetime.now:hh:mm:ss}");
            hkey.close();
        };
        monitor.start();
        console.writeline($"{keyname}监听中...");
    }

    console.writeline("收工");
    console.readline();
}

运行结果:
image
可以看到,创建50个监听,而进程的总线程数只有7个。因此使用线程池是最佳方案。

注意事项

  1. 官方文档有说明,调用regnotifychangekeyvalue需要再持久化的线程中,如果不能保证调用线程持久化(如在线程池中调用),则可以加上reg_notify_thread_agnostic标识
  2. 示例中的监听都是一次性的,重复监听只需要在事件触发后再次执行regnotifychangekeyvalue的流程即可

基础代码

/// <summary>
/// 指示应报告的更改
/// </summary>
[flags]
enum regnotifyfilter
{
    /// <summary>
    /// 通知调用方是添加还是删除了子项
    /// </summary>
    changename = 0x00000001,
    /// <summary>
    /// 向调用方通知项属性(例如安全描述符信息)的更改
    /// </summary>
    changeattributes = 0x00000002,
    /// <summary>
    /// 向调用方通知项值的更改。 这包括添加或删除值,或更改现有值
    /// </summary>
    changelastset = 0x00000004,
    /// <summary>
    /// 向调用方通知项的安全描述符的更改
    /// </summary>
    changesecurity = 0x00000008,
    /// <summary>
    /// 指示注册的生存期不得绑定到发出 regnotifychangekeyvalue 调用的线程的生存期。<b>注意</b> 此标志值仅在 windows 8 及更高版本中受支持。
    /// </summary>
    threadagnostic = 0x10000000
}
/// <summary>
/// 通知调用方对指定注册表项的属性或内容的更改。
/// </summary>
/// <param name="hkey">打开的注册表项的句柄。密钥必须已使用key_notify访问权限打开。</param>
/// <param name="bwatchsubtree">如果此参数为 true,则函数将报告指定键及其子项中的更改。 如果参数为 false,则函数仅报告指定键中的更改。</param>
/// <param name="dwnotifyfilter">
/// 一个值,该值指示应报告的更改。 此参数可使用以下一个或多个值。<br/>
/// reg_notify_change_name 0x00000001l 通知调用方是添加还是删除了子项。<br/>
/// reg_notify_change_attributes 0x00000002l 向调用方通知项属性(例如安全描述符信息)的更改。<br/>
/// reg_notify_change_last_set 0x00000004l 向调用方通知项值的更改。 这包括添加或删除值,或更改现有值。<br/>
/// reg_notify_change_security 0x00000008l 向调用方通知项的安全描述符的更改。<br/>
/// reg_notify_thread_agnostic 0x10000000l 指示注册的生存期不得绑定到发出 regnotifychangekeyvalue 调用的线程的生存期。<b>注意</b> 此标志值仅在 windows 8 及更高版本中受支持。
/// </param>
/// <param name="hevent">事件的句柄。 如果 fasynchronous 参数为 true,则函数将立即返回 ,并通过发出此事件信号来报告更改。 如果 fasynchronous 为 false,则忽略 hevent 。</param>
/// <param name="fasynchronous">
/// 如果此参数为 true,则函数将立即返回并通过向指定事件发出信号来报告更改。 如果此参数为 false,则函数在发生更改之前不会返回 。<br/>
/// 如果 hevent 未指定有效的事件, 则 fasynchronous 参数不能为 true。
/// </param>
/// <returns></returns>
[dllimport("advapi32.dll", charset = charset.unicode, setlasterror = true)]
private static extern int regnotifychangekeyvalue(safehandle hkey, bool bwatchsubtree, regnotifyfilter dwnotifyfilter, safehandle hevent, bool fasynchronous);

(0)

相关文章:

  • C#调用exe文件的方法详解

    需求最近同事使用python开发了一款智能文字转语音的程序,经讨论部署在windows环境服务器下,因此需要生成目标为可执行程序文件,即exe文件。需要在web应用程序里进行调用,…

    2024年05月28日 编程语言
  • C#如何实现子进程跟随主进程关闭

    C#如何实现子进程跟随主进程关闭

    前言多进程开发经常会遇到主进程关闭,子进程需要跟随主进程一同关闭。比如调ffmpeg命令行实现的录屏程序,录屏程序关闭,ffmpeg进程也需要退出。我们通常在程... [阅读全文]
  • 详解C# wpf如何嵌入外部程序

    详解C# wpf如何嵌入外部程序

    前言实现嵌入各种窗口控件后,其实还会有一种需求:嵌入外部程序,我们有时可能需要嵌入一个浏览器或者或者播放器等一些已有的程序,其嵌入原理也和前面差不多,只要能获取... [阅读全文]
  • C#使用itextsharp打印pdf的实现代码

    C#使用itextsharp打印pdf的实现代码

    引言提到打印,恐怕对于很多人都不会陌生,无论是开发者,还是非计算机专业的人员都会接触到打印。对于项目开发中使用到打印的地方会非常多,在.net项目中,选择打印的... [阅读全文]
  • C#如何使用PaddleOCR进行图片文字识别功能

    paddlepaddle介绍paddlepaddle(飞桨)是百度开发的深度学习平台,旨在为开发者提供全面、灵活的工具集,用于构建、训练和部署各种深度学习模型。它具有开放源代码、高…

    2024年05月28日 编程语言
  • 使用C#实现生成一个绿色文件

    生成一个绿色文件免去了安装的繁琐过程,直接运行,非常方便。新建一个类库项目在类库class1中实现简单的sum方法。using system;using system.collec…

    2024年05月28日 编程语言

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com