dotnet X11 視窗之間傳送滑鼠訊息 模擬滑鼠輸入

lindexi發表於2024-05-15

本文記錄我閱讀 Avalonia 程式碼過程中所學習到的在 X11 的視窗之間傳送滑鼠訊息,可以跨程序給其他程序的視窗傳送滑鼠訊息,透過此方式可以實現模擬滑鼠輸入

直接使用 XSendEvent 給指定視窗傳送訊息即可,如以下示例程式碼

            var xEvent = new XEvent
            {
                MotionEvent =
                {
                    type = XEventName.MotionNotify,
                    send_event = true,
                    window = Window,
                    display = Display,
                    x = x,
                    y = y
                }
            };
            XSendEvent(Display, Window, propagate: false, new IntPtr((int) (EventMask.ButtonMotionMask)), ref xEvent);

以上的 Window 是自己程序的主視窗,傳送的相關定義程式碼是我從 Avalonia 和 CPF 程式碼倉庫裡面抄的,所有程式碼放在 githubgitee 上,可以使用如下命令列拉取程式碼

先建立一個空資料夾,接著使用命令列 cd 命令進入此空資料夾,在命令列裡面輸入以下程式碼,即可獲取到本文的程式碼

git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin 7636387e97780403ce473f553540a9cc1e0652ef

以上使用的是 gitee 的源,如果 gitee 不能訪問,請替換為 github 的源。請在命令列繼續輸入以下程式碼,將 gitee 源換成 github 源進行拉取程式碼

git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
git pull origin 7636387e97780403ce473f553540a9cc1e0652ef

獲取程式碼之後,進入 DikalehebeekaJaqunicobo 資料夾,即可獲取到原始碼

以上程式碼是對自己程序內的主視窗傳送滑鼠移動訊息的示例,核心程式碼如下

while (true)
{
    var xNextEvent = XNextEvent(Display, out var @event);
    if (@event.type == XEventName.MotionNotify)
    {
        var x = @event.MotionEvent.x;
        var y = @event.MotionEvent.y;

        XDrawLine(Display, Window, GC, x, y, x + 100, y);
    }

    var count = XEventsQueued(Display, 0 /*QueuedAlready*/);
    if (count == 0)
    {
        for (int i = 0; i < 100; i++)
        {
            var xEvent = new XEvent
            {
                MotionEvent =
                {
                    type = XEventName.MotionNotify,
                    send_event = true,
                    window = Window,
                    display = Display,
                    x = i,
                    y = i
                }
            };
            XSendEvent(Display, Window, propagate: false, new IntPtr((int) (EventMask.ButtonMotionMask)), ref xEvent);
        }
    }
}

如上述程式碼可以看到只需更改 XSendEvent 裡面的 Window 對應的引數,即可決定傳送給哪個視窗。比如有兩個視窗,可以透過此方式讓視窗 2 收到滑鼠訊息時,自動轉發給視窗 1 上,核心程式碼如下


var handle = XCreateWindow(display, rootWindow, 0, 0, xDisplayWidth, xDisplayHeight, 5,
    32,
    (int)CreateWindowArgs.InputOutput,
    visual,
    (nuint)valueMask, ref xSetWindowAttributes);


var window1 = new FooWindow(handle, display);


var window2 = new FooWindow(XCreateWindow(display, rootWindow, 0, 0, xDisplayWidth, xDisplayHeight, 5,
    32,
    (int) CreateWindowArgs.InputOutput,
    visual,
    (nuint) valueMask, ref xSetWindowAttributes), display);


while (true)
{
    var xNextEvent = XNextEvent(display, out var @event);
    if (xNextEvent != 0)
    {
        break;
    }

    if (@event.type == XEventName.MotionNotify)
    {
        var x = @event.MotionEvent.x;
        var y = @event.MotionEvent.y;

        if (@event.MotionEvent.window == window1.Window)
        {
            XDrawLine(display, window1.Window, window1.GC, x, y, x + 100, y);
        }
        else
        {
            var xEvent = new XEvent
            {
                MotionEvent =
                {
                    type = XEventName.MotionNotify,
                    send_event = true,
                    window = window1.Window,
                    display = display,
                    x = x,
                    y = y
                }
            };
            XSendEvent(display, window1.Window, propagate: false, new IntPtr((int)(EventMask.ButtonMotionMask)),
                ref xEvent);
        }
    }
}

