How to write a text editor in Win32 API with file save and file open dialogs

102 views Asked by At

I'm currently working on a text editor with the C++ windows.h built-in GUI libraries.

Every time I try to save the file, or open a file and display it to the window, it returns an error, or the open file dialog fails. The error is Exception: Segmentation Fault, and it always occurs on the line of GetOpenFileName(); Also, if it does work, FATAL ERROR: Couldn't display file and FATAL ERROR: Couldn't write file always happen. I am using g++ from MINGW MSYS, so maybe my compiler is bad?

Also, I've seen people use L"your text", but when I do it returns an error and doesn't compile, so I resorted to _T("your text"). This is because UNICODE was not enabled.

On a side note, my VS Code Intellisense is telling me that I have errors where there are none, and the program compiles correctly (the errors weren't in the place where I open/save the files).

P.S. I haven't really found any documentation on what I'm trying to do.

Here is my code as of now:

#include <windows.h>
#include <commctrl.h>
#include <shellapi.h>
#include <tchar.h>

#define ID_BUTTON_1 1
#define ID_BUTTON_2 2
#define ID_BUTTON_3 3
#define ID_BUTTON_4 4
#define ID_BUTTON_5 5
#define ID_BUTTON_6 6
#define ID_BUTTON_7 7
#define ID_BUTTON_8 8
#define ID_BUTTON_9 9
#define ID_FILE_NEW 1000
#define ID_FILE_OPEN 1010
#define ID_FILE_SAVE 1020
#define ID_FILE_QUIT 1030


//Global Variables

//Main window class name
static TCHAR szWindowClass[] = _T("Desktop Application");

//Main window title bar text
static TCHAR szTitle[] = _T("C++ GUI Application");

//Instance handle for Win32 API calls
HINSTANCE hInst;
HWND hWnd;
HWND hWndFileText;
int hWndWidth = 600;
int hWndHeight = 400;

//Forward declarations of functions included in this code
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);

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

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

    if (!RegisterClassEx(&wcex)) {
        MessageBox(NULL,
        _T("Call to RegisterClassEx Failed!"),
        _T("Error"), 
        MB_ICONERROR);
        
        return 1;
    }

    //Store instance handle in our global variable
    hInst = hInstance;

    HMENU hFileMenu = CreateMenu();
    HMENU hMenuBar = CreateMenu();
    AppendMenu(hFileMenu, MF_STRING, ID_FILE_NEW, _T("New"));
    AppendMenu(hFileMenu, MF_STRING, ID_FILE_OPEN, _T("Open"));
    AppendMenu(hFileMenu, MF_STRING, ID_FILE_SAVE, _T("Save"));
    AppendMenu(hFileMenu, MF_STRING, ID_FILE_QUIT, _T("Quit"));
    AppendMenu(hMenuBar, MF_POPUP, (UINT_PTR)hFileMenu, _T("File"));

    hWnd = CreateWindowEx(
        WS_EX_OVERLAPPEDWINDOW, //Extended window style
        szWindowClass,          //Name of application
        szTitle,                //Title bar text
        WS_OVERLAPPEDWINDOW,    //window characteristics
        CW_USEDEFAULT,          //Default Position x
        CW_USEDEFAULT,          //Default Position y
        hWndWidth,              //Window size (width)
        hWndHeight,             //Window size (height)
        NULL,                   //Window parent
        hMenuBar,               //Menu bar
        hInstance,              //first parameter from WinMain
        NULL
    );

    if (!hWnd) {
        MessageBox(NULL,
        _T("Call to CreateWindowEx Failed!"),
        _T("Error"),
        MB_ICONERROR);

        return 1;
    }

    HWND hWndFileText = CreateWindow(
        _T("EDIT"),  
        _T("File text goes here"), 
        WS_VISIBLE | WS_CHILD | ES_LEFT | WS_VSCROLL | ES_AUTOVSCROLL | ES_MULTILINE, 
        5, 30, hWndWidth - 5, hWndHeight - 30, 
        hWnd, NULL, hInst, NULL);
    
    if (!hWndFileText) {
        MessageBox(hWnd, 
        _T("Call to create texbox failed!"),
        _T("Error"), 
        MB_ICONERROR);
        
        return 1;
    }

    ShowWindow(hWnd, nCmdShow); //Value returned from CreateWindowEx, 
                                //fourth WinMain parameter
    UpdateWindow(hWnd);

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

    return msg.wParam;
}

