【问题标题】:VT100 Terminal Emulation in Windows WPF or SilverlightWindows WPF 或 Silverlight 中的 VT100 终端仿真
【发布时间】:2011-03-14 06:51:25
【问题描述】:

我正在考虑创建一个类似于终端窗口的 WPF 或 Silverlight 应用程序。除了,因为它在 WPF/Silverlight 中,它将能够通过效果、图像等“增强”终端体验。

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

我看到的问题是性能。终端可能一次只能获取几个字符,并且能够随时将它们加载到文本框中,我将不断创建 TextRanges 并使用 Load() 加载 RTF。此外,为了完成每个加载“会话”,它必须完全描述 RTF。例如,如果当前颜色为红色,则每次加载到 TextBox 中都需要 RTF 代码使文本变为红色,或者我假设 RTB 不会将其加载为红色。

这似乎非常多余——由仿真生成的 RTF 文档将非常混乱。此外,插入符号的移动似乎不会由 RTB 理想地处理。我需要一些定制的东西,我想,但这让我害怕!

希望听到关于现有解决方案的好主意或建议。也许有一种方法可以嵌入一个实际的终端并在它上面覆盖一些东西。我发现的唯一东西是一个旧的 WinForms 控件。

更新:在下面我的回答中查看建议的解决方案如何因性能而失败。 :(
VT100 Terminal Emulation in Windows WPF or Silverlight

【问题讨论】:

    标签: .net wpf silverlight terminal vt100


    【解决方案1】:

    如果您尝试使用 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。例如,光标可能是一个闪烁的矩形,而不是一个实心的。

    这段代码写起来很有趣。希望它对你有用。它可能有一两个(或三个)错误,因为我从未真正执行过它,但我希望这些会很容易清除。如果您解决问题,我欢迎对此答案进行编辑。

    【讨论】:

    • 我修复了一些错误:VT100 有 24 行,而不是 25;您需要在每行更改 80 到 132 个字符后重置缓冲区;代码 7 和 8 不仅保存/恢复光标位置,还保存/恢复属性; reset 需要重置一切;有些东西缺少默认值。
    • 我认为最大的问题是我相信您误解了双倍高度/宽度的工作原理。倍增模式是缓冲线的属性,而不是当前单元格的属性。因此,当您发送#6 代码时,VT100 将光标所在行的像素时钟减半,从而使每个像素的宽度增加一倍。这意味着您在该行上只能有 40 或 66 个字符,并且光标不能前进到位置 40 或 66。
    • 我还应该注意,DataTriggers 和 DockPanel 的使用使得这个 WPF-only 没有一点重构。
    • 哇 :) 真的很有趣!由于我仍然是 wpf 新手,因此我需要一段时间来消化这一点。我会说虽然我已经在 RTB 中尝试过它作为概念证明,是的,我很快就会遇到限制。首先是我最初的恐惧——它很慢!一次写入几个字符(当它们进入时)会占用我的 CPU 到 50% 并且需要很长时间来处理它,就像再次使用 1200 波特调制解调器一样。这种技术必须要快很多。
    • “我用脚趾而不是手指在几分钟内写了这个。我什至不需要编译一次,因为我就是那么棒。”说真的,看起来你花了 1 个多小时在上面。也许你已经用类似的语言写过类似的东西?
    【解决方案2】:

    有效显示文本的唯一方法是使用 TextFormatter。 我已经为基于文本的 RPG 游戏实现了 telnet 客户端,它运行良好。 您可以在http://mudclient.codeplex.com查看来源

    【讨论】:

    • 但是它支持完全仿真吗?例如,您需要能够随时打印到屏幕上的任何单元格,而不仅仅是逐行向前。
    • 不,不支持完全仿真,但我认为添加它没有任何问题。
    • 此方法仅适用于 WPF。对于 Silverlight,我想你应该看看最新版本的 XNA 支持。
    【解决方案3】:

    我不明白您为什么会担心 RTF 变得令人费解。是的,它会。但处理它不是你的负担,微软程序员前一段时间做过,必须编写代码来渲染复杂的 RTF。它运作良好,对您来说完全不透明。

    是的,它不会超快。但是,嘿,您正在模拟一个曾经以 9600 波特率运行的 80x25 显示器。完全更换控件以使其达到最佳状态几乎没有意义,这将是一项重大任务。

    【讨论】:

    • 是的。我想我担心它会有多慢。此外,由于我将保留缓冲区,因此该文档可能会增长到很大。它要吃多少内存?我想我只需要尝试一下。光标呢?我希望有一个,但不希望用户能够通过单击来移动它。但我确实希望他们能够突出显示部分并复制它。这有点像只读模式有点不完全。
    • 它永远不会变得太大,你只需要模拟一个屏幕。这也可以防止它变慢。从 RTB 派生出您自己的类来吃掉鼠标点击和键盘敲击。
    • 感谢您的意见。在标记答案之前,我会稍等一下,看看我是否可以得到任何其他想法。如果没有其他人插话,我想就是这样:)
    • 哦,我真的不想吃鼠标点击,因为我仍然希望他们能够选择文本并复制它,或者有一个上下文菜单。像这样的小事情让我觉得 RTB 不是一个合适的工具,尽管它已经尽可能接近了,而无需重新开始。
    • 好的,然后跟踪虚拟终端光标位置,与实际插入符号位置分开。如果您也接受输入,则在获得额外输出时确实需要将插入符号移回。也许您可以使用当您看到任何鼠标点击时重新触发的计时器。 VT100 不支持用鼠标复制文本 :)
    【解决方案4】:

    好吧,为了报告我的状态,我已经确定这对于 WPF 或 Silverlight 并不真正可行

    提出的方法的问题是有80*24的TextBlocks加上一些其他元素,前景色、背景色等有多个绑定。当屏幕需要滚动时,这些绑定中的每一个都必须重新计算,并且它非常非常慢。更新整个屏幕需要几秒钟。在我的应用程序中这是不可接受的,屏幕将不断滚动。

    我尝试了很多不同的方法来优化它。我尝试使用一个文本块,每行运行 80 次。我尝试批处理更改通知。我尝试将其设置为手动更新每个文本块的“滚动”事件。没有什么真正有帮助的——缓慢的部分是更新 UI,而不是它的完成方式。

    如果我设计了一种机制,而不是为每个单元格运行一个文本块或运行,而只在文本样式发生变化时更改文本块,那将会有所帮助。因此,例如,一串相同颜色的文本将只有 1 个文本块。但是,这会非常复杂,最终只会帮助屏幕上风格变化不大的场景。我的应用程序会有很多颜色飞过(想想 ANSI 艺术),所以在这种情况下它仍然会很慢。

    我认为会有所帮助的另一件事是,如果我不更新文本块,而是在屏幕滚动时向上滚动它们。所以文本块将从顶部移动到底部,然后只有新的需要更新。我设法通过使用可观察的集合来实现这一点。它有帮助,但它仍然太慢了!

    我什至考虑过使用 OnRender 的自定义 WPF 控件。我创建了一个以各种方式使用drawingContext.RenderText 来查看它的速度有多快。但是即使这样也太慢了,无法处理不断更新的屏幕。

    就是这样......我已经放弃了这个设计。相反,我正在考虑使用此处描述的实际控制台窗口:

    No output to console from a WPF application?

    我不太喜欢这样,因为窗口与主窗口是分开的,所以我正在寻找一种将控制台窗口嵌入 WPF 窗口的方法,如果可能的话。我将就此提出另一个 SO 问题,并在我这样做时将其链接到此处。

    更新:嵌入控制台窗口也失败了,因为它的标题栏被移除了。我已经将它实现为一个低级的绘画自定义 WinForms 控件,并且我将它托管在 WPF 中。效果很好,经过一些优化后,速度非常快。

    【讨论】:

      猜你喜欢
      • 2021-02-05
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-12-02
      • 2016-08-08
      • 2015-11-10
      • 2011-08-29
      • 1970-01-01
      相关资源
      最近更新 更多