Решение, которое я собираюсь предложить, будет далеко не полным, но я надеюсь, что оно проиллюстрирует некоторые концепции, которые должны помочь вам добиться успеха. Я собираюсь создать панель с двумя подвижными изображениями, но сделаю это, используя шаблоны, которые должны упростить поддержку этого кода.
Я настоятельно рекомендую разделить ваш слой рендеринга (ваш JComponents
) и вашу модель. слой (формы и их расположение) в отдельные объекты. Это называется разделением интересов и в конечном итоге облегчит вашу жизнь.
Сначала давайте определим одну из ваших фигур, которая появляется на экране. Я собираюсь сделать мой очень простым и создам один с местоположением, размером и BufferedImage
:
class DrawNode
{
private Rectangle bounds;
private BufferedImage image;
public DrawNode()
{
}
public Rectangle getBounds()
{
return bounds;
}
public void setBounds( Rectangle bounds )
{
this.bounds = bounds;
}
public BufferedImage getImage()
{
return image;
}
public void setImage( BufferedImage image )
{
this.image = image;
}
}
Далее, давайте создадим модель для хранения коллекции ваших фигур. Одна из полезных вещей, которую мы можем сделать с моделью, - это принять слушателя, который будет вызываться всякий раз, когда части нашего 2D-пространства становятся «недействительными». Части пространства могут стать недействительными, и потребуется перекрасить, если узел ранее находился в заданной области и был перемещен в новую область. Мы также включим некоторые вспомогательные методы для определения формы, занимающей данное пространство, и т. Д. c. Эти помощники могут быть значительно оптимизированы с точки зрения их эффективности, но моя простая версия будет использовать итерацию грубой силы:
class DrawPanelModel
{
private final List<DrawNode> nodes;
private final Consumer<Rectangle> invalidAreaListener;
public DrawPanelModel( Consumer<Rectangle> invalidAreaListener )
{
this.invalidAreaListener = invalidAreaListener;
nodes = new ArrayList<>();
}
public void addNode( DrawNode node )
{
nodes.add( node );
}
public Optional<DrawNode> getNodeForPoint( Point p )
{
return nodes.stream()
.filter( node -> node.getBounds().contains( p ))
.findFirst();
}
public Stream<DrawNode> getNodesInRectangle( Rectangle r )
{
return nodes.stream()
.filter( node -> node.getBounds().intersects( r ));
}
public void setNodeLocation( DrawNode node, Point p )
{
Rectangle bounds = (Rectangle)node.getBounds().clone();
bounds.setLocation( p );
setNodeBounds( node, bounds );
}
public void setNodeBounds( DrawNode node, Rectangle bounds )
{
Rectangle old = node.getBounds();
node.setBounds( Objects.requireNonNull( bounds ));
if ( old == null || !old.equals( bounds ))
{
invalidAreaListener.accept( bounds );
if ( old != null ) {
invalidAreaListener.accept( old );
}
}
}
}
Далее нам нужен способ визуализации наших фигур / узлов на экране. Мы могли бы рисовать каждый узел каждый раз, когда мы вызываем paint(...)
, но это не очень эффективно, так как нам действительно нужно перерисовать недопустимые области. Мы можем оставить остальные области в покое, убедившись, что JComponent
использует двойную буферизацию: https://docs.oracle.com/javase/7/docs/api/javax/swing/JComponent.html#setDoubleBuffered (логическое значение)
Редактировать: Небольшая коррекция: Двойная буферизация даст преимущества, главным образом, в том, что касается прокрутки, если вы поместите компонент в область прокрутки. Я не думаю, что это повлияет на поведение простых перерисовок. (Устал, когда я писал это вчера)
Одна из распространенных техник, используемых для выполнения sh в Swing, - это использование CellRendererPane
в сочетании с JLabel
, который вы используете в качестве средства визуализации. Всякий раз, когда нам нужно нарисовать узел в указанном c месте, мы можем назначить желаемое изображение и размер для JLabel
и сделать CellRendererPane
визуализацию этого JLabel
во время процедуры рисования (возможно, более одного раза) в различные местоположения.
Давайте создадим такой подкласс JLabel
и дадим ему вспомогательный метод для инициализации его состояния из заданного узла:
class ShapeRenderer extends JLabel
{
private static final long serialVersionUID = 1L;
public ShapeRenderer() {
}
public void initFrom( DrawNode node )
{
setIcon( new ImageIcon( node.getImage() ));
setSize( node.getBounds().getSize() );
}
// Methods below are overridden as a performance optimization:
@Override
public void invalidate() {
}
@Override
public void validate() {
}
@Override
public void revalidate() {
}
@Override
public void repaint( long tm, int x, int y, int width, int height ) {
}
@Override
public void repaint( Rectangle r ) {
}
@Override
public void repaint() {
}
@Override
protected void firePropertyChange( String propertyName, Object oldValue, Object newValue ) {
}
@Override
public void firePropertyChange( String propertyName, boolean oldValue, boolean newValue ) {
}
}
На данный момент у нас есть JComponent
слева, давайте создадим пользовательский. Мы дадим ему DrawPanelModel
, CellRendererPane
и наш ShapeRenderer
. Он собирается подписаться на модель в качестве слушателя, вызывая repaint(Rectangle)
в ответ на аннулирование модели в области всякий раз, когда узел перемещается. Помните, что repaint(...)
не рисует сразу, а скорее планирует событие рисования в будущем. Это означает, что мы можем на самом деле переместить несколько узлов, и среда Swing предоставит нам одно событие рисования, в котором объединение всех наших недопустимых прямоугольников будет определено как область клипа Graphic's
. Но нашему коду на самом деле все равно, объединяются ли недопустимые области или нет. Если фреймворк решит дать нам событие рисования для каждой из областей, которые мы аннулируем, мы также можем справиться с этим:
class DrawPanel extends JComponent
{
private static final long serialVersionUID = 1L;
private final CellRendererPane renderPane;
private final ShapeRenderer renderer;
private final DrawPanelModel model;
public DrawPanel()
{
renderPane = new CellRendererPane();
add( renderPane );
setDoubleBuffered( true );
renderer = new ShapeRenderer();
model = new DrawPanelModel( this::repaint );
DrawMouseListener listener = new DrawMouseListener();
addMouseListener( listener );
addMouseMotionListener( listener );
}
public void addNode( BufferedImage image, Point loc )
{
DrawNode node = new DrawNode();
node.setImage( image );
model.addNode( node );
model.setNodeBounds( node, new Rectangle( loc, new Dimension( image.getWidth(), image.getHeight() )));
}
@Override
public void doLayout()
{
renderPane.setSize( getSize() );
}
private void paintBackground( Graphics2D g )
{
g.setColor( Color.WHITE );
g.fill( g.getClip() );
}
private void paintNodes( Graphics2D g )
{
model.getNodesInRectangle( g.getClipBounds() )
.forEach( node -> paintNode( node, g ));
}
private void paintNode( DrawNode node, Graphics2D g )
{
Rectangle r = node.getBounds();
renderer.initFrom( node );
renderPane.paintComponent( g, renderer, this, r );
}
@Override
public void paintComponent( Graphics aG )
{
Graphics2D g = (Graphics2D)aG.create();
paintBackground( g );
paintNodes( g );
}
class DrawMouseListener extends MouseAdapter
{
private Optional<DrawNode> movingNode;
public DrawMouseListener()
{
movingNode = Optional.empty();
}
@Override
public void mousePressed( MouseEvent e )
{
movingNode = model.getNodeForPoint( e.getPoint() );
}
@Override
public void mouseReleased( MouseEvent e )
{
movingNode = Optional.empty();
}
@Override
public void mouseDragged( MouseEvent e )
{
movingNode.ifPresent( node -> {
model.setNodeLocation( node, e.getPoint() );
} );
}
}
}
Наконец, ручной тест:
@Test
public void testPanel() throws InvocationTargetException, InterruptedException
{
SwingUtilities.invokeLater( () -> {
// Create frame:
JFrame frame = new JFrame();
frame.setLayout( new GridLayout( 1, 1 ));
// Create draw panel:
DrawPanel drawPanel = new DrawPanel();
frame.add( drawPanel );
// Show frame:
frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
frame.setSize( new Dimension( 1000, 600 ));
frame.setVisible( true );
// Create first image:
BufferedImage image1 = new BufferedImage( 50, 50, BufferedImage.TYPE_INT_ARGB );
Graphics2D g = image1.createGraphics();
g.setColor( Color.BLUE );
g.fillOval( 0, 0, 50, 50 );
// Add first image to draw panel:
drawPanel.addNode( image1, new Point( 100, 100 ));
// Create second image:
BufferedImage image2 = new BufferedImage( 50, 50, BufferedImage.TYPE_INT_ARGB );
Graphics2D g2 = image2.createGraphics();
g2.setColor( Color.RED );
g2.fillOval( 0, 0, 50, 50 );
// Add second image to draw panel:
drawPanel.addNode( image2, new Point( 200, 100 ));
} );
Thread.sleep( Long.MAX_VALUE );
}
Результаты :