Categories

Senin, 2008 Juni 30

RSS Ticker with AJAX

Download the demo

Description: RSS is a popular format for syndicating and displaying external content on your site, such as the latest headlines from CNN. Well, with this powerful RSS ticker script, you can now easily display any RSS content on your site in a ticker fashion! This script uses a simple PHP based RSS parser called LastRSS for retrieving a RSS feed, then Ajax and DHTML to display the feed dynamically and with flare. As a pre-requisite then, your site itself must support PHP, though the page using this ticker can be any regular HTML file.

Requirement of this script: Ability to run PHP on your site. Note that page(s) displaying the ticker and the backend PHP script must be on the same domain due to Ajax limitations.

Here are some features of Advanced RSS Ticker (Ajax invocation):

  • Displays any RSS feed on the web in a ticker fashion. Specify exactly what components of the feed items to display, such as the title of the item, description, or post date.
  • Each RSS feed is cached on the server for best performance. LastRSS feature.
  • Total customization of each RSS ticker through the frontend JavaScript- easily specify the RSS cache period, CSS class name to style the ticker, delay between message change, and what parts of the feed to show (ie: title+description).
  • Enable or disable a fading effect between message change just by adding or removing two lines of CSS code!
  • Ticker pauses onMouseover.
  • Display multiple RSS tickers on a page, each with their own independent settings.

A demo trumps any explanation, so here it is:

Demos:

Download the demo

Each ticker is called independently on the page, using the core function:

Detailed info below.


Directions: The easiest way to install Advanced Ajax ticker is to download the zip file below:

-rsstickerajax.zip

which contains all the files that make up the script. They are:

  • demo.htm: working demo on Advanced RSS ticker
  • rssticker.js: RSS ticker JavaScript library
  • lastrss/bridge.php: PHP script to output RSS feed in XML format, and communicate with our JavaScript via Ajax
  • lastrss/lastRSS.php: lastRSS.php class, unmodified.

1) For demo.htm:

Open up "demo.htm", and copy the code found inside into the page(s) you wish the ticker to be displayed in. Make sure the code:

inside the HEAD section correctly references the location of "rssticker.js" on your server, if it's been moved to a different directory. The code inside the BODY section shows how to invoke an RSS ticker instance:

2) For rssticker.js:

Open up "rssticker.js", and at the top, simply make sure the path to "bridge.php" on your server is correct:

//Relative URL:

var lastrssbridgeurl="lastrss/bridge.php"
//Absolute URL (uncomment below)
//var lastrssbridgeurl="http://"+window.location.hostname+"/lastrss/bridge.php"

If you wish to use an absolute reference to "bridge.php", simply uncomment the last line and configure that instead. The root domain is dynamically constructed due to Ajax being finicky about the syntax. See Load Absolute URL explanation.

3) For bridge.php:

Bridge.php is a custom PHP script that communicates between our ticker script and lastRSS.php using Ajax. Open up this file using any text editor, and edit the variables as instructed by the comments. It is recommended you read up on the documentation for lastRSS to get a full understanding of what each variable means and how you can take advantage of all of the available features.

4) For lastRSS.php: No editing required. Upload as is.

And there you have it!

More information on Advanced RSS Ticker (Ajax invocation)

Once you've successfully installed the script, most day to day changes to the ticker is done easily and on the front end, via the main RSS ticker function:

A few notes on the parameters above:

  • "RSS_id" must be the name of the RSS feed to display as it appears in "bridge.php."
  • "cachetime" is in seconds. A good time is at least 30 minutes, or 1800. *
  • "divId" is the desired ID for this ticker instance. Can be arbitrary but must be unique for each ticker instance on the page.
  • "bbcclass" is the CSS class name to be passed in to style this ticker instance.**
  • "optionalswitch" is a string used internally by the ticker script to decide what parts of an item to display. By default, it will just display the title+link of each item. You can pass in "date", or "date+description" to display additional information.

* Regarding the cache time, a tip is to set it to 0 while you're testing out the ticker, to make sure any changes are instantly visible, then change it back to the desired number afterwards. This is extremely helpful, for example, if you've made changes to "bridge.php" to change the format of the date output- that change will only show up instantly if you've set cache to 0.

** If you want a fading effect to occur between message change for the ticker, just add the code:

filter:progid:DXImageTransform.Microsoft.alpha(opacity€);

-moz-opacity: 0.8;

inside your CSS class for that ticker instance. It's that easy! The fade effect uses up about 1/2 second, so you'll want to increase the "delay" parameter accordingly if enabled.

Alternate Ajax Techniques

By now, nearly everyone who works in web development has heard of the term Ajax, which is simply a term to describe client-server communication achieved without reloading the current page. Most articles on Ajax have focused on using XMLHttp as the means to achieving such communication, but Ajax techniques aren't limited to just XMLHttp. There are several other methods; we'll explore some of the more common ones in this series of articles.

Dynamic Script Loading

The first alternate Ajax technique is dynamic script loading. The concept is simple: create a new




The JavaScript file example1.js contains a single line:

callback("Hello world!");

When the button is clicked, the makeRequest() function is called, initiating the dynamic script loading. Since the newly loaded script is in context of the page, it can access and call the callback() function, which can do use the returned value as it pleases. This example works in any DOM-compliant browsers (Internet Explorer 5.0+, Safari, Firefox, and Opera 7.0+), try it for yourself or download the examples.

More Complex Communication

Sometimes you'll want to load a static JavaScript file from the server, as in the previous example, but sometimes you'll want to return data based on some sort of information. This introduces a level of complexity to dynamic script loading beyond the previous example.

First, you need a way to pass data to the server. This can be accomplished by attaching query string arguments to the JavaScript file URL. Of course, JavaScript files can't access query string information about themselves, so you'll need to use some sort of server-side logic to handle the request and output the correct JavaScript. Here's a function to help with the process:

function makeRequest(sUrl, oParams) {
for (sName in oParams) {
if (sUrl.indexOf("?") > -1) {
sUrl += "&";
} else {
sUrl += "?";
}
sUrl += encodeURIComponent(sName) + "=" + encodeURIComponent(oParams[sName]);
}

var oScript = document.createElement("script");
oScript.src = sUrl;
document.body.appendChild(oScript);
}

This function expects to be passed a URL for a JavaScript file and an object containing query string arguments. The query string is constructed inside of the function by iterating over the properties of this object. Then, the familiar dynamic script loading technique is used. This function can be called as follows:

var oParams = {
"param1": "value1",
"param2": "value2"
};
makeRequest("/path/to/myjs.php", oParams)

Pengantar AJAX

Sebelum membahas lebih jauh tentang AJAX. Apa sih sebenarnya AJAX? AJAX bukanlah barang baru dan bukan teknologi baru. AJAX merupakan paduan dari beberapa teknologi yang sudah dikenal sebalumnya yaitu HTML, DOM, XML, Javascript dan teknologi pendukung lainnya. AJAX adalah akronim dari Asynchronous JavaScript and XML, komponen-komponen AJAX meliputi:

  • HTML (HyperText Markup Language) digunakan dalam membuat halaman web dan dokumen-dokumen lain yang dapat ditampilkan dalam browser. HTML merupakan standar internasional dengan spesifikasi yang ditetapkan oleh World Wide Web Consortium (W3C). Versi terakhir saat tulisan ini dibuat adalah HTML 4.01.
  • XHTML (Extensible HyperText Markup Language), adalah bahasa markup sebagaimana HTML, tetapi dengan gaya bahasa lebih baik. Versi terakhir saat tulisan ini dibuat adalah XHTML 2.0.
  • CSS (Cascading Style Sheets ) adalah sebuah mekanisme sederhana untuk memberikan style (seperti font, warna, jarak spasi, dll) kepada dokumen web yang ditulis dalam HTML atau XML (termasuk beberapa variasi bahasa XML seperti XHTML dan SVG).
  • JavaScript adalah bahasa scripting kecil, ringan, berorientasi-objek dan lintas platform. JavaScript tidak dapat berjalan dengan baik sebagai bahasa mandiri, melainkan dirancang untuk ditanamkan pada produk dan aplikasi lain seperti peramban web.
  • DOM (Document Object Model) adalah sebuah API (Application Program Interface) untuk dokumen HTML dan XML. DOM menyediakan representasi dokumen secara terstruktur, dimungkinkan untuk merubah isi dan presentasi visual. Pada dasarnya, DOM menghubungkan halaman web dengan script atau bahasa pemprograman.
  • XML (Extensible Markup Language) adalah bahasa markup untuk keperluan umum yang disarankan oleh W3C untuk membuat dokumen markup keperluan khusus. Keperluan utama XML adalah untuk pertukaran data antar sistem yang beraneka ragam.
  • XSLT (Extensible Stylesheet Language Transformations ) adalah sebuah bahasa berbasis-XML untuk transformasi dokumen XML. Walaupun proses merujuk pada transformasi, dokumen asli tidak berubah melainkan dokumen XML baru dibuat dengan basis isi dokumen yang sudah ada. XSLT biasanya digunakan untuk merubah skema XML ke halaman web atau dokumen PDF.
  • Objek XMLHttpRequest untuk melakukan pertukaran data secara asinkron dengan peladen (server) web. Beberapa kerangka-kerja Ahax dan dalam beberapa situasi, objek IFrame digunakan selain objek XMLHttpRequest untuk melakukan pertukaran data dengan peladen web.
  • JSON (JavaScript Object Notation) yaitu format pertukaran data komputer yang ringan dan mudah. Keuntungan JSON dibandingkan dengan XML adalah pada proses penterjemahan data menggunakan Javascript. Javascript dapat menterjemahkan JSON menggunakan built-in procedure eval().

Dalam penerapannya, tidak semua teknologi di atas digunakan. Terdapat beberapa teknik komunikasi yang digunakan untuk implementasi AJAX. Teknik yang umum digunakan adalah dengan menggunakan:

  • Hidden Frame
    Metode ini memanfaatkan frame yang tersembunyi. Biasanya salah satu frame diset dengan ukuran tinggi/lebar 0 sehingga tidak terlihat di halaman. Frame tersembunyi inilah yang sebenarnya melakukan request ke dan menerima respon dari server, sehingga frame yang tampil kelihatan tidak melakukan postback ke server. Javascript digunakan untuk mengambil data dan mengisi data yang ada di frame tersembunyi ini.
  • Hidden IFrame
    Metode ini hampir sama dengan hidden frame, perbedaannya hanya pada elemen yang digunakan yaitu IFrame, bukan Frame.
  • Objek XMLHttpRequest
    Metode yang satu ini memanfaatkan ActiveX Objek (IE) atau objek javascript XMLHttpRequest (Mozilla/Firefox, Safari, Opera). Objek ini yang akan melakukan postback ke server dan menerima respon balik berupa data (bukan halaman). Data yang didapat dari server kemudian diolah di klien untuk ditampilkan ke halaman.

Teknik pemprosesan halaman secara umum ada dua:

  • Pemprosesan halam dengan pembuatan/manipulasi objek dokumen menggunakan javascript.
    Klien mengirimkan data dalam format XML/JSON kepada server dan mendapatkan data dari server berupa XML/JSON. Data XML/JSON kemudian diolah untuk memanipulasi objek dokumen menggunakan DOM dan javascript.
  • Parsial rendering.
    Pada teknik ini UI dan behaviour tidak diproses di klien melainkan diproses di server. Klien menerima UI dan behaviour kemudian melakukan rendering pada bagian halaman tertentu.

Perpaduan teknologi-teknologi tersebut dapat meningkatkan kinerja aplikasi web dan lebih responsif terhadap aksi pengguna. Dengan AJAX pertukaran data antara klien dan server lebih ringan karena hanya data yang dipertukarkan (bukan halaman) sehingga aplikasi web dapat berjalan lebih cepat.

Penggunaan Dasar

Dua keistimewaan Ajax adalah dapat:

  • Membuat permintaan kepada server tanpa memuat kembali (reload) halaman.
  • Mengurai (parse) dan bekerja dengan dokumen XML dan atau JSON.

1. Membuat Permintaan HTTP (HTTP Request)

Untuk membuat permintaan HTTP kepada server menggunakan JavaScript, diperlukan sebuah class yang menyediakan fungsi-fungsi ini. Pada Internet Explorer tersedia objek ActiveX yang disebut XMLHTTP. Pada Mozilla, Safari, Opera dan beberapa peramban lain, menerapkan sebuah class Javascript objek XMLHttpRequest yang mendukung method dan properties objek Microsoft ActiveX.

Untuk membuat instance (objek) class lintas-browser (cross-browser), dapat dilakukan dengan:

if (window.XMLHttpRequest) {       // Mozilla, Safari, ...
http_request = new XMLHttpRequest ();
} else if (window.ActiveXObject) { // IE
http_request = new ActiveXObject("Microsoft.XMLHTTP");
}

Catatan: kode di atas hanya sebagai ilustrasi saja. Kode tersebut merupakan versi paling sederhana untuk membuat instance XMLHttp. Untuk penggunaan secara nyata dapat lihat di bagian 3 artikel ini.

Beberapa versi browser Mozilla tidak bekerja dengan baik jika respon dari server tidak mengandung header mime-type XML. Untuk memenuhi kebutuhan ini, panggil method untuk mengganti/menambahkan header yang dikirim oleh server.

http_request = new XMLHttpRequest();
http_request overrideMimeType('text/xml');

Setelah itu, buat fungsi untuk mengolah setelah data diterima dari server berdasarkan permintaan yang dibuat sebelumnya. Tahap ini, daftarkan fungsi JavaScript yang menangani respon dari server. Setting properties onreadystatechange objek dengan nama fungsi Javascript yang disiapkan untuk mengerjakan proses respon.

http_request.onreadystatechange = namaFungsi;

Perlu dicatat bahwa tidak ada tanda kurung setelah nama fungsi dan tanpa parameter yang dilewatkan. Selain memberikan nama fungsi, dapat juga digunakan teknik JavaScript dalam pendefinisian fungsi (tanpa nama) saat program berjalan (run-time) -- yang disebut fungsi anonymous -- dan mendefinisikan tindakan untuk melakukan proses, seperti berikut:

http_request.onreadystatechange = function() {
// do the thing
};

Selanjutnya, setelah deklarasi dan segera setelah menerima respon, kemudian buat permintaan. Panggil
method open() dan send() dari klas permintaan HTTP seperti kode berikut:

http_request.open('GET', 'http://www.example.org/somefile', true);
http_request.send(null);
  • Parameter panggil pertama dari open() adalah method permintaan HTTP GET, POST, HEAD atau method lain yang didukung oleh server. Gunakan huruf kapital sebagaimana standar HTTP. Untuk informasi lebih lanjut mengenai method permintaan HTTP dapat dilihat pada spesifikasi W3C.
  • Parameter kedua adalah URL dari halaman yang diminta. Demi keamanan, panggilan tidak dapat dilakukan pada halaman domain pihak ketiga. Pastikan untuk menggunakan nama domain yang pasti pada semua halaman jika tidak ingin mendapat error 'permision denied' ketika melakukan panggilan open().
  • Parameter ketiga diset ketika permintaan adalah asinkron. Jika diset TRUE, eksekusi fungsi JavaScript akan berlanjut walau tanggapan dari peladen belum sampai. Ini adalah A dalam AJAX.

Parameter untuk method send() dapat berupa sembarang data untuk dikirim ke server saat mengirimkan permintaan POST. Data harus dalam format query string, seperti:

name=value&anothername=othervalue&so=on

Catatan bahwa jika ingin mengirim (POST) data, ganti tipe MIME permintaan menggunakan bari berikut:

http_request.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); 

Bila tidak, server akan mengabaikan data yang dikirim.

2. Penanganan Respon Server

Ingat bahwa ketika permintaan dikirim, fungsi JavaScript harus telah disediakan untuk menangani respon.

http_request.onreadystatechange = namaFungsi;

Apa yang seharusnya dikerjakan oleh fungsi ini. Pertama, fungsi untuk memeriksa status tanggapan. Jika status memiliki nilai 4, berarti bahwa seluruh tanggapan peladen telah diterima dan siap dilanjutkan proses berikutnya.

if (http_request.readyState == 4) {
// everything is good, the response is received
} else {
// still not ready
}

Berikut adalah daftar nilai status readyState

  • 0 (tidak diinisialisasi)
  • 1 (dalam proses memuat)
  • 2 (telah dimuat)
  • 3 (interaktif)
  • 4 (lengkap)

Berikutnya adalah pemeriksaan kode status dari respon HTTP server. Daftar kode status respon HTTP dapat dilihat di situs W3C. Pada contoh kali ini hanya digunakan status 200 yaitu respon OK.

if (httpRequest.status == 200) {
// perfect!
} else {
// there was a problem with the request,
// for example the response may be a 404 (Not Found)
// or 500 (Internal Server Error) response codes
}

Setelah semua status permintaan diperiksa dan kode status HTTP telah ada respon, data respon dari server dapat diolah. Terdapat dua pilihan untuk mengakses data.

  • httpRequest.responseText -- akan mengembalikan respon berupa teks string
  • httpRequest.responseXML -- akan mengembalikan respon berupa objek XMLDocument object yang dapat diakses dengan fungsi DOM JavaScript

3. Contoh Sederhana

Tulis bagian program menjadi satu permintaan HTTP sederhana. JavaScript akan meminta sebuah dokumen HTML yaitu test.html yang hanya mengandung tulisan "I'm a test" dan kemudian memanggil alert() dengan isi dari berkas test.html

<script type="text/javascript" language="javascript">
var http_request = false;
function makeRequest(url) {
http_request = false ;
if (window.XMLHttpRequest) { // Mozilla, Safari ,...
http_request = new XMLHttpRequest();
if (http_request.overrideMimeType) {
http_request.overrideMimeType('text/xml');
// See note below about this line
}
} else if (window.ActiveXObject) { // IE
var aVersions = [ "MSXML2.XMLHttp.6.0", "MSXML2.XMLHttp.5.0", "MSXML2.XMLHttp.4.0", "MSXML2.XMLHttp.3.0", "Microsoft.XMLHTTP" ];
for (var i = 0; i < style="color: rgb(0, 0, 255);">length; i++) {
try { http_request = new ActiveXObject(aVersions[i]);
break;
}
catch (e)
{
// Do nothing
}
}
}
if (!http_request) {
alert ('Giving up :( Cannot create an XMLHTTP instance');
return false;
}
http_request.onreadystatechange = alertContents;
http_request.open('GET', url, true);
http_request.send(null);
}
function alertContents() {
if (http_request.readyState == 4) {
if (http_request.status == 200) {
alert(http_request.responseText);
} else {
alert('There was a problem with the request.');
}
}
}
script>
<span style="cursor: pointer; text-decoration: underline" onclick ="makeRequest('test.html')">Make a request
span >

Pada contoh di atas:

  • Ketika pengguna menekan klik pada link "e;Make a request"e; maka fungsi makeRequent dipanggil dengan parameter nama file HTML test.html dalam direktori yang sama.
  • Permintaan dibuat dan kemudian event onreadystatechange melakukan eksekusi alertContents()
  • alertContents() memeriksa jika tanggapan telah diterima dalam keadaan baik dan kemudian tampilkan isi berkas test.html menggunakan fungsi alert()

Contoh di atas dapat diuji dengan klik di sini dan juga berkas test dapat dilihat di sini. Catatan: baris http_request.overrideMimeType('text/xml'); di atas mengakibatkan error Console pada Firefox 1.5b seperti tercantum dalam dokumen pada https://bugzilla.mozilla.org/show_bug.cgi?id=311724 jika halaman yang dipanggil dengan XMLHttpRequestbukan XML yang valid (seperti plaintext).

4. Bekerja dengan Respon XML

Pada contoh sebelumnya, setelah tanggapan dari permintaan HTTP diterima, digunakan sifat responseText dari objek permintaan yang mengandung isi file test.html. Sekarang, coba gunakan sifat responseXML Buat dokumen XML yang valid dengan nama test.xml seperti contoh di bawah:

xml version ="1.0" ?>
<root>
I'm a test.
root>

Ganti baris permintaan pada script untuk melakukan request dengan:

...
onclick ="makeRequest('test.xml')">
...

Kemudian pada alertContents() ganti pada baris alert(http_request.responseText); diganti dengan:

var xmldoc = http_request.responseXML;
var root_node = xmldoc.getElementsByTagName('root').item(0);
alert(root_node.firstChild.data);

Perintah tersebut untuk mengambil objek XMLDocument yang diberikan oleh responseXML dengan menggunakan method DOM untuk mengakses data dalam dokumen XML.

4. Petukaran data menggunakan JSON

Telah di sebutkan di atas bahwa JSON merupakan salah satu format pertukaran data yang dapat secara langsung diterjemahkan ke dalam objek Javascript. Pada contoh berikut akan dijelaskan bagaimana implementasi JSON pada AJAX dibandingkan dengan XML.

Pertama, buat data JSON dalam sebuah file dengan nama test.txt yang isinya:

{ "FirstName" : "Ahmad", "LastName" : "Masykur" } 

Ganti baris perintah pada script untuk melakukan request dengan:

...
onclick ="makeRequest('test.txt')">
...

Kemudian pada alertContents() ganti pada baris alert(http_request.responseText); diganti dengan:

eval("jsonObj="+http_request.responseText+";");
alert(jsonObj.FirstName + ' ' + jsonObj.LastName);

Dari contoh di atas terlihat bahwa JSON lebih sederhana dan ringan dibandingkan dengan XML. Jumlah data yang terkandung lebih banyak dan total byte yang dikirim lebih kecil. Juga pada penulisan di javascript lebih sederhana karena notasi data dapat langsung diterjemahkan menjadi objek Javascript dengan fungsi eval().

Pada implementasi di projek nyata, data (baik XML maupun JSON) biasanya diambil dari application server atau webservice. Demikian tulisan singkat dan contoh sederhana mengenai AJAX. Semoga dapat membuka wawasan mengenai AJAX untuk dapat memulai mengembangkan aplikasi web yang AJAX-enabled.

Jumat, 2008 Juni 27

Windows Temp Directory

This tip for getting the windows temp directory from within windows was submitted by Daniel Barnes. When you've just got to create some temporary storage files, the window temp directory is the recommended place for shoving them! If you're feeling really good, the best place for this to go would be within the PFE layer probably in n_cst_filesrvwin32.
// Declare this as your Local External Function
FUNCTION ulong GetTempPathA (long nBufferLength, &
ref string lpBuffer ) LIBRARY "KERNEL32.DLL"

// Now you write a function somewhere in your app, probably in some global
// function manager, or perhaps where you store all your .dll calls
// Code for function of_gettempdir(ref_dir) (or what ever you
// choose to name it!)

constant long lcl_maxpath = 260
string ls_tempdir
ulong lul_retval

// Windows requires padded string
ls_tempdir = Fill('0', lcl_maxpath)

// Call local external function.
lul_retval = THIS.GetTempPatha(lcl_maxpath, ls_tempdir)

// Windows returns length of string returned, so if anything
// greater than zero, then windows returned something useful.

IF lul_retval > 0 THEN
ref_dir = ls_tempdir
RETURN SUCCESS
END IF

// Nothing found, or dll call failed.
ref_dir = ''

RETURN FAILURE

Make a Window Stay On Top

