6. 图形程序设计
6.1. Swing概述
Java1.0刚刚出现的时候,包含了一个用于基本GUI程序设计的类库,Sun将它称为抽象窗口工具箱(Abstract Window Toolkit,AWT)。基本AWT库采用将处理用户界面元素的任务委派给每个目标平台的本地GUI工具箱的方式,由本地GUI工具箱负责用户界面元素的创建和动作。例如,如果使用最初的AWT在Java窗口中放置一个文本框,就会有一个低层的“对等体”文本框,用来真正地处理文本输入。从理论上说,程序可以运行在任何平台上,但观感(look and feel)的效果却依赖于目标平台。
基于对等体方法的缺点:
- 不能给予用户一致的、可预见性的界面操作方式
- AWT构建的GUI应用程序不如Windows等平台应用程序美观,也没有那些平台用户认知的功能
- 不同平台上的AWT用户界面库中存在着不同的bug
于是后来有了Swing。
- Swing是不对等给予GUI工具箱,是Java基础类库(Java Foundation Class,JFC)的一部分。
- Swing没有完全替代AWT,而是基于AWT架构之上。在采用Swing编写的程序之中,还需要使用基本的AWT处理事件。
Swing的优势:
- 拥有一个丰富、便捷的用户界面元素集合。
- 对底层平台依赖的很少,因此与平台相关的bug很少。
- 给予不同平台的用户一致的感觉。
7.2. 创建框架
- 框架(frame):顶层窗口(没有包含在其他窗口中的窗口)。在Swing中,名为JFrame,扩展于AWT库中的Frame类。JFrame是极少数几个不绘制在画布上的Swing组件之一。因此,它的修饰部件(按钮、标题栏、图标等)由用户的窗口系统绘制,而不是Swing。
- !绝大多数Swing组件类都以“J”开头。部分没有以“J”开头的类属于AWT组件。如果偶然地忘记书写“J”,程序仍然可以编译和运行,但将Swing和AWT组件混合使用将会导致视觉和行为的不一致。
public class SimpleFrameTest {
//初始化语句结束后,main方法退出。但程序没有终止,只是终止了主线程。
//事件分派线程保持程序处于激活状态,直到关闭框架或调用System.exit方法终止程序。
public static void main(String[] args) {
//所有Swing组件必须由事件分派线程(event dispatch thread)配置
//线程将鼠标点击和按键控制转移到用户接口组件
EventQueue.invokeLater(new Runnable() { //interface?
public void run() {
SimpleFrame frame = new SimpleFrame();
//定义用户关闭框架时的响应动作
//(在包含多个框架的程序中,不能在用户关闭其中的一个程序时就让程序退出。
//在默认情况下,用户关闭窗口时只是将框架隐藏起来)
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
//简单地构造框架是不会自动地显示出来的。需要调用setVisible(true)框架可见。
frame.setVisible(true);
}
});
}
}
class SimpleFrame extends JFrame{
private static final int DEFAULT_WIDTH=300;
private static final int DEFAULT_HEIGHT=200;
public SimpleFrame(){
setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
}
}
7.3. 框架定位
JFrame类本身只包含若干个改变框架外观的方法。通过继承,从JFrame的各个超类中继承了许多用于处理框架大小和位置的方法。
方法 | 功能 |
---|---|
setLocation(int x,int y) | 设置框架的位置 |
setBounds(int x,int y,int width,int height) | 设置框架位置和大小 |
setIconImage(Image image) | 设置窗口系统在标题栏、任务切换窗口等位置显示的图标 |
setTitle(String s) | 设置框架标题栏的文字 |
setResizable(boolean b) | 设置框架大小是否允许用户改变 |
注:对于框架而言,坐标相对于整个屏幕。对容器中的组件,坐标相对于容器。
JFrame类的继承层次
框架属性
组件类的很多方法是以获取/设置方法对形式出现的。如,public String getTitle() public void setTitle(String title)。这样一个获取/设置方法对被称为一种属性。属性包含属性名和类型。
对于类型为boolean的属性,获取方法由is开头。
确定合适的框架大小
如果没有明确地指定框架的大小,所有框架的默认值为0*0像素。对于专业应用程序来说,应该检查屏幕的分辨率,并根据分辨率编写代码重置框架的大小。
public class SizedFrameTest {
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
public void run() {
JFrame frame = new SizedFrame();
frame.setTitle("SizedFrame");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
});
}
}
class SizedFrame extends JFrame{
public SizedFrame(){
//得到屏幕大小
//Toolkit类包含很多与本地窗口系统打交道的方法
Toolkit kit = Toolkit.getDefaultToolkit();
//获取屏幕大小
Dimension screenSize=kit.getScreenSize();
int screenHeight=screenSize.height;
int screenWidth=screenSize.width;
//设置框架大小并让窗口系统选用窗口位置(通常是距最后一个显示窗口很少偏移量的位置)
setSize(screenWidth/2, screenHeight/2);
setLocationByPlatform(true);
//设置框架图标
Image image= new ImageIcon("icon.gif").getImage();
setIconImage(image);
}
}
7.4. 在组件中显示信息
可以将消息字符直接绘制在框架中,但这并不是一种好的编程习惯。在Java中,框架被设计为放置组件的容器,可以将菜单栏和其他的用户界面元素放置在其中。在通常情况下,应该在另一组件上绘制信息,并将这个组件添加到框架中。
JFrame的内部结构(从里到外)
框架
根窗格:组织内容窗格以及实现观感
层级窗格:组织内容窗格以及实现观感
菜单栏(可选项)
内容窗格(content pane)
玻璃窗格:组织内容窗格以及实现观感
在设计框架时,把所有组件添加到内容窗格:
Container contentPane=frame.getContentPane();
Component c=…;
contentPane.add(c);
public class NotHelloWorld {
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
public void run() {
JFrame frame=new NotHelloWorldFrame();
frame.setTitle("Not Hello World");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
});
}
}
class NotHelloWorldFrame extends JFrame{
public NotHelloWorldFrame(){
add(new NotHelloWorldComponent());
//设置框架大小。框架将被设为刚好能够防止所有组件的大小。
pack();
}
}
//绘制一个组件,需要定义一个扩展JComponent的类,并覆盖其中的paintComponent方法
class NotHelloWorldComponent extends JComponent{
public static final int MESSAGE_X=75;
public static final int MESSAGE_Y=100;
public static final int DEFAULT_WIDTH=300;
public static final int DEFAULT_HEIGHT=200;
//Graphics类型的参数保存着用于绘制图像和文本的设置
//所有的绘制都必须使用Graphics对象,其中包含了绘制图案、图像和文本的方法
//一定不要自己调用paintComponent方法。在应用程序需要重新绘图的时候,这个方法会被自动地调用。
//重新绘图:扩大窗口;极小化窗口后又恢复大小;窗口被另一个弹出的窗口覆盖;窗口第一次显示
public void paintComponent(Graphics g){
g.drawString("Not a hello world program", MESSAGE_X, MESSAGE_Y);
}
//覆盖getPreferredSize方法,设置组件首选大小
public Dimension getPreferredSize(){
return new Dimension(DEFAULT_WIDTH, DEFAULT_HEIGHT);
}
}
6.5. 处理2D图形
Graphics类包含绘制直线、矩形和椭圆等方法。但绘制图形的操作能力非常有限。如,不能改变线的粗细,不能旋转图形。
Java2D库实现了一组强大的图形操作。
使用Java2D库绘制图形,需要获得一个Graphics2D类对象。这个类是Graphics类的子类。paintComponent方法会自动地获得一个Graphics2D类对象,只需要一次类型转换就可以了。
Java2D库采用面向对象的方式将几何图形组织起来,包括描述直线、矩形和椭圆的类:Line2D Rectangle2D Ellipse2D。 这些类全都实现了Shape接口。
因为使用浮点坐标指定图形时,单精度完全可以满足要求。另外在某些平台上,float计算的速度比较快,并且只占据double值一般的存储量。因此Java中包含了两个版本的图形类:一个提供float类型的坐标,一个提供double类型的坐标。Rectangle2D类是一个拥有两个具体子类的抽象类,Rectangle2D.Float和Rectangle2D.Double两个子类是静态内部类。但Rectangle2D.Float对象存储float类型的宽度,getWidth方法也返回一个double值。
Point2D类也是图形类中的一种。使用Poin2D对象比使用单独的x和y更有面向对象的风格。许多构造器和方法都接收Point2D型参数。在可能的情况下使用Poin2D对象比较好。
public class DrawTest {
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
public void run() {
JFrame frame = new DrawFrame();
frame.setTitle("DrawTest");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
});
}
}
class DrawFrame extends JFrame{
public DrawFrame(){
add(new DrawComponent());
pack();
}
}
class DrawComponent extends JComponent{
private static final int DEFAULT_WIDTH=400;
private static final int DEFAULT_HEIGHT=400;
public void paintComponent(Graphics g){
Graphics2D g2= (Graphics2D)g;
//绘制长方形
double leftX=100;
double topY=100;
double width=200;
double height=150;
//绘制图形,首先要创建一个实现了Shape接口类的对象,然后调用Graphi2D类中的draw方法
//只知道矩形的两个对角点的时候应该创建一个空矩形,再调用setFrameFromDiagonal
Rectangle2D rect=new Rectangle2D.Double(leftX, topY, width, height);
g2.draw(rect);
//绘制椭圆
//创建Ellipse2D.Double对象
Ellipse2D ellipse = new Ellipse2D.Double();
//用外接矩形构造椭圆
ellipse.setFrame(rect);
g2.draw(ellipse);
//绘制对角线
//Line2D.Double(startX,startY,endX,endY)
g2.draw(new Line2D.Double(leftX,topY,leftX+width,topY+height));
//绘制圆形
double centerX = rect.getCenterX();
double centerY=rect.getCenterY();
double radius=150; //半径
Ellipse2D circle= new Ellipse2D.Double();
circle.setFrameFromCenter(centerX, centerY, centerX+radius, centerY+radius);
g2.draw(circle);
}
public Dimension getPreferredSize(){
return new Dimension(DEFAULT_WIDTH,DEFAULT_HEIGHT);
}
}
6.6. 使用颜色
- 使用Graphics2D类的setPaint方法可以为图形环境上所有后续的绘制操作选择颜色。
g2.setPaint(Color.RED);
g2.drawString(warning,100,100);
- 只需要将调用draw替换为调用fill就可以用一种颜色填充一个封闭图形的内部。
Rectangle2D rect=...;
g2.setPaint(Color.RED);
g2.fill(rect);
- 要想绘制多种颜色,就需要按照选择颜色、绘制图形,再选择另一种颜色、再绘制图形的过程实施。
- Color类用于定义颜色。在java.awt.Color类中提供了13个预定义的常量,表示13种标准颜色(BLACK,BLUE,CYAN…)。
- 也可以通过提供红、绿、蓝三色成分来创建一个color对象,以达到定制颜色的目的。
g2.setPaint(new Color(0,128,128));
- 要想设置背景颜色,需要使用Component类中的setBackground方法。
MyComponent p=new MyComponent();
p.setBackground(Color.PINK);
- Color类中有brighter()方法和darker()方法,可以加亮或变暗当前的颜色。实际上,brighter()只稍微加亮一点点。要达到耀眼的效果,需要调用三次这个方法:
c.brighter().brighter().brighter();
- SystemColor类中预定义了很多颜色的名字。在这个类的常量,封装了用户系统的各个元素的颜色,例如
p.setBackground(SystemColor.window);
它把面板的背景颜色设定为用户桌面上所有窗口使用的默认颜色。当希望让绘制的用户界面元素与用户桌面上已经存在的其他元素的颜色匹配时,使用SystemColor类中的颜色非常有用。 - -
6.7. 文本使用特殊字体
- 可以通过字体名(font face name)指定一种字体。字体名由”Helvetica”这样的字体家族名(font family name)和一个可选的”Bold”后缀组成。
- 要想知道某台特定计算机 上允许使用的字体,就需要调用GraphicsEnvironment类中的getAvailableFontFamilyNames方法。这个方法返回一个字符型数组,其中包含了所有可用的字体名。GraphicsEnvironment类描述了用户系统的图形环境,为了得到这个类的对象,需要调用getLocalGraphicsEnvironment方法。
String[] fontNames=GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames();
- 字体中有外观相似的仿制品。如Helvetica的仿制品就是windows中成为Arial的字体。为了创建一个公共基准,AWT定义了五个逻辑字体名:SansSerif,Serif,Monospaced,Dialog,DialogInput。这些字体奖将被映射到计算机上的实际字体。如,在Windows系统中,SansSerif将被映射到Arial.
- 要想使用某种字体绘制字符,必须首先利用指定的字体名、字体风格和字体大小创建一个Font类对象。
Font sansbold14=new Font("SansSerif",Font.BOLD,14);
字体风格的值可以是:Font.PLAIN,Font.BOLD,Font.ITALIC,Font.BOLD+Font.ITALIC.
/* 使用字体,并把字符串绘制在屏幕中央 */
public class FontTest {
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
public void run() {
JFrame frame=new FontFrame();
frame.setTitle("FontTest");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
});
}
}
class FontFrame extends JFrame{
public FontFrame(){
add(new FontComponent());
pack();
}
}
class FontComponent extends JComponent{
private static final int DEFAULT_WIDTH=300;
private static final int DEFAULT_HEIGHT=200;
public void paintComponent(Graphics g){
Graphics2D g2= (Graphics2D)g;
String message= "Hello world";
Font font=new Font("Serif", Font.BOLD, 36);
//设置字体
g2.setFont(font);
//获取包围字符串的矩形。矩形的高度是上坡度、下坡度和行间距的总和。
FontRenderContext context=g2.getFontRenderContext();
Rectangle2D bounds=font.getStringBounds(message, context);
//计算字符串两侧剩余的空间
double x=(getWidth()-bounds.getWidth())/2;
double y=(getHeight()-bounds.getHeight())/2;
double ascent=-bounds.getY();//上坡度
double baseY=y+ascent;
g2.drawString(message, (int)x, (int)baseY);
g2.setPaint(Color.LIGHT_GRAY);
//绘制基线
g2.draw(new Line2D.Double(x,baseY,x+bounds.getWidth(),baseY));
//绘制长方形
Rectangle2D rect=new Rectangle2D.Double(x,y,bounds.getWidth(),bounds.getHeight());
g2.draw(rect);
}
public Dimension getPreferredSize(){
return new Dimension(DEFAULT_WIDTH,DEFAULT_HEIGHT);
}
}
6.8. 显示图像
/* 图像平铺 */
public class ImageTest {
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
public void run() {
JFrame frame=new ImageFrame();
frame.setTitle("ImageTest");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
});
}
}
class ImageFrame extends JFrame{
public ImageFrame(){
add(new ImageComponent());
pack();
}
}
class ImageComponent extends JComponent{
private static final int DEFAULT_WIDTH=300;
private static final int DEFAULT_HEIGHT=400;
private Image image;
public ImageComponent(){
//读取图像
image =new ImageIcon("pic.png").getImage();
}
public void paintComponent(Graphics g){
if(image==null) return;
int imageWidth= image.getWidth(this);
int imageHeight=image.getHeight(this);
//先在左上角显示图像的一个拷贝
//再使用copyArea将图像拷贝到整个窗口
//最后一个参数是绘制进程中以通告为目的的对象(可能为null)
g.drawImage(image, 0,0,null);
for (int i = 0; i*imageWidth <=getWidth() ; i++) {
for (int j = 0; j*imageHeight <=getHeight(); j++) {
if(i+j>0)
g.copyArea(0, 0, imageHeight, imageHeight, i*imageWidth, j*imageHeight);
}
}
}
public Dimension getPreferredSize(){
return new Dimension(DEFAULT_WIDTH, DEFAULT_HEIGHT);
}
}