摘要: 很久沒(méi)有回來(lái)這里寫(xiě)技術(shù)BLOG了,這里的氛圍還行,大家都對(duì)一個(gè)問(wèn)題積極的思考(至少之前這里給我的感覺(jué)是這樣的),2年里面自己也忙著做些事情,沒(méi)有寫(xiě),最近有空也就寫(xiě)寫(xiě),偶爾會(huì)去oschine.net看看新聞,然后就在那里看到了一個(gè)人提出的問(wèn)題很有意思,就是怎么表達(dá)式求解,例如(1 + 2) / 3 - 1 * 2 + 5 / (3 + 2)這樣的字符串輸入,怎么樣解析之后輸出結(jié)果。說(shuō)來(lái)也好笑,對(duì)于我...
閱讀全文
posted @
2011-11-09 10:36 ruislan 閱讀(1761) |
評(píng)論 (6) |
編輯 收藏
摘要: 很久沒(méi)來(lái)了,不是一位朋友給我發(fā)郵件問(wèn)我關(guān)于swing的問(wèn)題,才想起,然后翻看了之前的代碼,發(fā)現(xiàn)當(dāng)年還實(shí)現(xiàn)了一個(gè)Vista風(fēng)格的按鈕沒(méi)有放出來(lái),現(xiàn)在補(bǔ)上,也許現(xiàn)在人們的swing水平對(duì)我這代碼不屑一顧,不過(guò)還是依然拋磚引玉,給未知的人們一個(gè)啟發(fā)。還是老慣例,上效果圖:
正常情況下:(和vista一樣具有焦點(diǎn)的按鈕的顏色漸深漸淺的循環(huán)變化)
鼠標(biāo)在區(qū)域內(nèi)時(shí):
鼠標(biāo)按下去:
然后是代...
閱讀全文
posted @
2009-09-12 12:54 ruislan 閱讀(2486) |
評(píng)論 (3) |
編輯 收藏
認(rèn)為自己是達(dá)人的就不用看了。只是一點(diǎn)小技巧,不敢班門(mén)弄斧,做個(gè)總結(jié),為那些還不知道的解解惑,隨便告訴大家我還活著。
最近客戶提了個(gè)小改動(dòng),客戶網(wǎng)站上圖片存放的目錄需要改動(dòng)一下。例如在網(wǎng)上訪問(wèn)是www.tkk7.com/images/*.*,在服務(wù)器上的目錄是D:/<webroot>/images/*.*,客戶想把這個(gè)images目錄下的資源全部移動(dòng)到E:/data/里面去,但是在網(wǎng)上www.tkk7.com/images/*.*還是同樣可以訪問(wèn)得到,我剛開(kāi)始犯了形式主義的錯(cuò)誤,老是想用程序解決,一會(huì)filter,一會(huì)servlet/action,后來(lái)我配置程序的時(shí)候突然看到了server.xml,于是我想到了選擇用映射的方式。正好,server.xml中的<Context>就是做這個(gè)事情的。于是乎我們?cè)?lt;Host></Host>中增加了一個(gè)<Context docBase="E:/data/images" path="/images">,OK,重啟之后,所有檢索www.tkk7.com/images路徑下的資源實(shí)際上都由E:/data/images下的資源提供了。
posted @
2008-02-15 16:41 ruislan 閱讀(1272) |
評(píng)論 (3) |
編輯 收藏
在寫(xiě)多線程程序的時(shí)候,你就像個(gè)經(jīng)理,手下有那么或多或少的職員,你負(fù)責(zé)協(xié)調(diào)職員之間的工作,如果你稍不留神,職員之間就陷入了相互等待的尷尬狀態(tài)。還好,大多數(shù)時(shí)候多線程都還在我們掌控之內(nèi),即便是遇到這樣的deadlock情況,我們也能夠去修正,但是有的時(shí)候生活就是那么不盡人意,特別是NIO這種你不能掌控的時(shí)候,且看下面的代碼:
/**
* @(#)DeadLock.java v0.1.0 2007-12-13
*/
package ruislan.rswing.test;
import java.net.InetSocketAddress;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.concurrent.Executors;
/**
* NIO DeadLock
*
* @author ruislan <a href="mailto:z17520@126.com"/>
* @version 0.1.0
*/
public class DeadLock {
public static void main(String[] args) throws Exception {
Service service = new Service();
Executors.newSingleThreadExecutor().execute(service);
SocketChannel channel = SocketChannel.open();
channel.configureBlocking(false);
channel.connect(new InetSocketAddress("http://www.tkk7.com", 80));
service.addChannel(channel);
}
static class Service implements Runnable {
Selector selector;
public Service() {
}
public void run() {
try {
selector = Selector.open();
while (true) {
selector.select();
System.out.println(selector.selectedKeys().size());
}
} catch (Exception e) {
}
}
public void addChannel(SocketChannel channel) {
try {
channel.register(selector, SelectionKey.OP_CONNECT
| SelectionKey.OP_READ);
System.out.println("can reach here?when pigs fly!");
} catch (ClosedChannelException e) {
e.printStackTrace();
}
}
}
}
乍看之下,我們的代碼沒(méi)有問(wèn)題,但是運(yùn)行之后你會(huì)發(fā)現(xiàn),這句System.out.println("can reach here?when pigs fly!");永遠(yuǎn)無(wú)法執(zhí)行,也就是說(shuō)register()方法被阻塞了!Oh god bless,讓我們看看JavaDoc是怎么說(shuō)的:
...
可在任意時(shí)間調(diào)用此方法。如果調(diào)用此方法的同時(shí)正在進(jìn)行另一個(gè)此方法或 configureBlocking 方法的調(diào)用,則在另一個(gè)操作完成前將首先阻塞該調(diào)用。然后此方法將在選擇器的鍵集上實(shí)現(xiàn)同步,因此如果調(diào)用此方法時(shí)并發(fā)地調(diào)用了涉及同一選擇器的另一個(gè)注冊(cè)或選擇操作,則可能阻塞此方法的調(diào)用。
...
看這句“可在任意時(shí)間調(diào)用此方法。”,也就是說(shuō)我們調(diào)用的時(shí)間沒(méi)有任何限制,而阻塞的情況只會(huì)出現(xiàn)在“如果調(diào)用此方法的同時(shí)正在進(jìn)行另一個(gè)此方法或 configureBlocking 方法的調(diào)用”的情況下,即便是阻塞了,我相信“正在進(jìn)行另一個(gè)此方法或configureBlocking”也不會(huì)花掉太多的時(shí)間,況且這里沒(méi)有上面這樣的情況出現(xiàn)。那register()是被誰(shuí)擋住了?或者是BUG?
我們來(lái)分析一下程序,程序有兩個(gè)線程主線程和Service線程,主線程啟動(dòng)后啟動(dòng)了Service線程,Service線程啟動(dòng)Selector然后Service線程陷入select()的阻塞中,同時(shí),主線程調(diào)用Service的addChannel()方法來(lái)添加一個(gè)SocketChannel,嗯,兩個(gè)線程之間唯一的聯(lián)系就是selector,看來(lái)要從selector尋找線索,很可惜,selector的實(shí)現(xiàn)沒(méi)有源代碼可查,不過(guò)可以肯定是channel的register()會(huì)調(diào)用selector的register(),雖然此時(shí)持有selector的Service線程被select()方法所阻塞,但是并不影響其他線程對(duì)其操作吧?那么,剩下的解釋就是Selector的select()方法和register()方法公用了一個(gè)鎖,select()方法阻塞住了,所以register()拿不到這個(gè)鎖了,那么這樣一來(lái)我們就只能保證讓select()或者register()不能同時(shí)調(diào)用或者register()調(diào)用的時(shí)候select()不持有這個(gè)鎖,也就是說(shuō)我們要用Service線程自己來(lái)執(zhí)行addChannel()方法,所以改進(jìn)如下:
/**
* @(#)DeadLock.java v0.1.0 2007-12-13
*/
package ruislan.rswing.test;
import java.net.InetSocketAddress;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Queue;
import java.util.concurrent.LinkedBlockingQueue;
/**
* NIO DeadLock
*
* @author ruislan <a href="mailto:z17520@126.com"/>
* @version 0.1.0
*/
public class DeadLock {
public static void main(String[] args) {
Service service = new Service();
new Thread(service).start();
for (int i = 0; i < 5; i++) {
new Thread(new ChannelAdder(service)).start();
}
}
static class ChannelAdder implements Runnable {
private Service service;
public ChannelAdder(Service service) {
this.service = service;
}
@Override
public void run() {
try {
SocketChannel channel = SocketChannel.open();
channel.configureBlocking(false);
channel.connect(new InetSocketAddress(
"http://www.tkk7.com", 80));
service.addChannel(channel);
} catch (Exception e) {
e.printStackTrace();
}
}
}
static class Service implements Runnable {
private Selector selector;
private Queue<SocketChannel> pendingRegisters;
public Service() {
pendingRegisters = new LinkedBlockingQueue<SocketChannel>();
}
public void run() {
try {
selector = Selector.open();
while (true) {
selector.select();
System.out.println(selector.selectedKeys().size());
handlePendingRegisters();
}
} catch (Exception e) {
}
}
public void handlePendingRegisters() {
while (!pendingRegisters.isEmpty()) {
SocketChannel channel = pendingRegisters.poll();
try {
channel.register(selector, SelectionKey.OP_CONNECT);
System.out.println("can reach here?yeah!");
} catch (ClosedChannelException e) {
e.printStackTrace();
}
}
}
public void addChannel(SocketChannel channel) {
pendingRegisters.offer(channel);
selector.wakeup();
}
}
}
新的代碼,我們?cè)赟ervice的線程提供了一個(gè)待處理Channel隊(duì)列,然后在添加一個(gè)SocketChannel到隊(duì)列中時(shí)喚醒這個(gè)selector,取消阻塞,然后在Service的循環(huán)中處理這個(gè)pendingChannel,這樣就避免這個(gè)Deadlock的發(fā)生了。當(dāng)然我們亦可以在那個(gè)代碼上將select的超時(shí)時(shí)間設(shè)置非常的短,然后讓兩個(gè)線程去競(jìng)爭(zhēng),這樣做有太多的不可控性,不推薦了。
posted @
2007-12-13 18:31 ruislan 閱讀(1349) |
評(píng)論 (3) |
編輯 收藏
UI作為用戶與電腦的交互界面,如何更好的服務(wù)于人,讓人們用起來(lái)方便、簡(jiǎn)單、快捷一直是UI開(kāi)發(fā)者應(yīng)該有的覺(jué)悟,作為開(kāi)發(fā)人員的我們來(lái)說(shuō),不應(yīng)該只是把UI推給電腦平面設(shè)計(jì)人員,更不應(yīng)該一手包辦了(如果你不是一個(gè)人的話)。我們開(kāi)發(fā)人員常常在開(kāi)發(fā)UI的時(shí)候避重就輕,基本上都在強(qiáng)調(diào)code的美學(xué),模式的應(yīng)用而忽略了真實(shí)用戶的感受。我們常常得意于自己技術(shù)的美麗,而將一些比自己水平低的應(yīng)用嗤之以鼻。但是用戶卻從來(lái)不關(guān)心代碼是如何寫(xiě)的,他們關(guān)心這個(gè)應(yīng)用是否對(duì)他們有用,順手乎?聰明乎?所以如果我們只是美麗于自己的設(shè)計(jì),太關(guān)注軟件的本身而忽略了用戶的感受,就跟某些象牙塔里拿著錢(qián)做些無(wú)用的研究的人沒(méi)什么兩樣,或許有個(gè)美麗的名詞,為了科學(xué)。
那么如何才能算是好的UI人性化的設(shè)計(jì)呢?這個(gè)得看針對(duì)的用戶主要是哪些。我們熟知的操作系統(tǒng)Windows XP,Windows Vista,Vista是微軟最新的操作系統(tǒng),包含了很多開(kāi)發(fā)人員辛苦的結(jié)晶,但是在我身邊的很多人都不愿意裝它,也包括一些新聞的調(diào)查也說(shuō)Vista不如當(dāng)年XP出世那般火爆,他們大多數(shù)不愿裝的都說(shuō)了同樣的話,XP都還有很多不懂,怕Vista更搞不懂,說(shuō)實(shí)話我用過(guò)Vista,就我這么一個(gè)算是業(yè)內(nèi)人士用起來(lái)當(dāng)然駕輕就熟,再加上我們都有勇于創(chuàng)新的精神,所以常常去用新的東西,而普通客戶就不這么想了,我問(wèn)了幾個(gè)不懂電腦才安裝了Vista的用戶的感受,“開(kāi)始菜單的‘開(kāi)始’兩個(gè)字沒(méi)有了,我還以為換了位置”,“界面比XP漂亮啊,但是我的機(jī)器好像有點(diǎn)慢,是不是要設(shè)置個(gè)什么啊”……再來(lái)我們熟知的AJAX,我已經(jīng)接到過(guò)很多次不同的人給我的電話,說(shuō)“為什么網(wǎng)頁(yè)打開(kāi)的時(shí)候突然好卡了,以前不這樣???”,“網(wǎng)頁(yè)瀏覽不了,老說(shuō)請(qǐng)稍候,數(shù)據(jù)加載中,等了很久,就是不出現(xiàn)”……面對(duì)這些電話或許我們會(huì)說(shuō),你們?cè)趺茨敲幢堪?,它卡是因?yàn)樵谙聳|西,在執(zhí)行JS,寫(xiě)JS的人太垃圾,浪費(fèi)了資源,不出現(xiàn)就刷新啊,不要瀏覽那個(gè)不專業(yè)的網(wǎng)站了,等等就OK了等等回答,其實(shí)很多時(shí)候我們可以避免用戶的問(wèn)題出現(xiàn),例如你的AJAX的JS太大的時(shí)候,可以先提示用戶說(shuō),數(shù)據(jù)量較大,請(qǐng)稍后,如果長(zhǎng)時(shí)間無(wú)反應(yīng),請(qǐng)按瀏覽器的刷新按鈕,或者嘗試按下F5鍵。
我還見(jiàn)過(guò)許多軟件鼓吹自己的功能如何強(qiáng)大,如何厲害,多么的人性化,但是我打開(kāi)他們的軟件,居然發(fā)現(xiàn)只能用鼠標(biāo)操作!!這是多么大的UI設(shè)計(jì)失敗!在舉一個(gè)例子,MSN和QQ兩個(gè)IM,如果你用MSN,在聯(lián)系人框里按上下的話,MSN會(huì)很聰明的明白你是要選擇上一位或者下一位聯(lián)系人,而QQ會(huì)很聰明的明白你是要拖動(dòng)滑動(dòng)條!@#$,還有很多軟件記憶力太差,不管我如何操作,它就是記不住,關(guān)閉軟件重新啟動(dòng)后又回到了最初的模樣,還有的軟件自信心不足,一再問(wèn)我“你確定嗎?”,“真的要這樣做嗎?”,“或許您不小心點(diǎn)了?”而我只是在點(diǎn)關(guān)閉這個(gè)娛樂(lè)性質(zhì)的軟件而已,而有些軟件又特膽肥,做了一個(gè)不可恢復(fù)的操作盡然連提示都沒(méi)有,還有的軟件文化太差,常常把一個(gè)按鈕或者圖標(biāo)該表達(dá)的含義弄得模棱兩可,以至于常常讓我們會(huì)錯(cuò)意,做錯(cuò)操作,或者把一些高風(fēng)險(xiǎn)的操作放在常用操作的旁邊,很容易點(diǎn)錯(cuò),還有的把不常用的操作也放到常用操作區(qū),還不告訴用戶怎么去掉,這樣的例子不勝枚舉。
出現(xiàn)這些問(wèn)題的原因在于我們與用戶之間的思維方式有著很大的不同。例如在寫(xiě)文章之前我才將老爸從我的電腦椅上請(qǐng)下來(lái),請(qǐng)下來(lái)之前他正在看我吃飯前的網(wǎng)頁(yè)——“界面九宮格”,我說(shuō)您能看得懂嘛,他說(shuō)“不懂,不過(guò)這軟件的界面不都這樣嘛?再說(shuō)了,一張紙就8個(gè)方位,加上中間正好九個(gè),你的東西不擺這里擺哪里?。?#8221;,我正要解釋一下這與軟件設(shè)計(jì)的關(guān)系,但是突然一想,是啊,有道理啊,要是我給他老人家再解釋一下可以放在上面和下面,那不就是3D的了。再比如我一直都很不屑一顧的網(wǎng)絡(luò)實(shí)名,但是當(dāng)它被我在很多人的機(jī)器里面消滅之后,很多人都打電話問(wèn)我,怎么在地址欄里面輸入漢字,跳出搜索界面了,不是那個(gè)漢字的網(wǎng)站了,以前是有的,原來(lái)我只看到了它流氓的一面,忽略了普通用戶是根本記不住網(wǎng)址在哪里,甚至有些用戶不懂英文,你怎么讓他記得住全是英文的網(wǎng)址呢?不過(guò)過(guò)了幾天,他們都說(shuō)不用了,有一個(gè)網(wǎng)站導(dǎo)航網(wǎng)址做了他們的主頁(yè),他們平日想去的網(wǎng)站都在上面列著的,我后來(lái)才知道,就是被我以前同寢室的刪除了半天的hao123。所以我們必須充分考慮我們的應(yīng)用是針對(duì)哪些用戶,他們是哪一類人,習(xí)慣是什么,當(dāng)然還有就是UI設(shè)計(jì)的一些基本的東西,例如鼠標(biāo)能夠完成的動(dòng)作,同樣鍵盤(pán)也能完成等等。
posted @
2007-11-11 13:36 ruislan 閱讀(1427) |
評(píng)論 (5) |
編輯 收藏
Swing作為一個(gè)完整的UI解決方案,包含了一個(gè)GUI程序所擁有的方方面面,當(dāng)然包括作為普通程序也好,作為GUI程序也好,作為Web程序等等程序都共有的線程概念。
Swing中的線程有三種:初始線程,事件線程,工作線程
這三種線程基本上包括了讓一個(gè)GUI完美工作的方方面面,首先,初始線程被用來(lái)創(chuàng)建GUI組件、資源加載和啟動(dòng)GUI組件,眾所周知,Swing是事件驅(qū)動(dòng)的,所以當(dāng)UI出現(xiàn)了之后,初始線程就完成了它的使命,并將接力棒交給了事件線程,Event Dispatch Thread,這個(gè)時(shí)候所有組件的事件行為都交給了這個(gè)線程去處理,當(dāng)然我們自己也要需要用線程來(lái)運(yùn)行許多任務(wù),優(yōu)秀的GUI程序是絕不能讓界面被卡死不動(dòng)的,那會(huì)讓用戶崩潰,所以這個(gè)時(shí)候就需要工作線程了,也可以說(shuō)是在背后運(yùn)行的線程,這種線程是勞動(dòng)階級(jí),任勞任怨的執(zhí)行者長(zhǎng)時(shí)間的工作。
初始線程的寫(xiě)法很簡(jiǎn)單,這樣就可以了:
SwingUtilities.invokeLater(new Runnable() {
public void run() {
initGUI();
showGUI();
}
}
但是Applet中,你可能需要調(diào)用SwingUtilities.invokeAndWait這個(gè)方法,要是init方法返回了,瀏覽器開(kāi)始展現(xiàn)Applet,但是GUI的創(chuàng)建還在thread中,出錯(cuò)也是可想而知的。
至于invokeLater和invokeAndWait這兩個(gè)線程的簡(jiǎn)單點(diǎn)的區(qū)別就是invokeLater是異步的,你不知道它什么時(shí)候會(huì)開(kāi)始執(zhí)行,invokeAndWait則是同步的,它會(huì)等到動(dòng)作執(zhí)行完成之后才返回。
Event Dispatch Thread不是線程安全的,所以要用線程來(lái)與它打交道要注意了,同步問(wèn)題總是讓人頭痛。
在1.5之前應(yīng)該說(shuō)工作線程都是由開(kāi)發(fā)人員自己去定義的,但是現(xiàn)在Swing推薦了SwingWorker這個(gè)類,包括Swing最新的符合JSR標(biāo)準(zhǔn)的Swing AppFramework也使用了SwingWorker這個(gè)類來(lái)處理所有在GUI背后做的事情。
了解了Swing中的線程定義,能夠讓我們更好的寫(xiě)出優(yōu)美的基于Swing的GUI程序。
posted @
2007-11-04 12:40 ruislan 閱讀(1269) |
評(píng)論 (2) |
編輯 收藏
很多人都說(shuō)我們這行的人是偏執(zhí)狂,我也覺(jué)得我是有一點(diǎn)倔脾氣,就像看連續(xù)劇,從第一集開(kāi)始一直到最后一集才會(huì)關(guān)上電腦,一旦一個(gè)研究開(kāi)始,就一定要有一個(gè)結(jié)果或者令自己滿意的結(jié)果才結(jié)束,但是當(dāng)我看到這位“偏執(zhí)狂”之后,我覺(jué)得我只是稍微有一點(diǎn)偏執(zhí)而已。
研究UI繪制的時(shí)候很容易陷入另外一個(gè)領(lǐng)域,圖像領(lǐng)域,或者是游戲領(lǐng)域,我不喜歡做游戲是因?yàn)槲覑?ài)玩游戲,如果玩和工作綁在一起了的話,那么工作之后的休閑就也是工作了。
好了,說(shuō)了點(diǎn)廢話,下面是對(duì)FengGUI的介紹.
FengGUI是一個(gè)建立在OpenGL上的GUI的API,F(xiàn)engGUI提供了很多標(biāo)準(zhǔn)的UI組件,像Button,TextField,Panel之類的,下面先看看截圖:

