Tuesday, January 10, 2012

Handling Mouse Events in Smart Mobile Studio

After I finished my very first Smart program, I wanted to enhance it with some interactivity. I wanted the user to be able to navigate through the Mandelbrot set by using mouse and touch. For now, I have only implemented the mouse part, touch events will be handled later (soon, I hope).

I wanted to achieve few different ways of navigation:

  • Click & drag with the left mouse button shows a rectangle on the screen. When the mouse button is released, program zooms in to display the selected rectangle.
  • Double-clicking zooms in around the point of click.
  • Right-clicking zooms out around the center of the image.
  • Right-click & drag shows a line on the screen. When the mouse button is released, program moves the current view (point of first click is moved to the point of release).
  • If a drag operation is in progress, user can click and release the other mouse button (left if right-drag is in progress, right if left-drag is in progress) to cancel the drag operation.

You can see the navigation in action in this (very low quality, sorry) YouTube video.

Smart tries to make mouse handling similar to the VCL. TW3CustomControl class (which is, as you can guess, a base class for all custom controls) defines following mouse events:

type
TMouseDownEvent = procedure (Sender: TObject; Button: TMouseButton;
X, Y: Integer);
TMouseUpEvent = procedure (Sender: TObject; Button: TMouseButton;
X, Y: Integer);
TMouseMoveEvent = procedure (Sender: TObject; X, Y: Integer);
TMouseEnterEvent = procedure (Sender: TObject; X, Y: Integer);
TMouseLeaveEvent = procedure (Sender: TObject; X, Y: Integer);
TMouseClickEvent = procedure (Sender: TObject);
TMouseDblClickEvent = procedure (Sender: TObject);

TW3CustomControl = class(TW3Component, IW3CustomControl)
...
property OnMouseDown: TMouseDownEvent;
property OnMouseUp: TMouseUpEvent;
property OnMouseMove: TMouseMoveEvent;
property OnMouseEnter: TMouseEnterEvent;
property OnMouseLeave: TMouseLeaveEvent;
property OnClick: TMouseClickEvent;
property OnDblClick: TMouseDblClickEvent;
...
end;

Hooking events is done the same as in Delphi. As there is currently no visual designer in the alpha release, I had to connect all events manually.

  FCanvas.OnMouseDown := HandleMouseDown;
FCanvas.OnMouseMove := HandleMouseMove;
FCanvas.OnMouseUp := HandleMouseUp;
FCanvas.OnDblClick := HandleDoubleClick;

In addition to mouse event handlers, main form uses an array to “remember” which button was pressed.

    FSelecting: array [mbLeft..mbRight] of boolean;

HandleMouseDown is called when a mouse button is pressed. It simply sets the FSelecting element corresponding to the pressed button to True and remembers the current coordinates in FSelectStart. It also checks if both buttons are pressed and cancels drag operation. FCanvas.HideOverlay hides the rectangle/line painted by the drag operation. [I’ll cover actual painting in the next installment.]

procedure TFormMain.HandleMouseDown(Sender: TObject; button: TMouseButton; 
X, Y: integer);
begin
if button in [mbLeft, mbRight] then begin
FSelecting[button] := true;
if FSelecting[mbLeft] and FSelecting[mbRight] then begin
FSelecting[mbLeft] := false;
FSelecting[mbRight] := false;
FCanvas.HideOverlay;
Exit;
end;
FSelectStart := Point(X, Y);
end;
end;

When a mouse is moved, HandleMouseMove first checks if any drag operation is in progress. Then it checks whether the mouse has moved enough to consider this a real selection or a real move operation and hides the rectangle/line if that is not the case. Otherwise, it shows either the rectangle (ShowMarquee) or line (ShowArrow).

procedure TFormMain.HandleMouseMove(Sender:TObject; X, Y: integer);
var
rect: TRect;
begin
if FSelecting[mbLeft] or FSelecting[mbRight] then begin
rect := w3_Rect(FSelectStart.X, FSelectStart.Y, X, Y);
if (FSelecting[mbLeft] and (not IsConsideredSelection(rect))) or
(FSelecting[mbRight] and
(not IsConsideredMove(FSelectStart, Point(X, Y))))
then
FCanvas.HideOverlay
else if FSelecting[mbLeft] then
FCanvas.ShowMarquee(rect)
else if FSelecting[mbRight] then
FCanvas.ShowArrow(FSelectStart, Point(X, Y));
end;
end;

The reason behind the IsConsideredSelection/IsConsideredMove checks is that it’s hard not to move a mouse when double-clicking. Windows does the same – if you move the mouse just a little during a double-click, it will still be considered a double-click. If you move it more than some amount, it will be considered as a drag followed by a click.

Same goes for right-clicking. I don’t want right-click to do any line-painting if it is just a right-click, i.e. if mouse was not moved (or if it was moved just a little).

IsConsidered functions are quite simple. IsConsideredMove checks if mouse has moved more than 5 pixels in any direction (horizontal or vertical) and IsConsideredSelection checks if mouse has moved more than 5 pixels in both directions.

function TFormMain.IsConsideredMove(p1, p2: TPoint): boolean;
begin
Result := (Abs(p1.X - p2.X) > CDoubleClickDrift) or
(Abs(p1.Y - p2.Y) > CDoubleClickDrift);
end;

function TFormMain.IsConsideredSelection(rect: TRect): boolean;
begin
Result := (Abs(rect.Right - rect.Left) > CDoubleClickDrift) and
(Abs(rect.Bottom - rect.Top) > CDoubleClickDrift);
end;

When mouse button is released, HandleMouseUp hides the rectangle/line and either zooms in (left drag), moves the image (right drag) or zooms out (right click).

procedure TFormMain.HandleMouseUp(Sender: TObject; button: TMouseButton; 
X, Y: integer);
var
rect: TRect;
midX, midY: integer;
width2, height2: integer;
begin
if FSelecting[button] then begin
FSelecting[button] := false;
if button = mbLeft then begin
FCanvas.HideOverlay;
rect := w3_Rect(FSelectStart.X, FSelectStart.Y, X, Y);
if IsConsideredSelection(rect) then
Zoom(rect);
end;
if button = mbRight then begin
FCanvas.HideOverlay;
if IsConsideredMove(FSelectStart, Point(X, Y)) then
Move(FSelectStart.X - X, FSelectStart.Y - Y)
else
ZoomMid(
Point(FCanvas.Left + FCanvas.Width div 2,
FCanvas.Top + FCanvas.Height div 2),
CZoomFactor);
end;
end;
end;

The simplest of all, HandleDoubleClick, just zooms in around the point that was remembered in the OnMouseDown event.

procedure TFormMain.HandleDoubleClick(Sender: TObject);
begin
ZoomMid(FSelectStart, 1/CZoomFactor);
end;

And that’s all folks! At least as far as the actual mouse handling is in question. In the next installment I’ll cover painting improvements allowing the app to run correctly on the iPad and painting embelishments (rectangles and lines).

No comments:

Post a Comment