C, PHP, VB, .NET

Дневникът на Филип Петров


* Notepad с помощта на NetBeans

Публикувано на 26 ноември 2015 в раздел УКИ.

Ще покажем как можем сами да направим популярното приложение Notepad. Ще се научим как се работи с менюта, някои допълнителни свойства на JTextArea, както и ще се запознаем с това как да вграждаме чужд код в нашето приложение. Ще се научим и как да отваряме сайт с уеб браузъра по подразбиране чрез команда от Java приложение. Ще се научим и как сами да добавяме ново събитие към съществуващ обект.

Стартирайте нов проект с празен JFrame, който именовайте като JNotepad. Изберете BorderLayout за основния прозорец. Добавете JLabel долу (SOUTH) и JScrollPane в центъра. JScrollPane е специален вид контейнер (панел) – ако вложените в него компоненти излезнат извън екранната рамка, той ще покаже вертикален и/или хоризонтален scrollbar, с който да можете да се движите вътре в прозореца. Накрая вътре във вложения JScrollPane сложете един JTextArea. Резултатът трябва да е следния:

1

Вмъкнете Menu Bar в основния фрейм.

2

JMenuBar е обект, в който може да вграждате други обекти от тип JMenu. По подразбиране имате две – File и Edit. Можете да ги изтриете или да добавяте други:

3

Добавете основните менюта за приложението Notepad – освен вече съществуващите File и Edit, добавете Format, View и Help:

В тези менюта от своя страна можете да вграждате JMenuItem – крайните опции, които ще бъдат избирани. Ето как можем да добавим бутон в меню Help:

 

4

Сега разгледайте приложението Notepad в Windows. Направете следните действия:

  • Добавете същите опции в менюто, които има то (File > New, Open…, Save, и т.н.);
  • Подравнете сложения JLabel вдясно и му сложете текст „Ln 1, Col 1“;
  • Именовайте всеки един от обектите по подходящ начин.

В крайна сметка трябва да получите нещо като това:

5

В конкретната реализация сме спестили няколко бутона в менюто:

  • File > Page Setup и File > Print: за тях ще ни трябват по-специфични знания за работа с принтери, затова ги пропускаме;
  • Edit > Find, Edit > Find Next, Edit > Replace, Edit > Go To… и Edit > Time/Date: те не са толкова сложни и накрая ще ги оставим за домашна работа.

Сега нека започнем с реализацията на всяка една от споменатите функционалности. Ще започнем в обратен ред – първото, което ще реализираме е Help > Help и Help > About.

Опцията Help > About в Notepad отваря диалогов прозорец със някаква стандартна информация. В нашата реализация просто ще изпишем съобщение на екрана и ще сложим бутон OK, с който прозореца да бъде затворен. Веднага бихме се сетили, че това може да се направи като добавим нов фрейм в нашето приложение, който да отваряме при натискане на бутона. Да, но About диалоговия прозорец в Notepad е „модален“ (не може да се върнете в основния прозорец ако не го затворите). Не че е невъзможно JFrame да бъде направен модален, но не е планиран да бъде такъв. Затова в Java има един специално създаден за това обект наречен JDialog. Изтеглете го някъде в работното поле извън рамката на текущия прозорец:

6

Този диалогов прозорец ще се появи в „Other Components“:

7

Прекръстете обекта на infoDialog. Ще използваме този прозорец, за да изкарваме съобщения от различни компоненти. Отворете го в Design View. Направете го с Border Layout. В центъра сложете jLabel, който го центрирайте и го именовайте като infoText. Долу  (SOUTH) сложете панел, в чийто център сложете OK бутон:

 

8

На основния JDialog обект задължително отбележете Modal и изключете Resizable, както е показано на картинката горе. Като title задайте стандартен „Info“.

Важно: Задължително задайте на диалоговия прозорец и minimumSize да бъде със същата стойност като preferredSize (в нашия случай това е [400, 300]). С това ще си спестим да указваме размери на прозореца по-нататък, когато го отваряме (по подразбиране винаги ще се отваря на minimumSize).

Сега създайте actionPerformed събитие на infoDialogOKButton със следния код:

infoDialog.setVisible(false);

По този начин отворения диалогов прозорец ще бъде затворен.

Какво ще прави нашия Help > About бутон? Точно обратното на infoDialogOKButton – той трябва да отваря прозореца. Върнете се в основния JFrame и създайте actionPerformed събитие за бутона Help > About със следния код:

 infoText.setText("Нашият текстови редактор");
 infoDialog.setVisible(true);

Дотук направихме така, че Help > About да работи:

9

Сега да се фокусираме върху Help > Help. Стандартно това отваря help файла на Notepad в Windows. Ние го нямаме този файл (освен ако не го намерим и не го вмъкнем като ресурс в проекта си например). Можем обаче за тренировка да направим така, че при избиране на този бутон от менюто човека да бъде препратен на уебсайта на Майкрософт, в който е даден Help за приложението Notepad (а по-нататък да си направим свой собствен уебсайт с помощ за това приложение и да пренасочваме към него). Направете следното – отворете Source и в началото на файла добавете следния код непосредствено под „package jnotepad“:

import java.awt.Desktop;
import java.net.URL;

Първата бибилотека (Desktop) е системна и позволява на нашето Java приложение да комуникира с компоненти от операционната системат. Второто е специфичен обект за работа с уебсайт адреси. Сега създайте actionPerformed на Help > Help бутона със следния код:

try {
   String site = "http://windows.microsoft.com/en-us/windows/notepad-faq";
   Desktop.getDesktop().browse(new URL(site).toURI());
} 
catch (URISyntaxException | IOException e) {
   infoText.setText("Не мога да отворя help сайта");
   infoDialog.setVisible(true);
}

С този код казваме следното: „опитай (try) се да отвориш този уебсайт със стандартния браузър в операционната система, а ако не успееш (catch и съответната грешка), отвори диалоговия прозорец и изпиши съобщение за грешка“. Тези try-catch блокове са важна част от работата с обекти в Java – понякога обектите вместо да свършат работата, която сме им указали, те генерират т.нар. „изключение“. В случая в catch блока се обработва такова изключение. При нормална работа на програмата в catch блока няма да се достигне.

Свършихме с меню Help. Минаваме към меню „View“. В него има единствена опция – Status Bar. При натискане тя трябва да показва или да скрива обекта statusBarText. Създайте събитие за натиснат бутон на View > Status Bar със следния код:

if(statusBarText.isVisible()){
   statusBarText.setVisible(false);
}
else{
   statusBarText.setVisible(true);
}

Логиката е следната – ако се вижда, правим го невисим, а ако не се вижда, правим го видим.

Преминаваме към следващата опция – Format > Word Wrap. В него трябва да направим две неща:

  • Ако в текстовото поле myText опцията lineWrap е изключена, трябва да я включим и да забраним ползването на бутон „view > status bar“;
  • Ако опцията lineWrap на myText е включена, трябва да я изключим и да пуснем бутон „view > status bar“.

Създайте actionPerformed събитие на Format > Word Wrap със следния код:

if(myText.getLineWrap()==true){
   myText.setLineWrap(false);
   viewStatusBar.setEnabled(true);
}
else{
   myText.setLineWrap(true);
   viewStatusBar.setEnabled(false);
   statusBarText.setVisible(false);
}

Сега трябва да направим Format > Font. Тук ще изпитаме огромно затруднение, понеже Java Swing не предоставя стандартен контрол за промяна на шрифта – трябва да си направим такъв сами. Ако отворите Format > Font на Notepad, ще видите, че това… не е лека задача, защото има много компоненти. Естествено можем да го направим, но може би е по-добре да потърсим някакво готово решение – да вземем кода наготово от друг проект взет някъде от интернет и да го вградим в нашия. Точно така и ще постъпим! Отваряме Гугъл и търсим ключови фрази като „Font Chooser Java“, евентуално добавяме ключови думи като „free“ или „open source“, защото искаме да е безплатен. Вероятно ще има много различни сайтове, които ще предлагат подобни решения. В случая си харесваме отговора на David в StackOverflow:

http://stackoverflow.com/a/7528129

