В прошлой статье я наотрез отказался рассматривать два последних параметра в функции ExtCreatePen, отвечающих за пользовательский стиль пера. Хотя упоминал про ГОСТы и прочую формальность. Давайте теперь рассмотрим их.
Что такое пользовательский стиль пера
Стиль пера — это последовательность отрезков заданной длины и интервалов между ними. Тире, точка, тире-точка-точка. Есть возможность выбрать предопределённые стили. Но также есть возможность задать и свою последовательность отрезков. Вот такая своя последовательность и называется пользовательским стилем.
Где это может понадобиться
Если заглянем в ГОСТ 2.303-68, то можем увидеть, что параметры разнообразных штрих-пунктирных линий весьма формализованы. Люди, которые будут использовать наш софт, могут хотеть придерживаться этих формальностей. Поэтому, желательно, чтобы наши пунктиры позволяли соответствовать стандартам.
Например:
4. Штриховая | |
5. Штрихпунктирная тонкая | |
9. Штрихпунктирная с двумя точками тонкая |
Не обязательно строго придерживаться стандартов. Надо дать возможность пользователю их придерживаться. Стандарты могут меняться, а мы народ ленивый, чтобы подпрыгивать на каждый чих. Поэтому давайте назовём это заботой о пользователе. И дадим пользователю самому формировать виды линий, которые его устроят. И которые устроят комиссию, принимающую эти чертежи.
Помимо всего вышесказанного, есть ещё и просто задумка программиста, креативный подход, томный художественный взгляд. В статье будут рассмотрены примеры эффекта бегущей линий и эффект «бегущих муравьев». Если первый эффект — это попса, то второй просто незаменим при выделении областей на картинке, чертеже и т.д.
Пользовательский стиль пера в GDI
Как уже говорилось ранее, за пользовательский стиль отвечают два параметра в функции ExtCreatePen.
StyleCount | Длина массива настроек стиля | |
Style | Массив пользовательского стиля |
Берём исходник из предыдущей статьи и немного модифицируем.
Во-первых, теперь будем создавать как геометрическое, так и косметическое перо. Пользовательский стиль можно создать для обоих типов.
Во-вторых, учтём, что для косметического пера стиль применим только для толщины, равной единице. Другие ошибочные ситуации обрабатывать не будем, но ниже описаны ситуации, когда перо не будет создано.
В-третьих, по прежнему доступна заливка битмапом. Правда, только для геометрического пера. Она тут не особо нужна, оставил просто как доступную опцию.
Пользовательский стиль содержится в массиве AUserStyle: TArray<Cardinal> (ребят, не могу я уже в Delphi 7 писать, нет её). Давайте воспринимать его просто, как некий массив. В два последних параметры функции передаётся длина массива и сам массив.
Если бы массив стиля был объявлен, как:
1 2 |
const DashDotArray: array[0..3] of Cardinal = (2,4,8,4); |
То вызов функции выглядел бы так:
1 2 3 |
// Создать перо Result := ExtCreatePen(s, AWidth, b, Length(DashDotArray), @DashDotArray); |
В массиве содержатся длины штрихов и пробелов. Например, если посмотреть на DashDotArray чуть выше, то в нём содержится следующая информация: Штрих-2px, Пробел-4px, Штрих-8px, Пробел-4px.
Если размер массива чётный, то тут всё понятно, последовательность штрихов и пробелов неизменна. Что будет, если размер нечётный? Тогда при следующем заходе на начало стиля, GDI будет воспринимать его, как размер не штриха, а пробела.
Чтобы лучше понимать, как себя поведёт нечётный массив, просто допишите мысленно тот же массив в конец. Например: (2 4 8) = (2 4 8 2 4 8). Теперь массив чётный и поведение становится предсказуемым. Что, собственно, рисунок 1 и демонстрирует.
И да! Всё это работает только для стиля PS_USERSTYLE!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
type // Соединение линий TxLineJoin = ( xljRound // Закругленное соединение ,xljBevel // Соединение с отсечением ,xljMiter // Заостренное соединение ); // Наконечники линий TxLineCap = ( xlcRound // Скругленный ,xlcSquare // Квадратный ,xlcFlat // Плоский ); // Создать перо, либо косметическое, либо геометрическое function CreateExtPen( AGeometric: Boolean;// Создавать ли геометричесоке перо AStyle: TPenStyle; // Стиль пера AWidth: DWORD; // Толщина пера AColor: Longint; // Цвет пера ALineJoin: TxLineJoin = xljRound; // Тип соединения линий ALineCap: TxLineCap = xlcRound; // Тип наконечника линий ABitmap: TBitmap = nil; // Растр для заливки AUserStyle: TArray<Cardinal> = nil ): HPEN; // Дескриптор пера const // константы типов пера CNS_STYLES: array[Boolean] of DWORD = (PS_COSMETIC, PS_GEOMETRIC); // константы типов соединения линий CNS_JOINS: array[TxLineJoin] of DWORD = (PS_JOIN_ROUND, PS_JOIN_BEVEL, PS_JOIN_MITER); // константы типов наконечников линий CNS_CAPS: array[TxLineCap] of DWORD = (PS_ENDCAP_ROUND, PS_ENDCAP_SQUARE, PS_ENDCAP_FLAT); var b: TLogBrush; s: UINT; begin // Если перо косметическое, установим толщину в 1 if not AGeometric then AWidth := 1; if ABitmap = nil then begin // Устанавливаем заливку цветом b.lbStyle := BS_SOLID; b.lbColor := AColor; end else begin // Устанавливаем заливку растром b.lbStyle := BS_PATTERN; b.lbHatch := ABitmap.Handle; end; // Формирование стиля s := CNS_STYLES[AGeometric] OR DWORD(AStyle) OR CNS_JOINS[ALineJoin] OR CNS_CAPS[ALineCap]; // Создать перо Result := ExtCreatePen(s, AWidth, b, Length(AUserStyle), AUserStyle); end; |
Когда создать перо не получится
Для косметического пера
Если при любом стандартном стиле, даже сплошном, попытаться установить толщину пера, не равную единице, нас ждёт фиаско.
Если в массиве стиля какой-нибудь из элементов равен 0, косметическое перо воспримет это как оскорбление. Для геометрического пера указывать ноль можно где угодно, и мы этим воспользуемся чуть ниже.
Для всех перьев
Если параметры функции ExtCreatePen StyleCount и Style не равны 0 и nil, то при попытке создать перо со стилем, отличным от PS_USERSTYLE, не создаст ничего.
И наоборот, если создаёте со стилем PS_USERSTYLE, но в StyleCount и Style ничего нет — то пиши пропало.
Документация по ExtCreatePen прямо говорит, что размер массива стиля не может быть больше 16. Если попытаетесь передать бОльший размер, перо не будет создано. Тут часто делают ошибку, в надежде, что если DWORD, то можно всё. Не, максимум 16, не больше.
[in] cStyle
Длина массива lpStyle в единицах DWORD. Это значение должно быть равным нулю, если dwPenStyle не PS_USERSTYLE.
Число стилей ограничено 16.
Совет
Для пользовательского, да и любого другого стиля, отличного от сплошного, лучше использовать стиль наконечника PS_ENDCAP_FLAT. Если толщина линии будет больше единицы, а наконечник — полукруг, либо квадрат, то чем больше ширина линии, тем ближе будут располагаться концы отрезков друг к другу. В итоге они просто сольются в одну линию. Хотя, на этом тоже можно построить интересный визуальный эффект.
Однако, если мы хотим строгих размеров для штрихов, то наконечник — только PS_ENDCAP_FLAT.
Что касается соединения линий, то тут уж по обстоятельствам. Я предпочитаю PS_JOIN_MITER.
Использование
Изначально планировал показать, как, используя пользовательский стиль пера, можно соблюсти стандарты, миллиметры и расстояния. Но эта затея оказалась аж на целую статью. Да не особо и интересно, если честно. Бросил, допишу потом, если будет интерес у публики.
Намного интересней что-то из области «живых» картинок. Штрих-точка, это здорово, конечно. Но если воспринимать пользовательский стиль не как статичную линию из ГОСТа, а как элемент анимации?
Первая часть будет рассказывать про принцип использования. В реальности, для таких штук надо использовать не GDI (что угодно, только не GDI). А вот почему не надо использовать GDI, и что надо использовать, расскажет вторая часть.
Для анимации и живости в приложении появляется таймер, который увеличивает счётчик FTimerTick на единицу в каждом своём срабатывании.
Бегущая линия
Идея состоит в том, что мы рисуем один и тот же массив координат, но всякий раз с разным стилем, где первый элемент, отвечающий за штрих — всегда ноль, второй — уменьшающийся пробел, третий — бесконечно длинный штрих.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
// Создать траекторию спирали function CreateSpiralPoints(const ARect: TRect; APoints: TList<TPoint>; ASpiralCount: Integer = 10 ): Integer; const DR = PI/180; var ctr: TPoint; i: Integer; p: TPoint; d, w, v, Q: Double; begin // Чистим список APoints.Clear; // Спираль будет начинаться от центра ctr := ARect.CenterPoint; // Сразу же этот центр и добавляем APoints.Add(ctr); // Максимальный радиус для спирали Q := Min(ARect.Width, ARect.Height)/2; // Приращение на каждый градус d := Q / (360 * ASpiralCount); // Пока не знаю какая длина Result := 0; // Радиус спирали равен 0 w := 0; // Пока радиус не достиг максимального радиуса while w < Q do begin // Делаем оборот на 360 градусов for i := 1 to 360 do begin // Считаем угол в радианах v := i * DR; // Находим координаты p.X := Round(ctr.X + cos(v)*w); p.Y := Round(ctr.Y - sin(v)*w); // Добавляем точку в траекторию APoints.Add(P); // Радиус увеличиваем на приращение w := w + d; end; // Тупо суммируем как длины окружностей Result := Result + Round(2*PI*w); end; end; |
Формируем пользовательский стиль таким образом:
1 2 3 4 5 6 7 8 9 |
if SpiralMode.Checked then begin // Величина пробела уменьшается v := Round(FSpiralLength-FTimerTick*10); if v < 0 then v := 0; // 0-нет начального штриха, v - уменьшающийся пробел, // 1000000 - очень длинный штрих Result := [0, v, 1000000]; end; |
Рисуем точно также (ну почти), как и в предыдущей статье:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
// Список точек пуст, больше здесь делать нечего if FPoints.Count=0 then exit; //*** СОЗДАЁМ ПЕРО ************************* NewPen := CreateExtPen( Geometric.Checked, PenStyle, // Стиль SpinEdit1.Value, // Ширина ColorToRGB(clr), // Цвет TxLineJoin(n), // Соединение TxLineCap(m), // Окончания BitmapCache, // Растр UserStyle); // Пользовательский стиль // Устанавливаем новое перо в контекст // В OldPen будет находиться дескриптор предыдущего пера // Того самого, которое clBtnShadow с толщиной 1 OldPen := SelectObject(Handle, NewPen); //****************************************** // Рисуем список точек новым пером try // Рисуем трассу целиком одной ломаной if ToArray.Checked then if PtEqual(FPoints[0], FPoints.Last) then Polygon(FPoints.ToArray) else Polyline(FPoints.ToArray) else // Рисуем ломаную отрезками линий begin MoveTo(FPoints[0].X, FPoints[0].Y); for i := 1 to FPoints.Count - 1 do LineTo(FPoints[i].X, FPoints[i].Y); end; finally //*** ОСВОБОЖДАЕМ ПЕРО ******************* // Возвращаем старое перо в контекст // Если специально не инициализировать перо в канвасе // перед отрисовкой рамки, и не выполнить эту строку, // будет чёрт знает что SelectObject(Handle, OldPen); // Просим удалить геометрическое перо за ненадобностью DeleteObject(NewPen); //**************************************** end; |
То есть мы рисуем один и тот же массив. Анимация осуществляется исключительно за счёт пользовательского стиля.
«Однако, я же могу ту же саму спираль нарисовать просто выводя не весь массив, а просто смещать по таймеру указатель на начало в рисуемом массиве» — скажет искушённый программист. И будет прав. Но я показываю модель применения пользовательского стиля, который служит не только для скучных штрих-пунктирных линий.
И потом, как быть с бегущим контуром текста? Неужели заворачиваться, брать массив из пути, считать длину, считать указатель? Когда можно всё сделать быстро, просто и изящно.
Бегущий контур текста
Использую паттерн для заливки. С кэшем битмапа работа абсолютна такая же, как в предыдущей статье (голимый копипаст, зря что ли публикую). Сама отрисовка (под спойлером):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
procedure TFmMain.pbPaint(Sender: TObject); var bmp, tmp: TBitmap; rct: TRect; str: string; br: TLogBrush; Style: array[0..1] of Cardinal; NewPen, OldPen: HPEN; begin str := pb.Hint; rct := pb.ClientRect; bmp := TBitmap.Create; bmp.SetSize(rct.Width, rct.Height); try tmp := BitmapCache; with bmp.Canvas do begin rct := ClipRect; Brush.Color := clWhite; FillRect(rct); Font.Assign(pb.Font); // Заливаем либо цветом, либо растром if tmp = nil then begin // Устанавливаем заливку цветом br.lbStyle := BS_SOLID; br.lbColor := ColorToRGB(Font.Color); end else begin // Устанавливаем заливку растром br.lbStyle := BS_PATTERN; br.lbHatch := tmp.Handle; end; // Простой стиль из двух элементов Style[0] := FTimerTick; // Меняющий длину штрих Style[1] := 1000000; // Бесконечной длинный пробел // Создаём геометрическое перо NewPen := ExtCreatePen( PS_GEOMETRIC OR PS_USERSTYLE OR PS_ENDCAP_FLAT OR PS_JOIN_BEVEL, SpinEdit1.Value, br, Length(Style), @Style); OldPen := SelectObject(Handle, NewPen); try // Если не сделать прозрачным, вокруг текста будет рамка Brush.Style := bsClear; // Путь старует BeginPath(Handle); try // Рисуем текст TextOut( (rct.Width - TextWidth(str)) div 2, (rct.Height - TextHeight(str)) div 2, str); finally // Путь завершается EndPath(Handle); end; // Рисуем путь пером StrokePath(Handle); finally SelectObject(Handle, OldPen); DeleteObject(NewPen); end; Pen.Color := clBtnShadow; Rectangle(rct); end; finally pb.Canvas.Draw(0,0,bmp); bmp.Free; end; end; |
В данном случае используем стиль из двух элементов. Первый — это равномерно увеличивающийся (или уменьшающийся) штрих, второй — бесконечный пробел.
Понятно, что на этом принципе можно построить конфетку. Например, затухание при исчезновении. Можно сделать иллюзию пока не проявившихся «новых» нарощенных линий. Эдакий серый след, который приобретает цветность с каждой новой отрисовкой.
Но пока только принцип.
Бегущие муравьи
Незаменимая штука, когда надо выделить какой-то участок изображения. PhotoShop, GIMP, Paint.net и куча других аналогичных продуктов имеют в арсенале этот инструмент. В стандартном API конечно этой прелести нет. Поэтому давайте его реализуем.
Понятно, что мы будем рисовать по-прежнему какой-то статичный массив, но с разным стилем. Предлагается 4 вида таких стилей, которые будут циклично менять друг друга. У нас есть переменная v, которая последовательно принимает значения от 0 до 3.
1 2 3 4 5 6 |
case v of 0: Result := [4,4,0,0]; 1: Result := [2,4,2,0]; 2: Result := [0,4,4,0]; else Result := [0,2,4,2]; end; |
Мы создаём анимацию с постепенно исчезающим штрихом и появляющимся с другой стороны массива.
Здесь мы рисуем всё ту же спираль, но бегающими насекомыми.
Листинг отрисовки точно такой же.
Как выглядят муравьи при выделении участка изображения:
Вот как их такими сделать, расскажу во второй части. Подписывайтесь на телегу, все новости там.
Продолжение. Пользовательский стиль пера. Часть II: Бегущие муравьи
Скачать
Друзья, спасибо за внимание!
Бегущие линии и бегущие муравьи
Исходник (zip) 282 Кб. Delphi XE 7, XE 10, XE 11
Исполняемый файл (zip) 1.07 Мб.
Бегущий контур текста
Исходник (zip) 79 Кб. Delphi XE 7, XE 10, XE 11
Исполняемый файл (zip) 940 Кб.
Друзья! Буду чрезвычайно признателен за комментарии, регистрацию на сайте, подписку в телеге. Это очень большая моральная поддержка для меня.