(qt)【学习记录】实现wacom压感绘图
要做压感绘图要考虑很多综合的问题。1。要有高效的图片绘制方法,一是后台对像素图片的绘制,二是如何高速的把修改完的图片显示出来2.要有同步的无损失的笔的数据获取方法,如果说通过qt自带的压感笔事件来做的话,可能会因为绘图的时间过长而跳过了一部分笔事件,那就获取到的笔信息有损失了这里获取笔信息的方法是通过监听windows系统消息,qt里是 nativeEventFilter可以参考githu...
开头附上我博客上的链接
http://www.hbzmlab.tech/index.php/2019/03/09/45/
要做压感绘图要考虑很多综合的问题。
1。要有高效的图片绘制方法,一是后台对像素图片的绘制,二是如何高速的把修改完的图片显示出来
一开始我做这个选择的是c#,因为c#有wintabdn的样例。但是发现c#绘图效率有点低,尤其是对像素图片的编辑处理。而qt中的pixmap则非常高效
2.要有同步的无损失的笔的数据获取方法,如果说通过qt自带的压感笔事件来做的话,可能会因为绘图的时间过长而跳过了一部分笔事件,那就获取到的笔信息有损失了
这里获取笔信息的方法是通过监听windows系统消息,qt里是 nativeEventFilter
可以参考github上的这个项目
https://github.com/liuyanghejerry/Qt-TabletSupport
这一个项目调用的是wintab32.dll,这个方案支持大多数数位板,但是平板可能不会带这个库
还有一种方案是getpenpointinfo 是windows 自家的库,但是这个是只支持win8以上,但对平板的兼容性应该要高一点
还有一种方案是tablet pc api估计是windows原先的自家的库,但是资料挺难找,所以还没有研究过。
这里我暂时只尝试了wintab的方案 wintab具体的原理我也不太清楚,wacom现在官网也没法找到wintab的开发资料了,现在好像是个叫will的新的东西,
TabletSupport::TabletSupport(QtGuiApplication1 *window)
:wintab_module(nullptr),
window_(window),
logContext(nullptr)
{
if(!loadWintab()) {
return;
}
if(!mapWintabFuns()){
qCritical()<<"Error with function mapping!";
return;
}
if(!hasDevice()){
qCritical()<<"No Device found!";
return;
}
qCritical() << "No Devid!";
logContext = new tagLOGCONTEXTA;
auto handle = (HWND)window_->winId();
callFunc().ptrWTInfoA(WTI_DEFSYSCTX, 0, logContext);
logContext->lcOptions |= CXO_MESSAGES;
//logContext->lcMoveMask = PACKETDATA;
logContext->lcBtnUpMask = logContext->lcBtnDnMask;
AXIS TabletX;
AXIS TabletY;
callFunc().ptrWTInfoA( WTI_DEVICES, DVC_X, &TabletX );
callFunc().ptrWTInfoA( WTI_DEVICES, DVC_Y, &TabletY );
logContext->lcInOrgX = 0;
logContext->lcInOrgY = 0;
logContext->lcInExtX = TabletX.axMax;
logContext->lcInExtY = TabletY.axMax;
/* output the data in screen coords */
logContext->lcOutOrgX = logContext->lcOutOrgY = 0;
logContext->lcOutExtX = GetSystemMetrics(SM_CXSCREEN);
/* move origin to upper left */
logContext->lcOutExtY = GetSystemMetrics(SM_CYSCREEN);
printf("\ntx%d\n", TabletY.axMax);
logContext->lcPktData = PACKETDATA;
logContext->lcPktMode = PACKETMODE;
tabapis.context_ = callFunc().ptrWTOpenA(handle,
(LPLOGCONTEXTA)logContext,
true);
}
以上是从那个github项目中移植来的构造函数,
bool TabletSupport::loadWintab()
{
wintab_module = LoadLibrary(L"wintab32.dll");
if(!wintab_module) {
DWORD err = GetLastError();
printf("\nCannot load wintab32.dll:\n");
return false;
}
return true;
}
第一步是加载dll
bool TabletSupport::mapWintabFuns()
{
bool isOk = true;
isOk = isOk && getProcAddr<WinTabAPI::WTINFOA>(tabapis.ptrWTInfoA, "WTInfoA");
isOk = isOk && getProcAddr<WinTabAPI::WTOPENA>(tabapis.ptrWTOpenA, "WTOpenA");
isOk = isOk && getProcAddr<WinTabAPI::WTGETA>(tabapis.ptrWTGetA, "WTGetA");
isOk = isOk && getProcAddr<WinTabAPI::WTSETA>(tabapis.ptrWTSetA, "WTSetA");
isOk = isOk && getProcAddr<WinTabAPI::WTOPENA>(tabapis.ptrWTOpenA, "WTOpenA");
isOk = isOk && getProcAddr<WinTabAPI::WTCLOSE>(tabapis.ptrWTClose, "WTClose");
isOk = isOk && getProcAddr<WinTabAPI::WTPACKET>(tabapis.ptrWTPacket, "WTPacket");
isOk = isOk && getProcAddr<WinTabAPI::WTOVERLAP>(tabapis.ptrWTOverlap, "WTOverlap");
isOk = isOk && getProcAddr<WinTabAPI::WTSAVE>(tabapis.ptrWTSave, "WTSave");
isOk = isOk && getProcAddr<WinTabAPI::WTCONFIG>(tabapis.ptrWTConfig, "WTConfig");
isOk = isOk && getProcAddr<WinTabAPI::WTRESTORE>(tabapis.ptrWTRestore, "WTRestore");
isOk = isOk && getProcAddr<WinTabAPI::WTEXTSET>(tabapis.ptrWTExtSet, "WTExtSet");
isOk = isOk && getProcAddr<WinTabAPI::WTEXTGET>(tabapis.ptrWTExtGet, "WTExtGet");
isOk = isOk && getProcAddr<WinTabAPI::WTQUEUESIZESET>(tabapis.ptrWTQueueSizeSet, "WTQueueSizeSet");
isOk = isOk && getProcAddr<WinTabAPI::WTDATAPEEK>(tabapis.ptrWTDataPeek, "WTDataPeek");
isOk = isOk && getProcAddr<WinTabAPI::WTPACKETSGET>(tabapis.ptrWTPacketsGet, "WTPacketsGet");
isOk = isOk && getProcAddr<WinTabAPI::WTMGROPEN>(tabapis.ptrWTMgrOpen, "WTMgrOpen");
isOk = isOk && getProcAddr<WinTabAPI::WTMGRCLOSE>(tabapis.ptrWTMgrClose, "WTMgrClose");
isOk = isOk && getProcAddr<WinTabAPI::WTMGRDEFCONTEXT>(tabapis.ptrWTMgrDefContext, "WTMgrDefContext");
isOk = isOk && getProcAddr<WinTabAPI::WTMGRDEFCONTEXTEX>(tabapis.ptrWTMgrDefContextEx, "WTMgrDefContextEx");
return isOk;
}
第二步是加载一大坨库函数
AXIS TabletX;
AXIS TabletY;
callFunc().ptrWTInfoA( WTI_DEVICES, DVC_X, &TabletX );
callFunc().ptrWTInfoA( WTI_DEVICES, DVC_Y, &TabletY );
logContext->lcInOrgX = 0;
logContext->lcInOrgY = 0;
logContext->lcInExtX = TabletX.axMax;
logContext->lcInExtY = TabletY.axMax;
这里是获取数位板最大范围TabletX.axMax和TabletY.axMax,为以后屏幕坐标对应运算做准备
最后会
tabapis.context_ = callFunc().ptrWTOpenA(handle, (LPLOGCONTEXTA)logContext, true);
通过这个来 开启接收数据
void TabletSupport::start()
{
if(hasDevice()) {
auto dispacher = QAbstractEventDispatcher::instance(window_->thread());
dispacher->installNativeEventFilter(this);
}
}
这里注册系统消息监听
bool lastdown = 0, curdown = 0;
bool TabletSupport::nativeEventFilter(const QByteArray &eventType,
void *message, long *)
{
if (eventType == "windows_generic_MSG") {
MSG* ev = static_cast<MSG *>(message);
switch(ev->message){
case WT_PACKET:
PACKET pkt;
if(!callFunc().ptrWTPacket((HCTX)ev->lParam,
ev->wParam,
&pkt)){
return false;
}
auto preRange_s = normalPressureInfo();
int preRange = preRange_s.axMax - preRange_s.axMin +1;
auto tpreRange_s = tangentialPressureInfo();
int tpreRange = tpreRange_s.axMax - tpreRange_s.axMin +1;
QDesktopWidget* desktopWidget = QApplication::desktop();
int curMonitor = desktopWidget->screenNumber(window_);
QRectF deskRect = desktopWidget->screenGeometry(curMonitor);
lastdown = curdown;
curdown = pkt.down;
QPointF toCurScreen(pkt.pkX*1.0*deskRect.width() / logContext->lcInExtX, pkt.pkY*1.0*deskRect.height() / logContext->lcInExtY);
QPointF xx=window_->getCurCanvas()->mapToGlobal(QPoint(0,0));
QPointF widgetToCurScreen(xx.x() - deskRect.x(), xx.y() - deskRect.y());
QPointF curPen = toCurScreen - widgetToCurScreen;
printf("x:%f y:%f %f\n", curPen.x(), curPen.y(),deskRect.height());
//printf("w:%f h:%f\n", widgetToCurScreen.x(), widgetToCurScreen.y());
//printf("x:%d y:%d\n", window_->getCurCanvas()->width(), window_->getCurCanvas()->height());
//printf("test x:%f y:%f pre:%-5d down:%-5d mode:%-5d %-5d %-5d %-5d\n", pkt.pkX*logContext->lcInExtX*1.0/logContext->lcOutExtX, pkt.pkY*logContext->lcInExtY*1.0 / logContext->lcOutExtY, pkt.pkNormalPressure, pkt.down, pkt.pkMode,pkt.pkButtons,pkt.pkContext);//,pkt.pkOrientation
auto btn_state = HIWORD(pkt.pkButtons);
if (lastdown > curdown) {//1 0
printf("release\n");
points[writepos].pre = 0;
points[writepos].x = window_->getCurCanvas()->transformX(curPen.x());
points[writepos].y = window_->getCurCanvas()->transformY(curPen.y());
writepos++; if (writepos == 10000)writepos = 0; pcount++;
}
else if (lastdown < curdown) { //0 1
printf("down\n");
points[writepos].pre = pkt.pkNormalPressure*1.0/preRange;
points[writepos].x = window_->getCurCanvas()->transformX(curPen.x());
points[writepos].y = window_->getCurCanvas()->transformY(curPen.y());
writepos++; if (writepos == 10000)writepos = 0; pcount++;
}
else if (lastdown == 1) {//1 1
printf("downMove\n");
points[writepos].pre = pkt.pkNormalPressure*1.0 / preRange;
points[writepos].x = window_->getCurCanvas()->transformX(curPen.x());
points[writepos].y = window_->getCurCanvas()->transformY(curPen.y());
writepos++; if (writepos == 10000)writepos = 0; pcount++;
}
return true;
break;
}
}
return false;
}
这里没有用事件系统,因为开头已经描述过事件系统会引起数据损失
packet的结构不能照着项目的写,因为我那样照着写了之后,数据的顺序是错乱的。具体原因不清楚,还请了解的大佬告知
typedef struct __TAG {
UINT pkOrientation;//unknown
UINT pkMode;
UINT down;
LONG pkX;
LONG pkY;
DWORD pkButtons;//unknown
UINT pkNormalPressure;
HCTX pkContext;//unknown
} __TYPES ;
目前一共有8个变量,有三个是我还不知道干什么的变量,但是少一个就会出错,多一个好像没什么影响
mode变量是橡皮和笔的标志
down是笔有没有碰到屏幕
pkx,y是未转化的横纵坐标
pkNormalPressure就是压感值
if(!callFunc().ptrWTPacket((HCTX)ev->lParam,
ev->wParam,
&pkt)){
return false;
}
调用函数获取包内容到结构体内,
然后是坐标计算。获取到的坐标是数位板坐标。不是显示器像素,
举个例子:假设数位板x坐标的最大范围xmax是 10000;那这个获取到的x坐标就是0-10000的整数;要转换到显示器坐标就得是屏幕宽度w*x/xmax;
然后要再转换到相对控件的坐标,那就得先获取到控件的坐标,
QPointF xx=window_->getCurCanvas()->mapToGlobal(QPoint(0,0));
这个函数是将相对控件的(0,0)坐标转换为全局坐标。全局坐标的原点不一定是屏幕的左上角,因为可能是多个屏幕,所以我们要获取相对屏幕的坐标,就得获取屏幕左上角的坐标
QDesktopWidget* desktopWidget = QApplication::desktop();
int curMonitor = desktopWidget->screenNumber(window_);
QRectF deskRect = desktopWidget->screenGeometry(curMonitor);
通过以上的代码可以获取到控件对应的显示器编号,然后获取显示器对应的长方体
然后此时控件相对于屏幕的坐标就是
QPointF widgetToCurScreen(xx.x() - deskRect.x(), xx.y() - deskRect.y());
这个时候 【笔相对于控件的坐标】 = 【笔相对屏幕的坐标】-【控件相对屏幕的坐标】
if (lastdown > curdown) {//1 0
printf("release\n");
points[writepos].pre = 0;
points[writepos].x = window_->getCurCanvas()->transformX(curPen.x());
points[writepos].y = window_->getCurCanvas()->transformY(curPen.y());
writepos++; if (writepos == 10000)writepos = 0; pcount++;
}
else if (lastdown < curdown) { //0 1
printf("down\n");
points[writepos].pre = pkt.pkNormalPressure*1.0/preRange;
points[writepos].x = window_->getCurCanvas()->transformX(curPen.x());
points[writepos].y = window_->getCurCanvas()->transformY(curPen.y());
writepos++; if (writepos == 10000)writepos = 0; pcount++;
}
else if (lastdown == 1) {//1 1
printf("downMove\n");
points[writepos].pre = pkt.pkNormalPressure*1.0 / preRange;
points[writepos].x = window_->getCurCanvas()->transformX(curPen.x());
points[writepos].y = window_->getCurCanvas()->transformY(curPen.y());
writepos++; if (writepos == 10000)writepos = 0; pcount++;
}
这里就是状态判断,是刚按下,还是刚抬起,还是正在按着,然后把笔数据传入队列,交给绘制的线程读取,这里用的是环形数组,可能有点low。。
到此为止笔的数据收集就完成了
下次有空再写绘制部分
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)