Blog - Title

April, 2006

Sorting it all Out
Michael Kaplan's random stuff of dubious value
Be sure to read the disclaimer here first!
  • Sorting it all Out

    Get off my freaking key!

    • 10 Comments

    Regular readers may remember how I have complained in the past about the way Word interferes with key strokes on keyboards that are required for a given language.

    I have long hoped that the folks over in Office would fix this issue.

    After all, it is a pretty easy issue, conceptually -- if a particular key stroke combination is assigned in the current keyboard layout, then perhaps it should not be overrided to do some handy intuitive shortcut to functionality in Word, right?

    But I just read a but from Jensen Harris's blog such as these three posts, and it is clear that a ton of thought has gone into all kinds of exciting new stuff like better documentation of shortcuts, work to support the old accelerators (though I wonder how that works in localized product!?), and KeyTips and such. But as far as I can tell, no one decided to fix simple problem of "get off my freaking key!" for other language keyboards.

    I mean, it is all well and good to say:

    We've also used the data from the Customer Experience Improvement Program to track down the most frequently-used features without good keyboard shortcuts so that we could add them. For instance, you'll find CTRL+ALT+V added for Paste Special.

    But I have wonder if sufficient data came from where > 60% of the customer base comes from. After all, if you are using the Czech, Croatian, Serbian, Slovenian, or Hungarian keyboard layouts, than CTRL+ALT+V is also ALTGR+V, and that is the keystroke combination you use to get the @, a pretty important character for email addresses. So you won't even be able to send email to Microsoft to complain about this bit of keystroke hijacking. :-(

    Which is not to say that the folks using the Syriac or Divehi keyboard layouts (where it is U+200d, the ZERO WIDTH JOINER), are going to be much happier.

    And that is just one keystroke. Reading Jensen's words about the research that went into the issue, I am sure there are others that may be just as useful to some (and just as problematic for others!).

    Admittedly, it can be hard to interrogate a keyboard for all that information. But after posting almost an entire series on the issue (most recent post here), all of which is based on calling publicly available Win32 API functions, it is obvious that the information is out there now. And come to think about it, that it always has been out there (though perhaps not as well documented!).

    So now my plea to the folks in Office -- please fix the this bug, which has become the #1 bug affecting the usability of multilingual keyboards....

    (Or, if it has been fixed but no one has told anyone yet -- please get the word out!)

    Alternately, I'll probably have to finish that add-in I have been working on and off for the last few years to do the job for them at some point. Perhaps that can be a new part for the series? :-)

     

    This post brought to you by "@" (U+0040, a.k.a. COMMERCIAL AT)

  • Sorting it all Out

    The Unicode Lame List

    • 6 Comments

    It might take you back to Almost Live and the Lame List, and if so then I was able to inspire the right memories.

    It is not a weekly posting, so it's not 'What's weak, this week'. But it will likely just be me periodically posting something that I think shows a certain ignorance of the importance of Unicode/internationalization in software, so perhaps you can think of it as something more like a 'Who put Unicode in the commode' (you have to work a little harder to get the scansion right, but it is possible!).

    Anyway, I am looking over at Heath Stewart's blog, and his recent post Opening Patch Files when Compiled for Unicode. Now I am not calling Heath lame here at all -- he isn't, and this is really good information, and I am glad he is putting it out there:

    If you want to open a .msp file with the Windows Installer APIs, you must pass MSIDBOPEN_PATCHFILE to the MsiOpenDatabase function, or ERROR_OPEN_FAILED (110) is returned. Below is the definition of both MSIDBOPEN_PATCHFILE and MSIDBOPEN_READONLY from msiquery.h in the Windows Installer SDK.

    #define MSIDBOPEN_READONLY (LPCTSTR)0
    #define MSIDBOPEN_PATCHFILE 32/sizeof(*MSIDBOPEN_READONLY)

    LPCTSTR is defined as LPCWSTR when UNICODE is defined, which is defined as wchar_t*. Since sizeof(wchar_t) is 2, the value of MSIDBOPEN_PATCHFILE is 16 when UNICODE is defined. If you pass this to either the MsiOpenDatabaseA function or the MsiOpenDatabaseW function ERROR_OPEN_FAILED is still returned. The value must always be defined as 32.

    For the automation method Installer.OpenDatabase the second parameter must be set to msiOpenDatabaseModePatchFile to open a patch, which is always defined as 32.

    The lame part is a general approach that people take when they think about Unicode, due to specific attitudes both inside and outside of Microsoft:

    Most developers probably haven't run into this problem yet because of support for Windows 95, 98, and Me, where Unicode is not natively supported and it's typically undesirable to have to ship and support two bootstrap applications. Since Windows NT, 2000, XP, 2003, and future platforms support both ANSI and Unicode it makes sense to compile bootstrap applications for ANSI or MBCS.

    It is actually this particular prevailing attitude that finally inspired MSLU as a project -- there had to be a way to get people supporting Unicode, even if they did have to support Win9x.

    The problem now is that no one wants to support Win9x in their platforms, but if there is any kind of downlevel story involved (whether it is the Windows Installer folks not supporting a Win9x Unicode version of their support or the C++ folks not wanting to ship an MSLU-ized version of MFC, or whatever) it amounts to a passive-agressive, whiney "it's too late to support your Unicode solution in our product, but we have to keep encouraging the customer ANSI solution of our product."

    I wonder how many more years will these teams try to wish the problem away (as people did on the Windows side for so many years before approval was given to do MSLU) before they give up and finally do something about it. Because until it is easy to do, until there is a good backcompat story, and until it is the default setting, most people will not choose Unicode for their solution....

     

    This post brought to you by "Ề" (U+1ec0, a.k.a. LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND GRAVE)

  • Sorting it all Out

    She slipped out last Friday

    • 2 Comments

    As I hinted at the other day, Gretchen Ledgard is no longer at Microsoft. Since I won't be able to write about her as eloquently as Korby did, I won't try to do that.

    But I'll mention some of what I will miss now that she is officially no longer in the building next door....

    • Being the only person who would still call her Miss Garland (the copy of Outlook on my laptop was aiming to ask her out, I think -- it seemed to be in denial that she had married Josh and assumed her alias was ggarland!);
    • Knowing that if it was a little too late to scare up lunch plans I could IM her and it would turn out she was about to grab a bite too;
    • All of the insight into HR and recruiting at Microsoft and in the real world;

    Ok, I'll stop now, like I said I won't be as eloquent as Korby -- but I agree with everything in that post he put up so you can just paste that post in here for the rest.... :-)

    A colleague just IM'ed me and mentioned that although he didn't know her, it was like the public face of Microsoft recruiting was leaving. And there may be some truth in that -- there is not a ton overlap between JobsBlog and Heather's blog, for example. Even when you look at the just the recruiting posts.

    I mean, even mini-Microsoft said farewell. How often would you expect that sort of thing to happen?

    (I know a GM who used to joke about how wanting to be RIF'ed might be a worthy carer goal; I think it may be a worthy goal for those of us who are not as important to be thought of as bad attrition when we leave)

    Sure, Gretchen and I will see each other again, we may even have lunch again some day if she can be talked out of showing up in velour jogging outfit (don't ask, it's not a pretty story!). But it won't be the same....

    So, is the public face of Microsoft recruiting leaving? Well, one of those faces most certainly is. And if it's the one you know then it is quite the apple-cart upsetter.

    Gretchen, I hope you will always feel free to IM me about some blog trick that you see here that you want to try out. Or if you and Josh are looking for a third wheel to go do Kamkaze shooters with.

    And I wish you the best of luck -- may your real life exceed your dreams in all that you do. What does Betsy always say again? Oh, that's right -- Live it vivid, Gretchen!

  • Sorting it all Out

    MFC/CRT build instructions tweaklets

    • 0 Comments

    The other day I got some mail about the instructions for rebuilding the Microsoft Foundation Classes and the C Runtime with the MS Layer for Unicode:

    Hi Michael, two things need to be tweaked in the (otherwise, delightfully clear) docs on rebuilding the CRT and MFC libs. For 7.1, the atl path for the location of atlmfc.mak is given as "vc7/atlmfc", when I found it in "/vc7/atlmfc/src".

    And second, and much more important, is that when compiling the MFC portion, please specify that a new Command Prompt window should be used after compiling the CRT libs. I gather that the make process twiddles with a lot of the env vars, corrupting them for the MFC make, causing a bunch of C2220 errors. That cose me a better part of a day trying to track that down:

    ...while the CRT stuff worked just as expected, my recompile of the ATL threw some errors. Specifically, this is for the 7.1 libs. And I am getting the error:

    D:\MSVC .Net\Vc7\atlmfc\src>nmake -f atlmfc.mak MFC libname=MFC71L

    setting PLATFORM=INTEL

    if not exist ..\lib\INTEL md ..\lib\INTEL

    cd mfc

    nmake / DEBUG=0 _OD_EXT= PLATFORM=INTEL

    cl @C:\DOCUME~1\MSMITH~1.COR\LOCALS~1\Temp\nm6773.tmp

    objcore.cpp

    D:\MSVC .Net\Vc7\atlmfc\include\afxv_w32.h(134) : error C2220: warning treated as error - no object file generated

    D:\MSVC .Net\Vc7\atlmfc\include\afxv_w32.h(134) : warning C4005: 'NOSERVICE' : macro redefinition

    command-line arguments : see previous definition of 'NOSERVICE'

    D:\MSVC .Net\Vc7\atlmfc\include\atldef.h(261) : warning C4005: 'WIN32_LEAN_AND_MEAN' : macro redefinition

    command-line arguments : see previous definition of 'WIN32_LEAN_AND_MEAN'

    I talked to the author if the instructions, and I did take care of that first part, the error in the path.

    But he noticed that the second part was already covered in the instructions, for all four versions (6.0, 7.0, 7.1, and 8.0):

    ...as the CRT build defines some symbols that MFC build also uses, a user cannot build MFC in the same command prompt "session" as the CRT.  They must exit the command prompt and create a new command prompt session and then start the MFC build from that session.. 

    Actually, come to think of it, we already mention this in the instructions: "The CRT build is now done.  Before proceeding any further we need to close the command prompt that we used to build the CRT because it created certain environment variables that will cause compile errors in the next step, building the Unicode version of MFC."

    So if the user had followed those instructions they would have succeeded beyond their wildest dreams.   :)

    I went ahead and made it RED in each set of instructions so that people would be able to feel embarrassed if they did not see it. :-)

  • Sorting it all Out

    Offense can be a cultural thing

    • 4 Comments

    I was reading Benjamin's brief history of 'spaz' on Language Log, and I think it is quite interesting how words that can be quite innocuous in one culture can be quite incendiary in another.

    In the particular example with Tiger Woods and the word spaz it is not even between languages since it is English in both cases. Perhaps we could distinguish different dialects in terms of whether there are such "points of passion" in one but not another?

    For myself, I know that I should speak about my own problems in a more polite way than I do since other take offense. But it is easier to laugh off the situation with the occasional self-deprecting gimp.

    Or to counter my ex-fiance's glib 'girls with glasses don't get passes' with a 'man with stick can't find a chick' or the newer 'man who scoots gets no beauts'.

    Or to answer questions like 'what's your height?' with 4"1', or even better 124cm (which is about what my height is when I am sitting in the scooter).

    Of course its considered slightly different if you are a member of the group you are perhaps nominally insulting. It doesn't bother me obviously, but sometimes it makes others uncomfortable (especially people who feel I am working against the progress for sensitivity on the issue).

    We call it Political Correctness mostly, as if saying that I "have multiple sclerosis" is somehow magically better than saying I "am afflicted with multiple sclerosis". Which in my humble opinion is completely asinine; on a day that I do not leave my house for fear of being able to walk more than 20 feet without falling (it's rare but it has happened) calling it an affliction is probably the most polite way to adequately describe the situation.

    On the other hand, I probably might be offended if someone used such a term on a day that I was feeling just fine -- perhaps going so far as to say the only affliction I was suffering from that day was the presence of the person making the statement....

    And I have had quite spastic moments before, and could easily keep someone up at night if they had to be around me and my muscle spams (I take Baclofen now, so I generally don't have the problem). Somehow calling myself a spaz in those situations is slightly less frowned upon by most. Even in England.

    But, to get back to cultural issues, we hit the real problem -- it really shouldn't be a cultural issue so much as a situational one. It is an issue where we shouldn't use the most extreme form of the problem as the default. Better to save the exteme stuff for when you feel pretty afflicted by it.

    How on earth can someone know about every single possible point of offense in every single culture? It is truly impossible, unless you have every word you say pre-screened by native speakers before you say anything. Which is not always going to be possible.

    I doubt we can add a LOCALE_SWORDSTHATOFFENDTHECRAPOUTOFPEOPLE flag to GetLocaleInfo to solve the problem programatically, though it would be fun to collect the data!

    So, if anything I post here in the blog offends someone in a particular culture, I can promise that it was not intentional (unless I specifically say otherwise!).

    Or, at the very least remember that I am sometimes a gimp and/or a spaz....

  • Sorting it all Out

    Long term planning is not always done

    • 9 Comments

    Starting in the 2.0 version of the .NET Framework, Microsoft has had the UmAlQuraCalendar Class, which is a more secular version of the HijriCalendar class in that it is based on a table-based algorithm, and does not support the "advance date" functionality I have talked about in the past.

    While still somewhat controversial with religious authorities who object to a calendar that is not based on the moon sighting (just as they object to technology to help with the sighting in some cases), it is to others a reasonable compromise for the need to have a more predictable calendar in the secular world.

    Which is not to say there are not some limitations.

    Try the following code:

    using System;
    using System.Globalization;

    public class Test
    {

        public static void Main()
        {
            DateTime dtLater = new DateTime(2030, 1, 1);
            UmAlQuraCalendar uaqc = new UmAlQuraCalendar();
            DateTime dtMuchLater = uaqc.AddMonths(dtLater, 1);
        }

    }

    The clever among you might object that this code does nothing with the date it has calculated, so what is its's point?

    Well, try it out and compile it. Once you run the small application, here is what it will say:

    Unhandled Exception: System.ArgumentOutOfRangeException: Specified time is not supported in this calendar. It should be between 04/30/1900 00:00:00 (Gregorian date) and 05/13/2029 23:59:59 (Gregorian date), inclusive.
    Parameter name: time
       at System.Globalization.UmAlQuraCalendar.CheckTicksRange(Int64 ticks)
       at System.Globalization.UmAlQuraCalendar.GetDatePart(DateTime time, Int32 part)
       at System.Globalization.UmAlQuraCalendar.AddMonths(DateTime time, Int32 months)
       at Test.Main()

    Hmmm... that seems like a problem, doesn't it?

    Well, it is working as designed, per the docs:

    Remarks:
    The UmAlQuraCalendar class is nearly identical to the HijriCalendar class, except the Saudi Hijri calendar uses a table-based algorithm licensed from the Saudi government to calculate dates, can express dates to the year 1450 A.H., and does not support the HijriAdjustment property.

    And one of the downsides of a table-based calendar is that until tables exist there is no especially good answer on what to do other than throw an exception (the moral equivalent of walking off the end of the table).

    But certainly such a problem can have an impact on being able to use the calendar in everyday secular matters, couldn't it?

    Obviously something better will have to happen here for people to be able make appropriate use of it as a calendar in many different scenarios....

     

    This post brought to you by "ޓ" (U+0793, a.k.a. THAANA LETTER TAVIYANI)

  • Sorting it all Out

    Every character has a story; some of them have cartoons!

    • 1 Comments

    Adam Hill contacted me via that contact link to point out Hacklog (Blogamundo). With a subtitle like "Poking holes in the language barrier since approximately one month from now" I guess it is saying something interesting!

    A quick perusal shows lots of interesting topics -- it even linked to me after the Channel 9 interview....

    Anyway, Adam specifically gave me a heads up about a potential regular feature:

    Unicode comic strips, the End Days are nigh.

    Pat promises/threatens to do more :)

    Well, since they all have stories, and some may now have cartoons, I have to wonder how long before we get the first graphic novel?

    Blogamundo seems to me like a quite worthy addition to the blogs I read, in any case. :-)

     

    This post brought to you by "⚇" (U+2687, a.k.a. WHITE CIRCLE WITH TWO DOTS)

  • Sorting it all Out

    Unicode isn't advanaced mathematics

    • 6 Comments

    As Peter Vogel pointed out about Unicode a few years back, Unicode is hardly nuclear physics. I am pretty sure its not advanced mathematics, either (a point that was just proven again last night!).

    It has been a while since I have posted about things from the Unicode List, though I think that has mostly been to help keep up the respect for standards of my regular readers. :-)

    But Mark Davis did post a pretty helpful little chart about Unicode 5.0 last night that gives the official count of characters in Unicode as of Unicode 5.0 (set to be released soon!):

    I tend to use the following (ie, excluding private use & noncharacters).

    Unicode
    2.0.0 2.1.2 3.0.0 3.1.0 3.2.0 4.0.0 4.1.0 5.0.0
    Letter 36,121 36,121 45,443 89,762 89,957 90,547 91,395 92,496
    Mark 446 446 575 605 653 941 1,009 1,065
    Number 374 374 431 486 536 612 695 836
    Punctuation 240 240 288 288 351 360 420 437
    Symbol 1,671 1,673 2,414 2,851 3,508 3,764 3,978 4,032
    Separator 17 17 19 19 20 21 20 20
    Control/Format 81 81 89 194 196 202 203 203
    Graphic+C/F 38,950 38,952 49,259 94,205 95,221 96,447 97,720 99,089


    Too bad we were just 11 short of 100,000 for 5.0!

    Mark

    Of course some people pointed out the small math mistake there, though Curtis Clark had the funniest way of saying it:

    Maybe it's a floating point error in my calc.exe, but I come up with 911
    short (which will be just about right to encode *all* the Phaistos
    characters, when the rest of the corpus is discovered). :-)

    I'll probably post about the Phaistos characters another day, after the dust settles on the proposal....

    It is all goodness though; it was a small typo (this morning Mark admitted to his '...math mistake; sorry for the confusion....'), and there is plenty of time to find 911 new characters (and probably more than that) for Unicode 5.1! :-)

     

    This post brought to you by "8" and "∞" (U+0038 and U+221e, a.k.a. DIGIT EIGHT and INFINITY)
    Two characters who are great friends, merely a single best fit mapping away from each other, and quite confusable if you are lying down!

  • Sorting it all Out

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

    • 13 Comments

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

    In my last post, I promised to provide the updated version of the code sample that will handle any keyboard layout that ships with Windows, including the Canadian Multilingual Standard layout....

    A brief note on why it may seem like I am picking on this particular layout.... it represents to me the prototypical example of a government to try to support a mutlilingual keyboard. But as I discussed in Keyboards: Monolingual or Multilingual? such attempts, while being perhaps quite useful for a limited segment of the population, can be quite difficult to use for many in the intended market....

    The other problem is of course the extensive use of the custom shift states, when there was actually room in the other, more conventional shift states for all of the assigned letters. Though I guess if they never did that, I would not have been posting this particular part of the series. :-)

    At the end of this post you'll be able to see the layout and you can decide for yourself if you would swaer by the layout, or at it, were you to use it....

    Anyway, here is the updated code (which fixes a few bugs exposed by this particular layout):

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

    namespace KeyboardLayouts {

        //  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
            ShftMenuCtrl    = Shft | Menu | Ctrl,   // 7
            Xxxx            = 8,                    // 8
            ShftXxxx        = Shft | Xxxx,          // 9
        }

        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)]
            internal 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[(int)ShiftState.ShftXxxx + 1,2];
            private string[,] m_rgss = new string[(int)ShiftState.ShftXxxx + 1,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.ShftMenuCtrl, false);
                    string stCaps = this.GetShiftState(ShiftState.MenuCtrl, true);
                    return(
                        (stBase.Length > 0) &&
                        (stShift.Length > 0) &&
                        (! stBase.Equals(stShift)) &&
                        (stShift.Equals(stCaps)));
                }
            }

            public bool IsXxxxGrCapsEqualToXxxxShift {
                get {
                    string stBase = this.GetShiftState(ShiftState.Xxxx, false);
                    string stShift = this.GetShiftState(ShiftState.ShftXxxx, false);
                    string stCaps = this.GetShiftState(ShiftState.Xxxx, 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.GetUpperBound(0); 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) |
                        (this.IsXxxxGrCapsEqualToXxxxShift ? 8 : 0);
                    sbRow.Append(string.Format("\t{0}", capslock));


                    for(ShiftState ss = 0; ss <= Loader.MaxShiftState; 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", CharSet=CharSet.Unicode, EntryPoint="VkKeyScanExW", ExactSpelling=true)]
            private static extern ushort VkKeyScanEx(char ch, IntPtr dwhkl);

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

            private static KeysEx m_XxxxVk = KeysEx.None;
            public static KeysEx XxxxVk {
                get {
                    return m_XxxxVk;
                }
                set {
                    m_XxxxVk = value;
                }
            }

            public static ShiftState MaxShiftState {
                get {
                    return (Loader.XxxxVk == KeysEx.None ? ShiftState.ShftMenuCtrl : ShiftState.ShftXxxx);
                }
            }

            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);
                if(Loader.XxxxVk != KeysEx.None) {
                    // The Xxxx key has been assigned, so let's include it
                    lpKeyState[(int)Loader.XxxxVk]       = (((ss & ShiftState.Xxxx) != 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 <= Loader.MaxShiftState; 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));

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

                    // See if there is a special shift state added
                    for(KeysEx vk = KeysEx.None; vk <= KeysEx.VK_OEM_CLEAR; vk++) {
                        uint sc = VirtualKey.MapVirtualKeyEx((uint)vk, 0, hkl);
                        uint vkL = VirtualKey.MapVirtualKeyEx(sc, 1, hkl);
                        uint vkR = VirtualKey.MapVirtualKeyEx(sc, 3, hkl);
                        if((vkL != vkR) &&
                            ((uint)vk != vkL)) {
                            switch(vk) {
                                case KeysEx.VK_LCONTROL:
                                case KeysEx.VK_RCONTROL:
                                case KeysEx.VK_LSHIFT:
                                case KeysEx.VK_RSHIFT:
                                case KeysEx.VK_LMENU:
                                case KeysEx.VK_RMENU:

                                    break;

                                default:
                                    Loader.XxxxVk = vk;
                                    break;
                            }
                        }
                    }

                    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 <= Loader.MaxShiftState; 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));
                                    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));
                                        }
                                        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));
                                        }
                                    }
                                    else if(rc < 0) {
                                        rgKey[iKey].SetShiftState(ss, sbBuffer.ToString().Substring(0, 1), true, (caps != 0));

                                        // 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);
                                        DeadKey dk = null;
                                        for(int iDead = 0; iDead < alDead.Count; iDead++) {
                                            dk = (DeadKey)alDead[iDead];
                                            if(dk.DeadCharacter == rgKey[iKey].GetShiftState(ss, caps != 0)[0]) {
                                                break;
                                            }
                                            dk = null;
                                        }
                                        if(dk == null) {
                                            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.Write("\nSC\tVK  \t\t\tCAPS\t_\t_C\ts\tsC\tc\tcC\tsc\tscC\t\tca\tcaC\tsca\tscaC");
                    if(Loader.XxxxVk != KeysEx.None) {
                        Console.Write("\tx\txC\tsx\tsxC");
                    }
                    Console.WriteLine();
                    Console.Write("==\t==========\t\t====\t====\t====\t====\t====\t====\t====\t====\t====\t====\t====\t====\t====");
                    if(Loader.XxxxVk != KeysEx.None) {
                        Console.Write("\t====\t====\t====\t====");
                    }
                    Console.WriteLine();

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

                }
            }
        }
    }

    The code above fixes a bug not found with previous versions that would cause dead key tables to show up more than once if the dead key is defined multiple times....

    And now, here is that keyboard with the 00011009 KLID value (you'll want a console window with 170 columns in it to print it out yourself!):

    C:\KeyboardLayouts\bin\Debug>KeyboardLayouts.exe 00011009

    SC      VK                      CAPS    _       _C      s       sC      c       cC      sc      scC     ca      caC     sca     scaC    x       xC      sx      sxC
    ==      ==========              ====    ====    ====    ====    ====    ====    ====    ====    ====    ====    ====    ====    ====    ====    ====    ====    ====
    46      03 - VK_CANCEL          0       0003       ·    0003       ·    0003       ·      -1      -1      -1      -1      -1      -1      -1      -1      -1      -1
    0e      08 - VK_BACK            0       0008       ·    0008       ·    007f       ·      -1      -1      -1      -1      -1      -1      -1      -1      -1      -1
    7c      09 - VK_TAB             0       0009       ·    0009       ·      -1      -1      -1      -1      -1      -1      -1      -1      -1      -1      -1      -1
    1c      0d - VK_RETURN          0       000d       ·    000d       ·    000a       ·      -1      -1      -1      -1      -1      -1      -1      -1      -1      -1
    01      1b - VK_ESCAPE          0       001b       ·    001b       ·    001b       ·      -1      -1      -1      -1      -1      -1      -1      -1      -1      -1
    39      20 - VK_SPACE           0       0020       ·    0020       ·    0020       ·      -1      -1    00a0       ·      -1      -1    0020       ·    0020       ·
    0b      30 - VK_0               0       0030       ·    0029       ·      -1      -1      -1      -1    005d       ·      -1      -1      -1      -1      -1      -1
    02      31 - VK_1               0       0031       ·    0021       ·      -1      -1      -1      -1      -1      -1      -1      -1    00b9       ·    00a1       ·
    03      32 - VK_2               0       0032       ·    0040       ·      -1      -1      -1      -1      -1      -1      -1      -1    00b2       ·      -1      -1
    04      33 - VK_3               0       0033       ·    0023       ·      -1      -1      -1      -1      -1      -1      -1      -1    00b3       ·    00a3       ·
    05      34 - VK_4               0       0034       ·    0024       ·      -1      -1      -1      -1    00a4       ·      -1      -1    00bc       ·    20ac       ·
    06      35 - VK_5               0       0035       ·    0025       ·      -1      -1      -1      -1      -1      -1      -1      -1    00bd       ·    215c       ·
    07      36 - VK_6               0       0036       ·    003f       ·      -1      -1      -1      -1      -1      -1      -1      -1    00be       ·    215d       ·
    08      37 - VK_7               0       0037       ·    0026       ·      -1      -1      -1      -1    007b       ·      -1      -1      -1      -1    215e       ·
    09      38 - VK_8               0       0038       ·    002a       ·      -1      -1      -1      -1    007d       ·      -1      -1      -1      -1    2122       ·
    0a      39 - VK_9               0       0039       ·    0028       ·      -1      -1      -1      -1    005b       ·      -1      -1      -1      -1    00b1       ·
    1e      41 - VK_A               1       0061    0041    0041    0061      -1      -1      -1      -1      -1      -1      -1      -1    00e6       ·    00c6       ·
    30      42 - VK_B               1       0062    0042    0042    0062      -1      -1      -1      -1      -1      -1      -1      -1    201d       ·    2019       ·
    2e      43 - VK_C               1       0063    0043    0043    0063      -1      -1      -1      -1      -1      -1      -1      -1    00a2       ·    00a9       ·
    20      44 - VK_D               1       0064    0044    0044    0064      -1      -1      -1      -1      -1      -1      -1      -1    00f0       ·    00d0       ·
    12      45 - VK_E               1       0065    0045    0045    0065      -1      -1      -1      -1    20ac       ·      -1      -1    0153       ·    0152       ·
    21      46 - VK_F               1       0066    0046    0046    0066      -1      -1      -1      -1      -1      -1      -1      -1      -1      -1    00aa       ·
    22      47 - VK_G               1       0067    0047    0047    0067      -1      -1      -1      -1      -1      -1      -1      -1    014b       ·    014a       ·
    23      48 - VK_H               1       0068    0048    0048    0068      -1      -1      -1      -1      -1      -1      -1      -1    0127       ·    0126       ·
    17      49 - VK_I               1       0069    0049    0049    0069      -1      -1      -1      -1      -1      -1      -1      -1    2192       ·    0131       ·
    24      4a - VK_J               1       006a    004a    004a    006a      -1      -1      -1      -1      -1      -1      -1      -1    0133       ·    0132       ·
    25      4b - VK_K               1       006b    004b    004b    006b      -1      -1      -1      -1      -1      -1      -1      -1    0138       ·      -1      -1
    26      4c - VK_L               1       006c    004c    004c    006c      -1      -1      -1      -1      -1      -1      -1      -1    0140       ·    013f       ·
    32      4d - VK_M               1       006d    004d    004d    006d      -1      -1      -1      -1      -1      -1      -1      -1    00b5       ·    00ba       ·
    31      4e - VK_N               1       006e    004e    004e    006e      -1      -1      -1      -1      -1      -1      -1      -1    0149       ·    266a       ·
    18      4f - VK_O               1       006f    004f    004f    006f      -1      -1      -1      -1      -1      -1      -1      -1    00f8       ·    00d8       ·
    19      50 - VK_P               1       0070    0050    0050    0070      -1      -1      -1      -1      -1      -1      -1      -1    00fe       ·    00de       ·
    10      51 - VK_Q               1       0071    0051    0051    0071      -1      -1      -1      -1      -1      -1      -1      -1      -1      -1    2126       ·
    13      52 - VK_R               1       0072    0052    0052    0072      -1      -1      -1      -1      -1      -1      -1      -1    00b6       ·    00ae       ·
    1f      53 - VK_S               1       0073    0053    0053    0073      -1      -1      -1      -1      -1      -1      -1      -1    00df       ·    00a7       ·
    14      54 - VK_T               1       0074    0054    0054    0074      -1      -1      -1      -1      -1      -1      -1      -1    0167       ·    0166       ·
    16      55 - VK_U               1       0075    0055    0055    0075      -1      -1      -1      -1      -1      -1      -1      -1    2193       ·    2191       ·
    2f      56 - VK_V               1       0076    0056    0056    0076      -1      -1      -1      -1      -1      -1      -1      -1    201c       ·    2018       ·
    11      57 - VK_W               1       0077    0057    0057    0077      -1      -1      -1      -1      -1      -1      -1      -1    0142       ·    0141       ·
    2d      58 - VK_X               1       0078    0058    0058    0078      -1      -1      -1      -1    00bb       ·      -1      -1      -1      -1      -1      -1
    15      59 - VK_Y               1       0079    0059    0059    0079      -1      -1      -1      -1      -1      -1      -1      -1    2190       ·    00a5       ·
    2c      5a - VK_Z               1       007a    005a    005a    007a      -1      -1      -1      -1    00ab       ·      -1      -1      -1      -1      -1      -1
    52      60 - VK_NUMPAD0         0       0030       ·      -1      -1      -1      -1      -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      -1      -1      -1      -1
    50      62 - VK_NUMPAD2         0       0032       ·      -1      -1      -1      -1      -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      -1      -1      -1      -1
    4b      64 - VK_NUMPAD4         0       0034       ·      -1      -1      -1      -1      -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      -1      -1      -1      -1
    4d      66 - VK_NUMPAD6         0       0036       ·      -1      -1      -1      -1      -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      -1      -1      -1      -1
    48      68 - VK_NUMPAD8         0       0038       ·      -1      -1      -1      -1      -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      -1      -1      -1      -1
    37      6a - VK_MULTIPLY        0       002a       ·    002a       ·      -1      -1      -1      -1      -1      -1      -1      -1      -1      -1      -1      -1
    4e      6b - VK_ADD             0       002b       ·    002b       ·      -1      -1      -1      -1      -1      -1      -1      -1      -1      -1      -1      -1
    4a      6d - VK_SUBTRACT        0       002d       ·    002d       ·      -1      -1      -1      -1      -1      -1      -1      -1      -1      -1      -1      -1
    53      6e - VK_DECIMAL         0       002e       ·    002e       ·      -1      -1      -1      -1      -1      -1      -1      -1      -1      -1      -1      -1
    35      6f - VK_DIVIDE          0       002f       ·    002f       ·      -1      -1      -1      -1      -1      -1      -1      -1      -1      -1      -1      -1
    27      ba - VK_OEM_1           0       003b       ·    003a       ·      -1      -1      -1      -1    00b0       ·      -1      -1    00b4@      ·    02dd@      ·
    0d      bb - VK_OEM_PLUS        0       003d       ·    002b       ·      -1      -1      -1      -1    00ac       ·      -1      -1    00b8@      ·    02db@      ·
    33      bc - VK_OEM_COMMA       0       002c       ·    0027       ·      -1      -1      -1      -1    003c       ·      -1      -1    2015       ·    00d7       ·
    0c      bd - VK_OEM_MINUS       0       002d       ·    005f       ·      -1      -1      -1      -1      -1      -1      -1      -1      -1      -1    00bf       ·
    34      be - VK_OEM_PERIOD      0       002e       ·    0022       ·      -1      -1      -1      -1    003e       ·      -1      -1    02d9@      ·    00f7       ·
    35      bf - VK_OEM_2           1       00e9    00c9    00c9    00e9      -1      -1      -1      -1      -1      -1      -1      -1      -1      -1    02d9@      ·
    28      c0 - VK_OEM_3           1       00e8    00c8    00c8    00e8      -1      -1      -1      -1      -1      -1      -1      -1      -1      -1    02c7@      ·
    1a      db - VK_OEM_4           0       005e@      ·    00a8@      ·      -1      -1      -1      -1    0060@      ·      -1      -1      -1      -1    02da@      ·
    2b      dc - VK_OEM_5           1       00e0    00c0    00c0    00e0      -1      -1      -1      -1      -1      -1      -1      -1      -1      -1    02d8@      ·
    1b      dd - VK_OEM_6           1       00e7    00c7    00c7    00e7      -1      -1      -1      -1    007e@      ·      -1      -1    007e       ·    00af@      ·
    29      de - VK_OEM_7           0       002f       ·    005c       ·      -1      -1      -1      -1    007c       ·      -1      -1      -1      -1    00ad       ·
    56      e2 - VK_OEM_102         1       00f9    00d9    00d9    00f9      -1      -1      -1      -1      -1      -1      -1      -1      -1      -1    00a6       ·

    0x00b4  25
            0x0020  0x00b4
            0x0061  0x00e1
            0x0041  0x00c1
            0x0063  0x0107
            0x0043  0x0106
            0x0065  0x00e9
            0x0045  0x00c9
            0x0069  0x00ed
            0x0049  0x00cd
            0x006c  0x013a
            0x004c  0x0139
            0x006e  0x0144
            0x004e  0x0143
            0x006f  0x00f3
            0x004f  0x00d3
            0x0072  0x0155
            0x0052  0x0154
            0x0073  0x015b
            0x0053  0x015a
            0x0075  0x00fa
            0x0055  0x00da
            0x0079  0x00fd
            0x0059  0x00dd
            0x007a  0x017a
            0x005a  0x0179

    0x02dd  5
            0x0020  0x02dd
            0x006f  0x0151
            0x004f  0x0150
            0x0075  0x0171
            0x0055  0x0170

    0x00b8  17
            0x0020  0x00b8
            0x0063  0x00e7
            0x0043  0x00c7
            0x0067  0x0123
            0x0047  0x0122
            0x006b  0x0137
            0x004b  0x0136
            0x006c  0x013c
            0x004c  0x013b
            0x006e  0x0146
            0x004e  0x0145
            0x0072  0x0157
            0x0052  0x0156
            0x0073  0x015f
            0x0053  0x015e
            0x0074  0x0163
            0x0054  0x0162

    0x02db  9
            0x0020  0x02db
            0x0061  0x0105
            0x0041  0x0104
            0x0065  0x0119
            0x0045  0x0118
            0x0069  0x012f
            0x0049  0x012e
            0x0075  0x0173
            0x0055  0x0172

    0x02d9  10
            0x0020  0x02d9
            0x0063  0x010b
            0x0043  0x010a
            0x0065  0x0117
            0x0045  0x0116
            0x0067  0x0121
            0x0047  0x0120
            0x0049  0x0130
            0x007a  0x0017
            0x005a  0x017b

    0x02c7  19
            0x0020  0x02c7
            0x0063  0x010d
            0x0043  0x010c
            0x0064  0x010f
            0x0044  0x010e
            0x0065  0x011b
            0x0045  0x011a
            0x006c  0x013e
            0x004c  0x013d
            0x006e  0x0148
            0x004e  0x0147
            0x0072  0x0159
            0x0052  0x0158
            0x0073  0x0161
            0x0053  0x0160
            0x0074  0x0165
            0x0054  0x0164
            0x007a  0x017e
            0x005a  0x017d

    0x005e  25
            0x0020  0x005e
            0x0061  0x00e2
            0x0041  0x00c2
            0x0063  0x0109
            0x0043  0x0108
            0x0065  0x00ea
            0x0045  0x00ca
            0x0067  0x011d
            0x0047  0x011c
            0x0068  0x0125
            0x0048  0x0124
            0x0069  0x00ee
            0x0049  0x00ce
            0x006a  0x0135
            0x004a  0x0134
            0x006f  0x00f4
            0x004f  0x00d4
            0x0073  0x015d
            0x0053  0x015c
            0x0075  0x00fb
            0x0055  0x00db
            0x0077  0x0175
            0x0057  0x0174
            0x0079  0x0177
            0x0059  0x0176

    0x00a8  13
            0x0020  0x00a8
            0x0061  0x00e4
            0x0041  0x00c4
            0x0065  0x00eb
            0x0045  0x00cb
            0x0069  0x00ef
            0x0049  0x00cf
            0x006f  0x00f6
            0x004f  0x00d6
            0x0075  0x00fc
            0x0055  0x00dc
            0x0079  0x00ff
            0x0059  0x0178

    0x0060  11
            0x0020  0x0060
            0x0061  0x00e0
            0x0041  0x00c0
            0x0065  0x00e8
            0x0045  0x00c8
            0x0069  0x00ec
            0x0049  0x00cc
            0x006f  0x00f2
            0x004f  0x00d2
            0x0075  0x00f9
            0x0055  0x00d9

    0x02da  5
            0x0020  0x02da
            0x0061  0x00e5
            0x0041  0x00c5
            0x0075  0x016f
            0x0055  0x016e

    0x02d8  7
            0x0020  0x02d8
            0x0061  0x0103
            0x0041  0x0102
            0x0067  0x011f
            0x0047  0x011e
            0x0075  0x016d
            0x0055  0x016c

    0x007e  11
            0x0020  0x007e
            0x0061  0x00e3
            0x0041  0x00c3
            0x0069  0x0129
            0x0049  0x0128
            0x006e  0x00f1
            0x004e  0x00d1
            0x006f  0x00f5
            0x004f  0x00d5
            0x0075  0x0169
            0x0055  0x0168

    0x00af  11
            0x0020  0x00af
            0x0061  0x0101
            0x0041  0x0100
            0x0065  0x0113
            0x0045  0x0112
            0x0069  0x012b
            0x0049  0x012a
            0x006f  0x014d
            0x004f  0x014c
            0x0075  0x016b
            0x0055  0x016a

    Now the code could have been expanded to handle more "special" shift states, though as I mentioned back in Part 9a, this does not seem as immediately important given the fact that there are no other keyboards defined at the moment that make use of this functionality.

    Obviously if the code needed to stretch in that direction, it would make sense to take a few additional steps:

    • Keep an array of 64 bools, one per shift state, and globally avoid displaying any shift state that has no entries in it (or alternately create a VirtualKeys collection that will check this information later);
    • Keep a list of those 64 names so that they can be easily filled in when dumping out the info;
    • Make the dump info and VirtualKey,LayoutRow able to dynamically mak use of the above info.

    For now, I just can't believe how hard it is to get some of this infornation!

     

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

  • Sorting it all Out

    To Sara, from Gretchen (via Michael‽)

    • 7 Comments

    (If there was ever a use for the INTERROBANG, this post would likely be it.... I wish it would work in the title of the post!)

    I am not sure where the term 'blog pimping' came from, but I guess it is time that I used it, since everyone else seems to be....

    The process first started for me when I got some help in making my blog look more like I wanted it to from Josh Ledgard, as I described here.

    In other (sad) news, the better half of blogging power couple #2 (Gretchen) is leaving Microsoft, and her last day is Friday. :-(

    But the reason I am posting right now is that Sara Ford (who as an aside now reports to Josh) asked Gretchen

    How did you get this on your blog?  CSS override?  New Community Server feature?

    This comments section should be used to discuss the current post.

    If you have a question about Microsoft technical recruiting, please visit our
    FAQ or search our archives.

    If you have a specific question or feedback about your recruiting experience, please contact your Microsoft recruiter directly. Recruiters are always happy to hear from you!

    If you have experienced a technical issue with the Microsoft careers website, please
    log your bug.

    And of course, if you would like to apply for opportunities at Microsoft (that would be great!), you can
    send your resume to us today!

    Thanks as always for your comments!

    The JobsBlog Team

    Of course what was funny was that Sazra asked the question about an hour after the change went live on JobsBlog!

    I actually started doing it because of the problems I was having with anonymous comments just this last February, some of which have been resolved by some of which have not yet. Anyway, this last Friday Gretchen noticed and asked me how I did it.... so I sent her the code and a few days later she went live with it (check any recent post on JobsBlog to see what she put right above where you enter comments!).

    I'll have to ask Gretchen how she gets people noticing the bottom of her posts so much more quickly than they notice mine!

    So Gretchen answered Sara's question thusly:

    It's an addition of some code in the "news" section. Michael ... I sense a blog hacking post in your future. ;-)

    You can think of this post as a present for Sara, from Gretchen. It never would have occurred to me to write this up if she hadn't had this premonition. Or does it not count as a premonition if the person being psychic suggests it? :-)

    Anyway, for the how it works piece, the actual pimping in this post....

    Two parts to the script I added:

    • One piece for posts where comments are closed to replace the "New comments to this post are disabled" text that Community Server generically adds, and
    • One to handle the posts that are still taking comments (where Community Server adds the generic "What do you think?" text in an H3).

    The script:

    <script language=javascript type="text/javascript"><!--
    window.onload = function() {
      // Enumerate headers after document is loaded.
      var i, h3s = document.body.getElementsByTagName("h3");
      for (i = 0; i < h3s.length; i++) {
        var elem = h3s.item(i);
        if (elem.innerHTML == "What do you think?") {
            elem.innerHTML = "<big>So what do you think about it?</big><br><br><small><small>A few words about comments:<ul><li>No HTML tags are currently supported;</li><li>Comments can only be made up to 90 days after the post originally went online.</li></ul></small></small>";
        }

      // Enumerate italics after document is loaded.
      var k, EMs = document.body.getElementsByTagName("em");
      for (k = 0; k < EMs.length; k++) {
        var words = EMs.item(k);
        if (words.innerText == "New Comments to this post are disabled") {
          words.innerHTML = "Comments are closed after 90 days. Frustrating? Perhaps. But you could maybe <a href="/michkap/articles/512061.asp">Suggest a Topic</a>, instead?"
    }

    And that's it -- you can put as much or as little text as you like, or even HTML if you want to....

    Enjoy!

     

    This post brought to you by "" (U+203d, a.k.a. INTERROBANG)

  • Sorting it all Out

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

    • 4 Comments

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

    Today, we're going to take a look at the more complex shift states -- like when a keyboard layout takes the Control, Alt, and/or Shift keys on the right side of the keyboard and give yourself up to three additional "shift" keys. This gives you a theoretical total of up to 62 potential shift states (64 - 2, since we do not use the LEFT ALT or the LEFT ALT+LEFT SHIFT).

    Think of the X1, X2, and X3 below as being the three RIGHT shift state keys (without forcing the identity of any of them, specifically):

      _T("                 "),   /* 0           */
      _T("LS               "),   /* 1           */
      _T("   LC            "),   /* 2           */
      _T("LS+LC            "),   /* 3           */
      _T("      LA         "),   /* 4:not used  */
      _T("LS+   LA         "),   /* 5:not used  */
      _T("   LC+LA         "),   /* 6           */
      _T("LS+LC+LA         "),   /* 7           */
      _T("         X1      "),   /* 8           */
      _T("LS+      X1      "),   /* 9           */
      _T("   LC+   X1      "),   /* 10          */
      _T("LS+LC+   X1      "),   /* 11          */
      _T("      LA+X1      "),   /* 12          */
      _T("LS+   LA+X1      "),   /* 13          */
      _T("   LC+LA+X1      "),   /* 14          */
      _T("LS+LC+LA+X1      "),   /* 15          */
      _T("            X2   "),   /* 16          */
      _T("LS+         X2   "),   /* 17          */
      _T("   LC+      X2   "),   /* 18          */
      _T("LS+LC+      X2   "),   /* 19          */
      _T("      LA+   X2   "),   /* 20          */
      _T("LS+   LA+   X2   "),   /* 21          */
      _T("   LC+LA+   X2   "),   /* 22          */
      _T("LS+LC+LA+   X2   "),   /* 23          */
      _T("         X1+X2   "),   /* 24          */
      _T("LS+      X1+X2   "),   /* 25          */
      _T("   LC+   X1+X2   "),   /* 26          */
      _T("LS+LC+   X1+X2   "),   /* 27          */
      _T("      LA+X1+X2   "),   /* 28          */
      _T("LS+   LA+X1+X2   "),   /* 29          */
      _T("   LC+LA+X1+X2   "),   /* 30          */
      _T("LS+LC+LA+X1+X2   "),   /* 31          */
      _T("               X3"),   /* 32          */
      _T("LS+            X3"),   /* 33          */
      _T("   LC+         X3"),   /* 34          */
      _T("LS+LC+         X3"),   /* 35          */
      _T("      LA+      X3"),   /* 36          */
      _T("LS+   LA+      X3"),   /* 37          */
      _T("   LC+LA+      X3"),   /* 38          */
      _T("LS+LC+LA+      X3"),   /* 39          */
      _T("         X1+   X3"),   /* 40          */
      _T("LS+      X1+   X3"),   /* 41          */
      _T("   LC+   X1+   X3"),   /* 42          */
      _T("LS+LC+   X1+   X3"),   /* 43          */
      _T("      LA+X1+   X3"),   /* 44          */
      _T("LS+   LA+X1+   X3"),   /* 45          */
      _T("   LC+LA+X1+   X3"),   /* 46          */
      _T("LS+LC+LA+X1+   X3"),   /* 47          */
      _T("            X2+X3"),   /* 48          */
      _T("LS+         X2+X3"),   /* 49          */
      _T("   LC+      X2+X3"),   /* 50          */
      _T("LS+LC+      X2+X3"),   /* 51          */
      _T("      LA+   X2+X3"),   /* 52          */
      _T("LS+   LA+   X2+X3"),   /* 53          */
      _T("   LC+LA+   X2+X3"),   /* 54          */
      _T("LS+LC+LA+   X2+X3"),   /* 55          */
      _T("         X1+X2+X3"),   /* 56          */
      _T("LS+      X1+X2+X3"),   /* 57          */
      _T("   LC+   X1+X2+X3"),   /* 58          */
      _T("LS+LC+   X1+X2+X3"),   /* 59          */
      _T("      LA+X1+X2+X3"),   /* 60          */
      _T("LS+   LA+X1+X2+X3"),   /* 61          */
      _T("   LC+LA+X1+X2+X3"),   /* 62          */
      _T("LS+LC+LA+X1+X2+X3"),   /* 63          */

    And thus we have 64 shift states. I left X1, X2, and X3 defined as there is no standard for the order, and the only keyboard that has them defined uses the RIGHT CONTROL character, which is not exactly reflecting the order on the left side....

    Now I mentioned that this is a theoretical limit, and my belief is based on several factors:

    • No human could comfortably type in all these different shift states comfortably;
    • No human could really remember all of these shift states;
    • Factors like ALTGR would interfere ptretty heavily in many cases....
    • Only two additional shift states have ever shipped with Windows, so by aplying the first Tester's Axiom (if you have not tested it, assume it is broken), it is likely that they won't all work;
    • The following comment exists in the tool that creates the layouts:
         //
         // Note that we do not really document additional state labels
         // past 7 very well. But this may actually be a good thing.
         //
    • And no, MSKLC does not support any of this. :-)

    Anticipating another question -- I am not covering how to create such keyboards in this post; I am still trying to figure out how to interrogate a layout to get the information from it....

    So, I talked about VkKeyScan and VkKeyScanEx recently, so let's make some good use of it. If I load up the Canadian Multilingual Standard keyboard and call VkKeyScanEx to ask for some of the characters that are only in those shift states, here is what comes back:

    • U+007c (AltGR+OEM_7) -- 0x06de
    • U+00b9 (Rt. CTRL+1) -- 0x0831
    • U+00b3 (Rt. CTRL+3) -- 0x0833
    • U+00a1 (SHFT+Rt. CTRL+1) -- 0x0931

    So, the VkKeyScanEx docs got it wrong again -- it was right about that 0x06 in the high byte is CRTL+ALT or AltGR, but that 0x08 and 0x09 in the high byte are not Hankaku an SHFT+Hankaku! They are both defined by the keyboard layout.

    But, trying to set the "pressed" bit on VK_RCONTROL in our FillKeyState function created in prior posts does not create these characters. So how can we get at them?

    The trick is in the way the keyboard actually remaps to another VK value and makes it the one that will act as the shifting key. Then, using MapVirtualKeyEx and two of it's amazing uMapType values:

    • 1 -- uCode is a scan code and is translated into a virtual-key code that does not distinguish between left- and right-hand keys. If there is no translation, the function returns 0.
    • 3 -- Windows NT/2000/XP: uCode is a scan code and is translated into a virtual-key code that distinguishes between left- and right-hand keys. If there is no translation, the function returns 0.

    And running code like the following:

    for(KeysEx vk = KeysEx.None; vk <= KeysEx.VK_OEM_CLEAR; vk++) {
        uint sc = VirtualKey.MapVirtualKeyEx((uint)vk, 0, hkl);
        uint vkL = VirtualKey.MapVirtualKeyEx(sc, 1, hkl);
        uint vkR = VirtualKey.MapVirtualKeyEx(sc, 3, hkl);
        if(vkL != vkR) {
            if((uint)vk != vkL) {
                Console.WriteLine("{0}\t{1:x2}\t{2}\t{3}",
                    vk.ToString(),
                    sc,
                    ((KeysEx)vkL).ToString(),
                    ((KeysEx)vkR).ToString());
            }
        }
    }

    The following output gets produced on the US and most other keyboards:

    VK_LSHIFT       2a      VK_SHIFT        VK_LSHIFT
    VK_RSHIFT       36      VK_SHIFT        VK_RSHIFT
    VK_LCONTROL     1d      VK_CONTROL      VK_LCONTROL
    VK_RCONTROL     1d      VK_CONTROL      VK_LCONTROL
    VK_LMENU        38      VK_MENU VK_LMENU
    VK_RMENU        38      VK_MENU VK_LMENU

    Notice how the first column, which lists the original VK, shows only six VKs that will not roundtrip the same way between a regular VK->SC->VK mapping and one that distinguishes between the left and right hand keys? And it is exactly the ones we expected!

    If you look carefully at the VK_RMENU and VK_RCONTROL virtual keys, you will see that they are mapped to their left handed counterparts; this is due to the fact that they do not haved defined scan codes that are different other than by being EXTENDED scan codes, and MapVirtualKeyEx strips out the extended bit for reasons that have never been adequately explained to me....

    But that is okay -- we just need to know the identity of the six keys; it is only of secondary importance to distinguish them from each other....

    Anyway, running that same code again, the following output gets produced on the Canadian Multilingual Standard keyboard:

    VK_LSHIFT       2a      VK_SHIFT        VK_LSHIFT
    VK_RSHIFT       36      VK_SHIFT        VK_RSHIFT
    VK_LCONTROL     1d      VK_CONTROL      VK_LCONTROL
    VK_LMENU        38      VK_MENU VK_LMENU
    VK_RMENU        38      VK_MENU VK_LMENU
    VK_OEM_8        1d      VK_CONTROL      VK_LCONTROL

    Do you see what happened? The VK_RCONTROL is no longer returning different results, as the VK_OEM_8 key has been mapped to it. If we treat the VK_OEM_8 key as our shifting key in the FillKeyState function, then we suddenly have access to those two new shift states!

    So what we would have to theoretically do here would be to look back at the list of shift states from earlier and assume that we now had 16 shift states (the 8 old ones and now 8 new ones), but I'm going to just pick up the additional two since I know that is all that is in the keyboard.

    It is important to remember that first tester's axiom and not write a bunch of code that I can't really test effectively....

    I'll post the updated version of our code later today (in Part #9b), along with the full version that gets spit out with the Canadian Multilingual Standard keyboard....

  • Sorting it all Out

    Case sensitive Visual Basic!

    • 2 Comments

    Ok, we start with the following code -- just paste it into Notepad, and save in UTF-8 or Unicode encoding with a nice generic name like Module1.vb:

    Module Module1

        Sub Main()
            Call Pröcedure1()
        End Sub

        Sub Pröcedure1()
            Console.WriteLine("Schrödinger's cat is not dead.")
        End Sub

        Sub PRÖCEDURE1()
            Console.WriteLine("Schrödinger's cat is dead.")
        End Sub

    End Module

     

    Compile it in with VBC (1.0, 1.1, or 2.0):

    C:\TMP\Sample\CaseSensitiveVB>vbc Module1.vb

    Then run the code:

    C:\TMP\Sample\CaseSensitiveVB>Module1.exe
    Schrödinger's cat is dead.

    The appearance here is that VB is not only allowing function names that vary in no way other than case to co-exist, but also that will appear to be calling the wrong function sometimes!

    Hmmm.... how did that happen?

    I am sure regular readers will know what I did here; it's a bit like that Normalization as obfuscation in C# post, but with the case variation thrown in.

    • String #1 uses U+00f6, a.k.a. LATIN SMALL LETTER O WITH DIAERESIS
    • String #2 uses U+006f U+0308, a.k.a. LATIN SMALL LETTER O and COMBINING DIAERESIS
    • String #3 uses U+00d6, a.k.a. LATIN CAPITAL LETTER O WITH DIAERESIS

    So now VB gets to not only be weirdly obfuscated, it also has lost its case insensitivity!

    Doesn't this seem worthy of a fix? Even if it requires a different compiler setting for backcompat with the people crazy enough to code this way, for the sanity of those who didn't want case sensitive VB it seems like a good idea....

    (I realize I have thrown down the gauntlet to the VB team to try to get this issue fixed now!)

    :-)

     

    This post brought to you by "Ö" (U+00d6, a.k.a. LATIN CAPITAL LETTER O WITH DIAERESIS)
    (A character that is proud to not be part of the dyslexic heritage that will confuse people between U+00f6 and U+006f!)

     

  • Sorting it all Out

    I daresay it is often <= -1

    • 10 Comments

    A lot of the documentation for functions in the NLS API makes the point that for a length parameter you can either pass the length or you can pass -1 to signal that the string is NULL terminated.

    (I have mentioned this previously.)

    As it turns out, most of the functions will treat any negative value this way, and it has been that way since the functions were first written (in many/most cases, over a decade ago).

    So, let's look at them one at a time....

    • CompareString <= -1
    • CompareStringOrdinal == -1
    • FindNLSString == -1
    • FoldString <= -1
    • GetStringTypeEx <= -1
    • GetStringType[A|W] <= -1
    • IsNormalizedString == -1
    • LCMapString <= -1
    • MultiByteToWideChar <= -1
    • NormalizeString == -1
    • WideCharToMultiByte <= -1

    Believe it or not, some of the comments specifically talk about the parameter being equal to -1 even though the check underneath the comment is for less than or equal to one.

    Of course we can't change the behavior due to the backcompat concerns, though maybe the comments should be updated someday. Though the fact that no one has pointed it out before makes me wonder if anyone ever reads them anyway? :-)

    If you look at the functions, you may even see the pattern -- it is only functions added later that have the "equal to -1" behavior. So that is something, right? We are consistent, in our own way.

    In any case, it is safer to just pass -1 since some functions work that way. And it is an easier rule to remember than a big list of functions....

     

    This post brought to you by "-" (U+002d, a.k.a. HYPHEN-MINUS)

  • Sorting it all Out

    Not a complete waste of space

    • 4 Comments

    Well, I looked at Hugh's cartoon:

    (you can click on the image to go to his site, and get a much bigger copy of the cartoon).

    I am not sure where it puts me since what I do actually gives me neither power nor money.

    It is true that I am able to turn useless oxygen into valuable carbon dioxide for the plants, but since I have no plants there is no immediate benefit to that either. :-)

    I guess it means that there is more to life than a gaping void or something....

  • Sorting it all Out

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

    • 7 Comments

    (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:

    • It can act as the equivalent of a persistent SHIFT for selected keys, or
    • It can push the keyboard into an entirely different layout with its own independent SHIFT state.

    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:

    • the harder shift states
    • chained dead keys

    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!

Page 3 of 5 (62 items) 12345