Qt之Windows键盘消息学习

为了找到 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掉某些键

    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 程序设计

 

  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值