Introduction
This is one of those small quick projects from someone who uses WTL regularly. There is nothing ground breaking here, I knew from the beginning it would be more Windows application programming style than dipping into DirectX or something more appropriate to keep things simple and focused on WTL. I always found it interesting to see how someone actually went about developing a project. So here is an account of the design and implementation of a program to improve your typing in C++. You can use this to show your kids all the wrong ways to develop a program. Yet there may be something of worth here. Who knows? Oh the mystery!
Background
So I�m on the curb in a pretty slummy part of Mexico City, feeling a little down on my luck. Yeah you know how it is. Replaying things back in my head. What can I do to get pushed up to a 150K a year salary? Shoot I�m only making 6.5K. Then as one of those stray brown Dobermans passed by a heavily spray painted wall, the revelation begin. It�s because I don�t type fast enough.
Yeah that makes sense, whenever my boss comes by and sees me kind of trickling out some syntax at a decent velocity, he looks upon it as a measure of my productivity. While some of the old school guys can type out those memorized quick sort routines at the speed of secretaries. He�s thinking �Yeah the kid is alright, but now Jose, he�s my backup man. He�s the Guns of Navarone. The guy has to be like sixteen times more productive than Mark over here. I should give Jose a raise, show him I appreciate him.� Yeah I can read it off his grim little face as he apathetically lets out a �good work� directed at me and leaves before I can look up.
I�m not here to save the people from misconceptions, or to educate upper management. That�s too much effort. I�ll just learn to type faster and I�ll be seeing the raises in no time. And so now I�m into this all motivated. And then I start checking out the typing programs. And all that stuff is great for learning how to type English but I�m not getting much practice out of my pointer to member vocabulary (->).
So as an engineer I take to the �Well I�ll just build a better typing program� mentality. But that takes creativity, discipline, motivation and a little of some forth thing. So what do I usually do when I need inspiration? I don�t know, it doesn�t usually warrant much need but I see guys in movies taking long walks or scenic motorcycle rides. But I�m thinking it would help if I got some tacos. A jar of horchata and 8 tacos al pastor later, I had enough ideas to catch Microsoft off guard.
Step 1 - Designing
This is almost always done on my bed listening to music. First thing I do is, organize all the ideas I have to make the whole problem more concrete. I do that by coming up with a sentence for the problem. I came up with �To help a programmer type faster when programming�. Then from that I come up with features that would support that in classic requirements form.
Requirements:
- Allow a person to measure their typing speed and keep records.
- Not be boring.
- Be able to use it at short intervals. (Programmers are busy.)
- Use custom word lists.
Then from here I start drawing out the program�s main windows and what it�s going to look like. Unless you�re designing the next Apache, this is the most important part. The philosophy being that the interface is how your user interacts with your software and that it could be really smart and great but it�s the personality the interface portrays that wins the user over. That doesn�t mean you have to make it look all slick with skins, that would just be part of the statement. There are a lot of pretty people in this world without many friends. Like WinAmp3.
So I�m drawing out the windows, crossing stuff out and adding controls for different features. This helps to visualize the final product. Don�t be afraid to cross things out. Paper is great in letting you get dialogs up very quickly. You can cross stuff out, use arrows, write items in lists without having to call InsertItem
. Just try to get the message across. After I have most of the windows drawn, I start trying to divide data into groups and then come up with good names for the main classes. One thing I can�t stand is having to go back and rename files and classes after they are in use. Coming up with good class names is hard but important. You don�t want to be working with CSimpleAbstractGameTypingInformationDlgEx
s, so I try to get at least the names right.
So after about 40 minutes I have the windows I want, I have a pretty good vision now of the final product. At this point, you could take it to the next level and start writing a functional spec, a schedule and then UML diagrams. This is a small project so I�m going to live dangerous and jump into prototyping without proper planning.
But just before that I like to give the program a good name. An identity. Luckily I come prepared with a huge list of program names I have saved for when I start a new project so that it doesn�t halt a new project. The names: Code Cookies, Turtle Death Squad and Fish Heat were all strong. But I ended with Lowercase Cause. Now that�s a name with spunk, with a statement. A true contender in the highly lucrative typing tutor software industry.
The program I came up with was this. Windows with text pop up, you start typing their text and when you are finished, hit enter and they disappear. Brilliant! I don�t have to do anything graphical, I let windows be windows. There will even be a mode, check this out, survival. Where the windows will disappear if you don�t type them away quick enough! Oh man the excitement.
Step 2 - Implementing
Day 1
Here is where I start designing the dialogs. This is how I start off. Laying out the windows I�m going to be working with mainly. I don�t bother lining things up or getting the flow right, I just want to see what has what. Adjusting tab stops is straight out of the question at this point. This is just helping me envision what I�m going to be creating. Understanding the problem.
Once that is finished and I can imagine how it�s going to work, I start converting the information into classes. I called this class game_session
and realized that it was going to need counts of characters typed incorrect and correct to get statistics. I want characters per minute and accurate characters per minute. I put in enough just to make it work, I know that down the line I�m going to want more features and statistics but I leave them out for upfront simplicity. I want something up and going as quick as possible. game_session
is finished. Very basic.
class game_session
{
public:
game_sessiong() { reset(); };
void init(HWND hwnd);
void reset();
void increment_valid_characters();
void increment_invalid_characters();
void increment_closed_windows();
void set_timer();
void start();
void end();
bool is_over();
BEGIN_MSG_MAP(game_session)
MESSAGE_HANDLER(WM_TIMER, OnTimer)
END_MSG_MAP()
LRESULT OnTimer(UINT , WPARAM ,
LPARAM , BOOL& );
private:
HWND _hwnd;
int _total_characters;
int _correct_characters;
int _closed_windows;
CTime _start_time;
CTime _end_time;
long _timer;
};
Now I quickly get WTL CDialogImpl
skeletons up for CInputDlg
, CMainDlg
, and CMenuDlg
. I do this by using VC++ to add a class, and then I remove the constructors and destructors. Then all you need for the properties window to begin filling in events is the following:
class dialog1 : public<CDialogImpl, dialog1>
{
public:
enum { idd = IDD_DIALOG1 };
DECLARE_MSG_MAP(dialog1)
END_MSG_MAP()
}
You actually don�t even need the DECLARE_MSG_MAP
, VC++ will add that if you don�t have it when you add an event. However, if it�s missing when you try to compile it, you�ll get a �cannot initialize abstract class� error. So out of preventive habit I start the message map. After that I quickly get it to the point where they can open each other.
Nothing works right now, all I have is a skinny class definition that�s not being used and three dialog classes which do nothing except show each other. This is the perfect setup, this is what I want. This is a good foundation. I don�t like to be working on code for too long without testing.
So voil�, this is what it looks like so far, and what I have in my head. I have great ideas for how it�s going to look like in the end but I don�t like playing them out in the beginning. I find it better to get something out first and then slowly shape it over time instead of trying to tax my mind with the full final complete UI design up front. I�ve tried this before and it always comes out wrong and I�m so sure on the design and everything in the beginning because of all the UML, that it becomes inflexible. My new philosophy, in which I achieve better results, is start out as simple as possible and then modify when needed. Kind of a Kent Beck style.
CMenuDlg
: This is where a user comes to pick a new game. They can slide the difficulty up. There�s a survival mode, I also have in mind a timed mode but I didn�t put it on here because I was taking too much time trying to make it look cute. The list is for the phrase lists for requirement 4: use custom word lists. Nothing here works except the "New Game" button which loads up CMainDlg
.
CMainDlg
: A textbox on the right where a user types the text to be measured on. The list to the left is where the statistics will go for things like CPM (characters per minute) and things like that.
CInputDlg
: These will be the phrases that popup waiting to be typed away. There is a timer for survival mode and a percentage bar for survival mode too.
This is where I start visualizing how information is going to move. Is the CInputDlg
going to handle the text sent to them? Should game_session
be a singleton or passed around? Things of that nature. At this point, I add OnClose
and OnInitDialog
events for each dialog, OnInitDialog
s attach any member controls and OnClose
s just close themselves or the program.
CMainDlg
is the frame for the application but creates itself invisible and immediately displays CMenuDlg
. CMenuDlg
is passed a game_session
object which it will fill in on New Game. If CMenuDlg
is closed, the application terminates. CInputDlg
s are created from CMainDlg
.
I realize how much simpler it would be to handle everything in CMainDlg
and use it as the dialog which receives all the text from the other CInputDlg
s. I write SpyCharMessage
to be called for each message in PreTranslateMessage
, this will modify the message so that our main edit control receives text even if they try to get focus from one of the client dialogs. Perfect, everything will be handled in CMainDlg
, this is good because how else would users control which window gets the text? By clicking or Alt + tab? That�s no good. This way they can enter text and CMainDlg
can compare their text to see if they match any of the windows out there. So we got:
if(pMsg->message == WM_CHAR)
{
HWND edit_hwnd = GetDlgItem(ED_TEXT);
std::string edit_text = hf::get_window_text(edit_hwnd);
if(pMsg->hwnd != edit_hwnd)
{
::SetFocus(edit_hwnd);
pMsg->hwnd = edit_hwnd;
}
}
If we lose focus, we regain it on a WM_CHAR
and the message that didn�t get sent to it will be redirected anyways. Now at this point what I want to do next is get some basic CInputDlg
s up and handle WM_CHAR
s to send to the proper CInputDlg
. So I modify CInputDlg
to accept a string through the constructor, the phrase, and add a set_text
function for defining the text that has currently been typed. I write a function in CMainDlg
to fire off a CInputDlg
and keep a reference of it in a list. The next hour was spent in confusion. I mean what could be more simpler right? Have a WM_CHAR
handler to iterate through the windows that are shown and set the text of what�s typed if it matches at least a little bit. For example, if you have a window with �abc� and you type �a� that matches because a is the beginning of �abc.� So I have a loop trying to find the typed text in the phrase string at index 0 (the beginning), if it doesn�t find it in any window, it chops off the last letter of the typed text and tries again. This is for incorrect spellings of a phrase, so that if you type �ad� when the phrase is �abc�, �ad� will still hit because the a is good, it assumes you mistyped b with d (which you did if you are aiming at abc).
std::string typed_text = hf::get_window_text(_edTyped);
bool match_found = false;
for(int compare_index = typed_text.size();
!match_found && compare_index >= 0; compare_index--)
{
input_dlg_list_t::iterator i;
for(i = _input_dlgs.begin(); i != _input_dlgs.end(); i++)
{
CInputDlg* current_dlg = *i;
std::string search_text = typed_text.substr(0, compare_index);
if(current_dlg->get_text().find(search_text) == 0)
{
match_found = true;
current_dlg->set_text(typed_text);
}
}
}
So in the beginning I had this loop in the SpyCharMessage
. The loop was fine, however I wasn�t getting the last character typed. The message would hit but get_window_text
wouldn�t pick up the current text, it would always be missing the last letter. So I�m thinking oh alright, PreTranslateMessage
right, the message hasn�t been processed so that hasn�t been entered into the text box so thus I can�t get it yet with get_window_text
. Alright I�ll call DefWindowProc
, doesn�t work. Alright I�ll move it into a WM_CHAR
handler of the text box.
In order to do this, you have to derive from CEdit
and then subclass or use CContainedWindowT
and subclass which is more convenient as it will reroute the message to your parent�s ALT_MSG_MAP(X)
, you get to specify X
through CContainedWindowT
�s constructor. So define CContainedWindowT<CEdit> _edTyped;
and then initialize it as _edTyped(this, 1);
. Now you have your own ALT_MSG_MAP
for the CContainedWindowT
in the parent, no need for a new class. Still doesn�t work, I�m calling DefWindowProc
, I can�t figure this out. Check deja.com, nothing of worth. Then when debugging I notice that the character typed is already displayed visually in the text box when I hit my breakpoint inside WM_CHAR
. Oh, I bet it�s this get_window_text
function I wrote. Yeah it is, off by 1 error strikes again at the kid that never learns.
So now it�s working great, you can type the characters and watch them appear one by one under the CInputDlg
even though they are being sent to CMainDlg
. Each time you enter a letter, it checks to see if it was incorrect or correct. It determines this by checking to see if the text you have currently is a perfect substring of one of the windows. I made sure to ignore keys such as backspace and the arrow keys because they would add to the correct letters if not checked. They could have a correct substring and then start typing backspace then a letter then backspace and a letter and generating WM_CHAR
s all the while due to always having a correct substring, even if you are never getting anywhere. When you hit Enter, it compares the text with the phrase texts and if they match, it closes that window through DestroyInputDlg
.
The problem now is that all the dialogs show up in the same corner, I have to manually drag them out to see them and to type them out. I write CMainDlg::CalculateInputDlgStartUpRect
which requires CInputDlg::GetSize
and a hf::are_intersected
function. I take the are_intersected
function from the net and write GetSize
, which just called DrawText
with DT_CALCRECT
to get the size of the string passed in. CalculateInputDlgStartUpRect
gets a random position by iterating through the existing CInputDlg
s, trying to make sure that no one is going to overlap someone and that it�s not off the edge. Now CInputDlg
just needs to resize its controls correctly. I derive it from CResizeDialog
and add the resizing map to allow this.
Day 2
The input dialogs have an ugly proportional font so I set out right away to fix that. I hate creating HFONT
s in each dialog so I create it once in a singleton named settings
. Next I begin working on the statistics part of the program. Games will be timed and so I need to get a little tricky. I want game_session
to handle that logic so it will have its own timer checking for if its time limit has been reached in a timed game or a million other things it needs to keep track of. (Note: in the end, the timer didn't really do anything but check to see if it had died yet. Lesson, don't complicate the design for flexibility that isn't needed. It wasn't all bad as it helps decouple and makes game_session
more independent.) I have two options, I can call SetTimer
with a TimerProc
which will require that I make everything static. Which doesn�t sound like too bad of an idea since really you�re only going to have one session going on at once. Or I can call SetTimer
on an HWnd
with an ID. To do this I need to keep track of an HWnd
and have CMainDlg CHAIN_MSG_MAP_MEMBER
to _current_session
. I chose the latter because it seems more natural to keep _current_session
as a non static class so you can pass it into CMenuDlg
. Now that the timer is ready we can begin to calculate CPM.
Now I write InitStatistics
which just adds items to the listview and sets it up. These items are my �columns� and I set their subitem to the information associated with the item.
Now it�s a simple matter to write get_cpm
and get_awpm
and then UpdateDisplay
. Now it�s looking like a real typing program, with the CPM and AWPM and accuracy. However these numbers are very jerky varying a great deal from update to update. I also really have no idea what is a good CPM. One that will give me a raise.
It�s time for breakfast so I get some papaya and a tamale and read about WPM (words per minute) while eating. Then I found out that WPM is how many words of 5 characters you can type a minute. Aww man, I had no idea. I thought they would somehow count the words they are typing and then divide into the time typed and that was WPM. And that it was all relative to what you were typing. That�s why I was doing characters per minute, how would I cut something like _lvwStats.SetItemText(COL_CPM, 1, hf::int_to_str(_current_session.get_cpm()).c_str());
into words without writing an article on just that?
With this new knowledge, I replaced all the CPM and ACPM related with WPM and AWPM.
Now what was next was waiting until the first key is pressed before calculating WPM. This is done by not starting the game until OnChar
is called which will start it if it�s not started. When started, the _start_time
is initialized to current time and this is used to calculate WPM. There was something unsettling about watching the dialog come up with a WPM of 0 and the seconds start counting. You feel cheated. Like you missed the gun.
Next I begin the whole lifetime concept with CInputDlg
s which involved a new _life
member and a modified constructor which takes in the life. CInputDlg
itself has a timer which just counts down. It doesn�t close itself however because CMainDlg
must check for it being �dead� and destroy it as well as the very important remove it from its internal list so that it isn�t one of the windows to be checked for when typing.
Then I added simple things like having windows come up if a window gets closed and moving the main window to bottom middle. Then I spent the evening overhauling the dialogs now that it�s coming together, drawing icons and eating tacos.
Day 3
I�m realizing that the CInputDlg
s are pretty weak because if you�re typing fast you don�t have time to go look at the timer on the right, nor do you have time to go fishing out each dialog for the lowest time. So I began making them fade into another color. Also I don�t like it how when you mistype something you can�t always tell. Once the text no longer matches, it should be another color.
I begin writing my own custom controls to put the text in a different color when it�s wrong. I�m comfortable writing my own custom controls especially ones these simple. It�s just a Rectangle
GDI call and then a DrawText
but I could have used a RichEdit or probably even two statics with the WM_CTLCOLORSTATIC
message. After that and playing with the fade color, I decided to remove the static with the count down and instead put it as the title bar text.
Now that the loop and CInputDlg
s are pretty solid, I need to load lists. All this time, it would be the same text as it was hard coded, but it was easy to drop in a function to get that text. So phrase_list
was created which parsed a text file into phrases and kept them in a list. phrase_list
had the ever important get_random_phrase
function which made everything easy. Now CreateInputDlg
will pick a random phrase trying to make sure it hasn�t already been used. Then since the file path for the phrase_list
was hard coded I added CMenuDlg
's ability to fill a list of phrase files from the current directory and modified CMenuDlg
to accept a phrase_list
as an argument. CMainDlg
now gets its phrase_list
from CMenuDlg
where the user chooses.
I now begin working on high_score
and dialogs associated with high scores. I left this for last because it is the least important part and also because I never look forward to parsing. But it was pretty straightforward, pipe delimited text. Only one high score is allowed per game theme. When you finish a game, it creates a high_score
object and then compares it to the high_score_manager
using is_high_score
. If true, it simply adds it and saves. CHighScoresDlg
creates a tab for each phrase file and works by setting the filter of CHighScoresListCtrl
. CHighScoresListCtrl
is just a simple derivative of CListViewCtrl
which handles no messages, it merely adds a couple of functions to decouple it with its parent. The filter is just what phrase file is associated with the high score. It then deletes all items and selectively calls Add
for each item that matches the current phrase file.
Finished! Now I�m just testing and playing with it for a while. I notice right away what a pain it is when you are going for a window and its lifetime goes out before you can kill it. You are there with text and it�s recording everything as wrong. You must quickly delete all the text and start with a new one. I�m wondering if Enter should just clear the text whether it's good or not. Then I think naw, it�s not like that in the IDE, you have to delete it all manually some how. Yet there shouldn�t be much penalty for trying to type out the text and not making it. So I at least wanted Ctrl + a to work.
These required me to make a new class CEditExCtrl
from a CEdit
to handle the Select All code. I then had to add code for the acceleration table and add Ctrl + a for Select All. Now you can use Ctrl + a for Select All.
I also notice that the logic of the lifetimes doesn�t seem right, so I investigate that. Before I would simply calculate how long it would take to type that phrase and multiple it by how many windows are up. I was wrong in multiplying it by how many windows are up. It results in unfair time for any windows that aren�t of average text length, either being a time below or above the average. The fix to this involved some reengineering to allow an average length member into game_session
which would be used for lifetime logic. This helped the lifetimes feel more natural for the difficulty.
So now the finishing touches, I start editing some sounds to use in the program. I sprinkle in PlaySound
s and disaster! PlaySound
won�t play two sounds at the same time. Aww man, I could have sworn, it did. This sounds horrible and I worked somewhat hard on the sounds so I didn�t just want to try.
Today was a long day and it�s pretty late so I figure I�ll just gather data. I check the usual suspects, CodeProject, MSDN, deja to see if anything can be done to play two sounds at the same time. And everyone is pretty much like yeah, use DirectSound. Shoot, so much for not using anything complicated in this project, but I figure well I better bite the bullet. So far, requirements 1, 3 and 4 are met. But a typing program without sound is boring. I lay down to sleep while the 154 MB DirectX SDK downloads. I already have code samples ready for me in the morning.
Day 4
Waking up but what do I find on the screen? A finished SDK download you may say but no oh my brothers. It was the intriguing coincidence of a latest best pick with the title CWaveBox with a description of �Multiwave player ('Waveform Audio Interface' PCM wave wrapper)�. This was crucial for me and exactly what I needed, a simple class I dropped in and allowed me to play multiple .wav files at the same time.
This was great and I put on my best Iggy Pop records. �El D�a de los Reyes Magos� came late this year, but they visited me in the night. I read that this class could make what I needed so, and the demo was promising.
Yet code transitions never go smoothly. At first I was unable to load my Waves with CWaveBox
, I quickly realized that it was checking for certain bits set in the Wave to let it know that it was a Wave that mine didn�t have. I took the check out and it worked, they were playing! No way, it can�t be this easy, it�s not even 6 a.m. yet. So I added a singleton for sound, sound_system
and got all my Waves together and then played them based on what key you pressed.
Then I started noticing that all the sounds had this pop or crackling at the end which sounded pretty bad. I didn�t cut them like that, and all my other programs don�t play them with pops. So I investigate that, with a minor false path. Reading that it is not double buffering that causes pops and crackling. I don�t know much about sound programming but I guess that it was double buffered by looking at the code. Then I started realizing that it wasn�t being read correctly.
So I opened the Waves in a hex editor and realized that they had extra information in the beginning that CWaveBox
wasn�t expecting/aware of. Since my $50 shareware Wav edit program puts this random information there, I kludged CWaveBox
into working on Waves of my type by adjusting where to read the data block coming in.
I spent the rest of the day tweaking the sounds and working on the flamboyant CAboutDlg
. Making phrase files and refactoring. I also created a Python script really quick to generate the "Intense Cartoon Cursing" phrase file. I hope you enjoy my work.
Aftermath
I've only been using it a couple of days but the "Intense Cartoon Cursing" file is really helping me out. I think my boss is noticing because he was just watching me type (I was doing a heck of a lot better than before) and then I looked up. While still typing with one hand, I took a sip of water and then floored it up to a new comfortable WPM. He just kind of made this "Whoa" expression and walked out. Today he brought me some pirated DVDs he got outside of metro balderas. I'll keep you guys posted.
References
Uses CWaveBox
by zenith__ - CWaveBox - WAI wrapper for playing PCM multiwaves, useful in game development.
History
- Wednesday April 20th, 2005: Created.