2 июля 2014

Zetes: Java с мультиплатформенным GUI, но без Oracle JVM

JavaC
Tutorial
image

Аннотация


В статье описывается фреймворк, позволяющий создавать графические кроссплатформенные приложения, написанные на языке Java, но при этом абсолютно не зависящие ни от Oracle JRE, ни от OpenJDK. Основная идеология фреймворка — по возможности снять с разработчика заботы об обеспечении «родного» look and feel для приложения под каждой операционной системой.

Фактически, на выходе вы получите исполняемый файл, опирающийся только на системные API, на котором нигде не будет клейма «написано на Java».

Все компоненты фреймворка имеют либеральные лицензии (BSD либо Apache), что позволяет использовать их в любых (в том числе, коммерческих) разработках.

Фреймворк находится в стадии публичной alpha-версии, что означает некоторую его работоспособность, но непроверенность. Использование поощряется (я постараюсь прислушаться к жалобам на проблемы и помогу их решить), но работоспособность не гарантируется.

Готовая, собранная версия приложений, демонстрирующих работу фреймворка под всеми платформами, находится здесь.

Всех интересующихся подробностями милости прошу под кат.


1. Введение


Здесь будет небольшое лирическое вступление. Этот раздел можно спокойно пропустить, если вам неинтересно, почему этот проект зародился и как он развивался. Техническая сторона вопроса начинается с раздела 2.

1.1. О названии


Zetes (в русском переводе — Зет) — один из «незаметных» олимпийских богов — братьев Бореадов, участвовавших в походе аргонавтов. Довольно харизматичный персонаж с крыльями на ногах и голове.

1.2. Первые шаги


Чуть более года назад я написал свою первую статью на Хабр. Материал был для меня самого новым, но сразу показался мне очень интересным. Речь шла об альтернативной имплементации виртуальной машины Java, называемой Avian. Этот проект позволяет встраивать виртуальную машину непосредственно в исполняемый файл и распространяется под лицензией BSD, что дает возможность пользователю безнаказанно использовать полученное решение в любых целях. Наконец-то я как Java разработчик, почувствовал полную свободу. И не преминул этой радостью поделиться, за что и получил благодарность и инвайт. В комментариях нашлись люди, которые мечтательно высказались в том смысле, что неплохо бы еще какой-нибудь GUI кроссплатформенный принести сюда и, признаться, меня самого подобные мысли тоже посещали, равно как и разработчиков Avian…

Мне захотелось максимально упростить процесс рзработки GUI-приложений без потери в качестве результата. Я начал думать. Передо мной были примеры проектов, написанных на Java и запускавшихся на Windows, Linux и OS X. Например, Eclipse. Или SmartGIT. Все они имели неплохой (хоть и не идеальный) UI, а также все использовали графическую библиотеку SWT. Библиотека эта вполне поддерживалась Avian-ом. Осталось только собрать это всё вместе, слегка «подточить» некоторые детали, — и можно использовать для небольших, но амбициозных проектов.

Увы, в процессе работы появились неизбежные трудности. Большая часть из них была связана с несовершенством стандартной библиотеки классов, входящей в состав Avian. Проект, разрабатываемый небольшой группой людей, не может пока похвастаться полной совместимостью. Порой там недостает самых основных вещей… Например, сокеты добавлял лично я. Попытавшись завести с «родным» classpath некоторые более-менее сложные программы и фреймворки и потерпев неудачу, я осознал, что мало кто из Java-разработчиков захочет углубляться в допиливание классов стандартной библиотеки, и пыл мой изрядно поугас.

Прошло довольно много времени, прежде чем я вернулся к этой теме. Решение проблемы должно было состоять в том, чтобы найти какую-то альтернативную библиотеку классов Java Core. К сожалению, единственная известная мне на тот момент библиотека — GNU Classpath. Но ее лицензия меня не устраивала. Возможно, мои изыскания покажутся разбирающимся людям результатом наивности. Но, увы, наивность действительно имела место, потому что мне понадобился еще примерно месяц (не активных, но периодических поисков), чтобы набрести на ныне почивший проект Apache Harmony. Проект этот содержал в себе все необходимые классы и должен был работать под всеми интересующими меня ОСями, но, увы, он был мёртв. Добавить его к Avian представлялось очень трудоёмкой задачей для одного человека. Я обратился к разработчикам Avian с идеей, они указали мне на то, что добавлять классы оттуда непосредственно в Avian не будут, так как лицензия BSD более раскрепощенная, чем Apache. Однако существует возможность собрать Avian с необходимыми компонентами, взятыми из Android Classpath. Это знание ударило, как гром среди ясного неба. Ну, конечно же! Android содержит в себе имплементацию Java Classpath, не только свободную от лицензирования Oraсle, но еще и выстраданную в судебных тяжбах между компаниями (хотя в свете последних событий, думаю, нам всем следует быть осторожнее с подобными утверждениями). И лицензия Apache позволяет пользоваться этой библиотекой. К этому стоит еще добавить, что Android Classpath — потомок того самого Apache Harmony. Так что вариантов не осталось.

Находясь в состоянии эйфории, я выкачал всё необходимое и собрал дома на машине Apple. После небольших допиливаний всё заработало. Исполняемый файл существенно «потолстел», а сборка заняла 15 минут, но в результате я получил работающую полноценную автономную Джаву. В тот момент я уже мысленно начал писать эту статью, но радость рассосалась, как только я попробовал всё это собрать под Windows. Увы…

1.3. Допиливание Android Classpath


Разумеется, Android построен на базе Linux. И разумеется, вся нативная часть Classpath упирается корнями в Posix. Передо мной встала серьёзная задача — портировать Android Classpath на Win32. Это было месяца 3 назад. Так как вся сборка основана на MinGW, некоторые основные Posix-функции присутствовали (те из них, что в точности повторены в Windows). Но, увы, часть из них работает несколько иначе (например, WinSock, хоть и стремится совпадать с BSD Sockets, но, увы, местами довольно сильно от них отличается, особенно в части, связанной со вводом-выводом). Другая часть и вовсе отсутствует в Windows в том виде, в котором необходима. К примеру, безымянные трубы (unnamed pipes) в Windows реализованы очень ограниченно и не позволяют использовать неблокирующий ввод/вывод, который необходим некоторым Java API функциям.

К счастью, мне в моих изысканиях помог товарищ JustAMan, без него бы я, в лучшем случае, всё еще блуждал по Posix-докам. Кое-что мы написали сами и протестировали с помощью простых приложений, а также небольшого Simple Framework-а. Другое мы позаимствовали из открытых источников, старательно обходя GPL-ный код. Слава богу, не мы одни в мире занимаемся подобными вещами…

Целью нашей, разумеется, не было воссоздать эмуляцию Posix API (с этим замечательно справляется Cygwin, который, правда, тоже имеет не самую либеральную лицензию). Мы просто хотели, чтобы заработал LUNI (аббревиатура из Android. Означает Lang Utils Net IO — четыре основных пакета, составляющих java core).

В данный момент эта работа в основной своей части завершена и находится в стадии багфикса. Есть еще некоторые заморочки с путями, но это всё уже мелочи.

То, что получилось у нас в результате, мы и предоставляем вашему вниманию.

2. Зачем это всё нужно


Вопрос, которого я, наверное, боюсь больше всего. Честный ответ такой: я занимаюсь этим потому, что мне это интересно. И надеюсь, что это пригодится кому-то еще. Но, несмотря на то, что обосновать полезность библиотеки могут только ее пользователи (которых пока что совсем мало), я попробую, хотя бы, объяснить ее основную идеологию.

Задача Zetes в том, чтобы продолжить Java в сторону Desktop-разработки. Несмотря на то, что сам по себе язык является кроссплатформенным (и даже, в некотором роде, переносимым в скомпилированном виде), при разработке мультисистемных приложений на нем, если разработчик хочет, чтобы его приложение выглядело хоть сколько-нибудь «родным», он вынужден учитывать «культурные различия». Примеров тьма. В Windows (и в Linux+X+GTK) приложение обязано создать хотя бы одно окно, чтобы изобразить строку меню. В OS X меню отделено от окон и прекрасно существует в их отсутствие. В Windows пункт Exit обычно помещают в File, а пункт About — в Help. В OS X оба они расположены в системном меню. Но это — мелочи. Есть разница и серьёзнее. Например, в OS X приложение с GUI не может быть запущено в двух экземплярах — каждая программа обязана поддерживать либо Multi Document Interface, либо Single Document Interface. То есть при открытии нового документа оболочка просто передает его имя уже запущенному приложению. Подобное поведение удобно (если реализовано), но в Windows и Linux его необходимо имитировать с помощью других системных механизмов.

Эти (и, в будущем, все подобные расхождения) и должен брать на себя Zetes. На очереди, например, разработка универсального кроссплатформенного API, позволяющего приложению хранить свои настройки и в системном реестре Windows, и в соответствующих папках Library в OS X, и в GConf на Linux.

Иными словами, задача фреймворка — привести API популярных систем «к общему знаменателю», дополнив полезные функции одних полезными функциями других.

И, предвосхищая вопросы, которые неминуемо возникнут, скажу: нет, эта библиотека не будет распространяться на мобильные ОС, во всяком случае, пока. Они слишком сильно отличаются от десктопных как идеологией, так и подходами к программированию.

3. Так что же такое Zetes?


Фреймворк позволяет взять созданное java-приложение и собрать/упаковать его в нативный исполняемый файл, зависящий только от системных библиотек (в случае использования SWT, к приложению прилагается лишь несколько SWT-библиотек). Вам понадобятся только утилиты сборки. Единственное, что придется написать помимо, собственно, программы, — простейший makefile, который определяет такие вещи, как имя итогового исполняемого файла, а также название java-класса, содержащего точку входа (функцию main).

При более продвинутом использовании сборочная система позволит вам добавлять исходные файлы на C++, а также подключать внешние библиотеки — как динамические, так и статические. Так что довольными в итоге должны, по идее, оказаться как разработчики на «чистой» Java, даже не знающие, что такое JNI, так и «матёрые волки», использующие JNI повсеместно.

4. Из чего состоит Zetes?


Для того, чтобы охватить по возможности больший класс задач, фреймворк был разбит на три части:

4.1. ZetesFeet


В основе находится библиотека ZetesFeet. Она содержит в себе Avian, а также один из двух вариантов библиотеки классов — Avian Classpath или Android Classpath.

Плюсы и минусы каждой библиотеки приведены в таблице:
Критерий сравнения Avian Classpath Android Classpath
Полнота и соответствие стандартам Содержит только самый минимум, хотя и расширяется. Однако запустить хоть сколько-нибудь серьёзный готовый проект на данной платформе не представляется возможным Содержит имплементацию всех основных классов Java Core. Теоретически, должна поддерживать любое не слишком замороченное приложение. На практике возможны осложнения, связанные с переносом библиотеки на Win32. Разумеется, все подобные ошибки будут исправляться как можно скорее
Размер исполняемого файла Исполняемый файл, не содержащий в себе SWT-классов, имеет размер чуть более мегабайта Исполняемый файл раздувается до 25 мегабайт. Это, в основном, происходит не из-за самих классов Android (которые занимают около 5МБ), а из-за таблиц кодировок, входящик в состав ICU4C — библиотеки поддержки языков, входящей в состав Android Classpath
Лицензия BSD (Совместима с GPL) Apache (тоже открытая, но несовместима с GPL)

Иными словами, если вы просто хотите работающую Java без заморочек и написания своих «велосипедов», используйте Android Classpath.

Помимо этого, в ZetesFeet должны присутствовать все классы Zetes, которые не являются специфичными для какого-то типа приложений, а полезны при создании любых.

Над ZetesFeet построены две библиотеки, альтернативные друг другу.

4.2. ZetesHands


Первая из них — ZetesHands — позволяет создавать консольные приложения. Если хотите сделать новую системную утилиту или, например, какой-нибудь интерпретатор DSL, вам сюда. Неграфические приложения используют ZetesHands. В ОС Windows такое приложение получает консольное окно. В OS X оно компилируется в исполняемый файл, но bundle для него не создается.

4.3. ZetesWings


Вторая библиотека — ZetesWings — используется для создания GUI-приложений. Сборочный процесс включает добавление иконок, сама часть фреймворка содержит базовые классы, которые разработчик должен переопределить для того, чтобы создать своё приложение. В Windows такое приложение консоль не выделяет, в OS X оно при сборке формируется в bundle. «Культурные различия» при работе с GUI (примеры которых были приведены выше) учитываются именно здесь. Основная идеология API — обычна для задач пользовательского интерфейса. Речь идет об архитектуре "Документ-Представление".

Для того, чтобы собрать приложение с использованием Zetes, в его makefile необходимо добавить ссылку либо на ZetesHands.mk, либо на ZetesWings.mk, в зависимости от того, какую из надстроек вы выбрали.

5. Как им пользоваться? Разберем пример — просмотрщик изображений Tiny Viewer




Здесь я опишу простейший вариант использования. Прежде всего, нам потребуется собранная версия Zetes. Взять ее можно со страницы релиза на GitHub. Создадим папку для наших экспериментов с Zetes. Назовем ее, скажем, zetes-works. Зайдем в нее и внутри создадим папку zetes, в которую и распакуем папку target-** из скачанного архива. Теперь вернемся в zetes-works и создадим там папку с названием tinyviewer. Приложение, которое мы собираемся создать, будет называться TinyViewer. Оно демонстрирует основные возможности ZetesWings. Приложение представляет собой простейший просмотрщик картинок. Оно позволяет загрузить одновременно несколько файлов, показывая каждый в отдельном окне. Готовый вариант данной программы находится среди представленных мной примеров в репозитории zetes-examples. Для нетерпеливых в конце статьи приведена ссылка на скомпилированную релизную версию приложений-примеров.

5.1. makefile


Начнем с makefile. Как я и обещал, он будет простым и коротким.

ZETES_PATH = ../zetes

APPLICATION_NAME = Tiny\ Viewer
BINARY_NAME = tinyview
ENTRY_CLASS = tinyviewer/TinyViewerApplication

include $(ZETES_PATH)/common-scripts/globals.mk
include $(ZETES_WINGS_PATH)/ZetesWings.mk

all: package


Разберем построчно. Первым делом определяется переменная ZETES_PATH, задающая путь к библиотеке Zetes относительно директории с проектом. Далее идет APPLICATION_NAME — человекочитаемое название приложения. Затем — BINARY_NAME — название исполняемого файла и ENTRY_CLASS — имя класса, содержащего «точку входа» — функцию public static void main(String... args);. Имя класса задается через слеши (/), а не через точки, как привычно Java-разработчикам. То есть, на самом деле класс будет называться tinyviewer.TinyViewerApplication.

Следующие две строки — подключение, собственно, «мяса» — сборочного движка, входящего в Zetes и позволяющего нам написать коротенький makefile. Первый из подключенных файлов — globals.mk — содержит общие определения, константы и правила, файл ZetesWings.mk — специфичен именно для ZetesWings. В случае использования ZetesHands, мы бы подключали, соответственно, его makefile.

Далее идет определение основного правила. Так как сборка сложного пользовательского проекта может содержать какие-то продвинутые шаги, вы должны определить основное правило all сами. В данном случае мы просто выполним package — правило, которое соберет приложение и запакует его в архив (или, на OS X, в образ dmg). Если вы не хотите паковать его, а хотите только получить работающее приложение, пишите all: app.

5.2. Основные классы


Так как ZetesWings предназначен для написания кроссплатформенных приложений, часть архитектуры вашего приложения он берет на себя. В частности модель документов-представлений (Document-View) определяется им. Поэтому для того, чтобы сделать приложения, вам необходимо создать по классу, отнаследованному от следующих абстракций:
  1. ApplicationBase — основной класс приложения, определяющий глобальные параметры его поведения. По факту, создается в одном экземпляре (хотя синглтоном в строгом смысле не является). Также желательно поместить в него точку входа (функцию main)
  2. ViewWindowsManagerBase — класс, занимающийся управлением окнами-View. Создает окна для документов, хранит на них ссылки. Общение между окнами осуществляется через него.
  3. MenuConstructorBase — класс, управляющий меню — как глобальным (в OS X), так и меню окон. Отвечает за появление/исчезновение/отключение пунктов меню, за правильное определение горячих клавиш. Работа с основным меню приложения в обход этого класса крайне нежелательна. Все просьбы об усовершенствовании принимаются. Основная задача — обеспечить «культурную независимость» меню от платформы
  4. ViewWindowBase — окно, показывающее содержимое некоторого документа. В данной реализации тесно связано с понятием Shell из SWT
  5. Document — документ. Наиболее абстрактная из всех описанных сущностей. Должен уметь сообщать свой заголовок (для формирования заголовка окна и пункта меню) функцией getTitle(), а также принудительно чистить свои ресурсы с помощью функции dispose()

