Jeff B
Jeff B

Reputation: 1060

Trouble getting Unicode character to display when printing to console (using a double buffer)

I have been having an issue when passing in unicode codes into my double buffer. Now I have tried stepping through the code, and I will point out where the unicode character is still correct. (Look at the draw method)

I think the issue lies somewhere in the final "print" method.

To give a quick overview of what happens:
- The buffer is created, allowing the draw function to insert characters into the buffer
- The print function will send the buffer to the console so that it is displayed

From the example I provide, I pass in the unicode character '\u2580', but it prints the ascii character '80'.

Using this link:
http://www.kreativekorp.com/charset/font.php?font=Consolas
I can correctly print out Basic Latin and Latin 1, but nothing else.

After doing further research, I do not believe the issue lies with the console code page. In addition to doing testing switching code pages (which had no effect), I can still do Console.Out.WriteLine("\u2580") and get the correct unicode character.

To provide some additional information... below is the final function that is called (as a result of the print method)

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

The documentation for this can be found here:
https://msdn.microsoft.com/en-us/library/windows/desktop/ms687404(v=vs.85).aspx
Now I can guarantee that when the lpbuffer is passed in, it has a multidimensional array, of which the first value (in this case) is composed of the attribute, and the character - which IS still correct at this time, checked using debugger.

I have provided the full code to allow it to be run.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;
using System.IO;
using System.Text;

namespace DoubleBuffer
{

    ///<summary>
    ///This class allows for a double buffer in Visual C# cmd promt. 
    ///The buffer is persistent between frames.
    ///</summary>
    class buffer
    {
        private int width;
        private int height;
        private int windowWidth;
        private int windowHeight;
        private SafeFileHandle h;
        private CharInfo[] buf;
        private SmallRect rect;

        [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, CharSet = CharSet.Auto)]
        static extern bool WriteConsoleOutput(
          SafeFileHandle hConsoleOutput,
          CharInfo[] lpBuffer,
          Coord dwBufferSize,
          Coord dwBufferCoord,
          ref SmallRect lpWriteRegion);

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

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

        [StructLayout(LayoutKind.Explicit)]
        public struct CharUnion
        {
            [FieldOffset(0)]
            public char 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
        {
            private short Left;
            private short Top;
            private short Right;
            private short Bottom;
            public void setDrawCord(short l, short t)
            {
                Left = l;
                Top = t;
            }
            public void setWindowSize(short r, short b)
            {
                Right = r;
                Bottom = b;
            }
        }

        /// <summary>
        /// Consctructor class for the buffer. Pass in the width and height you want the buffer to be.
        /// </summary>
        /// <param name="Width"></param>
        /// <param name="Height"></param>
        public buffer(int Width, int Height, int wWidth, int wHeight) // Create and fill in a multideminsional list with blank spaces.
        {
            if (Width > wWidth || Height > wHeight)
            {
                throw new System.ArgumentException("The buffer width and height can not be greater than the window width and height.");
            }
            h = CreateFile("CONOUT$", 0x40000000, 2, IntPtr.Zero, FileMode.Open, 0, IntPtr.Zero);
            width = Width;
            height = Height;
            windowWidth = wWidth;
            windowHeight = wHeight;
            buf = new CharInfo[width * height];
            rect = new SmallRect();
            rect.setDrawCord(0, 0);
            rect.setWindowSize((short)windowWidth, (short)windowHeight);
            Clear();
            Console.OutputEncoding = System.Text.Encoding.Unicode;
        }
        /// <summary>
        /// This method draws any text to the buffer with given color.
        /// To chance the color, pass in a value above 0. (0 being black text, 15 being white text).
        /// Put in the starting width and height you want the input string to be.
        /// </summary>
        /// <param name="str"></param>
        /// <param name="x"></param>
        /// <param name="y"></param>
        /// <param name="attribute"></param>
        public void Draw(String str, int x, int y, short attribute) //Draws the image to the buffer
        {
            if (x > windowWidth - 1 || y > windowHeight - 1)
            {
                throw new System.ArgumentOutOfRangeException();
            }
            if (str != null)
            {
                Char[] temp = str.ToCharArray(); //From testing I know the unicode character is still correct here

                int tc = 0;
                foreach (Char le in temp)
                {
                    buf[(x + tc) + (y * width)].Char.UnicodeChar = le; //Height * width is to get to the correct spot (since this array is not two dimensions).
                    System.Console.Out.WriteLine(buf[(x + tc) + (y * width)].Char.UnicodeChar); //once again, a simple test to see if the unicode character is working. Enter debugging and you will see the value is correct.
                    if (attribute != 0)
                        buf[(x + tc) + (y * width)].Attributes = attribute;
                    tc++;
                }
            }


        }
        /// <summary>
        /// Prints the buffer to the screen.
        /// </summary>
        public void Print() //Paint the image
        {
            if (!h.IsInvalid)
            {

                bool b = WriteConsoleOutput(h, buf, new Coord((short)width, (short)height), new Coord((short)0, (short)0), ref rect); //This is the point where I think it is messing up, but I am at a loss at to what is happening.
            }
        }
        /// <summary>
        /// Clears the buffer and resets all character values back to 32, and attribute values to 1.
        /// </summary>
        public void Clear()
        {
            for (int i = 0; i < buf.Length; i++)
            {
                buf[i].Attributes = 1;
                buf[i].Char.UnicodeChar = '\u0020';
            }
        }
        /// <summary>
        /// Pass in a buffer to change the current buffer.
        /// </summary>
        /// <param name="b"></param>
        public void setBuf(CharInfo[] b)
        {
            if (b == null)
            {
                throw new System.ArgumentNullException();
            }

            buf = b;
        }

