What's New in MFC 9.0 (Orcas): Command Link Buttons

What Are Command Links?

Command Links are a new thing in Vista. If you aren't familiar with them, they look like the figure below. You can find additional information here.

A Command Link essentially has two parts: the main text and a note, as shown below.

A Command Link, as a matter of fact, is not a new class of control . It is just a new style for the BUTTON class. One can make a commonly used button control look like a command link by adding the BS_COMMANDLINK style. The Vista UI guidelines suggest one to use Command Links as a means of conveying additional information related to the specific commands.

MFC Enhancements for Command Links

In Visual Studio 2008 (beta 2 is available for download now), MFC has been enhanced to accomodate the new Vista features. The MFC classes are now in sync with Vista's demands. Thus, the following member functions have been added to CButton class w.r.t. Command Links:

  • CButton::GetNote
  • CButton::GetNoteLength
  • CButton::SetNote

These probably are self explanatory for those familiar with MFC's function naming convention. In addition, the resource editor toobox has been enhanced to reflect the new controls. See below:

The Problem

Note: Before proceeding, make sure you have downloaded and installed Visual Studio 2008 beta 2, if you have not done so already.

Application developers often write applications that are targeted for multiple operating systems. In typical deployment scenarios, it is not uncommon to have a single binary targeting multiple versions of the operating systems. This situation demands that, as an application developer, one tries to cut down on features that aren't commonly available on all of them, or, if it all possible, one will attempt to have some workaround to make them functionally identical even though, under the hood, there will be platform-specific code dealing with all those limitations.

This practise becomes even more challenging on the user interface side, wherein applications are required to have the best UI, and a common UI across all platforms because it directly relates to how the user interacts with the software itself. Any differences in the look and feel or behavior can lead to slow adoption of the software by customers, increased customer support calls, and so forth.

Often times, most software applications also ship with offline user guides. This means a uniform look and feel is a must; otherwise, it will have an impact on maintainence of different versions of the guides catering to different scenarios.

How does it all relate to Command Links? Simple. Command Links aren't available on platforms earlier than Vista. So, how does one go about having an application that is more user-friendly with command links, but one that also has a similar functionality on pre-Vista platforms?

The problem will be even more evident if one creates a sample MFC dialog project with Visual Studio 2008 (uses MFC 9.0), drops a command link on the dialog resource, builds, and runs it. If your development environment is pre-Vista, you will see that the Command Link is simply not visible. It does appear when you run the same application on Vista.

The Workaround

I call this a workaround, because it is no more than that. It is not a complete solution. A complete solution would mean identical behavior on pre-Vista, but that is like rewriting the control itself. The workaround discussed enables the software to use the command links that come with Vista, but degrades gracefully to provide, if not the same experience, a similar experience on pre-Vista platforms without going overboard trying to do so.

The problem discussed in the earlier section was that, on pre-Vista platforms, the common control DLL cannot interpret the BS_COMMANDLINK style. It doesn't seem that Microsoft has any intention on making them available on newer commctrl.dll for pre-Vista platforms anyway. However, the Command Link button is a pretty simple one. Perhaps there is a way to get at least a similar functionality on pre-Vista platforms?

Let me dig a little deeper. Okay, you know that the BS_COMMANDLINK style doesn't help much. What if you leave the style as is on Vista platforms, but remove it for non-Vista platforms? That should work, because then it is a regular button control.

There are a couple of ways one can do this:

  • Let the application remove the style on its own by using button.ModifyStyle(BS_COMMANDLINK,0);.
  • Subclass the button control the MFC way and modify it from within the class. What I am getting at it is, what if you write your own class, say CCGCommandLinkButton, derived from CButton and handle it all within this class? The usage then becomes easy. All the users will have to do is associate the command link resources in the template with CCGCommandLinkButton objects.

