Back to Software

Windows Calculator

8 May 2017
Progress: Complete

Why? Why would we want to go and do something like recreate windows calculator? I must admit that having bedded myself quite comfortably in the world of Javascript, where every user interface is instantly, universally cross-platform, diving into the Windows API does seem like a step backwards. But it's educational. It's one of those rights-of-passage things.

The Windows API lets you build graphical applications in C. In the past I've always used huge libraries for GUIs, but there's a certain shame associated with it – even the simplest application usually ends up a good chunk of a megabyte. Doing the same with just the Windows API should produce an .exe file of only a couple kilobytes.

Like many things, once you get started you realize it's actually quite easy. The first step is setting up the build environment. There are many C compilers out there, but the one I chose here was lcc-win32. This is an old, proprietary C compiler for windows, with an utterly abysmal IDE, but it's extremely lightweight, the whole install was something like 20MB.

Windows API

All we need to do to start using windows API calls is include windows.h :

#include <windows.h>

int main(void) {
  MessageBox(NULL, "Hello World", "Hello", MB_OK);
  return 0;
}
Hello world in an alert box

Woohoo. The first parameter is the "parent window" which the message box belongs to. We haven't made one yet so we leave it null (normally, if you click on the parent window while a message box is open, it flashes the title bar of the message again). The last parameter is a bitwise flag which can be used to change the styling.

Lcc-win32 is supposed to come with an offline reference for the Windows API, but I couln't use it for some reason. So the only reference we have is MSDN, which is pretty bloated and slow. Generally just Googling "winapi messagebox" or whatever finds plenty of information on it. It may be an old technology, but that's a good thing: every problem you encounter has probably been solved a dozen times already.

There's a certain amount of terminology that we need to be aware of. Everything is a "window", even buttons or text edit boxes. What we'd call a "text input" or "textarea" in HTML are called an "EDIT" window, which needs the ES_MULTILINE flag set to be a textarea. ES stands for Edit Style.

Note that for all but the most trivial programs, we don't use main() as our entry point function, rather WinMain(). No doubt some stuff goes on in the background. It also hands us some useful parameters.

Windows programming is generally event-based. Windows uses a message queue, messages come in, get looked at, then get dispatched again. These messages range from "this button was clicked" to "the screen is about to be redrawn". Unlike Javascript, the program's message loop is part of our code, so we can change it or put code outside of it if needed.

  MSG Msg;

  while(GetMessage(&Msg, NULL, 0, 0) > 0) 
  {
    TranslateMessage(&Msg);
    DispatchMessage(&Msg);
  }
GetMessage() blocks execution until the next message comes in; another function, PeekMessage() returns immediately if the queue is empty. TranslateMessage() specifically looks at keyboard events and generates new messages based on vitual key codes (i.e. independent of keyboard layout, which is usually more useful). DispatchMessage() sends the message to the window procedure – this is a function we need to provide when we create our window class.

If we want to create a button, we call CreateWindow() with the "BUTTON" class. If we want to make our own type of window, such as the main window of our application, we register our own window class. There are a bunch of trivial parameters to the window class, but the important one is the window procedure. This gets called whenever that window receives a message. Inside it, there is nearly always a big switch statement which splits the messages by ID, then possibly further by their parameters.

When we call CreateWindow with our window class, we can then give it lots of window style flags, such as if it's resizeable, has min/maximize buttons in the top corner, etc. CreateWindow seems to have been superseded with a similar function, CreateWindowEx, which has an extra parameter, to accept even more style flags. There are extensive lists of possible styles on MSDN, but I'm not sure if it's worth linking to since the URLs appear to keep changing.

There are many tutorials on the Windows API out there, this summary is mostly just to remind myself. So, let's get started on our calculator.

The buttons and the display

There are two ways to fill our main window with child windows (the buttons). They can either be created manually, or loaded from a resource file. The resource file is a list of things that would be boring to type out, and most IDEs have resource editors so you can visually lay out your dialog boxes and such. But, I like to keep things simple, so, to hell with resource files.

In the main window procedure we process a message called WM_CREATE. At this point we can have a bunch more CreateWindow() calls to make our buttons. Since they're all nearly identical I defined a macro for it.

#define makeButton(name,id,x,y,w,h) CreateWindow("BUTTON",name,WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON, x,y,w,h, hwnd,(HMENU)(id),GetModuleHandle(NULL),NULL)
Notice that the ID parameter goes into the hMenu (handle to menu) argument, even needs to be cast to type HMENU. Don't ask me why, but that's where the ID goes. When the main window procedure receives the WM_COMMAND message, the wParam parameter will contain the ID of the button clicked.

It would be tedious to #define an ID for every button, so I just numbered them 100 to 109 for buttons 0 to 9.

We also need to create an EDIT control as the display. This will be disabled ( ES_READONLY ) but being a text box means the user can select and copy the text easily.

Some logic

