вторник, 11 ноября 2008 г.

Тайная жизнь GetWindowText

Это перевод The secret life of GetWindowText. Автор: Реймонд Чен.

Функция GetWindowText сложнее, чем может вам показаться на первый взгляд. Документация пытается объяснить её сложность простыми словами, которые прекрасно подойдут, если вы не понимаете сложных слов, но это также значит, что вам говорят не всё.

Этот пост - попытка рассказать "всё".

Как окна управляют своим текстом

Для окна (в смысле WinAPI) существует два способа управлять своим текстом (заголовком). Они могут делать это сами или позволить делать это системе. По умолчанию управлением занимается система.

Если оконный класс даёт управление своим текстом системе, то система сделает следующее:
- Обработчик по-умолчанию для сообщения WM_NCCREATE возьмёт параметр lpWindowName (передваваемый CreateWindow/Ex) и сохранит его в "особом месте".
- Обработчик по-умолчанию для сообщения WM_GETTEXT копирует строку из этого "особого места".
- Обработчик по-умолчанию для сообщения WM_SETTEXT копирует строку в это "особое место".

С другой сторны, если оконный класс решит управлять своим текстом сам, то система не будет делать никаких дополнительных действий, а оконный класс будет обязан реагировать на сообщения WM_GETTEXT/WM_SETTEXT и руками сохранять/восстанавливать строку.

Каркасные классы обычно отдают управление текстом системе, а пользовательские классы предпочитают ручное управление.

GetWindowText

У функции GetWindowText есть одна маленькая проблема: она должна возвращать текст окна без зависания. К примеру, функции FindWindow нужно получать заголовки окон во время поиска. Приложениям по переключению задач (Task-switching applications) нужно получать заголовки окон для отображения в окошке переключения. Нельзя допустить, чтобы одно зависшее приложении влияло бы на другие приложения. Это было бы очень нехорошо.

Именно поэтому в этих сценариях не будет работать отправка сообщения WM_GETTEXT, потому что целевое для WM_GETTEXT окно может висеть. Чтобы избавиться от этой проблемы, GetWindowText должна использовать "особое место", т.к. оно не подвержено влиянию зависшего приложения.

С другой стороны, GetWindowText часто используется для чтения строк в элементах управления (контролах) диалогов, а эти контролы часто используют ручное управление текстом. Это требует отправки сообщения WM_GETTEXT, поскольку это единственный способ получения текста у окон с ручным управлением.

Поэтому GetWindowText использует компромиссное решение:
- Если вы пытаетесь использовать GetWindowText для окна в своём собственном процессе, то GetWindowText будет использовать сообщение WM_GETTEXT.
- Если вы пытаетесь использовать GetWindowText для окна в чужом процессе, то GetWindowText будет использовать строку из "особого места" и не будет отправлять сообщение.

Согласно первому правилу, если вы пытаетесь получить текст из окна в своём процессе, а окно при этом висит, то, конечно же, GetWindowText также повиснет. Но поскольку зависшее окно принадлежит вашему процессу, то это будет целиком вашей виной. Вы проиграли, а другие не пострадали - всё честно. При этом отправка сообщений WM_GETTEXT гарантирует корректную работу окон с ручным управлением текстом.

Согласно же второму правилу, если вы пытаетесь получить текст из окна в чужом процессе, тогда GetWindowText не будет отправлять сообщение - она просто прочтёт строку из "особого места". Поскольку самой частой причиной для вызова GetWindowText для окна чужого процесса является чтение заголовков окон и поскольку такие оконные классы используют системное управление, то такое поведение обычно приводит к желаемому результату.

Что касается документации, то она упрощает это до одной фразы: "GetWindowText() cannot retrieve text from a window from another application." ("GetWindowText не может читать текст из окна чужого приложения.").

Что, если мне не нравятся эти правила?

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

Только не забудьте, что если интересующее вас окно будет висеть, то ваше приложение также повиснет, т.к. SendMessage не вернёт управление, пока целевое окно не ответит.

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

А можешь дать пример, когда это на что-то влияет?

Рассмотрим такой элемент управления:
procedure TMyControl.DefaultHandler(var Message);
begin
  with TMessage(Message) do
    case Msg of
      WM_GETTEXT:
      begin
        StrLCopy(PChar(lParam), 'Booga!', wParam);
        Result := StrLen(PChar(lParam));
      end;
      WM_GETTEXTLENGTH:
        Result := 7; // = Length('Booga!') + 1 (#0-terminator)  
      ... 
    end;
end;

Пусть теперь приложение А делает:
MyControl := TMyControl.Create(nil);
// Где-то внутри это эквивалентно:
// FHandle := CreateWindow('Sample', 'Frappy', ...);

И пусть у нас есть процесс Б, у которого есть описатель окна (hwnd = MyControl.Handle), созданного процессом А (не важно, как он его получил):
var
  szBuf: array[0..79] of Char;
...
GetWindowText(hwnd, szBuf, 80);

Этот код вернёт szBuf = 'Frappy', потому что это именно та строка, которая хранится в "особом месте".
Однако:
SendMessage(hwnd, WM_GETTEXT, 80, LPARAM(@szBuf));

Вернёт szBuf = 'Booga!'.

Продолжение:
Почему GetWindowText подчиняется таким странным правилам?

10 комментариев:

  1. Спасибо! Был как раз затык - GetWindowsText не возвращала заголовок. SendMessage помогло!

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

    GetWindowText(win,@_string,250);

    заменил на
    SendMessage(win,WM_GETTEXT,250,integer(@_string));

    ОтветитьУдалить
  3. Спасибо за статью - сильно помогла осмыслить причину, почему разнятся заголовки некоторых окон в SPYXX и в 1С. У меня просто стояла обратная задача - как из родного 1С-го процесса получить заголовок, отражающийся в SPYXX.
    Вдруг кто не знает - нужно позвать DefWindowProc.

    ОтветитьУдалить
  4. начало было как везде - оптимистическое, далее опять как везде туман
    Нужно то просто с помощью GetWindowText прочитать имя класса открытого
    окна Windows и как это сделать просто и не заморачиваться заморочками
    составивших эту функцию , это по-моему и надо было описать

    ОтветитьУдалить
  5. Все, написанное в статье - АБСОЛЮТНАЯ ЕРУНДА.
    Может в древних версиях идиотской гейтсовой виндючни так и было, но в поколениях виндючего дерьма после дебильной "Vista"
    команды "GetWindowtextLength" и "GetWindowtext" рассылают именно системные сообщения ВСЕМ идентификаторам ("Handle" это никак не номер процесса),
    положительно отвечающим на системный запрос "IsWindow" и прекраснейшим образом ВИСНУТ на любых приложениях, а не только на вашем.
    И не читайте всякую хрень из т.н. "базы знаний" - виндючню тупого Била пишут одни безграмотные индусы, а статьи к ней - другие безграмотные
    индусы, которые с первыми никогда не общаются.

    ОтветитьУдалить
    Ответы
    1. Извините, но "GetWindowText рассылают именно системные сообщения ВСЕМ идентификаторам" - это бред. Можете проверить сами вот этим несложным кодом.

      И GetWindowText и WM_GETTEXT отправляют сообщение WM_GETTEXT окну своего процесса, поэтому оба варианта получат актуальный заголовок - даже с включённой опцией "Override WM_GETTEXT".

      С другой стороны, для окна другого приложения:
      1. При выключенной опции "Override WM_GETTEXT" оба варианта (GetWindowText и прямая отправка WM_GETTEXT) дадут один и тот же результат (актуальный заголовок окна), несмотря на различные способы его получения.
      2. GetWindowText не отправляет сообщение WM_GETTEXT, поэтому при включённой опции "Override WM_GETTEXT" GetWindowText покажет "старое" содержимое. Прямое же сообщение WM_GETTEXT, само собой, получит "новое" перегруженное значение.
      3. Если сначала нажать кнопку "Hang", а затем попытаться получить текст, то GetWindowText вернёт управление моментально (и вернёт "старый" текст), в то время как прямое сообщение WM_GETTEXT повиснет.

      Это поведение имеется в любой системе Windows - от Windows 95 вплоть до Windows 10 Anniversary Update (Redstone #1).

      Если у вас что-то работает иначе - есть два варианта:
      1. Вы ошиблись в тестировании.
      2. Кто-то иной меняет поведение системных функций, возможно, установив на них хук (антивирус? расширение оболочки? бяка? читалка экрана?).
      3. Либо одно из трёх.

      Удалить
    2. Если кроме самовлюбленного апломба у вас есть хоть какие-то навыки в программировании - напишите простенький тестер с ловушками системных сообщений и посмотрите, что происходит в системе.
      А потом будете мудрствовать, пока не уписяитесь. В том числе и про антивирус, хук и т.п.

      Удалить
    3. Шта?

      - Проигнорировал тест, его результаты.
      - Сам ни строчки кода не привёл.
      - Назвал не абы кого, а Реймонда Чена - безграмотным индусом.

      (А у меня, значит - "апломб")

      Анонимный друг, тебе там бревно в глазу не мешает? Или это троллинг такой?

      Удалить
  6. Спасибо. Кратко, по делу, помогло и без идиотских советов RTFM.

    ОтветитьУдалить

Можно использовать некоторые HTML-теги, например:

<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>

Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку.

Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.

Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.

Примечание. Отправлять комментарии могут только участники этого блога.