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:
(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:
# Michael S. Kaplan on 10 Apr 2006 9:45 AM:
referenced by