2010-07-10 9 views
7

Estoy pensando en crear una aplicación WPF o Silverlight que funcione como una ventana de terminal. Excepto que, como está en WPF/Silverlight, será capaz de "mejorar" la experiencia de la terminal con efectos, imágenes, etc.Emulación de terminal VT100 en Windows WPF o Silverlight

Estoy tratando de encontrar la mejor manera de emular una terminal. Sé cómo manejar la emulación VT100 en cuanto a análisis, etc. ¿Pero cómo mostrarla? Consideré usar un RichTextBox y esencialmente convertir los códigos de escape de VT100 en RTF.

El problema que veo con eso es el rendimiento. El terminal puede estar recibiendo solo unos pocos caracteres a la vez, y para poder cargarlos en el cuadro de texto mientras creamos, estaré constantemente creando TextRanges y usando Load() para cargar el RTF. Además, para que cada 'sesión' de carga esté completa, debería estar describiendo por completo RTF. Por ejemplo, si el color actual es rojo, cada carga en el cuadro de texto necesitaría los códigos RTF para que el texto sea rojo, o supongo que el RTB no lo cargará como rojo.

Esto parece muy redundante: el documento RTF resultante creado por la emulación será extremadamente desordenado. Además, el movimiento del cursor no parece ser manejado idealmente por el RTB. Necesito algo personalizado, creo, ¡pero eso me asusta!

Esperando escuchar ideas brillantes o consejos para soluciones existentes. Quizás haya una manera de insertar un terminal real y superponer cosas sobre él. Lo único que he encontrado es un viejo control WinForms.

ACTUALIZACIÓN: Ver cómo la solución propuesta falla debido a Perf en mi respuesta a continuación. :(
VT100 Terminal Emulation in Windows WPF or Silverlight

Respuesta

16

Si intenta implementar esto con RichTextBo x y RTF rápidamente se encontrará con muchas limitaciones y se encontrará pasando mucho más tiempo trabajando en torno a las diferencias que si hubiera implementado la funcionalidad usted mismo.

De hecho, es bastante fácil implementar la emulación de terminal VT100 usando WPF. Lo sé porque ahora mismo implementé un emulador VT100 casi completo en una hora más o menos.Para ser precisos, que implmented todo excepto:

  • entrada de teclado,
  • juegos de caracteres alternos,
  • Unos modos VT100 esotéricos nunca he visto aplicar,

Las partes más interesantes fueron:

  • Los caracteres de doble ancho/doble altura, para los cuales utilicé RenderTransform y RenderTransformOrigin
  • El parpadeo, para el que utilicé una animación en un objeto compartido para todos los caracteres parpadean juntos
  • El subrayado, para lo cual utilicé una cuadrícula y un rectángulo para que se viera más como una pantalla VT100
  • El cursor y selección, para los cuales establezco un indicador en las celdas mismas y uso DataTriggers para cambiar la pantalla
  • El uso de una matriz unidimensional y una matriz anidada apuntando a los mismos objetos para facilitar el desplazamiento y selección

Aquí está el 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> 

Y aquí está el código:

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; 

} 

Tenga en cuenta que si no te gusta el estilo visual acaba de actualizar el TerminalCell DataTemplate. Por ejemplo, el cursor podría ser un rectángulo parpadeante en lugar de uno sólido.

Este código fue divertido de escribir. Espero que te sea útil. Probablemente tiene un error o dos (o tres) ya que en realidad nunca lo ejecuté, pero espero que se aclaren fácilmente. Me gustaría recibir una edición de esta respuesta si arreglas algo.

+0

He reparado algunos errores: el VT100 tiene 24 líneas, no 25; necesita restablecer el búfer después de cambiar entre 80 y 132 caracteres por línea; los códigos 7 y 8 guardan/restauran no solo la posición del cursor, sino también los atributos; reinicio necesario para reiniciar todo; algunas cosas faltaban por defecto. – Gabe

+0

Creo que el mayor problema es que creo que has malinterpretado cómo funciona la doble altura/ancho. El modo de duplicación es una propiedad de una línea de almacenamiento intermedio, no un atributo de la celda actual. Por lo tanto, cuando envía el código n. ° 6, el VT100 reduce a la mitad el reloj de píxeles de la línea en la que se encuentra el cursor, lo que hace que cada píxel tenga el doble de ancho. Esto significa que sólo puede tener 40 o 66 caracteres en esa línea, y el cursor no puede avanzar más allá de la posición 40 o 66. – Gabe

+0

También debo señalar que el uso de DataTriggers y DockPanel hacen de este WPF sólo sin un poco de refactorización. – Gabe

1

No entiendo por qué te gustaría estar preocupándose por la RTF conseguir contorneado. Sí que lo hará. Pero no es su carga a tratar con él, un programador de Microsoft hizo que hace un tiempo , tener que escribir el código para renderizar RTF intrincado. Funciona bien y es totalmente opaco para usted.

Sí, no va a ser muy rápido. Pero qué hey, está emulando una pantalla de 80x25 que solía funcionar a 9600 baudios. Reemplazar completamente el control para tratar de hacerlo óptimo tiene poco sentido y va a ser una empresa importante.

+0

Verdadero. Supongo que me preocupa lo lento que podría ser. También el documento podría crecer a un tamaño grande ya que mantendré un buffer. ¿Cuánta memoria va a comer? Supongo que tendré que intentarlo. ¿Qué pasa con el cursor? Quiero que haya uno, pero no para que el usuario pueda moverlo haciendo clic en. Pero sí quiero que puedan resaltar secciones y copiarlo. Es un poco como el modo de solo lectura poco. – InfinitiesLoop

+0

Nunca puede ser demasiado grande, solo tiene que emular una pantalla. Lo que evita que se vuelva lento también. Derive su propia clase de RTB para comer clics del mouse y trazos del teclado. –

+0

Apreciar su opinión. Voy a esperar un poco para ver si puedo obtener otras ideas antes de marcar la respuesta. Si nadie más interviene, supongo que es esto :) – InfinitiesLoop

1

Bueno, para informar de mi estado, he determinado que esto no es realmente factible con WPF o Silverlight.

El problema con el enfoque propuesto es que hay 80 * 24 TextBlocks más algunos otros elementos, con enlaces múltiples para forecolor, backcolor, etc. Cuando la pantalla necesita desplazarse, cada uno de estos enlaces debe ser reevaluado, y es muy, muy lento. Actualizar la pantalla completa toma unos segundos. En mi aplicación no es aceptable, la pantalla se desplazará constantemente.

Probé muchas cosas diferentes para optimizarlo. Intenté usar un bloque de texto con 80 ejecuciones por fila. Traté de poner en orden las notificaciones de cambio. Intenté hacerlo para que un evento 'scroll' actualizara manualmente cada bloque de texto. Nada realmente ayuda: la parte lenta es actualizar la IU, no la forma en que se hace.

Una cosa que me hubiera ayudado es si ideé un mecanismo para no tener un bloque de texto o ejecutar para cada celda, sino solo para cambiar los bloques de texto cuando cambia el estilo del texto. Entonces, una cadena de texto del mismo color, por ejemplo, sería solo 1 bloque de texto. Sin embargo, eso sería muy complicado y, al final, solo ayudaría a los escenarios que tienen poca variación de estilo en la pantalla. Mi aplicación va a tener muchos colores volando (piense en arte de ANSI), por lo que aún sería lento en ese caso.

Otra cosa que pensé que ayudaría es si no actualicé los bloques de texto, sino que los desplacé hacia arriba a medida que la pantalla se desplazaba. Así que los bloques de texto se moverían de arriba a abajo y solo los nuevos necesitarían actualización. Me las arreglé para hacerlo funcionar usando una colección observable. Ayudó, ¡pero TODAVÍA ES DEMASIADO LENTO!

Incluso consideré un control de WPF personalizado utilizando OnRender. Creé uno que usó drawingContext.RenderText de varias maneras para ver qué tan rápido podría ser. Pero AUN ESO es demasiado lento para manejar constantemente la actualización de la pantalla.

Así que por eso .. Yo he renunciado a este diseño. Estoy buscando en su lugar utilizando una ventana de consola real como se describe aquí:

No output to console from a WPF application?

que no me gusta mucho, ya que la ventana está separada de la ventana principal, aunque, por lo que estoy buscando una manera de incrusta la ventana de la consola en la ventana de WPF, si eso es posible. Voy a hacer otra pregunta sobre eso y lo vincularé aquí cuando lo haga.

ACTUALIZACIÓN: La incrustación de la ventana de la consola también falló, porque no se necesita tener su barra de título eliminada. Lo he implementado como un control personalizado de bajo nivel de WinForms, y estoy alojando eso en WPF. Eso funciona muy bien y después de algunas optimizaciones, es muy muy rápido.

1

La única manera de mostrar texto use efectivamente TextFormatter. Implementé el cliente Telnet para juegos de rol basados ​​en texto y funciona bastante bien. Puede consultar las fuentes en http://mudclient.codeplex.com

+0

¿Pero es compatible con la emulación completa? Debe poder imprimir en cualquier celda de la pantalla en cualquier momento, no solo avanzar línea por línea, por ejemplo. – InfinitiesLoop

+0

No, la emulación completa no es compatible, pero no veo ningún problema para agregarla. – petka

+0

Este enfoque funciona solo con WPF. Para Silverlight, creo que debería consultar el soporte de XNA en la última versión. – petka

Cuestiones relacionadas