VC 仿QQ窗口靠边自动收缩隐藏效果

一、观察

模仿前最重要的一步就是观察,经过半天对QQ的摆弄和摸索,总结出了以下一些特点:

1、窗口开始粘附时,检测的是鼠标坐标与桌面边界的距离,特别地,粘附在下面的时候,检测的是与任务栏的距离;

2、在向上移动窗口时,窗口边界永远不会超出桌面上面边界;

3、窗口是个 TopMost 风格;

4、当窗口粘附在上面、左边或右边并显示时,你把鼠标移动到最顶端,光标变成改变窗口大小的图标,而单单是把窗口的top坐标设置为0是不行的;

5、粘附在下面的时候,当处于移动状态,那么窗口的底边是与任务栏顶边对齐的,但从隐藏到显示的时候,窗口的底端是与屏幕底边对齐的;

6、隐藏后显露出来的那条线可能是一个Border,但肯定的是绝不包含Client区域;

7、关于响应鼠标的进入与移出窗口,绝对不是WM_MOUSEMOVE、WM_MOUSELEAVE。证明:你以及其慢的速度接触隐藏状态的QQ边界,你会发现几乎是“一触即发”,你又以及其慢的速度移出显示状态的QQ,你会发现它的收缩反而不是“一触即发”的,而是离边缘10象素左右。而WM_MOUSEMOVE,WM_MOUSELEAVE,只有在进入、移出Client区域才响应,明显和QQ不同,其实从第6点也可以知道;

8、粘附在两边的时候,高度会调整为桌面上边界到任务栏下边界的距离;

9、在“拖动时显示窗口内容”模式下(桌面属性-外观-效果),粘附在两边的拖动出来时;如果收缩之前高度比收缩后小则回复原来高度,在非“拖动时显示窗口内容”模式下,光栅会回复原来高度,但释放左键时,高度却是收缩时调整后的高度,一开始我以为这是个BUG,但我编写时同样出现这个问题,发现这两种模式会影响WM_MOVING参数的意义;

10、粘附在两边的时候当你设置任务栏自动隐藏,QQ窗口会自动调整高度充满屏幕高度;

11、窗口显示或隐藏不是一瞬间的,这点在第9点提到的两种模式下,会有所不同;

12、任务栏并不显示QQ窗口;

二、编写代码

观察完毕,就开始编写了。
首先新建一个基于对话框的MFC程序,命名为QQHideWnd,在对话框属性的styles页把border改为Resizing,你也可同时把Entended styles 的 tool window 钩上,对于这点我在程序了动态修改了。
在QQHideWndDlg.h头文件添加以下成员函数:

protected :
// 修正移动时窗口的大小
void FixMoving(UINT fwSide, LPRECT pRect);
// 从收缩状态显示窗口
void DoShow();
// 从显示状态收缩窗口
void DoHide();
// 重载函数,只是为了方便调用,实际调用CWnd的SetWindowPos(…)
BOOL SetWindowPos( const CWnd * pWndInsertAfter,LPCRECT pCRect, UINT nFlags = SWP_SHOWWINDOW);

继续添加成员变量:

private ::
BOOL m_isSizeChanged;
// 窗口大小是否改变了
BOOL m_isSetTimer; // 是否设置了检测鼠标的Timer
INTm_oldWndHeight; // 旧的窗口宽度
INTm_taskBarHeight; // 任务栏高度
INTm_edgeHeight; // 边缘高度
INTm_edgeWidth; // 边缘宽度
INTm_hideMode; // 隐藏模式
BOOL m_hsFinished; // 隐藏或显示过程是否完成
BOOL m_hiding; // 该参数只有在!m_hsFinished才有效
// 真:正在隐藏,假:正在显示

增加消息响应,需要注意的是有些消息你只有把右下角的 Filter for message设置为window才能看到。
WM_ NCHITTEST
WM_MOVING
WM_CREATE
WM_TIMER

然后来到对应的cpp文件,在头部定义一些宏:

// 收缩模式
#define HM_NONE0 // 不收缩
#define HM_TOP1 // 向上收缩
#define HM_BOTTOM2 // 向下收缩
#define HM_LEFT3 // 向左收缩
#define HM_RIGHT4 // 向右收缩
#define CM_ELAPSE200 // 检测鼠标是否离开窗口的时间间隔
#define HS_ELAPSE5 // 伸缩过程每步的时间间隔
#define HS_STEPS10 // 伸缩过程分成多少步完成
#define INTERVAL20 // 触发粘附时鼠标与屏幕边界的最小间隔,单位为象素
#define INFALTE10 // 触发收缩时鼠标与窗口边界的最小间隔,单位为象素

然后在构造函数初始化成员变量:

m_isSizeChanged = FALSE;
m_isSetTimer
= FALSE;
m_hsFinished
= TRUE;
m_hiding
= FALSE;
m_oldWndHeight
= MINCY;
m_taskBarHeight
= 30 ;
m_edgeHeight
= 0 ;
m_edgeWidth
= 0 ;
m_hideMode
= HM_NONE;

完成了一些初始的工作,那么就开始进入关键的函数实现了。首先是在OnCreate做些窗口的初始化和获得一些系统信息。

int CQQHideWndDlg::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CDialog::OnCreate(lpCreateStruct) == - 1 )
return - 1 ;

// TODO: Add your specialized creation code here
// 获得任务栏高度
CWnd * p;
p
= this -> FindWindow( " Shell_TrayWnd " ,NULL);
if (p != NULL)
{
CRect tRect;
p
-> GetWindowRect(tRect);
m_taskBarHeight
= tRect.Height();
}

// 修改风格使得他不在任务栏显示
ModifyStyleEx(WS_EX_APPWINDOW, WS_EX_TOOLWINDOW);
// 去掉关闭按键(如果想画3个按键的话)
// ModifyStyle(WS_SYSMENU,NULL);

// 获得边缘高度和宽度
m_edgeHeight = GetSystemMetrics(SM_CYEDGE);
m_edgeWidth
= GetSystemMetrics(SM_CXFRAME);

return 0 ;
}

接着如何知道鼠标进入或移出窗口呢?在前面我已经证明了WM_MOUSEMOVE和WM_MOUSELEAVE不符合我们的要求,于是我用了WM_ NCHITTEST这个消息,你可以看到我在这个消息响应函数中用了两个SetTimer,一个用于检测鼠标是否离开,一个用于伸缩过程,不管你喜欢不喜欢,要达到第7点和第11点,这个是必须的,考虑的效率问题,在不需要的时候关闭这些Timer就好了。


UINT CQQHideWndDlg::OnNcHitTest(CPoint point)
{
// TODO: Add your message handler code here and/or call default
CString str;
str.Format(
" Mouse (%d,%d) " ,point.x,point.y);
GetDlgItem(IDC_CURSOR)
-> SetWindowText(str);
if (m_hideMode != HM_NONE && ! m_isSetTimer &&
// 防止鼠标超出屏幕右边时向右边收缩造成闪烁
point.x < GetSystemMetrics(SM_CXSCREEN) + INFALTE)
{
// 鼠标进入时,如果是从收缩状态到显示状态则开启Timer
SetTimer( 1 ,CM_ELAPSE,NULL);
m_isSetTimer
= TRUE;

m_hsFinished
= FALSE;
m_hiding
= FALSE;
SetTimer(
2 ,HS_ELAPSE,NULL); // 开启显示过程
}
return CDialog::OnNcHitTest(point);
}

然后在OnTimer中

void CQQHideWndDlg::OnTimer(UINT nIDEvent)
{
// TODO: Add your message handler code here and/or call default
if (nIDEvent == 1 )
{
POINT curPos;
GetCursorPos(
& curPos);

CString str;
str.Format(
" Timer On(%d,%d) " ,curPos.x,curPos.y);
GetDlgItem(IDC_TIMER)
-> SetWindowText(str);

CRect tRect;
// 获取此时窗口大小
GetWindowRect(tRect);
// 膨胀tRect,以达到鼠标离开窗口边沿一定距离才触发事件
tRect.InflateRect(INFALTE,INFALTE);

if ( ! tRect.PtInRect(curPos)) // 如果鼠标离开了这个区域
{
KillTimer(
1 ); // 关闭检测鼠标Timer
m_isSetTimer = FALSE;
GetDlgItem(IDC_TIMER)
-> SetWindowText( " Timer Off " );

m_hsFinished
= FALSE;
m_hiding
= TRUE;
SetTimer(
2 ,HS_ELAPSE,NULL); // 开启收缩过程
}
}

if (nIDEvent == 2 )
{
if (m_hsFinished) // 如果收缩或显示过程完毕则关闭Timer
KillTimer( 2 );
else
m_hiding
? DoHide() : DoShow();
}
CDialog::OnTimer(nIDEvent);
}

暂时不管OnTimer中的DoHide(); DoShow();

先来看看核心的函数之一的 FixMoving,该函数在OnMoving中被调用,FixMoving通过检测鼠标位置和窗口位置来决定窗口的收缩模式,并修正粘附边界时窗口的位置,从而达到像移动QQ时出现的效果。

View Code
void CQQHideWndDlg::FixMoving(UINT fwSide, LPRECT pRect)
{
POINT curPos;
GetCursorPos(
& curPos);
INT screenHeight
= GetSystemMetrics(SM_CYSCREEN);
INT screenWidth
= GetSystemMetrics(SM_CXSCREEN);
INT height
= pRect -> bottom - pRect -> top;
INT width
= pRect -> right - pRect -> left;

if (curPos.y <= INTERVAL)
{
// 粘附在上边
pRect -> bottom = height - m_edgeHeight;
pRect
-> top = - m_edgeHeight;
m_hideMode
= HM_TOP;
}
else if (curPos.y >= (screenHeight - INTERVAL - m_taskBarHeight))
{
// 粘附在下边
pRect -> top = screenHeight - m_taskBarHeight - height;
pRect
-> bottom = screenHeight - m_taskBarHeight;
m_hideMode
= HM_BOTTOM;
}
else if (curPos.x < INTERVAL)
{
// 粘附在左边
if ( ! m_isSizeChanged)
{
CRect tRect;
GetWindowRect(tRect);
m_oldWndHeight
= tRect.Height();
}
pRect
-> right = width;
pRect
-> left = 0 ;
pRect
-> top = - m_edgeHeight;
pRect
-> bottom = screenHeight - m_taskBarHeight;
m_isSizeChanged
= TRUE;
m_hideMode
= HM_LEFT;
}
else if (curPos.x >= (screenWidth - INTERVAL))
{
// 粘附在右边
if ( ! m_isSizeChanged)
{
CRect tRect;
GetWindowRect(tRect);
m_oldWndHeight
= tRect.Height();
}
pRect
-> left = screenWidth - width;
pRect
-> right = screenWidth;
pRect
-> top = - m_edgeHeight;
pRect
-> bottom = screenHeight - m_taskBarHeight;
m_isSizeChanged
= TRUE;
m_hideMode
= HM_RIGHT;
}
else
{
// 不粘附
if (m_isSizeChanged)
{
// 如果收缩到两边,则拖出来后会变回原来大小
// 在"拖动不显示窗口内容下"只有光栅变回原来大小
pRect -> bottom = pRect -> top + m_oldWndHeight;
m_isSizeChanged
= FALSE;
}
if (m_isSetTimer)
{
// 如果Timer开启了,则关闭之
if (KillTimer( 1 ) == 1 )
m_isSetTimer
= FALSE;
}
m_hideMode
= HM_NONE;
GetDlgItem(IDC_TIMER)
-> SetWindowText( " Timer off " );
}
}

收缩模式和位置决定后,剩下的工作就由最后两个核心函数完成了:实现收缩的DoHide(),实现伸展的DoShow()。在这两个过程中m_hsFinished,m_hiding 这两个变量起到很重要的控制作用。由于伸缩过程没完成时,hsFinished始终为FALSE,所以Timer 2 不会关闭,于是在OnTimer中会重复调用这两个函数之一,在这两个函数体内,窗口位置有规律地递减或递增就可以达到QQ的“抽屉”效果了,有趣的是即使伸缩过程还没完成,你也可以在这个过程中改变m_hiding这个值来决定他是伸还是缩,正如QQ一样。你可以把Timer 2 的事件间隔调大一点,然后在窗口伸缩时,鼠标来回地进出窗口就会很容易看到这样有趣的效果(还没缩进去又被拉了出来,或者还没拉出来又缩进去了)。

