Статьи‎ > ‎Qt‎ > ‎

Перехват нажатия Print Screen в Qt

Нарисовалась не так давно задачка, суть которой сводится к тому что бы глобально перехватить нажатие клавиши  Print Screen в системе. Казалось бы довольно простая задача, но не тут то было. Глобальных хук в Qt сделать вообще не просто, для этого понадобилось подтянуть библиотеку Qxt, в ней есть замечательный класс  QxtGlobalShortcut который и позволяет делать глобальные хуки. Но каково было мое разочарование когда после написания небольшого примера, я узнал что и она не ловит Print Screen. Стало ясно что придется привязываться к API каждой системы, на которой будет работать мой софт.

Windows

Не очень я люблю эту систему, а ее API еще больше, ее code style заставляет сходить меня с ума. Но что уж делать задачу решать как то нужно. Поискал в интернете, поспрашивал на форумах и нашел вот такой кусочек кода 

#include <windows.h>
#include <stdlib.h>
#include <string.h>
#include <tchar.h>

#define _WIN32_WINNT 0x0400
#include <Windows.h>


///////////////////////////////////////////////////////////////////////////////


LRESULT CALLBACK LowLevelKeyboardProc(int nCode, 
   WPARAM wParam, LPARAM lParam) {

   BOOL fEatKeystroke = FALSE;

   if (nCode == HC_ACTION) {
      switch (wParam) {
      case WM_KEYDOWN:  case WM_SYSKEYDOWN:
      case WM_KEYUP:    case WM_SYSKEYUP: 
         PKBDLLHOOKSTRUCT p = (PKBDLLHOOKSTRUCT) lParam;
         fEatKeystroke = 
            ((p->vkCode == VK_SNAPSHOT)) ||            
            ((p->vkCode == VK_LWIN)) ||            
            ((p->vkCode == VK_RWIN)) ||            
               ((p->vkCode == VK_TAB) && ((p->flags & LLKHF_ALTDOWN) != 0)) ||
            ((p->vkCode == VK_ESCAPE) && ((p->flags & LLKHF_ALTDOWN) != 0)) ||
            ((p->vkCode == VK_ESCAPE) && ((GetKeyState(VK_CONTROL) & 0x8000) != 0));
         break;
      }
   }
   return(fEatKeystroke ? 1 : CallNextHookEx(NULL, nCode, wParam, lParam));
}


///////////////////////////////////////////////////////////////////////////////


int WINAPI WinMain(HINSTANCE hinstExe, HINSTANCE, LPSTR pszCmdLine, int) {

   // Install the low-level keyboard & mouse hooks
   HHOOK hhkLowLevelKybd  = SetWindowsHookEx(WH_KEYBOARD_LL, 
      LowLevelKeyboardProc, hinstExe, 0);

   // Keep this app running until we're told to stop
   MessageBox(NULL, 
      TEXT("Alt+Esc, Ctrl+Esc, and Alt+Tab are now disabled.\n")
      TEXT("Click \"Ok\" to terminate this application and re-enable these keys."),
      TEXT("Disable Low-Level Keys"), MB_OK);
   UnhookWindowsHookEx(hhkLowLevelKybd);

   return(0);
}

он перехватывает не только снапшот, но и еще некоторые кнопки. В принципе меня этот пример полностью устраивал, но оставалась еще дна целевая ОС это Linux

Linux

Эта ос мне нравится гораздо больше, поэтому и пример нашелся и заработал гараздо быстрее. Для перехвата Print Screen в Linux пришлось правда влезть в дебри X11, а примерчик то нашелся  следующего содержания

#include <X11/Xlib.h>

int main(int argc, char **argv)
{
   Display *dpy;
   XEvent ev;
   char *s;
   unsigned int kc;
   int quit = 0;

   if (NULL==(dpy=XOpenDisplay(NULL))) {
      perror(argv[0]);
      exit(1);
   }

   /*
    * You might want to warp the pointer to somewhere that you know
    * is not associated with anything that will drain events.
    *  (void)XWarpPointer(dpy, None, DefaultRootWindow(dpy), 0, 0, 0, 0, x, y);
    */

   XGrabKeyboard(dpy, DefaultRootWindow(dpy),
                 True, GrabModeAsync, GrabModeAsync, CurrentTime);

   printf("KEYBOARD GRABBED!  Hit 'q' to quit!\n"
          "If this job is killed or you get stuck, use Ctrl-Alt-F1\n"
          "to switch to a console (if possible) and run something that\n"
          "ungrabs the keyboard.\n");


   /* A very simple event loop: start at "man XEvent" for more info. */
   /* Also see "apropos XGrab" for various ways to lock down access to
    * certain types of info. coming out of or going into the server */
   for (;!quit;) {
      XNextEvent(dpy, &ev);
      switch (ev.type) {
         case KeyPress:
            kc = ((XKeyPressedEvent*)&ev)->keycode;
            s = XKeysymToString(XKeycodeToKeysym(dpy, kc, 0));
            /* s is NULL or a static no-touchy return string. */
            if (s) printf("KEY:%s\n", s);
            if (!strcmp(s, "q")) quit=~0;
            break;
         case Expose:
               /* Often, it's a good idea to drain residual exposes to
                * avoid visiting Blinky's Fun Club. */
               while (XCheckTypedEvent(dpy, Expose, &ev)) /* empty body */ ;
            break;
         case ButtonPress:
         case ButtonRelease:
         case KeyRelease:
         case MotionNotify:
         case ConfigureNotify:
         default:
            break;
      }
   }

   XUngrabKeyboard(dpy, CurrentTime);

   if (XCloseDisplay(dpy)) {
      perror(argv[0]);
      exit(1);
   }

   return 0;
}
 
