zl程序教程

您现在的位置是:首页 >  工具

当前栏目

类似QQ贴边掩藏功能的实现(二)

QQ 实现 功能 类似
2023-09-14 09:16:31 时间

        总的来讲,QQ的这种贴边掩藏的效果是通过Windows的消息机制和窗口的移动来实现的。

        根据上面的研究,首先要弄清楚窗口的目标位置,这样我们在掩藏和显示时才知道将窗口移动到哪个位置。在窗口掩藏时所显露的boarder部分的高度是系统定义的窗口边框长度,于是我们首先要获取并记录:窗口向上掩藏后的boarder的高度和窗口向左或右掩藏后的boarder的宽度,代码如下:

// 获得边缘高度与宽度
m_nEdgeHeight = GetSystemMetrics( SM_CYEDGE );  // 窗口向上掩藏后的boarder的高度
m_nEdgeWidth = GetSystemMetrics( SM_CXEDGE );   // 窗口向左或右掩藏后的boarder的宽度

        获取到boarder的度量信息后就能确定窗口的目标坐标了。在窗口移动的过程中,要找到移动前后不变的值,要找到目标位置的参照点,这样就能很容易的确定目标位置的坐标了。对于向上掩藏,触发掩藏时刻前后的窗口的left坐标和right坐标是不变的,掩藏后的目标位置的bottom坐标就是boarder的高度值m_nEdgeHeight;另外向上掩藏时窗口的高度是维持不变的,所以目标位置的top坐标就能计算出来了。向左掩藏和向右掩藏则和向上掩藏就不太一样了,从高度方向看,掩藏后的窗口将充满整个桌面的客户区。以向左掩藏为例,掩藏后的目标位置的right坐标就是boarder的宽度值m_nEdgeWidth,掩藏前后窗口的宽度是不变的,所以就可以计算出目标位置的left坐标;由于从高度方向上看,移动后的窗口是充满整个桌面客户区的,所以目标位置的top坐标为0,bottom坐标就是桌面客户区域的bottom值。

        我们还需要记录窗口的当前掩藏状态,所以定义了以下的枚举体。

// 主窗口贴边掩藏模式
enum EmSideHideMode
{
    SIDEHIDE_NONE   = 0,  // 不掩藏
    SIDEHIDE_TOP    = 1,   // 向上掩藏
    SIDEHIDE_LEFT   = 2,   // 向左掩藏
    SIDEHIDE_RIGHT  = 3,   // 向右掩藏
};

        那么在满足什么触发条件后实施窗口掩藏呢?我们事先定义了决定是否贴边掩藏的光标与桌面边界的最小间隔SIDE_HIDE_INTERVAL,为20个像素,当光标与桌面边界的距离小于20时会触发掩藏。当检测到满足触发条件后,我们需要将窗口停靠到桌面边界上,为即将到来的掩藏做好准备。要完成触发条件检测和在满足触发条件后将窗口一到桌面边界上这两项工作,我们需要为TrueLink的主窗口添加WM_MOVING的消息映射及消息响应函数OnMoving。在对OnMoving函数中实现这两个功能,代码如下:

#define  SIDE_HIDE_INTERVAL           20  // 决定是否贴边掩藏的光标与屏幕边界的最小间隔,单位为象素

void CMainFrameWnd::OnMoving( UINT nSide, LPRECT lpRect )
{
    FixMoving( lpRect ); //修正pRect
    CSkinFrameWnd::OnMoving( nSide, lpRect );
}

 我们对相关代码做了封装,将具体的处理细节封装到FixMoving函数中,至于窗口停靠在桌面边界上的位置坐标这里就不详细说了,请查阅下面的代码:

void CMainFrameWnd::FixMoving( LPRECT lpRect )
{
	// 实时获取任务栏高度(考虑到用户在使用过程中可能改变任务栏的高度)
	CRect rect;
	CWnd* pTaskBarWnd = this->FindWindow( "Shell_TrayWnd", NULL );
	if( pTaskBarWnd != NULL )
	{
		pTaskBarWnd->GetWindowRect( &rect );
		m_nTaskBarHeight = rect.Height();
	}
	
// 获取当前光标位置
	CPoint curPos;
	GetCursorPos( &curPos );  

// 获取屏幕坐标
	int nScreenHeight = GetSystemMetrics( SM_CYSCREEN );
	int nScreenWidth = GetSystemMetrics( SM_CXSCREEN );

      // 记录修正前的窗体的高度和宽度
	int nWndHeight = lpRect->bottom - lpRect->top; 
	int nWndWidth = lpRect->right - lpRect->left;
	
	if ( curPos.x <= SIDE_HIDE_INTERVAL ) // 向左掩藏(由光标坐标判断)
	{
		// 保存掩藏前的窗口高度
		if ( !m_bMainWndSizeChanged )
		{
			GetWindowRect( &rect );
			m_nOldMainWndHeight = rect.Height();
		}
		
           // 计算停靠在桌面左边界的窗口位置,注意它是掩藏前窗口的停靠位置
		lpRect->left = 0;
		lpRect->top = -m_nEdgeHeight;
		lpRect->right = nWndWidth;
		lpRect->bottom = nScreenHeight - m_nTaskBarHeight;
		m_bMainWndSizeChanged = TRUE;
		m_emSideHideMode = SIDEHIDE_LEFT;

		// 当我的电脑 -> 属性 -> 高级 -> 视觉效果页面中没有勾选“拖动窗口时显示内容”选项时,
		// 执行贴边操作,从窗口的高度方向看,窗口不能充满屏幕客户区域,此处调用MoveWindow以解
		// 决该问题,原因是:通常情况下,窗口的移动过程激发了多个WM_MOVING,在移动的最后时刻
		// 激发WM_MOVE,WM_MOVING激发时窗口并未真正的移动,真正的移动是在最终的那个WM_MOVE激
		// 发时刻。(向右掩藏解释相同)
		MoveWindow( lpRect );
	}
	else if ( curPos.x >= nScreenWidth-SIDE_HIDE_INTERVAL ) // 向右掩藏(由光标坐标判断)
	{
		// 保存掩藏前的窗口高度
		if ( !m_bMainWndSizeChanged )
		{
			GetWindowRect( &rect );
			m_nOldMainWndHeight = rect.Height();
		}

           // 计算停靠在桌面右边界的窗口位置,注意它是掩藏前窗口的停靠位置	
	     lpRect->left = nScreenWidth - nWndWidth;
		lpRect->top = -m_nEdgeHeight;
		lpRect->right = nScreenWidth;
		lpRect->bottom = nScreenHeight - m_nTaskBarHeight;
		m_bMainWndSizeChanged = TRUE;
		m_emSideHideMode = SIDEHIDE_RIGHT;

		MoveWindow( lpRect );
	}
	else if ( curPos.y < SIDE_HIDE_INTERVAL && m_emSideHideMode != SIDEHIDE_LEFT && m_emSideHideMode != SIDEHIDE_RIGHT ) // 向上掩藏(由光标坐标判断)
	{
           // 计算停靠在桌面上边界的窗口位置,注意它是掩藏前窗口的停靠位置(此时left坐标和right坐标不用改动)
		lpRect->top = -m_nEdgeHeight;
		lpRect->bottom = nWndHeight - m_nEdgeHeight;
		m_emSideHideMode = SIDEHIDE_TOP;

		MoveWindow( lpRect );
	}
	else // 不贴边掩藏
	{
           // 恢复掩藏前的窗口高度,是针对向左和向右掩藏的
		if ( m_bMainWndSizeChanged )
		{
			lpRect->bottom = lpRect->top + m_nOldMainWndHeight;
			m_bMainWndSizeChanged = FALSE;

			MoveWindow( lpRect );
		}
		
           // 关闭检测
		if ( m_bMouseDetectTimerSet )
		{
			KillTimer( TIMER_EVENT_MOUSE_DETECT );
			m_bMouseDetectTimerSet = FALSE;
		}
		
		m_emSideHideMode = SIDEHIDE_NONE;
	}
}

        在窗口处于掩藏状态,我们将光标放到boarder上,窗口会自动显示出来;当此时光标从窗口中移走后,窗口会自动掩藏起来,这又是如何实现的呢?上面提到过,这个boarder块其实是窗口的一部分,它属于窗口的非客户区域。要检测光标是否在boarder上,只需要添加WM_NCHITTEST消息映射及响应函数。在该函数中,如果发现窗口当前处于掩藏状态,则显示窗体;并开启检测光标是否在窗体内的定时器,通过该定时器来检测鼠标是否离开窗口,当鼠标离开窗口后,自动掩藏窗口。对应的代码如下:

