文章目录
- 1. 监听器 - MouseListener(鼠标监听器)
- 2. 监听器 - MouseMotionListener(鼠标运动监听器)
- 3. 事件 - MouseEvent(鼠标事件)
- 4. 示例
- 5. 适配器
1. 监听器 - MouseListener(鼠标监听器)
在控件上调用 addMouseListener() 以添加,或是 removeMouseListener() 以移除此监听器对象。
这两个方法都是在 JComponent 类中定义的,故鼠标监听器适用于 JComponent 及其派生类。
MouseListener 是用于监听 鼠标对控件进行的操作 的,共有以下 5 5 5 个方法:
public void mouseClicked(MouseEvent e); //鼠标点击
public void mousePressed(MouseEvent e); //鼠标按下
public void mouseReleased(MouseEvent e); //鼠标松开
public void mouseEntered(MouseEvent e); //鼠标浮动
public void mouseExited(MouseEvent e); //鼠标离开
事实上,前三个方法的触发,不考虑 按下/松开的是哪个键、是否属于一个双击等。
即只要发生了一次如注释所描述的事件,相应方法就会被触发一次。
“鼠标浮动”,指的是鼠标移动到控件上、但没有点击的时候。
严肃点应该叫“进入”(大多数博客也是这么写的),但我个人感觉“浮动”更形象。
1.1 注意事项 1:瞬时行为
rt,“鼠标浮动”、“鼠标离开”等描述,指的并非一种长时间的状态,而是 一瞬间的状态的改变。
(有点像英语中 wear 和 put on 的区别,而上述方法 均属后者)
废话不多说,上丑图:
1.2 注意事项 2:事件的触发
这次的关注点在鼠标监听器的前三个方法。
还记得吗?“鼠标按下”、“鼠标松开”这样的方法言简意赅,没什么歧义。于是问题就出在:“鼠标点击”到底是指什么?
其实仍然可以用生活中的经验来感性理解。平时我们要是说“点一下这个”,意思不就是“先按下去、再松开来”吗?
事实上,“鼠标点击”有点像是对前两者的“总结”。当一次“鼠标按下”加“鼠标松开”依次被触发后,紧接着就会调用“鼠标点击”事件。
鼠标点击 { 鼠标按下 鼠标松开 鼠标点击\begin{cases}鼠标按下\\鼠标松开\end{cases} 鼠标点击{鼠标按下鼠标松开
至于它们的触发顺序:
图中,左侧的事件有点像是右侧事件的“先决条件”,即“有左才有右”,一步一步向右触发。
下面是一些小例子 1:
- 鼠标在别处按下,“拖到”控件中再松开。
似乎应当触发一个“鼠标松开”事件。不过根据上述,由于并没有在控件中触发“鼠标按下”,自然也不会有“鼠标松开”。最后结果是并未触发这 3 3 3 个事件中任意一个(如下动图所示):
- 鼠标在控件中按下,“拖到”别处再松开。
由于没有在控件中进行“鼠标松开”,自然也不会有“鼠标点击”。然而很奇怪,最终触发“鼠标按下”与“鼠标松开”(如下动图所示):
1.3 示例代码
1.1 和 1.2 节中的动图示例就是使用的如下代码:
import java.awt.BorderLayout;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
public class DemoFrame {
public static void main(String[] args) {
JFrame frame = new JFrame("示例-1.3");
JButton button = new JButton("I am a button"); //控件,后面将为它添加鼠标监听器
JTextArea console = new JTextArea(9, 16); //用于显示输出的“控制台”
JScrollPane panel = new JScrollPane(console); //防止输出过长,为“控制台”添加滚动条
console.setEditable(false); //禁止修改文本
console.setFocusable(false); //禁止框选文本
button.addMouseListener(new MouseListener() { //用于观察各个方法的触发
@Override public void mouseClicked(MouseEvent e) { console.append("Mouse Clicked!\n"); }
@Override public void mousePressed(MouseEvent e) { console.append("Mouse Pressed!\n"); }
@Override public void mouseReleased(MouseEvent e) { console.append("Mouse Released!\n"); }
@Override public void mouseEntered(MouseEvent e) { console.append("Mouse Entered!\n"); }
@Override public void mouseExited(MouseEvent e) { console.append("Mouse Exited!\n"); }
});
//测试代码:
frame.setSize(480, 270);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(button, BorderLayout.CENTER);
frame.add(panel, BorderLayout.SOUTH);
frame.setVisible(true);
}
}
效果就不用再录张图了吧?上面已经有那么多了。
2. 监听器 - MouseMotionListener(鼠标运动监听器)
此监听器同样适用于 JComponent 及其派生类。
虽然 MouseMotionListener 也是个监听器,但 Swing 并没有提供“MouseMotionEvent”这样的事件,所以 MouseListener 和 MouseMotionListener 都是用的 MouseEvent 类。(详见后文)
方法介绍:
public void mouseMoved(MouseEvent e); //鼠标移动
public void mouseDragged(MouseEvent e); //鼠标拖动
这里的“鼠标移动”是狭义的,也就是它并不包含“鼠标拖动”,因此这两个事件是“互斥”的。
3. 事件 - MouseEvent(鼠标事件)
3.0 链接:官方文档
没找到想看内容或没看懂的戳这里:
- JDK17 在线文档(可以把路径中的 17 17 17 改为自己的版本号,但需大于等于 11 11 11)。
- 离线文档下载:JDK17 或 JDK8(可以把路径中的 17 17 17 或 8 8 8 改为自己的版本号)。
要是所有人都懂得自己翻文档,你就不会看到这篇博客了。(bushi)
3.1 getXOnScreen()、getYOnScreen()、getLocationOnScreen()
返回事件发生时鼠标的绝对坐标。
“绝对坐标”指相对于整个电脑屏幕的坐标。以左上角为原点。
public int getXOnScreen(); //X 坐标,非负整数(毕竟鼠标又不能跑出屏幕)
public int getYOnScreen(); //Y 坐标,同上
public Point getLocationOnScreen(); //X 和 Y 坐标包成一块返回
其中的 getLocationOnScreen() 方法返回 java.awt.Point 类型,定义如下:
public int x, y;
闲话:注意到它们既没有被 final 关键字修饰、也缺少 synchronized 的 Getter、Setter 方法,因此 Point 类是可变的。(也许因为 Swing 中广泛地使用线程封闭技术)
3.1.1 示例:输出鼠标指针的绝对坐标
log.info(e.getLocationOnScreen());
//大多数 Logger 会自动调用 Point 类的 toString 方法
//例:java.awt.Point[x=0,y=0]
out.printf("(%d, %d)\n", e.getXOnScreen(), e.getYOnScreen());
//用 printf 格式化,推荐方式
//大多数 Logger 肯定也会提供类似 printf 的输出方式,如 log4j
3.2 getX()、getY()、getPoint()
事件发生时鼠标相对于此控件的坐标。
相对坐标就不多说了吧,你把绝对坐标看成相对坐标的特例(以整个屏幕为参照系),类比一下即可。
public int getX(); //X 坐标,注意可能出现负数
public int getY(); //Y 坐标,同上
public java.awt.Point getPoint(); //返回鼠标指针的相对坐标
3.3 getClickCount()
仅在 MouseListener 的前三个事件(“鼠标按下”、“鼠标松开”、“鼠标点击”)中,返回值有意义。
public int getClickCount(); //返回点击的次数(不局限于 1~2)
按下不同键不构成双击。例如 1,按顺序快速点击左键与右键,则两次都返回
1
1
1。
如果是在没有鼠标点击的事件中调用此方法(“鼠标浮动”等),则返回
0
0
0。
注意,此方法的返回值并不局限于
0
0
0、
1
1
1 和
2
2
2,如果连续点击
3
3
3 次,则 getClickCount() 就为
3
3
3。
3.4 getButton()
同理,用于“鼠标按下”、“鼠标松开”和“鼠标点击”事件:
public int getButton(); //返回触发事件的鼠标按钮的编号(常量)
//鼠标按钮常量(定义于 MouseEvent 中):
public static final int NOBUTTON = 0; //没有按下鼠标按键(例如“鼠标浮动”等事件)
public static final int BUTTON1 = 1; //鼠标左键
public static final int BUTTON2 = 2; //鼠标中键
public static final int BUTTON3 = 3; //鼠标右键
鼠标中键其实就是按下滚轮
(打游戏的各位肯定都懂)。
如果你的鼠标有三个以上的按钮,则在 MouseEvent 中找不到对应的常量。比如你的鼠标有 5 5 5 个按钮,则可能返回以下数值:
数值 | 对应常量 | 鼠标按键 |
---|---|---|
0 0 0 | j a v a . a w t . e v e n t . M o u s e E v e n t . N O B U T T O N \mathsf{java.awt.event.MouseEvent.NOBUTTON} java.awt.event.MouseEvent.NOBUTTON | (无) |
1 1 1 | j a v a . a w t . e v e n t . M o u s e E v e n t . B U T T O N 1 \mathsf{java.awt.event.MouseEvent.BUTTON1} java.awt.event.MouseEvent.BUTTON1 | 左键 |
2 2 2 | j a v a . a w t . e v e n t . M o u s e E v e n t . B U T T O N 2 \mathsf{java.awt.event.MouseEvent.BUTTON2} java.awt.event.MouseEvent.BUTTON2 | 中键 |
3 3 3 | j a v a . a w t . e v e n t . M o u s e E v e n t . B U T T O N 3 \mathsf{java.awt.event.MouseEvent.BUTTON3} java.awt.event.MouseEvent.BUTTON3 | 右键 |
4 4 4 | (无) | (第 4 4 4 个按键) |
5 5 5 | (无) | (第 5 5 5 个按键) |
3.4.1 示例:检测按下的是什么键
用 If-else 语句:
if (e.getButton() == MonseEvent.BUTTON1)
return "鼠标左键";
else if (e.getButton() == MonseEvent.BUTTON2)
return "鼠标中键";
else if (e.getButton() == MonseEvent.BUTTON3)
return "鼠标右键";
else
return "按键 " + e.getButton();
或是用 Switch 语句:
switch (e.getButton()) {
case MouseEvent.BUTTON1:
return "鼠标左键";
case MouseEvent.BUTTON2:
return "鼠标中键";
case MouseEvent.BUTTON3:
return "鼠标右键";
default:
return "按键 " + e.getButton();
}
3.5 getWhen()
顾名思义,返回事件所发生的时间,以 UTC 时间 为标准。
public long getWhen();
//返回事件发生的时间戳(1970 年 1 月 1 日 00:00 至今的毫秒数)
3.5.1 示例:输出事件发生时间(不同格式)
平时可以用 Date 类进行快捷地处理,如果需要还可以用 SimpleDateFormat 类进行可定制的处理:
import java.util.Date;
import java.text.SimpleDateFormat;
log.info(e.getWhen());
//直接打印未经处理的毫秒数,如:“1675421707077”(没人这样干)
//使用 Date 类:
log.info(new Date(e.getWhen()));
//大多数 Logger 会自动调用 Date 类的 toString() 方法
//如:“Fri Feb 03 18:55:07 CST 2023”
//使用 SimpleDateFormat 进行可定制的格式化:
final SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
log.info(formatter.format(e.getWhen())); //如:“2023-02-03 18:55:07”
甚至还可以这样:
out.printf("%tc", e.getWhen());
。
示例输出:周五 2月 03 18:55:07 CST 2023
3.6 paramString()
把上述几乎所有方法全塞到一个字符串里扔给你。
示例输出:
MOUSE_PRESSED,(52,93),absolute(59,163),button=1,modifiers=Ctrl+Button1,extModifiers=Ctrl,clickCount=2
看到这里,你可能会说:啊?
……
好吧,正常一点:
示例输出 | 通用输出 | 简要解释 | 参见章节 |
---|---|---|---|
MOUSE_PRESSED | %s | 触发的事件类型 | 3.9 3.9 3.9 |
(52,93) | (%d,%d) | 触发事件时,鼠标相对于此组件的坐标 | 3.2 3.2 3.2 |
absolute(59,163) | absolute(%d,%d) | 触发事件时,鼠标相对于屏幕的坐标 | 3.1 3.1 3.1 |
button=1 | button=%d | 触发事件的鼠标按键 | 3.4 3.4 3.4 |
modifiers=Ctrl+Button1 | modifiers=%s | 触发事件的修饰符 | 3.7 3.7 3.7 |
extModifiers=Ctrl | extModifiers=%s | 触发事件的修饰符 | 3.8 3.8 3.8 |
clickCount=2 | clickCount=%d | combo 的次数 | 3.3 3.3 3.3 |
3.7 getModifiers()、getMouseModifiersText()
注意:getModifiers() 方法已被官方打上 Deprecated 注解,被 getModifiersEx() 取代。
getModifiers() 方法返回事件的修饰符,常量在 InputEvent 中定义,用位或方式结合。
例如,我按住 Ctrl 键用左键触发事件,返回的修饰符如下:
e.getModifiers() == InputEvent.CTRL_MASK | InputEvent.BUTTON1_MASK;
之所以被废除,个人猜测是因为有时会出现奇怪的 BUG(也许和跨平台有关):
//例如我按下鼠标中键,它返回这个:
InputEvent.ALT_MASK | InputEvent.BUTTON2_MASK //可实际上我并没有按 Alt
再说说静态方法 getMouseModifiersText() 吧。
public static String getMouseModifiersText(int modifiers);
//将 getModifiers() 返回的修饰符翻译成字符串
//比如这个:
InputEvent.CTRL_MASK | InputEvent.BUTTON1_MASK
//它会返回:
"Ctrl+Button1"
3.8 getModifiersEx()、getModifiersExText()
静态方法 getModifiersExText() 在 InputEvent 类中定义。
上一节中 getModifiers() 和 getMouseModifiersText() 的替代品。
注意:上一节所使用的常量为 XXX_MASK
,这节所介绍的方法所使用的常量为 XXX_DOWN_MASK
。
getModifiersEx() 同样有一些奇怪的 BUG(特性?),我把它分为 2 2 2 种情况(测试于台式机,Win10):
一、BUG(特性?)发作,不仅有上一节中介绍的凭空多出按键,而且附赠忽略鼠标按钮。还是上一节中的范例:
//我按下鼠标中键:
InputEvent.BUTTON2_DOWN_MASK //我认为它应该返回这个
InputEvent.ALT_DOWN_MASK //结果事实让我震惊了
//(它先幻想了一个 Alt,然后又把 Button2 给忽略掉了)
二、奇奇怪怪的修饰符也不加了,按钮也不忽略了,按下哪个就返回哪个。总之就突然恢复正常了。如:
//我按下 Ctrl + Alt + 左键,它正常返回:
InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK | InputEvent.BUTTON1_DOWN_MASK
就挺玄学的,我在不同事件中测试,结果如下:
监听器 | 事件 | 状态 |
---|---|---|
MouseListener | mousePressed | 正常返回 |
MouseListener | mouseReleased | 离谱特性 |
MouseListener | mouseClicked | 离谱特性 |
MouseMotionListener | mouseDragged | 正常返回 |
其他事件则与鼠标点击无关。
至于 getModifiersExText(),则和上一节中的 getMouseModifiers() 作用相同。
但 getModifiersExText() 用的是 XXX_DOWN_MASK
,getModifiersText() 用的 XXX_MASK
,所以最好不要混起来用。即上一节中的两个方法配套使用,这一节的两个方法也只能配套使用。
3.9 getID()
返回触发的事件(int
类型常量),用于编写静态方法(工具)时使用(来自文档)。
可用的常量如下:
常量名称 | 事件 | 值 |
---|---|---|
MOUSE_PRESSED | mousePressed(鼠标按下) | 501 |
MOUSE_RELEASED | mouseReleased(鼠标松开) | 502 |
MOUSE_CLICKED | mouseClicked(鼠标点击) | 500 |
MOUSE_ENTERED | mouseEntered(鼠标浮动) | 504 |
MOUSE_EXITED | mouseExited(鼠标离开) | 505 |
MOUSE_MOVED | mouseMoved(鼠标移动) | 503 |
MOUSE_DRAGGED | mouseDragged(鼠标拖动) | 506 |
还有一个较为特殊,这个是属于 MouseWheelListener(鼠标滚轮监听器)的:
常量名称 | 事件 | 值 |
---|---|---|
MOUSE_WHEEL | mouseWheelMoved(鼠标滚轮滚动) | 507 |
3.9.1 示例:id --> 字符串
由于常量的值是 int
类型,要转化为字符串,可以用 Switch 或 If-else 暴力检测,也可以用反射获取常量名。
反正 Switch、If-else 就是疯狂复制粘贴,这里就贴一个简短的反射代码吧(尽管几乎没有实用意义
Q
A
Q
QAQ
QAQ):
public static String getEventName(final int id)
throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException {
Class<?> c = MouseWheelEvent.class; //MouseWheelEvent 包含上表中所有常量
Field first = c.getField("MOUSE_FIRST"), last = c.getField("MOUSE_LAST"); //跟 MouseEvent 类的一些设计有关,详情见下
if (first.getInt(null) <= id && id <= last.getInt(null)) //常量是否存在
for (Field field : c.getFields())
if (field.get(null) instanceof Integer
&& field.getInt(null) == id //如果检测到了 int 常量且值相等
&& !field.equals(first) && !field.equals(last))
return field.getName();
throw new IllegalArgumentException("The id is .");
}
/****** 关于上面的 first 和 last: ******/
//In MouseEvent.class:
public static final int MOUSE_FIRST = 500; //注意,和 MOUSE_CLICKED 的值相等
public static final int MOUSE_LAST = 507; //和 MOUSE_WHEEL_MOVED 的值相等
你可能注意到,这里顺带提了两个于 MouseEvent 中定义的很奇怪的常量,也就是 MOUSE_FIRST 和 MOUSE_LAST。
顾名思义,它们跟鼠标什么的没什么关系,而是描述了所有事件常量的值的上下界,即上述所有事件常量的值刚好不重不漏取完 [ M O U S E _ F I R S T , M O U S E _ L A S T ] [\mathrm{MOUSE\_FIRST}, \mathrm{MOUSE\_LAST}] [MOUSE_FIRST,MOUSE_LAST]。
至于作用,可以参考代码的第 6 6 6 行,即用于检测给定 id 的合法性。
3.10 getSource()
此方法返回触发事件的对象,即贯穿全文的所谓“控件”。
返回类型为 Object,所以需要进行显式的向下转型。
public Object getSource(); //返回触发事件的对象
常用于不同控件添加同一个监听器对象,还有自己的静态方法(工具)中使用。(来自文档)
3.10.1 示例:输出点击的按钮名称
就是上面说的不同按钮添加同一个监听器对象:
public static void main(String[] args) {
MouseListener ml = new MouseAdapter() {
//由于只实现了一个 mouseClicked 方法,使用鼠标适配器(参见 5.1 节)
@Override
public void mouseClicked(MouseEvent e) {
JButton source = (JButton) e.getSource(); //获取触发的按钮对象
System.out.printf("Clicked button: \"%s\".\n", source.getText()); //输出按钮名
}
};
//测试代码:
JButton button1 = new JButton("Button-1"),
button2 = new JButton("Button-2");
button1.addMouseListener(ml); //添加同一个监听器
button2.addMouseListener(ml);
JFrame frame = new JFrame("示例-3.10.1");
frame.setSize(240, 180);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLayout(new GridLayout(1, 2)); //这里用网格布局管理器,单行两列
frame.add(button1);
frame.add(button2);
frame.setVisible(true);
}
4. 示例
4.1 实时显示鼠标坐标
实时显示鼠标相对于 JFrame 窗口的坐标。
这是 MouseMotionListener 监听器的示例:
public static void main(String[] args) {
JFrame frame = new JFrame("示例-4.1");
JLabel label = new JLabel(); //标签用于显示鼠标坐标
frame.addMouseMotionListener(new MouseMotionListener() {
@Override
public void mouseMoved(MouseEvent e) {
label.setText(String.format("X=%d, Y=%d", e.getX(), e.getY()));
}
@Override
public void mouseDragged(MouseEvent e) {
label.setText(String.format("X=%d, Y=%d", e.getX(), e.getY()));
}
});
//测试代码:
frame.setSize(240, 180);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(label);
frame.setVisible(true);
}
效果类似这样:
4.2 鼠标拖动 JLabel
利用 MouseMotionListener,在鼠标拖动时让 JLabel 的坐标保持和鼠标同步移动,实现拖动效果。1
import java.awt.Color;
import java.awt.Point;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import javax.swing.BorderFactory;
import javax.swing.JFrame;
import javax.swing.JLabel;
@SuppressWarnings("serial")
public class DemoFrame extends JFrame {
private Point mousePos; //跟踪鼠标坐标
private JLabel label = new JLabel("I am a label.", JLabel.CENTER);
public DemoFrame(String title, int width, int height) {
this.setTitle(title);
this.setSize(width, height);
this.setDefaultCloseOperation(EXIT_ON_CLOSE);
this.setLayout(null); //默认布局管理器,直接控制组件的坐标
this.add(label);
label.setSize(width / 4, height / 8);
label.setBorder(BorderFactory.createLineBorder(Color.BLACK, 2)); //黑色边框,便于拖动观察
label.addMouseMotionListener(new MouseMotionListener() {
@Override
public void mouseMoved(MouseEvent e) //实时记录鼠标坐标
{ mousePos = e.getLocationOnScreen(); }
@Override
public void mouseDragged(MouseEvent e) {
Point offset = new Point(e.getXOnScreen() - mousePos.x, e.getYOnScreen() - mousePos.y); //计算鼠标坐标偏移量
label.setBounds(label.getX() + offset.x, label.getY() + offset.y, label.getWidth(), label.getHeight()); //跟着鼠标移动 JLabel
mousePos = e.getLocationOnScreen(); //更新鼠标坐标
}
});
}
public static void main(String[] arg)
{ new DemoFrame("示例-4.2", 480, 360).setVisible(true); }
}
5. 适配器
MouseListener、MouseMotionListener 监听器的适配器都是 MouseAdapter,它同时也是 MouseWheelListener(鼠标滚轮监听器)的适配器(它同时导出自这 3 3 3 个监听器接口)。
适配器:例如,你想监听某个控件的“鼠标点击”事件,尽管你只需要 mouseClicked() 这一个方法,但由于 MouseListener 是一个接口,你还必须要实现它的其他方法:
component.addMouseListener(new MouseListener() { @Override public void mouseClicked(MouseEvent e) { System.out.println("hello, world"); } @Override public void mousePressed(MouseEvent e) {} @Override public void mouseReleased(MouseEvent e) {} @Override public void mouseEntered(MouseEvent e) {} @Override public void mouseExited(MouseEvent e) {} });
这样相当麻烦且相当不美观,于是有了:
component.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { System.out.println("hello, world"); } });
为什么可以这样做呢?瞅一眼 MouseAdapter 的定义吧:
public abstract class MouseAdapter implements MouseListener, MouseWheelListener, MouseMotionListener { public void mouseClicked(MouseEvent e) {} public void mousePressed(MouseEvent e) {} public void mouseReleased(MouseEvent e) {} public void mouseEntered(MouseEvent e) {} public void mouseExited(MouseEvent e) {} public void mouseWheelMoved(MouseWheelEvent e) {} public void mouseDragged(MouseEvent e) {} public void mouseMoved(MouseEvent e) {} }
(略有删改,请以您的 JDK 的实际实现为准)
最后,友情提示:使用适配器时请注意加上
@Override
注解,否则一发mouseCilcked
可以让你调半年。