Starting Handicaps

In the Handicap UDF, there’s a call to another UDF: GetStartingHC. Every golfer has a handicap to start and it’s stored in TblPlayers. For most golfers, it’s a carry over from last season. For new members, it can be their USGA handicap. When a golfer doesn’t have three scores, the starting handicap is used as one of the scores.

Public Function GetStartingHC(ByVal lPlyr As Long) As Long
   
    Dim rsPlayers As ADODB.Recordset
   
    Const sBEGHC As String = “BegHC”
   
    If gadoCon Is Nothing Then
        InitGlobals
    End If
   
    Set rsPlayers = New ADODB.Recordset
    rsPlayers.Open “TblPlayers”, gadoCon, adOpenDynamic
   
    rsPlayers.MoveFirst
    rsPlayers.Find “Player = “ & lPlyr
   
    If Not rsPlayers.EOF Then
        GetStartingHC = CLng((rsPlayers.Fields(sBEGHC) / 0.8) + 36)
    End If
   
End Function

Nothing too fancy, just using the Find method to get to the proper record quickly. This recordset is definitely a candidate for moving global so it doesn’t need to be created and destroyed every time the function is called. I so skillfully created a constant to refer to the field in the recordset, but I leave 0.8 and36 as literals. Shameful. If this program will ever be useful outside of my league, those are going to be user settings. I need to make them user settings right now instead of being so lazy.

Outlook for the Blind Redux

My brilliant piece of altruistic code posted in Get Outlooks Currently Selected Time isn’t all that it’s cracked up to be. Had it been for anyone except a blind guy, it would have been just a poor hack job. But for him, it’s practically unusable. You would think the visually impaired would have no need for code such as Application.ScreenUpdating = False. You would think screen flicker would be the least of their concerns. However, the screen reading programs tries to read the screen when it’s updated, so it’s a big mess.

I haven’t been able to find a way, in Outlook, to stop the screen from updating. If you know of a way, please drop me a line.

Stopwatch

John asks how one macro can be used to stop another macro. One way to do it is with a variable that both procedures can see. In this implementation, I have two buttons from the Forms toolbar that start and stop a counter in A1.

two buttons on an excel sheet and a number in A1

The code is all in one standard module and looks like this:

Private bStopped As Boolean
 
Sub StartIt()
 
    bStopped = False
    Sheet1.Shapes(“Button 1”).Locked = True
   
    Do While Not bStopped
        DoEvents
        With Sheet1.Range(“A1”)
            .Value = .Value + 1
        End With
    Loop
   
    Sheet1.Shapes(“Button 1”).Locked = False
   
End Sub
 
Sub StopIt()
 
    bStopped = True
   
End Sub

StartIt runs until the variable is made true by StopIt. The DoEvents in the loop releases control of the computer to any other processes in the queue. In this case, running StopIt will queue it up but won’t run unless you let it. It’s kind of like letting people in to your lane – DoEvents is that jerk in front of you that’s letting everyone in.

I set the Locked property of the Start button, but I’m not sure if I know what I’m doing. Without those lines pressing Start repeatedly crashed Excel without fail. With those lines, no problems.

The key is to make your variable available to both the start and stop procedures. Since both procedures in this example are in a standard module, my variable can be private to that module. It’s the smallest scope that both procedures can see. If my procedures were in a userform, a variable that was private to the userform’s class module would do the trick. If the procedures are in different modules, you’ll need to declare the variable as Public in a standard module.

Prioritize List on a Worksheet

Jed asks:

I’ve got a spreadsheet that is essentially a list of items in priority order. I’d like to be able to add buttons to each row that will enable me to move that row up or down. Is this possible?

Here’s the way I would do it: I would put a SpinButton control on the worksheet and the user would use that to move cells. Assume you have a list in A1:A5 and you want to allow the user to order the list however they want. First, put a SpinButton control (from the Control Toolbox) on the sheet.

Excel range with adjacent spin button

Then put this code in the sheet’s module:

Private Sub SpinButton1_SpinDown()
 
    Dim vTemp As Variant
   
    If ActiveCell.Row < ActiveSheet.Rows.Count Then
        If Not IsEmpty(ActiveCell.Offset(1, 0)) Then
            vTemp = ActiveCell.Offset(1, 0).Formula
            ActiveCell.Offset(1, 0).Formula = ActiveCell.Formula
            ActiveCell.Formula = vTemp
            ActiveCell.Offset(1, 0).Select
        End If
    End If
   
End Sub
 
Private Sub SpinButton1_SpinUp()
   
    Dim vTemp As Variant
   
    If ActiveCell.Row > 1 Then
        If Not IsEmpty(ActiveCell.Offset(-1, 0)) Then
            vTemp = ActiveCell.Offset(-1, 0).Formula
            ActiveCell.Offset(-1, 0).Formula = ActiveCell.Formula
            ActiveCell.Formula = vTemp
            ActiveCell.Offset(-1, 0).Select
        End If
    End If
   
