Search code examples
cuser-interfacewinapiwine

win32 without mouse, how to assign keyboard shortcuts to buttons


I wrote a simple win32 GUI application with buttons and other controls, in pure C, no MFC. I want to make it more accessible for those who can not use mouse.

  1. First, my example code does not respond to Tab key to move focus from one button to another. I can press UI button using mouse, than it becomes focused and i can activate button using Space-bar, but i can't move focus to other buttons using Tab or Shift+Tab. How can i fix this?

  2. I want to assign keyboard cues (little underscores) to buttons, so user can use keyboard shortcuts to activate buttons.

I have google it around, but answers are hard googleable, so i need someone to point me to some documentation. A little piece of code would be very helpful.

Here is the code i have. I compile and run it on Linux using WINE + TinyCC


#include <stdio.h>
#include <windows.h>

/* Yeres distributed forums node v0001 */

/* compile: wine tcc_win/tcc/tcc.exe win_yeres */


HWND status_text_hwnd = NULL;
HWND shutdown_hs_button_hwnd = NULL;

unsigned int button_height = 26;
unsigned int window_length = 400;
#define V_BUTTON_PADDING 2

int tor_running = 0;
int select_running = 0;


#include "yeres_bind.c"
#include "win_yeres_w1_proc.c"


int APIENTRY WinMain(
        HINSTANCE hInstance,
        HINSTANCE hPrevInstance,
        LPSTR lpCmdLine,
        int nCmdShow
        ) {

  MSG msg;
  WNDCLASS wc;
  HWND hwnd;

  printf("\n  **  YERES v0001  **\n\n");


  // Fill in window class structure with parameters that describe
  // the main window.

  ZeroMemory(&wc, sizeof wc);
  wc.hInstance     = hInstance;
  wc.lpszClassName = "yeres_wcl";
  wc.lpfnWndProc   = (WNDPROC)w1_proc;
  wc.style         = CS_DBLCLKS|CS_VREDRAW|CS_HREDRAW;
  wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 0);
  wc.hIcon         = LoadIcon(NULL, IDI_EXCLAMATION);
  wc.hCursor       = LoadCursor(NULL, IDC_ARROW);

  if (FALSE == RegisterClass(&wc)) return 0;

  RECT client_rect;
  client_rect.left = 0;
  client_rect.top = 0;
  client_rect.right = window_length;
  client_rect.bottom = 500;

  /* calculate window size from known client area size */
  if (0 == AdjustWindowRect(&client_rect, wc.style, 0)) return 0;

  // create main window
  hwnd = CreateWindow(
    "yeres_wcl",
    "YERES control panel",
    WS_OVERLAPPEDWINDOW|WS_VISIBLE,
    30,
    30,
    client_rect.right,   //CW_USEDEFAULT
    client_rect.bottom,
    0,
    0,
    hInstance,
    0);

  if (NULL == hwnd) return 0;

  // Main message loop:
  while (GetMessage(&msg, NULL, 0, 0) > 0) {
    TranslateMessage(&msg);
    DispatchMessage(&msg);
  }

  return msg.wParam;
}

#define ID_TOR_CONTROL_PORT 1
#define ID_BIND 2
#define ID_UNBIND 3
#define ID_TOR_SHUTDOWN_HS 4
#define ID_PORT_NUM 5
#define ID_QUIT 6


