Сообщений 0    Оценка 10        Оценить  
Система Orphus

X-Window: Несекретные окна

Автор: Андрей Боровский
Источник: RSDN Magazine #0
Опубликовано: 28.01.2002
Исправлено: 13.03.2005
Версия текста: 1.2
«Сейчас я вас щелкну…»
Вверх и вниз по дереву окон

В этой статье речь пойдет о работе с X-Window средствами Kylix. Мы рассмотрим такие полезные возможности, как генерация скриншотов окон и отдельных элементов управления, поиск окна в иерархии окон X-Window, и некоторые другие. Интерфейсы для работы с X-Window в Borland Kylix предоставляются модулями Xlib и Qt.

«Сейчас я вас щелкну…»

Скриншоты (изображения элементов графического вывода программы) часто используются в качестве иллюстраций в материалах, посвященных описанию приложений и визуальных сред разработки (например, в этой статье). Неудивительно поэтому, что практически все графические оболочки поставляются со средствами получения скриншотов. Однако такие программы, как KSnapshot, подходят не для всех ситуаций. Приведу несколько примеров. Допустим вы, программист, работающий в среде Borland Kylix, хотите сделать скриншот отдельного элемента управления (control), а не всего окна. Или же нужно получить «снимок», отражающий состояние программы в строго определенный момент ее выполнения. Возможно также, что вы захотите получить серию изображений для создания простейшей анимации. Во всех этих случаях было бы желательно иметь в своем распоряжении средства генерации скриншотов «изнутри» самой программы.

Получить скриншот элемента управления или окна в Kylix-приложении совсем несложно. CLXDisplay API, являющийся оболочкой библиотеки Qt, включает две функции,: QPixmap_grabWidget и QPixmap_grabWindow, предназначенные для получения соответственно скриншотов элементов управления библиотеки Qt и окон X-Window. Обе эти функции возвращают графические данные в объекте QPixmap, указатель на который должен быть им передан. Главное различие между функциями QPixmap_grabWidget и QPixmap_grabWindow заключается в способе получения изображения. Функция QPixmap_grabWidget вызывает перерисовку элемента управления (и его дочерних элементов) при помощи метода PaintEvent, перенаправляя данные во внутренний буфер, в то время как QPixmap_grabWindow считывает изображение, созданное системой X-Window. Обычно QPixmap_grabWindow выполняется быстрее, чем QPixmap_grabWidget. Однако при использовании первой функции результирующее изображение получается таким, каким оно представляется на экране, т. е. если отображаемое окно частично скрыто другими окнами, скрытые части скриншота будут заполнены черным цветом. Функция QPixmap_grabWidget генерирует полное изображение элемента управления, вне зависимости от его положения на экране. Следует отметить, что получить изображения окон X-Window, не связанных с Qt-объектами, можно только при помощи функции QPixmap_grabWindow. Объект-приемник QPixmap позволяет выполнять различные операции с полученным изображением, например, сохранять его на диске или копировать в буфер обмена.

Используя QPixmap_grabWidget, напишем процедуру GrabControl, позволяющую получить скриншот элемента управления Kylix и сохранить его на диске в заданном формате.

procedure GrabControl(Control : TWidgetControl; 
const FileName, Format : String);
var
PM : QPixmapH;
FN : WideString;
begin
PM := QPixmap_create;
QPixmap_grabWidget(PM, Control.Handle, 0, 0, -1, -1);
FN := FileName;
QPixmap_save(PM, @FN, PChar(Format));
QPixmap_destroy(PM);
end;

Первым аргументом процедуры должен быть экземпляр одного из потомков класса TWidgetControl, т. е. любой элемент управления Kylix. Второй аргумент – имя файла, в котором следует сохранить полученное изображение. Третьим параметром процедуры является строка, в которой передается формат сохранения изображения. Допустимыми значениями являются графические форматы, поддерживаемые библиотекой Qt ('BMP', 'PNG', 'XPM' и т. п.). Рассмотрим подробнее функцию QPixmap_grabWidget. Первый параметр этой функции – ссылка на созданный ранее объект QPixmap, которому передается изображение. Лежащая в основе этой функции статическая функция библиотеки Qt QPixmap::grabWidget сама создает новый объект QPixmap, однако в CLXDisplay API этот механизм изменен.


Скриншот элемента управления, полученный с помощью GrabControl

Объект QPixmap является основой компонента TBitmap. Следующий код позволяет отобразить скриншот при помощи компонента TImage (Image1):

QPixmap_grabWidget(Image1.Picture.Bitmap.Handle,
Control.Handle, 0, 0, -1, -1);
Image1.Refresh;

При помощи процедуры GrabControl можно получить скриншот формы приложения со всем ее содержимым (в следующей строке процедура вызывается из метода формы):

GrabControl(Self, 'form.png', 'PNG');

Однако на скриншоте, полученном таким способом, будет отображена только клиентская область формы, т. е. внутренняя часть окна без заголовка и обрамляющих элементов. Объясняется это тем, что элементы обрамления окна не являются частью элемента управления, лежащего в основе компонента TForm. Для отображения окна формы «целиком» нам потребуется другая процедура:

procedure GrabForm(Form : TCustomForm; 
const FileName, Format : String);
var
PM : QPixmapH;
FN : WideString;
Root, Parent, Wnd : TWindow;
Children: PWindow;
NChildren : Integer;
begin
if not Form.Visible then Exit;
PM := QPixmap_create;
Wnd := QWidget_winID(Form.Handle);
XQueryTree(QtDisplay, Wnd, @Root, @Parent,
@Children, @NChildren);
XFree(Children);
while Parent <> Root do
begin
Wnd := Parent;
XQueryTree(QtDisplay, Wnd, @Root, @Parent,
@Children, @NChildren);
XFree(Children);
end;
QPixmap_grabWindow(PM, Wnd, 0, 0, -1, -1);
FN := FileName;
QPixmap_save(PM, @FN, PChar(Format));
QPixmap_destroy(PM);
end;

Формат вызова процедуры GrabForm такой же, как и у GrabControl, разница в том, что в первом параметре GrabForm передается указатель не на любой элемент управления, а только на форму.

Чтобы понять, как работает эта процедура, рассмотрим механизм прорисовки окон в системе X-Window. Прежде всего следует отметить, что все окна в X-Window организованы в иерархическую структуру. Список окон представляет собой дерево, корнем которого является окно оболочки (desktop window). Вывод окон осуществляется оконным менеджером (window manager), который и создает все обрамляющие элементы. Получив запрос на создание нового окна, оконный менеджер создает базовое окно-контейнер, которое «отвечает» за прорисовку заголовка и обрамления, и внутри которого размещаются клиентское окно приложения и окна кнопок заголовка. Клиентское окно и окна кнопок являются дочерними окнами окна-контейнера, а само окно-контейнер является непосредственным потомком корневого окна.


Чтобы получить идентификатор окна-контейнера, необходимо получить сперва идентификатор клиентского окна, а затем подняться вверх по иерархии окон до окна-контейнера. Для этого мы используем функцию XQueryTree, которая позволяет для заданного окна получить идентификатор его родительского окна, корневого окна и список идентификаторов дочерних окон (если они есть). Вызов XQueryTree выполняется в цикле, так как в разных оконных менеджерах «родословная» клиентского окна относительно окна-контейнера может различаться. Например, в KDE окно-контейнер является «дедушкой» клиентского окна, тогда как в WindowMaker клиентское окно – непосредственный потомок окна-контейнера. Но в любом случае окно-контейнер должно быть потомком окна Desktop, так что мы поднимаемся вверх до тех пор, пока не найдем такое окно. Поскольку теперь мы имеем дело с окном X-Window, а не с Qt-объектом, придется воспользоваться функцией QPixmap_grabWidget. При этом следует учесть особенности этой функции (например, проследить, чтобы окно формы располагалось на экране поверх других окон). Обращение к процедуре GrabForm может происходить следующим образом:

Self.BringToFront;
GrabForm(Self, 'form.png', 'PNG');


Изображения формы, полученные в результате выполнения процедур GrabControl и GrabForm.

Вверх и вниз по дереву окон

Мы уже знакомы с функцией XQueryTree, позволяющей получить идентификаторы предков и потомков окна в иерархии окон X-Window. Эту функцию можно использовать для поиска и анализа окон, не принадлежащих вызывающему ее приложению. В качестве примера использования XQueryTree рассмотрим функцию FindXWindow, находящую окно, имя которого совпадает с заданным, и возвращающую идентификатор этого окна.

function FindXWindow(Display : PDisplay; 
Root : TWindow; const Name : String) : TWindow;
const
StackDepth = 32;
type
TWinArray = array[0..0] of TWindow;
PWinArray = ^TWinArray;
StackElem = record
Children : PWinArray;
NChildren, Position : Integer;
end;
var
Stack : array[0..StackDepth-1] of StackElem;
StackPtr, i : Integer;
retName : PChar;
Wnd : TWindow;
begin
// Проверяем корневое окно
XFetchName(Display, Root, @retName);
if Name = retName then
begin // Корневое окно подходит
XFree(retName);
Result := Root;
Exit;
end;
Result := 0;
StackPtr := 0;
Stack[StackPtr].Position := 0;
XQueryTree(Display, Root, @Wnd, @Wnd,
@(Stack[StackPtr].Children), @(Stack[StackPtr].NChildren));
// Начинаем обход поддерева Root
while Result = 0 do
if Stack[StackPtr].Position < Stack[StackPtr].NChildren then
begin
Wnd := Stack[StackPtr].Children^[Stack[StackPtr].Position];
XFetchName(Display, Wnd, @retName);
if Name = retName then
begin
// Окно найдено
Result := Wnd;
XFree(retName);
for i := 0 to StackPtr do XFree(Stack[i].Children);
end else
begin
XFree(retName);
if StackPtr < StackDepth - 1 then
begin
Inc(StackPtr);
XQueryTree(Display, Wnd, @Wnd, @Wnd,
@(Stack[StackPtr].Children), @(Stack[StackPtr].NChildren));
Stack[StackPtr].Position := 0;
end else Inc(Stack[StackPtr].Position);
end;
end else
begin XFree(Stack[StackPtr].Children);
Dec(StackPtr);
if StackPtr = -1 then Exit; // Окно не найдено
Inc(Stack[StackPtr].Position);
end;
end;

Первый аргумент функции – указатель на X-дисплей. В параметре Root передается идентификатор окна, среди потомков которого следует вести поиск (для поиска по всей системе следует передать идентификатор корневого окна). Третий аргумент функции – имя окна, идентификатор которого нужно получить. Если окно с указанным именем не будет найдено среди потомков окна Root, функция вернет 0. Константа StackDepth ограничивает максимальную «глубину погружения» при обходе дерева окон. Следующая строка кода иллюстрирует вызов функции FindXWindow:

KonsoleID := FindXWindow(QtDisplay, QWidget_winID(Application.Desktop), 'Konsole');

Функцию FindXWindow несложно преобразовать в процедуру создания списка всех окон системы, которая может использоваться, например, в отладочных приложениях.

Если можно получить идентификатор окна, не принадлежащего нашей программе, значит, можно и выполнять некоторые операции с «чужими» окнами. Напишем приложение, аналогичное поставляемой с KDE утилите KSnapshot. Создайте новый проект. Добавьте в раздел uses модуля Unit1 модули Qt и Xlib. Поместите в окно формы стандартную кнопку и компонент TImage. Добавьте в объект TForm1 поле Grabbing типа Boolean и присвойте этому полю значение False в конструкторе формы. В обработчике события OnClick объекта Button1 задайте следующий код:

procedure TForm1.Button1Click(Sender: TObject);
begin
Grabbing := True;
XGrabPointer(QtDisplay, QWidget_winID(Self.Handle), 1, ButtonPressMask,
GrabModeAsync, GrabModeAsync, 0, 0, 0);
end;

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

procedure TForm1.FormMouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
var
Root, Child : TWindow;
Dummy : Integer;
begin
if Grabbing then
begin
Grabbing := False;
if XQueryPointer(QtDisplay, QWidget_winID(Application.Desktop),
@Root, @Child, @Dummy, @Dummy, @Dummy, @Dummy, @Dummy)<>0 then
begin
QPixmap_grabWindow(Image1.Picture.Bitmap.Handle, Child,
0, 0, -1, -1);
Image1.Refresh;
end;
end;
end;

Функция XGrabPointer позволяет приложению отслеживать состояние мыши, когда указатель мыши находится за пределами окна приложения. Эта функция аналогична функции SetCapture Windows API. Функция XQueryPointer позволяет определить, в каком окне находится отслеживаемый указатель. В принципе, после получения скриншота мы должны были бы вызвать функцию XUngrabPointer, снимающую контроль над мышью, но в этом нет необходимости, так как Kylix «любезно» выполнит соответствующую операцию за нас.

В заключение один «опасный» пример. Хотите превратить описанное выше приложение в аналог системной утилиты XKill? Тогда замените код обработчика OnMouseDown следующим:

procedure TForm1.FormMouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
var
Root, Child : TWindow;
Dummy : Integer;
Children : PWindow;
begin
if Grabbing then
begin
Grabbing := False;
if XQueryPointer(QtDisplay, QWidget_winID(Application.Desktop),
@Root, @Child, @Dummy, @Dummy, @Dummy, @Dummy, @Dummy)<>0 then
begin
XQueryTree(QtDisplay, Child, @Root, @Root, @Children, @Dummy);
while Children <> nil do
begin
Child := Children^;
XFree(Children);
XQueryTree(QtDisplay, Child, @Root, @Root, @Children, @Dummy);
end;
XKillClient(QtDisplay, Child);
end;
end;
end;

Теперь после нажатия на кнопку Button1, щелчок мышью в каком-либо окне приведет к закрытию соответствующего приложения. Завершение приложения-владельца окна X-Window осуществляется функцией XKillClient, которой передается идентификатор окна. Обратите внимание, что в этой процедуре мы выполняем действия, обратные тем, что мы выполняли в процедуре GrabWindow – спускаемся вниз по дереву окон. Это необходимо потому, что функция XQueryPointer возвращает идентификатор окна-контейнера, и если мы передадим этот идентификатор функции XKillClient, будет «закрыт» компонент X-Window, отвечающий за прорисовку обрамляющих элементов окон. Чтобы избежать этого неприятного явления, мы ищем среди потомков первого «ребенка» окна-контейнера дочернее окно, не имеющее дочерних окон. Это окно уж точно принадлежит приложению, а не системному компоненту.


Эта статья опубликована в журнале RSDN Magazine #0. Информацию о журнале можно найти здесь
    Сообщений 0    Оценка 10        Оценить