Are we really alone in this vast universe? BECKLEY WEATHER  


                                                                               


Multiplication Grid Generator

After seeing one of my daughters come home with a simple grid for multiplication I thought that this wasn't truly teaching multiplication as much as it was showing a sequence.  I mean if the first line is simply 1 x 1, 1 x 2, 1 x 3 and so forth then the answers were just 1,2,3, etc..
So I set out to create a program that would generate a grid that was scrambled so the student would absolutely have to think about each answer.  The following is what I came up with.

/********************************************************************************
 *                                                                                                             *
 * C++ PROGRAM                                                                                  *
 *                                                                                                             *
 * Author    : Joseph S. Hale                                                                   *
 * Contact   : jshale.programmer@gmail.com                                         *
 *                                                                                                             *
 * Purpose   : Generate multiplication practice grids in RTF format with*
 * visible cell borders, supporting sequential or random headers.          *
 *                                                                                                             *
 *******************************************************************************/

#include <windows.h> // Include standard Windows API functions
#include <commctrl.h> // Include common controls library for GUI elements

#include <iostream>  // For standard I/O (not heavily used in GUI part)
#include <fstream>   // For file stream operations (writing RTF file)
#include <iomanip>   // For I/O manipulators (used with time formatting)
#include <vector>    // For using std::vector to store headers
#include <random>    // For random number generation and shuffling
#include <ctime>     // For time functions (used to generate unique ID)
#include <algorithm> // For std::shuffle function
#include <sstream>   // For string stream operations
#include <string>    // For using std::string

// Define constants for unique control IDs in the Win32 GUI
constexpr int IDC_BTN_GENERATE = 1001;
constexpr int IDC_EDT_TEACHER = 1002;
constexpr int IDC_EDT_START = 1003;
constexpr int IDC_EDT_END = 1004;
constexpr int IDC_CHK_ORDERED = 1005;

// Function prototypes
void writeRTFHeader(std::ofstream& out);
void writeRTFFooter(std::ofstream& out);
void writeMultiplicationTable(std::ofstream& out, const std::vector<int>& rowHeaders,
    const std::vector<int>& colHeaders, bool blank, int cellWidth, int headerCellWidth,
    const std::string& uniqueID);
std::vector<int> generateShuffledHeaders(int size, int minValue, int maxValue, bool ordered);
std::string generateUniqueID();
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

// Helper function to convert std::wstring (used in Windows GUI) to std::string
static std::string narrow_from_wstring(const std::wstring& ws) {
    return std::string(ws.begin(), ws.end());
}

// Function to write the standard RTF file header
void writeRTFHeader(std::ofstream& out) {
    out << "{\\rtf1\\ansi\\deff0\n"; // Standard RTF header, ANSI character set
    out << "{\\fonttbl{\\f0 Arial;}}\n"; // Define font 0 as Arial

    // --- FORCE LANDSCAPE ORIENTATION ---
    // \landscape must be followed by a section definition (\sectd)
    // and custom page width/height swapped. (Dimensions are in twips: 1/20th of a point)
    out << "\\landscape\\sectd\\pgwsxn16837\\pghsxn11905\\marglsxn720\\margrsxn720\\margtsxn720\\margbsxn720\n";

    out << "\\f0\\fs20\n"; // Set default font (f0: Arial) and font size (fs20: 10pt)
}

// Function to write the standard RTF file footer
void writeRTFFooter(std::ofstream& out) {
    out << "\\par}\n"; // End of the last paragraph and closing RTF bracket
}

// Function to generate a unique ID based on the current timestamp
std::string generateUniqueID() {
    std::ostringstream oss; // Output string stream
    std::time_t t = std::time(nullptr); // Get current time
    tm tmStruct; // Time structure

// Use platform-specific safe localtime functions
#if defined(_MSC_VER)
    localtime_s(&tmStruct, &t); // Microsoft Visual C++
#else
    localtime_r(&t, &tmStruct); // POSIX
#endif
    // Format time into a string (YYYYMMDDHHMMSS)
    oss << std::put_time(&tmStruct, "%Y%m%d%H%M%S");
    return oss.str(); // Return the unique ID string
}

// Function to generate the list of numbers for row/column headers
std::vector<int> generateShuffledHeaders(int size, int minValue, int maxValue, bool ordered) {
    std::vector<int> headers; // Vector to hold the numbers
    // Populate the vector sequentially from minValue to maxValue
    for (int i = minValue; i <= maxValue; ++i) headers.push_back(i);
   
    // If not ordered, shuffle the vector
    if (!ordered) {
        std::random_device rd; // Obtain a random seed from hardware
        std::mt19937 g(rd()); // Standard mersenne_twister_engine seeded with rd()
        std::shuffle(headers.begin(), headers.end(), g); // Shuffle the headers
    }
    return headers; // Return the header list
}

// Function to write the multiplication table into the RTF file
void writeMultiplicationTable(std::ofstream& out, const std::vector<int>& rowHeaders,
    const std::vector<int>& colHeaders, bool blank, int cellWidth, int headerCellWidth,
    const std::string& uniqueID) {

    int size = (int)rowHeaders.size(); // Determine grid size (N x N)
    // Write table title, centered and bold
    out << "{\\pard\\qc\\b Multiplication Grid " << uniqueID << "\\par}\n";

    // Start position of the table relative to the page margin (in twips)
    const int tableLeft = 720;
   
    // Top header row definition
    out << "{\\trowd\\trgaph108\\trleft" << tableLeft << "\n"; // Start row definition, set table left indent
   
    // Calculate the right boundary of the first cell (corner cell)
    int cellPos = tableLeft + headerCellWidth;
    // Define borders for the first cell
    out << "\\clbrdrt\\brdrs\\brdrw10\\clbrdrl\\brdrs\\brdrw10"
        << "\\clbrdrb\\brdrs\\brdrw10\\clbrdrr\\brdrs\\brdrw10"
        << "\\cellx" << cellPos; // Set right boundary for the first cell
   
    // Remaining cells (column headers)
    for (int j = 0; j < size; ++j) {
        // Define borders for the current cell
        out << "\\clbrdrt\\brdrs\\brdrw10\\clbrdrl\\brdrs\\brdrw10"
            << "\\clbrdrb\\brdrs\\brdrw10\\clbrdrr\\brdrs\\brdrw10";
        cellPos += cellWidth; // Advance the cell position by fixed width
        out << "\\cellx" << cellPos; // Set right boundary for the cell
    }
    out << "\n"; // End of table row definition

    // Top header row content (centered for headers)
    out << "\\qc\\intbl "; // Center content, start first cell
    // If it's the blank sheet, put 'X' in the corner cell
    if (blank) {
        out << "{\\b X}";
    }
    out << "\\cell "; // End corner cell
   
    // Write column header numbers (bold and centered)
    for (int j = 0; j < size; ++j) {
        out << "\\qc\\intbl {\\b " << colHeaders[j] << "}\\cell ";
    }
    out << "\\row}\n"; // End of row
   
    // Data rows (including row headers)
    for (int i = 0; i < size; ++i) {
        // Redefine row properties for each row (necessary for RTF)
        out << "{\\trowd\\trgaph108\\trleft" << tableLeft << "\n";
       
        // Recalculate cell positions for the row definition
        cellPos = tableLeft + headerCellWidth;
        // First cell (row header) definition
        out << "\\clbrdrt\\brdrs\\brdrw10\\clbrdrl\\brdrs\\brdrw10"
            << "\\clbrdrb\\brdrs\\brdrw10\\clbrdrr\\brdrs\\brdrw10"
            << "\\cellx" << cellPos;
           
        // Remaining cells (data cells) definition
        for (int j = 0; j < size; ++j) {
            out << "\\clbrdrt\\brdrs\\brdrw10\\clbrdrl\\brdrs\\brdrw10"
                << "\\clbrdrb\\brdrs\\brdrw10\\clbrdrr\\brdrs\\brdrw10";
            cellPos += cellWidth;
            out << "\\cellx" << cellPos;
        }
        out << "\n"; // End of table row definition

        // Row content
        // Write row header (bold and centered)
        out << "\\qc\\intbl {\\b " << rowHeaders[i] << "}\\cell ";
       
        // Data cells: centered content
        for (int j = 0; j < size; ++j) {
            if (blank) out << "\\qc\\intbl \\cell "; // Blank cell for practice sheet (centered)
            else out << "\\qc\\intbl " << (rowHeaders[i] * colHeaders[j]) << "\\cell "; // Answer cell (centered)
        }
        out << "\\row}\n"; // End of row
    }
}

// -------------------- WinMain & WindowProc --------------------