Създайте нов Java Class във вашия проект с име JFontChooser. В него вмъкнете кода от горния линк. За удобство можете и директно да изтеглите файла от тук >>> JFontChooser <<< и да го вмъкнете в директорията на проекта си. Единственото, което трябва да промените в него е пакета, с който работи. В примерния код е „package paint;“, а при вас трябва да е „package jnotepad;“. Запаметете и … готово! Тази библиотека вече е част от вашия проект! Остава само да го използваме. Създайте actionPerformed събитие на Format > Font със следния код:

JFontChooser js = new JFontChooser();
js.showDialog(myText);
myText.setFont(js.getSelectedFont());

Този код разбира се ще бъде различен ако използвате друг проект за реализация на въпросния диалогов прозорец. В случая следваме документацията на проекта, който вградихме. На първия ред създаваме обект с новия диалогов прозорец. На втория ред показваме диалоговия прозорец и му подаваме myText обекта (той вътрешно ще си го запази и ще работи с него). След като затворим този диалогов прозорец, управлението ще се върне към основния и ще се изпълни командата setFont на myText, която ще вземе резултата от създадения преди това обект js.

Сега преминаваме към менюто Edit. Реализирането на бутон „Undo“ няма да е лесна задача. Трябва да създадем т.нар. „undo manager“ и да му укажем, че трябва да следи за промени в myText обекта. Отворете Source и добавете в началото библиотеката UndoManager:

import javax.swing.undo.UndoManager;

След това намерете конструктора – действието public JNotepad(). Преди него (извън тялото му оградено с фигурни скоби) декларирайте променлива:

UndoManager manager;

По този начин тази променлива ще е видима в целия код на приложението ви. Сега вмъкнете следните два реда СЛЕД командата initComponents():

manager = new UndoManager();
myText.getDocument().addUndoableEditListener(manager);

Написаното от вас трябва да изглежда по следния начин:

10

Готово – създадохме UndoManager, който следи нашия myText обект. Остава да създадем actionPerformed събитие на бутона Edit > Undo, което извиква undo метода на мениждъра:

manager.undo();

Готово! Имате работещ бутон Edit > Undo.

Реализацията на Cut, Copy и Paste е може би най-лесното нещо в тази задача. За всеки един от бутоните създайте събитие, в което реализирайте последователно следните три действия:

За cut:

myText.cut();

За copy:

myText.copy();

За paste:

myText.paste();

За бутона Delete нещата не са по-сложни – те се свеждат до замяна на селектирания в момента текст с празен низ:

myText.replaceSelection("");

Бутон Select All също е тривиален:

myText.selectAll();

За реализацията на елементите в меню File ще изпитаме известни затруднения. Първо различните команди са свързани:

  • Ако сме отворили нов файл и натиснем Save, това трябва всъщност да извика Save As…;
  • Ако натистем New, Open или Exit, но не сме запаметили текущия файл, трябва да попитаме човека дали иска да го запамети.

Тоест на нас ни трябва някаква обща за всички команди индикация – с кой файл работим и запаметен ли е. Ще реализираме това по следния начин – ще дефинираме три променливи за име на файл, директория и дали е запаметен. Добавете следния код в началото на файла там, където преди дефинирахме UndoManager променливата:

String currentFile;
String currentFileDir;
boolean fileIsSaved;

 

След това вътре в конструктора (метод public JNotepad()) добавете следното:

currentFile = "";
currentFileDir = "";
fileIsSaved = false;

Също така в началото на файла добавете следната клауза – това е библиотека, която ще използваме, за да съхраним файла:

import java.io.FileWriter;

Ще започнем от реализацията на Save As. Знаете, че при натискане на такъв бутон в менюто се появява диалогов прозорец на Windows, в който трябва да изберем директория, да напишем името на файла и да натиснем Save. Прозорецът е същия като прозореца за Open, но с различен бутон и естествено различно действие. И двата в Java Swing се реализират с компонент JFileChooser. Издърпайте го в празно поле на проекта си:

11

Ще видите, че jFileChooser1 ще се появи вдясно в раздела Other Components. Преименувайте го просто на fileChooser. След това добавете събитие за бутона Save As… със следния код (дълъг е и сме поставили коментари):

