Windows消息Hook简介

1、前言

众所周知,Windows应用程序都是消息(事件)驱动的,任何一个窗口都能够接收消息,并对消息进行处理,处理完成后进入下一轮循环。
通常情况下,程序员可以在窗口过程中处理接收到的消息,但是在一些应用中常常需要获取和处理另外应用程序的消息,而实现此类功能的技术也就本文要讨论的主题――消息拦截技术。

2、消息机制

2.1 来源

Windows应用程序的消息来源有4种:输入消息,控制消息、系统消息、用户消息。
而根据消息产生的方式又可以分为两大类,即硬件消息和软件消息。
硬件消息,常指由硬件所产生的事件,通过系统消息队列中转,再转发给应用程序消息队列。
软件消息,常指由系统或其它应用程序发送的信息,它直接发送到应用程序消息队列。

2.2 构成

一个消息由一个消息名称[UINT],和两个参数[WPARAM, LPARAM]。不同的消息,对应的参数含义也不一致。
所有系统消息的定义在Winuser.h中都可以找到。常用消息参考

2.3 处理

一个消息通常必须由一个窗口接收。而窗口程序本身也会实现窗口过程函数来处理接收的消息。函数原型如下:

LRESULT Wndproc(HWND hWnd,UINT message,WPARAM wParam,LPARAM lParam);

当然,程序也可以使用GetMessage直接从消息队列中获取消息进行处理。

3、详解

3.1 WH_CALLWNDPRO、WH_CALLWNDPROCRET
  • 功能:在消息发送到窗口过程前、窗口过程处理完消息后调用
  • 参数:
    wParam含义lParam
    如果消息由当前线程发送,则为非零;否则为零指向 CWPSTRUCT 结构的指针
  • 示例
  LRESULT(CALLBACK HOOK_CALLWNDPROC)(int code, WPARAM wParam, LPARAM lParam) {
    Ud_Print(L"[Hook] process = %ld, res = %ld\n", (DWORD)wParam,
             (DWORD)((PCWPRETSTRUCT)lParam)->lResult);
    return CallNextHookEx(NULL, code, wParam, lParam);
  }
3.2 WH_CBT
  • 功能:当窗口过程接收到系统事件消息调用

  • 参数:

    nCode含义wParamlParam
    HCBT_MOVESIZE即将移动窗口或调整其大小指定要移动或调整大小的窗口的句柄指定指向包含窗口坐标的 RECT 结构的指针
    HCBT_MINMAX窗口即将最小化或最大化指定要最小化或最大化的窗口的句柄在低序字中,指定一个显示窗口值 (SW_) 指定操作
    HCBT_QS系统已从系统消息队列中检索WM_QUEUESYNC消息必须为零必须为零
    HCBT_CREATEWND即将创建一个窗口指定新窗口的句柄指向包含窗口初始化参数 的CBT_CREATEWND 结构的指针
    HCBT_DESTROYWND窗口即将被销毁指定要销毁的窗口的句柄必须设置为零
    HCBT_ACTIVATE系统即将激活窗口指定要激活的窗口的句柄指向 CBTACTIVATESTRUCT 结构的指针
    HCBT_CLICKSKIPPED系统已从系统消息队列中删除鼠标消息指定从系统消息队列中删除的鼠标消息指向 MOUSEHOOKSTRUCT 结构的指针
    HCBT_KEYSKIPPED系统已从系统消息队列中删除键盘消息指定虚拟密钥代码指定重复计数、扫描代码、键转换代码、以前的键状态和上下文代码
    HCBT_SYSCOMMAND即将执行系统命令指定系统命令值包含与WM_SYSCOMMAND消息的 lParam 值相同的数据
    HCBT_SETFOCUS窗口即将接收键盘焦点指定获得键盘焦点的窗口的句柄指定失去键盘焦点的窗口的句柄
  • 示例

  LRESULT(CALLBACK HOOK_CBT)(int code, WPARAM wParam, LPARAM lParam) {
    wchar_t* event = UnKnown;
    switch (code) {
      case HCBT_CLICKSKIPPED: {
        event = str(HCBT_CLICKSKIPPED);
        Ud_Print(L"[Hook] message = %s, wParam = %ld, point = (%d, %d)\n", event,
                 (DWORD)wParam, ((PMOUSEHOOKSTRUCT)lParam)->pt.x,
                 ((PMOUSEHOOKSTRUCT)lParam)->pt.y);
        return 0;
      }
      case HCBT_KEYSKIPPED:
        event = str(HCBT_KEYSKIPPED);
        break;
      case HCBT_QS:
        event = str(HCBT_QS);
        break;
        // allow the action with 0, otherwise forbidden
      case HCBT_ACTIVATE: {
        event = str(HCBT_ACTIVATE);
        Ud_Print(L"[Hook] message = %s, Wnd = %ld, mouse = %d\n", event,
                 (DWORD)wParam, ((LPCBTACTIVATESTRUCT)lParam)->fMouse);
        return 0;
      }
      case HCBT_CREATEWND: {
        event = str(HCBT_CREATEWND);
        Ud_Print(L"[Hook] message = %s, Name = %s, (x,y,l,h) = (%d,%d),(%d,%d)\n",
                 event, ((LPCBT_CREATEWND)lParam)->lpcs->lpszName,
                 ((LPCBT_CREATEWND)lParam)->lpcs->x,
                 ((LPCBT_CREATEWND)lParam)->lpcs->y,
                 ((LPCBT_CREATEWND)lParam)->lpcs->cx,
                 ((LPCBT_CREATEWND)lParam)->lpcs->cy);
        return 0;
      }
      case HCBT_MOVESIZE: {
        event = str(HCBT_MOVESIZE);
        Ud_Print(L"[Hook] message = %s, Wnd = %ld, Point = (%d,%d), (%d,%d)\n",
                 event, (DWORD)wParam, ((PRECTL)lParam)->left,
                 ((PRECTL)lParam)->top, ((PRECTL)lParam)->right,
                 ((PRECTL)lParam)->bottom);
        return 0;
      }
      case HCBT_SYSCOMMAND: {
        event = str(HCBT_SYSCOMMAND);
        Ud_Print(L"[Hook] message = %s, event = %ld, point = (%d, %d)\n", event,
                 (DWORD)wParam, GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
        return 0;
      }
      case HCBT_DESTROYWND:
        event = str(HCBT_DESTROYWND);
        break;
      case HCBT_MINMAX:
        event = str(HCBT_MINMAX);
        break;
      case HCBT_SETFOCUS:
        event = str(HCBT_SETFOCUS);
        break;
      default:
        return CallNextHookEx(NULL, code, wParam, lParam);
    }
    Ud_Print(L"[Hook] message = %s, wParam = %ld, lParam = %ld\n", event,
             (DWORD)wParam, (DWORD)lParam);
    return 0;
  }
3.3 WH_DEBUG
  • 功能:当有挂钩过程创建时调用

  • 参数:

    wParamlParam
    即将调用的挂钩类型指向 DEBUGHOOKINFO 结构的指针
  • 示例

  LRESULT(CALLBACK HOOK_DEBUG)(int code, WPARAM wParam, LPARAM lParam) {
    if (code == HC_ACTION) {
      Ud_Print(L"[Hook] message = %s, hook type = %ld\n", str(HC_ACTION),
               (DWORD)wParam);
    }
    return CallNextHookEx(NULL, code, wParam, lParam);
  }
3.4 WH_FOREGROUNDIDLE
  • 功能:当有前台线程空闲时调用

  • 参数:

  • 示例

  LRESULT(CALLBACK HOOK_FOREGROUNDIDLE)(int code, WPARAM wParam, LPARAM lParam) {
    if (code == HC_ACTION) {
      Ud_Print(L"[Hook] message = %s\n", str(HC_ACTION));
    }
    return CallNextHookEx(NULL, code, wParam, lParam);
  }
3.5 WH_GETMESSAGE
  • 功能:当消息发布到消息队列时调用

  • 参数:

    wParamlParam
    指定是否已从队列中删除消息指向包含消息详细信息的 MSG 结构的指针
  • 示例

  LRESULT(CALLBACK HOOK_GETMESSAGE)(int code, WPARAM wParam, LPARAM lParam) {
    if (code == HC_ACTION) {
      Ud_Print(L"[Hook] message = %s, event = %ld, msg = %d\n", str(HC_ACTION),
               (DWORD)wParam, ((PMSG)lParam)->message);
    }
    return CallNextHookEx(NULL, code, wParam, lParam);
  }
3.6 WH_JOURNALPLAYBACK、WH_JOURNALRECORD
  • 功能:记录和回放消息调用

  • 参数:

    nCode含义lParam
    HC_ACTION指向 EVENTMSG 结构的指针
    HC_SYSMODALOFF系统模式对话框已被销毁
    HC_SYSMODALON正在显示系统模式对话框
    HC_GETNEXT挂钩过程必须将当前消息复制到 lParam指向 EVENTMSG 结构的指针
    HC_NOREMOVE指示在 PeekMessage 处理后不会从消息队列中删除该消息
    HC_SKIP挂钩过程必须准备将下一个鼠标或键盘消息复制到 lParam
  • 示例

  void DO_JOURNALRECORD(bool);
  void DO_JOURNALPLAYBACK(bool);
  
  bool end_ = false;
  int cur_ = 0;
  bool flag = false;
  LRESULT(CALLBACK HOOK_JOURNALPLAYBACK)
  (int code, WPARAM wParam, LPARAM lParam) {
    wchar_t* event = UnKnown;
    if (code == HC_GETNEXT) {
      event = str(HC_GETNEXT);
    } else if (code == HC_SKIP) {
      event = str(HC_SKIP);
    } else if (code == HC_NOREMOVE) {
      event = str(HC_NOREMOVE);
    } else if (code == HC_SYSMODALOFF) {
      event = str(HC_SYSMODALOFF);
    } else if (code == HC_SYSMODALON) {
      event = str(HC_SYSMODALON);
    }
  
    Ud_Print(L"[Hook] message = %s, wParam = %ld, lParam = %ld\n", event,
             (DWORD)wParam, (DWORD)lParam);
  
    if (code == HC_SKIP) {
      flag = true;
      if (++cur_ >= ev_record_.size()) {
        std::cout << "DO_JOURNALPLAYBACK Empty!" << std::endl;
        DO_JOURNALPLAYBACK(false);
        PostQuitMessage(0);
        end_ = true;
      }
      return 0;
    } else if (code == HC_GETNEXT) {
      DWORD time = 0;
      PEVENTMSG pEv = (PEVENTMSG)lParam;
      if (cur_ < ev_record_.size() && pEv) {
        *pEv = ev_record_[cur_];
        if (flag) {
          time =
              ev_record_[cur_ + 1 >= ev_record_.size() ? cur_ : cur_ + 1].time -
              ev_record_[cur_].time;
          flag = false;
        }
      }
      if (time < 0) time = 1;
      return time;
    } else if (code == HC_NOREMOVE) {
      return 0;
    }
  
    return CallNextHookEx(NULL, code, wParam, lParam);
  }
  
  bool stop_ = false;
  LRESULT(CALLBACK HOOK_JOURNALRECORD)
  (int code, WPARAM wParam, LPARAM lParam) {
    wchar_t* event = UnKnown;
    if (code == HC_ACTION) {
      event = str(HC_ACTION);
    } else if (code == HC_SYSMODALOFF) {
      event = str(HC_SYSMODALOFF);
      stop_ = false;
    } else if (code == HC_SYSMODALON) {
      event = str(HC_SYSMODALON);
      stop_ = true;
    }
    PEVENTMSG pEv = (PEVENTMSG)lParam;
    Ud_Print(L"[Hook] message = %s, wParam = %ld, lParam = %ld\n", event,
             (DWORD)wParam, (DWORD)(code == HC_ACTION ? pEv->message : lParam));
    if (!stop_ && !end_) {
      // https://learn.microsoft.com/zh-cn/windows/win32/inputdev/keyboard-input-notifications
      // https://learn.microsoft.com/zh-cn/windows/win32/inputdev/mouse-input-notifications
      if (pEv->message == WM_KEYDOWN) {
        if (LOBYTE(pEv->paramL) == VK_CANCEL) {
          std::cout << "DO_JOURNALRECORD VK_CANCEL!" << std::endl;
          DO_JOURNALRECORD(false);
          PostQuitMessage(0);
          return 0;
        }
      }
      ev_record_.push_back(*pEv);
    }
    return CallNextHookEx(NULL, code, wParam, lParam);
  }
  
  void DO_MONITOR_JOURNAL() {
    MSG msg;
    BOOL bRet;
    while (!end_) {
      // The call is made by sending a message to the thread that installed the
      // hook. Therefore, the thread that installed the hook must have a message
      // loop.
      if ((bRet = GetMessage(&msg, NULL, 0, 0)) != 0) {
        if (bRet == -1) {
          std::cout << "GetMessage Error = " << GetLastError() << std::endl;
          break;
        } else {
          TranslateMessage(&msg);
          if (msg.message == WM_CANCELJOURNAL) {
            std::cout << "DO_JOURNALRECORD WM_CANCELJOURNAL!" << std::endl;
            hook_playback_ = NULL;
            hook_record_ = NULL;
            end_ = true;
          }
          DispatchMessage(&msg);
        }
      }
    }
  }
  • 注意:安装日志挂钩的线程必须实现消息循环,否则日志挂钩无法从系统中获取消息。
3.7 WH_KEYBOARD
  • 功能:当窗口接收到键盘消息时调用

  • 参数:

    nCodewParamlParam
    HC_ACTION | HC_NOREMOVE生成 击键 消息的密钥的虚拟密钥代码重复计数、扫描代码、扩展键标志、上下文代码、以前的键状态标志和转换状态标志
  • 示例

  LRESULT(CALLBACK HOOK_KEYBOARD)(int code, WPARAM wParam, LPARAM lParam) {
    if (code == HC_ACTION) {
      Ud_Print(L"[Hook] message = %s, key event = (%ld, %ld)\n", str(HC_ACTION),
               (DWORD)wParam, (DWORD)lParam);
    } else if (code == HC_NOREMOVE) {
      Ud_Print(L"[Hook] message = %s, key event = (%ld, %ld)\n", str(HC_NOREMOVE),
               (DWORD)wParam, (DWORD)lParam);
    }
    return CallNextHookEx(NULL, code, wParam, lParam);
  }
3.8 WH_MOUSE
  • 功能:当窗口接收到鼠标消息时调用

  • 参数:

    nCodewParamlParam
    HC_ACTION | HC_NOREMOVE鼠标消息的标识符指向 MOUSEHOOKSTRUCT 结构的指针
  • 示例

  LRESULT(CALLBACK HOOK_MOUSE)(int code, WPARAM wParam, LPARAM lParam) {
    if (code == HC_ACTION) {
      Ud_Print(L"[Hook] message = %s, event = %ld, point = (%d, %d)\n",
               str(HC_ACTION), (DWORD)wParam, ((PMOUSEHOOKSTRUCT)lParam)->pt.x,
               ((PMOUSEHOOKSTRUCT)lParam)->pt.y);
    } else if (code == HC_NOREMOVE) {
      Ud_Print(L"[Hook] message = %s, event = %ld, point = (%d, %d)\n",
               str(HC_NOREMOVE), (DWORD)wParam, ((PMOUSEHOOKSTRUCT)lParam)->pt.x,
               ((PMOUSEHOOKSTRUCT)lParam)->pt.y);
    }
    return CallNextHookEx(NULL, code, wParam, lParam);
  }
3.9 WH_KEYBOARD_LL
  • 功能:当系统接收到键盘消息时调用

  • 参数:

    nCodewParamlParam
    HC_ACTION键盘消息的标识符指向 KBDLLHOOKSTRUCT 结构的指针
  • 示例

  static bool end_ = false;
  LRESULT(CALLBACK HOOK_KEYBOARD_LL)(int code, WPARAM wParam, LPARAM lParam) {
    if (code == HC_ACTION) {
      Ud_Print(L"[Hook] message = %s, event = %ld, key event = (%d, %d)\n",
               str(HC_ACTION), (DWORD)wParam, ((PKBDLLHOOKSTRUCT)lParam)->vkCode,
               ((PKBDLLHOOKSTRUCT)lParam)->flags);
      if (wParam == WM_KEYDOWN) {
        if (((PKBDLLHOOKSTRUCT)lParam)->vkCode == VK_CANCEL) {
          end_ = true;
          return 1;
        }
      }
    }
  
    return CallNextHookEx(NULL, code, wParam, lParam);
  }
  • 注意:安装低级挂钩的线程必须实现消息循环,否则挂钩无法从系统中获取消息。
3.10 WH_MOUSE_LL
  • 功能:当系统接收到鼠标消息时调用

  • 参数:

    nCodewParamlParam
    HC_ACTION鼠标消息的标识符指向 MSLLHOOKSTRUCT 结构的指针
  • 示例

  static bool end_ = false;
  LRESULT(CALLBACK HOOK_MOUSE_LL)(int code, WPARAM wParam, LPARAM lParam) {
    if (code == HC_ACTION) {
      Ud_Print(L"[Hook] message = %s, event = %ld, point = (%d, %d)\n",
               str(HC_ACTION), (DWORD)wParam, ((PMSLLHOOKSTRUCT)lParam)->pt.x,
               ((PMSLLHOOKSTRUCT)lParam)->pt.y);
    }
    return CallNextHookEx(NULL, code, wParam, lParam);
  }
  • 注意:安装低级挂钩的线程必须实现消息循环,否则挂钩无法从系统中获取消息。
3.11 WH_MSGFILTER
  • 功能:当系统接收到键盘消息时调用

  • 参数:

    nCode含义lParam
    MSGF_DDEMGR
    MSGF_DIALOGBOX事件发生在消息框或对话框中指向 MSG 结构的指针
    MSGF_MENU事件发生在菜单中指向 MSG 结构的指针
    MSGF_SCROLLBAR事件发生在滚动条中指向 MSG 结构的指针
  • 示例

  LRESULT(CALLBACK HOOK_MSGFILTER)(int code, WPARAM wParam, LPARAM lParam) {
    wchar_t* event = UnKnown;
    switch (code) {
      case MSGF_DDEMGR:  // DDEML event
        event = str(MSGF_DDEMGR);
        break;
      case MSGF_DIALOGBOX:  // dialog event
        event = str(MSGF_DIALOGBOX);
        break;
      case MSGF_MENU:  // menu event
        event = str(MSGF_MENU);
      case MSGF_SCROLLBAR:  // scrollbar event
        event = str(MSGF_SCROLLBAR);
        break;
      default:
        return CallNextHookEx(NULL, code, wParam, lParam);
    }
    Ud_Print(L"[Hook] message = %s, event = %d\n", event,
             ((PMSG)lParam)->message);
    return CallNextHookEx(NULL, code, wParam, lParam);
  }
3.12 WH_SYSMSGFILTER
  • 功能:当系统接收到键盘消息时调用

  • 参数:

    nCode含义lParam
    MSGF_DIALOGBOX事件发生在消息框或对话框中指向 MSG 结构的指针
    MSGF_MENU事件发生在菜单中指向 MSG 结构的指针
    MSGF_SCROLLBAR事件发生在滚动条中指向 MSG 结构的指针
  • 示例

  LRESULT(CALLBACK HOOK_SYSMSGFILTER)(int code, WPARAM wParam, LPARAM lParam) {
    wchar_t* event = UnKnown;
    switch (code) {
      case MSGF_DIALOGBOX:  // dialog event
        event = str(MSGF_DIALOGBOX);
        break;
      case MSGF_MENU:  // menu event
        event = str(MSGF_MENU);
        break;
      case MSGF_SCROLLBAR:  // scrollbar event
        event = str(MSGF_SCROLLBAR);
        break;
      default:
        return CallNextHookEx(NULL, code, wParam, lParam);
    }
    Ud_Print(L"[Hook] message = %s, event = %d\n", event,
             ((PMSG)lParam)->message);
    return CallNextHookEx(NULL, code, wParam, lParam);
  }
3.12 WH_SHELL
  • 功能:当系统接收到Shell事件时调用

  • 参数:

    nCode含义wParamlParam
    HSHELL_WINDOWCREATED已创建顶级的无所有者窗口所创建窗口的句柄
    HSHELL_WINDOWDESTROYED一个顶级的、无所有者的窗口即将被销毁已销毁窗口的句柄
    HSHELL_ACTIVATESHELLWINDOWshell 应激活其main窗口
    HSHELL_WINDOWACTIVATED激活已更改为其他顶级的无所有者窗口已激活窗口的句柄如果窗口处于全屏模式,则值为 TRUE,否则值为 FALSE
    HSHELL_GETMINRECT窗口正在最小化或最大化最小化或最大化窗口的句柄指向 RECT 结构的指针
    HSHELL_REDRAW任务栏中窗口的标题已重绘重绘窗口的句柄如果窗口闪烁,则值为 TRUE,否则值为 FALSE
    HSHELL_TASKMAN用户已选择任务列表
    HSHELL_LANGUAGE键盘语言已更改或加载了新的键盘布局窗口的句柄键盘布局的句柄
    HSHELL_ACCESSIBILITYSTATE辅助功能状态已更改指示哪个辅助功能已更改状态
    HSHELL_APPCOMMAND用户完成了输入事件指示最初发送WM_APPCOMMAND消息的位置包含与WM_APPCOMMAN消息的 lParam 值相同的数据
    HSHELL_WINDOWREPLACED正在替换顶级窗口要替换的窗口的句柄新窗口的句柄
  • 示例

  LRESULT(CALLBACK HOOK_SHELL)(int code, WPARAM wParam, LPARAM lParam) {
    wchar_t* event = UnKnown;
    switch (code) {
      case HSHELL_ACCESSIBILITYSTATE:  // The accessibility state has changed.
        event = str(HSHELL_ACCESSIBILITYSTATE);
        break;
      case HSHELL_ACTIVATESHELLWINDOW:  // The shell should activate its main
                                        // window
        event = str(HSHELL_ACTIVATESHELLWINDOW);
        break;
      case HSHELL_TASKMAN:  // The user has selected the task list
        event = str(HSHELL_TASKMAN);
        break;
      case HSHELL_LANGUAGE:  // Keyboard language was changed
        event = str(HSHELL_LANGUAGE);
        break;
      case HSHELL_REDRAW:  // The title of a window in the task bar has been
                           // redrawn.
        event = str(HSHELL_REDRAW);
        break;
      case HSHELL_WINDOWACTIVATED:  // 	The activation has changed to a
                                    // different top-level, unowned window.
        event = str(HSHELL_WINDOWACTIVATED);
        break;
      case HSHELL_WINDOWCREATED:  // 	A top-level, unowned window has been
                                  // created.
        event = str(HSHELL_WINDOWCREATED);
        break;
      case HSHELL_WINDOWDESTROYED:  // A top-level, unowned window is about to be
                                    // destroyed.
        event = str(HSHELL_WINDOWDESTROYED);
        break;
      case HSHELL_WINDOWREPLACED:  // A top-level window is being replaced.
        event = str(HSHELL_WINDOWREPLACED);
        break;
      case HSHELL_GETMINRECT:  // A window is being minimized or maximized.
      {
        PRECT pRect = (PRECT)lParam;
        Ud_Print(
            L"[Hook] message = %s, Wnd = %ld, Point = (%d,%d), "
            L"(%d,%d)\n",
            str(HSHELL_GETMINRECT), (DWORD)wParam, pRect->left, pRect->top,
            pRect->right, pRect->bottom);
        return 0;
      }
      case HSHELL_APPCOMMAND: {
        Ud_Print(L"[Hook] message = %s, Cmd = %d, Device = %d, KeyState = %d\n",
                 str(HSHELL_APPCOMMAND), GET_APPCOMMAND_LPARAM(lParam),
                 GET_DEVICE_LPARAM(lParam), GET_KEYSTATE_LPARAM(lParam));
        return 0;
      };
      default:
        return CallNextHookEx(NULL, code, wParam, lParam);
    }
  
    Ud_Print(L"[Hook] message = %s, wParam = %ld, lParam = %ld\n", event,
             (DWORD)wParam, (DWORD)lParam);
    return 0;
  }
  • 16
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值