It's time to get started. If you do not have the project yet, crank up a dialog-based MFC project from within VS2008, accepting all defaults. Drop a "Command Button control" onto the dialog. Build and run. For the purpose of this article, you will use a pre-Vista platform for testing (because on Vista platforms, it should just work). You will see a dialog with OK and Cancel, but no Command Link button.

  1. Close the dialog resource if open. Go to menu Project->"Add Class". Select "MFC" and on the right hand side, select "MFC Class". Click on "Add". Give the class name as "CCGCommandLinkButton". Change the base class from CWnd to CButton. Click "Finish".
  2. Go to resource view. Open the dialog resource. Click on the command button to select it, right-click on it, and select "Add Variable". Set the variable name as, say, m_btnCommand. Select "Finish".
  3. Open the dialog class' header file. At the top, #include "CGCommandLinkButton.h" and change the declaration of m_btnCommand from CButton to CCGCommandLinkButton.
  4. Build and run. You won't see the command link button yet.
  5. The next step is to remove the BS_COMMANDLINK style on non-Vista platforms. For the sake of simplicity, add a BOOL private variable to the CCGCommandLinkButton class, called m_bPreVista. In the constructor, set it to TRUE. Later, you will replace this hard setting by the proper value by querying the Windows Version.
    CCGCommandLinkButton::CCGCommandLinkButton()
    {
       m_bPreVista = TRUE;
    }
    
  6. Open the CCGCommandLinkButton.h file. Make sure you are within the class scope in the header file. You should now see the Properties panel with a button for "Overrides". Select this and, from the list, select PreCreateWindow and drop down and add PreCreateWindow. Similarly, add PreSubclassWindow.
  7. PreCreateWindow is a virtual function called as a part of CWnd::Create/Ex function before the window is actually created. It gives you a chance to modify the CREATESTRUCT. You tap into this feature to remove the BS_COMMANDLINK style if one were to use Create/Ex functions to create the button.

    Add the following code:

    BOOL CCGCommandLinkButton::PreCreateWindow(CREATESTRUCT& cs)
    {
       // TODO: Add your specialized code here and/or call the
       //       base class
       if(TRUE == m_bPreVista)
       {
          //remove the BS_COMMANDLINK style because it doesn't
          //apply to preVista platforms
          cs.style &= (~BS_COMMANDLINK);
       }
       return CButton::PreCreateWindow(cs);
    }
    
  8. PresubclassWindow is a virtual function called as a part of MFC's DoDataExchange to subclass the actual control the first time. It gives you a chance to modify any styles after window creation. This is useful in the dialog template scenario where it is too late to influence the creation procedure. You tap into this feature to remove the BS_COMMANDLINK style again.

    Add the following code:

    void CCGCommandLinkButton::PreSubclassWindow()
    {
       // TODO: Add your specialized code here and/or call the
       //       base class
       if(TRUE == m_bPreVista)
       {
          //remove the BS_COMMANDLINK style because it doesn't
          //apply to preVista platforms
          ModifyStyle(BS_COMMANDLINK,0);
       }
       CButton::PreSubclassWindow();
    }
    
  9. The above code snippets do something simple. They remove the BS_COMMANDLINK style on pre-Vista platforms because they don't work. Build and run. You should "see" the button now.
  10. Now, open the dialog class implementation file and add code to set the note information like below to the end of OnInitDialog:
    // TODO: Add extra initialization here
    m_btnCommand.SetNote(_T("This is a test note"));
    return TRUE;    // return TRUE unless you set the focus to
                    // a control
    
    Build and run. You will see that this doesn't change anything. The text doesn't appear anywhere. This means is that the note is not part of the window text at all.
  11. Now, your task is to make the button at least look like a command link. That is, you need to show the button text, and beneath it, the note. There are couple of ways you can achieve this.
    • You could combine the note and actual window text as one, with a newline in between, and set this combined text as the window text. However, this won't work from the experiment you just did in that doing so alters the window text. This is not desirable.
    • The other approach is to maintain the two texts independently, but draw them on the button yourself. This means you need an owner drawn button (BS_OWNERDRAW style). This is doable. For one, people using Command Link buttons will most likely not be using the BS_OWNERDRAW style. If one were using BS_OWNERDRAW to do custom drawing, why use the Command Link style anyway? So, take this approach.