// Standard Win32 entry point
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE, LPSTR, int nCmdShow) {
    const wchar_t CLASS_NAME[] = L"MultiplicationGridWindowClass"; // Window class name

    WNDCLASSW wc = {}; // Structure to hold window class attributes
    wc.lpfnWndProc = WindowProc; // Pointer to the window procedure function
    wc.hInstance = hInstance; // Handle to the application instance
    wc.lpszClassName = CLASS_NAME; // Pointer to the class name string
    wc.hCursor = LoadCursor(nullptr, IDC_ARROW); // Load the standard cursor

    RegisterClassW(&wc); // Register the window class

    // Create the main application window
    HWND hwnd = CreateWindowExW(
        0, // Optional window styles
        CLASS_NAME, // Window class name
        L"Multiplication Grid Generator", // Window title
        WS_OVERLAPPEDWINDOW & ~WS_MAXIMIZEBOX, // Window style (prevent maximizing)
        CW_USEDEFAULT, CW_USEDEFAULT, 500, 300, // Position and size
        nullptr, // Parent window handle
        nullptr, // Menu handle
        hInstance, // Instance handle
        nullptr // Additional application data
    );

    if (!hwnd) return 0; // Check for window creation failure

    ShowWindow(hwnd, nCmdShow); // Display the window
    UpdateWindow(hwnd); // Send WM_PAINT message

    MSG msg; // Structure to hold message information
    // Message loop: retrieve and dispatch messages until WM_QUIT is received
    while (GetMessageW(&msg, nullptr, 0, 0)) {
        TranslateMessage(&msg); // Translate virtual-key messages into character messages
        DispatchMessageW(&msg); // Dispatch message to the window procedure
    }
    return (int)msg.wParam; // Return the exit code
}

