БлогNot. Visual C++: пишем простой многодокументный редактор текста RTF

Visual C++: пишем простой многодокументный редактор текста RTF

То есть, MDI-приложение. Пример вот отсюда как-то мне не очень нравится, потому что написан левой ногой и не использует стандартные возможности по управлению дочерними окнами. Этот пример тоже, в общем, пишется левой ногой и прямо сейчас, но постараемся сделать хотя бы классическую картинку "много окон документов внутри главного окна приложения".

В 2015-й (или выше) Visual Studio создадим приложение Windows Forms на C++.

На форму приложения перетащим главное меню MenuStrip и стандартные диалоги для работы с файлами OpenFileDialog, SaveFileDialog. У диалогов достаточно настроить свойство Filter на нужный нам тип файлов, с которым будем работать, выберем "Файлы RTF|*.rtf".

В меню предусмотрим несколько пунктов, которые покажут разные возможности работы с файлами, текстом в дочерних окнах и самими окнами:

простое меню родительской формы MDI-приложения
простое меню родительской формы MDI-приложения

Также не забудем установить родительской форме свойство IsMdiContainer = true.

Затем добавим к приложению ещё одну форму (меню Проект - Добавить новый элемент - Viual C++ - UI - Форма Windows Forms). У меня она назвалась MyForm1, пусть так и остаётся.

Настроим новую форму, перетащив на неё какое-нибудь многострочное текстовое поле, например, стандартный RichTextBox и установив у него в окне Свойств размер во всю область дочернего окна (свойство Dock = Fill).

В дочернее окно добавим также контекстное меню contextMenuStrip1, которое будет вызываться правой кнопкой мыши. Предусмотрим там 4 пункта (Вырезать, Копировать, Копировать все, Вставить). Это позволит приложению обмениваться кусками текста между окнами через системный Буфер Обмена. Сначала все пункты отключены (свойство Enabled = false).

Это меню укажем в свойстве ContextMenuStrip компоненты richTextBox1.

Добавим в дочернее окно его собственный SaveFileDialog, чтобы можно было выбрать имя файла для сохранения, если мы закрываем дочернее окно с изменённым текстом. На диалог поставим такой же фильтр, как у родительской формы. Вообще, конечно, лучше было бы не плодить компоненты, а воспользоваться неким отдельным классом-наследником SaveFileDialog, который мы могли написать.

форма дочернего окна
форма дочернего окна

Теперь дочернюю форму мы пропишем вверху файла родительской формы MyForm.h:

#pragma once
#include "MyForm1.h" /* прописал класс потомка в предке */

а весь остальной код как обычно писать в MyForm.h под директивой #pragma endregion :

#pragma endregion
public:
 static int winCounter; //Просто счётчик окон

 private: void createNewWindow() { //Метод для создания нового окна
  MyForm1 ^ newForm = gcnew MyForm1(); //Вверху файла есть #include "MyForm1.h"
  newForm->MdiParent = this; //Указали новой форме, кто её родитель
  newForm->Text = "Noname " + ++winCounter; //Номер нового окна - в заголовок
   //Так же при необходимости ставим любые другие свойства новой формы
  newForm->Show(); //Показываем новую форму
 }

Мы уже умеем создавать обработчики событий двойным кликом из списка событий в окне "Свойства", так что приведу только "внутренности" функций-обработчиков стандартных событий.

По выбору пункта меню "Создать" просто вызовем только что написанный метод:

createNewWindow();

Пункт "Выход" запрограммировать тоже просто, будем вызывать стандартный метод закрытия главной формы:

this->Close();

Для записи файла, имя которого было введено или выбрано в стандартном диалоге сохранения, напишем обработчик пункта меню "Сохранить...":

Form ^ activeChild = this->ActiveMdiChild;
 if (activeChild != nullptr) {
  RichTextBox ^ theBox = (RichTextBox ^)activeChild->ActiveControl;
  if (!activeChild->Text->Empty) saveFileDialog1->FileName = activeChild->Text;
  if (saveFileDialog1->ShowDialog() == System::Windows::Forms::DialogResult::OK) {
   try {
    theBox->SaveFile(saveFileDialog1->FileName);
   }
   catch (...) {
    MessageBox::Show(String::Format(L"Ошибка сохранения файла {0}", saveFileDialog1->FileName));
   }
   activeChild->Text = saveFileDialog1->FileName;
  }
 }
 else {
  MessageBox::Show(L"Не выбрано ни одно окно");
 }

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

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

createNewWindow(); //Открываем всегда в новом окне
 Form ^ activeChild = this->ActiveMdiChild;
 if (activeChild != nullptr) {
  RichTextBox ^ theBox = (RichTextBox ^)activeChild->ActiveControl;
  if (!activeChild->Text->Empty) openFileDialog1->FileName = activeChild->Text;
  if (openFileDialog1->ShowDialog() == System::Windows::Forms::DialogResult::OK) {
   if (openFileDialog1->FileName == nullptr) return;
   try {
    theBox->LoadFile(openFileDialog1->FileName);
    theBox->Modified = false;
   }
   catch (IO::FileNotFoundException^ e) {
    MessageBox::Show(e->Message + "\nНет такого файла", "Ошибка",
     MessageBoxButtons::OK, MessageBoxIcon::Exclamation);
   }
   catch (Exception^ e) { // Отчет о других ошибках
    MessageBox::Show(e->Message, "Ошибка", MessageBoxButtons::OK, MessageBoxIcon::Exclamation);
   }
   activeChild->Text = openFileDialog1->FileName;
  }
 }
 else {
  MessageBox::Show(L"Не выбрано ни одно окно");
 }

Удобство MDI-приложения в том, что при закрытии главного окна обработчки события FormClosing дочерних окон будут вызваны автоматически, так что каждому экземпляру дочернего окна достаточно будет позаботиться о сохранности только своего текста из своего RichTextBox.

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

Ниже приведён обработчик события FormClosing главной формы. Комментарием показано, как можно было бы обойти в цикле все дочерние окна.

/*
 for (int x = 0; x < this->MdiChildren->Length; x++) {
 Form ^ tempChild = (Form ^)this->MdiChildren[x]; //Так можно перебрать потомков
 }
 */
 //Для MDI-приложений метод сам вызовет FormClosing потомков
 e->Cancel = false; //Отказ в потомке не приводит к отмене закрытия родительского окна

Если последний оператор закомментировать, нажатие кнопки "Отмена" в ответ на предложение сохранить любой из несохранённых файлов будет отменять и закрытие родительского окна.

Выбор пункта меню "Дата и время" вставит в конец текста активного окна текущие дату и время, примерно как в стандартном Блокнотике Windows (только там - в место расположения курсора). Эта функция добавлена просто для иллюстрации:

Form ^ activeChild = this->ActiveMdiChild;
 if (activeChild != nullptr) {
  try {
   RichTextBox ^ theBox = (RichTextBox ^ )activeChild->ActiveControl;
   if (theBox != nullptr) {
    DateTime ^d = gcnew DateTime (DateTime::Now.Year, DateTime::Now.Month, DateTime::Now.Day,
     DateTime::Now.Hour, DateTime::Now.Minute, DateTime::Now.Second);
    theBox->Text += String::Format("\n{0}, {1}\n",d->ToLongDateString(),d->ToLongTimeString());
   }
  }
  catch (...) {
   MessageBox::Show(L"Ошибка вставки даты и времени");
  }
 }
 else {
  MessageBox::Show(L"Не выбрано ни одно окно");
 }

Наконец, пункт "Каскад" уложит каскадом имеющиеся дочерние окна, по аналогии легко сделать остальные стандартные "Укладки":

 //Уложить дочерние окна каскадом, там же - другие способы укладки
 this->LayoutMdi(MdiLayout::Cascade);

Напишем что-нибудь в файле MyForm1.h, программируя функционал дочерних окон. Весь код будет располагаться после директивы #pragma endregion

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

Метод checkForClipboardFormat будет проверять, что находится в Буфере Обмена и разрешать вставлять только RTF:

private: System::Void checkForClipboardFormat() { 
  if (Clipboard::GetDataObject()->GetDataPresent(DataFormats::Rtf))
   contextMenuStrip1->Items[3]->Enabled = true; //Включить пункт Вставить
  else contextMenuStrip1->Items[3]->Enabled = false;
}

Мы вызовем его в обрабтчике события загрузки (Load) дочерней формы:

checkForClipboardFormat();

Также напишем для richTextBox1 обработчики событий SelectionChanged (изменение выбора текста)

if (richTextBox1->SelectionLength == 0)
  for (int i = 0; i<2; i++) contextMenuStrip1->Items[i]->Enabled = false;
 else
  for (int i = 0; i<2; i++) contextMenuStrip1->Items[i]->Enabled = true; //Включить Вырезать, Копировать
 checkForClipboardFormat();

