страничка tripsin'а


Главная | Статьи | Заметки | Файлы | Ссылки | я

Приручение мыши. Имитация ввода пользователя.

Почему-то очень часто на форуме возникает вопрос: «Как мне программно кликнуть мышкой?» или «Как мне программно нажать на кнопку в другом приложении?» Пора положить этому конец! :)  Здесь мы подробно разберем 2 способа. У каждого есть свои достоинства и недостатки. Какой удобнее, такой и пользуй.

1 Способ. Посылка сообщений.

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

procedure TForm1.Button2MouseUp(Sender: TObject; Button: TMouseButton;
    Shift: TShiftState; X, Y: Integer);
begin
if Button = mbLeft then begin // Жмем левую кнопку
    SendMessage(button1.Handle, WM_LBUTTONDOWN, 0,0);
    Sleep(1000); // Application.ProcessMessages;
    SendMessage(button1.Handle, WM_LBUTTONUP, 0,0);
end;
if Button = mbRight then begin // Жмем правую кнопку
    SendMessage(button1.Handle, WM_RBUTTONDOWN, 0,0);
    Sleep(1000);
    SendMessage(button1.Handle, WM_RBUTTONUP, 0,0);
end;
end;

Здесь при нажатии на Button2 имитируется нажатие на Button1, причем нажатия на левую и правую кнопки мыши обрабатываются отдельно. Sleep(1000) – вставлено, чтобы ты лучше увидел, как кнопочка нажимается. Для посылки сообщения используется API-функция SendMessage.

В 1 параметре передается хэндл окна, в которое посылаем сообщение (кнопка ведь разновидность окна). Если вместе хэндла поставить HWND_BROADCAST, то сообщение придет всем окнам верхнего уровня. Дочерние окна (к которым относятся и кнопки) сообщения не получат. Так что нажать сразу на все кнопки на экране не получится. :)

Во 2 параметре – собственно номер сообщения. Кроме указанных сообщений можно послать еще: WM_LBUTTONDBLCLK, WM_RBUTTONDBLCLK, WM_MBUTTONDBLCLK, WM_MOUSEWHEEL, WM_XBUTTONDOWN, WM_XBUTTONUP, WM_XBUTTONDBLCLK. Думаю назначение их понятно из названий.

Вместо нуля в 3 параметре (wParam) можно передать информацию о дополнительно нажатых кнопках. Это может быть сочетание (через or) следующих флагов:

Последние 2 флага действуют только в последних версия винды: Windows 2000/XP, ну и наверное в Vista :)

В 4 параметре (lParam) передаются координаты клика в виде примерно структуры TSmallPoint.

После того, как оконная функция программы обработает сообщение,  SendMessage возвращает 0. Чтобы не ждать конца обработки сообщения, можно просто поместить сообщение в очередь с помощью PostMessage (она возвращает управление сразу). Параметры у нее те же.

Впрочем, в собственной программе можно просто вызвать обработчик нажатия кнопки напрямую, но тогда ты не увидишь, как нажимается кнопочка. С другими приложениями может возникнуть проблема с поиском окошка. Во-первых, надо вообще знать какое конкретно окно тебе нужно. Искать окна можно с помощью EnumWindows, FindWindow, FindWindowEx.


2 Способ. Помещение  информации непосредственно во входной буфер мыши.

Мы имеем возможность воздействовать непосредственно на мышь и симулировать любые ее события. При этом нам не надо будет искать окна. Мы сможем воздействовать на любое приложение, двигать мышью, кликать иконки на экране, т.е. все, что может мышь сделать, мы можем сымитировать. Обычно для этого используется функция: mouse_event. (Ее описание ты сможешь найти в различных хелпах, например DRKB)

Но в NT/2000/XP-системах MSDN рекомендует использовать новую функцию SendInput (хотя она появилась еще в Windows 98). Ее-то мы и рассмотрим. Кроме мыши эта функция может симулировать клавиатуру и другие устройства ввода. Вообще довольно трудно придумать мирное применение имитации нажатия клавиш и кнопок мыши. Кроме шуточных приколов, я думаю, эта тема может пригодиться в программе удаленного визуального администрирования (типа Remote Administrator). Приведенное ниже описание фактически адаптированный для Delphi перевод MSDN.

Синтаксис. Для Delphi определен в Windows.pas (все остальные структуры там же):

function SendInput(cInputs: UINT;var pInputs: TInput;cbSize: Integer): UINT; stdcall;

