Miscellaneous TD tips
Miscellaneous Team Developer
How to get the application runtime folder
To determine from where the application was started, use the following function :
Function: PALGetRuntimeFolder Returns String: Local variables String: sDir String: sDummy String: sDrive String: sFolder Actions ! The next Vis function is from vtdos.apl Call VisDosSplitPath( strArgArray, sDrive, sDir, sDummy, sDummy ) Set sFolder = sDrive || sDir ! This function will allways return the folder with an ending backslash If SalStrRightX( sFolder, 1 ) != "\\" Set sFolder = sFolder || "\\" Return sFolder
The function SalFileGetCurrentDirectory is not the same !
That function returns the current folder which may change during runtime (eg when changing it programatically or a SalDlgFile function is used).
The PALGetRuntimeFolder function described here will always return the folder where the executable was initially started from.
Here you can download a sample:
How to check at runtime, if a given string is a valid window template
To check if a string representing a window template is valid, you can use SalCompileAndEvaluate.
Pass the template name to this function and check the return value (type).
For instance, if you want to check if "frmMain" is a valid template (when you pass that template to SalCreateWindowEx).
Set sTemplate = "frmMain" Set nType = SalCompileAndEvaluate( sTemplate, nError, nErrorPos, nReturn, sReturn, dtReturn, hWndReturn, TRUE, SalContextCurrent( ) )
If nType > 0 the template is valid. If it is 0 (zero) it is invalid.
Here you can download a sample:
How to flush files using SalProfile functions
It may happen when using SalSetProfileString function the contents of the file is not flushed completely to disc.
To force the files to be flushed use this:
Call SalSetProfileString( STRING_Null, STRING_Null, STRING_Null, sIniFilename )
How to start or stop windows services
An easy way to start or stop windows services (like task scheduler, messenger or event log):
Call SalLoadApp( "NET", "START <ServiceName>" ) Call SalLoadApp( "NET", "STOP <ServiceName>" ) ! ! Example to stop task scheduler : Call SalLoadApp( "NET", "STOP Schedule" )
Here you can download a sample:
Recreate PATH variable
Your application could have some requirements for the PATH variable.
- one or more folders should be deleted from PATH
- one or more folders should be added to the PATH (in front)
One way is to set the PATH using a custom setup (msi) during installation of your application.
Problem is this will be a global setting for the entire system for all applications.
Another way is to create a .bat file in which the PATH is defined and from the .bat file your application
When you need to setup the Team Developer Runtime folder, the setup or .bat method is the only way.
But when you need to set other folders you app needs, there is a better solution.
Change the PATH variable from the application itself.
The changes are local, so other applications do not "see" the changes made by your app.
And when the application ends, the changed PATH variable will be reverted also.
At application startup, you can read in the PATH variable, check it's contents and change it accordingly.
Using a WinAPI function you can write the PATH variable. No need for extra authorizations.
A custom made ready-2-go function does this for you:
Boolean PALRecreatePath( psPathsINIFile )
This function will change the PATH variable using specifications defined in an INI file.
Here a sample INI file:
; RecreatePath.ini ------- ; ; This file contains settings to change the current PATH variable. ; ; The section [PATH_INCLUSIONS] contains folders which will be added to the front of the PATH folder list. ; When the folder is already present in the PATH list, it will be moved to the front. ; ; The section [PATH_EXCLUSIONS] contains folders which will be deleted from the PATH folder list. ; Configure the complete folder name of use partial name to delete multiple folders. ; ; The [DEFAULTS] section contains general settings ; ;---------------------------------------------------------------------------------------------------------- [DEFAULTS] ; The CHANGE_CASE setting can be 0 (no changes), 1 (change to lowercase), 2 (change to uppercase) CHANGE_CASE = 1 ; The DUPLICATES setting can be 0 (no duplicate folders), 1 (no change to as is PATH) DUPLICATES = 0 [PATH_INCLUSIONS] ; Folders to add to the PATH ; Use PATHx as entryname where x is an increasing number PATH1 = C:\Any folder\MyApplication\ PATH2 = C:\Any folder\MySecondApplication\ [PATH_EXCLUSIONS] ; Folders to remove from PATH. Specify partial name to remove multiple folders ; Use PATHx as entryname where x is an increasing number PATH1 = gupta\centura\151 PATH2 = C:\Any folder\TheOtherApplication\ PATH3 = attachmate\infoconnect PATH4 = oracle
The INI file contains the folders to add and to remove from the PATH.
The function also cleans-up the PATH to make it more readable.
It removes duplicates and it can make it single-case (upper or lower).
The function and a sample can be downloaded here:
Automatically close messagebox: timed messages
A messagebox created with SalMessageBox waits for user input to be clicked on any of the defined buttons.
Mostly, the choice made on the messagebox results in further actions in your application.
But what to do if you do not want an user action?
For instance, when a messagebox is opened you would like to close it automatically after a certain time.
Having a timeout for the messagebox, when the timeout occurs, a predefined choice is automatically executed and
will continue the actions in the application based on that choice.
Another implementation could be that opened messageboxes displayed on other applications must be programmatically controlled.
Or when a specific event occurs in your application, you want to automatically dismiss the messagebox.
To create such features in TD applications can be found in this article and is shown in the supplied sample.
To be able to control the messagebox currently open, we first have to find the window handle of the messagebox.
This handle is normally unknown because the SalMessageBox function does not supply us with the handle.
Also, the messagebox is a modal dialog, so the execution context is only given back after the messagebox has been closed.
The trick is to search the messagebox window using the Windows API function FindWindow
hWndMessageBox = FindWindowA( "#32770", "Title of messagebox" )
The first parameter is the classname/ID to search for. Messageboxes have classname/ID "#32770".
The second parameter is the window title. In this case it is the window title of the messagebox as specified in SalMessageBox.
(so not the body text).
So when a messagebox is open, the code above will get us the window handle.
Having the window handle we can access it programmatically. We can at this point just close it by sending a WM_CLOSE message.
But then we can not control which button will be virtually pressed. We would like to simulate a specific choice !
To do this, we need to find the wanted button on the messagebox window by using the WinApi function GetDlgItem.
The buttons have internally a specific ID which is the same ID as used in the return value of SalMessageBox.
So, when we have two buttons, OK and CANCEL, the ID's for those buttons are : IDOK and IDCANCEL.
GetDlgItem is called with as input the messagebox window and the button ID. It returns the button window handle when found.
Then we send a message to the messagebox window to simulate a click on that button.
The messagebox will close and returns the ID of the simulated click on the button.
Set hWndButton = GetDlgItem( hWndMessageBox, IDYES ) If hWndButton Call SalSendMsg( hWndMessageBox, WM_COMMAND, VisNumberMakeLong( IDYES, 0 ), SalWindowHandleToNumber( hWndButton ) )
This is in fact all what is needed.
When you need a timed messagebox, create a timer just before showing the messagebox and after the messagebox kill the timer:
Call SalTimerSet( hWndForm, 1, 3000 ) ! 3 seconds timeout Set nRet = SalMessageBox( "This message will automatically close after 3 seconds !", "Timed message", MB_AbortRetryIgnore ) Call SalTimerKill( hWndForm, 1 )
And on the timer event, execute the closing of the messagebox:
On SAM_Timer If wParam = 1 ! Close the messagebox by simulating click on the ABORT button Call OnTimerCloseMessageBox( ) ! This function searches the msgbox and does the simulated click
A working sample of this implementation, a screen-shot:
Here you can download the sample. It contains both ANSI and UNICODE versions:
Sal compression: detailed info
The functions SalStrCompress and SalStrUncompress are in the TD product for...well for ages.
This article will discuss the use of these functions and also offers two new custom functions.
As the function name implies, SalStrCompress takes a string variable as input and compresses it.
By compressing, the data contained in the string variable is reduced in size, like other compression tools like WinZIP and ARJ.
But the TD function uses an internal TD specific compression algorithm.
The string variable could contain "normal" text but also other data like images, Word documents, audio data etc.
Using SalStrUncompress, passing a string variable which was earlier compressed, the original contents is restored.
The main usage of this out-of-the-box SAL compression feature are:
- Reduce the size of data before it is saved to disk or stored in the database
- Encrypt data
The last one, data encryption, is to prevent users to easily inspect data when some kind of minor security is needed.
But the encryption here is not really safe. When real security is needed, the data should be encrypted using more secure
algorithms like DES or AES. But to prevent users to just open/inspect data and see/use the contents it could be a handy trick
to use the TD compression.
The function SalStrCompress treats the supplied data as-is, so the contents of the string is not taken into account. The bytes in the string buffer are compressed without any analysis.
BEWARE: the function SalStrCompress will not check if the data is already compressed.
So, the function will compress the data again. The compression header of previous compressions are repeated.
When data is compressed twice for example, you need to uncompress it twice to get the original data back.
Only SalStrUncompress is able to restore the compressed data back to the original state.
When this function is called, it will first check if the supplied data is compressed.
This is done by checking the first few bytes of the data, the compression header.
Compressed data by SalStrCompress does contain a fixed header to hold information about the compression.
(more info on the header later on in this article).
When data is not compressed, SalStrUncompress on that data will return FALSE.
It returns TRUE if the data is compressed and was correctly restored to the original state.
The functions SalStrCompress and SalStrUncompress are compatible over all TD versions (ANSI and UNICODE).
So it means that data compressed by TD 1.5.1 for instance and stored in the database can be uncompressed by TD 6.1.
The reverse is also the case, TD 6.2 compressed data can be uncompressed by TD 1.5.1.
No changes are made to the algorithm between the oldest and latest versions of TD.
Addendum: Old 16Bit TD versions used a different way of compression. See the red colored BEWARE paragraph inside the section "Compression header" below.
The TD compression is fair, but it does not compress (reduce) size of data like WinZIP or ARJ.
So if you need maximum possible compression results, the TD compression could be not your choice.
But TD compression/uncompression is fast.
As for every compression algorithm, the source of data being compressed does matter in how much reduction in size is achieved.
For instance, data with many repeating bytes can be efficiently reduced in size. But data which already has some kind
of internal compression could be compressed much harder. For instance image files having internal compression can mostly not be compressed any further, like jpg or png files.
It is also possible that the TD algorithm actually produces a compressed file which is larger in size compared to the original.
To prevent this, the TD compression checks if the resulting data is larger than the original and if so, it will not compress the data.
Instead, it adds the compression header to the data and appends the original data without any changes.
So, there is no need to check if resulting compressed files will be larger in size. SalStrCompress takes care of that.
ANSI vs UNICODE vs Binary
Starting from TD 5.1, text strings are encoded as UNICODE internally (2 bytes per character). Older versions all use ANSI (1 byte per character).
TD compression does not take the encoding of text strings into account.
As said earlier, data is compressed as-is. So when passing an ANSI text string to SalStrCompress, the ANSI data is compressed.
When passing UNICODE text strings, the UNICODE data is compressed.
Remember, what results is ALWAYS binary data !
And what comes in comes out. So when a ANSI string is compressed it will be uncompressed as ANSI.
When UNICODE string is compressed it will be uncompressed as UNICODE.
When binary data (eg an image) is compressed it will be uncompressed as binary (the image).
Imagine this workflow:
An older TD application created in TD 4.2 compresses ANSI text strings and saves it to the database.
This application also is able to read the compressed data from the database and uncompresses it to be used as ANSI text.
But another TD application created in TD 6.1, which is UNICODE, should also be reading the compressed data from the same database and use the text.
This is the flow for saving the data: ANSI text -> (compress) -> binary data -> database
This is the flow for loading the data: database -> binary data -> (uncompress) -> ANSI text
The save/load flow in the older TD version is straightforward: it compresses and saves and it loads and uncompresses.
But the TD 6.1 application is UNICODE internally, so what happens in the flow then:
database -> binary data -> (uncompress) -> ANSI text
The TD 6.1 application will have as result an ANSI text which can not be displayed/used as ANSI.
This means that after the compression (result is ANSI text) should be converted to UNICODE before using the data.
database -> binary data -> (uncompress) -> ANSI text -> (encode as UNICODE) -> UNICODE text
When the TD 6.1 application should save compressed ANSI text to the database, this must be the flow:
UNICODE text -> (encode as ANSI) -> ANSI text -> (compress) -> binary data -> database
So beware: never encode ANSI/UNICODE on the compressed data !!!
Rule is: what comes in SalStrCompress comes out in SalStrUncompress.
When data is compressed using SalStrCompress, it contains a fixed header with compression information.
When evaluating a string with compressed contents, you will have noticed that the string contains this:
The first two characters (bytes) are > and <. Mostly this is a sign for you that the data is compressed.
But the header contains more info, which is internally used by SalStrCompress and SalStrUncompress.
This info is present:
- leading > and < character
- 'data is original data' indicator. Compression-type byte (see values below)
- Value 0 -> no compression
- Value 1 -> compressed with SWCOMP.exe (implode)
- Value 2 -> compressed with new 32Bit internal compression algorithm
- original size (in bytes)
- compressed size (in bytes)
So both the sizes of the original and compressed data is stored in the header.
When the compressed data should exceed the original size, the original data is stored in the buffer.
This is indicated by the 'data is original data' indicator, with value 0.
In this case, the original and compressed sizes are equal.
BEWARE: Very old 16 Bit TD applications used an external application to compress/uncompress data.
Those applications set the compression-type in the header to value 1.
The application used in this case is SWCOMP.exe, which is deployed with TD runtime.
When this application is not present on the system, the uncompression of data will fail.
Also be aware that every TD application, starting from TD 1.0 is unable to uncompress on 64Bit Windows.
This is a bug in TD which hopefully will be solved in new TD versions.
Having this info in the header, it can be used in custom functions on TD compressed data.
The next part will discuss two new functions, which read the compression header and two functions as wrappers for the Sal functions.
- bool = PALStrIsCompressed( psData )
- bool = PALStrGetCompressionInfo( psData, rpbHasOriginalData, rpnOriginalSize, rpnCompressedSize )
- bool = PALStrCompressOnce( rpsData )
- bool = PALStrUncompressAll( rpsData )
This function checks if the supplied (data) string is compressed using SalStrCompress.
So with this you can check before any other action if data is stored as compressed.
This function reads the information from the compression header.
With this you can get the original and compressed size and also if the data is original or not.
This function is a wrapper for SalStrCompress.
It will compress once. So if data is already compressed, it will not compress again.
This function is a wrapper for SalStrUncompress.
It will uncompress all compression data. So if data was compressed multiple times, this function will keep uncompressing until the original data is fully uncompressed
Download the source for these new functions and a sample from here:
SalPause vs Sleep
This is from the helpfile about SalPause:
bOk = SalPause (nMilliseconds ) Causes a non-blocking pause in the application for the specified number of milliseconds. Use this with caution, especially in COM environments, since it may cause issues with re-entrant code.
The mentioned side effect can easily be missed but can actually result in very strange issues in your application.
So this article will explain what is meant by "non-blocking" and what to expect when using SalPause.
You would expect that when calling SalPause, the application is put on hold for the given time.
So that execution of code will be halted and then will continue.
Well, that is the case indeed. You will find that when placing this function within actions, the SalPause will wait and after the given time will continue on the next line of code.
But, and this has to be stressed here, this does not mean that window messages are not processed while SalPause is running.
So let's give an example:
While the application is halted by SalPause, messages will continue to be processed by your application. These can be messages from Windows (WM) but also all SAM and user defined messages.
This means that a SAM_Click (on a button for example) will be processed while SalPause being executed. Or the user can close the window by clicking the close X in the window caption.
And if you are unaware of this "feature", this could lead to unexpected results:
For instance, a button on a screen starts creating some files on disk by opening files reading them and saving calculated results in other files.
Another button on the same screen will close the window.
The process of reading and writing to files depends on being able to exclusively opening the files and a way to do this is to repeat trying to open files for a max retry amount of time.
So you could decide to have a while loop and trying to open a file.
When the file could not be opened (another user has it open for instance) you call SalPause( 1000 ) to wait 1 second and then try again.
This seems plausible and this actually works ok.
But when using SalPause the non-blocking feature will break your application: While your code is trying to open files and SalPause is being executed, a SAM_Click on the Close button can be executed. This means that the user is ABLE to click on the close button and the code behind it will be executed.
In the example what happens? While the reading/writing of files actions have not finished, the user closes the window and probably this will lead into crashes or issues with the initial intended behaviour.
So is SalPause safe to use?
Generally, I would say not. When you want to halt your application for a certain time and you want to be sure the user can not start any other action, SalPause is not a safe implementation.
SalPause can only be used in these situations:
- Your application must be able to keep processing messages and the user is shielded from using other features
- To fix timing issues
The last situation means that in some cases your application has some timing issues with destruction of GUI objects or (re)painting issues.
When Windows does not get enough time to redraw the GUI, a very small SalPause (lets say 1 to 20 ms) could give some breathing space to redraw the GUI before starting another action.
For all other cases, do not use SalPause !
Is there an alternative? Yes, use the WinApi function Sleep.
This is a blocking pause which halts your application and will not process messages.
While Sleep is executing, no messages will be processed and the user is shielded to use any GUI objects (like clicking buttons or closing windows).
After Sleep has ended it will continue your code execution and will post process any messages which are queued.
To show the difference between SalPause and Sleep, use the supplied sample.
When SalPause is executing, press buttons, close window etc to see that it keeps processing those user actions.
The Sleep implementation actually blocks those actions.
Sample can be downloaded here:
Use fonts as application resource: icon font sample
This article will explain how to use custom fonts in your application without installing/registering them on the workstation.
As an example, a free icon font is used to display characters as icons.
Windows ships by default a lot of fonts which can be used in applications.
They are installed/registered in the Windows Fonts folder. Any application can access these fonts.
To make a more unique application GUI you might want to use a custom font, specifically tailored for your application.
Or use any of the zillion freely available fonts which can be downloaded from font websites.
To use these non-default fonts, you have to install them in the Windows Fonts folder.
But this has some drawbacks:
- Installing/registering fonts on a system is mostly only allowed with admin rights.
- Other applications (like Word for example) can access these fonts which you might not want
- Only one version of a font can be installed/registered. A new version of a font will overwrite already installed ones on the system
To overcome these drawbacks you are able to use custom fonts by using them as a file resource from your application.
You do not need to register them, other applications will not be able to use the font and multiple application versions can use different font versions on the same system.
The Windows API offers these functions:
Library name: gdi32.dll UNICODE >>> Function: AddFontResourceExW Returns Number: INT Parameters String: LPCWSTR Number: DWORD Number: LPVOID Function: RemoveFontResourceExW Returns Number: INT Parameters String: LPCWSTR Number: DWORD Number: LPVOID ANSI >>> Function: AddFontResourceExA Returns Number: INT Parameters String: LPCSTR Number: DWORD Number: LPVOID Function: RemoveFontResourceExA Returns Number: INT Parameters String: LPCSTR Number: DWORD Number: LPVOID
With AddFontResourceEx you can load a font file from disc and add them as resource for your application at runtime.
After this call, the font can be used in your GUI. When you mark the resources as private only, other applications will not be able to use it.
(see MSDN documentation for more details).
After using the font (when the application closes), remove the resource by using RemoveFontResourceEx.
This way you can deploy any font file along with your application and load it at runtime without ever registering it.
Here a small example how to use it:
! Load a ttf font file and mark it as private If AddFontResourceExW( "MyCustomFont.ttf", FR_PRIVATE, 0 ) > 0 ! Now use the font name to set a datafield font (16 points) Call SalFontSet( dfFieldDemo, "MyCustomFont", 16, FONT_EnhNormal ) ! Set a complete form (and it's childs) to use the font Call SalFontSet( hWndForm, "MyCustomFont", 10, FONT_EnhNormal )
But remember: the font resource is loaded for the current process. So when you run your TD application from executable, the font will be
loaded and used by that executable and will be "unloaded" when the application ends.
Knowing this, what about using the TD IDE Gui layout designer to create your GUI's?
If you do not install the font on the development system, TD IDE does not have access to the custom font and you are not able to design the GUI having the
needed font. This could be an issue if you must see the GUI having the font when designing/developing it.
There are two solutions for this:
- On the development system, install the font. Normally a development system does have extra rights to install fonts. Windows will first search the font in the Fonts folder
when AddFontResourceEx is called and when not found it will use the font file you specify in the API function.
- Trick: run the application from IDE once and let your application not call RemoveFontResourceEx when ending the application. In that case, TD will
have the font loaded and is available in the TD IDE. You can even create a simple application which only loads the font.
An alternative to resources like .ico, .bmp and .png files for your GUI is to use an icon font.
An icon font contains images or little graphs which are encoded as single string characters.
So, instead of setting an icon or bmp file as image for a button, you can set the same image as a string character.
These are the advantages of using an icon font:
- depending on the type of images, an icon font could be much smaller in size (on disc/memory) compared to bitmap or icon resources
- Icon fonts are scalable. They are vector based, so resizing to any size without loosing quality
- Performance might be much better when using a string character instead of an image
- You can display icons on objects which originally to not support images. As long the object can display text it can display the icon font
But also disadvantages:
- Icon fonts are monochrome (single-color). Color can only be changed at font text level. So setting an icon from a font to a button, to change the color you have to
change the text color of the button.
- The icon font must be installed on the system or be added as resource as described in the beginning of this article
- Icon fonts do not support complex images, like photo's
So, if you have a bunch of icons to show in your gui and they are simple mono-color images, consider using an icon font.
There are many free icon fonts to get from Internet. Just google for icon font and there will be many hits.
An example of free icon font:
Have a look on this page to see which icons are present for this font:
Install the font or load it as resource in your application to use it.
Use the UNICODE numbers to get the needed icon and place it in the GUI where needed.
An example (TD 5.1 and up) how to implement using fonts as resource and also how to use an icon font can be downloaded from here:
How to check if files have equal contents
You might come into the situation that you want to be sure that files have the same contents.
For instance, you have copied a file on the network and afterwards want to check if the file has been changed.
Or you have saved the contents of a file to the database and after fetching the data back and saving to disk want to check if the originally saved data is equal to the file which was just created.
Also during file copy/transfer, the data may become corrupt. How then to make sure the data is 100% correct?
In any case, checking the size and date/timestamp of the files is not enough. These attributes can easily be the same, but could have
different file contents.
The way to compare files is to run CRC (checksum) on these files and compare the CRC values.
There are many ways to run a checksum (CRC) on files.
If you are not interested in the CRC value itself, but want to do a simple check (there are other more sophisticated CRC's), the Windows API offers a handy function for that:
Library name: imagehlp.dll ANSI >>>> Function: MapFileAndCheckSumA Returns Number: DWORD Parameters String: LPCSTR Receive Number: LPDWORD Receive Number: LPDWORD UNICODE >>>> Function: MapFileAndCheckSumW Returns Number: DWORD Parameters String: LPCWSTR Receive Number: LPDWORD Receive Number: LPDWORD
With MapFileAndCheckSum you can pass a filename (incl path) which then will return a CRC number of that file.
You can pass any file type (text, images, documents etc) to calculate the CRC number.
Having this, calculate the CRC numbers for the files you are interested in and compare the values.
When the files have equal contents, the CRC numbers are equal. Even when the file datetime-stamp or any other attribute differs.
If you save files to the database and want to restore them on disk and be sure the files have original contents, you could do this:
Get the CRC number of the file before saving to the database and save that number along with the file data in the database.
Later, fetch the file data and CRC number, save the file to disk and compare the CRC of the file on disk with the one from the database.
The sample application contains two ready-to-go global functions to be used to get CRC numbers and check files:
- Number PALFileChecksumCompare( psFile1, psFile2 )
- Boolean PALFileCheckSum( psFile, pnChecksumToCheck, rpnChecksumFile )
Number PALFileChecksumCompare( psFile1, psFile2 ) Compares the checksum numbers of two files. When the files are equal in content, this function returns the checksum number (>0). Parameter Type Description psFile1 String File 1 psFile2 String File 2 Return value: Number = checksum number > 0 when files are equal. Returns 0 (zero) when error or when files are not equal Boolean PALFileCheckSum( psFile, pnChecksumToCheck, rpnChecksumFile ) Evaluate the file checksum. This function checks if the given checksum number equals the checksum of the file. When no checksum number was given (param is zero), this function will return the file checksum. Parameter Type Description psFile String File (incl path) pnChecksumToCheck Number >0 then do checksum check. If zero (0) then this function will return the file checksum rpnChecksumFile Receive Number OUTPUT: Checksum number of the file. When -1, the file was not found Return value: Boolean = TRUE when the checksum equals the file checksum. Also returns TRUE when no checksum was given and the file checksum could be returned. FALSE when error (eg file does not exist)
Here you can download the sample having these two functions:
Google Analytics & TD applications
This article will explain how to monitor the usage of your TD build application with Google Analytics.
If you are not familiar with Google Analytics :
It is a freemium analytics service provided by Google that tracks and reports website traffic.
Mostly used on websites to see how many users are using the site, which pages are visited, how long they stay there.
Along with this, information about the user is gathered, like geographic location, which OS they use, how often they return, etc etc.
There are a lot of metrics which are logged by default.
The Google Analytics web site gives extensive real-time overviews and reporting and is highly customizable. The service is free when the traffic is not that extensive. Even for commercial use, the amount of traffic which is freely accepted is fairly high.
But Google Analytics can be used for other platforms also, which is not that well known.
Besides the Android and IOS support, it can monitor anything you like. Using the open API you can register events on Google Analytics from everywhere.
So also Team Developer applications.
To give some usages why Google Analytics can he valuable for your TD build applications:
- See the geographic locations the application is used
- How many users are using the application and when
- Track the usage of your application. Which screens are opened, which buttons pressed etc
- Discover the workflow of the application. Which parts are used often, which are not
- How long do users stay on a particular screen or workflow
- See which OS the application runs on, which screen resolutions or any other system metric like CPU, memory etc
- Report errors. Discover how often specific errors/warnings occur when the application is used
- Marketing purposes. Which products are sold using the application based on which campaigns
These are just a few suggestions you can implement in the application.
There is a lot of information on Google Analytics out there. Just google it and see tons of usages and how-to's on YouTube.
So, how to do this?
Well, it is fairly easy. The implementation in TD to execute an event on Google Analytics is very simple.
What do you need?
First you need to setup Google Analytics.
- Create a free Google account (or use an existing one)
- Add Google Analytics to your account (by adding it to the profile, just like GMail, YouTube etc)
- Get a free tracking ID for Google Analytics:
When you first use the Google Analytics web application it will ask you to create a new app/site tracking account.
Create a mobile application site and name it properly. For instance, you could use the TD application name.
When this is setup, Press "Get Tracking ID".
This Tracking ID is needed in the API when calling the Google Analytics API from your application.
You can create multiple tracking ID's by creating multiple apps/sites. One for each application you develop for instance.
An example of a tracking ID is: UA-96046876-1
This is all to setup Google Analytics. When all steps are performed, you have a dashboard waiting for the events to arrive.
How to call the Google Analytics API from your TD application: You do not need any local components or installation of Google API. It is purely sending HTTP messages to the Google service.
Starting from Team Developer 4.2, you have a HTTP class in xmllib.apl which offers all you need.
Earlier TD versions can use the Microsoft HTTP component by generating the ActiveX library using the ActiveX Wizard.
The Analytics API is straightforward:
- The URL to send the message to is: http://www.google-analytics.com/collect
- Pass the metric info using parameters, like ?v=1&t=event&tid=UA-96046876-1&cid=3405e781-9f55-4f52-af1b-9f714d8790e8
So, to call the API you should create a string having the URL and parameters and HTTP POST it:
Set sPrefixURL = "http://www.google-analytics.com/collect?" Set sURL = sPrefixURL || psParameters ! Call uHTTPRequest.open( HTTP_POST, sURL, "", "" ) Call uHTTPRequest.sendText( sURL )
That is all. It is fire-and-forget. You do not get a response message.
When the above code is executed, just wait to see this event appear in Google Analytics real-time dashboard.
There are a lot of parameters to be used in this API. Some are required and some optional.
Read the Google Analytics API documentation how to use the API and which parameters to use:
A sample has been created to show the implementation.
The sample runs on TD 4.2 and up. It is using xmllib.apl which was introduced on TD 4.2.
(When you need it running on older TD versions, create the MS HTTP ActiveX library and use it in the sample)
How to use the sample:
For your convenience, a working Google account having Google Analytics is created to be used.
It will work out-of-the-box. It has already a valid tracking ID populated in the GUI.
Also the API parameters are setup so it will work immediately.
First, open the Google Analytics dashboard.
You can login using the predefined username and password. The info can be found in the sample description.
Press the button "Open Google Analytics" to start a web browser and after login will show the real-time dashboard. Or use this link:
Using the API parameters section in the sample you can change the parameters.
Using the "New user" button you can generate a new GUID to represent a new user.
You can change the location to see in Google Analytics that the event originated at the location you specify.
The full parameter string is automatically displayed in the multi-line field.
This is the actual API parameter string which will be used in the API call.
Press the "HIT ME" button to call the Google API.
Have a look in Google Analytics website in the real-time section and see the event popup after some time.
Change the parameters and redo hitting the API call. Play around in Google Analytics to see which info is reported.
Example screenshots of Google Analytics:
Here you can download the sample :
Parse INI files
Team Developer has build-in functions to read and write INI files.
Generally, an INI file has this structure:
[Section1] Key1 = Value Key2 = Value [Section2] Key1 = Value Key2 = Value
To read or write specific values, you need to know the section-name (name between brackets ) and key-name to access the value using these SAL functions:
In the case you do not know the keynames under a section but want to enumerate them, use this function:
- VisProfileEnumStrings (from vtmisc.apl)
This will give all key-names as array for a section. The array only contains the key-names. The values need to be accessed using the Sal functions described above.
In the case you want to enumerate all section-names, we can use a feature which is not that well known.
The function SalGetProfileString supports the enumeration of sections in the case when you pass an empty sectionname to the function:
Set nBytes = SalGetProfileString( STRING_Null, STRING_Null, "DefaultValue", sSections, "MySettings.ini" )
The function will enumerate all section-names and return it in the value parameter.
The section-names are separated with a NULL character. So you need to implement some extra code to get the section-names as an array of names.
The provided sample has this implemented. This global function does the work
Set nItems = PALGetProfileSections( sINIFile, saSections[*] )
It will return an array of section-names.
Now, using these Sal, Vis and the PALGetProfileSections function you are able to parse an INI file without knowing the contents of the file.
The sample will parse an INI file and display it. There are specific ANSI and UNICODE versions.
Here you can download the sample :