User Tools

Site Tools


tutorials:cpp_cinder:getting_started_2_hello_multitouch

Getting Started II (Hello Multitouch)

Introduction

In this tutorial, you will be creating a simple application to display touchpoint data on the screen, using the C++ bindings to connect to the GestureWorks Core DLL. You will learn how to load and initialize GestureWorks, as well as how to receive basic touchpoint data. For this tutorial you will need the GestureWorks Core multitouch framework; a free trial is available.

Download the code for all of the C++ & Cinder multitouch tutorials here: tutorials_cpp_cinder.zip


Requirements

Estimated time to complete: 1 hour

  • Working knowledge of C++
  • GestureWorks Core license and SDK files
  • Microsoft Windows 7 or 8
  • Microsoft Visual Studio (2010 or 2012, Express or Professional)
  • A copy of Cinder (see http://libcinder.org/ for documentation and downloads)
  • Multitouch display device
  • Visual Studio solution configured to work with GestureWorks Core (see C++ & Cinder: Getting Started I (Hello World)) and Cinder

Process Overview

    • Configuration
    • Connecting to the GestureworksCore32.dll or GestureworksCore64.dll
    • Registering the Display Window
    • Handling Frame Rate
    • Processing Frames
    • Consuming touchpoint data
    • Displaying Touch Points

Process Detail

1. Why Cinder?

Cinder is one of the most easy-to-use frameworks for getting started with interactive multimedia applications. Additionally, it has broad community support, and comes with example programs that concisely demonstrate each of its features. For the purposes of our tutorials, Cinder provides a quick, clean way to write a graphical application for integration with GestureWorks.

2. Setup

If you have gone through the first tutorial, you should be familiar with the process for configuring a Visual Studio solution to make use of GestureWorks Core. In addition, you will need to set up your solution to use Cinder.

In short, the .cpp and .h files for GestureWorks Core and GWCUtils that were included in your installation package need to be added to the source tree for your project, and the directory containing these files needs to be added to VC++ Directories in your project settings.

For this tutorial, you will also need to install Cinder, and add it to your project. The easiest way to generate a new Cinder project is to use the project generator (tools/TinderBox-WIn/TinderBox.exe) that comes with the Cinder download package. GestureWorks is much easier to setup than Cinder, so first create a new Cinder project then add the GestureWorks libraries according to the previous tutorial. Call the new project HelloMultitouch.

Our code is split out the code into .cpp and .h files but TinderBox.exe will create the project with the two files combined. TinderBox.exe appends the word App onto all of the project’s main class files, so the file to look at will be HelloMultitouchApp.cpp.

First we’ll need the following include statements and namespace declarations (Note: GestureWorks.h):

HelloMultitouchApp.h

#include "cinder/app/AppNative.h"
#include "cinder/gl/gl.h"
#include "cinder/gl/TextureFont.h"

using namespace ci;
using namespace ci::app;

HelloMultitouch.cpp

#include "HelloMultitouchApp.h"
#include "GestureWorks.h"
#include "cinder/Rect.h"

Cinder applications all derive from the base class AppNative, so our application needs to inherit from this class. This will be stubbed out for you by the project generator. To the HelloMultitouchApp class definition, add the Cinder prepareSettings function, a Cinder Font variable, and an OpenGL font texture. This is specific to Cinder, so if you need help understanding the code below, then consult with the Cinder documentation.

HelloMultitouchApp.h

class HelloMultitouchApp : public AppNative {
public:
    Font font;
    gl::TextureFontRef texture_font;
    int screen_width;
    int screen_height;
    bool use_pixels;

    std::pair<int, int> normScreenToWindowPx(float screen_x, float screen_y);
    void prepareSettings(Settings *settings);
    void setup();
    void mouseDown(MouseEvent event);
    void update();
    void draw();
};

Define the prepareSettings function using the Cinder Settings object. This code is specific to Cinder, but is going to be very important for GestureWorks. The coordinate system GestureWorks references is that of the touch monitor. It knows nothing about your application’s window or it’s coordinates, so you’ll need to first set up your window and then later (in the next function) relay that information to GestureWorks. You’ll want to set the frame rate of the application here as well, because we’ll use that to update the GestureWorks processing engine.

HelloMultitouchApp.cpp

void HelloMultitouchApp::prepareSettings(Settings *settings) {
    settings->setWindowSize(1024, 768);
    settings->setTitle("Hello Multitouch!");
    settings->setFrameRate(60.0f);

    screen_width = GetSystemMetrics(SM_CXSCREEN);
    screen_height = GetSystemMetrics(SM_CYSCREEN);
}

Next, create a global variable called active_points of type std::map. We’ll use this to store active touch point x, y coordinates through the GestureWorks gwc::Point object.

HelloMultitouchApp.h

std::map<int, gwc::Point> active_points;

Define the setup function. The setup function is called once by the Cinder framework. Among other things, we’ll utilize it to set up the Gesureworks Core. First though, we’ll instantiate some variables we have already declared.

HelloMultitouchApp.cpp

void HelloMultitouchApp::setup()
{
    // create font
    font = Font("Arial", 22);
    texture_font = gl::TextureFont::create(font);

    active_points = std::map<int, gwc::Point>();

Expand on the setup function by setting up GestureWorks and linking it to your application. These are necessary functions for all C++ GestureWorks applications:

  LoadGestureWorks
  InitializeGestureWorks
  RegisterWindowForTouchByName (alternative function exists)

You can see these functions in the code below:

HelloMultitouchApp.cpp

void HelloMultitouchApp::setup()
{
    // create font
    font = Font("Arial", 22);
    texture_font = gl::TextureFont::create(font);

    active_points = std::map<int, gwc::Point>();

    if (!GestureWorks::Instance()->LoadGestureWorks(L"GestureworksCore32.dll")) {
        console() << "Error loading gestureworks dll" << std::endl;
    }

    GestureWorks::Instance()->InitializeGestureWorks(0, 0);

    if (!GestureWorks::Instance()->LoadGML(L"basic_manipulation.gml")) {
        console() << "Could not find gml file" << std::endl;
    }

    if (!GestureWorks::Instance()->RegisterWindowForTouchByName(L"Hello Multitouch!")) {
        console() << "Could not register target window for touch." << std::endl;
    }

    use_pixels = true;
    GestureWorks::Instance()->SetUsePixels(use_pixels);
}

The LoadGestureworks function must be given a string path to the GestureWorks DLL. It can be given a relative or absolute path. This will load the DLL and import all of the functions required to interact with it through the bindings. It is critical that this is the first function called from the bindings. If any other functions are called before this one, your program will likely suffer an access violation exception. If the call succeeds, it will return 0. If the call fails, it will return an error code to indicate why it failed.

It is critical at this point that you have loaded the DLL that corresponds to your architecture. GestureWorks comes with both 32- and 64-bit DLLs, and your compiled project must load the DLL whose architecture matches your target architecture. If you attempt to load a 64-bit DLL into a 32-bit program, or vice versa, the function will fail. If you are unsure which is your target architecture, you can check under Project Properties → Platform.

The LoadGML function must be given a string path to the desired GML file. This will cause GestureWorks to load and parse a GML file of your choosing. The call will return true if it succeeds, and false if it cannot find the GML file specified.

The InitializeGestureWorks function initializes the core. If 0 is passed for both parameters, GestureWorks Core will initialize based on the screen width and height. This is good for most cases.

The RegisterWindowForTouchByName function takes the name of the application window and returns true if it successfully connects to it. For precision, we could also call RegisterWindowForTouch – which accepts a handle to the window – but acquiring the window handle is outside the scope of this tutorial. This is the last function that needs to be called before processing events.

The SetUsePixels function is option but will cause ConsumePointEvents and ConsumeGestureEvents to return event location data in pixels instead of relative screen coordinates.

3. The Update Function

The update function is called by the Cinder framework once per cycle prior to the draw function. It is here that we will cue GestureWorks to process input, and consume any events it generates.

Sync the GestureWorks processing engine to the update loop of the Cinder framework. Remember that you already set the application’s frame rate to 60 fps. We can make GestureWorks update at the same rate by placing the GestureWorks ProcessFrame function in the Cinder update function. This function tells GestureWorks to take all touch input received since the last time we called the function, process it, and queue up any events generated with the input. This process keeps the GestureWorks core engine moving along and is necessary for meaningful data to be returned in the form of PointEvents.

HelloMultitouchApp.cpp

void HelloMultitouchApp::update()
{
    GestureWorks::Instance()->ProcessFrame();

Any touchpoint events generated from our call to processFrame can then be acquired by calling ConsumePointEvents:

HelloMultitouchApp.cpp

void HelloMultitouchApp::update()
{
    GestureWorks::Instance()->ProcessFrame();
    std::vector<gwc::PointEvent> point_events = GestureWorks::Instance()->ConsumePointEvents();

    for (std::vector<gwc::PointEvent>::iterator event_it = point_events.begin(); event_it != point_events.end(); event_it++) {

        std::pair<int, int> pair = normScreenToWindowPx(event_it->position.x, event_it->position.y);

        switch (event_it->status) {
        case gwc::TOUCHADDED:
        case gwc::TOUCHUPDATE:
            //If the point is being added, this will place it in our point map;
            //the same line of code will update the point's position if it's
            //already present, so we can use this command to handle new points
            //as well as point updates
            active_points[event_it->point_id] = gwc::Point(pair.first, pair.second);
            break;
        case gwc::TOUCHREMOVED:
            //Remove the point from the map
            active_points.erase(event_it->point_id);
            break;
        }
    }
}

Because we’re using a map of ID’s to point to data, we can use the same logic for point updates and point additions, since both addition and value assignments use the same logic. If we’ve received a gwc::TOUCHREMOVED event, we simply erase the point from the map. TOUCHADDED is not utilized here, but you use it to reference touch down events.

The application window is not fullscreen, the points that come out of GestureWorks Core need to be normalized to the window area. Add the following function to HelloMultitouchApp.cpp:

HelloMultitouchApp.cpp

std::pair<int, int> HelloMultitouchApp::normScreenToWindowPx(float screen_x, float screen_y){
    Vec2i rect = getWindowSize();

    int x_px;
    int y_px;
    if (!use_pixels)
    {
        x_px = static_cast<int>(screen_x * screen_width);
        y_px = static_cast<int>(screen_y * screen_height);
    }
    else
    {
        x_px = static_cast<int>(screen_x);
        y_px = static_cast<int>(screen_y);
    }

    int x = x_px - getWindowPosX();
    int y = y_px - getWindowPosY();

    return std::pair<int, int>(x, y);
}

4. The Draw Function

All of our actual visualization happens in the Cinder draw function. This gets called once per cycle, and we’ll utilize it to draw a visual representation of touch points to the screen. When finished, it should look something like this:

We begin by setting the background to gray normalized RGB values (0.66, 0.66, 0.66) using gl::clear. This also clears the screen of any drawing commands left from the previous frame. Then we enable alpha blending using: gl::enableAlphaBlending(); Finally we set the color for all subsequent drawing commands using gl:color. These are routine Cinder drawing commands.

HelloMultitouchApp.cpp

void HelloMultitouchApp::draw()
{
    gl::clear(Color(0.66f, 0.66f, 0.66f), true);
    gl::enableAlphaBlending();
    gl::color(Color(1.0f, 0.2f, 0.0f));

The next step is to loop through the map of active touchpoints, using the x and y coordinates of each point object, as well as the point’s ID to present each active touchpoint as an outlined circle with it’s x and y coordinates and it’s ID adjacent. You can see the for loop appended to the draw function below:

HelloMultitouchApp.cpp

void HelloMultitouchApp::draw()
{
    gl::clear(Color(0.66f, 0.66f, 0.66f), true);
    gl::enableAlphaBlending();
    gl::color(Color(1.0f, 0.2f, 0.0f));

    //For each active point
    for (std::map<int, gwc::Point>::iterator points_it = active_points.begin(); points_it != active_points.end(); points_it++)
    {
        float x = points_it->second.x;
        float y = points_it->second.y;

        //Generate a circle at this point's location
        gl::drawSolidCircle(Vec2f(x, y), 40.0f);
        gl::drawStrokedCircle(Vec2f(x, y), 50.0f);

        //Generate a stringstream for each value with which we're concerned
        std::stringstream xvals; xvals << x;
	std::stringstream yvals; yvals << y;
        std::stringstream ids; ids << points_it->first;

        //Annotate the circle we just drew with the id, x and y values for the corresponding point
        mTextureFont->drawString("ID: " + ids.str() + "    X: " + xvals.str() + "  " + " Y: " + yvals.str(), Vec2f(x + 40.0f, y - 40.0f));
    }
}

Next, draw two circles one with a solid fill and one as an outline using gl::drawSolidCircle, and gl::drawStrokedCircle. We set the positions of these circle to the coordinates of the touch points.

Last, we pass the x and y coordinate values as well as the point’s ID into a set of stringstreams so that we can assemble a string with these values. These use these strings to draw the given text at the given coordinates. Draw the text using a texture_font, and give a 40 pixels offset from the circle, so that the text does not overlap with the circle.

If you are having difficulty with the display functions, it may be useful to print this info to the console using std::cout. This can help you to determine whether you’re receiving coordinates correctly.


Troubleshooting

If any of the initialization functions fail, ensure that they’re being called with the correct path information. If initialization succeeds, but you aren’t receiving any touch data, double-check that you called RegisterWindowForTouchByName with the exact title of the window. If you are still having issues, confirm that you are loading the correct version of the GestureWorksCore.dll file, 32-bit or 64-bit.


Next tutorial: C++ & Cinder: Interactive Bitmaps
Previous tutorial: C++ & Cinder: Getting Started I (Hello World)

tutorials/cpp_cinder/getting_started_2_hello_multitouch.txt · Last modified: 2015/11/12 14:45 by glass