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