主    题:怎样建立磁性窗体  
作    者: xingzhou  
杂志期号:《程序员》2002年第01期 
文章内容:
    磁性窗体技巧很有趣,CSDN上的相关帖子也很多,本文对实现磁性窗体的相关细节进行了详细分析,并给出了完整的关键代码(C++ Builder)。
    关键词:磁性窗体,WM_MOVING,WM_MOVE,SetWindowsPos 
    一些著名的共享软件不但功能卓著,而且在程序界面的设计技巧上往往领导了一种时尚,WinAmp就是其中的一个代表。WinAmp有两个绝活,一是可以更换窗体的外观,也就是现在俗称的给软件换“皮肤”;另一个及时磁性窗体技巧。
     
    CDOK的磁性窗体
    磁性窗体即若干窗体靠近到一定距离以内时会互相粘在一起,或者说相互吸附在一起,然后在拖动主窗体时,粘在其上的其它窗体也一起跟着移动,好像变成了一个窗体。国内的MP3播放器新秀CDOK也实现了这种技巧,而且更绝,把几个窗体粘在一起后,窗体没有主从之分,拖动其中任意一个窗体都会使其它的窗体一起移动。在CSDN上有关怎样设计磁性窗体的帖子非常多,说明这个技巧深得广大程序员的青睐。
    本文先把几位网友的方法略加分析,然后给出我认为比较可行的实现方法和源代码。
    实现磁性窗体基本上分为两步,第一步是实现当两个窗体靠近到一定距离以内时实现窗体间的粘贴操作,第二步是一定窗体时,同时移动与它粘在一起的其它窗体。
    
    实现窗体的粘贴
    实现粘贴的难点在于什么时候进行这个操作,假设有两个窗体Form1和Form2,移动Form2向Form1靠近,当Form2与Form1的最近距离小于distance时粘贴在一起。显然,应该在移动Form2的过程中进行判断,问题是在程序的什么位置插入判断代码呢?
    CSDN上有人认为可以使用定时器,每隔一定的时间检查各个窗体的位置。这种方法有着明显的弊病,不说定时器要无谓地浪费系统资源,单单它的即时性就难以保证。如果缩短计时值,浪费的CPU资源就更多了,所以我也就不多说了。
    合理的方法是利用系统产生的消息,但是利用什么消息呢?窗体在移动时会产生WM_WINDOWPOSCHANGING和WM_MOVING消息,移动结束后会产生WM_WINDOWPOSCHANGED和WM_MOVE消息。WM_WINDOWPOSCHANGING和WM_WINDOWPOSCHANGED消息的参数lParam是结构WINDOWPOS的指针,WINDOWPOS定义如下:
    typedef struct _WINDOWPOS {
        HWND hwnd; // 窗口句炳
        HWND hwndInsertAfter; // 窗口的Z顺序
        int  x; // 窗口x坐标
        int  y; // 窗口的y坐标
        int  cx; // 窗口的宽度
        int  cy; // 窗口的高度
        UINT flags; // 标志位,根据它设定窗口的位置
    } WINDOWPOS;
    可以看出,WM_WINDOWPOSCHANGED消息不仅仅在窗口移动时产生,而且在它的Z顺序发生变化时产生,包括窗口的显示和隐藏。所以我认为这个消息不是最佳选择。
    WM_MOVING和WM_MOVE消息的参数lParam是一个RECT结构指针,与WM_WINDOWPOSCHANGED消息相比较为单纯,我采用的即是这个消息。下面我给出用C++ Builder写的示例程序。
    为了方便程序的阅读,先定义了一个枚举数据类型,表示窗体的粘贴状态。同时定义了一个类,封装了窗体粘贴相关的数据,其中的Enable是为了防止重复进行操作,方法是操作时设置Enable为否,操作结束时恢复为真,而在操作前检查这个标志是否为否,否则直接返回。
     
    图2 窗体的粘贴状态示例
    // 窗体粘贴状态,含义见图2
    enum enumAttachStyle
    {
        AS_NONE,    // 没有粘贴
        AS_TOP,
        AS_BOTTOM, 
        AS_T_TOP,
        AS_LEFT,
        AS_RIGHT,
        AS_L_LEFT
    };
    // 处理窗体粘贴的类,为了简化,采用了public声明
    class CFormAttachStyle
    {
        public:
            bool Enabled; // 防止重复进行粘贴相关的操作
            HWND  AttachTo; // 被粘贴到哪个窗口
            int XStyle; // 左右方向的粘贴状态
            int YStyle; // 上下方向的粘贴状态
            int xPos; // 粘贴到的x坐标
            int yPos; // 粘贴到的y坐标
            CFormAttachStyle() // 初使化数据
            {
                XStyle =AS_NONE;
    YStyle =AS_NONE;
                Enabled=true;
                hAttachTo=NULL;
            }
    };
    函数DistanceIn用于判断整数两个整数的距离是否在指定范围内:
    // 整数i1和i2的差的绝对值小于i3
    bool DistanceIn(unsigned int i1,unsigned int i2,unsigned int i3)
    {
        if(i1>i2)
        {  // 确保i2>=i1;
            int t=i1;
            i1=i2;
            i2=t;
        }
        return i2-i1<=i3;
    }
    //---------------------------------------------------------------------------
    // i1<=i2<i3
    bool Mid(unsigned int i1,unsigned int i2,unsigned int i3)
    {
        return ((i1<=i2) && (i2<i3));
    }
    //---------------------------------------------------------------------------
    AttachToForm是处理窗体粘贴的关键函数,如果进行了粘贴,则保存粘贴到的窗体的句柄,并调整窗体的位置。在函数中使用了窗体的Tag属性保存了一个CFormAttachStyle类的数据指针,原因将在稍后进行说明,参数distance表示可以进行粘贴的距离。窗口粘贴在上下、左右各有3种形式,都需要加以判断。
    // 把窗体My粘到主窗体上
    bool AttachToForm(TForm *My, TForm *Form, RECT *r,int distance)
    {
        CFormAttachStyle *MyStyle=(CFormAttachStyle *)My->Tag;
        if(MyStyle==NULL)return false;  // 这个窗体不支持粘贴
        //准备粘贴到的窗体的位置
        RECT rMain;
        GetWindowRect(Form->Handle,&rMain);
        MyStyle->AttachTo=NULL;
        MyStyle->yPos=r->top;
        MyStyle->xPos=r->left;
        // 上下方向判断
        MyStyle->YStyle=AS_NONE;
        if(  Mid(rMain.left,r->left,rMain.right)
     || Mid(r->left,rMain.left,r->right)
    || (MyStyle->XStyle!=AS_NONE))
        {
            if(DistanceIn(r->top,rMain.bottom,space))
            {
                MyStyle->YStyle=AS_BOTTOM;
                MyStyle->yPos=rMain.bottom;
            }else if(DistanceIn(r->top,rMain.top,space))
            {
                MyStyle->YStyle=AS_TOP;
                MyStyle->yPos=rMain.top;
            }else if(DistanceIn(r->bottom,rMain.top,space))
            {
                MyStyle->YStyle=AS_T_TOP;
                MyStyle->yPos=rMain.top-(r->bottom-r->top);
            }
        }
        // 左右方向判断
        MyStyle->XStyle=AS_NONE;
        if(  Mid(rMain.top,r->top,rMain.bottom) 
    || Mid(r->top,rMain.top,r->bottom)
    || (MyStyle->YStyle!=AS_NONE))
        {
            if(DistanceIn(r->left,rMain.left,space))
            {
                MyStyle->XStyle=AS_LEFT;
                MyStyle->xPos=rMain.left;
            }else if(DistanceIn(r->left,rMain.right,space))
            {
                MyStyle->XStyle=AS_RIGHT;
                MyStyle->xPos=rMain.right;
            }else if(DistanceIn(r->right,rMain.left,space))
            {
                MyStyle->XStyle=AS_L_LEFT;
                MyStyle->xPos=rMain.left-(r->right-r->left);
            }
        }
        My->Left=MyStyle->xPos;
        My->Top=MyStyle->yPos;
        if(MyStyle->XStyle!=AS_NONE || MyStyle->YStyle!=AS_NONE)
        {  // 吸附完成
            MyStyle->AttachTo= Form->Handle;
        }
        return bool(MyStyle->AttachTo); 
    }
    函数Do_WM_MOVING在消息循环中处理WM_MOVING时调用,参数My为处理消息的窗体,Msg为消息参数。
    // 处理WM_MOVING事件
    void Do_WM_MOVING(TForm *My,TMessage &Msg)
    {
        CFormAttachStyle *MyStyle=(CFormAttachStyle *)My->Tag;
        if(MyStyle && MyStyle->Enabled)
        {
            MyStyle->Enabled=false; // 防止重复操作
            RECT *r=(RECT *)Msg.LParam ;
            // 处理粘贴,这里只对粘贴到主窗体进行判断
            Tform *FormApplication->MainForm;
            AttachToForm(My,r,12); // 检查是否可以粘贴窗体
            MyStyle->Enabled=true; // 恢复操作状态
        }
        Msg.Result=0;  // 通知系统,消息已经处理
    }
    实现窗体的关联移动
    与处理窗体粘贴相比,关联窗体的难度小一些。但是从CSDN上的帖子看,采用的方法都单调而且不佳,我都不推荐。
    比较直观的方法是使用窗体的MOUSEDOWN、MOUSEMOVE和MOUSEUP事件,先定义一个标志鼠标是否按下的变量:
    bool bMouseDown;
    在MOUSEDOWN事件中设置:
    bMouseDown=true;
    在MOUSEUP事件中设置:
    bMouseDown=false;
    在MOUSEMOVE事件中:
    if(bMouseDown)
    {
      // 移动当前窗体
      ……
    // 计算窗体移动的位移
      int dx; 
      int dy; 
      // 计算出dx和dy
      ……
      // 移动其它粘贴到当前窗体的窗体
      ……
    }
    这个方法的最明显的问题有两个:1、鼠标在窗体上的控件上按下时,不能收到窗体的MOUSEDOWN和MOUSEUP事件,如果同时监控各个控件的事件,麻烦是相当大的。

