用户操作
[留言]  [发消息]  [加为好友] 
订阅我的博客
XML聚合    FeedSky
订阅到鲜果
订阅到Google
订阅到抓虾
windboyzsj的公告
独乐乐,不如众乐乐,好东西就应该拿出来分享。
文章分类
    Blog
    longshanks
    存档

    原创  超强仿QQ自动伸缩窗口 收藏

                              
       某天在CSDN论坛上看到有人发帖询问QQ自动伸缩窗口是怎么实现的,我也好想知道,于是到百度一搜索,结果不多,来来去去都是那几篇,下载那些demo运行一下,发觉效果与QQ相差很大,于是决定自己动手做个,要求要近乎完美地模仿这个功能。由于是些效果的东西,贴图也看不出来,所以文章里就不截图了,想看效果的就直接运行源代码的demo吧。
     
    一、观察
     
    模仿前最重要的一步就是观察,经过半天对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
          
    INT m_oldWndHeight;    //旧的窗口宽度
    INT m_taskBarHeight;   //任务栏高度
    INT m_edgeHeight;      //边缘高度
    INT m_edgeWidth;       //边缘宽度
     
    INT m_hideMode;        //隐藏模式
    BOOL m_hsFinished;      //隐藏或显示过程是否完成
    BOOL m_hiding;          //该参数只有在!m_hsFinished才有效
                               //真:正在隐藏,假:正在显示
     
    增加消息响应,需要注意的是有些消息你只有把右下角的 Filter for message 设置为window才能看到。
    WM_ NCHITTEST
    WM_MOVING
    WM_CREATE
    WM_TIMER
     
    然后来到对应的cpp文件,在头部定义一些宏:
     
    //收缩模式
    #define HM_NONE     0   //不收缩
    #define HM_TOP      1   //向上收缩
    #define HM_BOTTOM   2   //向下收缩
    #define HM_LEFT     3   //向左收缩
    #define HM_RIGHT    4   //向右收缩
     
    #define CM_ELAPSE   200 //检测鼠标是否离开窗口的时间间隔
    #define HS_ELAPSE   5   //伸缩过程每步的时间间隔
    #define HS_STEPS    10 //伸缩过程分成多少步完成
     
    #define INTERVAL    20 //触发粘附时鼠标与屏幕边界的最小间隔,单位为象素
    #define INFALTE     10 //触发收缩时鼠标与窗口边界的最小间隔,单位为象素
     
    然后在构造函数初始化成员变量:
     
    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时出现的效果。
     
    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 的事件间隔调大一点,然后在窗口伸缩时,鼠标来回地进出窗口就会很容易看到这样有趣的效果(还没缩进去又被拉了出来,或者还没拉出来又缩进去了)。
     
    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);
    }


    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上增加按键,外国一些开源网站有源代码,我下载并看了一下,发现里面有个知识点很有趣,那就是更改消息路由,有兴趣的可以去下载来学习一下。
    QQ的成功很大部分在于他的界面比较人性化(用了MSN后深有感受),而这些界面实现起来原理也许很简单,难的是观察东西心要细、设计东西心要密、开发东西心要异。
    (次文已经在vcbase发表,鉴于他们整理出的样式不方便浏览,故在次自己整理一下) 

    发表于 @ 2007年09月23日 13:45:00 | 评论( loading... ) | 编辑| 举报| 收藏

    旧一篇:堆和栈的区别 | 新一篇:探索Win32系统之窗口类(Window Classes in Win32)

    • 发表评论
    • 评论内容:
    •  
    Copyright © windboyzsj
    Powered by CSDN Blog