Getting all you can out of a keyboard layout, Part #7

by Michael S. Kaplan, published on 2006/04/06 04:01 -04:00, original URI: http://blogs.msdn.com/b/michkap/archive/2006/04/06/569632.aspx


(Previous posts in this series: Parts 0, 1, 2, 3, 4, 5, and 6)

Ok, I have been stalling enough; the time has come to dive into getting the dead keys. Not just getting the dead keys, but the actual defined base characters that go with them to create defined composed characters.

This is code that is very self-conscious about its state; I have a small procedure named ClearKeyboardBuffer, which is defined below:

private static void ClearKeyboardBuffer(uint vk, uint sc, IntPtr hkl) {
    StringBuilder sb = new StringBuilder(10);
    int rc = 0;
    while(rc != 1) {
        rc = ToUnicodeEx(vk, sc, lpKeyStateNull, sb, sb.Capacity, 0, hkl);
    }
}

I am reasonably certain that this procedure is called way more often than it has to be; an analagous problem exists in MSKLC. I have found, however, that any time I try to remove one of the calls it messes up the reading of the keyboard. Since there is no harm as long as it is called with a safe VK value, I am not going to let this worry me....

(If you look back at prior posts, you can find at least one case where a bug occurs due to not calling this function often enough!)

Another thing this latest version of the code does is save the keyboard state and dump it out at the end, rather than the prior versions that were dumping information with each shift state. Kind of important given how much interim processing has to be done.

using System;
using System.Text;
using System.Collections;
using System.Windows.Forms;
using System.Runtime.InteropServices;

namespace KeyboardLayouts {

    public enum ShiftState : int {
        Base            = 0,                    // 0
        Shft            = 1,                    // 1
        Ctrl            = 2,                    // 2
        ShftCtrl        = Shft | Ctrl,          // 3
        Menu            = 4,                    // 4 -- NOT USED
        ShftMenu        = Shft | Menu,          // 5 -- NOT USED
        MenuCtrl        = Menu | Ctrl,          // 6
        ShiftMenuCtrl   = Shft | Menu | Ctrl,   // 7
    }


    //  You'll want to insert that enumeration from part #0 here!

    public class DeadKey {
        private char m_deadchar;
        private ArrayList m_rgbasechar = new ArrayList();
        private ArrayList m_rgcombchar = new ArrayList();

        public DeadKey(char deadCharacter) {
            this.m_deadchar = deadCharacter;
        }

        public char DeadCharacter {
            get {
                return this.m_deadchar;
            }
        }

        public void AddDeadKeyRow(char baseCharacter, char combinedCharacter) {
            this.m_rgbasechar.Add(baseCharacter);
            this.m_rgcombchar.Add(combinedCharacter);
        }

        public int Count {
            get {
                return this.m_rgbasechar.Count;
            }
        }

        public char GetBaseCharacter(int index) {
            return (char)this.m_rgbasechar[index];
        }

        public char GetCombinedCharacter(int index) {
            return (char)this.m_rgcombchar[index];
        }

        public bool ContainsBaseCharacter(char baseCharacter) {
            return this.m_rgbasechar.Contains(baseCharacter);
        }
    }

    public class VirtualKey {
        [DllImport("user32.dll", CharSet=CharSet.Unicode, EntryPoint="MapVirtualKeyExW", ExactSpelling=true)]
        private static extern uint MapVirtualKeyEx(
            uint uCode,
            uint uMapType,
            IntPtr dwhkl);

        private IntPtr m_hkl;
        private uint m_vk;
        private uint m_sc;
        private bool[] m_rgfDeadKey = new bool[8];
        private string[] m_rgss = new string[8];

        public VirtualKey(IntPtr hkl, KeysEx virtualKey) {
            this.m_sc = MapVirtualKeyEx((uint)virtualKey, 0, hkl);
            this.m_hkl = hkl;
            this.m_vk = (uint)virtualKey;
        }

        public VirtualKey(IntPtr hkl, uint scanCode) {
            this.m_vk = MapVirtualKeyEx(scanCode, 1, hkl);
            this.m_hkl = hkl;
            this.m_sc = scanCode;
        }

        public KeysEx VK {
            get { return (KeysEx)this.m_vk; }
        }

        public uint SC {
            get { return this.m_sc; }
        }

        public string GetShiftState(ShiftState shiftState) {
            if(this.m_rgss[(uint)shiftState] == null) {
                return("");
            }
           
            return(this.m_rgss[(uint)shiftState]);
        }

        public void SetShiftState(ShiftState shiftState, string value) {
            this.SetShiftState(shiftState, value, false);
        }
        public void SetShiftState(ShiftState shiftState, string value, bool isDeadKey) {
            this.m_rgss[(uint)shiftState] = value;
            this.m_rgfDeadKey[(uint)shiftState] = isDeadKey;
        }

        public bool IsEmpty {
            get {
                for(int i = 0; i < this.m_rgss.Length; i++) {
                    if(this.GetShiftState((ShiftState)i).Length > 0) {
                        return(false);
                    }
                }
                return true;
            }
        }

        public string LayoutRow {
            get {
                StringBuilder sbRow = new StringBuilder();

                // First, get the SC/VK info stored
                sbRow.Append(string.Format("{0:x2}\t{1:x2} - {2}", this.SC, (byte)this.VK, ((KeysEx)this.VK).ToString().PadRight(13)));

                for(ShiftState ss = 0; ss <= ShiftState.ShiftMenuCtrl; ss++) {
                    if(ss == ShiftState.Menu || ss == ShiftState.ShftMenu) {
                        // Alt and Shift+Alt don't work, so skip them
                        continue;
                    }
                    string st = this.GetShiftState(ss);

                    if(st.Length == 0) {
                        sbRow.Append("\t  -1");
                    }
                    else if(this.m_rgfDeadKey[(int)ss]) {
                        sbRow.Append(string.Format("\t{0:x4}@", ((ushort)st[0])));
                    }
                    else {
                        StringBuilder sbChar = new StringBuilder((5 * st.Length) + 1);
                        for(int ich = 0; ich < st.Length; ich++) {
                            sbChar.Append(((ushort)st[ich]).ToString("x4"));
                            sbChar.Append(' ');
                        }
                        sbRow.Append(string.Format("\t{0}", sbChar.ToString(0, sbChar.Length - 1)));
                    }
                }

                return sbRow.ToString();
            }
        }
    }

    public class Loader {

        private const uint KLF_NOTELLSHELL  = 0x00000080;

        internal static KeysEx[] lpKeyStateNull = new KeysEx[256];

        [DllImport("user32.dll", CharSet=CharSet.Unicode, EntryPoint="LoadKeyboardLayoutW", ExactSpelling=true)]
        private static extern IntPtr LoadKeyboardLayout(string pwszKLID, uint Flags);

        [DllImport("user32.dll", ExactSpelling=true)]
        private static extern bool UnloadKeyboardLayout(IntPtr hkl);

        [DllImport("user32.dll", CharSet=CharSet.Unicode, ExactSpelling=true)]
        private static extern int ToUnicodeEx(
            uint wVirtKey,
            uint wScanCode,
            KeysEx[] lpKeyState,
            StringBuilder pwszBuff,
            int cchBuff,
            uint wFlags,
            IntPtr dwhkl);

        [DllImport("user32.dll", ExactSpelling=true)]
        private static extern int GetKeyboardLayoutList(int nBuff, [Out, MarshalAs(UnmanagedType.LPArray)] IntPtr[] lpList);

        private static void FillKeyState(KeysEx[] lpKeyState, ShiftState ss, bool fCapsLock) {
            lpKeyState[(int)KeysEx.VK_SHIFT]    = (((ss & ShiftState.Shft) != 0) ? (KeysEx)0x80 : (KeysEx)0x00);
            lpKeyState[(int)KeysEx.VK_CONTROL]  = (((ss & ShiftState.Ctrl) != 0) ? (KeysEx)0x80 : (KeysEx)0x00);
            lpKeyState[(int)KeysEx.VK_MENU]     = (((ss & ShiftState.Menu) != 0) ? (KeysEx)0x80 : (KeysEx)0x00);
            lpKeyState[(int)KeysEx.VK_CAPITAL]  = (fCapsLock ? (KeysEx)0x01 : (KeysEx)0x00);
        }

        private static DeadKey ProcessDeadKey(
            uint iKeyDead,              // The index into the VirtualKey of the dead key
            ShiftState shiftStateDead,  // The shiftstate that contains the dead key
            KeysEx[] lpKeyStateDead,    // The key state for the dead key
            VirtualKey[] rgKey,         // Our array of dead keys
            IntPtr hkl) {               // The keyboard layout

            KeysEx[] lpKeyState = new KeysEx[256];
            DeadKey deadKey = new DeadKey(rgKey[iKeyDead].GetShiftState(shiftStateDead)[0]);

            for(uint iKey = 0; iKey < rgKey.Length; iKey++) {
                if(rgKey[iKey] != null) {
                    StringBuilder sbBuffer = new StringBuilder(10);     // Scratchpad we use many places

                    for(ShiftState ss = ShiftState.Base; ss <= ShiftState.ShiftMenuCtrl; ss++) {
                        int rc = 0;
                        if(ss == ShiftState.Menu || ss == ShiftState.ShftMenu) {
                            // Alt and Shift+Alt don't work, so skip them
                            continue;
                        }

                        // First the dead key
                        while(rc >= 0) {
                            // We know that this is a dead key coming up, otherwise
                            // this function would never have been called. If we do
                            // *not* get a dead key then that means the state is
                            // messed up so we run again and again to clear it up.
                            // Risk is technically an infinite loop but that should
                            // be impossible here.
                            rc = ToUnicodeEx((uint)rgKey[iKeyDead].VK, rgKey[iKeyDead].SC, lpKeyStateDead, sbBuffer, sbBuffer.Capacity, 0, hkl);
                        }

                        // Now fill the key state for the potential base character
                        FillKeyState(lpKeyState, ss, false);

                        sbBuffer = new StringBuilder(10);
                        rc = ToUnicodeEx((uint)rgKey[iKey].VK, rgKey[iKey].SC, lpKeyState, sbBuffer, sbBuffer.Capacity, 0, hkl);
                        if(rc == 1) {
                            // That was indeed a base character for our dead key.
                            // And we now have a composite character. Let's run
                            // through one more time to get the actual base
                            // character that made it all possible?
                            char combchar = sbBuffer[0];
                            sbBuffer = new StringBuilder(10);
                            rc = ToUnicodeEx((uint)rgKey[iKey].VK, rgKey[iKey].SC, lpKeyState, sbBuffer, sbBuffer.Capacity, 0, hkl);

                            char basechar = sbBuffer[0];

                            if(deadKey.DeadCharacter == combchar) {
                                // Since the combined character is the same as the dead key,
                                // we must clear out the keyboard buffer.
                                ClearKeyboardBuffer((uint)KeysEx.VK_DECIMAL, rgKey[(uint)KeysEx.VK_DECIMAL].SC, hkl);
                            }

                            if((((ss == ShiftState.Ctrl) || (ss == ShiftState.ShftCtrl)) &&
                                (char.IsControl(basechar))) ||
                                (basechar.Equals(combchar))) {
                                // Per people who know much better than us:
                                // ToUnicodeEx has an internal knowledge about those
                                // VK_A ~ VK_Z keys to produce the control characters,
                                // when the conversion rule is not provided in keyboard
                                // layout files

                                // Therefore, if the base character and combining are equal,
                                // and its a CTRL or CTRL+SHIFT state, and a control character
                                // is returned, then we do not add this "dead key" (which
                                // is not really a dead key).
                                continue;
                            }

                            if(! deadKey.ContainsBaseCharacter(basechar)) {
                                deadKey.AddDeadKeyRow(basechar, combchar);
                            }
                        }
                        else if(rc > 1) {
                            // Not a valid dead key combination, sorry! We just ignore it.
                        }
                        else if(rc < 0) {
                            // It's another dead key, so we ignore it (other than to flush it from the state)
                            ClearKeyboardBuffer((uint)KeysEx.VK_DECIMAL, rgKey[(uint)KeysEx.VK_DECIMAL].SC, hkl);
                        }
                    }
                }
            }
            return deadKey;
        }

        private static void ClearKeyboardBuffer(uint vk, uint sc, IntPtr hkl) {
            StringBuilder sb = new StringBuilder(10);
            int rc = 0;
            while(rc != 1) {
                rc = ToUnicodeEx(vk, sc, lpKeyStateNull, sb, sb.Capacity, 0, hkl);
            }
        }

        [STAThread]
        static void Main(string[] args) {
            int cKeyboards = GetKeyboardLayoutList(0, null);
            IntPtr[] rghkl = new IntPtr[cKeyboards];
            GetKeyboardLayoutList(cKeyboards, rghkl);           
            IntPtr hkl = LoadKeyboardLayout(args[0], KLF_NOTELLSHELL);
            if(hkl == IntPtr.Zero) {
                Console.WriteLine("Sorry, that keyboard does not seem to be valid.");
            }
            else {
                KeysEx[] lpKeyState = new KeysEx[256];
                VirtualKey[] rgKey = new VirtualKey[256];
                ArrayList alDead = new ArrayList();

                // Scroll through the Scan Code (SC) values and get the valid Virtual Key (VK)
                // values in it. Then, store the SC in each valid VK so it can act as both a
                // flag that the VK is valid, and it can store the SC value.
                for(uint sc = 0x01; sc <= 0x7f; sc++) {
                    VirtualKey key = new VirtualKey(hkl, sc);
                            //uint vkTmp = MapVirtualKeyEx(sc, 3, hkl);
                            //if(vkTmp != vk) Console.WriteLine("\t{0}\t{1:x2}\t{2:x2}", ((KeysEx)vk).ToString(), sc, vkTmp);
                    if(key.VK != 0) {
                        rgKey[(uint)key.VK] = key;
                    }
                }

                // add the special keys that do not get added from the code above
                for(KeysEx ke = KeysEx.VK_NUMPAD0; ke <= KeysEx.VK_NUMPAD9; ke++) {
                    rgKey[(uint)ke] = new VirtualKey(hkl, ke);
                }
                rgKey[(uint)KeysEx.VK_DIVIDE] = new VirtualKey(hkl, KeysEx.VK_DIVIDE);
                rgKey[(uint)KeysEx.VK_CANCEL] = new VirtualKey(hkl, KeysEx.VK_CANCEL);
                rgKey[(uint)KeysEx.VK_DECIMAL] = new VirtualKey(hkl, KeysEx.VK_DECIMAL);

                for(uint iKey = 0; iKey < rgKey.Length; iKey++) {
                    if(rgKey[iKey] != null) {
                        StringBuilder sbBuffer;    // Scratchpad we use many places

                        for(ShiftState ss = ShiftState.Base; ss <= ShiftState.ShiftMenuCtrl; ss++) {
                            if(ss == ShiftState.Menu || ss == ShiftState.ShftMenu) {
                                // Alt and Shift+Alt don't work, so skip them
                                continue;
                            }

                            ClearKeyboardBuffer((uint)KeysEx.VK_DECIMAL, rgKey[(uint)KeysEx.VK_DECIMAL].SC, hkl);
                            FillKeyState(lpKeyState, ss, false);
                            sbBuffer = new StringBuilder(10);
                            int rc = ToUnicodeEx((uint)rgKey[iKey].VK, rgKey[iKey].SC, lpKeyState, sbBuffer, sbBuffer.Capacity, 0, hkl);
                            if(rc > 0) {
                                if(sbBuffer.Length == 0) {
                                    // Someone defined NULL on the keyboard; let's coddle them
                                    rgKey[iKey].SetShiftState(ss, "\u0000");
                                }
                                else {
                                    rgKey[iKey].SetShiftState(ss, sbBuffer.ToString().Substring(0, rc));
                                }
                            }
                            else if(rc < 0) {
                                rgKey[iKey].SetShiftState(ss, sbBuffer.ToString().Substring(0, 1), true);

                                // It's a dead key; let's flush out whats stored in the keyboard state.
                                ClearKeyboardBuffer((uint)KeysEx.VK_DECIMAL, rgKey[(uint)KeysEx.VK_DECIMAL].SC, hkl);
                                alDead.Add(ProcessDeadKey(iKey, ss, lpKeyState, rgKey, hkl));
                            }
                        }
                    }
                }

                foreach(IntPtr i in rghkl) {
                    if(hkl == i) {
                        hkl = IntPtr.Zero;
                        break;
                    }
                }

                if(hkl != IntPtr.Zero) {
                    UnloadKeyboardLayout(hkl);
                }

                // Okay, now we can dump the layout
                Console.WriteLine("\nSC\tVK                 \t_\ts\tc\tsc\tca\tsca");
                Console.WriteLine("==\t==========\t\t====\t====\t====\t====\t====\t====");

                for(uint iKey = 0; iKey < rgKey.Length; iKey++) {
                    if((rgKey[iKey] != null) &&
                        ( !rgKey[iKey].IsEmpty)) {
                        Console.WriteLine(rgKey[iKey].LayoutRow);
                    }
                }

                foreach(DeadKey dk in alDead) {
                    Console.WriteLine();
                    Console.WriteLine("0x{0:x4}\t{1}", ((ushort)dk.DeadCharacter).ToString("x4"), dk.Count);
                    for(int id = 0; id < dk.Count; id++) {
                        Console.WriteLine("\t0x{0:x4}\t0x{1:x4}",
                            ((ushort)dk.GetBaseCharacter(id)).ToString("x4"),
                            ((ushort)dk.GetCombinedCharacter(id)).ToString("x4"));
                    }
                }
                Console.WriteLine();

            }
        }
    }
}

Ok, we have a lot going on here -- two new classes (DeadKey and VirtualKey), and some exciting new procedures like the VirtualKey.LayoutRow property and the ProcessDeadKey procedure.

That ProcessDeadKey proc. is the one that does the magic for us -- every time a dead key is found, this procedure is called and to find out which characters to combine with the dead key, every single other defined VK and shift state is combined with the dead key to see if a character pops out.

In case you are thinking this ought to be easier, I am not going to disagree with you.

There are several other interesting bits in the code here, especially areas that will need to be updated later to support some of the additional features like CAPS LOCK and SGCAPS and such. We'll get there soon enough. For now I'll dump out a keyboard layout so you can see the code in action, with the French layout (0000040c):

C:\KeyboardLayouts\bin\Debug>KeyboardLayouts.exe 0000040c

SC      VK                      _       s       c       sc      ca      sca
==      ==========              ====    ====    ====    ====    ====    ====
46      03 - VK_CANCEL          0003    0003    0003      -1      -1      -1
0e      08 - VK_BACK            0008    0008    007f      -1      -1      -1
7c      09 - VK_TAB             0009    0009      -1      -1      -1      -1
1c      0d - VK_RETURN          000d    000d    000a      -1      -1      -1
01      1b - VK_ESCAPE          001b    001b    001b      -1      -1      -1
39      20 - VK_SPACE           0020    0020    0020      -1      -1      -1
0b      30 - VK_0               00e0    0030    0000      -1    0040      -1
02      31 - VK_1               0026    0031      -1      -1      -1      -1
03      32 - VK_2               00e9    0032      -1      -1    007e@     -1
04      33 - VK_3               0022    0033      -1      -1    0023      -1
05      34 - VK_4               0027    0034      -1      -1    007b      -1
06      35 - VK_5               0028    0035      -1    001b    005b      -1
07      36 - VK_6               002d    0036      -1    001f    007c      -1
08      37 - VK_7               00e8    0037      -1      -1    0060@     -1
09      38 - VK_8               005f    0038      -1    001c    005c      -1
0a      39 - VK_9               00e7    0039      -1    001e    005e      -1
10      41 - VK_A               0061    0041    0001    0001      -1      -1
30      42 - VK_B               0062    0042    0002    0002      -1      -1
2e      43 - VK_C               0063    0043    0003    0003      -1      -1
20      44 - VK_D               0064    0044    0004    0004      -1      -1
12      45 - VK_E               0065    0045    0005    0005    20ac      -1
21      46 - VK_F               0066    0046    0006    0006      -1      -1
22      47 - VK_G               0067    0047    0007    0007      -1      -1
23      48 - VK_H               0068    0048    0008    0008      -1      -1
17      49 - VK_I               0069    0049    0009    0009      -1      -1
24      4a - VK_J               006a    004a    000a    000a      -1      -1
25      4b - VK_K               006b    004b    000b    000b      -1      -1
26      4c - VK_L               006c    004c    000c    000c      -1      -1
27      4d - VK_M               006d    004d    000d    000d      -1      -1
31      4e - VK_N               006e    004e    000e    000e      -1      -1
18      4f - VK_O               006f    004f    000f    000f      -1      -1
19      50 - VK_P               0070    0050    0010    0010      -1      -1
1e      51 - VK_Q               0071    0051    0011    0011      -1      -1
13      52 - VK_R               0072    0052    0012    0012      -1      -1
1f      53 - VK_S               0073    0053    0013    0013      -1      -1
14      54 - VK_T               0074    0054    0014    0014      -1      -1
16      55 - VK_U               0075    0055    0015    0015      -1      -1
2f      56 - VK_V               0076    0056    0016    0016      -1      -1
2c      57 - VK_W               0077    0057    0017    0017      -1      -1
2d      58 - VK_X               0078    0058    0018    0018      -1      -1
15      59 - VK_Y               0079    0059    0019    0019      -1      -1
11      5a - VK_Z               007a    005a    001a    001a      -1      -1
52      60 - VK_NUMPAD0         0030      -1      -1      -1      -1      -1
4f      61 - VK_NUMPAD1         0031      -1      -1      -1      -1      -1
50      62 - VK_NUMPAD2         0032      -1      -1      -1      -1      -1
51      63 - VK_NUMPAD3         0033      -1      -1      -1      -1      -1
4b      64 - VK_NUMPAD4         0034      -1      -1      -1      -1      -1
4c      65 - VK_NUMPAD5         0035      -1      -1      -1      -1      -1
4d      66 - VK_NUMPAD6         0036      -1      -1      -1      -1      -1
47      67 - VK_NUMPAD7         0037      -1      -1      -1      -1      -1
48      68 - VK_NUMPAD8         0038      -1      -1      -1      -1      -1
49      69 - VK_NUMPAD9         0039      -1      -1      -1      -1      -1
37      6a - VK_MULTIPLY        002a    002a      -1      -1      -1      -1
4e      6b - VK_ADD             002b    002b      -1      -1      -1      -1
4a      6d - VK_SUBTRACT        002d    002d      -1      -1      -1      -1
53      6e - VK_DECIMAL         002e    002e      -1      -1      -1      -1
35      6f - VK_DIVIDE          002f    002f      -1      -1      -1      -1
1b      ba - VK_OEM_1           0024    00a3    001d      -1    00a4      -1
0d      bb - VK_OEM_PLUS        003d    002b      -1      -1    007d      -1
32      bc - VK_OEM_COMMA       002c    003f      -1      -1      -1      -1
33      be - VK_OEM_PERIOD      003b    002e      -1      -1      -1      -1
34      bf - VK_OEM_2           003a    002f      -1      -1      -1      -1
28      c0 - VK_OEM_3           00f9    0025      -1      -1      -1      -1
0c      db - VK_OEM_4           0029    00b0      -1      -1    005d      -1
2b      dc - VK_OEM_5           002a    00b5    001c      -1      -1      -1
1a      dd - VK_OEM_6           005e@   00a8@   001b      -1      -1      -1
29      de - VK_OEM_7           00b2      -1      -1      -1      -1      -1
35      df - VK_OEM_8           0021    00a7      -1      -1      -1      -1
56      e2 - VK_OEM_102         003c    003e    001c      -1      -1      -1

0x007e  7
        0x0020  0x007e
        0x0061  0x00e3
        0x0041  0x00c3
        0x006e  0x00f1
        0x004e  0x00d1
        0x006f  0x00f5
        0x004f  0x00d5

0x0060  11
        0x0020  0x0060
        0x0061  0x00e0
        0x0041  0x00c0
        0x0065  0x00e8
        0x0045  0x00c8
        0x0069  0x00ec
        0x0049  0x00cc
        0x006f  0x00f2
        0x004f  0x00d2
        0x0075  0x00f9
        0x0055  0x00d9

0x005e  11
        0x0020  0x005e
        0x0061  0x00e2
        0x0041  0x00c2
        0x0065  0x00ea
        0x0045  0x00ca
        0x0069  0x00ee
        0x0049  0x00ce
        0x006f  0x00f4
        0x004f  0x00d4
        0x0075  0x00fb
        0x0055  0x00db

0x00a8  12
        0x0020  0x00a8
        0x0061  0x00e4
        0x0041  0x00c4
        0x0065  0x00eb
        0x0045  0x00cb
        0x0069  0x00ef
        0x0049  0x00cf
        0x006f  0x00f6
        0x004f  0x00d6
        0x0075  0x00fc
        0x0055  0x00dc
        0x0079  0x00ff

I will be digging more into pieces of this later on (I left the comments in on the most interesting parts for people who are curious and want to dig in!).

 

This post brought to you by "7" (U+0037, DIGIT SEVEN)
A Unicode character that is in the very small family of those whose VK value is the same as it's code point!


no comments

referenced by

2007/10/27 VK_DECIMAL is always valid (except formerly in Serbia)

2007/02/08 On how to do nothing else but to call it

2006/04/22 Getting all you can out of a keyboard layout, Part #10a

2006/04/13 Getting all you can out of a keyboard layout, Part #9b

2006/04/12 Getting all you can out of a keyboard layout, Part #9a

2006/04/10 Getting all you can out of a keyboard layout, Part #8

go to newer or older post, or back to index or month or day