        /// <summary>
        /// Set the x and y cordnants where you wish to draw your buffered image.
        /// </summary>
        /// <param name="x"></param>
        /// <param name="y"></param>
        public void setDrawCord(short x, short y)
        {
            rect.setDrawCord(x, y);
        }

        /// <summary>
        /// Clear the designated row and make all attribues = 1.
        /// </summary>
        /// <param name="row"></param>
        public void clearRow(int row)
        {
            for (int i = (row * width); i < ((row * width + width)); i++)
            {
                if (row > windowHeight - 1)
                {
                    throw new System.ArgumentOutOfRangeException();
                }
                buf[i].Attributes = 0;
                buf[i].Char.UnicodeChar = '\u0020';
            }
        }

        /// <summary>
        /// Clear the designated column and make all attribues = 1.
        /// </summary>
        /// <param name="col"></param>
        public void clearColumn(int col)
        {
            if (col > windowWidth - 1)
            {
                throw new System.ArgumentOutOfRangeException();
            }
            for (int i = col; i < windowHeight * windowWidth; i += windowWidth)
            {
                buf[i].Attributes = 0;
                buf[i].Char.UnicodeChar = '\u0020';
            }
        }

        /// <summary>
        /// This function return the character and attribute at given location.
        /// </summary>
        /// <param name="x"></param>
        /// <param name="y"></param>
        /// <returns>
        /// byte character
        /// byte attribute
        /// </returns>
        public KeyValuePair<byte, byte> getCharAt(int x, int y)
        {
            if (x > windowWidth || y > windowHeight)
            {
                throw new System.ArgumentOutOfRangeException();
            }
            return new KeyValuePair<byte, byte>((byte)buf[((y * width + x))].Char.UnicodeChar, (byte)buf[((y * width + x))].Attributes);
        }
    }
}

And the example to run is:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DoubleBuffer
{
    class ExampleClass
    {
        static int width = 80;
        static int height = 30;
        public static void Main(string[] args)
        {
            Console.CursorVisible = false;
            Console.Title = "Double buffer example";
            System.Console.SetBufferSize(width, height);
            System.Console.SetWindowSize(width, height);
            Console.Clear();
            buffer myBuf = new buffer(width, height, width, height);
            backgroundbuf.Draw("\u2580", 0, 0, 2);
            myBuf.Print();
            Console.ReadLine();
        }
    }
}

Let me know if I need to provide any more information! (Note: This code is directly from a code example I have on msdn. Was not aware of this bug until now, I want to make sure this is fixed!!!)

Upvotes: 1

Views: 1116

Answers (1)

Jeff B
Jeff B

Reputation: 1060

Here are links that helped me solve this issue!

