Одно из наиболее распространенный применений аплетов связано с рисованием простых или анимированных растровых изображений. На серверах Web изображения обычно хранятся в форматах GIF или JPEG. Оба эти формата обеспечивают сжатие изображения, что весьма актуально из-за невысокой скорости передачи данных в сети Internet.
Рисование растровых изображений в приложениях для операционной системы Microsoft Windows, составленных на языке программирования С - не простая задача. В классическом программном интерфейсе этой операционной системы отсутствуют функции, с помощью которых можно было бы непосредственно рисовать содержимое файлов с растровыми изображениями.
Программист был вынужден работать с заголовками таких файлов, выделять таблицу цветов и биты изображений, создавать и реализовывать палитру, заниматься восстановлением сжатых данных и так далее. Мы описали этот непростой процесс для файлов формата BMP в 14 томе “Библиотеки системного программиста”, который называется “Графический интерфейс GDI в Microsoft Windows”.
Разработчик приложений Java находится в намного лучшем положении, так как библиотеки классов Java содержат простые в использовании и мощные средства, предназначенные для работы с растровыми изображениями. В этой главе мы научим вас рисовать в окне аплета содержимое файлов GIF и JPEG, выполняя при необходимости масштабирование, а также создавать на базе таких файлов анимационные изображения, показывая по очереди отдельные кадры небольшого видеофильма.
Загрузка растрового изображения из файла выполняется очень просто - с помощью метода getImage, определенного в классе Applet:
public Image getImage(URL url); public Image getImage(URL url, String name);
Первый вариант метода предполагает использование только одного параметра - адреса URL файла графического изображения. Второй позволяет дополнительно указать относительное расположение файла изображения относительно адреса URL, например:
Image img; img = getImage("http://www.glasnet.ru/~frolov/pic", "cd.GIF");
Если аплет желает загрузить изображение, расположенное в том же каталоге, что и он сам, это можно сделать следующим образом:
img = getImage(getCodeBase(), "cd.GIF");
Метод getCodeBase, определенный в классе Applet, возвращает адрес URL аплета. Вместо него можно использовать метод getDocumentBase, который также определен в классе Applet и возвращает адрес URL документа HTML, содержащего аплет:
img = getImage(getDocumentBase(), "cd.GIF");
В любом случае метод getImage создает объект класса Image.
Заметим, что на самом деле метод getImage вовсе не загружает изображение через сеть, как это можно было бы подумать. Он только создает объект класса Image. Реальная загрузка файла растрового изображения будет выполняться методом рисования drawImage, который определен в классе Graphics:
public abstract boolean drawImage(Image img, int x, int y, ImageObserver observer); public abstract boolean drawImage(Image img, int x, int y, Color bgcolor, ImageObserver observer); public abstract boolean drawImage(Image img, int x, int y, int width, int height, ImageObserver observer); public abstract boolean drawImage(Image img, int x, int y, int width, int height, Color bgcolor, ImageObserver observer);
Как видите, существует четыре варианта этого метода.
В качестве первого параметра любому варианту метода передается ссылка на объект класса Image, полученный ранее с помощью метода getImage.
Параметры x и y задают координаты верхнего левого угла прямоугольной области, внутри которой будет нарисовано изображение. Эти параметры также задаются для любого варианта метода drawImage.
Параметр bgcolor задает цвет фона, на котором будет нарисовано изображение. Как мы говорили в 29 томе “Библиотеки системного программиста”, изображения GIF могут быть прозрачными. В этом случае цвет фона может иметь большое значение.
Если для рисования выбраны варианты метода drawImage с параметрами width и height, изображение будет нарисовано с масштабированием. При этом указанные параметры будут определять, соответственно, ширину и высоту изображения.
Параметр observer представляет собой ссылку на объект класса ImageObserver, который получит извещение при загрузке изображения. Обычно в качестве такого объекта используется сам аплет, поэтому данный параметр указывается как this.
Вот два примера использования метода drawImage:
g.drawImage(FloppyDiskImg, 25, 3, this); g.drawImage(FloppyDiskImg, 25, 42, 200, 200, this);
В первой строке изображение FloppyDiskImg рисуется в точке с координатами (25, 3) без масштабирования, во второй - в точке с координатами (25, 42), причем высота и ширина нарисованного изображения будет равна 200 пикселам.
Метод drawImage запускает процесс загрузки и рисования изображения, а затем, не дожидаясь его завершения, возвращает управление. Так как загрузка файла изображения по сети может отнять немало времени, она выполняется асинхронно в отдельной задаче.
Как видите, процесс рисования растрового изображения в окне аплета предельно прост - вам достаточно загрузить изображение методом getImage и затем нарисовать его методом drawImage.
Но не забывайте, что метод getImage в действительности только создает объект класса Image, но не загружает его. Давайте посмотрим на класс Image.
В этом классе имеется единственный конструктор без параметров:
public Image();
Вы, однако, скорее всего будете создавать объекты класса Image при помощи метода getImage.
Методы getHeight и getWidth, определенные в классе Image, позволяют определить, соответственно, высоту и ширину изображения:
public abstract int getHeight(ImageObserver observer); public abstract int getWidth(ImageObserver observer);
Так как при вызове этих методов изображение еще может быть не загружено, в качестве параметров методам передается ссылка на объект ImageObserver. Этот объект получит извещение, когда будет доступна высота или ширина изображения.
Метод getGraphics позволяет получить так называемый внеэкранный контекст отображения для рисования изображения не в окне аплета, а в оперативной памяти:
public abstract Graphics getGraphics();
Эта техника используется для того, чтобы вначале подготовить изображение в памяти, а затем за один прием отобразить его на экране.
Еще один метод класса Image, который мы рассмотрим, называется flush:
public abstract void flush();
Он освобождает ресурсы, занятые изображением.
Теперь мы знаем все необходимое, чтобы приступить к рисованию растровых изображений в окне аплета. Приложение ImageDraw, о котором мы сейчас расскажем, рисует в своем окне четыре изображения: два изображения флоппи-диска и два - компакт-диска (рис. 4.1).
Рис. 4.1. Рисование растровых изображений в окне приложения ImageDraw
В верхнем левом углу окна аплета нарисованы исходные изображения. Справа вверху изображение компакт-диска нарисовано растянутым по горизонтали. Нижнюю часть окна аплета занимает пропорционально увеличенный рисунок флоппи-диска.
Исходный текст приложения ImageDraw вы найдете в листинге 4.1.
Листинг 4.1. Файл ImageDraw\ImageDraw.java
// ========================================================= // Рисование растровых изображений // // (C) Фролов А.В, 1997 // // E-mail: frolov@glas.apc.org // WWW: http://www.glasnet.ru/~frolov // или // http://www.dials.ccas.ru/frolov // ========================================================= import java.applet.*; import java.awt.*; public class ImageDraw extends Applet { // Изображение флоппи-диска Image FloppyDiskImg; // Изображение компакт-диска Image CDDiskImg; // ------------------------------------------------------- // getAppletInfo // Метод, возвращающей строку информации об аплете // ------------------------------------------------------- public String getAppletInfo() { return "Name: ImageDraw\r\n" + "Author: Alexandr Frolov\r\n" + "E-mail: frolov@glas.apc.org" + "WWW: http://www.glasnet.ru/~frolov" + "Created with Microsoft Visual J++ Version 1.0"; } // ------------------------------------------------------- // init // Метод init, получает управление // при инициализации аплета // ------------------------------------------------------- public void init() { // Загружаем изображение флоппи-диска FloppyDiskImg = getImage(getCodeBase(), "disk.GIF"); // Загружаем изображение флоппи-диска CDDiskImg = getImage(getCodeBase(), "cd.GIF"); } // ------------------------------------------------------- // paint // Метод paint, выполняющий рисование в окне аплета // ------------------------------------------------------- public void paint(Graphics g) { // Определяем текущие размеры окна аплета Dimension dimAppWndDimension = size(); // Выбираем в контекст отображения белый цвет g.setColor(Color.white); // Закрашиваем внутреннюю область окна аплета g.fillRect(0, 0, dimAppWndDimension.width - 1, dimAppWndDimension.height - 1); // Выбираем в контекст отображения черный цвет g.setColor(Color.black); // Рисуем рамку вокруг окна аплета g.drawRect(0, 0, dimAppWndDimension.width - 1, dimAppWndDimension.height - 1); // Рисуем увеличенное изображение флоппи-диска g.drawImage(FloppyDiskImg, 25, 42, 200, 200, this); // Рисуем нормальное изображение флоппи-диска g.drawImage(FloppyDiskImg, 25, 3, this); // Рисуем нормальное изображение компакт-диска g.drawImage(CDDiskImg , 70, 3, this); // Рисуем вытянутое изображение компакт-диска g.drawImage(CDDiskImg , 115, 3, 40, 25, this); } }
Листинг 4.2 содержит исходный текст документа HTML, созданный для нашего аплета автоматически системой Microsoft Visual J++.
Листинг 4.2. Файл ImageDraw\ImageDraw.html
<html> <head> <title>ImageDraw</title> </head> <body> <hr> <applet code=ImageDraw.class id=ImageDraw width=320 height=250 > </applet> <hr> <a href="ImageDraw.java">The source.</a> </body> </html>
В главном классе нашего аплета определено два поля и несколько методов. Рассмотрим эти поля и самые важные методы.
В классе ImageDraw определено два поля:
Image FloppyDiskImg; Image CDDiskImg;
В первом из них хранится ссылка на изображение флоппи-диска, во втором - ссылка на изображение компакт-диска.
Метод init создает два объекта класса Image для файлов disk.GIF и cd.GIF:
FloppyDiskImg = getImage(getCodeBase(), "disk.GIF"); CDDiskImg = getImage(getCodeBase(), "cd.GIF");
В качестве первого параметра методу getImage передается адрес URL аплета, полученный при помощи метода getCodeBase. При этом предполагается, что файлы disk.GIF и cd.GIF находятся в том же каталоге, что и аплет.
После раскрашивания окна аплета в белый цвет и рисования вокруг окна черной рамки метод paint вызывает четыре раза метод drawImage, рисуя изображения флоппи-диска и компакт-диска:
g.drawImage(FloppyDiskImg, 25, 42, 200, 200, this); g.drawImage(FloppyDiskImg, 25, 3, this); g.drawImage(CDDiskImg , 70, 3, this); g.drawImage(CDDiskImg , 115, 3, 40, 25, this);
В первый раз флоппи-диск рисуется с масштабированием, во второй раз - в исходном виде. Компакт-диск вначале рисуется в исходном виде, а затем - растянутым по горизонтали.
Как мы уже говорили, загрузка изображений из сети Internet - длительный процесс, который в среднем идет со скоростью 1 Кбайт в секунду. Поэтому изображения загружаются навигатором в отдельной задаче. При этом метод getImage только создает объект класса Image, а метод drawImage инициирует загрузку изображения и рисует его. Причем если файл изображения имеет большую длину, он будет появляться в окне аплета постепенно по мере загрузки.
Однако в некоторых случаях было бы удобно вначале загрузить изображение полностью, а лишь затем выполнять рисование, чтобы изображение появилось на экране сразу. Кроме того, аплет может рисовать сразу несколько изображений в разных местах своего окна или показывать их по очереди в одном и том же месте для достижения эффекта анимации.
Есть ли способ определить, когда изображение будет загружено полностью?
Есть, и причем целых два. Один из них связан с использованием класса MediaTracker, специально предназначенного для этой цели и достаточно удобного в использовании, другой основан на переопределении одного из методов интерфейса ImageObserver.
Для того чтобы выполнить ожидание загрузки нескольких изображений, проще воспользоваться классом MediaTracker, а не интерфейсом ImageObserver.
Как это сделать?
Обычно метод init аплета создает объект класса MediaTracker с помощью конструктора и добавляет в него все изображения, загрузки которых необходимо дождаться.
Объект класса MediaTracker создается следующим образом:
MediaTracker mt; mt = new MediaTracker(this);
Конструктору класса MediaTracker передается ссылка на компонент, для которого необходимо отслеживать загрузку изображений. В данном случае это наш аплет, поэтому мы передаем конструктору значение this.
Далее метод init должен создать все необходимые объекты класса Image и добавить их в объект MediaTracker методом addImage. Ниже мы показали фрагмент кода, в котором выполняется добавление трех изображений:
Image img1; Image img2; Image img3; img1 = getImage(getCodeBase(), "pic1.GIF"); img2 = getImage(getCodeBase(), "pic2.GIF"); img3 = getImage(getCodeBase(), "pic3.GIF"); mt.addImage(img1 , 0); mt.addImage(img2 , 0); mt.addImage(img3 , 0);
В качестве первого параметра методу addImage передается ссылка на изображение, загрузку которого необходимо отслеживать, а в качестве второго - идентификатор, который можно будет использовать в процессе отслеживания. Если все, что вам нужно, это дождаться окончания загрузки изображений, то для второго параметра вы можете указать нулевое значение.
Для того чтобы убедиться, что все изображения загружены, вы можете воспользоваться методом waitForAll. Этот метод инициирует загрузку изображений, а также задержит выполнение вызвавшей его задачи до момента полной загрузки всех изображений, добавленных в объект класса MediaTracker:
try { mt.waitForAll(); } catch (InterruptedException ex) { }
Обратите внимание, что метод waitForAll может создавать исключение InterruptedException. Это исключение возникает, если по какой-либо причине процесс ожидания прерывается.
Чаще всего рисование выполняется в отдельной задаче, поэтому метод waitForAll должен вызываться в начале соответствующего метода run. Ниже мы привели исходные тексты приложения ImageDrawWait, в котором такое ожидание выполняется в методе paint, что приводит, однако, к блокировке работы аплета до момента загрузки всех изображений. В данном случае это не критично, так как кроме рисования изображений наш аплет ничего не делает, однако более предпочтительным является выполнение длительных процессов в отдельной задаче.
Какие другие полезные методы, кроме методов addImage и waitForAll есть в классе MediaTracker?
public boolean waitForAll(long ms);
Метод waitForAll с параметром ms позволяет выполнять ожидание в течение заданного времени. Время ожидания задается в миллисекундах. При этом если за указанное время все изображения были успешно загружены, метод waitForAll возвращает значение true, если нет - false.
Вариант метода checkAll с параметром load позволяет проверить, завершилась ли загрузка отслеживаемых изображений:
public boolean checkAll(boolean load);
Если значение параметра load равно true, метод инициирует загрузку изображений.
Если при добавлении изображений методом addImage вы использовали второй параметр этого метода для присваивания разным группам изображений различные идентификаторы, то с помощью метода checkID можно дождаться завершения загрузки отдельной группы изображений:
public boolean checkID(int id);
Есть также вариант этого метода, позволяющий проверить загрузку группы изображений с заданным идентификатором:
public boolean checkID(int id, boolean load);
Метод waitForID с параметрами id и ms позволяет выполнять ожидание загрузки группы изображений с заданным идентификатором в течении указанного периода времени:
public boolean waitForID(int id, long ms);
Класс MediaTracker предоставляет также возможность прослеживать сам процесс загрузки всех добавленных в него изображений или отдельных групп изображений с помощью методов statusAll и statusID:
public int statusAll(boolean load); public int statusID(int id, boolean load);
В зависимости от значения параметра load эти методы могут инициировать загрузку изображений. Если параметр равен true, загрузка изображений инициируется, если false - выполняется только проверка текущего состояния загрузки.
Методы statusAll и statusID возвращают значение, составленное из отдельных битов состояния при помощи логической операции ИЛИ. Ниже мы перечислили эти биты состояния и привели их краткое описание.
Биты состояния |
Описание |
MediaTracker.LOADING |
Один или несколько отслеживаемых файлов продолжают загружаться |
MediaTracker.ABORTED |
Загрузка одного или нескольких файлов была прервана |
MediatTracker.ERRORED |
При загрузке одного или нескольких файлов произошла ошибка |
MediaTracker.COMPLETE |
Загрузка всех отслеживаемых файлов произошла полностью и успешно |
Еще четыре метода, определенных в классе MediaTracker, связаны с обработкой ошибок:
public boolean isErrorAny(); public boolean isErrorID(int id); public Object[] getErrorsAny(); public Object[] getErrorsID(int id);
Методы isErrorAny и isErrorID позволяют проверить, возникла ли ошибка при загрузке, соответственно, любого из отслеживаемых изображений или изображений из заданной группы. Если ошибка произошла, возвращается значение true, если нет - значение false.
Методы getErrorsAny и getErrorsID возвращают массив объектов, при ожидании загрузки которых произошла ошибка. Первый из этих методов возвращает массив для всех отслеживаемых объектов, второй - только объектов из заданной группы.
Второй способ ожидания завершения процесса загрузки изображений связан с интерфейсом ImageObserver. Определение этого интерфейса не занимает много места, поэтому мы приведем его полностью:
public interface java.awt.image.ImageObserver { // ------------------------------------------------------- // Биты флагов для параметра infoflags метода imageUpdate // ------------------------------------------------------- public final static int ABORT; public final static int ALLBITS; public final static int ERROR; public final static int FRAMEBITS; public final static int HEIGHT; public final static int PROPERTIES; public final static int SOMEBITS; public final static int WIDTH; // ------------------------------------------------------- // Метод imageUpdate // ------------------------------------------------------- public abstract boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height); }
Как видите, в интерфейсе ImageObserver определен единственный метод imageUpdate и набор битовых флагов для этого метода.
Класс Component, от которого происходит класс Applet, реализует интерфейс ImageObserver:
public abstract class java.awt.Component extends java.lang.Object implements java.awt.image.ImageObserver { . . . }
Этот интерфейс используется для отслеживания процесса загрузки и перерисовки изображений и других компонент, расположенных внутри компонента. В частности, он используется для отслеживания загрузки и рисования растровых изображений в окне аплета, чем мы и воспользуемся.
В процессе загрузки вызывается метод imageUpdate, поэтому чтобы отслеживать загрузку изображений, наш аплет должен переопределить этот метод.
Процедура ожидания загрузки изображений достаточно проста.
Прежде всего, аплет должен передать в последнем параметре методу drawImage ссылку на интерфейс ImageObserver, который будет применяться для отслеживания процесса загрузки:
g.drawImage(Img, x, y, width, height, this);
Здесь в качестве ссылки на интерфейс ImageObserver мы передали значение this. При этом будет применен интерфейс нашего аплета. Соответственно, нам нужно определить в классе аплета метод imageUpdate, который будет вызываться в процессе загрузки изображений.
Ниже мы привели возможный вариант реализации этого метода, который позже будет описан в деталях:
public boolean imageUpdate(Image img, int flags, int x, int y, int w, int h) { // Проверяем, все ли биты изображения загружены fAllLoaded = ((flags & ALLBITS) != 0); // Если все, перерисовываем окно if(fAllLoaded) repaint(); // Если все биты загружены, дальнейшие вызовы // метода imageUpdate не нужны return !fAllLoaded; }
Через первый параметр img методу imageUpdate передается ссылка на изображение, загрузка которого отслеживается.
Параметр flags отражает состояние процесса загрузки.
Через остальные параметры x, y, w и h передаются, соответственно, координаты и размеры изображения.
Основное, что должен делать метод imageUpdate для отслеживания процесса загрузки - это проверять флаги flags, дожидаясь установки нужных флагов.
Флаги определены следующим образом:
public final static int WIDTH; public final static int HEIGHT = 2; public final static int PROPERTIES = 4; public final static int SOMEBITS = 8; public final static int FRAMEBITS = 16; public final static int ALLBITS = 32; public final static int ERROR = 64; public final static int ABORT = 128;
Ниже мы привели краткое описание перечисленных выше флагов.
Флаг |
Описание |
WIDTH |
Изображение загружено настолько, что стала доступна его ширина. Значение ширины изображения можно получить из параметра w метода imageUpdate |
HEIGHT |
Аналогично предыдущему, но для высоты изображения. Высоту изображения можно получить из параметра h метода imageUpdate |
PROPERTIES |
Стали доступны свойства изображения, которые можно получить методом getProperty класса Image. В нашей книге мы опустили описание этого метода |
SOMEBITS |
Стали доступны биты изображения для рисования в масштабе. Через параметры x, y, h и w передаются координаты и размеры прямоугольной области, которая ограничивает загруженную часть изображения |
FRAMEBITS |
Загружен очередной фрейм изображения, состоящего из нескольких фреймов. Параметры x, y, h и w следует игнорировать |
ALLBITS |
Изображение загружено полностью. Параметры x, y, h и w следует игнорировать |
ERROR |
При загрузке произошла ошибка |
ABORT |
Загрузка изображения была прервана или отменена |
Анализируя состояние флагов, метод imageUpdate может следить за ходом загрузки изображений, отображая, например, процент завершения процесса загрузки или выполняя какие-либо другие действия.
Если вам нужно только дождаться завершения процесса загрузки, достаточно использовать флаг ALLBITS. Для проверки ошибок воспользуйтесь флагами ERROR и ABORT.
В приложении ImageDrawWait мы демонстрируем использование класса MediaTracker для ожидания процесса завершения загрузки изображений, показанных на рис. 4.1. Эти изображения рисуются приложением ImageDrawWait не на белом фоне, а на фоне другого изображения.
Файл изображение фона имеет значительный размер, поэтому без применения техники ожидания завершения загрузки он будет появляться в окне по частям (именно так работает метод imageUpdate, определенный в классе Applet), а затем будут нарисованы остальные изображения. Наше приложение сначала дожидается завершения загрузки всех изображений, а затем рисует их, поэтому все изображения появятся практически одновременно и целиком.
В процессе загрузки окно приложения ImageDrawWait отображает сообщение о ходе загрузки (рис. 4.2).
Рис. 4.2. Сообщение о ходе процесса загрузки изображений
После загрузки в окне аплета рисуется изображение фона
Исходные тексты приложения ImageDrawWait вы найдете в листинге 4.3.
Листинг 4.3. Файл ImageDrawWait\ImageDrawWait.java
// ========================================================= // Рисование растровых изображений с ожиданием их загрузки // // (C) Фролов А.В, 1997 // // E-mail: frolov@glas.apc.org // WWW: http://www.glasnet.ru/~frolov // или // http://www.dials.ccas.ru/frolov // ========================================================= import java.applet.*; import java.awt.*; public class ImageDrawWait extends Applet { // Фоновое изображение Image BkgImg; // Изображение флоппи-диска Image FloppyDiskImg; // Изображение компакт-диска Image CDDiskImg; // Ссылка на MediaTracker MediaTracker mt; // ------------------------------------------------------- // getAppletInfo // Метод, возвращающей строку информации об аплете // ------------------------------------------------------- public String getAppletInfo() { return "Name: ImageDrawWait\r\n" + "Author: Alexandr Frolov\r\n" + "E-mail: frolov@glas.apc.org" + "WWW: http://www.glasnet.ru/~frolov" + "Created with Microsoft Visual J++ Version 1.0"; } // ------------------------------------------------------- // init // Метод init, получает управление // при инициализации аплета // ------------------------------------------------------- public void init() { // Создаем объект класса MediaTracker mt = new MediaTracker(this); // Загружаем фоновое изображение BkgImg = getImage(getCodeBase(), "bkg.GIF"); // Добавляем его в список объекта MediaTracker mt.addImage(BkgImg , 0); // Загружаем изображение флоппи-диска FloppyDiskImg = getImage(getCodeBase(), "disk.GIF"); // Добавляем его в список объекта MediaTracker mt.addImage(FloppyDiskImg, 0); // Загружаем изображение флоппи-диска CDDiskImg = getImage(getCodeBase(), "cd.GIF"); // Добавляем его в список объекта MediaTracker mt.addImage(CDDiskImg, 0); } // ------------------------------------------------------- // paint // Метод paint, выполняющий рисование в окне аплета // ------------------------------------------------------- public void paint(Graphics g) { // Определяем текущие размеры окна аплета Dimension dimAppWndDimension = size(); // Выбираем в контекст отображения белый цвет g.setColor(Color.white); // Закрашиваем внутреннюю область окна аплета g.fillRect(0, 0, dimAppWndDimension.width - 1, dimAppWndDimension.height - 1); // Выбираем в контекст отображения черный цвет g.setColor(Color.black); // Рисуем рамку вокруг окна аплета g.drawRect(0, 0, dimAppWndDimension.width - 1, dimAppWndDimension.height - 1); // Рисуем сообщение о начале загрузки g.drawString("Подождите, идет загрузка...", 20, dimAppWndDimension.height / 2); // Ждем, пока все изображения не будут загружены try { mt.waitForAll(); } catch (InterruptedException ex) { } // Рисуем изображение фона g.drawImage(BkgImg, 1, 1, dimAppWndDimension.width - 2, dimAppWndDimension.height - 2, this); // Рисуем увеличенное изображение флоппи-диска g.drawImage(FloppyDiskImg, 25, 42, 200, 200, this); // Рисуем нормальное изображение флоппи-диска g.drawImage(FloppyDiskImg, 25, 3, this); // Рисуем нормальное изображение компакт-диска g.drawImage(CDDiskImg , 70, 3, this); // Рисуем вытянутое изображение компакт-диска g.drawImage(CDDiskImg , 115, 3, 40, 25, this); } }
В листинге 4.4 приведен исходный текст документа HTML, созданный для нашего аплета.
Листинг 4.4. Файл ImageDrawWait\ImageDrawWait.html
<html> <head> <title>ImageDrawWait</title> </head> <body> <hr> <applet code=ImageDrawWait.class id=ImageDrawWait width=320 height=250 > </applet> <hr> <a href="ImageDrawWait.java">The source.</a> </body> </html>
Опишем наиболее важные методы приложения ImageDrawWait.
Прежде всего метод init создает объект класса MediaTracker, который будет использоваться для отслеживания процесса загрузки изображений:
mt = new MediaTracker(this);
Далее метод init последовательно создает три объекта класса Image (соответственно, для изображений фона, для флоппи-диска и для компакт-диска), а затем добавляет их в объект MediaTracker с помощью метода addImage:
BkgImg = getImage(getCodeBase(), "bkg.GIF"); mt.addImage(BkgImg , 0); FloppyDiskImg = getImage(getCodeBase(), "disk.GIF"); mt.addImage(FloppyDiskImg, 0); CDDiskImg = getImage(getCodeBase(), "cd.GIF"); mt.addImage(CDDiskImg, 0);
Метод paint прежде всего раскрашивает окно аплета в белый цвет и обводит его черной рамкой. Затем на подготовленной таким образом поверхности он пишет сообщение о начале процесса загрузки изображений:
g.drawString("Подождите, идет загрузка...", 20, dimAppWndDimension.height / 2);
Далее ожидается загрузка всех изображений, для чего вызывается метод waitForAll из класса MediaTracker:
try { mt.waitForAll(); } catch (InterruptedException ex) { }
Когда все изображения будут загружены, следует серия вызовов метода drawImage, с помощью которых рисуется изображение фона, два изображения флоппи-диска и два изображения компакт-диска:
g.drawImage(BkgImg, 1, 1, dimAppWndDimension.width - 2, dimAppWndDimension.height - 2, this); g.drawImage(FloppyDiskImg, 25, 42, 200, 200, this); g.drawImage(FloppyDiskImg, 25, 3, this); g.drawImage(CDDiskImg , 70, 3, this); g.drawImage(CDDiskImg , 115, 3, 40, 25, this);
Приложение DrawImageObserver рисует в своем окне изображение фона, такое же, как и в предыдущем приложении. При этом для ожидания процесса загрузки изображения перед рисованием фона мы используем интерфейс ImageObserver.
Если бы изображение фона, имеющее значительный размер, рисовалось без ожидания его загрузки, оно появлялось бы в окне аплета по частям. Наше приложение рисует его сразу, так как дожидается полной загрузки.
Главный файл исходных текстов приложения DrawImageObserver приведен в листинге 4.5.
Листинг 4.5. Файл DrawImageObserver\DrawImageObserver.java
// ========================================================= // Рисование растровых изображений с ожиданием их загрузки // Для ожидания применяется интерфейс ImageObserver // // (C) Фролов А.В, 1997 // // E-mail: frolov@glas.apc.org // WWW: http://www.glasnet.ru/~frolov // или // http://www.dials.ccas.ru/frolov // ========================================================= import java.applet.*; import java.awt.*; public class DrawImageObserver extends Applet { // Фоновое изображение Image BkgImg; boolean fAllLoaded = false; // ------------------------------------------------------- // getAppletInfo // Метод, возвращающей строку информации об аплете // ------------------------------------------------------- public String getAppletInfo() { return "Name: DrawImageObserver\r\n" + "Author: Alexandr Frolov\r\n" + "E-mail: frolov@glas.apc.org" + "WWW: http://www.glasnet.ru/~frolov" + "Created with Microsoft Visual J++ Version 1.0"; } // ------------------------------------------------------- // init // Метод init, получает управление при // инициализации аплета // ------------------------------------------------------- public void init() { // Загружаем фоновое изображение BkgImg = getImage(getCodeBase(), "bkg.GIF"); } // ------------------------------------------------------- // imageUpdate // Вызывается, когда появляется информация об изображении // ------------------------------------------------------- public boolean imageUpdate(Image img, int flags, int x, int y, int w, int h) { // Проверяем, все ли изображение загружено fAllLoaded = ((flags & ALLBITS) != 0); // Если все, перерисовываем окно if(fAllLoaded) repaint(); // Если изображение загружено полностью, дальнейшие // вызовы метода imageUpdate не нужны return !fAllLoaded; } // ------------------------------------------------------- // paint // Метод paint, выполняющий рисование в окне аплета // ------------------------------------------------------- public void paint(Graphics g) { // Определяем текущие размеры окна аплета Dimension dimAppWndDimension = size(); // Выбираем в контекст отображения белый цвет g.setColor(Color.white); // Закрашиваем внутреннюю область окна аплета g.fillRect(0, 0, dimAppWndDimension.width - 1, dimAppWndDimension.height - 1); // Выбираем в контекст отображения черный цвет g.setColor(Color.black); // Рисуем рамку вокруг окна аплета g.drawRect(0, 0, dimAppWndDimension.width - 1, dimAppWndDimension.height - 1); if(fAllLoaded == false) { // Рисуем сообщение о начале загрузки g.drawString("Подождите, идет загрузка...", 20, dimAppWndDimension.height / 2); } // Рисуем изображение фона g.drawImage(BkgImg, 1, 1, dimAppWndDimension.width - 2, dimAppWndDimension.height - 2, this); } }
В листинге 4.6 вы найдете исходный текст документа HTML, созданного автоматически для нашего аплета.
Листинг 4.6. Файл DrawImageObserver\DrawImageObserver.html
<html> <head> <title>DrawImageObserver</title> </head> <body> <hr> <applet code=DrawImageObserver.class id=DrawImageObserver width=320 height=240 > </applet> <hr> <a href="DrawImageObserver.java">The source.</a> </body> </html>
Наше приложение переопределяет метод imageUpdate, в котором отслеживает процесс загрузки фонового изображения.
Опишем основные методы, определенные в приложении DrawImageObserver.
В процессе инициализации аплета метод init создает объект класса Image, соответствующий изображению фона:
public void init() { BkgImg = getImage(getCodeBase(), "bkg.GIF"); }
Как вы уже знаете, при этом реальная загрузка файла изображения не выполняется.
Свою работу метод paint начинает с раскрашивания окна аплета и рисования рамки вокруг окна. Затем метод проверяет флаг fAllLoaded, начальное значение которого равно false:
if(fAllLoaded == false) { g.drawString("Подождите, идет загрузка...", 20, dimAppWndDimension.height / 2); }
Флаг fAllLoaded служит индикатором полной загрузки изображения и устанавливается методом imageUpdate, отслеживающим загрузку. Пока значение этого флага равно false, метод paint отображает в окне аплета сообщение о том, что идет процесс загрузки.
Когда изображение будет полностью загружено, метод imageUpdate устанавливает значение флага fAllLoaded, равное true, а затем принудительно перерисовывает окно аплета, вызывая метод repaint. При этом метод paint рисует в окне аплета полностью загруженное изображение фона:
g.drawImage(BkgImg, 1, 1, dimAppWndDimension.width - 2, dimAppWndDimension.height - 2, this);
Метод imageUpdate периодически вызывается в процессе загрузки изображения, конструируя каждый раз значение флага полной загрузки fAllLoaded следующим образом:
fAllLoaded = ((flags & ALLBITS) != 0);
Когд изображение будет полностью загружено, в параметре flags метода imageUpdate будет установлен флаг ALLBITS, после чего флаг fAllLoaded будет установлен в значение true.
Как только это произойдет, метод imageUpdate вызовет метод repaint, выполнив принудительную перерисовку окна аплета:
if(fAllLoaded) repaint();
При этом метод paint нарисует в окне аплета изображение фона, закрасив им сообщение о ходе процесса загрузки изображения.
Метод imageUpdate должен возвратить значение false или true. Если изображение еще не загружено, возвращается значение true:
return !fAllLoaded;
При этом метод imageUpdate будет вызываться еще раз для отслеживания процесса загрузки.
Когда загрузка будет завершена, метод imageUpdate возвратит значение false, после чего этот метод вызываться больше не будет.
Наиболее динамичные страницы сервера Web содержат анимационные изображения в виде небольших видеофильмов. Как мы рассказывали в 29 томе “Библиотеки системного программиста”, который называется “Сервер Web своими руками”, вы можете подготовить видеофильм как файл AVI или как многосекционный файл GIF.
Файл AVI представляет собой многопоточный файл, содержащий видео и звук. О том, как создавать такие файлы, мы рассказали в 15 томе “Библиотеки системного программиста” с называнием “Мультимедиа для Windows”. Файлы AVI можно создавать при помощи специального видеоадаптера, который способен оцифровывать сигнал с видеокамеры или видеомагнитофона, а также из отдельных изображений, составляющих кадры видеофильма.
Заметим, однако, что озвученный видеофильм в формате AVI продолжительностью в 1 минуту занимает мегабайты дискового пространства. При существующих на сегодняшний день скоростях передачи данных через Internet не имеет никакого смысла размещать на страницах сервера Web такие файлы.
Многосекционные файлы GIF не содержат звуковой информации и состоят обычно из одного-двух десятков кадров. Для каждого такого кадра вы можете задавать время отображения и координаты, где этот кадр будет отображаться. Можно также добиться зацикленного отображения видеофильма, созданного как многосекционный файл GIF.
Аплеты Java предоставляют вам еще одну возможность отображения небольших видеофильмов на страницах сервера Web.
Для реализации этой возможности вы должны подготовить и разместить в одном из каталогов сервера Web файлы отдельных кадров видеофильма в формате GIF или JPEG.
Аплет Java должен загрузить эти изображения, дождавшись окончания процесса загрузки, что можно сделать либо при помощи рассмотренного в этой главе класса MediaTracker либо при помощи интерфейса ImageObserver.
Как только все изображения будут полностью загружены, аплет может начинать их поочередное отображение в цикле. Этот цикл должен выполняться в отдельной задаче.
Так как аплет полностью контролирует отображение кадров фильма, он может реализовывать эффекты, недостижимые при использовании файлов AVI или многосекционных файлов GIF. Например, аплет может накладывать или смешивать кадры различных фильмов, рисовать поверх кадров произвольные изображения или делать надписи, масштабировать отдельные фрагменты кадров или весь кадр и так далее. Здесь все ограничивается главным образом вашей фантазией.
Так как мы уже научились выполнять все необходимые для показа видеофильма операции, перейдем сразу к исходным текстам приложения CDRotation.
Задача отображения видеофильмов в окне Java настолько важна, что Microsoft включил в Visual J++ специальные средства для создания шаблона исходных текстов аплета с анимацией.
Если на третьем шаге системы автоматизированной создания исходных текстов аплетов Java Applet Wizard включить переключатель Yes в поле Would you like your applet to be multi-threaded, а также переключатель Yes в поле Would you like support for animation (рис. 4.3), для вас будут созданы исходные тексты аплета, в окне которого находится изображение земного шара, вращающегося вдоль вертикальной оси.
Рис. 4.3. Включение исходного текста для работы с анимацией в создаваемый аплет
Все, что вам остается сделать, это изменить созданные для вас исходные тексты таким образом, чтобы они соответствовали вашим потребностям. Именно так мы создали исходные тексты приложения CDRotation, в окне которого изображается вращающийся компакт-диск.
Когда будете запускать приложение CDRotation, обратите внимание, что в левом верхнем углу каждого кадра отображается его порядковый номер. Этот номер не нарисован в файлах кадров, а надписывается приложением после рисования очередного кадра. Такое невозможно, если располагать в документе HTML файл AVI или многосекционный файл GIF.
Главный файл исходных текстов приложения CDRotation представлен в листинге 4.7.
Листинг 4.7. Файл CDRotation\CDRotation.java
// ========================================================= // Рисование вращающегося компакт-диска // // (C) Фролов А.В, 1997 // // E-mail: frolov@glas.apc.org // WWW: http://www.glasnet.ru/~frolov // или // http://www.dials.ccas.ru/frolov // ========================================================= import java.applet.*; import java.awt.*; public class CDRotation extends Applet implements Runnable { // Ссылка на задачу рисования // вращающегося компакт-диска Thread m_CDRotation = null; // Контекст отображения для рисования private Graphics m_Graphics; // Массив изображений компакт-диска private Image m_Images[]; // Номер текущего изображения private int m_nCurrImage; // Ширина изображения private int m_nImgWidth = 0; // Высота изображения private int m_nImgHeight = 0; // Флаг загрузки всех изображений private boolean m_fAllLoaded = false; // Общее количество изображений private final int NUM_IMAGES = 11; // ------------------------------------------------------- // getAppletInfo // Метод, возвращающей строку информации об аплете // ------------------------------------------------------- public String getAppletInfo() { return "Name: CDRotation\r\n" + "Author: Alexandr Frolov\r\n" + "E-mail: frolov@glas.apc.org" + "WWW: http://www.glasnet.ru/~frolov" + "Created with Microsoft Visual J++ Version 1.0"; } // ------------------------------------------------------- // displayImage // Рисование текущего изображения, если все изображения // уже загружены // ------------------------------------------------------- private void displayImage(Graphics g) { // Если не все изображения загружены, // ничего не делаем if (!m_fAllLoaded) return; // Рисуем текущее изображение в центре окна аплета g.drawImage(m_Images[m_nCurrImage], (size().width - m_nImgWidth) / 2, (size().height - m_nImgHeight) / 2, null); // Рисуем в вернем левом углу кадра его порядковый номер g.drawString((new Integer(m_nCurrImage)).toString(), (size().width - m_nImgWidth) / 2, ((size().height - m_nImgHeight) / 2) + 10); } // ------------------------------------------------------- // paint // Метод paint, выполняющий рисование в окне аплета // ------------------------------------------------------- public void paint(Graphics g) { // Определяем текущие размеры окна аплета Dimension dimAppWndDimension = size(); // Выбираем в контекст отображения белый цвет g.setColor(Color.white); // Закрашиваем внутреннюю область окна аплета g.fillRect(0, 0, dimAppWndDimension.width - 1, dimAppWndDimension.height - 1); // Выбираем в контекст отображения черный цвет g.setColor(Color.black); // Рисуем рамку вокруг окна аплета g.drawRect(0, 0, dimAppWndDimension.width - 1, dimAppWndDimension.height - 1); // Если все изображения загружены, рисуем // текущее изображение if (m_fAllLoaded) { displayImage(g); } // Если не загружены, рисуем сообщение // о загрузке else g.drawString("Подождите, идет загрузка...", 10, dimAppWndDimension.height / 2); } // ------------------------------------------------------- // start // Метод вызывается при первом отображении окна аплета // ------------------------------------------------------- public void start() { if (m_CDRotation == null) { m_CDRotation = new Thread(this); m_CDRotation.start(); } } // ------------------------------------------------------- // stop // Метод вызывается, когда окно аплета исчезает с экрана // ------------------------------------------------------- public void stop() { if (m_CDRotation != null) { m_CDRotation.stop(); m_CDRotation = null; } } // ------------------------------------------------------- // run // Метод, который работает в рамках отдельной задачи // Он рисует в окне аплета изображения - кадры // клипа "вращающийся компакт-диск" // ------------------------------------------------------- public void run() { // Инициализируем номер текущего изображения m_nCurrImage = 0; // Проверяем, все ли изображения загружены. // Если нет, загружаем их if (!m_fAllLoaded) { // Перерисовываем окно аплета repaint(); // Получаем контекст отображения для окна m_Graphics = getGraphics(); // Создаем массив изображений m_Images = new Image[NUM_IMAGES]; // Создаем объект MediaTracker для контроля // загружки изображений MediaTracker tracker = new MediaTracker(this); // Переменная для хранения имени файла изображения String strImage; // Цикл загрузки изображений for (int i = 0; i < NUM_IMAGES; i++) { // Записываем в строку strImage имя текущего файла // с изображением strImage = "images/cdimg0" + ((i < 10) ? "0" : "") + i + ".GIF"; // Инициируем получение изображения m_Images[i] = getImage(getDocumentBase(), strImage); // Добавляем изображение в объект MediaTracker tracker.addImage(m_Images[i], 0); } // Ожидаем окончание загрузки всех изображений try { tracker.waitForAll(); // Если не было ошибок, устанавливаем флаг // окончания загрузки m_fAllLoaded = !tracker.isErrorAny(); } catch (InterruptedException e) { } // Если при загрузке изображений произошла ошибка, // останавливаем задачу и рисуем сообщение об // ошибке if (!m_fAllLoaded) { stop(); m_Graphics.drawString( "При загрузке изображений произошла ошибка", 10, size().height / 2); return; } // Сохраняем ширину и высоту первого изображения m_nImgWidth = m_Images[0].getWidth(this); m_nImgHeight = m_Images[0].getHeight(this); } // Перерисовываем окно аплета repaint(); // Запускаем цикл рисования изображений while (true) { try { // Рисуем текущее изображение displayImage(m_Graphics); // Увеличиваем номер текущего изображения m_nCurrImage++; // Если достигли максимального номера, // начинаем с самого начала if(m_nCurrImage == NUM_IMAGES) m_nCurrImage = 0; // Выполняем задержку в 30 миллисекунд Thread.sleep(30); } // Если в процессе рисования возникло // исключение, останавливаем задачу catch (InterruptedException e) { stop(); } } } }
Листинг 4.8 содержит исходный текст документа HTML, созданного для аплета CDRotation.
Листинг 4.8. Файл CDRotation\CDRotation.html
<html> <head> <title>CDRotation</title> </head> <body> <hr> <applet code=CDRotation.class id=CDRotation width=320 height=240 > </applet> <hr> <a href="CDRotation.java">The source.</a> </body> </html>
Рассмотрим наиболее важные методы нашего приложения.
В задачу метода start, который получает управление при отображении окна аплета, входит создание и запуск задачи, отображающий кадры видеофильма с изображением вращающегося компакт-диска:
if (m_CDRotation == null) { m_CDRotation = new Thread(this); m_CDRotation.start(); }
Задача создается как объект класса Thread, причем конструктору передается ссылка на главный класс аплета. Поэтому при запуске задачи управление получит метод run, определенный в классе аплета.
Метод stop останавливает работу задачи, когда окно аплета исчезает с экрана:
if(m_CDRotation != null) { m_CDRotation.stop(); m_CDRotation = null; }
Для остановки вызывается метод stop.
Сразу после получения управления, метод paint закрашивает окно аплета белым цветом и рисует вокруг него черную рамку.
Затем метод проверяет содержимое флага m_fAllLoaded. Этот флаг установлен в значение true, когда все кадры видеофильма загружены и сброшен в значение false, когда загрузка кадров еще не завершена. Последняя ситуация возникает всегда при первом вызове метода paint.
Если все изображения загружены, метод paint вызывает метод displayImage, определенный в нашем приложении:
if(m_fAllLoaded) { displayImage(g); }
Этот метод, о котором мы еще расскажем подробнее, отображает в окне аплета текущий кадр видеофильма.
Если же кадры видеофильма еще не загружены, в окне аплета отображается соответствующее сообщение:
else g.drawString("Подождите, идет загрузка...", 10, dimAppWndDimension.height / 2);
Метод run работает в рамках отдельной задачи. Он занимается последовательным рисованием кадров нашего видеофильма.
Прежде всего метод run записывает нулевое значение в поле m_nCurrImage, хранящее номер текущего отображаемого кадра:
m_nCurrImage = 0;
Далее выполняется проверка, загружены ли все кадры видеофильма, для чего анализируется содержимое флага m_fAllLoaded.
Если изображения не загружены (а в самом начале так оно и есть) метод run перерисовывает окно аплета и получает контекст отображения для этого окна. Затем создается массив объектов Image для хранения кадров видеофильма:
m_Images = new Image[NUM_IMAGES];
Метод run создает также объект класса MediaTracker для ожидания загрузки всех кадров видеофильма:
MediaTracker tracker = new MediaTracker(this);
Далее метод run в цикле загружает изображения и добавляет их в объект класса MediaTracker для того чтобы можно было дождаться загрузки всех кадров:
for (int i = 0; i < NUM_IMAGES; i++) { strImage = "images/cdimg0" + ((i < 10) ? "0" : "") + i + ".GIF"; m_Images[i] = getImage(getDocumentBase(), strImage); tracker.addImage(m_Images[i], 0); }
Здесь предполагается, что файлы изображений находятся в каталоге images, который, в свою очередь, размещен там же, где и двоичный файл аплета.
Имена файлов, составляющих отдельные кадры, начинаются с префикса cdimg0, вслед за которым идет номер кадра (00, 01, 02, и так далее), и расширение имени .GIF.
Ожидание загрузки кадров выполняется с помощью метода waitForAll, о котором мы вам уже рассказывали:
try { tracker.waitForAll(); m_fAllLoaded = !tracker.isErrorAny(); } catch (InterruptedException e) { }
После окончания ожидания флаг завершения загрузки устанавливается только в том случае, если метод isErrorAny вернул значение false, то есть если не было никаких ошибок.
Если же произошла ошибка, в окне аплета отображается соответствующее сообщение, после чего работа метода run (и, следовательно, работа созданной для него задачи) заканчивается:
if(!m_fAllLoaded) { stop(); m_Graphics.drawString( "При загрузке изображений произошла ошибка", 10, size().height / 2); return; }
В случае удачной загрузки всех кадров метод run получает ширину и высоту первого кадра видеофильма и сохраняет эти значения в переменных m_nImgWidth и m_nImgHeight:
m_nImgWidth = m_Images[0].getWidth(this); m_nImgHeight = m_Images[0].getHeight(this);
Далее окно аплета перерисовывается:
repaint();
При этом метод paint отображает в окне аплета первый кадр видеофильма.
На следующем этапе работы метода run запускается цикл отображения кадров фильма:
while (true) { try { displayImage(m_Graphics); m_nCurrImage++; if(m_nCurrImage == NUM_IMAGES) m_nCurrImage = 0; Thread.sleep(30); } catch (InterruptedException e) { stop(); } }
В этом бесконечном цикле вызывается метод displayImage, рисующий текущий кадр видеофильма, после чего номер текущего кадра увеличивается на единицу. Если показаны все кадры, номер текущего кадра становится равным нулю, а затем процесс продолжается.
Между отображением кадров выполняется задержка величиной 30 миллисекунд.
Метод displayImage вызывается из двух мест - из метода paint при перерисовке окна аплета и из метода run (периодически).
Если кадры видеофильма не загружены, содержимое флага m_fAllLoaded равно false и метод displayImage просто возвращает управление, ничего не делая:
if(!m_fAllLoaded) return;
Если же загрузка изображений завершена, этот метод рисует в центре окна текущий кадр видеофильма, вызывая для этого знакомый вам метод drawImage:
g.drawImage(m_Images[m_nCurrImage], (size().width - m_nImgWidth) / 2, (size().height - m_nImgHeight) / 2, null);
После того как кадр нарисован, мы надписываем на нем его порядковый номер, вызывая для этого метод drawString:
g.drawString((new Integer(m_nCurrImage)).toString(), (size().width - m_nImgWidth) / 2, ((size().height - m_nImgHeight) / 2) + 10);