1

Тема: AutoHotkey: правильно нажимаем кнопки

Речь пойдет о том, как правильно скриптом нажимать всяческие кнопки, разнообразных окон. Например, кнопки диалогов подтверждений.
Эта инфа частично воспроизводит мой пост на прежнем форуме.
Поехали. Возьмем для примера Total Commander (неважно, используете вы Total Commander или нет - детали, про которые мы будем говорить, понятны и так). Как известно, при копировании, ТС выдает запрос на подтверждение этого действия. И избавиться от этого подтверждения средствами самого ТС нет никакой возможности. Допустим, нас это достало и мы хотим, чтобы за нас кнопку "ОК" нажимал скрипт. Казалось бы "чего проще"? Пишем скрипт.

#Persistent ; скрипт будет запущен постоянно
SetTimer, Auto_Window, 200 ; переходить к указанной подпрограмме через каждые 0.2 секунды
Return ; закончить автовыполняющуюся часть

Auto_Window: ; метка входа таймера автоматизации окон
; TOTAL COMMANDER: ПОДТВЕРДИТЬ КОПИРОВАНИЕ
IfWinActive, ahk_class TInpComboDlg ; если активно окно, где мы подтверждаем копирование/перемещение, то...
    Send, {Enter} ; собственно подтверждение
Return ; конец подпрограммы по таймеру

Работает? Работает. Теперь представьте ситуацию: скрипт определил, что нужное окно сейчас активно и вот-вот нажмет Enter. Но прежде, чем он успел это сделать, выскочило окно какой-нибудь другой проги (маловероятно, что у вас кроме ТС не запущено других прог, а вот то, что кто-то из них чего-то у вас спросит - очень даже вероятно. Например, файрволл выскочит с сообщением, что "Файл 'Офигеть-какой-вирус' запрашивает входящий доступ. Разрешить?"...). А тут как раз наш скрипт дошел до команды "Send, {Enter}", и выполнил её. Пока вы поймете что произошло, упомянутый файл уже успеет бухнуть в командную строку "format c:" или еще чего по-тихому наворочать. Ситуацию я, конечно, утрирую, но только чтобы подчеркнуть смысл: всякие Send'ы лучше, по-возможности, направлять в целевое место.

Это можно сделать несколькими способами:
Первый способ - вместо команды "Send, {Enter}" написать:

{
    ControlFocus, TButton5 ; установить фокус на кнопку ОК
    ControlSend,, {Enter} ; подтвердить
}

Тогда можно почти не сомневаться, что всё сработает как надо. (О надежности способа см. ниже).
Кстати, конкретно в этом случае, можно не использовать команду "ControlFocus, TButton5", т.к. фокус и так на кнопке "ОК", но я всё же её написал, потому что в другом случае, фокус может быть и не на нужной кнопке, а команда "ControlSend,, {Enter}" нажимает ту кнопку, которая имеет фокус.
Логично было бы предположить, что будет работать команда "ControlSend, TButton5, {Enter}", однако она почему-то не работает (т.е. работает, но только потому, что TButton5 - кнопка "с фокусом", а если её изменить на, например, TButton4, то все равно будет нажиматься TButton5).

Второй способ - использовать вместо команды "Send, {Enter}" команду "ControlClick":

ControlClick, TButton5 ; подтвердить

Третий способ - вместо команды "Send, {Enter}" посылаем кнопке не ENTER, а SPACE (вот гадство!):

ControlSend, TButton4, {Space} ; подтвердить

Теперь о надежности этих способов:
Способ 1. Иногда в силу разных причин (винда занята каким-то важным для нее процессом, антивирус чрезмерно бдит и т.д.), нужный контрол не успевает получить фокус, и последующая команда нажмет не ту кнопку. Можно или мириться с ненадежностью скрипта (но это не наш выбор), или повесить цикл, в котором будет устанавливаться и проверяться фокус.

Loop ; убеждаемся, что подтверждение принято
{
    ControlFocus, TButton5 ; ставим фокус на нужный контрол
    If ErrorLevel = 0 ; если контрол получил фокус, то...
        Break ; конец цикла
    Sleep, 10*A_index ; постепенно увеличивающаяся пауза, чтобы контрол успевал среагировать
}
ControlSend,, {Enter} ; собственно подтверждение

Способ 2. У него страдания те же (нужный контрол не успевает получить фокус), но зависят от движений мыши. Т.е. если в ответственный момент клика, вы (по злому умыслу, например), двинете мышью, то, если момент выбран правильно клик промахнется. Лечить можно так-же как и в первом случае, а можно немного видоизменить проверку:

Loop ; убеждаемся, что подтверждение принято
{
    ControlClick, TButton5 ; подтвердить
    Sleep, 10*A_index ; постепенно увеличивающаяся пауза, чтобы контрол успевал среагировать
    IfWinNotExist ; если (последнее найденное) окно уже НЕ существует, то...
        Break ; конец цикла
}

Способ 3. По моему наблюдению (быть может недостаточному, неквалифицированному или предвзятому), этот способ работает надежнее всего. Правда и здесь не все 100% надежности, но мне так и не удалось выяснить отчего он иногда не срабатывает. Я рекомендую использовать этот способ, но в особо ответственных скриптах подстаховаться нижеприведенным методом:

Loop ; убеждаемся, что подтверждение принято
{
    ControlSend, TButton5, {Space} ; подтвердить
    Sleep, 10*A_index ; постепенно увеличивающаяся пауза, чтобы контрол успевал среагировать
    IfWinNotExist ; если (последнее найденное) окно уже НЕ существует, то...
        Break ; конец цикла
}

Таким образом, самый надежный вариант целиком выглядит так:

#Persistent ; скрипт будет запущен постоянно
SetTimer, Auto_Window, 200 ; переходить к указанной подпрограмме через каждые 0.2 секунды
Return ; закончить автовыполняющуюся часть

Auto_Window: ; метка входа таймера автоматизации окон
; TOTAL COMMANDER: ПОДТВЕРДИТЬ КОПИРОВАНИЕ
IfWinExist, ahk_class TInpComboDlg ; если СУЩЕСТВУЕТ окно, где мы подтверждаем копирование/перемещение, то...
    Loop ; убеждаемся, что подтверждение принято
    {
        ControlSend, TButton5, {Space} ; подтвердить
        Sleep, 10*A_index ; постепенно увеличивающаяся пауза, чтобы контрол успевал среагировать
        IfWinNotExist ; если (последнее найденное) окно уже НЕ существует, то...
            Break ; конец цикла
    }
Return ; конец подпрограммы по таймеру

Да, немного громоздко, но зато надежно!
Обратите внимание, у такого способа есть еще одно преимущество. Можно проверять не АКТИВНОСТЬ окна (а команда Send высылает клавиши ТОЛЬКО в активное окно), а СУЩЕСТВОВАНИЕ окна. Т.е. если какое нибудь окно в неподходящий момент перекроет то окно с которым мы работаем, то это нисколько не помешает сработать скрипту правильно.
В связи с изложенным, общее правило таково: если вместо команды Send, есть возможность использовать ControlSend или ControlSetText, то лучше (надежнее) их и использовать. И не ленитесь вводить дополнительные проверки нестандартных ситуаций (глюкующие проги-скрипты вряд ли кого-нибудь радуют).
К описанным способам, добавлю, что пробовал я нажимать кнопки и через SendMessage, но выигрыша в надежности по сравнению с третьим способом не заметил, а вот усложнение - заметное. Пока в поиске. Если кто нашел - поделитесь, думаю буду рад не я один.

Крокодил, крокожу и буду крокодить! (Твёрдое обещание нетрезвого кодера).