Имитировать щелчок мышью в MSPaint

У меня есть консольное приложение, которое должно рисовать случайную картинку в MSPaint (мышь вниз -> позволить курсору произвольно нарисовать что-то -> мышь вверх. Это то, что у меня есть до сих пор (я добавил комментарии к методу Main для лучшего понимания, что я хочу достигать):

[DllImport("user32.dll", CallingConvention = CallingConvention.StdCall)]
public static extern void mouse_event(long dwFlags, uint dx, uint dy, long cButtons, long dwExtraInfo);
private const int MOUSEEVENTF_LEFTDOWN = 0x201;
private const int MOUSEEVENTF_LEFTUP = 0x202;
private const uint MK_LBUTTON = 0x0001;

public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr parameter);

[DllImport("user32.dll", SetLastError = true)]
static extern IntPtr FindWindow(string lpClassName, string lpWindowName);

[DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr FindWindowEx(IntPtr parentHandle, IntPtr childAfter, string className, string windowTitle);

[DllImport("user32.dll", CharSet = CharSet.Auto)]
static extern IntPtr SendMessage(IntPtr hWnd, UInt32 Msg, IntPtr wParam, IntPtr lParam);

[DllImport("user32.dll", SetLastError = true)]
public static extern bool EnumChildWindows(IntPtr hwndParent, EnumWindowsProc lpEnumFunc, IntPtr lParam);

static IntPtr childWindow;

private static bool EnumWindow(IntPtr handle, IntPtr pointer)
{
    childWindow = handle;
    return false;
}

public static void Main(string[] args)
{
    OpenPaint(); // Method that opens MSPaint
    IntPtr hwndMain = FindWindow("mspaint", null);
    IntPtr hwndView = FindWindowEx(hwndMain, IntPtr.Zero, "MSPaintView", null);
    // Getting the child windows of MSPaintView because it seems that the class name of the child isn't constant
    EnumChildWindows(hwndView, new EnumWindowsProc(EnumWindow), IntPtr.Zero);
    Random random = new Random();
    Thread.Sleep(500);

    // Simulate a left click without releasing it
    SendMessage(childWindow, MOUSEEVENTF_LEFTDOWN, new IntPtr(MK_LBUTTON), CreateLParam(random.Next(10, 930), random.Next(150, 880)));
    for (int counter = 0; counter < 50; counter++)
    {
        // Change the cursor position to a random point in the paint area
        Cursor.Position = new Point(random.Next(10, 930), random.Next(150, 880));
        Thread.Sleep(100);
    }
    // Release the left click
    SendMessage(childWindow, MOUSEEVENTF_LEFTUP, new IntPtr(MK_LBUTTON), CreateLParam(random.Next(10, 930), random.Next(150, 880)));
}

Я получил этот код для имитации клика из здесь.

Щелчок имитируется, но ничего не рисует. Кажется, что щелчок не работает внутри MSPaint. Курсор меняется на «крест» MSPaint, но, как я уже упоминал... щелчок не работает.

FindWindow устанавливает значение hwndMain равным 0. Изменение параметра mspaint на MSPaintApp ничего не меняет. Значение hwndMain остается равным 0.

Если это поможет, вот мой метод OpenPaint():

private static void OpenPaint()
{
    Process.process = new Process();
    process.StartInfo.FileName = "mspaint.exe";
    process.StartInfo.WindowStyle = "ProcessWindowStyle.Maximized;
    process.Start();
}

Что я делаю не так?


person diiN__________    schedule 02.12.2016    source источник
comment
Шаг первый: попробуйте, если оно работает лучше с каким-либо другим приложением, чем Paint, а затем сообщите!   -  person TaW    schedule 03.12.2016
comment
Привет! Мне нравится этот вопрос, и мне любопытно, вы уже нашли на него ответ или он все еще открыт? Если вы не нашли ответ сейчас, я попробую сегодня вечером сам.   -  person TripleEEE    schedule 05.12.2016
comment
@TripleEEE Я еще не нашел ответа. Я не могу проверить, потому что я болен..   -  person diiN__________    schedule 06.12.2016
comment
Привет @diiN_ только что опубликовал ответ;)   -  person TripleEEE    schedule 06.12.2016


Ответы (2)


Как и было обещано, я вчера сам протестировал - если честно, мой курсор просто двигался, но не в окне и без какого-либо влияния - при отладке я увидел, что var hwndMain = FindWindow("mspaint ", null); было значением 0. Я думал, что это должно быть проблемой, поэтому я взглянул на другую тему stackoverflow, из которой вы получили свой код. Я понял, что решение использует другое имя окна, которое они искали в FindWindow(), поэтому я попытался.

var hwndMain = FindWindow("MSPaintApp", null);

После изменения вызова метода это сработало для меня - хотя - после перемещения MsPaint туда курсор все еще оставался в исходной открытой позиции - вы можете подумать об этом и, возможно, спросить окно о его позиции. Возможно, имя изменилось с Win7/8/10?

Изменить:

В Windows 10 название Paint, кажется, изменилось — так что я думаю, у вас все еще есть проблемы с получением правильного дескриптора окна — это было доказано Хансом Пассантом, который хорошо объяснил, в чем проблема с обработчиком (ссылка ниже). Один из способов решить эту проблему — получить обработчик из самого процесса, а не из FindWindow().

Я предлагаю вам изменить OpenPaint() следующим образом:

 private IntPtr OpenPaint()
 {
    Process process = new Process();
    process.StartInfo.FileName = "mspaint.exe";
    process.StartInfo.WindowStyle = ProcessWindowStyle.Maximized;
    process.Start();
    // As suggested by Thread Owner Thread.Sleep so we get no probs with the handle not set yet
    //Thread.Sleep(500); - bad as suggested by @Hans Passant in his post below, 
    // a much better approach would be WaitForInputIdle() as he describes it in his post.         
    process.WaitForInputIdle();
    return process.MainWindowHandle;
 }

Ссылка на Hans Passant decription для объяснения, почему Thread.Sleep() просто плохая идея.

Далее последовал звонок:

IntPtr hwndMain = OpenPaint(); // Method that opens MSPaint

Таким образом, вы должны получить правильный дескриптор окна, и ваш код должен работать, независимо от того, как Microsoft назвала его в win10.

person TripleEEE    schedule 06.12.2016
comment
У меня было такое раньше, но тогда это вообще не работало. При использовании Console.WriteLine(process.ProcessName); выводится mspaint. Курсор перемещается внутри краски, но не щелкает... - person diiN__________; 07.12.2016
comment
Но вы не спрашиваете имя процесса — вы ищете имя окна, это нечто другое. Если параметр lpWindowName не равен NULL, FindWindow вызывает функцию GetWindowText, чтобы получить имя окна для сравнения См.: msdn.microsoft.com/de-de/library/windows/desktop/ - person TripleEEE; 07.12.2016
comment
@diiN_ может быть, это поможет, если вы предоставите код ` OpenPaint(); // Метод, который открывает MSPaint` - возможно, есть какая-то ошибка. Кстати: вы отлаживали, чтобы увидеть, возвращает ли FindWindow дескриптор или он равен нулю? - person TripleEEE; 07.12.2016
comment
@diiN_ Хорошо, я попробую, у тебя Win7/8/10? - person TripleEEE; 07.12.2016
comment
Я использую Вин 10. - person diiN__________; 07.12.2016
comment
@diiN_ хорошо - может быть, поэтому имя не работает - я отредактирую свой ответ - есть другой (я думаю, даже лучший способ сделать это) - person TripleEEE; 07.12.2016
comment
Большое спасибо. Единственное, что мне пришлось добавить к вашему решению, это Thread.Sleep(500); перед возвратом дескриптора. Не дожидаясь, возвращаемое значение осталось равным 0. Теперь это работает. - person diiN__________; 07.12.2016
comment
@diiN_ Хорошо, я могу тебе помочь! Я добавил Thread.Sleep() к своему ответу для других, проходящих мимо;) - person TripleEEE; 07.12.2016

IntPtr hwndMain = FindWindow("mspaint", null);

