燈籠簡介
一、簡介
Lanterna是一個用於建立基於文字的使用者介面的庫,為我們提供了與Curses C 庫類似的功能。然而,Lanterna 是用純 Java 寫的。它還使我們能夠透過使用模擬終端,即使在純圖形環境中產生終端 UI。
在本教程中,我們將了解 Lanterna。我們將了解它是什麼、我們可以用它做什麼以及如何使用它。
2. 依賴關係
在使用 Lanterna 之前,我們需要在建置中包含最新版本,在撰寫本文時為3.1.2 。
如果我們使用 Maven,我們可以在pom.xml
檔案中包含此依賴項:
<dependency>
<groupId>com.googlecode.lanterna</groupId>
<artifactId>lanterna</artifactId>
<version>3.1.2</version>
</dependency>
此時,我們已準備好開始在我們的應用程式中使用它。
3. 存取終端
在我們進行終端 UI 工作之前,我們需要一個終端。這可能是我們正在運行的實際系統終端,或是模擬終端的 Swing 框架。
存取此類終端的最安全方法是使用DefaultTerminalFactory
。這將根據其運行環境做出最好的事情:
try (Terminal terminal = new DefaultTerminalFactory().createTerminal()) {
// Terminal functionality here
}
預設情況下,其工作方式因係統而異——要么創建一個根據System.out
和System.in
工作的終端,要么在 Swing 或 AWT 框架中創建一個模擬終端。無論如何,我們總是會有一個可以渲染 UI 的終端。
或者,我們可以透過直接實例化適當的類別來創建我們想要的確切類型的終端:
try (Terminal terminal = new SwingTerminalFrame() {
// Terminal functionality here
}
Lanterna 提供了幾種不同的實作供我們選擇。然而,重要的是我們選擇一個合適的,否則它將無法按預期工作 - 例如, SwingTerminalFrame
只能在可以運行 Swing 應用程式的環境中工作。
創建後,我們可能希望在使用期間啟動私人模式:
terminal.enterPrivateMode();
// Terminal functionality here
terminal.exitPrivateMode();
私有模式會擷取終端的副本,然後將其清除。這意味著我們可以根據需要操縱終端,最後終端將返回其原始狀態。
請注意,我們需要追蹤我們是否處於私人模式。如果我們已經處於所需的狀態,進入或退出私有模式將引發例外狀況。
但是, close()
方法將正確追蹤我們是否處於私有模式,並且只有在處於私有模式時才會退出。這使我們能夠安全地依賴 try-with-resources 模式來為我們進行整理。
4. 低階終端操作
一旦我們可以訪問我們的終端,我們就可以開始使用它了。
我們能做的最簡單的事情就是向終端寫入字元。我們可以使用終端機上的putCharacter()
方法來完成此操作:
terminal.putCharacter('H');
terminal.putCharacter('e');
terminal.putCharacter('l');
terminal.putCharacter('l');
terminal.putCharacter('o');
terminal.flush();
還需要呼叫flush()
來確保字元被傳送到終端。如果沒有這個,底層輸出流將在必要時刷新自身,這可能導致終端意外更新:
4.1.遊標位置
當我們進入私人模式時,Lanterna 會清除螢幕並將遊標移到終端的左上角。如果我們不使用私有模式,那麼遊標將保留在先前的位置。這將列印當前遊標位置處的字符,然後遊標向右移動一個字符。如果我們到達一行的末尾,那麼這將繞到下一行。
如果需要,我們可以使用setCursorPosition()
方法手動將遊標定位到我們想要的任何位置:
terminal.setCursorPosition(10, 10);
terminal.putCharacter('H');
terminal.putCharacter('e');
terminal.putCharacter('l');
terminal.putCharacter('l');
terminal.putCharacter('o');
terminal.setCursorPosition(11, 11);
terminal.putCharacter('W');
terminal.putCharacter('o');
terminal.putCharacter('r');
terminal.putCharacter('l');
terminal.putCharacter('d');
作為其中的一部分,我們需要知道終端有多大。 Lanterna 讓我們可以透過getTerminalSize()
方法來存取它:
TerminalSize size = terminal.getTerminalSize();
System.out.println("Rows: " + size.getRows());
System.out.println("Columns: " + size.getColumns());
4.2.文字樣式
除了寫出角色之外,我們還可以進行一定程度的造型。
我們可以使用setForegroundColor()
和setBackgroundColor()
指定字元的顏色,提供要使用的顏色:
terminal.setForegroundColor(TextColor.ANSI.RED);
terminal.putCharacter('H');
terminal.putCharacter('e');
terminal.putCharacter('l');
terminal.putCharacter('l');
terminal.putCharacter('o');
terminal.setForegroundColor(TextColor.ANSI.DEFAULT);
terminal.setBackgroundColor(TextColor.ANSI.BLUE);
terminal.putCharacter('W');
terminal.putCharacter('o');
terminal.putCharacter('r');
terminal.putCharacter('l');
terminal.putCharacter('d');
這些方法需要使用顏色。使用提供的 ANSI 顏色名稱枚舉提供了最安全的顏色設定方法。我們也可以使用TextColor.RGB
類別提供完整的 RGB 顏色。但是,並非所有終端都支援此功能,並且在不支援它的終端上使用它們可能會提供未定義的行為。
我們也可以指定其他樣式,例如粗體或底線。這些是使用enableSGR()
和disableSGR()
方法完成的-指定要啟用和停用列印字元的SGR屬性:
terminal.enableSGR(SGR.BOLD);
terminal.putCharacter('H');
terminal.putCharacter('e');
terminal.putCharacter('l');
terminal.putCharacter('l');
terminal.putCharacter('o');
terminal.disableSGR(SGR.BOLD);
terminal.enableSGR(SGR.UNDERLINE);
terminal.putCharacter('W');
terminal.putCharacter('o');
terminal.putCharacter('r');
terminal.putCharacter('l');
terminal.putCharacter('d');
最後,我們可以使用resetColorAndSGR()
方法將所有顏色和樣式清除回預設值。這將使一切恢復到終端首次開啟時的狀態。
4.3.接收鍵盤輸入
除了寫入終端機之外,我們還可以從終端接收鍵盤輸入。我們有兩種不同的方法來實現這一目標。 readInput()
是一個阻塞調用,它將等待直到收到按鍵。或者, pollInput()
是一種非阻塞替代方案,它會傳回下一個可用的鍵輸入,如果沒有可用的則傳回null
。
這兩個方法都會傳回一個表示按下的鍵的KeyStroke
物件。然後我們需要使用getKeyType()
方法來決定按下的鍵的類型。如果這是KeyType.Character
,則表示按下了標準字元之一,我們可以使用getCharacter()
來確定是哪一個。
例如,讓我們回顯在終端上鍵入的字符,並在按下Escape
鍵後立即停止:
while (true) {
KeyStroke keystroke = terminal.readInput();
if (keystroke.getKeyType() == KeyType.Escape) {
break;
} else if (keystroke.getKeyType() == KeyType.Character) {
terminal.putCharacter(keystroke.getCharacter());
terminal.flush();
}
}
此外,我們可以使用isCtrlDown()
和isAltDown()
方法來偵測按鍵時是否按下了Ctrl
或Alt
鍵。我們無法明確偵測是否直接按下了Shift
鍵,但它會反映在getCharacter()
傳回的字元中。
5. 緩衝螢幕API
除了對終端的低階存取之外,Lanterna 還為我們提供了一個緩衝的 API 來表示整個螢幕。這不具有使用較低等級 API 的靈活性,但對於進行大規模螢幕操作要簡單得多。
為了使用這個 API,我們首先需要建構一個Screen
。我們可以透過直接包裝我們已有的Terminal
實例來創建它:
try (Screen screen = new TerminalScreen(terminal)) {
screen.startScreen();
// Screen functionality here
}
或者,如果我們還沒有創建Terminal
,那麼我們可以直接從DefaultTerminalFactory
創建Screen
:
try (Screen screen = new DefaultTerminalFactory().createScreen()) {
screen.startScreen();
// Screen functionality here
}
在這兩種情況下,我們也必須呼叫startScreen()
方法。這將設定所有必需的詳細信息,其中包括將底層終端移至私人模式。請注意,這意味著我們不能自己將終端移至私有模式,否則將會失敗。
還有一個對應的stopScreen()
方法,但它會被close()
方法自動調用,因此我們仍然可以依靠 try-by-resources 模式來為我們清理。
每當我們使用Screen
包裝器時,它都會追蹤螢幕上應該顯示的所有內容。這意味著我們不應該同時使用較低層級的Terminal
API 來操作它,因為它無法理解這些更改,而且我們不會獲得所需的結果。
螢幕被緩衝的事實意味著我們所做的任何更改都不會立即顯示。相反,我們的Screen
物件會在記憶體中建立一個表示。然後我們需要使用refresh()
方法將整個螢幕寫入終端。
5.1.列印到螢幕
一旦我們有了一個Screen
實例,我們就可以在它上面繪圖了。與低階 API 不同,我們可以在一次呼叫中將整個格式化字串繪製到所需的點。
例如,讓我們使用setCharacter()
呼叫繪製單一字元:
screen.setCharacter(5, 5,
new TextCharacter('!',
TextColor.ANSI.RED, TextColor.ANSI.YELLOW_BRIGHT,
SGR.UNDERLINE, SGR.BOLD));
screen.refresh();
在這裡,我們提供了角色的座標、角色本身、前景色和背景色以及要使用的任何屬性:
或者,讓我們使用TextGraphics
物件以相同的樣式呈現多個整個字串:
TextGraphics text = screen.newTextGraphics();
text.setForegroundColor(TextColor.ANSI.RED);
text.setBackgroundColor(TextColor.ANSI.YELLOW_BRIGHT);
text.putString(5, 5, "Hello");
text.putString(6, 6, "World!");
screen.refresh();
在這裡,我們產生一個帶有要使用的顏色的TextGraphics
對象,然後使用它直接將整個字串列印到螢幕上:
5.2.處理螢幕大小調整
與低階渲染一樣,了解螢幕的大小以便能夠在正確的位置進行繪製非常重要。然而,在緩衝模式下,Lanterna 也知道這一點也很重要。
在每個渲染循環開始時,我們應該呼叫doResizeIfNecessary()
來更新內部緩衝區。這也將向我們傳回新的終端大小,如果自上次檢查以來終端沒有更改大小,則傳回null
:
TerminalSize newSize = screen.doResizeIfNecessary();
if (newSize != null) {
// React to resize
}
這使我們能夠對螢幕大小調整做出反應 - 例如,根據新大小清除並重新繪製整個螢幕。
6. 文字 GUI
到目前為止,我們已經了解瞭如何將自己的文字渲染到終端上,要么單獨放置每個字符,要么將整個螢幕視為要繪製的緩衝區。然而, Lanterna 也在此之上提供了一個層,我們可以在其中管理基於完整文本的 GUI。
這些 GUI 由MultiWindowTextGUI
類別管理,該類別本身包裝了一個Screen
實例:
MultiWindowTextGUI gui = new MultiWindowTextGUI(screen);
// Render GUI here
gui.updateScreen();
與我們的Terminal
和Screen
類別不同,這不需要任何啟動或停止方法。相反,當呼叫updateScreen()
方法時,它會直接呈現到提供的Screen
。
這反過來會導致螢幕刷新,因此我們不需要自己管理。然而,我們應該只使用一個TextGUI
實例,否則,事情將會不同步。
6.1.視窗
GUI 中的所有內容都在視窗內呈現。 MultiWindowTextGUI
能夠一次顯示多個窗口,但預設情況下,這些視窗將是模態的,並且只有最近的一個視窗才是互動的。
Lanterna 提供了許多我們可以使用的不同的Window
子類別。例如,讓我們使用MessageDialog
向使用者顯示一個簡單的訊息框:
MessageDialog window = new MessageDialogBuilder()
.setTitle("Message Dialog")
.setText("Dialog Contents")
.build();
gui.addWindow(window);
一旦我們創建了視窗並使用addWindow()
呼叫將其新增至 GUI,Lanterna 將正確呈現它:
我們可以使用的最靈活的視窗是BasicWindow
:
BasicWindow window = new BasicWindow("Basic Window");
gui.addWindow(window);
它沒有預先定義的內容或行為,而是允許我們自己定義所有這些。
預設情況下,我們的視窗都會以某種方式顯示。視窗將有一個邊框,在背景元素上投射陰影,調整自身大小以適應其內容,並從螢幕頂部層疊。但是,我們可以向 Lanterna 提供提示以覆蓋所有這些:
BasicWindow window = new BasicWindow("Basic Window");
window.setHints(Set.of(Window.Hint.CENTERED,
Window.Hint.NO_POST_RENDERING,
Window.Hint.EXPANDED));
gui.addWindow(window);
這將使視窗在 GUI 中居中,將其展開以填充大部分(但不是全部)螢幕,並防止它在背景元素上投射陰影:
6.2.成分
現在我們的 GUI 中已經有了一個窗口,我們需要能夠在其中添加內容。 Lanterna 提供了一系列我們可以新增到視窗中的元件,包括標籤、按鈕、文字方塊等等。
我們的視窗可以使用 set Component()
方法來新增一個元件。這需要我們想要使用的元件:
window.setComponent(new Label("This is a label"));
這是新元件渲染所需的全部內容:
如果我們沒有給視窗任何關於其大小的提示,它會自動調整大小以適合該元件。
然而,僅向視窗添加單個組件的能力是相當有限的。 Lantera 以與 AWT/Swing 類似的方式解決這個問題。我們可以新增一個Panel
元件並使用LayoutManager
配置它,以按照我們想要的佈局排列多個元件:
BasicWindow window = new BasicWindow("Basic Window");
Panel innerPanel = new Panel(new LinearLayout(Direction.HORIZONTAL));
innerPanel.addComponent(new Label("Left"));
innerPanel.addComponent(new Label("Middle"));
innerPanel.addComponent(new Label("Right"));
Panel outerPanel = new Panel(new LinearLayout(Direction.VERTICAL));
outerPanel.addComponent(new Label("Top"));
outerPanel.addComponent(innerPanel);
outerPanel.addComponent(new Label("Bottom"));
window.setComponent(outerPanel);
這為我們提供了一個具有三個垂直佈局組件的面板 - 兩個標籤和另一個面板,該面板本身有三個水平佈局的標籤:
6.3.互動元件
到目前為止,我們所有的元件都是被動元件。然而, Lanterna 也為我們提供了一系列互動式元件 - 包括文字方塊、按鈕等。
有些元件可以根據自己的權限自動互動 - 例如,文字方塊將允許我們在其中輸入內容並正確更新自身。其他元件(例如按鈕)允許我們新增偵聽器以對使用者輸入做出反應:
TextBox textbox = new TextBox();
Button button = new Button("OK");
button.addListener((b) -> {
System.out.println(textbox.getText());
window.close();
});
Panel panel = new Panel(new LinearLayout(Direction.VERTICAL));
panel.addComponent(textbox);
panel.addComponent(button);
這將為我們提供一個文字方塊和一個按鈕。啟動按鈕將列印出在文字方塊中輸入的值並關閉視窗:
為了讓我們的視窗處理輸入,我們需要呼叫waitUntilClosed()
。此時,Lanterna 將處理焦點組件中的鍵盤輸入並讓使用者與其互動。請注意,此方法將阻塞直到視窗關閉,這表示我們需要先設定任何適當的處理程序。
七、結論
這是對 Lanterna 的快速介紹。使用這個函式庫可以做更多的事情,包括更多的 GUI 元件。下次您需要建立基於文字的 UI 時,為什麼不嘗試呢?
與往常一樣,本文中的所有範例都可以在 GitHub 上找到。