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.
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.
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.
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.
To activate stated CSS styles, do the following:
- Initialize an instance of the DxHtmlPainterContext class. This object stores all instances of your custom-drawn templates, and can emulate mouse events.
- Pass this instance as the
DrawHtml
method parameter when you draw templates. - 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. - 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.
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:
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 ofe.DrawHtml
.The signature of the DevExpress
e.DrawHtml
method accepts an optionalAction<DxHtmlPainterArgs> setupArgs
parameter that allows you to pass required settings (for instance, the interactivity key) to the internally createdDxHtmlPainterArgs
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.