// Показваме Save диалогов прозорец
int result = fileChooser.showSaveDialog(this);
// Ако човекът е натиснал Save бутона в прозореца
if (result == fileChooser.APPROVE_OPTION) {
   // Записваме името и директорията на избрания файл
   currentFile = fileChooser.getSelectedFile().getName();
   currentFileDir = fileChooser.getCurrentDirectory().toString();
   // Опитваме се да запишем информация във файла
   FileWriter fw = null;
   try{
      fw = new FileWriter(fileChooser.getSelectedFile());
      myText.write(fw);
   }
   // Ако не стане, отпечатваме съобщение за грешка
   catch(IOException e){
      infoText.setText("Проблем със запис във файла");
      infoDialog.setVisible(true);
   }
   // Накрая независимо какво е станало, затваряме файла
   finally{
      try{
         if(fw!=null) fw.close();
      }
      catch(Exception e2){}
   }
   // Слагаме title на JNotePad, с който указваме името на файла
   this.setTitle(currentFileDir+"\\"+currentFile);
   // Указваме в променливата, че файлът е запаметен
   fileIsSaved = true;
}
else if (result == fileChooser.CANCEL_OPTION) {
   // Тук не правим нищо - човекът е натиснал Cancel
}

Сега командата Save. При нея ако все още не сме запаметявали, трябва да извикаме Save As. Ако вече сме запаметявали, трябва да презапишем същия файл отгоре.

if(fileIsSaved == false){
   fileSaveAsActionPerformed(evt);
}
else{
   FileWriter fw = null;
   try{
      fw = new FileWriter(currentFileDir+"\\"+currentFile);
      myText.write(fw);
   }
   catch(IOException e){
      infoText.setText("Проблем със запис във файла");
      infoDialog.setVisible(true);
   }
   // Накрая независимо какво е станало, затваряме файла
   finally{
      try{
         if(fw!=null) fw.close();
      }
      catch(Exception e2){}
   }
   fileIsSaved = true;
}

Сега ще направим така, че ако променим каквото и да е вътре в текста, променливата fileIsSaved да стане false. За целта няма готово събитие в JTextArea – трябва сами да си направим такова. Ще използваме събитието DocumentListener, което генерира DocumentEvent. Добавете import в началото на файла:

import javax.swing.event.DocumentListener;
import javax.swing.event.DocumentEvent;

След това добавете това създайте събитието и го добавете към вашето текстово поле. Добавете следния код в public JNotepad():

DocumentListener docListener = new DocumentListener(){
   public void changedUpdate(DocumentEvent e) {
      fileIsSaved = false;
   }
   public void removeUpdate(DocumentEvent e) {
      fileIsSaved = false;
   }
   public void insertUpdate(DocumentEvent e) {
      fileIsSaved = false;
   }
};
myText.getDocument().addDocumentListener(docListener);

Готово! Създадохте допълнително събитие от тип DocumentListener в myText обекта. Виждате, че каквото и да се случи с документа, fileIsSaved ще става false. Тази стойност е важна за операциите New и Open – при тях трябва да се допитваме до променливата и ако fileIsSaved не е true, трябва да информираме човека, че първо трябва да запамети. Тоест ще ни трябва диалогов прозорец с два бутона – Yes или No. Този път ще го реализираме с нов компонент – JOptionPane. Работата с него е подобна на тази на JDialog, но доста по-опростена. Първо го добавяме:

12

Ще видите, че то идва с един бутон и стандартно съобщение по подразбиране. Няма да ги променяме – ще го направим тогава, когато го извикваме. Създайте събитие за File > New със следния код (поставили сме коментари):

// Ако файлът не е запаметен
if(fileIsSaved == false) {
   // Извикваме диалоговия прозорец
   int dResult = wantToSave.showConfirmDialog(null,
                         // Съобщението
                         "Искате ли да запаметите?",
                         // Тип на иконката
                         "Warning",
                         // Какви бутони да има
                         wantToSave.YES_NO_OPTION);
   // Ако човекът е натиснал Yes
   if(dResult == wantToSave.YES_OPTION){
      fileSaveActionPerformed(evt);
   }
}
 
// Няма else - независимо какво е станало преди това продължаваме
currentFile = "";
currentFileDir = "";
fileIsSaved = false;
myText.setText("");

Команда File > Exit е малко по-простичка:

if(fileIsSaved == false) {
   int dResult = wantToSave.showConfirmDialog(null,
                            "Искате ли да запаметите?",
                            "Warning",
                            wantToSave.YES_NO_OPTION);
   if(dResult == wantToSave.YES_OPTION){
      fileSaveActionPerformed(evt);
   }
}
// С тази команда затваряме прозореца
this.dispose();

Накрая команда File > Open. Тя практически ще е с обратното действие на File > Save As. Вместо да записва вътре във файла, трябва да го прочете. За целта ни е нужен обект FileReader:

import java.io.FileReader;

Сега създаваме събитието за File > Open:

// Текущият файл запаметен ли е?
if (fileIsSaved == false) {
   int dResult = wantToSave.showConfirmDialog(null,
                            "Искате ли да запаметите?",
                            "Warning",
                             wantToSave.YES_NO_OPTION);
   if(dResult == wantToSave.YES_OPTION){
      fileSaveActionPerformed(evt);
   }
}
 
// Показваме Open диалогов прозорец
int result = fileChooser.showOpenDialog(this);
if (result == fileChooser.APPROVE_OPTION) {
   // Записваме името и директорията на избрания файл
   currentFile = fileChooser.getSelectedFile().getName();
   currentFileDir = fileChooser.getCurrentDirectory().toString();
   // Отваряме файла
   FileReader fr = null;
   try{
      fr = new FileReader(currentFileDir+"\\"+currentFile);
      myText.read(fr, currentFileDir+"\\"+currentFile);
   }
   catch (IOException e) {
      infoText.setText("Проблем с отварянето на файла");
      infoDialog.setVisible(true);
   } // Накрая независимо какво е станало, затваряме файла
   finally {
      try {
         if (fr != null) {
         fr.close();
      }
      } catch (Exception e2){}
   }
}
fileIsSaved = true;

До довършването на програмата остават няколко подробности. Първата от тях е съществена, затова ще я обясним подробно. Създадохме събитие File > Exit, но не се погрижихме за бутончето „X“ най-горе вдясно на прозореца. До този момент ако то бъде натиснато, прозореца се затваря независимо дали сме запаметили документа. Създайте ново събътие на основния JFrame, което е WindowClosing:

13

Неговият код ще е идентичен с този на File > Exit. Тоест можем направо да извикаме File > Exit.

fileExit.doClick();

Това не е най-правилния начин за работа (едно събитие да извика друго), но на този етап ще ни върши чудесна работа. По-нагоре се възползвахме от подобна техника при Save и Save As. По-нататък ще е добре да изолираме общия код в отделни методи и да извикваме тези методи. Засега с цел да не въвеждаме прекалено много нови неща ще работим по този начин.

Последното нещо, което ще направим, е да променяме текста в status bar в зависимост от позицията, на която се намираме. В момента той е статичен ln 1, col 1. Добавете събитие CaretUpdate на myText:

14

В кода на това събитие вмъкнете следното:

int caretpos = myText.getCaretPosition();
try{
   int row = myText.getLineOfOffset(caretpos);
   int column = caretpos - myText.getLineStartOffset(row);
   row++;
   column++;
   statusBarText.setText("Ln "+row+", Col "+column);
}
catch(Exception e){}

Допълнителна задача 1. Ако сте имали включен status bar, натиснете Format > Word Wrap и след това го изключите отново с Format > Word Wrap, status bar няма да е активен. Тоест при изключване Format > Word Wrap не помни дали status bar е бил включен или не.

Допълнителна задача 2. Опитайте се да реализирайте Edit > Find, Edit > Find Next, Edit > Replace и Edit > Go To.

Допълнителна задача 3. При стартиране и незабавно изключване на програмата, ще ви попита дали искате да запаметите. В оригиналният Notepad не е така. Направете така, че при начален и напълно празен документ, да не пита.

Допълнителна задача 4. Направете така, че при промяна вътре в документа да се появява една звездичка в края на title на основния прозорец.

 



Добави коментар

Адресът на електронната поща няма да се публикува


*