и TextChanged (изменение самого текста), имеющие схожие цели.

if (richTextBox1->Text->Length<1) {
  richTextBox1->Modified = false; //Пустой текст не будем предлагать сохранить
  for (int i = 1; i<3; i++) contextMenuStrip1->Items[2]->Enabled = false; //и копировать
 }
 else //Включить Копировать, Копировать все
  for (int i = 1; i<3; i++) contextMenuStrip1->Items[2]->Enabled = true;
 checkForClipboardFormat();

Пункт меню "Вырезать" будет программировать просто:

if (richTextBox1->SelectionLength>0) richTextBox1->Cut();

"Копировать" тоже:

if (richTextBox1->SelectionLength>0) richTextBox1->Copy();

"Копировать все" тоже:

richTextBox1->SelectAll(); richTextBox1->Copy(); richTextBox1->DeselectAll();

А вот обработчик пункта "Вставить" будет дополнительно проверять, есть ли в системном Буфере данные подходящего формата и не выделено ли что-то в тексте из richTextBox1:

if (Clipboard::GetDataObject()->GetDataPresent(DataFormats::Rtf)) { //Есть Rtf в буфере
   if (richTextBox1->SelectionLength > 0) { //И что-то выделено,
    //спросим, как вставлять - поверх выделенного или в конец?
    if (MessageBox::Show(L"Вставить поверх выделения?",L"Сообщение",
     MessageBoxButtons::YesNo)==System::Windows::Forms::DialogResult::No)
     richTextBox1->SelectionStart = richTextBox1->Text->Length;
   }
   richTextBox1->Paste();
  }

Перед закрытием формы в обработчике события FormClosing нам придётся запросить у пользователя сохранение файла, если оно требуется, и дать ему возможность сохранить изменения, отказаться от сохранения или отменить закрытие окна:

if (richTextBox1->Modified == false) return;
 saveFileDialog1->FileName = this->Text;
 auto MBox = MessageBox::Show(
   String::Format(L"Текст в файле {0} был изменен.\nСохранить изменения?",saveFileDialog1->FileName), 
   L"Простой редактор", MessageBoxButtons::YesNoCancel, MessageBoxIcon::Exclamation);
 if (MBox == System::Windows::Forms::DialogResult::No) return;
 if (MBox == System::Windows::Forms::DialogResult::Cancel) e->Cancel = true;
 if (MBox == System::Windows::Forms::DialogResult::Yes) {
  if (saveFileDialog1->ShowDialog() == System::Windows::Forms::DialogResult::OK) {
   try {
    richTextBox1->SaveFile(saveFileDialog1->FileName);
    richTextBox1->Modified = false;
   }
   catch (Exception^ e) {
    MessageBox::Show(L"Ошибка сохранения файла");
   }
   return; 
  }
  else e->Cancel = true; // Передумал закрывать 
 }

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

 Скачать архив .zip с папкой этого проекта Visual Studio 2015 (12 Кб)

В простейшем случае, добавив и настроив toolStripContainer, а на нём кнопку "B" (выделение жирным), мы можем написать примерно такой код для переключения шрифта контрола активного окна между жирным и обычным (обработка щелчка по кнопке):

private: System::Void toolStripLabel1_Click(System::Object^  sender, System::EventArgs^  e) {
 //toolStrip1 - кнопка "B" (выделить жирным)
 Form ^activeChild = this->ActiveMdiChild;
 if (activeChild) {
  RichTextBox ^box = (RichTextBox ^)activeChild->ActiveControl;
  if (box) {
   System::Drawing::Font ^f = gcnew System::Drawing::Font
    (box->SelectionFont, FontStyle::Bold ^ box->SelectionFont->Style);
     //переключить шрифт контрола между жирным и обычным
   box->SelectionFont = f; //установить новый шрифт
   box->Select(); //применить к выделению
   toolStripLabel1->ForeColor = //переключить цвет кнопки - изначально был Black
   ( box->SelectionFont->Bold == true ? Color::Red : Color::Black ); 
  }
 }
}

 Аналогичный MDI-редактор RTF на C# Windows Forms, статья в PDF (560 Кб)

 Проект на C# Windows Forms из статьи (Visual Studio 2019) (18 Кб)

25.09.2018, 18:06 [7886 просмотров]


теги: textprocessing программирование форматы c# c++/cli

К этой статье пока нет комментариев, Ваш будет первым