在智能客戶端應用程序中使用備用輸入設備
本文內容
布魯斯·托馬斯
2005 年 5 月
適用于:
原始輸入 API
人機接口設備 (HID)
總結:觸摸屏和遙控器等輸入設備稱為人機界面設備 (HID) ,原始輸入 API 提供了一種可靠的方法來接受任何 HID 組合的輸入。 (11 個打印頁面)
下載本文隨附的示例 ,ode.msi。
目錄
簡介
傳統輸入模式
原始輸入模式為何不同?
使用原始輸入 API 入門
為原始輸入模式注冊設備
通過遠程控制設備驅動Smart-應用程序
結論
簡介
面向使用者的用戶界面(如 XP Media )越來越受歡迎,該版本將傳統鍵盤和鼠標組合作為主要輸入設備,演示了應用程序如何利用替代輸入設備來增強整體用戶體驗。 只需添加新的輸入法,應用程序的范圍就可以擴展到全新的客戶群。 作為開發人員,我們知道為應用程序選擇正確的輸入方案是多么重要。 鼠標單擊數、鍵盤快捷方式和控制選項卡順序可能會對工作效率和客戶滿意度產生巨大影響。 對于大多數應用程序,傳統鍵盤和鼠標用戶輸入設備是可接受的,如果不需要應用程序的最低用戶交互級別。 但是,對于越來越多的應用程序,這還不夠或不再表示收集用戶輸入的最合適方式。
除了傳統設備(如鍵盤和鼠標)外,輸入設備(如游戲板、游戲桿、全局定位系統 (GPS) 、麥克風、觸摸屏、遙控器和其他許多設備)都可以在應用程序中以可靠且靈活的方式利用。 這些設備統稱為 人機接口設備 (HID) ,原始輸入 API 提供了一種可靠的方法來接受任何 HID 組合的輸入。
傳統輸入模式
在了解使用原始輸入 API 的具體信息之前只能作為輸入設備的是,讓我們簡要回顧鍵盤和鼠標的原始輸入模型。 傳統上,應用程序以發布到應用程序消息隊列的消息的形式從設備接收其輸入。 例如,鍵盤生成由系統解釋的設備特定的掃描代碼數據,并將此數據以消息的形式轉發給應用程序。 這樣,應用程序就可以在硬件的更高級別抽象下運行,但也可能會阻止一些重要的設備信息到達應用程序。 雖然原始輸入模型適用于標準鍵盤和基于鼠標的應用程序,但如果需要在同一應用程序中支持其他基于輸入的設備,其缺點就變得非常明顯。
下面是使用原始輸入模式的主要缺點列表:
原始輸入模型專門用于解決這些問題,并允許從所有 HID(包括鍵盤和鼠標)輕松訪問原始輸入。
原始輸入模式為何不同?
相比之下,原始輸入模型要求應用程序注冊要從中接收輸入的 HID。 默認情況下,除非應用程序專門注冊原始輸入數據,并且所有原始輸入數據都通過消息接收,否則不會收到任何原始輸入數據。 顧名思義,從已注冊設備接收的數據特定于設備、低級別數據,并且由應用程序正確解釋此數據。 讓我們看看可在應用程序中使用的原始輸入函數列表。 有關更多詳細信息,請參閱 原始輸入 API 參考。
函數描述
此函數調用默認的原始輸入過程,為應用程序未處理的任何原始輸入消息提供默認處理。 此函數可確保處理每個消息。
此函數對原始輸入數據執行緩沖讀取。
此函數從指定設備獲取原始輸入。
o
此函數獲取有關原始輸入設備的信息。
t
此函數枚舉附加到系統的原始輸入設備。
此函數獲取有關當前應用程序的原始輸入設備的信息。
ces
此函數注冊提供原始輸入數據的設備。
使用原始輸入 API 入門
若要了解在應用程序中使用原始輸入 API 的感覺,讓我們首先獲取系統上當前可用的現有輸入設備列表。 t 函數用于請求設備列表,我們可以使用 o 函數收集每個設備的名稱。 每個設備被歸類為三種類型之一:、和。 表示不是鼠標或鍵盤的每個設備。 請注意,某些設備可以表示多種類型,因為稍后我們將使用遠程控制設備。
private const int RIDI_DEVICENAME = 0x20000007;
[StructLayout(LayoutKind.Sequential)]
internal struct RAWINPUTDEVICELIST
{
public IntPtr hDevice;
[MarshalAs(UnmanagedType.U4)]
public int dwType;
}
[DllImport("User32.dll")]
extern static uint GetRawInputDeviceList(IntPtr pRawInputDeviceList,
ref uint uiNumDevices, uint cbSize);
[DllImport("User32.dll")]
extern static uint GetRawInputDeviceInfo(IntPtr hDevice, uint
uiCommand, IntPtr pData, ref uint pcbSize);
我們從原始輸入 API 的典型結構和方法聲明開始。 但是,由于許多原始輸入 API 結構和方法都使用變量數組,因此獲取結果并不簡單。
uint deviceCount = 0;
int dwSize = (Marshal.SizeOf(typeof(RAWINPUTDEVICELIST)));
if (GetRawInputDeviceList(IntPtr.Zero, ref deviceCount, (uint) dwSize) != 0)
throw new ApplicationException("Error!");
IntPtr pRawInputDeviceList = Marshal.AllocHGlobal((int) (dwSize * deviceCount));
GetRawInputDeviceList(pRawInputDeviceList, ref deviceCount, (uint) dwSize);
for (int i = 0; i < deviceCount; i++)
{
string deviceName = string.Empty;
uint pcbSize = 0;
RAWINPUTDEVICELIST rid = (RAWINPUTDEVICELIST) Marshal.PtrToStructure(
new IntPtr((pRawInputDeviceList.ToInt32() + (dwSize * i))),
typeof(RAWINPUTDEVICELIST)
);
GetRawInputDeviceInfo(rid.hDevice, RIDI_DEVICENAME, IntPtr.Zero, ref pcbSize);
if (pcbSize > 0)

{
IntPtr pData = Marshal.AllocHGlobal((int) pcbSize);
GetRawInputDeviceInfo(rid.hDevice, RIDI_DEVICENAME, pData, ref pcbSize);
deviceName = (string) Marshal.PtrToStringAnsi(pData);
Marshal.FreeHGlobal(pData);
}
Console.WriteLine("hDevice: {0}, dwType: {1}, Name: {2}",
rid.hDevice, rid.dwType, deviceName);
}
Marshal.FreeHGlobal(pRawInputDeviceList);Device, uint uiCommand, IntPtr
pData, ref uint pcbSize);
在上面的示例代碼中,我們首先調用 t 函數來獲取當前系統上的設備計數。 接下來,我們分配一些非托管內存,以便根據設備數保存列表,對傳入指向內存的指針的 t 進行第二次調用。 然后枚舉內存,將每個內存復制到 結構。 最后,我們使用 結構中的設備句柄重復此過程,以調用 o 函數來獲取設備的名稱。
為原始輸入模式注冊設備
現在,我們了解原始輸入 API 的工作原理,讓我們讓應用程序注冊,以便從其中一些設備接收輸入。 如前所述,默認情況下,應用程序在注冊設備之前不會從任何設備接收任何原始輸入。 若要注冊一個或多個設備,我們將 結構的數組傳遞給 ce 函數。 結構包含 和 屬性,用于確定要注冊的類中的設備和設備類。 統一地說,“使用情況”頁和“使用情況”指定設備的頂級集合 (TLC) 。 我們可以指定 屬性來確定 TLC 的解釋方式。
[DllImport("User32.dll")]
extern static bool RegisterRawInputDevices(RAWINPUTDEVICE[]
pRawInputDevice, uint uiNumDevices, uint cbSize);
[StructLayout(LayoutKind.Sequential)]
internal struct RAWINPUTDEVICE
{
[MarshalAs(UnmanagedType.U2)]
public ushort usUsagePage;
[MarshalAs(UnmanagedType.U2)]
public ushort usUsage;
[MarshalAs(UnmanagedType.U4)]
public int dwFlags;
public IntPtr hwndTarget;
}
private void Form1_Load(object sender, System.EventArgs e)
{
RAWINPUTDEVICE[] rid = new RAWINPUTDEVICE[5];
rid[0].usUsagePage = 0xFFBC; // adds HID remote control
rid[0].usUsage = 0x88;
rid[0].dwFlags = RIDEV_INPUTSINK;
rid[0].hwndTarget = this.Handle;
rid[1].usUsagePage = 0x0C; // adds HID remote control

rid[1].usUsage = 0x01;
rid[0].dwFlags = RIDEV_INPUTSINK;
rid[0].hwndTarget = this.Handle;
rid[2].usUsagePage = 0x0C; // adds HID remote control
rid[2].usUsage = 0x80;
rid[0].dwFlags = RIDEV_INPUTSINK;
rid[0].hwndTarget = this.Handle;
rid[3].usUsagePage = 0x01; // adds HID mouse with no legacy messages
rid[3].usUsage = 0x02;
rid[3].dwFlags = RIDEV_NOLEGACY;
rid[4].usUsagePage = 0x01; // adds HID keyboard with no legacy message
rid[4].usUsage = 0x06;
rid[4].dwFlags = RIDEV_NOLEGACY;
if (!RegisterRawInputDevices(rid, (uint) rid.Length, (uint) Marshal.SizeOf(rid[0])))
throw new ApplicationException("Failed to register raw input devices.");
}
在此方案中,應用程序注冊以接收來自三個不同的設備的原始輸入:遠程控制、鼠標和鍵盤。 此外,此示例代碼演示應用程序可以練習的控制量,以確定要接收的輸入類型。 如果不使用原始輸入模式,則無法執行此操作。 數組的元素大小為 5,因為遠程控制設備需要為一組不同的按鈕注冊三個 TLC。 為遠程控件的 TLC 設置標志,以便即使應用程序不在前臺,也可以從該設備接收輸入。 對于鼠標和鍵盤,我們可以消除舊消息,例如和,并且只能通過原始輸入處理這些事件。 成功調用 ces 函數后,應用程序將開始通過消息接收原始輸入數據。 此處的一個重要概念是只能作為輸入設備的是,即使設備當前未連接到系統,也可以注冊設備。 連接設備后,應用程序將開始從該設備接收輸入。
通過遠程控制設備驅動Smart-應用程序
我們開始討論面向消費者的應用程序日益普及,因此讓我們看看如何從其中一臺設備收集數據來驅動應用程序。 在本演示中,我們將使用通過 USB 端口連接的 XP 媒體中心遠程控制設備。 從 XP Pack 1 開始,添加了對紅外遠程控制設備輸入的支持。
此設備有趣的是,它結合了傳統輸入模型和原始輸入模型。 某些按鈕(如數字 1-9、ENTER 和 CHAN/PAGE UP)生成傳統消息,例如和。 但是,其他按鈕(如 GUIDE、 TV 和 DVD MENU)僅適用于原始輸入模型。 我們希望應用程序能夠接收來自所有遠程按鈕的輸入。 首先創建 類來表示應用程序中遠程控制設備的功能。
// some form in our app...
RemoteControlDevice _remote
protected override void WndProc(ref Message message)
{
_remote.ProcessMessage(message);
base.WndProc(ref message);
}
public sealed class RemoteControlDevice
{
public delegate void RemoteControlDeviceEventHandler(object
sender, RemoteControlEventArgs e);
public event RemoteControlDeviceEventHandler ButtonPressed;

public RemoteControlDevice()
{
// register device here...
}
public void ProcessMessage(Message message)
{
int param;
switch (message.Msg)
{
case WM_KEYDOWN:
param = message.WParam.ToInt32();
ProcessKeyDown(param);
break;
case WM_APPCOMMAND:
param = message.LParam.ToInt32();
ProcessAppCommand(param);
break;
case WM_INPUT:
ProcessInputCommand(ref message);
break;
}
}
...
}
在上面的示例代碼中,首先在主窗體中重寫 ,以處理消息。 這些消息將傳遞到 類的實例,用于篩選和處理。 在這種情況下,我們對按下按鈕時遠程生成的三條消息感興趣。 這可以減少到使用以前演示的技術注冊沒有舊版支持的設備。
private void ProcessInputCommand(ref Message message)
{
RemoteControlButton rcb = RemoteControlButton.Unknown;
uint dwSize = 0;
GetRawInputData(message.LParam, RID_INPUT, IntPtr.Zero, ref
dwSize, (uint) Marshal.SizeOf(typeof(RAWINPUTHEADER)));
IntPtr buffer = Marshal.AllocHGlobal((int) dwSize);
try
{
if (GetRawInputData(message.LParam, RID_INPUT, buffer, ref
dwSize, (uint) Marshal.SizeOf(typeof(RAWINPUTHEADER))) != dwSize)

return;
RAWINPUT raw = (RAWINPUT) Marshal.PtrToStructure(buffer, typeof(RAWINPUT));
byte[] bRawData = new byte[raw.hid.dwSizHid];
int pRawData = buffer.ToInt32() + Marshal.SizeOf(typeof(RAWINPUT)) + 1;
Marshal.Copy(new IntPtr(pRawData), bRawData, 0, raw.hid.dwSizHid - 1);
int rawData = bRawData[0] | bRawData[1] << 8; // do a
little bit shifting to assign the button value
switch (rawData)
{
case RAWINPUT_DETAILS:
rcb = RemoteControlButton.Details;
break;
case RAWINPUT_GUIDE:
rcb = RemoteControlButton.Guide;
break;
case RAWINPUT_DVDMENU:
rcb = RemoteControlButton.DVDMenu;
break;
...
}
if (rcb != RemoteControlButton.Unknown &&
this.ButtonPressed != null)
this.ButtonPressed(this, new
RemoteControlEventArgs(rcb, GetDevice(message.LParam.ToInt32())));
}
finally
{
Marshal.FreeHGlobal(buffer);
}
}
可以看到,用于使用 函數檢索實際原始輸入數據的代碼遵循我們用于收集有關 HID 的其他信息所用的相同模式。 此時,我們確實要由應用程序決定如何使用此信息。 在這種情況下,原始數據包含在遠程上按下的按鈕的值,但對于 GPS 輸入設備,此數據可能包含當前位置的坐標。 但是,必須為每個 HID 解釋此信息,因此需要有關原始數據的詳細信息才能正確執行此解釋。
結論
作為開發人員,我們通過編寫支持更廣泛的輸入設備的應用程序來獲得很多好處。 除了引入完成任務的新方法外,備用設備支持還有助于我們查找應用程序的新受眾,減少培訓障礙,并允許最終用戶與更簡單、更熟悉的設備進行交互。 利用原始輸入 API,以可靠的方式打開應用程序以支持新設備并不如你想象的那樣困難。
關于作者
Bruce 是簡化 (首席軟件架構師兼首席軟件架構師 ) ,該公司致力于利用其在 .NET 和的專業知識來幫助公司構建更好的軟件解決方案。 Bruce 專門從事企業級應用程序的設計和開發。 他可以到達。