網頁

2010年7月1日 星期四

[GWT] GWTCanvas 簡易範例

GWTCanvas 是 Google Web Toolkit Incubator 的一個子項目,其作用是提供了操作 HTML5 Canvas 的 API 供 GWT 使用。

在官方的 wiki 上可以看到簡易的操作說明,因為並不複雜,所以這裡就不多做說明了。有興趣者也可以參考 API


接下來我利用 GWTCanvas,配合 UiBinder, Timer,試著寫了一個 Bubble Sort 的教學範例,其功能就像是以往常見的 Applet。(由於此篇文章的重點是在於 Canvas 而不是 Bubble Sort,所以請原諒我只撰寫了非常陽春的 Demo)

完整的專案可以在此下載: canvas.zip

程式碼內容如下: BubbleSortDemo.java
package canvas.client;

import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.core.client.GWT;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiFactory;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.uibinder.client.UiHandler;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.HTMLPanel;
import com.google.gwt.user.client.ui.RootLayoutPanel;
import com.google.gwt.widgetideas.graphics.client.Color;
import com.google.gwt.widgetideas.graphics.client.GWTCanvas;

public class BubbleSortDemo implements EntryPoint {

    interface uiBinder extends UiBinder<HTMLPanel, BubbleSortDemo> {
    }

    private static uiBinder uiBinder = GWT.create(uiBinder.class);
    
    @UiField GWTCanvas canvas;
    @UiFactory GWTCanvas makeCanvas(int width, int height) {
        return new GWTCanvas(width, height);
    }
    
    public static final int LENGTH = 10;
    
    private Timer t;
    private int i;
    private int j;
    private int[] arr;

    @UiField Button nextBtn;
    @UiField Button playBtn;
    @UiField Button stopBtn;
    @UiField Button resetBtn;
    
    private void initArray(){
        arr = new int[LENGTH];
        
        for(int i=0;i<LENGTH;i++){
            arr[i] = (int)(Math.random()*100); 
        }
    }

    private void initVariable() {
        i = LENGTH-1;
        j = 0;
    }
    
    private void display(){
        canvas.clear();
        
        for(int i=0;i<LENGTH;i++){          
            int gray = arr[i]*2;
            canvas.setFillStyle(new Color(gray, gray, gray));

            int height = arr[i];
            int x = 30+i*30;
            int y = 130-height;
            canvas.fillRect(x, y, 20, height);
        }
    }   
    
    private void swap(int a, int b){
        int t = arr[a];
        arr[a] = arr[b];
        arr[b] = t; 
    }

    @Override 
    public void onModuleLoad() {

        HTMLPanel outer = uiBinder.createAndBindUi(this);
        RootLayoutPanel.get().add(outer);
        
        initArray();
        
        display();

        /*
        // This is the original version bubble sort.
        for(int i=LENGTH-1;i>=0;i--){
            for(int j=0;j<i;j++){
                if(arr[j]>arr[j+1]){
                    swap(j, j+1);
                }
            }
        }
        */

        initVariable();
        
        t = new Timer() {
            public void run() {
                while(i>=0){
                    
                    while(j<i){
                        
                        if(arr[j]>arr[j+1]){
                            swap(j, j+1);
                        }
                        
                        display();
                        
                        j++;
                        return;
                    }
                    
                    i--;
                    j = 0;
                    return;
                }
                
                t.cancel();             
            }
        };
        
    }
    
    @UiHandler("nextBtn")
    void handleNext(ClickEvent e){
        t.run();
    }
    
    @UiHandler("playBtn")
    void handlePlay(ClickEvent e){
        t.scheduleRepeating(500);
    }
    
    @UiHandler("stopBtn")
    void handleStop(ClickEvent e){
        t.cancel();
    }
    
    @UiHandler("resetBtn")
    void handleReset(ClickEvent e){
        initArray();
        initVariable();
        display();
    }

}



其對應的 UiBinder: BubbleSortDemo.ui.xml
<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
<ui:UiBinder xmlns:ui="urn:ui:com.google.gwt.uibinder"
 xmlns:g="urn:import:com.google.gwt.user.client.ui" xmlns:graphics="urn:import:com.google.gwt.widgetideas.graphics.client">
 <ui:style>  
  .canvas{
   border: solid 1px black;
  }
 </ui:style>
 <g:HTMLPanel>
  <graphics:GWTCanvas ui:field="canvas" styleName="{style.canvas}" width="350" height="200"></graphics:GWTCanvas>
  
  <br />
  <g:Button ui:field="nextBtn">Next</g:Button>
  <g:Button ui:field="playBtn">Play</g:Button>
  <g:Button ui:field="stopBtn">Stop</g:Button>
  <g:Button ui:field="resetBtn">Reset</g:Button>
 </g:HTMLPanel>
</ui:UiBinder> 

2010年2月24日 星期三

[Java] Concurrency in Swing

參考 Java TutorialsConcurrency in Swing 章節所寫的筆記。

一個好的 Swing 程式會利用 concurrency 去建立不會 "凍結" 的 UI — 無論程式正在做什麼,都會回應使用者的操作。

一個 Swing programmer 將需要處理以下三種類型的 thread:
  1. Initial thread:
    啟動應用程式的 thread
  2. Event dispatch thread:
    所有的 Event-handling 程式都位於此 thread,大部分與 Swing 互動的程式也在此 thread 上執行。
  3. Worker thread:
    也稱做 background thread,需要消耗較長時間的 task 位於此 thread。

一個 Swing 程式應該這樣寫:
  1. Initial thread 沒有太多事要做,它最主要的工作就是建立一個 Runnable 物件負責建立 GUI,並且將這個物件交由 event dispatch thread 負責執行。 
     
  2. 將建立 GUI 交由 event dispatch thread 負責的方法為 javax.swing.SwingUtilities.invokeLater 或著 javax.swing.SwingUtilities.invokeAndWait,這兩個 method 都傳入一個 Runnable 物件。它們的差別在於 invokeLater 只會將工作加入排程後便返回,而 invokeAndWait 將會等待工作執行結束後才返回。
         
  3. 在 Applet 中,應該在 init 方法中呼叫 invokeAndWait,否則 init 可能會在 GUI 建立完成前就返回,這將可能造成瀏覽器啟動 applet 時的問題。
         
  4. 在其他類型的程式中,建立 GUI 通常都是 initial thread 中的最後一項工作,因此無論呼叫 invokeLater 或 invokeAndWait 皆可。
  5. 一旦 GUI 建立完成,程式的主要運作就變成 event-driven。其中較短的 task 將交由 event dispath thread,較長的 task 則交給 worker thread。

2010年2月14日 星期日

[GWT] 使用外部 resource

使用外部 resourceDeclarative Layout with UensureInjected()iBinder 其中的一個章節,並且已經由 PsMonkey 完成此文的翻譯

我在實作這個章節所提及的方法時,遭遇了一點問題,因此提出來與大家分享。

在此說明一下我所遭遇的問題:

  1. 我寫了一個名為 Resources 的 interface,打算在其他的 template file 中使用,內容如下。


    public interface Resources extends ClientBundle {
        @Source("Style.css")
        MyStyle style();
    
        @Source("Logo.jpg")
        ImageResource logo();
    
        public interface MyStyle extends CssResource {
            String red();
        }
    } 
  2. 當然 Style.css 與 Logo.jpg 都是存在的,並且 Sytle.css 中也定義了 .red
  3. 然而當我使用如下的 template file 去使用 Resources 時,雖然 Image 能夠正常顯示,但是所有與 CssResource 相關的設定都沒有作用。


    <ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
        xmlns:g='urn:import:com.google.gwt.user.client.ui'>
    
      <ui:with field='res' type='com.my.app.widgets.logoname.Resources'/>
    
      <g:HTMLPanel>
    
        <g:Image resource='{res.logo}'/>
    
        <div class='{res.style.red}'>
          this text should be red
        </div>
    
      </g:HTMLPanel>
    </ui:UiBinder> 
  4. 如果使用 Firebugs 等工具觀察, 會看到對應的 HTML 元素確實被指定了 CSS class,然而在 CSS file 內卻沒有對應的設定存在。

