windows消息机制-4(MFC)

2023-05-16

消息分类与消息队列

Windows中,消息使用统一的结构体(MSG)来存放信息,其中message表明消息的具体的类型,

而wParam,lParam是其最灵活的两个变量,为不同的消息类型时,存放数据的含义也不一样。

time表示产生消息的时间,pt表示产生消息时鼠标的位置。

按照类型,Windows将消息分为:

(0) 消息ID范围

系统定义消息ID范围:[0x0000, 0x03ff]     例:WM_NULL(0x0000)表示空消息  WM_CLOSE(0x0010)表示窗口关闭消息
用户自定义的消息ID范围:
WM_USER: 0x0400-0x7FFF (例:WM_USER+10)
WM_APP(winver> 4.0):0x8000-0xBFFF (例:WM_APP+4)
RegisterWindowMessage:0xC000-0xFFFF【用来和其他应用程序通信,为了ID的唯一性,使用::RegisterWindowMessage来得到该范围的消息ID 】

(1) 窗口消息:即与窗口的内部运作有关的消息,如创建窗口,绘制窗口,销毁窗口等。

     可以是一般的窗口,也可以是MainFrame,Dialog,控件等。 

     如:WM_CREATE, WM_PAINT, WM_MOUSEMOVE, WM_CTLCOLOR, WM_HSCROLL等

(2) 当用户从菜单选中一个命令项目、按下一个快捷键或者点击工具栏上的一个按钮,都将发送WM_COMMAND命令消息。

     LOWORD(wParam)表示菜单项,工具栏按钮或控件的ID;如果是控件, HIWORD(wParam)表示控件消息类型。

     #define LOWORD(l) ((WORD)(l))

     #define HIWORD(l) ((WORD)(((DWORD)(l) >> 16) & 0xFFFF))

(3) 随着控件的种类越来越多,越来越复杂(如列表控件、树控件等),仅仅将wParam,lParam将视为一个32位无符号整数,已经装不下太多信息了。

    为了给父窗口发送更多的信息,微软定义了一个新的WM_NOTIFY消息来扩展WM_COMMAND消息。

    WM_NOTIFY消息仍然使用MSG消息结构,只是此时wParam为控件ID,lParam为一个NMHDR指针,

    不同的控件可以按照规则对NMHDR进行扩充,因此WM_NOTIFY消息传送的信息量可以相当的大。

注:Window 9x 版及以后的新控件通告消息不再通过WM_COMMAND 传送,而是通过WM_NOTIFY 传送,
      但是老控件的通告消息, 比如CBN_SELCHANGE 还是通过WM_COMMAND 消息发送。

(4) windwos也允许程序员定义自己的消息,使用SendMessage或PostMessage来发送消息。

windows消息还可以分为:

(1) 队列消息(Queued Messages)
消息会先保存在消息队列中,消息循环会从此队列中取出消息并分发到各窗口处理
如:WM_PAINT,WM_TIMER,WM_CREATE,WM_QUIT,以及鼠标,键盘消息等。
其中,WM_PAINT,WM_TIMER只有在队列中没有其他消息的时候才会被处理,
WM_PAINT消息还会被合并以提高效率。其他所有消息以先进先出(FIFO)的方式被处理。

(2) 非队列消息(NonQueued Messages) 
消息会绕过系统消息队列和线程消息队列,直接发送到窗口过程进行处理 
如:WM_ACTIVATE, WM_SETFOCUS, WM_SETCURSOR,WM_WINDOWPOSCHANGED

Windows系统的整个消息系统分为3个层级:

    ① Windows内核的系统消息队列

    ② App的UI线程消息队列

    ③ 处理消息的窗体对象

Windows内核维护着一个全局的系统消息队列;按照线程的不同,系统消息队列中的消息会分发到应用程序的UI线程的消息队列中;

应用程序的每一个UI线程都有自己的消息循环,会不停地从自己的消息队列取出消息,并发送给Windows窗体对象;

