zl程序教程

您现在的位置是:首页 >  其它

当前栏目

使用Layered分层窗口实现视频会议中的桌面区域共享

实现 窗口 共享 桌面 区域 分层 视频会议 使用
2023-09-14 09:16:31 时间

目录

1、项目背景与需求

2、带透明区域窗口的实现思路

3、duilib界面库

4、调用UpdateLayeredWindow接口返回失败

5、点击标题栏无法拖动窗口

6、选择区域坐标该怎么设置给底层的图像采集编码层呢?


       你用带有WS_EX_LAYERED风格的Layered分层窗口实现过异形窗口的效果吗?在很多软件中都能看到异形窗口的身影,异形窗口以其独有的视觉效果,被广泛地采用。以常见的360安全卫士的加速球窗口为例:

异形窗口的边界一般是各种形状的圆滑线条,被做成了各种妙趣横生的各种图案效果,给用户带来了良好的视觉效果和体验。 

       其实,窗口在创建时是矩形形状的,只不过呈现出来的图案是各种独特形状的,除这些形状区域以外,矩形窗口的其他区域被做成透明效果了,并且鼠标是可以穿透这些透明的区域的。本文要讲一种特殊的异形窗口,下面请看我详细道来。

开发工具:Visual Studio 2010

开发语言:C++

1、项目背景与需求

       最近公司中标了一个比较大的项目,客户对我们的会议软件总体上是很满意的,但客户要求在已有的功能上增加一个功能,要在桌面共享功能中增加桌面区域共享的功能。客户明确提出,这个需求是硬性功能指标,必须实现这个功能后才能完成项目的中标。于是相关部门将开发需求落到了我们研发部,要求我们在最短的时间内尽快地实现这个功能。

       这个桌面区域共享,和整个桌面共享及应用窗口共享实现机制时完全一样的,都是抓取某一个区域的图像,只要UI层告诉采集编码层要抓取的区域坐标就可以了。所以主要的工作量在UI层,图像采集编码层不用做大的改动即可实现。所以这个功能点的实现重心就在于UI层的交互实现,即UI层如何选定要分享的区域,然后都需要支持哪些操作。

       这个桌面部分区域的共享,很多友商都支持了,在时间紧急没有头绪的情况下,赶紧看一下友商的实现方式。经过对比发现,小鱼易联和ZOOM的会议软件,该功能的UI交互和实现机制竟然是一模一样的,至于谁先做出来、谁模仿谁,就不得而知了。于是大概地看了一下他们的实现机制,创建一个特殊的窗口,窗口是有边界的,然后窗口的中间区域是透明的、且鼠标可穿透的,如下所示:(这种显示背景下的截图)

该特殊窗口的边界框住的区域就是要分享的区域,即框住的图像就是要分享出去的图像。通过这个窗口实现了待分享区域的选择,该选择区域的窗口支持标题栏拖动,支持拖动边框改变大小。

       初步猜测该窗口应该是具有WS_EX_LAYERED风格的分层窗口,调用UpdateLayeredWindow系统API将窗口中间区域透明掉。使用SPY++工具查看了一下该窗口的属性:

确实是分层窗口,并且设置了TOPMOST窗口置顶的属性。我们有调用UpdateLayeredWindow实现透明窗口的经验,所以本例中这样的窗口效果我们也能实现,于是基本拟定了当前桌面区域共享的几个需求点:

1)选择区域的窗口使用具有WS_EX_LAYERED窗口样式的分层窗口,调用UpdateLayeredWindow实现窗口中间区域的透明及鼠标穿透;

2)该选择区域的窗口,支持标题栏拖动窗口,支持拖动窗口边框改变窗口大小。

2、带透明区域窗口的实现思路

       这种带透明区域且鼠标可穿透的窗口,直接设计出一个带透明区域的图片,然后调用UpdateLayeredWindow系统API,将图片贴到目标窗口上即可。

       具体的实现思路是,先创建带有WS_EX_LAYERED窗口样式的分层窗口(目标窗口),将带透明区域的图片绘制到内存DC上,中间透明区域则全部是RGB(0,0,0)纯黑色,然后调用UpdateLayeredWindow将内存DC画板中的内容绘制到目标窗口上,中间黑色的区域会变成透明、鼠标可穿透的区域,图片的非透明区域则是不透明的。

       但本例中的窗口大小是可变的,需要支持拖动窗口边框改变大小的,所以不能直接将整个图片贴到窗口上,因为UCD组(美工组)提供的图片是固定大小的,不能做缩放操作的。所以我们要从图片中抠出窗口left、top、right、bottom四个方向上的边框区域,然后分块贴到内存DC上即可,只要保证中间要透明的区域是黑色的色块就可以了。贴图及调用UpdateLayeredWindow的相关代码如下所示:

