I am trying to have a custom DatagridViewCell that has 3 clickable buttons horizontally. I have gotten as far as I can in code as shown below but I need a way to display the 3 buttons in the cell. I have only been able to paint text so far. I have even tried to declare a Panel object in case that will be easier to manipulate the buttons with.
public partial class CustomButtonCell : DataGridViewButtonCell
{
private Panel buttonPanel;
private Button editButton;
private Button deleteButton;
private Button approveButton;
private Button cancelButton;
public bool Enabled { get; set; }
public CustomButtonCell()
{
this.buttonPanel = new Panel();
this.editButton = new Button();
this.deleteButton = new Button();
this.approveButton = new Button();
this.cancelButton = new Button();
this.editButton.Text = "Edit";
this.deleteButton.Text = "Delete";
this.approveButton.Text = "Approve";
this.cancelButton.Text = "Cancel";
this.buttonPanel.Controls.Add(this.editButton);
this.buttonPanel.Controls.Add(this.deleteButton);
this.buttonPanel.Controls.Add(this.approveButton);
this.buttonPanel.Controls.Add(this.cancelButton);
this.Enabled = true;
}
// Override the Clone method so that the Enabled property is copied.
public override object Clone()
{
CustomButtonCell cell = (CustomButtonCell )base.Clone();
cell.Enabled = this.Enabled;
return cell;
}
protected override void Paint(Graphics graphics,
Rectangle clipBounds, Rectangle cellBounds, int rowIndex,
DataGridViewElementStates elementState, object value,
object formattedValue, string errorText,
DataGridViewCellStyle cellStyle,
DataGridViewAdvancedBorderStyle advancedBorderStyle,
DataGridViewPaintParts paintParts)
{
// Call the base class method to paint the default cell appearance.
base.Paint(graphics, clipBounds, cellBounds, rowIndex, elementState,
value, formattedValue, errorText, cellStyle,
advancedBorderStyle, paintParts);
// Calculate the area in which to draw the button.
Rectangle buttonArea1 = cellBounds;
Rectangle buttonAdjustment = this.BorderWidths(advancedBorderStyle);
buttonArea1.X += buttonAdjustment.X;
buttonArea1.Y += buttonAdjustment.Y;
buttonArea1.Height -= buttonAdjustment.Height;
buttonArea1.Width -= buttonAdjustment.Width;
Rectangle buttonArea2 = cellBounds;
Rectangle buttonAdjustment2 = this.BorderWidths(advancedBorderStyle);
buttonArea2.X += buttonAdjustment2.X + buttonArea1.Width;
buttonArea2.Y += buttonAdjustment2.Y;
buttonArea2.Height -= buttonAdjustment2.Height;
buttonArea2.Width -= buttonAdjustment2.Width;
Rectangle buttonArea3 = cellBounds;
Rectangle buttonAdjustment3 = this.BorderWidths(advancedBorderStyle);
buttonArea3.X += buttonAdjustment3.X + buttonArea2.Width;
buttonArea3.Y += buttonAdjustment3.Y;
buttonArea3.Height -= buttonAdjustment3.Height;
buttonArea3.Width -= buttonAdjustment3.Width;
Rectangle buttonArea4 = cellBounds;
Rectangle buttonAdjustment4 = this.BorderWidths(advancedBorderStyle);
buttonArea4.X += buttonAdjustment4.X + buttonArea3.Width;
buttonArea4.Y += buttonAdjustment4.Y;
buttonArea4.Height -= buttonAdjustment4.Height;
buttonArea4.Width -= buttonAdjustment4.Width;
// Draw the disabled button.
ButtonRenderer.DrawButton(graphics, buttonArea1, PushButtonState.Default);
ButtonRenderer.DrawButton(graphics, buttonArea2, PushButtonState.Default);
ButtonRenderer.DrawButton(graphics, buttonArea3, PushButtonState.Default);
ButtonRenderer.DrawButton(graphics, buttonArea4, PushButtonState.Default);
// Draw the disabled button text.
TextRenderer.DrawText(graphics, "Test", this.DataGridView.Font, buttonArea1, SystemColors.GrayText);
TextRenderer.DrawText(graphics, "Test", this.DataGridView.Font, buttonArea2, SystemColors.GrayText);
TextRenderer.DrawText(graphics, "Test", this.DataGridView.Font, buttonArea3, SystemColors.GrayText);
TextRenderer.DrawText(graphics, "Test", this.DataGridView.Font, buttonArea4, SystemColors.GrayText);
}
// Force the cell to repaint itself when the mouse pointer enters it.
protected override void OnMouseEnter(int rowIndex)
{
}
// Force the cell to repaint itself when the mouse pointer leaves it.
protected override void OnMouseLeave(int rowIndex)
{
}
}
public class CustomButtonColumn : DataGridViewColumn
{
public CustomButtonColumn()
{
this.CellTemplate = new CustomButtonCell ();
}
}
I agree that sometimes there's a solid use case to display a UserControl whether it's "three buttons" or each row having its own rolling Chart of real time data or whatever! One approach that long-term has worked for me, tried and true, is having a DataGridViewUserControlColumn class similar to the one coded below that can host a control in the cell bounds instead of just drawing one.
The theory of operation is to allow the bound data class to have properties that derive from Control. The corresponding auto-generated column(s) in the DGV can be swapped out. Then, when a DataGridViewUserControlCell gets "painted" instead of drawing the cell what happens instead is that the control is moved (if necessary) so that its bounds coincide with the cell bounds being drawn. Since the user control is in the DataGridView.Controls collection, the UC stays on top in the z-order and paints the same as any child of any container would.
The item's UserControl is added to the DataGridView.Controls collection the first time it's drawn and removed when the cell's DataGridView property is set to null (e.g. when user deletes a row). When the AllowUserToAddRows options is enabled, the "new row" list item doesn't show a control until the item editing is complete.
Typical Record class
class Record : INotifyPropertyChanged
{
public Record()
{
Modes.TextChanged += (sender, e) =>
OnPropertyChanged(nameof(Description));
Actions.Click += (sender, e) =>
{ _ = execTask(); };
}
public string Description
{
get => $"{Modes.Text} : {_description}";
set
{
if (!Equals(_description, value))
{
_description = value;
OnPropertyChanged();
}
}
}
string _description = string.Empty;
#region B O U N D C O N T R O L S o f A N Y T Y P E
public ButtonCell3Up Modes { get; } = new ButtonCell3Up();
public ProgressBar Actions { get; } = new ProgressBar { Value = 1 };
#endregion B O U N D C O N T R O L S o f A N Y T Y P E
private async Task execTask()
{
Actions.Value = 0;
while(Actions.Value < Actions.Maximum)
{
await Task.Delay(250);
Actions.Value++;
}
}
private void onModesTextChanged(object sender, EventArgs e) =>
OnPropertyChanged(nameof(Description));
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Configure DGV
public partial class MainForm : Form
{
public MainForm() => InitializeComponent();
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
dataGridView.DataSource = Records;
dataGridView.RowTemplate.Height = 50;
dataGridView.MouseDoubleClick += onMouseDoubleClick;
#region F O R M A T C O L U M N S
Records.Add(new Record()); // <- Auto-configure columns
dataGridView.Columns[nameof(Record.Description)].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
dataGridView.Columns[nameof(Record.Modes)].Width = 200;
DataGridViewUserControlColumn.Swap(dataGridView.Columns[nameof(Record.Modes)]);
dataGridView.Columns[nameof(Record.Actions)].Width = 200;
dataGridView.Columns[nameof(Record.Actions)].DefaultCellStyle.Padding = new Padding(5);
DataGridViewUserControlColumn.Swap(dataGridView.Columns[nameof(Record.Actions)]);
Records.Clear();
#endregion F O R M A T C O L U M N S
// FOR DEMO PURPOSES: Add some items.
for (int i = 0; i < 5; i++)
{
Records.Add(new Record { Description = "Voltage Range" });
Records.Add(new Record { Description = "Current Range" });
Records.Add(new Record { Description = "Power Range" });
}
for (int i = 1; i <= Records.Count; i++)
Records[i - 1].Modes.Labels = new[] { $"{i}A", $"{i}B", $"{i}C", };
}
Custom Cell with Paint override
public class DataGridViewUserControlCell : DataGridViewCell
{
private Control _control = null;
private DataGridViewUserControlColumn _column;
public override Type FormattedValueType => typeof(string);
private DataGridView _dataGridView = null;
protected override void OnDataGridViewChanged()
{
base.OnDataGridViewChanged();
if((DataGridView == null) && (_dataGridView != null))
{
// WILL occur on Swap() and when a row is deleted.
if (TryGetControl(out var control))
{
_column.RemoveUC(control);
}
}
_dataGridView = DataGridView;
}
protected override void Paint(
Graphics graphics,
Rectangle clipBounds,
Rectangle cellBounds,
int rowIndex,
DataGridViewElementStates cellState,
object value,
object formattedValue,
string errorText,
DataGridViewCellStyle cellStyle,
DataGridViewAdvancedBorderStyle advancedBorderStyle,
DataGridViewPaintParts paintParts)
{
using (var brush = new SolidBrush(getBackColor(#default: Color.Azure)))
{
graphics.FillRectangle(brush, cellBounds);
}
if (DataGridView.Rows[rowIndex].IsNewRow)
{ /* G T K */
}
else
{
if (TryGetControl(out var control))
{
SetLocationAndSize(cellBounds, control);
}
}
Color getBackColor(Color #default)
{
if((_column != null) && (_column.DefaultCellStyle != null))
{
Style = _column.DefaultCellStyle;
}
return Style.BackColor.A == 0 ? #default : Style.BackColor;
}
}
public void SetLocationAndSize(Rectangle cellBounds, Control control, bool visible = true)
{
control.Location = new Point(
cellBounds.Location.X +
Style.Padding.Left,
cellBounds.Location.Y + Style.Padding.Top);
control.Size = new Size(
cellBounds.Size.Width - (Style.Padding.Left + Style.Padding.Right),
cellBounds.Height - (Style.Padding.Top + Style.Padding.Bottom));
control.Visible = visible;
}
public bool TryGetControl(out Control control)
{
control = null;
if (_control == null)
{
try
{
if ((RowIndex != -1) && (RowIndex < DataGridView.Rows.Count))
{
var row = DataGridView.Rows[RowIndex];
_column = (DataGridViewUserControlColumn)DataGridView.Columns[ColumnIndex];
var record = row.DataBoundItem;
var type = record.GetType();
var pi = type.GetProperty(_column.Name);
control = (Control)pi.GetValue(record);
if (control.Parent == null)
{
DataGridView.Controls.Add(control);
_column.AddUC(control);
}
}
}
catch (Exception ex) {
Debug.Assert(false, ex.Message);
}
_control = control;
}
else control = _control;
return _control != null;
}
}
Custom Column
public class DataGridViewUserControlColumn : DataGridViewColumn
{
public DataGridViewUserControlColumn() => CellTemplate = new DataGridViewUserControlCell();
public static void Swap(DataGridViewColumn old)
{
var dataGridView = old.DataGridView;
var indexB4 = old.Index;
dataGridView.Columns.RemoveAt(indexB4);
dataGridView.Columns.Insert(indexB4, new DataGridViewUserControlColumn
{
Name = old.Name,
AutoSizeMode = old.AutoSizeMode,
Width = old.Width,
DefaultCellStyle = old.DefaultCellStyle,
});
}
protected override void OnDataGridViewChanged()
{
base.OnDataGridViewChanged();
if ((DataGridView == null) && (_dataGridView != null))
{
_dataGridView.Invalidated -= (sender, e) => refresh();
_dataGridView.Scroll -= (sender, e) => refresh();
_dataGridView.SizeChanged -= (sender, e) => refresh();
foreach (var control in _controls.ToArray())
{
RemoveUC(control);
}
}
else
{
DataGridView.Invalidated += (sender, e) =>refresh();
DataGridView.Scroll += (sender, e) =>refresh();
DataGridView.SizeChanged += (sender, e) =>refresh();
}
_dataGridView = DataGridView;
}
// Keep track of controls added by this instance
// so that they can be removed by this instance.
private readonly List<Control> _controls = new List<Control>();
internal void AddUC(Control control)
{
_controls.Add(control);
DataGridView.Controls.Add(control);
}
internal void RemoveUC(Control control)
{
_controls.Remove(control);
if (_dataGridView != null)
{
_dataGridView.Controls.Remove(control);
}
}
int _wdtCount = 0;
private void refresh()
{
var capture = ++_wdtCount;
// Allow changes to settle.
Task
.Delay(TimeSpan.FromMilliseconds(10))
.GetAwaiter()
.OnCompleted(() =>
{
if (DataGridView != null)
{
foreach (var row in DataGridView.Rows.Cast<DataGridViewRow>().ToArray())
{
if (row.Cells[Index] is DataGridViewUserControlCell cell)
{
if (row.IsNewRow)
{ /* G T K */
}
else
{
var cellBounds = DataGridView.GetCellDisplayRectangle(cell.ColumnIndex, cell.RowIndex, true);
if (cell.TryGetControl(out var control))
{
cell.SetLocationAndSize(cellBounds, control, visible: !row.IsNewRow);
}
}
}
}
}
});
}
private DataGridView _dataGridView = null;
}
Related
I'm using Winform DataGridView to display images. But when image fills cell, I don't see blue selection or in very low quantity. Please see:
When an cell is selected, I expect make cell whole transparent blue, not just sides or sides which isn't occupied by image. like:
Currently I tried coloring blue myself in paint event but it updates too frequently which hangs software.
I also modify image to look bluish in selection changed event, but again it slows down software.
Is there fix to this ? any workaround or something ? without compromising performance ?
EDIT: This is source code on how I display images on datagridview:
int colms = 4; // total no. of columns in our datagridview
//this create 4 image columns in datagridview
for (int c = 0; c < colms; c++)
{
var imgColm = new DataGridViewImageColumn();
imgColm.AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
imgColm.ImageLayout = DataGridViewImageCellLayout.Zoom;
grid.Columns.Add(imgColm);
}
int colm = 0;
int row = 0;
//this get all images and display on datagridview
foreach (var img in Directory.GetFiles(#"C:\Users\Administrator\Desktop\images"))
{
if (colm >= colms)
{
row++;
colm = 0;
grid.Rows.Add();
}
((DataGridViewImageCell)grid.Rows[row].Cells[colm]).Value = Thumb.GetThumbnail(img, ThumbSize.LowRes);
colm++;
}
Currently cell painting I use just a workaround, that draws border on selected cell. But its slow when data is large and secondly draws on unselected cell as well.
Here's two examples to test yourself regarding the performance and the fill style of the selected cells. Since your code snippet does not show in what context the code is called, especially creating the image columns part, and to avoid repeating unnecessary routines, use the grid designer to add 4 columns of type DataGridViewImageColumn and set the auto size and layout properties from there.
Normal Mode
In the Form's ctor, use Reflection to enable the grid's DoubleBuffered property to reduce the flicker. The emptyImage bitmap is the null value of the empty cells.
public partial class SomeForm : Form
{
private Bitmap emptyImage;
public SomeForm()
{
dgv.GetType()
.GetProperty("DoubleBuffered", BindingFlags.Instance | BindingFlags.NonPublic)
.SetValue(dgv, true);
emptyImage = new Bitmap(1, 1);
foreach (var col in dgv.Columns.OfType<DataGridViewImageColumn>())
col.DefaultCellStyle.NullValue = emptyImage;
}
Override the OnLoad method to populate the grid or to call a method for that.
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
var imgFolder = #"Your-Image-Folder-Path";
LoadImages(imgFolder);
}
protected override void OnFormClosed(FormClosedEventArgs e)
{
base.OnFormClosed(e);
emptyImage.Dispose();
}
private void LoadImages(string path)
{
string[] allFiles = Directory.GetFiles(path);
for (int i = 0; i < allFiles.Length; i += 4)
{
var files = allFiles.Skip(i).Take(4);
dgv.Rows.Add(
files.Select(f =>
{
using (var img = Image.FromFile(f, true))
return Thumb.GetThumbnail(img, ThumbSize.LowRes);
}).ToArray());
}
}
Note, your Thumb.GetThumbnail method returns a new image so you need to dispose of the original image.
Implement the CellPainting event to draw everything except the DataGridViewPaintParts.SelectionBackground and fill the selected cells with a semi-transparent color.
private void dgv_CellPainting(object sender, DataGridViewCellPaintingEventArgs e)
{
e.Paint(e.ClipBounds, e.PaintParts & ~DataGridViewPaintParts.SelectionBackground);
if (e.RowIndex >= 0 &&
e.Value != null && e.Value != emptyImage &&
(e.State & DataGridViewElementStates.Selected) > 0)
{
using (var br = new SolidBrush(Color.FromArgb(100, SystemColors.Highlight)))
e.Graphics.FillRectangle(br, e.CellBounds);
}
e.Handled = true;
}
}
Virtual Mode
You need here to have data store to cache only the images you need to display. The images of each cell of the visible rows. For that, The Cache class is
created to manage the relevant functionalities including:
Calculating the total number of rows required to display 4 images per visible row. So the SetMaxRows method should be called when the grid is first created and resized to recalculate the visible rows.
Loading, creating, and caching the current visible rows images in a Dictionary<int, Image> where the keys are the cell numbers.
Passing the requested images when the CellValueNeeded event is raised.
public partial class SomeForm : Form
{
private readonly Cache cache;
public SomeForm()
{
dgv.GetType()
.GetProperty("DoubleBuffered", BindingFlags.Instance | BindingFlags.NonPublic)
.SetValue(dgv, true);
dgv.VirtualMode = true;
var imgFolder = #"Your-Image-Folder-Path";
cache = new Cache(imgFolder, dgv.ColumnCount);
dgv.RowCount = cache.GetRowCount();
SetMaxRows();
}
protected override void OnFormClosed(FormClosedEventArgs e)
{
base.OnFormClosed(e);
cache.Dispose();
}
private void dgv_CellValueNeeded(object sender, DataGridViewCellValueEventArgs e) =>
e.Value = cache.GetImage(e.RowIndex, e.ColumnIndex);
private void dgv_Resize(object sender, EventArgs e) => SetMaxRows();
// Change dgv.RowTemplate.Height as needed...
private void SetMaxRows() =>
cache.MaxRows = (int)Math
.Ceiling((double)dgv.ClientRectangle.Height / dgv.RowTemplate.Height);
private class Cache : IDisposable
{
private readonly Dictionary<int, Image> dict;
private readonly Bitmap nullImage;
private int currentRowIndex = -1;
private Cache()
{
dict = new Dictionary<int, Image>();
nullImage = new Bitmap(1, 1);
}
public Cache(string path, int columnCount) : this()
{
ImageFolder = path;
ColumnCount = columnCount;
}
public string ImageFolder { get; set; }
public int ColumnCount { get; set; }
public int MaxRows { get; set; }
public Bitmap NullImage => nullImage;
public Image GetImage(int rowIndex, int columnIndex)
{
var ri = rowIndex - (rowIndex % MaxRows);
if (ri != currentRowIndex)
{
foreach (var img in dict.Values) img?.Dispose();
currentRowIndex = ri;
dict.Clear();
}
var i = (rowIndex * ColumnCount) + columnIndex;
Image res = nullImage;
if (!dict.ContainsKey(i))
{
var file = Directory.EnumerateFiles(ImageFolder)
.Skip(i).FirstOrDefault();
if (file != null)
{
using (var img = Image.FromFile(file, true))
dict[i] = res = Thumb.GetThumbnail(img, ThumbSize.LowRes);
}
}
else
{
res = dict[i];
}
return res;
}
public int GetRowCount()
{
var count = Directory.EnumerateFiles(ImageFolder).Count();
return (int)Math.Ceiling((double)count / ColumnCount);
}
public void Dispose()
{
foreach (var img in dict.Values) img?.Dispose();
nullImage.Dispose();
}
}
Finally, the CellPainting event remains almost the same except that you get the null image from the cache instance.
private void dgv_CellPainting(object sender, DataGridViewCellPaintingEventArgs e)
{
e.Paint(e.ClipBounds, e.PaintParts & ~DataGridViewPaintParts.SelectionBackground);
if (e.RowIndex >= 0 &&
e.Value != null && e.Value != cache.NullImage &&
(e.State & DataGridViewElementStates.Selected) > 0)
{
using (var br = new SolidBrush(Color.FromArgb(100, SystemColors.Highlight)))
e.Graphics.FillRectangle(br, e.CellBounds);
}
e.Handled = true;
}
}
I have a mesh system for a MMO and it's uses A* to find paths. Occasionally it fails because I have nodes that are badly placed. To fix this, I made a mesh visualiser. It works OKish - I can see that some nodes are badly placed. But I can't see which nodes.
Here is my code to show the nodes:
foreach (var node in FormMap.Nodes)
{
var x1 = (node.Point.X * sideX);
var y1 = (node.Point.Y * sideY);
var x = x1 - nodeWidth / 2;
var y = y1 - nodeWidth / 2;
var brs = Brushes.Black;
//if (node.Visited)
// brs = Brushes.Red;
if (node == FormMap.StartNode)
brs = Brushes.DarkOrange;
if (node == FormMap.EndNode)
brs = Brushes.Green;
g.FillEllipse(brs, (float)x, (float)y, nodeWidth, nodeWidth);
I know I can redo this and make thousands of small buttons and add events for them but that seems overkill.
Is there any way I can add tooltips to the nodes I am painting on the panel?
Yes, you can show a tooltip for your nodes that you have drawn on the drawing surface. To do so, you need to do the followings:
Implement hit-testing for your node, so you can get the node under the mouse position.
Create a timer and In mouse move event handler of the drawing surface, do hit-testing to find the hot item. If the hot node is not same as the current hot node, you stop the timer, otherwise, if there's a new hot item you start the timer.
In the timer tick event handler, check if there's a hot item, show the tooltip and stop the time.
In the mouse leave event of the drawing surface, stop the timer.
And here is the result, which shows tooltip for some points in a drawing:
The above algorithm, is being used in internal logic of ToolStrip control to show tooltip for the tool strip items (which are not control). So without wasting a lot of windows handle, and using a single parent control and a single tooltip, you can show tooltip for as many nodes as you want.
Code Example - Show Tooltip for some points in a drawing
Here is the drawing surface:
using System.ComponentModel;
using System.Drawing.Drawing2D;
public class DrawingSurface : Control
{
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[Browsable(false)]
public List<Node> Nodes { get; }
public DrawingSurface()
{
Nodes = new List<Node>();
ResizeRedraw = true;
DoubleBuffered = true;
toolTip = new ToolTip();
mouseHoverTimer = new System.Windows.Forms.Timer();
mouseHoverTimer.Enabled = false;
mouseHoverTimer.Interval = SystemInformation.MouseHoverTime;
mouseHoverTimer.Tick += mouseHoverTimer_Tick;
}
private void mouseHoverTimer_Tick(object sender, EventArgs e)
{
mouseHoverTimer.Enabled = false;
if (hotNode != null)
{
var p = hotNode.Location;
p.Offset(16, 16);
toolTip.Show(hotNode.Name, this, p, 2000);
}
}
private System.Windows.Forms.Timer mouseHoverTimer;
private ToolTip toolTip;
Node hotNode;
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
var node = Nodes.Where(x => x.HitTest(e.Location)).FirstOrDefault();
if (node != hotNode)
{
mouseHoverTimer.Enabled = false;
toolTip.Hide(this);
}
hotNode = node;
if (node != null)
mouseHoverTimer.Enabled = true;
Invalidate();
}
protected override void OnMouseLeave(EventArgs e)
{
base.OnMouseLeave(e);
hotNode = null;
mouseHoverTimer.Enabled = false;
Invalidate();
}
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
if (Nodes.Count >= 2)
e.Graphics.DrawLines(Pens.Black,
Nodes.Select(x => x.Location).ToArray());
foreach (var node in Nodes)
node.Draw(e.Graphics, node == hotNode);
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
if (mouseHoverTimer != null)
{
mouseHoverTimer.Enabled = false;
mouseHoverTimer.Dispose();
}
if (toolTip != null)
{
toolTip.Dispose();
}
}
base.Dispose(disposing);
}
}
Here is the node class:
using System.Drawing.Drawing2D;
public class Node
{
int NodeWidth = 16;
Color NodeColor = Color.Blue;
Color HotColor = Color.Red;
public string Name { get; set; }
public Point Location { get; set; }
private GraphicsPath GetShape()
{
GraphicsPath shape = new GraphicsPath();
shape.AddEllipse(Location.X - NodeWidth / 2, Location.Y - NodeWidth / 2,
NodeWidth, NodeWidth);
return shape;
}
public void Draw(Graphics g, bool isHot = false)
{
using (var brush = new SolidBrush(isHot ? HotColor : NodeColor))
using (var shape = GetShape())
{
g.FillPath(brush, shape);
}
}
public bool HitTest(Point p)
{
using (var shape = GetShape())
return shape.IsVisible(p);
}
}
And here is the example form, which has a drawing surface control on it:
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
drawingSurface1.Nodes.Add(new Node() {
Name = "Node1", Location = new Point(100, 100) });
drawingSurface1.Nodes.Add(new Node() {
Name = "Node2", Location = new Point(150, 70) });
drawingSurface1.Nodes.Add(new Node() {
Name = "Node3", Location = new Point(170, 140) });
drawingSurface1.Nodes.Add(new Node() {
Name = "Node4", Location = new Point(200, 50) });
drawingSurface1.Nodes.Add(new Node() {
Name = "Node5", Location = new Point(90, 160) });
drawingSurface1.Invalidate();
}
I had to implement a way to navigate between different element that are stacked in xamarin.forms. In the code below, you can see that we already manage the ability to distinguish every single one of the child of the container. At the moment, what I have written works but I need some help figuring out if it’s the good way or if there is a better way or even, if I can improve what I’ve done.
Currently, when you swipe from left to right or whatever move, I just use a SimpleOnGestureListener.
Here are some samples of what we are currently achieving:
I declared my custom elements such as the container & the children and then I created the customRenderer for each of them in an android library (only working on android at the moment).
“MyContent” is simply a modified frame with a specific Draw method and an event for each move available (up, down, left, right).
Code of the container
using System;
using Xamarin.Forms;
namespace Elements
{
public class MyContainer : Layout<MyContent>
{
public MyContainer ()
{
}
protected override void LayoutChildren (double x, double y, double width, double height)
{
for (int i = 0; i < Children.Count ; i++) {
MyContent child = Children[i];
// skip invisible children
if(!child.IsVisible)
continue;
int soffset = Children.Count - i;
int eoffset = soffset - 1;
Rectangle startBoundingRegion = new Rectangle (12 * soffset + Padding.Left, 14 * soffset + Padding.Top, width - 24 * soffset, height - 8 * soffset);
Rectangle endBoundingRegion = new Rectangle (12 * eoffset + Padding.Left, 14 * eoffset + Padding.Top, width - 24 * eoffset, height - 8 * eoffset);
child.OriginalBounds = endBoundingRegion;
LayoutChildIntoBoundingRegion (child,startBoundingRegion);
child.LayoutTo(endBoundingRegion,100,Easing.Linear);
}
}
protected override SizeRequest OnSizeRequest (double widthConstraint, double heightConstraint)
{
var height = 0;
var minHeight = 0;
var width = 0;
var minWidth = 0;
for (int i = 0; i < Children.Count; i++) {
MyContent child = Children[i];
// skip invisible children
if(!child.IsVisible)
continue;
var childSizeRequest = child.GetSizeRequest (double.PositiveInfinity, height);
height = (int)Math.Max (height, childSizeRequest.Minimum.Height);
minHeight = (int)Math.Max (minHeight, childSizeRequest.Minimum.Height);
width += (int)childSizeRequest.Request.Width;
minWidth += (int)childSizeRequest.Minimum.Width;
}
return new SizeRequest (new Size (width, height), new Size (minWidth, minHeight));
}
}
}
Code of its renderer
{using ...}
[assembly: ExportRenderer (typeof(MyContainer), typeof(MyContainerRenderer))]
namespace MyApp
{
public class MyContainerRenderer : ViewRenderer<Layout<MyContent>,Android.Views.View>
{
private readonly MyContentGestureListener listener;
private readonly GestureDetector detector;
private bool isAnimating;
private MyContent currentMyContent;
public MyContainerRenderer ()
{
listener = new MyContentGestureListener();
detector = new GestureDetector(listener);
}
protected override void OnElementChanged (ElementChangedEventArgs<Layout<MyContent>> e)
{
base.OnElementChanged(e);
if (e.NewElement == null)
{
this.Touch -= HandleTouch;
listener.OnSwipeLeft -= HandleOnSwipeLeft;
listener.OnSwipeRight -= HandleOnSwipeRight;
listener.OnSwipeTop -= HandleOnSwipeTop;
listener.OnSwipeDown -= HandleOnSwipeDown;
listener.OnPanVertical -= HandleOnPanVertical;
listener.OnPanHorizontal -= HandleOnPanHorizontal;
}
if (e.OldElement == null)
{
this.Touch += HandleTouch;
listener.OnSwipeLeft += HandleOnSwipeLeft;
listener.OnSwipeRight += HandleOnSwipeRight;
listener.OnSwipeTop += HandleOnSwipeTop;
listener.OnSwipeDown += HandleOnSwipeDown;
listener.OnPanVertical += HandleOnPanVertical;
listener.OnPanHorizontal += HandleOnPanHorizontal;
}
}
void HandleTouch(object sender, TouchEventArgs e)
{
currentMyContent = ((MyContainer)this.Element).Children.Last();
if (!isAnimating) {
if (e.Event.Action == MotionEventActions.Down) {
currentMyContent.ScaleTo (1.01, 100, Easing.Linear);
} else if ( e.Event.Action == MotionEventActions.Up){
if (!isAnimating) {
isAnimating = true;
currentMyContent.ScaleTo (1, 100, Easing.Linear);
currentMyContent.LayoutTo (currentMyContent.OriginalBounds, 100, Easing.CubicIn);
isAnimating = false;
listener.ResetFlags ();
}
}
detector.OnTouchEvent (e.Event);
}
}
void HandleOnPanVertical(object sender, EventArgs distanceY)
{
Rectangle dest = new Rectangle (currentMyContent.Bounds.X,currentMyContent.Bounds.Y - ((EventArgs<float>)distanceY).Value,currentMyContent.Bounds.Width,currentMyContent.Bounds.Height);
currentMyContent.Layout(dest);
}
void HandleOnPanHorizontal(object sender, EventArgs distanceX)
{
Rectangle dest = new Rectangle (currentMyContent.Bounds.X - ((EventArgs<float>)distanceX).Value,currentMyContent.Bounds.Y,currentMyContent.Bounds.Width,currentMyContent.Bounds.Height);
currentMyContent.Layout (dest);
}
async void animateNext (MyContent mContent, Rectangle dest)
{
isAnimating = true;
await mContent.LayoutTo (dest, 150, Easing.Linear);
((MyContainer)this.Element).Children.Remove (mContent);
((MyContainer)this.Element).Children.Insert (0, mContent);
isAnimating = false;
}
void HandleOnSwipeLeft(object sender, EventArgs e)
{
currentMyContent.OnSwipeLeft();
Rectangle dest = new Rectangle (currentMyContent.Bounds.X,currentMyContent.Bounds.Y,currentMyContent.Bounds.Width,currentMyContent.Bounds.Height);
dest.Left -= Width;
animateNext (currentMyContent, dest);
}
void HandleOnSwipeRight(object sender, EventArgs e)
{
currentMyContent.OnSwipeRight();
Rectangle dest = new Rectangle (currentMyContent.Bounds.X,currentMyContent.Bounds.Y,currentMyContent.Bounds.Width,currentMyContent.Bounds.Height);
dest.Left += Width;
animateNext (currentMyContent, dest);
}
void HandleOnSwipeTop(object sender, EventArgs e)
{
Rectangle dest = new Rectangle (currentMyContent.Bounds.X, currentMyContent.Bounds.Y, currentMyContent.Bounds.Width, currentMyContent.Bounds.Height);
dest.Top -= Height;
animateNext (currentMyContent, dest);
}
void HandleOnSwipeDown(object sender, EventArgs e)
{
Rectangle dest = new Rectangle (currentMyContent.Bounds.X,currentMyContent.Bounds.Y,currentMyContent.Bounds.Width,currentMyContent.Bounds.Height);
dest.Top += Height;
animateNext (currentMyContent, dest);
}
}
class MyContentGestureListener : GestureDetector.SimpleOnGestureListener
{
private static int SWIPE_THRESHOLD = 50;
private static int SWIPE_VELOCITY_THRESHOLD = 20;
private bool isPanningV;
private bool isPanningH;
public event EventHandler OnSwipeDown;
public event EventHandler OnSwipeTop;
public event EventHandler OnSwipeLeft;
public event EventHandler OnSwipeRight;
public event EventHandler OnPanHorizontal;
public event EventHandler OnPanVertical;
public override bool OnScroll(MotionEvent ev1, MotionEvent ev2,float distanceX,float distanceY)
{
if (Math.Abs (distanceX) > Math.Abs (distanceY) && OnPanHorizontal != null && !isPanningV) {
isPanningH = true;
OnPanHorizontal (this,new EventArgs<float> (distanceX));
} else if (OnPanVertical != null && !isPanningH) {
isPanningV = true;
OnPanVertical (this, new EventArgs<float> (distanceY));
}
return base.OnScroll (ev1, ev2, distanceX, distanceY);
}
public override bool OnFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY)
{
float diffY = e2.GetY() - e1.GetY();
float diffX = e2.GetX() - e1.GetX();
if (Math.Abs(diffX) > Math.Abs(diffY) && Math.Abs(diffX) > SWIPE_THRESHOLD && Math.Abs(velocityX) > SWIPE_VELOCITY_THRESHOLD)
{
if (diffX > 0 && OnSwipeRight != null)
{
OnSwipeRight (this, null);
isPanningH = false;
}
else if (OnSwipeLeft != null) {
OnSwipeLeft (this, null);
isPanningH = false;
}
}
else if (Math.Abs(diffY) > SWIPE_THRESHOLD && Math.Abs(velocityY) > SWIPE_VELOCITY_THRESHOLD)
{
if (diffY > 0 && OnSwipeDown != null)
{
OnSwipeDown (this, null);
isPanningV = false;
}
else if (OnSwipeTop != null) {
OnSwipeTop (this, null);
isPanningV = false;
}
}
return base.OnFling (e1, e2, velocityX, velocityY);
}
public void ResetFlags(){
isPanningH = false;
isPanningV = false;
}
}
public class EventArgs<T> : EventArgs
{
public EventArgs(T value)
{
Value = value;
}
public T Value { get; private set; }
}
}
I want to create GUI application using .NET and I need to implement such kind of scrollable list with custom items. All items should grouped to 3 different groups and to include images, text and favourite star button as shown here:
I would also like to implement filtering using search box, but once I would be able to create such list with customizable items then I hope it would be much easier.
I tried using .NET list view, but I want each cell to look like 1 cell - with out any borders between the image, text, image.
I would extremely appreciate any thoughts about this manner!
The best way would be making control template for either WPF ListView or ListBox control.
For example, ListBox.ItemTemplate allows for custom item look.
If you would like to use 3rd party components, Better ListView allows this using owner drawing (requires subclassing BetterListView):
Here is a setup code for the control:
this.customListView = new CustomListView
{
Dock = DockStyle.Fill,
Parent = this
};
this.customListView.BeginUpdate();
for (int i = 0; i < 6; i++)
{
var item = new BetterListViewItem
{
Image = imageGraph,
Text = String.Format("Item no. {0}", i)
};
this.customListView.Items.Add(item);
}
var group1 = new BetterListViewGroup("First group");
var group2 = new BetterListViewGroup("Second group");
var group3 = new BetterListViewGroup("Third group");
this.customListView.Groups.AddRange(new[] { group1, group2, group3 });
this.customListView.Items[0].Group = group1;
this.customListView.Items[1].Group = group1;
this.customListView.Items[2].Group = group2;
this.customListView.Items[3].Group = group2;
this.customListView.Items[4].Group = group2;
this.customListView.Items[5].Group = group3;
this.customListView.AutoSizeItemsInDetailsView = true;
this.customListView.GroupHeaderBehavior = BetterListViewGroupHeaderBehavior.None;
this.customListView.ShowGroups = true;
this.customListView.LayoutItemsCurrent.ElementOuterPadding = new Size(0, 8);
this.customListView.EndUpdate();
and CustomListView class (implements custom drawing and interaction with star icons):
using BetterListView;
internal sealed class CustomListView : BetterListView
{
private const int IndexUndefined = -1;
private Image imageStarNormal;
private Image imageStarHighlight;
private int lastStarIndex = IndexUndefined;
private int currentStarIndex = IndexUndefined;
public CustomListView()
{
this.imageStarNormal = Image.FromFile("icon-star-normal.png");
this.imageStarHighlight = Image.FromFile("icon-star-highlight.png");
}
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
var item = HitTest(e.Location).ItemDisplay;
if (item == null)
{
return;
}
var bounds = GetItemBounds(item);
if (bounds == null)
{
return;
}
Rectangle boundsStar = GetStarBounds(bounds);
UpdateStarIndex(boundsStar.Contains(e.Location)
? item.Index
: IndexUndefined);
}
protected override void OnMouseLeave(EventArgs e)
{
base.OnMouseLeave(e);
UpdateStarIndex(IndexUndefined);
}
protected override void OnDrawGroup(BetterListViewDrawGroupEventArgs eventArgs)
{
eventArgs.DrawSeparator = false;
base.OnDrawGroup(eventArgs);
}
protected override void OnDrawItem(BetterListViewDrawItemEventArgs eventArgs)
{
base.OnDrawItem(eventArgs);
Graphics g = eventArgs.Graphics;
BetterListViewItemBounds bounds = eventArgs.ItemBounds;
int imageWidth = this.imageStarNormal.Width;
int imageHeight = this.imageStarNormal.Height;
g.DrawImage(
(this.currentStarIndex == eventArgs.Item.Index) ? this.imageStarHighlight : this.imageStarNormal,
GetStarBounds(bounds),
0, 0, imageWidth, imageHeight,
GraphicsUnit.Pixel);
Rectangle boundsSelection = bounds.BoundsSelection;
g.DrawRectangle(
Pens.Gray,
new Rectangle(boundsSelection.Left, boundsSelection.Top, boundsSelection.Width - 1, boundsSelection.Height - 1));
}
private void UpdateStarIndex(int starIndex)
{
if (starIndex == this.lastStarIndex)
{
return;
}
bool isUpdated = false;
if (this.lastStarIndex != IndexUndefined)
{
Items[this.lastStarIndex].Invalidate();
isUpdated = true;
}
if (starIndex != IndexUndefined)
{
Items[starIndex].Invalidate();
isUpdated = true;
}
this.lastStarIndex = this.currentStarIndex;
this.currentStarIndex = starIndex;
if (isUpdated)
{
RedrawItems();
}
}
private Rectangle GetStarBounds(BetterListViewItemBounds bounds)
{
Rectangle rectInner = bounds.BoundsInner;
int widthImage = this.imageStarNormal.Width;
int heightImage = this.imageStarNormal.Height;
return (new Rectangle(
rectInner.Width - widthImage,
rectInner.Top + ((rectInner.Height - heightImage) >> 1),
widthImage,
heightImage));
}
}
I have a multi column combo box.When i use it for the first time then it works fine then
when i clear it and again enter data into it,all the columns shrink and only 1-2 letters are visible from each row. But I want to display the text such as all the letters in the table are visible,like in the first image.
Code:
namespace MultiColumnComboBoxDemo
{
public class MultiColumnComboBox : ComboBox
{
public MultiColumnComboBox()
{
DrawMode = DrawMode.OwnerDrawVariable;
}
public new DrawMode DrawMode
{
get
{
return base.DrawMode;
}
set
{
if (value != DrawMode.OwnerDrawVariable)
{
throw new NotSupportedException("Needs to be DrawMode.OwnerDrawVariable");
}
base.DrawMode = value;
}
}
public new ComboBoxStyle DropDownStyle
{
get
{
return base.DropDownStyle;
}
set
{
if (value == ComboBoxStyle.Simple)
{
throw new NotSupportedException("ComboBoxStyle.Simple not supported");
}
base.DropDownStyle = value;
}
}
protected override void OnDataSourceChanged(EventArgs e)
{
base.OnDataSourceChanged(e);
InitializeColumns();
}
protected override void OnValueMemberChanged(EventArgs e)
{
base.OnValueMemberChanged(e);
InitializeValueMemberColumn();
}
protected override void OnDropDown(EventArgs e)
{
base.OnDropDown(e);
this.DropDownWidth = (int)CalculateTotalWidth();
}
const int columnPadding = 5;
private float[] columnWidths = new float[0];
private String[] columnNames = new String[0];
private int valueMemberColumnIndex = 0;
private void InitializeColumns()
{
PropertyDescriptorCollection propertyDescriptorCollection = DataManager.GetItemProperties();
columnWidths = new float[propertyDescriptorCollection.Count];
columnNames = new String[propertyDescriptorCollection.Count];
for (int colIndex = 0; colIndex < propertyDescriptorCollection.Count; colIndex++)
{
String name = propertyDescriptorCollection[colIndex].Name;
columnNames[colIndex] = name;
}
}
private void InitializeValueMemberColumn()
{
int colIndex = 0;
foreach (String columnName in columnNames)
{
if (String.Compare(columnName, ValueMember, true, CultureInfo.CurrentUICulture) == 0)
{
valueMemberColumnIndex = colIndex;
break;
}
colIndex++;
}
}
private float CalculateTotalWidth()
{
float totWidth = 0;
foreach (int width in columnWidths)
{
totWidth += (width + columnPadding);
}
return totWidth + SystemInformation.VerticalScrollBarWidth;
}
protected override void OnMeasureItem(MeasureItemEventArgs e)
{
base.OnMeasureItem(e);
if (DesignMode)
return;
for (int colIndex = 0; colIndex < columnNames.Length; colIndex++)
{
string item = Convert.ToString(FilterItemOnProperty(Items[e.Index], columnNames[colIndex]));
SizeF sizeF = e.Graphics.MeasureString(item, Font);
columnWidths[colIndex] = Math.Max(columnWidths[colIndex], sizeF.Width);
}
float totWidth = CalculateTotalWidth();
e.ItemWidth = (int)totWidth;
}
protected override void OnDrawItem(DrawItemEventArgs e)
{
base.OnDrawItem(e);
if (DesignMode)
return;
e.DrawBackground();
Rectangle boundsRect = e.Bounds;
int lastRight = 0;
//Shakir
//using (Pen linePen = new Pen(SystemColors.GrayText))
using (Pen linePen = new Pen(Color.Black))
{
using (SolidBrush brush = new SolidBrush(e.ForeColor))
//using (SolidBrush brush = new SolidBrush(BackColor))
{
if (columnNames.Length == 0 && e.Index >=0 )
{
e.Graphics.DrawString(Convert.ToString(Items[e.Index]), Font, brush, boundsRect);
}
else
{
for (int colIndex = 0; colIndex < columnNames.Length; colIndex++)
{
string item = Convert.ToString(FilterItemOnProperty(Items[e.Index], columnNames[colIndex]));
boundsRect.X = lastRight;
boundsRect.Width = (int)columnWidths[colIndex] + columnPadding;
lastRight = boundsRect.Right;
if (colIndex == valueMemberColumnIndex)
{
using (Font boldFont = new Font(Font, FontStyle.Bold))
{
e.Graphics.DrawString(item, boldFont, brush, boundsRect);
}
}
else
{
e.Graphics.DrawString(item, Font, brush, boundsRect);
}
if (colIndex < columnNames.Length - 1)
{
e.Graphics.DrawLine(linePen, boundsRect.Right, boundsRect.Top, boundsRect.Right, boundsRect.Bottom);
}
}
}
}
}
e.DrawFocusRectangle();
}
}
}
This is the code which fills data into the multicolumn combo box:
SupplierDisplay is a DataTable
SupplierMaster is the Table in the database
DBRdRw is class
SupplierDisplay = DbRdRw.SqlDbRead("Select SupplierID, SupplierName From SupplierMaster", "SupplierMaster");//data filled to a datatable
//data loading starts
mcbxSupplier.DataSource = SupplierDisplay;
mcbxSupplier.ValueMember = "SupplierName";
mcbxSupplier.DisplayMember = "SupplierName";//ends
add an empty table when u reload the multicolumn combo box just before the SQL statement
here is the full code
DataTable dummytable = new DataTable;
mcbxSupplier.DataSource = dummytable;
SupplierDisplay = DbRdRw.SqlDbRead("Select SupplierID, SupplierName From SupplierMaster", "SupplierMaster");//data filled to a datatable
//data loading starts
mcbxSupplier.DataSource = SupplierDisplay;
mcbxSupplier.ValueMember = "SupplierName";
mcbxSupplier.DisplayMember = "SupplierName";//ends