Realbasic: Canvas Tutorial Lesson 4

Now it is time to add some interactivity to our canvas. We want our users to be able to drag and drop the comic covers around and organize them as they want.

Let’s start with some properties we’ll need on the ABCanvas:

mLastX as integer ' our last X position
mLastY as integer ' our last Y position
DragElem as ABelement ' the element we're dragging

We are going to use some of the Canvas events to do the mouse handeling, but we still want those events to be available later so we have to declare them:

Event MouseDown(x as integer, y as integer) As boolean
Sub ()

Event MouseDrag(x as integer, y as integer)
Sub ()

Event MouseUp(X as integer, Y as integer)
Sub ()

Event MouseMove(X as integer, Y as integer)
Sub ()

Next we need some functions on the ABCanvas to refresh an ABElement, to find an ABElement and to bring this element to the front.

RefreshElement(): this function will refresh only the given element:

Sub RefreshElement(tmpElem as ABElement)
  Dim iLeft, iTop as Integer
  
  ' get the real left and right from our center x and y
  iLeft = tmpElem.x - tmpElem.w \ 2 
  iTop = tmpElem.y - tmpElem.h \ 2
  	
  ' redraw only me and all other ABElements we are covering
  DrawMe iLeft,iTop,tmpElem.w,tmpElem.h 
End Sub

In Tutorial 3 we learned how to find a mousedown in a irriguar shape. For this tutorial we’ll going to use only part of this method in ElementHit(). As all our objects are rectangles and we’ll know their positions we are going to find them the conventional way. To check if we’re in the shadow, we’ll use the method from tutorial 3.

Function ElementHit(x as integer, y as integer) As ABElement
  ' Find the ABelement hit by the point x,y (if any).
  Dim tmpElem As ABElement
  Dim i, halfw, halfh As Integer
  
  for i = UBound(MyElements) downTo 0
    tmpElem = MyElements(i)
    if tmpElem.Visible then
      ' all ABElements x and y positions are in the center, so we take half the width and height to get the left and top
      halfw = tmpElem.w / 2
      halfh = tmpElem.h / 2
      ' Are we within the bounds of this picture?
      if Abs(x - tmpElem.x) < halfw and Abs(y - tmpElem.y) < halfh then
        ' yes we are, but we want it only if our mask is not transparent
        if ComicMask.Graphics.Pixel(Abs(x - tmpElem.x + halfw), Abs(y - tmpElem.y + halfh)) = &c000000 then
          return tmpElem
        end if
      end if
    end if
  next
  
  ' nothing found so we return nil
  return nil
End Function

And then a function to bring the ABElement to the front if we click on it:

Sub BringToFront(FrontElem as ABelement)
  #pragma disableBackgroundTasks
  
  ' Bring the given ABElement to the front, so it's drawn on top of all others.
  Dim i As Integer
  Dim j as integer
  Dim maxi as integer
  
  maxi = UBound(MyElements)
  
  ' in reverse order
  for i = maxi downTo 0
    if MyElements(i) = FrontElem then
      ' Found! Remove it from the list
      MyElements.Remove i
      ' and add it at the end
      MyElements.Append FrontElem
      Return
    end if
  next
End Sub

Now we’re ready to add some interactivity to our canvas! Let’s start with changing our mouse cursor if we are above a comic cover. We’ll use the MouseMove() event of the canvas for this

Sub MouseMove(X As Integer, Y As Integer)
  ' if we are above one of our Elements, we change the mouse cursor to a little hand
  if ElementHit(X,Y) <> nil then
    self.MouseCursor = System.Cursors.FingerPointer
  else
    self.MouseCursor = System.Cursors.StandardPointer
  end if
  
  ' continue with the default MouseMove event
  MouseMove X,Y
End Sub

Done! Let’s grab the cover and drag it a little around.
We start in the MouseDown() event of the canvas. We’ll search if we have hit an ABElement. If so we’ll remember our position, bring it to the front and set the DragElem to the found element:

Function MouseDown(X As Integer, Y As Integer) As Boolean
  Dim tmpElem as ABElement
  tmpElem = ElementHit(X,Y)
  if tmpElem <> nil then
    ' remember our current position
    mLastX = X
    mLastY = Y
    
    ' bring the found ABelement to the front
    BringToFront tmpElem
    
    ' refresh the element so it is redrawn 
    RefreshElement tmpElem
    
    ' remember this ABElement so we can drag it around
    DragElem = tmpElem
    Return true
  end if
  
  ' continue with the default MouseDown event
  return MouseDown(X,Y)
End Function

In the MouseDrag() event we’ll redraw our ABElement to the position of the cursor. Here a couple of things that need to happen:
– If our current position = our previous position, don’t redraw
– We add some extra pixels around the object to prevent ‘trailing’
– We calculate our new position
– As we draw our element to its new position, we also have to redraw the background and other objects on the old position.
– We remember our current position for the next pass

Sub MouseDrag(X As Integer, Y As Integer)
  #pragma disableBackgroundTasks
  
  if DragElem <> nil then
    if mLastX = X and mLastY = Y then
      ' no need to redraw, it is the same position as before
      Return
    end if
    
    Dim oldX, oldY, newX, newY, fullLeft, fullTop, fullWidth, fullHeight as integer
    
    Dim Extra as integer
    ' we add some extra pixels around the object when we redraw because otherwise we sometimes ge a 'trail'
    Extra = 5
    
    ' we remember the old position
    oldX = DragElem.X
    oldY = DragElem.Y
    
    ' calculate the new position
    newX = oldX + X - mLastX
    newY = oldY + Y - mLastY
    
    ' Find the union between the old and the new position
    fullLeft = Min(oldX,newX) - DragElem.w \ 2 - Extra
    fullTop = Min(oldY,newY) - DragElem.h \ 2 - Extra
    fullWidth = Max(oldX,newX) - DragElem.w \ 2 + DragElem.w - fullLeft + Extra * 2
    fullHeight = Max(oldY,newY) - DragElem.h \ 2 + DragElem.h - fullTop + Extra * 2
    
    ' set our new position
    DragElem.X = newX
    DragElem.Y = newY
    
    ' redraw only what is needed
    DrawMe fullLeft, fullTop, fullWidth, fullHeight
    
    ' remember the last mouse position
    mLastX = X
    mLastY = Y
  end if
  
  ' continue with the default MouseDrag event
  MouseDrag X,Y
End Sub

And last we handle the mouse up. Just to be sure, we do a last redraw of our element and we set the DragElem back to nil.

Sub MouseUp(X As Integer, Y As Integer)
  if DragElem <> nil then
    ' refresh one last time so we have the very last position
    RefreshElement DragElem
    ' set our DragElem to nothing
    DragElem = Nil
  end if
  
  ' continue with the default MouseUp event
  MouseUp X,Y
End Sub

Ok, Let us run our program! We should be able to see by the mouse cursor if we’re above an Element. If we click on it, it will jump to the front and we can drag it around. A lot of stuff happended in this tutorial so if you have questions or suggestions, please leave a message.

Here is a small video to show what we made:

In our next lesson we’ll expand a little what we have learned so far so that we can play around with multiple types of objects.

Source code for this tutorial: http://www.gorgeousapps.com/Tut4.zip

Happy programming!

Click here to Donation if you like my work

Advertisements

About Alwaysbusy

My name is Alain Bailleul and I'm the Senior Software Architect/Engineer at One-Two. I like to experiment with new technologies, Computer Vision and A.I. My projects are programmed in B4X , Xojo, C#, java, HTML, CSS and JavaScript. View all posts by Alwaysbusy

16 responses to “Realbasic: Canvas Tutorial Lesson 4

  • Mathias Maes

    Hello,

    Why do you use your own implementation of hittest, and not the build-in REALbasic.Rect.Contains() function? Is there a particular reason?

    • Alwaysbusy

      Hi Mathias,

      The main reason is I mostly use RB2007R4 for graphics programming on Windows and the Rect class did not yet exist in this version.

      So absolutly no reason not to use it if you have it available in your RB version.

  • Charles Fasano

    I tried removing the ComicMask and Glow as I don’t need it but now I am getting a “trail”

    • Charles Fasano

      Apparently, if you don’t have a background image you get the “trail”

      By adding a background image, it fixes the “trail”.

  • Charles Fasano

    I am a little confused on how the code knows which element you are hovering over. In my app, I have 50 60×88 rectangles and I need to know which one the mouse is hovering over so I can display some stuff about the picture.

    I know that an Array is used but I am not quite sure how it knows which element in the array the mouse is hovering over.

    • Alwaysbusy

      In the MouseMove() event, the function ElementHit() is called. It checks if the current x,y is in the bounded box of the element.

      if Abs(x – tmpElem.x) < halfw and Abs(y – tmpElem.y) < halfh then

      If yes, it's returned and you know exactly the element it is over.

      • Charles Fasano

        I’m still a little confused on how to know what rectangle we are in outside of the ElementHit function.

        While I am not having the rectangle dragged anywhere I want stuff to happen depending on what rectangle is being clicked on or hovered over (after a certain amount of time has passed.)

        Sorry for the 20 questions but I am new to this part of the Canvas control. I used to do it where each rectangle had its own canvas control and the 50 Canvases were in an array.

      • Alwaysbusy

        I’m not really sure what you mean. Once you get the element from the ElementHit() function, you know everything about the rectangle and can act accordingly. You have it’s center point, it’s width and it’s height. So the left = centerX – width/2 and the top = centerY – height/2. In tutorial 7, I’ll go even deeper into this subject with a menu that pops up when you click on an element.

      • Charles Fasano

        What I want to happen is when the cursor hovers over an Element a timer stars and goes for 2 seconds and then fires a subroutine only if the cursor is still on the same Element. If the cursor moves to a different Element, the time resets and goes for 2 seconds. If the cursor is no longer over an Element, the timer stops.

  • Alwaysbusy

    I’m not on my pc right now, so this is from the top of my head 🙂

    Make a new property LastElement as Element on the form. In the mousemove event, use ElementHit.

    something like:

    elem = ElementHit(X,Y)
    if elem = nil then
    lastelem = nil
    timer.enabled = false
    elseif lastelem = nil then
    lastelem = elem
    timer1.enabled = true
    elseif elem lastelem then ‘ or check the ID’s
    lastelem = elem
    timer.enabled = false
    timer.enabled = true ‘ should reset the timer I think
    end if

    when the timer ticks, the 2 seconds will have past and the lastelem will be the one it was over for 2 seconds.

    • Charles Fasano

      Which MouseMove event should I add this to, Form.MouseMove or Form.ABCanvase.MouseMove?

    • Charles Fasano

      I put the code in the Form.MouseMove Event and I get an error saying that there is no item called ElementHit. I even changed it to ABCanvas.ElementHit(X,Y) but no go. When I hover over ABCanvas.ElementHit(X,Y), it shows that it exists but I guess it can’t be used as part of an Equation.

      Also you have in your code:

      elseif elem lastelem then

      which is no good.

      Did you mean elem = lastelem?

      Thanks for the help.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: