我正在考虑创建一个像终端窗口一样的WPF或Silverlight应用程序。除了,因为它在WPF / Silverlight中,它将能够通过效果,图像等“增强”终端体验。
我正在试图找出模拟终端的最佳方法。我知道如何处理VT100仿真,就解析等而言。但是如何显示呢?我考虑过使用RichTextBox并将VT100转义代码转换为RTF。
我看到的问题是性能。终端可能一次只能获得几个字符,并且为了能够将它们加载到文本框中,我将不断创建TextRanges并使用Load()来加载RTF。此外,为了完成每个加载“会话”,它必须完全描述RTF。例如,如果当前颜色为红色,则每次加载到TextBox中都需要RTF代码才能使文本变为红色,或者我认为RTB不会将其加载为红色。
这似乎非常多余 - 由仿真构建的RTF文档将非常混乱。此外,插入符号的移动似乎不是理想情况下RTB处理的。我需要一些自定义的东西,但这让我害怕!
希望听到明智的想法或对现有解决方案的指示。也许有一种方法可以在其上嵌入一个实际的终端和覆盖物。我发现的唯一一件事是旧的WinForms控件。
更新:在下面的回答中查看由于perf的建议解决方案如何失败。 :(
VT100 Terminal Emulation in Windows WPF or Silverlight
答案 0 :(得分:16)
如果您尝试使用RichTextBox和RTF实现此功能,您将很快遇到许多限制,并发现自己花费更多时间来解决差异,而不是自己实现这些功能。
事实上,使用WPF实现VT100终端仿真非常容易。我知道,因为刚才我在一个小时左右的时间内实现了几乎完整的VT100仿真器。确切地说,除了:
之外,我实施了一切最有趣的部分是:
这是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 DataTemplate即可。例如,光标可以是闪烁的矩形而不是实心矩形。
这段代码很有趣。希望它对你有用。它可能有一两个(或三个)错误,因为我从未真正执行它,但我希望这些可以很容易地清除。如果您解决了问题,我欢迎您对此答案进行编辑。
答案 1 :(得分:2)
有效显示文本的唯一方法是使用TextFormatter。 我已经为基于文本的RPG游戏实现了telnet客户端,它运行得很好。 您可以在http://mudclient.codeplex.com
查看来源答案 2 :(得分:1)
我不明白为什么你会担心RTF会变得错综复杂。是的,它会的。但是,处理它并不是你的负担,微软程序员不久前做了这件事,不得不编写代码来渲染复杂的RTF。它运作良好,对你来说完全不透明。
是的,它不会超级快。但是,嘿,你正在仿效80x25的显示器,曾经以9600波特率运行。完全取代控件以使其达到最佳状态毫无意义,并且将成为一项重大任务。
答案 3 :(得分:1)
好吧,为了报告我的状态,我已经确定使用WPF或Silverlight 这是不可行的。
提出的方法的问题是有80 * 24个TextBlocks加上一些其他元素,前景色,背景色等多个绑定。当屏幕需要滚动时,必须重新评估这些绑定中的每一个,并且非常非常慢。更新整个屏幕需要几秒钟。在我的应用程序中,这是不可接受的,屏幕将不断滚动。
我尝试了很多不同的东西来优化它。我尝试使用一个文本块,每行80次运行。我尝试批量更改通知。我尝试制作它,以便手动更新每个文本块的“滚动”事件。没有什么真正有用 - 缓慢的部分是更新UI,而不是它的完成方式。
如果我设计的机制不是为每个单元格提供文本块或运行,而是仅在文本样式发生变化时更改文本块,那么有一点可能会有所帮助。因此,例如一串相同颜色的文本将只有1个文本块。然而,这将是非常复杂的,并且最终,它将仅有助于在屏幕上具有很小风格变化的场景。我的应用程序会有很多颜色飞过(想想ANSI艺术),所以在这种情况下它仍然会很慢。
我认为另一件事有帮助,如果我没有更新文本块,而是在屏幕滚动时滚动它们。因此,文本块将从顶部移动到底部,然后只需要更新新文本块。我设法通过使用可观察的集合来实现这一点。它有所帮助,但它仍然很慢!
我甚至考虑使用OnRender进行自定义WPF控件。我创建了一个以各种方式使用drawingContext.RenderText来查看它有多快。但即使不断更新屏幕,这也太慢了。
就是这样......我已经放弃了这个设计。我正在考虑使用如下所述的实际控制台窗口:
No output to console from a WPF application?
我真的不喜欢,因为窗口与主窗口是分开的,所以我正在寻找一种方法将控制台窗口嵌入WPF窗口,如果可能的话。我会在那个问题上提出另一个问题,并在此时将其链接到这里。
UPDATE:嵌入控制台窗口也失败了,因为删除标题栏并不合适。我已经将它实现为低级绘制自定义WinForms控件,我在WPF中托管它。这非常有效,经过一些优化,非常快。