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!
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