中国象棋程序的设计与实现(十一)--棋盘绘制算法(尽管注释非常详细,完全理解仍有难度)
上几篇中,我们详细介绍了,棋盘类的定义和关键属性,简要介绍了棋盘绘制算法的骨架。
本篇,我们将详细解读棋盘绘制算法的每一个细节。
强烈建议,大家结合文章末尾的“棋盘截图”来思考绘制算法细节,不然,很可能会遇到问题。
有些绘制细节,很难懂,不好描述,不再详细叙述。
1.绘制算法骨架
/** * 绘制棋盘 * <P> * 绘制棋盘背景 * </P> * <P> * 10条横线 * <P> * 9条纵线 * </P> * <P> * 炮兵卒14个标记 * </P> * <P> * 九宫格 * </P> * <P> * 楚河漢界 * </P> * <P> * 如果有棋子移动,画出2个提示框,每个提示框由8条线组成 * </P> * <P> * 绘制可选走法的提示框 * </P> * <P> * 绘制竖线标记 * </P> * * * 根据需要还绘制棋子移动的标记 */ protected void paintComponent(Graphics g) { super.paintComponent(g); // 绘制棋盘背景 drawBackgroundImage(g); Graphics2D g2 = (Graphics2D) g; // 兵、卒、炮标记笔画 BasicStroke bsFlag = new BasicStroke(2); // 楚河汉界、棋盘边框笔画 BasicStroke bsLine = new BasicStroke(2); // 棋盘线笔画 BasicStroke bs1 = new BasicStroke(1); // 绘制直线 drawLines(g2, bsLine, bs1); // 绘制九宫格 drawJiuGongLines(g2, bs1); // 绘制楚河漢界 drawChuheHanjieString(g2); // 绘制炮和兵标记 drawPaoBingFlag(g2, bsFlag); // 如果有棋子移动,画出2个提示框,每个提示框由8条线组成 drawMoveFlag(g2); // 绘制可选走法的提示框 drawWillMoveFlag(g2); // 设置字体和线宽,为画坐标做准备 BasicStroke bsOld = new BasicStroke(1); g2.setStroke(bsOld); g2.setFont(new Font("宋体", Font.PLAIN, 14)); g2.setColor(new Color(0, 0, 0)); // 绘制竖线标记 drawShuXianFlag(g2); }
2.绘制算法细节
2.1绘制棋盘背景
/** * 绘制棋盘背景 */ private void drawBackgroundImage(Graphics g) { //获得棋盘背景 Image image = getBackgroundImage(); if (image != null) { Dimension size = new Dimension(super.getWidth(), super.getHeight()); //在指定的矩形区域绘制image图像 g.drawImage(image, 0, 0, size.width, size.height, null); } } // 默认不绘制背景图片 protected Image getBackgroundImage() { return null; }
需要特别说明的是,这里用到了“模版方法模式”。
具体的子类可以重载getBackgroundImage方法,从而可以自定义背景图像。
2.2绘制直线(10条横线和9条竖线)
private void drawLines(Graphics2D g2, BasicStroke bsLine, BasicStroke bs1) { // 10条横线 for (int j = 1; j <= Y; j++) { // 4条需要加粗的横线 if (j == 1 || j == 5 || j == 6 || j == 10) { g2.setStroke(bsLine); // TODO 重复代码可以提取出来 g2.drawLine(chessPoints[1][j].getX(), chessPoints[1][j].getY(), chessPoints[X][j].getX(), chessPoints[X][j].getY()); } // 6条不需要加粗的横线 else { g2.setStroke(bs1); g2.drawLine(chessPoints[1][j].getX(), chessPoints[1][j].getY(), chessPoints[X][j].getX(), chessPoints[X][j].getY()); } } // 9条纵线 for (int i = 1; i <= X; i++) { // 中间的纵线 if (i != 1 && i != X) { g2.setStroke(bs1); //上半区的纵线 g2.drawLine(chessPoints[i][1].getX(), chessPoints[i][1].getY(), chessPoints[i][Y - 5].getX(), chessPoints[i][Y - 5].getY()); //下半区的纵线 g2.drawLine(chessPoints[i][Y - 4].getX(), chessPoints[i][Y - 4].getY(), chessPoints[i][Y].getX(), chessPoints[i][Y].getY()); } // 两边的加粗的纵线 else { g2.setStroke(bsLine); g2.drawLine(chessPoints[i][1].getX(), chessPoints[i][1].getY(), chessPoints[i][Y].getX(), chessPoints[i][Y].getY()); } } }
2.3绘制九宫格
private void drawJiuGongLines(Graphics2D g2, BasicStroke bs1) { // 红黑双方将帅的九宫格,4条斜线 g2.setStroke(bs1); g2.drawLine(chessPoints[4][1].getX(), chessPoints[4][1].getY(), chessPoints[6][3].getX(), chessPoints[6][3].getY()); g2.drawLine(chessPoints[6][1].getX(), chessPoints[6][1].getY(), chessPoints[4][3].getX(), chessPoints[4][3].getY()); g2.drawLine(chessPoints[4][8].getX(), chessPoints[4][8].getY(), chessPoints[6][Y].getX(), chessPoints[6][Y].getY()); g2.drawLine(chessPoints[4][Y].getX(), chessPoints[4][Y].getY(), chessPoints[6][8].getX(), chessPoints[6][8].getY()); }
2.4绘制楚河漢界4个汉字
private void drawChuheHanjieString(Graphics2D g2) { // 楚河、汉界 g2.setFont(new Font("宋体", Font.PLAIN, 32)); g2.drawString("漢 界", chessPoints[2][5].getX(), chessPoints[2][5].getY() + 2 * UNIT_HEIGHT / 3 + 2); g2.drawString("楚 河", chessPoints[6][5].getX(), chessPoints[2][5].getY() + 2 * UNIT_HEIGHT / 3 + 2); }
2.5绘制炮和兵的位置标记
private void drawPaoBingFlag(Graphics2D g2, BasicStroke bsFlag) { // 画炮和兵的位置的标记 int size = sidePoints.size(); // 棋子中心点到标记直角边交点的水平距离 double x = PIECE_WIDTH / 9; // 标记的长度 double side = PIECE_WIDTH / 6; for (int i = 0; i < size; i++) { double a = sidePoints.get(i).getX(); double b = sidePoints.get(i).getY(); g2.setStroke(bsFlag); if (i >= 0 && i <= 9) { //绘制中间的炮兵10个棋子 drawPBMiddle(g2, x, side, a, b); } else if (i == 10 || i == 11) { //右边的1个卒和1个兵 TODO 方法名称不够合理 drawPBRight(g2, x, side, a, b); } else if (i == 12 || i == 13) { //左边的1个卒和1个兵 TODO 方法名称不够合理 drawPBLeft(g2, x, side, a, b); } } }//绘制中间的炮兵10个棋子,1个完整的标记由8条线构成 private void drawPBMiddle(Graphics2D g2, double x, double side, double a, double b) { // 左上角 g2.drawLine((int) (a - x), (int) (b - x), (int) (a - x), (int) (b - x - side)); g2.drawLine((int) (a - x), (int) (b - x), (int) (a - x - side), (int) (b - x)); // 左下角 g2.drawLine((int) (a - x), (int) (b + x), (int) (a - x), (int) (b + x + side)); g2.drawLine((int) (a - x), (int) (b + x), (int) (a - x - side), (int) (b + x)); // 右上角 g2.drawLine((int) (a + x), (int) (b - x), (int) (a + x), (int) (b - x - side)); g2.drawLine((int) (a + x), (int) (b - x), (int) (a + x + side), (int) (b - x)); // 右下角 g2.drawLine((int) (a + x), (int) (b + x), (int) (a + x), (int) (b + x + side)); g2.drawLine((int) (a + x), (int) (b + x), (int) (a + x + side), (int) (b + x)); }
2.6如果有棋子移动,画出2个提示框,每个提示框由8条线组成
先计算出,棋子起始位置的坐标,然后绘制提示框。
类似于“绘制炮和兵的位置标记”。
2.7绘制可选走法的提示框
先计算出,所有可选走法位置的坐标,然后绘制提示框。
类似于“绘制炮和兵的位置标记”。
2.8绘制竖线标记
// 默认,竖线标记1到9,一到九,是按照红方在下,黑方在上绘制的。如果子类不应该这样话,应该重载此方法,重新绘制。 //方便棋手走棋,“馬八进七”。 protected void drawShuXianFlag(Graphics2D g2) { // 绘制上方的1到9 for (int i = 1; i <= X; i++) { g2.drawString("" + i, i * UNIT_WIDTH - 4, UNIT_HEIGHT / 2 - 4); } // 绘制下方的一到九 for (int i = 1; i <= X; i++) { g2.drawString("" + ChessUtils.numToZi(10 - i), i * UNIT_WIDTH - 4, 10 * UNIT_HEIGHT + 34); } }
3.棋盘效果
4.总结
棋盘绘制算法的核心思路就是,定制算法骨架,分别实现每一个子算法。
主要用到的是Java图形界面和绘图类库,包括Swing GUI, Graphics、Graphics2D、BasicStroke。
如果有疑问,建议,读者多参考Java API文档。
今后,如有可能,我们再详细介绍这些类库的用法。
5.痛点
绘制棋盘是中国象棋程序非常有难度的一个问题。
限于时间、耐心、表达能力有限,本文仅仅是较为详细地介绍了一部分算法细节。
如果想要更好的介绍,1种方式是“当面交流”,或者是“图文并茂”。
可惜,后2种方式,不够现实,太麻烦。
今后如有可能,我会尝试后面2种方式的。
相关阅读
http://FansUnion.cn/articles/2919