Skip to main content
All docs
V23.2

Custom Draw Templates

  • 9 minutes to read

This topic explains how you can utilize HTML Templates to add additional elements to DevExpress and standard WinForms controls. The article also explains why this method may help you achieve your goals more easily than the use of the GraphicsCache API in CustomDraw~ events.

GraphicsCache API Usage in CustomDraw Events

Numerous DevExpress controls expose CustomDraw~ events that allow you to call methods of the GraphicsCache class to draw lines, shapes, and text strings on top of these controls. For example, the code below draws a notification badge on top of a Ribbon button.

Notification badge painted using CustomDraw API

using System.Drawing;

void ribbon_CustomDrawItem(object sender, BarItemCustomDrawEventArgs e) {
    if (e.RibbonItemInfo.Text == "Notifications") {
        Rectangle circleBounds = new Rectangle(
            e.Bounds.X + e.Bounds.Width - 30,
            e.Bounds.Y + e.Bounds.Height - 30,
            24, 24);
        Point circleCenter = new Point(
            circleBounds.X + circleBounds.Width / 2,
            circleBounds.Y + circleBounds.Height / 2);
        FontFamily fontFamily = new FontFamily("Arial");
        Font captionFont = new Font(fontFamily, 18, FontStyle.Regular,
           GraphicsUnit.Pixel);
        e.Cache.FillEllipse(Brushes.Red, circleBounds);
        e.Cache.DrawString(unreadAlerts.ToString(), captionFont, Brushes.White,
            new Point(circleCenter.X - 4, circleCenter.Y - 9));
    }
}

The custom drawing API allows you to perform easy tasks, but poses certain restrictions, including the following:

  • Coordinate and size calculation for every Draw~ method is a challenging task.
  • GraphicsCache API allows you to create primitive objects only and is unfit for more complex requirements.
  • Drawing methods allow you to paint only static objects. Dynamic user interaction support may dramatically increase your code complexity.

In v22.2 and newer, you can utilize an alternative approach: create an HTML & CSS template and draw it on top of a control.

Draw a Simple Static Template

The figure below illustrates a sample template painted inside Data Grid cells.

Static Custom Draw Template

The template is designed and stored inside the HtmlTemplateCollection component, and the template’s code is as follows:

<div class="container">
    <div id="btn_1" class="customDrawBtn">Custom Draw</div>
</div>
.container {
    height: 100%;
    display: flex;
    justify-content: flex-end;
    align-content: center;
    align-items: center;
    margin-right: 10px;
}
.customDrawBtn {
    padding: 4px;
    border-radius: 11px;
    background-color: @Green;
    color: @White;
    font-size: 10px;
    cursor: pointer;
}

To render a template, handle the control’s required CustomDraw~ event and call its e.DrawHtml method. This example handles the GridView.CustomDrawCell event.

void OnCustomDrawCell(object sender, RowCellCustomDrawEventArgs e) {
    e.DefaultDraw();
    e.Handled = true;
    if (e.Column.FieldName == "Name")
        e.DrawHtml(htmlTemplateCollection1[0]);
}

Draw a Data-Aware Template

The following example extends the previous sample. This time, the buttons obtain their text from a data source.

Template with Data retrieved from a different source

If the required data field is stored in the same data source, use the following syntax to embed the field value: ${FieldName}.

<div class="container">
    <div id="btn_1" class="customDrawBtn">${SolarObjectType}</div>
</div>

Otherwise, if you need to retrieve data from a different source, use the DrawHtml method overload that allows you to specify additional DxHtmlPainterArgs parameters. Call the SetFieldValue method to set up a relation between a data field name in the HTML template, and a data source.

<div class="container">
    <div id="btn_1" class="customDrawBtn">${UnboundDataField}</div>
</div>
void OnCustomDrawCell(object sender, RowCellCustomDrawEventArgs e) {
    e.DefaultDraw();
    e.Handled = true;
    GridView view = sender as GridView;
    string objectName = view.GetRowCellValue(e.RowHandle, view.Columns["Name"]) as string;
    if (e.Column.FieldName == "Name")
        e.DrawHtml(htmlTemplateCollection1[0], args => {
            // Retrieve values from the "SolarObjectTypes" dictionary
            args.SetFieldValue("UnboundDataField", SolarObjectTypes[objectName]);
    });
}

Interactive HTML Elements

CSS properties allow you to create styles applied when users hover over, click, and select HTML elements.

.button {
    background-color: @Green;
    color: @White;
    opacity: 0.8;
    border: 1px solid @Green;
    border-radius: 4px;
    padding: 8px 18px;
    font-size: 13px;
    margin: 8px;
    text-align: center;
    cursor: pointer;
}
    .button:hover {
        background-color: @Green;
        box-shadow: 0px 0px 3px @Green;
        opacity: 0.9;
    }

If a template is drawn on a CustomDraw~ event, it does not utilize these additional styles. This happens because you can paint a template anywhere within the target event bounds and the DevExpress HTML engine has no information whether current mouse coordinates match element bounds. To leverage these styles, you need to manually trigger required mouse events.

Single CustomDraw Template

The CustomDrawEmptyForeground event HTML demo illustrates a ListBox control with a single template painted when the control has no items. This template adds a button that users can click to generate ListBox items.

Custom Draw Empty Foreground

