tags:

views:

1720

answers:

6

I have a program that monitors debug messages and I have tried using a TextBox and appended the messages to it but it doesn't scale very well and slows way down when the number of messages gets large. I then tried a ListBox but the scrolling was snapping to the top when appending new messages. It also doesn't allow for cut and paste like the text box does.

What is a better way to implement a console like element embedded in a winforms window.

Edit: I would still like to be able to embed a output window like visual studio but since I can't figure out an easy way here are the two solutions I use. In addition to using the RichTextBox which works but you have to clear it every now and then. I use a console that I pinvoke. Here is a little wrapper class that I wrote to handle this.


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;

namespace Con
{
   class Ext_Console 
   {
      static bool console_on = false;

      public static void Show(bool on,string title)
      {
         console_on = on;
         if (console_on)
         {
            AllocConsole();
            Console.Title = title;
            // use to change color
            Console.BackgroundColor = System.ConsoleColor.White;
            Console.ForegroundColor = System.ConsoleColor.Black;

         }
         else
         {
            FreeConsole();
         }
      }

      public static void Write(string output)
      {
         if (console_on)
         {
            Console.Write(output);
         }
      }

      public static void WriteLine(string output)
      {
         if (console_on)
         {
            Console.WriteLine(output);
         }
      }

      [DllImport("kernel32.dll")]
      public static extern Boolean AllocConsole();
      [DllImport("kernel32.dll")]
      public static extern Boolean FreeConsole();
   }
}


// example calls
Ext_Console.Write("console output  ");
Ext_Console.WriteLine("console output");
Ext_Console.Show(true,"Title of console");


+1  A: 

set the selectedindex of the listbox to the last element to make it scroll to the bottom

also, limit the number of items in the listbox to something reasonable (delete from the top, keep the later items) so you don't chew up all of your memory

Steven A. Lowe
+6  A: 

RichTextBox has an AppendText method that is fast. And it can handle large text well.
I believe it is the best for what you need.

Ovidiu Pacurar
Large is not infinite. If used for logging/debug messages, it will eventually get bogged down and start thrashing.
dbkk
+1  A: 

Ive previously used a textbox. Add it to your form, set Multipline property to true, Scrollbars to Vertical. And finally add this following code:

    private void AddConsoleComment(string comment)
    {
        textBoxConsole.Text += comment + System.Environment.NewLine;
        textBoxConsole.Select(textBoxConsole.Text.Length,0);
        textBoxConsole.ScrollToCaret();
    }

Essentially its adding your comment to the existing text, also appending a linefeed. And finally selecting last bit of text of length = 0. ScrollToCaret forces the textbox to scroll down to where the cursor is positioned (at the last line)

Hope this helps.

I will be slower with each line added.
Ovidiu Pacurar
I've done this. Performance is terrible.
Corey Trager
People who have voted this up have not actually tried it.
Corey Trager
True... so true...
Ovidiu Pacurar
+1  A: 

I've had this exact challenge. I've solved it two different ways, both work and perform will under heavy load. One way is with a ListView. Adding a line of text is like this:

        ListViewItem itm = new ListViewItem();
        itm.Text = txt;
        this.listView1.Items.Add(itm);
        this.listView1.EnsureVisible(listView1.Items.Count - 1);

The other way is with a DataGridView in virtual mode. I don't have that code as handy. Virtual mode is your friend.

EDIT: re-reading, I see you want copy/paste to work. Maybe the RichText control performs ok - don't know, but if you use the ListView or DataGrid, you'd have to do more coding to get Copy/Paste to work.

Corey Trager
+2  A: 

I do this in my C# window programs (WInforms or WPF) using a Win32 console window. I have a small class that wraps some basic Win32 APIs, thin I create a console when the program begins. This is just an example: in 'real life' you'd use a setting or some other thing to only enable the console when you needed it.

using System; using System.Windows.Forms; using Microsoft.Win32.SafeHandles; using System.Diagnostics; using MWin32Api;

namespace WFConsole { static class Program { static private SafeFileHandle ConsoleHandle;

