Issue No. 19 (June 1, 2000)
********************************** COMMENTS Welcome to the 19th issue of the Excel Experts E-letter (or EEE), by David Hager. EEE is normally a monthly publication. Feel free to distribute copies of EEE to your friends and colleagues and to contribute your Excel gems to EEE so that others can benefit from your work. Back issues are available for download from the EEE web page located on John Walkenbach's web site. New issues are normally available on the 1st of each month. There will be periods when EEE is not published due to time and travel constraints. http://www.j-walk.com/ss/excel/eee/index.htm ********************************** Top Excel Sites See: http://www.officevba.com for VBA articles and downloadables files of the highest order. ********************************** POWER FORMULA TECHNIQUES by Leo Heuser and Eero TibarHow can you get a list of unique entries in an n * m array by using a worksheet formula? Example: Assuming data in B5 : GR10, enter this array formula in e.g. G12. G11 must be empty or, if it has a value, this value must not occur in B5 : GR10. =OFFSET($B$5,MIN(IF(COUNTIF($G$11:G11,$B$5:$GR$10)=0,ROW($B$5:$GR$10)- ROW($B$5))),MOD(MIN(IF(COUNTIF($G$11:G11,$B$5:$GR$10)=0,ROW($B$5:$GR$10)- ROW($B$5) +(COLUMN($B$5:$GR$10)-COLUMN($B$5))/1000)),1)*1000) Drag down until the value in G12 begins repeating. Here is slightly different approach to extract unique items from a N*M table (named as "tbl" in the formula). Type "Unique items from the table" in A1 and enter the following formula as an array into A2 and copy it down. =INDEX(tbl,MIN(IF(COUNTIF($A$1:A1,tbl)=0,ROW(tbl)-MIN(ROW(tbl))+1)), MATCH(0,COUNTIF($A$1:A1,INDEX(tbl,MIN(IF(COUNTIF($A$1:A1,tbl)=0,ROW(tbl) -MIN(ROW(tbl))+1)),,1)),0),1) ********************************** VBA CODE EXAMPLES by David Hager I like using the Pick List feature in Excel 97 (and later), but I don't like having to select the menu item for that feature everytime I go to a new cell. How can I solve this problem? Place this event procedure in the ThisWorkbook module. Then, any time you select a cell where the pick list would pop up when called from a menu item, it will instead pop up automatically. Private Sub Workbook_SheetSelectionChange(ByVal Sh As Object, ByVal Target _ As Excel.Range) On Error Resume Next With Target If .Value <> "" Then Exit Sub If .Row = 1 Or .Row = 65536 Then Exit Sub If .Offset(-1, 0).Value = "" And .Offset(1, 0).Value = "" Then _ Exit Sub If Selection.Address <> .Address Then Exit Sub End With Application.EnableEvents = False SendKeys "+{F10}k" Application.EnableEvents = True End Sub by Bob Umlas How can you give users a Print Preview and not allow them to change any of thesettings? Use the following procedure. It disables key buttons at the top of the preview window. Sub PrtPvw() ActiveSheet.PrintPreview False '"False"==> no changes allowed ActiveWindow.View = xlNormalView ' In case user switched to Page Brake Preview. End Sub by Bob Umlas How can I step through a For-Next loop by using a custom step process? If you need to loop through code with an index which takes on specific values like 1,4,5,18,28,33,34,85 instead of the more commom 1,2,3,4,5,6.. or 1,3,5,7,..., then you can use this technique: Sub OddLoop() For i = 1 to 8 j=Array(1,4,5,18,28,33,34,85)(i) 'Now use j as your subscript Next End Sub ********************************** POWER PROGRAMMING TECHNIQUE by Bob Umlas This procedure contains VBA code to add to your existing VBA code -- basically, it puts one statement at the beginning of each procedure in every module (class modules and event procedures not included). This statement is a call to a routine (which YOU need to write) and passes the sub/function name. For example... Before: Sub ABC() Dim i as Integer For each x in sheets Next End Sub Sub xyz() End Sub After: Sub ABC() MyProc "ABC" Dim i as Integer For each x in sheets Next End Sub Sub xyz() MyProc "xyz" End Sub Notice that afterwards, there's a new line immediately after the sub. It calls MyProc (this is changeable) and passes the name of the procedure it's in. You can use MyProc to trace flow, track the time, etc -- you can get creative here! There are 2 main routines: Addit, and Deleteit. Running Addit will insert the one-liner, running Deleteit will remove this one-liner. The code is inserted into the active workbook. The first line inside the VBE for AddALine.xls is: Public Const TheProcName As String = "MyProc" '============CHANGE THIS LINE and whatever you change "MyProc" to will be the routine called inside each procedure of your code. Public Const TheProcName As String = "MyProc" '===============CHANGE THIS LINE Sub Addit() '=========================== 'RUN THIS CODE TO INSERT THE LINE INTO THE ACTIVE WORKBOOK's CODE '=========================== AddALine MsgBox "Done....Don't forget to write procedure " & _ TheProcName & "!", vbExclamation End Sub Sub Deleteit() '=========================== 'RUN THIS CODE TO DELETE THE LINE '=========================== DelALine MsgBox TheProcName & " has been deleted from each procedure." End Sub Sub AddALine() Dim ProcName As String, ProcNames() As String, Boo As Boolean Dim LngR As Long, TheLine As Long, LngI As Long Set VBP = ActiveWorkbook.VBProject nocomponents = VBP.VBComponents.Count On Error Resume Next For i = 1 To nocomponents If VBP.VBComponents(i).Type = 1 Then 'module With VBP.VBComponents(i).CodeModule If .Name = "ModInserter" Then GoTo NextOne col = .CountOfLines codl = .CountOfDeclarationLines ProcName = .ProcOfLine(codl + 1, LngR) If ProcName = "" Then GoTo NextOne If LngR <> 0 Then GoTo NextOne TheLine = .ProcBodyLine(ProcName, vbext_pk_Proc) thetext = .Lines(TheLine, 1) If Right(thetext, 1) = "_" Then j = 2 Else j = 1 .InsertLines TheLine + j, TheProcName & """" & _ ProcName & """" LngI = codl + 1 col = col + 1 2: If LngI > col Then GoTo 1 If ProcName <> .ProcOfLine(LngI, LngR) Then ProcName = .ProcOfLine(LngI, LngR) If LngR <> 0 Then GoTo 3 TheLine = .ProcBodyLine(ProcName, vbext_pk_Proc) thetext = .Lines(TheLine, 1) If Right(thetext, 1) = "_" Then j = 2 Else j = 1 .InsertLines TheLine + j, TheProcName & """" & _ ProcName & """" col = col + 1 End If 3: LngI = LngI + 1 GoTo 2 1: End With End If NextOne: Next End Sub Sub DelALine() Dim ProcName As String, ProcNames() As String, Boo As Boolean Dim LngR As Long, TheLine As Integer, LngI As Integer If MsgBox("Are you sure you want to delete " & TheProcName & _ " from each procedure?", vbYesNo + vbQuestion) = vbNo Then Exit Sub Set VBP = ActiveWorkbook.VBProject nocomponents = VBP.VBComponents.Count On Error Resume Next For i = 1 To nocomponents If VBP.VBComponents(i).Type = 1 Then 'module With VBP.VBComponents(i).CodeModule If .Name = "ModInserter" Then GoTo NextOne col = .CountOfLines codl = .CountOfDeclarationLines ProcName = .ProcOfLine(codl + 1, LngR) If ProcName = "" Then GoTo NextOne If LngR <> 0 Then GoTo NextOne TheLine = .ProcBodyLine(ProcName, vbext_pk_Proc) thetext = .Lines(TheLine, 1) If Right(thetext, 1) = "_" Then j = 2 Else j = 1 If Left(.Lines(TheLine + j, 1), 5) <> Left(TheProcName, 5) Then ' MsgBox TheProcName & " not found in procedure """ & _ ProcName & """... ignoring" GoTo 22 End If .DeleteLines TheLine + j, 1 22: LngI = codl + 1 2: If LngI > col Then GoTo 1 If ProcName <> .ProcOfLine(LngI, LngR) Then ProcName = .ProcOfLine(LngI, LngR) If LngR <> 0 Then GoTo 3 If ProcName = "" Then GoTo 3 TheLine = .ProcBodyLine(ProcName, vbext_pk_Proc) thetext = .Lines(TheLine, 1) If Right(thetext, 1) = "_" Then j = 2 Else j = 1 If Left(.Lines(TheLine + j, 1), 5) <> _ Left(TheProcName, 5) Then ' MsgBox TheProcName & " not found in procedure """ & _ ProcName & """... ignoring" GoTo 3 End If .DeleteLines TheLine + j, 1 End If 3: LngI = LngI + 1 GoTo 2 1: End With End If NextOne: Next End Sub Sub Showcode() MsgBox "Before running ""Addit"", activate the workbook whose code " & _ "this routine will update." MsgBox "Change ""TheProcName"" to the name of the procedure you want " & _ "to run for each sub." Application.SendKeys "{up}{up}" Application.Goto "Addit" End Sub ********************************** DEVELOPER TIPS by Chip Pearson and Stephen Bullen Why use class modules? Basically, a Class is the definition of an Object. The word "object" is deliberately vague. And object is anything that you want to design. It is defined entirely (mostly) by its properties, methods, and events. In Excel, there are hundreds of "built-in" objects, all defined by class modules. The "class" is the definition of an "object". For example, a Worksheet is an object. And there is a class module which defines just what a Worksheet really is. There are various properties of a Worksheet object (e.g., Visible). Properties simply define and set various attributes. Think of properties as "Adjectives" which describe an object. An object also has Methods. Methods are the "Verbs" of objects. For example, a Worksheet object has an Activate method. This causes something to happen. Finally there are Events. I can't think of a good grammatical analogy for events. Essentially, Events are how an object tells the rest of the world that something happened. For example, in a Worksheet object, there is a Change event. This is the Worksheet object's way of telling the rest of world, "Hey, look at me, I changed". The rest of the world can ignore that event, or it may take action. But the world has been told that object has done something (or had done something to it). Now, you use Class Modules to create your own objects. Suppose you were writing an application that was used for employee tracking. Using a class module, you would define your own object called "Employee". This class would define a single, generic, employee. With the DIM and SET statement, you can create a specific employee, based on the "template" or "definition" of a generic employee. The Employee class would have several Properties, such as Name, Address, and Salary. It could also have methods, such as Promote, GiveRaise, and Fire. In your application, the Promote method would do the same things -- e.g., increasing the Salary property, updating a central database, sending an email to another department to buy him a nicer computer, etc. These actions are all the same whenever you Promote any employee. By using a Class Module to define a "generic" employee, you only have to write the code once. Then to work with a *specific* employee, you just call the methods for that employee: Dim ThisEmp As CEmployee ' more code Set ThisEmp = New CEmployee ThisEmp.Name = "John Smith" ' more code ThisEmp.Promote All of the code related to the Promote event is contained in the Class modules (the definition of any employee), so you can simply call the Promote method. Once you've defined the Class, you never have to worry about what Promote actually does. Here's another way to think about it. In the Worksheet object, there is a PrintOut method. Within the PrintOut method, there is all the code that actually formats the worksheet for printing, determines what printer you have, and actually does all the work of printing the sheet. As a VBA programmer, you don't have to worry about any of that. You simply call PrintOut, and let that do all the work for you. You don't have to worry about what sort of printer the user has, whether it can print color, and a hundred other things. You just call PrintOut and let the Object do all the work. Class Modules let you create you own objects, or extend the functionality of other, existing objects. They are very useful because they allow you to write the code once, and then simply create new objects based on the class (think of it like a blueprint for a house). It is write the code once, and use it many times. For example, I have a class module that extends the functionality of a standard list box. The standard list box doesn't have a MoveUp method, which simply moves the selected item one row up in the list. By using a Class Module, I added a MoveUp method (as well as MoveDown, MoveToTop, MoveToBottom, etc). I wrote that class one time. Now, whenever I need to use "better" list boxes in my applications, I just use that Class. I don't have to "re-invent the wheel" for every application I write. This just scratches the surface of what a Class is and how to use them. If you've ever heard the term "object oriented", Classes are the foundation of this entire design philosophy. Just to provide the opposite end of the spectrum to Chip's excellent answer, class modules can also be though of as user-defined types (UDT) on steroids. A simple UDT can be used to store related information about a particular thing, such as Chip's employee: Type Employee Name As String DOB As String Grade As String Salary As Double End Type If you wanted to do stuff with an employee, you'd use a normal procedure somewhere: Sub RaiseEmployee(uEmp As Employee, sNewGrade As String) 'Validate Grade '... uEmp.Grade = sNew Grade 'Do stuff to work out new salary etc. '... End Sub Sub FireEmployee(uEmp As Employee) uEmp.Grade = "F" uEmp.Salary = 0 End Sub Sub SetSalary(uEmp As Employee, dNewSalary As Double) 'Validate Salary '... 'Does new salary mean a new grade? '... End Sub etc. That's fine as far as it goes and you can create some great programs without ever using class modules. The main thing wrong with it is that the *data* for the object (i.e. the contents of your UDT) is separated from the *actions* that are performed on the data (the RaiseEmployee and FireEmployee subs). Hence, you have to be very careful that the same validation is performed in each sub and that one sub doesn't alter the data in a way that will cause another sub to fail; this is often the cause of some of the hardest bugs to find - logic problems. If you use a class module instead, you can include the validation and other functionality *with* the data; to the extent that the data can *not* be changed unless it's valid. You can think of it as that the 'Grade' property of the Employee (for example) can validate *itself* and can refuse to be updated, or it can know *itself* that when it changes to a valid new grade, it needs to change the salary too. In the example above, with two simple procedures, think what would happen if we had to add another check before changing the grade, or introduce a new action to be performed if the grade is changed (such as notifying their manager). In the procedural approach, we'd have to change two or three routines to handle it - i.e. wherever the grade is set. In the clas module approach, it is simply another action to be performed by the 'grade' property *itself* - none of the other code needs to know about it. i.e: Dim msGrade As String 'Data that only code in the class can 'see' 'Property to read the grade Public Property Get Grade() As String: Grade = msGrade: End property 'Property to set the grade Public Property Let Grade(sNew As String) If Not sNew Is Valid Then Err.Raise "Not a valid grade" Exit Property End If 'Grade is valid, so we can safely store it msGrade = sNew 'Now what else do we need to do when the grade changes? Select Case sNew Case "F" 'Being fired, better ask for a redundancy slip Salary = 0 Case "M" 'Being made a manager, better ask for a better car 'Increase the Salary too Case "D" 'Being demoted, schedule for more frequent reviews 'Decrease the Salary 'etc End Select End Property Public Sub Fire() Grade = "F" End Sub Now, everything that needs to be done when the grade is changed has been made an *integral* part of changing the grade - there's no way that the grade can be changed by anywhere else in the system without those checks and actions happening. Really, though, it just boils down to a different design and development style, and one that hopefully takes us further down the road of improved code reuse, more stability and fewer opportunities for bugs to creep in. The hardest thing to work out, though, is to decide which functionality should be 'in' the class module and which should be on the outside, but using the class module. For example, do we have a '.Fire' method within the class, or a Fire(oEmp As Employee) procedure outside that just sets the grade to "F"? I find that I'm using class modules more and more; it's almost at the stage where if I'm asked "Why use a class module", my reply is "Why not?" ********************************** Issue No.19 OF EEE (PUBLISHED 31May2000) Next issue scheduled for 05July2000. BY David Hager dchager@compuserve.com **********************************
Excel Expert Newsletter Archives
Here you'll find the archives of David Hager's Excel Expert's E-Letter, produced in 1999-2001. This information is old and unorganized, but it's here because it still contains lots of useful information. The newsletters contains quite a few links. Needless to say, most are no longer valid.
It's interesting to note that some of the key problems back then are still key problems today.