What's New in MFC 9.0 (Orcas): Command Link Buttons

  1. Modify the PreCreateWindow and PreSubclassWindow to add BS_OWNERDRAW style like below.
  2. BOOL CCGCommandLinkButton::PreCreateWindow(CREATESTRUCT& cs)
    {
       // TODO: Add your specialized code here and/or call the
       // base class
       if(TRUE == m_bPreVista)
       {
          //remove the BS_COMMANDLINK style because it doesn't
          //apply to preVista platforms
          cs.style &= (~BS_COMMANDLINK);
          //add OWNERDRAW style because now we draw text and note
          //ourselves
          cs.style |= BS_OWNERDRAW;
       }
       return CButton::PreCreateWindow(cs);
    }
    
    void CCGCommandLinkButton::PreSubclassWindow()
    {
       // TODO: Add your specialized code here and/or call the
       // base class
       if(TRUE == m_bPreVista)
       {
          //remove the BS_COMMANDLINK style because it doesn't
          //apply to preVista platforms
          //add OWNERDRAW style because now we draw text and note
          // ourselves
          ModifyStyle(BS_COMMANDLINK,BS_OWNERDRAW);
       }
       CButton::PreSubclassWindow();
    }
    
  3. Adding BS_OWNERDRAW style means that you have to draw the button yourself. Traditionally, this is done by the parent window. But, in MFC, you can make this a self-contained functionality of a class by overriding DrawItem. So, like you added PreCreateWindow, go ahead and add the DrawItem override to the CCGCommandLinkButton class.

    Add a simple drawing code like below. It simply draws the window text.

    void CCGCommandLinkButton::DrawItem(LPDRAWITEMSTRUCT
                                        lpDrawItemStruct)
    {
       // TODO:  Add your code to draw the specified item
       CString szWindowText;
       GetWindowText(szWindowText);
    
       CDC dc;
       dc.Attach(lpDrawItemStruct->hDC);
       dc.SetBkMode(TRANSPARENT);
       dc.DrawText(szWindowText,&lpDrawItemStruct->rcItem,DT_LEFT);
       dc.Detach();
    }
    
  4. How do you get the note text now? If you thought GetNote() were an answer, you will see that it won't work because pre-Vista platforms aren't aware of it at all. To get the note, this is what you have to do.

    Realize that calling SetNote(sometext), simply is a call to SendMessage BCM_SETNOTE passing the text as lParam. This is a good hint. All you need to do now is to handle this message in the CCGCommandLinkButton class and cache it within a CString member variable for later use. You do this only for preVista. For Vista and above, you let the default go through.

    Again, open the CCGCommandLinkButton.h file. Add protected function like below:
    afx_msg LRESULT OnSetNote(WPARAM wParam, LPARAM lParam);
    Add a private CString variable called m_szNote;. Open CCGCommandLinkButton.cpp. Add a message map entry like below:
    BEGIN_MESSAGE_MAP(CCGCommandLinkButton, CButton)
       ON_MESSAGE(BCM_SETNOTE,&OnSetNote)
    END_MESSAGE_MAP()
    
    Add the implementation like below:
    LRESULT CCGCommandLinkButton::OnSetNote(WPARAM wParam,
                                            LPARAM lParam)
    {
       if(TRUE == m_bPreVista)
       {
          m_szNote = (LPTSTR)lParam;
          //when note has changed, force a refresh
          Invalidate();
          UpdateWindow();
       }
       else
       {
          //for Vista and above, let the default do what it needs
          //to do
          Default();
       }
       return TRUE;
    }
    

    That takes care of caching the note information. You now need to udpate your implementation of DrawItem to reflect the note. A simple implementation would be to draw the text below the window text. You will do a little more than that in the code snippet below to make the window text bold font and the note in normal font. The code below is a simple code to do all that.

    void CCGCommandLinkButton::DrawItem(LPDRAWITEMSTRUCT
                                        lpDrawItemStruct)
    {
       // TODO:  Add your code to draw the specified item
       RECT itemRect = lpDrawItemStruct->rcItem;
    
       CString szWindowText;
       GetWindowText(szWindowText);
    
       CDC dc;
       dc.Attach(lpDrawItemStruct->hDC);
    
       //draw the background
       CBrush brBtnShadow;
       brBtnShadow.CreateSolidBrush(GetSysColor(COLOR_BTNSHADOW));
       dc.FrameRect(&itemRect, &brBtnShadow);
    
       //get current font
       CFont* pFont = GetFont();
       CFont* pOldFont = NULL;
       CFont boldFont;
       if(pFont)
       {
          LOGFONT lf;
          pFont->GetLogFont(&lf);
          lf.lfWeight = FW_BOLD;
          boldFont.CreateFontIndirect(&lf);
          pOldFont = dc.SelectObject(&boldFont);
       }
    
       InflateRect(&itemRect, -5, -5);
       RECT oldRect = itemRect;
    
       //first get the rectangle required to draw window text.
       //The returned rectangle is used to see what height was
       //used to draw window text, so the top coordinate for
       //painting the note text can be set appropriately.
       dc.DrawText(szWindowText,&itemRect,DT_LEFT | DT_WORDBREAK |
                   DT_CALCRECT );
       //now do the actual drawing
       dc.DrawText(szWindowText,&itemRect,DT_LEFT | DT_WORDBREAK);
       if(pFont)
       {
          //done with window text in bold font, restore old font
          //back.
          dc.SelectObject(pOldFont);
       }
       //draw note text
       oldRect.top = itemRect.bottom + 5;
       dc.DrawText(m_szNote,&oldRect,DT_LEFT | DT_WORDBREAK);
    
       dc.Detach();
    }
    
    Build and run. You should see the button and the note below it now in the proper fonts.
  5. That takes care of the SetNote call. You now have two more calls to take care of, GetNote and GetNoteLength. Now that you have the note cached, this shouldn't be a tough job as long as you handle the proper messages.

    Add a message map entry each for BCM_GETNOTE and BCM_GETNOTELENGTH messages and their corresponding implementations like below:

    ON_MESSAGE(BCM_GETNOTELENGTH,&OnGetNoteLength)
    ON_MESSAGE(BCM_GETNOTE,&OnGetNote)
    
    LRESULT CCGCommandLinkButton::OnGetNoteLength(WPARAM wParam,
                                                  LPARAM lParam)
    {
       if(TRUE == m_bPreVista)
       {
          return m_szNote.GetLength();
       }
       else
       {
          //for vista and above, let the default do what it needs
          //to do
          return Default();
       }
    }
    
    LRESULT CCGCommandLinkButton::OnGetNote(WPARAM wParam,
                                            LPARAM lParam)
    {
       if(TRUE == m_bPreVista)
       {
          //first check the passed in length if it is sufficient
          DWORD dwRequiredLen = m_szNote.GetLength() + 1;
          DWORD* pSize = (DWORD*)wParam;
          if((*pSize) < dwRequiredLen)
          {
             //from MSDN documentation
             //not enough space. Set *wParam to the required
             //length and set last error as
             //ERROR_INSUFFICIENT_BUFFER
             *pSize = dwRequiredLen;
             SetLastError(ERROR_INSUFFICIENT_BUFFER);
             return FALSE;
          }
          else
          {
             //copy the text to lParam
             _tcscpy_s((LPTSTR)lParam,(*pSize),m_szNote);
             return TRUE;
          }
       }
       else
       {
          //for Vista and above, let the default do what it needs
          //to do
          return Default();
       }
    }
    
    The result is like shown below:

    [result.png]

  6. What is now left is to make the m_bPreVista variable be configured at runtime. You use the GetVersionEx API and check for the major version. For Vista, the major version is 6. The code for the CCGCommandLinkButton constructor becomes something like this:
    CCGCommandLinkButton::CCGCommandLinkButton()
    {
       OSVERSIONINFO osvi;
    
       ZeroMemory(&osvi, sizeof(OSVERSIONINFO));
       osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFO);
    
       GetVersionEx(&osvi);
    
       m_bPreVista = osvi.dwMajorVersion < 6;
    }
    
    Build and run the exe on both pre-Vista and Vista platform.

    On pre-Vista platforms, you should see something like that below:

    [result.png]

    On Vista platforms, you should see something like below:

    [resultvista.png]

