【Java】Swing 设计技巧

Java Swing 设计技巧

一、所见即所得编辑器的代码形式

我们先以Qt Creator为例,看看它是如何把C++代码自动生成出来的。当我们看界面代码时,我们会发现它的组件变量全都声明在一个类中。

QToolBox *toolBox;
QWidget *page;
QLabel *label;
QLabel *label_2;
QLabel *label_3;
QLabel *label_4;
QLabel *label_5;
QLineEdit *lineEdit;
QLineEdit *lineEdit_2;
QLineEdit *lineEdit_3;
QLineEdit *lineEdit_4;
QLineEdit *lineEdit_5;
// 省略

每个变量的实例化以及对坐标、宽高之类的属性的设置都是在当前类的一个方法中进行的。

void setupUi(QWidget *Widget)
{
	if (Widget->objectName().isEmpty())
		Widget->setObjectName(QString::fromUtf8("Widget"));
	Widget->resize(800, 600);
	toolBox = new QToolBox(Widget);
	toolBox->setObjectName(QString::fromUtf8("toolBox"));
	toolBox->setGeometry(QRect(40, 100, 181, 461));
	page = new QWidget();
	page->setObjectName(QString::fromUtf8("page"));
	page->setGeometry(QRect(0, 0, 181, 341));
	label = new QLabel(page);
	label->setObjectName(QString::fromUtf8("label"));
	label->setGeometry(QRect(10, 10, 51, 31));
	// 省略
}

由此可见,当我们写Java UI界面时,我们完全可以像Qt Creator一样,将变量的定义、实例化以及相关属性的设置全都放到一个类中。这样有一个好处,那就是各个对象之间都是可视的,当我们去做一个类似于将一个button添加到panel中去的操作时,我们只需要调用panel的add(Component comp)方法就行了,非常的方便。

但是,我们这么做会带来一个问题,当我们想要对其中某个地方进行修改的时候,比如,如果我做完这个程序之后,某个label的位置太靠下了,我想修改一下它的坐标。那么,设置这个label坐标的代码在哪里来着?

如果这个程序本身就比较简短,比如只有不到一百行,那我们就能很快地定位到想找的代码。但是,如果程序代码有成千上万行呢?我们是不是也要一行一行地去找呢?

为什么Qt Creator能这么写呢?因为找代码的工作并不是由程序员来承担的。程序员需要看到的、需要操作的,只有ui的图形设计界面。当我们在用Java Swing写界面代码的时候,为了增强程序的可扩展性,也为了让后期便于我们去维护程序,这种写法是不行的。

那我们该如何去规划我们的程序代码呢?对于新手而言,只要一个原则,那就是让写的程序便于维护。为此,我们需要将我们的程序划分为多个层次。

二、层次化设计

1.JFrame

作为一个界面而言,最外层是frame。在Swing中有一个名为JFrame的类,JFrame继承自Frame,Frame继承自Window,Window继承自Container,Container继承自Component。一般而言,为了得到期望中的组件,我们往往是让我们的组件去继承Swing中组件,然后我们便能继承里面的配置并且使用里面自带的方法。

import javax.swing.*;

public class MainFrame extends JFrame {
    public MainFrame() {
        setTitle("Java Swing 设计技巧");
        setBounds(400, 250, 1000, 600);
        setVisible(true);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }
}

这里的setTitle(String title)实现了ConsoleWindow接口,是修改界面的标题栏。

setBounds(int x, int y, int width, int height)继承自Component的方法,其中的四个参数分别是界面的横坐标、纵坐标、宽、高,某些时候,当我们并不想同时设置这四个属性时,我们可以使用setLocation(int x, int y)setSize(int width, int height)来分别设置坐标和宽高。

setVisible(boolean b)也实现了ConsoleWindow接口,当里面的参数为true时,这个frame才能被我们看到。

如果我们不加setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE)这一行代码的话,当我们点击界面上的×时,虽然界面确实是关闭了,但是程序还没有正常退出,这一点我们可以从控制台上看出来。其实,在点击×时,我们可以整点别的花样,比如弹出一个询问窗口,询问用户是不是真的要退出,这里我们可以用下面这行代码替换掉上述构造方法中的最后一句。

setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
addWindowListener(new WindowAdapter() { 
	@Override
    public void windowClosing(WindowEvent e) {
    	if (JOptionPane.showConfirmDialog(null, 
    		"是否要退出" + Constant.FRAME_TITLE + "?", "退出", JOptionPane.OK_CANCEL_OPTION, 
    		JOptionPane.INFORMATION_MESSAGE) == JOptionPane.OK_OPTION) {
    		System.exit(0);
		}
	}
});

这里不要忘记引入相关的类。

import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;

2.JPanel

JPanel继承自JCompoment,JComponent继承自Container,Container继承自Component。

在层次化设计中,panel起到非常重要的作用,重要到如果没有panel,我们的分层将很难进行。为什么呢?对比与frame,panel可以在一个界面中同时出现多个,甚至可以嵌套使用;而与label和button而言,panel中可以放很多组件。panel的这些特性非常有利于我们将系统的设计进行模块化,比如说,如果我们需要一个导航栏,我们完全可以将导航栏的内部的界面设计和功能在一个继承自JPanel的类中实现。

import javax.swing.*;
import java.awt.*;

public class Navigation extends JPanel {

    private NavButton[] buttons;

    public Navigation(int x, int y){
        setBounds(x, y, 1000, 60);
        setBackground(new Color(36, 41, 46));
        setLayout(null);
        
        buttons = new NavButton[3];
        String[] texts = {"点赞", "收藏", "关注"};
        for(int i = 0; i < buttons.length; i ++){
            buttons[i] = new NavButton(50 + 110 * i, 10, texts[i]);
            add(buttons[i]);
        }
    }
    
    public NavButton[] getButtons() {
        return buttons;
    }
    
    private static class NavButton extends JButton {
		// 具体细节后文中会提到
    }
}

这里我们在导航栏构造方法中设置了两个参数,分别是它的横纵坐标。实际上,当我们把导航栏代码从主界面上分离出去并抽象为导航栏类的时候,从导航栏的角度看,它并不知道谁会调用它,所以也就不知道当它显示在界面上的时候会是在什么位置。当某个panel调用导航栏时,如果想让它显示在最左上方,那声明和实例化时只需要进行以下步骤。

Navigation nav = new Navigation(0, 0);
add(nav);

是不是很简洁?其实,如果上述的panel如果需要将该导航栏对象进行传递的话,我们可能需要将Navigation对象声明在全局变量中,而如果panel不调用导航栏构造方法以外的方法,也就是要把导航栏只用来显示,那我们甚至连变量都不用声明。

add(new Navigation(0, 0));

setBackground(Color bg)继承自Component的方法,就是设置背景色,setForeground(Color fg)与它用法相似,但后者是设置字体的颜色。这里的颜色选择有时候也很关键,我们在做某个程序界面的时候,用Color类自带的常量比如Color.Red的话,从用户的角度来看,这种颜色太过于生硬了,我们有时候需要自己找合适的颜色。上面导航栏的背景色new Color(36, 41, 46)就是GitHub导航栏的背景色,程序整体的风格也可以按照GitHub的方式。当然,对于新手而言,我们也完全可以按照其他网站的风格去设计我们自己的界面。

setLayout(LayoutManager mgr)是设置容器内部的布局方式,常见的布局方式有FlowLayout、BorderLayout、GridLayout,我个人倾向于不使用这些布局方式,只往里面传一个null,这样的话我们就需要给容器里面的每一个组件都设置坐标和宽高。但有个别情况例外,比如说,我想在ScrollPane(滚动面板)中添加一些组件,这时使用FlowLayout让各个内部组件流式排列即可。

对于向导航栏中添加的按钮,从导航栏本身来看,它并没有必要去关心这些按钮的颜色是什么,它只需要给按钮一个坐标即可。而具体的按钮细节,我们可以将它封装在NavButton类中。

这就有点分而治之的思想了,要设计一个相对复杂的、能实现多个功能的用户界面,我们完全可将这个大的问题分解成多个小的问题,如果这个小问题还不好解决,那就把它继续分解,直到我们能够解决。

这里的导航栏可以认为是一个子界面,里面的按钮也可以认为是导航栏的子界面,其他的一些业务界面也完全可以设计成一个个的子界面,每个子界面只需要把它内部的组件布局和功能做好,只对外提供必要的接口即可。

下面我们再以界面切换功能为例,阐述一下这样做的好处。

假如我们需要Like、Collect、Follow三个子界面,当我们点击上述导航栏的按钮时,显示的界面会发生变化,比方说,当我点击“点赞”按钮时,Like界面就会显示。

我们发现,这三个类有很多相同的属性比如宽高和布局方式,当导航栏中的某个按钮被点击时,这三个界面也会以类似的方式显示或者隐藏。既然它们有这么多相同的地方,那将这些共同点放到一个抽象类中再让这三个类去继承这个抽象类不失为一个好办法。

import javax.swing.*;
import java.awt.*;

public abstract class GoodBehavior extends JPanel {
    GoodBehavior(){
        setBounds(50, 100, 900, 400);
        setLayout(null);
        launch();
    }
    public abstract void launch();
}

对于三个类中的区别,我们通过重写launch()方法实现。

import java.awt.*;

public class Like extends GoodBehavior {
    @Override
    public void launch() {
        setBackground(Color.RED);
    }
}
import java.awt.*;

public class Collect extends GoodBehavior {
    @Override
    public void launch() {
        setBackground(Color.GREEN);
    }
}
import java.awt.*;

public class Follow extends GoodBehavior {
    @Override
    public void launch() {
        setBackground(Color.YELLOW);
    }
}

这样的话,当我们去将这三个功能界面实例化的时候,来自父类的构造方法中的launch()会根据实际对象不同而调用不同类中的方法。

GoodBehavior[] goodBehaviors = new GoodBehavior[3];
goodBehaviors[0] = new Like();
goodBehaviors[1] = new Collect();
goodBehaviors[2] = new Follow();

那切换界面该如何去实现呢?首先,我们需要将功能界面的对象和导航栏中按钮的对象放到同一个类或者同一个方法中。但是,导航栏和另外三个功能界面在主面板中(我们将它命名为mainPanel),而按钮在导航栏中,因为我们将导航栏中的细节代码完全从mainPanel中移出去了,所以此时三个功能界面是无法“看”到按钮的,类似的道理,按钮也“看”不到功能界面的对象。

功能界面在外层,导航按钮在内层,要想把它们放在同一层,要么把按钮拿到外层,要么把功能界面拿到内层。在前文的导航栏类中,我们发现我们将内部按钮声明成一个全局变量,且有个方法就是返回这个按钮对象。那在mainPanel中,在将实例化导航栏对象之后,我们可以利用getButtons()方法将button拿出来,然后再赋予它切换界面的功能。

private NavButton[] navButton;
private int currentPanelIndex = 0;

private void changePanel() {
	for(int i = 0; i < 3; i ++){
		add(goodBehaviors[i]);
		goodBehaviors[i].setVisible(false);
		int finalI = i;
		buttons[i].addMouseListener(new MouseAdapter() {
		    @Override
		    public void mouseClicked(MouseEvent e) {
		    	goodBehaviors[currentPanelIndex].setVisible(false);
		        goodBehaviors[finalI].setVisible(true);
		        currentPanelIndex = finalI;
		    }
		});
	}
}

从上面的代码中,我们发现了一个之前没用过的方法addMouseListener(MouseListener l),其作用是给鼠标添加监听事件,包括点击、移入、移出、按下、松开。这里的MouseListener是一个interface,在里面定义了鼠标监听方法。而MouseAdapter实现了MouseListener,不过里面的方法体为空。当我们去使用它的时候,需要根据需要重写部分方法。

在这里,我们重写了mouseClicked(MouseEvent e),当我们点击这个按钮的时候,系统便会调用这个方法,让当前panel设为不可见,并让对应索引的panel显示,这样界面的切换就完成了。值得一说的是,虽然这种方式确实把界面切换了,但是从mainPanel的角度看,这三个功能界面一直是存在的。

另外,我们并没有给MouseAdapter类一个变量名,这种用法叫做“匿名内部类”,因为实例化对象之后直接就进入到组件的监听事件里了,只用了一次,也确实没必要声明个变量来保存它。如果某个监听事件会用到两次,比如在登录的时候,我既想点击登录按钮进行登录,又想在密码框中敲回车进行登录,那完全值得给这个对象一个变量名了。

如果我们希望某些界面经常使用的话,只设置它的可见性的话可以避免子界面频繁的出入总界面。但是,也存在某些界面用的次数足够少的情况,比如嵌入到整个主界面中的登录界面,我们只会在一开始使用一次,登录成功后,一般情况下是不会再次返回到该界面。这样的话,让一个用不到的界面一直在主界面中隐藏着确实不划算,这时我们换一种更换界面的方式,用例和上面的一样。

private int currentPanelIndex = -1;

private void changePanel() {
	for(int i = 0; i < 3; i ++){
		int finalI = i;
		buttons[i].addMouseListener(new MouseAdapter() {
		    @Override
		    public void mouseClicked(MouseEvent e) {
		    	if(currentPanelIndex != -1) {
		    		remove(goodBehaviors[currentPanelIndex]);
		    	}
		    	add(goodBehaviors[finalI]);
		        currentPanelIndex = finalI;
		        repaint();
		    }
		});
	}
}

首先currentPanelIndex的初始值设为了-1,因为后文中我们会把当前索引的功能界面移除,与简单地设置可见性属性不同,如果想要移除一个组件的话,我们需要保证这个组件在当前界面里面,否则,程序可能找不到要移除的组件,然后出错。

后面的切换界面我们的逻辑不再是设置可见性了,而是实实在在地将组件添加和移除。这里不要忘记在后面加一句repaint(),不然的话,界面不一定能刷新成功。

这就是将按钮对象从导航栏中拿到主界面的方式,还有一种方式是把主界面对象拿到导航栏中。那有人就问了,子界面中有主界面,主界面中有子界面,这样不就陷入死循环了吗?其实,我们并不是在导航栏中new一个主界面对象,而是将原来主界面对象的地址传进来,如此,导航栏中的按钮就可以“看到”主界面并可以调用主界面中的公开方法了。为此,我们要修改一下Navigation类。

    public Navigation(int x, int y, MainPanel mainPanel){
        // 导航栏属性及内部组建的初始化
    }

这里我们修改了导航栏的构造方法,那我们在主界面中实例化导航栏的时候也要更改一下要传递的参数。

add(new Navigation(0, 0, this));

这种从外层向内层传递的方式有时候会特别好用,特别是当程序需要实现的功能比较多的时候。虽然我们把子界面内部组件的细节全部封装在子界面内部,只把可能会与主界面或其他子界面有功能联系的组件给return出去,但是,如果程序本身的功能比较多,那么把一些组件和功能实现全都交给mainPanel的话,对于mainPanel来说,其负担还是比较大的,在这种时候,就推荐使用向内层传地址的形式。

3.JButton

如果要想让用户能与界面进行的一定的交互的话,按钮作为视图模块与控制模块之间最重要的桥梁,在我们的程序设计中往往是必不可少的,我们先来看一些上文省略过去的导航栏中按钮的代码。

private static class NavButton extends JButton {

	private static Color NAV_FG = Color.WHITE;
    private static Color NAV_BG = new Color(36, 41, 46);
    private static Color NAV_FG_ENTER = Color.BLACK;
    private static Color NAV_BG_ENTER = Color.YELLOW;

    private NavButton(int x, int y, String text) {
        setBounds(x, y, 70, 40);
        setText(text);
        setColor(Constant.NAV_FG, Constant.NAV_BG);
        setFont(new Font("黑体", Font.PLAIN, 30));
        setBorder(null);

        addMouseListener(new MouseAdapter() {
            @Override
            public void mouseEntered(MouseEvent e) {
                setColor(Constant.NAV_FG_ENTER, Constant.NAV_BG_ENTER);
            }

            @Override
            public void mouseExited(MouseEvent e) {
                setColor(Constant.NAV_FG, Constant.NAV_BG);
            }
        });

        setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
    }
    
    private void setColor(Color fore, Color back) {
        setForeground(fore);
        setBackground(back);
    }
}

我们这里把导航栏按钮设为一个私有内部类,原因在于我希望只在导航栏中使用这个按钮类,而不希望在其他地方调用它。当然,如果我们想要做一个通用的按钮的话,那我们完全可以把它做成一个公开的类。

如果程序代码量比较多而且后期可能会经常改动的话,我们最好建一个Constant类来专门存储这些常量。一方面,我们在修改某些组件属性的时候,我们只需要打开这个常量类进行修改,不必非要打开这个组件类;另一方面,我们设置的某个类的属性可能需要参照其他类的属性,比如导航栏的宽度可能会和frame的宽度一样,这时候,把frame的宽度设为常量并且把导航栏的宽度设成该常量是有必要,一旦后期我们把frame的宽度修改了,那么此时的导航栏宽度也会跟着改变。

给鼠标添加监听事件,除了实现某些实实在在的系统功能之外,我们还可以单纯地给鼠标改变样式,比如当鼠标移入的时候,按钮变个颜色,鼠标移出的时候,按钮的颜色回复成原来的样式。

setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR))的意思是,当鼠标移到按钮上时,鼠标会变成小手的外观,便于用户能是一眼识别出这是一个按钮。这个方法因为是所有按钮都被建议使用的,如果每个按钮都要写一行这个代码的话,没什么意义,那我们可以写一个XButton继承JButton,把公共代码写到XButton里面,然后当我们需要写其他特定的按钮类的话,直接继承XButton就可以了。

这里有一个问题,什么是按钮?或者这么问,满足什么条件才能算是按钮呢?是不是只要继承JButton才能是按钮,不继承就不是按钮吗?还是说只要实现按钮的功能就算是按钮了?

值得一说的是,addMouseListener(MouseListener l)setCursor(Cursor cursor)都是来自Component类,这样的话我们写个panel类,然后也给它添加个鼠标监听,也让鼠标移到它上面时变成小手,那么,从用户角度看,这跟按钮没什么区别。既然没区别,当我们发现继承JButton的按钮满足不了需求了,不妨用panel替代它,然后伪装成按钮。

panel有个优势,那就是我们可以往里添加很多组件,如果我们想给一个“按钮”添加个图标,再添加个文字,那么我们完全可以在panel里加几个标签实现。

三、MVC设计模式

有一种比较有名的架构模式叫做“MVC框架”,Model、View和Controller。

前面的界面设计就是View,是能呈现给用户的,用户能直接操作的也是View。

Model是模型,比如学生模型。假如某个模型中存储着大量学生信息,如果我们想要成绩排序,那我们直接调用相关的方法,然后方法的返回值就是排好序的成绩列表。至于排序过程用到了什么算法,都在这个模型的某个方法中,调用者只要结果,不需要知道具体细节。

Controller的作用主要是将View和Model联系起来,比如用户点击了某个按钮,模型中的数据就有可能会发生改变。

这里提及MVC的目的还是为了让程序代码分离,一些业务逻辑代码其实并没有必要写在视图部分的代码里,不妨用静态方法或者写一个代理类的方式,把这些代码从视图代码中拿出来,使得我们的代码逻辑更加清晰。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值