跳转至

SOUI 事件系统

SOUI 提供了完善的事件处理机制来响应用户交互和控件状态变化。本文将详细介绍 SOUI 中的事件系统,包括事件映射表和事件订阅两种处理方式,以及在复杂应用中的事件分发机制。

概述

事件处理的重要性

在现代UI开发中,一个优秀的事件系统应该能够: - 响应用户交互和控件状态变化 - 提供灵活的事件处理方式 - 支持动态和静态事件绑定 - 保证处理性能和代码可维护性

两种事件处理方式

SOUI 提供了两种主要的事件处理方式:

  1. 事件映射表:类似 MFC/WTL 的消息映射,适合静态控件的集中处理
  2. 事件订阅:基于观察者模式,适合动态控件和灵活的事件处理

事件映射表(Event Map)

基本概念

事件映射表是 SOUI 中最常用的事件处理方式,它通过重载 SHostWnd 的虚函数来实现:

virtual BOOL SHostWnd::_HandleEvent(SOUI::EventArgs *pEvt) { return FALSE; }

为了简化事件处理,SOUI 提供了一组宏来构造事件处理函数,提供类似消息映射的编程体验。

事件映射宏

基础映射宏

EVENT_MAP_BEGIN()     // 开始事件映射表
EVENT_MAP_END()       // 结束事件映射表,将未处理事件传递给基类
EVENT_MAP_BREAK()     // 结束事件映射表,不传递给基类
EVENT_MAP_DECLEAR()   // 声明事件处理函数(用于头文件)
EVENT_MAP_BEGIN2(classname) // 在 .cpp 文件中实现事件映射表

事件处理宏

宏名称 用途 示例
EVENT_ID_COMMAND(id, func) 通过 ID 映射命令事件 EVENT_ID_COMMAND(1, OnClose)
EVENT_NAME_COMMAND(name, func) 通过名称映射命令事件 EVENT_NAME_COMMAND(L"btn_ok", OnOk)
EVENT_NAME_CONTEXTMENU(name, func) 映射右键菜单事件 EVENT_NAME_CONTEXTMENU(L"edit_1", OnMenu)
EVENT_HANDLER(code, func) 自定义事件处理 EVENT_HANDLER(EVT_CUSTOM, OnCustom)

基本使用示例

class CMainDlg : public SHostWnd
{
public:
    CMainDlg() : SHostWnd(_T("LAYOUT:XML_MAINWND")) {}

protected:
    // 事件处理函数声明
    void OnClose();
    void OnMaximize();
    void OnBtnMsgBox();
    void OnBtnMenu();

    // 事件映射表
    EVENT_MAP_BEGIN()
        EVENT_ID_COMMAND(1, OnClose)
        EVENT_ID_COMMAND(2, OnMaximize)
        EVENT_NAME_COMMAND(L"btn_msgbox", OnBtnMsgBox)
        EVENT_NAME_COMMAND(L"btn_menu", OnBtnMenu)
        EVENT_NAME_CONTEXTMENU(L"edit_1140", OnEditMenu)
    EVENT_MAP_END()
};

// 事件处理函数实现
void CMainDlg::OnClose()
{
    PostMessage(WM_QUIT);
}

void CMainDlg::OnBtnMsgBox()
{
    SMessageBox(NULL, _T("Hello SOUI!"), _T("提示"), MB_OK);
}

优势与局限性

优势: - 集中处理事件分发,代码结构清晰 - 类似 MFC/WTL 编程风格,学习成本低 - 编译期确定,性能较好

局限性: - 静态映射表,无法处理运行时动态创建的控件 - 大型项目中可能导致映射表过于庞大

事件订阅(Event Subscription)

基本概念

事件订阅基于观察者模式,允许在运行时动态将控件事件关联到处理函数。这种方式特别适合:

  • 运行时动态创建的控件
  • 需要灵活事件处理的场景
  • 脚本环境中的事件响应

基本使用方法

1. 事件订阅语法

// 基本订阅语法
pControl->GetEventSet()->subscribeEvent(EventType, Subscriber(&ClassName::HandlerFunc, this));

// 取消订阅
pControl->GetEventSet()->unsubscribeEvent(EventType, Subscriber(&ClassName::HandlerFunc, this));

2. 实际使用示例