class FooWindow
{
    public FooWindow(nint windowHandle, nint display)
    {
        Window = windowHandle;

        XEventMask ignoredMask = XEventMask.SubstructureRedirectMask | XEventMask.ResizeRedirectMask |
                                 XEventMask.PointerMotionHintMask;
        var mask = new IntPtr(0xffffff ^ (int)ignoredMask);
        XSelectInput(display, windowHandle, mask);

        XMapWindow(display, windowHandle);
        XFlush(display);

        var screen = XDefaultScreen(display);
        var white = XWhitePixel(display, screen);

        var gc = XCreateGC(display, windowHandle, 0, 0);
        XSetForeground(display, gc, white);

        GC = gc;
    }

    public nint Window { get; }
    public IntPtr GC { get; }
}

以上程式碼放在 githubgitee 上,可以使用如下命令列拉取程式碼

先建立一個空資料夾,接著使用命令列 cd 命令進入此空資料夾,在命令列裡面輸入以下程式碼,即可獲取到本文的程式碼

git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin c8354f643998d01eed8f56757a558623e4d94a8a

以上使用的是 gitee 的源,如果 gitee 不能訪問,請替換為 github 的源。請在命令列繼續輸入以下程式碼,將 gitee 源換成 github 源進行拉取程式碼

git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
git pull origin c8354f643998d01eed8f56757a558623e4d94a8a

獲取程式碼之後,進入 DikalehebeekaJaqunicobo 資料夾,即可獲取到原始碼

以上測試的是相同程序內的,跨程序其實也可以,只需要獲取其他程序的視窗對應的指標即可。其實在這裡我不確定 X11 的視窗 IntPtr 是否稱為指標是合適的。但行為上看起來和 Windows 下的控制代碼非常類似

如以下的測試程式碼,啟動自身作為新的程序,然後傳入當前程序的視窗,讓另一個程序獲取當前程序的視窗,接著測試在另一個程序將滑鼠訊息傳送到當前程序上

var handle = XCreateWindow(display, rootWindow, 0, 0, xDisplayWidth, xDisplayHeight, 5,
    32,
    (int)CreateWindowArgs.InputOutput,
    visual,
    (nuint)valueMask, ref xSetWindowAttributes);


var window1 = new FooWindow(handle, display);
XSync(display, false);

IntPtr window2Handle = IntPtr.Zero;

if (args.Length == 0)
{
    var currentProcess = Process.GetCurrentProcess();
    var mainModuleFileName = currentProcess.MainModule!.FileName;
    Process.Start(mainModuleFileName, [window1.Window.ToString(), window1.GC.ToString()]);
}
else if (args.Length == 2)
{
    if (long.TryParse(args[0], out var otherProcessWindowHandle))
    {
        window2Handle = new IntPtr(otherProcessWindowHandle);
    }
}

while (true)
{
    var xNextEvent = XNextEvent(display, out var @event);
    if (xNextEvent != 0)
    {
        Console.WriteLine($"xNextEvent {xNextEvent}");
        break;
    }

    if (@event.type == XEventName.Expose)
    {
        if (args.Length == 0)
        {
            XDrawLine(display, window1.Window, window1.GC, 0, 0, 100, 100);
        }
    }
    else if (@event.type == XEventName.MotionNotify)
    {
        var x = @event.MotionEvent.x;
        var y = @event.MotionEvent.y;

        if (window2Handle != 0 && window2GCHandle != 0)
        {
            // 繪製是無效的
            //XDrawLine(display, window2Handle, window2GCHandle, x, y, x + 100, y);

            var xEvent = new XEvent
            {
                MotionEvent =
                {
                    type = XEventName.MotionNotify,
                    send_event = true,
                    window = window2Handle,
                    display = display,
                    x = x,
                    y = y
                }
            };
            XSendEvent(display, window2Handle, propagate: false, new IntPtr((int)(EventMask.ButtonMotionMask)),
                ref xEvent);
        }
        else
        {
            XDrawLine(display, window1.Window, window1.GC, x, y, x + 100, y);
        }
    }
}

放在 githubgitee 上,可以使用如下命令列拉取程式碼

先建立一個空資料夾,接著使用命令列 cd 命令進入此空資料夾,在命令列裡面輸入以下程式碼,即可獲取到本文的程式碼

git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin ec8242cfe08a0eb23ba637c655083fceb0a8edb3

以上使用的是 gitee 的源,如果 gitee 不能訪問,請替換為 github 的源。請在命令列繼續輸入以下程式碼,將 gitee 源換成 github 源進行拉取程式碼

