Typer - унивесальная система для печати символов

предварительное замечание:
В системе SWE большинство примитивных оперций сделано собственными средствами, без использования высокоуровневых библиотек, обычно не прибегая к VCL и даже иногда не используя функции API.
Налицо типичное изобретение велосипедов, т.е. повторение уже кем-то найденных решений. Причина - примернае равенство затрат труда на написание новых функций и на поиск существующих.
Преимущества подхода с изобретением велосипедов:
  1. Свобода, развязанные руки для любого решения
  2. Достижение любой задуманной функциональности
Недостатки подхода:
  1. Возможность совершения ошибок, повторения чужих ошибок
  2. Нарушение принципа бритвы Оккама - "не создавай новых сущностей"

Если вы, читатель, нашли в программе решения, которые можно без потерь выполнить стандартными средствами - напишите мне, я заменю участок кода.
Обычное решение для реализации форматированного вывода на экран - это использование готового компонента для RTF-редактирования.
Следующая к фундаменту ступенька - использование VCL функций типа TCanvas.TextOut.
Последний, казалось бы, шаг - использование API функций для вывода строк на поверхность окна, которые заметно быстрее VCL-функций и дают полный контроль над выводом.

Однако и API функции вывода строк имеют ограничения. Именно, в одной строке нельзя выводить символы разным шрифтом. Поэтому в проекте SWE сделана попытка создать свою надстройку над API, универсальную и дающую абсолютный контроль над выводом текста.

Первоначальная идея была в том, чтобы выводить текст посимвольно. Одна и та же процедура и рисовала символ и определяла его геометрию (Rect). По времени такая процедура работала удовлетворительно и на моноширинных шрифтах себя оправдывала:
function TTyper.OneCharRectCode(Ch:char;QShow:boolean;ahitty:TPoint;DC:HDC):TRect;
var S   : string;
    sz  : TagSIZE;
begin
  FillChar(sz,SizeOf(sz),#0);
  S := Ch;
  Windows.GetTextExtentPoint32(DC, PChar(S), 1, sz);
  if QShow then Windows.TextOut(DC,ahitty.X,ahitty.Y,PChar(S),1);
  Result := Rect(ahitty.X,ahitty.Y,ahitty.X+sz.CX,ahitty.Y+sz.CY);
end;
Где DC - это DeviceContext объекта Canvas, на котором рисуется текст, его можно получить, сделав присваивание DC := Form.Canvas.Handle;

ahitty - точка начала рисования (левый нижний угол)
result - прямоугольник, обрамляющий отрисованный текст
QShow - переключатель который позволяет либо рисовать текст, либо только получать его размеры и положение, т.е. заполнять result без рисования
GetTextExtentPoint32 и TextOut - это WinAPI функции

Объект типа TTyper необходим, чтобы выводить символ в разных кодировках. Описанная выше OneCharRectCode годится, когда входной символ Ch соответствует кодировке текущего фонта. Это соответствие нетрудно обеспечить для Win и DOS - кодировок. Для KOI кодировки текст надо выводить преобразуя его в UniCODE. Для такого случая служила процедура
function TTyper.OneCharRectUNI (Ch:char;QShow:boolean;ahitty:TPoint;DC:HDC):TRect;
в которой GetTextExtentPoint32 и TextOut заменены на GetTextExtentPoint32W и TextOutW

В итоге, описание TTyper выглядит следующим образом:
type

  TOneCharRectFunc = function(Ch:char;QShow:boolean;ahitty:TPoint;DC:HDC):TRect of object;

  TTyper = class(TObject)
    OneCharRect: TOneCharRectFunc;
function OneCharRectUNI (Ch:char;QShow:boolean;ahitty:TPoint;DC:HDC):TRect;
function OneCharRectCode(Ch:char;QShow:boolean;ahitty:TPoint;DC:HDC):TRect;
function OneCharRectBMP (Ch:char;QShow:boolean;ahitty:TPoint;DC:HDC):TRect;
  end;
Печать каждого символа в редакторе SWE или определение геометрии для размещения курсора выглядит как вызов Typer.OneRectChar, где OneRectChar содержит адрес одной из трёх реальных процедур. Адрес подставляется в зависимости от текущей кодировки текста.
Третий вариант, OneCharRectBMP, это пока заготовка функции, где реальный TrueType фонт может быть заменён на массив BMP-картинок с изображением каждой буквы.
Зачем этот третий вариант нужен?
Представьте книгу на старославянском языке, которую желательно перевести в электронный вид. Сканирование шрифта, создание массива BMP-картинок и затем распознавание всего текста помогут создать образ этой старославянской книги. Такая книга в редакторе SWE будет представлена не в виде JPG картинок, а в компактном виде, в виде текста, по которому возможны все обычные текстовые операции, во-первых поиск, а во вторых редактирование этого текста. замечание:
функция OneCharRectBMP на сегодняшний день лишь задумка. Буду благодарен тому, кто возьмётся её реализовать.
При более внимательном взгляде на посимвольный вывод текста выявились его недостатки. Оказалось, что в немоноширинных шрифтах положение текущего символа зависит от начертания текущего и предыдущего символов. Например, в паре букв A,V второя буква приближена к первой так, что обрамляющие их прямоугольники слегка перекрываются. Это называется кернинг. Описания TrueType шрифтов содержат в себе таблицы кернинга для различных пар символов.
Таким образом ширина символа A + ширина символа V не равна ширине слова AV. Посимвольную процедуру рисования и определения геометрии пришлось модифицировать.
В объект TTyper добавлено поле
  PrevChar : char;
которое используется как глобальная переменная, запоминающая символ напечатанный при прошлом вызове функции OneCharRect.
function TTyper.OneCharRectCode(Ch:char;QShow:boolean;ahitty:TPoint;DC:HDC):TRect;
var S   : string;
    S0  : string;
    S2  : string;
    sz  : TagSIZE;
    sz0 : TagSIZE;
    sz2 : TagSIZE;
    Kerning : integer;
begin
  FillChar(sz,SizeOf(sz),#0);
  S := Ch;
  if PrevChar = ' ' then begin  (* после пробела кернинг для любого *)
    Kerrning := 0;              (*    следующего символа равен нулю *)
    Windows.GetTextExtentPoint32(DC, PChar(S) , 1, sz);
  end
  else begin
    sz0 := sz; sz2 := sz;
    S0 := PrevChar; S2 := PrevChar + Ch;
    Windows.GetTextExtentPoint32(DC, PChar(S0), 1, sz0);
    Windows.GetTextExtentPoint32(DC, PChar(S) , 1, sz);
    Windows.GetTextExtentPoint32(DC, PChar(S2), 2, sz2);
    if sz.cx = 0 then WARN('Не задан DC!');
    Kerning := sz2.cx - (sz0.cx+sz.cx);
  if QShow then begin
    Windows.TextOut(DC,ahitty.X+Kerning,ahitty.Y,PChar(S),1);
  end;
  PrevChar := Ch;
  Result := Rect(ahitty.X+Kerning,ahitty.Y,ahitty.X+Kerning+sz.CX,ahitty.Y+sz.CY);
end;
Оказалось, что и такая функция работает достаточно быстро на современных компьютерах. Потребовалось только дополнительно инициировать поле PrevChar, присваивая ему значение пробела в начале рисования каждой новой строки и при каждой смене текущего фонта.

Но и это решение оказалось неполным. Если печатать текст курсивом, т.е. наклонныым шрифтом, то видно, что правые верхние уголки некоторых символов обрезаны. Дело в том, что символы наклонных шрифтов образованы не прямоугольниками, а параллелограммами, у которых вершины смещены вправо относительно оснований. Таким образом функция TextOut выводит символ в прямоугольник, который реально шире прямоугольника, получающегося из функции GetTextExtentPoint32. Поэтому левый вертикальный край последующего символа затирает косой уголок предыдущего. Чтобы выйти из этой ситуации надо печатать прозрачным фонтом. Т.е. для текущего контекста устройства DC надо вызвать функцию:
 Windows.SetBkMode(DC, TRANSPARENT);
а перед каждым выводом строки очищать или заполнять фоновой картинкой область печати. При редактировании ввод каждого нового символа должен сопровождаться обновлением картинки всей строки.


Вернуться к теме проект редактора SWE

(с) Можаровский С.Г. // mailto:mozharovskys@mail.ru // swHome page