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

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


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

If you have been following this series, you may have noticed that as it moves concentrically outward it gets harder and harder to figure out why things are so freaking complicated to figure out.

(Or maybe that is just me; but in any case it seems clear that while things are [almost] always available, no one really seemed interested in making in easy for those trying to find out about a layout!)

For this post, I am going to swing around and pick up all the information about the CAPS LOCK key. So if you are one of those who hates the CAPS LOCK key, you'll want to give this one a miss and find some random web site to look at instead....

Now there are two different, mutually exclusive purposes per key:

Of course, in keeping with the general practice of making nothing easy (!), there is no direct way to query for this information. You essentially have to query:

  1. the character(s) in the BASE state;
  2. the character(s) in the SHIFT state;
  3. the character(s) in the CAPSLOCK state;
  4. the characters in the SHIFT+CAPSLOCK state (when the first three prove that this key has an SGCAPS assignment)

(There is a related per-keyboard layout setting known as SHIFTLOCK which I have discussed before; I'll talk more about it another day)

The rules are simple -- for any particular VK, if #1 does not equal #2, #2 does equal #3, and #2/#3 is assigned, then you are in the place that MSKLC defines as "CAPS == SHIFT", the basic CAPSLOCK thing.

And then you repeat the same process for ALTGR, SHIFT+ALTGR, and CAPSLOCK+ALTGR, to find out if you have an ALTGRCAPSLOCK thing going on.

Then to figure out whether an SGCAPS thing is going on, any time that neither BASE nor SHIFT matches CAPSLOCK, and/or any time that neither BASE nor SHIFT matches SHIFT+CAPSLOCK (in other words if you have three entirely different assignments here), then you have detected an SGCAPS thing.

(And there is no ALTGRSGCAPS, as I pointed out in Just one code point for SGCAPS.... -- along with the obvious limitation I pointed out in the post name -- as Nick mentioned in the comments, it is UTF-16 code unit we are talking about).

Of course, to make things even more confusing, you can create (with MSKLC or the DDK) a keyboard that uses SGCAPS but (by nature of the assigments you create) looks identical to one without SGCAPS. In which case you will not be able to detect the difference, at all.

For the most part, Microsoft-supplied keyboards in Windows do not do this sort of thing. Though if they did there would be no way for you to tell. :-)

Now to query about the CAPSLOCK, our updated FillKeyState procedure already has what it needs:

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);
}

It properly handles the difference between a keypress (where you set the high bit) and a toggle key pressed (where you set the low bit), just as I discussed in Is the CAPS LOCK on?

Another interesting little detail I found out working on MSKLC -- ToUnicodeEx with the CTRL shift state has an internal knowledge about the VK_A ~ VK_Z Virtual Key values and it will produce (as characters) the control characters represented by the VK values minus 0x40, when the conversion rule is not provided in keyboard layout files.

Many keyboards also include a few of control characters in the CTRL shift state in other VK values, which is a good thing, since there are programs that actually depend on them (e.g. Microsoft's Telnet searches for the key that contains 0x1b so that it can say on startup something like "Escape Character is 'CTRL+]'").

It is of course a bad thing that MSKLC was stripping all of these characters (rather than just the ones in VK_A ~ VK_Z), and worse that if anyone adds them back explicitly we give a warning about this. But we do live and learn, I suppose. And no one pays attention to warnings anyway. :-)

The rest of the change is just some overhead, storing these updated states and providing properties to get them (which would probably come in handy for people trying to do more than just display what is in the keyboard). Mostly self-explanatory, though I'll talk about the meaning of the reporting stuff in LayoutRow in a bit.

One interesting note -- I am taking a very different approach here than I did in MSKLC's method for showing the same information. Mainly because I can, and because all of the efforts in MSKLC to try and mask the complexity are fundamentally different than my efforts here (to expose the complexity!)....

Here is the new version of the code (as usual the stuff that is changed is black):

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,2];
        private string[,] m_rgss = new string[8,2];

        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, bool capsLock) {
            if(this.m_rgss[(uint)shiftState, (capsLock ? 1 : 0)] == null) {
                return("");
            }
           
            return(this.m_rgss[(uint)shiftState, (capsLock ? 1 : 0)]);
        }

        public void SetShiftState(ShiftState shiftState, string value, bool isDeadKey, bool capsLock) {
            this.m_rgfDeadKey[(uint)shiftState, (capsLock ? 1 : 0)] = isDeadKey;
            this.m_rgss[(uint)shiftState, (capsLock ? 1 : 0)] = value;
        }

        public bool IsSGCAPS {
            get {
                string stBase = this.GetShiftState(ShiftState.Base, false);
                string stShift = this.GetShiftState(ShiftState.Shft, false);
                string stCaps = this.GetShiftState(ShiftState.Base, true);
                string stShiftCaps = this.GetShiftState(ShiftState.Shft, true);
                return(
                    ((stCaps.Length > 0) &&
                    (! stBase.Equals(stCaps)) &&
                    (! stShift.Equals(stCaps))) ||
                    ((stShiftCaps.Length > 0) &&
                    (! stBase.Equals(stShiftCaps)) &&
                    (! stShift.Equals(stShiftCaps))));
            }
        }

        public bool IsCapsEqualToShift {
            get {
                string stBase = this.GetShiftState(ShiftState.Base, false);
                string stShift = this.GetShiftState(ShiftState.Shft, false);
                string stCaps = this.GetShiftState(ShiftState.Base, true);
                return(
                    (stBase.Length > 0) &&
                    (stShift.Length > 0) &&
                    (! stBase.Equals(stShift)) &&
                    (stShift.Equals(stCaps)));
            }
        }

        public bool IsAltGrCapsEqualToAltGrShift {
            get {
                string stBase = this.GetShiftState(ShiftState.MenuCtrl, false);
                string stShift = this.GetShiftState(ShiftState.ShiftMenuCtrl, false);
                string stCaps = this.GetShiftState(ShiftState.MenuCtrl, true);
                return(
                    (stBase.Length > 0) &&
                    (stShift.Length > 0) &&
                    (! stBase.Equals(stShift)) &&
                    (stShift.Equals(stCaps)));
            }
        }

        public bool IsEmpty {
            get {
                for(int i = 0; i < this.m_rgss.Length; i++) {
                    for(int j = 0; j <= 1; j++) {
                        if(this.GetShiftState((ShiftState)i, (j == 1)).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)));

                // Now the CAPSLOCK value
                int capslock =
                    0 |
                    (this.IsCapsEqualToShift ? 1 : 0) |
                    (this.IsSGCAPS ? 2 : 0) |
                    (this.IsAltGrCapsEqualToAltGrShift ? 4 : 0);
                sbRow.Append(string.Format("\t{0}", capslock));

                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;
                    }

                    for(int caps = 0; caps <= 1; caps++) {
                        string st = this.GetShiftState(ss, (caps == 1));

                        if(st.Length == 0) {
                            // No character assigned here, put in -1.
                            sbRow.Append("\t  -1");
                        }
                        else if((caps == 1) && st == (this.GetShiftState(ss, (caps == 0)))) {
                            // Its a CAPS LOCK state and the assigned character(s) are
                            // identical to the non-CAPS LOCK state. Put in a MIDDLE DOT.
                            sbRow.Append("\t   \u00b7");
                        }
                        else if(this.m_rgfDeadKey[(int)ss, caps]) {
                            // It's a dead key, append an @ sign.
                            sbRow.Append(string.Format("\t{0:x4}@", ((ushort)st[0])));
                        }
                        else {
                            // It's some characters; put 'em in there.
                            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
            bool fCapsLock,             // Was the caps lock key pressed?
            IntPtr hkl) {               // The keyboard layout

            KeysEx[] lpKeyState = new KeysEx[256];
            DeadKey deadKey = new DeadKey(rgKey[iKeyDead].GetShiftState(shiftStateDead, fCapsLock)[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;
                        }

                        for(int caps = 0; caps <=1; caps++) {
                            // 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 per Hiroyama
                                // 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, (caps == 0 ? false : true));

                            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))) {
                                    // 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

                                    // Additionally, dead key state is lost for some of these
                                    // character combinations, for unknown reasons.

                                    // 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;
                            }

                            for(int caps = 0; caps <= 1; caps++) {
                                ClearKeyboardBuffer((uint)KeysEx.VK_DECIMAL, rgKey[(uint)KeysEx.VK_DECIMAL].SC, hkl);
                                FillKeyState(lpKeyState, ss, (caps == 0 ? false : true));
                                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", false, (caps == 0 ? false : true));
                                    }
                                    else {
                                        if((rc == 1) &&
                                            (ss == ShiftState.Ctrl || ss == ShiftState.ShftCtrl) &&
                                            ((int)rgKey[iKey].VK == ((uint)sbBuffer[0] + 0x40))) {
                                            // 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
                                            continue;
                                        }
                                        rgKey[iKey].SetShiftState(ss, sbBuffer.ToString().Substring(0, rc), false, (caps == 0 ? false : true));
                                    }
                                }
                                else if(rc < 0) {
                                    rgKey[iKey].SetShiftState(ss, sbBuffer.ToString().Substring(0, 1), true, (caps == 0 ? false : 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, caps == 1, 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                 \tCAPS\t_\t_C\ts\tsC\tc\tcC\tsc\tscC\t\tca\tcaC\tsca\tscaC");
                Console.WriteLine("==\t==========\t\t====\t====\t====\t====\t====\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();

            }
        }
    }
}

For the display of the layout, a console width of at least 135 character is recommended, since all of the CAPSLOCK states are now included.

Here is the output with the Hebrew keyboard (0000040d), which shows what happens when some keys have the SGCAPS feature. Watch the CAPS column of the layout -- it is a bitmask of info (1==CAPSLOCK, 2==SGCAPS, 4==ALTGRCAPSLOCK):

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

SC      VK                      CAPS    _       _C      s       sC      c       cC      sc      scC     ca      caC     sca     scaC
==      ==========              ====    ====    ====    ====    ====    ====    ====    ====    ====    ====    ====    ====    ====
46      03 - VK_CANCEL          0       0003       ·    0003       ·    0003       ·      -1      -1      -1      -1      -1      -1
0e      08 - VK_BACK            0       0008       ·    0008       ·    007f       ·      -1      -1      -1      -1      -1      -1
7c      09 - VK_TAB             0       0009       ·    0009       ·      -1      -1      -1      -1      -1      -1      -1      -1
1c      0d - VK_RETURN          0       000d       ·    000d       ·    000a       ·      -1      -1      -1      -1      -1      -1
01      1b - VK_ESCAPE          0       001b       ·    001b       ·    001b       ·      -1      -1      -1      -1      -1      -1
39      20 - VK_SPACE           0       0020       ·    0020       ·    0020       ·      -1      -1      -1      -1      -1      -1
0b      30 - VK_0               2       0030       ·    0028    05c1      -1      -1      -1      -1      -1      -1      -1      -1
02      31 - VK_1               2       0031       ·    0021    05b1      -1      -1      -1      -1      -1      -1      -1      -1
03      32 - VK_2               2       0032       ·    0040    05b2      -1      -1      -1      -1      -1      -1      -1      -1
04      33 - VK_3               2       0033       ·    0023    05b3      -1      -1    200e       ·      -1      -1      -1      -1
05      34 - VK_4               2       0034       ·    0024    05b4      -1      -1    200f       ·    20aa       ·      -1      -1
06      35 - VK_5               2       0035       ·    0025    05b5      -1      -1      -1      -1      -1      -1      -1      -1
07      36 - VK_6               2       0036       ·    005e    05b6      -1      -1    001e       ·      -1      -1      -1      -1
08      37 - VK_7               2       0037       ·    0026    05b7      -1      -1      -1      -1      -1      -1      -1      -1
09      38 - VK_8               2       0038       ·    002a    05b8      -1      -1      -1      -1      -1      -1      -1      -1
0a      39 - VK_9               2       0039       ·    0029    05c2      -1      -1      -1      -1      -1      -1      -1      -1
1e      41 - VK_A               1       05e9    0041    0041    05e9      -1      -1      -1      -1      -1      -1      -1      -1
30      42 - VK_B               1       05e0    0042    0042    05e0      -1      -1      -1      -1      -1      -1      -1      -1
2e      43 - VK_C               1       05d1    0043    0043    05d1      -1      -1      -1      -1      -1      -1      -1      -1
20      44 - VK_D               1       05d2    0044    0044    05d2      -1      -1      -1      -1      -1      -1      -1      -1
12      45 - VK_E               1       05e7    0045    0045    05e7      -1      -1      -1      -1    20ac       ·      -1      -1
21      46 - VK_F               1       05db    0046    0046    05db      -1      -1      -1      -1      -1      -1      -1      -1
22      47 - VK_G               1       05e2    0047    0047    05e2      -1      -1      -1      -1      -1      -1      -1      -1
23      48 - VK_H               1       05d9    0048    0048    05d9      -1      -1      -1      -1    05f2       ·      -1      -1
17      49 - VK_I               1       05df    0049    0049    05df      -1      -1      -1      -1      -1      -1      -1      -1
24      4a - VK_J               1       05d7    004a    004a    05d7      -1      -1      -1      -1    05f1       ·      -1      -1
25      4b - VK_K               1       05dc    004b    004b    05dc      -1      -1      -1      -1      -1      -1      -1      -1
26      4c - VK_L               1       05da    004c    004c    05da      -1      -1      -1      -1      -1      -1      -1      -1
32      4d - VK_M               1       05e6    004d    004d    05e6      -1      -1      -1      -1      -1      -1      -1      -1
31      4e - VK_N               1       05de    004e    004e    05de      -1      -1      -1      -1      -1      -1      -1      -1
18      4f - VK_O               1       05dd    004f    004f    05dd      -1      -1      -1      -1      -1      -1      -1      -1
19      50 - VK_P               1       05e4    0050    0050    05e4      -1      -1      -1      -1      -1      -1      -1      -1
10      51 - VK_Q               1       002f    0051    0051    002f      -1      -1      -1      -1      -1      -1      -1      -1
13      52 - VK_R               1       05e8    0052    0052    05e8      -1      -1      -1      -1      -1      -1      -1      -1
1f      53 - VK_S               1       05d3    0053    0053    05d3      -1      -1      -1      -1      -1      -1      -1      -1
14      54 - VK_T               1       05d0    0054    0054    05d0      -1      -1      -1      -1      -1      -1      -1      -1
16      55 - VK_U               1       05d5    0055    0055    05d5      -1      -1      -1      -1    05f0       ·      -1      -1
2f      56 - VK_V               1       05d4    0056    0056    05d4      -1      -1      -1      -1      -1      -1      -1      -1
11      57 - VK_W               1       0027    0057    0057    0027      -1      -1      -1      -1      -1      -1      -1      -1
2d      58 - VK_X               1       05e1    0058    0058    05e1      -1      -1      -1      -1      -1      -1      -1      -1
15      59 - VK_Y               1       05d8    0059    0059    05d8      -1      -1      -1      -1      -1      -1      -1      -1
2c      5a - VK_Z               1       05d6    005a    005a    05d6      -1      -1      -1      -1      -1      -1      -1      -1
52      60 - VK_NUMPAD0         0       0030       ·      -1      -1      -1      -1      -1      -1      -1      -1      -1      -1
4f      61 - VK_NUMPAD1         0       0031       ·      -1      -1      -1      -1      -1      -1      -1      -1      -1      -1
50      62 - VK_NUMPAD2         0       0032       ·      -1      -1      -1      -1      -1      -1      -1      -1      -1      -1
51      63 - VK_NUMPAD3         0       0033       ·      -1      -1      -1      -1      -1      -1      -1      -1      -1      -1
4b      64 - VK_NUMPAD4         0       0034       ·      -1      -1      -1      -1      -1      -1      -1      -1      -1      -1
4c      65 - VK_NUMPAD5         0       0035       ·      -1      -1      -1      -1      -1      -1      -1      -1      -1      -1
4d      66 - VK_NUMPAD6         0       0036       ·      -1      -1      -1      -1      -1      -1      -1      -1      -1      -1
47      67 - VK_NUMPAD7         0       0037       ·      -1      -1      -1      -1      -1      -1      -1      -1      -1      -1
48      68 - VK_NUMPAD8         0       0038       ·      -1      -1      -1      -1      -1      -1      -1      -1      -1      -1
49      69 - VK_NUMPAD9         0       0039       ·      -1      -1      -1      -1      -1      -1      -1      -1      -1      -1
37      6a - VK_MULTIPLY        0       002a       ·    002a       ·      -1      -1      -1      -1      -1      -1      -1      -1
4e      6b - VK_ADD             0       002b       ·    002b       ·      -1      -1      -1      -1      -1      -1      -1      -1
4a      6d - VK_SUBTRACT        0       002d       ·    002d       ·      -1      -1      -1      -1      -1      -1      -1      -1
53      6e - VK_DECIMAL         0       002e       ·    002e       ·      -1      -1      -1      -1      -1      -1      -1      -1
35      6f - VK_DIVIDE          0       002f       ·    002f       ·      -1      -1      -1      -1      -1      -1      -1      -1
27      ba - VK_OEM_1           2       05e3    003b    003a    05e3      -1      -1      -1      -1      -1      -1      -1      -1
0d      bb - VK_OEM_PLUS        2       003d       ·    002b    05bc      -1      -1      -1      -1      -1      -1      -1      -1
33      bc - VK_OEM_COMMA       2       05ea    002c    003e    05ea      -1      -1      -1      -1      -1      -1      -1      -1
0c      bd - VK_OEM_MINUS       2       002d       ·    005f    05b9      -1      -1    001f       ·    05bf       ·      -1      -1
34      be - VK_OEM_PERIOD      2       05e5    002e    003c    05e5      -1      -1      -1      -1      -1      -1      -1      -1
35      bf - VK_OEM_2           2       002e    002f    003f    002e      -1      -1      -1      -1      -1      -1      -1      -1
29      c0 - VK_OEM_3           2       003b       ·    007e    05b0      -1      -1      -1      -1      -1      -1      -1      -1
1a      db - VK_OEM_4           2       005d    005b    007d    005d    200e       ·      -1      -1      -1      -1      -1      -1
2b      dc - VK_OEM_5           2       005c       ·    007c    05bb    001c       ·      -1      -1      -1      -1      -1      -1
1b      dd - VK_OEM_6           2       005b    005d    007b    005b    200f       ·      -1      -1      -1      -1      -1      -1
28      de - VK_OEM_7           2       002c    0027    0022    002c      -1      -1      -1      -1      -1      -1      -1      -1
56      e2 - VK_OEM_102         0       005c       ·    007c       ·    001c       ·      -1      -1      -1      -1      -1      -1

This is definitely getting more complicated, next time maybe we'll add a legend to the output stream so that some of the more obscure points can be documented....

There are only a few items left on my list to cover:

Although both of them are made more interesting by the fact that they are so hard to create keyboard layouts that have the features....

Though I do have the Canadian Multilingual Standard keyboard to use for the first one, which makes the example a bit easier, there are no keyboards with chained dead keys at the moment. I guess I'll have to build a few, maybe. Hmmm.

In any case, we're nearly done. Anyone not tired of this sample yet?

(Warning: it gets a bit more embarrassing if we go on!)

 

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


# Martin Bohring on 10 Apr 2006 8:20 AM:

Hello Michael,
no I am not tired of the examples yet. I had to crawl my way through all this while creating a touch screen keyboard a few month ago.

BTW: Did you recognize that the most soft keyboard code out there on the net does not take the keyboard layout into consideration. They are all hard coded and no I18N support of course.

It is so much harder (9 to 10 times I would say) to implement it correctly. And this guestimate is without IME support. That is really another story.

There are still some dark corners for me, which you are lighting up
with your examples.

# Michael S. Kaplan on 10 Apr 2006 9:45 AM:

Hi Martin --

Good to hear! I never know how engaged people are, otherwise. :-)

I know what you mean about the code out there. The Tablet PC virtual keyboard had the same problem until Wei and I got involved to give them a better design....

referenced by

2008/11/04 Strange control over CTRL and control characters

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

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