Archive for August 2007

Positioning a Userform Over a Cell

keepITcool has developed a method to position a userform over a specific range. Finding a range’s screen coordinates is tricky. I usually resort to “close enough”.

He says:

First I found a bug. Excel 97 thru 2007.
The VisibleRange of Pane 2 and 3 (in a 4 pane window) are inconsistent.
depending on the sequence in which the vertical and horizontal split bars are set it will sometimes
return the range of the upper-right pane, sometimes the lower-left… and in both panes it may return activepane.index = 2
I found the cause and a workaround.

I soon got desperate using PointsToScreenPixels with non-standard zoom. Then I found that using XLM macro’s is the only reliable way to consistently get the “crosshair” on the screen under a variety of splits, zooms and display options. Some fiddling to offset from that point in case of frozen panes.. et voila!

You can download RangePos Beta1.zip.

Update: Download RangePos Beta3.zip

Ian’s Favorite Excel Sites

Table Drive Ribbon

Mike Alexander has updated his table driven Ribbon customizer. You can find it here:

http://www.datapigtechnologies.com/Custom_UI_Builder.zip

He says:

The utility now creates and adds a module with all the call-back functions to the newly created buttons, allowing the buttons to work right away.

As always, the source code is open for anyone who wants to build on this and create a better table-driven Ribbon Customizer.

If you try it out, post your comments here.

Collection Add and Functions

When I was creating sample data for Returning a Limited Collection…, I had to fill a bunch of collections with custom classes. Instead of creating the class and adding it to the collection, I created a function that returned the class and use the function call in the Add method. Here’s an example:

Set clsGroup = New CGroup
clsGroup.Name = “Group1″
Set colContacts = New Collection
colContacts.Add CreateContact(“Dick Kusleika”, _
    “Company1″, “NE”), “Dick Kusleika”
colContacts.Add CreateContact(“John Doe”, _
    “Company2″, “NE”), “John Doe”
colContacts.Add CreateContact(“Jane Doe”, _
    “Company2″, “MA”), “Jane Doe”
Set clsGroup.Contacts = colContacts
colGroups.Add clsGroup, clsGroup.Name

The CreateContact function looks like this:

Function CreateContact(sName As String, sCompany As String, _
    sState As String) As CContact
   
    Dim clsCon As CContact
   
    Set clsCon = New CContact
   
    clsCon.Name = sName
    clsCon.Company = sCompany
    clsCon.State = sState
   
    Set CreateContact = clsCon
   
End Function

I never thought of calling a function directly from the Add method of a Collection, but I can’t think of any reason not to do it.

Most Impressive Excel App?

What’s the most impressive Excel application that you’ve seen?

Today I had an opportunity to revisit Ivan F. Moala’s ImageToXcel, and I spent some time looking at the VBA code. It gets my vote. Yowsers!

Other nominations?

Excel 2003 Lessons

Are you still using Excel 2003? Me too. Microsoft has some lessons that you can listen to, or read, I guess. Unfortunately, I can’t put them on my iPod and I don’t see me sitting in front of my computer listening to a 50 minute lesson on macro security. One of you guys do it and tell me if they’re any good. :)

Returning a Limited Collection from a Class

I have a custom class, CGroup, which has children of the type CContact (another custom class). I need to loop through all the CContacts for each CGroup and print out those that meet a certain condition – in this example, those with State property of “NE”. Not all CGroups will have a CContact that meets the condition. The CContact class has Name, Company, and State properties. The CGroup class has a Name property and Contacts collection. In addition, it has a read-only ContactsState property that returns a subset of the Contacts collection that have the provided state.

Public Property Get ContactsState(sState As String) As Collection
   
    Dim i As Long
    Dim colTemp As Collection
   
    Set colTemp = New Collection
   
    For i = 1 To mcolContacts.Count
        If mcolContacts.Item(i).State = sState Then
            colTemp.Add mcolContacts.Item(i), mcolContacts.Item(i).Name
        End If
    Next i
   
    Set ContactsState = colTemp
   
End Property

The data looks like this:

Excel Immediate Window

The final report looks like this:

