Tuesday, January 30, 2007

OzFox Rocks!

As Craig Bailey has blogged, it's unfortunate that Craig Boyd had to pull out of OzFox. I'm bummed about it, not only because of the reason, but I was also looking forward to hanging out with Craig for several days. However, I'm also excited that Rick Schummer has stepped up to fill the void. Rick's been a good friend for years and is a great speaker, so while we'll miss Craig, the conference will still have a great set of sessions. Rick and I are going to do the keynote together in Craig's place. The two of us combined don't have the energy level of Craig (I remember whispering to him to breathe during the Southwest Fox 2006 keynote), but I think it'll be a great session anyway -- we'll be showing cool features in Sedna, including the Vista Toolkit Craig's been working on for Microsoft.

If you haven't registered for OzFox yet, hurry and do so before the cost goes up on Thursday. As anyone who attended the one in 2004 can tell you, this is a don't-miss conference. The energy and excitement levels were off the charts last time, and looking over the speaker and session list, I fully expect it to be the same this time.

Wednesday, January 24, 2007

Finding the Paths for Special Folders

Storing an application's data files in the correct place has gotten trickier under Windows Vista. Most developers simply write to INI files stored in the application folder (usually C:\Program Files\Some Folder) and store the data files in a Data subdirectory of that folder. While Microsoft strongly discouraged this practice in the past, now it's enforced; even if you're logged in as an administrator, writes to certain protected resources, such as C:\Program Files or the HKEY_LOCAL_MACHINE hive in the Registry, fail.

This would break most "legacy" apps (anything written before Vista), so to prevent that, they're run in XP compatibility mode. Through a process called virtualization, writes to protected resources are redirected by the operating system to somewhere else. For example, trying to write to a file in C:\Program Files\Some Folder causes that file to be created or updated in C:\Users\username\AppData\Local\VirtualStore\Program Files\Some Folder instead.

While it's great that Microsoft prevents our applications from breaking, this really just a short-term solution. It's better to revisit where your data is stored and use one of the preferred locations, such as C:\ProgramData for global settings and C:\Users\username\AppData\Local for user-specific data. The problem is that your shouldn't used a hard-coded location for these paths, especially if some users are on Vista and some on XP. Instead, use Windows API functions to determine those locations.

SpecialFolders.PRG is a wrapper for these functions. It returns the appropriate values regardless of whether the application is running in Vista or XP. See the comments in the header for a description of what parameters to pass. This code doesn't handle all special folders, only the more common ones, but it could easily be updated to support other folders as well.
*==============================================================================
* Program: SpecialFolders.PRG
* Purpose: Determine the path to the specified special folder
* Author: Doug Hennig
* Last revision: 01/24/2007
* Parameters: tuFolder - the folder to get the path for. Specify
* the CSIDL value of the desired folder (which can
* be obtained from:
* http://msdn.microsoft.com/library/default.asp?url=/library/en-us/shellcc/platform/shell/reference/enums/csidl.asp
* or use one of the following strings:
* "AppData": application-specific data
* "CommonAppData": application data for all users
* "Desktop": the user's Desktop
* "LocalAppData": data for local (nonroaming)
* applications
* "Personal": the My Documents folder
* Returns: The path for the specified folder or blank if the
* folder wasn't found
* Environment in: None
* Environment out: Error 11 occurs if tuFolder isn't specified
* properly
* Notes: This code was adapted from:
* http://msdn2.microsoft.com/en-us/library/aa140088(office.10).aspx
* Support for other CSIDLs can easily be added
*==============================================================================

lparameters tuFolder
local lcPath, ;
lnFolder, ;
lcFolder, ;
lnPidl, ;
lnPidlFound, ;
lnFolderFound

* Define the CSIDLs for the different folders.

#define CSIDL_APPDATA 0x1A
* Application-specific data:
* XP: C:\Documents and Settings\username\Application Data
* Vista: C:\Users\username\AppData\Roaming
#define CSIDL_COMMON_APPDATA 0x23
* Application data for all users:
* XP: C:\Documents and Settings\All Users\Application Data
* Vista: C:\ProgramData
#define CSIDL_DESKTOPDIRECTORY 0x10
* The user's Desktop:
* XP: C:\Documents and Settings\username\Desktop
* Vista: C:\Users\username\Desktop
#define CSIDL_LOCAL_APPDATA 0x1C
* Data for local (nonroaming) applications:
* XP: C:\Documents and Settings\username\Local Settings\Application Data
* Vista: C:\Users\username\AppData\Local
#define CSIDL_PERSONAL 0x05
* The My Documents folder:
* XP: C:\Documents and Settings\username\My Documents
* Vista: C:\Users\username\Documents

* Define some other constants.

#define ERR_ARGUMENT_INVALID 11
#define MAX_PATH 260
#define NOERROR 0
#define SUCCESS 1

* Test the parameter.

do case

* If it's numeric, assume it's a valid CSIDL; if not, the API function will
* return a blank string.

case vartype(tuFolder) = 'N'
lnFolder = tuFolder

* An invalid data type or empty folder name was passed.

case vartype(tuFolder) <> 'C' or empty(tuFolder)
error ERR_ARGUMENT_INVALID
return ''

* If a string was passed, convert it to the appropriate CSIDL.

otherwise
lcFolder = upper(tuFolder)
do case
case lcFolder = 'APPDATA'
lnFolder = CSIDL_APPDATA
case lcFolder = 'COMMONAPPDATA'
lnFolder = CSIDL_COMMON_APPDATA
case lcFolder = 'DESKTOP'
lnFolder = CSIDL_DESKTOPDIRECTORY
case lcFolder = 'LOCALAPPDATA'
lnFolder = CSIDL_LOCAL_APPDATA
case lcFolder = 'PERSONAL'
lnFolder = CSIDL_PERSONAL
otherwise
error ERR_ARGUMENT_INVALID
return ''
endcase
endcase

* Declare the API functions we need.

declare long SHGetSpecialFolderLocation in shell32 long hWnd, long nFolder, ;
long @ ppidl
declare long SHGetPathFromIDList in shell32 long Pidl, string @ pszPath
declare CoTaskMemFree in ole32 long pvoid

* Initialize the variables the API functions will update.

lcPath = space(MAX_PATH)
lnPidl = 0

* Get the path of the specified folder.

lnPidlFound = SHGetSpecialFolderLocation(0, lnFolder, @lnPidl)
if lnPidlFound = NOERROR
lnFolderFound = SHGetPathFromIDList(lnPidl, @lcPath)
if lnFolderFound = SUCCESS
lcPath = left(lcPath, at(chr(0), lcPath) - 1)
endif lnFolderFound = SUCCESS
endif lnPidlFound = NOERROR
CoTaskMemFree(lnPidl)
lcPath = alltrim(lcPath)
return lcPath

Automatically Sizing ListView Columns

I use the Microsoft ListView ActiveX control a fair bit. It has several benefits over display-only grids, including multiple row selection and easy support for images. I usually try to avoid having a horizontal scroll bar if possible, so I size the ListView and its columns so the data nicely fits. However, one thing that messes that up is when a vertical scroll bar appears, which automatically happens when there are more rows than the ListView can display. In that case, if you've sized the columns so they exactly fit the width of the ListView, you get a horizontal scroll bar because the vertical scroll bar takes away some of the width of the ListView that was used by columns.

To deal with this, I decided it was better to initially size one of the columns larger than it needs to be to display its data (if possible), then make it narrower if a vertical scroll bar appears. The only problem: how do you know when the vertical scroll bar is visible? There isn't any property indicating that, so I decided to take a brute force method to figure it out.

The following code assumes the ListView is named oList and has two columns, the second of which will be resized after loading the list. There are a couple of "magic" numbers here: 1 is the height of a grid line in the ListView and 22 is the height of the column headers. The code determines whether a vertical scroll bar appears by calculating the height of each item (the height of the font plus the grid line), multiplying by the number of rows, and adding the height of the column headers. If that's greater than the height of the ListView, there's a vertical scroll bar, so the column width is adjusted, accounting for the width of the ListView, the width of the first column, the width of the ListView border, and the width of the vertical scroll bar.
with This.oList
llScroll = .ListItems.Count * ;
(fontmetric(1, .Object.Font.Name, .Object.Font.Size) + ;
fontmetric(5, .Object.Font.Name, .Object.Font.Size) + 1) + ;
22 > .Height
.ColumnHeaders.Item(2).Width = .Width - ;
.ColumnHeaders.Item(1).Width - sysmetric(4) - ;
iif(llScroll, sysmetric(7), 0)
endwith
Adjust this code as necessary if you have more than two columns or want to resize a different column or more than one column.

Thursday, January 11, 2007

Tag, I'm It

I've been tagged by Alex Feldstein, so it's my turn to tell five things about me that you may not know.

1. I'm lucky to be alive. A nasty combination of curiosity and clumsiness have been my downfall, literally, countless times. Here are a few:

  • When I was very young (three and younger), I nearly killed myself many times. I swallowed a penny, which my mother luckily poked down just as I passed out. I climbed onto a box which I stacked on a stool which I placed on a chair to get into my parent's liquor stash and had a couple of glugs of whiskey. I placed two nails in my mouth and played elephant, trying to poke my tusks into an electrical outlet. I wanted to iron shirts like my mom did, but forgot to take my shirt off first. The folks in the ER got to know me pretty well.

  • I've broken a number of bones, including my right humerus (which wasn't funny), left wrist, and the joint in my right thumb, all of which while doing somewhat dumb things.

  • I've had three concussions, one pretty serious (unconscious for several hours). The first two were playing football, the last only a few years ago while watersliding (OK, that one's kind of embarrassing).

  • While swimming at the bottom of a waterfall, I was pulled into a very wicked set of rapids. I emerged about 400 meters later with bruises that lasted four months.

2. I have a Master's degree, sort of. After graduating with a Bachelor of Science degree in Biochemistry, I joined the Biology department of the University of Dallas at Texas as a PhD candidate. However, after a year and a half studying molecular genetics, I came to the realization that most university professors (which I figured I'd be one day) were really just managers -- they manage graduate students who do the real science and spend most of their time filling out grant applications and attending meetings. Since I'd owned a computer for about four years at this point (1982), I decided to find a job in the computer field and that was the end of my academic career. However, I discovered some years later that I'd completed enough courses to qualify for a non-thesis Master of Science degree, but by that time, it was too late to apply.

3. I love sports. I loved football in elementary school, playing every day, even during blizzards in the winter. I also swam a lot, and took diving classes. I started wrestling in high school and liked it, although I wasn't very good (if you've ever met me, you know upper body strength isn't a forte). I continued playing a lot of football (not organized -- I tried it once and didn't care for the brutal mentality of my coach). I ran cross-country and track (400 m, 4x100 m, and 4x400 m) in 12th grade and was fairly successful (3rd in the city in the 400 and set city records in the 4x100 and 4x400). In college, I started scuba diving (which I haven't done since then), played a lot of floor hockey (which I was also good at, thanks to being fast), and played football a couple of times a week. After college, I took up cross-country skiing, which I did for a few years. Currently, I play racquetball twice a week (I took it up twenty years ago this year) and golf about a dozen times in the summer. I took a snowboarding lesson last weekend with my son and loved it, so I'm expecting to add that to my regular list of sports.

4. I don't like hockey. OK, that's not completely true -- I watch one game a year or so. I'm one of the only able-bodied Canadian males who never played ice hockey. Even Calvin Hsia plays hockey and he's from Hawaii! It comes down to the fact that I can hardly skate. It seems completely anti-Canadian, but it just wasn't something I got into as a kid.

5. I was a disco king. I was in the Canadian Reserves (a communications squadron) during college in the late 70s and we had dances once a month or so. My girlfriend at the time wanted to do the disco thing, so we practiced a lot in her living room. We got pretty good and won a couple of dance contests. Fortunately, there are no pictures of me in my white suit (that I'm aware of), but I'm not afraid to admit that I still like disco music to this day.

OK, here are the people I'm tagging:

John Koziol
Ted Roche
Craig Boyd
Ken Levy
Cesar Chalom