跳转至

通知中心 SNotifyCenter

在客户端开发中,我们经常需要在非 UI 线程中执行一些耗时操作(如网络请求、文件读写等),然后将结果通知给 UI 线程进行界面更新。SOUI 提供了 SNotifyCenter 通知中心来优雅地处理这类异步通知需求。

SNotifyCenter 通知中心

为了解决传统方法的局限性,SOUI 引入了 SNotifyCenter 通知中心,专门用于处理跨线程通知,可以通过SApplication对象来启用或者禁用通知中心。

核心优势

  1. 全局访问:通过单例模式,可以在代码的任意位置获取通知中心指针
  2. 灵活处理:采用 SOUI 的事件系统进行事件派发,结合事件订阅机制,可以在任意位置处理发出的事件

核心接口

/**
 * @class SNotifyCenter
 * @brief 通知中心类,管理事件的注册、注销和触发
 */
class SOUI_EXP SNotifyCenter
    : public INotifyCenter
    , public SSingleton2<SNotifyCenter>
    , public SEventSet
    , protected INotifyCallback {
    SINGLETON2_TYPE(SINGLETON_NOTIFYCENTER)
    friend SApplication;

  private:
    /**
     * @brief 构造函数
     * @param nIntervel 事件处理间隔时间(毫秒)
     */
    SNotifyCenter(int nIntervel = 20);

    /**
     * @brief 析构函数
     */
    ~SNotifyCenter(void);

  public:
    /**
     * @brief 触发一个同步通知事件
     * @param e 事件参数对象
     *
     * @details 只能在UI线程中调用
     */
    STDMETHOD_(void, FireEventSync)(THIS_ IEvtArgs *e) OVERRIDE;

    /**
     * @brief 触发一个异步通知事件
     * @param e 事件参数对象
     *
     * @details 可以在非UI线程中调用,EventArgs *e必须是从堆上分配的内存,调用后使用Release释放引用计数
     */
    STDMETHOD_(void, FireEventAsync)(THIS_ IEvtArgs *e) OVERRIDE;

    /**
     * @brief 注册一个处理通知的对象
     * @param slot 事件处理对象
     * @return 成功返回TRUE,失败返回FALSE
     */
    STDMETHOD_(BOOL, RegisterEventMap)(THIS_ const IEvtSlot *slot) OVERRIDE;

    /**
     * @brief 注销一个处理通知的对象
     * @param slot 事件处理对象
     * @return 成功返回TRUE,失败返回FALSE
     */
    STDMETHOD_(BOOL, UnregisterEventMap)(THIS_ const IEvtSlot *slot) OVERRIDE;

    /**
     * @brief 在UI线程中运行一个可运行对象
     * @param pRunnable 可运行对象
     * @param bSync 同步执行标志
     */
    STDMETHOD_(void, RunOnUI)(THIS_ IRunnable *pRunnable, BOOL bSync) OVERRIDE;

    /**
     * @brief 在UI线程中运行一个函数
     * @param fun 函数指针
     * @param wp WPARAM参数
     * @param lp LPARAM参数
     * @param bSync 同步执行标志
     */
    STDMETHOD_(void, RunOnUI2)(THIS_ FunRunOnUI fun, WPARAM wp, LPARAM lp, BOOL bSync) OVERRIDE;

  public:
#ifdef ENABLE_RUNONUI
    /**
     * @brief 在UI线程中同步运行一个闭包
     * @param fn 闭包函数
     */
    void RunOnUISync(std::function<void(void)> fn);

    /**
     * @brief 在UI线程中异步运行一个闭包
     * @param fn 闭包函数
     */
    void RunOnUIAsync(std::function<void(void)> fn);
#endif

  protected:
    /**
     * @brief 触发事件
     * @param e 事件参数对象
     */
    virtual void OnFireEvent(IEvtArgs *e);

    /**
     * @brief 触发多个事件
     */
    virtual void OnFireEvts();

    tid_t m_dwMainTrdID; // 主线程ID

    SList<IEvtSlot *> m_evtHandlerMap; // 事件处理对象列表

    SNotifyReceiver *m_pReceiver; // 通知接收器

    SCriticalSection m_cs;         // 临界区对象
    SList<IEvtArgs *> m_ayncEvent; // 异步事件列表
    BOOL m_bRunning;               // 运行状态标志
    int m_nInterval;               // 事件处理间隔时间(毫秒)

    SList<SAutoRefPtr<IRunnable> > m_asyncRunnable; // 异步可运行对象列表
};

自动事件映射注册模板

template<class T>
class TAutoEventMapReg
{
    typedef TAutoEventMapReg<T> _thisClass;
public:
    TAutoEventMapReg()
    {
        SNotifyCenter::getSingleton().RegisterEventMap(Subscriber(&_thisClass::OnEvent,this));
    }

    ~TAutoEventMapReg()
    {
        SNotifyCenter::getSingleton().UnregisterEventMap(Subscriber(&_thisClass::OnEvent,this));
    }

protected:
    bool OnEvent(EventArgs *e){
        T * pThis = static_cast<T*>(this);
        return !!pThis->_HandleEvent(e);
    }
};

使用步骤

1. 启用通知中心

DECLARE_INTERFACE_(IApplication, IObjRef){
    STDMETHOD_(void, EnableNotifyCenter)(THIS_ BOOL bEnable, int interval DEF_VAL(20)) PURE;
};