解決的方法為,建立一個 Resources 實體並呼叫 CssResource 的 ensureInjected() 方法,以確保 CssResource 有被加入 DOM 中。
Resources resources = GWT.create(Resources.class);
resources.style().ensureInjected(); 

值得注意的是,無論創造了幾個 MyStyle 實體(包含在 Resources 內),也只需要執行一次 ensureInjected() 。所以合理的作法應該是在 EntryPoint 內呼叫 ensureInjected(),如果希望所有的頁面都能使用同一個 Resources 實體,可以將此處建立的物件傳給其他頁面,詳細作法參考同一篇文章中的 Share resource instances 章節(中譯: 共用 resource 的 instances)。

2010年1月7日 星期四

[Java] ImageIO.read() 的怪異行為

我個人覺得這個問題還算蠻棘手的,所以在這篇文章的開頭先提供幾個關鍵字,讓同樣遇到此問題的朋友更有機會找到這篇文章。

[關鍵字]

ImageIO
Socket
Serializable
BufferedImage
NullPointerException


[問題描述]

我寫了一支 Server/Client 程式,Server 端會利用 ImageIO.write(image, "png", out) 將圖片傳給 Client 端,Client 端則用 ImageIO.read(in) 讀進為 BufferedImage 物件。

但是這支程式只有第一張圖片能夠正確的傳送,從第二張開始 ImageIO.read() 就只會回傳 null,如果在你的 code 中還有對圖片進行操作,那就可能會出現 NullPointerException 或著其他的錯誤。

[原因]

經過仔細分析後,我發現 ImageIO.read() 在讀進 PNG 格式的資料時,會留下 16 byte 在 stream 內,因此在讀取第二張圖片時就會讀到這多餘的 16 byte 而造成錯誤。

經由 PTT Java 板板友 LPH66 說明,這多出的 16 byte 中前 4 byte 是 PNG IDAT 區的 checksum,最後 12 byte 是 IEND 區。即使沒有這些資料,依然可以正確的解析圖片。

PTT Java 板板友 sbrhsieh 並補充這是因為 ImageReader 在處理數據的順序/數量上的不匹配造成的。在針對 PNG 格式時,ImageIO.read() 有短缺消耗的問題;而在處理 JPG 格式時,則有過度消耗的行為,也就是讀取一張圖片時,可能會連下一張圖片的資料也被消耗掉,使得下一張圖片無法正確讀入。在使用 ImageIO 與 FileInputStream 配合時,通常一個檔案內只會有一張圖片的資料,在讀進 JPG 時會遇到 EOF,因此並不會造成問題。

[解決方法]

當需要利用 ImaqeIO 與 Stream 傳送多張圖片時,我建議使用下列的方式:
// 修改自 PTT Java 板板友 ogamenewbie 所提供的程式
public class SerializableImage implements Serializable {
    
    byte[] data;

    public SerializableImage(BufferedImage image, String type) throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ImageIO.write(image, type, baos);
        data = baos.toByteArray();
    }

    public BufferedImage getBufferedImage() throws IOException {
        return ImageIO.read(new ByteArrayInputStream(data));
    }
}
寫入圖片時:
SerializableImage serialImage = new SerializableImage(image, "png");            
out.writeObject(serialImage);
讀取圖片時:
SerializableImage serialImage = (SerializableImage)in.readObject();                
BufferedImage image = serialImage.getBufferedImage();    

[GWT] 使用具有 Constructor 參數的 Widget

本文是根據這篇文章重新以中文說明 (Using a widget that requires constructor args)

下方是使用 uiBinder 建立 UI 的簡單例子:
<!-- UserDashboard.ui.xml -->

<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
    xmlns:g='urn:import:com.google.gwt.user.client.ui'
    xmlns:my='urn:import:com.my.app.widgets' >

    <g:HTMLPanel>
        <my:CricketScores ui:field='scores' />
    </g:HTMLPanel>
