绘制象棋棋盘 | Java界面实战三
- 继承
-
- 1. 继承的概念
- 2. 继承的特点
- 3. 继承的格式
- 4. 继承的说明
- 5. 在界面中的运用
- 6. 创建界面
- Graphics 绘制
-
- 1. 画直线(drawLine)
- 2. 绘制矩形
- 3. 绘制椭圆与圆弧
- 4. 绘制多边形
- 5. 绘制文字
- 6. 设置颜色与字体
- 7. 绘制图像
- 棋盘绘制
-
- 1. 网格线
- 2. 楚河汉界
- 3. 棋盘美化:背景图片与防遮挡
- 4. 绘制棋子
- 完整源码整合:GoUI、MyFrame、XQ 三合一
-
- GoUI类
- MyFrame类
- XQ类
继承
在正式开始之前,我们需要掌握继承的相关知识
1. 继承的概念
继承是面向对象编程中的重要特性。子类通过继承父类,可以获得父类的属性和方法,并在此基础上进行扩展和改造。
- 接口:通过一个类实现接口,类中必须重写接口中的方法。
- 类继承:通过继承另一个类,也可以重写这个类的方法。
2. 继承的特点
3. 继承的格式
class 子类名 extends 父类名 {
// 子类内容
}
4. 继承的说明
- 子类可以替代父类。
- 子类基于父类方法进行扩展。
5. 在界面中的运用
在编写带有图形界面的游戏时,我们会创建一个自己的类,让它继承 JFrame,从而拥有窗体的基本能力。
public class MyFrame extends JFrame {
// 示例:继承 JFrame 后,可以重写窗体的刷新方法
// 重写paint方法 窗体被改变尺寸时 会自动调用
@Override
public void paint(Graphics g) {
super.paint(g);// 绘制原本的窗体中的白板
// 在这里添加需要绘制到界面上的内容
}
}
程序讲解
public class MyFrame extends JFrame
定义一个自己的窗体类 MyFrame,并通过 extends 继承 JFrame。这样 MyFrame 就直接拥有了窗体的基本能力,例如标题栏、可调整大小、可关闭等。
@Override
@Override 是一个注解,表示下面的 paint 方法不是新写一个普通方法,而是在重写父类已有的方法。加上它以后,如果我们把方法名写错,例如写成 Paint,编译器会直接提示错误,帮我们提前发现问题。
public void paint(Graphics g)
paint 是窗体需要重新绘制时自动调用的方法。例如:
- 窗体第一次显示时;
- 窗体大小被改变时;
- 窗体被遮挡后又重新显示时。
Graphics g 可以理解为一支“画笔”,后面要在界面上画棋盘、画游戏页面,主要就是通过这个 g 来完成。
super.paint(g);
super 代表父类对象。这里调用父类的 paint 方法,意思是:先让 JFrame 完成它原本该做的绘制工作,再执行我们自己添加的绘制内容。
如果不写 super.paint(g);,父类原有的基础绘制逻辑可能不会执行,窗口看起来会不完整,例如背景没有被正常刷新。
自定义绘制内容
在注释 // 在这里添加需要绘制到界面上的内容 的位置,就可以使用 g 来绘制自己的内容,例如:
g.drawRect(x, y, width, height); // 画矩形
g.drawString("游戏开始", x, y); // 画文字
棋盘、棋子、游戏页面等都可以通过这一步骤绘制出来。
6. 创建界面
package Frame0813;
import javax.swing.*;
public class GoUI {
public void showUI(){
MyFrame jf = new MyFrame();
jf.setTitle("象棋");
jf.setSize(900, 900);
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
jf.setVisible(true);
}
static void main(){
GoUI ui = new GoUI();
ui.showUI();
}
}
在代码中,重点关注这一句:MyFrame jf = new MyFrame();。
在实战一、二中,我们写的都是 JFrame jf = new JFrame();,为什么这里要换成 MyFrame 呢?主要原因有两点:
- MyFrame 继承自 JFrame,所以它本质上仍然是一个窗体,JFrame 原有的方法都可以继续使用。
- MyFrame 中重写了 paint(Graphics g) 方法。只有创建 MyFrame 对象,窗体刷新时才会执行我们自定义的绘制逻辑;如果仍然创建 JFrame jf = new JFrame();,就不会调用 MyFrame 中的 paint 方法,后面也就无法在窗体上画出棋盘和游戏页面。
因此,MyFrame jf = new MyFrame(); 可以理解为:创建了一个“带自定义绘制能力”的窗体。
运行效果:

Graphics 绘制
了解完接口后,我们再来看一看 Graphics 绘制。Graphics 可以理解为前面 paint(Graphics g) 中的那支“画笔”,接下来画直线、矩形、文字以及棋盘,都要依靠它来完成。
1. 画直线(drawLine)
- drawLine(int x1, int y1, int x2, int y2):画直线,从 (x1, y1) 到 (x2, y2)。
g.drawLine(10, 10, 100, 100);

2. 绘制矩形
- drawRect(int x, int y, int width, int height):画空心矩形,x,y 是左上角坐标。
- fillRect(int x, int y, int width, int height):填充实心矩形。
g.drawRect(50, 50, 200, 100); // 空心矩形
g.fillRect(300, 50, 200, 100); // 实心矩形

3. 绘制椭圆与圆弧
- drawOval(int x, int y, int w, int h):画椭圆;当 w = h 时就是圆。
- fillOval(int x, int y, int w, int h):填充实心椭圆或圆。
- drawArc(int x, int y, int w, int h, int startAngle, int arcAngle):画圆弧。
- fillArc(int x, int y, int w, int h, int startAngle, int arcAngle):填充扇形。
注:在画圆弧与扇形时,startAngle 表示起始角度,arcAngle 表示从起始角度开始扫过的角度。在 Java 的坐标系中,0° 指向右侧,角度沿逆时针方向增加。
g.drawOval(50, 200, 100, 100); // 正圆
g.fillOval(200, 200, 100, 60); // 实心椭圆
g.drawArc(350, 200, 100, 100, 0, 180); // 半圆弧
g.fillArc(500, 200, 100, 100, 0, 90); // 扇形

说明:矩形和椭圆的 x,y 坐标都表示它们“外接矩形”的左上角位置。因此,只要让矩形和椭圆的 x,y,w,h 保持一致,就能画出一个矩形的内切椭圆;当 w = h 时,这个内切椭圆就变成了矩形的内切圆。
// 先画矩形,再画出它的内切圆
g.drawRect(50, 50, 100, 100);
g.drawOval(50, 50, 100, 100);

4. 绘制多边形
- drawPolygon(int[] xPoints, int[] yPoints, int nPoints):画多边形。
- fillPolygon(int[] xPoints, int[] yPoints, int nPoints):填充多边形。
参数说明:
- xPoints:多边形所有顶点的 x 坐标数组。
- yPoints:多边形所有顶点的 y 坐标数组;xPoints[i] 与 yPoints[i] 共同确定第 i 个顶点。
- nPoints:实际参与绘制的顶点数量,通常等于数组长度。如果数组较长但只想使用前面几个顶点,可以传入更小的数值。
绘制时,程序会按照顶点顺序依次连线,最后自动连接首尾顶点,形成封闭的多边形。
下面再通过两个例子加深理解。
示例 1:绘制正五边形
// 1. 绘制正五边形:5 个顶点均匀分布在圆周上,相邻顶点夹角 72°
int[] xs = {150, 207, 185, 115, 93};
int[] ys = {340, 381, 449, 449, 381};
g.drawPolygon(xs, ys, 5);

示例 2:小房子
// 2. 先画三角形屋顶
int[] roofX = {150, 200, 250};
int[] roofY = {300, 250, 300};
g.drawPolygon(roofX, roofY, 3);
// 再画四边形墙体
int[] bodyX = {160, 160, 240, 240};
int[] bodyY = {300, 350, 350, 300};
g.drawPolygon(bodyX, bodyY, 4);
这里通过组合两个多边形,就能画出一个小房子。实际项目中很多复杂图形也是这样由基础多边形拼接出来的。 
5. 绘制文字
- drawString(String str, int x, int y):绘制文字,x,y 是文字左下角位置。
g.drawString("Hello Java", 20, 500);
6. 设置颜色与字体
- setColor(Color c):设置画笔颜色。
- setFont(Font font):设置字体。
g.setColor(Color.RED);
g.setFont(new Font("宋体", Font.BOLD, 20));
三个结合的效果:
g.setColor(new Color(12,123,25));
g.setFont(new Font("宋体", Font.BOLD, 30));
g.drawString("Hello Java", 20, 500);

7. 绘制图像
- ImageIcon(String filename):根据文件路径加载一张图片,生成 ImageIcon 对象。
- getImage():从 ImageIcon 对象中取出真正用于绘制的 Image 图片。
- drawImage(Image img, int x, int y, int width, int height, ImageObserver observer):把图片绘制到界面上。
- x,y:图片左上角在窗口中的坐标。
- width,height:图片绘制后的宽度和高度;如果不写这两个参数,则按图片原本大小绘制。
- observer:通常直接写 null。
ImageIcon img = new ImageIcon("D:\\\\CodeTeach\\\\Java\\\\chinesechess1.png");
g.drawImage(img.getImage(), 0, 0, 900, 900, null);
代码说明
- new ImageIcon("…") 用来加载本地电脑上的棋盘背景图片;路径中的 \\\\ 是 Java 字符串里 \\ 的转义写法。
- img.getImage() 取出 ImageIcon 中的 Image 对象,再交给 g 去绘制。
- g.drawImage(img.getImage(), 0, 0, 900, 900, null) 表示从窗口左上角 (0, 0) 开始,把图片绘制成 900 × 900 的大小。
- 如果图片路径写错或文件不存在,图片会加载失败,界面上就看不到背景图;实际使用时需要先确认图片位置是否正确。
效果: 
棋盘绘制
1. 网格线
在画之前,我们需要先定义一批绘制棋盘时要用到的常量。这样后面画网格线、画棋子时都可以直接使用这些变量,统一修改位置和大小也会更加方便。
- X = 80:棋盘左上角的 x 坐标,决定棋盘在窗口中的横向起点。
- Y = 100:棋盘左上角的 y 坐标,决定棋盘在窗口中的纵向起点。
- SIZE = 80:每个格子的间距,也就是相邻两条线之间的距离。SIZE 是棋盘绘制的关键参数,后面调整棋盘大小主要就是修改它。
- ROW = 9:棋盘的行数,表示横向排列的行数。
- COL = 8:棋盘的列数,表示纵向排列的列数。
// 棋盘绘制参数
int X = 80; // 棋盘左上角 x 坐标
int Y = 100; // 棋盘左上角 y 坐标
int SIZE = 80; // 每个格子的间距
int ROW = 9; // 棋盘行数
int COL = 8; // 棋盘列数
接着,我们使用 for 循环来画网格线。网格线的本质,就是由多条横线和竖线交叉组成的:因此,我们只需要分别控制每条横线、竖线的位置,就能用循环批量绘制出完整棋盘。
package Frame0813;
import javax.swing.*;
import java.awt.*;
public class MyFrame extends JFrame {
int X = 80,Y=100,SIZE=80,ROW=9,COL=8;
@Override
public void paint(Graphics g) {
super.paint(g);
for (int i = 0; i <=ROW; i++) {
g.drawLine(X,Y+i*SIZE,X+COL*SIZE,Y+i*SIZE);
}
for (int i = 0; i <=COL; i++) {
g.drawLine(X+i*SIZE,Y,X+i*SIZE,Y+ROW*SIZE);
}
}
}