Из этого примера ясно что есть некоторый цыкл который перехватывает события от X11,
 и мы собственно можем рулить этими событиями. Есть одно но, если запустить данный
 пример, события клавиатуры будут все перехвачены приложением и до системы не 
дойдут, в нашей реализации попробуем победить этот недостаток.

Реализация Hook Key
Класс для примера я решил назвать HookKeyboard, посмотрим описание класса


#ifdef Q_OS_WIN32 || Q_OS_WIN
#include <windows.h>
#include <stdlib.h>
#include <string.h>
#include <tchar.h>
#define _WIN32_WINNT 0x0400
#include <Windows.h>
LRESULT CALLBACK LowLevelKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam);
#endif
class HookKeyboard : public QObject
{
    Q_OBJECT
    public:
        static HookKeyboard *instance() {
            if (m_instance == NULL)
                m_instance = new HookKeyboard();
            return m_instance;
        }
        enum HookKey {
            Print = 0,
            Shift,
            Ctrl
        };
        void startHook();
        void endHook();
        bool isActive(){return m_start;}
     public slots:
        void press(int );

     private:
        HookKeyboard(QObject *parent = 0);
        static HookKeyboard *m_instance;
        #ifdef Q_OS_WIN32 || Q_OS_WIN
            HHOOK hhkLowLevelKybd;
        #endif
        bool m_start;
     signals:
        void keyPress(HookKeyboard::HookKey );
};


Из описания видно что данный класс описан как синглтон, здесь пожалуй нету какой то
 заковырки просто показалось что это будет удобно. Есть два метода startHook и endHook
 которые собственно запускают и прекращают хук Print Screen. Так же немаловажно что
 есть enum клавишь которые нам интересны и сигнал который будет вызываться каждый
 раз когда нажата интересующая нас клавиша, в котором будет передаваться код клавиши
(из нашего enum, не системный). 

А вот и реализация класса


#include <QDebug>
#include <QtConcurrentRun>

