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() {
HANDLE hMutex = ::CreateMutex( NULL, false, s_lpszMutexGUID );
if(hMutex && (ERROR_ALREADY_EXISTS == GetLastError())) {
::CloseHandle(hMutex);
hMutex = NULL;
HWND hWnd = FindPreExistingHWND();
if( hWnd ) {
if(m_lpCmdLine && !lstrcmp( TEXT("-shutdown"),
m_lpCmdLine) ) {
::PostMessage(hWnd, WM_USER_SHUTDOWN, 0, 0);
}
}
return FALSE;
}else {
if(m_lpCmdLine && !lstrcmp( TEXT("-shutdown", m_lpCmdLine) ) {
}
}
...
}
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.
bool CCalendarMaker::IsLeapYear(int nYear) {
if(nYear%4 == 0) {
if(nYear%100 == 0) {
if(nYear%400 == 0 ) {
return true;
}
} else
return true;
}
return false;
}
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.
int CCalendarMaker::s_years2000[] = {
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
};
int CCalendarMaker::s_years1900[] = {
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
};
int CCalendarMaker::s_days2000[] = {
1,
6,
4,
2,
0,
5,
3
};
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.
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.
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.
bool CCalendarMaker::MakeCalendar(LPCWSTR lpszInFilePathName,
LPCWSTR lpszOutFilePathName){
SYSTEMTIME st = {0};
GetLocalTime(&st);
CTime timeNow(st);
int nCurrentYear = timeNow.GetYear();
int nCurrentMonth = timeNow.GetMonth();
int nCurrentDayOfMonth = timeNow.GetDay();
int nStartDayOfCurrentYear = GetStartDay(nCurrentYear);
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;
}
int scX = GetSystemMetrics(SM_CXSCREEN);
int scY = GetSystemMetrics(SM_CYSCREEN);
scX = (scX - (scX/10));
scY = (scY - (scY/10));
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));
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);
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);
} 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);
}
}
dc.SetBkMode(TRANSPARENT);
int nRectWidth = scX/7;
int nRectHeight = scY/8;
if(m_fontTitle.GetSafeHandle()==0) {
MakeFonts(dc);
}
dc.SetTextColor(GetSysColor(COLOR_DESKTOP));
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);
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;
}
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;
}
}
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);
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
Credits: