Windows WPFまたはSilverlightのVT100端末エミュレーション
-
13-09-2020 - |
質問
ターミナルウィンドウと同じように機能するWPFまたはSilverlightアプリを作成しています。 WPF / Silverlightの中にあるため、エフェクト、イメージなどのターミナルエクスペリエンスを「強化」できるようになります。
端末をエミュレートするための最良の方法を把握しようとしています。私は、解析などの範囲でVT100エミュレーションを処理する方法を知っていますが、それを表示する方法は?私はRichTextBoxを使用し、基本的にVT100エスケープコードをRTFに変換します。
それがある問題はパフォーマンスです。ターミナルは一度にわずか数文字だけ上がっている可能性があり、それらをTextBox AS-We-Goにロードできるようにしてもよく、RTFをロードするにはTexTrangesを使用してload()を使用してください。また、各ロード「セッション」が完了するためには、RTFを十分に説明する必要があります。たとえば、現在の色が赤の場合、テキストボックスへの各ロードはテキストを赤くするためにRTFコードを必要とするか、またはRTBが赤く読み込まれないと仮定します。
これは非常に冗長です - エミュレーションによって作成された結果のRTF文書は非常に厄介です。また、キャレットの動きは、RTBによって理想的に取り扱われるように見えないようです。私は習慣、メチンクを必要ですが、それは私を怖がらせます!
既存の解決策への明るいアイデアやポインタを聞きたいと思っています。おそらく、実際の端末を埋め込む方法やその上にオーバーレイのものがあります。私が見つけた唯一のものは古いwinformsコントロールです。
アップデート:下記の答えでPERFにより提案されたソリューションがどのように失敗するかを参照してください。 :(
Windows WPFまたはSilverlight のvt100端末エミュレーション
解決
RichTextBoxやRTFでこれを実装しようとすると、すぐに多くの制限に遭遇し、機能性を自分で実装した場合よりも違いを回避する時間をはるかに費やすことができます。
実際には、WPFを使用してVT100端末エミュレーションを実装するのは非常に簡単です。今すぐ私は1時間ほどほぼ完全なVT100エミュレータを実装しました。正確になるために、私はすべてを除くすべてのものを紹介しました:
- キーボード入力、
- 代替文字セット、
- 私が使用したことがないesoteric VT100モード、
最も興味深い部分は次のとおりです。
- rendertransformとrendertransformorigin を使用した二重幅/二重の高さ文字
- 点滅している点滅し、共有オブジェクトでアニメーションを使用しているため、すべての文字がまとめて
- 下線を付けて、私はグリッドと長方形を使用しているので、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;
}
.
ビジュアルスタイリングが常にターミナルCell DataTempleを更新するだけでなく、たとえば、カーソルは、固体のものではなく点滅の長方形である可能性があります。
このコードは書くのが楽しいです。うまくいけば、それはあなたに役立ちます。私は実際にそれを実行しなかったのでおそらくバグまたは2(または3つ)を持っていますが、私はそれらが簡単にクリアされることを期待しています。あなたが何かを解決した場合、この答えに編集を歓迎します。
他のヒント
テキストを効果的に表示する唯一の方法はTextFormatterを使用しています。 テキストベースのRPGゲーム用のTelnetクライアントを実装しました。 http://mudclient.codeplex.com
なぜRTFが回避されるのかを理解していません。はい、それはそうします。しかし、それに対処するのはあなたの負担ではありません。それはうまく機能し、あなたにとって完全に不透明です。
はい、それは超高速になることはありません。しかし、ねえ、あなたは9600ボーで実行するために使用された80x25ディスプレイをエミュレートしています。コントロールを完全に置き換えるために最適に理解しようとし、主要な事業になるだろう。
well、私のステータスを報告するために、私はこれがWPFまたはSilverlightで実現可能ではないと判断しました。
提案されたアプローチの問題は、80 * 24のテキストブロックと他の要素と他の要素とともに、複数のバインディングがあるということです。画面がスクロールする必要がある場合は、これらの各バインディングを再評価する必要があります。それは非常に遅いです。画面全体を更新すると数秒かかります。私のアプリでは受け入れられないので、画面は絶えずスクロールされる予定です。
私はそれを最適化するためにさまざまなことをたくさん試しました。 1行ごとに80回実行して1つのTextBlockを使用してみました。変更通知をバッチ処理しました。私はそれを作ってみました、それを「スクロール」イベントが手動で各テキストブロックを更新しました。本当に役に立ちません - 遅い部分はそれが行われた方法ではなくUIを更新しています。
助けを受けたことの1つのことは、テキストブロックを持っていないか、すべてのセルに対して実行されないようにしたが、テキストのスタイルが変わったときにのみテキストブロックを変更することを考える。したがって、同じカラーテキストの文字列は、ただ1つのテキストブロックだけです。ただし、それは非常に複雑になり、最後に、画面にスタイルの変化がほとんどないシナリオのみを助けるだけです。私のアプリは(Ansi Art)で飛行する色がたくさんあることになるので、その場合は遅くなります。
私がテキストブロックを更新しなかったが、スクロールをスクロールすると、私が思ったもう一つのことは、私がテキストブロックを更新しなかったが、スクロールされた画面としてそれらをスクロールした。そのため、テキストブロックは上から下へ移動してから新しいものだけが更新が必要になるでしょう。私は観察可能なコレクションを使用してその作業を得ることができました。それは助けましたが、そのまだ遅すぎる!
OnRenderを使用したカスタムWPF制御と考えられています。私はrengingContext.renderTextを様々な方法で使用したものを作成しました。しかし、それが絶えず画面を更新するのを常に更新するのが遅すぎるあまりにも遅いです。
それはそれがこのデザインをあきらめた。代わりに、ここに記載されているように実際のコンソールウィンドウを使用しているのを見ています。
ウィンドウがメインウィンドウとは別のものであるため、それが本当に好きではないので、それが可能な場合は、コンソールウィンドウをWPFウィンドウに埋め込む方法を探しています。私は別の質問をしてくれるので、私がそうするときにそれをここにリンクするでしょう。
アップデート:タタイトルバーを削除してもよいため、コンソールウィンドウの埋め込みに失敗しました。私はそれを低レベルの絵画カスタムWinformsコントロールとして実装しています、そして私はWPFでそれをホストしています。それは美しくそしていくつかの最適化の後、それは非常に速いです。