A clock utility

Home
Back To Tips Page

Welcome to FlounderClock!  download.gif (1234 bytes)

This code is written to support both VS.NET 2003 and VS6, and contains both .dsw/.dsp files and .vcproj/.sln files. It comes with both ANSI and Unicode builds.

A product of FlounderCraft Ltd.
Copyright © 2005-2006, FlounderCraft Ltd. All Rights Reserved.
You are free to redistribute FlounderClock without charge, providing this documentation and all copyright notices are included.

This little clock is designed to be a laptop accessory for a variety of purposes.

It is an alarm clock, a bedside clock, and a presentation clock.

Double-click the clock icon to launch it. It will come up in the last mode you set. If the alarm was set when the clock closed off, it is set when the clock restarts.

Alarm clock function

After the alarm clock is set, as described below, you can use the following keyboard commands

A (or a) Turns the alarm function on and off.
Esc Cancels the alarm and sets it up to sound on the next day at the selected time.
? Plays the alarm sound. You can use this to make sure you have not muted the sounds and that it will wake you up. You can also use it in conjunction with your volume controls to adjust the alarm to a pleasant but effective level.
Space If the alarm is sounding, it goes into snooze mode. The clock status display will indicate that the alarm is snoozing, and the countdown timer will display the time until the next alarm sound.

Note that for these keyboard commands to work, the clock must have the focus.

Clock Formats

The FlounderClock has three different formats: full screen, medium, and small. You can easily toggle among these just by double-clicking anywhere on the clock. Or you can use the right mouse click and select the desired size from the menu.

Small Format

The small format clock is useful when you need a little clock on the screen. It is particularly useful when doinhg PowerPoint presentations when you have a two-monitor setup on your laptop. In the small format, the clock program is set to "Always on top" and will sit in front of whatever programs are currently up, including the PowerPoint presentation view.

Also, in the small format only, the opacity of the clock can be changed. In addition, the selection of colors used can also be changed in all formats, and in the various opacities in the small format, the colors can change the appearance relative to various backgrounds.

Medium Format

The top line in the image above is the time. In this case, the 24-hour format is used. The next line down, on the left, is the alarm status. This indicates that the alarm is on, and is set to go off at 8:00. On the right is the seconds counter. The third line down on the left has a "countdown timer" which represents the hours, minutes, and seconds until the alarm will sound. If the alarm is off, this countdown timer is not displayed. On the right, the time zone information is displayed. The bottom line shows the date. If the entire date will not fit across the line, the month will be truncated, then the day of the week.

Large Format

The large display is a full-screen display. When the clock is in full-screen display, the screen saver is disabled. The same information as the medium display is shown. The large format is maximized and fills the entire screen.

The Menu

Settings... brings up the Settings menu which allows choice of all the parameters, such as colors, fonts, time zone, and such as described below.

Alarm On/Alarm Off sets the alarm on or off, and the A or a keys provide the keyboard interface for this.

Large, Medium, and Small select the appropriate size, which can be selected by double-clicking the clock itself. The Minimize option is not currently supported.

Help... is what brought this help menu up.

About... produces the canonical program description.

Exit closes the program.

Setting the alarm

To set the alarm, right-click on the clock and select Alarm... The window shown below will come up.

The large spin controls allow setting the hours and minutes of the alarm time, expressed in 24-hour time. If 12-hour time is selected, the additional AM/PM indicator is shown. The rightmost spin conrol sets the "snooze" time to be some interval from 2 to 20 minutes.

The Clock setting indicates whether a 12-hour clock display or 24-hour clock display will be used.

For the opacity settings, see the discussion below.

The Sound selection selects the desired wakeup sound. The sounds will vary with various releases of the program. The button to the right will play the sound so you can hear it.

There are two special Sound selections. One is called "Random" and it means that each time the alarm clock sounds, it sounds with a randomly-selected sound from the set of sounds. The other is called "User File" and allows you to specify a .wav file which is the desired sound. The sound should take less than 15 seconds to sound, because the clock chimes every 15 seconds once the alarm goes off. Note that if you have a user-defined sound, it will not take part in the "Random" selections, which apply only to the predefined sounds. Note that if for some reason the sound file you specify is deleted, the "User File" mode will act as the "Random" mode, rather than failing to sound at all.

The Time Zone selection selects any of the many time zones throughout the world. Shown in the boxes are the UTC time, the time set on your computer, and "Local" time as selected by your selection of time zone. As you select various time zones, the "Local" setting will change to show the time in the time zone you have selected.

The Locale setting selects which locale is used for names of the month, weekday, and AM/PM indicators, and when appropriate, which digits to use. The Use Local Digits will translate digits to the appropriate localized format. These controls work much better when the Unicode version of the clock is used, and when an appropriate font, such as shown in the illustration under "International Support".

The Colors selection allows the selection of Text and Background colors for each of the three sizes. If no colors are specified, black is used for the background and red is used for the text. The colors are treated as "more-specific to least-specific" according to the following rule: The large size checks for a user selection, if not available, it takes the default colors. For a medium size display, it looks for a specific setting, if it doesn't find it, it looks for the lage size, and then it uses the default. For the small size, the path is to look for small size, then medium size, then maximized size, then the default. If the Make background transparent option is checked, the background is made transparent, as shown below, to the left. If the background is not transparent, the appearance is more like on the right, which is a transparency value of 128 applied to a solid black background with solid red digits. Which of these you may choose is up to you.

The Font selection allows the selection of a font. The font size is irrelevant, and is ignored; all font sizes are all computed on-the-fly as required.

Font Selection

The default font is whatever the default dialog font is on your machine. To select the font, right click on the clock and select the Font... menu item.

The nice font shown here is "LCD Mono" from http://www.spinwardstars.com/scrfonts/lcdmono.html. (This font is Copyright © 1999 by Samuel Reynolds. All rights reserved). You can download and install your own copy.

Localized versions for nontrival character sets work best with the Unicode release, with a font such as Arial Unicode MS, which is delivered with Microsoft Office.

Positioning the clock

You can drag the clock around by its caption bar when it is other than maximized. Just click and drag as you would with any other window. The caption bar colors follow your default color scheme. The clock will favor the position it is in, and if you switch from small to medium format it will make sure that the entire clock is displayed on a single monitor (it is multi-monitor aware), and insofar as it is able, it will return the clock to its original position. However, if the clock is split across two monitors, it will make sure it is redisplayed entirely on the monitor which is "most favored" (the one that had the biggest piece of the clock). When the clock background is transparent, it can still be dragged around by its caption bar,

International Support

FlounderClock will support, in its Unicode release, any international format which Microsoft supports. The standard Windows APIs for national language support are used to obtain the representations for digits, separators, names of months and days, and so on. However, the parts of the text which are in the STRINGTABLE resources are not internationalized, so for now the words are still in English. Note in the illustration below the digits and AM/PM indicator, weekday, and month name are localized, because there are API calls to provide this information; the notations such as "Alarm Off" and the time zone designation still come from the STRINGTABLE resource, which I have not internationalized.

 

If anyone sends me internationalized versions of any of the resources, I will attempt to incorporate them into a future release, and will give the contributor credit on the "About" dialog. Anyone who submits a translation of this "Help" page in their local language will be similarly credited.

The building of FlounderClock

This clock project was designed to serve two purposes.  The primary purpose was because I got tired of the dime store clocks often found in hotels.  I travel enough that I'm seeing a new kind of clock at least once a month, sometimes twice a month, and figuring out (a) how the alarm works (b) where the AM/PM indication is (c) how to turn of the obnoxious buzzing while mostly asleep (d) the need to read the clock when I don't have my glasses on and (e) getting rid of that obnoxious noise were all factors.  In addition, there is the continuing lack of intelligence in the design of hotel rooms; by definition, apparently, the clock and the phone have to be on opposite sides of the bed, which is bad enough; but some hotels, apparently frightened by the prospect of the theft of their $3 clock radios, manage to clamp them down so they can't be moved.  Those which are movable are carefully rearranged each day by the cleaning staff to make sure they are turned around to point outward into the room, instead of sideways so they can be read by someone in the bed, is yet one more annoyance.  So I wanted a decent clock beside my bed.

Next, when I teach, about half the rooms I teach in don't have wall clocks, or the wall clock is placed in the room where the instructor cannot see it (such as on the front wall).  While it might look tacky looking at my wristwatch, I'm saved from that because I haven't worn a wristwatch in nearly a quarter-century, so the point is moot.  So I needed a clock that would be visible when I was doing my PowerPoint displays.  I didn't particularly feel like writing two programs, so the one I wrote for the bedside display was adapted.

I added features like the AM/PM indicator when I decided to release it, which added a lot of work I would not normally have done; I prefer using a 24-hour clock (all the clocks I use have 24-hour displays).  I once nearly missed a class because I slept in, having misread the AM/PM indicator.  That doesn't happen if you use 24-hour times.

I was also involved in doing some internationalization efforts, so as long as I was engaged in that, I decided to generalize it to explore the use of the National Language Support (NLS) APIs.

