I'm trying to add a clickable image/button to a datagridview button column.
The image/button will be an icon for play or stop. If the user clicks the play button a service on the system is started, if the user clicks the stop button a service is stopped.
I already have written functions for starting and stopping the service. What I'm having difficulty with is getting the button/image to show up in the datagrid and making it clickable.
Here's what I have for code:
this.dgrdServices.RowPrePaint +=new DataGridViewRowPrePaintEventHandler(dgv_RowPrePaint);
this.dgrdServices.Rows.Add();
this.dgrdServices.Rows[0].Cells[0].Value = Image.FromFile(#"C:\users\brad\desktop\green-dot.gif");
this.dgrdServices.Rows[0].Cells[1].Value = "MyServer";
this.dgrdServices.Rows[0].Cells[2].Value = "MyService";
this.dgrdServices.Rows[0].Cells[3].Value = "Started";
this.dgrdServices.Rows[0].Cells[4].Value = new DataGridViewButtonCell();
this.dgrdServices.Rows[0].Cells[5].Value = "Uninstall";
I can't work out if it would be better to use a button which is an image or an just an image that's clickable. I also can't get a button to show up correctly.
Thanks
Brad
Show Image On Button
You can add a DataGridViewButtonColumn, then handle CellPainting event of the grid and check if the event is raised for your button column, then draw an image on it. At the end of event, don't forget to set e.Handled = true;.
In the below code I suppose you have an image resource like someImage:
private void grid_CellPainting(object sender, DataGridViewCellPaintingEventArgs e)
{
if (e.RowIndex < 0)
return;
//I supposed your button column is at index 0
if (e.ColumnIndex == 0)
{
e.Paint(e.CellBounds, DataGridViewPaintParts.All);
var w = Properties.Resources.SomeImage.Width;
var h = Properties.Resources.SomeImage.Height;
var x = e.CellBounds.Left + (e.CellBounds.Width - w) / 2;
var y = e.CellBounds.Top + (e.CellBounds.Height - h) / 2;
e.Graphics.DrawImage(someImage, new Rectangle(x, y, w, h));
e.Handled = true;
}
}
Show Image Without Button
To show a single image on all rows including new row, you can set the Image property of DataGridViewImageColumn. This way the image will be shown in that column on for all rows:
dataGridView1.Columns.Add(new DataGridViewImageColumn(){
Image = someImage, Name = "someName", HeaderText = "Some Text"
});
Also if you may want to have different images for cells, you can set the formatted value of DataGridViewImageColumn in CellFormatting event:
void grid_CellFormatting(object sender, DataGridViewCellFormattingEventArgs e)
{
if (e.RowIndex < 0)
return;
//I supposed the image column is at index 1
if (e.ColumnIndex == 1)
e.Value = someImage;
}
You also can set Image property of DataGridViewImageColumn to an image, but the image will not show on new row.
Handle Click
To handle Click on image/button you can handle CellClick or CellContentClick event:
void grid_CellClick(object sender, DataGridViewCellEventArgs e)
{
if (e.RowIndex < 0)
return;
//I suposed you want to handle the event for column at index 1
if (e.ColumnIndex == 1)
MessageBox.Show("Clicked!");
}
If you handled CellContentClick you should exactly click on image when you are using image column.
Screenshot
Here is the result. First column is a button column showing an image and second column is a normal image column set to show a single image:
Important Note
In above examples I assume you have an image in someImage member
variable:
Image someImage = Properties.Resources.SomeImage
Make sure you dispose someImage on disposal of the form and avoid using Properties.Resources.SomeImage directly everywhere you need.
Related
I have a data-bound DataGridView which I want to manipulate through the use of buttons which appear in the DataGridView next to the contents of the row they will affect (pictured below). Each row contains a DataGridViewTextBoxColumn as well a DataGridViewButtonColumn, both of which I want to show at all times.
When a mouse is hovered over a row, additional buttons appear to rearrange the data in the list, seen in row 2 (index 1) of the picture.
The dynamic buttons have Button.Click events to perform the desired actions. However, simply hovering over a row and clicking on a button does not call the OnClick method. The buttons do not highlight when hovered over or produce a clicking animation as is typical. If the mouse hovers over the static button in the DataGridViewButtonColumn of the same row, the dynamic buttons suddenly perform as desired, interacting with mouse hovering and OnClick event. Moving the mouse to another row resets this behavior.
How can I make the OnClick method properly trigger when a dynamic button is clicked without first interacting with static elements in the row? I have tried giving focus programmatically to various controls with no success. I do not want the dynamic buttons to be additional columns. The dynamic buttons should be to be clicked simply by moving the mouse into the grid, without addition interaction with other components.
Thanks.
Relevant Code:
// Instantiation
{
/* .... */
// Dynamic button declarations
Button deleteButton= new Button();
deleteButton.Text = "🗙"
deleteButton.Click += (sender, e) => OnClick(sender, e, ButtonType.Delete);
/* Repeat for remaining dynamic buttons */
/* .... */
// Set up grid columns
// Column
grid.AutoGenerateColumns = false;
DataGridViewTextBoxColumn column = new DataGridViewTextBoxColumn()
{
Name = "Condition",
DataPropertyName = "Value",
AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill
};
grid.Columns.Add(column);
// Type
DataGridViewButtonColumn num = new DataGridViewButtonColumn()
{
Name = "Type",
Width = 40,
DataPropertyName = "Type"
};
num.CellTemplate.Style.Font = new Font("Arial", 8, FontStyle.Italic);
grid.Columns.Add(num);
// Data binding
grid.DataSource = (BindingList)data
// Interaction with DataGridView
grid.CellMouseEnter += (sender, e) => GridHover(sender, e);
grid.CellMouseLeave += (sender, e) => GridLeave(sender, e);
/* .... */
}
// Control methods
// Mouse hovering over row
protected void GridHover(object sender, DataGridViewCellEventArgs e)
{
int row = e.RowIndex;
if (row >= 0 && row < ((DataGridView)sender).Rows.Count)
{
((DataGridView)sender).Focus();
((DataGridView)sender).Controls.Add(deleteButton);
deleteButton.Location = new Point(((DataGridView)sender).GetCellDisplayRectangle(0, row, true).Right - 20, ((DataGridView)sender).GetCellDisplayRectangle(0, row, true).Top);
deleteButton.Size = new Size(20, ((DataGridView)sender).GetCellDisplayRectangle(0, row, true).Height);
deleteButton.Show();
/* Repeat for remaining dynamic buttons */
}
}
// Mouse stops hovering over row
protected void GridLeave(object sender, DataGridViewCellEventArgs e)
{
int row = e.RowIndex;
if (row >= 0 && row < ((DataGridView)sender).Rows.Count)
{
upButton.Hide();
/* Repeat for remaining dynamic buttons */
}
}
// Mouse clicks dynamic button
protected void GridButtonClick(object sender, EventArgs e, ButtonType buttonType)
{
/* Program does not reach this point without first hovering over DataGridViewButtonColumn */
}
Per discussion with #TaW, the issue is that the GridLeave is called when the mouse hovered over the dynamic buttons. This behavior only exhibits itself when Hide/Show are called, as GridLeave and GridEnter are called in rapid succession, preventing the form from focusing on the buttons, and therefor preventing the buttons from being clicked.
The problem is resolved if the buttons are not hidden when the mouse hovers over them. Adding the following check at the beginning of GridLeave resolves the issue:
if ((sender as DataGridView).RectangleToScreen(upButton.Bounds).Contains(Control.MousePosition) ||
(sender as DataGridView).RectangleToScreen(downButton.Bounds).Contains(Control.MousePosition) ||
(sender as DataGridView).RectangleToScreen(deleteButton.Bounds).Contains(Control.MousePosition))
return;
I want a Delete button at the end of each row of DataGridView and by clicking that I want to remove the desired row from the binding list which is data source of my grid.
But I can't seem to do it I have created a button object in product class and instantiated it with the unique id to remove that object from list. but button is not showing in the row.
There are TextBoxes in the form and users can enter text, and when they press Add button, a new object of product is instantiated with the provided fields and then it is added to the BindingList.
Finally this list is bound to the DataGridView and details are shown in the grid. (I have done this part).
and at last by clicking save button the list is saved in the DB.
public class Product{
public string Brand { get; set; }
public int ProductPrice { get; set; }
public int Quantity { get; set; }
public product(string brand,int productPrice, int quantity){
this.Brand = brand;
this.ProductPrice = productPrice;
this.Quantity = quantity;
}
}
public partial class MainForm: Form{
.....
BindingList<Product> lProd = new BindingList<Product>();
private void btnAddProduct_Click(object sender, EventArgs e){
string Brand = txtProBrand.Text;
int Price = Convert.ToInt32(txtPrice.Text);
int Quantity = Convert.ToInt32(txtQuantity.Text);
Product pro = new Product(Brand, Price, Quantity);
lProd.Add(pro);
dataGridView1.DataSource = null;
dataGridView1.DataSource = lProd;
}
.....
}
To show a button on DataGridView rows, you should add a DataGridViewButtonColumn to columns of your grid. Here is some common tasks which you should know when using button column:
Add Button Column to DataGridView
Show Image on Button
Set Text of Button
Handle Click Event of Button
Add Button Column to DataGridView
To show a button on each row of your grid, you can add a DataGridViewButtonColumn to columns of your grid programmatically or using designer:
var deleteButton=new DataGridViewButtonColumn();
deleteButton.Name="dataGridViewDeleteButton";
deleteButton.HeaderText="Delete";
deleteButton.Text="Delete";
deleteButton.UseColumnTextForButtonValue=true;
this.dataGridView1.Columns.Add(deleteButton);
Show Image on Button
If you prefer to draw image on button, you should have an image in a resource and then handle CellPainting event of your grid:
void dataGridView1_CellPainting(object sender, DataGridViewCellPaintingEventArgs e)
{
if (e.RowIndex == dataGridView1.NewRowIndex || e.RowIndex < 0)
return;
if (e.ColumnIndex == dataGridView1.Columns["dataGridViewDeleteButton"].Index)
{
var image = Properties.Resources.DeleteImage; //An image
e.Paint(e.CellBounds, DataGridViewPaintParts.All);
var x = e.CellBounds.Left + (e.CellBounds.Width - image.Width) / 2;
var y = e.CellBounds.Top + (e.CellBounds.Height - image.Height) / 2;
e.Graphics.DrawImage(image, new Point(x, y));
e.Handled = true;
}
}
Set Text of Button
You can use either of these options:
You can set Text property of your DataGridViewButtonColumn and also set its UseColumnTextForButtonValue to true, this way the text will display on each cells of that column.
deleteButton.Text="Delete";
deleteButton.UseColumnTextForButtonValue=true;
Also you can use Value property of cell:
this.dataGridView1.Rows[1].Cells[0].Value = "Some Text";
Also as another option, you can handle CellFormatting event of your grid. This way may be useful when you want to set different texts for buttons.
void dataGridView1_CellFormatting(object sender, DataGridViewCellFormattingEventArgs e)
{
//If this is header row or new row, do nothing
if (e.RowIndex < 0 || e.RowIndex == this.dataGridView1.NewRowIndex)
return;
//If formatting your desired column, set the value
if (e.ColumnIndex=this.dataGridView1.Columns["dataGridViewDeleteButton"].Index)
{
e.Value = "Delete";
}
}
Handle Click Event of Button
To hanlde clicks on button, you can handle CellClick or CellContentClick event of your grid. Both events fires by click and by pressing Space key.
void dataGridView_CellClick(object sender, DataGridViewCellEventArgs e)
{
//if click is on new row or header row
if( e.RowIndex == dataGridView1.NewRowIndex || e.RowIndex < 0)
return;
//Check if click is on specific column
if( e.ColumnIndex == dataGridView1.Columns["dataGridViewDeleteButton"].Index)
{
//Put some logic here, for example to remove row from your binding list.
//yourBindingList.RemoveAt(e.RowIndex);
// Or
// var data = (Product)dataGridView1.Rows[e.RowIndex].DataBoundItem;
// do something
}
}
Get data of the record on Click event
You have e.RowIndex, then you can get the data behind the row:
var data = (Product)dataGridView1.Rows[e.RowIndex].DataBoundItem;
// then you can get data.Id, data.Name, data.Price, ...
You need to cast it to the data type of the recore, for example let's say Product.
If the data binding has been setup to use a DataTable, the the type to cast is DataRowView.
You can also use dataGridView1.Rows[e.RowIndex].Cells[some cell index].Value to get value of a specific cell, however DataBoundItem makes more sense.
Note
As mentioned by Ivan in comments, when you use BindingList you don't need to set datasource of grid to null and back to binding list with every change. The BindingList itself reflects changes to your DataGridView.
I want to have a data gridview that is bounded to SQL data source that has a column with name status also I need this column to be combobox, to be better to the user to select the value instead of writing it, is it possible ?
I've searched and I've found this Adding bound combobox to datagridview and it answered how to add comobobox to bounded datagridview, but this is not what I need
I've also tried to make another solution in a different way by using menustrip and it works fine but I can't edit the cell value after the user select menusttrip item as the datagridview is bounded to data source
Here is a part of my code related to the 2nd solution:
dtSamples = cMaster.Select_Samples(nKeyNo, DateTime.Today, strMachine);
dataGridView_Samples.DataSource = dtSamples;
//here is the event that shows the menustrip when the user clicked on the desired column
private void dataGridView_Samples_CellMouseClick(object sender, DataGridViewCellMouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
DataGridView.HitTestInfo hit = dataGridView_Samples.HitTest(e.X, e.Y); //Get the clicked cell
if (e.RowIndex < nNewRowIndex) //If it's a header, ignore
return;
dataGridView_Samples.CurrentCell = dataGridView_Samples[e.ColumnIndex, e.RowIndex]; //Select the cell for future info
StatusRowIndex = e.RowIndex;
if (dataGridView_Samples.CurrentCell.ColumnIndex == 4) //If this is the priority column
{
contextMenuStrip_Status.Show(Cursor.Position.X, Cursor.Position.Y); //Show the strip
}
}
}
//here is the event of selecting item of menustrip and it doesn't show the value in the cell
private void toolStripMenuItem_Repair_Click(object sender, EventArgs e)
{
if (nNewRowIndex == -1)
{
dataGridView_Samples.CurrentCell.Value = "Repair";
dataGridView_Samples.Rows[StatusRowIndex].Cells[4].Value = 4;
dataGridView_Samples.NotifyCurrentCellDirty(true);
}
else
{
dataGridView_Samples.Rows[nNewRowIndex].Cells[4].Value = "Repair";
// dataGridView_Samples.Rows[nNewRowIndex].Cells[4].Value = 4;
dataGridView_Samples.NotifyCurrentCellDirty(true);
dataGridView_Samples.BeginEdit(true);
dataGridView_Samples.EndEdit();
}
}
I am not getting the intended DataGridViewHitTestType.Cell hit whenever I click on a cell in my DataGridView control. I searched to no avail for a clue of why I am instead getting the DataGridViewHitTestType.ColumnHeader hit when I click on a cell in the first column, and DataGridViewHitTestType.TopLeftHeader when I click on a cell in the remaining two other columns.
My control has three columns, and its SelectionMode is set to CellSelect. Below is my code:
private void dgvYears_CellMouseDown(object sender, DataGridViewCellMouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
DataGridView.HitTestInfo hit = dgvYears.HitTest(e.X, e.Y);
if (hit.Type == DataGridViewHitTestType.Cell)
{
//What I want to do here....
}
}
}
I should admit that this is not well documented, but e.X and e.Y in this event are relative to the upper left coordinate of the cell, while HitTest expects data grid view client coordinates.
To get the correct hit test, you can use something like this
var dgv = (DataGridView)sender;
var cellRect = dgv.GetCellDisplayRectangle(e.ColumnIndex, e.RowIndex, false);
var hit = dgv.HitTest(e.X + cellRect.X, e.Y + cellRect.Y);
I need to write a function which will set the color in TableLayoutPanel cells depending on some condition during running the program.
TableLayoutPanel is divided by 16x16. There is some condition at the start of the program. If the condition is true for a cell this sell must be painted blue color. For example:
private void start_Click(object sender, EventArgs e)
{
foreach (string str in some_list)
{
if (some condition)
{
set_color_in_cell at row[i] colum[j] //(what shoud i use here?)
}
}
}
I found such example:
private void tableLayoutPanel_CellPaint(object sender, TableLayoutCellPaintEventArgs e)
{
if (e.Row == 0 && e.Column == 1)
{
e.Graphics.FillRectangle(new SolidBrush(Color.Black), e.CellBounds);
}
}
But I don't understand how to use it. If somebody knows about this please help me.
private void start_Click(object sender, EventArgs e)
{
string SyncAnswer = "";
foreach (string file_string in Data_from_file)
{
COM_Port.WriteLine(file_string);
while (SyncAnswer != "READY")
{
SyncAnswer = COM_Port.ReadLine();
if (SyncAnswer.Substring(0, 4) == "Fire")
{
//raise event
//paint for example a cell in Row=i Colum=j
}
else if (SyncAnswer.Substring(0, 4) == "Skip")
{
//raise event
}
}
}
}
Option 1 - Using CellPaint Event
Here is a step by step example:
Create a Form
Put a TableLayoutPanel from toolbox on your Form
Select tableLayoutPanel1 on design surface and Press F4 Key to see properties.
From toolbar of property grid, you can select to show Properties or Events . Click on events icon and from the list, double click on CellPaint event to create tableLayoutPanel1_CellPaint event handler in code.
You can paint each cells background in this method based on some criteria. The event will raise for painting each cells background and e.Row is the row index, e.Column is column index and e.CellBounds is bound of the painting cell.
For example in below sample, we draw black background if ((e.Column + e.Row) % 2 == 1) otherwise, we draw white background:
private void tableLayoutPanel1_CellPaint(object sender, TableLayoutCellPaintEventArgs e)
{
if ((e.Column + e.Row) % 2 == 1)
e.Graphics.FillRectangle(Brushes.Black, e.CellBounds);
else
e.Graphics.FillRectangle(Brushes.White, e.CellBounds);
}
To change Color Dynamically
To change the color from another point of program, for example in a Click event of a button, you should store back colors of each cell in an 2-dimension array and use that color to create a brush for that cell:
Define bgColors in your form:
Color[,] bgColors = new Color[2, 2] {
{ SystemColors.Control, SystemColors.Control },
{ SystemColors.Control, SystemColors.Control }
};
Draw background of cells this way:
private void tableLayoutPanel1_CellPaint(object sender, TableLayoutCellPaintEventArgs e)
{
using (var b = new SolidBrush(bgColors[e.Column, e.Row]))
{
e.Graphics.FillRectangle(b , e.CellBounds);
}
}
To change the BackColor of a Cell you can:
private void Button1_Click(object sender, EventArgs e)
{
//column: 0 ,row: 1
bgColors[0, 1] = Color.Red;
tableLayoutPanel1.Refresh();
}
Option 2 - Hosting Panel in Cells
As another simple option, you can put Panel in each cell, and set the Dock property of Panel to Fill and set its Margin property to 0,0, then each time you want to change color of a panel at position (column, row) you can use this code:
this.tableLayoutPanel1.GetControlFromPosition(column, row).BackColor = Color.Red;
Another way that is pretty simple is to dynamically add multiple PictureBox controls (ex: pbCellColor below) to you cells in your TableLayoutPanel, or some other more simple simple control, dock it, set the margin to
zero, then whenever you want:
pbCellColor.Dock = DockStyle.None;
pbCellColor.Margin = new Size(0, 0, 0, 0);
pbCellColor.Backcolor = Color.Red;
Easy, no event handling or state checks. Just set it and forget it. Worst case, if you call from a non-gui thread, you will need to Invoke the action.