The Road to Delphi

Delphi – Free Pascal – Oxygene

VCL Styles and Owner Draw

20 Comments

UPDATE

A Updated and improved version of the code shown on this article can be found in the Vcl.Styles.OwnerDrawFix unit which is part of the Vcl Styles Utils.

The Issue

When you uses the Vcl Styles, you expect which at least all the standard (and common) controls (TListBox, TEditBox, TListView, TMemo, Treeview, and so on) are skinned according to the style selected, but maybe you are observed some minor issues in controls like TListBox, TListView and TTreeView.

Check the next image, which had a form with a TListBox and a TListView

As you can see the highlight color and the checkboxes doesn’t use the Vcl Styles elements.

The Explanation

So why this happen? is a Vcl Style bug? well let me answer both questions :

First exist basically two ways how the vcl styles skin a control, if the control doesn’t have a windows handle (like the TLabel), the control is draw (usually) in the  paint method using the properties and procedures of the StyleServices (TCustomStyleServices) class, otherwise if the control is a TWinControl descendent then use the Style Hooks , the styles hooks handles the windows messages of the controls wrapped by the VCL and use Windows messages and WinApi calls to draw directly over the Canvas of the control or set the properties of the controls (when is possible) like the background  or foreground color using the SendMessage function.

In this point the windows messages are the key,   some Windows controls doesn’t fire some messages at least which the control was in an owner draw (or Custom Draw) mode.

for example if you want to change the highlight color of  a listview

1) You must receive the WM_NOTIFY message
2) then check the NM_CUSTOMDRAW notification code
3) after check for the current drawing stage (CDDS_ITEMPREPAINT in this case)
4) to finally pass a NMLVCUSTOMDRAW record with the new colors to use.

So in this case if the list view has the OwnerDraw property set to false these messages never will sent to our application. Because that is not possible implement a Style hook as there are not windows messages to process.

Note : Is technically possible write a Style hook for receive such owner draw messages, but that will implies create a style hook which need modify the ownerdraw property and then full draw the control.

The Fix

So how the style hooks are discarded, in this case we can owner draw the contols using the Vcl Styles classes and functions. (I don’t spend much time writing these routines , so can be incomplete)

OnDrawItem implementation for a TListbox

procedure TFrmMain.ListBox1DrawItem(Control: TWinControl; Index: Integer; Rect: TRect; State: TOwnerDrawState);
Var
 LListBox : TListBox;
 LStyles  : TCustomStyleServices;
 LDetails : TThemedElementDetails;
begin
  LListBox :=TListBox(Control);
  LStyles  :=StyleServices;
  //check the state
  if odSelected in State then
    LListBox.Brush.Color := LStyles.GetSystemColor(clHighlight);

  //get the details (states and parts) to use
  LDetails := StyleServices.GetElementDetails(tlListItemNormal);
    
  LListBox.Canvas.FillRect(Rect);
  Rect.Left:=Rect.Left+2;
  //draw the text
  LStyles.DrawText(LListBox.Canvas.Handle, LDetails, LListBox.Items[Index], Rect, [tfLeft, tfSingleLine, tfVerticalCenter]);

  //draw the Highlight rect using the vcl styles colors
  if odFocused In State then
  begin
    LListBox.Canvas.Brush.Color := LStyles.GetSystemColor(clHighlight);
    LListBox.Canvas.DrawFocusRect(Rect);
  end;
end;

OnDrawItem implementation for a TListView

procedure TFrmMain.ListView1DrawItem(Sender: TCustomListView; Item: TListItem;
  Rect: TRect; State: TOwnerDrawState);
var
  r         : TRect;
  rc        : TRect;
  ColIdx    : Integer;
  s         : string;
  LDetails  : TThemedElementDetails;
  LStyles   : TCustomStyleServices;
  BoxSize   : TSize;
  Spacing   : Integer;
  LColor    : TColor;
begin
  Spacing:=4;
  LStyles:=StyleServices;
  //get the color text of the items 
  if not LStyles.GetElementColor(LStyles.GetElementDetails(ttItemNormal), ecTextColor, LColor) or  (LColor = clNone) then
  LColor := LStyles.GetSystemColor(clWindowText);

  //get and set the backgroun color
  Sender.Canvas.Brush.Color := LStyles.GetStyleColor(scListView);

  //set the font color
  Sender.Canvas.Font.Color  := LColor;
  Sender.Canvas.FillRect(Rect);

  r := Rect;
  inc(r.Left, Spacing);
  //iterate over the columns
  for ColIdx := 0 to TListView(Sender).Columns.Count - 1 do
  begin
    r.Right := r.Left + Sender.Column[ColIdx].Width;

    if ColIdx > 0 then
      s := Item.SubItems[ColIdx - 1]
    else
    begin
      BoxSize.cx := GetSystemMetrics(SM_CXMENUCHECK);
      BoxSize.cy := GetSystemMetrics(SM_CYMENUCHECK);
      s := Item.Caption;
      if TListView(Sender).Checkboxes then
       r.Left:=r.Left+BoxSize.cx+3;
    end;

    if ColIdx = 0 then
    begin
      if not IsWindowVisible(ListView_GetEditControl(Sender.Handle)) and ([odSelected, odHotLight] * State <> []) then
      begin
        if ([odSelected, odHotLight] * State <> []) then
        begin
          rc:=Rect;
          if TListView(Sender).Checkboxes then
           rc.Left:=rc.Left+BoxSize.cx+Spacing;

          if not TListView(Sender).RowSelect then
           rc.Right:=Sender.Column[0].Width;
          
          Sender.Canvas.Brush.Color := LStyles.GetSystemColor(clHighlight);
          //draw the highlight rect using the current the vcl styles colors
          Sender.Canvas.FillRect(rc);
        end;
      end;
    end;

    if TListView(Sender).RowSelect then
      Sender.Canvas.Brush.Color := LStyles.GetSystemColor(clHighlight);

    //draw the text of the item
    LDetails := StyleServices.GetElementDetails(tlListItemNormal);
    Sender.Canvas.Brush.Style := bsClear;
    LStyles.DrawText(Sender.Canvas.Handle, LDetails, s, r, [tfLeft, tfSingleLine, tfVerticalCenter, tfEndEllipsis]);

    //draw the check box 
    if (ColIdx=0) and TListView(Sender).Checkboxes then
    begin
      rc := Rect;
      rc.Top    := Rect.Top + (Rect.Bottom - Rect.Top - BoxSize.cy) div 2;
      rc.Bottom := rc.Top + BoxSize.cy;
      rc.Left   := rc.Left + Spacing;
      rc.Right  := rc.Left + BoxSize.cx;

      if Item.Checked then
       LDetails := StyleServices.GetElementDetails(tbCheckBoxUncheckedNormal)
      else
       LDetails := StyleServices.GetElementDetails(tbCheckBoxcheckedNormal);

      LStyles.DrawElement(Sender.Canvas.Handle, LDetails, Rc);
    end;

    if ColIdx=0 then
     r.Left:=Sender.Column[ColIdx].Width + Spacing
    else
     inc(r.Left, Sender.Column[ColIdx].Width);
  end;

end;

After of apply the above code , this is the result


Check the source of the demo project on Github.

Author: Rodrigo

Just another Delphi guy.

20 thoughts on “VCL Styles and Owner Draw

  1. Excellent solution! Thank you very much! Very useful in my project!

  2. Thank you very much!!

  3. Great, I have same problem with TStatusBar.OnDrawItem. When i use VCL Styles this event was not fired.

  4. When I use VCL Style,CheckBox and RadioButton cannot change color and fontcolor,How can I do?

  5. Could you add support for ImageList drawing to the ListView fix? Can’t seem to get LStyles.DrawIcon to work. Thanks!

  6. It really works, but, the checkstate can not be changed by mouse anymore, only respond to keyboard.

    • You can use the OnMouseDown method to detect the mouse clicks, try this sample.

      const
        Spacing =4;
      var
        LDetails  : TThemedElementDetails;
        Size      : TSize;
      begin
       if TListView(Sender).OwnerDraw and (TListView(Sender).Checkboxes) then
        begin
         LDetails := StyleServices.GetElementDetails(tbCheckBoxCheckedNormal);
         Size.cx:=0;
         Size.cy:=0;
      
         if StyleServices.GetElementSize(TListView(Sender).Canvas.Handle, LDetails, esMinimum, Size) and (X>Spacing) and (X<=Size.Width) then
          TListView(Sender).Selected.Checked:=not TListView(Sender).Selected.Checked;
        end;
      end;
      
      
  7. Do you have an idea how to do the same with TEdit, TMemo and TCheckBox?

  8. HI Rodrigo,

    I have a Question and also I have an odd problem.

    ¿ How do you detect that there are not windows themes enabled, and how do you draw items (like check-boxes) without a theme ?

    I’m using a similar solution to draw check-boxes in a DBGrid, it seems to work with themes, but when I execute the program through a Remote Desktop Connection, check marks are not displayed at all.

    • If the Themes are not active in the system you can use the DrawFrameControl function to draw the checkbox. and to check if the themes are enabled you can use the StyleServices.Enabled function.

      • Hi Rodrigo,

        Thanks for your answer, DrawFrameControl and StyleServices.Enable do the work, thanks. But I still have the problem with Remote Desktop, in both cases with and without themes the owner drawed checkboxes are not displayed, but running the program in my computer works well, any ideas ?

  9. Hi Rodrigo,

    I’m using Delphi XE2 ( Is this an Issue ? should I use a newer version ? ) in a Windows 7 environment.

    The problem with RD happen with and without VCL Styles, so i think the problem is VCL, what do you think ?.

    I Also downloaded and compiled your code and it behaves weird with both VCL Styles and Windows Visual Effects disabled /enabled ( in my regular environment not the RD one ).

    Because I haven’t read your previous post I assume that this could be an issue with XE2, Am’I right ? sorry about that.

    I realized that you speaks Spanish , Don’t you ?.

    • Rafa, por supuesto que hablo español (soy de Chile), el blog esta en ingles solo para hacer el contenido mas accesible a todos. Ahora volviendo a tu pregunta ya que el problema aparece incluso cuando los VCL Styles estan desactivados, entonces la causa debe ser relativa a la VCL. Si quieres prepara una aplicacion de ejemplo para reproducir el problema y enviame el codigo a mi correo.

  10. Pingback: VCL Styles Utils – New feature | The Road to Delphi - a Blog about programming

Leave a comment