问题
I have the following "tree" of objects:
JPanel
JScrollPane
JPanel
JPanel
JScrollPane
JTextPane
When using the mouse wheel to scroll over the outer JScrollPane I encounter one annoying problem. As soon as the mouse cursor touches the inner JScrollPane, it seems that the scrolling events get passed into that JScrollPane and are not processed anymore by the first one. That means that scrolling the "parent" JScrollPane stops.
Is it possible to disable only the mouse wheel on the inner JScrollPane? Or even better, disable scrolling if there is nothing to scroll (most of the time the textpane only contains 1-3 lines of text), but enable it if there is more content?
回答1:
I have run into this annoying problem also, and Sbodd's solution was not acceptable for me because I needed to be able to scroll inside tables and JTextAreas. I wanted the behavior to be the same as a browser, where the mouse over a scrollable control will scroll that control until the control bottoms out, then continue to scroll the parent scrollpane, usually the scrollpane for the whole page.
This class will do just that. Just use it in place of a regular JScrollPane. I hope it helps you.
/**
* A JScrollPane that will bubble a mouse wheel scroll event to the parent
* JScrollPane if one exists when this scrollpane either tops out or bottoms out.
*/
public class PDControlScrollPane extends JScrollPane {
public PDControlScrollPane() {
super();
addMouseWheelListener(new PDMouseWheelListener());
}
class PDMouseWheelListener implements MouseWheelListener {
private JScrollBar bar;
private int previousValue = 0;
private JScrollPane parentScrollPane;
private JScrollPane getParentScrollPane() {
if (parentScrollPane == null) {
Component parent = getParent();
while (!(parent instanceof JScrollPane) && parent != null) {
parent = parent.getParent();
}
parentScrollPane = (JScrollPane)parent;
}
return parentScrollPane;
}
public PDMouseWheelListener() {
bar = PDControlScrollPane.this.getVerticalScrollBar();
}
public void mouseWheelMoved(MouseWheelEvent e) {
JScrollPane parent = getParentScrollPane();
if (parent != null) {
/*
* Only dispatch if we have reached top/bottom on previous scroll
*/
if (e.getWheelRotation() < 0) {
if (bar.getValue() == 0 && previousValue == 0) {
parent.dispatchEvent(cloneEvent(e));
}
} else {
if (bar.getValue() == getMax() && previousValue == getMax()) {
parent.dispatchEvent(cloneEvent(e));
}
}
previousValue = bar.getValue();
}
/*
* If parent scrollpane doesn't exist, remove this as a listener.
* We have to defer this till now (vs doing it in constructor)
* because in the constructor this item has no parent yet.
*/
else {
PDControlScrollPane.this.removeMouseWheelListener(this);
}
}
private int getMax() {
return bar.getMaximum() - bar.getVisibleAmount();
}
private MouseWheelEvent cloneEvent(MouseWheelEvent e) {
return new MouseWheelEvent(getParentScrollPane(), e.getID(), e
.getWhen(), e.getModifiers(), 1, 1, e
.getClickCount(), false, e.getScrollType(), e
.getScrollAmount(), e.getWheelRotation());
}
}
}
回答2:
Sadly, the obvious solution (JScrollPane.setWheelScrollingEnabled(false)) doesn't actually deregister for MouseWheelEvents, so it doesn't achieve the effect you want.
Here's a crude-hackery way of disabling scrolling altogether that will let the MouseWheelEvents reach the outer JScrollPane:
for (MouseWheelListener mwl : scrollPane.getMouseWheelListeners()) {
scrollPane.removeMouseWheelListener(mwl);
}
If you do this to your inner JScrollPane, it'll never respond to scroll wheel events; the outer JScrollPane will get all of them.
If you want to do it "cleanly", you'd need to implement your own ScrollPaneUI, and set that as the JScrollPane's UI with setUI(). Unfortunately, you can't just extend BasicScrollPaneUI and disable its mouse wheel listener, because the relevant member variables are private and there aren't any flags or guards on the ScrollPaneUI's installation of its MouseWheelListener.
For your "even better" solution, you'd have to dig deeper than I have time to into the ScrollPaneUI, find the hooks where the scrollbars get made visible / invisible, and add/remove your MouseWheelListener at those points.
Hope that helps!
回答3:
@Nemi has a good solution already.
I boiled it down a bit further, putting the follwing method in my library:
static public void passMouseWheelEventsToParent(final Component pComponent, final Component pParent) {
pComponent.addMouseWheelListener((final MouseWheelEvent pE) -> {
pParent.dispatchEvent(new MouseWheelEvent(pParent, pE.getID(), pE.getWhen(), pE.getModifiers(), 1, 1, pE.getClickCount(), false, pE.getScrollType(), pE.getScrollAmount(), pE.getWheelRotation()));
});
}
回答4:
Inspired by the existing answers, I
- took the code from Nemi's answer
- combined it with kleopatra's answer to a similar question to avoid constructing the
MouseWheelEvent
verbosely - extracted the listener into its own top-level class so that it can be used in contexts where the
JScrollPane
class cannot be extended - inlined the code as far as possible.
The result is this piece of code:
import java.awt.Component;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import javax.swing.JScrollBar;
import javax.swing.JScrollPane;
import javax.swing.SwingUtilities;
/**
* Passes mouse wheel events to the parent component if this component
* cannot scroll further in the given direction.
* <p>
* This behavior is a little better than Swing's default behavior but
* still worse than the behavior of Google Chrome, which remembers the
* currently scrolling component and sticks to it until a timeout happens.
*
* @see <a href="https://stackoverflow.com/a/53687022">Stack Overflow</a>
*/
public final class MouseWheelScrollListener implements MouseWheelListener {
private final JScrollPane pane;
private int previousValue;
public MouseWheelScrollListener(JScrollPane pane) {
this.pane = pane;
previousValue = pane.getVerticalScrollBar().getValue();
}
public void mouseWheelMoved(MouseWheelEvent e) {
Component parent = pane.getParent();
while (!(parent instanceof JScrollPane)) {
if (parent == null) {
return;
}
parent = parent.getParent();
}
JScrollBar bar = pane.getVerticalScrollBar();
int limit = e.getWheelRotation() < 0 ? 0 : bar.getMaximum() - bar.getVisibleAmount();
if (previousValue == limit && bar.getValue() == limit) {
parent.dispatchEvent(SwingUtilities.convertMouseEvent(pane, e, parent));
}
previousValue = bar.getValue();
}
}
It is used like this:
JScrollPane pane = new JScrollPane();
pane.addMouseWheelListener(new MouseWheelScrollListener(pane));
Once an instance of this class is created and bound to a scroll pane, it cannot be reused for another component since it remembers the previous position of the vertical scroll bar.
来源:https://stackoverflow.com/questions/1377887/jtextpane-prevents-scrolling-in-the-parent-jscrollpane