LRESULT CALLBACK w1_proc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) {

  int y = 4;
  int x;
  RECT rc;
  switch (message) {

    case WM_CREATE:

    ZeroMemory(&rc, sizeof(rc));
    GetClientRect(hwnd, &rc);
    if (rc.right) window_length = rc.right - rc.left;

    status_text_hwnd = CreateWindow("Static",
                "** YERES v0001 **\n\nPress 'Bind to localhost'..",
                WS_CHILD | WS_VISIBLE | SS_CENTER,
                3, y, window_length, (button_height * 3),
                hwnd, (HMENU) 1, NULL, NULL);
    y += button_height * 3;

    x = 2;
    CreateWindow("Static", "Port:",
                WS_CHILD | WS_VISIBLE | SS_CENTER,
                x, y, 60, button_height,
                hwnd, (HMENU) 1, NULL, NULL);
    x += 60;
    /* editable field here */
    CreateWindow("Edit", "19407", 
                WS_CHILD | WS_VISIBLE | WS_BORDER,
                x, y, 90, button_height, hwnd, (HMENU) ID_PORT_NUM,
                NULL, NULL);
    x += 120;

    CreateWindow("Button", "Bind to localhost",
                WS_VISIBLE | WS_CHILD ,
                x, y, 140, button_height,
                hwnd, (HMENU) ID_BIND, NULL, NULL);
    x += 140;
    CreateWindow("Button", "Unbind",
                WS_VISIBLE | WS_CHILD ,
                x, y, window_length - 2 - x, button_height,
                hwnd, (HMENU) ID_UNBIND, NULL, NULL);

    y += button_height + V_BUTTON_PADDING;

    CreateWindow("Button", "Use tor control port to start HiddenService",
                WS_VISIBLE | WS_CHILD ,
                2, y, window_length - 4, button_height,
                hwnd, (HMENU) ID_TOR_CONTROL_PORT, NULL, NULL);
    y += button_height + V_BUTTON_PADDING;

    shutdown_hs_button_hwnd = CreateWindow("Button", "Shutdown HiddenService",
                WS_VISIBLE | WS_CHILD ,
                2, y, window_length - 4, button_height,
                hwnd, (HMENU) ID_TOR_SHUTDOWN_HS, NULL, NULL);
    EnableWindow(shutdown_hs_button_hwnd, 0); /* inactive button */
    y += button_height + V_BUTTON_PADDING;

    CreateWindow("Button", "Shutdown EVERYTHING and QUIT",
                WS_VISIBLE | WS_CHILD ,
                2, y, window_length - 4, button_height,
                hwnd, (HMENU) ID_QUIT, NULL, NULL);
    y += button_height + V_BUTTON_PADDING;

    break;

    case WM_DESTROY:
    if (!tor_running && !select_running) PostQuitMessage(0);
    break;


    case WM_COMMAND:
    switch (LOWORD(wParam)) {
      case ID_BIND:
      yeres_bind();
      break;

      case ID_QUIT:
      PostQuitMessage(0);
      break;

      default: DefWindowProc(hwnd, message, wParam, lParam);
    }
    break;

    default:
    return DefWindowProc(hwnd, message, wParam, lParam);

  };
  return 0;
};

P.S. Sorry my bad English :)


Solution

  • It was simple. In main message processing loop, i call IsDialogMessage() with proper HWND. Then, if this function returns zero, i call normal TranslateMessage and DispatchMessage functions. Here is code:

    while(GetMessage(&msg, NULL, 0, 0) > 0) {
      if(!IsDialogMessage(hwnd, &msg)) {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
      };
    
      yeres_select(); /* scan incoming connections on sockets using select(), activated by timer and/or other messages */
    
    }
    

    Every button in the window has bit WS_TABSTOP set, so when i press Tab, IsDialogMessage() will catch message and move focus to the next button. Editable also can be selected this way.

    WS_GROUP didn't work for me. In theory it should allow me to navigate using arrow keys. I have placed WS_GROUP bit in first button.

    CreateWindow("Button", "&Bind to localhost",
                WS_VISIBLE | WS_CHILD | WS_TABSTOP | WS_GROUP,
                x, y, 140, button_height,
                hwnd, (HMENU) ID_BIND, NULL, NULL);
    

    Ampersand mnemonics work even without TranslateAccelerator() call, just put it into label text "&Bind to localhost", and button can be activated using Alt+B or so. It is not case-sensitive.

    I did not check it on Windows, but i think if it works in WINE it should be OK.