UINT CMainFrameWnd::OnNcHitTest( CPoint point )
{
	if ( m_emSideHideMode != SIDEHIDE_NONE && !m_bMouseDetectTimerSet &&
		point.x <= GetSystemMetrics( SM_CXSCREEN ) + SIDE_HIDE_INFALTE )
	{
		// 开启检测鼠标是否离开窗口定时器
		SetTimer( TIMER_EVENT_MOUSE_DETECT, MOUSE_DETECT_TIME_INTERVAL, NULL );
		m_bMouseDetectTimerSet = TRUE;
		
		if ( m_bHide )
		{
			DoShowWnd();
		}
	}
	
	return CSkinFrameWnd::OnNcHitTest( point );
}

         接下来,给窗口添加定时器消息及响应函数,在接收到检测光标是否在窗体类的定时器消息后,判断光标是否在窗体内,如果不在窗体内则将窗口自动掩藏,代码如下:

void CMainFrameWnd::OnTimer( UINT nIDEvent )
{
	if ( TIMER_EVENT_MOUSE_DETECT == wParam ) // 检测鼠标是否在主窗口中的定时器
	{
		if ( !m_bEnableSideHide ) // 判断是否取消贴边掩藏功能
		{
			// 关闭鼠标检测定时器
			KillTimer( TIMER_EVENT_MOUSE_DETECT );
			m_bMouseDetectTimerSet = FALSE;
			break;
		}
		
		CPoint curPos;
		GetCursorPos( &curPos );
		
		CRect rectWnd;
		GetWindowRect( &rectWnd );
		// 膨胀rectWnd,以达到鼠标离开窗口边沿一定距离才触发事件
		rectWnd.InflateRect( SIDE_HIDE_INFALTE, SIDE_HIDE_INFALTE );
		
		if ( !rectWnd.PtInRect( curPos ) )
		{
			// 关闭鼠标检测定时器
			KillTimer( TIMER_EVENT_MOUSE_DETECT );
			m_bMouseDetectTimerSet = FALSE;
			
			if ( !m_bHide )
			{
				DoHideWnd();
			}	
		}
	}

	CSkinFrameWnd::OnTimer(nIDEvent);
}

        上面的接口中用到的DoHideWnd函数和DoShowWnd函数,都是根据相对位置计算出窗口的目标坐标,然后移动窗口,来实现窗口的掩藏和显示的。窗口的掩藏和显示可采用两种做法:一次性已到位;分步移动以实现动画效果。

        DoHideWnd函数的代码如下:(注意下面的代码采取一次性一到位的做法,分步移动以达到动画效果的代码已被注释,具体原因见本文的第四部分。DoShowWnd函数也作同样的处理)

