На страницах этой публикации мы попытаемся создать текстовой редактор с закладками (многостраничный текстовой редактор). Вы наверняка уже видели такие редакторы, и вот теперь настало время создать свой собственный многостраничный текстовой редактор. В тексте этой статьи ничего не будет очень страшного и сложного, по прочтении всей статьи вы сможете самостоятельно создать полноценный многостраничный текстовой редактор. В качестве языка программирования будем использовать всеми знакомый и любимый язык Pascal (Delphi) и будем разрабатывать наш редактор в старой и доброй среде разработки Borland Delphi 7. Прочитав эту статью, вы научитесь создавать динамические компоненты и присваивать обработку событий им (ведь само по себе создание нового компонента на готовой форме – это довольно простое дело).
Почему я взялся за написание это статьи? Когда я читал книгу Тома Свана «Основы программирования в Delphi для Windows 95», то мне очень не понравилась следующая фраза: «Приложение TabEdit является многостраничным текстовым редактором, аналогичным редактору модулей Delphi. Исходный код TabEdit слишком большой, чтобы привести его здесь полностью, поэтому в книгу включены отдельные, сравнительно небольшие, фрагменты» [Сван Т. Основы программирования в Delphi для Windows 95. С. 272]. Я пытался понять принцип создания такого редактора на этих «небольших фрагментах», но у меня ничего не выходило, хотя уже на тот период я имел некоторый опыт программирования в среде Delphi. Попыток было много, но все они были практически безрезультатными. Но вот четвертого мая 2008 года я решил попытаться во чтобы то ни стало создать многостраничный текстовой редактор – и у меня получилось. Насколько удачно – это уже вам судить, дорогие читатели, но я все-таки старался.
Если быть более точным (и честным), то следует сказать, что это вторая версия такого редактора, так как при разработке первой версии редактора возникли некоторые трудности, и код редактора пришлось переписать практически с начала.
Я был поражен, почему Том Сван посчитал, что программный код многостраничного редактора большой – вовсе нет. У меня этот код уместился на нескольких страницах формата А4. Зато сколько удовольствия доставляет многостраничный текстовой редактор – просто не передать!
Итак, эта статья может пригодиться начинающим и более-менее опытным программистам Delphi. На страницах этой статьи мы создадим многостраничный текстовой редактор, используя компонент TRichEdit. Если вы только начинаете изучать программирование в среде Delphi, то многому научитесь прочитав эту статью: создавать динамические компоненты (то есть новые компоненты в процессе работы программы), присваивать этим компонентам свойства и события, научитесь создавать меню и научитесь работать с многими компонентами среды Delphi: TRichEdit, TStatusBar, TMainMenu и другими компонентами.
Запускаем среду разработки Delphi (я включаю седьмую версию, а вы ту, которая присутствует у вас на компьютере) и создаем новое приложение. Сразу сохраняем пустой проект, присваивая форме имя fMain.pas, а проекту MultiPageEditor.dpr. Теперь необходимо задать некоторые свойство нашей форме, на которой в дальнейшем мы поместим различные компоненты и добавим ей много различных функций и процедур.
Итак, задайте следующие свойства форме fMain.pas:
Свойство | Значение |
Caption | MultiPageEditor |
Name | Main |
Position | poScreenCenter |
Здесь мы задали заголовок главному (и единственному окну) – свойство Caption, имя по которому мы будем обращаться к нашей единственной форме (Name) и задали свойство, которое будет размещать при выполнении (запуске) нашего приложения данное окно по центру экрана (свойство Position). Теперь можно сохранить эти изменения на форме. Теперь помещаем компонент TMainMenu и создаем следующее меню, представленное в виде таблицы:
Caption | Name | ShortCut |
Файл | mnuFile | |
Новый | mnuFile_New | Ctrl+N |
Открыть… | mnuFile_Open | Ctrl+O |
- | mnuFile_Line0 | |
Сохранить | mnuFile_Save | Ctrl+S |
Сохранить как… | mnuFile_SaveAs | F12 |
Сохранить все | mnuFile_SaveAll | Shift+Ctrl+S |
Закрыть | mnuFile_Close | |
Закрыть все… | mnuFile_CloseAll | |
- | mnuFile_Line1 | |
Выход | mnuFile_Exit |
Поместите в блок private переменную PageCounter : Integer; а имеющийся блок var формы замените следующим блоком.
var
MyPath : string;
Main : TMain;
const
EDITORPREFIX = 'Editor';
Теперь немного поясним то, что мы только сделали. Переменная PageCounter будет отвечать за количество открытых (или созданных) страниц-закладок. Переменная MyPath необходимо для первоначальной загрузки диалогов сохранения. Константа EDITORPREFIX будет отвечать за то имя редактора (свойство Name), которое будем присваивать очередному созданному редактору.
Создайте следующий обработчик события.
procedure TMain.FormCreate(Sender: TObject);
begin
MyPath := ExtractFilePath(Application.ExeName);
PageCounter := 0;
end; // procedure TMain.FormCreate
С помощью функции ExtractFilePath мы определяем только адрес файла (без имени файла) в качестве файла в данном случае мы передаем имя приложения. При создании формы необходимо установить счетчик количества редакторов (страниц-закладок) в ноль. Теперь помещаем на форму компоненты TPageControl и TStatusBar, расположенные на закладке Win32. Следует разместить полосу состояния (компонент TStatusBar) таким образом, чтобы она была расположена не на TPageControl, а на форме.
Выделяем полосу состояния и создаем на ней три панели. Первой панели необходимо задать свойство Alignment как taCenter (чтобы текст данной панели располагался по центру). Теперь для первых двух панелей полосы состояния задайте ширину равную 100 (свойство Width).
Расположите TPageControl по всей области формы (свойство Align – alClient).
Теперь создайте следующую процедуру:
// Процедура позволяет создать новую закладку с именем PageCaption,
// которая будет находиться на PageControl. На закладке будет расположен
// редактор RichEdit (TRichEdit)
procedure NewPageEditor(PageControl : TPageControl;
const PageCaption : string; const RichEditName : string);
var
TabSheet : TTabSheet;
RichEdit : TRichEdit;
begin
// Создаем закладку TabSheet (TTabSheet)
// и задаем ей необходимые параметры
TabSheet := TTabSheet.Create(PageControl);
TabSheet.PageControl := PageControl;
TabSheet.Caption := PageCaption;
// Делаем созданную закладку активной
PageControl.ActivePageIndex := PageControl.PageCount - 1;
// Создаем редактор RichEdit (TRichEdit)
RichEdit := TRichEdit.Create(TabSheet);
RichEdit.Parent := TabSheet;
RichEdit.Align := alClient;
RichEdit.Name := RichEditName;
RichEdit.Hint := '';
RichEdit.Clear;
end; // procedure NewPageEditor
Теперь необходимо создать следующую процедуру, благодаря которой будет изменяться содержимое панелей строки состояния StatusBar1, расположенной на форме нашего редактора.
// С помощью процедуры DoPanels можно изменять содержимое панелей
// компонента Main.StatusBar1
procedure DoPanels(RichEdit : TRichEdit);
begin
with Main.StatusBar1 do
begin
Panels[0].Text := IntToStr(RichEdit.CaretPos.Y + 1) + ' : ' +
IntToStr(RichEdit.CaretPos.X);
if ( RichEdit.Modified = True ) then
Panels[1].Text := 'Изменен'
else
Panels[1].Text := '';
Panels[2].Text := RichEdit.Hint;
end; // with
end; // procedure DoPanels
На первой панели строки состояния будет отображена текущая позиция каретки редактора RichEdit, вторая панель (Panels[1]) будет содержать текст «Изменен» только в том случае, если свойство Modified редактора RichEdit истинно (True), в противном случае панель не будет содержать текста (пустая строка). Третья панель будет содержать адрес файла, содержащегося в данном редакторе в свойстве Hint. Я конечно же, понимаю, что свойство Hint предназначено несколько для другого, но я ничего лучшего не придумал, чем поместить адрес файла в свойство Hint, так как и адрес файла и свойство Hint оперирует со строчным типом string.
// Данная процедура позволяет создать новую закладку с редактором
// и загрузить файл FileName в этот редактор.
procedure OpenPageEditor(PageControl : TPageControl;
const FileName : string; const RichEditName : string);
var
I, Max : Integer;
Component : TComponent;
begin
NewPageEditor(PageControl, ExtractFileName(FileName), RichEditName);
// Может быть существует более эффективный способ загрузки файла в редактор,
// но тогда придется полностью переместить процедуру NewPageEditor в этот код
// (то есть продублировать код и в этой процедуре). Но так как активная закладка
// содержит только компонент типа TRichEdit, то цикл должен обрабатываться довольно
// быстро, поэтому и используется данный код, как наиболее эффективный код из
// наденных автором
Max := PageControl.ActivePage.ComponentCount - 1;
for I := 0 to Max do
begin
Component := PageControl.ActivePage.Components[I];
if ( Component.ClassType = TRichEdit ) then
with ( Component as TRichEdit ) do
begin
Lines.LoadFromFile(FileName);
Modified := False;
Hint := FileName;
Break;
end; // if
end; // for
end; // procedure OpenPageEditor
Может быть существует более быстрый и эффективный способ поиска компонентов на текущей странице, чем перебор всех компонентов с помощью цикла, но я такой простой способ не нашел, поэтому ограничился таким «примитивным», но довольно простым и удобным кодом. Итак, переменной Max передается общее количество компонентов на текущей закладке (так как на закладке мы создаем только редактор типа TRichEdit), то число Max будет равно нулю, но так как возможно вы захотите в дальнейшем совершенствовать редактор, я не стал писать более простой (и может быть более эффективный код). В противном случае данный код можно было бы значительно сократить и упростить, избавившись от цикла. Но в нашем случае цикл производит всего лишь одну итерацию от нуля до нуля (нуль – значение переменной Max).
На данный момент мы имеем три процедуры, благодаря которым мы можем создавать новую закладку с редактором типа TRichEdit, а также процедуру для «разметки» панелей строки состояния нашего окна, а также процедуру, позволяющую открывать файлы на новых страницах.
Но на это время, мы создали только часть нашего редактора, нам необходимо создать еще множество других процедур. Теперь перейдем к процедуре сохранения.
// Процедура позволяет сохранить содержимое редактора активной страницы.
// Эта процедура вызывается командой File|Save и если Hint редактора
// не содержит текста, то вызывается операция File|Save As...,
// в противном случае происходит сохранение содержимого текущего редактора
procedure SaveNowPage(PageControl : TPageControl);
var
I, Max : Integer;
Component : TComponent;
begin
Max := PageControl.ActivePage.ComponentCount - 1;
for I := 0 to Max do
begin
Component := PageControl.ActivePage.Components[I];
if ( Component.ClassType = TRichEdit ) then
begin
if ( (Component as TRichEdit).Modified = True ) then
begin
if ( (Component as TRichEdit).Hint = '' ) then
Main.mnuFile_SaveAs.Click
else
begin
(Component as TRichEdit).Lines.SaveToFile((Component as TRichEdit).Hint);
(Component as TRichEdit).Modified := False;
DoPanels(TRichEdit(Component));
Break;
end; // if..else
end; // if
end; // if
end; // for
end; // procedure SaveNowPage
Итак, эта процедура позволяет сохранить содержимое текущей закладки (редактора). Здесь используется перебор всех компонентов текущей закладки (о целесообразности использования цикла мы говорили выше). Затем определяется класс каждого компонента, расположенного на текущей закладке компонента PageControl, и если данный компонент является компонентом типа TRichEdit, то определяется его свойство Modified. При истинном значении этого свойства определяется свойство Hint. Если это свойство равно пустой строке, то это означает, что данный редактор имеет некое содержимое, но оно еще не было сохранено, в этом случае будет выполняться обращение к событию команды меню «Сохранить как…» (Main.mnuFile_SaveAs.Click). В противном случае сохраняем содержимое редактора по адресу, указанному в свойстве Hint. Задаем свойство Modified данного редактора как «ложь» (False) и вызываем процедуру DoPanels, после чего выходим из цикла и на этом заканчивается данная процедура.
// Процедура позволяет сохранить содержимое редактора активной страницы
// PageControl под новым именем или по другому адресу
procedure SaveAsNowPage(PageControl : TPageControl; const FileName : string);
var
I, Max : Integer;
Component : TComponent;
begin
Max := PageControl.ActivePage.ComponentCount - 1;
for I := 0 to Max do
begin
Component := PageControl.ActivePage.Components[I];
if ( Component.ClassType = TRichEdit ) then
begin
(Component as TRichEdit).Lines.SaveToFile(FileName);
(Component as TRichEdit).Modified := False;
(Component as TRichEdit).Hint := FileName;
PageControl.ActivePage.Caption := ExtractFileName(FileName);
DoPanels(TRichEdit(Component));
Break;
end; // if
end; // for
end; // procedure SaveAsNowPage
Здесь используется уже известный вам цикл в теле которого происходит сохранение содержимого данного компонента по адресу FileName, затем задается свойство Modified как False, Hint – FileName, а текущая закладка получает заголовок имени файла с помощью функции ExtractFileName. Так как теперь свойство Modified наверняка было изменено, то необходимо вызвать процедуру DoPanels, чтобы отобразить это на панелях.
Только что мы создали две процедуры, которые позволяют сохранять содержимое текущего редактора по адресу, а также процедуру, которая позволяет сохранить содержимое данного редактора по другому адресу или под новым именем.
// Позволяет сохранить все открытые закладки-редакторы
procedure SaveAllEditor(PageControl : TPageControl);
var
I, Max, Actived : Integer;
begin
// Переменная Actived получает индекс активной (текущей) страницы,
// после обработки цикла активная страница будет также Actived,
// то есть пользователь практически ничего не заметит
Actived := PageControl.ActivePageIndex;
Max := PageControl.PageCount - 1;
for I := 0 to Max do
begin
PageControl.ActivePageIndex := I;
SaveNowPage(PageControl);
end; // for
PageControl.ActivePageIndex := Actived;
end; // procedure SaveAllEditor
С помощью процедуры SaveAllEditor можно сохранить все закладки-редакторы, если это, конечно же требуется. С помощью первой и последней строки кода, пользователь ничего не увидит при этом, так как цикл переключается между страницами компонента PageControl.
Этим пока можно ограничиться. Если сейчас запустить приложение, то ничего оно делать не будет, хотя и содержит довольно большой объем исходного кода, но на данный момент мы не подключили созданные процедуры с событиями меню – этим мы займемся в следующей части статьи.
Теперь необходимо создать следующие компоненты на форму со странички Dialogs палитры компонентов Delphi: TOpenDialog и TSaveDialog. Выделите диалоговое окно сохранения TSaveDialog и задайте следующие свойства этому компоненту:
Свойство | Значение |
DefaultExt | rtf |
Filter | Текстовые документы (*.rtf)|*.rtf|Текстовые документы (*.txt)|*.txt|Все файлы (*.*)|*.* |
Name | dSave |
Options --> ofOverwritePrompt | True |
Мы задали свойство ofOverwritePrompt = True, что позволяет выводить сообщение предупреждающее о сохранении. Например, мы ввели имя в диалоге, а такой файл уже есть, так благодаря этому параметру будет выводиться сообщение и только после утверждающего ответа будет произведено сохранение. Очень удобное свойство – не позволяет сохранить на месте нужного файла например пустой документ.
Теперь выделяем компонент TOpenDialog и задаем ему следующие свойства:
Свойство | Значение |
DefaultExt | rtf |
Filter | Текстовые документы (*.rtf; *.txt)|*.rtf; *.txt|Все файлы (*.*)|*.* |
Name | dOpen |
Для этого диалога мы не стали изменять свойства Options, здесь нет необходимости этого делать, ведь это диалог открытия файлов, а не сохранения. Открыли не тот файл и закрыли его – ведь у нас многостраничный текстовой редактор. Теперь вернемся к процедуре TMain.FormCreate и добавим в нее следующие строки кода.
dOpen.InitialDir := MyPath;
dSave.InitialDir := MyPath;
Теперь при первом сохранении и первом открытии файла в программе, будут отображена папка, в которой находится исполняемый файл нашей программы.
procedure TMain.mnuFile_NewClick(Sender: TObject);
begin
Inc(PageCounter);
NewPageEditor(PageControl1, 'Untitled' + IntToStr(PageCounter), EDITORPREFIX + IntToStr(PageCounter));
end; // procedure TMain.mnuFile_NewClick
procedure TMain.mnuFile_OpenClick(Sender: TObject);
begin
with Main.dOpen do
if Execute then
OpenPageEditor(PageControl1, FileName, EDITORPREFIX + IntToStr(PageCounter));
end; // procedure TMain.mnuFile_OpenClick
procedure TMain.mnuFile_SaveClick(Sender: TObject);
begin
SaveNowPage(PageControl1);
end; // procedure TMain.mnuFile_SaveClick
procedure TMain.mnuFile_SaveAsClick(Sender: TObject);
begin
with Main.dSave do
if Execute then
SaveAsNowPage(PageControl1, FileName);
end; // procedure TMain.mnuFile_SaveAsClick
Сейчас с помощью этого кода мы значительно добавили функциональности нашему многостраничному текстовому редактору. Мы добавили событие позволяющее создавать новую закладку-редактор, открывать и сохранять файлы нашего приложения.
Но у нас впереди все еще много работы, поэтому не будем останавливаться на достигнутом и пойдем далее.
// Данная процедура позволяет закрыть текущую закладку
procedure CloseNowPage(PageControl : TPageControl);
var
I, Max : Integer;
Component : TComponent;
begin
Max := PageControl.ActivePage.ComponentCount - 1;
for I := 0 to Max do
begin
Component := PageControl.ActivePage.Components[I];
if ( Component.ClassType = TRichEdit ) then
begin
if ( TRichEdit(Component).Modified = True ) then
case ( MessageDlg('Файл ' + TRichEdit(Component).Hint +
' изменен. Сохранить изменения?',
mtConfirmation, [mbYes, mbNo, mbCancel], 0) ) of
mrYes : SaveNowPage(PageControl);
mrNo : ;
mrCancel : Exit;
end; // case
PageControl.Pages[PageControl.ActivePageIndex].Destroy;
Break;
end; // if
end; // for
end; // procedure CloseNowPage
// Данная процедура позволяет закрыть все закладки-редакторы
procedure CloseAllEditor(PageControl : TPageControl);
var
I, Max : Integer;
begin
Max := PageControl.PageCount - 1;
for I := Max downto 0 do
try
PageControl.ActivePageIndex := I;
CloseNowPage(PageControl);
except
end; // for
end; // procedure CloseAllEditor
Процедура CloseNowPage позволяет закрыть текущую закладку-редактора, но при этом она будет выводить соответствующее сообщение, но только в том случае, если содержимое данного редактора было изменено.
Процедура CloseAllEditor позволяет закрыть все закладки PageControl, при каждой очередной итерации цикла обращаясь к процедуре CloseNowPage. Здесь используется обратный цикл от Max до 0 – это сделано специально, так как в противном случае невозможно закрыть некоторые страницы, а в таком виде это можно сделать для любого числа открытых закладок-редакторов.
Теперь необходимо создать соответствующие обработчики событий для пунктов меню.
procedure TMain.mnuFile_SaveAllClick(Sender: TObject);
begin
SaveAllEditor(PageControl1);
end; // procedure TMain.mnuFile_SaveAllClick
procedure TMain.mnuFile_CloseClick(Sender: TObject);
begin
CloseNowPage(PageControl1);
end; // procedure TMain.mnuFile_CloseClick
procedure TMain.mnuFile_CloseAllClick(Sender: TObject);
begin
CloseAllEditor(PageControl1);
end; // procedure TMain.mnuFile_CloseAllClick
procedure TMain.mnuFile_ExitClick(Sender: TObject);
begin
Close;
end; // procedure TMain.mnuFile_ExitClick
Мы добавили еще четыре обработчика событий – это очень хорошо с каждой строкой кода, наш многостраничный текстовой редактор все улучшается и улучшается, все обрастает и обрастает новыми функциями и возможностями. Но до сих пор наш редактор не имеет очень удобной и нужной возможности, это возможности сохранения файлов при закрытии приложения. Можете попробовать сейчас, но на нужных файлах все-таки не тренируйтесь.
Но и это мы сейчас исправим, но для этого нам необходимо всего лишь добавить следующий обработчик события для формы.
procedure TMain.FormCloseQuery(Sender: TObject; var CanClose: Boolean);
var
I, PageCount, J, ComponentCount : Integer;
Component : TComponent;
begin
PageCount := PageControl1.PageCount - 1;
for I := PageCount downto 0 do
try
PageControl1.ActivePageIndex := I;
ComponentCount := PageControl1.ActivePage.ComponentCount - 1;
for J := 0 to ComponentCount do
begin
Component := PageControl1.ActivePage.Components[I];
if ( Component.ClassType = TRichEdit ) then
begin
if ( TRichEdit(Component).Modified = True ) then
case ( MessageDlg('Файл ' + TRichEdit(Component).Hint +
' изменен. Сохранить изменения?',
mtConfirmation, [mbYes, mbNo, mbCancel], 0) ) of
mrYes : SaveNowPage(PageControl1);
mrNo : ;
mrCancel : CanClose := False;
end; // case
end; // if
end; // for
except
end; // try..except
end; // procedure TMain.FormCloseQuery
Итак, теперь при закрытии нашего редактора будет происходить просмотр всех открытых страниц-редакторов и при необходимости выдавать сохранение о том, что такой-то файл изменен и требует сохранения. Этот фрагмент кода получается путем слияния процедур CloseNowPage и CloseAllEditor воедино. Здесь у вас может возникнуть вопрос: почему нельзя использовать процедуру, как для события закрытия всех файлов? А вот нельзя, так как в этом случае нам придется модифицировать процедуры CloseNowPage и CloseAllEditor, а так мы избавимся от этого.
Вот сейчас мы имеем вполне работоспособный текстовой редактор, позволяющий размещать каждый отдельный документ на новой закладке. Удобно, но все еще наш редактор требует определенной доработки, так как при изменении содержимого редактора не происходит моментального отображения этого на панелях, а ведь это необходимо. Поэтому самое время этим и заняться.
procedure TMain.MyEditorSelectionChange(Sender: TObject);
begin
DoPanels(Sender as TRichEdit);
end; // procedure TMain.MyEditorSelectionChange
А в блок private формы следует ввести следующий код.
procedure MyEditorSelectionChange(Sender: TObject);
Теперь в этом блоке, кроме объявления переменной PageCounter имеется и объявление процедуры. После этого необходимо немного модифицировать процедуру NewPageEditor, а точнее добавить в нее всего одну строку кода.
RichEdit.OnSelectionChange := Main.MyEditorSelectionChange;
С помощью этой строки кода, строка состояния будет адекватна состоянию редактора, а с помощью следующего обработчика события адекватное состояние будет и при переходе между отдельными закладками нашего редактора.
procedure TMain.PageControl1Change(Sender: TObject);
var
I, Max : Integer;
Component : TComponent;
begin
Max := PageControl1.ActivePage.ComponentCount - 1;
for I := 0 to Max do
begin
Component := PageControl1.ActivePage.Components[I];
if ( Component.ClassType = TRichEdit ) then
begin
DoPanels(TRichEdit(Component));
Break;
end; // if
end; // for
end; // procedure TMain.PageControl1Change
Вот и все. Наш многостраничный текстовой редактор готов. В конце статьи я хотел бы сказать следующее: если вы захотите модифицировать данный код или использовать его в своих собственных разработках, то автор очень будет вам признателен, если вы напишите соответствующую благодарность либо лично автору, либо в окне About вашей программы строку о том, что используется код из статьи «Разработка многостраничного текстового редактора» со ссылкой на данный сайт.
Здесь я не стал создавать события для меню Правка (вам должно быть и так понятно, как это сделать на основании данной статьи). Главное – это то, что мы смогли сделать многостраничный текстовой редактор и это у нас заняло 349 строк кода (если верить многостраничному редактору среды Delphi!). Вот так мы опровергли Тома Свана!