Click here to Skip to main content
15,891,409 members
Articles / Desktop Programming / Windows Forms
Article

DataGridView Image Button Cell

Rate me:
Please Sign up or sign in to vote.
4.69/5 (30 votes)
24 Jun 2008CPOL5 min read 331.7K   21.1K   134   34
A clickable button cell that can display an icon in a DataGridView
CodeProject01.JPG

Introduction

The DataGridView has a button column that can display text, and it also has an image column. But, it does not have a button column that can display an image. This was a problem because I wanted my newer projects to be visually consistent with some earlier projects written using C++ Builder and TMS Software's Advanced StringGrid. In these earlier C++ projects, an image was used to display information, and buttons were used to execute actions. You never clicked on an image to perform an action. Buttons with text are not an acceptable option because they take up too much space compared to a smaller button with an icon on it. In addition to having a button display an image, I also wanted to be able to enable or disable the button based on programming or security logic.

The major technical obstacle I ran into was that when the DataGridView.Columns.Add() method is invoked, behind the scenes in the .NET Framework, only the empty constructor for the corresponding DataGridViewButtonCell class is called. So, creating an extended button cell class where the images are passed as a parameter of the constructor was not a design option. Writing hard-coded classes for each type of image button cell created a lot of redundant code, so that was a problem too.

The solution I came up with was to write an abstract DataGridViewImageButtonCell class that derived from the DataGridViewButtonCell class. This class has several concrete methods and a single abstract method called LoadImages(). When you derive a new cell from this class, you will be forced by the compiler to write a new LoadImages() routine.

So, in order to create a particular button (for example, a Delete button), there are three steps:

Step 1: Derive a specific DataGridViewImageButtonDeleteCell class from the abstract DataGridViewImageButtonCell class. Override the abstract LoadImages() method with a new method that loads the images that represent what this new cell's action does. In this example, I would load Delete images. Depending on the method you use to load the images, this routine can be as short as three lines of code.

C#
public class DataGridViewImageButtonDeleteCell : DataGridViewImageButtonCell
{
    public override void LoadImages()
    {
       // Load the Normal, Hot and Disabled "Delete" images here.
       // Load them from a resource file, local file, hex string, etc.

        _buttonImageHot = Image.FromFile("C:\\delete_16_h.bmp");
        _buttonImageNormal = Image.FromFile("C:\\delete_16.bmp");
        _buttonImageDisabled = Image.FromFile("C:\\delete_d.bmp");
    }
}

Step 2: Create a DataGridViewImageButtonDeleteColumn that derives from DataGridViewButtonColumn to display the Delete button cells.

C#
public class DataGridViewImageButtonDeleteColumn : DataGridViewButtonColumn
{
    public DataGridViewImageButtonDeleteColumn()
    {
        this.CellTemplate = new DataGridViewImageButtonDeleteCell();
        this.Width = 22;
        this.Resizable = DataGridViewTriState.False;
    }
}

Step 3: Add the column to the grid in order to display the Delete image button.

C#
DataGridViewImageButtonDeleteColumn columnDelete =
    new DataGridViewImageButtonDeleteColumn();
dataGridView1.Columns.Add(columnDelete);

While this solution did the things I wanted it to do, I did not want to have to use a local image file or add an image to the resource file of every project I wanted to use the button column in. I wanted to be able to create a Delete button column and drop it into any project I had without any additional steps. So, I decided to embed the images in each class as a byte array. In order to do this, I wrote a small utility to read a bitmap and convert it into a hex string.

C#
using System.IO; // MemoryStream
using Microsoft.VisualBasic;
// Hex function. Also add as a resource to the project.

//-----------------------------

StringBuilder sb = new StringBuilder();

Image image = Image.FromFile("Enter filename here");
MemoryStream ms = new MemoryStream();
image.Save(ms, ImageFormat.Bmp);

byte[] byteArray = ms.ToArray();

for (int idx = 0; idx < byteArray.Length; idx++)
{
    // After writing 16 values, write a newline.
    if (idx % 15 == 0)
    {
        sb.Append("\n");
    }

    // Prepend a "0x" before each hex value.
    sb.Append("0x");

    // If the hex value is a single digit, prepend a "0"
    if (byteArray[idx] < 16)
    {
        sb.Append("0");
    }

    // Use the Visual Basic Hex function to convert the byte.
    sb.Append(Conversion.Hex(byteArray[idx]));
    sb.Append(", ");
}