Sometimes when developing applications you want your window to stay on top of all other windows.

One way to achieve this is to declare an API call, but PowerBuilder has support for this built in. If at all possible it is best not to go directly to an API call as is makes your application platform specific.

Anyway, there is a method of the window object, SetPosition() which takes an enumerated datatype of TopMost! or NoTopMost!.

If you call the function with TopMost! then your window will always be on top of all other windows in the system. Calling the function with NoTopMost! will return the window back into the normal layering of windows.

Kamis, 2008 Juni 26

Writing Your Own GPS Applications

Introduction

What is it that GPS applications need to be good enough to use in a commercial environment, such as in-car navigation? Also, how does the process of interpreting GPS data actually work? In this two-part series, I will cover both topics and give you the skills you need to write a commercial-grade GPS application that works with a majority of GPS devices in the industry today.

One Powerful Sentence

This first part in the series will explore the task of interpreting raw GPS data. Fortunately, the task is simplified thanks to the National Marine Electronics Association (www.nmea.org) which introduced a standard for the industry, now in use by a vast majority of GPS devices. To give developers a head start, I chose to use some Visual Studio .NET source code from my “GPS.NET Global Positioning SDK” component. (The code is stripped of features like multithreading and error handling for brevity.)

NMEA data is sent as comma-delimited “sentences” which contain information based on the first word of the sentence. There are over fifty kinds of sentences, yet an interpreter really only needs to handle a few to get the job done. The most common NMEA sentence of all is the “Recommended Minimum” sentence, which begins with “$GPRMC”. Here is an example:

$GPRMC,040302.663,A,3939.7,N,10506.6,W,0.27,358.86,200804,,*1A

This one sentence contains nearly everything a GPS application needs: latitude, longitude, speed, bearing, satellite-derived time, fix status and magnetic variation.

The Core of An Interpreter

The first step in making an NMEA interpreter is writing a method which does two things: separating each sentence into its individual words and examining the first word to figure out what information is available to extract. Listing 1-1 shows the start of the interpreter class.

(Listing 1-1: The core of an NMEA interpreter is a function which divides NMEA sentences into individual words.)

Collapse
'*******************************************************

'** Listing 1-1. The core of an NMEA interpreter

'*******************************************************


Public Class NmeaInterpreter

' Processes information from the GPS receiver

Public Function Parse(ByVal sentence As String) As Boolean
' Divide the sentence into words

Dim Words() As String = GetWords(sentence)
' Look at the first word to decide where to go next

Select Case Words(0)
Case "$GPRMC" ' A "Recommended Minimum" sentence was found!

' Indicate that the sentence was recognized

Return True
Case Else
' Indicate that the sentence was not recognized

Return False
End Select
End Function

' Divides a sentence into individual words

Public Function GetWords(ByVal sentence As String) As String()
Return sentence.Split(","c)
End Function

End Class

The next step is to perform actual extraction of information, starting with latitude and longitude. Latitude and longitude are stored in the form “DDD°MM’SS.S”, where D represents hours (also called “degrees”), M represents minutes, and S represents seconds. Coordinates can be displayed in shorthand, such as “DD°MM.M’” or even “DD°”. The fourth word in the sentence, “3939.7”, shows the current latitude as hours and minutes (39°39.7’), except the numbers are squished together. The first two characters (39) represent hours and the remainder of the word (39.7) represents minutes. Longitude is structured the same way, except that the first three characters represent hours (105°06.6’). Words five and seven indicate the “hemisphere”, where “N” means “North”, “W” means “West” etc. The hemisphere is appended to the end of the numeric portion to make a complete measurement. I’ve found that NMEA interpreters are much easier to work with as they are event-driven. This is because data arrives in no particular order. An event-driven class gives the interpreter the most flexibility and responsiveness to an application. So, I’ll design the interpreter to report information using events. The first event, PositionReceived, will be raised whenever the current latitude and longitude are received. Listing 1-2 expands the interpreter to report the current position.

(Listing 1-2: The interpreter can now report the current latitude and longitude.)

Collapse
'*******************************************************

'** Listing 1-2. Extracting information from a sentence

'*******************************************************

Public Class NmeaInterpreter

' Raised when the current location has changed

Public Event PositionReceived(ByVal latitude As String, _
ByVal longitude As String)

' Processes information from the GPS receiver

Public Function Parse(ByVal sentence As String) As Boolean
' Look at the first word to decide where to go next

Select Case GetWords(sentence)(0)
Case "$GPRMC" ' A "Recommended Minimum" sentence was found!

Return ParseGPRMC(sentence)
Case Else
' Indicate that the sentence was not recognized

Return False
End Select
End Function

' Divides a sentence into individual words

Public Function GetWords(ByVal sentence As String) As String()
Return sentence.Split(","c)
End Function

' Interprets a $GPRMC message

Public Function ParseGPRMC(ByVal sentence As String) As Boolean
' Divide the sentence into words

Dim Words() As String = GetWords(sentence)
' Do we have enough values to describe our location?

If Words(3) <> "" And Words(4) <> "" And Words(5) <> "" And _
Words(6) <> "" Then
' Yes. Extract latitude and longitude

Dim Latitude As String = Words(3).Substring(0, 2) & "°" ' Append hours

Latitude = Latitude & Words(3).Substring(2) & """" ' Append minutes

Latitude = Latitude & Words(4) ' Append the hemisphere

Dim Longitude As String = Words(5).Substring(0, 3) & "°" ' Append hours

Longitude = Longitude & Words(5).Substring(3) & """" ' Append minutes

Longitude = Longitude & Words(6) ' Append the hemisphere

' Notify the calling application of the change

RaiseEvent PositionReceived(Latitude, Longitude)
End If
' Indicate that the sentence was recognized

Return True
End Function
End Class

One thing to watch out for here is that some GPS devices will report blank values when no information is known. Therefore, it’s a good idea to test each word for a value before parsing. If you need to type the degree symbol (°), hold down the Alt key and type “0176” on the numeric keypad.

Taking Out the Garbage

A checksum is calculated as the XOR of bytes between (but not including) the dollar sign and asterisk. This checksum is then compared with the checksum from the sentence. If the checksums do not match, the sentence is typically discarded. This is okay to do because the GPS devices tend to repeat the same information every few seconds. With the ability to compare checksums, the interpreter is able to throw out any sentence with an invalid checksum. Listing 1-3 expands the interpreter to do this.

(Listing 1-3: The interpreter can now detect errors and parse only error-free NMEA data.)

Collapse
'*******************************************************

'** Listing 1-3. Detecting and handling NMEA errors

'*******************************************************


Public Class NmeaInterpreter

' Raised when the current location has changed

Public Event PositionReceived(ByVal latitude As String, _
ByVal longitude As String)

' Processes information from the GPS receiver

Public Function Parse(ByVal sentence As String) As Boolean
' Discard the sentence if its checksum does not match our calculated

'checksum

If Not IsValid(sentence) Then Return False
' Look at the first word to decide where to go next

Select Case GetWords(sentence)(0)
Case "$GPRMC" ' A "Recommended Minimum" sentence was found!

Return ParseGPRMC(sentence)
Case Else
' Indicate that the sentence was not recognized

Return False
End Select
End Function

' Divides a sentence into individual words

Public Function GetWords(ByVal sentence As String) As String()
Return sentence.Split(","c)
End Function

' Interprets a $GPRMC message

Public Function ParseGPRMC(ByVal sentence As String) As Boolean
' Divide the sentence into words

Dim Words() As String = GetWords(sentence)
' Do we have enough values to describe our location?

If Words(3) <> "" And Words(4) <> "" And Words(5) <> "" And _
Words(6) <> "" Then
' Yes. Extract latitude and longitude

Dim Latitude As String = Words(3).Substring(0, 2) & "°" ' Append hours

Latitude = Latitude & Words(3).Substring(2) & """" ' Append minutes

Latitude = Latitude & Words(4) ' Append the hemisphere

Dim Longitude As String = Words(5).Substring(0, 3) & "°" ' Append hours

Longitude = Longitude & Words(5).Substring(3) & """" ' Append minutes

Longitude = Longitude & Words(6) ' Append the hemisphere

' Notify the calling application of the change

RaiseEvent PositionReceived(Latitude, Longitude)
End If
' Indicate that the sentence was recognized

Return True
End Function

' Returns True if a sentence's checksum matches the calculated checksum

Public Function IsValid(ByVal sentence As String) As Boolean
' Compare the characters after the asterisk to the calculation

Return sentence.Substring(sentence.IndexOf("*") + 1) = GetChecksum(sentence)
End Function

' Calculates the checksum for a sentence

Public Function GetChecksum(ByVal sentence As String) As String
' Loop through all chars to get a checksum

Dim Character As Char
Dim Checksum As Integer
For Each Character In sentence
Select Case Character
Case "$"c
' Ignore the dollar sign

Case "*"c
' Stop processing before the asterisk

Exit For
Case Else
' Is this the first value for the checksum?

If Checksum = 0 Then
' Yes. Set the checksum to the value

Checksum = Convert.ToByte(Character)
Else
' No. XOR the checksum with this character's value

Checksum = Checksum Xor Convert.ToByte(Character)
End If
End Select
Next
' Return the checksum formatted as a two-character hexadecimal

Return Checksum.ToString("X2")
End Function
End Class

Wireless Atomic Time

Time is the cornerstone of GPS technology because distances are measured at the speed of light. Each GPS satellite contains four atomic clocks which it uses to time its radio transmissions within a few nanoseconds. One fascinating feature is that with just a few lines of code, these atomic clocks can be used to synchronize a computer’s clock with millisecond accuracy. The second word of the $GPRMC sentence, “040302.663”, contains satellite-derived time in a compressed format. The first two characters represent hours, the next two represent minutes, the next two represent seconds, and everything after the decimal place is milliseconds. So, the time is 4:03:02.663 AM. However, satellites report time in universal time (GMT+0), so the time must be adjusted to the local time zone. Listing 1-4 adds support for satellite-derived time and uses the DateTime.ToLocalTime method to convert satellite time to the local time zone.

(Listing 1-4: This class can now use atomic clocks to synchronize your computer’s clock wirelessly.)

Collapse
'********************************************************

'** Listing 1-4. Add support for satellite-derived time

'********************************************************


Public Class NmeaInterpreter

' Raised when the current location has changed

Public Event PositionReceived(ByVal latitude As String, _
ByVal longitude As String)
Public Event DateTimeChanged(ByVal dateTime As DateTime)

' Processes information from the GPS receiver

Public Function Parse(ByVal sentence As String) As Boolean
' Discard the sentence if its checksum does not match our

' calculated checksum

If Not IsValid(sentence) Then Return False
' Look at the first word to decide where to go next

Select Case GetWords(sentence)(0)
Case "$GPRMC" ' A "Recommended Minimum" sentence was found!

Return ParseGPRMC(sentence)
Case Else
' Indicate that the sentence was not recognized

Return False
End Select
End Function

' Divides a sentence into individual words

Public Function GetWords(ByVal sentence As String) As String()
Return sentence.Split(","c)
End Function

' Interprets a $GPRMC message

Public Function ParseGPRMC(ByVal sentence As String) As Boolean
' Divide the sentence into words

Dim Words() As String = GetWords(sentence)
' Do we have enough values to describe our location?

If Words(3) <> "" And Words(4) <> "" And Words(5) <> "" And _
Words(6) <> "" Then
' Yes. Extract latitude and longitude

Dim Latitude As String = Words(3).Substring(0, 2) & "°" ' Append hours

Latitude = Latitude & Words(3).Substring(2) & """" ' Append minutes

Latitude = Latitude & Words(4) ' Append the hemisphere

Dim Longitude As String = Words(5).Substring(0, 3) & "°" ' Append hours

Longitude = Longitude & Words(5).Substring(3) & """" ' Append minutes

Longitude = Longitude & Words(6) ' Append the hemisphere

' Notify the calling application of the change

RaiseEvent PositionReceived(Latitude, Longitude)
End If
' Do we have enough values to parse satellite-derived time?

If Words(1) <> "" Then
' Yes. Extract hours, minutes, seconds and milliseconds

Dim UtcHours As Integer = CType(Words(1).Substring(0, 2), Integer)
Dim UtcMinutes As Integer = CType(Words(1).Substring(2, 2), Integer)
Dim UtcSeconds As Integer = CType(Words(1).Substring(4, 2), Integer)
Dim UtcMilliseconds As Integer
' Extract milliseconds if it is available

If Words(1).Length > 7 Then
UtcMilliseconds = CType(Words(1).Substring(7), Integer)
End If
' Now build a DateTime object with all values

Dim Today As DateTime = System.DateTime.Now.ToUniversalTime
Dim SatelliteTime As New System.DateTime(Today.Year, Today.Month, _
Today.Day, UtcHours, UtcMinutes, UtcSeconds, UtcMilliseconds)
' Notify of the new time, adjusted to the local time zone

RaiseEvent DateTimeChanged(SatelliteTime.ToLocalTime)
End If
' Indicate that the sentence was recognized

Return True
End Function

' Returns True if a sentence's checksum matches the calculated checksum

Public Function IsValid(ByVal sentence As String) As Boolean
' Compare the characters after the asterisk to the calculation

Return sentence.Substring(sentence.IndexOf("*") + 1) = _
GetChecksum(sentence)
End Function

' Calculates the checksum for a sentence

Public Function GetChecksum(ByVal sentence As String) As String
' Loop through all chars to get a checksum

Dim Character As Char
Dim Checksum As Integer
For Each Character In sentence
Select Case Character
Case "$"c
' Ignore the dollar sign

Case "*"c
' Stop processing before the asterisk

Exit For
Case Else
' Is this the first value for the checksum?

If Checksum = 0 Then
' Yes. Set the checksum to the value

Checksum = Convert.ToByte(Character)
Else
' No. XOR the checksum with this character's value

Checksum = Checksum Xor Convert.ToByte(Character)
End If
End Select
Next
' Return the checksum formatted as a two-character hexadecimal

Return Checksum.ToString("X2")
End Function
End Class

Direction & Speed Alerts

GPS devices analyze your position over time to calculate speed and bearing. The $GPRMC sentence at the beginning of this article also includes these readings. Speed is always reported in knots, and bearing is reported as an “azimuth”, a measurement around the horizon measured clockwise from 0° to 360° where 0° represents north, 90° means east, and etc. A little math is applied to convert knots into miles per hour. The power of GPS is again demonstrated with one line of code in listing 1-5 which figures out if a car is over the speed limit.

(Listing 1-5: This class can now tell you which direction you’re going and help prevent a speeding ticket.)

Collapse
'*******************************************************

'** Listing 1-5. Extracting speed and bearing

'*******************************************************

Public Class NmeaInterpreter

' Raised when the current location has changed

Public Event PositionReceived(ByVal latitude As String, _
ByVal longitude As String)
Public Event DateTimeChanged(ByVal dateTime As DateTime)
Public Event BearingReceived(ByVal bearing As Double)
Public Event SpeedReceived(ByVal speed As Double)
Public Event SpeedLimitReached()

' Processes information from the GPS receiver

Public Function Parse(ByVal sentence As String) As Boolean
' Discard the sentence if its checksum does not match our calculated

' checksum

If Not IsValid(sentence) Then Return False
' Look at the first word to decide where to go next

Select Case GetWords(sentence)(0)
Case "$GPRMC" ' A "Recommended Minimum" sentence was found!

Return ParseGPRMC(sentence)
Case Else
' Indicate that the sentence was not recognized

Return False
End Select
End Function

' Divides a sentence into individual words

Public Function GetWords(ByVal sentence As String) As String()
Return sentence.Split(","c)
End Function

' Interprets a $GPRMC message

Public Function ParseGPRMC(ByVal sentence As String) As Boolean
' Divide the sentence into words

Dim Words() As String = GetWords(sentence)
' Do we have enough values to describe our location?

If Words(3) <> "" And Words(4) <> "" And Words(5) <> "" And _
Words(6) <> "" Then
' Yes. Extract latitude and longitude

Dim Latitude As String = Words(3).Substring(0, 2) & "°" ' Append hours

Latitude = Latitude & Words(3).Substring(2) & """" ' Append minutes

Latitude = Latitude & Words(4) ' Append the hemisphere

Dim Longitude As String = Words(5).Substring(0, 3) & "°" ' Append hours

Longitude = Longitude & Words(5).Substring(3) & """" ' Append minutes

Longitude = Longitude & Words(6) ' Append the hemisphere

' Notify the calling application of the change

RaiseEvent PositionReceived(Latitude, Longitude)
End If
' Do we have enough values to parse satellite-derived time?

If Words(1) <> "" Then
' Yes. Extract hours, minutes, seconds and milliseconds

Dim UtcHours As Integer = CType(Words(1).Substring(0, 2), Integer)
Dim UtcMinutes As Integer = CType(Words(1).Substring(2, 2), Integer)
Dim UtcSeconds As Integer = CType(Words(1).Substring(4, 2), Integer)
Dim UtcMilliseconds As Integer
' Extract milliseconds if it is available

If Words(1).Length > 7 Then UtcMilliseconds = _
CType(Words(1).Substring(7), Integer)
' Now build a DateTime object with all values

Dim Today As DateTime = System.DateTime.Now.ToUniversalTime
Dim SatelliteTime As New System.DateTime(Today.Year, Today.Month, _
Today.Day, UtcHours, UtcMinutes, UtcSeconds, UtcMilliseconds)
' Notify of the new time, adjusted to the local time zone

RaiseEvent DateTimeChanged(SatelliteTime.ToLocalTime)
End If
' Do we have enough information to extract the current speed?

If Words(7) <> "" Then
' Yes. Convert it into MPH

Dim Speed As Double = CType(Words(7), Double) * 1.150779
' If we're over 55MPH then trigger a speed alarm!

If Speed > 55 Then RaiseEvent SpeedLimitReached()
' Notify of the new speed

RaiseEvent SpeedReceived(Speed)
End If
' Do we have enough information to extract bearing?

If Words(8) <> "" Then
' Indicate that the sentence was recognized

Dim Bearing As Double = CType(Words(8), Double)
RaiseEvent BearingReceived(Bearing)
End If
' Indicate that the sentence was recognized

Return True
End Function

' Returns True if a sentence's checksum matches the calculated checksum

Public Function IsValid(ByVal sentence As String) As Boolean
' Compare the characters after the asterisk to the calculation

Return sentence.Substring(sentence.IndexOf("*") + 1) = _
GetChecksum(sentence)
End Function

' Calculates the checksum for a sentence

Public Function GetChecksum(ByVal sentence As String) As String
' Loop through all chars to get a checksum

Dim Character As Char
Dim Checksum As Integer
For Each Character In sentence
Select Case Character
Case "$"c
' Ignore the dollar sign

Case "*"c
' Stop processing before the asterisk

Exit For
Case Else
' Is this the first value for the checksum?

If Checksum = 0 Then
' Yes. Set the checksum to the value

Checksum = Convert.ToByte(Character)
Else
' No. XOR the checksum with this character's value

Checksum = Checksum Xor Convert.ToByte(Character)
End If
End Select
Next
' Return the checksum formatted as a two-character hexadecimal

Return Checksum.ToString("X2")
End Function
End Class

Are We Fixed Yet?

The $GPRMC sentence includes a value which indicates whether or not a “fix” has been obtained. A fix is possible when the signal strength of at least three satellites is strong enough to be involved in calculating your location. If at least four satellites are involved, altitude also becomes known. The third word of the $GPRMC sentence is one of two letters: “A” for “active”, where a fix is obtained, or “V” for “invalid” where no fix is present. Listing 1-6 includes code to examine this character and report on fix status.

(Listing 1-6: The interpreter now knows when the device has obtained a fix.)

Collapse
'*******************************************************

'** Listing 1-6. Extracting satellite fix status

'*******************************************************

Public Class NmeaInterpreter

' Raised when the current location has changed

Public Event PositionReceived(ByVal latitude As String, _
ByVal longitude As String)
Public Event DateTimeChanged(ByVal dateTime As DateTime)
Public Event BearingReceived(ByVal bearing As Double)
Public Event SpeedReceived(ByVal speed As Double)
Public Event SpeedLimitReached()
Public Event FixObtained()
Public Event FixLost()

' Processes information from the GPS receiver

Public Function Parse(ByVal sentence As String) As Boolean
' Discard the sentence if its checksum does not match our calculated

' checksum

If Not IsValid(sentence) Then Return False
' Look at the first word to decide where to go next

Select Case GetWords(sentence)(0)
Case "$GPRMC" ' A "Recommended Minimum" sentence was found!

Return ParseGPRMC(sentence)
Case Else
' Indicate that the sentence was not recognized

Return False
End Select
End Function

' Divides a sentence into individual words

Public Function GetWords(ByVal sentence As String) As String()
Return sentence.Split(","c)
End Function

' Interprets a $GPRMC message

Public Function ParseGPRMC(ByVal sentence As String) As Boolean
' Divide the sentence into words

Dim Words() As String = GetWords(sentence)
' Do we have enough values to describe our location?

If Words(3) <> "" And Words(4) <> "" And Words(5) <> "" And _
Words(6) <> "" Then
' Yes. Extract latitude and longitude

Dim Latitude As String = Words(3).Substring(0, 2) & "°" ' Append hours

Latitude = Latitude & Words(3).Substring(2) & """" ' Append minutes

Latitude = Latitude & Words(4) ' Append the hemisphere

Dim Longitude As String = Words(5).Substring(0, 3) & "°" ' Append hours

Longitude = Longitude & Words(5).Substring(3) & """" ' Append minutes

Longitude = Longitude & Words(6) ' Append the hemisphere

' Notify the calling application of the change

RaiseEvent PositionReceived(Latitude, Longitude)
End If
' Do we have enough values to parse satellite-derived time?

If Words(1) <> "" Then
' Yes. Extract hours, minutes, seconds and milliseconds

Dim UtcHours As Integer = CType(Words(1).Substring(0, 2), Integer)
Dim UtcMinutes As Integer = CType(Words(1).Substring(2, 2), Integer)
Dim UtcSeconds As Integer = CType(Words(1).Substring(4, 2), Integer)
Dim UtcMilliseconds As Integer
' Extract milliseconds if it is available

If Words(1).Length > 7 Then UtcMilliseconds = _
CType(Words(1).Substring(7), Integer)
' Now build a DateTime object with all values

Dim Today As DateTime = System.DateTime.Now.ToUniversalTime
Dim SatelliteTime As New System.DateTime(Today.Year, Today.Month, _
Today.Day, UtcHours, UtcMinutes, UtcSeconds, UtcMilliseconds)
' Notify of the new time, adjusted to the local time zone

RaiseEvent DateTimeChanged(SatelliteTime.ToLocalTime)
End If
' Do we have enough information to extract the current speed?

If Words(7) <> "" Then
' Yes. Convert it into MPH

Dim Speed As Double = CType(Words(7), Double) * 1.150779
' If we're over 55MPH then trigger a speed alarm!

If Speed > 55 Then RaiseEvent SpeedLimitReached()
' Notify of the new speed

RaiseEvent SpeedReceived(Speed)
End If
' Do we have enough information to extract bearing?

If Words(8) <> "" Then
' Indicate that the sentence was recognized

Dim Bearing As Double = CType(Words(8), Double)
RaiseEvent BearingReceived(Bearing)
End If
' Does the device currently have a satellite fix?

If Words(2) <> "" Then
Select Case Words(2)
Case "A"
RaiseEvent FixObtained()
Case "V"
RaiseEvent FixLost()
End Select
End If
' Indicate that the sentence was recognized

Return True
End Function

' Returns True if a sentence's checksum matches the calculated checksum

Public Function IsValid(ByVal sentence As String) As Boolean
' Compare the characters after the asterisk to the calculation

Return sentence.Substring(sentence.IndexOf("*") + 1) = GetChecksum(sentence)
End Function

' Calculates the checksum for a sentence

Public Function GetChecksum(ByVal sentence As String) As String
' Loop through all chars to get a checksum

Dim Character As Char
Dim Checksum As Integer
For Each Character In sentence
Select Case Character
Case "$"c
' Ignore the dollar sign

Case "*"c
' Stop processing before the asterisk

Exit For
Case Else
' Is this the first value for the checksum?

If Checksum = 0 Then
' Yes. Set the checksum to the value

Checksum = Convert.ToByte(Character)
Else
' No. XOR the checksum with this character's value

Checksum = Checksum Xor Convert.ToByte(Character)
End If
End Select
Next
' Return the checksum formatted as a two-character hexadecimal

Return Checksum.ToString("X2")
End Function
End Class

As you can see, a whole lot of information is packed into a single NMEA sentence. Now that the $GPRMC sentence has been fully interpreted, the interpreter can be expanded to support a second sentence: $GPGSV. This sentence describes the configuration of satellites overhead, in real-time.

Real-Time Satellite Tracking

Knowing the location of satellites is important when determining how precise readings are and how stable a GPS fix is. Since GPS precision will be covered in detail in part two of this series, this section will focus on interpreting satellite location and signal strength. There are twenty-four operational satellites in orbit. Satellites are spaced in orbit so that at any time a minimum of six satellites will be in view to users anywhere in the world. Satellites are constantly in motion, which is good because it prevents the existence of “blind spots” in the world with little or no satellite visibility. Just like finding stars in the sky, satellite locations are described as the combination of an azimuth and an elevation. As mentioned above, azimuth measures a direction around the horizon. Elevation measures a degree value up from the horizon between 0° and 90°, where 0° represents the horizon and 90° represents “zenith”, directly overhead. So, if the device says a satellite’s azimuth is 45° and its elevation is 45°, the satellite is located halfway up from the horizon towards the northeast. In addition to location, devices report each satellite’s “Pseudo-Random Code” (or PRC) which is a number used to uniquely identify one satellite from another. Here’s an example of a $GPGSV sentence:

$GPGSV,3,1,10,24,82,023,40,05,62,285,32,01,62,123,00,17,59,229,28*70

Each sentence contains up to four blocks of satellite information, comprised of four words. For example, the first block is “24,82,023,40” and the second block is “05,62,285,32” and so on. The first word of each block gives the satellite’s PRC. The second word gives each satellite’s elevation, followed by azimuth and signal strength. If this satellite information were to be shown graphically, it would look like figure 1-1.

(Figure 1-1: Graphical representation of a $GPGSV sentence, where the center of the circle marks the current position and the edge of the circle marks the horizon.)

Perhaps, the most important number in this sentence is the “signal-to-noise ratio” (or SNR for short). This number indicates how strongly a satellite’s radio signal is being received. Remember, satellites transmit signals at the same strength, but things like trees and walls can obscure a signal beyond recognition. Typical SNR values are between zero and fifty, where fifty means an excellent signal. (SNR can be as high as ninety-nine, but I’ve never seen readings above fifty even in wide open sky.) In Figure 1-1, the green satellites indicate a strong signal, whereas the yellow satellite signifies a moderate signal (in part two, I will provide a way to classify signal strengths). Satellite #1’s signal is completely obscured. Listing 1-7 shows the interpreter after it is expanded to read satellite info.

(Listing 1-7: The interpreter is improved to interpret the location of GPS satellites currently in view.)

Collapse
'*******************************************************

'** Listing 1-7. Extracting satellite information

'*******************************************************

Public Class NmeaInterpreter

' Raised when the current location has changed

Public Event PositionReceived(ByVal latitude As String, _
ByVal longitude As String)
Public Event DateTimeChanged(ByVal dateTime As DateTime)
Public Event BearingReceived(ByVal bearing As Double)
Public Event SpeedReceived(ByVal speed As Double)
Public Event SpeedLimitReached()
Public Event FixObtained()
Public Event FixLost()
Public Event SatelliteReceived(ByVal pseudoRandomCode As Integer, _
ByVal azimuth As Integer, _
ByVal elevation As Integer, _
ByVal signalToNoiseRatio As Integer)

' Processes information from the GPS receiver

Public Function Parse(ByVal sentence As String) As Boolean
' Discard the sentence if its checksum does not match our calculated

' checksum

If Not IsValid(sentence) Then Return False
' Look at the first word to decide where to go next

Select Case GetWords(sentence)(0)
Case "$GPRMC" ' A "Recommended Minimum" sentence was found!

Return ParseGPRMC(sentence)
Case "$GPGSV" ' A "Satellites in View" message was found

Return ParseGPGSV(sentence)
Case Else
' Indicate that the sentence was not recognized

Return False
End Select
End Function

' Divides a sentence into individual words

Public Function GetWords(ByVal sentence As String) As String()
Return sentence.Split(","c)
End Function

' Interprets a $GPRMC message

Public Function ParseGPRMC(ByVal sentence As String) As Boolean
' Divide the sentence into words

Dim Words() As String = GetWords(sentence)
' Do we have enough values to describe our location?

If Words(3) <> "" And Words(4) <> "" And Words(5) <> "" And _
Words(6) <> "" Then
' Yes. Extract latitude and longitude

Dim Latitude As String = Words(3).Substring(0, 2) & "°" ' Append hours

Latitude = Latitude & Words(3).Substring(2) & """" ' Append minutes

Latitude = Latitude & Words(4) ' Append the hemisphere

Dim Longitude As String = Words(5).Substring(0, 3) & "°" ' Append hours

Longitude = Longitude & Words(5).Substring(3) & """" ' Append minutes

Longitude = Longitude & Words(6) ' Append the hemisphere

' Notify the calling application of the change

RaiseEvent PositionReceived(Latitude, Longitude)
End If
' Do we have enough values to parse satellite-derived time?

If Words(1) <> "" Then
' Yes. Extract hours, minutes, seconds and milliseconds

Dim UtcHours As Integer = CType(Words(1).Substring(0, 2), Integer)
Dim UtcMinutes As Integer = CType(Words(1).Substring(2, 2), Integer)
Dim UtcSeconds As Integer = CType(Words(1).Substring(4, 2), Integer)
Dim UtcMilliseconds As Integer
' Extract milliseconds if it is available

If Words(1).Length > 7 Then UtcMilliseconds = _
CType(Words(1).Substring(7), Integer)
' Now build a DateTime object with all values

Dim Today As DateTime = System.DateTime.Now.ToUniversalTime
Dim SatelliteTime As New System.DateTime(Today.Year, Today.Month, _
Today.Day, UtcHours, UtcMinutes, UtcSeconds, UtcMilliseconds)
' Notify of the new time, adjusted to the local time zone

RaiseEvent DateTimeChanged(SatelliteTime.ToLocalTime)
End If
' Do we have enough information to extract the current speed?

If Words(7) <> "" Then
' Yes. Convert it into MPH

Dim Speed As Double = CType(Words(7), Double) * 1.150779
' If we're over 55MPH then trigger a speed alarm!

If Speed > 55 Then RaiseEvent SpeedLimitReached()
' Notify of the new speed

RaiseEvent SpeedReceived(Speed)
End If
' Do we have enough information to extract bearing?

If Words(8) <> "" Then
' Indicate that the sentence was recognized

Dim Bearing As Double = CType(Words(8), Double)
RaiseEvent BearingReceived(Bearing)
End If
' Does the device currently have a satellite fix?

If Words(2) <> "" Then
Select Case Words(2)
Case "A"
RaiseEvent FixObtained()
Case "V"
RaiseEvent FixLost()
End Select
End If
' Indicate that the sentence was recognized

Return True
End Function

' Interprets a "Satellites in View" NMEA sentence

Public Function ParseGPGSV(ByVal sentence As String) As Boolean
Dim PseudoRandomCode As Integer
Dim Azimuth As Integer
Dim Elevation As Integer
Dim SignalToNoiseRatio As Integer
' Divide the sentence into words

Dim Words() As String = GetWords(sentence)
' Each sentence contains four blocks of satellite information.

' Read each block and report each satellite's information

Dim Count As Integer
For Count = 1 To 4
' Does the sentence have enough words to analyze?

If (Words.Length - 1) >= (Count * 4 + 3) Then
' Yes. Proceed with analyzing the block. Does it contain any

' information?

If Words(Count * 4) <> "" And Words(Count * 4 + 1) <> "" _
And Words(Count * 4 + 2) <> "" And Words(Count * 4 + 3) <> "" Then
' Yes. Extract satellite information and report it

PseudoRandomCode = CType(Words(Count * 4), Integer)
Elevation = CType(Words(Count * 4 + 1), Integer)
Azimuth = CType(Words(Count * 4 + 2), Integer)
SignalToNoiseRatio = CType(Words(Count * 4 + 2), Integer)
' Notify of this satellite's information

RaiseEvent SatelliteReceived(PseudoRandomCode, Azimuth, Elevation, _
SignalToNoiseRatio)
End If
End If
Next
' Indicate that the sentence was recognized

Return True
End Function

' Returns True if a sentence's checksum matches the calculated checksum

Public Function IsValid(ByVal sentence As String) As Boolean
' Compare the characters after the asterisk to the calculation

Return sentence.Substring(sentence.IndexOf("*") + 1) = GetChecksum(sentence)
End Function

' Calculates the checksum for a sentence

Public Function GetChecksum(ByVal sentence As String) As String
' Loop through all chars to get a checksum

Dim Character As Char
Dim Checksum As Integer
For Each Character In sentence
Select Case Character
Case "$"c
' Ignore the dollar sign

Case "*"c
' Stop processing before the asterisk

Exit For
Case Else
' Is this the first value for the checksum?

If Checksum = 0 Then
' Yes. Set the checksum to the value

Checksum = Convert.ToByte(Character)
Else
' No. XOR the checksum with this character's value

Checksum = Checksum Xor Convert.ToByte(Character)
End If
End Select
Next
' Return the checksum formatted as a two-character hexadecimal

Return Checksum.ToString("X2")
End Function
End Class

A World-Class Interpreter

International readers may have spotted a subtle problem early on that was not handled in the listings – numbers were being reported in the numeric format used in the United States! Countries like Belgium and Switzerland which use different formats for numbers, require adjustments to the interpreter in order to work at all. Fortunately, the .NET framework includes built-in support for converting numbers between different cultures, so the changes to the interpreter required are straightforward. In the interpreter, the only fractional value is speed, so only one change is necessary. The NmeaCultureInfo variable represents the culture used for numbers within NMEA sentences. The Double.Parse method is then used with this variable to convert speed into the machine’s local culture. Listing 1-8 shows the completed interpreter, now ready for use internationally.

(Listing 1-8: The completed interpreter, suitable for use anywhere in the world.)

Collapse
'*************************************************************

'** Listing 1-8. Adding support for international cultures

'*************************************************************

Imports System.Globalization
Public Class NmeaInterpreter

' Represents the EN-US culture, used for numers in NMEA sentences

Private NmeaCultureInfo As New CultureInfo("en-US")
' Used to convert knots into miles per hour

Private MPHPerKnot As Double = Double.Parse("1.150779", NmeaCultureInfo)

' Raised when the current location has changed

Public Event PositionReceived(ByVal latitude As String,_
ByVal longitude As String)
Public Event DateTimeChanged(ByVal dateTime As DateTime)
Public Event BearingReceived(ByVal bearing As Double)
Public Event SpeedReceived(ByVal speed As Double)
Public Event SpeedLimitReached()
Public Event FixObtained()
Public Event FixLost()
Public Event SatelliteReceived(ByVal pseudoRandomCode As Integer, _
ByVal azimuth As Integer, _
ByVal elevation As Integer, _
ByVal signalToNoiseRatio As Integer)

' Processes information from the GPS receiver

Public Function Parse(ByVal sentence As String) As Boolean
' Discard the sentence if its checksum does not match our calculated

' checksum

If Not IsValid(sentence) Then Return False
' Look at the first word to decide where to go next

Select Case GetWords(sentence)(0)
Case "$GPRMC" ' A "Recommended Minimum" sentence was found!

Return ParseGPRMC(sentence)
Case "$GPGSV"
Return ParseGPGSV(sentence)
Case Else
' Indicate that the sentence was not recognized

Return False
End Select
End Function

' Divides a sentence into individual words

Public Function GetWords(ByVal sentence As String) As String()
Return sentence.Split(","c)
End Function

' Interprets a $GPRMC message

Public Function ParseGPRMC(ByVal sentence As String) As Boolean
' Divide the sentence into words

Dim Words() As String = GetWords(sentence)
' Do we have enough values to describe our location?

If Words(3) <> "" And Words(4) <> "" _
And Words(5) <> "" And Words(6) <> "" Then
' Yes. Extract latitude and longitude

Dim Latitude As String = Words(3).Substring(0, 2) & "°" ' Append hours

Latitude = Latitude & Words(3).Substring(2) & """" ' Append minutes

Latitude = Latitude & Words(4) ' Append the hemisphere

Dim Longitude As String = Words(5).Substring(0, 3) & "°" ' Append hours

Longitude = Longitude & Words(5).Substring(3) & """" ' Append minutes

Longitude = Longitude & Words(6) ' Append the hemisphere

' Notify the calling application of the change

RaiseEvent PositionReceived(Latitude, Longitude)
End If
' Do we have enough values to parse satellite-derived time?

If Words(1) <> "" Then
' Yes. Extract hours, minutes, seconds and milliseconds

Dim UtcHours As Integer = CType(Words(1).Substring(0, 2), Integer)
Dim UtcMinutes As Integer = CType(Words(1).Substring(2, 2), Integer)
Dim UtcSeconds As Integer = CType(Words(1).Substring(4, 2), Integer)
Dim UtcMilliseconds As Integer
' Extract milliseconds if it is available

If Words(1).Length > 7 Then
UtcMilliseconds = CType(Words(1).Substring(7), Integer)
End If
' Now build a DateTime object with all values

Dim Today As DateTime = System.DateTime.Now.ToUniversalTime
Dim SatelliteTime As New System.DateTime(Today.Year, Today.Month, _
Today.Day, UtcHours, UtcMinutes, UtcSeconds, UtcMilliseconds)
' Notify of the new time, adjusted to the local time zone

RaiseEvent DateTimeChanged(SatelliteTime.ToLocalTime)
End If
' Do we have enough information to extract the current speed?

If Words(7) <> "" Then
' Yes. Parse the speed and convert it to MPH

Dim Speed As Double = Double.Parse(Words(7), NmeaCultureInfo) _
* MPHPerKnot
' Notify of the new speed

RaiseEvent SpeedReceived(Speed)
' Are we over the highway speed limit?

If Speed > 55 Then RaiseEvent SpeedLimitReached()
End If
' Do we have enough information to extract bearing?

If Words(8) <> "" Then
' Indicate that the sentence was recognized

Dim Bearing As Double = CType(Words(8), Double)
RaiseEvent BearingReceived(Bearing)
End If
' Does the device currently have a satellite fix?

If Words(2) <> "" Then
Select Case Words(2)
Case "A"
RaiseEvent FixObtained()
Case "V"
RaiseEvent FixLost()
End Select
End If
' Indicate that the sentence was recognized

Return True
End Function

' Interprets a "Satellites in View" NMEA sentence

Public Function ParseGPGSV(ByVal sentence As String) As Boolean
Dim PseudoRandomCode As Integer
Dim Azimuth As Integer
Dim Elevation As Integer
Dim SignalToNoiseRatio As Integer
' Divide the sentence into words

Dim Words() As String = GetWords(sentence)
' Each sentence contains four blocks of satellite information.

' Read each block

' and report each satellite's information

Dim Count As Integer
For Count = 1 To 4
' Does the sentence have enough words to analyze?

If (Words.Length - 1) >= (Count * 4 + 3) Then
' Yes. Proceed with analyzing the block. Does it contain any information?

If Words(Count * 4) <> "" And Words(Count * 4 + 1) <> "" _
And Words(Count * 4 + 2) <> "" And Words(Count * 4 + 3) <> "" Then
' Yes. Extract satellite information and report it

PseudoRandomCode = CType(Words(Count * 4), Integer)
Elevation = CType(Words(Count * 4 + 1), Integer)
Azimuth = CType(Words(Count * 4 + 2), Integer)
SignalToNoiseRatio = CType(Words(Count * 4 + 2), Integer)
' Notify of this satellite's information

RaiseEvent SatelliteReceived(PseudoRandomCode, Azimuth, Elevation, _
SignalToNoiseRatio)
End If
End If
Next
' Indicate that the sentence was recognized

Return True
End Function

' Returns True if a sentence's checksum matches the calculated checksum

Public Function IsValid(ByVal sentence As String) As Boolean
' Compare the characters after the asterisk to the calculation

Return sentence.Substring(sentence.IndexOf("*") + 1) = GetChecksum(sentence)
End Function

' Calculates the checksum for a sentence

Public Function GetChecksum(ByVal sentence As String) As String
' Loop through all chars to get a checksum

Dim Character As Char
Dim Checksum As Integer
For Each Character In sentence
Select Case Character
Case "$"c
' Ignore the dollar sign

Case "*"c
' Stop processing before the asterisk

Exit For
Case Else
' Is this the first value for the checksum?

If Checksum = 0 Then
' Yes. Set the checksum to the value

Checksum = Convert.ToByte(Character)
Else
' No. XOR the checksum with this character's value

Checksum = Checksum Xor Convert.ToByte(Character)
End If
End Select
Next
' Return the checksum formatted as a two-character hexadecimal

Return Checksum.ToString("X2")
End Function
End Class

Final Thoughts

You should now have a good understanding that an NMEA interpreter is all about extracting words from sentences. You can harness the power of satellites to determine your location, synchronize your computer clock, find your direction, watch your speed, and point to a satellite in the sky on a cloudy day. This interpreter will also work with the .NET Compact Framework without any modifications. If sentences were also stored in a file, the interpreter can be used to play back an entire road trip. These are all great features, especially considering the small size of the class, but is this interpreter ready to drive your car and pilot an airplane? Not quite yet. There is one important topic remaining which is required to make GPS applications safe for the real world: precision. GPS devices are designed to report any information they find, even if the information is inaccurate. In fact, information about the current location can be off as much as half a football field, even when devices are equipped with the latest DGPS and WAAS correction technologies! Unfortunately, several developers are not aware of this problem. There are some third-party components out there which are not suitable for commercial applications that require enforcing a minimum level of precision. Keep this article handy, however, because in part two of this series, I will explain precision enforcement in detail and take the interpreter even further to make it suitable for professional, high-precision applications!

Programmatically Setting Control Adapters for URL Rewriting and AJAX

Introduction

Anyone who has spent time developing URL rewriters will know that these do not always play nicely with AJAX components. This is because the HtmlForm element written out by .NET uses the actual URL for post-backs, and not the page's virtual URL that you are trying to preserve. This article discusses how this can be resolved cleanly, and also shows how control adapters may be set programmatically using Reflection. This is useful for creating plug & play components such as URL rewriters in order to minimise the amount of configuration required.
Background

In order to preserve the rewritten URL on post-back (and fix AJAX components), the HtmlForm's action attribute needs to be changed to the virtual URL.

Without this fix, you may experience the following error when using Asp.Net Ajax. This happens when your virtual URL is in a different directory than the real path.

Sys.WebForms.PageRequestManagerServerErrorException:
An Unknown error occurred while processing the request on the server.

The standard approach might be to extend the HtmlForm and override its Render method so that it alters the action attribute on render. Whereas this would work, it is a poor solution. It requires that all ASPX pages are edited in order to apply the fix. It also requires prior knowledge of the issue when other developers start creating pages, and it makes the URL rewriter component less pluggable.

A better solution would be to use a control adapter. This can be added via configuration settings in order to change the rendered HTML of all HtmlForm elements within an application. This would then not only apply the fix to all existing pages, but also to any future pages.

For anyone new to control adapters, these are discreet classes extended from the ControlAdapter class. These can be mapped to specific .NET controls via config settings located in the App_Browsers special directory. The adapters are invoked by the framework at runtime, and can alter the rendered HTML of any .NET control. A common use is to make the standard .NET controls more CSS friendly.

OK, so sounds like a good solution? Not yet! Imagine we are developing a component to be used by a third party that requires a control adapter. After they have plugged in our component, how do we ensure they add the control adapter to the solution? The standard approach is to provide heaps of documentation, and if it breaks... well... it's user error.

A smarter solution is to set the control adapter programmatically from within our component. In this way, the component is self-contained. It will be attached, when required, automatically, wherever and whenever our component is used. The bad news is, this ability isn't supported in the current framework. The good news is, it can be done, and here's how.
Solution

Firstly, we need a control adapter to correct the action attribute of the HtmlForm element. Here's one I made earlier.

public class HtmlFormAdapter : ControlAdapter
{
protected override void Render(HtmlTextWriter writer)
{
base.Render(new HtmlFormWriter(writer));
}
private class HtmlFormWriter : HtmlTextWriter
{
public HtmlFormWriter(HtmlTextWriter writer)
: base(writer)
{
this.InnerWriter = writer.InnerWriter;
}
public HtmlFormWriter(TextWriter writer)
: base(writer)
{
this.InnerWriter = writer;
}
public override void WriteAttribute(string key, string value, bool fEncode)
{
if (string.Compare(key, "action")==0)
{
value = HttpContext.Current.Request.RawUrl;
}
base.WriteAttribute(key, value, fEncode);
}
}
}

The next step is to set the control adapter programmatically. In order to do this, we use Reflection to access the hidden private _adapter property of the HtmlForm control and set this to an instance of our adapter. In this example, we assume we are running from within a HttpModule that is doing the URL rewriting, and hook the PreRequestHandlerExecute event. We can then hook the page's PreRender event in order to grab the HtmlForm control and attach the adapter.
Collapse

private void context_PreRequestHandlerExecute(object sender, EventArgs e)
{
HttpApplication application = (HttpApplication)sender;
HttpContext context = application.Context;
if (context.Handler is Page)
{
Page page = (Page)context.Handler;
page.PreRender += new EventHandler(RegisterControlAdapters);
}
}

private void RegisterControlAdapters(object sender, EventArgs e)
{
Page page = (Page)sender;
page.PreRender -= new EventHandler(RegisterControlAdapters);
if (page.Form != null)
{
// attach new instance of control adapter
FieldInfo adapterFieldInfo = page.Form.GetType().GetField("_adapter",
BindingFlags.NonPublic | BindingFlags.Instance);
if (adapterFieldInfo != null)
{
HtmlFormAdapter adapter = new HtmlFormAdapter();
FieldInfo controlFieldInfo = adapter.GetType().GetField("_control",
BindingFlags.NonPublic | BindingFlags.Instance);
if (controlFieldInfo != null)
{
controlFieldInfo.SetValue(adapter, page.Form);
adapterFieldInfo.SetValue(page.Form, adapter);
}
}
}
}

