Сэр Серж aka Sir Serge (Сергей Лебедев) - official site
Статьи и заметкиРасчетыСтихиПрозаО сайте

Внимание!

Статья очень старая, и имеет чисто историческую ценность. Многие вещи давно делаются не так, как в ней описано!

Использование строк UnicodeString и маркированных кодовой страницей AnsiString/RawByteString в приложениях Lazarus/LCL

Для начала определим функцию, которой мы будем смотреть внутреннее представление строки.

На всякий случай.

Type AAB=array [0..65535] of byte;
     PAAB=^AAB;

function PrintByte(p:PAAB; len:integer):string;
Var i:integer;
begin
  Result:='';
  for i:=0 to len-1 do begin
     Result+=Format('%2.2x ',[p^[i]]);
  end;
  Result:=Trim(Result);
end; 

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

Теперь определимся с нашей "тестовой платформой".

Для тестирования используем исключительно MS Windows, разрядность значения не имеет.

Базовая ОС - Windows 7.

Компилятор и Lazarus - из комплекта сборки CodeTyphon 3.10

(Lazarus 1.1.0 Source from SVN 09-12-2012 Rev 39490

FreePascal 2.7.1 Source from SVN 28-11-2012 Rev 23076)

Для более ранних по версии компиляторов (2.6.х, 2.4.х) всё сказанное - недействительно!

Windows - исключительно из за того, что у нее базовая кодовая страница не UTF8, на линуксе многие ошибки легко можно просмотреть, кроме того, принципиально ошибочный код может выдавать корректные результаты - из-за совпадения кодовой страницы операционной системы и кодировки Lazarus'а.

Программное обеспечение стенда - стандартная форма, на которую брошен компонент Memo1:TMemo и несколько кнопок, по которым вызываются тестовые процедуры.
Перед каждым вызовом чистим поле вывода вызовом Memo1.Clear;

Обращаю особое внимание: в lazarus не используются ни прямое определение кодовой страницы исходника директивой {$codepage}, ни задание этой кодовой страницы аргументом компилятора -cutf8. Это очень важно! На данной особенности построена сама возможность работать с библиотеками LCL и позволяет использовать текстовые строки, определённые в тексте программы. Стоит директиву определить, как появляются на первый взгляд чудные результаты. На самом деле, результаты - вполне предсказуемые, но об этом - ближе к концу.

Отдельным юнитом пристыковываем то, что будет играть у нас роль юникодной библиотеки, к которой мы делаем обращение:

unit MyPPunit1;

{$mode objfpc}{$H+}
{$codepage UTF8}

interface

uses
  Classes, SysUtils;


function RUnicodeString:UnicodeString;
function RRawByteStringFromUC:RawByteString;
function RRawByteStringFromS:RawByteString;
function RUTF8Str:UTF8String;
function SumUString(a,b:UnicodeString):UnicodeString;

implementation

function RUnicodeString:UnicodeString;
begin
  Result:='АБВГД';
end;

function RRawByteStringFromUC:RawByteString;
begin
  Result:=RUnicodeString;
end;

function RRawByteStringFromS:RawByteString;
begin
  Result:='АБВГД в RawByte непосредственно';
end;

function RUTF8Str:UTF8String;
begin
  Result:='АБВГД в UTF8';
end;

function SumUString(a,b:UnicodeString):UnicodeString;
begin
  Result:=a+b;
end;

end.

Здесь кодовая страница исходника определена соответствующей директивой; Кодировка исходника, естественно UTF8.

Итак, тестовый этап 1.

Работаем на стандартном модуле, содержащем форму lazarus. То есть, без директивы {$codepage}

Небольшое лирическое отступление.

В статье под названием "Символы и строки в Unicode-версиях FreePascal" несколько неправильно сказано, что этой директивой запускается механизм логической трансляции строк, что не совсем верно. Запускается то он запускается, если абстрагироваться от того, что на самом деле этот механизм включен всегда, ибо жестко вкомпилирован в логику работы компилятора. На самом деле - если директивой {$codepage ...} определена какая-либо кодовая страница, то компилятор начинает интерпретировать любой текст, подходящий по определению под строки как нечто, что необходимо преобразовать в UCS2, и в дальнейшем конвертировать согласно предопределенному строковому типу переменных, которым делаются присвоения [компилятором].

В некоторых источниках сказано, что правило действует только на строки, содержащие символы за пределами таблицы ASCII, но нет: любые строки, полученные компилятором таким образом, маркируются как принадлежащие указанной кодовой странице. Соответственно, логично предположить, что они тоже переводятся в UCS2 и компилятор внутри себя работает только с UCS2 и с немаркированными байтовыми последовательностями - строками, заданными в файлах с неопределенной кодировкой.

