Visual C++: пишем простой многодокументный редактор текста RTF
То есть, MDI-приложение. Пример вот отсюда как-то мне не очень нравится, потому что написан левой ногой и не использует стандартные возможности по управлению дочерними окнами. Этот пример тоже, в общем, пишется левой ногой и прямо сейчас, но постараемся сделать хотя бы классическую картинку "много окон документов внутри главного окна приложения".
В 2015-й (или выше) Visual Studio создадим приложение Windows Forms на C++.
На форму приложения перетащим главное меню MenuStrip и стандартные диалоги для работы с файлами OpenFileDialog, SaveFileDialog.
У диалогов достаточно настроить свойство Filter на нужный нам тип файлов, с которым будем работать, выберем "Файлы RTF|*.rtf
".
В меню предусмотрим несколько пунктов, которые покажут разные возможности работы с файлами, текстом в дочерних окнах и самими окнами:
простое меню родительской формы 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 [8539 просмотров]