How to Make Your Form "Snap" to the Screen Edge or Another Form

By: mykle hoban

Abstract: This document details the steps necessary to create "snapping" or "magnetic" windows, much like in Winamp.

Creating "Magnetic" Windows that Snap To Things

This question has been posed many times in various newsgroups: How do I make my form "snap" to things (screen edge/other windows, etc). Winamp can do it, so how do I? This is actually not a very difficult thing to acheive. It requires adding a couple of message-handlers to your form and that's about it.

Table Of Contents
Objective
Creating the Message Handlers
Code
Sample Project Source Code

Objective
In this article, I will explain how to create some message handlers for your form so that it will be able to "snap" to the screen edge or any other window you specify. This document assumes that you have a grasp of the following concepts:

  • Borland C++Builder
  • Some Win32 API
  • Understanding of Win32 Messaging system
  • VCL Experience

Making your form snap-able

There is no built-in feature of the Win32 API that allows windows to snap to each other, so we will have to add this functionality ourselves. To do this, we add two message handlers to our form. The first one, WM_SETTINGCHANGE. This one updates our snap area if the user changes the size of the screen, etc. The brunt of the work will be handled by WM_WINDOWPOSCHANGING. This message is sent whenever the window is moved or resized or focused, but all we really care about is when it's moved.

To set a message handler for your form, you use the MESSAGE_MAP macros:

BEGIN_MESSAGE_MAP
	MESSAGE_HANDLER(<message>,<msg struct type>,<handler>);
	MESSAGE_HANDLER(...);
	....
END_MESSAGE_MAP(TForm);
For the WM_SETTINGCHANGE message, we have several things: First, a private RECT structure (work_area) that tells us what our current work area is. We'll update this with an UpdateWorkArea function. UpdateWorkArea will be called from our message handler

Here is how it will look (you can see how it all fits together below in the code section):

//in our header file
MESSAGE_HANDLER(WM_SETTINGCHANGE,TMessage,WMSettingChanged);
//---------------------------------------------------------------------------
void __fastcall Tsnapform::WMSettingChanged(TMessage &msg)
{
  UpdateWorkArea();
}
//---------------------------------------------------------------------------
void __fastcall Tsnapform::UpdateWorkArea()
{
  SystemParametersInfo(SPI_GETWORKAREA, 0, &work_area, 0);
  //get the size of the desktop or whatever
}


Now we will set about making our WM_WINDOWPOSCHANGING handler. This will do what is necessary to have a nice snap-to window; you'll even be able to tell it how far to snap from.

As above, we tell our form about our message handler, then put it in a function. In this case, the function will be WMWindowPosChanging. The code is listed below, but with the comments it should be relatively straight forward.

WM_WINDOWPOSCHANGING sends us a WINDOWPOS structure through the lParam parameter. This structure tells us where our window is going and how big it's going to be. We can change this info through the WINDOWPOS structure. This is how we get it to snap.

There are a couple of private class members used in this function:
snapped (bool) tells it if it's snapped or not
snapwin (HWND) the window to snap to
thresh (int) is the number of pixels at which to snap
msg.WindowPos (WINDOWPOS) is the WINDOWPOS structure from lParam


void __fastcall Tsnapform::WMWindowPosChanging(TWMWindowPosChanging &msg)
{
  RECT sr;  //rect to snap to
  snapped=false;
  //test window
  if (snapwin && IsWindowVisible(snapwin)) //if we should snap to another window
  {
    if (GetWindowRect(snapwin,&sr)) //get it's bounds
    {
      //if we're within the snap threshold then snap
      if ( (msg.WindowPos->x <= (sr.right+thresh)) && 
           (msg.WindowPos->x >= (sr.right-thresh)) ) {  
        if ((msg.WindowPos->y > sr.top) && (msg.WindowPos->y < sr.bottom)) {
        //disallow "open air" snaps
          snapped=true;
          msg.WindowPos->x = sr.right;  
        }
      }
      else if ((msg.WindowPos->x + msg.WindowPos->cx) >= (sr.left-thresh) &&
               (msg.WindowPos->x + msg.WindowPos->cx) <= (sr.left+thresh)) {
        if ((msg.WindowPos->y > sr.top) && (msg.WindowPos->y < sr.bottom)) {
          snapped=true;
          msg.WindowPos->x = sr.left-msg.WindowPos->cx;
        }
      }

      if ( (msg.WindowPos->y <= (sr.bottom+thresh)) &&
           (msg.WindowPos->y >= (sr.bottom-thresh)) ) {
        if ((msg.WindowPos->x > sr.left) && (msg.WindowPos->x < sr.right)) {
          snapped=true;
          msg.WindowPos->y = sr.bottom;
        }
      }
      else if ((msg.WindowPos->y + msg.WindowPos->cy) <= (sr.top+thresh) &&
               (msg.WindowPos->y + msg.WindowPos->cy) >= (sr.top-thresh)) {
        if ((msg.WindowPos->x > sr.left) && (msg.WindowPos->x < sr.right)) {
          snapped=true;
          msg.WindowPos->y = sr.top-msg.WindowPos->cy;
        }
      }
    }
  }

  //test screen
  sr = work_area;
  //we have to test this in reverse so we switch the testing of the rect
  if (abs(msg.WindowPos->x) <= (sr.left+thresh)) {
    snapped=true;
    msg.WindowPos->x = sr.left;
  }
  else if ((msg.WindowPos->x + msg.WindowPos->cx) >= (sr.right-thresh) &&
           (msg.WindowPos->x + msg.WindowPos->cx) <= (sr.right+thresh)) {
    snapped=true;
    msg.WindowPos->x = sr.right-msg.WindowPos->cx;
  }

  if (abs(msg.WindowPos->y) <= (sr.top+thresh)) {
    snapped=true;
    msg.WindowPos->y = sr.top;
  }
  else if ((msg.WindowPos->y+msg.WindowPos->cy) >= (sr.bottom-thresh) &&
           (msg.WindowPos->y+msg.WindowPos->cy) <= (sr.bottom+thresh)) {
    snapped=true;
    msg.WindowPos->y = sr.bottom-msg.WindowPos->cy;
  }
}

The one part that really warrants explanation is testing the work area (screen). When we test for the window to snap, we only snap right-to-left, left-to-right, top-to-bottom, and bottom-to-top. (This is more explanatory if you run the example application). When we test for the screen we must reverse the order, snapping the right of our window to the right edge of the scree, etc.

Code
this is our form

mainunit.h

//---------------------------------------------------------------------------
#ifndef mainunitH
#define mainunitH
//---------------------------------------------------------------------------
#include <Classes.hpp>
#include <Controls.hpp>
#include <StdCtrls.hpp>
#include <Forms.hpp>

//---------------------------------------------------------------------------
class Tsnapform : public TForm
{
__published:	// IDE-managed Components
  TEdit *Edit1;
  TButton *Button1;
  TEdit *Edit2;
  TButton *Button2;
  TCheckBox *move;
  void __fastcall Button1Click(TObject *Sender);
  void __fastcall Button2Click(TObject *Sender);
private:	// User declarations
  HWND snapwin;
  RECT work_area;
  bool snapped;
  bool winprocthing;
  int thresh;
  void __fastcall SettingChanged(TMessage &msg);
  void __fastcall WMWindowPosChanging(TWMWindowPosChanging &msg);
  void __fastcall UpdateWorkArea();
public:		// User declarations
  __fastcall Tsnapform(TComponent* Owner);
  __fastcall ~Tsnapform();

BEGIN_MESSAGE_MAP
  MESSAGE_HANDLER(WM_WINDOWPOSCHANGING,TWMWindowPosChanging,WMWindowPosChanging);
  MESSAGE_HANDLER(WM_SETTINGCHANGE,TMessage,SettingChanged);
END_MESSAGE_MAP(TForm);
};
//---------------------------------------------------------------------------
extern PACKAGE Tsnapform *snapform;
//---------------------------------------------------------------------------
#endif

mainunit.cpp

//---------------------------------------------------------------------------

#include <vcl.h>
#pragma hdrstop

#include "mainunit.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
Tsnapform *snapform;
//---------------------------------------------------------------------------
__fastcall Tsnapform::Tsnapform(TComponent* Owner)
  : TForm(Owner)
{
  snapped=false;
  UpdateWorkArea();  //set our workarea rect
  thresh = StrToInt(Edit2->Text); //get threshold
  snapwin = FindWindow(Edit1->Text.c_str(),NULL); //get window to snap to
}
//---------------------------------------------------------------------------
__fastcall Tsnapform::~Tsnapform()
{
}
//---------------------------------------------------------------------------
void __fastcall Tsnapform::SettingChanged(TMessage &msg)
{
  UpdateWorkArea();
}
//---------------------------------------------------------------------------
void __fastcall Tsnapform::WMWindowPosChanging(TWMWindowPosChanging &msg)
{
  RECT sr;
  snapped=false;
  //test window
  if (snapwin && IsWindowVisible(snapwin))
  {
    if (GetWindowRect(snapwin,&sr))
    {
      if ( (msg.WindowPos->x <= (sr.right+thresh)) &&
           (msg.WindowPos->x >= (sr.right-thresh)) ) {
        if ((msg.WindowPos->y > sr.top) && (msg.WindowPos->y < sr.bottom)) {
          snapped=true;
          msg.WindowPos->x = sr.right;
        }
      }
      else if ((msg.WindowPos->x + msg.WindowPos->cx) >= (sr.left-thresh) &&
               (msg.WindowPos->x + msg.WindowPos->cx) <= (sr.left+thresh)) {
        if ((msg.WindowPos->y > sr.top) && (msg.WindowPos->y < sr.bottom)) {
          snapped=true;
          msg.WindowPos->x = sr.left-msg.WindowPos->cx;
        }
      }

      if ( (msg.WindowPos->y <= (sr.bottom+thresh)) &&
           (msg.WindowPos->y >= (sr.bottom-thresh)) ) {
        if ((msg.WindowPos->x > sr.left) && (msg.WindowPos->x < sr.right)) {
          snapped=true;
          msg.WindowPos->y = sr.bottom;
        }
      }
      else if ((msg.WindowPos->y + msg.WindowPos->cy) <= (sr.top+thresh) &&
               (msg.WindowPos->y + msg.WindowPos->cy) >= (sr.top-thresh)) {
        if ((msg.WindowPos->x > sr.left) && (msg.WindowPos->x < sr.right)) {
          snapped=true;
          msg.WindowPos->y = sr.top-msg.WindowPos->cy;
        }
      }
    }
  }

  //test screen
  sr = work_area;
  if (abs(msg.WindowPos->x) <= (sr.left+thresh)) {
    snapped=true;
    msg.WindowPos->x = sr.left;
  }
  else if ((msg.WindowPos->x + msg.WindowPos->cx) >= (sr.right-thresh) &&
           (msg.WindowPos->x + msg.WindowPos->cx) <= (sr.right+thresh)) {
    snapped=true;
    msg.WindowPos->x = sr.right-msg.WindowPos->cx;
  }

  if (abs(msg.WindowPos->y) <= (sr.top+thresh)) {
    snapped=true;
    msg.WindowPos->y = sr.top;
  }
  else if ((msg.WindowPos->y+msg.WindowPos->cy) >= (sr.bottom-thresh) &&
           (msg.WindowPos->y+msg.WindowPos->cy) <= (sr.bottom+thresh)) {
    snapped=true;
    msg.WindowPos->y = sr.bottom-msg.WindowPos->cy;
  }
}
//---------------------------------------------------------------------------
void __fastcall Tsnapform::UpdateWorkArea()
{
  SystemParametersInfo(SPI_GETWORKAREA, 0, &work_area, 0);
  //get the size of the desktop or whatever
}
//---------------------------------------------------------------------------
void __fastcall Tsnapform::Button1Click(TObject *Sender)
{
  snapwin = FindWindow(Edit1->Text.c_str(),NULL);
}
//---------------------------------------------------------------------------
void __fastcall Tsnapform::Button2Click(TObject *Sender)
{
  thresh = StrToInt(Edit2->Text);
}
//---------------------------------------------------------------------------

Server Response from: ETNASC01