The Locale Explorer: ScriptGetProperties
(and an owner-draw header control with rotated text)

Home
Back To Tips Page

Back to LocaleExplorer

This is an example of using ScriptGetProperties. The code in the lower edit control is the code that would be written to retrieve the highlighted feature. This code can be copied and pasted into an application and either used directly or form the basis of your own code. This page also illustrates how to create a header control with vertical text. The trick is to set a font whose height is large enough to force the header control height to a desired height and then using the custom-draw feature to draw the rotated text.

Vertical Text Header Control

The rotated header control was more difficult to do than it should have been.  This is due to both poor design of the header control and hopelessly inadequate documentation.

The problem with rotating the text is not one of just drawing it; what is essential is that the header control be tall enough to hold the text.  The poor design of the header control, when it is part of the CListView control, is that there is absolutely no way to specify the height.  Any attempt to set the height is defeated by the control, which knows better than you do what height you want to make it.  Never mind that (a) it is none of its business to decide how tall it should be except an initial default height and (b) once you set the height, it should honor the request you make.  If it won't handle SetWindowPos or MoveWindow, the CListView class should export a function SetHeaderHeight which gives control.  So the trick is to fool it into drawing the control the right height.  A well-designed control would not require tricks like this, but this is CListView.

So what I do is compute the height of vertical text, and create a font which is as tall as the height I want.  This forces the header control to get tall enough to hold text in the font established by SetFont.  Tricks like this would not be necessary in a sane world.

There are other problems.  To get the vertical text, I make the header control an owner-drawn control.  But if I set the style for column alignment, it erases the style I explicitly set in the header control!  It loses the HDF_OWNERDRAW style bit.  An attempt to "or" this bit into the header style fails, so I have to create a special function in my CListCtrl subclass, CListCtrlEx (note that Microsoft makes it really easy to subclass multiple levels, by ignoring every subclass below the immediate derivation from the base class.  This grotesque failure was propagated into VS.NET, even though it was known to be a blunder in VS6.  But why should we expect improvement in a product when the goal of VS.NET was clearly to destroy the usability of the user interface?)

void CListCtrlEx::SetColumnAlignment(int col, int fmt)
    {
     // Key here is that using SetColumn destroys the flag
     // values set for this column in the header control,
     // which could cause the HDF_OWNERDRAW flag to be removed
     CHeaderCtrl * hdr = GetHeaderCtrl();
     HDITEM item = {0};

     if(hdr != NULL)
        { /* has header */
         item.mask = HDI_FORMAT;
         hdr->GetItem(col, &item);
        } /* has header */

     LVCOLUMN coldata = {0};
     coldata.mask = LVCF_FMT;
     coldata.fmt = fmt;
     coldata.iSubItem = col;
     SetColumn(col, &coldata);

     // Now replace the owner-draw flag
     if(hdr != NULL)
        { /* fix up header */
         int od = (item.fmt & HDF_OWNERDRAW);

         hdr->GetItem(col, &item);
         item.fmt |= od;
         hdr->SetItem(col, &item);
        } /* fix up header */
    } // CListCtrlEx::SetColumnAlignment

Note this is yet another defect in the design of the CListView: there is no mechanism to preserve the owner-draw capability of the header if the alignment of the subitem is changed.

I save the style flags in the LPARAM field of the header control.

To manipulate the control, I extract it from the CListView:

     c_Header.SubclassWindow(c_UnicodeInfo.GetHeaderCtrl()->m_hWnd);

where the variable is declared as

CVerticalHeader c_Header;

The text drawing is largely just a bit of tedious geometry to handle the rotation and alignment issues.  However, there are some tricks that are useful.

If the text is wider than the column, it will still be drawn, and it will therefore overwrite the adjacent column.  This is definitely ugly. So before doing any drawing, a clipping region is created and selected into the DC.

void CVerticalHeader::DrawItem(LPDRAWITEMSTRUCT dis) 
   {
    CDC * dc = CDC::FromHandle(dis->hDC);
    CRect rect = dis->rcItem;
    ... lots of stuff in here...
    CRgn rgn;
    rgn.CreateRectRgn(rect.left, rect.top, rect.right, rect.bottom);

    dc->SelectClipRgn(&rgn);
    
    dc->SetTextAlign(align);
    dc->TextOut(x, y, s);
#ifdef _DEBUG
    ::GdiFlush();
#endif

There is another trick here that is useful when debugging.  Normally, GDI requests are queued up, and therefore you will not see the immediate effects of a drawing.  When debugging, this makes it hard to figure out exactly what has just been drawn.  By forcing a ::GdiFlush request, the GDI queue is flushed and you will see the effects immediately.  This has a negative impact on performance, so usually it is done inside an #ifdef.  Alternatively, if you were doing a lot of these, you could do

#ifdef _DEBUG
#define GDIFLUSH() GdiFlush()
#else
#define GDIFLUSH()
#endif

I did one other trick.  If the style was horizontal, and the column becomes too narrow, I rotate the text 90.

    sz = dc->GetTextExtent(s);
    if(sz.cx > rect.Width() && ((flags & DT_ROTATEMASK) == DT_0))
       { /* maybe rotate */
        dc->SelectObject(&font90);
        align = TA_TOP | TA_LEFT;
        y = rect.bottom - gap ;
        x = rect.left + gap;
       } /* maybe rotate */

In this case, I did not bother to recompute the height of the control, so the text can overflow the area.

Problems with SetFont

If you set the font for a CListCtrl, it sets the font for both the header control and the body of the text control.  If it weren't for the defective design that fails to allow you to set the height of the header control, this would not be a problem.  But my trick of setting the font to force the header control is defeated if there is a SetFont  So what I had to do in the case of setting the font (this is from the Unicode display) was to extract the font from the header control. calling SetFont, and then restoring the font to the header control.  The variable c_Header was set by

 c_Header.SubclassWindow(c_UnicodeInfo.GetHeaderCtrl()->m_hWnd);

The function to set the font in the control is handled by

void CUnicode::SetFontToControls()
    {
     CFont * f = c_Header.GetFont();
     c_CodePoints.SetFont(&UnicodeFont);
     c_UnicodeInfo.SetFont(&UnicodeFont);
     c_Header.SetFont(f);
    } // CUnicode::SetFontToControls

A fully-general solution could implement this in a subclass of CListCtrl such as my CListCtrlEx class, but I chose to do it in the SetFontToControls.  Because the SciprtGetProperties code does not change the font, it was not necessary in that property page handler.

Problems with HDN_ITEMCLICK

I wanted to implement active column buttons.  I used ClassWizard to add, to the control I wanted, an HDN_ITEMCLICK handler

ON_NOTIFY(HDN_ITEMCLICK, IDC_UNICODE_INFO, OnItemclickUnicodeInfo)

Unfortunately, I was getting no response to the mouse clicks.  I detected this by putting a breakpoint in the OnItemclickUnicodeInfo handler; the breakpoint was never taken. Spy++ is relatively useless for debugging WM_NOTIFY messages, since it does not actually decode the message, so it is impossible to extract any usable information from the Spy++ traces.  I was reduced to detailed debugging of the CCmdTarget::OnCmdMsg handler, a very tedious exercise.  What I discovered was that the control ID which came in with the WM_NOTIFY message was 0.  So I changed the declaration in the message map, as an experiment, to

ON_NOTIFY(HDN_ITEMCLICK, 0, OnItemclickUnicodeInfo)

and got my intended behavior.  Of course, this would mean that no more than one such control could be in a dialog.  So instead, I created a solution which involved setting the control ID of the header control separately:

     c_Header.SubclassWindow(c_UnicodeInfo.GetHeaderCtrl()->m_hWnd);
     ::SetWindowLong(c_Header.m_hWnd, GWL_ID, c_UnicodeInfo.GetDlgCtrlID()); 

It would probably be best to handle this in the PreSubclassWindow of the CListCtrlEx class, and perhaps in some revision I will make that modification.  Meanwhile, be aware that there is a potential glitch with using HDN_ notifications from CListCtrl, apparently triggered by my use of SubclassWindow to subclass the header control.  I have not experimented with variants of this to determine the real cause of the problem, but the workaround illustrated above has let me continue development.  I suspect this is Yet Another Design Error in the design of CListCtrl; anyone who wants to do an owner-draw header control

[Dividing Line Image]

The views expressed in these essays are those of the author, and in no way represent, nor are they endorsed by, Microsoft.

Send mail to newcomer@flounder.com with questions or comments about this web site.
Copyright 2005 Joseph M. Newcomer/FlounderCraft Ltd.  All Rights Reserved.
Last modified: May 14, 2011