A couple weeks ago (I'm writing this on 19-Mar-06) I was pointed at an article on layered windows, so I decided to add layered window support as well.

From a programming viewpoint, this has a lot of interesting features. These include

Fitting text to a specific width

One of the problems that sometimes arises is to fit a piece of text to a specific width. But we are not given an option to specify a width of a string, only a height of a font. Fortunately, this is easy to do.

The trick is to pick an initial height, compute the width of the string resulting, and then adjust the height accordingly. It almost doesn't matter what your initial choice is, although realistically it makes sense to choose a "maximum height" font size. In my case, I chose a height that would allow for space at the bottom for the date. I come in with a desired logical font, which is the font which was saved. The lfHeight is always recomputed. Note that I have to query the parent window state to see if I have a small window (which will not have the status line) or either of the other windows.

I create a font and compute the width of the text to display. In this case, I take advantage of the fact that in virtually every font, digits are always the same width, so I can use a test string of "00:00" and be correct. In general, you would have to use the actual string  you wanted to display.

To allow a little space around the edges. I deflate the rectangle by the width of the SM_CXBORDER value.

I compute the ratio of the actual size of the text to the desired size. If the actual size will fit, I leave the size alone (because it is already the maximum height I'm willing to use). If it is too long, I compute the ratio of the desired value to the actual value. This gives me a rescaling factor. I then rescale the height by this factor, create a new font, and I'm done. Note the use of Detach, because if I'm going to kill the font, I will do it by obtaining a temporary CFont object with GetFont. See my essay on the use of Attach/Detach.

void CClockDisplay::SetDisplayFont(LOGFONT & lf)
    {
     CClientDC dc(this);

     CRect r;
     GetClientRect(&r);
     r.InflateRect(-::GetSystemMetrics(SM_CXBORDER), -::GetSystemMetrics(SM_CXBORDER));

     // If we are in a small display, there will be no space required for
     // any display except the hh:mm, so we will use r.bottom for the font size
     // If we are in a restored or maximized display, we want to leave space at the
     // bottom for the date, so we will subtract off the date height
     CSize resolution(dc.GetDeviceCaps(LOGPIXELSX), dc.GetDeviceCaps(LOGPIXELSY));
     CSize date(resolution.cx / 2, resolution.cy / 2);

     if(QueryParentState() != WindowState::Small)
        r.bottom -= date.cy;

     // Make a font that fits in the rectangle we are going to use
     lf.lfHeight = r.Height();
     CFont font;
     font.CreateFontIndirect(&lf);

     dc.SelectObject(&font);

     // Now compute the size of the time string 00:00. What we want is the
     // correct height so the width is not exceeded
     CSize sz = dc.GetTextExtent(CString(_T("00:00")));

     // If it is too wide at the specified height, scale down by the
     // ratio of the desired size to the actual size, and create a
     // new font which will just fit.

     if(sz.cx > r.Width())
        { /* too big */
         double rescale = (double)r.Width() / (double)sz.cx;
         lf.lfHeight = (int) ((double)lf.lfHeight * rescale);
         font.DeleteObject();
         font.CreateFontIndirect(&lf);
        } /* too big */

     // If we had previously set a font, get rid of it.
     CFont * old = GetFont();
     if(old != NULL)
        old->DeleteObject();

     SetFont(&font);
     // Make sure that when font is deleted, the actual HFONT
     // is not deleted
     font.Detach();

     // Now force a redraw
     Invalidate();
    } // CClockDisplay::SetDisplayFont

Using sound resources

This is fortunately very easy. To import a .wav file into the project, right click on the resource and select the Import... menu item. The sound comes in as a resource of type "WAVE". What I do is change the ID from IDR_WAVE1 to IDW_FILENAME=nnnn. I chose arbitrarily to use the filename part of the wave file as the resource ID. So, for example, if the resource was in a file called DING.WAV, I started the values at 5001, so I might have set IDW_DING=5004. Then I added a STRINGTABLE entry as follows:

IDS_SOUND_START   5000  <<This is not used for anything>>
IDW_WESTMINSTER   5001  Westminster chimes
IDW_DING          5004  Ding!
IDW_27_4          5005  Wood blocks
...
IDS_SOUND_END     5100  <<This is the end of the sounds>>
IDS_USER_FILE     5101  User File
IDS_RANDOM        5102  Random

The sounds could be any value in the range of IDS_SOUND_START+1 to IDS_SOUND_END-1, and I use this knowledge so that my code is insensitive to the arrangement of the sounds. I can add new sounds, drop old sounds out, and as long as I'm in the range specified, it works perfectly.

In case you are wondering why most of my files have such odd names, my CD-ROM of 2000 public domain sounds had such meaningful names as "27_4.wav", but I wanted the resource names to be easily correlated with the file names of the files.

For example, the loop to load the combo box is

    for(int i = IDS_SOUNDS_START + 1; i < IDS_SOUNDS_END; i++)
       { /* scan sounds */
        CString s;
        s.LoadString(i);
        if(s.IsEmpty())
           continue;
        int n = c_Sounds.AddString(s);
        c_Sounds.SetItemData(n, i); // save sound ID
       } /* scan sounds */

    { /* special values */
     CString s;
     int n;

     s.LoadString(IDS_USER_FILE);
     n = c_Sounds.InsertString(0, s);
     c_Sounds.SetItemData(n, IDS_USER_FILE);

     s.LoadString(IDS_RANDOM);
     n = c_Sounds.InsertString(0, s);
     c_Sounds.SetItemData(n, IDS_RANDOM);

    } /* special values */

Note the special use of InsertString so the two special cases of "User File" and "Random" appear at the front of the otherwise-sorted combo box listbox.

Playing sound resources

To play a sound resource, I only need to use PlaySound. However, if I play it synchronously, the seconds-update will pause for the duration of the playing, which is not a desirable effect. So I play it asynchronously. In the example below, the variable sound is the integer ID of the sound resource.

 ::PlaySound(MAKEINTRESOURCE(sound), AfxGetInstanceHandle(), SND_RESOURCE | SND_ASYNC);

Stopping the playing immediately

The first morning it woke me up, I hit the space bar to enter "snooze" mode. The sound kept playing to the end. This was not the intended result of my hitting the "snooze" option. So I had to add a way to shut the sound off immediately. ::PlaySound supports this option using the SND_PURGE flag.

void CClockDisplay::ShutUp()
    {
     ::PlaySound(NULL, NULL, SND_PURGE);
    } // CClockDisplay::ShutUp

Handling laptop power events

At the end of the class, I shut my laptop. This is configured to put it into hibernation mode. In hibernation mode, the contents of the memory are written to disk and the motherboard is powered down. Later that night, I opened it up and reactivated it. I noticed my countdown timer (let's hear it for debugging aids! Read my essay on the Graphical Developer Interface and why these are always good ideas) didn't look right. To get up at 7a.m., given it was slightly after 10p.m., I should have seen a countdown of slightly under 9 hours. Instead, I saw a countdown of 13 hours! Whoops! So I toggled the alarm off, then back on, and got the right countdown time. The next evening, I added power management.

To deal with this, I had to intercept the WM_POWERBROADCAST  message in my clock display. This is broadcast to all the top-level windows, but the top-level window had no clue as to what to do with it. So to the main dialog window (this is a dialog app) I added

BEGIN_MESSAGE_MAP(CClockDlg, CDialog)
        ON_MESSAGE(WM_POWERBROADCAST, OnPowerBroadcast)

All I did in the handler was forward this first to all the child windows, and then to the default handler for the main window:

LRESULT CClockDlg::OnPowerBroadcast(WPARAM wParam, LPARAM lParam)
    {
     SendMessageToDescendants(WM_POWERBROADCAST, wParam, lParam);
     return Default();
    } // CClockDlg::OnPower

The Default( ) call is what calls DefWindowProc or DefDialogProc for the window handler. This is one of those really dumb ideas that crept into MFC, in a misguided attempt to "optimize" its behavior by making it work incorrectly, but we're stuck with this bad design now.

In the alarm clock display window, which is where all the smarts about the behavior of the clock are implemented, I added

BEGIN_MESSAGE_MAP(CClockDisplay, CWnd)
        ON_MESSAGE(WM_POWERBROADCAST, OnPowerBroadcast)

and put the appropriate reactions in the handler:

LRESULT CClockDisplay::OnPowerBroadcast(WPARAM wParam, LPARAM)
    {
     switch(wParam)
        { /* wparam */
         case PBT_APMBATTERYLOW:
         case PBT_APMOEMEVENT:
         case PBT_APMPOWERSTATUSCHANGE:
         case PBT_APMQUERYSUSPEND:
         case PBT_APMQUERYSTANDBY:
         case PBT_APMQUERYSUSPENDFAILED:
         case PBT_APMQUERYSTANDBYFAILED:
         case PBT_APMSUSPEND:
         case PBT_APMSTANDBY:
            return 0;
         case PBT_APMRESUMEAUTOMATIC:
         case PBT_APMRESUMECRITICAL:
         case PBT_APMRESUMESUSPEND:
         case PBT_APMRESUMESTANDBY:
            break;
        } /* wparam */
     
     // By calling this here, we force the time delta to be recomputed
     // This now produces the correct wait time and ignores the time
     // during which the machine was in standby or hibernation modes
     
     if(AlarmState != Disabled)
        SetAlarmState(Armed);
     return 0;
    } // CClockDisplay::OnPowerBroadcast

Note that I come out of the switch statement only if there is a PBT_APMRESUME* message. All other messages are ignored. My SetAlarmState does the correct computations to force the time delta to be recomputed.

An approach to timing

This was conceived of as a quickie personal project. I certainly had no intention of publishing it when I started working on it. So I made a number of decisions based on what was simplest and what I understood most readily. I played a bit with waitable timers, but doing things like computing the absolute delta-T turn out to be a bit tricky, particularly when you are trying to compute only something based on HH:MM. There isn't a lot of good support for time.

So what I did was compute the desired time until I wanted the next alarm event (whether it was the time until the next alarm sound, the time for the snooze, or the time until the next alarm the next day). So I created a thread that simply did a WaitForSingleObject with a timeout value.

UINT CClockDisplay::waiter(LPVOID p)
    {
     CClockDisplay * self = (CClockDisplay *)p;
     switch(::WaitForSingleObject(self->Timer, self->delay))
        { /* wait */
         case WAIT_OBJECT_0:
            self->PostMessage(UWM_TIMER_CANCELLED);
            break;
         case WAIT_TIMEOUT:
            self->PostMessage(UWM_TIMER_EXPIRED);
            break;
         default:
            ASSERT(FALSE);
        } /* wait */
     return 0;
    } // CClockDisplay::waiter

Now the Timer variable is not a handle to a waitable timer (although in one draft of the code it was, until I started trying to do time delta arithmetic), but a handle to a manual-reset event. If I want to cancel the timing, I just do a SetEvent on the Timer variable and the wait drops out with a WAIT_OBJECT_0 notification, and I indicate that the timer has been cancelled. If, however, it times out, I get out with a WAIT_TIMEOUT. In these cases, I simply use PostMessage to post a user-defined message to my main GUI thread, which then responds appropriately. Then the thread terminates.

Now, you may say, "This is not efficient". Like it matters? Of course I could do something more complex, but why write a program that is more complex than it needs to be, so it can save milliseconds on an event that happens once a day, or once every ten minutes, or for that matter once every 15 seconds? A sense of proportion is necessary. It is not sensible to over-engineer a solution when a trivial implementation solves the problem effectively, and with no meaningful loss of efficiency. This is not Digital Signal Processing; this is not trying to do a real-time FFT, or time-sensitive response to a high-speed device, or a convolution algorithm of a hundred-megapixel full-color image. It's an alarm clock!

Of course, this is what led to the problems of the laptop power events needing to be noticed, but that is not nearly as complex as the alternative solutions were appearing. Besides, I had three days to write this whole thing before I left for a teaching gig, and my laptop is not an ideal development platform. Its screen is too small to see (I don't use it for development, so why buy an 8-pound brick that is too large to use on an economy tray table when I can buy a 3.5 pound little box that fits nicely in my carryon luggage, and can play Stargate SG-1 DVDs while I'm flying?)

The use of state machines

I'm a great believer in rule-based systems. They are easy to design, easy to write the detailed specifications for, easy to implement, easy to understand, easy to debug, and easy to maintain. I am not a great fan of deeply-nested if-statements. So I tend to favor simple switch statements for a lot of programming.

For example, the set of transitions when moving from one state to another can be interestingly complex. Here's the actual documentation as I wrote it in my source code:

/****************************************************************************
*                          CClockDisplay::SetAlarmState
* Inputs:
*       AlarmStates nextState: The desired state we wish to be in
* Result: void
*       
* Effect: 
*       Marks that we are in an alarm state. The action taken depends on what
*       state we were already in.
* Notes:
*               | Disabled | Armed   | Ringing  | Snoozing | <= Next State 
*      ---------+----------+---------+----------+----------+
*      Disabled |A:   -    |B:init   |C:   -    |D:   -    |
*               |          |->Armed  |          |          |
*      ---------+----------+---------+----------+----------+
*      Armed    |E:cancel  |F:cancel |G:sound   |H:   -    |
*               |->Disabled| [init]  | [ReRing] |          |
*               |          |         |->Ringing |          |
*      ---------+----------+---------+----------+----------+
*      Ringing  |I:cancel  |J:cancel |K:sound   |L:        |
*               |->Disabled| [init]  |[Rering]  |[Snooze]  |
*               |          |->Armed  |          |->Snoozing|
*      ---------+----------+---------+----------+----------+
*      Snoozing |M:cancel  |N:cancel |O:sound   |P:cancel  |
*               |->Disabled| [init]  |[Rering]  |[Snooze]  |
*               |          |->Armed  |->Ringing |          |
*      ---------+----------+---------+----------+----------+
*          ^
*          |
*      Current State
****************************************************************************/

In the interest of saving space, I don't usually include the function headers in these pages, but they're there, for every function I write, and I can quickly add them to the functions ClassWizard produces. These are not designed to be parseable by automated tools, mostly because I don't believe the documentation these tools produce are worth the powder to blow their backup floppy away. Such documentation concentrates far too much on what is essentially trivial detail, at the expense of giving a useful overview. It is either redundant with reading the code, or misleading because it gets out of sync, or irrelevant because you really don't care about the details unless you are reading the code, and architectural diagrams are far more useful. I put most automated documentation of this nature in the same class as flowcharts (something I haven't used as a tool in about 35 years).

Notice that this state table says that starting at a given state, and being asked to move to a new state, there are a set of actions that should be taken. The little notations explain what I want to do. The letters are used as commentary to quickly identify a piece of code. For example, here are a few excerpts from the code that implements the alarm notification event. The variable AlarmState holds the current alarm state, and nextstate is the state I wish to be in.

  void CClockDisplay::SetAlarmState(AlarmStates nextstate)
     {
      switch(MAKELONG(AlarmState, nextstate))
        { /* AlarmState */
         ...
         //****************
         // B:
         //****************
         case MAKELONG(Disabled, Armed):
            InitiateNextAlarmEvent();
            AlarmState = Armed;
            Invalidate();
            break;
         ... omitted code
         //****************
         // E:
         //****************
         case MAKELONG(Armed, Disabled):
            // cancel
            // ->Disabled
            ShutUp();
            CancelAlarm();
            AlarmState = Disabled;
            Invalidate(); // force a redraw
            break;
         ...
         //****************
         // H:
         //****************
         case MAKELONG(Armed, Ringing):
            // sound alarm
            // set next event for +ring delta
            SetTheNextEvent(SecondsToMs(ALARM_DELTA));
            SoundAlarm();
            AlarmState = Ringing;
            Invalidate();
            break;
          ...
         //****************
         // L:
         //****************
         case MAKELONG(Ringing, Snoozing):
             { /* snooze */
              ShutUp();
              SYSTEMTIME LocalTime;
              GetMyLocalTime(LocalTime);
              SetTheNextEvent(MinutesToMs(SNOOZETIME) - SecondsToMs(LocalTime.wSecond));
             } /* snooze */
            AlarmState = Snoozing;
            Invalidate();
            break;

This is orders of magnitude simpler than some complex set of strangely convoluted if-statements. I knew what I was writing, I could focus on exactly the problem at hand, the complexity of decoding the flow of control is trivial. It may be a bit bulkier than some "optimized" set of code, by some peculiar notion of "optimized", but this is the actual optimized code because it optimizes the dimensions that really matter: it came up quickly, is easy to understand, works correctly, was trivial to debug (virtually no debugging at all! Simple code is written correctly the first time), and is easy to maintain.

Managing Time Zone Information

I did not want to have to keep switching my processor clock whenever I crossed time zones. I wanted local control  of the time zone to be settable in my clock program.  This led to a new series of adventures about how time zone information is encoded.

One thing that is odd is that time zone information is encoded as the inverse sign of the offset from UTC (the new name for GMT). So if the offset of Eastern Standard Time is GMT -5:00, then the bias value is +5.00, which is the amount of time that must be added to the local time to obtain UTC time. So the normal way of thinking about time is backward from the way the operating system represents it.

Surprisingly, there seems to be no operation that enumerates time zone information built into the operating system. Perhaps I just missed it. But what I ended up doing was going to the clock and tediously copying all the time zone information for the huge number of time zones that exist in the world into a sequence of STRINGTABLE entries. Borrr...ring.

Because I was so bored with doing this, I decided to play around a bit with some arithmetic classes. I wanted a way to encode the time zone offset, including the applicability of Daylight Savings Time. And I wanted to use my existing CIDCombo class so I wouldn't have to invent something new. But it takes a DWORD value, and I wanted to encode two pieces of information: the DST flag, and the offset. In addition, it turns out to be useful to encode in this the offset into the table where it is found. A MAKELONG with LOWORD and HIWORD seemed an inordinately crude way to do this, and when I decided that life was a good deal simpler if I could keep the ID code in the field as well, it looked like the code would be really ugly. So why not use a C++ class for all of this?

I learned a few things about bit fields as well. Bit fields are not a very-well-specified feature of the C language, and allow a lot of latitude to the compiler writers. And I got done in rather badly by this in my first attempt.

To compress all this into a single DWORD/UINT value, I created the following class

#pragma pack(push, 1)
class TZData {
    public:
       TZData(DWORD d) { ASSERT(sizeof(TZData) == sizeof(UINT)); *(DWORD*)this = d; }
       TZData(BOOL dst, UINT nid, int b) {
          ASSERT(sizeof(TZData) == sizeof(UINT)); 
          DST = dst;
          id = nid;
          Bias = b;
       }
       __inline operator int() const {
           return *(int *)this; }
    public:
          WORD DST:1;
          WORD id:15;
          short Bias:16;
          
       };
#pragma pack(pop)

I had two situations that required packing the data. One which arose later was the need to take a 32-bit value and create a new timezone data object which copied it directly; this was necessary when copying a value out of the table, which had a DWORD data type. There was also a need to extract it as an int value, which lead to another nasty situation.

The first case was creating a data entry for the combo box initialization. Initially I represented it as a sequence of entries of the form

{IDS_GMT_MINUS_5, TZData(TRUE, IDS_GMT_MINUS_5, -5 * 60) },
{IDS_GMT_MINUS_6, TZData(TRUE, IDS_GMT_MINUS_6, -6 * 60) },
...

This had the annoying feature that I had declared the fields DST and id as UINT. The resulting TZData object was 64 bits, which did not fit. Of course, I got no errors in compiling this, and got some very strange runtime behavior. To check this, I added the two ASSERT statements shown above, and discovered that indeed, sizeof(TZData) was not the same as sizeof(UINT). Oops. A bit of fiddling revealed that the problem disappeared when I changed the types of DST and id to WORD. Note that the bias is a signed value.

When it came to extracting the value from a GetItemData call and casting it back to a TZData, I could not write

TZData bias = (TZData)c_TimeZones.GetItemData(c_TimeZones.GetCurSel());

The compiler simply refused to do this. So I had to create a constructor that took the DWORD value returned by GetItemData and create a data type acceptable to the compiler. This generated the constructor. But I could not create a set of internal casts that would compile. To get it to compile, I had to cast this to a DWORD *, then store by derferencing the DWORD * pointer!

In another case, I had to have a situation in which I converted the 32-bit value to an int value. The simple cast of (int)tz where tz was of type TZData would not compile. I probably could have used a union, but instead I wrote the typecast shown; I cast this to an int * pointer, then deferenced that pointer, and got the value I wanted. When I earlier tried to write

__inline operator int() const { return (int)this; } // Generates stack overflow

I got a stack overflow, because it applied the int cast over and over again. I felt rather foolish when I got the stack overflow and saw what I had written! It also suggested that it was long past my bedtime, so I made the fix and went to bed. The corrected code was

__inline operator int() const { return *(int *)this; } // fixes the stack overflow problem

This was half the problem. I looked at what I had written, -5 * 60, and realized that for some of the time zones it looked really ugly: for example, I was getting values such as 5 * 60 + 45, 9 * 60 + 30, and other really ugly-looking values. Yechh! Wouldn't it be nicer to have a datatype that had a constructor which was hours, minutes?

So I created a new class, HHMM, to represent this:

class HHMM {
    public:
       HHMM() { time = 0; }
       HHMM(int h, int m) { 
           ASSERT(h >= 0);
           ASSERT(m >= 0 && m < 60);
           time = h * 60 + m; }
       int HH() { return time / 60; }
       int MM() { return time % 60; }
       int Minutes() { return time; }
    public: // arithmetic operators
#define HHMM_INLINE __inline
       
       HHMM_INLINE operator int() { return *(int *)this; }
       HHMM_INLINE BOOL operator ==(const HHMM & t) { return time == t.time; }
       HHMM_INLINE BOOL operator !=(const HHMM & t) { return time != t.time; }
       HHMM_INLINE BOOL operator >(const HHMM & t)  { return time  > t.time; }
       HHMM_INLINE BOOL operator <(const HHMM & t)  { return time  < t.time; }
       HHMM_INLINE BOOL operator >=(const HHMM & t) { return time >= t.time; }
       HHMM_INLINE BOOL operator <=(const HHMM & t) { return time <= t.time; }
       HHMM_INLINE HHMM operator +(const HHMM &t)    { return HHMM(time + t.time);}
       HHMM_INLINE HHMM operator -(const HHMM &t)    {
                                                    return HHMM(time - t.time);}
       HHMM_INLINE HHMM operator -() {
                                     return HHMM(-time); }
       HHMM_INLINE HHMM operator --(int) { return HHMM(time--); } // postfix decrement
       HHMM_INLINE HHMM operator --()    { return HHMM(--time); } // prefix decrement
       HHMM_INLINE HHMM operator ++(int) { return HHMM(time++); } // postfix increment
       HHMM_INLINE HHMM operator ++()    { return HHMM(++time); } // prefix increment
  protected:
       HHMM(int n) { time = n; }
       int time;
};

This had some nice features. For example, I could make sure that the hours value was not negative, and the minutes were in the range 0..59. Now I needed to do arithmetic between values of this class. Unfortunately in C++ it is not possible to subclass scalar types. It would have been a lot easier to have simply write this as a public subclass of int and inherited the basic scalar operators.

I wrote functions to extract the hours and minutes from these, so I could simply apply the HH or MM methods to get the components if I needed them. If I needed the integer number of minutes, the Minutes method extracted that (which was easy, because that was the units used for the representation).

Then I created inline operators for the six comparison operators, binary addition and subtraction, negation, and pre and post increment. Note that postfix increment and decrement require a (int) parameter; this is just a piece of C++ magic that has to be known. Note that there is a protected constructor which takes an integer value; this is used by the increment and decrement operators to recast the int value of the time field back to an HHMM data type.

It's no fun to do a simple project if you can't learn something interesting along the way.

Rather than do some complicated adjustment of the time based on the computer's preset time zone and the desired time zone for the clock program, I greatly simplified the problem by converting the computer's time to UTC time, then converting UTC time to the desired time I wanted.

void CClockDisplay::GetMyLocalTime(SYSTEMTIME & LocalTime)
    {
     TIME_ZONE_INFORMATION tz;
     ::GetTimeZoneInformation(&tz);
     ConvertToDesiredTimeZone(&tz);

     SYSTEMTIME systime;
     ::GetSystemTime(&systime);

     ::SystemTimeToTzSpecificLocalTime(&tz, &systime, &LocalTime);
    } // CClockDisplay::GetMyLocalTime

void CClockDisplay::ConvertToDesiredTimeZone(TIME_ZONE_INFORMATION * tz)
    {
     tz->Bias = baseTZ.Bias;
     if(TZ.IsValid())
         tz->Bias = TZ.GetBias();
        
    } // CClockDisplay::ConvertToDesiredTimeZone

What I did here was obtain the current time zone information from the computer, convert it to the desired time zone by adjusting its bias value (using my GetBias method which negates the offset so the properly-signed bias is presented; that is, EST is GMT-5:00, so the value is stored in the table as -HHMM(5, 0) and GetBias returns the negation of this value. Then I get the current system time, and use ::SystemTimeToTzSpecificLocalTime.

Using WM_NCHITTEST

I didn't want a system menu. I didn't want a caption bar, a close box, or the like. But that led to some questions about how to drag the clock around. A bit of experimentation convinced me that the idea of a caption bar is deeply ingrained, and I found the absence of it confusing. If I, who wrote the app, found it confusing, then certainly anyone else would find it equally confusing. But using the small caption bar was unsatisfactory; I didn't want it visible when I was in full-screen mode. So I decided to simply fake it.

To fake it, I created a gap above the static control that defined the actual clock display. The dialog box was configured with no system menu and no caption bar. So what I did was implement a hander for WM_NCHITTEST. This is a question the system sends a window that says "where is the mouse now?". The response, which is the return value, is a value like HTCLIENT, HTCAPTION, and a bunch of other values. I had chosen to not make this window resizable, because I didn't want any borders. So values that would tell me that I was on a resizing border were not interesting to me. There is even an HTNOWHERE return value that basically says "the mouse is not anyplace you would care about".

What I did in the OnNcHitTest handler was see if the mouse was in the little border above the clock display. If it was, I returned HTCAPTION. This allowed me to grab the bar and have it behave like the expected caption bar; the DefWindowProc handler enters window-dragging mode when its query on WM_NCHITTEST returns HTCAPTION.

As an odd piece of behavior, I discovered that if I returned HTCAPTION for a maximized window, I could drag the maximized window around! So I initially returned HTNOWHERE if the window was maximized, but for reasons I will discuss later, this wasn't ideal either. So I ended up returning HTCLIENT instead. This had other interesting implications.

UINT CClockDlg::OnNcHitTest(CPoint point) 
   {
    if(IsZoomed())
       return HTCLIENT;
    else
       return HTCAPTION;
   }

The handling of the OnNcHitTest led to some confusion. I had wanted to handle double-click and right-button click in special ways. The right mouse button would bring up a menu, and a double-click would transition from small to medium to large to medium to small. The problem was that neither of these worked if the mouse was clicked in the caption bar.

Part of this was easy to deal with. For example, a double-click in the clock area went to the clock display window, which handled double-click like this:

void CClockDisplay::OnLButtonDblClk(UINT nFlags, CPoint point) 
   {
    GetParent()->SendMessage(UWM_TOGGLE_SIZE);
    //CWnd::OnLButtonDblClk(nFlags, point);
   }

That is, it has no idea how to handle a double click, so it reports it to its parent as a request to toggle the size. So that's fine. But what about the parent, which is actually the area of the simulated "caption bar"? Well, because WM_NCHITTEST will return HTCAPTION, then I have to handle it as a non-client double-click

void CClockDlg::OnNcLButtonDblClk(UINT nHitTest, CPoint point) 
   {
    SendMessage(UWM_TOGGLE_SIZE);
    //CDialog::OnNcLButtonDblClk(nHitTest, point);
   }

Note that the receiving window has no idea who sent the UWM_TOGGLE_SIZE message; thus it always behaves the same.

This worked fine until I maximized the window. My first attempt to prevent dragging was to simply return HTNOWHERE, but then double-clicks were not transmitted, because they were neither in the client nor non-client area. I could not return HTCAPTION, because this allowed for dragging, and the other choices were equally poor. So instead I returned HTCLIENT. Of course, this meant that double-clicks were no longer sent to the OnNcLButtonDblClk handler, but since I was returning the HTCLIENT code, I could add.

void CClockDlg::OnLButtonDblClk(UINT nFlags, CPoint point) 
   {
    SendMessage(UWM_TOGGLE_SIZE);
    //CDialog::OnLButtonDblClk(nFlags, point);
   }

I handled the right-button-down in a similar fashion.

void CClockDlg::OnNcRButtonDown(UINT nHitTest, CPoint point) 
   {
    SendMessage(WM_CONTEXTMENU, (WPARAM)m_hWnd, MAKELPARAM(point.x, point.y));
        
    //CDialog::OnNcRButtonDown(nHitTest, point);
   }
   void CClockDisplay::OnRButtonDown(UINT nFlags, CPoint point) 
   {
    GetParent()->SendMessage(WM_CONTEXTMENU, (WPARAM)m_hWnd, MAKELPARAM(point.x, point.y));
   }
   

Note that if I clicked the right button down in the HTCLIENT area this would generate a WM_CONTEXTMENU message with the parameters as shown.

Disabling the Screen Saver

What was odd about this need was that less than a week before I had to research this topic. So it was actually fairly straightforward (more straightforward than the problem I had been trying to solve). What I didn't want to do was wake up a 3:30am and find some nice screensaver image running, thus obscuring the time. This is not a desirable situation.

So when the clock is maximized, the screen saver is turned off.

To do this, you need to use the SystemParametersInfo call. To turn off the screen saver, use

 SystemParametersInfo(SPI_SETSCREENSAVEACTIVE, FALSE, NULL, 0);

To restore the screen saver, use the call

 SystemParametersInfo(SPI_SETSCREENSAVEACTIVE, TRUE, NULL, 0);

Of course, it is necessary to determine if the screen saver was actually active; this can be done by using the call

BOOL active;
if(SystemParametersInfo(SPI_GETSCREENSAVEACTIVE, 0, &active, 0))
    ... record the fact here

In the small or medium displays, I put the screen saver in the mode the user has selected; when in maximized mode, I turn the screen saver off. When I restore the screen, I set the screen saver mode to its original mode.

Multiple Monitor Support

Both my main development machine and my laptop are multiple-monitor machines. This adds some complexity to how I determined where the clock was. There are some intrinsic problems about screen resolution. For example, supposed I'm in 1280 × 768 resolution (the native resolution for my laptop). I place the clock down in the lower right corner. Then I change the laptop resolution to 1024 × 768, and restart the clock program. I have carefully remembered where the clock was: it was a small clock down in the lower right corner, whose lower right corner was at position 1280 horizontally. When I bring the clock up again, it is offscreen. And I can't get it back! (I first hit this years ago on Win16 when I lost a toolbox; I'd moved a file from one machine to another, and the second one had lower resolution. I had to edit the binary state file of the program to get the toolbox back!)

So what I do now is to make sure that when a window is restored, I make sure that (depending on the application) it is totally, or at least partially, on-screen. When it is a modeless dialog, and I don't want to force it to be fully on-screen, I force it to have at least its entire vertical caption bar area, and twice the caption bar height as caption bar width, available horizontally (either the left edge or the right edge of the caption bar, depending on the window placement). So I don't lose toolboxes and other dialog items. But the clock was trickier, and multiple monitors added more complexity.  For example, when testing it, I wanted to park it on the secondary monitor. So if I used my normal algorithm it would not work.

I also made some other decisions, such as I wanted the medium image displayed on a single monitor, rather than split across two monitors. So if the little clock was over on the right side of the left-hand monitor and I expanded it, leaving its initial point at the top left would split it across monitors. If I had a single monitor, the new image would be off-screen. So I added a lot of code to recompute the "anchor point" when there was an issue like this. The nice thing is that if I return the window to its small size, it goes back to where it was, rather than be anchored always at the top left corner.

To determine if the current clock is "on the monitor", I have to determine the rectangle of the monitor. Fortunately, this is easy. I used the ::MonitorFromWindow API call:

BOOL CClockDlg::GetMonitorRect(CRect & w)
    {
     HMONITOR monitor = ::MonitorFromWindow(m_hWnd, MONITOR_DEFAULTTOPRIMARY);

     MONITORINFO info = {sizeof(MONITORINFO)};
     if(::GetMonitorInfo(monitor,&info))
        { /* got it */
         w = info.rcMonitor;
        } /* got it */
     else
        { /* failed */
         w.left = 0;
         w.top = 0;
         w.right = ::GetSystemMetrics(SM_CXSCREEN);
         w.bottom = ::GetSystemMetrics(SM_CYSCREEN);
        } /* failed */
     return TRUE;
    } // CClockDlg::GetMonitorRect

Note that if this call fails, I revert to the size of the screen using the old-fashioned technique of knowing that 0,0 is the top left and ::GetSystemMetrics using  SM_CXSCREEN and SM_CYSCREEN give the coordinates of the lower right. The ::MonitorFromWindow API returns the monitor handle for the monitor which holds the largest part of the window; since I do this in the CDialog-derived subclass of the main application, the m_hWnd member is the application window. So the parameter value delivered.

The key thing here is to recognize that it is legitimate to have negative coordinates! The primary monitor has a coordinate of 0,0 for its top left corner. But other monitors in the system depend upon the user's selection of their relative position to the primary monitor to determine their coordinates. A secondary monitor could be to the left of the primary monitor, so its x-coordinates would all be negative. Or it could be above the primary monitor, making all its y-coordinates negative. Overall, you can't predict how the user has configured the monitors. I can even imagine three monitors in an L-configuration, or four in a T-configuration or inverted-T configuration, so doing a UnionRect operation (which presumes the monitors are in a rectangular configuration of some sort) would not work.

Creating an RTF file and using it as a resource

To create the help, I didn't want an external help file; this would only add complexity to the construction and distribution.  So I wanted to just use a rich edit control. This led to some interesting problems but they were actually interesting problems to address.

To create an RTF file that is compatible with a Rich Edit control, I used WordPad, which is just a rich edit control itself. You cannot create an RTF file that is usable in a rich edit control by using Microsoft Word, because it dumps tons of really uninteresting junk into the RTF file, none of which is usable by a simple Rich Text control.

To add the resource to VS6, I simply right clicked on the resources root and selected the Import... menu item. When it asked for the resource type, I just said "RTF" (as a string) and gave it an ID of IDR_HELP.

Streaming the resource into a Rich Edit control

In order to implement a stream, I need to pass a "cookie", which is a way that context is passed into the read callback function. In this program, I needed a pointer to the buffer, a length of the buffer, and a current buffer position. I called it a StreamPos structure. Note that I deliberately chose the pointer type to be an LPCSTR because I know that my resource type is a stream of 8-bit characters.

class StreamPos {
    public:
        LPCSTR base;
        DWORD pos;
        DWORD len;
        StreamPos() { pos = 0; len = 0; base = NULL; }
};

The callback function is supposed to read the next n characters from the input stream and deliver them back to the Rich Edit control. One of the parameters it receives is the "cookie" which in my case is a StreamPos. Note that since I cannot have an error in this simple reader, I do not need to return anything other than a success code, which is 0.

DWORD CALLBACK StreamCallback (DWORD_PTR cookie, LPBYTE pbuff, LONG cb, LPLONG bytesTransferred)
    {
     StreamPos * stream = (StreamPos *)cookie;
     
     int len = stream->len - stream->pos;
     if(len == 0)
        { /* done */
         *bytesTransferred = 0;
         return 0;
        } /* done */

     if(len < cb)
        { /* transfer all */
         memcpy (pbuff, &stream->base[stream->pos], len);
         *bytesTransferred = len;
         stream->pos += len;
         return 0;
        } /* transfer all */
     else
        { /* transfer some */
         memcpy (pbuff, &stream->base[stream->pos], cb);
         *bytesTransferred = cb;
         stream->pos += cb;
         return 0;
        } /* transfer some */
     return 0; // 0 =>success
    } // StreamCallback

This is the function which loads the resource.

BOOL CHelp::LoadHelpText()
   {
    HRSRC resource = FindResource (AfxGetInstanceHandle(), MAKEINTRESOURCE(IDR_HELP), _T("RTF"));
    if(resource == NULL)
       { /* failed */
        ASSERT(FALSE);
        return TRUE;
       } /* failed */

    HGLOBAL h = LoadResource (AfxGetInstanceHandle(), resource);
    if(h == NULL)
       { /* failed load */
        ASSERT(FALSE);
        return TRUE;
       } /* failed load */

    LPVOID p = LockResource (h);
    if(p == NULL)
       { /* failed lock */
        ASSERT(FALSE);
        return TRUE;
       } /* failed lock */

    StreamPos cookie;
    cookie.len = SizeofResource(AfxGetInstanceHandle(), resource);
    cookie.base = (LPSTR)p;

    EDITSTREAM es = {
                     (DWORD_PTR) &cookie,
                     0, 
                     StreamCallback};

    c_Text.AutoURLDetect(TRUE);
    DWORD mask =c_Text.GetEventMask();
    c_Text.SetEventMask(mask | ENM_LINK);

    c_Text.StreamIn(SF_RTF, es);

    return TRUE;
   } // CHelp::LoadHelpText

Supporting the Rich Edit 2.0 control

To support the Rich Edit control it is necessary to call AfxInitRichEdit2 in the InitInstance handler. To support this, I wrote a little interface program called rich2.cpp.  Note that if this file is compiled in VS7, it will produce nothing, because AfxInitRichEdit2 is already defined in VS7. (Or so I thought; it turns out that not all the methods of RichEdit2 are supported in VS.NET.  You begin to wonder what these guys are doing, other than destroying the usability of the interface.  The system would be a lot better if people spent time worrying about what programmers need, instead of catering to the ego trip of an incompetent designer).

In order to compile this, note that the MFC source file afximpl.h is being included. The directory in which this appears does not normally appear in the include search path. Unfortunately, Microsoft does not provide a convenient macro for search paths in VS6. So instead, you will have to hand-edit the project file to set the include path to the place where you have installed Visual Studio 6. In my case it is c:\Program files\Microsoft Visual Studio\vc98\mfc\src.  Note that in VS.NET they finally got smart about this, and the macro $(VCInstall) is actually defined, and the VS.NET version of the build actually includes this macro definition in the project definition.

#include "stdafx.h"
#if _MFC_VER < 0x0700
#include <afximpl.h>
BOOL PASCAL AfxInitRichEdit2()
 {
  _AFX_RICHEDIT_STATE* pState = _afxRichEditState;
  ASSERT (pState->m_hInstRichEdit == NULL);
  if (pState->m_hInstRichEdit == NULL)
     pState->m_hInstRichEdit = LoadLibraryA("RICHED20.DLL");
  return pState->m_hInstRichEdit != NULL;
}
#endif // _MFC_VER < 0x0700

The corresponding header file rich2.h is

#if _MFC_VER < 0x0700
BOOL PASCAL AfxInitRichEdit2();
#endif // _MFC_VER < 0x0700

This allows the initialization to take place. However, I also wanted to use some Rich Edit 2.0 features not otherwise available in MFC6, so I added the following methods to the subclass I created. Note that these are also under the _MFC_VER conditional switch.

class CRTFHelp : public CRichEditCtrl {
    ...
public:

	__inline BOOL AutoURLDetect(BOOL mode) {
		ASSERT(::IsWindow(m_hWnd));
		return ::SendMessage(m_hWnd, EM_AUTOURLDETECT, (WPARAM)mode, 0); }

	int GetTextRange(int start, int end, CString & s);

	__inline BOOL SetOleCallback(LPVOID p) {
		ASSERT(::IsWindow(m_hWnd));
		return ::SendMessage(m_hWnd, EM_SETOLECALLBACK, 0, (LPARAM)p);
	}

where the implementation of GetTextRange is

int CRTFHelp::GetTextRange(int start, int end, CString & s)
   {
    if(start == end)
       { /* nothing */
        s = _T("");
        return 0;
       } /* nothing */
#ifdef _UNICODE // If we are using RichEdit 2.0 or greater then we need to use this conditional to get it to compile
    TEXTRANGEW tr;
#else
    TEXTRANGEA tr;
#endif
    tr.chrg.cpMin = start;
    tr.chrg.cpMax = end;
    tr.lpstrText = s.GetBuffer(end - start + 2);
    int result = ::SendMessage(m_hWnd, EM_GETTEXTRANGE, 0, (LPARAM)&tr);
    s.ReleaseBuffer();
    return result;
   } // CRTFHelp::GetTextRange

Note the odd conditional. This is because MFC forces the Rich Edit control version _RICHEDIT_VER to be fixed at 0x0100 (version 1.0) which causes the platform SDK to compile only the ANSI version of TEXTRANGE. However, the TEXTRANGEW structure is explicitly available, and I then used TEXTRANGEA for symmetry.

Like the AutoURLDetect and SetOleCallback methods shown above, this is essentially just a wrapper around a SendMessage call.

WM_GETTEXTLENGTH is incompatible

The Rich Edit 2.0 control does not return the actual character length for a WM_GETTEXTLENGTH message.  Instead, it is necessary to use the EM_GETTEXTLENGTHEX (that's Length-Ex, not Lengt-Hex) using the GTL_NUMCHARS option.

#if _MFC_VER < 0x0700
long CRichEditCtrlEx::GetTextLength() const
   {
    ASSERT(::IsWindow(m_hWnd));
    GETTEXTLENGTHEX gt;
    gt.flags = GTL_NUMCHARS;
#ifdef _UNICODE
    gt.codepage = 1200;
#else
    gt.codepage = CP_ACP;
#endif
    return (long)::SendMessage(m_hWnd, EM_GETTEXTLENGTHEX, (WPARAM)&gt, 0);
   } // CRichEditCtrlEx::GetTextLength
#endif

The choice of the magical constant "1200" is based entirely on the documentation of the GETTEXTLENGTHEX structure, which says "[codepage] is CP_ACP for ANSI Code Page and 1200 for Unicode".

Launching a hyperlink

I need GetTextRange to handle the hyperlinks. To enable hyperlink recognition, I informed the Rich Edit 2.0 control that I wanted this feature by using AutoURLDetect with a TRUE argument. Then I had to recognize the range that contained the link.

To cause this to happen, I had to enable for EN_LINK notifications

DWORD mask = c_Text.GetEventMask();
c_Text.SetEventMask(mask | ENM_LINK);

Then I had to explicitly add the ON_NOTIFY handler because the Visual Studio 6 ClassWizard does not understand the EN_LINK notification:

BEGIN_MESSAGE_MAP(CHelp, CDialog)
    ON_NOTIFY(EN_LINK, IDC_TEXT, OnURLLink)

The handling is a bit tricky. The problem is that the link notification is sent as long as you move the mouse over the hyperlinked text. So I had to set it up so that I could detect the mouse-down event, and not accidentally trigger multiple launches if I was dragging across it. So I use GetKeyState to see if the left button is down, and if it is, I get the URL that this notification is telling me about, and launch the browser using ShellExecute.

void CHelp::OnURLLink(NMHDR * pNotifyStruct, LRESULT * pResult)
    {
     SHORT state = GetKeyState(VK_LBUTTON);
     if(state < 0)
        { /* click on URL */
         ENLINK * enlink = (ENLINK *)pNotifyStruct;
         int start = enlink->chrg.cpMin;
         int end = enlink->chrg.cpMax;

         CString link;
         c_Text.GetTextRange(start, end, link);
         ShellExecute(m_hWnd, _T("open"), link, NULL, NULL, SW_RESTORE);
        } /* click on URL */

     *pResult = 0;  // tells control to handle it. This creates the little
                    // pointy-hand cursor (setting this to 1 suppresses
                    // the change of cursor shape)
    } // CHelp::OnURLLink

OLE Object insertion

I added a variant of the project by Mike O'Neill, which can be found in http://www.codeproject.com/richedit/COleRichEditCtrl.asp. For all the details of how the fully-general solution works, consult that article.

 

[Dividing Line Image]

The views expressed in these essays are those of the author, and in no way represent, nor are they endorsed by, Microsoft.

Send mail to newcomer@flounder.com with questions or comments about this web site.
Copyright © 2006, The Joseph M. Newcomer Co. All Rights Reserved.
Last modified: May 14, 2011