2. 楚河汉界
象棋棋盘中间是楚河汉界,通常不需要竖线,而是一段空白区域。绘制时,我们可以先画一个白色矩形,把中间多余的网格线覆盖住,让棋盘中央自然形成楚河汉界。
g.setColor(Color.WHITE);
g.fillRect(X, Y+4*SIZE, (ROW–1)*SIZE, SIZE);
最后,在白色矩形上绘制“楚河汉界”四个字:
g.setColor(Color.BLACK);
String str1="楚",str2="河",str3="汉",str4="界";
Font font = new Font("微软雅黑",Font.BOLD,60);
g.setFont(font);
g.drawString(str1,X+SIZE,Y+SIZE*5–20);
g.drawString(str2,X+SIZE*2,Y+SIZE*5–20);
g.drawString(str3,X+SIZE*5,Y+SIZE*5–20);
g.drawString(str4,X+SIZE*6,Y+SIZE*5–20);
除了楚河汉界,象棋棋盘上下两端还各有一个九宫格。我们还需要在上下两个九宫格中各画两条交叉的斜线,组成“米”字格。九宫格中的斜线主要用于标出“士”的行走路线:士每次只能沿着这些斜线移动一格,因此画好斜线后,九宫格区域才完整。
g.drawLine(X+SIZE*3,Y,X+SIZE*5,Y+SIZE*2);
g.drawLine(X+SIZE*5,Y,X+SIZE*3,Y+SIZE*2);
g.drawLine(X+SIZE*3,Y+SIZE*9,X+SIZE*5,Y+SIZE*7);
g.drawLine(X+SIZE*3,Y+SIZE*7,X+SIZE*5,Y+SIZE*9);

3. 棋盘美化:背景图片与防遮挡
棋盘绘制完成后,整体略显单调。这里分两步处理:
ImageIcon img = new ImageIcon("D:\\\\CodeTeach\\\\Java\\\\chinesechess1.png");
g.drawImage(img.getImage(), 0, 0, 900, 900, null);

g.setColor(Color.ORANGE);
g.fillRect(X–SIZE/2,Y–SIZE/2,SIZE*COL+SIZE,ROW*SIZE+SIZE);

4. 绘制棋子
棋子数量较多,逐个手写会非常麻烦。这里改用二维数组保存棋子布局,再通过循环统一遍历绘制。
更多 Java 数组与 switch 语句讲解,可参考:Java数组相关知识
先定义棋盘参数和初始棋子数组。数组中的数字表示棋子类型,0 表示空位:
public class XQ {
int X = 80,Y=100,SIZE=80,ROW=9,COL=8;
int[][] initChessArr = {
{5, 4, 3, 2, 1, 2, 3, 4, 5},
{0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 6, 0, 0, 0, 0, 0, 6, 0},
{7, 0, 7, 0, 7, 0, 7, 0, 7},
{0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0}
};
}
接着用双层循环遍历数组,读取每个位置的值;值不为 0 时,表示这里有棋子,调用 drawChess 进行绘制:
// 棋盘初始化棋子
public void initChess(Graphics g) {
for (int i = 0; i < 10; i++) {
for (int j = 0; j < 9; j++) {
int num = initChessArr[i][j];
if (num != 0) {
drawChess(g,num,i,j);
}
}
}
}
drawChess 方法负责绘制单个棋子:
public void drawChess(Graphics g, int num,int i,int j) {
String str = "";
switch (num) {
case 1 -> str = "帅";
case 2 -> str = "仕";
case 3 -> str = "相";
case 4 -> str = "车";
case 5 -> str = "马";
case 6 -> str = "炮";
case 7 -> str = "兵";
}
int cx = j*SIZE+X–SIZE/2;
int cy = i*SIZE+Y–SIZE/2;
g.setColor(Color.GREEN);
g.fillOval(cx,cy,SIZE,SIZE);
g.setColor(Color.BLACK);
Font font = new Font("微软雅黑", Font.BOLD, 45);
g.setFont(font);
g.drawString(str,cx+SIZE/6,cy+55);
}
最后,在 paint 方法中创建 XQ 对象并调用棋子绘制方法:
XQ xq = new XQ();
xq.initChess(g);// 绘制棋子
效果:
当然,你也可以不画棋盘板子: 
完整源码整合:GoUI、MyFrame、XQ 三合一
GoUI类
package Frame0813;
import javax.swing.*;
public class GoUI {
public void showUI(){
MyFrame jf = new MyFrame();
jf.setTitle("象棋 ");
jf.setSize(900, 900);
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
jf.setVisible(true);
}
static void main(){
GoUI ui = new GoUI();
ui.showUI();
}
}
MyFrame类
package Frame0813;
import javax.swing.*;
import java.awt.*;
public class MyFrame extends JFrame {
int X = 80,Y=100,SIZE=80,ROW=9,COL=8;
@Override
public void paint(Graphics g) {
super.paint(g);
// 背景图
ImageIcon img = new ImageIcon("D:\\\\CodeTeach\\\\Java\\\\chinesechess1.png");
g.drawImage(img.getImage(), 0, 0, 900, 900, null);
// 棋盘板子
g.setColor(Color.ORANGE);
g.fillRect(X–SIZE/2,Y–SIZE/2,SIZE*COL+SIZE,ROW*SIZE+SIZE);
// 网格线
g.setColor(Color.BLACK);
for (int i = 0; i <=ROW; i++) {
g.drawLine(X,Y+i*SIZE,X+COL*SIZE,Y+i*SIZE);
}
for (int i = 0; i <=COL; i++) {
g.drawLine(X+i*SIZE,Y,X+i*SIZE,Y+ROW*SIZE);
}
// 楚河汉界
g.setColor(Color.WHITE);
g.fillRect(X, Y+4*SIZE, (ROW–1)*SIZE, SIZE);
g.setColor(Color.BLACK);
String str1="楚",str2="河",str3="汉",str4="界";
Font font = new Font("微软雅黑",Font.BOLD,60);
g.setFont(font);
g.drawString(str1,X+SIZE,Y+SIZE*5–20);
g.drawString(str2,X+SIZE*2,Y+SIZE*5–20);
g.drawString(str3,X+SIZE*5,Y+SIZE*5–20);
g.drawString(str4,X+SIZE*6,Y+SIZE*5–20);
// 九宫格
g.drawLine(X+SIZE*3,Y,X+SIZE*5,Y+SIZE*2);
g.drawLine(X+SIZE*5,Y,X+SIZE*3,Y+SIZE*2);
g.drawLine(X+SIZE*3,Y+SIZE*9,X+SIZE*5,Y+SIZE*7);
g.drawLine(X+SIZE*3,Y+SIZE*7,X+SIZE*5,Y+SIZE*9);
// 绘制棋子
XQ xq = new XQ();
xq.initChess(g);
}
}
XQ类
package Frame0813;
import java.awt.*;
public class XQ {
int X = 80,Y=100,SIZE=80,ROW=9,COL=8;
int[][] initChessArr = {
{5, 4, 3, 2, 1, 2, 3, 4, 5},
{0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 6, 0, 0, 0, 0, 0, 6, 0},
{7, 0, 7, 0, 7, 0, 7, 0, 7},
{0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0}
};
// 棋盘初始化棋子
public void initChess(Graphics g) {
for (int i = 0; i < 10; i++) {
for (int j = 0; j < 9; j++) {
int num = initChessArr[i][j];
if (num != 0) {
drawChess(g,num,i,j);
}
}
}
}
public void drawChess(Graphics g, int num,int i,int j) {
String str = "";
switch (num) {
case 1 -> str = "帅";
case 2 -> str = "仕";
case 3 -> str = "相";
case 4 -> str = "车";
case 5 -> str = "马";
case 6 -> str = "炮";
case 7 -> str = "兵";
}
int cx = j*SIZE+X–SIZE/2;
int cy = i*SIZE+Y–SIZE/2;
g.setColor(Color.GREEN);
g.fillOval(cx,cy,SIZE,SIZE);
g.setColor(Color.BLACK);
Font font = new Font("微软雅黑", Font.BOLD, 45);
g.setFont(font);
g.drawString(str,cx+SIZE/6,cy+55);
}
}



