Friday, June 22, 2012

Tutorial 3 - A Simple Button Class (Updated 10/1/16)


Webcomic Courtesy of Ethanol & Entropy

3.0 Creating a Button in Codea


A control that you will use frequently is a button. Now that we have the foundations sorted we can punch out a simple button quite easily. Mostly because a sample button class is provided with Codea. There is a bit of cutting and pasting involved but we will go through each line of the Button() class so you can understand what is what.

Whip back to our old friend the Sounds Plus example project and copy all of the contents of the Button class tab. Back in your Menu project, create a new tab, call the Class Button and paste the code you just copied. Refer to Tutorial 2 if you have forgotten the exact steps.

Your Menu project should now contain 3 tabs: Main, RoundRect and Button. We will have a look at the Button class first. I have inserted a bunch of additional comments (in blue) so you can understand what is happening.

3.1 The Button Class


Button = class()

-- [[ There are no classes in standard Lua however they are handy concepts so Codea includes a global function called class() which provides equivalent functionality. You can read more about Codea classes in the wiki ]]

function Button: init(displayName)

-- [[ The Init function gets called before setup(). This is where you define and initialise your class and its member variables. The class variables are fairly self explanatory but for completeness: displayName: Is the text displayed on your button. The button will scale up and down to fit the text. pos: Defines the x and y - coordinates of the button using a vector. size: Is a vector which contains the width and height of the button, which is set by the display name text, and is used to determine if a button has been hit.  action: Is the function that you want called when the button is tapped. color: Is the color of the button fill. ]]

    -- you can accept and set parameters here

    self.displayName = displayName
    
    self.pos = vec2(0,0)
    self.size = vec2(0,0)
    self.action = nil
    self.color = color(113, 66, 190, 255)

end

function Button:draw()

-- [[ Your main code needs to explicitly call this function to draw the button, it won't happen automatically. We will see how this works when we update the main() class. ]]

    -- Codea does not automatically call this method

    pushStyle()

-- [[ pushStyle() saves the current graphic styles like stroke, width, etc. You can then do your thing and call popStyle at the end to return to this state.]]

    fill(self.color)

-- [[ fill is used initially to set the colour of the button, then the font type and size is set. You could change this in your implementation of the button class if you wish. Click here to see the available fonts. ]]
    
    font("ArialRoundedMTBold")
    fontSize(22)
    
    -- use display name for size

    local w,h = textSize(self.displayName)
    w = w + 20
    h = h + 30
    
-- [[ As stated in the code, displayName is used to size the button and then we use the class we looked at in Tutorial 2 to draw a rounded rectangle. ]]

    roundRect(self.pos.x - w/2,
              self.pos.y - h/2,
              w,h,30)
            
    self.size = vec2(w,h)

-- [[ Note that class variables are designated using the self keyword. e.g. self.size. The next block of code sets the colour of the button text and its position on the button. ]]
            
    textMode(CENTER)
    fill(54, 65, 96, 255)
    text(self.displayName,self.pos.x+2,self.pos.y-2)
    fill(255, 255, 255, 255)
    text(self.displayName,self.pos.x,self.pos.y)
    
-- [[ Return the graphic style to what it was before you entered this function. This is considered polite behaviour for a function because it can be hard to track down if the style is being changed deep within some function and you don't want it to. ]]

    popStyle()

end

function Button:hit(p)

-- [[ This function works out if the last touch (after you lift your finger) was on this button, using the size and pos variables. Returns true if it was and false if it wasn't.  The local keyword defines a local variable. Unlike global variables, local variables have their scope limited to the block where they are declared. A block is the body of a control structure, the body of a function, or a chunk (the file or string with the code where the variable is declared). ]]

    local l = self.pos.x - self.size.x/2
    local r = self.pos.x + self.size.x/2
    local t = self.pos.y + self.size.y/2
    local b = self.pos.y - self.size.y/2

    if p.x > l and p.x < r and
       p.y > b and p.y < t then
        return true
    end
    
    return false
end

function Button:touched(touch)

    -- Codea does not automatically call this method

-- [[ As with the draw() function the touched function is also not called automatically by your code. If you don't call this then you won't know if someone has tapped your button. It reminds me of the old joke, "what do you call a boomerang that doesn't come back?" ..."A stick!" The test, if self.action checks whether you have defined a function to call when the button is tapped. If self.action is nil then nothing will happen.]]

    if touch.state == ENDED and
       self:hit(vec2(touch.x,touch.y)) then
        if self.action then
            self.action()
        end
    end
end


3.2 The Main Class


Now that you are an expert on the Button Class, we can have a look at what is required in your Main Class to instantiate and use a button. It is fairly simple.

-- Use this function to perform your initial setup

function setup()

    print("Button Test Project")
    
-- [[ Create a new button, it wont be visible until you draw it. The init of the button will also set the displayName. You can change this later if you wish by changing the string assigned to button.displayName. The action variable is assigned the function you want to call when the button is tapped. We haven't attempted to be too ambitious with this first attempt.]]
    
    button = Button("Press Me")
    button.action = function() buttonPressed() end
    
end

-- This function gets called once every frame

function draw()

    -- This sets a dark background color 

    background(40, 40, 50)

    -- Do your drawing here, drawButton is defined below.
    
    drawButton()
    
end

function drawButton()

-- [[ Draw the button at some arbitrary spot on the screen and then call the buttons draw() function. You MUST include this step within the Main draw() function. ]]

    button.pos = vec2(400, HEIGHT/2)
    button: draw()

end

function buttonPressed()
    
-- [[ This is where the action happens. Whenever the button is tapped, this function will be called. You can call it whatever you want but it must match the function that you assign to the button.action variable. We aren't doing anything too exciting here but it should illustrate the point. ]]

    print("Button Pressed")
    
end

function touched(touch)

-- [[ Like the button draw() function this is another one that you MUST call for the button to work. It passes the touch detected in the main class to the button class to see if it needs to do anything with it. If the button detects a hit then the action function gets called. ]]
      
    button:touched(touch)
     
end
   
You can download a copy of the files from here.
   

3.3 An Alternative Approach


You now know how to implement a button and assign an event handler for when it gets tapped.

There are a number of other approaches that you can take to solve this problem. Over on the Codea forum Bri_G, Maxiking16 and Reldonas have all contributed sample code to help make your buttons look even sexier. 

3.4 Other Alternatives (Mesh or Sprites)


Vega has come up with a button class which uses meshes to generate the buttons. This class includes buttons in the Apple style, Windows style and customised buttons. And ChrisF has come up with another approach which uses sprites.

19 comments:

  1. When I type the code I get an error in Main setup.

    I edited button.action by removing the word function().

    The program now runs but nothing seems to happen. Button Pressed shows up before I press the button. I edited button touched and added an else to print "touch". touch is now displayed each time I press the button.

    Did I miss something?

    ReplyDelete
    Replies
    1. Hi Bruce,

      It could be a few things. Codea doesn't handle Lua multiline comments very well. The ones which look like --[[ ]].

      I suggest you don't use these and replace them with single line comments (-- which just start with two dashes on each line). You will notice in later tutorials that I have stopped using them.

      Did you include the roundRect() function from Tutorial 2? It wont work without this.

      If you can't find the problem then you can download the file from here: https://www.dropbox.com/s/offa9e57szv3pf5/Tutorial_3_Simple_Button.lua

      If you have any other questions let me know.

      Delete
    2. Same problem here. It's an error in the text above. Change all references to 'button' in the Main code to 'Button' (note the capital 'B').

      Delete
    3. Hi Anonymous,

      There is a difference between the Button class (with capital) and the button instance of that class (lower case). This is a common pattern. If you use Button as the class instance it may work but you will only be able to have one button in your program which is probably not what you want.

      Regards,

      D

      Delete
  2. I'm having some trouble making a button work in my project
    I've looked through the program several times the button is supposed to move a sprite right on the screen and it won't work heres right now im just trying to get the button to work so instaed of moving the sprite (which i havent made yet) it should just print "right button pushed" in the output instead im getting errors. can some one tell me what im doing wrong. Thanks in advance!!:):
    ButtonRight = class()

    function ButtonRight:init(displayName)
    -- you can accept and set parameters here
    self.displayName = "Right" -- it didnt work when i had this set to displayName
    --instead in line 36 the output says says "bad argument #1 to 'text' (string expected, got nil)
    --it said it was an error in my Button Right class
    --this happened even when i changed it to "Right" which is a string right?
    --i put a comment a line 36 to help you find it without counting all the lines
    self.pos = vec2(0,0)
    self.size = vec2(0,0)
    self.action =function (actionright)
    print("Right button pressed")
    end
    self.color = color(113, 66, 190, 255)
    end
    function ButtonRight:draw()
    -- Codea does not automatically call this method
    pushStyle()
    fill(127, 127, 127, 255)

    font("ArialRoundedMTBold")
    fontSize(22)

    -- use longest sound name for size
    local w,h = textSize(self.displayName)
    w = w + 20
    h = h + 30

    roundRect(self.pos.x - w/2,
    self.pos.y - h/2,
    w,h,30)

    self.size = vec2(w,h)

    textMode(CENTER)
    fill(54, 65, 96, 255)
    text(self.displayName,self.pos.x+2, self.pos.y-2)--36
    fill(255, 255, 255, 255)
    text(self.displayName,self.pos.x+2, self.pos.y-2)

    popStyle()
    end

    function ButtonRight:hit(p)
    local l = self.pos.x - self.size.x/2
    local r = self.pos.x + self.size.x/2
    local t = self.pos.y + self.size.y/2
    local b = self.pos.y - self.size.y/2
    if p.x > l and p.x < r and
    p.y > b and p.y < t then
    return true
    end

    return false
    end

    function ButtonRight:touched(touch)
    -- Codea does not automatically call this method
    if touch.state == ENDED and
    self:hit(vec2(touch.x,touch.y)) then
    if self.action then
    self.action()
    end
    end
    end

    And here's my main if you need it:

    supportedOrientations(LANDSCAPE_ANY)
    function setup()
    print("hold the device in LANDSCAPE mode!!!!")
    parameter("posx",1,700)
    parameter("posy",1,700)

    end
    -- This function gets called once every frame
    function draw()
    -- This sets a dark background color
    background(40, 40, 50)
    sprite("Documents:background",376.44,385.94,HEIGHT/1,WIDTH/.93)
    strokeWidth(5)
    drawButtonRight()
    drawButtonLeft()
    -- Do your drawing here

    end
    function setup(Buttons)
    print("push the right button to move your ship right!")
    print("push the left button to move you ship left!")
    button=ButtonRight(Right)
    button=ButtonLeft(Left)
    ButtonRight.action=function()ButtonRightPressed()end
    end
    function drawButtonRight()
    ButtonRight.pos=vec2(posx,posy)
    ButtonRight:draw()
    end
    function ButtonRightPressed()
    print("Right button pressed")
    end
    function touched(touch)
    ButtonRight:touched(touch)
    ButtonLeft:touched(touch)
    end
    these tutorial are VERY helpful im am a tenth grader trying to learn how to program
    this is probably like the longest comment of all time!!

    ReplyDelete
    Replies
    1. Hi Anonymous,

      Great to hear that the tutes are useful and well done on teaching yourself to program. You wont regret it. And yes congratulations on winning the longest comment award. You are on the right track but there are a few problems with your code. Here is what I suggest:

      1. There is no need for you to modify the Button class. The point of having a class is that you can make lots of the same object from this class. The problem with having a ButtonRight class is that you will then need a ButtonLeft class, ButtonUp class, etc. So stick with a base Button class - you can download just the Button class code from: https://www.dropbox.com/s/5ckr3u7hs2y0x34/Button.lua

      You mix objects and classes up in your code, but this isn't your main problem.

      2. In your Main tab you need to define your button object (which is an "instance" of your Button class - don't worry about the terminology too much at this stage). The convention is to start a class name with a capital and objects with a lower case letter. You are doing this in the function setup(Buttons) - which NEVER gets called. Take this code and put it in the setup() function which is called automatically by Codea when the program starts. As per point 1. I would change your code so that it reads:

      buttonRight = Button("Right")
      buttonRight.action=function()ButtonRightPressed()end
      buttonLeft = Button("Left")
      buttonLeft.action=function()ButtonLeftPressed()end

      You will need to add in the ButtonLeftPressed function obviously.

      3. In your drawing and touch functions, make sure that you refer to the object not the base class (e.g. ButtonRight: draw() should be buttonRight:draw(), ButtonLeft:touched(touch) should be buttonLeft:touched(touch).

      4. Finally if you want to see how it is done, skip ahead to Interlude 9 - moving a sprite with buttons (http://codeatuts.blogspot.com.au/2012/07/interlude-9-control-object-movement.html) or Tutorial 8 - A simple D-Pad class (http://codeatuts.blogspot.com.au/2012/07/tutorial-8-directional-pad-dpad-class.html). I would suggest that you try and get your code working first, you will learn more that way.

      Good luck and feel free to ask more questions if you get stuck.

      Cheers,

      David

      Delete
    2. Sorry one last point...

      5. This button class needs the roundRect() function from Tutorial 2. It wont work without this.

      Delete
    3. P.S. You were getting the error "bad argument #1 to 'text' (string expected, got nil) because displayName was never set and hence is nil. You need to pass the display name when you create the object and yes it should be a string. e.g.

      buttonRight = Button("Right")

      Delete
    4. im trying to make a game like the bit invader example but with button control and enemies that shoot and go after you

      Delete
    5. Sounds like fun - I look forward to playing it.

      Delete
  3. I'm having some trouble again I got the right and left buttons to do what I want and decided to add a fire button to I added it by copying the right and left buttons exactly except for the location I didnt get any error messages when I ran the program the fire button never came up and the left button stopped working however the right button continued to work perfectly. I am using the single button class from the link you gave me and got the RoundRect from the soundsplus example. heres what i have:
    --spaceteroids
    --A game were you shoot down asteroids and aliens before they destroy you
    -- Use this function to perform your initial setup
    supportedOrientations(LANDSCAPE_ANY)
    function setup()
    print("hold the device in LANDSCAPE mode!!!!")
    iparameter("posx",1,700)
    iparameter("posy",1,700)
    buttonRight = Button("Right")
    buttonRight.action=function()ButtonRightPressed()end
    buttonLeft = Button("Left")
    buttonLeft.action=function()ButtonLeftPressed()end
    buttonFire = Button("Fire")
    buttonFire.action =function()ButtonFirePressed()end

    end
    -- This function gets called once every frame
    function draw()
    -- This sets a dark background color
    background(40, 40, 50)
    sprite("Documents:background",376.44,385.94,HEIGHT/1,WIDTH/.93)
    strokeWidth(5)
    drawButtonRight()
    drawButtonLeft()
    drawButtonFire()
    -- Do your drawing here

    end

    function drawButtonRight()
    buttonRight.pos=vec2(700,40)
    buttonRight:draw()
    end
    function drawButtonLeft()
    buttonLeft.pos=vec2(600,40)
    buttonLeft:draw()
    end
    function drawButtonFire()
    buttonLeft.pos=vec2(500,500)
    buttonFire:draw()
    end

    function ButtonRightPressed()
    print("Right button pressed")
    end
    function ButtonLeftPressed()
    print("Left button pressed")
    end
    function ButtonFirePressed()
    print("Fire button pressed")
    end
    function touched(touch)
    buttonRight:touched(touch)
    buttonLeft:touched(touch)
    buttonFire:touched(touch)
    end

    ReplyDelete
  4. found my mistake :)

    ReplyDelete
    Replies
    1. Excellent - BTW if you arent planning on moving your buttons I would initialise their position (e.g. buttonLeft.pos=vec2(500,500)) in setup() not draw(). draw() gets called up to 60 times per second so you want to minimise the amount of code in that function.

      Delete
    2. ok thanks, I will do that

      Delete
  5. David, I graduated from Tut. 2, Thanks.
    My Main is what you have:

    function setup()
    print("Button Test Project")
    button = Button("Press Me")
    button.action = function() buttonPressed() end
    end

    function draw()
    background(40, 40, 50)
    drawButton()
    end

    function drawButton()
    button.pos = vec2(400, HEIGHT/2)
    button: draw()
    end

    function buttonPressed()
    print("Button Pressed")
    end

    function touched(touch)
    button:touched(touch)
    end

    This program draws a purple button with white text. When I touch the button, the "Button Pressed" text print.

    1. Is this supposed to be the behavior/purpose of the tutorial?
    2. Please explain further the Button fuction Button:touched(touch). How does it work? Hows does the 2nd if work? if self.action then self.action?
    3. I don't know why in Main function touched (touch) works or how. Could you further explain?


    Thanks, Jose.

    ReplyDelete
    Replies
    1. Hi Jose,

      1. Yes.
      2. and 3. Let me answer these together since they are related. In the Main tab, the function touched(touch) gets called automatically by Codea whenever a user taps the screen. When this happens we pass the touch object to the touched function in our Button class.

      In the Button class, the first "if" checks if the touch was on the button using the hit() function and the second "if" calls the function that you have assigned to the button "action" variable, if it has been defined.

      In Lua, you can assign a function to a variable and we use this to provide a call back function for the button. When you define the button, one of the lines you will see is:

      button.action = function() buttonPressed() end

      This assigns the function buttonPressed() to the variable action.

      In the button touched function we test whether action exists (if it is nil then it hasn't been defined), and if it does we call that function every time the button is tapped. The function buttonPressed() just prints out a message to the console in this simple example.

      function buttonPressed()
      print("Button Pressed")
      end

      You can read about what gets returned in the touch object here: http://twolivesleft.com/Codea/Reference/#detail/index/touch

      I hope that makes sense.

      Regards,

      David

      Delete
    2. David, thanks again. I have several questions.
      Main> button.action = function() buttonPressed() end
      Main> function touched(touch) button:touched(touch) end.
      Button> Button = class()... function Button: ...
      Button> function Button:hit(p)local l = self.pos.x ...
      Button> function Button:touched(touch)
      if touch.state == ENDED and self.hit(vec2(touch.x,touch.y) ...

      1. button.action = function() buttonPressed() end
      Why does this function have () at the end?
      2. In the Button class, must all subsequent function declarations use "Button"?
      3. self.hit(vec2(touch.x, touch.y) invokes function Button:hit(p). Correct?
      3a. How are touch.x, touch.y transferred to Button:hit(p)?
      3b. When/where is the first time in the code that touch.x, touch.y acquire a value?
      3c. How can self:hit(vec2(touch.x,touch.y))have a vector parameter when it is not defined in function Button:hit(p)?
      3d. Button class variables self.pos and self.size ARE defined as vectors. But how can a function in the Button class Button:hit(p)use these values if it is not defined in the function?

      Thanks ... a little convoluted, but I hope you can help!
      Thanks again, Jose.

      Delete
    3. Jose,

      1. That is how you define a function in Lua and assign it to a variable. I'm not sure I can explain it any clearer. That is the correct syntax. Have a look at this, http://lua-users.org/wiki/FunctionsTutorial, and see if that makes it clearer.

      2. If you want them associated with the Button class they do.

      3. Yes - in this case self = Button.

      3a. Inside the touched function of the button, hit gets called with touch.x and touch.y passed as a vector referred to as p in the hit function. I reused hit from some other code, it doesn't have to be a vector that's just the way it was coded.

      3b. Whenever someone taps the screen, touch gets populated and the main touched function is called. Did you read the specs on touch?

      3c. Lua is a dynamically typed language. This means that variables do not have types; only values do. There are no type definitions in the language. All values carry their own type.

      3d. See answer to 3c.

      It may be worth you spending some time reading the Lua reference manual. It is available here: http://www.lua.org/manual/5.1/manual.html

      Cheers,

      D

      Delete