У формы имеется событие OnPaint, в обработчике которого можно что-то нарисовать на форме. Но рисовать на ней, используя свойство Canvas, можно и в любом другом методе формы. В чём разница между рисованием в обработчике OnPaint и в другом методе формы?
Варианты ответов:
Никакой разницы, OnPaint служит для локализации кода рисования в одном месте для удобства
Перед вызовом OnPaint форма специальным образом готовится, чтобы рисование выполнялось быстрее
OnPaint вызывается во время обратного хода луча развёртки монитора, чтобы исключить мерцание
Нарисованное в OnPaint не стирается после перекрытия формы другими окнами
Комментарий: В Windows изображение окна нигде само по себе не хранится. Соответственно, после того как окно было полностью или частично перекрыто другими окнами, чтобы восстановить изображение, надо заново выполнить тот код, который его нарисовал. Если этот код был не в событии OnPaint формы, система не сможет повторить его выполнение автоматически, и изображение будет потеряно. А событие OnPaint будет возникать всегда, когда форме потребуется полная или частичная перерисовка, и повреждённая часть изображения будет немедленно восстановлена. Это касается не только форм, но и других оконных и неоконных визуальных компонентов, имеющих событие OnPaint.
Если рассматривать, как это всё устроено на нижнем уровне, то система, когда замечает, что окно нуждается в перерисовке, посылает ему сообщение WM_PAINT. Компоненты VCL устроены так, что обработчик этого сообщения вызывает виртуальный метод Paint, который и выполняет рисование. У тех компонентов, которые имеют событие OnPaint, оно возбуждается в этом методе.
Сколько пикселей изменит свой цвет на чёрный в результате выполнения этого кода? Полагаем, что до его выполнения ни один из пикселей не был чёрным.
Варианты ответов:
Ни одного
3 пикселя
4 пикселя
5 пикселей
Комментарий: На первый взгляд кажется, что должно быть закрашено 4 пикселя: (20; 20), (21; 21), (22; 22) и (23; 23). На самом деле будут закрашены только 3 пикселя. Метод LineTo, так же, как и API-функция LineTo, «не дорисовывает» последний пиксель, поэтому пиксель (23; 23) не изменит свой цвет. Сделано это, видимо, для того, чтобы при вызове нескольких LineTo подряд угловые пиксели рисовались только один раз (в некоторых случаях это важно – например, при использовании растровых операций, при которых результирующий цвет пикселя зависит не только от цвета пера, но и от его начального цвета). Если необходимо, чтобы концевой пиксель включался в линию, надо либо нарисовать его вручную после вызова LineTo, либо в LineTo указать координаты линии, которая будет на один пиксель длиннее. Впрочем, последний вариант не применим для линий, идущих под произвольным углом, так как не всегда понятно, какой пиксель был бы следующим.
Вопрос №3
Следующий код рисует две прямоугольные рамки – красную и зелёную – разными способами.
Будет ли после его выполнения видна какая-то часть красной рамки, или зелёная скроет её целиком?
Варианты ответов:
Красная рамка не будет видна вообще
Красная рамка будет видна целиком
Будут видны только верхняя и нижняя стороны красной рамки
Будут видны только правая и левая стороны красной рамки
Будут видны только левая и верхняя стороны красной рамки
Будут видны только правая и нижняя стороны красной рамки
Комментарий: Когда мы рисуем рамку с помощью MoveTo/LineTo, линии идут строго по тем координатам, которые указаны. А вот Rectangle смещает правый нижний угол на один пиксель вверх и влево, т.е. в данном случае для правой стороны горизонтальная координата будет равна 199, и вертикальная координата для нижней стороны – тоже 199. Из-за этого нижняя и правая стороны зелёной рамки не закроют соответствующие стороны красной рамки, и они останутся видны.
Такой пересчёт координат прямоугольника используется в Windows везде, не только непосредственно при выводе графики. Например, если задать окну прямоугольник (0, 0, 300, 150), то для правой стороны будет X=299, для нижней – Y=149. Это связано с расчётом ширины и высоты прямоугольника. Если бы прямоугольники рисовались по «честным» координатам, то ширина рассчитывалась бы по формуле X2-X1+1. Необходимость добавления единицы здесь не сразу очевидна, но рассмотрим ситуацию на простом примере. Возьмём прямоугольник с координатами (7, 7, 8, 8). Очевидно, что он имеет размер 2x2, хотя 8-7=1. Именно поэтому приходится добавлять единицу. Но кто-то из разработчиков Windows решил, что каждый раз добавлять единицу накладно, вместо этого лучше не рисовать правый столбец и нижнюю строку пикселей у прямоугольника. Теперь высота рассчитывается по формуле Y2-Y1, ширина – X2-X1. Можно спорить о том, удобно ли это, но GDI Windows реализована именно так, и в своих программах это необходимо учитывать.
Комментарий: FrameRect рисует незаполненный прямоугольник. Но для его рисования использует не перо (Pen), а кисть (Brush). Поэтому в нашем случае получится квадрат зелёного цвета и без заполнения. Ширина рамки квадрата будет равна одному пикселю независимо от значения Canvas.Pen.Width.
Вопрос №5
Следующий код рисует окружность (толщину пера полагаем единичной) и точку
Где окажется точка относительно центра окружности?
Варианты ответов:
Попадёт точно в центр
Будет левее и выше центра
Будет левее и ниже центра
Будет правее и выше центра
Будет правее и ниже центра
Комментарий: Эллипс в GDI задаётся описанным вокруг него прямоугольником. А прямоугольники в GDI всегда уменьшаются на один пиксель влево и вверх. Соответственно, X-координата самой правой точки данной окружности будет не 20, а 19, и аналогично для самой нижней точки. Из-за этого центр окружности окажется в точке с координатами (14.5; 14.5), т.е. поставленная в данном коде точка окажется на полпикселя правее и ниже этого центра.
Вопрос №6
На форме требуется нарисовать линии геометрическим пером, поддержка которого отсутствует в классе TPen. Для этого в обработчике события OnPaint формы написан такой код
var
Brush: TLogBrush;
begin
Brush.lbStyle := BS_SOLID;
Brush.lbColor := Self.Font.Color;
Canvas.Pen.Handle :=
ExtCreatePen(PS_GEOMETRIC or PS_DASHDOT, 5, Brush, 0, nil);
// Здесь рисуем
end;
Какая ошибка есть в этом коде?
Варианты ответов:
Ошибок нет
Полю Brush.lbColor нельзя присваивать значение типа TColor
Не присвоено значение полю Brush.lbHatch
Перо со стилем PS_DASHDOT не может иметь толщину 5, толщина должна быть равна 1
Свойству Canvas.Pen.Handle нельзя присваивать дескриптор расширенного пера, расширенным пером можно рисовать только с помощью API-функций
Комментарий: Функция ExtCreatePen возвращает такой же дескриптор пера THPen, как и функция CreatePen, использующаяся для создания обычных перьев. Работа с этим дескриптором выполняется в дальнейшем так же, как и с дескриптором обычного пера, так что присваивание такого дескриптора свойству TPen.Handle не приведёт ни к каким ошибкам. Геометрические перья имеют больше возможностей, чем обычные и косметические, и при любом их стиле можно задавать любую толщину. Значения поля TLogBrush.lbHatch при lbStyle = BS_SOLID игнорируется, поэтому в данном случае его можно не присваивать. А вот использование в API-функциях типа TColor – это неправильно. Системный тип COLORREF – это подмножество типа TColor, TColor может принимать такие значения, которые не поддерживаются типом COLORREF. Это касается значений, кодирующих системные цвета: clWindow, clWindowText, clBtnFace и т.п. Если использовать такое значение там, где требуется значение типа COLORREF, результат окажется далёк от желаемого. Чтобы преобразовать значение типа TColor в тип COLORREF с учётом настроек системных цветов, используйте функцию ColorToRGB.
Вопрос №7
Имеется растровый рисунок, хранящийся в объекте B типа TBitmap. Свойство B.PixelFormat равно pf24Bit. Пусть есть следующий код
Что можно сказать о соотношении значений I1 и I2 после выполнения такого кода? Считаем, что B.Height > 20, т.е. выхода за пределы допустимого индекса при вызове ScanLine нет.
Варианты ответов:
I1 < I2
I1 > I2
I1 = I2
В общем случае это непредсказуемо
Комментарий: При PixelFormat <> pfDevice для хранения рисунка класс TBitmap использует DIB-секцию (DIB – Device Independent Bitmap). Этот формат предусматривает хранение рисунка как единого целого в непрерывной области памяти. Рисунок хранится построчно, т.е. каждая строка занимает непрерывную область памяти, но порядок следования строк не совсем обычен: первой идёт самая нижняя строка, а самая верхняя – последней. Свойство ScanLine возвращает указатель на начало соответствующей строки в DIB-секции. Из-за обратного порядка следования строк чем меньше номер строки, тем больше численное значение указателя, поэтому в нашем случае I1 будет больше, чем I2. Соответственно, если бы мы хотели получить указатель на самое начало DIB-секции, надо было бы использовать выражение B.ScanLine[B.Height-1].
GDI поддерживает DIB-секции и с прямым порядком хранения строк. Для этого высота рисунка должна быть отрицательной. Но класс TBitmap не поддерживает такой формат DIB-секций. Присвоить свойству Height отрицательное значение можно, но оно будет превращено в положительное.
Вопрос №8
Условия – те же, что и в предыдущем вопросе, но теперь B.PixelFormat имеет значение pfDevice. Что теперь можно сказать о соотношении значений I1 и I2?
Варианты ответов:
I1 < I2
I1 > I2
I1 = I2
В общем случае это непредсказуемо
Использование ScanLine с таким рисунком приведёт к исключению
Комментарий: При PixelFormat = pfDevice для хранения рисунка используется формат DDB (Device Dependent Bitmap). Этот формат жёстко связан с устройством (для TBitmap – с дисплеем), по сути дела это буфер, который драйвер устройства заполняет так, как ему удобнее, никакой единой для всех случаев структуры этого буфера не существует. Поэтому работать с этим буфером напрямую нельзя.
В справке Delphi написано, что свойство ScanLine нельзя использовать для рисунков в формате DDB. Однако это не так. В этом случае обращение к свойству ScanLine приводит к созданию копии рисунка в DIB-секции, и указатель возвращается на строку этой копии. Такое создание происходит при каждом обращении к свойству, поэтому в нашем коде P1 и P2 будут ссылаться на строки из разных копий. Место для очередной копии выделяет менеджер памяти, и предсказать, где окажется эта копия по отношению к предыдущей, в общем случае невозможно. Поэтому и соотношение величин I1 и I2 будет непредсказуемым.
Каждый следующий вызов ScanLine для DDB-растра не только создаёт новую DIB-копию, но и уничтожает ту копию, которая была создана при предыдущем вызове ScanLine для этого рисунка. Поэтому в нашем коде после второго вызова ScanLine указатель P1 становится «битым».
// Изменяем рисунок через ScanLine
P := B.ScanLine[0];
P^.rgbtRed := 255;
P^.rgbtGreen := 0;
P^.rgbtBlue := 0;
// Выводим рисунок на форму
Canvas.Draw(10, 10, B);
Какое изображение будет выведено на форму?
Варианты ответов:
Белый квадрат
Белый квадрат с красной точкой в левом верхнем углу
Белый квадрат с красной точкой в левом нижнем углу
При выполнении этого кода возникнет исключение
Комментарий: Здесь следует обратить внимание на то, что B.PixelFormat имеет значение pfDevice. Как уже было сказано в комментарии к предыдущему вопросу, для таких рисунков обращение к свойству ScanLine приводит к созданию DIB-копии рисунка, и возвращаемый указатель – это указатель на строку в этой копии. Соответственно, все изменения, сделанные через него, затрагивают только эту копию, но не оригинальный рисунок, поэтому на форму будет выведен белый квадрат без всяких красных точек.
А вот если в этом коде pfDevice заменить на pf24Bit, то в квадрате действительно появится красный пиксель в левом верхнем углу.
Вопрос №10
Растровый рисунок хранится в объекте типа TBitmap в виде DIB-секции (т.е. PixelFormat <> pfDevice). Какой способ попиксельной модификации рисунка будет работать быстрее – через свойство TBitmap.Canvas.Pixels или через свойство TBitmap.ScanLine?
Варианты ответов:
Быстрее через TBitmap.Canvas.Pixels
Быстрее через TBitmap.ScanLine
Одинаково
На разных компьютерах может быть по-разному
Комментарий: ScanLine – это очень быстрый способ модификации рисунка. Через это свойство осуществляется прямой доступ к памяти, в которой хранится изображение, поэтому работа через этой свойство выполняется быстро. А вот TCanvas.Pixels очень медленное свойство. Оно основано на API-функциях SetPixel и GetPixel. Во-первых, эти функции универсальны, т.е. могут использоваться с любым контекстом устройства, а не только с DIB-секцияим. Это означает, что они задействуют драйвер устройства, и, следовательно, при их вызове тратится время на переключение в режим ядра и обратно. Во-вторых, на уровне драйвера в Windows не предусмотрена поддержка вывода отдельного пикселя. GDI выкручивается из этого, создавая для вывода пикселя временный растр размером 1x1 пиксель, который копируется на требуемый контекст устройства. Такой подход также существенно снижает производительность этих функций.
Вопрос №11
Пусть необходимо создать объект TBitmap, хранящий рисунок размером 100x100 и имеющий формат пикселей pf8Bit. Для этого предложено два варианта кода.
Какой из этих двух вариантов будет работать быстрее?
Варианты ответов:
Первый вариант быстрее
Второй вариант быстрее
Одинаково
На разных компьютерах может быть по-разному
Комментарий: Когда мы меняем значение свойства PixelFormat, TBitmap создаёт новый рисунок в новом формате и копирует в него старый рисунок, который затем уничтожается. Разумеется, этот процесс занимает тем больше времени, чем больше рисунок. А сам рисунок не создаётся до тех пор, пока свойства Width и Height не получат значение, отличное от нуля. В первом варианте кода свойство PixelFormat меняется до того, как рисунок создан, поэтому время на преобразование вообще не тратится. Во втором случае на момент изменения этого свойства рисунок уже имеет ненулевые размеры, поэтому преобразование выполняется, что приводит к снижению производительности. Поэтому первый вариант кода работает быстрее.
Вопрос №12
Следующий код выводит надпись «fluff». Так как при выводе установлен зелёный фон, надпись получится на фоне зелёного прямоугольника. Затем по размерам текста рисуется красная рамка.
Где будет находится красная рамка по отношению к зелёному прямоугольнику?
Варианты ответов:
Рамка будет очерчивать границы прямоугольника
Рамка окажется больше прямоугольника и будет содержать его целиком
Рамка окажется меньше прямоугольника и будет содержаться целиком в нём
Области рамки и прямоугольника будут перекрываться лишь частично
Комментарий: Для каждого глифа (изображения символа) в шрифте предусмотрено определённое прямоугольное знакоместо. Однако запрета на выход символа за пределы этого знакоместа нет – отдельные элементы глифа могут выходить за него справа или слева, обеспечивая примитивный кернинг (при вычислении позиции символа в строке учитывается только размер его знакоместа, поэтому символы с выступающими элементами будут заходить в соседнее, «чужое» знакоместо). Строчная буква «f» наклонного варианта гарнитуры Times New Roman – яркий пример символа, который выступает за пределы своего знакоместа и справа, и слева.
При выводе текста выступающие элементы глифов учитываются, и фоновый прямоугольник включает в себя всю надпись. А вот при расчёте размеров строки с помощью TextExtent (а также TextWidth и TextHeight – они все работают через одну API-функцию GetTextExtentPoint32) учитываются только размеры знакоместа, а выступающие части глифов игнорируются. Если строка начинается с символа, глиф которого выступает за пределы знакоместа слева, левая граница надписи и обрамляющего его прямоугольника также сместится влево. Аналогично правая граница сместится вправо, если глиф последнего символа выходит за пределы знакоместа справа. Но рамка, размеры которой вычисляются с помощью TextExtent, эти смещения не учтёт, поэтому в данном случае размеры зелёного прямоугольника окажутся больше, чем красной рамки, и он будет включать её в себя.
Заметьте, что функции вывода текста при выравнивании текста по левой и верхней границам помещают в заданные координаты левую верхнюю точку знакоместа первого символа. Так как в нашем случае глиф первого символа выступает за пределы этого знакоместа влево, часть и самой надписи, и зелёного прямоугольника будет лежать левее линии Y = 100.
Если нужно учесть размеры выступающих элементов глифов, их можно узнать с помощью API-функций GetCharABCWidths и GetCharABCWidthsFloat.
Если вы заметили орфографическую ошибку на этой странице, просто выделите ошибку мышью и нажмите Ctrl+Enter. Функция может не работать в некоторых версиях броузеров.