    /// <summary>
    /// Initialize the Win32 console for this process.
    /// </summary>
    static private void InitWin32Console()
    {
        if ( !K32.AllocConsole() ) {
            MessageBox.Show( "Cannot allocate console",
                             "Error",
                             MessageBoxButtons.OK,
                             MessageBoxIcon.Error );
            return;
        }

        IntPtr handle = K32.CreateFile(
                             "CONOUT$",                                    // name
                             K32.GENERIC_WRITE | K32.GENERIC_READ,         // desired access
                             K32.FILE_SHARE_WRITE | K32.FILE_SHARE_READ,   // share access
                             null,                                         // no security attributes
                             K32.OPEN_EXISTING,                            // device already exists
                             0,                                            // no flags or attributes
                             IntPtr.Zero );                                // no template file.

        ConsoleHandle = new SafeFileHandle( handle, true );

        if ( ConsoleHandle.IsInvalid ) {
            MessageBox.Show( "Cannot create diagnostic console",
                             "Error",
                             MessageBoxButtons.OK,
                             MessageBoxIcon.Error );
            return;
        }

        //
        // Set the console screen buffer and window to a reasonable size
        //  1) set the screen buffer sizse
        //  2) Get the maximum window size (in terms of characters) 
        //  3) set the window to be this size
        //
        const UInt16 conWidth     = 256;
        const UInt16 conHeight    = 5000;

        K32.Coord dwSize = new K32.Coord( conWidth, conHeight );
        if ( !K32.SetConsoleScreenBufferSize( ConsoleHandle.DangerousGetHandle(), dwSize ) ) {
            MessageBox.Show( "Can't get console screen buffer information.",
                             "Error",
                             MessageBoxButtons.OK,
                             MessageBoxIcon.Error );
            return;
        }

        K32.Console_Screen_Buffer_Info SBInfo = new K32.Console_Screen_Buffer_Info();
        if ( !K32.GetConsoleScreenBufferInfo( ConsoleHandle.DangerousGetHandle(), out SBInfo ) ) {
            MessageBox.Show( "Can't get console screen buffer information.",
                             "Error",
                             MessageBoxButtons.OK,
                             MessageBoxIcon.Exclamation);
            return;
        }

        K32.Small_Rect sr; ;
        sr.Left = 0;
        sr.Top = 0;
        sr.Right = 132 - 1;
        sr.Bottom = 51 - 1;

        if ( !K32.SetConsoleWindowInfo( ConsoleHandle.DangerousGetHandle(), true, ref sr ) ) {
            MessageBox.Show( "Can't set console screen buffer information.",
                             "Error",
                             MessageBoxButtons.OK,
                             MessageBoxIcon.Error );
            return;
        }

        IntPtr conHWND = K32.GetConsoleWindow();

        if ( conHWND == IntPtr.Zero ) {
            MessageBox.Show( "Can't get console window handle.",
                             "Error",
                             MessageBoxButtons.OK,
                             MessageBoxIcon.Error );
            return;
        }

        if ( !U32.SetForegroundWindow( conHWND ) ) {
            MessageBox.Show( "Can't set console window as foreground.",
                             "Error",
                             MessageBoxButtons.OK,
                             MessageBoxIcon.Error );
            return;
        }

        K32.SetConsoleTitle( "Test - Console" );

        Trace.Listeners.Add( new ConsoleTraceListener() );
    }

    /// <summary>
    /// The main entry point for the application.
    /// </summary>
    [STAThread]
    static void Main()
    {
        InitWin32Console();
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault( false );
        Application.Run( new Main() );
    }
}

}

using System; using System.Runtime.InteropServices;

namespace MWin32Api { #region Kernel32 Functions

//--------------------------------------------------------------------------
/// <summary>
/// Functions in Kernel32.dll
/// </summary>
public sealed class K32
{
    #region Data Structures, Types and Constants
    //----------------------------------------------------------------------
    // Data Structures, Types and Constants
    // 

    [StructLayout( LayoutKind.Sequential )]
    public class SecurityAttributes
    {
        public UInt32  nLength;
        public UIntPtr lpSecurityDescriptor;
        public bool    bInheritHandle;
    }

    [StructLayout( LayoutKind.Sequential, Pack = 1, Size = 4 )]
    public struct Coord
    {
        public Coord( UInt16 tx, UInt16 ty )
        {
            x = tx;
            y = ty;
        }
        public UInt16 x;
        public UInt16 y;
    }

    [StructLayout( LayoutKind.Sequential, Pack = 1, Size = 8 )]
    public struct Small_Rect
    {
        public Int16 Left;
        public Int16 Top;
        public Int16 Right;
        public Int16 Bottom;

        public Small_Rect( short tLeft, short tTop, short tRight, short tBottom )
        {
            Left = tLeft;
            Top = tTop;
            Right = tRight;
            Bottom = tBottom;
        }
    }

    [StructLayout( LayoutKind.Sequential, Pack = 1, Size = 24 )]
    public struct Console_Screen_Buffer_Info
    {
        public Coord      dwSize;
        public Coord      dwCursorPosition;
        public UInt32     wAttributes;
        public Small_Rect srWindow;
        public Coord      dwMaximumWindowSize;
    }


