15 мая 2023 года "Исходники.РУ" отмечают своё 23-летие!
Поздравляем всех причастных и неравнодушных с этим событием!
И огромное спасибо всем, кто был и остаётся с нами все эти годы!

Главная Форум Журнал Wiki DRKB Discuz!ML Помощь проекту


Работа с клавиатурой.

Каждое окно в системе устроено таким образом, что может получать сообщения при нажатии клавиш на клавиатуре. Эти сообщения имеют два типа: виртуальные коды и символьные сообщения. Для того, чтобы преобразовывать виртуальные коды клавиш в соответствующием им символьные, к окну должен быть прикреплён цикл трансляции сообщений. Если окно отображает ввод с клавиатуры, то оно должно создавать и отображать каретку для указания места появления следующего символа.

Обработка нажатия клавиш

При нажатии клавиш на клавиатуре в окно (а точнее в оконную процедуру), которое имеет фокус поступают сообщения WM_KEYDOWN, WM_KEYUP, WM_SYSKEYDOWN, и WM_SYSKEYUP. Основное сообщение, которое система отправляет окну при нажатии клавиши - WM_KEYDOWN.

При поступлении сообщения WM_KEYDOWN, оконная процедура должна проверить виртуальный код (который содержится в параметре wParam сообщения) и решить что дальше с ним делать. Обычно приложения обрабатывают только функциональные клавиши, стрелки, а так же специальные клавиши, такие как INS, DEL, HOME, и END, то есть все клавиши кроме букв и цифр.

Следующий пример демонстрирует работу обычной оконной процедуры, получающей и обрабатывающей нажатия клавиш:

        case WM_KEYDOWN:
            switch (wParam)
            {
                case VK_LEFT:

                    // Нажата стрелка влево.

                    break;

                case VK_RIGHT:

                    // Нажата стрелка враво.

                    break;

                case VK_UP:

                    // Нажата стрелка вверх.

                    break;

                case VK_DOWN:

                    // Нажата стрелка вниз.

                    break;

                case VK_HOME:

                    // Нажата клавиша HOME.

                    break;

                case VK_END:

                    // Нажата клавиша END.

                    break;

                case VK_INSERT:

                    // Нажата клавиша INS.

                    break;

                case VK_DELETE:

                    // Нажата клавиша DEL.

                    break;

                case VK_F2:

                    // Нажата клавиша F2.

                    break;


                // Обработка других не-буквенных клавиш.

                default:
                    break;
            }

Трансляция символьных сообщений

Каждый поток, который получает ввод с клавиатуры, должен иметь в своём цикле сообщений функцию TranslateMessage. Эта функция позволяет отделить нажатия функциональных клавиш от символьных и преобразовать виртуальный код клавиши в символьный, а так же поместить его в очередь сообщений. При этом в параметре wParam символьного сообщения будет содержаться код символа.

Вообще, цикл сообщений использует функцию TranslateMessage для трансляции всех сообщений, а не только для клавиатуры. Однако, применение TranslateMessage к другим типам сообщений не несёт никакого эффекта, зато гарантирует корректную трансляцию ввода с клавиатуры.

Пример:

MSG msg;
BOOL bRet;