Conclusion

What we just did is by no means a complete solution (it doesn't handle different appearances of the button in different states, and it doesn't handle themes). The source code in the downloads section is a better one, although it doesn't handle themes. My goal for this article was to enable writing common code for all OSes without sacrificing on some of the newer features. The command link button possibly serves as a useful addition and one that probably would be used much more meaningfully in place of checkboxes and radio buttons. I hope this small class goes some way to achieving that.

There are some articles that probably already do the theme handling and I have included one such article in the references below. This could be used to enhance the look and feel. But, personally, I wouldn't go to extra lengths in doing that because this is just a stop-gap measure until Vista becomes a more regularly used platform.

References



Downloads

Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Managing your company's financials is the backbone of your business and is vital to the long-term health and viability of your company. To continue applying the necessary financial rigor to support rapid growth, the accounting department needs the right tools to most efficiently do their job. Read this white paper to understand the 10 essentials of a complete financial management system and how the right solution can help you keep up with the rapidly changing business world.

  • Event Date: April 15, 2014 The ability to effectively set sales goals, assign quotas and territories, bring new people on board and quickly make adjustments to the sales force is often crucial to success--and to the field experience! But for sales operations leaders, managing the administrative processes, systems, data and various departments to get it all right can often be difficult, inefficient and manually intensive. Register for this webinar and learn how you can: Align sales goals, quotas and …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds