Click here to Skip to main content
15,883,901 members
Articles / Multimedia / GDI+
Article

A Perpetual Calendar Generator... well... at least until the year 2099!

Rate me:
Please Sign up or sign in to vote.
4.25/5 (10 votes)
11 Jul 2006CPOL7 min read 76.5K   862   38   18
Combines images from the 'My Pictures' folder with 'on-the-fly' generated calendar grids, and sets the desktop background to the resulting image.

Sample Image - pcalgen.jpg

Introduction

I am sick of floating over the clock on the taskbar to get the date. Or worse, double clicking on it to get a glimpse of what the current month looks like. Also, I want to use my own images in some way that they are automatically used as my desktop background. I wrote this project to ease my pain.

There are similar projects out there, but this one is unique in that it uses the ATL ‘CImage’ object based on GDI+, random images from the ‘My Pictures’ folder, and a home brewed perpetual calendar generator. In addition, the project was generated using Visual Studio 2005.

As a utility: PCalGen is a perpetual calendar generator (as far as our lives are concerned anyway) that uses random images from the 'My Pictures' folder to automatically generate a calendar grid each time you start your computer, change a display setting, or the date changes. The image is used as a watermark under the calendar grid. The desktop color is used when generating the text of the calendar page.

As an example: The PCalGen project demonstrates several cool things. The project contains code used to generate a calendar page for any given date between the years 1901 and 2099. See the file named 'CalendarMaker.cpp' for more details. The same file also contains code that demonstrates how to use the AlphaBlend() function to create a watermark-like image. The project also contains code that demonstrates how to coax the Shell to use the newly generated calendar image as the desktop background. Finally, the project contains code that demonstrates a simple implementation of a mechanism used to shut down a hidden window application. See CcalgenApp::InitInstance() in the file 'calgen.cpp' for more information on this mechanism.

Latest Release

Use the "Latest Release" link at the top of this article to get the most recent release version. The download includes the fully featured product with install and uninstall (MSI).

The Gregorian Calendar

The calendar generator (CalendarMaker.h, .cpp) code was some what fun to write. I did just enough research to get myself going. This means I make some assumptions with respect to the accuracy of the information I gathered to produce the logic. I believe I have done a pretty good job of isolating all the relevant factors into the object defined by this file. A code review (with comments posted in the message section of this article) is all I ask for if you want to use this code. I’ll make updates to this article and the associated downloads, as required.

Points of Interest

First, in Figure (1), we'll look at the CcalgenApp::InitInstance() method. This is where we do the shutdown processing. The shutdown processing goes something like this: look for an existing mutex, and if found, look for an existing window by class name. Any instance of the application that is beyond the shutdown processing has created a window based on this class name. If the window is found and we have the shutdown flag, we communicate it to the window via a user message which, in turn, causes the window to force the instance of the application that owns it to exit. If the mutex does not previously exist and we have the shutdown flag, the current instance quits. This allows us to use the shutdown flag without having to first check for a previous instance. The code beyond that registers the window class, creates an instance of the window based on the newly registered class, and assigns it as the main frame so that MFC behaves.

BOOL CcalgenApp::InitInstance() {
    // Single instance stuff all mixed
    // in with '-shutdown' command line processing.

    // Used to enforce a single instance<

    HANDLE hMutex = ::CreateMutex( NULL, false, s_lpszMutexGUID );
    // If the mutex already existed then
    // we can assume a previous instance exists.

    if(hMutex && (ERROR_ALREADY_EXISTS == GetLastError())) {
        // Let this instance's handle to the mutex go.

        ::CloseHandle(hMutex);
        hMutex = NULL;
        // Find the window in the previous instance and signal

        // it to shutdown if '-shutdown' is on the command line.

        HWND hWnd = FindPreExistingHWND();
        if( hWnd ) {
            if(m_lpCmdLine && !lstrcmp( TEXT("-shutdown"), 
                                        m_lpCmdLine) ) {
                ::PostMessage(hWnd, WM_USER_SHUTDOWN, 0, 0);
            }
        }
        return FALSE; // Exist this instance.

    }else {
        // Still want to test for the shutdown flag...
        // but not a second time so in this else here.

        if(m_lpCmdLine && !lstrcmp( TEXT("-shutdown", m_lpCmdLine) ) {

        }
    }
    // If we got here this is the first (primary) instance...

    ...
}

Figure (1)

The file 'CalGenWnd.cpp' defines the MFC CWnd derivation called 'CCalGenWnd' which is used as our main frame. This object deals with: standard window overhead; creating the list of image files used in the random selection of an image; handling the timer used to check for the need for updates; quitting the application when signaled to do so via the shutdown notification. I don't have anything particular that I want to highlight in 'CalGenWnd.cpp', but you should examine this file if you feel the need to.

The next file that I want us to examine is named 'MakeCalander.cpp'. This file is the meat of the project. It contains the code to generate calendar grids based on any date from the year 2001 to the year 2097 or so.

In Figure (2), we see the method used to determine leap years. The logic is outlined in the five steps. The code is my own attempt to code the logic, so please feel free to improve on it.

//1. If the year is evenly divisible by 4, go to step 2. Otherwise, go to step 5. 

//2. If the year is evenly divisible by 100, go to step 3. Otherwise, go to step 4. 

//3. If the year is evenly divisible by 400, go to step 4. Otherwise, go to step 5. 

//4. The year is a leap year (it has 366 days). 

//5. The year is not a leap year (it has 365 days). 

/*static*/ bool CCalendarMaker::IsLeapYear(int nYear) {
    if(nYear%4 == 0) { // step 1
        if(nYear%100 == 0) { // step 2
            if(nYear%400 == 0 ) { // step 3
                return true; // step 4
            }
        } else
            return true; // step 4
    }
    return false; // step 5
}

Figure (2)

The two arrays in Figure (3) are something that could be researched better for confirmation. They are used to determine the name of the first day of the year. I did some testing, and they seem to work.... I just don't have the mathematical proofs. Feel free to provide any proof if you are hip to that sort of thing, and I will detail them here.

The first array is a table (two dimensional) that contains the years from 2001 to 2097, arranged in ascending increments of four, and in columns that indicate the name of they day the first of the year falls on. Note that the days are not in normal order. The second array represents the order of the days specified in the header of the first array. The numbers represent the normal order of the days of the week.

To use the table to determine the name of the first day of a given year, one must find the largest value that is nearest being equal to the year in question without going over it. Note the day in the column header for the year, and count off any additional days, one for each year needed to be added to the year in the column to reach the year in question.

/*static*/ int CCalendarMaker::s_years2000[] = {
        /* Mon   Sat   Thu   Tue   Sun   Fri   Wed */
           2001, 2005, 2009, 2013, 2017, 2021, 2025,
           2029, 2033, 2037, 2041, 2045, 2049, 2053,
           2057, 2061, 2065, 2069, 2073, 2077, 2081,
           2085, 2089, 2093, 2097
};

/*static*/ int CCalendarMaker::s_years1900[] = {
        /* Tue   Sun   Fri   Wed   Mon   Sat   Thu */
           1901, 1905, 1909, 1913, 1917, 1921, 1925,
           1929, 1933, 1937, 1941, 1945, 1949, 1953,
           1957, 1961, 1965, 1969, 1973, 1977, 1981,
           1985, 1989, 1993, 1997
};

/*static*/ int CCalendarMaker::s_days2000[] = {
    1,
    6,
    4,
    2,
    0,
    5,
    3
};

/*static*/ int CCalendarMaker::s_days1900[] = {
    2,
    0,
    5,
    3,
    1,
    6,
    4
};

Figure (3)

Finally, here in Figure (4), is the code that uses the arrays described above to determine the name (via numeric reference) of the day the first of a given year (January 1) falls on.

// 0=sun, 1=mon, 2=tue, 3=wed, 4=thu, 5=fri, 6=sat

/*static*/ int CCalendarMaker::GetStartDay(int nYear) {
    if(nYear >= 1901 && nYear <= 2000) {
        int nIndex = 0;
        while( nYear >= s_years1900[nIndex] )
            nIndex++;
        int nDay = s_days1900[(nIndex-1)%];
        nDay += (nYear - s_years1900[nIndex-1]);
        if(nDay>6)
            nDay -= 7;
        return nDay;
    } else if(nYear >= 2001 && nYear <= 3000) {
        int nIndex = 0;
        while( nYear >= s_years2000[nIndex] )
            nIndex++;
        int nDay = s_days2000[(nIndex-1)%];
        nDay += (nYear - s_years2000[nIndex-1]);
        if(nDay>6)
            nDay -= 7;
        return nDay;
    }
    return -1;
}

Figure (4)

In Figure (5), we can see the array of values used to represent the amount of days in each month. We will look at the code to see how we account for leap years later.

/*static*/ int CCalendarMaker::s_daysPerMonth[] = {
    31,
    28,
    31,
    30,
    31,
    30,
    31,
    31,
    30,
    31,
    30,
    31
};

Figure (5)

The rest of the code detailed in Figure (6) is the 'CCalendarMaker::MakeCalendar()' method. At first glance, this method appears to be a bit of a bear but is really pretty straightforward.

First, we get the current date, and extract some data from it that we need for our calculations. Next, we determine the name of the day that the first of the current month falls on. Now, we have everything we need to make a calendar grid.

In the comments that follow, I have included a floor plan for the grid that I plan to generate. The code to generate the grid and the blended image are beyond that. This is where you will find examples of using the CImage object and the AlphaBlend() method.

// Image file to use, output path... will be a jpg.

bool CCalendarMaker::MakeCalendar(LPCWSTR lpszInFilePathName, 
                                  LPCWSTR lpszOutFilePathName){
    // Get the current time
    // and stash it in one of these cool CTime objects.

    SYSTEMTIME st = {0};
    GetLocalTime(&st);
    CTime timeNow(st);

    // Use the time object to get some value
    // we need for generating a calendar page.

    int nCurrentYear = timeNow.GetYear();
    int nCurrentMonth = timeNow.GetMonth();
    int nCurrentDayOfMonth = timeNow.GetDay();
    int nStartDayOfCurrentYear = GetStartDay(nCurrentYear);

    // Get the month and year display strings ready.

    CString strMonth = CCalendarMaker::s_months[nCurrentMonth-1];
    CString strYear;
    strYear.Format(L"%d", timeNow.GetYear());

    int nFirstDayOfMonth = 0;
    if(nCurrentMonth==1) {
        nFirstDayOfMonth = nStartDayOfCurrentYear;
    } else {
        int nDayAccum = 0;
        int nIndexMonth = 0;
        while(nIndexMonth < (nCurrentMonth-1)) {
            nDayAccum += s_daysPerMonth[nIndexMonth++];
        }

        nDayAccum += nCurrentDayOfMonth;

        if(nCurrentMonth >= 3 && IsLeapYear(nCurrentYear)) 
            nDayAccum += 1;

        nFirstDayOfMonth = 
          (nDayAccum-(nCurrentDayOfMonth-nStartDayOfCurrentYear))%7;
    }

    // I think we have everything we need now.

    // Generate the calendar image to using
    // this floorprint... *today* should be in red.


 //     0   1   2   3   4   5   6    Given:  January 9th, 2006

 //   |---------------------------|  |---------------------------|

 // 0 |     <MONTH>   <YEAR>      |  |       January  2006       |

 //   |---|---|---|---|---|---|---|  |---|---|---|---|---|---|---|

 // 1 | S | M | T | W | T | F | S |  | S | M | T | W | T | F | S |

 //   |---|---|---|---|---|---|---|  |---|---|---|---|---|---|---|

 // 2 |   |   |   |   |   |   |   |  |   |   |   |   |   |   | 1 |

 //   |---|---|---|---|---|---|---|  |---|---|---|---|---|---|---|

 // 3 |   |   |   |   |   |   |   |  | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

 //   |---|---|---|---|---|---|---|  |---|---|---|---|---|---|---|

 // 4 |   |   |   |   |   |   |   |  |<9>| 10| 11| 12| 13| 14| 15|

 //   |---|---|---|---|---|---|---|  |---|---|---|---|---|---|---|

 // 5 |   |   |   |   |   |   |   |  | 16| 17| 18| 19| 20| 21| 22|

 //   |---|---|---|---|---|---|---|  |---|---|---|---|---|---|---|

 // 6 |   |   |   |   |   |   |   |  | 23| 24| 25| 26| 27| 28| 29|

 //   |---|---|---|---|---|---|---|  |---|---|---|---|---|---|---|

 // 7 |   |   |   |   |   |   |   |  | 30| 31|   |   |   |   |   |

 //   |---|---|---|---|---|---|---|  |---|---|---|---|---|---|---|

 // 8 |         copyright         |  |         copyright         |

 //   |---|---|---|---|---|---|---|  |---|---|---|---|---|---|---|


    // Get the screen dims.

    int scX = GetSystemMetrics(SM_CXSCREEN);
    int scY = GetSystemMetrics(SM_CYSCREEN);

    scX = (scX - (scX/10));
    scY = (scY - (scY/10));

    // Make a bitmap 10% smaller in width and height

    HWND hwndDesktop = GetDesktopWindow();
    HDC hDCDeskTop = GetDC(hwndDesktop);

    HDC hDC = CreateCompatibleDC(hDCDeskTop);
    if(hDC) {
        HBITMAP hBitmap = CreateCompatibleBitmap(hDCDeskTop, scX, scY);
        CDC dc;
        CBitmap bitmapCal;
        bitmapCal.Attach(hBitmap);
        dc.Attach(hDC);
        dc.SelectObject(bitmapCal);
        dc.SelectObject(GetStockObject(WHITE_BRUSH));

        // Create the background

        dc.Rectangle(0,0,scX, scY);
        CImage imagePicture;
        HRESULT hr = imagePicture.Load(lpszInFilePathName);
        int nPicWidth = imagePicture.GetWidth();
        int nPicHeight = imagePicture.GetHeight();
        if(S_OK == hr) {
            HDC hDCPic = CreateCompatibleDC(hDCDeskTop);
            HBITMAP hBitmapPic = 
               CreateCompatibleBitmap(hDCDeskTop, scX, scY);
            CBitmap bitmapPic;
            bitmapPic.Attach(hBitmapPic);
            CDC dcPic;
            dcPic.Attach(hDCPic);
            dcPic.SelectObject(hBitmapPic);
            dcPic.SelectObject(GetStockObject(WHITE_BRUSH));
            dcPic.Rectangle(0,0,scX, scY);
            imagePicture.BitBlt(dcPic.GetSafeHdc(), 0, 0, SRCCOPY);

            BITMAP bm;
            bitmapPic.GetBitmap(&bm);

            // dc.BitBlt(0,0,scX,scY,&dcPic,0,0,SRCCOPY);
            // Normal draw code for debugging.

            BLENDFUNCTION bf;
            bf.AlphaFormat = 0;
            bf.BlendFlags = 0;
            bf.BlendOp = AC_SRC_OVER;
            bf.SourceConstantAlpha = 64;

            if(scX > nPicWidth) {
                dc.AlphaBlend((scX/2)-(nPicWidth/2),(scY/2)-(nPicHeight/2),
                   nPicWidth,nPicHeight,&dcPic,0,0,nPicWidth,nPicHeight,bf);
                   // Watermark type drawing.

            } else {
                int nLeftPos = nPicWidth/2-(scX/2);
                int nTopPos = nPicHeight/2-(scY/2);
                if(nLeftPos<0)
                    nLeftPos=0;
                if(nTopPos<0)
                    nTopPos=0;
                dc.AlphaBlend(0,0,scX,scY,&dcPic,nLeftPos,nTopPos,
                   scX-nLeftPos,scY-nTopPos,bf); // Watermark type drawing.

            }
        }

        dc.SetBkMode(TRANSPARENT);

        int nRectWidth = scX/7;
        int nRectHeight = scY/8;

        if(m_fontTitle.GetSafeHandle()==0) {
            MakeFonts(dc);
        }

        dc.SetTextColor(GetSysColor(COLOR_DESKTOP));

        // Draw header (row 0)

        dc.SelectObject(&m_fontTitle);
        CString strOut = strMonth;
        strOut += L"  ";
        strOut += strYear;
        CRect rectHeader(0, 0, scX, nRectHeight/2);
        dc.DrawText(strOut, strOut.GetLength(), rectHeader, 
                    DT_CENTER|DT_VCENTER|DT_SINGLELINE);
  
        // Draw days of the week (row 1)

        dc.SelectObject(&m_fontDays);
        CRect rectDay(0, nRectHeight/2, nRectWidth, nRectHeight*2);
        for(int nIndexDayNames = 0; nIndexDayNames <= 6; nIndexDayNames++) {
            dc.DrawText(s_dayNames[nIndexDayNames], 
                        lstrlen(s_dayNames[nIndexDayNames]), 
                        rectDay, DT_CENTER|DT_VCENTER|DT_SINGLELINE);
            rectDay.left += nRectWidth;
            rectDay.right = rectDay.left + nRectWidth;
        }

        // Draw days of the month in each the grid (rows 2-7)

        dc.SelectObject(&m_fontNumbers);
        rectDay.left = nRectWidth * nFirstDayOfMonth;
        rectDay.right = rectDay.left + nRectWidth;
        rectDay.top = nRectHeight+nRectHeight/2;
        rectDay.bottom = rectDay.top+nRectHeight;
        int nNextRow = nFirstDayOfMonth;
        int nLastDayInMonth = CCalendarMaker::s_daysPerMonth[nCurrentMonth-1];
        if(nCurrentMonth==2 && IsLeapYear(nCurrentYear) )
            nLastDayInMonth += 1;
            for(int nIndexDayNumbers = 1; nIndexDayNumbers <= nLastDayInMonth; 
                                                         nIndexDayNumbers++) {
                CString strDayNumber;
                strDayNumber.Format(L"%d", nIndexDayNumbers);
                if(nIndexDayNumbers==nCurrentDayOfMonth) {
                    dc.SetTextColor(RGB(255,0,0));
                }
            dc.DrawText(strDayNumber, strDayNumber.GetLength(), 
                        rectDay, DT_CENTER|DT_VCENTER|DT_SINGLELINE);
            if(nIndexDayNumbers==nCurrentDayOfMonth)
                dc.SetTextColor(GetSysColor(COLOR_DESKTOP));

            rectDay.left += nRectWidth;
            rectDay.right = rectDay.left + nRectWidth;
            nNextRow++;
            if(nNextRow == 7) {
                rectDay.left = 0;
                rectDay.right = nRectWidth;
                rectDay.top += nRectHeight;
                rectDay.bottom = rectDay.top+nRectHeight;
                nNextRow = 0;
            }
        }

        //Draw footer (row 8)

        dc.SelectObject(&m_fontTitle);
        CRect rectFooter(0, nRectHeight*7, scX, nRectHeight*8);
        rectFooter.bottom -= 10;
        strOut.LoadStringW(IDS_WEBSITE);
        dc.DrawText(strOut, strOut.GetLength(), rectFooter, 
                    DT_CENTER|DT_VCENTER|DT_SINGLELINE);
        dc.SelectObject(&m_fontDays);
        strOut.LoadStringW(IDS_COPYRIGHT);
        dc.DrawText(strOut, strOut.GetLength(), rectFooter, 
                    DT_CENTER|DT_BOTTOM|DT_SINGLELINE);

        // Store the image

        CImage newImage;
        newImage.Attach((HBITMAP)dc.GetCurrentBitmap()->GetSafeHandle());
        newImage.Save(lpszOutFilePathName);
    }
    return true;
}

Figure (6)

If I left out any details you think should be mentioned in the article, please let me know.

If you could take one last second to rate this article or even leave a comment, it would be much appreciated.

Thanks for reading!

History:

  • 1.04 - July 1, 2006
    • Fixed bug in installer that caused program to not launch after reboot.
    • Config dialog view mode combo is a pull down list... can no longer type into it.
  • 1.03 - June 16, 2006
    • Fixed bug that caused bogus calculation off the start day for years 2007 and 2008.
  • 1.02 - June 15, 2006
    • Indicates some holidays (New Year's Day, Valentine's Day, President's Day, St. Patrick's Day, Easter, Memorial Day, Cinco De Mayo, Mother's Day, Father's Day, Independence Day, Labor Day, Halloween, and Christmas).
  • 1.01 - June 14, 2006
    • Includes configuration tool.
    • User selected layout.
    • User selected colors.
    • User selected alpha blend value.
    • Fully featured install and uninstall.
  • 1.00 - June 10, 2006
    • Initial release.

Credits:

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Web Developer
United States United States
16yrs of GUI programming experience gained at: (most recent first) BlackBall, Veritas, Seagate Software, Arcada, Stac, Mountain, and Emerald Systems.

Languages/Scripting: C, C++, JAVA, BASIC, JAVASCRIPT, HTML, XML, PHP, and SQL

Tools: MS Visual Studio, MS Visual SourceSafe, CVS, PVCS, Bounds Checker, VMWare, ToDoList, InstallShield, and Office Applications

Libraries and API: RTL, STL, WIN32, MFC, ATL, .NET, ActiveX, DirectX, COM, DCOM, Shell Extensions, and Shell Namespaces

Strengths: Honest, communicative, keen eye for usability, good at estimating workload and completion dates, ready to take on grunt work, team player, experienced working with QA, localization, Tech Pubs, Sales, and Marketing teams.

Comments and Discussions

 
QuestionPerpetual Calenar Pin
robertjb202-Apr-15 4:05
professionalrobertjb202-Apr-15 4:05 
GeneralBug : The Calendar is mangled by the toolbar(s) Pin
andré_k24-Jan-08 7:04
andré_k24-Jan-08 7:04 
GeneralLatest Release download.... NOT! Pin
minhski5-Jul-06 16:59
minhski5-Jul-06 16:59 
GeneralRe: Latest Release download.... NOT! Pin
Shaun Harrington7-Jul-06 2:52
Shaun Harrington7-Jul-06 2:52 
GeneralCannot install or remove [modified] Pin
Richard Jones4-Jul-06 4:27
Richard Jones4-Jul-06 4:27 
GeneralRe: Cannot install or remove Pin
Richard Jones4-Jul-06 5:02
Richard Jones4-Jul-06 5:02 
GeneralRe: Cannot install or remove Pin
Shaun Harrington4-Jul-06 9:21
Shaun Harrington4-Jul-06 9:21 
GeneralBug in the title Pin
Skizmo4-Jul-06 3:23
Skizmo4-Jul-06 3:23 
GeneralRe: Bug in the title [modified] Pin
Shaun Harrington4-Jul-06 9:19
Shaun Harrington4-Jul-06 9:19 
GeneralWhen I run, nothing appears... Pin
Jun Du11-Jun-06 3:22
Jun Du11-Jun-06 3:22 
GeneralRe: When I run, nothing appears... Pin
Shaun Harrington11-Jun-06 5:40
Shaun Harrington11-Jun-06 5:40 
GeneralRe: When I run, nothing appears... Pin
Jun Du11-Jun-06 8:58
Jun Du11-Jun-06 8:58 
GeneralRe: When I run, nothing appears... [modified] Pin
Shaun Harrington11-Jun-06 9:09
Shaun Harrington11-Jun-06 9:09 
QuestionDownload file size? Pin
PJ Arends10-Jun-06 16:35
professionalPJ Arends10-Jun-06 16:35 
AnswerRe: Download file size? Pin
Shaun Harrington10-Jun-06 17:09
Shaun Harrington10-Jun-06 17:09 
GeneralRe: Download file size? Pin
Jun Du11-Jun-06 3:35
Jun Du11-Jun-06 3:35 
GeneralRe: Download file size? Pin
Shaun Harrington11-Jun-06 5:45
Shaun Harrington11-Jun-06 5:45 
GeneralRe: Download file size? Pin
Shaun Harrington12-Jun-06 10:59
Shaun Harrington12-Jun-06 10:59 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.