gpt4 book ai didi

.net - Windows WPF或Silverlight中的VT100终端仿真

转载 作者:行者123 更新时间:2023-12-04 09:38:01 28 4
gpt4 key购买 nike

我正在考虑创建一个WPF或Silverlight应用程序,其功能类似于终端窗口。除此之外,由于它在WPF/Silverlight中,因此可以通过效果,图像等“增强”终端体验。

我正在尝试找出模拟终端的最佳方法。我知道在解析等方面如何处理VT100仿真。但是如何显示它呢?我考虑过使用RichTextBox,并将VT100转义码实质上转换为RTF。

我看到的问题是性能。终端一次只能获取几个字符,并且要能够将它们立即加载到文本框中,我将不断创建TextRanges并使用Load()加载RTF。同样,为了使每个加载“ session ”完成,它必须完整地描述RTF。例如,如果当前颜色为红色,则每次加载到TextBox中都将需要RTF代码以使文本变为红色,否则我认为RTB不会将其加载为红色。

这似乎非常多余-由仿真生成的RTF文档将非常困惑。此外,插入符号的移动似乎不太可能由RTB理想地处理。我需要定制的东西,方法,但这吓到我了!

希望听到聪明的主意或指向现有解决方案的指示。也许有一种方法可以嵌入实际的终端并在其上面覆盖内容。我发现的唯一东西是一个旧的WinForms控件。

更新:在下面的我的回答中,请参阅建议的解决方案如何由于性能下降而失败。 :(
VT100 Terminal Emulation in Windows WPF or Silverlight

最佳答案

如果您尝试使用RichTextBox和RTF来实现这一点,那么您将很快遇到许多限制,并且发现自己花在解决差异上的时间要比自己实现该功能花费更多。

实际上,使用WPF实现VT100终端仿真非常容易。我知道,因为刚才我在一个小时左右的时间内就实现了几乎完整的VT100仿真器。确切地说,我实现了除以下内容以外的所有内容:

  • 键盘输入
  • 备用字符集
  • 我从未见过的一些深奥的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;

    }

    请注意,如果您不喜欢视觉样式,则只需更新TerminalCell DataTemplate。例如,光标可能是一个闪烁的矩形,而不是一个实心的矩形。

    这段代码写起来很有趣。希望它将对您有用。由于我从未实际执行过,所以它可能有一个或两个(或三个)错误,但是我希望可以轻松清除这些错误。如果您解决了某些问题,欢迎您对此答案进行编辑。

    关于.net - Windows WPF或Silverlight中的VT100终端仿真,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/3219819/

    28 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com