// Window procedure function to handle window messages
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
    // Static handles for persistent GUI controls
    static HWND hTeacher = nullptr, hStart = nullptr, hEnd = nullptr, hButton = nullptr, hCheckOrdered = nullptr;
   
    switch (uMsg) {
    case WM_CREATE: // Message sent when the window is first created
        // Create Static Text (Label) for Teacher Name
        CreateWindowW(L"STATIC", L"Teacher Name:", WS_VISIBLE | WS_CHILD, 20, 20, 110, 20, hwnd, nullptr, nullptr, nullptr);
        // Create Edit Control for Teacher Name
        hTeacher = CreateWindowW(L"EDIT", L"", WS_VISIBLE | WS_CHILD | WS_BORDER | ES_AUTOHSCROLL, 140, 20, 280, 22, hwnd, (HMENU)IDC_EDT_TEACHER, (HINSTANCE)GetWindowLongPtrW(hwnd, GWLP_HINSTANCE), nullptr);

        // Create Static Text (Label) for Start Number
        CreateWindowW(L"STATIC", L"Start Number:", WS_VISIBLE | WS_CHILD, 20, 55, 110, 20, hwnd, nullptr, nullptr, nullptr);
        // Create Edit Control for Start Number (default "1")
        hStart = CreateWindowW(L"EDIT", L"1", WS_VISIBLE | WS_CHILD | WS_BORDER, 140, 55, 100, 22, hwnd, (HMENU)IDC_EDT_START, (HINSTANCE)GetWindowLongPtrW(hwnd, GWLP_HINSTANCE), nullptr);

        // Create Static Text (Label) for End Number
        CreateWindowW(L"STATIC", L"End Number:", WS_VISIBLE | WS_CHILD, 20, 90, 110, 20, hwnd, nullptr, nullptr, nullptr);
        // Create Edit Control for End Number (default "12")
        hEnd = CreateWindowW(L"EDIT", L"12", WS_VISIBLE | WS_CHILD | WS_BORDER, 140, 90, 100, 22, hwnd, (HMENU)IDC_EDT_END, (HINSTANCE)GetWindowLongPtrW(hwnd, GWLP_HINSTANCE), nullptr);

        // Create Checkbox for Sequential Order
        hCheckOrdered = CreateWindowW(L"BUTTON", L"Sequential Order", WS_VISIBLE | WS_CHILD | BS_AUTOCHECKBOX, 260, 90, 150, 22, hwnd, (HMENU)IDC_CHK_ORDERED, (HINSTANCE)GetWindowLongPtrW(hwnd, GWLP_HINSTANCE), nullptr);

        // Create Generate Button (Default Push Button)
        hButton = CreateWindowW(L"BUTTON", L"Generate RTF Grid", WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_DEFPUSHBUTTON, 140, 130, 200, 36, hwnd, (HMENU)IDC_BTN_GENERATE, (HINSTANCE)GetWindowLongPtrW(hwnd, GWLP_HINSTANCE), nullptr);
        return 0; // Handled WM_CREATE

    case WM_COMMAND: // Message sent by controls when an event occurs
        // Check if the source is the Generate button
        if (LOWORD(wParam) == IDC_BTN_GENERATE) {
            // Buffers to hold text from edit controls
            wchar_t teacherBuf[256] = {}, startBuf[32] = {}, endBuf[32] = {};
            // Get text from Teacher Name edit box
            GetWindowTextW(hTeacher, teacherBuf, (int)std::size(teacherBuf));
            // Get text from Start Number edit box
            GetWindowTextW(hStart, startBuf, (int)std::size(startBuf));
            // Get text from End Number edit box
            GetWindowTextW(hEnd, endBuf, (int)std::size(endBuf));

            std::wstring wTeacher(teacherBuf); // Convert to wstring
            std::string teacher = narrow_from_wstring(wTeacher); // Convert to string

            int minVal = 1, maxVal = 12; // Default values
            try {
                // Convert Start/End text to integers
                minVal = std::stoi(std::wstring(startBuf));
                maxVal = std::stoi(std::wstring(endBuf));
            }
            catch (...) {
                // Catch conversion errors
                MessageBoxW(hwnd, L"Start and End must be valid integers.", L"Input Error", MB_OK | MB_ICONERROR);
                return 0;
            }

            // Input validation: check range and order (1 to 15, Start <= End)
            if (minVal < 1 || maxVal < minVal || maxVal > 15) {
                MessageBoxW(hwnd, L"Numbers must be integers between 1 and 15, with Start <= End.", L"Input Error", MB_OK | MB_ICONERROR);
                return 0;
            }

            int gridSize = maxVal - minVal + 1; // Calculate the size of the grid (e.g., 12 for 1-12)
            std::string uniqueID = generateUniqueID(); // Generate a unique ID for the file
            // Check the state of the Sequential Order checkbox
            bool ordered = (SendMessageW(hCheckOrdered, BM_GETCHECK, 0, 0) == BST_CHECKED);

            // Generate row and column headers based on input and order flag
            std::vector<int> rowHeaders = generateShuffledHeaders(gridSize, minVal, maxVal, ordered);
            std::vector<int> colHeaders = generateShuffledHeaders(gridSize, minVal, maxVal, ordered);

            // Sanitize teacher name for use in the filename
            std::string safeTeacher = teacher;
            for (char& c : safeTeacher) if (c == ' ' || c == '/') c = '_';
            // Construct the output filename
            std::string filename = safeTeacher.empty() ?
                "multiplication_grid_" + uniqueID + ".rtf" :
                safeTeacher + "_multiplication_grid_" + uniqueID + ".rtf";

            // Open the output file stream in binary mode
            std::ofstream outfile(filename, std::ios::binary);
            if (!outfile) {
                MessageBoxW(hwnd, L"Error opening file.", L"File Error", MB_OK | MB_ICONERROR);
                return 0;
            }

            // Define fixed cell widths (in twips)
            const int cellWidth = 900;
            const int headerCellWidth = 900;

            // --- Generate Answer Sheet ---
            writeRTFHeader(outfile); // Write RTF opening tags and settings
            if (!teacher.empty()) outfile << "\\pard " << teacher << "\\par\\par\n"; // Write teacher name
            outfile << "{\\b Multiplication Answers}\\par\n"; // Write heading
            // Write table with answers (blank = false)
            writeMultiplicationTable(outfile, rowHeaders, colHeaders, false, cellWidth, headerCellWidth, uniqueID);
            outfile << "\\page\n"; // Insert a page break

            // --- Generate Practice Sheet ---
            if (!teacher.empty()) outfile << "\\pard " << teacher << "\\par\\par\n"; // Write teacher name
            // Write student name line and instructions
            outfile << "\\pard\\tx720 Student Name________________________\\par\\par\n";
            outfile << "\\pard\\tx720 Multiply the number on the left by the number at the top to find the answer.\\par\\par\n";
            outfile << "\\pard\\qc {\\b Student Practice Grid}\\par\\par\n";
            // Write table with blank cells (blank = true)
            writeMultiplicationTable(outfile, rowHeaders, colHeaders, true, cellWidth, headerCellWidth, uniqueID);

            writeRTFFooter(outfile); // Write RTF closing tags
            outfile.close(); // Close the file stream

            // Display success message
            std::wstring wmsg = L"Grid generated:\n";
            std::wstring wfilename(filename.begin(), filename.end());
            wmsg += wfilename.c_str();
            MessageBoxW(hwnd, wmsg.c_str(), L"Success", MB_OK);
        }
        return 0; // Handled WM_COMMAND

    case WM_DESTROY: // Message sent when the window is being closed
        PostQuitMessage(0); // Send the WM_QUIT message to exit the message loop
        return 0; // Handled WM_DESTROY

    default:
        // Default message processing for any unhandled messages
        return DefWindowProcW(hwnd, uMsg, wParam, lParam);
    }
}