To activate stated CSS styles, do the following:

  1. Initialize an instance of the DxHtmlPainterContext class. This object stores all instances of your custom-drawn templates, and can emulate mouse events.
  2. Pass this instance as the DrawHtml method parameter when you draw templates.
  3. Handle the required control’s event and call the related DxHtmlPainterContext method. For example, if you handle the control’s MouseMove event, call the OnMouseMove method.
  4. Your control’s mouse events are now passed to templates through the DxHtmlPainterContext object. As a final step, tell the control it should update its visual state (and as a result, redraw the template with the required style applied). For DevExpress controls, call the Invalidate~ method to do this.
DxHtmlPainterContext ctx = new DxHtmlPainterContext(); // 1

listControl.CustomDrawEmptyForeground += (s, e) => {
    e.DrawHtml(htmlTemplate, ctx); // 2
};

listControl.MouseMove += (s, e) => {
    if(listControl.ItemCount == 0) {
        ctx.OnMouseMove(e); // 3
        listControl.Cursor = ctx.GetCursor(e.Location);
        listControl.Invalidate(); // 4
    }
    else listControl.Cursor = Cursors.Default;
};

Multiple CustomDraw Templates

The CustomDrawRowPreview event HTML demo illustrates a DataGrid whose Row Preview Sections display a template with a clickable link.

Multiple Templates in Data Grid

In this scenario you need to follow the same steps as in the Single CustomDraw Template section, but in addition specify interaction keys for each template. An interaction key is a unique tag that you assign to each instance of a custom-drawn template. This tag can then be used inside mouse event handlers to call the related mouse event for the specific template instance.

DxHtmlPainterContext ctx = new DxHtmlPainterContext();

gridView.CustomDrawRowPreview += (s, e) => {
    // Use a data source row index as a template instance ID
    int index = (s as GridView).GetDataSourceRowIndex(e.RowHandle);
    e.DrawHtml(htmlTemplate, ctx, (args) => args.InteractivityKey = index);
    e.Handled = true;
};

gridView.MouseMove += (s, e) => {
    GridView view = s as GridView;
    GridHitInfo hitInfo = view.CalcHitInfo(e.Location);
    if(hitInfo.RowHandle >= 0) {
        int index = view.GetDataSourceRowIndex(hitInfo.RowHandle);
        // Use the same interaction key value to find the correct template
        ctx.OnMouseMove(e, index);
        view.GridControl.Cursor = ctx.GetCursor(e.Location, index);
        view.InvalidateRow(hitInfo.RowHandle);
    }
};

Custom Draw in Standard Controls

Custom draw events for standard controls accept arguments of standard ~EventArgs classes, which do not have DrawHtml methods. However, you can call the static DxHtmlPainter class to render DevExpress anywhere.

The following figure illustrates a standard Data Grid View control with an HTML template painted inside its cells:

DevExpress HTML templates painted inside a standard Grid control

using DevExpress.Utils.Html;
using DevExpress.Utils.Drawing;
using System;
using System.Windows.Forms;

namespace MyApp {
    public partial class MyForm : Form {

        DxHtmlPainterContext ctx = new DxHtmlPainterContext();

        public MyForm() {
            InitializeComponent();
            dataGridView1.DataSource = SolarObjects.InitSampleSource();
            dataGridView1.CellPainting += OnCellPainting;
            dataGridView1.MouseMove += OnMouseMove;
        }

        void OnCellPainting(object sender, DataGridViewCellPaintingEventArgs e) {
            e.Paint(e.CellBounds, DataGridViewPaintParts.All);
            e.Handled = true;
            DxHtmlPainterArgs args = new DxHtmlPainterArgs();
            if (e.ColumnIndex == 0 && e.RowIndex >= 0) {
                using (GraphicsCache cache = new GraphicsCache(e.Graphics)) {
                    args.Bounds = e.CellBounds;
                    args.Cache = cache;
                    args.LookAndFeel = DevExpress.LookAndFeel.UserLookAndFeel.Default;
                    args.InteractivityKey = e.ColumnIndex.ToString() + "_" + e.RowIndex.ToString();
                    DxHtmlPainter.Default.Draw(htmlTemplateCollection1[0], args, ctx);
                }
            }
        }

        private void OnMouseMove(object sender, MouseEventArgs e) {
            var hInfo = dataGridView1.HitTest(e.X, e.Y);
            if (hInfo.Type == DataGridViewHitTestType.Cell) {
                if (hInfo.ColumnIndex == 0 && hInfo.RowIndex >= 0) {
                    var key = hInfo.ColumnIndex.ToString() + "_" + hInfo.RowIndex.ToString();
                    ctx.OnMouseMove(e, key);
                    dataGridView1.InvalidateRow(hInfo.RowIndex);
                }
            }
        }
    }
}

This sample exhibits the following differences when compared to the technique used in the Multiple CustomDraw Templates section.

  • To paint a template, you call the static DxHtmlPainter.Default.Draw method instead of e.DrawHtml.

  • The signature of the DevExpress e.DrawHtml method accepts an optional Action<DxHtmlPainterArgs> setupArgs parameter that allows you to pass required settings (for instance, the interactivity key) to the internally created DxHtmlPainterArgs instance. For standard controls you need to manually initialize these instances and set up their bounds, look&feel settings, and GraphicsCache.

Other than these differences, the sequence of required steps remains the same.