Developing Windows Programs using Eclipse C++

Eclipse CDT (C/C++ Development Toolkit) is perfectly fine for developing Windows applications in C++. But you might be asking, why would I want to do that when Visual Studio is so readily available?

I started down this path because I spend most of my time with the Eclipse IDE (Integrated Development Environment) running on my machine and often with multiple workspaces on both Windows and Linux, but the C++ code I develop isn’t intended to run on Windows. But ever so often I will develop a simple tool with a Graphical User Interface (GUI). Since I’m familiar with Eclipse, I would prefer not to have to switch to another IDE just for a simple tool. But one thing you notice when you look at an IDE like Visual Studio is that it generates a significant amount of code for you and it makes use of various libraries, but how much of this do you really need?

I assume you’ve built with the Eclipse CDT. If not, follow this guide first:
https://www.codeproject.com/Articles/14222/C-Development-using-eclipse-IDE-Starters-guide

Also, if you want to skip ahead, the source projects are located in the following locations:

http://www.timothyfish.com/Examples/Windows4Eclipse/HelloMsg.zip

http://www.timothyfish.com/Examples/Windows4Eclipse/ExampleWin.zip

I started down this path because I spend most of my time with the Eclipse IDE (Integrated Development Environment) running on my machine and often with multiple workspaces on both Windows and Linux, but the C++ code I develop isn’t intended to run on Windows. But ever so often I will develop a simple tool with a Graphical User Interface (GUI). Since I’m familiar with Eclipse, I would prefer not to have to switch to another IDE just for a simple tool. But one thing you notice when you look at an IDE like Visual Studio is that it generates a significant amount of code for you and it makes use of various libraries, but how much of this do you really need?

One of my favorite books when I was younger was Programming Windows 3.1 by Charles Petzold. He’s updated it as the Windows OS has moved on, but that book is still in my library. The thing that surprised me when I first read it was that just to display a simple window on the screen required two pages of C code. In the fifth edition of Programming Windows he has that down to a much simpler program:

#include <windows.h>
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR szCmdLine, int iCmdShow)
{

    MessageBox (NULL, TEXT ("Hello, Windows 98!"), 
                TEXT ("HelloMsg"), 0);
    return 0;
}

While that kind of program will let you test that you are able to compile and run a program from your IDE, it only hints at the more complicated nature of the Windows API (Application Programming Interface). For me, I not only wanted to know that I could write a program in Eclipse that would display a message box, but that I could use Eclipse for more useful programs as well. But I also wanted it to follow a more C++ oriented programming style.

But before we move farther, let’s compile and run a version of his C program from Eclipse. On my machine I have installed the following:

In the Eclipse C++ Perspective, create a new C++ Project.

Select C++ Managed Build and click Next.

Select Empty Project and give it a name. Click Next.

At this next window we could set some specifics, but we’ll ignore it for now. Click Finish.

Right click on the project name and create a new file.

Enter the following code and click the build button:


#include <windows.h>
int WINAPI WinMain (HINSTANCE hInstance,
                    HINSTANCE hPrevInstance,
                    PSTR szCmdLine,
                    int iCmdShow)
{
    MessageBox (NULL, TEXT ("Hello, World!"),
                TEXT ("HelloMsg"), 0);
    return 0;
}

If everything is installed correctly then this program should build, and you should be able to run it by clicking the Run Button. This will display something like the following:

But as I said before, this isn’t particularly useful other than to verify that your development environment is setup properly. What we would like to do is have a program that displays a window in which we specify what controls are used and where they are located on the page. We may even want to open another window.

Create another new project following the same steps as before. Call the project “ExampleWin” and add a file called “start.cpp” to the project.

Going back to Charles Petzold and Programming Windows, because he was writing in C, his WinMain consisted of a page of code that created a window and then went into a message loop. He also had one callback function WndProc that handled the WM_PAINT message by drawing text on the screen. It also handled WM_DESTROY by posting 0 to the message queue so that the loop in WinMain would terminate. We don’t see the code for it, but the MessageBox function is doing all of this in its code. So, there’s really no reason why we have to have such a long WinMain and since we’re working with C++ instead of C, we can create a class that handles it rather than just a function. We still have to put all the details somewhere, but we can keep WinMain simple. This is very useful if you are using a test environment that needs to replace WinMain with its own. It’s much easier to tell it how to replace one call than it is to tell it how to replace a page of code.

