графічні об'єкти
Для виведення примітивів потрібні певні настройки: установка кольору і товщини ліній ( «пера»), виду заливки суцільних областей ( «кисті»), розмірів і накреслення шрифтів і так далі. Як правило, різні графічні програмні інтерфейси зберігають такі параметри як власних структур даних - графічних об'єктів. Це справедливо як для GDI, так і для GDI +, однак використання об'єктів різниться. Розглянемо ці відмінності на прикладі маленької завдання - малювання квадрата.
Stateful model в GDI
У GDI використана програмна модель, яка називається stateful model. Це означає, що для пристрою виведення запам'ятовуються зроблені настройки і є поняття поточного обраного об'єкта. Перед виведенням примітивів необхідний графічний об'єкт потрібно вибрати в пристрій виведення (за допомогою функції SelectObject). Встановивши таким чином, наприклад, товщину лінії в 3 пікселі, потім можна вивести кілька відрізків обраної товщини:
Такий підхід має свої переваги - один раз встановивши вбрання перо (HPEN), не потрібно щоразу передавати його в функцію виведення. Однак простота дуже часто оберталася серйозними проблемами - сумно відомої витоком ресурсів.
Погляньте на виділену рядок. Навіщо знадобилося запам'ятовувати і відновлювати в контексті старе перо, якщо воно все одно не використовувалося для малювання? Вся справа в тому, що обраний в контексті графічний об'єкт не може бути видалений - така вже архітектура була обрана при створенні GDI. Отже, без цього рядка виклик DeleteObject завершиться невдало, і створене перо залишиться «висіти» в GDI Heap. Так як повідомлення WM_PAINT приходить вікна досить часто, в системах Windows 9x (де розмір купи GDI обмежений величиною 64 Кб) це швидко призведе до вичерпання графічних ресурсів системи. Після цього всі програми почнуть вести себе дуже незвично: не перемальовувати деякі елементи меню та іконки, малювати текст підозрілими шрифтами і т.д. Системи на платформі NT будуть «триматися на плаву» трохи довше, так як їх графічна купа може рости в міру необхідності, але і там існує межа на число одночасно створених в процесі графічних об'єктів - 12 тисяч.
Інша проблема полягає в тому, що C ++ - програмісти звикли до автоматичному очищенню використовуваних ресурсів завдяки використанню класів з деструкторами і «розумних покажчиків». При цьому легко взагалі забути про необхідність щось явно видаляти. Однак існуючі бібліотеки класів-обгорток GDI не можуть за програміста вирішити проблему видалення обраного в контексті об'єкта. Це також призводить до витоку GDI-ресурсів.
Stateless model в GDI +
Відмінною для програміста особливістю GDI + є зміна програмної моделі в роботі з пристроями виведення. Концепція поточного обраного в пристрої графічного об'єкта викинута як неефективна і застаріла. Замість послідовної установки параметрів контексту (Graphics) використовується перерахування атрибутів при кожному виклику графічного методу. Пристрій виведення як би не володіє станом, а отримує необхідну інформацію через параметри. Така модель має назву stateless model.
Зрозуміло, така класифікація носить умовний характер. Існують і такі параметри (наприклад, поточні настройки згладжування, обрана система координат і т.д.), які зберігаються в класі Graphics і пов'язаних з ним структурах. Але стан примітивів GDI + не зберігається в контексті відображення.
Тепер кожен метод, який використовує для виведення графічний об'єкт, вимагає в якості параметра явної вказівки цього об'єкта:
Скориставшись параметром callbackData, можна, наприклад, передати в процедуру metaCallback (версії для C ++) покажчик на метафайл, записи якого перераховуються в даний момент. Тоді для виконання графічних команд можна буде скористатися методом Metafile :: PlayRecord:
Параметр recordType при кожному виклику callback-процедури приймає значення з перерахування EmfPlusRecordType. що містить аж 253 елемента (найбільшого перерахування GDI +). Всі ці елементи або прямо відповідають командам GDI / GDI +, або позначають службові записи метафайлу. При виклику PlayRecord можна підміняти цей параметр своїми значеннями для виконання абсолютно інших команд GDI + в методі PlayRecord. Але для цього необхідно твердо знати значення відповідних недокументованих параметрів recordData і dataSize (і передавати, відповідно, змінені значення і в них).
Перерахування записів: специфіка .NET
У документації по WinForms вказується, що навколишнє середовище .NET надає власну версію делегата для виконання відповідних команд метафайлу в тілі нашого callback-обробника. Ця версія передається в обробник в параметрі callbackData. Для виконання команди досить викликати отриманий делегат (приклад скопійований з документації):
Увага! Це явна помилка документації .NET Framework (ох, вже вкотре!) Наведений приклад відкомпілюйте, але його виконання ні до чого доброго не приведе. Про те ж сказано і в книзі Петцольда [3].
Експерименти показали, що в параметрі callbackData завжди передається нульове значення, незалежно від фази місяця. Виклик такого «делегата» завершиться винятком NullReferenceException.
Все-таки створити робочу версію застосування методу EnumerateMetafile для .NET цілком можливо. Для цього нам доведеться трохи повозитися з методом Metafile.PlayRecord. Справа в тому, що він як параметр data приймає масив байтів:
Так, для досягнення заповітної мети (перебору записів метафайлу в WinForms) нам довелося трохи попрацювати. Але ці труднощі з лишком окупаються іншим чудовим якістю .NET - reflection. Будь-який елемент перерахування (навіть такого гігантського, як EmfPlusRecordType), «знає» своє строкове ім'я, що дозволяє легко створити версію програми, перераховуються записи обраного метафайлу поіменно. Ось фрагмент демонстраційної програми:
Просто, чи не так? Метод ToString поверне строкове ім'я елемента перерахування, позбавляючи програміста від стомлюючого кодування 253 строкових констант. Ось результат роботи даного методу:
Повний текст програми (воно дозволяє виконувати тільки вибрані користувачем команди метафайлів) знаходиться на компакт-диску до журналу. Там же є відкомпільоване версія, яку можна використовувати для вивчення вмісту різних метафайлів (наприклад, що входять в комплект поставки Microsoft Visual Studio):
Удачі в дослідницькій роботі!