#define   DESKTOP_SHARE_AREA_SEL_WND_EDGESIZE_TOP_HEIGHT       27
#define   DESKTOP_SHARE_AREA_SEL_WND_EDGESIZE_LEFT_WIDTH       5
#define   DESKTOP_SHARE_AREA_SEL_WND_EDGESIZE_RIGHT_WIDTH      5
#define   DESKTOP_SHARE_AREA_SEL_WND_EDGESIZE_BOTTOM_HEIGHT    5


void CDesktopShareAreaSelDlg::Update()
{
	if ( m_BkImg.IsNull() )
	{
		return;
	}

	RECT rcWnd;
	::GetWindowRect(m_hWnd, &rcWnd);
	int nWndWidth = rcWnd.right - rcWnd.left;
	int nWndHeight = rcWnd.bottom - rcWnd.top;

	// Create the alpha blending bitmap
	BITMAPINFO bmi;        // bitmap header

	ZeroMemory(&bmi, sizeof(BITMAPINFO));
	bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
	bmi.bmiHeader.biWidth = nWndWidth;
	bmi.bmiHeader.biHeight = nWndHeight;
	bmi.bmiHeader.biPlanes = 1;
	bmi.bmiHeader.biBitCount = 32;         // four 8-bit components
	bmi.bmiHeader.biCompression = BI_RGB;
	bmi.bmiHeader.biSizeImage = nWndWidth * nWndHeight * 4;

	BYTE *pvBits;          // pointer to DIB section
	HBITMAP hbitmap = CreateDIBSection(NULL, &bmi, DIB_RGB_COLORS, (void **)&pvBits, NULL, 0);
	if (pvBits == NULL) {
		return;
	}

	ZeroMemory(pvBits, bmi.bmiHeader.biSizeImage);

	// 创建内存DC
	HDC hMemDC = CreateCompatibleDC(NULL);
	HBITMAP hOriBmp = (HBITMAP)SelectObject(hMemDC, hbitmap);

	int nImgWidth = m_BkImg.GetWidth();
	int nImgHeight = m_BkImg.GetHeight();

	// 将窗口left、top、right、bottom四个方向上的图片绘制到窗口边框的位置
	m_BkImg.Draw( hMemDC, 0, 0, DESKTOP_SHARE_AREA_SEL_WND_EDGESIZE_LEFT_WIDTH, nWndHeight, 0, 0, DESKTOP_SHARE_AREA_SEL_WND_EDGESIZE_LEFT_WIDTH, nImgHeight );
	m_BkImg.Draw( hMemDC, nWndWidth - DESKTOP_SHARE_AREA_SEL_WND_EDGESIZE_RIGHT_WIDTH, 0, DESKTOP_SHARE_AREA_SEL_WND_EDGESIZE_RIGHT_WIDTH, nWndHeight, nImgWidth-DESKTOP_SHARE_AREA_SEL_WND_EDGESIZE_RIGHT_WIDTH, 0, DESKTOP_SHARE_AREA_SEL_WND_EDGESIZE_RIGHT_WIDTH, nImgHeight );
	m_BkImg.Draw( hMemDC, 0, 0, nWndWidth, DESKTOP_SHARE_AREA_SEL_WND_EDGESIZE_TOP_HEIGHT, 0, 0, nImgWidth, DESKTOP_SHARE_AREA_SEL_WND_EDGESIZE_TOP_HEIGHT );
	m_BkImg.Draw( hMemDC, 0, nWndHeight-DESKTOP_SHARE_AREA_SEL_WND_EDGESIZE_BOTTOM_HEIGHT, nWndWidth, DESKTOP_SHARE_AREA_SEL_WND_EDGESIZE_BOTTOM_HEIGHT, 0, nImgHeight-DESKTOP_SHARE_AREA_SEL_WND_EDGESIZE_BOTTOM_HEIGHT, nImgWidth, DESKTOP_SHARE_AREA_SEL_WND_EDGESIZE_BOTTOM_HEIGHT );

	POINT ptDst = {rcWnd.left, rcWnd.top};
	POINT ptSrc = {0, 0};
	SIZE WndSize = {nWndWidth, nWndHeight};
	BLENDFUNCTION blendPixelFunction= { AC_SRC_OVER, 0, 255, AC_SRC_ALPHA };

	BOOL bRet= UpdateLayeredWindow(m_hWnd, NULL, &ptDst, &WndSize, hMemDC,
		&ptSrc, 0, &blendPixelFunction, ULW_ALPHA);

	DWORD dwRet = GetLastError();

	InvalidateRect( m_hWnd, &rcWnd, TRUE );
	//_ASSERT(bRet); // something was wrong....

	// Delete used resources
	SelectObject(hMemDC, hOriBmp);
	DeleteObject(hbitmap);
	DeleteDC(hMemDC);
}