The code for start.cpp should look like this:

// *
// * StartApp.h
// *
// *  Created on: Sep 1, 2018
// *      Author: Timothy Fish
// *      Website: http://www.timothyfish.com
// *

#ifndef STARTAPP_H_
#define STARTAPP_H_
#include <windows.h>

class StartApp {
public:
    static StartApp& getInstance();
    virtual ~StartApp();

    int begin(HINSTANCE hInstance,
              HINSTANCE hPrevInstance,
              PSTR    lpCmdLine,
              int       nCmdShow);

    HINSTANCE getHInstance(){ return hInst; }
    void setHInstance(HINSTANCE h){ hInst = h; }
private:
    static StartApp* instance;
    bool running;

    HINSTANCE hInst;
    const char* className = "StartApp";
    const char* appName   = "Example Windows App";
    StartApp();
};

#endif /* STARTAPP_H_ */

Click the build button and you should expect to see the following error:

..\start.cpp:2:10: fatal error: StartApp.h: No such file or directory
 #include <StartApp.h>
          ^~~~~~~~~~~~

This is telling us that it doesn’t know where StartApp.h is. You could put it into quotes, but since some coding standards tell us not to do that sort of thing, let’s add the source directory to the include directories. To do that, Right click on the ExampleWin project name and select properties. Select C/C++ General and go to the Includes tab. Add ${ProjDirPath} to the GNU C++ Language all configurations. (You may add it to the other languages if you like, but we’re only using C++ here. Apply and close.

Now when you build you should get the following errors:

start.o: In function `WinMain@16':
C:\Users\timot\Eclipse4Windows\ExampleWin\Debug/../start.cpp:9: undefined reference to `StartApp::getInstance()'
C:\Users\timot\Eclipse4Windows\ExampleWin\Debug/../start.cpp:9: undefined reference to `StartApp::begin(HINSTANCE__*, HINSTANCE__*, char*, int)'
collect2.exe: error: ld returned 1 exit status

These are both linker errors. They exist because we haven’t added the functions to the StartApp.cpp file yet. To do this, create another new file StartApp.cpp and add the following code:

// *
// * StartApp.cpp
// *
// *  Created on: Sep 1, 2018
// *      Author: Timothy Fish
// *      Website: http://www.timothyfish.com
// *

#include <StartApp.h>

#include <iostream>
#include <windows.h>
#include <windowsx.h>

StartApp* StartApp::instance = 0;

StartApp& StartApp::getInstance(){

    if(instance == 0){
        instance = new StartApp();
    }

    return *instance;
}

StartApp::StartApp(): running(0), hInst(0) {
}

StartApp::~StartApp() {
}

int StartApp::begin(HINSTANCE hInstance,
                    HINSTANCE hPrevInstance,
                    PSTR    lpCmdLine,
                    int       nCmdShow)
{
    return 0;
}

Another thing that I would recommend doing at this point is add static linking of gcc by adding the following to you link command: -static -static-libgcc -static-libgcc. This will save you having to find the dll to run outside of Eclipse.

At this point, you should be able to build and run without errors. With multiple projects in Eclipse you may need to specify which one to run by selecting the project and then selecting Run As|Local C/C++ Application.

If you run it at this point, it will appear that it did nothing, since we haven’t yet created a window and we don’t have a message loop. If you like, you can run it using the debugger (click the bug button) and step through the code to verify that it is actually running something. Otherwise the code will run and exit without showing you anything. To correct this, we need to modify the begin function.

int StartApp::begin(HINSTANCE hInstance,
                    HINSTANCE hPrevInstance,
                    PSTR    lpCmdLine,
                    int       nCmdShow)
{
    UNREFERENCED_PARAMETER(hPrevInstance);
    UNREFERENCED_PARAMETER(lpCmdLine);

    if(running) return 0; // only run one copy

    MyRegisterClass(hInstance);

    if (!InitInstance (hInstance, nCmdShow))
    {
        std::cout<<"Start - GetLastError - InitInstance: "<<GetLastError()<<std::endl;
        return FALSE;
    }

    MSG msg;

    while (GetMessage(&msg, nullptr, 0, 0))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    return (int) msg.wParam;
}

We are introducing some new functions here, so you will also need to modify StartApp.h to add the following as private member functions to the class:

    ATOM MyRegisterClass(HINSTANCE hInstance);
    BOOL InitInstance(HINSTANCE hInstance, int nCmdShow);
    static LRESULT CALLBACK WndProc(HWND hwnd, 
                                     UINT message, 
                                     WPARAM wParam, 
                                     LPARAM lParam);

Building at this point should result in the following errors:

StartApp.o: In function `_::ZN8StartApp5beginEP11HINSTANCE(char *, int) static':
C:\Users\timot\Eclipse4Windows\ExampleWin\Debug/../StartApp.cpp:42: undefined reference to `StartApp::MyRegisterClass(HINSTANCE__*)'
C:\Users\timot\Eclipse4Windows\ExampleWin\Debug/../StartApp.cpp:44: undefined reference to `StartApp::InitInstance(HINSTANCE__*, int)'
collect2.exe: error: ld returned 1 exit status

We will implement MyRegisterClass and InitInstance in a moment, but first let’s look at begin.

    UNREFERENCED_PARAMETER(hPrevInstance);
    UNREFERENCED_PARAMETER(lpCmdLine);

These like trick the compiler into thinking that these unused variables are actually used, so we don’t see warnings.

if(running) return 0; // only run one copy

This line prevents begin from being called twice.

    MSG msg;

    while (GetMessage(&msg, nullptr, 0, 0))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    return (int) msg.wParam;

This code is the standard message loop for a Windows program. We retrieve the message from the queue and pass it back to Windows. We keep doing this until we GetMessage returns 0. If we need to do something special within this loop we can, but we have no need to at this point.

Add the following code to StartApp.cpp:

ATOM StartApp::MyRegisterClass(HINSTANCE hInstance)
{
    WNDCLASSEX windowClass; //window class

    windowClass.cbSize              = sizeof(WNDCLASSEX);
    windowClass.style               = CS_HREDRAW | CS_VREDRAW;
    windowClass.lpfnWndProc         = WndProc;
    windowClass.cbClsExtra          = 0;
    windowClass.cbWndExtra          = 0;
    windowClass.hInstance           = hInstance;
    windowClass.hIcon               = LoadIcon(0, IDI_APPLICATION);
    windowClass.hCursor             = LoadCursor(0, IDC_ARROW);
    windowClass.hbrBackground       = GetSysColorBrush(COLOR_WINDOW);
    windowClass.lpszMenuName        = 0;
    windowClass.lpszClassName       = className;
    windowClass.hIconSm             = LoadIcon(0, IDI_WINLOGO);
    return RegisterClassEx(&windowClass);
}

BOOL StartApp::InitInstance(HINSTANCE hInstance, int nCmdShow)
{
    hInst = hInstance; // Store instance handle in our global variable
    RECT    windowRect;

    int width = 300;
    int height = 200;

    windowRect.left =(long)0;       //set left value to 0
    windowRect.right =(long)width;  //set right value to requested width
    windowRect.top =(long)0;        //set top value to 0
    windowRect.bottom =(long)height;//set bottom value to requested height

    AdjustWindowRectEx(&windowRect, WS_OVERLAPPEDWINDOW, FALSE, WS_EX_APPWINDOW | WS_EX_WINDOWEDGE);

    HWND hwnd = CreateWindowEx(0, className,  //class name
            appName,       //app name
            WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN | WS_CLIPSIBLINGS,
            400, 300,                         //x and y coords
            windowRect.right - windowRect.left,
            windowRect.bottom - windowRect.top,//width, height
            0,                 //handle to parent
            0,                 //handle to menu
            hInstance,    //application instance
            0);                //no xtra params

    if (!hwnd)
    {
        std::cout<<"Start - GetLastError - CreateWindowEx: "<<GetLastError()<<std::endl;
        return FALSE;
    }

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

    return TRUE;
}

LRESULT CALLBACK StartApp::WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    static HINSTANCE hInstance;
    UNREFERENCED_PARAMETER(hInstance);

    switch (message)
    {
    case WM_CREATE:
        hInstance = ((LPCREATESTRUCT)lParam)->hInstance;
        break;
    case WM_PAINT:
        {
            PAINTSTRUCT ps;
            HDC hdc = BeginPaint(hwnd, &ps);
            UNREFERENCED_PARAMETER(hdc);
            // TODO: Add any drawing code that uses hdc here...

            EndPaint(hwnd, &ps);
        }
        break;
    case WM_DESTROY:
        PostQuitMessage(0);
        break;
    default:
        return DefWindowProc(hwnd, message, wParam, lParam);
    }
    return 0;
}



