11

I am making a map editor for a game I am working on. There is a JPanel in the JScrollPane that displays the map to be edited. What I would like to do is make it that when the user is holding down the Spacebar and dragging their mouse in the JPanel, the JScrollPanel will scroll along with the dragging. Here is what I have so far:

panelMapPanel.addMouseMotionListener(new MouseMotionListener(){

        @Override
        public void mouseDragged(MouseEvent e) {
            //Gets difference in distance x and y from last time this listener was called
            int deltaX = mouseX - e.getX();
            int deltaY = mouseY - e.getY();
            mouseX = e.getX();
            mouseY = e.getY();
            if(spacePressed){
                //Scroll the scrollpane according to the distance travelled
                scrollPane.getVerticalScrollBar().setValue(scrollPane.getVerticalScrollBar().getValue() + deltaY);
                scrollPane.getHorizontalScrollBar().setValue(scrollPane.getHorizontalScrollBar().getValue() + deltaX);
            }
        }

});

Currently it works but the scrolling is not smooth at all. Moving the mouse a lot at a time is fine but doing small drags makes the scrollpane go berserk.

Any ideas how to improve this?

For those who enjoy a visual to help, here is the editor:

Map Editor

Addition Notes (Edit):

  • I have tried scrollPane.getViewport().setViewPosition(new Point(scrollPane.getViewport().getViewPosition().x + deltaX, scrollPane.getViewport().getViewPosition().y + deltaY));
  • The dragging is more fidgety when moving the mouse slowly, while big movements are more smooth
  • I tried using scrollRectToVisible without luck
