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

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


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

We're going to do a bit of preparatory adjustment in this post. Just so we can be ready for what comes later, you see.

If you look at information like the Scan Code and the Virtual Key, they really are independent of shift state. It means the code is kind of wasteful, continually asking for mappings over and over that it already has gotten.

Or perhaps I should say would be wasteful once we started adding new shift states to the mix. At the moment we are only mildly wasteful, where the code gets the scan code for VK_SPACE more than once.

Let's fix it now....

(As before, the older code is gray, the new code is black)

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!

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

                for(uint vk = 0x01; vk < rgScOfVk.Length; vk++) {
                    if(rgScOfVk[vk] != 0) {
                        StringBuilder sb = new StringBuilder(10);
                        int rc = ToUnicodeEx(vk, rgScOfVk[vk], lpKeyState, sb, sb.Capacity, 0, hkl);
                        if(rc > 0) {
                            StringBuilder sbChar = new StringBuilder(5 * rc);
                            for(int ich = 0; ich < rc; ich++) {
                                sbChar.Append(((ushort)sb.ToString()[ich]).ToString("x4"));
                                sbChar.Append(' ');
                            }
                            Console.WriteLine("{0:x2}\t{1:x4}\t{2:x2}\t{3}\t{4}",
                                rgScOfVk[vk],
                                sbChar.ToString(0, sbChar.Length - 1),
                                vk,
                                ((KeysEx)vk).ToString(),
                                ((Keys)vk).ToString());
                        }
                        else if(rc < 0) {
                            Console.WriteLine("{0:x2}\t{1:x4}\t{2:x2}\t{3}\t{4}\t\t\tDEAD!!!",
                                rgScOfVk[vk],
                                ((ushort)sb.ToString()[0]),
                                vk,
                                ((KeysEx)vk).ToString(),
                                ((Keys)vk).ToString());

                            // It's a dead key; let's flush out whats stored in the keyboard state.
                            ToUnicodeEx((uint)KeysEx.VK_SPACE, rgScOfVk[(uint)KeysEx.VK_SPACE], lpKeyState, sb, sb.Capacity, 0, hkl);
                        }
                    }
                }
                foreach(IntPtr i in rghkl) {
                    if(hkl == i) {
                        hkl = IntPtr.Zero;
                        break;
                    }
                }

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

So for now, since we have two items (the VK and the SC), and the VK is basically always a byte, it is easiest to store in a small array (for our purposes, the scan codes always fit into a byte too, but indexes in .NET are easier to work with when they are int or uint types.

If we need to store more data than a new class might make sense, but we'll put that off for now as we watch the complexity unfold before us. It is something we will make a decision on later.

At the moment we can be pleased with the fact that we have saved ourselves a few function calls -- calls that in some situations on some versions of Windows may actually map to kernel calls as that kernel mode component (userk) is the one that has lots of the keyboard information....

As an aside -- yesterday, I asked the question What the %$#!* is wrong with VkKeyScan[Ex]? and I did not mention the weirdest problem with the function -- it's name! It takes a TCHAR and returns a VK and a SHIFT STATE. But it does not return a keyboard scan code, which of course makes the name sort of misleading.

As I happen to be in the middle of the muddle of mapping between these things, it becomes even more noticable than usual. :-)

You may or may not remember how earlier in the series, I promised to explain why the code enumerated Scan Codes rather than Virtual Keys. Well, whether you do or not, I am not quite ready to do that just yet. It'll happen soon, I promise. :-)

There will also need to be some thought on displaying all the information -- once we move into new shift states, a bit more economy will be needed. That change will come with the shift states, which are next in the part....

 

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


# Mihai on 31 Mar 2006 2:10 PM:

Oups! Disregard my previous comment!

GetKeyNameText takes the scan-code in the bits 16-23 (8 bits), and when you go above, you override bit 24 ("Extended-key flag").

# Michael S. Kaplan on 31 Mar 2006 2:40 PM:

Hi Mihai,

Soon I will be discussing some of the problems getting info for scan keys with the extended info on them. :-)

referenced by

2008/02/11 Who assigns the VK_OEM_* values in keyboards?

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/28 Getting all you can out of a keyboard layout, Part #5

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