Make your own ActiveX Control Make a new project, but this time choose MFC ActiveX control as the project type, instead of MFC Application. Name the project StopLightX. [ The screen shots below unfortunately have it named Stoplight2004.] Accept all the defaults. The plan is to make a stoplight, complete with blinking lights, that can be used as a control. Examine Class View and File View to see what classes and files have been created for you. Note the .idl file and the classes, CStopLightXCtrl. This is where the properties and methods of our control will be specified. The .idl file is not a C++ file. IDL stands for Interface Definition Language. This is the language in which interfaces are specified. An interface consists of properties, methods, and events, and specifies how other programs can communicate (in both directions) with the program that implements that interface. In Class View, right-click CStopLightXCtrl. You will see a choice Add Event. Don’t choose it yet, just notice where that choice is. Now notice that you also have a node labeled StopLightXLib. Expanding that node you should see a node labeled _DStopLightX. Right-click that node and notice that you have an option Add Property. Here’s a screen shot (with a slightly different project name) So now, when the time comes, you know how to add events and properties. The Design Phase What properties, methods, and events will we need to specify, control, and interact with a stoplight? Properties that come to mind are the three colors of the lights, the size of the bounding rectangle, the diameter of the lights, possibly more numbers to specify the spacing of the lights, etc. (but for simplicity we will use only the bounding rectangle and diameter of the lights); then we need properties that tell us which light is on (or which lights are on). That leads us to wonder whether we want to always have exactly one light on (in which case it would do to have a property that tells which light is on), or whether we want to be able to have them all off or more than one of them on (in which case we need more properties than one). For simplicity, let’s go for having exactly one light on at all times. Here “on” includes the possibility of blinking. Now we need the blink time, in milliseconds. We assume it will be the same for all lights. If it is zero that means the light does not blink. So we need the following properties: LightDiameter RectangleWidth RectangleHeight TopColor MiddleColor BottomColor WhichLight BlinkTime Since we also want to demonstrate how to program using events, we want to give the user a way to interact with the control. Let’s have the control fire events TopLightClicked, MiddleLightClicked, and BottomLightClicked. That completes the design. Now for the implementation. Defining the Properties and Events Right-click DStopLightX in Class View and choose Add Property. [You have to expand a node first to find DStopLightX, like this: Add the three color properties; here is an example: You will see that you have a choice whether to implement properties using member variables, or using Get and Set methods. I chose member variables. Add the other properties, using the types OLE_XSIZE_PIXELS and OLE_YSIZE_PIXELS for RectangleWidth and RectangleHeight, and OLE_XSIZE_PIXELS for LightDiameter, and SHORT for WhichLight and BlinkTime. Just so we try both ways, choose Get/Set methods for these last two properties. You can expand the node in class view to see that your properties are there: Now build the program. You will see that the last step in the build process is “performing registration”. This will not work in the open labs, where you do not have permission to modify the registry. Therefore, you will not be able to duplicate this demo in the open labs, but it should work at home. If you try to run your program, you’ll see that you cannot. It is not a standalone program, it is an ActiveX control, meant to be used by another program. We will see how to test it later on. So far, it wouldn’t do anything anyway. Right-click CStopLightXEvents and choose Add Event. Then add the three events. None of these events needs a parameter so we don’t enter anything in the parameter box. Note, this time it is CStopLight..., not DStopLight. Programming the Control You can see by expanding your Ctrl class in class view that member variables and functions have been automatically created for you. All we have to do now is initialize those variables and implement those member functions. For the items that change the appearance of the light, we just call Invalidate(). The work of drawing the light will be done in OnDraw. In SetBlinkTime(), we have to kill the old timer and set a new timer. In OnTimer, we call Invalidate(FALSE). We use FALSE since there is no reason to erase the background—the new light has the same shape as the old, just a different color. Using the Timer Where will the timer first be set? Not in the constructor, since the window handle doesn’t exist yet, and a timer message may cause a crash, or just a non-working timer. We could do it the first time OnDraw is called; using a static variable to tell it this is the first time: static int flag; if(!flag) { flag = 1; SetTimer(1,500,NULL); } We could avoid the static variable by setting the timer in OnCreate, after first adding OnCreate using Class Wizard. Where will the timer messages go? The documentation says that if the last parameter of SetTimer is NULL, then the messages will go to the application message queue. But this isn’t in an application! It’s in an ActiveX control. So if we map WM_TIMER, will OnTimer get the timer messages, or will the (presently unknown) parent get the timer messages? Experiment shows that the ActiveX control will get them! That’s good, as otherwise it would be difficult to create a self-contained blinking traffic light control. void CStoplight2004Ctrl::OnTimer(UINT nIDEvent) { // TODO: Add your message handler code here and/or call default static int counter; // count the ticks mod 5 ++ counter; if(counter == 5) counter = 0; // light turns yellow at counter 2, red at 3, green at 0 if(counter == 0) { SetWhichLight(0); } else if(counter == 2) { SetWhichLight(1); } else if(counter == 3) { SetWhichLight(2); } Invalidate(FALSE); COleControl::OnTimer(nIDEvent); } void CStopLightXCtrl::OnDraw( CDC* pDC, const CRect& rcBounds, const CRect& rcInvalid) { if(m_hWnd == NULL) return; // without this it crashes when you try to insert one in your dialog GetClientRect(&m_BoundingBox); static int flag; if(!flag) { flag = 1; SetTimer(1,500,NULL); } pDC->FillSolidRect(&m_BoundingBox,RGB(128,128,128)); CBrush Brush[3]; Brush[0].CreateSolidBrush(GetWhichLight() == 2 ? m_TopColor: RGB(0,0,0)); Brush[1].CreateSolidBrush(GetWhichLight() == 1 ? m_MiddleColor:RGB(0,0,0)); Brush[2].CreateSolidBrush(GetWhichLight() == 0 ? m_BottomColor: RGB(0,0,0)); CRect r; CBrush *oldBrush; int i; int x = (m_BoundingBox.Width() - m_LightDiameter)/2; for(i=0;i<3;i++) { if(i==0) oldBrush = pDC->SelectObject(&Brush[i]); else pDC->SelectObject(&Brush[i]); r.SetRect(x,5*(i+1) +i*m_LightDiameter, x + m_LightDiameter, (i+1)*(5+m_LightDiameter)); pDC->Ellipse(r); } pDC->SelectObject(oldBrush); } Here’s the code for GetBlinkTime and SetBlinkTime. Note that even though we chose to have get and set functions, we still need to add a member variable to make this work. But since we have to call KillTimer and SetTimer, we needed functions here. However, it would have been easier to use only a member variable for WhichLight. The get and set functions for WhichLight are not shown here as all they do is change a member variable. SHORT CStoplight2004Ctrl::GetBlinkTime(void) { AFX_MANAGE_STATE(AfxGetStaticModuleState()); return m_BlinkTime; } void CStoplight2004Ctrl::SetBlinkTime(SHORT newVal) { AFX_MANAGE_STATE(AfxGetStaticModuleState()); m_BlinkTime = newVal; KillTimer(1); SetTimer(1,newVal,NULL); SetModifiedFlag(); } Here’s the final version of the constructor, showing the initializations of all the variables: CStoplight2004Ctrl::CStoplight2004Ctrl() : m_WhichLight(0) , m_BlinkTime(0) { InitializeIIDs(&IID_DStoplight2004, &IID_DStoplight2004Events); SetWhichLight(0); // Green light to start with // SetBlinkTime(500); causes a crash as window doesn't exist yet m_BottomColor = RGB(0,255,0); m_TopColor = RGB(255,0,0); m_MiddleColor = RGB(255,255,0); m_RectangleWidth = 40; m_RectangleHeight = 90; m_LightDiameter = 30; } Testing an ActiveX control A program that uses an ActiveX control is called a container. To debug or test your new control, you will need a container. There are two possibilities: Make a new project of your own as the container. Use the ActiveX Control Test Container provided by Microsoft. First we will use the ActiveX Test Control Container. To do so, the easiest way is to invoke it from the Tools menu of Visual C++. When it comes up, you see a blank screen and some menus. This application permits you to add ActiveX controls dynamically and invoke their methods and change their properties. Choose Edit | Insert new control. You should see your new control, StopLightX Control, on the list of available controls. Choose it. One will be added to your screen. You will see that it is an awkward size, but if you followed all the steps correctly, the lights will be working. Choose File | Save Session As. Save it as StopLightX. Now you can get back to this point next time, without having to add a new control. Click on the border of your control, so it appears selected. Then choose Control | Invoke Methods. ( If it is grayed out, you have not selected the control first.) This takes you to a dialog that allows you to invoke the methods of your control, and see if they work or not. Invoking the Control’s Methods Recall the SetBlinkTime method: void CStopLightXCtrl::SetBlinkTime(short nNewValue) { KillTimer(1); SetTimer(1,nNewValue,NULL); SetModifiedFlag(); } (There is another line of automatically generated code that is not shown, but it doesn’t do anything anyway.) Now, to test this, choose Control | Invoke Methods and use the dialog that comes up: You can change the blink time from the default 500 to 200, for example, and press Set Value and then Invoke. You will see the lights blink faster. From here on, the development of the StopLightX control is routine. Making Your Own Container For serious development, you will want to make your own container application. To do that, just create a new project using AppWizard, but at the first screen, check the radio button Add to solution near the bottom left.. Now just create an MFC .exe project as you have done all semester. A CFormView based project will be easy to use as a container for StopLightX controls. Working with Several Projects in One Workspace When AppWizard is done, you have a workspace with two projects. The active project is built when you choose Build. You need to keep both projects up to date. You can do that either manually, or using Build | Batch Build. You will see that Batch Build is set up to allow you to develop a debug and release version of each project, and in the case of your ActiveX project, both Unicode and non-Unicode versions of both the debug and release versions; so there are four versions of the ActiveX project and two versions of the container project. For simplicity, you can uncheck all but the non-unicode debug versions. Then you can choose Batch Build to build both projects at once. Warning. A common error is to have one project active, make a change in the code for the other project, click Build, and wonder why the code change didn’t do anything. Well, you haven’t compiled that code yet! You have two build buttons on the toolbar. One is “build solution” and one is “build project”. Use the “build solution” button.