Это не достаточно хорошо. Распространенная ошибка в коде pinvoke: программисты на C#, как правило, слишком сильно полагаются на исключение, чтобы спрыгнуть с экрана и дать им пощечину, чтобы сказать им, что что-то пошло не так. .NET Framework делает это необычайно хорошо. Но это не работает так же, когда вы используете API, основанный на языке C, например winapi. C — язык-динозавр, и он вообще не поддерживал исключения. Это все еще не так. Вы получите исключение только в случае сбоя подключения pinvoke, обычно из-за неправильного объявления [DllImport] или отсутствующей DLL. Он не сообщает об успешном выполнении функции, но возвращает код возврата ошибки.

Это делает полностью вашей собственной работой обнаружение и сообщение о сбое. Просто обратитесь к документации MSDN, он всегда говорит вам, как функция winapi указывает на сбой. Не совсем совместимо, поэтому вам нужно посмотреть, в этом случае FindWindow возвращает null, когда окно не может быть найдено. Поэтому всегда кодируйте это так:

IntPtr hwndMain = FindWindow("mspaint", null);
if (hwndMain == IntPtr.Zero) throw new System.ComponentModel.Win32Exception();

Сделайте это и для всех остальных пинвоков. Теперь вы можете двигаться вперед, вы гарантированно получите исключение вместо того, чтобы работать с неверными данными. Что, как это часто бывает с плохими данными, не так уж и плохо. NULL на самом деле является допустимым дескриптором окна, ОС предположит, что вы имели в виду окно рабочего стола. Ой. Вы автоматизируете совершенно неправильный процесс.


Понимание того, почему FindWindow() терпит неудачу, требует некоторого понимания, это не очень интуитивно понятно, но хорошее сообщение об ошибках имеет решающее значение. Метод Process.Start() только гарантирует, что программа запущена, он никоим образом не ждет, пока процесс завершит свою инициализацию. И в этом случае он не ждет, пока создаст свое главное окно. Таким образом, вызов FindWindow() выполняется примерно на пару десятков миллисекунд раньше. Дополнительное недоумение, поскольку он отлично работает при отладке и пошаговом выполнении кода.

Возможно, вам знакома такая ошибка, это ошибка гонки потоков. Самый подлый вид программной ошибки. Печально известен тем, что не вызывает постоянных сбоев и его очень сложно отлаживать, поскольку гонки зависят от времени.

Надеюсь, вы понимаете, что предложенное решение в принятом ответе тоже недостаточно хорошо. Произвольное добавление Thread.Sleep(500) просто повышает вероятность того, что теперь вы будете ждать достаточно долго, прежде чем вызывать FindWindow(). Но откуда вы знаете, что 500 достаточно? Достаточно всегда?

Нет. Thread.Sleep() никогда не является правильным решением для ошибки гонки потоков. Если машина пользователя медленная или слишком сильно загружена из-за нехватки доступной неотображенной оперативной памяти, то пара миллисекунд превращается в секунды. Вам приходится иметь дело с наихудшим случаем, и это действительно наихудший случай, всего ~10 секунд — это обычно минимум, который вам нужно учитывать, когда машина начинает дергаться. Это становится очень непрактичным.

Надежная блокировка — настолько распространенная потребность, что ОС имеет для этого эвристику. Должен быть эвристическим, а не вызовом WaitOne() для объекта синхронизации, поскольку сам процесс вообще не взаимодействует. Как правило, вы можете предположить, что программа с графическим интерфейсом достаточно продвинулась, когда она начинает запрашивать уведомления. «Прокачка цикла сообщений» на просторечии Windows. Эта эвристика также попала в класс Process. Исправить:

private static void OpenPaint()
{
    Process.process = new Process();
    process.StartInfo.FileName = "mspaint.exe";
    process.StartInfo.WindowStyle = "ProcessWindowStyle.Maximized;
    process.Start();
    process.WaitForInputIdle();          // <=== NOTE: added
}

Было бы упущением, если бы я не указал, что вы должны использовать для этого встроенный API. Вызывается Автоматизация пользовательского интерфейса, умело обернутая в пространство имен System.Windows.Automation. Позаботится обо всех этих неприятных мелочах, таких как гонки потоков и превращение кодов ошибок в хорошие исключения. Наиболее подходящее руководство находится вероятно здесь.

person Hans Passant    schedule 07.12.2016