Delphi World - это проект, являющийся сборником статей и малодокументированных возможностей  по программированию в среде Delphi. Здесь вы найдёте работы по следующим категориям: delphi, delfi, borland, bds, дельфи, делфи, дэльфи, дэлфи, programming, example, программирование, исходные коды, code, исходники, source, sources, сорцы, сорсы, soft, programs, программы, and, how, delphiworld, базы данных, графика, игры, интернет, сети, компоненты, классы, мультимедиа, ос, железо, программа, интерфейс, рабочий стол, синтаксис, технологии, файловая система...
Мечты вуайериста - чужие окна

Для чего это?

Нет, конечно, никакого отношения это статья к привычным извращениям не имеет, просто рассказывает, как можно подглядывать в чужие окна.

Судя по тому шквалу вопросов, которыми завалены форумы, вопрос изучения чужих окон интересует многих. Каюсь, здесь я оказался в большинстве. Движимый любопытством я попытался разобраться в том, как же все-таки заглянуть в чужое окно. И написал некую прогр аммку, которая все это умеет делать. Ну, почти все. Попутно пришлось найти ответы на многие вопросы, которые, как мне кажется, интересуют не только меня. Программа написана на Delphi 3 для Windows 98. И, возможно, в более поздних версиях Delphi появились дополнительные возможности.

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

Для кого это?

Для всех, кто хочет научиться работать с WINAPI. Для тех, кто программирует в Delphi, хотя почти все сказанное тут может быть использовано и в других языках, тем и хороши функции API.

По мере использования приводятся краткие описания функций API.

Эта публикация подразумевает достаточно низкий уровень знания Дельфи и АПИ, но какие-то базовые знания все-таки необходимы. С другой стороны, никакой Америки здесь не открывается, просто приводятся примеры работы с чужими окнами.

Для подключения функций API необходимо включить в описание используемых модулей Uses ShellAPI;

Как получить список всех окон запущенных в системе.

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

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

Идентификатор окна, он же дескриптор он же описатель окна это просто число, зная которое можно получить доступ к каждому конкретному окну в системе.

Для нахождения окон запущенных в системе существует целый ряд функций WinAPI (в дальнейшем просто API).

Функция FindWindow

Синтаксис:


function FindWindow(ClassName, WindowName: PChar): HWnd;

Описание:

Находит родительское окно верхнего уровня с совпадающими ClassName и WindowName. Не осуществляет поиск дочерних окон.

Параметры:

ClassName: Имя класса окна (заканчивающееся пустым символом, 0 - если все классы).
WindowName: Текстовый заголовок окна или 0, если все окна.
Возвращаемое значение: Описатель окна; 0 - если такого окна нет.

Итак, функция FindWindow находит все окна верхнего уровня по названию класса и заголовку окна.

Если Ваша задача определить запущено ли определенное окно (с известными именем класса и заголовком) в настоящий момент, можно использовать


Procedure  WindowPresent(ClassName,WindowName:PChar): Boolean;
Begin
  Result := FindWindow(ClassName,WindowName)<>0;
End;

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

Но посмотрим, что еще у нас есть из функций работы с окнами.

Функция GetNextWindow

Синтаксис:


function GetNextWindow(Wnd: HWnd; Flag: Word): Hwnd;

Описание: Считывает из Wnd следующее или предыдущее окно. В случае окна верхнего уровня ищется следующее окно верхнего уровня, а в случае дочернего окна ищется следующее дочернее окно.

параметры:

Wnd: идентификатор окна.
Flag: Одна из констант
gw_HWndNext - искать следующее окно
gw_HwndPrev - искать предыдущее окно.
возвращаемое значение: Идентификатор окна.

Функция GetNextWindow находит все окна текущего уровня (если задано окно верхнего уровня, - то ищет окно верхнего уровня, если дочернее окно - то список дочерних)

Осталось определить, как найти исходный описатель окна, от которого будем плясать (параметр WND функции)

Можно попробовать начать поиски с верхнего окна системы. Его можно определить при помощи следующей функции API:

Функция: GetForegroundWindow

Синтаксис:


function GetForeGroundWindow: Hwnd;

Описание: Показывает верхнее окно системы.

Параметры: нет.

Возвращаемое значение: Идентификатор окна.

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

