下載 項目源代碼
在本系列的第一篇文章中,我們構建了一個簡單的可在任何支持 TCP/ip 插槽的 MIDP 設備上運行的終端模擬器。它包含一個實現 Telnet 協議的 Connection
和一個經過定制來顯示終端內容的 Canvas
。在第二篇文章中,我假定您已經閱讀了第一篇文章并熟悉了這些組件。
現在我們將進一步加強這個應用程序。首先,我們將通過添加一個更高級的終端類型來增加一些復雜性。然后我們將添加對用戶輸入的支持——注意大多數移動設備的各種限制。完成后,我們就能夠使用這個應用程序來通過 Telnet 連接到遠程服務器并運行許多種類的程序,而這一切都是從您的 MIDP 設備來進行的。
在上一篇文章中我們實現了一個“啞巴”終端。它顯示的僅僅是一個字符流,當進入的字符到達屏幕的邊緣時就換行到下一行,在遇到一個換行符時就跳到下一行。雖然這種程度的交互性對于整代的命令行應用程序是足夠的,但較復雜的軟件則將屏幕作為一個整體來處理,這就需要編寫和清除特定位置的字符以提供更好的用戶體驗。
這里有一段有趣的歷史。在 20 世紀 70 年代,眾多生產視頻終端的廠商提供的屏幕操縱功能專有且各不相同,并以不兼容的方式來實現。要編寫可利用多種這樣的設備的面向屏幕的軟件就很困難。
American National Standards Institute (ANSI) 參與進來。堅持其通過可互操作性來支持商業的要求,ANSI 介入并發布了標準 X3.64:Additional Controls for Use with American National Standard Code for Information Interchange。這個文件定義了現在所知的 ANSI 終端類型。它對將光標移動到屏幕上的特定位置、在光標位置插入和刪除字符都規定了標準的命令序列。
最為重要的是命令序列定義自身,因為即使是未實現所有命令的終端也至少能夠將不支持的命令識別為命令,并安全地忽視它們。這一進步使得軟件開發商可以按照通用標準來編寫,并確信他們的應用程序能夠至少占用較小的容量來在很多廠商的設備上運行。ANSI 的故事是軟件行業一個重復出現的主題的一個很好的例子:采用一種標準會極大地擴展可互操作性。
ANSI 終端標準是一種很像 Telnet 協議的協議。它定義了多個特殊的字符序列,使一個應用程序可區分要解釋的命令和要顯示到屏幕的數據。
ANSI 終端是我們將要模擬的終端類型。TelnetConnection
已經在其到達屏幕之前過濾出 Telnet 握手和協商;現在我們將增加另一個過濾器來過濾出并解釋 ANSI 命令。因為這些命令是我們的終端的指令,實現該邏輯的最好的地方就是在 TelnetCanvas
類自身中。
當 Telnet 使用字節值 255 來以信號方式表示一個命令序列時,ANSI 使用 ASCII 轉義序列,其值是 27。ANSI 將如下工作:
我們從輸入讀取一個字節。如果其值不是 27 (
ESC
),則它不是一個命令∶我們將它直接送到應用程序并繼續讀取。我們讀取下一個字節。如果其值不是 133 (
[
),則它不是一個命令;我們將該字節后所跟的 27 直接送到應用程序并繼續讀取。我們將繼續讀取,直到讀取到大于 63 的一個字節。這些字節形成一個字符串,其中含有命令的參數。最后一個字節(是 64 或更大)是命令代碼。我們處理(或忽略)該命令并繼續讀取。
許多命令從官方來講是標準的一部分。雖然完全模擬是一個值得的目標,我們仍將精力放在獲得足夠的功能來使眾多軟件可以接受地運行。我們將實現以下命令:
Cursor Control SequencesErase SequencesA
Move cursor up n lines@
Insert n blank spacesB
Move cursor down n linesJ
Erase display: after cursor (n=0), before cursor (n=1), or entirely (n=2).C
Move cursor forward n spacesD
Move cursor backward n spacesK
Erase line: after cursor (n=0), before cursor (n=1), or entirely (n=2).G
Move cursor to column xH
Move cursor to column x, row yL
Insert n new blank linesd
Move cursor to row yM
Delete n lines from cursors
Save current cursor positionP
Delete n characters from cursoru
Return to saved cursor position
大多數這些命令期望參數采用分號分割字符串的形式。參數出現在序列 ESC [
后和命令字節前。例如,一條用于將光標移動到第 10 行和第 10 列的命令將如下所示:ESC [ 1 0 ; 1 0 H。
許多命令只帶一個參數;例如,ESC [ 2 J
將清除屏幕顯示。漏掉的參數將默認為 1,所以 ESC [ H
將把光標返回到坐標 (1,1),而 ESC [ B
則將光標移動到下一行。
雖讓有很多的東西要處理,但協議卻相當簡單。
我們需要在兩個領域中更新 TelnetCanvas。
它需要解釋從遠程主機接收的 ANSI 命令并發送 ANSI 命令來響應用戶輸入。對于輸入,我們將允許用戶使用設備上的鍵盤來移動光標。
雖然我們必須修改 TelnetCanvas
類的內部,但卻沒有必要更改公共接口。所有數據將仍由 receive()
方法來接收。我們只更改其實現來監視轉義序列:
/*** Appends the specified ascii byte to the output.*/public void receive( byte b ){ // ignore nulls if ( b == 0 ) return; if ( state == PARAMS_STATE ) { // if still receiving parameters if ( b < 64 ) { argbuf[0]++; // grow if needed if ( argbuf[0] == argbuf.length ) { char[] tmp = new char[ argbuf.length * 2 ]; System.arraycopy( argbuf, 0, tmp, 0, argbuf.length ); argbuf = tmp; } argbuf[ argbuf[0] ] = (char) b; } else // final byte: PRocess the command { processCommand( b ); // reset for next command argbuf[0] = 0; state = NORMAL_STATE; } } else if ( state == ESCAPE_STATE ) { // if a valid escape sequence if ( b == '[' ) { state = PARAMS_STATE; } else // not an escape sequence { // allow escape to pass through state = NORMAL_STATE; processData( (byte) 27 ); processData( b ); } } else // NORMAL_STATE { if ( b == 27 ) { state = ESCAPE_STATE; } else { processData( b ); } }}
該方法實現了一個具有三個狀態的簡單狀態機。NORMAL_STATE
監視任何 ESC
字節并將其他任何東西發送到 processData()。
當一個 ESC
字節來到時,ESCAPE_STATE
會接管并檢查下一個字節是否是 133 ([
)。如果是,我們就轉到 PARAMS_STATE
,同時累加參數字符串直到我們碰到此命令字符。我們在執行該操作時,調用 processCommand()
,然后轉回 NORMAL_STATE
。
讀取命令參數的代碼值得進行檢查。為了避免創建和保存 StringBuffer
所帶來的系統開銷,我們使用一個稱為 argbuf
的字符數組。為了避免不斷的內存重新分配,我們使其比所需的大一些并保持富余,同時根據需要擴大它。最后,為了跟蹤下一個字符到達何處,我們借用 Pascal 的一個技巧,將參數字符串的長度存儲在數組的第一個元素中。getArgument()
和 getArgumentCount()
參數處理來自該數組的單個參數的分析和提取。
我們現在將曾經位于 receive()
方法中的代碼轉到 processData()
方法。邏輯是一樣的,將進入的字節放在當前的光標位置,除了接近方法主體末尾的這兩行外,代碼沒有變化:
/*** Appends the specified byte to the display buffer.*/protected void processData( byte b ){ ... // increment bound if necessary while ( cursor > bound ) bound += columns; ...}
采用較早版本的 MIDTerm 的簡單的面向流的方法時,光標不僅標出了進入數據的插入點,同時標出了應該在屏幕上顯示的數據緩沖區的外界。由于光標現在可以向上和向前移動到數據緩沖區,就需要一個額外的變量來跟蹤數據的外界,以便我們可以確定屏幕的底部在什么位置。這個變量稱為 bound
,必須跟蹤光標并在光標移動時位于它前面。
雖然 processData()
處理了大部分字節,但在接收到合法的終端命令時還是要調用 processCommand()
。這個方法是我們的 ANSI 實現的核心。
/*** Executes the specified ANSI command, oBTaining arguments* as needed from the getArgument() and getArgumentCount() * methods.*/protected void processCommand( byte command ){ try { switch ( command ) { ... // other commands go here case 'd': // cursor to row x if ( argbuf[0] > 0 ) { cursor = bound - ((rows-getArgument( 0 )+1)*columns) + ( cursor % columns ); } break; case 'G': // cursor to column x if ( argbuf[0] > 0 ) { cursor = cursor - ( cursor % columns ) + getArgument( 0 ); } break; ... // other commands go here default: System.err.println( "unsupported command: " + (char) command + " : " + new String( argbuf, 1, argbuf[0] ) ); } } catch ( Throwable t ) { // probably parse exception or wrong number of args System.err.println( "Error in processCommand: " ); t.printStackTrace(); }}
processCommand()
其實是一個大的 switch 語句,為所支持的每條命令都有一個 case。不支持的命令進入默認 case 并被忽略。這里所列出的兩個 case 示范了處理命令的剩余部分的邏輯。
這里您可以了解操縱鼠標所需的數組算法的種類了。記住,我們的二維屏幕是由一維字節數組來表示的,而且我們在內存用盡之前將不會丟棄滾出屏幕頂端的數據。我們盡可能多地保留,以便用戶可以滾動回來來查看他們可能漏看的任何內容。由于這個原因,屏幕的原點必須相對于數組的末尾而不是開頭來計算,所以原點的位置在第 1 行、第 1 列。
如果我們不支持 scrollback 特性,要計算一對坐標的數組下標就很簡單:y * columns + x
。要支持這一特性,我們就需要多做一些工作,并必須相對于我們的顯示緩沖區的外界來計算坐標。如果這會使我們的用戶滿意,做這些額外的工作是值得的。
為了使用戶更滿意而又不需要太多工作,可使他們能夠使用他們設備上的鍵盤來與遠程主機交互。
我們較早的應用程序與遠程主機交互的方式,只能是通過執行一組腳本形式的命令。用戶只能等待操作完成,然后使用箭頭鍵來滾動數據緩沖區來查看返回的內容。
在下一版本的 MIDTerm 中,我們將用戶的鍵擊信號直接發送到遠程主機。要發送任何東西,我們都需要一個輸出流,所以 TelnetCanvas
現在有一個 setOutputStream()
方法用于此目的。用戶輸入通過如下修改 keyPressed()
方法來處理:
...private byte[] move = new byte[] { 27, (byte) '[', 0 };...public void keyPressed( int keyCode ){ switch ( getGameAction( keyCode ) ) { case LEFT: // move cursor left one column move[2] = 'D'; send( move ); break; case RIGHT: // move cursor right one column move[2] = 'C'; send( move ); break; case DOWN: if ( isScrolling() ) { // scroll down one row scrollY++; if ( scrollY > calcLastVisibleScreen() ) { scrollY = calcLastVisibleScreen(); } repaint(); } else { // move cursor down one row move[2] = 'B'; send( move ); } break; case UP: if ( isScrolling() ) { // scroll up one row scrollY--; if ( scrollY < 0 ) scrollY = 0; repaint(); } else { // move cursor down one row move[2] = 'A'; send( move ); } break; case FIRE: // send a line feed: send( (byte) '/n' ); break; default: // send code directly send( (byte) keyCode ); }}
要注意的第一件事情是,在決定按哪個鍵之前我們要使用 getGameAction()
來將按鍵代碼轉換為游戲代碼。MIDP 設備的鍵盤布局不同:有些有箭頭按鍵和數字小鍵盤,有些只有數字小鍵盤可用作箭頭按鍵。getGameAction()
方法隱藏了這些復雜性。
如果所按的鍵是 UP、DOWN、LEFT 或
RIGHT,
我們生成相應的 ANSI 命令并將其發送到遠程主機。注意 UP
和 DOWN
有兩個模式:一個用于移動光標,一個用于滾動顯示內容。當前的滾動模式由兩種新的方法 isScrolling()
和 setScrolling()
來發現和控制。如果打開滾動,UP
和 DOWN
將滾動輸出而不是移動光標。注意,通過直接測試 scrolling
變量而不是調用 isScrolling()
來測試,我們可以節省一些系統開銷。
鍵發送一個換行符,它類似于一般鍵盤上的 Enter 或 Return 鍵。這種特性對于基于菜單的應用程序很有用,由箭頭鍵突出顯示一個選項并用 Enter 鍵選擇突出顯示的選項;Lynx web 瀏覽器就是一個很好的例子。這些應用程序僅使用用戶手持設備上的鍵盤就可以完全發揮作用。FIRE
如果按鍵代碼沒有映射到任何游戲動作,就會將它直接發送到遠程主機。MIDP 規范中規定,具有比標準手機的按鍵多的設備應發送等同的 ASCII 字符作為其按鍵代碼。直接發送這些鍵代碼使得應用程序可以完全利用具有完整鍵盤的設備的優勢。具有這些設備的用戶只需開始輸入即可;他們的鍵擊會通過要建立的連接而發送。
最后,因為 TelnetCanvas
組件隱藏了來自應用程序的其他部分的所有終端模擬邏輯,我們需要新的方法 getRows()
、getColumns()
和 getTerminalType()
來宣布屏幕尺寸和實現所支持的終端模擬的種類。
與以前一樣,MIDlet 類自身 MIDTerm
將 TelnetConnection
和 TelnetCanvas
捆綁在一起。只需要首位改變一下連接的設置:
...connection = new TelnetConnection( (StreamConnection) Connector.open( connectString, Connector.READ_WRITE, true ), canvas.getColumns(), canvas.getRows(), canvas.getTerminalType() );input = connection.openInputStream();output = connection.openOutputStream();canvas.setOutputStream( output );...
但 MIDTerm 需要做更多。雖然 TelnetCanvas
處理基本用戶輸入,僅有數字鍵的鍵盤則需要特殊的方式來輸入文字,如重復按某個鍵來選擇某個字母,或預測式文字輸入法。利用這些內在功能的唯一途徑就是使用 MIDP 的 TextField
和 TextBox
組件。因為這些組件不能與 Canvas
位于相同的屏幕,文字輸入需要另外一個屏幕。雖然我們在屏幕上,但應用程序還應提供另一個屏幕來建立連接,這要用到主機名稱和要在其上連接的端口的字段。MIDTerm 將用作中心代理來將這些屏幕捆綁在一起。
Form
類非常靈活,所以我們不需要每個類都有一個單獨的子類,而這一點很讓人高興。大多數 MIDP 設備用于應用程序存儲的空間都有限,并限制應用程序的大小,通常是最大 32 KB 或 64 KB,所以大小問題很重要。應用程序中的每個類都會至少將 JAR 文件的大小增加半個千字節,即使在模糊處理后仍是如此,所以應盡可能地避免創建子類。
輸入窗體會經常用到,所以將它創建在 MIDTerm 的構造函數中,并在應用程序運行期間保持待用狀態。這種方法消除了創建輸入窗體帶來的任何可感知的延遲,并避免為窗體進行重復內存分配和垃圾收集而引起的內存擾動(memory churn)。輸入窗體的設置非常簡單:
...inputForm = new Form( "Input" );inputField = new TextField( null, "", 255, TextField.ANY );inputForm.append( inputField );inputOptions = new ChoiceGroup( null, Choice.MULTIPLE );inputOptions.append( INPUT_ENTER, null );inputOptions.append( INPUT_CTRL, null );inputOptions.append( INPUT_ESC, null );inputOptions.setSelectedIndex( 0, true ); // default trueinputForm.append( inputOptions );scrollOptions = new ChoiceGroup( null, Choice.MULTIPLE );scrollOptions.append( INPUT_SCROLL, null );inputForm.append( scrollOptions ); inputForm.addCommand( okCommand );inputForm.addCommand( cancelCommand );inputForm.setCommandListener( this );...
TelnetCanvas
會在用戶希望發送一些文字時激活該窗體。第一個字段是 TextField
,而且在大部分平臺上它都應獲得默認焦點。稍微費點功夫,用戶就可以快速調用該窗體,輸入一些文字,然后選擇 OK 命令來發送數據。
因為許多基于終端的應用程序假定用戶應采用終端類型的鍵盤,用戶就需要某種方法來發送特殊的鍵擊,如 Return、Escape 或 Control 加按鍵組合。我們為此而使用 ChoiceGroup
,將其設置為 MULTIPLE
模式,以便我們得到復選框而不是互斥的單選按鈕。根據兩種復選框設置,用戶的文本將在 Escape 字符后發送,或在換行字符后發送。如果選擇了 Send as CTRL 選項,發送每個字符時就像是按下了 Control 鍵一樣(Control 加按鍵代碼的計算方法,是將一個字符轉換為其大寫形式,然后從其值減 64)。Append ENTER 選項默認為 true
,因為可用性測試表明大多數文字輸入后跟的是一個換行符。
最后,該窗體是允許客戶設置 TelnetCanvas
的滾動模式的一個很好的地方。這一特性在功能上等同于終端鍵盤上的 Scroll Lock 鍵,而復選框也同樣地進行標記。為了進行可視區分,這個選項加入一個單獨的 ChoiceGroup。
登錄窗體在一般的應用程序生命周期內很少會用到兩次以上,所以我們只根據需要來創建它,當不用時就將其清除。其創建和布局在 onShowLogin()
中進行:
public void onShowLogin(){ // create and populate login form loginForm = new Form( "Connect to" ); hostField = new TextField( "Host", host, 50, TextField.URL ); loginForm.append( hostField ); portField = new TextField( "Port", port, 6, TextField.NUMERIC ); loginForm.append( portField ); loginForm.addCommand( exitCommand ); loginForm.addCommand( openCommand ); loginForm.setCommandListener( this ); // show form display.setCurrent( loginForm );}
雖然這是一個簡單的窗體,可用性仍是我們最重要的考慮。一些實現可以利用一個 TextField
(URL
或 NUMERIC
)上的“線索”并適當地定制用戶界面。因為用戶通常將在 Host 中輸入服務器的域名,指出所期望的用戶輸入像一個 URL 就很有意義。某些設備可能有特殊的屏幕布局,它們針對字母和符號而不是數字進行優化。同樣地,Port 字段應僅限于數字輸入;一些設備可能允許用戶在它們的鍵盤上快速輸入數字,而繞過任何重復按鍵式文字輸入系統。
MIDlet 是登錄窗體、輸入窗以及 Telnet canvas 的命令監聽器??蓪⑵湟曌鲬贸绦蚬ぷ髁鞯南驅?。
在啟動時,將出現登錄窗體,選項為 Open 或 Exit。 一旦打開,就會出現 Telnet canvas,選項為 Close 或 Input。Input 選項顯示輸入窗體,其中有一個 OK 選項用于隱藏窗體并發送文字,另有一個 Cancel 選項僅僅隱藏窗體。Close 選項隱藏 Telnet canvas 并顯示一個登錄窗體。最后,Exit 選項調用 notifyDestroyed()
并退出應用程序。所有這些命令由 MIDTerm
的 commandAction()
方法來處理。
現在所有組件已經就緒,我們就可以使我們的終端模擬器工作起來了。終端應用程序在企業和教育計算環境中得到廣泛應用,用于企業應用程序、軟件開發、系統管理甚至對策模擬。MIDTerm 使得所有這些種類的應用程序和資源都可以從您的移動設備來訪問。
更進一步,任何這些應用程序都可以被“刮擦”:您可以編寫一個移動應用程序來與遠程服務器上的資源密集型程序進行交互、提取輸出并將它呈現在一個用戶友好的圖形界面上。企業開發者通常使用這種技術來在遺留系統上構造新式的用戶界面。
MIDP 平臺上的有效 Telnet 和 ANSI 終端實現清除了這些種類的軟件項目的主要障礙。
通過實現對 ANSI 的終端轉義序列的支持,我們已經更新了第一篇文章的未完善的終端顯示。這個應用程序還更好地利用了 MIDP 的用戶界面功能,并具有對用于用戶輸入的鍵盤和定制窗體的支持。您可以組合和匹配這些軟件組建來為新的種類的網絡意識(network-aware)移動應用程序提供基礎。
(出處:http://www.49028c.com)
新聞熱點
疑難解答