每一个窗体对象都使用窗体过程函数(WindowProc)来处理接收到的各种消息。


 1 LRESULT CALLBACK WindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
 2 {
 3     PAINTSTRUCT ps;
 4     HDC hdc;
 5 
 6     switch (message)
 7     {
 8     case WM_COMMAND:
 9         break;
10     case WM_PAINT:
11         hdc = BeginPaint(hWnd, &ps);
12         // TODO: 在此添加任意绘图代码...
13         EndPaint(hWnd, &ps);
14         break;
15     case WM_DESTROY:
16         PostQuitMessage(0);
17         break;
18     default:
19         return DefWindowProc(hWnd, message, wParam, lParam);
20     }
21     return 0;
22 }  

需要的话,在WindowProc中,可以用::GetMessageTime获取当前消息产生的时间,
用::GetMessagePos获取当前消息产生时鼠标光标所在的位置。

(1) 各个窗口消息由各个窗体(或控件)自身的WindowProc(虚函数)接收并处理。

(2) WM_COMMAND命令消息统一由当前活动主窗口的WindowProc接收,经过绕行后,可被其他的CCmdTarget对象处理。

(3) WM_COMMAND控件通知统一由子窗口(控件)的父窗口的WindowProc接收并处理,也可以进行绕行被其他的CCmdTarget对象处理。

     (例如:CFormView具备接受WM_COMMAND控件通知的条件,又具备把WM_COMMAND消息派发给关联文档对象处理的能力,

         所以给CFormView的WM_COMMAND控件通知是可以让文档对象处理的。)

     另外,WM_COMMAND控制通知会先调用ReflectLastMsg反射通知子窗口(控件),如果子窗口(控件)处理了该消息并返回TRUE,则消息会停止分发;

     否则,会继续调用OnCmdMsg进行命令发送(如同WM_COMMAND命令消息一样)。

注:WM_COMMAND命令消息与WM_COMMAND控件通知的相似之处:
WM_COMMAND命令消息和WM_COMMAND控制通知都是由WindowProc给OnCommand处理,
OnCommand通过wParam和lParam参数区分是命令消息或通知消息,然后送给OnCmdMsg处理。
事实上,BN_CLICKED控件通知消息的处理和WM_COMMAND命令消息的处理完全一样。
因为该消息的通知代码是0,ON_BN_CLICKED(id,memberfunction)和ON_COMMAND(id,memberfunction)是等同的。

(4)WM_NOTIFY消息只是对WM_COMMAND控件通知进行了扩展,与WM_COMMAND控件通知具有相同的特点。

SendMessage与PostMessage

PostMessage 把消息投递到消息队列后,立即返回;
SendMessage把消息直接送到窗口过程处理,处理完才返回。

GetMessage与PeekMessage

GetMessage 有消息且该消息不为WM_QUIT,返回TRUE。
              有消息且该消息为WM_QUIT,返回FALSE。
                  没有消息时,挂起该UI线程,控制权交还给系统。
PeekMessage 有消息返回TRUE,如果没有消息返回FALSE;不会阻塞。
                   是否从消息队列中删除此消息(PM_REMOVE),由函数参数来指定。

要想在没有消息时做一些工作,就必须使用PeekMessage来抓取消息,以便在没有消息时,能在OnIdle中执行空闲操作(如下):


 1 while (TRUE) 
 2 {
 3     if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE) 
 4     {
 5         if (msg.message == WM_QUIT)
 6             break;
 7         TranslateMessage(&msg);
 8         DispatchMessage(&msg);
 9     }
10     else 
11     {
12         OnIdle();
13     }
14 }  

例如:MFC使用OnIdle函数来清理一些临时对象及未使用的动态链接库。

只有在OnIdle返回之后程序才能继续处理用户的输入,因此不应在OnIdle进行较长的任务。

MFC消息处理

在CWnd中,MFC使用OnWndMsg来分别处理各类消息:

如果是WM_COMMAND消息,交给OnCommand处理;然后返回。

如果是WM_NOTIFY消息,交给OnNotify处理;然后返回。

如果是WM_ACTIVATE消息,先交给_AfxHandleActivate处理,再继续下面的处理。

如果是WM_SETCURSOR消息,先交给_AfxHandleSetCursor处理,然后返回。

如果是其他的窗口消息(包括WM_ACTIVATE消息),则

  首先在消息缓冲池(一个hash表,用于加快消息处理函数的查找)进行消息匹配,
    若匹配成功,则调用相应的消息处理函数;
    若不成功,则在消息目标的消息映射数组中进行查找匹配,看它是否能处理当前消息。
  如果消息目标处理了该消息,则会匹配到消息处理函数,调用它进行处理;