void CMainDlg::InitListCtrl()
{
    // 找到列表控件
    SListCtrl *pList = FindChildByName2<SListCtrl>(L"lc_test");
    if (pList)
    {
        // 列表控件的唯一子控件即为表头控件
        SWindow *pHeader = pList->GetWindow(GSW_FIRSTCHILD);

        // 订阅表头点击事件
        pHeader->GetEventSet()->subscribeEvent(
            EVT_HEADER_CLICK, 
            Subscriber(&CMainDlg::OnListHeaderClick, this)
        );

        // 订阅列表项双击事件
        pList->GetEventSet()->subscribeEvent(
            EVT_LB_DBCLICK,
            Subscriber(&CMainDlg::OnListItemDbClick, this)
        );
    }
}

// 事件处理函数
bool CMainDlg::OnListHeaderClick(EventArgs *pEvtBase)
{
    // 事件对象强制转换
    EventHeaderClick *pEvt = (EventHeaderClick*)pEvtBase;
    SHeaderCtrl *pHeader = (SHeaderCtrl*)pEvt->sender;

    // 从表头控件获得列表控件对象
    SListCtrl *pList = (SListCtrl*)pHeader->GetParent();

    // 列表数据排序
    SHDITEM hditem;
    hditem.mask = SHDI_ORDER;
    pHeader->GetItem(pEvt->iItem, &hditem);
    pList->SortItems(funCompare, &hditem.iOrder);

    return true;
}

bool CMainDlg::OnListItemDbClick(EventArgs *pEvtBase)
{
    EventLBDbClick *pEvt = (EventLBDbClick*)pEvtBase;

    // 处理双击逻辑
    int nItem = pEvt->nCurSel;
    SStringT strText = _T("双击了第 ") + SStringT().Format(_T("%d"), nItem) + _T(" 项");
    SMessageBox(NULL, strText, _T("提示"), MB_OK);

    return true;
}

常见事件类型

事件类型 说明 适用控件
EVT_CMD 命令事件 按钮、菜单项等
EVT_LB_SELCHANGE 列表选择改变 ListBox、ListCtrl
EVT_LB_DBCLICK 列表双击 ListBox、ListCtrl
EVT_HEADER_CLICK 表头点击 HeaderCtrl
EVT_TC_SELCHANGE 标签页切换 TabCtrl
EVT_EN_CHANGE 编辑框内容改变 Edit、RichEdit
EVT_BN_CLICKED 按钮点击 Button
EVT_CONTEXTMENU 右键菜单 所有控件

动态控件事件处理

void CMainDlg::CreateDynamicControls()
{
    // 动态创建按钮
    SButton *pBtn = new SButton();
    pBtn->SetName(L"dynamic_btn");
    pBtn->SetWindowText(L"动态按钮");

    // 添加到父容器
    SWindow *pContainer = FindChildByName(L"container");
    pContainer->InsertChild(pBtn);
    pBtn->Move(10, 10, 100, 30);

    // 订阅点击事件
    pBtn->GetEventSet()->subscribeEvent(
        EVT_CMD,
        Subscriber(&CMainDlg::OnDynamicBtnClick, this)
    );
}

bool CMainDlg::OnDynamicBtnClick(EventArgs *pEvt)
{
    SMessageBox(NULL, _T("动态按钮被点击了!"), _T("提示"), MB_OK);
    return true;
}

事件分发机制

问题背景

在大型项目中,如果将所有控件事件集中在一个映射表中处理,会导致:

  • 代码可维护性差
  • 事件映射表过于庞大
  • 不同功能模块之间耦合度高

事件分发的解决方案

SOUI 提供了类似 WTL 的事件分发机制,使用以下宏进行事件转发:

CHAIN_EVENT_MAP(ChainClass)              // 转发到基类
CHAIN_EVENT_MAP_MEMBER(theChainMember)   // 转发到成员对象
EVENT_CHECK_SENDER_ROOT(pRoot)           // 检查事件来源

实际应用示例

1. 主窗口事件分发

class CMainDlg : public SHostWnd
{
public:
    CMainDlg() : SHostWnd(_T("LAYOUT:XML_MAINWND")) {}

protected:
    // 各功能模块的事件处理对象
    CImageMergerHandler m_imgMergerHandler;
    CCodeLineCounter m_codeLineCounter;
    CUnicodeHandler m_2UnicodeHandler;
    CFolderScanHandler m_folderScanHandler;
    CCalcMd5Handler m_calcMd5Handler;

