Created
September 2, 2023 11:00
-
-
Save Kiterai/4bcb2543ce0a8b6ea4e464ceecb0271d to your computer and use it in GitHub Desktop.
windows RealTimeStylus demo
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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