针对笔记本键盘Fn+字母键不能打出数字的终极解决思路与一种实现方案

前几天用Finale打谱需要用到小键盘的数字键,然后发现Fn键加uio等字母按不出来数字,实际出来的是NumLk没有打开情况下的方向键与PgUp和PgDn等。如果打开键盘的NumLk,按字母键可以打出数字,但我需要的是Fn直接能打出小键盘数字,这样在许多地方才方便,之前用过NEC、DELL的笔记本都能做到。

很容易可以想到,之所以会打出方向键是因为Windows认为NumLk在关着。开始并没觉得这个问题很严重,就去问了下谷哥,参考到这篇帖子:

http://acer.it168.com/thread-2136449-1-1.html

该帖子提到可以通过按外接键盘上的NumLk来激活Windows的NumLk,而通常笔记本的NumLk只是用于按字母键时输入小键盘的扫描码,与操作系统的NumLk没有关系。也就是说,当电脑收到小键盘的扫描码时,再根据是否打开了NumLk来转换为虚拟键码告诉应用程序是输入数字还是输入方向。不过我手头没有外接键盘,然后帖子又说,没有外接键盘可以打开Windows的屏幕键盘也就是osk.exe然后点一下那个NumLk键,这样就能解决问题。

然而我试了之后发现按字母就直接成了数字,也就是说当我按键盘上的NumLk时,系统的NumLk也会打开,当我点击系统的NumLk时键盘的NumLk也会打开。这种情况说明我的键盘在硬件设计上就有问题,在硬件设计上混淆了笔记本键盘的NumLk功能和101键盘的NumLk的本质,多此一举地把它们绑定了

正确的笔记本键盘按下NumLk键的作用应该是将字母与Fn+字母所发送的扫描码颠倒,而这一操作和系统无关。

而我的键盘按下NumLk会向系统发送101键盘的NumLk键扫描码,系统收到NumLk按键就设置自己的NumLk状态,然后向键盘发送设置101键盘右上角的三个指示灯的指令。然后我的键盘收到NumLk灯亮起的指令后就打开了物理上的NumLk,造成字母与Fn字母反转。

了解到这样的过程后就有解决思路了。第一,拦截按下NumLk键的消息,然后向键盘发送指示灯命令,而不把这个消息告诉操作系统。第二,拦截操作系统的指示灯命令,将其中NumLk灯的数据改为第一个方面中物理键盘应该的数。

显然这个需要写驱动。个人觉得可以考虑写个PS/2键盘驱动来搞定,然而我不大会写驱动,也不想为了个期末作业去折腾各种的蓝屏,毕竟已经转行了。鉴于PS/2可以通过操作60H、64H端口来控制,所以下面给出的实现方案为基于WinIO的端口程序,供大家参考。

#include <windows.h>
#include <stdio.h>
#pragma comment(lib,"user32.lib")

bool bExitFlag = false;

bool (__stdcall* InitializeWinIo)() = NULL;
bool (__stdcall* ShutdownWinIo)() = NULL;
bool (__stdcall* GetPortVal)(WORD, PDWORD, BYTE) = NULL;
bool (__stdcall* SetPortVal)(WORD, DWORD, BYTE) = NULL;

bool bNumLk = false;
WNDPROC oldWndProc = NULL;

BYTE PortIn(WORD addr)
{
	DWORD bVal;
	GetPortVal(addr, &bVal, 1);
	return (BYTE)bVal;
}

void PortOut(WORD addr, BYTE data)
{
	SetPortVal(addr, data, 1);
}

//BYTE WaitForKbRead()
//{
//	BYTE bVal = 0;
//	do
//	{
//		Sleep(10);
//		bVal = PortIn(0x64);
//	}
//	while (!(bVal & 1));
//	return bVal;
//}

BYTE WaitForKbWrite()
{
	BYTE bVal;
	do
	{
		//Sleep(10);
		bVal = PortIn(0x64);
	}
	while (bVal & 2);
	return bVal;
}