Параметры:

  1. cInputs - Устанавливает число структур TInput, передаваемых в массиве во второй параметр.
  2. pInputs - Массив структур TInput. Описание смотри ниже. За один вызов функции можно создать много событий и их последовательность заносится в этот массив. Вообще сюда передается указатель на этот массив, если вызывать функию по нормальному на Си, поэтому в качестве указателя надо указать первый элемент массива. (Разработчики Delphi почему-то решили, что так удобнее.) Каждая структура обозначает событие, которое будет вставлено во входной  буфер клавиатуры или мыши. То есть можно сразу управлять и клавой и мышкой.
  3. cbSize - Размер структуры TInput в байтах.  Если размер будет указан неправильно, то функция завершится с ошибкой.

Функция возвращает число событий, которые она успешно вставило во входной поток мыши или клавиатуры. Если функция возвращает нуль, значит ввод данных был блокирован другим потоком. Чтобы получить дополнительные сведения об ошибке, вызывай функцию GetLastError.

Функция SendInput вставляет события из массива структур TInput последовательно во входной поток мыши или клавиатуры. Эти события не смешиваются с событиями ввода данных  от пользователя (когда он юзает клавиатуру или мышь), вызовами функций keybd_event, mouse_event, и другими вызовами SendInput.

Эта функция не сбрасывает текущее состояние клавиатуры. Любые кнопки, которые уже нажаты, когда функция вызывается, останутся нажатыми. Поэтому, чтобы избежать неприятностей  при работе с клавиатурой, проверяй состояние клавиатуры при помощи функции GetAsyncKeyState и  по мере необходимости корректируй свои действия.


Структура TInput передается  в функцию SendInput, и содержит информацию для синтезирования событий ввода типа нажатий клавиши, перемещения и нажатия кнопок мыши. Она является как-бы контейнером для трех других структур. В Си это называется объединение (union), т.е. используется одна из этих структур

Объявление

  tagINPUT = packed record
    Itype: DWORD;
    case Integer of 
      0: (mi: TMouseInput);
      1: (ki: TKeybdInput);
      2: (hi: THardwareInput);
  end;
  TInput = tagINPUT;

Члены структуры

Itype - Устанавливает тип события ввода и какая из трех структур будет использоваться Этот член может быть одним из нижеследующих значений.

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


Так как мы решили приручить мышь, то рассмотрим только структуру TMouseInput. Структура TMouseInput содержит информацию об имитируемом событии нажатия кнопки мыши и используется с объединении структуры TInput. Эта структура содержит информацию идентичную той, которая используется в списке параметров функции mouse_event.

Объявление.

  tagMOUSEINPUT = packed record
    dx: Longint;
    dy: Longint;
    mouseData: DWORD;
    dwFlags: DWORD;
    time: DWORD;
    dwExtraInfo: DWORD;
  end;
  TMouseInput = tagMOUSEINPUT;

Члены структуры

dx,  dy – Используются когда установлен флаг MOUSEEVENTF_MOVE, т.е. если ты хочешь, чтобы мышь переместилась. Устанавливают абсолютные или относительные координаты мыши в зависимости от флага MOUSEEVENTF_ABSOLUTE. Если он не установлен, то координаты определяют смещение от прежней позиции мыши. Смещение вводится не в пикселах, а непонятно в чем. Я честно говоря не понял. Смещал на 100 ед. – первый раз курсор сместился на 170 пикселов, а второй на 254. :(  Положительные значения обозначают смещение вправо и вниз, отрицательные – влево и вверх.

Если MOUSEEVENTF_ABSOLUTE установлен, поля dx и dy содержат нормализованные абсолютные координаты между 0 и 65 535. Координата (0,0) обозначает левый верхний угол экрана; координата (65535,65535) обозначает  нижний правый угол. Преобразовать координаты из пикселов в нормализованные ты можешь примерно так:

normalized_x := x_in_pixels * 65535 div screen_width;

 Во много экранной системе эти координаты относятся к главному монитору. Если хочешь использовать весь виртуальный рабочий стол (т.е. несколько мониторов), то установи флаг MOUSEEVENTF_VIRTUALDESK (это только в Windows 2000/XP).

mouseData – Передает данные для дополнительных кнопок и колесика. Значение этого члена опять зависит от флагов установленных в параметре dwFlags:

dwFlags – В этом поле устанавливается набор битовых флажков, которые определяют различные виды движений и щелчков мышки и содержимое остальных полей структуры. Биты в этом поле могут быть любой разумной комбинацией нижеследующих значений.

Не надо устанавливать состояние кнопок мыши каждый раз при перемещении. Есть при движении мыши левая кнопка должна быть нажата, то надо один раз установить  MOUSEEVENTF_LEFTDOWN перед движением, потом двигать мышой сколько надо и в конце отжать левую кнопку (MOUSEEVENTF_LEFTUP). Перечислю эти флаги. Часть из них уже была описана выше.

Следующие флаги есть только в новых версия виндов (>=Windows 2000/XP)

time - Отметка времени события, в миллисекундах. Если этот параметр = 0, система поставит свою собственную отметку времени. Практической пользу от него я не для себя не нашел. Кто знает - пишите. Как я понял это время передается в структуру TMsg при вызове GetMessage в цикле обработки сообщений.

dwExtraInfo - Устанавливает дополнительное значение, связанное с событием нажатия кнопки мыши. С помощью функции GetMessageExtraInfo можно получить эту дополнительную информацию. Например, для общения между прогами можно использовать сообщения, но в целях безопасности желательно знать, а от нужной ли проги пришло сообщение?  Так вот чтобы отличить поддельное сообщение можно устанавливать в этом параметре какой-нибудь идентификатор сессии (число то есть) Простые программы могут это делать с помошью SetMessageExtraInfo.

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


Хватит нудной теории! Сейчас мы забацаем в качестве примера, что-нибудь прикольное. Я обозвал прогу Mad Mouse.

 Она будет случайным образом перетаскивать объекты на экране: ярлыки, куски текста, файлы в броузере и т.п., что под руку (тьфу, по мышку :) ) попадется. Будет драг_энд_дропать по черному безо всяких там COM`ов и интерфейсов! Будет кликать по всем подряд и левой, и правой, и даблкликом, а еще временами жать Enter. На рабочем столе, да и на винте тоже, очень скоро наступит полный армагеддон (сокращенно – писец). Чем больше ярлыков на рабочем столе, тем интереснее эффект. Кстати прикол довольно жестокий, так что призываю использовать его только в исследовательских и познавательных целях. За использование тобой проги во вредных целях автор отвественности не несет.

Также сделаем в программе предохранитель от случайного запуска в виде мессаджбокса с вопросом. А еще прога будет завершаться при нажатии на ESC. От греха подальше, я отлаживал прогу на Windows 98 (в XP тоже все работает) в виртуальном компьютере VMvare, чего и тебе советую. Через десять минут рабочий стол превратился вот в это:

А через 20 минут Безумная Мышь установила несколько программ из дисрибутивов и приступила к «поиску и установке нового оборудования».

Создавай стандартный проект и удаляй из него нафиг Unit1 с его формой. Начинаем (ш)кодить без графического интерфейса.

program madmouse;
...

Обойдемся только API-функциями

...
uses Windows;
...

Объявим переменные.

...
var
  arrI: array [0..2] of TInput; 
...

Массив для событий ввода. Нам потребуется максимум 4 события на 1 шкод. Шкоды - это неделимые единицы пакостных действий. :)

...
  desk_dc: HDC;
  desk_wnd: HWND;
...

Контекст и хэндл окна экрана нужны, что бы получить его размеры.

...
  screen_width, screen_height: Integer;
...

Размеры экрана

...
  screen_width_factor, screen_height_factor: Integer;
...

Коэффициенты преобразований для структуры TMouseInput. Читай ее описание.

Дальше собственно исполняемый код. Сначала вставим наш предохранитель.

...
  if MessageBox(0, ‘Одумайся! Ты запускаешь программу Mad Mouse!’ +
                    #13#10 + ‘Если что - жми <ESC>’,
                    ‘Последнее предупреждение’,
                    MB_OKCANCEL or MB_DEFBUTTON2) = IDCANCEL
                    then ExitProcess(0);
...

Получим размеры экрана

...
begin
  desk_wnd := GetDesktopWindow;
  desk_dc := GetWindowDC(desk_wnd);
  screen_width := GetDeviceCaps(desk_dc,HORZRES);
  screen_height := GetDeviceCaps(desk_dc,VERTRES);
  screen_width_factor := 65535 div screen_width;
  creen_height_factor := 65535 div screen_height;
  ReleaseDC(desk_wnd, desk_dc);
...

Здесь мы получаем хэндл окна экрана. По нему получаем контекст экрана и извлекаем из него размеры. По размерам рассчитываем коэффициенты для преобразований.

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

...
  ZeroMemory(@arrI, SizeOf(TInput) * 4); 
  arrI[0].Itype := INPUT_MOUSE;
  arrI[1].Itype := INPUT_MOUSE;
  arrI[2].Itype := INPUT_MOUSE;
  arrI[3].Itype := INPUT_MOUSE;
  Randomize;
...

Запускаем основной рабочий цикл.

...
repeat
  arrI[0].mi.dx := Random(screen_width) * screen_width_factor;
  arrI[0].mi.dy := Random(screen_height) * screen_height_factor;
  n := 0;
  case Random(6) of
...

Координаты мышки в первом элементы массива будут нужных в любом случае, поэтому сразу пихаем в них случайные значения. В «n» будет определяться количество задаваемых сразу событий. Дальше случайно выбираем один из шести придуманных нами шкодов, и заполняем нужные поля в структурах для каждого события:

...
  0: // Просто двигаем мышу
    begin
      n := 1;
      arrI[0].mi.dwFlags := MOUSEEVENTF_MOVE
                            or MOUSEEVENTF_ABSOLUTE;
    end;
  1: // Левый клик
    begin
      n := 2;
      arrI[0].mi.dwFlags := MOUSEEVENTF_MOVE
                            or MOUSEEVENTF_ABSOLUTE
                            or MOUSEEVENTF_LEFTDOWN;
      arrI[1].mi.dwFlags := MOUSEEVENTF_LEFTUP;
    end;
  2: // Правый клик
    begin
      n := 2;
      arrI[0].mi.dwFlags := MOUSEEVENTF_MOVE
                            or MOUSEEVENTF_ABSOLUTE
                            or MOUSEEVENTF_RIGHTDOWN;
      arrI[1].mi.dwFlags := MOUSEEVENTF_RIGHTUP;
    end;
  3: // Двойной клик   }
    begin
      n := 4;
      arrI[0].mi.dwFlags := MOUSEEVENTF_MOVE
                            or MOUSEEVENTF_ABSOLUTE
                            or MOUSEEVENTF_LEFTDOWN;
      arrI[1].mi.dwFlags := MOUSEEVENTF_LEFTUP;
      arrI[2].mi.dwFlags := MOUSEEVENTF_LEFTDOWN;
      arrI[3].mi.dwFlags := MOUSEEVENTF_LEFTUP;
    end;
  4: // Drag`n`drop
    begin
      n := 3;
      arrI[0].mi.dwFlags := MOUSEEVENTF_MOVE
                            or MOUSEEVENTF_ABSOLUTE
                            or MOUSEEVENTF_LEFTDOWN;
      arrI[1].mi.dwFlags := MOUSEEVENTF_MOVE or MOUSEEVENTF_ABSOLUTE;
      arrI[1].mi.dx := Random(screen_width) * screen_width_factor;
      arrI[1].mi.dy := Random(screen_height) * screen_height_factor;
      arrI[2].mi.dwFlags := MOUSEEVENTF_LEFTUP;
    end;
  5: // Нажмем энтер
    begin
      PressEnter;
    end;
  end;
...

Наконец-то, мы передаем весь массив с событиями в функцию SendInput, если n будет больше нуля (n = 0 только в последнем случае, когда нажимаем кнопку <Enter>).

...
  if n <> 0 then SendInput(n,arrI[0],SizeOf(TInput));
  Sleep(300);
...

После запуска событий делаем задержку небольшую, чтобы система могла очухаться, а ты смог ощутить весь ужас происходящего (почти актерская пауза :) ), и затем продолжаем крутить цикл пока кто-нибудь не нажмет на <ESC>

...
until GetAsyncKeyState(VK_ESCAPE) <> 0;
end.

В последнем шкоде я решил нажимать энтер, для получения большего эффекта. Вот код процедуры PressEnter:

procedure PressEnter;
var K: array [0..1] of TInput; // массив на 2 события: нажать и отжать
begin
  ZeroMemory(@K, SizeOf(TInput)*2); // обнуляем массив
  K[0].Itype := INPUT_KEYBOARD; // Заполняем поля
  K[0].ki.wVk := VK_RETURN; // Это код виртуальной клавиши 
  K[1].Itype := INPUT_KEYBOARD;
  K[1].ki.wVk := VK_RETURN;
  K[1].ki.dwFlags := KEYEVENTF_KEYUP; // Отжимаем кнопку
  SendInput(2,K[0],SizeOf(TInput)); // Запускаем события
end;

Вот и весь код маленькой, но весьма зловредной проги Mad Mouse (исходник прилагается к статье). Можешь сам добавить другие события: иногда жать ESC, крутить колесико мышки, а еще жать CTRL, SHIFT и ALT, но отпускать не сразу, а после других событий, тогда эффект будет еще страшнее. Если надумаешь сделать боевую версию, то прикрути к ней какой-нибудь автозапуск и запуск вредного кода в зависимости, например, от даты. Еще надо предотвращать повторный запуск проги, а то она будет сама себе мешать. Тогда ты получишь настоящую бомбу с часовым механизмом и поразишь своего недруга прямо в прокуренный и пропитанный пивом ... жесткий диск.

MadMouse с исходником.

(C) tripsin, 2006 год.

Hosted by uCoz