Tuesday, May 04, 2010

Southwest Fox 2010 News

Some exciting news about Southwest Fox 2010:

  • Speakers and sessions have been announced. Organizers once again had a very tough time selecting from the excellent submissions. I’m personally really looking forward to the Windows 7 sessions presented by Craig Boyd and Steve Ellenoff and Bo Durban’s Direct2D session. Actually, I want to see every session, but of course that’s not possible. Thank goodness one of the requirements of speaking at Southwest Fox is providing white papers so you still get the content from every session, even those you miss.
  • New this year is a Web Development track with seven regular sessions, two pre-conference sessions, and four post-conference sessions (see below) in this track alone.
  • We also have three new (well, new to Southwest Fox) speakers, continuing our tradition of trying to invite new speakers every year: Kevin Cully, Uwe Habermann, and Venelina Jordanova. Kevin is likely familiar to many people, as he has attended Southwest Fox before and hosted a conference himself a few years ago. Although Uwe and Venelina are new to North American conferences, they both are veteran speakers at European conferences, including both German and Prague DevCons.
  • A free one-day "VFP to Silverlight" workshop is being held the day after Southwest Fox ends (Monday, October 18), sponsored by the German FoxPro User Group (dFPUG) and presented by Uwe and Venelina. I’m planning on attending this and suspect it’ll be very popular.
  • Southwest Fox platinum sponsor Tomorrow's Solutions, LLC is offering a scholarship of $150 for one new attendee. Also, White Light Computing has changed their scholarship to cover TWO attendees this year.

See you in Phoenix in October!

Tuesday, March 30, 2010

Fixing a Report Designer Bug

A bug in the VFP Report Designer has always, ahem, bugged me: something seems to turn on the Printer Environment setting for a report. Unless you want a report to always use a certain printer, that setting should be off. Turning it on can make a report much slower to open (for example, if the printer saved with the report isn’t available on your system) and can cause other problems.

I don’t remember who discovered this (sorry) but the cause turned out to be clicking the font button in the Field or Label Properties dialogs, even if you then choose Cancel. But how would that affect the Printer Environment setting?

Tired of having to deal with this, I decided to track it down. Fortunately, the source code for the Report Designer dialogs is included with VFP (unzip HOME() + “TOOLS\XSource\XSource.ZIP” and look in the resulting VFPSource\ReportBuilder folder). After some digging, I found the culprit in the ChooseFont method of FRXFormatUtil (in FRXBuilder.VCX), which is called when you click the font button. That method checks whether the TAG memo field in the header record of the FRX is empty or not. If not, it uses SYS(1037, 3) to update TAG and TAG2 from the current printer environment (the code is actually in FRXCursor.PopPrintEnv, which ChooseFont calls). The idea is that if the report has a saved printer environment, that environment should be used since it may impact which fonts are available. However, here’s the bug: TAG may not be empty even when TAG2 (which contains the binary printer environment) is. I’ve seen a single CHR(8) in TAG when TAG2 is empty, which means there is no saved printer environment, but it passes the NOT EMPTY(TAG) check in ChooseFont so the printer environment of the report is overwritten.

The solution is simple: change the test in ChooseFont from IS NOT EMPTY(TAG) to IS NOT EMPTY(TAG2), then rebuild ReportBuilder.APP. I’ve already made this change in the upcoming Stonefield Query version 4.0.

Wednesday, March 17, 2010

Taking out the Slow Parts, Again

While looking up something else, I came across Brad Martinez’s article on extending the functionality of the TreeView control. One of his points about how to load the TreeView more quickly caught my eye:

Make sure the parent Node's Sorted property is set to False: If Sorted = True, the TreeView must sort every Node as it is added under the parent Node. Make Sorted = True after all child Nodes are added.

I wondered how much of an improvement it would make, so I changed some generic TreeView loading code I use in lots of places to follow his advice. Loading 1,329 nodes dropped from 4.128 to 0.533 seconds, which is almost eight times faster!

As I’ve said before, I love taking out the slow parts!

Monday, February 22, 2010

TRY … CATCH and text merge

I recently ran into a problem with some text merge code. Under certain conditions, the text merge file contained only the first part of the text being output. I had a hard time tracking it down until I found this article in which someone else ran into the same problem. The culprit was a function I called from within the text merge code; that function has a TRY … CATCH structure and under some conditions, an error occurred and the CATCH caught it. The issue is that when CATCH fires, it sets _TEXT, the variable containing the handle for the text merge output file, to –1, preventing further output to the file.

The solution is to save the current value of _TEXT, set it to –1, execute the code with the TRY … CATCH structure, and reset _TEXT to the saved value at the end. Temporarily setting _TEXT to –1 prevents the file from being closed if an error occurs.

lnText = _text
_text = -1
try
* some code here
catch
* some code here
endtry
_text = lnText

Thursday, February 04, 2010

A Replacement for FULLPATH()

Are you as annoyed as I am that FULLPATH() returns the full path for a file as upper-case? That makes it a little hard to respect the case of a user-entered filename. Fortunately, the GetFullPathName Windows API function doesn’t change the case. Here’s a little function that accepts a filename and returns the full path using that API function:

lparameters tcName
local lcBuffer1, ;
lcBuffer2, ;
lnLen
#define MAX_PATH 260
declare long GetFullPathName in Win32API ;
string lpFileName, long nBufferLength, string @lpBuffer, ;
string @lpFilePart
store space(MAX_PATH) to lcBuffer1, lcBuffer2
lnLen = GetFullPathName(tcName, MAX_PATH, @lcBuffer1, @lcBuffer2)
return left(lcBuffer1, lnLen)

Wednesday, February 03, 2010

Multiple Monitor Class

Almost three years ago, I wrote a blog post on handling multiple monitors. Since then, I’ve refactored the code so all the monitor-handling code is in one place.

There are actually two classes: SFSize, which simply has properties that represent the dimensions of a monitor, and SFMonitors, which does the work. SFMonitors is actually a subclass of SFSize because it uses those same properties for the virtual desktop (all combined monitors if there’s more than one).

Here’s the code for SFSize:

define class SFSize as Custom
nLeft = -1
nRight = -1
nTop = -1
nBottom = -1
nWidth = 0
nHeight = 0

function nLeft_Assign(tnValue)
This.nLeft = tnValue
This.SetWidth()
endfunc

function nRight_Assign(tnValue)
This.nRight = tnValue
This.SetWidth()
endfunc

function nTop_Assign(tnValue)
This.nTop = tnValue
This.SetHeight()
endfunc

function nBottom_Assign(tnValue)
This.nBottom = tnValue
This.SetHeight()
endfunc

function SetWidth
with This
.nWidth = .nRight - .nLeft
endwith
endfunc

function SetHeight
with This
.nHeight = .nBottom - .nTop
endwith
endfunc
enddefine


SFMonitors has several methods. Init sets up the Windows API functions we’ll need and gets the dimensions for the primary monitor:



define class SFMonitors as SFSize
nMonitors = 0
&& the number of monitors available

function Init
local loSize

* Declare the Windows API functions we'll need.

declare integer MonitorFromPoint in Win32API ;
long x, long y, integer dwFlags
declare integer GetMonitorInfo in Win32API ;
integer hMonitor, string @lpmi
declare integer SystemParametersInfo in Win32API ;
integer uiAction, integer uiParam, string @pvParam, integer fWinIni
declare integer GetSystemMetrics in Win32API integer nIndex

* Determine how many monitors there are. If there's only one, get its size.
* If there's more than one, get the size of the virtual desktop.

with This
.nMonitors = GetSystemMetrics(SM_CMONITORS)
if .nMonitors = 1
loSize = .GetPrimaryMonitorSize()
.nRight = loSize.nRight
.nBottom = loSize.nBottom
store 0 to .nLeft, .nTop
else
.nLeft = GetSystemMetrics(SM_XVIRTUALSCREEN)
.nTop = GetSystemMetrics(SM_YVIRTUALSCREEN)
.nRight = GetSystemMetrics(SM_CXVIRTUALSCREEN) - abs(.nLeft)
.nBottom = GetSystemMetrics(SM_CYVIRTUALSCREEN) - abs(.nTop)
endif .nMonitors = 1
endwith
endfunc


GetPrimaryMonitorSize returns an SFSize object for the primary monitor. Note that this takes into account the Windows Taskbar and any other desktop toolbars, which reduce the size of the available space.



  function GetPrimaryMonitorSize
local lcBuffer, ;
loSize
lcBuffer = replicate(chr(0), 16)
SystemParametersInfo(SPI_GETWORKAREA, 0, @lcBuffer, 0)
loSize = createobject('SFSize')
with loSize
.nLeft = ctobin(substr(lcBuffer, 1, 4), '4RS')
.nTop = ctobin(substr(lcBuffer, 5, 4), '4RS')
.nRight = ctobin(substr(lcBuffer, 9, 4), '4RS')
.nBottom = ctobin(substr(lcBuffer, 13, 4), '4RS')
endwith
return loSize
endfunc


Pass GetMonitorSize X and Y coordinates and it’ll figure out what monitor contains that point and return an SFSize object containing its dimensions, again accounting for the Taskbar.



  function GetMonitorSize(tnX, tnY)
local loSize, ;
lhMonitor, ;
lcBuffer
loSize = createobject('SFSize')
lhMonitor = MonitorFromPoint(tnX, tnY, MONITOR_DEFAULTTONEAREST)
if lHMonitor > 0
lcBuffer = bintoc(40, '4RS') + replicate(chr(0), 36)
GetMonitorInfo(lhMonitor, @lcBuffer)
with loSize
.nLeft = ctobin(substr(lcBuffer, 21, 4), '4RS')
.nTop = ctobin(substr(lcBuffer, 25, 4), '4RS')
.nRight = ctobin(substr(lcBuffer, 29, 4), '4RS')
.nBottom = ctobin(substr(lcBuffer, 33, 4), '4RS')
endwith
endif lHMonitor > 0
return loSize
endfunc
enddefine


SFMonitors uses the following constants:



#define MONITOR_DEFAULTTONULL    0 
#define MONITOR_DEFAULTTOPRIMARY 1
#define MONITOR_DEFAULTTONEAREST 2

#define SM_XVIRTUALSCREEN 76 && virtual left
#define SM_YVIRTUALSCREEN 77 && virtual top
#define SM_CXVIRTUALSCREEN 78 && virtual width
#define SM_CYVIRTUALSCREEN 79 && virtual height
#define SM_CMONITORS 80 && number of monitors


Here’s some code that uses SFMonitors. Code (not shown here) before the following code reads a form’s previous Height, Width, Top, and Left from somewhere (such as the Registry) from the last time the user had it open into custom nHeight, nWidth, nTop, and nLeft properties, and then sizes and moves the form (referenced in loForm) to those values. This code makes sure the form isn’t off the screen, which can happen if, for example, the user had the form open on a second monitor but now only has one monitor, such as an undocked laptop. Note that this code uses several SYSMETRIC() functions to determine the height and width of the window border and title bar, since those values aren’t included in a form’s Height and Width. Also note in the comment a workaround for a peculiarity with an “in top-level form” being restored to a different monitor than the top-level form it’s associated with.



loMonitors = newobject('SFMonitors', 'SFMonitors.prg')

* For desktop or dockable forms, get the size of the virtual desktop. If
* there's only one monitor, use the primary monitor size. Otherwise, use the
* size of whichever monitor the form is on.

if pemstatus(loForm, 'Desktop', 5) and (loForm.Dockable = 1 or ;
loForm.Desktop or loForm.ShowWindow = 2)
if loMonitors.nMonitors = 1
loSize = loMonitors
else
loSize = loMonitors.GetMonitorSize(.nLeft, .nTop)
endif loMonitors.nMonitors = 1
lnMaxLeft = loSize.nLeft
lnMaxTop = loSize.nTop
lnMaxWidth = loSize.nWidth
lnMaxHeight = loSize.nHeight
lnMaxRight = loSize.nRight
lnMaxBottom = loSize.nBottom

* For any other forms, use the size of _screen.

else
lnMaxLeft = 0
lnMaxTop = 0
lnMaxWidth = _screen.Width
lnMaxHeight = _screen.Height
lnMaxRight = lnMaxWidth
lnMaxBottom = lnMaxHeight
endif pemstatus(loForm ...

* Only restore Height and Width if the form is resizable.

llTitleBar = pemstatus(loForm, 'TitleBar', 5) and loForm.TitleBar = 1
lnBorderStyle = iif(pemstatus(loForm, 'BorderStyle', 5), ;
loForm.BorderStyle, 0)
if lnBorderStyle = 3
loForm.Width = min(max(.nWidth, 0, loForm.MinWidth), lnMaxWidth)
loForm.Height = min(max(.nHeight, 0, loForm.MinHeight), lnMaxHeight)
endif lnBorderStyle = 3

* Calculate the total width of the form, including the window borders.

if llTitleBar
lnTotalWidth = loForm.Width + ;
iif(loForm.BorderStyle = 3, sysmetric(3), sysmetric(12)) * 2
else
lnTotalWidth = loForm.Width + ;
icase(lnBorderStyle = 0, 0, ;
lnBorderStyle = 1, sysmetric(10), ;
lnBorderStyle = 2, sysmetric(12), ;
sysmetric(3)) * 2
endif llTitleBar
do case

* If we're past the left edge, move it to the left edge.

case .nLeft < lnMaxLeft
loForm.Left = lnMaxLeft

* If we're past the right edge of the screen, move it to the right edge.

case .nLeft + lnTotalWidth > lnMaxRight
loForm.Left = lnMaxRight - lnTotalWidth

* We're cool, so put it where it was last time. If this form has ShowWindow
* set to 1-In Top-Level Form and the current top-level form is on a
* different monitor than the saved position, do this code twice; the first
* time, it gives a value that places the form on the wrong monitor but it
* works the second time.

otherwise
loForm.Left = .nLeft
loForm.Left = .nLeft
endcase

* Calculate the total height of the form, including the title bar and window
* borders.

if llTitleBar
lnTotalHeight = loForm.Height + sysmetric(9) + ;
icase(lnBorderStyle = 3, sysmetric(4), sysmetric(13)) * 2
else
lnTotalHeight = loForm.Height + ;
icase(lnBorderStyle = 0, 0, ;
lnBorderStyle = 1, sysmetric(11), ;
lnBorderStyle = 2, sysmetric(13), ;
sysmetric(4)) * 2
endif llTitleBar
do case

* If we're past the top edge, move it to the top edge.

case .nTop < lnMaxTop
loForm.Top = lnMaxTop

* If we're past the bottom edge of the screen, move it to the bottom edge.
* Note that we have to account for the height of the title bar and top and
* bottom window frame.

case .nTop + lnTotalHeight > lnMaxBottom
loForm.Top = lnMaxBottom - lnTotalHeight

* We're cool, so put it where it was last time.

otherwise
loForm.Top = .nTop
endcase

Wednesday, January 20, 2010

My First HTML Help Builder Add-In

Although I’ve used West Wind HTML Help Builder to create HTML Help (CHM) files for my applications for more than 10 years, and have done lots of advanced things such as supporting dynamic text and generating help projects programmatically, I haven’t created an add-in for it. HTML Help Builder supports add-ins using a simple mechanism: once you’ve registered an add-in, it appears in the Tools, Add-Ins menu, ready to run whenever you need it.

The add-in I created does two things:

  • Fixes the icon for the INDEX topic. Every HTML Help Builder project has one topic of type INDEX, and I use that as the “welcome to this help file” topic. Unfortunately, when it builds the CHM, HTML Help Builder uses the wrong icon for that topic. I always had to manually run HTML Help Workshop, edit the icon for that topic (from “11” to “auto”), and rebuild the CHM file. My add-in does that automatically.
  • Turns on searching for the HTML content. In addition to providing CHM files, we post our help files as HTML on our Web site (for example, the Stonefield Query SDK help file), both so Google can index it and so we can provide links to help topics in support messages without having to say “open the help file, open the “How To” heading, then navigate to the “Whatever” topic”. HTML Help Builder recently added a search function to the HTML files it generates, but the generation of that function is turned off by default and I often forget to turn it on. My add-in automatically changes one of the generated files to enable search.

Add-ins can be VFP code (PRG or APP/EXE), a .Net assembly, or a COM object. You register an add-in using the Tools, Add-In Manager function. This is the only cumbersome part of the process: because the add-ins registry table (AddIns.DBF) is stored in the program folder, under Vista or Windows 7 it’s read-only, so you have to launch HTML Help Builder as administrator. Perhaps in a future release, author Rick Strahl will move this file to a writable folder so this isn’t necessary.

image

In my case, I created a class named FixHelp in FixHelp.PRG, and put the code for the add-in into the Activate method. Here’s the code for the class (thanks to Chris Wolf for handling the 64-bit stuff). It should be easy enough to follow.

#define CSIDL_PROGRAM_FILES 0x0026
#define HKEY_LOCAL_MACHINE -2147483646

define class FixHelp as custom
function Activate(toHelpForm)
local loHelp, ;
lcProjectFile, ;
lcPath, ;
lcFile, ;
lcText, ;
lcProgramFiles, ;
lcRegVCX, ;
loRegistry, ;
lcKey, ;
llGotPath, ;
lcLogFile

* Get a reference to the help object, then figure out the path for the
* current project.

loHelp = toHelpForm.oHelp
lcProjectFile = loHelp.cFileName
lcPath = addbs(justpath(lcProjectFile))

* Turn on searching in case it wasn't turned on when the files were
* generated.

lcFile = lcPath + 'index2.htm'
lcText = filetostr(lcFile)
lcText = strtran(lcText, 'var AllowSearch = false;', ;
'var AllowSearch = true;')
strtofile(lcText, lcFile)

* Remove the image number for the root node so it defaults to "auto".

lcFile = forceext(lcProjectFile, 'hhc')
lcText = filetostr(lcFile)
lcText = strtran(lcText, '' + ;
chr(13) + chr(10), '', 1, 1)
strtofile(lcText, lcFile)

* Find the location of HTML Help Workshop. Try the 32-bit registry key
* first. If that doesn't work, try the 64-bit key.

lcProgramFiles = This.GetSpecialFolder(CSIDL_PROGRAM_FILES)
lcRegVCX = addbs(lcProgramFiles) + ;
'Microsoft Visual FoxPro 9\FFC\Registry.vcx'
loRegistry = newobject('Registry', lcRegVCX)
lcPath = ''
lcKey = '\Microsoft\Windows\CurrentVersion\App Paths\hhw.exe'
llGotPath = loRegistry.GetRegKey('Path', @lcPath, ;
'SOFTWARE' + lcKey, HKEY_LOCAL_MACHINE) = 0
if not llGotPath
llGotPath = loRegistry.GetRegKey('Path', @lcPath, ;
'\SOFTWARE\Wow6432Node' + lcKey, HKEY_LOCAL_MACHINE) = 0
endif not llGotPath

* Compile the CHM file if we found it. Log the results.

if llGotPath
lcPath = This.ShortPath(forcepath('hhc.exe', lcPath))
lcProjectFile = '"' + forceext(lcProjectFile, 'hhp') + '"'
lcLogFile = '"' + addbs(justpath(lcProjectFile)) + 'log.txt"'
erase (lcLogFile)
run &lcPath &lcProjectFile > &lcLogFile
if file(lcLogFile)
declare integer ShellExecute in SHELL32.DLL ;
integer nWinHandle, string cOperation, string cFileName, ;
string cParameters, string cDirectory, integer nShowWindow
ShellExecute(0, 'Open', lcLogFile, '', '', 1)
else
erase (lcLogFile)
endif file(lcLogFile)
else
messagebox('Cannot locate HTML Help Workshop')
endif llGotPath
return .T.
endfunc

* Get the short (8.3) path for the specified path.

function ShortPath(tcPath)
local lcPath, ;
lnLength, ;
lcBuffer, ;
lnResult
declare integer GetShortPathName in Win32API ;
string @lpszLongPath, string @lpszShortPath, integer cchBuffer
lcPath = tcPath
lnLength = 260
lcBuffer = space(lnLength)
lnResult = GetShortPathName(@lcPath, @lcBuffer, lnLength)
return iif(lnResult = 0, '', left(lcBuffer, lnResult))
endfunc

* Get the location of the specified "special" folder.

function GetSpecialFolder(tnFolder)
local lcSpecialFolderPath, ;
lcPath
lcSpecialFolderPath = space(255)
declare SHGetSpecialFolderPath in shell32.dll ;
long hwndOwner, string @cSpecialFolderPath, long nWhichFolder
SHGetSpecialFolderPath(0, @lcSpecialFolderPath, tnFolder)
lcPath = alltrim(lcSpecialFolderPath)
return lcPath
endfunc
enddefine


Now when I want to generate a help file, I click Build Help, select the “Don’t build Help file” option (since my add-in will generate the CHM, there’s no need to do it twice), and let it run. I then choose Tools, Add-Ins, Generate Help File (my add-in) to tweak the generated files and build the CHM. There’s a couple of less manual tasks for my build process.

Wednesday, December 30, 2009

DateTimes Through OLEDB in VFP

Rob Eisler wrote an interesting blog post about an issue he ran into dealing with certain DateTime values stored in SQL Server accessed in VFP through OLEDB.

Tuesday, December 29, 2009

Axialis Software Sale

I regularly use Axialis Software’s IconWorkshop to create icons and other images. Good news: it (and other Axialis products) are on sale for half-price until the end of the year. That makes IconWorkshop just $34.95!

Friday, December 18, 2009

OEM Agreement with Dydacomp

Here’s a link for a press release about an OEM agreement Stonefield Software and Dydacomp recently signed:

http://finance.yahoo.com/news/Stonefield-Software-Inc-Maker-bw-2728191172.html?x=0&.v=1