解决方案 »

  1.   

    移动其它窗体的方法也有多种,有人采用发送消息的方式,具体如下:
        // dx和dy是当前窗体移动的距离
        // hMove是要移动的窗体
        // WM_MOVEFORM是自定义的消息
        PostMessage(hMove, WM_MOVEFORM,dx,dy);
        被移动的窗体处理WM_MOVEFORM消息时,移动自己到新的位置。
        如果是VB、Delphi一类的语言,可以直接设置其Left和Top属性。我采用的方法是使用API函数SetWindowPos,该函数重新设置指定窗口的位置。我的参考代码如下:
        // 移动被粘贴在一起的其它窗体
        void UnionOtherForm(TForm *My,TForm *Form,int dx,int dy)
        {
            if(Form==NULL)return;
            CFormAttachStyle *MyStyle=(CFormAttachStyle *)(Form->Tag);
            if(MyStyle)
            {
                if(MyStyle->Enabled && MyStyle->AttachTo==My)
                {
                    MyStyle->Enabled=false;
                    int X1=Form->Left;
                    int Y1=Form->Top;
                    SetWindowPos(Form->Handle,My->Handle,
        X1+dx,Y1+dy,Form->Width,Form->Height,
        SWP_NOSIZE|SWP_NOACTIVATE);
                    MyStyle->Enabled=true;
                }
            }
        }
        // 移动被粘贴在一起的其它窗体
        void AdjuctFormPos(TForm *My,RECT *r)
        {
            // 调整窗口位置
            int dy=r->top-My->Top;
            int dx=r->left-My->Left;
            My->Top=r->top;
            My->Left=r->left;
            // 逐一检查创建的窗体
            for(int i=0;i<Screen->FormCount;i++)
            {
                TForm *Form=Screen->Forms[i];
                if(Form!=My)
                {
                    // 调整被吸附的窗口位置
                    UnionOtherForm(My,Form,dx,dy);
                }
            }
        }
        // 处理WM_MOVE事件
        void Do_WM_MOVE(TForm *My,TMessage &Msg)
        {
            // 处理粘贴成功后的位置调整
            CFormAttachStyle *MyStyle=(CFormAttachStyle *)My->Tag;
            if(MyStyle && MyStyle->Enabled)
            {
                if(MyStyle->Enabled && MyStyle->AttachTo)
                {  // 粘贴成功
                    My->Left=MyStyle->xPos;
                    My->Top=MyStyle->yPos;
                }
            }
            Msg.Result=1;  // 通知系统,消息已经处理
        }
        在这里有一个C++ Builder编程的技巧,即使用Screen全局对象。如果在初使化需要使用粘贴功能的窗体时,把一个CFormAttachStyle实例的指针赋值给该窗体的Tag窗体,那么除了处理它的WM_MOVING和WM_MOVE事件外,其它的操作都可以省略了。关键的代码如下:
        // 注:应把这个函数的声明加到TForm1的类声明中
        void __fastcall TForm1::WndProc(TMessage &Msg)
        {
            TForm::WndProc(Msg);
            switch(Msg.Msg)
            {
                case WM_MOVING: // 处理移动事件
                {
                    Do_WM_MOVING(this,Msg);
                    break;
                }
                case WM_MOVE: // 处理移动事件
                {
                    Do_WM_MOVE(this,Msg);
                    break;
                }
            }
        }
        void __fastcall TForm1::FormCreate(TObject *Sender)
        {
            // 建立磁性窗体特性类
            CFormAttachStyle *AttachStyle=new CFormAttachStyle;
            Tag=(int)AttachStyle;
        }
        void __fastcall TForm1::FormDestroy(TObject *Sender)
        {  // 删除CformAttachStyle实例
            CFormAttachStyle *AttachStyle=(CFormAttachStyle *)Tag;
            delete AttachStyle;
        }
        以下是主窗体处理WM_MOVING消息的代码:
        void __fastcall TfmMain::WndProc(TMessage &Msg)
        {
            TForm::WndProc(Msg);
            switch(Msg.Msg)
            {
                case WM_MOVING: // 处理移动事件
                {
                    AdjuctFormPos(this,(RECT *)(Msg.LParam));
                    break;
                }
            }
        }
        到此,实现磁性窗体的步骤基本上都介绍完了,如果读者感兴趣,可以到《程序员》网站下载本文的可执行程序和源代码。
      

  2.   

    VC的窗口效果很复杂,你可以求助于现成的组件!toobar97可以的!
      

  3.   

    我的意思是怎么使一个子窗体能够DOCK到主窗体的任意边框,就象画图中的工具栏一样。