Selecting and Moving Elements
The next requirement to tackle is to allow a user to select and then drag a graphical element on the page. This will again involve our mouse events (MouseDown, MouseMove, and MouseUp). From an algorithm perspective, the code needs to handle the following sequence of events:
- Determine when the user clicks the mouse and they are not intending to draw.
- Check to see if the user's click intersects with any of their previously drawn elements.
- Visually indicate the selection to the user.
- Re-draw the element as the user moves it about the page.
- Re-lock the element on the page.
The first requirement is obviously the simplest, just an If statement inside of the MouseDown
event (see Listing 1
As for the second requirement, you need to write code to solve a common problem called "hit-testing." Hit-testing involves capturing the point of the user's cursor at the time of their click and then determining if that captured point is within the bounds of any other element.
For simplicity sake, in this version, the code only supports single item select (no multi-select) and drag. In addition, elements are hit-tested in the reverse order that they were drawn to the screen (or added to the Elements collection class). This way, if two elements overlapped, the top-most Element would be selected every time. Let's look at the code that handles this.
First, add a routine to the Elements collection class called GetHitElement
. This routine takes a Point (testPoint
) type that represents the user's click (and our testing point). The routine returns the hit (or user selected) element (or a null in the case of no selection). You call this method from the MouseDown
event on the control:
Element el =
Inside the GetHitElements
method you simply loop the collection backwards and call each Element's internal HitTest
method as follows:
//search the list backwards
for (int i=this.List.Count; i>0; i--)
Element e = (Element)this.List[i-1];
Next you need to make each concrete Element class expose their own HitTest
method. This is required because each element's shape can be different (rectangle vs. ellipse, for example). Thankfully, GDI+ makes hit-testing pretty simple. First create a System.Drawing.Drawing2D.GraphicsPath instance:
GraphicsPath gp = new GraphicsPath();
You'll use the GraphicsPath
object to contain a version of the given Element. Therefore, you add the element to the graphics path:
new Rectangle(this.Position, this.Size));
Next, you check the value of the IsVisible
property of the GraphicsPath instance to determine if the user's click point is inside the element:
If the call to IsVisible
, you have a hit. Once you have determined the user's selected object, you need to indicate that visually to the user. To do this you'll add code to the PageControl's MouseDown
event. Once again you'll leverage the object model. Now set the hit element to the Document's SelectedElement
property, which allows you to add code to the control's OnPaint
event that checked to make sure that a SelectedElement
exists. If so, the Paint event tells the selected element to draw itself as follows:
if(m_doc.SelectedElement != null)
Of course, you have to create a DrawSelected
method for each concrete Element class. In this case, to represent selection the code will just draw the object's outline in blue since the application does not allow the user to create objects with anything other than black pens.
Finally, to illustrate movement of the selected element about the page, you reset the selected element's Position property to that of the newly moved-to position. Before doing this, you need to set the moved point's offset values based on a difference calculated in the MouseDown
m_doc.SelectedElement.Position = mp;