End Sub
 
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
 
    If Target.Column = 1 And Not IsEmpty(Target) Then
        With Me.SpinButton1
            .Visible = True
            .Top = Target.Top + (Target.Height / 2) – (.Height / 2)
            .Left = Target.Left + Target.Width + 10
        End With
    Else
        Me.SpinButton1.Visible = False
    End If
   
End Sub

The SelectionChange event hides or shows (and positions) the SpinButton when a non-empty cell in Column A is selected. This way, you only need one control on the worksheet. The SpinUp and SpinDown events trade values with the cell above or below, respectively.

There are a couple of error checks in the SpinUp and SpinDown events. Look at the SpinUp event, for example. If A1 is the ActiveCell, then you can’t spin up, so the code only runs if the row is two or greater. Next, it doesn’t allow a trade with an empty cell. It assumes that empty cells are the borders of the list.

Okay, now the funky part. Select cell A1. Click the Up button of the SpinButton. With the arrow keys, not the mouse, select A2. Did the values from A1 and A2 swap? They did for me. When I put break points in the code, everything works fine.

xlColumnWidths Bug

Mike asks

…But when you try to do Selection.PasteSpecial Paste:=xlColumnWidths and try to run the macro, Excel say xlColumnWidths is undefined. Strange, since this is what the macro recorder gave me. …

I can honestly say that I’ve never pasted special column widths before. In fact, I didn’t even know it was an option. But Mike did indeed discover a bug that MS describes in KB231090. When I recorded a macro, I got

Sub Macro1()

‘ Macro1 Macro
‘ Macro recorded 6/13/2005 by Dick Kusleika



   Selection.Copy
    Range(“E1”).Select
    Selection.PasteSpecial Paste:=xlcolumnwidths, Operation:=xlNone, _
        SkipBlanks:=False, Transpose:=False
End Sub

But it looks like the Excel guys forgot to create the constant xlColumnWidths. Use it’s intended literal value, 8, instead, e.g.

Selection.PasteSpecial Paste:=8

Daily Dose Logo Contest

The license plate holder on the back of the Maxima is getting pretty faded. It’s a Colorado Avalanche license plate holder, and because I’m so embittered about the strike, I won’t be replacing it with a similar one. (They forced me to watch baseball this Spring. I don’t know if I can forgive them for that.)

The only obvious choice for a replacement is a custom Daily Dose of Excel license plate holder. I went over to cafepress.com to see what they had, and it looks pretty good. Now I just need some art to upload and I’ll be set. That’s where you come in. You see, my skills with graphic design and the computer programs associated with it have been compared to a monkey having “relations” to a football. You want proof? Here’s what 36 straight hours of graphic design gets me:

poor attempt at a logo

The interlaced D was a pixel-by-pixel affair that I hope I never have to repeat.

If you are so inclined, you can create a logo for me. I generally like the concept of the above logo, but I’m not married to it. Feel free to use parts of it, or not. If you email a new logo to me by June 24th and I use it on this site, you get a prize. You will receive, shipped free to your North American address, the first season of Carnivale on DVD and your choice of DDE logo wear from cafepress. If you’re not in North America, you can still win, but I reserve the right not to pay the shipping. I doubt, however, that the DVDs will work for you anyway.

If you send me the second best logo, you still win a prize. You get your choice of DDE logo wear. Of course, the logo will not be the one you designed and will be a constant reminder of your failure.

If you’re moderately good at this kind of thing, and you like the HBO show Carnivale, then this is the contest for you.

The Handicap Function

It’s time to write the first formula for my golf league program. I started with a function to compute the handicap because it was at the bottom of my sketch. I knew that this program was going to be primarily UDF’s that filled worksheets. Because of that, I need to be aware of speed throughout the development. I start by establishing an ADO connection to my database and keeping that connection live as long as the workbook is open. This is the contents of my MGlobals module:

Public gadoCon As ADODB.Connection
 
Public Const sQRYPLYRWKSCR As String = “SELECT TblPlayers.Player, TblScores.WeekNum, “ & _
    “[Hole1]+[Hole2]+[Hole3]+[Hole4]+[Hole5]+[Hole6]+[Hole7]+[Hole8]+[Hole9] AS WeekScore, “ & _
    “TblScores.Team FROM TblPlayers INNER JOIN TblScores ON TblPlayers.Player = TblScores.Player”
 
Sub InitGlobals()
   
    Dim sCon As String
   
    sCon = “DSN=MS Access 97 Database;”
    sCon = sCon & “DBQ=C:DickGolfLeagueTenthHole.mdb;”
    sCon = sCon & “DefaultDir=C:DickGolfLeague;DriverId=281;”
    sCon = sCon & “FIL=MS Access;MaxBufferSize=2048;PageTimeout=5;”
   
    Set gadoCon = New ADODB.Connection
   
    gadoCon.Open sCon
   