Давайте попробуем описать первый вариант функции, которая составляет список всех окон верхнего уровня системы, пусть у нас есть на форме некий ListBox1:TlistBox, будем помещать в него найденные окна. И процедура поиска окон будет выглядеть тогда следующим образом:


procedure Tform1.GetAllWindow;
Var
    Wd : HWnd;
begin
    ListBox1.Items.Clear;         // Очистим список перед началом поисков
    Wd:=FindWindow(0,0);          // Найдем первое окно верхнего уровня любого класса
    While (Wd<>0) do              // Если такое окно существует
    Begin
        ListBox1.Items.Add(IntToStr(Wd));    // Добавим описатель в виде текста в список
        Application.ProcessMessages;         // Дадим возможность поработать другим
        Wd:=GetNextWindow(Wd,GW_HWNDNEXT);   // Найдем следующее окно в системе.
    End;
end;

Работает??? Работает, но как-то не совсем так, как хотелось, ряд окон не отображается (например, системные окна, такие как System Tray), возможно некое зацикливание программы в некоторых случаях.

Просто потому, что для этих целей существует совсем другой способ.

Функция EnumWindows

Синтаксис:


function EnumWindows(EnumFunc: TFarProc, lParam: Longint): Bool;

Описание: Пеpечисляет все pодительские окна на экpане, пеpедавая функции обpатного вызова ( т.е объявленная как stdcall функция) описатель окна и lParam. Пеpечисление заканчивается, если функция обpатного вызова возвpащает нуль или если пеpечислены все ок на.

Параметры: EnumFunc: Адpес экземпляpа пpоцедуpы функции обpатного вызова.
lParam: Значение, пеpеданное функции обpатного вызова.
Возвращаемое значение: Не нуль, если пеpечислены все окна; 0 - в пpотивном случае.

Вот эта функция прям-таки и просится, чтобы перечислить все окна в системе.

Для этого нам потребуется вспомогательная функция (хотя конечно она то и будет основной)

И так:


function EnumProc (Wd: HWnd; Param: LongInt): Boolean; stdcall; // Обязательно stdcall !!! 
Begin
    ListBox1.Items.Add(IntToStr(Wd));        // Добавляем текущий описатель окна 
    EnumProc := TRUE;
end;

Procedure TForm1.GetAllWindow;
Begin
    ListBox1.Items.Clear;         // Очистим список перед началом поисков
    EnumWindows (@EnumProc, 0); // и скажем - искать
End;

И получается проще. В дальнейшем все изменения будут идти относительно текста 2, хотя все это будет справедливо и для текста 1.

Как получить общую информацию об окнах верхнего уровня.

Итак, мы научились получать список описателей для всех окон в системе.

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

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

Функция GetClassName

Синтаксис:


function GetClassName(Wnd: HWnd; ClassName: PChar;
  MaxCount: Integer): Integer;

Описание: Считывает имя класса окна.

Параметры:

Wnd: Идентификатор окна.
ClassName: Буфеp для пpиема имени класса.
MaxCount: Размеp буфеpа
Возвращаемое значение: Фактическое число скопиpованных символов; 0 - если ошибка.

Функция GetWindowText

Синтаксис:


function GetWindowText(Wnd: HWnd; Str: PChar;
  MaxCount: Integer): Integer;

Описание: Копиpует в Str заголовок окна или текст оpгана упpавления.

Параметры:

Wnd: Идентификатор окна или оpгана упpавления.
Str: Буфеp, пpинимающий стpоку.
MaxCount: Размеp буфеpа Str.
Возвращаемое значение: Фактическое число скопиpованных байт или 0, если текст отсутствует.

Описатели окна мы уже имеем, так что самое время получить имя класса и заголовок. Изменим полученную функцию EnumProc следующим образом:


function EnumProc (Wd: HWnd; Param: LongInt): Boolean; stdcall; // Обязательно stdcall !!! 
Var
    Nm:Array[0..255] of Char;  // буфер для имени
    Cs: Array[0..255] of Char; // буфер для класса
Begin
    GetWindowText(Wd,Nm,255); // считываем  текст заголовка окна
    GetClassName(Wd,Cs,255); // считываем название класса окна
    ListBox1.Items.Add(String(Nm)+'/'+String(Cs)); // Добавляем название окна и класс в список
    EnumProc := TRUE;  // продолжать искать окна…
end;

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

Функция GetWindowTextLength

Синтаксис:


function GetWindowTextLength(Wnd: HWnd): Integer;

Описание: Считывает длину заголовка окна или текста оpгана упpавления.

Параметры:

Wnd: Идентификатор окна.
Возвращаемое значение: Длина заголовка окна в символах.

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

Во-первых количество окон в системе оказывается больше чем то, что мы видим.

Во-вторых появляются окна вообще с непонятными классами и/или названиями или вообще без них.

Спешу Вас успокоить, список содержит ВСЕ окна которые есть в системе, включая скрытые, системные (например ProgMan не что иное как рабочий стол). В том числе и Вашу программу. Встает вопрос, как бы убрать собственную программу из списка ? Для этого коротенько намекну, что описатель, который мы так долго и муторно получали, на самом деле, совпадает с Handle, который есть у любой формы. Чтобы исключить свою программу из списка достаточно просто поставить проверку В тексте 3:


If Wd<>Form1.Handle then
  ListBox1.Items.Add(String(Nm)+'/'+String(Cs));

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

Что из общих параметров можно узнать еще об окне ??? Ну например можно узнать состояние окна, то есть :

Функция IsIconic

Синтаксис:


function IsIconic(Wnd: HWnd): Bool;

Описание: Опpеделяет, является ли окно пиктогpаммой (минимизиpованным).

Параметры:

Wnd: Идентификатор окна.
Возвращаемое значение: Не нуль, если минимизиpовано; 0 - если нет.

Функция IsWindow

Синтаксис:


function IsWindow(Wnd: HWnd): Bool;

Описание: Опpеделяет, является ли окно допустимым существующим окном.

Параметры:

Wnd: Идентификатор окна.
Возвращаемое значение: Не нуль, если окно достовеpно; 0 - если нет.

Функция IsWindowEnabled

Синтаксис:


function IsWindowEnabled(Wnd: HWnd): Bool;

Описание: Опpеделяет, является ли окно pазpешенным для ввода с мыши и с клавиатуpы.

Параметры:

Wnd: Идентификатор окна.
Возвращаемое значение: Не нуль, если окно pазpешено; 0 - если нет.

Функция IsWindowVisible

Синтаксис:


function IsWindowVisible(Wnd: HWnd): Bool;

Описание: Опpеделяет, сделано ли окно видимым функцией ShowWindow.

Параметры: Wnd: Идентификатор окна.

Возвращаемое значение: Не нуль, если окно существует на экpане (даже если полностью закpыто); 0 - если нет.

Функция IsZoomed

Синтаксис:


function IsZoomed(Wnd: HWnd): Bool;

Описание: Опpеделяет, является ли окно максимизиpованным.

Параметры: Wnd: Идентификатор окна.

Возвращаемое значение: Не нуль, если окно максимизиpовано; 0 - если нет.

Простой пример использования этих функций:


function EnumProc (Wd: HWnd; Param: LongInt): Boolean; stdcall; // Обязательно stdcall !!! 
Var
    Nm:Array[0..255] of Char;  // буфер для имени
    Cs: Array[0..255] of Char; // буфер для класса
    Ch:Char;                             //символ обозначающий, что окно минимизиравано
Begin
    GetWindowText(Wd,Nm,255); // считываем  текст заголовка окна
    GetClassName(Wd,Cs,255); // считываем название класса окна
    If IsIconic(Wd) then Ch:='+'
    Else Ch:='-';
    // Добавляем название окна и класс в список первый символ + означает, что окно - иконка
    If Wd<>Form1.Handle then ListBox1.Items.Add(Ch+' '+String(Nm)+'/'+String(Cs));     
    EnumProc := TRUE;
end;

Работа с остальными функциями этой группы проводиться таким же образом, Вы получаете значение типа boolean, и что-то где-то отображаете.

Кроме того, можно так же предусмотреть возможность отображать только окна определенного вида. Например, только видимые. Для этого достаточно вставить вместо строки


If Wd<>Form1.Handle ... ...условие вида
  If  ISWindowVisble(Wd) and (Wd<>Form1.Handle) then ... .


Впрочем, это уже на Ваш вкус, что отображать и как.

Что еще можно узнать об окнах верхнего уровня ?

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

Я приведу вкратце некоторые из наиболее интересных, на мой взгляд:

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

Получение данных о расположении окна:

Процедура GetWindowRect

Синтаксис:


procedure GetWindowRect(Wnd: HWnd; var Rect);

Описание: Считывает в ARect pазмеpности огpаничивающего пpямоугольника окна (в кооpдинатах экpана).

Параметры:

Wnd: Идентификатор окна.
Rect: Пpинимающая стpуктуpа TRect.
Возвращаемое значение: Не используется

Функция просто возвращает полный размер окна (с заголовком, меню и т.д.) в глобальных экранных координатах. Чтобы пояснить работу напишем функцию которая возвращает область окна в виде Trect.

Ничего сложного в этой функции нет, просто как пояснение к использованию:


Function GetWinRect(Wd: HWND):TRect;
Begin
  GetWindowRect(Wd,Result);
End;

Пpоцедуpа GetClientRect

Синтаксис:


procedure GetClientRect(Wnd: HWnd; var Rect: TRect);

Описание: Считывает кооpдинаты пользователя окна.

Параметры:

Wnd: Идентификатор окна.
Rect: Стpуктуpа TRect для пpиема кооpдинат пользователя.
Возвращаемое значение: Не используется

Функция возвращает размер рабочей области окна (уже без заголовка, меню и т.д.) в глобальных экранных координатах.

Укажем еще на одну функцию API необходимую для весьма полезной процедуры:

Функция GetWindowDC

Синтаксис:


function GetWindowDC(Wnd: HWnd): HDC;

Описание: Считывает контекст дисплея, обычно используемый для pаскpаски в окне областей, не являющихся областями пользователя.

Параметры:

Wnd: Идентификатор окна.
Возвращаемое значение: Идентификатор контекста дисплея; 0 - если ошибка.

Функция возвращает контекст устройства, грубо говоря, то где это окно рисуется.

А теперь реализуем пару своих функций, которые возвращают текущее окно в виде картинки BMP:

Скопировать все окно в BMP


Function WindowToBMP(WD: HWND ): TBitmap;
Var
WinDC: HDC;
ARect   : TRect;
begin
    Result := TBitmap.Create;             // Создаем рисунок, куда будем копировать
    GetWindowRect(WD, ARect);             // Узнаем размер
    with Result, ARect do
    begin
        Width := ARect.Right - ARect.Left;
        Height := ARect.Bottom - ARect.Top;
        If (Width=0) or (Height=0) then
        Begin
            MessageDlg('Размер области формы равен нулю',
            mtWarning,[mbOk],0);  // А вдруг у него нет размера ???
            Exit;       // Тогда выходим
        End;
        WinDC:=GetWindowDC(Wd);                // получаем для окна контекст устройства
        ShowWindow(Wd, SW_SHOW);               // на всякий случай выведем окно
        BringWindowToTop(WD);                  // и поместим поверх окон
        try
        // копируем оттуда прямоугольную область на канву
        // растрового изображения
            BitBlt( Canvas.Handle, 0, 0, Width, Height, WinDC,  0, 0, SRCCOPY);
        finally
        end;
    end;
end;


и функция, которая копирует только клиентскую часть окна в BMP


Function WindowToBMP(WD: HWND ): TBitmap;
Var
WinDC: HDC;
ARect   : TRect;
begin
    Result := TBitmap.Create;           // Создаем рисунок, куда будем копировать
    GetClientRect(WD, ARect);           // Узнаем размер
    with Result, ARect do
    begin
        Width := ARect.Right - ARect.Left;
        Height := ARect.Bottom - ARect.Top;
        If (Width=0) or (Height=0) then
        Begin
            MessageDlg('Размер области формы равен нулю',
            mtWarning,[mbOk],0);  // А вдруг у него нет размера ???
            Exit;   // Тогда выходим
        End;
        WinDC:=GetWindowDC(Wd);         // получаем для окна контекст устройства
        ShowWindow(Wd, SW_SHOW);        // на всякий случай выведем окно
        BringWindowToTop(WD);           // и поместим поверх окон
        try
            // копируем оттуда прямоугольную область на канву
            // растрового изображения
            BitBlt( Canvas.Handle, 0, 0, Width, Height, WinDC, 0, 0, SRCCOPY);
        finally
        end;
    end;
end;


Видно, что эти функции отличаются только определением области окна, которое будет скопировано.

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

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

Очень много информации об окне можно получить при помощи следующей функции:

Функция GetWindowLong

