Java基础---图形用户界面GUI(一)

本文介绍了Java中的图形用户界面GUI,包括组件、容器、AWT线程、事件处理机制和图形绘制。讲解了事件监听器、事件适配器的概念,以及如何处理各种事件。此外,还探讨了GUI组件上的图形操作,如Graphics类的使用,以及双缓冲技术来提高重绘速度。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


GUI:Graphical User Interface(图形用户接口)就是应用程序提供给用户操作的图形界面,包括窗口、菜单、工具栏和其它各种屏幕元素。

图形界面程序可以使用各种各样的图形界面元素,如文本框、按钮、列表框、对话框等,我们将这些图形界面元素称为GUI组件,在这些组件上不能容纳其它的组件。

容器其实也是一种组件,是一种比较特殊对的组件,它可以用来容纳其他的组件,如窗口、对话框、所有的容器类都是java.awt.Container的直接或间接子类,Container类是Component类的一个子类,由此可见容器本身也具有组件的功能和特点,也可以被当作基本组件一样使用。

继承关系图


AWT线程

程序产生Frame对象时,创建了一个新的线程,我们称之为AWT线程。

AWT事件处理机制

对于这种GUI程序与用户操作的交互功能,Java使用了一种自己的专门方式,称之为事件处理机制。

在事件处理机制中,我们需要理解三个重要的概念:

事件:用户对组件的一个操作,称之为一个事件。

事件源:发生事件的组件就是事件源。

事件处理器:负责处理事件的方法。

三者之间的关系


事件处理器(事件监听器)首先与组件(事件源)建立关系,当组件接受到外部作用(事件)时,组件就会产生一个相应的事件对象,并把此对象传给之关联的事件处理器,事件处理器就会启动并执行行相关的代码来处理该对象

Java程序对象对事件进行处理的方法放在一个类对象中的,这个类对象就是事件监听器。

我们必须将一个事件监听器对象同某个事件源的某种事件进行关联,这样,当某个事件源上发生了某种事件后,关联的事件监听器对象的有关代码才会被执行。我们把这个关联过程称为向事件源注册事件监听器对象。

事件用以描述发生了什么事情。AWT对各种不同的事件,按事件的动作(如鼠标操作)、效果(如窗口的关闭和激活)等进行了分类,一类事件对应一个AWT事件类。

java.awt.event包中列出了所有的事件类。

MouseEvent类对应鼠标事件,包括鼠标按下,鼠标释放,鼠标点击(按下后释放)等。

WindowEvent类对应窗口事件,包括用户点击了关闭按钮,窗口得到与失去焦点,窗口被最小化等。

ActionEvent类对应一个动作事件,它不是代表一个具体的动作,而是一种语义,如按钮或菜单被鼠标单击,单行文本框中按下回车键等都可以看作是ActionEvent事件。

通过各种事件类提供的方法,我们可以获得事件源对象,以及程序对这一事件可能要了解的一些特殊信息。

某一类事件,其中又包含触发这一事件的诺干具体情况。对一类事件的处理由一个事件监听器对象来完成,对于触发这一事件的每一种情况,都对应着事件监听对象中的一个不同方法。某种事件监听器对象中的每个方法名称必须是固定的,事件源才能依据事件的具体发生情况找到事件监听器对象中对应的方法。在面向对象的编程语言中,这种限定就是通过接口类来表示的。

事件监听器接口的名称与事件的名称是相对应的,如MouseEvent的监听器接口名称MouserListener

如果某个事件的监听器接口中只有一个方法,那么这个事件就是语义事件,如ActionListener中只有一个方法,ActionEvent就是一种语义事件,反之,则为低级事件

用事件监听器处理事件

示例(程序添加窗口关闭的代码)

import java.awt.*;
import java.awt.event.*;
public class TestFram 
{
	public static void main(String[] args) 
	{
		Frame f = new Frame("我的图形界面");
		f.setSize(300,300);
		f.setVisible(true);
		f.addWindowListener(new MyWindowListener());
	}
}