vedi0boy
  • 1,030
  • 4
  • 11
  • 32
  • You need to change the `JViewport`'s viewable area or position – MadProgrammer Jul 01 '15 at 20:58
  • Will this automatically adjust the scrollbars? – vedi0boy Jul 01 '15 at 21:00
  • What method do I use to change the position? setBounds(), setAlignmentX() – vedi0boy Jul 01 '15 at 21:02
  • [`JViewport#setViewPosition`](http://docs.oracle.com/javase/7/docs/api/javax/swing/JViewport.html#setViewPosition(java.awt.Point)) would be a good start, but remember, this is the top/left corner of the viewable area. You might also have a look at the methods that `JScrollPane` provides or even just use [`JCompoint#scrollRectToVisible`](http://docs.oracle.com/javase/7/docs/api/javax/swing/JComponent.html#scrollRectToVisible(java.awt.Rectangle)) – MadProgrammer Jul 01 '15 at 21:04
  • Oh how did I not see that? :) I tried it, still is just as fidgety as before. Edit: Only difference is that now I can scroll out of bound :/ (Passed/below scrollbar max/min) – vedi0boy Jul 01 '15 at 21:06
  • 1
    I'd have a look at `scrollRectToVisible` and call it on the component where the `MouseListener` is installed, just remember, you will need to calculate the offset from the top/left point and the mouse point – MadProgrammer Jul 01 '15 at 21:08
  • I did that, actually it doesn't seem it matters since the cursor is a custom one with the point set to the center. I don't see how scrollRectToVisible will help? Is that just to avoid scroll out of bounds? thanks for helping by the way, I appreciate it :) Perhaps there is a way to edit how many times the listener is called, I think the issue might have to do with inaccuracy since the listener is not called very often and the scrolling does big jumps as a result. – vedi0boy Jul 01 '15 at 21:12
  • As a (kind of) [example](http://stackoverflow.com/questions/19778717/moving-a-view-port-over-a-larger-image-jlableljscrollpane/19779491#19779491) – MadProgrammer Jul 01 '15 at 21:25
  • Ok seems to make sense in theory but when I try it, it won't work (Unless I am doing it wrong). I tried this: `scrollPane.scrollRectToVisible(scrollPane.getViewport().getViewRect());` and `panelMapPanel.scrollRectToVisible(scrollPane.getViewport().getViewRect());`, they both have the same result as before. By the way `panelMapPanel` is the name of the JPanel that I draw the map image onto. – vedi0boy Jul 01 '15 at 21:31
  • Please edit your question to include a [complete example](http://stackoverflow.com/help/mcve) that shows your approach, for [example](http://stackoverflow.com/a/7203419/230513). – trashgod Jul 01 '15 at 22:25
  • @vedi0boy Having mucked about with this, the problem seems to come down to the `MouseMoitionListener` seeing the viewport position change as a drag event of some kind ... which accounts for the flickering ... – MadProgrammer Jul 01 '15 at 23:24

4 Answers4

17

Okay, that ended up been much simpler then I though it would be...

First, don't mess with the JViewport, instead, use JComponent#scrollRectToVisible directly on the component which is acting as the contents of the JScrollPane, onto which the MouseListener should be attached.

The following example simply calculates the difference between the point at which the user clicked and the amount they have dragged. It then applies this delta to the JViewport's viewRect and uses JComponent#scrollRectToVisible to update the viewable area, simple :)

enter image description here

public class Test {

    public static void main(String[] args) {
        new Test();
    }

    public Test() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                    ex.printStackTrace();
                }

                JFrame frame = new JFrame("Testing");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.add(new TestPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class TestPane extends JPanel {

        private JLabel map;

        public TestPane() {
            setLayout(new BorderLayout());
            try {
                map = new JLabel(new ImageIcon(ImageIO.read(new File("c:/treasuremap.jpg"))));
                map.setAutoscrolls(true);
                add(new JScrollPane(map));

                MouseAdapter ma = new MouseAdapter() {

                    private Point origin;

                    @Override
                    public void mousePressed(MouseEvent e) {
                        origin = new Point(e.getPoint());
                    }

                    @Override
                    public void mouseReleased(MouseEvent e) {
                    }

                    @Override
                    public void mouseDragged(MouseEvent e) {
                        if (origin != null) {
                            JViewport viewPort = (JViewport) SwingUtilities.getAncestorOfClass(JViewport.class, map);
                            if (viewPort != null) {
                                int deltaX = origin.x - e.getX();
                                int deltaY = origin.y - e.getY();

                                Rectangle view = viewPort.getViewRect();
                                view.x += deltaX;
                                view.y += deltaY;

                                map.scrollRectToVisible(view);
                            }
                        }
                    }

                };

                map.addMouseListener(ma);
                map.addMouseMotionListener(ma);
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }

        @Override
        public Dimension getPreferredSize() {
            return new Dimension(200, 200);
        }

    }

}
MadProgrammer
  • 343,457
  • 22
  • 230
  • 366
  • 1
    Yes, this solution did work but for the record, the only change I needed to make was adding the `origin` point when the mouse is pressed and calculating the deltaX and deltaY from that. that was the solution and I was still allowed to use the code I have in my example that changed the scrollbar scrolling. In the end, the main issue was that I was calculating the deltaX and deltaY by using the distance of mouse x and y from the last time mouse dragged was called, not from the beginning of the drag.Thank you for you help @MadProgrammer :) and everyone else. – vedi0boy Jul 02 '15 at 18:12
  • 2
    For my money, `scrollRectToVisible` is still a better solution, as you could use on a n-deepth child components without needing to translate the coordinates between different coordinate contexts, but that's just me ;) – MadProgrammer Jul 03 '15 at 00:55
  • 1
    Wow - a fully runnable example demonstrating the requested question and still a unsolicited downvote - oh, "It didn't work for me" - that doesn't make the answer wrong in the context of the question – MadProgrammer Aug 10 '20 at 21:33
5

I found this (very common) requirement surprisingly hard to solve. This is the stable solution we have had in production for probably over 10 years.

The accepted answer seems very tempting, but has usability glitches once you start to play with it (e.g. try to immediately drag to the lower right and then back, and you should notice that during the backward movement, no moving takes places for a long time).

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.event.MouseEvent;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JViewport;
import javax.swing.border.MatteBorder;
import javax.swing.event.MouseInputAdapter;

public class Mover extends MouseInputAdapter {
  public static void main(String[] args) {
    JFrame f = new JFrame();
    f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    f.setSize(200, 160);
    f.setLocationRelativeTo(null);
    f.setLayout(new BorderLayout());

    JScrollPane scrollPane = new JScrollPane();
    f.add(scrollPane, BorderLayout.CENTER);

    JPanel view = new JPanel();
    view.add(new JLabel("Some text"));
    view.setBorder(new MatteBorder(5, 5, 5, 5, Color.BLUE));
    view.setBackground(Color.WHITE);
    view.setPreferredSize(new Dimension(230, 200));
    new Mover(view);
    scrollPane.setViewportView(view);

    f.setVisible(true);
  }

  private JComponent m_view            = null;
  private Point      m_holdPointOnView = null;

  public Mover(JComponent view) {
    m_view = view;
    m_view.addMouseListener(this);
    m_view.addMouseMotionListener(this);
  }

  @Override
  public void mousePressed(MouseEvent e) {
    m_view.setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
    m_holdPointOnView = e.getPoint();
  }

  @Override
  public void mouseReleased(MouseEvent e) {
    m_view.setCursor(null);
  }

  @Override
  public void mouseDragged(MouseEvent e) {
    Point dragEventPoint = e.getPoint();
    JViewport viewport = (JViewport) m_view.getParent();
    Point viewPos = viewport.getViewPosition();
    int maxViewPosX = m_view.getWidth() - viewport.getWidth();
    int maxViewPosY = m_view.getHeight() - viewport.getHeight();

    if(m_view.getWidth() > viewport.getWidth()) {
      viewPos.x -= dragEventPoint.x - m_holdPointOnView.x;

      if(viewPos.x < 0) {
        viewPos.x = 0;
        m_holdPointOnView.x = dragEventPoint.x;
      }

      if(viewPos.x > maxViewPosX) {
        viewPos.x = maxViewPosX;
        m_holdPointOnView.x = dragEventPoint.x;
      }
    }

    if(m_view.getHeight() > viewport.getHeight()) {
      viewPos.y -= dragEventPoint.y - m_holdPointOnView.y;

      if(viewPos.y < 0) {
        viewPos.y = 0;
        m_holdPointOnView.y = dragEventPoint.y;
      }

      if(viewPos.y > maxViewPosY) {
        viewPos.y = maxViewPosY;
        m_holdPointOnView.y = dragEventPoint.y;
      }
    }

    viewport.setViewPosition(viewPos);
  }
}

demo

Reto Höhener
  • 5,419
  • 4
  • 39
  • 79
  • 1
    good stuff! worked for me after just `new DragMover(myJButton);` (Well, myJButton works as an image, but that's a different story). The code before that was: `var myJSP=new JScrollPane(myJButton); myJSP.setXXXScrollbarPolicy(...);` – 18446744073709551615 Mar 03 '20 at 17:23
0

I'm currently working on a map editor myself. I have gotten mouse scrolling to work smoothly on mine although it is a pretty verbose solution.

I wrote two custom AWTEventListeners one for mouse events the other for mouse move events. I did this because my map is a custom JComponent and as such does not fill the entire view-port. This means that scroll pane mouse events wont be detected if the cursor is over the component.

For me this works very smoothly, the content scrolls in perfect lock-step with the mouse cursor.

(I should mention I use the mouse wheel click and not the space bar but it's easy to change).

    Toolkit.getDefaultToolkit().addAWTEventListener(new AWTEventListener() {
        public void eventDispatched(AWTEvent event) {
            if(event instanceof MouseEvent){
                MouseEvent e = (MouseEvent)event;
                //Begin a scroll if mouse is clicked on our pane
                if(isMouseInMapPane()){
                    if(e.getID() == MouseEvent.MOUSE_PRESSED){
                        if(e.getButton() == MouseEvent.BUTTON2){
                            mouseWheelDown = true;
                            currentX = MouseInfo.getPointerInfo().getLocation().x;
                            currentY = MouseInfo.getPointerInfo().getLocation().y;
                        }
                    }
                }
                //Stop the scroll if mouse is released ANYWHERE
                if(e.getID() == MouseEvent.MOUSE_RELEASED){
                    if(e.getButton() == MouseEvent.BUTTON2){
                        mouseWheelDown = false;
                    }
                }
            }
        }
    }, AWTEvent.MOUSE_EVENT_MASK);

    Toolkit.getDefaultToolkit().addAWTEventListener(new AWTEventListener() {
        public void eventDispatched(AWTEvent event) {
            if(event instanceof MouseEvent){
                MouseEvent e = (MouseEvent)event;

                //Update the scroll based on delta drag value
                if(e.getID() == MouseEvent.MOUSE_DRAGGED){
                    if(mouseWheelDown){
                        int newX = MouseInfo.getPointerInfo().getLocation().x;
                        int newY = MouseInfo.getPointerInfo().getLocation().y;
                        int scrollStepX = (currentX - newX);
                        int scrollStepY = (currentY - newY);
                        currentX = newX;
                        currentY = newY;

                        //mapScroll is the reference to JScrollPane
                        int originalValX = mapScroll.getHorizontalScrollBar().getValue();
                        mapScroll.getHorizontalScrollBar().setValue(originalValX + scrollStepX);

                        int originalValY = mapScroll.getVerticalScrollBar().getValue();
                        mapScroll.getVerticalScrollBar().setValue(originalValY + scrollStepY);
                    }
                }

            }
        }
    }, AWTEvent.MOUSE_MOTION_EVENT_MASK);

This is the isMouseInPane method:

    private boolean isMouseInMapPane(){
    //Note: mapPane does not need to be your scroll pane.
    //it can be an encapsulating container as long as it is in
    //the same position and the same width/height as your scrollPane.
    //For me I used the JPanel containing my scroll pane.
    Rectangle paneBounds = mapPane.getBounds();
    paneBounds.setLocation(mapPane.getLocationOnScreen());
    boolean inside = paneBounds.contains(MouseInfo.getPointerInfo().getLocation());

    return inside;
}

This code can be placed anywhere that you have access to your scroll pane reference or you could create a custom scroll pane class and add it there.

I hope it helps!

0

I've come up to solution as below (the method above didn't work for me, JDK 1.8):

  1. Attach the MouseDragged event to your JScrollPane;
  2. The event function is fired twice, on the start and at the end of the drag;
  3. You'll need a global variables to store initial mouse pointer position (xS and yS);
  4. Here's the code for your MouseDragged:
yourJScrollPaneMouseDragged(java.awt.event.MouseEvent evt) {                                        
        Rectangle view = yourJScrollPane.getVisibleRect();
        if (xS == 0 && yS == 0) { // first time event fired, store the initial mouse position
            xS = evt.getX();
            yS = evt.getY();
        } else {                  // second time event fired - actual scrolling
            int speed = 20;
            view.x += Integer.signum(xS - evt.getX()) * speed;
            view.y += Integer.signum(yS - evt.getY()) * speed;
                  // The view is scrolled by constant value of 20.
                  // For some reason, periodically, second position values were off for me by alot,
                  // which caused unwanted jumps.
                  // Integer.signum gets the direction the movement was performed.
                  // You can ommit the signum and constant and
                  // check if it works for you without jagging.

            yourJScrollPane.getViewport().scrollRectToVisible(view);
              // you actually have to fire scrollRectToVisible with the child 
              // component within JScrollPane, Viewport is the top child
            
            // reset globals:
            xS = 0;
            yS = 0;
        }
}      
vic
  • 1
  • 1
  • 2