NOVINKA - Online rekvalifikační kurz Java programátor. Oblíbená a studenty ověřená rekvalifikace - nyní i online.
NOVINKA – Víkendový online kurz Software tester, který tě posune dál. Zjisti, jak na to!

Diskuze: Divné chování funkce SendInput() [Win32 API]

V předchozím kvízu, Online test znalostí C++, jsme si ověřili nabyté zkušenosti z kurzu.

Aktivity
Avatar
Horrigan
Člen
Avatar
Horrigan:26.9.2015 10:55

Zdravím,
narazil jsem na takový docela zajímavý problém, se kterým si tady lámu hlavu už asi týden a nemůžu s tím hnout. Udělal jsem si mininimální verzi daného problému, na které jde reprodukovat, dám jí na konec tototo příspěvku (UI je konzolovka psaná v C#, ale jde mi spíše o knihovnu, která je čistě céčková).
Vyvíjím aplikaci, která má (v minimální verzi) kliknout do okna prohlížeče (handler a souřadnice dodávám ručně přes Spy++) a následně poslat několik úderů do klávesnice, kde potom (v ostré verzi následuje další logika co s tím, v té minimální se jen chvíli počká) a následně další kliknutí na stejný prvek a poslání opět několik úderů do klávesnice aby se vybraný prvek vrátil do původního stavu.
Problémem je, že na mém stroji (který není nijak výjimečný) to funguje a běhá skvěle a přesně jak má, ovšem vezmu-li to na několik dalších počítačů (i s různými OS - W7/W10) tak nikoli i přesto, že návratová hodnota SendInput pošle správný počet úhozů, resp. eventů klávesnice a GetLastError vrací nulu. Ovšem, žádné úhozy se fyzicky u cíle neobjeví, kliknutí je v pořádku, ale když má posílat struktury přes SendInput, tak dělá že nic, ač vrací hodnoty správné.

Pokud si někdo bude potřebovat problém replikovat, ideálně to provádět na webové aplikaci společnosti HighLow (www.highlow.net [nedělám doufám nepovolenou reklamu, nic s nima nemám společnýho, jen potřebuju automatizovat užití jejich aplikace], kde je nutné si otevřít demo (to je zdarma a bez registrace) a následně běží o ten combobox s výběrem instrumentů co je vpravo (default tam je All).

Zná někdo, v čem by mohl být problém? Co je tom kódu špatně? Případně neznáte alernativu k SendInput(), která by udělala stejný job, ale spolehlivě? Díky moc

Zdrojáky zde:

****C# ovládacdí aplikace té knihovny, ****
nic složitého i když v tom neděláte, co ten kód má za úkol pochopí snad každý. Je to minimalistická verze, neošetřju výjimky na vstupu a podobný balast, se kterým se mi nechtěo psát, přeci jen smysl programu je jinde :)

  using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Runtime.InteropServices;
using System.Globalization;

namespace clickTest1
{
    class Program
    {
        enum SYMBOLS
        {
            AUDJPY, AUDNZD, CADJPY, CHFJPY, EURAUD, EURGBP,
            EURJPY, EURUSD, GBPAUD, GBPJPY, GBPUSD, NZDJPY,
            NZDUSD, USDCAD, USDCHF, USDJPY, GOLD, UNKNOWN
        };

        const int SW_SHOWMAXIMIZED = 3;
        const int SW_HIDE = 0;
        const uint SWP_NOSIZE = 0x0001;
        const uint SWP_NOMOVE = 0x0002;
        const uint SWP_SHOWWINDOW = 0x0040;


        [DllImport(@"tradeInterop.dll")]
        public static extern void PutSingleClick(IntPtr hwnd, int x, int y);
        [DllImport(@"tradeInterop.dll")]
        public static extern uint PutKeystroke(IntPtr shouldBeNULL, int times, int isDown, out int err);
        [DllImport(@"user32.dll")]
        public static extern int SetWindowPos(IntPtr hwnd, IntPtr precendingHwnd, int x, int y, int cx, int cy, uint flags);
        [DllImport(@"user32.dll")]
        public static extern int ShowWindow(IntPtr hwnd, int cmdShow);
        [DllImport(@"user32.dll")]
        public static extern IntPtr GetParent(IntPtr hwnd);
        static void Main(string[] args)
        {
            UInt32 handler = 0;
            int x, y = 0;
            int keystrokesCount = 0;
            IntPtr HWND_BOTTOM = new IntPtr(1);
            IntPtr HWND_TOPMOST = new IntPtr(-1);
            UInt32 originalHandler = 0;
            while (true)
            {
                Console.WriteLine("H WND (hexadecimal) 0 = exit: ");
                handler = UInt32.Parse(Console.ReadLine(), NumberStyles.HexNumber, CultureInfo.InvariantCulture);
                if (handler == 0)
                {
                    ShowBrowser((IntPtr)originalHandler, IntPtr.Zero);
                    break;
                }
                originalHandler = handler;
                HideBrowser((IntPtr)handler, HWND_BOTTOM);
                Console.WriteLine("X: ");
                x = int.Parse(Console.ReadLine());
                Console.WriteLine("Y: ");
                y = int.Parse(Console.ReadLine());
                Console.WriteLine("Choose instrument index (e.g. EURUSD=7)");
                keystrokesCount = int.Parse(Console.ReadLine());
                Console.WriteLine("Getting browser maximized and topomost");

                ShowBrowser((IntPtr)handler, HWND_TOPMOST);

                Console.WriteLine("Clicking on combobox");
                PutSingleClick((IntPtr)handler, x, y);
                Thread.Sleep(1000); // for content of combo full load
                Console.WriteLine("Putting keystrokes");
                int lastErr = -1;
                uint strokesPut = PutKeystroke(IntPtr.Zero, keystrokesCount, 1, out lastErr); // 1 = TRUE, is key down

                System.Threading.Thread.Sleep(5000);
                HideBrowser((IntPtr)handler, HWND_BOTTOM);

                Console.WriteLine("Keys down pressed, {0} would be choosen", (SYMBOLS)keystrokesCount);
                Console.WriteLine("From library: Sent {0} strokes, error: {1}", strokesPut, lastErr);
                Console.WriteLine("Press enter to continue");
                Console.ReadLine();

                ShowBrowser((IntPtr)handler, HWND_TOPMOST);
                PutSingleClick((IntPtr)handler, x, y);
                Thread.Sleep(1000);
                strokesPut = PutKeystroke(IntPtr.Zero, keystrokesCount + 5, 0, out lastErr);

                System.Threading.Thread.Sleep(5000);
                HideBrowser((IntPtr)handler, HWND_BOTTOM);

                Console.WriteLine("Keys up pressed, ALL would be choosen");
                Console.WriteLine("From library: Sent {0} strokes, error: {1}", strokesPut, lastErr);
                Console.WriteLine("Press enter to continue");
                Console.ReadLine();
            }
        }

        private static void ShowBrowser(IntPtr hwnd, IntPtr precHwnd)
        {
            Console.WriteLine("Showing browser...");
            ShowWindow(GetParent(hwnd), SW_SHOWMAXIMIZED);
            SetWindowPos(GetParent(hwnd), precHwnd, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW);
        }

        private static void HideBrowser(IntPtr hwnd, IntPtr precHwnd)
        {
            Console.WriteLine("Hiding browser...");
            SetWindowPos(GetParent(hwnd), precHwnd, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW);
            ShowWindow(GetParent(hwnd), SW_HIDE);
        }
    }

**knihovna, resp. relevantní část ****

int _stdcall DllMain(HINSTANCE hInst, DWORD fdwReason, PVOID pvReserved)
{
    return true;
}

extern "C" __declspec(dllexport) void __stdcall PutSingleClick(HWND hwnd, int x, int y, BOOL needFocus = TRUE)
{
    if(needFocus == TRUE)
    {
        SendMessage(hwnd, WM_LBUTTONDOWN, MK_LBUTTON, MAKELPARAM(0,0));
        SendMessage(hwnd, WM_LBUTTONUP, MK_LBUTTON, MAKELPARAM(0,0));
    }
    SendMessage(hwnd, WM_LBUTTONDOWN, MK_LBUTTON, MAKELPARAM(x, y));
    SendMessage(hwnd, WM_LBUTTONUP, MK_LBUTTON, MAKELPARAM(x, y));

}
extern "C" __declspec(dllexport) UINT __stdcall PutKeystroke(HWND hnd, int index, BOOL isDown, int& err)
{
    INPUT ip = {0};
    ip.type = INPUT_KEYBOARD;
    ip.ki.wVk =isDown? VK_DOWN : VK_UP;
    int times;
    if(index < 2)
        times = index+1;
    else
        times = index+2;

    INPUT ips[100] = {0};

    for(int i = 0; i < times*2; i+=2)
    {
        ip.ki.dwFlags = 0;
        ips[i] = ip;

        ip.ki.dwFlags = KEYEVENTF_KEYUP;
        ips[i+1] = ip;
    }

    ip.ki.dwFlags = 0;
    ip.ki.wVk = VK_RETURN;
    ips[times*2] = ip;

    ip.ki.dwFlags = KEYEVENTF_KEYUP;
    ips[(times*2)+1] = ip;
    err = -1;
    UINT s = SendInput((times*2)+2, &ips[0], sizeof(INPUT));
    err = GetLastError();
    return s;
}

Tuší někdo, v čem je problém, popř. co zkusit? Já už jsem úplně v koncích a bez nápadu, a bez vyřešení tohoto s projektem dále nepohnu, je téměř hotový, zbývá jen toto odřešit. A je mi fakt strašně divný, že to u mně funguje dobře (takže kód nijak moc zprasený být nemůže), ale jinde ne.

Za každou pomoc a dobře míněnou radu děkuji

 
Odpovědět
26.9.2015 10:55
Avatar
Lukáš Hruda
Tvůrce
Avatar
Odpovídá na Horrigan
Lukáš Hruda:26.9.2015 13:48

Osobně jsem pro tenhle účel kdysi používal funkci keybd_event.

 
Nahoru Odpovědět
26.9.2015 13:48
Avatar
Horrigan
Člen
Avatar
Horrigan:26.9.2015 14:40

Fajn, díky za tip, zkusím to s tím :) I když MSDN tvrdí, že je zastaralá a nahrazená právě fcí SendInput. Co s druhým parametrem, bScanCode? Prý to aplikace většinou ingnorují, mám spy-nout LPARAM eventu příchozího z fyzického stisku klávesy a předat ho, nebo je to zbytečný a opravdu se to ignoruje (takže předaná prostá nula bude stačit)?. Jak jsi to používal?

Navíc - tam nepošlu všechny keyeventy naráz jako při sendinput, takže ve smyčce? "nezaspamuju" vstupní frontu? Těch stisků je kolem 20 (max) párů press/release, takže 40 eventů, je rozumný to hodit do smyčky? Někde jsem četl, že práve sendinput tohle nemá úplně nejradši kvůli "spamu" smyčky událostí, že je pak ochotný mi některé eventy zahodit, resp. nezpracovat

Dám vědět, zda pomohlo

 
Nahoru Odpovědět
26.9.2015 14:40
Avatar
Lukáš Hruda
Tvůrce
Avatar
Odpovídá na Horrigan
Lukáš Hruda:26.9.2015 16:14

Zkus to a uvidíš. Nepamatuju si přesně jak se ta funkce používá, najdi si dokumentaci.

 
Nahoru Odpovědět
26.9.2015 16:14
Avatar
Horrigan
Člen
Avatar
Horrigan:30.9.2015 10:23

Tak jsem to zkusil s tím keybd_eventem a stále stejný problém.

Zkusil jsem to ještě se C#ovým SendKeys.SendWait z Sys.Win.Forms, a chová se to taky stejně - u mně jo, ale jinde ne.

Ještě mně napadla podstatná maličkost: já si sice forcuju okno kam potřebuju psát na topmost
pomocí ShowWindow, resp. SetWindowPos (viz ten C# úsek kódu jak si tam schovávám a ukazuju okno prohlížeče), ale přijde mi, jako by to psal do okna konzole programu. To by i vysvětlovalo, proč ta SendInput (původní), ale i všechny další pokusy. Kliknutí je správně, ten combo se rozjede na browseru (ale taky to je správně, protože na SendClicks předávám handler), ale úhozy vypadá, že zadává jinam - ty ENTERy na konci si totiž provede konzolovka sama, nepočká, až je dostane na vstupu.

Nějaký nápad, jak vynutit, aby ten SendInput/keyb­d_event, whatever se směroval správně?

 
Nahoru Odpovědět
30.9.2015 10:23
Avatar
Martin Dráb
Tvůrce
Avatar
Odpovídá na Horrigan
Martin Dráb:30.9.2015 17:00

S těmihle věcmi jsem si nikdy moc nehrál, ale zdá se mi, že tam u posílání proudu klávesových událostí nikde nespecifikuješ okno, kterému se mají doručit (žádné SetForegroundWin­dows třeba).

Teoreticky by také mohl být problém v tom, že integrity level procesu prohlížeče je vyšší než IL tvého programu (ať už kvůli UI Privilege Isolation nebo proto, že prohlížeč prošel UAC dialogem a tvoje aplikace ne). Ale to je taková hodně teoretická možnost a nepředpokládám, že v ní tkví tvůj problém.

Nahoru Odpovědět
30.9.2015 17:00
2 + 2 = 5 for extremely large values of 2
Avatar
Horrigan
Člen
Avatar
Horrigan:30.9.2015 21:30

Jo, chybějící SetForegroundWin­dow(HWND), tam chybí, dopsal jsem ho tam a opravdu se to začíná posílat kam je potřeba (ještě to není dokonalý, prý to hýbe vertikálním posuvníkem stránky a a ne opšnama v comboboxu, ale už snad má správnou cestu). Měl jsem za to, že si ten focus vynutí právě kliknutí na combo, ale tak jsem si to myslel BLBĚ :) Ještě není úplně dokonale zřejmý, zda je potřeba ten SetForegroundWindow hodit na handler zobrazené karty nebo na jeho rodiče (nemám to jak otestovat, nemám náhradní stroj dneska, ale tak snad se metodou pokusů a omylů konečně (snad po deseti dnech, co tady řeším takovouhle "blbost") dojdeme k funčnímu řešení.

Díky oběma za čas a nápady.

 
Nahoru Odpovědět
30.9.2015 21:30
Děláme co je v našich silách, aby byly zdejší diskuze co nejkvalitnější. Proto do nich také mohou přispívat pouze registrovaní členové. Pro zapojení do diskuze se přihlas. Pokud ještě nemáš účet, zaregistruj se, je to zdarma.

Zobrazeno 7 zpráv z 7.