Introduction This article is for those enthusiastic VC++ developers who want to build their own ActiveX controls but don’t know where to start with. This article will take you to build your first ActiveX control. This article will show you the step by step method to build a control which draws different waveforms (Sin/Cos).I assume that you are familiar with VC++ and know some basics of ActiveX control.
Creating an ActiveX Control Simply follow these steps in order to create an ActiveX control. 1.
Using the AppWizard to create an ActiveX Control project
1. 2.
Select New from File menu
3. 4.
Name of new project Plot and then click the OK button.
5.
From the next dialog (titled MFC ActiveX Control Wizard - Step 2 of 2), locate the combo box with the prompt Which window class, if any, should this control subclass?. Drop down the list and select the entry STATIC from that list. We're using a static control in this example since we'll just be displaying data (and not accepting input).
6. 7.
Click on Advanced button and check the Flicker free activation checkbox.
Click the Projects tab and select the type of project as MFC ActiveX Control Wizard from the list. Take all the defaults values of the next dialog (titled MFC ActiveX Control Wizard Step 1 of 2) and click the Next button
Now click the Finish button. At this point, the AppWizard will generate the following three classes on your behalf:
2.
CPlotApp - The ActiveX "appliction" class is derived from COleControlModule. The COleControlModule class is the base class from which you derive an OLE control module object. This class provides member functions for your control module's initialization (InitInstance) and cleanup code (ExitInstance). CPlotCtrl - The second class is derived from COleControl and will provide most of the functionality to your control. This is the class where you will write the majority of the your code. CPlotPropPage - The third class is CPlotPropPage, (derived from COlePropertyPage). This class is used to manage the property page dialog of your control. In addition to this class, a dialog resource is also created that will serve as the property page for the control.
Adding "Stock" properties The term stock properties means that its one of a set of common properties that MFC code stores and initializes for you. The MFC code also performs appropriate action when the value of a stock property is changed. The ClassWizard provides code for changing nine (9) such properties. Since these properties are built-in, they are obviously the easiest to deal with in terms of the work required by the developer. As you'll see shortly, you literally don't add a single line of code to add stock property to your control! The ClassWizard provides code for the following stock properties: o o o o o o o o o
Appearance BackColor ForeColor BorderStyle Font Caption Enable Text hWnd
From these, we will work with the Appearance, BackColor, ForeColor and BorderStyle properties. We'll start by adding the BackColor stock property
1. 2.
From the ClassWizard, click the Automation tab.
3.
Once the Add Property dialog does appear, select the BackColor property from the External Name combo box. (It is called the "external name" because this is how users of the control will refer to it.)
4. 5.
Verify that the Stock radio button is selected in the Implementation groupbox.
Ensure that the CPlotCtrl class is selected in the Class name combobox. Now, Click the Add Property button to display the Add Property dialog.
Click the OK button to finish the generation of the stock property. At this point, the ClassWizard will store the value of the BackColor property and initialize it to background color of the container window. The ClassWizard will also add the following line to your control's ODL file:
6. [id(DISPID_BACKCOLOR), bindable, requestedit] OLE_COLOR BackColor; Finally, ClassWizard will also add the code to invalidate the control whenever the value of the BackColor property changes, thereby forcing the OnDraw function to redraw the control. The only thing you have to do is to use the color contained in the property to paint the background color of control. 7.
At this point, add the following two (2) lines of code to the end of the CPlotCtrl::OnDraw member function
8. CBrush bkBrush(TranslateColor(GetBackColor())); 9. pdc->FillRect(rcBounds,&bkBrush);
10. Now, let's add a Color property page to the program. This page will allow users of the control to change the BackColor our newly added property property at design-time. To do this, simply open the PlotCtrl.cpp file and locate the // Property pages comment. Once you've done that, you should see the following:
BEGIN_PROPPAGEIDS(CPlotCtrl, 1) PROPPAGEID(CPlotPropPage::guid) END_PROPPAGEIDS(CPlotCtrl) The first line tells the compiler how many pages exist. Notice that it's set to 1. Change this value to 2 as we're going to add a new page. Now insert the following line just before the END_PROPPAGEIDS line (The CLSID_CColorPropPage is defined automatically since this is a property page CLSID for a stock property).
PROPPAGEID(CLSID_CColorPropPage) Once you've finished, your new property page id map should look like the following:
BEGIN_PROPPAGEIDS(CPlotCtrl, 2) PROPPAGEID(CPlotPropPage::guid) PROPPAGEID(CLSID_CColorPropPage) END_PROPPAGEIDS(CPlotCtrl) Once you make the above changes,the stock property page is automatically linked to the BackColor property.
11. Now that you've seen how to add the BackColor stock property to an ActiveX control, follow these same steps in order to add the Appearance, ForeColor and BorderStyle properties. Note that you do not need to add a property page for the other properties.
12. After adding these stock properties, build your control and test it using ActiveX Test Container (which is usually found under the Tools menu. As you can see in the figure below, the ClassWizard has added the appropriate controls for changing the stock properties.
3.
Adding Custom Properties Custom properties are properties you devise yourself for your control. For the plot control I have added only four custom properties "Grid On/Off" and "X-Log". The grid properties will control the visibility of the control grid. The "x-log" property will be used to plot the horizontal axis in the logarithmic scale. Let's start with the grid properties.
13. From the ClassWizard, click the Automation tab. 14. Ensure that the CPlotCtrl class is selected in the Class name combobox. Now, Click the Add Property button to display the Add Property dialog.
15. Once the Add Property dialog is displayed, enter ShowGrid into the External Name combo box. 16. Then select BOOL as the properties type.
17. Verify that the Member Variable radio button selected in the Implementation group box. 18. Click the OK button to have the ClassWizard create a boolean custom property named ShowGrid. Also note that the internal member variable name (the name used in the control's code) is m_showGrid. Now, whenever the container changes the value of this property, the MFC code will reflect that value in the m_showGrid member variable and will call the CPlotCtrl::OnShowGridChanged notification function. 19. Because ShowGrid is a custom property, we have to write our own initialization and implementation code. Add the following code (marked in bold) in order to initialize the m_showGrid member variable.
20. void CPlotCtrl::DoPropExchange(CPropExchange* pPX) 21. { 22. ExchangeVersion(pPX, MAKELONG(_wVerMinor, _wVerMajor)); 23. COleControl::DoPropExchange(pPX); 24. 25. PX_Bool(pPX,_T("ShowGrid"),m_showGrid,FALSE); 26. } Locate the CPlotCtrl::OnShowGridChanged member function that the ClassWizard added to your code when you created the ShowGrid property (it should be at the end of the PlotCtl.cpp file. Insert the following line (marked in bold) to that function. (This call simply invalidate the control when the ShowGrid property value is changed)
void CPlotCtrl::OnShowGridChanged() {
InvalidateControl(); SetModifiedFlag(); }
27. Since ShowGrid is a custom property we have to do little more work to have it included on the property page. To do this, open the Resource View tab and open the Property page dialog (IDD_PROPPAGE_PLOT). 28. Using the dialog editor add a check box with the ID IDC_CHECK1 and text value of "Show Grid".
29. Run the ClassWizard, select the Member Variables tab 30. Add a member variable for the control id IDC_CHECK1 called m_bShowGrid of type BOOL. Make sure that you set the Optional property name to ShowGrid. When you're finished, the dialog should look like the figure below.
31. Now, add the custom property "X-Log" where the following values are shown in the following figure.
32. Add the following line of code to the CPlotCtrl::DoPropExchange member function:
33. PX_Bool(pPX,_T("ShowGrid"),m_xLog,FALSE); 34. Just as you did with the ShowGrid property, add a checkbox to the control's property page for the X-Log property and then add a member variable for it as well. 35. Insert the following code (marked in bold) in the CPlotCtrl::OnXLogChanged member function
36. void CPlotCtrl::OnXLogChanged() 37. { 38. InvalidateControl(); 39. SetModifiedFlag(); 40. } 41. To implement these properties, add the following members to the control:
42. private: 43. CRect wndRect,m_DrawRect; 44. CDC* m_pDC; 45. 46. //Function to Intialize DC and mapping mode 47. void PrepareForPlotting(CRect rect); 48. void DrawGrid();
49. Modify the CPlotCtrl::OnDraw member function as follows. (Note: you will need to include the math.h file due to the use of the sin function)
50. //Function to plot Grid 51. void CPlotCtrl::OnDraw(CDC* pdc, 52. const CRect& rcBounds, 53. const CRect& rcInvalid) 54. { 55. DoSuperclassPaint(pdc, rcBounds); 56. 57. m_pDC = pdc; 58. m_DrawRect = rcBounds;
59. wndRect = rcBounds; 60. 61. PrepareForPlotting(&rcBounds); 62. 63. CBrush hbrBackground(TranslateColor(GetBackColor())); 64. pdc->FillRect (m_DrawRect,&hbrBackground); 65. 66. if(m_showGrid) 67. DrawGrid(); 68. 69. float y; 70. m_pDC->SelectObject(CPen(PS_SOLID,1,GetForeColor())); 71. m_pDC->MoveTo(m_DrawRect.right/2, 72. m_DrawRect.bottom/2); 73. for (int i=0;i<2000;i=i++) 74. { 75. y = 512*sin(2*3.1415926535*i/1000)+512; 76. m_pDC->MoveTo(i , y); 77. m_pDC->LineTo(i+1 , 78. 512*sin(2*3.1415926535*(i+1)/1000)+512); 79. } 80. } 81. Add the following code for the DrawGrid function that you declared.
82. 83. void CPlotCtrl::DrawGrid() 84. { 85. CPen Pen (PS_SOLID| PS_INSIDEFRAME,1,TranslateColor(GetForeColor())); 86. CPen* oldPen = m_pDC->SelectObject (&Pen); 87. 88. switch(m_xLog) 89. { 90. case FALSE: 91. int i; 92. 93. for (i = m_DrawRect.left; 94. i <= m_DrawRect.right ; 95. i = i+(( m_DrawRect.right - m_DrawRect.left )/10)) 96. { 97. m_pDC->MoveTo (i, m_DrawRect.top ); 98. m_pDC->LineTo (i, m_DrawRect.bottom ); 99. } 100. 101. for (i = m_DrawRect.top; 102. i <= m_DrawRect.bottom; 103. i = i+ (( m_DrawRect.bottom - m_DrawRect.top )/8)) 104. { 105. m_pDC->MoveTo (m_DrawRect.left,i ); 106. m_pDC->LineTo (m_DrawRect.right,i); 107. } 108. break; 109. 110. case TRUE: 111. int x,X; 112. 113. for(int j=1;j<= 10;j++) 114. { 115. x= (int)(log10(j)*285.7143); 116. m_pDC->MoveTo (x,m_DrawRect.top); 117. m_pDC->LineTo (x,m_DrawRect.bottom );
118. 119. 120. 121. 122. 123. 124. 125. 126. 127. 128. 129. 130. 131. 132. 133. 134. 135. 136. 137. 138. 139. 140. 141. 142. 143. 144. 145. 146. 147. 148. 149. 150. 151. 152. 153. 154. 155. 156. 157. 158. 159. 160. 161. 162. 163. 164. 165. 166. 167. 168. 169. 170. 171. 172. 173. 174. 175. 176. 177. 178. 179.
} X= x; m_pDC->SelectObject(&Pen); m_pDC->MoveTo (x,m_DrawRect.top); m_pDC->LineTo (x,m_DrawRect.bottom ); m_pDC->SelectObject (&Pen); m_pDC->TextOut (x,m_DrawRect.bottom-5,"10.0K"); for( j=1;j<= 10;j++) { x= X+(int)(log10(j)*285.7143); m_pDC->MoveTo (x,m_DrawRect.top); m_pDC->LineTo (x,m_DrawRect.bottom ); } X= x; m_pDC->SelectObject(&Pen); m_pDC->MoveTo (x,m_DrawRect.top); m_pDC->LineTo (x,m_DrawRect.bottom ); m_pDC->SelectObject (&Pen); m_pDC->TextOut (x,m_DrawRect.bottom-5,"100.0K"); for( j=1;j<= 10;j++) { x= X+(int)(log10(j)*285.7143); m_pDC->MoveTo (x,m_DrawRect.top); m_pDC->LineTo (x,m_DrawRect.bottom ); } X= x; m_pDC->SelectObject(&Pen); m_pDC->MoveTo (x,m_DrawRect.top); m_pDC->LineTo (x,m_DrawRect.bottom ); m_pDC->SelectObject (&Pen); m_pDC->TextOut (x,m_DrawRect.bottom-5,"1.0M"); for( j=1;j<= 10;j++) { x= X+(int)(log10(j)*285.7143); m_pDC->MoveTo (x,m_DrawRect.top); m_pDC->LineTo (x,m_DrawRect.bottom ); } X= x; m_pDC->SelectObject(&Pen); m_pDC->MoveTo (x,m_DrawRect.top); m_pDC->LineTo (x,m_DrawRect.bottom ); m_pDC->SelectObject (&Pen); m_pDC->TextOut (x,m_DrawRect.bottom-5,"10.0M"); for( j=1;j<= 10;j++) { x= X+(int)(log10(j)*285.7143); m_pDC->MoveTo (x,m_DrawRect.top); m_pDC->LineTo (x,m_DrawRect.bottom ); }
180. X= x; 181. 182. m_pDC->SelectObject(&Pen); 183. m_pDC->MoveTo (x,m_DrawRect.top); 184. m_pDC->LineTo (x,m_DrawRect.bottom ); 185. m_pDC->SelectObject (&Pen); 186. m_pDC->TextOut (x,m_DrawRect.bottom-5,"10.0M"); 187. 188. for( j=1;j<= 10;j++) 189. { 190. x= X+(int)(log10(j)*285.7143); 191. m_pDC->MoveTo (x,m_DrawRect.top); 192. m_pDC->LineTo (x,m_DrawRect.bottom ); 193. } 194. 195. X= x; 196. 197. m_pDC->SelectObject(&Pen); 198. m_pDC->MoveTo (x,m_DrawRect.top); 199. m_pDC->LineTo (x,m_DrawRect.bottom ); 200. m_pDC->SelectObject (&Pen); 201. m_pDC->TextOut (x,m_DrawRect.bottom-5,"10.0M"); 202. 203. for( j=1;j<= 10;j++) 204. { 205. x= X+(int)(log10(j)*285.7143); 206. m_pDC->MoveTo (x,m_DrawRect.top); 207. m_pDC->LineTo (x,m_DrawRect.bottom ); 208. } 209. 210. break; 211. } 212.} 213.Add the following code for the PrepareForPlotting function that you declared.
214.void CPlotCtrl::PrepareForPlotting(CRect rect) 215.{ 216. m_pDC->SetMapMode(MM_HIMETRIC); 217. m_pDC->SetMapMode(MM_ANISOTROPIC); 218. m_pDC->SetWindowExt (2000,1024); 219. m_pDC->SetViewportExt (rect.right , rect.bottom ); 220. m_pDC->DPtoLP(&m_DrawRect); 221. 222. return; 223.} At this point, you have completed your control and should be able to build and test it using the ActiveX Test Container The following figure shows an example of the control being tested.
ACTIVEX……….
Good Morning and welcome to the first instalment in this ActiveX control tutorial. I'm your amazingly geeky host Karl Moore — and it's my job to ensure your ride on the Visual Basic train to ActiveX control land is an exciting one. Well, maybe just a slight-amusing one. Hmm, perhaps just a ride. But don't let my pair of glass-bottle-bottom spectacles fool you - this isn't just a journey for mega-geeks. Whatever Visual Basic programming experience you may have, learning about the wonderful world of ActiveX could improve your career, your bank balance and your love life*. * (Love life claims based solely on life-long research by author Karl Moore and his numerous worldwide cybergirlfriends)
Today, we'll be:
• • • •
Getting the low-down on ActiveX components Finding out the difference between ActiveX components and ActiveX controls Discussing a few nerdy things (too boring to mention) And... ... we'll be creating our own ActiveX control!
I know you're excited — but please, hold it in. So without further ado, let's tootle off into the magical realms of ActiveX...
Bonjour and welcome to the second part in this jabber-wockingly cool ActiveX tutorial. As ever, I'm your surprisingly sophisticated host Karl Moore — and this week we'll be covering: • • • •
Creating property procedures Enumeration (whoah!) Dealing with resizing Some other stuff
If you missed part one, it's probably a good idea to check it out before reading on click here! I can see you're getting all giddy now, so let's don our anoraks and thick glasses as we hop on the pink boat destined for the fluffy world of ActiveX controls... Guten Morgan and welcome to the third part of this surprisingly super ActiveX control tutorial. If you've missed the previous two instalments, be sure to check out part one here and part two here. As ever, I'm your host Karl Moore and this week we'll continue to chip away at the Super Cool Text Box project we started a couple of weeks ago. We'll be: • • • •
Adding a chunk of code to make our text box work Discussing how to add events to your code Discussing how to add events to your diary Getting a Wizard to type code for us
So mount the stallion they call VeeBee — it's time to ride off into the glorious, 24-bit pixelised Microsoft sunset... Hola and welcome to the fourth instalment of the only ActiveX control tutorial that's groovier than Austin Powers and perhaps even hotter than Felicity Shagwell's pants*.
* 'Course, that all depends on whether you adopt the American or saucier English definition. I opt for the latter. Any complaints? If you've missed any of the previous slots, be sure to check them out before continuing: • • •
Part One — Introduction to ActiveX controls Part Two — Properties, Enumeration, Resizing Part Three — Events, Mapping, ActiveX Wizard
As ever, I'm your wizzy host Karl Moore and this week, we'll be: • • • •
Discussing what that wizard did last week Figuring out what property bags are all about Figuring out what old bags are all about Adding our very own custom property pages (cool stuff!)
So grab that copy of Visual Basic and let's fly off into the programmatic world of ActiveX controls... Ciao and welcome to the fifth and final instalment of our ActiveX control tutorial. As ever, I'm your host Karl Moore, and if you've missed any of the previous slots, check them out here: • • • •
Part Part Part Part
One — Introduction to ActiveX controls Two — Properties, Enumeration, Resizing Three — Events, Mapping, ActiveX Wizard Four — Wizardy Whatnots Explained and Property Bags/Pages
This week, we'll be: • • • •
Finding out how to distribute your control Answering common questions Learning techniques to ensure your creations stand out above the crowd Looking at how you can use this new skill to enhance your career
So grab your poisoned-nib arrow and SAS survival handbook - and let's finish the adventure of a lifetime. Alternatively, sit back and enjoy the tutorial. Thankfully, by the end of this tutorial you will have finished the Super Cool Text Box control we've been working on. And that's good, 'cause I'm getting bored now. In fact, I haven't experienced this much boredom since the time London Ritz Hotel made a slight booking error and placed the entire Wool Appreciation Society in the same meeting hall as the International Morris Dancer's convention. <Ed: Really? How did you find out about that, Karl?>
So far in our lil' project, we've added a few funky properties, checked up on enumeration and resizing, figured out raising events and even used a wizard to help save time with any run-of-the-mill coding. But last week, I left you on a cliffhanger. Oh yes, don't deny it. You see, after the wizard finished running, it left a wad of 'read and write properties' code all over the place, remember? The question was, what does it all do? Well, sit back my little cherubs, as I explain all. Let's say you go ahead and add our spicy control to a nice test project. You then alter the 'AcceptType' property to 'Letters', save everything and marinade overnight in a healthy dollop of hard disk juice. The next morning, you return and open the project. But <shock, horror> the 'AcceptType' property is now displaying the default 'All'. So what happened? Who stole the property? Well, let me give you a clue: it wasn't the Butler. Oh no. The truth is we simply haven't told Visual Basic that it should save the property. "Why doesn't VB save it automatically instead of forcing programmers to do all the hard work?" - Good question. Let's think about it hmm, I guess some properties don't really need saving. For example, the SelText property of a Text Box, which returns any text currently selected, doesn't need saving but it is still a property. Yet other properties such as 'AcceptType' clearly do need saving and Visual Basic does provides a neat method for doing this. OK, so how do you actually tell VB to save your property?
When an instance of your control is saved, the WriteProperties event of the UserControl (that's your control 'workspace') fires. In addition, a 'PropertyBag' is passed as an argument to this event.
Private Sub UserControl_WriteProperties(PropBag As PropertyBag)
A property bag is just a virtual bag that stores, err, properties. <Ed: No? Gee, nothing gets past you, Karl!> In effect, it's a holding place for values. And you can read and write to this group of values by using two of the property bag methods. So let's pretend a developer is using our Super Cool Text Box control and changes the Text property in design mode. The geezer then hits the Save button. Quicker than you can say antidisestablishmentarianism, Visual Basic (*) yells to your control, "Hey, I'm trying to save here" and fires its WriteProperties event passing a property bag to it. Private Sub UserControl_WriteProperties(PropBag As PropertyBag)
You can imagine this property bag as a shopping trolley without the wonky wheels your task is to fill it up with the information you need to save. You do the "filling up" like this: Call PropBag.WriteProperty("Text", _ txtTextBox.Text, "")
Here you're running the WriteProperty method of the passed property bag. You're telling it to save this information under the heading of "Text", with a value of txtTextBox.Text. The final double-quotation marks indicate a default value of, well, nothing actually.
So you write values to the property bag and hence, save your control properties, such as Text - using this template: Call PropBag.WriteProperty(PropertyName, _ PropValue, Default)
Let's take a peek at another code example: Call PropBag.WriteProperty("ConvertCase", _ m_ConvertCase, m_def_ConvertCase)
Here, our code is saying "Save this item under a heading of ConvertCase, with a value of m_ConvertCase and a default value of m_def_ConvertCase". So in brief, the WriteProperties method fires when a control is being saved. You're passed an 'empty shopping trolley' and asked to put all the stuff you need to save into it. Typically this trolley is stored along with the thing that uses it, such as in a Form FRM file. Take a quick look at your Super Cool Text Box project. Notice how the Wizard automatically added all this code for you? You'll get a chance at writing your own WriteProperty code later. Anyway, that's how you tell Visual Basic to save the values of your properties, such as AcceptType. But what about when the user opens his or her project? How do we get the property values we've just put into the property bag back out? Top Tip: You may wonder why, in addition to passing a 'PropValue' value, we also send the WriteProperties method a 'Default' value. Erm, why bother passing across two values? Won't it simply be the 'PropValue' that gets saved not the default? That one puzzled me for a while. But it appears the WriteProperty method compares both the property value and the default and only saves if they're different. After all, it would waste disk space to save the redundant default value of a control such as the Text Box BackColour being white, when it's white by default. But by supplying this default, it allows the property bag to compare and decide which bits it should save. Clever stuff, eh? You'll understand this concept more as we delve further into default values over the next section. * Asteriskical Top Tip (hah, and they sed I couldn't spel!): Actually, when you finally distribute your control it's not only Visual Basic developers who'll be able to utilise it. When you create an ActiveX component of any type, it's automatically compatible with all ActiveX/COM-aware programming languages, including Visual C++ Next up on my nerdy curriculum, we have the ReadProperties event. Excited yet? Not exactly an inspiring name, I grant you — but please bear with it. Ironically, despite doing exactly the opposite of the WriteProperties method we just uncovered, ReadProperties is surprisingly similar in usage.
Hmm, I could cunningly ask you to reread the previous section backwards or something, but I don't think the Ed would like that. In fact, he'd have me off the site before you can say antidis... Now, let's pretend one groovy son-of-a-developer used your Super Cool Text Box control yesterday to start developing his Parcel Tracking program. He's now returned to continue work and opens the Visual Basic project. What happens? As the form bearing your creation opens, Visual Basic says "Hey diddly dandy, I'm loading you up here" and fires the control's ReadProperties event:
Private Sub UserControl_ReadProperties(PropBag _ As PropertyBag)
Here, you're passed a property bag that potentially contains a handful of property values, such as those previously saved. It's at this stage you can read those properties. Let's peek at an example: m_AcceptType = PropBag.ReadProperty("AcceptType", _ m_def_AcceptType)
Here, we're passing ReadProperty the name of the item to find within the bag ('AcceptType') and a default value of m_def_AcceptType. If, after a quick rummage around, the ReadProperty method manages to find a value for our item, it's passed back otherwise our default value is returned. Either way our m_AcceptType variable, which ultimately handles the AcceptType property is set appropriately. Let's look at another supercool example: txtTextBox.Text = PropBag.ReadProperty("Text", "")
Here, we're directly setting the Text property of our Text Box equal to the "Text" value in the property bag. If no value exists in the bag, ReadProperty passes back our default of "" an empty string. So you read values from the property bag in other words, retrieve your control properties - using this template: Destination = PropBag.ReadProperty(PropertyName, Default)
In brief, the ReadProperties event fires when your control is being loaded. It passes you a possible bagful of properties and allows you to pick out the items you want and set variables or object properties accordingly. Do you understand how both the reading and writing of properties fit together? Take a closer look at your project. Do you understand what the wizard has done? Why don't you practice these techniques by adding a 'play around' property, such as Text2? You can access it via a property Let and Get routine, then use the WriteProperty and ReadProperty skills you've just learned to 'permanently' store the information alongside your control. Top Tip: Here's a great way to chat up geekesses. Instead of saying you used the 'ReadProperty' and 'WriteProperty' methods to save stuff, simply mention how you 'persisted' the values. Trust me, you'll sound much more of an anorak and the grrls just love it! But hold on one lil' minute, missy we've still not figured out the meaning of that mysterious 'PropertyChanged' statement our wizard threw inside every Let procedure - remember? Grab that cigar Sam; it's time to investigate... Behind virtually all of our Property Let routines, you will notice the wizard added a strange 'PropertyChanged' statement, as so: Public Property Let ConvertCase(ByVal _ New_ConvertCase As CaseType) m_ConvertCase = New_ConvertCase PropertyChanged "ConvertCase" End Property
Why? Well, this is very simple. And I'm not talking about 'E=mc2' simple here. I'm taking more along the lines of '1=1' simple. All PropertyChanged does is inform the thing using your control that a particular property has changed. In other words: PropertyChanged "ConvertCase"
...would send a message back to Visual Basic saying, "Hey, somebody has altered ConvertCase so make sure the Properties window is up-to-date. Oh, and don't forget that 'cause a change has been made, you'll probably want to save the project some time soon!" Once again, the Wizard has automatically added all this for you. If you need to do it for yourself however, use this format: Public Property Let PropName(ByVal _ vNewValue As Variant) ' Usual code for processing property goes here PropertyChanged PropName End Property
Hey, wait a minute. I've done nothing but babble on about theory so far. It's time to get physical! Ahem. Have you used the ADO control before? If you right click on it, then select Properties you'll see a small form that enables you to select various options. It's really just an advanced version of the Properties window.
Click here for larger image Lot's of professional components have these 'property pages'. They allow you, as the developer to have much more control over how you can display and allow users to select properties. That means you can use Tabs, Combo Boxes, TreeViews, Command Buttons basically anything you can add to a regular form - to brighten up the selection of options. For the rest of today, I'm going to show you how to create your own property page. My example won't be anything spectacular, but hopefully it will give you a few ideas as to how you can create your own. So wave goodbye to alphabetically ordered Property window, and say hello to the allsinging, all-dancing property page: • •
Open our Super Cool Text Box control project Click Project, Add Property Page
You should be presented with a screen allowing you to add a raw property page or use the Property Page Wizard. For now, let's be lazy and go with the Wizard we'll do a little manual work later. • •
Select the 'VB Property Page Wizard' and click Open Click Next at the introduction
Your screen should now look like this:
Each item in this list will represent one 'tab' when you view the property page of your control. For instance, this control has five different property pages each represented by a tab:
•
Uncheck the StandardColor and StandardFont boxes
• •
Click 'Add' and type: General Click OK
Top Tip: Our project will only have one property page, General. But you could have many more as the above screenshot demonstrates! •
Click Next
Note that Visual Basic has already selected a list of 'Available Properties', though sadly not 'AcceptType' or 'ConvertCase'. Don't worry, we'll deal with this later. • • • •
Add all available properties to the 'General' list Click Next Select No for the Summary Report and click Finish After the Wizard has finished, click OK
Open up the property page our Wizard created. It should look something like this:
Click here for larger image Huh, not very exciting. In fact, you're looking at the only computer screenshot so plain it ships with its own aviation certificate. Let's take a peek at some of the code behind this rather dull property page. Underneath all the Change events of the Text Boxes and the Click events of the Check Boxes, you should find this code: Changed = True
This tells Visual Basic that at least one property on the page has changed. Of course, it doesn't signify that the change should be saved - you mustn't forget that all property pages have an OK, Cancel and Apply button. It simply informs VB that a property has been altered a little like the PropertyChanged method of your control. Now, just as a regular Form has its own events, so does the Property Page. When someone hits the OK or Apply buttons, the ApplyChanges event runs then it's up to you to save the properties. This is a little like the WriteProperties event of your control.
Likewise, when someone first opens your Property Page, the SelectionChanged event fires. This can be compared to the ReadProperties event of your control it allows you to 'set' the various whatnots on your property page. So let's take a peek at some of the code the wizard threw behind those two events: Private Sub PropertyPage_ApplyChanges() SelectedControls(0).Text = txtText.Text SelectedControls(0).PasswordChar = txtPasswordChar.Text ' ... etc ... ' This is like WriteProperties ' It passes the changes on your ' property page back to the control ' SelectedControls(0) End Sub Private Sub PropertyPage_SelectionChanged() txtText.Text = SelectedControls(0).Text txtPasswordChar.Text = SelectedControls(0).PasswordChar ' ... etc ... ' This is like ReadProperties ' It reads the properties of your control ' via SelectedControls(0) and displays ' the info via your property page text boxes, ' check boxes, combos, etc. End Sub
Now, you've probably noticed 'SelectedControls' appearing once or thrice throughout the code. This is a method of your property page - basically a direct 'link' to the running instance of your control. So if you wanted to check out a property of your control, say the PasswordChar value, you can access it via the SelectedControls(0) item. You can read any of our control properties using this format: SelectedControls(0).PropertyName
For example: SelectedControls(0).PasswordChar
... passes back the value of the controls PasswordChar property. The SelectedConrols(0) bit here is just a gateway to your control and its properties. And if you can understand that, the code should look simple. Excellent! Let's summarise: In the ApplyChanges event, you're simply setting properties of the user's control dependant on the various Text Boxes, Check Boxes, etc. present on your property page. You're applying changes made in the property page direct to the control.
In the SelectionChanged event, which remember is similar to our control's ReadProperties event, you merely set the values of your Text Boxes, Check Boxes, etc. dependant on the properties of the user's control. Put simply, the SelectedControls(0) statement just gives you direct access to the user's instance of your control and all it's properties. Try testing your control! Add it to a demo project, right click and select Properties. Try changing a few of the options, then click Apply or OK. Notice how the Properties window updates itself? Now I don't know about you, but I ain't awfully satisfied with my property page. It looks about as stunning as a sumo wrestler's bottom. So let's try to spruce it up a little by adding support for our custom AcceptType and ConvertCase properties: •
Add two Labels and two Combo Boxes to your Property Page
Click here for larger image • •
Name the Combo Boxes 'cboAcceptType' and 'cboConvertCase' respectively Change both their Style properties to '2 - Dropdown List'
Now, when the property page opens, we want those two Combo Boxes to hold our enumeration options such as 'All' or 'Letters'. Unfortunately there isn't an easy way to automatically do this, so we'll add them manually under the property page's Initialize event (similar to Form_Load). •
Add the following code to your property page:
Private Sub PropertyPage_Initialize() With cboAcceptType .AddItem ("Anything") .AddItem ("Just Numbers") .AddItem ("Just Letters") End With With cboConvertCase .AddItem ("Anything") .AddItem ("Upper Case")
.AddItem ("Lower Case") End With End Sub
Try testing your project now. When you open the property page, are you able to select an item from the list? Good let's continue: •
In the Click event behind each Combo Box, add the code:
Changed=True
Remember, this tells Visual Basic that at least one property on the page has been changed. Now let's add a little code to the bit which reads properties and sets the value of your Text Boxes, etc. •
Add the following code to the SelectionChanged event:
Select Case SelectedControls(0).AcceptType ' Note that I'm using the enumeration name prefix ' instead of the ambiguous "All" Case CharacterType.All cboAcceptType.Text = "Anything" Case CharacterType.Letters cboAcceptType.Text = "Just Letters" Case CharacterType.Numbers cboAcceptType.Text = "Just Numbers" End Select Select Case SelectedControls(0).ConvertCase Case CaseType.All cboConvertCase.Text = "Anything" Case CaseType.LowerCase cboConvertCase.Text = "Lower Case" Case CaseType.UpperCase cboConvertCase.Text = "Upper Case" End Select
Here, we're just examining the current AcceptType and ConvertCase properties, then changing the Combos as appropriate. So that's how we get the initial properties into the property page. Now we need to deal with how to get them out, after the user clicks OK or Apply. •
Add the following code to the ApplyChanges event:
Select Case cboAcceptType.Text Case "Anything" SelectedControls(0).AcceptType = _ CharacterType.All Case "Just Letters" SelectedControls(0).AcceptType = _ CharacterType.Letters
Case "Just Numbers" SelectedControls(0).AcceptType = _ CharacterType.Numbers End Select Select Case cboConvertCase.Text Case "Anything" SelectedControls(0).ConvertCase = _ CaseType.All Case "Lower Case" SelectedControls(0).ConvertCase = _ CaseType.LowerCase Case "Upper Case" SelectedControls(0).ConvertCase = _ CaseType.UpperCase End Select
Here, we're just doing the reverse of the SelectionChanged event. We're analysing the content of the two Combo Box controls, then changing the user's control properties to reflect them. Top Tip: There are numerous ways in which this code can be improved, but has been left as shown for simplicity. Can you think of a different method to set our properties other than via text comparison in a Select Case statement? Can you see where you could use the With keyword? Go ahead; test your property page in the usual manner. Does it work? Notice how changes made in the Properties window are instantly reflected in your property page, and vice versa. That's all down to the mysterious 'PropertyChanged' command, keeping everyone up-to-date as to the latest happenings. Phew! What a difficult section! But it's time to celebrate congratulations on getting this far! You've successfully completed all the main components of the Super Cool Text Box control! Well done! Note: A full download of the Super Cool Text Box control is available here. Today, we've explained the mysterious property keywords that cropped up in last week's tutorial. The three major methods we discovered were WriteProperty, ReadProperty and PropertyChanged. Each assist in keeping your control both stable and user-friendly. In the last half of this tutorial, we looked at the advantages of Property Pages, their similarity with the UserControl concepts discussed in the first section, plus learned how you can implement them to enhance your control's usability.
We discovered that if you're after something more than just an alphabetical list, it's time to turn to the property page. And I'm not talking about the back cover of your local rag. For a little homework, why not try to improve some of the code here? Add your own properties, along with the appropriate read and write code. You could even inject a little razzmatazz into the Property Page we finished building just a few minutes ago. And guess what prizes we're offering for the best design? Well, somebody claimed the three-year old Mars bar last week, so we're down to... oh. Nothing, actually. Next week, we'll conclude the tutorial with instructions on how you can package your control for the end user. I'll also be providing a host of free, commercial-quality examples (with full source code!) plus a handful of top tips to ensure your control stands out above the crowd. But until next time, this is your host Karl Moore waving you all a goodnight for tonight. Goodnight!