пятница, 20 декабря 2013 г.

Задачка на понимание №2

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

Итак, на скорую руку ваяем приложение в котором есть форма, кнопка и TMemo.
Дано 4 варианта реализации кода обработчика кнопки:

var
  ACanvas: THandle;
  AText: string;
begin
  ACanvas := Canvas.Handle;
  AText := Memo1.Lines[0];
  TextOut(ACanvas, Button1.Left, 20, PChar(AText), Length(AText));
end;

...

begin
  ACanvas := Canvas.Handle;
  AText := Memo1.Text;
  TextOut(ACanvas, Button1.Left, 20, PChar(AText), Length(AText));
end;

...

begin
  AText := Memo1.Lines[0];
  ACanvas := Canvas.Handle;
  TextOut(ACanvas, Button1.Left, 20, PChar(AText), Length(AText));
end;

...

begin
  AText := Memo1.Text;
  ACanvas := Canvas.Handle;
  TextOut(ACanvas, Button1.Left, 20, PChar(AText), Length(AText));
end;

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

В результате на форме будет либо выведен текст, либо он будет не выведен, либо уплывет бэкграунд (ХР и ниже), но текст все равно отобразится.

Задача выглядит следующим образом:



1. Определить какой из вариантов кода правилен.
2. Объяснить причину такого поведения.
3. Если выполнили первые два пункта - объяснить причину отрисовки в неправильных вариантах (ХР и ниже).

Дам пояснения:
Тот кто даст ответ на первый вопрос - хорошист. Время решения - 10 минут (без использования компьютера).
Тот кто разъяснит второй вопрос - отличник, как минимум. Время решения - час. (Отладчик в руки и вперед - пока не найдете).

А вот с третьим вопросом сложнее, такое поведение можно воспроизвести только под ХР и ниже, да и до кучи ответ на данный вопрос, как минимум требует очень хорошего знания книжек Фень Юаня, откуда можно вынести понимание данного поведения (ну этот ответ конечно на 5 с плюсом, но таких подробностей на собеседованиях я никогда не требовал, ибо дай то бог решить второй пункт :)

Удачи :)

UPD:
Для тех кто все-же против того что рисуется не в OnPaint, вот такой вариант кода:

type
  TForm1 = class(TForm)
    Button1: TButton;
    Memo1: TMemo;
    procedure FormPaint(Sender: TObject);
    procedure Button1Click(Sender: TObject);
  private
    ACanvas: THandle;
    AText: string;
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.Button1Click(Sender: TObject);
begin
  ACanvas := Canvas.Handle;
  AText := Memo1.Lines[0];
  Repaint;
end;

procedure TForm1.FormPaint(Sender: TObject);
begin
  if ACanvas <> 0 then
    TextOut(ACanvas, Button1.Left, 20, PChar(AText), Length(AText));
end;


17 комментариев:

  1. Думаю, что aCanvas.Handle надо "дёргать" в первую очередь

    ОтветитьУдалить
  2. А потом уже Memo1.Text.

    Потому что он посылает WM_GetText.

    А Memo1.Lines[0] лишь "надстройка".

    Т.е. правильный вариант - ВТОРОЙ. Мне так кажется...

    ОтветитьУдалить
  3. Хотя... ПОСЛЕДНИЙ наверное...

    Сначала GetText, а потом Handle.

    Почему?

    Потому что иначе GetText может "дёрнуть" Handle контрола, а он - пересоздать окно.

    ОтветитьУдалить
  4. Ответы
    1. Хм, нюанс...
      Такое поведение гарантированно воспроизводится начиная с Delphi7 по ХЕ4 включительно (остальных версий у меня нет в наличии), но думаю и на остальных будет такая же петрушка.

      Удалить
  5. Признаюсь, ответа не знаю и поиск по исходникам мне не помог.
    Это даже не ответ на поставленный вопрос, но 1-й вариант не отрисовывает совсем ничего.

    ОтветитьУдалить
  6. 1) Сразу выбрал варианты 3 и 4 т.к. создание hdc лучше делать непосредственно перед "использованием", ну или если через tcanvas - использовать методы lock/unlock
    2) На этом этапе к выбранным 3 и 4 добавляется вполне корректно работающий вариант 2. Значит вся "соль" в разности работы 1 и 2. Более грамотного объяснения чем следующее дать не смогу:
    Первый вариант не работает т.к. происходит смена контекста вывода при обработке сообщения EM_GETLINE, отправка которого осуществляется при запросе строки - AText := Memo1.Lines[0]. При работе второго варианта смена контекста не выполняется.:)
    3) Могу только предположить, что на вывод (корректность вывода) в ОС XP и ниже могут оказать различные параметры выравнивания текста.

    ОтветитьУдалить
  7. Нет правильного варианта. 1. Неправильное место (button click), 2. Канвас не подготовлен для вывода текста

    ОтветитьУдалить
  8. В VCL всё рисование всегда заключается в Lock/Unlock (Paint, PaintTo, OnPaint и т.п.), либо производится на локально выделенном DC. Соответственно, если этот код вызывать в OnPaint, то работать он будет. Иначе нет гарантий, что канва не будет пересоздана. По этому соображению методы 3 и 4 "более правильные", т.к. канва берётся позднее и нет промежуточных операций.

    Далее, есть подозрение, что разница между методами в том, что один из них дёргает SendMessage, а второй - Perform. Соответственно, SendMessage приводит к полноценному циклу обработки, в котором, кажется, вызывается FreeDeviceContexts.

    В целом, я бы заключил код в Lock/Unlock, либо выполнял бы его из OnPaint.

    ОтветитьУдалить
  9. Интересно... почему до сих пор никто не увидел самого простого в этих вариантах..
    В задаче явно не оговорено содержимое Memo, а значит Memo1.Lines[0] не может быть правильным, т.к. вернёт только первую строку многострочного текста (в отличии от Memo1.Text).

    Почему-то я думаю, что именно это и есть ответ на 4ку :)

    ОтветитьУдалить
  10. Хотя, если предположить, что текст заранее определён однострочным (неточность в условии), то тут есть что подумать (насчёт уплывания фона и прочих неприятностей).

    У меня мысли такие.
    OnClick - это событие, которое приходит из очереди сообщений. Следом за OnClick пойдёт перерисовка кнопки в отпущенном состоянии (т.к. перед OnClick она была нарисована нажатой), перерисовку "дёрнет" сообщение типа WM_PAINT, а перед ним, обычно, приходит ERASEBACKGROUND. Эти сообщения на перерисовку инициируется вызовом Invalidate (или InvalidateRect). Таких вызовов в общем случае может быть много, поэтому ОС их запоминает, а ERASEBACKGROUND и PAINT получаются отложенными до определённого момента.

    Соответственно вызовы Memo.Lines[0], Memo.Text, Canvas.Handle - могут как-то повлиять на очередь WM_PAINT.

    Это я пишу по памяти, т.к. ковырял VCL, читал Фень Юаня (замечательнейшая книга!) и писал компоненты.

    Поэтому, если взять в руки отладчик, то на что обращать внимание - для меня понятно. Но я подожду ответа уважаемого (без сарказма) автора. Спасибо

    ОтветитьУдалить
  11. >> 1. Определить какой из вариантов кода правилен.
    Я тоже считаю, что правильного варианта здесь нет. Рисовать нужно в OnPaint.

    ОтветитьУдалить
  12. Для тех кто против отрисовки вне OnPaint, добавил и такой вариант. :)

    ОтветитьУдалить
  13. Rouse_, а текст-то всё же однострочный или нет?

    ОтветитьУдалить
    Ответы
    1. В данном случае роли не играет, допустим, пусть будет однострочный.

      Удалить
  14. Я один, что ли тут на := Memo1.Lines[0]; смотрю с большим опасением? У Lines, что ли не бывает Count = 0? Я посмотрю, конечно. Но, даже если так, то сам такие вещи делать бы всё равно не стал - Index out of bounds - плохая штука.

    ОтветитьУдалить
  15. TCanvas в данном случае является TControlCanvas, для которых Device context'ы хранятся в глобальном "пуле" ограниченного размера. При создании нового контекста, VCL приходится освобождать место в пуле, освобождать ранее созданные контексты.

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