In addition, the Microsoft Dev Center has some great resources for questions like this as well.

OK! I found the solution! It is actually quite a simple one. First off, I need to get rid of "CharUnion" and just put that in the "CharInfo" (not sure why, but it works - If you know how to fix CharUnion, please post!). In addition, I need to declare CharSet = CharSet.Auto, else it defaults to ascii. (Shout out to whoever posted their answer but deleted it - you had the right idea!). -- new struct CharInfo (replaces struct CharUnion):

[StructLayout(LayoutKind.Explicit, CharSet = CharSet.Auto)]
        public struct CharInfo
        {
            [FieldOffset(0)]
            public char UnicodeChar;
            [FieldOffset(0)]
            public byte bAsciiChar;
            [FieldOffset(2)]
            public short Attributes;
        }

Next, not that I think it changes anything, but instead of creating a new file, I now get a handle on the current output buffer. This removes the need for SafeFileHandle CreateFile.

public const Int32 STD_OUTPUT_HANDLE = -11;

        [DllImportAttribute("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
        public static extern ConsoleHandle GetStdHandle(Int32 nStdHandle);

For ease of use, for any future readers, this is the code I am currently using. Note, there may be some random snippets that have no use to the buffer working, I took this directly out of the project I am working in. You should get the point though.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;
using System.IO;
using System.Text;

namespace DoubleBuffer
{
    /*
     * Copyright [2012] [Jeff R Baker]

     * Licensed under the Apache License, Version 2.0 (the "License");
     * you may not use this file except in compliance with the License.
     * You may obtain a copy of the License at    
     * 
     *          http://www.apache.org/licenses/LICENSE-2.0
     * 
     * Unless required by applicable law or agreed to in writing, software
     * distributed under the License is distributed on an "AS IS" BASIS,
     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     * See the License for the specific language governing permissions and
     * limitations under the License.
     * 
     * v 1.2.0
     */
    ///<summary>
    ///This class allows for a double buffer in Visual C# cmd promt. 
    ///The buffer is persistent between frames.
    ///</summary>
    class buffer
    {
        private int width;
        private int height;
        private int windowWidth;
        private int windowHeight;
        private ConsoleHandle h;
        private CharInfo[] buf;
        private SmallRect rect;

        public const Int32 STD_OUTPUT_HANDLE = -11;

        [DllImportAttribute("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
        public static extern ConsoleHandle GetStdHandle(Int32 nStdHandle);

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

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

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



        [StructLayout(LayoutKind.Explicit, CharSet = CharSet.Auto)]
        public struct CharInfo
        {
            [FieldOffset(0)]
            public char UnicodeChar;
            [FieldOffset(0)]
            public byte bAsciiChar;
            [FieldOffset(2)]
            public short Attributes;
        }


        [StructLayout(LayoutKind.Sequential)]
        public struct SmallRect
        {
            private short Left;
            private short Top;
            private short Right;
            private short Bottom;
            public void setDrawCord(short l, short t)
            {
                Left = l;
                Top = t;
            }
            public short DrawCordX()
            {
                return Left;
            }
            public short DrawCordY()
            {
                return Top;
            }
            public void setWindowSize(short r, short b)
            {
                Right = r;
                Bottom = b;
            }
        }

        /// <summary>
        /// Consctructor class for the buffer. Pass in the width and height you want the buffer to be.
        /// </summary>
        /// <param name="Width"></param>
        /// <param name="Height"></param>
        public buffer(int Width, int Height, int wWidth, int wHeight) // Create and fill in a multideminsional list with blank spaces.
        {
            if (Width > wWidth || Height > wHeight)
            {
                throw new System.ArgumentException("The buffer width and height can not be greater than the window width and height.");
            }
            h = GetStdHandle(STD_OUTPUT_HANDLE);
            width = Width;
            height = Height;
            windowWidth = wWidth;
            windowHeight = wHeight;
            buf = new CharInfo[width * height];
            rect = new SmallRect();
            rect.setDrawCord(0, 0);
            rect.setWindowSize((short)windowWidth, (short)windowHeight);


            Console.OutputEncoding = System.Text.Encoding.Unicode;
            Clear();

        }

        /// <summary>
        /// This method draws any text to the buffer with given color.
        /// To chance the color, pass in a value above 0. (0 being black text, 15 being white text).
        /// Put in the starting width and height you want the input string to be.
        /// </summary>
        /// <param name="str"></param>
        /// <param name="x"></param>
        /// <param name="y"></param>
        /// <param name="attribute"></param>
        public void Draw(String str, int x, int y, short attribute) //Draws the image to the buffer
        {

            if (x > windowWidth - 1 || y > windowHeight - 1)
            {
                throw new System.ArgumentOutOfRangeException();
            }
            if (str != null)
            {
                Char[] temp = str.ToCharArray();

                int tc = 0;
                foreach (Char le in temp)
                {
                    buf[(x + tc) + (y * width)].UnicodeChar = le; //Height * width is to get to the correct spot (since this array is not two dimensions).

                    if (attribute != 0)
                        buf[(x + tc) + (y * width)].Attributes = attribute;
                    tc++;


                }
            }


        }
        /// <summary>
        /// Prints the buffer to the screen.
        /// </summary>
        public void Print() //Paint the image
        {
            if (!h.IsInvalid)
            {

                bool b = WriteConsoleOutput(h, buf, new Coord((short)width, (short)height), new Coord((short)0, (short)0), ref rect);
            }
        }
        /// <summary>
        /// Clears the buffer and resets all character values back to 32, and attribute values to 1.
        /// </summary>
        public void Clear()
        {
            for (int i = 0; i < buf.Length; i++)
            {
                buf[i].Attributes = 1;
                buf[i].UnicodeChar = '\u0020';
            }
        }
        /// <summary>
        /// Pass in a buffer to change the current buffer.
        /// </summary>
        /// <param name="b"></param>
        public void setBuf(CharInfo[] b)
        {
            if (b == null)
            {
                throw new System.ArgumentNullException();
            }

            buf = b;
        }

        /// <summary>
        /// Set the x and y cordnants where you wish to draw your buffered image.
        /// </summary>
        /// <param name="x"></param>
        /// <param name="y"></param>
        public void setDrawCord(short x, short y)
        {
            rect.setDrawCord(x, y);
        }

        /// <summary>
        /// Clear the designated row and make all attribues = 1.
        /// </summary>
        /// <param name="row"></param>
        public void clearRow(int row)
        {
            for (int i = (row * width); i < ((row * width + width)); i++)
            {
                if (row > windowHeight - 1)
                {
                    throw new System.ArgumentOutOfRangeException();
                }
                buf[i].Attributes = 0;
                buf[i].UnicodeChar = '\u0020';
            }
        }

        /// <summary>
        /// Clear the designated column and make all attribues = 1.
        /// </summary>
        /// <param name="col"></param>
        public void clearColumn(int col)
        {
            if (col > windowWidth - 1)
            {
                throw new System.ArgumentOutOfRangeException();
            }
            for (int i = col; i < windowHeight * windowWidth; i += windowWidth)
            {
                buf[i].Attributes = 0;
                buf[i].UnicodeChar = '\u0020';
            }
        }

        /// <summary>
        /// This function return the character and attribute at given location.
        /// </summary>
        /// <param name="x"></param>
        /// <param name="y"></param>
        /// <returns>
        /// byte character
        /// byte attribute
        /// </returns>
        public KeyValuePair<byte, byte> getCharAt(int x, int y)
        {
            if (x > windowWidth || y > windowHeight)
            {
                throw new System.ArgumentOutOfRangeException();
            }
            return new KeyValuePair<byte, byte>((byte)buf[((y * width + x))].UnicodeChar, (byte)buf[((y * width + x))].Attributes);
        }

        public class ConsoleHandle : SafeHandleMinusOneIsInvalid
        {
            public ConsoleHandle() : base(false) { }

            protected override bool ReleaseHandle()
            {
                return true; //releasing console handle is not our business
            }
        }

        public int X
        {
            get { return width; }
        }

        public int Y
        {
            get { return height; }
        }

        public int dX
        {
            get { return rect.DrawCordX(); }
        }

        public int dY
        {
            get { return rect.DrawCordY(); }
        }
    }
}

Any changes to this may be updated on the "Console Double Buffer C#" on MSDN.

Upvotes: 2

Related Questions