Friday, April 27, 2007

Elevating Tasks in Vista

One of the Stonefield Query dialogs has a command button that allows the user to launch the ODBC Administrator, likely because they need to set up or modify a DSN. The button has "ODBC Admin" as its Caption and the following code in Click:
lcSystem = Thisform.oUtility.GetSystemDirectory()
try
run /n1 &lcSystem..odbcad32.exe
catch
endtry
(GetSystemDirectory is a utility method that returns the location of the Windows System folder; I'll show the code for that later.)

This worked great until I tried it on Vista; it fails there because the ODBC Administrator requires admin privileges and the RUN command doesn't support requesting elevation. Fortunately, there's an easy solution to that: use ShellExecute instead. I changed the code to:
lcSystem = oUtility.GetSystemDirectory()
try
oUtility.ShellExecute(lcSystem + 'odbcad32.exe', 'RunAs')
catch
endtry
(Like GetSystemDirectory, ShellExecute is another utility function. It runs the specified command. The cool thing about ShellExecute is that it'll launch the appropriate program. In the case of an EXE, it runs it. In the case of an HTML file, it launches the default browser. In the case of a DOC file, it launches Word.)

Now when the user clicks the button, they're requested to elevate to administrator before the ODBC Administrator appears. To make it obvious that this will happen, I've followed the Vista convention of adding a shield image to the button to alert the user that elevation will be requested. To do that, I set PicturePosition to 1-Left of Caption, Picture to VistaShield.BMP (an image I created using SnagIt to grab the shield icon displayed in the User Accounts control panel applet), and shortened Caption to just "ODBC" so it all fits. Here's what the button looks like:



Here's the code for GetSystemDirectory:
#define cnMAX_PATH 260
#define ccNULL chr(0)
local lcBuffer
lcBuffer = space(cnMAX_PATH)
declare integer GetSystemDirectory in Win32API ;
string @szBuffer, integer nLength
GetSystemDirectory(@lcBuffer, cnMAX_PATH)
return addbs(alltrim(left(lcBuffer, at(ccNULL, lcBuffer) - 1)))
Here's the code for ShellExecute:
lparameters tcFileName, ;
tcOperation, ;
tcWorkDir, ;
tcParameters
local lcFileName, ;
lcWorkDir, ;
lcOperation, ;
lcParameters, ;
lnShow
if empty(tcFileName)
return -1
endif empty(tcFileName)
lcFileName = alltrim(tcFileName)
lcWorkDir = iif(vartype(tcWorkDir) = 'C', alltrim(tcWorkDir), '')
lcOperation = iif(vartype(tcOperation) = 'C' and not empty(tcOperation), ;
alltrim(tcOperation), 'Open')
lcParameters = iif(vartype(tcParameters) = 'C', alltrim(tcParameters), '')
lnShow = iif(upper(lcOperation) = 'Print', 0, 1)
declare integer ShellExecute in SHELL32.DLL ;
integer nWinHandle, ; && handle of parent window
string cOperation, ; && operation to perform
string cFileName, ; && filename
string cParameters, ; && parameters for the executable
string cDirectory, ; && default directory
integer nShowWindow && window state
return ShellExecute(0, lcOperation, lcFilename, lcParameters, lcWorkDir, ;
lnShow)

Why I Haven't Signed

As many of you are aware, there's a petition VFP developers are being urged to sign to try to convince Microsoft to reverse their decision to not continue development of VFP. I think every VFP developer should sign this if they feel it will have an impact. However, to date, I, some other MVPs, and some prominent VFP people have not signed it.

Apparently, this is a problem for some people. A debate has been going on over on the FoxWiki (as well as other forums) about this. Some people have suggested that those who don't sign the petition have an agenda, that somehow we want VFP to die. Nothing could be further from the truth. This decision impacts my revenue as much as it does everyone else's. Others have suggested that we won't speak out against Microsoft, that somehow we're their cheerleaders or that we won't bite the hand that feeds (or as someone suggested, pays) us. Again, not true. I wish Microsoft hadn't made this decision. I've bitched about them plenty over things they've done in the past and likely will do so again in the future.

I can't speak for anyone else, but my reason for not signing the petition is simple: I believe it's a futile effort and I don't think someone should put their name on something they don't believe in.

However, I consider myself to be an open-minded person. If someone can give me a convincing argument about why I should sign the petition, I'll do so.

"It can't hurt" isn't a convincing argument. In fact, I'd argue that this petition has done more harm than good for our community, since some people now have an us (the signers) against them (non-signers) attitude and are looking for conspiracy reasons why someone won't sign.

"It only takes 30 seconds" isn't a convincing argument. This isn't about how long it takes; it's a matter of principle.

"It'll show you support the community" is also not a convincing argument. I think writing more than 100 articles over 10 years, presenting more than 200 sessions at conferences and user groups, writing blog entries describing how to do some complex things in VFP, being a co-administrator and a project leader for VFPX, and spending thousands of hours online helping other developers shows that I support our community as much as anyone else. The same is true of other MVPs. The very definition of an MVP is someone who spends a lot of personal time supporting the community. This whole argument that not signing the petition means you don't support is community is almost identical to another ludicrous argument: that if you don't believe in the war in Iraq, you don't support the troops. Like the petition, those two things have nothing to do with each other.

Please post your arguments on this blog. I have been convinced to change my position on issues in the past. All it takes it a well-thought out, logical (not emotional) argument.

Thursday, April 26, 2007

Scheduling Tasks

Stonefield Query has had a reports scheduler for several years. We simply use the Windows Task Scheduler to run Stonefield Query, with the name of the report to run and where to send the output as parameters, at the desired schedule. To communicate with the Task Scheduler, we use the freeware TaskScheduler.DLL, written by Mark Pryor. It doesn't look like the site I downloaded it from is available anymore, so it's in the download mentioned later.

However, after upgrading to Windows Vista, I discovered the scheduler didn't work. In researching this issue, I discovered that Vista includes Task Scheduler 2.0, while the DLL was written to support Task Scheduler 1.0, which has a completely different interface. So, back to the drawing board.

Fortunately, the MSDN documentation on Task Scheduler 2.0 has tons of detail and several examples in VBScript, which are easily converted to VFP. Because I still need to use Task Scheduler 1.0 with XP and earlier systems, I decided to create a TaskScheduler base class and XP and Vista subclasses of it.

I won't show the code for these classes here because it's fairly lengthy (you can download it from the Technical Papers page of my Web site), but here are some examples of how easy they are to use to schedule tasks. These examples run on Vista; use XPTaskScheduler instead for Windows XP or earlier.
* Create a task that runs at 3:00 AM every day.

loSchedule = createobject('VistaTaskScheduler')
with loSchedule
.TaskName = 'My Task Name'
.UserName = 'Your Windows UserName'
.Password = 'Your Windows Password'
.StartTime = {^2007-04-26 03:00:00}
.EXEName = 'Full path to EXE'
.EXEParameters = 'Any parameters to pass'
.ScheduleType = 1 && daily
if not .CreateTask()
messagebox(.ErrorMessage)
endif not .CreateTask()
endwith

* Create a weekly task that runs at 3:00 AM Tues, Thurs, and Sat
* of every second week.

loSchedule = createobject('VistaTaskScheduler')
with loSchedule
.TaskName = 'My Task Name'
.UserName = 'Your Windows UserName'
.Password = 'Your Windows Password'
.StartTime = {^2007-04-26 03:00:00}
.EXEName = 'Full path to EXE'
.EXEParameters = 'Any parameters to pass'
.ScheduleType = 2 && weekly
.Interval = 2
store .T. to .DaysOfWeek[3], .DaysOfWeek[5], .DaysOfWeek[7]
if not .CreateTask()
messagebox(.ErrorMessage)
endif not .CreateTask()
endwith

* Create a monthly task that runs at 3:00 AM on the 1st and 15th
* of every month.

loSchedule = createobject('VistaTaskScheduler')
with loSchedule
.TaskName = 'My Task Name'
.UserName = 'Your Windows UserName'
.Password = 'Your Windows Password'
.StartTime = {^2007-04-26 03:00:00}
.EXEName = 'Full path to EXE'
.EXEParameters = 'Any parameters to pass'
.ScheduleType = 3
store .T. to .DaysOfMonth[1], .DaysOfMonth[15]
.MonthsOfYear = .T. && initialize all 12 elements of array to .T.
if not .CreateTask()
messagebox(.ErrorMessage)
endif not .CreateTask()
endwith

Wednesday, April 18, 2007

Handling Multiple Monitors

One of the things I did when I got my new laptop is to set up multiple monitors. Developers I know and respect have raved about this for years, so I figured it was time to give it a try. Needless to say, I love it. I typically have browser and explorer windows open on the second monitor and keep the primary monitor for those things I live in all day long (VFP and Outlook, mostly). I'm more productive now that I'm not digging through stacks of windows and constantly moving or resizing one window or another.

However, one of the things I discovered today is that some of my applications don't respect the second monitor. For example, I have a class called SFPersistentForm that I drop on most of my forms. It saves the form size and position when the form is closed and restores it when the form is reopened, giving the user the experience they expect when working with that form. However, I discovered that if I opened a form and moved it to the second monitor then closed it, when I reopened it, the form displayed on the primary monitor instead (this was a form with Desktop set to .T. so it can exist outside the application's window).

I quickly found out why: the persistence code was trying to prevent the situation where the form may open outside the screen boundaries, making it invisible. The following code handled that:
Thisform.Width  = min(max(Thisform.Width,  0, Thisform.MinWidth), ;
_screen.Width)
Thisform.Height = min(max(Thisform.Height, 0, Thisform.MinHeight), ;
_screen.Height)
Thisform.Left = min(max(Thisform.Left, 0), _screen.Width - 50)
Thisform.Top = min(max(Thisform.Top, 0), _screen.Height - 50)
("- 50" is used to ensure the form doesn't start at exactly the right or bottom boundaries of the monitor, making it essentially invisible.)

There are actually two problems with this code. First, the reliance on _SCREEN assumed the form exists within _SCREEN; with a top-level form or one with Desktop set to .T., that's not necessarily the case. Second, even if _SCREEN is maximized, that only fits it in the current monitor. If the form is on the other monitor, _SCREEN's dimension are irrelevant.

I initially changed the code to:
if Thisform.Desktop or Thisform.ShowWindow = 2
lnWidth = sysmetric(1)
lnHeight = sysmetric(2)
else
lnWidth = _screen.Width
lnHeight = _screen.Height
endif Thisform.Desktop ...
Thisform.Width = min(max(Thisform.Width, 0, Thisform.MinWidth), ;
lnWidth)
Thisform.Height = min(max(Thisform.Height, 0, Thisform.MinHeight), ;
lnHeight)
Thisform.Left = min(max(Thisform.Left, 0), lnWidth - 50)
Thisform.Top = min(max(Thisform.Top, 0), lnHeight - 50)
However, it turns out that SYSMETRIC() only returns values for the primary monitor. So, I changed those two statements to:
declare integer GetSystemMetrics in Win32API integer
#define SM_CXVIRTUALSCREEN 78 && Virtual Width
#define SM_CYVIRTUALSCREEN 79 && Virtual Height
lnWidth = GetSystemMetrics(SM_CXVIRTUALSCREEN)
lnHeight = GetSystemMetrics(SM_CYVIRTUALSCREEN)
Now the form reopens in the exact same size and position, including the monitor it was on.

Update: As I posted in a comment, this code correctly handle the situation where the form was originally placed on a second monitor but now that monitor doesn't exist. However, it didn't handle the case where the secondary monitor is on the left, so the starting X position is negative. See the code in the comment to deal with that.