A number button gets pressed, we need to push its value onto the display. We use GetWindowTextLength(), then GetDlgItemText() to load this into a char array. It's a null terminated string, so to append all we need to do is set the last element (null) to the new number, and set the following element to null. Then SetDlgItemText() writes it back to the display.

To get the number from the ID, by the way I defined it (possibly lacking foresight) we need to subtract 100, then add the ascii value of the character zero.

When we start calculating, we need that number as a float. Or actually a long double float, since we want our calculator to be nice and precise. We can use the standard function strtold(), which is like strtof() but longer and doublier. To write values back to the display I used sprintf().

And with that a basic calculator appears to be functioning! There are some subtleties to the behaviour, for instance, pressing buttons in a nonsensical order has a defined behaviour in the real windows calculator. And pressing equals again repeats the last operation. I've been doing so much embedded programming lately that I always worry about efficiency, but here I have to remind myself that there is completely negligible cost in adding a few extra variables to keep track of these things.

Screenshot of the Amazing calculator

Keyboard

The real windows calculator can be operated by keyboard events. In fact it's pretty useless without it. We can listen to the WM_KEYDOWN messages to implement this. We could send the appropriate button-click message back into the queue in response, but instead I just put the case statement for the keydown events directly above the button click events, and let it fall through.

To find the keycodes I got it to open a message box with the parameter value on each keydown event. I may be doing this the hard way, but for the number 8, it could also be the asterisk which would correspond to multiply. This means we have to track shift as well, but that's easy enough to do. A single line for the WM_KEYUP event zeros our shift variable.

With the keycode known, we then write new values into the wParam corresponding to the button ID, and let it fall into the WM_COMMAND case statement.

This works great until we start mixing mouse clicks with keyboard presses. The keydown message is sent to the window that has focus. At the beginning the main window has focus, but after a button is clicked, it's the button that captures all the key events, and our keyboard code stops working. Trying to SetFocus() back to the main window doesn't really work, since by the time the keyboard events come in, it's too late, and doing it on button clicks causes them to not visually depress properly. Instead, I decided to modify the message loop:

  while(GetMessage(&Msg, NULL, 0, 0) > 0) {
    if (Msg.message == WM_KEYDOWN || Msg.message == WM_KEYUP) Msg.hwnd = hwnd;
    TranslateMessage(&Msg);
    DispatchMessage(&Msg);
  }
Doing this, all keyup and keydown events are now redirected to the main window (hwnd, declared earlier). In a remarkably satisfying way, this method works perfectly.

Conclusion

That about sums up my windows calculator experience. The windows API. Not so scary really. The full source code is below, just a single .c file, no resources or headers. Also, on the odd chance you want it and if you dare download it, the compiled .exe file.

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

#define windowClassName "myWindowClass"
#define IDC_EDIT 500

int typing = 0;
BOOL final=0;
long double num1=0.0;
long double num2=0.0;
int op=0;
BOOL shift=0;

long double getScreen(HWND hwnd){
  int len = GetWindowTextLength(GetDlgItem(hwnd, IDC_EDIT));
  char buf[32];
  if (len>0) {
    GetDlgItemText(hwnd, IDC_EDIT, buf, len+1);
    return strtold(buf,NULL);
  }
  return 0.0;
};

void equate(HWND hwnd){
  switch (op) {
    case 110: num1 += num2; break;
    case 111: num1 -= num2; break;
    case 112: num1 *= num2; break;
    case 113: num1 /= num2; break;
  }
  char buf[32];
  sprintf(buf,"%.16Lg",num1);
  SetDlgItemText(hwnd, IDC_EDIT, buf);
}