class MyWindowListener implements WindowListener
{
	public void windowClosing(WindowEvent e)
	{
		e.getWindow().setVisible(false);
		((Window)e.getComponent()).dispose();
		System.exit(0);
	}
	public void windowActivated(WindowEvent e){}
	public void windowClosed(WindowEvent e){}
	public void windowDeactivated(WindowEvent e){}
	public void windowDeiconified(WindowEvent e){}
	public void windowIconified(WindowEvent e){}
	public void windowOpened(WindowEvent e){}
}
在上面代码中,编写了一个新类MyWindowListener来实现窗口事件监听对象的程序代码,并调用Window.addWindowListener方法将事件组册到Frame类(Frame类继承java.awt.Window类)创建的框架窗口上。在WindowListener接口里有7个方法,正好对应窗口事件的7种情况,因为我们只想处理鼠标点击窗口标题栏上的关闭按钮这一事件,对其他窗口事件我们并不关心,所以,在MyWindowListener的代码中,我们只对windowClosing方法进行了编码,其他方法只是简单实现。

总结:要处理GUI组件上的XxxEvent事件下的某种情况,首先要编写一个实现了XxxListener接口的事件监听器类,然后再XxxListener类和要处理的具体事件情况相对应的方法中编写处理程序代码,最后将类XxxListener创建的对象通过addXxxListener方法组册到GUI组件上。Xxx可以是各种不同的事件,如Window、Mouse、Key、Action等
事件适配器

为了简化编程,JDK针对大多数事件监听器接口定义了相应的实现类,我们称之为事件适配器(Adapter)类。

在适配器中,实现了相应的监听器接口中所有的方法,但不做任何事情,子类子类只要继承适配器类,就等于实现了相应的监听器接口,如果要对某类事件的某种情况进行处理,只要覆盖相应的方法就可以了,其他的方法再也不用“简单实现”了。

修改MyWindowListener,代码如下:

class MyWindowListener extends WindowAdapter
{
	public void windowClosing(WindowEvent e)
	{
		e.getWinddow().setVisible(false);
		((Window)e.getComponet()).dispose();
		System.exit(0);
	}
}
按钮的事件监听代码中要访问框架窗口 一种简单的办法就是将事件监听器的代码和产生GUI组件的代码放在同一个类中实现
import java.awt.*;
import java.awt.event.*;
public class TestFrame implements ActionListener
{
	Frame f = new Frame("图形界面");
	public static void main(String[] args)
	{
		TestFrame tf = new TestFrame();
		tf.init();
		
	}
	public void init()
	{
		Button btn = new Button("退出");
		btn.addActionListener(new TestFrame());
		f.add(btn);
		f.setSize(300,300);
		f.setVisible(true);
	}
	public void actionPerformed(ActionEvent e)
	{
		f.setVisible(false);
		f.dispose();
		System.exit(0);
	}
}
事件监听器的匿名内置实现方式
对上面例子,我们用匿名内置的语法进行改写
import java.awt.*;
import java.awt.event.*;
public class TestFrame
{
	Frame f = new Frame("图形界面");
	public static void main(String[] args)
	{
		new TestFrame().init();
	}
	public void init()
	{
		Button btn = new Button("退出");
		btn.addActionListener(new ActionListener(){
			public void actionPerformed(ActionEvent e)
			{
				System.out.println("zhixingle ");
				f.setVisible(false);
				f.dispose();
				System.exit(0);
			}
		});
		f.add(btn);
		f.setSize(300,300);
		f.setVisible(true);
	}
}
事件处理的多重运用

用户的一个操作,在触发了低级事件的同时,可能也会触发语义事件。一般情况下,如果语义事件能够满足我们的需求,我们就不再处理低级事件。

