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

Monday, November 09, 2009

Executable Signing with Inno Setup

As I discussed a couple of years ago, code signing your executables is important for a variety of reasons. Our installation creation tool of choice is Inno Setup and version 5.2.4 released in March 2009 made it easier to sign the setup executable through the new Signtool directive in the [Setup] section.

This directive works like this (assuming you already have a digital certificate):

  • Run the Inno Setup UI and choose Configure Sign Tools in the Tools menu.
  • Create a new “sign tool” by clicking Add and specifying a name and the command line to execute your signing application. For example, I created one called “Standard” with the following command line to call Microsoft’s SIGNTOOL.EXE:

"C:\Program Files\Microsoft Visual Studio 8\SDK\v2.0\Bin\signtool.exe" sign /f CertPath\mycert.pfx /p MyPassword

where CertPath\mycert.pfx is the name and location of the PFX file and MyPassword is the password.

  • In the [Setup] section of your Inno Setup script (ISS) file, add the following:

SignTool=Standard /d $qStonefield Query Installer$q $f

Specify the name of your “sign tool” in place of “Standard” and a descriptive name for your setup program in place of “Stonefield Query Installer.” $q is an Inno Setup constant representing a quote and $f is a constant containing the name and path of the setup EXE created by Inno Setup.

Now, every time you build your setup using the Inno Setup UI, your setup executable is automatically signed.

However, I also like to build setups as part of a build process, so I call the Inno Setup compiler via the command line. Once I added the SignTool directive in my ISS files, my command line builds failed.

Here’s the solution: you need to add the same “sign tool” definition you did to the UI to the command line. For example:

"C:\Program Files\Inno Setup 5\iscc" "/sStandard=C:\Program Files\Microsoft Visual Studio 8\SDK\v2.0\Bin\signtool.exe sign /f CertPath\mycert.pfx /p MyPassword $p" sfquery.iss

Now, whether I build using the UI or through a build process, my setup executable is always signed.

Friday, November 06, 2009

Previewing PDF Attachments in Outlook 2007

One thing that’s always annoyed me about Outlook is that there’s no built-in previewer for PDF attachments on emails. I always had to double-click the attachment to write it out to a temporary file and then open the application (Adobe Reader or whatever) to view it.

If necessity is the mother of invention, annoyance is the mother of Google (or Bing or whatever) searches. Tim Heuer’s Foxit PDF Preview Handler sounded pretty good, so after installing Foxit (ya gotta love a tool with “Fox” in its name), I installed Tim’s previewer and voila, PDF preview in Outlook. Thanks, Tim!

Monday, October 26, 2009

Dates for Southwest Fox 2010 Announced

At the closing session for Southwest Fox 2009, we announced the dates for Southwest Fox 2010: October 14-17, 2010. Planning for next year’s conference started before Southwest Fox 2009 had even commenced.

SWFox2010_Animated