GridLayout的截圖

可分割的面板
要說(shuō)到最大的特色,莫過(guò)于FengGUI基于OpenGL,并且可以在組件里面直接使用OpenGL,可以輕松的集成jME(java Monkey Engine,一個(gè)非常棒的Java 3D游戲引擎),jogl(Java OpenGL API),lwjgl(輕量級(jí)Java游戲庫(kù)),jPCT(同樣非常棒的Java 3D游戲引擎),但是在跑它的demo時(shí)我也感受到了CPU 80% 工作率的壓力,所以就目前的我膚淺的了解,用它來(lái)做普通的GUI程序估計(jì)還為時(shí)有點(diǎn)早,但是如果是游戲中的組件的話確實(shí)是與上述引擎和API的非常好的補(bǔ)充。
posted @
2007-10-27 20:34 ruislan 閱讀(1707) |
評(píng)論 (2) |
編輯 收藏
以前或許大家對(duì)一個(gè)UI組件是否透明沒(méi)有那么關(guān)心,但是自從Vista的毛玻璃出現(xiàn)后,UI透明就成了大家非常關(guān)注的一個(gè)話題,于是Java陣營(yíng)開(kāi)始了鋪天蓋地的討論如何實(shí)現(xiàn)透明的效果,但是很不幸的是無(wú)論組件如何透明和變換,但是能夠放置于屏幕任何位置的Window一族就是沒(méi)法透明和變形,原生代碼的問(wèn)題還是交給原生代碼來(lái)解決吧。
自己寫(xiě)原生代碼是可怕的,特別是對(duì)我這種只知道Java的平凡程序員,所以我們得借助一個(gè)非常方便的跨平臺(tái)的調(diào)用OS function方便的Lib,JNA便是最佳選擇(那個(gè)誰(shuí),這里不是討論JRuby&JPython的)。
so, all robots, transform!
下面我們要做一個(gè)界面是圓角四邊形的,中間有一個(gè)滑動(dòng)條來(lái)滑動(dòng)調(diào)節(jié)透明度。先看看做好的截圖。