void CloseKbInt()
{
	WaitForKbWrite();
	PortOut(0x64, 0x60);
	WaitForKbWrite();
	PortOut(0x60, 0x46);
}

void OpenKbInt()
{
	WaitForKbWrite();
	PortOut(0x64, 0x60);
	WaitForKbWrite();
	PortOut(0x60, 0x47);
}

void WriteKey(DWORD keyCode)
{
	WaitForKbWrite();
	SetPortVal(0x64, 0xD2, 1);
	WaitForKbWrite();
	SetPortVal(0x60, keyCode, 1);
}

//DWORD ReadKey()
//{
//	DWORD keyCode = 0;
//	while (!bExitFlag && WaitForKbRead() & 0x20)
//		Sleep(10);	//mouse
//	GetPortVal(0x60, &keyCode, 1);
//	return keyCode;
//}

void SetLed(BYTE led)
{
	WaitForKbWrite();
	PortOut(0x60, 0xED);
	WaitForKbWrite();
	PortOut(0x60, led);
}

void CheckLock()
{
	static int lastLed = -1;
	int led = 0;
	if ((GetKeyState(0x14) & 0xffff) != 0)
		led |= 4;
	if ((GetKeyState(0x90) & 0xffff) != 0)
		led |= 0x100;
	if ((GetKeyState(0x91) & 0xffff) != 0)
		led |= 1;
	if (bNumLk)
		led |= 2;
	if (led != lastLed)
	{
		lastLed = led;
		SetLed(led & 0x00000007);
	}
}

int MainLoop()
{
	if (!bExitFlag)
	{
		BYTE kbc = PortIn(0x64);
		if (kbc & 1 && !(kbc & 0x20))
		{
			BYTE key = PortIn(0x60);
		
			OpenKbInt();
			switch (key)
			{
			case 0xC5:
				bNumLk = !bNumLk;		
			case 0x45:	//num lock
				break;
			default:
				WriteKey(key);
			}
			Sleep(1);
			CheckLock();
			CloseKbInt();
		}
		Sleep(1);
	}
	return 0;
}

LRESULT CALLBACK WinProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)  
{  
    switch (message)   
    {  
        case WM_CLOSE:  
            bExitFlag = true;  
            //PostQuitMessage(0);  
            break;  
    }  
    return CallWindowProc(oldWndProc, hWnd, message, wParam, lParam);  
}  

int __stdcall WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
	HMODULE hModule = LoadLibraryA("WinIo32.dll");
	InitializeWinIo = (bool(__stdcall*)())GetProcAddress(hModule, "InitializeWinIo");
	ShutdownWinIo = (bool(__stdcall*)())GetProcAddress(hModule, "ShutdownWinIo");
	GetPortVal = (bool(__stdcall*)(WORD, PDWORD, BYTE))GetProcAddress(hModule, "GetPortVal");
	SetPortVal = (bool(__stdcall*)(WORD, DWORD, BYTE))GetProcAddress(hModule, "SetPortVal");


	HWND hWnd = CreateWindowA((LPSTR)32770, "KeyMon", WS_OVERLAPPEDWINDOW, 0, 0, 100, 100, NULL, NULL, hInstance, NULL);
	//ShowWindow(hWnd, nCmdShow);
	//UpdateWindow(hWnd);
	oldWndProc = (WNDPROC)SetWindowLongA(hWnd, GWL_WNDPROC, (LONG)&WinProc);

	bool bRet = InitializeWinIo();
	CloseKbInt();
	MSG msg;
	while (!bExitFlag)
	{
		if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
		{
			TranslateMessage(&msg);  
            DispatchMessage(&msg);
		}
		else
		{
			MainLoop();
		}
	}
	
	OpenKbInt();
	bRet = ShutdownWinIo();
	return msg.wParam;
}

因为有涉及关键盘中断,所以该程序一定要通过窗口消息关闭,而万万不可结束进程!否则,嘿嘿嘿!

参考文章:

http://bbs.pediy.com/showthread.php?t=129215

http://bbs.pediy.com/showthread.php?t=89589

Leave a Reply

Your email address will not be published. Required fields are marked *