Summary

This is a pretty specific example; however, the point is that using this technique greatly aids the creation of smart components that require minimal configuration. It also provides a greater degree of power to component developers, who are then able to hook into the actual rendering of all the controls in an application from a single line in the web.config.



This leads to more pluggable components and fewer problems when it comes to development cycles or swapping out of components.

Selasa, 2008 Juni 24

12 Types of RowFocusIndicator

This tip is a script used to create 12 types of RowFocusIndicators including Color-Bands (White, Light-Yellow, ...) or Raised-Text-Band or Standard PowerBuilder RowFocusIndicators. The function also creates, destroys and changes them at run-time.

/*-----------------------------------------------

FUNCTION:
- boolean SetRfi(datawindow adw, int ai_rfi_cd)

PURPOSE/FUNCTIONALITY:
- Create ColorBand or RaisedTextBand to
serve as RowFocusIndicator for a Grid/Tabular
DataWindow.

- Switch Off/Destroy previously created
RowFocusIndicator

ARGUMENTS:
- adw : datawindow reference
- ai_rfi_cd : type of RowFocusIndicator to be set.

legal values: 0 to 12
0=Switch Off/Destroy

RETURNS:
- bool. True=Successful, False=Failed

HOW TO USE?
- Create this function as a window function or as a function

in a NVO with two arguments as under:
1. datawindow adw ... by value
2. integer ai_rfi_cd ... by value

and with boolean return
- If created as a function/event of datawindow, arument-1 (adw) is
not required. (Use "This" in lieu of "adw")
- Call it after dataobject is assigned to dataWindowControl.

- Function Call Example:
This.SetRfi(dw, 7)

- if created as a window function and called from the window,
assuming dataWindowControl is names as dw.

------------------------------------------------------------------------

Author:
Rajnikant Puranik/Nov.8,1998
email:puranik@bom2.vsnl.net.in

------------------------------------------------------------------------
NO BAR ON USAGE
*/

string ls_h, ls_err, ls_msg, ls_mod, ls_obj
string ls_bg_colour //background color
long ll_width //width of band

ll_width = Long(adw.Object.DataWindow.HorizontalScrollMaximum)

//ai_rfi_cd = Integer Code for type of SetRowFocusIndicator
CHOOSE CASE ai_rfi_cd
CASE 0 //No RowFocusIndicator
adw.SetRowFocusIndicator(Off!)
//Destroy Previously created RowFocusIndicator Rectangle
//named "rf_rect"
ls_obj = adw.Object.DataWindow.Objects
if Pos(ls_obj, "rf_rect") > 0 then
ls_mod = "destroy rf_rect"
ls_err = adw.Modify(ls_mod)
if ls_err <> "" then
ls_err = "Modify Error:" + ls_err
PopulateError(-1, ls_err)
goto lbl_err
end if
end if

CASE 1
adw.SetRowFocusIndicator(Hand!)

CASE 2
adw.SetRowFocusIndicator(FocusRect!)

CASE 4 to 11
/*
CREATE COLORED BAND/RECTANGLE NAMED : rf_rect

It is preferable to define colors and other
constants in a global NVO. e.g.:
constant long LIGHT_YELLOW = RGB(255, 255, 200)

In case the NVO is named gc ("g" fot Global
and "c" fot constants). then one could
substitute below:
ls_bg_colour = string(RGB(255, 255, 200))
by
ls_bg_colour = string(gc.LIGHT_YELLOW)
*/

CHOOSE CASE ai_rfi_cd
CASE 4
ls_bg_colour = string(RGB(255, 255, 255)) //WHITE

CASE 5
ls_bg_colour = string(RGB(217, 217, 217)) //LIGHTER GRAY

CASE 6
ls_bg_colour = string(RGB(192, 192, 192)) //LIGHT GRAY

CASE 7
ls_bg_colour = string(RGB(255, 255, 200)) //LIGHT YELLOW

CASE 8
ls_bg_colour = string(RGB(255, 179, 217)) //LIGHT PINK

CASE 9
ls_bg_colour = string(RGB(140, 200, 200)) //LIGHT GREEN

CASE 10
ls_bg_colour = string(RGB(255, 211, 168)) //LIGHT ORANGE

CASE 11
ls_bg_colour = string(RGB(200, 255, 255)) //LIGHT BLUE

END CHOOSE
ll_width += adw.width


//Create Rectangle String
ls_mod = "Create Rectangle(band=detail" + &
" x='" + string(adw.X) + "'" +&
" y='0'" +&
" height='80~t if(1=1, RowHeight(), 80)'" +&
" width='" + string(ll_width) + "'" +&
" name=rf_rect " +&
" visible='1~t if(currentrow() = getrow(), 1, 0)'" +&
" brush.hatch='6'" + &
" brush.color='" + ls_bg_colour + "'" +&
" pen.style='0'" +&
" pen.width='5'" +&
" pen.color='" + string(gc.BLACK) + "'" +&
" background.mode='2'" +&
" background.color='0'" +&
")"

CASE 12
if adw.VscrollBar then
ll_width += adw.width - 130
else
ll_width += adw.width - 20
end if
//create string for raised text rectangle named : rf_rect
ls_mod = "create text(band=Detail" +&
" color='0'" +&
" border='6'" +&
" x='" + string(adw.X + 10) + "'" +&
" y='0'" +&
" height='80~t if(1=1, RowHeight() - 5, 80)'" +&
" width='" + string(ll_width) + "'" +&
" text=''" +&
" name=rf_rect" +&
" visible='1~t if(currentrow() = getrow(), 1, 0)'" +&
" background.mode='2'" +&
" background.color='12632256'" +&
" )"

CASE ELSE
ls_err = "Illegal Option: " + String(ai_rfi_cd) + " !"
PopulateError(-1, ls_err)
goto lbl_err
END CHOOSE

CHOOSE CASE ai_rfi_cd
CASE 4 to 12
ls_err = adw.Modify(ls_mod)

if ls_err <> "" then
ls_err = "Modify Error:" + ls_err
PopulateError(-1, ls_err)
goto lbl_err
end if


if adw.SetPosition("rf_rect", "detail", FALSE) <> 1 then
ls_err = "SetPosition Error."
PopulateError(-1, ls_err)
goto lbl_err
end if
END CHOOSE

return true

//-------------------------------------------------------------------
lbl_err:

ls_msg = error.Text + ". " +&
"Error/Msg No.=" + String(error.Number) + "; " +&
"Window/Menu=" + error.WindowMenu + "; " +&
"Object=" + error.Object + "; " +&
"ObjectEvent=" + error.Object + "; " +&
"Error Line No.=" + String(error.Line) + "."

MessageBox("Message/Error", ls_msg, Exclamation!)

return false

Update Multiple Tables from a single Datawindow

Although the Datawindow painter does not allow you to update multiple tables. You can add this functionality to the Datawindow.

All of the datawindows attributes are updateable through the modify command or the dot notation. The update specifications that you specify in the Rows Update menu item are accessible in the same way as other attributes. The attributes you will need to set are the Update and Key attribute of the columns you want to update, then the UpdateTable attribute of the Datawindow.Table object.

Add a new function to the datawindow and pass the definition of your update tables to the function. The best way to do this is to create a structure which contains the table, columns and key columns. You can then pass an unbound array of these structures to the function. In the new function you would loop through the array elements using the structures attributes to swap the datawindow attributes with the passed values. Then call the Update() function with the no reset buffer option.

After all the updates are successful you would COMMIT the changes and call the ResetUpdate() function of the datawindow to reset the buffers.

Senin, 2008 Juni 23

Preventing User Access From Other Tools in powerbuilder

When users have other products on their machines that allow them to connect to a database, a tool like Access for example, they have the ability to connect to your Application Database. If the user knows their ID and password, which they do when they login via your application logon screen then there is nothing stopping them from connecting via Access and bypassing all your lovely business rules in your PowerBuilder client and hacking the data.

The simplest way to stop this is to encrypt the users password. It does not have to be anything complicated, but it should be more than just reversing the password for example. Take a look at the encryption routines on the software page.

When the user logs on you should attempt to logon the user with their ID and the encrypted version of their password. If this fails, try their password in plain text. If you get a connection then encrypt the password. This way you can secure the user accounts in place and re-encrypt when the users password is reset.

If you want to take the security a little further you can also hide the encryption algorithm from your fellow developers by writing the encryption routine as a C++ DLL and make an external function call.

IF Statements using SQL

When building list windows for users you often want to allow them to specify search criteria. If the search criteria are simple and only have a single field or all fields must be entered than a simple WHERE clause with retrieval arguments will do the job.

For very complicated arguments with multiple selections you will have to resort to dynamically altering the SQL behind the datawindow. But you may not realise that if you do not require multiple selections for a single field then with some clever SQL coding you can avoid time consuming dynamic SQL.

For example if we were building a selection window for employees, you may want to allow the user to search based on employee number, employee name, Address or any combination of the three. We can achieve this by declaring three retrieval arguments of the correct datatypes. In your retrieve script on the window you would have designed the arguments input criteria using a datawindow! so you could select the empty field is null option of the edit control. If you have not and shame on you then if the field is empty you will need to manually set it to null.

Then in the SQL you would code for the NULL values:

SELECT emp_no, emp_name, emp_addr1, emp_telno
FROM employee
WHERE ( emp_no = :al_emp_no OR :al_emp_no IS NULL )
AND ( emp_name = :as_emp_name OR :as_emp_name IS NULL )
AND ( emp_addr1 = :as_emp_addr OR :as_emp_addr1 IS NULL )

You would also want to concatenate '%' on to the end of the strings to allow for pattern matching and convert both sides of the comparison to Lower case to make it more user friendly, you would convert the retrieval argument to lower case once in Powerscript and pass it to the datawindow:

   ( Lower( emp_name ) LIKE :as_emp_name...

Sabtu, 2008 Juni 21

Ancestry Check Function

This function will check if an object inherits from the past in class name. Submitted by Erik Toft.

/////////////////////////////////////////////////////////////////////////
//
// Global service function: f_is_a
//
// Purpose: Allows a program to check the ancestry of any
// object to ensure that it is derived from an
// expected ancestor, therefore having an
// expected interface.
//
// Usefull in dynamic scripting, generic code,
// and inspective/intorspective applications.
//
// Programmer: Erik Toft
// Date: 03/24/2003
//
/////////////////////////////////////////////////////////////////////////

boolean lb_ret = false
classdefinition lcd_class_def

if isnull(apo_to_check) then return false
if not isvalid(apo_to_check) then return false
if isnull(apo_to_check.classdefinition) then return false
if not isvalid(apo_to_check.classdefinition) then return false
lcd_class_def = apo_to_check.classdefinition

do while not isnull(lcd_class_def) and isvalid(lcd_class_def)
if upper(lcd_class_def.name) = upper(as_class_name) then
lb_ret = true
exit //the loop
end if
lcd_class_def = lcd_class_def.ancestor
loop

return lb_ret

Listview Custom Sort in powerbuilder

PBDelta uses a variety of controls for the UI and in the File Selection window I use the Treeview and Listview objects. The combination gives an explorer style navigation for selecting files. I used the out of the box sort for the list view columns, however two of the columns where not "string" data but a size and a date column, when the out of the box sort was used it sorted them as string not as the original data type and was not particularly useful.

I wanted to make the columns sort using their underlying data types, but I did not want to keep two copies of the data around and then sort the rows manually based on the seconds copy of the data, so I set about coding the "sort" event on the list view. The sort event works a little bit like a comparator on a list, it gets called by PB and asks you to tell PB if item a is before, after or identical item b.

I coded this up and it worked just fine, however on a large list I noticed the performance when populating the list was slow. Even once populated the performance of the text columns was not as good as it was previously using the in built PB routines.

Some testing revealed that the complete sort process was firing every time I added an item to the list, which makes sense but was a bit of a performance hit. As my data was loaded in the correct order in the first place I did not even need the sort!

To improve matter I switched off the custom sort property so there was no sort by default and put a check in the columnclicked event to see which column was clicked, if it was a basic string data type I called the standard PB function as follows:

this.Sort( Ascending!, column )

Then if it was a complex data type I called the custom routine:

this.Sort( UserDefinedSort!, column )

Finally I added code to the sort event which used the 3 parameter version of the GetItem command. This version allows you to get the value from a list view column as follows:

this.GetItem( index1, column, ls_Item1 )
this.GetItem( index2, column, ls_Item2 )

ls_Item1 now contains the string value of the column, I just convert that into the original data type and then can perform the comparison as normal:

ll_Item1 = Long( ls_Item1 )
ll_Item2 = Long( ls_Item2 )
IF ll_Item1 > ll_Item2 THEN RETURN 1
IF ll_Item1 < ll_Item2 THEN RETURN -1

Jumat, 2008 Juni 20

Get Executable Path and Name

Purpose

To get the executable name and path for the current application
How To Use

I personally usually call this call in a custom application object which gets created when the application starts up. It may, however, be called virtually anywhere either as a global function or object function.

Code

Please note that the following external function call must be defined for the function to work.

External Function Definition
Function ulong GetModuleFileName (ulong hinstModule, ref string lpszPath, ulong cchPath ) Library "KERNEL32.DLL" Alias for "GetModuleFileNameA"

Function

of_GetAppPath( ref string as_AppFilePath, ref string as_AppFileName ) returns integer

integer li_pos, &
li_ActualPos
ulong lul_Handle, &
lul_Length
string ls_FullPath

// Get the path & executable name
lul_Handle = handle( getapplication() )
lul_Length = 255
ls_FullPath = Space( lul_Length )
GetModuleFilename( lul_Handle, ls_FullPath, lul_Length )

// Find the final backslash which marks the end of the path
li_Pos = Pos( ls_FullPath, "\" )
DO WHILE ( li_Pos > 0 )
li_ActualPos = li_Pos
li_Pos = Pos( ls_FullPath, "\", li_Pos + 1 )

LOOP

// Separate the path from the filename
IF ( li_ActualPos > 0 ) THEN
as_AppFileName = Mid( ls_FullPath, li_ActualPos + 1 )
as_AppFilePath = Left( ls_FullPath, li_ActualPos )

ELSE
as_AppFileName = ls_FullPath
as_AppFilePath = ""

END IF

RETURN 1


Note

Please note that if you're dealing in particularly long path and filenames you may wish to increase the value of lul_Length, which stores the maximum length of the path returned (including the filename).

Datawindow timer event

Purpose

It's quite possible to add a timer to a datawindow, it's just not very well documented! This page tells you how to do it.
How to do it
Starting

Create a new Datawindow control, declare a new event within this object and call it whatever you like (I personally use ue_timer). Map this event to the pbm_timer event id. You've just created a timer event for your datawindow control.
Setting the timer interval

To make use of the timer event, the attached Datawindow object must have the Timer Interval property set. In Powerbuilder v7, this is set in the General tab of the Properties pane, the interval is set in milliseconds.

If you wish to set the value programatically, the property is "DataWindow.Timer_Interval" when using the Modify syntax, or "dw_control.Object.DataWindow.Timer_Interval" using the dot notation.
Using the event

Enter code into your event, it should now run at the specified interval. Please also see the notes below.
Notes

* The timer value is set in milliseconds, unlike the Timer() function for standard Timer events.
* You need to include at least one field with an expression calling Now() or Today(), in the datawindow object attached to the datawindow control, to make the timer work.

Suggestions

This is useful stuff for a framework, it may be useful to define the event in your base datawindow class. It would also be advisable to create a function to set the timer_interval, so you have a standard interface. If you're being particularly adventurous you could also use create, within a datawindow modify, to add a field with an expression mapping to the Now() function, thereby eliminating the need to remember to add this field.

Kamis, 2008 Juni 19

Shutdown Window with no Prompts

When a serious error is encountered, it is sometimes necessary to close the window. Unless you
are careful, two problems can occur when closing the window:

1. The user will be prompted to save outstanding changes.
2. Some further logic might execute after the window closes causes a serious application error.

The window function f_Shutdown() avoids the two problems and it is easy for developers to call.

Function: f_ShutdownWindow

Parameters: ab_DisplayMessage: indicates if a window closing message
should be displayed.

string ls_MsgId

// display standard message, if caller wants one
IF ab_DisplayMessage THEN
// display message
gnv_app.inv_error.of_Message("sl_syserr_windowclosing")
END IF

// Stop PFC from prompting to save and then close the window
ib_DisableCloseQuery = True
This.Post pfc_close // close is posted so that scripts complete first

Replacement for pfc_u_tv's of_FindItem

The following function was written to stand in place for pfc_u_tv's of_FindItem function. At least in 16-bit environments (not sure of 32-bit), PFC's of_FindItem gets a stack overflow and GPFs when it has to search more than 50 tree items. This function avoids the problem by searching the tree items in a loop instead of recursively. The parameters are the same as of_FindItem, except the parameter "ai_Level" is replaced by "ab_AllDescendants" -- I found the parameter switch made the function easier to use.

Function: f_FindChild

Purpose: Searches a TreeView Item's immediate children (or all its descendants)
for the target value. In trying to match, it checks either the Label
or Data attribute on each child TreeView item. Only expanded TreeView
items are searched.

Parameters:
in - as_Attribute The attribute of the TreeView item to search ("Label",
or "Data")
in - aa_Target A variable of type Any containg the search target.
in - al_Begin The handle of the TreeView item to begin searching, if
0 entire tree will be searched.
in - ab_AllDescendants True indicates all descendants to be searched (False for only
immediate children).
in - ab_RespectCase True - search is case sensitive,
False - search is not case sensitive. Only used if the
target is a string.
in - ab_FullCompare True - Label or Data and Target must be equal
False - Label or Data can contain Target (uses POS() function).
Only used if the target is a string.

Returns: Long
>0: the handle of the item whose Label or Data matches the target
0: item not found
-1: a serious error occurred

String ls_Label, ls_Target
Integer li_Level, li_ChildLevel
Long ll_Handle, ll_Found, ll_Parent
Long ll_MatchingItem = 0
TreeViewItem ltvi_Item

// Get the first expanded child of the starting point
ll_Handle = f_GetFirstExpandedChild(al_Begin)

// Save the level for the child
IF ll_Handle > 0 THEN
IF This.GetItem(ll_Handle, ltvi_Item) = -1 Then Return -1
li_ChildLevel = ltvi_Item.Level
li_level = ltvi_Item.Level
END IF

// Checking for this item and stop when we get to the level higher than
// we're looking
DO WHILE ll_Handle > 0 AND ll_MatchingItem = 0 AND li_level >= li_ChildLevel

// only do comparison if we're at the correct level (i.e. immediate child
// or checking all levels)
IF li_Level = li_ChildLevel OR ab_AllDescendants THEN

// Check if this item matches using pfc_u_tv function
IF Trigger Event pfc_searchcompare(ll_Handle, as_Attribute, aa_Target, &
ab_RespectCase, ab_FullCompare) Then
ll_MatchingItem = ll_Handle
END IF
END IF

// Get the next Tree Item, going deeper into tree if the caller requested it
IF ab_AllDescendants THEN
ll_Handle = FindItem(NextVisibleTreeItem!, ll_Handle)
ELSE
ll_Handle = FindItem(NextTreeItem!, ll_Handle)
END IF

IF ll_Handle > 0 THEN
IF This.GetItem(ll_Handle, ltvi_Item) = -1 Then Return -1
li_level = ltvi_Item.Level
END IF
LOOP

Return ll_MatchingItem

This supporting function gets the first expanded child for a specified TreeView item, if any.

Function: GetFirstExpandedChild (supports f_FindChild)

Long ll_Handle
TreeViewItem ltvi_Item

// if whole tree is being searched, start looking from the first tree item
IF al_Parent = 0 THEN
ll_Handle = FindItem(RootTreeItem!, 0)
ELSE
IF This.GetItem(al_Parent, ltvi_Item) = -1 Then Return -1 // error getting TVI

// If the specified tree item is not expanded, it will have no expanded children
IF ltvi_Item.Expanded = False THEN
ll_Handle = 0 // return not found
ELSE
// it's expanded, so get handle to first child
ll_Handle = FindItem(ChildTreeItem!, al_Parent)
END IF
END IF

Return ll_Handle

Rabu, 2008 Juni 18

Message Handling in a PFC DataWindow

A general purpose DataWindow message function can provide two important capabilities:

* Suppress Messages on CloseQuery: when closing a window, it is desirable to suppress the display of error messages and allow PFC to display a suitable generic message in its CloseQuery logic. Since PFC invokes AcceptText() on any updateable DataWindows in CloseQuery, ItemChanged logic from various DataWindows may execute. A DataWindow message function should ensure that error messages are suppressed during CloseQuery.
* Set Focus to DataWindow: before displaying an error message related to a DataWindow, focus should be set to the DataWindow so the user understands the context. This involves both setting focus the DataWindow and making sure that if the DataWindow is on a Tab control, then the appropriate TabPage is selected.

The function called f_Message can be placed on u_dw. The function provides the above capabilities and it calls the standard PFC Error message function to actually display the error message.

Function: f_Message

Purpose: If the ParentWindow isn't closing, the function sets focus to the current DW
and displays the specified message.

Parameters: in - as_MsgId: Message Id of message to display
in - as_MsgParms: array of message parms for message.

Returns: Integer
>0: button selected for the message
0: message not displayed since window is in close process.
-1: error occurred.

integer li_Return = 1
boolean lb_DisplayMsg = True

// Determine if parent window is in the process of closing (i.e. in CloseQuery)
IF NOT IsNull(iw_ParentWin) THEN
IF iw_ParentWin.of_GetCloseStatus() THEN
lb_DisplayMsg = False
END IF
END IF

// If the window is NOT closing, then proceed with display of message
IF lb_DisplayMsg THEN
// first set focus to Window, DW and Tabpage
iw_ParentWin.ShowWindow() // see function in general tips
This.f_MakeTabPageCurrent() // see function in general tips
This.SetFocus()
li_Return = gnv_app.inv_error.of_message(as_MsgId, as_MsgParms)
ELSE
li_Return = 0
END IF

Return li_Return

Notes:

* iw_ParentWin is a pointer to the parent window which is initialized in the DW Constructor event by calling the of_GetParentWindow() function.
* This function can be overloaded to take no parameters and additional message parameters.

Making PFC Help Easily Accessible

To make the PFC Help easily accessible, customize the PowerBuilder Toolbar, as follows:

On PowerBuilder toolbar, choose right-mouse, "Customize..."
Choose "Custom Toolbar" radio button
Select a toolbar icon (to represent PFC Help) from top list and drag it to bottom list.
Specify the following toolbar details:

Command Line: WINHELP C:\PWRS\PB5\SYS\PBPFC050.HLP
or WINHELP C:\PROGRAM FILES\PB6\HELP\PBPFC060.HLP
Item Text: PFC Help
Item MicroHelp: Invoke PFC Help

Selasa, 2008 Juni 17

Avoiding "Double" Error Messages in DW Validation

A common problem when setting up validation logic in ItemChanged is that two message get
displayed. First, the intended message in ItemChanged and then a default PowerBuilder message:
"Item '' does not pass validation test". One simple solution to avoid this problem is to
have descendents set a flag (ib_SuppressMsg) in the ItemChanged event, when they've already
displayed an error message.

In ItemChanged event:

IF ... error situation ... THEN
... display message ...
li_Return = 1 // reject value
END IF

// Bottom of ItemChanged script ...
// set flag so ItemError know to suppress its default message
IF li_Return = 1 THEN
ib_SuppressMsg = True
END IF
In ItemError, check this flag and, if set, return 1 so the default ItemError  message is suppressed:

IF ib_SuppressMsg THEN
li_Return = 1
ib_SuppressMsg = False // reset flag for next time
ELSE
... error not caused by ItemChanged
END IF

Full ItemChanged and ItemError code samples are available.

Powerbuilder General Routine to Validate DDDW Input

When working with enterable Drop down DataWindows (DDDWs), a common requirement is to
validate the data entered into the DDDW. In addition, it is frequently necessary to get another
value from the DDDW (e.g. user enters a code which has to be validated and if the code is valid
you need to get the corresponding description from the DDDW). The function below serves both
those purposes and it is straightforward to call from ItemChanged.

Function: f_GetDddwValue

Purpose: First it locates the DDDW row that corresponds to the value entered. If
the DDDW entry was valid, it gets the value from another column on that DDDW row.

Parameters:
in - as_DDDWColumnName: Column name in parent DW (i.e. the DW with the DDDW).
This is the DW column name using the DDDW edit style.
in - as_ColumnValue - The DDDW value supplied by the user. It is used to
locate the appropriate DDDW row.
in - as_OtherDDDWColumnName - Name of the DDDW column whose value is desired.
This is the column name in the actual DDDW datawindow (e.g. the description).
out- as_OtherDDDWColumnValue - Value of DDDW Column (e.g. description) on the
selected DDDW row.

Returns: Long
>0 Row number where value was found
0 No input value provided
-1 User provided a bad value -- value entered was not found in DDDW.
-2 Internal Error, bad input column name (not a DDDW?)

string ls_ChildDDDWColumn
string ls_FindString
long ll_FoundRow
string ls_ErrorArgs[]

DatawindowChild ldwc_Source

IF Len(as_ColumnValue) > 0 THEN
// If this was a valid DW, continue to look for entry
IF This.GetChild(as_DDDWColumnName,ldwc_Source) = 1 THEN

// Determine the name of the column in the child datawindow to search
ls_ChildDDDWColumn = This.Describe(as_DDDWColumnName + ".DDDW.DataColumn")

CHOOSE CASE Lower(Mid(This.Describe(as_DDDWColumnName+'.ColType'), 1, 5))
CASE "decim", "long", "ulong", "numbe", "real"
ls_FindString = ls_ChildDDDWColumn + "=" + as_ColumnValue
CASE ELSE
ls_FindString = ls_ChildDDDWColumn + "='" + as_ColumnValue + "'"
END CHOOSE

// Search the datawindow for the value
ll_FoundRow = ldwc_source.Find( ls_FindString, 1, ldwc_Source.RowCount() )

// If the value is not found set user error return value: -1
IF ll_FoundRow <= 0 THEN
ll_FoundRow = -1
ELSE
// If the value is found, get the other DDDW column value requested
as_OtherDDDWColumnValue = String(f_GetItemAnyDWC(ll_FoundRow, &
as_OtherDDDWColumnName,ldwc_source))
END IF
ELSE
MessageBox("Error","Serious Error in f_GetDddwValue: invalid column name specified")
ll_FoundRow = -2
END IF
ELSE
// Return 0 to indicate no value was passed
ll_FoundRow = 0
as_OtherDDDWColumnValue = '' // reset description to EmptyString to correspond to input
END IF

Return (ll_FoundRow)

Note:
Sample invocation of this function can be found in the ItemChanged code sample.
f_GetItemAnyDWC can be modelled on of_GetItemAny in PFC's Base Services (with
the handle to the DW child passed as an argument).

Senin, 2008 Juni 16

Making Grids DW's Look "Nice" in Powerbuilder

When using a grid on the non-editable list datawindow, there are a few ugly-looking side-effects that you might want to avoid.

1. If you put the mouse on a "cell" on a grid, PB defaults to turning the colour of that field black and changing the row focus (and selecting the row if you are using a row selection facility). If you don't want this cell blackening (i.e. you don't want to cut/paste columns of data to other tools), then do the following:
a) Go into the DataWindow (not the DW control on the window)
b) Go to the white area (i.e. not a field on the window), hit right mouse, look at the "General" tab – on this tab, look at the "grid" GroupBox.
c) In the grid groupbox, turn "Mouse Selection" OFF.

2. You may also not get the row selection behaviour that you are looking for, if one or more columns in your grid DataWindow is editable. Giving all columns a tab order of 0 to get a smooth row selection line.

3. If you have functionality where the user can resort rows, by clicking the column header, you will find that the whole column turns black when the header is clicked on (a visually displeasing side effect). If you don’t want the column blackening, you essentially have to turn the grid capabilities off. This means column resizing and column movement will no longer be supported. To turn of the grid capabilities on the grid DW, follow procedures under point 1 a) - 1c) but choose grid "Off" in the drop down.

Making "GroupBoxes" inside a DataWindow (pre-PB6)

Prior to PowerBuilder 6, DataWindows did not have groupboxes as windows did. Here's how to make a "pretend" groupbox on your DW that works pretty well:

1. Paint your columns that you're going to group onto the DW.

2. Choose the recentangle (or box) on the DW Painter. Give the box a TRANSPARENT fill to reduce problems associated with determining whether the rectangle or the fields are "in front".

3. Size the Box over the fields you're grouping

4. Choose Right-mouse on the rectangle and choose the "Layer-Band" attribute. This avoids the PB "feature" that causes the box to move around when you resize the band on the DataWindow.

Getting Two Columns from the Same Row onto a Powerbuilder Graph

On a PB Graph DW, PowerBuilder wants different values on the graph (e.g. different pieces of the pie) to come from the SAME column but from different rows in the table. This can be frustrating when you are trying to graph two different columns in the SAME row of a table.

The solution (where two columns are graph are 'A' and 'B') is to select the two columns separately and union themm as follows:

SELECT A Value_Col, 'Desc of A' Category_Col FROM TAB_X WHERE criteria
UNION
SELECT B Value_Col, 'Desc of B' Category_Col FROM TAB_X WHERE criteria;

This will let you graph A and B with their own description.

The Invisible MessageBox in Powerbuilder

The MessageBox may not display in the example below:

SELECT cust_name
INTO :ls_CustName
FROM Customer
WHERE cust_id = :ls_custid;

MessageBox( "Warning!", "Customer: " + ls_CustName + &
" does not have sufficient credit." )

If the customer is not found or if the customer name is Null, then no MessageBox will display. The reason: MessageBox displays nothing if an input parameter is Null (or evaluates to Null when concatenated).

Having no MessageBox display could clearly result in both logic and business problems. Since the problem is essentially hidden, it might not be discovered for a long time.

The solution: if you are using a framework (like PFC) and you should consistently use its Message (or "Error") service which should handle such a situation. Otherwise, create your own function like f_MessageBox (see sample code). As shown in the sample code, f_MessageBox explicitly checks for Null input. It can display an error and/or log the error when it receives Null input.

Setting Focus to the TabPage with the Control in powerbuilder

A common problem when using Tab objects is that you can set focus to a control (e.g. a DataWindow) on the tab but that control is not actually visible, since another Tab Page is selected. This may happen because another TabPage (e.g. TabPage 2) has focus when the save process starts and the DW on this Tabpage (e.g. TabPage 3) has an ItemChanged Error message to display.

It is useful to create a general-purpose function that determines whether the control is on a Tab object and, if it is, sets focus to the Tab Page where the control is placed. The general-purpose function below was placed on the ancestor DataWindow object (u_dw in PFC) but it could be placed on any ancestor object.

Function Name: f_MakeTabPageCurrent

Purpose: This function determines whether this control is on a Tab Object.
If so, it makes the tab page that has this control current. If this object
is not on a tabpage, then nothing happens.

Parameters: None

Returns: Long >0 Index of tabpage where object was placed (and focus was set)
0 the object was not placed on a tab object.
-1 Error encountered

integer li_Index
boolean lb_TabParentFound = False
boolean lb_TabPageFound = False
long ll_RetCode

u_tab lu_tab // u_tab is a standard visual user object of type "tab"
powerobject lo_CurrentParent, lo_CurrentChild, lo_TabPage

// Loop through control hierarchy until a Tab control or Window is found
lo_CurrentParent = This.GetParent()
DO WHILE (NOT lb_TabParentFound) AND (lo_CurrentParent.TypeOf() <> Window!)
// we found tab control
IF lo_CurrentParent.TypeOf() = Tab! THEN
lu_tab = lo_CurrentParent // keep handle to Tab control
lb_TabParentFound = True
ELSE
// if we're not at window yet, keep looking for a window or tab control
IF lo_CurrentParent.TypeOf() <> Window! THEN
lo_CurrentChild = lo_CurrentParent // keep handle for the Child object
lo_CurrentParent = lo_CurrentParent.GetParent() // Get next parent
END IF
END IF
LOOP

// If a tab parent was found for this control, then make
// the tab page containing our control current
IF lb_TabParentFound THEN
lo_TabPage = lo_CurrentChild // tabpage was second last object looked at
li_Index = UpperBound(lu_Tab.Control[])

// Loop through child tabpages of Tab control looking for the tabpage
DO WHILE NOT lb_TabPageFound and (li_Index > 0)
IF lu_Tab.Control[li_Index] = lo_TabPage THEN
// tab page found ... select it
lu_tab.SelectTab(li_Index)
lb_TabPageFound = True
ELSE
li_Index = li_Index - 1
END IF
LOOP

IF lb_TabPageFound THEN
ll_RetCode = li_Index
ELSE
// TabPage not found -- maybe not in control array? (PB5 possibility)
ll_RetCode = -1
END IF
ELSE
ll_RetCode = 0
END IF

Return ll_RetCode

Minggu, 2008 Juni 15

How to store your error messages with powerbuilder

If you have an application which uses the PFC error message service (n_cst_error), you might have given some thought to distributing your application with the messages stored in a table. At first, the error message table looked very attractive. And it is, for most situations, especially those which access a central database. However, if your application is deployed to many different locations and each location contains a database, the table approach can become a nightmare.

As with many technical problems, a alternative approach came to me right after implementing a valid solution. At present, I'm storing this data within a database table which is immediately cached as soon as the application starts. I'm going to change the approach so that the datawindow contains the data all the time via the Rows/Data method.

The specific problem I'm trying to solve is the updating of the messages table as used by the PFC error service. At present, the options provided with PFC are limited to sourcing the messages from a file or from the database. I want a third source: the datawindow, unretrieved. I'm going to extend the error object via the PFE and add a third version of of_SetPredefinedSource. When a new message is to be added to the list, I'll do it directly within the datawindow painter via Rows/Data.

If I was continue maintaining the data within the table, then I would need to distribute updates to that table when new messages were added. Somehow these would be loaded into the database if they hadn't already been loaded. I think the PFC extension approach leaves less room for error. The application will always have the error messages associated with that version of the application.

Using the Resize and Preference service together in powerbuilder

The resize service (n_cst_resize) is useful for windows which contain controls which can be resized. The preference service (n_cst_preferences) is great for users. It allows windows to remember their last position and size. However, unless you use these services in the correct order, you can get into strife. For example, if you don't set the resize service and register your controls before the ancestor's open script completes, you may have wind up with controls that have not been resized properly.

Below are two screens. The first has not resized correctly but the second one has correctly resized. The difference between the two windows is the following script in the pfc_preopen event. This script will be executed before the open event of pfc_w_master which is where the preferences are reset. You'll note that of_SetOrigSize() is commented out. I've never actually found a situation where this is needed or useful. But that doesn't mean it isn't useful. Somewhere...out there.

this.of_SetResize(TRUE)
//this.inv_Resize.of_SetOrigSize(this.width, this.height)
this.inv_Resize.of_Register(dw_1, "ScaleToRight&Bottom")


resize1.gif (49186 bytes)
Incorrect resizing

resize2.gif (36495 bytes)
Correct resizing

Caching data and refreshing in powerbuilder

If you're not caching your data, perhaps you might want to look at the n_cst_dwcache server from PFC. The basic objective of a cache is to reduce database access and network traffic. But there are some traps which you should be aware of.
Drop down datawindows

One such trap is drop down datawindows which use the cache. Suppose you have a datawindow which contains a dddw. You use GetDatawindowChild to populate the dddw with the data from the cache. For our example, let's assume that the dddw displays ProductName. In the maintenance screen, the user changes the name of the product and saves the results. The cache contains the refreshed and correct name. However, the dddw will not display properly. It will contain the correct data, as shares with the cache. But the screen needs to be repainted so the user sees the new name and not the old name. The function of_RefreshSheets accomplishes this repaint. We've added this function to n_cst_appmanager, but it could be placed on just about any other object.

In some circumstances, the cached data needs to be refreshed because the database has been modified directly. In this case, see n_cst_dwcache::of_Refresh().

of_RefreshSheets

/*

Purpose: this is a fudge to fix up cache refresh.
when a cache is refreshed, we must repaint all datawindows.
this is the easy way to do this.

Parameters: nil

Results: 1

*/

w_frame lw_Frame
w_sheet lw_Sheet

lw_Frame = gnv_App.of_GetFrame()

lw_Sheet = lw_Frame.GetFirstSheet()
if IsValid(lw_Sheet) then
DO
// this will repaint any dddw etc.
lw_Sheet.SetRedraw(FALSE)
lw_Sheet.SetRedraw(TRUE)

lw_Sheet = lw_Frame.GetNextSheet(lw_Sheet)
LOOP UNTIL NOT IsValid(lw_Sheet)
end if

return 1

Table maintenance

Another issue with caching is table maintenance. If you link your datawindow with the cache and allow the users to modify that data, what happens if they decide not to save any changes? Not very nice.

So an option is not to share your table maintenance with the cache. Instead, load your data directly from the database. Then, after every save, refresh you cache. Here's what we do:

// make sure that the cached version is refreshed appropriately.
gnv_App.inv_dwCache.of_Refresh(is_cached_dw)

// make sure the sheets are displaying the correct data
gnv_App.of_Refresh_Sheets()

Another option, which I've thought about, but haven't tried, is to load your table maintenance from cache with a RowsCopy. That would eliminate that database transfer if the data had already been loaded.

Sabtu, 2008 Juni 14

Calling Oracle Stored Procs/Functions from Powerbuilder

Whenever you want to make a call to an Oracle stored procedure or stored function, a good approach is to first declare it as an external function and then invoke it based on that declaration.

Step 1: Declaring an Oracle Stored Procedure so that PowerBuilder Knows About it

This function/procedure declaration is done in the transaction user object (e.g. n_tr for a PFC App). Once inside the transaction user object, choose "Declare-Local External Functions" and follow the syntax below.

1.1 Stored Procedure (no package)

The declaration syntax for a stored procedure (on its own, outside package) is:

SUBROUTINE SubRtnName(args) RPCFUNC

In example 1.1, the declaration passes a string by value (i.e. IN) and a string by reference (i.e. IN OUT or OUT).

SUBROUTINE CalcAmount(string LS_In1, ref string LS_Out2) RPCFUNC

Notes:
- if the procedure is not in a package and does not take any array parameters, then you can click the procedures button to paste in the procedure declaration directly from the database.
- an optional alias clause can be added to allow PowerBuilder to use a different function name from Oracle (see alias format used with package declarations).

1.2 Procedure inside an Oracle package

The declaration syntax for a stored procedure inside a package is:

SUBROUTINE SubRtnName(args) RPCFUNC ALIAS FOR "PackageName.ProcName"

In example 1.2, the declaration passes a string by value (i.e. IN) and a string array by reference
(i.e. IN OUT or OUT).

SUBROUTINE CalcPenaltyAmt(string LS_In1, ref string LS_Out2[]) RPCFUNC ALIAS FOR "Penalty.P_Calc_Amount"

1.3 Stored Function (no package)

The declaration syntax for a stored function (on its own, outside package) is:

FUNCTION ReturnType FcnName(args) RPCFUNC

In example 1.3, the declaration passes a string by value (i.e. IN) and a string array by reference (i.e. IN OUT or OUT) and it returns a long.

FUNCTION long CalcAmount(string LS_In1, ref string LS_Out2[]) RPCFUNC

Note: the same notes given for stored procedure declarations apply to stored functions.

1.4 Function inside an Oracle package

The declaration syntax for a stored function inside a package is:

FUNCTION ReturnType FcnName(args) RPCFUNC ALIAS FOR "PackageName.FunctionName"

In example 1.4, the declaration passes a string by value (i.e. IN) and a string array by reference (i.e. IN OUT or OUT) and returns a long.

FUNCTION long CalcPenaltyAmt(string LS_In1, ref string LS_Out2[])) RPCFUNC ALIAS FOR "Penalty.f_Calc_Amount"

2. Invoking an Oracle Stored Procedure/Function

This is the invocation syntax for a stored procedure/function that has been declared in the transaction object is shown below.

Notes on Variables passed by Reference

Dynamically-sized output variables (i.e. strings and arrays) must be preallocated up to the size needed. When using this invocation method, PowerBuilder does not dynamically allocate the space needed for them.

Array Size Limitation: number of array elements times maximum element size cannot exceed 32K.

2.1 Invoking a Stored Procedure

The invocation syntax for a stored procedure is:

TransactionObjectName.ProcName(args)

Sample invocation:

string in_parm1
string out_parm2

in_parm1 = "input value"
out_parm2 = space(50) // preallocating space for string

SQLCA.CalcAmount(in_parm1, out_parm2)

2.2 Invoking a Stored Function (shown using an array variable)

The invocation syntax is:

ReturnValue = TransactionObjectName.FcnName(args)

Sample invocation:

string in_parm1
string out_parm2[5] // defining fixed sized array
long ll_return

in_parm1 = "input value"

// preallocating space for 500 chars for whole string array.
// Each element will effectively be 500 bytes by allocating
// the first.
out_parm2[1] = space(500)

ll_Return = SQLCA.CalcAmount(in_parm1, out_parm2[])

Building a Systray Icon

I've been getting a lot of requests for an example of how to put an icon in the systray. The systay is that small box near the time on the task bar. The object I've described is the main work horse and you should import this object into your Powerbuilder library.

Basically there are a bunch of API calls which this object calls. The functions on the object provide a nice friendly interface to the API calls:

--START OF OBJECT
$PBExportHeader$nca_systray.sru
$PBExportComments$Systray Utilities V2.0
forward
global type nca_systray from nonvisualobject
end type
end forward

type notifyicondata from structure
long cbsize
long hwnd
long uid
long uflags
long ucallbackmessage
long hicon
character sztip[ 64 ]
end type

global type nca_systray from nonvisualobject autoinstantiate
end type

type prototypes
function long Shell_NotifyIcon ( long dwMessage, ref notifyicondata lpData )
library "shell32"
function long LoadImageA ( long hInstance, string lpszName, uint uType, int
a, int b, uint l ) library "user32"
function long LoadIconA( long hInstance, long lpcstr ) library 'user32'
end prototypes

type variables
Protected:
notifyicondata istr_lpdata
window iw_win
boolean ib_SystemTrayExists

CONSTANT long NIF_MESSAGE=1
CONSTANT long NIF_ICON=2
CONSTANT long NIF_TIP=4

CONSTANT long NIM_ADD=0
CONSTANT long NIM_MODIFY=1
CONSTANT long NIM_DELETE=2

end variables

forward prototypes
public subroutine addicon (string as_iconname, string as_tiptext, window aw_win)
public subroutine delicon ()
public subroutine changeicon (string as_IconName)
public subroutine changetip (string as_TipText)
public function boolean ib_systemtrayexists ()
end prototypes

public subroutine addicon (string as_iconname, string as_tiptext, window aw_win);
IF ib_SystemTrayExists THEN RETURN

iw_Win = aw_Win
istr_lpData.szTip = as_TipText + Char ( 0 )
istr_lpData.uFlags = NIF_ICON + NIF_TIP + NIF_MESSAGE
istr_lpData.uID = 100
istr_lpData.cbSize = 88
istr_lpData.hwnd = Handle ( iw_Win )
istr_lpData.uCallbackMessage = 1024
istr_lpData.hIcon = LoadImageA( 0, as_IconName, 1, 0, 0, 80 )

shell_notifyicon ( NIM_ADD, istr_lpData )

ib_SystemTrayExists = TRUE
end subroutine
public subroutine delicon ();
IF NOT ib_SystemTrayExists THEN RETURN

istr_lpData.uID = 100
istr_lpData.cbSize = 88
istr_lpData.hwnd = Handle ( iw_Win )

shell_notifyicon ( NIM_DELETE, istr_lpData )

ib_SystemTrayExists = FALSE

end subroutine

public subroutine changeicon (string as_IconName);
IF NOT ib_SystemTrayExists THEN RETURN

istr_lpData.hIcon = LoadImageA ( 0, as_IconName, 1, 0, 0, 80 )
istr_lpData.uFlags = NIF_ICON

shell_notifyicon( NIM_MODIFY, istr_lpData )


end subroutine

public subroutine changetip (string as_TipText);
IF NOT ib_SystemTrayExists THEN RETURN

istr_lpData.szTip = as_TipText + Char ( 0 )
istr_lpData.uFlags = NIF_TIP

shell_notifyicon ( NIM_MODIFY, istr_lpData )

end subroutine

public function boolean ib_systemtrayexists ();
RETURN ib_systemtrayexists
end function

on nca_systray.create
TriggerEvent( this, "constructor" )
end on

on nca_systray.destroy
TriggerEvent( this, "destructor" )
end on

event destructor;
IF ib_SystemTrayExists THEN this.DelIcon()
end event
--END OF OBJECT

Then in your window you need to instantiate the object and call the addicon function. In your open event call the function like:

inca_sysTray.AddIcon( 'pbbrtray.ico', 'PBBrowse V2', this )

Then you need to add an event to your window so you can receive notification of when the user clicked your systray icon. Add an event called systray for pbm_custom01.

Then to make the following script work, add a static text called st_status to the window and add this code to the systay event:

constant long WM_MOUSEMOVE=512
constant long WM_LBUTTONDOWN=513
constant long WM_LBUTTONUP=514
constant long WM_LBUTTONDBLCLK=515
constant long WM_RBUTTONDOWN=516
constant long WM_RBUTTONUP=517
constant long WM_RBUTTONDBLCLK=518

Choose case Message.LongParm
case WM_MOUSEMOVE
st_status.text = 'Mouse Moved'

case WM_LBUTTONDOWN
st_status.text = 'Left Mouse Down'

case WM_LBUTTONUP
st_status.text = 'Left Mouse Up (clicked)'

case WM_LBUTTONDBLCLK
st_status.text = 'Left Mouse Double Clicked'

case WM_RBUTTONUP
st_status.text = 'Right Mouse Up (clicked)'

case WM_RBUTTONDOWN
st_status.text = 'Right Mouse Down'

case WM_RBUTTONDBLCLK
st_status.text = 'Right Mouse Double Clicked'

case else
st_Status.text = 'something else happened'

end choose

Viola! a systray icon for your PowerBuilder application.

Interfacing with and Sending Data to Microsoft Word using powerbuilder

This is the first in a two-part set of 10-Minute Solutions about interfacing with Microsoft Word and outputting a set of data from a datawindow. (The second part demonstrates how to read the data back in from the Word document into the datawindow control.) This example could be very useful in a workflow application where you want to pre–fill out a Word form and (using what I wrote about in my last couple of Solutions) send the Word document as an attachment. Eventually the Word file comes back filled out and the PowerBuilder app gets the mail message, opens the Word attachment, extracts the data, and completes the transaction.

I am using PowerBuilder 6.5 and Word 2000, although this will also work with Word 97. The code will not work reliably with PowerBuilder 5, however, so you will need at least version 6. This article's code uses a hard-coded array of column names. You could alter this code to output all of the columns from a datawindow, but in practice you will probably not want to write out all of the columns from the datawindow, but rather just a selected few columns.

Start off by creating a new application and allowing PowerBuilder to create its simple sample application. Then open up the sheet window and place two datawindow controls. For my example I am using psDemoDB supplied with PowerBuilder and the customer table. I created a list style datawindow that shows a list of customers and a freeform style datawindow that shows the full details of the customer. I placed these two datawindows in the two controls and added code so that when I click on a customer in the list, the full details are displayed in the freeform datawindow. Then I added two buttons—one called cb_out with the label "Word Out" and a second one called cb_in with the label "Word In":

Next you need a function that allows you to get data from the datawindow without knowing its data type, so create a window function:


any GetItemAny( datawindow adw_DW, long al_Row, string as_Column )

Add the following code to the function:


Long ll_Col
Any la_A

ll_Col = Long( adw_dw.Describe( as_Column + ".ID" ) )
IF ll_Col > 0 THEN &
la_A = adw_dw.object.data.primary.current[ al_Row, ll_Col ]

RETURN la_A

This function takes any valid row and column name and returns the data as the Any data type. You will use this function to allow you to send the data to Word without having to check and convert the column data types manually.

Edit the clicked event for the Word Out command button and add the following variables:


OleObject lole_OLE, lole_Selection
Long ll_Col, ll_Cols
String ls_Columns[8]={'id', 'fname', 'lname', &
'address', 'city', 'state', 'zip', 'phone'}

You have defined two OLE objects, one of which is the container for the Word object, while the other is used for holding sub Word objects during the processing to help improve the performance. There are two longs, one for the index and one for the maximum. Then you have a bound array of the column names that you want to send to Word. This array could be changed to a list of all column names extracted from the datawindow.

First you need to connect to the Word OLE interface and start up a Word document. Ask for Word version 8. By specifying this version number, you are telling PowerBuilder that you want an OLE object of at least version 8; if a higher version is available you will get it. (Word 97 is "version 8" and Word 2000 is "version 9.")


lole_OLE = CREATE OleObject

SetPointer( HourGlass! )

// Connect to the word application
lole_OLE.ConnectToNewObject( 'word.application.8' )
lole_OLE.Documents.Add()

lole_Selection = lole_OLE.Selection

You have created the OLE object, connected to Word, and assigned the lole_Selection variable to the lole_OLE.Selection OLE object. Most of the code in the following example uses the selection object in Word, so that by obtaining a reference to the selection object you can speed up the processing and shorten all of your commands.

The first thing you need to do is make sure that the Word document is visible, so that you can see what is going on. Then you need to add a table to the current Word document. This table puts the data in a nice easy format for the user to read. Reference the activedocument tables object and call the add method, passing the current selection (cursor point) as the insertion point. Specify one row and two columns:


// Use a table to insert the freeform record and allow editing.
lole_OLE.Application.Visible = TRUE
lole_OLE.ActiveDocument.Tables.Add( lole_Selection.Range, 1, 2 )

Now it is a simple matter of looping through all the columns and putting the column name and data into the table. One slight twist is that you will need some way of retrieving the data once the user has edited the document. To do this, use Word's bookmark feature. This allows a range of data to be known by a name, much like in Microsoft Excel. As you add each data item you also add a bookmark that points to the current data cell:


// Add each column and then the data items as a bookmark
ll_Cols = UpperBound( ls_Columns )
FOR ll_col = 1 TO ll_Cols
lole_Selection.TypeText( ls_Columns[ ll_Col ] )
lole_Selection.MoveRight( 12 )
lole_Selection.TypeText( String( &
GetItemAny( dw_maint, 1, ls_Columns[ ll_Col ] ) ) )

// Quick way to select cell
lole_Selection.MoveLeft( 12 )
lole_Selection.MoveRight( 12 )

lole_Selection.Bookmarks.Add( ls_Columns[ ll_Col ], lole_Selection.Range )
// If not the last item then move right to create another row
IF ll_Col <> ll_cols THEN lole_Selection.MoveRight( 12 )
NEXT

Now that all the data is in the table, make the table look pretty for the user by resizing all the column widths to the maximum data item. Disconnect from Word and delete your OLE object:


// Goto the top of the table ready for editing
lole_Selection.MoveUp( 5, 8 )
lole_Selection.HomeKey( 5 )

// Make the data table look pretty
lole_Selection.Tables[1].AutoFormat( 16, True, &
True, False, True, True, False, False, True )

lole_OLE.DisconnectObject()
DESTROY lole_OLE

Run the application and click on the Word Out button. The Word document should appear, populated with a table for the row you clicked upon.

In the next installment of this two-parter, I will complete the example and show how to extract data back out of the Word document.

Sending E-mail with MAPI using powerbuilder

This is the first in a two-part set of 10-Minute Solutions about using the MAPI (Mail Application Programming Interface) from within PowerBuilder. For this article I am using PowerBuilder 6.5 with Microsoft Outlook 98 as my mail client software. The same code works for any MAPI-compliant mail software.

PowerBuilder supports a large set of mail interface commands and objects. The first, and probably the most important, is the "mailsession" object. This object is used by PowerBuilder to set up a communication port to the mail application on your system. To establish this session, you must create a new mail session object and ask it to connect to the mail software.

There are two ways to establish the mail session, depending on your users and how your mail system is configured. The first option uses the mail's internal user ID and password validation dialog box; the second option allows you to prompt for the user ID and password and then specify these details for the mail session. The following two examples show you both ways of making the connection:


mailsession lms_MAPISession

lms_MAPISession = CREATE mailsession
lms_MAPISession.MailLogon()

The code above creates a session. If the mail application requires a user ID or password, the user is prompted by the mail application. The following code allows you either to hard-code or prompt the user for an ID or password:


mailsession lms_MAPISession

lms_MAPISession = CREATE mailsession
lms_MAPISession.MailLogon( "userid", "password" )

The MailLogon command returns a return type of MailCodeReturn, which tells you whether the command was executed all right or not. You can see the use of this return type in the final example.

To send a mail message, use the MailSend command. This command accepts a single argument of an object of type MailMessage. This MailMessage object has a bunch of properties—such as the recipient and the text of the message—which you can fill out with details of your mail message.

Finally, after sending the message close your mail session and destroy the mail session object. The MailMessage object is a structure, so it will be automatically cleaned up. The full example is:


mailsession lms_MAPISession
mailmessage lmm_Message

lms_MAPISession = CREATE mailsession
IF lms_MAPISession.MailLogon() = MailReturnSuccess! THEN
// Mail Sesion is ok so Populate the Message
lmm_Message.Subject = "Hello Ken Howe"
lmm_Message.NoteText = "Inquiry.com is my favourite web site"
lmm_Message.Recipient[ 1 ].Name = 'khowe@pbdr.com'
lmm_Message.Recipient[ 2 ].Name = 'Howe, Ken'

IF lms_MAPISession.MailSend( lmm_Message ) = MailReturnSuccess! THEN
Messagebox( 'Mail', 'Message was sent.' )
END IF

lms_MAPISession.MailLogoff()
END IF

DESTROY lms_MAPISession

In this example you can see the message object being populated. The recipient can either be an e-mail address or the name of a person in your address book. You can also see that the functions are tested to make sure they work before continuing to the next command.

Other 10-Minute Solutions
Building a Systray Icon
Export Data to Excel Through OLE Automation
Using the Microsoft Browser Control with PowerBuilder
Copy a Window Image to Word
Sending E-mail with MAPI
Receiving E-mail with MAPI
Interfacing with and Sending Data to Microsoft Word
Reading Data From a Word Document Into a Datawindow Control
Font List Extraction: Part I
Font List Extraction: Part II
Font List Extraction: Part III
Font List Extraction: Part IV

Receiving E-mail with MAPI using Powerbuilder

This article is the second of two 10-Minute Solutions on using the MAPI (Mail Application Programming Interface) from within PowerBuilder. For this article I am using PowerBuilder 6.5 with Microsoft Outlook 2000 as my e-mail client software. The same code will work for any MAPI-compliant mail software, however.

As you saw in my previous 10-Minute Solution, "Sending E-mail with MAPI", PowerBuilder supports a large set of e-mail interface commands and objects. Read it for more details on the session object.

For this article, you will need to create a datawindow and a window. First create a datawindow, which you will use to display your list of messages. MAPI works by using a unique ID field to reference mail items within the mail system. You want to display the message header to the user, but have access to the mail ID so you can retrieve the body of the e-mail message as the user scrolls through the list.

Create a new datawindow with a result set that matches the following:

Delete the ID field from the generated display and move the header field to line up next to the left side of the datawindow. Save the datawindow and create a new window.

On the window place a datawindow control and name it dw_mail, add a command button named cb_mail, and finally add a multiline edit field named mle_body. Set up the window with the datawindow at the top of the window with the multiline edit below.

Place the datawindow object you created previously into the datawindow control. Now you will use the MAPI objects to populate the list of messages in your inbox.

As in the previous article, you need to connect your mail session object to the mail system and check for errors. See the previous article for more information on making this connection.

Next, obtain a list of the mail messages in the inbox. To do this, use a function of the mail session to get a list of all the message IDs in the inbox. You use the MailGetMessage function to do this. This function allows you to specify whether you want to get only unread messages or all messages. This is great for writing applications that automatically respond to, or process, incoming e-mail messages. Ask the mail session object to retrieve all the message headers to make sure there is something to see in the list.

The MailGetMessage function populates an unbound array of strings, which is an attribute of the Mail Session Object. You can find out how many messages there are by using the upperbound function. Then use another function to retrieve the actual message header into a Mail Message Object, where you can examine the properties of the message.

To obtain the message details, use MailReadMessage, which accepts a message ID from the Mail Session Object and populates a MailMessage object. This command allows you to specify how much or little information you want to extract from the mail system. At this point you just want the message headers, so specify MailEnvelopeOnly; you can request as little or as much information as you require. The obvious benefit of this is that it is much quicker to retrieve just the header than to get the body and attachments of the message as well. The last parameter allows you to mark the message as read or unread. Use FALSE to leave any unread messages as unread.

Finally, populate the information into the datawindow ready for your users to see the list. The first script, which should be placed in the command button, is as follows:


Long ll_I, ll_Max
mailsession lms_MAPISession
mailmessage lmm_Message

lms_MAPISession = CREATE mailsession
IF lms_MAPISession.MailLogon() = MailReturnSuccess! THEN
lms_MAPISession.MailGetMessages( "", FALSE )
ll_Max = UpperBound( lms_MAPISession.MessageID )
FOR ll_I = 1 TO ll_Max
lms_MAPISession.MailReadMessage( &
lms_MAPISession.MessageID[ ll_I ], &
lmm_Message, MailEnvelopeOnly!, FALSE )

dw_mail.ImportString( &
lms_MAPISession.MessageID[ ll_I ] + '~t' + &
lmm_Message.Subject + '~r~n' )
NEXT
lms_MAPISession.MailLogoff()
END IF

DESTROY lms_MAPISession

Now that the list of messages is in the datawindow, you can add a script to the datawindow control that populates the multiline edit when the user clicks on a header. To do this you need to extract the message ID from the datawindow and make another call to the MailReadMessage function—only this time use a different enumerated type to request the message body. You can then display the message body in the multiline edit.

Add the following code to the clicked event of the datawindow control:


mailsession lms_MAPISession
mailmessage lmm_Message
String ls_ID

lms_MAPISession = CREATE mailsession
IF lms_MAPISession.MailLogon() = MailReturnSuccess! THEN
ls_ID = this.GetItemString( row, 'id' )

lms_MAPISession.MailReadMessage( ls_ID, &
lmm_Message, mailSuppressAttachments!, FALSE )

mle_mail.Text = lmm_Message.NoteText

lms_MAPISession.MailLogoff()
END IF

DESTROY lms_MAPISession

Run the application, click the command button to populate the list of mail messages in the inbox, and then click on a message header to see the body of your mail message.

The MAPI commands built into PowerBuilder are very powerful. I hope these two articles have inspired you to write your own custom e-mail program!

Using the Microsoft Browser control with PowerBuilder

This article will explain how to get the Microsoft Browser control working from a PowerBuilder window. You will need to have a copy of the Microsoft Browser on your machine for this to work.

The best way of using the browser control is to create a user object in PowerBuilder and dynamically create the object at runtime. This way you can test the registry on the machine to see if the browser control exists before opening the user object with the control and stop an error from occurring. It would take too long to explain that process but the test you can use to see if the browser exists on a machine and then create the browser control or not is as follows:

String ls_Temp
RegistryGet( 'HKEY_CLASSES_ROOT\Shell.Explorer.2\CLSID', '', ls_Temp )
IF NOT IsNull( ls_Temp ) AND ls_Temp <> '' THEN
// Browser found so create the control
END IF

To get the Browser control working, open up PowerBuilder and create a new window. Create a single line edit control named sle_url; this control will be used to type in the URL for the browser. Also add three command buttons: the first named cb_browse, adding some suitable text for the prompt; the second named cb_back, with a label of back; and the third named cb_fore, with a label of forward. Arrange these controls in a line across the top of the window so they sort of resemble a browser.

Next, click on the OLE control option and you will be presented with the OLE control list dialog. Click on the Insert Control tab and scroll the list of Control Types until you see the Microsoft Web Browser control. Double click on the control and the dialog will be dismissed. Then click on the window to place the control. Name the control ole_browser.

Next we need to add some code to trap and ignore errors generated by the browser control. To do this, right-click on the browser control, select script, and go to the error event. Add the following code:

action = ExceptionIgnore!

Next we need to add code to allow the URLs that are typed-in to be opened. To do this, right-click on cb_browse, go to the clicked event and add the following code:

ole_browser.object.Navigate( sle_url.text )

Next we need to add the code to the navigation buttons. In the cb_back button add the following code:

ole_browser.object.goback()

In the cb_fore button add the following code:

ole_browser.object.GoForward()

The last thing we need to do is to resize the browser work space based on the size of the window. To do this we need to add some code to the window resize event:

ole_browser.Resize( newwidth - 30, newheight - ole_browser.Y )
ole_browser.object.Width = UnitsToPixels( ole_browser.width,
XUnitsToPixels! )
ole_browser.object.Height = UnitsToPixels( ole_browser.Height,
YUnitsToPixels! )
ole_browser.object.refresh()

Close the window and save it as w_browser, then add a line of code to your application open event to open the window and give the application a test.

Run the application, type in a URL into the single line edit (for example, http://www.inquiry.com) and click on the browse button. Your favorite web site should now be displayed. Note that you can also use the same control to browse your hard drive by typing in a file specification such as c:\.

SQL Tutorial -Powerbuilder SQL implementation

One of the great things about Powerbuilder is the implementation of SQL (Structured Query Language). The Powerbuilder Datawindow generates SQL to retrieve data from the database. These SQL examples assume you already have some basic Powerbuilder / SQL experience. If not, you may want to use a resource like our Powerbuilder OOP ebook to get up to speed. These examples are implemented using Powerbuilder 10 and ASA (Adaptive Server Anywhere) V9.
Using SQL in the Powerbuilder IDE
Here is an example of a datawindow SQL select statement.
Example - SQL Select
The select statement is at the heart of SQL DML (Data Manipulatuion Language)
SELECT "fdlvdet"."season",
"fdlvdet"."pool_code",
"fdlvdet"."dist_code",
"fdlvdet"."memnum",
"fdlvdet"."mar_code",
"fdlvdet"."ctn_code",
"fdlvdet"."fclass",
"fdlvdet"."fcount",
"fdlvdet"."num_cartons",
"fdlvdet"."wt_kg"
FROM "fdlvdet"
WHERE ( "fdlvdet"."season" = :arg_seas ) AND
( "fdlvdet"."pool_code" = :arg_pool )
The above example is a straight forward select statement using two retrieval arguments.
SQL Tutorial - Create Tables
Example - SQL DDL
DDL (data definition language) is used to create and modify table structure and data. For example the create table statement is used to create a new database table.
Consider the following SQL script which creates a customer financial table.
CREATE TABLE "dba"."cusfin"
("cuscode" char(6) NOT NULL DEFAULT NULL,
"currbal" numeric(12,2) DEFAULT NULL,
"days_0" numeric(12,2) DEFAULT NULL,
"days_30" numeric(12,2) DEFAULT NULL,
"days_60" numeric(12,2) DEFAULT NULL,
"days_90" numeric(12,2) DEFAULT NULL,
"days_120" numeric(12,2) DEFAULT NULL,
"credit_limit" numeric(12,2) DEFAULT NULL,
"credit_stop" char(1) DEFAULT NULL ,
"status" char(1) NOT NULL,
PRIMARY KEY ("cuscode")) ;
You can run script like this in the Interactive SQL window within the Powerbuilder IDE database painter.
________________________________________
SQL Tutorial - Embedded Static SQL
You can place SQL code within your Powerscript code.
Example - Commit and Rollback statements
if dw_1.update()=1 then
commit using sqlca;
dw_1.reset()
dw_1.insertrow(0)
else
rollback using sqlca;
end if
SQL Tutorial - Dynamic SQL
SQL can be executed at run-time (dynamically). There are four formats
Format 1 is appropriate for DDL statements, such as create, drop, insert, grant
Example - Consider the following SQL script - Dynamic SQL Format 1:
________________________________________

string ls_isql

ls_isql="CREATE TABLE dba.cusfin " + &
("cuscode char(6) NOT NULL DEFAULT NULL, " + &
"currbal numeric(12,2) DEFAULT NULL, " + &
"days_0 numeric(12,2) DEFAULT NULL, " + &
"days_30 numeric(12,2) DEFAULT NULL, " + &
"days_60 numeric(12,2) DEFAULT NULL, " + &
"days_90 numeric(12,2) DEFAULT NULL, " + &
"days_120 numeric(12,2) DEFAULT NULL, " + &
"credit_limit numeric(12,2) DEFAULT NULL, " + &
"credit_stop char(1) DEFAULT NULL , " + &
"status char(1) NOT NULL, " + &
"PRIMARY KEY (cuscode)) ; "

Execute immediate :ls_isql using sqlca;

________________________________________
Example : - Dynamic SQL format 2
This format is used when a known input parameter needs to be used. For example:
delete from cusfin where cuscode=ls_code
Here is a sample SQL scipt
string ls_code
ls_code=sle_1.text
prepare SQLSA FROM "delete from cusfin where cuscode=?" using sqlca;
execute SQLSA using :ls_code;
SQLSA is a private Powerbuilder datatype called DynamicStagingArea. It is used to store information about the SQL statement.
________________________________________
Example - Dynamic SQL format 3
This format is used when there is a result set and a known number of input parameters. For example:
select * from cusfin where credit_stop=ls_code
Here is a sample SQL scipt
string ls_result,ls_code,sqlstmt
ls_code=sle_1.text

declare fin_curs dynamic cursor for sqlsa;
sqlstmt="select * from cusfin where credit_stop=?"
prepare SQLSA FROM :sqlstmt;
open dynamic fin_curs using :ls_code;
fetch fin_curs inyo :ls_result;
lb_names.additem(ls_result)
do while SQLCA.SQLCode =0
fetch fin_curs into :ls_result;
lb_names.additem(ls_result)
loop
________________________________________
Example - Dynamic SQL format 4
This format is used when the parameters and result set are not known at design-time.
This format is dealt with in more detail in our Powerbuilder ebook.
________________________________________
Additional examples will be added here on a regular basis, so check back often.

Jumat, 2008 Juni 13

ItemChanged Event in powerbuilder

This event occurs whenever the
data is changed and the current field looses its focus (clicking on other
field or other control, pressing tab, etc.). Use this event to validate the
data and trigger ItemError event, whenever there is an error in the data. This
is one of the frequently used events in the DataWindow control. This event
gives access to the old data (which is retrieved from the database), as well as
the new data (the data changed by the user).

Let's write some validation code
in the event. For example, let's display an error message, if the user enters
an existing product_no, while entering a new record. Actually, database gives
an error message if duplicate product_no is entered, because, product_no is a
primary key for the product_master table. The reason we would like to validate
here is, to display the error message as soon as the user enters a duplicate
product_no, instead of waiting till the whole record is entered and sent to the
database. Which means that we need to check for the existence of the
product_no, as soon as the user presses the tab key in the new record.

This check can be done by using
embedded SQL or with a hidden DataWindow control. We didn't teach embedded SQL
yet, so, let's go with the second method.

Place a new DataWindow control
in the window and name it as dw_product_check. Go to the properties dialog box
for this DataWindow control and assign d_display_product DataWindow object to
this control. In case you don't remember, d_display_product takes an argument
product_no and brings the data for the given product_no. If you don't get any
results for the given product_no, it means that the product_no is not existing.

We find no reason to display
dw_product_check DataWindow control to the user, so, turn off its Visible
property.

Do you remember what we should
do before we do any database operation on this DataWindow control? I am sure
you guessed it by this time! We need to set the transaction object. Write the
following code:





// Object: Window w_product_master

// Event: Open

// Append the following line to the existing code.

dw_product_check.SetTransObject( SQLCA )

Write the following code in the
ItemChanged event for the dw_product DataWindow.





// Object: dw_product in w_product_master Window

// Event: ItemChanged

If This.GetColumnName() = "product_no" And &

( This.GetItemStatus( Row, 0, Primary! ) = New! Or &

This.GetItemStatus( Row, 0, Primary! ) = &

NewModified! ) Then

dw_product_check.Retrieve( Integer( Data ) )

If dw_product_check.RowCount() = 1 Then

MessageBox( "Error", "Product No: '" + Data + &

" ->" + &

dw_product_check.GetItemString(Row, &

"product_description") +&

"' already exists.", StopSign!, OK!,1)

Return 1

End If

End If

This code works fine here,
because, product_description column is defined as NOT NULL in the database,
meaning product_description column always contain something and it is never
null. If the column allows NULL value, and if the content of the
product_description for that product is NULL, then the MessageBox() will never
display, because you are sending a NULL value as the parameter to the
MessageBox(). If you want to learn about how the MessageBox() function behaves
when the message text is NULL value, Place a CommandButton in w_script_practice
and write string ls_null; MessageBox( "Test", ls_null), run that
window and click on that button. You will never see that message box being
popped up.) If that is the case, call GetItemString() on a separate line and
check for the NULL value, substitute with appropriate message and then display
it. For ex:





String l_desc1

l_desc1 = dw_product_check.GetItemString( Row, &

"product_description" )

if IsNull( l_desc1 ) Then l_desc1 = "Not Defined."

MessageBox( "Error", "Product No: '" + l_desc1 + &

"' already exists, StopSign!, OK!,1)

The first IF statement is
checking for two things. One, whether the user is tabbing out of product_no
column or any other column. We are interested in it only if the user is leaving
product_no column. GetColumnName() returns the current column name. Just FYI,
GetColumn() returns the column number.

As you have learned in the
"DataWindow Buffers" section, a row can be in one of the four
statuses. New!, NewModified!, NotModified!, DataModified!.
Typically, user might enter the product_no, since it is the first column in the
DataWindow. When the row is inserted, it is in the New! status and remains in
the New! status till the user presses the tab and the ItemChanged event
completes the script execution successfully. That's why we check for the New!
status.

Sometimes, user might enter data
in the new record and navigate to other records and come back to change the
product_no. For that reason, we need to check for the NewModified!
status.

GetItemStatus() gives the status
of the specified column, in the specified buffer. If you specify the column
number as 0 (zero), it returns the row status, instead of the column status.
The third argument is the buffer name. Here, we are interested in the Primary
buffer.

PowerBuilder sends the data
entered by the user as parameters to this event in the Data variable. Remember
that it is always in the string format. We need to convert into appropriate
formats. The next line is bringing data from the database. Observe the datatype
conversion of the Retrieve() function parameter. RowCount() function returns
the number of rows in the specified DataWindow. If the row count is greater
than zero, it means that the given product is existing in the product_master.
(Here, we are checking for 1 because, the product_no in product_master table is
unique, meaning product_master would never have multiple records for a given
product_no. Checking for >0 instead of = 1 will also serve the purpose; It
is useful when you are expecting one or more rows.) Then we are displaying the
error information by using the MessageBox() function.

The last line Return 1 is very
important. This statement tells PowerBuilder, what it has to do after
completing the script execution.

PrintPage Event in powerbuilder

This event occurs for each page
printed (just before the page gets printed. Please note that,
RetrieveRow event occurs after the row is retrieved by PowerBuilder, but
before it displays on the screen). This event gets PageNumber that will be
printed as well as the number of the copy, Copy as parameters to this event.





// Object: DataWindow dw_product in w_product_master window

// Event: PrintPage

w_mdi_frame.SetMicroHelp( "Printing Page # " + &

String( PageNumber) )

In fact, you need not code the
above script, because, when you call DataWindow's Print() function with TRUE as
an argument, PowerBuilder automatically displays the page number being printed
and also allows you to cancel if needed.

If you want to skip printing a
particular page, return 1 as the return value from this event

PrintStart Event in powerbuilder

This event occurs as soon as
PowerBuilder starts printing. The total number of pages that are going to
print, PagesMax, is passed as parameter to this event. You might want to ask
the user whether the printer has that many papers in the tray or not. However,
you can’t stop printing from this event programmatically, instead you need to
code in the PrintPage event.





// Object: DataWindow dw_product in w_product_master window

// Event: PrintStart

w_mdi_frame.SetMicroHelp( "Starting to Print…" )

Kamis, 2008 Juni 12

RetrieveEnd Event in Powerbuilder

This event triggers as soon as
the last row of the result set is retrieved. The total number of retrieved rows
is passed to this event as a event parameter, RowCount.





// Object: DataWindow "dw_query" in w_product_master window
// Event: RetrieveEnd
w_mdi_frame.SetMicroHelp( "Rows Retrieved: " + &
 String( RowCount ) + &
 ". Query execution complete.")

RetrieveRow Event in Powerbuilder

This event triggers immediately
after retrieving each row and before displaying it on the screen. This is the
right place to write the code, if you want to do some process on the row,
before the user sees the row on the screen.

Writing code in this event and
setting the Asynchronous option of the transaction object to TRUE, makes
PowerBuilder to display the row on the screen, as soon as the code in this
event is executed. For example, say the result set of a query has 10,000 rows.
User will see the data on the screen only when PowerBuilder completes
retrieving 10,000th row*. By writing code in this event (even a
single line comment), user will see the data on the screen as soon as the first
row is received by PowerBuilder (of course, code for this event is executed
before it is displayed on the screen).

By writing code or comment
increases the total time required to retrieve the result set. To reduce the
time, you might want to write the code in the RetrieveEnd (explained later),
which is executed after retrieving all the rows in the result set. Writing code
for the RetrieveRow or RetrieveEnd depends on the application requirement.

* There is an exception to this
behavior. If RetrieveAsNeeded option is set, PowerBuilder displays data as soon
it completes retrieving one screen full of data. Whenever user tries to scroll
down the screen, another screen full of data will be retrieved. PowerBuilder,
depending on the DataWindow control size at run-time determines the number of
rows that fit on a single screen.

Another exception is, you should
not use aggregate functions—such as Sum(), Avg(), Min(), Max(), Count()—etc in
the DataWindow and should not set the sort criteria in the DataWindow. In this
situation, PowerBuilder needs the full result set in order to calculate the
aggregate functions or to sort the data.

The following code displays the
number rows retrieved on the status bar like a counter.





// Object: DataWindow dw_query in w_product_master window
// Event: RetrieveRow
w_mdi_frame.SetMicroHelp( "Rows Retrieved: " + &
 String( row ) )

RetrieveStart Event in powerbuilder

This event triggers as soon as
PowerBuilder gets notification from the database saying "I processed your
query and this is the result set you are waiting for". Upon successful
execution of this event’s code, PowerBuilder starts retrieving the result set.

This event has no parameters and
returns zero by default meaning ‘continue processing’. For any reason if you do
not want to retrieve the result set, then return 1 from this event.

By default, each time you call
Retrieve() function, PowerBuilder clears all the existing data in the
DataWindow, i.e., previously retrieved data, any changes you have done to the
retrieved data, and new rows you inserted after previous Retrieve()and
populates the DataWindow with the results of Retrieve() function. For any
reason, if you want to keep the existing data in the DataWindow and want to
append the new result set, just return 2 from this event.

The following code displays a
message on the status bar, just before PowerBuilder starts retrieving the data.
To understand the behavior of return value, you may want to append RETURN
statement with different return values as explained earlier to the following
code and execute it.

// Object: DataWindow dw_query in w_product_master window
// Event: RetrieveStart
w_mdi_frame.SetMicroHelp( "Starting to Retrieve Rows…" )

Menjejaki jalannya aplikasi pada powerbuilder

Pada powerbuilder anda dapat menjejaki jalannya aplikasi(tracing) yang telah di compile (berbentuk EXE) dengan cara menjalankan aplikasi tersebut di ikuti opsi/PBDEBUG.

Nama_aplikasi.exe/PBDEBUG

Setelah itu, anda dapat menjalankan aplikasi seperti biasa. Pada direktori yang bersangkutan, sebuah file dengan extensi DBG (nama aplikasi.dbg) akan terbentuk dan berisi urutan informasi jalannya aplikasi. Cara ini biasanya di lakukan sebagai bagian dari proses debug untuk mencari cacat/kesalahan program.

Rabu, 2008 Juni 11

Mengirim penekanan tombol pada powerbuilder

Kita dapat membuat seolah – olah terjadi penekanan tombol pada sebah kontrol.

Deklarasikan modul win32 API pada declareà local external functions

Subroutine keybd_event( int bvk, int bscan, int dwflags, int dwextrainfo) library “user32.dll”

Berikut ini adalah contoh program untuk mestimulasikan penekananan tombol A

Integer li_vkey

Li_vkey = 65 //karakter “A”

Sle_1.setfocus() //kontrol yang di tuju

Keybd_event(li_vkey,1,0,0)

Contoh lainnya, adalah untuk mensimulasikan penekanan tombol backspace.

Integer li_vkey

Integer li_pos

Ll_pos = len(sle_1.text) + 1

Sle_1.selecttext(ll_pos,0)

//posisi kursor di akhir teks

Li_vkey = asc(“-b”) //backspace

Keybd_event(li_vkey, 1, 0, 0)

Mengirim karakter heksadesimal ke printer dengan powerbuilder

Untuk menggunakan printer tipe tertentu atau untuk tujuan khusus, anda perlu mengirim kode-kode heksadesimal ke printer tersebut untuk langsung mengontrolnya dan mem-bypass printer driver. Pada contoh di bawah ini diperlihatkan bagaimana hal itu bisa di lakukan.

Int li_job

Li_job = printopen()

//print mode

Printtext(li_lob,”~hiB-h21-001”, 0, 0)

//spacing

Printtext(li_job,”~h1B-h33~001”,0,0)

Printclose(li_job)

Membuka internet browser dari aplikasi dengan powerbuilder

Dari aplikasi powerbuilder, anda dapat membuka program internet browser(default) sekaligus membuka halaman web sesuai yang anda inginkan.

Inet linet_base

Getcontentservice(“internet”,linet_base)

Linet_base.hyperlinktourl(http://belajar-powerbuilder.blogspot.com)

If isvalid(linet_base) then destroy linet_base

Senin, 2008 Juni 09

Membuat fungsi terbilang di powerbuilder

Dalam pembuatan aplikasi database perkantoran menggunakan powerbuilder, kita membutuhkan suatu fungsi untuk menampilkan angka dalam faktur atau kwitansi menjadi huruf. berikut ini contoh script pada user function di powerbuilder.

/* Konversi Bilangan Ke Huruf */
string s_bul,bulat,s_sen
Integer ee=0,dawa=0

s_bul = String(ado_angka)
dawa = len(s_bul)

For ee = (dawa - 3) To dawa
If Mid(s_bul,ee,1) = '.' Then
bulat = Mid(s_bul,1,ee)
s_sen = Mid(s_bul,ee + 1,2)
Exit
Else
bulat = s_bul
SetNull(s_sen)
End If
Next

/* Untuk Konversi Bilangan Bulat */
String huruf[15],nilai[15],tampilan,ket[15],ket2[15],kt
Integer angka[15],panjang=0,i=1,i2=1,k=1,k2=0,aa
Double ang
ang = Double(bulat)
panjang = len(string(ang))
k=panjang
i=panjang

do while 1 <= i angka[i2]=integer(Mid(string(ang),i,1))
aa=angka[i2]
// huruf[i2]=wf_rubah(aa)

choose case aa
case 1
huruf[i2]=" Satu"
case 2
huruf[i2]=" Dua"
case 3
huruf[i2]=" Tiga"
case 4
huruf[i2]=" Empat"
case 5
huruf[i2]=" Lima"
case 6
huruf[i2]=" Enam"
case 7
huruf[i2]=" Tujuh"
case 8
huruf[i2]=" Delapan"
case 9
huruf[i2]=" Sembilan"
case else
huruf[i2]=""
end choose

i2 ++
i --
loop
k=1
i=panjang
DO WHILE k <= panjang choose case k
case 1,4,7,10,13
if panjang > k then
if angka[k]=0 and angka[k +1]=0 and angka[K +2]=0 then
kt = "0"
else
kt="1"
end if
else
kt="2"
end if
if k=13 then
ket2[k]=" Triliun "
if kt="0" then
ket[k]=""
else
ket[k]=huruf[k]+" Triliun "
end if
elseif k=1 then
if kt="0" then
ket[k]=""
else
ket[k]=huruf[k]
end if
elseif k=4 then
ket2[k]=" Ribu"
if kt="0" then
ket[k]=""
elseif kt="2" then
if angka[k] = 1 then
ket[k]=" Seribu"
else
ket[k]=huruf[k] + " Ribu "
end if
else
if angka[k + 1]<>1 then
ket[k]= huruf[k]+" Ribu "
else
ket[k]= " Ribu "
end if
end if
elseif k=7 then
ket2[k]=" Juta "
if kt="0" then
ket[k]=""
else
ket[k]=huruf[k]+" Juta "
end if
elseif k=10 then
ket2[k]=" Miliar "
if kt="0" then
ket[k]=""
else
ket[k]=huruf[k]+" Miliar "
end if
end if
case 2,5,8,11,14
if angka[k]=1 then
ket[k - 1]=""+ket2[k - 1]
if angka[k -1]=0 then
ket[k]=" Sepuluh"
elseif angka[k - 1]=1 then
ket[k]=" Sebelas"
else
ket[k]=huruf[k -1]+" belas"
end if
elseif angka[k]=0 then
ket[k]=""
else
ket[k]=huruf[k]+" Puluh"
end if
case 3,6,9,12
if angka[k]=1 then
ket[k]=" Seratus "
elseif angka[k]=0 then
ket[k]=""
else
ket[k]=huruf[k]+" Ratus "
end if
END CHOOSE
k ++
loop
i=panjang
i2=1
do while 1 <= i
tampilan=tampilan+ket[i]
i2 ++
i --
loop
String nilai_rp
nilai_rp = tampilan
return nilai_rp

Mengambil format tanggal dari Registry

Dengan mengambil sebuah nilai dari registry windows, kita bisa mengetahui format tanggal pada sebuah komputer. format ini selanjutnya bisa anda gunakan untuk menampilkan tanggal sesuai dengan setting pada komputer pengguna.

string ls_shortdate

RegistryGet("HKEY_CURRENT_USER\Control Panel\International","sShortDate", ls_shortdate)

messagebox("ShortDate",ls_shortdate)

Mengembalikan nilai saat aplikasi ditutup

Jika sebuah aplikasi powerbuilder di jalankan dari program lain atau shell script, programmer biasanya menginginkan sebuah nilai di kembalikan (return code) saat aplikasi tersebut ditutup untuk mengetahui status/hasil pengeksekusiannya.

Untuk mengembalikan
sebuah nilai, set property message.longparm pada event close dari sebuah objek application dengan nilai yang bersesuaian.

if ib_endingOk then
message.longparm = 1
else
message.longparm = 0
enf if

Kamis, 2008 Juni 05

A Location Tracking System Using PowerBuilder, a GPS Receiver, and Microsoft MapPoint

Did you ever wonder where your kids are driving around on a Saturday night? Perhaps you are a business owner and need to know where your workforce is located on different job sites. With the help of PowerBuilder, a wireless connection, a GPS receiver, and Microsoft MapPoint, you can track the location of any individual in real-time. This article will show you how.

To get things started you will need a few things:

  • PowerBuilder
  • A serial-based GPS receiver (Bluetooth will do)
  • MS MapPoint 2004
  • MS Visual C# (for sockets implementation)
  • A static IP address or a router capable of port forwarding
  • A wireless connection for the client
  • Some time to learn!
Introduction to Client/Server Programming
First, an introduction to socket-based client/server programming: In this project we will write two components. One will be a server to listen for requests on port 1968. The other will be a client that will read information from the GPS receiver and send it to the server. The message the server will receive will be the client's current latitude and longitude. It will then use this information to plot the current location on a map using MS MapPoint.

You may be wondering why it is necessary to use port 1968. The answer is simple: It is not a well-known port, so we are free to use any number we like provided it is less than 2^8. In this case, I choose 1968 because it was the year I was born, but you can choose any number you like.

Determining Your IP Address
The next part of the equation is to get the IP address of the server. One way you can do this is to visit a site called http://whatsmyipaddress.com. This will give you the IP address the outside world uses to communicate with your computer. However, unless you have a static IP address, your server machine will have been assigned a local address by your router that is not accessible to the outside world. To get around this, you will need to set up port forwarding on the router. For example, if I login to my Linksys router, which is usually at 192.168.1.1, I can choose the applications and gaming tab to specify that I would like all requests to port 1968 to go to the machine my server is listening on. And how to do I know that address? Just use ipconfig at a command line. Here is an example so you get the idea.

Writing the Server
The server component is responsible for listening for requests from the client. In order to "listen" for requests, you will need two pieces of information: the IP address of the server machine and the port you wish to listen on. The IP address can be obtained by running ipconfig at a command line, while the port can be anything you want as long as it is not well known, and is less than 2^8. Note, in the example, I have hard-coded in the IP address and port number. However, in a production implementation, you probably want these values to be user-configurable.

To implement our TCP client/server sockets, we have a couple of choices. One is to use the Win32 API; the other is to create a .NET assembly. In this case, I chose the latter. In essence, what we are doing is calling a .NET assembly from PowerBuilder using COM wrappers. What this allows us to do is to create a class using C#, for example, and create a DLL that we can call from PowerBuilder. Wondering how to do that? See Bruce Armstrong's article at http://pbdj.sys-con.com/read/258395.htm for details. However, in terms of the assembly, I will share my C# code with you in Listing 1 but defer the details of creating the assembly to his article.

You may wonder at this point, now that you have created a .NET assembly, how you use it in PowerBuilder. The following code listing shows how I am calling my assembly from PowerBuilder from a ue_postopen event. Notice that I have hard-coded in the IP address and port number. In a production version you would want these to be user-configurable options.

Integer li_Return

ole_map.object.Units = 1
ioo_map = ole_map.object.NewMap(1)

ioo_listner = CREATE OleObject
li_Return = ioo_Listner.ConnectToNewObject( "DotNetListner.DotNetSocketListner" )

ioo_listner.ipAddr = "192.168.1.108";
ioo_listner.portNumber = 1968;

ioo_listner.Initialize();

Once we have initialized our "socket listener," we will need to retrieve data from the client. We will also need some agreement as to the protocol for the messages it accepts from the client. In this case, I am having the client pass the following information (each separated by a comma): latitude, longitude, user, and current time. For example, here is a sample message that will be received by the server:

42.43608, -89.022857, Chance, 1:22 a.m..

The code for retrieving data from the client is shown below. There are some important points to consider here. One, this is not a multi-threaded application, thus we have the infinite loop. However, I have placed some calls to Yield() to allow for any queued messages to be processed in between data retrievals from the client. Messy, but it works.

Double ldb_lat, ldb_long

do while 1=1
Yield()
ioo_Listner.GetData()
ldb_lat = Double(ioo_Listner.Latitude);
ldb_long = Double(ioo_Listner.Longitude);
Yield()
event post ue_plotdata(ldb_lat, ldb_long)
loop

Finally, once the client has retrieved the current latitude and longitude from the GPS receiver and sent it to the server, we need to plot the current position on the map. There are many mapping solutions out there, from Yahoo! Maps to Google Maps to MS MapPoint. (Figure 1) I choose the latter, primarily because - well, what would happen if your Internet connection went down? There would be no way to make the mapping calls. Given that, the MS MapPoint code for plotting a given latitude and longitude is in Listing 2.

Writing the Client
The client component is responsible for reading information from the GPS receiver and sending it to the server. You will need to decide how often this occurs by programming the timer event. In this case, I chose every five minutes. As far as reading GPS information, this can be done using Win32 API calls that "talk" to the comm port for which the receiver is configured. (Figure 2)

The protocol the receiver uses is called NMEA (for more, see www.gpsinformation.org/dale/nmea.htm). In general, the device sends sentences that describe the current position. Of interest to us is the GPGAA message that includes the current location. NMEA sentences are comma-delimited, so they are prime candidates for easy parsing using an external data window. For this example, I used Ian Thain's NMEA parser, which can be found on Code-Exchange. So, the sequence of events is:

  • Read an NMEA sentence from the GPS receiver
  • Determine if it is GPGAA
  • Convert the data to decimal degrees
  • Send the message described above to the server
First, we need a way to communicate with the GPS server. To do that, we can use some Win32 API calls. Following are their prototypes. Why not just use the built-in PowerBuilder I/O functions such as FileOpen and FileRead? The underlying reason is that PowerBuilder will buffer the data for you (on top of the native buffering that the OS provides). This can be problematic for serial I/O devices.
  • Function Long CreateFile(ref string lpszName, long fdwAccess, long fdwShareMode, long lpsa, long fdwCreate, long fdwAttrsAndFlags, long hTemplateFile) Library "Kernel32.dll"
  • Function Long CloseHandle(Long hObject) Library "Kernel32.dll"
  • Function Long ReadFile(Long hFile, ref string lpBuffer, long nBytesToRead, ref Long & nBytesRead, Long lNull) Library "Kernel32.dll"
  • Function Long WriteFile(Long hFile, ref string lpBuffer, long nBytesToWrite, ref Long & nBytesWritten, LONG lNull) Library "Kernel32.dll"
The next order of business is to see how Ian Thain wrote his object to go about parsing a particular NMEA stream. For that, all we have to do is take a look at the format of an example sentence. Since our code picks off GGA; essential fix data that provide 3D location and accuracy data, we will look at that. A sample is in Listing 3.

What you can see from this example is that the data is comma delimited. This makes it a prime candidate for easy parsing by storing it in an external data window using the ImportString function. To get at the individual elements, we only need to index them by their position in the data window.

To extract each individual sentence out of the stream to be passed for parsing to the NMEA object, I wrote the wf_get_token function. This will extract a string based on a delimiter. For the NMEA stream, that is the newline character. (Figure 3)

Finally, we need a way to send our message to our server. You can write a .NET assembly by following the example that acts as a client in order to send the GPS information to the client.

One final note: If you plan on connecting to a Bluetooth serial port greater than COM9, you will need to use the syntax "\\.\COMx" as the port name. Just substitute the port number you wish to use for x. The cause of the problem is that CreateFile accepts strings "COM1" - "COM9" as names of devices, but rejects those with two or more digits.

That's it! You have now rolled your own location tracker. If you want to improve it, you might consider tracking more than one device at a time. This could easily be managed by an external data window that contains the user's name and last position. When a new position comes in, simply do a lookup on the user name, update the data window, and plot the new location on the map. Additionally, all the hard-coded references to ports and IP addresses would well be served by user configuration files.

Senin, 2008 Juni 02

How to repair MDF files not detached from SQL Server

If you have an mdf file that was not properly detached from SQL Server (possibly due to a hard drive crash), then you may need to repair the mdf before you are able to attach the database. The following are instructions on how to repair the mdf file. Replace the filenames with your filename !!!
  1. Make sure you have a copy of eshadata.MDF (or gendata.mdf)
  2. Create a new database called fake (default file locations)
  3. Stop SQL Service
  4. Delete the fake_Data.MDF and copy eshadata.MDF (or gendata.mdf) to where fake_Data.MDF used to be and rename the file to fake_Data.MDF
  5. Start SQL Service
  6. Database fake will appear as suspect in EM
  7. Open Query Analyser and in master database run the following :
    sp_configure ´allow updates´,1
    go
    reconfigure with override
    go
    update sysdatabases set
    status=-32768 where dbid=DB_ID(´fake´)
    go
    sp_configure ´allow updates´,0
    go
    reconfigure with override
    go
    This will put the database in emergency recovery mode
  8. Stop SQL Service
  9. Delete the fake_Log.LDF file
  10. Restart SQL Service
  11. In QA run the following (with correct path for log)
    dbcc rebuild_log(´fake´,´h:\fake_log.ldf´)
    go
    dbcc checkdb(´fake´) -- to check for errors
    go
  12. Now we need to rename the files, run the following (make sure there are no connections to it) in Query Analyser (At this stage you can actually access the database so you could use DTS or bcp to move the data to another database .)
    use master
    go

    sp_helpdb ´fake´
    go

    /* Make a note of the names of the files , you will need them in the next bit of the script to replace datafilename and logfilename - it might be that they have the right names */

    sp_renamedb ´fake´,´eshadata´
    go

    alter database eshadata
    MODIFY FILE(NAME=´datafilename´, NEWNAME = ´eshadata´)
    go

    alter database eshadata
    MODIFY FILE(NAME=´logfilename´, NEWNAME = ´eshadata_Log´)
    go

    dbcc checkdb(´eshadata´)
    go

    sp_dboption ´eshadata´,´dbo use only´,´false´
    go

    use eshadata
    go

    sp_updatestats
    go
  13. You should now have a working database. However the log file will be small so it will be worth increasing its size. Unfortunately your files will be called fake_Data.MDF and fake_Log.LDF but you can get round this by detaching the database properly and then renaming the files and reattaching it.
    Run the following in QA
    sp_detach_db eshadata

    --now rename the files then reattach

    sp_attach_db ´eshadata´,´h:\dvd.mdf´,´h:\DVD.ldf´

Sending Key Press

With this tips, you can make a control likely pressing (a) key(s). First declare the Win32 API modul on Declare -> Local External Functions

Subroutine keybd_event( int bVk, int bScan, int dwFlags, int dwExtraInfo) Library "user32.dll"

Next is an example to simulate pressing "A" key
integer li_vkey
li_vkey = 65 // Character A
sle_1.setfocus() // the desired control to view
keybd_event( li_vkey, 1, 0, 0)

Another example to simulate "Backspace" key
integer li_vkey
integer li_pos
li_pos = len(sle_1.Text) + 1
sle_1.selectText(li_pos, 0)// Cursor position on last text
li_vkey = asc ("~b") // backspace
keybd_event( li_vkey, 1, 0, 0)

Getting Computer Name

You can get the computer name from within the application. Declare the Win32 API modul on Declare -> Local External Functions

Function boolean GetComputerNameA (ref string lpBuffer, ref ulong nSize) Library "kernel32.dll"

Now write this code:
string ls_temp
ulong lul_value
boolean lb_rc

lul_value = 255
ls_temp = space(255)
lb_rc = GetComputerNameA (ls_temp, lul_value)
messageBox ('Computer Name', ls_temp, information!)

Getting Active Directory

With this simple trick you can get the current active directory. Declare the Win32 API modul on Declare -> Local External Functions

Function boolean GetCurrentDirectoryA (long nBufferLength, ref string lpBuffer) Library "kernel32.dll"

Write this code:
string ls_temp
ulong lul_value
boolean lb_rc

lul_value = 255
ls_temp = space(255)
lb_rc = GetCurrentDirectoryA (lul_value, ls_temp)
If lb_rc Then MessageBox('Current Directory', ls_temp, information!)

Mapping a Network Drive

This tip show how to use the Window API for mapping a network drive from PowerBuilder.
Function Declaration:

FUNCTION ulong WNetUseConnectionA (ulong hwndOwner, &
REF s_netresource lpNetResource, string lpPassword,
string lpUsername, ulong dwFlags, REF string lpAccessName, &
REF ulong lpBufferSize, REF ulong lpResult) library "mpr.dll"

Structure Definition:
$PBExportHeader$s_netresource.srs
global type s_netresource from structure
unsignedlong dwScope
unsignedlong dwType
unsignedlong dwDisplayType
unsignedlong dwUsage
string lpLocalName
string lpRemoteName
string lpComment
string lpProvider
end type

Mapping Code:
CONSTANT ulong NO_ERROR = 0
CONSTANT ulong CONNECT_REDIRECT = 128
CONSTANT ulong RESOURCETYPE_DISK = 1

s_netresource lstr_netresource

String ls_null
String ls_buffer
String ls_MappedDrive

uLong ll_bufferlen
uLong ll_null
uLong ll_ErrInfo
uLong ll_success

SetNull(ll_null)
SetNull(ls_null)

ls_buffer = Space(32)
ll_bufferlen = Len(ls_buffer)

lstr_netresource.dwType = RESOURCETYPE_DISK
lstr_netresource.lpLocalName = ls_null
lstr_netresource.lpRemoteName = "UNC resource name here"
lstr_netresource.lpProvider = ls_null

ll_ErrInfo = WNetUseConnectionA(ll_null, lstr_netresource, &
'password', 'username', &
CONNECT_REDIRECT, ls_buffer, ll_bufferlen, ll_success)

IF ll_ErrInfo = NO_ERROR THEN
MessageBox("Drive Mapped", "Drive Letter is " + ls_buffer)
Return 1
ELSE
MessageBox("Mapping Falied", "Error is " + String(ll_ErrInfo))
Return -1
END IF

Get Volume Information

In order to show a list of Drives We needed to get the name of the Drive. The following API call gets the volume name as well as the serial number.

Create an NVO add the following local external function:

function long GetVolumeInformationA( ref string ls_RootPath, &
ref string ls_VolName, long ll_VolLen, ref string ls_volserial, &
long ll_maxcomplen, long ll_systemflags, &
ref string ls_SystemName,&
long ll_SystemLen ) Library 'kernel32'

Then add a function called GetVolumeName which accepts a string and returns a string and add the following code:
// Call the API function to get the volume
// label from a drive letter
String ls_Volume
String ls_Drive, ls_FileSys, ls_Flags, ls_Serial
Long ll_Max, ll_Flags, ll_RC, ll_FileSys
Long ll_Volume

ls_Drive = as_Volume
ls_Volume = Space(32)
ls_FileSys = Space(32)
ls_Serial = Space(32)
ll_Volume = Len( ls_Volume )
ll_FileSys = Len( ls_Filesys )

ll_RC = GetVolumeInformationA( ls_Drive, ls_Volume, ll_Volume, &
ls_Serial, ll_Max, ll_Flags, ls_FileSys, ll_FileSys )

IF ll_RC = 0 THEN
ls_Volume = ''
ELSE
ls_Volume = Trim( ls_Volume )
END IF

RETURN ls_Volume

Call the function passing the drive you want the volume name for. For example C:\

Get The Name of a Network Share

Using the Get Volume API tip previously posted will return the names of the drives connected to your machine. Unfortunately for network drives this returns the name of the physical drive not the name of the share you have mapped to. If you have many shares mapped to a single physical network drive you will not be able to distinguish between the drives.

To improve this situation you can use an API call to return the name of the network share. This is more meaningful and easier for the user. The API call you will need to use is WNetGetConnectionA. To use the API call declare the local external function as follows:

function ulong WNetGetConnectionA( string szLocalPath, &
ref string szNameBuffer, ref ulong lpBufferSize) Library "mpr.dll"

Then in your script you can call the API as follows:
Ulong lul_Max
Long ll_RC
String ls_Drive, ls_Volume

// hard coded for the f: drive
ls_Drive = 'f:'
lul_Max = 2000

// pre allocate string to stop GPF
ls_Volume = Space( 2000 )

// call the API function, the share name
// will be in the ls_Volume variable
ll_RC = WNetGetConnectionA( ls_Drive, ls_Volume, lul_Max )
ls_Volume = Trim( ls_Volume )

Registering OCX Components in an Exe

Having problems with OCX's which work fine in development and EXE on your machine but when you ship the EXE to a users machine and install your program the OCX does not work?

The problem is that the way OCX's are designed to work is that every OCX should attempt on its own to register with the system during its constructor event. This is done by the container calling a function that is in EVERY OCX called DLLRegisterServer.

The problem is that PowerBuilder does not call this function. Even if you run the REGSRV utility, the OCX will fail to register itself. To correct this problem, in object where you are using the OCX you need to goto the constructor event and call the OCX DLLRegesterServer function.

Declare a local external function in the container object

Function long DllRegisterServer() Library "ocxname.OCX"

In the constructor event

LONG ll_RC
ll_RC = DllRegisterServer()

C/C++ Datatype Conversion

If you want to use the Win32 API within PowerBuilder, you have to know the prototype modul. The following list is not an exhaustive list of all C and C++ to PowerBuilder Datatype but most of the common Datatype that I've used over the years of interfacing PowerBuilder and C/C++ programs. There are two lists, the first is PowerBuilder to C++ Datatype, useful for the C Proxy Generator. Then there is a list of C/C++ to PowerBuilder Datatype useful for people writing DLL's and the like:

PowerBuilder DatatypeC/C++ Datatype
BlobPBBlob
Booleanint
Characterchar
DatePBDate
DateTimePBDateTime
DecimalPBDecimal
Doubledouble
Integerint
Realfloat
StringPBString
TimePBTime
UnSignedIntegerunsigned int

C/C++ DatatypePowerBuilder Datatype
BOOLBoolean
WORDUnSignedInteger
DWORDUnSignedLong
HANDLEUnSignedLong
HWNDUnSignedLong
LPSTRString Ref
LPBYTEString Ref
LPINTLong Ref
charBlob {1}
intInteger
unsigned intUnsignedInt
longLong
ULONG/unsigned longUnsignedLong
doubleDouble
char *String Ref

Hiding Desktop and Taskbar

With this tips you can easily hide the desktop and taskbar from the user

1. Declare win32 API module on Declare -> Local External FUnctions

Function Long FindWindowEx (Long hwnd1, Long hwnd2, String lpsz1, String lpsz2) Library "user32" Alias For "FindWindowExA"
Function Long ShowWindow (Long hwnd, Long nCmdShow) Library "user32" Alias For "ShowWindow"


2. Declare constants below on Declare -> Instance Variables
Constant Long SW_HIDE = 0
Constant Long SW_NORMAL = 1
Constant Long SW_SHOWMINIMIZED = 2
Constant Long SW_SHOWMAXIMIZED = 3
Constant Long SW_SHOWNOACTIVATE = 4
Constant Long SW_SHOW = 5
Constant Long SW_MINIMIZE = 6
Constant Long SW_SHOWMINNOACTIVE = 7
Constant Long SW_SHOWNA = 8
Constant Long SW_RESTORE = 9
Constant Long SW_SHOWDEFAULT = 10


3. Type the example codes below to hide the desktop and taskbar
String ls_ShellViewWnd = "Progman"
String ls_ShellTaskBarWnd = "Shell_TrayWnd"
String ls_Null
Long ll_HTaskBar, ll_HDeskTop

setNull (ls_Null)
//Hide TaskBar
ll_HTaskBar = FindWindowEx ( 0, 0, ls_ShellTaskBarWnd, ls_Null)
ShowWindow ( ll_HTaskBar, SW_HIDE )
//Hide Desktop
ll_HDeskTop = FindWindowEx ( 0, 0, ls_ShellViewWnd, ls_Null )
ShowWindow ( ll_HDeskTop, SW_HIDE )


To show up the desktop and taskbar again, use codes below
//Show Task Bar
ll_HTaskBar = FindWindowEx ( 0, 0, ls_ShellTaskBarWnd, ls_Null)
ShowWindow ( ll_HTaskBar, SW_SHOW )
//Show Desktop
ll_HDeskTop = FindWindowEx ( 0, 0, ls_ShellViewWnd, ls_Null )
ShowWindow ( ll_HDeskTop, SW_SHOW )

Hiding Application on Windows Taskbar

With this simple tips you can hide the application on Windows Taskbar even if its running
1. Declare Win32 API module on Declare -> Local External Functions:

FUNCTION boolean SetWindowLongA (ulong hWnd, int nIndex, long newValue) Library "user32.dll"


2. Add below codes on Open event on main window
IF NOT SetWindowLongA(Handle(This), -20,128) THEN
messageBox ('Error','Cannot Hide!')
END IF


In order the code to take effect you should compile your application into EXE first

Run an Application Only Once

In this tips, I would like to discuss how to make an application run only once at a time. When user try to run your application, first try to see if it is already running. If it is already running bring it to top and return. There are some sleek SDK functions you can make use of to implement this. The following are the steps to create this feature:

  1. Create a "Custom Class UserObject"
    Custom Class PowerBuilder UserObject
  2. Set the AutoInstantiate to TRUE
  3. Declare the Win32 API modul at the UserObject on Declare -> Local External Functions
    Function Long GetLastError () Library 'kernel32.dll'
    Function ULong CreateMutex (ULong lpsa, Boolean fInitialOwner, String lpszMutexName) Library 'kernel32.dll' Alias for CreateMutexA
  4. Create a UserObject Function like this:
    Create UserObject Function - isRunning
    String ls_name
    If Handle(GetApplication()) > 0 Then
    ls_name = GetApplication().AppName + Char(0)
    CreateMutex(0, True, ls_name)
    If GetLastError() = 183 Then Return True
    End If
    Return False
  5. Save the UserObject as uo_mutex (for example)
Below is how to use the object. Put this code at the beginning of open event from your Application object:
uo_mutex lou_mutex
If lou_mutex.uf_isrunning() Then
MessageBox ('Warning', 'Application is already running', StopSign!)
HALT CLOSE
End If
//...
// Your next line code
//...

Return Code on Application Exit

Sometime when a PowerBuilder application is called by other program or a shell script, programmer want a return code after exiting the PowerBuilder application, just to get the status of the application called.

To give a return code, simply set the Message.LongParm property on the close event of the Application object with the desirable value.

   If ib_endingOK then
Message.LongParm = 1
Else
Message.LongParm = 0
End If

Getting Date Format from Windows Registry

By getting the date format from the Windows registry, we can use it for various need when you build a PowerBuilder application

RegistryGet("HKEY_CURRENT_USER\Control Panel\International","sShortDate", ls_shortdate
messageBox ("ShortDate", ls_shortdate)

Calling an Internet Browser from Application

From PowerBuilder application, we can call (open) the default internet browser, as well as setting the default home page for the browser

Inet linet_base
GetContextService("Internet", linet_base)
linet_base.HyperlinkToURL("http://...")
If IsValid(linet_base) Then Destroy linet_base

Sending Hexadecimal Character to Printer

When using several types of printer for a special purpose, sometime we have to send hexadecimal character to control the printer and to by pass the printer driver.
The example is shown below

int li_job
li_job = PrintOpen()
// Print Mode
PrintText ( li_job, "~h1B~h21~001", 0, 0)
// Spacing
PrintText ( li_job, "~h1B~h33~001", 0, 0)
PrintClose ( li_job )

Center a Response Window

You can centralized your windows with the following code and put it on open event:

LONG   ll_X, ll_Y, ll_XCtr, ll_YCtr
WINDOW lWin

lWin = aw_Window.ParentWindow();

ll_XCtr = lWin.X + ( lWin.Width / 2 );
ll_YCtr = lWin.Y + ( lWin.Height / 2 );

ll_X = ll_XCtr - ( aw_Window.Width / 2 );
ll_Y = ll_YCtr - ( aw_Window.Height / 2 );

IF ll_X < 0 THEN ll_X = 0;
IF ll_Y < 0 THEN ll_Y = 0;

RETURN aw_Window.Move( ll_X, ll_Y )


For MDI child window you can use the following code:
LONG   ll_X, ll_Y, ll_XCtr, ll_YCtr
WINDOW lWin

lWin = aw_Window.ParentWindow();

IF ( ( ( lWin.WindowType = MDI! ) OR ( lWin.WindowType = MDIHelp! ) ) AND &
( aw_Window.WindowType = Child! ) ) THEN
ll_XCtr = lWin.WorkSpaceWidth() / 2;
ll_YCtr = lWin.WorkSpaceHeight() / 2;

IF lWin.WindowType = MDIHelp! THEN
ll_YCtr = ll_YCtr - 40;
END IF

ll_X = ll_XCtr - ( aw_Window.Width / 2 );
ll_Y = ll_YCtr - ( aw_Window.Height / 2 ) - 200;

IF ll_X < 0 THEN ll_X = 0;
IF ll_Y < 0 THEN ll_Y = 0;
ELSE
ll_X = aw_Window.X;
ll_Y = aw_Window.Y;
END IF

RETURN aw_Window.Move( ll_X, ll_Y )

Using Windows Scripting Host

You can use Windows Scripting Host on PowerBuilder for many purpose. On sample code below, you can use it to get the network domain, user name and computer name. To do so, you need Windows Scripting Host Object Model component (wshom.ocx)

integer li_rc
OleObject ole_wsh

ole_wsh = CREATE OleObject
li_rc = ole_wsh.ConnectToNewObject ( "WScript.Network" )
IF li_rc = 0 THEN
MessageBox ("Domain", string( ole_wsh.UserDomain ))
MessageBox ("User", string( ole_wsh.UserName ))
MessageBox ("Computer", string( ole_wsh.ComputerName ))
END IF


With WSH, you can execute other application from PowerBuilder application. On example below, Notepad will be called by clicking a button from PowerBuilder application
integer li_rc
OleObject ole_wsh

ole_wsh = CREATE OleObject
li_rc = ole_wsh.ConnectToNewObject ( "WScript.Shell" )
ole_wsh.Run ("Notepad")
ole_wsh.AppActivate("Untitled - Notepad")
Sleep (500)
ole_wsh.SendKeys (''Hello from PB")


Sleep is a function from Win32 API module with a declaration like this :
Subroutine Sleep (Long dwMilliseconds) Library "kernel32" Alias for "Sleep"


You also can run Calculator program and send some key stroke on it
integer li_rc
OleObject ole_wsh

ole_wsh = CREATE OleObject
li_rc = ole_wsh.ConnectToNewObject ( "WScript.Shell" )
ole_wsh.Run("calc")
ole_wsh.AppActivate ("Calculator", 100)
Sleep (1000)
ole_wsh.SendKeys("1{+}")
Sleep (1000)
ole_wsh.SendKeys("2")
Sleep (1000)
ole_wsh.SendKeys("=")
Sleep (1000)
ole_wsh.SendKeys("*4")
Sleep (1000)
ole_wsh.SendKeys("=")
// 1+2 = ( 3, then) * 4 = 12


You also can run VBScript from PowerBuilder easily. To do that you need Microsoft Script Control component (msscript.ocx)
OleObject ole_wsh
Integer li_rc, i, j, k

ole_wsh = Create OleObject
li_rc = ole_wsh.ConnectToNewObject ("MSScriptControl.ScriptControl")
ole_wsh.language = "vbscript"
ole_wsh.addcode("function retfnc(s) retfnc = s end function")
ole_wsh.executestatement ('if 1 > 1 then msgbox retfnc("true") else msgbox retfnc("false") end if

Tracing on Running Application

On PowerBuilder we can trace the running compiled application (*.exe) by running application with the /PBDEBUG option.

application_name.exe /PBDEBUG

After that, just run your application as usual. On the same directory with the compiled PowerBuilder application, you will find file with an extension *.dbg ( eg. application_name.dbg). Within this file you will find useful orderly manner information . This is just an alternative for debugging the program.

Re-initializing an Unbounded Array

Suppose that you have been filling an array with values. Now you want to reinitialize the array to its original empty state. PowerBuilder doesn’t have an explicit function to help re-initialize the array, so you have to do it manually as described below.

Declare a dummy array of the same type. Never put any values into this array. When you want to re-initialize your "working array", assign it to the value of the empty array. This will clear out the "working" array and make the UpperBound function return the correct value (zero). See sample code below:

string ls_EmptyArray[]
string ls_NameArray[]

ls_NameArray[1] = "James"
... more work ...

// empty out Name Array
ls_NameArray = ls_EmptyArray

Calling Oracle Stored Procs/Functions from PB

Whenever you want to make a call to an Oracle stored procedure or stored function, a good approach is to first declare it as an external function and then invoke it based on that declaration.

A. Declaring an Oracle Stored Procedure so that PowerBuilder Knows About it
This function/procedure declaration is done in the transaction user object. First create transaction object. Select File > New then choose Standard Class under Object Tab. Then select Transaction Type on Select Standard Class Type. Once inside the transaction user object, choose "Declare-Local External Functions" and follow the syntax below.

  1. Stored Procedure (no package)
    The declaration syntax for a stored procedure (on its own, outside package) is:

        SUBROUTINE SubRtnName(args) RPCFUNC


    In example, the declaration passes a string by value (i.e. IN) and a string by reference (i.e. IN OUT or OUT).

        SUBROUTINE CalcAmount(string LS_In1, ref string LS_Out2) RPCFUNC

    Notes:
    • if the procedure is not in a package and does not take any array parameters, then you can click the procedures button to paste in the procedure declaration directly from the database.
    • an optional alias clause can be added to allow PowerBuilder to use a different function name from Oracle (see alias format used with package declarations).
  2. Procedure inside an Oracle package
    The declaration syntax for a stored procedure inside a package is:

        SUBROUTINE SubRtnName(args) RPCFUNC ALIAS FOR "PackageName.ProcName"


    In example, the declaration passes a string by value (i.e. IN) and a string array by reference
    (i.e. IN OUT or OUT).

        SUBROUTINE CalcPenaltyAmt(string LS_In1, ref string LS_Out2[]) RPCFUNC ALIAS FOR "Penalty.P_Calc_Amount"
  3. Stored Function (no package)
    The declaration syntax for a stored function (on its own, outside package) is:

        FUNCTION ReturnType FcnName(args) RPCFUNC


    In example, the declaration passes a string by value (i.e. IN) and a string array by reference (i.e. IN OUT or OUT) and it returns a long.

        FUNCTION long CalcAmount(string LS_In1, ref string LS_Out2[]) RPCFUNC


    Note: the same notes given for stored procedure declarations apply to stored functions.
  4. Function inside an Oracle package
    Function inside an Oracle package

    The declaration syntax for a stored function inside a package is:

        FUNCTION ReturnType FcnName(args) RPCFUNC ALIAS FOR "PackageName.FunctionName"


    In example, the declaration passes a string by value (i.e. IN) and a string array by reference (i.e. IN OUT or OUT) and returns a long.

        FUNCTION long CalcPenaltyAmt(string LS_In1, ref string LS_Out2[])) RPCFUNC ALIAS FOR "Penalty.f_Calc_Amount"

B. Invoking an Oracle Stored Procedure/Function
This is the invocation syntax for a stored procedure/function that has been declared in the transaction object is shown below.
Notes on Variables passed by Reference:
  • Dynamically-sized output variables (i.e. strings and arrays) must be preallocated up to the size needed. When using this invocation method, PowerBuilder does not dynamically allocate the space needed for them.
  • Array Size Limitation: number of array elements times maximum element size cannot exceed 32K.
  1. Invoking a Stored Procedure
    The invocation syntax for a stored procedure is:

        TransactionObjectName.ProcName(args)


    Sample invocation:

    string in_parm1
    string out_parm2

    in_parm1 = "input value"
    out_parm2 = space(50) // preallocating space for string

    SQLCA.CalcAmount(in_parm1, out_parm2)
  2. Invoking a Stored Function (shown using an array variable)
    The invocation syntax is:

        ReturnValue = TransactionObjectName.FcnName(args)


    Sample invocation:

    string in_parm1
    string out_parm2[5] // defining fixed sized array
    long ll_return

    in_parm1 = "input value"

    // preallocating space for 500 chars for whole string array.
    // Each element will effectively be 500 bytes by allocating
    // the first.
    out_parm2[1] = space(500)

    ll_Return = SQLCA.CalcAmount(in_parm1, out_parm2[])

Keeping Column into Array

With this simple tips you can keep all the column name on DataWindow into an array

int colNum, numCols
string colName[]

numCols = Integer(dw_control.Describe("DataWindow.Column.Count"))

FOR colNum = 1 TO numCols //Get Column Name with describe
colName[colNum] = dw_control.Describe("#"+String(colNum) +".name")
NEXT