Sara Ford blogged Friday that all VFPX projects are now accessible using their name as a subdomain. This makes the URL for VFPX the nice and short http://vfpx.codeplex.com.
Monday, March 02, 2009
Replacement for SYS(2014)
SYS(2014) is a great little function that helps make your applications portable. Since it returns the relative path of a file to a specific folder, you can use it on an absolute file name (such as one returned from GETFILE()) and store the relative rather than absolute path.
However, one thing that's always bugged me about SYS(2014) is that it returns the path in upper-case. If you want to display the path to the user, they'll wonder why the path is upper-cased in your program but in a different case on disk.
Fortunately, there's a simple Windows API function you can call that does the same thing as SYS(2014) but respects the case. Here's an example of how to use that function:
(UPDATE: Walt Krzystek pointed out that GetRelativePath returned a blank string if the two paths are on different drives. Also, I neglected to trim trailing spaces from the return value.)
function GetRelativePath(tcTo, tcFrom)
#define FILE_ATTRIBUTE_DIRECTORY 0x10
#define FILE_ATTRIBUTE_NORMAL 0x80
#define MAX_PATH 260
declare integer PathRelativePathTo in shlwapi.dll ;
string @out, string @from, integer fromattrib, ;
string @to, integer toattrib
lcPath = space(MAX_PATH)
lcFrom = iif(vartype(tcFrom) = 'C', tcFrom, ;
sys(5) + curdir()) + chr(0)
lnFromAttrib = iif(directory(lcFrom), FILE_ATTRIBUTE_DIRECTORY, ;
FILE_ATTRIBUTE_NORMAL)
lcTo = iif(vartype(tcTo) = 'C', tcTo, ;
sys(5) + curdir()) + chr(0)
lnToAttrib = iif(directory(lcTo), FILE_ATTRIBUTE_DIRECTORY, ;
FILE_ATTRIBUTE_NORMAL)
PathRelativePathTo(@lcPath, @lcFrom, lnFromAttrib, @lcTo, lnToAttrib)
lcPath = alltrim(strtran(lcPath, chr(0), ' '))
if empty(lcPath)
lcPath = tcTo
endif empty(lcPath)
return lcPath
Sunday, March 01, 2009
Cleaning up _MemberData
The MemberData Editor, the replacement New Property/Method dialog, and the fantastic new PEM Editor all make it easy to get VFP to display the desired case for your custom properties and methods in IntelliSense and the Properties window rather than the lame lower-case it normally uses. The secret is _MemberData, a custom property added to a form or class that contains XML providing additional information about members, such as display case, whether they appear in the Favorites tab of the Properties window, and scripts to execute when you double-click properties in the Properties window.
One of the problems with _MemberData, though, is that it's limited to 8K. If you've ever worked with XML, you know it's very wordy and it doesn't take long to blow through the 8K limit. This is especially a problem with subclasses: not only do they contain the _MemberData XML for their own custom members, they also contain the XML for all parent class members. The deeper you go in a class hierarchy, the longer the XML gets and the closer you get to the 8K limit.
However, a little known fact about _MemberData is that it's cumulative: VFP internally combines the XML from the current class and all parent classes. Even if you blank _MemberData in a subclass, your members still display in the proper case. So, one way to keep that 8K limit at bay is to strip everything but the XML for the members added in the current class from _MemberData.
That would be a tedious chore to do manually. Fortunately, a couple of new features in version 4.0 of PEM Editor (currently in development) make this a breeze. The Cleanse MemberData function in the shortcut menu does this explicitly, but PEM Editor also does this automatically when you add a new member to a class.
If you can't wait for PEM Editor 4.0, I've uploaded a small PRG that does the same thing (in fact, it's the same code as in PEM Editor 4.0) to the Technical Papers page of the Stonefield Web site. To use it, open a class or form, then DO MDCleaner.PRG. Just to be safe, please back up your form or class first.
Tuesday, February 24, 2009
Southwest Fox Call for Speakers
Today we released the Call for Speakers for Southwest Fox 2009. Anyone interested in presenting should visit http://www.swfox.net/callforspeakers.aspx, read the Call for Speakers document, and download the proposal submission application. Session proposals are due by March 16.
We're adding a track this year called Technology for the VFP Developer. It'll cover things other than VFP that make life easier for FoxPro developers, like source control and virtual machines. Of course, we still plan to offer a good selection of topics in core VFP development, extending VFP, using VFP with other technologies, and of course, VFPX.
If you think you have something to say to the VFP community, please submit session proposals, even if you've never spoken at a conference before. Our community is strengthened when more people take an active role. Do be aware that speaking at a conference is a serious commitment. Even for experienced speakers, preparing a new session takes 40-80 hours. Doing it well at the conference calls for several rehearsals beforehand, too. So make sure you can commit the necessary time--the Call for Speakers lays out all the deadlines.
We're looking forward to seeing what you all come up with. Reading through the proposals we receive each year is really exciting, and choosing among them is always a challenge.
Tuesday, February 10, 2009
Using Tables Over 2GB in VFP
J.D. Mullin posted a video showing how to use Advantage Database Server to access DBF files that are over 2 GB in size, something you can't do if you access the table natively in VFP.
Friday, January 16, 2009
Playing with Windows 7
I've been playing with Windows 7 for about a week now. Like Andrew MacNeill, I installed it on a Virtual PC and also like Andrew, I got a BSOD when I tried to install Virtual Machine Additions (and in fact had to restore Windows 7 to a previous restore point to get it to work again). Fortunately, I remembered GIYF ("Google is your friend") and quickly found that Virtual PC SP1 was needed for Windows 7. After upgrading to SP1, I had no problems installing Virtual Machine Additions (which VPC can be a PITA to deal with if you don't).
I have a few other glitches right now, but they are all "Windows 7 on VPC" issues rather than Windows 7 issues:
- No Aero Glass or related features (like the new Aero Peek). Come to think of it, I don't get Aero Glass when running Vista in a VPC either.
- No sound.
- Dragging a file from my main system to the VPC window gives an error. Copying a file from a shared folder on my main system to the VPC also has problems.
There are lots of cool things about Windows 7; Tim Sneath has blogged about some of them. The new taskbar and the concept of libraries will likely take some getting used to, but so far it feels like Vista with more fit-and-finish things taken care of.
Wednesday, January 14, 2009
PEM Editor is a Must-Have Tool
Jim Nelson has done a ton of work on the PEM Editor, the new name for the former Edit Property/Method replacement dialog project on VFPX that Marcia Akins originally created. He's made this into an incredible tool that I used about a hundred times a day. It completely replaces the native Edit Property/Method, New Property, and New Method dialogs, and in fact can even replace the Properties and Document View windows to some extent. Because it's modeless, resizable, and dockable, you never have to close it if you don't want to. Its customizable color coding and filtering abilities make working with forms and classes a lot easier. This is simply one of those must-have tools.
If you haven't already installed it, do yourself a favor and download it today. Installing it is as simple as DO PEMEditor.APP. From then on, you'll wonder what you ever did without it.
Changing the Color and Orientation of Images
Recently, I needed to change the color of an image. I have a container that includes an image and depending on the user's color choices, the container's background color might change. That means certain colors in the image must also change so the image blends in with the container properly, and rather than creating a number of images with pre-defined colors, I wanted to change the colors programmatically. One other wrinkle: because the orientation of the container might also change under some circumstances, I also wanted to rotate the image.
Fortunately, the VFPX GDIPlusX project makes this a snap and thanks to a recent blog posting by Cesar, it didn't take too long to figure out what to do. The following code reads the color of the upper left pixel in the image whose file name is in the lcImageFile variable and converts that color to the one specified in This.BackColor. It also rotates the image if it's wide and short but the container is narrow and tall. It then writes the new image directly to the PictureVal property of the imgImage object so there's no additional disk I/O.
* Create the GDIPlusX objects if necessary.
if type('_screen.System.Name') <> 'O'
do System.APP
endif type('_screen.System.Name') <> 'O'
with _screen.System.Drawing
* Load the image.
loBmp = .Bitmap.FromFile(lcImageFile)
* Get the color of the upper left pixel.
loColor = loBmp.GetPixel(0, 0)
* Create a color map and tell it to convert from the color of
* the pixel to our background color.
loColorMap = .Imaging.ColorMap.New()
loColorMap.OldColor = loColor
loColorMap.NewColor = .Color.FromRGB(This.BackColor)
* Create an attributes object and make it use the color map.
loAttr = .Imaging.ImageAttributes.New()
loAttr.SetRemapTable(loColorMap)
* Create a new image and draw the old image onto it using the new
* color mapping.
loDestBmp = .Bitmap.New(loBmp.Width, loBmp.Height, ;
.Imaging.PixelFormat.Format24bppRGB)
loGfx = .Graphics.FromImage(loDestBmp)
loRect = loBmp.GetBounds()
loGfx.DrawImage(loBmp, loRect, loRect, .GraphicsUnit.Pixel, loAttr)
* Flip the image if its orientation doesn't match ours.
if This.Width > This.Height and loBmp.Height > loBmp.Width
loDestBmp.RotateFlip(.RotateFlipType.Rotate90FlipNone)
endif This.Height > This.Width ...
* Write the image directly to the PictureVal property of our image
* object.
loDestBmp.Save(This.imgImage, .Imaging.ImageFormat.Bmp)
endwith
Wednesday, December 17, 2008
Microsoft Security Update
Microsoft recently released a security update that lists Visual FoxPro 8 and 9 amongst the affected applications. In case you're wondering exactly what this is about, here are the details. This update provides patched versions of the following ActiveX controls that ship with VFP:
- MSWinSck.ocx (Microsoft WinSock Control 6.0)
- MSMask.ocx (Microsoft Masked Edit Control 6.0)
- MSFlxGrd.ocx (Microsoft FlexGrid Control 6.0)
- MSHFlxGd.ocx (Microsoft Hierarchical FlexGrid Control 6.0)
- MSChrt20.ocx (Microsoft Chart Control 6.0)
- ComCt232.ocx (Microsoft Animation Control 5.0, Microsoft UpDown Control 5.0)
- MSComCt2.ocx (Microsoft Animation Control 6.0, Microsoft Date and Time Picker 6.0, Microsoft Flat Scrollbar Control 6.0, Microsoft MonthView Control 6.0, Microsoft UpDown Control 6.0)
In addition to updating your own system, remember to distribute the updated versions of any OCXs your applications use to your clients.
Tuesday, December 09, 2008
ATC(), Only Better
Because our flagship product, Stonefield Query, is a reporting tool, we do a LOT of parsing here, especially expression parsing. One parsing task we frequently do is look for the existence of a field in an expression. It's not as simple as it sounds. For example, suppose you have a SQL statement like this stored in a variable named lcSelect:
select Employees.FirstName,Employees.LastName,Employees.CountryOfBirth from ...
and you want to know if the Country field is involved in the query. Unfortunately, ATC('Country', lcSelect) will return a false positive because "Country" is contained within "CountryOfBirth".
One of Stonefield's genius developers, Trevor Mansuy, wrote a replacement for ATC() called ATCWord that searches for words rather than substrings. He used the VBScript regular expression parser to do the hard work. So, using the example above, ATCWord('Country', lcSelect) returns 0.
There are a lot of comments in ATCWord that explains how it works. Thanks, Trevor.
Update: Mike Potjer tweaked the code so it returns the correct position when the string you're searching for comes after a substring match, such as looking for "Country" in "Employees.CountryOfBirth,Employees.Country". Thanks, Mike.
*======================================================================
* Function: ATCWord
* Purpose: Performs an ATC() on whole words, not substrings
* Author: Trevor Mansuy, with a tweak by Mike Potjer
* Last revision: 12/09/2008
* Parameters: tcSearch - the string to search for
* tcString - the string to search for a match in
* tnOccurrence - which occurrence to search for
* Returns: the index of the first character of the match
* Environment in: VBScript.RegExp can be instantiated
* Environment out: none
*======================================================================
lparameters tcSearch, ;
tcString, ;
tnOccurrence
local lnOccurrence, ;
loRegExp, ;
lcLeftBoundary, ;
lcRightBoundary, ;
lcSearch, ;
loMatches, ;
lnReturn, ;
loMatch, ;
lnMatch
* Ensure the necessary parameters were passed.
assert vartype(tcSearch) = 'C' and not empty(tcSearch) ;
message 'ATCWord: invalid search string passed'
assert vartype(tcString) = 'C' ;
message 'ATCWord: invalid string passed'
* Bug out if the string is empty.
if empty(tcString)
return 0
endif empty(tcString)
* Set an occurrence if one wasn't passed.
if not vartype(tnOccurrence) = 'N'
lnOccurrence = 1
else
lnOccurrence = tnOccurrence
endif not vartype(tnOccurrence) = 'N'
* Create a VBScript.RegExp object.
loRegExp = createobject('VBScript.RegExp')
loRegExp.IgnoreCase = .T.
loRegExp.Global = .T.
* This is here for future proofing. Right now, the function expects
* whatever we search for to begin and end with non-word characters,
* but just in case, save the boundary characters in variables so we
* can change them easily. Right now, we are using word-boundaries
* (the space between a word and a non-word character. I originally
* wanted to use non-word characters (\W) for this, but the match
* consumes the character, so side-by-side matches won't be found. The
* drawback of \b is if we do an ATC() for a string with a non-word
* character at the beginning or end, there is no guarantee that there
* is a word boundary on the other side of that character.
lcLeftBoundary = '\b'
lcRightBoundary = '\b'
* Certain RegExp special characters prevent matches, so escape them.
lcSearch = AddRegExEscape(tcSearch)
* In the pattern below, the three sets of parentheses represent three
* match groups. The first match group, (\W|^), means we first match
* either a non-word character or the beginning of a string. Similarly,
* (\W|$) means at the end we match either a non-word character or the
* end of the string. This means we make a match in every situation
* except where the string in lcSearch is a substring of one of the
* strings in tcString.
loRegExp.Pattern = '(' + lcLeftBoundary + '|^)(' + lcSearch + ')(' + ;
lcRightBoundary + '|$)'
* Test the string. If a match collection is returned, check how many
* matches were made. If the number of matches is less than the
* occurrence passed, return 0, otherwise, do the ATC.
loMatches = loRegExp.Execute(tcString)
if loMatches.Count < lnOccurrence
lnReturn = 0
* Retrieve the specified occurrence from the Item collection. If the
* match is the one we want, return its start location in the string.
else
loMatch = loMatches.Item(lnOccurrence - 1)
lcMatch = loMatch.Value
lnReturn = iif(upper(tcSearch) = upper(lcMatch), ;
loMatch.FirstIndex + 1, 0)
endif loMatches.Count < lnOccurrence
return lnReturn
*======================================================================
* Function: AddRegExEscape
* Purpose: Escape certain characters in regular expressions
* Author: Trevor Mansuy
* Last revision: 09/03/2008
* Parameters: tcString - the string to escape
* Returns: the string with certain characters escaped
* Environment in: none
* Environment out: none
*======================================================================
function AddRegExEscape
lparameters tcString
local lcString
lcString = strtran(tcString, '\', '\\')
lcString = strtran(lcString, '?', '\?')
lcString = strtran(lcString, '*', '\*')
lcString = strtran(lcString, '+', '\+')
lcString = strtran(lcString, '.', '\.')
lcString = strtran(lcString, '|', '\|')
lcString = strtran(lcString, '{', '\{')
lcString = strtran(lcString, '}', '\}')
lcString = strtran(lcString, '[', '\[')
lcString = strtran(lcString, ']', '\]')
lcString = strtran(lcString, '(', '\(')
lcString = strtran(lcString, ')', '\)')
lcString = strtran(lcString, '$', '\$')
return lcString
