Java桌面技術(shù)

          Java Desktop Technology

          導(dǎo)航

          <2014年4月>
          303112345
          6789101112
          13141516171819
          20212223242526
          27282930123
          45678910

          留言簿(25)

          隨筆分類

          文章分類

          隨筆檔案

          文章檔案

          相冊(cè)

          閱讀排行榜

          常用鏈接

          統(tǒng)計(jì)

          友情連接

          最新評(píng)論

          SWT自定義組件之Slider

             曾經(jīng)介紹過(guò)用SWT實(shí)現(xiàn)MSN風(fēng)格的下拉框,SWT雖然沒(méi)有Swing那么強(qiáng)大,尤其是在打造專業(yè)外觀上,不支持L&F,但是通過(guò)自定義組件,同樣可以達(dá)到用戶要求。下面就向大家介紹本人實(shí)現(xiàn)的一個(gè)具備專業(yè)外觀的Slider控件。
              首先來(lái)參考一下組件的實(shí)際運(yùn)行效果,并和SWT原生組件進(jìn)行一下對(duì)比。 

                                                                 

          可以看出,經(jīng)過(guò)自定義的組件在外觀上要比SWT直接調(diào)用本地組件顯得更加專業(yè)。當(dāng)用戶托拽滑動(dòng)塊時(shí),還會(huì)出現(xiàn)一個(gè)虛擬的滑動(dòng)塊用來(lái)標(biāo)識(shí)將要移動(dòng)到的位置。演示就到此為止,下面詳細(xì)介紹這個(gè)很Cool的組件是如何通過(guò)SWT實(shí)現(xiàn)的。

              基本設(shè)計(jì)思想:與其他自定義組件一樣,是通過(guò)繼承org.eclipse.swt.widgets.Composite來(lái)實(shí)現(xiàn),定義該類為Slider,另外滑動(dòng)塊(thumb)也是Composite,并放在Slider之上,當(dāng)鼠標(biāo)移動(dòng)thumb時(shí),調(diào)用setBounds方法定位在Slider在父組件(Slider)上的位置,從而達(dá)到拖拽thumb的目的。此外通過(guò)實(shí)現(xiàn)PaintListener接口進(jìn)行自定義繪制,繪制的對(duì)象包括組件邊框、被填充的格子、未被填充的格子、虛擬滑塊。

              接觸過(guò)GUI編程的程序員都應(yīng)該知道像Scroll、Slider、ProgressBar這樣的控件都有setMaxValue、setMinValue、setValue這樣的方法,除了鼠標(biāo)拖拽thumb來(lái)改變當(dāng)前數(shù)值外,可直接調(diào)用setValue來(lái)設(shè)置當(dāng)前值。此外這些控件還有水平(Horizontal)、垂直(Vertical)兩種布局,對(duì)于事件處理一般都要有一個(gè)從java.util.EventObject繼承而來(lái)的事件類,還要編寫事件監(jiān)聽(tīng)器(Listener)接口,因此在開(kāi)始編寫Slider控件之前先定義3個(gè)類,代碼都不是很長(zhǎng),如果你熟悉AWT、Swing的事件處理機(jī)制,相信你能輕松跳過(guò)。


          public enum SliderOrientation {
           HORIZONTAL, VERTICAL;
          }

          public class SliderEvent extends EventObject {

           private int value;

           public SliderEvent(Object source, int value) {
            super(source);
            this.value = value;
           }

           public int getValue() {
            return value;
           }
          }

          public interface SliderListener {

           public void valueChanged(SliderEvent event);
          }

              接下來(lái)著重介紹Slider。首先是繼承Composite并實(shí)現(xiàn)ControlListener、PaintListener、MouseListener,、MouseMoveListener,、MouseTrackListener,然后自動(dòng)生成接口方法代碼,通過(guò)Eclipse可以輕松實(shí)現(xiàn),需要注意的是MouseListener,有java.awt.event.MouseListener和org.eclipse.swt.events.MouseListener兩種,不要混淆,否則錯(cuò)誤很難找到。然后是要采集一些數(shù)據(jù)信息,分別是:邊框顏色、已有數(shù)據(jù)部分的填充顏色(上圖中組件的綠色部分)、未達(dá)到數(shù)據(jù)部分的填充顏色(上圖中組件的白色部分)、被禁用時(shí)的填充顏色、水平滑塊的圖標(biāo)(正常、托拽中兩種)、垂直滑塊圖標(biāo)(正常、托拽中兩種)、水平、垂直虛擬滑塊圖標(biāo)。以上這些數(shù)據(jù)對(duì)應(yīng)的常量聲明如下:

           private final Color BORDER_COLOR = new Color(Display.getCurrent(), 180, 188, 203);

           private final Color FILL_COLOR = new Color(Display.getCurrent(), 147, 217, 72);

           private final Color BLANK_COLOR = new Color(Display.getCurrent(), 254, 254, 254);

           private final Color DISABLE_COLOR = new Color(Display.getCurrent(), 192, 192, 192);

           private final Image THUMB_ICON_V = new Image(Display.getDefault(), "slider_up_v.png");

           private final Image THUMB_OVER_ICON_V = new Image(Display.getDefault(), "slider_over_v.png");

           private final Image THUMB_ICON_H = new Image(Display.getDefault(), "slider_up_h.png");

           private final Image THUMB_OVER_ICON_H = new Image(Display.getDefault(), "slider_over_h.png");

           private final Image TEMP_H = new Image(Display.getDefault(), "temp_h.png");

           private final Image TEMP_V = new Image(Display.getDefault(), "temp_v.png");

           除了這些常量,還應(yīng)該聲明默認(rèn)最大值的常量,private final int DEFAULT_MAX_VALUE = 100;
          接下來(lái)定義當(dāng)前數(shù)值和最大值,
          private int value;
          private int maxValue = DEFAULT_MAX_VALUE;
          并生成以上兩個(gè)成員屬性的get方法
          然后定義滑動(dòng)塊和布局
          private SliderOrientation orientation;
          private Composite thumb;
          要處理數(shù)值變化,需要實(shí)現(xiàn)一組監(jiān)聽(tīng)器,添加如下代碼
          private List<SliderListener> listeners = new ArrayList<SliderListener>();
          public void addSliderListener(SliderListener sliderListener) {
           listeners.add(sliderListener);
          }
          public void removeSliderListener(SliderListener sliderListener) {
           listeners.remove(sliderListener);
          }
          接下來(lái)定義2個(gè)輔助方法,實(shí)現(xiàn)value<->pelsLength轉(zhuǎn)換。其中value是當(dāng)前的數(shù)值,由具體業(yè)務(wù)來(lái)決定,下文中稱其業(yè)務(wù)值。例如一個(gè)音量控制器,音量范圍在0~500,那么從業(yè)務(wù)上來(lái)講可以將數(shù)值設(shè)置在0~500之間的任何數(shù),而pelsLength則由控件的長(zhǎng)/高度來(lái)決定,單位是像素。但是value與pelsLength之間存在著一個(gè)比例關(guān)系式:value/maxValue=pelsLength/控件長(zhǎng)度或高度。這樣不難得出兩個(gè)函數(shù)的定義。
          private int valueToPels(int value) {
            float widgetLength = (orientation == SliderOrientation.HORIZONTAL) ? getBounds().width
              : getBounds().height;
            return (int) (widgetLength * (float) value / (float) maxValue);
           }

           private int pelsToValue(int pels) {
            float widgetLength = (orientation == SliderOrientation.HORIZONTAL) ? getBounds().width
              : getBounds().height;
            return (int) ((float) pels * (float) maxValue / (float) widgetLength);
           }
          最后定義構(gòu)造器。代碼如下
          public Slider(Composite parent, SliderOrientation orientation) {
            super(parent, SWT.FLAT);
            this.orientation = orientation;
            thumb = new Composite(this, SWT.FLAT);
            thumb
              .setBackgroundImage(orientation == SliderOrientation.VERTICAL ? THUMB_ICON_V
                : THUMB_ICON_H);
            addControlListener(this);
            addPaintListener(this);
            thumb.addMouseListener(this);
            thumb.addMouseMoveListener(this);
            thumb.addMouseTrackListener(this);
           }
          在構(gòu)造器中,注入布局對(duì)象,然后在控件上創(chuàng)建滑動(dòng)塊組件thumb,并添加鼠標(biāo)處理等。
          到此為止,基本的成員和方法的定義完畢,下面循序漸進(jìn)討論如何實(shí)現(xiàn)這一Slider。

          一、繪制邊框
          由于是繪制操作,所以一切繪制代碼均在paintControl方法內(nèi)實(shí)現(xiàn),先將如下代碼拷貝到paintControl內(nèi)
          int w = getBounds().width;
          int h = getBounds().height;
          int fillLength = valueToPels(getValue());
          GC gc = e.gc;
          switch (orientation) {
           case HORIZONTAL:
            break;
           case VERTICAL:
            break;
          }
          分析如下
          首先獲取控件的長(zhǎng)度與高度,因?yàn)榻酉聛?lái)的繪制要經(jīng)常用到這兩個(gè)變量。
          “int fillLength = valueToPels(getValue());”這一行代碼稍后作解釋,然后是獲得繪制上下文對(duì)象,下一步是根據(jù)布局不同采用不同的處理,除了paintControl函數(shù),在其他很多地方都對(duì)布局進(jìn)行判斷,但是簡(jiǎn)單起見(jiàn),只對(duì)水平布局進(jìn)行介紹,垂直部分參考完整程序。
          接下來(lái)的繪制操作均在case HORIZONTAL中進(jìn)行,首先將顏色設(shè)置為邊框的顏色
          gc.setForeground(BORDER_COLOR);然后繪制一個(gè)矩形gc.drawRectangle(0, 2, w - 1, h - 5);
          關(guān)于為什么要偏移2像素、長(zhǎng)度為什么減1、高度為什么減5,請(qǐng)參考有關(guān)繪圖的基本知識(shí),上一篇也有簡(jiǎn)單的介紹。

          現(xiàn)在,你就可以編寫測(cè)試程序來(lái)驗(yàn)證結(jié)果了,看看邊框是否與示例的效果一樣。

          二、托拽thumb的實(shí)現(xiàn)
          桌面GUI編程領(lǐng)域技術(shù)深淺的度量衡通常有4項(xiàng)指標(biāo):皮膚(外觀,swing組件體系稱其L&F)、繪圖、自定義組件布局(Layout)、自定義組件。而托拽是實(shí)現(xiàn)自定義組件和繪圖不可或缺的技術(shù),也是難點(diǎn)之一,因此掌握的深淺是衡量桌面編程水平的標(biāo)志。
          雖然作為難點(diǎn),但是也有章可循,其基本實(shí)現(xiàn)簡(jiǎn)單到只監(jiān)聽(tīng)鼠標(biāo)事件這么簡(jiǎn)單,基本流程是:當(dāng)鼠標(biāo)在thumb上按下時(shí),記住這個(gè)位置,然后按住鼠標(biāo)左鍵托拽,最后松開(kāi)鼠標(biāo)計(jì)算兩個(gè)位置之間的距離(位移),根據(jù)位移量移動(dòng)thumb的位置并換算出等價(jià)的value增量(可能為負(fù)值)進(jìn)行業(yè)務(wù)邏輯處理。下面通過(guò)代碼循序漸進(jìn)完成。
          定義一個(gè)位置變量用來(lái)存儲(chǔ)鼠標(biāo)單擊的位置,private Point controlPoint;然后實(shí)現(xiàn)public void mouseDown(MouseEvent e)和public void mouseUp(MouseEvent e)兩個(gè)方法。
          public void mouseDown(MouseEvent e) {
            controlPoint = new Point(e.x, e.y);
            thumb
              .setBackgroundImage(orientation == SliderOrientation.VERTICAL ? THUMB_OVER_ICON_V
                : THUMB_OVER_ICON_H);
           }

           public void mouseUp(MouseEvent e) {
            try {
             thumb
               .setBackgroundImage(orientation == SliderOrientation.VERTICAL ? THUMB_ICON_V
                 : THUMB_ICON_H);
             countValue(e);
            } finally {
             controlPoint = null;
            }
           }
          “controlPoint = new Point(e.x, e.y);”這一行實(shí)現(xiàn)記住鼠標(biāo)點(diǎn)的位置,注意,這個(gè)位置是相對(duì)滑塊thumb的,因?yàn)槭莟humb監(jiān)聽(tīng)的鼠標(biāo)事件。接下來(lái)是設(shè)置滑動(dòng)塊背景,不難理解當(dāng)鼠標(biāo)松開(kāi)時(shí),應(yīng)該將背景恢復(fù)。然后進(jìn)行非常重要的換算工作,通過(guò)countValue方法實(shí)現(xiàn),最后務(wù)必要把鼠標(biāo)位置清空,用try-finally是有必要這樣的。之所以要在方法結(jié)束的時(shí)候清空controlPoint,是因?yàn)樵谑髽?biāo)移動(dòng)的時(shí)候需要對(duì)controlPoint進(jìn)行更加復(fù)雜的計(jì)算。稍后講解mouseMove實(shí)現(xiàn)的時(shí)候再作解釋,接下來(lái)著重分析countValue方法。
          如前所述,countValue完成計(jì)算鼠標(biāo)按下、松開(kāi)的位移量,換算成與業(yè)務(wù)相關(guān)的數(shù)據(jù)(value)。
          private void countValue(MouseEvent e) {
            switch (orientation) {
            case HORIZONTAL:
             int movedX = e.x - controlPoint.x;
             setValue(getValue() + pelsToValue(movedX));
             break;
            case VERTICAL:
             ......
            }
           }
          “int movedX = e.x - controlPoint.x;”實(shí)現(xiàn)了位移量的計(jì)算,保存到movedX,調(diào)用pelsToValue方法將movedX轉(zhuǎn)換成業(yè)務(wù)值的增量,然后調(diào)用setValue重新賦值,注意pelsToValue得到的是增量,需要與原值(getValue()得到)疊加。接下來(lái)分析setValue方法。
          public void setValue(int value) {
            if (value < 0) {
             this.value = 0;
            } else if (value > getMaxValue()) {
             this.value = getMaxValue();
            } else {
             this.value = value;
            }
            try {
             moveThumb();
             redraw();
            } finally {
             for (SliderListener listener : listeners) {
              try {
               SliderEvent event = new SliderEvent(this, getValue());
               listener.valueChanged(event);
              } catch (Exception e) {
               e.printStackTrace();
              }
             }
            }
           }
          方法開(kāi)始處的一系列if語(yǔ)句對(duì)value進(jìn)行驗(yàn)證后再賦值,然后是調(diào)用moveThumb實(shí)現(xiàn)滑塊移動(dòng)、調(diào)用redraw對(duì)組件重新繪制,最后是處理具體的業(yè)務(wù),可以看出處理業(yè)務(wù)是setValue方法的關(guān)鍵,所以要用try-finally,誰(shuí)也不敢確保moveThumb、redraw不出問(wèn)題。對(duì)于實(shí)現(xiàn)業(yè)務(wù)是遍歷監(jiān)聽(tīng)器列表然后執(zhí)行每個(gè)監(jiān)聽(tīng)器的valueChanged方法,這種事件源-監(jiān)聽(tīng)器模型也是Java2以后的GUI事件實(shí)現(xiàn)模型。

           private void moveThumb() {
            Image icon = thumb.getBackgroundImage();
            int iconw = (icon != null) ? icon.getBounds().width : 0;
            int iconh = (icon != null) ? icon.getBounds().height : 0;
            switch (orientation) {
            case HORIZONTAL:
             int x = valueToPels(getValue()) - iconw / 2;
             if (x < 0) {
              x = 0;
             } else if (x > getBounds().width - iconw) {
              x = getBounds().width - iconw;
             }
             thumb.setBounds(x, 0, iconw, iconh);
             break;
            case VERTICAL:
             ......  }
           }
          不難理解,moveThumb的任務(wù)就是根據(jù)業(yè)務(wù)值value來(lái)將滑塊移動(dòng)到正確的位置。
          以上代碼聲明滑塊的位置是“x”,通過(guò)轉(zhuǎn)換函數(shù)獲得,但是還要減去滑塊的一半,因?yàn)榫唧w坐標(biāo)應(yīng)該落到滑動(dòng)塊的中間,仔細(xì)想想不難得出。if-else是對(duì)x進(jìn)行驗(yàn)證,最后通過(guò)setBound來(lái)定位thumb。
          對(duì)于redraw,他的作用是觸發(fā)paintControl方法進(jìn)行重繪,因?yàn)橹乩L出了邊框還要根據(jù)value繪制填充格子,而value已經(jīng)在redraw方法調(diào)用前被賦了值,所以這時(shí)候應(yīng)該進(jìn)行重繪。
          現(xiàn)在你可以托拽thumb了,美中不足的是滑動(dòng)塊不能隨時(shí)跟隨鼠標(biāo)的軌跡移動(dòng),這個(gè)稍后會(huì)實(shí)現(xiàn)。

          三、填充格子
          現(xiàn)在的組件外觀只是繪制了邊框,現(xiàn)在進(jìn)行格子填充。在講述邊框繪制的時(shí)候提到了一行代碼“int fillLength = valueToPels(getValue());”現(xiàn)在不難理解吧,就是將當(dāng)前業(yè)務(wù)值value轉(zhuǎn)換成實(shí)際的長(zhǎng)度。在繪制邊框之后,加入如下代碼:
          if (getEnabled()) {
              gc.setBackground(FILL_COLOR);
              for (int i = 2; i < w - 2; i += 4) {
               if (i > fillLength) {
                gc.setBackground(BLANK_COLOR);
               }
               gc.fillRectangle(i, 4, 3, h - 8);
              }
             } else {
              gc.setBackground(DISABLE_COLOR);
              gc.fillRectangle(1, 4, w - 1, h - 8);
             }
          首先判斷是否是enable,然后設(shè)置填充顏色FILL_COLOR,然后在for循環(huán)中執(zhí)行正方形格子的填充,遞增量“i”從2開(kāi)始是空開(kāi)2像素間隔,同理i也不能超過(guò)w-2(對(duì)稱性),i+=4是相鄰兩個(gè)格子左邊坐標(biāo)間距4像素,然后“gc.fillRectangle(i, 4, 3, h - 8);”這一行進(jìn)行填充繪制正方形。留意,x坐標(biāo)是i,y坐標(biāo)是從4開(kāi)始畫的,出于對(duì)稱高度也要“h-8”,長(zhǎng)度之所以是“3”是保持相鄰兩個(gè)格子之間保持1像素的間隔(想想“i+=4”就不難得出答案)。此外還要對(duì)fillLength進(jìn)行判斷以便決定顏色采用綠色還是白色以示區(qū)分。對(duì)于繪圖操作來(lái)說(shuō),千萬(wàn)不要埋怨考慮細(xì)節(jié)過(guò)多,事實(shí)上,GUI編程過(guò)程中“坐標(biāo)系”這個(gè)概念是需要經(jīng)常被考慮的,試想如果上述代碼for循環(huán)中把“i+=4”,寫成“i+=5”,只是一個(gè)像素之差繪制效果差之千里,如果想知道筆者是如何得到這些坐標(biāo)數(shù)據(jù)的,實(shí)話說(shuō),是靠多次調(diào)試得出的結(jié)果。
          現(xiàn)在你運(yùn)行程序,應(yīng)該能根據(jù)value進(jìn)行格子填充了。到此為止,絕大多數(shù)的功能已經(jīng)實(shí)現(xiàn),但是人性化的界面設(shè)計(jì)應(yīng)該在托拽時(shí)出現(xiàn)一個(gè)虛擬的滑動(dòng)塊用來(lái)標(biāo)識(shí)將要移動(dòng)到的位置。好,繼續(xù)實(shí)現(xiàn)這一功能。
          定義一個(gè)int變量紀(jì)錄thumb的臨時(shí)位置,private int tempLocation;然后在paintControl添加如下紅色代碼
          case HORIZONTAL:
             gc.setForeground(BORDER_COLOR);
             gc.drawRectangle(0, 2, w - 1, h - 5);
             if (getEnabled()) {
              gc.setBackground(FILL_COLOR);
              for (int i = 2; i < w - 2; i += 4) {
               if (i > fillLength) {
                gc.setBackground(BLANK_COLOR);
               }
               gc.fillRectangle(i, 4, 3, h - 8);
              }
              if (controlPoint != null) {
               gc.drawImage(TEMP_H, tempLocation, 0);
              }
             } else {
              gc.setBackground(DISABLE_COLOR);
              gc.fillRectangle(1, 4, w - 1, h - 8);
             }
             break;
          很直觀,對(duì)于水平布局就是在(tempLocation,0)畫出“TEMP_H”圖標(biāo)。外面的if很重要,還記得mouseDown方法中的語(yǔ)句“controlPoint = new Point(e.x, e.y);”和mouseUp方法中的語(yǔ)句“controlPoint = null;”嗎,當(dāng)鼠標(biāo)按下托拽開(kāi)始時(shí),為controlPoint賦值,當(dāng)鼠標(biāo)完成托拽松開(kāi)時(shí),將controlPoint置null,在這個(gè)托拽過(guò)程中controlPoint一直保持非null狀態(tài),所以paintControl方法才將圖標(biāo)畫出。如果現(xiàn)在就著急運(yùn)行程序則會(huì)發(fā)現(xiàn),鼠標(biāo)托拽時(shí)候虛擬圖標(biāo)總停留在最左邊(如果垂直布局停留在最上邊,因?yàn)椴](méi)有為tempLocation賦值默認(rèn)是0),不會(huì)跟隨鼠標(biāo)移動(dòng)。如果要實(shí)現(xiàn)真正的托拽那么必須在鼠標(biāo)移動(dòng)時(shí)反復(fù)執(zhí)行paintControl,下面花大量的筆墨詳細(xì)地介紹mouseMove方法的實(shí)現(xiàn),并簡(jiǎn)單介紹繪圖操作的一些原理和流程。
          開(kāi)門見(jiàn)山,直接將mouseMove函數(shù)的全部代碼列出。
          public void mouseMove(MouseEvent e) {
            if (controlPoint == null) {
             return;
            }
            int maxLength;
            int maxLocator;
            switch (orientation) {
            case HORIZONTAL:
             maxLength = valueToPels(getMaxValue());
             maxLocator = maxLength - TEMP_H.getBounds().width;
             int movedX = e.x - controlPoint.x;
             redraw(tempLocation, 0, TEMP_H.getBounds().width,
               TEMP_H.getBounds().height, false);
             tempLocation = valueToPels(getValue()) + movedX
               - TEMP_H.getBounds().width / 2;
             if (tempLocation < 0) {
              tempLocation = 0;
             } else if (tempLocation > maxLocator) {
              tempLocation = maxLocator;
             }
             break;
            case VERTICAL:
             ......
             break;
            }
           }
          最前面的if語(yǔ)句表明,只有鼠標(biāo)按下時(shí)移動(dòng)鼠標(biāo)才算托拽,道理前面已經(jīng)闡明了。maxLength代表最大值轉(zhuǎn)換得到的像素,maxLocator是虛擬圖標(biāo)左端(上端)最大坐標(biāo),movedX代表托拽的位移量。最下面的if-else if目的很明了。整個(gè)mouseMove函數(shù)中
          redraw(tempLocation, 0, TEMP_H.getBounds().width,
               TEMP_H.getBounds().height, false);
             tempLocation = valueToPels(getValue()) + movedX
               - TEMP_H.getBounds().width / 2;
          是整個(gè)托拽操作最難懂也是技術(shù)含量最高的兩條語(yǔ)句。簡(jiǎn)單起見(jiàn)暫時(shí)用下面的語(yǔ)句代替
          tempLocation = valueToPels(getValue()) + movedX
               - TEMP_H.getBounds().width / 2;
             if (tempLocation < 0) {
              tempLocation = 0;
             } else if (tempLocation > maxLocator) {
              tempLocation = maxLocator;
             }
             redraw();
          其中“tempLocation”的賦值語(yǔ)句不變,變化的是redraw函數(shù)的調(diào)用位置和參數(shù)。這樣的變化使得意思就不難理解了,首先確定tempLocation的值,等號(hào)右邊的計(jì)算結(jié)果也不難理解,然后調(diào)用redraw方法重畫組件。如果這時(shí)候你運(yùn)行程序,托拽時(shí)確實(shí)虛擬光標(biāo)會(huì)跟隨鼠標(biāo)移動(dòng),但是也會(huì)發(fā)現(xiàn)組件閃爍得很厲害!具體程度取決于用戶計(jì)算機(jī)的性能,關(guān)于“組件重繪時(shí)閃爍”的問(wèn)題是繪圖操作的一個(gè)常見(jiàn)問(wèn)題,不僅僅是Java,任何支持繪圖的計(jì)算機(jī)語(yǔ)言都可以暴露這樣的問(wèn)題,當(dāng)年在大學(xué)用MFC、VB的編寫過(guò)畫圖板的人應(yīng)該熟悉這類問(wèn)題。
          在具體討論之前先簡(jiǎn)單講述鼠標(biāo)監(jiān)聽(tīng)器中的mouseMove操作
          一旦為GUI組件添加鼠標(biāo)移動(dòng)監(jiān)聽(tīng)器,當(dāng)鼠標(biāo)光標(biāo)在組件上移動(dòng)時(shí)便調(diào)用監(jiān)聽(tīng)器接口的mouseMove(MouseEvent e)方法,mouseMove調(diào)用的頻率與鼠標(biāo)移動(dòng)的快慢有關(guān),移動(dòng)越快mouseMove被調(diào)用的次數(shù)就越少,反之就越多。假設(shè)鼠標(biāo)從組件上的A點(diǎn)移動(dòng)到B點(diǎn),如果鼠標(biāo)移動(dòng)得足夠快,那么就可以理解為從A直接到B而不經(jīng)過(guò)中間的任何一個(gè)點(diǎn),那么mouseMove函數(shù)僅僅調(diào)用一次。反之鼠標(biāo)慢慢從A移動(dòng)到B,那么中間可能會(huì)經(jīng)過(guò)C、D、E、F......一系列的點(diǎn),而mouseMove也會(huì)被執(zhí)行多次。總之當(dāng)鼠標(biāo)在組件上移動(dòng)時(shí),mouseMove會(huì)頻繁被調(diào)用,之所以閃爍問(wèn)題出在redraw方法,如果redraw方法不加任何參數(shù)那么將對(duì)組件全部重繪,對(duì)于鼠標(biāo)托拽這種操作鼠標(biāo)每移動(dòng)一次就要對(duì)組件全部重繪,性能的代價(jià)可想而知,不閃才怪呢。解決的辦法就是只重繪變化的部分。
          如何做到這一點(diǎn),要先了解必要的繪圖機(jī)制,在SWT中(Swing繪圖原理與其類似)redraw其實(shí)會(huì)包含2個(gè)含義,擦除、繪制,所以得名于redraw,意思就是重繪。首先是根據(jù)傳入redraw方法的參數(shù)確認(rèn)需要擦除的范圍,如果沒(méi)有則擦除全部,然后底層會(huì)創(chuàng)建一個(gè)繪制請(qǐng)求交給操作系統(tǒng)去處理,但是這個(gè)請(qǐng)求不會(huì)在redraw方法調(diào)用完畢后立即被處理,即重繪操作不會(huì)立即執(zhí)行,它是被送進(jìn)底層的事件隊(duì)列中。處理時(shí)的繪制操作是由paintControl完成,所以redraw的調(diào)用會(huì)導(dǎo)致paintControl的執(zhí)行。
          再將那兩行代碼列出來(lái)。
          redraw(tempLocation, 0, TEMP_H.getBounds().width,
               TEMP_H.getBounds().height, false);
             tempLocation = valueToPels(getValue()) + movedX
               - TEMP_H.getBounds().width / 2;
          如果光標(biāo)從A移到B處,redraw方法所做的事情是通知重繪光標(biāo)在A點(diǎn)時(shí)虛擬圖標(biāo)范圍內(nèi)的圖像,然后立即創(chuàng)建一個(gè)繪制請(qǐng)求送到系統(tǒng)事件隊(duì)列,然后更新tempLocation的值。現(xiàn)在再將繪制的那部分代碼列出
          if (controlPoint != null) {
               gc.drawImage(TEMP_H, tempLocation, 0);
              }
          將這兩部分代碼列出來(lái)做個(gè)比較,還有非常重要的一點(diǎn)需要指出,托拽時(shí)虛擬光標(biāo)能呈連續(xù)性移動(dòng),這一功能之所以能得以實(shí)現(xiàn)非常重要的一點(diǎn)是:從底層請(qǐng)求送入事件隊(duì)列到請(qǐng)求被執(zhí)行存在時(shí)間差,利用這個(gè)時(shí)間差(時(shí)間非常短)可以執(zhí)行一些“更新操作”,比如上面的更新tempLocation。簡(jiǎn)單的理解是redraw方法調(diào)用會(huì)以異步方式執(zhí)行擦除、繪制。當(dāng)執(zhí)行繪制操作“gc.drawImage(TEMP_H, tempLocation, 0);”時(shí),tempLocation已經(jīng)是更新后的值了,而redraw表明擦除舊區(qū)域的圖像,因?yàn)楫?dāng)paintControl執(zhí)行時(shí)這部分圖像區(qū)域根據(jù)計(jì)算結(jié)果已經(jīng)不再是虛擬滑塊了。下面做一個(gè)試驗(yàn)。
          添加下面紅色代碼
          redraw(tempLocation, 0, TEMP_H.getBounds().width,
               TEMP_H.getBounds().height, false);
             try {
              Thread.sleep(1000);
             } catch (InterruptedException e1) {
              e1.printStackTrace();
             }
             tempLocation = valueToPels(getValue()) + movedX
               - TEMP_H.getBounds().width / 2;
          這樣在繪制執(zhí)行時(shí),tempLocation還沒(méi)有得到更新,效果運(yùn)行下知曉。

          現(xiàn)在,你可以運(yùn)行完整的程序了,看一下托拽時(shí)的效果。
          完整的程序這里下載

          posted on 2007-10-23 13:35 sun_java_studio@yahoo.com.cn(電玩) 閱讀(13149) 評(píng)論(13)  編輯  收藏 所屬分類: NetBeansSWT

          評(píng)論

          # re: SWT自定義組件之Slider 2007-10-23 13:42 BeanSoft

          我來(lái)頂一下!  回復(fù)  更多評(píng)論   

          # re: SWT自定義組件之Slider 2007-10-30 11:22 Matthew Chen

          redraw(tempLocation, 0, TEMP_H.getBounds().width,TEMP_H.getBounds().height, false);
          是只重繪temp thumb原來(lái)的區(qū)域,因?yàn)閟lider本身添加自己為PaintListener,故重畫的時(shí)候paintControl被調(diào)用,那么paintControl方法作用的是整個(gè)圖形區(qū)域還是temp thumb原來(lái)的區(qū)域?如果是后者,那新的temp thumb所在的區(qū)域由誰(shuí)在何時(shí)繪制? 如果是前者,重繪的不還是整體而不是thumb的區(qū)域嗎?  回復(fù)  更多評(píng)論   

          # re: SWT自定義組件之Slider 2007-10-30 12:47 Matthew Chen

          進(jìn)一步觀察發(fā)現(xiàn),lz的代碼運(yùn)行時(shí)虛擬滑塊的邊緣往往缺失,結(jié)合上一條評(píng)論談到的,我想移動(dòng)滑塊時(shí)重

          繪的是移動(dòng)操作的上一次tempLocation指出的舊滑塊的區(qū)域,而我們想要看到的新滑塊的區(qū)域,并沒(méi)有全部被納入重畫的范圍,修改的方法:
          redraw(tempLocation, 0, TEMP_H.getBounds().width,TEMP_H.getBounds().height, false);
          tempLocation = valueToPels(getValue()) + movedX - TEMP_H.getBounds().width / 2;
          redraw(tempLocation, 0, TEMP_H.getBounds().width,TEMP_H.getBounds().height, false);
          還沒(méi)想到更好的方法。  回復(fù)  更多評(píng)論   

          # re: SWT自定義組件之Slider 2007-10-30 15:47 sun_java_studio@yahoo.com.cn(電玩)

          @Matthew Chen
          paintControl方法作用的是整個(gè)圖形區(qū)域,也就是說(shuō)畫是整個(gè)區(qū)域的重畫,但是擦除如果是整個(gè)區(qū)域擦除的話那屏幕就會(huì)閃了,你可以將redraw(tempLocation, 0, TEMP_H.getBounds().width,TEMP_H.getBounds().height, false); 這行代碼理解為“擦除”,擦除原來(lái)區(qū)域的圖像(在執(zhí)行擦除前,原來(lái)區(qū)域的區(qū)域的圖像是舊虛擬滑塊,等到操作系統(tǒng)執(zhí)行繪制時(shí),那部分區(qū)域已不是虛擬滑塊了),新的temp thumb繪制是在paintControl方法完成的。
          畫是整個(gè)區(qū)域的重畫,擦是部分部分被擦。  回復(fù)  更多評(píng)論   

          # re: SWT自定義組件之Slider 2007-10-30 15:51 sun_java_studio@yahoo.com.cn(電玩)

          @Matthew Chen
          大可不必調(diào)用redraw(tempLocation, 0, TEMP_H.getBounds().width,TEMP_H.getBounds().height, false)兩次。
          開(kāi)始我也是這么寫的,后來(lái)改進(jìn)只在tempLocation賦值后調(diào)用,我運(yùn)行程序的時(shí)候沒(méi)發(fā)現(xiàn)邊緣缺失的現(xiàn)象。  回復(fù)  更多評(píng)論   

          # re: SWT自定義組件之Slider 2007-10-30 19:39 Matthew Chen

          我截了圖
          http://www.aygfsteel.com/Files/djsl6071/桌面.rar
          第一張是正常情況,第2,3張分別顯示向上、下拖動(dòng)時(shí)出現(xiàn)的缺失。
          你可以用windows自帶的圖片查看器打開(kāi),并放大,仔細(xì)看虛擬滑塊的邊緣。  回復(fù)  更多評(píng)論   

          # re: SWT自定義組件之Slider 2007-10-30 22:25 sun_java_studio@yahoo.com.cn(電玩)

          @Matthew Chen
          虛擬滑塊的圖片本來(lái)就是圓角矩形,輪廓是用虛線勾出來(lái)的,你替換其他圖片試試,不應(yīng)該是程序的原因。  回復(fù)  更多評(píng)論   

          # re: SWT自定義組件之Slider 2007-11-20 11:14 黑巧克力

          LZ的虛擬滑塊確似有缺失的現(xiàn)象,當(dāng)朝一個(gè)方向滑動(dòng)時(shí),那個(gè)方向最邊緣的虛線顯示不出來(lái)。當(dāng)垂直方向移動(dòng)一下鼠標(biāo)便可以顯示出來(lái)。  回復(fù)  更多評(píng)論   

          # re: SWT自定義組件之Slider 2007-11-20 16:59 電玩

          @黑巧克力
          My god.代碼實(shí)現(xiàn)中 水平 和垂直 是互逆的,怎么會(huì)出現(xiàn)垂直正常,水平有缺失的情況呢。如果缺失的話不妨采用+1像素長(zhǎng)度試試,只能靠微調(diào)了。  回復(fù)  更多評(píng)論   

          # re: SWT自定義組件之Slider 2007-11-20 18:07 黑巧克力

          redraw(tempLocation-1, 0, TEMP_H.getBounds).width+2,TEMP_H.getBounds().height, false);
          似乎在兩邊多加一個(gè)象素就可以畫全了
            回復(fù)  更多評(píng)論   

          # re: SWT自定義組件之Slider 2007-11-20 18:08 黑巧克力

          @電玩
          我說(shuō)的垂直是垂直于slider的方向,大概是運(yùn)動(dòng)的時(shí)候redraw少畫的象素。  回復(fù)  更多評(píng)論   

          # re: SWT自定義組件之Slider 2014-04-11 09:47 Angus

          這個(gè)DEMO還有嗎?DEMO和源碼能否發(fā)過(guò)來(lái)學(xué)習(xí)下?謝謝
          jaykkll@126.com  回復(fù)  更多評(píng)論   

          # re: SWT自定義組件之Slider 2014-04-11 09:49 Angus

          主要要想做一個(gè)雙滑塊的,能設(shè)置滑塊長(zhǎng)度或者同時(shí)讀取雙滑塊VALUE的SLIDER
          望發(fā)來(lái)看看
          謝謝  回復(fù)  更多評(píng)論   

          TWaver中文社區(qū)
          主站蜘蛛池模板: 准格尔旗| 东安县| 周宁县| 中阳县| 义乌市| 晋州市| 泉州市| 大化| 弥渡县| 巨鹿县| 政和县| 内黄县| 德格县| 黄平县| 延边| 遵化市| 霍城县| 伽师县| 安新县| 西城区| 临洮县| 田东县| 闽清县| 黄山市| 崇礼县| 礼泉县| 仙桃市| 长白| 桂阳县| 土默特右旗| 五莲县| 泰兴市| 天柱县| 镇赉县| 红原县| 木里| 满城县| 南城县| 冷水江市| 和田县| 湾仔区|