上述接口要在WM_SIZE消息中调用,即窗口大小改变时,要重新绘制窗口:

LRESULT CDesktopShareAreaSelDlg::HandleMessage( UINT message, WPARAM wParam, LPARAM lParam )
{
	TNotifyUI msg;
	if ( WM_SIZE == message )
	{
		Update();
	}
	else if ( WM_PAINT == message )
	{
		Update();
		return true;
	}
}

3、duilib界面库

        本文涉及到的很多代码,都和duilib有关,所以此处需要简单地说明一下,我们程序UI使用的是开源的duilib界面库,是基于微软directui思想的一套界面库。

        做UI的朋友应该大部分都知道这个duilib界面库,现在很多公司都在用,比如百度、华为、网易、爱奇艺、ZOOM等,这些公司的Windows开发招聘岗位上有对熟悉duilib库的要求。当然,这些厂商会对duilib进行深入的优化和改进,解决了很多bug。我们这边也不例外,也使用过程中也发现了很多问题,也对代码进行了一些优化和改进。

4、调用UpdateLayeredWindow接口返回失败

       在创建CDesktopAreaShareDlg窗口时设置了WS_EX_LAYERED分层窗口的风格,结果运行显示出来的窗口并没有显示图片边框,窗口中间也不是透明、鼠标可穿透的。调试代码发现,UpdateLayeredWindow函数调用失败了,GetLastError返回的错误码是87

到VS的错误查找工具中搜索了一下:

该错误码的含义是:参数错误。但详细检查了一下传入的参数值,并没有异常非法的值,这个就有点奇怪了。

       首先创建的位图是包含Alpha通道的32位位图,如果是非32位位图则可能导致UpdateLayeredWindow函数调用失败。其次传入到UpdateLayeredWindow接口中的参数都没问题的,可为啥还会失败呢?
       难道是WS_EX_LAYERED窗口风格设置失败了?于是想用SPY++抓了一下窗口属性,看看窗口到底有没有WS_EX_LAYERED风格。但是该窗口因为调用UpdateLayeredWindow失败了,桌面上根本没显示这个窗口,没关系,我们可以直接到SPY++中搜索该窗口,通过创建窗口时使用的类名搜索(只填充类名,将其他输入框清空):

 搜索到该窗口,查看窗口属性中的窗口风格:

果然没有WS_EX_LAYERED风格,这就奇怪了,明明设置了WS_EX_LAYERED窗口风格,为啥没生效呢?

       这个窗口是继承duilib框架中的CAppWidnow(我们自行封装的,duilib开源代码是没有的)通用窗口类的,好像该通用窗口类中有个设置透明度的选项,可能是处理窗口透明度的代码将WS_EX_LAYERED风格给取消了。

       于是到duilib的代码中,找到设置透明度的代码,确实是这个设置透明度的接口将WS_EX_LAYERED风格取消了:

void CPaintManagerUI::SetTransparent(int nOpacity)
{
	if (NULL == m_hWndPaint)
	{
		return;
	}

	typedef BOOL(__stdcall *PFUNCSETLAYEREDWINDOWATTR)(HWND, COLORREF, BYTE, DWORD);
	PFUNCSETLAYEREDWINDOWATTR fSetLayeredWindowAttributes;

	HMODULE hUser32 = ::GetModuleHandle(_T("User32.dll"));
	if (hUser32)
	{
		fSetLayeredWindowAttributes =
			(PFUNCSETLAYEREDWINDOWATTR)::GetProcAddress(hUser32, "SetLayeredWindowAttributes");

		if (NULL == fSetLayeredWindowAttributes)
		{
			return;
		}
	}

	DWORD dwStyle = ::GetWindowLong(m_hWndPaint, GWL_EXSTYLE);
	DWORD dwNewStyle = dwStyle;
	if (nOpacity >= 0 && nOpacity < 255)
	{
		dwNewStyle |= WS_EX_LAYERED;
	}
	else
	{
		dwNewStyle &= ~WS_EX_LAYERED;
	}

	if (dwStyle != dwNewStyle)
	{
		::SetWindowLong(m_hWndPaint, GWL_EXSTYLE, dwNewStyle);
	}

	fSetLayeredWindowAttributes(m_hWndPaint, 0, nOpacity, LWA_ALPHA);
}

我们给该窗口设置的透明度为255,即不透明,所以上述代码中发现传入的透明度参数nOpacity为255,就将WS_EX_LAYERED风格给取消了。此处的代码是有问题的,将窗口的透明度设置为不透明,不应该将WS_EX_LAYERED风格取消掉,因为可能会调用处理分层窗口的其他系统API函数,比如我们本案例用到的UpdateLayeredWindow。所以要将这个取消WS_EX_LAYERED风格的代码注释掉。
       但代码注释后运行,还是有问题,UpdateLayeredWindow接口还是执行失败了,lasterror值依旧是87。依稀记得,好像设置窗口透明度的接口SetLayeredWindowAttributes和处理异形窗口的接口UpdateLayeredWindow是不能同时调用的。如上的代码所示,设置透明度的接口CPaintManagerUI::SetTransparent中不管是否设置了有效的透明度(小于255),都调用了SetLayeredWindowAttributes,所以这点也是不合理的,应该设置不透明时,就不应该调用SetLayeredWindowAttributes。修改后的代码

        重新运行代码后,发现有效果了,窗口边界图片显示出来了,窗口的中间区域也是可穿透的了。

5、点击标题栏无法拖动窗口

       对于窗口支持标题栏拖动、窗口支持拖动边框改变窗口大小,duilib中的窗口框架都支持,只要在xml文件中设置两个属性就可以了。对于标题栏拖动,设置caption属性即可,即caption="0,0,0,27";对于窗口边界可拖动,设置sizebox属性即可,即sizebox="5,5,5,5",窗口对应的xml文件如下:

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<Window size="794,500" mininfo="400,250" sizebox="5,5,5,5" caption="0,0,0,27" shadow="false">
	<Font name="微软雅黑" size="12" />
  <Default name="VScrollBar" value="width=&quot;10&quot; showbutton1=&quot;false&quot; showbutton2=&quot;false&quot; thumbnormalimage=&quot;res='IDB_DEFAULT_SCROLL_BAR' restype='png' source='20,0,30,21' &quot; bknormalimage=&quot;res='IDB_DEFAULT_SCROLL_BAR' restype='png' source='30,0,40,10' &quot; bkhotimage=&quot;res='IDB_DEFAULT_SCROLL_BAR' restype='png' source='30,0,40,10' &quot; bkpushedimage=&quot;res='IDB_DEFAULT_SCROLL_BAR' restype='png' source='30,0,40,10' &quot; bkdisabledimage=&quot;res='IDB_DEFAULT_SCROLL_BAR' restype='png' source='30,0,40,10' &quot; " />
  <VerticalLayout name="desktopareaselwnd" bkcolor="#FFFF0000">
  </VerticalLayout>
</Window>

       添加属性后,我们测试了一下效果,窗口边界拖动窗口大小是没问题的,但标题栏是无法拖动窗口的。当鼠标移动到窗口中时,会产生WM_NCHITTEST消息,按讲移到窗口标题栏区域时,应该返回HTCAPTION,以支持窗口的拖动。

       于是到duilib框架代码中查看CAppWindow类处理WM_NCHITTEST消息的代码:

