пятница, 12 августа 2011 г.

Что делает стиль оконного класса CS_OWNDC?

Это перевод What does the CS_OWNDC class style do? Автор: Реймонд Чен.

Вспомните, что оконные DC (device context - контексты устройств) обычно используются только временно. Если вам нужно нарисовать что-то на окне, то вы вызываете BeginPaint или, если рисование происходит вне цикла рисования, GetDC (хотя вам следует избегать прорисовки вне цикла рисования). Оконный менеджер создаёт для окна DC и возвращает его. Вы используете полученный DC для рисования, а затем восстанавливаете состояние окна, возвращая DC оконному менеджеру вызовом EndPaint (или ReleaseDC). Внутренне, оконный менеджер хранит небольшой кэш DC, который он использует, когда его просят дать DC для окна, и когда DC возвращается - он помещается обратно в кэш (прим.пер.: отдельный кэш каждому потоку, кэш - per-thread). Поскольку оконные DC используются только временно, то число кэшированных DC обычно не сильно велико, так что небольшой кэш достаточен, чтобы удовлетворять запросы на DC в типичной работающей системе.

Если вы регистрируете оконный класс с флагом CS_OWNDC в классовых стилях, то оконный менеджер создаст DC для окна и поместит его в кэш, пометив специальным флагом "Не удалять этот DC из кэша, потому что он принадлежит окну с CS_OWNDC". Если вы вызовите BeginPaint или GetDC для получения DC окна со стилем CS_OWNDC, то этот DC всегда будет найден в кэше и всегда будет помещаться обратно в него (потому что он был помечен "никогда не удалять"). Из этого есть следствия: хорошие, плохие и ещё хуже.

Хорошая часть - поскольку DC был создан специально для окна и никогда не удаляется, то вам не нужно беспокоиться об очистке DC перед его возвращением в кэш. Всякий раз, когда вы вызываете BeginPaint или GetDC для окна с CS_OWNDC - вы всегда получаете назад один и тот же "специальный" DC. В самом деле, именно в этом смысл окон с CS_OWNDC: вы можете создать окно с CS_OWNDC, получить его DC, настроить так, как вам угодно (шрифты, цвета, стили и т.п.) - один раз, а затем при каждом повторном получении DC вы будете получать ровно этот же DC со всеми вашими настройками - в том состоянии, в котором вы его оставили.

Плохая часть - вы берёте нечто, что было предназначено только для временного использования (оконный DC), и используете его постоянно. Ранние версии Windows имели очень сильные ограничения на DC (восемь или около того), так что освобождать DC как можно быстрее было крайней необходимостью. Это ограничение было существенно ослаблено, но общий принцип остаётся тем же: вам не следует беспечно выделять DC. Вы могли заметить, что реализация CS_OWNDC использует кэш DC, который также используется для обслуживания всех прочих DC; просто DC от окон CS_OWNDC получают специальную отметку, так что оконный менеджер работает с ними особым образом. Это означает, что большое количество DC от окон с CS_OWNDC могут забить кэш DC, замедляя выполнение вызовов BeginPaint и ReleaseDC, которым нужно производить поиск в кэше.

(Почему мы не оптимизировали менеджер DC для случая с большим количеством окон с CS_OWNDC? Во-первых, как я уже указал, оригинальный DC менеджер не беспокоился об этом случае по той простой причине, что в системе вообще не могло быть большого количества DC любого типа. Во-вторых, даже после поднятия ограничения на число DC, было не много смысла в переписывании менеджера DC для оптимизации обработки DC с CS_OWNDC, потому что программисты уже знали, что их надо использовать экономно. Это один из практических аспектов разработки программного обеспечения: вы можете сделать только вот столько. Всё, что вы решите делать, будет стоить вам чего-то ещё. Трудно оправдать оптимизацию сценария, которого вы советуете избегать и которого на самом деле уже избегают программисты. Вы не оптимизируете случаи злоупотребления вашей системой. Это всё равно что тратить время специально на проектирование двигателя автомобиля так, чтобы он поддерживал хороший расход топлива, когда в автомобиль не залито масло.)

А худшая часть заключается в том, что практически весь код по работе с окнами в библиотеках и фреймворках подразумевает, что ваши окна не имеют стиля CS_OWNDC. Посмотрите на следующий код, который рисует текст двумя разными шрифтами, используя первый шрифт для опорной точки по размещению букв второго шрифта. Выглядит вполне нормально, не так ли?
procedure FunnyDraw(hwnd: HWND; hf1, hf2: HFONT);
var
  hdc1, hdc2: HDC;
  hfPrev1, hfPrev2: HFONT;
  taPrev1: UINT;
  Text: String;
  psz: PChar;
  pt: TPoint;
begin
  hdc1 := GetDC(hwnd);
  hfPrev1 := SelectFont(hdc1, hf1);
  taPrev1 := SetTextAlign(hdc1, TA_UPDATECP);
  MoveToEx(hdc1, 0, 0, NULL);
  hdc2 := GetDC(hwnd);
  hfPrev2 := SelectFont(hdc2, hf2);
  
  Text := 'Hello';
  psz := PChar(Text);
  while psz^ <> #0 do
  begin
    GetCurrentPositionEx(hdc1, @pt);
    TextOut(hdc2, pt.x, pt.y + 30, psz, 1);
    TextOut(hdc1, 0, 0, psz, 1);
    Inc(psz);
  end;
  
  SelectFont(hdc1, hfPrev1);
  SelectFont(hdc2, hfPrev2);
  
  SetTextAlign(hdc1, taPrev1);
  
  ReleaseDC(hwnd, hdc1);
  ReleaseDC(hwnd, hdc2);
end;
Мы получаем два DC для окна. В первом мы устанавливаем шрифт 1, во втором - шрифт 2. В первом DC мы также устанавливаем выравнивание текста в TA_UPDATECP, что означает игнорирование координат, передаваемых в функцию TextOut. Вместо этого текст рисуется в "текущей позиции", и "текущая позиция" смещается после вывода текста, так что следующий вызов TextOut продолжит в том месте, где закончил свою работу предыдущий.

Когда оба DC настроены, мы рисуем строку символ за символов. Мы запрашиваем у первого DC текущую позицию и рисуем символ на втором DC в этой же позиции, но чуть ниже, после чего рисуем символ на первом DC (и это рисование также сдвигает текущую позицию).

После окончания цикла рисования мы восстанавливаем исходные состояния обоих DC.

Смысл функции - нарисовать что-то вроде этого, где первый шрифт больше, чем второй (чем-то напоминает тень от текста):

Hello
Hello

И если окно не имеет стиля CS_OWNDC, то вы получаете желаемый результат. Но если у окна установлен стиль CS_OWNDC, то произойдут плохие вещи. Попробуйте сами - и вы получите такой вывод:

HHeelllloo

Конечно же, если вы понимаете, как работает CS_OWNDC, то навряд ли такой результат будет для вас неожиданностью. Ключ к пониманию - запомнить, что когда окно имеет стиль CS_OWNDC, то GetDC всегда возвращает один и тот же DC, не важно сколько раз вы его вызвали. Теперь вам нужно просто посмотреть на код функции FunnyDraw, помня о том, что hdc1 равно hdc2:
procedure FunnyDraw(hwnd: HWND; hf1, hf2: HFONT);
var
  hdc1, hdc2: HDC;
  hfPrev1, hfPrev2: HFONT;
  taPrev1: UINT;
  Text: String;
  psz: PChar;
  pt: TPoint;
begin
  hdc1 := GetDC(hwnd);
  hfPrev1 := SelectFont(hdc1, hf1);
  taPrev1 := SetTextAlign(hdc1, TA_UPDATECP);
  MoveToEx(hdc1, 0, 0, NULL);
Пока что выполнение функции идёт отлично.
  hdc2 := GetDC(hwnd);
Поскольку у окна установлен стиль CS_OWNDC, то DC, возвращаемое в hdc2 - ровно то же, что и в hdc1. Другими словами, hdc1 = hdc2! Вот теперь становится интересно.
  hfPrev2 := SelectFont(hdc2, hf2);
Поскольку hdc1 = hdc2, то на самом деле этот код удалит из DC шрифт hf1 и установит в него шрифт hf2.
  Text := 'Hello';
  psz := PChar(Text);
  while psz^ <> #0 do
  begin
    GetCurrentPositionEx(hdc1, @pt);
    TextOut(hdc2, pt.x, pt.y + 30, psz, 1);
    TextOut(hdc1, 0, 0, psz, 1);
    Inc(psz);
  end;
Вот теперь этот цикл окончательно рассыпается. На первой итерации мы получаем текущую позицию и это будет (0, 0), потому что мы ещё не двигались. Затем мы рисуем букву "H" в позицию (0, 30) на второй DC. Но поскольку он равен первому, то на самом деле мы выводим "H" на первый DC - а у него установлен режим TA_UPDATECP. Поэтому координаты игнорируются, а буква "H" показывается (вторым шрифтом) в позиции (0, 0), после чего текущая позиция смещается вправо, после буквы "H". Наконец, мы рисуем "H" на первый DC (а это ровно он же). Мы думаем, что мы рисуем его первым шрифтом, но на самом деле - вторым. Мы думаем, что мы рисуем его в (0, 0), но на самом деле - в (x, 0), где x - ширина буквы "H", потому что первый вызов TextOut(hdc2, ...) сместил текущую позицию.

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

Но постойте, катастрофа ещё не случилась. Посмотрите на наш код очистки:
  SelectFont(hdc1, hfPrev1);
Это восстанавливает исходный шрифт в DC.
  SelectFont(hdc2, hfPrev2);
А этот код выбирает в DC первый шрифт! Мы не сумели восстановить DC в исходное состояние, что привело к возврату "недопустимого" DC в кэш менеджера окон.

Вот почему я описываю стиль CS_OWNDC как "наихудший". Потому что он приводит к тому, что код, который работает, перестаёт работать - нарушением предположений, которые делают о DC большинство людей (часто даже не осознавая этого).

И теперь вы думаете, что CS_OWNDC - это плохо. В следующий раз я покажу вам, что такое по-настоящему плохо - катастрофа по имени CS_CLASSDC.

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

  1. А зачем оно вообще существует, раз оно так плохо? Ошибки проектирования ОС?

    ОтветитьУдалить
  2. Подозреваю, что CS_OWNDC появился в стародавние времена, когда он ещё мог приносить пользу - для окон, интенсивно выполняющих перерисовку.

    ОтветитьУдалить
  3. Вполне логично, что CS_OWNDC установлен для класса элемента управления MCIWnd, который занимается отображением видео.

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

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

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

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

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

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

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