#ifdef Q_OS_LINUX
#include <stdio.h>
#include <stdlib.h>
#include <iostream>
#include <string.h>
#include <X11/Xlib.h>
void qt_x11_hookKey(HookKeyboard *);
#endif
HookKeyboard *HookKeyboard::m_instance = NULL;
HookKeyboard::HookKeyboard(QObject *parent)
    :QObject(parent)
{
    m_start = false;
}
void HookKeyboard::startHook()
{
    #ifdef Q_OS_WIN32 || Q_OS_WIN
      hhkLowLevelKybd =  SetWindowsHookEx(WH_KEYBOARD_LL, (LowLevelKeyboardProc), 0, 0);
    #endif
    #ifdef Q_OS_LINUX
      QtConcurrent::run(qt_x11_hookKey,this);
    #endif
    m_start = true;
}
void HookKeyboard::endHook()
{
    #ifdef Q_OS_WIN32 || Q_OS_WIN
        UnhookWindowsHookEx(hhkLowLevelKybd);
    #endif
    m_start = false;
}
void HookKeyboard::press(int key)
{
    emit keyPress(static_cast<HookKeyboard::HookKey>(key));
}
#ifdef Q_OS_WIN32 || Q_OS_WIN
LRESULT CALLBACK LowLevelKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) {
   BOOL fEatKeystroke = FALSE;
   if (nCode == HC_ACTION) {
      switch (wParam) {
      case WM_KEYDOWN:  case WM_SYSKEYDOWN:
      case WM_KEYUP:    case WM_SYSKEYUP:
         PKBDLLHOOKSTRUCT p = (PKBDLLHOOKSTRUCT) lParam;
         fEatKeystroke = ((p->vkCode == VK_SNAPSHOT) && (((GetKeyState(VK_CONTROL) & 0x8000) != 0)) && (((GetKeyState(VK_SHIFT) & 0x8000) != 0)));
         if (fEatKeystroke)
         {
             HookKeyboard::instance()->press(HookKeyboard::Print | HookKeyboard::Ctrl | HookKeyboard::Shift);
             return(fEatKeystroke ? 1 : CallNextHookEx(NULL, nCode, wParam, lParam));
         }
         fEatKeystroke = ((p->vkCode == VK_SNAPSHOT) && (((GetKeyState(VK_CONTROL) & 0x8000) != 0)));
         if (fEatKeystroke)
         {
             HookKeyboard::instance()->press(HookKeyboard::Print | HookKeyboard::Ctrl);
             return(fEatKeystroke ? 1 : CallNextHookEx(NULL, nCode, wParam, lParam));
         }
         fEatKeystroke = ((p->vkCode == VK_SNAPSHOT) && (((GetKeyState(VK_SHIFT) & 0x8000) != 0)));
         if (fEatKeystroke)
         {
             HookKeyboard::instance()->press(HookKeyboard::Print | HookKeyboard::Shift);
             return(fEatKeystroke ? 1 : CallNextHookEx(NULL, nCode, wParam, lParam));
         }
         fEatKeystroke=(p->vkCode == VK_SNAPSHOT);
         if (fEatKeystroke)
         {
             HookKeyboard::instance()->press(HookKeyboard::Print);
             return(fEatKeystroke ? 1 : CallNextHookEx(NULL, nCode, wParam, lParam));
         }
         break;
      }
   }
   return(fEatKeystroke ? 1 : CallNextHookEx(NULL, nCode, wParam, lParam));
}
#endif
#ifdef Q_OS_LINUX
void qt_x11_hookKey(HookKeyboard *hk)
{
    Display *dpy;
    XEvent ev;
    char *s;
    unsigned int kc;
    if (NULL==(dpy=XOpenDisplay(NULL))) {
       qDebug() << Q_FUNC_INFO << "error XOpenDisplay";
    }
    XGrabKeyboard(dpy, DefaultRootWindow(dpy),
                  true, GrabModeAsync, GrabModeAsync, CurrentTime);
    bool ctrlmodified = false;
    bool shiftmodified = false;

    while(hk->isActive()) {
       XNextEvent(dpy, &ev);
       switch (ev.type) {
          case KeyPress:
             kc = ((XKeyPressedEvent*)&ev)->keycode;
             s = XKeysymToString(XKeycodeToKeysym(dpy, kc, 0));

             if (!strcmp(s, "Control_L")) ctrlmodified=true;
             if (!strcmp(s, "Control_R")) ctrlmodified=true;
             if (!strcmp(s, "Shift_L")) shiftmodified=true;
             if (!strcmp(s, "Shift_R")) shiftmodified=true;
             if (!strcmp(s, "Print") && ctrlmodified && shiftmodified) {
                hk->press(HookKeyboard::Print | HookKeyboard::Shift | HookKeyboard::Ctrl);
                continue;
             }
             if (!strcmp(s, "Print") && ctrlmodified) {
                hk->press(HookKeyboard::Print | HookKeyboard::Ctrl);
                continue;
             }
             if (!strcmp(s, "Print") && shiftmodified) {
                hk->press(HookKeyboard::Print | HookKeyboard::Shift);
                continue;
             }

             if (!strcmp(s, "Print")) {
                hk->press(HookKeyboard::Print);
                continue;
             }
             XSendEvent(dpy, 0, false, KeyPressMask,&ev);
             break;
          case Expose:
             while (XCheckTypedEvent(dpy, Expose, &ev)) /* empty body */ ;
             break;
          case ButtonPress:
          case ButtonRelease:
          case KeyRelease:
             kc = ((XKeyReleasedEvent*)&ev)->keycode;
             s = XKeysymToString(XKeycodeToKeysym(dpy, kc, 0));
             if (!strcmp(s, "Control_L")) ctrlmodified=false;
             if (!strcmp(s, "Control_R")) ctrlmodified=false;
             if (!strcmp(s, "Shift_L")) shiftmodified=false;
             if (!strcmp(s, "Shift_R")) shiftmodified=false;
          case MotionNotify:
          case ConfigureNotify:
          default:
             break;
       }
    }
    XUngrabKeyboard(dpy, CurrentTime);
    if (XCloseDisplay(dpy)) {
       qDebug() << Q_FUNC_INFO << "error XCloseDisplay";
    }
}
#endif


Рассмотри тонкие моменты которые здесь присутствуют

1. Разделение на ОС идет с помощью дефайнов, наверное нового ничего в этом нету, но просто данная вещь присутствует.
2. В конструкторе класса ставим переменную m_start в false тем самым говоря что хук пока не производится.
3. При вызове startHook проверяем текущую ОС и вызваем либо фцию LowLevelKeyboardProc
для виндовс либо с помощью QtConcurrent запускаем ф-цию qt_x11_hookKey которая ловит
события под иксами.

Собственно дальше все просто, эти ф-ции ловят, нажатия клавишь, и вызывают слот press
нашего класса, который в свою очередь эмитит сигнал keyPress.

4. Остался один тонкий момент, это то что под X11 как уже говорилось ловятся все события
с клавиатуры и не возвращаются в случае если это не те события которые нам нужны.
Здесь как оказалось тоже все просто, после того как мы проверили события, и оказалось
что это не то событие которое нам нужно, необходимо вызвать ф-цию XSendEvent() которая
передаст событие системы.

На этом пожалуй все, готовый пример приложу ;) Как всегда критика в реализации
приветствуется. 

P.S.
За написание статьи спасибо пользователям рунета bsa и boostcoder они мне здорово помогли.
ċ
hook_print.zip
(4k)
Антон Михайлов,
25 дек. 2011 г., 0:17
Comments