用户在一个按钮上按下鼠标,触发了鼠标事件,也触发了按钮的动作事件,(需求:当按钮被点击后,我们除了要执行按钮对应的程序功能外,还希望鼠标按下时能改变按钮标题的内容,在按钮上要注册两个事件监听器对象,一处理鼠标事件,另一个处理按钮的动作事件。)

通过查看这些addXxxListener方法,就知道了这个组件所支持的事件类型。

一个组件上的一个动作可以产生多种不同类型的事件,因而可以向同一个事件源上注册多种不同类型的监听器,

一个事件监听器对象可以注册到多个事件源上,即多个事件源的同一事件都由一个对象统一来处理

一个事件源也可以注册对同一个事件进行处理的多个事件监听器对象,当这一事件发生时,各事件监听器对象依次被调用。


高级事件处理

默认情况下,组件屏蔽了对所有事件的响应,也就不管发生了什么情况,事件都不会在这个组件上发生,组件都不会产生任何事件对象。只有在一个组件上注册某种事件的事件监听对象后,组件才可以对这种事做出响应,当发生了对应这种事的情况后,事件才能在这个组件上发生,组件才会产生这种事件对象。

即使没有在组件上注册事件监听器,我们只要调用了enableEvents函数,设置组件能够进行响应的事件,在响应的情况发生后,组件仍然能够产生对应事件。enableEvents函数定义如下:

protected final void enableEvents(long eventsToEnable)

参数eventsToEnable  processEvent(AWTEvent e)方法中参数类型是AWTEvent 在该类中可以看到这些常量的定义

如鼠标移动事件对应的常量为MOUSE_MOTION_EVENT_MASK,这样,当我们想让组件响应鼠标移动事件时,可以使用enableEvents(AWTEvent.AWTEvent.MOUSE_MOTION_EVENT_MASK);语句来完成。

import java.awt.*;
import java.awt.event.*;
class MyButton extends Button
{
	private MyButton friend;
	public void setFriend(MyButton friend)
	{
		this.friend = friend;
	}
	public MyButton(String name)
	{
		super(name);
		enableEvents(AWTEvent.MOUSE_MOTION_EVENT_MASK);
	}
	protected void processMouseMotionEvent(MouseEvent e)
	{
		setVisible(false);
		friend.setVisible(true);
	}
}

public class TestMyButton
{
	public static void main(String[] args)
	{
		MyButton btn1 = new MyButton("你来抓我呀!");
		MyButton btn2 = new MyButton("你来抓我呀!");
		btn1.setFriend(btn2);
		btn2.setFriend(btn1);
		btn1.setVisible(false);
		Frame f = new Frame("用户界面");
		f.add(btn1,"North");//将btn1增加到f的北部
		f.add(btn2,"South");//将btn2增加到f的南部
		f.setSize(300,300);
		f.setVisible(true);
		btn2.setVisible(true);
		
	}
}

GUI组件上的图形操作

Graphics类

组件对象本身不提供这些操作的方法,它只提供一个getGraphics方法,getGraphics方法返回一个包含有该组件的屏幕显示外观信息的Graphics类对象,Graphics类提供了在组件显示表面绘画图形,打印文字,显示图像等操作方法。

画直线

import java.awt.*;
import java.awt.event.*;
public class DrawLine
{
	Frame f = new Frame("图形界面");
	public static void main(String[] args)
	{
		new DrawLine().init();
	}
	public void init()
	{
		f.setSize(300,300);
		f.setVisible(true);
		f.addMouseListener(new MouseAdapter(){
			int orgx;
			int orgy;
			public void mousePressed(MouseEvent e)
			{
				orgx=e.getX();
				orgy = e.getY();
			}
			public void mouseReleased(MouseEvent e)
			{
				Graphics g = f.getGraphics();
				g.setColor(Color.red);
				g.setFont(new Font("隶书",Font.BOLD,30));
				g.drawString(new String(orgx+","+orgy),orgx,orgy);

				//f.getGraphics().setColor(Color.red);//设置绘图颜色为红色
				f.getGraphics().drawLine(orgx,orgy,e.getX(),e.getY());
			}
		});
	}
}

注:放大缩小就会没有了,因为AWT线程都会重新绘制组件,组件上原来绘制的图形也就不复存在了。

组件重绘

我们将程序窗口最小化后再恢复正常化显示,发现所绘图全部消失了,在组件大小改变或隐藏后又显示,AWT线程都会重新绘制组件,组件上原来绘制的图形也就不复存在了,这一过程称为“曝光”。要想让用户感觉到所绘的图形一直存在,我们只需在组件重新绘制后,立即将原来的图形重新画上去,这个过程是肉眼感觉不到的。AWT线程在重新绘制组件后,会立即调用组件的paint方法,所以我们的图形重绘代码应该在paint方法中编写。

paint方法是这样定义的

public void paint(Graphics g);

可见,AWT线程已经获得了组件上的Graphics对象,并将它传递给了paint方法,在paint方法中绘图时只能使用这个Graphics对象。

当我们想要执行paint方法中的程序代码时,应用程序不应直接调用paint方法,如果我们想执行paint方法中的程序代码,需调用Component.repaint方法,Component.repaint方法调用Component.update方法,Component.update再调用Component.paint。组件重绘关系如下图

由于我们不可能直接进入到某个组件的paint方法中修改程序代码,我们需要定义一个继承了该组件的子类,在子类中覆盖paint方法,在新的paint方法中编写重绘图形程序代码,并将原来创建的组件对象改为由这个子类创建,就可以达到我们的目的了。

我们修改上面代码,使其具有重绘效果:

import java.awt.*;
import java.awt.event.*;
public class DrawLine extends Frame
{
	int orgX;
	int orgY;
	int endX;
	int endY;
	public static void main(String[] args)
	{
		new DrawLine().init();
	}
	public void paint(Graphics g)
	{
		//这里是重绘效果
		g.drawLine(orgX,orgY,endX,endY);
	}
	public void init()
	{
		this.addMouseListener(new MouseAdapter(){
			public void mousePressed(MouseEvent e)
			{
				orgX = e.getX();
				orgY = e.getY();
			}
			public void mouseReleased(MouseEvent e)
			{
				endX = e.getX();
				endY = e.getY();
				Graphics g = getGraphics();
				g.setColor(Color.red);
				g.setFont(new Font("隶书",Font.BOLD,30));
				//会被上面paint画的重绘
				g.drawString(new String(orgX+","+orgY),orgX,orgY);//打印鼠标按下时的坐标文本
				g.drawString(new String(endX+","+endY),endX,endY);//打印鼠标释放时的坐标文本
				g.drawLine(orgX,orgY,endX,endY);
			}
		});
		setSize(300,300);
		setVisible(true);
	}
}

如果我们想完全重绘窗口的内容,需要将每一条直线的坐标保存到一个集合类中,在paint方法中取出该集合中的每一条直线坐标,逐一绘制。首先我们创建一个新的类MyLine,对应所画的一条直线,该类中定义一个drawMe方法,用于将该直线在某个Graphics对应的组件上画出自己。
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import javax.swing.*;
class MyLine
{
	private int x1;
	private int y1;
	private int x2;
	private int y2;
	public MyLine(int x1,int y1,int x2,int y2)
	{
		this.x1 = x1;
		this.y1 = y1;
		this.x2 = x2;
		this.y2 = y2;
	}
	public void drawMe(Graphics g)
	{
		g.drawLine(x1,y1,x2,y2);
	}
}
public class RerawAllLine extends Frame
{
	Vector vLines = new Vector();
	public static void main(String[] args)
	{
		new RerawAllLine().init();
	}
	public void paint(Graphics g)
	{
		g.setColor(Color.red);
		Enumeration e = vLines.elements();
		while(e.hasMoreElements())
		{
			MyLine ln = (MyLine)e.nextElement();
			ln.drawMe(g);
		}
	}
	public void init()
	{
		this.addWindowListener(new WindowAdapter(){
			public void windowClosing(WindowEvent e)
			{
				((Window)e.getSource()).dispose();
				System.exit(0);
			}
		});
		this.addMouseListener(new MouseAdapter()
		{
			int orgX;
			int orgY;
			public void mousePressed(MouseEvent e)
			{
				orgX = e.getX();
				orgY = e.getY();
			}
			public void mouseReleased(MouseEvent e)
			{
				Graphics g = e.getComponent().getGraphics();
				g.setColor(Color.red);
				g.drawLine(orgX,orgY,e.getX(),e.getY());
				vLines.add(new MyLine(orgX,orgY,e.getX(),e.getY()));
			}
		});
		this.setSize(300,300);
		setVisible(true);
	}
}

图像操作

我们可以通过Graphics.drawImage(Image img,int x,int y,ImageObserver)方法在组件上显示图像,其中,img参数是要显示的图像,x,y是图像显示的左上角坐标,observer是用于监视图像创建进度的一个对象。

drawImage是一个异步方法,即使img对应的图像还没完全装载(在创建Image类对象时并没有在内存中创建图像数据)时,drawImage也会立即返回。如果程序想了解图像创建的对象传递给drawImage方法。如果程序不关心图像创建的进度消息,可以传递要显示图像的那个组件对象,因为Component类已实现了ImageObserver接口。

显示图像的示例程序。

import java.awt.*;
import java.awt.event.*;
public class DrawImage
{
	public static void main(String[] args)
	{
		Frame f = new Frame("图形界面");
		/*会抛出java.lang.NullPointException异常,因为只有在组件已显示在窗口上时,getGraphics方法才能正确返回一个Graphics对象。
		Image img = f.getToolkit().getImage("C:\\Users\\49921_000\\Desktop\\1.jpg");
		f.getGraphics().drawImage(img,0,0,f);
		*/
		f.setSize(300,300);
		f.setVisible(true);
		f.addWindowListener(new WindowAdapter(){
			public void windowClosing(WindowEvent e)
			{
				System.exit(0);
			}
		});
		Image img = f.getToolkit().getImage("C:\\Users\\49921_000\\Desktop\\1.jpg");
		//运行不再有错
		f.getGraphics().drawImage(img,0,0,f);
	}
}
上面图片并没有显示出来。这是因为在窗口初始化显示也会被调用paint方法,paint方法会擦除窗口上绘制的图形,这里drawImage方法先于paint方法执行,所以,drawImage方法绘制的图像被paint方法擦除掉了。

对于上面问题,我们可以将图像放在paint方法中显示,修改后的代码

import java.awt.*;
import java.awt.event.*;
public class DrawImage extends Frame
{
	Image img = null;
	public static void main(String[] args)
	{
		DrawImage f = new DrawImage();
		f.init();
	}
	public void init()
	{
		img = this.getToolkit().getImage("C:\\Users\\49921_000\\Desktop\\2.gif");
		setSize(300,300);
		setVisible(true);
		this.addWindowListener(new WindowAdapter(){
			public void windowClosing(WindowEvent e)
			{
				System.exit(0);
			}
		});
		/*img放这里 编译运行后,有时图像立即就能正常显示出来,有时候图像并没有显示出来,而是在命令行窗口出现如下错误
		java.lang.NullPointerException 改变窗口大小,这时图像就显示出来了。原因在于,这时候正好碰到了AWT线程调用paint
		方法早于getImage方法情况,而在paint方法执行drawImage的时候,img对象仍然为null.
		img = this.getToolkit().getImage("C:\\Users\\49921_000\\Desktop\\2.gif");
		*/
	}
	public void paint(Graphics g)
	{
		getGraphics().drawImage(img,0,0,this);
	}
}

在实际工作中,可能会碰到许多小细节,更希望知道老手们解决问题的方法和思想,对于程序中的

Image img = f.getToolkit().getImage("c:\\1.gif");

这行代码,可能会问:“如果是我自己写程序,我怎么能够知道产生一个Image对象,要用到这些方法和这些调用关系呢?”其实,最聪明的人是最会使用工具的人,特别是编写程序,如果我们有一个好的帮组系统,经常会有事半功倍的特效。JDK文档。有了这样的帮组系统,我们可以进行模糊查询了。由Image是抽象类,我们不能使用构造方法,只能通过某个方法来产生一个Image对象了,这个方法名,估计就是createImage,getImage之类的英文单组组合,按照这种思想,通过帮助索引查询,一般都会有较大的收获,我们查到了Toolkit.getImage(String path)方法,由于Toolkit是抽象类,不能直接创建,接着用getTookit又找到了Component.getToolkit方法返回Toolkit对象,这样顺藤摸瓜写出了这个完整的语句了。

双缓冲的技术

在画线重绘程序中,窗口重画时,需要逐一重新绘制窗口上原来的图形(直线,文本),如果原来的图形非常多,这个过程就显得比较慢了。一种改进的办法是,我们可以调用Component.createImage方法,在内存中创建一个Image对象,当我们组件上绘图时,也在这个Image对象上执行同样的绘制,即Image对象中的图像是组件表面内容的复制,当组件重画时,只需要将内存中的这个Image对象在组件上画出,不管组件上原来的图形有多少,在重绘时都只是一副图像而已,在组件上的图形非常多时,重绘数度明显提高,这就是一种被称为双缓冲的技术。

应用示例:

import java.awt.*;
import java.awt.event.*;
class  DrawLine extends Frame
{
	Image oimg = null;
	Graphics og = null;
	public static void main(String[] args) 
	{
		new DrawLine().init();
	}
	public void init()
	{
		setSize(300,300);
		setVisible(true);
		Dimension d = getSize();
		oimg = createImage(d.width,d.height);
		og = oimg.getGraphics();
		addMouseListener(new MouseAdapter(){
			int orgx;
			int orgy;
			public void mousePressed(MouseEvent e)
			{
				orgx = e.getX();
				orgy = e.getY();
				System.out.println(orgx+","+orgy);
			}

			public void mouseReleased(MouseEvent e)
			{
				/*Graphics g = getGraphics();
				g.setColor(Color.red);//设置绘图颜色
				g.setFont(new Font("隶书",Font.BOLD,30));//设置文本字体
				g.drawString(new String(orgx+","+orgy),orgx,orgy);//打印鼠标按下时的坐标
				g.drawString(new String(e.getX()+","+e.getY()),e.getX(),e.getY());//打印鼠标释放时的坐标
				g.drawLine(orgx,orgy,e.getX(),e.getY());*/
				System.out.println(e.getX()+","+e.getY());
				og.setColor(Color.red);
				og.setFont(new Font("隶书",Font.BOLD,30));
				og.drawString(new String(orgx+","+orgy),orgx,orgy);//打印鼠标按下时的坐标
				og.drawString(new String(e.getX()+","+e.getY()),e.getX(),e.getY());//打印鼠标释放时的坐标
				og.drawLine(orgx,orgy,e.getX(),e.getY());
			}
		});
		addWindowListener(new WindowAdapter(){
			public void windowClosing(WindowEvent e)
			{
				System.exit(0);
			}
		});
	}
	public void paint(Graphics g)
	{
		if(oimg!=null)
			g.drawImage(oimg,0,0,this);
	}
}
如果在paint方法中没有使用if语句对oimg进行是否为null的检查,就会出现java.lang.NullPointException异常

createImage是Component的一个方法,只有部件显示在桌面上之后才能去调用这个方法,部件显示时会调用它的paint方法,也就是createImage方法只能在第一次paint方法调用之后才能被调用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值