Статьи

Совет по разработке пользовательских компонентов Swing: не все прозрачно

В этой статье объясняется прозрачность (непрозрачность) и как избежать некоторых распространенных ошибок, возникающих при разработке пользовательских компонентов Swing. Предыдущая статья этой серии под названием Insets Matter познакомила нас с новым разработчиком Swing по имени Toni и документировала его усилия по созданию пользовательского компонента Swing. названный шар. Цель Orb — нарисовать простой синий круг в границах компонента.

Текущий код и вывод Тони можно посмотреть ниже.

private class Orb extends JComponent {
@Override
protected void paintComponent(Graphics graphics) {
super.paintComponent(graphics);

//create a new graphics2D instance
Graphics2D g2 = (Graphics2D) graphics.create();

//determine the actual x, y, width and height
int x = getInsets().left;
int y = getInsets().top;
int w = getWidth() - getInsets().left - getInsets().right;
int h = getHeight() - getInsets().top - getInsets().bottom;

g2.setPaint(Color.BLUE);
g2.fillOval(x, y, w, h);
}
}

У Тони все шло очень хорошо, все в компании были впечатлены недавней демонстрацией его Orb. После нескольких дней безупречного тестирования Orb был распространен на веб-сайте компании, в результате чего поступили заказы на Orb.

Через несколько недель покупатели Orb начали жаловаться на две серьезные проблемы. Во-первых, они не могут изменить цвет фона шара, а во-вторых, после попытки изменить цвет фона стали появляться странные артефакты рендеринга.

После прочтения отчетов об ошибках Тони сразу понимает, что его код не учитывает непрозрачность компонента и цвет фона. Тони понимает прозрачность компонентов как противоположность прозрачности. Когда компонент имеет opaque = false, он будет прозрачным и не закрашивает свой фон. Тем не менее, когда компонент имеет opaque = true, он не будет прозрачным, а затем будет использовать свой цвет фона, чтобы закрасить фон компонента.

Чтобы воспроизвести дефект, Тони пишет некоторый код для создания нового экземпляра Orb, устанавливает его непрозрачность в true и устанавливает его цвет фона в красный.

Orb orb = new Orb();				
orb.setBackground(Color.RED);
orb.setOpaque(true);

После запуска теста Тони ясно видит, что Сфера не учитывает непрозрачность компонента. Тони теперь корректирует свой код следующим образом.

private class Orb extends JComponent {
@Override
protected void paintComponent(Graphics graphics) {
super.paintComponent(graphics);

//create a new graphics2D instance
Graphics2D g2 = (Graphics2D) graphics.create();

//determine the actual x, y, width and height
int x = getInsets().left;
int y = getInsets().top;
int w = getWidth() - getInsets().left - getInsets().right;
int h = getHeight() - getInsets().top - getInsets().bottom;

//this will fill the component's background
//based on the component's opacity and
//the component's border and insets
if (isOpaque()) {
g2.setPaint(getBackground());
g2.fillRect(x, y, w, h);
}

g2.setPaint(Color.BLUE);
g2.fillOval(x, y, w, h);
}
}

Тони снова запускает свою тестовую программу и рад видеть, что фон Орба становится красным в его тесте. Что касается артефактов рендеринга, быстрые тесты Тони не смогли воспроизвести их в любой форме, и он быстро (и наивно) отложил их до некоторой причуды на компьютере клиента.

Все довольны изменениями Тони, и обновление будет разослано всем клиентам.

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

Давайте на мгновение оставим Тони и обсудим проблему, с которой он сейчас сталкивается. Проблема на самом деле заключается в понимании Тони непрозрачности компонента. Тони понимает непрозрачность компонента примерно на 85% правильно. Да, непрозрачность компонента определяет, является ли компонент прозрачным и должен ли его фон заполняться цветом фона. Однако это немного больше к этому.

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

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

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

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

Возвращаясь к коду Тони, мы видим, что, принимая во внимание границу компонента, используя вставки, он оставляет область границы компонента неокрашенной. Это то, что вызывает артефакты рендеринга Тони. Можно утверждать, что граница должна рисовать эти пиксели, однако это не может быть гарантировано, так как граница может быть пустым или не полностью непрозрачной.

Чтобы правильно обрабатывать непрозрачность компонента, заполненная фоновая область должна игнорировать вставки компонента и заполнять все границы компонента.

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

private class Orb extends JComponent {
@Override
protected void paintComponent(Graphics graphics) {
super.paintComponent(graphics);

//create a new graphics2D instance
Graphics2D g2 = (Graphics2D) graphics.create();

//determine the actual x, y, width and height
int x = getInsets().left;
int y = getInsets().top;
int w = getWidth() - getInsets().left - getInsets().right;
int h = getHeight() - getInsets().top - getInsets().bottom;

//this will fill the component's background
//based on the component's opacity and
//fill the entire bounds of the component.
if (isOpaque()) {
g2.setPaint(getBackground());
g2.fillRect(0, 0, getWidth(), getHeight());
}

g2.setPaint(Color.BLUE);
g2.fillOval(x, y, w, h);
}
}

После изменения кода Тони снова запускает тест.

Хотя результаты теста не являются визуально привлекательными, они отображают правильное поведение и подчиняются правилам, установленным платформой Swing.

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

Дополнительные статьи о свинге можно найти на сайте Custom Swing Components в разделе блогов.