[Java] 自定义UI窗口
0 目录
文章目录
1 前言
在经历过很多课设之后,我发现每次设计UI都需要占用我一大半的时间。为了之后的便利,索性写了几个窗口的封装模板,期末写课设的时候调用即可。本篇博客将手把手教会读者如何创建一个自己自定义的窗口,还附带很多拓展(例如:窗口组件随窗口变化而调整、JFrame的窗口偏移)。
警告:本片内容需要读者拥有一定的Java的awt包使用经验;本篇文章可能存在版本差异;本篇文章并非严格的设计模式。
2 准备阶段
我写过很多次的封装模板,但是每一次几乎都不太合格,也就在最近,通过老师的点评和自己的努力,已经总结出一些最重要的步骤:
2.1 思考需求
对于这一点,你需要知道你想写这个窗口干嘛、或者分为几个部分。我这里举个例子:在我做完课设的时候,我发现一般的课设都包含三个重要的面板:展示、信息、other。那么我将我的需求设定为:展示,信息,其他。那么我会将这三个模板设计为show、message、other面板,然后在窗口中调用。
JPanel show;
JPanel message;
JPanel other;
2.2 设计UI
通过在纸上或者在PPT里面设计一个窗口UI,此处UI不需要太过于细分,因为这是一个模板。之后使用的时候也可能需要调整其中面板的大小。所以,仅仅需要将面板的大致关系写出来即可。其实就跟使用HTML一样,先将面板的具体位置关系设计出来。下面是我设计的基本UI:
2.3 设计变量
因为我们仅仅需要模板,之后的窗口比例是用户来自定义,则窗口的一些变量设置很重要。例如左右的占比大小,上下的占比大小,这些都可以让之后的使用和扩展更为方便。因为上面被分为了三个部分,那么可以使用下面的变量来控制组件的位置和大小:
int verticalProportion;
int horizontalProportion;
int gapPx;
JPanel content;
其中gapPx代表组件间隙单位是px;content面板用来装左边的面板,方便调整左右比例。
2.4 设计代码实现
我们可以确定以下的属性变量:
public class MyFrame extend JFrame {
private JPanel show;
private JPanel message;
private JPanel other;
private JPanel content;
private int verticalProportion;
private int horizontalProportion;
private int gapPx;
}
最后不要忘记添加构造器和访问器(content除外)。
3 具体实现
3.1 用户使用
如何让用户使用这个窗口类?我思考了两个方案:
- 构造方法传参。用户通过组合的方式,将自定义三个面板,然后通过构造方法传参的方式,将面板放入指定的MyFrame中。
- 重写自定义三个方法——initShow、initMessage、initOther。用户通过继承的方式,重写面板的初始化方法达到用户可自定义效果。
但实际想,若使用第二方案——继承,那么用户没有通过方法的方式修改参数,而是重写的方式修改了父类的参数,这与private属性相违背。简单来说就是违背了封装的设计特性。使得类的参数暴露在用户中。因为笔者暂时还没有学习设计模式,无法想到更加优秀的方法。
在此篇文章使用第一个方法——构造方法传参。
3.2 默认值
设置默认值,提供给用户一个无参构造,然后用户通过无参构造就可以看到默认的MyFrame窗口了。为了方便设置默认值,我们阶梯式设置四个构造方法:
public MyFrame() {
this(new JPanel(), new JPanel(), new JPanel());
}
public MyFrame(JPanel show, JPanel message, JPanel other) {
this(show, message, other, 1500, 900);
}
public MyFrame(JPanel show, JPanel message, JPanel other, int frameWidth, int frameHeight) {
this(show, message, other, frameWidth, frameHeight, 80, 80, 10);
}
public MyFrame(JPanel show, JPanel message, JPanel other,
int frameWidth, int frameHeight, int verticalProportion, int horizontalProportion, int gapPx) {
}
然后通过对最后一个构造方法设计即可。
3.3 实现UI
3.3.1 将构造方法分为不同的子方法
为了便利和分模块写代码,我们将构造方法分开写,这样可以避免出错和方便Debug。
- initPane——初始化面板组件
- initData——初始化数据
- initUI——初始化窗口和组件属性
- initLocation——初始化组件位置
- initListener——初始化监听器
public MyFrame(JPanel show, JPanel message, JPanel other,
int frameWidth, int frameHeight, int verticalProportion, int horizontalProportion, int gapPx) {
initPane(show, message, other);
initData(verticalProportion, horizontalProportion, gapPx);
initUI(frameWidth, frameHeight);
initLocation();
initListener();
}
private void initPane(JPanel show, JPanel message, JPanel other) {}
private void initData(int verticalProportion, int horizontalProportion, int gapPx) {}
private void initUI(int frameWidth, int frameHeight) {}
private void initLocation() {}
private void initListener() {}
前两个都是初始化属性变量,最后一个则是添加监听,是为了之后的扩展。我们仅需要正对initUI和initLocation即可。下面笔者将介绍UI的一些awt的特性。
3.3.2 JPanel的常用方法和JFrame的特性
JPanel的常用方法:
JPanel jp1 = new JPanel();
JPanel jp2 = new JPanel();
jp1.add(jp2);
jp1.setBackground(new Color(0xFFFFFF));
jp1.setLayout(null);
jp1.setBounds(x, y, width, height);
-
add——jp2添加进jp1;
-
setBackGround——设置背景颜色;
-
setLayout——设置布局,null为自由布局;
-
setBounds——x、y为父面板的起始点坐标;width、height为横跨和竖跨像素大小;
JFrame的特性:
- JFrame只能存放最多三个面板。超过添加也不会显示在窗口中。
- JFrame和JPanel的默认布局的设定为BorderLayout,该布局无法自由设置组件的大小和位置。
- JFrame的默认关闭方式为窗口关闭不会结束程序。
- JFrame存在XY轴偏移。Y轴偏移:程序设置的窗口大小包括了窗口的控制栏内容;X轴偏移:不清楚也可能不存在。
对于JFrame的特性问题,笔者给出对应的解决方案:
-
设置一个JPanel名叫box,然后将box添加进入窗口。之后的所有窗口都添加进入box即可;
-
我们仅需要将box的布局设置为null,就可以实现内部组件的自定义大小和位置;
-
可以使用下面方法,设置关闭窗口后程序结束:
JFrame frame = new JFrame(); frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
-
通过笔者测试,X轴偏移量为16个像素,Y轴偏移量是39个元素。
3.3.3 initUI方法
private void initUI(int frameWidth, int frameHeight) {
setSize(frameWidth, frameHeight);
JPanel box = new JPanel();
content = new JPanel();
add(box);
box.add(content);
box.add(message);
content.add(show);
content.add(other);
box.setLayout(null);
content.setLayout(null);
message.setLayout(null);
show.setLayout(null);
other.setLayout(null);
setBackground(new Color(0xf2f2f2));//grey
content.setBackground(new Color(0xf2f2f2));
message.setBackground(new Color(0xFFFFFF));//white
show.setBackground(new Color(0xFFFFFF));
other.setBackground(new Color(0xFFFFFF));
}
3.3.4 initLocation方法
通过减法可以严格地扩充面板。
private void initLocation(){
//offsetX/offsetY
int frameWidth = getFrameWidth() - 16;
int frameHeight = getFrameHeight() - 39;
//get the px size after calculating the proportion
double verticalRatio = verticalProportion / 100.0;
double horizontalRatio = horizontalProportion / 100.0;
int contentWidth = (int) ((frameWidth - 3 * gapPx) * verticalRatio);
int messageWidth = frameWidth - contentWidth - 3 * gapPx;
int boxHeight = frameHeight - 2 * gapPx;
int showHeight = (int) ((boxHeight - gapPx) * horizontalRatio);
int otherHeight = boxHeight - gapPx - showHeight;
content.setBounds(gapPx, gapPx, contentWidth, boxHeight);
message.setBounds(contentWidth + gapPx * 2, gapPx,messageWidth, boxHeight);
show.setBounds(0, 0, contentWidth, showHeight);
other.setBounds(0, showHeight + gapPx, contentWidth, otherHeight);
}
3.3.5 initPane和initData方法
这两个方法很简单,目的就是为了传递参数。因为不同类型存在不同的判定,为了后续的发展,这里分开两边写,当然也可以一起写。
private void initPane(JPanel show, JPanel message, JPanel other) {
this.show = show;
this.message = message;
this.other = other;
}
private void initData(int verticalProportion, int horizontalProportion, int gapPx) {
this.verticalProportion = verticalProportion;
this.horizontalProportion = horizontalProportion;
this.gapPx = gapPx;
}
3.3.6 测试
最后在该类里面写一个main方法测试一下:
public static void main(String[] args) {
new MyFrame().setVisible(true);
}
效果如下:
4 效果扩展
4.1 动态窗口
我们可以通过添加监听,实现动态窗口。主要思路为:
- 使用窗口监听,获取窗口大小是否被调整了。
- 若窗口大小被重新调整了,我们使用initLocation方法重置位置。
具体实现如下代码:
private void initListener() {
addComponentListener(new ComponentAdapter() {
@Override
public void componentResized(ComponentEvent e) {
initLocation();
}
});
}
在main中创建两个相同的窗口,移动效果如下:
过程非常流畅,丝毫不卡顿。
5 最终代码
(别忘了设置package)
import javax.swing.*;
import java.awt.*;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
/**
* @author ghsont
*/
public class MyFrame extends JFrame {
private JPanel show;
private JPanel message;
private JPanel other;
private JPanel content;
private int verticalProportion;
private int horizontalProportion;
private int gapPx;
public static void main(String[] args) {
new MyFrame().setVisible(true);
}
public MyFrame() {
this(new JPanel(), new JPanel(), new JPanel());
}
public MyFrame(JPanel show, JPanel message, JPanel other) {
this(show, message, other, 1500, 900);
}
public MyFrame(JPanel show, JPanel message, JPanel other, int frameWidth, int frameHeight) {
this(show, message, other, frameWidth, frameHeight, 80, 80, 10);
}
public MyFrame(JPanel show, JPanel message, JPanel other,int frameWidth, int frameHeight, int verticalProportion, int horizontalProportion, int gapPx) {
initPane(show, message, other);
initData(verticalProportion, horizontalProportion, gapPx);
initUI(frameWidth, frameHeight);
initLocation();
initListener();
}
private void initPane(JPanel show, JPanel message, JPanel other) {
this.show = show;
this.message = message;
this.other = other;
}
private void initData(int verticalProportion, int horizontalProportion, int gapPx) {
this.verticalProportion = verticalProportion;
this.horizontalProportion = horizontalProportion;
this.gapPx = gapPx;
}
private void initUI(int frameWidth, int frameHeight) {
setSize(frameWidth, frameHeight);
JPanel box = new JPanel();
content = new JPanel();
add(box);
box.add(content);
box.add(message);
content.add(show);
content.add(other);
box.setLayout(null);
content.setLayout(null);
message.setLayout(null);
show.setLayout(null);
other.setLayout(null);
setBackground(new Color(0xf2f2f2));
content.setBackground(new Color(0xf2f2f2));
message.setBackground(new Color(0xFFFFFF));
show.setBackground(new Color(0xFFFFFF));
other.setBackground(new Color(0xFFFFFF));
}
private void initListener() {
addComponentListener(new ComponentAdapter() {
@Override
public void componentResized(ComponentEvent e) {
initLocation();
}
});
}
private void initLocation() {
int frameWidth = getWidth() - 16;
int frameHeight = getHeight() - 39;
double verticalRatio = verticalProportion / 100.0;
double horizontalRatio = horizontalProportion / 100.0;
int contentWidth = (int) ((frameWidth - 3 * gapPx) * verticalRatio);
int messageWidth = frameWidth - contentWidth - 3 * gapPx;
int boxHeight = frameHeight - 2 * gapPx;
int showHeight = (int) ((boxHeight - gapPx) * horizontalRatio);
int otherHeight = boxHeight - gapPx - showHeight;
content.setBounds(gapPx, gapPx, contentWidth, boxHeight);
message.setBounds(contentWidth + gapPx * 2, gapPx,messageWidth, boxHeight);
show.setBounds(0, 0, contentWidth, showHeight);
other.setBounds(0, showHeight + gapPx, contentWidth, otherHeight);
}
public JPanel getShow() {
return show;
}
public void setShow(JPanel show) {
this.show = show;
}
public JPanel getMessage() {
return message;
}
public void setMessage(JPanel message) {
this.message = message;
}
public JPanel getOther() {
return other;
}
public void setOther(JPanel other) {
this.other = other;
}
public int getVerticalProportion() {
return verticalProportion;
}
public void setVerticalProportion(int verticalProportion) {
this.verticalProportion = verticalProportion;
}
public int getHorizontalProportion() {
return horizontalProportion;
}
public void setHorizontalProportion(int horizontalProportion) {
this.horizontalProportion = horizontalProportion;
}
public int getGapPx() {
return gapPx;
}
public void setGapPx(int gapPx) {
this.gapPx = gapPx;
}
}
6 个人感悟
大一开始一直想写一个窗口类,然后大二上寒假写了好久,反反复复遇到很多问题。期间自己还不断地扩充内容,最后弄得不好扩展,代码写得像小说一样。最近终于对这个抱有了一些好奇,经历了两三天的时间,最后代码重写成功了。相比原来版本的代码更加符合代码的设计,但实际上肯定存在某些不足,例如content的设计比较别扭;其实我原本的代码还加入了frameWidth和frameHeight两个属性,这个其实完全没必要的,最后一次重写让我修改了;对于比例值还需要判定大小,我懒没写,读者可以实现。
这次让我学到了:
- 分方法——具体需求具体分析;
- 窗口调整如此简单。
之后,我还会根据类似的设计写出其他窗口。若读者需要,可以联系我邮箱。