Delphi programming blog
Источник: http://teran.karelia.pro/articles/item_6148.html
 

Оконные сообщения в FireMonkey

Опубликовано 15.11.2013 г. 18:46

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

    procedure wmTest(var msg : TMessage); message WM_TEST;

Подобный функционал можно использовать в случае, если коды сообещений мы определяем заранее в виде констант в диапазоне от WM_USER до WM_APP. Значения выше этой границы мы также можем использовать, эти значения регистриуются в системе с помощью системного метода RegisterWindowMessage(). Подозреваю, что подобный синтаксис описания классов/методов в языке был введен специально для этих целей, поскольку в Windows архитектуре сообщения используются повсеместно. Однако переходя к фреймворку (или платформе, а вернее к кросс-платформе  FireMonkey/FMX) мы отдаляемся от истоков, и фактически, в нашем приложении окнами, имеющими HWND дескриптор являются только непосредственно сами окна приложения. Код передачи и обработки цикла сообщений скрыт в implementation секции модля FMX.Platform.Win. Собственно,  итог здесь один: если в приложении FireMonkey мы определим метод описанный выше, то он никогда не получит отправленного с помощью Send/PostMessage() сообщения.
Поскольку, повторюсь, реалиазция цикла обработки скрыта в implementation секции модуля, то вклиниться в ее обработку мы попросту никак не можем. Следовательно с этого момента есть два пути:

  • Использовать ловушки сообщений (hooks)
  • Заменить оконную процедуру на свою

Использовать первый метод гораздо проще. Поскольку ловушка устаналивается для приложения в целом и получает сообщения для всех окон. Минус этого метода - ловушка будет вызываться для каждого сообщения, а их многие-многие тысячи. Установка ловушки происходит с помощью метода SetWindowsHookEx.
Второй метод не шибко сложнее. При создании окна нам необходимо переопределить указатель на оконную процедуру. Как это часто бывало в VCL (пр обработке собщений выше WM_APP), наша новая процедура обработки цикла сообщений WndProc, должна вызывать старую процедуру, а затем можем выполнить какие то свои действия.

Код для использования ловушки я вынес в отдельный класс и модуль. Посольку ловушка одна, то методы класса, простите за каламбур, классовые. Код ловушки передает сообщения только диапазона WM_USER..WM_APP. Собствено метод ловушки сначала проверяет значения кода nCode на равенство HC_ACTION, которое говорит нам что мы должны что то обрабатывать. Затем отфильтровыываются значения сообщения не выходящий в диапазон. Затем проверяется дескриптор окна (с помощью метода FindWindow(), это не класический FindWindow из WinApi, а метод возвращающий FMX-дескриптор по значенияю HWND-дескриптора окна). И если форма с нужным дескриптором найдена, то сообщение передается экземпляру формы используя обычный механизм TObject.Dispatch():

unit fmxMessages;

interface

type
    TFmxMessageHook = class(TObject)
      strict private
       class var
        FHook : THandle;
      public
        class procedure  InitMsgHook();
        class destructor Destroy();

        class property Hook : THandle read FHook;
    end;

implementation
uses WinApi.Windows, WinApi.Messages,
     fmx.Forms, fmx.Platform.Win;


    function FmxMsgHookProc(nCode : integer; wParam : WParam; lParam : LPARAM):LResult; stdcall;
    var m : PMsg;
        msg : TMessage;
        cf : TCommonCustomForm;
    begin
        if nCode = HC_ACTION then begin
             m := PMsg(lParam);
             if (m.message >= WM_USER) and (m.message <= WM_APP) then begin
                cf := Fmx.Platform.Win.FindWindow(m.hwnd);
                if assigned(cf) then begin
                    msg.Msg := m.message;
                    msg.WParam := m.wParam;
                    msg.LParam := m.lParam;
                    msg.Result := 0;
                    try
                        cf.Dispatch(msg);
                    except
                    end;
                end;
             end;
        end;

        result := CallNextHookEx(TFmxMessageHook.Hook,
                                 nCode, wParam, lParam);
    end;

