教程 3
在第三个教程中,我们将研究如何使用 Lanterna 中可用的下一层,它建立在您在教程 1 和 2 中看到的终端界面之上。
Screen与双缓冲显存类似,它有两个表面,可以直接寻址和修改,并通过调用特殊方法将后缓冲的内容移到前面。但是,a 不是像素,而是Screen包含两个文本字符表面(正面和背面),它们对应于终端中的每个“单元格”。您可以自由修改后“缓冲区”,也可以从前“缓冲区”读取,调用 refreshScreen()方法将内容从后缓冲区复制到前缓冲区,这将使 Lanterna 也应用更改,以便用户可以看到它们在终端。
DefaultTerminalFactory defaultTerminalFactory = new DefaultTerminalFactory();
Screen screen = null;
try {
您可以使用DefaultTerminalFactory来创建一个屏幕,这通常会为您提供TerminalScreen 可能是您想要使用的实现。请参阅VirtualScreen有关单独实现的更多详细信息,该实现允许您创建大于运行软件的终端仿真器的物理尺寸的终端表面。为了证明 aScreen位于 a 之上Terminal,我们将创建一个手动而不是使用DefaultTerminalFactory.
Terminal terminal = defaultTerminalFactory.createTerminal();
screen = new TerminalScreen(terminal);
屏幕只能在私有模式下工作,虽然您可以调用方法来改变其状态,但在您可以使任何这些更改可见之前,您需要调用 startScreen() 来准备和设置终端。
screen.startScreen();
让我们关闭本教程的光标
screen.setCursorPosition(null);
现在让我们在屏幕缓冲区中绘制一些随机内容
Random random = new Random();
TerminalSize terminalSize = screen.getTerminalSize();
for(int column = 0; column < terminalSize.getColumns(); column++) {
for(int row = 0; row < terminalSize.getRows(); row++) {
screen.setCharacter(column, row, new TextCharacter(
' ',
TextColor.ANSI.DEFAULT,
// This will pick a random background color
TextColor.ANSI.values()[random.nextInt(TextColor.ANSI.values().length)]));
}
}
所以此时,我们只修改了屏幕中的后台缓冲区,还没有任何可见的内容。为了将内容从后台缓冲区移动到前台缓冲区并刷新屏幕,我们需要调用refresh()
screen.refresh();
现在终端中应该有完全随机的彩色单元格(假设您的终端(模拟器)支持颜色)。让我们看两秒钟或直到用户按下一个键。
long startTime = System.currentTimeMillis();
while(System.currentTimeMillis() - startTime < 2000) {
// The call to pollInput() is not blocking, unlike readInput()
if(screen.pollInput() != null) {
break;
}
try {
Thread.sleep(1);
}
catch(InterruptedException ignore) {
break;
}
}
好的,现在我们循环并不断修改屏幕,直到用户通过按键盘上的 Escape 退出或关闭输入流。当使用 Swing/AWT 捆绑的模拟器时,如果用户关闭窗口,这将导致 EOF KeyStroke。
while(true) {
KeyStroke keyStroke = screen.pollInput();
if(keyStroke != null && (keyStroke.getKeyType() == KeyType.Escape || keyStroke.getKeyType() == KeyType.EOF)) {
break;
}
Screens 会自动监听并记录大小的变化,但你必须Screen知道何时是更新其内部缓冲区的好时机。通常你应该在你的“绘图”循环开始时这样做,如果你有的话。这可确保缓冲区的尺寸保持不变,并且在您绘制内容时不会改变。该方法doReizeIfNecessary()将检查自上次调用以来终端是否已调整大小(或者如果这是第一次调用,则自创建屏幕以来)并相应地更新缓冲区尺寸。如果终端自上次以来未更改大小,则返回 null。
TerminalSize newSize = screen.doResizeIfNecessary();
if(newSize != null) {
terminalSize = newSize;
}
// Increase this to increase speed
final int charactersToModifyPerLoop = 1;
for(int i = 0; i < charactersToModifyPerLoop; i++) {
我们选择一个随机位置
TerminalPosition cellToModify = new TerminalPosition(
random.nextInt(terminalSize.getColumns()),
random.nextInt(terminalSize.getRows()));
再次选择随机背景颜色
TextColor.ANSI color = TextColor.ANSI.values()[random.nextInt(TextColor.ANSI.values().length)];
在后台缓冲区中更新它,注意就像TerminalPositionand一样TerminalSize,TextCharacter 对象是不可变的,所以withBackgroundColor(..)下面的调用返回一个修改了背景颜色的副本。
TextCharacter characterInBackBuffer = screen.getBackCharacter(cellToModify);
characterInBackBuffer = characterInBackBuffer.withBackgroundColor(color);
characterInBackBuffer = characterInBackBuffer.withCharacter(' '); // Because of the label box further down, if it shrinks
screen.setCharacter(cellToModify, characterInBackBuffer);
}
就像使用 一样Terminal,使用 绘制可能更容易TextGraphics。让我们这样做来放置一个带有终端窗口大小信息的小框
String sizeLabel = "Terminal Size: " + terminalSize;
TerminalPosition labelBoxTopLeft = new TerminalPosition(1, 1);
TerminalSize labelBoxSize = new TerminalSize(sizeLabel.length() + 2, 3);
TerminalPosition labelBoxTopRightCorner = labelBoxTopLeft.withRelativeColumn(labelBoxSize.getColumns() - 1);
TextGraphics textGraphics = screen.newTextGraphics();
//This isn't really needed as we are overwriting everything below anyway, but just for demonstrative purpose
textGraphics.fillRectangle(labelBoxTopLeft, labelBoxSize, ' ');
画水平线,先上后下
textGraphics.drawLine(
labelBoxTopLeft.withRelativeColumn(1),
labelBoxTopLeft.withRelativeColumn(labelBoxSize.getColumns() - 2),
Symbols.DOUBLE_LINE_HORIZONTAL);
textGraphics.drawLine(
labelBoxTopLeft.withRelativeRow(2).withRelativeColumn(1),
labelBoxTopLeft.withRelativeRow(2).withRelativeColumn(labelBoxSize.getColumns() - 2),
Symbols.DOUBLE_LINE_HORIZONTAL);
手动做边缘和(因为它只有一个)垂直线,首先在左边然后在右边
textGraphics.setCharacter(labelBoxTopLeft, Symbols.DOUBLE_LINE_TOP_LEFT_CORNER);
textGraphics.setCharacter(labelBoxTopLeft.withRelativeRow(1), Symbols.DOUBLE_LINE_VERTICAL);
textGraphics.setCharacter(labelBoxTopLeft.withRelativeRow(2), Symbols.DOUBLE_LINE_BOTTOM_LEFT_CORNER);
textGraphics.setCharacter(labelBoxTopRightCorner, Symbols.DOUBLE_LINE_TOP_RIGHT_CORNER);
textGraphics.setCharacter(labelBoxTopRightCorner.withRelativeRow(1), Symbols.DOUBLE_LINE_VERTICAL);
textGraphics.setCharacter(labelBoxTopRightCorner.withRelativeRow(2), Symbols.DOUBLE_LINE_BOTTOM_RIGHT_CORNER);
最后将文本放入框内
textGraphics.putString(labelBoxTopLeft.withRelative(1, 1), sizeLabel);
好的,我们完成了,可以显示更改了。让我们也好一点,允许操作系统调度其他线程,这样我们就不会完全阻塞核心。
screen.refresh();
Thread.yield();
每次我们调用刷新时,整个终端都不会重新绘制。相反,Screen它将比较前后缓冲区并仅找出已更改的部分并仅更新这些部分。这就是为什么在上面绘制尺寸信息框的代码中,我们每次循环都将其写出来,但实际上除了第一次外并没有发送到终端,因为Screen知道内容已经存在并且没有更改。因此,在使用 a 时,您永远不应该使用底层Terminal对象,Screen因为这会导致 Screen 不知道的修改。
}
}
catch(IOException e) {
e.printStackTrace();
}
finally {
if(screen != null) {
try {
这里的close()调用将通过退出私有模式来恢复终端,这在调用到startScreen()
screen.close();
}
catch(IOException e) {
e.printStackTrace();
}
}
}
本教程的完整代码可在源代码的测试部分中找到
继续教程 4