否则,该消息没有被应用程序处理,OnWndMsg返回FALSE。

MFC消息映射

消息映射实际是MFC内建的一个消息分派机制。

把MFC中的宏进行展开(如下),可以得到消息映射表整个全貌。

注:GetMessageMap为虚函数。
     {0, 0, 0, 0, AfxSig_end, (AFX_PMSG)0}:对象消息映射表的结束标识

窗口消息只能由CWnd对象来处理,采用向基类直线上朔的方式,来查找对应的消息响应函数进行处理。

一旦找到消息响应函数(若有返回值且为TRUE),就停止上朔。因此,我们经常会看到这样的代码:

增加一个消息处理函数来写我们的逻辑时,MFC ClassWizard会在该函数之前或之后显示调用其基类对应的函数,保证基类中逻辑被执行。

命令消息可由CCmdTarget对象接收并处理(OnCmdMsg为虚函数),除了向基类直线上朔方式外,还有命令绕行机制(要防止形成圈,死循环)。

在某种程度上,控制通知消息由窗口对象处理是一种习惯和约定。然而,控件通知消息也是可以有CCmdTarget对象接收并处理,并进行命令绕行的。

下图为MFC经典单文档视图框架的命令消息绕行路线:

函数调用过程如下(如果没有任何对象处理该条WM_COMMAND消息,最后会被::DefWindowProc处理)。

非模态对话框的消息处理


1 static CAboutDlg aboutDlg;
2 aboutDlg.Create(IDD_ABOUTBOX, this);
3 aboutDlg.ShowWindow(SW_SHOW);  

应用程序只有一个消息循环。

对于窗口消息,非模态对话框(及其子控件)与父窗口(及其子控件)都是用自身的WindowProc函数接收并处理,互不干扰。

对于命令消息,由当前活动主窗口的WindowProc接收(例如:当前活动主窗口为非模态对话框,则命令消息会被非模态对话框接收)。

可以在当前活动主窗口的OnCmdMsg中做命令绕行,使得其他的CCmdTarget对象也可以处理命令消息。

对于控件通知,由其父窗口的WindowProc接收并处理,一般不进行命令绕行被其他的CCmdTarget对象处理。

模态对话框的消息处理


1 CAboutDlg aboutDlg;
2 aboutDlg.DoModal();  

(1) 模态对话框弹出来后,首先会让父窗口失效,使其不能接受用户的输入(键盘鼠标消息)。


1 EnableWindow(hwndParent, FALSE) ;  

(2) 父窗口消息循环被阻塞(会卡在DoModal处,等待返回),由模态对话框的消息循环来接管(因此整个程序不会卡住)。

    接管后,模态对话框的消息循环仍然会将属于父窗口及其子控件的窗口消息(不包括键盘鼠标相关的窗口消息)发送给它们各自的WindowProc窗口函数,进行响应处理。

(3) 模态对话框销毁时(点击IDOK或IDCANCEL),父窗口消息循环重新**,继续DoModal后的逻辑。

    **后,父窗口有可以重新接受用户的输入(键盘鼠标消息)。


1 EnableWindow(hwndParent, TRUE) ;  

从上面的过程中,我们可以得到如下结论:

对于窗口消息,模态对话框主窗口(及其子控件)与父窗口(及其子控件)都是用自身的WindowProc函数接收并处理,互不干扰。

只是父窗口(及其子控件)无法接受到键盘鼠标消息相关的窗口消息。

对于命令消息,由模态对话框主窗口的WindowProc接收。可以在模态对话框主窗口的OnCmdMsg中做命令绕行,使得其他的CCmdTarget对象也可以处理命令消息。

对于控件通知,由其父窗口的WindowProc接收并处理,一般不进行命令绕行被其他的CCmdTarget对象处理。

参考

《深入浅出MFC》- 侯捷

《MFC教程》- 消息映射的实现

参考:https://www.cnblogs.com/kekec/p/3210696.html

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

