----------------------- Page 1-----------------------
Е. Б У Г si 3B>J-J
Пишем
ПРОГРАММЫ
У с и ИГРЫ
для сотовых
ТЕЛЕФОНОВ
ОЮ20094628492651 020094628492.
А -И
V/.
----------------------- Page 2-----------------------
Е. Буткевич
Пишем
ПРОГРАММЫ
Ни ИГРЫ
для сотовых
ТЕЛЕФОНОВ
Москва • Санкт-Петербург • Нижний Новгород • Воронеж
Ростов-на-Дону • Екатеринбург • Самара • Новосибирск
Киев • Харьков • Минск
2006
----------------------- Page 3-----------------------
ББК 32.882.9
УДК 621.395:004.3
Б93
Буткевич Е. Л .
Б93 Пишем программы и игры для сотовых телефонов. — СПб.: Питер, 2006. —
204 с : ил.
ISBN 5-469-01139-9
Эта книга — общедоступный самоучитель программирования для мобильных телефонов, с по-
мощью которого любой, даже не искушенный в секретах программирования читатель, сможет
овладеть базовыми знаниями языка Java, навыками работы с необходимыми инструментами и
утилитами, написать свои первые программы, а главное — увидеть и показать друзьям результат
на экране своего мобильника. Книга написана доступным, живым языком и ориентирована вовсе
не на профессионалов, а на всех, кто хотел бы расширить возможности своего мобильника и
испытывать себя в роли программиста.
Попробуйте — это совсем не сложно!
ББК 32.882.9
УДК 621.395:004.3
Все права защищены. Никакая часть данной книги не может быть воспроизведена в какой бы то ни было форме без письменного
разрешения владельцев авторских прав.
Информация, содержащаяся в данной книге, получена из источников, рассматриваемых издательством как надежные. Тем не
менее, имея в виду возможные человеческие или технические ошибки, издательство не может гарантировать абсолютную
точность и полноту приводимых сведений и не несет ответственности за возможные ошибки, связанные с использованием книги.
ISBN 5-469-01139-9 ' © ЗАО Издательский дом «Питер», 2006
----------------------- Page 4-----------------------
Краткое содержание
Предисловие 10
Глава 1 . С чего начинать? 14
Глава 2. Hello, World! 23
Глава 3. Пользовательский интерфейс высокого уровня 32
Глава 4. Работа с графикой 42
Глава 5. Структуры и форматы данных в J2ME 52
Глава 6. Организация многопоточных приложений 66
Глава 7. Сохранение данных и параметров приложения 74
Глава 8. Стандартные средства пользовательского интерфейса ..8 5
Глава 9. Организация потоков, работа с файлами 96
Глава 10. Ввод текста, работа со строками 107
Глава 11 . Работа со временем, датой и календарем 119
Глава 12. Работа над ошибками 127
Глава 13. Расширение функциональности с помощью API 137
Глава 14. Приложения для работы с Интернетом 147
Глава 15. Защита приложений 16 1
Bonus. Распространение мобильных приложений.
Практические советы по продажам мобильного контента 17 1
Приложение 1 . Полный листинг программы «Змейка» 173
Приложение 2. Полный листинг программы «BookReader» 190
Приложение 3. Полный листинг программы «AddressBook» 195
Литература 203
----------------------- Page 5-----------------------
Содержание
Предисловие 10
К читателям 10
От издательства 12
История языка Java 12
Глава 1 . С чего начинать? 14
Оборудование 14
Программное обеспечение 18
Эмуляторы 20
Глава 2. Hello, World! 23
Класс MIDlet 23
Создание мидлета 25
Импорт классов и пакетов 28
Компиляция и запуск мидлета 29
Упаковка мидлета 31
Г л а ва 3 . П о л ь з о в а т е л ь с к и й и н т е р ф е й с в ы с о к о г о у р о в н я ... . 3 2
Класс Form 33
Класс Display 34
Отображаем картинку , 34
Класс Command 36
Интерфейс CommandListener 38
Глава 4. Работа с графикой 42
Класс Canvas 42
Класс Graphics 42
Класс Graph 45
Рисование текста 47
Обработка клавишных событий 48
Глава 5. Структуры и форматы данных в J2ME 52
Объекты 52
Примитивные типы 53
Массивы 53
----------------------- Page 6-----------------------
Содержание 7
Класс Vector 54
Класс Snake 55
Класс Stack 64
Глава 6. Организация многопоточных приложений 66
Многозадачность 66
Класс Thread , 67
Приоритеты тредов 68
Синхронизация тредов \ 70
Класс Snake 72
Глава 7 . Сохранение данных и параметров приложения 74
RecordStore — хранилище записей 75
RecordEnumeration — нумератор списка записей 76
RecordFilter и RecordComparator — фильтр и компаратор 78
RecordListener — лови момент 80
RecordStoreException — возможные проблемы 80
High Score — таблица рекордов 81
Глава 8. Стандартные средства
пользовательского интерфейса 85
Класс List 85
Interface Choice 86
Класс SnakeGame 87
Класс Gauge 93
Глава 9. Организация потоков, работа с файлами 96
Класс InputStream : 96
Класс DatalnputStream 99
Класс BookReader 101
Класс ByteArraylnputStream 106
Класс OutputStream j 106
Глава 10. Ввод текста, работа со строками 107
Класс TextBox 107
Класс TextField 109
Класс AddressBook I l l
Глава 11 . Работа со временем, датой и календарем 119
Класс Date 119
Класс DateField 120
Класс Calendar 122
Класс TimeZone 123
Класс BirthdayFilter 124
----------------------- Page 7-----------------------
8 Содержание
Глава 12. Работа над ошибками 127
Класс Exception 127
Формирование исключений 130
Класс Alert 132
Класс AlertType 133
Глава 13. Расширение функциональности с помощью API .. . 137
Nokia API 137
Samsung API 141
Siemens API 144
Глава 14. Приложения для работы с Интернетом 147
Класс Connector 147
Интерфейс Connection 148
Интерфейсы InputConnection и OutputConnection 149
Интерфейс StreamConnection 149
Интерфейс StreamConnectionNotifier 153
Интерфейс DatagramConnection 153
Интерфейс ContentConnection 154
Интерфейс HttpConnection 155
Глава 15. Защита приложений 16 1
Java Decompiler 161
Защита приложений 167
Обфускация 167
Bonus. Распространение мобильных
приложений. Практические советы
по продажам мобильного контента 17 1
Приложение 1 .
Полный листинг программы «Змейка» 173
Приложение 2.
Полный листинг программы «BookReader» 190
Приложение 3.
Полный листинг программы «AddressBook» 195
Литература 203
----------------------- Page 8-----------------------
Посвящаю эту книгу двум самым близким
мне людям — отцу Леониду Михайловичу
и супруге Ксюше.
----------------------- Page 9-----------------------
Предисловие
К читателям
Этот самоучитель поможет вам сделать первые шаги в программировании для мо-
бильный телефон. Книга предназначена для всех желающих научиться разраба-
тывать приложения для мобильников, причем адресована в первую очередь тем,
кто уже владеет базовыми навыками программирования и основами объектно-
ориентированных языков. Тем не менее, она написана так, что даже самый нео-
пытный программист будет в состоянии реализовывать приведенные примеры
и обучаться всем хитростям сразу на практике. В таком случае, конечно же, не
помешает дополнительная литература, поскольку многие базовые вещи програм-
мирования остались за кадром.
Для разработки приложений мы будем использовать платформу J2ME компании
Sun Microsystems, поскольку на сегодняшний день это единственный универсаль-
ный инструмент программирования для мобильных телефонов.
Книга построена в виде самоучителя, разделенного на главы-уроки. В нем вы най-
дете больше практических советов и примеров, нежели теоретических сведений
из области объектно-ориентированного программирования. Я постарался изло-
жить материал максимально доступно, на пальцах, зачастую не вдаваясь в под-
робности и тонкости. Многочисленные примеры, приведенные в книге, хоть и не
являются образцом аккуратности кодирования или истиной в последней инстан-
ции, но реализованы и протестированы с точки зрения доступности непрофесси-
оналам. Опытные программисты могут найти книгу скучной, с избытком инфор-
мации об азах программирования, местами несерьезной в отношении к сложным
задачам, а кто-то будет с удовольствием читать этот букварь. Основной упор кни-
ги делается на практическом применении материала, на том, какие средства мо-
гут потребоваться во время разработок и как ими пользоваться.
Красивая, элегантная, аккуратно реализованная идея имеет хорошие шансы при-
нести создателю некоторую прибыль. Поэтому после обучения собственно про-
граммированию мы поговорим о процедуре получения денег за свой программ-
ный продукт. Пусть перспектива возможного извлечения прибыли послужит для
вас хорошим стимулом к скорейшему обучению!
Книга содержит 15 глав, каждая из которых обозревает отдельную тему. Первые
две главы практически не касаются собственно программирования, но содержат
подробную вводную информацию о том, как начать разработки, какое оборудова-
ние и программное обеспечение вам понадобится для этого. Это базовая инфор-
мация, основа, без которой дальнейшее программирование невозможно.
Главы, касающиеся программирования, содержат теоретическую и практическую
части. В теоретической части обсуждаются классы и методы, реализующие воз-
----------------------- Page 10-----------------------
К читателям 1 1
можности платформы, а в практической подробно рассмотрена реализация при-
меров, использующих описанные выше классы. Большинство примеров перехо-
дят из главы в главу, расширяя свою функциональность, поэтому рекомендуется
не прогуливать уроки, а аккуратно выполнять все самостоятельные задания.
В тексте глав примеры приведены не полностью, а только та их часть, которая
реализует новую функциональность. Если вы все же запутаетесь, то полный лис-
тинг некоторых примеров с подробными комментариями вы найдете в составе
приложений в конце книги.
Далее приведен список всех глав с их кратким описанием.
Глава 1. С чего начинать? Обзор необходимых средств для мобильного програм-
мирования: модели телефонов, поддерживающие J2ME; коммуникационные ка-
бели; необходимое программное обеспечение; студии разработки; эмуляторы; заг-
рузка приложений в телефон.
Глава 2. Первое приложение «Hello, World!» Описание создания первой програм-
мы в студии разработки J2ME Wireless Toolkit. Структура программы, компиля-
ция, тестирование программы на эмуляторе.
Глава 3. Пользовательский интерфейс высокого уровня. Иерархия класса Screen,
отображение формы и взаимодействие с ней. Классы Display, Screen, Form, Command.
Пример программы фотоальбома («Slide Show»).
Глава 4. Работа с графикой. Классы Canvas, Graphics. Общая организация графи-
ки, рисование простых геометрических фигур. Разбор примера программы управ-
ления точкой, рисующей фигуры на экране («Float Point»).
Глава 5. Структуры и форматы данных в J2ME. Типы данных. Классы Vector, Stack.
Работа с данными. Расширение программы «Float Point» до игры «Змейка».
Глава 6. Организация многопоточных приложений, параллельное программиро-
вание. Класс Thread. Вопросы синхронизации. Расширение программы «Змейка»:
пример приобретает вид законченной, качественной игры.
Глава 7. Сохранение данных приложения, сохранение параметров программ (RMI,
класс RecordStore). Расширение «Змейки», добавление запоминания настроек и
таблицы рекордов.
Глава 8. Стандартные средства пользовательского интерфейса, организация меню.
Классы List, ChoiceGroup. Управляющие сигналы высокого уровня. Реализация
меню для «Змейки».
Глава 9. Организация потоков, работа с файлами. Класс InputStream. Вопросы ло-
кализации, проблема корректного отображения русского текста. Обзор програм-
мы «Book Reader». Класс Font.
Глава 10. Ввод текста, работа со строками. Классы TextBox, TextField. Средства
ввода и редактирования текста. Разбор программы «AddressBook», реализующей
записную книжку.
Глава 11. Работа со временем, датой и календарем. Классы Date, Calendar. Извле-
чение внутренней даты и времени телефона. Расширение записной книжки до
«Birthday Reminder».
Глава 12. Работа над ошибками. Классы Al ert, Error, Exception. Обработка исклю-
чительных ситуаций, процесс отладки программ.
----------------------- Page 11-----------------------
12 Предисловие
Глава 13. Расширение функциональности MIDP 1.0 с помощью API фирмы-про-
изводителя мобильного телефона. Nokia API, Samsung API, Siemens API. Работа
со звуком, дополнительные возможности.
Глава 14. Методы передачи и приема данных. Классы Connector, Connection,
OatagramConnecti on, StreamConnecti on, ContentConnecti on, HttpConnecti on. Реализация
и разбор примеров интернет-приложений.
Глава 15. Защита приложений (обфускация). Обзор декомпиляторов: просмотр,
редактирование и усовершенствование чужих приложений.
Bonus. Распространение мобильных приложений. Практические советы по про-
дажам мобильного контента.
Приложение 1. Полный листинг программы «Змейка».
Приложение 2. Полный листинг программы «Book Reader».
Приложение 3. Полный листинг программы «Birthday Reminder».
Весь этот путь он начала до конца я пройду вместе с вами и реализую все описан-
ные примеры. В процессе реализации и тестирования примеров я буду приводить
скриншоты, чтобы никто не усомнился в успешном тестировании. То есть все ре-
ализовано, все проверено, все работает, чего и вам желаю. А теперь закончим всту-
пительную часть словами классика. За мной, читатель!
От издательства
Ваши замечания, предложения и вопросы отправляйте по адресу электронной
почты comp@piter.com (издательство «Питер», компьютерная редакция).
Мы будем рады узнать ваше мнение!
Все исходные тексты, приведенные в книге, вы можете найти по адресу http://
www.piter.com/download.
Подробную информацию о наших книгах вы найдете на веб-сайте издательства:
http://www.piter.com.
История языка Java
По сравнению со своими старшими братьями в большой семье языков програм-
мирования, Java — совсем еще ребенок, причем едва достигший десятилетнего
возраста. Но посмотрите, какой это ребенок! Давайте вместе оглянемся на первые
дни этого малыша.
Родители вундеркинда работали в компании Sun Microsystems, которая в начале
90-х годов стояла перед задачей создания передового программного обеспечения
для различных бытовых приборов, грубо говоря — для чайников. Основная про-
блема состояла в том, что для каждой новой модели бытовой техники существо-
вали свои технические стандарты.
Ведущему инженеру проекта Патрику Нотону приходилось поддерживать в раз-
работке сотни интерфейсов, пока это ему окончательно не надоело. В один пре-
----------------------- Page 12-----------------------
Иаория языка Java 13
красный день он решил уволиться ко всем чертям и понес заявление директору.
Нужно отдать должное дальновидному руководству в лице Скотта МакНили,
который попросил изложить причины ухода в подробном письме. Тут уж Патрик
Нотон оторвался от души и написал все, что он думает о компании и о ее техноло-
гиях, беспощадно раскритиковав недостатки Sun Microsystems.
Письмо возымело на удивление большой успех как у ведущих инженеров, так
и у высшего руководства компании, а именно у Билла Джоя, основателя Sun
Microsystems, и Джеймса Гослинга, непосредственного начальника Нотона.
В день планируемого увольнения Патрик Нотон получает зеленый свет на реа-
лизацию всех своих идей и собственную группу ведущих разработчиков под ко-
довым названием Green, чтобы они делали что угодно, но создали нечто нео-
быкновенное.
Команда приступила к разработке нового объектно-ориентированного языка про-
граммирования, который должен был стать ведущим на рынке бытовой электро-
ники. Вскоре Нотон предложил использовать новые наработанные технологии
для интернет-приложений, в связи с чем были написаны компилятор Java и брау-
зер Hotjava. 23 мая 1995 года компания Sun официально представила Java
и Hotjava на выставке SunWorld'95. Именно тогда наш «малыш» сделал первый
шаг к вашему мобильнику.
Платформа Java 2 Micro Edition (J2ME) была разработана уже ближе к нашим
дням для устройств с ограниченными ресурсами памяти и процессора, таких как
сотовые телефоны, пейджеры, смарт-карты, органайзеры и миникомпьютеры.
J2ME позволяет запускать Java-приложения на ресурсо-ограниченных вычисли-
тельных устройствах. Для данных целей J2ME адаптирует существующую Java-
технологию.
----------------------- Page 13-----------------------
Глава 1
С чего начинать?
Эта глава содержит обзор необходимых технических средств для программиро-
вания: моделей телефонов, поддерживающих J2ME, коммуникационных кабелей,
программного обеспечения, студий разработки, эмуляторов.
Перед тем как перейти непосредственно к программированию, рассмотрим все,
что может пригодиться нам в процессе разработки приложений для мобильных
телефонов. Подавляющее большинство людей обычно пропускают введение
в любой книжке, которая попадает к ним в руки. Если вы относитесь к этой ка-
тегории, то ничего страшного не произошло: информация там была чисто по-
знавательного характера. Но начиная с этой главы, приготовьтесь к серьезной
работе, поскольку программирование — это такая наука, которую нельзя вы-
учить в теории.
Нужно отметить, что сейчас начинается самый главный этап в нашем обучении.
Не сделав всего, что написано в первой главе, дальше можете и не продолжать
читать этот самоучитель, поскольку его чтение без реальной практики — пустая
трата времени.
В любом самоучителе на любую тему есть упражнения, которые необходимо вы-
полнять. Ваши упражнения — это реализация примеров, описанных в книге, а без
первого этапа — поиска и установки всего необходимого оборудования и программ-
ного обеспечения — эти упражнения выполнять просто невозможно. Помните:
чем строже учитель, тем большего результата он добьется. Запаситесь терпением
и настойчивостью и не делайте себе поблажек.
Оборудование
Итак, начинаем! Первое и самое необходимое в нашем деле — это, конечно же,
сотовый телефон с поддержкой языка Java. Тот, кто уже давно подключил свой
телефон к компьютеру и успешно качает на него мелодии, картинки и Java-при-
ложения, может смело пропустить вводную часть вплоть до программного обес-
печения. Ну а те, у кого уже установлено и все описанное далее программное обес-
печение, могут задуматься о том, не следует ли им отдать предпочтение более
серьезной литературе.
Далеко не все телефоны поддерживают Java-приложения, и если вы не уверены
относительно нужной вам модели, то стоит заглянуть в документацию телефона
или на сайт разработчиков developers.sun.com/techtopics/mobility/device с полным
списком моделей телефонов, поддерживающих Java-технологию.
----------------------- Page 14-----------------------
Оборудование 15
Мобильные устройства не содержат собственных средств разработки — редакто-
ров, компиляторов и прочих инструментов, да и не нужно это аппарату с двадца-
тью кнопками и однодюймовым экранчиком. Поэтому придется прибегнуть к меж-
платформенной разработке, то есть программировать на компьютере, а только
потом переносить готовую программу на телефон. Таким образом, наличие ком-
пьютера — такое же необходимое условие в разработке мобильных приложений,
как и наличие телефона. Надеюсь, что на компьютере у вас установлена операци-
онная система Windows (которую ругают все кому не лень, но тем не менее про-
должают ею пользоваться), поскольку все рассмотренное далее программное обес-
печение предназначено именно для этой системы.
Связать компьютер с телефоном можно несколькими способами. Мы рассмотрим
наиболее простой и наименее дорогой способ связи через дата-кабель. Бывает,
что дата-кабель идет в комплектации с телефоном, считайте тогда, что вам повез-
ло. Если нет, то придется купить его отдельно, выложив за него от 15 до ,20 долла-
ров. Покупайте кабель только для своей модели телефона (обычно совместимые
модели указаны на упаковке). Даже если разъем кабеля подходит к вашему теле-
фону, то это еще ни о чем не говорит. В комплектации кабеля должен быть диск,
содержащий драйвера для этого кабеля.
Для начала следует установить драйвер для вашего кабеля. Большинство совре-
менных кабелей подключаются к USB-порту компьютера и при включении будут
обнаружены системой как новое USB-устройство. Вслед за обнаружением нового
устройства автоматически запустится мастер обнаружения нового оборудования,
который помогает установить драйвер устройства (рис. 1.1).
Найден новый мастер оборудования
Установка драйверов оборудования
Драйвер • это программа, позволяющая устройству взаимодействовать с
операционной системой.
ШШШжшш
Этот мастер заверши установку для устройстьа
Драйвером называется программа, ооеспечиваюшая работу устройства в
операционной системе Для любого нового устройства требуется установить
драйвер пая Window:. Чтобы найти Файлы драйверов и завершить установку.
r-iwwfji!!*, дей.-iet*. к-.-тог.-ч* . 'rfiiy i ылючнип, i.iac-iepi)
•••• Йровести поип- под"эдд«иего"араймра для'устранстьа (рекомендуется].]
С Довести слиси всех известных драйверов для зтсго устройства, из
"< Назад;:-: j -;У; Даяее>;.||:,- ^'-.Отмена j ;
:
Рис. 1.1 . Мастер оборудования поможет установить нужный драйвер
Вставьте диск с драйверами для кабеля, выберите в окне мастера переключатель
Провести поиск подходящего драйвера для устройства, на следующем этапе мастера
----------------------- Page 15-----------------------
16 Глава 1 . С чего начинать?
выберите пункт размещение будет указано, а на следующем шаге укажите на диске
драйвер для вашей операционной системы. Если поиск нужного драйвера на дис-
ке вызовет у вас затруднения, то можно вернуться к предыдущему этапу мастера
и выбрать пункт дисководы компакт-дисков, тогда система выберет подходящий
драйвер с диска автоматически.
Далее система установит драйвер и уведомит вас о том, что установка программ-
ного обеспечения для нового устройства завершена, указав его название, к приме-
ру, Prolific USB-to-Serial Comm Port. Жмите кнопку Готово.
Теперь можно посмотреть результат установки устройства. Щелкните правой
кнопкой мыши на значке Мой компьютер на рабочем столе и выберите в контекст-
ном меню самый нижний пункт — Свойства. Перейдите на вкладку Оборудование,
запустите диспетчер устройств, нажав соответствующую кнопку, и проверьте
пункт Порты СОМ и LPT. Здесь должно появиться новое устройство, а в скобках —
номер СОМ-порта, с которым оно связано (рис. 1.2). Запомните этот номер: он
вам пригодится для настройки программы связи компьютера с телефоном.
В-Щ KMILLER
ф Ш DVD и компакт-дисководы
$••§ Видеоадаптеры
Ш - в Дисководы гибких дисков
ф 0 Дисковые устройства
^ Другие устройства
Звуковые, видео и игровые устройства
Ш iSg Клавиатуры
ф HJ Компьютер
ф- в Контроллеры IDE ATA/ATAPI
ffi © Контроллеры гибких дисков
Контроллеры универсальной последовательной шины USB
__ Мониторы
Й ^ Мыши и иные указывающие устройства
&J? Порты СОМ и LPT
Jjf Порт принтера ЕСР (LPT1)
• Jjf Последовательный порт (СОМ1)
••Jjf Последовательный порт (COM2)
Сетевые платы
Системные устройства
ш-,
Ш - Ш Тома хранения данных
Рис. 1.2. В списке устройств появился дата-кабель
Если вы видите новое устройство, значит установка прошла успешно. На самом
деле, дата-кабель может дать неограниченные возможности по управлению вашим
телефоном, от закачки новых игр, программ, мелодий и картинок до обеспечения
компьютеру выхода в Интернет (технология GPRS) и прошивки исполняющей
системы телефона. Кстати, с прошивками будьте аккуратны и всегда отдавайте себе
отчет в том, что вы делаете — хотя бы для того, чтобы специалист, в случае чего,
смог вам помочь.
----------------------- Page 16-----------------------
Оборудование 17
Программы связи компьютера с телефоном не входят в комплект дата-кабеля,
обычно они содержатся на диске, который идет в комплектации самого телефона.
Если по каким-то причинам программа для закачки приложений не обнаружива-
ется на диске или отсутствует сам диск, то придется искать программу, совмести-
мую с вашей моделью телефона, в дебрях Интернета. Вооружитесь терпением:
если у вашей модели телефона есть возможность закачки, значит должна быть
и программа. Кто ищет, тот... сами знаете — что.
Для кого-то более легким покажется другой путь: зайти в киоск с программным
обеспечением и купить себе один из многочисленных дисков типа «Все програм-
мы для телефона...» (нужное вписать). К сожалению, в рамках данной главы мы
не сможем рассмотреть программы для всех моделей телефонов, поэтому для
примера ограничимся одной из подобных программ для аппаратов Nokia 7210,
3510i, 6100, 6610. Программа связи компьютера с телефоном, которую мы рас-
смотрим, называется Mobile Media Browser (MobiMB), ее можно скачать по ад-
ресу www.logomanager.co.uk, однако там она стоит денег, поэтому будем рассмат-
ривать ее только в качестве наглядного пособия для всех программ такого рода,
поскольку все они обладают схожими настройками и управлением.
Для настройки программы связи в меню Connection settings (Параметры соедине-
ния) следует добавить новое соединение с помощью команды Add connection (До-
бавить соединение). Вам будет предложено на выбор несколько типов кабелей, из
которых вы должны выбрать тип своего кабеля, указанный на упаковке (типы
кабелей могут быть, например, такими: FBUS, DLR-3, DKU-5). В свойствах ново-
го соединения следует также указать номер СОМ-порта, с которым операцион-
ная система связала наш кабель на этапе его установки.
Теперь подключаем телефон к компьютеру с помощью дата-кабеля, в программе
MobiMB, может быть, потребуется нажать кнопку Refresh (Обновить), чтобы уско-
рить процесс определения телефона программой. После того как телефон будет
обнаружен и определен, в левом окне программы появится изображение телефона
со списком, содержащим имена нескольких папок (рис. 1.3). Если на этом этапе
соединение не устанавливается, попробуйте подключить кабель к другому USB-
порту, проверьте соответствие кабеля вашей модели телефона, убедитесь, что ка-
бель установлен в операционной системе, а соединение настроено правильно.
[ I f MobiMB Mobile Media Browser
File E * tfew Help
'P] % © & 0
В-^ Nokia6100 : 1 Stee l
Name й й ; ? ^ ; > \ : ; > : : ' . : ''••.:'.•.'
B - Q Gallery SFIoatPoint 41KB
1 j Q Images SelackCat 58 KB
' P3 Tones
63 KB
j i||j Applications
_ | Games
. ,„„„„
Рис. 1.З. Медиа-браузер дает доступ к памяти телефона
----------------------- Page 17-----------------------
18 Глава 1 . С чего начинать?
Мы видим, что программа успешно обнаружила аппарат и дала нам доступ к четы-
рем папкам телефона: Gatlery\Images, Gallery\Tones, Applications, Games, где хранятся
картинки, мелодии, Java-приложения и Java-игры соответственно. В нижней стро-
ке выводится объем свободной памяти телефона. Нас, конечно же, интересуют две
последние папки: именно там мы будем размещать созданные нами программы.
Немного о программах, которые мы будем разрабатывать. Приложение для мо-
бильного телефона состоит, как правило, из двух файлов: архива с расширением
.jar, содержащего байт-код исполняемой части приложения и его ресурсы, и фай-
ла-описателя с расширением .jad для первоначальной проверки совместимости
с устройством и настройки различных атрибутов. После того как связь с телефо-
ном установлена, с закачкой Java-приложения в телефон справится даже ребе-
нок: нужно выделить jar- Hjad-файлы и перетащить их мышью в папку Applications
или Games, после чего должен начаться процесс копирования.
Если копирование не началось, значит аппарат отвергает приложение, ориенти-
руясь на параметры, указанные в jad-файле описателя программы. Причин может
быть несколько: либо размер приложения превосходит максимально допустимый
(например, для аппаратов Nokia серии 40 максимально допустимый размер при-
ложения составляет 64 Кбайт), либо недостаточно свободной памяти, либо дан-
ные в файле описателя некорректны.
Если приложение было успешно скопировано, то можно отсоединять телефон от
провода, заходить в раздел, куда вы скопировали приложение, и запускать вашу
программу, которая теперь не зависит от средств разработки, устройств связи
и дополнительного оборудования. Теперь приложение всегда у вас в кармане
и вы можете воспользоваться им в любой момент.
На этом обзор необходимого оборудования и его настройки закончен. Следу-
ющий этап — поиск и установка средств разработки мобильных Java-приложе-
ний. На этом этапе вам желательно иметь доступ в Интернет, поскольку именно
там мы будем черпать необходимые программные ресурсы. Рассчитывайте на то,
что в общей сложности вам придется скачать около 100 Мбайт. Если это непри-
емлемо, то вам прямая дорога в интернет-кафе или крупный магазин с програм-
мным обеспечением.
Программное обеспечение
Теперь рассмотрим необходимый минимум программного обеспечения, кото-
рый должен быть установлен на вашем компьютере, чтобы перейти непосред-
ственно к программированию. Сразу оговорюсь, что существует масса средств
разработки, программ, студий, интегрируемых сред, и мы разберем лишь самый
простой вариант программного обеспечения, своего рода необходимый «канди-
датский минимум», без которого говорить дальше просто не о чем. Настоятель-
но рекомендую устанавливать именно то, что я скажу, а не то, что под руку по-
падется: так всем будет проще, поскольку на первом этапе объяснения я буду
производить на пальцах — в стиле «нажми то, нажми это». Скажу также, что все
программное обеспечение распространяется компанией Sun Microsystems абсо-
лютно безвозмездно, то есть даром, поэтому, в худшем случае, вам придется пла-
тить лишь за трафик.
----------------------- Page 18-----------------------
Программное обеспечение 19
Технология языка Java связана с тем, что для запуска приложений требуется
виртуальная машина Java. Наша операционная система, не буду лишний раз го-
ворить какая, содержит свою версию виртуальной машины, которая, мягко го-
воря, нам не подходит, поэтому следует раздобыть и установить исполняющую
среду разработчика JavaTM 2 SDK, Standard Edition (J2SE SDK), version 1.4.2.
Скачивается эта штуковина с сайта компании Sun Microsystems по адресу
Java.sun.com/j2se/1.4.2/download.html, «весит» 52 Мбайт и устанавливается без
особых проблем: когда попросят, смело жмите Next и соглашайтесь на все пред-
ложенные условия. На всякий случай запомните, куда вы установили исполня-
ющую среду, поскольку в дальнейшем какие-нибудь ленивые программы могут
вас об этом спросить.
Второе, что нам понадобится, — это набор средств для разработки Java-приложе-
ний Sun Java Wireless Toolkit (J2ME WTK). На данный момент на том же сайте
разработчиков по адресу java.sun.com/products/sjwtoolkit/ доступна версия WTK 2.3
Beta размером двадцать с небольшим мегабайт. Я же пользовался чуть ли не первой
версией этого набора, которой оказалось вполне достаточно. Сам набор содержит
все необходимые нам средства разработки: компилятор, упаковщик, несколько стан-
дартных эмуляторов телефонов и еще некоторые полезные утилиты.
При установке WTK выполняет поиск виртуальной машины Java, уже нами ус-
тановленной, и уточняет ее местоположение. Важно: устанавливать программу
следует в папку, полный путь к которой не содержит пробелов и русских симво-
лов, поскольку в дальнейшем это может привести к проблемам. На все вопросы
в процессе установки следует отвечать утвердительно.
Основная утилита набора, которой мы будем пользоваться на протяжении всей
книги, называется KToolbar (рис. 1.4).
I H J2ME Wireless Toolkit
File Edit Help
I ^ New Project... | g :j * i Project Й • л Rs.;;- jj ^ gea r Console I
;
: Device 1 . ••..:. d
Create a new p r o j e c t or open an existing one
Рис. 1.4. Основное окно утилиты Ktoolbar
Это и есть, собственно, наше окно — если не в Европу, то в большой и увлекатель-
ный мир мобильного программирования: необходимый минимум функциональ-
ности, удобная навигация, в которой при всем желании сложно запутаться.
На этом «обязательные программы», без которых просто нельзя разработать мо-
бильное приложение, заканчиваются, и начинаются «произвольные программы»,
которые не обязательны, но желательны.
----------------------- Page 19-----------------------
20 Глава 1 . С чего начинать?
Эмуляторы
При разработке приложений нам неоднократно потребуется их тестировать, что-
бы вовремя выявить возможные ошибки и неточности. Можно, конечно, каждый
раз копировать программу на телефон, но, боюсь, это сделает процесс разработки
слишком трудоемким и утомительным. Поэтому в процессе отладки удобно вос-
пользоваться эмулятором телефона — программой, которая имитирует реальное
устройство на вашем компьютере. Эмуляторы предоставляют возможность запус-
ка, отладки и управления мобильным приложением, что позволит избежать мно-
гократного копирования и поможет предотвратить ошибки, способные повредить
содержимое памяти вашего мобильного устройства. Таким образом, весь цикл
разработки приложения переносится на компьютер, а в телефон загружается толь-
ко окончательная версия приложения.
Несколько эмуляторов содержатся в уже установленном нами наборе J2ME Wireless
Toolkit. В дальнейшем мы будем их использовать, однако они имеют ряд серьезных
недостатков. Во-первых, это эмуляторы «телефонов вообще», а не каких-то конк-
ретных моделей, поэтому они не смогут отразить всей специфики конкретного ап-
парата, начиная с размеров экрана и расположения функциональных клавиш и за-
канчивая поддержкой специальных функций от разработчиков определенной марки.
Во-вторых, эмуляторы встроены в J2ME WTK и не позволяют запускать приложе-
ния, созданные не в этой среде разработки, то есть просто скопированные на компь-
ютер. При желании это ограничение можно обойти, но особого смысла я в этом не
вижу, поскольку существуют более продвинутые эмуляторы, которыми мы и вос-
пользуемся.
К более продвинутым относится эмулятор телефона Nokia 7210, которым можно
воспользоваться при тестировании приложений для любых моделей самой распро-
страненной серии 40 телефонов Nokia. Эмулятор входит в состав среды разработки
приложений Nokia 7210 MIDP SDK 1.0, который можно скачать бесплатно на сай-
те разработчиков www.forum.nokia.com/tools (размер дистрибутива — 18 Мбайт). При
установке пакета программа потребует серийный номер, который нужно отдельно
получить на том же сайте. Соглашайтесь на все условия и вводите серийный номер.
А теперь снова внимание: эмулятор нужно установить в папку wtklib\devices, нахо-
дящуюся внутри директории, где установлен J2ME Wireless Toolkit. Делать это сле-
дует для того, чтобы наш новый эмулятор автоматически добавился в список имити-
руемых устройств интерфейса J2ME WTK, а также чтобы компилятор самостоятельно
обнаружил специальные функции производителя.
После того как установка завершена, можно запускать эмулятор, который не толь-
ко выглядит презентабельнее стандартных эмуляторов от Sun Microsystems и по-
зволяет запускать Java-приложения, но и эмулирует все остальные функции теле-
фона Nokia: записная книжка, будильник и даже настройки — все по-настоящему.
Чтобы запустить приложение, нужно в меню File (Файл) выбрать команду Open
(Открыть), после чего указать путь к приложению, которое следует запустить. Те-
перь, прежде чем загружать себе в телефон все подряд, можно убедиться, что игра
или программа действительно того стоит. Этим эмулятором я и буду пользоваться
на протяжении всей книги (рис. 1.5).
Кроме собственно запуска приложений, с его помощью можно следить за трафи-
ком сетевых приложений и распределением выделяемой памяти в окне статуса
----------------------- Page 20-----------------------
Эмуляторы 2 1
выполнения приложения, которое можно открыть
через пункт меню Tools (Инструменты). Отметим
также, что без исполняющей среды J2SE SDK ус-
тановить эмулятор нельзя, поэтому не спешите,
а делайте все по порядку, не забегая вперед.
Мобильный вариант языка Java разрабатывал-
ся как универсальный, то есть совместимый со
всеми моделями телефонов, поддерживающи-
ми стандарт J2ME. Однако в каждом аппарате
могут быть свои нюансы: различия в размерах
экрана, ограничение на объем приложений
и другие подводные камни. Так что если вы со-
брались продавать ваш программный продукт,
будь то игра или просто полезное приложение,
то обязательно нужно протестировать его на
каждом аппарате из заявленного модельного
ряда. Мало ли что — а подрыв репутации мо-
жет быстро свести ваши заработки к нулю. Про-
верить нужно хотя бы на эмуляторах, однако
найти, скачать и установить эмуляторы всех мо-
делей с поддержкой Java — занятие, мягко го-
воря, кропотливое и требующее как минимум
железной нервной системы.
Здесь на помощь может прийти сетевая система
под названием Tira Developer Network от компа-
нии Tira Wireless, специализирующейся на раз-
работке мобильных приложений. Данная систе-
ма, доступная по адресу www.tiradeveloper.com,
создана для тестирования приложений на совме- Рис. 1.5. Так выглядит эмулятор
телефона с запущенной
стимость со всеми распространенными моделя- на нем игрой
ми телефонов. Для тестирования предлагается
около 150 мобильных устройств с поддержкой J2ME. В процессе проверки прило-
жение может пройти около 60 специальных тестов, способных проверить совмес-
тимость приложения со всеми распространенными стандартами.
Для пользования системой вам достаточно зарегистрироваться на сайте, выбрать
интересующее вас устройство, с помощью специальной формы закачать свое при-
ложение и получить вердикт о соответствии вашего продукта выбранному уст-
ройству. Удобно, не правда ли, тем более что платить за это никому не нужно.
Хотя немного смущает тот факт, что для проведения тестов приложения нужно
закачивать к ним на сайт. Поэтому, если вы категорически не желаете, чтобы ваше
приложение безвозмездно оказалось в чужих руках, то лишний раз подумайте,
надеяться на их порядочность или нет.
Итак, чтобы начать разработку приложений для мобильных телефонов, нам по-
требуется:
• телефон с поддержкой Java-технологии,
• компьютер с операционной системой Windows,
• кабель для связи компьютера с телефоном.
----------------------- Page 21-----------------------
22 Глава 1 . С чего начинать?
При этом на компьютере должно быть установлено следующее программное обес-
печение:
• драйвер кабеля,
• программа для закачки приложений в телефон,
• исполняющая среда Java-разработки J2SE SDK (version 1.4.2),
• набор средств для разработки J2ME WTK,
• эмулятор вашей модели телефона — желателен, но не обязателен.
Объем всего перечисленного программного обеспечения составляет не более
100 Мбайт.
' . ' , . • * * *
На этом самый важный подготовительный этап закончен. В следующей главе
мы перейдем непосредственно к программированию. Базовый набор программ
и оборудования, рассмотренный в этой главе, является той самой точкой опоры,
без которой никакие рычаги нам не помогут. Я понимаю, что вам уже не терпит-
ся перейти к самому интересному, как в детективе — заглянуть в самый конец,
но я не рекомендую этого делать, не усвоив всего, что написано в этой главе.
Дальше вас ждет масса интересных примеров приложений. Реализуйте их, со-
вершенствуйте, оптимизируйте — и помните, что не ошибается только тот, кто
ничего не делает. Добро пожаловать в современный мир программирования для
мобильных устройств!
----------------------- Page 22-----------------------
Глава 2
Hello, World!
В этой главе мы рассмотрим структуру программы для мобильного телефона,
а также все этапы ее создания и загрузки в телефон. Будем считать, что все необ-
ходимые провода уже приобретены и подключены к телефону, который поддер-
живает платформу J2ME, а соответствующее программное обеспечение успешно
установлено на вашем компьютере.
Для создания полноценного мобильного приложения нам придется выполнить
следующие этапы разработки:
• написание кода (текста) программы на языке J2ME;
• компилирование программы;
• тестирование приложения на эмуляторах;
• упаковка приложения в jar-архив;
• загрузка приложения в мобильный телефон.
Не будем спешить и для начала выясним, что же представляет собой приложение
для мобильного телефона и что у него внутри.
Класс MIDIet
Поскольку Java 2 Micro Edition поддерживает большое количество разных уст-
ройств, то существуют различные конфигурации этой платформы. Не вдаваясь
в подробности, мы будем использовать конфигурацию CDLC (Connected Limited
Device Configuration), расширенную профайлом MIDP (Mobile Information Device
Profile), который обеспечивает необходимую функциональность для программи-
рования под мобильные телефоны, пользовательский интерфейс, постоянное хра-
нение данных, таймеры, средства сетевой связи. Мидлет (MIDIet) — это и есть
приложение Java, которое использует профайл MIDP и конфигурацию CLDC.
Название «мидлет» происходит от имени основного класса мобильного прило-
жения javax. mi croedi ti on. mi dl et. MIDI et. Основной класс приложения должен быть
унаследован от абстрактного класса MIDIet и должен реализовывать методы
startAppC), pauseApp() и destroyApp(boolean). К сожалению, основы объектно-ори-
ентированных языков остаются за рамками этой книги, поэтому те, кто считает,
что ООП — это Организация Освобождения Палестины, могут на этом этапе
почитать дополнительную литературу. Особо нетерпеливые могут сразу перей-
ти к водным процедурам и подхватывать идеологию и терминологию языка на
ходу, аккуратно разбирая приведенные в дальнейшем примеры программ.
----------------------- Page 23-----------------------
24 Глава 2. Hello, World!
Реализация MIDP дает программисту много возможностей и большую свободу
действий, однако и эти возможности не безграничны, поэтому начнем с ложки
дегтя — той головы, выше которой не прыгнуть в классической реализации язы-
ка. MIDP не поддерживает системного программирования, не дает доступа к сис-
темным функциям устройства и самой исполняющей системе. В компетенцию
MIDP не входит управление пользовательскими приложениями и обеспечение
средств для загрузки, хранения или запуска приложений.
При запуске загруженного в телефон приложения события развиваются следу-
ющим образом. Управляющая система создает экземпляр основного класса, на-
следованного от класса MIDI et, используя конструктор без аргументов, доставшийся
в наследство от базового класса. Приложение переходит в состояние PAUSED, после
чего система вызывает метод startApp(), который и будет являться для нас точкой
входа в программу. «А как же mainO?», — спросят опытные Java-программисты.
Метода main() мы не видим, но он есть! Это часть реализации МШР-профайла,
и программное обеспечение телефона вызывает его без нашего ведома.
После вызова метода startAppC) приложение переходит в состояние ACTIVE, захва-
тывает необходимые ресурсы и начинает заниматься своими делами. Приложе-
ние выполняется до тех пор, пока система не переведет его в одно из состояний —
PAUSED или DESTROYED. Три упомянутых состояния и составляют жизненный цикл
приложения. Остановимся на каждом из них.
• ACTIVE — активное состояние. Приложение готово к запуску или уже вы-
полняется. Система может приостановить выполнение приложения, выз-
вав метод pauseAppO, или совсем прервать выполнение с помощью метода
destroyApp(boolean) .
• PAUSE — приостановленное состояние. Приложение прекращает выполнение
задач и освобождает часть ресурсов. Выполнение приложения может быть во-
зобновлено системой с помощью метода startAppC) или полностью остановле-
но методом destroyApp(boolean).
• DESTROYED — прерванное состояние. Приложение остановлено и уже не может
перейти в другие состояния. Все используемые ресурсы освобождаются.
«Жизненный цикл» приложения может быть представлен схемой, показанной на
рис. 2.1.
Система управляет сменой состояний приложения с помощью абстрактных ме-
тодов класса MIDlet, обязательных к реализации. Кроме того, класс MIDI et предо-
ставляет нам еще несколько методов для контроля над состоянием приложения
и получения информации о нем:
• abstract void startAppC) — вызывается системой при переходе мидлета в активное
состояние. Метод может быть вызван только из приостановленного состояния;
• abstract void pauseApp() — вызывается системой при переходе мидлета в при-
остановленное состояние. Приложение должно освободить совместно исполь-
зуемые ресурсы;
• abstract void destroyApp(boolean unconditional) — вызывается при переходе мид-
лета в прерванное состояние. Приложение должно освободить все ресурсы
и сохранить все необходимые данные в постоянном хранилище. Если аргумент
uncondi t i ona 1 задает значение true, то приложение будет прервано в любом слу-
----------------------- Page 24-----------------------
Создание мидлета 25
чае. Если передан аргумент f a I se, то система может отказаться переводить при-
ложение в прерванное состояние и сформировать исключение MIDI etStateChange
Exception;
new О
destroyAppO
Рис. 2.1 . «Жизненный цикл» приложения
void resumeRequest() — используется для оповещения системы о желании прило-
жения перейти в активное состояние, когда система будет готова перевести мид-
лет в активное состояние, она сама вызовет метод sta rtApp(). Метод resumeRequest ()
вызывается приложением в приостановленном состоянии, в котором поддер-
живается обработка таймера;
void noti fyPaused() — приложение информирует систему о переходе в приоста-
новленное состояние. Метод не будет иметь эффекта, если приложение еще не
запущено или уже прервано;
void noti fyDestroyed() — мидлет информирует систему о переходе в прерванное
состояние. С этого момента система будет считать все ресурсы свободными;
Stri ng getAppProperty (Stri ng key) — возвращает параметр приложения, хранящийся
в файле описателя мидлета, в виде строки. Аргумент key задает имя необходимо-
го параметра. Если запрашиваемый параметр не задан, то возвращается значе-
ние nul 1. Имена параметров мы рассмотрим позже вместе со структурой файла
описателя.
Создание мидлета
Итак, для создания мидлета остановимся на одном из перечисленных в первой
главе средств разработки J2ME Wireless Toolkit (WTK), с помощью которого
реализованы все приведенные в этой книге примеры. После установки Wireless
Toolkit запускаем его основную утилиту KToolbar. Доступные после запуска дей-
ствия — это NewProject (Новый проект) и OpenProject (Открыть проект). Можно
----------------------- Page 25-----------------------
26 Глава 2 . Hello, World!
открыть демонстрационные проекты и на готовых примерах рассмотреть воз-
можности и особенности языка, но мы создадим новый проект и назовем его
MyFirstMidlet. Так же назовем и основной класс мидлета (MIDlet Class Name)
в окне создания нового проекта (рис. 2.2).
Help
& New Project P ? Open Project . j . • I . Consul?
}
Create a new project or open an existing one
jENew Project
Pioiect Nan* JMyFirstMidlet
MIDlet Class Name JMyFirstMidlet !
CteateProiect | Cencel
Рис. 2.2 . Окно создания нового проекта
После нажатия кнопки Create Project (Создать проект) появится диалоговое окно
ввода информации для файла — описателя приложения (рис. 2.3). Диалоговое окно
состоит из нескольких вкладок, первая из которых — Required (Обязательные) —
содержит обязательные атрибуты файла-описателя:
• MIDlet-Jar-Size — размер jar-файла в байтах. Обратите внимание, что данный
атрибут прописывается в файле описателя автоматически средствами WTK
при упаковке проекта. Будьте аккуратны при ручном редактировании jar-ар-
хива: несовпадение заявленного и реального размеров jar-файла послужит при-
чиной ошибки при загрузке приложения в телефон;
SffiSettings for project "MyFirstMidtet" xj
ч|' ed | Optional j User Defined | MIDtets |
Key Value
MIDlet-Jar-Size 1100
MIDIet-Jar-URL iMyRrstMidletjar
MIDIet-Name JMyFirstMidiet
MIDIet-yersion
rticroEdition-Configuration JCLDCTF
MicroEdition-Profile IMIDP-1.0
OK Cancel
Рис. 2.З . Определение обязательных атрибутов приложения
----------------------- Page 26-----------------------
Создание мидлета 2 7
• MIDlet-Jar-URL — сетевой адрес и название jar-файла. В нашем случае это поле
содержит лишь имя jar-файла;
• MIDlet-Name — имя мидлета (или комплекта мидлетов), которое будет показа-
но пользователю в меню запуска приложения;
• MIDlet-Vendor — разработчик приложения (организация или частное лицо);
• MIDLet-Version — версия мидлета в формате <major>.<minor>.<micro>; может
быть использована при установке и обновлении приложения;
• MicroEdition-Configuration — требуемая конфигурация J2ME для выполнения
мидлета. В рамках нашей книги используется конфигурация CDLC-1.0;
• MicroEdition-ProfiLe — требуемый профайл J2ME для выполнения мидлета. Здесь
мы рассматриваем работу с профайлом MIDP-1.0.
Поля ввода значений необязательных атрибутов расположены на вкладке Optional
(Факультативные):
• MIDlet-Data-Size — минимальное количество байт данных для постоянного хра-
нения, требуемое мидлетом. Значение поля по умолчанию — ноль;
• MIDlet-Delete-Confirm — определяет, должна ли система запрашивать подтверж-
дение пользователя при удалении приложения из устройства;
• MIDlet-Description — описание мидлета или набора мидлетов;
• MIDlet-Icon — имя графического файла .png в jar-архиве, который будет исполь-
зован системой для данного мидлета в меню запуска приложений;
• MIDlet-Info-URL — сетевой адрес с описанием приложения или информацией
о разработчике;
• MIDlet-Install-Notify — указывает, должна ли система уведомлять пользователя
перед установкой нового мидлета.
Разработчик также имеет возможность определить собственные атрибуты прило-
жения на вкладке User Defined (Определенные пользователем). Имена атрибутов
не должны начинаться с префикса MIDlet- и могут быть получены во время вы-
полнения приложения с помощью метода getAppProperty (String key) класса MIDI et.
После того как все необходимые атрибуты указаны, нажимаем кнопку О К и полу-
чаем сообщение о том, что проект был успешно создан. Разберемся, что в данный
момент произошло. Для этого перейдем в папку, где установлен Wireless Toolkit.
В каталоге проектов apps появилась новая папка с именем нашего проекта
MyFirstMidlet. В корневом каталоге нашего проекта были автоматически созданы
еще несколько папок. Рассмотрим их назначение:
• /bin — папка для хранения файлов приложения: jar-архив приложения, файл
описателя приложения .jad и файл манифеста MANIFEST.MF. Автоматически соз-
данные файлы MyFirstMidlet.jad и MANIFEST.MF содержат указанные при созда-
нии проекта атрибуты;
• /res — файлы ресурсов, используемые приложением во время выполнения.
Файлы могут содержать текстовые или бинарные данные, картинки в формате
PNG (Portable Network Graphics);
• /src — исходные файлы приложения, содержащие собственно код программы.
Файлы должны иметь расширение .Java.
----------------------- Page 27-----------------------
28 Глава 2 . Hello, World!
Пришло время перейти непосредственно к программированию, уверен, что вам
уже надоело читать это скучное введение и уже не терпится перейти со своим
телефоном «на ты». К сожалению, среда Wireless Toolkit не содержит текстового
редактора, поэтому нам потребуется любой другой редактор, на худой конец обыч-
ный блокнот. Достаточно простые и удобные в обращении, с моей точки зрения,
такие редакторы, как UltraEdit или EditPlus, хотя опять же это дело вкуса, дан-
ный вопрос не принципиален, можете пользоваться и блокнотом.
Импорт классов и пакетов
Начнем с того, что классы, предоставляемые нам реализацией J2ME, объединены
в пакеты, представляющие группу классов определенной тематики. Для компи-
лятора имя класса составляют название класса и полное название пакета, где он
содержится. Такое имя может быть достаточно длинным, поэтому можно импор-
тировать класс с помощью оператора import, а в дальнейшем пользоваться лишь
названием класса. Синтаксис оператора import достаточно простой:
import <имя пакета>.<имя класса>:
Таких операторов может быть сколько угодно. Они должны располагаться в са-
мом начале программы. Существует возможность импортировать сразу все клас-
сы, содержащиеся в пакете. Для этого в операторе import вместо определенного
имени класса следует указать символ *:
import <имя пакета>.*
Для знатоков языка C++ и основ объектно-ориентированного программирования
отметим, что import не является аналогом директивы include языка С, поскольку
никакие файлы во время импорта не подключаются. Реализация J2ME, которую
мы рассматриваем, включает в себя следующие пакеты, классы которых мы бу-
дем рассматривать подробно на протяжении всей книги:
• Java. i о — классы работы с потоками ввода и вывода данных;
• Java. 1 ang — классы типов данных языка J2ME, а также некоторые системные
классы;
• Java ,'utii — классы дополнительных утилит, реализующие структуры данных,
а также работу со временем;
• javax,microedition.io — классы, обеспечивающие сетевые соединения;
• javax.microedition.Icdui — классы пользовательского интерфейса профайла
MIDP;
• javax.microedition.midlet — содержит единственный класс MIDIet, являющий-
ся основным классом мобильного приложения;
• javax. mi croedi ti on. rms — классы поддержки долговременного хранения данных.
Итак, первой строчкой нашей программы мы импортируем основной класс мо-
бильного приложения:
import javax.microedition.mi diet.MIDI et :
Далее реализуем свой собственный класс MyFi rstMi dl et, расширяющий класс MIDI et.
Как уже говорилось, обязательны к реализации три абстрактных метода класса
----------------------- Page 28-----------------------
Компиляция и запуск мидлета 2 9
MI Diet. Они могут быть и пустыми, но присутствовать в нашем классе обязаны.
В методе startAppO получим атрибут имени приложения с помощью метода
getAppProperty(String key) класса MIDI et и отобразим его области системных сооб-
щений вместе с сакраментальным «Hello, World!».
Область системных сообщений доступна нам с помощью поля out системного клас-
са System. Поле out является потоком вывода системной информации и поддержи-
вает вывод данных всех примитивных типов, а также строковых данных в область
системных сообщений с помощью методов print и println, которые различаются
лишь форматом вывода. Кроме этого, класс System содержит поле err, являюще-
еся потоком для вывода сообщений об ошибках, а также несколько методов для
доступа к системной информации. Как оформляются комментарии, вы без труда
догадаетесь сами. Таким образом, весь код нашей программы будет выглядеть
следующим образом:
import javax.microedition.midlet.MIDlet:
public class MyFirstMidlet extends MIDlet
{
public void pauseAppO {
}
public void destroyApp(boolean unconditional ) {
// уведомить систему о завершении
notifyDestroyedO;
}
public void startAppO {
// получить строку атрибута имени мидлета
String name - getAppPropertyC'MIDlet-Name") ;
// вывести сообщение в системную область
System.out.printlnC'MIDlet "+ name + " says : Hello . World!") :
Обратим внимание на то, что мы не импортировали классы String и System из па-
кета Java. I ang, но без особых проблем используем их в нашей программе. Все вер-
но. Дело в том, что пакет java. 1 ang просматривается компилятором всегда и в им-
порте не нуждается.
Компиляция и запуск мидлета
После того как весь код нашего первого приложения написан, сохраним получив-
шийся файл в папке исходных файлов приложения /src с именем MyfirstMidlet.java.
Заметим, что имя файла должно полностью совпадать с именем основного класса
приложения, иначе компилятор выдаст следующую ошибку: Class MyFirstMidlet is
public, should be declared in a file named MyFirstMidlet.java.
----------------------- Page 29-----------------------
30 Глава 2 . Hello, World!
Половина дела сделана, хотя пока что написанная программа является не более,
чем простым текстом, который теперь нужно преобразовать в настоящее мобиль-
ное приложение. Этим вопросом занимается компилятор — специальная програм-
ма, переводящая написанный нами текст в машинные команды, понимаемые уст-
ройством.
Для того чтобы откомпилировать наше приложение, следует выбрать команду Build
(Компилировать) на панели управления главного окна KToolbar. В основном окне
утилиты KToolbar мы увидим результат компиляции — количество ошибок в про-
грамме, местонахождение и описание каждой из них. Если все в порядке, то ото-
бразится сообщение о том, что компиляция прошла успешно (рис. 2.4).
Щ 32МЕ Wireless Toolkit - MyFirstMWIet
hi Mi l fi..) ' ' ii . , .
Clear Console
Device JDeiaultOrayPhone
Project settings saved
Building "MyFirstHidlet"
Build complete
Рис. 2.4 . Сообщение об успешном завершении компиляции
На этом этапе в папке нашего проекта было создано еще несколько директорий,
в одну из которых, /classes, и было помещено откомпилированное приложение,
представленное файлом MyFirstMidlet.class.
Сгенерированный компилятором файл уже является программой, готовой к ис-
полнению на эмуляторе телефона. Чтобы запустить написанное нами приложе-
ние, в списке Device (Аппарат) панели управления утилиты KToolbar нужно выб-
рать один из стандартных или установленных нами эмуляторов. Эмулятор вместе
с созданным нами приложением запускается с помощью команды Run (Запустить)
панели управления. По умолчанию приложение запустится в эмуляторе с много-
обещающим названием «DefaultGrayPhone».
Поскольку никаких особенных действий наша программа не выполняет, то при ее
запуске на эмуляторе ничего не произойдет. Однако, если все прошло успешно,
то мы увидим желаемый результат в области системных сообщений утилиты
KToolbar (рис. 2.5).
B j Z M L Wirrlrs» loulkit - MyFirstMidiet IBIfca. : . •: . • - : - . : i - i : : : : " ^ : • • • • • • • • • • • • . • • • . • . • . • . . . : : : : • • : • • . . . . . . . J.Ql.xI
•v New Project „ | f e cper, Project. , j ^Settuv.. .
, ..! ,-,M , - • • • ; • . . • ; - ; ; . :
HIDlet MyrirstMidlet says: Hello , World ! 1
^ L— - . . • . - ».,..•...•.
Рис. 2.5 . Результат работы программы «Hello World!»
----------------------- Page 30-----------------------
Упаковка мидлета 3 1
После закрытия эмулятора в область.системных сообщений выводится разнооб-
разная статистика. Оставим пока ее в покое, так как пользы сейчас нам от нее
никакой.
Упаковка мидлета
Заключительным этапом создания полноценного мобильного приложения, гото-
вого для загрузки в телефон, является упаковка созданных файлов в jar-архив,
который уже пригоден для загрузки в мобильный телефон. Для упаковки прило-
жения следует выбрать в меню Project (Проект) утилиты KToolbar команду Create
Package (Создать архив).
После упаковки в папке/bin нашего приложения появился файл MyfirstMidlet.jar.
Это и есть нужный нам jar-архив. Современные архиваторы типа WinZIP или
WinRAI^ поддерживают этот формат и позволяют нам заглянуть внутрь сформи-
рованного архива. Ничего нового там мы не увидим, только все тот же файл
MyFirstMidlet.class и файл манифеста в папке META-INF. В таком виде приложение
уже готово к загрузке в телефон с помощью средств, рассмотренных в предыду-
щей главе. Особого смысла загружать наше приложение в телефон нет, посколь-
ку никакой реакции аппарата мы не увидим, так что отложите эту процедуру до
следующей главы.
На этом процесс создания мобильного приложения завершен. Мы рассмотрели
все этапы, от написания текста программы до тестирования приложения на эму-
ляторе, упаковки и загрузки в телефон. Все эти этапы, кроме написания текста,
мы проделали с помощью среды разработки J2ME Wireless Toolkit.
Обратим внимание, что эмулятор, конечно, необходим в разработке приложений,
но далеко не достаточен. Как показывает практика, реальные аппараты могут
в любой момент «подложить нам свинью» в том месте, где ее совсем не ждешь.
Поэтому нужно как следует протестировать все функции программы на реальном
телефоне.
* * *
Итак, если у вас все получилось, то самое трудное уже позади. В программирова-
нии, как и в любом деле, главное — начать, а дальше будет только легче. Не реко-
мендую продолжать чтение книги, не выполнив первого задания, хотя это ваше
личное дело. В дальнейшем вас ждет еще масса интересных примеров с их под-
робным описанием. Не лишайте себя удовольствия, учитесь на чужих ошибках.
----------------------- Page 31-----------------------
Глава 3
Пользовательский
интерфейс высокого уровня
После того как мы познакомились с основной структурой мидлета, будем посте-
пенно продвигаться в наших познаниях. Java — язык объектно-ориентирован-
ный, поэтому, изучив все его объекты, мы сможем конструировать из них, как из
кубиков, большие многофункциональные приложения. Первым делом разберем-
ся с тем, как выводить информацию на экран телефона, чтобы демонстрировать
результат работы наших приложений. Выводимая информация может быть раз-
нообразной: текст, картинки, средства общения (интеракции) с пользователем.
Все объекты, которые можно отобразить на экране, наследуются из общего клас-
са, название которого говорит само за себя — javax. mi croedi ti on. 1 cdui. Di spl ayabl e.
Дальнейшая эволюция классов, наследуемых из класса Displayable, идет в двух
направлениях:
1. Класс Canvas предоставляет пользователю интерфейс низкого уровня, органи-
зует непосредственный доступ к прорисовке экрана. Подробнее на этом классе
мы остановимся в следующей главе.
2. Класс Screen является родителем для классов пользовательского интерфейса
высокого уровня. Это четыре класса: Alert, Form, Li st, TextBox, объекты которых
являются стандартными средствами связи с пользователем. Внешний вид та-
ких объектов зависит от конкретной модели телефона. Свобода действий над
этими объектами, изменение их вида, свойств и поведения ограничены функ-
циями реализации J2ME. Иерархию отображаемых объектов можно предста-
вить следующей схемой (рис. 3.1).
Alert Form List TextBox
Рис. 3 . 1 . Иерархия отображаемых объектов J2ME
----------------------- Page 32-----------------------
Класс Form 3 3
Класс Form
В этой главе мы рассмотрим работу с формой (объектом класса Form) на примере
программы фотоальбома. Форма является определенного рода контейнером, ко-
торый может содержать различные визуальные элементы, такие как текстовые
строки, поля символьного ввода, картинки, меню выбора, шкалы.
С точки зрения объектов, элементы, которые можно добавить в форму, должны
быть порождены одним из следующих классов: String, Image или Item. Элемент
класса Stri ng представляет собой статическую строку, автоматически отформа-
тированную согласно размерам экрана конкретного аппарата. Элемент класса Image
представляет собой картинку. Фактически, оба этих элемента дублируются объек-
тами классов, порожденных из класса Item.
Класс Item является базовым для целой группы элементов, которые могут быть
добавлены в форму: ChoiceGroup (меню выбора), DateField (поле ввода даты и вре-
мени), Gauge (шкала), Imageltem (изображение), Stringltem (статическая строка),
TextField (поле ввода).
Внутренняя реализация формы скрыта от программиста, мы можем контролиро-
вать лишь порядок отображения элементов. Расположение элементов на экране,
навигация между элементами, вертикальная прокрутка экрана при необходимости
контролируются внутренней реализацией и зависят от конкретной модели аппара-
та. Работа с формой осуществляется с помощью следующих методов класса Form:
• Form(String title) — конструктор; создает пустую форму с заголовком title ;
• FormCString title . Item[] items) — конструктор; создает форму с заголовком
title , содержащую элементы массива items;
• i nt append (Image i mg) — добавляет объект изображения i mg в форму, возвраща-
ет индекс, присвоенный формой элементу;
т int append(String str) — добавляет статическую строку str в форму, возвраща-
ет индекс, присвоенный формой элементу;
• int appenddtem item)—добавляет элемент item в форму, возвращает индекс,
присвоенный формой элементу. Следует отметить, что один экземпляр эле-
мента может быть добавлен лишь единожды и только в одну форму;
• void delete( int itemNum) — удаляет из формы элемент с индексом itemNum. Ну-
мерация элементов начинается с нуля;
• Item get (int itemNum) — возвращает элемент формы с индексом itemNum, сама
форма при этом остается неизменной;
• void insertdnt itemNum. Item item) — вставляет в форму элемент item в пози-
цию, заданную переменной itemNum;
• void set (int itemNum. Item item) —заменяет в форме элемент с индексом itemNum
на элемент item;
• void setltemStateListenerdtemStateListener i Listener) — задает блок прослуши-
вания элемента 1 Li stener. Поскольку реализация элементов скрыта от нас, блок
прослушивания элемента — это единственное средство контроля непосредствен-
но во время ввода информации. Как только данные элемента изменяются пользо-
вателем, который выбирает новый пункт меню или вводит очередной символ,
----------------------- Page 33-----------------------
34 Глава 3 . Пользовательский интерфейс высокого уровня
автоматически вызывается функция блока прослушивания itemStateChangecK Item
item). Следует отметить, что функция вызовется только при изменении элемен-
та пользователем. Если изменение происходит программно, то вызова не про-
изойдет;
• int size() — возвращает количество элементов в форме.
Все объекты иерархии класса Screen, в том числе и форма, обладают необязатель-
ными заголовком и бегущей строкой. При создании формы в конструкторе мож-
но указать заголовок отображаемого объекта, а можно оставить заголовок пус-
тым, передав аргумент null. В процессе работы с формой можно получить ее
заголовок с помощью метода String getTitle() и установить новый заголовок, ис-
пользуя метод void setTitleCString s).
Бегущая строка представлена объектом класса Ticker. Текст бегущей строки пе-
редается в конструкторе объекту класса. Поместить бегущую строку в отобража-
емый объект можно с помощью метода void setTickerdicker ticker), получить
объект бегущей строки — используя метод Ticker getTickerO. Программное ре-
дактирование бегущей строки осуществляется с помощью методов класса Ticker:
• void setStringCString str ) — изменить текст бегущей строки;
• String getStringO — получить текст бегущей строки.
Класс Display
Остановимся на вопросе, каким образом рассмотренные объекты отображаются на
экране. Этим процессом руководит менеджер дисплея — объект класса Di spl ay. Он
создается автоматически реализацией MI DP при запуске мидлета и сохраняется
до вызова функции destroyApp(). Самим такой объект создать нельзя, зато можно
получить ссылку на него с помощью метода getDi spl ay (MIDI et), который принимает
в качестве аргумента ссылку на текущий мидлет. В языке Java, как и в C++, ссылка
на текущий объект обозначается ключевым словом this.
Когда мы получили ссылку на Display, любой объект, наследованный из класса
Di spl ayabl e, может быть выведен на экран с помощью метода setCurrent (Di spl ayable).
Ввиду ограниченности ресурсов мобильных аппаратов одновременно на экране
может быть отображен только один объект, перекрытие и наложение окон не под-
держивается. Таким образом, любое приложение может быть рассмотрено как за-
данная последовательность отображаемых объектов.
Во время отображения формы через менеджер дисплея обновление экрана про-
исходит автоматически. Это значит, что при изменении содержания формы или
ее элементов нет необходимости вызывать какие-либо дополнительные функции,
так как изменения сразу будут отображены на экране. Это относится ко всем объек-
там, порожденным классом Screen.
Отображаем картинку
Приведенной информации вполне достаточно, чтобы написать программу, ото-
бражающую картинку на экране телефона. Заметим, что не все картинки одина-
----------------------- Page 34-----------------------
Отображаем картинку 3 5
ково полезны. Во-первых, изображение должно быть в формате PNG (Portable
Network Graphics). Во-вторых, размер картинки не должен превышать размера
дисплея вашего аппарата. Если для работы над мидлетом выбран WTK (Wireless
Toolkit), то после создания нового проекта картинка должна быть помещена
в папку ресурсов приложения /res.
Структуру мидлета мы рассмотрели в прошлой главе. Помним, что основной класс
мидлета должен быть наследован из абстрактного класса MIDI et. He забываем так-
же о его функциях, обязательных к реализации.
Для того чтобы вывести картинку на экран, нам потребуется несколько объектов,
которые будут объявлены как члены класса. Предварительно импортируем необ-
ходимые библиотеки:
import javax.microedition.lcdui.Form ;
import javax.microedition.lcdui.Image ;
import javax.microedition.lcdui.Display ;
private Form form : // форма, отображаемая на экране
private Image image; // картинка для отображения
private Display display : // менеджер дисплея
Далее в функции startApp() реализуем следующие действия:
• получим ссылку на менеджер дисплея;
• создадим объект картинки;
• создадим новую форму;
• добавим картинку в форму;
• отобразим форму на экране через менеджер дисплея.
Код функции будет выглядеть следующим образом:
public void startAppO {
// получить ссылку на менеджер дисплея
display = Display.getDisplay(this) :
try {
// создать картинку из файла flower.png
image = Image.createlmage("/flower.png") ;
}
catch (IOException ioe ) {
// обработать исключительную ситуацию .
// если файл не может быть открыт
System.out.pri ntln(i oe.getMessageC)) ;
}
// создать новую форму
form = new FormC'SlideShow") ;
// добавить картинку в форму
form.append(image);
// вывести форму на экран
display.setCurrent(form) :
----------------------- Page 35-----------------------
36 Глава 3 . Пользовательский интерфейс высокого уровня
^ (п| х|1 Компилируем приложение, как это было рассмотрено
ранее, запускаем на стандартном эмуляторе из WTK
и получаем вот такой результат (рис. 3.2).
Все это просто замечательно, но ничего нового мы пока
что не изобрели. С таким же успехом можно было про-
сто поместить картинку в галерею, если таковая име-
ется. Мы пойдем дальше, задействуем клавиши теле-
фона, чтобы организовать смену картинок в нашем
фотоальбоме.
Класс Command
Итак, сейчас мы работаем с формой, относящейся
к иерархии класса Displayable, который является ба-
зовым для всех отображаемых объектов. Кроме стан-
дартных визуальных средств интеракции, объекты, по-
рожденные этим классом, поддерживают обработку
событий, возникающих в результате действий пользо-
вателя. К отображаемому объекту, в том числе и к фор-
ме, могут быть привязаны различные команды, вызов
которых приведет к некоторым действиям.
Команды представлены объектами класса Command из
пакета javax.microedition.lcdui. Создается коман-
Рис. 3.2 . Вывод картинки
на экран телефона да с помощью конструктора: Command (String label, int
commandType.int priority),где:
• 1 abel — название команды для отображения ее на экране;
• commandType — тип команды;
• priority — приоритет команды.
Поскольку команды относятся к пользовательскому интерфейсу высокого уров-
ня, то при создании команды мы не можем заранее связать ее с какой-то опреде-
ленной клавишей телефона. Порядок вызова команды определит за нас реализа-
ция MIDP, мы лишь можем высказать свои пожелания с помощью параметров
типа команды и ее приоритета.
Тип команды задает внутреннее представление намеченного использования ко-
манды. MIDP предусматривает следующие типы команд, определяемые констан-
тами класса Command:
• BACK — возврат к логически предыдущему экрану;
• CANCEL — выбор отрицательного ответа в диалоге, реализуемом текущим экра-
ном;
• EXIT — выход из приложения;
• HELP — вызов помощи или подсказки;
• ITEM — команда имеет отношение к определенному элементу экрана;
----------------------- Page 36-----------------------
Класс Command 3 7
• OK — выбор положительного ответа в диалоге, реализуемом текущим экраном;
• SCREEN — команда имеет отношение к отображаемому в настоящее время экрану;
м STOP — остановка выполняемой в настоящее время операции.
Заметим, что константы задают лишь тип команды. Они не предписывают ни-
каких действий. Смена экрана, выход из приложения и прочие действия не вы-
полняются автоматически, они должны быть реализованы в соответствующем
месте. Тип лишь ориентирует реализацию MIDP, как разместить команды на
экране и организовать их вызов в соответствии с общим стилем меню и функци-
ональными клавишами конкретной модели телефона.
Приоритет задает преимущество перед другими командами. Это влияет на поря-
док отображения команд при объединении их в группу.' Приоритет задается це-
лым числом. Наименьшее число означает наивысший приоритет.
Такой вариант значительно ограничивает свободу наших действий, но решает мас-
су проблем, связанных с транспортабельностью приложений. Теперь мы можем
быть уверены, что на любом аппарате с поддержкой J2ME команды будут предло-
жены пользователю корректно, с учетом общей политики отображения и стиля
команд конкретного аппарата.
После создания объекта команды необходимо добавить его в форму, которую мы
собираемся отображать на экране. Методы добавления и удаления команд реали-
зует класс Displayable, то есть команды могут быть добавлены к любому отобра-
жаемому объекту:
• void addCommand(Command and) — добавляет команду and к отображаемому объек-
ту. Будет ли команда привязана к функциональной клавише или объединена
с другими командами в меню, зависит от реализации MIDP конкретного аппа-
рата. Команда может быть добавлена в объект не более одного раза, но может
быть включена одновременно в несколько объектов;
• void removeCommand(Command and) — удаляет команду and из отображаемого объекта.
Для примера проведем эксперимент на эмуляторе Nokia. Добавим в форму четы-
ре однотипных команды с разным приоритетом:
Command cl = new CommandC'Commandl" . Command.OK . 4 ) :
form.addCommand(cl):
Command c2 = new Command("Command2" , Command.OK . 3 ) ;
form.addCommand(c2) :
Command c3 = new CommandC"Command3" . Command.OK . 2 ) ;
form.addCommand(c3);
Command c4 = new Command("Command4" . Command.OK . 1) ;
form.addCommand(c4):
display.setCurrent(form);
В результате все команды объединились в меню Options, привязанное к левой функ-
циональной клавише телефона, при нажатии на которую появился список наших
команд. Это аппаратно реализованный список, позволяющий выбрать одну из
команд клавишей Select или вернуться к предыдущему экрану клавишей Back
(рис. 3.3).
----------------------- Page 37-----------------------
38 Глава 3 . Пользовательский интерфейс высокого уровня
* ( 1 ) Nokia 721USDX Заметьте, что в соответствии с заданными при-
Tools Help оритетами последняя добавленная команда на-
ходится первой в списке. Использование интер-
фейса высокого уровня приводит к тому, что та
же самая программа, запущенная на другом ап-
парате, будет выглядеть совершенно иначе.
Интерфейс
СопишппЗ CommandListener
Command2
Command] Как уже было замечено, сам объект команды
не предписывает программе выполнение каких-
либо действий, он является лишь связующим зве-
ном между исполнителем команды и формой.
Приемником и исполнителем команды может яв-
ляться любой объект, реализующий интерфейс
CommandListener, который имеет всего лишь один
метод: void commandAction(Command с. Displayable
d). Этот метод нам нужно написать самим и реа-
лизовать там связанные с командами действия.
Метод corrmandAction вызовется автоматически при
выборе пользователем одной из команд, которая
и будет передана в метод в качестве аргумента.
В нашем случае удобно сделать так, чтобы ос-
новной класс мидлета содержал блок прослуши-
вания команд в себе самом. В общем случае блок
Рис. 3.3 . Аппаратно может содержаться в любом объекте. Таким об-
реализованный разом, основной класс мидлета будет расширять
список команд класс MIDIet и одновременно реализовывать ин-
терфейс CommandListener:
public class SlideShow extends MIDlet implements CommandListener
Приемник команд, как и сами команды, должен быть привязан к объекту типа
Displayable, в нашем случае это форма. В каждый момент времени отобража-
емый объект может иметь только один приемник . Связь приемника команд
с отображаемым объектом осуществляется с помощью метода класса Di spl ayabl e:
void setCommandListener(CommandListener I) . В нашем случае, когда основной класс
мидлета реализует блок прослушивания команд, в качестве аргумента переда-
ется ссылка на главный объект. Так же как и в случае с менеджером дисплея, мы
опять воспользуемся ключевым словом this .
Теперь у нас есть все необходимые средства для реализации полноценного фото-
альбома. Подготовим картинки для фотоальбома, учитывая размер экрана и мак-
симальный объем приложения вашей модели телефона. Назовем картинки, кото-
рые мы хотим демонстрировать, так: l.png, 2.png и т. д. — и разместим их в папке /
res. Заодно добавим два члена класса SlideShow: int slideNum и int maxSlideNum,
----------------------- Page 38-----------------------
Интерфейс CommandListener 3 9
которые будут содержать текущий номер картинки и общее количество картинок
соответственно.
Далее реализуем обработку команд и добавим логику для смены картинок на эк-
ране при нажатии клавиш. В конечном итоге программа полноценного фотоаль-
бома будет выглядеть следующим образом:
import javax.microedition.midlet.MIDlet :
import javax.microedition.lcdui.Display ;
import javax.microedition.lcdui.Form :
import javax.microedition.lcdui.Command ;
import javax.microedition.lcdui.CommandListener ;
import javax.microedition.lcdui.Displayable :
import javax.microedition.lcdui.Image ;
import javax.microedition.lcdui.Ticker ;
import java.io.IOException :
public class SlideShow extends MIDlet implements CommandListener
{
private Display display ; // менеджер дисплея
private Form form ; // отображаемая форма
private Command next ; // команда перехода к следующей картинке
private Command back ; // команда перехода к предыдущей картинке
private Image image ; // объект картинки
private int slideNunri : // номер текущей картинки
private int maxSlideNum=5 ; // общее количество картинок
public void startAppO
{ // получить ссылку на менеджер дисплея
display - Display.getDisplay(this) :
// создать новую форму без заголовка
form = new Form(null) ;
// установить приемник команд для формы
form.setCommandListener(this) :
// создать команду перехода к следующей картинке
next = new Command("Next" . Command.OK , 1 ) ;
// добавить команду в форму
form.addCommand С next) :
// создать команду возврата к предыдущей картинке
back • new CommandC'Back" , Command.BACK . 1) :
// добавить команду в форму
form.addCommand(back) :
// создать объект бегущей строки
Ticker t • new TickerC'My Photoalbum"):
// добавить бегущую строку в форму
form.setTicker(t) ;
// добавить в форму первую картинку
setlmageC'/l.png") :
----------------------- Page 39-----------------------
4 0 Глава 3 . Пользовательский интерфейс высокого уровня
// отобразить форму на экране
di splay.setCurrent(form) ;
}
// метод обработки команд
// реализует действия , предписанные командами
public void commandAction(Command c . Displayable d ) {
// команда перехода к следующей картинке .
// если текущая картинка не последняя
if (с == next && slideNum<maxSlideNum )
// увеличить номер текущей картинки
slideNum++:
// команда возврата к предыдущей картинке .
// если текущая картинка не первая
if (с — back & & slideNum>l )
// увеличить номер текущей картинки
slideNum--;
// удалить из формы текущую картинку
form.delete(O);
// получить имя файла картинки из ее номера
// и добавить полученную картинку в форму
setImage("/"+Integer.toString(s1ideNum)+".png") :
// функция setlmage принимает имя файла картинки,
// создает объект картинки и добавляет в форму
public void setImage(String path)
{
t r y {
// создать картинку из файла , переданного в аргументе
image = Image.createlmage(path) ;
}
// обработать исключительную ситуацию ,
// если файл не может быть открыт
catch (IOException ioe ) {
System.out.pri ntln(i oe.getMessage()) :
}
// добавить картинку в форму
form.append(image):
Отметим, что для смены изображения на экране мы лишь помещаем новую кар-
тинку в отображаемую на экране форму, после чего картинка сразу появляется на
экране (рис. 3.4). Заметим, что в данном эмуляторе изображение не поместилось
на экране, поэтому реализация организовала вертикальную прокрутку экрана,
----------------------- Page 40-----------------------
Интерфейс CommandListener 4 1
обозначенную стрелочкой вниз. Горизонтальная про-
крутка экрана в реализациях, как правило, отсутствует.
Поздравляю! Если вы не поленились оторваться от
книжки и реализовать вышеописанное, то загрузите по-
лучившийся мидлет в телефон, и вы будете всегда иметь
под рукой маленький фотоальбом. Можно пойти даль-
ше и добавить нехитрую логику перехода к первой кар-
тинке после демонстрации последней и наоборот.
К сожалению, стандартная реализация J2ME не под-
держивает доступ к внутренним параметрам телефона,
к записной книжке или экранной заставке, поэтому,
в общем случае, установить отображаемую картинку
в качестве фона-заставки телефона программно мы не
можем. Но не стоит отчаиваться, потому что подобная
функция может быть реализована производителем ап-
парата в специализированном пакете функций API. На
некоторых таких пакетах подробнее мы остановимся
в одной из заключительных глав.
С картинками нужно быть аккуратными, ведь многие
модели телефонов накладывают серьезные ограниче-
ния на размер приложения, а картинки в этом плане
очень ресурсоемки. Здесь придется искать оптималь-
ное соотношение параметров цена и качество: платим
в данном случае мы объемом памяти, занимаемым кар-
тинкой. Именно поэтому оптимизацию мидлетов на- Рис. 3.4 . Окончательный
вид фотоальбома
чинают с оптимизации картинок, различными средства-
ми стараясь уменьшить занимаемый ими объем памяти.
На сегодняшний день существует масса различных средств для обработки карти-
нок, от классического PhotoShop до специализированных ImageOptimizer, кото-
рые порой дают лучшие результаты в плане оптимизации, чем монстры дизайна.
Часто производители телефонов предлагают свои средства для редактирования
и оптимизации изображений. Например, Nokia PC Suite содержит свой замеча-
тельный Nokia Image Converter.
Итак, основа проектирования любого приложения — создание структуры демон-
стрирования различных отображаемых объектов и реализация логики перехо-
дов между этими объектами. Логику переходов можно организовать с помощью
пользовательского интерфейса высокого уровня, который мы рассмотрели в этой
главе. Для этого нужно спроектировать отображаемые объекты, добавить к ним
требуемые команды и реализовать интерфейс прослушивания и исполнения ко-
манд. Приложения, организованные таким образом, более транспортабельны
и могут быть перенесены на различные модели телефонов. Пользовательский
интерфейс низкого уровня мы рассмотрим в следующей главе.
----------------------- Page 41-----------------------
Глава 4
Работа с графикой
В прошлой главе мы рассмотрели пример работы с пользовательским интерфейсом
высокого уровня, который представлен иерархией класса Screen. При работе
с объектами такого типа мы не имели прямого доступа к экрану аппарата, а пользова-
лись формой как визуальной абстракцией высокого уровня. Изменения изображе-
ния на экране осуществлялись с помощью методов формы и добавленных в нее
объектов. То есть, работая с высокоуровневым интерфейсом, при всем желании
нарисовать поперек экрана, например, жирную синюю полосу мы не сможем.
Решить проблему непосредственного доступа к экрану нам поможет низкоуров-
невый пользовательский интерфейс, который представлен второй веткой иерар-
хии отображаемых объектов типа Displayable — классом Canvas.
Класс Canvas
Класс Canvas является абстрактным. Это значит, что мы не можем просто создать
объект этого класса. Если все же попытаться создать такой объект, то компилятор
подтвердит вышесказанное вот такими словами:
javax.microedition . lcdui .Canvas is abstract; cannot be instantiated
Canvas с = newCanvasO:
В объектно-ориентированном программировании абстрактными называются та-
кие классы, которые содержат в себе хотя бы один абстрактный метод. Использо-
вать абстрактные классы можно только как основу для новых классов, в которых
обязательна реализация абстрактного метода.
В классе Canvas абстрактным является метод abstract void paintCGraphics g), кото-
рый отвечает за перерисовку экрана и получает в качестве аргумента объект клас-
са Graphics, который создается автоматически при инициализации объекта Canvas.
Класс Graphics
Собственно класс Graphics и предоставляет возможности низкоуровневого гра-
фического рисования. Возможности класса Graphics далеко не безграничны: он
содержит методы для рисования и заливки базовых геометрических фигур, таких
как линии, прямоугольники и дуги. Также поддерживаются возможности отобра-
жения текстовых символов и смены цвета для рисования.
----------------------- Page 42-----------------------
Класс Graphics 4 3
Теперь все в наших руках, включая и все точки экрана, каждая из которых, кста-
ти, имеет свои координаты по осям хиу. Начало координат (точка 0,0) находится
в левом верхнем углу экрана. То есть нужно учесть, что ось у направлена вниз.
А теперь перейдем к рисаванию и рассмотрим методы класса Graphics, которые
могут нам понадобиться. В первую очередь заметим, что размеры экрана у разных
моделей могут серьезно различаться, поэтому сразу будем писать транспортабель-
ные приложения. Таким образом, первым делом нам требуется получить размеры
экрана конкретного аппарата. Делается это с помощью двух методов:
• int getClipHeightO — возвращает высоту текущей области для рисования;
• int getCl ipWidth() — возвращает ширину текущей области для рисования.
Сам класс Canvas также содержит методы для определения размеров отобража-
емой области экрана, которые не зависят от текущей области для рисования
и остаются неизменными во время работы приложения:
• i nt getHei ght () — возвращает высоту отображаемой области экрана;
• int getWidthC) — возвращает ширину отображаемой области экрана.
Теперь, получив ширину и высоту области для рисования, мы можем использо-
вать все точки с соответствующими координатами. По умолчанию текущей обла-
стью для рисования является весь экран, но и это можно изменить, использовав
метод SetC lip:
• void setClip( int x. inty . int width, int height) — устанавливает новую прямо-
угольную область рисования с левым верхним углом в точке (х, у), высотой
height и шириной width пикселов.
Иногда удобно сместить начало координат из левого верхнего угла в более удоб-
ное место. Делается это с помощью метода void trans late (int x. int у), который
переносит начало координат в точку с координатами (х, у). Получить текущее
положение начала координат относительно левого верхнего угла можно следу-
ющими методами: int getTranslateXO и int getTranslateYO.
После того как мы определились с областью для рисования и началом координат,
рассмотрим несколько методов для отображения на экране базовых геометриче-
ских фигур:
• void drawLinednt xl, intyl , intx2 . inty2) — рисует линию из точки с коорди-
натами (Xj, г/,) в точку с координатами (х. , у ). В классе Graphics нет метода для
2 2
рисования одной точки, поэтому отдельная точка рисуется с помощью линии,
начало и конец которой заданы одними и теми же координатами;
• void drawRect( int x. inty , int width, int height) — рисует прямоугольник с ле-
вым верхним углом в точке (х, у), шириной width и высотой height пикселов;
• voiddrawRoundRect(intx. inty. intwidth, intheight, intarcWidth, intarcHeight) —
рисует прямоугольник с закругленными углами. Первые четыре аргумента
аналогичны аргументам предыдущего метода, acrWidth задает диаметр закруг-
ляющей дуги вокруг оси х, arcHeight задает диаметр закругляющей дуги вок-
руг оси у;
• void drawArc( int x, inty , intwidth. intheight, int startAngle. int arcAngle)—
рисует дугу, вписанную в прямоугольник с левым верхним углом в точке
с координатами (х,у). Параметр startAngle задает угол, с которого будет начи-
----------------------- Page 43-----------------------
4 4 Глава 4 . Работа с графикой
наться дуга, а параметр arcAngle задает угловое расстояние, на которое будет
простираться дуга. Углы задаются в градусах. Таким образом, чтобы нарисо-
вать окружность, нужно в квадрат вписать дугу с угловым расстоянием 360.
Линии, прямоугольники и дуги могут быть нарисованы двумя способами штри-
хования — сплошной и пунктирной линиями. Стиль штрихования задается с по-
мощью метода void setStrokeStyleCint style), в качестве аргумента style передает-
ся одна из констант класса Graphics: SOLIO — для рисования сплошных линий,
DOTTED — для рисования пунктирных линий. Получить текущий стиль штрихова-
ния позволяет метод int getStrokeStyle().
До сих пор, по умолчанию, мы рисовали черным по белому, но в настоящее время
аппаратов с цветными дисплеями, которые поддерживают технологию J2ME, го-
раздо больше, чем их монохромных собратьев. Поэтому не будем ограничивать
свою фантазию и раскрасим экран нашего телефона веселыми красками.
Для начала необходимо узнать, цветной ли дисплей у телефона, который достал-
' ся в распоряжение нашей программе. Эту информацию можно извлечь из менед-
жера дисплея, используя метод boolean isColorC), который вернет значение true,
если аппарат поддерживает работу с цветом. Количество доступных цветов или,
в некоторых моделях, количество градаций одного серого цвета можно получить
с помощью метода int numColorsO.
В отличие от других языков программирования, в J2ME константы для определе-
ния цвета не заданы, поэтому придется нам самим, как настоящим художникам,
наносить на палитру три цветовых составляющих: красный, зеленый и синий (мо-
дель образования цвета RGB). Доля каждого из этих цветов задается числом от 0 до
255. Например, комбинация (0,0,255) задает синий цвет, а (0,0,0) — черный. Наши
краски не кончаются, поэтому экспериментировать мы можем сколько угодно. Цвет
рисования устанавливается с помощью двух методов класса Graphics:
• void setColorCint red, int green, int blue) — задает цвет рисования, заданный
цветовыми составляющими red, green, Ы ue;
• void setColorCint RGB) — устанавливает цвет, заданный цветовыми составля-
ющими, объединенными в одно число RGB, где каждые два байта представляют
свою составляющую. Например, красному цвету (255,0,0) будет соответство-
вать число RGB 0xFF0000.
Узнать текущий цвет рисования можно опять же двумя способами: либо полу-
чить каждую цветовую компоненту по отдельности, используя методы int
getRedComponentO, int getGreenComponentO и int getBlueComponentO, либо полу-
чить число RGB с помощью метода int getColorO.
Теперь мы можем рисовать не только черным, что, несомненно, скрасит нашу
жизнь, но пока что только по белому. Изменить цвет фона или какой-то его части
можно с помощью методов рисования закрашенных фигур:
• void fi 11 Rect(int x. int у. int width. int height) — рисует закрашенный прямо-
угольник;
• voidfillRoundRect(intx, inty, 1ntwidth, intheight. intarcWidth. intarcHeight) —
рисует закрашенный прямоугольник с закругленными углами;
• void fillArcCint x, inty . int width, intheight. int startAngle, int arcAngle) —
рисует закрашенный сектор.
----------------------- Page 44-----------------------
Класс Graph 4 5
Все аргументы методов рисования закрашенных фигур идентичны аргументам
аналогичных методов рисования простых фигур. Фигуры закрашиваются теку-
щим цветом, и если мы хотим очистить экран, то нужно установить белый теку-
щим цветом и нарисовать закрашенный прямоугольник размером на весь экран.
Класс Graph
Самое время остановиться и попробовать использовать полученные знания на
практике. В качестве примера нарисуем график параболы. Красная парабола на
желтом фоне — разве это не прекрасно?!
Представим наш график новым классом Graph, который расширит класс низкоуров-
него интерфейса Canvas и реализует его абстрактный метод paint. В методе pai nt мы
зальем экран желтым фоном, нарисуем рамку и оси координат, сместим начало ко-
ординат в центр экрана, а затем в цикле вычислим координаты для каждой точки
параболы по формуле. Наш класс будет выглядеть следующим образом:
import javax.microedition.midlet.MIDlet :
import javax.microedition.lcdui.Display ;
import javax.microedition.lcdui.Displayable ;
import javax.microedition.lcdui.Canvas ;
import javax.microedition.lcdui.Graphics ;
public class Graph extends Canvas (
// функция прорисовки экрана
public void paint(Graphics g ) {
// получить ширину экрана
int width - g.getClipWidthO:
// получить высоту экрана
int height = g.getClipHeightO :
// установить текущий цвет желтым
g.setColorC255.255.0) :
// нарисовать закрашенный прямоугольник
// размером на весь экран
g.fillRect(0.0.width.height);
// установить текущий цвет черным
g.setColor(O.O.O) :
// нарисовать рамку
g.drawRect(O,O,width-l.height-l);
// нарисовать оси координат
g.drawLine(width/2.0.width/2.height) :
д.drawLine(0,height/2.width.height/2);
// сместить начало координат в центр экрана
g.translate(width/2.height/2);
// установить текущий цвет красным
g.setColor(255.0.0);
----------------------- Page 45-----------------------
46 Глава 4 . Работа с графикой
// для каждой точки по оси х
f o r d n t x=-width/2 ; x<width/2; x++) {
// вычислить значение у по формуле
int у = -х*х/40;
// нарисовать точку параболы
g.drawline(x.y.x.y);
Класс, отвечающий за рисование графика, готов. Осталось в стартовом методе при-
ложения получить ссылку на менеджер дисплея и, как в случае с формой, вывес-
ти отображаемый объект на экран:
public void startAppO {
// создать объект графика
Graph graph = new GraphO;
// получить ссылку на менеджер дисплея
display = Display.getOisplay(this) ;
// вывести график на экран
display.setCurrent(graph);
}
Компилируем, запускаем и наслаждаемся полученным
результатом (рис. 4.1).
Обратим внимание на формулу, по которой мы вы-
считывали координаты точек параболы: -х*х/40. Те-
перь попытаемся вспомнить школьный курс алгебры:
если первый коэффициент квадратного многочлена
отрицательный, то ветви параболы должны быть на-
правлены вниз. Однако на эмуляторе мы видим, что
ветви параболы направлены вверх. Дело в том, что
в телефоне ось у направлена не вверх, как в школь-
ных учебниках, а в обратном направлении.
Вторая проблема в том, что, как мы видим, область
для рисования, предоставленная нам аппаратом, мень-
ше, чем реальные размеры экрана, а сверху пристро-
илась незваная полоска с названием основного клас-
са мидлета. Действительно, зачастую разработчики
оставляют себе часть экрана для вспомогательной ин-
формации.
Полный экран можно получить двумя способами: ис-
пользовав функцию профайла MIDP2, обзор которого
останется за переплетом этой книги, или задействовав
специальную API-функцию производителя. На втором
способе мы остановимся в одной из заключительных
Рис. 4.1. Парабола, созданная глав,
средствами J2ME
----------------------- Page 46-----------------------
Рисование текста 4 7
Рисование текста
Рисовать, конечно, здорово, но иногда на картинке требуется что-нибудь напи-
сать, например GAME OVER. В прошлой главе, когда мы отображали текст при помо-
щи интерфейса высокого уровня, форматирование строки происходило автома-
тически. Теперь ответственность за размещение текста на экране, выравнивание
и перенос строк целиком лежит на плечах программиста.
Позиция для текста задается при помощи координат точки размещения текста
и точки привязки . Отображаемая строка заключается в воображаемый прямо-
угольник, точка привязки которого будет помещена в заданные координаты. Точ-
ка привязки определяется комбинацией вертикальной и горизонтальной констант
класса Graphics.
• BASELINE — размещение нижней границы текста по координате у.
• ТОР — размещение верхней границы текста по координате у.
• LEFT — размещение левой границы текста по координате х.
• RIGHT — размещение правой границы текста по координате х.
• HCENTER — размещение середины текста по координате х.
На рисунке показано расположение точек привязки на прямоугольнике, ограни-
чивающем границы текста.
Left | Top Hcenter | Top Right | Тор
Some text to draw . '
Left | Baseline Hcenter | Baseline Right | Baseline
Рис. 4.2 . Схема точек привязки текста
Класс Graphics предоставляет следующие методы рисования текста:
• void drawChar(char character , int x. int y. int anchor) — отображает символ
character, размещая его точку привязки, заданную аргументом anchor, в точке
с координатами (х,у);
• void drawChars(char[] data, intoffset . intlength . intx . inty , int anchor) — ри-
сует 1 ength символов массива data, начиная с символа с индексом offset. Логи-
ка размещения текста на экране остается прежней;
• voiddrawString(Stringstr, intx , inty . intanchor) — рисует строку str в указан-
ной позиции;
• void drawSubstring(String str , intoffset , intlen , intx , inty , int anchor) — ри-
сует len символов строки str , начиная с символа с индексом offset.
Работу с текстом мы рассмотрим подробнее в дальнейшем на примере программы
для чтения книг Book Reader. Кроме базовых геометрических фигур и текста, как
и в прошлой главе, мы можем вывести на экран картинку, представленную фай-
лом в формате PNG . Теперь мы можем четко указать положение картинки на эк-
ране, используя константы привязки для рисования текста, а также одну допол-
----------------------- Page 47-----------------------
4 8 Глава 4 . Работа с графикой
нительную константу VCENTER, которая размещает точку привязки в середине изоб-
ражения по оси у. Метод для отображения картинки:
• void drawlmage(Image Img, intx . inty . int anchor) — рисует картинку, заданную
аргументом img, разместив точку ее привязки anchor в координатах (х, у).
Обработка клавишных событий
Интерфейс низкого уровня позволяет нам отслеживать нажатия клавиш. Если
в интерфейсе высокого уровня события формировались только для добавлен-
ных команд, то интерфейс низкого уровня формирует события при нажатии
любой клавиши, причем теперь можно узнать, какая конкретно клавиша была
нажата.
Класс Canvas также определяет блоки прослушивания событий другого уровня.
Теперь в зависимости от действий пользователя будет автоматически вызван один
из следующих методов класса Canvas:
• void keyPressedC int keyCode) — вызывается, если какая-либо клавиша была на-
жата;
• void keyRel eased( int keyCode) — вызывается, если какая-либо клавиша была от-
пущена;
• void keyRepeated( int keyCode) — вызывается при длительном нажатии клавиши.
Все методы принимают аргумент, представляющий код нажимаемой клавиши.
Коды клавиш задаются следующими константами класса Canvas:
• KEY_NUM0, KEYJUM1, KEYJUM2,... KEYJUM9 - представляют цифры на клавиатуре
телефона;
• LEFT, RIGHT, UP, DOWN — представляют клавиши телефона, обозначенные стрел-
ками;
• KEY_POUND — представляет клавишу с символом # (решетка);
• KEY_STAR — представляет клавишу с символом * (звездочка).
Для того чтобы привязать какие-то действия к желаемому событию, в классе-по-
томке класса Canvas необходимо переписать соответствующую функцию и там
реализовать необходимые действия.
Перейдем от теории к практике. Рассмотрим подробнее обработку событий низ-
коуровневого интерфейса на примере программы управляемой точки, рисующей
фигуры на экране мобильника.
Реализуем класс Point, наследованный из класса Canvas, который будет содер-
жать членами класса текущие координаты точки, размеры отображаемой облас-
ти экрана, команду очистки экрана, а также флаг, управляющий очисткой экра-
на. Блок прослушивания событий низкого уровня будет отслеживать нажатия
клавиш и менять текущие координаты точки соответственно нажатой цифре.
Блок прослушивания команд будет поднимать флаг в случае вызова команды
очистки экрана.
Код класса Point выглядит следующим образом:
public class Point extends Canvas implements Commandl_istener {
private int x: // координата х точки
----------------------- Page 48-----------------------
Обработка клавишных событий 4 9
private i n t у ; // координата у точки
private i n t width : // ширина экрана
private i n t height : // высота экрана
private boolean cirFlag - true : // флаг очистки экрана
private Command clear : // команда очистки экрана
// конструктор класса Point
public Point О {
// вызов конструктора родительского класса
superO:
// получить ширину экрана
width=getWidth() :
// получить высоту экрана
height=getHeight();
// установить текущие координаты точки в центр экрана
x=width/2 :
y=height/2 :
// создать объект команды очистки экрана
clear = new Command(«Clear» , Command.OK . 1) ;
// добавить команду очистки экрана
addCommand(clear) :
// установить блок прослушивания команды
setCommandLi stener(this) :
}
// метод перерисовки экрана
public void paintCGraphics g ) {
// проверить флаг очистки экрана
if (cirFlag ) {
// установить текущий цвет белым
g.setColor(Oxffffff) :
// залить прямоугольник размером с экран
g.fillRect(O . 0 . width , height) :
// сбросить флаг очистки экрана
cirFlag - false :
}
// установить текущий цвет точки красным
g.setColor(255.0.0);
// отобразить точку в текущих координатах
g.drawLine(x.y.x.y) :
}
// метод , реализующий блок прослушивания команды
public void commandActiontCommand с . Displayable s ) {.
// если сработала команда очистки
if (с -- clear ) {
// поднять флаг очистки экрана
cirFlag = true :
// установить текущие координаты точки в центр экрана
----------------------- Page 49-----------------------
5 0 Глава 4 . Работа с графикой
x=width/2 :
y=height/2 :
// инициировать перерисовку экрана
repaintO :
// блок прослушивания событий низкого уровня
public void keyPressed(int keyCode ) {
switch (keyCode ) {
// сместить текущие координаты точки
// в соответствии с нажатой клавишей
case KEYJIUM1 : х- : у- ; break :
case KEYJUM2 : у- ; break ;
case KEYJUM3 : х++ ; у- ; break ;
case KEYJIUM4 : х- : break ;
case KEY_NUM6 : x++ : break :
case KEYJUM7 : x- : y++ : break :
case KEY_NUM8 : y++ : break :
case KEY_NUM9 : x++ : y++ ; break :
}
// инициировать перерисовку экрана
repaintO;
Обратим внимание на то, что в отличие от работы с формой, изменения изобра-
жения не сразу отражаются на экране. Перерисовка экрана, реализованная в ме-
тоде pai nt, должна быть вызвана явно, с помощью одного из следующих методов
класса Canvas:
• void repaintdnt х, inty , int width, int height) — перерисовка прямоугольной
области экрана шириной width и высотой height, левый верхний угол которой
находится в точке с координатами (х,у);
• void repaintO — перерисовка экрана целиком. Аналогична такому вызову пре-
дыдущего метода: repaint(0. 0, getWidthO. getHeightO).
Сам же метод paint вызывается внутренней реализацией, мы никогда не будем
вызывать его явно.
Основная работа выполнена. Отображение управляемой точки на экране выпол-
няется в стартовом методе мидлета точно так же, как и в предыдущем случае:
public void startAppO {
// создать объект управляемой точки
Point point = new PointO :
// получить ссылку на менеджер дисплея
display = Display.getDisplay(this) ;
// вывести управляемую точку на экран
di splay.setCurrent(poi n t ) ;
----------------------- Page 50-----------------------
Обработка клавишных событий 5 1
Немного поиграв в то, что получилось, мне удалось нарисовать вот такую картин-
ку (рис. 4.3).
С этого примера мы начнем разработку законченной, качественной игры, ко-
торой уже и не стыдно будет похвастаться даже перед профессионалами. Идея
далеко не оригинальна: это знакомая нам с детства «Змейка», так любимая раз-
работчиками корпорации Nokia. Самое время дать наш ответ заграничным Чем-
берленам и укротить непослушную змею. На
этом примере мы и будем постигать тонкости
программирования под мобильные телефоны,
постепенно усложняя и совершенствуя нашу
игру. Но это будет уже в следующих главах...
* * *
В этой главе мы рассмотрели работу с пользова-
тельским интерфейсом низкого уровня, а также
графические возможности языка J2ME и логику
обработки событий. Поскольку класс Canvas не
является потомком класса Screen, он не исполь-
зует ни одной высокоуровневой абстракции, оп-
ределяемой иерархией класса Screen, например
добавление заголовка или бегущей строки невоз-
можно. С другой стороны, класс Canvas принад-
лежит к иерархии отображаемых объектов типа
Di spl ayabl e, поэтому он может задействовать об-
работку команд высокого уровня и реализовать
блок прослушивания команд.
Для использования интерфейса низкого уров-
ня необходимо создать новый класс, расширя-
ющий класс Canvas, и реализовать его абстракт-
ный метод paint, который и будет осуществлять
прорисовку экрана. В этой главе мы также рас-
смотрели большинство возможностей и мето-
дов классов Graphics и Canvas. Кое-что осталось
на самостоятельное изучение, благо корпора-
ция Sun Microsystems предоставляет подроб- Рис. 4.3 . Текст, написанный
с помощью «управляемой
ную документацию языка. точки»
----------------------- Page 51-----------------------
Глава 5
Структуры
и форматы данных в J2ME
Самое время перейти к теме, с которой, наверное, стоит начинать изучение любо-
го языка программирования: типы и структуры данных. Какие-то типы мы уже
использовали в примерах, теперь рассмотрим все существующие типы, а также
структуры хранения данных.
Кому-то вводная информация о типах данных и контейнерах языка Java может по-
казаться скучной. В таком случае лучше сразу перейти к водным процедурам — ко
второй части главы, где мы продолжим разработки нашей игры в змейку. С теми
же, кому требуется дополнительный ликбез в области общего программирования,
мы продолжим.
Объекты
Язык Java является объектно-ориентированным языком даже в большей степени,
чем остальные языки, оперирующие понятием «объект». Любые данные представ-
ляются в виде объектов — сущностей, имеющих набор свойств-полей и методов
для работы с этими свойствами.
Создается новый объект с помощью операции new, где указывается тип создава-
емого объекта. При создании объекта автоматически вызывается его конструк-
тор — особый метод, отвечающий за инициализацию объекта. Конструктор мо-
жет иметь входные параметры инициализации, которые передаются в операции
new через круглые скобки, например:
String st r - new StringC'some text") :
Объект может иметь несколько конструкторов. Вызывается тот конструктор, пара-
метры которого совпадут по количеству и типу с параметрами, переданными че-
рез new.
В двух словах коснемся наследования классов. На основе любого класса можно
создать свой собственный класс, который будет содержать все поля и методы ро-
дительского класса, а также те свойства и методы, которые мы решим придать
новому классу. Реализуется наследование с использованием ключевого слова
extends, которое мы уже применяли в прошлой главе:
public class Graph extends Canvas
Для знатоков C++ заметим, что в языке Java множественное наследование запре-
щено вообще. Вместо этого предлагается сущность, именуемая интерфейсом.
----------------------- Page 52-----------------------
Массивы 5 3
Интерфейс отличается от обычного класса тем, что содержит только заголовки
методов и не содержит их реализации. Любой создаваемый класс может реализо-
вать один или несколько интерфейсов, как мы это делали в прошлой главе:
public class Point extends Canvas implements CommandListener
К сожалению, мы не имеем возможности глубоко вникать в идеологию объектно-
ориентированного программирования, поэтому на данном этапе можно почитать
дополнительную литературу по этому вопросу.
Примитивные типы
Несмотря на то, что Java является объектно-ориентированным языком, представ-
ляющим любые данные в виде объектов, все же существуют особые типы данных,
называемые примитивными, которые очень широко используются в мобильном
программировании.
Поддержка таких типов обусловлена тем, что зачастую малоэффективно созда-
вать полноценный объект, используя оператор new и всю систему объектного за-
хвата памяти, для одной маленькой циферки. Далее приведены примитивные типы,
которые поддерживает J2ME, с объемом памяти, выделяемым каждому типу:
byte 1 байт
short 2 байта
char 2 байта
int 4 байта
long 8 байт
В прошлых главах мы рассматривали набор методов различных классов. Перед
каждым методом указан тип данных, который возвращает тот или иной метод.
Например, метод класса Canvas int getHeightO возвращает высоту экрана в виде
числа, представленного примитивным типом int. Часто методы выполняют ка-
кие-то действия, но не возвращают никаких данных или значений. В таком слу-
чае тип возвращаемых данных в заголовке метода должен быть определен как voi d
(пустой тип данных). В этом случае оператор return предписывает лишь выход из
метода и не возвращает никаких значений.
Справедливости ради заметим, что любой примитивный тип данных может быть
представлен соответствующим объектом одного из классов: Byte, Character, Short,
Integer, Long.
Массивы
Массивом называется набор однотипных данных, хранимых в смежных ячейках
оперативной памяти. Работа с массивами в языке Java производится в три этапа.
1. Объявление массива. На этом этапе задается переменная и тип массива. Квад-
ратные скобки указывают на то, что объявляется именно массив. Например:
int array[] ;
Image images[] :
----------------------- Page 53-----------------------
54 Глава 5 . Структуры и форматы данных в J2ME
2. Определение массива осуществляется с помощью оператора new, в котором
требуется указать точный размер массива, называемый длиной. На этом этапе
происходит захват памяти для размещения массива. Ссылка на захваченную
память присваивается переменной массива:
array - new int[50] ;
images = new Image[20] :
3. Инициализация массива — присвоение значений элементам массива. Каждый
элемент имеет свой индекс — порядковый номер от нуля до длины массива без
единицы. Обращение к элементам массива выглядит так:
аггау[0] = 5;
аггауШ = 10:
Одна из основных проблем обычных массивов состоит в том, что при создании
массива нужно точно указать количество хранимых в нем данных, что далеко не
всегда удобно или вообще невозможно. Кроме обычных массивов существуют
классы-контейнеры, позволяющие хранить произвольное количество данных.
Такие контейнеры позволяют удобно решать задачи, требующие частого добавле-
ния и удаления неизвестного заранее количества данных. Все контейнеры содер-
жатся в пакете Java. uti 1. Класс Vector содержится в языке Java с самых первых ее
версий.
Класс Vector
Класс Vector предназначен для хранения произвольного числа аргументов самого
общего типа Object, то есть хранимые данные должны быть тем или иным образом
наследованы из класса Object. Под эту категорию попадают любые объекты языка
J2ME, кроме примитивных типов данных.
Размер вектора (size) — это количество хранимых в нем элементов. Каждый эле-
мент при добавлении получает свой порядковый индекс, начиная с нуля. Таким
образом, индекс последнего элемента вектора на единицу меньше его размера.
Метод int size() возвращает текущий размер вектора, а метод boolean is Empty ()
возвращает значение true, если вектор не содержит элементов.
Метод void setSi ze(int newSize) устанавливает новый размер вектора, причем если
значение newSi ze превышает текущий размер вектора, то все новые элементы при-
нимают значение null. Если значение newSi ze меньше, чем текущий размер векто-
ра, то все элементы с индексами newSize и выше удаляются.
Добавить новый элемент в вектор можно с помощью трех методов:
• void addElement(Object obj)—добавляет новый элемент в конец вектора. До-
бавленный элемент получает наивысший индекс, размер вектора увеличива-
ется на единицу;
• void insertElementAt(Object obj. int index) — добавляет новый элемент с за-
данным индексом 1 ndex. Индекс элемента, находившегося на этом месте, а так-
же все следующие индексы увеличиваются на единицу. Аргумент index дол-
жен быть больше или равен нулю и меньше, или равен размеру вектора;
----------------------- Page 54-----------------------
Класс Snake 5 5
• void setElementAKObject obj, int index) — заменяет элемент с индексом index
на новый элемент obj, при этом старый элемент удаляется из вектора.
Удалить элементы из вектора можно также следующими методами:
• void removeElementAt(int index)—удаляет из вектора элемент с индексом index,
при этом все индексы элементов, выше удаляемого, уменьшаются на единицу;
• boolean removeElement(Object obj)—удаляет из вектора первое вхождение эле-
мента obj с логикой сдвига индексов, как и в предыдущем случае. Метод воз-
вращается значение true, если удаляемый элемент действительно находился
в векторе;
• void removeAll Elements О — удаляет все элементы вектора; аналогично вызову
setSize(O).
Получить конкретный элемент вектора можно с помощью следующих методов:
• Object firstElementO — возвращает первый элемент вектора;
• Object lastElementO — возвращает последний элемент вектора;
• Object elementAtdnt index) — возвращает элемент вектора с индексом 1 ndex.
Следующие методы предоставляют возможности поиска элемента в векторе:
• boolean contains(Object el em) — возвращает значение true, если элемент el em
содержится в векторе;
• int indexOf (Object el em) — возвращает индекс первого вхождения элемента
el em в вектор; в случае отсутствия элемента в векторе возвращает - 1 ;
• int indexOf (Object elem, int index) — возвращает индекс первого вхождения
элемента elem в вектор, начиная поиск с элемента с индексом index;
• int I a s 11 ndexOf (Ob ject elem) — возвращает индекс последнего вхождения эле-
мента elem в вектор;
• int lastlndexOf (Object elem, int index)— возвращает индекс последнего вхож-
дения элемента el em в вектор. Поиск осуществляется в обратном направлении,
начиная с элемента с индексом index.
Класс Snake
Самое время вернуться к нашей змее. В прошлой главе мы рассматривали пример
программы управляемой точки. Теперь расширим это приложение до управля-
емой змейки. Сейчас мы заложим основу и реализуем скелет нашей будущей игры.
Первым делом нарисуем собственно управляемую змейку, которая будет состо-
ять из маленьких кусочков, каждый из которых будет представлен готовой кар-
тинкой. По сценарию змея должна не просто ползать по экрану, а охотиться на
воображаемую добычу, поэтому также нам потребуется дополнительная логика
для управления добычей и процессом «поедания». И самое главное: если змея
укусит свой хвост или упрется в стену, то она неминуемо погибнет.
Основной класс, который будет реализовывать всю логику игры, будем наследо-
вать от рассмотренного в прошлой главе класса Canvas, который отвечает нашим
----------------------- Page 55-----------------------
56 Глава 5 . Структуры и форматы данных в J2ME
требованиям рисования произвольной графики, а также организации управления
на низком уровне:
private class Snake extends Canvas
Для реализации гибкого змеиного тела нам потребуется пять различных элемен-
тов: голова, хвост, тело и два варианта сгибов тела, различающихся направлением
поворота. Эти типы частей в нашем классе будут представлены следующими кон-
стантами:
// константы , определяющие тип части тела змеи
private int HEAD - 0 ; // голова змеи
private int TAIL - 1; // хвост змеи
private int BODY - 2 : // тело змеи
private int ACLOCKWISEJURN = 3 // сгиб тела против часовой стрелки
private int CLOCKWISEJRJRN = 4 ;// сгиб тела по часовой стрелке
В каждый момент времени мы точно знаем, в какую сторону ползет змея, и в соот-
ветствии с этим делаем необходимые проверки и продвигаем змею в нужном на-
правлении. Возможные направления представлены следующими константами:
// константы , определяющие направление движения
private int UP =0 : // вверх
private int LEFT = 1; // влево
private int RIGHT = 2 // вправо
private int DOWN • 3 : // вниз
private int direction // текущее направление движения змеи
Текущее направление движения представлено полем int direction, которое при-
нимает значение одной из констант, определяющих направление движения.
Поскольку мы взялись за красивую качественную игру, то и змея наша должна
выглядеть достаточно реалистично, поэтому в зависимости от направления дви-
жения различные части змеи будут выглядеть по-разному. Заготовим для каждой
возможной части отдельные картинки и разместим их в папке ресурсов приложе-
ния /res. Здесь можно проявить фантазию и художественное мастерство. Я нари-
совал вот такие части (рис. 5.1).
HeadLeft. HeadUp . HeadDown . HeadRight.
png png png png
BBBB
TailLefl. TailUp. TailDown. TailRight.
png png png png
ваш
BodyLeft. BodyUp.
png png
ili
TurnDownLeft. TurnUpLefl. TurnDownRight. TurnUpRight.
png png png png
Рис. 5 . 1 . Части змеи выглядят по-разному в зависимости от направления
----------------------- Page 56-----------------------
Класс Snake 5 7
Частей змеи у нас фиксированное количество, не требующее динамического до-
бавления или удаления, поэтому в данном случае достаточно обычного массива,
который также будет являться членом класса Snake:
private Image[] images;
Размер картинки также будем хранить в отдельном поле:
private int imageSize:
Каждая часть тела змеи будет представлена объектом класса SnakePart, который
будет содержать тип части, координаты и направление движения. Если для уп-
равления точкой нам было достаточно хранить только текущие координаты, то
теперь нам потребуется хранилище для всех частей. Класс Vector полностью отве-
чает нашим требованиям, поэтому класс SnakePart должен быть наследован от клас-
са Object. Согласно правилам хорошего тона программирования работу с внут-
ренними полями нового класса организуем через дополнительные методы. Код
класса SnakePart выглядит следующим образом:
private class SnakePart extends Object {
private int x: // координата х
private int у: // координата у
private int part : // код части тела змеи
private int dir ; // код направления движения
// конструктор, принимающий все поля как аргументы
public SnakePart(int _x, int _y. int _part, int _dir) {
x = _x:
У - _У:
part = _part :
dir = _dir :
}
// получить код части тела змеи
private int getPartO { return part : }
// получить код направления движения
private int getDirO { return dir : }
// получить координату х
private int getX O { return x; }
// получить координату у
private int getY O { return y ; }
// установить тип части и направление
private void setPartDir(int _part . int _dir) {
// если второй аргумент 0 . то
// направление остается неизменным
part = _part :
ifCJir != 0 ) dir - _dir ;
Все элементы змеи будет хранить поле
private Vector snake;
----------------------- Page 57-----------------------
58 Глава 5 . Структуры и форматы данных в J2ME
Также для организации графики нам потребуются следующие поля:
private int width : // ширина экрана
private int height ; // высота экрана
private int xHead : // координата х головы змейки
private int yHead : // координата у головы змейки
После всех необходимых приготовлений и организации структуры класса можно
переходить к конструированию нового объекта класса Snake:
// конструктор класса Snake
public SnakeO {
// конструктор родительского класса
superO :
// создать массив для картинок-частей змейки
images • new Image[20] :
// создать обьекты картинок для каждой части
try {
// голова во всех направлениях
images[0] = Image.createlmageC'/HeadUp.png") :
images[l] » Image.createlmageC'/HeadLeft.png") ;
images[2] = Image.createlmageC'/HeadRight.png") ;
images[3] = Image.createlmageC'/HeadDown.png") :
// хвост во всех направлениях
images[4] = Image.createlmageC'/TailUp.png") ;
images[5] = Image.createlmageC'/TailLeft.png"):
images[6] = Image.createlmageC'/TailRight.png") :
images[7] = Image.createlmageC/Tail Down.png") :
// тело
images[8] - Image.createlmageC"/BodyUp.png") :
images[9] » Image.createlmageC"/BodyLeft.png") ;
images[10] = images[9] :
images[ll] = images[8] :
// сгиб тела против часовой стрелки
images[12] = Image.createlmageC"/TurnDownLeft.png") ;
images[13] = Image.createlmageC'/TurnUpLeft.png") ;
images[14] = Image.createlmageC"/TurnDownRight.png") ;
images[15] » Image.createlmageC"/TurnUpRight.png") ;
// сгиб тела по часовой стрелке
images[16] - images[14] ;
images[17] = images[12] ;
images[18] = images[15] :
images[19] = images[13] ;
}
catch (IOException ioe ) {}
// получить размер одного элемента змеи
imageSize = images[O].getWidth() ;
/7 создать вектор , хранящий элементы змейки
----------------------- Page 58-----------------------
Класс Snake 5 9
snake = new VectorO :
// получить ширину экрана
width = getWidthO ;
// получить высоту экрана
height = getHeightC) :
// установить координаты головы в центре экрана
// для транспортабельности координаты кратны imageSize
xHead = ((width/2)/imageSize)*imageSize :
yHead = ((height/2)/imageSize)*iiriageSize;
// создать элемент головы змейки
SnakePart parts - new SnakePart(xHead,yHead.HEAD.UP) ;
// добавить элемент в вектор
snake.addElement(partS);
// создать элемент тела змейки
parts = new SnakePart(xHead.yHead+imageSize.BODY,UP) ;
snake.addElement(partS) :
// создать элемент хвоста змейки
parts - new SnakePart(xHead.yHead+2*imageSize.TAIL.UP) ;
snake.addElement(partS);
// установить текущее направление движения
direction = UP ;
// сбросить флаг окончания игры
gameOverFlag = false :
}
Как описывалось в прошлой главе, для любых классов, расширяющих класс Canvas,
обязателен к реализации метод pai nt (), отвечающий за перерисовку экрана. Прой-
дем по всем элементам вектора и выведем на экран соответствующие части змей-
ки согласно их координатам:
public void paint(Graphics g ) {
// очистить экран
g.setColor(OxFFFFFF) :
g.fillRect(O.0,width.height);
g.setColor(OxOOOOOO);
// нарисовать рамку
g.drawRect(l.l,width-3.height-3);
// получить длину змейки
int snakeLen = snake.sizeO ;
// если поднят флаг конца игры
if ( gameOverFlag ) {
// вывести сообщение
g.drawStringC'GAME OVER" .width/2.height/2-10.g.HCENTER|g.TOP):
// вывести счет , равный длине змейки
String score - new StringC'YOUR SCORE : "+snakeLen) :
g.drawString(score,width/2.height/2+10.g.HCENTER|g.TOP) :
return :
----------------------- Page 59-----------------------
6 0 Глава 5 . Структуры и форматы данных в J2ME
} else {
// для каждого элемента змейки
for ( int 1-0 : i<snakeLen ; i++ ){
// вывести картинку , индекс которой вычисляется
// с помощью комбинации кода части тела и кода направления
g.drawImage(images[4*((SnakePart)snake.elementAt(i)).getPart()+
((SnakePart)snake.elementAt(i)).getDir()].
((SnakePart)snake.elementAt(i)).getX().
((SnakePart)snake.elementAt(i)).getY().
g.HCENTER | g.VCENTER) ;
Теперь добавим управление нашей змейкой. Для этого реализуем метод key Pressed (i nt
keyCode). Перед тем как осуществлять передвижение, будем проверять, можно ли
ползти в заданном направлении. Если на пути стена или собственный хвост, то
поднимаем флаг конца игры.
public void keyPressed(int keyCode ) {
// если флаг конца игры не поднят
if ( IgameOverFlag )!{
switch (keyCode ) { i
// проверить , можно ли передвигаться в заданном направлении .
// и вызвать соответствующую функцию передвижения
case KEY_NUM2 :
checkMoveUHead, yHead-imageSize) ; moveUpO : break :
case KEY_NUM4 :
checkMoveCxHead- imageSize . yHead) : moveLeftO : break :
case KEY_NUM6 :
checkMove(xHead+ imageSize . yHead) : moveRightO ; break :
case KEY_NUM8:
checkMove(xHead . yHead+ imageSize) : moveDownO : break :
}
// вызвать перерисовку экрана
repaintO :
// проверить возможность передвижения
// в точку с координатами (хН.уН )
public void checkMove(int xH , int yH ) {
// для каждого элемента змеи
for ( int 1-3 ; i<snake.size()-l : i++ ) {
// если проверяемая точка совпадает с координатами элемента
if ( хН — ((SnakePart)(snake.elementAt(i))).getX( ) &&
yH == ((SnakePart)(snake.elementAt(i))).getYO )
// поднять флаг конца игры
----------------------- Page 60-----------------------
Класс Snake 6 1
gamedverFlag=triie;
// передвижение влево
private void moveLeftO {
// если текущее направление не направо
if ( direction!=RIGHT ) {
// сдвинуть координату головы влево
xHead--imageSize :
// проверить выход за левую границу
if ( xHead < 4 ) { gameOverFlag=true ; return ; }
// получить первый элемент змеи
SnakePart head - (SnakePart)(snake.firstElementC)) :
// если текущее направление вверх
if ( direction—U P )
// заменить голову на сгиб против часовой стрелки
head.setPartDi r(ACLOCKWISE_TURN.LEFT) :
else {
// если текущее направление вниз
if ( direct!on==DOWN )
// заменить голову на сгиб по часовой стрелке
head.setPartDi r(CLOCKWISE_TURN.LEFT) ;
else // если текущее направление влево
// заменить голову на тело
head.setPartDir(BODY.LEFT);
}
// создать новый элемент для головы
SnakePart sPart = new SnakeParttxHead.yHead.HEAD.LEFT) ;
// добавить голову первым элементом вектора
snake.insertElementAt(sPart.O) :
// продвинуть хвост
// удалить последний элемент
snake.removeElement(snake.1astElement()):
// назначить хвостом предпоследний элемент
((SnakePart)snake.lastElementО).setPartDi r(TAIL,-1) ;
// установить текущее направление
direction - LEFT :
Передвижение в остальных направлениях реализуйте самостоятельно в функци-
ях moveUp(), moveRight(), moveDown() по образу и подобию приведенной выше функ-
ции moveLeftO.
Итак, основа игры написана. Приложение уже можно компилировать и запускать.
На экране мы увидим вот такого милого червячка (рис. 5.2).
----------------------- Page 61-----------------------
62 Глава 5 . Структуры и форматы данных в J2ME
Управление змейкой уже реализовано, поэтому можно смело делать первые шаги,
точнее, первые ползки. Покрутимся в разные стороны и убедимся, что все пово-
роты реализованы без ошибок. При попытке уползти за пределы экрана мы долж-
ны получить сообщение о завершении игры (рис. 5.3).
Щ$Щ№&- # ( l ) Nokia 7210 SDK
File Tools Help
Рис. 5.2 . Змейка в начале игры Рис. 5.3 . «Game over!»
Итак, все работает отменно, кроме главного момента — змея не растет. Как мы
помним из аналогов, для роста змее требуется вкусная и здоровая пища, которая
появляется на экране случайным образом. Пища в нашей игре будет представле-
на сердечком, картинку для которого сохраним в файле heart.png, находящемся
в папке /res вместе с остальными картинками элементов змейки. Для реализации
правильного питания в класс Snake добавим несколько полей:
private Image heart : // картинка сердца
private in t xHeart: // координата х сердца .
private in t yHeart : // координата у сердца
private boolean eatFlag : // флаг удлинения змеи
Также нам понадобится метод вычисления координат для случайного размеще-
ния сердечка на экране. Здесь нам потребуется генератор случайных чисел,
----------------------- Page 62-----------------------
Класс Snake 6 3
представленный в языке J2ME классом Random. Очередное случайное число гене-
рируется с помощью метода int nextlnt О. Заметим, что число может быть отрица-
тельным, поэтому требуется получить его абсолютное значение, используя метод
класса Math int absdnt a).
// получить новые координаты для сердца
public void setNewHeartO {
// получить количество возможных позиций по координате х
int xPartMaxNum • (width-5)/imageSize;
// получить количество возможных позиций по координате у
int yPartMaxNum = (height-5)/imageSize :
iI получить длину змеи
int len • snake.sizeO :
// создать объект генератора случайных чисел
Random rnd = new RandomO :
// флаг корректной генерации
boolean setFlag = false ;
// повторяем , пока не сгенерируем корректные координаты сердца
while ( IsetFlag ) {
// поднять флаг корректной генерации
setFlag - true :
// сгенерировать случайную координату х . кратную imageSize
xHeart = imageSize*(Math.abs(rnd.nextInt())PxPartMaxNum+l):
// сгенерировать случайную координату у , кратную imageSize
yHeart - imageSize*(Math.abs(rnd.nextInt()UyPartMaxNum+l) ;
// проверить совпадение координат сердца
// с координатами элементов змеи
for ( int i=0 : i<len-l : i++ )
if ( xHeart == ((SnakePart)(snake.elementAt(i))).getX( ) &&
yHeart == ((SnakePart)(snake.elementAt(i))).getYO )
// опустить флаг корректной генерации
setFlag • false :
В конструкторе класса Snake добавим создание объекта картинки для сердечка
heart = Image.createlmage("/heart.png") и в самом конце вызовем метод вычисле-
ния координат для этой картинки setNewHeart(). В методе paint О вызовем про-
рисовку картинки : g.drawlmage(heart,xHeart .yHeart.д.HCENTER | g.VCENTER).
Осталось реализовать логику удлинения змеи при встрече с сердечком. В методе
checkMove() при попытке передвижения проверяем, совпадают ли координаты пред-
полагаемого передвижения с координатами сердечка, и в случае равенства под-
нимаем флаг удлинения змеи:
i f ( хН == xHeart && уН — yHeart )
eatFlag • true :
else • .
eatFlag * false :
----------------------- Page 63-----------------------
64 Глава 5 . Структуры и форматы данных в J2ME
В методах передвижения moveUp (), moveLeft О, moveRi ght (), moveDown (), перед тем как
продвинуть хвост, удаляя последний элемент вектора, нужно проверить, не под-
нят ли флаг удлинения змеи. В случае удлинения вызовем метод генерации но-
вых координат для сердечка setNewHeart(), а продвижение хвоста производить не
будем:
// если флаг удлинения змеи поднят
if (eatFlag )
// получить новые координаты для сердца
setNewHeartO :
else {
// продвинуть хвост
// удалить последний элемент
snake.removeElement(snake.1astElement()) :
// назначить хвостом предпоследний элемент
(CSnakePart)snake.lastElementС)).setPartDir(TAIL.-l);
Вот и все, самое страшное уже позади. Теперь подключаем все необходимые пакеты:
<&(1) Nokia 7210 SDK import javax.microedition.midlet.MIDlet;
import javax.microedition.lcdui.Image;
import javax.microedition.lcdui.Canvas:
import javax.microedition.lcdui.Graphics:
import javax.microedition.Icdui.Display:
import Java.util.Vector:
import Java.util.Random:
import java.io.IOException:
компилируем получившееся приложение и ис-
правляем ошибки, без которых не обойтись, осо-
бенно на первых этапах освоения нового языка.
Запускаем приложение и посвящаем час-другой
превращению червяка в анаконду (рис. 5.4).
Заметим, что мы написали только основу для
игры, ведь змейка пока что не ползает самосто-
ятельно, так что играть в такую игру будет ин-
тересно только детям дошкольного возраста,
и то лишь первые десять минут. Также хотелось
бы расширить игровое поле на весь экран, реа-
лизовать систему хранения таблицы рекордов
и многое другое, чем мы и займемся в следу-
ющих главах.
Класс Stack
В заключении главы рассмотрим еще один класс-
Рис. 5.4. Змейка в действии контейнер, реализующий структуру данных стек.
----------------------- Page 64-----------------------
Класс Stack 6 5
Данная структура характерна особым подходом к добавлению и удалению элемен-
тов. Принцип хранения данных LIFO (Last In, First Out) означает, что извлечь из
структуры можно только тот элемент, который был добавлен последним. Для ясно-
сти проведем аналогию с узким стаканом, в который можно складывать различные
предметы, но достать можно лишь тот предмет, который находится выше всех.
В языке J2ME класс Stack реализован на основе класса Vector, расширяя его функ-
циональность следующими методами:
• Object push (Object item)—добавляет элемент на вершину стека;
• Object pop О — извлекает элемент с вершины стека;
• Object peek () — возвращает элемент с вершины стека без его извлечения;
• boolean empty() — возвращает значение true, если стек пуст.
ш int search(Object о) — возвращает позицию искомого объекта относительно
вершины стека. Если объект содержится в стеке несколько раз, то возвращает-
ся позиция самого верхнего; если элемент не содержится в стеке, то возвраща-
ется значение — 1.
* * * \
Итак, мы рассмотрели в теории и применили на практике основные типы и струк-
туры данных языка J2ME. Корректное обращение с данными и преобразование
типов данных предотвратит массу возможных ошибок, поэтому к этому моменту
стоит отнестись особо внимательно. Перед разработкой любого приложения сле-
дует хорошо обдумать, каким образом будут храниться данные приложения, ка-
кова область применения тех или иных данных, и в зависимости от этого уже про-
ектировать структуру классов и самого мидлета.
----------------------- Page 65-----------------------
Глава 6
Организация
многопоточных приложений
В прошлой главе мы реализовали основную часть игры «Змейка», придержива-
ясь классических принципов игры. Используя стандартное управление, можно
ползать по всему экрану и собирать сердечки, удлиняя змею. При столкновении
змеи со стеной или с собственным телом игра заканчивается. Пока что главное
отличие от классической игры заключается в том, что змейка стоит на месте, если
не нажимать управляющие клавиши. Таким образом, эта увлекательная и разви-
вающая игра теряет всякий смысл.
Основная проблема в реализации самостоятельного продвижения змеи такова,
что если программно зациклить какое-то действие, то приложение не будет иметь
возможности выйти из цикла и обработать нажатие функциональных клавиш. На
помощь в решении этой проблемы приходит многозадачность мобильной опера-
ционной системы.
Многозадачность
В современных операционных системах, в том числе и мобильных, реализовано
понятие многозадачности, которое заключается в одновременном выполнении не-
скольких программ. Слово «одновременно» здесь надо понимать так: каждой из
программ поочередно отводится определенное количество процессорного време-
ни, в течение которого программа выполняется, а затем снимается с процессора,
уступая свое место другой программе.
Сама программа, в нашем случае — мидлет, также может создавать несколько па-
раллельных процессов, которые будут выполняться одновременно. Такие процес-
сы в оригинале языка Java называются thread. Понятие thread в русской литерату-
ре переводят по-разному. В буквальном переводе термин обозначает «нить», но
мы все же не швеи-мотористки, а какие-никакие программисты. Часто thread пе-
реводят как «поток», но, к сожалению, этот термин уже занят потоками ввода-
вывода (streams), на которых мы остановимся позже. Так что в данной главе мы
будем пользоваться транслитерацией термина — «тред».
В любом мидлете существует, по крайней мере, хотя бы один тред — главный тред
выполнения мидлета, из которого можно создать другие треды для одновремен-
ного выполнения. В языке Java треды представлены объектами класса Thread.
----------------------- Page 66-----------------------
Класс Thread 6 7
Класс Thread
Класс Thread реализует интерфейс Runnable, который содержит объявление лишь
одного метода: voi d run () . В методе run () должны быть реализованы все действия,
предусмотренные для выполнения в конкретном треде. Сам класс Thread содер-
жит пустую реализацию метода run (), поэтому задать действия для создаваемого
треда можно следующими двумя способами:
• расширить класс Thread и переопределить метод гип();
• реализовать интерфейс Runnable.
Мы будем пользоваться вторым способом, поскольку в наших примерах класс,
создающий тред, уже наследован от какого-то другого класса, а множественное
наследование в языке Java не поддерживается. Для каждого способа класс Thread
содержит отдельный конструктор:
• Thread О — конструктор, не принимающий аргументов; вызывается из конст-
руктора класса-потомка, расширяющего класс Thread;
• Thread(Runnable target) — конструктор, принимающий в качестве аргумента
объект, реализующий интерфейс Runnabl e;
После того как объект треда создан, предписанные в методе run О действия еще не
начинают выполняться. Для управления тредом существует несколько методов
класса Thread:
• void start () — начать выполнение треда. Виртуальная машина вызывает метод
run(), который никогда не вызывается программистом явно. После вызова ме-
тода start О начинается параллельное выполнение программы в двух направ-
лениях: тред идет своей дорогой, а поезд идет своей;
• void sieepClong mill is) — приостанавливает выполнение треда на mill is мил-
лисекунд.
В этом месте мы остановимся и попробуем применить изученную теорию на прак-
тике. Вернемся к программе фотоальбома, который мы реализовали во второй гла-
ве. Усовершенствуем эту программу таким образом: добавим показ слайд-шоу, то
есть автоматическую смену картинок на экране.
Организуем управление автоматической сменой картинок в отдельном треде. Для
этого в основном классе нашего фотоальбома SlideShow реализуем интерфейс
Runnable. Теперь декларация класса будет выглядеть следующим образом:
public class SlideShow extends MIDlet implements CommandListener. Runnable
Следует также добавить в класс SI i deShow новое поле для объекта треда:
private Thread thread;
Как уже было сказано ранее, все действия, выполняемые тредом, следует реали-
зовать в методе гип():
public void runО {
// вечный цикл
while (true) {
try {
// удалить из формы текущую картинку
----------------------- Page 67-----------------------
68 Глава 6 . Организация многопоточных приложений
form.delete(O):
// добавить в форму новую картинку
// увеличиваем текущий номер картинки и берем
// остаток от деления на общее число картинок
setlmage("/"+((siideNum++)PmaxSlideNum+1)+".png");
// остановить выполнение треда на 3 секунды
thread.sleep(3000) :
// обработать исключительную ситуацию
} catch (InterruptedException e ) {
В методе startAppC) осталось создать объект треда и дать команду для начала его
выполнения:
// создать объект треда
thread = new Thread(this) ;
// начать выполнение треда
thread . startO ;
Заметим, что после запуска слайд-шоу остановить смену картинок будет очень
проблематично, поэтому стоит все же воздержаться от бесконечных циклов и за-
вести флаг, сбрасывая который, можно организовать завершение работы треда.
Приоритеты тредов
Как уже было отмечено, одновременное выполнение нескольких тредов доста-
точно условно, поскольку физически в каждый момент времени выполняется толь-
ко один тред. Переключение тредов и количество выделенного процессорного
времени зависит от операционной системы, однако программист тоже может по-
влиять на этот процесс. Иногда некоторым тредам требуется выделить большее
или меньшее количество процессорного времени. Технология мультитрединга
позволяет определить приоритеты выполнения для каждого треда.
Приоритеты тредов представлены целыми числами в диапазоне от 1 до 10. Чем
выше значение приоритета, тем больше времени выделяется треду операционной
системой. Также в классе Thread определены константы MIN_PRIORITY, NORM_PRIORITY
и MAX_PRIORITY, представляющие значения минимального, нормального и макси-
мального приоритетов соответственно.
Работа с приоритетами осуществляется с помощью следующих методов класса Thread:
• void setPrioritydnt newPriority) — установить новый приоритет треду. При
попытке выставить приоритет, выходящий за допустимый диапазон, будет
сформировано исключение IllegalArgumentException;
• int getPriorityC) — возвращает текущий приоритет треда.
Продемонстрируем на небольшом примере многозадачность системы. Запустим
параллельно два треда, каждый из которых в цикле будет выводить сообщения
----------------------- Page 68-----------------------
Приоритеты тредов 69
в системную область (рис. 6.1). Главный тред будет выполнять основной код мид-
лета в методе startApp(), а для второго треда создадим отдельный класс SecondThread:
public void startAppO {
// создать второй тред
SecondThread thread = new SecondThreadO ;
// запустить второй тред
thread.startO :
// в цикле выводить сообщения в системную область
for(int 1-1 ; i<=5 ; i++ )
System.out.printlnC'Main Thread: "+i) :
private class SecondThread extends Thread {
// в цикле выводить сообщения в системную область
public void run( ) {
fordnt 1-1 : i<=5 : 1++ )
System.out.println("Second Thread: "+i) :
IS J2ME Wireless Toolkit - SlideShm»
i l e f in • |, • H e l p
I O a New Project... | jjP Open Project % Settings \H$ Bum % Run
ifaultGrayPhone
Main Thread: 1
Second Thread: 1
|Main Thread: 2
Second Thread: 2
Main Thread: 3
Second Thread: 3
Main Thread: 4
Second Thread: 4
Main Thread: 5
Second Thread: 5
«I • illllli Ш
Рис. 6 . 1 . Результаты параллельной работы двух тредов с одинаковым приоритетом
На рисунке мы видим, что переключение тредов работает, как швейцарские часы,
и сообщения от каждого треда выдаются по очереди, несмотря на то, что вывод
сообщений в программе представлен единым непрерывным циклом.
Попробуем теперь перед запуском второго треда выставить ему повышенный при-
оритет thread.setPriority(Thread.MAX_PRIORITY) и посмотрим, что же из этого по-
лучится (рис. 6.2).
Видим, что картина существенно поменялась: главный тред не начал выполнять-
ся, пока тред с повышенным приоритетом не закончил работу. Не исключена си-
----------------------- Page 69-----------------------
70 Глава 6 . Организация многопоточных приложений
туация, что при наличии множества тредов с высоким приоритетом какой-то низ-
коприоритетный тред вообще не получит управления. Такую ситуацию можно
предотвратить с помощью следующих методов класса Thread:
• void yield() — принудительное переключение системы на выполнение следу-
ющего треда;
• void joinC) — ожидание завершения работы треда; останавливает работу про-
граммы, из которой был вызван данный тред.
jZME Wireless Toolkit - SlideShow
File Edit Project Help . . .
. | ; j g Open Project ' Buid Run
Device: JDefaultGtayPhone
•
Second Thread : 1
Second Thread : 2
Second Thread : 3
Second Thread : 4
Second Thread : 5
Kain Thread : 1
Main Thread : 2
Main Thread : 3
Main Thread : 4
Main Thread : 5
-i i И
'
Рис. 6.2 . Результаты параллельной работы двух тредов с разным приоритетом
При создании тред получает приоритет создавшего его треда. Как правило, все треды
работают с нормальным (NORMPRIORITY) приоритетом. Треды, ожидающие какого-то
события, например нажатия кнопки, могут получить повышенный приоритет, а тре-
ды, выполняющие длительную, ресурсоемкую работу — пониженный приоритет.
Отследить состояние системы с точки зрения выполняющихся в конкретный мо-
мент тредов можно с помощью следующих методов:
• boolean isAiiveO — возвращает значение true, если тред был запущен ид о сих
пор не прекратил выполнение;
• Thread currentThreadC) — возвращает тред, выполняемый в данный момент вре-
мени;
• i nt acti veCount С) — возвращает количество тредов, выполняемых системой в дан-
ный момент времени.
Синхронизация тредов
Основная сложность при работе с тредами заключается в том, что разные треды, по-
рожденные одним мидлетом, имеют общее адресное пространство, то есть одни и те
же данные могут быть доступны из разных тредов. В чем здесь криминал? Рассмот-
рим, например, ситуацию, когда два треда работают с одним и тем же вектором. Пер-
вый тред получает длину вектора, затем происходит смена выполняемого треда,
и второй тред удаляет элементы из этого же вектора. Первый тред возобновляет ра-
----------------------- Page 70-----------------------
Синхронизация тредов 7 1
боту и с удивлением обнаруживает, что получен-
ная длина вектора не соответствует действитель-
ности, формирует исключение неверного индекса
и падает с таким вот грохотом (рис. 6.3).
Это далеко не единственный пример. Контек-
стное переключение тредов может произойти
в любой момент. В самой простейшей операции
между вычислением и присваиванием резуль-
тата может вклиниться другой тред и поменять java/lang/ArraylndexOut
OfBoundsException
данные. Результаты такой путаницы могут быть
совершенно непредсказуемы.
Описанная проблема разрешается в языке Java
при помощи технологии синхронизации тредов. Back
Идея заключается в блокировке тредом доступа
к объекту на время работы с общими данными.
Перед тем как начать работать с данными, тред
ставит блок на доступ к объекту, и все осталь-
ные треды при необходимости работы с этими
данными ожидают снятия блока.
Блокировка осуществляется с помощью опера-
тора synchronized(Object obj) {...}. В фигурных
скобках размещается критическая секция про-
граммы — код приложения, требующий синхро-
низации. В качестве аргумента obj оператору пе-
редается объект, который будет заблокирован.
Если в момент выполнения оператора synchro-
nized синхронизируемый объект уже заблокиро-
ван каким-то другим тредом, то выполнение при- Рис. 6.3 . Демонстрация ошибки при
остановится до тех пор, пока блок не будет снят. синхронизации
Если какой-то метод требует синхронизации всех
своих операторов, то весь метод может быть помечен как synchronized, например:
public synchronized void someActionWithDataO {...}
В таком случае блокируется объект this, вызвавший данный метод. Блок снима-
ется только после завершения работы метода.
Очень неприятная ситуация может возникнуть при перекрестной блокировке, когда
один тред получил управление синхронизированным объектом А, а второй — синх-
ронизированным объектом В. После этого первый тред пытается вызвать синхрони-
зированный метод объекта В, а второй — аналогичный метод объекта А. Оба треда
застрянут и будут ждать освобождения объектов, блокированных друг другом, а про-
грамма с такой структурой тредов просто зависнет. В теории такая ситуация называ-
ется взаимоблокировкой (deadlock). Так что при разработке приложений следует
очень внимательно отнестись к вопросам корректной реализации синхронизации.
В языке Java существует способ предотвращения взаимоблокировки. Внутри син-
хронизированной критической секции можно приостановить работу треда с по-
следующим ее возобновлением. Класс Object содержит несколько методов wait,
которые откладывают выполнение треда. Отличие этих методов от метода sleep
----------------------- Page 71-----------------------
72 Глава 6 . Организация многопоточных приложений
класса Thread заключается в том, что во время приостановки работы треда блоки-
ровка с синхронизируемого объекта снимается. Методы класса Object, отвеча-
ющие за остановку и возобновление тредов:
• void wait() — текущий тред снимает блокировку с объекта и приостанавлива-
ется до тех пор, пока из какого-либо другого треда не будут вызваны методы
notify О или notifyAlK);
• void wait (long timeout) — текущий тред снимает блокировку и откладывает
выполнение, пока из какого-либо другого треда не будут вызваны методы
noti fy() или noti fyAI 1 () или не истечет тайм-аут длиной timeout миллисекунд;
• voidwaitClongtimeout. intnanos) — аналогичен предыдущему методу, с точнос-
тью тайм-аута до наносекунд, которая задается аргументом nanos. Вызов этих
трех методов допускается только из синхронизированных методов или крити-
ческих секций программы;
• void notify() — возобновляет работу одного приостановленного треда, произ-
вольно выбранного системой;
• void noti fyAI 10 — возобновляет работу всех приостановленных на данном объекте
тредов. Фактически начинает работу только один тред, первым восстановивший
блокировку объекта, остальные ждут снятия блока в обычном режиме.
Следует заметить, что при обращении к общим данным синхронизация нужна
только в том случае, если один из тредов изменяет данные. В случае, когда все
треды просто читают данные, не изменяя их, синхронизация тредов не требуется.
Класс Snake
Теперь у нас достаточно информации, чтобы добавить в игру самостоятельное
продвижение змейки. Для этого необходимо реализовать в классе Snake интер-
фейс Runnabie, а в методе run О организовать цикл продвижения змеи в текущем
направлении. Декларация класса Snake будет выглядеть теперь так:
private class Snake extends Canvas implements Runnable
В методе run() будем проверять текущее направление движения змейки и в зави-
симости от этого вызывать соответствующий метод продвижения. Обратим вни-
мание, что метод run О и метод keyPressed(int keyCode) будут изменять данные од-
ного и того же объекта, поэтому здесь потребуется синхронизация. В объявлении
метода keyPressed добавим ключевое слово synchronized, а в методе гип() выделим
критическую секцию кода:
public void run() {
// пока не поднят флаг конца игры
while (IgameOverFlag ) {
// синхронизировать с управлением
synchronized (this ) {
// продвинуть змейку в текущем направлении
if(direction==UP) {checkMove(xHead . yHead-7) ; moveUpO: }
if(direction==LEFT) {checkMove(xHead-7 , yHead) : moveLeftO: }
if(direction==RIGHT) {checkMove(xHead+7 . yHead) ; moveRightO: }
if(direction==DOWN) {checkMove(xHead, yHead+7) : moveDownO: }
----------------------- Page 72-----------------------
Класс Snake 7 3
// вызвать перерисовку экрана
repaintO :
try {
// приостановить тред на speed миллисекунд
Thread.sieep(Ievel*speed) :
} catch (InterruptedException e ) {
Заметим, что в класс Snake мы ввели два новых поля, private byte level и private int
speed, которые будут отвечать за скорость передвижения нашей змейки. Поле level
содержит уровень сложности игры, а поле speed — коэффициент ускорения для
вычисления времени, на которое «засыпает» тред перед очередным продвижением
змейки. Чем меньше этот параметр, тем быстрее будет двигаться наша змейка.
В конструкторе установим speed = 100, level = 5, а в методе checkMove после каждого
«съеденного» сердечка будем увеличивать скорость передвижения змейки:
// проверить совпадение координат передвижения
// с координатами сердца
if ( хН == xHeart & & уН == уHeart ) {
eatFlag = true ;
// увеличить скорость
speed--;
} else
eatFlag - false :
Все, что осталось сделать, это создать в методе startApp() новый тред и запустить его:
// создать объект треда автоматического передвижения
Thread moveThread = new Thread(curSnake);
// начать выполнение треда
moveThread. s t a r t O :
Компилируем, запускаем и наслаждаемся процессом! Играть стало интересней,
играть стало веселее, а корпорация Nokia кусает локти от зависти. На достигну-
том мы, конечно же, останавливаться не будем и в следующих главах еще усовер-
шенствуем нашу игру. Добавим картинок-заставок, таблицу рекордов, возмож-
ность выбора уровня игры и прочие прелести. Оставайтесь с нами, не переключайте
канал, дальше будет еще интереснее!
* * *
В этой главе мы опробовали многозадачность виртуальной машины Java на не-
хитрых примерах. Иногда без создания нового треда просто не обойтись как это
(
было в случае со змейкой. При создании большой полноценной программы сле-
дует задуматься, какие из частей программы могут выполняться одновременно.
Зачастую оправдано создание отдельных тредов для каждой подзадачи. После
создания тредов следует подумать над распределением приоритетов. Особое вни-
мание следует уделить синхронизации тредов, если они обращаются к одним
и тем же данным программы.
----------------------- Page 73-----------------------
Глава 7
Сохранение данных
и параметров приложения
Как показывает практика, люди делятся на две категории: на тех, кто прошел Doom
от начала до конца, и на тех, у кого не хватило терпения, реакции или секретных
кодов, чтобы выпустить кишки всем злобным монстрам, которыми щедро нашпи-
гован каждый новый уровень. Поколение игры Doom хорошо знает основной прин-
цип ЗО-стрелялок: «Главное — не забыть сохраниться!» В этой главе мы рассмот-
рим, как организовать долговременное хранение данных в мобильном телефоне.
Хранение данных организовано в J2ME с помощью системы управления запися-
ми (Record Management System), реализованной в пакете javax.microedition.rms.
Данные представлены записями (массив байтов типа byte[]), которые помеща-
ются в хранилище записей (Record Store) и не стираются при закрытии приложе-
ния, перезагрузке телефона или замене батареи. Размер и физическое местополо-
жение хранилища в памяти телефона зависят от конкретной модели аппарата.
Хранилище жестко привязано к мидлету, и когда вы удаляете приложение из сво-
его телефона, то все данные, относящиеся к нему, удаляются вместе с ним. По
сути, хранилище записей — это база данных, которая может быть представлена
следующей схемой (рис. 7.1).
Хранилище записе й "Test Storage"
"Test Storage" <record 1>
<record 2>
"Blondes"
<record N>
"SNAKE"
"Blondes"
"SNAKE" <record 1>
<record <record 2>
<record 2>
<record N>
<record N>
Рис. 7 . 1 . Схема хранилища записей
----------------------- Page 74-----------------------
RecordStore — хранилище записей 7 5
Применения для RMS могут быть разные, от сохранения параметров и пользова-
тельских настроек приложения до поддержки полноценной адресной книги с ис-
черпывающей личной информацией и гибким поиском. В игре, которую мы пи-
шем, мы будем использовать систему управления записями для хранения уровня
сложности игры, а также таблицы рекордов.
RecordStore — хранилище записей
Итак, хранилище записей представлено объектом класса RecordStore, работа с ко-
торым начинается с метода открытия хранилища для дальнейшей работы static
RecordStore openRecordStore(StringrecordStoreName. booleancreatelfNecessary). Первый
аргумент метода — уникальное имя хранилища. Второй аргумент указывает, дол-
жно ли быть создано новое хранилище с таким именем, если оно еще не существу-
ет. Имя хранилища чувствительно к регистру, длина имени не должна превышать
32 символа.
Добавление записи в хранилище осуществляется с помощью метода i nt addRecord (byte[ ]
data, int offset, int numBytes). Сохраняемые данные должны быть представлены
последовательностью байтов, содержащихся в массиве data. Переменная offset
указывает, с какой позиции в массиве начинаются данные, предназначенные для
записи, a numBytes определяет размер сохраняемых данных. Метод возвращает
номер новой записи, присвоенный ей при сохранении (recordID).
Рассмотрим вкратце еще некоторые методы хранилища записей RecordStore:
• void deleteRecorddnt recordld) — удалить из хранилища запись с номером
recordld;
• static void del eteRecordStore(StringrecordStoreName) — удалить хранилище за-
писей с именем recordStoreName; \
• 1 ong getLastModi f i ed() — возвращает время последнего изменения данных в хра-
нилище записей в виде количества миллисекунд, прошедших с 1 января 1970 года
до момента последнего изменения (Именно так!.. А кому сейчас легко?);
• String getName() — получить имя хранилища данных;
• 1 nt getNextRecord ID () — возвращает номер, который будет присвоен следующей
добавленной записи;
• int getNumRecordsO — возвращает количество записей, содержащихся на дан-
ный момент в хранилище;
• intgetRecordSize(int recordld) — возвращает размер (в байтах) конкретной за-
писи хранилища с номером recordld;
• int getSizeO — возвращает объем памяти (в байтах), который занимают все
записи хранилища;
• int getSizeAvailableO — возвращает объем памяти (в байтах), доступный для
расширения данного хранилища;
• int getVersion() — получить номер версии хранилища. При каждой операции
с хранилищем, добавлении, удалении или изменении любой его записи, под-
нимается номер версии хранилища;
----------------------- Page 75-----------------------
7 6 Глава 7 . Сохранение данных и параметров приложения
• static String[] listRecordStoresO — возвращает массив имен всех хранилищ
мидлета;
• voidsetRecorddnt recordld, byte[] newData. intoffset. intnumBytes) —изменить
данные, содержащиеся в записи с номером recordld; остальные параметры иден-
тичны параметрам метода addRecord.
Доступ к данным хранилища предоставляет метод byte[] getRecorddnt reccrdID),
где recordID — уникальный номер необходимой записи. То есть, чтобы получить
запись, нужно точно знать ее номер. На этом этапе возникает небольшая пробле-
ма: как запоминать и где хранить эти номера. Вариант сохранения номеров запи-
сей в самом хранилище отпадает — по той же самой причине. На выручку прихо-
дит нумератор — интерфейс RecordEnumerati on, осуществляющий перечисление всех
записей в хранилище.
Record Enumeration —
нумератор списка записей
Используя метод enumerateRecords (RecordFi I ter f i 1 ter, RecordComparator comparator,
boolean keepUpdated), можно получить список записей конкретного хранилища.
Навигация по списку номеров осуществляется с помощью методов нумератора
nextRecordldO и previousRecordldO, которые возвращают по порядку все номера
записей хранилища, или с помощью методов nextRecordO и previousRecordO, ко-
торые возвращают непосредственно записи в формате bytet].
Для наглядности приведем небольшой пример кода программы, которая записы-
вает в хранилище пронумерованные строки, а затем считывает их:
1 mport javax.microedition.rms.RecordStore ;
import javax.mi croedi ti on.rms.RecordEnumerati on ;
import javax.microedi ti on.rms.RecordStoreExcepti on :
try {
String str ;
// открыть хранилище записей с именем "Test Storage "
RecordStore recordStore = RecordStore.openRecordStore
("Test Storage" , true) :
// получить нумератор списка записей
RecordEnumeration re - recordStore.enumerateRecords(null . null , false) :
// добавить в хранилище 5 записей , содержащих пронумерованные строки
fordnt 1-1 ;1<-5:1.-м-) {
str = "String no " + i;
int id = recordStore.addRecord(str.getBytes(),0.str.lengthO) :
System.out.println(str+" was stored with recordID = "+id) :
}
// перестроить список
re.rebuildO :
// получить все записи хранилища по их номерам
while (re.hasNextElementO ) {
----------------------- Page 76-----------------------
RecordEnumeration — нумератор списка записей 77
int id - re.nextRecordldO ;
str = new String(recordStore.getRecord(id)) :
System.out.printlnC'RecordID - "+id+" : "+str) ;
// вернуть нумератор в исходное положение
re . reset О :
// получить все записи хранилища
for (int i-1 : i<=re.numRecords() : i++ ) {
str = new String(re.previousRecordO) ;
System.out.println("Record : "+str) ;
// освободить ресурсы нумератора
re . destroy О ;
// закрыть хранилище записей
recordStore.closeRecordStore() :
catch(RecordStoreException rse) {
System.out.pri ntln(rse.getMessage()) ;
Результат работы примера выглядит так (рис. 7.2).
Device pefaultGrayPhone
String no 1 was stored with recordXD = 1
String no 2 was stored with recordID - 2
String no 3 was stored with recordID * 3
String no 4 was stored with recordID • 4
String no 5 was stored with recordID = 5
RecordID • 5 : String no 5
RecordID = 4 : String no 4
RecordID • 3 : String no 3
RecordID • 2 : String no 2
RecordID • 1 : String no I
Record : String no 1
Record : String no 2
Record : String no 3
Record : String no 4
Record : String no S
—I J
Рис. 7.2 . Пример операций с записями хранилища
----------------------- Page 77-----------------------
7 8 Глава 7 . Сохранение данных и параметров приложения
Рассмотрим методы нумератора, использованные в примере:
• byte[ ] nextRecord () — возвращает копию следующей записи в списке. Любые дей-
ствия с полученным массивом не повлияют на запись, содержащуюся в храни-
лище. После вызова функции происходит продвижение по списку, то есть сле-
дующий вызов вернет уже другую запись. Первый вызов функции возвращает
первую в списке запись;
• byte[] previousRecordO — возвращает копию предыдущей записи в списке. Пер-
вый вызов функции возвращает последнюю в списке запись;
• int nextRecordldC) — возвращает номер следующей записи в списке по тому же
принципу, что и метод nextRecord;
• int previousRecordldC) — возвращает номер предыдущей записи в списке;
• boolean hasNextElementO — возвращает значение true, если список содержит
следующую запись, то есть возможно продвижение с использованием nextRecord;
• boolean hasPreviousElementO — возвращает значение true, если список содер-
жит предыдущую запись, то есть возможно продвижение с использованием
previousRecord;
• i nt numRecords () — получить количество записей, содержащихся в списке;
• void reset О — возвращает нумератор в исходное положение;
• void rebui ld() — перестроить список после того, как записи были удалены или
добавлены в хранилище;
• void destroy() — освобождает все ресурсы, задействованные нумератором.
RecordFilter и RecordComparator —
фильтр и компаратор
Вернемся к вышеприведенному примеру. При получении списка записей в мето-
де enumerateRecords в качестве параметров RecordFi Her и RecordComparator мы пере-
дали null, тем самым получив список всех без исключения записей хранилища.
На самом деле, можно отделить зерна на этапе получения списка и выбрать толь-
ко необходимые записи в определенном порядке. Для достижения этих целей слу-
жат два интерфейса RMS — фильтр и компаратор.
Начнем с фильтра . Чтобы выбрать из хранилища только интересующие нас за-
писи, необходимо реализовать интерфейс RecordFilter и переписать его метод
boolean matches (byte[] candidate), который должен проверить запись на соответ-
ствие установленному нами критерию и вернуть true в положительном случае.
Компаратор — интерфейс сравнения записей, позволяющий отсортировать спи-
сок в заданном нами порядке. Для этих целей необходимо реализовать интерфейс
RecordComparator и переписать его метод int compare (byte[] reel . byte[] rec2), кото-
рый возвращает константы:
• RecordComparator.PRECEDES — если первая запись (reel) предшествует второй
записи (гес2) согласно нашей логике;
• RecordComparator. FOLLOWS — если первая запись следует за второй согласно ус-
тановленному нами критерию;
----------------------- Page 78-----------------------
RecordFilter и RecordComparator — фильтр и компаратор 7 9
• RecordComparator. EQUIVALENT — если с точки зрения упорядочивания записи равны.
Объекты классов фильтра и компаратора передаются в качестве аргументов в ме-
тод хранилища записей enumerateRecords(RecordFilter filter . RecordComparator
compa rator . bool ean keepUpdated). Обратим внимание на третий аргумент. Разработчи-
ки утверждают, что если передать значение true, то список будет автоматически пе-
рестраиваться во время работы с хранилищем после добавления или удаления запи-
сей. Предупреждение гласит, что использование этой опции может существенно
повлиять на производительность приложения. В приведенном примере для целей
синхронизации списка и хранилища был использован метод rebuildC). Эксперимен-
ты с параметром можете провести сами.
Для примера использования фильтра и компаратора допустим, что хранилище
содержит картотеку с информацией о высоких незамужних блондинках, а пер-
вый байт каждой записи представляет их возраст. Ставим перед собой задачу:
выбрать всех представительниц моложе тридцати лет в порядке возрастания. Для
этого реализуем два класса, MyRecordFi Her и MyRecordComparator:
// класс фильтра
private class MyRecordFi H e r implements RecordFilter {
public boolean matches(byte [ ] candidate )
{
// проверяем первый байт записи
if(candidate[0]<30) return true : else return false ;
// класс компаратора
private class MyRecordCompa rato r implements RecordCornparator
public in t compare(byte[] reel . byte[] rec2)
{
// устанавливаем порядок записей по первому байту
if(recl[0]<rec2[0]) retur n RecordComparator.PRECEDES:
if(recl[0]>rec2[0]) retur n RecordComparator.FOLLOWS:
return RecordComparator.EQUIVALENT:
// открыть хранилище записей с именем "Blondes "
RecordStore recordStore - RecordStore.openRecordStoreC'Blondes" , true) ;
// создать объект фильтра
MyRecordFi H e r filter = new MyRecordFi iterO ;
// создать объект компаратора
MyRecordComparator comparator = new MyRecordComparatorO ;
// получить список записей согласно критерию в порядке возрастания
RecordEnumeration re = recordStore.enumerateRecords
(filter, comparator , false) :
----------------------- Page 79-----------------------
8 0 Глава 7 . Сохранение данных и параметров приложения
Таким образом, мы получили список интересующих нас кандидатур, с которы-
ми теперь мы можем производить любые действия, как было показано в первом
примере.
Record Listener — лови момент
Для контроля над хранилищем записей используют блок Прослушивания запи-
сей. Применение блока прослушивания более актуально для организации много-
поточных приложений, на которых мы остановимся более подробно в следующей
главе. Смысл блока в том, чтобы отследить момент удаления, добавления или из-
менения записи в хранилище. При возникновении одного из этих событий авто-
матически вызовется соответствующая функция.
Для того чтобы создать блок прослушивания, нужно реализовать интерфейс
RecordListener, в котором объявлены следующие методы:
• recordAddecKRecordStore recordStore, int recordld) — вызывается сразу после
добавления в хранилище recordStore новой записи с номером recordld;
• recordChanged(RecordStore recordStore, int recordld) — вызывается сразу после
того, как запись с номером recordld была изменена;
• recordDeleted(RecordStore recordStore, int recordld) — вызывается после того,
как запись с номером recordld была удалена. Функция вызывается после уда-
ления, поэтому не стоит пытаться получить в ней запись с номером recordld —
в этом случае будет сформировано исключение InvalidRecordlDException.
Процедура взаимосвязи блока прослушивания и конкретного хранилища запи-
сей осуществляется с помощью следующих методов хранилища: addRecord Li stener
(RecordListener listener), removeRecordListener(RecordLi stener listener). Один блок
прослушивания может обслуживать одновременно несколько хранилищ.
RecordStoreException —
возможные проблемы
Остановимся на обработке исключительных ситуаций, которые могут возникнуть
при работе с системой управления записями. Все виды исключений RMS насле-
дованы от одного класса RecordStoreException, поэтому в try-блоке можно отсле-
живать только объект этого класса, как и было сделано в примере.
Если же мы хотим получить более подробную информацию о возникшей ошибке
или предпринять особые действия для устранения возникшей проблемы, то сле-
дует добавить обработку следующих исключений:
• Inval idRecordlDException — формируется в случае невозможности выполнения
операции из-за некорректного номера записи;
• RecordStoreFul I Exception — операция не может быть завершена, так как храни-
лище переполнено, то есть памяти, выделенной конкретной моделью, оказа-
лось недостаточно;
----------------------- Page 80-----------------------
High Score — таблица рекордов 8 1
• RecordStoreNotFoundException — формируется при попытке открыть хранилище
с несуществующим именем;
• RecordStoreNotOpenException — возникает при попытке работы с неоткрытым
хранилищем.
Таким образом, большинства из возможных проблем можно избежать, грамотно
проектируя приложение. Следует внимательно следить за своевременным откры-
тием и закрытием хранилища, актуальностью номеров записей. Распространен-
ная ошибка — работа со списком записей, полученным до удаления или добавле-
ния некоторых записей, поэтому не забываем про метод rebui I d().
High Score — таблица рекордов
Вернемся к нашим змеям и попробуем усовершенствовать игру, применив выше-
изложенное на практике. В долговременном хранилище записей будем хранить
запись, состоящую из двух байт, первый из которых будет содержать текущий
рекорд, а второй — уровень игры, от которого будет зависеть скорость передвиже-
ния змейки. Для реализации наших идей нам потребуется импортировать пакеты
поддержки хранилища данных, а также завести новые поля в классе Snake:
import javax.microedition.rms.RecordStore ;
import javax.microedition.rms.RecordEnumeration :
i mport javax.microediti on.rms.RecordStoreExcepti on ;
private RecordStore recordStore ; // хранилище данных
private int recordID ; // ID записи параметров
private byte level ; // уровень сложности игры
private byte speed : // коэффициент вычисления задержки треда
private byte highScore : // текущий рекорд
В конструкторе класса Snake добавим считывание параметров из хранилища дан-
ных или создание новой записи для первого запуска игры:
// параметры для долговременного хранения
// первый байт - текущий рекорд
// второй байт - уровень сложности игры
byte buff[ ] = {0.5} ;
try {
// открыть хранилище записей с именем "SNAKE "
recordStore = RecordStore.openRecordStore("SNAKE" , true) ;
// получить список записей хранилища
RecordEnumeration re = recordStore.enumerateRecordstnull , null , false) :
// если хранилище пусто
if (re.numRecords()==0 )
// добавить новую запись параметров игры .
recordID = recordStore.addRecord(buff . 0 . 2 ) ;
else
// получить id записи параметров игры
----------------------- Page 81-----------------------
8 2 Глава 7 . Сохранение данных и параметров приложения
recordID = re.nextRecprdldO :
// считать запись параметров игры
buff = recordStore.getRecord(recordID) ;
// первый байт - текущий рекорд
highScore = b u f f [ 0 ] :
// второй байт - уровень сложности игры
level = b u f f [ l ] ;
// установить коэффициент вычисления задержки
speed - 100;
} catch(RecordStoreException rse ) {
}
Теперь при завершении игры будем сравнивать набранный результат с текущим
рекордом. Сообщение о новом рекорде будем выводить красным цветом, а резуль-
тат записывать в хранилище данных (рис. 7.3).
Добавим эти изменения в метод paint О:
// если поднят флаг конца игры
if ( gameOverFlag ) {
// вывести сообщение конца игры
g.drawStringC'GAME OVER".width/2.height/2-10.g.HCENTERfg.TOP) ;
// строка сообщения счета игры
String scoreStr ;
// массив параметров для долговременного хранения
byte buff[ ] - new byte[2] ;
// если текущий рекорд меньше нового результата
if(highScore < snakeLen ) { • •
highScore = (byte)snakeLen :
// инициализировать массив с новыми параметрами
biiffTO] - highScore ;
buff[l ] = level :
try {
// записать новый рекорд в хранилище
recordStore.setRecordt recordID.buff.0.2) :
} catch(RecordStoreException rse ) {
}
// установить цвет текста красным
g.setColor(OxffOOOO) :
// сформировать строку с новым рекордом
scoreStr - new StringC"HIGH SCORE : "+snakeLen) ;
} else
// сформировать строку со счетом
scoreStr - new StringC'YOUR SCORE : "+snakeLen) ;
// вывести счет
g.drawString(scoreStr.width/2.height/2+10.g.HCENTER|g.TOP);
} else
----------------------- Page 82-----------------------
High Score — таблица рекордов 83
Готово, можно уже запускать! Правда, играть # ( 1 ) Nokia 7210 S
стало гораздо интереснее? Теперь можно ста- Ffe look
: . : • • : •
вить новые рекорды. Именно этим параметром
завлекают многие и многие игрушки, как ком- Г
пьютерные, так и мобильные. Вспомните, как
в детстве, раз за разом, вы запускали игрушку,
даже такую тупую, как «Ну, погоди!» или «Тай-
ны океана», и старались изо всех сил вписать
свое имя в таблицу славы, побив чужой рекорд.
Самые хитрые, естественно, ничего не прохо-
дили, а просто вскрывали программу и припи-
сывали себе любые результаты. Как это делает-
ся, мы рассмотрим в одной из заключительных
глав.
Заметим, что для хранения текущего рекорда
у нас отведен всего один байт, то есть максималь-
ный рекорд, который можно поставить, ограни-
чивается длиной змеи, равной 255 элементам.
Хотя с такой реализацией увеличения скорос-
ти после каждого «съеденного» сердечка нам это
не грозит. При изменении этой логики, возмож-
но, следует добавить памяти для хранения теку-
щего рекорда. При проектировании приложения
всегда стоит задуматься, отвечает ли заведенный
тип данных характеру хранимых данных. Как мы
уже видели, тип byte может принимать значения
от 0 до 255.
В данный момент мы реализовали хранение Рис. 7.3 . Сообщение о новом
только одного рекорда, а хотелось бы, конечно, рекорде
иметь таблицу лучшей десятки с их именами.
Всему свое время: пока что нам не хватает знаний о реализации ввода текста
и работе со строками, но и до этого мы обязательно доберемся.
* * *
В этой главе мы достаточно подробно рассмотрели систему управления записями
RMS, которая представляет собой простую абстракцию базы данных (RecordStore),
связанную с записями. Записи не имеют определенного типа, а хранятся как мас-
сив байтов. Записи можно получать по уникальному идентификатору записи (ID),
если он известен, или же получить список всех записей. Фильтр записей (Record
Fi Her) реализует механизм запросов, с помощью которого можно выбирать толь-
ко соответствующие каким-то критериям записи. Компаратор записей (Record
Comparator) определяет порядок извлечения записей из списка и реализует сорти-
ровку записей.
Стоит отметить, что производительность RMS даже в современных моделях
достаточно низка, поэтому стоит использовать RMS только в тех случаях, когда
----------------------- Page 83-----------------------
84 Глава 7 . Сохранение данных и параметров приложения
это действительно необходимо. Есть мнение, что производительность может
увеличиваться, если не обновлять некоторые элементы, а переписать полно-
стью все содержимое хранилища.
Чтобы оценить производительность написанной программы, лучше воспользо-
ваться реальным аппаратом, поскольку эмулятор в этом случае может не отобра-
зить реальной картины. Вот, собственно, и все. Мы еще вернемся к системе уп-
равления записями в одном из следующих примеров.
----------------------- Page 84-----------------------
Глава 8
Стандартные средства
пользовательского интерфейса
Если вы смогли оторваться от игры, побив все мыслимые рекорды по откармли-
ванию змей, то продолжим уроки мобильного программирования. Посмотрим кри-
тическим взглядом на нашу игру и подумаем, чего в ней еще не хватает. Уважа-
ющий себя театр должен начинаться с гардероба, а приложение для мобильного
телефона, как и ресторан, начинается с меню. Меню мы изобретать не будем,
а воспользуемся готовым пользовательским интерфейсом высокого уровня.
Класс List
Простейший вариант организации меню — использование готового интерфейса
высокого уровня, предоставляющего все необходимые функции навигации и вы-
бора одного из пунктов. Меню представлено классом Li st , который наследован от
класса Screen, то есть является объектом, готовым к демонстрации через менед-
жер дисплея. Создается объект меню с помощью одного из конструкторов:
ListCString title , int listType)
или
List(String t i t l e , int listType, String[] stringElements,
Image[] imageElements)
Первый конструктор создает пустой объект, в который можно будет добавить не-
обходимые пункты, второй же принимает пункты меню в виде массива строк
stringElements. Параметр imageElements определяет массив изображений, которые
можно поставить в соответствие каждому пункту меню. Массив stringElements,
как и каждый его элемент, не должн быть пустым. Длина массива определяет ко-
личество пунктов меню. Массив imageElements может иметь значение nul 1 или со-
держать такое же количество элементов, как и массив stringElements.
Параметр t i t l e задает заголовок меню, а 11 stType определяет один из трех типов
меню, представленных следующими константами класса Li st :
• IMPLICIT — каждое перемещение по пунктам меню вызывает немедленное опове-
щение через блок прослушивания команд. Класс Li st содержит особую команду
Command SELECT_COMMAND, которая формируется при выборе нового пункта и пере-
дается в качестве аргумента в метод блока прослушивания commandActi on (Command,
Displayable);
----------------------- Page 85-----------------------
8 6 Глава 8 . Стандартные средства пользовательского интерфейса
• EXCLUSIVE — позволяет выбор единственного пункта меню; оповещения прило-
жения не генерируются;
• MULTIPLE— позволяет выбор нескольких пунктов меню в любом сочетании; опо-
вещения не генерируются.
Каждый из пунктов меню может находиться в двух состояниях — выбранный или
невыбранный. В режимах работы IMPLICIT и EXCLUSIVE выбранным может быть толь-
ко один пункт меню, в режиме MULTIPLE выбранными могут быть несколько пунк-
тов меню одновременно.
Класс List расширяет класс Screen и реализует интерфейс Choice. Все его методы
определяются реализуемым интерфейсом.
Interface Choice
Интерфейс Choice определяет набор методов для элементов пользовательского
интерфейса верхнего уровня, предоставляющих возможность выбора из несколь-
ких предложенных вариантов (класс List и класс ChoiceGroup). Каждый из вари-
антов представляется текстовой строкой и необязательным графическим изобра-
жением — пиктограммой.
Для организации работы с меню интерфейс Choice определяет следующие мето-
ды, реализованные в классе Li st :
• int size() — возвращает количество пунктов меню. Все пункты меню индек-
сируются целыми числами от 0 до size( )-l;
• int appencKString stringPart , Image imagePart) — добавляет новый пункт меню,
представленный строкой stringPart и картинкой imagePart, в конец списка
и возвращает присвоенный элементу индекс;
• void insert(int elementNum, String stringPart, Image imagePart) — аналогично
предыдущему методу, вставляет новый пункт меню в позицию списка, задан-
ную параметром elementNum;
• void deletednt elementNum) — удаляет пункт меню с индексом elementNum;
• void set (int elementNum, String stringPart, Image imagePart) — изменяет пункт
меню с индексом el ementNumв соответствии с параметрами stri ngPart и imagePart;
• String getStringdnt elementNum) — возвращает строку, представляющую пункт
меню с индексом elementNum;
• Image getlmage(int elementNum) — возвращает картинку, связанную с пунктом
меню, имеющим индекс elementNum;
• boolean i sSel ected( int elementNum) —возвращает значение true, если пункт меню
с индексом elementNum является выделенным;
• int getSe1ectedIndex() — возвращает индекс выделенного пункта меню. В ре-
жиме MULTIPLE всегда возвращает значение - 1 ;
• void setSelectedlndexUnt elementNum, boolean selected) — в режиме MULTIPLE
устанавливает пункт меню с индексом elementNum в состояние, соответству-
ющее параметру sel ected. В остальных режимах параметр sel ected должен иметь
значение true, при этом соответствующий пункт меню переходит в выделен-
----------------------- Page 86-----------------------
Класс SnakeGame 8 7
ное состояние. Пункт, бывший до этого в выделенном состоянии, переходит
в невыделенное состояние. В режиме IMPLICIT никаких неявных вызовов не
происходит;
• int getSeiectedFlags(boolean[] selectedArray_return) — в параметр selected
Array_return возвращается массив флагов, соответствующих пунктам меню.
Элемент массива содержит значение true, если выделен соответствующий пункт
меню. В режиме MULTIPLE значение true может содержать произвольное число
элементов массива, в остальных режимах — только один элемент. Сам же ме-
тод возвращает количество выделенных элементов;
• void setSel ectedFl ags (bool ean[] selectedArray) — устанавливает пункты меню
в выделенное состояние в соответствии с массивом флагов selectedArray.
В режимах EXCLUSIVE и IMPLICIT только один элемент массива sel ectedArray дол-
жен иметь значение true. Если таких элементов нет, то выделенным становит-
ся первый пункт меню. Если элементов больше, чем один, то первый соответ-
ствующий пункт принимает выделенное состояние, остальные игнорируются.
Класс SnakeGame
Добавим в игру стартовое меню, которое будет иметь несколько команд: начать
новую игру, установить уровень сложности игры и посмотреть текущий рекорд.
При запуске игры сначала будем демонстрировать меню, а по окончании игры
возвращаться туда же, а не зависать намертво, как мы делали до этого. Как мы уже
выяснили, для воплощения наших идей нам потребуется создать объект класса
List, который будет представлять стартовое меню игры, а также реализовать ин-
терфейс CommandListener для прослушивания команд.
Для начала нам потребуется подключить несколько пакетов, реализующих необ-
ходимые классы;
import javax.microedition.lcdui.CommandListener:
import javax.microedition.lcdui.Command:
import javax.microedition.lcdui.Displayable: .
import javax.microedition.lcdui.List :
Интерфейс CommandListener мы реализуем в основном классе мидлета SnakeGame.
Его декларация теперь будет выглядеть следующим образом:
public class SnakeGame extends MIDlet implements CommandListener
Также нам потребуются два новых члена класса, представляющих собственно
объект меню, а также команда для выбора пункта меню:
private List menu; // стартовое меню
private Command ok; // команда выбора пункта меню
Дальнейшие изменения произведем в методе startAppO, из которого уберем со-
здание объекта змейки и старт нового треда, а добавим создание меню и отобра-
жение его на экране. Теперь стартовый метод мидлета будет выглядеть так:
public void startAppO {
// массив строк с названиями пунктов меню
String menuOptions[ ] - {"New Game","Set Level "."High Score"} :
----------------------- Page 87-----------------------
88 Глава 8 . Стандартные средства пользовательского интерфейса
// создать новое меню
menu = new ListC",List.IMPLICIT.menuOptions.null) ;
// создать команду выбора пункта меню
ok = new Command("Ok" . Command.OK . 1) ;
// добавить команду в меню
menu.addCommand(ok) ;
// установить блок прослушивания команд для меню
menu.setCommandListener(this) ;
// получить ссылку на менеджер дисплея
display = Display.getDisplay(this) :
// отобразить меню на экране
display.setCurrent(menu):
Чтобы откомпилировать и посмотреть, что у нас получилось, нужно реализовать
метод commandAction, представляющий блок прослушивания команд. Этот метод
и будет предписывать те или иные действия в зависимости от выбранного пункта
меню. Пока что можно оставить его пустым, откомпилировать приложение и по-
смотреть, как выглядит меню на экране телефо-
# ( 1 ) Nokia 7210 SWC
на (рис. 8.1).
File Tools Help
Итак, мы видим на экране аппарата строки, пред-
ставляющие пункты меню, заданные нами в мас-
сиве menuOptions. Один из пунктов подсвечен си-
ней полоской. Одна из функциональных клавиш
связана с командой Ok, при нажатии на которую,
правда, пока что ничего не происходит.
Перед тем как продвигаться дальше, стоит тща-
тельно обдумать структуру приложения. Это от-
носится к любому программированию вообще,
поскольку просчеты в проектировании програм-
мы могут привести к тому, что придется начинать
с чистого листа и все переписывать заново. Так
что, как говорится, лучше час потерять, а потом
за пять минут долететь.
Обратить внимание следует на логику смены эк-
ранов. Если для начала новой игры нам просто
потребуется создать объект змейки и отобразить
его на экране, то для демонстрации текущего ре-
корда и установки нового уровня сложности по-
требуется новый отображаемый объект, который
будет нести необходимую информацию, а также
обладающий возможностью возврата к стартово-
му меню программы. В качестве отображаемого
объекта выберем уже знакомую нам форму. Для
удобства напишем новый метод, который будет
Рис. 8 . 1 . Теперь игра начинается создавать форму, принимая необходимые пара-
с меню метры, и отображать ее на экране.
----------------------- Page 88-----------------------
Класс SnakeGame 8 9
private void showNewScreen(String t i t l e . Command c. Item item) {
// создать форму для нового экрана
Form newScreenForm = new Form(title) :
// добавить команду для возврата из формы
newScreenForm.addCommand(c);
// добавить элемент в форму
newScreenForm.append(item);
// установить блок прослушивания команд
newScreenForm.setCommandLi stener(this) ;
// отобразить форму на экране
display.setCurrentCnewScreenForm);
}
Обратим внимание, что и стартовое меню программы, и новая форма пользуются
одним и тем же блоком прослушивания команд, реализованном в основном клас-
се мидлета SnakeGame.
Кроме того, текущий рекорд и уровень сложности игры связаны с параметрами,
находящимися в хранилище данных, поэтому напишем еще один метод, кото-
рый будет читать необходимую запись хранилища и возвращать нужный пара-
метр:
private byte getParameter(int index ) {
// массив параметров игры
byte buff[ ] = {0.0} :
try {
// открыть хранилище записей с именем "SNAKE"
RecordStore recordStore = RecordStore.openRecordStore
("SNAKE", true) ;
// получить список записей хранилища
RecordEnumeration re = recordStore.enumerateRecords
(null. null . false) ;
// если хранилище не пусто
if (re.numRecordsO !=0) {
// получить id и считать запись параметров игры
buff • recordStore.getRecord(re.nextRecordldO) :
}
} catch(RecordStoreException rse ) {
}
// вернуть параметр
return buff[index] ;
}
Перейдем к блоку прослушивания команд, который, собственно, и будет дири-
жировать нашей программой и предписывать действия в зависимости от выб-
ранного пункта меню. В обязательном к реализации методе commandAction есть
два входных параметра, которые определяют вызванную команду и отобража-
емый экран, из которого была вызвана команда. В зависимости от этих парамет-
----------------------- Page 89-----------------------
9 0 Глава 8 . Стандартные средства пользовательского интерфейса
ров мы и будем определять необходимые действия. В первую очередь обработа-
ем команду Ok:
public void commandAction(Com(nand c. Displayable d) {
// если была команда Ok из главного меню игры
i f (с == ok && d == menu) {
// получить выбранный пункт меню •
int sel Index = menu.getSelectedlndexO ;
switch(se1Index) {
// первый пункт меню - новая игра
case 0 :
// создать обьект змейки
curSnake = new SnakeO;
// создать обьект треда автоматического передвижения
Thread moveThread = new Thread(curSnake):
// начать выполнение треда
moveThread.start(); ч
// отобразить змейку на экране
display.setCurrent(curSnake);
break:
// второй пункт меню - установить уровень сложности
case 1 :
// массив строк с названиями пунктов меню
String levelOptions[ ] = {"Level 1","Level 2" .
"Level 3"."Level 4"} :
// создать меню выбора уровня игры
levelChoice =
new ChoiceGroupt"".List.EXCLUSIVE,1evelOptions.nul1) :
// выделить пункт меню
// соответствующий текущему уровню сложности
levelChoice.setSelectedIndex(5-getParameter(l).true) :
// отобразить новую форму
(
showNewScreenC'Set Level".set.level Choice) :
break ;
// третий пункт меню - посмотреть текущий рекорд
case 2 :
// получить строку с текущим рекордом
String strScore = (new Integer(getParameter(0))).toString() ;
// создать объект элемента-строки
Stringltem highScoreltem = new StringltemC " .strScore) ;
// отобразить новую форму
showNewScreenC'HIGH SCORE".ok,highScoreltem) :
break :
// если была команда Ok . вызванная из формы
if(c==ok && d!=menu )
----------------------- Page 90-----------------------
Класс SnakeGame 9 1
// отобразить на экране стартовое меню # < 1 ) Nokia 721О51Ж
display.setCurrent(menu) :
Итак, как мы видим из кода, меню установки
уровня сложности представлено объектом клас-
са ChoiceGroup, который принадлежит к иерархии
класса Item, поэтому он может быть отображен с
помощью формы. На этом примере мы наглядно
видим использование полиморфизма объектно-
ориентированного языка: метод showNewScreen при-
нимает в качестве одного и того же аргумента раз-
личные объекты классов Choi ceGroup и Stri ngltem,
порожденных одним классом Item.
Из собственного опыта у меня сформировался
следующий подход к программированию : при
добавлении любой новой функциональности сто-
ит запустить программу и убедиться, что все ра-
ботает корректно. Такой подход позволяет вов-
ремя отследить допущенные просчеты и ошибки.
Именно поэтому рекомендую не пытаться напи-
сать сразу всю программу целиком, чтобы потом
ломать голову, где же в этой куче кода была до-
пущена ошибка, а продвигаться от главы к главе
постепенно. Это не детектив, чтобы заглядывать
в конец книжки: кто же убийца? Полный листинг
программы в конце приведен не для бездумного
переписывания, а для дополнительного контро-
ля и помощи в поиске ошибок.
Рис. 8.2 . Демонстрация наилучшего
Для того чтобы откомпилировать и запустить результата
программу на данном этапе, требуется несколь-
ко дополнительных штрихов. Первое: требуется
импортировать необходимые пакеты, классы которых мы будем использовать:
import javax.microedition.lcdui.Form;
import javax.microedition.lcdui.Item;
import javax.microedition.lcdui.ChoiceGroup;
import javax.microedition.lcdui.Stringltem;
Второе: в классе SnakeGame мы завели два новых поля для реализации меню выбо-
ра уровня игры:
private ChoiceGroup TevelChoice: // меню выбора уровня сложности игры
private Command set : // команда выбора уровня сложности игры
В стартово м методе мидлет а startAppC ) создади м объект к о м а н д ы выбор а у р о в н я
с л о ж н о с т и и г р ы :
set = new Command("Set". Command.BACK, 1) ;
Вот теперь можно запустить игру. Обратите внимание, что обработка команды
set в блоке прослушивания команд еще не реализована, поэтому пока что удов-
летворимся просмотром текущего рекорда (рис. 8.2).
----------------------- Page 91-----------------------
9 2 Глава 8 . Стандартные средства пользовательского интерфейса
При нажатии функциональной клавиши формируется команда Ok, и приложение
снова демонстрирует на экране стартовое меню. Выглядит это все, конечно, не
очень презентабельно, что обусловлено ограниченностью возможностей формы.
Например, наложить текст на картинку мы не сможем при всем желании. Так что
в качестве самостоятельной работы можете попробовать использовать вместо
формы класс, порожденный от класса Canvas. В методе pai nt можно будет нарисо-
вать все, что душе угодно, а возврат в основное меню отработать с помощью мето-
да keyPressed.
Однако не будем отвлекаться и допишем в блоке прослушивания команд обра-
ботку команды set, где получим выбранный уровень сложности и запишем его
в хранилище записей:
if(с — set) {
try {
// массив параметров игры
byte buff[ ] - {0.5} :
// открыть хранилище записей с именем "SNAKE "
RecordStore recordStore = RecordStore.openRecordStore
("SNAKE", true) ;
// получить список записей хранилища
RecordEnumeration re - recordStore.enumerateRecords
(null. null . false) ;
// если хранилище не пусто
if (re.numRecords()!=0 ) {
// получить id записи параметров игры
int id - re.nextRecordldO ;
// считать запись с параметрами игры
buff = recordStore.getRecord(id) ;
// вычислить текущий уровень игры
buff[l ] - (byte)(5 - levelChoice.getSelectedlndexO) ;
// записать параметры игры
recordStore . setRecorddd , buff . 0.2) :
} else {
// добавить новую запись параметров игры
recordStore.addRecord(buff . 0 . 2 ) :
}
} catch(RecordStoreException rse ) { .
}
// отобразить на экране стартовое меню
display.setCurrent(menu);
}
Теперь все пункты стартового меню полностью соответствуют заявленным дей-
ствиям. Попробуем, как работает меню выбора уровня сложности игры (рис. 8.3).
В данном случае мы использовали режим EXCLUSIVE, поэтому меню отличается от
стартового внешним видом. Команда Select появилась автоматически и отвечает
за выделение одного из пунктов меню точкой, так как перемещение подсвеченной
----------------------- Page 92-----------------------
Класс Gauge 93
синей полоски по пунктам не приводит к выде- Ф(1) Nokia 7Z1»!
лению одного из них. Реализованная нами ко- File Tools (Help
манда Set вызывает запись нового уровня слож-
ности, который учитывается при запуске новой
игры. Теперь каждый может задать свой соб-
ственный темп, что только добавит увлекатель-
ности нашей игре.
Класс Gauge о Level 2
о Level 3
Благодаря гибкой реализации и полиморфиз- ® Level 4
му, можно легко поменять реализацию выбора
сложности игры, используя, например, не меню
выбора ChoiceGroup, а шкалу Gauge. Шкала так-
же принадлежит иерархии класса Item, поэто-
му метод showNewScreen изменять не потребует-
ся. Внешний вид шкалы, как и всех объектов
пользовательского интерфейса высокого уров-
ня, зависит от конкретной модели телефона.
Например, в стандартном цветном эмуляторе
шкала будет выглядеть так (рис. 8.4).
Здесь мы видим привычные глазу прямоуголь-
нички, меняющие цвет в зависимости от выбо-
ра. У разработчиков корпорации Nokia свое ви-
дение шкалы, поэтому то же самое на эмуляторе
Nokia 7210 будет выглядеть следующим образом
(рис. 8.5).
Изменить внешний вид этого элемента у нас нет Рис. 8.З . Меню выбора уровня
сложности
никакой возможности, поэтому придется до-
вольствоваться имеющимися методами:
• Gauge(String label, boolean interactive, int maxValue, int initialValue) —
конструктор класса Gauge. Создает шкалу с заголовком 1 abel и возможным диа-
пазоном значений от 0 до maxVal ue. Значение по умолчанию выставляется рав-
ным параметру 1 initial Val ue. Если параметр i nteracti ve имеет значение true, то
значение шкалы можно изменять, иначе, как в стрип-клубе: смотреть — мож-
но, трогать — нельзя;
int getMaxVal ue() — возвращает максимально возможное значение шкалы;
int getVal ue() — возвращает текущее значение шкалы;
boolean isInteractiveO — возвращает значение true, если пользователь может
изменять значение шкалы;
void setLabel (String label) — устанавливает новый заголовок шкалы;
void setMaxValuednt maxValue) — устанавливает новое максимально возмож-
ное значение шкалы;
void setValueCint value) — возвращает выбранное пользователем значение
шкалы.
----------------------- Page 93-----------------------
94 Глава 8 . Стандартные средства пользовательского интерфейса
# ( 1 ) Nokia 7210 SDK
•File "tools fcjelp
Рис. 8.4 . Шкала в стандартном Рис. 8.5 . Шкала на эмуляторе
эмуляторе Nokia 7210
Последний штрих для полного счастья — заставка, которая будет выводиться пе-
ред запуском игры. Сохраним картинку для заставки в файле title.png и добавим
в стартовом методе приложения startAppC) следующий код:
Image title = null ;
try {
// создать объект заставки
title - Image.createlmageC'/title.phg") ;
} catch (IOException ioe ) { }
// создать новый элемент формы
Imageltem item - new ImageltemC " .title . Imageltem.LAYOUT_CENTER,"") ;
// отобразить заставку
showNewScreent"SNAKE",ok,item);
Для отображения заставки мы воспользовались все тем же универсальным мето-
дом showNewScreen, который в качестве аргумента принимает объект класса Imageltem,
содержащий картинку (рис. 8.6).
Вот теперь точно — все!
----------------------- Page 94-----------------------
Класс Gauge 95
* * * # ( 1 ) Nokia 7218 SMC
В этой главе мы разобрались с организацией
меню с помощью пользовательского интерфей-
са высокого уровня. На этом наше знакомство
с компонентами интерфейса высокого уровня не
заканчивается. Мы еще вернемся к некоторым
из них в других примерах. Лучше всего такие
вещи усваиваются на практике: почитал — сра-
зу реализовал — и почувствовал силу в руках.
Основной принцип высокоуровнего интерфей-
са в том, что все объекты иерархии класса
Displayabie можно отобразить на экране теле-
фона, а все объекты иерархии класса Item мож-
но включить в отображаемую форму. Всегда
требуется следить за переходами по пунктам
меню: доступен ли необходимый отображаемый
объект в конкретный момент времени. В нашем
случае основной класс приложения содержит
стартовое меню в качестве поля, а форма вто-
рого уровня создается каждый раз заново при
вызове соответствующего пункта меню с необ-
ходимыми параметрами. Немного практики,
и у вас все получится.
Рис. 8.6 . Теперь у игры есть даже
заставка
----------------------- Page 95-----------------------
Глава 9
Организация потоков,
работа с файлами
Надеюсь, что вы старательно выполняете самостоятельные работы и к моменту
чтения этой главы уже смогли похвастаться перед друзьями своими руками со-
творенной «Змейкой». В этой игре мы выводили на экран, главным образом,
картинки, графику и прочую визуализацию, которую приложение считывало из
файлов-ресурсов. Ресурсами в этом случае являлись картинки в определенном,
воспринимаемом телефоном, формате. На самом же деле ресурсами могут яв-
ляться не только картинки. Любая информация в самом разнообразном виде
может храниться в файле-ресурсе. Каким образом информация из файла title.png
превратилась в картинку на экране мобильника, осталось для нас за кадром: мы
просто вызвали метод createlmage и получили готовый объект Image. В этой гла-
ве мы рассмотрим подробнее, что такое потоки, как работать с файлами-ресур-
сами и как организовать процедуру чтения из файла.
На этот раз только мы знаем, какого типа информацию содержит ресурс, как ее
следует обрабатывать, использовать или отображать. Черновую работу формиро-
вания объекта из файла, как это было в случае с картинкой, выполнять теперь за
нас некому.
Класс InputStream
Связь между источником данных и приложением осуществляется с помощью
объекта класса InputStream, который можно сравнить с трубой, пропускающей че-
рез себя поток данных. Класс InputStream является базовым для всех классов вво-
да и предоставляет основные методы работы с потоком ввода:
• int read О — прочитать следующий байт из потока данных. Эта функция явля-
ется абстрактной, то есть в любом классе-потомке обязательна ее реализация;
• int read(byte[] b) — считать данные из потока в буфер. Функция читает все
доступные данные, ограничиваясь размером буфера, и возвращает количество
прочитанных байт;
• int read(byte[] b. int off, int len) — считать данные длины len из потока
данных и разместить их в буфере, начиная с позиции off;
• int avai I abi е() — возвращает количество байт, доступных для чтения в данном
потоке;
----------------------- Page 96-----------------------
Класс InputStream 9 7
• void mark(int readiimit) — помечает текущую позицию в потоке данных с це-
лью предоставления возможности повторного чтения с этой позиции. Аргу-
мент указывает, сколько байт может быть прочитано, прежде чем метка станет
недействительной;
• boolean markSupportedO — проверка возможности повторного чтения данных
с помеченной позиции. Возвращает true, если позиция была задана и число
прочитанных после этого данных не превысило лимита;
• void reset О — возвращает поток данных в состояние, сохраненное при послед-
нем вызове функции mark;
• 1 ong ski p( 1 ong n) — пропустить п байтов данных потока. Возвращает реальное
количество пропущенных байт;
• void close() — закрывает поток и освобождает все используемые им ресурсы.
Следует отметить, что в базовом классе InputStream такие функции, как avai I abl е,
mark, reset, работать не будут. Они объявлены для последующей реализации
в классах-потомках, а пока что не выполняют никаких действий и возвращают
нулевое значение.
Рассмотрим использование класса InputStream на простом примере вывода текста
из файла на экран телефона. Поместим текстовый файл pushkin.txt в папку ресур-
сов приложения /res. Чтобы связать ресурс с потоком, воспользуемся методом
getResourceAsStream(Stringname) класса Class. Затем с помощью метода потока read
считаем содержимое файла и поместим его на форму, связанную с экраном. При-
мер кода будет выглядеть так:
import java.io.InputStream ; \
import java.io.IOException ;
Form form = new FormO :
Display display = Display.getDisplay(this) :
// связать файл с потоком is
InputStream is = getClass0.getResourceAsStreamC7pushkin.txt") :
byte[ ] bArr - new byte[150] ;
try {
is.read(bArr) : // считать содержимое потока в массив байт
catchUOException e ) { }
// сформировать строку из массива байт
String str = new String(bArr) ;
form.append(str); // поместить строку в форму
display.setCurrent(form); // отобразить форму на экране
Запускаем приложение в стандартном эмуляторе из WTK и получаем вот такой
замечательный результат (рис. 9.1).
Эмулятор справился с заданием на твердую пятерку: навигацию нам реализовала
форма, стрелочки перематывают текст вниз и вверх, на экране не появляется ника-
----------------------- Page 97-----------------------
98 Глава 9 . Организация потоков, работа с файлами
ких лишних символов, связанных с тем, что размер запрошенного нами массива
(new byte[150]) превышает объем текста. Красота!
Но, как показала практика, радоваться было рано. Запускаем наш пример на эму-
ляторе от Nokia 7210, что более близко соответствует запуску на реальном аппа-
рате, и видим печальную картину (рис. 9.2).
#(l)Nokia72lOSD»: BlzLsfl
1 вилась ты ,
<ак мимолетное
i«денье,
<ак гений чисто ! Ёаё aaiee тёпш ё
;расоты.
ё б а Ш й Х Ш Ш ] !
DDDnaaUDDDDQ
Рис. 9 . 1 . Вы знали, что на телефоне Рис. 9.2 . Осталось только научиться
можно читать книги?
это читать...
Русский текст превратился в шифровку агентов МОСАДа, а снизу прилипли квад-
ратики неиспользованных байтов массива. Отсюда делаем вывод, что кодировка
win 1251 не совпала с той, что запрограммирована в аппарате Nokia. Единственное
правильное решение в данной ситуации — пользоваться универсальной кодиров-
кой, которая подходила бы ко всем моделям. Благо, такая кодировка существует.
Ее название говорит само за себя — Unicode.
Встает вопрос: как теперь перевести текст с привычной глазу кириллицы на
Unicode? Если текст находится в коде программы и является, например, заголов-
ком формы или текстовым сообщением, то можно воспользоваться утилитой
native2asci 1 из Java SDK, который мы ставили в самую первую очередь.
Вызывается эта утилита из командной строки следующим образом:.. \bin \ native
2ascii -encoding cpl251 fiTel. Java > file2.java, где параметр encoding указывает
----------------------- Page 98-----------------------
Класс DatalnputStream 9 9
исходную кодировку, filel — исходный файл, f 11 е2 — конвертированный файл.
Обратный перевод из Unicode на родной язык осуществляется с использованием
параметра -reverse.
Например, после преобразования исходная строка
form.append("Здравствуй, мир!") ;
будет выглядет ь так :
form.append("\u0417\u0434\u0440\u0430\u0432\u0441\u0442\u0432\u0443\u0439.
\u043c\u0438\u0440!11);
Такой вариант действительно решает проблему корректного отображения рус-
ского текста. Однако в нашем случае текст находится в файле-ресурсе приложе-
ния, а значит конверсия исходного кода программы нам Не поможет. Предлага-
емое решение основывается на поочередном преобразовании каждого символа при
считывании его из потока.
Класс DatalnputStream
На этот раз мы воспользуемся классом Data I nputStream, который является наслед-
ником класса InputStream и реализует чтение основных типов данных. То есть, кроме
основного метода readO, этот класс содержит методы readBooleanO, readByteO,
readCharC), reacilntC), readLong(), readShortC), readtlnsignedByte(), readUnsignedShort().
Преобразование будем выполнять с помощью кодовой строки, которая каждому
символу кириллицы ставит в соответствие его код в универсальной кодировке.
Таким образом, наша программа приобретает следующий вид:
import Java.io.DatalnputStream;
Form form - new FormC):
Display display • Display.getDisplay(this);
InputStream is = getClass0.getResourceAsStreamC7pushkin.txt"):
DatalnputStream dis - new DatalnputStream(is):
int i = 0;
char[] bArr « new char[150] ;
try { // преобразовать символы в Unicode и записать в массив
while(true) bArr[i++ ] - (char)convert(dis.readUnsignedByteO) :
}
catchdOException ioe) { } • >
String st r = new String(bArr.O.i-l) ;
form.append(str) :
char converttint ch) {
return (ch < 128) ? (char)ch : WIN1251_T0_UNIC0DE.charA«ch-128):
}
// кодовая строка для преобразования символа из winl25 1 в Unicode
private String WIN1251_TO_UNICODE -
"\u0402\u0403\u201a\u0453\u201e\u2026\u2020\u2021\u20ac\u2030\u0409\u2039\u040a\u040c\u040b\
U040f\u0452\u2018\u2019\u201c\u201d\u2022\u2013\u2014\ufffd\u2122\u0459\u203a\u045a\u045c\
----------------------- Page 99-----------------------
100 Глава 9 . Организация потоков, работа с файлами
u045b\u045f\u00a0\u040e\u045e\u0408\u00a4\u0490\u00a6\u00a7\u0401\u00a9\u0404\u00ab\u00ac\
U00ad\u00ae\u0407\u00b0\u00bl\u0406\u0456\u0491\u00b5\u00b6\u00b7\u0451\u2116\u0454\u00bb\
U0458\u0405\u0455\u0457\u0410\u0411\u0412\u0413\u0414\u0415\u0416\u0417\u0418\u0419\u041a\
U041b\u041c\u041d\u041e\u041f\u0420\u0421\u0422\u0423\u0424\u0425\u0426\u0427\u0428\u0429\
U042a\u042b\u042c\u042d\u042e\u042f\u0430\u0431\u0432\u0433\u0434\u0435\u0436\u0437\u0438\
U043a\u043b\u043c\u043d\u043e\u043f\u0440\u0441\u0442\u0443\u0444\u0445\u0446\u0439\u0447\
U0448\u0449\u044a\u044b\u044c\u044d\u044e\u044f";
Символы читаются из потока по одному в виде байта без знака, а после преобра-
зования заносятся в массив символов. Проблема лишних байт решена с помощью
конструктора класса String, принимающего длину формируемой строки. Длину
мы получили при побайтном чтении потока. Запускаем приложение на эмулято-
ре от Nokia и видим, что все наши проблемы решены (рис. 9.3). Шрифт, конечно,
немного крупноват, но для начала сойдет.
i • : . ; . , ••••. ;
Как мимолетное
виденье,
Как гений чистой
красоты.
Рис. 9.3 . Демонстрация практической Рис. 9.4 . Так телефон признается
пользы от Unicode в своей несостоятельности
Останавливаться на достигнутом мы, конечно же, не собираемся и ставим задачу
написать удобное приложение для чтения небольших книжек. Очередная проблема
приведенных примеров заключается в том, что всю информацию ресурса мы разме-
----------------------- Page 100-----------------------
Класс BookReader 1 0 1
щаем в памяти программы и захватываем область памяти, равную по объему все-
му содержимому ресурса. Такой подход сводит на нет все преимущества потока,
способного выдавать данные мелкими порциями по мере надобности. В качестве
эксперимента помещаем в ресурс небольшой рассказик объемом 20 килобайтов
и пробуем, что из этого получится. Стандартный эмулятор после двухминутных
раздумий все же справился с задачей (поверьте на слово или проверьте, если хо-
тите), а эмулятор от Nokia опять сошел с дистанции, выдав вот такое милое сооб-
щение (рис. 9.4).
Класс BookReader
Итак , пише м приложени е для удобного чтения литературы с экран а телефона .
Д ля начал а хотелось бы уменьшить шрифт , выводимы й на экране . Смену шрифт а
поддерживает класс Graphics, содержащий метод setFont(Font font) . В примерах
мы пользовалис ь объектом класса Form, которы й не поддерживает прорисовку
объектов класса Graphics . В нашем случае воспользуемся отображаемым классом
Canvas, которы й м ы уж е рассматривали в одной из первых глав .
Остановимся на классе Font, который и представляет отображаемый шрифт . По-
лучить новы й шриф т можн о с помощью метода Font getFont(int face , in t style .
i nt size) , параметры которого определяют , как будут выглядеть символы :
• face — вид шрифт а определяется константами FACE_SYSTEM, FACE_MONOSPACE ил и
FAC E_PROPORTIONAL;
• styl e — стил ь шрифт а задается константой STYLE_PLAIN ил и комбинацие й кон-
стант STYLEJOLD, STYLEJTALIC и STYLEJJNDERLINED;
• siz e — размер шрифт а задается одной из констант SIZE_SMALL, SIZE_MEDIUM ил и
SIZE_LARGE.
Если в классе Form форматировани е текста по размеру экран а выполнялос ь авто-
матически , то сейчас вся ответственность з а корректно е позиционировани е тек-
ста переходит к нам . Класс Font содержит ряд методов для определения размер а
текста в пискселах :
• i n t getHeight() — возвращает высоту строки шрифт а в пикселах ;
• i n t charWidth(char ch) — возвращает ширину символ а ch в пикселах ;
• in t charsWidth(char[] ch . in t offset , in t length) — возвращает ширину набора
символов , содержащихся в массив е ch, начиная с позици и offset длин ы 1 ength ;
• i n t stringWidth(String s t r ) — возвращает ширину строки s t r в пикселах ;
• i n t substringWidthCString s t r , i n t offset , i n t Ten) — возвращает ш и р и н у
фрагмента строк и str , начинающегося с п о з и ц и и offset и имеющег о д л и н у
1еп.
В классе BookCanvas реализуем следующую логику отображения текста. В стеке
Page Index будем хранить смещение от начала текста для каждой страницы, что-
бы организовать возможность обратной прокрутки текста. Функция paint чи-
тает из потока данных по одному слову и отображает его, если на экране доста-
точно пространства. Клавиша 2 осуществляет переход к следующей странице
----------------------- Page 101-----------------------
1 02 Глава 9 . Организация потоков, работа с файлами
текста, клавиша 1 — к предыдущей странице, а клавиша 0 возвращает к началу
текста:
import Java.io.InputStream :
import java.io.EOFException ;
import java.io.IOException ;
import java.util.Stack ;
public class BookCanvas extends Canvas {
private Stack Pagelndex ; // стек смещений страниц от начала текста
public BookCanvasO {
Pagelndex = new StackO :
protected void paint(Graphics g ) {
// получить ширину и высоту рабочей области экрана
int gw = g.getClipWidthO :
int gh = g.getClipHeightO :
// создать шрифт
Font font = Font.getFont(Font.FACE_MONOSPACE.Font.STYLE_PLAIN.Font.SIZE_SMALL) :
// установить шрифт
g.setFont(font) :
// инициализировать поток
InputStream is = getClass0.getResourceAsStreamC7story.txt") :
MyDataInputStream mdis = new MyDatalnputStream(is) ;
int x=O.y=O : // текущее положение вывода на экране
int offset=0 : // смещение по исходному тексту
// очистка экрана
g.setColor(255.255.255) :
g.fillRect(O.O.gw.gh) :
g.setColor(O.O.O) :
// получить смещение для очередной страницы
if ( IPagelndex.emptyO )
offset=( ( Integer)PageIndex.peekO ) .intValueO :
try{
// сместиться по тексту до необходимой страницы
----------------------- Page 102-----------------------
Класс BookReader 1 0 3
mdi s.ski pBytes(offset) ;
} catchUOException ioe ) { }
String sWord ;
do {
// прочитать очередное слово из потока
sWord = mdis.readWordO ;
// продвинуть смещение по файлу на одно слово
offset+=sWord.1ength();
// если текущая позиция, и пиксельная ширина слова
// не выходят за экран
if(x+font.stringWidth(sWord)<=gw) {
// отобразить слово , начиная с текущей позиции
g.drawString(sWord, x.y . g.TOPjg.LEFT) :
// сместить текущую позицию на пиксельную ширину слова
x+-font.stri ngWi dth(sWord) ;
// если слово - последнее в строке , перевести текущую позицию
// в начало новой строки на дисплее
if (mdis.b_endLine ) { y+=font.getHeight() : x=0 ; }
}
// слово не входит по ширине с текущей позиций
else {
// перейти на новую строку
y+=font.getHeight();
// если на экране есть место для новой строки .
// отобразить слово с начала строки
i f(y+font.getHei ght()<gh ) g.drawStri ng
(sWord. O.y . g.TOP|g.LEFT) :
// перевести текущую позицию на ширину слова
x=font.stringWidth(sWord);
• }
// повторять до тех пор . пока есть место для новой строки
// или не кончится файл
} while(y+font.getHeight()<gh & & !mdis.b_endFile) ;
int index ; // смещение начала страницы
// если цикл закончился чтением слова .
// которое не было отображено на экране ,
// сместить начало новой страницы назад на одно слово
if(mdis.b_endl_ine) index=offset ; else index=offset-sWord.length() ;
// поместить в стек начало следующей страницы
if(!mdis.b_endFile) Pagelndex.push(new Integer(index)) ;
// закрыть поток
try{
mdis.closeO;
----------------------- Page 103-----------------------
1 0 4 Глава 9 . Организация потоков , работа с файлами
} catchCIOException ioe) {}
public void keyPressed(int keyCode) {
// обработка нажатий клавиш
switch(keyCode) {
case KEYJUM1 :
// удалить из стека начала следующей и текущей страницы
PageJ.ndex.popO; . '
1f(!PageIndex.empty()) Pagelndex.pop() ;
repaint О ;
break ;
case KEY_NUM2 :
// перерисовать экран (начало следующей страницы уже в стеке )
repaint О :
break ;
case KEYJUMO :
// очистить стек и перерисовать экран
PageIndex.removeAl1Е1ements();
repaintO :
break :
Поток в данном случае представлен классом, наследованным из DatalnputStream и
реализующим функцию чтения слова readWordO. При чтении очередного слова
функция выставляет флаги конца строки и конца файла. Сам класс выглядит сле-
дующим образом:
import java.io.InputStream :
import java.io.DatalnputStream ;
import java.io.EOFException ;
import java.io.IOException :
public class MyDatalnputStream extends DatalnputStream {
private String WIN1251_T0_UNIC0DE = "...": // кодовая строка
// преобразования символов
public boolean b_endl_ine » false ; // флаг конца строки
public boolean b_endFile • false ; // флаг конца файла
public MyDataInputStream(InputStream is ) {
super(is);
public String readWordO {
int 1-0; // счетчик символов в текущем слове
----------------------- Page 104-----------------------
Класс BookReader 105
b_endFile = false :
char[] word = new char[100] ; // массив для чтения слова
try {
// читать слово посимвольно до пробела или перевода строки
do word[i++]=(char)convert(readUnsignedByte()) ;
while (word[i-l]!=32 && word[i-l]!=10) :
} catch(EOFException ioe) { i - - : b_endFile = true:}
catchUOException ioe)
if(word[i-l]==10) b_endLine=true: else b_endLine=false:
return (new String(word.O.i)) :
char convert(int ch) {
return (ch < 128) ? (char)ch WIN1251_T0JNIC0DE.charAt(ch-128);
При чтении данных потока могут быть сформированы следующие исключения:
• EOFExcepti on — формируется при достижении • j~t,x |
конца файла; FJe l o o t Help
• IOException — базовое исключение для всех
функций ввода-вывода.
В программе мидлета осталось лишь создать
объект класса BookCanvas и установить его в ка-
честве текущего дисплея, как мы уже неодно- В ~ BookReader
кратно делали. Запускаем получившееся при- В общем, Кирилл
вьется с этим и
ложение на эмуляторе и получаем в пользова- «пустышками» уже
почти гол. Я у него с
ние вот такую замечательную читалку (рис. 9.5). самого начала, но до
сих пор не понимаю
толком, чего он от них
Совершенству, как известно, предела нет, поэто- добивается, да, честно
ГОВОРИ,и понять
му некоторые моменты остались на самостоя- особенно не
тельную доработку. Непроработана ситуация,
когда длина слова превышает размер экрана —
здесь нужна логика деления слова на части
и переноса на следующую строку. При чтении
последней страницы очередное смещение в стек
не заносится, поэтому при прокрутке текста на-
зад с конечной позиции одна страница будет
пропущена. Кроме этого, текст не должен содер-
жать в конце пробелов или переводов строки: это
может стать причиной формирования исключе-
ния при просмотре последней страницы. Еще
можно поработать над более удобным, интуи-
тивным перемещением по тексту, задействовав
не цифры, а навигационные клавиши аппарата,
помеченные стрелками. Для разных моделей те-
лефонов эти клавиши формируют разные коды, Рис. 9.5 . «Алло! Мистер Шухарт?»
----------------------- Page 105-----------------------
1 06 Глава 9 . Организация потоков, работа с файлами
поэтому лучше пользоваться API-константами конкретной модели. На API-функ-
циях некоторых моделей мы остановимся позже.
Класс ByteArraylnputStream
На примерах мы рассмотрели работу с файлом-ресурсом, который предоставлял
приложению все необходимые данные. На самом деле, поток может быть сфор-
мирован из любых данных и представлен в виде последовательности байтов. Еще
один класс, который порожден из базового класса InputStream и представляет по-
ток в виде открытого массива байтов, — это ByteArraylnputStream.
Данный класс предоставляет все объявленные, но не реализованные функции
класса InputStream, позволяя получить заранее число доступных байтов потока,
пометить позицию возврата для повторного чтения данных.
Внутренняя структура потока открыта для нас. Программисту доступны следу-
ющие поля:
• byte[ ] buf — массив байтов для хранения данных, указанный при создании потока;
• int count — число доступных байтов потока;
• int mark — индекс текущей помеченной позиции для повторного чтения;
• i nt pos — индекс следующего байта потока для чтения.
Использование объектов класса ByteArraylnputStream мы рассмотрим в следующей
главе.
Класс OutputStream
До сих пор мы работали с потоком только одного направления — от ресурса к при-
ложению. Все рассмотренные классы, так или иначе, связаны со словом Input (ввод),
а методы чтения данных — со словом read. Вся система ввода зеркально отобража-
ется на работу с потоком обратного направления. Поток вывода данных сохраняет
всю иерархию классов потока ввода, с той лишь разницей, что в названиях классов
вместо слова Input фигурирует слово Output: OutputStream, ByteArrayOutputStream,
DataOutputStream. Каждому методу чтения, содержащему в названии слово read, со-
ответствует аналогичный метод записи, только со словом wri te.
Функции потоков чтения ski p, avai I abl e, mark и reset для потоков вывода не акту-
альны, вместо них потоки вывода реализуют лишь одну функцию flush(), кото-
рая очищает все данные выходного потока.
* * *
Это были простейшие примеры использования потоков данных. Но кроме ресур-
сов приложения существуют также и сетевые ресурсы, ведь мобильный телефон
это все-таки средство связи. Организация сетей, поддержка работы с сетевыми
ресурсами, коммуникация между мидлетами и создание межсетевых приложений
реализуются с помощью потоков данных. На некоторых примерах использова-
ния потоков для сетевых данных мы остановимся в заключительных главах, по-
этому оставайтесь с нами: впереди еще много интересного.
----------------------- Page 106-----------------------
Глава 10
Ввод текста, работа со строками
В этой главе мы рассмотрим стандартные средства связи с пользователем, позво-
ляющие не только выбрать один из предложенных пунктов меню, но и вводить
с клавиатуры телефона произвольный текст, и реализуем долговременное хране-
ние введенных данных, используя технологии, рассмотренные нами в прошлых
главах, такие как потоки данных и RMS.
В наш век научно-технического прогресса сотовый телефон практически всегда
под рукой, в отличие от обычной авторучки. Часто ли вы сталкивались с ситуаци-
ей, когда нужно записать какую-то важную информацию: почтовый адрес, e-mail,
названия лекарств, маркировку деталей, да мало ли что? Стандартная записная
книжка мобильного телефона рассчитана лишь на запись телефонного номера
и не очень заковыристых имени и фамилии его обладателя. Можно, конечно, ис-
пользовать не по назначению ремайндер, если он есть, или писать текст в теле
сообщения SMS, но в таком случае потерять информацию так же легко, как и слу-
чайный клочок бумаги. Мы же попробуем сейчас написать свою адресную книгу,
расширив количество хранимой информации согласно нашим требованиям.
Стандартные средства предлагают нам два варианта для реализации полей ввода
текста: TextField и TextBox. Рассмотрим подробно каждый из них.
Класс TextBox
Класс TextBox является потомком класса Screen и представляет собой экран, по-
зволяющий вводить и редактировать текст с клавиатуры аппарата. При создании
объекта в конструкторе нужно указать заголовок поля ввода, начальное содержи-
мое поля, максимально допустимое количество вводимых символов и ограничи-
тель для вводимой информации:
TextBox(String t i t l e . String text , intmaxSize, int constraints)
Ограничители позволяют заранее задать тип вводимого текста. Так, например,
если информация содержит только цифры, то буквы не будут появляться в этом
поле при нажатии на клавиши. Для полей ввода доступны следующие ограничи-
тели, которые определяются константами класса TextField:
• ANY — допускается ввод любых символов без ограничений;
• EMAILADDR — используется для ввода адреса электронной почты;
• NUMERIC — применяется для ввода целых чисел. Клавиша * позволяет вводить
отрицательные числа и добавляет знак - перед числом;
----------------------- Page 107-----------------------
1 08 Глава 10 . Ввод текста, работа со строками
• PHONENUMBER — ввод телефонного номера. Поле ввода такого типа привязано
к стандартному вводу телефонного номера аппарата. Это значит, что в меню
автоматически появится команда поиска номера в записной книжке Search;
• PASSWORD — используется в комбинации с другими константами для скрытия вво-
димой информации. Вводимые символы отображаются звездочками, как при
вводе PIN-кода. Комбинации констант задаются с помощью оператора | (или),
например: NUMERIC | PASSWORD;
• URL — ввод сетевого адреса интернет-страницы в формате URL;
• CONSTRAI NT_MASK — маска ограничителя; константа, используемая для получения
текущего ограничителя поля из значения, возвращенного методом GetConst га i nts () .
Маска накладывается с помощью оператора & (и) на возвращенное значение
и отбрасывает флаги модификаторов ограничителя, например, такие как PASSWORD.
После того как объект поля ввода создан, он может быть отображен на экране теле-
фона так же, как и все отображаемые объекты, наследованные из класса Di spl ayabl e,
с помощью метода класса Di spl ay setCurrent(TextBox). Если вводимый текст превы-
шает размер экрана, система сама обеспечит прокрутку без каких-либо усилий
с нашей стороны. Дальнейшая работа с созданным объектом обеспечивается следу-
ющими методами класса TextBox:
• void delete (int offset, int length) — удаляет из поля ввода length символов,
начиная с позиции offset;
• int getCaretPositionO — возвращает текущее положение позиции ввода (0 —
начало текста);
• int getCharsCchart] data) — копирует весь текст из поля ввода в символьный
массив data, возвращает число скопированных символов. Текст, отображаемый
в поле ввода, не изменяется;
• int getConstraints() — возвращает текущий ограничитель поля ввода;
• i nt getMaxSi ze () — возвращает максимально допустимое количество вводимых
символов;
• String getStringO — возвращает содержимое поля ввода в виде строки;
• void insert (Strings re, int position) — вставляет строку src в поле ввода, на-
чиная с позиции position. Если указанная позиция меньше или равна нулю, то
строка вставляется в начало поля, если позиция больше количества уже вве-
денных символов, то строка вставляется сразу за последним символом;
• void insert(char[] data, int offset, int length, int position) — вставляет в поле
ввода с позиции, заданной переменной position, length символов из массива data,
начиная с символа с индексом offset. Логика вставки, как в предыдущем методе;
• void setChars(char[] data, int offset, int length)— очищает поле ввода и встав-
ляет length символов из массива data, начиная с позиции массива offset;
• void setConstrai nts (int constraints) — устанавливает ограничители поля ввода;
• int setMaxSized'nt maxSize) — устанавливает максимально допустимое количе-
ство вводимых символов;
• void setStri ng(String text) — заменяет текущее содержимое поля ввода на строку,
заданную параметром text ;
• int size() — возвращает количество символов, содержащееся в поле ввода.
----------------------- Page 108-----------------------
Класс TextField 10 9
Класс TextField
Объект класса TextField представляет собой редактируемую строку, отобража-
емую на экране телефона и позволяющую пользователю редактировать и вводить
текстовые символы с клавиатуры аппарата.
Разница между объектами TextBox и TextField такая же, как и между рассмотрен-
ными ранее объектами List и ChoiceGroup. Они реализуют один и тот же набор
методов, а различаются только тем, что первый является самостоятельным ото-
бражаемым объектом иерархии класса Displayable, а второй для демонстрации
должен быть включен в форму.
Класс TextFi el d расширяет рассмотренный нами в программе слайд-шоу класс Item,
а это значит, что объект класса TextFi el d можно включить в обычную форму (class
Form) с помощью стандартного метода формы append.
Для примера рассмотрим программу калькулятора, выполняющего простые ариф-
метические действия. В калькуляторе будут использоваться три поля ввода: два
для аргументов и одно для символа арифметического действия:
import javax.microedition.lcdui.TextField :
private Display display :
private Form form :
private Command equal :
private MyCommandListener c l = new MyCommandListenerO;
private TextField argl.arg2.action : // поля ввода
public void startAppO {
// создать форму калькулятора
display - Display.getDisplay(this) :
form = new Form("Calculator") :
form.setCommandListener(cl):
// команда , запускающая вычисления
equal = new Command("Equal" . Command.OK , 1) :
form.addCommand(equal);
// поле ввода первого аргумента
a r g l = new TextFieldC'Argumentl " ."".5.TextField.NUMERIC) ;
form.append(argl) :
// поле ввода символа операции
action = new TextField("Action"."".l.TextField.ANY) :
form.append(action) :
// поле ввода второго аргумента
arg2 = new TextField("Argument2"."".5.TextField.NUMERIC) ;
----------------------- Page 109-----------------------
1 10 Глава 10 . Ввод текста, работа со строками
form.append(arg2);
display.setCurrent(form);
private class MyCommandListener implements CommandListener
{
public void commandAction(Command с Displayable d)
{
if(c==equal) { // запустить вычисление
// форма для экрана результата операции
Form resForm = new Form("Result") ;
resForm.setCommandListener(cl);
Command back = new CommandC'Back",Command.BACK.1) ;
resForm.addCommand(back) :
// проверить заполнение всех полей ввода
if(argl.size()==O || arg2.size()==0 || action.size()==O ) {
// ошибка , не все поля ввода заполнены
resForm.appendC"Missing argument") :
// отобразить сообщение
display.setCurrent(resForm);
// выйти
return;
// получить символ операции
char[] act = new char[l] ;
action.getChars(act);
// преобразование аргументов
int first = Integer.parselnt(argl.getStringO) :
int second - Integer.parseInt(arg2.getString()) ;
int res = 0 :
switchCact[O]) {
// выполнить действие в соответствии
// с символом операции
case '+' :
res - first + second :
break :
case
res = first - second :
break; ,
case '*':
res = first * second ;
break ;
----------------------- Page 110-----------------------
Класс AddressBook 111
case '/';
res = first / second ; ,
break ;
default :
// ошибка , некорректный символ операции
resForm.append("IIlegal Operation") :
// отобразить сообщение
display.setCurrent(resForm);
// выйти
return;
// преобразовать результат в строку и добавить в форму
resForm.appendUnew Integer (res) ) .toStringO) ;
// отобразить результат
display.setCurrent(resForm);
else
// команда Back формы результата приводит сюда
// отобразить первоначальную форму
display.setCurrent(form);
Вид и размещение полей ввода на экране зависит от
конкретной модели аппарата. На стандартном эмуля-
торе наш калькулятор будет выглядеть следующим об-
разом (рис. 10.1). calculator
Казалось бы, можно радоваться, поскольку мы полу- s.rgument11
чили бесплатный калькулятор, который отсутствует Action i
во многих моделях. Главная проблема состоит в том, <\rgument2
что виртуальная машина CDLC не поддерживает вы-
числений с плавающей точкой. Другими словами, наш
калькулятор сможет работать только с целыми числа-
ми, отбрасывая при делении дробную часть. К сожа-
лению, такой калькулятор сгодится лишь для подсче-
та сдачи в магазине.
Класс AddressBook
В прошлых главах мы рассмотрели методы хранения
информации в памяти телефона, создание меню, а так-
же потоковую передачу данных с использованием по-
токов ввода и вывода. Сейчас нам понадобятся все эти
знания, поскольку тот пример, который мы собираем-
ся реализовать, будет хранить данные с помощью RMS Рис. 10.1 . Так выглядит
и передавать их с помощью потоков. простейший калькулятор
----------------------- Page 111-----------------------
1 12 Глава 10 . Ввод текста, работа со строками
Порой мы недовольны стандартной записной книжкой телефона, которая, конеч-
но же, очень классная, только в ней для полного счастья не хватает какой-то очень
важной функции. Выход есть, ведь можно реализовать свою записную книжку,
где учесть все свои прихоти, а при надобности добавить еще и чужие. Этим мы
сейчас и займемся: реализуем основу для записной книжки, которая будет хра-
нить записи, содержащие имя, номер телефона и адрес электронной почты.
Для начала продумаем сценарий смены экранов. Приложение стартует с экрана
со списком всех имен, уже добавленных в записную книжку. Команда Ok демонст-
рирует форму, содержащую все параметры записи, соответствующей выбранно-
му имени, а команда Add начинает выполнение сценария добавления новой запи-
си. Сценарий добавления записи предоставляет по очереди несколько экранов для
ввода каждого из параметров, команда Next выполняет переход к следующему эк-
рану, а команда Back — возврат к предыдущему.
Рассмотрим основной класс мидлета AddressBook, который в качестве полей со-
держит экран списка имен, экраны сценария добавления записи, а также команды
перехода между экранами. Стартовый метод startAppC) инициализирует экраны и
команды и создает список имен из уже имеющихся записей:
import javax.microedition.midlet.MIDlet :
import javax.microedition.icdui.Command :
import javax.microedition.lcdui.CommandUstener ;
import javax.microedition.lcdui.Display :
import javax.microedition.lcdui.Displayable :
import javax.microedition.lcdui.Form :
import javax.microedition.lcdui.List :
import javax.microedition.lcdui.TextField :
import javax.microedition.lcdui.TextBox :
import Java.iо.ByteArrayOutputStream :
import java.io.ByteArraylnputStream ;
import java.io.DataOutputStream :
import java.io.DatalnputStream ;
import java.io.IOException :
import javax.microedition.rms.RecordStore :
import javax.microedition.rms.RecordEnumeration :
import javax.microedition.rms.RecordComparator :
import javax.microedition.rms.RecordStoreException :
public class AddressBook extends MIDlet implements CommandListener
{
private Display display : // менеджер дисплея
private RecordStore recordStore ; // хранилище записей
private List nameList : // список имен
private int reclndexes[] : // массив ID записей .
// соответствующий списку имен
private Command add.ok.next.back : // команды переходов между экранами
private TextBox tbName.tbPhone.tbEMail : // экраны ввода параметров записи
----------------------- Page 112-----------------------
Класс AddressBook 1 1 3
public void startAppO {
try {
// открыть хранилище записей с именем "Address-Book"
recordStore - RecordStore.openRecordStoreCAddress-Book" , true) ;
} catch (RecordStoreException rse ) { }
// получить ссылку на менеджер дисплея
display • Oisplay.getDisplay(this) :
// создание объектов команд переходов
ok - new CommandC'Ok" , Command.OK , 1) ;
add = new CommandC'Add" , Command.BACK , 1) :
next = new Command("Next" . Command.OK . 1) :
back » new Command("Back" , Command.BACK . 1) :
// поле ввода имени
tbName - new TextBox("Name:"."".15,TextField.ANY) :
tbName.addCoiraiand(next):
tbName.addCommand(back);
tbName.setCommandLi stener(this) :
// поле ввода номера телефона
tbPhone = new TextBox("Number:"."".15.TextField.PH0NENUMBER) :
tbPhone.addCommand(next) :
tbPhone.addCommandCback) :
tbPhone . setCommandLi stener (thi s«);
// поле ввода электронной почты
tbEMail = new TextBox("E-Mail:"."".35.TextField.EMAILADDR) :
tbEMail.addCommand(next);
tbEMa i1.addCommand(back) ;
tbEMail.setCommandListener(this) :
// создать список имен
BuildNameListO ;
// отобразить список имен на экране
di splay.setCurrent(nameLi st) ;
}
Обратим внимание, что если в предыдущей программе для ввода информации мы
использовали несколько объектов класса TextField, которые добавляли в одну
форму, то теперь каждое поле ввода представляет собой отдельный экран класса
TextBox. Каждый экран содержит команды Next и Back, которые осуществляют пе-
реходы от одного экрана ввода к другому.
Создание списка имен реализовано в методе BuildNameListO, который создает
и инициализирует новый объект списка, затем получает все записи хранилища,
отсортированные по именам в алфавитном-порядке. Имена добавляются в список
----------------------- Page 113-----------------------
114 Глава 10 . Ввод текста, работа со строками
имен, a ID соответствующих записей — в массив reclndexes, чтобы при надобнос-
ти можно было легко получить все остальные параметры необходимой записи:
// метод создает список имен из адресной книги
private void BuiidNameListO {
// создать объект списка
nameList = new List("Address-Book" . List.IMPLICIT) :
nameList.setCommandListener(this) :
// команда добавления записи
nameList.addComiTiand(add);
// команда получения параметров записи
nameList.addCommand(ok);
try {
// получить количество записей
int size = recordStore.getNumRecordsO :
// создать массив для хранения ID записей
reclndexes = new int[size] :
// создать объект компаратора в алфавитном порядке
AlphabeticalOrdering comparator = new AlphabeticalOrderingO ;
// получить список записей хранилища
RecordEnumeration re » recordStore.enumerateRecords
.. (null , comparator , false) ;
// индекс массива хранения ID записей
int 1=0 ;
// бесконечный цикл ; выход из цикла происходит
// по формированию исключения получения ID следующей записи ,
// после того как было получено ID последней записи
while(true ) {
// получить ID следующей записи
int id = re.nextRecordldO ;
// записать ID в массив
reclndexes[i++]=id :
// получить запись no ID
byte[ ] record = recordStore.getRecord(id) ;
// преобразовать запись в байтовый поток
ByteArraylnputStream bais = new ByteArraylnputStream(record) ;
// создать поток , поддерживающий чтение по типу
DatalnputStream dis = new DatalnputStream(bais) ;
// считать из потока первую строку и добавить в список
nameList . appendtdis.readUTFO.null) :
catch(RecordStoreException rse )
catchdOException ioe) ' { }
----------------------- Page 114-----------------------
Класс AddressBook 115
Алфавитный порядок добавления записей в список имен реализован с помощью
компаратора, представленного классом Alphabetical Ordering:
// класс компаратора записей по алфавиту
private class Alphabetical Ordering implements RecordComparator {
// метод сравнения записей
public in t compare(byte[] reel . byte[] rec2) {
// преобразовать записи в байтовый поток
ByteArraylnputStream bais l - new ByteArraylnputStream(recl) :
ByteArraylnputStream bais2 = new ByteArrayInputStream(rec2);
// создать потоки, поддерживающие чтение по типу
DatalnputStream dis l = new DatalnputStream(baisl) ;
OatalnputStream dis2 = new DataInputStream(bais2) :
// строки имен
String name! = null :
String name2 = null ;
t r y {
// считать строки с именами
namel = disl.readUTF С);
name2 = dis2.readUTF О :
catch (IOException ioe ) { }
// лексикографическое сравнение строк
int result = namel.compareTo(name2) ;
if (result < 0 )
// первая запись предшествует второй
return RecordComparator.PRECEDES :
else
if (result == 0 )
// записи содержат идентичные имена
return RecordComparator.EQUIVALENT:
else
// вторая запись предшествует первой
return RecordComparator.FOLLOWS:
Таким образом, остается реализовать только блок про-
слушивания команд, который определяет действия для
осуществления переходов между экранами. Если же
создать пустой метод commandActi on, то приложение уже
можно компилировать. Запустив приложение, при на-
личии записей, мы могли бы увидеть список имен, по
которому можно перемещаться с помощью навигаци-
онных клавиш (рис. 10.2). Рис. 10.2 . Если встроенная
записная книжка вас не
Осталось сделать последний шаг и написать олок про- устраивает, можно
слушивания команд, который будет контролировать создать свою
----------------------- Page 115-----------------------
116 Глава 10 . Ввод текста, работа со строками
порядок отображения экранов в зависимости от выбранной команды, а также ре-
ализует сценарий добавления новой записи:
// блок прослушивания команд
public void commandAction(Command с . Displayable d ) {
// команда Add отображает экран ввода имени
1f(c==add) display.setCurrent(tbName) :
// команда Ok
if(c==ok) {
// если команда вызвана из экрана со списком имен
if(d==nameList) {
try {
// получить в массиве reclndexes ID необходимой записи
int id = recIndexes[nameList.getSelectedIndex()] ;
// считать необходимую запись
byte[ ] record = recordStore.getRecord(id) :
// преобразовать запись в байтовый поток
ByteArraylnputStream bais =
new ByteArraylnputStream(record) :
// создать поток , поддерживающий чтение по типу
DatalnputStream dis = new DatalnputStream(bais ) ;
// создать форму для отображения параметров
Form infoForm = new FormC'") :
// считать строки с параметрами и добавить в форму
infoForm.append(dis.readUTF()+"\n");
infoForm.append(dis.readUTF()+"\n");
i nfoForm.append{di s.readUTFC)+"\n") ;
// добавить команду возврата к списку имен
infoForm.addCommand(ok);
infoForm.setCommandListener(this) :
// отобразить форму на экране
display.setCurrent(infoFdrm) :
}
catch(RecordStoreException rse ) {}
catchdOException ioe ) { }
} else { // если команда вызвана из формы параметров записи .
// вернуться к списку имен
display.setCurrent(nameList);
// команда Next : перейти к следующему полю ввода
if(c==next) {
// из поля ввода имени к полю ввода номера телефона
if(d==tbName) display.setCurrent(tbPhone) :
----------------------- Page 116-----------------------
Класс AddressBook 1 1 7
// из поля ввода номера телефона к полю ввода e-mail
if(d==tbPhone) display.setCurrentCtbEMai 1) :
// из поля ввода e-mail
if(d==tbEMail) {
.// создать байтовый поток вывода
ByteArrayOutputStream baos - new ByteArrayOutputStreamt) ;
// создать поток вывода , поддерживающий запись по типу
DataOutputStream dos = new DataOutputStream(baos) :
try {
// записать введенные параметры в поток вывода
dos.writeUTF(tbName.getStringO);
dos.wri teUTF(tbPhone.getStri ng С)) :
dos.writeUTF(tbEMail.getStringO);
// добавить запись в хранилище
recordStore.addRecord(baos.toByteArray() . 0 . baos.sizeO) :
}
catchUOException ioe ) { }
catch (RecordStoreException rse ) { }
// создать список имен
BuildNameListO ;
// отобразить список имен на экране
display.setCurrent(nameLi st) ;
// команда Back : возврат к предыдущему полю ввода
if(c==back) {
// из поля ввода имени к списку имен
i f(d==tbName ) di splay.setCurrent(nameLi st) ;
// из поля ввода номера телефона к полю ввода имени
if(d==tbPhone) display.setCurrent(tbName) ;
// из поля ввода e-mail к полю ввода номера телефона
if(d--tbEMail) display.setCurrent(tbPhone) :
На этом пока остановимся. Теперь все команды должны работать, можно запус-
кать и пробовать. Экран ввода параметров записи будет выглядеть следующим
образом (рис. 10.3).
После того как мы ввели параметры записи и добавили ее в хранилище, введенное
имя записи появится в списке. Можно выбрать его, используя навигационные кла-
виши, и с помощью команды Ok вывести на экран параметры записи (рис. 10.4).
Основное направление движения задано. Дальше вы можете продолжить самосто-
ятельно. Первым делом нужно реализовать поиск записи по имени или его фраг-
менту с помощью фильтра записей хранилища. Не лишней при добавлении новой
записи будет проверка, существует ли уже запись с подобным именем. Такую про-
верку можно организовать с помощью того же фильтра. Я уверен, что богатая фан-
----------------------- Page 117-----------------------
118 Глава 10 . Ввод текста, работа со строками
тазия программиста подскажет вам еще массу приятных и полезных нововведений.
Одно из возможных усовершенствований мы подробно рассмотрим в следующей
главе. Я думаю, что такого вы еще не видели ни в одной модели телефона.
Щ
РИС. 10.3 . Экран ввода параметров Рис. 10.4 . Информация из записной книжки
записи на стандартном эмуляторе выведена на экране
Один большой минус заключается в том, что, в общем случае, доступа из мидлета
к стандартной адресной книге телефона и ее записям нет, то есть невозможно по-
лучить программно записи из стандартной адресной книги. Иногда, если повезет,
такой доступ предоставляет API производителя конкретной модели телефона.
В этой главе мы рассмотрели на примерах работу с полями ввода, представленны-
ми объектами классов TextBox и TextFi el d. Даже в небольшой программе адресной
книги нам понадобились знания почти из всех предыдущих глав. Мы использова-
ли интерфейс высокого уровня для связи с пользователем и реализации логики
смены экранов, создали список имен с помощью объекта класса Li st, организова-
ли долговременное хранение записей с использованием RMS, осуществили за-
пись и чтение данных с помощью потоков ввода-вывода. Возможности мобильно-
го языка J2ME очень ограничены, нетрудно разобраться со всеми и попробовать
их в действии. Это поможет реально оценивать свои силы при проектировании
функциональности программ.
----------------------- Page 118-----------------------
Глава 1 1
Работа со временем,
датой и календарем
Продолжим наше знакомство с программированием для мобильных телефонов.
Современный аппарат — это больше, чем средство связи, ведь он напичкан все-
возможными, нужными и не очень, дополнениями, которые к самой связи имеют
отдаленное отношение. Некоторые из усовершенствований, которые продавцы са-
лонов связи демонстрируют первым делом, покажутся откровенно лишними,
а без других уже сложно представить себе телефон. Например, с появлением те-
лефона я совсем перестал пользоваться наручными часами, настольным будиль-
ником и карманным калькулятором.
Реализация J2ME предоставляет нам возможность пользоваться внутренним вре-
менем и календарем телефона. Стоит подключить фантазию и таким возможнос-
тям можно легко найти применение. Например, составление ежедневных индиви-
дуальных гороскопов или организация испытательных тридцатидневных версий
приложений. В этой главе мы рассмотрим использование объектов календаря и даты
для расширения записной книжки функцией напоминания о днях рождения.
Класс Date
Объект класса Date представляет собой текущее время с точностью до миллисекунд.
Время представлено в миллисекундах, прошедших с полуночи 1 января 1970 года.
Информация, безусловно, нужная, но в таком виде совершенно бесполезная. К со-
жалению, класс Date не содержит интерпретаторов для перевода даты в более по-
нятный для нас формат. Для этого нужно использовать класс Calendar, который мы
рассмотрим позже.
Класс Date имеет всего несколько методов. Все они оперируют с датой и временем
лишь в указанном формате. Рассмотрим эти методы:
• Date() — конструктор; создает новый объект даты, содержащий текущее время;
• Date( long date) — конструктор; создает новый объект даты, содержащий время,
заданное в аргументе date;
• boolean equals (Object obj) — возвращает значение true в том случае, если две
даты идентичны с точностью до миллисекунды;
• long getTime() — возвращает время, представленное объектом даты;
• void setTimef long time) — устанавливает объект даты в момент времени, пред-
ставленный параметром time.
----------------------- Page 119-----------------------
120 Глава 1 1 . Работа со временем, датой и календарем
Вот, собственно, и все. Разнообразием и гибкостью методов класс Date нас не ра-
дует. Самое время сформулировать задачу, которую мы хотим реализовать. К про-
грамме записной книжки, которую мы разбирали в прошлой главе, добавим фун-
кцию напоминания о днях рождения записанных абонентов. Для этого потребуется
расширить формат записи, содержащейся в хранилище записей, добавив поле даты
рождения. В сценарий ввода данных о новом абоненте потребуется добавить эк-
ран для ввода даты рождения. Здесь нас ждет приятный сюрприз, потому что поле
ввода даты предоставляет нам пользовательский интерфейс высокого уровня
в виде класса DateField.
Класс DateField
Класс DateField является потомком класса Item, то есть для его демонстрации на
экране нам потребуется еще и форма. Поле может работать в трех режимах ввода:
ввод только даты, ввод только времени, ввод даты и времени. При вводе только
времени дата по умолчанию будет соответствовать 1 января 1970 года. Для рабо-
ты с полем ввода класс DateField предоставляет следующие методы:
• DateField(String 1 abel. int mode) — конструктор; создает поле ввода даты с за-
головком 1 abel. Режим поля ввода определяется аргу-
ментом mode, который может принимать значение од-
ной из следующих определяющих констант:
• DATE — ввод только даты;
• DATE_TIME — ввод даты и времени;
• TIME — ввод только времени;
• Date getDateO — возвращает введенное значение
даты и времени в формате класса Date. При созда-
нии объекта исходное значение даты и времени не
определено, поэтому при запросе отсутствующей
введенной даты будет возвращено значение nul 1;
• i nt get I nputMode () — возвращает режим ввода поля
в виде определяющей константы;
• voi d setDate (Date date) — устанавливает в поле ввода
новое значение, представленное параметром date;
• void setInputMode( int mode) — устанавливает новый
режим ввода, заданный аргументом mode в виде оп-
ределяющей константы;
• void setLabel (String label) — устанавливает новый
заголовок поля ввода.
Поскольку поле ввода даты предоставлено интерфей-
сом высокого уровня, то внешний вид поля и логика
ввода зависят от конкретной модели телефона. Напри-
ИС 8 м е в
даты в стандартной ™ Р > стандартном эмуляторе от WTK поле ввода даты
эмуляторе WTK выглядит следующим образом (рис. 11.1).
----------------------- Page 120-----------------------
Класс DateFieid 1 2 1
На данном этапе информации достаточно, чтобы начать дополнение нашей за-
писной книжки. Прежде всего, импортируем необходимые нам библиотеки и до-
бавим в класс AddressBook два новых поля, которые будут предоставлять возмож-
ность ввода даты рождения:
import javax.microedition.1cdui.DateField :
import java.util.Date ;
import java.util.Calendar :
private Form dateForm ; // форма ввода дня рождения
private DateFieid dateField : // поле ввода дня рождения
В методе startApp() проинициализируем поле ввода даты аналогично полям вво-
да остальной личной информации:
// экран ввода дня рождения
dateField = new DateField("Birthday".DateFieid.DATE) :
dateForm = new FormC'") :
dateForm.append(dateField);
dateForm.addCommand(next) :
dateForm.addCommand(back);
dateForm.setCommandLi stener(thi s) ;
В блок прослушивания команд добавим логику демонстрации экрана ввода
дня рождения, а также дополнительное поле в форме отображения парамет-
ров записи. Хранимая запись теперь будет состоять из четырех частей: три тек-
стовых части и одна типа long, содержащая дату рождения в формате класса
Date.
Экран информации записи будет теперь формироваться следующим образом:
// если команда вызвана из экрана со списком имен
if(d==nameList) {
try {
// создать объект даты рождения
Date birthday = new Date(dis.readLongO) ;
// создать объект поля ввода даты
DateField df = new DateFi el d ( " \ DateField . DATE) :
// установить дату рождения в поле ввода даты
df.setDate(birthday) :
// добавить даты рождения в форму отображения параметров
infoForm.append(df);
display.setCurrent(infoForm);
----------------------- Page 121-----------------------
122 Глава 11 . Работа со временем, датой и календарем
Запись, в свою очередь, будет формироваться так:
try {
// записать введенные параметры в поток вывода
dos.wri teUTF(tbName.getSt r i n g ( ) ) ;
dos.writeUTF(tbPhone.getStringO):
dos.wnteUTF(tbEMail.getStnngO):
dos.writeLong(dateField.getDateO.getTimeO);
// добавить запись в хранилище
recordStore.addRecord(baos.toByteArray(), 0 . baos.sizeO) :
С корректной сменой экранов при обработке команд
Next и Back, думаю, вы справитесь самостоятельно. Те-
перь во время ввода нового контакта появится поле
ввода даты рождения, а форма информации об абонен-
те будет выглядеть как на рис. 11.2.
Заметим, что записи, введенные в программе из пре-
дыдущей главы, имеют другой формат, поэтому при
попытке считать их в новом формате будет формиро-
ваться исключение, а форма с информацией демонст-
рироваться не будет.
Осталось реализовать самый важный момент: нужно
получить текущую дату и сравнить ее с датами рожде-
ния, содержащимися во всех записях . Сейчас самое
время познакомиться с классом Calendar, без которого
пересчет компьютерных миллисекунд в человеческие
дни и месяцы изрядно попортил бы нам нервы.
Класс Calendar
Класс Calendar является абстрактным классом для ра-
боты с датой и временем, содержащим набор констант,
представляющих дни недели и месяцы. Для работы
с месяцами определены константы от JANUARY до DECEMBER,
Р и с - | а для работы с днями недели константы от MONDAY до
абоненте добавлена дата SUNDAY. Поскольку класс Calendar является абстрактным,
рождения м ы н е м о ж е м просто создать объект этого класса, но мы
можем получить его с помощью специального метода:
• stati c Calendar getlnstanceO — получить объект календаря, представляющий
текущие дату и время.
Класс Cai endar связан с уже рассмотренным классом Date следующими методами:
• Date getTime() — возвращает текущее время, представленное объектом кален-
даря в виде объекта класса Date;
• void setTime(Date date) — устанавливает календарю текущее время, представ-
ленное объектом класса Date.
----------------------- Page 122-----------------------
Класс TimeZone 1 2 3
Преобразование времени и даты осуществляется с помощью методов get и set :
• i n t get (int field ) — возвращает преобразованно е значени е одного из парамет-
ров календаря . Параметр определяется аргументом field . Аргумент file d мо-
жет п р и н и м а т ь значени е одно й из следующих констант : YEAR, MONTH, DATE,
DAY_OF_WEEK, HOUR_OF_DAY, HOUR, AM_PM, MINUTE, SECOND, MILLISECOND. Метод возвраща-
ет значени е запрошенног о параметра в виде константы , определяющей месяц
или день недели , либ о числовое значени е запрошенной единицы времени . Кон-
станта HOUR_OF_DAY определяет значение часа в 24-часовом формате , а констан-
та HOUR — в 12-часовом . Констант а АМ_РМ используется в 12-часовом формат е
для определения значения «до полудня» (AM) или «после полудня» (РМ);
• void s e t d n t field , int value) — устанавливает значени е value в один из пара-
метров даты или времени , который определяется аргументом field . Параметр
DAY_OF_WEEK не может быть установлен .
Таким образом , чтобы из объекта класса Date получить дату и время в виде удоб-
ных констант , необходимо получить объект календаря , присвоить ему время , пред-
ставленное объектом Date, и лиш ь затем интерпретировать его, используя методы
класса Calendar . Так ж е как и в классе Date, календарь может работать со време-
нем, представленным количеством миллисекунд , прошедших от начала 70-х:
• long getTimelnMi11 is() — возвращает время календаря в миллисекундах;
• void setTimelnMi Hi sdong mill is) — устанавливает время календаря, заданное
параметром mi 11 is .
Класс Calendar организует сравнение времени двух календарей с помощью следу-
ющих методов:
• boolean equals (Object obj) — возвращает значение true, если аргумент obj не
равен null, и представляет то же самое время, что и вызывающий объект ка-
лендаря;
• boolean before(Object when) — возвращает значение true, если время, представ-
ленное вызывающим объектом, ранее времени, представленного аргументом
obj;
• boolean after(Object when) — возвращает значение true, если время, представлен-
ное вызывающим объектом, позднее времени, представленного аргументом obj.
Кроме того, класс Calendar поддерживает работу с часовыми поясами:
• TimeZone getTimeZone() — получить часовой пояс календаря;
• void setTimeZone(TimeZone value) — установить часовой пояс календаря.
Класс TimeZone
Для полноты картины предоставляемых реализацией J2ME возможностей, рас-
смотрим абстрактный класс TimeZone, представляющий часовой пояс. Класс TimeZone
позволяет узнать, какой часовой пояс установлен на аппарате, выполняющем про-
грамму, и осуществляется ли переход на летнее время. Класс TimeZone имеет сле-
дующий набор методов:
• String[] getAvailablelDsO — возвращает аббревиатуры всех поддерживаемых
аппаратом часовых поясов в виде массива строк;
----------------------- Page 123-----------------------
1 24 Глава 1 1 . Работа со временем, датой и календарем
• TimeZone getDefaultC) — возвращает часовой пояс, используемый аппаратом по
умолчанию;
• String getID() — возвращает аббревиатуру текущего часового пояса, использу-
емого аппаратом;
• i nt getRawOf f set () — возвращает смещение данного часового пояса относительно
времени по Гринвичу;
• TimeZone getTimeZone(String ID) — получить объект часового пояса по его аббре-
виатуре;
• boolean useDaylightTimeO — возвращает значение true, если часовой пояс ис-
пользует переход на летнее время (DST — Daylight Savings Time).
Класс BirthdayFilter
Теперь мы располагаем всеми необходимыми средствами для реализации функции
напоминания о дне рождения в нашей записной книжке. Для выбора именинников
из хранилища записей реализуем класс фильтра Bi rthdayFi lter , который будет срав-
нивать даты рождения с текущей датой и выбирать лишь интересующие нас записи:
// класс фильтра записей по дням рождения
private class BirthdayFilter implements RecordFilter {
// метод сравнения записей
public boolean matches(byte[ ] candidate ) {
// преобразовать запись в байтовый поток
ByteArraylnputStream bais • new ByteArraylnputStream(candidate) ;
// создать поток , поддерживающий чтение по типу
DatalnputStream dis - new DatalnputStream(bais) :
// день рождения
Date birthDate = new DateO :
try {
// считать строковые параметры записи
dis.readUTFO :
dis.readUTFO ;
dis.readUTFO :
// считать дату рождения
bi rthDate.setTime(di s.readLong()) ;
}
catch (IOException ioe ) { return false ; }
// получить два календарях текущей датой
, Calendar rightNow » Calendar.getlnstanceO :
Calendar birthday - Calendar.getlnstanceC) ;
// установить дату рождения именинника
birthday.setTime(birthDate) :
// сравнить день и месяц рождения с текущей датой
if(rightNow.get(Calendar.DAY_OF_MONTH) == birthday.get(Calendaг.DAY_OF_MONTH ) & &
----------------------- Page 124-----------------------
Класс BirthdayFilter 1 2 5
rightNow.get(Calendar.MONTH) ~ birthday.get(Calendar.MONTH) )
return true :
else
return false ;
Реализуем функцию, которая будет фильтровать хранилище записей по текущей
дате и возвращать имя человека, у которого сегодня день рождения, или значение
null, если таковых не имеется:
// метод SearchBirthday возвращает строку с именем именинника ;
// если в этот день именинников нет . то возвращает null
private String SearchBirthday( ) {
String name = null ;
try {
// создать объект фильтра по дню рождения
BirthdayFilter filter = new BirthdayFilterO ;
// получить список записей с подходящим днем рождения
RecordEnumeration re = recordStore.enumerateRecords(filter . null , false) ;
// получить ID записи
int id • re.nextRecordldO ;
// получить запись по ID
byte[ ] record = recordStore.getRecord(id) :
// преобразовать запись в байтовый поток
ByteArraylnputStream bais = new ByteArrayInputStream(record) ;
// создать поток , поддерживающий чтение по типу
DatalnputStream dis - new DatalnputStream(bais) :
// считать из потока строку с именем
name = dis.readUTFO :
}
catch(RecordStoreException rse ) { }
catch(IOException ioe ) { }
// вернуть имя именинника
return name ;
}
Все, что нам остается сделать, это вызвать в стартовом методе приложения sta rtApp()
функцию поиска именинников и при необходимости вывести соответствующее
напоминание:
// создать список имен
BuildNameListO ;
// поиск именинника
String name • SearchBirthdayO ;
// если имя найдено
if(name!=null) {
// создать форму напоминания
----------------------- Page 125-----------------------
1 26 Глава 11 . Работа со временем, датой и календарем
Form remindForm = new Form("Reminder"):•
// добавить напоминание в форму
remindForm.append(name + " has a birthday today!") :
// добавить команду возврата
remindForm.addCommand(ok);
remindForm.setCormiandListener(this);
// отобразить форму
di splay.setCurrent(remi ndForm);
} else
// отобразить список имен на экране
display.setCurrent(nameList);
Теперь все готово. При первом запуске нужно добавить
контакт с текущей датой рождения, а затем заново за-
пустить приложение. Если все запрограммировано вер-
но, то при запуске появится сообщение, показанное
на рис. 11.3.
Основная недоработка программы заключается в том,
что если у нескольких ваших знакомых день рождения
в один день, то напоминание все равно будет только
одно. Неплохо было бы также добавить возможность
удаления и редактирования контактов. Напоминание
можно красочно оформить, а также с легкостью высчи-
тать возраст именинника. Все эти детали, а также лю-
бые другие капризы вашей фантазии, остаются вам на
самостоятельную доработку.
Итак, в этой главе мы изучили работу с датой, вре-
менем, календарем и часовым поясом. Мы рассмот-
рели лишь один пример из всего многообразия воз-
можных приложений, имеющих дело со временем
и датой. Часовой пояс, например, можно использо-
вать для интернационализации программ и автома-
тического выбора языка интерфейса. А теперь може-
Рис. 11.3. Телефон напомнит л -
вам, кого надо поздравить т е сделать паузу и опробовать изученный материал.
с днем рождения В добрый путь!
----------------------- Page 126-----------------------
Глава 12
Работа над ошибками
С обработкой исключительных ситуаций мы уже сталкивались неоднократно во
время реализации наших нехитрых примеров. Пришло время подробнее разоб-
раться со структурой и механизмом формирования исключений.
Во время выполнения программы может случиться много разных неприятнос-
тей, таких как деление на ноль, выход индекса массива за допустимые пределы,
неверное имя загружаемого файла и масса других. В функционально-ориенти-
рованных языках программирования, таких как С или Pascal, такие ситуации
обрабатывались с помощью многочисленных
операторов if...else, которые значительно уве- Pfe .lools . Help
личивают как объем, так и время выполнения
программы. В объектно-ориентированных язы-
ках реализован другой подход к обработке вне-
штатных ситуаций.
a—»»TestSarnple ™
Класс Exception java/lang/ArithrneticExce
ption
При возникновении исключительной ситуации
система формирует соответствующий объект-
исключение, содержащий сведения об ошибке,
и передает его на обработку программе, в кото-
рой произошла ошибка. Если программа не пре-
дусматривает особую обработку данного исклю-
чения, то объект передается системе, которая
останавливает выполнение программы и выво-
дит на экран тип сформированного исключения.
При делении на ноль, например, это происходит
как показано на рис. 12.1.
Остановку выполнения программы можно пре-
дотвратить, реализовав в программе обработку
возможных исключений. Как мы уже видели на
примерах, команды, потенциально формиру-
ющие исключения, заключаются в блок try{...},
а действия, которые следует выполнить в ис-
ключительной ситуации, заключаются в блок Рис. 12.1 . В случае деления на ноль
catch (Exception e) {...}. В качестве аргумента опе- программа прерывается
----------------------- Page 127-----------------------
128 Глава 12 . Работа над ошибками
ратор catch принимает объект исключения определенного типа в зависимости от
ситуации, которую мы хотим обработать. После одного блока try может следо-
вать несколько блоков catch с обработкой разных исключений. Управление будет
передано первому блоку, аргумент которого будет соответствовать сформирован-
ному исключению. Таким образом, синтаксис блока проверки и обработки исклю-
чений в общем случае выглядит так:
t r y { . . .
}
c a t c h C . . . ) { . . . }
c a t c h C . ) { . . . }
Остановимся на объектах исключений, которые формируются системой. Все они
наследованы от одного и того же класса Exception, который, в свою очередь, по-
рожден от класса Throwable. Только объекты, порожденные от класса Throwable,
могут быть обработаны в блоке catch или искусственно сформированы с помо-
щью оператора throw, который мы рассмотрим немного позже.
Класс Throwabl e и все его расширения содержат два конструктора:
• Throwablе() — конструктор по умолчанию;
• ThrowableCString message) — объект исключения будет содержать текстовую
информацию, переданную в строке message.
Класс Throwable несет дополнительную информацию о сформированном исклю-
чении, которую можно получить с помощью следующих методов:
• String getMessage() — возвращает строку с текстовой информацией об исклю-
чительной ситуации. Если объект был создан пользователем с помощью кон-
структора по умолчанию, то возвращает значение nul 1;
• String toStringO — возвращает строку с текстовым описанием исключения,
. состоящим из имени класса сформированного исключения, двоеточия (разде-
лителя) и текста, возвращаемого методом getMessage;
• void printStackTraceO — выводит в область сообщения об ошибках название
класса и метода, сформировавших исключение, со всей предыдущей историей
вызовов, включая внутреннюю реализацию. Первая выводимая строка содер-
жит результат работы метода toStri ng.
Система определяет два потока вывода сообщений: поток вывода информации
и поток вывода сообщений об ошибках. Потоки вывода доступны из любой точки
программы. Мы можем писать туда любую полезную информацию, включая со-
держимое переменных, с помощью системных методов: System.out.printlnCString
text) и System.err.println(String text). В нашем случае сообщения обоих пото-
ков появятся в основном окне WTK. При запуске приложения на реальном аппа-
рате никаких сообщений мы можем и не увидеть.
Теперь, если деление на ноль поместить в блок try, а в блоке catch вывести в сис-
темную область всю информацию о сформированном исключении следующим
образом:
int n=0:
try {
----------------------- Page 128-----------------------
Класс Exception 129
int к • 5/n:
} catch(Exception exc) {
System.out.printlnC'Get Massage: " + exc.getMessageO):
System.out.printlnC'To String : " + exc.toStringO) :
System.err.printlnC'Print Stack Trace:"):
exc.pri ntStackTrace():
}
System.out.printlnC'Continue program...") ;
то в области вывода системных сообщений мы увидим такую картину (рис. 12.2).
•Ш2МЕ Wireless Toolkit - TestSample
File Ed* Project
I V-NewProject.. | fig Open Protect [. 9, , Settings ... § ' Build V t Run F > Cle*r Console
, Device [L-laultGrayPhone
Gee Message: null
!: Го String : java.lang.ArithmeticException
Print Stack Trace:
java . lang. ArithmeticException
at TestSample.startApp(+2)
at javax.microedition.midlet.HIDletProxy,startApp(+7)
at com.sun.midp.midlet.Scheduler.schedule(+225)
at com.sun.midp.dev.DevMIDletSuitelmpl.schedule(+7)
at com.sun.midp.Hain.runLocalClass(+20)
at com.sun.midp.Hain.main(+68)
Continue program...
Рис. 12.2 . В основное окно WTK может быть выведена подробная информация об ошибке
Заметим, что программа после формирования исключения и вывода сообщений в
системную область не зависла, не упала, не вылетела, а спокойно продолжила свое
выполнение с команды, следующей за обработкой исключения.
Обработка некоторых исключений, называемых проверяемыми, является обяза-
тельной для программиста. Ее отсутствие приведет к ошибке на этапе компиля-
ции. Например, если убрать обработку исключения при открытии хранилища за-
писей, то программа не откомпилируется и выдаст следующую ошибку:
AddressBook.Java:44 : unreported exception javax.microedition.rms.RecordStoreException :
must be caught or declared to be thrown
recordStore • RecordStore.openRecordStore("Address-Book" . true) :
Рассмотрим некоторые наиболее распространенные исключения, формируемые
реализацией J2ME и уже использованные нами в примерах. Основная группа ис-
ключений, наследованных от класса RuntimeException, является непроверяемой,
поэтому к ним нужно относиться особо внимательно. В наших примерах обработ-
ка исключений была опущена везде, где только возможно, что не есть хорошо.
• Ari thmeti cExcepti on — формируется при ошибках в арифметических операци-
ях, например при делении на ноль;
----------------------- Page 129-----------------------
130 Глава 12 . Работа над ошибками
• ArrayStoreException — формируется при попытке записать в массив данные не-
верного типа;
• EmptyStackException — формируется во время работы со стеком при попытке
получения данных, когда стек пуст;
• 111 еда 1 Argument Except i on — возникает при передаче в метод некорректного зна-
чения аргумента. Если в качестве аргумента предусмотрена константа, то луч-
ше пользоваться ей в чистом виде во избежание подобных ситуаций;
• IndexOutOfBoundsException — указывает на то, что используемый индекс масси-
ва, вектора или строки выходит за пределы допустимых значений;
• NegativeArraySizeException — формируется при попытке создания массива от-
рицательной длины;
м NullPointerException — формируется при попытке вызвать метод или модифи-
цировать поле объекта, значение которого nul 1.
Исключения, сформированные хранилищем записей, наследованы от класса
RecordStoreException и являются проверяемыми, поэтому для успешной компи-
ляции всю работу с хранилищем мы оформляли в try...catch блоки. Возможные
исключения при работе с хранилищем записей (Inval idRecordlDExceotion,
RecordStoreFul1Excepti on, RecordStoreNotFoundExcepti on, RecordStoreNotOpenExcepti on)
мы уже рассмотрели в соответствующей главе, немного забегая вперед.
Еще одно исключение, InterruptedException, которое мы использовали при работе
с тредами, является прямым наследником класса Exception. Оно формируется в слу-
чае, когда один тред выводит другой из спящего или приостановленного состояния.
Исключения, наследованные от класса IOException, формируются при ошибках
с процедурами ввода-вывода:
• Connect!onNotFoundException — цель соединения не найдена;
• EOFException — конец файла был достигнут при работе с потоком ввода;
• InterruptedlOException — формируется в случае, если операция ввода-вывода
была прервана;
• UnsupportedEncodingException — данная кодировка не поддерживается;
• UTFDataFormatException — формируется при работе с Unicode, если символы стро-
ки не являются символами универсальной кодировки.
В рамках данной главы мы рассмотрели не все возможные исключения. В доку-
ментации, без которой вам в любом случае не обойтись, в описании каждого мето-
да содержится список всех возможных исключений, которые могут быть сформи-
рованы при выполнении данного метода.
Формирование исключений
Кроме существующих классов исключений можно формировать собственные ис-
ключения, реализовав свой класс, наследованный от класса Exception или от лю-
бого из его многочисленных потомков.
Рассмотрим формирование собственного исключения на примере программы каль-
кулятора, которую мы разбирали в одной из прошлых глав. Оформим проверку
----------------------- Page 130-----------------------
Формирование исключений 1 3 1
наличия всех аргументов в форме ввода в виде формирования исключения. Для
этого реализуем класс исключения MissingArgException:
class MissingArgException extends Exception {
// текстовое сообщение исключения
private String msg :
// конструктор без параметров
MissingArgExceptionf) { msg - null; }
// конструктор , принимающий строку
MissingArgExceptionCString s t r ) { msg - s t r ; }
// метод возврата текстового сообщения
public String toString(){
return "Exception (" + msg + " ) " ;
Исключения активизируются с помощью оператора throw, который позволяет
выбрасывать любые исключения иерархии класса Throwable из любого места про-
граммы. Команда throw new Ari thmeticException() приведет к такому асе эффекту,
как если бы здесь произошло деление на ноль, и обработка этого исключения пой-
дет своим чередом. Тот факт, что метод формирует исключение, обозначается
в заголовке метода ключевым словом throws и названием класса исключения. Реа-
лизуем метод проверки наличия аргументов в форме:
void checkArgsO throws MissingArgException {
// проверить заполнение всех полей ввода
// сформировать исключение в случае отсутствия аргумента
if(argl.size()-=O)
throw new MissingArgExceptionC"Missing First Argument") :
ifCarg2.size()==0)
throw new MissingArgExceptionC'Missing Second Argument") ;
if(action.size()==0)
throw new MissingArgExceptionC'Missin g Actio n S i g n " ) ;
}
Проверка заполнения формы ввода в методе commandActi on будет выглядеть теперь
следующим образом:
// проверить заполнение всех полей ввода
try {
checkArgsO;
} catchCMi ssingArgException maexc) {
// ошибка: не все поля ввода заполнены
resForm.append(maexc.toSt r i ng()) :
// отобразить сообщение
display.setCurrenttresForm):
// выйти
return;
----------------------- Page 131-----------------------
132 Глава 12 . Работа над ошибками
# ( i ) Nokia 72Ю *аж; Если запустить программу и вызвать команду
Ed? lools Help вычисления результатов, не задавая аргументов,
то сформируется соответствующее исключение,
а мы увидим на экране сообщение (рис. 12.3).
Для отображения сообщения об ошибке мы вос-
пользовались обычной формой. На самом деле
существует специально предназначенный для
a—-"Result этого класс.
Exception (Missing
First flrgument)
Класс Alert
Неважно, пользуется ли программой ваш друг
или человек, заплативший за это деньги, — си-
туация, когда приложение вылетает, оставляя за
собой лишь непонятное сообщение на экране,
считается дурным тоном в программировании.
Большинство исключений не интересует конеч-
ного пользователя программы, но иногда нужно
дать понять, что его действия некорректны.
Поскольку системная область сообщений недо-
ступна пользователю, существует цивилизован-
ный способ вывода подобной информации. Класс
Alert представляет собой сообщение, состоящее
из изображения и текста, которое отображается
на экране в течение заданного количества време-
ни, после чего автоматически происходит смена
экрана. Класс Alert используется для информи-
Рис. 12.3 . При отсутствии рования пользователя об ошибках и возникших
аргументов формируется
исключение и выводится исключительных ситуациях, а также для кратко-
сообщение об ошибке временных информационных сообщений о ре-
зультатах выполнения операций.
Время демонстрации сообщения может быть бесконечно большим. Тогда сооб-
щение будет отображаться на экране до тех пор, пока пользователь вручную не
закроет его. Аналогичная ситуация возникает, если большое количество инфор-
мации требует прокрутки экрана. Остановимся на методах класса Alert:
• AlertCString title) —конструктор создает пустое сообщение с заголовком title ;
• AlertCString title . String alertText, Image alertlmage. AlertType alertType) —
конструктор создает сообщение с заголовком title , текстом alertText и кар-
тинкой а 1 еrt Image. Параметр а 1 ertTyре задает тип сообщения, представленный
классом А1 ertType, который мы еще рассмотрим подробнее. Параметр а 1 ertType
может принимать значение nul 1 для создания обычного сообщения;
• void addCommand(Command cmd) — разумное объяснение наличию этого метода
придумать сложно, поскольку класс Al ert не поддерживает добавление команд.
При вызове метода будет сформировано исключение 111 еда 1 StateExcepti on;
• int getDefauHTimeoutC) — возвращает значение времени демонстрации сообще-
ния в миллисекундах, устанавливаемое по умолчанию при создании объекта
----------------------- Page 132-----------------------
Класс AlertType 1 3 3
класса Al ert . Может также возвратить значение константы FOREVER, если по умол-
чанию сообщение демонстрируется неограниченное количество времени;
Image getlmageO — возвращает объект картинки, установленной для сообще-
ния, или значение nul 1, если картинка не установлена;
String getStringO — возвращает текст данного сообщения;
int getTimeoutC) — возвращает время демонстрации данного сообщения в мил-
лисекундах или константу FOREVER;
AlertType getType() — возвращает тип данного сообщения в виде объекта клас-
са AlertType;
void setCommandListener(CommanciListener 1) — также.какиметодаййСоттагк!, при
вызове всегда формирует исключение 111 egal StateExcepti on;
void setlmage(Image img) — устанавливает новую картинку для сообщения;
void setString(String str) — устанавливает текст str данному сообщению;
void setTimeoutC int time) — устанавливает новое время демонстрации сообще-
ния. Время задается аргументом time в миллисекундах или константой FOREVER
класса Alert для неограниченного времени демонстрации сообщения;
void setType(AlertType type) — задает новый тип сообщения в виде объекта
класса AlertType.
Класс AlertType
Класс Al ertType задает тип сообщения класса Al ert . Тип сообщения определяет шаб-
лон, в котором будет продемонстрировано сообщение. Константы класса AlertType
определяют следующие шаблоны сообщений:
• ALARM — события, привязанные к определенным моментам, например напоми-
нание о назначенной встрече;
• CONFIRMATION — подтверждение успешных результатов действий пользователя,
например «Сообщение отправлено»;
• ERROR — сообщение об ошибке в результате действий пользователя, например
«Недостаточно памяти для сохранения данных»;
• INFO — отображение информационных сообщений;
• WARNING — предупреждение об опасных действиях пользователя, например «Все
данные будут уничтожены».
Класс AlertType содержит лишь два метода: конструктор, не принимающий
аргументов, и метод воспроизведения звукового сигнала, связанного с типом
сообщения:
• boolean playSound (Display display) — воспроизводит звук, соответствующий
данному типу сообщения. В определенных устройствах некоторые звуки мо-
гут отсутствовать. Возвращает значение true, если звук был воспроизведен,
и false— в противоположном случае. Метод playSound может быть вызван
и без использования сообщений класса Alert, например для организации зву-
ковых сигналов во время игры.
----------------------- Page 133-----------------------
134 Глава 12 . Работа над ошибками
# 0 ) Nokia 7210 SDK Внешний вид и звуковой сигнал сообщений каж-
дого типа зависят от конкретной модели теле-
фона. Например, на телефоне Nokia сообщение
информационного типа выглядит, как показано
на рис. 12.4.
Класс Al ert является потомком класса Screen, то
есть экраном, готовым к демонстрации. Посколь-
ку смена экрана по истечении тайм-аута проис-
ходит автоматически, то демонстрация сообще-
ния осуществляется с помощью особого метода
менеджера дисплея (объекта класса Display), ко-
торый принимает объект класса Alert, а также
объект следующего экрана:
• void setCurrent (Alert alert, Displayable
nextDi spl ayabl e) — устанавливает в качестве
текущего экрана сообщение, представленное
аргументом al ert, и задает экран nextDi spl ay-
abl е, который станет текущим после закры-
тия сообщения. Объект nextDi spl ayabl e не
может быть объектом класса Al ert или иметь
значение null.
Рассмотрим использование класса Al ert на при-
мере все той же программы калькулятора. До-
полним программу проверкой деления на ноль,
которую мы проигнорировали, из-за чего при-
ложение в такой ситуации вылетало с ошибкой.
В блоке catch перехватим исключение ошибки
Рис. 12.4 . Пример информационного в арифметической операции, создадим объект
сообщения сообщения об ошибке с неограниченным време-
нем демонстрации и отобразим его, указав основ-
ную форму ввода в качестве следующего демон-
стрируемого экрана:
import javax.microedition.lcdui.Alert;
import javax.microedition.lcdui.AlertType;
try
switch(act[O]) {
// выполнить действие в соответствии
// с символом операции
case '+' :
res = first + second :
break :
case '-':
res " first - second :
break :
case '*':
----------------------- Page 134-----------------------
Класс AlertType 135
res = first * second ;
break :
case 'У' :
res • first / second :
break;
default :
// ошибка : некорректный символ операции
resForm.appendCIllegal Operation") :
// отобразить сообщение
display.setCurrent(resForm):
// выйти
return :
}
// преобразовать результат в строку и добавить в форму
resForm.append((new Integer(res)).toStringO) :
// отобразить результат
displ ay.setCurrent(resForra) :
// блок перехвата арифметического исключения
} catch (ArithmeticException aexc ) {
// создать сообщение об ошибке
Alert aler t = new AlertCError","Divid e by zero" .nul l .AlertType.ERROR):
// установить бесконечное время демонстрации
alert.setTimeout(Alert.FOREVER);
// установить сообщение в качестве текущего экрана
di spl ay.setCurrent(alert.form) ;
Аналогичным образом вы самостоятельно можете об-
работать ошибку некорректного символа арифметиче-
ского действия. Попробуем программу в действии
и разделим что-нибудь на ноль. Теперь вместо останов-
ки программа продолжает свою работу, отображая на
экране сообщение, показанное на (рис. 12.5).
После нажатия клавиши Done программа возвращает-
ся к первоначальному экрану ввода. Заметим, что до-
бавление и обработку этой команды мы в программе
не предусматривали, она появилась в результате уста-
новки объекту класса Alert бесконечного времени де-
монстрации. В случае установки фиксированного вре-
мени демонстрации команда Done не отображается и
смена экранов происходит автоматически по истечении
заданного тайм-аута.
Рис . 12.5 . Теперь при де-
До сих пор в примерах мы лишь перехватывали воз- лении на ноль программа
не падает , а сообщает ,
можные исключения, чтобы программа компилирова- что пользователь
лась и не вылетала при возможных ошибках. Внима- был не прав
----------------------- Page 135-----------------------
1 36 Глава 12 . Работа над ошибками
ние на этом не заострялось, чтобы не отвлекаться от основной темы. В действи-
тельности же грамотная обработка исключений и вовремя предпринятые действия
помогут решить много проблем. Также неверно оставлять блок обработки исклю-
чения catch пустым, как мы это делали в примерах. Даже простое сообщение
в области ошибок принесет немало пользы в процессе отладки программы.
При обработке нескольких типов исключений в одном try-блоке следует быть вни-
мательными, не перехватывается ли необходимое исключение в одном из блоков
catch, расположенных выше. Так, если в первом же блоке catch перехватывается
исключение самого общего типа Excepti on, то до остальных блоков дело не дойдет
никогда. Немного практики и терпения, и вы научитесь лавировать меж подвод-
ных камней объектно-ориентированных программ, а главное, помните, что акку-
ратность — главный залог успеха программиста.
----------------------- Page 136-----------------------
Глава 13
Расширение
функциональности с помощью API
На данный момент мы рассмотрели большинство возможностей реализации
MIDP 1.0 мобильного языка J2ME. Возможности эти, прямо скажем, небезгра-
ничны, а порой просто разочаровывают. Это и есть плата за универсальность плат-
формы и совместимость приложений для многих моделей телефонов. Расширять
возможности J2ME можно в двух направлениях: с помощью профайла МШР 2.0
и с использованием специальных классов от производителей конкретной модели
телефона. Оба варианта значительно сокращают ряд моделей, где наша програм-
ма будет чувствовать себя комфортно, то есть работать как следует.
Возможно, в ближайшем будущем телефоны с поддержкой профайла MIDP 2.0
не будут дорогостоящей редкостью, что позволит освоить новые горизонты мо-
бильного программирования. А пока что подавляющее большинство моделей те-
лефонов на душу населения заключено в жесткие рамки профайла MIDP 1.0, на
который и сделан основной акцент в этой книге.
В этой главе мы остановимся подробнее на втором варианте расширения функцио-
нальности, то есть на API-функциях компаний-производителей. API (Application
Programming Interface, интерфейс программирования приложений) является на-
бором классов и методов для конкретных моделей определенного производителя.
Мы рассмотрим несколько разных API и начнем с самой распространенной моде-
ли, эмулятором которой мы пользовались на протяжении всей книги. Эмулятор
Nokia 7210 уже включает в себя все необходимые классы API, и если вы следова-
ли инструкциям и устанавливали все в указанные в первой главе папки, то J2ME
Wireless Toolkit обнаружит все, что нужно, самостоятельно. Расширять мы будем
нашу любимую «Змейку», в которую добавим несколько приятных моментов.
Nokia API
Посмотрим, чем же нас могут порадовать разработчики от корпорации Nokia.
На данный момент нам предлагаются два пакета классов com.nokia.mid.ui
и com. nokia .mid. sound, которые предоставляют возможности полноэкранного ре-
жима, низкоуровневого доступа к экрану, управления звуком, подсветкой, а так-
же вибрацией.
Помним, на чем мы остановились, программируя нашу змею? Название прило-
жения, отображенное на экране, благополучно отъело у нас по 16 пикселов не только
----------------------- Page 137-----------------------
138 Глава 13 . Расширение функциональности с помощью API
сверху, но и снизу — так, для симметрии (рис. 13.1). Экран и так не слишком ве-
лик, чтобы во время игры занимать его никому не нужными заголовками. Решим
эту проблему в нашей игре с помощью класса Ful I Canvas из Nokia API.
Делается это не просто, а очень просто. Импортируем необходимый нам класс
com. noki a. mi d. ui . Full Canvas и наследуем наш класс Snake не от стандартного Canvas,
а от расширенного Full Canvas.
import com. noki a. mi ci. in .FullCanvas;
private class Snake extends FullCanvas implements Runnable {
Вот, собственно, и все. Класс Ful I Canvas сам наследован от стандартного класса Canvas,
то есть содержит все его поля и методы, а это значит, что больше нам ничего пере-
писывать не придется. Новых методов класс Full Canvas не реализует, зато опреде-
ляет несколько дополнительных констант для клавиш, специфичных для телефо-
нов Nokia. Например , константы KEY_DOWN_ARROW, KEY_LEFT_ARROW, KEY_RIGHT_ARROW,
KEY_UP_ARROW могут быть использованы в процедуре keyPressed( int keyCode) для реа-
лизации управления джойстиком, характерным
look Help для аппаратов Nokia.
Итак, компилируем исправленное приложение .
Обратим внимание на то, что в строке Device (Ап-
парат) главного окна KToolbar должен быть выб-
ран эмулятор Nokia, иначе WTK не обнаружит
API-класса и выдаст следующую ошибку: package
com.nokia.mid.ui does not exist . Запускаем но-
вую игру и видим, что мы, не особо напрягаясь,
достигли желаемого результата и отвоевали
у аппарата 128 х 32 = 4096 пикселов (рис . 13.2).
Следующий класс, который мы рассмотрим, —
DeviceControl, из того же пакета com.nokia.mid.ui.
Он дает возможность управления подсветкой
и вибрацией телефона. В нашем распоряжении
оказываются следующие методы:
• void flashLightsClong duration) — мигание
подсветки на протяжении durat i on миллисе-
кунд. Метод возвращается сразу после вы-
зова и не блокирует исполняющий тред на
время выполнения эффекта ;
• void setLightsCint num. int level)—управ-
ление иллюминацией телефона . Аргумент
num представляет номер источника света для
аппаратов с дополнительными лампочками
и подсветками . Фоновая подсветка имеет
номер, равный нулю. Аргумент level зада-
Рис. 1 3 . 1 . Во время игры часть ет уровень интенсивности света в диапазо-
экрана оказывается недоступной не 0-100 . Значение 0 означает выключение
----------------------- Page 138-----------------------
Nokia API 139
света. В аппаратах без поддержки уровня ин-
тенсивности света любое значение аргумента,
большее нуля, означает включение света. Та-
ким образом, команда погашения фоновой
подсветки будет выглядеть так: Devi ceControl.
setLightsCO. 0);
• void startVibra(int freq. long duration) —
включает вибрацию телефона на duration
миллисекунд. Аргумент freq задает частоту
вибрации в диапазоне 0-100. Значение 0 мо-
жет быть использовано для определения, под-
держивается ли вибрация вообще. Если при
нулевом аргументе будет сформировано ис-
ключение 111 egal StateExcepti on, значит теле-
фон в принципе не вибрирует. Такое же ис-
ключение при другом аргументе может быть
сформировано, если система не разрешает ис-
пользование вибрации. Метод, так же как
и flashLights, возвращается сразу после вы-
зова и не блокирует выполнение программы
на время вибрации;
• void stopVibraO — остановить вибрацию,
организованную предыдущим методом. Если
вибрация не активна, то метод не выполня-
ет никаких действий и не формирует ис-
ключений.
Посмотрим на примере, как можно использовать Рис. 13.2 . Класс FullCanvas предо-
этот класс в нашей игре. Пусть в момент проиг- ставляет в наше распоряжение
рыша телефон забьется в конвульсиях вместе со весь экран целиком
змеей, включив вибрацию. Первым делом им-
портируем необходимый класс com.nokia. mid.
ui .Devi ceControl. Затем в методе paintCGraphics g), отрабатывающем действия конца
игры, включим вибрацию максимальной частоты на 100 миллисекунд:
import com.nokia.mid.ui.DeviceControl ;
public void paintCGraphics g) {
// если поднят флаг конца игры
i f ( gameOverFlag ) {
// включить вибрацию на ЮОис.
DeviceControl.startViЬгаСЮО. 100):
Заметим, что эмулятор умело подражает даже этой функции аппарата, и телефон,
изображенный на экране компьютера, заметно потряхивает.
----------------------- Page 139-----------------------
140 Глава 13 . Расширение функциональности с помощью API
Используем также для усовершенствования нашей игры класс Sound из пакета
com. noki a.mid. sound. Для начала рассмотрим методы класса Sound:
• Sound(int freq. long duration) — конструктор; создает звуковой объект часто-
той сигнала freq герц, продолжительностью duration миллисекунд. В докумен-
тации можно найти таблицу перевода частот в обычные ноты, хотя кому-то
это одинаково непонятно;
• Sound(byte[] data, int type) — конструктор; создает звуковой объект, пред-
ставленный массивом байтов data. Аргумент type задает формат звуковых дан-
ных. Класс Sound определяет две константы представления формата : FORMAT_
TONE — мелодия в формате Nokia, FORMATJJAV — мелодия в стандартном MIDI-
формате;
• int getConcurrentSoundCount(int type) — возвращает максимально возможное
количество одновременно проигрываемых мелодий типа type;
• int getGainO — возвращает уровень громкости звука в диапазоне 0-255 или
значение - 1 , если используется громкость по умолчанию;
• int getStateC) — возвращает текущее состояние звукового объекта, представлен-
ное одной из трех констант: SOUND_PLAYING, SOUND_STOPPED, SOUNDJJNINITIALIZED - звук
проигрывается, остановлен или не инициализирован соответственно;
• int[ ] getSupportedFormatsO — возвращает поддерживаемые аппаратом аудио-
форматы в виде массива констант FORMAT_TONE, FORMAT_WAV. Если воспроизведе-
ние звука не поддерживается аппаратом, то возвращается пустой массив;
• void init(in t freq, long duration) — инициализирует звуковой объект, аргу-
менты аналогичны первому рассмотренному конструктору;
• void init(byte[ ] data, int type) — инициализирует звуковой объект, аргумен-
ты аналогичны второму рассмотренному конструктору;
• void play(int loop) — воспроизвести звуковой объект loop раз. Если аргумент
1 оор принимает значение 0, то объект будет воспроизводиться до тех пор, пока
не будет принудительно остановлен. Во время воспроизведения объект нахо-
дится в состоянии SOL)ND_PLAYING;
• void releaseO — освобождает все используемые звуковым объектом ресурсы
и переводит его в состояние SOUNDJJNINITIALIZED. Если объект находится в со-
стоянии SOUND_PLAYING, то сначала автоматически вызовется метод stop() ;
• void resumeC) — возобновить воспроизведение остановленного звукового объек-
та. Если аппарат поддерживает такую возможность, то воспроизведение нач-
нется с того места, где оно было остановлено;
• void setGaindnt gain) — установить уровень громкости воспроизведения
в диапазоне 0—255;
• void stop О — останавливает воспроизведение звукового объекта и переводит
его в состояние SOUND_STOPPED.
Хотим теперь, чтобы при поедании очередного сердечка змея издавала радост-
ный писк. Для этого нужно импортировать необходимый класс, создать звуковой
объект и воспроизвести его в методе checkMove, контролирующем передвижение
в заданном направлении :
----------------------- Page 140-----------------------
Samsung API 1 4 1
import com.nokia . mi d.sound.Sound ;
// проверить возможность передвижения
// в точку с координатами (хН.уН )
public void checkMove(int xH . int уН ) {
// проверить совпадение координат передвижения
// с координатами сердца
if ( xH == xHeart & & уН -» уHeart ) {
eatFlag = true :
// создать звуковой объект
// частоты 1000 герц длиной полсекунды
Sound beep = new Sound(1000.500) :
// воспроизвести звуковой объект
beep.play(l) :
// увеличить скорость
speed--;
Осталось запустить приложение на эмуляторе и не забыть включить колонки.
Вместо неприятного однотонного писка можно сделать симпатичный звуковой
эффект, прогнав в цикле воспроизведение звуков разных частот, например вот
таким образом:
// пройти в цикле частоты от 100 до 1000 герц с шагом 50
forCint 1-100 : i<=1000 : i+=50 ) {
// создать звуковой объект
Sound beep = new Soundd ,50) ;
// воспроизвести звуковой объект
beep.play(l) :
}
Теперь у вас есть большое поле для экспериментов, чтобы начинить игру звуковы-
ми и тактильными спецэффектами. Кроме использованных нами классов пакет
com.nokia.mid.ui содержит также интерфейс DirectGraphics и класс DirectUti Is, рас-
ширяющие возможности работы с графикой. Здесь нам предоставляется масса ин-
тересных методов, таких как прорисовка и заливка треугольников и полигонов,
прямой доступ к любой точке экрана, повороты изображения, смена форматов цве-
товой гаммы и многие другие полезные вещи, интересные лишь Nokia-ориентиро-
ванным программистам.
Samsung AP I
На остальных производителях мы не будем останавливаться настолько подроб-
но. Рассмотрим, для сравнения, предоставляемые ими классы. Разработчики
----------------------- Page 141-----------------------
1 42 Глава 13 . Расширение функциональности с помощью API
телефонов Samsung предлагают пакет com.Samsung.util, который содержит анало-
ги уже знакомых нам классов контроля звука, подсветки и вибрации, а также два
новых класса, поддерживающих создание и рассылку SMS-сообщений. Рассмот-
рим вкратце все предложенные нам классы.
Класс Audi oCl i p отличается от Nokia-аналога расширенным списком воспроизво-
димых форматов, а также отсутствием простой пищалки, задающей частоту тона
в герцах. Конструктор AudioClipdnt type, byte[] audioData, int audioOffset, int
audioLength) создает звуковой объект из байтового массива audioData, начиная
с позиции audi oOf f set, длины audi oLength байтов. Второй конструктор Audi oCl i p( i nt
type. Stri ng f i 1 ename) получает звуковой объект прямо из файла ресурса по име-
ни f 11 ename. Аргумент type может принимать значения следующих констант: TYPE_MMF,
TYPE_MP3, TYPE_MIDI, с оговоркой, что на данный момент аппаратами поддерживает-
ся только TYPE_MMF.
Методы play (int loop, int volume), stopO, pauseO и resume О служат для начала,
прекращения, приостановки и возобновления воспроизведения звукового объек-
та соответственно. Метод boolean isSupportedO возвращает значение true, если
такая технология воспроизведения звука поддерживается аппаратом.
r
Класс Vibration заведует вибрацией телефона. Его метод boolean isSuppo ted()
аналогичен такому же методу класса AudioClip, а методы start (int duration, int
strength) и stop() начинают и прекращают вибрацию соответственно.
Класс LCDLight занимается лишь фоновой подсветкой, так как других источников
света Samsung не предусматривает. Методы bool ean 1 sSupported(), on(int durati on)
и of f () говорят о своих действиях сами за себя. •
Рассылка SMS поддержана в Samsung API двумя классами: первый класс, SM, со-
держит все необходимые методы для формирования сообщения, а второй, SMS,
предоставляет лишь два метода: уже набивший оскомину boolean isSupportedO,
а также send(SM sin), посылающий сообщение адресату. Конструктор SM(String dest,
String callback, String textMessage) создает объект сообщения для номера dest от
номера call back с текстом textMessage. Методы String getCallbackAddressO, String
getDataO, String getDestAddressO, void setCallbackAddress(String address), void
setData(String textMessage), void setDestAddress(String address) предназначены
для получения и установки трех параметров сообщения, рассмотренных в конст-
рукторе. Обратим внимание на интересный параметр номера отправителя са 11 back.
Не хотят ли они сказать, что мы с телефона можем послать SMS от чужого име-
ни? В документации говорится, что данный параметр не поддерживается, однако
на форуме умельцев с Самсунгами я уже видел ссылку на подобную программку.
Хотя не знаю, работает ли она: у меня, как вы уже, наверное, догадались, Nokia.
При попытке найти в Интернете эмуляторы телефонов Samsung, поддержива-
ющие вышеописанные классы, обнаружилась занятная вещь под названием Samsung
JaUmi Wireless Toolkit. Дизайн программки оказался до боли знакомым, за не-
большим отличием списка эмулируемых аппаратов (рис. 13.3).
В меню About обнаружилась следующая надпись: «Copyright © SAMSUNG Electronics
2003. All rights reserved». Интереса ради заглянул в аналогичный пункт меню при-
вычного нам J2ME Wireless Toolkit: «Copyright © 2002 Sun Microsystems, Inc., 90 1
San Antonio Road, Palo Alto, California 94303, U.S.A. All rights reserved». Видим, что,
----------------------- Page 142-----------------------
Samsung API 1 4 3
оказывается, ничего зазорного в использовании чужого кода нету. А если серьезно,
следите за соблюдением авторских прав в производимых и распространяемых вами
программных продуктах.
Щ SAMSUNG JaUmi Wireless Toolkit - SnakeGame
;GH-Z130
;GH_C207
SGH_D410
SGH_E100
SGH_E300
;GH_E618
;GH_E710
Рис. 13.3 . Интерфейс этой программы очень напоминает J2ME Wireless Toolkit
Посмотрим, как будет себя чувствовать наша змейка на телефонах типа Samsung,
предварительно заменив классы Nokia API на аналоги от Samsung, a Canvas вернув
в исходное положение. Змейка демонстрирует свою живучесть и хорошую совме-
стимость моделей (рис. 13.4).
SGH-E1O0
Рис. 13.4 . Самсунги тоже пригодны для обитания нашей змейки
----------------------- Page 143-----------------------
144 Глава 13 . Расширение функциональности с помощью API .
Siemens API
Нужно отдать должное, разработчики компании Siemens переплюнули всех и дали
самый широкий набор классов для управления их телефонами. Классы объедине-
ны аж в семь пакетов: com. Siemens, mp. game, com. Siemens, mp. colorjpne, com. Siemens .tnp.gsm,
com.Siemens.mp.io,com.Siemens.mp.media,com.Siemens.mp.media.control, com.Siemens.mp.ui.
Возможностей у этих классов масса, поэтому не будем вдаваться в подробности,
а лишь рассмотрим назначение каждого из них.
Пакет com.Siemens.mp.game содержит средства разработки игровых приложений.
Здесь присутствуют аналоги уже знакомых нам классов. Класс Light руководит
фоновой подсветкой телефона, а класс с двусмысленным названием Vibrator за-
ведует вибрацией. Класс Sound является обычной пищалкой, воспроизводящей тон
заданной частоты, зато классы Mel ody и Mel odyComposer позволяют составлять и вос-
производить мелодии из нот, представленных константами класса Mel odyComposer.
Намеков на поддержку стандартных звуковых форматов не обнаружено.
Следующий набор классов заведует графическими возможностями аппарата. Класс
Extendedlmage дает прямой низкоуровневый доступ к точкам экрана, а также прори-
совку изображения в обход стандартного метода paint. Графические объекты пред-
ставлены классом GraphicObject, являющимся базовым для классов Ti ledBackground
и Sprite. Первый из них осуществляет поддержку графического фона экрана, пред-
ставленного мозаикой.
Класс Sprite не имеет отношения к напитку, дающему смелые идеи, этот класс
осуществляет поддержку графических объектов типа «спрайт». Спрайтом назы-
вается фигура или элемент экрана, который может быть перемещен независимо
от остальных. Спрайт может содержать несколько кадров, сменяющих друг друга.
С помощью класса Sprite реализуется простейшая анимация.
Следующие два пакета com.Siemens.mp.ui и com.siemens.mp.color_game дают новый
подход к управлению графическими объектами. Первый пакет содержит лишь
один класс Image, расширяющий оригинал возможностями масштабирования
и зеркального отражения картинки. Второй пакет организует работу с графиче-
скими объектами через базовый класс слоя графики Layer, от которого наследова-
ны знакомые нам по прошлому пакету классы Sprite и Ti I ed'Layer, а также с помо-
щью класса менеджера слоев LayerManager. Кроме этого пакет содержит класс
GameCanvas, аналогично классу от Nokia, расширяющий стандартный Canvas.
Пакет com.Siemens.mp.gsm предоставляет средства связи, представленные следу-
ющими классами: PhoneBook для доступа к адресной книге, Call для организации
телефонного звонка по указанному номеру, SMS для формирования и рассылки
SMS-сообщений.
Пакеты com. si emens. mp. medi а и com. si emens. mp. medi a. control реализуют универсаль-
ный аудиопроигрыватель с опциями воспроизведения, перемотки, возможностью
прослушивания треков прямо из Интернета.
Последний пакет com. si emens. mp. i о позволяет приложению организовать свою
файловую систему с помощью класса Fi le . Это позволит не мучаться с хранили-
щем записей, а использовать обычную файловую систему с привычными нам ме-
тодами доступа. Интерфейс ConnectionListener и класс Connection реализуют воз-
----------------------- Page 144-----------------------
Siemens API 145
можность работы с внешними устройствами ввода-вывода телефона, такими как
мультимедийные карты ММС или инфракрасный порт IRDA.
Звучит это все, конечно, заманчиво, осталось только узнать, какие модели под-
держивают все эти прелести, а главное — по карману ли они нам. С точки зрения
программного обеспечения Siemens опять оказался на высоте и предлагает свой
собственный Siemens Mobile Toolkit (SMTK), который интегрируется со средой
разработки J Builder любой версии, начиная с пятой. В этой же программе можно
установить различные эмуляторы телефонов. Змейка же продолжает успешно
ползать по экранам разных моделей, теперь это Siemens M55 (рис. 13.5).
Рис. 13.5 . И в Сименсах водятся змеи
На этом этапе мы прощаемся с нашей змейкой, так многому нас научившей. Пол-
ный листинг программы для аппарата Nokia приведен в конце книги, однако, на-
деюсь, что вы не принялись сразу аккуратно списывать код, а действовали вместе
со мной поэтапно. Игра, конечно, далека от совершенства, поскольку реализована
как демонстрация различных возможностей языка J2ME, в ваших силах теперь
довести ее до ума, добавить необходимую логику, красочные картинки, проявив
фантазию и смекалку.
* * *
По-хорошему, каждая модель телефона требует особой доработки программы,
чтобы максимально задействовать возможности каждой конкретной модели. Про-
----------------------- Page 145-----------------------
1 46 Глава 13 . Расширение функциональности с помощью API
граммное обеспечение, API-классы, эмуляторы, документацию и прочие необхо-
димые вещи можно скачать на официальных сайтах компаний-производителей.
Как правило, делятся они этим безвозмездно. Также в Сети существует большое
количество «фан-клубов» (сайтов, порталов, форумов) телефонов основных про-
изводителей, где собираются разные умельцы, которые делятся идеями и програм-
мами. Там вы сможете получить много интересной информации конкретно о ва-
шем телефоне, а также скачать все необходимое программное обеспечение и даже
больше. Перефразируем слова Мичурина: не нужно ждать милостей от телефона,
взять их у него — наша задача!
----------------------- Page 146-----------------------
Глава 14
Приложения
для работы с Интернетом
Большая часть возможностей мобильного телефона и реализации J2ME уже поза-
ди. Мы написали несколько различных приложений, реализующих интеракцию
с пользователем, хранение и обмен данными, а также другие функции языка. Мо-
бильный телефон, однако, является все же средством связи и позволяет связывать-
ся не только с помощью голоса или SMS. Профайл MIDP предоставляет нам воз-
можности связи с Интернетом, получения и передачи различного вида данных.
Поскольку речь в этой главе пойдет о взаимодействии телефона с Интернетом, то
придется расширять сферу нашего обучения с мобильного программирования до
разработки серверных приложений на других объектно-ориентированных язы-
ках. Тем, кто не знает, что такое сокет (socket), и не сталкивался с разработкой
компьютерных приложений типа «клиент-сервер», придется трудновато. Однако
один из приведенных примеров, демонстрирующий работу по протоколу HTTP,
если уж вы дошли до этого места, реализовать вам вполне по силам. Разработка
приложений типа «клиент-сервер» — это отдельная наука. Мы же ограничимся
самыми простыми примерами, демонстрирующими первоначальное взаимодей-
ствие телефона с Интернетом.
Класс Connector
Не буду забивать вам голову тем, как устроена связь между беспроводной сетью
и Всемирной паутиной, а перейду сразу к делу, то есть к программированию. Се-
тевая работа и беспроводная коммуникация осуществляются в реализации MIDP
средствами пакета javax.m1croedition.io, который содержит набор интерфейсов
для различных типов соединений, а также единственный класс Connector. Органи-
зация соединений с любыми ресурсами осуществляется только через этот класс,
который предоставляет следующие методы:
• Connection open (String name) — основной метод класса Connector, обеспечива-
ющий открытие соединения с ресурсом, представленным аргументом name. Ар-
гумент имеет формат универсального идентификатора ресурса (URI), кото-
рый можно представить следующей схемой:
<протокол>://[<адрес>][<параметры>]
Протокол представляет собой тип передаваемых данных. Реализация MIDP
может поддерживать такие протоколы: http, socket, datagram, file, ftp, comm.
Совсем не обязательно, чтобы ваша реализация MIDP поддерживала все эти
----------------------- Page 147-----------------------
1 48 Глава 14 . Приложения для работы с Интернетом
протоколы, поскольку спецификация MIDP требует обязательной поддержки
только одного протокола HTTP 1.1. Не исключена также поддержка протоко-
лов, не указанных в списке. Адресом является обычный URL; в качестве до-
полнительных параметров может выступать номер порта. Ниже приведены
примеры создания различных типов соединений.
Connector.open("http://www.voolkansoft.com");
Connector.open("socket://129.144.111.222:9000");
Connector.open("datagram://129.144.111.333");
Connector.open("file:/foo.dat") :
Connector.open("comm:0;baudrate=9600");
Метод возвращает объект Connection, являющийся базовым для интерфейсов,
представляющих различные типы соединений, которые мы рассмотрим позже;
• Connection open(String name, int mode) — метод, аналогичный предыдущему,
с тем отличием, что параметр mode задает режим создаваемого соединения. Ре-
жимы соединений представлены следующими константами класса Connector:
• READ — открыть соединение с доступом только на чтение данных;
• WRITE — открыть соединение с доступом только на запись данных;
• READ_WRITE — открыть соединение с доступом и на чтение, и на запись данных;
• Connection open (String name, int mode, boolean timeouts)— метод отличается от
двух предыдущих наличием дополнительного параметра timeouts — флага, ко-
торый разрешает или запрещает исключения от тайм-аутов.
Следующие методы класса Connector позволяют сразу открывать потоки ввода
и вывода данных, рассмотренные в одной из предыдущих глав, минуя интерфейс
Connection. Когда мы уже работали с потоками ввода и вывода, мы получали дан-
ные из файла, являющегося локальным ресурсом приложения. Вся разница за-
ключается в том, что ресурс будет размещаться в Интернете и предоставлять дан-
ные в своем формате:
• DatalnputStream openDatalnputStreairK String name) — открывает поток ввода дан-
ных. Аргумент name имеет формат уникального идентификатора ресурса;
• DataOutputStream openDataOutputStream(String name) — открывает поток вывода
данных;
• InputStream open InputSt ream (String name) — открывает базовый поток ввода дан-
ных;
• OutputStream openOutputStream(String name) — открывает базовый поток вывода
данных.
Интерфейс Connection
Интерфейс Connection является базовым для всех последующих типов соедине-
ний. Он содержит единственный метод closeO, закрывающий соединение, ко-
торое открывается с помощью метода open О класса Connector. Этот интерфейс
является самым абстрактным во всей структуре возможных типов соединений.
Дальнейшие интерфейсы будут его расширять и дополнять новыми методами
и возможностями.
----------------------- Page 148-----------------------
Интерфейс StreamConnection 1 4 9
Итак, процесс установки соединения и работы с ним выглядит следующим обра-
зом. Приложение использует класс Connector для открытия соединения с сетевым
ресурсом. Метод ореп(...) класса Connector анализирует универсальный идентифи-
катор ресурса и проверяет, поддерживается ли необходимый протокол в данной
платформе. В случае ошибки формируется исключение, иначе возвращается объект
Connection, который содержит ссылки на входной и выходной потоки к сетевому
ресурсу. Приложение получает объекты типа InputStream и OutputStream из объекта
Connection и организует обмен данными с сетевым ресурсом через эти потоки. При
завершении работы приложение закрывает соединение с помощью метода close()
интерфейса Connection.
Интерфейсы InputConnection
и OutputConnection
Интерфейсы InputConnecti on и OutputConnecti on наследованы от интерфейса Connecti on
и расширяют его потоками ввода и вывода соответственно. Каждый их них опреде-
ляет по два новых метода для открытия потоков. Названия этих методов такие же,
как и у методов открытия потоков класса Connector, и отличаются они лишь отсут-
ствием аргументов. Использование этих методов открытия потоков мы еще уви-
дим в примерах.
Интерфейс StreamConnection
Интерфейс StreamConnection объединяет в себе два предыдущих интерфейса
InputConnection и OutputConnection, но не определяет никаких новых методов. За-
дача этого интерфейса в том, чтобы представить любой тип соединения, чьи дан-
ные могут быть обработаны как поток байтов. В зависимости от протокола соеди-
нения активизируются разные реализации интерфейса StreamConnection. Наша
среда разработки J2ME WTK предоставляет две реализации: одну для соедине-
ния с портами связи, например с инфракрасным портом, вторую для соединения
через сокеты. Мы рассмотрим второй тип соединения и реализуем пример, де-
монстрирующий работу соединения такого типа.
Сокет (socket) — это сетевой механизм транспортного уровня, который реализует
работу по протоколу TCP/IP. Грубо говоря, это та дверь, которая связывает про-
грамму с Интернетом. Как любая порядочная дверь в большой мир, сокет имеет
свой адрес, состоящий из сетевого адреса URL и номера порта, через который бу-
дет осуществляться соединение. Номер порта может быть любым в диапазоне
0-65 535, однако номера, меньшие чем 1024, обычно зарезервированы системой.
Структура сокета универсальна, поэтому программа, принимающая соединение,
может быть запущена на любой платформе и написана на любом языке, поддер-
живающем сокеты.
Итак, пришло время реализовать пример соединения, основанного на сокетах.
Работа приложения будет заключаться лишь в том, чтобы соединиться с сервер-
ной частью, послать туда приветствие, получить в ответ сообщение и продемон-
----------------------- Page 149-----------------------
150 Глава 14 . Приложения для работы с Интернетом
стрировать его на экране телефона. Для начала рассмотрим клиентскую часть
программы, написанную на уже привычном для нас языке J2ME:
import javax.microedition.mi diet.MIDIet ;
import javax.microedition.10.StreamConnection ;
import javax.microedition.io.Connector ;
import java.io.IOException :
import java.io.DataOutputStream ;
import java.io.DatalnputStream :
import javax.microedition.lcdui.Form :
import javax.microedition.lcdui.Display ;
try {
// открыть соединение через сокет с портом 3000 локальной машины
StreamConnection sc -
м
(StreamConnection)Connector.open("socket://127.0.0.1:3000 ):
// получить поток ввода соединения
DatalnputStream dis • sc.openDatalnputStreamO ;
// получить поток вывода соединения
DataOutputStream dos = sc.openDataOutputStreamO ;
// записать приветствие в поток вывода
dos,write("Hel 1 о . Server!\0".getBytes0.0,15) ;
// буфер для приема сообщения
byte[ ] buff = new byte[15] ;
// получить сообщение из потока ввода
dis.read(buff);
// преобразовать сообщение в строку
String msg - new String(buff) :
// создать форму для отображения сообщения
Form form = new Form("Message received") ;
// добавить полученное сообщение в форму
form.append(msg);
// получить менеджер дисплея
Display display - Display.getDisplay(this) :
// отобразить форму сообщения
display.setCurrent<form);
// закрыть соединение
sc.closeO :
} catchdOException ioe ) {
// вывести исключение в область системных сообщений
System.out.println("ERROR : "+ioe.getMessage()) ;
}
С этой стороны нет ничего сложного. Приложение открывает соединение через сокет
с портом 3000 локальной машины, представленной постоянным для любого компью-
тера IP-адресом 127.0.0.1. После установки соединения приложение получает пото-
ки ввода и вывода соединения и через них реализует интеракцию с серверной частью.
----------------------- Page 150-----------------------
Интерфейс StreamConnection 1 5 1
Перейдем к тому, что представляет собой серверная часть. В нашем случае это
программа, запущенная на локальном компьютере, открывающая сокет и ждущая
связи с клиентом. Программу напишем на языке C++ с использованием классов
MFC . С таким же успехом она может быть написана на любом языке, поддержи-
вающем технологию сокетов.
Язык C++ не входит в нашу учебную программу, поэтому не будем особо вдаваться
в подробности, а просто посмотрим на код некоторых функций, который может быть
кому-то полезен. Рассмотрим две основные функции: OnListerK), которая открыва-
ет сокет, ожидающий клиентского соединения, и OnRecei ve(), которая обрабатывает
событие получения сообщения. Функция отображаемого диалога OnListerK) вызы-
вается при нажатии на кнопку Listen основного диалога:
void CServerDlg::OnListen()
{
// получить информацию из поля ввода порта
UpdateDataO ;
// создать сокет с локальным адресом
if (m_Sock->Create(m_Port.SOCK_STREAM."127.0.0.1") )
{
// ждать связи с клиентом
if (m_Sock->Listen() ) {
// вывести сообщение с номером порта сокета
CStatic* stat = (CStatic*)GetDlgItem(IDC_STATIC) ;
CString str ;
str.Format("Listening on port %6", m_Port) :
stat->SetWindowText(str);
// вывести IP-адрес сокета
stat = (CStatic*)GetDlgItem(IDC_MSG) :
UINT port ;
m_Sock->GetSockName(str.port) ;
stat->SetWindowText(str);
Функция использует член класса диалога m_Port, связанный с полем ввода порта,
и m_Sock, являющийся объектом класса CLi stenSocket, наследованный от стандарт-
ного MFC-класса CSocket. При нажатии на кнопку Listen диалог выдаст такие со-
общения (рис. 14.1).
Вторая функция OnRecei ve() класса CLi stenSocket отслеживает момент приема сооб-
щения через сокет, выводит его в окне диалога и посылает ответ клиентскому при-
ложению:
void CListenSocket::OnReceive(int nErrorCode )
{
// поля диалога для вывода сообщений
CStatic* stat = (CStatic*)(m_pDlg->GetDlgItem(IDC_STATIC)) •
CStatic* statjnsg = (CStatic*)(m_pDlg->GetDlgItem(IDC_MSG)) ;
// буфер приема сообщения
----------------------- Page 151-----------------------
152 Глава 14 . Приложения для работы с Интернетом
TCHAR buff[20] ;
// прочитать сообщение в буфер
Rece1ve(&buff:20) :
// вывести подтверждение получения сообщения
stat->SetWindowText(_T("Message Received...")) ;
CString str ;
str.Format("Is",buff) :
// вывести полученное сообщение
stat_msg->SetWindowText(str) :
// копировать ответ в буфер
strcpy(buff."Hello . MIDlet!\0") :
// послать ответное сообщение
Send(&buff,15);
Uiening on port 3000
1270.0.1
Port J3Q00" Ш 0 :
Рис. 1 4 . 1 . Сервер сообщает, что слушает порт 3000 локальной машины
Те, кто уже знаком со студиями р'азработки C++, без труда смогут реализовать
данный пример, остальные же без труда смогут найти книжки о разработке визу-
альных приложений на языке C++.
Перейдем к самому интересному, сначала запускаем серверную часть и ждем со-
единения клиента, в качестве которого будет выступать стандартный эмулятор
среды разработки J2ME WTK. При запуске мобильного приложения сервер об-
наружит связь и получит сообщение от телефона (рис. 14.2).
Met.'age Received.
Hello Servefi
Рис. 14.2 . Сервер получил сообщение и вывел его в окне
После приема сообщения сервер формирует в ответ свое приветствие и посылает
его мобильному приложению, которое отображает принятое сообщение с помо-
щью обычной формы (рис. 14.3).
----------------------- Page 152-----------------------
Интерфейс DatagramConnection 1 5 3
Это был простейший пример, шапочное знакомство
сервера с телефоном на уровне «привет-привет». Тем
не менее, это и есть самый сложный момент. Если уж
наши программы встретились и поздоровались, то и до-
говориться смогут, а о чем им договариваться — зави-
сит только от нашей фантазии, простор для которой
здесь ничем не ограничен. Можно разработать инфор-
мационную систему, многопользовательскую онлайн-
игру, оперативный центр сбора и обработки информа-
ции и многое другое. Сервер — программа, размещенная
в Интернете, — должна взять на себя все организаци-
онные функции общения с клиентами, которых может
быть сколь угодно большое число, причем одновремен-
но. В классических сервер-приложениях отдельный тред
отводится для ожидания связи с новым клиентом, для
каждого из которых выделяется собственный тред. Та-
ким образом, клиенты не будут мешать друг другу, од-
нако вопросы синхронизации при доступе к общим дан-
ным встают особо остро.
Заметим, что не все реализации поддерживают сокет-
ные соединения, поэтому вполне возможна ситуация, что
при попытке создания соединения на вашем аппарате
будет сформировано исключение. Например, при попыт-
ке запуска того же самого приложения на эмуляторе от
Nokia мы получим в системной области следующее со-
общение: «ERROR: The requested protocol does not exist Р и с - " - 3 - Эмулятор принял
socket://127.0.0.1:3000». И ^ ^ ?
Интерфейс StreamConnectionNotifier
Функции сервера может выполнять и мобильный телефон с помощью интер-
фейса StreamConnectionNotifier. Этот интерфейс содержит единственный метод
StreamConnection acceptAndOpen О, который блокирует все операции до тех пор, пока
не появится клиентский запрос на соединение. После чего метод создает новый
объект соединения, через который и связывает серверную часть с клиентской.
При использовании данного интерфейса в качестве сервера будет выступать мо-
бильное устройство. Недостатки такой реализации заключаются в том, что уст-
ройство должно быть постоянно на связи в ожидании подключения клиентов, что
может быть дорого и неудобно.
Интерфейс DatagramConnection
Еще один тип соединений представлен интерфейсом DatagramConnection, который
напрямую наследован от интерфейса Connection. Если соединение через сокеты
основывалось на протоколе TCP, то данное соединение использует протокол пе-
редачи дейтаграмм UDP. Данный протокол используется различными интернет-
----------------------- Page 153-----------------------
154 Глава 14 . Приложения для работы с Интернетом
службами потоковых трансляций и основан на передаче дейтаграмм. В некото-
рых случаях он является более предпочтительным из-за эффективной и быстрой
пересылки данных, поскольку там нет гарантированной доставки сообщений, ис-
правлений искаженных сообщений и другого контроля, нагружающего связь.
Дейтаграмма представлена интерфейсом Datagram, который объединяет в себе два
интерфейса Data Input и DataOutput. Кроме этого интерфейс содержит информацию
о передаваемых или принимаемых данных и об адресе пункта назначения. Интер-
фейс Datagram предоставляет следующие методы:
• String getAddressO — установить адрес дейтаграммы;
• byte[] getDataO — возвращает данные дейтаграммы;
• int getLengthO — возвращает длину дейтаграммы;
• int getOffsetC) — возвращает текущее смещение указателя буфера данных;
• void reset() — восстанавливает позицию указателя буфера данных;
• void setAddress( Datagram reference)—устанавливает такой же адрес, как и у дей-
таграммы, переданной в параметре;
• void setAddress(String addr) — устанавливает адрес дейтаграммы;
• void setData(byte[] buffer, int offset, int 1 en)— устанавливает буфер дан-
ных, смещение указателя буфера и длину данных;
• void setLengthd'nt I en)—устанавливает длину полезной нагрузки дейтаграммы.
Интерфейс DatagramConnection позволяет отправлять и получать дейтаграммы
с помощью методов send (Datagram dgram) и receive(Datagram dgram). Также интер-
фейс позволяет создавать новые дейтаграммы с помощью нескольких методов:
• Datagram newDatagram(byte[] buf, int size) — возвращает новую дейтаграмму
с данными буфера buff длины size;
• Datagram newDatagram(byte[] buf, int size, String addr)— аналогиченпредыду-
щему методу, параметр addr задает адрес дейтаграммы;
• Datagram newDatagram(int size)—создает новую дейтаграмму заданного размера;
• Datagram newDatagramd'nt size . String addr) — создает новую дейтаграмму
с заданным размером и адресом.
Максимально допустимую длину дейтаграммы, а также номинальную длину дей-
таграммы можно получить с помощью методов int getMaximumLengthO и int
getNominalLengthO.
Так же как и StreamConnection, интерфейс DatagramConnection может быть использо-
ван как для клиентской, так и для серверной частей приложения. Если при созда-
нии дейтаграммы в параметре адреса не будет указано имя хоста (datagram: // : 3000),
то соединение будет работать в серверном режиме. Если имя хоста будет указано
(datagram://127.0.0.1:3000), то соединение будет работать в клиентском режиме.
Интерфейс ContentConnection
Интерфейс ContentConnection дополняет интерфейс StreamConnection. Главное от-
личие заключается в том, что если раньше интерфейс не вникал в суть передава-
----------------------- Page 154-----------------------
Интерфейс HttpConnection 15 5
емых данных, то теперь из него можно извлечь некоторую информацию, опреде-
ленную самим протоколом, с помощью следующих методов:
• String getTypeO — возвращает тип данных, передаваемых по протоколу. Для
протокола HTTP метод возвратит содержимое поля content-type заголовка;
• String getEncodingO — возвращает кодировку данных. Для протокола HTTP
возвращается содержимое поля content-encoding заголовка;
• 1 ong getLength () — возвращает длину доступных данных. Для протокола HTTP
возвращается содержимое поля content-length заголовка. Если сведения о дли-
не недоступны, то метод возвращает значение - 1 .
Интерфейс HttpConnection
Самым продвинутым интерфейсом во всей этой иерархии является интерфейс
HttpConnection, представляющий соединения, использующие протокол HTTP . По
этому протоколу организуется передача всех знакомых нам интернет-страниц.
Интерфейс HttpConnection реализует возможность получения информации полей
HTTP-заголовка ресурса, а также поддерживает передачу запросов и получение
откликов по протоколу HTTP . Кроме этого интерфейс определяет полный набор
констант, представляющих коды ошибок и статуса протокола HTTP . Интерак-
ция с веб-сервером осуществляется с помощью следующих методов интерфейса:
• long getDate() — возвращает дату из соответствующего поля заголовка. Дата
по традиции представлена количеством миллисекунд, прошедших с 1 января
1970 года;
• long getExpi ration() — выдает значение поля expires заголовка;
• String getFi 1е() — возвращает имя файла из поля URL данного соединения;
• String getHeaderField(int n) — выдает значение поля заголовка с номером п;
• String getHeaderFi el d( Stri ng name) — возвращает значение поля заголовка, оп-
ределенного ключом name. Если заголовок не содержит поля с запрошенным
именем, то возвращается значение nul 1;
• long getHeaderFieldlntCString name, long def) — возвращает поле заголовка
с именем name, представленное как целое число. Значение def возвращается,
если поле с данным именем отсутствует или значение поля нельзя преобразо-
вать в целое число;
• long getHeaderFieldDateCString name, long def) —аналогично предыдущему
методу возвращает запрошенное поле заголовка, представленное в виде даты;
• String getHeaderFi el dKey(int n) — возвращает имя поля заголовка с порядко-
вым номером п;
• String getHost() — получить имя хоста соединения или IP-адрес, если имя хо-
ста не задано;
• long getLastModifiedO — возвращает дату последней модификации ресурса,
представленную полем last-modified заголовка;
• int getPort() — возвращает номер порта соединения; 80 — стандартный номер
порта HTTP-соединений;
----------------------- Page 155-----------------------
1 56 Глава 14 . Приложения для работы с Интернетом
• Strin g getProtocol () — получить имя протокол а соединения ;
• Strin g getQuery() — выдает область запрос а адреса URL . Эт а часть начинается
после знак а ? в строке адреса ;
• Strin g getRef () — выдает область адреса URL , следующую посл е знак а #;
• String getRequestMethocK) — возвращает установленный метод HTTP-запроса ;
• Strin g getRequestProperty(String key) — возвращает установленно е свойство
key HTTP-запроса ;
• i n t getResponseCode() — получить код ответа сервера . Все возможны е коды от-
вета представлены в интерфейс е константами HTTP_ACCEPTED, HTTP_BAD_GATEWAY,
HTTP_NOT_FOUND, HTTPJORBIDDEN, HTTP_OK и т. д . Все 3 8 констант приводить здесь
не вижу смысла : кому понадобится , найдет их в документации ;
• String getResponseMessage() — получить ответ сервера в вид е строки с сообще-
нием ;
• String getURLC) — возвращает адрес U R L соединения ;
• void setRequestMethodCString method) — установить метод HTTP-запроса . До-
пустимы методы , представленны е следующими константами интерфейса : GET,
POST, HEAD;
• void setRequestProperty(String key. String value)—установит ь значени е value
свойству key запроса .
Следующий пример , которы й мы реализуем , получит по протоколу H T T P дан-
ные интернет-страницы , а затем пошлет сообщени е в гостевую книгу , разме-
щенную в Интернете . В данно м случа е нам потребуется реализация л и ш ь кли-
ентской част и п р и л о ж е н и я , поскольку серверная част ь представлен а обычны м
веб-сервером , с которы м мы связываемся с помощь ю браузера , заходя на стра-
ницу в Интернете .
Д ля реализаци и нам потребуются некоторы е знания самого протокола , а также
язык а H T M L . Опыт ы мы будем производить над гостевой книгой официальног о
сайта Ф К «Томь» , расположенной по адресу football.tomsk.ru/gb . Тольк о лиш ь из
большого уважения к этой команде , а не с целью как-то навредить . Д л я начала
стоит взглянуть , как устроен а гостевая книг а внутри , благо н а любо й страниц е
можно щелкнуть правой кнопкой и выбрать команду Просмотр в виде HTML, что
позволит нам увидеть весь код страницы в текстовом виде . Немног о поковыряв-
шись , выясняе м всю необходимую нам информацию : сообщения добавляются
с помощью HTTP-запрос а POST и скрипт а add.php. Параметры сообщения переда-
ются скрипту из форм ы в переменных username, ci ty , emai 1, homepage и message. Этой
информации нам вполн е достаточно , чтобы реализовать нашу затею .
Вот как будет выглядеть наше приложение , которое соединится с HTTP-серве-
ром , отправит запро с с помощью метода POST, а затем получит различную инфор-
мацию о сервер е и параметрах соединения :
import javax.microedition.midlet.MIDIet:
import javax.microedition.io.HttpConnection:
i mport javax.microedi t i on.io.Connector:
import java.io.OutputStream;
import java.io.IOException;
----------------------- Page 156-----------------------
Интерфейс HttpConnection 1 5 7
try {
// открыть соединение с HTTP-сервером
HttpConnection conn •
(HttpConnection)Connector.openC'http://football.tomsk.ru/gb/add.php"):
// установить метод запроса
conn.setRequestMethod("POST");
// установить свойства запроса
conn.setRequestProperty("Connection", "close") :
conn.setRequestProperty("User-Agent" . "ConnectDemo (MIDP-1.0 : J2ME)") :
conn.setRequestProperty("Content-Type" .
"application/x-www-form-urlencoded");
conn.setRequestProperty("Accept" . "text/plain") ;
// получить поток вывода соединения
OutputStream os - conn.openOutputStreamO ;
// записать параметры запроса POST в поток вывода
os.write("username=Noki a7210&message=WeAreTheChampi ons".getBytes()) :
// получить код ответа сервера
System.out.printlnC'Response Code : "+conn.getResponseCodeO) :
// получить текстовый ответ сервера
System.out.println("Response Msg : "+conn.getResponseMessage()) :
//получить базовые параметры соединения
System.out.println("Type : "+conn.getType()) :
System.out.println("Length : "+conn.getLength()) ;
System.out.pri ntln("Encodi ng ; "+conn.getEncodi ng()) ;
// получить в цикле имена и значения полей заголовка
for(int i=0;i<12;i++ )
System.out.pri ntln(conn.getHeaderFi eldKey(i)+
": "+conn.getHeaderField(i)) :
// получить и вывести параметры соединения
System.out.printlnC'Data : " + conn.getDateO) ;
System.out.printinCHost : " + conn.getHostO) :
System, out . pri ntl n ( "Port : " + conn.getPortO) ;
System, out . pri ntl n ( "Protocol : " + conn.getProtocolO) :
System, out . pri ntl n ( "Query : " + conn.getQueryO) ;
System.out.printlnC'Ref : " + conn.getRefO) ;
System.out.printlnt"Request Method : " + conn.getRequestMethodO) :
System, out . pri ntl n ( "URL : " + conn.getURLO) :
// закрыть соединение
conn.cioseO :
}
catchCIOException ioe ) {
// вывести сообщение об исключении
System.out.println("ERR:"+ioe.getMessageO) :
----------------------- Page 157-----------------------
158 Глава 14 . Приложения для работы с Интернетом
Если у вас есть выход в Интернет, то можно запускать приложение прямо на эму-
ляторе. Если вы используете прокси-сервер, то следует указать его адрес в свой-
ствах эмулятора. После запуска смотрим в области системных сообщений J2ME
WTK, насколько успешно мы соединились, анализируя ответы и параметры сер-
вера (рис. 14.4).
«Ж J2ME Wireless Toolkit - TestSample La
% Settings ... I Ц Build I % Run li I j j Clear Console:
device: |oei**Or»vPnon* 2J
Response Code: 302
Response list}: Found
Гуре: text/htal ; charset-Mindows-1251
Length: - 1
Encoding : null
date : Wed , OS Oct 200S 21:25:01 GHT
server: Apache/1.3.2 9 (Unix) mod_maoro/1.1.2 PHP/4.3.4 mod_ssl/2.8.16 OpenSSL/0.9. 6g гиэ/Р130.1 9
<-poKered-by : PHP/4.3.4
set-cookie : PHPSESSID*eSc83e4c6b599791f60e4f88ebS50a25 ; path- /
expires : Thu , 19 Nov 1981 08:52:00 GMT
cache-control : no-store , no-cache , must-revalidate , post-check=0 , pre-check-0
pragma : no-cache
set-cookie : gbcookieid=1128547501 ; expires»Fri , 04-Nov-05 21:25:01 GHT
set-cookie: gbusername=Nokia7210 ; expires«Fri , 04-Nov-05 21:25:02 GHT
set-cookie: gbcity=deleted ; expires=Tue , 05-0ct-04 21:25:01 GKT
set-cookie: gbemail=deleted ; expires=Tue , 05-0ct-04 21:25:01 GHT
set-cookie : gbhomepage=deleted ; expires=Tue , 05-0ct-04 21:25:01 GHT
Data : 1128547501000
Host : football.tomsk.ru
Port : 80
Protocol: http
Query: null
Ret: null
Request Method: POST
URL: http://football.tonsk.ru/gb/add.php
Рис. 14.4 . В области системных сообщений выведена информация о соединении с сервером
Видим, что свойства сервера нами успешно получены. Заметьте, что для тестиро-
вания программы на реальном аппарате и для успешного соединения с интернет-
ресурсом на телефоне должен быть активирован WAP-сервис. Теперь осталось
зайти на гостевую книгу по вышеуказанной ссылке и посмотреть результат наше-
го эксперимента (рис. 14.5).
Интересна реализация следующей идеи: отправка SMS-сообщений с сайта сотовой
компании, например www.mts.ru, что, возможно, даст некоторую экономию при по-
сылке SMS. Основная проблема заключается в том, что на сайте введена спам-за-
щита в виде картинки с трудночитаемыми цифрами и буквами, которые надо ввес-
ти в специальном поле. Обойти это можно следующим способом: проанализировать
HTML-текст сайта, получить ссылку на файл картинки, продемонстрировать ее на
экране мобильника и попросить пользователя ввести пароль вручную. Здесь нас
ожидает очередная проблема: формат картинки JPEG не совпадает с форматом PNG,
поддерживаемым телефонами, поэтому для отображения следует сначала конвер-
тировать картинку в нужный нам формат. Можно поискать готовый класс конвер-
----------------------- Page 158-----------------------
Интерфейс HttpConnection 159
тора (что-нибудь с именем JpegViewer, например), который можно встроить в при-
ложение, или самому связаться с таблицами Хофмана и прочими премудростями
структур графических файлов. Первый путь значительно проще, однако над реали-
зацией такого приложения придется изрядно потрудиться. А вам слабо?
-«Гостевая книга | Официальный сайт ФК "Turn," - Microsoft Internet ЕиЫогкг
I файл Правка Бид Избранное Сервис Pпрэи;э
4k
Объявление!
I Уважаемые болелыМики! До 6-го октября вы можете ;
• дублирующего состава "Томи" Эдуарду Барышеву,
Но J'J96d4 Nokia7Z10 . 05-10-2005,04:25:01
WeAreTheChampions
Na.33964 3 He., Город: Moo-ва 06-10-2005, 02.40:34
Рис. 14.5 . В гостевой книге появилось сообщение от Nokia7210
* * *
Таким образом, полную иерархию всех рассмотренных типов соединений можно
представить следующей схемой (рис. 14.6).
DatagramConnection InputConnection OutputConnection StreamConnectionNotifer
Рис. 14.6 . Иерархия типов соединений
----------------------- Page 159-----------------------
1 60 Глава 14 . Приложения для работы с Интернетом
Мы рассмотрели все типы соединений этой иерархии, выделив четыре основных
типа: потоковые соединения с коммуникационными портами, например IRDA,
соединения уровня приложений по протоколу HTTP, либо через сокеты по про-
токолу TCP/IP, дейтаграммные соединения по протоколу UDP. Из всех этих ти-
пов спецификация MIDP определяет обязательную реализацию лишь для под-
держки протокола HTTP. Попытка использования остальных типов соединений
может сформировать исключение на вашей модели телефона.
Информация, приведенная в этой главе, является лишь базовыми сведениями для
программирования сетевых приложений. Никто не говорит, что, потратив несколь-
ко минут на чтение, вы сможете написать интернет-браузер для вашего мобиль-
ника, однако, приложив некоторые усилия, организовать общение вашего теле-
фона с интернет-ресурсами вам вполне по зубам. И да пребудет с вами сила!
----------------------- Page 160-----------------------
Глава 15
Защита приложений
Если вы добрались до этого места книги, значит указанная тема должна быть для
вас уже актуальна. Допускаю, что бессонными ночами, тернистым путем проб
и ошибок, вы улучшали игру в змейку, довели ее до такого совершенства, что раз-
работчики корпорации Nokia ахнули бы и обзавидовались. Допускаю также, что
вы разработали уникальные алгоритмы для реализации игры, использовали не-
стандартные решения и феноменальные ходы.
Проблема заключается в том, что наряду с процессом компиляции, который пере-
водит программу из текстового вида в машинные команды, существует обратный
процесс декомпиляции, который позволяет восстановить исходный вид програм-
мы из конечного приложения. Конечно, есть альтруисты, которые практикуют
«open зоигсе»-программирование и публикуют все исходники программ, но если
мы хотим получать за наши труды хоть какое-то материальное вознаграждение,
то попытки расковырять наш продукт нужно пресекать.
Java Decompiler
Посмотрим для начала врагу в лицо и разберемся, как получить текст чужой про-
граммы (конечно же, только с образовательной точки зрения). Брать чужое нехо-
рошо, особенно извлекать из этого коммерческую выгоду, поэтому мы ограни-
чимся лишь просмотром и редактированием в своих, сугубо личных целях. Можно
адаптировать приложение для своей модели телефона, жизней побольше поста-
вить в игрушке или модернизировать полезную программку.
В первой главе мы рассматривали, как при помощи дата-кабеля и соответству-
ющих программ закачать приложение в телефон. Аналогичным образом можно из-
влечь приложение из телефона. Далеко за примером ходить не будем и вытащим
стандартный сборник шахматных задач «Chess Puzzle» из Nokia 6100 или чего-то
похожего. Если вы уже благополучно его стерли или никогда и не имели, то найти
это приложение в Интернете в свободном доступе не составит труда. Итак, вот он,
заветный файл chesspuzzle. jar, и нам, как совсем юным мобильным программис-
там, ну очень интересно развинтить его и посмотреть, как он устроен. Вооружаемся
отверткой типа WinRAR и смотрим, что же внутри у этого приложения (рис. 15.1).
Здесь мы наблюдаем два файла ресурсов 1 ang. xx и Puzzles. BIN, папку с картинками
icons и папку с манифестом приложения МЕТА-INF. Все самое интересное запрятано
в глубине папки с названием com. В ней мы обнаружим несколько файлов с расши-
рением *. cl ass. Это файлы откомпилированных классов приложения. Вытащим их
----------------------- Page 161-----------------------
162 Глава 15 . Защита приложений
из архива и сохраним у себя на компьютере. Внутрь этих файлов лучше и не совать-
ся: машинный язык доступен нам, простым смертным, лишь с переводчиком.
••Echesspuzzle.jar " WinRAR (evaluation copy)
Commands Favorites/Options Help
chesspuzzle.jar - ZIP archive, unpacked size 5S 475 bytes
tone Sire | Packed] Type :
;;CRC|2j
• com Папка 10,06.2005 16:40
Cj icons Папка 10.06.2005 16:40
QMETA-INF Папка 10,06,2005 16:40
(agllang.xx 697 453 File xx 10.06.2005 16:40 0A33DB1A
Ш Puzzles, BIN 8 586 7 120 File BIN 10.06.2005 16:40 2A48634B
TuU;.to,der> nd9;33b/ts5in2r
a
Рис. 15.1 . Архив мобильного приложения изнутри
Теперь самое интересное. Простой отверткой нам уже не обойтись, нужны более
мощные средства. Благодаря идеологии языка Java, связанной с транспортабель-
ностью приложений, очень много информации можно восстановить из конечного
приложения. Переводчик с машинного языка на привычный уже нам язык J2ME
называется Java-декомпилятором (Java Decompiler). Думаю, вы без труда найде-
те программное обеспечение подобного рода. В рамках этой книги мы рассмот-
рим лишь один из возможных декомпиляторов под названием DJ Java Decompiler
(по адресу dj.navexpress.com — информация о программе и разработчиках, а также
свободное распространение последней версии).
Качаем, устанавливаем, запускаем. А теперь смело открываем основной класс
приложения ChessPuzzleMidlet.dass и видим вот такую чудесную картину (рис. 15.2).
Создадим в обычном J2ME WTK проект с названием ChessPuzzleMidlet, деком или-
руем все классы приложения и сохраним в папке исходного кода /src с расшире-
нием .Java. Классы со знаком $ в названии декомпилировать не нужно, поскольку
они уже содержатся внутри класса, название которого предшествует знаку $. Фай-
лы ресурсов Lang.xx, Puzzles.BIN и картинки разархивируем и сохраним в папке
ресурсов /res с сохранением структуры каталогов.
Обратим внимание, что все классы объединены в один пакет package com.nokia.
mid. appl.chep, поэтому, чтобы компилятор обнаружил необходимые классы, нуж-
но в свойствах созданного проекта заменить название основного класса мидлета
на его полное имя, содержащее название пакета: com.nokia.mid.appl .chep.Chess
PuzzleMidlet. Делается это следующим образом: в пункте меню Project (Проект)
выбираем команду Settings (Параметры), где идем на вкладку MIDlets и редакти-
руем поле Class (рис. 15.3).
----------------------- Page 162-----------------------
Java Decompiler 1 6 3
| ChessPu/rleMidlet. jad - ОЛ Java Decompiler
Ed* Se*c h View Seftngs language loot , Це1р
в х ц .V * 3 :E g : -/' Q
i 1 J f
it {жятр1Ш by DJ v3.5.S.7? Copyright 200$ Alarms Neehkov Date: 06.10.1:006 0:52.31
•'•' Home Page: littp'J/itWfit6iu.foiUmeciy.curnfi>euM<cv^.Mmi •• Check often !ы теи; vei-siom
!.' Dvcdinpitor options: pacHimport&($)
packaae com.nokia.mid.appl.chep;
import java.io.PrintStrearn;
import Java.util.Timer;
impart javax.microedition.lcdui.'*;
import javax.microedition.midlet.MIDIet;
// Referenced classes of package corn nokis. mid appl. chep:
II FuiiCawasScreen, CbessRMS, CheasBosrd, HtghScoms,
Loca!
public class ChessPuzzleMidlet extends MIDIet
implOTisni-s CommandListener
class CountDown 0
public ChessPuzzleMidletil {)
.«•e 75
Рис. 15.2 . Декомпилированное приложение снова доступно для нашего понимания
| «Isettings for project "ChessPurcleMJdtet'*
Frequ«etl I Optional | User Defined [MO**] !
:
, ... ГКву ] * . Name [ Ж I teon J Class j
MICXet-1 i he PuzzleMidlel ChessPuzzleMidlet .png __Jcom.nokia.mid.appl.chep.ChessPuzzteMidlet I
':Шф \ •;••• -л •' m ] 1 j
• • • : • . , • ; ' . - : ч . • - . : . . . , • Cancel I
Рис. 15.3 . Изменение названия основного класса мидлета
Пришло время попытаться откомпилировать наш проект. Здесь все, как в детстве
с заводными игрушками: после того как разобрал и снова собрал, главное — чтобы
опять заработало. Возможны небольшие различия в трактовке кода разными ком-
пиляторами. Мне пришлось добавить несколько приведений возвращаемых
----------------------- Page 163-----------------------
164 Глава 15 . Защита приложений
типов, чтобы WTK перестал ругаться и выдавать ошибки. В остальном операция
прошла успешно, и пациент ожил на эмуляторе в лучшем виде.
Теперь весь исходный код приложения в нашем полном распоряжении. Разби-
раться в чужой программе всегда не очень просто, однако ничего нового для нас
мы не увидим, поскольку практически все классы и методы нами уже рассмотре-
ны. Любое приложение начинается с метода startApp(), это и есть та печка, от ко-
торой мы будем плясать.
Как мы видим, в программе используются классы Ful I Canvas и Devi ceControl из Nokia
API, то есть кроме как на аппаратах Nokia данное приложение нигде работать не
будет. Благо, мы уже знаем, как адаптировать мидлет и расширить его модельный
ряд. Для этого нужно заменить класс Full Canvas на обычный Canvas, а также акку-
ратно вырезать всю иллюминацию и любые упоминания о классе Devi ceControl.
В успешной транспортабельности приложения вы можете сами убедиться. Хотя
немного не совпали размеры экрана и часы заехали на доску, но это не такая боль-
шая проблема (рис. 15.4).
SGH-E710S«r
Рис. 15.4 . Перекомпилированное приложение успешно запущено
на телефоне другого производителя
Вопроса локализации (перевода на другой язык) приложений мы уже касались
в одной из глав. В программе шахматных задач у нас есть возможность поучиться
хорошему стилю программирования. Весь текст, который выдает приложение на
английском языке, — инструкции, пункты меню и прочее — содержится в файле
ресурса lang.xx. Все вопросы локализации решаются в одном классе Local. Рас-
смотрим процедуру получения требуемого текста:
public static synchronized String getText(int i . String as[]) {
try {
----------------------- Page 164-----------------------
Java Decompiler 1 6 5
if(loc — null)
loc - new Local О;
InputStream inputstream = loc.getClassO.getResourceAsStream
(Viang.11 + phoneLang);
if (inputstream — null)
inputstream = loc.getClassO.getResourceAsStreamC71ang.xx");
if(inputstream — null)
return "NoLang";
DataInputStream datainputstream = new DatalnputStream(inputstream) ;
datainputstream.skipBytes(i * 2 ) ;
short wordO » datainputstream.readShortO :
datainputstream.skipBytes(wordt) - i * 2 - 2) ;
String s = datainputstream.readUTFO ;
datainputstream.close() :
if(as != null )
if(as.length == 1 ) {
s = replaced . "XU", as[O]) :
} else {
forOint j = 0; з < as.length : j++ )
s - replace(s , "%" + j + "U", as[j]) :
}
return s ;
}
catch(Exception exception ) {
return "Err" ;
public static fina l String phoneLang =
System.getProperty("mi croediti on. 1 ocal e") :
Видим, что для перевода приложения на русский язык достаточно посмотреть код
локализации телефона, возвращаемый методом getProperty системного класса
System, и создать файл 1 ang с полученным расширением. Ниже в классе Local опре-
делены числовые константы для всех выводимых строчек. По коду следует разоб-
раться, каким образом проиндексирован файл ресурса. Сначала указаны номера
позиций файла, с которых начинаются требуемые строчки, затем идут сами строч-
ки. Осталось корректно сформировать файл с русским переводом. Возможно, по-
требуется дополнительная логика перевода в Unicode, которой мы пользовались
в программе BookReader. Вот и вся локализация. Если вы внимательно читали
книгу и выполняли упражнения, то вполне способны справиться с этой задачей
самостоятельно.
Как вы уже поняли, все необходимое в наших руках, и можно воротить все, что
душе угодно. Например, не нравится нам возможность обнуления установленно-
го рекорда: что это за рекорд такой, который можно отменить одним нажатием.
Дело за малым: найти место, где формируется меню Top Scores, а затем исключить
----------------------- Page 165-----------------------
166 Глава 15 . Защита приложений
из него последний пункт Reset. В основном классе приложения видим, что любое
меню формируется с помощью метода InitMenuScrdnt i , int a i [ ] . ...)спомощью
следующих команд:
f o r ( i n t k = 0; k < ai.length : k++)
m_ListScr.append(Local,getText(ai[k]) . null) :
Видим, что пункты меню формируются из строчек, представленных константа-
ми, переданными методу в массиве ai . Теперь ищем вызов метода InitMenuScr, со-
ответствующий формированию раздела меню Top Scores. Без труда обнаруживаем
нужный вызов в методе MainMenuScreenDone:
case 4: // Ч0041
int ai3[] - {
11. 10. 22
} :
InitMenuScr(HIGHSCORE_MENU_SCR, ai3 . Local.getText(25) . null . null . 0 ) ;
break ;
В массиве ai3 содержатся константы, представляющие три пункта меню: Easy,
Difficult и Reset. Очевидно, что следует оставить только два первых элемента мас-
сива, а последний аккуратно удалить:
int ai3[] = { 11. 10 }; '
Ломать — не строить. Мы удалили всего лишь несколько символов. Главное - най-
ти точку применения, а сами усилия могут быть мизерными. Компилируем и за-
пускаем приложение. А теперь, как говорится, найдите десять отличий (рис. 15.5).
J-J.XJ
•File Tools Help
Рис. 15.5 . Ликвидация пункта меню сводится к удалению из кода трех символов
Кроме бесценного опыта программистов от Nokia, что еще полезного можно из-
влечь из кода данного приложения? Решили вы, например, написать шахматы для
----------------------- Page 166-----------------------
Обфускация 16 7
мобильного телефона, чтобы в дороге играть с товарищем. Кое-что можно будет
использовать, например доску, фигурки, алгоритм перемещения фигур по доске
и секундомер. Однако если вы решите продавать свои шахматы, то придется ри-
совать и программировать все заново, чтобы не нарушить авторские права. Пре-
имущество заключается в том, что всегда можно проконсультироваться с гото-
вым кодом и разобраться, как это делают профессионалы.
Защита приложений
Легальное распространение мобильных приложений происходит через WAP-сай-
ты или SMS-сервис специализированных мобильных издательств, которые вни-
мательно следят за авторскими правами и отчислениями разработчикам продук-
тов. При уведомлении об успешной загрузке приложения на ваш телефон система
инициализации генерирует счет, который вы оплачиваете через вашего операто-
ра сотовой связи. Удобно, надежно, а если что, можно обратиться в службу под-
держки издательства: крупные и солидные конторы обычно заботливо относятся
к своим клиентам.
Теперь допустим, что вы приобрели какое-то приложение легальным образом. Кто
мешает скачать его с телефона в компьютер и выложить на общий доступ в Интер-
нет? Кто после этого будет платить деньги, когда ту же самую игру можно получить
бесплатно? Для защиты от подобного копирования можно ввести регистрацию: сам
продукт распространяется бесплатно, а вот его активация или расширение проб-
ной версии до полнофункциональной стоит денег. Вам потребуется реализовать
алгоритм регистрации и организовать удобную систему оплаты. Как вариант, мож-
но при первом запуске приложения сгенерировать случайным образом уникаль-
ный ключ и сохранить его в хранилище записей. Регистрационный механизм дол-
жен выслать код активации, соответствующий лишь данному ключу. При вводе кода
активации приложение генерирует собственный код, соответствующий сгенериро-
ванному ключу, и при совпадении разблокирует необходимую функциональность.
Существует масса методов защиты и регистрирования, однако вернемся к началу
этой главы. Казалось бы, что может быть проще: декомпилировать упрямое прило-
жение, вырезать все проверки авторизации и запросы регистрационных кодов,
а затем вернуть в исходное положение. Против таких умельцев, как мы, и действий,
именуемых не иначе, как «взлом», существует способ защиты, называемый обфус-
кацией. Не пугайтесь этого слова. Оно образовано от английского obfuscate — запу-
тывать, сбивать с толку. Сам метод заключается в предварительной модификации
исходного кода программы с целью затруднить процесс декомпиляции и анализа
приложения,
Обфускация
Более всего распространена символьная обфускация, которая меняет имена пе-
ременных, названия классов, полей и методов на бессмысленный набор симво-
лов. Например, был у нас метод GetPassword(), а после обфускации он превратил-
ся в LK23gdcasjJKHKJh(). Однако многие декомпиляторы уже справились с этим
----------------------- Page 167-----------------------
168 Глава 15 . Защита приложений
подвохом и заменяют длинные нечитаемые имена на более удобные, типа Method_l (),
Method_2() и т. п.
Существует и другой метод символьной обфускации, реализованный в J2ME
WTK, работу которого мы рассмотрим на примере. Этот метод действует с точно-
стью до наоборот, и переименовывают классы, поля и методы однобуквенными
именами, повторяющимися в каждом классе. Согласитесь, чтобы понять вот та-
кой пассаж:
b a = а.с(а. с.а);
потребуется немало усилий и тщательного изучения всей структуры программы,
которая может быть совсем не маленькой.
Обфускатор — это не что иное, как утилита, модифицирующая исходный код при-
ложения до неузнаваемости. К символьным относится, также обфускатор, встро-
енный в J2ME WTK. Для того чтобы скомпилировать приложение с предвари-
тельной обфускацией, при упаковке проекта вместо пункта Create Package (Создать
архив) следует выбрать команду Create Obfuscated Package (Создать архив с обфус-
кацией). Попробуем в деле встроенный обфускатор на тех же шахматах.
Попробуем теперь декомпилировать получившиеся классы. Сразу бросается
в глаза, что название сохранил лишь основной класс приложения, остальные на-
зываются теперь нехитро: а, Ь, с, d, e, f,... Посмотрим на оригинал конструктора
основного приложения. Так он выглядел до нашей обфускации:
public ChessPuzzleMidletO
{
m_Display = n u l l ;
m_HighScoreArray = n u l l ;
m_CurrentScreen = 0 :
m_LastScreen » 0 ;
MAIN_MENU_SCR = 1;
MODE_MENU_SCR - 2 ;
PUZZLE_MENU_SCR = 3 ;
HIGHSCORE_MENU_SCR = 4 ;
SPLASH_SCR = 5 :
PUZZLE_SCR = 6 :
LEVEL_MENU_SCR = 7 :
HIGHSCORE_DIS_SCR - 8 :
HIGHSCORE_DEL_SCR = 9 :
INSTRUCTION_SCR = 10 ;
TIMEO_MODE = 1 :
CASUAL_MODE = 0;
m_LastSelectedMenuIndex = 0;
m_AppMessage - "":
m_PlayMenu - false ;
m_ScreenWidth = 128 ;
m_ScreenHeight - 128 ;
----------------------- Page 168-----------------------
Обфускация 1 6 9
С такими именами констант и переменных разобраться в программе и внести
в нее изменения нам не составило труда. Боюсь даже предположить, сколько бы
это заняло времени и сил, если бы код выглядел следующим образом:
public ChessPuzzleMidletO
{
d = n u l l ;
flddo - n u l l ;
ab - 0:
s - 0;
p - 1 ;
W = 2:
Y = 3 : •
f = 4 :
1 - 5 ;
z - 6;
aa = 7;
T = 8;
D = 9 :
ac = 10;
_fldchar = 1;
Q = 0;
_fldcase = 0;
P = " " •
ae - false ;
v - 128; '
m = 128;
Если и до этого разобраться в чужой программе было не так просто, то теперь это
становится пыткой не для слабонервных, и поверьте, потраченное на расшифров-
ку время уж точно не стоит двух-трех долларов.
Кроме символьной, существуют и другие методы обфускации, реализованные
в обфускаторах второго порядка. Такие обфускаторы наряду с методами символь-
ной обфускации используют также обфускацию данных и графа потока управле-
ния. Здесь есть большой простор для введения людей в заблуждение: изменение
структур и местоположения данных, кодировка данных, объединение данных в мас-
сивы или наоборот, разделение массивов на отдельные переменные; добавление
неиспользуемых данных и формирование на их основе ложных условий, замена
статических присваиваний на вызовы методов; замена условий циклов на более
сложные и масса других хитростей, об которые можно сломать мозги, пытаясь ра-
зобраться в зашифрованном алгоритме. Следует отметить, что если обфускаторы
первого порядка немного сокращали размер программы, экономя на названиях, то
второй порядок обфускации может разбазарить столь ценные нам килобайты.
Как вы сами понимаете, обфускация не является панацеей от взлома, будь то не-
санкционированное расширение пробной версии или просмотр секретного алго-
----------------------- Page 169-----------------------
170 Глава 15 . Защита приложений
ритма. Единственная задача обфускаторов — сбить взломщика с толку и макси-
мально затруднить понимание, анализ и модификацию программы. В этой главе
мы подошли к проблеме с двух сторон — декомпиляция и защита. Нам придется
сталкиваться и с тем, и с другим. В любом случае, при компиляции своих про-
грамм не брезгуйте обфускаторами, даже если ваши программные изыски никого
не заинтересуют, символьная обфускация немного оптимизирует вашу програм-
му и освободит пару килобайтов для дополнительной функциональности или лиш-
ней картинки.
Как ни печально, но на этом этапе нам придется распрощаться. Я надеюсь, что
чтение не прошло для вас даром, что вы многому научились и открыли для себя
увлекательный мир мобильного программирования. Реализация приведенных
примеров, их понимание и совершенствование — вот золотой ключик от дверей
в чудо-мир мобильных технологий. Пробуйте, ошибайтесь, но не опускайте руки.
Опыт приходит с практикой!
* * *
Эта книга не является учебником, документацией или серьезным методическим
пособием, это именно самоучитель — практические советы, минимум теории
и максимум практики, личные наблюдения автора, начиная с самого нуля. Если
эта книжка уже вся исчиркана разными пометками, изрядно потрепана и залита
кофе, значит потраченное мной и вами время не прошло даром.
Если вы действительно серьезно увлеклись мобильным программированием, то
у вас неизбежно появится масса вопросов, благо на сайте разработчиков языка
java.sun.com/j2me/docs/ имеется исчерпывающая документация, масса книг и ста-
тей (правда, на английском языке). Также за советом можно сходить на специа-
лизированные форумы, например, forum.juga.ru/, раздел которого так и называет-
ся: J2ME — форум о Java в мобильнике. Пользуйтесь поиском, не стесняйтесь
спрашивать: программисты любят помогать друг другу.
Количество обладателей мобильных телефонов уже приблизилось к двум милли-
ардам (!) и неуклонно растет, и каждый из них — потенциальный пользователь
ваших программ. Простота в использовании и всеобщая доступность в любое вре-
мя — вот залог успеха. Будущее именно за мобильными, портативными техноло-
гиями, так что запрыгивайте в поезд, пока не поздно. В качестве бонус-трека вас
ожидают практические советы по продаже мобильных приложений.
Удачи вам!
----------------------- Page 170-----------------------
Bonus
Распространение мобильных
приложений. Практические советы
по продажам мобильного контента
Теперь у нас есть все необходимое для разработки игр и приложений для мобиль-
ных телефонов: аппарат с поддержкой J2ME, кабель для соединения с компьюте-
ром, необходимое программное обеспечение, а главное — знания и практические
навыки.
Если вы уже начинили свой телефон самодельными играми и приложениями, по-
делились ими с друзьями, похвастали перед коллегами, то можно сделать и следу-
ющий шаг: попытаться продать свои труды. И пусть вас не смущает, что програм-
мка вышла коротенькая, а разработка заняла не более нескольких дней: специфика
программирования для мобильных устройств играет сейчас нам на руку.
Игры для мобильных телефонов существенно отличаются от обычных компью-
терных игр, которые пишут целые команды разработчиков, крупные фирмы с боль-
шим штатом сотрудников, а потому написать что-то конкурентоспособное на этом
рынке одному человеку просто не под силу.
Ограниченная функциональность платформы, жесткие требования к размеру при-
ложения не позволяют писать зрелищные, интерактивные побоища типа Quake.
Маленькое 64-килобайтное тельце программы не способно вместить логику серь-
езной военно-экономической стратегии, да и не стоит велосипеду тягаться с го-
ночной машиной. Игра для мобильника и не должна быть сложной и наворочен-
ной. Обычно она заполняет небольшую паузу ввиду доступности в любое время
и в любом месте.
Красивая, элегантная, аккуратно реализованная идея имеет хорошие шансы при-
нести создателю некоторую прибыль. Остановимся на процедуре получения де-
нег за свой программный продукт, схеме движения финансовых потоков и, вооб-
ще, на том, как это работает.
Допустим, что вы реализовали какое-то нехитрое, но оригинальное приложение,
например шуточный мобильный тест на беременность, с предложением приложить
палец к обведенному месту на экране и с какой-то вероятностью получить один из
заготовленных ответов. Если вы считаете, что данная программа может принести
вам несколько долларов, то можно смело попытаться ее продать. В любом случае,
хуже не будет, ведь денег за эту попытку с вас никто не возьмет. Главный вопрос:
кто и каким образом будет заниматься продажей? Для этого и существуют фирмы,
которые специализируются на продажах мобильного контента. За определенный
----------------------- Page 171-----------------------
172 Распространение мобильных приложений
процент они размещают ваш продукт на своей платформе, рекламируют его, осу-
ществляют всю техническую поддержку, связанную с процедурой продажи.
Для начала нужно связаться с представителем такой фирмы и заключить договор
о передаче прав на продажу, где и будет оговорена сумма, которую вы получите
с каждой проданной копии программы.
Обычно договор не передает фирме все права на продукт, а только предоставляет
право распространения. То есть договор не должен препятствовать вам заклю-
чать соглашения параллельно с несколькими фирмами.
Устоявшаяся цена в России на момент написания этой книги, которую платит
конечный пользователь за скачанную игру или программу, — 2 доллара. Теперь
посмотрим, какая часть от этой суммы окажется у вас в кармане. Деньги за прило-
жение снимаются со счета пользователя и поступают оператору сотовой связи.
Оператор, как правило, половину этой суммы забирает себе, а половину перево-
дит на счет фирмы-распространителя. *
На этом этапе фирма имеет свой интерес и делит с вами прибыль согласно дого-
вору. При заключении договора не стесняйтесь торговаться в пределах разумно-
го. Здесь ставка по России колеблется в пределах 30-50 центов в пользу разра-
ботчика за каждую проданную копию программы.
Очевидно, что повышение вашего благосостояния напрямую зависит от числа лю-
дей, скачавших программный продукт. Дело за малым: заинтересовать пользовате-
ля полетом своей фантазии, оригинальностью, новизной, практической пользой.
Здесь уже все в ваших руках.
Особое внимание следует уделить соблюдению авторских прав, ответственность
за нарушение которых лежит целиком на разработчике. Когда речь идет о ком-
мерческом использовании, последствия нарушений могут быть очень серьезны-
ми, и наступают они куда с большей вероятностью, чем при установке операци-
онной системы с пиратского диска. Так что использование текстов, картинок,
программного кода не должно идти вразрез с чьими бы то ни было интересами.
Чтобы не быть голословными, приведем конкретные примеры и адреса наиболее
крупных в России фирм-распространителей контента для мобильных телефонов:
www.playmobile.ru, www.pLayfon.ru, www.infon.ru, www.javagames.ru. В любом случае,
такие фирмы совсем не трудно найти в поисковиках по любому запросу с ключе-
выми словами данной тематики. Серьезные фирмы отличаются качественно вы-
полненным сайтом, ежедневно обновляемыми новостяги, наличием менеджеров,
готовых оперативно выйти на связь.
Если вы попробовали свои силы на российском рынке мо» ильного программиро-
вания и чувствуете высокий коммерческий потенциал ваш й программы, то мож-
но шагнуть и за границу. Здесь также существуют давно на 1аженные схемы рас-
пространения программ, например через сайт forum.nokia.со n/business.
В современном мире сотовый телефон стал неотъемлемым атрибутом нашей жиз-
ни. Человек без мобильника вызывает сейчас не меньшее удивление, чем лет 10
назад человек с мобильником. Именно поэтому технология чрезвычайно перс-
пективна. Каждый третий человек на планете —" обладатель мобильного телефо-
на, а значит, и потенциальный потребитель программного продукта. Всех благ!
----------------------- Page 172-----------------------
Приложение 1
Полный листинг
программы «Змейка»
// - - —
ч
// Пример игры "Змейка " для телефона Nokia
//
// Реализован в рамках книги
// "Программируем игры для мобильных телефонов "
//
// (с ) Voolkan
// ---- - -
import javax.mi croedi ti on.mi dlet.MIDIet :
import javax.microedition.lcdui.Canvas ;
import javax.microedition.lcdui.ChoiceGroup ;
import javax.microedition.lcdui.Command :
import javax.microedition.lcdui.CommandListener ;
import javax.microedition.lcdui.Display ;
import javax.microedition.lcdui.Displayable ;
import javax.microedition.lcdui.Form;
import javax.microedition.lcdui.Gauge :
import javax.microedition.lcdui.Graphics :
import javax.microedition.lcdui.Image ;
import javax.microedition.lcdui.Imageltem :
import javax.microedition.lcdui.Item ;
import javax.microedition.lcdui.List ;
import javax.microedition.lcdui.Stringltem ;
import javax.microedition.rms.RecordStore ;
i mport javax.mi croedi ti on.rms.RecordEnumerati on :
import javax.mi croedi ti on.rms.RecordStoreExcepti on ;
import java.util.Vector :
import java.util.Random ;
import java.io.IOException :
import com.nokia.mid.ui.FullCanvas :
----------------------- Page 173-----------------------
1 74 Приложение 1 . Полный листинг программы «Змейка»
import com.nokia.mid.ui.DeviceControl :
import com.nokia.mid.sound.Sound :
// основной класс SnakeGame . реализующий сценарий смены экранов
public class SnakeGame extends MIDlet implements CommandListener {
private Display display : // менеджер дисплея
private Snake curSnake ; // объект змейки
private List menu : // стартовое меню
private Command ok : II команда выбора пункта меню
private ChoiceGroup levelChoice : // меню выбора уровня сложности игры
private Command set : // команда выбора уровня сложности игры
public void destroyAppCboolean destroy ) {
curSnake=null :
notifyDestroyed() :
public void pauseAppO { }
// стартовый метод мидлета
public void startAppO {
// массив строк с названиями пунктов меню
String menuOptions[ ] = {"New Game"."Set Level"."High Score"} :
// создать стартовое меню
menu = new ListC",List.IMPLICIT,menuOptions.null) :
// создать команду выбора пункта меню
ok = new Command("Ok" , Command.OK , 1) :
// добавить команду в меню
menu.addCommand(ok) :
// создать команду установки уровня игры
set = new Command("Set" , Command.BACK , 1) :
// установить блок прослушивания команд для меню
menu.setCommandLi stener(thi s) ;
// получить ссылку на менеджер дисплея
display «• Display.getDisplay(this) :
// отобразить на экране стартовую заставку
Image title = null ;
try {
// создать объект заставки
----------------------- Page 174-----------------------
Полный листинг программы «Змейка» 1 7 5
title » Image.createlmage("/title.png") ;
} catch (IOException ioe ) { }
// создать новый элемент формы
Imageltem item = new ImageltemC".title.ImageItem.LAYOUT_CENTER."") :
// отобразить заставку
showNewScreent'"SNAKE".ok.item):
// блок прослушивания команд
public void commandAction(Command с Dispiayable d ) {
// если была команда Ok из главного меню игры
if (с ~ ok & & d — menu ) {
// получить выбранный пункт меню
int selIndex • menu.getSelectedlndexO ;
switch(selIndex) {
// первый пункт меню : новая игра
case 0 :
// создать объект змейки
curSnake - new Snake() :
// создать объект треда автоматического передвижения
Thread moveThread • new Thread(curSnake) :
// начать выполнение треда
moveThread . startO :
// отобразить змейку на экране
display.setCurrent(curSnake);
break ;
// второй пункт меню : установить уровень сложности
case 1 :
// массив строк с названиями пунктов меню
String levelOptions[ ] = {"Level 1"."Level 2" ,
"Level 3"."Level 4"} ;
// создать меню выбора уровня игры
level Choi се = new Choi ceGroupC" . List . EXCLUSIVE .
levelOptions.null);
// выделить пункт меню, соответствующий
// текущему уровню сложности
levelChoice.setSelectedIndex(5-getParameter(l).true);
// отобразить новую форму
showNewScreen("Set Level".set.1evelChoice) :
break :
• // третий пункт меню : посмотреть текущий рекорд
case 2 :
// получить строку с текущим рекордом
----------------------- Page 175-----------------------
1 76 Приложение 1 . Полный листинг программы «Змейка»
String strScore - (new Integer(getParameter(0))).toStringO ;
// создать объект элемента-строки
Stringltem highScoreltem » new StringltemC"".strScore) ;
// отобразить новую форму
showNewScreenC'HIGH SCORE".ok.highScoreltem):
break;
// если была команда Ok . вызванная не из меню
if(C"-ok & & d!=menu )
// отобразить на экране стартовое меню
display.setCurrent(menu);
// если была команда Set из меню установки уровня сложности
if(с — set ) {
try {
// массив параметров игры
byte buff[ ] = {0.5} :
// открыть хранилище записей с именем "SNAKE"
RecordStore recordStore = RecordStore.openRecordStoreC"SNAKE" .
true) ;
// получить список записей хранилища
RecordEnumeration re = recordStore.enumerateRecords(null , null .
false);
// если хранилище не пусто
if (re.numRecords()!=0 ) {
// получить id записи параметров игры
int id = re.nextRecordldO :
// считать запись с параметрами игры
buff = recordStore.getRecord(id) ;
// вычислить текущий уровень игры
buff[l ] = (byte)(5 - levelChoice.getSelectedlndexO) :
// записать параметры игры
recordStore.setRecord(id.buff.0.2);
} else {
// добавить новую запись параметров игры
recordStore.addRecord(buff . 0 . 2 ) ;
} catch(RecordStoreException rse ) {
}
// отобразить на экране стартовое меню
di splay.setCurrent(menu) ;
----------------------- Page 176-----------------------
Полный листинг программы «Змейка» 1 7 7
// метод showNewScreen создает и отображает форму с элементом item
private void showNewScreentString title.Command с Item item ) {
// создать форму для нового экрана
Form newScreenForm - new Form(title) :
// добавить команду для возврата из формы
newScreenForm.addCommand(c);
// добавить элемент в форму
newScreenForm.append(item);
// установить блок прослушивания команд
newScreenForm.setCommandListener(this);
// отобразить форму на экране
display.setCurrent(newScreenForm);
// метод getParameter считывает из хранилища записей параметры игры
// и возвращает один из них , определенный входным параметром index
private byte getParameter(int index ) {
// массив параметров игры
byte buff[ ] = {0.0} ;
try {
// открыть хранилище записей с именем "SNAKE"
RecordStore recordStore = RecordStore.openRecordStoreC'SNAKE" .
true) ;
// получить список записей хранилища
RecordEnumeration re = recordStore.enumerateRecordstnull , null .
false);
// если хранилище не пусто
if (re.numRecords()!=0 ) {
// получить id и считать запись параметров игры
buff = recordStore . getRecord ( re . nextRecordldO) :
}
} catch(RecordStoreException rse ) {
}
// вернуть параметр
return buff[index] ;
// класс SnakePart представляет каждый элемент змейки
private class SnakePart extends Object {
private int x : // координата х
----------------------- Page 177-----------------------
1 78 Приложение 1 . Полный листинг программы «Змейка»
private int у ; // координата у
private int part ; // код части тела змеи
private int dir : // код направления движения
// конструктор ; принимает все прля как аргументы
public SnakePartCint _x . int _y . int _part , int _dir ) {
x=_x :
У=_У :
part=_part :
di r=_di г;
}
// получить код части тела змеи
private int getPartO { return part ; }
// получить код направления движения
private int getDirO { return dir ; }
// получить координату х
private int getX O { return x; }
// получить координату у
private int getY O { return y ; }
// установить тип части и направление
private void setPartDir(int _part , int jiir ) {
// если второй аргумент -1 .
// то направление остается неизменным
part=_part ;
1f(_d1r!--l) dir=_dir ;
// класс змейки
private class Snake extends Full Canvas implements Runnable {
// константы , определяющие направление движения
private int UP = 0; // вверх
private int DOWN = 3 ; // вниз
private int LEFT • 1; // влево ' .
private in t RIGHT - 2 ; // вправо
private int HEAD = 0 ; // голова змеи
private int TAIL - 1; // хвост змеи
private int BODY = 2 : // тело змеи
private int ACLOCKWISE_TURN = 3 ; // сгиб тела против часовой стрелки
private int CLOCKWISE_TURN = 4 : // сгиб тела по часовой стрелке
private Vector snake : // хранилище элементов змейки
----------------------- Page 178-----------------------
Полный листинг программы «Змейка» 1 7 9
private int width ; // ширина экрана
private int height : // высота экрана
private int xHead ; // координата х головы змейки
private int yHead ; // координата у головы змейки
private Image heart ; // картинка сердца
private int xHeart ; // координата х сердца
private int yHeart ; // координата у сердца
private Image[ ] images ; // массив картинок элементов змейки
private int imageSize : // размер картинки элемента змейки
private int direction : II текущее направление змейки
private boolean eatFlag ; // флаг удлинения змейки
private boolean gameOverFlag ; // флаг конца игры
private RecordStore recordStore ; // хранилище данных
private int recordID ; // ID записи параметров
private byte level ; // уровень сложности игры
private byte speed ; // коэффициент вычисления задержки треда
private byte highScore ; // текущий рекорд
// конструктор класса Snake
public SnakeO {
// конструктор родительского класса
superO ; Г
// создать массив для картинок-частей змейки
images = new Image[20] ;
// создать объекты картинок для каждой части
try {
// голова во всех направлениях
images[0] = Image.createlmageC'/HeadUp.png") :
images[l] = Image.createlmageC'/HeadLeft.png") ;
images[2] = Image.createlmageC'/HeadRight.png") :
images[3] = Image.createlmageC'/HeadDown.png") ;
// хвост во всех направлениях
images[4] - Image.createlmageC'/TailUp.png") :
images[5] = Image.createlmageC'/TailLeft.png") :
images[6] - Image.createlmageCVTailRight.png") ;
images[7] = Image.createlmageC'/TailDown.png") :
// тело
images[8] = Image.createlmageC'/BodyUp.png") ;
images[9] = Image.createlmageC'/BodyLeft.png") ;
images[10] = images[9] ;
images[ll] = images[8] ;
// сгиб тела против часовой стрелки
----------------------- Page 179-----------------------
1 80 Приложение 1 . Полный листинг программы «Змейка»
images[12] • Image.createlmageC'/TurnDownLeft.png") ;
images[13] = Image . createlmageCVTurnUpLeft.png") ;
images[14] » Image.createlmageC'/TurnDownRight.png") :
images[15] = Image . createlmageC'/TurnllpRight.png") :
// сгиб тела по часовой стрелке
images[16] = images[14] :
images[17] • images[12] :
images[18] - 1mages[15] :
images[19] = images[13] :
// сердце
heart » Image.createlmageC'/heart.png");
}
catch (IOException ioe ) { }
imageSize = images[O].getWidth() ;
// создать вектор , хранящий элементы змейки
snake = new VectorO ;
// получить ширину экрана
width = getWidthO :
// получить высоту экрана
height - getHeightO ;
// установить координаты головы в центре экрана
// для транспортабельности координаты кратны imageSize
xHead = ((width/2)/imageSize)*imageSize :
yHead = ((height/2)/imageSize)*imageSize ;
// создать элемент головы змейки
SnakePart parts • new SnakePartUHead.yHead.HEAD.UP) :
// добавить элемент в вектор
snake.addElement(partS) :
// создать элемент тела змейки
parts - new SnakeParttxHead.yHead+imageSize.BODY.UP) ;
snake.addElement(partS) :
// создать элемент хвоста змейки
parts - new SnakePart(xHead,yHead+2*imageSize.TAIL.UP) :
snake.addElement(partS) :
// установить текущее направление движения
direction • UP;
// получить новые координаты для сердца
setNewHeartO :
// сбросить флаг окончания игры
gameOverFlag = false :
// параметры для долговременного хранения
// первый байт - текущий рекорд
// второй байт - уровень сложности игры
----------------------- Page 180-----------------------
Полный листинг программы «Змейка» 1 8 1
byte buff[ ] - {0,5} ;
try {
// открыть хранилище записей с именем "SNAKE"
recordStore • RecordStore.openRecordStore("SNAKE" , true) ;
// получить список записей хранилища
RecordEnumeration re - recordStore.enumerateRecords(null , null .
false);
// если хранилище пусто
if (re.numRecords()==0 )
// добавить новую запись параметров игры
recordID - recordStore.addRecord(buff , 0 . 2 ) ;
else
// получить id записи параметров игры
recordID = re.nextRecordldO :
// считать запись параметров игры
buff = recordStore.getRecord(recordID) ;
// первый байт : текущий рекорд
highScore - buff[0] :
// второй байт : уровень сложности игры
level • buff[l] ;
// установить коэффициент вычисления задержки
speed = 100 ;
} catchCRecordStoreException rse ) {
// метод перерисовки экрана
public void paint(Graphics g ) {
// очистить экран
g.setColor(OxFFFFFF) :
g.fillRect(0.0,width.height);
g.setColor(OxOOOOOO);
// нарисовать рамку
g.drawRect(O.O.width-l.height-l);
// получить длину змейки
int snakeLen = snake.size() :
// если поднят флаг конца игры
if ( gameOverFlag ) {
// включить вибрацию на 100 мс
DeviceControl.startVibra(lOO.lOO) :
// вывести сообщение конца игры
g.drawStringCGAME OVER",width/2,height/2-10.g.HCENTER|g.TOP) ;
// строка сообщения счета игры
----------------------- Page 181-----------------------
1 82 Приложение 1 . Полный листинг программы «Змейка»
String scoreStr ;
// массив долговременных данных
byte b u f f О = new byte[2] ;
// если установлен новый рекорд
if(highScore < snakeLen ) {
highScore • (byte)snakeLen ;
// инициализировать массив новыми параметрами
buff[0 ] = highScore :
buff['l] - level :
try {
// записать новый рекорд в хранилище
recordStore.setRecord(recordID.buff . 0.2) ;
} catch(RecordStoreException rse ) {
}
// установить цвет текста красным
g.setColor(OxffOOOO);
// сформировать строку с новым рекордом
scoreStr = new StringC'HIGH SCORE : "+snakeLen) :
// если рекорд не установлен
} else
// сформировать строку со счетом
scoreStr = new StringC'YOUR SCORE : "+snakeLen) :
// вывести счет
g.drawString(scoreStr.width/2.height/2+10.g.HCENTER|g.TOP) :
// если флаг конца игры не поднят
} else {
// для каждого элемента змейки
for ( int 1=0 : i<snakeLen : i++ ){
// вывести картинку , индекс которой вычисляется
// с помощью комбинации кода части тела и кода направления
g.drawImage(images[4*((SnakePart)snake.elementAt(i)).getPart()+
((SnakePa rt)snake.elementAt(i)).getDi r()] .
((SnakePart)snake.elementAt(i)).getX().
((SnakePart)snake.elementAt(i)).getY().
g.HCENTER | g.VCENTER) ;
}
// вывести картинку сердца
g.drawImage(heart.xHeart.yHeart.g.HCENTER | g.VCENTER) :
// метод setNewHeart ; получает новые координаты для сердца
public void setNewHeart 0 {
// получить количество возможных позиций по координате х
----------------------- Page 182-----------------------
Полный листинг программы «Змейка» 1 8 3
int xPartMaxNum = (width-5)/imageSize ;
// получить количество возможных позиций по координате у
int yPartMaxNum = (height-5)/imageSize ;
// получить длину змеи
int len = snake.sizeO ;
// создать объект генератора случайных чисел
Random rnd • new Random() ;
// флаг корректной генерации
boolean setFlag - false ;
// повторяем , пока не сгенерируем корректные координаты сердца
while ( !setFlag ) {
// поднять флаг корректной генерации
setFlag - true ;
// сгенерировать случайную координату х
xHeart « imageSize*(Math.abs(rnd.nextInt(mxPartMaxNum+l) ;
// сгенерировать случайную координату у
yHeart = imageSize*(Math.abs(rnd.nextInt())PyPartMaxNum+l);
// проверить совпадение координат сердца
// с координатами элементов змеи
for ( int i-0; i<len-l : i++ )
if ( xHeart — ((SnakePart)(snake.elementAt(i))).getX( ) & &
yHeart — ((SnakePart ) (snake . elementAtd ))).getYO )
// опустить флаг корректной генерации
setFlag = false ;
// метод keyPressed обработки нажатий клавиш
public synchronized void keyPressed(int keyCode ) {
// если флаг конца игры не поднят
if ( IgameOverFlag ) {
switch (keyCode ) {
// проверить , можно ли передвигаться в заданном направлении
// и вызвать соответствующую функцию передвижения
case KEYJUM2 :
checkMove(xHead . yHead-imageSize) :
moveUpt) ;
break :
case KEY_NUM4 :
checkMove(xHead-imageSize . yHead) :
moveLeftO ;
break;
case KEY_NUM6 :
checkMove(xHead+imageSize, yHead) :
----------------------- Page 183-----------------------
1 84 Приложение 1 . Полный листинг программы «Змейка»
moveRightO :
break;
case KEYJUM8 :
checkMove(xHead, yHead+imageSize) ;
moveDownO ;
break;
// вызвать перерисовку экрана
repaintO :
} else {
// GameOver ! отобразить стартовое меню
display.setCurrent(menu) :
// метод автоматического продвижения змейки
public void run() {
// пока не поднят флаг конца игры
while (IgameOverFlag ) {
// синхронизировать с управлением
synchronized (this) {
// продвинуть змейку в текущем направлении
ifCdirection—UP) {checkMove(xHead, yHead-7) : moveUpO; }
if(direction==LEFT) {checkMove(xHead-7 . yHead) : moveLeftO: }
if(direction==RIGHT) {checkMove(xHead+7 , yHead) ; moveRightO: }
if(direction=-DOWN) {checkMove(xHead, yHead+7) : moveDownO; }
}
// вызвать перерисовку экрана
repaintO;
try {
// приостановить тред на sleep*level миллисекунд
Thread.sieep(1evel*speed);
} catch (InterruptedException e ) {
// проверить возможность передвижения
// в точку с координатами (хН.уН )
public void checkMoveOnt xH , int yH ) {
// для каждого элемента змеи
for ( int 1-3 : i<snake.size()-l : i++ ) {
----------------------- Page 184-----------------------
Полный листинг программы «Змейка» 1 8 5
// если проверяемая точка совпадает с координатами элемента
if ( хН — ((SnakePart)(snake.elementAt(i))).getX( ) &&
. уН — ((SnakePart)(snake.elementAt(i))).getYO )
// поднять флаг конца игры
gameOverFlag=true;
// проверить совпадение координат передвижения
// с координатами сердца
if ( хН == xHeart & & уН — yHeart ) {
// поднять флаг удлинения змейки
eatFlag - true ;
// пройти в цикле частоты от 100 до 1000 герц с шагом 50
for(int 1=100 : i<=1000 ; i+=50 ) {
// создать звуковой объект
Sound beep = new Sound(i.50) ;
// воспроизвести звуковой объект
beep.play(l) ;
}
// увеличить скорость
speed--;
}
else
// опустить флаг удлинения змейки
eatFlag - false ;
// передвижение влево
private void moveLeftO {
// если текущее направление направо , то возврат
if ( direction==RIGHT ) return ;
// сдвинуть координату головы влево
xHead-=imageSize;
// проверить выход за левую границу экрана
if ( xHead <=• imageSize/2+1 ) { gameOverFlag=true ; return ; }
// получить первый элемент змеи
SnakePart head = (SnakePartKsnake.firstElementO) ;
// если текущее направление вверх
if ( direction==UP )
// заменить голову на сгиб против часовой стрелки
head.setPartDi r(ACLOCKWISE_TURN.LEFT) ;
else {
// если текущее направление вниз
if ( direction-=DOWN )
----------------------- Page 185-----------------------
1 86 Приложение 1 . Полный листинг программы «Змейка»
// заменить голову на сгиб по часовой стрелке
head.setPartDi r(CLOCKWISE_TURN,LEFT) ;
else // если текущее направление влево
// заменить голову на тело
head.setPartDi rCBODY,LEFT) ;
// создать новый элемент для головы
SnakePart sPart • new SnakePart(xHead,yHead,HEAD,LEFT) ;
// добавить голову первым элементом вектора
snake.i nsertElementAt(sPa rt,0) ;
// если флаг удлинения змеи поднят
if (eatFlag ) {
// получить новые координаты для сердца
setNewHeartC);
else {
// продвинуть хвост ,
// удалить последний элемент
snake.reirioveElement(snake.lastElement());
// назначить хвостом предпоследний элемент
- ((SnakePart)snake.1astElement()).setPartDir(TAIL.-1) ;
// установить текущее направление
direction = LEFT ;
}
// передвижение вправо
private void moveRightO {
// если текущее направление налево , то возврат
if ( direction==LEFT ) return :
// сдвинуть координату головы вправо
xHead+=imageSize;
// проверить выход за правую границу экрана
if ( xHead >- width-imageSize/2 ) { gameOverFlag=true ; return ; )
// если текущее направление змейки вверх .
// заменить голову на сгиб по часовой стрелке
if ( direction — UP ) ((SnakePartKsnake.firstElementO)) .
setPartDi r(CLOCKWISEJ"URN , RIGHT) ;
else {
// если текущее направление змейки вниз ,
// заменить голову на сгиб против часовой стрелки
if ( direction == DOWN ) ((SnakePartKsnake.firstElementO)) .
----------------------- Page 186-----------------------
Полный листинг программы «Змейка» 18 7
setPartDi r(ACLOCKWISE_TURN.RIGHT) ;
// продвижение по лрямой , заменить голову на тело
else ((SnakePart)(snake.firstElement())).setPartDir(BODY.RIGHT) :
// добавить новый элемент головы змейки
SnakePart sPart - new SnakePart(xHead.yHead.HEAD.RIGHT) ;
snake.insertElementAt(sPart.O);
// если поднят флаг удлинения
if (eatFlag )
// получить новые координаты для сердца
setNewHeartO;
else {
// продвинуть хвост
snake . removeEl ement (snake . lastElementO) ;
((SnakePart Jsnake.lastElementO) . setPartDi r(TAIL.-l) :
}
// установить текущее направление движения
direction = RIGHT :
// передвижение вверх
private void movellpO {
// если текущее направление вниз , то возврат
if ( direction ~ DOWN ) return :
// сдвинуть координату головы вверх
yHead-=imageSize;
// проверить выход за верхнюю границу экрана
if ( yHead <= imageSize/2+1 ) { gameOverFlag=true ; return : }
// если текущее направление змейки влево .
// заменить голову на сгиб по часовой стрелке
if ( direction==LEFT ) ((SnakePartKsnake.firstElementO)) .
setPartDi r(CLOCKWISE_TURN.UP) ;
else {
// если текущее направление змейки направо ,
// заменить голову на сгиб против часовой стрелки
if ( direction==RIGHT ) ((SnakePartMsnake.firstElementO)) .
setPartDi r(ACLOCKWISEJURN.UP) ;
// продвижение по прямой, заменить голову на тело
else ((SnakePart)(snake.firstElementO)).setPartDir(B0DY.UP) ;
// добавить новый элемент головы змейки
----------------------- Page 187-----------------------
1 88 Приложение 1 . Полный листинг программы «Змейка»
SnakePart sPart - new SnakePart(xHead.yHead.HEAD.UP);
snake.insertElementAt(sPart.O);
// если поднят флаг удлинения
i f (eatFlag)
// получить новые координаты для сердца
setNewHeartO;
else {
// продвинуть хвост
snake.removeElement(snake.lastElementO);
((SnakePart)snake.lastElementO).setPartDir(TAIL.-l);
// установить текущее направление движения
direction = UP ;
}
// передвижение вниз
private void moveDownO {
// если текущее направление вверх, то возврат
if ( direct1on=4JP ) return ;
// сдвинуть координату головы вниз
yHead+=imageSize;
// проверить выход за нижнюю границу экрана
if ( yHead >= height-imageSize/2 ) { gameOverFlag-true,- return ; }
// если текущее направление змейки влево .
// заменить голову на сгиб против часовой стрелки
if ( direction—LEFT ) ((SnakePartKsnake.firstElementO)) .
setPartDi r(ACLOCKWISE_TURN.DOWN) :
else {
// если текущее направление змейки направо ,
// заменить голову на сгиб по часовой стрелке
if ( direction==RIGHT ) ((SnakePartXsnake.firstElementO)) .
setPartOi r(CLOCKWISE_TURN.DOWN) :
// продвижение по прямой, заменить голову на тело
else ((SnakePart)(snake.firstElement())).setPartDir(BODY.DOWN) :
// добавить новый элемент головы змейки
SnakePart sPart = new SnakeParUxHead.yHead.HEAD.DOWN) ;
snake.insertElementAt(sPart.O);
// если поднят флаг удлинения
if (eatFlag )
----------------------- Page 188-----------------------
Полный листинг программы «Змейка» 1 8 9
// получить новые координаты для сердца • •
setNewHeartO :
else {
// продвинуть хвост
snake.removeElement(snake.1astElement()):
((SnakePart)snake.lastElementO).setPartDir(TAIL.-l);
}
// установить текущее направление движения
direction - DOWN :
}
} // class Snake
} // class SnakeGame
----------------------- Page 189-----------------------
Приложение 2
Полный листинг
программы «BookReader»
// Пример программы "BookReader"
//
// Реализован в рамках книги
// "Программируем игры для мобильных телефонов"
//
// (с ) Voolkan
import javax.microedition.midlet.MIDlet ;
import javax.microedition.lcdui.* ;
import Java.util.Stack ;
import java.io.InputStream :
import java.io.EOFException :
import java.io.IOException ;
public class BookReader extends MIDlet {
private Display display : // менеджер дисплея
private BookCanvas fCanvas : // экранный фрагмент книги
,public void destroyApp(boolean flag ) {
}
public void pauseAppO {
// стартовый метод мидлета
public void startAppO {
// получить ссылку на менеджер дисплея
display = Display.getDisplay(this) :
----------------------- Page 190-----------------------
Полный листинг программы «BookReader» 1 9 1
// создать новую форму с именем основного класса
Form form = new Form(getClass( ) .getNameO) ;
// создать объект экранного фрагмента книги
fCanvas • new BookCanvasO ;
// отобразить экранный фрагмент книги
display.setCurrenttfCanvas);
// класс экранного фрагмента книги
public class BookCanvas extends Canvas {
private Stack Pagelndex ; // стек смещений страниц от начала текста
public BookCanvasO {
// создать новый объект стека номеров страниц
Pagelndex - new StackO :
// метод перерисовки объекта экранного фрагмента
protected void paint(Graphics g ) {
// получить ширину и высоту рабочей области экрана
int gw = g.getClipWidthO ;
int gh - g.getClipHeightO :
// создать шрифт
Font font = Font.getFont(Font.FACE_M0N0SPACE .
Font.STYLE_PLAIN .
Font.SIZE_SMALL) :
// установить шрифт
g.setFont(font) :
// инициализировать поток
InputStream is - getClass0.getResourceAsStreamC7story.txt") ;
MyDatalnputStream mdis = new MyDatalnputStream(is) ;
int x=0,y=0 : // текущее положение вывода на экране
int offset-0 ; // смещение по исходному тексту
// очистка экрана
g.setColor(255,255.255);
g.fillRect(O,O.gw.gh) :
g.setColor(O.O.O);
// получить смещение для очередной страницы
if(!Pagelndex.emptyO)
----------------------- Page 191-----------------------
1 92 Приложение 2 . Полный листинг программы «BookReader»
offset-C(Integer)PageIndex.peek()).intValuet) :
try {
// сместиться по тексту до необходимой страницы
mdis.skipBytes(offset) ;
} catchUOException ioe ) { }
String sWord :
do {
// прочитать очередное слово из потока
sWord - mdis.readWordO :
// продвинуть смещение по файлу на одно слово
offset+=sWord.1ength();
// если текущая позиция и пиксельная ширина слова
// не выходят за экран
if(x+font.stringWidth(sWord)<=gw) {
// отобразить слово , начиная с текущей позиции
g.drawStringtsWord . x.y . g.TOPjg.LEFT) :
// сместить текущую позицию на пиксельную ширину слова
x+=font.stringWidth(sWord);
II если слово последнее в строке , перевести текущую позицию
// в начало новой строки на дисплее
if (mdis.b_endline ) { y+=font.getHeight() ; x-0 : }
}
// слово не входит по ширине с текущей позиции
else {
// перейти на новую строку
y+-font.getHei ght() ;
// если на экране есть место для новой строки .
// отобразить слово с начала строки
1f(y+font.getHei ght()<gh )
g.drawString(sWord . O.y . g.TOP|g.LEFT) :
// перевести текущую позицию на ширину слова
x-font.stringWidth(sWord);
}
// повторять до тех пор , пока есть место
// для новой строки или не кончится файл
} while(y+font.getHeight()<gh & & !mdis.b_endFile) :
int index ; // смещение начала страницы
// если цикл закончился чтением слова , которое не было
// отображено на экране , сместить начало новой страницы
// назад на одно слово
if(mdis.b_endLine) index-offset ; else index=offset-sWord.length() :
----------------------- Page 192-----------------------
Полный листинг программы «BookReader» 1 9 3
// поместить в стек начало следующей страницы
if(!mdis.b_endFile) Pagelndex.push(new Integer(index)) :
// закрыть поток
t r y {
mdis.closet);
} catch(IOException ioe) {}
// метод обработки нажатий клавиш
public void keyPressedCint keyCode){
// обработка нажатий клавиш
switch(keyCode) {
case KEYJUM1:
// удалить из стека начала
// следующей и текущей страницы
Pagelndex.popO;
ifdPagelndex.emptyO) Pagelndex.popO;
repaintO:
break :
case KEYJUM2 :
// перерисовать экран
// (начало следующей страницы уже в стеке )
repaintO :
break :
case KEY_NUM0 :
// очистить стек и перерисовать экран
Pagelndex.removeAl1 Elements() ;
repaintO;
break;
} // class BookCanvas
} / / c l a s s BookReader
import java.io.InputStream ;
import java.io.DatalnputStream ;
import java.io.EOFException :
import java.io.IOException :
public class MyDatalnputStream extends OatalnputStream {
// кодовая строка преобразования символов
private String WIN1251_TO_UNICODE -
----------------------- Page 193-----------------------
1 94 Приложение 2 . Полный листинг программы «BookReader»
"\u0402\u0403\u201a\u0453\u201e\u2026\u2020\u2021\u20ac\u2030\u0409\u2039\u040a\u040c\u040b\
U040f\u0452\u2018\u2019\u201c\u201d\u2022\u2013\u2014\ufffd\u2122\u0459\u203a\u045a\u045c\
U045b\u045f\u00a0\u040e\u045e\u0408\u00a4\u0490\u00a6\u00a7\u0401\u00a9\u0404\u00ab\u00ac\
U00ad\u00ae\u0407\u00b0\u00bl\u0406\u0456\u0491\u00b5\u00b6\u00b7\u0451\u2116\u0454\u00bb\
U0458\u0405\u0455\u0457\u0410\u0411\u0412\u0413\u0414\u0415\u0416\u0417\u0418\u0419\u041a\
U041b\u041c\u041d\u041e\u041f\u0420\u0421\u0422\u0423\u0424\u0425\u0426\u0427\u0428\u0429\
U042a\u042b\u042c\u042d\u042e\u042f\u0430\u0431\u0432\u0433\u0434\u0435\u0436\u0437\u0438\
U0439\u043a\u043b\u043c\u043d\u043e\u043f\u0440\u0441\u0442\u0443\u0444\u0445\u0446\u0447\
U0448\u0449\u044a\u044b\u044c\u044d\u044e\u044f";
public boolean b_endLine » false ; // флаг конца строки
public boolean b_endFile = false ; // флаг конца файла
public MyDatalnputStreamdnputStream is ) {
super(is);
// метод чтения слова из потока
public String readWordO {
int i=0 ; // счетчик символов в текущем слове
b_endFile = false ;
char[] word = new char[100] : // массив для чтения слова
try {
// читать слово посимвольно до пробела или перевода строки
do word[i++]=(char)convert(readl)nsignedByte()) :
while (word[i-l]!=32 && word[i-l]!=10) :
} catch(EOFException ioe ) {
// конец файла
i--: b_endFile - true ;
}
catchdOException ioe ) { }
// если слово заканчивается переводом строки , поднять флаг
if(word[i-l]==10) b_endl_ine=true : else b_endLine=false :
// возвратить строку с прочитанным словом
return (new String(word.O.i)) ;
// метод конвертации симолов в Unicode
char convert(int ch ) {
return (ch < 128 ) ? (char)ch : WIN1251_T0_UNIC0DE.charAt(ch-128) :
----------------------- Page 194-----------------------
Приложение 3
Полный листинг
программы «AdressBook»
// Пример программы "AdressBook"
//
// Реализован в рамках книги
// "Программируем игры для мобильных телефонов"
//
// (с ) Voolkan
import javax.microedition.midlet.MIDlet:
import javax.microedition.lcdui.Command;
import javax.microedition.lcdui.CommandListener;
import javax.microedition.lcdui.Display;
import javax.microedition.lcdui.Displayable;
import javax.microedition.lcdui.Form;
import javax.microedition.lcdui.List ;
import javax.microedition.lcdui.TextField;
import javax.microedition.lcdui.DateField;
import javax.microedition.lcdui.TextBox;
import java.util.Date :
import java.util.Calendar ;
import java.io.ByteArrayOutputStream ;
import java.io.ByteArraylnputStream ;
import java.io.DataOutputStream :
import java.io.DatalnputStream :
import java.io.IOException ;
import javax.microedition.rms.RecordStore ;
import javax.mi croediti on.rms.RecordEnumerati on :
import javax.microediti on.rms.RecordComparator :
i mport java x . mi croedi ti on . rms . RecordFi H e r :
import javax.mi croedi ti on.rms.RecordStoreExcepti on ;
public class AddressBook extends MIDlet implements CommandListener
----------------------- Page 195-----------------------
1 96 Приложение 3 . Полный листинг программы «AdressBook»
private Display display ; // менеджер дисплея
private RecordStore recordStore ; // хранилище записей
private List nameList ; // список имен
private int redndexes[] : // массив ID записей .
// соответствующий списку имен
private Command add,ok,next,back ; // команды переходов между экранами
private TextBox tbName.tbPhone.tbEMail : // экраны ввода параметров записи
private Form dateForm : // форма ввода дня рождения
private DateField dateField ; // поле ввода дня рождения
public void destroyApp(boolean destroy )
notifyOestroyedO ;
public void pauseAppO { }
public void startAppO {
try {
// открыть хранилище записей с именем "Address-Book"
recordStore = RecordStore.openRecordStoreC"Address-Book" , true) :
} catch (RecordStoreException rse ) { }
// получить ссылку на менеджер дисплея
display - Display.getDisplay(this) ;
// создание объектов команд
ok = new Command("Ok" . Command.OK . 1) ;
add " new CommandC'Add" . Command.BACK , 1) ;
next • new Command("Next" . Command.OK . 1) ;
back - new CommandC'Back" , Command.BACK . 1) :
// поле ввода имени
tbName » new TextBox("Name:"."".15,TextField.ANY) ;
tbName.addCommand(next);
tbName.addCommand(back) :
tbName.setCommandListener(this);
// поле ввода номера телефона
tbPhone - new TextBox ( "Number:11 ."". 15 . TextField.PHONENUMBER) :
tbPhone.addCommand(next) :
tbPhone.addCommand(back) :
tbPhone.setCommandLi stener(this) :
// поле ввода электронной почты
tbEMail - new TextBox("E-Mail:"."".35,TextField.EMAILADDR) :
----------------------- Page 196-----------------------
Полный листинг программы «AdressBook» 1 9 7
tbEMa i1.addCommand(next) :
tbEMail.addCommand(back);
tbEMail.setCommandLiPtener(this):
// экран ввода дня рождения
dateField * new DateField("Birthday",DateField.DATE) ;
dateForm = new FormC"') ;
dateForm.append(dateField);
dateForm.addCommand(next) :
dateForm.addCommand(back);
dateForm.setCommandListener(this) :
// создать список имен ,
BuildNameListO ;
// поиск именинника
String name • SearchBirthdayO :
// если имя найдено
if(name!=null) {
// создать форму напоминания
Form remindForm = new Form("Reminder") ;
// добавить напоминание в форму
remindForm.append Сname + " has a birthday today!") ;
// добавить команду возврата
remindForm.addCommand(ok);
remi ndForm.setCommandLi stener (this) ;
// отобразить форму
display.setCurrent(remindForm); .
} else
// отобразить список имен на экране
di splay.setCurrent(nameLi st) :
// нетод SearchBirthday . возвращает строку с именем именинника
// если в этот день именинников нет , то возвращает null
private String SearchBirthday(j {
String name » null ;
try {
// создать объект фильтра по дню рождения
BirthdayFilter filter = new BirthdayFilterO ;
// получить список записей с подходящим днем рождения
RecordEnumeratiop . re - recordStore.enumerateRecords(fiHer . null .
false) :
// получить ID записи
int id - re.nextRecordldO :
// получить запись по ID
----------------------- Page 197-----------------------
1 98 Приложение 3 . Полный листинг программы «AdressBook»
byte[ ] record • recordStore.getRecord(id) :
// преобразовать запись в байтовый поток
ByteArraylnputStream bais = new ByteArraylnputStream(record) ;
// создать поток , поддерживающий чтение по типу
DatalnputStream dis • new DataInputStream(bais) ;
// считать из потока строку с именем
name =-dis,readUTFC) :
}
catch(RecordStoreException rse ) { }
• catchdOException ioe ) { }
// вернуть имя именинника
return name :
// метод BuildNameList ; создает список имен из адресной книги
private void BuildNameListО {
// создать объект списка
nameList = new ListC'Address-Book" , List.IMPLICIT) :
nameLi st.setCommandLi stener(this) :
// команда добавления записи
nameList.addCommand(add) :
// команда получения параметров записи
nameList.addCommand(ok) :
try {
// получить количество записей
int size = recordStore.getNumRecordsO ;
// создать массив для хранения ID записей
reclndexes - new int[size] ;
// создать объект компаратора в алфавитном порядке
Alphabetical Ordering comparator = new AlphabeticalOrderingO :
// получить список записей хранилища
RecordEnumeration re - recordStore.enumerateRecords(null , comparator .
false),:
// индекс массива хранения ID записей
int i=0 :
// бесконечный цикл ; выход из цикла происходит по
// формированию исключения получения'ID следующей записи .
// после того как было получено ID последней записи
while(true ) {
// получить ID следующей записи
int id = re.nextRecordldO ; ,
// записать ID в массив
reclndexes[i++]=id;
// получить запись по Ш
byte[ ] record - recordStore.getRecord(id) ;
----------------------- Page 198-----------------------
Полный листинг программы «AdressBook» 19 9
// преобразовать запись в байтовый поток
ByteArraylnputStream bais=new ByteArraylnputStream(record) ;
// создать поток , поддерживающий чтение по типу
DatalnputStream dis - new DatalnputStream(bais) :
// считать из потока первую строку и добавить в список
nameList.append(dis.readUTF().null) :
catch(RecordStoreException rse )
catch(IOException ioe ) { }
// блок прослушивания команд
public void commandAction (Command с Displayable d ) {
// команда Add отображает экран ввода имени
if(c==add)
display.setCurrent(tbName);
// команда Ok
if(c—ok) {
// если команда вызвана из экрана со списком имен
if(d==namel_ist) {
try {
// получить в массиве reclndexes ID,необходимой записи
int id - recIndexes[nameList.getSelectedIndex()] :
// считать необходимую запись
byte[ ] record - recordStore.getRecord(id) ;
// преобразовать запись в байтовый поток
ByteArraylnputStream bais=new ByteArrayInputStream(record) ;
// создать поток , поддерживающий чтение по типу
DatalnputStream dis = new DatalnputStream(bais) ;
// создать форму для отображения параметров
Form infoForm = new Form("") :
// считать строки с параметрами и добавить в форму
infoForm.append(dis.readUTF()+"\n") :
infoForm.append(dis.readUTF()+"\n");
infoForm.append(dis.readUTFO);
// создать объект даты рождения
Date birthday = new Date(dis.readLongO) :
// создать объект поля ввода даты
DateField df = new DateField("".DateField.DATE) ;
// установить дату рождения в поле ввода даты
df.setDate(birthday) :
// добавить даты рождения в форму отображения параметров
infoForm.append(df);
// добавить команду возврата к списку имен
----------------------- Page 199-----------------------
2 0 0 Приложение 3 . Полный листинг программы «AdressBook»
i nfoForm.addCommand(ok) ;
i nfoForm.setCommandLi stener(thi s ) :
// отобразить форму на экране
display.setCurrent(infoForm);
}
catch(RecordStoreException rse ) { }
catch(IOException ioe ) { }
} else { // если команда вызвана из формы параметров записи
// вернуться к списку имен
display.setCurrent(nameList);
// команда Next : перейти к следующему полю ввода
if(c==next) {
// из поля ввода имени - к полю ввода номера телефона
if(d==tbName) display.setCurrent(tbPhone) ;
// из поля ввода номера телефона - к полю ввода e-mail
if(d==tbPhone) display.setCurrentCtbEMail) :
// из поля ввода e-mail - к дню рождения
i f(d==tbEMai1 ) display.setCurrent(dateForm) ;
// из поля ввода дня рождения
if(d==dateForm) {
- // создать байтовый поток вывода
ByteArrayOutputStream baos - new ByteArrayOutputStream() ;
// создать поток вывода , поддерживающий запись по типу
DataOutputStream dos = new DataOutputStream(baos) ;
try {
// записать введенные параметры в поток вывода
dos.writeUTF(tbName.getStri ng()) :
dos.writeUTF(tbPhone.getStringO);
dos.writeUTF(tbEMail.getStringO);
dos.wri teLong(dateFi eld.getDate().getTi me()) ;
// добавить запись в хранилище
recordStore.addRecord(baos.toByteArray() . 0 . baos.sizeO) ;
}
catchdOException ioe) {}
catch (RecordStoreException rse) {}
// создать список имен
BuildNameListO :
// отобразить список имен на экране
display.setCurrent(nameList);
// команда Back : возврат к предыдущему полю ввода
----------------------- Page 200-----------------------
Полный листинг программы «AdressBook» 2 0 1
if(c==back) {
// из поля ввода имени - к списку имен
if(d==tbName) display.setCurrent(nameList) ;
// из поля ввода номера телефона - к полю ввода имени
if(d==tbPhone) display.setCurrent(tbName) ;
// из поля ввода e-mail - к полю ввода номера телефона
if(d--tbEMail) display.setCurrent(tbPhone) :
-II из поля в-вода даты рождения - к полю ввода e-mail
if(d==dateForm) display.setCurrent(tbEMai1) :
// класс фильтра записей по дням рождения
private class BirthdayFilter implements RecordFilter {
// метод сравнения записей
public boolean matches(byte[ ] candidate ) {
// преобразовать запись в байтовый поток
ByteArraylnputStream bais = new ByteArraylnputStream(candidate) ;
// создать поток поддерживающие чтение по типу
DatalnputStream dis - new DatalnputStream(bais) :
// день рождения
Date birthDate « new Date'O ;
try {
// считать строковые параметры записи
dis.readUTFO ;
dis.readUTFO ;
dis.readUTFO :
// считать дату рождения
birthDate.setTi me(di s.readLong()) ;
}
catch (IOExcepti.on ioe ) { return false : }
// получить два календаря с текущей датой
Calendar rightNow • Calendar.getlnstanceO :
Calendar birthday = Calendar.getlnstanceO :
// установить дату рождения именинника
bi rthday.setTi me(bi rthDate) ;
// сравнить день и месяц рождения с текущей датой
i f(rightNow.get(Calendar.DAY_OF_MONTH)==
birthday.get(Calendar.DAY_OF_MONTH) &&
ri ghtNow.get(Ca1enda r.MONTH)—
bi rthday.get(Ca1enda r.MONTH) )
return true ;
else
----------------------- Page 201-----------------------
2 0 2 Приложение 3 . Полный листинг программы «AdressBook»
return false ;
// класс компаратора записей по алфавиту
private class Alphabetical Ordering implements RecordComparator {
// метод сравнения записей
public int compare(byte[ ] reel . byte[ ] rec2 ) {
// преобразовать записи в байтовый поток
ByteArraylnputStream baisl = new ByteArraylnputStream(recl) ;
ByteArraylnputStream bais2 = new ByteArrayInputStream(rec2) :
// создать потоки , поддерживающие чтение по типу
DatalnputStream disl = new DataInputStream(baisl) ;
DatalnputStream dis2 = new DataInputStream(bais2) ;
// строки имен
String namel = null :
String name2 » null :
try {
// считать строки с именами
namel = disl.readUTF О :
name2 - dis2.readUTF () :
}
catch (IOException ioe ) { }
// лексикографическое сравнение строк
int result = namel.compareTo(name2) :
if (result < 0 )
// первая запись предшествует второй
return RecordComparator.PRECEDES ;
else
if (result == 0 )
// записи содержат идентичные имена
return RecordComparator.EQUIVALENT ;
else
// вторая запись предшествует первой
return RecordComparator.FOLLOWS ;
} // class AddressBook
----------------------- Page 202-----------------------
Литература
• Брюс Эккель. Философия Java. 3-е изд. СПб.: Питер, 2003.
• Жерздев С. B.Java 2 Micro Edition. ИТЛаб, ННГУ, ВМК, 2003.
• Вартан Пирумян. Платформа программирования J2ME для портативных уст-
ройств. КУДИЦ-Образ, 2002.
• Кен Арнольд, Джеймс Гослинг. Язык программирования JAVA. СПб.: Питер,
1997.
Использованы материалы порталов и форумов:
• java.sun.com/j2me/docs
• www.juga.ru
• www.javagu.ru
----------------------- Page 203-----------------------
Евгений Леонидович Буткевич
Пишем программы и игры для сотовых телефонов
Главный редактор Е. Строганова
Заведующий редакцией А. Кривцов
Руководитель проекта . А. Крузенштерн
Литературный редактор Е. Бурнашова
Иллюстрации . В. Демидова, С. Романов
Художник С. Мапикова
Корректоры И. Смирнова, Н. Шелковникова
Верстка С. Романов
Лицензия ИД № 05784 от 07.09.01.
Подписано в печать 18.11.05. Формат 70X100/16. Усл. п. л. 16,77. Тираж 3000 экз. Заказ№6647 .
ООО «Питер Принт». 194044, Санкт-Петербург, Б. Сампсониевский пр., 29а.
Налоговая льгота — общероссийский классификатор продукции ОК 005-93, том 2; 953005 — литература учебная.
Отпечатано с готовых диапозитивов в ФГУП «Печатный двор» им. A.M . Горького
Федерального агентства по печати и массовым коммуникациям.
197110, Санкт-Петербург, Чкаловский пр., 15.
----------------------- Page 204-----------------------
К Л У Б П Н О/ Ш С С И О НА Л
В1997 году по инициативе генерального директора Издательского дома «Питер»
Валерия Степанова и при поддержке деловых кругов города в Санкт-Петербурге
был основан «Книжный клуб Профессионал». Он собрал под флагом клуба про-
фессионалов своего дела, которых объединяет постоянная тяга к знаниям и любовь
к книгам. Членами клуба являются лучшие студенты и известные практики из раз-
ных сфер деятельности, которые хотят стать или уже стали профессионалами в той
или иной области.
Как и все развивающиеся проекты, с течением времени книжный клуб вырос
в «Клуб Профессионал». Идею клуба сегодня формируют три основные «клубные»
функции:
• неформальное общение и совместный досуг интересных людей;
• участие в подготовке специалистов высокого класса
(семинары, пакеты книг по специальной литературе);
• формирование и высказывание мнений современного профессионала
(при встречах и на страницах журнала).
КАК ВСТУПИТЬ В КЛУБ?
Для вступления в «Клуб Профессионал» вам необходимо:
• ознакомиться с правилами вступления в «Клуб Профессионал»
на страницах журнала или на сайте www.piter.com;
• выразить свое желание вступить в «Клуб Профессионал»
по электронной почте postbook@piter.com или по тел. (812) 103-73-74 ;
• заказать книги на сумму не менее 500 рублей в течение любого времени
или приобрести комплект «Библиотека профессионала».
«БИБЛИОТЕКА ПРОФЕССИОНАЛА»
Мы предлагаем вам получить все необходимые знания, подписавшись на «Библио-
теку профессионала». Она для тех, кто экономит не только время, но и деньги.
Покупая комплект - книжную полку ((Библиотека профессионала», вы получаете:
• скидку 1 5 % от розничной цены издания, без учета почтовых расходов;
• при покупке двух или более комплектов - дополнительную скидку 3% ;
• членство в «Клубе Профессионал»;
• подарок - журнал «Клуб Профессионал».
пзалтепьскип а ом
Закажите бесплатный журнал
0
«Клуб Профессионал» . "^SP www! р\гв "
----------------------- Page 205-----------------------
ЗАКАЗАТЬ КНИГИ ИЗДАТЕЛЬСКОГО ДОМА «ПИТЕР»
МОЖНО ЛЮБЫМ УДОБНЫМ ДЛЯ ВАС СПОСОБОМ:
• по телефону: (812) 103-73-74;
• по электронному адресу: postbook@piter.com;
• на нашем сервере: www.piter.com;
• по почте: 197198, Санкт-Петербург, а/я 619,
ЗАО «Питер Пост».
ВЫ МОЖЕТЕ ВЫБРАТЬ ОДИН ИЗ ДВУХ СПОСОБОВ ДОСТАВКИ
И ОПЛАТЫ ИЗДАНИЙ:
$g? Наложенным платежом с оплатой заказа при получении посылки на
ближайшем почтовом отделении. Цены на издания приведены ориентиро-
вочно и включают в себя стоимость пересылки по почте (но без учета
авиатарифа). Книги будут высланы нашей службой «Книга-почтой»
в течение двух недель после получения заказа или выхода книги из печати.
Щ Оплата наличными при курьерской доставке (для жителе й Москвы
и Санкт-Петербурга) . Курьер доставит заказ по указанному адресу
в удобное для вас время в течение трех дней.
ПРИ ОФОРМЛЕНИ И ЗАКАЗА УКАЖИТЕ :
• фамилию, имя, отчество, телефон, факс, e-mail;
• почтовый индекс, регион, район, населенный пункт,
улицу, дом, корпус, квартиру;
• название книги, автора, код, количество заказываемых
экземпляров.
ПОМ
Вы можете заказать бесплатный
журнал «Клуб Профессионал»
www. COM
----------------------- Page 206-----------------------
© СПЕЦИАЛИСТАМ
КНИЖНОГО БИЗНЕСА!
WWW.PITER.COM
ПРЕДСТАВИТЕЛЬСТВА ИЗДАТЕЛЬСКОГО ДОМА «ПИТЕР»
предлагают эксклюзивный ассортимент компьютерной, медицинской,
психологической, экономической и популярной литературы
РОССИЯ
Москва м. «Павелецкая», 1 -й Кожевнический переулок, д. 10; тел./факс (095) 234-38-15,
255-70-67, 255-70-68; e-mail: sales@piter.msk.ru
Санкт-Петербург м. «Выборгская», Б. Сампсониевский пр., д. 29а;
тел./факс (812) 703-73-73, 703-73-72; e-mail: sales@piter.com
Воронеж ул. 25 января, д. 4; тел./факс (0732) 39-43-62, 39-61 -70;
e-mail: pitervrn@comch.ru
Екатеринбург ул. 8 Марта, д. 2676, офис 203, 205; тел./факс (343) 225-39-94, 225-40-20;
e-mail: piter-ural@isnet.ru
Нижний Новгород ул. Совхозная, д. 13; тел. (8312) 41 -27-31;
e-mail: piter@infonet.nnov.ru
Новосибирск ул. Немировича-Данченко, д. 104, офис 502;
тел./факс (383) 354-13-09, 211-27-18; e-mail: piter-sib@risp.ru
Ростов-на-Дону ул. Ульяновская, д. 26; тел. (863) 269-91-22, 269-91 -30;
e-mail: jupiter@rost.ru
Самара ул. Молодогвардейская, д. 33, литер А2, ком. 225; тел. (846) 77-89-79;
e-mail: pitvolga@samtel.ru
УКРАИНА
Харьков ул. Суздальские ряды, д. 12, офис 10-11 ; тел./факс (10-38-057) 712-27-05,
751-10-02, (0572) 58-41-45; e-mail: piter@kharkov.piter.com
Киев пр. Московский, д. 6, кор. 1, офис 33; тел./факс (10-38-044) 490-35-68, 490-35-69;
e-mail: office@piter-press.kiev.ua
БЕЛАРУСЬ
Минск ул. Бобруйская, д. 21 , офис 3; тел./факс (10-375-17) 226-19-53;
e-mail: office@minsk.piter.com
Ищем зарубежных партнеров или посредников, имеющих выход на зарубежный рынок.
Телефон для связи: (812) 703-73-73 .
E-mail: grigorjan@piter.com
Издательский дом «Питер» приглашает к сотрудничеству авторов.
Обращайтесь по телефонам: Санкт-Петербург — (812) 703-73-72,
Москва - (095) 974-34-50 .
Заказ книг для вузов и библиотек: (812) 703-73-73.
Специальное предложение - e-mail: kozin@piter.com
----------------------- Page 207-----------------------
пзпАтшаьскпй дом
УВАЖАЕМЫЕ ГОСПОДА!
КНИГИ ИЗДАТЕЛЬСКОГО ДОМА «ПИТЕР»
.COM ВЫ МОЖЕТЕ ПРИОБРЕСТИ
ОПТОМ И В РОЗНИЦУ
У НАШИХ РЕГИОНАЛЬНЫХ ПАРТНЕРОВ.
Башкортостан
Уфа, «Азия», ул. Гоголя, д. 36, офис 5,
тел./факс (3472) 50-39-00,51-85-44.
Красноярск, «Книжный мир»,
E-mail: asiaufa@ufanet.ru
тел./факс (3912) 27-39-71.
E-mail: book-world@public.krasnet.ru
Дальний Восток
Владивосток, «Приморский торговый дом книги» Нижневартовск, «Дом книги»,
тел./факс (4232) 23-82-12.
тел. (3466) 23-27-14, факс 23-59-50.
E-mail: bookbase@mail.primorye.ru
E-mail: book@nvartovsk.wsnet.ru
Хабаровск, «Мире», Новосибирск, «Топ-книга»,
тел. (4212) 30-54-47, факс 22-73-30. тел. (3832) 36-10-26, факс 36-10-27.
E-mail: salebook@bookmirs.khv.ru E-mail: office@top-kniga.ru
http://www.top-kniga.ru
Хабаровск, «Книжный мир»,
тел. (4212) 32-85-51, факс 32-82-50.
Тюмень, «Друг»,
E-mail: postmaster@worldbooks.kht.ru
тел./факс (3452) 21-34-82.
E-mail: drug@tyumen.ru
Европейские регионы России
Архангельск, «Дом книги», Тюмень, «Фолиант»,
тел. (8182) 65-41-34, факс 65-41-34. тел. (3452) 27-36-06, факс 27-36-11.
E-mail: book@atnet.ru E-mail: foliant@tyumen.ru
Калининград, «Вестер», Челябинск, ТД «Эврика», ул. Барбюса, д. 61 ,
тел./факс (0112) 21-56-28,21-62-07. тел./факс (3512) 52-49-23.
E-mail: nshibkova@vester.ru E-mail:evrika@chel.sumet.ru
http://www.vester.ru
Татарстан
Северный Кавказ Казань, «Таис»,
Ессентуки, «Россы», ул. Октябрьская, 424, тел. (8432) 72-34-55, факс 72-27-82.
тел./факс (87934) 6-93-09. E-mail: tais@bancorp.ru
E-mail: rossy@kmw.ru
Урал
Сибирь Екатеринбург, магазин № 14,
Иркутск, «ПродаЛитЪ», ул. Челюскинцев, д . 23,
тел. (3952) 59-13-70, факс 51-30-70. тел./факс (3432) 53-24-90.
E-mail: prodalit@irk.ru E-mail: gvardia@mail.ur.ru
http://www.prodalit.irk.ru
Екатеринбург, «Валео-книга»,
Иркутск, «Антей-книга», ул. Ключевская, д. 5,
тел./факс (3952) 33-42-47. тел./факс (3432) 42-56-00.
E-mail: antey@irk.ru E-mail: valeo@etel.ru