LRESULT CAppWindow::OnNcHitTest( UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled )
{
	POINT pt; 
	pt.x = GET_X_LPARAM( lParam ); 
	pt.y = GET_Y_LPARAM( lParam );
	::ScreenToClient( *this, &pt );

	RECT rcClient;
	::GetClientRect( *this, &rcClient );
    
	// 改变大小
	if( !::IsZoomed(*this) ) 
	{
		RECT rcSizeBox = m_pm.GetSizeBox();
		if( pt.y < rcClient.top + rcSizeBox.top ) 
		{
			if( pt.x < rcClient.left + rcSizeBox.left ) return HTTOPLEFT;
			if( pt.x > rcClient.right - rcSizeBox.right ) return HTTOPRIGHT;
			return HTTOP;
		}
		else if( pt.y > rcClient.bottom - rcSizeBox.bottom ) 
		{
			if( pt.x < rcClient.left + rcSizeBox.left ) return HTBOTTOMLEFT;
			if( pt.x > rcClient.right - rcSizeBox.right ) return HTBOTTOMRIGHT;
			return HTBOTTOM;
		}
		if( pt.x < rcClient.left + rcSizeBox.left ) return HTLEFT;
		if( pt.x > rcClient.right - rcSizeBox.right ) return HTRIGHT;
	}

	// 标题栏响应
	RECT rcCaption = m_pm.GetCaptionRect();
	if( pt.x >= rcClient.left + rcCaption.left 
	  && pt.x < rcClient.right - rcCaption.right 
	  && pt.y >= rcCaption.top  
	  && pt.y < rcCaption.bottom ) 
	{
		// 考虑到标题栏区域会放置控件,比如常见的右上角的最小化、最大化和关闭按钮,
		// 所以要将按钮等控件过滤掉
		CControlUI* pControl = static_cast<CControlUI*>( m_pm.FindControl( pt ) );
		if( pControl 
			&& _tcscmp(pControl->GetClass(), _T("ButtonUI")) != 0 
			&& _tcscmp(pControl->GetClass(), _T("OptionUI")) != 0 
			&& _tcscmp(pControl->GetClass(), _T("TextUI")) != 0 )
		{
			return HTCAPTION;
		}
	}

	return HTCLIENT;
}

打断点调试发现,在判断鼠标位置落在标题栏区域时,会判断鼠标是否落在某个dui控件中,如果返回的dui控件指针为空(鼠标点击点是否落在窗口的dui控件上),是不会返回HTCAPTION值的。于是在CDesktopAreaShareDlg窗口类的HandleMesssge中拦截WM_HITTEST消息,将代码修改一下,去掉是否落在控件中的判断,我们这个窗口比较简单,也比较特殊:

LRESULT CDesktopShareAreaSelDlg::HandleMessage( UINT message, WPARAM wParam, LPARAM lParam )
{
	TNotifyUI msg;
	if ( WM_SIZE == message )
	{
		Update();
	}
	else if ( WM_PAINT == message )
	{
		Update();
		return true;
	}
	else if ( WM_NCHITTEST == message)
	{
		POINT pt; 
		pt.x = GET_X_LPARAM( lParam ); 
		pt.y = GET_Y_LPARAM( lParam );
		::ScreenToClient( m_hWnd, &pt );

		RECT rcClient;
		::GetClientRect( m_hWnd, &rcClient );

		// 改变大小
		if( !::IsZoomed(m_hWnd) ) 
		{
			RECT rcSizeBox = m_pm.GetSizeBox();
			if( pt.y < rcClient.top + rcSizeBox.top ) 
			{
				if( pt.x < rcClient.left + rcSizeBox.left ) return HTTOPLEFT;
				if( pt.x > rcClient.right - rcSizeBox.right ) return HTTOPRIGHT;
				return HTTOP;
			}
			else if( pt.y > rcClient.bottom - rcSizeBox.bottom ) 
			{
				if( pt.x < rcClient.left + rcSizeBox.left ) return HTBOTTOMLEFT;
				if( pt.x > rcClient.right - rcSizeBox.right ) return HTBOTTOMRIGHT;
				return HTBOTTOM;
			}
			if( pt.x < rcClient.left + rcSizeBox.left ) return HTLEFT;
			if( pt.x > rcClient.right - rcSizeBox.right ) return HTRIGHT;
		}

		// 标题栏响应
		RECT rcCaption = m_pm.GetCaptionRect();
		if( pt.x >= rcClient.left + rcCaption.left 
			&& pt.x < rcClient.right - rcCaption.right 
			&& pt.y >= rcCaption.top  
			&& pt.y < rcCaption.bottom ) 
		{
			return HTCAPTION;			
		}
	}

	return CAppWindow::HandleMessage( message, wParam, lParam );
}