while (( bRet = GetMessage(&msg, (HWND) NULL, 0, 0)) != 0)
{
    if (bRet == -1);
    {
        // обработка ошибки
    }
    else
    {
        if (TranslateAccelerator(hwndMain, haccl, &msg) == 0)
        {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
    }
}

Обработка буквенных сообщений

Если функция TranslateMessage перевела виртуальный код клавиши в буквенное значение, то оконная процедура получит одно из следующих сообщений: WM_CHAR, WM_DEADCHAR, WM_SYSCHAR, и WM_SYSDEADCHAR. По умолчанию, оконная процедура получает только сообщение WM_CHAR игнорируя при этом остальные буквенные сообщения. Функция TranslateMessage генерирует сообщение WM_CHAR если была нажата одна из следующих клавиш:

  • Любая буквенная клавиша
  • BACKSPACE
  • ENTER (возврат каретки)
  • ESC
  • SHIFT+ENTER (перевод строки)
  • TAB

При поступлении сообщения WM_CHAR, код символа содержится в параметре wParam.

Пример:

        case WM_CHAR:
            switch (wParam)
            {
                case 0x08: 

                    // backspace.

                    break;

                case 0x0A:

                    // Перевод строки.

                    break;

                case 0x1B:

                    // клавиша Esc. 

                    break;

                case 0x09:

                    // клавиша табуляции (tab).

                    break;

                case 0x0D:

                    // возврат каретки (Enter).

                    break;

                default:

                    // Обрабатываем остальные отображаемые символы.

                    break;
            }

Использование каретки

Обычно, каретка (текстовый курсор) используется для отображения позиции, в которой появится следующий символ. Если окно позволяет вводить текст, то при получении фокуса, оно должно создать и отобразить каретку и наоборот, при потере фокуса, необходимо скрыть и удалить каретку. Делается это в обработчиках сообщений WM_SETFOCUS и WM_KILLFOCUS.

Отображение ввода с клавиатуры

Ниже представлен пример, который демонстрирует, как приложение может получать введённые символы с клавиатуры, отображать их в клиентской области окна, и обновлять положение текстового курсора с каждым введённым символом. Так же в примере показывается как перемещать каретку в ответ на следующие клавиши: СТРЕЛКА ВЛЕВО, СТРЕЛКА ВПРАВО, HOME, и END, а так же как подсветить выделенный текст в ответ на комбинацию клавиш SHIFT+СТРЕЛКА ВПРАВО.

В обработчике сообщения WM_CREATE оконная процедура выделяет буфер объёмом 64K для хранения ввода с клавиатуры. Так же, пример получает текущие настройки шрифта, сохраняя высоту и среднюю ширину символов. Высота и ширина используются в обработчике WM_SIZE, чтобы вычислить длину строки и максимальное количество строк в клиентской области.

В обработчике WM_SETFOCUS, оконная процедура создаёт и отображает каретку, а в обработчике WM_KILLFOCUS скрывает и удаляет её.

В обработчике WM_CHAR, отображаются символы из буфера ввода, и изменение координаты каретки. Так же оконная процедура преобразует символы табуляции в четыре последовательных пробела. Символы Backspace, перевод строки и escape генерируют звуковой сигнал (beep).

В обработчике сообщения WM_KEYDOWN обрабатываются перемещения каретки влево, вправо, в конец (end) и в начало (home). При обработке правой стрелки, оконная процедура проверяет состояние клавиши SHIFT и, если она нажата, то выделяет символ справа от кратки при её перемещении.

Обратите внимание, что приведённый код будет работать как в Unicode, так и в ANSI кодировках.

Пример:

#define BUFSIZE 65535
#define SHIFTED 0x8000

LONG APIENTRY MainWndProc(HWND hwndMain, UINT uMsg,
                             WPARAM wParam, LPARAM lParam)
{
    HDC hdc;                   // дескриптор контекста устройства
    TEXTMETRIC tm;             // структура с параметрами текста
    static DWORD dwCharX;      // средняя ширина символов
    static DWORD dwCharY;      // высота символов
    static DWORD dwClientX;    // ширина клиентской области
    static DWORD dwClientY;    // Высота клиентской области
    static DWORD dwLineLen;    // длина строки
    static DWORD dwLines;      // кол-во строк в клиентской области
    static int nCaretPosX = 0; // горизонтальная координата каретки
    static int nCaretPosY = 0; // вертикальная координата каретки
    static int nCharWidth = 0; // ширина символа
    static int cch = 0;        // кол-во символов в буфере
    static int nCurChar = 0;   // номер текущего символа
    static PTCHAR pchInputBuf; // буфер ввода
    int i, j;                  // счётчики цикла
    int cCR = 0;               // счётчик возвратов каретки
    int nCRIndex = 0;          // номер последнего возврата каретки
    int nVirtKey;              // код виртуальной клавиши
    TCHAR szBuf[128];          // временный буфер
    TCHAR ch;                  // текущий символ
    PAINTSTRUCT ps;            // требуется для BeginPaint
    RECT rc;                   // прямоугольник для DrawText
    SIZE sz;                   // размерность строки
    COLORREF crPrevText;       // предыдущий цвет текста
    COLORREF crPrevBk;         // предыдущий цвет фона
	size_t * pcch;
	HRESULT hResult;

    switch (uMsg)
    {
        case WM_CREATE:

            // Получаем параметры текущего шрифта. 

            hdc = GetDC(hwndMain);
            GetTextMetrics(hdc, &tm);
            ReleaseDC(hwndMain, hdc);

            // Сохраняем среднюю ширину и высоту символа.

            dwCharX = tm.tmAveCharWidth;
            dwCharY = tm.tmHeight;

            // Выделяем буфер для хранения ввода с клавиатуры.

            pchInputBuf = (LPTSTR) GlobalAlloc(GPTR,
                BUFSIZE * sizeof(TCHAR));
            return 0;

        case WM_SIZE:

            // Сохраняем новую ширину и высоту клиентской области.

            dwClientX = LOWORD(lParam);
            dwClientY = HIWORD(lParam);

            // Вычисляем максимальную ширину строки и максимальное
            // количество строк в клиентской области.

            dwLineLen = dwClientX - dwCharX;
            dwLines = dwClientY / dwCharY;
            break;


        case WM_SETFOCUS:

            // Когда окно получает фокус, то создаём, позиционируем
            // и отображаем каретку.

            CreateCaret(hwndMain, (HBITMAP) 1, 0, dwCharY);
            SetCaretPos(nCaretPosX, nCaretPosY * dwCharY);
            ShowCaret(hwndMain);
            break;

        case WM_KILLFOCUS:

            // Когда окно теряет фокус, то скрываем и
            // уничтожаем каретку.

            HideCaret(hwndMain);
            DestroyCaret();
            break;

        case WM_CHAR:
		// Проверяем, не находится ли текущее положение близко
		// к концу буфера, чтобы не произошло его переполнение.
		// Если так, то добавляем ноль и отображаем содержимое.
	if (cch > BUFSIZE-5)
	{
		pchInputBuf[cch] = 0x00;
		SendMessage(hwndMain, WM_PAINT, 0, 0);
	}
            switch (wParam)
            {
                case 0x08:  // backspace
                case 0x0A:  // перевод строки
                case 0x1B:  // esc
                    MessageBeep((UINT) -1);
                    return 0;

                case 0x09:  // tab

                    // Преобразуем табы в четыре последовательных пробела.

                    for (i = 0; i < 4; i++)
                        SendMessage(hwndMain, WM_CHAR, 0x20, 0);
                    return 0;

                case 0x0D:  // возврат каретки

                    // Записываем возврат каретки и её позицию
                    // в начале новой строки.

                    pchInputBuf[cch++] = 0x0D;
                    nCaretPosX = 0;
                    nCaretPosY += 1;
                    break;

                default:    // отображаемые символы
 
                    ch = (TCHAR) wParam; 
                    HideCaret(hwndMain); 
 
                    // Получаем ширину сиволов и выводим
                    // символ.

                    hdc = GetDC(hwndMain);
                    GetCharWidth32(hdc, (UINT) wParam, (UINT) wParam,
                        &nCharWidth);
                    TextOut(hdc, nCaretPosX, nCaretPosY * dwCharY,
                        &ch, 1);
                    ReleaseDC(hwndMain, hdc);

                    // Store the character in the buffer.

                    pchInputBuf[cch++] = ch;

                    // Вычисляем новую горизонтальную координат каретки.
                    // Если координата достигла максимума, то вставляем
                    // перевод каретки и перемещаем каретку
                    // в начало следующей строки.

                    nCaretPosX += nCharWidth;
                    if ((DWORD) nCaretPosX > dwLineLen)
                    {
                        nCaretPosX = 0;
                        pchInputBuf[cch++] = 0x0D;
                        ++nCaretPosY;
                    }
                    nCurChar = cch;
                    ShowCaret(hwndMain);
                    break;
            }
            SetCaretPos(nCaretPosX, nCaretPosY * dwCharY);
            break;

        case WM_KEYDOWN:
            switch (wParam)
            {
                case VK_LEFT:   // стрелка влево
 
                    // Каретка может быть перемещена только
                    // в начало текущей строки.
 
                    if (nCaretPosX > 0)
                    {
                        HideCaret(hwndMain);

                        // Получаем символ слева от каретки, вычисляем
                        // ширину символов, а затем вычитаем ширину
                        // из текущей горизонтальной координаты
                        // каретки, чтобы получить новую координату
 
                        ch = pchInputBuf[--nCurChar]; 
                        hdc = GetDC(hwndMain); 
                        GetCharWidth32(hdc, ch, ch, &nCharWidth); 
                        ReleaseDC(hwndMain, hdc); 
                        nCaretPosX = max(nCaretPosX - nCharWidth, 
                            0); 
                        ShowCaret(hwndMain); 
                    } 
                    break; 
 
                case VK_RIGHT:  // стрелка вправо
 
                    // Перемещаем каретку вправо или, если достугнут
                    // перевод каретки, в начало следующей строки.
 
                    if (nCurChar < cch)
                    {
                        HideCaret(hwndMain);

                        // Получаем символ справа от каретки.
                        // Если это перевод каретки, то перемещаем
                        // курсор в начало следующей строки.
 
                        ch = pchInputBuf[nCurChar];
                        if (ch == 0x0D)
                        {
                            nCaretPosX = 0;
                            nCaretPosY++;
                        }

                        // Если символ не является переводом каретки,
                        // то проверяем, не нажата ли клавиша SHIFT.
                        // Если так, то инвертируем цвета текста
                        // и выводим символ.
 
                        else
                        {
                            hdc = GetDC(hwndMain);
                            nVirtKey = GetKeyState(VK_SHIFT);
                            if (nVirtKey & SHIFTED)
                            {
                                crPrevText = SetTextColor(hdc,
                                    RGB(255, 255, 255));
                                crPrevBk = SetBkColor(hdc,
                                    RGB(0,0,0));
                                TextOut(hdc, nCaretPosX,
                                    nCaretPosY * dwCharY,
                                    &ch, 1);
                                SetTextColor(hdc, crPrevText);
                                SetBkColor(hdc, crPrevBk);
                            }

                            // Получаем ширину символа и вычисляем
                            // новую горизонтальную координату каретки.
 
                            GetCharWidth32(hdc, ch, ch, &nCharWidth);
                            ReleaseDC(hwndMain, hdc);
                            nCaretPosX = nCaretPosX + nCharWidth;
                        }
                        nCurChar++;
                        ShowCaret(hwndMain);
                        break;
                    }
                    break;

                case VK_UP:     // стрелка вверх
                case VK_DOWN:   // стрелка вниз
                    MessageBeep((UINT) -1);
                    return 0;

                case VK_HOME:   // HOME

                    // Устанавливаем каретку в верхний левый
                    // угол клиентской области.
 
                    nCaretPosX = nCaretPosY = 0;
                    nCurChar = 0;
                    break;

                case VK_END:    // END

                    // Перемещаем каретку в конец текста.
 
                    for (i=0; i < cch; i++)
                    {
                        // Считаем возвраты каретки и сохраняем
                        // их номера.
 
                        if (pchInputBuf[i] == 0x0D)
                        {
                            cCR++;
                            nCRIndex = i + 1;
                        }
                    }
                    nCaretPosY = cCR;

                    // Копируем весь текст между последним возвратом
                    // каретки и окончанием ввода с клавиатуры
                    // во временный буфер.
 
                    for (i = nCRIndex, j = 0; i < cch; i++, j++)
                        szBuf[j] = pchInputBuf[i];
                    szBuf[j] = TEXT('\0');

                    // Устанавливаем горизонтальную координату каретки.
 
                    hdc = GetDC(hwndMain);
			hResult = StringCchLength(szBuf, 128, pcch);
			if (FAILED(hResult))
			{
			// обработчик ошибки
			}
                    GetTextExtentPoint32(hdc, szBuf, *pcch, &sz);
                    nCaretPosX = sz.cx;
                    ReleaseDC(hwndMain, hdc);
                    nCurChar = cch;
                    break;

                default:
                    break;
            }
            SetCaretPos(nCaretPosX, nCaretPosY * dwCharY);
            break;

        case WM_PAINT:
            if (cch == 0)       // в буфере ввода ничего нет
                break;

            hdc = BeginPaint(hwndMain, &ps);
            HideCaret(hwndMain);

            // Устанавливаем границы прямоугольника, а затем
            // рисуем в нём текст.

            SetRect(&rc, 0, 0, dwLineLen, dwClientY);
            DrawText(hdc, pchInputBuf, -1, &rc, DT_LEFT);

            ShowCaret(hwndMain);
            EndPaint(hwndMain, &ps);
            break;

        // Обрабатываем другие сообщения.
        
        case WM_DESTROY:
            PostQuitMessage(0);

            // Освобождаем буфер ввода.
 
            GlobalFree((HGLOBAL) pchInputBuf);
            UnregisterHotKey(hwndMain, 0xAAAA);
            break;

        default:
            return DefWindowProc(hwndMain, uMsg, wParam, lParam);
    }
    return NULL;
}