То есть - запомните - включаете директиву {$codepage} - все заданные в файле строки изначально приобретают метку указанной кодовой страницы. Нет этой директивы - строки воспринимаются AsIs, не контролируются по содержанию, категория кодовой страницы во внутреннем представлении у них установлена в "0".

Вспомним правила присвоения:

  • Строки не подлежат конверсии, если у строки - источника и строки - назначения - одинаковые метки кодовых страниц.
  • Строки не подлежат конверсии, если и источник и приемник - это RawByteString с кодовыми страницами CP_NONE. (Впрочем, второе правило буквально повторяет первое, не так ли?) И - сюрприз! - этот самый CP_NONE почему-то не ноль, а 65535!

    Если исследовать дополнительно, то выясняется, что не конвертируются и строки, у которых кодовая страница установлена в 0.

    То есть, чтобы нам правильно отобразить результаты, полученные от функций модуля, работающего с UnicodeStrings, мы должны:

  • получить результат либо в переменную типа UnicodeString, либо в переменную типа RawByteString.
  • Присвоить/конвертировать полученный результат так, чтобы это оказалась строка в реальной кодировке UTF8, но не содержащая маркёра этой кодовой страницы. Это можно сделать либо манипуляциями со сменой кодовой страницы функциями, приложимыми к RawByteString, либо использованием функций lazarus из модуля lazutf8.

    Итак,

    Var su:UnicodeString
    

    Простейший (и наиболее легитимный) метод применения результата:

      su:=RUnicodeString;
      Memo1.Append('su CodePage:'+IntToStr(StringCodePage(su)));
      Memo1.Append(UTF16toUTF8(su));
      Memo1.Append('su:'+PrintByte(@su[1],8));
    

    будет выведено:

    su CodePage:1200
    АБВГД
    su:10 04 11 04 12 04 13 04
    

    Для конверсии применяем функцию lcl UTF16toUTF8. Несмотря на название, к реальному UTF16 она никакого отношения не имеет, а предназначена для конвертирования в UTF8 строк типа UnicodeString. С помощью последней строки последовательности убеждаемся, что в переменной su ни что иное, как закодированная в UCS2 наша строка.

    Метод, построенный на применении встроенных функций RTL FreePascal:

    Var rb0: RawByteString;
    
      rb0:=RUnicodeString;
      Memo1.Append('rb0 перед конверсией:'+PrintByte(@rb0[1],8));      
      Memo1.Append('rb0 CodePage:'+IntToStr(StringCodePage(rb0)));
      SetCodePage(rb0,CP_UTF8,TRUE);
      Memo1.Append('rb0 после конверсии:'+PrintByte(@rb0[1],8));
      SetCodePage(rb0,0,FALSE);
      Memo1.Append(rb0);                
    

    будет выведено:

    rb0 перед конверсией:C0 C1 C2 C3 C4 00 31 31
    rb0 CodePage:1251
    rb0 после конверсии:D0 90 D0 91 D0 92 D0 93
    АБВГД
    

    Плохо. Мы получили необходимый результат, но за счёт преобразования в системную кодировку по умолчанию. Если какой-нибудь символ окажется за пределами системной кодовой страницы, он будет потерян или искажен. Предыдущий метод теоретически лучше.

    Заменим при первом присвоении RawByteString на UTF8String:

    Var u8: UTF8String;
        rb0: RawByteString;
    
      u8:=RUnicodeString;
      rb0:=u8;
      Memo1.Append('rb0 перед конверсией:'+PrintByte(@rb0[1],8));
      Memo1.Append('rb0 CodePage:'+IntToStr(StringCodePage(rb0)));
      SetCodePage(rb0,0,FALSE);
      Memo1.Append(rb0);             
    

    будет выведено:

    rb0 перед конверсией:D0 90 D0 91 D0 92 D0 93
    rb0 CodePage:65001
    АБВГД
    

    В принципе, необходимое достигнуто. Путем введения промежуточной переменной, возникающей из-за того, что для UTF8String невозможно заменить кодовую страницу, а при прямом присвоении rb0 возникает конверсия в байтовую кодовую страницу.

    Соблазн применить SetCodePage к rb0 _перед_ присвоением велик; но это действие не даст ничего - кодовая страница всё равно будет переписана на системную по умолчанию, что бы там не было возвращено из функции.

    Обратите внимание на установку в 0 кодовой страницы переменной перед выводом. Если этого не сделать, получится несовпадение кодовых страниц результата и назначения, и rtl переделает rb0 в кодировку по умолчанию - 1251 - без вашего ведома. На linux с локалью utf8 вы при этом и не заметите, что ошиблись, потому что конверсии не будет.

    Что интересно, если сотворить rb0:=UTF8String(RUnicodeString); то ожидаемого эффекта тоже не будет. Опять в rb0 после присвоения - строка в кодировке 1251. Вот такие неожиданности.

    Закрепим полученное. Только теперь из внешнего модуля возвращается не UnicodeString, а UTF8String.

    Var rb1:RawByteString;
    
      rb1:=RUTF8Str;
      Memo1.Append('rb1 CodePage:'+IntToStr(StringCodePage(rb1)));
      SetCodePage(rb1,0,FALSE);
      Memo1.Append('rb1:'+PrintByte(@rb1[1],8));   
    

    будет выведено:

    
    rb1 CodePage:65001
    rb1:D0 90 D0 91 D0 92 D0 93
    АБВГД в UTF8
    

    При получении каких либо иных маркированных строк, кроме UnicodeString, технология остаётся той же: присваиваем переменной типа RawByteString - при этом наследуется и кодовая страница и содержание - далее преобразуем содержимое к UTF8, обнуляем кодовую страницу и применяем к функциям LCL или присваиваем переменным, с которыми будет производиться дальнейшая работа в рамках LCL.

    Для обнуления кодовой страницы можно воспользоваться присвоениями переменным типа ShortString. Следующая последовательность операций пока вполне жизнеспособна, что будет в будущем - неясно:

      Var ss:string[200];
    

    Или

     
      Var ss:ShortString;
      
      ss:=RUTF8Str;
      Memo1.Append('ss CodePage:'+IntToStr(StringCodePage(rb1)));
      Memo1.Append('ss:'+PrintByte(@rb1[1],8));
      Memo1.Append(ss);
    

    Для передачи параметров типа UnicodeString придется применить функцию UTF8toUTF16 из lazutf8.

    Для передачи параметров, которые должны быть строками AnsiString с маркированной кодовой страницей, как то не просматривается иного выхода, чем использование промежуточных RawByteString с приведением к нужной кодовой странице, что будет довольно таки бесперспективным занятием, учитывая тенденции развития прототипа, под который методично подгоняется по поведению связка freepascal/lazarus - в дальнейшем может оказаться, что такой код окажется непортабельным для более новых версий LCL.

    Тестовым этапом №2 подразумевалось рассказать о том, что будет, если в начало модуля, содержащего классы и формы lazarus, влепить директиву {$codepage}. Давайте примем за должное - этой директивы в модулях, содержащих управление интерфейстными элементами - быть не должно, иначе вероятно появление слишком большого количества ошибок. Хотя, на самом деле все не так уж и страшно - надо просто запомнить, что если вы определили кодовую страницу для файла, вы включили маркированные строки, которые есть строки UCS2. Для того, чтобы в этом случае добавить к Memo1 какую нибудь строку, придётся писать, почти как в самом первом примере этой статьи:

       Memo1.Append(UTF16toUTF8('Добавляемая строка'));
    

    и даже так:

      Var s:string;
      ...
      s:=UTF16ToUTF8('Строка для инициализации переменной');
    

    То есть, уже ВСЕ строки станут "чужими" для взаимодействия с библиотекой, и придется очень внимательно отслеживать, чтобы маркированные строки не попали в аргументы функций и операторов LCL, придется бороться с дурной привычкой определять строковые сегменты, содержащие непосредственно коды UTF8 - теперь это должно быть закодировано в UCS2.

    Самое главное, что вы получите код, который рано или поздно может перестанет работать с выпуском новых версий Lazarus. Поэтому настоятельно рекомендую то, что работает с информацией в UnicodeString - выносить в отдельные модули и полностью отделять от кода, непосредственно взаимодействующего с LCL, или изолировать хотя бы логически.


    Вы можете добавить свои комментарии.

    Поскольку у нас тут абсолютная демократия, то комментарий появится на сайте только после того, как он будет одобрен администрацией. Оперативности, однако, не обещаем.

    Прошу соблюдать относительную корректность в высказываниях. Заявления типа "Пошел на...", посты, написанные в олбанской лексике и психоанализ личности автора и участников обсуждения в свет не выйдут. Также будут блокированы сообщения, не имеющие никакого отношения к заявленной тематике. Если вы не согласны с приведенным текстом - выскажите своё мнение, но обосновывайте его. Помните, что свою позицию доказываете Вы не мне, а другим читателям. Всячески приветствуются возможные технические поправки и исправления неточностей. Для возможности внесения комментариев в браузере должна быть включена поддержка JavaScript. Реклама и ссылки на сайты, не относящиеся к делу, являются прямым основанием блокировки. Поля "E-mail" и "WWW" обязательными для заполнения не являются, поле E-Mail не публикуется. Если хотите просто что-то написать автору статьи, без публикации на сайте - воспользуйтесь специальной формой под пунктом меню "О сайте". Администрация оставляет за собой право публиковать или не публиковать адреса, введенные в поле www, а также при необходимости редактировать текст вашего сообщения. Ответы на ваши сообщения по введенному вами E-mail автоматически сайтом не высылаются. Да, теги PHPBB и HTML не действуют, так что не старайтесь их вводить.

  • Copyright © 2003-2017 by Sir Serge