Qt之Windows鍵盤消息學習

爲了找到 QTBUG18896 問題的答案,只好先看看Windows下面的鍵盤消息處理,看到最後:發現這個問題和Windows彷佛沒有必然的聯繫 ^_^ (見 QToolBar焦點問題 (QTBUG18896) ) windows

Windows鍵盤消息

對產生可顯示字符的按鍵組合,Windows不只給程序發送按鍵消息,並且還發送字符消息。有些鍵不產生字符,這些鍵包括shift鍵、功能鍵、光標移動鍵和特殊字符鍵如Insert和Delete。對於這些鍵,Windows只產生按鍵消息app

按鍵消息ide

WM_KEYDOWN函數

通常是不帶Alt的按鍵(Windows自己不處理這些消息)ui

wParam 中保存虛擬鍵碼this

WM_KEYUPspa

WM_SYSKEYDOWN.net

系統按鍵,通常是帶 Alt(一般交由DefWindowProc處理)設計

WM_SYSKEYUPcode

字符消息

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掉某些鍵

    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;
                }
            }
    而 派發的shortcut事件在QShortcut::event進行處理。
    /*!
        \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 程序設計
相關文章
相關標籤/搜索