    public const int ZERO_HANDLE_VALUE = 0;
    public const int INVALID_HANDLE_VALUE = -1;

    #endregion
    #region Console Functions
    //----------------------------------------------------------------------
    // Console Functions
    // 
    [DllImport( "kernel32.dll", SetLastError = true )]
    public static extern bool AllocConsole();

    [DllImport( "kernel32.dll", SetLastError = true )]
    public static extern bool SetConsoleScreenBufferSize(
        IntPtr hConsoleOutput,
        Coord dwSize );

    [DllImport( "kernel32.dll", SetLastError = true )]
    public static extern bool GetConsoleScreenBufferInfo(
        IntPtr hConsoleOutput,
        out Console_Screen_Buffer_Info lpConsoleScreenBufferInfo );

    [DllImport( "kernel32.dll", SetLastError = true )]
    public static extern bool SetConsoleWindowInfo(
        IntPtr hConsoleOutput,
        bool bAbsolute,
        ref Small_Rect lpConsoleWindow );

    [DllImport( "kernel32.dll", SetLastError = true )]
    public static extern IntPtr GetConsoleWindow();

    [DllImport( "kernel32.dll", SetLastError = true )]
    public static extern bool SetConsoleTitle(
        string Filename );

    #endregion
    #region Create File
    //----------------------------------------------------------------------
    // Create File
    // 
    public const UInt32 CREATE_NEW          = 1;
    public const UInt32 CREATE_ALWAYS       = 2;
    public const UInt32 OPEN_EXISTING       = 3;
    public const UInt32 OPEN_ALWAYS         = 4;
    public const UInt32 TRUNCATE_EXISTING   = 5;
    public const UInt32 FILE_SHARE_READ     = 1;
    public const UInt32 FILE_SHARE_WRITE    = 2;
    public const UInt32 GENERIC_WRITE       = 0x40000000;
    public const UInt32 GENERIC_READ        = 0x80000000;

    [DllImport( "kernel32.dll", SetLastError = true )]
    public static extern IntPtr CreateFile(
        string Filename,
        UInt32 DesiredAccess,
        UInt32 ShareMode,
        SecurityAttributes SecAttr,
        UInt32 CreationDisposition,
        UInt32 FlagsAndAttributes,
        IntPtr TemplateFile );

    #endregion
    #region Win32 Miscelaneous
    //----------------------------------------------------------------------
    // Miscelaneous
    // 
    [DllImport( "kernel32.dll" )]
    public static extern bool CloseHandle( UIntPtr handle );

    #endregion

    //----------------------------------------------------------------------
    private K32()
    {
    }
}
#endregion

//--------------------------------------------------------------------------
/// <summary>
/// Functions in User32.dll
/// </summary>
#region User32 Functions
public sealed class U32
{
    [StructLayout( LayoutKind.Sequential )]
    public struct Rect
    {
        public Int32 Left;
        public Int32 Top;
        public Int32 Right;
        public Int32 Bottom;

        public Rect( short tLeft, short tTop, short tRight, short tBottom )
        {
            Left = tLeft;
            Top = tTop;
            Right = tRight;
            Bottom = tBottom;
        }
    }

    [DllImport( "user32.dll" )]
    public static extern bool GetWindowRect(
        IntPtr hWnd,
        [In][MarshalAs( UnmanagedType.LPStruct )]Rect lpRect );

    [DllImport( "user32.dll", SetLastError = true )]
    public static extern bool SetForegroundWindow(
        IntPtr hWnd );

    //----------------------------------------------------------------------
    private U32()
    {
    }
} // U32 class
#endregion

} // MWin32Api namespace

Foredecker
+2  A: 

You can't just keep adding logging items to a WinForms control (ListBox or RichTextBox) -- it will eventually get clogged and start swapping to disk.

I had this exact bug at one point. The solution I had was to clip the list of displayed messages occasionally. In pseudocode, this is something like:

void AddLogMessage(String message)
{
    list.Items.Add(message);

    // DO: Append message to file as needed

    // Clip the list
    if (list.count > ListMaxSize)
    {            
        list.Items.RemoveRange(0, list.Count - listMinSize);
    }

    // DO: Focus the last item on the list
}

ListMaxSize should be substantially bigger than ListMinSize, so clipping does not happen too often. ListMinSize is the number of recent messages that you'd normally need to look through in your logging list.

This is just pseudocode, there is actually no RemoveRange on ListBox item collection (but there is on List). You can figure out the exact code.

dbkk