(qt)【學習記錄】實現wacom壓感繪圖

開頭附上我博客上的鏈接
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。。


到此爲止筆的數據收集就完成了

下次有空再寫繪製部分

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章