Сайт Ивана Чередниченко | Разработка многостраничного текстового редактора

Разработка многостраничного текстового редактора

На страницах этой публикации мы попытаемся создать текстовой редактор с закладками (многостраничный текстовой редактор). Вы наверняка уже видели такие редакторы, и вот теперь настало время создать свой собственный многостраничный текстовой редактор. В тексте этой статьи ничего не будет очень страшного и сложного, по прочтении всей статьи вы сможете самостоятельно создать полноценный многостраничный текстовой редактор. В качестве языка программирования будем использовать всеми знакомый и любимый язык 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!). Вот так мы опровергли Тома Свана!