JAVA chapter10 图形化用户接口编程
10.2显示Frame
JFrame是最顶层窗口
10.2.2 Frame属性
setLocation,setBounds设定frame的位置
setIconImage,设定标题图标
setTitle更改title文字
setResizable设定窗口是否可以被改变大小。布尔值为参数
为了决定frame的适合尺寸,首先找出屏幕大小,调用Toolkit类的静态的getDefaultToolkit方法获得Toolkit对象。Toolkit类和本地操作系统有很多接口。
然后调用getScreenSize方法,它会返回一个Dimension对象,包含窗口大小。Dimension对象同时存储了width,height。举例如下
Toolkit kit=Toolket.getDefaultToolkit();
Dimension screenSize=kit.getScreenSize();
int screenWidth=screenSize.width;
int screenHeight=screenSize.height;
setSize(screenWidth/2,screenHeight/2);
你可以设置框架图标
Image img=new ImageIcon("icon.gif").getImage();
setIconImage(img);
13.3在Component中显示信息
内容窗口Content pane
Component c=...;
frame.add(c);
绘制COmponent,需要继承JComponent覆盖paintComponent方法
class MyComponent extends JComponent{
public void paintComponent(Graphics g){
}
}
不要自己调用PaintComponent,它会被应用自动调用。
Graphics类有很多绘图方法。我们简单实现:
public class NotHelloWorldComponent extends JComponent{
public static final int MESSAGE_X=75;
public static final int MESSAGE_Y=100;
public void paintComponent(Graphics g){
g.drawString("Not a hellolWorld program",MESSAGE_X,MESSAGE_Y);
}
...
}
最后,component应该告诉用户它有多大,覆盖getPreferredSize方法,并返回Dimension类,类中有建议的高和宽
public class NotHelloWorldComponent extends JComponent{
private static final int DEFAULT_WIDTH=300;
private static final int DEFAULT_HEIGHT=200;
...
public Dimension getPreferredSize(){
return new Dimension(DEFAULT_WIDTH,DEFAULT_HEIGHT);
}
}
当你用一个或多个component填充frame时,并且你仅想要他们倾向的值,使用pack,而不是setSize
class NotHelloWorldFrame extends JFrame{
public NotHelloWorldFrame(){
add(new NotHelloWorldComponent());
pack();
}
}
10.3.1 使用2d Shapes
Graphics类拥有方法画线,矩形,椭圆等等。现在被使用Java 2d库来替代。
为了使用库,你需要获得Graphics2D类的对象,它是Graphics类的子类,从Java1.2开始,paintComponent自动接收一个Graphics2D类的对象,简单类型转换即可。
public void paintComponent(Graphics g){
Graphics2D g2(Graphics2D) g;
...
}
Java 2D库组织了源于对象的几何形状,拥有代表线,矩形,椭圆的类。Line2D Rectangle2D Ellipse2D
这些类都实现了Shape接口。Java 2D库支持更多复杂形状,弧,立方体,二次方程。
为了画形状,首先创建一个实现了Shape接口的对象,然后调用draw方法
Rectangle2D rect=...;
g2.draw(rect);
Rectangle2D.Float Rectangle2D.Double是静态内部类。
当你创建Rectagnle2D.float对象时,使用float作为参数。Rectangle2D.double使用double作为参数
var floatRect=new Rectangle2D.Float(10.0F,25.0F,22.5F,20.0F);
var doubleRect=new Rectangle2D.Double(10.0,25.0,22.5,20.0);
第一,二参数为左上角坐标,三为宽度,四为高度。
Rectangle2D方法使用double作为参数和返回值。getWidth方法返回double值。即使Rectangle2D.Float对象存储width时使用的是float
尽量使用Double Shape类。仅当你需要创建数千对象时使用float类来节省内存空间。
椭圆,使用的是界限矩形方式创建。
var e=new Ellipse2D.Double(150,200,100,50);
150,200左上角坐标,100宽,50高
当你创建椭圆时,你通常知道它的中心,宽,高。但不知道边界点的坐标。setFrameFromCenter方法使用中间坐标,但它仍需边界点的坐标。
使用下面的方法创建椭圆:
var ellipse=new Ellipse2D.Double(centerX-width/2,centerY-height/2,width,height);
创建线,需要起点终点坐标,或Point2D对象。
var line=new Line2D.Double(start,end);
var line=new Line2D.Double(startX,startY,endX,endY);
10.3.2 使用颜色
Graphic2D类的setPaint方法让你选择接下来的所有绘图操作的颜色。例:
g2.setPaint(Color.RED);
g2.drawString("Warning!",100,100);
你可以填充封闭图形内部使用fill方法。
Rectangle2D rect=...;
g2.setPaint(Color.RED);
g2.fill(rect);//fill rect with red
使用多种颜色绘图,选择颜色,画或填充。再选择新颜色,画或填充。
可以使用java.awt.Color类提供的预定义常数设置颜色。
也可以通过new Color(R,G,B);构造函数创建颜色。
10.3.3 使用字体
为了找出在特定计算机上可使用的字体,调用GraphicsEnviornment的getAvailableFontFamilyNames方法。这个方法返回一个所有可用字体名的字符串数组。
为了获得用户系统图形环境的GraphicsEnvironment类的实例,使用静态的getLocalGraphicsEnviornment方法。如下:
import java.awt.*;
public class ListFonts{
pulbic static void main(String[] args){
String[] fontNames=GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames();
for(String fontName:fontNames)
System.out.println(fontName);
}
}
为了使用字体绘制字符,必须首先创建类Font的对象,说明字体名,字体样式,字体大小。如:
var sanbold14=new Font("SansSerif",Font.BOLD,14);
使用deriveFont获得需要的大小
Font f=f1.deriveFont(14.0F);
绘制"Hello,World"
var sansbold14=new Font("SansSerif",Font.BOLD,14);
g2.setFont(sansbold14);
var message="Hello,World";
g2.drawString(message,75,100);
为了使字符串绘制在正中间,需要直到字符串的宽,高。三个因素影响。
1.使用的字体
2.字符串
3.字体所在的设备
为了获得代表字体特征的对象,调用Graphics2D类的getFontRenderContext方法。它会返回一个FontRenderContext的对象,传递这个对象到Font类的getStringBounds方法:
FontRenderContext context=g2.getFontRenderContext();
Rectangle2D bounds=sansbold14.getStringBounds(message,context);
获取宽,高,ascent
double stringWidth=bounds.getWidth();
double stringHeight=bounds.getHeight();
double ascent=-bounds.getY();
获得descent leading
LineMetrics metrics=f.getLineMetrics(message,context);
float descent=metrics.getDescent();
float leading=metrics.getLeading();
10.3.5 展示图像
Image image=new ImageIcon(filename).getImage();
public void paintComponent(Graphics g){
..
g.drawImage(image,x,y,null);
}
10.4 事件处理
Java中,所有的事件对象源于java.util.EventObject。
不同的事件源可以产生不同种类的事件。button可以发送ActionEvent对象,window可以发送WindowEvent对象。
event listener是实现了listener接口的类的实例。
事件源是一个可以注册监听对象并且发送事件对象的对象。
事件源会发送事件到所有注册的监听器,当事件发生时。
监听器对象使用时间对象的信息决定对事件的反应。
例:
ActionListener listener=...;
var button=new JButton("OK");
button.addActionListener(listener);
为了实现ActionListener接口,listener类必须实现actionPerformed类,接收ActionEvent对象作为参数。
class MyListener implements ActionListener{
...
public void actionPerformed(ActionEvent event){
...
}
}
当用户点击按钮时,JButton对象创建一个ActionEvent对象,调用listener.actionPerformed(event),床底事件对象。如按钮这样的事件源有很多的监听器,在这个例子中,当用户点击按钮时会调用actionPerformed方法。
10.4.3 详述监听器
前面的部分,我们定义了一个事件监听的类,构建了该类的三个对象。一个监听器类拥有多个实例并不常见。更常见的是,每个监听器负责独立的行为,在这种情况,不需要分别的类,简单的使用lambda表达式。
exitButton.addActionListener(event->System.exit(0));
现在考虑我们有用多种相关行为的例子,比如先前的color buttons。这种情况下,我们实现一个辅助函数。
public void makeButton(String name,Color backgroundColor){
var button=new JButton(name);
buttonPanel.add(button);
button.addActionListener(event->
buttonPanel.setBackGround(backgroundColor));
}
接着我们简单调用
makeButton("yellow",Color.YELLOW);
makeButton("blue",Color.BLUE);
makeButton("red",Color.RED);
这里,我们创建了三个监听器对象,每个针对一种颜色,并没有显式定义一个类。每当辅助函数调用时,它会创建一个实现了ActionListener接口的类的实例,它的actionPerformed 的行为引用了backGroundColor的值,实际上存储在listener对象中。但所有的事情发生在你没有明确定义监听器类,实例变量,设置他们的构造函数。
10.4.4 Adapter Class
并不是所有的事件都是像按钮点击这样简单的。假设你要监视 当用户试图关闭主框架,弹出对话框并点击同意退出程序这样的事件时。
当用户试图关闭窗口,JFrame对象是WindowEvent的源。如果你想捕获这一事件,你必须使用一个合适的监听器对象,添加它到窗口监听器列表中。
WindowListener listener=...;
farme.addWindowListener(listener);
window listener必须是一个实现了WindowListener接口的对象。实际上WindowListener对象有七个方法,frame调用他们相应七种窗口对应的事件。他们的名字解释了用途,除过"iconified"是最小化窗口的意思。下列是完整的WindowListener接口。
public interface WindowListener{
void windowOpened(WindowEvent e);
void windowClosing(WindowEvent e);
void windowClosed(WindowEvent e);
void windowIconified(WindowEvent e);
void windowDeiconified(WindowEvent e);
void windowActivated(WindowEvent e);
void windowDeactivated(WindowEvent e);
}
当然,我们可以定义一个类实现了这些接,添加调用System.exit(0)到WindowClosing方法中。然后写6个空方法对于其他六个事件。但是,写六个没有任何作用的方法是乏味的工作。为了简化这一操作,每个AWT监听器接口拥有多于一种方法,并有对应的adapter类实现了这些接口中的方法,但方法什么也不做。比如,WindowAdapter类有7个什么都不做的方法。你可以扩展Adapter类实现你想要的方法,可以不实现所有的方法。一个只有一种方法的接口像ActionListener不需要适配器类。
class Terminator extends WindowAdapter{
public void windowClosing(WindowEvent e){
if(user agrees)
System.exit(0);
}
}
然后注册对象
var listener=new Terminator();
frame.addWindowListener(listener);
10.4.5 Actions
Action接口,An action是一个对象封装了:
命令的描述 字符串或者可选图标
命令所需的必要参数
Action接口有以下方法:
void actionPerformed(ActionEvent event)
void setEnabled(boolean b)
boolean isEnabled()
void putValue(String key,Object value)
void addPropertyChangeListener(PropertyChangeListener listener)
void removePropertyChangeListener(PropertyChangeListener listener)
第一个方法是一个ActionListener接口的非常熟悉的方法。实际上,Action接口继承于ActionListener接口。所以你可以像ActionListener那样使用Action接口。
第二个和第三个方法是让你可以启用或者禁用action,检查action是否可用。当action绑定在菜单或者工具条且action是disabled时,选项变为灰色。
putValue getValue方法允许你向action对象中存储或取出键值对。一对非常重要的预定义字符串是Action.name和Action.SMALL_ICON
action.putValue(Action.NAME,"Blue");
action.putValue(Action.SMALL_ICON,new ImageIcon("blue-ball.gif"));
如果action对象添加菜单或者工具栏,name和icon会自动取出并显示在菜单项和工具栏按钮上。
SHORT_DESCRIPTION值会变为工具提示。
最后两个方法允许其他对象,特别是触发action的菜单和工具栏,当action对象属性被更改时会被告知。比如说,一个菜单被添加,作为一个action对象的propety change listener,然后这个action对象被禁用,菜单会被调用,并且将action名变为灰色。
注意Action是一个接口,不是一个类。任何实现了这个接口的类必须实现这7个方法。幸运的是,我们有AbstractAction实现了所有的放啊发除过actionPerformed,这个类会存储键值对,管理listener的属性变化。你可以简单的继承该类并实现actionPerformed方法。
最后,我们要添加action对象到键盘敲击。当用户输入键盘命令时可以有响应。为了让键盘敲击与事件协作,你首先需要产生一个KeyStroke类的对象。这个便利的类封装了一个键的描述。为了产生一个KeyStroke类,不要使用构造函数,而是要使用静态的KeyStroke类的getKeyStroke方法。
KeyStroke ctrlBKey=KeyStroke.getKeyStroke("ctrl B");
为了理解下一步,你需要懂得键盘焦点的概念。用户界面可以有许多按钮,菜单,滚动条和其他组件。当你敲击一个键时,它会发送给当前聚焦的组件,这个组件一般情况下是可以视觉区分出来的。比如,一个聚焦的按钮会有一个矩形边框包围按钮文字。你可以使用Tab键转换组件的聚焦。当你按下空格键时,聚焦的按钮就会被点击。其他的按键也有不同的响应,比如方向键可以移动滚动条。
但,对于我们的例子,我们不想要键盘敲击作用于聚焦的组件。另一个方面,所有的按钮都必须知道如何处理ctrl+y ctrl+r
这是一个普遍的问题,Swing的设计者有一个简便的解决方案。每个JComponent有三个input maps,每个映射KeyStroke对象到对应的aciton。这三个map相应三种不同的情形。
WHEN_FOCUSED 当组件被键盘聚焦时
WHEN_ANCESTOR_OF_FOCUSED_COMPONENT 当组件包含的组件被聚焦时
WHEN_IN_FOCUSED_WINDOW 当组件共同的父窗口下的组件被聚焦时
KeyStroke处理检查这三个map按照以下顺序:
1.检查WHEN_FOCUSED map的组件输入的聚焦。如果keyStroke存在,那么相应的action会被启用,处理action并停止执行。
2.从输入聚焦的组件开始,检查WHEN_ANCESTOR_OF_FOCUSED_COMPONENT 映射到他们的父组件。一旦有KeyStroke和Action的映射,启用action,处理action并停止执行。
3.检查所有的可见,启用的组件。当窗口被输入聚焦时,然后所有注册在WHEN_IN_FOCUSED_WINDOW的映射,给予这些组件按照他们注册的KeyStroke顺序,去处理对应的响应,当响应处理完毕后,停止执行。
为了获得从组件获得input map,使用getInputMap()方法
InputMap imap=panel.getInputMap(JComponent.WHEN_FOCUSED);
WHEN_FOCUS情况意味着map会被查询当当前组件有键盘聚焦事件。在我们的解决方案里,这不是我们想要的map。我们需要按钮,而不是面板获得输入聚焦事件。另两个Map都是可行的,我们使用WHEN_ANCESTOR_OF_FOCUSED_COMONENT在我们的程序中。
InputMap并不直接映射KeyStroke对象到Action对象上。替代的是,它会映射到任何对象上。同时第二个映射,ActionMap实现的,映射对象到action。这使得当键盘敲击事件发生时,分享来自不同输入映射的相同的action。
每个组件都有三个Input maps和一个action map,为了将它们结合起来,你需要有action的名称,下面可以绑定键到action
imap.put(KeyStroke.getKeyStroke("ctrl Y"), "panel.yellow");
ActionMap amap=panel.getActionMap();
amap.("panel.yellow",yellowAction);
传统的使用"none"来不做任何动作,这可以方便来禁用key
imap.put(KeyStroke.getKeyStroke("ctrl C","none");
总结如下:
1.实现一个继承AbstractAction的类,你可以使用相同的类适用于多种相关的action
2.构建一个action类的对象。
3.从action对象中构建按钮或者菜单,构造函数需要从action对象中读取标签文本和图标。
4.对于会被键盘敲击触发的action,你需要更多的步骤,首先,定位窗口的最上级组件,比如包含其他组件的panel
5.获得WHEN_ANCESTOR_OF_FOCUSED_COMPONENT的input map,它是最高级组件的input map。为需要响应的敲击事件定义KeyStroke对象,制作一个action key 对象,包含描述你行为的字符串,添加键值对(keystroke,action key)到input map中。
6.最后,获得最高级组件的action map,将键值对添加到map中。
10.4.6 鼠标事件
如果你只是想处理点击按钮或者菜单事件,你不需要明确处理鼠标事件。这些操作已经被内部实现。如果你需要用户使用鼠标绘制,你需要处理鼠标移动,点击,拖拽事件。
当用户点击了鼠标按钮后,三个监听方法会被调用。mousePressed,当鼠标第一次按下时,mouseReleased 当鼠标释放时,最后mouseClicked事件。如果你关心完全点击,你可以忽略前两个方法。使用MouseEvent参数的getX,getY方法。你可以获得鼠标点击时的x,y坐标。分辨单击,双击,三击,使用getClickCount方法。
在我们的示范程序中,我们实现了mousePressed和mouseClicked方法。当你点击到不属于任何已绘制的正方形内的像素时,一个新的四边形会被添加,我们在mousePressed方法中实现,这样用户会收到立即的回应而不需要等待鼠标按键释放。当用户双击鼠标在一个已存在的正方形时,它会被擦除。我们实现了这个MouseClicked方法因为我们需要获得点击次数。
public void mousePressed(MouseEvent event){
current=find(event.getPoint());
if(current==null)//not inside a square
add(event.getPoint());
}
public void mouseClicked(MouseEvent event){
current=find(event.getPoint());
if(current!=null&&event.getClickCount()>=2)
remove(current);
}
public void mouseMoved(MouseEvent event){
if(find(event.getPoint())==null)
setCursor(Cursor.getDefaultCursor());
else
setCursor(Cursor.getPredefinedCursor(CUrsor.CROSSHAIR_CURROR));
}
如果用户当鼠标运动中点击了鼠标。mouseDragged就被调用,而不是mouseMoved。我们的测试应用让用户在游标下绘制正方形。我们简单的更新当前的以鼠标位置为中心的矩形,并且重绘。
public void mouseDragged(MouseEvent event){
if(current!=null){
int x=event.getX();
int y=event.getY();
current.setFrame(x-SIDELENGTH/2,y-SIDELENGTH/2,SIDELENGTH,SIDELENGTH);
repaint();
}
}
mouseMoved方法只在鼠标在组件内时被调用,mouseDragged方法在鼠标在组件外时也会被持续调用。
还有两个鼠标事件.moseEntered mouseExited。当鼠标进入组件,退出组件时调用。
最后我们来看如何监听鼠标事件。鼠标点击通过mouseClicked方法报告。它是MouseListener的一部分。许多应用只关心鼠标点击,不关心鼠标移动。当鼠标移动事件出现的如此频繁。鼠标移动和拖拽事件定义在不同的接口中。名为MouseMotionListener