Хочу рассказать об одном интересном случае из собственной практики. Мой коллега, Web-программист, имеет довольно забавное хобби. Он коллекционирует видеофильмы, естественно и сайт с описанием этих самых фильмов сделал и каталогизирует все аккуратно и трепетно. Где-то на просторах рунета скачал он оффлайновый каталог аналогичной направленности и посчитал, что некоторые из найденных в нем материалов оказались бы совершенно не лишними в контексте его сайта. Каталог назывался «Энциклопедия видеофильмов» и представлял собой обычную Windows программу. В дальнейшем я буду называть эту программу просто «Энциклопедия». Информация о фильмах хранилась в базе. Но авторы программы позаботились о защите своего труда от посягательств разного рода
коллекционерами. Денег за информацию они не просили, но полностью при выводе полнотекстового описания фильма блокировали выделение текста, что бы описания нельзя было скопировать через буфер обмена. Очевидно, что таким образом разработчики пытались стимулировать скачивание своего продукта с сайта, куда при каждой загрузке Энциклопедия стремилась зайти.

Программа имела вполне стандартный интерфейс. Список фильмов представлялся в виде таблицы. При двойном щелчке на записи, в левом верхнем углу экрана открывалось модальное окно, содержащее детальную информацию о фильме. Именно эти развернутые описания фильмов представляли для нас интерес.
Самым простым вариантом в сложившейся ситуации было бы подключиться к базе. Но вместо базы в каталоге с программой лежал файл с расширением
.xml, как вы понимаете ничего общего по формату с .xml не имеющий. От меня, собственно, требовалось понять, что же это за формат, и прицепиться к этой БД из Delphi. Однако формат распознанию так и не поддался. Казалось бы, что данные из этой базы вытащить так и не удастся. Авторы программы выбрали простое и достаточно эффективное решение. Блокируется возможность использовать буфер и закрывается прямой доступ к базе. Однако слабость такого метода заключается именно в том, что вся информация выводится в окне.

В Windows большинство элементов пользовательского интерфейса представлены окнами. С точки зрения пользователя окно – область, имеющая границы, заголовок, системное меню и другие отличительные признаки. Но технически окно – составляющая системной таблицы, имеющая соответствующий код. Большинство окон исполняют роль элементов управления. Так кнопка или список имеют окно, равно как и основная форма приложения. Кроме того, имеются окна, создаваемые приложениями, которые остаются скрытыми от пользователей и используются только для получения сообщений. Само приложение также имеет свое собственное окно (не путать с окном главной формы приложения). Очевидно, что элемент управления, отвечающий за вывод текста на экран пользователя, тоже является окном. А текст, отображаемый таким окном можно получить, используя сообщения.
Я выделил следующие подзадачи:

  • Вызвать окно описания фильма из своей программы;
  • Найти окно элемента, отображающее текст;
  • Забрать текст с описанием фильма из окна;
  • Сохранить описание фильма в своей базе;

Перейдем к реализации. Я для этой цели использовал Delphi 2005. Хотя, то же самое можно сделать используя практически любое средство разработки.
Я создал новое приложение и приступил к решению первой подзадачи. Однозначно идентифицировать окно можно только используя т.н. хэндл окна (Windows handle). Он генерируется системой во время создания окна и имеет тип HWND. Но если окно пересоздается заново, его хэндл будет уже другим. Тем не менее, определить хэндл окна главной формы приложения не составляет большого труда. Для этой цели я использовал компонент TTimer. Событие OnTimer я обработал следующим образом:

procedure TForm1.Timer1Timer(Sender: TObject);
begin
if not application.Active then
begin
fgw:=GetForeGroundWindow;
end
end;

Предварительно необходимо описать переменную
fgw:

public
{ Public declarations }
fgw:HWND;

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

Для удобства я присвоил свойству FormStyle главной формы своей программы значение fsStayOnTop, что позволило приложению оставаться поверх всех окон, даже тогда, когда оно не было активно. Теперь программу не придется искать на панели задач.

Далее я воспользовался прекрасным модулем, написанным Кеном Хендерсоном sndKey32. Этот модуль содержит две процедуры, имеющие аналог в Visual Basic. Это процедуры Sendkeys и AppActivate. Собственно, я использовал только первую процедуру, которая имитирует нажатие клавиш в активном окне. Sendkeys имеет два параметра. Первый параметр - строка, содержащая список тех клавиш, которые будут отсылаться активному окну. Второй – булева переменная, указывающая должна
ли программа дождаться окончания обработки переданных клавиш. Подробнее работа функции описана в тексте самого модуля.

Запустив Энциклопедию я на время отложил в сторону мышь и поэкспериментировал исключительно с клавиатурой. Как и следовало ожидать, окно с описанием фильма открылось нажатием клавиши “Enter” и закрылось нажатием «Esc». Переход по записям осуществлялся стрелками управления курсором. Этого «джентльменского набора» было вполне достаточно, что бы программно «управлять» Энциклопедией. 

На главной форме своей программы я поместил кнопку и обработал ее нажатие следующим образом:

procedure TForm1.Button1Click(Sender: TObject);
begin
SetForegroundWindow(FGW);
for j := 0 to 10 do
begin
sendkeys(‘~’,true);
Delay(50);
sendkeys(‘~’,true);
sendkeys('{DOWN}',true);
end;
end;

SetForegroundWindow(FGW) – делает активным окно с хэндлом FGW. Это будет именно то окно, с которым велась работа до активизации моей программы.

Далее в цикле выполняется следующая комбинация действий: отсылается клавиша «Enter», выдерживается пауза, вновь отсылается «Enter» и затем отсылается стрелка вниз. Несмотря на параметр true при вызове sendkeys, который теоретически должен гарантировать, что окно успеет обработать переданную ему клавишу, я предпочитаю делать принудительную паузу. Это особенно актуально, если вследствие отсылки клавиш открываются другие окна, обычно на это требуется некоторое время. Для того, что бы реализовать паузу в работе программы я использовал процедуру Delay. Вот ее текст:

procedure TForm1.Delay(MSecs: Longint);
var
FirstTick: Longint;
begin
FirstTick := GetTickCount;
repeat
Application.ProcessMessages;
until GetTickCount-FirstTick>=MSecs;
end;

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

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

function GetWindText(AHandle: THandle): String;
var
cb : DWord;
begin
cb := SendMessage(AHandle, WM_GETTEXTLENGTH, 0, 0);
SetLength(Result, cb);
if cb > 0 then
SendMessage(AHandle, WM_GETTEXT, cb+1, LParam(@Result[1]));
end;

Здесь сначала определяется длина текста, путем отсылки сообщения окну с хэндлом AHandle. 

cb := SendMessage(AHandle, WM_GETTEXTLENGTH, 0, 0);

А полученное значение cb служит параметром при посылке второго сообщения WM_GETTEXT тому же окну. Результатом функции будет текст этого окна.

Но, как я уже говорил, окно программы – не то самое окно, в котором выводится нужный текст. Даже окно модальной формы с детальной информацией о фильме тоже не будет искомым окном. А для того, что бы отследить то окно из которого следует забирать текст, я воспользовался прекрасным продуктом, включаемым в поставку Borland Delphi – WinSight32. Эта утилита предоставляет отладочную информацию об окнах загруженных приложений. Используя программу, пользователь имеет возможность отследить какие классы, и какие окна создаются в системе. Для того, что бы легче было ориентироваться в дереве окон, отображаемых WinSight32, я оставил как можно меньше работающих программ, иначе просто немудрено запутаться. Заголовок главного окна программы - «Энциклопедия видеофильмов», этого было достаточно для того, что бы найти нужную ветвь в дереве окон. Далее я нашел ту ветвь, которая отвечала за окно, отображающее текст. Точнее говоря, я сделал предположение о том, что это была именно та ветвь, т.к. класс одного из окон был TRichEdit. Именно экземпляр этого класса логичнее всего было бы использовать для вывода форматированного текста. 

Однако, не смотря на то, что детализированная информация об окне (появится после двойного щелчка на выбранной ветке дерева в WinSight32) дает хэндл этого окна, непосредственно использовать его в своей программе я бы не смог, так как хэндл становится другим после каждого пересоздания окна. Но имея хэндл главного окна приложения, я смог определить хэндл окна нужного элемента управления по имени его класса.
Для этого я использовал API функцию EnumChildWindow, позволяющую искать дочерние окна приложения. Также я использовал еще одну схожую функцию - EnumWindow, которая позволяет искать окна верхнего уровня. Под окном верхнего уровня в данном случае можно понимать окно формы, а дочерними окнами являются окна элементов управления. Вызов приведенных выше функций несколько необычен для Delphi. В качестве параметра должна передаваться функция обратного вызова (в нашем случае FindNextWindow). 

Измененный код обработчика нажатия кнопки и текст функции FindNextWindow приведен в листинге.
Давайте его проанализируем. Сначала я нашел нужное окно модальной формы с детальной информацией о фильме. Значение его хэндла сохраняется в переменной wndNeedHandle. А затем я передал это значение в качестве параметра функции EnumChildWindow и нашел окно того элемента управления, который отображает текст. Хэндл этого окна соответственно сохраняется в переменной wndNeedEdit. В качестве параметра при вызове обеих функций передается указатель на одну и ту же функцию – FindNextWindow. В коде функции проверяется, совпадает ли название класса окна с тем именем класса, которое я находил, используя WinSight32 (для окна верхнего уровня – FILMINFO, для дочернего окна - TRichEdit). 

begin
SetForegroundWindow(FGW);
Timer1.Enabled:=false;
for j := 0 to 10 do
begin
sendkeys('~',true);
wndNeedEdit := -1; wndNeedHandle := -1;
EnumWindows(@FindNeedWindow, 1);
If wndNeedHandle > -1 Then
EnumChildWindows(wndNeedHandle, @FindNeedWindow, 2);
if wndNeedEdit>-1 then
Memo1.Text := GetWindText(wndNeedEdit);

//////////////////////////////////////////////
// здесь необходимо осуществить запись данных в БД
//////////////////////////////////////////////

sendkeys('{ESC}',true);
Delay(50);
sendkeys('{DOWN}',true);
Delay(500);
end;
end;

Function FindNeedWindow(wHandle: HWND; LPARAM: Integer): BOOL; Stdcall;
Var TextLen: Integer;
S: String;
Begin
Result := True;
Case LPARAM Of
1:
Begin
SetLength(S, 20);
GetClassName(wHandle, PChar(S), 20);
S := PChar(S);
If Pos('FILMINFO', UpperCase(S)) > 0 Then
Begin
Form1.wndNeedHandle := wHandle;
Result := False;
End;
End;
2:
Begin
SetLength(S, 20);
GetClassName(wHandle, PChar(S), 20);
S := PChar(S);
If S = 'TRichEdit' Then
Begin
Result := False;
Form1.wndNeedEdit := wHandle;
End;
End;
End

А что бы проверить действительно ли функция GetWindText возвращает нужный текст
я сохранил его в размещенном на форме компоненте TMemo. Итог работы программы – описание последнего из десяти фильмов в TMemo моей программы. Описания предыдущих девяти фильмов туда также попадали, но до текущего момента не сохранялись, и, соответственно, заменялись описанием очередного фильма.

Далее осталась сохранить получаемую информацию в таблице. Я для этой цели использовал MS Access и компоненты доступа ADO. Я не стану подробно описывать настройку подключения программы к базе, об этом написано практически в любом учебнике по Delphi. Собственно, мне понадобилась одна таблица в моей базе, содержащая всего лишь два поля – ключевое поле и поле типа Memo. Код сохранения полученного из окна текста в базу занял всего несколько строк.

ADOTable1.Insert;
ADOTable1Inf.Assign(Memo1.Lines);
ADOTable1.Post;

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

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

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

  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    0 комментариев
    Межтекстовые Отзывы
    Посмотреть все комментарии