//the Window Procedure
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {

  switch(msg) {
    case WM_CREATE:
    {

      #define errorIfNull(x) if (NULL==x) MessageBox(hwnd, "Error: WM_CREATE", "Error", MB_OK | MB_ICONERROR);

      #define makeButton(name,id,x,y,w,h) errorIfNull(CreateWindow("BUTTON",name,WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON, x,y,w,h, hwnd,(HMENU)(id),GetModuleHandle(NULL),NULL))

      errorIfNull(CreateWindowEx(
        WS_EX_CLIENTEDGE,
        "EDIT",
        "0",        // default text
        WS_CHILD | WS_VISIBLE | ES_AUTOHSCROLL | ES_RIGHT | ES_READONLY, // Disabled, right align
        10,         // x position
        10,         // y position
        206,        // width
        25,         // height
        hwnd,       // Parent window
        (HMENU)IDC_EDIT,  // menu handle = ID
        GetModuleHandle(NULL), NULL));

      makeButton(".",99, 94,178,36,36);
      makeButton("0",100,10,178,78,36);
      makeButton("1",101,10,136,36,36);
      makeButton("2",102,52,136,36,36);
      makeButton("3",103,94,136,36,36);
      makeButton("4",104,10,94,36,36);
      makeButton("5",105,52,94,36,36);
      makeButton("6",106,94,94,36,36);
      makeButton("7",107,10,52,36,36);
      makeButton("8",108,52,52,36,36);
      makeButton("9",109,94,52,36,36);

      makeButton("+",110,136,178,36,36);
      makeButton("-",111,136,136,36,36);
      makeButton("*",112,136,94,36,36);
      makeButton("/",113,136,52,36,36);

      makeButton("=",114,178,136,36,78);
      makeButton("C",115,178,94,36,36);

    }
    break;
    case WM_KEYUP:
      if (wParam==VK_SHIFT) shift=0;
    break;

    case  WM_KEYDOWN :
      if (shift) {
        if (wParam==0x38) wParam=112; // shift 8 = multiply
        else if (wParam==0xbb) wParam=110;
        else break;
      } else {
        if (wParam>=0x30 && wParam <=0x39) {
          wParam+= 100-0x30;
        } else if (wParam>=0x60 && wParam <=0x69) { //numpad
          wParam+= 100-0x60;
        } else switch (wParam){
          case VK_SHIFT: shift=1; return 0;
          case VK_MULTIPLY: wParam =112; break;
          case VK_SUBTRACT: case 0xbd: wParam =111; break;
          case VK_ADD: wParam =110; break;
          case VK_DIVIDE: case 0xbf: wParam =113; break;
          case VK_DECIMAL: case 0xbe: wParam =99; break;
          case VK_RETURN: case 0xbb: wParam = 114; break;
          case 0x2e: wParam = 115; break;

          default:
            return 0;
        }
      }

    // break; Fall through
    case WM_COMMAND:
    {
      switch (wParam) {
        case 99:
          if (typing==2) break;

        case 109:case 108:case 107:case 106:case 105:case 104:case 103:case 102:case 101:case 100:
        {
          char out[32];
          int len = GetWindowTextLength(GetDlgItem(hwnd, IDC_EDIT));

          if (typing) {
            GetDlgItemText(hwnd, IDC_EDIT, out, len+1);
          } else {
            typing=1;
            len=0;
          }
          if (len<=16) {
            if (wParam==99) {
              if (len==0) out[len++]='0';
              out[len]='.';
              typing=2;
            } else out[len] = '0'+(wParam-100);
            out[len+1] =0;
            SetDlgItemText(hwnd, IDC_EDIT, out);
          }

          if (final) op=0;
          final=0;

        }
        break;
        case 111:
        case 110:
        case 112:
        case 113:
          if (typing && op) {
            num2 = getScreen(hwnd);
            equate(hwnd);
          } else num1 = getScreen(hwnd);
          typing=0;
          op=wParam;
          final=0;

        break;
        case 114: // =
          if (!op) break;
          if (typing)
            num2 = getScreen(hwnd);
          typing=0;

          equate(hwnd);
          final=1;

        break;
        case 115: // Clear
          num1=0.0;
          num2=0.0;
          op=0;
          final=0;
          typing=0;
          SetDlgItemText(hwnd, IDC_EDIT, "0");
        break;
      }
    }
    break;
    case WM_CLOSE:
      DestroyWindow(hwnd);
    break;
    case WM_DESTROY:
      PostQuitMessage(0);
    break;
    default:
      return DefWindowProc(hwnd, msg, wParam, lParam);
  }
  return 0;
}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
  WNDCLASSEX wc;
  HWND hwnd;
  MSG Msg;

  wc.cbSize    = sizeof(WNDCLASSEX);
  wc.style     = 0;
  wc.lpfnWndProc   = WndProc;
  wc.cbClsExtra  = 0;
  wc.cbWndExtra  = 0;
  wc.hInstance   = hInstance;
  wc.hIcon     = LoadIcon(NULL, IDI_APPLICATION);
  wc.hCursor     = LoadCursor(NULL, IDC_ARROW);
  wc.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);
  wc.lpszMenuName  = NULL;
  wc.lpszClassName = windowClassName;
  wc.hIconSm     = LoadIcon(NULL, IDI_APPLICATION);

  if(!RegisterClassEx(&wc)) {
    MessageBox(NULL, "Window Registration Failed!", "Error!", MB_ICONEXCLAMATION | MB_OK);
    return 0;
  }

  hwnd = CreateWindowEx(
    0,
    windowClassName,
    "Amazing calculator",
    WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX,
    CW_USEDEFAULT, CW_USEDEFAULT, 231, 250,
    NULL, NULL, hInstance, NULL);

  if(hwnd == NULL) {
    MessageBox(NULL, "Window Creation Failed!", "Error!", MB_ICONEXCLAMATION | MB_OK);
    return 0;
  }

  ShowWindow(hwnd, nCmdShow);
  UpdateWindow(hwnd);

  while(GetMessage(&Msg, NULL, 0, 0) > 0) {
    if (Msg.message == WM_KEYDOWN || Msg.message == WM_KEYUP) Msg.hwnd = hwnd;
    TranslateMessage(&Msg);
    DispatchMessage(&Msg);
  }
  return Msg.wParam;
}