Программа с многоязычным интерфейсом

Автор благодарит читателя этих уроков, программиста Alexey Salo за идею, без которой написание данного материала было бы невозможным. Сразу отмечу, что урок получился не совсем для начинающих, количество написанного кода в примерах испугало даже меня, но тем не менее, попробуйте разобраться. Повсюду даны описание команд, некоторые мы рассматривали в предыдущих уроках. Возможно непонятные моменты рассмотрены более подробно.
Для начала немного теории
Ваша собственная программа может быть полезна не только вам, но и вашим друзьям, организации, где вы работаете. Если вы работаете не только на себя. Живя в нашем веке компьютерных технологий, информация может распространяться с довольно большой скоростью. Примером тому служат нашумевшие недавно волны интернет-вирусов, за считанные дни облетевшие по многим серверам мира. Так же дело и обстоит с полезными программами. Отличием полезной программы от вредоносной есть сам метод переноса от компьютера к компьютеру. По степени ее уникальности и полезности она может понравиться многим. Я не буду говорить о методах рекламы программных продуктов, они такие же самые, как и реклама обычных продуктов, будь то интернет-ресурс или обычных хозяйственный товар. Дело в том, что ваша программа, выпущенная в свободное распространение (выложенная на сайте, отправленная друзьям по почте и т.п.) абсолютно независимо от вашего желания может попасть любому человеку. Этот человек может быть другой национальности, абсолютно не понимающий русского языка.
Если ваша программа изначально рассчитана на свободное распространение, свободное распространение с ограниченными функциями для последующего приобретения, если ваша программа может оказаться полезной для многих (утилита, игра, экранная заставка), то надо стараться изначально ее оформлять с англоязычным интерфейсом. Все дело в том, что большинство пользователей компьютеров в полной мере или частично знакомы с английским языком. Следовательно, разобраться в такой вашей программе смогут больше человек в мире, чем, скажем в программе с белорусским языковым интерфейсом. Здесь имеется в виду ни что иное, как глобальное внедрение вашего приложения в масштабе целой планеты, а не касательно, скажем, ваших знакомых. Но делая программу, даже с языком, являющимся международным (даже с китайским языком :), трудно рассчитывать на популярность во многих странах.
Большую популярность получили программы с многоязычным интерфейсом. Я имею в виду, что описанные выше программы получают большую популярность, чем аналогичные с однотипным диалоговым языком. К примеру, это некоторые командные оболочки (FAR, Windows Commander), антивирус DrWeb, интернет браузер Opera. В таких программах нужный язык можно выбрать из списка в окнах настройки.
В большинстве случаев, весь языковой интерфейс хранится в отдельном файле. И это правильно. Не стоит загромождать исполняемый EXE файл излишком информации, а настройка и адаптация нового языка, первоначально не включенного в такой программный пакет, происходит быстро и без перекомпиляции всего проекта.
Какой же метод выбрать для хранения и последующего считывания данных из файла языков. В большей степени для этого подходит рассмотренные нами в 16 уроке ini файлы. Такие файлы легко переносятся на другой компьютер вместе с самой программой, возможно редактировать из любого текстового редактора, изменить текст может любой человек.
Различные секции в таком ini-файле будут хранить в себе отдельные языковые интерфейсы. Например, секция [RUSSIAN] будет озаглавливать русскоязычный внешний вид программы, [ENGLISH] - англоязычный, и т.д. Я думаю, что с этим проблем у пользователя не будет.
Названия хранимых параметров состоят из названия окна (формы), в котором находится компонент плюс название самого компонента. Параметр должен состоять из одного слова. Хранимая величина - текст, который отображается на экране на этом компоненте. Это может быть свойство Caption или свойство Text, в зависимости от типа (класс) компонента. Например, для компонента Button1, находящегося в окне Form1 записываемый параметр и значение выглядит:
Form1.Button1=Кнопка1
В нашей программе при чтении такого параметра должна произойти замена:
Form1.Button1.Caption := 'Кнопка1';
Естественно, это делается автоматически для всех визуальных компонентов, на каких есть текст. Проблема может состоять в том, что таких компонентов на каждой форме может быть, скажем 200. Тогда это очень загромоздит программный код. При оперативном исправлении такой программы (добавление, удаление компонентов), необходимо будет исправлять и эту часть кода. Выходом из создавшейся проблемы может быть свойства для определенного окна ComponentCount и Components. Свойство Components позволяет через массив получить доступ к любому элементу управления формы. Свойство ComponentCount показывает, сколько этих элементов управления (компонентов) у нас присутствует в окне. Нам нужно будет просто организовать цикл от 1 до ComponentsCount и для каждого компонента прочитать соответствующее значение Caption или Text из INI файла.
Внутри такого цикла нужно определять тип компонента. Ведь для кнопки (Button, BitBtn, SpeedButton), метки (Label, StaticText), флажка (CheckBox, RadioButton) и пр. свойство Caption определяет текст, который будет виден на этом компоненте. Для Edit, Memo, ComboBox и пр. свойство Text. Следовательно, очень важно верно определить тип, выбранного из цикла компонента, чтобы в последствии правильно занести соответствующее значение в соответствующее свойство.
Следующим этапом, когда мы определили тип компонента, следует само чтение данных из ini-файла. Вот примерный кусок кода такой программы:
// если в окне есть хотя бы один элемент управления (компонент)
if ComponentCount <> 0 then
 // цикл от 1 до кол-ва компонентов
 for i:=1 to ComponentCount do
 // если текущий элемент является элементом класса TButton, то
 if Components[i-1].ClassType = TButton then
 (Components[i-1] as TButton).Caption := ЧТЕНИЕ_ДАННЫХ_ИЗ_INI
Разъясню последнюю строчку из этого примера. Через
(Components[i-1] as TButton)
Мы получаем доступ к свойствам компонента, представляя его к классу TButton. Для этого в предпоследней строке примера мы и производим проверку класса выбранного циклом компонента. Если такую проверку не производить, то во время выполнения программы при обращении, скажем к компоненту класса TEdit к свойству Caption, появится сообщение об ошибке (У TEdit свойство Text!).
(i-1) как вы наверное уже догадались, список массива элементов управления формы начинается с нуля. А заканчивается ComponentCount-1.
Еще один нюанс. Если такой многоязычный ini-файл был потерян в процессе эксплуатации, то пользователя рискует остаться вообще без каких либо намеков в вашей программе. Поэтому рекомендую все-таки изначально давать текст вашим компонентам как в обычной программе. В случае невозможности чтения данных из INI файла текст на этих компонентах останется нетронутым.
Теперь я даю вам универсальный модуль, который должен присутствовать в каждом оконном модуле вашей программы, где необходимо менять язык по указанию пользователя. Я являюсь автором этого куска программы, поэтому, возможно, что-то можно и сократить, тем не менее...
В раздел Uses необходимо дописать модуль для работы с ini-файлами:
uses IniFiles;
Раздел public дописываем одну строку объявления процедуры:
public
 { Public declarations }
 procedure ChangeLang(LangSection: string);
Сама процедура пишется изначально вручную вместе с заголовком:
procedure TForm1.ChangeLang(LangSection: string);
var
 // временная числовая переменная для выборки всех компонентов
 i: Integer;
 LangIniFile: TIniFile;
 // строковая переменная для получения каталога, где находится запущенный EXE файл
 ProgramPath: string;
begin
 // если в окне больше одного компонента
 if ComponentCount <> 0 then
 begin
 // получаем каталог, где лежит запущенный EXE файл
 ProgramPath := ExtractFileDir(Application.ExeName);
 // гарантированно устанавливаем последний символ '\' в конце строки
 if ProgramPath[Length(ProgramPath)] <> '\' then
 ProgramPath := ProgramPath + '\';
 // подготавливаем INI файл. Он должен иметь название lang.ini
 // и должен находиться в каталоге программы
 LangIniFile := TIniFile.Create(ProgramPath + 'lang.ini');
 // читаем заголовок окна
 Caption := LangIniFile.ReadString(LangSection, name, Caption);
 // перебираем все компоненты в этом окне
 for i:=1 to ComponentCount do
 begin
 // если выбран из массива компонент Button, то изменяем текст на кнопке
 if Components[i-1].ClassType = TButton then
 (Components[i-1] as TButton).Caption := LangIniFile.ReadString(LangSection,
 name+Components[i-1].name, (Components[i-1] as TButton).Caption);
 // Напомню описание функции ReadString:
 // ====================================
 // LangIniFile.ReadString( СЕКЦИЯ, ПАРАМЕТР, ЗНАЧЕНИЕ_ПО_УМОЛЧАНИЮ );
 // 1. LangSection - передаваемый параметр в процедуру.
 // В процедуру передается название секции для выбранного языка
 // 2. Name+Components[i-1].Name - Name - название формы,
 // Components[i-1].Name - название компонента
 // 3. (Components[i-1] as TButton).Caption - в случае неудачного чтения этого
 // параметра из ini файла (нет такого параметра), то ничего меняться не будет
 // аналогично для других типов:
 if Components[i-1].ClassType = TLabel then
 (Components[i-1] as TLabel).Caption := LangIniFile.ReadString(LangSection,
 name+Components[i-1].name, (Components[i-1] as TLabel).Caption);
 if Components[i-1].ClassType = TEdit then
 (Components[i-1] as TEdit).Text := LangIniFile.ReadString(LangSection,
 name+Components[i-1].name, (Components[i-1] as TEdit).Text);
 // ...
 // ...
 // ...
 end;
 LangIniFile.Free; // освобождаем ресурс
 end;
end;
Обратите внимание, в программе два окна. В каждом модуле для каждого отдельного окна присутствует эта вышеописанная процедура.
Вместо строк // ... вы можете добавлять другие типы компонентов, например, можете описать тип компонента TCheckBox, если таковой имеет место в вашей программе. В идеале описывайте по шаблону все или большинство типов компонентов, имеющиеся в наличие в delphi. Для этого вам понадобится несколько десятков строк программного кода, но зато вы гарантированно можете применять эту процедуру не только в одной вашей программе, не проверяя наличие всех используемых типов компонентов.
Напомню, аналогичную вышеприведенную процедуру, без изменений, вы должны вписать в каждый модуль вашей программы. При смене языка, например, в программе с тремя окнами (Form1, Form2, Form3) происходит следующим куском программного кода:
Form1.ChangeLang('RUSSIAN');
Form2.ChangeLang('RUSSIAN');
Form3.ChangeLang('RUSSIAN');
Поскольку процедуру смена языка мы объявляем в разделе public, то доступ к этой процедуре мы можем получить из любого места программы. Как вы заметили из примера, для переключения на русский язык указана имя секции RUSSIAN.
Теперь рассмотрим содержание самого INI файла. Его вы должны создавать самостоятельно. Во-первых, нужно помнить правила орфографии windows ini-файла, во-вторых, названия параметров должны соответствовать имени формы плюс названия компонента, хранимое значение следует за знаком равенства. Для примера, ini-файл с двумя языками, с двумя окнами, на каждом окне находится по две кнопки.
; начало файла lang.ini
[RUSSIAN]
Form1Button1=Кнопка 1 на форме 1
Form1Button2=Кнопка 2 на форме 1
Form2Button1=Кнопка 1 на форме 2
Form2Button2=Кнопка 2 на форме 2
[ENGLISH]
Form1Button1=Button 1 on form 1
Form1Button2=Button 2 on form 1
Form2Button1=Button 1 on form 2
Form2Button2=Button 2 on form 2
; конец файла lang.ini
Кроме текста на визуальных элементах управления вашей программы, текст содержится еще в заголовке программы, в панели задач. Еще могут присутствовать всплывающие подсказки Hint. Полномасштабный перевод всех компонентов на несколько языков в ini файле может несколько затруднить сам процесс программирования. Но ради гибкости настройки интерфейса программы пожертвуйте лишним временем. Ведь для изменения текста, добавления нового языка (возможно даже это делаете не вы) не нужно перекомпилировать проект. Это максимально упрощает языковую настройку. Можете вообще сделать ini-файл одноязычным. Перевести его можете после окончания проектирования, или вообще предоставьте это профессиональному переводчику.
Одним из слабых мест в такой программе есть необходимость помещать процедуру смены языка в каждый модуль. Если у вас в программе множество окон, это довольно сильно загромождает программный код, следовательно, увеличивает размер программы, следовательно, замедляет ее работу. Как же сделать так, чтобы весь процесс смены языка во всем приложении умещался с одной процедуре. Очень просто. А может и не просто. А в общем, через компонент Application. Он является по своей сути самой программой, значит, содержит в себе все компоненты форм (уже упоминалось в первых уроках, что сама форма и есть компонент). Таким образом:
// если в приложении есть компоненты форм (не консольное приложение)
if Application.ComponentCount <> 0 then
 // перебираем все компоненты
 for i := 1 to Application.ComponentCount do
 // если выбранный компонент является подклассом окна, то
 if Application.Components[i-1].ClassParent = TForm then
 begin
 // обработка переключения языка для этого окна
 end;
Очень похоже на предыдущий пример...
Извините, что на первый взгляд эта тема урока кажется, немного сложноватой для начинающего, но тут важно разобраться в общих принципах, в нескольких свойствах, внимательно перечитать весь урок снова и все может стать понятным. Обязательно рассмотрите прилагаемый первый пример, прежде чем продолжать рассматривать этот урок далее.
Опять ищем слабые места в программе. Для начинающих программистов может быть новостью, что в одной программе не может быть двух и более окон с одинаковыми названиями. А как же дело обстоит с MDI приложениями. Дочернее окно проектируется в единственном варианте, а в внутри родительской формы, во время работы программы, оно может создаваться теоретически в неограниченном количестве. Имена же самой вновь создаваемой дочерней форме присваиваются системой автоматически. Следовательно, читать параметр из ini-файла по свойству Name не подойдет. Таким методом можно максимально прочитать язык для компонентов только для одного дочернего MDI-окна. Отсюда следует, что нужно читать данные согласно свойству ClassName, которое является уникальным для отдельного класса окна. Например, для окна Form1, являющегося главным MDI-окном такой класс TForm1. Для окна Form2, дочернего MDI-окна класс TForm2. Вот, вы наконец и узнали, что же это за такая буква Т, стоящая в начале названия компонента. Это надкласс, объединяющий однотипные компоненты (в том числе и окно программы) в единую группу, с одинаковыми вложенными свойствами. Это краткое описание, можно сказать, своими словами.
Эти все "наваяния" организованы во втором примере. Там смена языка происходит из одной процедуры абсолютно для всех окон. Тем самым мы уменьшили программный код за счет увеличения качества, увеличили количество вложенных циклов. Программа примера номер 2 является незаконченным каркасом MDI-приложения. Не удивляйтесь, почему программа не выполняет функции редактора.
Объявление. Автор уроков для начинающих по delphi ищет темы, какие вам было бы интересно узнать. Свои предложения отсылайте мне, Semen'у, по адресу mailto:semen@krovatka.net?subject=предложение, указав в теме письма слово "предложение". Ваше предложение не должно быть очень сложным для программного решения, понятным для начинающего, тема не должна отклоняться от тематики ведения уроков (например, не рассматривается управление базами данных, SQL, internet и пр.). Материал, написанный по вашему предложению, ориентировочно должен быть дан в объеме одного урока. Предложение в текущий урок должно быть отправлено до пятницы.
По вашему дополнительному пожеланию ссылка на автора идеи будет помещена в начале урока. В таком случае обязательно делайте соответствующую пометку и указывайте свое имя с обратным адресом в письме с предложением.
;Примечание от Vit: Идея правильная, а вот реализация ужасна... И ужас этот здесь:
// если в окне есть хотя бы один элемент управления (компонент)
if ComponentCount <> 0 then
 // цикл от 1 до кол-ва компонентов
 for i:=1 to ComponentCount do
 // если текущий элемент является элементом класса TButton, то
 if Components[i-1].ClassType = TButton then
 (Components[i-1] as TButton).Caption := ЧТЕНИЕ_ДАННЫХ_ИЗ_INI
Во первых, непонятно зачем надо в цикле гонять через все компоненты если форма и все контролы "контейнеры" имеют методы FindComponent. Но самое главное не это - модуль локализации должен по идее лежать отдельно, и НЕ ЗАВИСЕТЬ от кода самого приложения, другими словами - в идеале - мы пишем модуль который подгружает локализацию, а потом создаём все формы и контролы, локализация же вызывается единственной стандартной функцией на onCreate формы например. В приведенном случае так сделать нельзя потому как код во-первых привязан к конкретному модулю, но главное - нам надо ЗАРАНЕЕ знать все классы и компоненты которые будут подвергаться локализации, и не дай бог добавить какой-то новый компонент или даже использовать наследование (что профессионалы используют довольно часто) и при этом забыть поправить модуль локализации - и всё - баг на лицо, локализация не работает... Скажу сразу - это исправимо, логика работы простая: Вначале локализационный ini пишется примерно так:
 [Form1]
 Button1.Caption='My Caption'
После создания всех форм, вызываем единократно локализацию.
В модуле локализации реализуем алгоритм:
 For Найти все разделы ini
 For найти первый элемент раздела
 (
 Вызвать FindComponent соответсвующей формы
 Используя RTTI найти указанное свойство данного класса по имени
 Присвоить значение
 )
Таким образом достигается возможность присвоения значения свойств любых заранее неизвестных классов любых скалярных типов в любых заранее неизвестных формах!
Ключевое слово тут RTTI , но я не буду тут приводить примеры, во-первых RTTI - это тема отдельного разговора, во-вторых тут же в ДРКБ вы найдёте целый большой раздел по RTTI, и в третьих раздел RTTI относится к той области знаний программиста на Дельфи, который профессионал обязан знать вне зависимости делает ли он локализацию или нет.
Author: Vit
Source: http://delphiworld.narod.ru/
ID: 04559
Category:Разработка приложений
Category:Статьи