如何快速将彩色输出写入控制台?

51

我想了解在使用C# .net时,除了使用简单的WriteBackgroundColorForegroundColor方法和属性之外,是否还有其他(更快)输出文本到控制台应用程序窗口的方法?我了解到每个单元格都有一个背景颜色和前景颜色,我想要比使用上述方法更快地缓存/缓冲/写入。

也许可以使用Out缓冲区来获取一些帮助,但我不知道如何将颜色编码到流中,如果颜色数据存储在那里的话。

这是为了实现我想要的复古风格的基于文本的游戏,在其中我将使用标准颜色和ASCII字符来布置游戏。

请帮帮我 :)

更新:

Out和buffer可能并不是我需要处理的内容。控制台拥有一个屏幕缓冲区,我不知道如何访问它,除非导入一些dll库,否则我可能没有办法了。

3个回答

60
更新:添加了一个样例
如果你想做一些P/Invoke的东西,这可能会有所帮助。基本上,如果你获取了控制台缓冲区的句柄,那么你可以使用标准的Win32 API来操作该缓冲区,甚至可以将整个缓冲区构建到屏幕外,并将其复制到控制台。
唯一棘手的部分是获取控制台缓冲区的句柄。我没有在.NET中尝试过这个,但在过去的几年中,你可以使用CreateFile(需要P/Invoke)并打开“CONOUT $”来获取当前控制台的句柄,然后你可以使用返回的句柄传递给其他API。
P/Invoke用于CreateFile
http://www.pinvoke.net/default.aspx/kernel32/CreateFile.html
你可以使用WriteConsoleOutput将所有字符及其属性从内存缓冲区移动到控制台缓冲区。
http://msdn.microsoft.com/en-us/library/ms687404(VS.85).aspx
你可以组合一个不错的库来提供对控制台缓冲区的低级别访问。
由于我正在努力提高我的.NET水平,所以我想试试这个并看看能否使其正常工作。这里有一个样例,它将用所有字母A-Z填充屏幕,并运行所有的前景属性0-15。我认为你会对性能感到印象深刻。老实说,我没有花太多时间审查这段代码,因此错误检查为零,可能还有一些小错误,但应该可以帮助你了解其他API的使用。
using System;
using System.IO;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;

namespace ConsoleApplication1
{
  class Program
  {
    
    [DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
    static extern SafeFileHandle CreateFile(
        string fileName,
        [MarshalAs(UnmanagedType.U4)] uint fileAccess,
        [MarshalAs(UnmanagedType.U4)] uint fileShare,
        IntPtr securityAttributes,
        [MarshalAs(UnmanagedType.U4)] FileMode creationDisposition,
        [MarshalAs(UnmanagedType.U4)] int flags,
        IntPtr template);

    [DllImport("kernel32.dll", SetLastError = true)]
    static extern bool WriteConsoleOutputW(
      SafeFileHandle hConsoleOutput, 
      CharInfo[] lpBuffer, 
      Coord dwBufferSize, 
      Coord dwBufferCoord, 
      ref SmallRect lpWriteRegion);

    [StructLayout(LayoutKind.Sequential)]
    public struct Coord
    {
      public short X;
      public short Y;

      public Coord(short X, short Y)
      {
        this.X = X;
        this.Y = Y;
      }
    };

    [StructLayout(LayoutKind.Explicit)]
    public struct CharUnion
    {
      [FieldOffset(0)] public ushort UnicodeChar;
      [FieldOffset(0)] public byte AsciiChar;
    }

    [StructLayout(LayoutKind.Explicit)]
    public struct CharInfo
    {
      [FieldOffset(0)] public CharUnion Char;
      [FieldOffset(2)] public short Attributes;
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct SmallRect
    {
      public short Left;
      public short Top;
      public short Right;
      public short Bottom;
    }


    [STAThread]
    static void Main(string[] args)
    {
      SafeFileHandle h = CreateFile("CONOUT$", 0x40000000, 2, IntPtr.Zero, FileMode.Open, 0, IntPtr.Zero);

      if (!h.IsInvalid)
      {
        CharInfo[] buf = new CharInfo[80 * 25];
        SmallRect rect = new SmallRect() { Left = 0, Top = 0, Right = 80, Bottom = 25 };

        for (byte character = 65; character < 65 + 26; ++character)
        {
          for (short attribute = 0; attribute < 15; ++attribute)
          {
            for (int i = 0; i < buf.Length; ++i)
            {
              buf[i].Attributes = attribute;
              buf[i].Char.AsciiChar = character;
            }
            
            bool b = WriteConsoleOutputW(h, buf,
              new Coord() { X = 80, Y = 25 },
              new Coord() { X = 0, Y = 0 },
              ref rect);
          }
        }
      }
      Console.ReadKey();
    }
  }
}  

Unicode示例

using Microsoft.Win32.SafeHandles;
using System;
using System.IO;
using System.Runtime.InteropServices;

namespace FastConsole
{
    class Program
    {