</ui:UiBinder>
執行時 GWT 會偷偷的用 GWT.create() 建立 CricketScores 的實體 (經由Deferred Binding)。

但如果 CricketScores 的 Constructor 必需要傳入參數時,例如:
public CricketScores(String... teamNames) {...} 
這時 GWT.create() 就會出現錯誤:
[ERROR] com.my.app.widgets.CricketScores has no default (zero args) constructor. To fix this, you can define a @UiFactory method on the UiBinder's owner, or annotate a constructor of CricketScores with @UiConstructor.

這時候共有三種解決方法:
  1. 在 UserDashboard.java 中加入 @UiFactory method:
    // method name is insignificant
    @UiFactory CricketScores makeCricketScores() {
        return new CricketScores(teamNames);
    }
    
    @UiFactory method 的 method name 沒有特定的命名規則,GWT 會自動根據 return type 做判斷。也正因如此,如果一個 class 當中出現兩個相同 return type 的 @UiFactory method 時,GWT 也會顯示錯誤訊息:

    [ERROR] Duplicate factory in class UserDashboard for type CricketScores
  2. 使用 @UiConstructor:

    將 CricketScores 的 Constructor 加入 @UiConstructor annotation:
    public @UiConstructor CricketScores(String teamNames) {
        // ....
    } 
    
    並在 UserDashboard.ui.xml 內,CricketScores 標籤內加入與 Constructor 參數相同的屬性即可:
    <my:CricketScores ui:field='scores' teamNames='AUS, SAF, WA, QLD, VIC'/>
    
  3. 由 UiField(provided=true) 提供已建立好的物件:
    於 UserDashboard's Constructor 將 CricketScores 物件傳入,並以 @UiField 宣告 reference 變數指向 CricketScores 物件:
    public class UserDashboard extends Composite {
        interface MyUiBinder extends UiBinder<Widget, UserDashboard>;
        private static final MyUiBinder uiBinder = GWT.create(MyUiBinder.class);
    
        @UiField(provided=true)
        final CricketScores cricketScores; // cannot be private
    
        public UserDashboard(CricketScores cricketScores) {
            this.cricketScores = cricketScores;
            initWidget(uiBinder.createAndBindUi(this));
        }
    }
    

2009年12月20日 星期日

[GWT] 利用 HandlerManager 實作共用的 Event Bus

GWT 從版本 1.6 開始提供了新的 Event Model - Handler,並捨棄了原先的 Listener 方式。

關於 Listener 與 Handler 的差別,我推荐看這篇文章: GWT’s new Event Model – Handlers in GWT 1.6 [1]。

在這篇文章中,我將兩者與本篇文章有關的部份做介紹:


Listener:
  1. 對於每一個 Event Source 會有一個對應的 Listener,例如 MouseListener, KeyboardListener...等。
  2. 在每一個 Widget 中,針對每一種 Event Source 的 Listener,都會用一個獨立的 ListenerCollection 去紀錄。
Handler:
  1. 每一個 Event Type 都會有一個對應的 Handler,因此原先 MouseListener 相關的事件,將會被重新區分成 MouseDownHandler, MouseUpHandler...等。這樣做的一個額外的好處是,當只需要針對一種 Event Type 做處理時,實作 Handler 的類別只需要 over-ridding 一個對應的 method,而不需要像寫 Listener 時,仍然需要 over-ridding 其他沒用到的 method。
  2. 每一個 Widget 中,都只會有一個 HandlerManager,負責紀錄所有的 Event Handler。

由於 HandlerManager 能夠針對不同的 Event,dispatch 其對應的 Handler 去處理,因此我們也可以利用 HandlerManager 來做為整個 Web App 共用的 Event Bus。[2], [3]


先說明一下本篇文章欲使用的例子:
在一個 Web App 上,有許多的 Panel,我們現在希望當按下 A Panel 上的 Button 時,B Panel 上的 TextBox 會輸出文字。

在原本的做法裡,我們會讓 A panel 實作 ClickHandler,當 A panel 收到事件時,就去呼叫 B panel 所提供的方法。