windows消息机制-4(MFC) 的相关文章

  • 使用 Python 将 Excel 中的图表导出为图像

    我一直在尝试将 Excel 中的图表导出为 Python 中的图像文件 JPG 或 ING 我正在查看 WIn32com 这是我到目前为止所拥有的 import win32com client as win32 excel win32 ge
  • 如何将CString转换为整数和浮点数? [复制]

    这个问题在这里已经有答案了 我正在尝试转换CStringint 和 float 但无法找到任何 C 库函数来完成此操作 请帮忙 正确的UNICODE 符合规定的方式MFC如下 CString sInt T 10 int n ttoi sIn
  • 获取已创建进程的进程句柄 Windows

    我需要获取运行程序时刚刚创建的所有进程的句柄或 PID 到目前为止 我已经使用了这段代码 每次创建进程时都会告诉我 问题是我只获取有关创建的进程的信息 但没有有关进程本身的任何信息 https msdn microsoft com en u
  • 如何在 PowerShell 中键入 TAB 字符?

    Task 默认情况下 在 Windows 命令提示符中按 TAB 键将输出文件名 而在 PowerShell 中则不会执行任何操作 我希望能够在交互模式下键入 TAB 字符 而不是通过脚本 Research 我在这个网站上和通过谷歌搜索发现
  • 为什么 cygwin 无法读取我的主目录中的 .ssh/config 文件?

    我正在使用 cygwin 并且正在尝试设置 ssh config 文件 Cygwin 的文件夹位于我的 C 驱动器中 我的主目录是 C Users USER 我注意到 当我编辑 C Users USER ssh config 时 cygwi
  • 使用 IDLE 编辑的 .py 文件消失了

    我曾经有过Edit with IDLE当我右键单击时的选项 py文件 但我多次卸载 重新安装以使某些东西正常工作 但现在它消失了 我检查了注册表HKEY CLASSES ROOT and HKEY LOCAL MACHINE对于价值低于Py
  • Python 无法在 git bash 命令行中工作

    Python 不会在 git bash Windows 中运行 当我在命令行中输入 python 时 它会将我带到一个空行 而不会像在 Powershell 中一样显示它已输入 python 2 7 10 它没有给我错误消息 但 pytho
  • Windows Workflow Foundation 4 (WF4) 延迟

    我正在与 Visual Studio 2010 的候选版本一起使用 Wf4 编写一个用于批准资源请求的新工作流程 在我的工作流程中 如果请求未获得批准 我希望请求在几天后过期 我们在 WF 3 5 Visual Studio 2008 中通
  • 如何从任何进程关闭 Windows 上的套接字(ipv4 和 ipv6)连接?

    如何在 Windows 上关闭 tcp v4 和 tcp v6 连接 我不想终止具有开放连接的整个进程 因为这显然会将其他人踢出该进程 我需要从一个单独的进程执行此操作 因此无法访问套接字句柄等 我正在使用 Windows API 来获取
  • 如何在 C++ 中急于提交分配的内存?

    总体情况 带宽 CPU 使用率和 GPU 使用率都极其密集的应用程序需要每秒从一个 GPU 向另一个 GPU 传输约 10 15GB 的数据 它使用 DX11 API 来访问 GPU 因此上传到 GPU 只能在每次上传都需要映射的缓冲区中进
  • 移动/调整窗口大小时闪烁

    我开发了一个显示 jpeg 图像的应用程序 它可以显示 4 个图像 屏幕的每个象限各一个 为此 它使用了 4 个窗口 窗口没有边框 框架 也没有标题栏 当加载新图像时 窗口大小会根据新图像进行调整 然后显示该图像 尤其是当窗户做得较大时 经
  • 设置 Form.KeyPreview = true 的缺点?

    我想知道 Form KeyPreview 属性实际上有什么用处 它为什么存在以及将其设置为 true 会带来什么 风险 我想它一定有some负面影响 否则它根本不应该存在 或者至少默认情况下是正确的 EDIT 我很清楚what确实如此 我问
  • 访问图像的 Windows“标签”元数据字段

    我正在尝试进行一些图像处理 所以现在我正在尝试读取图像 exif 数据 有 2 个内置函数可用于读取图像的 exif 数据 问题是我想读取图像标签 exifread and imfinfo这两个函数都不显示图像标签 Is there any
  • 本地推送通知到在应用程序内运行 JS 代码的 Win8 Live Tile

    我正在尝试将更新发送到我的应用程序的磁贴 当应用程序运行时 这可以正常工作 例如 当用户单击按钮时 我可以轻松地将磁贴更新通知发送到磁贴 我无法解决的是当应用程序无法运行时如何更新磁贴 我找到的唯一选择是使用以下命令从远程 Web 服务器拉
  • Vim 在 Mingw 上表现异常

    我在 MinGW 4 6 2 上的 Vim 表现得很奇怪 例如 在插入模式下按 Backspace 会删除字符 但我必须用箭头键移动光标才能删除的字符消失 而且它也会使我退出插入模式 另一个例子 按 Del 删除字符有时会生成奇怪的字符 例
  • 相当于Linux中的导入库

    在 Windows C 中 当您想要链接 DLL 时 您必须提供导入库 但是在 GNU 构建系统中 当您想要链接 so 文件 相当于 dll 时 您就不需要链接 为什么是这样 是否有等效的 Windows 导入库 注意 我不会谈论在 Win
  • 如何将 GIT 调用的输出获取到批处理脚本中的变量中?

    我有一个 git 命令来获取当前存储库的最新 SHA 如下所示 git log pretty format H n 1 我有一个 Windows 批处理脚本 我想按如下方式使用它 SET CURRENT SHA 但我不知道如何将从 git
  • 如何在批处理文件中回显换行符?

    如何从批处理文件输出中插入换行符 我想做类似的事情 echo hello nworld 这会输出 hello world Use echo hello echo echo world
  • 在哪里可以获得 PHP 5.3+ 的 runkit DLL 扩展?

    这是一个简单的问题 我在哪里可以获得 PHP 5 3 版本的 runkit 扩展 它的手册 http php net manual en book runkit php http php net manual en book runkit
  • Windows C++ 中的键盘钩子还是什么?

    我希望构建自己的应用程序 它可以将键盘命令 消息 发送到 Windows 操作系统 例如 当我按下组合键 ctrl shift n 时 我希望启动 notepad exe 我怎样才能做到这一点 您对所使用的概念有什么建议吗 我读过 何时使用

