目錄
1.WPF快捷鍵實現方式
2.全局快捷鍵設置界面
3.Windows API調用
4.注冊全局快捷鍵
5.快捷鍵觸發
WPF快捷鍵實現方式
WPF快捷鍵實現主要有自定義快捷鍵命令和全局快捷鍵兩種方式。
自定義快捷鍵命令方式是通過KeyBinding為命令綁定快捷鍵,按鍵組合可使用“+”進行連接。可以通過Modifiers+Key和Gesture兩種方式定義快捷鍵組合。可以任選其一進行使用,MSDN中建議使用Gesture方式定義以免發生混淆。
<Window.InputBindings> <KeyBinding Modifiers="Control+Alt" Key="Z" Command="{StaticResource CaptureScreen}" /> <KeyBinding Gesture="Control+Alt+Q" Command="{StaticResource FullScreen}" /> </Window.InputBindings>
全局快捷鍵方式是通過調用Windows API的RegisterHotKey函數來實現全局快捷鍵注冊,調用UnregisterHotKey函數實現全局快捷鍵注銷。這種方式WinForm和WPF通用。和自定義命令方式不同的是這種方式是在系統范圍內定義熱鍵,而前者是在窗口范圍內定義。窗口范圍內定義的快捷鍵觸發條件不僅要求窗口可見,並且要求窗口獲取鍵盤焦點。這里引入的問題是,如果命令的目標不具備獲取鍵盤焦點的能力,則命令將會無效。並且,和系統范圍內定義的快捷鍵相沖突時,優先級要低。
如果是Ribbon界面菜單,推薦使用自定義快捷鍵命令的方式。通過CanExecute方法控制當前命令在目標元素上是否可用,目標元素顯示可用或禁用狀態。如果是窗口無焦點下觸發快捷鍵,則只能選用全局快捷鍵方式了。
全局快捷鍵設置界面
以下是熱鍵設置的界面。接下來對全局快捷鍵的實現分步驟說明。
這是XAML頁面的代碼,這里有界面元素的定義。
...... <ItemsControl Margin="10" ItemsSource="{Binding HotKeyList,ElementName=win}"> <ItemsControl.ItemTemplate> <DataTemplate> <Grid Margin="7"> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" /> </Grid.ColumnDefinitions> <CheckBox Grid.Column="0" Content="{Binding Name}" IsChecked="{Binding IsUsable}" Style="{StaticResource ckbStyle1}" /> <CheckBox Grid.Column="1" Content="Ctrl" IsChecked="{Binding IsSelectCtrl}" IsEnabled="{Binding IsUsable}" Style="{StaticResource ckbStyle2}" /> <CheckBox Grid.Column="2" Content="Shift" IsChecked="{Binding IsSelectShift}" IsEnabled="{Binding IsUsable}" Style="{StaticResource ckbStyle2}" /> <CheckBox Grid.Column="3" Content="Alt" IsChecked="{Binding IsSelectAlt}" IsEnabled="{Binding IsUsable}" Style="{StaticResource ckbStyle2}" /> <ComboBox Grid.Column="4" ItemsSource="{Binding Keys}" SelectedItem="{Binding SelectKey}" IsEnabled="{Binding IsUsable}" Style="{StaticResource cmbStyle1}" /> </Grid> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl> ......
首先,新建一個自定義按鍵枚舉。WinForm中可以使用Keys枚舉轉換,WPF中Key枚舉是不正確的,應該使用system.Windows.Froms.Keys枚舉,或者自定義正確的枚舉或int常量。因為這里定義的枚舉會作為快捷鍵設置的可選項,可以只定義需要擊鍵。
/// <summary>
/// 自定義按鍵枚舉 /// </summary> public enum EKey { Space = 32, Left = 37, Up = 38, Right = 39, Down = 40, A = 65, B = 66, C = 67, D = 68, ...... }
新建快捷鍵模型類。在模型中,可以直接將EKey枚舉值集合綁定到界面的ComboBox上。
/// <summary> /// 快捷鍵模型 /// </summary> public class HotKeyModel { /// <summary> /// 設置項名稱 /// </summary> public string Name { get; set; } /// <summary> /// 設置項快捷鍵是否可用 /// </summary> public bool IsUsable { get; set; } /// <summary> /// 是否勾選Ctrl按鍵 /// </summary> public bool IsSelectCtrl { get; set; } /// <summary> /// 是否勾選Shift按鍵 /// </summary> public bool IsSelectShift { get; set; } /// <summary> /// 是否勾選Alt按鍵 /// </summary> public bool IsSelectAlt { get; set; } /// <summary> /// 選中的按鍵 /// </summary> public EKey SelectKey { get; set; } /// <summary> /// 快捷鍵按鍵集合 /// </summary> public static Array Keys { get { return Enum.GetValues(typeof(EKey)); } } }
Windows API調用
WM_HOTKEY為熱鍵消息,在用戶鍵入被RegisterHotKey函數注冊的熱鍵時發送。該消息將位於隊列的最前端,並且與注冊了這個熱鍵的進程關聯。
RegisterHotKey函數定義一個系統范圍內的熱鍵。 參數hWnd是接收熱鍵產生WM_HOTKEY消息的窗口句柄。若該參數null,傳遞給調用線程的WM_HOTKEY消息必須在消息循環中進行處理。 參數id為定義熱鍵的標識符。調用線程中的其他熱鍵,不能使用同樣的標識符。為了避免與其他動態鏈接庫定義的熱鍵沖突,一個DLL必須使用GlobalAddAtom函數獲取熱鍵的標識符。 參數fsModifiers是定義為了產生WM_HOTKEY消息而必須與由nVirKey參數定義的鍵一起按下的鍵,即Ctrl、Shift和Alt的按鍵組合。 參數vk是定義熱鍵的虛擬鍵碼,也就是選中的EKey中的按鍵。 PS:當某鍵被接下時,系統在所有的熱鍵中尋找匹配者。一旦找到一個匹配的熱鍵,系統將把WM_HOTKEY消息傳遞給登記了該熱鍵的線程的消息隊列。該消息被傳送到隊列頭部,因此它將在下一輪消息循環中被移除。該函數不能將熱鍵同其他線程創建的窗口關聯起來。若為一熱鍵定義的擊鍵已被其他熱鍵所定義,則RegisterHotKey函數調用失敗。若hWnd參數標識的窗口已用與id參數定義的相同的標識符登記了一個熱鍵,則參數fsModifiers和vk的新值將替代這些參數先前定義的值。
UnregisterHotKey函數釋放調用線程先前登記的熱鍵。 參數hWnd與被釋放的熱鍵相關的窗口句柄。若熱鍵不與窗口相關,則該參數為null。
GlobalAddAtom函數是向全局原子表添加一個字符串,並返回這個字符串的唯一標識符(原子ATOM)。Win32系統中,為了實現信息共享,系統維護了一張全局原子表,用於保存字符串與之對應的標識符的組合。 參數lpString為一個字符串,這個字符串的長度最大為255字節。返回值為一個short類型的原子。若函數調用失敗,則返回值為0。 PS:如果字符串中已經存在於全局原子表中,則返回現有的字符串的原子,並且原子的引用計數加1。與原子相關的字符串不會從內存中刪除,直到它的引用計數為零。全局原子不會在應用程序終止時自動刪除。每次調用GlobalAddAtom函數,必須相應的調用GlobalDeleteAtom函數刪除原子。
GlobalFindAtom函數是在表中搜索全局原子。 參數lpString為一個字符串。找到返回原子,沒找到返回0。
GlobalDeleteAtom函數是在表中刪除全局原子。 參數nAtom為全局原子。
/// <summary> /// 熱鍵管理器 /// </summary> public class HotKeyManager { /// <summary> /// 熱鍵消息 /// </summary> public const int WM_HOTKEY = 0x312; /// <summary> /// 注冊熱鍵 /// </summary> [DllImport("user32.dll", SetLastError = true)] public static extern bool RegisterHotKey(IntPtr hWnd, int id, ModifierKeys fsModifuers, int vk); /// <summary> /// 注銷熱鍵 /// </summary> [DllImport("user32.dll", SetLastError = true)] public static extern bool UnregisterHotKey(IntPtr hWnd, int id); /// <summary> /// 向原子表中添加全局原子 /// </summary> [DllImport("kernel32.dll", SetLastError = true)] public static extern short GlobalAddAtom(string lpString); /// <summary> /// 在表中搜索全局原子 /// </summary> [DllImport("kernel32.dll", SetLastError = true)] public static extern short GlobalFindAtom(string lpString); /// <summary> /// 在表中刪除全局原子 /// </summary> [DllImport("kernel32.dll", SetLastError = true)] public static extern short GlobalDeleteAtom(string nAtom); }
注冊全局快捷鍵
首先,重載OnSourceInitialized函數,這個事件發生在WPF窗體的資源初始化完成,並且可以通過WindowInteropHelper獲得該窗體的句柄用來與Win32交互后。調用HwndSource.FromHwnd方法獲取當前窗口句柄,再調用hWndSource.AddHook方法添加接收所有窗口消息的事件處理程序。重載OnContentRendered函數,在控件初始化完成后初始化快捷鍵。
protected override void OnSourceInitialized(EventArgs e) { base.OnSourceInitialized(e); // 獲取窗體句柄 m_Hwnd = new WindowInteropHelper(this).Handle; HwndSource hWndSource = HwndSource.FromHwnd(m_Hwnd); // 添加處理程序 if (hWndSource != null) hWndSource.AddHook(WndProc); } /// <summary> /// 所有控件初始化完成后調用 /// </summary> /// <param name="e"></param> protected override void OnContentRendered(EventArgs e) { base.OnContentRendered(e); // 注冊熱鍵 InitHotKey(); }
注冊全局快捷鍵到系統中。
/// <summary> /// 初始化注冊快捷鍵 /// </summary> /// <param name="hotKeyModelList">待注冊熱鍵的項</param> /// <returns>true:保存快捷鍵的值;false:彈出設置窗體</returns> private bool InitHotKey(ObservableCollection<SysParameterSettingHotKeyModel> hotKeyModelList = null) { var list = hotKeyModelList ?? SysParameterSettingsManager.Instance.LoadDefaultHotKey(); // 注冊全局快捷鍵 string failList = HotKeyHelper.RegisterGlobalHotKey(list, m_Hwnd, out m_HotKeySettings); if (string.IsNullOrEmpty(failList)) return true; MessageBoxResult mbResult = MessageBoxWindow.Show("提示", string.Format("無法注冊下列快捷鍵\n\r{0}是否要改變這些快捷鍵?", failList), MessageBoxButton.YesNo); var win = SysParameterSettingsWindow.CreateInstance(); if (mbResult == MessageBoxResult.Yes) { win.hotKeySet.IsSelected = true; if (!win.IsVisible) { win.ShowDialog(); } else { win.Activate(); } return false; } return true; }
新建一個熱鍵注冊幫助類HotKeyHelper。這里有兩個需要注意的地方。第一個是全局原子不會在應用程序終止時自動刪除,每次調用GlobalAddAtom函數,必須相應的調用GlobalDeleteAtom函數刪除原子。第二是若為一熱鍵定義的擊鍵已被其他熱鍵所定義,則RegisterHotKey函數調用失敗,所以每次調用RegisterHotKey函數注冊熱鍵時,必須先調用UnregisterHotKey函數注銷舊的熱鍵。
/// <summary> /// 熱鍵注冊幫助 /// </summary> public class HotKeyHelper { /// <summary> /// 記錄快捷鍵注冊項的唯一標識符 /// </summary> private static Dictionary<EHotKeySetting, int> m_HotKeySettingsDic = new Dictionary<EHotKeySetting, int>(); /// <summary> /// 注冊系統快捷鍵 /// </summary> /// <param name="hotKeyModelList">待注冊快捷鍵項</param> /// <param name="hwnd">窗口句柄</param> /// <param name="hotKeySettingsDic">快捷鍵注冊項的唯一標識符字典</param> /// <returns>返回注冊失敗項的拼接字符串</returns> public static string RegisterSystemHotKey(IEnumerable<HotKeyModel> hotKeyModelList, IntPtr hwnd, out Dictionary<EHotKeySetting, int> hotKeySettingsDic) { string failList = string.Empty; foreach (var item in hotKeyModelList) { if (!RegisterHotKey(item, hwnd)) { string str = string.Empty; if (item.IsSelectCtrl && !item.IsSelectShift && !item.IsSelectAlt) { str = ModifierKeys.Control.ToString(); } else if (!item.IsSelectCtrl && item.IsSelectShift && !item.IsSelectAlt) { str = ModifierKeys.Shift.ToString(); } else if (!item.IsSelectCtrl && !item.IsSelectShift && item.IsSelectAlt) { str = ModifierKeys.Alt.ToString(); } else if (item.IsSelectCtrl && item.IsSelectShift && !item.IsSelectAlt) { str = string.Format("{0}+{1}", ModifierKeys.Control.ToString(), ModifierKeys.Shift); } else if (item.IsSelectCtrl && !item.IsSelectShift && item.IsSelectAlt) { str = string.Format("{0}+{1}", ModifierKeys.Control.ToString(), ModifierKeys.Alt); } else if (!item.IsSelectCtrl && item.IsSelectShift && item.IsSelectAlt) { str = string.Format("{0}+{1}", ModifierKeys.Shift.ToString(), ModifierKeys.Alt); } else if (item.IsSelectCtrl && item.IsSelectShift && item.IsSelectAlt) { str = string.Format("{0}+{1}+{2}", ModifierKeys.Control.ToString(), ModifierKeys.Shift.ToString(), ModifierKeys.Alt); } str += string.Format("+{0}", item.SelectKey.ToString()); str = string.Format("{0} ({1})\n\r", item.Name, str); failList += str; } } hotKeySettingsDic = m_HotKeySettingsDic; return failList; } /// <summary> /// 注冊熱鍵 /// </summary> /// <param name="hotKeyModel">熱鍵待注冊項</param> /// <param name="hWnd">窗口句柄</param> /// <returns>成功返回true,失敗返回false</returns> private static bool RegisterHotKey(HotKeyModel hotKeyModel, IntPtr hWnd) { var fsModifierKey = new ModifierKeys(); var hotKeySetting = (EHotKeySetting)Enum.Parse(typeof(EHotKeySetting), hotKeyModel.Name); if (!m_HotKeySettingsDic.ContainsKey(hotKeySetting)) { // 全局原子不會在應用程序終止時自動刪除。每次調用GlobalAddAtom函數,必須相應的調用GlobalDeleteAtom函數刪除原子。 if (HotKeyManager.GlobalFindAtom(hotKeySetting.ToString()) != 0) { HotKeyManager.GlobalDeleteAtom(HotKeyManager.GlobalFindAtom(hotKeySetting.ToString())); } // 獲取唯一標識符 m_HotKeySettingsDic[hotKeySetting] = HotKeyManager.GlobalAddAtom(hotKeySetting.ToString()); } else { // 注銷舊的熱鍵 HotKeyManager.UnregisterHotKey(hWnd, m_HotKeySettingsDic[hotKeySetting]); } if (!hotKeyModel.IsUsable) return true; // 注冊熱鍵 if (hotKeyModel.IsSelectCtrl && !hotKeyModel.IsSelectShift && !hotKeyModel.IsSelectAlt) { fsModifierKey = ModifierKeys.Control; } else if (!hotKeyModel.IsSelectCtrl && hotKeyModel.IsSelectShift && !hotKeyModel.IsSelectAlt) { fsModifierKey = ModifierKeys.Shift; } else if (!hotKeyModel.IsSelectCtrl && !hotKeyModel.IsSelectShift && hotKeyModel.IsSelectAlt) { fsModifierKey = ModifierKeys.Alt; } else if (hotKeyModel.IsSelectCtrl && hotKeyModel.IsSelectShift && !hotKeyModel.IsSelectAlt) { fsModifierKey = ModifierKeys.Control | ModifierKeys.Shift; } else if (hotKeyModel.IsSelectCtrl && !hotKeyModel.IsSelectShift && hotKeyModel.IsSelectAlt) { fsModifierKey = ModifierKeys.Control | ModifierKeys.Alt; } else if (!hotKeyModel.IsSelectCtrl && hotKeyModel.IsSelectShift && hotKeyModel.IsSelectAlt) { fsModifierKey = ModifierKeys.Shift | ModifierKeys.Alt; } else if (hotKeyModel.IsSelectCtrl && hotKeyModel.IsSelectShift && hotKeyModel.IsSelectAlt) { fsModifierKey = ModifierKeys.Control | ModifierKeys.Shift | ModifierKeys.Alt; } return HotKeyManager.RegisterHotKey(hWnd, m_HotKeySettingsDic[hotKeySetting], fsModifierKey, (int)hotKeyModel.SelectKey); } }
通過判斷msg 是否為WM_HOTKEY來判斷當前快捷鍵是否觸發。通過附加參數wideParam獲得當前快捷鍵觸發的項,進而進行相應處理。另外,當前消息處理完成后需要將handled置為true。
/// <summary> /// 窗體回調函數,接收所有窗體消息的事件處理函數 /// </summary> /// <param name="hWnd">窗口句柄</param> /// <param name="msg">消息</param> /// <param name="wideParam">附加參數1</param> /// <param name="longParam">附加參數2</param> /// <param name="handled">是否處理</param> /// <returns>返回句柄</returns> private IntPtr WndProc(IntPtr hWnd, int msg, IntPtr wideParam, IntPtr longParam, ref bool handled) { var hotkeySetting = new EHotKeySetting(); switch (msg) { case HotKeyManager.WM_HOTKEY: int sid = wideParam.ToInt32(); if (sid == m_HotKeySettings[EHotKeySetting.全屏]) { hotkeySetting = EHotKeySetting.全屏; //TODO 執行全屏操作 } else if (sid == m_HotKeySettings[EHotKeySetting.截圖]) { hotkeySetting = EHotKeySetting.截圖; //TODO 執行截圖操作 } else if (sid == m_HotKeySettings[EHotKeySetting.播放]) { hotkeySetting = EHotKeySetting.播放; //TODO ...... } else if (sid == m_HotKeySettings[EHotKeySetting.前進]) { hotkeySetting = EHotKeySetting.前進; } else if (sid == m_HotKeySettings[EHotKeySetting.后退]) { hotkeySetting = EHotKeySetting.后退; } else if (sid == m_HotKeySettings[EHotKeySetting.保存]) { hotkeySetting = EHotKeySetting.保存; } else if (sid == m_HotKeySettings[EHotKeySetting.打開]) { hotkeySetting = EHotKeySetting.打開; } else if (sid == m_HotKeySettings[EHotKeySetting.新建]) { hotkeySetting = EHotKeySetting.新建; } else if (sid == m_HotKeySettings[EHotKeySetting.刪除]) { hotkeySetting = EHotKeySetting.刪除; }
MessageBox.Show(string.Format("觸發【{0}】快捷鍵", hotkeySetting.ToString())); handled = true; break; } return IntPtr.Zero; }