View Code
void CQQHideWndDlg::DoHide()
{
if (m_hideMode == HM_NONE)
return ;

CRect tRect;
GetWindowRect(tRect);

INT height
= tRect.Height();
INT width
= tRect.Width();

INT steps
= 0 ;

switch (m_hideMode)
{
case HM_TOP:
steps
= height / HS_STEPS;
tRect.bottom
-= steps;
if (tRect.bottom <= m_edgeWidth)
{
// 你可以把下面一句替换上面的 ...+=|-=steps 达到取消抽屉效果
// 更好的办法是添加个BOOL值来控制,其他case同样.
tRect.bottom = m_edgeWidth;
m_hsFinished
= TRUE; // 完成隐藏过程
}
tRect.top
= tRect.bottom - height;
break ;
case HM_BOTTOM:
steps
= height / HS_STEPS;
tRect.top
+= steps;
if (tRect.top >= (GetSystemMetrics(SM_CYSCREEN) - m_edgeWidth))
{
tRect.top
= GetSystemMetrics(SM_CYSCREEN) - m_edgeWidth;
m_hsFinished
= TRUE;
}
tRect.bottom
= tRect.top + height;
break ;
case HM_LEFT:
steps
= width / HS_STEPS;
tRect.right
-= steps;
if (tRect.right <= m_edgeWidth)
{
tRect.right
= m_edgeWidth;
m_hsFinished
= TRUE;
}
tRect.left
= tRect.right - width;
tRect.top
= - m_edgeHeight;
tRect.bottom
= GetSystemMetrics(SM_CYSCREEN) - m_taskBarHeight;
break ;
case HM_RIGHT:
steps
= width / HS_STEPS;
tRect.left
+= steps;
if (tRect.left >= (GetSystemMetrics(SM_CXSCREEN) - m_edgeWidth))
{
tRect.left
= GetSystemMetrics(SM_CXSCREEN) - m_edgeWidth;
m_hsFinished
= TRUE;
}
tRect.right
= tRect.left + width;
tRect.top
= - m_edgeHeight;
tRect.bottom
= GetSystemMetrics(SM_CYSCREEN) - m_taskBarHeight;
break ;
default :
break ;
}

SetWindowPos(
& wndTopMost,tRect);
}
View Code
void CQQHideWndDlg::DoShow()
{
if (m_hideMode == HM_NONE)
return ;

CRect tRect;
GetWindowRect(tRect);
INT height
= tRect.Height();
INT width
= tRect.Width();

INT steps
= 0 ;

switch (m_hideMode)
{
case HM_TOP:
steps
= height / HS_STEPS;
tRect.top
+= steps;
if (tRect.top >= - m_edgeHeight)
{
// 你可以把下面一句替换上面的 ...+=|-=steps 达到取消抽屉效果
// 更好的办法是添加个BOOL值来控制,其他case同样.
tRect.top = - m_edgeHeight;
m_hsFinished
= TRUE; // 完成显示过程
}
tRect.bottom
= tRect.top + height;
break ;
case HM_BOTTOM:
steps
= height / HS_STEPS;
tRect.top
-= steps;
if (tRect.top <= (GetSystemMetrics(SM_CYSCREEN) - height))
{
tRect.top
= GetSystemMetrics(SM_CYSCREEN) - height;
m_hsFinished
= TRUE;
}
tRect.bottom
= tRect.top + height;
break ;
case HM_LEFT:
steps
= width / HS_STEPS;
tRect.right
+= steps;
if (tRect.right >= width)
{
tRect.right
= width;
m_hsFinished
= TRUE;
}
tRect.left
= tRect.right - width;
tRect.top
= - m_edgeHeight;
tRect.bottom
= GetSystemMetrics(SM_CYSCREEN) - m_taskBarHeight;
break ;
case HM_RIGHT:
steps
= width / HS_STEPS;
tRect.left
-= steps;
if (tRect.left <= (GetSystemMetrics(SM_CXSCREEN) - width))
{
tRect.left
= GetSystemMetrics(SM_CXSCREEN) - width;
m_hsFinished
= TRUE;
}
tRect.right
= tRect.left + width;
tRect.top
= - m_edgeHeight;
tRect.bottom
= GetSystemMetrics(SM_CYSCREEN) - m_taskBarHeight;
break ;
default :
break ;
}

SetWindowPos(
& wndTopMost,tRect);
}

BOOL CQQHideWndDlg::SetWindowPos(
const CWnd * pWndInsertAfter, LPCRECT pCRect, UINT nFlags)
{
return CDialog::SetWindowPos(pWndInsertAfter,pCRect -> left, pCRect -> top,
pCRect
-> right - pCRect -> left, pCRect -> bottom - pCRect -> top, nFlags);
}

到此,程序终于完成了。在我的源代码中还有对WM_SIZING的处理和定义了与之相关的宏,这些主要是控制窗口在调整大小时不能超过最小的宽度和高度,与QQ的自动伸缩无关,所以不在这里提及了。

三、结束语

虽然还不能算是完美的模仿,但效果已经非常非常的接近了。也许有人会奇怪为什么要用Tool Window 风格,这是因为,这样在任务栏中不会显示窗口。从QQ的标题栏高度也可以判断出他也是这种风格,但这样一来就不能拥有最小化、最大化按键了。实际上QQ的最大化、最小化和关闭按键都是用DC画上去的。如何在Caption上增加按键,外国一些开源网站有源代码,我下载并看了一下,发现里面有个知识点很有趣,那就是更改消息路由,有兴趣的可以去下载来学习一下。

Demo: 下载源代码

原文地址: http://www.newxing.com/Tech/Program/VC/QQ_169.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值