随机推荐

  • MFC框架机制详细论述

    1 1 Windows消息机制要点 1 1 1 窗口过程 每个窗口会有一个称为窗口过程的回调函数 WndProc xff0c 它带有四个参数 xff0c 分别为 xff1a 窗口句柄 Window Handle 消息ID Message I
  • c#中WinForm中拖拽窗体实现移动功能(无边框模态窗体)

    在WindowsForm 应用程序中 如果将窗体的FormBorderStyle属性设置为none 这时 用鼠标拖拽窗体时就无法实现移动的功能了 xff0c 下面就是解决方案 在FormBordeStyle属性设置为none的窗体的后台代码
  • 关于SetCapture() 和 ReleaseCapture()的用法

    MSND中对SetCapture 函数的说明为 xff1a 该函数在属于当前线程的指定窗口里设置鼠标捕获 一旦窗口捕获了鼠标 xff0c 所有鼠标输入都针对该窗口 xff0c 无论光标是否在窗口的边界内 同一时刻只能有一个窗口捕获鼠标 如果
  • c# Invoke和BeginInvoke 区别详解

    Control Invoke 方法 Delegate 在拥有此控件的基础窗口句柄的线程上执行指定的委托 Control BeginInvoke 方法 Delegate 在创建控件的基础句柄所在线程上异步执行指定委托 以下为实际应用中碰到的问
  • WindowsAPI-Findwindow函数和FindWindowEx用法

    1 函数说明 FindWindow xff0c Win32 API函数 FindWindow函数返回与指定字符串相匹配的窗口类名或窗口名的最顶层窗口的窗口句柄 这个函数不会查找子窗口 2 函数原型 xff1a HWND FindWindow
  • 利用FindWindow和SendMessage进程通信

    利用FindWindow和SendMessage xff0c 特此记下 首先说FindWindow FindWindow返回与指定字符创相匹配的窗口类名或窗口名的最顶层窗口的窗口句柄 函数原型为 xff1a C 43 43 xff1a HW
  • vnc viewer,什么是vnc viewer

    在日常工作中 xff0c 经常会用到vnc xff0c 自然也会用到vncviewer xff0c 那你知道什么是vncviewer吗 xff1f 怎么使用vncviewer呢 xff1f 接下来让我们一起去看看吧 首先先让我们来看看一款v
  • 句柄的概念详解

    1 这里将句柄所能标识的所有东西 xff08 如窗口 文件 画笔等 xff09 统称为 对象 2 图中一个小横框表示一定大小的内存区域 xff0c 并不代表一个字节 xff0c 如标有0X00000AC6的横框表示4个字节 程序运行到某时刻
  • Server.Transfer 和 Response.Redirect 的用法

    在ASP NET中 xff0c 在后台传值方式目前大多都是用 Response Redirect 34 页面地址 34 来重定向页面的 xff0c 但是现在还有一种方式也可以达到重定向页面的作用 xff0c 而且在某些时刻会起到一种很棒的效
  • WebForm连接数据库实例

    登录页面 xff1a 用户名文本框 密码文本框 登录按钮 当用户名密码输入正确 xff0c 点击确定可以跳转到下一个页面 我们需要先引入命名空间 xff1a using System Data using System Data SqlCl
  • ASP.NET WebForm和Mvc开发的比较

    在初步了解MVC后 xff0c 发现很多人对于MVC和三层架构开发概念上会有很大的混淆 xff0c 所以把这两天的学习笔记整理一下 xff0c 分享给自己的同学们 同时也做一个小Demo xff0c 让没有接触过MVC开发的同学 xff0c
  • ASP.NET - MVC框架及搭建教程

    一 MVC简介 MVC xff1a Model View Controller xff08 模型 视图 控制器 xff09 xff0c MVC是一种软件开发架构模式 1 模型 xff08 Model xff09 模型对象是实现应用程序数据域
  • Web 绘图—服务器端绘图

    Web服务器端绘图的基本原理是 xff1a 首先在内存中创建一个Bitmap位图 xff0c 然后在此位图上绘制任意想要的图形 xff0c 绘制完成后保存输出到页面的输出流 这样 xff0c 一个页面就转换成了绘制的图片 1 xff0e 简
  • 在VS2015中安装Qt环境

    1 安装Qt 1 xff09 下载 下载网址 xff1a https download qt io archive qt 选择要下载的版本 xff0c 我选择5 12 5 xff0c 点击下载即可 点击 Detail 按钮可以看到安装文件资
  • Qt Quick 中 QML 与 C++ 混合编程详解

    Qt Quick 技术的引入 xff0c 使得你能够快速构建 UI xff0c 具有动画 各种绚丽效果的 UI 都不在话下 但它不是万能的 xff0c 也有很多局限性 xff0c 原来 Qt 的一些技术 xff0c 比如低阶的网络编程如 Q
  • windows消息机制深入详解-1

    Windows 是一个事件驱动的操作系统 事件驱动围绕着消息的产生与处 理展开 xff0c 事件驱动是靠消息循环机制来实现的 也可以理解为消息是一种报告有关事件发生 的通知 xff0c 消息是Windows 操作系统的灵魂 在屏幕显示一个窗
  • windows消息机制详解-3

    1 引言 Windows 在操作系统平台占有绝对统治地位 xff0c 基于Windows 的编程和开发越来越广泛 Dos 是过程驱动的 xff0c 而Windows 是事件驱动的 6 xff0c 这种差别的存在使得很多Dos 程序员不能 习
  • vnc server安装教程,在win系统下如何进行vnc server安装

    在日常工作中 xff0c 经常会使用到vnc server xff0c 那有小伙伴知道vnc server是什么吗 xff1f vnc server 是一般 Linux 发行版都会附带的 vnc服务器软件 vncserver 是一个为了满足
  • Windows消息机制详解-2

    消息是指什么 xff1f 消息系统对于一个win32程序来说十分重要 xff0c 它是一个程序运行的动力源泉 一个消息 xff0c 是系统定义的一个32位的值 xff0c 他唯一的定义了一个事件 xff0c 向 Windows发出一个通知
  • windows消息机制-4(MFC)

    消息分类与消息队列 Windows中 xff0c 消息使用统一的结构体 xff08 MSG xff09 来存放信息 xff0c 其中message表明消息的具体的类型 xff0c 而wParam xff0c lParam是其最灵活的两个变量