這個作法會使得 A panel 必須持有 B panel 的 reference(就 UML 的角度來說就是 A 對 B 有 Aggregation 關係),這樣會使得 A, B 之間的耦合度高,造成未來擴充、修改的困難。

接下來就是解決辦法啦!

  1. 首先,於 EntryPoint 建立 HandlerManager 物件,並且將 Reference 傳給所有的 sub-component,這個部份可以利用 Constructor 達成。
    HandlerManager eventBus = new HandlerManager(null); 
  2. 接下來為每一個欲處理的事件寫一組 Event / Handler
    public class FooEvent extends GwtEvent<FooEvent.FooHandler> {
    
        public interface FooHandler extends EventHandler{
            void onFoo(FooEvent event);
        }
    
        public static final GwtEvent.Type<FooHandler> TYPE =
                new GwtEvent.Type<FooHandler>();
    
        @Override
        protected void dispatch(FooHandler handler) {
            handler.onFoo(this);
        }
    
        @Override
        public GwtEvent.Type<FooHandler> getAssociatedType() {
            return TYPE;
        }
    }
    
  3. 然後在 B panel 裡頭寫上事件處理,並且向 Event Bus 註冊。
    @Override
    protected void onLoad(){
        // register event handler
        fooRegistration = eventBus.addHandler(FooEvent.TYPE, new FooHandler(){
            @Override
            public void onFoo(FooEvent event) {
                textBox.setText("A panel's button click!");
            }
        });
    }
    
    @Override
    protected void onUnload(){
        // unregister event handler
        fooRegistration.removeHandler();
    }
    
  4. 最後是 A panel 的事件觸發,當按下按鈕時:
    eventBus.fireEvent(new FooEvent());
    

這樣一來,A 與 B 彼此可以完全不知道對方的存在,將可以大大的降低耦合度。

參考資料:
[1]: GWT’s new Event Model – Handlers in GWT 1.6
[2]: Google Web Toolkit Architecture: Best Practices For Architecting Your GWT App
[3]: GWT Event Bus Discussions
[4]: 1.6 版多了什麼新東西?

[Java] 製作 Sign Applet

前言:

Applet 是用於網頁上的小程式,基於安全理由,它不允許你做任何的 IO 操作,(存取本機電腦 及 網路連線...等,只有存放該網頁的 server 例外)否則一旦開了惡意的 applet,電腦就如同中了木馬一般。

這種概念稱為 Sandbox,只允許你在盒子內部操作,所以無論你在盒子裡做了什麼事,都不會影響到盒子外的世界。如果有特殊的需求,可以使用 Sign Applet,在開啟程式前會先出現確認視窗,待使用者同意後,程式便能夠有較高的存取權限。

製作 Sign Applet 方法:

  1. 產生 key:
    keytool -genkey -keyalg RSA -alias "key_name"


    1. 如果 key store 目前並不存在,
      則必須設定 keystore 密碼;
      反之,如果先前已經設定過密碼,
      則此時就需要輸入先前所設定的密碼。
    2. 接著必須輸入一些基本資料,
      如: 姓名, 單位, 國碼, ... 等等,
      此處就不贅述了。
    3. 最後輸入 key 密碼,即可順利產生 key。

  2. 將 key 加入 applet 中:
    jarsigner "applet.jar" "key_name"
    此時需要依序輸入 key store 密碼與 key 密碼。 
  3.  Sign Applet 製作完成,
    此時連上嵌入此 Sign Applet 的網頁,
    即會跳出確認視窗,詢問是否同意進行操作。 
延伸說明:
  1. 在產生 keytool 時,如果沒有特別指定,
    將會存放在此: ~/.keystore
    註: 此處以 ubuntu 為例, Windows 作業系統將會在其他的位址。
    如果需要額外指定存放位址, 需加上參數 -keystore "path"。
  2. 列出目前已有的 key:
    keytool -list
  3. 檢查 jar 是否已經完成 Sign
    jarsigner -verify "applet.jar"