Все перечисленные классы (кроме простого интерфейса Document) являются шаблонами, имеющими связь между собой. И сейчас нам понадобится сделать по наследнику от каждого из них и связать этих наследников воедино.


Связанные шаблоны очень удобны — они задают как бы скелет приложения — некоторую «форму», в которую можно разложить готовые классы, и они будут работать совместно. Надеюсь, вам этот подход понравится так же сильно, как и мне.

5.2.1. ImageDocument



Строительство поведем по зависимостям «снизу вверх». Начнем с класса-документа. В нашем случае это изображение, которое необходимо загрузить средствами SWT и хранить в памяти. Наш класс будет выглядеть так:

ImageDocument.java
package tinyviewer;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.ImageData;
import org.eclipse.swt.widgets.Display;

import zetes.wings.abstracts.Document;

public class ImageDocument implements Document
{
	private Image image;
	private String fileName;

	private static Image loadImage(InputStream stream) throws IOException
	{
		try
		{
			Display display = Display.getDefault();
			ImageData data = new ImageData(stream);
			if (data.transparentPixel > 0) {
				return new Image(display, data, data.getTransparencyMask());
			}
			return new Image(display, data);
		}
		finally
		{
			stream.close();
		}
	}

	private static Image loadImage(String fileName) throws IOException
	{
		return loadImage(new FileInputStream(fileName));
	}

	public ImageDocument(String fileName) throws IOException
	{
		this.fileName = fileName;
		this.image = loadImage(fileName);
	}

	public String getTitle()
	{
		return fileName;
	}

	public Image getImage()
	{
		return image;
	}

	public void dispose()
	{
		if (image != null && !image.isDisposed()) image.dispose();
	}

	@Override
	protected void finalize() throws Throwable
	{
		dispose();
		super.finalize();
	}
}

Здесь всё почти очевидно. Функция loadImage отвечает за загрузку картинки и вызывается в конструкторе. Функция getTitle() возвращает имя файла, а dispose() освобождает ресурсы картинки. На всякий случай, также переопределен finalize(), чтобы вызывался dispose().

Так как мы имеем дело только с просмотром картинки, никаких модифицирующих методов не создаем.

5.2.2. ImageView


Этот класс не имеет прямого отношения к Zetes, но, тем не менее, без него наше приложение никак не обойдется. Это специально написанный SWT-компонент, отображающий картинку на экран.

ImageView.java
package tinyviewer;

import org.eclipse.swt.events.PaintEvent;

import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.swt.widgets.Composite;

public class ImageView extends Canvas
{
	Image image = null;
	Image zoomedImage = null;
	double zoom = 1.0;

	public ImageView(Composite arg0, int arg1)
	{
		super(arg0, arg1);

		addPaintListener(new PaintListener()
		{

			@Override
			public void paintControl(PaintEvent e)
			{
				draw(e.gc);

			}
		});
	}

	protected void draw(GC gc)
	{
		if (image != null) 
		{
			checkZoomedImage();
			Point viewSize = getSize();
			Point imageSize = new Point(zoomedImage.getBounds().width, zoomedImage.getBounds().height);

			int xpos = viewSize.x > imageSize.x ? viewSize.x / 2 - imageSize.x / 2 : 0;
			int ypos = viewSize.y > imageSize.y ? viewSize.y / 2 - imageSize.y / 2 : 0;

			gc.drawImage(zoomedImage, xpos, ypos);
		}
	}

	public Point desiredSize()
	{
		if (image == null)
			return new Point(1, 1);
		else
			return new Point((int)(image.getImageData().width * zoom), (int)(image.getImageData().height * zoom));
	}

	protected void checkZoomedImage()
	{
		if (zoomedImage == null || zoomedImage.isDisposed())
		{
			Rectangle bounds = image.getBounds();
			zoomedImage = new Image(image.getDevice(), (int)(bounds.width * zoom), (int)(bounds.height * zoom));
			GC gc = new GC(zoomedImage);
			gc.drawImage(image, 0, 0, bounds.width,               bounds.height, 
	                            0, 0, (int)(bounds.width * zoom), (int)(bounds.height * zoom));
			gc.dispose();
		}
	}

	public void setImage(Image image)
	{
		this.image = image;
		if (zoomedImage != null) zoomedImage.dispose();
		this.redraw();
	}

	public Image getImage()
	{	
		return image;
	}

	public void setZoom(double zoom)
	{
		this.zoom = zoom;
		if (zoomedImage != null) zoomedImage.dispose();
		this.redraw();
	}

	public double getZoom()
	{
		return zoom;
	}
}

Вся его сложность состоит в нескольких линейных выражениях, позиционирующих изображение по центру окна в случае, если оно меньше этого окна. Всё, что связано с zoom — возможность приближать/отдалять картинку, — в данный момент не используется. Если уважаемые читатели найдут в этом классе какие-нибудь глупости, очень прошу подробно объяснить мне в комментариях, где я неправ. Увы, SWT очень плохо документирован, поэтому многие вещи приходилось выяснять буквально «научным тыком».

5.2.3. ImageViewWindow


Перейдем к следующему классу — классу окна-View, отображающему картинку. Данный класс использует класс ImageView. В сущности, окно будет представлять собой просто рамку вокруг этого самого ImageView.

ImageViewWindow.java
package tinyviewer;

import java.util.HashSet;

import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.ScrolledComposite;
import org.eclipse.swt.dnd.DND;
import org.eclipse.swt.dnd.DropTarget;
import org.eclipse.swt.dnd.DropTargetAdapter;
import org.eclipse.swt.dnd.FileTransfer;
import org.eclipse.swt.dnd.Transfer;
import org.eclipse.swt.events.ControlEvent;
import org.eclipse.swt.events.ControlListener;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.wb.swt.SWTResourceManager;

import zetes.wings.base.ViewWindowBase;


public class ImageViewWindow extends ViewWindowBase<ImageDocument>
{
	private ScrolledComposite scrolledComposite;
	private DropTarget scrolledCompositeDropTarget, imageViewDropTarget;
	private ImageView imageView;
	private HashSet<DropTargetAdapter> dropTargetAdapters = new HashSet<DropTargetAdapter>();

	public void addDropTargetListener(DropTargetAdapter dropTargetAdapter)
	{
		dropTargetAdapters.add(dropTargetAdapter);

		if (imageViewDropTarget != null && !imageViewDropTarget.isDisposed())
		{
			scrolledCompositeDropTarget.addDropListener(dropTargetAdapter);
			imageViewDropTarget.addDropListener(dropTargetAdapter);
		}
	}

	public void removeDropTargetListener(DropTargetAdapter dropTargetAdapter)
	{
		dropTargetAdapters.remove(dropTargetAdapter);
		if (imageViewDropTarget != null && !imageViewDropTarget.isDisposed())
		{
			scrolledCompositeDropTarget.removeDropListener(dropTargetAdapter);
			imageViewDropTarget.removeDropListener(dropTargetAdapter);
		}
	}

	/**
	 * Create contents of the window.
	 * 
	 * @wbp.parser.entryPoint
	 */
	@Override
	protected Shell constructShell()
	{
		Shell shell = new Shell(SWT.TITLE | SWT.CLOSE | SWT.MIN | SWT.MAX | SWT.RESIZE | SWT.BORDER | SWT.DOUBLE_BUFFERED);

		shell.setBackground(SWTResourceManager.getColor(SWT.COLOR_BLACK));
		shell.setMinimumSize(new Point(150, 200));

		shell.setImages(new Image[] { 
				SWTResourceManager.getImage(ImageViewWindow.class, "/tinyviewer/wingphotos16.png"),		// Necessary in Windows (for taskbar)
				SWTResourceManager.getImage(ImageViewWindow.class, "/tinyviewer/wingphotos64.png"),		// Necessary in Windows (for Alt-Tab)
				SWTResourceManager.getImage(ImageViewWindow.class, "/tinyviewer/wingphotos512.png")		// Necessary in OS X
		});

		shell.setLayout(new FillLayout(SWT.HORIZONTAL));

		scrolledComposite = new ScrolledComposite(shell, SWT.H_SCROLL | SWT.V_SCROLL | SWT.NO_BACKGROUND);
		scrolledComposite.setExpandHorizontal(true);
		scrolledComposite.setExpandVertical(true);

		imageView = new ImageView(scrolledComposite, SWT.NONE);
		imageView.setBounds(0, 0, 200, 127);
		imageView.setVisible(false);
		imageView.setBackground(shell.getDisplay().getSystemColor(SWT.COLOR_BLACK));

		// Drop targets
		scrolledCompositeDropTarget = new DropTarget(scrolledComposite, DND.DROP_MOVE);
		scrolledCompositeDropTarget.setTransfer(new Transfer[] { FileTransfer.getInstance() });
		imageViewDropTarget = new DropTarget(imageView, DND.DROP_MOVE);
		imageViewDropTarget.setTransfer(new Transfer[] { FileTransfer.getInstance() });

		for (DropTargetAdapter adapter : dropTargetAdapters)
		{
			scrolledCompositeDropTarget.addDropListener(adapter);
			imageViewDropTarget.addDropListener(adapter);
		}

		scrolledComposite.setContent(imageView);
		scrolledComposite.setMinSize(imageView.desiredSize());
		scrolledComposite.setBackground(shell.getDisplay().getSystemColor(SWT.COLOR_BLACK));

		scrolledComposite.addControlListener(new ControlListener()
		{
			@Override
			public void controlResized(ControlEvent arg0)
			{
				updateImageViewSize();

			}

			@Override
			public void controlMoved(ControlEvent arg0) { }
		});

		return shell;
	}

	private void updateImageViewSize()
	{
		Point desired = imageView.desiredSize();
		Point clientAreaSize = new Point(scrolledComposite.getClientArea().width, scrolledComposite.getClientArea().height); 

		int width = Math.max(clientAreaSize.x, desired.x); 
		int height = Math.max(clientAreaSize.y, desired.y);
		Point newSize = new Point(width, height);

		Point oldSize = imageView.getSize();

		if (!oldSize.equals(newSize))
		{
			imageView.setSize(newSize);
		}
	}

	@Override
	public void setDocument(ImageDocument document)
	{
		super.setDocument(document);

		imageView.setImage(getDocument().getImage());
		scrolledComposite.setMinSize(imageView.desiredSize());
		updateImageViewSize();
		imageView.setVisible(true);
		getShell().forceActive();
	}

	@Override
	public boolean supportsFullscreen()
	{
		return true;
	}

	@Override
	public boolean supportsMaximizing()
	{
		return true;
	}
}

Прежде всего, стоит обратить внимание на то, что класс унаследован от шаблона ViewWindowBase, которому в качестве параметра передан наш класс документа. То есть, окно связано с документом данного типа. Явно показана зависимость между классами.

Задачи окна предельно просты: показать загруженное изображение (это делается в перегруженной функции setDocument). Затем просто радовать пользователя своим видом, пока тот его не закроет. Помимо этого, как нам указывают объекты DropTarget, окно реагирует на событие «бросания» в него файла заданного типа. В том же окне файл не откроется (это было бы просто неинтересно), он будет загружен как новый документ и для него будет создано отдельное окно. Заметим, что за открытие новых окон данный класс не отвечает, поэтому он выводит наружу listener с методами addDropTargetListener и removeDropTargetListener. Использование этих методов будет показано далее.

Я не буду заострять ваше внимание на конкретной реализации, тем более, что она далека от совершенства. Скажу только, для чего нужны основные переопределенные методы:

constructShell() вызывается при создании окна для формирования объекта Shell из SWT. Этот объект и есть наше окно. Аннотация @wbp.parser.entryPoint над методом — инструкция для визуального рисовальщика SWF-окошек в Eclipse редактировать именно этот метод при модификации окошка через его графичесеое представление в design-time. Так что, теоретически, вы сможете нарисовать окошко и кнопки мышью ;) (хотя сам я предпочитаю писать кодом — так качественнее). Именно здесь назначаются внутренние события. Про setDocument() я уже сказал. А методы supportsFullscreen() и supportsMaximizing() управляют соответствующими фичами в Zetes. Например, окно, возвращающее false на supportsFullscreen(), не будет иметь кнопки развертывания со стрелочками на OS X, а также не будет создавать пункт меню «Fullscreen» ни на одной платформе.

5.2.4. ImageViewWindowsManager



Этот класс занимается управлением окнами. Он их создает и уничтожает.

ImageViewWindowsManager.java
package tinyviewer;

import java.io.IOException;

import org.eclipse.swt.dnd.DropTargetAdapter;
import org.eclipse.swt.dnd.DropTargetEvent;
import org.eclipse.swt.dnd.FileTransfer;

import zetes.wings.base.ViewWindowsManagerBase;

public class ImageViewWindowsManager extends ViewWindowsManagerBase<ImageDocument, ImageViewWindow>
{
	private DropTargetAdapter viewWindowDropTargetAdapter = new DropTargetAdapter()
	{
		public void drop(DropTargetEvent event) {
			String fileList[] = null;
			FileTransfer ft = FileTransfer.getInstance();
			if (ft.isSupportedType(event.currentDataType)) {
				fileList = (String[]) event.data;
				for (int i = 0; i < fileList.length; i++)
				{
					ImageDocument document;
					try
					{
						document = new ImageDocument(fileList[i]);
						openWindowForDocument(document);
					}
					catch (IOException e)
					{
						e.printStackTrace();
					}
				}
			}
		}
	};
	
	@Override
	protected ImageViewWindow createViewWindow()
	{
		ImageViewWindow vw = new ImageViewWindow();
		vw.addDropTargetListener(viewWindowDropTargetAdapter);
		return vw;
	}

	public DropTargetAdapter getViewWindowDropTargetAdapter()
	{
		return viewWindowDropTargetAdapter;
	}
}

Он унаследован от generic-класса ViewWindowsManagerBase, связанного как с классом окон ImageViewWindow, так и с классом документов ImageDocument.

В нашем случае управление окнами происходит абсолютно стандартным способом. Единственная вещь, которую мы добавили, — назначение listener-а для DropTarget-ов в окне с картинкой (помните, мы ссылались на это в прошлом разделе?). Здесь же мы определили метод-фабрику createViewWindow() таким образом, чтобы при создании нового окна к нему сразу присоединялся обработчик Drag-n-drop.

5.2.5. TinyViewerMenuConstructor


Данный класс занимается управлением главным меню приложения. Вся ответственность за меню (как глобальное в OS X, так и оконное в остальных платформах) находится на нем. Поэтому при использовании Zetes непосредственное управление классами меню посредством SWT крайне нежелательно. Разумеется, здесь, увы, имеет место дырявая абстракция, но, так как по ряду очевидных причин полностью инкапсулировать SWT, по крайней мере, на данном этапе развития нашей библиотеки, невозможно, мы вынуждены мириться с этим.

TinyViewerMenuConstructor.java
package tinyviewer;

import zetes.wings.HotKey;
import zetes.wings.base.MenuConstructorBase;
import zetes.wings.actions.Action;
import zetes.wings.actions.Handler;

public class TinyViewerMenuConstructor extends MenuConstructorBase<ImageViewWindow>
{
	private Handler<ImageViewWindow> fileOpenHandler;
	private Action<ImageViewWindow> openAction;
	
	public TinyViewerMenuConstructor(ImageViewWindowsManager viewWindowsManager) {
		super(viewWindowsManager);
		
		openAction = new Action<>("&Open");
		openAction.setHotKey(new HotKey(HotKey.MOD1, 'O'));
		getFileActionCategory().addFirstItem(openAction);
	}
	
	public Handler<ImageViewWindow> getFileOpenHandler()
	{
		return fileOpenHandler;
	}

	public void setFileOpenHandler(Handler<ImageViewWindow> fileOpenHandler)
	{
		this.fileOpenHandler = fileOpenHandler;
		if (openAction.getHandlers().get(null) == null) {

			openAction.getHandlers().put(null, fileOpenHandler);
		}
	}

}

Этот класс связан только с окном и с менеджером окон (со вторым — через конструктор). Так как, собственно, view в нашей идеологии является само окно, а меню — лишь вспомогательный элемент интерфейса, полностью управляемый активной view, выполнять какие-либо манипуляции над данными (то есть, документом) должна именно view. То есть, если нам нужно добавить, скажем, команду «Copy», то у нашего окна должна появиться, например, функция actionCopy, которую и должен вызывать обработчик меню. То есть, еще раз: меню само по себе ничего не делает, оно всегда просит об этом либо окна, либо менеджер окон (в нашем случае, ImageViewWindowsManager) .

Подробнее об устройстве меню в ZetesWings
Тут надо пару слов сказать о том, как в ZetesWings организованы меню. Основным столпом является класс Action. Этот класс реализует пункт меню. Он состоит из заголовка («title»), горячего сочетания клавиш («hotKey») и обработчиков («handlers»), причем последние назначаются как Map, где ключами выступают ViewWindow. Суть проста. Когда то или иное окно создается, оно добавляет свои обработчики к тем action-ам, которые его интересуют. Помимо этого, есть один обработчик, не связанный ни с одним окном (имеющий ключ null). Этот обработчик зовется в том случае, когда активного окна нет (в OS X возможна ситуация, когда меню есть, а окон нету).

Приоритет таков. Когда пользователь выбирает пункт меню, сперва отыскивается обработчик для данного окна. Если такового нет, отыскивается глобальный обработчик.
Если отсутствуют, оба обработчика, то пункт меню просто прячется (например, в окне, которое не содержит элементов редактирования, команда «Copy» бессмысленна, равно как она бессмысленна при отсутствии окон, в отличие от, скажем, «Open»). При этом «обработчик» (то есть объект класса Handler) — это не просто listener. Он содержит такие опции, как isEnabled(), isVisible(), isChecked(). Кроме того, он способен перекрывать заголовок action-а своим (для случая контекстно-зависимого заголовка). На этот случай в нем есть свой getTitle().

При создании TinyViewerMenuConstructor создает объекты Action для основного меню. В нашем случае нестандартный элемент только один — «Open». Мы создаем его и присваиваем ему горячую клавишу HotKey(HotKey.MOD1, 'O'). MOD1 — это такой модификатор, который под OS X соответствует клавише Command, а под другими платформами — Control. Пользователи обеих традиций знают, что большинство горячих клавиш с Ctrl в Windows нажимаются в OS X именно с Cmd.

5.2.6. TinyViewerApplication



Последний (и основной) в нашей иерархии — класс-приложение TinyViewerApplication. Он связан со всеми вышеперечисленными классами. Плюс к этому, он связан с наследником AboutBox (в нашем случае — DefaultAboutBox), реализующим окошко «О приложении».

TinyViewerApplication.java
package tinyviewer;

import java.io.IOException;
import java.util.ArrayList;

import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.FileDialog;
import org.eclipse.swt.widgets.Shell;

import zetes.wings.base.ApplicationBase;
import zetes.wings.DefaultAboutBox;
import zetes.wings.actions.Handler;


public class TinyViewerApplication extends ApplicationBase<DefaultAboutBox, ImageDocument, ImageViewWindow, TinyViewerMenuConstructor, ImageViewWindowsManager>
{
	@Override
	public String getTitle()
	{
		return "Tiny Viewer";
	}

	@Override
	public DefaultAboutBox createAboutBox(ImageViewWindow window)
	{
		DefaultAboutBox res = new DefaultAboutBox(window);
		res.setApplicationName(getTitle());
		res.setIconResourceName("/tinyviewer/wingphotos64.png");
		res.setDescriptionText("A simple image file viewer.\nThis application shows the power of Avian + SWT");
		res.setCopyrightText("Copyright \u00a9 2013, Ilya Mizus");
		res.setWindowSize(new Point(370, 180));
		return res;
	}
	
	@Override
	public ImageDocument loadFromFile(String fileName)
	{
		try
		{
			return new ImageDocument(fileName);
		}
		catch (IOException e)
		{
			e.printStackTrace();
			return null;
		}
	}
	
	private Handler<ImageViewWindow> fileOpenHandler = new Handler<ImageViewWindow>() {
		
		@Override
		public void execute(ImageViewWindow window) {
			Shell dummyShell = new Shell(Display.getDefault());
			FileDialog fileDialog = new FileDialog(dummyShell, SWT.OPEN | SWT.MULTI);
			fileDialog.setText("Open image");
			fileDialog.setFilterNames(new String[] { "Image (*.png; *.bmp; *.jpg; *.jpeg)", "All files" });
			fileDialog.setFilterExtensions(new String[] { "*.png; *.bmp; *.jpg; *.jpeg", "*.*" });
			String firstFile = fileDialog.open();
			if (firstFile != null)
			{
				String[] names = fileDialog.getFileNames();
				ArrayList<ImageDocument> documents = new ArrayList<ImageDocument>();
				
				// Creating documents for files
				for (int i = 0; i < names.length; i++)
				{
					String fileName = fileDialog.getFilterPath() + "/" + names[i];
					try
					{
						documents.add(new ImageDocument(fileName));
					}
					catch (IOException e)
					{
						// TODO Show a message box here
						e.printStackTrace();
					}
				}
				
				getViewWindowsManager().openWindowsForDocuments(documents.toArray(new ImageDocument[] {}));
			}
			dummyShell.dispose();		
		}
	};
	
	public TinyViewerApplication()
	{
	}
	
	@Override
	public ImageViewWindowsManager createViewWindowsManager()
	{
		return new ImageViewWindowsManager();
	}

	@Override
	public TinyViewerMenuConstructor createMenuConstructor(ImageViewWindowsManager viewWindowsManager)
	{
		TinyViewerMenuConstructor menuConstructor = new TinyViewerMenuConstructor(viewWindowsManager);
		menuConstructor.setFileOpenHandler(fileOpenHandler);
		return menuConstructor;
	}

	@Override
	public boolean needsAtLeastOneView()
	{
		return false;
	}

	public static void main(String... args)
	{
		new TinyViewerApplication().run(args);
	}
}

Этот класс отвечает за загрузку документов. В нашем приложении именно он содержит объект fileOpenHandler, представляющий собой глобальный обработчик пункта меню «Open». Помимо этого, он содержит несколько методов-фабрик для наших «глобальных» объектов (конструктора меню, менеджера окон и окна «о программе»). Также в нем объявлен занятный метод needsAtLeastOneView(), о котором можно немного сказать особо.

Подробнее об needsAtLeastOneView()
Как известно, в Windows и в Linux для отображения меню приложение должно иметь хотя бы одно окно. Поэтому графические приложения открывают пустое окно при запуске — просто для того, чтобы строку меню было, где отобразить. В OS X строка меню — объект глобальный, ей не требуется никакое окно. Поэтому создание пустого окна — бессмысленное замусоривание рабочего пространства. Таким образом, при запуске приложения окна открываются только при необходимости.

