前言:通过前面两篇日志:
【面向对象课程项目:纸牌】Java实例学习(一):优秀源码的分析
【面向对象课程项目:纸牌】Java实例学习(二):优秀源码与自己模型的对比
我们了解到了自己的项目中存在的问题,也了解到了在整个架构上的缺陷.既然架构上的缺陷,也可以提出新的方案进行修改.套用某个大牛的一句话"简单的东西总是比复杂的东西更高效,也更优美"(忘了是谁说的了= =||).问题在于,投入的沉没成本,没有办法把他完全抛却,而且如果完全重写代码,会导致自己的自信心进一步受挫(等于枪毙了自己的编程思路,这是正在成长过程中的我最为忌讳的...).所以我决定在原有的命令行模式上加壳.另外,如果按照自己的思路写完后,才能体会到为什么自己的效率底下,而学姐的代码优秀,这个对比必须由一个完全由自己编写的代码来 提供样板. 但是以后如果真的要自己写东西,一定要在脑子里面打好草稿,才能够慢慢成长.对比下这两篇文章(1.编程能力的四种境界 ;2.程序员的四大境界)中描述的状态自己还处在 第一个阶段.无意识无知.要成长,还有相当长的路要走,相当多的文档要看.
一.对原来的模型中几个必要的修正
- 先看看这个在第二篇文章中提到的漏洞,关于移动牌堆的逻辑方法,应该放在包含各个 数据结构 的GameModel类中! 根据数据和相应的特质方法封装成一个类的原则,在命令行的代码中,应该只出现调用,而不应该出现凌乱的,让人摸不着头脑的switch语句.这个是非常不合时宜的.破坏了封装性的Command类完成了苦逼的 获取命令和 对应参数的的工作,还要去实现移动牌堆的任务.这个错误的思路,直接影响了我第一次加壳的尝试:我竟然愚蠢的认为,在 GUI版本的纸牌中,command类应该得以保留,而每一次点击形成一个命令,交给command流来进行处理! 相当愚蠢的饶了一大圈...这完全都是因为当时开发的时候,模块划分不够明确和 谨慎.如果不是因为写这篇日志,重新开始审视 设计模式,我不会发现这个一步错步步错的 结果. 看看修改后的GameModel代码:
这样看起来自然多了.以后像LIST_SIZE,这样的变量,能来看源码的人一看就懂.这样鸡肋的常量,以后在修正的时候,必要的时候再予以添加...package com.Cards.model; /** * @author Rock Lee * @version 2012-10-21 17:33:31 * @proposal to establish a game model for the game,leaving all the possible * interfaces to control the game * * */ public class GameModel { public final static int LISTS_NUM = 7; public final static int STACKS_NUM = 4; public CardHeap heap = null; public CardList lists[] = null; public CardStack stacks[] = null; public CardDeck deck = null; public GameModel() { this.initialize(); } private void initialize() { heap = new CardHeap(); lists = new CardList[LISTS_NUM]; stacks = new CardStack[STACKS_NUM]; } public void newGame() { heap.initialize();// get a new set of cards heap.shuffle(); for (int i = 0; i < lists.length; i++) {// 7 times to get 7 Card Lists lists[i] = new CardList(); for (int j = 0; j < i + 1; j++) {// i+1 times to give out the enough Cards to build list[i] Card tmp = heap.giveOutOneCard(); lists[i].addOneCard(tmp); } lists[i].turnLastCard();// turn the end card in the list } for (int i = 0; i < stacks.length; i++) {// 4 times to get 4 stacks stacks[i] = new CardStack(CardSetting.COLOR[i]); } // only one deck to set up // All the cards left in the heap sent to deck deck = new CardDeck(); while (!heap.isEmpty()) { Card tmp = heap.giveOutOneCard(); tmp.setVisiable(true); deck.addOneCard(tmp); } this.print(); } public void print() { System.out .println("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"); System.out.println("The Status of Deck is :\n\t" + deck.getCurrentGroup()); System.out.println("The Status of 7 lists are:"); for (int i = 0; i < lists.length; i++) { System.out.println("\t" + lists[i] + "\n"); } System.out.println("The Status of 4 stacks are:"); for (int i = 0; i < stacks.length; i++) { System.out.println("\t" + stacks[i]); } System.out .println("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"); } /* * 参见command中的ltl * * from: 0~6 to :0~6 */ public void listToList(int from, int to) { lists[from].moveTo(lists[to]); this.print(); } /* * 参见commmand中的rpd * * to :0~3 */ public void deckToStack(int to) { Card tmp = deck.peekOneCard(); if (stacks[to].allowToPush(tmp)) { stacks[to].push(deck.getOneCard()); } this.print(); } /* * 参见 command中的rpl * * form: 0~6 to : 0~3 */ public void listToStack(int from, int to) { Card tmp = lists[from].peekOneCard(); if (stacks[to].allowToPush(tmp)) { stacks[to].push(lists[from].getOneCard()); } this.print(); } /* * 参见 command中的dtl * * to: 0~6 */ public void deckToList(int to) { Card tmp = deck.peekOneCard(); if (lists[to].allowToAdd(tmp)) { lists[to].addOneCard(deck.getOneCard()); } this.print(); } /* * 显示下一组 * */ public void nextDeckToShow() { deck.getNextGroup(); print(); } }
二.加壳代码的具体思路
- 修正完上面的部分以后,我们接下来看看整个加壳的思路.
先看看布局图:- 时刻提醒自己注意,View部分只是用于回显 对model的修改
- 被点击后,调用各个被封装在GameModel中的方法,这里有两个选择,各个ViewCard进行监听 或者是使用 ViewGame中单独挂载一个监听类进行事件的监听. 最终向model传递改变指令
- 问题: 首先,我们考虑Card,也就是上图中的□ 应该以什么方式显示? 其次,如何 记录点击?
- 对于问题一,每一个□,都应该对应一个实例.我们应该写一个通用的父类,子类实现其重绘的方法.这个是思路的一种,要求把每一个ViewCard的图片标签,都直接贴到ViewGame的面板上;另外一种实现思路便是使用 和第二篇文章的代码一样,每一个对象都是JLayerdPane的一个实例的子对象.这样便可以工作.这两个思路直接影响了整个架构,但是思路有一点是一致的:将每一张牌 对应绘制到 相应的 位置上..
- 对于问题而,以前写 黑白棋的时候,似乎用的都是getSources来获得一个触发事件的对象 的指针.但这里点击的没有处理方法的JLabel,似乎有些不合适,而每个牌堆的牌太多,这个处理机制我觉得不够合适,于是乎放弃....另一种方案,在每一个pile都挂载上一个监听器,在模型□被点击时,像父类中添加记录,并处理下一步的工作...(这个方法要在ViewModel中实现内部匿名类)
- 但是有没有什么更适合我这个 渣渣的办法呢?既然我采取的思路,是直接向 整个大ViewGame这个panel中添加label组件,那么就应该 把对应的监听器放在整个panel上. 我的考虑参照了当时开发黑白棋的一种 使用JLable作为棋盘格的 思路:也即使用绝对坐标来实现.但这样一来,几乎无法 以panel的某一打牌来进行操作,拖拽的功能就必须放弃考虑了. 如此同时,也不必在意 □的实现了.
- 接下来,在每个ViewPile的子类中,给出在整个Panel绝对布局 中某个组件的坐标,用对应的drawXXX(x,y)类似的方法内,绘制到ViewGame中.当游戏需要刷新的时候,我们只需要对drawXXX进行重新调用即可.
- 具体实现中的几个问题,首先,ViewGame用什么Cotainer来盛放?其次 倘若model修改成功之后.重新drawXXX之时,原来的图片如何处理? Java中有没有对某个rectangle为脏区的处理方案? 或者简单的刷一刷背景,覆盖即可?写的时候,这些都是值得考虑的问题.
三.具体源码与详解
- 上一下ViewCard的源码,我门要记住的,ViewCard只是一个封装好了Card模型的JLabel,用来设置相应的图片而已.
package com.Cards.view; import javax.swing.ImageIcon; import javax.swing.JLabel; import com.Cards.model.Card; import com.Cards.model.GameModel; /** * @author Rock Lee * @version 2012-11-28 8:53:09 * @see com.Cards.model.Card.java * @aim A modified version to support the View of the Game Basic Part of the * Cards. * @note This class should be considered as a JLabel!!! with some special fields * for Card... * * */ public class ViewCard extends JLabel { private static final long serialVersionUID = 1L; private Card model_card = null; private ImageIcon cardFace = null; public ViewCard(Card card) { this.setOpaque(false); // TODO build a viewCard,using JLabel this.model_card = card; this.prepareCardFace(); } /* * 如果说,整个model_card给的参数一开始就是null,那么就给他一个空框的图片 * * 可见,不可见分开考虑 */ private void prepareCardFace() { if (this.model_card == null) {// there is no card in this place right now this.cardFace = ViewSetting.EMPTY_IMAGE; } else if (!this.model_card.isVisible()) {// get the cardFace ImageIcon for the JLabel<not Visible> this.cardFace = ViewSetting.BACK_IMAGE; } else { // System.out.println(this.model_card); this.cardFace = ViewSetting.getImageIconForCard(this.model_card); } this.setIcon(cardFace); } public Card getModelCard() { return this.model_card; } }
- 作为ViewCard初始化的支持类:ViewSetting
package com.Cards.view; import javax.swing.ImageIcon; import com.Cards.model.Card; public class ViewSetting { public final static ImageIcon BACK_IMAGE = new ImageIcon( ViewSetting.class.getResource("/image/back.png")); public final static ImageIcon EMPTY_IMAGE = new ImageIcon( ViewSetting.class.getResource("/image/empty.png")); public static ImageIcon getImageIconForCard(Card model_card) { ImageIcon cardFace = null; if (model_card.getColor().equals("♥")) { cardFace = new ImageIcon(ViewSetting.class.getResource("/image/" + "h" + model_card.getNum() + ".png")); } else if (model_card.getColor().equals("♠")) { cardFace = new ImageIcon(ViewSetting.class.getResource("/image/" + "s" + model_card.getNum() + ".png")); } else if (model_card.getColor().equals("♦")) { cardFace = new ImageIcon(ViewSetting.class.getResource("/image/" + "d" + model_card.getNum() + ".png")); } else if (model_card.getColor().equals("♣")) { cardFace = new ImageIcon(ViewSetting.class.getResource("/image/" + "c" + model_card.getNum() + ".png")); } //System.out.println(cardFace); return cardFace; } public final static int COMPONET_DISTANCE = 20; public final static int CARD_WIDTH = 100; public final static int CARD_HEIGHT = 150; }
- ViewPile.java 这是一个抽象类,他的工作就是将对应的model 数组形成相应的ViewCard 数组,等待下一步的绘制.实际上这两步的工作可以放到同一个方法调用中,但是由于我放到了两个方法中,我不得不去再维护一个ArrayList<ViewCard> viewCardList的属性,每次都要删除内容,重新装载,删除内容,重新装载..效率实在是不怎么高.如果把这个作为一个零时变量,就不用在下面的fillList代码中,每次都要clear()一下咯.
另外解释一下构造方法中的JPanel.这个就是用于添加各个ViewCard的 容器,在ViewGame中初始化一个ViewPile对象的时候,这个实参就是 thispackage com.Cards.view; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import javax.swing.JLayeredPane; import javax.swing.JPanel; import javax.swing.text.LayeredHighlighter; import com.Cards.model.Card; public abstract class ViewPile { ArrayList<ViewCard> viewCardList=null; Collection<Card> pileModel=null; JPanel mainnGameContainer=null; public ViewPile(Collection<Card> pileModel,JPanel container) { this.mainnGameContainer=container; viewCardList=new ArrayList<ViewCard>(); this.pileModel=pileModel; this.fillList(); } /* * 将model中的一张张牌 初始化 作 ViewCard的数组 * 并放入viewCardList,为下一步的准备 * */ public void fillList() { //清理原有的内容,避免上一轮绘制的模型list被重复添加 viewCardList.clear(); Iterator<Card> iter=pileModel.iterator(); while(iter.hasNext()) { ViewCard tmp=new ViewCard(iter.next()); viewCardList.add(tmp); } } abstract public void paintPile(int x,int y); }
- 看看ViewPileDeck类,其他的ViewPileList和ViewPileStack都是大同小异.
这里有几个地方要解释一下,control指的是哪个可以不断点击,以获得下一组三张牌的 JLabel. 之所以要每一次paint都要刷新一遍list,(filllist下),是因为上面的原因..这个的确是个败笔,代码可以进一步精简,这里就不给出了= =.package com.Cards.view; import java.util.Collection; import javax.swing.JLabel; import javax.swing.JPanel; import com.Cards.model.Card; public class ViewPileDeck extends ViewPile { public JLabel control = null; public ViewPileDeck(Collection<Card> pileModel, JPanel container) { super(pileModel, container); } /* 展示model deck中要显示的那三张牌 */ @Override public void paintPile(int x, int y) { super.fillList(); x += 10; y += 10; control = new JLabel(ViewSetting.BACK_IMAGE); control.setBounds(x, y, 100, 150);// 用于点击的那个label // 这真是神奇!setLocation 不管用,而setBounds则可以!? super.mainnGameContainer.add(control); x += 170; for (int i = 0; i < viewCardList.size(); i++) { viewCardList.get(i).setBounds(x + 20 * i, y, 100, 150); // 这一句掉了!不要理所当然的认为有 画笔去绘制这个组件!没有,因为我们通过的是add Label的,没有重画 super.mainnGameContainer.add(viewCardList.get(i), 0); // setLocation // 是将位置设置为相对于父组件的位置 // 但是,必须要添加到一个父组件(Container)中,才能得到显示 } } }
- 当时一开始使用setLocation方法老是不出现图片...一直弄不明白为什么.网上查阅资料以后,知道了setLocation 实质是调用setBounds的.但是为啥setBounds能够将图片正确显示,而setLocation不能呢?我查阅类库源码仍然不解,后来单步调试,看绘制过程,找到的原因: 在没有设置ViewCard的JLabel的高宽的时候,setLocation调用 setBounds使用的是setBounds(x,y,0,0)..如果细心,就会发现学姐的代码中有这么一部分:
病根就在这.不自己写写,永远不知道setLocation必须先自己设计好组件的大小囧public CardW(Card c) { super(); this.card=c; setFaceUp(card.isFaceUp()); setBounds(0,0,MainWindow.POKERWIDTH, MainWindow.POKERHEIGHT); }
这是ViewPileList,注意一下这里的super.mainGameContainer.add().参数中给了一个0...因为当时如果直接使用普通的add调用.我们会悲剧的发现:后面添加的组件反而被前面的组件盖住,放到了底下.查阅APIc,发现了这么一段既然默认是-1,添加到尾部,我给一个0吧.这样就解决了后面的组件被前面的覆盖了问题.(实际上这个地方还有一个处理方法,就是用JLayeredPane,像学姐一样,对每一个组件添加的时候,添加到不同的层即可 setLayer(cw,i))package com.Cards.view; import java.util.Collection; import javax.swing.JLabel; import javax.swing.JLayeredPane; import javax.swing.JPanel; import com.Cards.model.Card; public class ViewPileList extends ViewPile { //没有将组件添加进来?! 必须要显式的调用add public ViewPileList(Collection<Card> pileModel,JPanel container) { super(pileModel,container); } @Override public void paintPile(int x, int y) { super.fillList(); x+=10;y+=10; for (int i = 0; i < viewCardList.size(); i++) { viewCardList.get(i).setBounds(x,y+30*i,100,150); //viewCardList.get(i).setLocation(x,y+30*i); super.mainnGameContainer.add(viewCardList.get(i),0); } } }
- ViewPileStack,没什么好说的
package com.Cards.view; import java.util.Collection; import javax.swing.JLayeredPane; import javax.swing.JPanel; import com.Cards.model.Card; public class ViewPileStack extends ViewPile { public ViewPileStack(Collection<Card> pileModel,JPanel container) { super(pileModel,container); } @Override /*给定起始坐标,将目标CardView绘制出来*/ public void paintPile(int x, int y) { super.fillList(); x+=10;y+=10; //显示已经收割的牌堆中,最上面一张牌 ViewCard tmp=viewCardList.isEmpty()?new ViewCard(null):viewCardList.get(viewCardList.size()-1); tmp.setBounds(x, y,100,150); super.mainnGameContainer.add(tmp); } }
- 接下来看看让整个协同工作的类,我没有做任何美化工作,已经懒得进一步去写了=. =
package com.Cards.view; import java.awt.Graphics; import java.awt.Image; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import com.Cards.model.*; import javax.swing.JLabel; import javax.swing.JLayeredPane; import javax.swing.JPanel; import javax.swing.text.View; public class ViewGame extends JPanel { private ViewPileDeck viewDeck = null; private ViewPileStack[] viewStacks = null; private ViewPileList[] viewLists = null; boolean isClicked = false; int fromPosition = 0, toPosition = 0; final int STACK_INDEX_BASE = 1; final int LIST_INDEX_BASE = 5; private GameModel gameModel = null; public ViewGame(GameModel gameModel) { this.setLayout(null);// 设置为绝对布局 this.gameModel = gameModel; // 初始化几个部件 viewDeck = new ViewPileDeck(gameModel.deck.getStackShown(), this); // stacks viewStacks = new ViewPileStack[4]; for (int i = 0; i < viewStacks.length; i++) { viewStacks[i] = new ViewPileStack(gameModel.stacks[i].getStack(), this); } // lists viewLists = new ViewPileList[7]; for (int i = 0; i < viewLists.length; i++) { viewLists[i] = new ViewPileList(gameModel.lists[i].getLinkedList(), this); } this.repaint(); this.addMouseListener(new MyMouseAdapter()); } @Override public void paint(Graphics g) { this.removeAll(); this.setOpaque(false); int x = 0, y = 0; viewDeck.paintPile(x, y); x = 3 * (ViewSetting.COMPONET_DISTANCE + ViewSetting.CARD_WIDTH); for (int i = 0; i < viewStacks.length; i++) { viewStacks[i].paintPile(x + i * (ViewSetting.COMPONET_DISTANCE + ViewSetting.CARD_WIDTH), y); } x = 0; y = ViewSetting.CARD_HEIGHT + 30; for (int i = 0; i < viewLists.length; i++) { viewLists[i].paintPile(x + i * (ViewSetting.COMPONET_DISTANCE + ViewSetting.CARD_WIDTH), y); } super.paint(g); } class MyMouseAdapter extends MouseAdapter { @Override public void mouseClicked(MouseEvent e) { int x = e.getX() - 10, y = e.getY() - 10; if (-1==getComponentPosition(x, y)) { gameModel.nextDeckToShow(); } if (isClicked)// 如果前面一次已经点击 { isClicked = false; // 获得此次点击的组件标号 ViewGame.this.toPosition = getComponentPosition(x, y); if (toPosition >= 1 && toPosition <= 4) {// 表示要收割 if (fromPosition == 0) {// rpd gameModel.deckToStack(toPosition - STACK_INDEX_BASE); } else if (fromPosition >= 5 && fromPosition <= 11) {// rpl gameModel.listToStack(fromPosition - LIST_INDEX_BASE, toPosition - STACK_INDEX_BASE); } } else if (toPosition >= 5 && toPosition <= 11) {// 向某个 list移动 if (fromPosition == 0) {// dtl gameModel.deckToList(toPosition - LIST_INDEX_BASE); } else if (fromPosition >= 5 && toPosition <= 11 && fromPosition != toPosition) {// ltl gameModel.listToList(fromPosition - LIST_INDEX_BASE, toPosition - LIST_INDEX_BASE); } } } else {// 尚未有有效点击被记录,则记录下本次点击 isClicked = true; ViewGame.this.fromPosition = this.getComponentPosition(x, y); } ViewGame.this.repaint(); } /* * 注意这里的 index -1 代表 controlLabel 0 代表 deck 1~4 代表 stacks 5~11 代表 lists */ public int getComponentPosition(int x, int y) { int result = 0; if (y <= ViewSetting.CARD_HEIGHT) {// deck 或 stack的点击 result = x / (ViewSetting.COMPONET_DISTANCE + ViewSetting.CARD_WIDTH); switch (result) { case 0: return -1; case 1: case 2: return 0; case 3: default: return ((result-3) + STACK_INDEX_BASE); //注意此处,result-3 为实际在stacks中的下标 } } else {// 点击的是list部分 result = x / (ViewSetting.COMPONET_DISTANCE + ViewSetting.CARD_WIDTH); return (result + LIST_INDEX_BASE); } } } }
由于每一次返回的时候,我们都要找到对应的 组件标号(即Layout那一幅图中的哪个部分,上面的STACK_INDEX_BASE就是为了区别到底是 识别哪一组的一个小trick,因为0~3和0~6有重叠的地方,处理加上一个常量值,再在 对应调用处理方法的时候把这个常量剪掉就行了) - MainWindow.java 没有用involkedLater的队列,我觉得这个实在是没有必要...但是掌握还是需要的喂喂!!囧
package com.Cards.view; import javax.swing.JFrame; import com.Cards.model.GameModel; public class MainWindow extends JFrame { ViewGame viewPanel=null; GameModel myModel=null; public MainWindow(String title) { super(title); myModel=new GameModel(); myModel.newGame(); viewPanel=new ViewGame(myModel); this.add(viewPanel); this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); } public static void main(String[] args) { MainWindow myTest=new MainWindow("fucking paper!"); myTest.setSize(900,600); myTest.setVisible(true); } }
看看效果图吧..阿西莫多的fuckingpaper!!! - 最后还是忍不住吐槽了一下囧....说脏话了,各位看官对不起啦!
这里是 自己的源码,其中Card2.0为 命令行版,其中有半成品的图形界面(不能工作),不要理会他就是了;Card3.0为已经添加了图形界面的源码版本,地址戳我.