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

by Michael S. Kaplan, published on 2006/03/28 03:11 -05:00, original URI: http://blogs.msdn.com/b/michkap/archive/2006/03/28/561541.aspx


Previous posts in this series: Parts 0, 1, 2, 3, and 4.

I have been slowly building up a sample that is (so far) actually of limited use. I mean, who the hell cares about running through one shift state of a keyboard, even if it is definitive?

But it has only been a few days, and I am going to take care of the "easy shift states" now. So you can take the code from this sample and consider it done enough to handle a lot of the conventional scenarios and keyboards that exist.

Now it won't get you everything, but there are future posts for that.... :-)

We'll start by defining a nice enumeration to cover all those easy shift states:

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
}

Now, there are a few important things to note here:

And, how will we use this enumeration, exactly?

Well, we will build a Key State array (basically an array of Virtual Key values), and send it in our various calls to ToUnicodeEx. According to the documentation such as that in handy functions like GetKeyboardState:

...each member of the array pointed to by the lpKeyState parameter contains status data for a virtual key. If the high-order bit is 1, the key is down; otherwise, it is up.

There is more there, but we'll get to it later. :-)

The high order bit of a byte is the top bit -- thus setting the byte value to 0x80 will indicate it is pressed. We'll also add a handy function to do the bit work for us:

private static void FillKeyState(KeysEx[] lpKeyState, ShiftState ss) {
    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);
}

Since as I mentioned before, we are only dealing with the up/down state of these three keys, that's all we need to get the Key State array set up correctly.

Then, as I mentioned in Part #4, we do need to clean up our output a bit to keep it sensible. So, let's look at our function:

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

namespace KeyboardLayouts {
    class Class1 {

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

        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
        }

        internal const uint KLF_NOTELLSHELL  = 0x00000080;

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

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

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

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

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

        [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];
                uint[] rgScOfVk = new uint[256];

                // 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++) {
                    uint vk = MapVirtualKeyEx(sc, 1, hkl);
                    if(vk != 0) {
                        rgScOfVk[vk] = sc;
                    }
                }

                Console.WriteLine("SC\tVK                 \t_\ts\tc\tsc\tca\tsca");
                Console.WriteLine("==\t==========\t\t====\t====\t====\t====\t====\t====");
                for(uint vk = 0x01; vk < rgScOfVk.Length; vk++) {
                    if(rgScOfVk[vk] != 0) {
                        bool fSomeActualLetters = false;                // Were actual letters found in the row?
                        StringBuilder sbBuffer;                         // Scratchpad we use many places
                        StringBuilder sbLayout = new StringBuilder();   // Will hold an entire layout 'row'
                        StringBuilder sbStates = new StringBuilder();   // Will hold all of the shift state info

                        // First, get the SC/VK info stored
                        sbLayout.Append(string.Format("{0:x2}\t{1:x2} - {2}", rgScOfVk[vk], vk, ((KeysEx)vk).ToString().PadRight(13)));
                        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;
                            }

                            FillKeyState(lpKeyState, ss);
                            sbBuffer = new StringBuilder(10);
                            int rc = ToUnicodeEx(vk, rgScOfVk[vk], lpKeyState, sbBuffer, sbBuffer.Capacity, 0, hkl);
                            if(rc > 0) {
                                StringBuilder sbChar = new StringBuilder(5 * rc);
                                if(sbBuffer.Length == 0) {
                                    // Someone defined NULL on the keyboard; let's coddle them
                                    sbChar.Append("0000 ");
                                }
                                else {
                                    for(int ich = 0; ich < rc; ich++) {
                                        sbChar.Append(((ushort)sbBuffer.ToString()[ich]).ToString("x4"));
                                        sbChar.Append(' ');
                                    }
                                }
                                fSomeActualLetters = true;
                                sbStates.Append(string.Format("\t{0}", sbChar.ToString(0, sbChar.Length - 1)));
                            }
                            else if(rc < 0) {
                                fSomeActualLetters = true;
                                sbStates.Append(string.Format("\t{0:x4}@", ((ushort)sbBuffer.ToString()[0])));

                                // It's a dead key; let's flush out whats stored in the keyboard state.
                                ToUnicodeEx((uint)KeysEx.VK_DECIMAL, rgScOfVk[(uint)KeysEx.VK_DECIMAL], lpKeyState, sbBuffer, sbBuffer.Capacity, 0, hkl);
                            }
                            else {
                                sbStates.Append("\t  -1");
                            }
                        }
                        // Skip the layout rows that have nothing in them
                        if(fSomeActualLetters) {
                            sbLayout.Append(sbStates.ToString());
                            Console.WriteLine(sbLayout.ToString());
                        }
                    }
                }

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

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

        private static void FillKeyState(KeysEx[] lpKeyState, ShiftState ss) {
            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);
        }
    }
}

And there we go.

Note some of the important changes:

Let's take a look at a random keyboard when we run this code, say the French keyboard layout (0000040c), chosen since we get to see both dead keys and keys that have multiple code points in them (even though the latter messes up our columns for a few rows!):

SC      VK                      _       s       c       sc      ca      sca
==      ==========              ====    ====    ====    ====    ====    ====
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               007e 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               0060 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
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
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

And there we have it -- all of the easy shift states in a nice grid.

Items for future posts (not necessarily in this order!):

  • the base characters that go with the dead keys and the composite characters they create
  • the CAPS LOCK key
  • the harder shift states
  • SGCAPS
  • chained dead keys

    And perhaps a few more goodies if anyone is still reading by then....

     

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


  • # Stuart Dunkeld on 13 Nov 2007 10:47 AM:

    This is a great series, it's been extremely useful to me, thank you.

    This part doesn't compile though as FillKeyState references fCapsLock which isn't introduced until part 8 or so..


    referenced by

    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

    2006/04/06 Getting all you can out of a keyboard layout, Part #7

    2006/03/31 Getting all you can out of a keyboard layout, Part #6

    2006/03/29 Is the CAPS LOCK on?

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