Main Page
Статьи Компоненты Ссылки Разное

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

Андрей Боровский - kylixportal@narod.ru

Выкладываю статью, опубликованную в журнале RSDN (с любезного разрешения редакции). На форуме RSDN меня поправили, что нужно говорить X-Window (и это правильно :-)). Однако, во-первых термин X-Windows все же встречается в литературе, а во-вторых "из песни слова не выкинешь". Так что оставляю все как есть.

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

“Сейчас я вас щелкну…”

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

Получить скриншот элемента управления или окна в Kylix-приложении совсем несложно. CLXDisplay API, являющийся оболочкой Qt library, включает две функции : QPixmap_grabWidget и QPixmap_grabWindow, предназначенные для получения соответственно скриншотов элементов управления Qt library и окон X-Windows. Обе эти функции возвращают графические данные в объекте QPixmap, указатель на который должен быть им передан. Главное различие между функциями QPixmap_grabWidget и QPixmap_grabWindow заключается в способе получения изображения. Функция QPixmap_grabWidget вызывает перерисовку элемента управления (и его дочерних элементов) при помощи метода PaintEvent, перенаправляя данные во внутренний буфер, в то время как QPixmap_grabWindow считывает изображение, созданное системой X-Windows. Обычно QPixmap_grabWindow выполняется быстрее, чем QPixmap_grabWidget, однако при использовании первой функции результирующее изображение получается таким, каким оно представляется на экране, т. е. если отображаемое окно частично скрыто другими окнами, скрытые части скриншоте будут заполнены черным цветом, в то время как функция QPixmap_grabWidget генерирует полное изображение элемента управления, вне зависимости от его положения на экране. Следует отметить, что получить изображения окон X-Windows, не связанных с 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 library ('BMP', 'PNG', 'XPM' и т. п.). Рассмотрим подробнее функцию QPixmap_grabWidget. Первый параметр этой функции – ссылка на созданный ранее объект QPixmap, которому передается изображение. Лежащая в основе этой функции статическая функция Qt library 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;

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

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

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

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

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

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

Мы уже знакомы с функцией XQueryTree, позволяющей получить идентификаторы предков и потомков окна в иерархии окон X-Windows. Эту функцию можно использовать для поиска и анализа окон, не принадлежащих вызывающему ее приложению. В качестве примера использования 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;

Функция FindXWindow осуществляет обход дерева окон в прямом порядке. Первый аргумент функции – указатель на 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;

После того, как кнопка Button1 нажата, следующий щелчок мышью в окне какого-либо приложения вызовет генерацию скриншота этого окна. Поле Grabbing необходимо для того, чтобы скриншот не генерировался каждый раз при щелчке в окне формы приложения. Функция 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-Windows осуществляется функцией XKillClient, которой передается идентификатор окна. Обратите внимание, что в этой процедуре мы выполняем действия, обратные тому, что мы делали в процедуре GrabWindow – спускаемся вниз по дереву окон. Это необходимо потому, что функция XQueryPointer возвращает идентификатор окна-контейнера, и если мы передадим этот идентификатор функции XKillClient, будет “закрыт” компонент X-Windows, отвечающий за прорисовку обрамляющих элементов окон. Для того, чтобы избежать этого неприятного явления, мы ищем среди потомков первого “ребенка” окна-контейнера дочернее окно, не имеющее дочерних окон. Это окно уж точно принадлежит приложению, а не системному компоненту.

Истина где-то рядом?

Система X-Windows справедливо “славится” сложностью своего интерфейса программирования (в знаменитом Jargon File она удостоена эпитетов “hairy” и “over-complicated”). Однако, такие библиотеки как Qt library (и, соответственно, CLXDisplay API), в большинстве случаев избавляют нас от необходимости “прямого общения” с X-Windows, а использование отдельных функций X-Windows в приложении не вызывает, как мы видели, никаких затруднений. Двойственные чувства возникают в связи с возможностью получения информации и управления “чужими” окнами. С одной стороны, эти возможности позволяют реализовать ряд полезных утилит. С другой стороны, та же функция XKillClient действует “невзирая на звания и лица”, в результате чего становится возможным, например, закрыть приложение с высоким уровнем доступа из приложения с более низким уровнем.

Вообще же говоря, обращения к функциям X-Windows напрямую следует по возможности избегать, так как при использовании этих функций ObjectPascal приложение теряет кросс-платформенную переносимость, наконец-то обретенную с появлением Kylix.


Эта статья была впервые опубликована в журнале RSDN, #0, 2002 г. Перепечатка статьи возможна только с разрешения редакции журнала.

Используются технологии uCoz