軌跡球滾輪模擬

相信很多人剛入軌跡球坑的時候,第一次選的會是羅技木星,畢竟價錢差在那裏,
對於完全沒用過的人多數還是會想先試試看損失不會太大的款式吧。老實說它並不算好用的一款,
其中一個缺點就是沒有實體滾輪,用setpoint裡的模擬還不如自己到旁邊拉Scrollbar。

總之當時在這樣的背景下試著寫了個模擬滾輪的程式,寫完發現不得了,這東西好用的程度遠超預期OAO,
以至於之後換了有滾輪的款式也還是跟這個混著用。

因為又換了一支新的軌跡球,隔了四年技術也進步了,想說整理一下代碼,就剛好紀錄一下放上來吧。

先說說最終效果,用在軌跡球上面會變成很舒服很大顆的無段滾輪,而且有著不錯的加速度,細微的移動和快速大量移動都能做到。
非常適合看文件、小說或是其他長篇的東西。

如果用在觸控板上面其實跟兩指滑的感覺差不多,不過參數比較自由。

用在滑鼠上有點詭異,滑鼠拉太下面要拿回原位的時候比較麻煩。

缺點就是比起原生會要多按一個按鍵啟動,另外在對於滾動很耗效能的網頁上使用無段滾輪會很卡。

目標&想法:

目標是把滑鼠的移動量截斷下來,轉換成滾輪的移動。

實作:

截取滑鼠移動:

SetWindowsHookEx
Windows下有個很恐怖(?)的功能能夠截取所有輸入,而且不需要管理者權限,它叫Low-level hook。

g_hMouse = SetWindowsHookEx( WH_MOUSE_LL, mouseProc, GetModuleHandle(NULL), 0 );

掛上鉤子就像上面那樣,mouseProc是收到事件時的回調函數。

LRESULT CALLBACK mouseProc(int nCode, WPARAM wParam, LPARAM lParam){
  if (wParam==WM_MOUSEMOVE){
    const PMOUSEINPUT pmin = (PMOUSEINPUT) lParam;
    printf("x: %d, y: %d\n", pmin->dx, pmin->dy);
    return true;
  }
  return CallNextHookEx(g_hMouse, nCode, wParam, lParam);
}

這樣子就截下移動了,簡單到令人恐懼。

模擬輸入

SendInput
INPUT Structure
獲得移動之後就是想辦法轉成輸出了:

INPUT in;
in.type = INPUT_MOUSE;
in.mi.dx = 0;
in.mi.dy = 0;
in.mi.dwFlags = MOUSEEVENTF_WHEEL;
in.mi.time = 0;
in.mi.dwExtraInfo = 0;
in.mi.mouseData = scroll;
SendInput(1,&in,sizeof(in));

用SendInput就可以模擬出來了,dwFlags參考INPUT給的事件可以改成注入別的操作。

避免模擬輸入回饋(用法正確性不保證不過可以用)

SetMessageExtraInfo
為了把我們注入的輸入區別開來,我們需要為這個事件貼上一個額外的辨識碼。

#define EVTID 7851
void scroll(DWORD data){
    SetMessageExtraInfo(EVTID);
    INPUT in;
    in.type = INPUT_MOUSE;
    in.mi.dx = 0;
    in.mi.dy = 0;
    in.mi.dwFlags = MOUSEEVENTF_WHEEL;
    in.mi.time = 0;
    in.mi.dwExtraInfo = EVTID;
    in.mi.mouseData = data;
    SendInput(1,&in,sizeof(in));
    SetMessageExtraInfo(0);
}
LRESULT CALLBACK mouseProc(int nCode, WPARAM wParam, LPARAM lParam){
  if (GetMessageExtraInfo() == EVTID) return CallNextHookEx(g_hMouse, nCode, wParam, lParam);
  if (wParam==WM_MOUSEMOVE){
    scroll(120);
  }
  return CallNextHookEx(g_hMouse, nCode, wParam, lParam);
}

這樣一來遇到我們注入的事件時mouseProc就會自動跳過了。

加速度曲線

除了拋物線以外,也混上了指數曲線讓它在快速移動的時候可以滑得更遠。
(當時決定的方式是慢慢滑的時候我可以正常地看小說,快速滑可以一次滑過半本ww)

double g_sen = -120.0/15.0;
double ds = g_sen * ( pmin->dy - g_Origin.y );
const bool negative = ds < 0;
if(negative) ds = -ds;
ds = pow(ds, 1.15) * pow(1.1, ds/65);

按鍵組合延時

SetTimer
因為想要讓原本被占用的中鍵按久一點可以變回原本的作用,所以加了個定時器。

#define BUTTONTIMEOUT 500
VOID CALLBACK MBTimer(HWND hwnd, UINT uMsg, UINT_PTR idEvent, DWORD dwTime){
  printf("MBTimer %d Trigger\n", idEvent);
  g_MBTimerID = 0;
  KillTimer(NULL, idEvent);
}

...

  g_MBTimerID = SetTimer(NULL, g_MBTimerID, BUTTONTIMEOUT, MBTimer);

最終代碼

短按一下中鍵是開啟模擬,再短按一下結束,沒開啟模擬前長按是模擬中鍵,開啟後長按是關閉程式,
因為沒寫GUI所以只能這樣關,或是用工作管理員砍。
開啟模擬的時候按右鍵可以切換有段或無段滾輪模擬。

// Compile: g++ -mwindows main.cpp
//#define _DEBUG
#define BUTTONTIMEOUT 500
#define SCROLLTIMEOUT 500
#define WHEELMAX 2147483647
#define EVTID 8194
#define SCROLLTHRESHOLD 720
//#define WHEELMAX 500

#define _WIN32_WINNT 0x0600
#include <windows.h>
#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <cmath>
#include <ctime>
using namespace std;

#ifdef _DEBUG
#define DEBUG(format, args...) printf("[DBG] "format"@%s(%d)\n", ##args, __FILE__, __LINE__)
#else
#define DEBUG(args...)
#endif

bool g_enable=false;
bool g_smooth=true;
double g_dis = 0;
UINT_PTR g_MBTimerID = 0;
UINT_PTR g_SCTimerID = 0;
HHOOK g_hMouse;
POINT g_Origin;
double g_sen;

void scroll(DWORD data){
    SetMessageExtraInfo(EVTID);
    INPUT in;
    in.type = INPUT_MOUSE;
    in.mi.dx = 0;
    in.mi.dy = 0;
    in.mi.dwFlags = MOUSEEVENTF_WHEEL;
    in.mi.time = 0;
    in.mi.dwExtraInfo = EVTID;
    in.mi.mouseData = data;
    DEBUG("scroll: %d", in.mi.mouseData);
    SendInput(1,&in,sizeof(in));
    SetMessageExtraInfo(0);
}

VOID CALLBACK MBTimer(HWND hwnd, UINT uMsg, UINT_PTR idEvent, DWORD dwTime){
    DEBUG("MBTimer %d Trigger", idEvent);
    g_MBTimerID = 0;
    KillTimer(NULL, idEvent);
    if(!g_enable){
        DEBUG("Send MBUTTONDOWN event");
        SetMessageExtraInfo(EVTID);
        INPUT in;
        in.type = INPUT_MOUSE;
        in.mi.dx = 0;
        in.mi.dy = 0;
        in.mi.dwFlags = MOUSEEVENTF_MIDDLEDOWN;
        in.mi.time = 0;
        in.mi.dwExtraInfo = EVTID;
        SendInput(1,&in,sizeof(in));
        SetMessageExtraInfo(0);
    }else{
        DEBUG("Program Exit");
        PostQuitMessage (0);
    }
}

VOID CALLBACK SCTimer(HWND hwnd, UINT uMsg, UINT_PTR idEvent, DWORD dwTime){
    DEBUG("SCTimer %d Trigger", idEvent);
    g_SCTimerID = 0;
    KillTimer(NULL, idEvent);
    if(!g_enable){
        DEBUG("Ignore SCTimer");
    }else{
        DEBUG("Clear Distance");
        g_dis = 0;
    }
}

LRESULT CALLBACK mouseProc(int nCode, WPARAM wParam, LPARAM lParam){
    if (GetMessageExtraInfo() == EVTID) return CallNextHookEx(g_hMouse, nCode, wParam, lParam);
    if (wParam==WM_MOUSEMOVE){
        if(g_enable){
            const PMOUSEINPUT pmin = (PMOUSEINPUT) lParam;
            double ds = g_sen * ( pmin->dy - g_Origin.y );
            const bool negative = ds < 0;
            if(negative) ds = -ds;
            if(g_smooth){
                ds = pow(ds, 1.15) * pow(1.1, ds/65); // Acceleration
                bool cont = true;
                do{
                    if(ds < WHEELMAX){
                        cont = false;
                        scroll(negative ? (-ds) : ds);
                    }else{
                        scroll(negative ? -WHEELMAX : WHEELMAX);
                        ds -= WHEELMAX;
                    }
                    DEBUG("dy: %d", (pmin->dy- g_Origin.y));
                }while(cont);
            }else{
                g_SCTimerID = SetTimer(NULL, g_SCTimerID, SCROLLTIMEOUT, SCTimer);
                DEBUG("SCTimer %d Start", g_SCTimerID);
                //ds = pow(ds, 1.05) * pow(1.1, ds/3000); // Acceleration
                g_dis += (negative ? (-ds) : ds);
                while(abs(g_dis) >= SCROLLTHRESHOLD){
                    scroll(g_dis>0 ? 120 : (-120));
                    g_dis -= (g_dis>0 ? SCROLLTHRESHOLD : (-SCROLLTHRESHOLD));
                }
            }
            return true;
        }
    }
    else if (wParam == WM_RBUTTONDOWN){
        DEBUG("RBUTTONDOWN");
        if(g_enable){
            return true;  
        }
    }
    else if (wParam == WM_RBUTTONUP){
        DEBUG("RBUTTONUP");
        if(g_enable){
            g_smooth = !g_smooth;
            return true;  
        }
    }
    else if (wParam == WM_MBUTTONDOWN){
        DEBUG("MBUTTONDOWN");
        g_MBTimerID = SetTimer(NULL, g_MBTimerID, BUTTONTIMEOUT, MBTimer);
        DEBUG("MBTimer %d Start", g_MBTimerID);
        return true;
    }
    else if (wParam == WM_MBUTTONUP){
        DEBUG("MBUTTONUP");
        if(!g_enable){
            if(g_MBTimerID){
                DEBUG("Enable Wheel Emulator");
                KillTimer(NULL, g_MBTimerID);
                g_MBTimerID = 0;
                GetCursorPos(&g_Origin);
                g_enable = true;
                GetCursorPos(&g_Origin);
            }else{
                DEBUG("Send MBUTTONUP event");
                SetMessageExtraInfo(EVTID);
                INPUT in;
                in.type = INPUT_MOUSE;
                in.mi.dx = 0;
                in.mi.dy = 0;
                in.mi.dwFlags = MOUSEEVENTF_MIDDLEUP;
                in.mi.time = 0;
                in.mi.dwExtraInfo = EVTID;
                SendInput(1,&in,sizeof(in));
                SetMessageExtraInfo(0);
            }
        }else{
            if(g_MBTimerID){
                DEBUG("Disable Wheel Emulator");
                KillTimer(NULL, g_MBTimerID);
                g_MBTimerID = 0;
                g_enable = false;
            }else{
            }
        }
        return true;     
    }

    return CallNextHookEx(g_hMouse, nCode, wParam, lParam);
}

#ifdef _DEBUG
int main()
#else
int WINAPI WinMain (HINSTANCE hThisInstance,
                    HINSTANCE hPrevInstance,
                    LPSTR lpszArgument,
                    int nFunsterStil)
#endif
{
    MSG messages;            /* Here messages to the application are saved */
    g_sen = ((double)-120) / 15;
    g_hMouse = SetWindowsHookEx( WH_MOUSE_LL, mouseProc, GetModuleHandle(NULL), 0 );
    if (!g_hMouse){
        printf("Hook error: %d\n", GetLastError());
        return -1;
    }
    /* Run the message loop. It will run until GetMessage() returns 0 */
    while (GetMessage (&messages, NULL, 0, 0))
    {
        /* Translate virtual-key messages into character messages */
        TranslateMessage(&messages);
        /* Send message to WindowProcedure */
        DispatchMessage(&messages);
    }
    UnhookWindowsHookEx(g_hMouse);

    /* The program return-value is 0 - The value that PostQuitMessage() gave */
    return messages.wParam;
}