git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
git pull origin ec8242cfe08a0eb23ba637c655083fceb0a8edb3

獲取程式碼之後,進入 DikalehebeekaJaqunicobo 資料夾,即可獲取到原始碼

透過以上測試可以發現 X11 的滑鼠輸入是完全可以進行模擬輸入的,只需要拿到視窗指標,使用 XSendEvent 進行傳送即可

再進一步的實驗,也許大家也發現上面程式碼裡面有被我註釋的 XDrawLine 的呼叫。在 XDrawLine 裡面也是傳入 GC 和 Window 指標即可繪製線段,我就想著如果傳入別的程序的視窗是否適合,是否就能在其他程序的視窗上繪製出內容

我嘗試從另一個程序將 GC 傳回來,如下面程式碼

IntPtr window2Handle = IntPtr.Zero;
IntPtr window2GCHandle = IntPtr.Zero;

if (args.Length == 0)
{
    var currentProcess = Process.GetCurrentProcess();
    var mainModuleFileName = currentProcess.MainModule!.FileName;
    Process.Start(mainModuleFileName, [window1.Window.ToString(), window1.GC.ToString()]);
}
else if (args.Length == 2)
{
    if (long.TryParse(args[0], out var otherProcessWindowHandle))
    {
        window2Handle = new IntPtr(otherProcessWindowHandle);
    }

    if (long.TryParse(args[1], out var otherProcessGCHandle))
    {
        window2GCHandle = new IntPtr(otherProcessGCHandle);
    }
}

  ... // 忽略其他程式碼

         XDrawLine(display, window2Handle, window2GCHandle, x, y, x + 100, y);

此時發現執行程式碼,進入到 XDrawLine 報段錯誤,程序掛掉。原因是 gc 指標看起來是不能跨程序使用的,以上程式碼放在 githubgitee 上,可以使用如下命令列拉取程式碼

先建立一個空資料夾,接著使用命令列 cd 命令進入此空資料夾,在命令列裡面輸入以下程式碼,即可獲取到本文的程式碼

git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin c397b872a4d2cba187e1c04f7b015c8b2ca7092c

以上使用的是 gitee 的源,如果 gitee 不能訪問,請替換為 github 的源。請在命令列繼續輸入以下程式碼,將 gitee 源換成 github 源進行拉取程式碼

git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
git pull origin c397b872a4d2cba187e1c04f7b015c8b2ca7092c

獲取程式碼之後,進入 DikalehebeekaJaqunicobo 資料夾,即可獲取到原始碼

嘗試自己程序建立 GC 指標,如以下核心程式碼

IntPtr window2Handle = IntPtr.Zero;
IntPtr window2GCHandle = IntPtr.Zero;

if (args.Length == 0)
{
    var currentProcess = Process.GetCurrentProcess();
    var mainModuleFileName = currentProcess.MainModule!.FileName;
    Process.Start(mainModuleFileName, [window1.Window.ToString(), window1.GC.ToString()]);
}
else if (args.Length == 2)
{
    if (long.TryParse(args[0], out var otherProcessWindowHandle))
    {
        window2Handle = new IntPtr(otherProcessWindowHandle);
    }

    //if (long.TryParse(args[1], out var otherProcessGCHandle))
    //{
    //    window2GCHandle = new IntPtr(otherProcessGCHandle);
    //}
    // 不用別人傳的,從視窗進行建立
    window2GCHandle = XCreateGC(display, window2Handle, 0, 0);
    Console.WriteLine($"XCreateGC Window2 {window2GCHandle}");
}

如此程式碼經過實際測試發現沒有任何效果,當然了,也不會導致當前程序掛掉。以上程式碼放在 githubgitee 上,可以使用如下命令列拉取程式碼

先建立一個空資料夾,接著使用命令列 cd 命令進入此空資料夾,在命令列裡面輸入以下程式碼,即可獲取到本文的程式碼

git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin f0cb9bd3b4e4e9184fed831bdd84ef7e4b103888

以上使用的是 gitee 的源,如果 gitee 不能訪問,請替換為 github 的源。請在命令列繼續輸入以下程式碼,將 gitee 源換成 github 源進行拉取程式碼

git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
git pull origin f0cb9bd3b4e4e9184fed831bdd84ef7e4b103888

獲取程式碼之後,進入 DikalehebeekaJaqunicobo 資料夾,即可獲取到原始碼

更多 X11 開發請參閱 部落格導航

相關文章