TextBox1.Text = sb.ToString();

The hex string created as output from this utility is hard-coded into the LoadImages() method of the derived DataGridViewImageButtonDeleteCell class. This way, the Delete images becomes part of the class. By creating a collection of concrete classes with embedded images derived from the abstract DataGridViewImageButtonCell class on my hard-drive, I can link them into any project I write and have them all look identical without any extra effort. It also makes it easier to share them with other programmers since the column class, cell class, and the embedded images are now in a single text file.

Miscellaneous Notes

Each of the cells have a Normal, Hot, and Disabled image. If you have your desktop set to the Windows Classic theme, or you have the VisualStyles disabled in your program, then only the Normal and Disabled image will be seen. If you have Windows XP or Vista themes activated and VisualStyles enabled in your program, then moving the mouse over a button will activate the Hot image.

Also, this code is written for 16x16 icons displayed in 22x22 cells. Using a different size icon or cell will require you to tweak the variables and constants in the Paint() method of the abstract class.

Background

In my earlier programs, the grids I had would display various resources, and the buttons on each line would change their status or open forms to modify data. These resources could be dispatched, taken out-of-service, marked available, etc. at the click of a button. Having a series of buttons next to an item made this easy.

In this article and the included example, I used Save, Print, and Delete to show how the grid button would work. But, this was for illustrative purposes only. In a real program, I would just select a grid line and hit the Delete button, or put the Save and Print on the File menu where the user expects to see it. I'm not suggesting that putting these particular buttons in a grid is good GUI design.

Things to Do

I was not able to get transparency working on my buttons, but I only worked on this for a day so, maybe more later if I need it. I would not want to put both an icon and text on a grid button, but it could be an option for people who would. The utility to create the hex string is very basic. It would be nice to make it a full-featured program with a dialog box to select the image and allow a variety of formats instead of only the hardcoded BMP.

History

  • 06/20/08: Version 1.0 released
  • 06/22/08: Article updated to make the process for creating a button column more clear.
    I labelled each part Step 1, Step 2 and Step 3 and gave an explicit example of loading the image.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
United States United States
I wrote my first program on a Tandy computer using a 1963 black & white Zenith TV for a monitor.

I wrote my second program in Fortran using a card punch machine.

I've been hooked ever since...


Comments and Discussions

 
QuestionCannot change image button cell color Pin
superfly711-Jul-16 0:27
professionalsuperfly711-Jul-16 0:27 
AnswerRe: Cannot change image button cell color Pin
superfly711-Jul-16 22:00
professionalsuperfly711-Jul-16 22:00 
QuestionDataGridView Image Button Cell - vb.Net Pin
mond0075-Aug-14 5:21
mond0075-Aug-14 5:21 
QuestionFantastic! Out and out fantastic! Pin
Mark @Pacsoft6-Mar-14 9:38
Mark @Pacsoft6-Mar-14 9:38 
QuestionRe: Fantastic! Out and out fantastic! Pin
Member 866592622-Apr-14 3:39
Member 866592622-Apr-14 3:39 
AnswerVB Code Pin
Mark @Pacsoft22-Apr-14 10:45
Mark @Pacsoft22-Apr-14 10:45 
Hi,

Here's the code (trust it is fairly legible).

There are a couple of places in the code where I extended the base concept [because of specific requirements]. And I refactored some of the "Paint()" geometry to better fit our requirements.

VB
Option Explicit On
Option Strict On

#Region "  Imports  "
 Region "  System  "
' Because this makes the "browsable" decoration simpler to read
Imports System.ComponentModel
#End Region
#End Region

