Эмуляция терминала VT100 в Windows WPF или Silverlight
Я размышляю над созданием приложения WPF или Silverlight, которое работает так же, как окно терминала. За исключением того, что он есть в WPF/Silverlight, он сможет "улучшить" работу терминала с помощью эффектов, изображений и т. Д.
Я пытаюсь найти лучший способ эмулировать терминал. Я знаю, как справиться с эмуляцией VT100 в части анализа и т. Д. Но как это отобразить? Я подумал о том, чтобы использовать RichTextBox и по существу преобразовать escape-коды VT100 в RTF.
Проблема, которую я вижу с этим, является производительностью. Терминал может получать только несколько символов за раз, и чтобы иметь возможность загружать их в текстовое поле по ходу дела, я бы постоянно создавал TextRanges и использовал Load() для загрузки RTF. Кроме того, для того, чтобы каждая "сессия" загрузки была завершена, она должна полностью описывать RTF. Например, если текущий цвет красный, для каждой загрузки в TextBox нужны коды RTF, чтобы сделать текст красным, или я предполагаю, что RTB не будет загружать его красным.
Это кажется очень избыточным - полученный в результате эмуляции RTF-документ будет очень грязным. Кроме того, перемещение каретки не выглядит так, как будто бы идеально обрабатывается RTB. Мне нужно что-то нестандартное, я думаю, но это пугает меня!
Надеемся услышать яркие идеи или указатели на существующие решения. Возможно, есть способ встроить реальный терминал и наложить поверх него что-то еще. Единственное, что я нашел, это старый элемент управления WinForms.
ОБНОВЛЕНИЕ: Посмотрите, как предложенное решение не удается из-за перфект в моем ответе ниже:(
Эмуляция терминала VT100 в Windows WPF или Silverlight
4 ответа
Если вы попытаетесь реализовать это с помощью RichTextBox и RTF, вы быстро столкнетесь со многими ограничениями и обнаружите, что тратите гораздо больше времени на устранение различий, чем если бы вы реализовали эту функцию самостоятельно.
На самом деле, реализовать эмуляцию терминала VT100 довольно просто с помощью WPF. Я знаю, потому что только сейчас я внедрил почти полный эмулятор VT100 примерно через час. Если быть точным, я внедрил все, кроме:
- Ввод с клавиатуры,
- Альтернативные наборы символов,
- Несколько эзотерических режимов VT100, которые я никогда не видел, использовались,
Самые интересные части были:
- Символы двойной ширины / двойной высоты, для которых я использовал RenderTransform и RenderTransformOrigin
- Мигание, для которого я использовал анимацию на общем объекте, поэтому все персонажи будут мигать вместе
- Подчеркивание, для которого я использовал Grid и Rectangle, чтобы он выглядел больше как дисплей VT100
- Курсор и выделение, для которых я устанавливаю флажок на сами ячейки и использую DataTriggers для изменения отображения
- Использование как одномерного массива, так и вложенного массива, указывающего на одни и те же объекты, для упрощения прокрутки и выделения.
Вот XAML:
<Style TargetType="my:VT100Terminal">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="my:VT100Terminal">
<DockPanel>
<!-- Add status bars, etc to the DockPanel at this point -->
<ContentPresenter Content="{Binding Display}" />
</DockPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<ItemsPanelTemplate x:Key="DockPanelLayout">
<DockPanel />
</ItemsPanelTemplate>
<DataTemplate DataType="{x:Type my:TerminalDisplay}">
<ItemsControl ItemsSource="{Binding Lines}" TextElement.FontFamily="Courier New">
<ItemsControl.ItemTemplate>
<DataTemplate>
<ItemsControl ItemsSource="{Binding}" ItemsPanel="{StaticResource DockPanelLayout}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DataTemplate>
<DataTemplate DataType="{x:Type my:TerminalCell}">
<Grid>
<TextBlock x:Name="tb"
Text="{Binding Character}"
Foreground="{Binding Foreground}"
Background="{Binding Background}"
FontWeight="{Binding FontWeight}"
RenderTransformOrigin="{Binding TranformOrigin}">
<TextBlock.RenderTransform>
<ScaleTransform ScaleX="{Binding ScaleX}" ScaleY="{Binding ScaleY}" />
</TextBlock.RenderTransform>
</TextBlock>
<Rectangle Visibility="{Binding UnderlineVisiblity}" Height="1" HorizontalAlignment="Stretch" VerticalAlignment="Bottom" Margin="0 0 0 2" />
</Grid>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding IsCursor}" Value="true">
<Setter TargetName="tb" Property="Foreground" Value="{Binding Background}" />
<Setter TargetName="tb" Property="Background" Value="{Binding Foreground}" />
</DataTrigger>
<DataTrigger Binding="{Binding IsMouseSelected}" Value="true">
<Setter TargetName="tb" Property="Foreground" Value="White" />
<Setter TargetName="tb" Property="Background" Value="Blue" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
И вот код:
public class VT100Terminal : Control
{
bool _selecting;
static VT100Terminal()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(VT100Terminal), new FrameworkPropertyMetadata(typeof(VT100Terminal)));
}
// Display
public TerminalDisplay Display { get { return (TerminalDisplay)GetValue(DisplayProperty); } set { SetValue(DisplayProperty, value); } }
public static readonly DependencyProperty DisplayProperty = DependencyProperty.Register("Display", typeof(TerminalDisplay), typeof(VT100Terminal));
public VT100Terminal()
{
Display = new TerminalDisplay();
MouseLeftButtonDown += HandleMouseMessage;
MouseMove += HandleMouseMessage;
MouseLeftButtonUp += HandleMouseMessage;
KeyDown += HandleKeyMessage;
CommandBindings.Add(new CommandBinding(ApplicationCommands.Copy, ExecuteCopy, CanExecuteCopy));
}
public void ProcessCharacter(char ch)
{
Display.ProcessCharacter(ch);
}
private void HandleMouseMessage(object sender, MouseEventArgs e)
{
if(!_selecting && e.RoutedEvent != Mouse.MouseDownEvent) return;
if(e.RoutedEvent == Mouse.MouseUpEvent) _selecting = false;
var block = e.Source as TextBlock; if(block==null) return;
var cell = ((TextBlock)e.Source).DataContext as TerminalCell; if(cell==null) return;
var index = Display.GetIndex(cell); if(index<0) return;
if(e.GetPosition(block).X > block.ActualWidth/2) index++;
if(e.RoutedEvent == Mouse.MouseDownEvent)
{
Display.SelectionStart = index;
_selecting = true;
}
Display.SelectionEnd = index;
}
private void HandleKeyMessage(object sender, KeyEventArgs e)
{
// TODO: Code to covert e.Key to VT100 codes and report keystrokes to client
}
private void CanExecuteCopy(object sender, CanExecuteRoutedEventArgs e)
{
if(Display.SelectedText!="") e.CanExecute = true;
}
private void ExecuteCopy(object sender, ExecutedRoutedEventArgs e)
{
if(Display.SelectedText!="")
{
Clipboard.SetText(Display.SelectedText);
e.Handled = true;
}
}
}
public enum CharacterDoubling
{
Normal = 5,
Width = 6,
HeightUpper = 3,
HeightLower = 4,
}
public class TerminalCell : INotifyPropertyChanged
{
char _character;
Brush _foreground, _background;
CharacterDoubling _doubling;
bool _isBold, _isUnderline;
bool _isCursor, _isMouseSelected;
public char Character { get { return _character; } set { _character = value; Notify("Character", "Text"); } }
public Brush Foreground { get { return _foreground; } set { _foreground = value; Notify("Foreground"); } }
public Brush Background { get { return _background; } set { _background = value; Notify("Background"); } }
public CharacterDoubling Doubling { get { return _doubling; } set { _doubling = value; Notify("Doubling", "ScaleX", "ScaleY", "TransformOrigin"); } }
public bool IsBold { get { return _isBold; } set { _isBold = value; Notify("IsBold", "FontWeight"); } }
public bool IsUnderline { get { return _isUnderline; } set { _isUnderline = value; Notify("IsUnderline", "UnderlineVisibility"); } }
public bool IsCursor { get { return _isCursor; } set { _isCursor = value; Notify("IsCursor"); } }
public bool IsMouseSelected { get { return _isMouseSelected; } set { _isMouseSelected = value; Notify("IsMouseSelected"); } }
public string Text { get { return Character.ToString(); } }
public int ScaleX { get { return Doubling!=CharacterDoubling.Normal ? 2 : 1; } }
public int ScaleY { get { return Doubling==CharacterDoubling.HeightUpper || Doubling==CharacterDoubling.HeightLower ? 2 : 1; } }
public Point TransformOrigin { get { return Doubling==CharacterDoubling.HeightLower ? new Point(1,0) : new Point(0,0); } }
public FontWeight FontWeight { get { return IsBold ? FontWeights.Bold : FontWeights.Normal; } }
public Visibility UnderlineVisibility { get { return IsUnderline ? Visibility.Visible : Visibility.Hidden; } }
// INotifyPropertyChanged implementation
private void Notify(params string[] propertyNames) { foreach(string name in propertyNames) Notify(name); }
private void Notify(string propertyName)
{
if(PropertyChanged!=null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler PropertyChanged;
}
public class TerminalDisplay : INotifyPropertyChanged
{
// Basic state
private TerminalCell[] _buffer;
private TerminalCell[][] _lines;
private int _height, _width;
private int _row, _column; // Cursor position
private int _scrollTop, _scrollBottom;
private List<int> _tabStops;
private int _selectStart, _selectEnd; // Text selection
private int _saveRow, _saveColumn; // Saved location
// Escape character processing
string _escapeChars, _escapeArgs;
// Modes
private bool _vt52Mode;
private bool _autoWrapMode;
// current attributes
private bool _boldMode, _lowMode, _underlineMode, _blinkMode, _reverseMode, _invisibleMode;
// saved attributes
private bool _saveboldMode, _savelowMode, _saveunderlineMode, _saveblinkMode, _savereverseMode, _saveinvisibleMode;
private Color _foreColor, _backColor;
private CharacterDoubling _doubleMode;
// Computed from current mode
private Brush _foreground;
private Brush _background;
// Hidden control used to synchronize blinking
private FrameworkElement _blinkMaster;
public TerminalDisplay()
{
Reset();
}
public void Reset()
{
_height = 24;
_width = 80;
_row = 0;
_column = 0;
_scrollTop = 0;
_scrollBottom = _height;
_vt52Mode = false;
_autoWrapMode = true;
_selectStart = 0;
_selectEnd = 0;
_tabStops = new List<int>();
ResetBuffer();
ResetCharacterModes();
UpdateBrushes();
_saveboldMode = _savelowMode = _saveunderlineMode = _saveblinkMode = _savereverseMode = _saveinvisibleMode = false;
_saveRow = _saveColumn = 0;
}
private void ResetBuffer()
{
_buffer = (from i in Enumerable.Range(0, Width * Height) select new TerminalCell()).ToArray();
UpdateSelection();
UpdateLines();
}
private void ResetCharacterModes()
{
_boldMode = _lowMode = _underlineMode = _blinkMode = _reverseMode = _invisibleMode = false;
_doubleMode = CharacterDoubling.Normal;
_foreColor = Colors.White;
_backColor = Colors.Black;
}
public int Height { get { return _height; } set { _height = value; ResetBuffer(); } }
public int Width { get { return _width; } set { _width = value; ResetBuffer(); } }
public int Row { get { return _row; } set { CursorCell.IsCursor = false; _row=value; CursorCell.IsCursor = true; Notify("Row", "CursorCell"); } }
public int Column { get { return _column; } set { CursorCell.IsCursor = false; _column=value; CursorCell.IsCursor = true; Notify("Row", "CursorCell"); } }
public int SelectionStart { get { return _selectStart; } set { _selectStart = value; UpdateSelection(); Notify("SelectionStart", "SelectedText"); } }
public int SelectionEnd { get { return _selectEnd; } set { _selectEnd = value; UpdateSelection(); Notify("SelectionEnd", "SelectedText"); } }
public TerminalCell[][] Lines { get { return _lines; } }
public TerminalCell CursorCell { get { return GetCell(_row, _column); } }
public TerminalCell GetCell(int row, int column)
{
if(row<0 || row>=Height || column<0 || column>=Width)
return new TerminalCell();
return _buffer[row*Height + column];
}
public int GetIndex(int row, int column)
{
return row * Height + column;
}
public int GetIndex(TerminalCell cell)
{
return Array.IndexOf(_buffer, cell);
}
public string SelectedText
{
get
{
int start = Math.Min(_selectStart, _selectEnd);
int end = Math.Max(_selectStart, _selectEnd);
if(start==end) return string.Empty;
var builder = new StringBuilder();
for(int i=start; i<end; i++)
{
if(i!=start && (i%Width==0))
{
while(builder.Length>0 && builder[builder.Length-1]==' ')
builder.Length--;
builder.Append("\r\n");
}
builder.Append(_buffer[i].Character);
}
return builder.ToString();
}
}
/////////////////////////////////
public void ProcessCharacter(char ch)
{
if(_escapeChars!=null)
{
ProcessEscapeCharacter(ch);
return;
}
switch(ch)
{
case '\x1b': _escapeChars = ""; _escapeArgs = ""; break;
case '\r': Column = 0; break;
case '\n': NextRowWithScroll();break;
case '\t':
Column = (from stop in _tabStops where stop>Column select (int?)stop).Min() ?? Width - 1;
break;
default:
CursorCell.Character = ch;
FormatCell(CursorCell);
if(CursorCell.Doubling!=CharacterDoubling.Normal) ++Column;
if(++Column>=Width)
if(_autoWrapMode)
{
Column = 0;
NextRowWithScroll();
}
else
Column--;
break;
}
}
private void ProcessEscapeCharacter(char ch)
{
if(_escapeChars.Length==0 && "78".IndexOf(ch)>=0)
{
_escapeChars += ch.ToString();
}
else if(_escapeChars.Length>0 && "()Y".IndexOf(_escapeChars[0])>=0)
{
_escapeChars += ch.ToString();
if(_escapeChars.Length != (_escapeChars[0]=='Y' ? 3 : 2)) return;
}
else if(ch==';' || char.IsDigit(ch))
{
_escapeArgs += ch.ToString();
return;
}
else
{
_escapeChars += ch.ToString();
if("[#?()Y".IndexOf(ch)>=0) return;
}
ProcessEscapeSequence();
_escapeChars = null;
_escapeArgs = null;
}
private void ProcessEscapeSequence()
{
if(_escapeChars.StartsWith("Y"))
{
Row = (int)_escapeChars[1] - 64;
Column = (int)_escapeChars[2] - 64;
return;
}
if(_vt52Mode && (_escapeChars=="D" || _escapeChars=="H")) _escapeChars += "_";
var args = _escapeArgs.Split(';');
int? arg0 = args.Length>0 && args[0]!="" ? int.Parse(args[0]) : (int?)null;
int? arg1 = args.Length>1 && args[1]!="" ? int.Parse(args[1]) : (int?)null;
switch(_escapeChars)
{
case "[A": case "A": Row -= Math.Max(arg0??1, 1); break;
case "[B": case "B": Row += Math.Max(arg0??1, 1); break;
case "[c": case "C": Column += Math.Max(arg0??1, 1); break;
case "[D": case "D": Column -= Math.Max(arg0??1, 1); break;
case "[f":
case "[H": case "H_":
Row = Math.Max(arg0??1, 1) - 1; Column = Math.Max(arg0??1, 1) - 1;
break;
case "M": PriorRowWithScroll(); break;
case "D_": NextRowWithScroll(); break;
case "E": NextRowWithScroll(); Column = 0; break;
case "[r": _scrollTop = (arg0??1)-1; _scrollBottom = (arg0??_height); break;
case "H": if(!_tabStops.Contains(Column)) _tabStops.Add(Column); break;
case "g": if(arg0==3) _tabStops.Clear(); else _tabStops.Remove(Column); break;
case "[J": case "J":
switch(arg0??0)
{
case 0: ClearRange(Row, Column, Height, Width); break;
case 1: ClearRange(0, 0, Row, Column + 1); break;
case 2: ClearRange(0, 0, Height, Width); break;
}
break;
case "[K": case "K":
switch(arg0??0)
{
case 0: ClearRange(Row, Column, Row, Width); break;
case 1: ClearRange(Row, 0, Row, Column + 1); break;
case 2: ClearRange(Row, 0, Row, Width); break;
}
break;
case "?l":
case "?h":
var h = _escapeChars=="?h";
switch(arg0)
{
case 2: _vt52Mode = h; break;
case 3: Width = h ? 132 : 80; ResetBuffer(); break;
case 7: _autoWrapMode = h; break;
}
break;
case "<": _vt52Mode = false; break;
case "m":
if (args.Length == 0) ResetCharacterModes();
foreach(var arg in args)
switch(arg)
{
case "0": ResetCharacterModes(); break;
case "1": _boldMode = true; break;
case "2": _lowMode = true; break;
case "4": _underlineMode = true; break;
case "5": _blinkMode = true; break;
case "7": _reverseMode = true; break;
case "8": _invisibleMode = true; break;
}
UpdateBrushes();
break;
case "#3": case "#4": case "#5": case "#6":
_doubleMode = (CharacterDoubling)((int)_escapeChars[1] - (int)'0');
break;
case "[s": _saveRow = Row; _saveColumn = Column; break;
case "7": _saveRow = Row; _saveColumn = Column;
_saveboldMode = _boldMode; _savelowMode = _lowMode;
_saveunderlineMode = _underlineMode; _saveblinkMode = _blinkMode;
_savereverseMode = _reverseMode; _saveinvisibleMode = _invisibleMode;
break;
case "[u": Row = _saveRow; Column = _saveColumn; break;
case "8": Row = _saveRow; Column = _saveColumn;
_boldMode = _saveboldMode; _lowMode = _savelowMode;
_underlineMode = _saveunderlineMode; _blinkMode = _saveblinkMode;
_reverseMode = _savereverseMode; _invisibleMode = _saveinvisibleMode;
break;
case "c": Reset(); break;
// TODO: Character set selection, several esoteric ?h/?l modes
}
if(Column<0) Column=0;
if(Column>=Width) Column=Width-1;
if(Row<0) Row=0;
if(Row>=Height) Row=Height-1;
}
private void PriorRowWithScroll()
{
if(Row==_scrollTop) ScrollDown(); else Row--;
}
private void NextRowWithScroll()
{
if(Row==_scrollBottom-1) ScrollUp(); else Row++;
}
private void ScrollUp()
{
Array.Copy(_buffer, _width * (_scrollTop + 1), _buffer, _width * _scrollTop, _width * (_scrollBottom - _scrollTop - 1));
ClearRange(_scrollBottom-1, 0, _scrollBottom-1, Width);
UpdateSelection();
UpdateLines();
}
private void ScrollDown()
{
Array.Copy(_buffer, _width * _scrollTop, _buffer, _width * (_scrollTop + 1), _width * (_scrollBottom - _scrollTop - 1));
ClearRange(_scrollTop, 0, _scrollTop, Width);
UpdateSelection();
UpdateLines();
}
private void ClearRange(int startRow, int startColumn, int endRow, int endColumn)
{
int start = startRow * Width + startColumn;
int end = endRow * Width + endColumn;
for(int i=start; i<end; i++)
ClearCell(_buffer[i]);
}
private void ClearCell(TerminalCell cell)
{
cell.Character = ' ';
FormatCell(cell);
}
private void FormatCell(TerminalCell cell)
{
cell.Foreground = _foreground;
cell.Background = _background;
cell.Doubling = _doubleMode;
cell.IsBold = _boldMode;
cell.IsUnderline = _underlineMode;
}
private void UpdateSelection()
{
var cursor = _row * Width + _height;
var inSelection = false;
for(int i=0; i<_buffer.Length; i++)
{
if(i==_selectStart) inSelection = !inSelection;
if(i==_selectEnd) inSelection = !inSelection;
var cell = _buffer[i];
cell.IsCursor = i==cursor;
cell.IsMouseSelected = inSelection;
}
}
private void UpdateBrushes()
{
var foreColor = _foreColor;
var backColor = _backColor;
if(_lowMode)
{
foreColor = foreColor * 0.5f + Colors.Black * 0.5f;
backColor = backColor * 0.5f + Colors.Black * 0.5f;
}
_foreground = new SolidColorBrush(foreColor);
_background = new SolidColorBrush(backColor);
if(_reverseMode) Swap(ref _foreground, ref _background);
if(_invisibleMode) _foreground = _background;
if(_blinkMode)
{
if(_blinkMaster==null)
{
_blinkMaster = new Control();
var animation = new DoubleAnimationUsingKeyFrames { RepeatBehavior=RepeatBehavior.Forever, Duration=TimeSpan.FromMilliseconds(1000) };
animation.KeyFrames.Add(new DiscreteDoubleKeyFrame(0));
animation.KeyFrames.Add(new DiscreteDoubleKeyFrame(1));
_blinkMaster.BeginAnimation(UIElement.OpacityProperty, animation);
}
var rect = new Rectangle { Fill = _foreground };
rect.SetBinding(UIElement.OpacityProperty, new Binding("Opacity") { Source = _blinkMaster });
_foreground = new VisualBrush { Visual = rect };
}
}
private void Swap<T>(ref T a, ref T b)
{
var temp = a;
a = b;
b = temp;
}
private void UpdateLines()
{
_lines = new TerminalCell[Height][];
for(int r=0; r<Height; r++)
{
_lines[r] = new TerminalCell[Width];
Array.Copy(_buffer, r*Height, _lines[r], 0, Width);
}
}
// INotifyPropertyChanged implementation
private void Notify(params string[] propertyNames) { foreach(string name in propertyNames) Notify(name); }
private void Notify(string propertyName)
{
if(PropertyChanged!=null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler PropertyChanged;
}
Обратите внимание, что если вам не нравится визуальный стиль, просто обновите шаблон данных TerminalCell. Например, курсор может быть мигающим прямоугольником вместо сплошного.
Этот код было весело писать. Надеюсь, это будет полезно для вас. Возможно, в нем есть одна или две ошибки (или три), поскольку я никогда не выполнял их, но я ожидаю, что они будут легко устранены. Я бы приветствовал изменение этого ответа, если вы что-то исправите.
Единственный способ эффективно отобразить текст - это использовать TextFormatter. Я реализовал telnet-клиент для текстовых RPG-игр, и он работает довольно хорошо. Вы можете проверить источники на http://mudclient.codeplex.com/
Ну, чтобы сообщить о своем статусе, я решил, что это не реально с WPF или Silverlight.
Проблема с предлагаемым подходом состоит в том, что существует 80*24 TextBlocks плюс некоторые другие элементы, с несколькими привязками для переднего цвета, заднего цвета и т. Д. Когда экран должен прокручиваться, каждая из этих привязок должна быть переоценена, и ее очень, очень медленно. Обновление всего экрана занимает несколько секунд. В моем приложении это не приемлемо, экран будет постоянно прокручиваться.
Я перепробовал много разных вещей, чтобы оптимизировать его. Я попытался использовать один текстовый блок с 80 прогонов в каждой строке. Я пытался дозировать уведомления об изменениях. Я попытался сделать так, чтобы событие "прокрутки" вручную обновляло каждый текстовый блок. Ничто действительно не помогает - медленная часть обновляет интерфейс, а не способ, которым это делается.
Одна вещь, которая могла бы помочь, - это если бы я разработал механизм, чтобы не иметь текстового блока или работать для каждой ячейки, а изменять текстовые блоки только при изменении стиля текста. Таким образом, строка текста того же цвета, например, будет только 1 текстовым блоком. Тем не менее, это будет очень сложно, и, в конце концов, это поможет только сценариям с небольшим изменением стиля на экране. Мое приложение будет иметь много цветов (думаю, искусство ANSI), так что в этом случае оно все равно будет медленным.
Еще одна вещь, которая, как мне показалось, поможет, - это если я не обновлю текстовые блоки, а прокручиваю их вверх по мере прокрутки экрана. Таким образом, текстовые блоки будут перемещаться сверху вниз, и тогда только новые будут нуждаться в обновлении. Мне удалось добиться этого с помощью наблюдаемой коллекции. Это помогло, но его все еще слишком медленно!
Я даже рассмотрел пользовательский элемент управления WPF с помощью OnRender. Я создал один, который использовал DrawingContext.RenderText различными способами, чтобы увидеть, насколько быстро это может быть. Но ДАЖЕ ЧТО слишком чертовски медленно, чтобы справляться с постоянным обновлением экрана.
Вот и все... Я отказался от этого дизайна. Вместо этого я смотрю на использование фактического окна консоли, как описано здесь:
Нет вывода на консоль из приложения WPF?
Мне это не очень нравится, так как окно отделено от основного окна, поэтому я ищу способ встроить консольное окно в окно WPF, если это вообще возможно. Я задам еще один вопрос по этому вопросу и свяжу его здесь, когда я это сделаю.
ОБНОВЛЕНИЕ: Встраивание окна консоли также не удалось, потому что не нужно удалять его строку заголовка. Я реализовал его как пользовательский элемент управления WinForms низкого уровня рисования, и я размещаю его в WPF. Это прекрасно работает и после некоторых оптимизаций очень быстро.
Я не понимаю, почему вы будете беспокоиться о том, что RTF будет запутанным. Да, это будет. Но это не твое бремя, чтобы справиться с этим, программист Microsoft сделал это некоторое время назад, когда ему пришлось писать код для рендеринга RTF. Это работает хорошо и совершенно непрозрачно для вас.
Да, это не будет очень быстро. Но что за эй, вы эмулируете дисплей 80x25, который раньше работал на скорости 9600 бод. Полностью заменить контроль, чтобы попытаться сделать его оптимальным, не имеет особого смысла, и это станет серьезной задачей.