int main(){ 
    SApplication app;
    // 启用通知中心
    app.EnableNotifyCenter(TRUE);
    app.Run();
    return 0;
}

2. 定义事件类型

首先定义需要通过通知中心分发的事件类型:

// 定义自定义事件
class EventThreadStart : public TplEventArgs<EventThreadStart>
{
    SOUI_CLASS_NAME(EventThreadStart, L"on_thread_start")
public:
    EventThreadStart(SWindow *pSender) : TplEventArgs<EventThreadStart>(pSender) {}
    enum { EventID = EVT_EXTERNAL_BEGIN + 1000 };
};

class EventThreadStop : public TplEventArgs<EventThreadStop>
{
    SOUI_CLASS_NAME(EventThreadStop, L"on_thread_stop")
public:
    EventThreadStop(SWindow *pSender) : TplEventArgs<EventThreadStop>(pSender) {}
    enum { EventID = EVT_EXTERNAL_BEGIN + 1001 };
};

class EventThread : public TplEventArgs<EventThread>
{
    SOUI_CLASS_NAME(EventThread, L"on_thread")
public:
    EventThread(SWindow *pSender) : TplEventArgs<EventThread>(pSender) {}
    enum { EventID = EVT_EXTERNAL_BEGIN + 1002 };
};

注册事件类型:

void CMainDlg::OnBtnStartNotifyThread()
{
    if(IsRunning()) return;

    // 注册事件类型
    SNotifyCenter::getSingleton().addEvent(EVENTID(EventThreadStart));
    SNotifyCenter::getSingleton().addEvent(EVENTID(EventThreadStop));
    SNotifyCenter::getSingleton().addEvent(EVENTID(EventThread));

    // 触发同步事件
    EventThreadStart evt(this);
    SNotifyCenter::getSingleton().FireEventSync(&evt);

    BeginThread(); 
}

void CMainDlg::OnBtnStopNotifyThread()
{
    if(!IsRunning()) return;

    EndThread();

    // 触发同步事件
    EventThreadStop evt(this);
    SNotifyCenter::getSingleton().FireEventSync(&evt);

    // 移除事件类型
    SNotifyCenter::getSingleton().removeEvent(EventThreadStart::EventID);
    SNotifyCenter::getSingleton().removeEvent(EventThreadStop::EventID);
    SNotifyCenter::getSingleton().removeEvent(EventThread::EventID);
}

3. 订阅事件处理

让需要处理通知中心分发事件的类从 TAutoEventMapReg 继承,实现事件的自动订阅:

class CMainDlg : public SHostDialog
    , public TAutoEventMapReg<CMainDlg>  // 继承自动事件映射注册
{
    // ...
};

4. 处理异步事件

在事件映射表中处理事件:

// 事件处理映射表
BEGIN_MSG_MAP_EX(CMainDlg)
    // ...
END_MSG_MAP()

// 事件处理函数
BOOL CMainDlg::_HandleEvent(EventArgs *pEvt)
{
    if (pEvt->id == EventThread::EventID)
    {
        EventThread *pThreadEvt = (EventThread*)pEvt;
        // 处理线程事件
        SStringT strMsg;
        strMsg.Format(_T("线程消息: %d"), GetTickCount());
        m_pOutput->SetWindowText(strMsg);
        return TRUE;
    }
    else if (pEvt->id == EventThreadStart::EventID)
    {
        EventThreadStart *pStartEvt = (EventThreadStart*)pEvt;
        // 处理线程开始事件
        m_pOutput->SetWindowText(_T("线程已启动"));
        return TRUE;
    }
    else if (pEvt->id == EventThreadStop::EventID)
    {
        EventThreadStop *pStopEvt = (EventThreadStop*)pEvt;
        // 处理线程停止事件
        m_pOutput->SetWindowText(_T("线程已停止"));
        return TRUE;
    }
    return FALSE;
}

5. 在工作线程中触发异步事件

// 工作线程函数
static DWORD WINAPI ThreadProc(LPVOID lpParam)
{
    CMainDlg *pMainDlg = (CMainDlg*)lpParam;

    while(pMainDlg->IsRunning())
    {
        // 创建事件对象(必须在堆上分配)
        EventThread *pEvt = new EventThread(pMainDlg);

        // 触发异步事件(可在非UI线程中调用)
        SNotifyCenter::getSingleton().FireEventAsync(pEvt);

        // 释放事件对象引用
        pEvt->Release();

        // 休眠一段时间
        Sleep(1000);
    }

    return 0;
}

最佳实践

  1. 事件对象生命周期:使用 FireEventAsync 时,确保事件对象是从堆上分配的,并在调用后调用 Release() 释放引用计数

  2. 事件类型管理:合理注册和注销事件类型,避免事件ID冲突

  3. 线程安全FireEventSync 只能在 UI 线程中调用,而 FireEventAsync 可以在任意线程中调用

  4. 自动注册:使用 TAutoEventMapReg 模板类可以实现事件处理的自动注册和注销

注意事项

  • 确保在应用程序初始化时创建 SNotifyCenter 单例
  • 异步事件处理依赖于 UI 线程的消息循环
  • 及时注销不再需要的事件类型和事件处理器,避免内存泄漏

具体用法请参考 SOUI 的官方 demo 示例。