Now, when you Build and Run you should see a small window appear on the screen.

While this is a small success, to be useful we need it to either provide information to the user or to request information from the user. As a proof of concept, lets display an edit box that accepts a character and displays the ASCII representation of it when the user clicks the Submit button. We need to add an edit box, a static, and a button to the window. We do this by modifying the InitInstance function. Add the following code to InitInstance just before ShowWindow:

    CreateWindow(TEXT("edit"), TEXT("A"),
                 WS_VISIBLE | WS_CHILD | SS_CENTER|WS_BORDER,
                 width/2-10, 20, 20, 25,
                 hwnd, (HMENU) IDM_CHAR, NULL, NULL);

    CreateWindow(TEXT("static"), TEXT("65"),
                 WS_VISIBLE | WS_CHILD | SS_CENTER,
                 width/2-10, 45, 20, 25,
                 hwnd, (HMENU) IDM_INT, NULL, NULL);

    CreateWindow(TEXT("button"), TEXT("Submit"),
            WS_VISIBLE | WS_CHILD ,
            200, 140, 80, 25,
            hwnd, (HMENU) IDM_SUBMIT, NULL, NULL);

Add the following to the private section of the StartApp class:

    enum ControlIds{
        IDM_CHAR = 100,
        IDM_INT,
        IDM_SUBMIT
    };

When you run this code you should see something like the following:

But while it looks like we would expect, nothing happens when we click the Submit button. To correct that, we need to add some code that handles the button clicks. Add the following code to the switch statement in WndProc:

    case WM_COMMAND:
    {
        int wmId = LOWORD(wParam);
        switch (wmId)
        {
        case IDM_SUBMIT:
        {
            char buf[5];

            GetDlgItemText( hwnd, IDM_CHAR, buf,2 );

            int asciiVal = int(buf[0]);
            itoa(asciiVal, buf, 10);

            SetDlgItemText( hwnd, IDM_INT, buf );
        }
            break;
        default:
            return DefWindowProc(hwnd, message, wParam, lParam);
        }
    }
        break;

Each time a button is clicked the WM_COMMAND message is sent with an Id in LOWORD(wParam). In the internal switch we check to see if that Id is IDM_SUBMIT, which is the id we gave the Submit button when we created it. When it is, we get the text from the Edit Box, which has Id IDM_CHAR, place it in buf, convert the first character to an int, then we convert that int to ASCII and place the value in buf. We set the text in the static with Id IDM_INT to the value in buf.

If you are able to do this much within the Eclipse IDE, you can do the rest by researching the specifics of the Windows API. You can download the Eclipse projects, which include the source code in this example plus some more code that implements a menu that calls an About Dialog that is implemented as a separate class.

You will notice that some of what we are doing here could be done using a resource file. I stayed away from resource files in this example because having additional files makes it harder to follow where things are coming from. To use a resource file you will need to call windres on the .rc file to generate a .o while that you can link with your program.

The source projects are located in the following locations:

http://www.timothyfish.com/Examples/Windows4Eclipse/HelloMsg.zip

http://www.timothyfish.com/Examples/Windows4Eclipse/ExampleWin.zip

Comments

Popular posts from this blog

Review: WestBow Press

Is Tate Publishing a Scam?

Review: Raider Publishing