Pergunta

Estou pensando em criar um aplicativo WPF ou Silverlight que funcione como uma janela de terminal.Exceto que, por estar em WPF/Silverlight, será capaz de 'aprimorar' a experiência do terminal com efeitos, imagens, etc.

Estou tentando descobrir a melhor maneira de emular um terminal.Eu sei como lidar com a emulação do VT100 no que diz respeito à análise, etc.Mas como exibi-lo?Considerei usar um RichTextBox e essencialmente converter os códigos de escape do VT100 em RTF.

O problema que vejo com isso é o desempenho.O terminal pode receber apenas alguns caracteres por vez e, para poder carregá-los na caixa de texto à medida que avançamos, eu estaria constantemente criando TextRanges e usando Load() para carregar o RTF.Além disso, para que cada 'sessão' de carregamento seja concluída, ela teria que descrever completamente o RTF.Por exemplo, se a cor atual for Vermelho, cada carregamento no TextBox precisaria dos códigos RTF para tornar o texto vermelho, ou presumo que o RTB não o carregará como vermelho.

Isto parece muito redundante - o documento RTF resultante criado pela emulação será extremamente confuso.Além disso, o movimento do cursor não parece ser tratado de maneira ideal pelo RTB.Preciso de algo personalizado, acho, mas isso me assusta!

Na esperança de ouvir ideias brilhantes ou sugestões para soluções existentes.Talvez haja uma maneira de incorporar um terminal real e sobrepor coisas sobre ele.A única coisa que encontrei é um controle antigo do WinForms.

ATUALIZAR: Veja como a solução proposta falha devido ao desempenho na minha resposta abaixo.:(
Emulação de terminal VT100 no Windows WPF ou Silverlight

Foi útil?

Solução

Se você tentar implementar isso com RichTextBox e RTF, você rapidamente encontrará muitas limitações e gastará muito mais tempo resolvendo diferenças do que se você mesmo implementasse a funcionalidade.

Na verdade, é muito fácil implementar a emulação de terminal VT100 usando WPF.Eu sei porque agora implementei um emulador VT100 quase completo em cerca de uma hora.Para ser mais preciso, implementei tudo, exceto:

  • Entrada de teclado,
  • Conjuntos de caracteres alternativos,
  • Alguns modos esotéricos do VT100 que nunca vi usados,

As partes mais interessantes foram:

  • Os caracteres de largura dupla/altura dupla, para os quais usei RenderTransform e RenderTransformOrigin
  • O piscar, para o qual usei uma animação em um objeto compartilhado para que todos os personagens pisquem juntos
  • O sublinhado, para o qual usei uma grade e um retângulo para que ficasse mais parecido com um display VT100
  • O cursor e a seleção, para os quais defino um sinalizador nas próprias células e uso DataTriggers para alterar a exibição
  • O uso de uma matriz unidimensional e de uma matriz aninhada apontando para os mesmos objetos para facilitar a rolagem e a seleção

Aqui está o 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>

E aqui está o 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;

}

Observe que se você não gostar do estilo visual, basta atualizar o TerminalCell DataTemplate.Por exemplo, o cursor pode ser um retângulo piscante em vez de um retângulo sólido.

Este código foi divertido de escrever.Esperamos que seja útil para você.Provavelmente tem um bug ou dois (ou três), já que nunca o executei, mas espero que eles sejam facilmente resolvidos.Eu gostaria de editar esta resposta se você corrigir alguma coisa.

Outras dicas

A única maneira de exibir efetivamente o texto é usar o TextFormatter. Eu implementei o Telnet Client para jogos de RPG baseados em texto e funciona muito bem. Você pode verificar fontes em http://mudclient.codeplex.com

Eu não entendo porque você estaria se preocupando com o RTF sendo convolutado.Sim vai.Mas não é o seu fardo para lidar com isso, um programador da Microsoft fez isso há algum tempo, tendo que escrever o código para tornar o RTF Convolutado.Funciona bem e é totalmente opaco com você.

Sim, não será super rápido.Mas o que é o hey, você está emulando uma exibição de 80x25 que costumava ser executada em 9600 baud.Substituir completamente o controle tentar torná-lo ideal faz pouco sentido e será uma empresa importante.

Bem, para relatar meu status, determinei que isso não é realmente viável com WPF ou Silverlight .

O problema com a abordagem proposta é que existem 80 * 24 textblocks mais alguns outros elementos, com várias ligações para Forefólio, BackColor, etc. Quando a tela precisa rolar, cada uma dessas ligações deve ser reavaliada e é muito, muito lento. A atualização da tela inteira leva alguns segundos. No meu aplicativo, isso não é aceitável, a tela vai ser rolando constantemente.

Eu tentei muitas coisas diferentes para otimizá-lo. Eu tentei usar um bloco de texto com 80 execuções cada por linha. Eu tentei lotar as notificações de mudança. Eu tentei fazer isso, um evento 'scroll' atualizei manualmente cada texto. Nada realmente ajuda - a parte lenta está atualizando a interface do usuário, não a maneira como é feita.

Uma coisa que teria ajudado é se eu invisei um mecanismo para não ter um bloco de texto ou executar para cada célula, mas apenas alterar os blocos de texto quando o estilo do texto é alterado. Portanto, uma string de texto entre o mesmo modo, por exemplo, seria apenas 1 bloco de texto. No entanto, isso seria muito complicado, e no final, apenas ajudaria os cenários que tenham pouca variação de estilo na tela. Meu aplicativo vai ter muitas cores voando por (pense ANSI Art), então ainda seria lento nesse caso.

Outra coisa que eu pensei que ajudaria é se eu não atualizei os blocos de texto, mas os rolava como a tela rolada. Portanto, os textblocks estariam se movendo a partir do topo para baixo e, em seguida, apenas os novos precisariam atualizar. Eu consegui trabalhar usando uma coleção observável. Ajudou, mas ainda é muito lento!

Eu até mesmo considerei um controle personalizado do WPF usando OneRender. Eu criei um que usou DrawingContext.RendText de várias maneiras de ver o quão rápido poderia ser. Mas mesmo isso é muito lento para lidar constantemente a atualização da tela.

Então é isso .. Eu desisti desse design. Eu estou procurando usando uma janela de console real como descrito aqui:

Nenhuma saída para consolar de um aplicativo WPF?

Eu realmente não gosto disso, já que a janela é separada da janela principal, então estou procurando uma maneira de incorporar a janela do console na janela do WPF, se for possível. Eu vou estar perguntando a outra coisa sobre isso e linká-lo aqui quando eu faço.

update: incorporando a janela do console falhou, também, porque não é gentilmente para ter sua barra de título removida. Eu implementei como um controle de winforms personalizado de pintura de baixo nível e estou hospedando isso no WPF. Que funciona lindamente e depois de algumas otimizações, é muito rápido.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top