Skip to content

Instantly share code, notes, and snippets.

@Kiterai
Created September 2, 2023 11:00
Show Gist options
  • Save Kiterai/4bcb2543ce0a8b6ea4e464ceecb0271d to your computer and use it in GitHub Desktop.
Save Kiterai/4bcb2543ce0a8b6ea4e464ceecb0271d to your computer and use it in GitHub Desktop.
windows RealTimeStylus demo
//
// Write by Kiterai 2023
// https://chaosplant.tech/tech-notes/page50
//
// This source code is licensed under CC0
// http://creativecommons.org/publicdomain/zero/1.0/deed.ja
//
#include <windows.h>
#include <ole2.h>
#include <rtscom.h>
#include <rtscom_i.c>
#include <atlbase.h>
#include <string>
#include <map>
using namespace std::string_literals;
constexpr LPCWSTR windowClassName = L"MYWINDOWCLASS";
#define _TOSTRING(s) #s
#define TOSTRING(s) _TOSTRING(s)
#define CHK_ERR_THROW(x) if(FAILED(x)) throw std::exception(TOSTRING(x));
std::string packetPropGuidToName(GUID guid) {
if (guid == GUID_PACKETPROPERTY_GUID_X)
return "GUID_PACKETPROPERTY_GUID_X";
else if (guid == GUID_PACKETPROPERTY_GUID_Y)
return "GUID_PACKETPROPERTY_GUID_Y";
else if (guid == GUID_PACKETPROPERTY_GUID_Z)
return "GUID_PACKETPROPERTY_GUID_Z";
else if (guid == GUID_PACKETPROPERTY_GUID_PACKET_STATUS)
return "GUID_PACKETPROPERTY_GUID_PACKET_STATUS";
else if (guid == GUID_PACKETPROPERTY_GUID_TIMER_TICK)
return "GUID_PACKETPROPERTY_GUID_TIMER_TICK";
else if (guid == GUID_PACKETPROPERTY_GUID_SERIAL_NUMBER)
return "GUID_PACKETPROPERTY_GUID_SERIAL_NUMBER";
else if (guid == GUID_PACKETPROPERTY_GUID_NORMAL_PRESSURE)
return "GUID_PACKETPROPERTY_GUID_NORMAL_PRESSURE";
else if (guid == GUID_PACKETPROPERTY_GUID_TANGENT_PRESSURE)
return "GUID_PACKETPROPERTY_GUID_TANGENT_PRESSURE";
else if (guid == GUID_PACKETPROPERTY_GUID_BUTTON_PRESSURE)
return "GUID_PACKETPROPERTY_GUID_BUTTON_PRESSURE";
else if (guid == GUID_PACKETPROPERTY_GUID_X_TILT_ORIENTATION)
return "GUID_PACKETPROPERTY_GUID_X_TILT_ORIENTATION";
else if (guid == GUID_PACKETPROPERTY_GUID_Y_TILT_ORIENTATION)
return "GUID_PACKETPROPERTY_GUID_Y_TILT_ORIENTATION";
else if (guid == GUID_PACKETPROPERTY_GUID_AZIMUTH_ORIENTATION)
return "GUID_PACKETPROPERTY_GUID_AZIMUTH_ORIENTATION";
else if (guid == GUID_PACKETPROPERTY_GUID_ALTITUDE_ORIENTATION)
return "GUID_PACKETPROPERTY_GUID_ALTITUDE_ORIENTATION";
else if (guid == GUID_PACKETPROPERTY_GUID_TWIST_ORIENTATION)
return "GUID_PACKETPROPERTY_GUID_TWIST_ORIENTATION";
else if (guid == GUID_PACKETPROPERTY_GUID_PITCH_ROTATION)
return "GUID_PACKETPROPERTY_GUID_PITCH_ROTATION";
else if (guid == GUID_PACKETPROPERTY_GUID_ROLL_ROTATION)
return "GUID_PACKETPROPERTY_GUID_ROLL_ROTATION";
else if (guid == GUID_PACKETPROPERTY_GUID_YAW_ROTATION)
return "GUID_PACKETPROPERTY_GUID_YAW_ROTATION";
else if (guid == GUID_PACKETPROPERTY_GUID_WIDTH)
return "GUID_PACKETPROPERTY_GUID_WIDTH";
else if (guid == GUID_PACKETPROPERTY_GUID_HEIGHT)
return "GUID_PACKETPROPERTY_GUID_HEIGHT";
else if (guid == GUID_PACKETPROPERTY_GUID_FINGERCONTACTCONFIDENCE)
return "GUID_PACKETPROPERTY_GUID_FINGERCONTACTCONFIDENCE";
else if (guid == GUID_PACKETPROPERTY_GUID_DEVICE_CONTACT_ID)
return "GUID_PACKETPROPERTY_GUID_DEVICE_CONTACT_ID";
else
return "unknown";
}
std::string unitToName(PROPERTY_UNITS unit) {
switch (unit) {
case PROPERTY_UNITS_DEFAULT:
return "Default";
case PROPERTY_UNITS_INCHES:
return "Inches";
case PROPERTY_UNITS_CENTIMETERS:
return "Centimeters";
case PROPERTY_UNITS_DEGREES:
return "Degrees";
case PROPERTY_UNITS_RADIANS:
return "Radians";
case PROPERTY_UNITS_SECONDS:
return "Seconds";
case PROPERTY_UNITS_POUNDS:
return "Pounds";
case PROPERTY_UNITS_GRAMS:
return "Grams";
case PROPERTY_UNITS_SILINEAR:
return "SI Linear";
case PROPERTY_UNITS_SIROTATION:
return "SI Rotation";
case PROPERTY_UNITS_ENGLINEAR:
return "English Linear";
case PROPERTY_UNITS_ENGROTATION:
return "English Rotation";
case PROPERTY_UNITS_SLUGS:
return "Slugs";
case PROPERTY_UNITS_KELVIN:
return "Kelvin";
case PROPERTY_UNITS_FAHRENHEIT:
return "Fahrenheit";
case PROPERTY_UNITS_AMPERE:
return "Ampere";
case PROPERTY_UNITS_CANDELA:
return "Candela";
default:
return "Unknown Unit";
}
}
float convertToRadian(float value, PROPERTY_UNITS unit) {
switch (unit)
{
case PROPERTY_UNITS_DEGREES:
value *= 3.14159f / 180;
break;
case PROPERTY_UNITS_RADIANS:
break;
default:
OutputDebugStringA("Unexpected unit");
break;
}
return value;
}
struct MyTouchInfo {
int x, y;
int status;
bool inAir;
float tiltx, tilty;
float pressure;
};
class MyEventHandler : public IStylusAsyncPlugin {
LONG cRef;
IUnknown* pMarshaller;
HWND hWnd;
CComPtr<IInkRenderer> pInkRenderer;
HDC hDC;
std::map<STYLUS_ID, MyTouchInfo> touchInfos;
struct PacketDescription {
PACKET_PROPERTY propInfos[32];
};
std::map<TABLET_CONTEXT_ID, PacketDescription> packetDescs;
// 入力デバイス(Tablet)の情報を取得
void PrepareTabletInfo(IRealTimeStylus* pStylus, TABLET_CONTEXT_ID tcid) {
// 既に取得していた場合は何もしない
if (packetDescs.find(tcid) != packetDescs.end())
return;
float scaleX, scaleY;
ULONG nPacketProps;
PACKET_PROPERTY* pPacketProps;
pStylus->GetPacketDescriptionData(tcid, &scaleX, &scaleY, &nPacketProps, &pPacketProps);
PacketDescription desc;
for (ULONG i = 0; i < nPacketProps; i++) {
desc.propInfos[i] = pPacketProps[i];
std::string str = "Prop"s + std::to_string(i) + ": "s + packetPropGuidToName(desc.propInfos[i].guid) + "\n"s;
OutputDebugStringA(str.c_str());
}
packetDescs[tcid] = desc;
}
// 入力パケットの処理
void ProcessPacket(const StylusInfo* pStylusInfo, LONG* props, ULONG nProps, bool inair) {
const auto& desc = packetDescs[pStylusInfo->tcid];
long tmpX = 0, tmpY = 0;
auto& touchInfo = touchInfos[pStylusInfo->cid];
touchInfo.pressure = 0.5; // デフォルト: 0.5(中間値)
touchInfo.tiltx = 0; // デフォルト: 0(傾き無し)
touchInfo.tilty = 0; // デフォルト: 0(傾き無し)
touchInfo.inAir = inair;
touchInfo.status = inair; // デフォルト: タッチ中なら1, そうでないなら0
for (ULONG i = 0; i < nProps; i++) {
const auto& propInfo = desc.propInfos[i];
if (propInfo.guid == GUID_PACKETPROPERTY_GUID_X) {
tmpX = props[i]; // X座標
}
else if (propInfo.guid == GUID_PACKETPROPERTY_GUID_Y) {
tmpY = props[i]; // X座標
}
else if (propInfo.guid == GUID_PACKETPROPERTY_GUID_NORMAL_PRESSURE) {
touchInfo.pressure = props[i] / float(propInfo.PropertyMetrics.nLogicalMax); // 筆圧
}
else if (propInfo.guid == GUID_PACKETPROPERTY_GUID_X_TILT_ORIENTATION) {
touchInfo.tiltx = convertToRadian(props[i] / propInfo.PropertyMetrics.fResolution, propInfo.PropertyMetrics.Units); // 傾きX
}
else if (propInfo.guid == GUID_PACKETPROPERTY_GUID_Y_TILT_ORIENTATION) {
touchInfo.tilty = convertToRadian(props[i] / propInfo.PropertyMetrics.fResolution, propInfo.PropertyMetrics.Units); // 傾きY
}
else if (propInfo.guid == GUID_PACKETPROPERTY_GUID_PACKET_STATUS) {
touchInfo.status = props[i]; // 入力状態
}
}
pInkRenderer->InkSpaceToPixel(LONG_PTR(hDC), &tmpX, &tmpY); // インク空間座標からピクセル座標への変換
touchInfo.x = tmpX;
touchInfo.y = tmpY;
InvalidateRect(hWnd, NULL, TRUE); // 再描画
}
public:
MyEventHandler(HWND hWnd) : cRef(1), pMarshaller(NULL), hWnd(hWnd), hDC(GetDC(hWnd)) {
CHK_ERR_THROW(CoCreateFreeThreadedMarshaler(this, &pMarshaller));
CHK_ERR_THROW(pInkRenderer.CoCreateInstance(__uuidof(InkRenderer)));
}
virtual ~MyEventHandler() {
if (pMarshaller != NULL) pMarshaller->Release();
}
const std::map<STYLUS_ID, MyTouchInfo>& getTouchPoints() {
return touchInfos;
}
STDMETHOD_(ULONG, AddRef)() {
return InterlockedIncrement(&cRef);
}
STDMETHOD_(ULONG, Release)() {
ULONG nNewRef = InterlockedDecrement(&cRef);
if (nNewRef == 0) delete this;
return nNewRef;
}
STDMETHOD(QueryInterface)(REFIID riid, LPVOID* ppvObj) {
if ((riid == IID_IStylusSyncPlugin) || (riid == IID_IUnknown)) {
*ppvObj = this;
AddRef();
return S_OK;
}
else if ((riid == IID_IMarshal) && (pMarshaller != NULL)) {
return pMarshaller->QueryInterface(riid, ppvObj);
}
*ppvObj = NULL;
return E_NOINTERFACE;
}
STDMETHOD(DataInterest)(RealTimeStylusDataInterest* pEventInterest) {
*pEventInterest = (RealTimeStylusDataInterest)(RTSDI_AllData);
return S_OK;
}
// イベント処理
STDMETHOD(Packets)(IRealTimeStylus* pStylus, const StylusInfo* pStylusInfo, ULONG nPackets, ULONG nPacketBuf, LONG* pPackets, ULONG* nOutPackets, LONG** ppOutPackets) {
ULONG nPropsPerPacket = nPacketBuf / nPackets;
PrepareTabletInfo(pStylus, pStylusInfo->tcid);
for (ULONG i = 0; i < nPackets; i++) {
this->ProcessPacket(pStylusInfo, &pPackets[nPropsPerPacket * i], nPropsPerPacket, false);
}
return S_OK;
}
STDMETHOD(InAirPackets)(IRealTimeStylus* pStylus, const StylusInfo* pStylusInfo, ULONG nPackets, ULONG nPacketBuf, LONG* pPackets, ULONG* nOutPackets, LONG** ppOutPackets) {
ULONG nPropsPerPacket = nPacketBuf / nPackets;
PrepareTabletInfo(pStylus, pStylusInfo->tcid);
for (ULONG i = 0; i < nPackets; i++) {
this->ProcessPacket(pStylusInfo, &pPackets[nPropsPerPacket * i], nPropsPerPacket, true);
}
return S_OK;
}
STDMETHOD(StylusDown)(IRealTimeStylus* pStylus, const StylusInfo* pStylusInfo, ULONG nPropCountPerPkt, LONG* pPackets, LONG** ppOutPackets) {
PrepareTabletInfo(pStylus, pStylusInfo->tcid);
this->ProcessPacket(pStylusInfo, pPackets, nPropCountPerPkt, false);
return S_OK;
}
STDMETHOD(StylusUp)(IRealTimeStylus* pStylus, const StylusInfo* pStylusInfo, ULONG nPropCountPerPkt, LONG* pPackets, LONG** ppOutPackets) {
PrepareTabletInfo(pStylus, pStylusInfo->tcid);
this->ProcessPacket(pStylusInfo, pPackets, nPropCountPerPkt, true);
return S_OK;
}
STDMETHOD(StylusOutOfRange)(IRealTimeStylus*, TABLET_CONTEXT_ID, STYLUS_ID sid) {
// 入力が範囲外に出た場合に項目を削除
touchInfos.erase(sid);
return S_OK;
}
STDMETHOD(StylusInRange)(IRealTimeStylus*, TABLET_CONTEXT_ID, STYLUS_ID) { return S_OK; }
STDMETHOD(RealTimeStylusEnabled)(IRealTimeStylus*, ULONG, const TABLET_CONTEXT_ID*) { return S_OK; }
STDMETHOD(RealTimeStylusDisabled)(IRealTimeStylus*, ULONG, const TABLET_CONTEXT_ID*) { return S_OK; }
STDMETHOD(StylusButtonUp)(IRealTimeStylus*, STYLUS_ID, const GUID*, POINT*) { return S_OK; }
STDMETHOD(StylusButtonDown)(IRealTimeStylus*, STYLUS_ID, const GUID*, POINT*) { return S_OK; }
STDMETHOD(SystemEvent)(IRealTimeStylus*, TABLET_CONTEXT_ID, STYLUS_ID, SYSTEM_EVENT, SYSTEM_EVENT_DATA) { return S_OK; }
STDMETHOD(TabletAdded)(IRealTimeStylus*, IInkTablet*) { return S_OK; }
STDMETHOD(TabletRemoved)(IRealTimeStylus*, LONG) { return S_OK; }
STDMETHOD(CustomStylusDataAdded)(IRealTimeStylus*, const GUID*, ULONG, const BYTE*) { return S_OK; }
STDMETHOD(Error)(IRealTimeStylus*, IStylusPlugin*, RealTimeStylusDataInterest, HRESULT, LONG_PTR*) { return S_OK; }
STDMETHOD(UpdateMapping)(IRealTimeStylus*) { return S_OK; }
};
class MainWindow {
CComPtr<IRealTimeStylus> pRTS;
MyEventHandler* handler;
HPEN redPen, greenPen, bluePen, blackPen;
void Initialize(HWND hWnd) {
CHK_ERR_THROW(CoInitialize(NULL));
// RealTimeStylusオブジェクトの生成
CHK_ERR_THROW(pRTS.CoCreateInstance(__uuidof(RealTimeStylus)));
// イベントハンドラの設定
handler = new MyEventHandler(hWnd);
CHK_ERR_THROW(pRTS->AddStylusAsyncPlugin(0, handler));
// 受信するプロパティ情報
GUID lWantedProps[] = {
GUID_PACKETPROPERTY_GUID_X,
GUID_PACKETPROPERTY_GUID_Y,
GUID_PACKETPROPERTY_GUID_Z,
GUID_PACKETPROPERTY_GUID_PACKET_STATUS,
GUID_PACKETPROPERTY_GUID_TIMER_TICK,
GUID_PACKETPROPERTY_GUID_SERIAL_NUMBER,
GUID_PACKETPROPERTY_GUID_NORMAL_PRESSURE,
GUID_PACKETPROPERTY_GUID_TANGENT_PRESSURE,
GUID_PACKETPROPERTY_GUID_BUTTON_PRESSURE,
GUID_PACKETPROPERTY_GUID_X_TILT_ORIENTATION,
GUID_PACKETPROPERTY_GUID_Y_TILT_ORIENTATION,
GUID_PACKETPROPERTY_GUID_AZIMUTH_ORIENTATION,
GUID_PACKETPROPERTY_GUID_ALTITUDE_ORIENTATION,
GUID_PACKETPROPERTY_GUID_TWIST_ORIENTATION,
GUID_PACKETPROPERTY_GUID_PITCH_ROTATION,
GUID_PACKETPROPERTY_GUID_ROLL_ROTATION,
GUID_PACKETPROPERTY_GUID_YAW_ROTATION,
GUID_PACKETPROPERTY_GUID_WIDTH,
GUID_PACKETPROPERTY_GUID_HEIGHT,
GUID_PACKETPROPERTY_GUID_FINGERCONTACTCONFIDENCE,
GUID_PACKETPROPERTY_GUID_DEVICE_CONTACT_ID,
};
CHK_ERR_THROW(pRTS->SetDesiredPacketDescription(std::size(lWantedProps), lWantedProps));
// ウィンドウの設定
CHK_ERR_THROW(pRTS->put_HWND((HANDLE_PTR)hWnd));
// 入力の有効化
CHK_ERR_THROW(pRTS->put_Enabled(true));
redPen = CreatePen(PS_SOLID, 5, RGB(0xFF, 0x80, 0x80));
greenPen = CreatePen(PS_SOLID, 5, RGB(0x80, 0xFF, 0x80));
bluePen = CreatePen(PS_SOLID, 5, RGB(0x80, 0x80, 0xFF));
blackPen = CreatePen(PS_SOLID, 5, RGB(0, 0, 0));
}
void Uninitialize() {
CoUninitialize();
}
void Paint(HWND hWnd) {
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hWnd, &ps);
for (const auto& [sid, touch] : this->handler->getTouchPoints()) {
int r = (touch.pressure + 0.5) * 50;
float tiltLen = 100;
int dx = tiltLen * sin(touch.tiltx), dy = tiltLen * sin(touch.tilty);
if (touch.inAir)
SelectObject(hdc, this->greenPen);
else
SelectObject(hdc, this->redPen);
Ellipse(hdc, touch.x - r, touch.y - r, touch.x + r, touch.y + r);
SelectObject(hdc, this->bluePen);
MoveToEx(hdc, touch.x, touch.y, NULL);
LineTo(hdc, touch.x + dx, touch.y + dy);
}
SelectObject(hdc, this->blackPen);
int cnt = 0;
for (const auto& [sid, touch] : this->handler->getTouchPoints()) {
int tmp = touch.status;
for (int i = 0; i < 4; i++) {
if (tmp & 1) {
Ellipse(hdc, i * 30 + 20, cnt * 30 + 20, i * 30 + 40, cnt * 30 + 40);
}
tmp >>= 1;
}
}
EndPaint(hWnd, &ps);
}
static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) {
auto const thiz = reinterpret_cast<MainWindow*>(GetWindowLongPtrW(hWnd, GWLP_USERDATA));
switch (message)
{
case WM_CREATE:
{
auto const thiz = reinterpret_cast<MainWindow*>(LPCREATESTRUCT(lParam)->lpCreateParams);
SetWindowLongPtrW(hWnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(thiz));
try {
thiz->Initialize(hWnd);
}
catch (std::exception e) {
OutputDebugStringA(e.what());
return -1;
}
}
break;
case WM_PAINT:
thiz->Paint(hWnd);
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
ATOM MyRegisterClass(HINSTANCE hInstance) {
WNDCLASSW wc = {};
wc.lpfnWndProc = WndProc;
wc.hInstance = hInstance;
wc.hCursor = LoadCursor(nullptr, IDC_ARROW);
wc.hbrBackground = CreateSolidBrush(RGB(0xFF, 0xFF, 0xFF));
wc.lpszClassName = windowClassName;
return RegisterClassW(&wc);
}
public:
MainWindow() : pRTS(), handler(NULL) {}
~MainWindow() { Uninitialize(); }
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow) {
MyRegisterClass(hInstance);
HWND hWnd = CreateWindowW(windowClassName, L"RealTimeStylus Sample", WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, nullptr, nullptr, hInstance, this);
if (!hWnd)
{
return FALSE;
}
ShowWindow(hWnd, nCmdShow);
UpdateWindow(hWnd);
return TRUE;
}
};
int APIENTRY wWinMain(
HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPWSTR lpCmdLine,
int nCmdShow
) {
MainWindow mainWnd;
if (!mainWnd.InitInstance(hInstance, nCmdShow))
return FALSE;
MSG msg;
while (GetMessage(&msg, nullptr, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return (int)msg.wParam;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment