A Hyperlink Class: Subclassing the Label Control

Updated December 18, 2013: Source code for lblHyperlink is available here, parent class (lblLabel) source is available here.

The lblHyperlink label subclass

One of the more popular movements in desktop applications is “webification.” Users have become so used to the web interface via their browser, more and more desktop apps have added clickable hyperlinks as an important part of their UI. I’ve created a subclass of the lblLabel Visual FoxPro Label Control class to mimic a hyperlink.

This subclass of the contains the following properties which override the parent class:

MousePointer = 15
ForeColor = RGB(0,102,204)
Name = "lblHyperlink"

I set the ForeColor to a “blue”, simply for the visual cue in design time that I’m working with a “hyperlink” instead of a plain old label.

Plus a couple of other properties, for the ForeColor (we’ll change the ForeColor when the user hovers over the link):

nForeColor = 0
nHoverColor = 0

We’ll change the MousePointer to 15 (Hand), as a visual indication that it’s a link. I want the ForeColor of the link to be whatever Windows says the COLOR_HOTLIGHT (from GetSysColor()) should be for a hyperlink, and when the user hovers over the link I’ll change it to the COLOR_MENUHILIGHT value.

We’ll set the ForeColor (and hover color) in the Init event:

PROCEDURE Init
  DODEFAULT()

  ** Use GetSysColor to get the hyperlink color
  DECLARE INTEGER GetSysColor IN "user32" INTEGER nIndex

  This.nForeColor = GetSysColor(26)  && COLOR_HOTLIGHT
  This.nHoverColor = GetSysColor(29) && MENU_HOTLIGHT
ENDPROC

We’ll also add some code to the MouseEnter and MouseLeave events:

PROCEDURE MouseEnter
  LPARAMETERS nButton, nShift, nXCoord, nYCoord
  WITH THIS
    .ForeColor = .nHoverColor
    .FontUnderline = .T.
    .Caption = .Caption
  ENDWITH
ENDPROC

We set the ForeColor to the COLOR_MENUHILIGHT color and underline the label, just to provide an additional visual cue to the user at runtime that this is a hyperlink.

Notice the .Caption = .Caption line? I’ve seen “bleeding” with some form controls (including labels from time to time, when the app is minimized then restored) which have a BackStyle = 0 (Transparent). This line corrects the problem with “bleeding.”

The MouseLeave event looks like this:

PROCEDURE MouseLeave
  LPARAMETERS nButton, nShift, nXCoord, nYCoord
  WITH THIS
    .ForeColor = .nForeColor
    .FontUnderline = .F.
    .Caption = .Caption
  ENDWITH
ENDPROC

Once again we reset the ForeColor back to the COLOR_HOTLIGHT value in GetSysColor(). Then we remove the underline, and do the “bleeding” fix, just in case.

This is, of course, just basic functionality. To really make this shine, I recommend viewing Tamar Granor’s presentation “BindEvents for Better Applications” from the Online Visual FoxPro Users Group (OFUG) meeting (recorded on Tuesday, February 19, 2013).

And we’re done with the hyperlink!

Well, almost done. Since the hyperlink control is based on a Label, the link cannot get focus by using the keyboard. We need a way to be able to select (and click) the link without a mouse.

Enter, a container object.

The cntHyperlink container class

The cntHyperlink container class consists of a container object which contains the aforementioned lblHyperlink label and a CommandButton.

It is a subclass of cntContainer (shown below).

cntContainer has a custom abstract method named OnClick, which is called in the Click event:

DEFINE CLASS cntContainer AS Container
  Width = 194
  Height = 75
  BackStyle = 0
  BorderWidth = 0
  Name = "cntContainer"

  PROCEDURE onclick
    ** Abstract method called by the Click() event
  ENDPROC

  PROCEDURE Click
    This.OnClick()
  ENDPROC
ENDDEFINE

The cntHyperlink class has a custom property named cHotKey, which you could use as a hot key for the hyperlink:

cHotKey = ""

It also contains a lblHyperlink label. It remains the same, except we’ll add the following code in the OnClick() method which will “bubble up” to the parent container:

PROCEDURE lblHyperlink.OnClick
  This.Parent.OnClick()
ENDPROC

cntHyperlink container also has an OnClick() method, inherited from cntContainer.

We also have a CommandButton subclass added to the container. It contains the following events: LostFocus, GotFocus, and Init. It also contains a method named OnClick() (as do most of the controls in my base classes).

PROCEDURE cmdButton.LostFocus
  This.Parent.LostFocus()
ENDPROC

PROCEDURE cmdButton.GotFocus
  This.Parent.GotFocus()
ENDPROC

PROCEDURE cmdButton.Init
  This.Height = 0
  This.Width = 0
ENDPROC

PROCEDURE cmdButton.OnClick
  This.Parent.OnClick()
ENDPROC

Since it’s a hyperlink control, I don’t want the button to be visible on the form. In the Init event, we’ll size it to zero height and zero width. The GotFocus, LostFocus, and OnClick events simply “bubble up” to the parent container.

We’re done with the label and button, so all we have left is the container itself. In the Init, we add the following code which sets the Caption of the button to the cHotKey if it has been set:

PROCEDURE Init
  DODEFAULT()

  IF !EMPTY(This.cHotKey)
    This.cmdButton.Caption = "<" + This.cHotKey
  ENDIF
ENDPROC

Since we’re “bubbling up” events from the command button, we’ll add the following code to the GotFocus and LostFocus events:

PROCEDURE GotFocus
  WITH THIS
    .lblHyperlink.ForeColor = .lblHyperlink.nHoverColor
    .lblHyperlink.FontUnderline = .T.
  ENDWITH
ENDPROC

PROCEDURE LostFocus
  WITH THIS
    .lblHyperlink.ForeColor = .lblHyperlink.nForeColor
    .lblHyperlink.FontUnderline = .F.
  ENDWITH
ENDPROC

And now we’re finally ready to place the instance of the cntHyperlink class on a form, complete with Tab Order functionality, hot key functionality, etc.

All we’ll need is to add the instance code for the container OnClick() method to handle the actual clicks on the hyperlink.

Paint It (Almost) Black

In a previous post, I showed an ugly form. Ugly, because it looked awful when I ran the form with the Windows High Contrast Theme.

With the High Contrast Theme, normal black text appears yellow. Which is no problem at all when the text is drawn on a black background. But in my form, the text was drawn over a white shape control, and looked like this (the red arrows point to the yellow text):

Yellow text on a white background

Normal black text turns yellow in High Contrast #1 Theme.

So now I have a form that is unreadable in High Contrast. What I want to do is force Windows to draw the text black (since it is drawn over the white background).

 

There’s An Easy Fix For That!

This is simple to do. I set the ForeColor of the labels that I want to appear as black to RGB(2,2,2). This color change from RGB(0,0,0) is imperceptible to the human eye (it still looks black), but Windows sees it as an absolute color. And since I have changed it to something other than the default, Windows will display the label in my “near-black” color (since it is not the “Default”).

After my session at Southwest Fox, a couple of people said they do the same thing I do to force a label to be black, but they use RGB(1,1,1). Not sure why I picked 2,2,2 – but they’re both “black” to the naked eye — and either setting works fine.

Here’s an example of an instance of my “near-black” label on a form (the top label ForeColor is RGB(0,0,0) – the Default), while the label inside the white shape has a ForeColor of RGB(2,2,2) – or, as I like to call it, “forced black”:

Two labels, two different ForeColors, same effect.

Two labels, two different ForeColors, same effect.

And the same form, this time running in Windows High Contrast Theme:

The "near-black" label stays black

The “near-black” label stays black.

Later in the series, we’ll look at creating sub-classes for different controls. This one I’ll call the lblForceBlack control, subclassed from a base Label class.

Is Your App DPI-Aware?

When you tell Windows Vista (and 7 & 8) that your app is DPI-aware, Windows will not use DPI virtualization for your app and will no longer “lie” when you ask about the DPI settings.

Please note that by telling Windows that your app is DPI-aware, it is completely up to your app (you) to resize your UI in order to match user expectations based on their settings.

There are 2 different ways to tell Windows Vista, 7 and 8 that your app is DPI-aware:

  • a Windows API function, or
  • adding a section to your apps manifest file.

Using a Windows API Function

Windows Vista, 7 and 8 contain an API function named SetProcessDPIAware. The function has no parameters. It returns a zero if the function fails, and a non-zero result if the function succeeds.

Using it from within Visual FoxPro is easy:

DECLARE INTEGER SetProcessDPIAware IN WIN32API
SetProcessDPIAware()

The function simply tells Windows, “This app (process) is DPI Aware, so please do not lie to me. I will handle all of the resizing myself, so my users will see things the way they want to.”

Easy, right? Well, kind of. If you run the code listing above from the VFP Command Window, and if your Windows DPI setting has been set to 144dpi with DPI Virtualization turned on, you’ll get the correct (truthful) font factor from Windows (when you follow the API call with the code listing from a previous post).

BUT, remember this function has told Windows that this process is DPI-aware. And since you’re running the code from within the VFP IDE, you’re essentially telling Windows that Visual FoxPro is DPI-aware.

Unfortunately, Visual FoxPro is not DPI-aware, and has already been “DPI virtualized” by Windows. Causing Windows to suddenly accept that Visual FoxPro is DPI-aware can wreak havoc with the IDE.

If you don’t trust me on this, try it. It’s u-g-l-y!

So, one way we can force Windows to accept the fact that our app is DPI-aware is to simply call the SetProcessDPIAware function – preferably as early in your app as possible. I have seen several forum postings which report calling the function too late can cause Windows to continue to “lie” and use DPI virtualization anyway, though I have not recreated this problem myself.

 

Using Your App Manifest File

Another way to tell Windows Vista (and 7) that the app is DPI-aware is to add the following section to your app manifest file:

<asmv3:application xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
   <asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
      <dpiAware>true</dpiAware>
   </asmv3:windowsSettings>
</asmv3:application>

If you are not sure about manifest files and adding manifest resources to your Visual FoxPro applications, I recommend the following articles (I’ve been using Markus’ Manifest Tools in my work):

Calvin Hsia: Add a Manifest to Control Your Application Vista UAC Behavior

Craig Boyd: Apply Application Manifest At Compile Time With ProjectHook

Markus Winhard: ManifestTools.zip (based on code from Calvin’s blog above)

If you are not comfortable with messing around with manifest files, the SetProcessDPIAware API function is the way to go. Just be sure it only runs on Vista/Win7/Win8 (or call it in a TRY-CATCH-ENDTRY block) to prevent an error in pre-Vista versions of Windows!

Additional thoughts and (better) explanations of DPI-awareness in Vista can be found in the following article: http://www.rw-designer.com/DPI-aware

Windows Is Lying To You… With DPI Virtualization

DPI Virtualization in Windows

Windows Vista (and subsequently, Windows 7 & 8) introduced a new feature called DPI Virtualization.

DPI Virtualization means Windows will automatically “resize” your forms and form objects for you, with no coding or resizing required on your part.

Sounds great, doesn’t it? Well, not quite. Remember my “ugly” form in an earlier post, running at 144dpi? It was DPI Virtualized by Windows 7, and the result was a blurry window.

DPI Virtualization also requires the code listing from a previous post to fail, every time. Why? Because when the code asks Windows, “Hey, what’s the current setting?”, Windows Vista, 7 and 8 will always answer “your font factor is 1” (no resizing required) if the user has selected to use DPI Virtualization.

“Whoa, whoa, whoa, Kevin! If the user has selected DPI Virtualization?”

Yes. Windows Vista, 7 and 8 have two different modes of DPI scaling: XP Style, and DPI Virtualization.

Setting the DPI scale mode in Windows 7

Setting the DPI scale mode in Windows 7

If the user changes her DPI setting, and checks the “Use Windows XP style DPI scaling” checkbox, Windows Vista, 7 and 8 will return the expected (truthful) result from the previous code listing – just like XP did.

So, a DPI of 144dpi (150%) with this box unchecked, results in the following:

The correct font factor with XP style scaling turned on

The correct font factor with XP style scaling turned on

And, just like I would do with the XP result from earlier, I would want to resize my forms and form objects, making them 50% larger than “normal.”

But if the user has not checked the box, here’s what Windows tells me about the “font factor” (notice the messagebox is also a bit blurry):

Thanks, Windows, for lying and making my forms blurry

Thanks, Windows, for lying to me and making my forms blurry

Great. The user has selected 144dpi, but my app does not know this because Windows has said, “Don’t worry, no need to resize anything. All is good.”

And my user gets to see “somewhat” resized, yet blurry, forms in my app.

What we need is a “Windows Lie Detector” to make sure Windows stops lying to us.

How do we do this?

We need to tell Windows that the app is “DPI Aware”.

And that’s exactly what we’ll look at in tomorrow’s post.

 

Related Links (With much better explanations)

High DPI Settings In Windows
Fixing Windows Programming