Tablet pressure sensitivity in Python and Pygame

This blog post has its own page:  Check there for latest info.

In Pygame, tablets (or at least my Wacom Bamboo) acts just like a mouse.  But it appears that Pygame, and the underlying SDL don’t detect tablet pressure. I’ve read that starting with SDL 1.3/2.0 it will detect for tablet pressure, but until then (currently v1.2)… what options are there?  It doesn’t seem that Python itself has any built-in modules for this either (that I can find…).

After asking the user groups, I tracked down an interface into wintab api through the “Python Computer Graphics Kit” (cgkit).   The API was developed by LCS/Telegraphics and maintained by Wacom:  cgkit has a wintab wrapper (docs) that appears to hook into Wacom standards.

But even with their docs, it took me a bit of troubleshooting to turn it into something usable, but I have:  ‘tablet.py’ which contains a ‘Tablet’ class, and a ‘pressureTest.py’ Pygame app showing it off:

  • cgkit download.  Required to run tablet.py.
  • tablet.py – Module containing the Tablet class.
  • pressureTest.py – Simple Pygame example using the Tablet class.
  • Built with Python 2.6.2, Pygame 1.9.1, cgkit 2.0.0 alpha9

Details:

tablet.py is a module containing a Tablet class, that will give you tablet querying functionality in your Pygame programs.  In a nutshell, you create a Tablet object, then inside the main loop you query your Tablet.getData() method instead of pinging pygame.mouse.  That method spits out this data:

  • The currently pressed pen ‘button’ as an int.
  • x,y tablet cursor position, relative to the current Pygame screen.
  • The tablet ‘pressure’ as an int, mapped from 0-1023

Expected behavior:

  • Given the example in pressureTest.py, on my machine, the tablet maps 100% of it’s surface region into the Pygame window:  It’s impossible for me to move the cursor outside of the Pygame window using the tablet (but the mouse still has external control).
  • However, based on other users, I’ve received reports of the tabletspace->pygame screen mapping not working, and their tablet still has free run of the whole computer screen.  This would cause problems, but unfortuntealy I have no way of testing this without other tablets\computers :(  If you run into this, you can try to troubleshoot:  Go to the wintab docs.  In the Tablet class, I’m querying these Context attrs for the tablet resolution, for mapping purposes back into pygame:  Context.inextx, Context.inexty.  There are many more attrs though, and those just happened to work for me.  If you try other attrs and they solve your problem, please let me know so I can update my code.

Here’s the few lines of code you’d use to put it in a Pygame app (illustrated in pressureTest.py):

from tablet import Tablet
tablet = Tablet(screen)
# Main loop:
while looping:
    button, x, y, pressure = tablet.getData()

This is the general implementation behind capturing the tablet data (crack open tablet.py to see what’s going on).  I should point out this was all by trial and error.  There could be better\more efficient\more eloquent ways of doing this.  But, this works :)

  • the wintab module needs to know the ‘window handle’ \ ‘window id’ for the current pygame app.  This isn’t something you query, but something you set.  The Tablet object makes up a window id, and sets it as an environment var:
    • os.environ[“SDL_WINDOWID”] = str(hwnd)
  • Create a new wintab Context object that will capture our tablet data:
    • self.context = wintab.Context()
  • Define what type of data our context will return via Packet objects.  These are based on wintab constants:
    • self.context.pktdata = ( PK_X | PK_Y | PK_BUTTONS | PK_NORMAL_PRESSURE )
    • It should be noted that the default values for pkdata (if you don’t define your own) are: xpos, ypos, buttons.
  • Open out context:
    • self.context.open(self.hwnd, True)
  • Then later we can query our Tablet object once per Pygame loop for Packet object data.  A LOT of packet data can be returned.  So we query the “extent” of the packets that were returned:
    • packetExtents = self.context.queuePacketsEx()
    • If the pen is away from the tablet, it will return None, so in that case, we don’t eval anything.
  • Presuming we found packet data, we the last packet list from the sub-list of packets that were returned:
    • packets = self.context.packetsGet(packetExtents[1])
  • And, we isolate the last packet in that sublist: (did I mention there were a lot of packets being returned?)
    • packet = packets[-1]
  • Based on that Packet object, we can now query it for which buttons were pressed, the x & y position, and the all important pressure data, via its attributes.
  • It’s important to note that Pygame has it’s (0,0) coordinate in the top-left corner of the screen, while the tablet has it in the bottom-left corner.  So the class does some hoop-jumping to convert the coordinates to Pygame.

The key takeaway is setting the Context.pktdata attribute:  This is the step that controls what is returned via the packets.  Depending on your tablet, a lot more info can be returned.

R2D2 Maya Viewcube
Pygame Window Info
  • Trackback are closed
  • Comments (2)
    • Skylet
    • February 1st, 2014 3:46am

    Hi,

    thanks a lot for sharing your project. A very helpful explanation and useful basic class. Very nice for me to start with. You helped me a lot to get started and save some time. Have you already made more of an application based on this module?

    BR,
    Sky

  1. Actually I haven’t, other than what was listed. I tend to bounce around projects a lot 😉

Comment are closed.