суббота, 5 февраля 2011 г.

Неудачный эффект WM_SETREDRAW

Это перевод The Unfortunate Effect of WM_SETREDRAW. Автор: François Gaillard.

...или почему простая замена LockWindowUpdate на WM_SETREDRAW не так прямолинейна.

Как вы знаете, когда вам нужно избежать мерцания или множественных перерисовок формы во время набора операций обновления, даже хотя это очень заманчиво, но вы не должны использовать LockWindowUpdate. Сегодня документация Windows уже обновлена, в отличие от ранних дней, когда она просто манила людей использовать эту функцию для ошибочной цели - как в деталях пояснил Реймонд Чен.

Но простая замена LockWindowUpdate(MyForm.Handle) на SendMessage(MyForm.Handle, WM_SETREDRAW, 0, 0) для всей формы также не является решением. Даже хотя это иногда рекомендуют.

Не, это отлично заблокирует перерисовку формы - не вопрос. А если это используется только на короткий промежуток времени, то вы можете и не заметить проблему.

Симуляция длительных вычислений с изменением интерфейса

Создайте новое VCL приложение и поместите на его главную форму две кнопки. На обработчик первой кнопки назначьте такой код:
procedure TForm1.Button1Click(Sender: TObject);
begin
  Button1.Width := Button1.Width * 2;
  Repaint;
  sleep(2000);
  Button1.Width := Button1.Width div 2;
  Repaint;
end;
Если вы нажмёте на кнопку, её размер изменится, затем приложение замрёт на 2 секунды (вроде как аналог длительной работы), а потом размер вернётся к начальному. Если вы щёлкните на форме, пока она "заморожена", то ничего не произойдёт, если только у вас не назначен обработчик её OnClick - в этом случае событие сработает после "отмерзания" формы.

Если вам нужно заблокировать изменения интерфейса

Вы можете обернуть весь код в WM_SETREDRAW (на манер того, как вы сделали бы это с LockWindoUpdate):
procedure TForm1.Button1Click(Sender: TObject);
begin
  SendMessage(Handle, WM_SETREDRAW, Ord(False), 0);
  try
    Button1.Width := Button1.Width * 2;
    Repaint;
    sleep(2000);
    Button1.Width := Button1.Width div 2;
    Repaint;
  finally
    SendMessage(Handle, WM_SETREDRAW, Ord(True), 0);
  end;
end;
Запустите этот код теперь. Вроде всё нормально: размеры кнопок визуально не меняются. Но постойте!

Попробуйте щёлкнуть где угодно на форме, пока она занята.

Оопс! Вы щёлкнули сквозь неё, как если бы она не существовала!

Это даже более очевидно, когда форма расположена поверх редактора кода Delphi: как только вы щёлкните на кнопку формы, курсор изменит форму с обычной стрелки Arrow на текстовый IBeam.

Попробуйте с Блокнотом

Это на случай, если вы думаете, что это какой-то хитрый глюк в VCL - вы можете вставить код ниже на вторую кнопку. Он откроет Блокнот с новым документом и заблокирует его на 3 секунды.

Примечание: вам нужно адаптировать заголовок окна в параметрах FindWindow.
procedure TForm1.Button2Click(Sender: TObject);
var
  h: THandle;
begin
  ShellExecute(Handle, nil, 'Notepad.exe', '','', SW_SHOWNORMAL);
  sleep(100);
  // Вам может понадобится изменить эти параметры
  h := FindWindow('Notepad', 'Untitled - Notepad');
  if IsWindow(h) then 
  begin
    SendMessage(h, WM_SETREDRAW, Ord(False), 0);
    try
      sleep(3000);
    finally
      SendMessage(h, WM_SETREDRAW, Ord(True), 0);
    end;
  end;
end;
Попробуйте щёлкнуть где угодно в окне Блокнота, пока оно заморожено - вы "прощёлкнете" сквозь него!

Что если вам нужно запрещать перерисовку формы во время какой-то работы?

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

А если у вас такого нет, то вы всегда можете вставить промежуточную Panel с alClient прямо на форму - как главный контейнер, прослойку между формой и компонентами на ней.

Полезный совет: в отличие от DisableControls/EnableControls у DataSet, WM_SETREDRAW не учитывает вложенные вызовы - не важно сколько раз вы вызовите WM_SETREDRAW с False (хотя лишний раз их не слать - вполне желательно). Вам просто нужно убедиться, что вы вызовите WM_SETREDRAW с True в конце всего один раз.

Почему есть такое странное поведение?

Поведение по умолчанию для WM_SETREDRAW - скрытие окна, но без обновления экрана. Остаётся окно-призрак.

Это немного было объяснено Реймондом Ченом в последнем посте: Существует реализация WM_SETREDRAW по умолчанию, но вы можете сделать и лучше.

3 комментария:

  1. Вполне нормальное поведение: поощряет правильный стиль программирования. А правильный стиль - это: отключать контролы на время работы. Enabled им в False - и готово.

    ОтветитьУдалить
    Ответы
    1. Предсталяю, как будет мерцать ваша форма, если при её растягивании/сжатии вы будете постоянно делать Enable/Disable для всех контролов на форме.

      Удалить
  2. Обнаружил, что компонент TToolBar неадекватно реагирует на WM_SETREDRAW: после восстановления меняются позиции TToolButton'ов, также появляются артефакты. Капризный компонент! Проблема решилась следующими магическими действиями:
    1) Поместил TToolBar на TPanel, у которой установил FullRepaint := False;
    2) После восстанавливающего WM_SETREDRAW потребовалось ещё вызвать TToolBar.Invalidate;
    3) Но и это ещё не всё. Некоторые компоненты (например, TComboBox), размещённые не TToolbar'е, требуют для себя персонального Invalidate.
    После этого волшебным образом заработало, как надо.

    ОтветитьУдалить

Можно использовать некоторые HTML-теги, например:

<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>

Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку.

Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.

Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.

Примечание. Отправлять комментарии могут только участники этого блога.