Как создать собственный компонент в swing

Небольшая выдержка из замечательной книги по Swing: "Swing. Эффективные пользовательские интерфейсы" Ивана Портянкина, посвященная созданию собственного графического компонента. Статья раскрывает самый важный нюанс создания собственного компонента - программирование отрисовки его внешнего вида.


Введение в Swing в тезисах

Тяжелые и легкие компоненты

Тяжеловесные компоненты - часть библиотеки AWT. К ним относятся окна Frame и Window, а также диалоговое окно Dialog. Эти компоненты создаются и обрабатываются операционной системой и являются полотном для отрисовки легковесных компонентов, которыми являются компоненты Swing.

Двойная буферизация

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

Для экономии ресурсов, второй буфер не заводится для каждого компонента отдельно. Вместо этого существует единый буфер, управляемый RepaintManager-ом. Для отключения двойной буферизации во всем приложении используется метод RepaintManager.setDoubleBufferingEnabled(false). Двойную буферизацию можно отключать для конкретных компонентов методом setDoubleBuffered(false). Столь не разумный на первый (и второй :) ) взгляд ход, чаще всего требуется при отладке приложения.

Базовый компонент JComponent и механизм отрисовки.

Все компоненты Swing наследуются от JComponent. Этот базовый класс реализует механизм отрисовки легковесных компонентов.

Процесс отрисовки компонента начинается с вызова операционной системой метода paint() тяжеловесного компонента. Далее, вызов метода paint() происходит для всех вложенных в него компонентов.

Как бы не чесались руки, переопределить метод paint, делать этого не стоит!

На первом этапе метода paint() определяется текущий объект RepaintManager, у которого запрашивается информация об использовании двойной буферизации. В случае ее использования, получается буфер. Далее вычисляется прямоугольник отсечения (clip rectangle) по аргументам, переданным операционной системе родительскому тяжеловесному компоненту.

При включенной двойной буферизации вызывается метод paintDoubleBuffered() в котором происходит настройка внеэкранного буфера: получается изображение необходимого размера, для него создается объект Graphics и вызывается метод paintWithOffScreenBuffer() инициирующий непосредственно прорисовку компонента, которая происходит в три этапа.

Первому этапу соответствует метод paintComponent(). Именно этот метод отвечает за логику отрисовки компонента. Важной особенностью переопределения именно этого метода, а не метода paint(), является то, что переопределяя метод paintComponent вы избавляете себя от задач оптимизации вывода графики и отрисовки вложенных компонентов. Переопределение метода paintComponent - решение задачи в лоб. Более элегантное решение - реализация UI представителя для нового компонента, но этот товарищ останется за рамками данной статьи. Чтобы понять какую роль этот объект играет в отрисовке компонента, достаточно заглянуть в исходный код метода paintComponent:
protected void paintComponent(Graphics g) {
    if (ui != null) {
            Graphics scratchGraphics = (g == null) ? null : g.create();
        try {
            ui.update(scratchGraphics, this);
        }
            finally {
            scratchGraphics.dispose();
        }
    }
}

Следующим этапом идет отрисовка рамки компонента методом paintBorder(). Этот метод использует поле border компонента для отрисовки рамки и не требует переопределения. Единственным нюансом тут является тот факт, что рамка рисуется поверх результатов труда метода paintComponent. Поэтому при отрисовке компонента в paintComponent стоит учитывать наличие/отсутствие рамки у компонента и ее толщину.

Завершающим этапом является отрисовка вложенных компонентов в методе paintChildren(). Смысла в переопределении этого метода не больше, чем в переопределении метода paint.

Процесс отрисовки компонентов

Отладка графики

В swing есть замечательное средство для отладки процесса рисования, класс DebugGraphics. Он представляет собой обертку над Graphics и позволяет контролировать процесс отрисовки.

DebugGraphics предоставляет возможность вывода информации о выполняемых операциях, замедление процесса изображения (для это требуется отключение двойной буферизации), дублирование процесса изображения в отдельном окне. Для включения отладки отрисовки вызывается метод setDebugGraphicsOptions(int) у компонента, чей процесс отрисовки вы желаете отладить. Следует заметить, что отладка будет так же  включена для всех вложенных компонентов.

Режимы отладки заданы в статических константах класса DebugGraphics:

NONE_OPTIONОтключает режим отладки графики для используемого объекта.
LOG_OPTIONПозволяет выводить диагностические сообщения обо всех производимых графических операциях (то есть обо всех вызываемых методах). По умолчанию сообщения выводятся в стандартный поток вывода System.out, изменить поток вывода сообщений позволяет статический метод класса DebugGraphics setLogStream().
FLASH_OPTIONУстановка этого флага приводит к тому, что все появляющееся на экране рисуется в замедленном режиме и выделяется особым цветом, так что вы своими глазами можете проследить, как и где происходит рисование. Настроить данный режим позволяют три статических метода класса DebugGraphics: метод setFlashColor() устанавливает цвет, которым выделяются все операции рисования, метод setFlashCount() позволяет задать количество «мерцаний» при рисовании, наконец, метод setFlashTime() используется для того, чтобы указать задержку после каждой операции рисования. Имейте в виду, данный режим отладки доступен только при отключении двойной буферизации.
BUFFERED_OPTIONВ данном режиме операции рисования будут дополнительно выводиться в отдельное окно, создаваемое классом DebugGraphics. При этом учитываются остальные режимы отладки — они будут действовать в созданном окне.

Чтобы указать сразу несколько режимов, используйте логическое ИЛИ:
setDebugGraphicsOptions(DebugGraphics.LOG_OPTION | DebugGraphics.FLASH_OPTION);

Резюме:

Непосредственных инструкций по созданию компонента приводить смысла не вижу, все и так достаточно понятно: создаем класс, наследуем его от одного из существующих компонентов, переопределяем алгоритм отрисовки в методе paintComponent (не забываем учитывать возможность добавления рамки),   создаем необходимые события, с гордостью используем свой новый компонент.

Выбор родительского компонента зависит от конкретного разрабатываемого компонента. Если вам не нужна функциональность уже существующих компонентов, наследуйте от JComponent. Это очевидно.

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


Комментариев нет: