Перетаскиваемые фигуры на буферизованном изображении - PullRequest
0 голосов
/ 23 апреля 2020

Я разрабатываю простой редактор форм в Java, я рисовал все на BufferedImage, так как я хочу сохранить нарисованное изображение. Пользователь может рисовать фигуры, выбирая фигуру из меню, а затем нажимая на изображение. Я начал реализовывать перетаскивание фигур. Когда я перетаскиваю фигуру, создается путь этой фигуры ... Я предполагаю, что это потому, что я рисую на BufferedImage, так что это похоже на "временный". Или я не прав? Я думал о переписывании всего, на этот раз не рисуя на BufferedImage, а просто создавая Graphics2D, но у меня проблемы с реализацией слушателей, поскольку мне нужно перенести туда Graphics2D для добавления фигур, также я не знаю, как вызвать repaint в течение, например. мой класс MouseListener. Я планировал создать BufferedImage, когда пользователь хочет сохранить изображение, я бы создал его с помощью вектора, где я храню все фигуры (или это лучший способ?). Вот мой класс Panel:

public class Panel {

    public static Graphics2D img2;

    public static Graphics2D getIm() {
        return this.img2;
    }

    public Panel(JFrame frame) {

        JPanel panel = new JPanel(true) {

            @Override
            protected void paintComponent(Graphics g) {

                super.paintComponent(g);


                img2 = (Graphics2D)g.create();  
                img2.dispose();
            };
        };
        RenderingHints.VALUE_ANTIALIAS_ON); // setting rendering to achieve better looking shapes
        panel.setBackground(Color.WHITE);
        MouseListenerShapes mouseListenerShapes = new MouseListenerShapes();
        panel.addMouseListener(mouseListenerShapes);
        //MouseMoveAdapter mouseMouseMoveAdapter = new MouseMoveAdapter();
        //panel.addMouseMotionListener(mouseMouseMoveAdapter);
        //panel.addMouseListener(mouseMouseMoveAdapter);
        frame.add(panel);
    };
}

Я много читал, но все еще не могу сделать это правильно, мне нужно, чтобы мои фигуры были изменяемого размера, подвижными и т.д. c. но в то же время я хочу сохранить изображение потом,

1 Ответ

1 голос
/ 23 апреля 2020

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

Я настоятельно рекомендую разделить ваш слой рендеринга (ваш 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 );
}

Результаты :

enter image description here

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...