[Назад]
[Вперед]
Виктор Кон, vkBook, Java-First
Второй класс
Начинаем писать второй класс MainForm. Его мы поместим в тот же
файл, потому что первый класс фактически просто задал имя программы,
а сам внешний вид программы не написан. Первый кусок кода определяет
внутренние переменные класса. Если переменные не общие (public), то
их принадлежность программе класса можно не афишировать. Но все
используемые переменные обязательно нужно декларировать с указанием
их типа. Я буду иногда объекты класса называть переменными типа - это не совсем
правильная терминология, но она более привычна для тех, кто раньше с
классами не работал. Итак начало.
class MainForm extends JFrame implements ActionListener {
static JDesktopPane deskt;
int i,j,k,km,l,lm,nt;
String fn,ls; JMenu menu; JMenuItem submenu;
String[] name; int[] mne; int[] acc; String[] act;
String[] actt = new String[200];
|
Разберем, что означает написанное. Слово extends (развивает) означает,
что наш класс является потомком класса JFrame. Кроме того, он наследует все методы
абстрактного класса ActionListener. Это фактически не класс, а интерфейс как
в фортране или С - он только декларирует список методов. В данном случае наш
интерфейс, как следует из его названия определяет реакцию на действия
пользователя, например, выбор иконки в меню.
Далее, мы декларировали статический объект класса JDesktopPane. Этот
класс определяет внешнюю панель нашего окна, в которую мы будем помещать
внутренние окна. Внутренние окна, также как и диалоги, не имеют своих
иконок - они закрываются и открываются вместе с главным окном. Это удобно, так
как окна нашей программы на экране не будут путаться с окнами других программ.
Почему надо объявить объект статическим. Потому что он у нас фактически один,
но нам придется обращаться к нему из других классов. Мы будем делать
это таким образом - открывать как-бы новый объект этого класса и возвращать
из него только deskt. Так как он статический - мы не промахнемся и попадем
точно в наше окно. Это станет ясно позднее. Пока просто запомните эти слова.
Все остальные типы переменных нам знакомы кроме JMenu и JMenuItem. Но они очевидны
и мы ими скоро займемся. Хочу отметить, что все объекты только продекларированы,
в то время как массив строк actt уже определен более точно - указан его размер.
Начинаем определять методы. Первый метод - конструктор - обязан быть public и иметь
то же имя, что и класс. Он автоматически исполняется при задании объекта
с помощью слова new.
Как я уже предупреждал, мы задаем пустой конструктор. Так надо, иначе при получении
deskt в других классах мы будем делать лишнюю работу. Следующий метод make()
мы уже использовали. Теперь мы его зададим
public void make() throws IOException {
fn="icon.png";
BufferedImage bi = javax.imageio.ImageIO.read(new File(fn));
setIconImage(bi);
|
Первая строка ясная. Метод public так как мы его использовали в другом классе.
Обработка ошибок та же. Следующие строчки задают иконку в левый верхний угол нашего окна.
Предполагается, что иконка нарисована в файле с названием "icon.png". При работе программы этот
файл должен присутствовать в папке программы, иначе будет ошибка. Класс BufferedImage
очень важный и интересный класс. Его объект bi содержит нашу картинку в развернутом
виде матрицы пикселей. При этом статический метод read() класса javax.imageio.ImageIO
прочитывает файл с нашим именем и автоматически декодирует картинку в матрицу пикселей.
Чтобы не было конфликтов с методами read() других классов здесь надо полностью
указывать весь путь к классу. Далее функция (метод) класса JFrame setIconImage()
устанавливает эту картинку в качестве иконки окна. Очень важно знать какие типы
(классы) какая функция имеет в виде аргументов, иначе получится ошибка и компилятор
код забракует. Если указанный код не поставить, то ничего страшного не произойдет.
Виртуальная машина сама поставит иконку по умолчанию в виде чашки кофе. Далее
Эта функция класса JFrame устанавливает имя окна (title). Строку с именем мы уже
определили раньше, а теперь используем, и она нам еще не раз понадобится.
Dimension scrSize = Toolkit.getDefaultToolkit().getScreenSize();
km=scrSize.width; lm=scrSize.height-25;
ls=MyPro.readLine("First.ini",2); act = MyPro.split(ls,'|');
i=Integer.parseInt(act[0]); j=Integer.parseInt(act[1]); k=Integer.parseInt(act[2]); l=Integer.parseInt(act[3]);
i += (km-k)/2; j += (lm-l)/2; if(i < 0){i=0;} if(j < 0){j=0;} if(i > km-k){i=km-k;} if(j > lm-l){j=lm-l;}
setBounds(i,j,k,l);
|
Этот довольно большой кусок текста определяет размеры окна и его положение на экране.
Это можно делать разными способами. Рассмотрим сначала то, что написано. Первая строка
определяет размеры экрана дисплея в пикселах. Здесь указаны два метода, последовательно выполняемые
слева направо. Первый статический метод класса Toolkit возвращает новый объект класса
Toolkit, а второй метод уже принадлежит этому объекту. Но нам это даже не
обязательно разбирать. Размеры экрана дисплея возвращаются именно такой строкой и именно
в объект класса Dimension. Далее, вынимаем ширину и высоту этого объекта в пикселах в целые переменные km и lm.
Теперь считываем четыре числа из второй строки нашего файла с именем "First.ini".
Предполагается, что эти числа записаны с разделителем в виде символа вертикальной черты '|'
(ASCII код 124). Сначала мы конкретизируем объект ls класса String, заполняя его второй строкой
из нашего файла. Затем, используя статический метод split() нашего класса MyPro, разделяем одну строку
в массив строк, тем самым уточняя объект act. В нашем случае индексы этого массива
равны 0,1,2,3. Далее мы видим, что переменные типа целый тоже имеют свой класс Integer и
у этого класса есть статический метод parseInt(), который превращает строку в целое число,
которое последняя описывает. Применяя этот метод 4 раза получаем нужные нам 4 числа i,j,k,l.
Пусть первые два числа задают сдвиг центра нашего окна из центра экрана, а последние два
числа задают размеры окна (все в пикселах). Тогда сдвиг центра нужно пересчитать в сдвиг
левого верхнего угла, что и делается. Окончательно метод setBounds() класса JFrame задает размеры и положение
окна на экране. Отмечу, что вместо него также можно использовать методы setSize(k,l) и
setLocation(i,j) для раздельного задания размеров и положения окна. Если использовать метод
setLocationRelativeTo(null), то окно автоматически устанавливается в центре экрана. То есть если
лень много писать, то весь указанный выше код можно заменить на последнюю функцию и окно всегда будет
в центре экрана.
Теперь начинаем наполнять окно содержанием. Фактически мы здесь только установим полоску меню.
Для этого запишем следующий код
deskt = new JDesktopPane(); setContentPane(deskt);
deskt.setDragMode(JDesktopPane.OUTLINE_DRAG_MODE);
setJMenuBar(createMenuBar());
}
|
Здесь мы наконец определили нашу главную панель deskt и затем поместили ее в окно
методом setContentPane(). Затем мы установили новое свойство нашего окна. А именно,
как его перерисовывать при перемещении по экрану. Мы установили, что при перемещении
двигается только контур окна и только в конце перерисовывается все окно. Так быстрее
и проще работает компьютер. Наконец, функцией setJMenuBar() мы поставили полоску меню
в наше окно, но аргументом является не сама эта полоска - объект класса JMenuBar, а
функция (метод), которая эту полоску возвращает. Это стандартная практика в Java.
Эту последнюю функцию нам как раз предстоит определить. Итак начнем. Опять вспомним, что
все параметры окна мы считываем из файла "First.ini". Пусть третьей строкой этого файла
будет набито число разных разделов меню. А дальше на каждый раздел будет по одной строке,
в которой сначала будет набито имя этого раздела, затем через пробел (или несколько
пробелов) его код, затем через пробел будут заданы имена всех подразделов данного
раздела, разделенные символом '|', затем через пробел будет задана последовательность
Мнемонических кодов клавиш (снова разделенные символом '|') и затем через пробел
последовательность кодов клавиш ускорителей (снова с '|'). Чтобы было понятнее покажу
как это должно выглядеть
4
Prog 80 Open|Exit 79|69 48|49
Show 75 Picture|Figure|Image 80|70|73 50|51|52
Run 82 Palit|Editor|Calculator 80|69|67 53|54|55
Help 72 About|Web|File 65|87|70 56|57|48
|
Мнемонический код клавиши раздела меню открывает раздел через дополнительную клавишу [Alt].
Мнемонический код подраздела открывает подраздел прямо (без [Alt]), но только после того, как
раздел уже открыт. И наконец ускоритель подраздела меню сразу открывает подраздел и тоже
набирается через [Alt]. Поэтому все коды разделов меню и ускорителей подразделов должны быть
уникальными и ни разу не повторяться. В Java все константы кодов клавиш имеют длинные имена,
заканчивающиеся символами клавиш. Но нам при чтении данных из файла проще задавать числа.
Таблицу кодов клавиш в виде чисел можно получить прямо из Java кода соответствующего класса.
Ниже я ее привожу в более или менее полном объеме
Клавиши, которые предпочтительно использовать
[0] = 48 | [5] = 53 | [A] = 65 | [F] = 70 | [K] = 75 | [P] = 80 | [U] = 85 | [Z] = 90 | [F5] = 116
|
[1] = 49 | [6] = 54 | [B] = 66 | [G] = 71 | [L] = 76 | [Q] = 81 | [V] = 86 | [F1] = 112 | [F6] = 117
|
[2] = 50 | [7] = 55 | [C] = 67 | [H] = 72 | [M] = 77 | [R] = 82 | [W] = 87 | [F2] = 113 | [F7] = 118
|
[3] = 51 | [8] = 56 | [D] = 68 | [I] = 73 | [N] = 78 | [S] = 83 | [X] = 88 | [F3] = 114 | [F8] = 119
|
[4] = 52 | [9] = 57 | [E] = 69 | [J] = 74 | [O] = 79 | [T] = 84 | [Y] = 89 | [F4] = 115 | [F9] = 120
|
|
Клавиши, которые лучше не использовать, но если очень надо, то можно
[BackSp] = 8 | [Tab] = 9 | [Enter] = 10 | [Shift] = 16 | [Ctrl] = 17 | [Alt] = 18 | [CapsLock] = 20
|
[Esc] = 27 | [Space] = 32 | [PageUp] = 33 | [PageDn] = 34 | [End] = 35 | [Home] = 36
|
[LeftArr] = 37 | [UpArr] = 38 | [RightArr] = 39 | [DownArr] = 40
|
[,] = 44 | [-] = 45 | [.] = 46 | [/] = 47 | [;] = 59 | [=] = 61
|
[[] = 91 | []]=93 | [\] = 92 | [Insert] = 155 | [Delete]=127
|
|
Итак, начинаем писать программу определения меню и автоматической дешивровки входных данных файла
protected JMenuBar createMenuBar() {
JMenuBar menub = new JMenuBar(); StringTokenizer t; String mname;
int ik,im,isb,nl,nm,nsb;
|
Определили переменные и задали объект menub который мы будем возвращать
nl=3; ls=MyPro.readLine("First.ini",nl); t = new StringTokenizer(ls);
|
считали 3-ю строку файла и ввели ее в объект класса StringTokenizer. Объекты этого класса
умеют выделять из строки куски текста без пробелов используя пробелы как разделители. В отличие
от него метод split() класса String просто разделяет строку на систему строк, используя регулярные
выражения, например, признак конца строки, в то время как для объектов класса
String символы разделения строк, используемые в обычных редакторах, ничего не значат.
В один объект можно запихнуть хоть целую книгу. Интересно, что вместо написания собственной процедуры
разделения строк по указанному символу можно было бы использовать метод split() класса String.
Но, как показала практика, не все символы видимо являются регулярными выражениями - компилятор ошибок
не видит, а код работает неправильно. Поэтому пришлось написать свою процедуру разделения строк.
nm=Integer.parseInt(t.nextToken()); nt=0;
|
Определили число разделов меню. Метод nextToken() как раз возвращает очередную порцию текста
без пробелов.
for(im=0; im < nm; im++){
nl++; ls=MyPro.readLine("First.ini",nl); t = new StringTokenizer(ls);
mname=t.nextToken(); ik=Integer.parseInt(t.nextToken()); menu = new JMenu(mname); menu.setMnemonic(ik);
|
В цикле по разделам меню сначала считываем из файла очередную строку, вводим ее в разделитель текста
и берем первый кусок. Это имя раздела. Берем второй кусок и преобразуем в число. Определяем объект класса JMenu
с данным именем и данным мнемоническим кодом.
ls=t.nextToken(); name = MyPro.split(ls,'|');
|
Берем следующий кусок и расщепляем его в массив строк name. Это объект класса String[]. Он описан в самом
начале класса.
nsb=name.length; mne = new int[nsb]; acc = new int[nsb];
|
Поле (переменная) length у любого массива возвращает его размер. Поэтому nsb равно числу элементов массива
name. Используя это число определяем размер целых массивов mne и acc, то есть фактически определяем массивы.
ls=t.nextToken(); act = MyPro.split(ls,'|');
for(isb=0; isb < nsb; isb++){mne[isb]=Integer.parseInt(act[isb]);}
ls=t.nextToken(); act = MyPro.split(ls,'|');
for(isb=0; isb < nsb; isb++){acc[isb]=Integer.parseInt(act[isb]); act[isb]="ac"+nt; actt[nt]=act[isb]; nt++;}
|
Здесь мы два раза переопределили массив act и вынули из него целые числа - коды клавиш. Но нам еще необходимо
задать уникальное имя команды для каждого подраздела меню. Проще всего автоматически сгенерировать эти имена,
используя порядковый номер подраздела, что и было сделано. В конце концов массив act[] равен строкам, у которых
два первых знака равны ac, а следующие - текстовому представлению переменной nb. Здесь полезно отметить, что
строки можно соединять при помощи знака + и даже соединять с числами, при этом числа автоматически преобразуются
в текст. Кроме того, надо собрать весь набор команд в массиве actt[].
createSubMenu(); menub.add(menu);
}
|
После того как все необходимые массивы определены вызывается метод createSubMenu(), который формирует раздел меню и
затем добавляет его в полоску меню. После завершения полного цикла вся полоса меню, заданная из входных данных определена.
Но при желании можно еще принудительно добавить один раздел меню сверх установленных в файле. Пусть он имеет имя "About".
Это можно сделать более традиционным способом
menu = new JMenu("About"); menu.setMnemonic(KeyEvent.VK_A);
name = new String[]{"About Program","About Author"};
mne = new int[]{KeyEvent.VK_P, KeyEvent.VK_A};
acc = new int[]{KeyEvent.VK_F1, KeyEvent.VK_F2};
act = new String[]{"pppp", "llll"};
createSubMenu(); menub.add(menu);
return menub;
}
|
Здесь используется еще одна тонкость Java - объявляется массив и тут же он задается в фигурных скобках перечислением.
Вот теперь вся полоса меню задана. Но она использует функцию (метод) createSubMenu(). Этот метод имеет абсолютно
стандартный вид и вы можете ставить его в каждую свою программу не задумываясь. Я даже не буду его комментировать,
а просто приведу код
protected void createSubMenu(){
for( i=0; i < act.length; i++ ){
submenu = new JMenuItem(name[i]); submenu.setMnemonic(mne[i]);
submenu.setAccelerator(KeyStroke.getKeyStroke(acc[i], ActionEvent.ALT_MASK));
submenu.setActionCommand(act[i]); submenu.addActionListener(this);
menu.add(submenu);
}}
|
Обратите внимание, что этот метод ничего не возвращает и оперирует общими переменными класса.
Итак меню у нас есть, но нам необходимо обеспечить реакцию нашей программы на выбор каждого раздела
меню. Это можно делать самыми разными способами. Но мы пишем шаблон, поэтому проще всего формализовать
эту процедуру. Наш класс поддерживает интерфейс ActionListener. Согласно этому интерфейсу мы
должны переопределить функцию, написанную ниже
public void actionPerformed(ActionEvent e){
for(i=0; i < nt; i++){
if( actt[i].equals(e.getActionCommand()) ){MyPro.isn=i+1; execPro();}
}
if( "pppp".equals(e.getActionCommand()) ){addAbout(1);}
if( "llll".equals(e.getActionCommand()) ){addAbout(2);}
}
|
Как это работает. При выборе какого-либо раздела меню сгенерируется событие как объект класса ActionEvent и оно будет
передано автоматически в нашу функцию actionPerformed(). Метод e.getActionCommand() возвращает текст команды для данного
события. Мы должны сравнить все наши тексты команд с текстом данной команды, и при совпадении мы
получаем номер раздела меню в переменную i. Удобно зафиксировать этот номер в статической переменной isn
нашего класса MyPro, поскольку мы пока не знаем, где и когда будем использовать этот номер, и вызвать
стандартную функцию execPro(). Фактически можно было бы все написать прямо здесь, не вводя функцию, но
это неудобно читать.
А так мы можем теперь спокойно написать эту функцию. В общем
случае у этой функции может быть больше кода. Однако в нашем простом примере мы позаботимся только об одном.
А именно, код программы, как реакция на нажатие клавиши, может оказаться достаточно сложным. И вполне возможно
нам придется открывать новые окна, делать запросы у пользователя, запускать внешние программы и много
чего еще. При этом, чтобы указанные процессы могли быть выполнены нам надо уметь управлять нашим
основным процессом. Для этого основной процесс удобно запустить внутри определенной траектории (на Java
почему то ее называют "нить"). Объект класса Thread (Нить) как раз и реализует отдельную траекторию.
Он обладает рядом методов, в том числе и статических. Движение по траектории (нити) можно приостановить на определенное время,
даже если больше нет никаких процессов. Можно также его остановить до тех пор, пока не выполнятся процессы на
других траекториях.
Дело в том, что все приборные процессы, связанные с экраном, принтером, модемом, и многие другие реализуются в
отдельных нитях. При задании показа нового окна этот процесс всего лишь становится в очередь и ждет
когда кончится тот процесс (нить) который уже идет. Но если вам необходимо выполнить новый процесс
немедленно, то необходимо насильно приостановить основной процесс. Для этого его и надо вставить в объект
класса Thread. Довольно тонкий вопрос, но на практике о нем можно не задумываться. Делайте каждый раз
по указанному шаблону и все получится правильно. Итак
private void execPro(){
Thread tr = new Thread(new runPro()); tr.start();
}
|
Из этого текста видно, что весь код нашей программы мы теперь будем писать в классе runPro, который
обязан поддерживать интерфейс Runnable, так как его объект является аргументом объекта класса Thread.
При этом мы будем использовать переменную MyPro.isn. То есть нам в том классе придется сделать ветвление
и обрабатывать каждое значение этой переменной. Осталось написать функцию addAbout(). В
простейшей форме она просто сообщает некий текст в специальном окне. Тогда ее код может быть таким.
private void addAbout(int n){
String mess; mess=" ";
switch(n){
case 1:
mess =
"\n Первое сообщение "+
"\n текст второй строки "+
"\n и так далее "+
"\n ";
break;
case 2:
mess =
"\n Второе сообщение "+
"\n текст второй строки "+
"\n и так далее "+
"\n ";
break;
}
JOptionPane.showMessageDialog(this, mess, "About", JOptionPane.INFORMATION_MESSAGE);
}
|
Статический метод showMessageDialog() класса JOptionPane - это полностью готовая программа, которая
показывает текст в отдельном окне в режиме диалога, то есть программа
ждет и ничего не делает, пока пользователь не нажмет клавишу "OK". Итак у нас все практически готово
для работы. Но надо еще написать простой метод, который возвращает нам содержимое нашего окна. Имея
такую возможность мы сможем программно, то есть автоматически, ставить и убирать внутренние окна.
public JDesktopPane getDesktop(){
return deskt;
}
}
|
Итак, чтобы закончить программу, нам осталось написать еще два класса MyPro и runPro.
Это мы будем делать в других файлах. А этот файл написан и пора подумать о импорте пакетов Java,
которые мы использовали. Мы должны добавить указатели на эти пакеты в начале файла. Самый простой
способ - пустить файл на компиляцию и компилятор вам сам скажет каких классов он не нашел. Затем
надо посмотреть в каких они пакетах и добавить. Я как раз так и собираюсь проделать.
Компилятор не нашел следующих классов
Класс | Пакет |
Action Event | java.awt.event.* |
Action Listener | java.awt.event.* |
BufferedImage | java.awt.image.* |
Dimension | java.awt.* |
IOException | java.io.* |
JDesktopPan | javax.swing.* |
JFrame | javax.swing.* |
JMenu | javax.swing.* |
JMenuBar | javax.swing.* |
JMenuItem | javax.swing.* |
JOptionPane | javax.swing.* |
KeyEvent | java.awt.event.* |
KeyStroke | javax.swing.* |
StringTokenizer | java.util.* |
SwingUtilities | javax.swing.* |
Toolkit | java.awt.* |
UIManager | javax.swing.* |
|
Классы, входящие в пакет java.lang.* компилятор нашел, например String. Этот пакет
считается основой языка Java и компилятор в нем ищет классы в любой программе автоматически.
Все найденные классы имеются в версиях 1.2.2 и выше. Но используемый в явном виде класс
javax.imageio.ImageIO появился позднее версии 1.2.2.
Итак, нам необходимо в начало файла поставить следующие строки
import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import java.io.*;
import javax.swing.*;
import java.util.*;
|
После повторной компиляции компилятор не обнаружил всего два класса MyPro и runPro. Это
правильно, этих классов мы еще не написали и как раз сейчас этим займемся.
[Назад]
[Вперед]