Postings are provided as is with no warranties, and confer no rights. Opinions expressed here are my own delusions; my employers at best shake their heads and sigh, at worst repudiate the content with extreme prejudice, whenever it manages to appear on their radar.
This blog is unsuitable for overly sensitive persons with low self-esteem and/or no sense of humour. Proceed at your own risk. Use as directed. Do not spray directly into eyes. Caution: filling may be hot. Do not give to children under 60 years of age. Not labeled for individual sale. Do not read 'natas teews ym' backwards. Objects in mirror are closer than they appear. Chew before swallowing. Do not bend, fold, spindle or mutilate. Do not take orally unless directed by a physician. Remove baby before folding stroller. Not for use on unexplained calf pain.
A nice FLAIR (FLuid Attenuated Inversion Recovery) view from the not-too-distant past. Every abnormality you can see on this scan (and there is more than one!) is asymptomatic at present. Alongside is a picture of me walking the walls at Fremont Studios, a sign of a damaged brain.
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)
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.
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.
MSIDBOPEN_PATCHFILE
MsiOpenDatabase
ERROR_OPEN_FAILED
MSIDBOPEN_READONLY
#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.
LPCTSTR
LPCWSTR
UNICODE
wchar_t*
sizeof(wchar_t)
MsiOpenDatabaseA
MsiOpenDatabaseW
For the automation method Installer.OpenDatabase the second parameter must be set to msiOpenDatabaseModePatchFile to open a patch, which is always defined as 32.
Installer.OpenDatabase
msiOpenDatabaseModePatchFile
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)
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....
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!
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=INTELif not exist ..\lib\INTEL md ..\lib\INTELcd mfcnmake / DEBUG=0 _OD_EXT= PLATFORM=INTELcl @C:\DOCUME~1\MSMITH~1.COR\LOCALS~1\Temp\nm6773.tmpobjcore.cppD:\MSVC .Net\Vc7\atlmfc\include\afxv_w32.h(134) : error C2220: warning treated as error - no object file generatedD:\MSVC .Net\Vc7\atlmfc\include\afxv_w32.h(134) : warning C4005: 'NOSERVICE' : macro redefinitioncommand-line arguments : see previous definition of 'NOSERVICE'D:\MSVC .Net\Vc7\atlmfc\include\atldef.h(261) : warning C4005: 'WIN32_LEAN_AND_MEAN' : macro redefinitioncommand-line arguments : see previous definition of 'WIN32_LEAN_AND_MEAN'
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=INTELif not exist ..\lib\INTEL md ..\lib\INTELcd mfcnmake / DEBUG=0 _OD_EXT= PLATFORM=INTELcl @C:\DOCUME~1\MSMITH~1.COR\LOCALS~1\Temp\nm6773.tmpobjcore.cppD:\MSVC .Net\Vc7\atlmfc\include\afxv_w32.h(134) : error C2220: warning treated as error - no object file generatedD:\MSVC .Net\Vc7\atlmfc\include\afxv_w32.h(134) : warning C4005: 'NOSERVICE' : macro redefinitioncommand-line arguments : see previous definition of 'NOSERVICE'D:\MSVC .Net\Vc7\atlmfc\include\atldef.h(261) : warning C4005: 'WIN32_LEAN_AND_MEAN' : macro redefinitioncommand-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. :-)
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....
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); } }
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)
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.
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)
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
I tend to use the following (ie, excluding private use & noncharacters).
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
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!
(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 -10e 08 - VK_BACK 0 0008 · 0008 · 007f · -1 -1 -1 -1 -1 -1 -1 -1 -1 -17c 09 - VK_TAB 0 0009 · 0009 · -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -11c 0d - VK_RETURN 0 000d · 000d · 000a · -1 -1 -1 -1 -1 -1 -1 -1 -1 -101 1b - VK_ESCAPE 0 001b · 001b · 001b · -1 -1 -1 -1 -1 -1 -1 -1 -1 -139 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 -102 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 -104 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 -126 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 -115 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 -152 60 - VK_NUMPAD0 0 0030 · -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -14f 61 - VK_NUMPAD1 0 0031 · -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -150 62 - VK_NUMPAD2 0 0032 · -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -151 63 - VK_NUMPAD3 0 0033 · -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -14b 64 - VK_NUMPAD4 0 0034 · -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -14c 65 - VK_NUMPAD5 0 0035 · -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -14d 66 - VK_NUMPAD6 0 0036 · -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -147 67 - VK_NUMPAD7 0 0037 · -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -148 68 - VK_NUMPAD8 0 0038 · -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -149 69 - VK_NUMPAD9 0 0039 · -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -137 6a - VK_MULTIPLY 0 002a · 002a · -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -14e 6b - VK_ADD 0 002b · 002b · -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -14a 6d - VK_SUBTRACT 0 002d · 002d · -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -153 6e - VK_DECIMAL 0 002e · 002e · -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -135 6f - VK_DIVIDE 0 002f · 002f · -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -127 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 0x01620x02db 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 0x017d0x005e 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 0x016e0x02d8 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:
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!
(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:
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)
(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:
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:
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:
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_LSHIFTVK_RSHIFT 36 VK_SHIFT VK_RSHIFTVK_LCONTROL 1d VK_CONTROL VK_LCONTROLVK_RCONTROL 1d VK_CONTROL VK_LCONTROLVK_LMENU 38 VK_MENU VK_LMENUVK_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_LSHIFTVK_RSHIFT 36 VK_SHIFT VK_RSHIFTVK_LCONTROL 1d VK_CONTROL VK_LCONTROLVK_LMENU 38 VK_MENU VK_LMENUVK_RMENU 38 VK_MENU VK_LMENUVK_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....
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
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.exeSchrö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.
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!)
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....
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)
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....
(Previous posts in this series: Parts 0, 1, 2, 3, 4, 5, 6, and 7)
If you have been following this series, you may have noticed that as it moves concentrically outward it gets harder and harder to figure out why things are so freaking complicated to figure out.
(Or maybe that is just me; but in any case it seems clear that while things are [almost] always available, no one really seemed interested in making in easy for those trying to find out about a layout!)
For this post, I am going to swing around and pick up all the information about the CAPS LOCK key. So if you are one of those who hates the CAPS LOCK key, you'll want to give this one a miss and find some random web site to look at instead....
Now there are two different, mutually exclusive purposes per key:
Of course, in keeping with the general practice of making nothing easy (!), there is no direct way to query for this information. You essentially have to query:
(There is a related per-keyboard layout setting known as SHIFTLOCK which I have discussed before; I'll talk more about it another day)
The rules are simple -- for any particular VK, if #1 does not equal #2, #2 does equal #3, and #2/#3 is assigned, then you are in the place that MSKLC defines as "CAPS == SHIFT", the basic CAPSLOCK thing.
And then you repeat the same process for ALTGR, SHIFT+ALTGR, and CAPSLOCK+ALTGR, to find out if you have an ALTGRCAPSLOCK thing going on.
Then to figure out whether an SGCAPS thing is going on, any time that neither BASE nor SHIFT matches CAPSLOCK, and/or any time that neither BASE nor SHIFT matches SHIFT+CAPSLOCK (in other words if you have three entirely different assignments here), then you have detected an SGCAPS thing.
(And there is no ALTGRSGCAPS, as I pointed out in Just one code point for SGCAPS.... -- along with the obvious limitation I pointed out in the post name -- as Nick mentioned in the comments, it is UTF-16 code unit we are talking about).
Of course, to make things even more confusing, you can create (with MSKLC or the DDK) a keyboard that uses SGCAPS but (by nature of the assigments you create) looks identical to one without SGCAPS. In which case you will not be able to detect the difference, at all.
For the most part, Microsoft-supplied keyboards in Windows do not do this sort of thing. Though if they did there would be no way for you to tell. :-)
Now to query about the CAPSLOCK, our updated FillKeyState procedure already has what it needs:
private static void FillKeyState(KeysEx[] lpKeyState, ShiftState ss, bool fCapsLock) { lpKeyState[(int)KeysEx.VK_SHIFT] = (((ss & ShiftState.Shft) != 0) ? (KeysEx)0x80 : (KeysEx)0x00); lpKeyState[(int)KeysEx.VK_CONTROL] = (((ss & ShiftState.Ctrl) != 0) ? (KeysEx)0x80 : (KeysEx)0x00); lpKeyState[(int)KeysEx.VK_MENU] = (((ss & ShiftState.Menu) != 0) ? (KeysEx)0x80 : (KeysEx)0x00); lpKeyState[(int)KeysEx.VK_CAPITAL] = (fCapsLock ? (KeysEx)0x01 : (KeysEx)0x00);}
It properly handles the difference between a keypress (where you set the high bit) and a toggle key pressed (where you set the low bit), just as I discussed in Is the CAPS LOCK on?
Another interesting little detail I found out working on MSKLC -- ToUnicodeEx with the CTRL shift state has an internal knowledge about the VK_A ~ VK_Z Virtual Key values and it will produce (as characters) the control characters represented by the VK values minus 0x40, when the conversion rule is not provided in keyboard layout files.
Many keyboards also include a few of control characters in the CTRL shift state in other VK values, which is a good thing, since there are programs that actually depend on them (e.g. Microsoft's Telnet searches for the key that contains 0x1b so that it can say on startup something like "Escape Character is 'CTRL+]'").
It is of course a bad thing that MSKLC was stripping all of these characters (rather than just the ones in VK_A ~ VK_Z), and worse that if anyone adds them back explicitly we give a warning about this. But we do live and learn, I suppose. And no one pays attention to warnings anyway. :-)
The rest of the change is just some overhead, storing these updated states and providing properties to get them (which would probably come in handy for people trying to do more than just display what is in the keyboard). Mostly self-explanatory, though I'll talk about the meaning of the reporting stuff in LayoutRow in a bit.
One interesting note -- I am taking a very different approach here than I did in MSKLC's method for showing the same information. Mainly because I can, and because all of the efforts in MSKLC to try and mask the complexity are fundamentally different than my efforts here (to expose the complexity!)....
Here is the new version of the code (as usual the stuff that is changed is black):
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 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 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; } }
// 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));
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); }
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; }
// Now fill the key state for the potential base character FillKeyState(lpKeyState, ss, (caps == 0 ? false : true));
// 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; } }
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; } }
// 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 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 -10e 08 - VK_BACK 0 0008 · 0008 · 007f · -1 -1 -1 -1 -1 -17c 09 - VK_TAB 0 0009 · 0009 · -1 -1 -1 -1 -1 -1 -1 -11c 0d - VK_RETURN 0 000d · 000d · 000a · -1 -1 -1 -1 -1 -101 1b - VK_ESCAPE 0 001b · 001b · 001b · -1 -1 -1 -1 -1 -139 20 - VK_SPACE 0 0020 · 0020 · 0020 · -1 -1 -1 -1 -1 -10b 30 - VK_0 2 0030 · 0028 05c1 -1 -1 -1 -1 -1 -1 -1 -102 31 - VK_1 2 0031 · 0021 05b1 -1 -1 -1 -1 -1 -1 -1 -103 32 - VK_2 2 0032 · 0040 05b2 -1 -1 -1 -1 -1 -1 -1 -104 33 - VK_3 2 0033 · 0023 05b3 -1 -1 200e · -1 -1 -1 -105 34 - VK_4 2 0034 · 0024 05b4 -1 -1 200f · 20aa · -1 -106 35 - VK_5 2 0035 · 0025 05b5 -1 -1 -1 -1 -1 -1 -1 -107 36 - VK_6 2 0036 · 005e 05b6 -1 -1 001e · -1 -1 -1 -108 37 - VK_7 2 0037 · 0026 05b7 -1 -1 -1 -1 -1 -1 -1 -109 38 - VK_8 2 0038 · 002a 05b8 -1 -1 -1 -1 -1 -1 -1 -10a 39 - VK_9 2 0039 · 0029 05c2 -1 -1 -1 -1 -1 -1 -1 -11e 41 - VK_A 1 05e9 0041 0041 05e9 -1 -1 -1 -1 -1 -1 -1 -130 42 - VK_B 1 05e0 0042 0042 05e0 -1 -1 -1 -1 -1 -1 -1 -12e 43 - VK_C 1 05d1 0043 0043 05d1 -1 -1 -1 -1 -1 -1 -1 -120 44 - VK_D 1 05d2 0044 0044 05d2 -1 -1 -1 -1 -1 -1 -1 -112 45 - VK_E 1 05e7 0045 0045 05e7 -1 -1 -1 -1 20ac · -1 -121 46 - VK_F 1 05db 0046 0046 05db -1 -1 -1 -1 -1 -1 -1 -122 47 - VK_G 1 05e2 0047 0047 05e2 -1 -1 -1 -1 -1 -1 -1 -123 48 - VK_H 1 05d9 0048 0048 05d9 -1 -1 -1 -1 05f2 · -1 -117 49 - VK_I 1 05df 0049 0049 05df -1 -1 -1 -1 -1 -1 -1 -124 4a - VK_J 1 05d7 004a 004a 05d7 -1 -1 -1 -1 05f1 · -1 -125 4b - VK_K 1 05dc 004b 004b 05dc -1 -1 -1 -1 -1 -1 -1 -126 4c - VK_L 1 05da 004c 004c 05da -1 -1 -1 -1 -1 -1 -1 -132 4d - VK_M 1 05e6 004d 004d 05e6 -1 -1 -1 -1 -1 -1 -1 -131 4e - VK_N 1 05de 004e 004e 05de -1 -1 -1 -1 -1 -1 -1 -118 4f - VK_O 1 05dd 004f 004f 05dd -1 -1 -1 -1 -1 -1 -1 -119 50 - VK_P 1 05e4 0050 0050 05e4 -1 -1 -1 -1 -1 -1 -1 -110 51 - VK_Q 1 002f 0051 0051 002f -1 -1 -1 -1 -1 -1 -1 -113 52 - VK_R 1 05e8 0052 0052 05e8 -1 -1 -1 -1 -1 -1 -1 -11f 53 - VK_S 1 05d3 0053 0053 05d3 -1 -1 -1 -1 -1 -1 -1 -114 54 - VK_T 1 05d0 0054 0054 05d0 -1 -1 -1 -1 -1 -1 -1 -116 55 - VK_U 1 05d5 0055 0055 05d5 -1 -1 -1 -1 05f0 · -1 -12f 56 - VK_V 1 05d4 0056 0056 05d4 -1 -1 -1 -1 -1 -1 -1 -111 57 - VK_W 1 0027 0057 0057 0027 -1 -1 -1 -1 -1 -1 -1 -12d 58 - VK_X 1 05e1 0058 0058 05e1 -1 -1 -1 -1 -1 -1 -1 -115 59 - VK_Y 1 05d8 0059 0059 05d8 -1 -1 -1 -1 -1 -1 -1 -12c 5a - VK_Z 1 05d6 005a 005a 05d6 -1 -1 -1 -1 -1 -1 -1 -152 60 - VK_NUMPAD0 0 0030 · -1 -1 -1 -1 -1 -1 -1 -1 -1 -14f 61 - VK_NUMPAD1 0 0031 · -1 -1 -1 -1 -1 -1 -1 -1 -1 -150 62 - VK_NUMPAD2 0 0032 · -1 -1 -1 -1 -1 -1 -1 -1 -1 -151 63 - VK_NUMPAD3 0 0033 · -1 -1 -1 -1 -1 -1 -1 -1 -1 -14b 64 - VK_NUMPAD4 0 0034 · -1 -1 -1 -1 -1 -1 -1 -1 -1 -14c 65 - VK_NUMPAD5 0 0035 · -1 -1 -1 -1 -1 -1 -1 -1 -1 -14d 66 - VK_NUMPAD6 0 0036 · -1 -1 -1 -1 -1 -1 -1 -1 -1 -147 67 - VK_NUMPAD7 0 0037 · -1 -1 -1 -1 -1 -1 -1 -1 -1 -148 68 - VK_NUMPAD8 0 0038 · -1 -1 -1 -1 -1 -1 -1 -1 -1 -149 69 - VK_NUMPAD9 0 0039 · -1 -1 -1 -1 -1 -1 -1 -1 -1 -137 6a - VK_MULTIPLY 0 002a · 002a · -1 -1 -1 -1 -1 -1 -1 -14e 6b - VK_ADD 0 002b · 002b · -1 -1 -1 -1 -1 -1 -1 -14a 6d - VK_SUBTRACT 0 002d · 002d · -1 -1 -1 -1 -1 -1 -1 -153 6e - VK_DECIMAL 0 002e · 002e · -1 -1 -1 -1 -1 -1 -1 -135 6f - VK_DIVIDE 0 002f · 002f · -1 -1 -1 -1 -1 -1 -1 -127 ba - VK_OEM_1 2 05e3 003b 003a 05e3 -1 -1 -1 -1 -1 -1 -1 -10d bb - VK_OEM_PLUS 2 003d · 002b 05bc -1 -1 -1 -1 -1 -1 -1 -133 bc - VK_OEM_COMMA 2 05ea 002c 003e 05ea -1 -1 -1 -1 -1 -1 -1 -10c bd - VK_OEM_MINUS 2 002d · 005f 05b9 -1 -1 001f · 05bf · -1 -134 be - VK_OEM_PERIOD 2 05e5 002e 003c 05e5 -1 -1 -1 -1 -1 -1 -1 -135 bf - VK_OEM_2 2 002e 002f 003f 002e -1 -1 -1 -1 -1 -1 -1 -129 c0 - VK_OEM_3 2 003b · 007e 05b0 -1 -1 -1 -1 -1 -1 -1 -11a db - VK_OEM_4 2 005d 005b 007d 005d 200e · -1 -1 -1 -1 -1 -12b dc - VK_OEM_5 2 005c · 007c 05bb 001c · -1 -1 -1 -1 -1 -11b dd - VK_OEM_6 2 005b 005d 007b 005b 200f · -1 -1 -1 -1 -1 -128 de - VK_OEM_7 2 002c 0027 0022 002c -1 -1 -1 -1 -1 -1 -1 -156 e2 - VK_OEM_102 0 005c · 007c · 001c · -1 -1 -1 -1 -1 -1
This is definitely getting more complicated, next time maybe we'll add a legend to the output stream so that some of the more obscure points can be documented....
There are only a few items left on my list to cover:
Although both of them are made more interesting by the fact that they are so hard to create keyboard layouts that have the features....
Though I do have the Canadian Multilingual Standard keyboard to use for the first one, which makes the example a bit easier, there are no keyboards with chained dead keys at the moment. I guess I'll have to build a few, maybe. Hmmm.
In any case, we're nearly done. Anyone not tired of this sample yet?
(Warning: it gets a bit more embarrassing if we go on!)
This post brought to you by "8" (U+0038, DIGIT EIGHT)A Unicode character that is in the very small family of those whose VK value is the same as it's code point!