//WndProc (window process)
//
//Processes messages for the main window
//
//WM_PAINT - paints the main window
//WM_DESTROY - posts a quit message
char fileData[100];

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) {

    PAINTSTRUCT ps;
    HDC hdc; //handle to device context (hdc)
    TCHAR greeting[] = _T("Hello, world!"); //Text for window
    

    switch (message) {
    case WM_PAINT:
        hdc = BeginPaint(hWnd, &ps);
        
        //This is where stuff is put in the window
        //like text, buttons, etc.
        //For now, I'm putting a simple hello, world
        //at the top left corner of the window
        TextOut(hdc, 5, 5, greeting, _tcslen(greeting));

        EndPaint(hWnd, &ps); //releases device context and ends paint request
        break;
    case WM_DESTROY:
        PostQuitMessage(0);
        break;
    case WM_COMMAND: 
        switch (LOWORD(wParam)) {
        case ID_FILE_QUIT:
            DestroyWindow(hWnd);
            break;
        case ID_FILE_OPEN:
        {
            LPDWORD bytesRead = 0;
            OPENFILENAME fileName; //Dialog box structure
            HANDLE fileHandle;
            char szfileName[260];  //Max length of file name
            char* fileReadBuffer; //Place to put text read from file
            DWORD fileSize; 
            ZeroMemory(&fileName, sizeof(fileName));
            fileName.lStructSize = sizeof(fileName);
            fileName.hwndOwner = hWnd;
            fileName.lpstrFile = szfileName; 
            fileName.nMaxFile = sizeof(szfileName);
            fileName.lpstrFile[0] = '\0';
            fileName.lpstrFilter = _T("Text (.txt)\0*.TXT\0");
            fileName.nFilterIndex = 1;
            fileName.lpstrFileTitle = NULL;
            fileName.nMaxFileTitle = 0;
            fileName.lpstrInitialDir = NULL;
            fileName.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST;
            if (GetOpenFileName(&fileName)) {
                fileHandle = CreateFile(fileName.lpstrFile,
                                        GENERIC_READ,
                                        0,
                                        NULL,
                                        OPEN_EXISTING,
                                        FILE_ATTRIBUTE_NORMAL, 
                                        NULL);
            }
            else {
                MessageBox(hWnd, _T("Couldn't Open File!"), _T("ERROR"), MB_ICONERROR);
                break;
            } 

            fileSize = GetFileSize(fileHandle, NULL);
            fileReadBuffer = (char*)GlobalAlloc(GPTR, (fileSize+1));

            if (!ReadFile(fileHandle, (LPVOID)fileReadBuffer, fileSize, bytesRead, NULL)) {
                MessageBox(hWnd, _T("Fatal Error: Couldn't initialize file read"), _T("ERROR"), MB_ICONERROR);
            }
            else {
                MessageBox(hWnd, _T("File Read"), _T("Success"), MB_OK);
            }
            SetWindowText(hWndFileText, _T("Attempting to display file"));
            fileReadBuffer[fileSize] = '\0';
            if (!SetWindowText(hWndFileText, fileReadBuffer)) { //This never works
                MessageBox(hWnd, _T("Fatal Error: Couldn't display file"), _T("ERROR"), MB_ICONERROR);
            }
            else {
                MessageBox(hWnd, _T("File displayed"), _T("Success"), MB_OK);
            }

            CloseHandle(fileHandle);
            break;
        }
        case ID_FILE_SAVE:
        {
            LPDWORD bytesWritten;
            OPENFILENAME fileName; //Dialog box structure
            HANDLE fileHandle;
            char szfileName[260];  //Max length of file name
            char fileWriteBuffer[100]; //Place to put text from textbox
            fileName.lStructSize = sizeof(fileName);
            fileName.hwndOwner = hWnd;
            fileName.lpstrFile = szfileName; 
            fileName.nMaxFile = sizeof(szfileName);
            fileName.lpstrFile[0] = '\0';
            fileName.lpstrFilter = _T("Text (.txt)\0*.TXT\0");
            fileName.nFilterIndex = 1;
            fileName.lpstrFileTitle = NULL;
            fileName.nMaxFileTitle = 0;
            fileName.lpstrInitialDir = NULL;
            fileName.Flags = OFN_EXPLORER;
            if (GetOpenFileName(&fileName)) { //This is where error occurs
                fileHandle = CreateFile(fileName.lpstrFile,
                                        GENERIC_WRITE,
                                        0,
                                        NULL,
                                        OPEN_EXISTING,
                                        FILE_ATTRIBUTE_NORMAL, 
                                        NULL);
            }
            else {
                MessageBox(hWnd, _T("Couldn't Open File For Saving!"), _T("ERROR"), MB_ICONERROR);
                break;
            }

            GetWindowText(hWndFileText, fileWriteBuffer, 99);

            if(!WriteFile(fileHandle, fileWriteBuffer, sizeof(fileWriteBuffer), bytesWritten, NULL)){ //This never works
                MessageBox(hWnd, _T("FATAL ERROR: Couln't write to file!"), _T("ERROR"), MB_ICONERROR);
            }
            else {
                MessageBox(hWnd, _T("File saved successfully!"), _T("Success"), MB_OK);
            }
        }
        default:
            break;
        }
        break;
    case WM_CREATE:
           break;
    default:
        return DefWindowProc(hWnd, message, wParam, lParam);
        break;
    }
    return 0;
}

I know I don't need the 2nd and 3rd includes, I just haven't removed them. The buttons 1-9 are for later, I haven't gotten to that yet, but they shouldn't be causing the issue.

I've looked at a bunch of documentation on how to use the CreateFile(), ReadFile(), and WriteFile() functions, and this is what I've gotten from that so far, but none of it seemed to work.

Also, I don't know how to initialize a save file dialog, so it's an open file dialog for both (but for some reason the window title for the open file dialog in the save function is A[*62]< or some other nonsense (garbage?)).

This is the entire source code, so if you want to try it, you should be able to compile it as-is.

1

There are 1 answers

7
Adrian McCarthy On

Possibly not the only problem, but you're one null terminator short here:

fileName.lpstrFilter = _T("Text (.txt)\0*.TXT\0");

Admittedly, the documentation is a bit unclear here:

A buffer containing pairs of null-terminated filter strings. The last string in the buffer must be terminated by two NULL characters.

If I recall correctly (and based on some old code I just checked), you actually need a pair of null characters (i.e., a pair of empty strings) after the null character that terminates the second string of the last pair. So the buffer should end with three null characters.

The GetOpenFileName and GetSaveFileName dialogs are pretty old and crufty. The Common Item Dialogs present a more up-to-date interface. In some ways, they're harder to use, because you need to know a little about COM programming. Once you're over that hurdle, they're a little harder to use, since there are methods for setting each of the options, so you get a bit more type safety.