对于我们当前的窗口,只要落在窗口标题栏区域就直接返回HTCAPTION值了,不用再去做其他的判断了。

       至于为啥会出现鼠标落在的控件指针返回NULL的情况呢?我们的窗口类CDesktopAreaShareDlg的xml文件中已经配置了一个垂直根布局(只有这个根布局):

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<Window size="794,500" mininfo="400,250" sizebox="5,5,5,5" caption="0,0,0,27" shadow="false">
	<Font name="微软雅黑" size="12" />
  <Default name="VScrollBar" value="width=&quot;10&quot; showbutton1=&quot;false&quot; showbutton2=&quot;false&quot; thumbnormalimage=&quot;res='IDB_DEFAULT_SCROLL_BAR' restype='png' source='20,0,30,21' &quot; bknormalimage=&quot;res='IDB_DEFAULT_SCROLL_BAR' restype='png' source='30,0,40,10' &quot; bkhotimage=&quot;res='IDB_DEFAULT_SCROLL_BAR' restype='png' source='30,0,40,10' &quot; bkpushedimage=&quot;res='IDB_DEFAULT_SCROLL_BAR' restype='png' source='30,0,40,10' &quot; bkdisabledimage=&quot;res='IDB_DEFAULT_SCROLL_BAR' restype='png' source='30,0,40,10' &quot; " />
  <VerticalLayout name="desktopareaselwnd" bkcolor="#FFFF0000">
  </VerticalLayout>
</Window>

按讲根布局会铺满整个窗口的,鼠标肯定是落在这个根布局上。于是在CAppWidnow处理WM_NCHITTEST消息的接口中打断点单步调试发现,根布局的区域位置m_rcItem为(0,0,0,0),即根布局的区域大小为0,所以不会鼠标肯定不会落在根布局上。

       奥,原来是这样的,我们xml中控件是在所在窗口收到WM_PAINT消息时去布局控件(给控件设置位置)的,但一旦对窗口调用UpdateLayeredWindow后窗口就不会产生WM_PAINT消息的。我们会在xml中设置窗口的大小,我们在调用CreateWIndow(Ex)将窗口创建起来收WM_CREATE消息时区加载解析xml文件,会优先得到xml中设置的窗口大小,会调用SetWindowPos去设置窗口的大小,窗口大小会发生变化,就会产生WM_SIZE消息,而CDesktopAreaShareDlg窗口在收到这个消息时就会调用UpdateLayeredWindow,这样窗口后面就不会再产生WM_PAINT消息了,所以CDesktopAreaShareDlg窗口就没有排布xml中控件的机会了,所以根布局控件的大小始终是0。

6、选择区域坐标该怎么设置给底层的图像采集编码层呢?

       选择区域窗口大小可拖动,选择可以拖动标题栏拖动窗口,我们在窗口移动时或者窗口大小发生变化时,将选择区域的坐标实时的设置给图像采集编码层?如果由UI层调用接口将选择区域的坐标设置给媒控层,则存在两个问题:
1)    何时触发调用设置选择区域坐标给图像采集编码层的接口?在窗口移动时,在窗口大小发生变化时都要触发。这样接口调用会非常地频繁。
2)    UI层需要调用组件API层的接口,然后再经过组件层内部的多层后,再设置到采集编码库
中,这些层与层之间的接口都是异步的,很难保证选择区域的大小,实时地通知给采集编码层。
       其实,有个最好的办法,UI层给采集编码层设置用来选择区域窗口句柄,并将选择窗口四个边界的宽度或高度设置给采集编码层,如下所示:

这样媒控层可以在每次截图时实时去获取选择窗口的坐标,然后将选择窗口边界宽度与高度减掉,就能实时地得到选择区域的坐标了,这应该是最合理的方式!函数调用需要额外的开销,所以采用这种方式,既可以满足实时性获取要求,也能减少函数调用的开销!