Excel Immediate Window

In my main sub, I could loop through all of the CContacts for each CGroup and test the State property. If I do that, I need to know beforehand how many CContacts I have in that CGroup so I can tell it when (or if) to print the dotted line that separates them. I could create a property in CGroup that returns that number, but as long I’m looping through the whole collection, I figured I might as well grab the ones I need and return a collection with only those CContacts. I’m not sure if this really saves any cycles because I’m creating a new collection and adding to it rather than just increment a Long counter. But it does have the added advantage of cleaning up the non-class code by removing all that testing for the State.

Sub ListNE()
   
    Dim clsGroup As CGroup
    Dim colGroups As Collection
    Dim colConState As Collection
    Dim i As Long, j As Long
   
    Set colGroups = New Collection
   
    FillGroups colGroups
   
    For i = 1 To colGroups.Count
        Set clsGroup = colGroups(i)
       
        Set colConState = clsGroup.ContactsState(“NE”)
       
        For j = 1 To colConState.Count
            Debug.Print clsGroup.Name, colConState(j).Name, colConState(j).State
            If j = colConState.Count Then
                Debug.Print String(50, “-”)
            End If
        Next j
    Next i
   
End Sub

By working with the limited collection, colConState, it’s easy to determine when I’ve reached the last CContact with NE for the State. Since colConState is an empty collection for Group2 (because it has no CContacts with NE), the inner loop never gets run. Even though I don’t show it here, it’s probably more important that I properly destroy all of my class references using this method.

You can download LimitChildren.zip to see the whole thing if you like.

Double Click to Exclude Numbers

I have a table of numbers and formulas for each row, column, and for the table as a whole. The table is part of a report – the output of the application. The user wants to exclude certain numbers from the calculations after reviewing them. These numbers would be outliers and would skew the results. The calculations are AVERAGE and STDEV functions. If a number is excluded, it needs to still be shown on the reports, but with a strikethrough format.

The obvious course is to modify the formula when the user has identified a cell to exclude. With formulas for every row, column, and for the whole table, that’s a pretty big job. An easier way is to change the numbers to text. Both AVERAGE and STDEV ignore text, so this would have the effect of excluding the numbers from the formulas without having to change the formulas. I started with something like this:

With Target
    If .Font.Strikethrough Then
        .Value = CDbl(.Value)
        .Font.Strikethrough = False
    Else
        .Value = “‘” & .Value
        .Font.Strikethrough = True
    End If
End With

This is in the worksheet’s BeforeDoubleClick event. I use the strikethrough property to determine if the number has already been excluded. The user can double click the number to toggle between inclusion and exclusion. Excluded numbers have an apostrophe put in front of them (making them text) and the font is changed to strikethrough. Included numbers are changed back to a Double (using CDbl) and the strikethrough is removed.

Incidentally, not every number can be excluded. I’ve applied a particular style to those numbers that can be excluded and I limit the event like this:

If Target.Style.Name = “TBData2″ Then

A new wrinkle appeared. Now some of the numbers are actually formulas. That complicates the above code snippet a little.

excel range

With Target
    If .HasFormula Then
        lStart = 2
    Else
        lStart = 1
    End If
   
    If .Font.Strikethrough Then
        .Formula = “=” & Mid(.Formula, Len(“=TEXT()”), _
            Len(.Formula) – Len(“=TEXT()”,”) – Len(.NumberFormat))
        .Font.Strikethrough = False
    Else
        .Formula = “=TEXT(“ & Mid(.Formula, lStart, Len(.Formula)) & _
            “,”“” & .NumberFormat & “”“)”
        .Font.Strikethrough = True
    End If
End With

Instead of putting an apostrophe in front of the value to make it text, I surround it with the TEXT function. This has the added benefit of keeping the same number format applied to the text as was applied to the number. When a number is excluded (the Else part), I start with “=TEXT(“. Then I repeat the existing formula, removing the equal sign if there was one (Mid(.Formula, lStart, Len(.Formula))). The suffix to this string manipulation is the existing NumberFormat surrounded by double quotes.

When a number is included, the TEXT portion of the formula is removed. The Mid function starts at Len(“=TEXT()”), which is a verbose way of saying 7. The length of Mid is the length of the formula, minus the length the text function (including parentheses, the comma that separates the number format argument, and the quotes that surround the number format), minus the length of the numberformat.

This has the strange side effect of converting a number like 3 into a formula like =3 when it’s toggled. I can’t think of any ill effects of that, but there may be.

Two new range functions: Union and Subtract

This post discusses two functions I developed because of a recent need. The first is an enhanced version of the Excel Union method. The other is a Subtract function that operates on ranges.

The Union function

Those who use the Excel Application’s Union method with any sense of regularity know it doesn’t deal well with any argument being ‘nothing.’ Consequently, it is almost second nature to code


Sub RoundAboutCode()
    Dim Rng1 As Range, Rng2 As Range, Rslt As Range
    '...
    If Rng1 Is Nothing Then
        Set Rslt = Rng2
    ElseIf Rng2 Is Nothing Then
        Set Rslt = Rng1
    Else
        Set Rslt = Application.Union(Rng1, Rng2)
        End If
    End Sub

Recently, I found myself writing the above code for the 3rd time in a few days. Annoyed at not having modularized it years ago, I did just that. It’s below.


Function Union(Rng1 As Range, Rng2 As Range) As Range
    If Rng1 Is Nothing Then
        Set Union = Rng2
    ElseIf Rng2 Is Nothing Then
        Set Union = Rng1
    Else
        Set Union = Application.Union(Rng1, Rng2)
        End If
    End Function

With the above, one can code the below without worrying about whether Rslt or NextCell is Nothing.


        Set Rslt = Union(Rslt, NextCell)

True, unlike the Union method, the function accepts only 2 arguments. I did write a more generic function declared with a ParamArray argument only to discover that the Union method won’t accept a single variant (‘argument not optional’ error) or an array (‘type mismatch’ error). Maybe someone else can make the more generic case work.

The Range Subtract function

I also had reason to write a Subtract function. Given two ranges, Rng1, and Rng2, where Rng2 is a subset of Rng1, the result is Rng1 – Rng2, i.e., all those cells in Rng1 that are not part of Rng2.

I remembered a post by Tom Ogilvy from a long time ago that used the following method: In a temporary worksheet, in the range corresponding to the address of Rng1 enter some constant (say the value 1). Next, clear the cells corresponding to the address of Rng2, and finally, pick up the result with the SpecialCells method.


Function SubtractUsingWS(Rng As Range, RngToSubtract As Range)
    If Application.Intersect(Rng, RngToSubtract).Address _
        <> RngToSubtract.Address Then Exit Function

    Dim OldEventsValue
    OldEventsValue = Application.EnableEvents
    Application.EnableEvents = False
    On Error GoTo Finally1

    With ThisWorkbook.Worksheets("TempWS")
    .Cells.ClearContents
    .Range(Rng.Address).Value = 1
    .Range(RngToSubtract.Address).ClearContents
    Set SubtractUsingWS = _
        Rng.Parent.Range(.Cells.SpecialCells(xlCellTypeConstants).Address)
        End With
    ThisWorkbook.Saved = True
Finally1:
    Application.EnableEvents = OldEventsValue
    End Function

Sub testSubtract2()
    MsgBox SubtractUsingWS(Range("a1:C3"), Range("b2")).Address
    End Sub

The above is probably as efficient as one can get but concerned about issues such as write privileges, multiple people accessing an add-in on a network drive, etc., I wrote up a solution from first principles, as it were.


Function SubtractFirstPrinciples(Rng1 As Range, Rng2 As Range) As Range
    On Error Resume Next
    If Application.Intersect(Rng1, Rng2).Address <> Rng2.Address Then _
        Exit Function
    On Error GoTo 0
    Dim aCell As Range
    For Each aCell In Rng1
        Dim Rslt As Range
        If Application.Intersect(aCell, Rng2) Is Nothing Then
            Set Rslt = Union(Rslt, aCell)
            End If
        Next aCell
    Set SubtractFirstPrinciples = Rslt
    End Function

Sub testSubtractFirstPrinciples()
    Debug.Print SubtractFirstPrinciples( _
        Sheets(1).Range("A1:f10"), _
        Sheets(1).Range("A1,b2,c3,d4:e5,f6")).Address
    End Sub

The advantage of working from first principles is that it works correctly irrespective of the shape of the two arguments Rng1 and Rng2. We don’t have to worry about whether they consist of multiple areas or not. The disadvantage, of course, is that it checks each cell in Rng1 and consequently might be slow under certain circumstances.

Before proceeding further, one should remember that the need for any optimization is unproven. I don’t know what, if any, problems the first solution will run into nor do I know how slow the solution based on first principles will be. So, the benefits of the optimizations below are somewhat uncertain. By contrast, it is certain that there will be some cost to developing the code, testing it, and maintaining it.

The first step in optimization would be to start small: a single area from which we want to subtract a single area. Clearly, in this case the result will be at the most four ranges as shown below. The first image shows the area that we want to subtract in yellow. The second image shows the four areas that will remain after the subtraction operation is completed.
Rng1 is the entire range shown; Rng2, the range we want to subtract, is in yellow.
The result of the subtraction is four ranges. It would be three if the range to be subtracted touched one of the edges, and just two if it was in a corner.

The code below is a function that accepts two range arguments Rng1 and Rng2 and returns a range that corresponds to Rng1 – Rng2. It validates that each range consists of a single area. I don’t know what it means to subtract Rng2 from Rng1 if there is absolutely no overlap between the two ranges. So, I made the assumption that the result should be Rng1 itself. Before proceeding with the analysis, the code computes what part of Rng2 is actually within Rng1. Each of the four If statements enclose a block of code that calculates one of the four possible ranges in the result (see the above image). Finally, the code returns the result of the subtraction.


Function subtractOneArea(Rng1 As Range, inRng2 As Range) As Range
    If Rng1.Areas.Count > 1 Then Exit Function
    If inRng2.Areas.Count > 1 Then Exit Function
    If Application.Intersect(Rng1, inRng2) Is Nothing Then
        Set subtractOneArea = Rng1
        Exit Function
        End If
    Dim Rng2 As Range
    Set Rng2 = Application.Intersect(Rng1, inRng2)
    Dim aRng As Range, OKRng As Range, Rslt As Range, WS As Worksheet
    Set WS = Rng1.Parent
    If Rng2.Row > Rng1.Row Then
        Set Rslt = WS.Range(Rng1.Rows(1), Rng1.Rows(Rng2.Row - Rng1.Row))
        End If
    If Rng2.Row + Rng2.Rows.Count < Rng1.Row + Rng1.Rows.Count Then
        Set Rslt = Union(Rslt, _
            WS.Range(Rng1.Rows(Rng2.Row - Rng1.Row + Rng2.Rows.Count + 1), _
                Rng1.Rows(Rng1.Rows.Count)))
        End If
    If Rng2.Column > Rng1.Column Then
        Set Rslt = Union(Rslt, WS.Range(WS.Cells(Rng2.Row, Rng1.Column), _
            WS.Cells(Rng2.Row + Rng2.Rows.Count - 1, Rng2.Column - 1)))
       End If
    If Rng2.Column + Rng2.Columns.Count < Rng1.Column + Rng1.Columns.Count Then
        Set Rslt = Union(Rslt, _
            WS.Range(WS.Cells(Rng2.Row, Rng2.Column + Rng2.Columns.Count), _
                WS.Cells(Rng2.Row + Rng2.Rows.Count - 1, _
                    Rng1.Column + Rng1.Columns.Count - 1)))
        End If
    Set subtractOneArea = Rslt
    End Function

With the building block in place, writing the Subtract function to calculate Rng1 – Rng2 is a lot easier. All we need to do is accumulate the result as we loop through each area of Rng1 and subtract from it each area of Rng2.


Function Subtract(Rng1 As Range, Rng2 As Range) As Range
    On Error Resume Next
    If Application.Intersect(Rng1, Rng2).Address <> Rng2.Address Then _
        Exit Function
    On Error GoTo 0
    Dim Rslt As Range, Rng1Rslt As Range, J As Integer, I As Integer
    For J = 1 To Rng1.Areas.Count
        Set Rslt = subtractOneArea(Rng1.Areas(J), Rng2.Areas(1))
        For I = 2 To Rng2.Areas.Count
            Set Rslt = Application.Intersect( _
                Rslt, subtractOneArea(Rng1.Areas(J), Rng2.Areas(I)))
            Next I
        Set Rng1Rslt = Union(Rng1Rslt, Rslt)
        Next J
    Set Subtract = Rng1Rslt
    End Function

The code is used as in the following example:


Sub testSubtract()
    Debug.Print Subtract( _
        Sheets(1).Range("A1:f10"), _
        Sheets(1).Range("A1,b2,c3,d4:e5,f6")).Address
    Debug.Print Subtract( _
        Sheets(1).Range("A1:f9,a10:f10"), _
        Sheets(1).Range("A1,b2,c3,d4:e5,f6")).Address
    Debug.Print Subtract( _
        Sheets(1).Range("$I$1:$K$4,$L$4:$N$8,$K$7:$K$13"), _
        Sheets(1).Range("$K$4:$L$4,$K$8:$L$8")).Address
    End Sub

At some point a variant of the above will show up in the “publications and training” section of my web site.

And, that concludes all I have to share on this subject…at least for the time being.

Quick Notes

Wow, five posts in five days. That hasn’t happened in a while. Too bad I couldn’t finish it off with an Excel post. Here are some random thoughts.

Facebook:

Sean said

Get with it…if you aren’t using it already, now’s a good time to give it a try.

So I signed up. I don’t get it. I have two friends. I joined two groups whose members are MVPs and I’m not sure what the difference is or is supposed to be. I used to work at KPMG, but I can’t join that group because I don’t work there anymore. The university I attended has a group with 20,000 members. The utility of joining that group is suspect. I can’t join the Daniel J. Gross High School group because I’m not a student there.

I checked out the Omaha, Nebraska group. I thought Facebook was MySpace for college educated people. It turns out that it’s MySpace for people who are no longer teens, but haven’t quite hit 25 yet. To wit, this Facebook app. All I can say is WTF.

Mower:

I’m still using my mower and it’s going well. I need to do an in-depth review, as I promised, but reviews are hard.

Excel projects:

My goal this summer was to write an office add-in and sell it. In order to achieve that goal, I will have to turn down hourly consulting work. To date, I have not been able to do that. I hope to develop the intestinal fortitude soon.

I have a big office project at work, but I won’t be doing it. I’m so busy doing non-Excel work at my real job and doing Excel work at my fake job that I don’t have time to do the Excel work at my real job. Instead, I’m currently writing a spec so someone else can do it.

Ruby on Rails:

Instead of developing an office add-in, I’m considering developing a web app using Ruby on Rails. I think it would be fun.

JMT Utilities:

JMT Excel Consulting updated JMT Utilities (somewhat) recently. Also, they’re working on a version for 2007.

Tablet

I decided to get a tablet last winter, then talked myself out of it. I’m currently trying to talk myself back into it. If and when I get one, I’ve decided to get a Motion Computing tablet. I don’t need a tablet, it would be purely for entertainment purposes. What do you think: 47? flat screen or a tablet?

Apple TV

My dad recently got an Apple TV. It’s pretty cool, but I can’t get past iTunes. I use iTunes for my iPod, but I simply refuse to buy downloaded music through iTunes. I buy the CD through Amazon and rip the MP3′s. It’s way more work, but if I ever change devices, my songs go with me.

Bloglines:

Recently I changed my Bloglines preferences to show only feeds with new posts. Why didn’t I do that before? How long will it take before I discover that one blog that hasn’t had a post in years? Do I even care?

DailyLit:

Which book should I “read” first?

Dayton:

I really appreciate that the Dayton airport offers free WiFi. It’s a shame that both Bloglines and Delicious are blocked by something called WebMarshal.

webmarshal dayton airport wifi

My attempt has been recorded? Thanks Big Brother.