windowsforms-404153-common-features-html-css-based-desktop-ui-custom-draw-with-html-templates.md
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.
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.
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));
}
}
Imports System.Drawing
Private Sub ribbon_CustomDrawItem(ByVal sender As Object, ByVal e As BarItemCustomDrawEventArgs)
If e.RibbonItemInfo.Text = "Notifications" Then
Dim circleBounds As New Rectangle(e.Bounds.X + e.Bounds.Width - 30, e.Bounds.Y + e.Bounds.Height - 30, 24, 24)
Dim circleCenter As New Point(circleBounds.X + circleBounds.Width \ 2, circleBounds.Y + circleBounds.Height \ 2)
Dim fontFamily As New FontFamily("Arial")
Dim captionFont As 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))
End If
End Sub
The custom drawing API allows you to perform easy tasks, but poses certain restrictions, including the following:
Draw~ method is a challenging task.GraphicsCache API allows you to create primitive objects only and is unfit for more complex requirements.In v22.2 and newer, you can utilize an alternative approach: create an HTML & CSS template and draw it on top of a control.
The figure below illustrates a sample template painted inside Data Grid cells.
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]);
}
Private Sub OnCustomDrawCell(ByVal sender As Object, ByVal e As RowCellCustomDrawEventArgs)
e.DefaultDraw()
e.Handled = True
If e.Column.FieldName = "Name" Then e.DrawHtml(htmlTemplateCollection1(0))
End Sub
The following example extends the previous sample. This time, the buttons obtain their text from a data 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]);
});
}
Private Sub OnCustomDrawCell(ByVal sender As Object, ByVal e As RowCellCustomDrawEventArgs)
e.DefaultDraw()
e.Handled = True
Dim view As GridView = TryCast(sender, GridView)
Dim objectName As String = TryCast(view.GetRowCellValue(e.RowHandle, view.Columns("Name")), String)
If e.Column.FieldName = "Name" Then e.DrawHtml(htmlTemplateCollection1(0), Function(args)
args.SetFieldValue("UnboundDataField", SolarObjectTypes(objectName))
End Function)
End Sub
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.
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.
To activate stated CSS styles, do the following:
DrawHtml method parameter when you draw templates.MouseMove event, call the OnMouseMove method.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;
};
Dim ctx As New DxHtmlPainterContext() ' 1
AddHandler listControl.CustomDrawEmptyForeground, Sub(s, e) e.DrawHtml(htmlTemplate, ctx) ' 2
AddHandler listControl.MouseMove, Sub(s, e)
If listControl.ItemCount = 0 Then
ctx.OnMouseMove(e) ' 3
listControl.Cursor = ctx.GetCursor(e.Location)
listControl.Invalidate() ' 4
Else
listControl.Cursor = Cursors.Default
End If
End Sub
The CustomDrawRowPreview event HTML demo illustrates a DataGrid whose Row Preview Sections display a template with a clickable link.
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);
}
};
Dim ctx As New DxHtmlPainterContext()
AddHandler gridView.CustomDrawRowPreview, Sub(s, e)
' Use a data source row index as a template instance ID
Dim index As Integer = (TryCast(s, GridView)).GetDataSourceRowIndex(e.RowHandle)
e.DrawHtml(htmlTemplate, ctx, Sub(args) args.InteractivityKey = index)
e.Handled = True
End Sub
AddHandler gridView.MouseMove, Sub(s, e)
Dim view As GridView = TryCast(s, GridView)
Dim hitInfo As GridHitInfo = view.CalcHitInfo(e.Location)
If hitInfo.RowHandle >= 0 Then
Dim index As Integer = 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)
End If
End Sub
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:
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);
}
}
}
}
}
Imports DevExpress.Utils.Html
Imports DevExpress.Utils.Drawing
Imports System
Imports System.Windows.Forms
Namespace MyApp
Partial Public Class MyForm
Inherits Form
Private ctx As New DxHtmlPainterContext()
Public Sub New()
InitializeComponent()
dataGridView1.DataSource = SolarObjects.InitSampleSource()
AddHandler dataGridView1.CellPainting, AddressOf OnCellPainting
AddHandler dataGridView1.MouseMove, AddressOf OnMouseMove
End Sub
Private Sub OnCellPainting(ByVal sender As Object, ByVal e As DataGridViewCellPaintingEventArgs)
e.Paint(e.CellBounds, DataGridViewPaintParts.All)
e.Handled = True
Dim args As New DxHtmlPainterArgs()
If e.ColumnIndex = 0 AndAlso e.RowIndex >= 0 Then
Using cache As 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)
End Using
End If
End Sub
Private Sub OnMouseMove(ByVal sender As Object, ByVal e As MouseEventArgs)
Dim hInfo = dataGridView1.HitTest(e.X, e.Y)
If hInfo.Type = DataGridViewHitTestType.Cell Then
If hInfo.ColumnIndex = 0 AndAlso hInfo.RowIndex >= 0 Then
Dim key = hInfo.ColumnIndex.ToString() & "_" & hInfo.RowIndex.ToString()
ctx.OnMouseMove(e, key)
dataGridView1.InvalidateRow(hInfo.RowIndex)
End If
End If
End Sub
End Class
End Namespace
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.
See Also