注意圖中的JFrame邊角已經(jīng)變成了圓弧,滑動(dòng)滑塊,JFrame開(kāi)始透明,桌面的圖標(biāo)顯現(xiàn)出來(lái),下面是代碼。
/**
* @(#)TransparentFrame.java v0.1.0 2007-10-21
*/
package ruislan.rswing.test;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.RoundRectangle2D;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.border.LineBorder;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import com.sun.jna.examples.WindowUtils;
/**
* Transparent JFrame use JNA
*
* @author ruislan <a href="mailto:z17520@126.com"/>
* @version 0.1.0
*/
public class TransparentFrame {
private JFrame frame;
private JPanel container;
private JSlider slider;
private JPanel titleBar;
private JLabel titleLabel;
private JButton closeButton;
public static void main(String[] args) {
new TransparentFrame().launch();
}
private void launch() {
createUI();
launchUI();
}
protected void launchUI() {
frame.setVisible(true);
}
protected void createUI() {
System.setProperty("sun.java2d.noddraw", "true");
frame = new JFrame();
frame.setSize(200, 150);
frame.setAlwaysOnTop(true);
frame.setUndecorated(true);
container = new JPanel();
frame.setContentPane(container);
container.setLayout(new BorderLayout());
container.add(new JLabel("Ubunto vs Vista, I like both."),
BorderLayout.CENTER);
container.setBorder(new LineBorder(Color.BLACK));
titleBar = new JPanel();
titleBar.setLayout(new BorderLayout());
titleLabel = new JLabel("JNA is great!");
titleBar.add(titleLabel, BorderLayout.CENTER);
titleBar.setBorder(new LineBorder(Color.GRAY));
closeButton = new JButton("X");
closeButton.setFocusPainted(false);
closeButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.exit(0);
}
});
titleBar.add(closeButton, BorderLayout.EAST);
container.add(titleBar, BorderLayout.NORTH);
slider = new JSlider(0, 100);
slider.setValue(100);
slider.addChangeListener(new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
float value = slider.getValue();
WindowUtils.setWindowAlpha(frame, value * 0.01f);
}
});
container.add(slider, BorderLayout.SOUTH);
RoundRectangle2D.Float mask = new RoundRectangle2D.Float(0, 0, frame
.getWidth(), frame.getHeight(), 20, 20);
WindowUtils.setWindowMask(frame, mask);
centerWindow(frame);
}
public static void centerWindow(Container window) {
Dimension dim = Toolkit.getDefaultToolkit().getScreenSize();
int w = window.getSize().width;
int h = window.getSize().height;
int x = (dim.width - w) / 2;
int y = (dim.height - h) / 2;
window.setLocation(x, y);
}
}
利用JNA來(lái)制作透明效果非常簡(jiǎn)單,只需要調(diào)用
WindowUtils.setWindowAlpha(window, alpha)就可以了。當(dāng)然這是JNA特別為UI寫(xiě)的工具代碼。如果要改變形狀,用WindowUtils.setWindowMask(window, shape)或者WindowUtils.setWindowMask(window, icon)就可以了,但是要注意一點(diǎn)必須設(shè)置System.setProperty("sun.java2d.noddraw", "true"),否則JNA會(huì)告訴你這個(gè)程序不支持透明。當(dāng)然要運(yùn)行起來(lái),還得需要兩個(gè)Jar,jna.jar 和 examples.jar ,都不是很大,只有200多K。
雖然這篇文章只是介紹了一下JNA關(guān)于Swing的簡(jiǎn)單用法,但是有了這個(gè)我最先到的便是可以做類似于Yahoo Widget和Google Widget之類的東西了,還可以做好看的fishEye,SideBar,JNA的JAR兩個(gè)合起來(lái)不過(guò)400K,卻能讓這么多復(fù)雜的事情簡(jiǎn)單化,真是精湛的藝術(shù)啊,嗯。
posted @
2007-10-21 13:43 ruislan 閱讀(4385) |
評(píng)論 (12) |
編輯 收藏
很久沒(méi)有上來(lái)更新了,因?yàn)橐恍┰?。但是,我又回?lái)了,套用MASK的話,小子們,想我嗎?
回來(lái)了先報(bào)告一個(gè)Blogjava的BUG,就是用Opera9瀏覽器寫(xiě)文章時(shí)不能用鼠標(biāo)點(diǎn)擊編輯區(qū),還好用TAB可以切換過(guò)來(lái),然后就是用Google輸入法的話,標(biāo)點(diǎn)符號(hào)和數(shù)字輸入一次,編輯區(qū)會(huì)出來(lái)兩個(gè),最后就是編輯完之后好像內(nèi)容也保存不了,所以我又換回FF來(lái)編輯了。
回來(lái)了卻不知從何開(kāi)始了,組件的改造也算拋磚引玉了(當(dāng)然如果你們喜歡,我還可以繼續(xù)改造),冰封的程序要繼續(xù)改造的話就要開(kāi)一個(gè)工程了大家集體參與了,所以那就還是先從上次哪位仁兄提到的組件透明的問(wèn)題,從那個(gè)開(kāi)始吧!show time。
posted @
2007-10-20 16:30 ruislan 閱讀(433) |
評(píng)論 (1) |
編輯 收藏
昨天我們改進(jìn)了選擇區(qū),今天我們來(lái)繼續(xù)為選擇區(qū)選定之后添加一個(gè)操作框
下面是改進(jìn)后的截圖:

正常的情況,右下角出現(xiàn)了一個(gè)操作框

在三個(gè)(左、右、底)邊際的情況,我們重新計(jì)算了位置。
三個(gè)按鈕還沒(méi)有功能,也不是圖片,但是我們離QQ的截屏程序又進(jìn)一步了,嗯!
posted @
2007-09-14 18:48 ruislan 閱讀(1160) |
評(píng)論 (10) |
編輯 收藏
“千里冰封” 兄弟的截屏程序酷斃了,但是好像9月4日之后就沒(méi)有繼續(xù)更新了,我們來(lái)繼續(xù)為他的程序改進(jìn),順便也把我們這幾天都在講的2D繪制用進(jìn)來(lái),我們的目標(biāo)是讓冰封的截屏程序成為截屏程序里的王!
今天先改進(jìn)一下截圖時(shí)候的選框,還是先放上截圖的截圖(*o*):

這是原來(lái)的圖片,下面是改進(jìn)后的

和改進(jìn)的代碼部分:
這部分代碼插入 Temp類的paintComponent方法中的 if (showTip) 這句的前面
Graphics2D g2d = (Graphics2D) g.create();
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setComposite(AlphaComposite.getInstance(
AlphaComposite.SRC_OVER, 0.3F));
g2d.setColor(Color.RED.brighter().brighter());
int sX = Math.min(startX, endX);
int sY = Math.min(endY, startY);
g2d.fillRect(sX, sY, Math.abs(endX - startX), Math.abs(endY
- startY));
g2d.setComposite(AlphaComposite.getInstance(
AlphaComposite.SRC_OVER, 1F));
boolean drawCTip = endX - startX != 0 && endY - startY != 0;
if (drawCTip) {
String cTip = String.format("%dX%d", Math.abs(endX - startX),
Math.abs(endY - startY));
int cTipH = 20;
Font cTipFont = new Font("system", Font.BOLD, 16);
g2d.setFont(cTipFont);
int cTipW = SwingUtilities.computeStringWidth(
getFontMetrics(cTipFont), cTip);
g2d.setPaint(Color.BLACK);
int cStartY = sY - cTipH > 0 ? sY - cTipH : sY;
g2d.fillRect(sX, cStartY, cTipW, cTipH);
g2d.setPaint(Color.WHITE);
g2d.drawString(cTip, sX, cStartY == sY ? sY + cTipH - 3
: sY - 3);
}
g2d.dispose();
怎么樣,比起QQ的截圖程序,我們又近一步了,嗯。
posted @
2007-09-13 13:28 ruislan 閱讀(1488) |
評(píng)論 (10) |
編輯 收藏
摘要: 嗯,BeanSoft的話很有道理,令我敬佩,也許是昨天在下對(duì)那個(gè)“更好的”三個(gè)字感到一時(shí)憤慨,所以看到UI就自己擴(kuò)大了問(wèn)題,想到迎合LookAndFeel上面去了,在此說(shuō)句對(duì)不起了。你的回帖里面偏重于從整個(gè)組件的設(shè)計(jì)和重用性上,我的文章主要講的是如何將2D繪制和組件的繪制結(jié)合起來(lái),看客如果既了解了如何繪制自己想要的組件,又能設(shè)計(jì)得體,重用性高的話也算是對(duì)我拋磚引玉的欣慰了。...
閱讀全文
posted @
2007-09-12 13:36 ruislan 閱讀(2440) |
評(píng)論 (11) |
編輯 收藏
看來(lái)我們的JTextField之旅也到了一個(gè)階段,已經(jīng)很不錯(cuò)了,現(xiàn)在我們來(lái)改造JButton,讓那個(gè)呆板的Swing看起來(lái)舒服一些。
還是先放上完成后的效果圖:

普通的狀態(tài)

鼠標(biāo)滑過(guò)

鼠標(biāo)按下
和代碼:
1 /**
2 * @(#)RJButton.java 0.1.0 2007-9-11
3 */
4 package ruislan.rswing;
5
6 import java.awt.AlphaComposite;
7 import java.awt.Color;
8 import java.awt.Font;
9 import java.awt.GradientPaint;
10 import java.awt.Graphics;
11 import java.awt.Graphics2D;
12 import java.awt.RenderingHints;
13 import java.awt.Shape;
14 import java.awt.event.MouseAdapter;
15 import java.awt.event.MouseEvent;
16 import java.awt.geom.RoundRectangle2D;
17
18 import javax.swing.JButton;
19
20 /**
21 * Custom JButton
22 *
23 * @version 0.1.0
24 * @author ruislan <a href="mailto:z17520@126.com"/>
25 */
26 public class RButton extends JButton {
27 private static final long serialVersionUID = 39082560987930759L;
28 public static final Color BUTTON_COLOR1 = new Color(205, 255, 205);
29 public static final Color BUTTON_COLOR2 = new Color(51, 154, 47);
30 // public static final Color BUTTON_COLOR1 = new Color(125, 161, 237);
31 // public static final Color BUTTON_COLOR2 = new Color(91, 118, 173);
32 public static final Color BUTTON_FOREGROUND_COLOR = Color.WHITE;
33 private boolean hover;
34
35 public RButton() {
36 setFont(new Font("system", Font.PLAIN, 12));
37 setBorderPainted(false);
38 setForeground(BUTTON_COLOR2);
39 setFocusPainted(false);
40 setContentAreaFilled(false);
41 addMouseListener(new MouseAdapter() {
42 @Override
43 public void mouseEntered(MouseEvent e) {
44 setForeground(BUTTON_FOREGROUND_COLOR);
45 hover = true;
46 repaint();
47 }
48
49 @Override
50 public void mouseExited(MouseEvent e) {
51 setForeground(BUTTON_COLOR2);
52 hover = false;
53 repaint();
54 }
55 });
56 }
57
58 @Override
59 protected void paintComponent(Graphics g) {
60 Graphics2D g2d = (Graphics2D) g.create();
61 int h = getHeight();
62 int w = getWidth();
63 float tran = 1F;
64 if (!hover) {
65 tran = 0.3F;
66 }
67
68 g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
69 RenderingHints.VALUE_ANTIALIAS_ON);
70 GradientPaint p1;
71 GradientPaint p2;
72 if (getModel().isPressed()) {
73 p1 = new GradientPaint(0, 0, new Color(0, 0, 0), 0, h - 1,
74 new Color(100, 100, 100));
75 p2 = new GradientPaint(0, 1, new Color(0, 0, 0, 50), 0, h - 3,
76 new Color(255, 255, 255, 100));
77 } else {
78 p1 = new GradientPaint(0, 0, new Color(100, 100, 100), 0, h - 1,
79 new Color(0, 0, 0));
80 p2 = new GradientPaint(0, 1, new Color(255, 255, 255, 100), 0,
81 h - 3, new Color(0, 0, 0, 50));
82 }
83 g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
84 tran));
85 RoundRectangle2D.Float r2d = new RoundRectangle2D.Float(0, 0, w - 1,
86 h - 1, 20, 20);
87 Shape clip = g2d.getClip();
88 g2d.clip(r2d);
89 GradientPaint gp = new GradientPaint(0.0F, 0.0F, BUTTON_COLOR1, 0.0F,
90 h, BUTTON_COLOR2, true);
91 g2d.setPaint(gp);
92 g2d.fillRect(0, 0, w, h);
93 g2d.setClip(clip);
94 g2d.setPaint(p1);
95 g2d.drawRoundRect(0, 0, w - 1, h - 1, 20, 20);
96 g2d.setPaint(p2);
97 g2d.drawRoundRect(1, 1, w - 3, h - 3, 18, 18);
98 g2d.dispose();
99 super.paintComponent(g);
100 }
101 }
102
注意代碼中的幾個(gè)部分:
首先是paintComponent方法中最后一行,我們調(diào)用了父類的paintComponent方法,這是因?yàn)槲覀円扛割悂?lái)繪制字符,但是父類的這個(gè)方法除了繪制字符之外還會(huì)繪制其他的,所以我們需要關(guān)閉掉其他的(當(dāng)然我們也可以自己來(lái)繪制字符,但是JButton提供了方法為什么不用呢),所以我們?cè)跇?gòu)造方法那里調(diào)用了:
setBorderPainted(false);
setFocusPainted(false);
setContentAreaFilled(false);
告訴父類不用繪制邊框,不用繪制焦點(diǎn),不用繪制內(nèi)容部分,這部分我們自己來(lái)搞*o*。
然后就是這一句了g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)告訴繪制API我們需要平滑一點(diǎn),否則繪制出來(lái)會(huì)有很多鋸齒喲。
接下來(lái)的這一句g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,tran))告訴繪圖API我們需要繪制一個(gè)有透明度的,tran就是透明度(0-1)。
然后就是將邊框的邊角變直角為圓角,我們繪制一個(gè)RoundRectangle2D,這個(gè)就是邊角都為圓角的方形,然后我們根據(jù)這個(gè)方形來(lái)clip我們的方形,這樣方形就被RoundRectangle2D的圓角方形包裹,從而變成了圓角方形。
最后就是繪制外邊線和內(nèi)邊線,通過(guò)改變內(nèi)邊線和外邊線的色變從而造成陷入或者突出效果。
整個(gè)JButton改造完畢,如果你能夠活用clip的話,你也可以做一個(gè)五角星的JButton喲。
整個(gè)源代碼我連同Eclipse工程文件已經(jīng)打包成zip放在我的文件里面,下載鏈接如下:
http://www.tkk7.com/Files/ruislan/rswing-0.1.0.zip
posted @
2007-09-11 12:24 ruislan 閱讀(5553) |
評(píng)論 (20) |
編輯 收藏
昨天我們給JTextField增加了一個(gè)泡泡提示窗口,今天我們繼續(xù)昨天的,首先處理在顯示泡泡的時(shí)候忽略輸入的Backspace、Enter、Delete、Esc按鍵,然后加上錯(cuò)誤的時(shí)候的聲音提示,最后再給JTextField換裝備,讓它看起來(lái)像MSN 8.5beta的輸入框,還是先放上圖片:

這幅圖是MSN的輸入框,輸入框的內(nèi)部到光標(biāo)有一部分是有點(diǎn)毛玻璃的感覺(jué),不過(guò)這個(gè)style是死的,我們改進(jìn)一下,我們的JTextField在輸入之前是看似普通的,在鼠標(biāo)放上去之后,看起來(lái)就與MSN的輸入框類似了,而且我們還給這個(gè)輸入框加入一個(gè)淡黃色的背景。
好了,現(xiàn)在很明確要做的事情了
1. 加入鼠標(biāo)事件監(jiān)聽(tīng)器,監(jiān)聽(tīng)MouseEnter和MouseExit事件,根據(jù)這個(gè)兩個(gè)事件設(shè)置不同的背景色和邊框
2. 做一個(gè)能夠顯示毛玻璃效果的邊框
以下是邊框的代碼和部分MyJTextField的代碼,完整的代碼待我會(huì)打包傳上來(lái)的
MyJTextField.java
在初始化組件的時(shí)候加上Border的初始化:
hoverBorder = new CoolBorder(HOVER_BORDER_COLOR, 3);
border = BorderFactory.createCompoundBorder(new LineBorder(
BORDER_COLOR, 1), new EmptyBorder(new Insets(2, 2, 2, 2)));
setBackground(BACKGROUND_COLOR);
setBorder(border);
和事件的初始化:
addMouseListener(new MouseAdapter() {
@Override
public void mouseEntered(MouseEvent e) {
setBorder(hoverBorder);
setBackground(HOVER_BACKGROUND_COLOR);
repaint();
}
@Override
public void mouseExited(MouseEvent e) {
setBorder(border);
setBackground(BACKGROUND_COLOR);
repaint();
}
});
以及屏蔽功能性按鈕和發(fā)出聲音提示的代碼:
addKeyListener(new KeyAdapter() {
@Override
public void keyTyped(KeyEvent e) {
char input = e.getKeyChar();
// ESC27 ,Backspace 8 ,Enter 10, Del 127, must ignore
boolean ignoreInput = input == (char) KeyEvent.VK_ESCAPE
|| input == (char) KeyEvent.VK_BACK_SPACE
|| input == (char) KeyEvent.VK_ENTER
|| input == (char) KeyEvent.VK_DELETE;
if (ignoreInput) {
limitTip.setVisible(false);
numberTip.setVisible(false);
return;
}
if (getText().length() + 1 > limit) {
Toolkit.getDefaultToolkit().beep();
deleteInputChar(e);
limitTip.setVisible(true);
return;
} else {
limitTip.setVisible(false);
}
if (numberOnly) {
if (!Character.isDigit(input)) {
numberTip.setVisible(true);
Toolkit.getDefaultToolkit().beep();
deleteInputChar(e);
} else {
numberTip.setVisible(false);
}
}
}
private void deleteInputChar(KeyEvent source) {
source.setKeyChar((char) KeyEvent.VK_CLEAR);
}
});
下面是Border的完整代碼:
1 /**
2 * @(#)CoolBorder.java 0.1.2 2007-9-10
3 */
4 package ruislan;
5
6 import java.awt.Color;
7 import java.awt.Component;
8 import java.awt.Dimension;
9 import java.awt.GradientPaint;
10 import java.awt.Graphics;
11 import java.awt.Graphics2D;
12 import java.awt.Insets;
13
14 import javax.swing.border.Border;
15
16 /**
17 * Custom Border.
18 *
19 * @version 0.1.2, 2007-9-10
20 * @author ruislan <a href="mailto:z17520@126.com"/>
21 */
22 public class CoolBorder implements Border {
23 private int thickness;
24 private Insets insets;
25 private Dimension lastComponentSize;
26 private Color color;
27 private Color color2;
28
29 public CoolBorder(Color color, int thickness) {
30 this.color = color;
31 if (color == null) {
32 this.color = color = Color.gray;
33 }
34 color2 = new Color(210, 210, 210, 0);
35 this.thickness = thickness;
36 }
37
38 @Override
39 public Insets getBorderInsets(Component c) {
40 Dimension currentComponent = c.getSize();
41
42 if (currentComponent.equals(lastComponentSize)) {
43 return insets;
44 }
45
46 insets = new Insets(thickness, thickness, thickness, thickness);
47 lastComponentSize = currentComponent;
48 return insets;
49 }
50
51 @Override
52 public boolean isBorderOpaque() {
53 return true;
54 }
55
56 @Override
57 public void paintBorder(Component c, Graphics g, int x, int y, int width,
58 int height) {
59 Graphics2D g2d = (Graphics2D) g.create();
60 // 畫(huà)上邊緣
61 GradientPaint gp = new GradientPaint(x, y, color, x, y + thickness,
62 color2);
63 g2d.setPaint(gp);
64 g2d.fillRect(x, y, width, thickness);
65 // 畫(huà)下邊緣
66 gp = new GradientPaint(x, y + height - thickness - 1, color2, x, y
67 + height, color);
68 g2d.setPaint(gp);
69 g2d.fillRect(x, y + height - thickness - 1, width, thickness);
70 // 畫(huà)左邊緣
71 gp = new GradientPaint(x, y, color, x + thickness, y, color2);
72 g2d.setPaint(gp);
73 g2d.fillRect(x, y, thickness, height);
74 // 畫(huà)右邊緣
75 gp = new GradientPaint(x + width - thickness - 1, y, color2, x + width,
76 y, color);
77 g2d.setPaint(gp);
78 g2d.fillRect(x + width - thickness - 1, y, thickness, height);
79 // 畫(huà)外框
80 g2d.setPaint(color);
81 g2d.drawRect(x, y, width - 1, height - 1);
82 g2d.dispose();
83 }
84
85 }
86
然后來(lái)欣賞我們的結(jié)果吧:

鼠標(biāo)放上去之前

鼠標(biāo)放上去之后

我們輸入了不是數(shù)字的字符
至此,我們的JTextField又向前走了一步,下次我們還能如何改進(jìn)呢?把JTextField這個(gè)死板的長(zhǎng)方形改造成云狀的或者其他形狀的?
附源代碼下載地址:http://www.tkk7.com/Files/ruislan/myjtextfield.zip
posted @
2007-09-10 13:21 ruislan 閱讀(4347) |
評(píng)論 (3) |
編輯 收藏
摘要: 接著昨天的MyJTextField,我們繼續(xù)為JTextField增強(qiáng),今天我們?yōu)镸yJTextField增加一個(gè)泡泡提示。先看圖片:
當(dāng)輸入第三個(gè)字符'a'時(shí),由于昨天我們的MyJTextField做了處理,所以'a'不能被輸入,而且彈出泡泡提示你下次不要了喲!
同樣的,下一張圖片:
為MyJTextField增加泡泡提示功能要做以下幾件事情:
1. 泡泡窗口
2. 顯示的...
閱讀全文
posted @
2007-09-09 13:32 ruislan 閱讀(5179) |
評(píng)論 (2) |
編輯 收藏
在網(wǎng)上Google了一下,基本上的做法有兩種,第一種是JFormattedTextField;另外一種是自己繼承PlainDocument,
Override
insertString方法,然后用JTextFiled.setDocument的方法放入自己繼承的對(duì)象實(shí)例。但是最終我都沒(méi)有采用這兩種方法,首
先我對(duì)JFormattedTextField的方式不太舒服,然后感覺(jué)繼承PlainDocument有點(diǎn)太重,所以自己考慮了一種方案,也許有人跟我
的想法雷同,那就純屬巧合了。
下面放上代碼:
public class MyJTextField extends JTextField {
private int limit = Integer.MAX_VALUE;
private boolean numberOnly;
public MyJTextField() {
addKeyListener(new KeyAdapter() {
@Override
public void keyTyped(KeyEvent e) {
if (getText().length() + 1 > limit) {
deleteInputChar(e);
return;
}
if (numberOnly) {
char input = e.getKeyChar();
if (!Character.isDigit(input)) {
deleteInputChar(e);
}
}
}
private void deleteInputChar(KeyEvent source) {
source.setKeyChar((char) KeyEvent.VK_CLEAR);
}
});
}
public void setMaxTextLength(int limit) {
if (limit < 0) {
return;
}
this.limit = limit;
}
public int getMaxTextLength() {
return limit;
}
public void setNumberOnly(boolean numberOnly) {
this.numberOnly = numberOnly;
}
public boolean isNumberOnly() {
return this.numberOnly;
}
}
整個(gè)思路很簡(jiǎn)單,就是檢查T(mén)ype事件,如果超出了限制就刪除新增加的字符,如果設(shè)置了只是數(shù)字,那么不是數(shù)字的就刪除新增加的字符。使用的時(shí)候只需要
MyJTextField textField = new MyJTextField();
textField.setLimit(8);
textField.setNumberOnly(true);
結(jié)果就是最多輸入8個(gè)字符而且只能是數(shù)字。
posted @
2007-09-09 00:41 ruislan 閱讀(1285) |
評(píng)論 (2) |
編輯 收藏
其實(shí)認(rèn)識(shí)這里也有一段時(shí)間了,平常也常常上來(lái)看看,也用someone的給一些博主有些留言。但是因?yàn)樽约河辛薭logspot,所以始終沒(méi)有注冊(cè)??上У氖莃logspot被封太久了,我也懶得去改什么東西來(lái)破解,所以遺憾的放棄了blogspot,當(dāng)然這里也并非我退而其次的選擇。今天在這里注冊(cè)了就算正式與blogspot告別了。blogspot上的文章也不打算轉(zhuǎn)過(guò)來(lái)了。從頭開(kāi)始寫(xiě)吧。
posted @
2007-09-08 13:59 ruislan 閱讀(207) |
評(píng)論 (1) |
編輯 收藏