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

Смена MDIChild формы#2

Опубликовано 30.09.2010 г. 22:05
В прошлый раз я пытался найти решение для задачи обработки события смены активной MDI формы приложения. Самостоятельно мною решение найдено так и не было, однако комментарий JayDi помог мне решить данный вопрос.
Совет заключался в использовании ловушек. Сам я никогда ловушки не использовал, поэтому мало представляю с чем это едят. Общая суть, насколько я понимаю следующая: ловушка - функция обратного вызова, которая является некоторым фильтром цикла сообщений всего приложения. Есть еще системные глобальные ловушки но это к нам не относится. Устанавливаются ловушки с помощью вызова SetWindowsHookEx, первый параметр которой определяет тип ловушки. Нас в данном случае могут интересовать два варианта: WH_CALLWNDPROC и WH_CALLWNDPROCRET. Первый тип ловушки вызывается перед передачей сообщения в окну, следовательное данная процедура может каким то образом повлиять на сообщение в принципе. Второй тип ловушку вызывается после передачи и обработки сообщения приложением, т.е некоторый пост-фильтр. Таким образом, поскольку нам нужен только факт отправки такого сообщения, то мы остановимся на WH_CALLWNDPROCRET. Вторым параметром функции установки ловушки является сама функция ловушки. Тип данной функции определен как TFNHookProc и имеет следующую сигнатуру:
TFNHookProc = function (code: Integer; wparam: WPARAM; lparam: LPARAM): LRESULT stdcall;
Если мы создаем ловушку для текущего приложения, или его дочернего, то третий параметр процедуры должен быть нулем, а последний 4й параметр - идентификатор потока, в нашем случае это главный поток приложения, ID которого мы можем получить с помощью getCurrentThreadID(). Вернемся к функции ловушки. Как видно она имеет три параметра: первый code определяет, надо ли нам обрабатывать сообщение, второй - текущим процессом или нет отправлено сообщение... На этом этапе я вчера сделал большую глупость (: если сообщение внешнее, то второй параметр равен нулю. И тут я решил посмотреть как часто приходят внешние сообщения (: написав для этого код типа if wParam = 0 then showMessage('!'); Через 5 минут окошки сообщений съели 1,5ГБ оперативы. Винда стала подтупливать. Пришлось идти в ребут. Параметр lParam содержит адрес на структуру с описание сообщения. Теперь от нас потребуется написать сам код ловушки, руководствуясь несколькими правилами: обрабатывать сообщение в случае если code = HC_ACTION, далее нам потребуется отловить сообщение WM_MDIATIVATE и затем уведомить главную форму о переключении активной дочерней. Сначала я написал немного сложный код. С помощью postMessage в главную форму передавался сам заголовок нового окна. Т.е из сообщения TWMMDIActivate вытаскивался обработчик активного окна, с помощью getWindowText определялся его заголовок, при этом под него выделялась память. Далее указатель на заголовок передавался с сообщением в главную форму. Но зачем так мудрить, главная форма и так знает какая дочерняя активна. Необходимо только уведомить ее о смене. В конце своей работы функция ловушка должна вызвать следующую ловушку с помощью CallNextHookEx(); Когда ваша функция ловушка вам больше не требуется, то с помощью UnhookWindowsHookEx она отключается. На что же теперь похоже наше приложение?
var mdiHook : THandle;

    function mdiActivateHook(code : integer; wParam, lParam : cardinal):integer; stdcall;
    begin
        if code = HC_ACTION then begin
            if PCWPRetStruct(lParam).message = WM_MDIACTIVATE then begin
                PostMessage(application.MainForm.Handle, WM_MDI_CHILD_CHANGED, 0, 0);
            end;
        end;
        result := CallNextHookEx(mdiHook,code, wParam, lParam);
    end;

begin
    Application.Initialize;
    Application.MainFormOnTaskbar := True;
    Application.CreateForm(TForm1, Form1);

    mdiHook := SetWindowsHookEx(WH_CALLWNDPROCRET, @mdiActivateHook, 0, getCurrentThreadId());

    Application.Run;

    if mdiHook > 0 then
        UnhookWindowsHookEx(mdiHook);
end.
главная форма приложения приняв сообщение смены
const WM_MDI_CHILD_CHANGED = WM_USER;
....
  private
    procedure wmMDIChildChanged(var msg : TMessage); message WM_MDI_CHILD_CHANGED;
Выполнит, например, такое действие:
procedure TForm1.wmMDIChildChanged(var msg: TMessage);
begin
    panel1.Caption := self.ActiveMDIChild.Caption;
    msg.Result := 0;
end;
Чтобы заголовок формы менялся так же при создании новой формы необходимо обработать сообщение WM_MDICREATE. Один минус - при смене окна, главная форма будет получать два одинаковых сообщения, поскольку WM_MDIACTIVATE отсылается двум дочерним окнам.
Метки:  MDI  |  hook 

Комментарии

Chaa
01.10.2010 в 07:14
Если посмотреть код VCL, то можно увидеть, что все дочерние MDI формы в ответ на WM_MDIACTIVATE вызывают функцию PaletteChanged у главной формы.
Поэтому в главную форму добавляем функцию
function PaletteChanged(Foreground: Boolean): Boolean; override;

В ее конструктор
ControlState := ControlState + [csPalette];

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

Еще как вариант - возможно более правильный - обрабатывать через TApplicationEvents событие Application.OnIdle и в нем проверять, не изменилась ли дочерняя форма, и при ее смене обновлять заголовок окна. Так работаю все TAction, и это стандартное решение.
ter
01.10.2010 в 10:04
про paletteChanged видел, однако из за незнания что есть csPalette и на что это влияет использовать не стал.

по поводу onIdle вариант хороший, но не будет ли задержек при смене заголовка?
Chaa
01.10.2010 в 11:09
PaletteChanged это из старых времен для 16 или 256 цветов на мониторе. Когда при смене дочернего окна менялся набор этих самых цветов.

В OnIdle задержки не будет, по крайней мере заметной на глаз. Кнопки на тулбарах в нем обновляются, и задержки обновления не заметно.
ter
01.10.2010 в 15:09
и зачем этот paletteChange тащить с древних времен? если не используется фактически сейчас. совместимости ради?

самый эффективный вариант видимо таки остается наследование. ибо заголовок будет меняться именно тогда, когда это надо. один раз. в отличии от хука и onIdle которые вызываются часто и лишь будут выполнять лишние действия.
sw
01.10.2010 в 22:04
Вы рассматриваете вариант, когда заголовок MDI окна статичен. А если заголовок меняется по каким-то причинам (событиям)? Например, в заголовке отображается прогресс (в процентах), или что-то ещё.

Конечно такое можно предусмотреть путём наследования MDI-форм от некой базовой MDI-формы. Но для _уже_существующих_ приложений (с большим кол-вом форм) - это не выход.

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

Отсюда, как вывод, самый *простой + рабочий* вариант - обработчик OnIdle приложения. Тем более, как заметил Chaa, это самый _естественный_ для Delphi способ обновления состояний Action'ов и связанных с ними контролов.
ter
02.10.2010 в 00:37
ок. соглашусь.
спасибо за советы (:
aap
11.10.2010 в 10:31
Добрый день. Намного удобней все MDI формы наследовать от одной "базовой" формы. В этой базовой форме переопределить метод Activate где собственно и посылать сообщение. Как-то так:

type
TfrmMDIForm = class(TfrmDefForm)
protected
procedure Activate; override;
end;

procedure TfrmMDIForm.Activate;
begin
inherited Activate;
SendMessage(Application.MainForm.Handle, UM_MDI_CHILD_ACTIVE, Self.Handle, 0);
end;
ter
11.10.2010 в 20:57
об этом писалось в предыдущем посте на тему
http://blog.karelia.ru/teran/?p=358 (:
- Имя
- e-mail*
- Сайт
вы можете использовать теги [i],[b],[code],[quote]
Дополнительно