Archive for April 2008

Efficient Looping

In a previous post, I demonstrated how to use constants to improve your code. Then everyone started beating up my loop. The code wasn't solving a real life problem, so I just threw any old loop together. It wasn't relevant because that's not what the post was about. To fight back, I created a highly improbably backstory in the comments to make my loop look at least as efficient as everyone else's.

But no matter how crazy my story was, I couldn't subvert Peltier's comment about reading the range into an array. So I tried to see what kind of time differences we're talking about. I wrote this code:

Sub DoBoth()
   
    Dim lStart As Long
    Dim i As Long
   
    lStart = Timer
        For i = 1 To 10
            FindTotals2
        Next i
    Debug.Print Timer - lStart
    lStart = Timer
        For i = 1 To 10
            UseArray
        Next i
    Debug.Print Timer - lStart
   
End Sub
 
Sub FindTotals2()
   
    Dim rCell As Range
   
    Const sFIND As String = "Total"
   
    For Each rCell In Sheet1.Columns(1).Cells
        If Left$(rCell, Len(sFIND)) = sFIND Then
            'Do something
        End If
    Next rCell
   
End Sub
 
Sub UseArray()
   
    Dim vArr As Variant
    Dim i As Long
   
    Const sFIND As String = "Total"
   
    vArr = Sheet1.Columns(1).Value
   
    For i = LBound(vArr) To UBound(vArr)
        If Left$(vArr(i, 1), Len(sFIND)) = sFIND Then
            'Do something
        End If
    Next i
   
End Sub

And got these results:

The array is quite a bit faster. I don't think Timer is hyper-accurate, but relatively the differences are pretty clear.

Testing Strings Using Left

The "wrong" way:

Sub FindTotals()
   
    Dim rCell As Range
   
    For Each rCell In Sheet1.Columns(1).Cells
        If Left$(rCell, 5) = "Total" Then
            'Do something
        End If
    Next rCell
   
End Sub

The "right" way:

Sub FindTotals2()
   
    Dim rCell As Range
   
    Const sFIND As String = "Total"
   
    For Each rCell In Sheet1.Columns(1).Cells
        If Left$(rCell, Len(sFIND)) = sFIND Then
            'Do something
        End If
    Next rCell
   
End Sub

Both "right" and "wrong" are in quotes because, like many best practices, it can be a matter of taste and preference. In the second method, I can change the string in one place and I'm done. In the first, if I change the string, I also have to change the second argument of Left$().

Printing First Page of Email in Outlook

I print out my email. No, I'm not one of the Luddites that prints out every email (or has his secretary do it) because he doesn't understand how to work his email machine. My system for organization is loosely based on Getting Things Done and its derivative 43 Folders. Everything I have to do is represented by at least one piece of paper. So don't try to convince me to quit printing my email, because it's not going to happen.

If I get an email that requires some action in the future, I need one of these pieces of paper to represent that future action. Until recently, I would print the email to serve that purpose. As you already know, email can get quite long and that means I can end up with 10 sheets of paper where I only needed one. When you deal with accountants and lawyers, it's even worse because each of their messages in the thread has a page of disclaimers. Oddly, Outlook's print dialog doesn't let you specify a page range. There are some work arounds, but none of them are suitable for me. I read all my email in plain text and I reply in plain text (HTML is for web pages, not email). Since I'm using plain text, I use Outlook's built-in email editor rather than Word.

After I printed seven pages of an email today, I decided to finally write some code. I put a button on my new email commandbar and hooked it up to this procedure:

Sub PrintOnePage()
   
    Dim mi As MailItem
    Dim sBody As String
    Dim wdApp As Word.Application
   
    Const sORIG As String = "> -----Original Message-----"
   
    If TypeName(Application.ActiveInspector.CurrentItem) = "MailItem" Then 'only mail
        'create a forward to get the header
        Set mi = Application.ActiveInspector.CurrentItem.Forward
        sBody = mi.Body
        sBody = Mid(sBody, InStr(1, sBody, sORIG), 5000) 'Remove inserted signature
        Set wdApp = New Word.Application
       
        With wdApp.Documents.Add
            .Range.Text = sBody
            .PageSetup.LeftMargin = 18 '.25"
            .PageSetup.RightMargin = 18
            .PageSetup.TopMargin = 18
            .PrintOut False, False, wdPrintFromTo, "", "1", "1"
        End With
       
        wdApp.Quit False 'don't save changes
        Set wdApp = Nothing
        mi.Close olDiscard 'don't save changes
        Set mi = Nothing
    End If
   
End Sub

I didn't want to automate Word to do this, but I struggled with other options to limit it to one page. I know that printing out of Outlook puts 60 lines on the first page. However, when I tried to limit the text to the first 60 vbNewLine's, it didn't quite work out. I started to think that maybe Outlook doesn't put a vbNewLine after each line, but rather after each paragraph. I'm still not sure why that didn't work. So I resigned to automate Word and use it's page range feature to limit the print out.

I limit this to MailItems although it may work on other objects. I didn't want to test it. The MailItem I work with is a forwarded copy of the original. When I forward an email it puts the header information at the top of each email in the thread, so I get some needed information on my print out. Unfortunately, it also puts my signature in there, so I have to strip that part out by starting the string at the Original Message part. And I limit the string to 5,000 because that should be more than one page and there's no need to transfer more than that.

I kept getting a type mismatch error when using Word's PrintOut method. At first I thought it was because I was omitting optional arguments, but that really shouldn't be the case when I'm early binding. I seem to remember a problem with optional arguments using late binding - specifically that you have to include all optional arguments up until the one you want to include, then none after that. But I was still getting the error. Inexplicably, Word wants Strings for page numbers. You'll notice that my page numbers are in quotes.

Finally, I close Word without saving changes and discard the forwarded copy of the email.

VBE Bookmark add-in for Office 2000-2007

Hi all

New page on my site with a very useful add-in from Jim Rech.
http://www.rondebruin.nl/vbebookmarks.htm

VBEBookmarks.dll is a simple COM Add-in created by Jim Rech for the Microsoft Office 2000-2007 Visual
Basic Editor which lets you bookmark up to five locations in VBA projects for easy navigation among them.

Have fun

Ron de Bruin
http://www.rondebruin.nl/tips.htm

I’ve Arrived

I'm in Seattle for the MVP Summit. This hotel is great. I just downed a whole jar of complimentary Macademia nuts and... What's this thing that looks like a menu? Damn!

I'm off to register and score some of the Rock Bottom brew. I'll be sure to have one for those of you who couldn't make it this year.

Mike Won!

Mike from the Northeast won the 4-year anniversary contest. A copy of Dalgleish's Excel Pivot Table Recipes is soon to be rushing to his house from wherever Amazon rushes stuff. In the contest, I asked for your favorite non-Excel blogs, and here are some of them:

Office Live Small Business... - Why are Microsoft brand names a whole paragrah? Just say it already!
The Secret Diary of Steve Jobs
Sasha Frere-Jones
101-things-in-1001-days - I was going to do that once - too hard.
Pogue's Posts
Stuff White People Like - A little racism for variety.
Information Aesthetics
Hot Air
Old New Thing
Sean MacNair - Kiss over Zep? C'mon!
Experiences of an English Soldier
Gizmodo - I quit reading gadget sites for some reason. Maybe it's time to start again.
Creed Thoughts - Possibly my favorite non-Simpsons show

I haven't read many of these blogs and don't endorse them. But I casually glanced at them and will probably give them a second look.

I'm heading to Seattle tomorrow for the MVP Summit. If you read blogs by other Excel MVPs, you know we like to drink at the Rock Bottom. As is my tradition, I got a new pair of shoes right before the summit. I bought Sketchers this time because I'm trying to look younger. It must be a mid-life crisis thing. Now if I could just learn to balance on a skateboard.

Tab Groups

Scott wishes:

G’day Dick,

My wish list would be to have the functionality to group excel tabs within a workbook. So instead of having 20+ tabs going along the bottom of the screen you could group (similar to TabScrip in VBA ) them into a common theme. Maybe group all the raw data tabs, input sheets, output reports, State summaries, Yearly reports, etc.

Now, if you have some finance VBA code that already does it……then here’s your chance to make a wish come true. 

Cheers matey.

I couldn't find a picture of TabScrip, and if he meant TabStrip, then I don't know what he's talking about. You can color your tabs to group them. You can't move the tabs to any other position except the bottom, as far as I know.

I have a few workbooks that have a lot of tabs. In those rare situations I wish for better navigation of tabs. If you right click on the VCR buttons to the left of the tabs, you can quickly select a tab. If you have more than 16 tabs, you can't see them all. Instead you get a "More Sheets" options that brings up an unsortable dialog.

First, I'd like that right click list to go all the way to the top of the screen. Don't stop at 16, stop at 30 or some other number that gives me more options. If I do need to open the dialog, I want three sort options: the order they appear in the workook; alphabetical order; and zOrder (the order in which they were last accessed). I guess that's six sorts with ascending and descending options. I don't really need this because I only have a handful of really big workbooks. But it would be handy in those cases.

What do you think about Scott's wish or tabs in general?

Copy Paste to External Application

Sometimes I find myself copy-pasting between Excel and another application.
In this example, I have a table of three columns: First Name, Last Name, Birth Date.
My external application has 3 text boxes, one for each of those values.

I can't just copy the 3 cells from Excel and paste them to my App, because they would all end up in the first text box!
But, by running VBA SendKeys with a specially crafted string, I can send keystrokes for tabbing to the 2nd and 3rd text boxes.

I also need to activate the SendKeys procedure only when my cursor is positioned correctly, or things could get messy.

My approach is to run a macro that sits there listening for F6 before activating SendKeys.
I've also included listening for the Esc key, just in case I change my mind.

Declare Function GetAsyncKeyState Lib "user32" (ByVal vKey As Long) As Integer
 
' Virtual-Key Codes http://msdn2.microsoft.com/en-us/library/ms645540(VS.85).aspx
Const VK_F6 = &H75
Const VK_ESC = &H1B
 
Sub Scanning()
    Dim lngRow As Long, str As String
 
    lngRow = Selection.row
    str = Cells(lngRow, 1) & vbTab & Cells(lngRow, 2) & vbTab & _
            Format(Cells(lngRow, 3), "dd-mmm-yyyy")
 
    MsgBox "Click OK, then click the First Name box on the external application, then press F6 on the keyboard"
    WaitAndSend str, VK_F6, VK_ESC
End Sub
 
Sub WaitAndSend(SendString As String, ExecuteKey As Long, CancelKey As Long)
    Do
        DoEvents
        If GetAsyncKeyState(CancelKey) <> 0 Then Exit Do
        If GetAsyncKeyState(ExecuteKey) <> 0 Then
            SendKeys SendString, True
            Exit Do
        End If
    Loop
End Sub