        [DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
        static extern SafeFileHandle CreateFile(
            string fileName,
            [MarshalAs(UnmanagedType.U4)] uint fileAccess,
            [MarshalAs(UnmanagedType.U4)] uint fileShare,
            IntPtr securityAttributes,
            [MarshalAs(UnmanagedType.U4)] FileMode creationDisposition,
            [MarshalAs(UnmanagedType.U4)] int flags,
            IntPtr template);

        [DllImport("kernel32.dll", SetLastError = true)]
        static extern bool WriteConsoleOutputW(
          SafeFileHandle hConsoleOutput,
          CharInfo[] lpBuffer,
          Coord dwBufferSize,
          Coord dwBufferCoord,
          ref SmallRect lpWriteRegion);

        [StructLayout(LayoutKind.Sequential)]
        public struct Coord
        {
            public short X;
            public short Y;

            public Coord(short X, short Y)
            {
                this.X = X;
                this.Y = Y;
            }
        };

        [StructLayout(LayoutKind.Explicit)]
        public struct CharUnion
        {
            [FieldOffset(0)] public ushort UnicodeChar;
            [FieldOffset(0)] public byte AsciiChar;
        }

        [StructLayout(LayoutKind.Explicit)]
        public struct CharInfo
        {
            [FieldOffset(0)] public CharUnion Char;
            [FieldOffset(2)] public short Attributes;
        }

        [StructLayout(LayoutKind.Sequential)]
        public struct SmallRect
        {
            public short Left;
            public short Top;
            public short Right;
            public short Bottom;
        }


        [STAThread]
        static void Main(string[] args)
        {
            SafeFileHandle h = CreateFile("CONOUT$", 0x40000000, 2, IntPtr.Zero, FileMode.Open, 0, IntPtr.Zero);

            if (!h.IsInvalid)
            {
                CharInfo[] buf = new CharInfo[80 * 25];
                SmallRect rect = new SmallRect() { Left = 0, Top = 0, Right = 80, Bottom = 25 };

                for (ushort character = 0x2551; character < 0x2551 + 26; ++character)
                {
                    for (short attribute = 0; attribute < 15; ++attribute)
                    {
                        for (int i = 0; i < buf.Length; ++i)
                        {
                            buf[i].Attributes = attribute;
                            buf[i].Char.UnicodeChar = character;
                        }

                        bool b = WriteConsoleOutputW(h, buf,
                          new Coord() { X = 80, Y = 25 },
                          new Coord() { X = 0, Y = 0 },
                          ref rect);
                        Console.ReadKey();
                    }
                }
            }
            Console.ReadKey();
        }
    }
}

2
实际上,GetStdHandle可以替换对CreateFile的调用。 - Chris Taylor
4
CHAR_INFO结构体的成员Attribute,其低四位表示前景色,高四位表示背景色,都在最低有效字节中。例如,如果您想将上面的代码更改为具有绿色背景(调色板颜色2),则可以执行以下操作:buf[i].Attributes = (short)(attribute | (2 << 4)); - Chris Taylor
很棒的文章。我想知道是否可以以某种方式支持Unicode。是否可以调用WriteConsoleOutputW()来实现?或者有其他方法来支持宽字符输出吗? - John Alexiou
@JohnAlexiou,谢谢。我没有尝试过,但理论上你可以设置UnicodeChar来设置Unicode字符。请注意,这是一个相当旧的帖子,Microsoft建议使用VT Sequences https://learn.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences - Chris Taylor
1
@JohnAlexiou - 我有一分钟时间来尝试并对示例进行了一些更改,现在应该正确支持Unicode。我所做的两个更改是:修复了UnicodeChar的数据类型,并更新了代码以使用WriteConsoleOutputW。希望这可以帮到你。 - Chris Taylor
显示剩余9条评论

5
如果你查看Console的属性实现来更改控制台颜色,它们将委托给来自kernel32.dllSetConsoleTextAttribute方法。该方法输入字符属性以设置前景和背景颜色。
根据多个MSDN文档页面,每个屏幕缓冲区(控制台有一个)都有一个二维字符信息记录数组,每个记录由CHAR_INFO表示。这就是每个字符的颜色确定的方式。您可以使用SetConsoleTextAttribute方法来操作它,但这只适用于写入控制台的新文本 - 您无法操作已在控制台上的现有文本。
除非有低级别钩子进入控制台文本颜色属性(这似乎不太可能),否则我认为您必须使用这些方法。
您可以尝试创建一个新的屏幕缓冲区,向其中写入内容,然后使用SetConsoleActiveScreenBuffer将其切换为控制台的当前缓冲区。这样做可能会产生更快的输出,因为您将所有输出写入非活动缓冲区。

我尝试使用SetConsoleActiveScreenBuffer,但效果很差。闪烁没有减少,执行时间翻倍了。我稍后会再次检查我的实现,也许是CreateConsoleScreenBuffer中的设置问题... - CodeOrElse

2
我使用成功了

using (var stdout = Console.OpenStandardOutput(Cols * Rows))
{
    // fill
    stdout.Write(buffer, 0, buffer.Length);
    // rinse and repeat
}

但如果有人能指导我如何将扩展ASCII写入这个地方,我将不胜感激。

控制台支持UTF-8编码,当程序启动时,请设置Console.OutputEncoding = System.Text.Encoding.UTF8;。对于使用WriteConsoleOutput()或WriteConsole()的任何人,请在API调用中添加CharSet = CharSet.Unicode属性。 - Tim

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接