当前位置:C++技术网 > 资讯 > 在客户区拖动窗口(拖动无边框窗口)两种实现方法及原理分析

在客户区拖动窗口(拖动无边框窗口)两种实现方法及原理分析

更新时间:2015-06-23 14:39:33浏览次数:1+次

    正如简介所说,两种方式,各有所长。下面讲解一下两种方式。根据具体情况选择。
    第一种,基于MFC窗口机制。这种方式代码很简单。但是如果你不了解其中的机制,你就想不到可以这样使用。同时,如果你不了解,你也掌握不了,下次使用又记不住。提到记忆,只是因为没有理解,如果你理解,使用过几次后,就忘不了了。所以,理解内部机制,你才能够真正掌握,以后需要就新手拈来。先来看看代码。

LRESULT CbtnTextDlg::OnNcHitTest(CPoint point)
{
    UINT nHitTest = CDialogEx::OnNcHitTest(point);
    if(nHitTest == HTCLIENT)//如果是客户区
    nHitTest = HTCAPTION; //则把它当成标题栏
    return nHitTest;
}
//为了更清晰起见,将整个函数都贴出来,这样读者就知道这个函数在代码中的样子。

    这是MFC中的代码,所以,如果你是使用MFC的话,就直接可以复制粘贴就可以达到效果。当然,如果你不了解,呵呵,你根本就不知道这个段代码贴到哪去。所以,为了以后运用自如,慢慢看完分析解说。
    OnNcHitTest是消息WM_NCHITTEST的标准响应函数,所以,你使用类向导添加这个消息的响应,然后将函数内的代码写成代码段中的那样就可以实现了。如果你看到这个,就把代码贴过去解决了,然后不接着往下看看,那你就是码农了。OnNcHitTest返回的值,自然最终传递给系统,从而使系统来改变窗体。你要记住,你所有的代码都是请系统来帮忙的,你不会直接使窗体移动的,虽然是你写的代码,你只是委托系统完成的。这就是API调用的思想。而函数只有一个参数。这个参数是点结构体,有这个参数就够了。这个点是鼠标单击的屏幕点坐标,也就是原点是屏幕左上角。系统会根据这个鼠标的点,结合窗口的非客户区来判断这点落在这个区域没有,如果落在里面,则返回一个标志码,表示已经单击了非客户区,如果拖动的话,窗口就随之拖动了。这就是拖动标题栏自动拖动窗口的原理。平常我们拖动窗口并不会注意这个细节。但是,如果你的窗口选择了无边框的风格,那么,问题就来了。因为没有边框,标题栏都没了。既然没有了,那么就不可能有非客户区的单击了。这样就无法拖动窗口了。解决办法在这段代码中了。
    OnNcHitTest这个函数是非客户单击测试消息响应函数,其实这样会使很多人有误解,以为是单击非客户区才有的。其实不是的,这个函数名表示它要执行的功能只是测试非客户区,如果满足就执行一定的操作,如果不是就不执行默认的操作。也就是说,函数里面是对鼠标单击进行了测试的,只是忽略了客户区的单击。也就是说,单击客户区这个函数是可以接受到单击消息了。这也就是为什么使用这个函数可以实现响应移动窗口的原因。这个恐怕是很多人都很模糊的地方。
    现在来仔细分析这个代码。在类向导生成后,函数内只有一句代码: return CDialogEx::OnNcHitTest(point);这个就是进行默认的处理。所以如果我们进处理客户区移动窗口,就可以在这里,改写默认的代码实现。因为在查找消息时,并没有发现现成的WM_CHITTEST这个消息,也就是说,默认不提供客户区的响应消息。所以,我们可以借助非客户区来间接实现。当然,直接对客户区操作,当然是可以的,这就是第二种方法了。稍后再讲解。我们不需要写很多代码,去移动窗口。既然是借助系统的,那么我们就利用它的机制就行了。这里是偷换概念的方法。所以,默认的代码我们只要稍微加几句偷换的代码即可。所以第一句,UINT nHitTest = CDialogEx::OnNcHitTest(point);是利用默认的处理,得到一个返回值,就是测试结果。如果测试结果是非客户区的标题栏码HTCAPTION,那么系统就做相应的处理,如果我们按住左键移动了鼠标,窗口就随之移动了,如果我们双击标题栏,默认处理就是最大化或者最小化。具体的行为和你程序的风格有关。但是如果我们是移动客户区,那么测试结果是客户区码,我想你知道如何进一步处理了。既然我不是非客户区码,那系统就不会有默认行为,但是知道这个机制,是通过返回码这个“接口”概念来处理的,只要你提供的返回码是标题栏的就可以了,函数通过“接口”取得这个返回码就知道测试的结果,从而做相应的处理。所以,真是这个“接口”的概念,函数之间耦合度很低,我们并不用改变其他的函数即可轻易改变程序的行为,这也是接口的好处。所以,我们检测到客户区码时就将准备返回的测试结果码nHitTest替换为标题栏码HTCAPTION,这样的效果就如同标题栏一样,从而实现这个拖动客户区来移动的功能。这是很便捷的方式。
    但是,有时候,在其他方面应用客户区拖动窗口,就没有这个遍历的机制了,比如SDK开发。这个机制也不过就是对SDK消息的封装再处理。既然我们不用这个机制,如果我们再用这个机制来间接实现的话,就有点舍近求远了,不划算。比如说,你也可以模拟这个机制实现,但是没太大必要,除非你想做平台工具级别的,就可以很好的为以后使用准备。如果不是,就直接使用原始的消息来处理,其实也不难。
    第二种,基于鼠标单击的消息处理。基于这种方式,最灵活。简单来说,就是检测单击消息,如果落在客户区,然后进行响应即可。如何响应,你可以尽情发挥。具体的话,我们来往下继续分析。

//类成员变量:
//m_isMovable:表示是否可移动
//m_PrevPoint:移动前的鼠标坐标
//m_PrevRect:移动前的窗口区域
//基本思想:通过移动前的鼠标坐标和移动后的当前的鼠标坐标,形成X和Y轴的偏移量,来移动窗口,达到鼠标和窗口的同步移动。
void CMini::OnLButtonDown(UINT nFlags, CPoint point)
{
    m_isMovable = TRUE;        //按下鼠标,可以移动
    m_PrevPoint = point;       //按下鼠标,记录移动前鼠标坐标
    GetClientRect(&m_PrevRect);//按下鼠标,记录移动前窗口区域
    SetCapture();              //捕捉鼠标,使鼠标消息不受干扰
}

void CMini::OnLButtonUp(UINT nFlags, CPoint point)
{
    m_isMovable = FALSE;//松开鼠标,移动完毕
    ReleaseCapture();   //松开鼠标,释放捕捉,如果忘记则其他窗口不能正常响应鼠标消息
}

void CMini::OnMouseMove(UINT nFlags, CPoint point)
{
    if (GetCapture()!=this)
        m_isMovable = FALSE;//如果捕捉的不是当前窗口,则不能移动窗口
    if (m_isMovable)
    {
        CPoint OffsetPoint = point - m_PrevPoint;//计算偏移量
        //////形成新窗口矩形区域///////////////
        CRect newRect =  m_PrevRect;
        newRect.left  += OffsetPoint.x;
        newRect.right += OffsetPoint.x;
        newRect.top   += OffsetPoint.y;
        newRect.bottom+= OffsetPoint.y;
        //////形成新窗口矩形区域///////////////
        MoveWindow(&newRect);//移动窗口到新区域
        m_PrevRect = newRect;//设置原先的窗口区域为当前区域,使窗口可以连续移动
    }
}

    提醒:因为第二个方法是业务逻辑实现,因此方法还有很多,这里可能不是最优的方法,可能有Bug。如果发现Bug,请提出建议,改进代码,期待与你交流,相互学习。