''' <summary>
''' Encapsulates the abstract (must inherit) functionality
''' required to put an image-button in a DataGridView cell
''' </summary>
''' <history>
''' </history>
''' <remarks></remarks>
Public MustInherit Class DataGridViewImageButtonCell
    Inherits DataGridViewButtonCell

#Region "  Constructors  "
    ''' <summary>
    ''' Because this class is MustInherit, this is protected
    ''' </summary>
    ''' <remarks></remarks>
    Protected Sub New()
        'MyBase.New()
        MyBase.FlatStyle = Windows.Forms.FlatStyle.Flat

        ' Default behaviour is enabled
        Me._ButtonEnabled = True
        Me._ButtonState = System.Windows.Forms.VisualStyles.PushButtonState.Normal

        ' Changing this value affects the appearance of the image on the button.
        Me._ButtonImageOffset = 2I

        ' Because this is MustInherit, the designer decides which buttons are used where
        Me.LoadButtonImages()
    End Sub
#End Region
#Region "  Private  "
 Region "  Members  "
    Private _ButtonEnabled As Boolean

    Private _ButtonState As System.Windows.Forms.VisualStyles.PushButtonState

    ' Because the displayed image depends on the button's {PushButtonState}
    ' 1. Hot...
    Protected _ButtonImageHot As Image
    ' 2. Normal...
    Protected _ButtonImageNormal As Image
    ' 3. Disabled...
    Protected _ButtonImageDisabled As Image
    ' 4. Disabled...
    Protected _ButtonImagePressed As Image

    ' The amount of offset or border around the image
    Private _ButtonImageOffset As Integer

    ''' <summary>
    ''' Used to prevent untimely processing during load
    ''' </summary>
    ''' <remarks></remarks>
    Private _Loading As Boolean = True
#End Region
#Region "  Methods  "
    ''' <summary>
    ''' Manages destroying resources consumed by the object
    ''' </summary>
    ''' <history>
    ''' </history>
    ''' <remarks></remarks>
    Private Sub DisposeAndDestroyImageFiles()
        Try
            Me._ButtonImageDisabled.Dispose()
            Me._ButtonImageHot.Dispose()
            Me._ButtonImageNormal.Dispose()
            Me._ButtonImagePressed.Dispose()

        Finally
            Me._ButtonImageDisabled = Nothing
            Me._ButtonImageHot = Nothing
            Me._ButtonImageNormal = Nothing
            Me._ButtonImagePressed = Nothing
        End Try
    End Sub
#End Region
#Region "  Event Handlers  "
 #End Region
#End Region
#Region "  Protected  "
 Region "  OVERRIDES  "
    ''' <summary>
    ''' Responsible for painting the image on the button
    ''' </summary>
    ''' <param name="graphics"></param>
    ''' <param name="clipBounds"></param>
    ''' <param name="cellBounds"></param>
    ''' <param name="rowIndex"></param>
    ''' <param name="elementState"></param>
    ''' <param name="value"></param>
    ''' <param name="formattedValue"></param>
    ''' <param name="errorText"></param>
    ''' <param name="cellStyle"></param>
    ''' <param name="advancedBorderStyle"></param>
    ''' <param name="paintParts"></param>
    ''' <history>
    ''' </history>
    ''' <remarks></remarks>
    Protected Overrides Sub Paint(graphics As Graphics, clipBounds As Rectangle, cellBounds As Rectangle, rowIndex As Integer, elementState As DataGridViewElementStates, value As Object, _
     formattedValue As Object, errorText As String, cellStyle As DataGridViewCellStyle, advancedBorderStyle As DataGridViewAdvancedBorderStyle, paintParts As DataGridViewPaintParts)
        'MyBase.Paint(graphics, clipBounds, cellBounds, rowIndex, elementState, value, formattedValue, errorText, cellStyle, advancedBorderStyle, paintParts)

        ' Draw the cell background, if specified.
        If (paintParts And DataGridViewPaintParts.Background).Equals(DataGridViewPaintParts.Background) Then
            Using CellBackground As New SolidBrush(cellStyle.BackColor)
                graphics.FillRectangle(CellBackground, cellBounds)
            End Using
        End If

        ' Draw the cell borders, if specified.
        If (paintParts And DataGridViewPaintParts.Border).Equals(DataGridViewPaintParts.Border) Then
            PaintBorder(graphics, clipBounds, cellBounds, cellStyle, advancedBorderStyle)
        End If

        ' Because the area in which to draw the button needs to be known,
        ' start with the current bounds of the cell and the rectangle
        ' representing the borders (DataGridViewAdvancedBorderStyle)
        Dim ButtonArea As Rectangle = cellBounds,
            ButtonBorderAllowance As Rectangle = BorderWidths(advancedBorderStyle)

        ' Because there are some adjustments to be made
        Dim ImageHeight As Integer = Math.Min(ButtonArea.Height, If(Me.ButtonImage Is Nothing, 9999I, Me.ButtonImage.Size.Height)),
            ImageWidth As Integer = Math.Min(ButtonArea.Width, If(Me.ButtonImage Is Nothing, 9999I, Me.ButtonImage.Width)),
            WidthDiff As Integer = CInt((cellBounds.Width - Math.Min(32I, ImageWidth)) / 2I) - ButtonBorderAllowance.Width,
            HeightDiff As Integer = CInt((cellBounds.Height - Math.Min(32I, ImageHeight)) / 2I) - ButtonBorderAllowance.Height

        ' Because, now the borders are known, the area needs to be amended. Moving the
        ' (X,Y) in and down allows for the top and left borders while shrinking height
        ' and width allows for bottom and right
        With ButtonArea
            .X += ButtonBorderAllowance.X
            .Y += ButtonBorderAllowance.Y
            .Height -= (ButtonBorderAllowance.Height * 2)
            .Width -= (ButtonBorderAllowance.Width * 2)
        End With

        ' Because this is where the image will be drawn
        Dim ImageArea As New Rectangle(ButtonArea.X + WidthDiff, ButtonArea.Y + HeightDiff, ImageWidth, ImageHeight)

        ' Because the last step is to paint the button image
        ButtonRenderer.DrawButton(graphics, ButtonArea, Me.ButtonImage, ImageArea, False, Me.ButtonState)
    End Sub
#End Region
#End Region
#Region "  Public  "
 Region "  Properties  "
    ''' <summary>
    ''' Gets or sets whether the button is enabled or disabled
    ''' </summary>
    ''' <history>
    ''' </history>
    ''' <remarks></remarks>
    <EditorBrowsable(EditorBrowsableState.Always)>
    <Browsable(False)>
    Public Property Enabled() As Boolean
        Get
            Return _ButtonEnabled
        End Get

        Set(value As Boolean)
            _ButtonEnabled = value
            _ButtonState = If(value, System.Windows.Forms.VisualStyles.PushButtonState.Normal, System.Windows.Forms.VisualStyles.PushButtonState.Disabled)
        End Set
    End Property

    ''' <summary>
    ''' Gets or sets the <code>Forms.VisualStyles.PushButtonState</code> of the button
    ''' </summary>
    ''' <history>
    ''' </history>
    ''' <remarks></remarks>
    <EditorBrowsable(EditorBrowsableState.Always)>
    <Browsable(False)>
    Public Property ButtonState() As System.Windows.Forms.VisualStyles.PushButtonState
        Get
            Return _ButtonState
        End Get
        Set(value As System.Windows.Forms.VisualStyles.PushButtonState)
            _ButtonState = value
        End Set
    End Property

    ''' <summary>
    ''' Yields the correct image based on the current <code>Forms.VisualStyles.PushButtonState</code>
    ''' of the button
    ''' </summary>
    ''' <history>
    ''' </history>
    ''' <remarks></remarks>
    <EditorBrowsable(EditorBrowsableState.Always)>
    <Browsable(False)>
    Public ReadOnly Property ButtonImage() As Image
        Get
            Select Case _ButtonState
                Case System.Windows.Forms.VisualStyles.PushButtonState.Disabled
                    Return _ButtonImageDisabled

                Case System.Windows.Forms.VisualStyles.PushButtonState.Hot
                    Return _ButtonImageHot

                Case System.Windows.Forms.VisualStyles.PushButtonState.Normal
                    Return _ButtonImageNormal

                Case System.Windows.Forms.VisualStyles.PushButtonState.Pressed
                    Return _ButtonImagePressed

                Case System.Windows.Forms.VisualStyles.PushButtonState.[Default]
                    Return _ButtonImageNormal

                Case Else
                    ' Because this is default state
                    Return _ButtonImageNormal

            End Select
        End Get
    End Property
#End Region
#Region "  Methods  "
    ''' <summary>
    ''' Allows an individual image to be [over]written
    ''' </summary>
    ''' <param name="buttonState">
    ''' The <code>System.Windows.Forms.VisualStyles.PushButtonState</code> button's
    ''' image that will be replaced
    ''' </param>
    ''' <param name="buttonImage">
    ''' The image file to replace with
    ''' </param>
    ''' <history>
    ''' </history>
    ''' <remarks></remarks>
    <EditorBrowsable(EditorBrowsableState.Always)>
    <Browsable(False)>
    Public Sub SetButtonStateImage(ByVal buttonState As System.Windows.Forms.VisualStyles.PushButtonState, ByVal buttonImage As System.Drawing.Bitmap)
        ' NOTE WELL: [Default] image is [Normal] and anything outside the known values is ignored
        Select Case buttonState
            Case System.Windows.Forms.VisualStyles.PushButtonState.Disabled
                Me._ButtonImageDisabled.Dispose()
                Me._ButtonImageDisabled = buttonImage

            Case System.Windows.Forms.VisualStyles.PushButtonState.Hot
                Me._ButtonImageHot.Dispose()
                Me._ButtonImageHot = buttonImage

            Case System.Windows.Forms.VisualStyles.PushButtonState.Normal,
                System.Windows.Forms.VisualStyles.PushButtonState.Default
                Me._ButtonImageNormal.Dispose()
                Me._ButtonImageNormal = buttonImage

            Case System.Windows.Forms.VisualStyles.PushButtonState.Pressed
                Me._ButtonImagePressed.Dispose()
                Me._ButtonImagePressed = buttonImage

                'Case Else
                '    Throw New ApplicationException(String.Format("Button state {0} is not known or not understood", buttonState))

        End Select
    End Sub

    <EditorBrowsable(EditorBrowsableState.Always)>
    <Browsable(False)>
    Public Overloads Sub Dispose()
        MyBase.Dispose(True)
        Me.DisposeAndDestroyImageFiles()
    End Sub
#Region "  MUST OVERRIDE  "
    ''' <summary>
    ''' Because each "concrete" class will know which image it needs to load
    ''' </summary>
    ''' <remarks></remarks>
    Public MustOverride Sub LoadButtonImages()
#End Region
#Region "  OVERRIDES  "
 End Region
#End Region
#End Region
End Class

GeneralMy vote of 5 Pin
Britteandy4-Sep-13 23:34
Britteandy4-Sep-13 23:34 
QuestionDood, you got a 5 from me Pin
Kountree9-Aug-12 4:53
Kountree9-Aug-12 4:53 
GeneralMy vote of 5 Pin
neoraltech17-Jul-12 16:55
neoraltech17-Jul-12 16:55 
GeneralMy vote of 5 Pin
Manoj Kumar Choubey21-Jan-12 2:49
professionalManoj Kumar Choubey21-Jan-12 2:49 
NewsNew Idea Pin
User 582854827-Oct-11 3:59
User 582854827-Oct-11 3:59 
GeneralRe: New Idea Pin
Kountree9-Aug-12 4:52
Kountree9-Aug-12 4:52 
GeneralRe: New Idea Pin
shagaroo20-Aug-15 16:01
shagaroo20-Aug-15 16:01 
QuestionButtons Became Disabled Pin
Member 233481723-Jun-11 4:36
Member 233481723-Jun-11 4:36 
GeneralMy vote of 5 Pin
ksalvage4-Oct-10 1:04
ksalvage4-Oct-10 1:04 
Generalfabulous Pin
johnmaddison4-Oct-09 7:27
johnmaddison4-Oct-09 7:27 
GeneralHot image "stuck" - MouseLeave does not fire Pin
pcfountain10-Jun-09 9:20
pcfountain10-Jun-09 9:20 
GeneralRe: Hot image "stuck" - MouseLeave does not fire Pin
Darryl Caillouet21-Jul-09 2:29
Darryl Caillouet21-Jul-09 2:29 
GeneralRe: Hot image "stuck" - MouseLeave does not fire Pin
pcfountain21-Jul-09 7:47
pcfountain21-Jul-09 7:47 
GeneralRe: Hot image "stuck" - MouseLeave does not fire Pin
dmbrider5-Dec-09 0:53
dmbrider5-Dec-09 0:53 
GeneralGreat job Pin
nicholas_pei31-Mar-09 16:23
nicholas_pei31-Mar-09 16:23 
GeneralRe: Great job Pin
Darryl Caillouet31-Mar-09 17:18
Darryl Caillouet31-Mar-09 17:18 
QuestionHow to use a different set of Images Pin
D_Ana29-Mar-09 16:32
D_Ana29-Mar-09 16:32 
AnswerRe: How to use a different set of Images Pin
Darryl Caillouet30-Mar-09 3:10
Darryl Caillouet30-Mar-09 3:10 
GeneralGreat job! Pin
Dennis Betten4-Nov-08 0:16
Dennis Betten4-Nov-08 0:16 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.