为了找到 QTBUG18896 问题的答案,只好先看看Windows下面的键盘消息处理,看到最后:发现这个问题和Windows似乎没有必然的联系 ^_^ (见 QToolBar焦点问题 (QTBUG18896) )
Windows键盘消息
对产生可显示字符的按键组合,Windows不仅给程序发送按键消息,而且还发送字符消息。有些键不产生字符,这些键包括shift键、功能键、光标移动键和特殊字符键如Insert和Delete。对于这些键,Windows只产生按键消息
按键消息 | WM_KEYDOWN | 一般是不带Alt的按键(Windows本身不处理这些消息) | wParam 中保存虚拟键码 |
WM_KEYUP | |||
WM_SYSKEYDOWN | 系统按键,一般是带 Alt(通常交由DefWindowProc处理) | ||
WM_SYSKEYUP | |||
字符消息 | WM_CHAR | 从WM_KEYDOWN得到 | wParam中是 ANSI或Unicode代码 |
WM_DEADCHAR | |||
WM_SYSCHAR | 从WM_SYSKEYDOWN得到 | ||
WM_DEADSYSCHAR | |||
输入法(字符)消息 | WM_IME_CHAR | 如果不处理,DefWindowProc根据窗口注册的是unicode或ansi生成1个或2个WM_CHAR | |
WM_IME_KEYDOWN | 如果不处理,DefWindowProc生成相应的WM_KEYDOWN或WM_KEYUP | ||
WM_IME_KEYUP | |||
字符消息 | WM_UNICHAR | 不同于WM_CHAR(utf16),其内部是utf32(几乎不用该消息?) |
- 组合使用Ctrl键与字母键会产生从0x01(Ctrl-A)到0x1A(Ctrl-Z)的ASCII控制代码(字符消息)
- 下面的按键也会产生字符消息
按键 | ASCII码 | C转义表示 | |
Backspace | 0x08 | Ctrl-H | \b |
Tab | 0x09 | Ctrl-I | \t |
Ctrl-Enter | 0x0A | Ctrl-J | \n |
Enter | 0x0D | Ctrl-M | \r |
Qt 的处理
(注:以下代码来自Qt4.7.3)
直接看一下窗口的回调函数中对按键消息的处理
- 目标widget:
- Grabber 键盘事件的 widget
- 获得焦点的 widget
switch (message) {
case WM_KEYDOWN: // keyboard event
case WM_SYSKEYDOWN:
qt_keymapper_private()->updateKeyMap(msg);
// fall-through intended
case WM_KEYUP:
case WM_SYSKEYUP:
case WM_IME_CHAR:
case WM_IME_KEYDOWN:
case WM_CHAR: {
MSG msg1;
QWidget *g = QWidget::keyboardGrabber();
if (g)
widget = (QETWidget*)g;
else if (QApplication::activePopupWidget())
widget = (QETWidget*)QApplication::activePopupWidget()->focusWidget()
? (QETWidget*)QApplication::activePopupWidget()->focusWidget()
: (QETWidget*)QApplication::activePopupWidget();
else if (QApplication::focusWidget())
widget = (QETWidget*)QApplication::focusWidget();
else if (!widget || widget->internalWinId() == GetFocus()) // We faked the message to go to exactly that widget.
widget = (QETWidget*)widget->window();
if (widget->isEnabled())
result = sm_blockUserInput
? true
: qt_keymapper_private()->translateKeyEvent(widget, msg, g != 0);
break;
}
case WM_SYSCHAR:
result = true; // consume event
break;
translateKeyEvent
这中间最重要的是 translateKeyEvent 这个函数,它负责生成Qt的键盘事件
继续之前,先看一下QKeyEvent的几个成员函数:
Qt | int key () const | |
Qt::KeyboardModifiers modifiers () const | ||
QString text () const | ||
系统 | quint32 nativeModifiers () const | |
quint32 nativeScanCode () const | ||
quint32 nativeVirtualKey () const |
对照看一下:先获取系统提供的scancode、virtualkey和modifiers
bool QKeyMapperPrivate::translateKeyEvent(QWidget *widget, const MSG &msg, bool grab)
{
quint32 scancode = (msg.lParam >> 16) & 0xfff;
quint32 vk_key = MapVirtualKey(scancode, 1);
bool isNumpad = (msg.wParam >= VK_NUMPAD0 && msg.wParam <= VK_NUMPAD9);
quint32 nModifiers = 0;
// Map native modifiers to some bit representation
nModifiers |= (GetKeyState(VK_LSHIFT ) & 0x80 ? ShiftLeft : 0);
...
nModifiers |= (GetKeyState(VK_SCROLL ) & 0x01 ? ScrollLock : 0);
if (msg.lParam & ExtendedKey)
nModifiers |= msg.lParam & ExtendedKey;
然后进行转换(后面会看到处理WM_KEYDOWN时会处理掉WM_CHAR,这儿是漏网之鱼(WM_CHAR)):
// Get the modifier states (may be altered later, depending on key code)
int state = 0;
state |= (nModifiers & ShiftAny ? Qt::ShiftModifier : 0);
state |= (nModifiers & ControlAny ? Qt::ControlModifier : 0);
state |= (nModifiers & AltAny ? Qt::AltModifier : 0);
state |= (nModifiers & MetaAny ? Qt::MetaModifier : 0);
// A multi-character key not found by our look-ahead
if (msgType == WM_CHAR) {
QString s;
QChar ch = QChar((ushort)msg.wParam);
if (!ch.isNull())
s += ch;
k0 = q->sendKeyEvent(widget, grab, QEvent::KeyPress, 0, Qt::KeyboardModifier(state), s, false, 0, scancode, vk_key, nModifiers);
k1 = q->sendKeyEvent(widget, grab, QEvent::KeyRelease, 0, Qt::KeyboardModifier(state), s, false, 0, scancode, vk_key, nModifiers);
}
keydown的处理
- 查看上次的keydown记录,比对状态是否一致
// KEYDOWN ---------------------------------------------------------------------------------
if (msgType == WM_KEYDOWN || msgType == WM_IME_KEYDOWN || msgType == WM_SYSKEYDOWN) {
// Get the last record of this key press, so we can validate the current state
// The record is not removed from the list
KeyRecord *rec = key_recorder.findKey(msg.wParam, false);
// If rec's state doesn't match the current state, something has changed behind our back
// (Consumed by modal widget is one possibility) So, remove the record from the list
// This will stop the auto-repeat of the key, should a modifier change, for example
if (rec && rec->state != state) {
key_recorder.findKey(msg.wParam, true);
rec = 0;
}
- 系统消息队列中是否有对象的WM_CHAR,有则取出
// Find unicode character from Windows Message Queue
MSG wm_char;
UINT charType = (msgType == WM_KEYDOWN
? WM_CHAR
: msgType == WM_IME_KEYDOWN ? WM_IME_CHAR : WM_SYSCHAR);
QChar uch;
if (PeekMessage(&wm_char, 0, charType, charType, PM_REMOVE)) {
// Found a ?_CHAR
uch = QChar((ushort)wm_char.wParam);
if (msgType == WM_SYSKEYDOWN && uch.isLetter() && (msg.lParam & KF_ALTDOWN))
uch = uch.toLower(); // (See doc of WM_SYSCHAR) Alt-letter
if (!code && !uch.row())
code = asciiToKeycode(uch.cell(), state);
}
- 如果系统消息队列中没有,则尝试从keydown参数中生成字符
// If no ?_CHAR was found in the queue; deduct character from the ?_KEYDOWN parameters
if (uch.isNull()) {
if (msg.wParam == VK_DELETE) {
uch = QChar(QLatin1Char(0x7f)); // Windows doesn't know this one.
} else {
if (msgType != WM_SYSKEYDOWN || !code) {
UINT map = MapVirtualKey(msg.wParam, 2);
// If the high bit of the return value is set, it's a deadkey
if (!(map & 0x80000000))
uch = QChar((ushort)map);
}
}
if (!code && !uch.row())
code = asciiToKeycode(uch.cell(), state);
}
-
不 处理 windows 的系统热键:Alt+Tab 等
// Special handling of global Windows hotkeys
if (state == Qt::AltModifier) {
switch (code) {
case Qt::Key_Escape:
case Qt::Key_Tab:
case Qt::Key_Enter:
case Qt::Key_F4:
return false; // Send the event on to Windows
case Qt::Key_Space:
// do not pass this key to windows, we will process it ourselves
qt_show_system_menu(widget->window());
return true;
default:
break;
}
}
// Map SHIFT + Tab to SHIFT + BackTab, QShortcutMap knows about this translation
if (code == Qt::Key_Tab && (state & Qt::ShiftModifier) == Qt::ShiftModifier)
code = Qt::Key_Backtab;
- 如果有上次按键记录,则这次属于 auto-repeating
// If we have a record, it means that the key is already pressed, the state is the same
// so, we have an auto-repeating key
if (rec) {
if (code < Qt::Key_Shift || code > Qt::Key_ScrollLock) {
k0 = q->sendKeyEvent(widget, grab, QEvent::KeyRelease, code,
Qt::KeyboardModifier(state), rec->text, true, 0,
scancode, msg.wParam, nModifiers);
k1 = q->sendKeyEvent(widget, grab, QEvent::KeyPress, code,
Qt::KeyboardModifier(state), rec->text, true, 0,
scancode, msg.wParam, nModifiers);
}
}
- 如果与上次的按键记录不同,则生成press事件
// No record of the key being previous pressed, so we now send a QEvent::KeyPress event,
// and store the key data into our records.
else {
QString text;
if (!uch.isNull())
text += uch;
char a = uch.row() ? 0 : uch.cell();
key_recorder.storeKey(msg.wParam, a, state, text);
k0 = q->sendKeyEvent(widget, grab, QEvent::KeyPress, code, Qt::KeyboardModifier(state),
text, false, 0, scancode, msg.wParam, nModifiers);
bool store = true;
// Alt+<alphanumerical> go to the Win32 menu system if unhandled by Qt
(Q_OS_WINCE)
if (msgType == WM_SYSKEYDOWN && !k0 && a) {
HWND parent = GetParent(widget->internalWinId());
while (parent) {
if (GetMenu(parent)) {
SendMessage(parent, WM_SYSCOMMAND, SC_KEYMENU, a);
store = false;
k0 = true;
break;
}
parent = GetParent(parent);
}
}
if (!store)
key_recorder.findKey(msg.wParam, true);
}
keyup 事件处理 (略)
sendKeyEvent
bool QKeyMapper::sendKeyEvent(QWidget *widget, bool grab,
QEvent::Type type, int code, Qt::KeyboardModifiers modifiers,
const QString &text, bool autorepeat, int count,
quint32 nativeScanCode, quint32 nativeVirtualKey, quint32 nativeModifiers,
bool *)
{
QKeyEventEx e(type, code, modifiers,
text, autorepeat, qMax(1, int(text.length())),
nativeScanCode, nativeVirtualKey, nativeModifiers);
QETWidget::sendSpontaneousEvent(widget, &e);
if (!isModifierKey(code)
&& modifiers == Qt::AltModifier
&& ((code >= Qt::Key_A && code <= Qt::Key_Z) || (code >= Qt::Key_0 && code <= Qt::Key_9))
&& type == QEvent::KeyPress
&& !e.isAccepted())
QApplication::beep(); // Emulate windows behavior
return e.isAccepted();
QApplication::notify()
QApplication::notify负责事件的派发:
case QEvent::ShortcutOverride:
case QEvent::KeyPress:
case QEvent::KeyRelease:
{
bool isWidget = receiver->isWidgetType();
bool isGraphicsWidget = false;
#ifndef QT_NO_GRAPHICSVIEW
isGraphicsWidget = !isWidget && qobject_cast<QGraphicsWidget *>(receiver);
#endif
if (!isWidget && !isGraphicsWidget) {
res = d->notify_helper(receiver, e);
break;
}
QKeyEvent* key = static_cast<QKeyEvent*>(e);
if (key->type()==QEvent::KeyPress) {
// Try looking for a Shortcut before sending key events
if ((res = qApp->d_func()->shortcutMap.tryShortcutEvent(receiver, key)))
return res;
qt_in_tab_key_event = (key->key() == Qt::Key_Backtab
|| key->key() == Qt::Key_Tab
|| key->key() == Qt::Key_Left
|| key->key() == Qt::Key_Up
|| key->key() == Qt::Key_Right
|| key->key() == Qt::Key_Down);
}
bool def = key->isAccepted();
QPointer<QObject> pr = receiver;
while (receiver) {
if (def)
key->accept();
else
key->ignore();
res = d->notify_helper(receiver, e);
QWidget *w = isWidget ? static_cast<QWidget *>(receiver) : 0;
QGraphicsWidget *gw = isGraphicsWidget ? static_cast<QGraphicsWidget *>(receiver) : 0;
...
break;
这儿在派发键盘事件之前,先进行了shortcutEvent的处理:
QShortcutMap::tryShortcutEvent()
-
先派发类型为 QEvent::ShortcutOverride 的QKeyEvent事件。(接收者可以阻止shortcut的继续传递)
- 如果上面的事件未被accept,则查找shortcut
- 匹配,则派发QShortcutEvent事件
/*! \internal
Uses ShortcutOverride event to see if any widgets want to override
the event. If not, uses nextState(QKeyEvent) to check for a grabbed
Shortcut, and dispatchEvent() is found an identical.
\sa nextState dispatchEvent
*/
bool QShortcutMap::tryShortcutEvent(QObject *o, QKeyEvent *e)
{
Q_D(QShortcutMap);
bool wasAccepted = e->isAccepted();
bool wasSpontaneous = e->spont;
if (d->currentState == QKeySequence::NoMatch) {
ushort orgType = e->t;
e->t = QEvent::ShortcutOverride;
e->ignore();
QApplication::sendEvent(o, e);
e->t = orgType;
e->spont = wasSpontaneous;
if (e->isAccepted()) {
if (!wasAccepted)
e->ignore();
return false;
}
}
QKeySequence::SequenceMatch result = nextState(e);
bool stateWasAccepted = e->isAccepted();
if (wasAccepted)
e->accept();
else
e->ignore();
int identicalMatches = d->identicals.count();
switch(result) {
case QKeySequence::NoMatch:
return stateWasAccepted;
case QKeySequence::ExactMatch:
resetState();
dispatchEvent(e);
default:
break;
}
// If nextState is QKeySequence::ExactMatch && identicals.count == 0
// we've only found disabled shortcuts
return identicalMatches > 0 || result == QKeySequence::PartialMatch;
}
-
注:QMenu的event成员中对 QEvent::ShortcutOverride 进行处理,以override掉某些键
而 派发的shortcut事件在QShortcut::event进行处理。bool QMenu::event(QEvent *e) { Q_D(QMenu); switch (e->type()) { case QEvent::Polish: d->updateLayoutDirection(); break; case QEvent::ShortcutOverride: { QKeyEvent *kev = static_cast<QKeyEvent*>(e); if (kev->key() == Qt::Key_Up || kev->key() == Qt::Key_Down || kev->key() == Qt::Key_Left || kev->key() == Qt::Key_Right || kev->key() == Qt::Key_Enter || kev->key() == Qt::Key_Return || kev->key() == Qt::Key_Escape) { e->accept(); return true; } }
/*! \internal */ bool QShortcut::event(QEvent *e) { Q_D(QShortcut); bool handled = false; if (d->sc_enabled && e->type() == QEvent::Shortcut) { QShortcutEvent *se = static_cast<QShortcutEvent *>(e); if (se->shortcutId() == d->sc_id && se->key() == d->sc_sequence){ if (se->isAmbiguous()) emit activatedAmbiguously(); else emit activated(); handled = true; } } return handled; }
参考
-
MSDN TranslateMessage Function
- Windows 程序设计