void CMainFrameWnd::DoHideWnd()
{
	if ( m_emSideHideMode == SIDEHIDE_NONE /*|| m_bHide*/ )
	{
		return;
	}

	m_bHide = TRUE;

	CRect rectWnd;
	GetWindowRect( &rectWnd );
    int nWndHeight = rectWnd.Height();
	int nWndWidth = rectWnd.Width();

	int nStepLength = 0;
	switch ( m_emSideHideMode )
	{
	case SIDEHIDE_TOP: // 向上收缩
		{
// 			int nWndBottom = rectWnd.bottom;
// 			nStepLength = nWndHeight/SIDE_MOVE_STEP_NUM;

// 			// 动画滚动窗口
// 			for ( int nTime = 1; nTime <= SIDE_MOVE_STEP_NUM; nTime++ )
// 			{
// 				nWndBottom -= nStepLength;
// 				if ( nTime == SIDE_MOVE_STEP_NUM ) // 考虑到不能整除,循环到最后一次,移到最终位置
// 				{
// 					nWndBottom = m_nEdgeHeight;
// 				}
// 				rectWnd.bottom = nWndBottom;
// 				rectWnd.top = rectWnd.bottom - nWndHeight;
// 
// 				SetWindowPos( &wndTopMost, rectWnd.left, rectWnd.top, rectWnd.Width(), rectWnd.Height(), SWP_SHOWWINDOW );
// 				//RedrawWindow( NULL, NULL, RDW_INVALIDATE | RDW_UPDATENOW );
// 			}

			rectWnd.bottom = m_nEdgeHeight;
			rectWnd.top = rectWnd.bottom - nWndHeight;
			SetWindowPos( &wndTopMost, rectWnd.left, rectWnd.top, rectWnd.Width(), rectWnd.Height(), SWP_SHOWWINDOW );
		}
		break;

	case SIDEHIDE_LEFT: // 向左收缩
		{
// 			int nWndRight = rectWnd.right;
// 			nStepLength = nWndWidth/SIDE_MOVE_STEP_NUM;

// 			// 动画滚动窗口
// 			for ( int nTime = 1; nTime <= SIDE_MOVE_STEP_NUM; nTime++ )
// 			{
// 				nWndRight -= nStepLength;
// 				if ( nTime == SIDE_MOVE_STEP_NUM ) // 考虑到不能整除,循环到最后一次,移到最终位置
// 				{
// 					nWndRight = m_nEdgeWidth;
// 				}
// 				rectWnd.right = nWndRight;
// 				rectWnd.left = rectWnd.right - nWndWidth;
// 				
// 				SetWindowPos( &wndTopMost, rectWnd.left, rectWnd.top, rectWnd.Width(), rectWnd.Height(), SWP_SHOWWINDOW );
// 				//RedrawWindow( NULL, NULL, RDW_INVALIDATE | RDW_UPDATENOW );
// 			}

			rectWnd.right = m_nEdgeWidth;
			rectWnd.left = rectWnd.right - nWndWidth;
			SetWindowPos( &wndTopMost, rectWnd.left, rectWnd.top, rectWnd.Width(), rectWnd.Height(), SWP_SHOWWINDOW );
		}
		break;

	case SIDEHIDE_RIGHT: // 向右收缩
		{
// 			int nWndLeft = rectWnd.left;
// 			nStepLength = nWndWidth/SIDE_MOVE_STEP_NUM;

// 			// 动画滚动窗口
// 			for ( int nTime = 1; nTime <= SIDE_MOVE_STEP_NUM; nTime++ )
// 			{
// 				nWndLeft += nStepLength;
// 				if ( nTime == SIDE_MOVE_STEP_NUM ) // 考虑到不能整除,循环到最后一次,移到最终位置
// 				{
// 					nWndLeft = GetSystemMetrics( SM_CXSCREEN ) - m_nEdgeWidth;
// 				}
// 				rectWnd.left = nWndLeft;
// 				rectWnd.right = rectWnd.left + nWndWidth;
// 				
// 				SetWindowPos( &wndTopMost, rectWnd.left, rectWnd.top, rectWnd.Width(), rectWnd.Height(), SWP_SHOWWINDOW );
// 				//RedrawWindow( NULL, NULL, RDW_INVALIDATE | RDW_UPDATENOW );
// 			}

			rectWnd.left = GetSystemMetrics( SM_CXSCREEN ) - m_nEdgeWidth;
			rectWnd.right = rectWnd.left + nWndWidth;
			SetWindowPos( &wndTopMost, rectWnd.left, rectWnd.top, rectWnd.Width(), rectWnd.Height(), SWP_SHOWWINDOW );
		}
		break;

	default:
		break;
	}
}

        DoShowWnd函数的代码如下:

void CMainFrameWnd::DoShowWnd()
{
	if ( m_emSideHideMode == SIDEHIDE_NONE /*|| !m_bHide */)
	{
		return;
	}

	m_bHide = FALSE;

	CRect rectWnd;
	GetWindowRect( &rectWnd );
	int nWndHeight = rectWnd.Height();
	int nWndWidth = rectWnd.Width();

	int nStepLength = 0;

	switch ( m_emSideHideMode )
	{
	case SIDEHIDE_TOP: // 向下展开
		{
// 			int nWndTop = rectWnd.top;
// 			nStepLength = nWndHeight/SIDE_MOVE_STEP_NUM;

// 			// 动画滚动窗口
// 			for ( int nTime = 1; nTime <= SIDE_MOVE_STEP_NUM; nTime++ )
// 			{
// 				nWndTop += nStepLength;
// 				if ( nTime == SIDE_MOVE_STEP_NUM )  // 考虑到不能整除,循环到最后一次,移到最终位置
// 				{
// 					nWndTop =  - m_nEdgeHeight;
// 				}
// 				rectWnd.top = nWndTop;
// 				rectWnd.bottom = rectWnd.top + nWndHeight;
// 
// 				SetWindowPos( &wndTopMost, rectWnd.left, rectWnd.top, rectWnd.Width(), rectWnd.Height(), SWP_SHOWWINDOW );
// 				RedrawWindow( NULL, NULL, RDW_INVALIDATE | RDW_FRAME );  
// 				//Invalidate();
// 			}
// 			//RedrawWindow( NULL, NULL, RDW_INVALIDATE | RDW_UPDATENOW ); 
// 			Invalidate();

			rectWnd.top = -m_nEdgeHeight;
			rectWnd.bottom = rectWnd.top + nWndHeight;
			SetWindowPos( &wndBottom, rectWnd.left, rectWnd.top, rectWnd.Width(), rectWnd.Height(), SWP_SHOWWINDOW );
			//RedrawWindow( NULL, NULL, RDW_INVALIDATE | RDW_UPDATENOW ); 
		}
		break;

	case SIDEHIDE_LEFT: // 向右展开
		{
// 			int nWndLeft = rectWnd.left;
// 			nStepLength = nWndWidth/SIDE_MOVE_STEP_NUM;

// 			// 动画滚动窗口
// 			for ( int nTime = 1; nTime <= SIDE_MOVE_STEP_NUM; nTime++ )
// 			{
// 				nWndLeft += nStepLength;
// 				if ( nTime == SIDE_MOVE_STEP_NUM ) // 考虑到不能整除,循环到最后一次,移到最终位置
// 				{
// 					nWndLeft = -m_nEdgeWidth;
// 				}
// 				
// 				rectWnd.left = nWndLeft;
// 				rectWnd.right = rectWnd.left + nWndWidth;
// 				
// 				SetWindowPos( &wndTopMost, rectWnd.left, rectWnd.top, rectWnd.Width(), rectWnd.Height(), SWP_SHOWWINDOW );
// 				RedrawWindow( NULL, NULL, RDW_INVALIDATE | RDW_FRAME  );
// 				//Invalidate();
// 			}
// 			//RedrawWindow( NULL, NULL, RDW_INVALIDATE | RDW_UPDATENOW ); 
// 			Invalidate();

			rectWnd.left = -m_nEdgeWidth;
			rectWnd.right = rectWnd.left + nWndWidth;
			SetWindowPos( &wndBottom, rectWnd.left, rectWnd.top, rectWnd.Width(), rectWnd.Height(), SWP_SHOWWINDOW );
			//RedrawWindow( NULL, NULL, RDW_INVALIDATE | RDW_UPDATENOW ); 
		}
		break;

	case SIDEHIDE_RIGHT: // 向左展开
		{
// 			int nWndLeft = rectWnd.left;
// 			nStepLength = nWndWidth/SIDE_MOVE_STEP_NUM;

// 			// 动画滚动窗口
// 			for ( int nTime = 1; nTime <= SIDE_MOVE_STEP_NUM; nTime++ )
// 			{
// 				nWndLeft -= nStepLength;
// 				if ( nTime == SIDE_MOVE_STEP_NUM ) // 考虑到不能整除,循环到最后一次,移到最终位置
// 				{
// 					nWndLeft = GetSystemMetrics(SM_CXSCREEN) - nWndWidth;
// 				}
// 				
// 				rectWnd.left = nWndLeft;
// 				rectWnd.right = rectWnd.left + nWndWidth;
// 				
// 				SetWindowPos( &wndTopMost, rectWnd.left, rectWnd.top, rectWnd.Width(), rectWnd.Height(), SWP_SHOWWINDOW );
// 				//RedrawWindow( NULL, NULL, RDW_INVALIDATE | RDW_UPDATENOW );  
// 				Invalidate();
// 			}
// 			//RedrawWindow( NULL, NULL, RDW_INVALIDATE | RDW_UPDATENOW ); 
// 			Invalidate();

			rectWnd.left = GetSystemMetrics(SM_CXSCREEN) - nWndWidth;
			rectWnd.right = rectWnd.left + nWndWidth;
			SetWindowPos( &wndBottom, rectWnd.left, rectWnd.top, rectWnd.Width(), rectWnd.Height(), SWP_SHOWWINDOW );
			//RedrawWindow( NULL, NULL, RDW_INVALIDATE | RDW_UPDATENOW ); 
		}
		break;

	default:
		break;
	}
}

至此,贴边掩藏功能的思路和实现细节就讲完了,并给出了相应的代码。