class procedure TFmxMessageHook.InitMsgHook();
begin
    FHook := SetWindowsHookEx(WH_GETMESSAGE, fmxMsgHookProc, 0, GetCurrentThreadId());
end;

class destructor TFmxMessageHook.Destroy();
begin
    if FHook > 0 then
        UnhookWindowsHookEx(FHook);
end;

end.

Для того чтобы модуль начал работу в приложении, следует видоизменить код dpr файла вставив строку вызова метода InitHook(), например, после создания главной формы (Application.CreateForm()), и до начала цикцла обработки сообщений Application.Run().

begin
  Application.Initialize;
  Application.CreateForm(TMainForm, MainForm);

  TFmxMessageHook.InitMsgHook();

  Application.Run;
end.

 Все это я к чему. Кросс-платформенность это хорошо, но в Delphi XE6 хотелось бы увидеть маленькое дополнение в методе WndProc модуля fmx.Platform.Windows.  Например, такое:

    case uMsg of
    ........

        WM_USER..WM_APP : begin
            msg.Msg := uMsg;
            msg.WParam := WParam;
            msg.LParam := LParam;
            msg.Result := 0;
            try
                LForm.Dispatch(msg);
            except
            end;
        end;
    .......

    end;

В общем-то ограничиваться данными константами смысла нет, но это значительно упростит жизнь разработчикам которые хотят писать Windows-проложения  в FMX. Фактически, сейчас если вы хотите в приложении использовать, например списки переходов, то вам необходимо для начала получить сообщение о том, что кнопка на панели задач была создана. А для этого вам и потребуется использовать либо хук, либо переопределять оконную процедуру. В VCL переопределение было гораздо проще, поскольку можно было перекрыть метод DefaultHandler класса формы, добавив туда необходимый код.

Метки:  api  |  FireMonkey 

Комментарии

Роман Янковский
16.11.2013 в 14:54
Меткое замечание. Действительно, очень увлеклись кроссплатформенностью, в результате специфичные для каждой платформы фичи совсем потерялись.
Ярослав
18.11.2013 в 13:50
Хорошая статья.

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

Потому что на Windows 7 эта функция работает не так как на XP. А именно не дает просто задать хук. А требует повышенных привелегий, которые по-моему задаются как раз через манифест.

Спасибо.
teran
19.11.2013 в 11:51
вот уж хз :) я на столько глубоко в проблему не вдавался.
с манифестом ничего не делал, в MSDN про разницу в вин7 и винХп не видел.

допускаю что привилегии нужны при установке DEBUG хука, а так то какая кому разница, какой хук я на свое же окно повешу.
Константин
19.11.2013 в 01:27
Ярослав, мы в DevExpress используем хуки изначально. Никаких проблем от пользователей не было. Никаких специальных телодвижений по элевейтингу прав не припомню. Единственная проблема была из-за ограниченного стека для 32-битных приложений в 64-битной среде. Там очень лимитирована цепочка хуков, поэтому мне пришлось эмулировать её в коде. Но если честно, хуки это всегда костыли, когда архитектура не позволяет сделать нормально. Кстати, имхо, для FM, где число WinAPI окон сильно ограничено, городить хуки лишнее. Проще дать платформенный интерфейс для кустомизации WndProc
teran
19.11.2013 в 11:26
да, хуки не лучший способ, а скорее худший
Григорий
04.09.2017 в 12:55
Не тебе, а мне пришлось эмулировать :-)
B195868 я закрывал :-)
Георгий
20.11.2013 в 14:50
Господа, случаем не у кого нет идеи полностью кроссплатформенного решения?
woojin
27.03.2014 в 03:33
а не подскажите как отловить сообщение на изменение размера окна, при его растягивании или уменьшении
когда берёшься или за какой то из углов или за сторону
Nice post. I was checking continuously this blog and I am
impressed! Very helpful info particularly the
last part :) I care for such information much.
I was looking for this particular info for a long time.
Thank you and good luck.
- Имя
- e-mail*
- Сайт
вы можете использовать теги [i],[b],[code],[quote]
Дополнительно