Facing mortality

George_slusher
I don’t know why my neighbor’s death has affected me so deeply. Maybe it’s because I just turned 60 and I’ve realized that it’s all downhill from here. Or maybe it’s because his death was so sudden and unexpected. Or maybe it’s because I never got a chance to thank him for the lovely and tasty bouquet of lettuce that he left on our doorstep last Wednesday. Or maybe it’s because he was the best neighbor anyone could ask for. George helped us countless times over the past twenty years and never asked for or accepted anything in return.

Whatever the reason, I’m stunned by it and my heart is heavy.

George was not in the best of health, but he never gave up. He had survived a heart attack back in the seventies when treatment was practically nil. The result of that was a badly damaged heart and CHF. Because he was a long term smoker he also had developed COPD. But he coped and he found good doctors at the VA who fine-tuned his medicine. He looked great, and he stayed busy, always working in his garden, his shop or his greenhouse. And he never missed a good fishing day. He may have been retired, but he was always coming up with some new project. He had a very creative and fertile mind.

I last saw George when Connie and I were walking last Wednesday afternoon. He was driving his little pickup down our street, when he saw us he waved and flashed a big smile. I am so grateful that is the last image we have of him. I had wanted to tell him how great his lettuce bouquet idea was and that I thought it would be a big hit at the City Market.
lettuce

George was an artist, he painted and created intricate wood crafts. His cactus planters were amazing. He had perfected a system of growing cactus in his greenhouse, he had it down to a science. And he created these fantastic wooden planters that he artfully arranged the cactus in. They were his best seller at the City Market, he could hardly make enough of them.
Cactus

He painted the logo for our Tharpalooza burning man, and furnished enough wood for an inferno:
George

He loved going to the Farmer’s market at the KC City Market. He sold flowers, vegetables, and his wood crafts. He was amazed by how much city people would pay for his stuff and he thought they would buy anything. To test his hypothesis, he potted a thistle weed that was growing in his garden and sold it as an exotic flower for $25. That’s one of my favorite stories of his.

I loved visiting with George and listening to his stories. He was a great storyteller, a talent that I wish I had. George was active duty Air Force back in the fifties crewing a tanker. That was during the infancy of air refueling, when they were trying to perfect the technique. On one flight his plane went down in the North Atlantic, and he was the only survivor. I can’t imagine being alone in a life preserver in the middle of the frigid ocean and living to tell about it.

Needless to say, he was a tough old bird who had cheated death more than once. I worry that they put him into hospice too soon. I mean, the day after he checked into the VA, after driving himself down there? Did they just give up on him? I don’t know the particulars and the family didn’t know any, but I worry that hospice is sometimes code for euthanasia, just my opinion.

Regardless, I am thankful for twenty years of friendship with George, but my world is going to be a much colder place without him.

APK 3.1.20

I’ve spent much of my ‘free’ time during the last month been pouring over error reports received during the past year. Unfortunately most of the reports are vague and completely untraceable as to their cause. Focusing on the most commonly reported errors I’ve tried to deduce their causes, but I’m likely to be completely off base.

TfrmMain, TfrmPrint: EConvertError

‘Not a valid floating point value’. All of these error reports came from version .18. This was fixed in version .19 by changing all calls to StrToFloat to TryStrToFloat. Come on folks, updates are free, please keep your copies up-to-date.

TfrmMain: Range check error
There were several Range errors which I believe originate in the Topaz database engine (no trace available). I re-compiled Topaz with Range checking turned off, hopefully that will tone down the frequency of these error reports.

TfrmPrint: Multiple errors

Multiple errors were reported originating in the TfrmPrint class. Procedures flagged include: SaveConsult, SaveLevels, SavePatient, ValidPatData, GraphPrep, and ReDrawSerumLevelPlot. Hopefully recompiling Topaz will fix most of the Save errors. But, to add another layer of safety, I’ve added try..try..except..finally blocks to each procedure and changed the SerumLevel array from fixed to dynamic.

TfrmMonitor: No default printer selected
This was reported twice. I cannot imagine how or why someone would not have a default printer selected in Windows. Regardless, I’ve added this tiny bit of code to hopefully catch that condition (unfortunately I have no way of testing this):

1
if (Printer.PrinterIndex > 0)then
2
  begin
3
     // Print the consult
4
  end;

TfrmPKPD: Division by Zero
The function AUC was flagged in one report. I cannot imagine how this procedure gets called without valid parameters, but, once again, I added try..try..except..finally block that will prevent the program from crashing.

TfrmLevels; EConvertError
‘is not a valid integer value.’ Sounding like a broken record, I simply cannot imagine how this procedure gets called without valid parameters, regardless I changed StrToInt to TryStrToInt in hopes of fixing this impossible error.

TfrmMain: Pack date invalid
This error came up twice, with no traceable cause. Because the user is able to change date settings in Windows at will, I assumed an incompatible date format is causing the issue. I added some code to force the short date format into the standard USA: ‘MM/dd/yyyy’. Originally I had declared the OutputBuffer within the var block of the function, but that was causing a memory leak on exit, even after adding a StrDispose in the cleanup (finally) block:

01
procedure TfrmMain.SavePackDate();
02
var
03
   APKini  : TIniFile;
04
   strDateFormat : string;
05
   strTemp : string;
06
   OutputBuffer: PChar;
07
   SelectedLCID: LCID;
08
begin
09
   try
10
       // Determine local short date format
11
       OutputBuffer := StrAlloc(255);
12
       SelectedLCID := GetUserDefaultLCID;
13
       GetLocaleInfo(SelectedLCID, LOCALE_SSHORTDATE, OutputBuffer, 255);
14
       strTemp := string(OutputBuffer);
15
       // if first letter is d then set to 'dd/MM/yyyy' else 'MM/dd/yyyy'
16
       if Copy(strTemp, 1, 1) = 'd' then
17
          strDateFormat := 'dd/MM/yyyy'
18
       else
19
          strDateFormat := 'MM/dd/yyyy';
20
       try
21
          APKini := TIniFile.Create(PathName + 'apk.ini');
22
          try
23
             APKini.WriteString('dbMaint', 'Pack date', FormatDateTime(strDateFormat, Now));
24
          finally
25
             APKini.Free;
26
          end;
27
       except
28
          on E:Exception do begin
29
          MessageDlg('Error saving pack date',
30
                      E.Message,
31
                      mtError, [mbOK], 690, dckActiveForm);
32
          end;
33
       end;
34
   finally
35
      // Free up the string list memory
36
      StrDispose(OutputBuffer);
37
   end;
38
end;

After I moved the code into an inline function there were no more memory leaks:

01
procedure TfrmMain.SavePackDate();
02
var
03
   APKini  : TIniFile;
04
   strDateFormat : string;
05
   strTemp : string;
06
   function ShortDateFormat: string;
07
   var
08
      OutputBuffer: array[0..255] of Char;
09
      SelectedLCID: LCID; 
10
   begin
11
      SelectedLCID := GetUserDefaultLCID;
12
      GetLocaleInfo(SelectedLCID, LOCALE_SSHORTDATE, OutputBuffer, 255);
13
      Result := OutputBuffer;
14
   end;
15
begin
16
   try
17
      // Determine local short date format
18
      strTemp := ShortDateFormat;
19
      // if first letter is d then set to 'dd/MM/yyyy' else 'MM/dd/yyyy'
20
      if Copy(strTemp, 1, 1) = 'd' then
21
         strDateFormat := 'dd/MM/yyyy'
22
      else
23
         strDateFormat := 'MM/dd/yyyy';
24
      APKini := TIniFile.Create(PathName + 'apk.ini');
25
      try
26
          APKini.WriteString('dbMaint', 'Pack date', FormatDateTime(strDateFormat, Now));
27
      finally
28
          APKini.Free;
29
      end;
30
   except
31
      on E:Exception do begin
32
          MessageDlg('Error saving pack date',
33
                      E.Message,
34
                      mtError, [mbOK], 690, dckActiveForm);
35
      end;
36
   end;
37
end;

Population analysis improvements
I did find some time to make several improvements in this version of APK. The main focus of improvements this go round is the population analysis function. Probably the biggest change is removal of the 500 record limit, it now uses dynamic arrays so the number of records that may be analyzed is virtually unlimited. I added text to display the current model for comparison to the population analysis and a regression line of the current Kel or CL calculation.

apk_analysis_report

APK Population Analysis Report

Finally, I added a median Vd calculation, which is surprisingly complex:

01
02
function MedianVd: double;
03
var
04
    n,i,middle : integer;
05
    TempDouble : double;
06
    TempArray  : array of Double;
07
begin
08
    // Set Length of TempArray
09
    SetLength(TempArray,TotalPatients);
10
    // Copy first column of RegressTable array
11
    for i := 0 to TotalForArray do begin
12
       TempArray[i] := RegressTable[i,0];
13
    end;
14
    // use truncated selection sort to find median
15
    middle := (TotalForArray+1) div 2;
16
    for i := 0 to middle do begin
17
       for n := 1 to TotalForArray-i do begin
18
          if TempArray[n] > TempArray[n-1] then
19
             begin
20
                TempDouble := TempArray[n];
21
                TempArray[n] := TempArray[n-1];
22
                TempArray[n-1] := TempDouble
23
             end
24
       end
25
    end;
26
    // find median
27
    try
28
       if odd(high(TempArray)) then
29
          // when high(TempArray) is odd, there are an even number of elements in array.
30
          // define median as average of two middle values.
31
          result := (TempArray[middle] + TempArray[middle-1]) / 2
32
       else
33
          // when high(TempArray) is even, there are an odd number of elements in array.
34
          // median is the middle value.
35
          result := TempArray[middle];
36
    finally
37
       // Free memory used for TempArray
38
       SetLength(TempArray,0);
39
    end;
40
end;

As soon as I finish testing I will release this version into the wild. Enjoy.

And here’s my current favorite song (based on a true story), “Who’s the judge to decide how much this world should punish”:

UD Labels

In 1984, shortly after the pharmacy received a new IBM-PC XT with a green screen monitor and a dot matrix printer, I wrote my first practical pharmacy program: UD Labels.

In those days unit-dose packaging was a time-consuming and messy affair as the labels were printed with a mimeograph machine. It utilized stencils which were typed up on an IBM typewriter with the ribbon removed. The stencils were small and it was nearly impossible to line them up on the typewriter roller.

Before printing the roller had to be inked. Then, depending on how well it was inked, the first run of labels was either too faint or smeared with excess ink. After trial and error, eventually you were able to print labels that were somewhat legible.

The labels were pushed into the printing area with a mechanical arm, and often jammed or misaligned within the loud and complicated Rube Goldberg contraption. If you only needed a few pills for a rarely used drug, the machine was overkill. To save time, we dispensed a few pills into a prescription vial with a typed label. Any remaining pills had to be discarded.

It came to me one day while printing a report from the PC and watching (listening) to that ridiculous packaging machine chug away at labels, that a computer printer would be much easier, and would produce labels that could be easily read.

It took me about 2 days to write a simple little program in IBM BASIC, and it worked like a charm. My regional manager Russ Collins came by for a visit a few weeks later and I showed him what I’d developed. Shortly thereafter I was getting requests from other DOPS in our region for a copy of the program. And thus, my little software publishing enterprise began.

I’ve made countless improvements to the program in the thirty years since. The latest version is secure, has full database capabilities, and has the ability to print bar codes in any format.

A few software companies have since copied my idea, and charge a ridiculous price for their intellectual thievery. Even my current employer was duped into buying one of these ridiculously over-priced and impractical pieces of crapware. I was off one week then came back to find that all traces of my program were erased, they even erased all the old data. I was not advised or consulted. Blatant disrespect that I will certainly never forget.

Which brings us to 2014, thirty years after my brainstorm, UD labels has become the most popular RxKinetics PC software. Karma.

More Power (arr, arr, argh)

Tim “the tool man” approves.

“More models”.. I get this request often, and my usual answer is, in as nice a way as possible, do the work yourself lazy ass. I gave you the tools, learn how to use them.

But most people don’t get it. This sh*t isn’t carved into stone tablets by the hand of God. The way pharmacists are taught these days everything has to be in writing somewhere. So, here it is: Wagner: Fundamentals of Pharmacokinetics, nineteen-f**king-SEVENTY-FIVE. Yup, that’s right, almost FORTY f**king years ago. You weren’t even a gleam in your daddy’s eye. So, look it up old school in an actual lie-bary (sic) because it ain’t on the f**king inter-webs.

I’ve described the Wagner method before, in great detail here . No lie-bary required!

And now I’ve done all that work for you too! All it takes is a suck-up email to appeal to my altruism: “I like your program a lot because it is serious about PK/PD and pushes the concept of PK target attainment. As such, to optimize dosing with Abx is critical and apps to me are the future. I request that you build out the PK ability to dose other common beta lactams as well as the aztreonam and ceftazidime. I realize this can be done by entering the necessary parameters in the customizable part of the app but it would be easier for us if this was built in.”

Well, here it is.

I’ve also built the functionality to download these models into the Antibiotic Kinetics for Windows program.

AbPK how to
More Models!

Eventually this functionality will be added to the iOS and Android versions.

Group sing-along:

APK Eureka

Since adding EurekaLog to APK I’ve been able to track down the causes of some perplexing errors.

EConvertError

“X is not a valid floating point value”, this error has puzzled me for years. I have actually never seen it myself in 15+ years of using the program. I am only aware of it because of scattered reports from users.

Once nice feature of the EurekaLog report is a screenshot of the user’s current screen.
apk_fp_error

As usual, it is the user causing the error. Instead of entering a valid decimal value, ie, [1.02], they accidentally add a decimal point or two: [1.02.] or [1..02], etc. This error has cropped up twice in only 30 reports. I never would have imagined that someone would do something so fundamentally wrong. I also believe that it is impossible to imagine every possible scenario of software usage. I think it would take a troop of monkeys randomly pounding on multiple keyboards to approximate the myriad of potential mistakes.

Regardless, any software program must be able to prevent a user’s dumb mistake from causing a fatal error. All of the input boxes for numeric values in APK are coupled to procedures to prevent non-numeric input:

 Delphi |  copy code |? 
1
procedure TfrmMain.EditRecentSCrKeyPress(Sender: TObject; var Key: Char);
2
begin
3
  if not (Key in ['0'..'9', DecimalSep, #8]) then
4
    Key := #0;
5
end;

However, that’s not enough. I was under the impression that the StrToFloat function was fault tolerant, i.e., if the input was wrong, it would output a zero.

 Delphi |  copy code |? 
1
  RecentSCr :=0;
2
  if EditRecentSCr.Text <> '' then
3
     RecentSCr := StrToFloat(EditRecentSCr.Text);

Unfortunately that’s not true. Instead, execution is halted with a fatal error and the program crashes.

The little known expression SysUtils.TryStrToFloat does have fault tolerance.

 Delphi |  copy code |? 
01
  RecentSCr :=0;
02
  if EditRecentSCr.Text <> '' then
03
     TryStrToFloat(EditRecentSCr.Text, RecentSCr);
04
  if RecentSCr <= 0 then
05
    begin
06
      ValidPatData := False;
07
      MessageDlg('Data entry error',
08
                 'Serum creatinine is a required field.',
09
                  mtError, [mbOK], 100, dckActiveForm);
10
      EditRecentSCr.SetFocus;
11
      Exit;
12
    end;

Alternatively one can test the boolean result of TryStrToFloat within an if..then block to flag an error:

 Delphi |  copy code |? 
1
  KelSD:=0;
2
  if TryStrToFloat(EditKel.Text, KelSD) = False then
3
     begin
4
        IsOkay := False;
5
        ErrMsg := 'Kel SD';
6
     end;

So, I’ve spent the better part of 3 days re-coding all the StrToFloat calls to TryStrToFloat (also StrToInt -> TryStrToInt). And I’ve been able to prevent fatal errors.

However, if the field is linked to a database, the type of error appears to be unavoidable. Although it’s not a fatal error, it still generates a EurekaLog error dialog.
apk_fp_error_dialog

I feel that the EurekaLog dialog doesn’t really fit this situation. I’d rather catch the error, display a dialog, and rollback the edit which produced the error. But I’ve hit a brick wall, unable to get to the event triggering the error. I’ve tried BeforePost, PostError, BeforeEdit, and EditError, none of these events are trapped when a db field is edited. In the case of database fields it appears that no event is triggered. WTF!?

EIniFileException

Another error that EurekaLog discovered is “unable to save settings”. The most fundamental requirement for use of this program is to have full read and write privileges to the APK folder. Without that, you can’t really use the software. Again, regardless, any software program should not allow a user’s dumb mistake to cause a fatal error. So, I combed through all code, and surrounded all ini writes in try..try..finally..except blocks:

 Delphi |  copy code |? 
01
try
02
   UserIni := TIniFile.Create(AppDataPath + 'screen.ini');
03
   try
04
      UserIni.WriteInteger('Window Placement', 'Main.Top', frmMain.Top);
05
      UserIni.WriteInteger('Window Placement', 'Main.Left', frmMain.Left);
06
   finally
07
      UserIni.Free;
08
   end;
09
except
10
   on E:Exception do
11
      begin
12
         MessageDlg('Error saving window placement',
13
                     E.Message,
14
                     mtError, [mbOK], 690, dckActiveForm);
15
         Action := caFree;
16
      end;
17
end;

ERangeError

The other common error that I’ve addressed in this update is “range check error”. Unfortunately there are not enough reports to establish a pattern, so I simply switched off range error checking. Within the compiler settings, range error checking is turned off by default. I only switched it on because some Delphi guru advised it. Well, sorry guru, in the end your advice caused more problems than it solved.

Peace out.

Post script

Today I figured out how to bypass the EurekaLog dialog for the EDatabaseError ‘not a valid floating point value’:

  1. Activated Exception Filters in the EurekaLog options
  2. Added EDatabaseError
  3. Changed handler to RLT

Result is this “somewhat” more user friendly dialog, without the unnecessary “Send Error Report” option.:
apk_fp_error_RTL_dialog

30 years

It was 30 years ago that I first released the Kinetics program for the IBM PC. 1984 was also the year this classic film was released:
footloose_VW

I had a yellow bug and a hair cut almost like KB’s:

Kinetics originated a couple of years earlier on a Timex Sinclair ZX81. Advertised as the first computer for under $100, it cost $99 (a lot for a new college graduate). About all you could do with the Sinclair was to write programs, it came with BASIC burned into the ROM. I’ve always been a hands-on learner, willing to jump into something before knowing everything about it. So I played around with the Sinclair for weeks until one day it all clicked, and I was able to write programs that actually did what I wanted.

1984 was also the year of the famous Apple super bowl commercial by Ridley Scott.
1984

Oh, how I longed for an Apple PC, but there was no way I could afford one. Instead I bought an IBM PC jr.
ibm-pc-jr-ad

It had two floppy drives and two cartridge slots on the front panel. The keyboard was line-of-sight wireless, and would only connect when it was positioned just so. To save money I bought the amber monochrome monitor instead of color. Back in those days computers came with printed manuals (because they really did need them). I wish I had kept those IBM manuals, they were gorgeous and well written:
ibm_basic_compiler

The Kinetics program was originally written in IBM BASIC. On the PCjr, BASIC was supplied on a cartridge. The advantage being that a real programming language was always ready without taking up system memory. Being stored in ROM, the BASIC would load very quickly, not needing access to the floppy disk or other storage.

1984 was also the year that PC-SIG was founded. They published an annual mail order catalog of public domain and user supported software. Programmers could submit their work. A friend gave me a copy of the compiler for IBM BASIC. So I compiled the Kinetics program and sent it to PC-SIG. If memory serves it was published in the 3rd edition, 1986.
pc-sig

Before the internet there was CompuServe and the dial up Bulletin Board System. ASHP operated a BBS they called Pharm-Net. It was an electronic meeting ground and forum for exchange of ideas. Renato Cataldo, who was with ASHP at the time, asked me to post the Kinetics program in the download area of Pharm-Net. If memory serves that was around 1987.

My uncle I.J. retired from IBM in the early eighties. He was so tired of keeping up with the latest tech that he vowed never to buy another new electronic device. He still has a rotary phone to this day. The rest of us have not been so inclined.
computers-1984-vs-2014

Who needs google glass if you have this 1984 invention?

FreeKin update

It’s a FreeKin update yo’.

I can’t believe it’s been 12 years since I wrote this FreeKin modeler program. It was originally written in Delphi 5, which I no longer have installed on my PC. However, I was pleasantly surprised when Delphi XE2 imported it without a hitch.

I made a few FreeKin tweaks here and there:

  • Increased the size of the fonts and the dialogs.
  • Added a save to xls function.
  • Improved the report function.
  • Wrote a new help file (RTFM).
  • Miscellaneous improvements to the interface flow.

I doubt if anyone else has ever used this FreeKin modeler program, but I love it. Not just for the double entendre name, but also for the simplicity and no nonsense practicality.

Enjoy FreeKin, I do!

Main FreeKin Screen

Main FreeKin Screen

What’s old is new again

There was an excellent session at the Midyear entitled “Infectious Diseases Update: Using Guidelines to Optimize Treatment”. Dr. Michael Ryback spoke about the “Implications of the Vancomycin Consensus Guidelines”. He pointed out the shortcomings of the 2009 guidelines, specifically those patient populations who were omitted from the guidelines, obese patients being the largest omission (pun intended).

Dr. Ryback is on the panel currently revising these guidelines and he spoke about some of the changes the panel are considering. Most interesting to me was the (possible) recommendation to draw two post-dose serum levels following the loading dose.
first_dose_levels

To which I say a resounding yes. Considering the large interpatient variability in Vancomycin pk, why “guess” what a patient’s dosing requirements might be? Why wait 2-3 days to find out if your recommended dose is therapeutic or toxic? And then fiddle around for days with dose changes and more levels. With two levels following the loading you can nail down the patient’s vancomycin pk right off the bat. Home run.

Usually when I mention this as a solution to a younger pharmacist, I either get that “you’re crazy” look or a blank stare. Apparently these things have to come down from a mountain, carved into stone tablets. Or, at the very least, from someone with a half dozen initials behind their name. Sheep go to heaven, while goats go to hell (or at least exiled to the graveyard shift).

Regardless, this type of pk analysis is actually the original Sawchuck and Zaske method for gentamicin dosing, from 1977. All but forgotten except for a few devotees.

Kinetic model for gentamicin dosing with the use of individual patient parameters.

The “First dose 2- or 3-point” method of serum level analysis has been an option in all of my pk programs from the very beginning, some 30 years ago. Here’s a screen shot from the latest AbPK for Windows:
abpk_first_dose

I certainly hope this recommendation makes it to the new Vancomycin consensus document. Or, at least, it opens the eyes of some of the braver sheep.

Windows help devolution

I have been writing Windows help files for nearly 30 years. It’s a very time consuming process to set up and maintain application help files. And just when I think I have a stable, workable system, Microsoft pulls the rug out from underneath me.

With the introduction of Windows Vista in 2006, Microsoft deprecated WinHelp and no longer includes it with the factory images of Windows Vista, Windows 7 and Windows 8.

HTML_help is Microsoft’s replacement for WinHelp. However, the Windows HTML_help viewer is crippled. It will only open CHM help files on a PC, not those stored on an intranet. Instead of content, the viewer displays the absolutely meaningless “Navigation canceled” message:
HTML_help

I have never understood this. An intranet is an internal network, so why in the world would it be considered a security risk to open a file on your own internal network? Apparently, Internet Explorer (tightly integrated into the Windows OS), is not able to discern the difference between the internet and an intranet.

So, I have been sticking with WinHelp as it is freely available for download from Microsoft for Vista, Windows 7 and (surprisingly) Windows 8, with both 32- and 64- bit versions:

Not until my PC at work was (finally) updated to Windows 7, did I realize that WinHelp is now just as useless as HTML_help. Windows 7 now blocks WinHelp from opening HLP help files stored on an intranet.

Again, WTF? What is the security risk of opening a file on your own internal intranet?

I could not find a trustworthy third party HLP file viewer, so I decided to switch to the CHM help format and seek out a replacement for Microsoft’s HTML_help viewer for intranet applications. After hours of web searching, I eventually found xCHM, a free, open-source, multi-platform, functional replacement for Microsoft’s HTML_help viewer.

Although it doesn’t have all the bells and whistles, xCHM does, at least, open CHM help files stored on an intranet.

In order to utilize xCHM, the help calls in APK had to be rewritten. Logically there are three scenarios:

  1. If the program is running from a local drive, the default help handling system is called.
  2. If the program is running on an intranet, and xCHM is available, then xCHM is called to display the relevant help topic.
  3. If on an intranet, and xCHM is not found, then an error dialog is displayed with a link to web help on rxkinetics.com.

Code snippets

Determine if the program is running locally or via intranet:

01
function TfrmMain.IsOnLocalDrive(): Boolean;
02
var
03
  aDrive: string;
04
begin
05
  aDrive := ExtractFileDrive(Application.ExeName);
06
  if (GetDriveType(PChar(aDrive)) = DRIVE_REMOVABLE) or
07
     (GetDriveType(PChar(aDrive)) = DRIVE_FIXED) then
08
    Result := True
09
  else
10
    Result := False;
11
end;
12

Determine which help system is available:

01
function TfrmMain.LocalHelpAvailable(): integer;
02
begin
03
   if IsOnLocalDrive then
04
      result := 1
05
   else
06
      begin
07
         if XchmInstalled then
08
            result := 2
09
         else
10
            result := 0;
11
      end;
12
end;

Intercept the application help call and choose which help system to use:

01
function TfrmMain.ApplicationEvents1Help(Command: Word; Data: Integer;
02
  var CallHelp: Boolean): Boolean;
03
var
04
   MyHelpKeyWord : string;
05
begin
06
   case LocalHelpAvailable of
07
     0: begin
08
           // On network and xchm is *not* installed
09
           CallHelp := False;
10
           frmHelp_Link.ShowModal;
11
        end;
12
     1: CallHelp := True;  // Local drive
13
     2: begin
14
           // On network and xchm *is* installed
15
           CallHelp := False;
16
           // Get HelpContext of current active control
17
           MyHelpKeyWord := IntToStr(Screen.ActiveControl.HelpContext);
18
           // If control HelpContext is blank -> Get Parent HelpContext
19
           if MyHelpKeyWord='' then
20
              begin
21
                 MyHelpKeyWord := IntToStr(Screen.ActiveControl.Parent.HelpContext);
22
                 // If parent is blank and parent is not a form, get form HelpContext
23
                 if MyHelpKeyWord='' then
24
                    begin
25
                       if Screen.ActiveControl.Parent <> Screen.ActiveControl.Owner then
26
                          MyHelpKeyWord := IntToStr(Screen.ActiveForm.HelpContext);
27
                       // Fallback: if control, parent, and form HelpContext are all blank -> display start up topic
28
                       if MyHelpKeyWord='' then
29
                          MyHelpKeyWord := '10';
30
                    end;
31
              end;
32
           ShellToXchm(MyHelpKeyWord);
33
        end;
34
   end;
35
end;

Shell out to xCHM:

1
procedure TfrmMain.ShellToXchm(HelpTopic:string);
2
var
3
   strParameters : string;
4
begin
5
   // Create parameter string -- Requires short path name!
6
   strParameters := ' /c ' + HelpTopic + ' ' + GetShortName(PathName) + 'apk.chm';
7
   // Shell out to xCHM
8
   ShellExecute(Application.Handle, 'open', 'xchm.exe', PChar(strParameters), PChar(PathName), SW_SHOW);
9
end;

Looking back, this has probably been an exercise in futility. It seems as though most pharmacists either don’t know there is a help system built into the program, or they don’t care. Either way, no one has bothered to contact me to report a problem. Why am I not at all surprised?

Unicode support for RikiTikiWiki

The lack of Unicode support in the wiki is something that has bugged me for a couple of years. M$ Word is the usual source of these non-standard characters, such things as em- and en-dashes and curly (smart) quotes. If these non-ASCII (non-keyboard) characters were pasted into a topic they would be displayed as garbled nonsense in the viewer and editor.

garbled_display

Display

Editor

This project was written in Delphi 2007 which does not natively support Unicode. I have XE, but I have no desire to go through the hair-pulling, forehead-slapping ordeal of updating all of the components in this project to yet another version of Delphi.

Since it did not support Unicode, the most likely suspect to me was the standard TRichEdit component. So, back in April I purchased the TMS Unicode component package. Well, that didn’t get very far because there was a major bug in the search function. Really, what good is a wiki without a search function? So, I emailed support and they put it on their to-do list. Eventually it got fixed and I updated that component this morning. Unfortunately that didn’t fix anything. So began the dogged pursuit of a fix.

The next suspicious component on the list was the HTML viewer. So, I found an update to the 5 year old open source component on code.google. After removing the old and re-compiling the new, no change.

Still believing it was a component problem, I looked into the database driver as the culprit. I pulled up SQL manager and did a dump of the raw data. It was fine, all the proper Unicode characters were there.

Then I began to believe that it had to be a problem with the variable declaration. All the experts on stack overflow had an opinion on what was the best string type to use for Unicode, and they were all different. So, I systematically changed the variable holding the text to every oddball string type available in Delphi: Utf8String, WideString, ANSIstring, and RawByteString. It didn’t matter, the output was still garbled.

I finally came across the open source Fundamentals code library on SourceForge. Included in that library were various decoder routines for converting character sets and encodings to and from Unicode. A function within it called UTF8StringToUnicodeString was the trick. Finally, it displays Unicode formatted text properly.

clear_display

I hope all you copy-and-pasters appreciate how much fricking work this was. And don’t get me started on what a gawd-awful-piece-of-shit-poor-excuse-for-a-word-processor M$ Word is.