    // 主窗口事件映射表
    EVENT_MAP_BEGIN()
        EVENT_NAME_COMMAND(L"btn_close", OnClose)
        EVENT_NAME_COMMAND(L"btn_min", OnMinimize)
        EVENT_NAME_COMMAND(L"btn_max", OnMaximize)
        EVENT_NAME_COMMAND(L"btn_restore", OnRestore)

        // 事件分发到各功能模块
        CHAIN_EVENT_MAP_MEMBER(m_imgMergerHandler)
        CHAIN_EVENT_MAP_MEMBER(m_codeLineCounter)
        CHAIN_EVENT_MAP_MEMBER(m_2UnicodeHandler)
        CHAIN_EVENT_MAP_MEMBER(m_folderScanHandler)
        CHAIN_EVENT_MAP_MEMBER(m_calcMd5Handler)
    EVENT_MAP_END()

    void OnClose();
    void OnMinimize();
    void OnMaximize();
    void OnRestore();
};

2. 功能模块事件处理类

class CImageMergerHandler
{
friend class CMainDlg;

public:
    CImageMergerHandler(void);
    ~CImageMergerHandler(void);

    void OnInit(SWindow *pRoot);

protected:
    void OnSave();
    void OnClear();
    void OnModeHorz();
    void OnModeVert();

    // 功能模块的事件映射表
    EVENT_MAP_BEGIN()
        // 检查事件来源是否为本模块
        EVENT_CHECK_SENDER_ROOT(m_pPageRoot)

        EVENT_NAME_COMMAND(L"btn_save", OnSave)
        EVENT_NAME_COMMAND(L"btn_clear", OnClear)
        EVENT_NAME_COMMAND(L"radio_horz", OnModeHorz)
        EVENT_NAME_COMMAND(L"radio_vert", OnModeVert)
    EVENT_MAP_BREAK()  // 不传递给基类

private:
    SWindow *m_pPageRoot;      // 页面根节点
    SImgCanvas *m_pImgCanvas;  // 图像画布
};

// 初始化函数
void CImageMergerHandler::OnInit(SWindow *pRoot)
{
    m_pPageRoot = pRoot;
    m_pImgCanvas = pRoot->FindChildByName2<SImgCanvas>(L"img_canvas");
}

// 事件处理函数实现
void CImageMergerHandler::OnSave()
{
    if (m_pImgCanvas)
    {
        // 保存图像逻辑
        m_pImgCanvas->SaveToFile(_T("merged.png"));
    }
}

void CImageMergerHandler::OnClear()
{
    if (m_pImgCanvas)
    {
        // 清除图像逻辑
        m_pImgCanvas->Clear();
    }
}

3. 主窗口初始化

BOOL CMainDlg::OnInitDialog(HWND hWnd, LPARAM lParam)
{
    // 初始化各功能模块
    STabCtrl *pTabCtrl = FindChildByName2<STabCtrl>(L"tab_main");
    if (pTabCtrl)
    {
        // 初始化图像合并模块
        SWindow *pPageImgMerger = pTabCtrl->GetPage(0);
        m_imgMergerHandler.OnInit(pPageImgMerger);

        // 初始化代码行数统计模块
        SWindow *pPageCodeCounter = pTabCtrl->GetPage(1);
        m_codeLineCounter.OnInit(pPageCodeCounter);

        // 其他模块初始化...
    }

    return TRUE;
}

事件分发的优势

  • 模块化:不同功能的事件处理分离到独立对象
  • 可维护性:每个模块只关心自己的事件
  • 可复用性:事件处理对象可以在不同项目中复用
  • 解耦:降低模块间的耦合度

高级事件处理技巧

1. 事件过滤和预处理

class CMainDlg : public SHostWnd
{
protected:
    virtual BOOL _HandleEvent(EventArgs *pEvt) override
    {
        // 事件预处理
        if (PreProcessEvent(pEvt))
            return TRUE;

        // 调用映射表处理
        return __super::_HandleEvent(pEvt);
    }

private:
    BOOL PreProcessEvent(EventArgs *pEvt)
    {
        // 全局快捷键处理
        if (pEvt->GetID() == EVT_KEY_DOWN)
        {
            EventKeyDown *pKeyEvt = (EventKeyDown*)pEvt;
            if (pKeyEvt->nChar == VK_ESCAPE)
            {
                OnClose();
                return TRUE;
            }
        }

        // 全局鼠标事件处理
        if (pEvt->GetID() == EVT_MOUSE_HOVER)
        {
            // 显示提示信息
            ShowTooltip(pEvt);
        }

        return FALSE;
    }
};

2. 自定义事件

DEF_EVT_EXT(EventCustomData, EVT_EXTERNAL_BEGIN + 500, {
    SStringT strData;
    int nValue;
     });


// 触发自定义事件
void CMyControl::FireCustomEvent()
{
    EventCustomData evt(this);
    evt.strData = _T("自定义数据");
    evt.nValue = 100;

    FireEvent(evt);
}

// 处理自定义事件
bool CMainDlg::OnCustomData(EventArgs *pEvtBase)
{
    EventCustomData *pEvt = (EventCustomData*)pEvtBase;

    SStringT strMsg;
    strMsg.Format(_T("接收到数据:%s,值:%d"), 
                  pEvt->strData, pEvt->nValue);
    SMessageBox(NULL, strMsg, _T("自定义事件"), MB_OK);

    return true;
}

3. 批量事件处理

class CMainDlg : public SHostWnd
{
protected:
    // 批量按钮处理
    void OnBatchButton(EventArgs *pEvt)
    {
        SWindow *pSender = sobj_cast<SWindow>(pEvt->sender);
        SStringT strName = pSender->GetName();

        if (strName.Left(4) == L"btn_")
        {
            SStringT strAction = strName.Right(strName.GetLength() - 4);
            ProcessButtonAction(strAction);
        }
    }

    EVENT_MAP_BEGIN()
        // 使用通配符处理多个按钮
        EVENT_NAME_COMMAND(L"btn_action1", OnBatchButton)
        EVENT_NAME_COMMAND(L"btn_action2", OnBatchButton)
        EVENT_NAME_COMMAND(L"btn_action3", OnBatchButton)
    EVENT_MAP_END()

private:
    void ProcessButtonAction(const SStringT &strAction)
    {
        if (strAction == L"action1")
        {
            // 处理动作1
        }
        else if (strAction == L"action2")
        {
            // 处理动作2
        }
        // ...
    }
};

最佳实践

1. 选择合适的事件处理方式

  • 简单应用:使用事件映射表
  • 复杂应用:结合事件映射表和事件分发
  • 动态控件:使用事件订阅
  • 脚本环境:必须使用事件订阅

2. 事件处理函数设计原则

// 好的实践:返回值明确,参数类型安全
bool OnButtonClick(EventArgs *pEvt)
{
    // 1. 参数验证
    if (!pEvt || !pEvt->sender)
        return false;

    // 2. 类型转换
    EventCmd *pCmdEvt = sobj_cast<EventCmd>(pEvt);
    if (!pCmdEvt)
        return false;

    // 3. 业务逻辑处理
    ProcessButtonLogic();

    // 4. 返回是否已处理
    return true;
}

// 避免的做法:过于复杂的事件处理函数
void OnComplexEvent(EventArgs *pEvt)
{
    // 避免:在一个函数中处理多种不同的逻辑
    // 避免:没有参数验证
    // 避免:没有明确的返回值
}

3. 调试和日志

class CMainDlg : public SHostWnd
{
protected:
    virtual BOOL _HandleEvent(EventArgs *pEvt) override
    {
        // 调试模式下记录事件
        #ifdef _DEBUG
        LogEvent(pEvt);
        #endif

        return __super::_HandleEvent(pEvt);
    }

private:
    void LogEvent(EventArgs *pEvt)
    {
        SWindow *pSender = sobj_cast<SWindow>(pEvt->sender);
        if (pSender)
        {
            STRACE(_T("Event: %s from %s\n"), 
                   pEvt->GetName(), 
                   pSender->GetName());
        }
    }
};

总结

SOUI 的事件处理系统提供了灵活而强大的机制来响应用户交互:

双重机制:事件映射表 + 事件订阅满足不同场景需求
模块化设计:事件分发机制支持复杂应用的模块化开发
类型安全:强类型的事件参数提供更好的开发体验
灵活性:支持静态和动态控件的事件处理
可维护性:清晰的代码结构便于维护和扩展

掌握这些事件处理技巧,能够构建出响应迅速、结构清晰的 SOUI 应用程序。在实际开发中,建议根据项目复杂度选择合适的事件处理方式,并遵循最佳实践以确保代码质量。