End Sub

This sub is called from the Workbook_Open event to get the connection opening overhead out of the way before the first function is even called.

The logic of the handicap formula is this: Every player has a starting handicap and it’s stored in TblPlayers. The handicap is the average of a player’s most recent three scores times 80%. If there aren’t three scores, the starting handicap is used as one of the scores. A player’s handicap, then, may be the average of his first round and his starting handicap, thus only two “rounds” would be used. The theory behind these rules is that your handicap will reflect how you’re playing now, rather than how you’ve played over a whole season. Here’s the function:

Public Function Handicap(ByVal lPlyr As Long, _
    Optional ByVal lweek As Long = 0) As Long
   
    Dim rsWeekScore As ADODB.Recordset
    Dim sSql As String
    Dim lRecordsProcessed As Long
    Dim dTotalScore As Double
    Dim dAvgScore As Double
    Dim dAvgDiff As Double
    Dim sWeekWhere As String
   
    Const dSCALE As Double = 0.8  ‘only 80% of average scores are used
   Const sWEEKSCORE As String = “WeekScore”
   
    ‘If a week is provided, a where clause will be added to the sql to
   ‘limit the records to only previous weeks
   If lweek > 0 Then
        sWeekWhere = ” AND TblScores.WeekNum < “ & lweek & ” “
    End If
   
    ‘Open the connection if it’s not already
   If gadoCon Is Nothing Then
        InitGlobals
    End If
   
    Set rsWeekScore = New ADODB.Recordset
   
    ‘Limit the records to just the player and optionally the week
   ‘Records are ordered descending by week to use the most current
   ‘scores in the calculation
   sSql = sQRYPLYRWKSCR
    sSql = sSql & ” WHERE (((TblPlayers.Player)=” & lPlyr & “))” & sWeekWhere
    sSql = sSql & “ORDER BY TblScores.WeekNum DESC;”
   
    rsWeekScore.Open sSql, gadoCon
   
    ‘Add up the three most recent scores
   Do While Not rsWeekScore.EOF And lRecordsProcessed < 3
        lRecordsProcessed = lRecordsProcessed + 1
        dTotalScore = dTotalScore + rsWeekScore.Fields(sWEEKSCORE)
        rsWeekScore.MoveNext
    Loop
   
    ‘If there aren’t three scores to add up, include the starting HC
   If lRecordsProcessed < 3 Then
        dAvgScore = (dTotalScore + GetStartingHC(lPlyr)) / (lRecordsProcessed + 1)
    Else
        dAvgScore = dTotalScore / lRecordsProcessed
    End If
   
    Handicap = CLng((dAvgScore – 36) * dSCALE)
   
End Function

I needed to have the optional week argument because I need to compute two different handicaps. The handicap through last week is the handicap you use for this weeks match play. The handicap through this week determines which golfer you play next week (The low handicap golfer from one two-man team plays the low handicap golfer from another).

Each time this function is called I’m creating a new recordset. So much for those speed considerations. I think what I should do is bring in the recordset to a global variable without any where clauses, then iterate through the records picking out what I need. I’m guessing that the additional overhead of looping through the records will be more than offset by savings from not creating a recordset. Time for a little testing.

Property Sniffer

Chris directed me to an interesting thread at Mr. Excel regarding inpsecting the properties of a cell. There’s two parts to this thread: Viewing the properties and looping through the properties. I’ve tried to create a program that loops through the properties by reading the library. I always start at Chip’s site and his TypeLib Utilities download. I pour through the code getting ever closer to understanding it. Then my head explodes and I give up. I’m not saying it’s possible even if I did understand it. Just that it seems like a good place to start, and that’s as far as I get.

You could easily put all the properties of a cell into a message box, but you’d have to hard code all the properties, which brings us back to looping through the properties. What would be nice, I think, is if the Properties dialog (Control Toolbox > Properties) had a Selection object choice in the dropdown. Then, the properties of whatever you had selected would show in that box, be it a range or a shape or whatever. Alas, it doesn’t.

The only other convenient place to find the properties, that I couldn’t think of, is the Locals Window. To that end, I wrote this:

Sub SniffRange()

    Dim cell As Range
   
    If TypeName(Selection) = “Range” Then
        Set cell = Selection
        Application.CommandBars.FindControl(, 1695).Execute ‘show the vbe
       Application.VBE.CommandBars.FindControl(, 2555).Execute ‘show the locals window
       Stop ‘keep the variable in scope
   End If
   
End Sub

It may be possible to 1) make “cell” global so that you don’t have to Stop the macro and 2) use API’s to show the Locals Windows without showing the VBE as if it was a child window of the main Excel application. I leave that to you.