Feng.Li's Java See

          抓緊時間,大步向前。
          隨筆 - 95, 文章 - 4, 評論 - 58, 引用 - 0
          數(shù)據(jù)加載中……

          個Java畫圖板程序的設(shè)計

          本文講述一個畫圖板應(yīng)用程序的設(shè)計,屏幕抓圖如下。這篇文章帶有三個附件,其中兩個jar文件都是j2sdk1.4.2_08編譯打包,包含源代碼,可執(zhí)行,如下表:
          附件名稱及鏈接 詳情
          jDraw_basic.jar 本文是基于這個基本版本的,屏幕抓圖顯示的也是這個基本版本的界面。
          jDraw_extended.jar 在基礎(chǔ)版本上稍加擴展,加入文件讀存功能,即可將所畫的圖存入一個模型文件(特定的格式,見下)或者從文件中讀取,也可以將其導(dǎo)出到一個PNG格式的文件。由于擴展功能不是本文的重點,并且也不復(fù)雜,所以文中就不在對其進行闡述。它的源代碼只是在基本版本上增加了一些內(nèi)容。
          jdraw_demo.zip 屏幕抓圖中的圖形的模型文件,屬于純文本格式,為了節(jié)省空間,將其壓縮了一下,解壓縮取出其中的jdraw_demo.jdw文件后再使用。按理說,SGML/XML的格式才是正途,不過這只是個簡單的應(yīng)用,不用那么大動干戈了,就走個“邪道”吧:)

          『IShape』

          這是所有圖形類(此后稱作模型類)都應(yīng)該實現(xiàn)接口,外部的控制類,比如畫圖板類就通過這個接口跟模型類“交流”。名字開頭的I表示它是一個接口(Interface),這是eclipse用的一個命名法則,覺得挺有用的,就借鑒來了。這個接口定義了兩個方法:

          public void draw(java.awt.Graphics2D g);每個實現(xiàn)IShape的類都在這個方法里面指定它的圖形顯示代碼。public void processCursorEvent(java.awt.event.MouseEvent evt, int type);這個方法是在圖形(被用戶)繪制過程中,發(fā)生相關(guān)的鼠標(biāo)點擊和移動事件時調(diào)用的。第一個參數(shù)就是所發(fā)生的鼠標(biāo)事件對象;第二個參數(shù)取值于IShape所定義的三個常數(shù):RIGHT_PRESSED, LEFT_RELEASED,和CURSOR_DRAGGED。

          下面這個class diagram顯示了所有圖形類的結(jié)構(gòu)圖。FreeShape, RectBoundedShape,和PolyGon這三個類直接實現(xiàn)了IShape接口。其中,F(xiàn)reeShape和RectBoundedShape是抽象類,分別代表不規(guī)則圖形(比如鉛筆畫圖)和以一個長方形為邊界的規(guī)則圖形,由于分屬于這兩個類別的圖形對于鼠標(biāo)事件的處理基本上都是一致的,所以就抽象出來這兩個父類,避免重復(fù)代碼。PolyGon是一個具體類,它的命名沒有采用Polygon是為了避免同java.awt.Polygon重名。它代表的圖形是多邊形,由于它獨特的鼠標(biāo)處理方式,它不屬于上面兩種類型圖形的任何一種,所以它直接實現(xiàn)了IShape接口。

          IShape接口所定義的兩個方法到底是怎么被用到的呢?這個問題現(xiàn)在還不能立刻解答。在下面的部分,我們先講述FreeShape所定義的不規(guī)則圖形及其兩個具體子類PolyLine和Eraser,然后在這個基礎(chǔ)上講述一個縮略版的畫圖板類,到那個時候,上面問題的答案也就自然揭曉了。之后,我們再繼續(xù)講述其他的圖形類。

          『FreeShape』

          講到FreeShape,我們不得不先說一下PointsSet這個類。這是一個util類,被FreeShape和PolyGon用到,代表一個有序的點集合,并提供方便的方法來加入新的點和讀取點坐標(biāo)。為了方便對模型類代碼的理解,這里列出PointsSet類的API。

          public PointsSet();用默認(rèn)的初始容量(10)創(chuàng)建一個對象。public PointsSet(int initCap);用指定的初始容量(initCap)創(chuàng)建一個對象。public void addPoint(int x, int y);加入一個新的點到這個集合的末端;如果舊的末端點跟新的點重合,則不重復(fù)加入。public int[][] getPoints();將所有點以一個二維數(shù)組(int[2][n])返回。第一行是x坐標(biāo),第二行是y坐標(biāo)。public int[][] getPoints(int x, int y);類似上一個方法,只是最后將參數(shù)指定的點加在末尾(無論是否跟集合末端的點重合);這個方法只被PolyGon用到。

          好了,來看下面代碼中FreeShape對IShape接口的實現(xiàn)。FreeShape有三個屬性變量:color, stroke,和pointsSet。權(quán)限設(shè)成protected當(dāng)然是給子類用啦。color就是色彩了,stroke用來指定使用線條的粗細(xì)(當(dāng)然,Stroke類的對象還可以指定交接點形狀之類的屬性,不過這里都使用其默認(rèn)值了),pointsSet當(dāng)然就是包含了所有控制點(這里叫控制點似乎不太恰當(dāng),因為其實無法利用這些點來“控制”的,不過也想不到其他恰當(dāng)?shù)拿郑瓦@么叫吧)集合。值得注意的是構(gòu)造函數(shù)里面包含了起始點的坐標(biāo),這個點在函數(shù)里面被加到了控制點集中。

          這類圖形對鼠標(biāo)事件的處理很簡單,它只對IShape.CURSOR_DRAGGED類型的事件感興趣,每當(dāng)發(fā)生這類事件的時候,就把鼠標(biāo)拖拽到的新的點加入到控制點集中。當(dāng)然了,根據(jù)上面看到的PointsSet.addPoint(int,int)這個方法的“個性”,這個點是否真的被加入還要看它是否跟舊的末端點重合。

          import java.awt.*;import java.awt.event.MouseEvent;public abstract class FreeShape implements IShape {        protected Color color;    protected Stroke stroke;    protected PointsSet pointsSet;      protected FreeShape(Color c, Stroke s, int x, int y) {        pointsSet = new PointsSet(50);        color = c;        stroke = s;        pointsSet.addPoint(x, y);    }        public void processCursorEvent(MouseEvent e, int t) {        if (t != IShape.CURSOR_DRAGGED)            return;        pointsSet.addPoint(e.getX(), e.getY());    }}

          FreeShape類沒有實現(xiàn)IShape接口的draw(Graphics2D)方法,很明顯,這個方法是留給子類來完成的。PolyLine和Eraser繼承了FreeShape,分別代表鉛筆繪出的圖形和橡皮擦。其中PolyLine的構(gòu)造函數(shù)結(jié)構(gòu)跟其父類相似,直接調(diào)用父類的super方法來完成;相比之下,Eraser類就有點“叛逆”了,它的參數(shù)里面用一個JComponent替換了Color。Eraser類是通過畫出跟畫圖板背景色彩一致的線條來掩蓋原有圖形而實現(xiàn)橡皮擦的效果的,但由于畫圖板的背景色是可以調(diào)的(見抓圖的Color Settings部分),直接給Eraser的構(gòu)造函數(shù)一個色彩對象不太合適,所以干脆將畫圖板自己(JComponent)傳了進來,這樣,每次Eraser設(shè)定圖形色彩時,都直接問畫圖板要它的背景色。來看一下PolyLine對draw(Graphics2D)方法的實現(xiàn):

              public void draw(Graphics2D g) {        g.setColor(color);        g.setStroke(stroke);        int[][] points = pointsSet.getPoints();        int s = points[0].length;        if (s == 1) {            int x = points[0][0];            int y = points[1][0];            g.drawLine(x, y, x, y);        } else {            g.drawPolyline(points[0], points[1], s);        }    }

          這個方法里面有一個if-else結(jié)構(gòu),由于構(gòu)造函數(shù)里面已經(jīng)將起始點加入控制點集中,所以pointsSet.getPoints()會至少返回一個點。利用Graphics.drawPolyline(int[],int[],int)畫圖時,如果只有一個點,它是不會畫出來東西的,所以檢查一下點數(shù),如果只有一個,則改用Graphics.drawLine(int,int,int,int)將這個點畫出來。Eraser的draw(Graphics2D)方法跟上面基本上完全一樣,只是傳給Graphics.setColor(Color)的參數(shù)是通過JComponent.getBackground()得到的。

          『TestBoard』

          現(xiàn)在就來看一個精簡版的畫圖板類:TestBoard。下面的代碼,是通過代碼注釋進行解釋的。需要注意的是,TestBoard本身還不能直接運行,需要把它放到一個JFrame里面才行。同時畫圖工具的切換也需要外部的控件來處理。不過這些都比較簡單了,就不多說了。

          import java.awt.*;import java.awt.event.*;import javax.swing.*;import java.util.ArrayList;public class TestBoard extends JPanel                          implements MouseListener, MouseMotionListener {        //定義一些常量    public static final int TOOL_PENCIL = 1;    public static final int TOOL_ERASER = 2;    public static final Stroke STROKE = new BasicStroke(1.0f);    public static final Stroke ERASER_STROKE = new BasicStroke(15.0f);    private ArrayList shapes;     //保存所有的圖形對象(IShape)    private IShape currentShape;  //指向當(dāng)前還未完成的圖形    private int tool; //代表當(dāng)前使用的畫圖工具(TOOL_PENCIL或TOOL_ERASER)    public TestBoard() {        //進行一些初始化        shapes = new ArrayList();        tool = TOOL_PENCIL;        currentShape = null;                //安裝鼠標(biāo)監(jiān)聽器        addMouseListener(this);        addMouseMotionListener(this);    }        //外部的控制界面可以通過這個方法切換畫圖工具    public void setTool(int t) {        tool = t;    }        //override JPanel的方法。通過調(diào)用IShape.draw(Graphics2D)方法來顯示圖形    protected void paintComponent(Graphics g) {        super.paintComponent(g);        int size = shapes.size();        Graphics2D g2d = (Graphics2D) g;        for (int i=0; i<size; i++) {            ((IShape) shapes.get(i)).draw(g2d);        }    }        public void mousePressed(MouseEvent e) {        /* 當(dāng)左鍵點擊時,currentShape肯定指向null。根據(jù)當(dāng)前畫圖工具創(chuàng)建相應(yīng)圖形對象,           將currentShape指向它,并把這個對象加入到對象集合(shapes)中。另外,調(diào)用           repaint()方法將畫圖板的畫面更新一下。 */        if (e.getButton() == MouseEvent.BUTTON1) {            switch (tool) {            case TOOL_PENCIL:                currentShape = new PolyLine(getForeground(),                                                STROKE, e.getX(), e.getY());                break;            case TOOL_ERASER:                currentShape = new Eraser(this, ERASER_STROKE,                                                e.getX(), e.getY());                break;            }            shapes.add(currentShape);            repaint();        /* 當(dāng)右鍵點擊并且currentShape不指向null時,調(diào)用currentShape的          processCursorEvent(MouseEvent,int)方法,類型參數(shù)是      IShape.RIGHT_PRESSED。 repaint()*/        } else if (e.getButton() == MouseEvent.BUTTON3 && currentShape != null) {            currentShape.processCursorEvent(e, IShape.RIGHT_PRESSED);            repaint();        }    }        public void mouseDragged(MouseEvent e) {        /* 當(dāng)鼠標(biāo)拖拽并且currentShape不指向null時(這種情況下,左鍵肯定處于          按下狀態(tài)),調(diào)用currentShape的processCursorEvent(MouseEvent,int)方法,          類型參數(shù)是IShape.CURSOR_DRAGGED。 repaint()*/        if (currentShape != null) {            currentShape.processCursorEvent(e, IShape.CURSOR_DRAGGED);            repaint();        }    }        public void mouseReleased(MouseEvent e) {        /* 當(dāng)左鍵被松開并且currentShape不指向null時(這個時候,currentShape          肯定不會指向null的,多檢查一次,保險),調(diào)用currentShape的          processCursorEvent(MouseEvent,int)方法,類型參數(shù)是          IShape.CURSOR_DRAGGED。 repaint()*/        if (e.getButton() == MouseEvent.BUTTON1 && currentShape != null) {            currentShape.processCursorEvent(e, IShape.LEFT_RELEASED);            currentShape = null;            repaint();        }    }        //對下面這些事件不感興趣    public void mouseClicked(MouseEvent e) {}    public void mouseEntered(MouseEvent e) {}    public void mouseExited(MouseEvent e) {}    public void mouseMoved(MouseEvent e) {}    }

          至此,整個程序的流程就很清楚了,文章開頭部分的問題也被解開了。接下來,就繼續(xù)來看其他的模型類。

          『RectBoundedShape』

          RectBoundedShape構(gòu)造函數(shù)的結(jié)構(gòu)跟FreeShape一樣,在色彩和線條的運用上也是一樣的,也只對鼠標(biāo)拖拽事件感興趣。不過,它只有兩個控制點,起始點和結(jié)束點,所以,不需要用到PointsSet。本來,RectBoundedShape這個類是比FreeShape簡單的,在處理鼠標(biāo)拖拽事件時只要將結(jié)束點設(shè)置到新拖拽到的點就可以了。不過,這里我們多加入一個的功能,就是在shift鍵按下的情況下,讓圖形的邊界是個正方形(取原邊界中較短的那條邊)。這個功能是由regulateShape(int,int)這個方法來完成的,它的代碼相當(dāng)簡短,就不多做解釋了 。

          import java.awt.*;import java.awt.event.MouseEvent;public abstract class RectBoundedShape implements IShape {        protected Color color;    protected Stroke stroke;      protected int startX, startY, endX, endY;        protected RectBoundedShape(Color c, Stroke s, int x, int y) {        color = c;        stroke = s;        startX = endX = x;        startY = endY = y;    }        public void processCursorEvent(MouseEvent e, int t) {        if (t != IShape.CURSOR_DRAGGED)            return;        int x = e.getX();        int y = e.getY();        if (e.isShiftDown()) {            regulateShape(x, y);        } else {            endX = x;            endY = y;        }    }        protected void regulateShape(int x, int y) {        int w = x - startX;        int h = y - startY;        int s = Math.min(Math.abs(w), Math.abs(h));        if (s == 0) {            endX = startX;            endY = startY;        } else {            endX = startX + s * (w / Math.abs(w));            endY = startY + s * (h / Math.abs(h));        }    }    }

          有了RectBoundedShape這個父類打下的基礎(chǔ),它下面的子類所要做的事情就是畫圖啦。所有子類的構(gòu)造函數(shù)跟父類都是一樣的結(jié)構(gòu),基本上也都是直接調(diào)用super的構(gòu)造函數(shù),只是Diamond這個類為了提高畫圖效率,“私下”定義了一個數(shù)組。RectBoundedShape的子類包括Line, Rect, Oval, 和Diamond。除了Diamond需要根據(jù)邊界長方形進行稍微計算求得菱形的四個點外,它們的圖形都可以直接利用Graphics類提供的方法很方便的畫出來,詳情可以參看源代碼,就不多說了。現(xiàn)在看一下Line這個類。不同于其它幾個類,在shift鍵按下的情況下,根據(jù)角度不同,我們想畫出45度線,水平線,或者豎直線。所以,Line這個類不使用其父類定義的processCursorEvent(MouseEvent,int)方法,而是自己定義了一套。父類中regulateShape(int,int)方法的權(quán)限設(shè)成protected也是為了給Line用的。代碼如下:

              public void processCursorEvent(MouseEvent e, int t) {        if (t != IShape.CURSOR_DRAGGED)            return;        int x = e.getX();        int y = e.getY();        if (e.isShiftDown()) {            //這個情況單獨處理,不然就要除以0了            if (x - startX == 0) { //豎直                endX = startX;                endY = y;            } else {                //由于對稱性,只要算斜率的絕對值                float slope = Math.abs(((float) (y - startY)) / (x - startX));                //小于30度,變成水平的                if (slope < 0.577) {                    endX = x;                    endY = startY;                //介于30度跟60度中間的,變成45度,利用父類的regulateShape(int,int)完成                } else if (slope < 1.155) {                    regulateShape(x, y);                //大于60度,變成豎直的                } else {                    endX = startX;                    endY = y;                }            }        //如果shift鍵沒有按下,跟父類一樣處理        } else {            endX = x;            endY = y;        }    }

          『PolyGon』

          用戶畫多邊形的步驟是這樣的,先在一點按下鼠標(biāo)左鍵,定義一個頂點,然后將鼠標(biāo)拖拽到多邊形的下一個頂點,點鼠標(biāo)右鍵將這個點記錄,之后重復(fù)這個步驟直到所有頂點都記錄,松開左鍵,多邊形完成。在多邊形完成前,顯示出來的不是閉合圖形,當(dāng)左鍵松開時,圖形自動閉合。對于最后一個頂點,用戶不用點右鍵也會被自動記錄的。好了,來看一下這個過程是怎么來完成的。方便起見,直接用注釋在代碼上解釋了。

          import java.awt.*;import java.awt.event.MouseEvent;public class PolyGon implements IShape {        //類似于FreeShape和RectBoundedShape的變量    private Color color;    private Stroke stroke;    //記錄所有頂點坐標(biāo),姑且稱之為頂點集    private PointsSet pointsSet;      //記錄多邊形是否完成。true表示完成    private boolean finalized;        //記錄畫圖過程中鼠標(biāo)被拖拽到的點,姑且稱之為浮點吧^_^    private int currX, currY;        public PolyGon(Color c, Stroke s, int x, int y) {        pointsSet = new PointsSet();        color = c;        stroke = s;        pointsSet.addPoint(x, y);        //剛開始先把浮點設(shè)置到起始頂點        currX = x;        currY = y;        finalized = false;    }        public void processCursorEvent(MouseEvent e, int t) {        //首先更新浮點坐標(biāo)        currX = e.getX();        currY = e.getY();        //右鍵按下時,將浮點加入到頂點集里        if (t == IShape.RIGHT_PRESSED) {            pointsSet.addPoint(currX, currY);        //左鍵按下時,設(shè)置多邊形到完成狀態(tài),并且將浮點加入頂點集中        } else if (t == IShape.LEFT_RELEASED) {            finalized = true;            pointsSet.addPoint(currX, currY);        }        /* 注意:上面的if-else結(jié)構(gòu)只包含了RIGHT_PRESSED和LEFT_RELEASED兩種情況,           不過,這個方法也處理了CURSOR_DRAGGED這種情況,就是更新浮點坐標(biāo) */    }        public void draw(Graphics2D g) {        g.setColor(color);        g.setStroke(stroke);        if (finalized) {            //一旦圖形完成,浮點就不再用到了            int[][] points = pointsSet.getPoints();            int s = points[0].length;            //這部分跟PolyLine類似            if (s == 1) {                int x = points[0][0];                int y = points[1][0];                g.drawLine(x, y, x, y);            } else {                g.drawPolygon(points[0], points[1], s);            }        } else { //圖形沒完成的情況下,顯示的時候要用到浮點            int[][] points = pointsSet.getPoints(currX, currY);            g.drawPolyline(points[0], points[1], points[0].length);        }    }        }

          『其他』

          DrawingBoard(extends JPanel)是附件程序中用的畫圖板類,它是在TestBoard類上的一個擴展,加入了其他的模型類。另外,它提供了一些方法讓外部控制界面來設(shè)置繪圖色,畫圖板背景色,畫圖線條,橡皮擦大小(也是通過改變線條實現(xiàn)的)。這些就不再一一贅述了。

          AppFrame(extends JFrame)用來放畫圖板和控制面板。

          此外,在稍微變動代碼的情況下,還可以加入新的圖形類,當(dāng)然這些類要實現(xiàn)IShape接口,比如,直接繼承RectBoundedShape,定義新的圖形顯示代碼。 

          posted on 2007-05-25 12:19 小鋒 閱讀(1547) 評論(0)  編輯  收藏


          只有注冊用戶登錄后才能發(fā)表評論。


          網(wǎng)站導(dǎo)航:
           
          主站蜘蛛池模板: 扶沟县| 阿城市| 古田县| 长兴县| 碌曲县| 昌乐县| 荣成市| 小金县| 卫辉市| 铜陵市| 昌图县| 安国市| 左贡县| 合作市| 全州县| 青田县| 通州市| 罗平县| 衡东县| 新河县| 茂名市| 青龙| 马尔康县| 谷城县| 湟源县| 丰镇市| 新蔡县| 定结县| 紫云| 股票| 河北区| 玉龙| 家居| 甘德县| 海晏县| 昌宁县| 梅州市| 临海市| 寿阳县| 邯郸市| 吉首市|