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
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
- 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)
- 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.