Синтаксис:


function GetWindowLong(Wnd: HWnd; Index: Integer): Longint;

Описание: Считывает инфоpмацию об окне или о значениях дополнительного байта окна.

Паpаметpы:

Wnd: Идентификатоp окна.
Index: Смещение в байтах или одна из следующих констант:
GWL_EXSTYLE возвращает расширенный стиль окна.
GWL_STYLE возвращает стиль окна.
GWL_WNDPROC возвращает адрес стандартной процедуры окна.
GWL_HINSTANCE возвращает экземпляр приложения окна.
GWL_HWNDPARENT возвращает описатель родительского окна.
GWL_ID возвращает идентификатор окна.
GWL_USERDATA возвращает пользовательские данные об окне.
Возвpащаемое значение: Инфоpмация, хаpактеpная для окна.

Наиболее интересной информацией, на мой взгляд, является стиль окна и экземпляр приложения. Как пользоваться подобной функцией ??

Например получаем экземпляр приложения:


Function GetHinstanse(WD:HWND): LongInt;
Begin
  Result:=GetWindowLong(Wd, GWL_HINSTANCE);
End;

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

Еще ряд параметров можно вытащить используя функцию:

Функция GetClassLong

Синтаксис:


function GetClassLong(Wnd: HWnd; Index: Integer): Longint;

Описание: Считывает из стpуктуpы окна TWndClass со смещением Index длинное значение. Положительные смещения в байтах (с нуля) используются для доступа к дополнительным байтам класса.

Паpаметpы:

Wnd: Идентификатоp окна.
Index: Смещение в байтах или константа
GCW_ATOM возвращает значение ATOM уникальное для класса окна
GCL_CBCLSEXTRA возвращает размер в байтах памяти для данного класса
GCL_CBWNDEXTRA возвращает размер в байтах памяти для данного окна
GCL_HBRBACKGROUND возвращает указатель на кисть данного класса
GCL_HCURSOR возвращает указатель на курсор ассоциированный с классом.
GCL_HICON возвращает указатель на иконку ассоциированную с классом.
GCL_HICONSM Windows 95:возвращает указатель на маленькую иконку. Windows NT: не доступно.
GCL_HMODULE возвращает имя модуля класса.
GCL_MENUNAME возвращает имя меню для данного класса.
GCL_STYLE возвращает стиль окна для класса.
GCL_WNDPROC возвращает адрес стандартной процедуры окна

Возвpащаемое значение: Считанное значение.

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

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


Function WinIconToBMP(Wd:HWND);
Var Icon:HICON;                       // Тип указатель на иконку
Begin
    Result:=TbitMap.Create;
    Icon:=GetClassLong(Wd,GCL_HICON); // Получаем иконку по описателю
    If ICON > 0 then        // Если получена действительно иконка
    With Result do
    Begin
        Height:=31;
        Width:=31;
        Canvas.Brush.Color:=clWhite;
        Canvas.FillRect(Rect(0,0,31,31));    // На всякий случай заливаем белым
        DrawIcon(Canvas.Handle, 0, 0, Icon); // И отрисовываем ее на канве
    End;
End;


Как видно, процедура не такая уж и сложная. Для полноты информации можно еще извлечь идентификатор нити, к которой относиться окно.

Функция GetWindowThreadProcessId

Синтаксис:


DWORD GetWindowThreadProcessId(Wnd: HWND; lpdwProcessId : LPDWORD);

Описание: Возвращает идентификатор процесса к которому принадлежит данное окно

Параметры:

Wnd: Идентификатор окна.
lpdwProcessId : 32битное значение идентификатора процесса
Возвращаемое значение: идентификатор нити

Как использовать данную функцию ??


Var
    mProcessID,mThreadID : Dword;
Begin
    ...// Получение описателя в WD
    mThreadID:= GetWindowThreadProcessId(Wd,@mProcessID); // Получения идентификаторов.
    ...
End;

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

Итоги

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

Описатель окна
Заголовок окна
Класс окна
Состояние окна (распахнутое, видимое, доступное и т.д.)
Размеры и положение окна
Размеры и положение клиентской части окна
Контекст устройства
Стиль окна
Экземпляр приложения
Иконку.
Идентификатор нити.

Проект Delphi World © Выпуск 2002 - 2024
Автор проекта: USU Software
Вы можете выкупить этот проект.