Данная разница в поведении учтена в ZetesWings. Если функция needsAtLeastOneView() возвращает false, то при запуске окна на OS X будут открываться только для отображения загружаемых документов.

Однако и под OS X есть приложения, интерфейс которых должен открыться вне зависимости от наличия/отсутствия загружаемого документа (например, игра). Для таких приложений имеет смысл сделать needsAtLeastOneView(), возвращающий true. И окно будет открываться сразу, без документа. Помимо этого, в этом случае при закрытии последнего окна в OS X приложение будет тоже выгружаться так же, как оно выгружается в других ОС.

На этом наш обзор исходного кода данного проекта можно считать завершенным.

5.3. Вспомогательные файлы и ресурсы


Нам остались только некоторые дополнительные файлы, без которых проект не может обойтись. Эти файлы — файлы ресурсов — расположены в 4х папках:

  1. src/res — рядом с src/java — эти файлы будут помещены в пространство имен Java и доступны как обычные Java-ресурсы. В нашем примере там расположены 3 разрешения иконки приложения для загрузки через SWT командами:
    shell.setImages(new Image[] { 
    	SWTResourceManager.getImage(ImageViewWindow.class, "/tinyviewer/wingphotos16.png"),
    	SWTResourceManager.getImage(ImageViewWindow.class, "/tinyviewer/wingphotos64.png"),
    	SWTResourceManager.getImage(ImageViewWindow.class, "/tinyviewer/wingphotos512.png")
    });

  2. win-res — здесь должны быть расположены ресурсные файлы, которые попадут внутрь исполняемого файла Windows EXE. Это, как минимум, файл win.rc
    win.rc
    MainIcon ICON "win-res/wingphotos.ico"
    1 VERSIONINFO
    FILEVERSION 0,1,0,0
    PRODUCTVERSION 0,1,0,0
    BEGIN
      BLOCK "StringFileInfo"
      BEGIN
        BLOCK "040904E4"
        BEGIN
          VALUE "CompanyName", "bigfatbrowncat\0"
          VALUE "FileDescription", "A tiny multi document photo viewer app"
          VALUE "FileVersion", "0.1\0"
          VALUE "InternalName", "TinyViewer\0"
          VALUE "LegalCopyright", "Ilya Mizus\0"
          VALUE "OriginalFilename", "tinyviewer.exe\0"
          VALUE "ProductName", "TinyViewer\0"
          VALUE "ProductVersion", "0.1\0"
        END
      END
      BLOCK "VarFileInfo"
      BEGIN
        VALUE "Translation", 0x409, 1251
      END
    END
        

  3. osx-bundle — сюда помещается содержимое папки .app, которая под OS X называется «application bundle» и распознается системой как приложение с пользовательским интерфейсом и иконкой. Структура данной папки повторяет структуру самого bundle
  4. resources — помещенные в эту папку файлы под всеми системами, кроме OS X, будут скопированы в папку приложения. Под OS X же они будут помещены в папку Resources, расположенную внутри bundle. Эти файлы доступны из приложения как ресурсы через класс WinLinMacApi. В данном проекте эта папка отсутствует, но она присутствует в других проектах из числа zetes-examples.


5.4. Сборка и запуск


Данное приложение может быть собрано как с Android classpath, так и с Avian classpath. Инструкция по сборке находится там же, где и исходный код, — в проекте zetes-examples. Если у кого-то возникнут проблемы — задавайте вопросы, с удовольствием отвечу.

6. Заключение


Прежде всего, я хочу от души поблагодарить всех, кому хватило терпения (и у кого достало интереса) дочитать до этого места. Я очень рад, что эта тема интересна не только мне. Чем вас больше, тем больше у меня энтузиазма.

Кроме этого, я, конечно, не могу не отметить, что, хотя данная версия фреймворка весьма стабильна и, наверное, может даже быть использована с известной осторожностью для создания простых приложений, многое в нем будет перерабатываться и меняться. Сам я постараюсь в ближайшее время создать на его основе что-то более полезное, чем несколько проектов-примеров.

Любые пожелания/советы/критические замечания принимаются и обязательно будут рассмотрены.

7. Ссылки




Напоследок я хотел бы еще раз поблагодарить моего друга и соратника JustAMan, без помощи и моральной поддержки которого ни эта работа, ни эта статья, думаю, не были бы доведены до конца никогда. Кроме того, хочу сказать огромное спасибо любимой жене, которая не только безревностно позволяет мне вечерами заниматься всякой чушью за компьютером, но даже проверяет мои статьи и расставляет в них правильно забытые мной запятые.

Всех, кому небезразлична данная тема и кто интересуется ее развитием, прошу поставить звёздочки моим гитхаб-проектам zetes и zetes-examples, чтобы я видел интерес сообщества. Так как проект некоммерческий и развивается исключительно на моем энтузиазме, мне жизненно необходимы единомышленники. Разумеется, если у кого-нибудь возникнет желание внести посильную лепту в код, я буду только рад!

Спасибо за внимание!
Теги:JavaAvianAndroidC++embeddedmakefilecross-platformjvmjniGUIzetes
Хабы: Java C
+84
31,5k 289
Комментарии 64
Похожие публикации
Профессия Java-разработчик
1 декабря 202082 500 ₽SkillFactory
Профессия Android-разработчик
14 декабря 202071 000 ₽SkillFactory
Java QA Engineer
21 декабря 202060 000 ₽OTUS
Java Developer. Professional
22 декабря 202060 000 ₽OTUS
Android Developer. Basic
24 декабря 202070 000 ₽OTUS
Лучшие публикации за сутки